fix(gen): return 本帧存储改为声明感知 fail-fast(A2 方案 A)

A2 原本整体移除定义端 return <本帧地址> 的 fail-fast,导致无 @life(ret(...))
声明的函数返回本帧存储时静默通过(调用点只拿到 LIFE_UNKNOWN,悬垂一路无声)。
现改为按被调者自己的 ret 声明判定:
- ret(frame) / ret(borrows=<该形参>) → 放行;
- ret(heap|static|borrows=none) → 报错(声明与返回值矛盾);
- 无声明 → 报错(提示补 @life(ret(frame)) 或 ret(borrows=<param>))。
落点:AssignGenMixin.check_return_frame_decl(判据复用 life_of_expr,target_ty 传
返回类型,与移除前的口径一致);ret_kind 由 StructGenMixin.get_ret_kind_by_func
反查 EscFuncTable → EscDirTable(顶层函数/方法/嵌套 def 一次覆盖,无需猜类名),
当前 FunctionDef 取 StructGenMixin.NllCurFunc。
新检查是旧检查的严格子集 ⇒ 自举无回归。
This commit is contained in:
TermiNexus committed 2026-10-10 09:16:26 +08:00
1 parent 6da8e2e095
commit 79d1c86982
4 files changed
+80 -5

No files matched your search

+49
View File
@@ -844,6 +844,55 @@ novirtual class AssignGenMixin:
leak_l = self.Gen.StructGen.escape_leak_line(leak_func, leak_param)
if leak_l > 0: leak_f = self.Gen.TypeGenResolve.get_current_file()
self.Gen.TypeGenErr.fatal_error(node, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local; using it at {where} hands out a dangling pointer once this frame returns. Copy the value, or allocate on the heap (`new heap hlife ...`).", "LifetimeError", 0, 0, leak_f, leak_l)
return 0
# check_return_frame_decl - **定义端** `return <本帧存储>` 的**声明感知**检查(A2 / §14.3 第 6 条)。
# A2 移除了"一律拦"的旧判据:定义端分不清"交出本帧"与"借出调用方的存储"(后者是
# `return <借用形参>`,一律拦会误报)。但也不能一律放 —— 无声明的函数返回本帧存储会让调用点
# 永远只拿到"未知"(悬垂指针一路静默)。判据收敛到被调者自己的 `@life(ret(...))`:
# · `ret(frame)` → 放行(去向由调用点负责);
# · `ret(borrows=<该形参>)` → 放行(借出的正是那个形参);
# · 无声明 / `ret(heap|static|borrows=none)` → `LifetimeError`(提示补声明 / 改声明)。
# 拿不到当前函数定义(非三条函数体翻译路径)时**不判**:宁可少报,也不误报。
def check_return_frame_decl(self, trans: HT.Translator*, value: ast.AST*) -> int:
"""返回值是本帧存储且当前函数的 ret 声明不允许 → LifetimeError;0=放行"""
if trans is None or value is None: return 0
fd: ast.AST* = self.Gen.StructGen.NllCurFunc
if fd is None: return 0
rv_ty: mlirlite.LLVMType* = None
if trans._cur_func is not None: rv_ty = mlirlite.function_get_ret_ty(trans._cur_func)
# target_ty 必须传返回类型:裸本帧数组名在**指针**返回类型下是数组退让(交出栈地址),
# 在同型聚合返回类型下是按值拷贝(LIFE_VALUE,不在此列)。
if self.life_of_expr(trans, value, rv_ty) != GenTypes.LIFE_FRAME: return 0
rk: int = GenApi.get_ret_kind_by_func(fd)
if rk == GenApi.RET_KIND_FRAME: return 0
is_borrow_kind: int = 0
pi: int = -1
if rk >= GenApi.RET_KIND_BORROW_BASE:
is_borrow_kind = 1
pi = rk - GenApi.RET_KIND_BORROW_BASE
# 只有"返回的正是被声明的那个形参"才算借出;返回别的本帧存储仍是悬垂
if self.Gen.StructGen._ParamIndex(ast.FunctionDef*(fd), value) == pi: return 0
# 报文里给出名字(&x / x),便于定位
nm_txt: str = "expression"
nm_pre: str = ""
if value.kind() == ast.ASTKind.Name:
n2: ast.Name* = ast.Name*(value)
if n2.id is not None: nm_txt = n2.id
elif value.kind() == ast.ASTKind.UnaryOp:
u2: ast.UnaryOp* = ast.UnaryOp*(value)
if u2.operand is not None and u2.operand.kind() == ast.ASTKind.Name:
n3: ast.Name* = ast.Name*(u2.operand)
if n3.id is not None:
nm_txt = n3.id
nm_pre = "&"
if rk == 0:
self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local and this function has no `@life(ret(...))` declaration, so callers cannot know the result's lifetime. Declare `@life(ret(frame))` (the result may point into this frame; callers must keep it in-frame) or `@life(ret(borrows=<param>))` (the result borrows a parameter), or return a value copy / a heap allocation (`new heap hlife ...`).", "LifetimeError")
return 0
if is_borrow_kind != 0:
self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local but this function declares `@life(ret(borrows=<param>))` (parameter #{pi}), which only permits returning that parameter. Return that parameter, or declare `@life(ret(frame))`.", "LifetimeError")
return 0
self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local but this function is declared as non-frame (`@life(ret(heap))` / `ret(static)` / `ret(borrows=none)`); the declaration contradicts the returned value. Fix the declaration, or return a non-frame value.", "LifetimeError")
return 0
# _SlifeDropOwned - 该槽已被登记为 slife 绑定时交还所有权:释放它当前拥有的对象、影子清 0。
+6
View File
@@ -574,6 +574,12 @@ def get_ret_kind_method(pool: memhub.MemBuddy*, class_name: str, method_name: st
return 0
def get_ret_kind_by_func(fd: ast.AST*) -> int:
"""**定义端**按 FunctionDef* 取本函数的 `@life(ret(...))` 契约(顶层/方法/嵌套都覆盖);0 = 无声明"""
if _g_gen is not None: return _g_gen.StructGen.get_ret_kind_by_func(fd)
return 0
def lookup_ret_kind_via_vpi(cls_sha1: str, class_name: str, func_name: str) -> int:
"""跨模块给被调者的 `@life(ret(...))` 契约;0 = 无声明(含该模块未加载)"""
if _g_gen is not None: return _g_gen.CallGenBase.lookup_ret_kind_via_vpi(cls_sha1, class_name, func_name)
+6 -5
View File
@@ -434,11 +434,12 @@ novirtual class StmtGenCifMixin:
# f-string escape (return f"..."): its buffer lives on this frame's stack and dies on
# return; copy it out explicitly (GenApi.escape_fstring_copy) before returning it.
GenApi.fail_if_fstring_escape(rt.value, "function return value")
# A2(§14.3 第 6 条):此处**不再**对 `return &local` / `return arr`(指针位置的数组退让)
# 直接 fail-fast。返回值去向改由**调用点**按被调者的 `@life(ret(...))` 契约判定(见
# AssignGenMixin.life_of_expr 的 ret_kind 分支):定义端无法区分"交出本帧"与"借出调用方的
# 存储",一律拦会误报 `return <借用形参>`。必须在 `ret_kind` 契约(同批)落地后移除,
# 否则未声明的函数会静默漏检。
# A2(§14.3 第 6 条,方案 A):定义端对 `return <本帧存储>` 的**声明感知** fail-fast。
# A2 移除了"一律拦"的旧判据(定义端分不清"交出本帧"与"借出调用方的存储"),
# 但也不能一律放:判据收敛到被调者自己的 `@life(ret(...))` —— `ret(frame)` 或
# `ret(borrows=<该形参>)` 放行;无声明 / 声明为非帧 → LifetimeError。
# 实现见 AssignGenMixin.check_return_frame_decl。
self.Gen.AssignGen.check_return_frame_decl(trans, rt.value)
# return a, b → pack an anonymous struct value (multi-return, return type tuple[T1,T2])
if rt.value.kind() == ast.ASTKind.Tuple:
rtp: ast.Tuple* = ast.Tuple*(rt.value)
+19
View File
@@ -573,6 +573,25 @@ novirtual class StructGenMixin:
if idx < 0: return 0
return int(self.EscDirTable[idx * 4 + 3])
# get_ret_kind_by_func - **定义端**(`return` 处)用:按 FunctionDef* 反查本函数的 ret_kind。
# 不靠"类名拼接":EscFuncTable 里已有"名哈希 → FunctionDef*",反查拿到哈希后再去
# EscDirTable 取 ret_kind 列 —— 顶层函数 / 方法 / 嵌套 def 三种键形式一次性覆盖,
# 无需在翻译期猜测 Class 名。0 = 无声明(或该函数未被登记)。
def get_ret_kind_by_func(self, fd: ast.AST*) -> int:
"""当前函数体的 `@life(ret(...))` 编码;0 = 无声明"""
if fd is None or self.EscFuncTable is None: return 0
i: int = 0
while i < self.EscFuncCount:
if sizet(self.EscFuncTable[i * 2 + 1]) == sizet(fd):
h: sizet = self.EscFuncTable[i * 2]
j: int = 0
while j < self.EscDirCount:
if self.EscDirTable[j * 4] == h: return int(self.EscDirTable[j * 4 + 3])
j += 1
return 0
i += 1
return 0
# get_ret_kind_method - 方法版 `@life(ret(...))`:查 "Class.method" 并沿 MRO 回退。
def get_ret_kind_method(self, pool: memhub.MemBuddy*, class_name: str, method_name: str) -> int:
if method_name is None or class_name is None: return 0