feat(obs): precise return-borrow summaries and owned-local escape check
Each `-> &T` / `-> &mut T` function now records which parameter it borrows (a parameter index, `self`, or unknown) in a return-borrow registry, keyed by symbol and by `Class.method`. A reference initialized from a call registers only the summarized argument (the receiver for methods), and `return f(...)` checks only that argument; callees without a summary keep the conservative "all borrowed arguments" fallback. Borrowing a compiler-owned (`own`) local cannot escape the frame any more: `_OwnerOutlivesFrame` rejects `own`/slife bindings and only accepts reference parameters, `self`, statics, and non-owned heap pointers, so `return id(&ownLocal)` and `return refBinding` are rejected while `return id(&heapPtr)` still passes. The LSP mirrors the owned-local rule.
This commit is contained in:
1 parent
d4c1e3f535
commit
b78cddc401
5 files changed
+188
-19
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
# ============================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user