From 7070bc16080f082e3ae1c2c260382f6f96702b02 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 8 Oct 2026 12:10:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(gen):=20=E6=A0=88=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E9=80=83=E9=80=B8=E6=A3=80=E6=9F=A5=EF=BC=88LifetimeError?= =?UTF-8?q?=EF=BC=89=E3=80=81=E6=8C=89=E5=80=BC=E8=BF=94=E5=9B=9E=E8=81=9A?= =?UTF-8?q?=E5=90=88=E3=80=81=E6=A0=87=E9=87=8F=E2=86=92=E8=81=9A=E5=90=88?= =?UTF-8?q?=20fail-fast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GenApi.fail_if_stack_addr_escape:禁止把本帧栈地址写出帧外(显式 &local,以及局部数组名退让到指针目标,如 return arr with '-> char*');裸聚合名 + 同型聚合目标是值拷贝,不在其列;err_type 记为 LifetimeError。 - StmtGenCifMixin.translate_return:按值返回聚合时,对被指类型与返回聚合类型同一的指针补 load(与多返回值 struct 的 ret 同形),修复 return 局部数组报 CastError。 - ExprGenBase.coerce_to_type:聚合目标不接受标量(旧行为落成 store i32 0, ptr,只写首元素字节并抬高对齐),改为编译期 fail-fast。 --- App/lib/core/Gen/AssignGenMixin.vp | 15 +++++++++- App/lib/core/Gen/Expr/ExprGenBase.vp | 9 ++++++ App/lib/core/Gen/GenApi.vp | 36 ++++++++++++++++++++++++ App/lib/core/Gen/Stmt/StmtGenCifMixin.vp | 25 +++++++++++++++- 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 7d5ac41..4a17a11 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -1461,6 +1461,12 @@ novirtual class AssignGenMixin: # global variable: write to the module-scope global if GenApi.is_global_name(trans, nm.id) != 0: mod_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) + # 栈地址逃逸:`global G; G: T = &local`,或 `G: T* = arr`(数组退让)——与普通 + # 赋值同口径,禁止把本帧栈地址写出;目标为同型聚合时按值拷贝,不在此列。 + if aa.value is not None: + tgt_aa: mlirlite.LLVMType* = None + if mod_alloca is not None and mod_alloca.Ty is not None: tgt_aa = mod_alloca.Ty.Pointee + GenApi.fail_if_stack_addr_escape(aa.value, "a module-level global variable", trans, tgt_aa) if mod_alloca is not None and aa.value is not None: # P1-A context width propagation: pass the target type so integer literals materialize at target width aa_glob_ty: mlirlite.LLVMType* = None @@ -1841,7 +1847,14 @@ novirtual class AssignGenMixin: elif tk_f == ast.ASTKind.Subscript: GenApi.fail_if_fstring_escape(asgn.value, "element assignment") elif tk_f == ast.ASTKind.Name: nm_f: ast.Name* = ast.Name*(tgt_f) - if nm_f.id is not None and GenApi.is_global_name(trans, nm_f.id) != 0: GenApi.fail_if_fstring_escape(asgn.value, "global variable assignment") + if nm_f.id is not None and GenApi.is_global_name(trans, nm_f.id) != 0: + GenApi.fail_if_fstring_escape(asgn.value, "global variable assignment") + # 栈地址逃逸:`G = &local` 与 `G = arr`(G 为指针时的数组退让)都把本帧栈地址 + # 写进模块级全局,函数返回后悬垂;`G = arr` 且 G 为同型聚合时是值拷贝,不在此列。 + gv_sa: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm_f.id) + tgt_sa: mlirlite.LLVMType* = None + if gv_sa is not None and gv_sa.Ty is not None: tgt_sa = gv_sa.Ty.Pointee + GenApi.fail_if_stack_addr_escape(asgn.value, "a module-level global variable", trans, tgt_sa) # Translate the RHS; clear CurAssignAnnot/CurAssignNode (no target type to infer here) trans.CurAssignAnnot = None diff --git a/App/lib/core/Gen/Expr/ExprGenBase.vp b/App/lib/core/Gen/Expr/ExprGenBase.vp index 17556e1..d1e2bff 100644 --- a/App/lib/core/Gen/Expr/ExprGenBase.vp +++ b/App/lib/core/Gen/Expr/ExprGenBase.vp @@ -480,6 +480,15 @@ novirtual class ExprGenBase: if self.Gen.StmtGenDecl._is_void_type(target_ty) == 0: self.Gen.TypeGenErr.fatal_error(self._value_node_of(node), "a void value cannot take part in any operation (void is only allowed for void* and typedef aliases)", "TypeError") return val + # 聚合目标(结构体/数组)不接受标量(整数/浮点)初始化或赋值:标量→聚合在 + # TPV 无转换语义;旧行为把标量原样交给 build_store,而 build_store 不做类型 + # 校验,落成 `store i32 0, ptr %x`——只写聚合首元素字节并抬高对齐,其余部分 + # 保持未初始化(历史现场:`l: [10]char = 0` → 打印 "Hello"+乱码)。 + # 聚合零初始化必须写 `= None`(上方 IsNoneValue → _NoneValueAsTarget 分支)。 + tg_kind_agg: int = target_ty.kind() + if (tg_kind_agg == mlirlite.LLVMType.Struct or tg_kind_agg == mlirlite.LLVMType.Array) and self.is_ptr_type(val.Ty) == 0 and (self.get_llvm_type_bits(val.Ty) != 0 or self.get_llvm_float_bits(val.Ty) != 0): + self.Gen.TypeGenErr.fatal_error(self._value_node_of(node), "a scalar (integer/float) cannot initialize or assign to an aggregate (struct/array): only the first element's bytes would be written (and the alignment raised), leaving the rest of the aggregate uninitialized; for zero-initialization write `= None`, for contents use a list literal `[...]` or assign element by element", "TypeError") + return val # func -> ptr: bitcast (function Values have Func type, handled before # the integer/float conversions). match val.Ty: diff --git a/App/lib/core/Gen/GenApi.vp b/App/lib/core/Gen/GenApi.vp index 4affb59..e868946 100644 --- a/App/lib/core/Gen/GenApi.vp +++ b/App/lib/core/Gen/GenApi.vp @@ -457,6 +457,42 @@ def escape_fstring_copy(src: str) -> str: return out +# Stack-escape rule: the address of a stack object must not outlive the current frame. +# Two forms hand out this frame's stack address: +# (a) `&name` — the explicit address of a local variable / parameter; +# (b) `name` — a bare local ARRAY used where a pointer is expected (array decay, +# e.g. `def f() -> char*: ...; return arr`). +# A bare name with a matching aggregate target is a value copy, never an escape. +def fail_if_stack_addr_escape(node: ast.AST*, where: str, trans: HT.Translator*, target_ty: mlirlite.LLVMType* = None) -> int: + """Fail fast when the address of a stack object escapes the current frame.""" + if node is None or trans is None or _g_gen is None: return 0 + nm_sa: str = None + if node.kind() == ast.ASTKind.UnaryOp: + uo_sa: ast.UnaryOp* = ast.UnaryOp*(node) + if uo_sa.op == ast.OpKind.AddrOf and uo_sa.operand is not None and uo_sa.operand.kind() == ast.ASTKind.Name: + nm0_sa: ast.Name* = ast.Name*(uo_sa.operand) + if nm0_sa.id is not None: nm_sa = nm0_sa.id + elif node.kind() == ast.ASTKind.Name and target_ty is not None: + # 数组退让(array decay):本帧局部数组名出现在指针目标位置 ⇒ 交出首元素地址(栈地址) + if _g_gen.ExprGenBase.is_ptr_type(target_ty) != 0: + nm1_sa: ast.Name* = ast.Name*(node) + if nm1_sa.id is not None: + v1_sa: mlirlite.Value* = _g_gen.VarGen.lookup_var(trans.SymTab, nm1_sa.id) + if v1_sa is not None and v1_sa.Ty is not None and _g_gen.ExprGenBase.is_ptr_type(v1_sa.Ty) != 0: + inner1_sa: mlirlite.LLVMType* = v1_sa.Ty.Pointee + if inner1_sa is not None and inner1_sa.kind() == mlirlite.LLVMType.Array: nm_sa = nm1_sa.id + if nm_sa is None: return 0 + # Nonlocal captures are heap-promoted and may outlive the frame. + if is_nonlocal_name(trans, nm_sa) != 0: return 0 + # Only a local alloca of this frame is stack storage; module globals / function names + # do not resolve in the local scope and are allowed to escape. + if _g_gen.VarGen.lookup_var(trans.SymTab, nm_sa) is None: return 0 + _g_gen.TypeGenErr.fatal_error(node, f"stack address escapes: `{nm_sa}` refers to storage of the current frame (local variable/array or parameter); using it at {where} hands out a stack address, which becomes invalid once the function returns (dangling pointer). Return the value itself (a value copy) or allocate on the heap (`new heap hlife ...`).", "LifetimeError") + return 0 + + + + # ---------------- create_wrapper_main (delegates to TranslatorGenMixin.create_wrapper_main) ---------------- def create_wrapper_main(trans: HT.Translator*, tree: ast.AST*, i32_ty: mlirlite.LLVMType*) -> int: """Delegate to gen.create_wrapper_main""" diff --git a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp index d415dc8..b68c0ff 100644 --- a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp @@ -433,7 +433,12 @@ novirtual class StmtGenCifMixin: if rt.value is not None: # 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") + GenApi.fail_if_fstring_escape(rt.value, "function return value") + # 栈地址逃逸:`return &local`(显式取址)与 `return arr`(返回类型是指针时的数组退让, + # 如 `-> char*`)都交出本帧栈地址;`return arr` 且返回类型为同型聚合时按值拷贝,不在此列。 + rv_rt_sa: mlirlite.LLVMType* = None + if trans._cur_func is not None: rv_rt_sa = mlirlite.function_get_ret_ty(trans._cur_func) + GenApi.fail_if_stack_addr_escape(rt.value, "the function return value", trans, rv_rt_sa) # 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) @@ -570,6 +575,24 @@ novirtual class StmtGenCifMixin: ret_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(ret_ty_rt) val_is_ptr: int = self.Gen.ExprGenBase.is_ptr_type(val.Ty) ret_is_ptr: int = self.Gen.ExprGenBase.is_ptr_type(ret_ty_rt) + # 按值返回聚合(数组/结构体):聚合变量在 TPV 中以 alloca 指针表示,返回类型是 + # 聚合值时须 load 出整个聚合(与上方多返回值 struct 的 `build_load(... rt_struct_ty ...)` + # → ret 同形)。仅当被指类型与返回类型是同一聚合时成立;其余 Foo*→Foo 仍由 + # coerce_to_type fail-fast,不放开隐式解引用。 + if val_is_ptr != 0 and ret_is_ptr == 0 and ret_ty_rt is not None: + ret_kind_rv: int = ret_ty_rt.kind() + if ret_kind_rv == mlirlite.LLVMType.Struct or ret_kind_rv == mlirlite.LLVMType.Array: + v_inner_rv: mlirlite.LLVMType* = None + match val.Ty: + case mlirlite.LLVMType.Ptr(pi_rv): + v_inner_rv = pi_rv + case _: + pass + if v_inner_rv is not None and self.Gen.ExprGenBase._same_agg_ty(pool, v_inner_rv, ret_ty_rt) != 0: + loaded_rv: mlirlite.Value* = mlirlite.build_load(builder, ret_ty_rt, val) + if loaded_rv is not None: + val = loaded_rv + val_is_ptr = 0 # ptr mismatch: val is an integer but ret is a ptr (or vice versa), must convert if val_is_ptr != ret_is_ptr: # [FIX 2026-08-30] __new__ returns a class pointer (T*): during pending Phase A the