diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index de6cfbe..af9bcb1 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -1430,14 +1430,12 @@ novirtual class AssignGenMixin: if GenApi.is_nonlocal_name(trans, onm.id) != 0: return GenTypes.LIFE_UNKNOWN slot_lo: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, onm.id) if slot_lo is not None: - # `&ownedPtr` yields the heap object pointer (one load), not the frame slot. + # `&ownedPtr` / `&heapPtr` yields the heap object pointer (one load), + # not the frame slot; a plain value/parameter is still frame-local. if trans.StrictOBS != 0 and slot_lo.Ty is not None: - match slot_lo.Ty: - case mlirlite.LLVMType.Ptr(lo1): - match lo1: - case mlirlite.LLVMType.Ptr(_): return GenTypes.LIFE_HEAP - case _: pass - case _: pass + if self.Gen.VarGen._SlifeOwned(slot_lo) != 0 \ + or self.Gen.VarGen.get_var_life(trans.SymTab, onm.id) == GenTypes.LIFE_HEAP: + return GenTypes.LIFE_HEAP return GenTypes.LIFE_FRAME return GenTypes.LIFE_STATIC return GenTypes.LIFE_UNKNOWN @@ -1644,8 +1642,9 @@ novirtual class AssignGenMixin: fd: ast.AST* = self.Gen.StructGen.NllCurFunc if fd is None: return 0 # OBS has no `@life` contracts: a returned borrow must come from a reference parameter or - # `self`, which is the elided lifetime rule for reference return types. - if GenApi.strict_obs() != 0: + # `self`, which is the elided lifetime rule for reference return types. Use the translator's + # per-file flag: the module-level one can be clobbered by another file's vpi pass. + if trans.StrictOBS != 0: return self._check_return_borrow_obs(trans, ast.FunctionDef*(fd), value) rv_ty: mlirlite.LLVMType* = None if trans._cur_func is not None: rv_ty = mlirlite.function_get_ret_ty(trans._cur_func) @@ -1709,6 +1708,9 @@ novirtual class AssignGenMixin: def _check_return_borrow_obs(self, trans: HT.Translator*, fd: ast.FunctionDef*, value: ast.AST*) -> int: """Reject a frame-local return in an OBS file; borrow elision replaces `@life`.""" if trans is None or fd is None or value is None: return 0 + ret_rk: int = self.Gen.TypeGenResolve.ref_kind(fd.returns) + if ret_rk != 0: + return self._check_ref_return_obs(trans, fd, value) rv_ty: mlirlite.LLVMType* = None if trans._cur_func is not None: rv_ty = mlirlite.function_get_ret_ty(trans._cur_func) life_r: int = self.life_of_expr(trans, value, rv_ty) @@ -1727,30 +1729,62 @@ novirtual class AssignGenMixin: 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", "LifetimeError") return 0 - ret_rk: int = self.Gen.TypeGenResolve.ref_kind(fd.returns) - pi: int = self.Gen.StructGen._ParamIndex(fd, value) - nm_txt: str = "expression" - nm_pre: str = "" + self.Gen.TypeGenErr.fatal_error(value, "stack address escapes: this value is frame-local and OBS files have no `@life(ret(...))` contracts. Return a heap allocation (`new ...`), a reference parameter, or a value copy", "LifetimeError") + return 0 + + # _ParamIndexByName - parameter position by name (None when not a parameter). + def _ParamIndexByName(self, fd: ast.FunctionDef*, name: str) -> int: + if fd is None or name is None: return -1 + agr: ast.Arguments* = ast.Arguments*(fd.args) + if agr is None or agr.args is None: return -1 + i: int = 0 + while i < int(len(agr.args)): + pa: ast.AST* = agr.args.get(sizet(i)) + if pa is not None and pa.kind() == ast.ASTKind.Arg: + ar: ast.Arg* = ast.Arg*(pa) + if ar.arg is not None and string.strcmp(ar.arg, name) == 0: return i + i += 1 + return -1 + + # _check_ref_return_obs - a `-> &T` result may only borrow a reference parameter or `self`. + def _check_ref_return_obs(self, trans: HT.Translator*, fd: ast.FunctionDef*, value: ast.AST*) -> int: + """Structural borrow-elision check for reference-returning OBS functions.""" + if trans is None or fd is None or value is None: return 0 + if value.kind() == ast.ASTKind.Call: return 0 if value.kind() == ast.ASTKind.Name: - n2: ast.Name* = ast.Name*(value) - if n2.id is not None: nm_txt = n2.id - elif value.kind() == ast.ASTKind.UnaryOp: - u2: ast.UnaryOp* = ast.UnaryOp*(value) - 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 = "&" - if ret_rk != 0 and pi >= 0: - # Borrow elision: returning a reference parameter borrows the caller's value. - par: ast.Arg* = self._ParamAt(fd, pi) - if par is not None and self.Gen.TypeGenResolve.ref_kind(par.annotation) != 0: return 0 - self.Gen.TypeGenErr.fatal_error(value, f"cannot return `{nm_txt}` from a reference-returning function: parameter '{nm_txt}' is stored by value in this frame, so the result would dangle. Declare the parameter as `&T` / `&mut T`", "LifetimeError") + nm: ast.Name* = ast.Name*(value) + if nm.id is None: return 0 + if string.strcmp(nm.id, "self") == 0: return 0 + pi: int = self._ParamIndexByName(fd, nm.id) + if pi >= 0: + par: ast.Arg* = self._ParamAt(fd, pi) + if par is not None and self.Gen.TypeGenResolve.ref_kind(par.annotation) != 0: return 0 + self.Gen.TypeGenErr.fatal_error(value, f"cannot return `{nm.id}` from a reference-returning function: parameter '{nm.id}' is stored by value in this frame, so the result would dangle. Declare the parameter as `&T` / `&mut T`", "LifetimeError") + return 0 + # A reference binding: allowed when the object it borrows outlives this frame. + if self.Gen.VarGen.var_ref_kind(trans.SymTab, nm.id) != 0: + own_r: str = self.Gen.StmtGen._BorrowOwnerOf(trans, nm.id) + if own_r is not None: + pi_o: int = self._ParamIndexByName(fd, own_r) + if pi_o >= 0: + par_o: ast.Arg* = self._ParamAt(fd, pi_o) + if par_o is not None and self.Gen.TypeGenResolve.ref_kind(par_o.annotation) != 0: return 0 + else: + slot_o: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, own_r) + if slot_o is not None and (self.Gen.VarGen._SlifeOwned(slot_o) != 0 \ + or self.Gen.VarGen.get_var_life(trans.SymTab, own_r) == GenTypes.LIFE_HEAP): + return 0 + self.Gen.TypeGenErr.fatal_error(value, f"cannot return `{nm.id}`: it lives in this frame, and a reference result may only borrow a reference parameter or `self` (borrow elision)", "LifetimeError") return 0 - if ret_rk != 0: - self.Gen.TypeGenErr.fatal_error(value, f"cannot return `{nm_pre}{nm_txt}`: it lives in this frame, and a reference result may only borrow a reference parameter or `self` (borrow elision). Allocate the value with `new` and return an owned binding instead", "LifetimeError") + if value.kind() == ast.ASTKind.Attribute: + at_r: ast.Attribute* = ast.Attribute*(value) + if at_r.value is not None and at_r.value.kind() == ast.ASTKind.Name: + bn: ast.Name* = ast.Name*(at_r.value) + if bn.id is not None and string.strcmp(bn.id, "self") == 0: return 0 + if value.kind() == ast.ASTKind.UnaryOp: + self.Gen.TypeGenErr.fatal_error(value, "cannot return the address of a frame binding from a reference-returning function: return the reference parameter itself (`return p`) or the stored reference (`return self.field`)", "LifetimeError") return 0 - self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local and OBS files have no `@life(ret(...))` contracts. Return a heap allocation (`new ...`), a reference parameter, or a value copy", "LifetimeError") + self.Gen.TypeGenErr.fatal_error(value, "cannot return this expression from a reference-returning function: a reference result must borrow a reference parameter, `self`, or another reference", "LifetimeError") return 0 # _ParamAt - parameter node at a 0-based position (None when out of range). @@ -1772,6 +1806,38 @@ novirtual class AssignGenMixin: self.Gen.VarGen._SlifeNoteStore(pool, builder, sh, 0) return 1 + # _in_ctor - 1 when the current function is a constructor (`__init__` / `__before_init__`). + def _in_ctor(self, trans: HT.Translator*) -> int: + if trans is None or trans._cur_func is None: return 0 + fname: str = mlirlite.function_get_name(trans._cur_func) + if fname is None: return 0 + if string.strstr(fname, ".__init__") is not None: return 1 + if string.strstr(fname, ".__before_init__") is not None: return 1 + return 0 + + # _release_own_slot - free the value an `own` field holds before the field is overwritten. + def _release_own_slot(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, + mod: mlirlite.LLVMModule*, trans: HT.Translator*, + field_ptr: mlirlite.Value*) -> int: + if pool is None or builder is None or mod is None or trans is None or field_ptr is None: return 0 + if field_ptr.Ty is None or field_ptr.Ty.Pointee == 0: return 0 + fty: mlirlite.LLVMType* = field_ptr.Ty.Pointee + if self.Gen.ExprGenBase.is_ptr_type(fty) == 0: return 0 + cur: mlirlite.Value* = mlirlite.build_load(builder, fty, field_ptr) + if cur is None: return 0 + nulp: mlirlite.Value* = mlirlite.ConstNull(pool, fty, "null") + if nulp is None: return 0 + live: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, cur, nulp) + if live is None: return 0 + one: mlirlite.Value* = mlirlite.const_int(pool, 8, 1) + zero: mlirlite.Value* = mlirlite.const_int(pool, 8, 0) + if one is None or zero is None: return 0 + flag: mlirlite.Value* = mlirlite.build_select(builder, live, one, zero) + sh: mlirlite.Value* = mlirlite.build_alloca(builder, mlirlite.Int8(pool)) + if flag is None or sh is None: return 0 + mlirlite.build_store(builder, flag, sh) + return self.Gen.VarGen._SlifeEmitGuardedFree(pool, builder, mod, trans, field_ptr, sh) + # _slife_note_assign - M3 赋值处的所有权记账(§4.2 RAII / §4.3 编译器独占释放)。 # 只在**本地 Name 目标**的赋值落地处调用;目标槽 target_slot 即该变量的 alloca。 # ① RHS = `new heap slife T(...)`:登记该槽(首次建影子标志)→ 覆盖释放旧拥有对象 → 影子置 1; @@ -1781,6 +1847,20 @@ 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 + # An `own` target only takes an owned value: a move from another `own` binding, `new ...`, + # or a call that returns ownership. A raw pointer does not transfer ownership, so adopting + # it would free memory the program still treats as borrowed/aliased. + if target_name is not None and self.Gen.VarGen.var_is_own(trans.SymTab, target_name) != 0 \ + and rhs_node.kind() == ast.ASTKind.Name \ + and GenApi.is_none_literal(rhs_node) == 0: + rn_own: ast.Name* = ast.Name*(rhs_node) + if rn_own.id is not None: + src_own: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, rn_own.id) + src_is_own: int = self.Gen.VarGen.var_is_own(trans.SymTab, rn_own.id) + if src_own is not None and self.Gen.VarGen._SlifeOwned(src_own) != 0: src_is_own = 1 + if src_is_own == 0: + self.Gen.TypeGenErr.fatal_error(rhs_node, f"`{target_name}` is `own`: it can only take an owned value (move another `own` binding, allocate with `new heap hlife ...`, or call a function returning `own T`); a raw `{rn_own.id}` does not transfer ownership", "LifetimeError") + 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: @@ -1818,7 +1898,12 @@ novirtual class AssignGenMixin: # `own T`: explicit exclusive ownership (independent of the NLL demotion predicate). if target_name is not None and self.Gen.VarGen.var_is_own(trans.SymTab, target_name) != 0: if nn.Source == ast.NEW_SRC_STACK: - self.Gen.TypeGenErr.fatal_error(rhs_node, "`own T` stores an exclusively owned heap object; `new stack` dies with the frame, use `new` (heap) or `new memgr(...)`", "LifetimeError") + self.Gen.TypeGenErr.fatal_error(rhs_node, "`own T` stores an exclusively owned heap object; `new stack` dies with the frame, use `new heap hlife ...`", "LifetimeError") + return 0 + if nn.Source == ast.NEW_SRC_MEMGR: + # Pool memory is reclaimed by its manager, not by `free`; ownership would + # release it through the wrong allocator. + self.Gen.TypeGenErr.fatal_error(rhs_node, "`own T` cannot take a `new memgr(...)` allocation: that storage is reclaimed by the pool manager, not by `free`. Use `new heap hlife ...`, or keep the pool pointer as a plain `T*`", "LifetimeError") return 0 owned = 1 if owned != 0: @@ -2826,6 +2911,24 @@ novirtual class AssignGenMixin: if o_ref is not None: owner_b = o_ref if owner_b is not None and src_rk != 0: self.Gen.StmtGen._BorrowAdd(trans, owner_b, nm.id, src_rk, aa.value) + # A call returning `&T` borrows one of its arguments; until a precise + # return-borrow summary exists, every borrowed argument is treated as borrowed + # by the result (conservative: `del` of any of them is blocked). + if aa.value is not None and aa.value.kind() == ast.ASTKind.Call \ + and self.Gen.TypeGenResolve.ref_expr_kind(aa.value) == 0: + c_args: list[ast.AST*]* = (ast.Call*)(aa.value).args + if c_args is not None: + ai_b: int = 0 + while ai_b < int(len(c_args)): + arg_b: ast.AST* = c_args.get(sizet(ai_b)) + ai_b += 1 + ob: str = self._BorrowOwnerName(arg_b) + if ob is None and arg_b is not None and arg_b.kind() == ast.ASTKind.Name: + an_b: ast.Name* = ast.Name*(arg_b) + if an_b.id is not None and self.Gen.VarGen.var_ref_kind(trans.SymTab, an_b.id) != 0: + ob = self.Gen.StmtGen._BorrowOwnerOf(trans, an_b.id) + if ob is not None: + self.Gen.StmtGen._BorrowAdd(trans, ob, nm.id, src_rk, arg_b) # A call returning `own T` transfers ownership to this binding. if GenApi.annotation_is_own(annot) != 0 and aa.value is not None \ and aa.value.kind() == ast.ASTKind.Call: @@ -4162,6 +4265,10 @@ novirtual class AssignGenMixin: self.Gen.TypeGenErr.fatal_error(target, "assign a reference field through a named object (`obj.field = &value`); a reference held by a temporary cannot be tracked", "TypeError") else: self.Gen.StmtGen._BorrowAdd(trans, own_bf, brw_bf, src_rf, asgn.value) + # Overwriting an `own` field releases the value it held. Inside a + # constructor the memory is still uninitialized, so nothing is released. + if fe_rf is not None and fe_rf.IsOwn != 0 and self._in_ctor(trans) == 0: + self._release_own_slot(pool, builder, mod, trans, field_ptr) mlirlite.build_store(builder, store_val, field_ptr, vol_fw) own_tgt: int = 0 if fe_rf is not None and fe_rf.IsOwn != 0: own_tgt = 1 diff --git a/App/lib/core/Gen/Call/CallGenLib.vp b/App/lib/core/Gen/Call/CallGenLib.vp index 3c038e0..66bf2b5 100644 --- a/App/lib/core/Gen/Call/CallGenLib.vp +++ b/App/lib/core/Gen/Call/CallGenLib.vp @@ -200,6 +200,10 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): self.Gen.VarGen._FreedMark(src_o) continue self.Gen.TypeGenErr.fatal_error(an_o, f"cannot pass '{nm_o.id}' to an `own` parameter: it is not owned here; move an `own` binding or pass `new ...`", "LifetimeError") + elif an_o.kind() == ast.ASTKind.New: + nw_o: ast.New* = ast.New*(an_o) + if nw_o is not None and nw_o.Source == ast.NEW_SRC_MEMGR: + self.Gen.TypeGenErr.fatal_error(an_o, "cannot pass a `new memgr(...)` allocation to an `own` parameter: pool memory is reclaimed by its manager, not by `free`", "LifetimeError") elif an_o.kind() == ast.ASTKind.UnaryOp: self.Gen.TypeGenErr.fatal_error(an_o, "cannot pass a borrow to an `own` parameter: the callee takes ownership", "LifetimeError") # Reference parameters: a mutable reference only accepts `&mut value` (or a `&mut` diff --git a/App/lib/core/Gen/Func/FuncGenDef.vp b/App/lib/core/Gen/Func/FuncGenDef.vp index a5b4be7..1e875b3 100644 --- a/App/lib/core/Gen/Func/FuncGenDef.vp +++ b/App/lib/core/Gen/Func/FuncGenDef.vp @@ -103,6 +103,9 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): rc_n += 1 ref_buf[rc_n] = char(0) self.Gen.CallGenBase._register_ref_params(name, ref_buf, rc_n) + # Bare source name too: some call-site checks resolve the callee by its written name. + if fd.name is not None and string.strcmp(fd.name, name) != 0: + self.Gen.CallGenBase._register_ref_params(fd.name, ref_buf, rc_n) stdlib.free(ref_buf) return 1 diff --git a/App/lib/core/Gen/Stmt/StmtGenMixin.vp b/App/lib/core/Gen/Stmt/StmtGenMixin.vp index 6f4de75..c3553ca 100644 --- a/App/lib/core/Gen/Stmt/StmtGenMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenMixin.vp @@ -389,6 +389,12 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin): elif tgt.kind() == ast.ASTKind.Attribute: del_at: ast.Attribute* = ast.Attribute*(tgt) if del_at is not None: + # Compiler-managed `own` fields are released with their object; a manual `del` + # would free them twice. + fe_del: GenTypes.FieldEntry* = self.Gen.ExprGenAttr._resolve_attr_field_entry(pool, trans, del_at) + if fe_del is not None and fe_del.IsOwn != 0: + self.Gen.TypeGenErr.fatal_error(del_at, f"cannot `del` the `own` field '.{del_at.attr}': it is released together with its object; assign a new owned value instead", "LifetimeError") + continue del_prop: int = self.Gen.CallGenProp._try_property_deleter(pool, builder, mod, del_at, trans) if del_prop == 0: del_av: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, del_at, None, 0, trans) diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index aba30bb..29ad35c 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -1652,6 +1652,34 @@ def _ParamRefFlags(pool: memhub.MemManager*, detail: str) -> str: return str(buf) +def _CheckRefReturn(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, + a: LspSymbols.LspAnalysis*, ret_ann: str, value: ast.AST*, + errors: list[ast.ParseError*]*) -> None: + """A `-> &T` result may only borrow a reference binding, `self`, or a call.""" + if value is None: return + k: int = value.kind() + if k == ast.ASTKind.Call: return + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(value) + if nm.id is None: return + if string.strcmp(nm.id, "self") == 0: return + det: str = _SymDetailAt(pool, a, value) + if _RefDetail(det) != 0 or _DetailIsOwn(det) != 0: return + _EmitTypeError(pool, doc, value, + f"cannot return `{nm.id}` from a reference-returning function: it lives in this frame; return a reference parameter, `self`, or a call", errors) + return + if k == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(value) + if at.value is not None and at.value.kind() == ast.ASTKind.Name \ + and string.strcmp((ast.Name*)(at.value).id, "self") == 0: return + _EmitTypeError(pool, doc, value, + "cannot return a field of a frame-local value from a reference-returning function", errors) + return + if k == ast.ASTKind.UnaryOp: + _EmitTypeError(pool, doc, value, + "cannot return the address of a frame binding from a reference-returning function: return the reference parameter itself (`return p`)", errors) + + def _ClassSymbolByName(a: LspSymbols.LspAnalysis*, name: str) -> LspSymbols.LspSymbol*: """Class symbol (with its own scope) declared in this document, if any.""" if a is None or name is None or a.symbols is None: @@ -1919,8 +1947,12 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, if own_t != 0: _OwnAdd(pool, tn.id) if own_t != 0 and an.value is not None and an.value.kind() == ast.ASTKind.Name: sn: ast.Name* = ast.Name*(an.value) - if sn.id is not None and _IsOwnBinding(pool, a, an.value) != 0: - _MovedAdd(pool, sn.id) + if sn.id is not None: + if _IsOwnBinding(pool, a, an.value) != 0: + _MovedAdd(pool, sn.id) + else: + _EmitTypeError(pool, doc, an.value, + f"`{tn.id}` is `own`: it can only take an owned value (move another `own` binding, `new heap hlife ...`, or a call returning `own T`); a raw pointer does not transfer ownership", errors) _MovedClear(tn.id) # `r: &T = &x`: the binding holds a borrow, so `x` must outlive it. _BorrowDrop(tn.id) @@ -2132,6 +2164,8 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, continue if k == ast.ASTKind.Return: rt: ast.Return* = ast.Return*(s) + if rt.value is not None and _RefKindOfText(ret_ann) != 0: + _CheckRefReturn(pool, doc, a, ret_ann, rt.value, errors) if rt.value is not None and ret_ann is not None \ and _NullableDetail(ret_ann) == 0 \ and _NullableDetail(_SymDetailAt(pool, a, rt.value)) != 0: