Iter 16c — literal patterns via desugar

Desugar `Pattern::Lit` (top-level and nested in Ctor) to
`Term::If { cond = (== sv lit), then = body, else_ = fall_k }`.
After 16c, no Pattern::Lit survives the desugar pass; codegen
and typechecker never see one. `is_flat` reclassifies Lit as
non-flat to force the chain machinery; new `build_eq` helper
constructs the equality test per literal kind.

Tests: 99 → 103 (+1 e2e lit_pat_demo, +3 desugar unit). Demo
fixture exercises top-level lit arms (classify) and nested
lit-in-Ctor (categorize_first) with a local IntList; output
100/200/999/-1/0/7.

Known limitations documented in DESIGN.md and JOURNAL: (a)
chain machinery's Unit terminator means non-Wild-terminated
exhaustive matches with lit arms still need a trailing `_`
catch-all to type-check; (b) `==` is currently Int-only, so
Bool/Str/Unit lit patterns desugar correctly but error at
typecheck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 20:47:21 +02:00
parent 8860600e37
commit 4683af6114
6 changed files with 467 additions and 34 deletions
+17 -12
View File
@@ -758,8 +758,10 @@ Iter 14h via qualified `module.Type` / `module.Ctor` references in
both `(con ...)` and `(term-ctor ...)` / `(pat-ctor ...)` positions);
GC for ADT boxes, lambda envs, and closure pairs (Boehm conservative
collector wired up in Iter 14f, see Decision 9); nested constructor
sub-patterns inside `match` (lifted in Iter 16a via the desugar pass
literal sub-patterns are still rejected, see below).
sub-patterns inside `match` (lifted in Iter 16a via the desugar pass);
literal sub-patterns inside a Ctor pattern (lifted in Iter 16c — the
desugar pass rewrites every `Pattern::Lit` to a `Term::If` on `==`,
both at the top level of an arm and inside a Ctor sub-pattern).
- No effect handlers — only the built-in IO and Diverge ops.
- No refinements / SMT escalation.
@@ -772,10 +774,6 @@ literal sub-patterns are still rejected, see below).
instantiation, deferred.
- No higher-rank polymorphism. Passing a polymorphic fn to another
polymorphic fn (`apply(id, 42)`) is not supported.
- No literal sub-patterns inside a Ctor pattern. `(pat-ctor Cons 0 _)`
is rejected (`nested-ctor-pattern-not-allowed` from `ailang-check`)
because the pattern-match backend has no switch-on-i64 yet.
Workaround: bind a var and test in the arm body.
- No local recursive `let`. `let f = ... in ...` only sees `f`'s
binding inside the body, not inside its own RHS — recursion needs
a top-level def.
@@ -788,12 +786,19 @@ What **is** supported (and used as the smoke test for the pipeline):
- `if`, `let`, function calls, recursion.
- Effects on function signatures, with `do op(args)` for direct effect
ops (`io/print_int`, `io/print_bool`, `io/print_str`).
- **ADTs + pattern matching** (Iter 3, extended in Iter 16a). Sub-patterns
of a Ctor pattern may be `Var`, `Wild`, **or another `Ctor`** (the
desugar pass flattens nested Ctor patterns into a chain of let + match
before typecheck/codegen — see `ailang-core::desugar` and Pipeline
above). Literal sub-patterns (`(pat-ctor Cons 0 _)`) are still rejected
by `ailang-check`.
- **ADTs + pattern matching** (Iter 3, extended in Iter 16a/16c).
Sub-patterns of a Ctor pattern may be `Var`, `Wild`, another `Ctor`
(Iter 16a), or a literal (Iter 16c). The desugar pass flattens
nested Ctor patterns into a chain of let + match and rewrites every
`Pattern::Lit` (top-level or sub-) to a `Term::If` on `==` before
typecheck/codegen — see `ailang-core::desugar` and Pipeline above.
- Literal patterns at top level and inside Ctor sub-patterns (Iter 16c,
via desugar). `(pat-lit 0)` and `(pat-ctor Cons (pat-lit 0) _)` both
parse and lower; the rewrite is to `Term::If { cond = (== sv lit) }`,
so any literal kind whose `==` is supported by the typechecker is
authorable. Today that means `Int`; `Bool`/`Str`/`Unit` lit patterns
are accepted by desugar but reach typecheck as a type error because
`==` is `(Int, Int) -> Bool` only.
- **Imports + qualified cross-module references** via dotted names
(Iter 5). Extends to **types and constructors** (Iter 14h): a foreign
module's ADT is referenced as `(con std_pair.Pair a b)`, its ctors as
+125
View File
@@ -3270,3 +3270,128 @@ closure conversion or env-passing rewrite, with the panic at
desugar time as the boundary-marker until then), 16c (Lit-in-
Ctor patterns — would simplify `take`/`drop`), 17a (per-fn
arena, gated on user discussion of memory management).
## Iter 16c — literal patterns via desugar
**Goal.** Lift the last gate that survived the 16a-aux audit:
`Pattern::Lit` was rejected at the codegen level (an internal
error in the Match lowering) and at the typechecker level when
nested inside a Ctor (`nested-ctor-pattern-not-allowed`). 16c
makes lit patterns work **everywhere they parse** — top-level
arms and Ctor sub-patterns alike — by extending the existing
16a desugar pass.
**Design choice.** Desugar `Pattern::Lit { lit }` to
`Term::If { cond = (== sv lit), then = body, else_ = fall_k }`,
where `sv` is the let-bound scrutinee (top-level) or the
field-bound fresh var (sub-pattern). After 16c, no
`Pattern::Lit` survives the desugar pass — the codegen and
typechecker never see one.
Alternatives considered and rejected:
- **Switch-on-i64 in codegen** (a `switch i64` LLVM instruction
per Match with at least one Int-lit arm). Smaller IR for
many-arm dispatch, but adds a second match-lowering path in
codegen and grows the typechecker's exhaustiveness checker
(now needs to reason about lit coverage). The desugar-to-If
approach hands every problem to the existing infrastructure:
`==` is a typed builtin, `Term::If` already lowers correctly,
and the chain machinery from 16a already produces a
fall-through structure that fits.
- **Codegen-level lit-arm rejection only** (allow lit arms past
typecheck and trap at codegen). Worse than today: today's
codegen rejects with an internal error; future codegen would
need a real lowering. Strictly more work for no gain.
The pipeline invariant from 16a/16b.1 holds unchanged: the
desugar runs after `load_module`, so on-disk module hashes are
untouched. `ail diff` and `ail manifest` see only original-source
defs.
**What shipped.**
- `crates/ailang-core/src/desugar.rs` (+~120, of which ~80 are
doc/test): three replacements plus one new helper.
(a) The `Pattern::Lit` arm of `desugar_one_arm` now emits a
`Term::If { cond = (== s_var lit), then = arm.body, else_ =
fall_k }` instead of the old single-arm `Term::Match` with
the lit pattern preserved (which the codegen rejected).
(b) The `Pattern::Lit` branch of `wrap_sub` mirrors (a) on
the field-bound fresh variable, replacing the old recursive
`desugar_match` call.
(c) `is_flat` no longer classifies `Pattern::Lit` as flat —
the early-return path in `desugar_match` would otherwise
leak a Pattern::Lit arm through to typecheck/codegen
unchanged. With the change, lit-arms always take the
let-bind + chain path.
(d) New free function `build_eq(scrutinee, lit) -> Term`:
produces `(app == scrutinee lit)` for Int/Bool/Str and a
`Bool(true)` literal for Unit (every Unit value is equal).
Three new unit tests:
`top_level_lit_desugars_to_if`,
`nested_lit_in_ctor_desugars_to_if`, and
`flat_arm_with_lit_is_no_longer_flat` (regression guard for
the deliberate change in `is_flat`). A new `any_lit_pattern`
walker mirrors `any_nested_ctor`.
- `examples/lit_pat.ailx` + `examples/lit_pat.ail.json` — first
consumer fixture. Two fns: `classify` (top-level lit arms
for 0/1/default → 100/200/999) and `categorize_first`
(`Cons (pat-lit 0) _` nested-lit demonstration over a local
`IntList` ADT). The trailing `_` catch-all in
`categorize_first` is required by the 16a chain
machinery — the chain's terminator is a `Unit` literal, so
a final `_` arm dominates it and keeps the match
type-checking. Output (per line): 100, 200, 999, -1, 0, 7.
- `crates/ail/tests/e2e.rs::lit_pat_demo` (+15 incl. doc): e2e
count 37 → 38.
- `docs/DESIGN.md`: moved the "no literal sub-patterns inside a
Ctor" line out of "What is not (yet) supported" into the
"Recently lifted gates" preamble and into the "What is
supported" list, with a note that `Bool`/`Str`/`Unit` lit
patterns are accepted by desugar but reach typecheck as a
type error today (because `==` is `(Int, Int) -> Bool`
only — extending `==` to those types is a separate iter).
**What deliberately did NOT change.**
- Codegen's `Pattern::Lit` arm at `crates/ailang-codegen/src/lib.rs:1302`
still returns `CodegenError::Internal("MVP: lit patterns in
match not supported")`. Left as a never-reached safety net —
the desugar is the contract; the codegen panic catches future
regressions where a Pattern::Lit slips through.
- Typechecker's `Pattern::Lit` arm in `check_pattern` still runs
(it accepts the pattern but contributes nothing to
exhaustiveness). Same rationale: dead code at the source-AST
level after desugar, but defensive against a future caller
that bypasses desugar.
- `std_list::take` and `std_list::drop` still hand-write the
base-case-via-arm-body workaround (`(case (pat-ctor Cons h t)
(if (== n 0) Nil (...)))`). Refactoring them to use lit
patterns is queued as 16c-aux.
**Tests: 99 → 103 (+4).**
- e2e: 37 → 38 (`lit_pat_demo`).
- `ailang-core::desugar::tests`: 4 → 7 (the three new lit tests).
- All other crates unchanged.
**Cumulative state, post-16c.**
- Stdlib unchanged (5 modules, 29 combinators).
- `Term`/`Pattern`/`Literal` enums unchanged — additive at the
desugar level only, so all pre-existing fixture hashes stay
bit-identical.
- 16a desugar pass now does three jobs: nested-ctor-pattern
flattening (16a), LetRec lift (16b.1), and lit-pattern → If
rewrite (16c). Pass remains the single AST-→-AST hop between
`load_module` and typecheck.
- Compiler bugs surfaced and fixed in dogfood since 14a: 5/5
(unchanged).
**Queue update.** 16c done. Remaining: 16b.2 (LetRec capture —
closure conversion, unchanged), 16b.3 (LetRec let-binding
capture, unchanged), 17a (per-fn arena, gated on user
discussion of memory management, unchanged). 16c-aux
(`std_list::take`/`drop` refactor onto lit patterns) is a
separate iter, gated on orchestrator decision.