diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 4a17a11..56f7f07 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -583,6 +583,97 @@ novirtual class AssignGenMixin: if buf_a[0] == 0 or buf_b[0] == 0: return 0 return 1 if string.strcmp(buf_a, buf_b) == 0 else 0 + # ============================================================ + # 生命期(LifeTag)判定 / 传播 / 汇点检查(架构见 Debugger/ownership-and-new.md §11) + # 标签常量见 GenTypes define LIFE_*;有序,越小越"短命",汇合取 min。 + # ============================================================ + + # life_of_expr - 求表达式当前值的生命期标签。 + # target_ty 参与判定:裸本帧数组名在**指针**目标位置 = 数组退让(交出本帧栈地址), + # 在同型聚合目标位置 = 按值拷贝(LIFE_VALUE)。 + def life_of_expr(self, trans: HT.Translator*, node: ast.AST*, target_ty: mlirlite.LLVMType*) -> int: + """表达式的 LifeTag;无法判定时 LIFE_UNKNOWN(保守放行)""" + if node is None or trans is None: return GenTypes.LIFE_VALUE + k: int = node.kind() + if k == ast.ASTKind.Constant: + cn: ast.Constant* = ast.Constant*(node) + if cn is None: return GenTypes.LIFE_VALUE + if cn.const_kind == ast.CONST_STR: return GenTypes.LIFE_STATIC + if cn.const_kind == ast.CONST_NONE: return GenTypes.LIFE_UNKNOWN + return GenTypes.LIFE_VALUE + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(node) + if nm.id is None: return GenTypes.LIFE_UNKNOWN + if GenApi.is_global_name(trans, nm.id) != 0: return GenTypes.LIFE_STATIC + if GenApi.is_nonlocal_name(trans, nm.id) != 0: return GenTypes.LIFE_UNKNOWN + v: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) + if v is None: return GenTypes.LIFE_STATIC + if v.Ty is not None and self.Gen.ExprGenBase.is_ptr_type(v.Ty) != 0: + inner: mlirlite.LLVMType* = v.Ty.Pointee + if inner is not None and inner.kind() == mlirlite.LLVMType.Array: + # 本帧局部数组名:退让到指针目标交出栈地址;同型聚合目标是值拷贝 + if target_ty is not None and self.Gen.ExprGenBase.is_ptr_type(target_ty) != 0: return GenTypes.LIFE_FRAME + return GenTypes.LIFE_VALUE + # 指针/标量变量:读登记的生命期(默认 UNKNOWN) + return self.Gen.VarGen.get_var_life(trans.SymTab, nm.id) + if k == ast.ASTKind.UnaryOp: + uo: ast.UnaryOp* = ast.UnaryOp*(node) + if uo.op == ast.OpKind.AddrOf and uo.operand is not None and uo.operand.kind() == ast.ASTKind.Name: + onm: ast.Name* = ast.Name*(uo.operand) + if onm.id is not None: + if GenApi.is_global_name(trans, onm.id) != 0: return GenTypes.LIFE_STATIC + if GenApi.is_nonlocal_name(trans, onm.id) != 0: return GenTypes.LIFE_UNKNOWN + if self.Gen.VarGen.lookup_var(trans.SymTab, onm.id) is not None: return GenTypes.LIFE_FRAME + return GenTypes.LIFE_STATIC + return GenTypes.LIFE_UNKNOWN + if k == ast.ASTKind.Call: + # 分配类调用 → 堆;其余调用 → 未知(函数摘要落地前保守放行) + cl: ast.Call* = ast.Call*(node) + if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(cl.func) + if at.attr is not None: + if (string.strcmp(at.attr, "alloc") == 0 or string.strcmp(at.attr, "alloc_buf") == 0 or string.strcmp(at.attr, "calloc") == 0 or string.strcmp(at.attr, "realloc") == 0): return GenTypes.LIFE_HEAP + if cl.func is not None and cl.func.kind() == ast.ASTKind.Name: + fnm: ast.Name* = ast.Name*(cl.func) + if fnm.id is not None and string.strcmp(fnm.id, "malloc") == 0: return GenTypes.LIFE_HEAP + return GenTypes.LIFE_UNKNOWN + return GenTypes.LIFE_UNKNOWN + + # propagate_life - 把 RHS 的生命期写到赋值目标(仅 Name 目标;全局目标走汇点检查)。 + def propagate_life(self, trans: HT.Translator*, target_node: ast.AST*, rhs_node: ast.AST*, target_ty: mlirlite.LLVMType*) -> int: + """Propagate the RHS life tag onto a Name target; 0=done / 1=skipped""" + if trans is None or target_node is None: return 1 + if target_node.kind() != ast.ASTKind.Name: return 1 + tnm: ast.Name* = ast.Name*(target_node) + if tnm.id is None: return 1 + if GenApi.is_global_name(trans, tnm.id) != 0: return 1 + if GenApi.is_nonlocal_name(trans, tnm.id) != 0: return 1 + life: int = self.life_of_expr(trans, rhs_node, target_ty) + self.Gen.VarGen.set_var_life(trans.SymTab, tnm.id, life) + return 0 + + # fail_if_life_escapes - 汇点检查:被写出表达式的生命期为 LIFE_FRAME → LifetimeError。 + # 这是 GenApi.fail_if_stack_addr_escape 的唯一实现(语法判据已并入 life_of_expr)。 + def fail_if_life_escapes(self, node: ast.AST*, where: str, trans: HT.Translator*, target_ty: mlirlite.LLVMType*) -> int: + """Fail fast (LifetimeError) when the written-out value is frame-lifetime storage""" + if node is None or trans is None: return 0 + if self.life_of_expr(trans, node, target_ty) != GenTypes.LIFE_FRAME: return 0 + # 报文里给出名字(&x / x),便于定位;前缀单独存放,避免 str 相加(str 是 i8*,无 '+' 语义) + nm_txt: str = "expression" + nm_pre: str = "" + if node.kind() == ast.ASTKind.Name: + n2: ast.Name* = ast.Name*(node) + if n2.id is not None: nm_txt = n2.id + elif node.kind() == ast.ASTKind.UnaryOp: + u2: ast.UnaryOp* = ast.UnaryOp*(node) + 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 = "&" + self.Gen.TypeGenErr.fatal_error(node, f"stack address escapes: `{nm_pre}{nm_txt}` 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 + def _is_addrlike_call(self, node: ast.AST*) -> int: """Return 1 if the rhs is an explicit pointer expression (c.Addr/c.Deref/c.Cast/c.Ptr call, or new-syntax `&x` / `*p`).""" if node is None: return 0 @@ -1787,6 +1878,12 @@ novirtual class AssignGenMixin: # FakeDuck: auto-inject requires fields (e.g. __mbuddy__) for the with context self.Gen.CallGenFd._inject_requires_fields(trans, pool, builder, mod, fd_meta_alloca, "_str") + # 生命期传播(本地变量):`s: str = l` → Life(s) = Life(l)(数组退让即借用) + if aa.value is not None and nm.id is not None and alloca is not None: + tgt_life_ty: mlirlite.LLVMType* = None + if alloca.Ty is not None: tgt_life_ty = alloca.Ty.Pointee + self.propagate_life(trans, target, aa.value, tgt_life_ty) + return 0 # translate_assign - handle an Assign statement; returns the number of new variables @@ -1919,6 +2016,11 @@ novirtual class AssignGenMixin: else: VLogger.error("assignment rhs is None (asgn.value is empty)", "ASGN") return 0 + # 生命期传播(单 Name 目标):`s = l` → Life(s) = Life(l) + if len(targets) == 1: + t0_sl: ast.AST* = targets.get(0) + self.propagate_life(trans, t0_sl, asgn.value, asg_target_ty) + new_vars: int = 0 tn: sizet = len(targets) for ti in range(tn): diff --git a/App/lib/core/Gen/GenApi.vp b/App/lib/core/Gen/GenApi.vp index e868946..a0910cc 100644 --- a/App/lib/core/Gen/GenApi.vp +++ b/App/lib/core/Gen/GenApi.vp @@ -458,37 +458,16 @@ def escape_fstring_copy(src: str) -> str: # 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`). +# The judgement is now the unified **life-tag** mechanism (GenTypes define LIFE_*); the +# implementation lives in AssignGen.fail_if_life_escapes. It covers: +# (a) `&name` — explicit address of a local variable / parameter; +# (b) `name` — a bare local ARRAY used where a pointer is expected (array decay); +# (c) propagated borrows — `s: str = l` gives Life(s)=FRAME, so `return s` is caught. # 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.""" + """Fail fast (LifetimeError) when the address of a stack object escapes the 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 + return _g_gen.AssignGen.fail_if_life_escapes(node, where, trans, target_ty) diff --git a/App/lib/core/Gen/GenTypes.vp b/App/lib/core/Gen/GenTypes.vp index 4239717..ffd6a25 100644 --- a/App/lib/core/Gen/GenTypes.vp +++ b/App/lib/core/Gen/GenTypes.vp @@ -48,6 +48,15 @@ class TraceFrame: col_end: int +# ---- 生命期标签(LifeTag):有序,越小越"短命" ---- +# 逃逸检查只在 LIFE_FRAME 上触发;控制流汇合取更短者(Meet = min)。 +# 详见 Debugger/ownership-and-new.md §11。 +define LIFE_VALUE = 0 # 值语义(标量/聚合按值拷贝),不参与逃逸 +define LIFE_FRAME = 1 # 本帧栈存储:局部变量地址、数组退让、形参副本地址 +define LIFE_HEAP = 2 # 堆,hlife(释放靠手动/区域) +define LIFE_STATIC = 3 # 模块级全局 / 字符串常量 / 函数地址 +define LIFE_UNKNOWN = 4 # 未知(extern 返回等),保守放行 + # ============================================================ # VarEntry - variable table entry (formerly HandlesVar.VarEntry) class VarEntry: @@ -67,7 +76,9 @@ class VarEntry: # 1=this variable is a const object (annotation has t.CConst and the type is not a # pointer): assignment to it anywhere in scope fails fast. Pointer CConst only limits the pointee. IsConst: int - + # 生命期标签(LifeTag,见文件头 define LIFE_*):该变量**当前值**的生命期。 + # 追加在末尾以保持既有字段偏移不变。默认 LIFE_UNKNOWN(未登记 = 保守放行)。 + Life: int # ============================================================ # Scope - scope node (formerly HandlesVar.Scope) diff --git a/App/lib/core/Gen/VarGenMixin.vp b/App/lib/core/Gen/VarGenMixin.vp index 5875c87..d7963b7 100644 --- a/App/lib/core/Gen/VarGenMixin.vp +++ b/App/lib/core/Gen/VarGenMixin.vp @@ -178,6 +178,32 @@ novirtual class VarGenMixin: if entry is None: return 0 return entry.IsConst + # ============================================================ + # 生命期标签读写(LifeTag,见 GenTypes define LIFE_*;架构见 ownership-and-new.md §11) + # ============================================================ + + # set_var_life - write a variable's current-value life tag. + def set_var_life(self, symtab: GenTypes.SymbolTable*, name: str, life: int) -> int: + """Set a variable's LifeTag (VarEntry.Life), return 0=success / 1=failure""" + if symtab is None or name is None: return 1 + entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if entry is None: return 1 + entry.Life = life + return 0 + + # get_var_life - read a variable's life tag; LIFE_UNKNOWN when unregistered (conservative). + def get_var_life(self, symtab: GenTypes.SymbolTable*, name: str) -> int: + """A variable's LifeTag; LIFE_UNKNOWN when the variable is unknown""" + if symtab is None or name is None: return GenTypes.LIFE_UNKNOWN + entry: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if entry is None: return GenTypes.LIFE_UNKNOWN + return entry.Life + + # life_meet - control-flow merge: the shorter life wins (smaller tag = shorter). + def life_meet(self, a: int, b: int) -> int: + """Meet of two life tags (min); used where branches join""" + return a if a < b else b + # add_var - add a variable to the variable table # ============================================================ def add_var(self, vars_ptr: GenTypes.VarEntry*, var_count: int, name: str, alloca: mlirlite.Value*) -> int: @@ -199,6 +225,8 @@ novirtual class VarGenMixin: # Explicitly zero the const flag (do not rely on the entry's zero initialization): set_var_const # sets it to 1 only for real const objects, so zeroing avoids false positives. entry.IsConst = 0 + # 生命期标签显式初始化:未登记 = LIFE_UNKNOWN(保守放行)。 + entry.Life = GenTypes.LIFE_UNKNOWN # [S3 dual write] Value-type Spec (alloca.Ty is Ptr(value type), so take its pointee) entry.Spec = self._spec_of_var_alloca(alloca) return 0