From 7917992deb4e77218899a27906896cd98a1e8e5f Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 3 Nov 2026 16:11:22 +0800 Subject: [PATCH] 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. --- App/lib/core/Gen/MatchGenMixin.vp | 13 ++++++++----- App/lib/lsp/LspChecks.vp | 26 ++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/App/lib/core/Gen/MatchGenMixin.vp b/App/lib/core/Gen/MatchGenMixin.vp index 965091c..f0e1196 100644 --- a/App/lib/core/Gen/MatchGenMixin.vp +++ b/App/lib/core/Gen/MatchGenMixin.vp @@ -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 diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index a8fc36f..08bd086 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -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: