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:
@@ -0,0 +1,571 @@
|
||||
//! AST → AST rewriter that runs **after** [`crate::load_module`] and
|
||||
//! **before** anything that consumes a [`Module`] (typecheck, codegen).
|
||||
//!
|
||||
//! ## Iter 16a: nested constructor patterns in `match`
|
||||
//!
|
||||
//! Through Iter 15e, a [`Pattern::Ctor`]'s sub-patterns were restricted to
|
||||
//! [`Pattern::Var`] and [`Pattern::Wild`] — nested ctors like
|
||||
//! `(pat-ctor Cons a (pat-ctor Cons b _))` were rejected by the
|
||||
//! checker (`nested-ctor-pattern-not-allowed`) and silently mishandled
|
||||
//! by the codegen. Iter 16a lifts the gate by **desugaring** nested
|
||||
//! ctor sub-patterns into chains of single-level matches at the AST
|
||||
//! level, before either the checker or codegen sees the module. The
|
||||
//! rewrite is pure (no side effects, no I/O), runs in memory, and
|
||||
//! produces a [`Module`] semantically equivalent to its input.
|
||||
//!
|
||||
//! Lit sub-patterns inside a Ctor remain rejected — that is a separate
|
||||
//! iter. The [`crate::ast::Pattern`] enum, the JSON schema, and every
|
||||
//! on-disk hash are unchanged: the desugar runs **after** `load_module`,
|
||||
//! so canonical bytes computed from the source file are untouched. The
|
||||
//! checker / codegen consume the desugared form.
|
||||
//!
|
||||
//! ## Algorithm
|
||||
//!
|
||||
//! For a [`Term::Match`] whose arms contain a non-flat ctor pattern:
|
||||
//!
|
||||
//! 1. Bottom-up: recursively desugar the scrutinee and every arm body
|
||||
//! first (children are normalised before parents).
|
||||
//! 2. Let-bind the scrutinee to a fresh name `$mp_N` (so that we don't
|
||||
//! re-evaluate effectful scrutinees per arm).
|
||||
//! 3. Build a chain of single-level matches via
|
||||
//! `build_chain(s_var, arms, default)` where `default` is a unit
|
||||
//! literal — a placeholder for the unreachable fallthrough. Valid
|
||||
//! programs never reach it because the checker requires
|
||||
//! exhaustiveness (the catch-all arm dominates the chain).
|
||||
//! 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::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
|
||||
//! `Term::Match` with two arms: the flat ctor and a wildcard
|
||||
//! fall-through into the rest of the chain.
|
||||
//!
|
||||
//! `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),
|
||||
//! (Wild, fall_k)])`. The recursion is what handles arbitrary
|
||||
//! nesting depth (`Cons a (Cons b (Cons c _))`).
|
||||
//!
|
||||
//! The fall-through term is cloned per inner match because each branch
|
||||
//! that fails must continue to the same fallthrough. Worst-case size is
|
||||
//! O(arms × depth); 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 a user *could* write
|
||||
//! `$mp_0` themselves. To avoid collisions, the fresh-name generator
|
||||
//! reads a pre-collected set of every name appearing in any
|
||||
//! [`crate::ast::Term::Var`] or [`crate::ast::Pattern::Var`] of the
|
||||
//! module and bumps the counter until it lands on a name not in that
|
||||
//! set.
|
||||
//!
|
||||
//! ## What this module deliberately does not do
|
||||
//!
|
||||
//! - It does not alter [`crate::ast::Pattern`] or [`crate::ast::Term`].
|
||||
//! - It does not introduce a new IR or hashable form.
|
||||
//! - It does not implement Maranget-style decision trees; the rewrite
|
||||
//! is a literal-translation chain that the existing single-level
|
||||
//! match codegen already handles.
|
||||
|
||||
use crate::ast::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// Rewrite `m` so that every [`Pattern::Ctor`] sub-pattern is a
|
||||
/// [`Pattern::Var`] or [`Pattern::Wild`] (i.e. flat).
|
||||
///
|
||||
/// Pure / total: returns a new [`Module`]; does not mutate `m`.
|
||||
/// Idempotent: a module that is already flat is returned as-is
|
||||
/// modulo cloning.
|
||||
pub fn desugar_module(m: &Module) -> Module {
|
||||
let mut used: BTreeSet<String> = BTreeSet::new();
|
||||
for def in &m.defs {
|
||||
match def {
|
||||
Def::Fn(f) => collect_used_in_term(&f.body, &mut used),
|
||||
Def::Const(c) => collect_used_in_term(&c.value, &mut used),
|
||||
Def::Type(_) => {}
|
||||
}
|
||||
}
|
||||
let mut d = Desugarer { counter: 0, used };
|
||||
let mut out = m.clone();
|
||||
for def in &mut out.defs {
|
||||
match def {
|
||||
Def::Fn(f) => f.body = d.desugar_term(&f.body),
|
||||
Def::Const(c) => c.value = d.desugar_term(&c.value),
|
||||
Def::Type(_) => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Walks a term and inserts every [`Term::Var.name`] and every
|
||||
/// [`Pattern::Var.name`] into `used`. Used to seed
|
||||
/// [`Desugarer::fresh`] so a generated name `$mp_N` cannot shadow a
|
||||
/// source-level identifier.
|
||||
fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
|
||||
match t {
|
||||
Term::Lit { .. } => {}
|
||||
Term::Var { name } => {
|
||||
used.insert(name.clone());
|
||||
}
|
||||
Term::App { callee, args, .. } => {
|
||||
collect_used_in_term(callee, used);
|
||||
for a in args {
|
||||
collect_used_in_term(a, used);
|
||||
}
|
||||
}
|
||||
Term::Let { name, value, body } => {
|
||||
used.insert(name.clone());
|
||||
collect_used_in_term(value, used);
|
||||
collect_used_in_term(body, used);
|
||||
}
|
||||
Term::If { cond, then, else_ } => {
|
||||
collect_used_in_term(cond, used);
|
||||
collect_used_in_term(then, used);
|
||||
collect_used_in_term(else_, used);
|
||||
}
|
||||
Term::Do { args, .. } => {
|
||||
for a in args {
|
||||
collect_used_in_term(a, used);
|
||||
}
|
||||
}
|
||||
Term::Ctor { args, .. } => {
|
||||
for a in args {
|
||||
collect_used_in_term(a, used);
|
||||
}
|
||||
}
|
||||
Term::Match { scrutinee, arms } => {
|
||||
collect_used_in_term(scrutinee, used);
|
||||
for arm in arms {
|
||||
collect_used_in_pattern(&arm.pat, used);
|
||||
collect_used_in_term(&arm.body, used);
|
||||
}
|
||||
}
|
||||
Term::Lam { params, body, .. } => {
|
||||
for p in params {
|
||||
used.insert(p.clone());
|
||||
}
|
||||
collect_used_in_term(body, used);
|
||||
}
|
||||
Term::Seq { lhs, rhs } => {
|
||||
collect_used_in_term(lhs, used);
|
||||
collect_used_in_term(rhs, used);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Walks a pattern and inserts every [`Pattern::Var.name`] into `used`.
|
||||
fn collect_used_in_pattern(p: &Pattern, used: &mut BTreeSet<String>) {
|
||||
match p {
|
||||
Pattern::Wild => {}
|
||||
Pattern::Var { name } => {
|
||||
used.insert(name.clone());
|
||||
}
|
||||
Pattern::Lit { .. } => {}
|
||||
Pattern::Ctor { fields, .. } => {
|
||||
for sub in fields {
|
||||
collect_used_in_pattern(sub, used);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// State carried across a module-wide desugar pass.
|
||||
///
|
||||
/// `counter` is incremented for every fresh-name request; `used`
|
||||
/// contains every source-level identifier (var-bind or var-reference)
|
||||
/// in the module so that [`fresh`](Self::fresh) cannot shadow one.
|
||||
struct Desugarer {
|
||||
counter: u64,
|
||||
used: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl Desugarer {
|
||||
/// Returns a name of the form `$mp_N` not present in `used`.
|
||||
/// Increments the counter until it lands on a free name; the
|
||||
/// generated name itself is added to `used` so a subsequent call
|
||||
/// returns a distinct one.
|
||||
fn fresh(&mut self) -> String {
|
||||
loop {
|
||||
let n = format!("$mp_{}", self.counter);
|
||||
self.counter += 1;
|
||||
if !self.used.contains(&n) {
|
||||
self.used.insert(n.clone());
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively rewrites `t`: descends into every child first
|
||||
/// (bottom-up), then dispatches a [`Term::Match`] to
|
||||
/// [`desugar_match`](Self::desugar_match).
|
||||
fn desugar_term(&mut self, t: &Term) -> Term {
|
||||
match t {
|
||||
Term::Lit { .. } | Term::Var { .. } => t.clone(),
|
||||
Term::App { callee, args, tail } => Term::App {
|
||||
callee: Box::new(self.desugar_term(callee)),
|
||||
args: args.iter().map(|a| self.desugar_term(a)).collect(),
|
||||
tail: *tail,
|
||||
},
|
||||
Term::Let { name, value, body } => Term::Let {
|
||||
name: name.clone(),
|
||||
value: Box::new(self.desugar_term(value)),
|
||||
body: Box::new(self.desugar_term(body)),
|
||||
},
|
||||
Term::If { cond, then, else_ } => Term::If {
|
||||
cond: Box::new(self.desugar_term(cond)),
|
||||
then: Box::new(self.desugar_term(then)),
|
||||
else_: Box::new(self.desugar_term(else_)),
|
||||
},
|
||||
Term::Do { op, args, tail } => Term::Do {
|
||||
op: op.clone(),
|
||||
args: args.iter().map(|a| self.desugar_term(a)).collect(),
|
||||
tail: *tail,
|
||||
},
|
||||
Term::Ctor { type_name, ctor, args } => Term::Ctor {
|
||||
type_name: type_name.clone(),
|
||||
ctor: ctor.clone(),
|
||||
args: args.iter().map(|a| self.desugar_term(a)).collect(),
|
||||
},
|
||||
Term::Match { scrutinee, arms } => {
|
||||
// Recurse into children first (bottom-up).
|
||||
let scrutinee = self.desugar_term(scrutinee);
|
||||
let arms: Vec<Arm> = arms
|
||||
.iter()
|
||||
.map(|a| Arm {
|
||||
pat: a.pat.clone(),
|
||||
body: self.desugar_term(&a.body),
|
||||
})
|
||||
.collect();
|
||||
self.desugar_match(scrutinee, arms)
|
||||
}
|
||||
Term::Lam {
|
||||
params,
|
||||
param_tys,
|
||||
ret_ty,
|
||||
effects,
|
||||
body,
|
||||
} => Term::Lam {
|
||||
params: params.clone(),
|
||||
param_tys: param_tys.clone(),
|
||||
ret_ty: ret_ty.clone(),
|
||||
effects: effects.clone(),
|
||||
body: Box::new(self.desugar_term(body)),
|
||||
},
|
||||
Term::Seq { lhs, rhs } => Term::Seq {
|
||||
lhs: Box::new(self.desugar_term(lhs)),
|
||||
rhs: Box::new(self.desugar_term(rhs)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Lowers a `match` whose children have already been desugared.
|
||||
///
|
||||
/// If every arm is already flat ([`is_flat`] returns true), the
|
||||
/// match is reconstructed unchanged (no let-binding or chain
|
||||
/// allocation overhead). Otherwise the scrutinee is let-bound to
|
||||
/// a fresh `$mp_N` and the arms are translated to a chain of
|
||||
/// single-level matches over that variable.
|
||||
fn desugar_match(&mut self, scrutinee: Term, arms: Vec<Arm>) -> Term {
|
||||
if arms.iter().all(|a| is_flat(&a.pat)) {
|
||||
return Term::Match {
|
||||
scrutinee: Box::new(scrutinee),
|
||||
arms,
|
||||
};
|
||||
}
|
||||
let s = self.fresh();
|
||||
let s_var = Term::Var { name: s.clone() };
|
||||
// Document: `default` is unreachable for valid programs because
|
||||
// the typechecker requires a catch-all arm (`pat-wild` or
|
||||
// exhaustive ctor coverage). It only exists to give
|
||||
// `build_chain` a well-typed terminator.
|
||||
let default = Term::Lit { lit: Literal::Unit };
|
||||
let chain = self.build_chain(&s_var, &arms, &default);
|
||||
Term::Let {
|
||||
name: s,
|
||||
value: Box::new(scrutinee),
|
||||
body: Box::new(chain),
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively builds a chain of single-arm matches with a shared
|
||||
/// fall-through. Empty arms ⇒ `default`; otherwise the first arm
|
||||
/// is desugared with the rest of the chain as its fall-through.
|
||||
fn build_chain(&mut self, s_var: &Term, arms: &[Arm], default: &Term) -> Term {
|
||||
if arms.is_empty() {
|
||||
return default.clone();
|
||||
}
|
||||
let head = &arms[0];
|
||||
let rest = &arms[1..];
|
||||
let fall_k = self.build_chain(s_var, rest, default);
|
||||
self.desugar_one_arm(s_var, head, fall_k)
|
||||
}
|
||||
|
||||
/// Lowers one arm into a term. Wild/Var arms drop the chain (the
|
||||
/// arm matches everything); Lit and Ctor arms emit a `Term::Match`
|
||||
/// with the desugared head pattern as the first arm and a
|
||||
/// wildcard fall-through to `fall_k`.
|
||||
fn desugar_one_arm(&mut self, s_var: &Term, arm: &Arm, fall_k: Term) -> Term {
|
||||
match &arm.pat {
|
||||
Pattern::Wild => arm.body.clone(),
|
||||
Pattern::Var { name } => Term::Let {
|
||||
name: name.clone(),
|
||||
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::Ctor { ctor, fields } => {
|
||||
// Lift each field to a fresh var; build the flat outer
|
||||
// pattern, then walk right-to-left wrapping each inner
|
||||
// sub-pattern via `wrap_sub` so the deepest field is
|
||||
// matched first inside-out.
|
||||
let fresh_vars: Vec<String> = fields.iter().map(|_| self.fresh()).collect();
|
||||
let flat_fields: Vec<Pattern> = fresh_vars
|
||||
.iter()
|
||||
.map(|n| Pattern::Var { name: n.clone() })
|
||||
.collect();
|
||||
let mut inner = arm.body.clone();
|
||||
for (sub, fv) in fields.iter().zip(fresh_vars.iter()).rev() {
|
||||
inner = self.wrap_sub(fv, sub, inner, &fall_k);
|
||||
}
|
||||
Term::Match {
|
||||
scrutinee: Box::new(s_var.clone()),
|
||||
arms: vec![
|
||||
Arm {
|
||||
pat: Pattern::Ctor {
|
||||
ctor: ctor.clone(),
|
||||
fields: flat_fields,
|
||||
},
|
||||
body: inner,
|
||||
},
|
||||
Arm {
|
||||
pat: Pattern::Wild,
|
||||
body: fall_k,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps `body` so that it only runs when the value bound to `fv`
|
||||
/// matches `sub`. For `Var` / `Wild` the wrap is a let-bind / no-op;
|
||||
/// for nested `Ctor` / `Lit` the wrap is a recursive
|
||||
/// [`desugar_match`](Self::desugar_match) — that recursion is what
|
||||
/// flattens arbitrarily-nested patterns.
|
||||
fn wrap_sub(&mut self, fv: &str, sub: &Pattern, body: Term, fall_k: &Term) -> Term {
|
||||
match sub {
|
||||
Pattern::Wild => body,
|
||||
Pattern::Var { name } => Term::Let {
|
||||
name: name.clone(),
|
||||
value: Box::new(Term::Var {
|
||||
name: fv.to_string(),
|
||||
}),
|
||||
body: Box::new(body),
|
||||
},
|
||||
Pattern::Ctor { .. } | Pattern::Lit { .. } => self.desugar_match(
|
||||
Term::Var {
|
||||
name: fv.to_string(),
|
||||
},
|
||||
vec![
|
||||
Arm {
|
||||
pat: sub.clone(),
|
||||
body,
|
||||
},
|
||||
Arm {
|
||||
pat: Pattern::Wild,
|
||||
body: fall_k.clone(),
|
||||
},
|
||||
],
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn is_flat(p: &Pattern) -> bool {
|
||||
match p {
|
||||
Pattern::Wild | Pattern::Var { .. } | Pattern::Lit { .. } => true,
|
||||
Pattern::Ctor { fields, .. } => fields
|
||||
.iter()
|
||||
.all(|f| matches!(f, Pattern::Var { .. } | Pattern::Wild)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Helper: walk a term, return true iff any [`Pattern::Ctor`] in
|
||||
/// any reachable [`Term::Match`] has a non-flat field.
|
||||
fn any_nested_ctor(t: &Term) -> bool {
|
||||
match t {
|
||||
Term::Lit { .. } | Term::Var { .. } => false,
|
||||
Term::App { callee, args, .. } => {
|
||||
any_nested_ctor(callee) || args.iter().any(any_nested_ctor)
|
||||
}
|
||||
Term::Let { value, body, .. } => any_nested_ctor(value) || any_nested_ctor(body),
|
||||
Term::If { cond, then, else_ } => {
|
||||
any_nested_ctor(cond) || any_nested_ctor(then) || any_nested_ctor(else_)
|
||||
}
|
||||
Term::Do { args, .. } => args.iter().any(any_nested_ctor),
|
||||
Term::Ctor { args, .. } => args.iter().any(any_nested_ctor),
|
||||
Term::Match { scrutinee, arms } => {
|
||||
if any_nested_ctor(scrutinee) {
|
||||
return true;
|
||||
}
|
||||
for a in arms {
|
||||
if !is_flat(&a.pat) {
|
||||
return true;
|
||||
}
|
||||
if any_nested_ctor(&a.body) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Term::Lam { body, .. } => any_nested_ctor(body),
|
||||
Term::Seq { lhs, rhs } => any_nested_ctor(lhs) || any_nested_ctor(rhs),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 16a: a `match` containing `(Cons a (Cons b _))` desugars to
|
||||
/// a tree with no nested ctor sub-patterns. Property: the rewriter's
|
||||
/// output never has a `Pattern::Ctor` whose field is itself a
|
||||
/// `Pattern::Ctor`.
|
||||
#[test]
|
||||
fn nested_cons_cons_flattens() {
|
||||
// Build: match xs of
|
||||
// Cons a (Cons b _) -> ...
|
||||
// _ -> ...
|
||||
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::Var { name: "a".into() },
|
||||
Pattern::Ctor {
|
||||
ctor: "Cons".into(),
|
||||
fields: vec![
|
||||
Pattern::Var { name: "b".into() },
|
||||
Pattern::Wild,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
body: Term::App {
|
||||
callee: Box::new(Term::Var { name: "+".into() }),
|
||||
args: vec![
|
||||
Term::Var { name: "a".into() },
|
||||
Term::Var { name: "b".into() },
|
||||
],
|
||||
tail: false,
|
||||
},
|
||||
},
|
||||
Arm {
|
||||
pat: Pattern::Wild,
|
||||
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
||||
},
|
||||
],
|
||||
};
|
||||
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_nested_ctor(body),
|
||||
"desugarer must remove every nested-ctor sub-pattern; got: {body:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A flat-only match round-trips structurally: there should be no
|
||||
/// extra let-binding or chain wrapping.
|
||||
#[test]
|
||||
fn flat_match_is_unchanged_shape() {
|
||||
let original = Term::Match {
|
||||
scrutinee: Box::new(Term::Var { name: "xs".into() }),
|
||||
arms: vec![
|
||||
Arm {
|
||||
pat: Pattern::Ctor {
|
||||
ctor: "Nil".into(),
|
||||
fields: vec![],
|
||||
},
|
||||
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
||||
},
|
||||
Arm {
|
||||
pat: Pattern::Ctor {
|
||||
ctor: "Cons".into(),
|
||||
fields: vec![
|
||||
Pattern::Var { name: "h".into() },
|
||||
Pattern::Var { name: "t".into() },
|
||||
],
|
||||
},
|
||||
body: Term::Var { name: "h".into() },
|
||||
},
|
||||
],
|
||||
};
|
||||
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: original.clone(),
|
||||
doc: None,
|
||||
})],
|
||||
};
|
||||
let out = desugar_module(&m);
|
||||
let body = match &out.defs[0] {
|
||||
Def::Fn(f) => f.body.clone(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
// Already-flat match must round-trip with the same Match shape
|
||||
// at the top level (no Let-wrap).
|
||||
assert!(matches!(body, Term::Match { .. }));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,12 @@
|
||||
//! - **Workspace loading** — see [`workspace`] and [`load_workspace`].
|
||||
//! Resolves transitive imports of an entry module and returns a
|
||||
//! [`Workspace`].
|
||||
//! - **AST → AST desugaring** — see [`desugar`]. Pure rewrite that runs
|
||||
//! *after* `load_module` and before the typechecker / codegen
|
||||
//! consume a module. Iter 16a flattens nested constructor patterns
|
||||
//! in `match` so downstream stages always see single-level patterns;
|
||||
//! canonical bytes / hashes are unaffected because the pass runs in
|
||||
//! memory only.
|
||||
//!
|
||||
//! ## Place in the pipeline
|
||||
//!
|
||||
@@ -50,6 +56,7 @@
|
||||
|
||||
pub mod ast;
|
||||
pub mod canonical;
|
||||
pub mod desugar;
|
||||
pub mod hash;
|
||||
pub mod pretty;
|
||||
pub mod workspace;
|
||||
|
||||
Reference in New Issue
Block a user