feat(lifetime): §13 NLL——默认生命期的 new heap 证明帧内封闭时降级为帧对象

StructGenMixin: 新增 NllFrameConfined 及遍历器(_NllStmts/_NllExprUnsafe/_NllMentions/_NllInSet/_NllAdd/_NllCalleeEscMask)+
has_param_escapes(区分"有记录=权威不逃逸"与"无记录=判不定");别名集只增不减故单遍即可,if/while/for 全走、
不进嵌套 def/class;认不出的语句"不提及别名才放行";取址/实参位逃逸/return/长命目标一律按外泄拒绝。

CallGenCtor._new_is_nll_demotable:只降级"堆且未显式指定生命期"的 new;AssignGenMixin.propagate_life
判据成立则 life=FRAME(于是调用点同命裁剪放过 g(l,h)),_slife_note_assign 接 target_name、判据成立按托管堆
对象登记 → 复用 slife 的作用域出口释放(手动 free/del 也会被既有检查挡住)。

三条函数体翻译入口记录 NllCurFunc(谓词要遍历当前函数体),与 traceback 推帧同一处。

注:谓词按 §13.2 保守优先,判不定一律维持 HEAP;本提交未编译验证。
This commit is contained in:
TermiNexus committed 2026-10-09 20:49:21 +08:00
1 parent 79299ec47d
commit d0ffb094c2
6 files changed
+243 -6

No files matched your search

+14 -4
View File
@@ -711,6 +711,11 @@ novirtual class AssignGenMixin:
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)
# §13 NLL:默认生命期的 `new heap T()` 若证明在本函数内封闭(不外泄)→ 按帧生命期处理。
# 与 `new heap slife` 同口径(生命期 FRAME + 作用域出口释放,见 _slife_note_assign)。
if life == GenTypes.LIFE_HEAP:
if self.Gen.CallGenCtor._new_is_nll_demotable(trans, rhs_node, tnm.id) != 0:
life = GenTypes.LIFE_FRAME
self.Gen.VarGen.set_var_life(trans.SymTab, tnm.id, life)
return 0
@@ -762,12 +767,17 @@ novirtual class AssignGenMixin:
# ② 其它 RHS(`new stack` / `new … hlife` / 裸名别名 `q = p` / 外来指针 / None):
# 槽已登记时交还所有权(释放旧拥有对象 + 影子置 0)——绝不把外来指针当自己的对象释放。
# **必须在值写入之前调用**:覆盖释放要读到槽里的旧值,晚于写入就变成"释放刚写进去的对象"。
def _slife_note_assign(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, target_slot: mlirlite.Value*, rhs_node: ast.AST*) -> int:
def _slife_note_assign(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, target_slot: mlirlite.Value*, rhs_node: ast.AST*, target_name: str = None) -> int:
"""M3:赋值处的 slife 所有权记账;返回 1=该槽此刻是编译器拥有的 slife 绑定"""
if target_slot is None or rhs_node is None or trans is None: return 0
if rhs_node.kind() == ast.ASTKind.New:
nn: ast.New* = ast.New*(rhs_node)
if self.Gen.CallGenCtor._new_is_owned_heap(nn) != 0:
owned: int = self.Gen.CallGenCtor._new_is_owned_heap(nn)
if owned == 0:
# §13 NLL:判据成立时该 `new heap` 也按"编译器托管的堆对象"处理 ——
# 生命期已是 FRAME(propagate_life 同一谓词),这里补上**出口释放**这一半。
owned = self.Gen.CallGenCtor._new_is_nll_demotable(trans, rhs_node, target_name)
if owned != 0:
sh: mlirlite.Value* = self.Gen.VarGen._SlifeEnsureOwned(pool, builder, target_slot)
if sh is None: return 0
self._SlifeDropOwned(pool, builder, mod, trans, target_slot)
@@ -1989,7 +1999,7 @@ novirtual class AssignGenMixin:
vol_laa = 1
# M3 所有权记账:必须**先**记账再写值——覆盖释放要读到槽里的旧值
# (`p: T* = new heap slife …` → 释放旧对象 + 影子置 1;`q: T* = p` → 交还所有权)
self._slife_note_assign(pool, builder, mod, trans, alloca, aa.value)
self._slife_note_assign(pool, builder, mod, trans, alloca, aa.value, nm.id)
mlirlite.build_store(builder, rhs_val, alloca, vol_laa)
# P2 willFree:带注解声明/赋值后清除失效态(新变量本就为 0;复用槽兜底)
self.Gen.VarGen.clear_var_freed(trans.SymTab, nm.id)
@@ -2548,7 +2558,7 @@ novirtual class AssignGenMixin:
vol_las: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id))
# M3 所有权记账:必须先记账再写值(覆盖释放要读到旧值);
# `p = new heap slife …` → 释放旧对象 + 影子置 1;`q = p` / 外来指针 → 交还所有权
self._slife_note_assign(pool, builder, mod, trans, alloca, asgn.value)
self._slife_note_assign(pool, builder, mod, trans, alloca, asgn.value, nm.id)
mlirlite.build_store(builder, store_val, alloca, vol_las)
# P2 willFree:对同名变量重新赋值即重新获得有效值,清除失效态
# (新变量由 add_var 置 0;已存在变量走这里,见 clear_var_freed 注释)
+13
View File
@@ -318,6 +318,19 @@ novirtual class CallGenCtor(CallGenBase.CallGenBase):
# heap(或来源缺省=heap)+ slife → malloc + 函数出口自动 free(RAII);
# stack(含 stack slife)→ 帧内 alloca,随帧消失,不产生 free;
# memgr(x) slife → 归还池(D6 待定),本阶段不接管。
# _new_is_nll_demotable - §13 NLL:可降级为"帧对象"的 `new heap`(判据见 Debugger/ownership-and-new.md §13.2)
def _new_is_nll_demotable(self, trans: HT.Translator*, rhs: ast.AST*, var_name: str) -> int:
"""1 = 该 `new` 可当帧对象处理(生命期按 FRAME、出口释放复用 slife 通道);0 = 不必/判不定。
只降级"堆且**未显式**指定生命期"的:显式 `hlife` 是用户对自由生命期的要求,不动;
`slife` 本来就走帧生命期 + 出口释放,无需降级。判据本体在 StructGenMixin.NllFrameConfined。"""
if rhs is None or rhs.kind() != ast.ASTKind.New: return 0
nn: ast.New* = ast.New*(rhs)
if nn is None: return 0
if nn.Source != ast.NEW_SRC_HEAP and nn.Source != ast.NEW_SRC_DEFAULT: return 0
if nn.Life != ast.NEW_LIFE_DEFAULT: return 0
return self.Gen.StructGen.NllFrameConfined(trans, var_name, rhs)
def _new_is_owned_heap(self, new_node: ast.New*) -> int:
"""1=`new` 生成编译器独占释放的堆绑定 / 0=不是"""
if new_node is None: return 0
+4
View File
@@ -405,9 +405,13 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
if fd is not None and fd.name is not None:
# 方法体翻译 = 下潜进一个源码上下文:推一帧(def 位置),供 traceback 回溯。
tr_mf: str = self.Gen.TypeGenResolve.get_current_file()
# §13 NLL:记录当前方法体(谓词 NllFrameConfined 要遍历它)
prev_nf: ast.AST* = self.Gen.StructGen.NllCurFunc
self.Gen.StructGen.NllCurFunc = fd
self.Gen.TypeGenBase.push_trace(tr_mf, fd.lineno)
self._translate_method(trans, fd, struct_ty, class_name)
self.Gen.TypeGenBase.pop_trace()
self.Gen.StructGen.NllCurFunc = prev_nf
# If there is a vtable, generate it before __before_init__ (which reads VTableMethodCount);
# check via oop_entry directly to avoid cross-module same-name lookups.
+4
View File
@@ -53,9 +53,13 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
tr_nf: str = self.Gen.TypeGenResolve.get_current_file()
tr_nl: int = 0
if nfd_t is not None: tr_nl = nfd_t.lineno
# §13 NLL:记录当前函数体(谓词 NllFrameConfined 要遍历它)
prev_nf: ast.AST* = self.Gen.StructGen.NllCurFunc
self.Gen.StructGen.NllCurFunc = node
self.Gen.TypeGenBase.push_trace(tr_nf, tr_nl)
rc_nf: int = self.Gen.FuncGen.translate_nested_function_def(trans, node)
self.Gen.TypeGenBase.pop_trace()
self.Gen.StructGen.NllCurFunc = prev_nf
return rc_nf
elif k == ast.ASTKind.Return: return self.Gen.StmtGenCif.translate_return(trans, node)
elif k == ast.ASTKind.If: return self.Gen.StmtGenCif.translate_if(trans, node)
+204 -2
View File
@@ -178,7 +178,10 @@ novirtual class StructGenMixin:
# 3 槽/项:[名哈希, 强制逃逸位, 强制不逃逸位]。调用点在**所有推断与裁剪之后**最后套用,
# 是不可被推翻的契约("unsafe 但强制")。每文件重置计数。
EscDirTable: sizet*
EscDirCount: int
EscDirCount: int
# §13 NLL:当前正在翻译的函数定义(谓词 NllFrameConfined 要拿它的函数体做遍历)。
# 由三条函数体翻译入口设置/还原(顶层函数体 / 嵌套函数体 / 方法体)。
NllCurFunc: ast.AST*
def InitStructGen(self) -> int:
"""Initialize struct-mixin fields; called inline from subclass constructors."""
@@ -210,7 +213,8 @@ novirtual class StructGenMixin:
self.EscLeakWant = -1
self.EscLeakLine = 0
self.EscDirTable = None
self.EscDirCount = 0
self.EscDirCount = 0
self.NllCurFunc = None
self.StructNameIdx = None
self.StructTypeIdx = None
self.StructIdxCap = 0
@@ -524,6 +528,14 @@ novirtual class StructGenMixin:
if idx < 0: return 0
return int(self.EscTable[idx * 3 + 1])
def has_param_escapes(self, name: str) -> int:
"""本文件摘要表**是否有**该函数的记录(1/0)。
与"掩码是否为 0"必须区分:`@life(noescape(...))` 会把掩码清成 0,但那是**有记录**
(权威结论 = 不逃逸);而无记录 = 判不定(跨模块 / 未登记),调用方应保守处理。"""
if self._EscLookup(name) >= 0: return 1
return 0
def get_ret_borrow(self, name: str) -> int:
"""返回借用标记:1=该函数直接返回某形参;0=无记录"""
idx: int = self._EscLookup(name)
@@ -681,6 +693,196 @@ novirtual class StructGenMixin:
box[2] = (box[2] | sizet(ef_m)) & (~sizet(ne_m))
return 0
# ============================================================
# §13 NLL:非词法生命期 —— 堆对象降级为帧对象
# ============================================================
# 判定"变量 v 在本函数内封闭":不外泄(判据 A)。单函数内所有局部存储同帧消亡,
# 故"末次使用在本帧内"(判据 B)自动成立 —— 判据 A 成立即可降级为 LIFE_FRAME,
# 释放复用 slife 的 RAII 出口(见 §13.3)。
# 保守原则:**任何判不定都返回 0**(调用方维持 LIFE_HEAP = 现状),别名集**只增不减**。
def _NllInSet(self, al: sizet*, name: str) -> int:
"""name 是否在别名集里([0]=计数,[1..]=名哈希);1=是 / 0=否"""
if al is None or name is None: return 0
h: sizet = self._NameHash(name)
c: sizet = al[0]
i: sizet = 0
while i < c:
if al[1 + i] == h: return 1
i += 1
return 0
def _NllAdd(self, al: sizet*, name: str) -> int:
"""并入别名集(只增不减;满 64 则忽略 —— 只增语义下忽略仍是保守的)"""
if al is None or name is None: return 0
if self._NllInSet(al, name) != 0: return 0
c: sizet = al[0]
if c >= 63: return 0
al[1 + c] = self._NameHash(name)
al[0] = c + 1
return 0
def _NllMentions(self, node: ast.AST*, al: sizet*) -> int:
"""表达式/子树里是否出现别名集里的变量(保守:拿不到子树就算"出现")"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is not None and self._NllInSet(al, nm.id) != 0: return 1
return 0
if k == ast.ASTKind.Constant: return 0
ch: list[ast.AST*]* = node.children
if ch is None: return 1
n: sizet = len(ch)
i: sizet = 0
while i < n:
if self._NllMentions(ch.get(i), al) != 0: return 1
i += 1
return 0
def _NllAliasSource(self, value: ast.AST*, al: sizet*) -> int:
"""RHS 是否"恰好是一个别名变量"(`w = v`)→ 1"""
if value is None or value.kind() != ast.ASTKind.Name: return 0
vn: ast.Name* = ast.Name*(value)
if vn.id is None: return 0
return self._NllInSet(al, vn.id)
def _NllCalleeEscMask(self, cl: ast.Call*) -> int:
"""被调者形参逃逸掩码;**-1 = 判不定**(调用方按"不安全"处理)。
只认"本文件有摘要记录的裸名函数":其掩码是本次编译重算/登记的权威值。
方法调用 / 跨模块 / 认不出的名字(含 extern C:可能留存指针)一律 -1。"""
if cl is None or cl.func is None: return -1
if cl.func.kind() != ast.ASTKind.Name: return -1
fnm: ast.Name* = ast.Name*(cl.func)
if fnm.id is None: return -1
if self.has_param_escapes(fnm.id) == 0: return -1
return self.get_param_escapes(fnm.id)
def _NllExprUnsafe(self, node: ast.AST*, al: sizet*) -> int:
"""表达式里别名是否被放到"不安全位置"→ 1(取址 / 当被调者 / 交给会逃逸的形参位)"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.UnaryOp:
# 取址(以及任何一元运算):保守视为不安全
uo: ast.UnaryOp* = ast.UnaryOp*(node)
return self._NllMentions(uo.operand, al)
if k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(node)
if self._NllMentions(cl.func, al) != 0: return 1
if cl.args is None: return 0
mask: int = self._NllCalleeEscMask(cl)
na: sizet = len(cl.args)
j: sizet = 0
while j < na:
a: ast.AST* = cl.args.get(j)
idx: int = int(j)
j += 1
# 只有"实参恰好是别名变量本身"才算交出该变量(`h.q` 只是读字段值,不算交出 h)
if a is None or a.kind() != ast.ASTKind.Name: continue
an: ast.Name* = ast.Name*(a)
if an.id is None or self._NllInSet(al, an.id) == 0: continue
if mask < 0: return 1
if ((mask >> idx) & 1) != 0: return 1
return 0
ch: list[ast.AST*]* = node.children
if ch is None: return 0
n: sizet = len(ch)
i: sizet = 0
while i < n:
if self._NllExprUnsafe(ch.get(i), al) != 0: return 1
i += 1
return 0
def _NllStmts(self, stmts: list[ast.AST*]*, al: sizet*, def_node: ast.AST*, depth: int) -> int:
"""遍历一个块;1=块内所有别名使用都帧内安全 / 0=发现外泄(放弃降级)
别名集只增不减 ⇒ 单遍即可(无需循环固定点)。控制流感知体现在"if/while/for 的
分支与循环体全部走一遍";**不进嵌套 def/class**(闭包捕获按外泄处理)。"""
if stmts is None or al is None or depth > 64: return 1
n: sizet = len(stmts)
i: sizet = 0
while i < n:
st: ast.AST* = stmts.get(i)
i += 1
if st is None: continue
k: int = st.kind()
if k == ast.ASTKind.Return:
if self._NllMentions(ast.Return*(st).value, al) != 0: return 0
elif k == ast.ASTKind.Assign:
asg: ast.Assign* = ast.Assign*(st)
if def_node is not None and asg.value is not None:
if uint64(asg.value) == uint64(def_node): continue # 定义处本身,跳过
if asg.targets is not None:
nt: sizet = len(asg.targets)
t: sizet = 0
while t < nt:
tg: ast.AST* = asg.targets.get(t)
t += 1
if tg is None: continue
if tg.kind() == ast.ASTKind.Name:
tnm: ast.Name* = ast.Name*(tg)
if tnm.id is not None:
if self._NllAliasSource(asg.value, al) != 0:
self._NllAdd(al, tnm.id) # w = v → w 也是别名
else:
if self._NllInSet(al, tnm.id) != 0: return 0 # 把别名重绑成别的
if self._NllMentions(asg.value, al) != 0: return 0
continue
if self._NllMentions(asg.value, al) != 0: return 0
if self._NllExprUnsafe(asg.value, al) != 0: return 0
elif k == ast.ASTKind.AnnAssign:
aa: ast.AnnAssign* = ast.AnnAssign*(st)
if def_node is not None and aa.value is not None:
if uint64(aa.value) == uint64(def_node): continue
if aa.target is not None and aa.target.kind() == ast.ASTKind.Name:
tnm2: ast.Name* = ast.Name*(aa.target)
if tnm2.id is not None:
if self._NllAliasSource(aa.value, al) != 0:
self._NllAdd(al, tnm2.id)
else:
if self._NllInSet(al, tnm2.id) != 0: return 0
if self._NllMentions(aa.value, al) != 0: return 0
elif self._NllMentions(aa.value, al) != 0: return 0
if self._NllExprUnsafe(aa.value, al) != 0: return 0
elif k == ast.ASTKind.Expr:
if self._NllExprUnsafe(ast.Expr*(st).value, al) != 0: return 0
elif k == ast.ASTKind.If:
ifn: ast.If* = ast.If*(st)
if self._NllExprUnsafe(ifn.test, al) != 0: return 0
if self._NllStmts(ifn.children, al, def_node, depth + 1) == 0: return 0
if self._NllStmts(ifn.orelse, al, def_node, depth + 1) == 0: return 0
elif k == ast.ASTKind.While:
wn: ast.While* = ast.While*(st)
if self._NllExprUnsafe(wn.test, al) != 0: return 0
if self._NllStmts(wn.children, al, def_node, depth + 1) == 0: return 0
if self._NllStmts(wn.orelse, al, def_node, depth + 1) == 0: return 0
elif k == ast.ASTKind.For:
fr: ast.For* = ast.For*(st)
if self._NllExprUnsafe(fr.iter, al) != 0: return 0
if self._NllStmts(fr.children, al, def_node, depth + 1) == 0: return 0
if self._NllStmts(fr.orelse, al, def_node, depth + 1) == 0: return 0
else:
# 认不出的语句(pass/assert/del/嵌套 def 等):整棵子树不提及别名就放行,
# 提及则保守拒绝(嵌套 def 捕获闭包变量走这条)。
if self._NllMentions(st, al) != 0: return 0
return 1
def NllFrameConfined(self, trans: HT.Translator*, var_name: str, def_node: ast.AST*) -> int:
"""§13.2 判据:`new heap` 出来的 var_name 在本函数内封闭(不外泄)→ 1;判不定 → 0。
def_node = 该 `new` 表达式节点本身(用来跳过"定义处那条语句"的重绑定判定)。"""
if var_name is None or self.NllCurFunc is None: return 0
fd: ast.FunctionDef* = ast.FunctionDef*(self.NllCurFunc)
if fd is None or fd.children is None: return 0
al: sizet* = sizet*(stdlib.malloc(64 * 8))
if al is None: return 0
string.memset(al, 0, 64 * 8)
al[0] = 1
al[1] = self._NameHash(var_name)
r: int = self._NllStmts(fd.children, al, def_node, 0)
stdlib.free(al)
return r
# escape_leak_line - **错误路径专用**:重扫本文件被调者函数体,取 want_pi 号形参的
# "首个逃逸写入点"行号(0 = 未知)。只在 fail-fast 报错时调用一次,不进热路径;
# 跨模块拿不到 AST(EscFuncTable 按文件重置)时返回 0,调用点就不打泄漏点帧。
+4
View File
@@ -70,9 +70,13 @@ novirtual class TranslatorGenMixin:
# 函数体翻译 = 编译器下潜进一个源码上下文:推一帧(def 位置),
# 让函数体内部的错误在 traceback 里回溯到"正在翻译哪个函数"(调用链)。
tr_f: str = self.Gen.TypeGenResolve.get_current_file()
# §13 NLL:记录当前函数体(谓词 NllFrameConfined 要遍历它)
prev_nf: ast.AST* = self.Gen.StructGen.NllCurFunc
self.Gen.StructGen.NllCurFunc = child
self.Gen.TypeGenBase.push_trace(tr_f, fd_tc.lineno)
added: int = self.Gen.FuncGenDef.translate_function_def(trans, child)
self.Gen.TypeGenBase.pop_trace()
self.Gen.StructGen.NllCurFunc = prev_nf
added_total += added
elif trans._declare_only == 1:
self._register_cexport_from_funcdef(trans, child)