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:
@@ -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"]);
|
||||
}
|
||||
|
||||
@@ -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 <chain>`,
|
||||
// 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-12
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"}
|
||||
@@ -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<Int>: 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))))))))))))
|
||||
Reference in New Issue
Block a user