feat(obs): check owner access against live borrows
Borrow exclusivity now covers direct uses of the owner, not just creating another borrow. A write or move of an owner (`c.x = 1`, `del`/move, a method call whose `self` is `&mut` or implicit) conflicts with any live borrow; a read (`c.x`, passing it on) conflicts with a live `&mut` borrow. `_BorrowCheckAccess` resolves the root binding of the access place, applies the NLL dead-borrow rule, and is wired into name loads, attribute loads, assignment targets, own moves (field/argument/assignment), and method receivers (the receiver's `self` kind is read from the parameter registry; implicit `self` is treated conservatively as a write). Reads inside a borrow expression are suppressed so `&mut x` is not reported against its own new borrow. The LSP mirrors the same three checks (write target, owner read, method receiver) with the same messages.
This commit is contained in:
1 parent
c3c2e55fb4
commit
ad16a5c0b3
8 files changed
+261
-5
No files matched your search
@@ -1087,6 +1087,8 @@ novirtual class AssignGenMixin:
|
||||
src_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, src_nm.id)
|
||||
if src_slot is None: return 0
|
||||
if self.Gen.VarGen._SlifeOwned(src_slot) == 0: return 0
|
||||
# Moving an owned value out of a borrowed binding conflicts with the borrow.
|
||||
self.Gen.StmtGen._BorrowCheckAccess(trans, rhs, 3, rhs)
|
||||
if target_own == 0:
|
||||
self.Gen.TypeGenErr.fatal_error(rhs, f"cannot move `{src_nm.id}` into a slot that is not `own`: it would never be released. Declare the target as `own T`, or keep the owned binding and borrow it instead", "LifetimeError")
|
||||
return 0
|
||||
@@ -2125,6 +2127,8 @@ novirtual class AssignGenMixin:
|
||||
# A reference borrows: no ownership transfer, no shadow ownership.
|
||||
if target_name is not None and self.Gen.VarGen.var_ref_kind(trans.SymTab, target_name) != 0:
|
||||
return 0
|
||||
# Moving the owned value into the target conflicts with a live borrow.
|
||||
self.Gen.StmtGen._BorrowCheckAccess(trans, rhs_node, 3, rhs_node)
|
||||
# The source owns a heap pointer: a value-typed target cannot receive it
|
||||
# implicitly (same rule as `new T()` into `T`).
|
||||
if target_slot.Ty is None or target_slot.Ty.Pointee == 0 or self.Gen.ExprGenBase.is_ptr_type(target_slot.Ty.Pointee) == 0:
|
||||
@@ -3997,6 +4001,16 @@ novirtual class AssignGenMixin:
|
||||
if asgn.value is not None and (asgn.value.kind() == ast.ASTKind.Tuple or asgn.value.kind() == ast.ASTKind.List):
|
||||
return self._translate_tuple_literal_assign(trans, asgn, targets, pool, builder, mod)
|
||||
|
||||
# OBS: writing a place is an access of its root object: reject it while borrowed.
|
||||
if trans.StrictOBS != 0:
|
||||
ti_w: int = 0
|
||||
while ti_w < int(len(targets)):
|
||||
tgt_w: ast.AST* = targets.get(sizet(ti_w))
|
||||
ti_w += 1
|
||||
if tgt_w is None: continue
|
||||
k_w: int = tgt_w.kind()
|
||||
if k_w == ast.ASTKind.Name or k_w == ast.ASTKind.Attribute or k_w == ast.ASTKind.Subscript:
|
||||
self.Gen.StmtGen._BorrowCheckAccess(trans, tgt_w, 2, tgt_w)
|
||||
# const target: fail fast before assignment (Name/Attribute/Subscript)
|
||||
for ti_c in range(int(len(targets))):
|
||||
tgt_c: ast.AST* = targets.get(ti_c)
|
||||
|
||||
@@ -258,6 +258,18 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
|
||||
viperlib.snprintf(mangled2, string.strlen(cls_sha1) + string.strlen(lookup_name) + 2, "%s.%s", cls_sha1, lookup_name)
|
||||
call_name = mangled2
|
||||
|
||||
# OBS: calling a method accesses its receiver (`&Self` = read, `&mut Self` / implicit = write).
|
||||
if trans is not None and trans.StrictOBS != 0 and node is not None \
|
||||
and node.kind() == ast.ASTKind.Call:
|
||||
recv_f: ast.AST* = (ast.Call*)(node).func
|
||||
if recv_f is not None and recv_f.kind() == ast.ASTKind.Attribute:
|
||||
recv_n: ast.AST* = (ast.Attribute*)(recv_f).value
|
||||
if recv_n is not None and recv_n.kind() == ast.ASTKind.Name:
|
||||
self_kind: int = 2
|
||||
if self.Gen.CallGenBase._param_ref_known(call_name) != 0:
|
||||
rk_s: int = self.Gen.CallGenBase._param_ref_kind(call_name, 0)
|
||||
if rk_s == 1: self_kind = 1
|
||||
self.Gen.StmtGen._BorrowCheckAccess(trans, recv_n, self_kind, node)
|
||||
# Determine the return type (__before_init__ and __init__ return void).
|
||||
void_ty: mlirlite.LLVMType* = mlirlite.Void(pool)
|
||||
call_ret_ty: mlirlite.LLVMType* = void_ty
|
||||
@@ -869,6 +881,17 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
|
||||
found_name: char* = mlirlite.function_get_name(found_func)
|
||||
if found_name is not None: call_name = found_name
|
||||
|
||||
# OBS: calling a method accesses its receiver (`&Self` = read, `&mut Self` / implicit = write).
|
||||
if trans is not None and trans.StrictOBS != 0 and cl is not None \
|
||||
and cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
|
||||
recv_n2: ast.AST* = (ast.Attribute*)(cl.func).value
|
||||
if recv_n2 is not None and recv_n2.kind() == ast.ASTKind.Name:
|
||||
self_kind2: int = 2
|
||||
if self.Gen.CallGenBase._param_ref_known(call_name) != 0:
|
||||
rk_s2: int = self.Gen.CallGenBase._param_ref_kind(call_name, 0)
|
||||
if rk_s2 == 1: self_kind2 = 1
|
||||
self.Gen.StmtGen._BorrowCheckAccess(trans, recv_n2, self_kind2, cl)
|
||||
|
||||
# Determine the return type.
|
||||
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
|
||||
call_ret_ty: mlirlite.LLVMType* = i32_ty
|
||||
|
||||
@@ -195,6 +195,8 @@ novirtual class CallGenLib(CallGenBase.CallGenBase):
|
||||
if nm_o.id is None: continue
|
||||
src_o: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm_o.id)
|
||||
if src_o is not None and self.Gen.VarGen._SlifeOwned(src_o) != 0:
|
||||
# Moving the binding into the callee conflicts with any live borrow of it.
|
||||
self.Gen.StmtGen._BorrowCheckAccess(trans, an_o, 3, an_o)
|
||||
sh_o: mlirlite.Value* = self.Gen.VarGen._SlifeShadow(src_o)
|
||||
if sh_o is not None: self.Gen.VarGen._SlifeNoteStore(pool, builder, sh_o, 0)
|
||||
self.Gen.VarGen._FreedMark(src_o)
|
||||
|
||||
@@ -266,6 +266,10 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
|
||||
# symbol is fail-fast; the logic stays in _CheckStaticCrossModule, never inlined here.
|
||||
self._CheckStaticCrossModule(pool, trans, at)
|
||||
|
||||
# OBS: reading a field reads its root owner (`c.x`); a live `&mut` borrow conflicts.
|
||||
if trans is not None and trans.StrictOBS != 0:
|
||||
self.Gen.StmtGen._BorrowCheckAccess(trans, node, 1, node)
|
||||
|
||||
# magic attribute __doc__: consult the docstring registry; a hit inlines a string constant, a miss
|
||||
# returns null. Supports local and cross-module symbols (func / Class / Class.method / mod.func...).
|
||||
if string.strcmp(at.attr, "__doc__") == 0:
|
||||
|
||||
@@ -43,6 +43,10 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase):
|
||||
if trans is not None:
|
||||
if self.Gen.VarGen._FreedCheck(self.Gen.VarGen.lookup_var(trans.SymTab, nm_id)) != 0:
|
||||
self.Gen.TypeGenErr.fatal_error(node, f"cannot use '{nm_id}': already released (by `del`, a move, or a @life(willFree(...)) call)", "LifetimeError")
|
||||
# OBS: reading an owner while a `&mut` borrow of it is live is a conflict.
|
||||
if trans.StrictOBS != 0 and self.Gen.VarGen.lookup_var(trans.SymTab, nm_id) is not None \
|
||||
and self.Gen.VarGen.var_ref_kind(trans.SymTab, nm_id) == 0:
|
||||
self.Gen.StmtGen._BorrowCheckAccess(trans, node, 1, node)
|
||||
# DefineT compile-time constant: return the constant (float -> double, int -> i32).
|
||||
# Existing variables win over DefineT table entries injected from vpi stubs (lookup_var guard).
|
||||
cdef_val_d: double = self.Gen.TypeGenCDef.lookup_cdefine_double(nm_id)
|
||||
|
||||
@@ -821,7 +821,12 @@ novirtual class ExprGenOps(ExprGenBase.ExprGenBase):
|
||||
uo: ast.UnaryOp* = ast.UnaryOp*(node)
|
||||
if uo is None: return None
|
||||
|
||||
is_borrow_op: int = 0
|
||||
if uo.op == ast.OpKind.AddrOf or uo.op == ast.OpKind.AddrOfMut:
|
||||
is_borrow_op = 1
|
||||
self.Gen.StmtGen._AccessSuppress(1)
|
||||
operand: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, uo.operand, None, 0, trans)
|
||||
if is_borrow_op != 0: self.Gen.StmtGen._AccessSuppress(0)
|
||||
if operand is None: return None
|
||||
|
||||
# === Unary operator overload: -obj/+obj/~obj call __neg__/__pos__/__invert__ (only USub/UAdd/Invert have overloads).
|
||||
|
||||
@@ -18,13 +18,32 @@ import lib.Projectrans.Config as Config
|
||||
# OBS borrow table: (function, scope, owner, borrower, kind) rows for reference bindings and
|
||||
# reference fields. `borrower` is a path (`r`, `B.n`). `kind` is 1 for `&T` and 2 for `&mut T`.
|
||||
# A row is alive while its registration scope is still open and its root binding is declared.
|
||||
define BORROW_MAX = 1024
|
||||
define BORROW_MAX = 1024
|
||||
# Access classification for owner reads/writes/moves against live borrows.
|
||||
define BORROW_ACCESS_READ = 1
|
||||
define BORROW_ACCESS_WRITE = 2
|
||||
define BORROW_ACCESS_MOVE = 3
|
||||
_g_borrow_funcs: sizet* = None
|
||||
_g_borrow_owners: str* = None
|
||||
_g_borrow_borrowers: str* = None
|
||||
_g_borrow_kinds: sizet* = None
|
||||
_g_borrow_scopes: sizet* = None
|
||||
_g_borrow_count: int = 0
|
||||
# Statement sequence number of each row's registration (rows created by the statement currently
|
||||
# being translated are part of that very access and must not conflict with it).
|
||||
_g_borrow_stmts: sizet* = None
|
||||
# AST nodes of the borrow expressions registered in this function: accesses inside a borrow
|
||||
# expression (the operand being borrowed) are part of creating the borrow, not a separate access.
|
||||
_g_borrow_nodes: sizet* = None
|
||||
_g_borrow_node_count: int = 0
|
||||
# Source line of each row's borrow expression: an access on that same line is part of creating
|
||||
# the borrow (the value may be re-translated in a later pass).
|
||||
_g_borrow_lines: sizet* = None
|
||||
_g_borrow_count: int = 0
|
||||
_g_stmt_seq: int = 0
|
||||
# Line of the borrow expression currently being registered.
|
||||
_g_borrow_cur_line: int = 0
|
||||
# 1 while translating a borrow expression: reads inside it create the borrow.
|
||||
_g_access_suppress: int = 0
|
||||
|
||||
def _BorrowRoot(name: str) -> str:
|
||||
"""Root binding of a borrower path: `B.n.m` -> `B`."""
|
||||
@@ -45,7 +64,7 @@ def _BorrowRoot(name: str) -> str:
|
||||
def _BorrowTableAdd(func: mlirlite.Function*, scope: GenTypes.Scope*, owner: str,
|
||||
borrower: str, kind: int) -> int:
|
||||
"""Add/replace a row; 1 = added, 0 = duplicate refreshed, -1 = table full."""
|
||||
global _g_borrow_funcs, _g_borrow_owners, _g_borrow_borrowers, _g_borrow_kinds, _g_borrow_scopes, _g_borrow_count
|
||||
global _g_borrow_funcs, _g_borrow_owners, _g_borrow_borrowers, _g_borrow_kinds, _g_borrow_scopes, _g_borrow_stmts, _g_borrow_nodes, _g_borrow_lines, _g_borrow_count
|
||||
if func is None or owner is None or borrower is None: return 0
|
||||
if _g_borrow_funcs is None:
|
||||
_g_borrow_funcs = sizet*(stdlib.malloc(BORROW_MAX * 8))
|
||||
@@ -53,8 +72,11 @@ def _BorrowTableAdd(func: mlirlite.Function*, scope: GenTypes.Scope*, owner: str
|
||||
_g_borrow_borrowers = str*(stdlib.malloc(BORROW_MAX * 8))
|
||||
_g_borrow_kinds = sizet*(stdlib.malloc(BORROW_MAX * 8))
|
||||
_g_borrow_scopes = sizet*(stdlib.malloc(BORROW_MAX * 8))
|
||||
_g_borrow_stmts = sizet*(stdlib.malloc(BORROW_MAX * 8))
|
||||
_g_borrow_nodes = sizet*(stdlib.malloc(64 * 8))
|
||||
_g_borrow_lines = sizet*(stdlib.malloc(BORROW_MAX * 8))
|
||||
if _g_borrow_funcs is None or _g_borrow_owners is None or _g_borrow_borrowers is None: return 0
|
||||
if _g_borrow_kinds is None or _g_borrow_scopes is None: return 0
|
||||
if _g_borrow_kinds is None or _g_borrow_scopes is None or _g_borrow_stmts is None: return 0
|
||||
# Refresh an existing (func, borrower) row instead of growing the table.
|
||||
i: int = 0
|
||||
while i < _g_borrow_count:
|
||||
@@ -71,6 +93,8 @@ def _BorrowTableAdd(func: mlirlite.Function*, scope: GenTypes.Scope*, owner: str
|
||||
_g_borrow_borrowers[_g_borrow_count] = borrower
|
||||
_g_borrow_kinds[_g_borrow_count] = sizet(kind)
|
||||
_g_borrow_scopes[_g_borrow_count] = sizet(scope)
|
||||
_g_borrow_stmts[_g_borrow_count] = sizet(_g_stmt_seq)
|
||||
_g_borrow_lines[_g_borrow_count] = sizet(_g_borrow_cur_line)
|
||||
_g_borrow_count += 1
|
||||
return 1
|
||||
|
||||
@@ -95,6 +119,8 @@ def _BorrowTableDrop(func: mlirlite.Function*, name: str) -> int:
|
||||
_g_borrow_borrowers[j] = _g_borrow_borrowers[j + 1]
|
||||
_g_borrow_kinds[j] = _g_borrow_kinds[j + 1]
|
||||
_g_borrow_scopes[j] = _g_borrow_scopes[j + 1]
|
||||
_g_borrow_stmts[j] = _g_borrow_stmts[j + 1]
|
||||
_g_borrow_lines[j] = _g_borrow_lines[j + 1]
|
||||
j += 1
|
||||
_g_borrow_count -= 1
|
||||
continue
|
||||
@@ -107,7 +133,8 @@ def _BorrowTableDrop(func: mlirlite.Function*, name: str) -> int:
|
||||
# statements) and stays conservative: any mention inside a loop keeps the borrow live.
|
||||
def _BorrowClearFunc(func: mlirlite.Function*) -> int:
|
||||
"""Drop every row of a function (its frame is gone, so are its borrows)."""
|
||||
global _g_borrow_count
|
||||
global _g_borrow_count, _g_borrow_node_count
|
||||
_g_borrow_node_count = 0
|
||||
if func is None or _g_borrow_funcs is None: return 0
|
||||
i: int = 0
|
||||
while i < _g_borrow_count:
|
||||
@@ -119,6 +146,8 @@ def _BorrowClearFunc(func: mlirlite.Function*) -> int:
|
||||
_g_borrow_borrowers[j] = _g_borrow_borrowers[j + 1]
|
||||
_g_borrow_kinds[j] = _g_borrow_kinds[j + 1]
|
||||
_g_borrow_scopes[j] = _g_borrow_scopes[j + 1]
|
||||
_g_borrow_stmts[j] = _g_borrow_stmts[j + 1]
|
||||
_g_borrow_lines[j] = _g_borrow_lines[j + 1]
|
||||
j += 1
|
||||
_g_borrow_count -= 1
|
||||
continue
|
||||
@@ -135,6 +164,114 @@ import lib.core.Gen.DebugInfoGen as DebugInfoGen
|
||||
novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
|
||||
"""Statement/flow translation Mixin (top level, extends Decl)"""
|
||||
|
||||
# _BorrowCheckAccess - reject reading/writing/moving an owner while it is borrowed.
|
||||
# Kinds: 1 = read (only a live `&mut` conflicts), 2 = write / 3 = move (any live borrow).
|
||||
def _BorrowCheckAccess(self, trans: HT.Translator*, place: ast.AST*, kind: int,
|
||||
node: ast.AST*) -> int:
|
||||
if _g_access_suppress != 0: return 0
|
||||
if trans is None or place is None or node is None: return 0
|
||||
if trans.StrictOBS == 0 or trans._cur_func is None: return 0
|
||||
if self._IsBorrowOperand(place) != 0: return 0
|
||||
base: str = self._BorrowBaseName(trans.Pool, place)
|
||||
if base is None: return 0
|
||||
# The borrow's own line: the initializer is re-translated in a later pass, and its
|
||||
# operand read belongs to creating the borrow, not to a separate access.
|
||||
i0: int = 0
|
||||
while i0 < _g_borrow_count:
|
||||
if _g_borrow_funcs[i0] == sizet(trans._cur_func) and _g_borrow_owners[i0] is not None \
|
||||
and string.strcmp(_g_borrow_owners[i0], base) == 0 \
|
||||
and _g_borrow_lines[i0] == sizet(node.lineno):
|
||||
return 0
|
||||
i0 += 1
|
||||
# Accessing through a reference is an access of the referenced object, not of the binding.
|
||||
if self.Gen.VarGen.var_ref_kind(trans.SymTab, base) != 0: return 0
|
||||
i: int = 0
|
||||
while i < _g_borrow_count:
|
||||
if _g_borrow_funcs[i] == sizet(trans._cur_func) and _g_borrow_owners[i] is not None \
|
||||
and string.strcmp(_g_borrow_owners[i], base) == 0 \
|
||||
and _g_borrow_stmts[i] != sizet(_g_stmt_seq) \
|
||||
and _g_borrow_lines[i] <= sizet(node.lineno) \
|
||||
and self._BorrowEntryLive(trans, i) != 0 \
|
||||
and self._NllDead(trans, _g_borrow_borrowers[i], node) == 0:
|
||||
b_n: str = _g_borrow_borrowers[i]
|
||||
b_k: int = int(_g_borrow_kinds[i])
|
||||
if kind == BORROW_ACCESS_READ and b_k != 2:
|
||||
i += 1
|
||||
continue
|
||||
if kind == BORROW_ACCESS_READ:
|
||||
self.Gen.TypeGenErr.fatal_error(node, f"cannot read '{base}': it is mutably borrowed by '{b_n}'", "LifetimeError")
|
||||
elif kind == BORROW_ACCESS_WRITE:
|
||||
self.Gen.TypeGenErr.fatal_error(node, f"cannot modify '{base}': it is borrowed by '{b_n}'", "LifetimeError")
|
||||
else:
|
||||
self.Gen.TypeGenErr.fatal_error(node, f"cannot move '{base}': it is borrowed by '{b_n}'", "LifetimeError")
|
||||
i += 1
|
||||
return 0
|
||||
|
||||
# _AccessSuppress - suppress owner access checks while translating a borrow expression.
|
||||
def _AccessSuppress(self, flag: int) -> int:
|
||||
global _g_access_suppress
|
||||
_g_access_suppress = flag
|
||||
return 0
|
||||
|
||||
# _RecordBorrowNode - remember the AST node an access is read from while creating a borrow,
|
||||
# so the operand's own read is not mistaken for a separate access.
|
||||
def _RecordBorrowNode(self, expr: ast.AST*) -> int:
|
||||
global _g_borrow_node_count
|
||||
if expr is None or _g_borrow_nodes is None: return 0
|
||||
tgt: ast.AST* = expr
|
||||
if tgt.kind() == ast.ASTKind.UnaryOp:
|
||||
uo: ast.UnaryOp* = ast.UnaryOp*(tgt)
|
||||
if uo.op == ast.OpKind.AddrOf or uo.op == ast.OpKind.AddrOfMut:
|
||||
if uo.operand is not None: tgt = uo.operand
|
||||
depth: int = 0
|
||||
p: ast.AST* = tgt
|
||||
while p is not None and depth < 16:
|
||||
depth += 1
|
||||
if _g_borrow_node_count < 64:
|
||||
_g_borrow_nodes[_g_borrow_node_count] = sizet(p)
|
||||
_g_borrow_node_count += 1
|
||||
k: int = p.kind()
|
||||
if k == ast.ASTKind.Name: break
|
||||
if k == ast.ASTKind.Attribute: p = (ast.Attribute*)(p).value
|
||||
elif k == ast.ASTKind.Subscript: p = (ast.Subscript*)(p).value
|
||||
elif k == ast.ASTKind.UnaryOp:
|
||||
uo2: ast.UnaryOp* = ast.UnaryOp*(p)
|
||||
if uo2.op == ast.OpKind.Deref: p = uo2.operand
|
||||
else: break
|
||||
else: break
|
||||
return 1
|
||||
|
||||
# _IsBorrowOperand - 1 when the access node is inside a registered borrow expression.
|
||||
def _IsBorrowOperand(self, node: ast.AST*) -> int:
|
||||
if node is None or _g_borrow_nodes is None: return 0
|
||||
p: ast.AST* = node
|
||||
depth: int = 0
|
||||
while p is not None and depth < 12:
|
||||
depth += 1
|
||||
i: int = 0
|
||||
while i < _g_borrow_node_count:
|
||||
if _g_borrow_nodes[i] == sizet(p): return 1
|
||||
i += 1
|
||||
p = p.parent
|
||||
return 0
|
||||
|
||||
# _BorrowBaseName - root binding of an access place (`c`, `c.x`, `c[i]` -> `c`).
|
||||
def _BorrowBaseName(self, pool: memhub.MemBuddy*, place: ast.AST*) -> str:
|
||||
p: ast.AST* = place
|
||||
depth: int = 0
|
||||
while p is not None and depth < 16:
|
||||
depth += 1
|
||||
k: int = p.kind()
|
||||
if k == ast.ASTKind.Name: return (ast.Name*)(p).id
|
||||
if k == ast.ASTKind.Attribute: p = (ast.Attribute*)(p).value
|
||||
elif k == ast.ASTKind.Subscript: p = (ast.Subscript*)(p).value
|
||||
elif k == ast.ASTKind.UnaryOp:
|
||||
uo: ast.UnaryOp* = ast.UnaryOp*(p)
|
||||
if uo.op == ast.OpKind.Deref: p = uo.operand
|
||||
else: return None
|
||||
else: return None
|
||||
return None
|
||||
|
||||
def _BorrowScopeActive(self, trans: HT.Translator*, scope: GenTypes.Scope*) -> int:
|
||||
"""1 when `scope` is the current scope or one of its ancestors (still open)."""
|
||||
if trans is None or trans.SymTab is None or scope is None: return 0
|
||||
@@ -264,8 +401,11 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
|
||||
Minimal exclusivity rule: while an owner is mutably borrowed, no new borrow is allowed;
|
||||
a mutable borrow is rejected when any read-only borrow is still alive.
|
||||
"""
|
||||
global _g_borrow_cur_line
|
||||
if trans is None or owner is None or borrower is None: return 0
|
||||
if trans._cur_func is None: return 0
|
||||
if node is not None: _g_borrow_cur_line = int(node.lineno)
|
||||
else: _g_borrow_cur_line = 0
|
||||
i: int = 0
|
||||
while i < _g_borrow_count:
|
||||
if _g_borrow_funcs[i] == sizet(trans._cur_func) and _g_borrow_owners[i] is not None \
|
||||
@@ -281,6 +421,7 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
|
||||
elif kind == 2:
|
||||
self.Gen.TypeGenErr.fatal_error(node, f"cannot borrow '{owner}' as mutable: it is also borrowed as read-only by '{other}'", "LifetimeError")
|
||||
i += 1
|
||||
self._RecordBorrowNode(node)
|
||||
rc: int = _BorrowTableAdd(trans._cur_func, trans.SymTab.Current, owner, borrower, kind)
|
||||
if rc < 0:
|
||||
self.Gen.TypeGenErr.fatal_error(node, f"too many live references in this function (limit {BORROW_MAX}); split it into smaller functions", "LifetimeError")
|
||||
@@ -344,6 +485,8 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
|
||||
|
||||
def translate_stmt(self, trans: HT.Translator*, node: ast.AST*) -> int:
|
||||
"""Translate a single statement with a depth guard; returns the number of new variables"""
|
||||
global _g_stmt_seq
|
||||
_g_stmt_seq += 1
|
||||
if trans is not None:
|
||||
# --traceback: statement-level DWARF location (no-op outside a DI-attached
|
||||
# define). Every instruction then carries ", !dbg !N".
|
||||
|
||||
@@ -115,6 +115,8 @@ def _BorrowPathText(pool: memhub.MemManager*, expr: ast.AST*, depth: int = 0) ->
|
||||
|
||||
# Body statements of the function currently being checked (NLL looks ahead for later uses).
|
||||
_nll_body: list[ast.AST*]* = None
|
||||
# 1 while walking a borrow expression: reads inside it create the borrow.
|
||||
_access_suppress: int = 0
|
||||
# Class symbol whose body is being checked (`self.m()` receiver resolution).
|
||||
_nll_class: LspSymbols.LspSymbol* = None
|
||||
|
||||
@@ -333,6 +335,26 @@ def _BorrowLiveBorrower(owner: str, line: int = 0) -> str:
|
||||
return None
|
||||
|
||||
|
||||
def _AccessConflict(owner: str, kind: int, line: int) -> str:
|
||||
"""A live borrower that conflicts with a read (1, only `&mut`) / write or move (2/3)."""
|
||||
if owner 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:
|
||||
have: int = 0
|
||||
if _borrow_kinds is not None and i < int(len(_borrow_kinds)):
|
||||
have = _borrow_kinds.get(sizet(i))
|
||||
if kind == 1 and have != 2:
|
||||
i += 1
|
||||
continue
|
||||
if _BorrowIsDead(b, line) == 0: return b
|
||||
i += 1
|
||||
return None
|
||||
|
||||
|
||||
def _BorrowConflict(owner: str, borrower: str, kind: int, line: int = 0) -> 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
|
||||
@@ -1524,6 +1546,12 @@ def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
|
||||
k: int = e.kind()
|
||||
if k == ast.ASTKind.Name:
|
||||
nm_m: ast.Name* = ast.Name*(e)
|
||||
if nm_m.id is not None and _access_suppress == 0 \
|
||||
and _RefDetail(_SymDetailAt(pool, a, e)) == 0:
|
||||
cf_r: str = _AccessConflict(nm_m.id, 1, int(e.lineno))
|
||||
if cf_r is not None:
|
||||
_EmitTypeError(pool, doc, e,
|
||||
f"cannot read '{nm_m.id}': it is mutably borrowed by '{cf_r}'", errors)
|
||||
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 or released (ownership transferred away)", errors)
|
||||
elif nm_m.id is not None and _IsKnownPlainName(nm_m.id) == 0:
|
||||
@@ -1560,6 +1588,14 @@ def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
|
||||
_WalkChecksExprs(pool, doc, a, ws, table_pool, bo.right, errors)
|
||||
return
|
||||
if k == ast.ASTKind.UnaryOp:
|
||||
uo_b: ast.UnaryOp* = ast.UnaryOp*(e)
|
||||
if uo_b.op == ast.OpKind.AddrOf or uo_b.op == ast.OpKind.AddrOfMut:
|
||||
global _access_suppress
|
||||
saved_sup: int = _access_suppress
|
||||
_access_suppress = 1
|
||||
_WalkChecksExprs(pool, doc, a, ws, table_pool, uo_b.operand, errors)
|
||||
_access_suppress = saved_sup
|
||||
return
|
||||
_WalkChecksExprs(pool, doc, a, ws, table_pool, ast.UnaryOp*(e).operand, errors)
|
||||
return
|
||||
if k == ast.ASTKind.BoolOp:
|
||||
@@ -2213,6 +2249,21 @@ def _CheckCall(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
|
||||
fname: str = t.name
|
||||
if fname is None:
|
||||
fname = "function"
|
||||
# Calling a method accesses its receiver: `&Self` reads, `&mut Self` / implicit writes.
|
||||
if t.kind == LspSymbols.LSP_KIND_METHOD and call.func is not None \
|
||||
and call.func.kind() == ast.ASTKind.Attribute:
|
||||
recv_c: ast.AST* = (ast.Attribute*)(call.func).value
|
||||
if recv_c is not None:
|
||||
recv_root: str = _BorrowPathText(pool, recv_c)
|
||||
if recv_root is not None:
|
||||
self_kind: int = 2
|
||||
pref_m: str = _ParamRefFlags(pool, t.detail)
|
||||
if pref_m is not None and string.strlen(pref_m) > 0 and int(pref_m[0]) == 49:
|
||||
self_kind = 1
|
||||
cf_m: str = _AccessConflict(recv_root, self_kind, int(call.func.lineno))
|
||||
if cf_m is not None:
|
||||
_EmitTypeError(pool, doc, recv_c,
|
||||
f"cannot modify '{recv_root}': it is borrowed by '{cf_m}'", errors)
|
||||
pflags: str = _ParamNullFlags(pool, t.detail)
|
||||
pown: str = _ParamOwnFlags(pool, t.detail)
|
||||
pref: str = _ParamRefFlags(pool, t.detail)
|
||||
@@ -2436,6 +2487,16 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
|
||||
ti += 1
|
||||
if tgt is None:
|
||||
continue
|
||||
# Writing a place accesses its root owner.
|
||||
tg_root: str = _BorrowPathText(pool, tgt)
|
||||
if tg_root is not None and string.strchr(tg_root, '.') is not None:
|
||||
dot_t: str = string.strchr(tg_root, '.')
|
||||
tg_root = _BorrowRoot(tg_root)
|
||||
if tg_root is not None:
|
||||
cf_w: str = _AccessConflict(tg_root, 2, int(tgt.lineno))
|
||||
if cf_w is not None:
|
||||
_EmitTypeError(pool, doc, tgt,
|
||||
f"cannot modify '{tg_root}': it is borrowed by '{cf_w}'", errors)
|
||||
if tgt.kind() == ast.ASTKind.Attribute:
|
||||
_WalkChecksExprs(pool, doc, a, ws, table_pool, ast.Attribute*(tgt).value, errors)
|
||||
# Reference field: the slot holds a borrow, not an owner.
|
||||
|
||||
Reference in new issue
Block a user