feat(match): enforce nullable match exhaustiveness in the compiler

The language server reported a missing `None` arm while the compiler accepted the same match. Both paths now require `Some(...)` and the empty branch unless a wildcard covers them, and a wildcard arm without a `Some` arm runs for the non-null branch instead of falling through.
This commit is contained in:
TermiNexus committed 2026-11-03 16:31:41 +08:00
1 parent 273792e1be
commit d8884ff7dd
2 files changed
+32 -7

No files matched your search

+31 -6
View File
@@ -315,6 +315,7 @@ novirtual class MatchGenMixin:
if mc is not None:
pk: int = self._opt_pattern_kind(mc.pattern)
if pk == 1: return 1
if pk == 2 and (self._pattern_is_nil(mc.pattern) != 0 or self._pattern_is_none_singleton(mc.pattern) != 0): return 1
return 0
# _handle_optional_ptr_match - match over a nullable pointer (`own U?` / `T?`):
@@ -363,10 +364,26 @@ novirtual class MatchGenMixin:
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)`, `None`, `Nil()` and `_`", "TypeError")
# Both arms must be covered: the compiler enforces what the language server reports.
if (some_case is None and def_case is None) or (nil_case is None and def_case is None):
miss: str = ""
if some_case is None and def_case is None:
miss = "Some(...)"
if nil_case is None and def_case is None:
if string.strlen(miss) > 0: miss = f"{miss}, None"
else: miss = "None"
msg: bytes = pool.alloc(sizet(320))
if msg is not None:
viperlib.snprintf(msg, sizet(320),
"match is not exhaustive: missing branch(es): %s (add the missing `case` or `case _`)", miss)
self.Gen.TypeGenErr.fatal_error(node, str(msg), "TypeError")
mlirlite.build_cond_br(builder, cond, some_bb, nil_bb)
# Some branch.
mlirlite.position_at_end(builder, some_bb)
self.Gen.VarGen.enter_scope(trans.SymTab, SCOPE_MATCH)
# A wildcard arm also covers the non-null branch when no `Some` arm was written.
some_body: ast.MatchCase* = some_case
if some_body is None: some_body = def_case
if some_case is not None:
sp: ast.AST* = some_case.pattern
if sp is not None and sp.kind() == ast.ASTKind.MatchClass:
@@ -381,12 +398,12 @@ novirtual class MatchGenMixin:
mlirlite.build_store(builder, subject_val, slot)
self.Gen.VarGen.define_var(trans.SymTab, ma_s.name, slot)
self.Gen.VarGen.set_var_life(trans.SymTab, ma_s.name, GenTypes.LIFE_FRAME)
if some_case.children is not None:
bi: int = 0
while bi < int(len(some_case.children)):
st: ast.AST* = some_case.children.get(sizet(bi))
bi += 1
if st is not None: self.Gen.StmtGen.translate_stmt(trans, st)
if some_body is not None and some_body.children is not None:
bi: int = 0
while bi < int(len(some_body.children)):
st: ast.AST* = some_body.children.get(sizet(bi))
bi += 1
if st is not None: self.Gen.StmtGen.translate_stmt(trans, st)
self.Gen.VarGen.exit_scope(trans.SymTab)
if mlirlite.builder_cur_block_is_terminated(builder) == 0: mlirlite.build_br(builder, end_bb)
# Nil branch (explicit Nil arm wins over the wildcard arm).
@@ -1109,6 +1126,14 @@ 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_none_singleton(self, pat: ast.AST*) -> int:
"""1 for the `None` literal pattern (MatchSingleton with a CONST_NONE value)."""
if pat is None or pat.kind() != ast.ASTKind.MatchSingleton: return 0
v: ast.AST* = (ast.MatchSingleton*)(pat).value
if v is not None and v.kind() == ast.ASTKind.Constant:
if (ast.Constant*)(v).const_kind == ast.CONST_NONE: return 1
return 0
def _pattern_is_nil(self, pat: ast.AST*) -> int:
"""1 when a pattern is spelled `Nil` / `Nil()`."""
if pat is None: return 0
+1 -1
View File
@@ -1541,7 +1541,7 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
miss: str = ""
nil_name: str = "None"
if nullable_s == 0: nil_name = "Nil"
if has_some == 0: miss = "Some(...)"
if has_some == 0 and has_wild == 0: miss = "Some(...)"
if has_nil == 0 and has_wild == 0:
if string.strlen(miss) > 0: miss = f"{miss}, {nil_name}"
else: miss = nil_name