From 88b3724303482842175641e9d32e0e0ebaeeda22 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 4 Nov 2026 23:03:24 +0800 Subject: [PATCH] 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. --- App/lib/lsp/LspChecks.vp | 232 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 226 insertions(+), 6 deletions(-) diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index bf8891a..3a870a6 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -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)