Skip to main content

logicaffeine_kernel/interface/lean/
mod.rs

1//! Lean 4 kernel export (NDJSON 3.1) as an input surface of the LOGOS kernel.
2//!
3//! This is one more reader beside `term_parser`, `command_parser` and `literate_parser`: it builds
4//! LOGOS kernel terms from Lean's exported declarations and hands every declaration to the kernel,
5//! which re-decides it. The reader is untrusted; its verdicts are the kernel's. Unsupported
6//! declaration families are declined, never counted as invalid proofs. User names are encoded
7//! structurally. Nat and quotient identities are reserved and admitted only through their checked
8//! canonical declarations.
9use crate::{
10    defeq_for_test as definitionally_equal, infer_type, normalize_checked, Context, KernelError,
11    Term, Universe,
12};
13use serde_json::Value;
14mod diagnostic;
15pub use diagnostic::{DiagnosticCode, Outcome, Reason};
16pub mod emit;
17pub mod intake;
18pub mod native_decide;
19pub mod package;
20pub mod pipeline;
21pub mod receipt;
22pub mod reference;
23pub mod taint;
24pub use pipeline::run_cli;
25mod beta;
26mod dag;
27mod metadata;
28// Experimental trace controls do not alter importer production behavior.
29#[cfg(test)]
30#[path = "../../../../logicaffeine_tests/unit/logicaffeine_kernel/interface/lean/lazy_conversion.rs"]
31mod lazy_conversion;
32mod strings;
33use std::collections::{HashMap, HashSet};
34
35/// Charge decimal input and an upper bound on the existing BigInt parser's
36/// limb work, rather than placing a machine-word bound on the represented Nat.
37/// Each decimal digit needs fewer than four bits; multiplication by ten and
38/// addition each visit at most (digits / 16 + 1) 64-bit limbs.
39fn parse_natural_literal(digits: &str, budget: &mut usize) -> Result<logicaffeine_base::BigInt> {
40    *budget = budget.checked_sub(digits.len()).ok_or_else(|| {
41        declined(
42            DiagnosticCode::ResourceLimit,
43            "natural literal parsing budget",
44        )
45    })?;
46    if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
47        return Err(declined(
48            DiagnosticCode::MalformedExport,
49            "malformed natural literal",
50        ));
51    }
52    let significant = digits.trim_start_matches('0');
53    if significant.is_empty() {
54        return Ok(logicaffeine_base::BigInt::zero());
55    }
56    let n = significant.len();
57    let work = n.saturating_mul(n / 16 + 1).saturating_mul(2);
58    *budget = budget.checked_sub(work).ok_or_else(|| {
59        declined(
60            DiagnosticCode::ResourceLimit,
61            "natural literal parsing budget",
62        )
63    })?;
64    logicaffeine_base::BigInt::parse_decimal(significant)
65        .ok_or_else(|| declined(DiagnosticCode::MalformedExport, "malformed natural literal"))
66}
67
68/// The kernel's decision on an input it did not accept. `Reject`: the input is wrong. `Decline`: the
69/// input may be right, but this reader does not accept it (and never approximates it).
70#[derive(Debug)]
71pub enum Verdict {
72    Reject(Reason),
73    Decline(Reason),
74}
75impl Verdict {
76    pub fn reason(&self) -> &Reason {
77        match self {
78            Self::Reject(reason) | Self::Decline(reason) => reason,
79        }
80    }
81    /// The benchmark protocol's exit code: 1 for a rejection, 2 for a decline.
82    pub fn code(&self) -> i32 {
83        match self {
84            Self::Reject(_) => 1,
85            Self::Decline(_) => 2,
86        }
87    }
88    /// The `logos-lean-result/1` outcome this refusal decides.
89    pub fn outcome(&self) -> Outcome {
90        Outcome::of_refusal(self.reason().code(), matches!(self, Self::Reject(_)))
91    }
92    /// The same verdict, recorded as arising at 1-based input `line` (the innermost line wins).
93    pub fn at_line(self, line: usize) -> Verdict {
94        self.map_reason(|reason| reason.at_line(line))
95    }
96    /// The same verdict, recorded as arising in the Lean declaration `name` (the innermost wins).
97    pub fn in_declaration(self, name: impl Into<String>) -> Verdict {
98        let name = name.into();
99        self.map_reason(|reason| reason.in_declaration(name))
100    }
101    fn map_reason(self, f: impl FnOnce(Reason) -> Reason) -> Verdict {
102        match self {
103            Verdict::Reject(reason) => Verdict::Reject(f(reason)),
104            Verdict::Decline(reason) => Verdict::Decline(f(reason)),
105        }
106    }
107}
108fn emit_stage(
109    sink: Option<StageSink<'_>>,
110    declaration: Option<&(usize, String, String, Option<u64>)>,
111    stage: &str,
112    event: &str,
113) {
114    if let (Some(sink), Some((line, name, kind, name_index))) = (sink, declaration) {
115        sink(&serde_json::json!({
116            "schema":"logos-lean-stages/1","line":line,"name":name,
117            "kind":kind,"name_index":name_index,"stage":stage,"event":event,
118        }));
119    }
120}
121type Result<T> = std::result::Result<T, Verdict>;
122impl From<KernelError> for Verdict {
123    fn from(error: KernelError) -> Self {
124        kernel(error)
125    }
126}
127
128fn declined(code: DiagnosticCode, s: impl Into<String>) -> Verdict {
129    Verdict::Decline(Reason::new(code, s))
130}
131fn rejected(code: DiagnosticCode, s: impl Into<String>) -> Verdict {
132    Verdict::Reject(Reason::new(code, s))
133}
134impl Verdict {
135    /// The same verdict and code, its message prefixed with where it arose.
136    fn located(self, prefix: impl std::fmt::Display) -> Verdict {
137        self.map_reason(|reason| reason.located(prefix))
138    }
139}
140fn index(v: &Value) -> Result<usize> {
141    v.as_u64()
142        .and_then(|n| usize::try_from(n).ok())
143        .ok_or_else(|| declined(DiagnosticCode::MalformedExport, "malformed index"))
144}
145fn array(v: &Value) -> Result<&Vec<Value>> {
146    v.as_array()
147        .ok_or_else(|| declined(DiagnosticCode::MalformedExport, "expected array"))
148}
149/// A verdict the independent kernel did not give, as the reader reports it — always at the
150/// `independent_recheck` stage. A resource bound that stopped it is a resource limit; a refutation,
151/// or a construct the independent kernel does not cover, is a refused recheck.
152fn independent_verdict(error: &crate::ReCheckError, what: impl std::fmt::Display) -> Verdict {
153    let code = match error.incomplete() {
154        Some(incomplete) if incomplete.is_resource_limit() => DiagnosticCode::ResourceLimit,
155        _ => DiagnosticCode::RecheckRefused,
156    };
157    Verdict::Decline(
158        Reason::new(code, format!("{what}: {error:?}")).at_stage("independent_recheck"),
159    )
160}
161/// The two kernels' types for one declaration, compared by Lean's exact conversion. A comparison
162/// the conversion could not finish is kept as the resource limit it is; any other failure is a
163/// refused recheck.
164fn independent_comparison(compared: Result<()>, what: impl std::fmt::Display) -> Result<()> {
165    compared.map_err(|verdict| match verdict {
166        Verdict::Decline(reason) if reason.code() == DiagnosticCode::ResourceLimit => {
167            Verdict::Decline(reason.located(what).at_stage("independent_recheck"))
168        }
169        _ => declined(DiagnosticCode::RecheckRefused, what.to_string()),
170    })
171}
172fn kernel(e: KernelError) -> Verdict {
173    match e {
174        KernelError::IndependentRecheck { subject, error } => {
175            independent_verdict(&error, format!("Certification error: {subject}"))
176        }
177        KernelError::UnboundVariable(_)
178        | KernelError::NotAFunction(_)
179        | KernelError::NotAType(_)
180        | KernelError::TypeMismatch { .. }
181        | KernelError::PositivityViolation { .. }
182        | KernelError::InvalidDeclaration(_) => {
183            rejected(DiagnosticCode::InvalidProof, e.to_string())
184        }
185        _ => declined(DiagnosticCode::UnsupportedFeature, e.to_string()),
186    }
187}
188/// An inductive the kernel cannot decide — a nested-reconstruction boundary, an inconclusive
189/// positivity check — is an unsupported feature, declined and never approximated by an axiom;
190/// a negative occurrence and every other kernel refusal keep their classification.
191fn inductive_verdict(error: KernelError) -> Verdict {
192    match error {
193        KernelError::CertificationError(message) => {
194            declined(DiagnosticCode::UnsupportedFeature, message)
195        }
196        other => kernel(other),
197    }
198}
199/// An unsafe inductive is valid Lean outside the safe kernel fragment; a missing or
200/// non-Boolean flag is a malformed record.
201fn inductive_safety(flag: &Value, message: &str) -> Verdict {
202    match flag {
203        Value::Bool(true) => declined(DiagnosticCode::UnsupportedFeature, message),
204        _ => declined(DiagnosticCode::MalformedExport, message),
205    }
206}
207fn normal(ctx: &Context, t: &Term) -> Result<Term> {
208    normalize_checked(ctx, t).map_err(|e| {
209        let code = match e.1 {
210            crate::Unreduced::Cyclic => DiagnosticCode::UnsupportedFeature,
211            crate::Unreduced::BudgetSpent | crate::Unreduced::TooDeep => {
212                DiagnosticCode::ResourceLimit
213            }
214        };
215        declined(code, format!("normalization unfinished: {e:?}"))
216    })
217}
218fn sort(ctx: &Context, t: &Term) -> Result<Universe> {
219    match normal(ctx, &infer_type(ctx, t).map_err(kernel)?)? {
220        Term::Sort(u) => Ok(u),
221        _ => Err(rejected(
222            DiagnosticCode::InvalidProof,
223            "declaration type is not a type",
224        )),
225    }
226}
227
228fn exact(ctx: &Context, a: &Term, b: &Term) -> Result<()> {
229    // A conversion that answers "not equal" after one of its own reductions stopped for want of a
230    // resource compared a partial term: that is an undecided comparison — a resource limit, never
231    // evidence that valid Lean is an invalid proof. The conversion reports the stop itself, from
232    // inside (`observe_resource_stops`). It is never reconstructed afterwards by normalizing both
233    // whole sides again: that costs more than the conversion it audits, and it cannot see what the
234    // conversion compared — Arena `refute-cheap-first` is refuted at `false` against `true`
235    // without `count #200` ever being evaluated, and reported undecided by a whole-side audit.
236    let (equal, stopped) =
237        crate::reduction::observe_resource_stops(|| definitionally_equal(ctx, a, b));
238    if equal {
239        return Ok(());
240    }
241    Err(match stopped {
242        Some(reason) => declined(
243            DiagnosticCode::ResourceLimit,
244            format!("Lean conversion undecided: reduction stopped ({reason:?})"),
245        ),
246        None => rejected(
247            DiagnosticCode::InvalidProof,
248            "Lean conversion mismatch (cumulative subtyping is insufficient)",
249        ),
250    })
251}
252/// How many applied functions' types the reader normalized in full to find their `Π`.
253#[cfg(test)]
254static FUNCTION_TYPE_NORMALIZATIONS: std::sync::atomic::AtomicUsize =
255    std::sync::atomic::AtomicUsize::new(0);
256
257// Keep the host kernel's reduction, substitution and typing. This traversal
258// strengthens its cumulative application/let checks to Lean's exact conversion.
259pub fn lean_type(ctx: &Context, t: &Term) -> Result<Term> {
260    use crate::term_store::{deepen_stack, within_kernel_depth, KernelCall, TermStore, TypeCache};
261    if let Some(result) = crate::annotation::erased(t, |t| lean_type(ctx, t)) {
262        return result;
263    }
264    if !within_kernel_depth(t) {
265        return Err(declined(
266            DiagnosticCode::ResourceLimit,
267            "Lean typing input exceeds the kernel depth limit",
268        ));
269    }
270    // Size the stack before adapter recursion retains telescope frames. Caches
271    // belong to the worker call and never cross the scoped-thread boundary.
272    if let Some(result) = deepen_stack(t, || lean_type(ctx, t)) {
273        return result;
274    }
275    let call = KernelCall::begin();
276    let mut store = TermStore::new();
277    let mut cache = TypeCache::new(&call);
278    let mut equal = HashMap::new();
279    lean_type_cached(ctx, t, &mut store, &mut cache, &mut equal, &call, &[])
280}
281
282// The root context is immutable for this call. The environment key records
283// every extension made below it, including shadowing, rather than only the
284// expression's directly mentioned variables (types can depend on other locals).
285fn lean_type_cached<'call>(
286    ctx: &Context,
287    t: &Term,
288    store: &mut crate::term_store::TermStore,
289    cache: &mut crate::term_store::TypeCache<'call, Term>,
290    equal: &mut HashMap<crate::term_store::EnvId, crate::term_store::ScopedDefEqCache<'call>>,
291    call: &'call crate::term_store::KernelCall,
292    locals: &[(logicaffeine_base::Symbol, crate::term_store::TermId)],
293) -> Result<Term> {
294    let id = store.intern(t);
295    let env = store.env(locals);
296    if let Some(ty) = cache.get(env, id) {
297        return Ok(ty.clone());
298    }
299    let result = (|| {
300        match t {
301            Term::App(f, a) => {
302                let ft = lean_type_cached(ctx, f, store, cache, equal, call, locals)?;
303                // Only the head is demanded (Lean's `ensure_pi (whnf (infer f))`, and this
304                // kernel's own application rule). Normalizing the whole type would unfold every
305                // proof inside it — a `Fin` numeral carries its `Nat.mod_lt` proof, and theorems are
306                // transparent — for a `Π` that head reduction already exposes.
307                let ft = match crate::type_checker::exposed_function_type(ctx, ft) {
308                    Ok(exposed) => exposed,
309                    Err(ft) => {
310                        #[cfg(test)]
311                        FUNCTION_TYPE_NORMALIZATIONS
312                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
313                        normal(ctx, &ft)?
314                    }
315                };
316                let at = lean_type_cached(ctx, a, store, cache, equal, call, locals)?;
317                if let Term::Pi {
318                    param,
319                    param_type,
320                    body_type,
321                } = ft
322                {
323                    lean_exact_cached(ctx, &at, &param_type, store, equal, call, env)?;
324                    return Ok(crate::instantiate_codomain(
325                        &param,
326                        &param_type,
327                        &body_type,
328                        a,
329                    ));
330                }
331                return Err(rejected(
332                    DiagnosticCode::InvalidProof,
333                    "application head is not a function",
334                ));
335            }
336            Term::Pi {
337                param,
338                param_type,
339                body_type,
340            }
341            | Term::Lambda {
342                param,
343                param_type,
344                body: body_type,
345            } => {
346                let domain_sort =
347                    lean_type_cached(ctx, param_type, store, cache, equal, call, locals)?;
348                if !matches!(normal(ctx, &domain_sort)?, Term::Sort(_)) {
349                    return Err(rejected(
350                        DiagnosticCode::InvalidProof,
351                        "binder domain is not a type",
352                    ));
353                }
354                let name = store.symbol(param);
355                let domain = store.intern(param_type);
356                let mut inner = locals.to_vec();
357                inner.retain(|(n, _)| *n != name);
358                inner.push((name, domain));
359                let body = lean_type_cached(
360                    &ctx.extend(param, (**param_type).clone()),
361                    body_type,
362                    store,
363                    cache,
364                    equal,
365                    call,
366                    &inner,
367                )?;
368                if matches!(t, Term::Lambda { .. }) {
369                    return Ok(Term::Pi {
370                        param: param.clone(),
371                        param_type: param_type.clone(),
372                        body_type: Box::new(body),
373                    });
374                }
375            }
376            Term::Let {
377                name,
378                ty,
379                value,
380                body,
381            } => {
382                lean_type_cached(ctx, ty, store, cache, equal, call, locals)?;
383                let actual = lean_type_cached(ctx, value, store, cache, equal, call, locals)?;
384                lean_exact_cached(ctx, &actual, ty, store, equal, call, env)?;
385                if ctx.get(name).is_none() {
386                    std::thread_local! { static LET_PROBE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) }; }
387                    struct Probe(bool);
388                    impl Drop for Probe {
389                        fn drop(&mut self) {
390                            LET_PROBE.with(|flag| flag.set(self.0));
391                        }
392                    }
393                    let probe = Probe(LET_PROBE.with(|flag| flag.replace(true)));
394                    let nested = probe.0;
395                    let symbol = store.symbol(name);
396                    let domain = store.intern(ty);
397                    let mut inner = locals.to_vec();
398                    inner.push((symbol, domain));
399                    let result = lean_type_cached(
400                        &ctx.extend(name, (**ty).clone()),
401                        body,
402                        store,
403                        cache,
404                        equal,
405                        call,
406                        &inner,
407                    );
408                    drop(probe);
409                    match result {
410                        Ok(body_type) => {
411                            return Ok(crate::instantiate_codomain(name, ty, &body_type, value))
412                        }
413                        Err(error) if nested => return Err(error),
414                        Err(_) => {}
415                    }
416                }
417                return lean_type_cached(
418                    ctx,
419                    &crate::substitute(body, name, value),
420                    store,
421                    cache,
422                    equal,
423                    call,
424                    locals,
425                );
426            }
427            _ => {}
428        }
429        infer_type(ctx, t).map_err(kernel)
430    })()?;
431    cache.insert(env, id, result.clone());
432    Ok(result)
433}
434
435// Each environment owns a separate pair cache under the same immutable root.
436// This records checked pairs only; it never infers transitive equivalence.
437fn lean_exact_cached<'call>(
438    ctx: &Context,
439    a: &Term,
440    b: &Term,
441    store: &mut crate::term_store::TermStore,
442    equal: &mut HashMap<crate::term_store::EnvId, crate::term_store::ScopedDefEqCache<'call>>,
443    call: &'call crate::term_store::KernelCall,
444    env: crate::term_store::EnvId,
445) -> Result<()> {
446    use crate::term_store::DefEqCache;
447    let left = store.intern(a);
448    let right = store.intern(b);
449    if let Some(answer) = equal
450        .entry(env)
451        .or_insert_with(|| DefEqCache::scoped(call))
452        .get(left, right)
453    {
454        return if answer {
455            Ok(())
456        } else {
457            Err(rejected(
458                DiagnosticCode::InvalidProof,
459                "Lean conversion mismatch (cumulative subtyping is insufficient)",
460            ))
461        };
462    }
463    let answer = exact(ctx, a, b);
464    // Only a DECIDED comparison is evidence. An undecided one is asked again next time, never
465    // replayed from the cache as a rejection.
466    let decided = match &answer {
467        Ok(()) => Some(true),
468        Err(Verdict::Reject(_)) => Some(false),
469        Err(Verdict::Decline(_)) => None,
470    };
471    if let Some(decided) = decided {
472        equal.get_mut(&env).unwrap().insert(left, right, decided);
473    }
474    answer
475}
476
477fn apply(f: Term, a: Term) -> Term {
478    Term::App(Box::new(f), Box::new(a))
479}
480fn telescope_len(t: &Term) -> usize {
481    let mut n = 0;
482    let mut cur = t;
483    while let Term::Pi { body_type, .. } = cur {
484        n += 1;
485        cur = body_type;
486    }
487    n
488}
489// Count binders without evaluating their domains or the arguments of an
490// already-known inductive result. Normalize only a hidden telescope head.
491fn resolved_telescope_len(ctx: &Context, term: &Term) -> Result<usize> {
492    let mut ctx = ctx.clone();
493    let mut term = term.clone();
494    let mut count = 0;
495    loop {
496        if let Term::Pi {
497            param,
498            param_type,
499            body_type,
500        } = term
501        {
502            ctx = ctx.extend(&param, *param_type);
503            term = *body_type;
504            count += 1;
505            continue;
506        }
507        let mut head = &term;
508        while let Term::App(f, _) = head {
509            head = f;
510        }
511        if matches!(head, Term::Global(n) | Term::Const {name:n, ..} if ctx.is_inductive(n))
512            || matches!(head, Term::Sort(_))
513        {
514            return Ok(count);
515        }
516        term = normal(&ctx, &term)?;
517        if !matches!(term, Term::Pi { .. }) {
518            return Ok(count);
519        }
520    }
521}
522/// An inductive's arity as Lean declares it, with its telescope exposed (the kernel's
523/// `exposed_arity`): binder domains keep `outParam` and every other written form.
524fn declared_arity(ctx: &Context, arity: &Term) -> Result<Term> {
525    crate::context::exposed_arity(ctx, arity, &mut |ctx, t| normal(ctx, t))
526}
527fn open_binder(t: Term, name: &str) -> Result<(Term, Term)> {
528    if let Term::Pi {
529        param,
530        param_type,
531        body_type,
532    } = t
533    {
534        Ok((
535            *param_type,
536            crate::substitute(&body_type, &param, &Term::Var(name.into())),
537        ))
538    } else {
539        Err(rejected(
540            DiagnosticCode::InvalidProof,
541            "short declaration telescope",
542        ))
543    }
544}
545fn ctor_rule(
546    ctx: &Context,
547    rec: &str,
548    levels: &[String],
549    rec_ty: &Term,
550    ctor: &str,
551    ctor_ty: &Term,
552    nparams: usize,
553    prefix: usize,
554) -> Result<Term> {
555    let ctor_value = match ctx.poly_declaration(ctor) {
556        Some((parameters, _)) => Term::Const {
557            name: ctor.into(),
558            levels: parameters.iter().cloned().map(Universe::Var).collect(),
559        },
560        None => Term::Global(ctor.into()),
561    };
562    ctor_rule_applied(
563        ctx,
564        rec,
565        levels,
566        rec_ty,
567        ctor_value,
568        ctor_ty.clone(),
569        nparams,
570        prefix,
571        nparams,
572    )
573}
574
575fn ctor_rule_applied(
576    ctx: &Context,
577    rec: &str,
578    levels: &[String],
579    rec_ty: &Term,
580    mut ctor_value: Term,
581    mut cty: Term,
582    nparams: usize,
583    prefix: usize,
584    result_parameters: usize,
585) -> Result<Term> {
586    let mut binders = Vec::new();
587    let mut ty = rec_ty.clone();
588    for i in 0..prefix {
589        let name = format!("arena.rule.{i}");
590        let (domain, body) = open_binder(ty, &name)?;
591        binders.push((name, domain));
592        ty = body;
593    }
594    for (name, _) in binders.iter().take(nparams) {
595        let variable = Term::Var(name.clone());
596        if let Term::Pi {
597            param, body_type, ..
598        } = cty
599        {
600            cty = crate::substitute(&body_type, &param, &variable);
601        } else {
602            return Err(rejected(
603                DiagnosticCode::InvalidProof,
604                "constructor parameter telescope is too short",
605            ));
606        }
607        ctor_value = apply(ctor_value, variable);
608    }
609    while matches!(cty, Term::Pi { .. }) {
610        let name = format!("arena.rule.{}", binders.len());
611        let (domain, body) = open_binder(cty, &name)?;
612        ctor_value = apply(ctor_value, Term::Var(name.clone()));
613        binders.push((name, domain));
614        cty = body;
615    }
616    let mut result_args = Vec::new();
617    let mut head = &cty;
618    while let Term::App(f, a) = head {
619        result_args.push((**a).clone());
620        head = f;
621    }
622    result_args.reverse();
623    let mut rhs = if levels.is_empty() {
624        Term::Global(rec.into())
625    } else {
626        Term::Const {
627            name: rec.into(),
628            levels: levels.iter().cloned().map(Universe::Var).collect(),
629        }
630    };
631    for (name, _) in binders.iter().take(prefix) {
632        rhs = apply(rhs, Term::Var(name.clone()));
633    }
634    for index in result_args.into_iter().skip(result_parameters) {
635        rhs = apply(rhs, index);
636    }
637    rhs = apply(rhs, ctor_value);
638    for (name, domain) in binders.into_iter().rev() {
639        rhs = Term::Lambda {
640            param: name,
641            param_type: Box::new(domain),
642            body: Box::new(rhs),
643        };
644    }
645    // The rule comes from applying the checked recursor to its own constructor.
646    infer_type(ctx, &rhs).map_err(|e| {
647        declined(
648            DiagnosticCode::UnsupportedFeature,
649            format!("derived rule failed to typecheck: {e}"),
650        )
651    })?;
652    Ok(rhs)
653}
654
655// Projection lowering shares the kernel's checked match and iota rules. The
656// source projection's additional Prop restriction is checked before lowering.
657//
658// Lean's kernel (`infer_proj`) accepts a projection out of any fully applied
659// one-constructor family, indexed or not: it instantiates the constructor's
660// PARAMETERS from the value's type and walks the fields; index arguments are never
661// substituted into a field type. The lowering is the same checked `match`, whose
662// motive abstracts the family's indices when it has any. Indices are kept, never
663// erased: the motive ranges over every index, and the result is that motive at the
664// value's own indices.
665/// The reduction steps a projected value's type may take to reach its normal form before the
666/// projection reads the type's weak-head form instead.
667const PROJECTION_NORMAL_FORM_STEPS: usize = 1 << 12;
668fn projection(
669    ctx: &Context,
670    name: &str,
671    index: usize,
672    disc: Term,
673    budget: &mut usize,
674) -> Result<Term> {
675    if *budget == 0 {
676        return Err(declined(
677            DiagnosticCode::ResourceLimit,
678            "projection expansion budget",
679        ));
680    }
681    *budget -= 1;
682    let prefix = format!("arena.projection.{}", *budget);
683    // The family is read from the normal form of the value's type — whenever that normal form is
684    // reached within a small allowance, so every ordinary projection lowers exactly as before.
685    // A type whose normal form does not arrive in time (`Nat.below motive (n + 4)` inside
686    // `Nat.log2_terminates` unfolds well-founded division forever) is read as Lean's `infer_proj`
687    // reads every type, `whnf (infer_type e)`: the head is exposed without unfolding the rest,
688    // and its β-redexes are contracted. Only a head the bounded probe cannot reach keeps the full
689    // normalization, exactly as before.
690    let value_type = lean_type(ctx, &disc)?;
691    let ty = match crate::reduction::normalize_checked_within(
692        ctx,
693        &value_type,
694        PROJECTION_NORMAL_FORM_STEPS,
695    ) {
696        Ok(normal_form) => normal_form,
697        Err((_, crate::Unreduced::BudgetSpent)) => {
698            match crate::type_checker::exposed_inductive_type(ctx, value_type) {
699                Ok(exposed) => match crate::reduction::beta_normal_form(&exposed, &mut (1 << 12)) {
700                    Some(contracted) => contracted,
701                    None => normal(ctx, &exposed)?,
702                },
703                Err(value_type) => normal(ctx, &value_type)?,
704            }
705        }
706        Err(_) => normal(ctx, &value_type)?,
707    };
708    let mut head = &ty;
709    let mut args = Vec::new();
710    while let Term::App(f, a) = head {
711        args.push((**a).clone());
712        head = f;
713    }
714    args.reverse();
715    let (parent, levels) = match head {
716        Term::Global(n) => (n.as_str(), Vec::new()),
717        Term::Const { name, levels } => (name.as_str(), levels.clone()),
718        _ => {
719            return Err(rejected(
720                DiagnosticCode::InvalidProof,
721                "projection of a non-inductive value",
722            ))
723        }
724    };
725    if parent != name || !ctx.is_inductive(name) {
726        return Err(rejected(
727            DiagnosticCode::InvalidProof,
728            "projection parent mismatch",
729        ));
730    }
731    let ctors = ctx.get_constructors(name);
732    if ctors.len() != 1 {
733        return Err(rejected(
734            DiagnosticCode::InvalidProof,
735            "projection requires exactly one constructor",
736        ));
737    }
738    let parameters = ctx.inductive_num_params(name);
739    let arity = ctx
740        .get_global(name)
741        .ok_or_else(|| rejected(DiagnosticCode::InvalidProof, "missing projection parent"))?
742        .clone();
743    if args.len() != telescope_len(&arity) || args.len() < parameters {
744        return Err(rejected(
745            DiagnosticCode::InvalidProof,
746            "projection parent is not fully applied",
747        ));
748    }
749    let family = |arguments: Vec<Term>| {
750        arguments.into_iter().fold(
751            if levels.is_empty() {
752                Term::Global(name.into())
753            } else {
754                Term::Const {
755                    name: name.into(),
756                    levels: levels.clone(),
757                }
758            },
759            apply,
760        )
761    };
762    let ctor = if levels.is_empty() {
763        Term::Global(ctors[0].0.into())
764    } else {
765        Term::Const {
766            name: ctors[0].0.into(),
767            levels: levels.clone(),
768        }
769    };
770    let mut cty = infer_type(ctx, &ctor).map_err(kernel)?;
771    let mut arity_rest = if levels.is_empty() {
772        arity
773    } else {
774        infer_type(ctx, &family(Vec::new())).map_err(kernel)?
775    };
776    for arg in &args[..parameters] {
777        let (_, body) = open_binder(cty, &format!("{prefix}.parameter"))?;
778        cty = crate::substitute(&body, &format!("{prefix}.parameter"), arg);
779        let (_, rest) = open_binder(arity_rest, &format!("{prefix}.parameter"))?;
780        arity_rest = crate::substitute(&rest, &format!("{prefix}.parameter"), arg);
781    }
782    // The motive ranges over the family at its parameters and ARBITRARY indices.
783    let mut motive_context = ctx.clone();
784    let mut index_binders = Vec::new();
785    for i in parameters..args.len() {
786        let binder = format!("{prefix}.index.{}", i - parameters);
787        let (domain, rest) = open_binder(arity_rest, &binder)?;
788        motive_context = motive_context.extend(&binder, domain.clone());
789        index_binders.push((binder, domain));
790        arity_rest = rest;
791    }
792    let generic = if index_binders.is_empty() {
793        ty.clone()
794    } else {
795        family(
796            args[..parameters]
797                .iter()
798                .cloned()
799                .chain(
800                    index_binders
801                        .iter()
802                        .map(|(binder, _)| Term::Var(binder.clone())),
803                )
804                .collect(),
805        )
806    };
807    let maybe_prop = !Universe::Type(0).is_subtype_of(&sort(ctx, &ty)?);
808    let mut field_context = ctx.clone();
809    let mut fields = Vec::new();
810    while matches!(cty, Term::Pi { .. }) {
811        let field = format!("{prefix}.field.{}", fields.len());
812        let (domain, body) = open_binder(cty, &field)?;
813        // A previous data field needed anywhere in the remaining telescope
814        // would itself require an illegal projection out of a proof.
815        if maybe_prop
816            && fields.len() < index
817            && crate::substitute(
818                &body,
819                &field,
820                &Term::Global("arena.projection.marker".into()),
821            ) != body
822            && !sort(&field_context, &domain)?.equiv(&Universe::Prop)
823        {
824            return Err(rejected(
825                DiagnosticCode::InvalidProof,
826                "dependent projection traverses a propositional data field",
827            ));
828        }
829        field_context = field_context.extend(&field, domain.clone());
830        fields.push((field, domain));
831        cty = body;
832    }
833    let (selected, field_type) = fields.get(index).ok_or_else(|| {
834        rejected(
835            DiagnosticCode::InvalidProof,
836            "projection index out of range",
837        )
838    })?;
839    let motive_name = format!("{prefix}.value");
840    let motive_var = Term::Var(motive_name.clone());
841    let local = motive_context.extend(&motive_name, generic.clone());
842    let mut output = field_type.clone();
843    for (i, (field, _)) in fields.iter().enumerate().take(index).rev() {
844        let marked = crate::substitute(&output, field, &motive_var);
845        if marked != output {
846            let previous = projection(&local, name, i, motive_var.clone(), budget)?;
847            output = crate::substitute(&output, field, &previous);
848        }
849    }
850    if maybe_prop && !sort(&local, &output)?.equiv(&Universe::Prop) {
851        return Err(rejected(
852            DiagnosticCode::InvalidProof,
853            "a possibly propositional structure cannot project data",
854        ));
855    }
856    let mut case = Term::Var(selected.clone());
857    for (field, domain) in fields.into_iter().rev() {
858        case = Term::Lambda {
859            param: field,
860            param_type: Box::new(domain),
861            body: Box::new(case),
862        };
863    }
864    let mut motive = Term::Lambda {
865        param: motive_name,
866        param_type: Box::new(generic),
867        body: Box::new(output),
868    };
869    for (binder, domain) in index_binders.into_iter().rev() {
870        motive = Term::Lambda {
871            param: binder,
872            param_type: Box::new(domain),
873            body: Box::new(motive),
874        };
875    }
876    let result = Term::Match {
877        discriminant: Box::new(disc),
878        motive: Box::new(motive),
879        cases: vec![case],
880    };
881    infer_type(ctx, &result).map_err(kernel)?;
882    Ok(result)
883}
884
885/// The recursor of a registered non-recursive structure, as Lean's kernel reduces it:
886/// `λ params motives minors major. minor (proj₀ params major) … (projₙ params major)`, where the
887/// recursor's telescope has `telescope` binders, the major premise is the last, and `minor` is the
888/// position of the structure's one constructor's minor premise — `nparams + 1` for a lone
889/// structure, after every motive and every earlier member's minors for a member of a block.
890///
891/// Each projection is written out as its checked body, instantiated at the universe instance
892/// of the major premise's type, and never as a reference to the reader's internal projection
893/// constant. The recursor is a published declaration, and its body must mean the same thing in
894/// any environment that has the structure: a printed or replayed recursor cannot depend on a
895/// name that only this reader's context declares.
896fn structure_recursor_body(
897    ctx: &Context,
898    rec_ty: &Term,
899    nparams: usize,
900    minor: usize,
901    telescope: usize,
902    projections: &[String],
903) -> Result<Term> {
904    if nparams >= minor || minor + 1 >= telescope {
905        return Err(rejected(
906            DiagnosticCode::InvalidProof,
907            "structure recursor minor premise outside its telescope",
908        ));
909    }
910    let mut binders = Vec::new();
911    let mut ty = rec_ty.clone();
912    for i in 0..telescope {
913        let name = format!("arena.structure.rec.{i}");
914        let (domain, body) = open_binder(ty, &name)?;
915        binders.push((name, domain));
916        ty = body;
917    }
918    let (_, major_type) = &binders[telescope - 1];
919    let mut head = major_type;
920    while let Term::App(f, _) = head {
921        head = f;
922    }
923    let levels = match head {
924        Term::Global(_) => Vec::new(),
925        Term::Const { levels, .. } => levels.clone(),
926        _ => {
927            return Err(rejected(
928                DiagnosticCode::InvalidProof,
929                "structure recursor major premise is not the structure",
930            ))
931        }
932    };
933    let major = Term::Var(binders[telescope - 1].0.clone());
934    let mut result = Term::Var(binders[minor].0.clone());
935    for projection in projections {
936        let body = if levels.is_empty() {
937            ctx.get_definition_body(projection).cloned()
938        } else {
939            ctx.get_universe_poly(projection)
940                .map(|(declared, _, body)| {
941                    let subst = declared
942                        .iter()
943                        .cloned()
944                        .zip(levels.iter().cloned())
945                        .collect();
946                    crate::instantiate_universes(body, &subst)
947                })
948        };
949        let Some(mut projected) = body else {
950            return Err(rejected(
951                DiagnosticCode::InvalidProof,
952                "structure projection has no checked body",
953            ));
954        };
955        let arguments = binders[..nparams]
956            .iter()
957            .map(|(param, _)| Term::Var(param.clone()))
958            .chain(std::iter::once(major.clone()));
959        for argument in arguments {
960            let Term::Lambda { param, body, .. } = projected else {
961                return Err(rejected(
962                    DiagnosticCode::InvalidProof,
963                    "structure projection arity mismatch",
964                ));
965            };
966            projected = crate::substitute(&body, &param, &argument);
967        }
968        result = apply(result, projected);
969    }
970    for (name, domain) in binders.into_iter().rev() {
971        result = Term::Lambda {
972            param: name,
973            param_type: Box::new(domain),
974            body: Box::new(result),
975        };
976    }
977    Ok(result)
978}
979
980fn source_binder_kind(binder: &Value) -> Result<Option<crate::elaborate::ParamKind>> {
981    use crate::elaborate::ParamKind;
982    match binder.get("binderInfo") {
983        None => Ok(None),
984        Some(value) => Ok(Some(match value.as_str() {
985            Some("default") => ParamKind::Explicit,
986            Some("implicit") => ParamKind::Implicit,
987            Some("strictImplicit") => ParamKind::StrictImplicit,
988            Some("instImplicit") => ParamKind::Instance,
989            _ => {
990                return Err(declined(
991                    DiagnosticCode::MalformedExport,
992                    "unsupported binder information",
993                ))
994            }
995        })),
996    }
997}
998
999type StageSink<'a> = &'a (dyn Fn(&Value) + Sync);
1000
1001struct Importer<'a> {
1002    trace_sink: Option<StageSink<'a>>,
1003    trace_declaration: Option<(usize, String, String, Option<u64>)>,
1004    names: HashMap<usize, String>,
1005    levels: HashMap<usize, Universe>,
1006    level_vars: HashMap<usize, HashSet<String>>,
1007    level_depth: HashMap<usize, usize>,
1008    exprs: HashMap<usize, Value>,
1009    expr_depth: HashMap<usize, usize>,
1010    declarations: HashMap<String, usize>,
1011    ctx: Context,
1012    report: ImportReport,
1013    audit_declarations: bool,
1014    audited_names: HashMap<String, usize>,
1015    runtime_names: HashMap<String, &'static str>,
1016    runtime_target_pending: bool,
1017    retain: RetainSource,
1018    retained: Vec<(String, Term, Term)>,
1019    binder_names: bool,
1020    /// The export header's `meta.lean.version`: the Lean release whose kernel reads the export's
1021    /// string literals (`strings::Packing`). Empty when the header names none.
1022    lean_version: String,
1023}
1024impl<'a> Importer<'a> {
1025    fn new(audit_declarations: bool) -> Self {
1026        Self {
1027            trace_sink: None,
1028            trace_declaration: None,
1029            names: HashMap::from([(0, String::new())]),
1030            levels: HashMap::from([(0, Universe::Prop)]),
1031            level_vars: HashMap::from([(0, HashSet::new())]),
1032            level_depth: HashMap::from([(0, 0)]),
1033            exprs: HashMap::new(),
1034            expr_depth: HashMap::new(),
1035            declarations: HashMap::new(),
1036            ctx: Context::new(),
1037            report: ImportReport {
1038                declaration_audit_requested: audit_declarations,
1039                ..ImportReport::default()
1040            },
1041            audit_declarations,
1042            audited_names: HashMap::new(),
1043            runtime_names: HashMap::new(),
1044            runtime_target_pending: false,
1045            retain: RetainSource::Nothing,
1046            retained: Vec::new(),
1047            binder_names: false,
1048            lean_version: String::new(),
1049        }
1050    }
1051    fn name(&self, v: &Value) -> Result<String> {
1052        let name = self
1053            .names
1054            .get(&index(v)?)
1055            .ok_or_else(|| declined(DiagnosticCode::MalformedExport, "unknown name index"))?;
1056        Ok(canonical_name(name))
1057    }
1058    fn level(&self, v: &Value, params: Option<&HashSet<String>>) -> Result<Universe> {
1059        let i = index(v)?;
1060        let u = self
1061            .levels
1062            .get(&i)
1063            .ok_or_else(|| declined(DiagnosticCode::MalformedExport, "unknown level index"))?;
1064        if let Some(bound) = params {
1065            if !self.level_vars[&i].is_subset(bound) {
1066                return Err(rejected(
1067                    DiagnosticCode::InvalidProof,
1068                    "unbound universe parameter",
1069                ));
1070            }
1071        }
1072        Ok(u.clone())
1073    }
1074    // Inspect the source DAG before lowering: a let value or function argument
1075    // may disappear under reduction but remains subject to Lean's safety rule.
1076    fn check_runtime_references(&self, roots: &[&Value]) -> Result<()> {
1077        if self.runtime_names.is_empty() {
1078            return Ok(());
1079        }
1080        let mut work = roots
1081            .iter()
1082            .map(|id| index(id))
1083            .collect::<Result<Vec<_>>>()?;
1084        let mut seen = HashSet::new();
1085        while let Some(id) = work.pop() {
1086            if !seen.insert(id) {
1087                continue;
1088            }
1089            let row = self.exprs.get(&id).ok_or_else(|| {
1090                declined(
1091                    DiagnosticCode::MalformedExport,
1092                    "unknown safety-dependency expression",
1093                )
1094            })?;
1095            if let Some(constant) = row.get("const") {
1096                let name = self.name(&constant["name"])?;
1097                if let Some(safety) = self.runtime_names.get(&name) {
1098                    return Err(rejected(
1099                        DiagnosticCode::InvalidProof,
1100                        format!("safe declaration references {safety} runtime declaration {name}"),
1101                    ));
1102                }
1103            }
1104            for child in record_children(row) {
1105                work.push(index(child)?);
1106            }
1107        }
1108        Ok(())
1109    }
1110    /// The kernel name of a binder at `depth` (binders enclosing it). `arena.local.<depth>` is unique
1111    /// along every path of a declaration, which is what checking relies on; with binder names
1112    /// requested, Lean's own name follows as `arena.local.<depth>.<name>` — still unique along every
1113    /// path, so conversion, substitution and sharing see the same binding structure.
1114    fn binder_param(&self, depth: usize, name: &Value) -> String {
1115        let base = format!("arena.local.{depth}");
1116        if !self.binder_names {
1117            return base;
1118        }
1119        match index(name)
1120            .ok()
1121            .and_then(|i| self.names.get(&i))
1122            .filter(|n| !n.is_empty())
1123        {
1124            Some(encoded) => format!("{base}.{}", intake::lean_name(encoded)),
1125            None => base,
1126        }
1127    }
1128    fn term(
1129        &self,
1130        id: &Value,
1131        bound: &mut Vec<(String, Term, Option<Term>)>,
1132        params: &HashSet<String>,
1133        budget: &mut usize,
1134    ) -> Result<Term> {
1135        self.term_core(
1136            id,
1137            bound,
1138            params,
1139            budget,
1140            None,
1141            &mut ImportReport::default(),
1142            &mut None,
1143        )
1144    }
1145
1146    fn shared_term(
1147        &mut self,
1148        id: &Value,
1149        bound: &mut Vec<(String, Term, Option<Term>)>,
1150        params: &HashSet<String>,
1151        budget: &mut usize,
1152    ) -> Result<Term> {
1153        let plan = dag::Plan::new(self, index(id)?, bound.len())?;
1154        if plan.size(index(id)?) <= *budget && plan.depth(index(id)?) < dag::CLOSED_CHUNK_DEPTH {
1155            return self.term(id, bound, params, budget);
1156        }
1157        let mut context = self.ctx.clone();
1158        let mut report = ImportReport::default();
1159        let mut sharing = Some(dag::Sharing::new(plan, params));
1160        let result = self.term_core(
1161            id,
1162            bound,
1163            params,
1164            budget,
1165            Some(&mut context),
1166            &mut report,
1167            &mut sharing,
1168        );
1169        let term = result?;
1170        self.ctx = context;
1171        self.report.internal_helpers.extend(report.internal_helpers);
1172        Ok(term)
1173    }
1174
1175    fn term_core(
1176        &self,
1177        id: &Value,
1178        bound: &mut Vec<(String, Term, Option<Term>)>,
1179        params: &HashSet<String>,
1180        budget: &mut usize,
1181        mut ctx: Option<&mut Context>,
1182        report: &mut ImportReport,
1183        sharing: &mut Option<dag::Sharing>,
1184    ) -> Result<Term> {
1185        enum Frame<'a> {
1186            Visit(usize, bool),
1187            FinishSharing(dag::Capture),
1188            App(usize),
1189            BinderType(usize, &'a Value, &'static str),
1190            BinderValue(usize, &'a Value, Term, usize),
1191            BetaDomain(usize, &'a Value, &'a Value),
1192            BetaValue(usize, &'a Value, usize, Term, usize),
1193            BinderBody {
1194                source: usize,
1195                param: String,
1196                ty: Term,
1197                ty_depth: usize,
1198                value: Option<Term>,
1199                value_depth: usize,
1200                lowered: bool,
1201                kind: &'static str,
1202            },
1203            Projection(usize, String, usize),
1204        }
1205        let initial_scope = bound.len();
1206        let result = (|| {
1207            let mut work = vec![Frame::Visit(index(id)?, false)];
1208            let mut values = Vec::<Term>::new();
1209            let mut depths = Vec::<usize>::new();
1210            let mut checked_lets = HashSet::<usize>::new();
1211            // Source IDs mean one immutable expression/universe environment in
1212            // this invocation. The transaction only adds fresh checked helpers.
1213            // Only source-closed annotation/value pairs can reuse this evidence;
1214            // becoming closed after local substitution is not enough.
1215            let mut closed_let_checks = HashSet::<(usize, usize)>::new();
1216            while let Some(frame) = work.pop() {
1217                let mut completed: Option<(usize, Term, usize)> = None;
1218                match frame {
1219                    Frame::Visit(id, unshared) => {
1220                        if !unshared {
1221                            if let Some(shared) = sharing.as_mut() {
1222                                if shared.plan.shared(id) {
1223                                    let capture = shared.capture_budgeted(id, bound, budget)?;
1224                                    if let Some(term) = shared.cached(&capture, budget)? {
1225                                        depths.push(crate::term_store::term_depth(&term));
1226                                        values.push(term);
1227                                    } else {
1228                                        work.push(Frame::FinishSharing(capture));
1229                                        work.push(Frame::Visit(id, true));
1230                                    }
1231                                    continue;
1232                                }
1233                            }
1234                        }
1235                        if *budget == 0 {
1236                            return Err(declined(
1237                                DiagnosticCode::ResourceLimit,
1238                                "term expansion budget",
1239                            ));
1240                        }
1241                        *budget -= 1;
1242                        let row = self.exprs.get(&id).ok_or_else(|| {
1243                            declined(DiagnosticCode::MalformedExport, "unknown expression index")
1244                        })?;
1245                        if let Some(x) = row.get("app") {
1246                            if let Some(shared) = sharing.as_mut() {
1247                                let function = index(&x["fn"])?;
1248                                if let Some(lambda) =
1249                                    self.exprs.get(&function).and_then(|row| row.get("lam"))
1250                                {
1251                                    if shared.beta.is_none() {
1252                                        shared.beta = Some(beta::Certificates::new(
1253                                            self.ctx.clone(),
1254                                            params,
1255                                            bound,
1256                                            budget,
1257                                        )?);
1258                                    }
1259                                    let certificates =
1260                                        shared.beta.as_mut().expect("source certificate pass");
1261                                    if certificates.certify(self, &shared.plan, id, budget)? {
1262                                        work.push(Frame::BetaDomain(id, lambda, &x["arg"]));
1263                                        work.push(Frame::Visit(index(&lambda["type"])?, false));
1264                                        continue;
1265                                    }
1266                                }
1267                            }
1268                            work.push(Frame::App(id));
1269                            work.push(Frame::Visit(index(&x["arg"])?, false));
1270                            work.push(Frame::Visit(index(&x["fn"])?, false));
1271                        } else if let Some(x) = row.get("mdata") {
1272                            // Lean metadata is a transparent wrapper. Preserve its payload
1273                            // in the import report, without introducing a kernel redex.
1274                            work.push(Frame::Visit(index(&x["expr"])?, false));
1275                        } else if let Some(x) = row.get("proj") {
1276                            work.push(Frame::Projection(
1277                                id,
1278                                self.name(&x["typeName"])?,
1279                                index(&x["idx"])?,
1280                            ));
1281                            work.push(Frame::Visit(index(&x["struct"])?, false));
1282                        } else if let Some((kind, x)) = ["forallE", "lam", "letE"]
1283                            .into_iter()
1284                            .find_map(|kind| row.get(kind).map(|x| (kind, x)))
1285                        {
1286                            work.push(Frame::BinderType(id, x, kind));
1287                            work.push(Frame::Visit(index(&x["type"])?, false));
1288                        } else if row.get("strVal").is_some() {
1289                            let term = strings::expand(
1290                                self,
1291                                row,
1292                                id,
1293                                budget,
1294                                ctx.as_deref_mut(),
1295                                report,
1296                                sharing,
1297                            )?;
1298                            let depth = crate::term_store::term_depth(&term);
1299                            completed = Some((id, term, depth));
1300                        } else {
1301                            let term = if let Some(index_value) = row.get("bvar") {
1302                                let position = bound
1303                                    .len()
1304                                    .checked_sub(index(index_value)?.saturating_add(1))
1305                                    .ok_or_else(|| {
1306                                        rejected(
1307                                            DiagnosticCode::MalformedExport,
1308                                            "free bound variable",
1309                                        )
1310                                    })?;
1311                                if checked_lets.contains(&position) {
1312                                    sharing
1313                                        .as_mut()
1314                                        .expect("checked let transaction")
1315                                        .copy_checked_value(
1316                                            bound[position].2.as_ref().expect("checked let value"),
1317                                            budget,
1318                                        )?
1319                                } else {
1320                                    self.term_atom(row, bound, params, budget)?
1321                                }
1322                            } else {
1323                                self.term_atom(row, bound, params, budget)?
1324                            };
1325                            let depth = crate::term_store::term_depth(&term);
1326                            completed = Some((id, term, depth));
1327                        }
1328                    }
1329                    Frame::FinishSharing(capture) => {
1330                        let body = values.pop().expect("decoded shared source template");
1331                        depths.pop().expect("shared source depth");
1332                        let term = sharing.as_mut().expect("active sharing plan").finish(
1333                            ctx.as_deref_mut().expect("sharing transaction"),
1334                            report,
1335                            capture,
1336                            body,
1337                            budget,
1338                        )?;
1339                        depths.push(crate::term_store::term_depth(&term));
1340                        values.push(term);
1341                    }
1342                    Frame::App(source) => {
1343                        let arg = values.pop().expect("decoded application argument");
1344                        let fun = values.pop().expect("decoded application function");
1345                        let arg_depth = depths.pop().expect("application argument depth");
1346                        let fun_depth = depths.pop().expect("application function depth");
1347                        completed = Some((
1348                            source,
1349                            Term::App(Box::new(fun), Box::new(arg)),
1350                            1 + arg_depth.max(fun_depth),
1351                        ));
1352                    }
1353                    Frame::BinderType(source, x, kind) => {
1354                        let ty = values.pop().expect("decoded binder domain");
1355                        let ty_depth = depths.pop().expect("binder domain depth");
1356                        if kind == "letE" {
1357                            work.push(Frame::BinderValue(source, x, ty, ty_depth));
1358                            work.push(Frame::Visit(index(&x["value"])?, false));
1359                        } else {
1360                            let param = self.binder_param(bound.len(), &x["name"]);
1361                            if let Some(certificates) =
1362                                sharing.as_mut().and_then(|s| s.beta.as_mut())
1363                            {
1364                                certificates.enter(&ty, budget)?;
1365                            }
1366                            bound.push((param.clone(), ty.clone(), None));
1367                            work.push(Frame::BinderBody {
1368                                source,
1369                                param,
1370                                ty,
1371                                ty_depth,
1372                                value: None,
1373                                value_depth: 0,
1374                                lowered: false,
1375                                kind,
1376                            });
1377                            work.push(Frame::Visit(index(&x["body"])?, false));
1378                        }
1379                    }
1380                    Frame::BetaDomain(source, lambda, argument) => {
1381                        let ty = values.pop().expect("certified beta domain");
1382                        let ty_depth = depths.pop().expect("certified beta domain depth");
1383                        work.push(Frame::BetaValue(
1384                            source,
1385                            lambda,
1386                            index(argument)?,
1387                            ty,
1388                            ty_depth,
1389                        ));
1390                        work.push(Frame::Visit(index(argument)?, false));
1391                    }
1392                    Frame::BetaValue(source, lambda, value_source, ty, ty_depth) => {
1393                        let mut value = values.pop().expect("certified beta value");
1394                        let mut value_depth = depths.pop().expect("certified beta value depth");
1395                        let lowered = sharing
1396                            .as_mut()
1397                            .expect("beta sharing transaction")
1398                            .check_closed_let(
1399                                ctx.as_deref().expect("beta transaction"),
1400                                &ty,
1401                                &value,
1402                                budget,
1403                            )?;
1404                        if lowered {
1405                            value = sharing
1406                                .as_mut()
1407                                .expect("beta sharing transaction")
1408                                .share_closed_value(
1409                                    ctx.as_deref_mut().expect("beta transaction"),
1410                                    report,
1411                                    value_source,
1412                                    value,
1413                                    budget,
1414                                )?;
1415                            value_depth = crate::term_store::term_depth(&value);
1416                        }
1417                        // Generic source typing has already been certified. A beta
1418                        // redex is definitionally this let even if it stays open.
1419                        let param = self.binder_param(bound.len(), &lambda["name"]);
1420                        if lowered {
1421                            checked_lets.insert(bound.len());
1422                        }
1423                        if let Some(certificates) = sharing.as_mut().and_then(|s| s.beta.as_mut()) {
1424                            certificates.enter(&ty, budget)?;
1425                        }
1426                        bound.push((param.clone(), ty.clone(), Some(value.clone())));
1427                        work.push(Frame::BinderBody {
1428                            source,
1429                            param,
1430                            ty,
1431                            ty_depth,
1432                            value: Some(value),
1433                            value_depth,
1434                            lowered,
1435                            kind: "letE",
1436                        });
1437                        work.push(Frame::Visit(index(&lambda["body"])?, false));
1438                    }
1439                    Frame::BinderValue(source, x, ty, ty_depth) => {
1440                        let mut value = values.pop().expect("decoded let value");
1441                        let mut value_depth = depths.pop().expect("let value depth");
1442                        let source_pair = (index(&x["type"])?, index(&x["value"])?);
1443                        let let_body = index(&x["body"])?;
1444                        let retained = sharing
1445                            .as_ref()
1446                            .is_none_or(|shared| shared.retains_let(let_body));
1447                        let reusable = sharing.as_ref().is_some_and(|shared| {
1448                            shared.plan.closed(source_pair.0) && shared.plan.closed(source_pair.1)
1449                        });
1450                        let lowered = if retained {
1451                            false
1452                        } else if reusable && closed_let_checks.contains(&source_pair) {
1453                            true
1454                        } else if let Some(shared) = sharing.as_mut() {
1455                            let checked = shared.check_closed_let(
1456                                ctx.as_deref().expect("let transaction"),
1457                                &ty,
1458                                &value,
1459                                budget,
1460                            )?;
1461                            if checked && reusable {
1462                                closed_let_checks.insert(source_pair);
1463                            }
1464                            checked
1465                        } else {
1466                            false
1467                        };
1468                        if lowered {
1469                            value = sharing
1470                                .as_mut()
1471                                .expect("let sharing transaction")
1472                                .share_closed_value(
1473                                    ctx.as_deref_mut().expect("let transaction"),
1474                                    report,
1475                                    index(&x["value"])?,
1476                                    value,
1477                                    budget,
1478                                )?;
1479                            value_depth = crate::term_store::term_depth(&value);
1480                        }
1481                        let param = self.binder_param(bound.len(), &x["name"]);
1482                        if lowered {
1483                            checked_lets.insert(bound.len());
1484                        }
1485                        if let Some(certificates) = sharing.as_mut().and_then(|s| s.beta.as_mut()) {
1486                            certificates.enter(&ty, budget)?;
1487                        }
1488                        bound.push((param.clone(), ty.clone(), Some(value.clone())));
1489                        work.push(Frame::BinderBody {
1490                            source,
1491                            param,
1492                            ty,
1493                            ty_depth,
1494                            value: Some(value),
1495                            value_depth,
1496                            lowered,
1497                            kind: "letE",
1498                        });
1499                        work.push(Frame::Visit(index(&x["body"])?, false));
1500                    }
1501                    Frame::BinderBody {
1502                        source,
1503                        param,
1504                        ty,
1505                        ty_depth,
1506                        value,
1507                        value_depth,
1508                        lowered,
1509                        kind,
1510                    } => {
1511                        checked_lets.remove(&(bound.len() - 1));
1512                        if let Some(certificates) = sharing.as_mut().and_then(|s| s.beta.as_mut()) {
1513                            certificates.leave();
1514                        }
1515                        bound.pop();
1516                        let body = values.pop().expect("decoded binder body");
1517                        let body_depth = depths.pop().expect("binder body depth");
1518                        if lowered {
1519                            completed = Some((source, body, body_depth));
1520                        } else {
1521                            let body = Box::new(body);
1522                            let param_type = Box::new(ty);
1523                            let term = match kind {
1524                                "forallE" => Term::Pi {
1525                                    param,
1526                                    param_type,
1527                                    body_type: body,
1528                                },
1529                                "lam" => Term::Lambda {
1530                                    param,
1531                                    param_type,
1532                                    body,
1533                                },
1534                                _ => Term::Let {
1535                                    name: param,
1536                                    ty: param_type,
1537                                    value: Box::new(value.expect("let value")),
1538                                    body,
1539                                },
1540                            };
1541                            completed =
1542                                Some((source, term, 1 + body_depth.max(ty_depth).max(value_depth)));
1543                        }
1544                    }
1545                    Frame::Projection(source, name, field) => {
1546                        let disc = values.pop().expect("decoded projection discriminant");
1547                        depths.pop().expect("projection discriminant depth");
1548                        let mut local = ctx.as_deref().unwrap_or(&self.ctx).clone();
1549                        for (name, ty, _) in bound.iter() {
1550                            local = local.extend(name, ty.clone());
1551                        }
1552                        let mut trial_budget = *budget;
1553                        let projected =
1554                            projection(&local, &name, field, disc.clone(), &mut trial_budget);
1555                        // The first attempt consumed real work even if a local
1556                        // definition hid the structure and forces a retry.
1557                        *budget = trial_budget;
1558                        match projected {
1559                            Ok(projected) => {
1560                                let depth = crate::term_store::term_depth(&projected);
1561                                completed = Some((source, projected, depth));
1562                            }
1563                            Err(error) if !bound.iter().any(|(_, _, value)| value.is_some()) => {
1564                                return Err(error)
1565                            }
1566                            Err(_) => {
1567                                // A local definition can hide the structure head or
1568                                // a dependent field type. Expand only this lowering
1569                                // site, retaining the surrounding checked Let nodes.
1570                                let expand = |term: &Term, prefix: usize| {
1571                                    let mut term = term.clone();
1572                                    for (local, _, value) in bound[..prefix].iter().rev() {
1573                                        if let Some(value) = value {
1574                                            term = crate::substitute(&term, local, value);
1575                                        }
1576                                    }
1577                                    term
1578                                };
1579                                let mut transparent = ctx.as_deref().unwrap_or(&self.ctx).clone();
1580                                for (i, (local, ty, _)) in bound.iter().enumerate() {
1581                                    transparent = transparent.extend(local, expand(ty, i));
1582                                }
1583                                let projected = projection(
1584                                    &transparent,
1585                                    &name,
1586                                    field,
1587                                    expand(&disc, bound.len()),
1588                                    budget,
1589                                )?;
1590                                let depth = crate::term_store::term_depth(&projected);
1591                                completed = Some((source, projected, depth));
1592                            }
1593                        }
1594                    }
1595                }
1596                if let Some((source, mut term, mut depth)) = completed {
1597                    // The declaration's own term — what reaches the root through nothing but
1598                    // lowered lets, which pass their body on — is published as the declaration; a
1599                    // helper for it would only move the same term behind one more name.
1600                    if depth >= dag::CLOSED_CHUNK_DEPTH
1601                        && sharing.is_some()
1602                        && work
1603                            .iter()
1604                            .rev()
1605                            .any(|frame| !matches!(frame, Frame::BinderBody { lowered: true, .. }))
1606                    {
1607                        if let Some(shared) = sharing.as_mut() {
1608                            let (cut, did_cut) = shared.cut_closed(
1609                                ctx.as_deref_mut().expect("closed chunk transaction"),
1610                                report,
1611                                source,
1612                                term,
1613                                budget,
1614                            )?;
1615                            term = cut;
1616                            if did_cut {
1617                                depth = 1;
1618                            }
1619                        }
1620                    }
1621                    depths.push(depth);
1622                    values.push(term);
1623                }
1624            }
1625            let term = values.pop().expect("decoded expression");
1626            debug_assert!(values.is_empty());
1627            Ok(term)
1628        })();
1629        bound.truncate(initial_scope);
1630        result
1631    }
1632    fn term_atom(
1633        &self,
1634        r: &Value,
1635        bound: &[(String, Term, Option<Term>)],
1636        params: &HashSet<String>,
1637        budget: &mut usize,
1638    ) -> Result<Term> {
1639        if let Some(x) = r.get("sort") {
1640            return Ok(Term::Sort(self.level(x, Some(params))?));
1641        }
1642        if let Some(x) = r.get("bvar") {
1643            return bound
1644                .len()
1645                .checked_sub(index(x)?.saturating_add(1))
1646                .map(|i| Term::Var(bound[i].0.clone()))
1647                .ok_or_else(|| rejected(DiagnosticCode::MalformedExport, "free bound variable"));
1648        }
1649        if let Some(x) = r.get("const") {
1650            let name = self.name(&x["name"])?;
1651            if let Some(safety) = self.runtime_names.get(&name) {
1652                return Err(rejected(
1653                    DiagnosticCode::InvalidProof,
1654                    format!("safe term references {safety} runtime declaration {name}"),
1655                ));
1656            }
1657            let arity = self.declarations.get(&name).ok_or_else(|| {
1658                rejected(
1659                    DiagnosticCode::MissingDeclaration,
1660                    "undeclared or forward constant",
1661                )
1662            })?;
1663            let levels = array(&x["us"])?
1664                .iter()
1665                .map(|u| self.level(u, Some(params)))
1666                .collect::<Result<Vec<_>>>()?;
1667            if levels.len() != *arity {
1668                return Err(rejected(
1669                    DiagnosticCode::InvalidProof,
1670                    "constant universe arity mismatch",
1671                ));
1672            }
1673            return Ok(if levels.is_empty() {
1674                Term::Global(name)
1675            } else {
1676                Term::Const { name, levels }
1677            });
1678        }
1679        if let Some(x) = r.get("natVal") {
1680            let digits = x.as_str().ok_or_else(|| {
1681                declined(DiagnosticCode::MalformedExport, "malformed natural literal")
1682            })?;
1683            let value = parse_natural_literal(digits, budget)?;
1684            let nat = "Nat";
1685            let zero = "Zero";
1686            let succ = "Succ";
1687            let ctors = self.ctx.get_constructors(nat);
1688            if self.declarations.get(nat) != Some(&0)
1689                || ctors.len() != 2
1690                || ctors[0].0 != zero
1691                || ctors[1].0 != succ
1692                || self.ctx.inductive_num_params(nat) != 0
1693            {
1694                return Err(rejected(
1695                    DiagnosticCode::InvalidProof,
1696                    "natural literal requires canonical checked Nat",
1697                ));
1698            }
1699            let nt = Term::Global(nat.into());
1700            let declared = self
1701                .ctx
1702                .get_global(nat)
1703                .ok_or_else(|| rejected(DiagnosticCode::MissingDeclaration, "missing Nat"))?;
1704            exact(&self.ctx, declared, &Term::Sort(Universe::Type(0)))?;
1705            exact(&self.ctx, ctors[0].1, &nt)?;
1706            exact(
1707                &self.ctx,
1708                ctors[1].1,
1709                &Term::Pi {
1710                    param: "arena.nat.value".into(),
1711                    param_type: Box::new(nt.clone()),
1712                    body_type: Box::new(nt),
1713                },
1714            )?;
1715            return Ok(Term::Lit(crate::Literal::Nat(value)));
1716        }
1717        Err(declined(
1718            DiagnosticCode::UnknownRecordKind,
1719            "unsupported expression (metadata or new format)",
1720        ))
1721    }
1722
1723    fn parameters(&self, d: &Value) -> Result<Vec<String>> {
1724        let levels = array(&d["levelParams"])?
1725            .iter()
1726            .map(|x| self.name(x))
1727            .collect::<Result<Vec<_>>>()?;
1728        if levels.iter().collect::<HashSet<_>>().len() != levels.len() {
1729            return Err(rejected(
1730                DiagnosticCode::InvalidProof,
1731                "duplicate universe parameter",
1732            ));
1733        }
1734        Ok(levels)
1735    }
1736    fn declaration_term(&self, id: &Value, params: &[String]) -> Result<Term> {
1737        self.term(
1738            id,
1739            &mut Vec::new(),
1740            &params.iter().cloned().collect(),
1741            &mut 100_000,
1742        )
1743    }
1744    /// Register `name` as a structure with checked projection functions, returning their names, when
1745    /// its carrier is surely not a proposition. `None` leaves it an ordinary one-constructor family.
1746    fn structure_view(
1747        &mut self,
1748        name: &str,
1749        levels: &[String],
1750        ctor_type: &Term,
1751    ) -> Result<Option<Vec<String>>> {
1752        // Lean's `isStructureLike` asks `isRec` of the whole block before anything else: a family
1753        // whose block is recursive is an ordinary family, and no projection is built for it.
1754        if self.ctx.block_is_recursive(name) {
1755            return Ok(None);
1756        }
1757        let parameters = self.ctx.inductive_num_params(name);
1758        let mut ty = ctor_type.clone();
1759        let mut params = Vec::new();
1760        let mut local = self.ctx.clone();
1761        let mut carrier = if levels.is_empty() {
1762            Term::Global(name.into())
1763        } else {
1764            Term::Const {
1765                name: name.into(),
1766                levels: levels.iter().cloned().map(Universe::Var).collect(),
1767            }
1768        };
1769        for i in 0..parameters {
1770            let param = format!("arena.structure.parameter.{i}");
1771            let (domain, body) = open_binder(ty, &param)?;
1772            local = local.extend(&param, domain.clone());
1773            carrier = apply(carrier, Term::Var(param.clone()));
1774            params.push((param, domain));
1775            ty = body;
1776        }
1777        // A symbolic carrier which might be Prop needs conditional projection
1778        // rules. Those are handled at each projection use, not by generic metadata.
1779        if !Universe::Type(0).is_subtype_of(&sort(&local, &carrier)?) {
1780            return Ok(None);
1781        }
1782        let mut fields = Vec::new();
1783        while matches!(ty, Term::Pi { .. }) {
1784            let field = format!("arena.structure.field.{}", fields.len());
1785            let (domain, body) = open_binder(ty, &field)?;
1786            fields.push((field, domain));
1787            ty = body;
1788        }
1789        let disc_name = "arena.structure.value";
1790        local = local.extend(disc_name, carrier.clone());
1791        let mut scratch = self.ctx.clone();
1792        let mut projections = Vec::new();
1793        for i in 0..fields.len() {
1794            let mut body = projection(&local, name, i, Term::Var(disc_name.into()), &mut 100_000)?;
1795            body = Term::Lambda {
1796                param: disc_name.into(),
1797                param_type: Box::new(carrier.clone()),
1798                body: Box::new(body),
1799            };
1800            for (param, domain) in params.iter().rev() {
1801                body = Term::Lambda {
1802                    param: param.clone(),
1803                    param_type: Box::new(domain.clone()),
1804                    body: Box::new(body),
1805                };
1806            }
1807            let ty = infer_type(&scratch, &body).map_err(kernel)?;
1808            let projection_name = format!("arena.structure.projection.{name}.{i}");
1809            if levels.is_empty() {
1810                scratch
1811                    .declare(&projection_name, ty, body)
1812                    .map_err(kernel)?;
1813            } else {
1814                scratch
1815                    .declare_universe_poly(&projection_name, levels.to_vec(), ty, body)
1816                    .map_err(kernel)?;
1817            }
1818            projections.push(projection_name);
1819        }
1820        // η is granted by the kernel's one door, which re-decides everything built above. A
1821        // family Lean does not call structure-like (a field reaching its own mutual block) stays
1822        // an ordinary family; any other refusal means this reader built something the kernel
1823        // will not certify, and the declaration is declined rather than approximated.
1824        match scratch.register_structure_checked(name, &projections) {
1825            Ok(()) => {}
1826            Err(crate::StructureRefusal::Recursive { .. }) => return Ok(None),
1827            Err(refusal) => {
1828                return Err(declined(
1829                    DiagnosticCode::UnsupportedFeature,
1830                    format!("structure η for {name}: {refusal}"),
1831                ))
1832            }
1833        }
1834        self.ctx = scratch;
1835        Ok(Some(projections))
1836    }
1837
1838    fn quotient(&mut self, d: &Value) -> Result<()> {
1839        use crate::quotient::{self, Kind};
1840        let kind = match d["kind"].as_str() {
1841            Some("type") => Kind::Type,
1842            Some("ctor") => Kind::Constructor,
1843            Some("lift") => Kind::Lift,
1844            Some("ind") => Kind::Induction,
1845            _ => {
1846                return Err(rejected(
1847                    DiagnosticCode::UnknownRecordKind,
1848                    "unknown quotient primitive kind",
1849                ))
1850            }
1851        };
1852        let name = self.name(&d["name"])?;
1853        if name != kind.name() {
1854            return Err(rejected(
1855                DiagnosticCode::InvalidProof,
1856                "quotient primitive name does not match its kind",
1857            ));
1858        }
1859        if self.declarations.contains_key(&name) {
1860            return Err(rejected(
1861                DiagnosticCode::DuplicateDeclaration,
1862                "duplicate quotient declaration",
1863            ));
1864        }
1865        let params = self.parameters(d)?;
1866        if params.len() != kind.arity() {
1867            return Err(rejected(
1868                DiagnosticCode::InvalidProof,
1869                "quotient primitive universe arity",
1870            ));
1871        }
1872        let ty = self.declaration_term(&d["type"], &params)?;
1873        let levels = params
1874            .iter()
1875            .cloned()
1876            .map(Universe::Var)
1877            .collect::<Vec<_>>();
1878        let equality = "/s:\"Eq\"";
1879        let expected = quotient::signature(kind, &levels, equality).map_err(kernel)?;
1880        lean_type(&self.ctx, &ty)?;
1881        exact(&self.ctx, &ty, &expected)?;
1882        quotient::install(&mut self.ctx, kind, params.clone(), equality)
1883            .map_err(|e| rejected(DiagnosticCode::InvalidProof, e.to_string()))?;
1884        self.declarations.insert(name, params.len());
1885        Ok(())
1886    }
1887
1888    fn install_binder_kinds(&mut self, declaration: &Value) -> Result<()> {
1889        // The side table records a declaration's PARAMETERS, as LOGOS's own declaration door
1890        // does: the binders its signature introduces. A definition's parameters are the ones
1891        // its value abstracts, so a function type in the declared result (`(f : T) : T` with
1892        // `T := ⦃x⦄ → {y} → Type`) is not flattened into the declaration's own telescope.
1893        // A declaration without a value records its whole leading telescope.
1894        let parameters = match declaration.get("value") {
1895            Some(value) => Some(self.leading_binders(index(value)?, "lam")?),
1896            None => None,
1897        };
1898        let mut expression = index(&declaration["type"])?;
1899        let mut kinds = Vec::new();
1900        let mut binders = 0;
1901        let mut complete = true;
1902        while parameters.is_none_or(|count| binders < count) {
1903            let row = self.exprs.get(&expression).ok_or_else(|| {
1904                declined(
1905                    DiagnosticCode::MalformedExport,
1906                    "missing declaration binder expression",
1907                )
1908            })?;
1909            if let Some(metadata) = row.get("mdata") {
1910                expression = index(&metadata["expr"])?;
1911            } else if let Some(binder) = row.get("forallE") {
1912                match source_binder_kind(binder)? {
1913                    Some(kind) => kinds.push(kind),
1914                    None => complete = false,
1915                }
1916                binders += 1;
1917                expression = index(&binder["body"])?;
1918            } else {
1919                break;
1920            }
1921        }
1922        // Missing source metadata must not become a fabricated Explicit entry.
1923        // Only the declaration's leading telescope belongs in this side table;
1924        // nested function arguments retain their fully elaborated kernel terms.
1925        if complete {
1926            let name = self.name(&declaration["name"])?;
1927            self.ctx.set_binder_kinds(&name, kinds);
1928        }
1929        Ok(())
1930    }
1931
1932    /// The number of leading `kind` binders of a source expression, seen through metadata.
1933    fn leading_binders(&self, mut expression: usize, kind: &str) -> Result<usize> {
1934        let mut count = 0;
1935        loop {
1936            let row = self.exprs.get(&expression).ok_or_else(|| {
1937                declined(
1938                    DiagnosticCode::MalformedExport,
1939                    "missing declaration binder expression",
1940                )
1941            })?;
1942            if let Some(metadata) = row.get("mdata") {
1943                expression = index(&metadata["expr"])?;
1944            } else if let Some(binder) = row.get(kind) {
1945                count += 1;
1946                expression = index(&binder["body"])?;
1947            } else {
1948                return Ok(count);
1949            }
1950        }
1951    }
1952
1953    fn inductive(&mut self, d: &Value) -> Result<()> {
1954        let types = array(&d["types"])?;
1955        let ctors = array(&d["ctors"])?;
1956        let recs = array(&d["recs"])?;
1957        let mut source_roots = Vec::new();
1958        for item in types.iter().chain(ctors).chain(recs) {
1959            source_roots.push(&item["type"]);
1960        }
1961        for rec in recs {
1962            for rule in array(&rec["rules"])? {
1963                source_roots.push(&rule["rhs"]);
1964            }
1965        }
1966        self.check_runtime_references(&source_roots)?;
1967
1968        if types.is_empty() {
1969            // No Lean environment exports a block without a family: the record is malformed, not a
1970            // declaration the kernel refutes.
1971            return Err(declined(
1972                DiagnosticCode::MalformedExport,
1973                "empty inductive block",
1974            ));
1975        }
1976        if types.len() > 1 {
1977            return self.ordinary_mutual_inductive(types, ctors, recs);
1978        }
1979        let header = &types[0];
1980        let name = self.name(&header["name"])?;
1981        let params = self.parameters(header)?;
1982        let nested_count = index(&header["numNested"])?;
1983        for item in types.iter().chain(ctors).chain(recs) {
1984            if item["isUnsafe"] != false {
1985                return Err(inductive_safety(
1986                    &item["isUnsafe"],
1987                    "unsafe or unspecified inductive safety",
1988                ));
1989            }
1990        }
1991        let mut names = HashSet::new();
1992        for item in types.iter().chain(ctors).chain(recs) {
1993            let n = self.name(&item["name"])?;
1994            if self.declarations.contains_key(&n) || !names.insert(n) {
1995                return Err(rejected(
1996                    DiagnosticCode::InvalidProof,
1997                    "duplicate declaration in inductive block",
1998                ));
1999            }
2000        }
2001        let all = array(&header["all"])?
2002            .iter()
2003            .map(|x| self.name(x))
2004            .collect::<Result<Vec<_>>>()?;
2005        if all != vec![name.clone()] {
2006            return Err(rejected(
2007                DiagnosticCode::InvalidProof,
2008                "inconsistent inductive block membership",
2009            ));
2010        }
2011        let nparams = index(&header["numParams"])?;
2012        let nindices = index(&header["numIndices"])?;
2013        let arity = self.declaration_term(&header["type"], &params)?;
2014        lean_type(&self.ctx, &arity)?;
2015        let arity = declared_arity(&self.ctx, &arity)?;
2016        let mut result = &arity;
2017        while let Term::Pi { body_type, .. } = result {
2018            result = body_type;
2019        }
2020        if !matches!(result, Term::Sort(_))
2021            || nparams.checked_add(nindices) != Some(telescope_len(&arity))
2022        {
2023            return Err(rejected(
2024                DiagnosticCode::InvalidProof,
2025                "inductive arity or parameter/index count is invalid",
2026            ));
2027        }
2028        let declared_ctors = array(&header["ctors"])?
2029            .iter()
2030            .map(|x| self.name(x))
2031            .collect::<Result<Vec<_>>>()?;
2032        if declared_ctors.len() != ctors.len() {
2033            return Err(rejected(
2034                DiagnosticCode::InvalidProof,
2035                "constructor inventory mismatch",
2036            ));
2037        }
2038        let old_ctx = self.ctx.clone();
2039        // Temporary headers permit constructor type references; the checked block
2040        // registration below validates the complete group before it is used.
2041        if params.is_empty() {
2042            self.ctx
2043                .add_indexed_inductive(&name, arity.clone(), nparams);
2044        } else {
2045            self.ctx
2046                .add_universe_poly_inductive(&name, params.clone(), arity.clone(), nparams);
2047        }
2048        self.declarations.insert(name.clone(), params.len());
2049        let mut checked_ctors = Vec::new();
2050        for (i, c) in ctors.iter().enumerate() {
2051            let cname = self.name(&c["name"])?;
2052            if self.name(&c["induct"])? != name
2053                || index(&c["cidx"])? != i
2054                || cname != declared_ctors[i]
2055                || index(&c["numParams"])? != nparams
2056            {
2057                return Err(rejected(
2058                    DiagnosticCode::InvalidProof,
2059                    "constructor parent, order or parameter count mismatch",
2060                ));
2061            }
2062            if self.parameters(c)? != params {
2063                return Err(rejected(
2064                    DiagnosticCode::InvalidProof,
2065                    "constructor universe telescope mismatch",
2066                ));
2067            }
2068            let cty = self.declaration_term(&c["type"], &params)?;
2069            lean_type(&self.ctx, &cty)?;
2070            if nparams.checked_add(index(&c["numFields"])?)
2071                != Some(resolved_telescope_len(&self.ctx, &cty)?)
2072            {
2073                return Err(rejected(
2074                    DiagnosticCode::InvalidProof,
2075                    "constructor field count mismatch",
2076                ));
2077            }
2078            // Preserve the constructor's original index expressions. Lean's large-
2079            // elimination test recognizes bare variables, not normalized inverses.
2080            checked_ctors.push((cname, cty));
2081        }
2082        // Reserved primitive identities must denote exactly the checked Peano
2083        // datatype, even if this export never uses a literal. Otherwise native
2084        // reduction rules could act on an unrelated datatype with the same name.
2085        let reserved = |n: &str| matches!(n, "Nat" | "Zero" | "Succ");
2086        if reserved(&name) || checked_ctors.iter().any(|(n, _)| reserved(n)) {
2087            if name != "Nat"
2088                || !params.is_empty()
2089                || nparams != 0
2090                || nindices != 0
2091                || checked_ctors.len() != 2
2092                || checked_ctors[0].0 != "Zero"
2093                || checked_ctors[1].0 != "Succ"
2094            {
2095                return Err(declined(
2096                    DiagnosticCode::UnsupportedFeature,
2097                    "noncanonical datatype uses primitive Nat identities",
2098                ));
2099            }
2100            let nt = Term::Global("Nat".into());
2101            let canonical = exact(&self.ctx, &arity, &Term::Sort(Universe::Type(0)))
2102                .and_then(|_| exact(&self.ctx, &checked_ctors[0].1, &nt))
2103                .and_then(|_| {
2104                    exact(
2105                        &self.ctx,
2106                        &checked_ctors[1].1,
2107                        &Term::Pi {
2108                            param: "arena.nat.field".into(),
2109                            param_type: Box::new(nt.clone()),
2110                            body_type: Box::new(nt),
2111                        },
2112                    )
2113                });
2114            if canonical.is_err() {
2115                return Err(declined(
2116                    DiagnosticCode::UnsupportedFeature,
2117                    "noncanonical datatype uses primitive Nat identities",
2118                ));
2119            }
2120        }
2121        let block = crate::MutualInductive {
2122            name: name.clone(),
2123            sort: arity.clone(),
2124            num_params: nparams,
2125            constructors: checked_ctors.clone(),
2126        };
2127        let mut verified = old_ctx.clone();
2128        if params.is_empty() {
2129            verified
2130                .add_mutual_inductives(&[block])
2131                .map_err(inductive_verdict)?;
2132        } else {
2133            verified
2134                .add_universe_poly_inductives(&params, &[block])
2135                .map_err(inductive_verdict)?;
2136        }
2137        let nested_decl = if nested_count > 0 {
2138            // Parameters, universes, indices and indexed containers are reconstructed exactly,
2139            // by Lean's own elimination; a shape outside it is declined, never approximated.
2140            let decl = crate::NestedDecl {
2141                name: name.clone(),
2142                sort: arity.clone(),
2143                constructors: checked_ctors.clone(),
2144            };
2145            // Establish the reconstruction boundary before registration.
2146            crate::inductive_compile::eliminate_nested(
2147                &old_ctx,
2148                &crate::inductive_compile::NestedBlock {
2149                    members: std::slice::from_ref(&decl),
2150                    num_params: nparams,
2151                    levels: &params,
2152                },
2153            )
2154            .map_err(inductive_verdict)?;
2155            Some(decl)
2156        } else {
2157            None
2158        };
2159        self.ctx = verified;
2160        for (n, _) in &checked_ctors {
2161            self.declarations.insert(n.clone(), params.len());
2162        }
2163        if let Some(decl) = nested_decl {
2164            return self.native_nested_recursors(
2165                std::slice::from_ref(&decl),
2166                nparams,
2167                &params,
2168                recs,
2169                &all,
2170                nested_count,
2171            );
2172        }
2173        if recs.len() != 1 {
2174            // This path has already established a single, non-nested inductive
2175            // block. Such a block has exactly its one canonical recursor;
2176            // extra records cannot introduce arbitrary inhabitants as recursors.
2177            return Err(rejected(
2178                DiagnosticCode::InvalidProof,
2179                "noncanonical recursor inventory for a single non-nested inductive",
2180            ));
2181        }
2182        let rec = &recs[0];
2183        let rec_name = self.name(&rec["name"])?;
2184        if rec_name != format!("{name}/s:\"rec\"") {
2185            return Err(rejected(
2186                DiagnosticCode::InvalidProof,
2187                "recursor does not have its canonical generated name",
2188            ));
2189        }
2190        if index(&rec["numParams"])? != nparams
2191            || index(&rec["numIndices"])? != nindices
2192            || index(&rec["numMotives"])? != 1
2193            || index(&rec["numMinors"])? != ctors.len()
2194        {
2195            return Err(rejected(
2196                DiagnosticCode::InvalidProof,
2197                "recursor telescope counts mismatch",
2198            ));
2199        }
2200        if array(&rec["all"])?
2201            .iter()
2202            .map(|x| self.name(x))
2203            .collect::<Result<Vec<_>>>()?
2204            != all
2205        {
2206            return Err(rejected(
2207                DiagnosticCode::InvalidProof,
2208                "recursor block membership mismatch",
2209            ));
2210        }
2211        let expected_k = self.ctx.k_constructor(&name).is_some();
2212        if rec["k"].as_bool() != Some(expected_k) {
2213            return Err(rejected(
2214                DiagnosticCode::InvalidProof,
2215                "recursor K flag disagrees with its checked inductive",
2216            ));
2217        }
2218        let levels = self.parameters(rec)?;
2219        let (derived_levels, derived_ty, derived_body) =
2220            crate::derive_recursor_universe_poly(&self.ctx, &name).map_err(|e| {
2221                declined(
2222                    DiagnosticCode::UnsupportedFeature,
2223                    format!("cannot derive recursor: {e}"),
2224                )
2225            })?;
2226        if levels.len() != derived_levels.len() {
2227            return Err(rejected(
2228                DiagnosticCode::InvalidProof,
2229                "recursor universe telescope mismatch",
2230            ));
2231        }
2232        let subst = derived_levels
2233            .into_iter()
2234            .zip(levels.iter().cloned().map(Universe::Var))
2235            .collect();
2236        let derived_ty = crate::instantiate_universes(&derived_ty, &subst);
2237        let derived_body = crate::instantiate_universes(&derived_body, &subst);
2238        let mut computation = &derived_body;
2239        while let Term::Lambda { body, .. } = computation {
2240            computation = body;
2241        }
2242        let nonrecursive = !matches!(computation, Term::Fix { .. } | Term::MutualFix { .. });
2243        // Lean's iota reduces a structure's recursor on ANY major premise of a non-recursive,
2244        // never-`Prop` structure type by expanding it to `mk params (proj₀ e) … (projₙ e)`
2245        // (`toCtorWhenStruct`). Registering the structure first lets the recursor say exactly
2246        // that: its body applies the minor premise to the projections of the major premise,
2247        // which both kernels check against the derived type through structure η.
2248        let projections = if nindices == 0 && nonrecursive && checked_ctors.len() == 1 {
2249            self.structure_view(&name, &params, &checked_ctors[0].1)?
2250        } else {
2251            None
2252        };
2253        let derived_body = match &projections {
2254            Some(projections) => structure_recursor_body(
2255                &self.ctx,
2256                &derived_ty,
2257                nparams,
2258                nparams + 1,
2259                nparams + 3,
2260                projections,
2261            )?,
2262            None => derived_body,
2263        };
2264
2265        let exported_ty = self.declaration_term(&rec["type"], &levels)?;
2266        lean_type(&self.ctx, &exported_ty)?;
2267        exact(&self.ctx, &exported_ty, &derived_ty)?;
2268        // The recursor is registered at Lean's statement, which the derivation has just been
2269        // checked to be by exact conversion; its body is the derivation's.
2270        // Register only the derived implementation. The input's rules are evidence
2271        // to compare, never executable reducer instructions trusted on their own.
2272        if levels.is_empty() {
2273            self.ctx
2274                .declare(&rec_name, exported_ty.clone(), derived_body)
2275                .map_err(kernel)?;
2276        } else {
2277            self.ctx
2278                .declare_universe_poly(&rec_name, levels.clone(), exported_ty.clone(), derived_body)
2279                .map_err(kernel)?;
2280        }
2281        self.declarations.insert(rec_name.clone(), levels.len());
2282        let rules = array(&rec["rules"])?;
2283        if rules.len() != ctors.len() {
2284            return Err(rejected(
2285                DiagnosticCode::InvalidProof,
2286                "missing or additional recursor computation rule",
2287            ));
2288        }
2289        let mut seen = HashSet::new();
2290        for rule in rules {
2291            let cname = self.name(&rule["ctor"])?;
2292            let Some(i) = declared_ctors.iter().position(|n| n == &cname) else {
2293                return Err(rejected(
2294                    DiagnosticCode::InvalidProof,
2295                    "recursor rule names a foreign constructor",
2296                ));
2297            };
2298            if !seen.insert(cname.clone()) || rule["nfields"] != ctors[i]["numFields"] {
2299                return Err(rejected(
2300                    DiagnosticCode::InvalidProof,
2301                    "duplicate rule or wrong field count",
2302                ));
2303            }
2304            let expected = ctor_rule(
2305                &self.ctx,
2306                &rec_name,
2307                &levels,
2308                &derived_ty,
2309                &cname,
2310                &checked_ctors[i].1,
2311                nparams,
2312                nparams + 1 + ctors.len(),
2313            )?;
2314            let actual = self.declaration_term(&rule["rhs"], &levels)?;
2315            let actual_ty = lean_type(&self.ctx, &actual)?;
2316            let expected_ty = infer_type(&self.ctx, &expected).map_err(kernel)?;
2317            exact(&self.ctx, &actual_ty, &expected_ty)?;
2318            exact(&self.ctx, &actual, &expected)?;
2319        }
2320        Ok(())
2321    }
2322
2323    fn ordinary_mutual_inductive(
2324        &mut self,
2325        types: &[Value],
2326        ctors: &[Value],
2327        recs: &[Value],
2328    ) -> Result<()> {
2329        use crate::MutualInductive;
2330        struct Member {
2331            declaration: MutualInductive,
2332            indices: usize,
2333            constructors: Vec<String>,
2334        }
2335        let mut names = HashSet::new();
2336        for item in types.iter().chain(ctors).chain(recs) {
2337            if item["isUnsafe"] != false {
2338                return Err(inductive_safety(
2339                    &item["isUnsafe"],
2340                    "unsafe or unspecified mutual inductive safety",
2341                ));
2342            }
2343            let name = self.name(&item["name"])?;
2344            if self.declarations.contains_key(&name) || !names.insert(name.clone()) {
2345                return Err(rejected(
2346                    DiagnosticCode::InvalidProof,
2347                    "duplicate declaration in mutual block",
2348                ));
2349            }
2350            if matches!(name.as_str(), "Nat" | "Zero" | "Succ") || name.starts_with("kernel.quot.")
2351            {
2352                return Err(rejected(
2353                    DiagnosticCode::InvalidProof,
2354                    "primitive identities require their canonical declaration",
2355                ));
2356            }
2357        }
2358        let all = types
2359            .iter()
2360            .map(|h| self.name(&h["name"]))
2361            .collect::<Result<Vec<_>>>()?;
2362        let params = self.parameters(&types[0])?;
2363        let nparams = index(&types[0]["numParams"])?;
2364        let mut members = Vec::new();
2365        let mut shared_domains: Vec<Term> = Vec::new();
2366        let mut common_sort: Option<Universe> = None;
2367        let nested_count = index(&types[0]["numNested"])?;
2368        for (i, header) in types.iter().enumerate() {
2369            // Lean records the block's auxiliary count on every member.
2370            if index(&header["numNested"])? != nested_count {
2371                return Err(rejected(
2372                    DiagnosticCode::InvalidProof,
2373                    "mutual members disagree on the block's nested count",
2374                ));
2375            }
2376            if self.parameters(header)? != params || index(&header["numParams"])? != nparams {
2377                return Err(rejected(
2378                    DiagnosticCode::InvalidProof,
2379                    "mutual headers disagree on their shared parameters",
2380                ));
2381            }
2382            if array(&header["all"])?
2383                .iter()
2384                .map(|n| self.name(n))
2385                .collect::<Result<Vec<_>>>()?
2386                != all
2387            {
2388                return Err(rejected(
2389                    DiagnosticCode::InvalidProof,
2390                    "mutual header block membership mismatch",
2391                ));
2392            }
2393            let arity = self.declaration_term(&header["type"], &params)?;
2394            lean_type(&self.ctx, &arity)?;
2395            let arity = declared_arity(&self.ctx, &arity)?;
2396            let indices = index(&header["numIndices"])?;
2397            if nparams.checked_add(indices) != Some(telescope_len(&arity)) {
2398                return Err(rejected(
2399                    DiagnosticCode::InvalidProof,
2400                    "mutual header parameter/index count mismatch",
2401                ));
2402            }
2403            let mut telescope = arity.clone();
2404            let mut local = self.ctx.clone();
2405            for j in 0..nparams {
2406                let name = format!("arena.mutual.parameter.{j}");
2407                let (domain, body) = open_binder(telescope, &name)?;
2408                if i == 0 {
2409                    shared_domains.push(domain.clone());
2410                } else {
2411                    exact(&local, &domain, &shared_domains[j])?;
2412                }
2413                local = local.extend(&name, domain);
2414                telescope = body;
2415            }
2416            while let Term::Pi { body_type, .. } = telescope {
2417                telescope = *body_type;
2418            }
2419            let Term::Sort(level) = telescope else {
2420                return Err(rejected(
2421                    DiagnosticCode::InvalidProof,
2422                    "mutual header does not return a sort",
2423                ));
2424            };
2425            if let Some(common) = &common_sort {
2426                if !level.equiv(common) {
2427                    return Err(rejected(
2428                        DiagnosticCode::InvalidProof,
2429                        "mutual members have different result universes",
2430                    ));
2431                }
2432            } else {
2433                common_sort = Some(level);
2434            }
2435            let constructors = array(&header["ctors"])?
2436                .iter()
2437                .map(|n| self.name(n))
2438                .collect::<Result<Vec<_>>>()?;
2439            members.push(Member {
2440                declaration: MutualInductive {
2441                    name: all[i].clone(),
2442                    sort: arity,
2443                    num_params: nparams,
2444                    constructors: Vec::new(),
2445                },
2446                indices,
2447                constructors,
2448            });
2449        }
2450        let old_ctx = self.ctx.clone();
2451        for member in &members {
2452            let declaration = &member.declaration;
2453            if params.is_empty() {
2454                self.ctx.add_indexed_inductive(
2455                    &declaration.name,
2456                    declaration.sort.clone(),
2457                    nparams,
2458                );
2459            } else {
2460                self.ctx.add_universe_poly_inductive(
2461                    &declaration.name,
2462                    params.clone(),
2463                    declaration.sort.clone(),
2464                    nparams,
2465                );
2466            }
2467            self.declarations
2468                .insert(declaration.name.clone(), params.len());
2469        }
2470        let mut used = HashSet::new();
2471        for member in &mut members {
2472            for (i, name) in member.constructors.iter().enumerate() {
2473                let ctor = ctors
2474                    .iter()
2475                    .find(|c| self.name(&c["name"]).ok().as_ref() == Some(name))
2476                    .ok_or_else(|| {
2477                        rejected(
2478                            DiagnosticCode::InvalidProof,
2479                            "missing declared mutual constructor",
2480                        )
2481                    })?;
2482                if !used.insert(name.clone())
2483                    || self.name(&ctor["induct"])? != member.declaration.name
2484                    || index(&ctor["cidx"])? != i
2485                    || index(&ctor["numParams"])? != nparams
2486                {
2487                    return Err(rejected(
2488                        DiagnosticCode::InvalidProof,
2489                        "mutual constructor parent/order/parameter mismatch",
2490                    ));
2491                }
2492                if self.parameters(ctor)? != params {
2493                    return Err(rejected(
2494                        DiagnosticCode::InvalidProof,
2495                        "mutual constructor universe telescope mismatch",
2496                    ));
2497                }
2498                let ty = self.declaration_term(&ctor["type"], &params)?;
2499                lean_type(&self.ctx, &ty)?;
2500                if nparams.checked_add(index(&ctor["numFields"])?)
2501                    != Some(resolved_telescope_len(&self.ctx, &ty)?)
2502                {
2503                    return Err(rejected(
2504                        DiagnosticCode::InvalidProof,
2505                        "mutual constructor field count mismatch",
2506                    ));
2507                }
2508                member.declaration.constructors.push((name.clone(), ty));
2509            }
2510        }
2511        if used.len() != ctors.len() {
2512            return Err(rejected(
2513                DiagnosticCode::InvalidProof,
2514                "undeclared constructor in mutual block",
2515            ));
2516        }
2517        let block = members
2518            .iter()
2519            .map(|m| m.declaration.clone())
2520            .collect::<Vec<_>>();
2521        self.ctx = old_ctx;
2522        if params.is_empty() {
2523            self.ctx
2524                .add_mutual_inductives(&block)
2525                .map_err(inductive_verdict)?;
2526        } else {
2527            self.ctx
2528                .add_universe_poly_inductives(&params, &block)
2529                .map_err(inductive_verdict)?;
2530        }
2531        for member in &members {
2532            for name in &member.constructors {
2533                self.declarations.insert(name.clone(), params.len());
2534            }
2535        }
2536        if nested_count > 0 {
2537            let decls: Vec<crate::NestedDecl> = members
2538                .iter()
2539                .map(|m| crate::NestedDecl {
2540                    name: m.declaration.name.clone(),
2541                    sort: m.declaration.sort.clone(),
2542                    constructors: m.declaration.constructors.clone(),
2543                })
2544                .collect();
2545            return self.native_nested_recursors(
2546                &decls,
2547                nparams,
2548                &params,
2549                recs,
2550                &all,
2551                nested_count,
2552            );
2553        }
2554        if recs.len() != members.len() {
2555            return Err(rejected(
2556                DiagnosticCode::InvalidProof,
2557                "mutual recursor inventory mismatch",
2558            ));
2559        }
2560        // Lean's `isStructureLike` holds for a member with one constructor and no indices whose
2561        // BLOCK is not recursive (`isRec` is decided for the whole block), and its iota then reduces
2562        // that member's recursor on any major premise. Every such member is registered through the
2563        // kernel's one door — which refuses a recursive block, leaving the member an ordinary
2564        // family — before any recursor, because a structure's recursor relies on its η.
2565        let mut structures = Vec::with_capacity(members.len());
2566        for member in &members {
2567            structures.push(match &member.declaration.constructors[..] {
2568                [(_, constructor)] if member.indices == 0 => {
2569                    self.structure_view(&member.declaration.name, &params, constructor)?
2570                }
2571                _ => None,
2572            });
2573        }
2574        // Lean gives every motive in an ordinary mutual block one shared
2575        // elimination universe. A block whose result may be Prop is restricted
2576        // to Prop; the single-member singleton exception does not apply.
2577        let large = Universe::Type(0).is_subtype_of(common_sort.as_ref().expect("nonempty block"));
2578        let motive_offset = usize::from(large);
2579        let prefix = nparams
2580            .checked_add(members.len())
2581            .and_then(|v| v.checked_add(ctors.len()))
2582            .ok_or_else(|| {
2583                declined(
2584                    DiagnosticCode::ResourceLimit,
2585                    "mutual recursor telescope overflow",
2586                )
2587            })?;
2588        let mut checked = Vec::new();
2589        let mut minors_before = 0;
2590        for (member, projections) in members.iter().zip(&structures) {
2591            let minor = prefix - ctors.len() + minors_before;
2592            minors_before += member.constructors.len();
2593            let name = format!("{}/s:\"rec\"", member.declaration.name);
2594            let position = recs
2595                .iter()
2596                .position(|r| self.name(&r["name"]).ok().as_ref() == Some(&name))
2597                .ok_or_else(|| {
2598                    rejected(
2599                        DiagnosticCode::InvalidProof,
2600                        "missing canonical mutual recursor",
2601                    )
2602                })?;
2603            let rec = &recs[position];
2604            if index(&rec["numParams"])? != nparams
2605                || index(&rec["numIndices"])? != member.indices
2606                || index(&rec["numMotives"])? != members.len()
2607                || index(&rec["numMinors"])? != ctors.len()
2608            {
2609                return Err(rejected(
2610                    DiagnosticCode::InvalidProof,
2611                    "mutual recursor telescope counts mismatch",
2612                ));
2613            }
2614            if array(&rec["all"])?
2615                .iter()
2616                .map(|n| self.name(n))
2617                .collect::<Result<Vec<_>>>()?
2618                != all
2619            {
2620                return Err(rejected(
2621                    DiagnosticCode::InvalidProof,
2622                    "mutual recursor block membership mismatch",
2623                ));
2624            }
2625            if rec["k"].as_bool() != Some(false) {
2626                return Err(rejected(
2627                    DiagnosticCode::InvalidProof,
2628                    "multi-member recursor cannot have K flag",
2629                ));
2630            }
2631            let levels = self.parameters(rec)?;
2632            if levels.len() != params.len() + motive_offset {
2633                return Err(rejected(
2634                    DiagnosticCode::InvalidProof,
2635                    "mutual recursor universe telescope mismatch",
2636                ));
2637            }
2638            let (derived_levels, ty, body) =
2639                crate::derive_recursor_universe_poly(&self.ctx, &member.declaration.name).map_err(
2640                    |e| {
2641                        declined(
2642                            DiagnosticCode::UnsupportedFeature,
2643                            format!("cannot derive mutual recursor: {e}"),
2644                        )
2645                    },
2646                )?;
2647            let motive_count = derived_levels
2648                .len()
2649                .checked_sub(params.len())
2650                .ok_or_else(|| {
2651                    declined(
2652                        DiagnosticCode::UnsupportedFeature,
2653                        "derived mutual universe inventory mismatch",
2654                    )
2655                })?;
2656            if derived_levels[motive_count..] != params {
2657                return Err(declined(
2658                    DiagnosticCode::UnsupportedFeature,
2659                    "derived mutual inductive universe telescope mismatch",
2660                ));
2661            }
2662            let motive = if large {
2663                Universe::Var(levels[0].clone())
2664            } else {
2665                Universe::Prop
2666            };
2667            let mut substitution: HashMap<String, Universe> = derived_levels[..motive_count]
2668                .iter()
2669                .cloned()
2670                .map(|name| (name, motive.clone()))
2671                .collect();
2672            let parameter_substitution: HashMap<String, Universe> = params
2673                .iter()
2674                .cloned()
2675                .zip(levels[motive_offset..].iter().cloned().map(Universe::Var))
2676                .collect();
2677            substitution.extend(parameter_substitution.clone());
2678            let ty = crate::instantiate_universes(&ty, &substitution);
2679            let body = match projections {
2680                Some(projections) => structure_recursor_body(
2681                    &self.ctx,
2682                    &ty,
2683                    nparams,
2684                    minor,
2685                    prefix + 1,
2686                    projections,
2687                )?,
2688                None => crate::instantiate_universes(&body, &substitution),
2689            };
2690            let exported = self.declaration_term(&rec["type"], &levels)?;
2691            lean_type(&self.ctx, &exported)?;
2692            exact(&self.ctx, &exported, &ty)?;
2693            if levels.is_empty() {
2694                self.ctx.declare(&name, exported, body).map_err(kernel)?;
2695            } else {
2696                self.ctx
2697                    .declare_universe_poly(&name, levels.clone(), exported, body)
2698                    .map_err(kernel)?;
2699            }
2700            self.declarations.insert(name.clone(), levels.len());
2701            checked.push((name, levels, ty, position, parameter_substitution));
2702        }
2703        // All sibling implementations are now in scope for rule references.
2704        for (member, (name, levels, ty, position, substitution)) in members.iter().zip(&checked) {
2705            let rules = array(&recs[*position]["rules"])?;
2706            if rules.len() != member.constructors.len() {
2707                return Err(rejected(
2708                    DiagnosticCode::InvalidProof,
2709                    "mutual computation rule inventory mismatch",
2710                ));
2711            }
2712            let mut seen = HashSet::new();
2713            for rule in rules {
2714                let constructor = self.name(&rule["ctor"])?;
2715                let Some((_, ctor_ty)) = member
2716                    .declaration
2717                    .constructors
2718                    .iter()
2719                    .find(|(n, _)| n == &constructor)
2720                else {
2721                    return Err(rejected(
2722                        DiagnosticCode::InvalidProof,
2723                        "mutual rule names a foreign constructor",
2724                    ));
2725                };
2726                let field_count = resolved_telescope_len(&self.ctx, ctor_ty)?
2727                    .checked_sub(nparams)
2728                    .ok_or_else(|| {
2729                        rejected(
2730                            DiagnosticCode::InvalidProof,
2731                            "mutual constructor lacks shared parameters",
2732                        )
2733                    })?;
2734                if !seen.insert(constructor.clone()) || index(&rule["nfields"])? != field_count {
2735                    return Err(rejected(
2736                        DiagnosticCode::InvalidProof,
2737                        "mutual rule duplicate constructor or wrong field count",
2738                    ));
2739                }
2740                let value = if params.is_empty() {
2741                    Term::Global(constructor)
2742                } else {
2743                    Term::Const {
2744                        name: constructor,
2745                        levels: params.iter().map(|p| substitution[p].clone()).collect(),
2746                    }
2747                };
2748                let ctor_ty = crate::instantiate_universes(ctor_ty, substitution);
2749                let expected = ctor_rule_applied(
2750                    &self.ctx, name, levels, ty, value, ctor_ty, nparams, prefix, nparams,
2751                )?;
2752                let actual = self.declaration_term(&rule["rhs"], levels)?;
2753                let actual_type = lean_type(&self.ctx, &actual)?;
2754                let expected_type = infer_type(&self.ctx, &expected).map_err(kernel)?;
2755                exact(&self.ctx, &actual_type, &expected_type)?;
2756                exact(&self.ctx, &actual, &expected)?;
2757            }
2758        }
2759        Ok(())
2760    }
2761
2762    fn declaration_audit(
2763        &self,
2764        name: &str,
2765        kind: &str,
2766        source: &Value,
2767        ty: &Term,
2768        body: Option<&Term>,
2769    ) -> Result<DeclarationAudit> {
2770        self.trace_stage("audit", "begin");
2771        let main_type = infer_type(&self.ctx, ty).map_err(kernel)?;
2772        let independent_type = crate::recheck(&self.ctx, ty).map_err(|e| {
2773            independent_verdict(&e, format!("independent declaration-type check for {name}"))
2774        })?;
2775        independent_comparison(
2776            exact(&self.ctx, &main_type, &independent_type),
2777            format!("independent declaration-type disagreement for {name}"),
2778        )?;
2779        // Lean's `#print axioms` (`CollectAxioms`): a definition or theorem rests on the axioms its
2780        // type and value reach; an axiom rests on ITSELF, and its own type is not visited. The same
2781        // shared closure (`prelude::axiom_dependencies`) is the package rule's; a declaration this
2782        // import already audited answers with the axioms recorded for it.
2783        let known = |dependency: &str| {
2784            self.audited_names
2785                .get(dependency)
2786                .map(|i| self.report.declarations[*i].axioms.clone())
2787        };
2788        let mut usage = if kind == "axiom" {
2789            crate::prelude::AxiomUsage::default()
2790        } else {
2791            crate::prelude::axiom_dependencies_given(&self.ctx, ty, &known)
2792        };
2793        if let Some(body) = body {
2794            let independent = crate::recheck(&self.ctx, body).map_err(|e| {
2795                independent_verdict(&e, format!("independent declaration-body check for {name}"))
2796            })?;
2797            independent_comparison(
2798                exact(&self.ctx, &independent, ty),
2799                format!("independent declaration-body type disagreement for {name}"),
2800            )?;
2801            usage.absorb(crate::prelude::axiom_dependencies_given(&self.ctx, body, &known));
2802        }
2803        if !usage.unresolved.is_empty() {
2804            return Err(declined(
2805                DiagnosticCode::AuditIncomplete,
2806                format!(
2807                    "unresolved declaration audit dependencies for {name}: {:?}",
2808                    usage.unresolved
2809                ),
2810            ));
2811        }
2812        let mut axioms = std::collections::BTreeSet::new();
2813        for dependency in usage.axioms {
2814            // Checked opaque proofs stay opaque to conversion, while their
2815            // recorded proof dependencies remain visible to the trust audit.
2816            if let Some(i) = self.audited_names.get(&dependency) {
2817                axioms.extend(self.report.declarations[*i].axioms.iter().cloned());
2818            } else {
2819                axioms.insert(dependency);
2820            }
2821        }
2822        if kind == "axiom" {
2823            axioms.insert(name.to_string());
2824        }
2825        self.trace_stage("audit", "done");
2826        Ok(DeclarationAudit {
2827            name: name.into(),
2828            kind: kind.into(),
2829            type_expression: index(&source["type"])?,
2830            value_expression: body.map(|_| index(&source["value"])).transpose()?,
2831            axioms: axioms.into_iter().collect(),
2832        })
2833    }
2834
2835    fn native_nested_recursors(
2836        &mut self,
2837        members: &[crate::NestedDecl],
2838        nparams: usize,
2839        family_levels: &[String],
2840        recs: &[Value],
2841        all: &[String],
2842        nested_count: usize,
2843    ) -> Result<()> {
2844        fn compare(ctx: &Context, actual: &Term, expected: &Term, stage: &str) -> Result<()> {
2845            exact(ctx, actual, expected).map_err(|error| error.located(stage))
2846        }
2847        let native = crate::inductive_compile::derive_nested_block_recursors(
2848            &self.ctx,
2849            &crate::inductive_compile::NestedBlock {
2850                members,
2851                num_params: nparams,
2852                levels: family_levels,
2853            },
2854        )
2855        .map_err(inductive_verdict)?;
2856        let motives = native.len();
2857        if nested_count.checked_add(members.len()) != Some(motives) || recs.len() != motives {
2858            return Err(rejected(
2859                DiagnosticCode::InvalidProof,
2860                "native nested recursor inventory mismatch",
2861            ));
2862        }
2863        // Carriers and constructor groups are open exactly over the shared parameters; the
2864        // parameters are in scope for their sorts and telescopes.
2865        let parameters = native
2866            .first()
2867            .map(|rec| rec.parameters.clone())
2868            .unwrap_or_default();
2869        if parameters.len() != nparams {
2870            return Err(declined(
2871                DiagnosticCode::UnsupportedFeature,
2872                "native nested reconstruction lost the uniform parameter telescope",
2873            ));
2874        }
2875        let mut local = self.ctx.clone();
2876        for (parameter, domain) in &parameters {
2877            local = local.extend(parameter, domain.clone());
2878        }
2879        // The sort a carrier's family lives in, after its indices.
2880        let family_sort = |carrier: &Term| -> Result<Universe> {
2881            let mut ty = normal(&local, &infer_type(&local, carrier).map_err(kernel)?)?;
2882            while let Term::Pi { body_type, .. } = ty {
2883                ty = *body_type;
2884            }
2885            match ty {
2886                Term::Sort(level) => Ok(level),
2887                _ => Err(rejected(
2888                    DiagnosticCode::InvalidProof,
2889                    "a nested carrier is not a family of types",
2890                )),
2891            }
2892        };
2893        // Each group contains original constructors partially applied to the
2894        // fixed container arguments, with their original field telescope. Every carrier
2895        // lives in the block's own sort, as every member of Lean's auxiliary block does.
2896        let block_sort = family_sort(&native[0].carrier)?;
2897        let mut constructor_groups = Vec::new();
2898        let mut indices = Vec::new();
2899        let mut minors = 0usize;
2900        for rec in &native {
2901            if !family_sort(&rec.carrier)?.equiv(&block_sort) {
2902                return Err(declined(
2903                    DiagnosticCode::UnsupportedFeature,
2904                    "native nested container outside the family's universe",
2905                ));
2906            }
2907            let mut head = &rec.carrier;
2908            let mut arguments = Vec::new();
2909            while let Term::App(f, a) = head {
2910                arguments.push((**a).clone());
2911                head = f;
2912            }
2913            arguments.reverse();
2914            let (parent, levels) = match head {
2915                Term::Global(name) => (name, Vec::new()),
2916                Term::Const { name, levels } => (name, levels.clone()),
2917                _ => {
2918                    return Err(declined(
2919                        DiagnosticCode::UnsupportedFeature,
2920                        "native nested carrier has no inductive head",
2921                    ))
2922                }
2923            };
2924            if self.ctx.inductive_num_params(parent) != arguments.len() {
2925                return Err(declined(
2926                    DiagnosticCode::UnsupportedFeature,
2927                    "native nested container parameters are not fully fixed",
2928                ));
2929            }
2930            indices.push(self.ctx.inductive_arity(parent) - arguments.len());
2931            let mut constructors = Vec::new();
2932            for (name, _) in self.ctx.get_constructors(parent) {
2933                let mut value = match head {
2934                    Term::Const { .. } => Term::Const {
2935                        name: name.into(),
2936                        levels: levels.clone(),
2937                    },
2938                    _ => Term::Global(name.into()),
2939                };
2940                let mut ty = infer_type(&self.ctx, &value).map_err(kernel)?;
2941                for argument in &arguments {
2942                    let Term::Pi {
2943                        param, body_type, ..
2944                    } = ty
2945                    else {
2946                        return Err(declined(
2947                            DiagnosticCode::UnsupportedFeature,
2948                            "native constructor parameter telescope unavailable",
2949                        ));
2950                    };
2951                    ty = crate::substitute(&body_type, &param, argument);
2952                    value = apply(value, argument.clone());
2953                }
2954                let fields = resolved_telescope_len(&local, &ty)?;
2955                constructors.push((name.to_string(), value, ty, arguments.len(), fields));
2956            }
2957            minors = minors.checked_add(constructors.len()).ok_or_else(|| {
2958                declined(
2959                    DiagnosticCode::ResourceLimit,
2960                    "native minor inventory overflow",
2961                )
2962            })?;
2963            constructor_groups.push(constructors);
2964        }
2965        let prefix = nparams
2966            .checked_add(motives)
2967            .and_then(|n| n.checked_add(minors))
2968            .ok_or_else(|| {
2969                declined(
2970                    DiagnosticCode::ResourceLimit,
2971                    "native recursor prefix overflow",
2972                )
2973            })?;
2974        let mut checked = Vec::new();
2975        // All checked implementations enter scope before decoding any exported
2976        // computation rule, because the rules refer to sibling recursors.
2977        for (i, rec) in native.into_iter().enumerate() {
2978            // Each source member's own recursor, then the auxiliaries' as `rec_1, rec_2, …` of
2979            // the block's first member.
2980            let (owner, suffix) = if i < members.len() {
2981                (&members[i].name, "rec".to_string())
2982            } else {
2983                (&members[0].name, format!("rec_{}", i + 1 - members.len()))
2984            };
2985            let name = format!("{}/s:{}", owner, serde_json::to_string(&suffix).unwrap());
2986            let position = recs
2987                .iter()
2988                .position(|r| self.name(&r["name"]).ok().as_deref() == Some(name.as_str()))
2989                .ok_or_else(|| {
2990                    rejected(
2991                        DiagnosticCode::InvalidProof,
2992                        "missing canonical native nested recursor",
2993                    )
2994                })?;
2995            let exported = &recs[position];
2996            if index(&exported["numParams"])? != nparams
2997                || index(&exported["numIndices"])? != indices[i]
2998                || index(&exported["numMotives"])? != motives
2999                || index(&exported["numMinors"])? != minors
3000            {
3001                return Err(rejected(
3002                    DiagnosticCode::InvalidProof,
3003                    "native nested recursor telescope counts mismatch",
3004                ));
3005            }
3006            if array(&exported["all"])?
3007                .iter()
3008                .map(|n| self.name(n))
3009                .collect::<Result<Vec<_>>>()?
3010                != all
3011            {
3012                return Err(rejected(
3013                    DiagnosticCode::InvalidProof,
3014                    "native nested recursor block membership mismatch",
3015                ));
3016            }
3017            if exported["k"].as_bool() != Some(false) {
3018                return Err(rejected(
3019                    DiagnosticCode::InvalidProof,
3020                    "a nested recursor has an invalid K flag",
3021                ));
3022            }
3023            // Lean's auxiliary block is a mutual block: it eliminates into every sort through
3024            // one shared motive universe when its members are types, and only into `Prop`
3025            // when they may be propositions (no single-member exception applies).
3026            let large = Universe::Type(0).is_subtype_of(&block_sort);
3027            let levels = self.parameters(exported)?;
3028            if levels.len() != usize::from(large) + family_levels.len() {
3029                return Err(rejected(
3030                    DiagnosticCode::InvalidProof,
3031                    "native nested recursor universe telescope mismatch",
3032                ));
3033            }
3034            let derived_motives = rec
3035                .levels
3036                .len()
3037                .checked_sub(family_levels.len())
3038                .filter(|&m| rec.levels[m..] == *family_levels)
3039                .ok_or_else(|| {
3040                    declined(
3041                        DiagnosticCode::UnsupportedFeature,
3042                        "native reconstruction has unexpected motive universe parameters",
3043                    )
3044                })?;
3045            // The LOGOS generator supports independent motive universes; this checked
3046            // instantiation supplies Lean's single motive universe (or `Prop`) to all of them
3047            // and Lean's names to the family's own levels.
3048            let offset = usize::from(large);
3049            let substitution = rec
3050                .levels
3051                .iter()
3052                .enumerate()
3053                .map(|(i, p)| {
3054                    let lean = if i < derived_motives {
3055                        if large {
3056                            Universe::Var(levels[0].clone())
3057                        } else {
3058                            Universe::Prop
3059                        }
3060                    } else {
3061                        Universe::Var(levels[offset + (i - derived_motives)].clone())
3062                    };
3063                    (p.clone(), lean)
3064                })
3065                .collect();
3066            let ty = crate::instantiate_universes(&rec.ty, &substitution);
3067            let body = crate::instantiate_universes(&rec.body, &substitution);
3068            let expected = self.declaration_term(&exported["type"], &levels)?;
3069            lean_type(&self.ctx, &expected)?;
3070            compare(
3071                &self.ctx,
3072                &expected,
3073                &ty,
3074                &format!("native recursor {name} type"),
3075            )?;
3076            if levels.is_empty() {
3077                self.ctx.declare(&name, expected, body).map_err(kernel)?;
3078            } else {
3079                self.ctx
3080                    .declare_universe_poly(&name, levels.clone(), expected, body)
3081                    .map_err(kernel)?;
3082            }
3083            self.declarations.insert(name.clone(), levels.len());
3084            checked.push((name, levels, ty, position));
3085        }
3086        // The rule telescope binds the shared parameters first. Constructor groups are
3087        // instantiated at those binders, never at the canonical names they were built over.
3088        let rule_parameters: Vec<(String, Term)> = parameters
3089            .iter()
3090            .enumerate()
3091            .map(|(i, (parameter, _))| (parameter.clone(), Term::Var(format!("arena.rule.{i}"))))
3092            .collect();
3093        let offset = usize::from(Universe::Type(0).is_subtype_of(&block_sort));
3094        let at_rule = |term: &Term, levels: &[String]| {
3095            let universes = family_levels
3096                .iter()
3097                .cloned()
3098                .zip(levels[offset..].iter().cloned().map(Universe::Var))
3099                .collect();
3100            rule_parameters.iter().fold(
3101                crate::instantiate_universes(term, &universes),
3102                |term, (parameter, binder)| crate::substitute(&term, parameter, binder),
3103            )
3104        };
3105        for ((name, levels, ty, position), constructors) in checked.iter().zip(&constructor_groups)
3106        {
3107            let rules = array(&recs[*position]["rules"])?;
3108            if rules.len() != constructors.len() {
3109                return Err(rejected(
3110                    DiagnosticCode::InvalidProof,
3111                    "native nested recursor computation rule inventory mismatch",
3112                ));
3113            }
3114            let mut seen = HashSet::new();
3115            for rule in rules {
3116                let constructor = self.name(&rule["ctor"])?;
3117                let Some((_, value, ctor_ty, result_parameters, fields)) =
3118                    constructors.iter().find(|(n, ..)| n == &constructor)
3119                else {
3120                    return Err(rejected(
3121                        DiagnosticCode::InvalidProof,
3122                        "native nested rule names a foreign constructor",
3123                    ));
3124                };
3125                if !seen.insert(constructor) || index(&rule["nfields"])? != *fields {
3126                    return Err(rejected(
3127                        DiagnosticCode::InvalidProof,
3128                        "native nested rule has duplicate constructor or wrong field count",
3129                    ));
3130                }
3131                let expected = ctor_rule_applied(
3132                    &self.ctx,
3133                    name,
3134                    levels,
3135                    ty,
3136                    at_rule(value, levels),
3137                    at_rule(ctor_ty, levels),
3138                    0,
3139                    prefix,
3140                    *result_parameters,
3141                )?;
3142                let actual = self.declaration_term(&rule["rhs"], levels)?;
3143                let actual_ty = lean_type(&self.ctx, &actual)?;
3144                let expected_ty = infer_type(&self.ctx, &expected).map_err(kernel)?;
3145                compare(
3146                    &self.ctx,
3147                    &actual_ty,
3148                    &expected_ty,
3149                    &format!("native recursor {name} rule type"),
3150                )?;
3151                compare(
3152                    &self.ctx,
3153                    &actual,
3154                    &expected,
3155                    &format!("native recursor {name} rule value"),
3156                )?;
3157            }
3158        }
3159        Ok(())
3160    }
3161
3162    /// Formatting and identifier allocation are entirely opt-in. The sink is
3163    /// diagnostic-only: I/O failure cannot change a kernel verdict.
3164    fn trace_stage(&self, stage: &str, event: &str) {
3165        emit_stage(
3166            self.trace_sink,
3167            self.trace_declaration.as_ref(),
3168            stage,
3169            event,
3170        );
3171    }
3172
3173    fn record(&mut self, r: Value, line: usize) -> Result<()> {
3174        if self.trace_sink.is_none() {
3175            return self.record_inner(r);
3176        }
3177        let declaration = r
3178            .as_object()
3179            .filter(|object| object.len() == 1)
3180            .and_then(|object| object.iter().next())
3181            .filter(|(kind, _)| {
3182                matches!(
3183                    kind.as_str(),
3184                    "def" | "axiom" | "thm" | "opaque" | "inductive" | "quot"
3185                )
3186            });
3187        if let Some((kind, source)) = declaration {
3188            let name_source = if kind == "inductive" {
3189                &source["types"][0]["name"]
3190            } else {
3191                &source["name"]
3192            };
3193            let name = self
3194                .name(name_source)
3195                .unwrap_or_else(|_| "<unresolved>".into());
3196            self.trace_declaration = Some((line, name, kind.clone(), name_source.as_u64()));
3197            self.trace_stage("declaration", "begin");
3198        }
3199        let result = self.record_inner(r);
3200        self.trace_stage("declaration", if result.is_ok() { "done" } else { "error" });
3201        self.trace_declaration = None;
3202        result
3203    }
3204
3205    fn record_inner(&mut self, r: Value) -> Result<()> {
3206        let obj = r
3207            .as_object()
3208            .ok_or_else(|| declined(DiagnosticCode::MalformedExport, "record is not an object"))?;
3209        if let Some(i) = r.get("in") {
3210            if obj.len() != 2 {
3211                return Err(declined(
3212                    DiagnosticCode::MalformedExport,
3213                    "unknown name record fields",
3214                ));
3215            }
3216            let i = index(i)?;
3217            if self.names.contains_key(&i) {
3218                return Err(declined(
3219                    DiagnosticCode::MalformedExport,
3220                    "duplicate name index",
3221                ));
3222            }
3223            let (kind, x) = if let Some(x) = r.get("str") {
3224                ("s", x)
3225            } else if let Some(x) = r.get("num") {
3226                ("n", x)
3227            } else {
3228                return Err(declined(
3229                    DiagnosticCode::MalformedExport,
3230                    "unknown name kind",
3231                ));
3232            };
3233            let pre = self
3234                .names
3235                .get(&index(&x["pre"])?)
3236                .ok_or_else(|| declined(DiagnosticCode::MalformedExport, "unknown name prefix"))?;
3237            let part =
3238                if kind == "s" {
3239                    serde_json::to_string(x["str"].as_str().ok_or_else(|| {
3240                        declined(DiagnosticCode::MalformedExport, "bad name string")
3241                    })?)
3242                    .unwrap()
3243                } else {
3244                    index(&x["i"])?.to_string()
3245                };
3246            self.names.insert(i, format!("{pre}/{kind}:{part}"));
3247            return Ok(());
3248        }
3249        if let Some(i) = r.get("il") {
3250            if obj.len() != 2 {
3251                return Err(declined(
3252                    DiagnosticCode::MalformedExport,
3253                    "unknown level record fields",
3254                ));
3255            }
3256            let i = index(i)?;
3257            if self.levels.contains_key(&i) {
3258                return Err(declined(
3259                    DiagnosticCode::MalformedExport,
3260                    "duplicate level index",
3261                ));
3262            }
3263            let mut vars = HashSet::new();
3264            let mut depth = 0;
3265            let mut get = |v: &Value| -> Result<Universe> {
3266                let j = index(v)?;
3267                let u = self.level(v, None)?;
3268                vars.extend(self.level_vars[&j].iter().cloned());
3269                depth = depth.max(self.level_depth[&j] + 1);
3270                if depth > 256 {
3271                    return Err(declined(
3272                        DiagnosticCode::ResourceLimit,
3273                        "universe depth budget",
3274                    ));
3275                }
3276                Ok(u)
3277            };
3278            let u = if let Some(v) = r.get("succ") {
3279                get(v)?.succ()
3280            } else if let Some(v) = r.get("param") {
3281                let n = self.name(v)?;
3282                vars.insert(n.clone());
3283                Universe::Var(n)
3284            } else if let Some(v) = r.get("max").or_else(|| r.get("imax")) {
3285                let pair = array(v)?;
3286                if pair.len() != 2 {
3287                    return Err(declined(DiagnosticCode::MalformedExport, "bad level pair"));
3288                }
3289                let a = Box::new(get(&pair[0])?);
3290                let b = Box::new(get(&pair[1])?);
3291                if r.get("max").is_some() {
3292                    Universe::Max(a, b)
3293                } else {
3294                    Universe::IMax(a, b)
3295                }
3296            } else {
3297                return Err(declined(
3298                    DiagnosticCode::MalformedExport,
3299                    "unknown level kind",
3300                ));
3301            };
3302            self.levels.insert(i, u);
3303            self.level_vars.insert(i, vars);
3304            self.level_depth.insert(i, depth);
3305            return Ok(());
3306        }
3307        if let Some(i) = r.get("ie") {
3308            if obj.len() != 2 {
3309                return Err(declined(
3310                    DiagnosticCode::MalformedExport,
3311                    "unknown expression record fields",
3312                ));
3313            }
3314            let i = index(i)?;
3315            if self.exprs.contains_key(&i) {
3316                return Err(declined(
3317                    DiagnosticCode::MalformedExport,
3318                    "duplicate expression index",
3319                ));
3320            }
3321            // Elaborator `have` markers are declined, never dropped (see `metadata`).
3322            metadata::check(&r)?;
3323            let refs: Vec<&Value> = if let Some(x) = r.get("app") {
3324                vec![&x["fn"], &x["arg"]]
3325            } else if let Some(x) = r.get("mdata") {
3326                vec![&x["expr"]]
3327            } else if let Some(x) = r.get("proj") {
3328                vec![&x["struct"]]
3329            } else if let Some(x) = r.get("letE") {
3330                vec![&x["type"], &x["value"], &x["body"]]
3331            } else if let Some(x) = r.get("lam").or_else(|| r.get("forallE")) {
3332                source_binder_kind(x)?;
3333                vec![&x["type"], &x["body"]]
3334            } else if r.get("sort").is_some()
3335                || r.get("bvar").is_some()
3336                || r.get("const").is_some()
3337                || r.get("natVal").is_some()
3338                || r.get("strVal").is_some()
3339            {
3340                vec![]
3341            } else {
3342                // Lean's kernel never sees a metavariable or a free variable: one here is a
3343                // declaration that was never fully elaborated, whatever else the record says.
3344                let code = if r.get("mvar").is_some() || r.get("fvar").is_some() {
3345                    DiagnosticCode::UnresolvedMetavariable
3346                } else {
3347                    DiagnosticCode::UnknownRecordKind
3348                };
3349                return Err(declined(code, "unsupported expression record"));
3350            };
3351            let mut depth = if r.get("strVal").is_some() {
3352                strings::layout(&r)?.1 - 1
3353            } else {
3354                0
3355            };
3356            for v in refs {
3357                let j = index(v)?;
3358                let d = self.expr_depth.get(&j).ok_or_else(|| {
3359                    declined(
3360                        DiagnosticCode::MalformedExport,
3361                        "expression is not a backwards DAG reference",
3362                    )
3363                })?;
3364                depth = depth.max(d + 1);
3365            }
3366            if let Some(data) = r.get("mdata").and_then(|x| x["data"].as_object()) {
3367                for (key, value) in data {
3368                    self.report.metadata_losses.push(MetadataLoss {
3369                        expression: i,
3370                        key: key.clone(),
3371                        value: value.as_str().expect("validated metadata value").into(),
3372                    });
3373                }
3374            }
3375            self.expr_depth.insert(i, depth);
3376            self.exprs.insert(i, r);
3377            return Ok(());
3378        }
3379        if obj.len() != 1 {
3380            return Err(declined(
3381                DiagnosticCode::MalformedExport,
3382                "unknown declaration record fields",
3383            ));
3384        }
3385        let (kind, d) = obj.iter().next().unwrap();
3386        if kind == "quot" {
3387            self.trace_stage("registration", "begin");
3388            self.quotient(d)?;
3389            self.trace_stage("registration", "done");
3390            return Ok(());
3391        }
3392        if kind == "inductive" {
3393            self.trace_stage("registration", "begin");
3394            self.inductive(d)?;
3395            for field in ["types", "ctors", "recs"] {
3396                for declaration in array(&d[field])? {
3397                    self.install_binder_kinds(declaration)?;
3398                }
3399            }
3400            self.trace_stage("registration", "done");
3401            return Ok(());
3402        }
3403        if !["def", "axiom", "thm", "opaque"].contains(&kind.as_str()) {
3404            return Err(declined(
3405                DiagnosticCode::UnknownRecordKind,
3406                format!("unsupported declaration: {kind}"),
3407            ));
3408        }
3409        let mut runtime_safety = match d.get("safety") {
3410            None => None,
3411            Some(Value::String(s)) if s == "safe" => None,
3412            Some(Value::String(s)) if s == "unsafe" => Some("unsafe"),
3413            Some(Value::String(s)) if s == "partial" => Some("partial"),
3414            _ => {
3415                return Err(declined(
3416                    DiagnosticCode::MalformedExport,
3417                    "unknown declaration safety",
3418                ))
3419            }
3420        };
3421        if let Some(flag) = d.get("isUnsafe") {
3422            let flag = flag.as_bool().ok_or_else(|| {
3423                declined(
3424                    DiagnosticCode::MalformedExport,
3425                    "malformed declaration safety",
3426                )
3427            })?;
3428            if flag {
3429                if runtime_safety == Some("partial") {
3430                    return Err(rejected(
3431                        DiagnosticCode::MalformedExport,
3432                        "conflicting declaration safety",
3433                    ));
3434                }
3435                runtime_safety = Some("unsafe");
3436            }
3437        }
3438        if kind == "thm" && runtime_safety.is_some() {
3439            return Err(rejected(
3440                DiagnosticCode::InvalidProof,
3441                "theorem cannot be an unsafe or partial runtime declaration",
3442            ));
3443        }
3444        let name = self.name(&d["name"])?;
3445        if name.starts_with("kernel.quot.") {
3446            return Err(rejected(
3447                DiagnosticCode::InvalidProof,
3448                "quotient identities require a canonical primitive declaration",
3449            ));
3450        }
3451        if matches!(name.as_str(), "Nat" | "Zero" | "Succ") {
3452            return Err(declined(
3453                DiagnosticCode::UnsupportedFeature,
3454                "primitive Nat identities require a canonical inductive block",
3455            ));
3456        }
3457        if self.declarations.contains_key(&name) {
3458            return Err(rejected(
3459                DiagnosticCode::DuplicateDeclaration,
3460                "duplicate declaration",
3461            ));
3462        }
3463        let params = array(&d["levelParams"])?
3464            .iter()
3465            .map(|v| self.name(v))
3466            .collect::<Result<Vec<_>>>()?;
3467        let bound = params.iter().cloned().collect::<HashSet<_>>();
3468        if bound.len() != params.len() {
3469            return Err(rejected(
3470                DiagnosticCode::InvalidProof,
3471                "duplicate universe parameter",
3472            ));
3473        }
3474        if let Some(safety) = runtime_safety {
3475            self.trace_stage("runtime.quarantine", "begin");
3476            let type_expression = index(&d["type"])?;
3477            let value_expression = if kind == "axiom" {
3478                None
3479            } else {
3480                Some(index(&d["value"])?)
3481            };
3482            if !self.exprs.contains_key(&type_expression)
3483                || value_expression.is_some_and(|id| !self.exprs.contains_key(&id))
3484            {
3485                return Err(declined(
3486                    DiagnosticCode::MalformedExport,
3487                    "unknown quarantined runtime expression",
3488                ));
3489            }
3490            // Reserve the source name, but install neither a definition nor an
3491            // axiom in Context. Its header and body have NOT been kernel checked.
3492            self.runtime_names.insert(name.clone(), safety);
3493            self.declarations.insert(name.clone(), params.len());
3494            self.report.runtime_declarations.push(RuntimeDeclaration {
3495                name,
3496                kind: kind.clone(),
3497                safety: safety.into(),
3498                type_expression,
3499                value_expression,
3500            });
3501            self.runtime_target_pending = true;
3502            self.trace_stage("runtime.quarantine", "done");
3503            return Ok(());
3504        }
3505        let roots = if kind == "axiom" {
3506            vec![&d["type"]]
3507        } else {
3508            vec![&d["type"], &d["value"]]
3509        };
3510        self.check_runtime_references(&roots)?;
3511        let mut budget = 100_000;
3512        self.trace_stage("type.parse", "begin");
3513        let ty = self.shared_term(&d["type"], &mut Vec::new(), &bound, &mut budget)?;
3514        self.trace_stage("type.parse", "done");
3515        self.trace_stage("type.infer", "begin");
3516        lean_type(&self.ctx, &ty)?;
3517        let u = sort(&self.ctx, &ty)?;
3518        self.trace_stage("type.infer", "done");
3519        if kind == "thm" && !u.equiv(&Universe::Prop) {
3520            return Err(rejected(
3521                DiagnosticCode::InvalidProof,
3522                "theorem type is not a proposition",
3523            ));
3524        }
3525        let mut audit = None;
3526        if kind == "axiom" {
3527            if self.audit_declarations {
3528                audit = Some(self.declaration_audit(&name, kind, d, &ty, None)?);
3529            }
3530            self.trace_stage("registration", "begin");
3531            self.ctx
3532                .add_universe_poly_declaration(&name, params.clone(), ty)
3533                .map_err(kernel)?;
3534        } else {
3535            self.trace_stage("body.parse", "begin");
3536            let body = self.shared_term(&d["value"], &mut Vec::new(), &bound, &mut budget)?;
3537            self.trace_stage("body.parse", "done");
3538            if kind == "def" && params.is_empty() && !self.audit_declarations {
3539                let sink = self.trace_sink;
3540                let declaration = self.trace_declaration.clone();
3541                let retain = self.retain.covers(&name, kind);
3542                let mut retained = None;
3543                self.ctx
3544                    .declare_with_validation::<Verdict>(&name, ty, body, |ctx, ty, body| {
3545                        emit_stage(sink, declaration.as_ref(), "body.infer", "begin");
3546                        let actual = lean_type(ctx, body)?;
3547                        emit_stage(sink, declaration.as_ref(), "body.infer", "done");
3548                        emit_stage(sink, declaration.as_ref(), "exact", "begin");
3549                        exact(ctx, &actual, ty)?;
3550                        emit_stage(sink, declaration.as_ref(), "exact", "done");
3551                        if retain {
3552                            retained = Some((name.clone(), ty.clone(), body.clone()));
3553                        }
3554                        emit_stage(sink, declaration.as_ref(), "registration", "begin");
3555                        Ok(())
3556                    })?;
3557                self.retained.extend(retained);
3558            } else {
3559                self.trace_stage("body.infer", "begin");
3560                let actual = lean_type(&self.ctx, &body)?;
3561                self.trace_stage("body.infer", "done");
3562                // Lean requires conversion here, not LOGOS's cumulative subtyping.
3563                self.trace_stage("exact", "begin");
3564                exact(&self.ctx, &actual, &ty)?;
3565                self.trace_stage("exact", "done");
3566                if self.audit_declarations {
3567                    audit = Some(self.declaration_audit(&name, kind, d, &ty, Some(&body))?);
3568                }
3569                // Retain an already checked source term only when the caller explicitly
3570                // requests it (a target and its closure). Retention never changes publication.
3571                if self.retain.covers(&name, kind) {
3572                    self.retained.push((name.clone(), ty.clone(), body.clone()));
3573                }
3574                self.trace_stage("registration", "begin");
3575                // Lean's kernel unfolds a theorem (lowest-priority lazy delta); only `opaque`
3576                // is sealed. Both are checked in both kernels before publication.
3577                if kind == "thm" {
3578                    self.ctx
3579                        .declare_theorem_checked(&name, params.clone(), ty, body)
3580                        .map_err(kernel)?;
3581                } else if kind == "opaque" {
3582                    self.ctx
3583                        .declare_opaque_checked(&name, params.clone(), ty, body)
3584                        .map_err(kernel)?;
3585                } else if params.is_empty() {
3586                    self.ctx.declare(&name, ty, body).map_err(kernel)?;
3587                } else {
3588                    self.ctx
3589                        .declare_universe_poly(&name, params.clone(), ty, body)
3590                        .map_err(kernel)?;
3591                }
3592            }
3593            if !self.ctx.pending_obligations().is_empty() {
3594                return Err(declined(
3595                    DiagnosticCode::UnsupportedFeature,
3596                    "unresolved kernel obligation",
3597                ));
3598            }
3599        }
3600        self.install_binder_kinds(d)?;
3601        if let Some(audit) = audit {
3602            self.audited_names
3603                .insert(name.clone(), self.report.declarations.len());
3604            self.report.declarations.push(audit);
3605        }
3606        self.declarations.insert(name, params.len());
3607        // An axiom/header is not a checked safe target. The dependency-ordered
3608        // export must finish its runtime dependencies with a checked body.
3609        if kind != "axiom" {
3610            self.runtime_target_pending = false;
3611        }
3612        self.trace_stage("registration", "done");
3613        Ok(())
3614    }
3615}
3616/// The kernel identity of a structurally encoded Lean name (`/s:"Demo"/s:"andSwap"`).
3617///
3618/// The compact Nat representation has a fixed kernel identity. Only this datatype, its
3619/// constructors, and certified quotient primitives cross this boundary; every literal use checks
3620/// their complete imported signatures. [`intake::lean_name`] is the inverse.
3621pub(crate) fn canonical_name(encoded: &str) -> String {
3622    match encoded {
3623        "/s:\"Quot\"" => crate::quotient::Kind::Type.name().into(),
3624        "/s:\"Quot\"/s:\"mk\"" => crate::quotient::Kind::Constructor.name().into(),
3625        "/s:\"Quot\"/s:\"lift\"" => crate::quotient::Kind::Lift.name().into(),
3626        "/s:\"Quot\"/s:\"ind\"" => crate::quotient::Kind::Induction.name().into(),
3627        "/s:\"Nat\"" => "Nat".into(),
3628        "/s:\"Nat\"/s:\"zero\"" => "Zero".into(),
3629        "/s:\"Nat\"/s:\"succ\"" => "Succ".into(),
3630        _ => match encoded.strip_prefix("/s:\"Nat\"/") {
3631            Some(suffix) => format!("Nat/{suffix}"),
3632            None => encoded.to_string(),
3633        },
3634    }
3635}
3636/// The name index a declaration record declares (an inductive block by its first type).
3637fn declared_name_index(r: &Value) -> Option<usize> {
3638    let object = r.as_object().filter(|o| o.len() == 1)?;
3639    let (kind, source) = object.iter().next()?;
3640    let name = match kind.as_str() {
3641        "def" | "axiom" | "thm" | "opaque" | "quot" => &source["name"],
3642        "inductive" => &source["types"][0]["name"],
3643        _ => return None,
3644    };
3645    name.as_u64().and_then(|n| usize::try_from(n).ok())
3646}
3647fn record_children(r: &Value) -> Vec<&Value> {
3648    if let Some(x) = r.get("app") {
3649        vec![&x["fn"], &x["arg"]]
3650    } else if let Some(x) = r.get("mdata") {
3651        vec![&x["expr"]]
3652    } else if let Some(x) = r.get("proj") {
3653        vec![&x["struct"]]
3654    } else if let Some(x) = r.get("letE") {
3655        vec![&x["type"], &x["value"], &x["body"]]
3656    } else if let Some(x) = r.get("lam").or_else(|| r.get("forallE")) {
3657        vec![&x["type"], &x["body"]]
3658    } else {
3659        Vec::new()
3660    }
3661}
3662struct ImportOptions<'a> {
3663    trace: Option<StageSink<'a>>,
3664    audit_declarations: bool,
3665    retain: RetainSource,
3666    binder_names: bool,
3667    /// Read every `native_decide` leaf as its re-derivation by kernel evaluation ([`native_decide`]).
3668    native_decisions: bool,
3669}
3670/// Which checked declarations an import copies out, type and body, beyond what the context keeps.
3671/// A theorem's or opaque's proof is opaque in the context; only retention keeps it.
3672#[derive(Clone, Debug, Default, PartialEq, Eq)]
3673pub enum RetainSource {
3674    /// Nothing is copied.
3675    #[default]
3676    Nothing,
3677    /// The named declarations (importer keys) that have a body.
3678    Named(HashSet<String>),
3679    /// Every checked theorem and opaque declaration of the export.
3680    EveryProof,
3681}
3682impl RetainSource {
3683    fn covers(&self, name: &str, kind: &str) -> bool {
3684        match self {
3685            RetainSource::Nothing => false,
3686            RetainSource::Named(names) => names.contains(name),
3687            RetainSource::EveryProof => kind == "thm" || kind == "opaque",
3688        }
3689    }
3690}
3691/// How [`import_with`] imports.
3692#[derive(Clone, Debug, PartialEq, Eq)]
3693pub struct ImportRequest {
3694    /// Run the declaration audit (independent recheck of every definition, theorem, opaque and axiom,
3695    /// with its transitive axioms).
3696    pub audit: bool,
3697    pub retain: RetainSource,
3698    /// Carry Lean's binder names into the kernel terms (`arena.local.<depth>.<name>`, read back by
3699    /// [`lean_binder_name`]). Checking is identical either way; the benchmark protocol keeps them off
3700    /// so its refusal messages stay byte-for-byte what they were.
3701    pub lean_binder_names: bool,
3702}
3703impl Default for ImportRequest {
3704    fn default() -> Self {
3705        ImportRequest {
3706            audit: false,
3707            retain: RetainSource::Nothing,
3708            lean_binder_names: true,
3709        }
3710    }
3711}
3712/// The Lean binder name an imported binder carries, when the import kept one.
3713pub fn lean_binder_name(param: &str) -> Option<&str> {
3714    let rest = param.strip_prefix("arena.local.")?;
3715    let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
3716    (digits > 0).then_some(())?;
3717    rest[digits..].strip_prefix('.').filter(|n| !n.is_empty())
3718}
3719fn check_records(
3720    input: &str,
3721    options: ImportOptions<'_>,
3722) -> Result<(ImportedContext, ImportReport)> {
3723    let mut importer = Importer::new(options.audit_declarations);
3724    importer.trace_sink = options.trace;
3725    importer.retain = options.retain;
3726    importer.binder_names = options.binder_names;
3727    importer.lean_version = input
3728        .lines()
3729        .next()
3730        .and_then(|header| serde_json::from_str::<Value>(header).ok())
3731        .and_then(|header| {
3732            header["meta"]["lean"]["version"]
3733                .as_str()
3734                .map(str::to_owned)
3735        })
3736        .unwrap_or_default();
3737    let native = options
3738        .native_decisions
3739        .then(|| native_decide::NativeDecisions::read(input));
3740    for (line, s) in input.lines().skip(1).enumerate() {
3741        let line = line + 2;
3742        let r = serde_json::from_str::<Value>(s).map_err(|e| {
3743            declined(DiagnosticCode::MalformedExport, format!("line {line}: {e}")).at_line(line)
3744        })?;
3745        let r = match &native {
3746            Some(native) => native.rederive(r),
3747            None => r,
3748        };
3749        let declared = declared_name_index(&r);
3750        importer.record(r, line).map_err(|verdict| {
3751            let verdict = match &native {
3752                Some(native) => native.explain(verdict, declared),
3753                None => verdict,
3754            };
3755            let verdict = verdict.located(format!("line {line}")).at_line(line);
3756            match declared.and_then(|i| importer.names.get(&i)) {
3757                Some(name) => verdict.in_declaration(intake::lean_name(&canonical_name(name))),
3758                None => verdict,
3759            }
3760        })?;
3761    }
3762    if importer.runtime_target_pending {
3763        return Err(declined(
3764            DiagnosticCode::UnsupportedFeature,
3765            RUNTIME_UNCHECKED,
3766        ));
3767    }
3768    let depth = importer.expr_depth.values().copied().max().unwrap_or(0);
3769    importer.report.native_decisions = native
3770        .map(|native| native.rederivations())
3771        .unwrap_or_default();
3772    Ok((
3773        ImportedContext {
3774            context: Some(importer.ctx),
3775            depth,
3776            retained: importer.retained,
3777            requested: None,
3778        },
3779        importer.report,
3780    ))
3781}
3782pub fn check_str(input: &str) -> Result<()> {
3783    check_str_with_report(input).map(|_| ())
3784}
3785pub fn check_str_with_report(input: &str) -> Result<ImportReport> {
3786    check_import(input, false).map(|(_, report)| report)
3787}
3788fn check_import(input: &str, audit_declarations: bool) -> Result<(ImportedContext, ImportReport)> {
3789    check_import_with_trace(input, audit_declarations, None)
3790}
3791fn check_import_with_trace(
3792    input: &str,
3793    audit_declarations: bool,
3794    trace: Option<StageSink<'_>>,
3795) -> Result<(ImportedContext, ImportReport)> {
3796    check_import_with_options(
3797        input,
3798        ImportOptions {
3799            audit_declarations,
3800            trace,
3801            retain: RetainSource::Nothing,
3802            binder_names: false,
3803            native_decisions: false,
3804        },
3805    )
3806}
3807fn check_import_with_options(
3808    input: &str,
3809    options: ImportOptions<'_>,
3810) -> Result<(ImportedContext, ImportReport)> {
3811    let mut lines = input.lines();
3812    let header: Value = serde_json::from_str(
3813        lines
3814            .next()
3815            .ok_or_else(|| declined(DiagnosticCode::MalformedExport, "missing header"))?,
3816    )
3817    .map_err(|e| declined(DiagnosticCode::MalformedExport, e.to_string()).at_line(1))?;
3818    if header["meta"]["format"]["version"] != "3.1.0" {
3819        return Err(declined(
3820            DiagnosticCode::UnsupportedExportVersion,
3821            "unsupported NDJSON format version",
3822        )
3823        .at_line(1));
3824    }
3825    // Retain only depth numbers in this preliminary pass. Holding every JSON
3826    // declaration as a Value until checking would duplicate large environments.
3827    // record() still checks all backwards references and declaration contracts.
3828    let mut depths = HashMap::<usize, usize>::new();
3829    let mut depth = 0usize;
3830    for (line, s) in lines.enumerate() {
3831        let r = serde_json::from_str::<Value>(s).map_err(|e| {
3832            declined(
3833                DiagnosticCode::MalformedExport,
3834                format!("line {}: {e}", line + 2),
3835            )
3836            .at_line(line + 2)
3837        })?;
3838        if let Some(id) = r.get("ie").and_then(|v| index(v).ok()) {
3839            let children = record_children(&r);
3840            let d = if r.get("strVal").is_some() {
3841                strings::layout(&r)?.1
3842            } else {
3843                children
3844                    .iter()
3845                    .filter_map(|v| index(v).ok())
3846                    .filter_map(|id| depths.get(&id))
3847                    .copied()
3848                    .max()
3849                    .unwrap_or(0)
3850                    .saturating_add(1)
3851            };
3852            depths.insert(id, d);
3853            depth = depth.max(d);
3854        }
3855    }
3856    drop(depths);
3857    if depth <= crate::term_store::INLINE_DEPTH {
3858        return check_records(input, options);
3859    }
3860    // Do not enter the kernel's sized-call guard here: individual kernel calls
3861    // must still size their own stack from the actual terms they receive.
3862    std::thread::scope(|scope| {
3863        std::thread::Builder::new()
3864            .name("lean-arena-import".into())
3865            .stack_size(crate::term_store::stack_bytes_for_depth(depth))
3866            .spawn_scoped(scope, move || check_records(input, options))
3867            .map_err(|e| {
3868                declined(
3869                    DiagnosticCode::Infrastructure,
3870                    format!("cannot reserve importer stack: {e}"),
3871                )
3872            })?
3873            .join()
3874            .unwrap_or_else(|p| std::panic::resume_unwind(p))
3875    })
3876}
3877#[derive(Debug, Default, PartialEq, Eq)]
3878pub struct ImportReport {
3879    pub internal_helpers: Vec<InternalHelper>,
3880    pub metadata_losses: Vec<MetadataLoss>,
3881    pub declarations: Vec<DeclarationAudit>,
3882    pub declaration_audit_requested: bool,
3883    pub runtime_declarations: Vec<RuntimeDeclaration>,
3884    /// Declarations whose `native_decide` steps were re-derived by kernel evaluation.
3885    pub native_decisions: Vec<native_decide::Rederivation>,
3886}
3887/// Checked conservative definitions introduced to preserve source DAG sharing.
3888#[derive(Debug, PartialEq, Eq)]
3889pub struct InternalHelper {
3890    pub name: String,
3891    pub source_expression: usize,
3892    pub level_parameters: Vec<String>,
3893    pub captured_parameters: usize,
3894}
3895/// Source-only runtime metadata. No corresponding axiom or body enters Context.
3896#[derive(Debug, PartialEq, Eq)]
3897pub struct RuntimeDeclaration {
3898    pub name: String,
3899    pub kind: String,
3900    pub safety: String,
3901    pub type_expression: usize,
3902    pub value_expression: Option<usize>,
3903}
3904#[derive(Debug, PartialEq, Eq)]
3905pub struct DeclarationAudit {
3906    pub name: String,
3907    pub kind: String,
3908    pub type_expression: usize,
3909    pub value_expression: Option<usize>,
3910    pub axioms: Vec<String>,
3911}
3912/// Owned imported artifacts keep the importer's stack discipline when released.
3913pub struct ImportedContext {
3914    context: Option<Context>,
3915    depth: usize,
3916    /// Checked (type, body) copies of the requested declarations, in export order.
3917    retained: Vec<(String, Term, Term)>,
3918    /// The one declaration `import_audited_retaining_source` asked for.
3919    requested: Option<String>,
3920}
3921impl ImportedContext {
3922    /// The checked type and body of the declaration named by `import_audited_retaining_source`.
3923    pub fn retained_source(&self) -> Option<(&str, &Term, &Term)> {
3924        let requested = self.requested.as_deref()?;
3925        self.retained(requested)
3926            .map(|(ty, body)| (requested, ty, body))
3927    }
3928    /// The retained checked type and body of `name` — including an opaque theorem's proof, which
3929    /// the context itself keeps opaque — when the import was asked to retain it.
3930    pub fn retained(&self, name: &str) -> Option<(&Term, &Term)> {
3931        self.retained
3932            .iter()
3933            .find(|(n, ..)| n == name)
3934            .map(|(_, ty, body)| (ty, body))
3935    }
3936    /// Every retained declaration with its checked type and body, in export (dependency) order.
3937    pub fn retained_declarations(&self) -> impl Iterator<Item = (&str, &Term, &Term)> {
3938        self.retained
3939            .iter()
3940            .map(|(name, ty, body)| (name.as_str(), ty, body))
3941    }
3942}
3943impl std::ops::Deref for ImportedContext {
3944    type Target = Context;
3945    fn deref(&self) -> &Context {
3946        self.context.as_ref().expect("live imported context")
3947    }
3948}
3949impl Drop for ImportedContext {
3950    fn drop(&mut self) {
3951        let mut context = self.context.take();
3952        let source = std::mem::take(&mut self.retained);
3953        if self.depth <= crate::term_store::INLINE_DEPTH {
3954            drop(context);
3955            drop(source);
3956            return;
3957        }
3958        // The input DAG already determined the importer's stack reservation.
3959        // Retaining its context must not move deep Box destruction onto an
3960        // arbitrary caller stack when that caller finishes inspecting artifacts.
3961        std::thread::scope(|scope| {
3962            std::thread::Builder::new()
3963                .name("lean-import-drop".into())
3964                .stack_size(crate::term_store::stack_bytes_for_depth(self.depth))
3965                .spawn_scoped(scope, move || {
3966                    drop(context.take());
3967                    drop(source);
3968                })
3969                .expect("reserve imported-context destruction stack")
3970                .join()
3971                .unwrap_or_else(|p| std::panic::resume_unwind(p));
3972        });
3973    }
3974}
3975pub fn import_audited(input: &str) -> Result<(ImportedContext, ImportReport)> {
3976    check_import(input, true)
3977}
3978/// Import and additionally retain the checked type and body of one named declaration, including an
3979/// opaque theorem's proof. Only that declaration is copied; every other import retains nothing extra.
3980pub fn import_audited_retaining_source(
3981    input: &str,
3982    name: &str,
3983) -> Result<(ImportedContext, ImportReport)> {
3984    let imported = check_import_with_options(
3985        input,
3986        ImportOptions {
3987            audit_declarations: true,
3988            trace: None,
3989            retain: RetainSource::Named([name.to_string()].into_iter().collect()),
3990            binder_names: false,
3991            native_decisions: false,
3992        },
3993    )?;
3994    let imported = request(imported, name)?;
3995    if imported.0.retained(name).is_none() {
3996        return Err(declined(
3997            DiagnosticCode::MissingDeclaration,
3998            "requested source declaration has no checked body",
3999        ));
4000    }
4001    Ok(imported)
4002}
4003/// Import with the declaration audit, retaining the checked type and body of every named declaration
4004/// that has one — a target and its dependency closure ([`intake::Intake::closure`]). Nothing outside
4005/// `names` is copied: retention costs the closure, never the whole environment.
4006pub fn import_audited_retaining(
4007    input: &str,
4008    names: &[String],
4009) -> Result<(ImportedContext, ImportReport)> {
4010    import_with(
4011        input,
4012        ImportRequest {
4013            audit: true,
4014            retain: RetainSource::Named(names.iter().cloned().collect()),
4015            lean_binder_names: true,
4016        },
4017    )
4018}
4019/// Import the export and return its checked context: no audit, nothing retained, Lean's binder
4020/// names carried.
4021pub fn import(input: &str) -> Result<(ImportedContext, ImportReport)> {
4022    import_with(input, ImportRequest::default())
4023}
4024/// Import as `request` asks. This is LOGOS's import surface: every `native_decide` step is read as
4025/// its re-derivation by kernel evaluation and listed in the report's `native_decisions`
4026/// ([`native_decide`]); the Arena protocol ([`check_str`]) reads the export exactly as exported.
4027pub fn import_with(input: &str, request: ImportRequest) -> Result<(ImportedContext, ImportReport)> {
4028    check_import_with_options(
4029        input,
4030        ImportOptions {
4031            audit_declarations: request.audit,
4032            trace: None,
4033            retain: request.retain,
4034            binder_names: request.lean_binder_names,
4035            native_decisions: true,
4036        },
4037    )
4038}
4039/// The pipeline's import of `target`: audited, retaining `target`'s dependency closure, with
4040/// `target` as the [`retained_source`](ImportedContext::retained_source).
4041pub fn import_retaining_closure(
4042    input: &str,
4043    target: &str,
4044    closure: &[String],
4045) -> Result<(ImportedContext, ImportReport)> {
4046    request(import_audited_retaining(input, closure)?, target)
4047}
4048/// Why an import that ends on, or is asked for, an unchecked runtime declaration is declined.
4049const RUNTIME_UNCHECKED: &str =
4050    "runtime declarations remain unchecked without a subsequent checked safe target";
4051/// Name `target` as the declaration an import was made for. A runtime (`unsafe`/`partial`)
4052/// declaration is quarantined, never kernel checked, so a request for one is declined however many
4053/// checked declarations the export holds after it.
4054fn request(
4055    mut imported: (ImportedContext, ImportReport),
4056    target: &str,
4057) -> Result<(ImportedContext, ImportReport)> {
4058    if imported
4059        .1
4060        .runtime_declarations
4061        .iter()
4062        .any(|runtime| runtime.name == target)
4063    {
4064        return Err(declined(
4065            DiagnosticCode::UnsupportedFeature,
4066            RUNTIME_UNCHECKED,
4067        ));
4068    }
4069    imported.0.requested = Some(target.to_string());
4070    Ok(imported)
4071}
4072pub fn check_str_with_declaration_audit(input: &str) -> Result<ImportReport> {
4073    check_import(input, true).map(|(_, report)| report)
4074}
4075
4076#[derive(Debug, PartialEq, Eq)]
4077pub struct MetadataLoss {
4078    pub expression: usize,
4079    pub key: String,
4080    pub value: String,
4081}
4082impl ImportReport {
4083    pub fn to_json(&self) -> Value {
4084        serde_json::json!({
4085            "schema": "logos-lean-import-report/1",
4086            "acceptance_scope": "safe_kernel_environment",
4087            "internal_helpers": self.internal_helpers.iter().map(|h|serde_json::json!({
4088                "name":h.name, "source_expression":h.source_expression,
4089                "level_parameters":h.level_parameters, "captured_parameters":h.captured_parameters,
4090                "status":"independently_checked_definition",
4091            })).collect::<Vec<_>>(),
4092            "runtime_declarations": self.runtime_declarations.iter().map(|d| serde_json::json!({
4093                "name": d.name, "kind": d.kind, "safety": d.safety,
4094                "type_expression": d.type_expression, "value_expression": d.value_expression,
4095                "status": "quarantined_unchecked",
4096            })).collect::<Vec<_>>(),
4097            "declaration_audit": {
4098                "requested": self.declaration_audit_requested,
4099                "scope": ["def", "thm", "opaque", "axiom"],
4100                "declarations": self.declarations.iter().map(|d| serde_json::json!({
4101                    "name": d.name, "kind": d.kind, "type_expression": d.type_expression,
4102                    "value_expression": d.value_expression, "axioms": d.axioms,
4103                })).collect::<Vec<_>>(),
4104            },
4105            "losses": self.metadata_losses.iter().map(|loss| serde_json::json!({
4106                "category": "SOURCE_METADATA",
4107                "expression": loss.expression,
4108                "key": loss.key,
4109                "exported_value": loss.value,
4110            })).collect::<Vec<_>>(),
4111            "native_decisions": self.native_decisions.iter()
4112                .map(native_decide::Rederivation::to_json).collect::<Vec<_>>(),
4113        })
4114    }
4115}
4116
4117#[cfg(test)]
4118#[path = "../../../../logicaffeine_tests/unit/logicaffeine_kernel/interface/lean/lean_type_stack_controls.rs"]
4119mod lean_type_stack_controls;
4120
4121#[cfg(test)]
4122#[path = "../../../../logicaffeine_tests/unit/logicaffeine_kernel/interface/lean/exact_decision_controls.rs"]
4123mod exact_decision_controls;
4124#[cfg(test)]
4125#[path = "../../../../logicaffeine_tests/unit/logicaffeine_kernel/interface/lean/function_type_head_controls.rs"]
4126mod function_type_head_controls;
4127#[cfg(test)]
4128#[path = "../../../../logicaffeine_tests/unit/logicaffeine_kernel/interface/lean/independent_verdict_controls.rs"]
4129mod independent_verdict_controls;
4130
4131// Canonical source reconstruction participates in package source binding.
4132mod source_recursor;
4133
4134#[cfg(test)]
4135#[path = "../../../../logicaffeine_tests/unit/logicaffeine_kernel/interface/lean/reference_projection.rs"]
4136mod reference_projection;
4137
4138mod source_recursor_assembly;
4139
4140mod reference_projection_shared;
4141
4142mod runtime_refusal;