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.
This commit is contained in:
1 parent
1d668497a5
commit
5c6c6c9619
3 files changed
+148
-7
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user