feat(gen): 生命期标签(LifeTag)与借用传播(M2.1)

- GenTypes:新增 define LIFE_VALUE/FRAME/HEAP/STATIC/UNKNOWN 与 VarEntry.Life。 - VarGenMixin:set_var_life/get_var_life/life_meet;add_var 初始化 Life=UNKNOWN。 - AssignGenMixin:life_of_expr(&local、数组退让、变量 Life 读取)、propagate_life(赋值传播)、fail_if_life_escapes(汇点检查,LifetimeError);在 AnnAssign 与单 Name Assign 处挂传播。 - GenApi.fail_if_stack_addr_escape 改为薄壳并委托 fail_if_life_escapes;原 (a)(b) 语法判据并入 life_of_expr。于是 s: str = l; return s 被拦截。
This commit is contained in:
TermiNexus committed 2026-10-08 12:22:10 +08:00
1 parent 7070bc1608
commit ec47401151
4 files changed
+149 -29

No files matched your search

+102
View File
@@ -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):
+7 -28
View File
@@ -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)
+12 -1
View File
@@ -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)
+28
View File
@@ -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