Iter 16a: nested constructor patterns via AST desugaring

Lifts the gate that rejected `(pat-ctor Cons a (pat-ctor Cons b _))`.
Approach: a pure AST → AST pass `ailang_core::desugar::desugar_module`
that flattens nested-Ctor sub-patterns into chains of single-level
matches with let-bound fresh vars and duplicate fall-through. Both
ailang-check and ailang-codegen call the pass at pipeline entry.
Hash-relevant canonical bytes are untouched: the pass runs after
load_module, in memory only. `check`'s returned CheckedModule.symbols
still carries hashes of the original defs so `ail diff` / `ail manifest`
see on-disk identities.

Lit sub-patterns inside a Ctor still rejected — that's the narrower
scope of `nested-ctor-pattern-not-allowed` post-16a; the variant doc
reflects the new meaning.

Fresh-name safety: `$` is a valid ident character in form (A), so
the Desugarer pre-walks every Term::Var / Pattern::Var name into a
BTreeSet and bumps its counter past any user-spelled `$mp_N`.

Already-flat matches go through `is_flat()` and emit identical AST
shapes; every existing fixture produces the same output as before.

New: examples/nested_pat.{ailx,ail.json} prints 30 from
first_two_sum on [10, 20, 30]. e2e test
`nested_ctor_pattern_first_two_sum` guards the path.

Tests: 92/92 (e2e 32→33, ailang-core unit 10→12).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 19:10:01 +02:00
parent 0b1b39f829
commit 0e90709a94
8 changed files with 791 additions and 8 deletions
+98
View File
@@ -2670,3 +2670,101 @@ removed fn. Net same.)
**Cumulative state, post-15e.** No new language features. Two
canonical text projections collapsed to one (form A). Internal
cleanup; no behavioural change for any program in the repo.
---
## Iter 16a — nested constructor patterns in `match`
**Goal.** Lift the gate that rejected `(pat-ctor Cons a (pat-ctor Cons b _))`
and similar nested-Ctor sub-patterns. Lit-in-Ctor stays rejected;
that's a separate iter.
**Approach: AST-level desugar before check + codegen.** Pure rewrite
that flattens nested Ctor patterns into chains of single-level
matches with let-bound fresh vars and duplicate fall-through.
Hash-relevant canonical bytes untouched because the pass runs
*after* `load_module`, in memory only. The checker / codegen always
see flat patterns.
**Algorithm sketch.**
For a `Match` whose arms contain nested-Ctor sub-patterns:
1. Bottom-up: recursively desugar scrutinee and arm bodies first.
2. Let-bind the scrutinee to `$mp_N` (single eval).
3. Build a chain `arm_1 (else arm_2 (else ... default))`. Default
is `Lit Unit`, unreachable for valid programs.
4. Each arm's nested Ctor sub-patterns are lifted to fresh vars,
then deepest-first wrapped via `wrap_sub`, which on a Ctor sub
recursively re-enters `desugar_match` — that recursion handles
arbitrary depth.
`fall_k` is cloned per inner level → O(arms × depth) terms in
worst case. Acceptable for typical patterns.
**Fresh-name safety.** `$` is a valid identifier character in form
(A) (the lexer's `Ident` token is "anything not paren/int/string"),
so `$mp_0` is theoretically user-writable. The Desugarer pre-walks
every `Term::Var` and `Pattern::Var` name in the module into a
`BTreeSet<String>` and bumps the counter past collisions.
**Files.**
- New: `crates/ailang-core/src/desugar.rs` (571 LOC including
doc-comments and two unit tests). Public surface is
`pub fn desugar_module(m: &Module) -> Module` — pure, idempotent,
no I/O.
- `crates/ailang-core/src/lib.rs` — `pub mod desugar;` plus a
module-doc bullet describing the new pipeline layer.
- `crates/ailang-check/src/lib.rs` — three public entries
(`check_module`, `check_workspace`, `check`) call
`desugar_module` first. The gate at the old line 1538 is now
narrowed: nested **Ctor** sub-patterns are `unreachable!()`
(desugared away); nested **Lit** still emits the existing
`nested-ctor-pattern-not-allowed` diagnostic. Crucially:
`check`'s returned `CheckedModule.symbols` keeps hashes of
the *original* defs so `ail diff` and `ail manifest` see the
on-disk identity, not a post-desugar one.
- `crates/ailang-codegen/src/lib.rs` — `lower_workspace` desugars
every module up front; `emit_ir` (single-file shortcut) goes
through `lower_workspace` so the desugar runs there too.
- New: `examples/nested_pat.ailx` + `nested_pat.ail.json`.
`first_two_sum` matches `(pat-ctor Cons a (pat-ctor Cons b _))`;
prints `30` for the input `[10, 20, 30]`.
- `crates/ail/tests/e2e.rs` — new
`nested_ctor_pattern_first_two_sum` test.
**Behavioural property of the desugar.** Already-flat matches go
through `is_flat()` and emit identical AST shapes. Empirically:
every existing fixture (std_maybe, std_list, std_either, list_map,
sort, etc.) produces the same observable output as before because
their patterns were already flat — the desugar is a no-op clone
on them.
**Tests: 92/92.**
- e2e: 33 (was 32, +1 for nested_pat).
- ailang-core unit: 12 (was 10, +2 for the desugar tests).
- All other crates unchanged.
**No new compiler bug surfaced.** The transform was straightforward
because the AST already supported nested sub-patterns at the type
level — only the checker gate and codegen drop-on-floor were
artificial walls. Removing them via desugaring (rather than
expanding the codegen) keeps the pattern-matching backend simple
and lets future iters (Lit-in-pattern, exhaustiveness, decision
trees) plug in at the same desugar layer.
**Cumulative state, post-16a.**
- Stdlib: 3 modules, 19 combinators (unchanged from 15d).
- Language: nested Ctor patterns now legal; nested Lit-in-pattern
still rejected (intentional follow-up scope).
- Pipeline: `load → desugar → check → codegen`. The desugar layer
is the natural home for further surface-level smoothing
(Lit-in-pattern, `if`-as-syntactic-sugar, etc.) without
enlarging the core AST.
- Compiler bugs surfaced and fixed in dogfood since 14a: still 4
(no fresh ones in 16a — the desugar landed clean).
**Queue update.** 16a done. Remaining: 15f (`std_pair`, optional);
16b (local recursive `let`); 17a (per-fn arena); future
"lit-in-Ctor" follow-up under 16c if and when needed.