diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 90faaf5..302e2d8 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -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) diff --git a/App/lib/core/Gen/Func/FuncGenDef.vp b/App/lib/core/Gen/Func/FuncGenDef.vp index 0c12f62..c2cf954 100644 --- a/App/lib/core/Gen/Func/FuncGenDef.vp +++ b/App/lib/core/Gen/Func/FuncGenDef.vp @@ -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: diff --git a/App/lib/core/Gen/GenTypes.vp b/App/lib/core/Gen/GenTypes.vp index cf3bb7c..3baebea 100644 --- a/App/lib/core/Gen/GenTypes.vp +++ b/App/lib/core/Gen/GenTypes.vp @@ -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) diff --git a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp index 889cf74..662091c 100644 --- a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp @@ -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) diff --git a/App/lib/core/Gen/VarGenMixin.vp b/App/lib/core/Gen/VarGenMixin.vp index 888f9b9..87efc33 100644 --- a/App/lib/core/Gen/VarGenMixin.vp +++ b/App/lib/core/Gen/VarGenMixin.vp @@ -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)