From 5c6c6c9619639d41ccacf06074a594ea1a557929 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 5 Nov 2026 17:17:49 +0800 Subject: [PATCH] fix(obs): track field/element borrows and check call returns A borrow of `&obj.field` / `&arr[i]` now registers the root binding, so deleting that object is blocked while the reference lives. A reference binding may only be initialized by a call whose return annotation is `&T` / `&mut T` (resolved through the module's vpi); a `-> U` or `-> U*` call is rejected. Returning a call from a reference-returning function checks its borrowed arguments: borrowing a by-value parameter or a frame-local value is rejected, while a reference parameter, `self`, or a heap-owned value passes. The LSP mirrors all three rules. --- App/lib/core/Gen/AssignGenMixin.vp | 90 ++++++++++++++++++++++++++-- App/lib/core/Gen/Call/CallGenBase.vp | 20 +++++++ App/lib/lsp/LspChecks.vp | 45 +++++++++++++- 3 files changed, 148 insertions(+), 7 deletions(-) diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index af9bcb1..e2c2227 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -1066,6 +1066,14 @@ novirtual class AssignGenMixin: return self._BorrowOwnerName(uo_b.operand) if uo_b.op == ast.OpKind.Deref: return self._BorrowOwnerName(uo_b.operand) + # `&obj.field` / `&arr[i]`: the borrowed storage belongs to the root binding, so the + # borrow is tracked against it (`del obj` must wait until the reference is gone). + if expr.kind() == ast.ASTKind.Attribute: + at_b: ast.Attribute* = ast.Attribute*(expr) + return self._BorrowOwnerName(at_b.value) + if expr.kind() == ast.ASTKind.Subscript: + sb_b: ast.Subscript* = ast.Subscript*(expr) + return self._BorrowOwnerName(sb_b.value) return None def _move_out_assigned_source(self, trans: HT.Translator*, rhs: ast.AST*, @@ -1750,7 +1758,20 @@ novirtual class AssignGenMixin: 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.Call: + # 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). + cargs: list[ast.AST*]* = (ast.Call*)(value).args + if 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") + return 0 + return 0 if value.kind() == ast.ASTKind.Name: nm: ast.Name* = ast.Name*(value) if nm.id is None: return 0 @@ -1787,6 +1808,57 @@ 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 + # _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: + if trans is None or fd is None or expr is None: return -1 + if expr.kind() == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(expr) + if nm.id is None: return -1 + if self.Gen.VarGen.var_ref_kind(trans.SymTab, nm.id) == 0: return -1 + own_r: str = self.Gen.StmtGen._BorrowOwnerOf(trans, nm.id) + if own_r is None: return -1 + return self._OwnerOutlivesFrame(trans, fd, own_r) + if expr.kind() != ast.ASTKind.UnaryOp: return -1 + uo: ast.UnaryOp* = ast.UnaryOp*(expr) + if uo.op != ast.OpKind.AddrOf and uo.op != ast.OpKind.AddrOfMut: return -1 + tgt: ast.AST* = uo.operand + if tgt is None: return -1 + if tgt.kind() == ast.ASTKind.Name: + nm2: ast.Name* = ast.Name*(tgt) + if nm2.id is None: return -1 + return self._OwnerOutlivesFrame(trans, fd, nm2.id) + if tgt.kind() == ast.ASTKind.Attribute: + root: str = self.Gen.StmtGen._BorrowPathName(trans.Pool, tgt) + if root is None: return -1 + dot: str = string.strchr(root, '.') + base: str = root + if dot is not None: + n_base: sizet = sizet(uint64(dot) - uint64(root)) + buf: str = trans.Pool.alloc(n_base + 1) + if buf is None: return -1 + string.strncpy(buf, root, n_base) + buf[n_base] = '\0' + base = buf + if string.strcmp(base, "self") == 0: return 1 + return self._OwnerOutlivesFrame(trans, fd, base) + return -1 + + # _OwnerOutlivesFrame - 1 when `name` is a reference parameter, `self`, or a heap-owned value. + def _OwnerOutlivesFrame(self, trans: HT.Translator*, fd: ast.FunctionDef*, name: str) -> int: + if name is None: return -1 + if string.strcmp(name, "self") == 0: return 1 + pi: int = self._ParamIndexByName(fd, name) + 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 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 + return 0 + # _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 @@ -2894,10 +2966,18 @@ novirtual class AssignGenMixin: src_rk = self.Gen.VarGen.var_ref_kind(trans.SymTab, nm_i.id) if src_rk != 0: ok_ref = 1 elif aa.value.kind() == ast.ASTKind.Call: - # `find()` / `get()` / iterator APIs hand out a borrow of something the - # callee owns; the call result is a reference value. - ok_ref = 1 - src_rk = rk_decl + # `find()` / `get()` / iterator APIs hand out a borrow; only a callee + # whose return annotation is `&T` / `&mut T` may initialize one. + call_rk: int = self.Gen.CallGenBase.ret_annot_ref_kind(trans, ast.Call*(aa.value)) + if call_rk < 0: + # Unknown callee (foreign/extern): keep the permissive rule. + ok_ref = 1 + src_rk = rk_decl + elif call_rk == 0: + self.Gen.TypeGenErr.fatal_error(aa.value, f"`{nm.id}` is a reference: the call returns a value/pointer, not a reference; only a function returning `&T` / `&mut T` can initialize it", "TypeError") + else: + ok_ref = 1 + src_rk = call_rk if ok_ref == 0: self.Gen.TypeGenErr.fatal_error(aa.value, f"a reference must be initialized from a borrow: write `{nm.id}: &T = &value`, `&mut value`, or call a function returning `&T`", "TypeError") if rk_decl == 2 and src_rk == 1: diff --git a/App/lib/core/Gen/Call/CallGenBase.vp b/App/lib/core/Gen/Call/CallGenBase.vp index 185a674..f5c4224 100644 --- a/App/lib/core/Gen/Call/CallGenBase.vp +++ b/App/lib/core/Gen/Call/CallGenBase.vp @@ -1320,6 +1320,26 @@ novirtual class CallGenBase: if ret_ty is None: ret_ty = self._resolve_generic_annot_str(trans, pool, annot) return ret_ty + # ret_annot_ref_kind - reference kind of a call's return annotation: 0 = value/pointer, + # 1 = `&T`, 2 = `&mut T`, -1 = callee not resolvable (foreign/extern/unknown). + def ret_annot_ref_kind(self, trans: HT.Translator*, call: ast.Call*) -> int: + if trans is None or call is None or call.func is None: return -1 + if call.func.kind() != ast.ASTKind.Name: return -1 + fname: str = (ast.Name*)(call.func).id + if fname is None: return -1 + sha1: char* = trans.ModuleSha1 + if sha1 is None: return -1 + if self.Gen.CallGenBase.TempDir is not None and self.Gen.CallGenBase.TempDirLen > 0: + VpiReader._ensure_vpi_ret_annot_loaded(sha1, self.Gen.CallGenBase.TempDir) + annot: char* = VpiReader._lookup_vpi_ret_annot(sha1, None, fname) + if annot is None: return -1 + s: str = annot + while string.strlen(s) > 0 and s[0] == ' ': s += 1 + if string.strlen(s) == 0: return 0 + if s[0] != '&': return 0 + if string.strncmp(s, "&mut ", 5) == 0: return 2 + return 1 + def _vpi_module_defines(self, mod_sha1: char*, func_name: char*) -> int: """Return 1 when the module's .vpi declares a module-level function with this name.""" if mod_sha1 is None or func_name is None: return 0 diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index 29ad35c..25baf4a 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -1158,6 +1158,10 @@ def _BorrowOwnerText(expr: ast.AST*) -> str: uo: ast.UnaryOp* = ast.UnaryOp*(expr) if uo.op == ast.OpKind.AddrOf or uo.op == ast.OpKind.AddrOfMut or uo.op == ast.OpKind.Deref: return _BorrowOwnerText(uo.operand) + if expr.kind() == ast.ASTKind.Attribute: + return _BorrowOwnerText((ast.Attribute*)(expr).value) + if expr.kind() == ast.ASTKind.Subscript: + return _BorrowOwnerText((ast.Subscript*)(expr).value) return None @@ -1652,14 +1656,45 @@ def _ParamRefFlags(pool: memhub.MemManager*, detail: str) -> str: return str(buf) +def _CallReturnsReference(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, + a: LspSymbols.LspAnalysis*, ws: LspWorkspace.LspWorkspace*, + table_pool: memhub.MemManager*, call: ast.AST*) -> int: + """1 = callee returns `&T` / `&mut T`, 0 = not a reference, -1 = unknown.""" + if call is None or call.kind() != ast.ASTKind.Call: return -1 + t: LspIndex.LspTarget* = _TargetAtNode(ws, pool, table_pool, a, doc, (ast.Call*)(call).func) + if t is None or t.detail is None: return -1 + arrow: str = string.strstr(t.detail, "->") + if arrow is None: return -1 + s: str = arrow + 2 + while string.strlen(s) > 0 and s[0] == ' ': s += 1 + if string.strlen(s) == 0: return -1 + return 1 if s[0] == '&' else 0 + + 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: + if value.kind() == ast.ASTKind.Call: + args: list[ast.AST*]* = (ast.Call*)(value).args + if args is not None: + ai: int = 0 + while ai < int(len(args)): + arg: ast.AST* = args.get(sizet(ai)) + ai += 1 + if arg is None or arg.kind() != ast.ASTKind.UnaryOp: continue + uo: ast.UnaryOp* = ast.UnaryOp*(arg) + 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 + _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 + if value.kind() == ast.ASTKind.Name: nm: ast.Name* = ast.Name*(value) if nm.id is None: return if string.strcmp(nm.id, "self") == 0: return @@ -1959,6 +1994,12 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, tk: int = _RefKindOfText(_SymDetailAt(pool, a, an.target)) if tk != 0 and an.value is not None: sk: int = _RefExprKind(an.value) + if an.value.kind() == ast.ASTKind.Call: + cr: int = _CallReturnsReference(pool, doc, a, ws, table_pool, an.value) + if cr == 0: + _EmitTypeError(pool, doc, an.value, + f"`{tn.id}` is a reference: the call returns a value/pointer, not a reference; only a function returning `&T` / `&mut T` can initialize it", errors) + sk = -1 sn_r: str = None if sk == 0: if _RefKindOfText(_SymDetailAt(pool, a, an.value)) != 0: