feat(lsp): unknown-name warnings, Nil validation and match exhaustiveness

`Nil` is rejected outside nullable/`Option` matches in both the compiler and the language server, non-exhaustive nullable matches report the missing branch names, and unresolved plain names are published as warnings. Diagnostics now honour ParseError.severity.
This commit is contained in:
TermiNexus committed 2026-11-03 16:00:13 +08:00
1 parent 2781ba7466
commit 7289b027ec
4 files changed
+163 -7

No files matched your search

+26 -2
View File
@@ -81,8 +81,10 @@ novirtual class MatchGenMixin:
dpat: ast.AST* = ast.MatchCase*(dcase).pattern
if dpat is not None:
if dpat.kind() == ast.ASTKind.MatchAs and ast.MatchAs*(dpat).pattern is None:
# Count only guardless wildcard arms; guarded `case _ if ...` arms may repeat and are not counted as default.
if ast.MatchCase*(dcase).guard is None: default_cnt = default_cnt + 1
# `case Nil` is an Option pattern, not a wildcard; count only guardless `case _`.
dname_chk: str = ast.MatchAs*(dpat).name
if dname_chk is None or string.strcmp(dname_chk, "Nil") != 0:
if ast.MatchCase*(dcase).guard is None: default_cnt = default_cnt + 1
elif m_node.is_switch == 0:
dcj: int = dci + 1
while dcj < ccn:
@@ -554,6 +556,8 @@ novirtual class MatchGenMixin:
elif pk == ast.ASTKind.MatchAs:
ma: ast.MatchAs* = ast.MatchAs*(pat)
if ma.pattern is None:
if ma.name is not None and string.strcmp(ma.name, "Nil") == 0:
self.Gen.TypeGenErr.fatal_error(pat, "`Nil` is only valid for a nullable Option match (`own T?` / `T?`), not a renum match", "TypeError")
is_def = 1
has_default = 1
default_case_node = case_node
@@ -1102,10 +1106,30 @@ novirtual class MatchGenMixin:
gzero: mlirlite.Value* = mlirlite.const_int(pool, gbits if gbits > 0 else 32, 0)
return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, gv, gzero)
def _pattern_is_nil(self, pat: ast.AST*) -> int:
"""1 when a pattern is spelled `Nil` / `Nil()`."""
if pat is None: return 0
if pat.kind() == ast.ASTKind.MatchAs:
ma: ast.MatchAs* = ast.MatchAs*(pat)
if ma.pattern is None and ma.name is not None and string.strcmp(ma.name, "Nil") == 0: return 1
return 0
if pat.kind() == ast.ASTKind.MatchClass:
vnm_n: str = self._extract_variant_name_from_cls(pat)
if vnm_n is not None and string.strcmp(vnm_n, "Nil") == 0: return 1
return 0
def _handle_simple_match(self, trans: HT.Translator*, node: ast.Match*, subject_val: mlirlite.Value*) -> int:
pool: memhub.MemBuddy* = trans.Pool
builder: mlirlite.IRBuilder* = trans._cur_builder
func: mlirlite.Function* = trans._cur_func
# `Nil` is an Option pattern, never a wildcard/capture.
if node.cases is not None:
ni: int = 0
while ni < int(len(node.cases)):
nc: ast.MatchCase* = ast.MatchCase*(node.cases.get(sizet(ni)))
ni += 1
if nc is not None and self._pattern_is_nil(nc.pattern) != 0:
self.Gen.TypeGenErr.fatal_error(nc.pattern, "`Nil` is only valid for a nullable Option match (`own T?` / `T?`) or the builtin Option renum; it is not a wildcard", "TypeError")
# Same as the main path: locally track this simple match's br end_bb and overwrite _ctrl_merge_reachable at the end.
sm_merge_reach: int = 0
+134 -2
View File
@@ -608,7 +608,8 @@ def _ExprLen(doc: LspDocuments.LspDocument*, node: ast.AST*) -> int:
def _EmitTypeError(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
node: ast.AST*, msg: str, errors: list[ast.ParseError*]*):
node: ast.AST*, msg: str, errors: list[ast.ParseError*]*,
severity: int = 1):
if node is None or msg is None or errors is None:
return
if int(len(errors)) >= LSP_CHECK_DIAG_CAP:
@@ -638,6 +639,7 @@ def _EmitTypeError(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
e.col = sc
e.end_line = line0 + 1
e.end_col = ec
e.severity = severity
errors.append(e)
@@ -996,6 +998,97 @@ def _NullableDetail(det: str) -> int:
return 0
def _OccAtExpr(a: LspSymbols.LspAnalysis*, expr: ast.AST*) -> LspSymbols.LspOccurrence*:
"""Occurrence at a Name / Attribute expression's identifier position."""
if a is None or expr is None: return None
line0: int = 0
col: int = 0
name: str = None
if expr.kind() == ast.ASTKind.Name:
nm: ast.Name* = (ast.Name*)(expr)
if nm.id is None: return None
line0 = nm.lineno - 1
col = nm.col_offset
name = nm.id
elif expr.kind() == ast.ASTKind.Attribute:
at: ast.Attribute* = (ast.Attribute*)(expr)
if at.attr is None: return None
line0 = at.lineno - 1
col = at.col_offset
name = at.attr
else:
return None
colu: int = col
if a.doc is not None: colu = LspSource.ByteColToUtf16(a.doc, line0, col)
if a.occs is not None:
i: int = 0
while i < int(len(a.occs)):
o: LspSymbols.LspOccurrence* = a.occs.get(sizet(i))
i += 1
if o is not None and o.line == line0 and o.sc == colu and o.name is not None:
if string.strcmp(o.name, name) == 0: return o
return None
def _IsKnownPlainName(name: str) -> int:
if name is None: return 0
if string.strcmp(name, "self") == 0 or string.strcmp(name, "cls") == 0: return 1
if string.strcmp(name, "None") == 0 or string.strcmp(name, "True") == 0 or string.strcmp(name, "False") == 0: return 1
if string.strcmp(name, "Some") == 0 or string.strcmp(name, "Nil") == 0: return 1
if LspSymbols._IsBuiltinTypeName(name) != 0: return 1
if string.strcmp(name, "print") == 0 or string.strcmp(name, "len") == 0 or string.strcmp(name, "range") == 0: return 1
if string.strcmp(name, "malloc") == 0 or string.strcmp(name, "free") == 0 or string.strcmp(name, "sizeof") == 0: return 1
if string.strcmp(name, "memcpy") == 0 or string.strcmp(name, "memset") == 0: return 1
if string.strcmp(name, "exit") == 0 or string.strcmp(name, "abort") == 0: return 1
if string.strcmp(name, "min") == 0 or string.strcmp(name, "max") == 0 or string.strcmp(name, "abs") == 0: return 1
if string.strcmp(name, "ord") == 0 or string.strcmp(name, "chr") == 0: return 1
return 0
def _LspNilPattern(pat: ast.AST*) -> int:
"""1 when a pattern is spelled `Nil` / `Nil()` (not the MatchSingleton None)."""
if pat is None: return 0
k: int = pat.kind()
if k == ast.ASTKind.MatchAs:
ma: ast.MatchAs* = (ast.MatchAs*)(pat)
if ma.pattern is None and ma.name is not None and string.strcmp(ma.name, "Nil") == 0: return 1
return 0
if k == ast.ASTKind.MatchClass:
cls: ast.AST* = (ast.MatchClass*)(pat).cls
if cls is not None and cls.kind() == ast.ASTKind.Attribute:
at: ast.Attribute* = (ast.Attribute*)(cls)
if at.attr is not None and string.strcmp(at.attr, "Nil") == 0: return 1
if cls is not None and cls.kind() == ast.ASTKind.Name:
nm: ast.Name* = (ast.Name*)(cls)
if nm.id is not None and string.strcmp(nm.id, "Nil") == 0: return 1
return 0
def _LspOptPatternKind(pat: ast.AST*) -> int:
"""1=Some, 2=Nil/None, 3=wildcard/capture, 0=other."""
if pat is None: return 0
k: int = pat.kind()
if k == ast.ASTKind.MatchSingleton: return 2
if k == ast.ASTKind.MatchAs:
ma: ast.MatchAs* = (ast.MatchAs*)(pat)
if ma.pattern is None:
if ma.name is None: return 3
if string.strcmp(ma.name, "Nil") == 0: return 2
return 3
return 0
if k == ast.ASTKind.MatchClass:
cls: ast.AST* = (ast.MatchClass*)(pat).cls
vnm: str = None
if cls is not None and cls.kind() == ast.ASTKind.Attribute:
vnm = (ast.Attribute*)(cls).attr
elif cls is not None and cls.kind() == ast.ASTKind.Name:
vnm = (ast.Name*)(cls).id
if vnm is not None:
if string.strcmp(vnm, "Some") == 0: return 1
if string.strcmp(vnm, "Nil") == 0: return 2
return 0
def _SymDetailAt(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*,
expr: ast.AST*) -> str:
"""Resolved symbol detail (annotation text) at a Name/Attribute expression."""
@@ -1046,6 +1139,10 @@ def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
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)
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:
_EmitTypeError(pool, doc, e, f"unknown name '{nm_m.id}'", errors, 2)
return
if k == ast.ASTKind.Call:
call: ast.Call* = ast.Call*(e)
@@ -1400,7 +1497,42 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
_WalkChecks(pool, doc, a, ws, table_pool, s.children, errors)
continue
if k == ast.ASTKind.Match:
_WalkChecks(pool, doc, a, ws, table_pool, s.children, errors)
mt: ast.Match* = ast.Match*(s)
_WalkChecksExprs(pool, doc, a, ws, table_pool, mt.subject, errors)
det_s: str = _SymDetailAt(pool, a, mt.subject)
nullable_s: int = _NullableDetail(det_s)
opt_renum: int = 0
if det_s is not None and string.strstr(det_s, "Option[") is not None: opt_renum = 1
has_some: int = 0
has_nil: int = 0
has_wild: int = 0
if mt.cases is not None:
ci: int = 0
while ci < int(len(mt.cases)):
ck: ast.MatchCase* = (ast.MatchCase*)(mt.cases.get(sizet(ci)))
ci += 1
if ck is None: continue
pk_o: int = _LspOptPatternKind(ck.pattern)
if pk_o == 1: has_some = 1
elif pk_o == 2: has_nil = 1
elif pk_o == 3: has_wild = 1
elif _LspNilPattern(ck.pattern) != 0: has_nil = 1
if nullable_s == 0 and opt_renum == 0 and _LspNilPattern(ck.pattern) != 0:
_EmitTypeError(pool, doc, ck.pattern,
"`Nil` is only valid for a nullable Option match (`own T?` / `T?`) or the builtin Option renum", errors)
_WalkChecks(pool, doc, a, ws, table_pool, ck.children, errors)
if nullable_s != 0 or opt_renum != 0:
miss: str = ""
if has_some == 0: miss = "Some(...)"
if has_nil == 0 and has_wild == 0:
if string.strlen(miss) > 0: miss = f"{miss}, Nil"
else: miss = "Nil"
if string.strlen(miss) > 0:
msg_e: bytes = pool.alloc(sizet(320))
if msg_e is not None:
stdio.snprintf(msg_e, sizet(320),
"match is not exhaustive: missing branch(es): %s (add the missing `case` or `case _`)", miss)
_EmitTypeError(pool, doc, mt, str(msg_e), errors)
continue
+3 -1
View File
@@ -65,7 +65,9 @@ def _AddDiagnostic(pool: memhub.MemManager*, arr: json.JsonValue*,
if diag is None:
return
json.object_set(pool, diag, "range", rng)
json.object_set(pool, diag, "severity", json.int_val(pool, 1))
sev: int = e.severity
if sev <= 0: sev = 1
json.object_set(pool, diag, "severity", json.int_val(pool, int64(sev)))
json.object_set(pool, diag, "source", json.string_val(pool, "viperc"))
msg: str = e.message
if msg is None:
-2
View File
@@ -539,8 +539,6 @@ def _LexStream(pool: memhub.MemManager*,
typ = ST_KEYWORD
elif tok.type == ast.TokenType.Name and tok.str_val is not None and string.strcmp(tok.str_val, "Some") == 0:
typ = ST_CLASS
elif tok.type == ast.TokenType.Name and tok.str_val is not None and string.strcmp(tok.str_val, "Nil") == 0:
typ = ST_PROPERTY
elif tok.type == ast.TokenType.Name and _IsBuiltinFunctionName(tok.str_val) != 0:
typ = ST_FUNCTION
end_line0: int = line0