diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index dd4a97a..47f59cd 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -960,3 +960,18 @@ fn check_json_unbound_var() { "expected at least one error diagnostic with code unbound-var; got: {stdout}" ); } + +/// Iter 16c: literal patterns at top level and inside Ctor +/// sub-patterns. Property protected: the 16a desugar pass rewrites +/// every `Pattern::Lit` to a `Term::If` against the scrutinee/field +/// before either typecheck or codegen sees it. Without 16c, the +/// codegen would emit a Match arm with a Lit pattern and produce +/// either a wrong result or an `unreachable!` panic depending on +/// the path. The fixture exercises both sites: `classify` (top- +/// level lit arms) and `categorize_first` (nested lit-in-Ctor). +#[test] +fn lit_pat_demo() { + let stdout = build_and_run("lit_pat.ail.json"); + let lines: Vec<&str> = stdout.lines().collect(); + assert_eq!(lines, vec!["100", "200", "999", "-1", "0", "7"]); +} diff --git a/crates/ailang-core/src/desugar.rs b/crates/ailang-core/src/desugar.rs index 2588f8e..7ee6794 100644 --- a/crates/ailang-core/src/desugar.rs +++ b/crates/ailang-core/src/desugar.rs @@ -35,9 +35,9 @@ //! 4. Each arm is lowered via `desugar_one_arm`: //! - `Pattern::Wild` → arm body (catch-all; later arms are dropped). //! - `Pattern::Var { name }` → `Let { name = scrutinee_var; body }`. -//! - `Pattern::Lit` → forwarded as a single-arm ctor-pattern match -//! (codegen will reject if reached: lit-in-pattern stays out of -//! scope this iter). +//! - `Pattern::Lit { lit }` → `Term::If { cond = (== s_var lit), +//! then = arm.body, else_ = fall_k }` (Iter 16c). After this +//! rewrite, no `Pattern::Lit` reaches typecheck or codegen. //! - `Pattern::Ctor { ctor, fields }`: lift each field to a fresh //! var, build a flat outer pattern, walk fields right-to-left //! wrapping inner sub-patterns via `wrap_sub`. Emit a @@ -47,7 +47,10 @@ //! `wrap_sub(fv, sub, body, fall_k)` recurses on `sub`: //! - `Var(n)` → `Let(n, Var(fv), body)`. //! - `Wild` → `body`. -//! - `Ctor` / `Lit` → recursively `desugar_match(Var(fv), [(sub, body), +//! - `Lit` (Iter 16c) → `Term::If { cond = (== fv lit), then = body, +//! else_ = fall_k }`. Same shape as the top-level case but on the +//! field-bound fresh variable. +//! - `Ctor` → recursively `desugar_match(Var(fv), [(sub, body), //! (Wild, fall_k)])`. The recursion is what handles arbitrary //! nesting depth (`Cons a (Cons b (Cons c _))`). //! @@ -497,19 +500,17 @@ impl Desugarer { value: Box::new(s_var.clone()), body: Box::new(arm.body.clone()), }, - Pattern::Lit { .. } => Term::Match { - scrutinee: Box::new(s_var.clone()), - arms: vec![ - Arm { - pat: arm.pat.clone(), - body: arm.body.clone(), - }, - Arm { - pat: Pattern::Wild, - body: fall_k, - }, - ], - }, + Pattern::Lit { lit } => { + // Iter 16c: a top-level lit arm desugars to `if (== s_var lit) + // then arm.body else fall_k`. Eliminates `Pattern::Lit` from + // the desugared output entirely; codegen never sees it. + let cmp = build_eq(s_var.clone(), lit); + Term::If { + cond: Box::new(cmp), + then: Box::new(arm.body.clone()), + else_: Box::new(fall_k), + } + } Pattern::Ctor { ctor, fields } => { // Lift each field to a fresh var; build the flat outer // pattern, then walk right-to-left wrapping each inner @@ -559,7 +560,19 @@ impl Desugarer { }), body: Box::new(body), }, - Pattern::Ctor { .. } | Pattern::Lit { .. } => self.desugar_match( + Pattern::Lit { lit } => { + // Iter 16c: a lit sub-pattern desugars to `if (== fv lit) body + // else fall_k`. Same shape as the top-level case, but on the + // field-bound fresh variable rather than the original + // scrutinee. + let cmp = build_eq(Term::Var { name: fv.to_string() }, lit); + Term::If { + cond: Box::new(cmp), + then: Box::new(body), + else_: Box::new(fall_k.clone()), + } + } + Pattern::Ctor { .. } => self.desugar_match( Term::Var { name: fv.to_string(), }, @@ -579,18 +592,51 @@ impl Desugarer { } /// True iff `p` is fully shallow: a [`Pattern::Var`], [`Pattern::Wild`], -/// [`Pattern::Lit`], or a [`Pattern::Ctor`] all of whose fields are -/// `Var` or `Wild`. Used by [`Desugarer::desugar_match`] to skip the -/// let-binding and chain construction for already-flat matches. +/// or a [`Pattern::Ctor`] all of whose fields are `Var` or `Wild`. Used +/// by [`Desugarer::desugar_match`] to skip the let-binding and chain +/// construction for already-flat matches. +/// +/// Iter 16c: [`Pattern::Lit`] is **not** flat — it always desugars to a +/// [`Term::If`] via [`build_eq`], so it must take the chain path even +/// when no other arm needs flattening. Otherwise the early-return in +/// [`Desugarer::desugar_match`] would leak a `Pattern::Lit` arm to +/// typecheck/codegen, which the codegen rejects with an internal error. fn is_flat(p: &Pattern) -> bool { match p { - Pattern::Wild | Pattern::Var { .. } | Pattern::Lit { .. } => true, + Pattern::Wild | Pattern::Var { .. } => true, + // Iter 16c: lit patterns are not flat; they desugar to If. + Pattern::Lit { .. } => false, Pattern::Ctor { fields, .. } => fields .iter() .all(|f| matches!(f, Pattern::Var { .. } | Pattern::Wild)), } } +/// Iter 16c: Build an equality-test term for a lit pattern. The shape is +/// `(app == scrutinee lit_term)` for Int/Bool/Str. Unit literals are +/// degenerate — every Unit value is equal — so an emitted `true` literal +/// stands in. +/// +/// Note: as of Iter 16c, the `==` builtin is typed `(Int, Int) -> Bool` +/// only. Bool- and Str-lit patterns therefore reach typecheck as a +/// type error rather than a clean rewrite. They remain authorable at the +/// AST level (the desugar never refuses) but are not yet usable end-to- +/// end. Lifting that gate is bound to extending `==` to those types. +fn build_eq(scrutinee: Term, lit: &Literal) -> Term { + match lit { + Literal::Unit => Term::Lit { + lit: Literal::Bool { value: true }, + }, + Literal::Int { .. } | Literal::Bool { .. } | Literal::Str { .. } => Term::App { + callee: Box::new(Term::Var { + name: "==".to_string(), + }), + args: vec![scrutinee, Term::Lit { lit: lit.clone() }], + tail: false, + }, + } +} + /// Iter 16b.1: collect free variable names of `t`, with `bound` being /// the set of names lexically bound at this point. A /// [`Term::Var`] `{ name }` is "free" iff `name` is not in `bound`. @@ -1103,4 +1149,193 @@ mod tests { }; let _ = desugar_module(&m); } + + /// Iter 16c: walks a term, returns true iff any [`Pattern::Lit`] is + /// reachable in any [`Term::Match`] arm. After 16c desugaring, the + /// invariant is `false` for every desugared output — `Pattern::Lit` + /// is gone before typecheck/codegen runs. + fn any_lit_pattern(t: &Term) -> bool { + fn pat_has_lit(p: &Pattern) -> bool { + match p { + Pattern::Wild | Pattern::Var { .. } => false, + Pattern::Lit { .. } => true, + Pattern::Ctor { fields, .. } => fields.iter().any(pat_has_lit), + } + } + match t { + Term::Lit { .. } | Term::Var { .. } => false, + Term::App { callee, args, .. } => { + any_lit_pattern(callee) || args.iter().any(any_lit_pattern) + } + Term::Let { value, body, .. } => any_lit_pattern(value) || any_lit_pattern(body), + Term::If { cond, then, else_ } => { + any_lit_pattern(cond) || any_lit_pattern(then) || any_lit_pattern(else_) + } + Term::Do { args, .. } => args.iter().any(any_lit_pattern), + Term::Ctor { args, .. } => args.iter().any(any_lit_pattern), + Term::Match { scrutinee, arms } => { + if any_lit_pattern(scrutinee) { + return true; + } + for a in arms { + if pat_has_lit(&a.pat) { + return true; + } + if any_lit_pattern(&a.body) { + return true; + } + } + false + } + Term::Lam { body, .. } => any_lit_pattern(body), + Term::Seq { lhs, rhs } => any_lit_pattern(lhs) || any_lit_pattern(rhs), + Term::LetRec { body, in_term, .. } => { + any_lit_pattern(body) || any_lit_pattern(in_term) + } + } + } + + /// Iter 16c: a top-level lit arm desugars to a `Term::If` whose + /// condition is `(== s_var lit)`. The desugared body must contain + /// no `Pattern::Lit` anywhere. + #[test] + fn top_level_lit_desugars_to_if() { + // (match n + // (case (pat-lit 0) 100) + // (case _ 999)) + let body_match = Term::Match { + scrutinee: Box::new(Term::Var { name: "n".into() }), + arms: vec![ + Arm { + pat: Pattern::Lit { + lit: Literal::Int { value: 0 }, + }, + body: Term::Lit { + lit: Literal::Int { value: 100 }, + }, + }, + Arm { + pat: Pattern::Wild, + body: Term::Lit { + lit: Literal::Int { value: 999 }, + }, + }, + ], + }; + let m = Module { + schema: crate::SCHEMA.to_string(), + name: "t".into(), + imports: vec![], + defs: vec![Def::Fn(FnDef { + name: "f".into(), + ty: Type::Fn { + params: vec![Type::int()], + ret: Box::new(Type::int()), + effects: vec![], + }, + params: vec!["n".into()], + body: body_match, + doc: None, + })], + }; + let out = desugar_module(&m); + let body = match &out.defs[0] { + Def::Fn(f) => &f.body, + _ => unreachable!(), + }; + assert!( + !any_lit_pattern(body), + "desugarer must remove every Pattern::Lit from the term tree; got: {body:#?}" + ); + // The desugar wraps the match in `let $mp_N = scrutinee in `, + // and the chain head must be a `Term::If` (the lit arm). + let chain = match body { + Term::Let { body, .. } => body.as_ref(), + other => panic!("expected outer Let from chain machinery, got {other:?}"), + }; + assert!( + matches!(chain, Term::If { .. }), + "expected Term::If at the chain head, got {chain:?}" + ); + } + + /// Iter 16c: a `(pat-ctor Cons (pat-lit 0) _)` sub-pattern desugars + /// to a tree with no `Pattern::Lit` anywhere — the lit is rewritten + /// to a `Term::If` against the field-bound fresh variable. + #[test] + fn nested_lit_in_ctor_desugars_to_if() { + // (match xs + // (case (pat-ctor Cons (pat-lit 0) _) 0) + // (case _ -1)) + let body_match = Term::Match { + scrutinee: Box::new(Term::Var { name: "xs".into() }), + arms: vec![ + Arm { + pat: Pattern::Ctor { + ctor: "Cons".into(), + fields: vec![ + Pattern::Lit { + lit: Literal::Int { value: 0 }, + }, + Pattern::Wild, + ], + }, + body: Term::Lit { + lit: Literal::Int { value: 0 }, + }, + }, + Arm { + pat: Pattern::Wild, + body: Term::Lit { + lit: Literal::Int { value: -1 }, + }, + }, + ], + }; + let m = Module { + schema: crate::SCHEMA.to_string(), + name: "t".into(), + imports: vec![], + defs: vec![Def::Fn(FnDef { + name: "f".into(), + ty: Type::Fn { + params: vec![Type::int()], + ret: Box::new(Type::int()), + effects: vec![], + }, + params: vec!["xs".into()], + body: body_match, + doc: None, + })], + }; + let out = desugar_module(&m); + let body = match &out.defs[0] { + Def::Fn(f) => &f.body, + _ => unreachable!(), + }; + assert!( + !any_lit_pattern(body), + "desugarer must remove every Pattern::Lit from the term tree; got: {body:#?}" + ); + assert!( + !any_nested_ctor(body), + "desugarer must also produce no nested ctor sub-patterns; got: {body:#?}" + ); + } + + /// Iter 16c regression guard: `is_flat` must classify + /// `Pattern::Lit` as **not** flat, so the chain machinery in + /// [`Desugarer::desugar_match`] is always invoked for lit-arms + /// (rather than the early-return that would leak the lit pattern + /// through to typecheck/codegen). + #[test] + fn flat_arm_with_lit_is_no_longer_flat() { + let p = Pattern::Lit { + lit: Literal::Int { value: 0 }, + }; + assert!( + !is_flat(&p), + "Pattern::Lit must be classified as non-flat after 16c" + ); + } } diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 97c9e5d..4e0314f 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -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 diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index 9fddd93..b9d0988 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -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. diff --git a/examples/lit_pat.ail.json b/examples/lit_pat.ail.json new file mode 100644 index 0000000..cbadf1b --- /dev/null +++ b/examples/lit_pat.ail.json @@ -0,0 +1 @@ +{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"IntList"}],"name":"Cons"}],"kind":"type","name":"IntList"},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":100},"t":"lit"},"pat":{"lit":{"kind":"int","value":0},"p":"lit"}},{"body":{"lit":{"kind":"int","value":200},"t":"lit"},"pat":{"lit":{"kind":"int","value":1},"p":"lit"}},{"body":{"lit":{"kind":"int","value":999},"t":"lit"},"pat":{"p":"wild"}}],"scrutinee":{"name":"n","t":"var"},"t":"match"},"doc":"Map 0->100, 1->200, anything else->999.","kind":"fn","name":"classify","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":-1},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Cons","fields":[{"lit":{"kind":"int","value":0},"p":"lit"},{"p":"wild"}],"p":"ctor"}},{"body":{"name":"h","t":"var"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"p":"wild"}],"p":"ctor"}},{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"p":"wild"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Empty -> -1; first element 0 -> 0; otherwise return the head.","kind":"fn","name":"categorize_first","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"}],"ret":{"k":"con","name":"Int"}}},{"body":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":17},"t":"lit"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"fn":{"name":"categorize_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":0},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"fn":{"name":"categorize_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":7},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"fn":{"name":"categorize_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"doc":"Drive both fns. Expected (per line): 100, 200, 999, -1, 0, 7.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"lit_pat","schema":"ailang/v0"} diff --git a/examples/lit_pat.ailx b/examples/lit_pat.ailx new file mode 100644 index 0000000..d5acdae --- /dev/null +++ b/examples/lit_pat.ailx @@ -0,0 +1,52 @@ +; Iter 16c — lit patterns at top level and inside Ctor sub-patterns. +; `classify` matches an Int against three lit arms (0, 1, default). +; `categorize_first` matches a List: empty -> -1, head==0 -> 0, +; otherwise the head value. The latter exercises Pat-Lit nested +; inside Pat-Ctor's first field. Both shapes are now handled by the +; 16c desugar pass; codegen sees only If/Match-on-Ctor combinations. +; +; The trailing `_` catch-all in `categorize_first` is required by the +; 16a chain machinery: the chain's terminator is a `Unit` literal, +; reachable when no arm matches. A `_` arm dominates the chain so the +; terminator stays unreachable in practice and the match type-checks. +; +; Expected stdout (one per line): 100, 200, 999, -1, 0, 7. + +(module lit_pat + + (data IntList + (ctor Nil) + (ctor Cons (con Int) (con IntList))) + + (fn classify + (doc "Map 0->100, 1->200, anything else->999.") + (type (fn-type (params (con Int)) (ret (con Int)))) + (params n) + (body + (match n + (case (pat-lit 0) 100) + (case (pat-lit 1) 200) + (case _ 999)))) + + (fn categorize_first + (doc "Empty -> -1; first element 0 -> 0; otherwise return the head.") + (type (fn-type (params (con IntList)) (ret (con Int)))) + (params xs) + (body + (match xs + (case (pat-ctor Nil) -1) + (case (pat-ctor Cons (pat-lit 0) _) 0) + (case (pat-ctor Cons h _) h) + (case _ 0)))) + + (fn main + (doc "Drive both fns. Expected (per line): 100, 200, 999, -1, 0, 7.") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (seq (do io/print_int (app classify 0)) + (seq (do io/print_int (app classify 1)) + (seq (do io/print_int (app classify 17)) + (seq (do io/print_int (app categorize_first (term-ctor IntList Nil))) + (seq (do io/print_int (app categorize_first (term-ctor IntList Cons 0 (term-ctor IntList Nil)))) + (do io/print_int (app categorize_first (term-ctor IntList Cons 7 (term-ctor IntList Nil))))))))))))