From a2a04f567ee316e98581a1e0e7d7ab85439d7b04 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 5 Nov 2026 15:56:09 +0800 Subject: [PATCH] feat(obs): check reference argument kinds A read-only borrow (or a `&T` binding) cannot fill a `&mut T` parameter; the call site now reports it, and the LSP mirrors the same rule. --- App/lib/core/Gen/Call/CallGenLib.vp | 21 +++++++++ App/lib/lsp/LspChecks.vp | 69 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/App/lib/core/Gen/Call/CallGenLib.vp b/App/lib/core/Gen/Call/CallGenLib.vp index b15c309..3c038e0 100644 --- a/App/lib/core/Gen/Call/CallGenLib.vp +++ b/App/lib/core/Gen/Call/CallGenLib.vp @@ -202,6 +202,27 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): self.Gen.TypeGenErr.fatal_error(an_o, f"cannot pass '{nm_o.id}' to an `own` parameter: it is not owned here; move an `own` binding or pass `new ...`", "LifetimeError") elif an_o.kind() == ast.ASTKind.UnaryOp: self.Gen.TypeGenErr.fatal_error(an_o, "cannot pass a borrow to an `own` parameter: the callee takes ownership", "LifetimeError") + # Reference parameters: a mutable reference only accepts `&mut value` (or a `&mut` + # binding); a read-only borrow may fill a read-only parameter. + if trans is not None and cargs is not None and trans.StrictOBS != 0 \ + and self.Gen.CallGenBase._param_ref_known(call_name) != 0: + arg_n_r: int = int(len(cargs)) + ai_r: int = 0 + while ai_r < arg_n_r: + an_r: ast.AST* = cargs.get(sizet(ai_r)) + ai_r += 1 + if an_r is None: continue + pidx_r: int = ai_r - 1 + self_off + pk_r: int = self.Gen.CallGenBase._param_ref_kind(call_name, pidx_r) + if pk_r != 2: continue + sk_r: int = self.Gen.TypeGenResolve.ref_expr_kind(an_r) + if sk_r == 0 and an_r.kind() == ast.ASTKind.Name: + nm_r2: ast.Name* = ast.Name*(an_r) + if nm_r2.id is not None: + sk_r = self.Gen.VarGen.var_ref_kind(trans.SymTab, nm_r2.id) + if sk_r == 0: continue + if sk_r == 1: + self.Gen.TypeGenErr.fatal_error(an_r, "cannot pass a read-only borrow to a `&mut` parameter: create it with `&mut value`", "LifetimeError") # Strict OBS: a nullable binding/field may be None and must not flow into a non-nullable # parameter. Same-module callees use the nullable registry, cross-module ones use their # vpi param annotations; an unknown callee signature is left alone. diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index d6cd6b8..aba30bb 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -1593,6 +1593,65 @@ def _ParamOwnFlags(pool: memhub.MemManager*, detail: str) -> str: return str(buf) +def _ParamRefFlags(pool: memhub.MemManager*, detail: str) -> str: + """Per-parameter reference kind from a `def f(a: &T, b: &mut U)` header. + + '0' = not a reference, '1' = `&T`, '2' = `&mut T`, '3' = 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_kind: 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] = char(51) # '3' + else: buf[cnt] = char(48 + seg_kind) # '0' / '1' / '2' + cnt += 1 + if c == ')': + break + seg_len = 0 + in_ann = 0 + ann_pos = 0 + seg_kind = 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 == '&': + seg_kind = 1 + if i + 4 < n and detail[i + 1] == 'm' and detail[i + 2] == 'u' \ + and detail[i + 3] == 't' and detail[i + 4] == ' ': + seg_kind = 2 + 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: @@ -1721,6 +1780,7 @@ def _CheckCall(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, fname = "function" pflags: str = _ParamNullFlags(pool, t.detail) pown: str = _ParamOwnFlags(pool, t.detail) + pref: str = _ParamRefFlags(pool, t.detail) if argc > param_n and sig.has_vararg == 0: msg: bytes = pool.alloc(sizet(256)) if msg is not None: @@ -1781,6 +1841,15 @@ def _CheckCall(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, 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) + # A `&mut T` parameter only accepts `&mut value` (or a `&mut` binding). + if pref is not None and arg is not None and sizet(pi) < string.strlen(pref) \ + and int(pref[sizet(pi)]) == 50: + sk_p: int = _RefExprKind(arg) + if sk_p == 0: + sk_p = _RefKindOfText(_SymDetailAt(pool, a, arg)) + if sk_p == 1: + _EmitTypeError(pool, doc, arg, + "cannot pass a read-only borrow to a `&mut` parameter: create it with `&mut value`", errors) ai += 1