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