diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 27af6fa..13781a8 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -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) diff --git a/App/lib/core/Gen/Call/CallGenCall.vp b/App/lib/core/Gen/Call/CallGenCall.vp index c6ee099..cafe529 100644 --- a/App/lib/core/Gen/Call/CallGenCall.vp +++ b/App/lib/core/Gen/Call/CallGenCall.vp @@ -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 diff --git a/App/lib/core/Gen/Call/CallGenLib.vp b/App/lib/core/Gen/Call/CallGenLib.vp index 66bf2b5..33654c4 100644 --- a/App/lib/core/Gen/Call/CallGenLib.vp +++ b/App/lib/core/Gen/Call/CallGenLib.vp @@ -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) diff --git a/App/lib/core/Gen/Expr/ExprGenAttr.vp b/App/lib/core/Gen/Expr/ExprGenAttr.vp index 16b48a0..1a5120f 100644 --- a/App/lib/core/Gen/Expr/ExprGenAttr.vp +++ b/App/lib/core/Gen/Expr/ExprGenAttr.vp @@ -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: diff --git a/App/lib/core/Gen/Expr/ExprGenName.vp b/App/lib/core/Gen/Expr/ExprGenName.vp index 17724ad..5299da3 100644 --- a/App/lib/core/Gen/Expr/ExprGenName.vp +++ b/App/lib/core/Gen/Expr/ExprGenName.vp @@ -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) diff --git a/App/lib/core/Gen/Expr/ExprGenOps.vp b/App/lib/core/Gen/Expr/ExprGenOps.vp index eea2249..746c402 100644 --- a/App/lib/core/Gen/Expr/ExprGenOps.vp +++ b/App/lib/core/Gen/Expr/ExprGenOps.vp @@ -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). diff --git a/App/lib/core/Gen/Stmt/StmtGenMixin.vp b/App/lib/core/Gen/Stmt/StmtGenMixin.vp index 01db6b3..4eb2f3c 100644 --- a/App/lib/core/Gen/Stmt/StmtGenMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenMixin.vp @@ -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". diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index 70e69d5..06fa27e 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -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.