Skip to main content

oxilean_kernel/inductive/
nested.rs

1//! Nested-to-mutual inductive elaboration (C10).
2//!
3//! A *nested* inductive is one whose constructor fields mention the type being
4//! defined under an already-declared foreign container, e.g.
5//! `node : Array Syntax → Syntax` (`Syntax` occurs inside `Array`). Lean's
6//! kernel compiles these by SPECIALIZING each container into an auxiliary
7//! mutual inductive (the family type becomes a fresh leading parameter),
8//! CHECKING that genuinely-mutual family with the ordinary machinery, then
9//! RESTORING recursors stated over the *real* container so the exported
10//! recursor re-verifies by def-eq.
11//!
12//! This module is built in stages (see
13//! `docs/audit/2026-07-17-nested-inductives/design.md`):
14//!
15//! - **S1 (this file, now):** [`detect`] — recover the container `C`, its level
16//!   arguments, its argument spine, and which argument positions mention a
17//!   family-being-defined type. Behaviour of the kernel is unchanged; nested
18//!   inductives are still rejected at the positivity leaf in `derive.rs`.
19//! - **S2:** `specialize` — copy the container's mutual block into fresh
20//!   auxiliaries, abstracting the family-occurring args as new leading params.
21//! - **S3:** `restore` — repackage the derived recursors over the real
22//!   container and drop the abstracted parameters.
23
24use super::derive::{
25    check_and_derive_family, has_const_occ, recursor_matches, DerivedFamily, InductiveSpec,
26};
27use crate::declaration::{ConstantInfo, InductiveVal, RecursorRule, RecursorVal};
28use crate::expr_util::{get_app_fn_args, mk_app, replace_expr};
29use crate::instantiate::instantiate_type_lparams;
30use crate::subst::instantiate;
31use crate::{Environment, Expr, KernelError, Level, Name, NameView, TypeChecker};
32
33/// A nested occurrence `C Ds Is`: a family-being-defined type appears inside the
34/// arguments of an already-declared foreign inductive `C`.
35///
36/// This is the raw *syntactic* capture produced at a strict-positivity leaf. It
37/// does not yet decide whether `C` is a declared inductive, nor whether the
38/// family occurs in a legal parameter position versus an (unsupported) index
39/// position — that validation happens in S2 against `C`'s `InductiveVal`.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct NestedOcc {
42    /// The foreign container inductive `C` (e.g. `List`, `Array`).
43    pub container: Name,
44    /// `C`'s universe-level arguments.
45    pub container_levels: Vec<Level>,
46    /// The full argument spine `Ds ++ Is` of the container application, in
47    /// source order.
48    pub args: Vec<Expr>,
49    /// Indices into [`args`](NestedOcc::args) whose term mentions a
50    /// family-being-defined type. In `Array Syntax` this is `[0]`; these are
51    /// the positions S2 abstracts into fresh leading parameters (after checking
52    /// they are parameter, not index, positions of `C`).
53    pub family_positions: Vec<usize>,
54}
55
56/// Detect a nested occurrence at a strict-positivity leaf.
57///
58/// Given the WHNF of a constructor-field type whose spine is not a direct
59/// application of a family member, return the nested occurrence when the spine
60/// head is a *foreign* constant (not itself a family member) and at least one
61/// argument mentions a family-being-defined type. Returns `None` for
62/// - a non-`Const` head (nothing to un-nest here),
63/// - a direct occurrence of a family member (`C ∈ family_names` — that is
64///   ordinary recursion, handled elsewhere), or
65/// - a foreign constant none of whose arguments mention the family (a plain
66///   non-recursive field such as `List Nat`).
67///
68/// The check is purely syntactic and environment-free so it can be unit-tested
69/// in isolation; declaredness and parameter-position validation are deferred to
70/// S2.
71pub fn detect(family_names: &[Name], ty: &Expr) -> Option<NestedOcc> {
72    let (head, args) = get_app_fn_args(ty);
73    let (container, container_levels) = match head {
74        Expr::Const(n, levels) => (n.clone(), levels.clone()),
75        _ => return None,
76    };
77    if family_names.contains(&container) {
78        // A direct occurrence `T …` is ordinary (mutual) recursion, not nesting.
79        return None;
80    }
81    let family_positions: Vec<usize> = args
82        .iter()
83        .copied()
84        .enumerate()
85        .filter(|(_, a)| has_const_occ(a, family_names))
86        .map(|(i, _)| i)
87        .collect();
88    if family_positions.is_empty() {
89        // `C` is present but carries no family type: an ordinary field.
90        return None;
91    }
92    let args = args.into_iter().cloned().collect();
93    Some(NestedOcc {
94        container,
95        container_levels,
96        args,
97        family_positions,
98    })
99}
100
101// ---------------------------------------------------------------------------
102// S2 — specialization (nested container → auxiliary mutual family).
103// ---------------------------------------------------------------------------
104
105/// How one auxiliary inductive maps back to the real container it specializes.
106/// Retained through derivation so S3 restoration can substitute the auxiliary
107/// type / constructors / recursor back to the real container constants and drop
108/// the abstracted parameters.
109#[derive(Clone, Debug)]
110pub struct AuxMap {
111    /// The fresh auxiliary inductive type name (e.g. `T._nested_0`).
112    pub aux_type: Name,
113    /// The real container type-former it specializes (e.g. `List`).
114    pub container: Name,
115    /// The container's universe-level arguments at the nested occurrence.
116    pub container_levels: Vec<Level>,
117    /// The concrete arguments `As` the container was specialized at (the
118    /// abstracted parameters restoration must re-insert / drop).
119    pub params: Vec<Expr>,
120    /// `(auxiliary ctor name, real container ctor name)` in declaration order.
121    pub aux_ctors: Vec<(Name, Name)>,
122    /// The auxiliary recursor name the kernel will derive (`<aux_type>.rec`).
123    pub aux_rec: Name,
124    /// The real container's recursor name (`<container>.rec`).
125    pub container_rec: Name,
126}
127
128/// The nesting-free mutual family produced by [`specialize`]: the originals (with
129/// nested container applications rewritten to auxiliary references) followed by
130/// the freshly-synthesized auxiliary inductives.
131#[derive(Clone, Debug)]
132pub struct ExpandedFamily {
133    /// Originals (rewritten) followed by auxiliaries — a genuine mutual family.
134    pub specs: Vec<InductiveSpec>,
135    /// Number of leading `specs` that are the original (user-facing) types.
136    pub num_originals: usize,
137    /// One entry per auxiliary type (restoration metadata for S3).
138    pub aux_maps: Vec<AuxMap>,
139    /// Shared level parameters of the family.
140    pub lparams: Vec<Name>,
141    /// Shared parameter count of the family.
142    pub num_params: u32,
143}
144
145/// A nested container application found while scanning a constructor type.
146struct Found {
147    container: Name,
148    levels: Vec<Level>,
149    /// The container's parameter arguments (length = container `numParams`).
150    as_args: Vec<Expr>,
151    num_params: usize,
152}
153
154fn lookup_inductive<'a>(env: &'a Environment, name: &Name) -> Option<&'a InductiveVal> {
155    match env.find(name)? {
156        ConstantInfo::Inductive(iv) => Some(iv),
157        _ => None,
158    }
159}
160
161/// `(full type, level params)` of a constructor, if declared.
162fn lookup_ctor(env: &Environment, name: &Name) -> Option<(Expr, Vec<Name>)> {
163    match env.find(name)? {
164        ConstantInfo::Constructor(cv) => {
165            Some((cv.common.ty.clone(), cv.common.level_params.clone()))
166        }
167        _ => None,
168    }
169}
170
171fn last_component(name: &Name) -> String {
172    match name.view() {
173        NameView::Str(_, s) => s.to_string(),
174        _ => "ctor".to_string(),
175    }
176}
177
178/// Instantiate the leading `args.len()` Pi binders of `ty` with `args`.
179fn instantiate_leading(ty: &Expr, args: &[Expr]) -> Expr {
180    let mut cur = ty.clone();
181    for arg in args {
182        match cur {
183            Expr::Pi(_, _, _dom, body) => {
184                cur = instantiate(&body, arg);
185            }
186            _ => break,
187        }
188    }
189    cur
190}
191
192/// Replace every occurrence of `container container_levels as_args` (matched on
193/// the first `np` arguments) with `aux` applied to the remaining (index)
194/// arguments.
195fn replace_container_app(
196    ty: &Expr,
197    container: &Name,
198    container_levels: &[Level],
199    as_args: &[Expr],
200    np: usize,
201    aux: &Name,
202) -> Expr {
203    replace_expr(ty, &mut |e, _depth| {
204        let (head, args) = get_app_fn_args(e);
205        if let Expr::Const(n, levels) = head {
206            if n == container && levels.as_slice() == container_levels && args.len() >= np {
207                let matches = as_args
208                    .iter()
209                    .zip(args.iter().take(np))
210                    .all(|(a, b)| a == *b);
211                if matches {
212                    let rest: Vec<Expr> = args[np..]
213                        .iter()
214                        .map(|x| {
215                            replace_container_app(x, container, container_levels, as_args, np, aux)
216                        })
217                        .collect();
218                    return Some(mk_app(Expr::Const(aux.clone(), vec![]), &rest));
219                }
220            }
221        }
222        None
223    })
224}
225
226/// Find the first nested container application anywhere inside `e`.
227///
228/// Returns `Err(UnsupportedNestedInductive)` when a family type occurs in an
229/// *index* position of the container (Lean's kernel cannot un-nest those).
230#[allow(clippy::result_large_err)]
231fn scan(env: &Environment, all_family: &[Name], e: &Expr) -> Result<Option<Found>, KernelError> {
232    let (head, args) = get_app_fn_args(e);
233    if let Expr::Const(c, levels) = head {
234        if !all_family.contains(c) {
235            if let Some(iv) = lookup_inductive(env, c) {
236                let np = iv.num_params as usize;
237                if args.len() >= np {
238                    let fam_in_param = args.iter().take(np).any(|a| has_const_occ(a, all_family));
239                    let fam_in_index = args.iter().skip(np).any(|a| has_const_occ(a, all_family));
240                    if fam_in_index {
241                        return Err(KernelError::UnsupportedNestedInductive(
242                            all_family.first().cloned().unwrap_or_default(),
243                        ));
244                    }
245                    if fam_in_param {
246                        let as_args = args.iter().take(np).map(|x| (**x).clone()).collect();
247                        return Ok(Some(Found {
248                            container: c.clone(),
249                            levels: levels.clone(),
250                            as_args,
251                            num_params: np,
252                        }));
253                    }
254                }
255            }
256        }
257    }
258    match e {
259        Expr::App(f, a) => {
260            if let Some(x) = scan(env, all_family, f)? {
261                return Ok(Some(x));
262            }
263            scan(env, all_family, a)
264        }
265        Expr::Lam(_, _, ty, body) | Expr::Pi(_, _, ty, body) => {
266            if let Some(x) = scan(env, all_family, ty)? {
267                return Ok(Some(x));
268            }
269            scan(env, all_family, body)
270        }
271        Expr::Let(_, ty, val, body) => {
272            if let Some(x) = scan(env, all_family, ty)? {
273                return Ok(Some(x));
274            }
275            if let Some(x) = scan(env, all_family, val)? {
276                return Ok(Some(x));
277            }
278            scan(env, all_family, body)
279        }
280        Expr::Proj(_, _, inner) => scan(env, all_family, inner),
281        _ => Ok(None),
282    }
283}
284
285/// Specialize the nested container occurrences of `specs` into an auxiliary
286/// mutual family (S2). Returns `Ok(None)` when there is no nesting (the caller
287/// should use the ordinary derivation path), or when the shape is not yet
288/// supported by this stage (parametrized families or multi-member containers),
289/// in which case the ordinary path re-produces the current `UnsupportedNested`
290/// verdict — so behaviour never silently changes.
291#[allow(clippy::result_large_err)]
292pub fn specialize(
293    env: &Environment,
294    lparams: &[Name],
295    num_params: u32,
296    specs: &[InductiveSpec],
297) -> Result<Option<ExpandedFamily>, KernelError> {
298    // S2 scope: family with no shared parameters (covers `Lean.Syntax` and the
299    // textbook `NestedT`). Parametrized nested families need family-parameter
300    // re-binding in the auxiliaries — deferred; fall back to the ordinary path.
301    if num_params != 0 {
302        return Ok(None);
303    }
304
305    let orig_names: Vec<Name> = specs.iter().map(|s| s.name.clone()).collect();
306    let mut all_family = orig_names.clone();
307    let mut cur_specs = specs.to_vec();
308    let mut aux_maps: Vec<AuxMap> = Vec::new();
309    let mut aux_count = 0usize;
310    let mut fuel = 256usize;
311
312    loop {
313        fuel = fuel.checked_sub(1).ok_or_else(|| {
314            KernelError::Other("nested inductive specialization did not terminate".to_string())
315        })?;
316
317        // Find the first not-yet-specialized nested occurrence in any ctor type.
318        let mut found: Option<Found> = None;
319        'outer: for spec in &cur_specs {
320            for (_, cty) in &spec.ctors {
321                if let Some(f) = scan(env, &all_family, cty)? {
322                    found = Some(f);
323                    break 'outer;
324                }
325            }
326        }
327        let Some(f) = found else { break };
328
329        let iv = match lookup_inductive(env, &f.container) {
330            // Multi-member mutual containers are not handled by this stage.
331            Some(iv) if iv.all.len() == 1 => iv.clone(),
332            _ => return Ok(None),
333        };
334
335        let aux_name = Name::mk_str(orig_names[0].clone(), format!("_nested_{}", aux_count));
336        aux_count += 1;
337        let c_levels = f.levels.clone();
338        let as_args = f.as_args.clone();
339        let np = f.num_params;
340
341        // Auxiliary type: the container's type with its level params and its
342        // `np` parameters fixed. (No family parameters to re-bind: `num_params`
343        // is 0 in this stage.)
344        let aux_ty = {
345            let t = instantiate_type_lparams(&iv.common.ty, &iv.common.level_params, &c_levels);
346            instantiate_leading(&t, &as_args)
347        };
348
349        // Auxiliary constructors: each container ctor specialized at `As`, with
350        // the container application rewritten to the auxiliary reference.
351        let mut aux_ctors: Vec<(Name, Expr)> = Vec::with_capacity(iv.ctors.len());
352        let mut ctor_name_map: Vec<(Name, Name)> = Vec::with_capacity(iv.ctors.len());
353        for cn in &iv.ctors {
354            let (cty, clp) = lookup_ctor(env, cn).ok_or_else(|| {
355                KernelError::Other(format!(
356                    "constructor '{}' of nested container '{}' not found",
357                    cn, f.container
358                ))
359            })?;
360            let t = instantiate_type_lparams(&cty, &clp, &c_levels);
361            let t = instantiate_leading(&t, &as_args);
362            let t = replace_container_app(&t, &f.container, &c_levels, &as_args, np, &aux_name);
363            let aux_cn = Name::mk_str(aux_name.clone(), last_component(cn));
364            aux_ctors.push((aux_cn.clone(), t));
365            ctor_name_map.push((aux_cn, cn.clone()));
366        }
367
368        aux_maps.push(AuxMap {
369            aux_type: aux_name.clone(),
370            container: f.container.clone(),
371            container_levels: c_levels.clone(),
372            params: as_args.clone(),
373            aux_ctors: ctor_name_map,
374            aux_rec: Name::mk_str(aux_name.clone(), "rec".to_string()),
375            container_rec: Name::mk_str(f.container.clone(), "rec".to_string()),
376        });
377
378        all_family.push(aux_name.clone());
379        cur_specs.push(InductiveSpec::new(aux_name.clone(), aux_ty, aux_ctors));
380
381        // Rewrite `container As` → aux across every ctor type (originals + aux).
382        for spec in cur_specs.iter_mut() {
383            for (_, cty) in spec.ctors.iter_mut() {
384                *cty = replace_container_app(cty, &f.container, &c_levels, &as_args, np, &aux_name);
385            }
386        }
387    }
388
389    if all_family.len() == orig_names.len() {
390        return Ok(None);
391    }
392
393    Ok(Some(ExpandedFamily {
394        specs: cur_specs,
395        num_originals: orig_names.len(),
396        aux_maps,
397        lparams: lparams.to_vec(),
398        num_params,
399    }))
400}
401
402/// Nested-aware family derivation. When `specs` contains a nested inductive,
403/// specialize it into an auxiliary mutual family and derive that (returning the
404/// [`ExpandedFamily`] so S3 can restore the recursors); otherwise fall back to
405/// the ordinary [`check_and_derive_family`].
406///
407/// The derived recursors are still the **raw** auxiliary recursors — restoration
408/// over the real container is S3.
409#[allow(clippy::result_large_err)]
410pub fn check_and_derive_family_nested(
411    env: &Environment,
412    lparams: &[Name],
413    num_params: u32,
414    specs: &[InductiveSpec],
415) -> Result<(DerivedFamily, Option<ExpandedFamily>), KernelError> {
416    // Before the nested occurrences are specialized: the family as declared.
417    super::derive::check_specs_closed(specs)?;
418    match specialize(env, lparams, num_params, specs)? {
419        None => Ok((
420            check_and_derive_family(env, lparams, num_params, specs)?,
421            None,
422        )),
423        Some(exp) => {
424            let fam = check_and_derive_family(env, &exp.lparams, exp.num_params, &exp.specs)?;
425            Ok((fam, Some(exp)))
426        }
427    }
428}
429
430// ---------------------------------------------------------------------------
431// S3 — restoration (auxiliary family → real container).
432// ---------------------------------------------------------------------------
433
434/// Rewrite auxiliary type-formers and constructors back to the real container:
435/// `Const(aux_type)` → `container As`, `Const(aux_ctor)` → `real_ctor As`. The
436/// auxiliary recursor names are left untouched (they are retained, restored, in
437/// the environment). Because only the head `Const` is rewritten, any following
438/// arguments (indices / constructor fields) are preserved.
439fn restore_expr(e: &Expr, aux_maps: &[AuxMap]) -> Expr {
440    replace_expr(e, &mut |sub, _depth| {
441        if let Expr::Const(name, _levels) = sub {
442            for m in aux_maps {
443                if name == &m.aux_type {
444                    return Some(mk_app(
445                        Expr::Const(m.container.clone(), m.container_levels.clone()),
446                        &m.params,
447                    ));
448                }
449                for (aux_ctor, real_ctor) in &m.aux_ctors {
450                    if name == aux_ctor {
451                        return Some(mk_app(
452                            Expr::Const(real_ctor.clone(), m.container_levels.clone()),
453                            &m.params,
454                        ));
455                    }
456                }
457            }
458        }
459        None
460    })
461}
462
463/// Map an auxiliary constructor name in a recursor rule back to the real
464/// container constructor, so the reducer keys the rule by the real ctor.
465fn restore_ctor_name(ctor: &Name, aux_maps: &[AuxMap]) -> Name {
466    for m in aux_maps {
467        for (aux_ctor, real_ctor) in &m.aux_ctors {
468            if ctor == aux_ctor {
469                return real_ctor.clone();
470            }
471        }
472    }
473    ctor.clone()
474}
475
476/// Restore a raw expanded-family derivation (S3): rewrite the auxiliary
477/// type-formers / constructors back to the real container in every recursor
478/// type and rule, re-key rules by the real constructor, drop the auxiliary
479/// inductives and constructors, keep only the original types' inductives and
480/// constructors (with `num_nested` set and `all` narrowed to the originals),
481/// and RETAIN the auxiliary recursors — now stated over the real container —
482/// because reducing a user-facing recursor still emits them.
483pub fn restore(fam: DerivedFamily, exp: &ExpandedFamily) -> DerivedFamily {
484    let orig_names: Vec<Name> = exp.specs[..exp.num_originals]
485        .iter()
486        .map(|s| s.name.clone())
487        .collect();
488    let num_nested = exp.aux_maps.len() as u32;
489
490    let mut inductives = Vec::with_capacity(exp.num_originals);
491    for ci in fam.inductives.iter().take(exp.num_originals) {
492        if let ConstantInfo::Inductive(iv) = ci {
493            let mut iv = iv.clone();
494            iv.common.ty = restore_expr(&iv.common.ty, &exp.aux_maps);
495            iv.all = orig_names.clone();
496            iv.num_nested = num_nested;
497            inductives.push(ConstantInfo::Inductive(iv));
498        }
499    }
500
501    let mut constructors = Vec::new();
502    for ci in &fam.constructors {
503        if let ConstantInfo::Constructor(cv) = ci {
504            if orig_names.contains(&cv.induct) {
505                let mut cv = cv.clone();
506                cv.common.ty = restore_expr(&cv.common.ty, &exp.aux_maps);
507                constructors.push(ConstantInfo::Constructor(cv));
508            }
509        }
510    }
511
512    let mut recursors = Vec::with_capacity(fam.recursors.len());
513    for ci in &fam.recursors {
514        if let ConstantInfo::Recursor(rv) = ci {
515            let mut rv = rv.clone();
516            rv.common.ty = restore_expr(&rv.common.ty, &exp.aux_maps);
517            rv.all = orig_names.clone();
518            rv.rules = rv
519                .rules
520                .iter()
521                .map(|rule| RecursorRule {
522                    ctor: restore_ctor_name(&rule.ctor, &exp.aux_maps),
523                    nfields: rule.nfields,
524                    rhs: restore_expr(&rule.rhs, &exp.aux_maps),
525                })
526                .collect();
527            recursors.push(ConstantInfo::Recursor(rv));
528        }
529    }
530
531    DerivedFamily {
532        inductives,
533        constructors,
534        recursors,
535    }
536}
537
538// ---------------------------------------------------------------------------
539// S4 — export replay: verify exported recursors against the restored family.
540// ---------------------------------------------------------------------------
541
542/// The sorted set of constructor names a recursor's rules fire on. Uniquely
543/// identifies which (original or container) member a restored recursor belongs
544/// to, so exported and re-derived recursors can be paired without relying on a
545/// naming convention.
546fn rule_ctor_set(rv: &RecursorVal) -> Vec<Name> {
547    let mut s: Vec<Name> = rv.rules.iter().map(|r| r.ctor.clone()).collect();
548    s.sort();
549    s
550}
551
552/// Rewrite recursor self-references according to `name_map` (provisional → real
553/// exported name), in every recursor's own name and in the `Const` references
554/// inside recursor types and rule right-hand sides.
555fn rename_recursors(fam: DerivedFamily, name_map: &[(Name, Name)]) -> DerivedFamily {
556    fn rename_expr(e: &Expr, name_map: &[(Name, Name)]) -> Expr {
557        replace_expr(e, &mut |sub, _depth| {
558            if let Expr::Const(n, levels) = sub {
559                for (from, to) in name_map {
560                    if n == from {
561                        return Some(Expr::Const(to.clone(), levels.clone()));
562                    }
563                }
564            }
565            None
566        })
567    }
568    let mut recursors = Vec::with_capacity(fam.recursors.len());
569    for ci in fam.recursors {
570        if let ConstantInfo::Recursor(mut rv) = ci {
571            for (from, to) in name_map {
572                if rv.common.name == *from {
573                    rv.common.name = to.clone();
574                    break;
575                }
576            }
577            rv.common.ty = rename_expr(&rv.common.ty, name_map);
578            rv.rules = rv
579                .rules
580                .into_iter()
581                .map(|r| RecursorRule {
582                    ctor: r.ctor,
583                    nfields: r.nfields,
584                    rhs: rename_expr(&r.rhs, name_map),
585                })
586                .collect();
587            recursors.push(ConstantInfo::Recursor(rv));
588        } else {
589            recursors.push(ci);
590        }
591    }
592    DerivedFamily {
593        inductives: fam.inductives,
594        constructors: fam.constructors,
595        recursors,
596    }
597}
598
599/// Verify a nested `inductive` bundle for export replay and return the
600/// kernel-derived family to install (recursors renamed to the exported names).
601///
602/// Runs the nested-aware derivation (specialize → derive → restore), pairs each
603/// re-derived recursor with an exported one by the set of constructors its rules
604/// fire on, renames the re-derived recursors to the exported names (so
605/// self-references align), then requires every exported recursor to be
606/// definitionally equal to the corresponding re-derived one
607/// (`recursor_matches`). A missing pair or a failed def-eq is a rejection —
608/// no exported recursor is ever accepted without a sound kernel re-derivation.
609///
610/// Returns [`KernelError::UnsupportedNestedInductive`] for shapes this stage
611/// cannot un-nest (parametrized families, family in an index position,
612/// non-inductive or multi-member containers), which replay buckets as the named
613/// `nested inductives` unsupported feature — never a wrong accept.
614#[allow(clippy::result_large_err)]
615pub fn verify_nested_bundle(
616    env: &Environment,
617    lparams: &[Name],
618    num_params: u32,
619    specs: &[InductiveSpec],
620    exported_recs: &[RecursorVal],
621) -> Result<DerivedFamily, KernelError> {
622    let (raw, exp_opt) = check_and_derive_family_nested(env, lparams, num_params, specs)?;
623    let Some(exp) = exp_opt else {
624        return Err(KernelError::UnsupportedNestedInductive(
625            specs.first().map(|s| s.name.clone()).unwrap_or_default(),
626        ));
627    };
628    let restored = restore(raw, &exp);
629
630    if restored.recursors.len() != exported_recs.len() {
631        return Err(KernelError::InvalidRecursor(format!(
632            "nested inductive '{}': kernel derived {} recursors but the export lists {}",
633            specs[0].name,
634            restored.recursors.len(),
635            exported_recs.len()
636        )));
637    }
638
639    // Pair each re-derived recursor with an exported one by rule-constructor set.
640    let mut name_map: Vec<(Name, Name)> = Vec::with_capacity(restored.recursors.len());
641    for ci in &restored.recursors {
642        if let ConstantInfo::Recursor(rv) = ci {
643            let set = rule_ctor_set(rv);
644            let er = exported_recs
645                .iter()
646                .find(|er| rule_ctor_set(er) == set)
647                .ok_or_else(|| {
648                    KernelError::InvalidRecursor(format!(
649                        "nested inductive '{}': no exported recursor eliminates the same \
650                         constructors as the kernel-derived '{}'",
651                        specs[0].name, rv.common.name
652                    ))
653                })?;
654            name_map.push((rv.common.name.clone(), er.common.name.clone()));
655        }
656    }
657
658    let renamed = rename_recursors(restored, &name_map);
659
660    // Every exported recursor must def-eq the re-derived one of the same name.
661    let mut tc = TypeChecker::new(env);
662    for er in exported_recs {
663        let mut ok = false;
664        for ci in &renamed.recursors {
665            if let ConstantInfo::Recursor(d) = ci {
666                if d.common.name == er.common.name && recursor_matches(&mut tc, d, er) {
667                    ok = true;
668                    break;
669                }
670            }
671        }
672        if !ok {
673            return Err(KernelError::InvalidRecursor(format!(
674                "exported recursor '{}' does not match the kernel-derived nested recursor",
675                er.common.name
676            )));
677        }
678    }
679
680    Ok(renamed)
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686    use crate::Node;
687
688    fn c(name: &str, levels: Vec<Level>) -> Expr {
689        Expr::Const(Name::str(name), levels)
690    }
691
692    fn app(f: Expr, a: Expr) -> Expr {
693        Expr::App(Node::new(f), Node::new(a))
694    }
695
696    #[test]
697    fn detects_list_of_family() {
698        // field type `List T`, family = {T}: nested under List at position 0.
699        let fam = [Name::str("T")];
700        let ty = app(c("List", vec![Level::zero()]), c("T", vec![]));
701        let occ = detect(&fam, &ty).expect("nested occurrence");
702        assert_eq!(occ.container, Name::str("List"));
703        assert_eq!(occ.container_levels, vec![Level::zero()]);
704        assert_eq!(occ.family_positions, vec![0]);
705        assert_eq!(occ.args.len(), 1);
706    }
707
708    #[test]
709    fn direct_occurrence_is_not_nested() {
710        // `T x` — head is the family member itself: ordinary recursion.
711        let fam = [Name::str("T")];
712        let ty = app(c("T", vec![]), c("x", vec![]));
713        assert!(detect(&fam, &ty).is_none());
714    }
715
716    #[test]
717    fn foreign_container_without_family_is_not_nested() {
718        // `List Nat` — foreign container, no family type inside.
719        let fam = [Name::str("T")];
720        let ty = app(c("List", vec![Level::zero()]), c("Nat", vec![]));
721        assert!(detect(&fam, &ty).is_none());
722    }
723
724    #[test]
725    fn bare_const_is_not_nested() {
726        let fam = [Name::str("T")];
727        assert!(detect(&fam, &c("Nat", vec![])).is_none());
728    }
729
730    #[test]
731    fn family_in_later_argument_position() {
732        // `Prod Nat T` — family at position 1 only.
733        let fam = [Name::str("T")];
734        let ty = app(
735            app(
736                c("Prod", vec![Level::zero(), Level::zero()]),
737                c("Nat", vec![]),
738            ),
739            c("T", vec![]),
740        );
741        let occ = detect(&fam, &ty).expect("nested occurrence");
742        assert_eq!(occ.container, Name::str("Prod"));
743        assert_eq!(occ.family_positions, vec![1]);
744        assert_eq!(occ.args.len(), 2);
745    }
746
747    #[test]
748    fn family_nested_deeper_in_argument() {
749        // `Array (List T)` — family occurs (transitively) inside Array's arg 0.
750        let fam = [Name::str("T")];
751        let inner = app(c("List", vec![Level::zero()]), c("T", vec![]));
752        let ty = app(c("Array", vec![Level::zero()]), inner);
753        let occ = detect(&fam, &ty).expect("nested occurrence");
754        assert_eq!(occ.container, Name::str("Array"));
755        assert_eq!(occ.family_positions, vec![0]);
756    }
757
758    #[test]
759    fn multiple_family_members() {
760        // family = {T, U}; `Sum T U` nests both at positions 0 and 1.
761        let fam = [Name::str("T"), Name::str("U")];
762        let ty = app(
763            app(c("Sum", vec![Level::zero(), Level::zero()]), c("T", vec![])),
764            c("U", vec![]),
765        );
766        let occ = detect(&fam, &ty).expect("nested occurrence");
767        assert_eq!(occ.family_positions, vec![0, 1]);
768    }
769
770    // -- S2: specialization + nested derivation ----------------------------
771
772    #[test]
773    fn non_nested_family_uses_ordinary_path() {
774        // `inductive Foo | mk : Foo` has no nesting: specialize returns None.
775        let env = Environment::new();
776        let spec = InductiveSpec::new(
777            Name::str("Foo"),
778            Expr::Sort(Level::succ(Level::zero())),
779            vec![(Name::str("Foo.mk"), c("Foo", vec![]))],
780        );
781        let (fam, exp) =
782            check_and_derive_family_nested(&env, &[], 0, &[spec]).expect("ordinary derivation");
783        assert!(exp.is_none(), "non-nested family must not expand");
784        assert_eq!(fam.recursors.len(), 1);
785    }
786
787    #[test]
788    fn nested_t_derives_via_specialization() {
789        // `inductive NestedT | mk : List NestedT → NestedT`.
790        let mut env = Environment::new();
791        crate::builtin::init_builtin_env(&mut env).expect("builtin env");
792        let t = c("NestedT", vec![]);
793        let list_t = app(c("List", vec![Level::zero()]), t.clone());
794        let spec = InductiveSpec::new(
795            Name::str("NestedT"),
796            Expr::Sort(Level::succ(Level::zero())),
797            vec![(
798                Name::str("NestedT.mk"),
799                Expr::Pi(
800                    crate::BinderInfo::Default,
801                    Name::str("l"),
802                    Node::new(list_t),
803                    Node::new(t),
804                ),
805            )],
806        );
807
808        let (fam, exp) = check_and_derive_family_nested(&env, &[], 0, &[spec])
809            .expect("nested inductive now derives");
810        let exp = exp.expect("NestedT must be recognised as nested");
811
812        // Expanded to the mutual family { NestedT, aux List-of-NestedT }.
813        assert_eq!(exp.num_originals, 1);
814        assert_eq!(exp.specs.len(), 2);
815        assert_eq!(exp.aux_maps.len(), 1);
816        let m = &exp.aux_maps[0];
817        assert_eq!(m.container, Name::str("List"));
818        let real_ctors: Vec<Name> = m.aux_ctors.iter().map(|(_, r)| r.clone()).collect();
819        assert!(real_ctors.contains(&Name::str("List.nil")));
820        assert!(real_ctors.contains(&Name::str("List.cons")));
821
822        // Two inductives, three constructors (mk, nil✝, cons✝), two recursors.
823        assert_eq!(fam.inductives.len(), 2);
824        assert_eq!(fam.constructors.len(), 3);
825        assert_eq!(fam.recursors.len(), 2);
826
827        // NestedT's recursor carries a motive per family member (2) and a minor
828        // per constructor (3), and fires on `NestedT.mk`.
829        let nestedt_rec = fam
830            .recursors
831            .iter()
832            .find_map(|ci| match ci {
833                ConstantInfo::Recursor(rv) if rv.get_rule(&Name::str("NestedT.mk")).is_some() => {
834                    Some(rv)
835                }
836                _ => None,
837            })
838            .expect("a recursor eliminating NestedT.mk");
839        assert_eq!(nestedt_rec.num_motives, 2);
840        assert_eq!(nestedt_rec.num_minors, 3);
841    }
842
843    #[test]
844    fn nested_t_aux_recursor_iota_reduces() {
845        // The raw auxiliary recursor must iota-reduce like any mutual recursor:
846        // `ListNestedT.rec mN mL minMk minNil minCons ListNestedT.nil → minNil`.
847        let mut env = crate::Environment::new();
848        crate::builtin::init_builtin_env(&mut env).expect("builtin env");
849        let t = c("NestedT", vec![]);
850        let list_t = app(c("List", vec![Level::zero()]), t.clone());
851        let spec = InductiveSpec::new(
852            Name::str("NestedT"),
853            Expr::Sort(Level::succ(Level::zero())),
854            vec![(
855                Name::str("NestedT.mk"),
856                Expr::Pi(
857                    crate::BinderInfo::Default,
858                    Name::str("l"),
859                    Node::new(list_t),
860                    Node::new(t),
861                ),
862            )],
863        );
864        let (fam, exp) = check_and_derive_family_nested(&env, &[], 0, &[spec]).expect("derive");
865        let exp = exp.expect("nested");
866
867        // Install the whole expanded family so the reducer can see the aux
868        // constructor (for its cidx) and the aux recursor (for its rules).
869        let mut env2 = env.clone();
870        for ci in fam.clone().into_constant_infos() {
871            env2.add_constant(ci).expect("install expanded family");
872        }
873
874        let aux = &exp.aux_maps[0];
875        let aux_nil = aux
876            .aux_ctors
877            .iter()
878            .find(|(_, r)| *r == Name::str("List.nil"))
879            .expect("aux nil ctor")
880            .0
881            .clone();
882
883        // The recursor gets a fresh elimination level param (large elimination);
884        // any level argument works for a reduction that never typechecks.
885        let ph = |s: &str| Expr::Const(Name::str(s), vec![]);
886        let e = mk_app(
887            Expr::Const(aux.aux_rec.clone(), vec![Level::zero()]),
888            &[
889                ph("mN"),
890                ph("mL"),
891                ph("minMk"),
892                ph("minNil"),
893                ph("minCons"),
894                Expr::Const(aux_nil, vec![]),
895            ],
896        );
897
898        let reduced = crate::TypeChecker::new(&env2).whnf(&e);
899        let head = get_app_fn_args(&reduced).0.clone();
900        assert_eq!(
901            head,
902            Expr::Const(Name::str("minNil"), vec![]),
903            "aux recursor did not iota-reduce to the nil minor: {:?}",
904            reduced
905        );
906    }
907
908    #[test]
909    fn reach_through_double_nesting_expands_to_three() {
910        // `inductive Two | mk : List (List Two) → Two` reaches through one
911        // container into another (like `Lean.Syntax`'s Array-then-List), so the
912        // fixpoint must synthesize TWO auxiliaries: List-of-Two and
913        // List-of-(List-of-Two).
914        let mut env = crate::Environment::new();
915        crate::builtin::init_builtin_env(&mut env).expect("builtin env");
916        let two = c("Two", vec![]);
917        let list = |x: Expr| app(c("List", vec![Level::zero()]), x);
918        let spec = InductiveSpec::new(
919            Name::str("Two"),
920            Expr::Sort(Level::succ(Level::zero())),
921            vec![(
922                Name::str("Two.mk"),
923                Expr::Pi(
924                    crate::BinderInfo::Default,
925                    Name::str("l"),
926                    Node::new(list(list(two.clone()))),
927                    Node::new(two),
928                ),
929            )],
930        );
931        let (fam, exp) = check_and_derive_family_nested(&env, &[], 0, &[spec]).expect("derive");
932        let exp = exp.expect("nested");
933        assert_eq!(exp.num_originals, 1);
934        assert_eq!(exp.specs.len(), 3, "Two + two List auxiliaries");
935        assert_eq!(exp.aux_maps.len(), 2);
936        // Three members ⇒ three motives / four minors (mk + nil/cons ×2 = 5?).
937        // mk(1) + aux0 nil,cons(2) + aux1 nil,cons(2) = 5 minors, 3 motives.
938        let rec = fam
939            .recursors
940            .iter()
941            .find_map(|ci| match ci {
942                ConstantInfo::Recursor(rv) if rv.get_rule(&Name::str("Two.mk")).is_some() => {
943                    Some(rv)
944                }
945                _ => None,
946            })
947            .expect("Two's recursor");
948        assert_eq!(rec.num_motives, 3);
949        assert_eq!(rec.num_minors, 5);
950    }
951
952    // -- S3: restoration over the real container ---------------------------
953
954    #[test]
955    fn nested_t_restore_produces_real_list_recursor() {
956        use crate::expr_util::collect_consts;
957        let mut env = crate::Environment::new();
958        crate::builtin::init_builtin_env(&mut env).expect("builtin env");
959        let t = c("NestedT", vec![]);
960        let list_t = app(c("List", vec![Level::zero()]), t.clone());
961        let spec = InductiveSpec::new(
962            Name::str("NestedT"),
963            Expr::Sort(Level::succ(Level::zero())),
964            vec![(
965                Name::str("NestedT.mk"),
966                Expr::Pi(
967                    crate::BinderInfo::Default,
968                    Name::str("l"),
969                    Node::new(list_t),
970                    Node::new(t.clone()),
971                ),
972            )],
973        );
974        let (fam, exp) = check_and_derive_family_nested(&env, &[], 0, &[spec]).expect("derive");
975        let exp = exp.expect("nested");
976        let aux_type = exp.aux_maps[0].aux_type.clone();
977        let aux_rec = exp.aux_maps[0].aux_rec.clone();
978        let restored = restore(fam, &exp);
979
980        // Only the user-facing type survives: 1 inductive, 1 ctor, 2 recursors.
981        assert_eq!(restored.inductives.len(), 1);
982        assert_eq!(restored.constructors.len(), 1);
983        assert_eq!(restored.recursors.len(), 2);
984
985        match &restored.inductives[0] {
986            ConstantInfo::Inductive(iv) => {
987                assert_eq!(iv.num_nested, 1);
988                assert_eq!(iv.all, vec![Name::str("NestedT")]);
989            }
990            _ => panic!("expected inductive"),
991        }
992
993        // NestedT.mk now mentions the real `List`, not the auxiliary type.
994        match &restored.constructors[0] {
995            ConstantInfo::Constructor(cv) => {
996                let consts = collect_consts(&cv.common.ty);
997                assert!(consts.contains(&Name::str("List")));
998                assert!(!consts.contains(&aux_type));
999            }
1000            _ => panic!("expected constructor"),
1001        }
1002
1003        // Install the restored family (real `List` already present); every
1004        // restored recursor type must re-typecheck.
1005        let mut env2 = env.clone();
1006        for ci in restored.clone().into_constant_infos() {
1007            env2.add_constant(ci).expect("install restored family");
1008        }
1009        for ci in &restored.recursors {
1010            if let ConstantInfo::Recursor(rv) = ci {
1011                crate::TypeChecker::new(&env2)
1012                    .infer_type(&rv.common.ty)
1013                    .expect("restored recursor type re-checks");
1014            }
1015        }
1016
1017        let ph = |s: &str| Expr::Const(Name::str(s), vec![]);
1018        let nil_app = mk_app(
1019            Expr::Const(Name::str("List.nil"), vec![Level::zero()]),
1020            std::slice::from_ref(&t),
1021        );
1022
1023        // The retained auxiliary recursor iota-reduces on a REAL `List.nil`.
1024        let e_aux = mk_app(
1025            Expr::Const(aux_rec, vec![Level::zero()]),
1026            &[
1027                ph("mN"),
1028                ph("mL"),
1029                ph("minMk"),
1030                ph("minNil"),
1031                ph("minCons"),
1032                nil_app.clone(),
1033            ],
1034        );
1035        let r_aux = crate::TypeChecker::new(&env2).whnf(&e_aux);
1036        assert_eq!(
1037            get_app_fn_args(&r_aux).0.clone(),
1038            Expr::Const(Name::str("minNil"), vec![]),
1039            "restored aux recursor did not fire on real List.nil: {:?}",
1040            r_aux
1041        );
1042
1043        // The user-facing recursor fires on `NestedT.mk (@List.nil NestedT)`.
1044        let main_rec = restored
1045            .recursors
1046            .iter()
1047            .find_map(|ci| match ci {
1048                ConstantInfo::Recursor(rv) if rv.get_rule(&Name::str("NestedT.mk")).is_some() => {
1049                    Some(rv.common.name.clone())
1050                }
1051                _ => None,
1052            })
1053            .expect("NestedT.rec");
1054        let major = app(c("NestedT.mk", vec![]), nil_app);
1055        let e_main = mk_app(
1056            Expr::Const(main_rec, vec![Level::zero()]),
1057            &[
1058                ph("mN"),
1059                ph("mL"),
1060                ph("minMk"),
1061                ph("minNil"),
1062                ph("minCons"),
1063                major,
1064            ],
1065        );
1066        let r_main = crate::TypeChecker::new(&env2).whnf(&e_main);
1067        assert_eq!(
1068            get_app_fn_args(&r_main).0.clone(),
1069            Expr::Const(Name::str("minMk"), vec![]),
1070            "restored NestedT.rec did not fire on NestedT.mk: {:?}",
1071            r_main
1072        );
1073    }
1074
1075    // -- S4: verify exported recursors against the restored family ----------
1076
1077    /// Build NestedT env + spec, and the exported recursors as Lean would name
1078    /// them (`NestedT.rec` over `NestedT.mk`, `NestedT.rec_1` over List ctors).
1079    fn nested_t_bundle() -> (crate::Environment, InductiveSpec, Vec<RecursorVal>) {
1080        let mut env = crate::Environment::new();
1081        crate::builtin::init_builtin_env(&mut env).expect("builtin env");
1082        let t = c("NestedT", vec![]);
1083        let list_t = app(c("List", vec![Level::zero()]), t.clone());
1084        let spec = InductiveSpec::new(
1085            Name::str("NestedT"),
1086            Expr::Sort(Level::succ(Level::zero())),
1087            vec![(
1088                Name::str("NestedT.mk"),
1089                Expr::Pi(
1090                    crate::BinderInfo::Default,
1091                    Name::str("l"),
1092                    Node::new(list_t),
1093                    Node::new(t),
1094                ),
1095            )],
1096        );
1097        let (raw, exp) = check_and_derive_family_nested(&env, &[], 0, std::slice::from_ref(&spec))
1098            .expect("derive");
1099        let exp = exp.expect("nested");
1100        let restored = restore(raw, &exp);
1101        // Canonical export names, keyed by which constructors each rec fires on.
1102        let name_map: Vec<(Name, Name)> = restored
1103            .recursors
1104            .iter()
1105            .filter_map(|ci| match ci {
1106                ConstantInfo::Recursor(rv) => {
1107                    let exported = if rule_ctor_set(rv).contains(&Name::str("NestedT.mk")) {
1108                        Name::mk_str(Name::str("NestedT"), "rec".to_string())
1109                    } else {
1110                        Name::mk_str(Name::str("NestedT"), "rec_1".to_string())
1111                    };
1112                    Some((rv.common.name.clone(), exported))
1113                }
1114                _ => None,
1115            })
1116            .collect();
1117        let canonical = rename_recursors(restored, &name_map);
1118        let exported_recs: Vec<RecursorVal> = canonical
1119            .recursors
1120            .iter()
1121            .filter_map(|ci| match ci {
1122                ConstantInfo::Recursor(rv) => Some(rv.clone()),
1123                _ => None,
1124            })
1125            .collect();
1126        (env, spec, exported_recs)
1127    }
1128
1129    #[test]
1130    fn verify_nested_bundle_accepts_matching_export() {
1131        let (env, spec, exported_recs) = nested_t_bundle();
1132        let out = verify_nested_bundle(&env, &[], 0, &[spec], &exported_recs)
1133            .expect("matching export verifies");
1134        // Recursors renamed to the exported names.
1135        let names: Vec<Name> = out
1136            .recursors
1137            .iter()
1138            .filter_map(|ci| match ci {
1139                ConstantInfo::Recursor(rv) => Some(rv.common.name.clone()),
1140                _ => None,
1141            })
1142            .collect();
1143        assert!(names.contains(&Name::mk_str(Name::str("NestedT"), "rec".to_string())));
1144        assert!(names.contains(&Name::mk_str(Name::str("NestedT"), "rec_1".to_string())));
1145    }
1146
1147    #[test]
1148    fn verify_nested_bundle_rejects_corrupted_recursor() {
1149        let (env, spec, mut exported_recs) = nested_t_bundle();
1150        // Corrupt one exported recursor's metadata: the def-eq match must fail.
1151        exported_recs[0].num_minors += 1;
1152        let err = verify_nested_bundle(&env, &[], 0, &[spec], &exported_recs)
1153            .expect_err("corrupted export must be rejected");
1154        assert!(matches!(err, KernelError::InvalidRecursor(_)));
1155    }
1156
1157    #[test]
1158    fn verify_nested_bundle_rejects_wrong_recursor_count() {
1159        let (env, spec, mut exported_recs) = nested_t_bundle();
1160        exported_recs.pop();
1161        let err = verify_nested_bundle(&env, &[], 0, &[spec], &exported_recs)
1162            .expect_err("wrong recursor count must be rejected");
1163        assert!(matches!(err, KernelError::InvalidRecursor(_)));
1164    }
1165}