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}