Skip to main content

logicaffeine_kernel/interface/
match_compile.rs

1//! The equation compiler: surface patterns → the kernel's positional `Match` tree.
2//!
3//! The kernel's `Term::Match` is deliberately primitive. Its `cases` vector is indexed by
4//! CONSTRUCTOR, in the inductive's registration order, one case per constructor, each case a
5//! λ-telescope over that constructor's value fields. It has no patterns, no nesting, no
6//! wildcard and no notion of order among alternatives. That is the right primitive — every
7//! extra feature in an eliminator is another thing the trusted base has to be right about —
8//! but it is not how mathematics is written. Lean writes
9//!
10//! ```text
11//! def f : Nat → Nat → Nat
12//!   | 0,      _      => 1
13//!   | _,      0      => 2
14//!   | .succ _, .succ _ => 3
15//! ```
16//!
17//! and compiles it. This module is that compiler: Maranget/Augustsson column-based
18//! pattern-matrix compilation, with Lean's top-to-bottom first-match semantics, producing a
19//! tree of positional `Match` nodes the kernel already certifies.
20//!
21//! # What it guarantees
22//!
23//! * **Registration order, never source order.** A clause is placed in the slot of the
24//!   constructor it NAMES. `Bool` is registered `true` then `false`; writing the `false` clause
25//!   first must not silently compute the other function. For every equal-arity inductive —
26//!   every enumeration — this is the difference between a correct program and a wrong one that
27//!   type-checks.
28//! * **First match wins.** Overlapping equations are split into disjoint constructor cases by
29//!   specialising the matrix top-to-bottom, which is the only reading under which `pick 0 0`
30//!   is `1` rather than `2`.
31//! * **Totality.** A constructor no clause can reach is an error naming the missing case, not a
32//!   silently-invented value and not a partial function. The kernel is total; so is its surface —
33//!   and so is Lean's: `def classify : Nat → Nat | 0 => 0 | Nat.succ (Nat.succ k) => k` answers
34//!   `error: missing cases: (Nat.succ Nat.zero)` (`Lean/Elab/Match.lean:991-992`).
35//! * **Numerals.** `When 0:` is the unary spelling of `0` in whatever inductive the column has —
36//!   Lean's `expandNatValuePattern` (`Lean/Meta/Match/Match.lean:629`), except that the zero and
37//!   the successor are derived from the declaration instead of hard-coded to `Nat`.
38//! * **Nothing is relaxed at the kernel.** This is a SURFACE pass. It emits exactly the
39//!   `Term::Match` the kernel already accepts, one case per constructor at every node. A
40//!   hand-written match with the wrong case count is still `WrongNumberOfCases`.
41//!
42//! # What it deliberately does not do
43//!
44//! The compiled tree is non-dependent: every node's motive is the CONSTANT `λ_:I. T` at the
45//! same result type `T`. That is what a surface equation set means (`f : A → B`, one result
46//! type for every branch). A genuinely dependent elimination is written with an explicit motive
47//! against `Term::Match` directly, which this module never touches.
48
49use super::spelled_names;
50use crate::type_checker::substitute;
51use crate::{
52    infer_type, is_subtype, normalize, surface_elaborate_against, Context, KernelError,
53    KernelResult, Term,
54};
55use std::collections::HashSet;
56
57/// The binder prefix for a field the compiler introduces.
58///
59/// The prefix alone does NOT make the name fresh. `«#fld0»` is a `«quoted name»`, which
60/// `scan_name` accepts everywhere an identifier is read, so a clause binder can be spelled
61/// exactly like one of these — and the variable and catch-all rules bind a pattern variable by
62/// SUBSTITUTING it, column by column, so a collision makes one column's substitution rewrite
63/// what the previous column's had just introduced. `When Pair a «#fld0»: Yield a.` returned
64/// the SECOND field: a well-typed term, certified by both kernels, denoting a different
65/// function. Freshness is therefore CHECKED, against every name the clause set and the
66/// constructor telescopes can spell — see [`State::next_name`].
67const FIELD_MARK: &str = "#fld";
68
69/// A surface pattern, exactly as a `When` clause spells it.
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub enum Pattern {
72    /// `_` — matches anything, binds nothing.
73    Wildcard,
74    /// `k` — matches anything, binds the name to the whole matched value.
75    Var(String),
76    /// `Succ (Succ k)` — a constructor applied to one sub-pattern per VALUE field.
77    Ctor { name: String, args: Vec<Pattern> },
78    /// `0`, `2`, `17` — a NUMERAL, which is the unary spelling of that number in the inductive
79    /// the column has. Lean's `| 0 => … | 1 => …` (`Lean/Meta/Match/Match.lean:629`
80    /// `expandNatValuePattern`, reached through `isNatValueTransition` at :171) expands a `Nat`
81    /// literal pattern into `Nat.zero`/`Nat.succ` applications before the matrix ever sees it,
82    /// and so does this compiler — see `peano_shape`, which derives the pair from the
83    /// declaration rather than hard-coding `Nat`, so a user's own unary numeral type works too.
84    Numeral(u64),
85}
86
87impl Pattern {
88    /// Every name this pattern binds, left to right.
89    pub fn binders(&self) -> Vec<String> {
90        let mut out = Vec::new();
91        self.collect_binders(&mut out);
92        out
93    }
94
95    fn collect_binders(&self, out: &mut Vec<String>) {
96        match self {
97            Pattern::Wildcard | Pattern::Numeral(_) => {}
98            Pattern::Var(n) => out.push(n.clone()),
99            Pattern::Ctor { args, .. } => {
100                for a in args {
101                    a.collect_binders(out);
102                }
103            }
104        }
105    }
106
107    /// How this pattern reads back, for a diagnostic.
108    pub fn render(&self) -> String {
109        match self {
110            Pattern::Wildcard => "_".to_string(),
111            Pattern::Numeral(n) => n.to_string(),
112            Pattern::Var(n) => n.clone(),
113            Pattern::Ctor { name, args } if args.is_empty() => name.clone(),
114            Pattern::Ctor { name, args } => {
115                let rendered: Vec<String> = args
116                    .iter()
117                    .map(|a| match a {
118                        Pattern::Ctor { args, .. } if !args.is_empty() => {
119                            format!("({})", a.render())
120                        }
121                        _ => a.render(),
122                    })
123                    .collect();
124                format!("{name} {}", rendered.join(" "))
125            }
126        }
127    }
128}
129
130/// One equation: one pattern per column, and the body those patterns' variables are free in.
131#[derive(Clone, Debug)]
132pub struct Clause {
133    pub patterns: Vec<Pattern>,
134    pub body: Term,
135}
136
137/// What the compiler noticed on the way through, beyond the term itself.
138#[derive(Clone, Debug, Default, PartialEq, Eq)]
139pub struct CompileReport {
140    /// Clause indices no input can ever select, because an earlier clause already covers
141    /// everything they do. First-match semantics makes them dead rather than wrong, so this is
142    /// information for the surface to surface.
143    ///
144    /// ★ WHAT LEAN DOES WITH THE SAME FACT, and it is not what the previous sentence here
145    /// claimed. Lean does NOT quietly accept a redundant alternative: `Lean/Elab/Match.lean:999`
146    /// calls `logError "redundant alternative"` for every index in `result.unusedAltIdxs`, and
147    /// the run on this box confirms it — `def h : Hue → Bool | .ruby => true | .jade => false |
148    /// _ => true` answers `error: redundant alternative`. The error is non-fatal (`#eval h
149    /// Hue.ruby` still prints `true`, so the declaration is added), and
150    /// `Lean/Elab/Match.lean:985-988` declares the option that turns it off:
151    /// `match.ignoreUnusedAlts`, default `false`.
152    ///
153    /// So the MEANING is the same in both compilers and only the default severity differs: LOGOS
154    /// behaves as Lean does under `set_option match.ignoreUnusedAlts true`. What is still missing
155    /// is the report reaching a reader — `literate_parser::parse_consider` discards this census,
156    /// so a dead clause is silent rather than merely non-fatal. That needs a warning channel on
157    /// `Command`, which this module does not own.
158    pub unreachable_clauses: Vec<usize>,
159}
160
161/// Compile a clause matrix against a list of scrutinees.
162///
163/// `result_type` is the match's motive at every node: a raw type, which the kernel wraps as the
164/// constant motive `λ_:I. result_type`.
165///
166/// Returns the compiled term. Use [`compile_clauses_reporting`] when the caller wants the
167/// unreachable-clause census as well.
168pub fn compile_clauses(
169    ctx: &Context,
170    scrutinees: &[Term],
171    result_type: &Term,
172    clauses: &[Clause],
173) -> KernelResult<Term> {
174    compile_clauses_reporting(ctx, scrutinees, result_type, clauses).map(|(t, _)| t)
175}
176
177/// [`compile_clauses`], plus what the compiler noticed.
178pub fn compile_clauses_reporting(
179    ctx: &Context,
180    scrutinees: &[Term],
181    result_type: &Term,
182    clauses: &[Clause],
183) -> KernelResult<(Term, CompileReport)> {
184    if clauses.is_empty() {
185        return Err(KernelError::CertificationError(
186            "a pattern match needs at least one clause".to_string(),
187        ));
188    }
189    for (i, c) in clauses.iter().enumerate() {
190        if c.patterns.len() != scrutinees.len() {
191            return Err(KernelError::CertificationError(format!(
192                "clause {} has {} pattern(s) but the match has {} scrutinee(s) — every equation \
193                 must cover the same columns",
194                i + 1,
195                c.patterns.len(),
196                scrutinees.len()
197            )));
198        }
199        // A name bound TWICE in one clause is refused, as Lean refuses it. It is not a harmless
200        // shadow: each field is bound by SUBSTITUTING the clause's binder, so the second
201        // occurrence would find nothing left to replace and the body would silently read the
202        // FIRST field for both — a well-typed term denoting a different function. (Writing
203        // `When Cons x x:` to mean "the two fields are equal" needs a decidable equality and a
204        // guard; that is a different feature, not this one by accident.)
205        let mut seen: Vec<String> = Vec::new();
206        for pat in &c.patterns {
207            for b in pat.binders() {
208                if seen.contains(&b) {
209                    return Err(KernelError::CertificationError(format!(
210                        "clause {} binds `{b}` more than once. A pattern variable names ONE \
211                         field; repeating it would silently read the first field for both.",
212                        i + 1
213                    )));
214                }
215                seen.push(b);
216            }
217        }
218    }
219
220    let rows: Vec<Row> = clauses
221        .iter()
222        .enumerate()
223        .map(|(origin, c)| Row {
224            pats: c.patterns.clone(),
225            body: c.body.clone(),
226            origin,
227        })
228        .collect();
229
230    let mut taken: HashSet<String> = HashSet::new();
231    for s in scrutinees {
232        spelled_names(s, &mut taken);
233    }
234    spelled_names(result_type, &mut taken);
235    for c in clauses {
236        for pat in &c.patterns {
237            taken.extend(pat.binders());
238        }
239        spelled_names(&c.body, &mut taken);
240    }
241
242    let mut state = State {
243        fresh: 0,
244        reached: vec![false; clauses.len()],
245        taken,
246    };
247    let term = compile(ctx, scrutinees, result_type, &rows, &[], &mut state)?;
248    let report = CompileReport {
249        unreachable_clauses: state
250            .reached
251            .iter()
252            .enumerate()
253            .filter(|(_, r)| !**r)
254            .map(|(i, _)| i)
255            .collect(),
256    };
257    Ok((term, report))
258}
259
260/// The types a pattern's binders take when it is matched against a value of `disc_ty`.
261///
262/// This is the same constructor-telescope walk the compiler itself does, exposed so a parser
263/// can put the binders in scope with their real types BEFORE it parses the clause body. Two
264/// computations of one fact is how a compiler and its surface drift apart; there is one here.
265pub fn pattern_binder_types(
266    ctx: &Context,
267    disc_ty: &Term,
268    pattern: &Pattern,
269) -> KernelResult<Vec<(String, Term)>> {
270    let mut out = Vec::new();
271    walk_binder_types(ctx, disc_ty, pattern, &mut out)?;
272    Ok(out)
273}
274
275fn walk_binder_types(
276    ctx: &Context,
277    disc_ty: &Term,
278    pattern: &Pattern,
279    out: &mut Vec<(String, Term)>,
280) -> KernelResult<()> {
281    match pattern {
282        // A numeral names a closed value, so like `_` it introduces no binder.
283        Pattern::Wildcard | Pattern::Numeral(_) => Ok(()),
284        Pattern::Var(n) => {
285            out.push((n.clone(), disc_ty.clone()));
286            Ok(())
287        }
288        Pattern::Ctor { name, args } => {
289            let ty = normalize(ctx, disc_ty);
290            let (ind, type_args) = split_type_app(&ty).ok_or_else(|| {
291                KernelError::NotAnInductive(format!(
292                    "`{}` cannot be matched against the constructor pattern `{}`",
293                    ty,
294                    pattern.render()
295                ))
296            })?;
297            let ctor_ty = resolve_constructor(ctx, &ind, name, pattern)?;
298            let params = &type_args[..ctx.inductive_num_params(&ind).min(type_args.len())];
299            let fields = constructor_field_types(&ctor_ty, params, &[]);
300            if fields.len() != args.len() {
301                return Err(arity_error(name, fields.len(), args.len()));
302            }
303            // A DEPENDENT constructor's later field types mention its earlier fields. Each
304            // earlier field's value is recoverable from the sub-pattern that matched it
305            // (`pattern_to_term`); a `_` names nothing, so the dependency is left standing —
306            // scope types are the parser's convenience, and `compile` is the authority.
307            let mut bound: Vec<(String, Term)> = Vec::new();
308            for (i, sub) in args.iter().enumerate() {
309                let (orig, mut fty) = fields[i].clone();
310                for (prev, value) in &bound {
311                    fty = substitute(&fty, prev, value);
312                }
313                walk_binder_types(ctx, &fty, sub, out)?;
314                if let Some(value) = pattern_to_term(sub) {
315                    bound.push((orig, value));
316                }
317                let _ = i;
318            }
319            Ok(())
320        }
321    }
322}
323
324/// A clause's right-hand side, read at the result type the equations declare.
325///
326/// This is the stage that first knows "this body must have type R", in the scope the body is
327/// actually written in — `ctx` here carries the case's field binders. The kernel's `Match` rule
328/// elaborates its cases against nothing, because a case is a λ-telescope whose expected type it
329/// would have to reconstruct from the constructor; the compiler already has the result type at
330/// every leaf. In Lean's terms this is `OfNat` and friends at the expected type: `| true => 1`
331/// in a function declared `→ Cel` is the `Cel` whose field is `1`.
332///
333/// Best-effort by design, in two directions, and both matter:
334///
335/// * a body that ALREADY fits is returned untouched, so nothing that type-checks today is
336///   re-shaped, and the common case costs one `infer_type` rather than a full elaboration;
337/// * a body elaboration cannot reconcile — most often one naming the function being defined,
338///   whose `fix` binder is tied after compilation — is handed on UNCHANGED, so the mismatch is
339///   reported by the kernel against the real expected type. Elaboration may CHOOSE a term; it may
340///   never change what one means.
341fn elaborate_leaf(ctx: &Context, result_type: &Term, body: &Term) -> Term {
342    if matches!(infer_type(ctx, body), Ok(native) if is_subtype(ctx, &native, result_type)) {
343        return body.clone();
344    }
345    surface_elaborate_against(ctx, body, Some(result_type)).unwrap_or_else(|_| body.clone())
346}
347
348/// The VALUE a pattern denotes, when it denotes one: a variable is itself, a constructor
349/// pattern is its constructor applied to its sub-patterns' values, and `_` denotes nothing.
350fn pattern_to_term(pattern: &Pattern) -> Option<Term> {
351    match pattern {
352        // A numeral's value is known but its SPELLING is not — the constructors it expands into
353        // belong to the column's inductive, which this walk does not carry. `compile` expands it
354        // before any case is built; here the dependency is simply left standing, which is what
355        // `None` already means for `_`.
356        Pattern::Wildcard | Pattern::Numeral(_) => None,
357        Pattern::Var(n) => Some(Term::Var(n.clone())),
358        Pattern::Ctor { name, args } => {
359            let mut out = Term::Global(name.clone());
360            for a in args {
361                out = Term::App(Box::new(out), Box::new(pattern_to_term(a)?));
362            }
363            Some(out)
364        }
365    }
366}
367
368// ============================================================
369// THE MATRIX
370// ============================================================
371
372#[derive(Clone, Debug)]
373struct Row {
374    pats: Vec<Pattern>,
375    body: Term,
376    origin: usize,
377}
378
379struct State {
380    fresh: usize,
381    reached: Vec<bool>,
382    /// Every name the input can spell: the clause set's binders and bodies, the scrutinees, the
383    /// result type, and — added as each node resolves its inductive — the constructor
384    /// telescopes' own parameter names. A generated binder is stepped until it is outside this
385    /// set, which is what makes the substitutions capture-free by CHECK rather than by an
386    /// assumption about the lexer.
387    taken: HashSet<String>,
388}
389
390impl State {
391    fn next_name(&mut self) -> String {
392        loop {
393            let candidate = format!("{FIELD_MARK}{}", self.fresh);
394            self.fresh += 1;
395            if !self.taken.contains(&candidate) {
396                return candidate;
397            }
398        }
399    }
400}
401
402/// One step of the constructor path taken to reach a leaf, for the missing-case diagnostic.
403#[derive(Clone)]
404struct Step {
405    ctor: String,
406    arity: usize,
407}
408
409fn compile(
410    ctx: &Context,
411    cols: &[Term],
412    result_type: &Term,
413    rows: &[Row],
414    trail: &[Step],
415    state: &mut State,
416) -> KernelResult<Term> {
417    // No row survives: nothing in the clause set covers this shape. A total language cannot
418    // admit a partial function, so this is where Lean's "missing cases" error lives.
419    let Some(first) = rows.first() else {
420        return Err(KernelError::CertificationError(format!(
421            "this definition is not exhaustive: no equation covers `{}`. A LOGOS definition is a \
422             TOTAL function, so every constructor of the type being matched needs a clause — add \
423             one, or write `When _:` for the rest.",
424            render_trail(trail)
425        )));
426    };
427
428    // Every column consumed: first match wins, and this row is the first.
429    if cols.is_empty() {
430        state.reached[first.origin] = true;
431        return Ok(elaborate_leaf(ctx, result_type, &first.body));
432    }
433
434    // The VARIABLE rule: no row inspects this column's constructor, so the column is just a
435    // binding. Bind it and move on — splitting here would be sound but would fan the tree out
436    // over a distinction no equation makes.
437    // A NUMERAL inspects its column exactly as a constructor pattern does — it IS one, once the
438    // inductive is known — so it must not fall through to the variable rule.
439    if rows
440        .iter()
441        .all(|r| !matches!(r.pats[0], Pattern::Ctor { .. } | Pattern::Numeral(_)))
442    {
443        let bound: Vec<Row> = rows
444            .iter()
445            .map(|r| {
446                let body = match &r.pats[0] {
447                    Pattern::Var(v) => substitute(&r.body, v, &cols[0]),
448                    _ => r.body.clone(),
449                };
450                Row {
451                    pats: r.pats[1..].to_vec(),
452                    body,
453                    origin: r.origin,
454                }
455            })
456            .collect();
457        return compile(ctx, &cols[1..], result_type, &bound, trail, state);
458    }
459
460    // The CONSTRUCTOR rule: split this column over the inductive's constructors, in the
461    // inductive's OWN order.
462    let scrutinee = &cols[0];
463    let disc_ty = normalize(ctx, &infer_type(ctx, scrutinee)?);
464    let (ind, type_args) = split_type_app(&disc_ty).ok_or_else(|| {
465        KernelError::NotAnInductive(format!(
466            "`{disc_ty}` is not an inductive type, so it cannot be matched on"
467        ))
468    })?;
469    let constructors: Vec<(String, Term)> = ctx
470        .get_constructors(&ind)
471        .into_iter()
472        .map(|(n, t)| (n.to_string(), t.clone()))
473        .collect();
474    if constructors.is_empty() {
475        return Err(KernelError::NotAnInductive(format!(
476            "`{ind}` has no constructors, so there is nothing to match"
477        )));
478    }
479
480    // An INDEXED discriminant is refused rather than approximated. The kernel types an indexed
481    // match by a different rule (`infer_indexed_match`): the motive abstracts the indices as
482    // well as the scrutinee, and coverage is over the constructors the indices still ALLOW, not
483    // over all of them. A clause list carries no motive and cannot say which indices it means,
484    // so compiling one into a plain positional match would emit a tree whose case count is right
485    // by luck. Say so instead — an indexed elimination is written against `Term::Match` with the
486    // motive supplied.
487    let num_params = ctx.inductive_num_params(&ind);
488    if type_args.len() > num_params {
489        return Err(KernelError::CertificationError(format!(
490            "`{ind}` is an INDEXED inductive ({} parameter(s), {} index argument(s) here). An \
491             indexed elimination needs an explicit motive over its indices, which a clause list \
492             cannot express — write the `match` directly with its `return` clause.",
493            num_params,
494            type_args.len() - num_params
495        )));
496    }
497
498    // NUMERALS BECOME CONSTRUCTOR PATTERNS HERE, where the column's inductive is finally known —
499    // Lean's `expandNatValuePattern` (`Lean/Meta/Match/Match.lean:629`), at the same point in the
500    // same algorithm. Everything below this line sees only `Ctor`/`Var`/`Wildcard`.
501    let expanded: Vec<Row> = rows
502        .iter()
503        .map(|r| match &r.pats[0] {
504            Pattern::Numeral(n) => {
505                let head = numeral_pattern(ctx, &ind, *n)?;
506                let mut pats = vec![head];
507                pats.extend_from_slice(&r.pats[1..]);
508                Ok(Row {
509                    pats,
510                    body: r.body.clone(),
511                    origin: r.origin,
512                })
513            }
514            _ => Ok(r.clone()),
515        })
516        .collect::<KernelResult<Vec<Row>>>()?;
517    let rows: &[Row] = &expanded;
518
519    // Reject a clause that names something that is not a constructor of THIS inductive before
520    // any slotting happens — mis-slotting a foreign name is exactly the silent wrong answer the
521    // positional case vector used to allow.
522    for row in rows {
523        if let Pattern::Ctor { name, args } = &row.pats[0] {
524            let ctor_ty = resolve_constructor(ctx, &ind, name, &row.pats[0])?;
525            let fields = constructor_field_types(&ctor_ty, &type_args, &[]);
526            if fields.len() != args.len() {
527                return Err(arity_error(name, fields.len(), args.len()));
528            }
529        }
530    }
531
532    // A constructor's own field names are substituted into its later field TYPES, so they are
533    // names this node can spell too — seeded before any binder is generated for it.
534    for (_, ctor_ty) in &constructors {
535        for (field, fty) in constructor_field_types(ctor_ty, &type_args, &[]) {
536            state.taken.insert(field);
537            spelled_names(&fty, &mut state.taken);
538        }
539    }
540
541    let mut cases = Vec::with_capacity(constructors.len());
542    for (ctor_name, ctor_ty) in &constructors {
543        let fields = constructor_field_types(ctor_ty, &type_args, &[]);
544
545        // One fresh binder per value field, with its type closed into this case's scope: a
546        // dependent field type naming an EARLIER field is rewritten to that field's binder.
547        // The context is extended lazily so a nullary constructor — most nodes in most trees —
548        // costs no clone at all.
549        let mut binders: Vec<(String, Term)> = Vec::new();
550        let mut extended: Option<Context> = None;
551        for (i, (_, fty)) in fields.iter().enumerate() {
552            let mut closed = fty.clone();
553            for j in 0..i {
554                closed = substitute(&closed, &fields[j].0, &Term::Var(binders[j].0.clone()));
555            }
556            let name = state.next_name();
557            let base = extended.as_ref().unwrap_or(ctx);
558            extended = Some(base.extend(&name, closed.clone()));
559            binders.push((name, closed));
560        }
561        let inner_ctx: &Context = extended.as_ref().unwrap_or(ctx);
562
563        // `C type_args… field_binders…` — the value this case's scrutinee is known to be, which
564        // is what a variable or catch-all head pattern gets bound to.
565        let mut ctor_value = Term::Global(ctor_name.clone());
566        for ta in &type_args {
567            ctor_value = Term::App(Box::new(ctor_value), Box::new(ta.clone()));
568        }
569        for (name, _) in &binders {
570            ctor_value = Term::App(Box::new(ctor_value), Box::new(Term::Var(name.clone())));
571        }
572
573        // Specialise: keep the rows this constructor can still reach, in their original order.
574        let mut specialised: Vec<Row> = Vec::new();
575        for row in rows {
576            match &row.pats[0] {
577                Pattern::Ctor { name, args } if name == ctor_name => {
578                    let mut pats = args.clone();
579                    pats.extend_from_slice(&row.pats[1..]);
580                    specialised.push(Row { pats, body: row.body.clone(), origin: row.origin });
581                }
582                Pattern::Ctor { .. } => {}
583                Pattern::Wildcard => {
584                    let mut pats = vec![Pattern::Wildcard; binders.len()];
585                    pats.extend_from_slice(&row.pats[1..]);
586                    specialised.push(Row { pats, body: row.body.clone(), origin: row.origin });
587                }
588                Pattern::Var(v) => {
589                    let mut pats = vec![Pattern::Wildcard; binders.len()];
590                    pats.extend_from_slice(&row.pats[1..]);
591                    specialised.push(Row {
592                        pats,
593                        body: substitute(&row.body, v, &ctor_value),
594                        origin: row.origin,
595                    });
596                }
597                // Unreachable by construction — the head column's numerals were rewritten into
598                // constructor patterns the moment `ind` was known, a few lines above. Reported
599                // rather than `unreachable!()`d: a kernel surface answers, it does not abort, and
600                // a future edit that moves the expansion gets a sentence instead of a panic.
601                Pattern::Numeral(n) => {
602                    return Err(KernelError::CertificationError(format!(
603                        "the numeral pattern `{n}` reached case specialisation for `{ind}`                          without being expanded into its unary spelling"
604                    )))
605                }
606            }
607        }
608
609        let mut sub_cols: Vec<Term> = binders.iter().map(|(n, _)| Term::Var(n.clone())).collect();
610        sub_cols.extend_from_slice(&cols[1..]);
611
612        let mut sub_trail = trail.to_vec();
613        sub_trail.push(Step {
614            ctor: ctor_name.clone(),
615            arity: binders.len(),
616        });
617
618        let mut case = compile(
619            inner_ctx,
620            &sub_cols,
621            result_type,
622            &specialised,
623            &sub_trail,
624            state,
625        )?;
626        for (name, ty) in binders.iter().rev() {
627            case = Term::Lambda {
628                param: name.clone(),
629                param_type: Box::new(ty.clone()),
630                body: Box::new(case),
631            };
632        }
633        cases.push(case);
634    }
635
636    Ok(Term::Match {
637        discriminant: Box::new(scrutinee.clone()),
638        motive: Box::new(constant_motive(disc_ty, result_type, state)),
639        cases,
640    })
641}
642
643/// The CANONICAL motive for a non-dependent node: `λ_:I. T`, never the bare `T`.
644///
645/// The kernel's `Match` rule accepts both — it wraps a `Sort`-typed motive as this very
646/// constant before it checks the branches — so from inside the kernel the two are the same
647/// term. They are not the same DECLARATION. Only the λ records which inductive the node
648/// eliminates, and that fact is unrecoverable from a stored tree: the discriminant a compiled
649/// node splits on is a `Var`, whose type needs the local telescope that a consumer walking the
650/// term does not have. Program extraction reads the motive to enumerate the constructors, and a
651/// bare result type left it with none — an arm-less `match n { }` the kernel accepts and no
652/// backend can emit. The compiler holds `disc_ty` at every node already (it cannot split the
653/// column without it), so the richer form costs nothing and is stated once, here, rather than
654/// re-derived differently by each consumer.
655///
656/// The binder is `_` — the spelling [`crate::elaborate::infer_match_motive`] gives the same
657/// motive — stepped to a generated name in the one case where the result type has a free
658/// variable actually called `_`, so the constant can never capture.
659fn constant_motive(disc_ty: Term, result_type: &Term, state: &mut State) -> Term {
660    let param = if crate::type_checker::occurs_free(result_type, "_") {
661        state.next_name()
662    } else {
663        "_".to_string()
664    };
665    Term::Lambda {
666        param,
667        param_type: Box::new(disc_ty),
668        body: Box::new(result_type.clone()),
669    }
670}
671
672// ============================================================
673// CONSTRUCTOR SHAPE
674// ============================================================
675
676/// Split a type application into its head inductive's name and its arguments.
677/// `List A` → `("List", [A])`; `Nat` → `("Nat", [])`.
678fn split_type_app(ty: &Term) -> Option<(String, Vec<Term>)> {
679    let mut args = Vec::new();
680    let mut cur = ty;
681    while let Term::App(f, a) = cur {
682        args.push((**a).clone());
683        cur = f;
684    }
685    args.reverse();
686    match cur {
687        Term::Global(n) => Some((n.clone(), args)),
688        _ => None,
689    }
690}
691
692/// A constructor's VALUE fields: its `Π` telescope past the inductive's parameters, with the
693/// parameters instantiated by the discriminant's own type arguments.
694///
695/// This mirrors `type_checker::compute_case_type`, which is what the kernel will check the
696/// emitted case against. `extra` lets a caller pre-substitute names already bound.
697fn constructor_field_types(
698    ctor_ty: &Term,
699    type_args: &[Term],
700    extra: &[(String, Term)],
701) -> Vec<(String, Term)> {
702    let mut all: Vec<(String, Term)> = Vec::new();
703    let mut cur = ctor_ty;
704    while let Term::Pi {
705        param,
706        param_type,
707        body_type,
708    } = cur
709    {
710        all.push((param.clone(), (**param_type).clone()));
711        cur = body_type;
712    }
713    let split = type_args.len().min(all.len());
714    let params: Vec<String> = all[..split].iter().map(|(n, _)| n.clone()).collect();
715    all[split..]
716        .iter()
717        .map(|(n, t)| {
718            let mut ty = t.clone();
719            for (p, a) in params.iter().zip(type_args.iter()) {
720                if p != "_" {
721                    ty = substitute(&ty, p, a);
722                }
723            }
724            for (n2, t2) in extra {
725                ty = substitute(&ty, n2, t2);
726            }
727            (n.clone(), ty)
728        })
729        .collect()
730}
731
732/// The unary spelling of `n` in `ind` — `2` over `Nat` is `Succ (Succ Zero)`.
733fn numeral_pattern(ctx: &Context, ind: &str, n: u64) -> KernelResult<Pattern> {
734    let (zero, succ) = peano_shape(ctx, ind)?;
735    let mut out = Pattern::Ctor {
736        name: zero,
737        args: Vec::new(),
738    };
739    for _ in 0..n {
740        out = Pattern::Ctor {
741            name: succ.clone(),
742            args: vec![out],
743        };
744    }
745    Ok(out)
746}
747
748/// The two constructors a numeral needs: the one with NO value fields, and the one with exactly
749/// ONE field of the inductive's own type.
750///
751/// Lean hard-codes `Nat.zero`/`Nat.succ` (`Lean/Meta/Match/Match.lean:629`). Deriving the pair
752/// from the DECLARATION costs nothing and is strictly more: a user who writes
753/// `A Tally is either Nought or a Tick with prev: Tally.` gets numeral patterns for free, and a
754/// type that is not unary-numeral-shaped is told so by name instead of silently mis-expanding
755/// into constructors that happen to be called `Zero` and `Succ`.
756fn peano_shape(ctx: &Context, ind: &str) -> KernelResult<(String, String)> {
757    let mut zero: Option<String> = None;
758    let mut succ: Option<String> = None;
759    let ctors = ctx.get_constructors(ind);
760    let shape_error = |why: &str| {
761        KernelError::CertificationError(format!(
762            "a numeral pattern is the unary spelling of a number, and `{ind}` has no unary              numeral form: {why}. Write the constructors out, or match on a type with one              field-free constructor and one that carries a single `{ind}`."
763        ))
764    };
765    for (name, ty) in &ctors {
766        let fields = constructor_field_types(ty, &[], &[]);
767        match fields.len() {
768            0 if zero.replace((*name).to_string()).is_none() => {}
769            1 if matches!(&fields[0].1, Term::Global(g) if g == ind)
770                && succ.replace((*name).to_string()).is_none() => {}
771            _ => {
772                return Err(shape_error(&format!(
773                    "`{name}` takes {} field(s) and cannot be a zero or a successor",
774                    fields.len()
775                )))
776            }
777        }
778    }
779    match (zero, succ) {
780        (Some(z), Some(s)) => Ok((z, s)),
781        (None, _) => Err(shape_error(
782            "it has no field-free constructor to be the zero",
783        )),
784        (_, None) => Err(shape_error(
785            "it has no constructor carrying a single value of itself",
786        )),
787    }
788}
789
790fn resolve_constructor(
791    ctx: &Context,
792    ind: &str,
793    name: &str,
794    pattern: &Pattern,
795) -> KernelResult<Term> {
796    match ctx.constructor_inductive(name) {
797        Some(owner) if owner == ind => Ok(ctx
798            .get_global(name)
799            .cloned()
800            .expect("a registered constructor always has a type")),
801        Some(owner) => Err(KernelError::CertificationError(format!(
802            "`{name}` is a constructor of `{owner}`, not of `{ind}` — the pattern `{}` cannot \
803             match a value of `{ind}`",
804            pattern.render()
805        ))),
806        None => Err(KernelError::CertificationError(format!(
807            "`{name}` is not a constructor of `{ind}`. Its constructors are: {}.",
808            ctx.get_constructors(ind)
809                .iter()
810                .map(|(n, _)| *n)
811                .collect::<Vec<_>>()
812                .join(", ")
813        ))),
814    }
815}
816
817fn arity_error(name: &str, expected: usize, found: usize) -> KernelError {
818    KernelError::CertificationError(format!(
819        "the constructor `{name}` takes {expected} field(s), but the pattern gives it {found}"
820    ))
821}
822
823/// The shape no clause covered, as a pattern a reader can act on.
824fn render_trail(trail: &[Step]) -> String {
825    let mut acc: Option<String> = None;
826    for step in trail.iter().rev() {
827        let mut parts: Vec<String> = Vec::new();
828        for i in 0..step.arity {
829            match (i, &acc) {
830                (0, Some(inner)) if inner.contains(' ') => parts.push(format!("({inner})")),
831                (0, Some(inner)) => parts.push(inner.clone()),
832                _ => parts.push("_".to_string()),
833            }
834        }
835        acc = Some(if parts.is_empty() {
836            step.ctor.clone()
837        } else {
838            format!("{} {}", step.ctor, parts.join(" "))
839        });
840    }
841    acc.unwrap_or_else(|| "_".to_string())
842}