diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index ad3c3a6..3ddb2e8 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -1768,16 +1768,23 @@ novirtual class AssignGenMixin: self.Gen.TypeGenErr.fatal_error(value, "cannot return this call from a reference-returning function: the callee's return type cannot be verified (no resolvable `.vpi` entry)", "LifetimeError") return 0 # The callee returns a reference to one of its borrowed arguments: that argument must - # outlive this frame (a reference parameter / `self` / a heap-owned value). + # outlive this frame (a reference parameter / `self` / a static or non-owned heap + # value). A precise summary checks only that argument; otherwise check them all. cargs: list[ast.AST*]* = (ast.Call*)(value).args - if cargs is not None: + rb_k: int = self.Gen.CallGenBase.ret_borrow_of_call(trans, ast.Call*(value)) + if rb_k >= 0 and cargs is not None and rb_k < int(len(cargs)): + arg_k: ast.AST* = cargs.get(sizet(rb_k)) + if self._BorrowSourceReturnOk(trans, fd, arg_k) == 0: + self.Gen.TypeGenErr.fatal_error(arg_k, f"cannot return this call: argument {rb_k + 1} borrows a value that dies with this frame; pass a reference parameter, `self`, or a static/non-owned heap value", "LifetimeError") + return 0 + elif rb_k != -2 and cargs is not None: ai: int = 0 while ai < int(len(cargs)): arg: ast.AST* = cargs.get(sizet(ai)) ai += 1 ok_arg: int = self._BorrowSourceReturnOk(trans, fd, arg) if ok_arg == 0: - self.Gen.TypeGenErr.fatal_error(arg, f"cannot return this call: argument {ai} borrows a value that dies with this frame; pass a reference parameter, `self`, or a heap-owned value", "LifetimeError") + self.Gen.TypeGenErr.fatal_error(arg, f"cannot return this call: argument {ai} borrows a value that dies with this frame; pass a reference parameter, `self`, or a static/non-owned heap value", "LifetimeError") return 0 return 0 if value.kind() == ast.ASTKind.Name: @@ -1808,8 +1815,9 @@ novirtual class AssignGenMixin: 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): + if slot_o is not None and self.Gen.VarGen._SlifeOwned(slot_o) == 0 \ + and self.Gen.VarGen.get_var_life(trans.SymTab, own_r) == GenTypes.LIFE_HEAP: + # A plain (non-owned) heap object is not freed when this frame ends. 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 @@ -1831,6 +1839,17 @@ novirtual class AssignGenMixin: 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 + # _BorrowOwnerForArg - borrowed owner of a call argument (`&x`, `&*p`, or a reference + # binding); None when the argument is not a borrow. + def _BorrowOwnerForArg(self, trans: HT.Translator*, arg: ast.AST*) -> str: + if arg is None: return None + ob: str = self._BorrowOwnerName(arg) + if ob is None and arg.kind() == ast.ASTKind.Name: + an: ast.Name* = ast.Name*(arg) + if an.id is not None and self.Gen.VarGen.var_ref_kind(trans.SymTab, an.id) != 0: + ob = self.Gen.StmtGen._BorrowOwnerOf(trans, an.id) + return ob + # _BorrowSourceReturnOk - 1 = a borrow argument stays valid past this frame, 0 = it does not, # -1 = the expression is not a borrow (nothing to check). def _BorrowSourceReturnOk(self, trans: HT.Translator*, fd: ast.FunctionDef*, expr: ast.AST*) -> int: @@ -1881,9 +1900,11 @@ novirtual class AssignGenMixin: if par is not None and self.Gen.TypeGenResolve.ref_kind(par.annotation) != 0: return 1 return 0 slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, name) - if slot is not None and (self.Gen.VarGen._SlifeOwned(slot) != 0 \ - or self.Gen.VarGen.get_var_life(trans.SymTab, name) == GenTypes.LIFE_HEAP): - return 1 + if slot is not None: + # An `own`/slife binding is released when this frame ends: borrowing it must not + # escape (the pointer would dangle after the free). A non-owned heap pointer survives. + if self.Gen.VarGen._SlifeOwned(slot) != 0: return 0 + if self.Gen.VarGen.get_var_life(trans.SymTab, name) == GenTypes.LIFE_HEAP: return 1 return 0 # _IsStaticName - 1 for module globals / nonlocal storage (lifetime = whole program). @@ -1894,6 +1915,59 @@ novirtual class AssignGenMixin: if GenApi.is_nonlocal_name(trans, name) != 0: return 1 return 0 + # ret_borrow_index - which parameter a `-> &T` function returns a borrow of: + # >=0 parameter index (including `self` for methods), -2 = `self`, -3 = unknown/mixed, + # -1 = the function does not return a reference. + def ret_borrow_index(self, fd: ast.FunctionDef*) -> int: + if fd is None or self.Gen.TypeGenResolve.ref_kind(fd.returns) == 0: return -1 + found: int = -3 + cnt: int = 0 + ok: int = self._ScanRetBorrows(fd, fd.children, &found, &cnt, 0) + if ok == 0: return -3 + if cnt == 0: return -3 + return found + + # _ScanRetBorrows - walk a statement list (and nested blocks) collecting returned borrows. + def _ScanRetBorrows(self, fd: ast.FunctionDef*, stmts: list[ast.AST*]*, found: int*, + cnt: int*, depth: int) -> int: + if stmts is None or depth > 12: return 1 + i: int = 0 + while i < int(len(stmts)): + st: ast.AST* = stmts.get(sizet(i)) + i += 1 + if st is None: continue + if st.kind() == ast.ASTKind.Return: + rt: ast.Return* = ast.Return*(st) + idx: int = self._ReturnBorrowIndex(fd, rt.value) + if idx == -3: return 0 + if cnt[0] == 0: found[0] = idx + elif found[0] != idx: return 0 + cnt[0] += 1 + continue + if st.children is not None: + if self._ScanRetBorrows(fd, st.children, found, cnt, depth + 1) == 0: return 0 + return 1 + + # _ReturnBorrowIndex - parameter borrowed by one return value (-3 = cannot tell). + def _ReturnBorrowIndex(self, fd: ast.FunctionDef*, value: ast.AST*) -> int: + if value is None: return -3 + if value.kind() == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(value) + if nm.id is None: return -3 + if string.strcmp(nm.id, "self") == 0: return -2 + 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 pi + return -3 + if value.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(value) + if at.value is not None and at.value.kind() == ast.ASTKind.Name: + bn: ast.Name* = ast.Name*(at.value) + if bn.id is not None and string.strcmp(bn.id, "self") == 0: return -2 + return -3 + return -3 + # _ParamAt - parameter node at a 0-based position (None when out of range). def _ParamAt(self, fd: ast.FunctionDef*, idx: int) -> ast.Arg*: if fd is None or idx < 0: return None @@ -3024,22 +3098,31 @@ 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). + # A call returning `&T` borrows one of its arguments. When the callee has a + # return-borrow summary only that argument is registered; otherwise every + # borrowed argument is (conservative). 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: + rb_k: int = self.Gen.CallGenBase.ret_borrow_of_call(trans, ast.Call*(aa.value)) + if rb_k >= 0 and c_args is not None and rb_k < int(len(c_args)): + arg_k: ast.AST* = c_args.get(sizet(rb_k)) + ob_k: str = self._BorrowOwnerForArg(trans, arg_k) + if ob_k is not None: + self.Gen.StmtGen._BorrowAdd(trans, ob_k, nm.id, src_rk, arg_k) + elif rb_k == -2: + # The result borrows the receiver: `c.get(...)` -> `c`. + f_k: ast.AST* = (ast.Call*)(aa.value).func + if f_k is not None and f_k.kind() == ast.ASTKind.Attribute: + ob_r: str = self._BorrowOwnerName((ast.Attribute*)(f_k).value) + if ob_r is not None: + self.Gen.StmtGen._BorrowAdd(trans, ob_r, nm.id, src_rk, aa.value) + elif 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) + ob: str = self._BorrowOwnerForArg(trans, arg_b) 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. diff --git a/App/lib/core/Gen/Call/CallGenBase.vp b/App/lib/core/Gen/Call/CallGenBase.vp index cac7028..57ea9d4 100644 --- a/App/lib/core/Gen/Call/CallGenBase.vp +++ b/App/lib/core/Gen/Call/CallGenBase.vp @@ -114,6 +114,11 @@ novirtual class CallGenBase: # byte-per-parameter '0' (none) / '1' (read-only) / '2' (mutable). RefParams: hashtable.HashTable* = None + # --- return-borrow summary (`-> &T` returning a borrow of which parameter): + # key = symbol name or "Class.method" -> decimal code (>=0 param index incl. self, + # -2 = borrows `self`, -3 = unknown/mixed). Missing = no summary. + RetBorrows: hashtable.HashTable* = None + def InitCallGen(self) -> int: """Initialize the call mixin fields (inlined by subclass constructors since NoVTable does not forward inherited symbols)""" if self is None: return 1 @@ -225,6 +230,70 @@ novirtual class CallGenBase: if i != idx: return 0 return int(kinds[idx]) - 48 + # _register_ret_borrow - store a function's return-borrow code (PermPool-persistent). + def _register_ret_borrow(self, key: str, code: int) -> int: + if key is None: return 0 + perm: memhub.MemBuddy* = CompilerFactory.factory.PermPool + if perm is None: return 0 + if self.RetBorrows is None: + self.RetBorrows = hashtable.HashTable(perm) + if self.RetBorrows is None: return 0 + k: char* = perm.alloc(string.strlen(key) + 1) + if k is None: return 0 + string.strcpy(k, key) + v: char* = perm.alloc(16) + if v is None: return 0 + viperlib.snprintf(v, 16, "%d", code) + self.RetBorrows[k] = v + return 1 + + # _ret_borrow_code - stored return-borrow code for a key (-1 when absent). + def _ret_borrow_code(self, key: str) -> int: + if self.RetBorrows is None or key is None: return -1 + v: rawptr = self.RetBorrows.__getitem__(key) + if v is None: return -1 + s: str = rawptr(v) + if s is None: return -1 + sign: int = 1 + p: str = s + if p[0] == '-': + sign = -1 + p += 1 + n: int = 0 + while p[0] != 0: + c: int = int(p[0]) + if c < 48 or c > 57: break + n = n * 10 + (c - 48) + p += 1 + return sign * n + + # ret_borrow_of_call - which argument a reference result borrows: + # >=0 argument index (receiver excluded), -2 = borrows the receiver/`self`, + # -1 = no summary (caller should fall back to a conservative rule). + def ret_borrow_of_call(self, trans: HT.Translator*, call: ast.Call*) -> int: + if trans is None or call is None or call.func is None: return -1 + f: ast.AST* = call.func + if f.kind() == ast.ASTKind.Name: + fname: str = (ast.Name*)(f).id + return self._ret_borrow_code(fname) + if f.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = ast.Attribute*(f) + if at.attr is None or at.value is None: return -1 + if at.value.kind() != ast.ASTKind.Name: return -1 + cls: str = self._receiver_class(trans, at.value) + if cls is None: return -1 + n_key: sizet = string.strlen(cls) + string.strlen(at.attr) + 2 + buf: str = trans.Pool.alloc(n_key) + if buf is None: return -1 + viperlib.snprintf(buf, n_key, "%s.%s", cls, at.attr) + code: int = self._ret_borrow_code(buf) + if code == -2: return -2 + if code >= 0: + # Method summary indexes include `self` at 0; call arguments exclude it. + return code - 1 if code > 0 else 0 + return -1 + return -1 + # _register_nullable_params - store per-parameter nullability flags for a callee symbol name. def _register_nullable_params(self, name: char*, flags: char*, count: int) -> int: """Store per-parameter '0'/'1' nullable flags (PermPool-persistent) keyed by callee symbol name.""" diff --git a/App/lib/core/Gen/Class/ClassGenVTable.vp b/App/lib/core/Gen/Class/ClassGenVTable.vp index 41bfca0..8184b4f 100644 --- a/App/lib/core/Gen/Class/ClassGenVTable.vp +++ b/App/lib/core/Gen/Class/ClassGenVTable.vp @@ -776,6 +776,14 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase): rc_m += 1 ref_buf_m[rc_m] = char(0) self.Gen.CallGenBase._register_ref_params(mangled_name, ref_buf_m, rc_m) + # Return-borrow summary keyed by `Class.method` (index includes `self`). + rb_m: int = self.Gen.AssignGen.ret_borrow_index(fd) + if rb_m != -1 and class_name is not None and method_name is not None: + rb_key: sizet = string.strlen(class_name) + string.strlen(method_name) + 2 + rb_buf: str = pool.alloc(rb_key) + if rb_buf is not None: + viperlib.snprintf(rb_buf, rb_key, "%s.%s", class_name, method_name) + self.Gen.CallGenBase._register_ret_borrow(rb_buf, rb_m) # *args variadic: set IsVarArg=1 so the signature carries the ... suffix (the body # pipeline allocates the va_list buffer, same as a module-level function). diff --git a/App/lib/core/Gen/Func/FuncGenDef.vp b/App/lib/core/Gen/Func/FuncGenDef.vp index 1e875b3..e3ac332 100644 --- a/App/lib/core/Gen/Func/FuncGenDef.vp +++ b/App/lib/core/Gen/Func/FuncGenDef.vp @@ -107,6 +107,12 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): 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-borrow summary: which parameter a `-> &T` result borrows. + rb: int = self.Gen.AssignGen.ret_borrow_index(fd) + if rb != -1: + self.Gen.CallGenBase._register_ret_borrow(name, rb) + if fd.name is not None and string.strcmp(fd.name, name) != 0: + self.Gen.CallGenBase._register_ret_borrow(fd.name, rb) return 1 # ============================================================ diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index ac2d273..87ed550 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -1703,8 +1703,11 @@ def _CheckRefReturn(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, if uo.op != ast.OpKind.AddrOf and uo.op != ast.OpKind.AddrOfMut: continue if uo.operand is None or uo.operand.kind() != ast.ASTKind.Name: continue det_a: str = _SymDetailAt(pool, a, uo.operand) - if _RefDetail(det_a) != 0 or _DetailIsOwn(det_a) != 0: continue - if det_a is not None and string.strchr(det_a, '*') is not None: continue + if _RefDetail(det_a) != 0: continue + # A non-owned pointer (e.g. `new heap hlife`) survives the frame; an `own` + # binding is released at exit, so a borrow of it must not escape. + if det_a is not None and string.strchr(det_a, '*') is not None \ + and _DetailIsOwn(det_a) == 0: continue _EmitTypeError(pool, doc, arg, f"cannot return this call: argument {ai} borrows a value that dies with this frame; pass a reference parameter, `self`, or a heap-owned value", errors) return