From 79d1c869828114c3aa308d20ada6ef432fd44140 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 10 Oct 2026 09:16:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(gen):=20return=20=E6=9C=AC=E5=B8=A7?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E6=94=B9=E4=B8=BA=E5=A3=B0=E6=98=8E=E6=84=9F?= =?UTF-8?q?=E7=9F=A5=20fail-fast=EF=BC=88A2=20=E6=96=B9=E6=A1=88=20A?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A2 原本整体移除定义端 return <本帧地址> 的 fail-fast,导致无 @life(ret(...)) 声明的函数返回本帧存储时静默通过(调用点只拿到 LIFE_UNKNOWN,悬垂一路无声)。 现改为按被调者自己的 ret 声明判定: - ret(frame) / ret(borrows=<该形参>) → 放行; - ret(heap|static|borrows=none) → 报错(声明与返回值矛盾); - 无声明 → 报错(提示补 @life(ret(frame)) 或 ret(borrows=))。 落点:AssignGenMixin.check_return_frame_decl(判据复用 life_of_expr,target_ty 传 返回类型,与移除前的口径一致);ret_kind 由 StructGenMixin.get_ret_kind_by_func 反查 EscFuncTable → EscDirTable(顶层函数/方法/嵌套 def 一次覆盖,无需猜类名), 当前 FunctionDef 取 StructGenMixin.NllCurFunc。 新检查是旧检查的严格子集 ⇒ 自举无回归。 --- App/lib/core/Gen/AssignGenMixin.vp | 49 ++++++++++++++++++++++++ App/lib/core/Gen/GenApi.vp | 6 +++ App/lib/core/Gen/Stmt/StmtGenCifMixin.vp | 11 +++--- App/lib/core/Gen/StructGenMixin.vp | 19 +++++++++ 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 007844e..7591632 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -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=))` (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=))` (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。 diff --git a/App/lib/core/Gen/GenApi.vp b/App/lib/core/Gen/GenApi.vp index f83bf8b..dddd7be 100644 --- a/App/lib/core/Gen/GenApi.vp +++ b/App/lib/core/Gen/GenApi.vp @@ -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) diff --git a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp index bfa0a7a..a7b909a 100644 --- a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp @@ -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) diff --git a/App/lib/core/Gen/StructGenMixin.vp b/App/lib/core/Gen/StructGenMixin.vp index 0d99be0..2a2a9df 100644 --- a/App/lib/core/Gen/StructGenMixin.vp +++ b/App/lib/core/Gen/StructGenMixin.vp @@ -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