diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index 3a870a6..d6cd6b8 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -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)