Skip to main content

logicaffeine_kernel/
eval.rs

1//! A call-by-value evaluator for the kernel's computational fragment — the engine behind
2//! `native_decide`.
3//!
4//! Unlike [`normalize`](crate::reduction::normalize), which β/ι/δ-reduces by SUBSTITUTION
5//! and normalizes under binders to full normal form, this evaluates a CLOSED term to weak
6//! head normal form using an ENVIRONMENT (no substitution), stopping as soon as the head
7//! constructor is known. So deciding `Eq Nat 10000 10000` is a linear environment walk, not
8//! a quadratic substitution blowup — the point of `native_decide`.
9//!
10//! TRUST: `native_decide` trusts this evaluator to agree with the kernel's reduction (as
11//! Lean's `native_decide` trusts its compiler). It is validated by a differential test
12//! against `normalize`, and fail-safes to `None` (declining) on anything it cannot decide,
13//! but it IS part of the trusted base whenever `native_decide` is used.
14
15use crate::context::Context;
16use crate::term::{int_lit, lit_bigint, Literal, Term};
17use std::cell::RefCell;
18use std::collections::HashMap;
19use std::rc::Rc;
20
21/// A persistent environment mapping bound variables to already-evaluated values.
22enum Scope {
23    Empty,
24    Bind(String, Val, Rc<Scope>),
25}
26
27#[derive(Clone)]
28enum Val {
29    /// A constructor applied to ALL its arguments (parameters + value arguments), evaluated.
30    Ctor(String, Vec<Val>),
31    /// A `λ` closure: (parameter name, body) and the captured environment.
32    Clos(Rc<(String, Term)>, Rc<Scope>),
33    /// A `fix`: (recursive-binder name, body) and the captured environment.
34    Fix(Rc<(String, Term)>, Rc<Scope>),
35    /// A primitive literal (`Lit(Int)` and friends) — the ALU-computed values.
36    Lit(Literal),
37    /// A partially-applied arithmetic/comparison primitive (`add`/`le`/…),
38    /// accumulating its two arguments; computes exactly as the kernel's
39    /// `try_primitive_reduce` does (checked — overflow/div-zero goes Stuck).
40    Prim(&'static str, Vec<Val>),
41    /// No progress possible (free variable, opaque/axiom global, `Sort`/`Π`/`Hole`, a
42    /// stuck application, or fuel exhaustion). A Bool decision that reaches `Stuck` is simply
43    /// undecided, so `native_decide` declines — fail-safe.
44    Stuck,
45}
46
47/// The canonical name of a binary Int primitive, if `n` is one.
48fn prim_name(n: &str) -> Option<&'static str> {
49    match n {
50        "add" => Some("add"),
51        "sub" => Some("sub"),
52        "mul" => Some("mul"),
53        "div" => Some("div"),
54        "mod" => Some("mod"),
55        "le" => Some("le"),
56        "lt" => Some("lt"),
57        "ge" => Some("ge"),
58        "gt" => Some("gt"),
59        _ => None,
60    }
61}
62
63/// The outcome of a binary Int primitive on two literals.
64enum PrimResult {
65    Int(Literal),
66    Bool(bool),
67    Stuck,
68}
69
70/// The pure arithmetic/comparison of a binary Int primitive on two literals — the SAME checked
71/// semantics as the kernel reduction's `try_primitive_reduce` (overflow and division by zero
72/// are `Stuck`, never wrong; a fast i64 path with an exact BigInt fallback for K6). Shared by
73/// the tree-walker [`prim_compute`] and the compiled [`cprim_compute`], so both agree with
74/// `normalize` — including on big numbers.
75fn prim_arith(name: &str, al: &Literal, bl: &Literal) -> PrimResult {
76    if let (Literal::Int(x), Literal::Int(y)) = (al, bl) {
77        let fast = match name {
78            "add" => x.checked_add(*y),
79            "sub" => x.checked_sub(*y),
80            "mul" => x.checked_mul(*y),
81            "div" => x.checked_div(*y),
82            "mod" => x.checked_rem(*y),
83            _ => None,
84        };
85        if let Some(r) = fast {
86            return PrimResult::Int(Literal::Int(r));
87        }
88        match name {
89            "le" => return PrimResult::Bool(x <= y),
90            "lt" => return PrimResult::Bool(x < y),
91            "ge" => return PrimResult::Bool(x >= y),
92            "gt" => return PrimResult::Bool(x > y),
93            _ => {}
94        }
95    }
96    let (Some(xb), Some(yb)) = (lit_bigint(al), lit_bigint(bl)) else {
97        return PrimResult::Stuck;
98    };
99    let big = match name {
100        "add" => Some(xb.add(&yb)),
101        "sub" => Some(xb.sub(&yb)),
102        "mul" => Some(xb.mul(&yb)),
103        "div" => xb.div_rem(&yb).map(|(q, _)| q),
104        "mod" => xb.div_rem(&yb).map(|(_, r)| r),
105        _ => None,
106    };
107    if let Some(r) = big {
108        return PrimResult::Int(int_lit(r));
109    }
110    match name {
111        "le" => PrimResult::Bool(xb <= yb),
112        "lt" => PrimResult::Bool(xb < yb),
113        "ge" => PrimResult::Bool(xb >= yb),
114        "gt" => PrimResult::Bool(xb > yb),
115        _ => PrimResult::Stuck,
116    }
117}
118
119/// Compute a fully-applied Int primitive over tree-walker [`Val`]s.
120fn prim_compute(name: &str, a: &Val, b: &Val) -> Val {
121    let (Val::Lit(al), Val::Lit(bl)) = (a, b) else {
122        return Val::Stuck;
123    };
124    match prim_arith(name, al, bl) {
125        PrimResult::Int(l) => Val::Lit(l),
126        PrimResult::Bool(b) => Val::Ctor(if b { "true" } else { "false" }.to_string(), Vec::new()),
127        PrimResult::Stuck => Val::Stuck,
128    }
129}
130
131const FUEL: u64 = 50_000_000;
132
133/// Evaluate a closed Bool term to `true`/`false`, or `None` if it does not reduce to a Bool
134/// constructor within the fuel budget (fail-safe).
135///
136/// FAST PATH: the decision is first COMPILED to a tree of native closures and run
137/// ([`native_compile_decide`]) — this handles the full computational fragment (recursion via
138/// `Fix`, pattern matching via `Match`, δ-unfolding of definitions), so a recursive Bool
139/// decision runs as closure calls with no `Term`-tree walking. Anything the compiler declines
140/// falls back to the tree-walking interpreter [`eval_bool_tree`]. The two are differential-
141/// tested to give IDENTICAL results, so the trusted `reduceBool` reduction and `native_decide`
142/// both get the compiled speedup with no change in meaning.
143pub fn eval_bool(ctx: &Context, term: &Term) -> Option<bool> {
144    if let Some(b) = native_compile_decide(ctx, term) {
145        return Some(b);
146    }
147    eval_bool_tree(ctx, term)
148}
149
150/// The tree-walking interpreter decision — walks the `Term` each step. The fallback for
151/// [`eval_bool`] and the differential oracle the compiled path is validated against.
152pub fn eval_bool_tree(ctx: &Context, term: &Term) -> Option<bool> {
153    let mut fuel = FUEL;
154    match eval(ctx, &Rc::new(Scope::Empty), term, &mut fuel) {
155        Val::Ctor(c, _) if c == "true" => Some(true),
156        Val::Ctor(c, _) if c == "false" => Some(false),
157        _ => None,
158    }
159}
160
161/// Build a `native_decide` proof of `prop` from a `Decidable prop` instance `inst`: if the
162/// decision procedure NATIVELY evaluates to `true`, return the proof term
163/// `of_decide_eq_true prop inst (ofReduceBool (decide prop inst) true (refl Bool true))`,
164/// which the (main) kernel checks by running this evaluator via the `reduceBool` hook —
165/// never by re-normalizing the decision procedure. Returns `None` (fail-safe) when the
166/// decision is not `true`, so it can only ever produce proofs of genuinely-true goals.
167pub fn native_decide(ctx: &Context, prop: &Term, inst: &Term) -> Option<Term> {
168    let g = |s: &str| Term::Global(s.to_string());
169    let ap = |f: Term, x: Term| Term::App(Box::new(f), Box::new(x));
170    let decide_app = ap(ap(g("decide"), prop.clone()), inst.clone());
171    if eval_bool(ctx, &decide_app) != Some(true) {
172        return None;
173    }
174    let refl_true = ap(ap(g("refl"), g("Bool")), g("true"));
175    let of_reduce = ap(ap(ap(g("ofReduceBool"), decide_app), g("true")), refl_true);
176    Some(ap(ap(ap(g("of_decide_eq_true"), prop.clone()), inst.clone()), of_reduce))
177}
178
179fn lookup(scope: &Rc<Scope>, name: &str) -> Option<Val> {
180    let mut s = scope;
181    loop {
182        match s.as_ref() {
183            Scope::Empty => return None,
184            Scope::Bind(n, v, rest) => {
185                if n == name {
186                    return Some(v.clone());
187                }
188                s = rest;
189            }
190        }
191    }
192}
193
194/// Count the leading `Π`s of a constructor whose domain is a `Sort` — the inductive's
195/// parameters, when it did not declare a split (mirrors `reduction::count_type_params`).
196fn count_type_params(ty: &Term) -> usize {
197    let mut n = 0;
198    let mut cur = ty;
199    while let Term::Pi { param_type, body_type, .. } = cur {
200        if matches!(param_type.as_ref(), Term::Sort(_)) {
201            n += 1;
202            cur = body_type;
203        } else {
204            break;
205        }
206    }
207    n
208}
209
210fn eval(ctx: &Context, scope: &Rc<Scope>, term: &Term, fuel: &mut u64) -> Val {
211    if *fuel == 0 {
212        return Val::Stuck;
213    }
214    *fuel -= 1;
215    match term {
216        Term::Var(n) => lookup(scope, n).unwrap_or(Val::Stuck),
217        Term::Global(n) => {
218            if let Some(body) = ctx.get_definition_body(n) {
219                // δ-reduction: definitions are closed, so evaluate in the empty environment.
220                eval(ctx, &Rc::new(Scope::Empty), body, fuel)
221            } else if ctx.is_constructor(n) {
222                Val::Ctor(n.clone(), Vec::new())
223            } else if let Some(p) = prim_name(n) {
224                Val::Prim(p, Vec::new())
225            } else {
226                Val::Stuck // inductive type, axiom, or other opaque global
227            }
228        }
229        Term::Lambda { param, body, .. } => {
230            Val::Clos(Rc::new((param.clone(), (**body).clone())), scope.clone())
231        }
232        Term::Fix { name, body } => {
233            Val::Fix(Rc::new((name.clone(), (**body).clone())), scope.clone())
234        }
235        Term::App(f, a) => {
236            let fv = eval(ctx, scope, f, fuel);
237            let av = eval(ctx, scope, a, fuel);
238            apply(ctx, fv, av, fuel)
239        }
240        Term::Match { discriminant, cases, .. } => {
241            match eval(ctx, scope, discriminant, fuel) {
242                Val::Ctor(c, args) => {
243                    // ι-reduction: select the case for `c`, applied to its VALUE arguments
244                    // (dropping the inductive's leading parameters).
245                    let ind = match ctx.constructor_inductive(&c) {
246                        Some(i) => i,
247                        None => return Val::Stuck,
248                    };
249                    let ctors = ctx.get_constructors(ind);
250                    let (idx, ctor_ty) = match ctors.iter().position(|(cn, _)| *cn == c) {
251                        Some(i) => (i, ctors[i].1.clone()),
252                        None => return Val::Stuck,
253                    };
254                    if idx >= cases.len() {
255                        return Val::Stuck;
256                    }
257                    let nparams = ctx
258                        .inductive_declared_params(ind)
259                        .unwrap_or_else(|| count_type_params(&ctor_ty));
260                    let value_args: &[Val] = if nparams < args.len() { &args[nparams..] } else { &[] };
261                    let mut sel = eval(ctx, scope, &cases[idx], fuel);
262                    for va in value_args {
263                        sel = apply(ctx, sel, va.clone(), fuel);
264                    }
265                    sel
266                }
267                _ => Val::Stuck,
268            }
269        }
270        Term::Lit(l) => Val::Lit(l.clone()),
271        // Sorts, Πs, holes, universe-poly consts: not part of a Bool computation.
272        _ => Val::Stuck,
273    }
274}
275
276fn apply(ctx: &Context, fv: Val, av: Val, fuel: &mut u64) -> Val {
277    if *fuel == 0 {
278        return Val::Stuck;
279    }
280    match fv {
281        Val::Clos(pb, cenv) => {
282            let ext = Rc::new(Scope::Bind(pb.0.clone(), av, cenv));
283            eval(ctx, &ext, &pb.1, fuel)
284        }
285        Val::Ctor(c, mut args) => {
286            args.push(av);
287            Val::Ctor(c, args)
288        }
289        Val::Prim(name, mut args) => {
290            args.push(av);
291            if args.len() < 2 {
292                Val::Prim(name, args)
293            } else {
294                prim_compute(name, &args[0], &args[1])
295            }
296        }
297        Val::Fix(rb, fenv) => {
298            // Unfold only when the argument is in constructor form — matches the kernel's
299            // guarded `Fix` reduction and guarantees progress on kernel-checked terms.
300            if matches!(av, Val::Ctor(..)) {
301                let ext = Rc::new(Scope::Bind(rb.0.clone(), Val::Fix(rb.clone(), fenv.clone()), fenv));
302                let unfolded = eval(ctx, &ext, &rb.1, fuel);
303                apply(ctx, unfolded, av, fuel)
304            } else {
305                Val::Stuck
306            }
307        }
308        // A literal is not a function.
309        Val::Lit(_) => Val::Stuck,
310        Val::Stuck => Val::Stuck,
311    }
312}
313
314// ---------------------------------------------------------------------------
315// Native closure-compiled decisions (N) — compile a ground Bool decision ONCE
316// into a tree of rustc-compiled native closures and run it, with NO Term-tree
317// walking at run time. The closures ARE machine code (rustc compiled their
318// bodies), so this is genuine "compile the decision to native code and run it",
319// the essence of `native_decide` — sound via the same `ofReduceBool` gate that
320// wraps [`native_decide`]. Reused decisions pay the compile cost once.
321// ---------------------------------------------------------------------------
322
323type IntThunk = Box<dyn Fn() -> i128>;
324type BoolThunk = Box<dyn Fn() -> bool>;
325
326fn as_binop(term: &Term) -> Option<(&str, &Term, &Term)> {
327    if let Term::App(fx, y) = term {
328        if let Term::App(op, x) = fx.as_ref() {
329            if let Term::Global(name) = op.as_ref() {
330                return Some((name.as_str(), x.as_ref(), y.as_ref()));
331            }
332        }
333    }
334    None
335}
336
337/// Compile a ground integer expression (`Int`/`BigInt` literals, `add`/`sub`/`mul`) into a
338/// native closure. `None` if the expression falls outside the compilable fragment.
339fn compile_int(term: &Term) -> Option<IntThunk> {
340    match term {
341        Term::Lit(Literal::Int(n)) => {
342            let n = *n as i128;
343            Some(Box::new(move || n))
344        }
345        Term::Lit(Literal::BigInt(n)) => {
346            let n = n.to_i64().map(|v| v as i128)?;
347            Some(Box::new(move || n))
348        }
349        _ => {
350            let (op, x, y) = as_binop(term)?;
351            let cx = compile_int(x)?;
352            let cy = compile_int(y)?;
353            match op {
354                "add" => Some(Box::new(move || cx() + cy())),
355                "sub" => Some(Box::new(move || cx() - cy())),
356                "mul" => Some(Box::new(move || cx() * cy())),
357                _ => None,
358            }
359        }
360    }
361}
362
363/// Compile a ground Bool decision (`true`/`false`, comparisons of compiled integer
364/// expressions, and `&&`/`||`-shaped `and`/`or`) into a native closure.
365fn compile_bool(term: &Term) -> Option<BoolThunk> {
366    match term {
367        Term::Global(n) if n == "true" => Some(Box::new(|| true)),
368        Term::Global(n) if n == "false" => Some(Box::new(|| false)),
369        _ => {
370            let (op, x, y) = as_binop(term)?;
371            match op {
372                "le" | "lt" | "ge" | "gt" => {
373                    let cx = compile_int(x)?;
374                    let cy = compile_int(y)?;
375                    Some(match op {
376                        "le" => Box::new(move || cx() <= cy()),
377                        "lt" => Box::new(move || cx() < cy()),
378                        "ge" => Box::new(move || cx() >= cy()),
379                        _ => Box::new(move || cx() > cy()),
380                    })
381                }
382                "and" | "or" => {
383                    let cx = compile_bool(x)?;
384                    let cy = compile_bool(y)?;
385                    Some(if op == "and" {
386                        Box::new(move || cx() && cy())
387                    } else {
388                        Box::new(move || cx() || cy())
389                    })
390                }
391                _ => None,
392            }
393        }
394    }
395}
396
397/// Compile a ground Bool decision into native closures and run it — `None` if the term is
398/// outside the compilable fragment (the caller falls back to the [`eval_bool`] interpreter).
399/// Semantics are IDENTICAL to `eval_bool`/`normalize` (differential-tested), so the
400/// `ofReduceBool` certificate `native_decide` builds is equally sound.
401pub fn native_compile_bool(term: &Term) -> Option<bool> {
402    compile_bool(term).map(|c| c())
403}
404
405// ---------------------------------------------------------------------------
406// Native RECURSOR-aware compiled decisions (N) — the general backend. The
407// arithmetic `compile_bool` above handles only ground ×/+/comparisons; this
408// compiles the FULL computational fragment — variables, λ, `fix`, application,
409// `match`, and δ-unfolding of definitions — to a tree of native closures, so a
410// RECURSIVE Bool decision (e.g. `even 1000`, a list predicate) runs as closure
411// calls with NO `Term`-tree walking. Each `Term` shape is dispatched ONCE at
412// compile time; the closures ARE the compiled decision. Semantics are IDENTICAL
413// to the tree-walking `eval` (differential-tested against it and `normalize`),
414// so the `reduceBool` hook that consumes it stays sound.
415// ---------------------------------------------------------------------------
416
417/// A run-time environment for the compiled evaluator (bound variables → values).
418enum CScope {
419    Empty,
420    Bind(Rc<str>, CVal, Rc<CScope>),
421}
422
423/// A compiled run-time value. `Fun`/`Fix` carry COMPILED bodies (closures), so applying them
424/// is a closure call, not a `Term` walk — the essence of the compiled backend.
425#[derive(Clone)]
426enum CVal {
427    /// A constructor applied to all its arguments (parameters + value args).
428    Ctor(Rc<str>, Rc<Vec<CVal>>),
429    /// A compiled `λ`: apply it to an argument to run its compiled body.
430    Fun(Rc<dyn Fn(CVal, &mut u64) -> CVal>),
431    /// A compiled `fix`: its compiled body, recursive-binder name, and captured environment.
432    Fix(Rc<FixClosure>),
433    Lit(Literal),
434    /// A partially-applied Int primitive accumulating its two arguments.
435    Prim(&'static str, Rc<Vec<CVal>>),
436    Stuck,
437}
438
439/// The captured pieces of a compiled `fix`, unfolded on application (in [`capply`]).
440struct FixClosure {
441    body: Code,
442    name: Rc<str>,
443    env: Rc<CScope>,
444}
445
446/// A compiled term: a closure from an environment (and a fuel budget) to a value.
447type Code = Rc<dyn Fn(&Rc<CScope>, &mut u64) -> CVal>;
448
449fn clookup(scope: &Rc<CScope>, name: &str) -> CVal {
450    let mut s = scope;
451    loop {
452        match s.as_ref() {
453            CScope::Empty => return CVal::Stuck,
454            CScope::Bind(n, v, rest) => {
455                if n.as_ref() == name {
456                    return v.clone();
457                }
458                s = rest;
459            }
460        }
461    }
462}
463
464/// Compute a fully-applied Int primitive over compiled [`CVal`]s — same [`prim_arith`] as the
465/// tree-walker, so the compiled path agrees with `eval`/`normalize`.
466fn cprim_compute(name: &str, a: &CVal, b: &CVal) -> CVal {
467    let (CVal::Lit(al), CVal::Lit(bl)) = (a, b) else {
468        return CVal::Stuck;
469    };
470    match prim_arith(name, al, bl) {
471        PrimResult::Int(l) => CVal::Lit(l),
472        PrimResult::Bool(b) => CVal::Ctor(Rc::from(if b { "true" } else { "false" }), Rc::new(Vec::new())),
473        PrimResult::Stuck => CVal::Stuck,
474    }
475}
476
477/// Apply a compiled value to an argument — the run-time counterpart of the tree-walker's
478/// [`apply`], with the SAME rules: a `fix` unfolds only on a constructor argument (matching the
479/// kernel's guarded `Fix` reduction), a partial primitive accumulates then computes. Fuel bounds
480/// the reduction so a non-terminating (never kernel-checked) input fails safe to `Stuck`.
481fn capply(f: CVal, arg: CVal, fuel: &mut u64) -> CVal {
482    if *fuel == 0 {
483        return CVal::Stuck;
484    }
485    *fuel -= 1;
486    match f {
487        CVal::Fun(func) => func(arg, fuel),
488        CVal::Ctor(c, args) => {
489            let mut v = (*args).clone();
490            v.push(arg);
491            CVal::Ctor(c, Rc::new(v))
492        }
493        CVal::Prim(name, args) => {
494            let mut v = (*args).clone();
495            v.push(arg);
496            if v.len() < 2 {
497                CVal::Prim(name, Rc::new(v))
498            } else {
499                cprim_compute(name, &v[0], &v[1])
500            }
501        }
502        CVal::Fix(fx) => {
503            if matches!(arg, CVal::Ctor(..)) {
504                let self_val = CVal::Fix(fx.clone());
505                let ext = Rc::new(CScope::Bind(fx.name.clone(), self_val, fx.env.clone()));
506                let unfolded = (fx.body)(&ext, fuel);
507                capply(unfolded, arg, fuel)
508            } else {
509                CVal::Stuck
510            }
511        }
512        CVal::Lit(_) | CVal::Stuck => CVal::Stuck,
513    }
514}
515
516/// Compiles `Term`s to [`Code`]. Holds the constructor→(index, params) table (so a compiled
517/// `Match` selects a case without touching the `Context` at run time) and a per-definition
518/// SLOT map: a definition's body is compiled once into a shared cell that recursive and mutual
519/// references read at run time — tying the knot without infinite compile-time recursion.
520struct Compiler<'c> {
521    ctx: &'c Context,
522    ctor_map: Rc<HashMap<String, (usize, usize)>>,
523    slots: RefCell<HashMap<String, Rc<RefCell<Option<Code>>>>>,
524}
525
526impl<'c> Compiler<'c> {
527    fn new(ctx: &'c Context) -> Self {
528        // Precompute, for every registered constructor, its index within its inductive and that
529        // inductive's parameter count — the exact `(case index, value-arg offset)` a `Match`
530        // needs, resolved once here instead of per reduction.
531        let mut ctor_map = HashMap::new();
532        let inductives: Vec<String> = ctx.iter_inductives().map(|(n, _)| n.to_string()).collect();
533        for ind in &inductives {
534            let declared = ctx.inductive_declared_params(ind);
535            for (idx, (cname, cty)) in ctx.get_constructors(ind).iter().enumerate() {
536                let nparams = declared.unwrap_or_else(|| count_type_params(cty));
537                ctor_map.entry(cname.to_string()).or_insert((idx, nparams));
538            }
539        }
540        Compiler { ctx, ctor_map: Rc::new(ctor_map), slots: RefCell::new(HashMap::new()) }
541    }
542
543    fn compile(&self, term: &Term) -> Code {
544        match term {
545            Term::Var(n) => {
546                let n: Rc<str> = Rc::from(n.as_str());
547                Rc::new(move |env, _| clookup(env, &n))
548            }
549            Term::Lit(l) => {
550                let l = l.clone();
551                Rc::new(move |_, _| CVal::Lit(l.clone()))
552            }
553            Term::Global(n) => self.compile_global(n),
554            Term::Lambda { param, body, .. } => {
555                let param: Rc<str> = Rc::from(param.as_str());
556                let cbody = self.compile(body);
557                Rc::new(move |env, _| {
558                    let env = env.clone();
559                    let cbody = cbody.clone();
560                    let param = param.clone();
561                    CVal::Fun(Rc::new(move |arg, fuel| {
562                        let ext = Rc::new(CScope::Bind(param.clone(), arg, env.clone()));
563                        cbody(&ext, fuel)
564                    }))
565                })
566            }
567            Term::Fix { name, body } => {
568                let name: Rc<str> = Rc::from(name.as_str());
569                let cbody = self.compile(body);
570                Rc::new(move |env, _| {
571                    CVal::Fix(Rc::new(FixClosure {
572                        body: cbody.clone(),
573                        name: name.clone(),
574                        env: env.clone(),
575                    }))
576                })
577            }
578            Term::App(f, a) => {
579                let cf = self.compile(f);
580                let ca = self.compile(a);
581                Rc::new(move |env, fuel| {
582                    let fv = cf(env, fuel);
583                    let av = ca(env, fuel);
584                    capply(fv, av, fuel)
585                })
586            }
587            Term::Match { discriminant, cases, .. } => {
588                let cd = self.compile(discriminant);
589                let ccases: Vec<Code> = cases.iter().map(|c| self.compile(c)).collect();
590                let ctor_map = self.ctor_map.clone();
591                Rc::new(move |env, fuel| match cd(env, fuel) {
592                    CVal::Ctor(c, args) => {
593                        let (idx, nparams) = match ctor_map.get(c.as_ref()) {
594                            Some(p) => *p,
595                            None => return CVal::Stuck,
596                        };
597                        if idx >= ccases.len() {
598                            return CVal::Stuck;
599                        }
600                        // ι-reduction: the case is run in the CURRENT environment (it may use
601                        // outer variables), then applied to the constructor's VALUE arguments.
602                        let mut sel = ccases[idx](env, fuel);
603                        let value_args: &[CVal] =
604                            if nparams < args.len() { &args[nparams..] } else { &[] };
605                        for va in value_args {
606                            sel = capply(sel, va.clone(), fuel);
607                        }
608                        sel
609                    }
610                    _ => CVal::Stuck,
611                })
612            }
613            // `MutualFix` / `Let` / sorts / `Π` / holes / universe-poly consts are not part of
614            // the compiled Bool fragment (the tree-walker declines them too); a `Stuck` node
615            // makes `eval_bool` fall back to the interpreter, preserving identical results.
616            _ => Rc::new(|_, _| CVal::Stuck),
617        }
618    }
619
620    fn compile_global(&self, n: &str) -> Code {
621        if self.ctx.get_definition_body(n).is_some() {
622            // δ-reduction: compile the (closed) body once into a shared slot; a recursive or
623            // mutual reference reads the same slot at run time, so the knot is tied without
624            // infinite compile-time recursion. The body runs in the EMPTY environment.
625            let slot = self.def_slot(n);
626            Rc::new(move |_env, fuel| match slot.borrow().clone() {
627                Some(code) => code(&Rc::new(CScope::Empty), fuel),
628                None => CVal::Stuck,
629            })
630        } else if self.ctx.is_constructor(n) {
631            let n: Rc<str> = Rc::from(n);
632            Rc::new(move |_, _| CVal::Ctor(n.clone(), Rc::new(Vec::new())))
633        } else if let Some(p) = prim_name(n) {
634            Rc::new(move |_, _| CVal::Prim(p, Rc::new(Vec::new())))
635        } else {
636            // Inductive type, axiom, or other opaque global.
637            Rc::new(|_, _| CVal::Stuck)
638        }
639    }
640
641    /// Get-or-create the shared compiled-body slot for definition `n`, compiling its body once.
642    /// The slot is registered BEFORE its body is compiled, so a self/mutual reference during
643    /// compilation resolves to the same (initially empty) cell and reads it at run time.
644    fn def_slot(&self, n: &str) -> Rc<RefCell<Option<Code>>> {
645        if let Some(s) = self.slots.borrow().get(n) {
646            return s.clone();
647        }
648        let s = Rc::new(RefCell::new(None));
649        self.slots.borrow_mut().insert(n.to_string(), s.clone());
650        let body = self.ctx.get_definition_body(n).expect("def_slot on a definition").clone();
651        let cbody = self.compile(&body);
652        *s.borrow_mut() = Some(cbody);
653        s
654    }
655}
656
657/// Compile a closed Bool decision to native closures and run it — `None` if it does not reduce
658/// to a Bool constructor (fail-safe; the caller falls back to the tree-walking interpreter).
659/// Unlike [`native_compile_bool`], this handles the FULL computational fragment (recursion,
660/// pattern matching, definitions), so recursor-based decisions compile to native code. Its
661/// verdict is IDENTICAL to [`eval_bool_tree`]/`normalize` (differential-tested).
662pub fn native_compile_decide(ctx: &Context, term: &Term) -> Option<bool> {
663    let compiler = Compiler::new(ctx);
664    let code = compiler.compile(term);
665    let mut fuel = FUEL;
666    match code(&Rc::new(CScope::Empty), &mut fuel) {
667        CVal::Ctor(c, _) if c.as_ref() == "true" => Some(true),
668        CVal::Ctor(c, _) if c.as_ref() == "false" => Some(false),
669        _ => None,
670    }
671}