fix(match): reject a bare Nil pattern

A bare `Nil` parses as a capture, so a nullable match silently treated it as the null branch. Both the compiler and the language server now reject it and point at `None` (nullable `T?`) or `Nil()` (builtin `Option[T]`). Exhaustiveness reports `None` as the missing nullable branch, and the builtin generic type names no longer trigger the unknown-name warning.
This commit is contained in:
TermiNexus committed 2026-11-03 16:11:22 +08:00
1 parent 7289b027ec
commit 7917992deb
2 files changed
+30 -9

No files matched your search

+8 -5
View File
@@ -288,7 +288,7 @@ novirtual class MatchGenMixin:
return 0
# _opt_pattern_kind - classify a nullable-pointer Option pattern:
# 1 = Some(...) / Some, 2 = Nil / Nil(), 3 = wildcard `_`, 0 = unsupported.
# 1 = Some(...) / Some, 2 = None / Nil(), 3 = wildcard `_`, 0 = unsupported.
def _opt_pattern_kind(self, pat: ast.AST*) -> int:
if pat is None: return 0
k: int = pat.kind()
@@ -318,7 +318,7 @@ novirtual class MatchGenMixin:
return 0
# _handle_optional_ptr_match - match over a nullable pointer (`own U?` / `T?`):
# Some(v) -> non-null branch, v = the pointer; Nil -> null branch.
# Some(v) -> non-null branch, v = the pointer; None / Nil() -> null branch.
def _handle_optional_ptr_match(self, trans: HT.Translator*, node: ast.Match*, subject_val: mlirlite.Value*) -> int:
pool: memhub.MemBuddy* = trans.Pool
builder: mlirlite.IRBuilder* = trans._cur_builder
@@ -355,11 +355,14 @@ novirtual class MatchGenMixin:
if mc.guard is not None:
self.Gen.TypeGenErr.fatal_error(mc, "nullable Option match does not support a guard; remove the guard", "NotImplementedError")
pk: int = self._opt_pattern_kind(mc.pattern)
if pk == 2 and mc.pattern is not None and mc.pattern.kind() == ast.ASTKind.MatchAs:
# A bare `Nil` parses as a capture, so accepting it would silently swallow every value.
self.Gen.TypeGenErr.fatal_error(mc.pattern, "`Nil` is not a pattern by itself; use `None` for a nullable `T?` value, or `Nil()` for the builtin `Option[T]` renum", "TypeError")
if pk == 1: some_case = mc
elif pk == 2: nil_case = mc
elif pk == 3: def_case = mc
elif mc.pattern is not None:
self.Gen.TypeGenErr.fatal_error(mc.pattern, "nullable Option match only accepts `Some(v)`, `Nil` / `Nil()` and `_`", "TypeError")
self.Gen.TypeGenErr.fatal_error(mc.pattern, "nullable Option match only accepts `Some(v)`, `None`, `Nil()` and `_`", "TypeError")
mlirlite.build_cond_br(builder, cond, some_bb, nil_bb)
# Some branch.
mlirlite.position_at_end(builder, some_bb)
@@ -557,7 +560,7 @@ novirtual class MatchGenMixin:
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")
self.Gen.TypeGenErr.fatal_error(pat, "`Nil` is not a pattern by itself; use `None` for a nullable `T?` value, or `Nil()` for the builtin `Option[T]` renum", "TypeError")
is_def = 1
has_default = 1
default_case_node = case_node
@@ -1129,7 +1132,7 @@ novirtual class MatchGenMixin:
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")
self.Gen.TypeGenErr.fatal_error(nc.pattern, "`Nil` is not a pattern by itself; use `None` for a nullable `T?` value, or `Nil()` for the builtin `Option[T]` renum", "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
+22 -4
View File
@@ -1035,6 +1035,8 @@ def _IsKnownPlainName(name: str) -> int:
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 string.strcmp(name, "Option") == 0 or string.strcmp(name, "Box") == 0: return 1
if string.strcmp(name, "RawT") == 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
@@ -1045,6 +1047,15 @@ def _IsKnownPlainName(name: str) -> int:
return 0
def _LspBareNil(pat: ast.AST*) -> int:
"""1 when a pattern is a bare `Nil` capture (not the `Nil()` call form)."""
if pat is None: return 0
if pat.kind() != ast.ASTKind.MatchAs: return 0
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
def _LspNilPattern(pat: ast.AST*) -> int:
"""1 when a pattern is spelled `Nil` / `Nil()` (not the MatchSingleton None)."""
if pat is None: return 0
@@ -1517,16 +1528,23 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
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:
bad_nil: int = 0
if _LspBareNil(ck.pattern) != 0:
bad_nil = 1
elif nullable_s == 0 and opt_renum == 0 and _LspNilPattern(ck.pattern) != 0:
bad_nil = 1
if bad_nil != 0:
_EmitTypeError(pool, doc, ck.pattern,
"`Nil` is only valid for a nullable Option match (`own T?` / `T?`) or the builtin Option renum", errors)
"`Nil` is not a pattern by itself; use `None` for a nullable `T?` value, or `Nil()` for the builtin `Option[T]` renum", errors)
_WalkChecks(pool, doc, a, ws, table_pool, ck.children, errors)
if nullable_s != 0 or opt_renum != 0:
miss: str = ""
nil_name: str = "None"
if nullable_s == 0: nil_name = "Nil"
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: miss = f"{miss}, {nil_name}"
else: miss = nil_name
if string.strlen(miss) > 0:
msg_e: bytes = pool.alloc(sizet(320))
if msg_e is not None: