feat(lsp): mirror own parameters, reference borrows and del

Call arguments are checked against `own T` parameters (a borrow or a non-owned binding is reported, an owned argument is moved); reference-field and reference-binding assignments register a borrow so `del` reports a still-live reference; `del` of a reference binding itself is reported; own parameters count as owned bindings for the move-on-assign rules.
This commit is contained in:
TermiNexus committed 2026-11-04 23:03:24 +08:00
1 parent e7ef23b432
commit 88b3724303
1 file changed
+226 -6
+226 -6
View File
@@ -28,6 +28,10 @@ _expr_count: int = 0
# Move analysis for `own` bindings: names declared own, and names whose value was moved.
_own_names: list[str]* = None
_moved_names: list[str]* = None
# OBS reference borrows inside the current function: parallel name lists, where
# `_borrow_owners[i]` is borrowed by `_borrow_borrowers[i]`.
_borrow_owners: list[str]* = None
_borrow_borrowers: list[str]* = None
def _NameListFind(lst: list[str]*, name: str) -> int:
@@ -41,9 +45,11 @@ def _NameListFind(lst: list[str]*, name: str) -> int:
def _MoveReset(pool: memhub.MemManager*) -> None:
global _own_names, _moved_names
global _own_names, _moved_names, _borrow_owners, _borrow_borrowers
_own_names = list[str](pool, 8)
_moved_names = list[str](pool, 8)
_borrow_owners = list[str](pool, 8)
_borrow_borrowers = list[str](pool, 8)
def _OwnHas(name: str) -> int:
@@ -72,6 +78,52 @@ def _MovedIs(name: str) -> int:
return 1 if _NameListFind(_moved_names, name) >= 0 else 0
def _BorrowAdd(pool: memhub.MemManager*, owner: str, borrower: str) -> None:
"""Record that reference `borrower` refers to `owner` in the current function."""
if owner is None or borrower is None: return
if _borrow_owners is None: _MoveReset(pool)
if _borrow_owners is None: return
_borrow_owners.append(owner)
_borrow_borrowers.append(borrower)
def _BorrowDrop(borrower: str) -> None:
"""Forget the borrows held by `borrower` (it was reassigned or released)."""
if borrower is None or _borrow_borrowers is None: return
i: int = 0
while i < int(len(_borrow_borrowers)):
v: str = _borrow_borrowers.get(sizet(i))
if v is not None and string.strcmp(v, borrower) == 0:
_borrow_borrowers.set(sizet(i), "")
_borrow_owners.set(sizet(i), "")
i += 1
def _BorrowOwnerOf(borrower: str) -> str:
"""Owner borrowed by a reference binding (None when it holds no registered borrow)."""
if borrower is None or _borrow_borrowers is None: return None
i: int = 0
while i < int(len(_borrow_borrowers)):
v: str = _borrow_borrowers.get(sizet(i))
if v is not None and string.strcmp(v, borrower) == 0:
return _borrow_owners.get(sizet(i))
i += 1
return None
def _BorrowLiveBorrower(owner: str) -> str:
"""A borrower of `owner` that is still in scope (None when none)."""
if owner is None or _borrow_owners is None: return None
i: int = 0
while i < int(len(_borrow_owners)):
v: str = _borrow_owners.get(sizet(i))
if v is not None and string.strcmp(v, owner) == 0:
b: str = _borrow_borrowers.get(sizet(i))
if b is not None and string.strlen(b) > 0: return b
i += 1
return None
def _AnnIsPointerLike(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
an: ast.AnnAssign*) -> int:
"""1 when the annotation is a pointer/own/optional type (so it can hold an own value)."""
@@ -1020,6 +1072,17 @@ def _CheckAssignValue(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
_EmitTypeError(pool, doc, value, msg, errors)
def _BorrowOwnerText(expr: ast.AST*) -> str:
"""Borrowed variable of `r = &x` / `&*p` (None when the source is not a binding)."""
if expr is None: return None
if expr.kind() == ast.ASTKind.Name: return (ast.Name*)(expr).id
if expr.kind() == ast.ASTKind.UnaryOp:
uo: ast.UnaryOp* = ast.UnaryOp*(expr)
if uo.op == ast.OpKind.AddrOf or uo.op == ast.OpKind.Deref:
return _BorrowOwnerText(uo.operand)
return None
def _RefDetail(det: str) -> int:
"""1 when an annotation text is a reference (`&T` / `&mut T`, optional trailing `?`)."""
if det is None: return 0
@@ -1030,6 +1093,27 @@ def _RefDetail(det: str) -> int:
return 1 if s[0] == '&' else 0
def _DetailIsOwn(det: str) -> int:
"""1 when an annotation text starts with the `own` prefix (`own T` / `own (T)`)."""
if det is None: return 0
s: str = det
while string.strlen(s) > 0 and s[0] == ' ': s += 1
if string.strlen(s) < 3: return 0
if s[0] != 'o' or s[1] != 'w' or s[2] != 'n': return 0
if string.strlen(s) == 3: return 1
c4: char = s[3]
return 1 if c4 == ' ' or c4 == '(' or c4 == '[' else 0
def _IsOwnBinding(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, node: ast.AST*) -> int:
"""1 when `node` names an `own` binding (a local `own T` or an `own` parameter)."""
if node is None or node.kind() != ast.ASTKind.Name: return 0
nm: ast.Name* = ast.Name*(node)
if nm.id is None: return 0
if _OwnHas(nm.id) != 0: return 1
return _DetailIsOwn(_SymDetailAt(pool, a, node))
def _NullableDetail(det: str) -> int:
"""1 when an annotation text is the nullable `T?` form."""
if det is None: return 0
@@ -1188,7 +1272,7 @@ def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
if k == ast.ASTKind.Name:
nm_m: ast.Name* = ast.Name*(e)
if nm_m.id is not None and _MovedIs(nm_m.id) != 0:
_EmitTypeError(pool, doc, e, f"cannot use '{nm_m.id}': its value was moved (ownership transferred to another binding)", errors)
_EmitTypeError(pool, doc, e, f"cannot use '{nm_m.id}': its value was moved or released (ownership transferred away)", errors)
elif nm_m.id is not None and _IsKnownPlainName(nm_m.id) == 0:
occ_u: LspSymbols.LspOccurrence* = _OccAtExpr(a, e)
if occ_u is not None and occ_u.sym is None and occ_u.head is None and occ_u.builtin_type == 0:
@@ -1207,13 +1291,15 @@ def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
return
if k == ast.ASTKind.Call:
call: ast.Call* = ast.Call*(e)
_CheckCall(pool, doc, a, ws, table_pool, call, errors)
# Walk the arguments first: `_CheckCall` may mark an `own` argument as moved, and the
# argument itself is still a valid use at the call site.
_WalkChecksExprs(pool, doc, a, ws, table_pool, call.func, errors)
if call.args is not None:
i: int = 0
while i < int(len(call.args)):
_WalkChecksExprs(pool, doc, a, ws, table_pool, call.args.get(sizet(i)), errors)
i += 1
_CheckCall(pool, doc, a, ws, table_pool, call, errors)
return
if k == ast.ASTKind.BinOp:
bo: ast.BinOp* = ast.BinOp*(e)
@@ -1369,6 +1455,66 @@ def _ParamNullFlags(pool: memhub.MemManager*, detail: str) -> str:
return str(buf)
def _ParamOwnFlags(pool: memhub.MemManager*, detail: str) -> str:
"""Per-parameter ownership from a `def f(a: T, p: own U)` header.
'1' = annotation starts with `own`, '0' = annotation without it, '2' = no annotation.
"""
if detail is None:
return None
lp: str = string.strchr(detail, '(')
if lp is None:
return None
n: sizet = string.strlen(detail)
start: sizet = sizet(uint64(lp) - uint64(detail)) + 1
if start >= n:
return None
buf: bytes = pool.alloc(n + 2)
if buf is None:
return None
cnt: int = 0
i: sizet = start
seg_len: int = 0
depth: int = 0
in_ann: int = 0
ann_pos: int = 0
seg_own: int = 0
while i < n:
c: int = int(detail[i])
if depth == 0 and (c == ',' or c == ')'):
if seg_len > 0:
if in_ann == 0: buf[cnt] = 50 # '2'
elif seg_own != 0: buf[cnt] = 49 # '1'
else: buf[cnt] = 48 # '0'
cnt += 1
if c == ')':
break
seg_len = 0
in_ann = 0
ann_pos = 0
seg_own = 0
i += 1
continue
if c == '(' or c == '[' or c == '{':
depth += 1
elif c == ')' or c == ']' or c == '}':
if depth > 0: depth -= 1
elif c == ':' and depth == 0:
in_ann = 1
elif c != ' ' and c != '\t':
seg_len += 1
if in_ann != 0 and c != ' ' and c != '\t' and c != ':':
if ann_pos == 0 and c == 'o' and i + 2 < n \
and detail[i + 1] == 'w' and detail[i + 2] == 'n':
nxt: int = 0 if i + 3 >= n else int(detail[i + 3])
if nxt == 32 or nxt == 40 or nxt == 91:
seg_own = 1
ann_pos += 1
i += 1
buf[cnt] = 0
return str(buf)
def _ClassSymbolByName(a: LspSymbols.LspAnalysis*, name: str) -> LspSymbols.LspSymbol*:
"""Class symbol (with its own scope) declared in this document, if any."""
if a is None or name is None or a.symbols is None:
@@ -1496,6 +1642,7 @@ def _CheckCall(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
if fname is None:
fname = "function"
pflags: str = _ParamNullFlags(pool, t.detail)
pown: str = _ParamOwnFlags(pool, t.detail)
if argc > param_n and sig.has_vararg == 0:
msg: bytes = pool.alloc(sizet(256))
if msg is not None:
@@ -1536,6 +1683,25 @@ def _CheckCall(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
"argument %d of '%s' is not nullable: a nullable value may be None; declare it as `T?` or unwrap it with `match` first",
ai + 1, fname)
_EmitTypeError(pool, doc, arg, str(msg_n), errors)
# Ownership: an `own` parameter takes the argument's ownership. A local `own` binding is
# moved in; `new` / calls are already owned; a borrow or a plain value is rejected.
if pown is not None and arg is not None and sizet(pi) < string.strlen(pown) \
and int(pown[sizet(pi)]) == 49:
if arg.kind() == ast.ASTKind.UnaryOp \
and (ast.UnaryOp*)(arg).op == ast.OpKind.AddrOf:
_EmitTypeError(pool, doc, arg,
"cannot pass a borrow to an `own` parameter: the callee takes ownership", errors)
elif arg.kind() == ast.ASTKind.Name:
an_o: ast.Name* = ast.Name*(arg)
if an_o.id is not None and _MovedIs(an_o.id) == 0:
if _DetailIsOwn(_SymDetailAt(pool, a, arg)) != 0:
_MovedAdd(pool, an_o.id)
else:
msg_o: bytes = pool.alloc(sizet(320))
if msg_o is not None:
stdio.snprintf(msg_o, sizet(320),
"cannot pass '%s' to an `own` parameter: it is not owned here; move an `own` binding or pass `new ...`", an_o.id)
_EmitTypeError(pool, doc, arg, str(msg_o), errors)
ai += 1
@@ -1605,8 +1771,14 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
if own_t != 0: _OwnAdd(pool, tn.id)
if own_t != 0 and an.value is not None and an.value.kind() == ast.ASTKind.Name:
sn: ast.Name* = ast.Name*(an.value)
if sn.id is not None and _OwnHas(sn.id) != 0: _MovedAdd(pool, sn.id)
if sn.id is not None and _IsOwnBinding(pool, a, an.value) != 0:
_MovedAdd(pool, sn.id)
_MovedClear(tn.id)
# `r: &T = &x`: the binding holds a borrow, so `x` must outlive it.
_BorrowDrop(tn.id)
if _RefDetail(_SymDetailAt(pool, a, an.target)) != 0 and an.value is not None:
own_d: str = _BorrowOwnerText(an.value)
if own_d is not None: _BorrowAdd(pool, own_d, tn.id)
continue
if k == ast.ASTKind.Assign:
asn: ast.Assign* = ast.Assign*(s)
@@ -1638,6 +1810,17 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
stdio.snprintf(msg_f, sizet(320),
"field '.%s' is a reference: assign a borrow (`&value`), not an owner", at_f.attr)
_EmitTypeError(pool, doc, rv_f, str(msg_f), errors)
else:
# The field holds a borrow: the containing object is the borrower,
# so the owner cannot be deleted while it is in scope.
own_f: str = _BorrowOwnerText(rv_f)
if own_f is None and _RefDetail(_SymDetailAt(pool, a, rv_f)) != 0:
own_f = _BorrowOwnerOf(_BorrowOwnerText(rv_f))
at_bf: ast.Attribute* = ast.Attribute*(tgt)
if own_f is not None and at_bf.value is not None \
and at_bf.value.kind() == ast.ASTKind.Name:
brw_f: str = (ast.Name*)(at_bf.value).id
if brw_f is not None: _BorrowAdd(pool, own_f, brw_f)
continue
if tgt.kind() == ast.ASTKind.Subscript:
sb_t: ast.Subscript* = ast.Subscript*(tgt)
@@ -1665,7 +1848,7 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
_WalkChecksExprs(pool, doc, a, ws, table_pool, asn.value, errors)
if asn.value is not None and asn.value.kind() == ast.ASTKind.Name:
sn_a: ast.Name* = ast.Name*(asn.value)
if sn_a.id is not None and _OwnHas(sn_a.id) != 0:
if sn_a.id is not None and _IsOwnBinding(pool, a, asn.value) != 0:
ti_m: int = 0
while ti_m < int(len(asn.targets)):
tm: ast.AST* = asn.targets.get(sizet(ti_m))
@@ -1689,7 +1872,44 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
ti_c += 1
if tc is not None and tc.kind() == ast.ASTKind.Name:
nv: ast.Name* = ast.Name*(tc)
if nv.id is not None: _MovedClear(nv.id)
if nv.id is not None:
_MovedClear(nv.id)
# Reassignment drops the borrows the binding used to hold.
_BorrowDrop(nv.id)
if asn.value is not None and asn.value.kind() == ast.ASTKind.UnaryOp \
and (ast.UnaryOp*)(asn.value).op == ast.OpKind.AddrOf:
if _RefDetail(_SymDetailAt(pool, a, tc)) != 0:
own_nv: str = _BorrowOwnerText(asn.value)
if own_nv is not None: _BorrowAdd(pool, own_nv, nv.id)
continue
if k == ast.ASTKind.Delete:
dl: ast.Delete* = ast.Delete*(s)
if dl.targets is not None:
di_d: int = 0
while di_d < int(len(dl.targets)):
dt: ast.AST* = dl.targets.get(sizet(di_d))
di_d += 1
if dt is None or dt.kind() != ast.ASTKind.Name:
continue
dn: ast.Name* = ast.Name*(dt)
if dn.id is None:
continue
if _RefDetail(_SymDetailAt(pool, a, dt)) != 0:
_EmitTypeError(pool, doc, dt,
f"cannot `del {dn.id}`: it is a reference ('&T' / '&mut T'), not an owner", errors)
continue
live_d: str = _BorrowLiveBorrower(dn.id)
if live_d is not None:
msg_d: bytes = pool.alloc(sizet(320))
if msg_d is not None:
stdio.snprintf(msg_d, sizet(320),
"cannot `del %s`: the reference '%s' is still in scope; reassign it (or delete after it is gone) first",
dn.id, live_d)
_EmitTypeError(pool, doc, dt, str(msg_d), errors)
continue
# Releasing the borrower drops the borrows it held.
_BorrowDrop(dn.id)
_MovedAdd(pool, dn.id)
continue
if k == ast.ASTKind.AugAssign:
ag: ast.AugAssign* = ast.AugAssign*(s)