fix(obs): reject frame/own plain-pointer arguments to reference returns

`return id(p)` where the callee takes a reference could hand out a pointer that dies with the frame: `_BorrowSourceReturnOk` returned -1 (nothing to check) for a plain name that is not a reference binding. It now classifies that name with the same owner rule used elsewhere: a reference parameter, `self`, a module global, or a non-owned heap pointer is allowed; an `own`/slife local, a by-value parameter, or a frame pointer is rejected, and unknown cases are rejected conservatively. Reference bindings follow their registered owner, and field/element sources are classified by their root. The LSP flags the same plain-name case when the argument is `own` or initialized from `&local`.
This commit is contained in:
TermiNexus committed 2026-11-05 19:14:55 +08:00
1 parent 2f397eb301
commit c3c2e55fb4
2 files changed
+42 -6

No files matched your search

+17 -5
View File
@@ -1857,10 +1857,22 @@ novirtual class AssignGenMixin:
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 string.strcmp(nm.id, "self") == 0: return 1
# A reference parameter borrows the caller's value; any other parameter is a frame
# copy, so handing it out as a reference dangles.
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 1
return 0
# A reference binding: follow the object it borrows.
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 None: return 0
return self._OwnerOutlivesFrame(trans, fd, own_r)
# A plain name passed where the callee takes a reference is itself the borrowed
# object: it must outlive this frame (own/slife and frame pointers do not).
return self._OwnerOutlivesFrame(trans, fd, nm.id)
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
@@ -1870,7 +1882,7 @@ novirtual class AssignGenMixin:
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:
if tgt.kind() == ast.ASTKind.Attribute or tgt.kind() == ast.ASTKind.Subscript:
root: str = self.Gen.StmtGen._BorrowPathName(trans.Pool, tgt)
if root is None: return -1
dot: str = string.strchr(root, '.')
+25 -1
View File
@@ -2000,6 +2000,24 @@ def _CallReturnsReference(pool: memhub.MemManager*, doc: LspDocuments.LspDocumen
return 1 if s[0] == '&' else 0
def _ArgLeavesFrame(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, node: ast.AST*) -> int:
"""1 when a plain pointer argument cannot outlive the frame: `own` binding or `&local`."""
if node is None or node.kind() != ast.ASTKind.Name: return 0
det: str = _SymDetailAt(pool, a, node)
if _DetailIsOwn(det) != 0: return 1
occ: LspSymbols.LspOccurrence* = _OccAtExpr(a, node)
if occ is None or occ.sym is None or occ.sym.decl is None: return 0
d: ast.AST* = occ.sym.decl
if d.kind() != ast.ASTKind.AnnAssign: return 0
v: ast.AST* = (ast.AnnAssign*)(d).value
if v is None: return 0
if v.kind() == ast.ASTKind.New: return 0
if v.kind() == ast.ASTKind.UnaryOp:
op: int = (ast.UnaryOp*)(v).op
if op == ast.OpKind.AddrOf or op == ast.OpKind.AddrOfMut: return 1
return 0
def _CheckRefReturn(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
a: LspSymbols.LspAnalysis*, ws: LspWorkspace.LspWorkspace*,
table_pool: memhub.MemManager*, ret_ann: str, value: ast.AST*,
@@ -2019,7 +2037,13 @@ def _CheckRefReturn(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
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
if arg is None: continue
if arg.kind() == ast.ASTKind.Name:
if _ArgLeavesFrame(pool, a, arg) != 0:
_EmitTypeError(pool, doc, arg,
f"cannot return this call: argument {ai} is released with this frame; pass a reference parameter, `self`, or a non-owned heap value", errors)
continue
if 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