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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user