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:
1 parent
e7ef23b432
commit
88b3724303
1 file changed
+226
-6
+226
-6
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user