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