feat(lsp): mirror mutable borrows and reference fields

Diagnostics follow the new rules: `&mut` exclusivity conflicts, a read-only borrow assigned to a `&mut` slot, borrows through nested field paths (so `del` is reported), `Box[T]` as an ownership spelling, and reference bindings initialized from calls.
This commit is contained in:
TermiNexus committed 2026-11-05 15:49:27 +08:00
1 parent 3764afbaa6
commit 38a8d338c1
1 file changed
+142 -23
+142 -23
View File
@@ -11,6 +11,7 @@ import lib.lsp.LspWorkspace as LspWorkspace
import lib.lsp.LspNavigation as LspNavigation
import lib.lsp.LspTypes as LspTypes
import lib.lsp.LspSigCache as LspSigCache
import lib.core.CompilerFactory as CompilerFactory
# Semantic checks that are cheap enough to run on every diagnostics request.
@@ -28,10 +29,12 @@ _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]`.
# OBS reference borrows inside the current function: parallel lists, where
# `_borrow_owners[i]` is borrowed by `_borrow_borrowers[i]` with kind `_borrow_kinds[i]`
# (1 = `&T`, 2 = `&mut T`). Borrowers are paths (`B.n`).
_borrow_owners: list[str]* = None
_borrow_borrowers: list[str]* = None
_borrow_kinds: list[int]* = None
def _NameListFind(lst: list[str]*, name: str) -> int:
@@ -45,11 +48,12 @@ def _NameListFind(lst: list[str]*, name: str) -> int:
def _MoveReset(pool: memhub.MemManager*) -> None:
global _own_names, _moved_names, _borrow_owners, _borrow_borrowers
global _own_names, _moved_names, _borrow_owners, _borrow_borrowers, _borrow_kinds
_own_names = list[str](pool, 8)
_moved_names = list[str](pool, 8)
_borrow_owners = list[str](pool, 8)
_borrow_borrowers = list[str](pool, 8)
_borrow_kinds = list[int](pool, 8)
def _OwnHas(name: str) -> int:
@@ -78,24 +82,59 @@ 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:
def _BorrowRoot(name: str) -> str:
"""Root binding of a borrower path: `B.n.m` -> `B`."""
if name is None: return None
dot: str = string.strchr(name, '.')
if dot is None: return name
n: sizet = sizet(uint64(dot) - uint64(name))
root: str = CompilerFactory.factory.PermPool.alloc(n + 1)
if root is None: return name
string.strncpy(root, name, n)
root[n] = '\0'
return root
def _BorrowPathText(pool: memhub.MemManager*, expr: ast.AST*, depth: int = 0) -> str:
"""Dotted name of a variable/field chain (`B`, `B.n`); None for other expressions."""
if expr is None or depth > 8: return None
if expr.kind() == ast.ASTKind.Name:
return (ast.Name*)(expr).id
if expr.kind() == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(expr)
if at.attr is None: return None
base: str = _BorrowPathText(pool, at.value, depth + 1)
if base is None: return None
n: sizet = string.strlen(base) + string.strlen(at.attr) + 2
out: bytes = pool.alloc(n)
if out is None: return None
stdio.snprintf(out, n, "%s.%s", base, at.attr)
return str(out)
return None
def _BorrowAdd(pool: memhub.MemManager*, owner: str, borrower: str, kind: int = 1) -> 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)
if _borrow_kinds is not None: _borrow_kinds.append(kind)
def _BorrowDrop(borrower: str) -> None:
"""Forget the borrows held by `borrower` (it was reassigned or released)."""
"""Forget the borrows held by `borrower` or by a path rooted at it."""
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), "")
if v is not None:
root: str = _BorrowRoot(v)
if string.strcmp(v, borrower) == 0 \
or (root is not None and string.strcmp(root, borrower) == 0):
_borrow_borrowers.set(sizet(i), "")
_borrow_owners.set(sizet(i), "")
i += 1
@@ -124,6 +163,42 @@ def _BorrowLiveBorrower(owner: str) -> str:
return None
def _BorrowConflict(owner: str, borrower: str, kind: int) -> str:
"""Existing borrower that conflicts with a new borrow (None when allowed)."""
if owner is None or borrower is None or _borrow_owners is None: return None
i: int = 0
while i < int(len(_borrow_owners)):
o: str = _borrow_owners.get(sizet(i))
b: str = _borrow_borrowers.get(sizet(i))
if o is not None and b is not None and string.strlen(b) > 0 \
and string.strcmp(o, owner) == 0 and string.strcmp(b, borrower) != 0:
have: int = 0
if _borrow_kinds is not None and i < int(len(_borrow_kinds)):
have = _borrow_kinds.get(sizet(i))
if have == 2 or kind == 2: return b
i += 1
return None
def _RefKindOfText(det: str) -> int:
"""0 / 1 (`&T`) / 2 (`&mut T`) from an annotation text."""
if det is None: return 0
s: str = det
while string.strlen(s) > 0 and s[0] == ' ': s += 1
if string.strlen(s) == 0 or s[0] != '&': return 0
if string.strncmp(s, "&mut ", 5) == 0: return 2
return 1
def _RefExprKind(node: ast.AST*) -> int:
"""0 / 1 (`&x`) / 2 (`&mut x`) for an explicit borrow expression."""
if node is None or node.kind() != ast.ASTKind.UnaryOp: return 0
uo: ast.UnaryOp* = ast.UnaryOp*(node)
if uo.op == ast.OpKind.AddrOf: return 1
if uo.op == ast.OpKind.AddrOfMut: return 2
return 0
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)."""
@@ -185,6 +260,9 @@ def _AnnIsOwn(ann: ast.AST*) -> int:
nm: ast.Name* = ast.Name*(sub.value)
if nm.id is None: return 0
if string.strcmp(nm.id, "OwnT") == 0: return 1
# `Box[T]` / `BoxT[T]` are the explicit ownership spellings (the compiler treats them
# like `own T`; a user generic named Box is rare enough to accept the approximation).
if string.strcmp(nm.id, "Box") == 0 or string.strcmp(nm.id, "BoxT") == 0: return 1
if string.strcmp(nm.id, "OptionT") == 0 or string.strcmp(nm.id, "NullableT") == 0 \
or string.strcmp(nm.id, "Option") == 0:
node = sub.slice
@@ -1078,7 +1156,7 @@ def _BorrowOwnerText(expr: ast.AST*) -> str:
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:
if uo.op == ast.OpKind.AddrOf or uo.op == ast.OpKind.AddrOfMut or uo.op == ast.OpKind.Deref:
return _BorrowOwnerText(uo.operand)
return None
@@ -1688,7 +1766,8 @@ def _CheckCall(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
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:
and ((ast.UnaryOp*)(arg).op == ast.OpKind.AddrOf \
or (ast.UnaryOp*)(arg).op == ast.OpKind.AddrOfMut):
_EmitTypeError(pool, doc, arg,
"cannot pass a borrow to an `own` parameter: the callee takes ownership", errors)
elif arg.kind() == ast.ASTKind.Name:
@@ -1776,9 +1855,27 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
_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)
tk: int = _RefKindOfText(_SymDetailAt(pool, a, an.target))
if tk != 0 and an.value is not None:
sk: int = _RefExprKind(an.value)
sn_r: str = None
if sk == 0:
if _RefKindOfText(_SymDetailAt(pool, a, an.value)) != 0:
sk = _RefKindOfText(_SymDetailAt(pool, a, an.value))
sn_r = _BorrowRoot(_BorrowOwnerText(an.value))
if _BorrowOwnerOf(sn_r) is not None: sn_r = _BorrowOwnerOf(sn_r)
else:
sn_r = _BorrowOwnerText(an.value)
if sk != 0 and tk == 2 and sk == 1:
_EmitTypeError(pool, doc, an.value,
f"`{tn.id}` is a mutable reference (`&mut T`): create it with `&mut value`, not `&value`", errors)
elif sk != 0 and sn_r is not None:
cf_r: str = _BorrowConflict(sn_r, tn.id, sk)
if cf_r is not None:
_EmitTypeError(pool, doc, an.value,
f"cannot borrow '{sn_r}': it is already borrowed by '{cf_r}' (a `&mut` borrow is exclusive)", errors)
else:
_BorrowAdd(pool, sn_r, tn.id, sk)
continue
if k == ast.ASTKind.Assign:
asn: ast.Assign* = ast.Assign*(s)
@@ -1798,7 +1895,7 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
rv_f: ast.AST* = asn.value
if rv_f is not None:
if rv_f.kind() == ast.ASTKind.UnaryOp:
if (ast.UnaryOp*)(rv_f).op == ast.OpKind.AddrOf: ok_f = 1
if _RefExprKind(rv_f) != 0: ok_f = 1
elif rv_f.kind() == ast.ASTKind.Constant:
if (ast.Constant*)(rv_f).const_kind == ast.CONST_NONE: ok_f = 1
elif _RefDetail(_SymDetailAt(pool, a, rv_f)) != 0:
@@ -1813,14 +1910,24 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
else:
# The field holds a borrow: the containing object is the borrower,
# so the owner cannot be deleted while it is in scope.
fk: int = _RefKindOfText(det_f)
sk_f: int = _RefExprKind(rv_f)
if sk_f == 0:
sk_f = _RefKindOfText(_SymDetailAt(pool, a, rv_f))
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)
brw_f: str = _BorrowPathText(pool, tgt)
if fk == 2 and sk_f == 1:
_EmitTypeError(pool, doc, rv_f,
"field is a mutable reference (`&mut T`): assign `&mut value`, not `&value`", errors)
elif own_f is not None and brw_f is not None:
cf_f: str = _BorrowConflict(own_f, brw_f, sk_f)
if cf_f is not None:
_EmitTypeError(pool, doc, rv_f,
f"cannot borrow '{own_f}': it is already borrowed by '{cf_f}' (a `&mut` borrow is exclusive)", errors)
else:
_BorrowAdd(pool, own_f, brw_f, sk_f)
continue
if tgt.kind() == ast.ASTKind.Subscript:
sb_t: ast.Subscript* = ast.Subscript*(tgt)
@@ -1876,11 +1983,23 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
_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:
tk_nv: int = _RefKindOfText(_SymDetailAt(pool, a, tc))
sk_nv: int = _RefExprKind(asn.value)
if tk_nv != 0 and asn.value is not None:
if sk_nv == 0:
sk_nv = _RefKindOfText(_SymDetailAt(pool, a, asn.value))
if tk_nv == 2 and sk_nv == 1:
_EmitTypeError(pool, doc, asn.value,
f"`{nv.id}` is a mutable reference (`&mut T`): assign `&mut value`, not `&value`", errors)
elif sk_nv != 0:
own_nv: str = _BorrowOwnerText(asn.value)
if own_nv is not None: _BorrowAdd(pool, own_nv, nv.id)
if own_nv is not None:
cf_nv: str = _BorrowConflict(own_nv, nv.id, sk_nv)
if cf_nv is not None:
_EmitTypeError(pool, doc, asn.value,
f"cannot borrow '{own_nv}': it is already borrowed by '{cf_nv}' (a `&mut` borrow is exclusive)", errors)
else:
_BorrowAdd(pool, own_nv, nv.id, sk_nv)
continue
if k == ast.ASTKind.Delete:
dl: ast.Delete* = ast.Delete*(s)