feat(pure): integrate pure_str with lifetime and NLL

This commit is contained in:
TermiNexus committed 2026-10-26 13:52:00 +08:00
1 parent ab562764b4
commit d88cafa12c
5 files changed
+230

No files matched your search

+170
View File
@@ -1201,6 +1201,99 @@ novirtual class AssignGenMixin:
return 0
sc_m = sc_m.Parent
return 0
# ---- pure_str lifetime helpers -----------------------------------------
# A pure_str is a value struct {s: str, size: sizet}; the payload pointer is
# what the lifetime checker tracks. Borrowed params and owned concatenation
# results must not be confused with an ordinary value copy.
def _pure_str_slot(self, pool: memhub.MemBuddy*, alloca: mlirlite.Value*) -> int:
"""1 when the slot holds a pure_str value (alloca type = Ptr(pure_str))."""
if alloca is None or alloca.Ty is None: return 0
if self.Gen.ExprGenBase.is_ptr_type(alloca.Ty) == 0: return 0
if alloca.Ty.Pointee == 0: return 0
if self.Gen.ExprGenBase._pure_str_struct_ty(pool, alloca.Ty.Pointee) is not None: return 1
return 0
def _pure_str_name(self, trans: HT.Translator*, name: str) -> int:
"""1 when the named local is a pure_str value."""
if trans is None or name is None: return 0
v: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, name)
return self._pure_str_slot(trans.Pool, v)
def _pure_str_call_owned(self, trans: HT.Translator*, cl: ast.Call*) -> int:
"""1 when the call allocates a new pure_str payload (owned heap result)."""
if cl is None or cl.func is None: return 0
fk: int = cl.func.kind()
fname: str = None
if fk == ast.ASTKind.Name:
fnm: ast.Name* = ast.Name*(cl.func)
if fnm is not None: fname = fnm.id
elif fk == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(cl.func)
if at is not None:
fname = at.attr
if at.value is not None and self._pure_str_expr(trans, at.value) != 0:
if fname is not None:
if string.strcmp(fname, "__add__") == 0: return 1
if string.strcmp(fname, "upper") == 0: return 1
if string.strcmp(fname, "lower") == 0: return 1
if string.strcmp(fname, "concat") == 0: return 1
if fname is None: return 0
if string.strcmp(fname, "_pure_concat") == 0: return 1
if string.strcmp(fname, "_pure_alloc") == 0: return 1
if string.strcmp(fname, "_pure_upper") == 0: return 1
if string.strcmp(fname, "_pure_lower") == 0: return 1
if string.strcmp(fname, "_pure_clone") == 0: return 1
# A module function whose declared return is pure_str owns a new value.
if fk == ast.ASTKind.Name and trans is not None and trans.Module is not None:
fn_r: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(trans.Module, fname)
if fn_r is not None:
rt_r: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(fn_r)
if rt_r is not None and self.Gen.ExprGenBase._pure_str_struct_ty(trans.Pool, rt_r) is not None: return 1
return 0
def _pure_str_expr(self, trans: HT.Translator*, node: ast.AST*) -> int:
"""1 when the expression has pure_str value type (best effort, conservative)."""
if node is None or trans is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm is None: return 0
return self._pure_str_name(trans, nm.id)
if k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(node)
if cl is None: return 0
if self._pure_str_call_owned(trans, cl) != 0: return 1
if cl.func is not None and cl.func.kind() == ast.ASTKind.Name:
fnm: ast.Name* = ast.Name*(cl.func)
if fnm is not None and fnm.id is not None:
if string.strcmp(fnm.id, "_pure_from") == 0: return 1
if k == ast.ASTKind.BinOp:
bo: ast.BinOp* = ast.BinOp*(node)
if bo is not None and bo.op == ast.OpKind.Add and trans._in_pure != 0:
return self._pure_str_expr(trans, bo.left) | self._pure_str_expr(trans, bo.right)
if k == ast.ASTKind.Constant and trans._in_pure != 0:
cn: ast.Constant* = ast.Constant*(node)
if cn is not None and cn.const_kind == ast.CONST_STR: return 1
if k == ast.ASTKind.IfExp:
ie: ast.IfExp* = ast.IfExp*(node)
if ie is not None:
return self._pure_str_expr(trans, ie.body) | self._pure_str_expr(trans, ie.orelse)
return 0
def _pure_str_rhs_owned(self, trans: HT.Translator*, rhs: ast.AST*) -> int:
"""1 when the RHS produces a newly allocated pure_str payload (owned)."""
if rhs is None or trans is None: return 0
k: int = rhs.kind()
if k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(rhs)
return self._pure_str_call_owned(trans, cl)
if k == ast.ASTKind.BinOp:
bo: ast.BinOp* = BinOp*(rhs)
if bo is not None and bo.op == ast.OpKind.Add and trans._in_pure != 0:
return self._pure_str_expr(trans, bo.left) | self._pure_str_expr(trans, bo.right)
return 0
# life_of_expr - 求表达式当前值的生命期标签。
# target_ty 参与判定:裸本帧数组名在**指针**目标位置 = 数组退让(交出本帧栈地址),
# 在同型聚合目标位置 = 按值拷贝(LIFE_VALUE)。
@@ -1225,6 +1318,14 @@ novirtual class AssignGenMixin:
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
# pure_str: the tracked thing is the payload pointer, not the struct value.
# A borrowed/param value is frame-scoped; an owned concatenation result is
# treated as frame-scoped too (NLL) so it can be freed at scope exit.
if self._pure_str_slot(trans.Pool, v) != 0:
rg_p: int = self.Gen.VarGen.get_var_life(trans.SymTab, nm.id)
if rg_p == GenTypes.LIFE_HEAP or rg_p == GenTypes.LIFE_STATIC or rg_p == GenTypes.LIFE_FRAME or rg_p == GenTypes.LIFE_PURE_OWNED: return rg_p
if rg_p == GenTypes.LIFE_UNKNOWN: return GenTypes.LIFE_UNKNOWN
return GenTypes.LIFE_FRAME
# 变量已登记为堆/静态生命期(如 `o: T* = new heap T()` 的指针变量):以登记为准,
# 下面按"值类型"放宽只针对未登记的帧内局部变量,避免把堆/静态对象误放宽成值拷贝。
rg: int = self.Gen.VarGen.get_var_life(trans.SymTab, nm.id)
@@ -1274,6 +1375,17 @@ novirtual class AssignGenMixin:
return GenTypes.LIFE_UNKNOWN
if k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(node)
# pure_str results: concatenation allocates an owned payload; _pure_from
# borrows its argument.
if cl is not None and self._pure_str_expr(trans, node) != 0:
if self._pure_str_call_owned(trans, cl) != 0: return GenTypes.LIFE_HEAP
if cl.func is not None and cl.func.kind() == ast.ASTKind.Name:
fnm_p: ast.Name* = ast.Name*(cl.func)
if fnm_p is not None and fnm_p.id is not None and string.strcmp(fnm_p.id, "_pure_from") == 0:
a0_p: ast.AST* = None
if cl.args is not None and len(cl.args) > 0: a0_p = cl.args.get(0)
if a0_p is not None: return self.life_of_expr(trans, a0_p, None)
return GenTypes.LIFE_UNKNOWN
# §14 A2:`@life(ret(...))` 声明**优先于任何推断**(无条件)。
rk: int = self._CallRetKind(trans, cl)
if rk >= 0:
@@ -1327,6 +1439,11 @@ novirtual class AssignGenMixin:
if nn.Life == ast.NEW_LIFE_SLIFE: return GenTypes.LIFE_FRAME
if nn.Life == ast.NEW_LIFE_DEFAULT and nn.Source == ast.NEW_SRC_STACK: return GenTypes.LIFE_FRAME
return GenTypes.LIFE_HEAP
if k == ast.ASTKind.BinOp:
# pure_str `+` allocates a new payload (owned heap value).
if trans._in_pure != 0 and self._pure_str_expr(trans, node) != 0:
bo_p: ast.BinOp* = ast.BinOp*(node)
if bo_p is not None and bo_p.op == ast.OpKind.Add: return GenTypes.LIFE_HEAP
return GenTypes.LIFE_UNKNOWN
# life_of_lvalue - 求赋值目标(左值)所在存储的生命期。
@@ -1365,6 +1482,9 @@ novirtual class AssignGenMixin:
if trans is None or target_node is None or rhs_node is None: return 0
lv: int = self.life_of_lvalue(trans, target_node)
if lv != GenTypes.LIFE_HEAP and lv != GenTypes.LIFE_STATIC: return 0
if self.life_of_expr(trans, rhs_node, target_ty) == GenTypes.LIFE_PURE_OWNED:
self.Gen.TypeGenErr.fatal_error(rhs_node, "pure value escapes: the owned pure_str payload is released at the end of this function, so it cannot be stored into a longer-lived field/element/global; return or store a copy instead (e.g. append \"\" to allocate a new value)", "LifetimeError")
return 0
GenApi.fail_if_stack_addr_escape(rhs_node, "a longer-lived field/element", trans, target_ty)
return 0
@@ -1386,6 +1506,11 @@ novirtual class AssignGenMixin:
if life == GenTypes.LIFE_HEAP:
if self.Gen.CallGenCtor._new_is_nll_demotable(trans, rhs_node, tnm.id) != 0:
life = GenTypes.LIFE_FRAME
# pure_str: an owned heap payload bound to a local is frame-confined (the checker
# rejects escapes), so it gets the NLL frame lifetime and is freed at scope exit.
if self._pure_str_name(trans, tnm.id) != 0:
if life == GenTypes.LIFE_HEAP: life = GenTypes.LIFE_PURE_OWNED
elif life == GenTypes.LIFE_VALUE: life = GenTypes.LIFE_FRAME
self.Gen.VarGen.set_var_life(trans.SymTab, tnm.id, life)
return 0
@@ -1455,7 +1580,25 @@ novirtual class AssignGenMixin:
# target_ty 必须传返回类型:裸本帧数组名在**指针**返回类型下是数组退让(交出栈地址),
# 在同型聚合返回类型下是按值拷贝(LIFE_VALUE,不在此列)。
life_r: int = self.life_of_expr(trans, value, rv_ty)
# An owned pure_str local transfers its payload to the caller (NLL move).
if life_r == GenTypes.LIFE_PURE_OWNED: return 0
if life_r == GenTypes.LIFE_FRAME and trans._in_pure != 0 and value.kind() == ast.ASTKind.Name:
# A pure_str local that owns its heap payload transfers ownership on return
# (NLL move): the caller's call expression is treated as an owned value.
n_p: ast.Name* = ast.Name*(value)
if n_p is not None and n_p.id is not None:
slot_p: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, n_p.id)
if slot_p is not None and self._pure_str_slot(trans.Pool, slot_p) != 0 and self.Gen.VarGen._SlifeOwned(slot_p) != 0: return 0
if life_r != GenTypes.LIFE_FRAME: return 0
# Pure values are owned copies: a borrowed frame value must not escape, even with
# an explicit @life(ret(borrows=...)) contract.
if trans._in_pure != 0:
nm_pure: str = "expression"
if value.kind() == ast.ASTKind.Name:
n_pure: ast.Name* = ast.Name*(value)
if n_pure is not None and n_pure.id is not None: nm_pure = n_pure.id
self.Gen.TypeGenErr.fatal_error(value, f"pure value escapes: `{nm_pure}` is borrowed from the caller; pure results must be owned value copies. Return a newly allocated value instead (e.g. `{nm_pure} + \"\"` or a concatenation)", "LifetimeError")
return 0
rk: int = GenApi.get_ret_kind_by_func(fd)
if rk == GenApi.RET_KIND_FRAME: return 0
is_borrow_kind: int = 0
@@ -1508,6 +1651,33 @@ novirtual class AssignGenMixin:
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
# pure_str: the owned resource is the payload field; ownership uses the same
# shadow mechanism, but release goes through _pure_free.
if self._pure_str_slot(pool, target_slot) != 0:
if rhs_node.kind() == ast.ASTKind.Name:
rn_p: ast.Name* = ast.Name*(rhs_node)
if rn_p is not None and rn_p.id is not None:
src_p: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, rn_p.id)
if src_p is not None and uint64(src_p) == uint64(target_slot): return 0
if src_p is not None and self._pure_str_slot(pool, src_p) != 0 and self.Gen.VarGen._SlifeOwned(src_p) != 0:
tsh_p: mlirlite.Value* = self.Gen.VarGen._SlifeEnsureOwned(pool, builder, target_slot)
if tsh_p is not None:
self._SlifeDropOwned(pool, builder, mod, trans, target_slot)
self.Gen.VarGen._SlifeNoteStore(pool, builder, tsh_p, 1)
ssh_p: mlirlite.Value* = self.Gen.VarGen._SlifeShadow(src_p)
if ssh_p is not None: self.Gen.VarGen._SlifeNoteStore(pool, builder, ssh_p, 0)
# The source is a borrow of the target after the move; returning it
# must be rejected instead of handing out a payload the target frees.
self.Gen.VarGen.set_var_life(trans.SymTab, rn_p.id, GenTypes.LIFE_FRAME)
return 1
if self._pure_str_rhs_owned(trans, rhs_node) != 0:
sh_p: mlirlite.Value* = self.Gen.VarGen._SlifeEnsureOwned(pool, builder, target_slot)
if sh_p is not None:
self._SlifeDropOwned(pool, builder, mod, trans, target_slot)
self.Gen.VarGen._SlifeNoteStore(pool, builder, sh_p, 1)
return 1
self._SlifeDropOwned(pool, builder, mod, trans, target_slot)
return 0
if rhs_node.kind() == ast.ASTKind.New:
nn: ast.New* = ast.New*(rhs_node)
owned: int = self.Gen.CallGenCtor._new_is_owned_heap(nn)
+7
View File
@@ -1209,6 +1209,13 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
cls_nm: str = None
if arg2.annotation is not None: cls_nm = self.Gen.TypeGenResolve.extract_class_name_from_annotation(arg2.annotation, imported_modules, pool, trans._from_imports)
if cls_nm is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, arg2.arg, cls_nm)
# pure: str params are borrowed pure_str values and `self` is a
# borrowed receiver. Frame lifetime makes both unable to escape.
if has_pure != 0:
if string.strcmp(arg2.arg, "self") == 0:
self.Gen.VarGen.set_var_life(trans.SymTab, arg2.arg, GenTypes.LIFE_FRAME)
elif resolved2 is not None and self.Gen.ExprGenBase._pure_str_struct_ty(pool, resolved2) is not None:
self.Gen.VarGen.set_var_life(trans.SymTab, arg2.arg, GenTypes.LIFE_FRAME)
# t.VolatileT param: marked at declaration time so loads/stores of this name carry volatile
# (a volatile param object's read-modify-write must not be hoisted/merged, e.g. a spin-wait arg).
if arg2.annotation is not None:
+1
View File
@@ -55,6 +55,7 @@ define LIFE_FRAME = 1 # 本帧栈存储:局部变量地址、数组退让
define LIFE_HEAP = 2 # 堆,hlife(释放靠手动/区域)
define LIFE_STATIC = 3 # 模块级全局 / 字符串常量 / 函数地址
define LIFE_UNKNOWN = 4 # 未知(extern 返回等),保守放行
define LIFE_PURE_OWNED = 5 # owned pure_str heap payload; freed at function exit, transferred on return (NLL)
# ============================================================
# VarEntry - variable table entry (formerly HandlesVar.VarEntry)
+9
View File
@@ -574,6 +574,15 @@ novirtual class StmtGenCifMixin:
# coerce val to the function return type: i1→i8 etc. use zext (bool semantics, 1 stays 1);
# int→ptr must use inttoptr even at equal bit width, else llc rejects the return type.
# pure: returning an owned pure_str local transfers its payload to the caller (NLL move),
# so clear the slife shadow; the caller's call expression becomes the owner.
if val is not None and rt.value is not None and rt.value.kind() == ast.ASTKind.Name:
rt_nm_p: ast.Name* = ast.Name*(rt.value)
if rt_nm_p is not None and rt_nm_p.id is not None:
rt_slot_p: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, rt_nm_p.id)
if rt_slot_p is not None and self.Gen.AssignGen._pure_str_slot(trans.Pool, rt_slot_p) != 0:
rt_sh_p: mlirlite.Value* = self.Gen.VarGen._SlifeShadow(rt_slot_p)
if rt_sh_p is not None: self.Gen.VarGen._SlifeNoteStore(trans.Pool, builder, rt_sh_p, 0)
cur_func_rt: mlirlite.Function* = trans._cur_func
ret_ty_rt: mlirlite.LLVMType* = None
if cur_func_rt is not None and val is not None and val.Ty is not None: ret_ty_rt = mlirlite.function_get_ret_ty(cur_func_rt)
+43
View File
@@ -708,6 +708,46 @@ novirtual class VarGenMixin:
mlirlite.position_at_end(builder, merge_bb)
return 1
def _SlifeEmitPureStrFree(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, alloca: mlirlite.Value*, sh: mlirlite.Value*) -> int:
"""Free a pure_str slot's owned payload through _pure_free, guarded by its shadow."""
if pool is None or builder is None or alloca is None or sh is None: return 0
st_pf: mlirlite.LLVMType* = alloca.Ty.Pointee
sha_pf: str = self.Gen.StructGen.get_struct_sha1("pure_str")
if sha_pf is None: return 0
nm_cap: sizet = string.strlen(sha_pf) + 16
nm_pf: str = pool.alloc(nm_cap)
if nm_pf is None: return 0
viperlib.snprintf(nm_pf, nm_cap, "%s._pure_free", sha_pf)
if mod is not None and self.Gen.CallGenBase.find_func_in_module(mod, nm_pf) is None:
fn_pf: mlirlite.Function* = mlirlite.create_declare(pool, mod, nm_pf, mlirlite.Void(pool))
if fn_pf is not None: mlirlite.add_param(pool, fn_pf, mlirlite.Ptr(pool, st_pf), "p")
zero8_pf: mlirlite.Value* = mlirlite.const_int(pool, 8, 0)
if zero8_pf is None: return 0
shv_pf: mlirlite.Value* = mlirlite.build_load(builder, mlirlite.Int8(pool), sh)
if shv_pf is None: return 0
cond_pf: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, shv_pf, zero8_pf)
if cond_pf is None: return 0
cur_pf: mlirlite.Function* = builder.Func
if cur_pf is None: return 0
cnt_pf: int = trans._label_counter
trans._label_counter = cnt_pf + 1
b1: str = pool.alloc(40)
b2: str = pool.alloc(40)
if b1 is None or b2 is None: return 0
viperlib.snprintf(b1, 40, "pure.free.%d", cnt_pf)
viperlib.snprintf(b2, 40, "pure.free.end.%d", cnt_pf)
free_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, cur_pf, b1)
end_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, cur_pf, b2)
if free_bb is None or end_bb is None: return 0
mlirlite.build_cond_br(builder, cond_pf, free_bb, end_bb)
mlirlite.position_at_end(builder, free_bb)
arg_pf: mlirlite.Value* = alloca
arg_pf.Next = None
mlirlite.build_call(builder, nm_pf, arg_pf, 1, mlirlite.Void(pool), 0)
mlirlite.build_br(builder, end_bb)
mlirlite.position_at_end(builder, end_bb)
return 1
def _SlifeEmitGuardedFree(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, alloca: mlirlite.Value*, sh: mlirlite.Value*) -> int:
"""释放该槽**确实拥有**的对象:先按 §3.6 调度析构 `__del__`(若类定义了),再 `free`。
@@ -716,6 +756,9 @@ novirtual class VarGenMixin:
值绝不参与释放。析构那一步另有条件分支(见 _SlifeEmitDtorBranch)。"""
if pool is None or builder is None or alloca is None or sh is None: return 0
if alloca.Ty is None or alloca.Ty.Pointee == 0: return 0
# pure_str: the owned resource is the payload field, not the struct slot.
if self.Gen.ExprGenBase._pure_str_struct_ty(pool, alloca.Ty.Pointee) is not None:
return self._SlifeEmitPureStrFree(pool, builder, mod, trans, alloca, sh)
# §3.6:先析构,再 free
self._SlifeEmitDtorBranch(pool, builder, mod, trans, alloca, sh)
zero8: mlirlite.Value* = mlirlite.const_int(pool, 8, 0)