Skip to main content

logicaffeine_kernel/interface/lean/
native_decide.rs

1//! `native_decide`, re-proved by the LOGOS kernel's own evaluation.
2//!
3//! # Lean's proof
4//!
5//! Lean 4.14.0 closes a closed decidable goal `p` with `native_decide` in two declarations
6//! (`Lean.Elab.Tactic.evalNativeDecide`):
7//!
8//! ```text
9//! def     T._nativeDecide_1 : Bool := Decidable.decide p inst
10//! theorem T : p := of_decide_eq_true p inst
11//!                     (Lean.ofReduceBool T._nativeDecide_1 Bool.true (Eq.refl.{1} Bool Bool.true))
12//! ```
13//!
14//! `Lean.ofReduceBool d b h : d = b` is an axiom, and Lean's kernel accepts `Eq.refl b` as
15//! `h : Lean.reduceBool d = b` only by running the compiled `d`: the proof rests on the compiler,
16//! and `#print axioms` names `Lean.ofReduceBool`.
17//!
18//! # The re-derivation
19//!
20//! The step Lean hands to its compiler is a closed decision, and the LOGOS kernel decides it
21//! itself. The leaf is read as
22//!
23//! ```text
24//! of_decide_eq_true p inst (Eq.refl.{1} Bool Bool.true)
25//! ```
26//!
27//! which checks exactly when the kernel converts `Bool.true` with `Decidable.decide p inst`: its own
28//! reduction and certified accelerations, in the primary kernel and in the independent re-checker.
29//! The statement is untouched; the justification of one step changes from Lean's compiled
30//! evaluation to LOGOS's kernel evaluation, and the result discloses it. A decision the kernel
31//! evaluates to `Bool.false` is a conversion mismatch, and the claim is refused; one it cannot
32//! finish within its budget is a resource limit. Neither is ever trusted.
33//!
34//! # What is a leaf
35//!
36//! Structure, never a name alone. An export expression is a leaf when it is
37//! `of_decide_eq_true p inst (Lean.ofReduceBool d Bool.true (Eq.refl.{1} Bool Bool.true))` and the
38//! export declares
39//!
40//! * `Lean.ofReduceBool` as an axiom stating `∀ a b : Bool, Lean.reduceBool a = b → a = b`, over an
41//!   opaque `Lean.reduceBool : Bool → Bool`;
42//! * `of_decide_eq_true` as a theorem stating `∀ (p : Prop) (inst : Decidable p),
43//!   Decidable.decide p inst = true → p`, with `Decidable.decide : ∀ p : Prop, Decidable p → Bool`;
44//! * `d` as a definition without universe parameters, of type `Bool`, whose value is
45//!   `Decidable.decide p inst` for the same `p` and `inst`, expression for expression (binder names
46//!   and binder annotations aside);
47//! * `Bool` as an inductive whose constructors are `Bool.false` and `Bool.true`, and `Eq` as an
48//!   inductive whose one constructor is `Eq.refl`.
49//!
50//! Every other mention of `Lean.ofReduceBool` is read as exported and rests on the axiom; the
51//! assumption policy refuses it.
52//!
53//! # Where it applies
54//!
55//! The LOGOS import surface ([`import_with`](super::import_with), hence the pipeline and the
56//! package) reads every leaf of an export this way and lists each re-derivation in its report. The
57//! Lean Kernel Arena protocol ([`check_str`](super::check_str), [`run_cli`](super::run_cli)) reads the
58//! export exactly as exported. A package discloses the re-derived leaves of its target's closure as
59//! [`INFORMATION_CATEGORY`] information changes, and a replay recomputes that disclosure from the
60//! carried export and refuses a package whose manifest does not state it exactly.
61
62use super::intake::{self, Component};
63use super::{DiagnosticCode, Verdict};
64use serde_json::{json, Value};
65use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
66
67/// The trust escape a re-derived leaf no longer rests on, as Lean's `#print axioms` names it.
68pub const ELIMINATED_AXIOM: &str = "Lean.ofReduceBool";
69
70/// The information-change category a re-derivation is disclosed under.
71pub const INFORMATION_CATEGORY: &str = "NATIVE_DECIDE_REDERIVED";
72
73/// How a disclosed step was re-derived.
74pub const METHOD: &str = "kernel_evaluation";
75
76const OF_DECIDE_EQ_TRUE: &[&str] = &["of_decide_eq_true"];
77const OF_REDUCE_BOOL: &[&str] = &["Lean", "ofReduceBool"];
78const REDUCE_BOOL: &[&str] = &["Lean", "reduceBool"];
79const DECIDE: &[&str] = &["Decidable", "decide"];
80const DECIDABLE: &[&str] = &["Decidable"];
81const EQ: &[&str] = &["Eq"];
82const EQ_REFL: &[&str] = &["Eq", "refl"];
83const BOOL: &[&str] = &["Bool"];
84const BOOL_FALSE: &[&str] = &["Bool", "false"];
85const BOOL_TRUE: &[&str] = &["Bool", "true"];
86
87/// One recognized leaf: Lean's appeal to its compiler, and the pieces its re-derivation keeps.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct NativeDecision {
90    /// The leaf, `of_decide_eq_true p inst (Lean.ofReduceBool d Bool.true h)`.
91    pub leaf: usize,
92    /// `of_decide_eq_true p inst`, which the re-derived leaf keeps.
93    pub premise: usize,
94    /// Lean's appeal, `Lean.ofReduceBool d Bool.true h`, which the re-derived leaf drops.
95    pub appeal: usize,
96    /// `h = Eq.refl.{1} Bool Bool.true`, the proof of `Decidable.decide p inst = Bool.true` the
97    /// re-derived leaf supplies directly.
98    pub evaluation: usize,
99    /// The proposition `p`.
100    pub proposition: usize,
101    /// Its decision procedure `inst : Decidable p`.
102    pub instance: usize,
103    /// The constant Lean compiled and evaluated, as Lean writes its name.
104    pub decision: String,
105}
106
107impl NativeDecision {
108    pub fn to_json(&self) -> Value {
109        json!({
110            "leaf_expression": self.leaf,
111            "appeal_expression": self.appeal,
112            "evaluation_expression": self.evaluation,
113            "proposition_expression": self.proposition,
114            "instance_expression": self.instance,
115            "decision": self.decision,
116        })
117    }
118}
119
120/// One declaration whose type or value holds re-derived leaves.
121#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct Rederivation {
123    /// The kernel identity of the declaration.
124    pub declaration: String,
125    /// Its Lean name, as Lean writes it.
126    pub lean: String,
127    /// Its leaves, in export order.
128    pub decisions: Vec<NativeDecision>,
129}
130
131impl Rederivation {
132    /// The disclosure, stated plainly: what Lean's proof rested on and what LOGOS did instead.
133    pub fn detail(&self) -> String {
134        let decisions: Vec<&str> = self.decisions.iter().map(|d| d.decision.as_str()).collect();
135        let steps = if decisions.len() == 1 {
136            "one step".to_string()
137        } else {
138            format!("{} steps", decisions.len())
139        };
140        format!(
141            "{}: Lean proved {steps} by native_decide, resting on {ELIMINATED_AXIOM} (Lean's compiled \
142             evaluation of {}). LOGOS re-derived each step by kernel evaluation: Decidable.decide reduces \
143             to Bool.true in the primary kernel and in the independent re-checker. The proof does not rest \
144             on {ELIMINATED_AXIOM}; the statement is unchanged.",
145            self.lean,
146            decisions.join(", "),
147        )
148    }
149
150    pub fn to_json(&self) -> Value {
151        json!({
152            "declaration": self.declaration,
153            "lean": self.lean,
154            "eliminated": ELIMINATED_AXIOM,
155            "method": METHOD,
156            "decisions": self.decisions.iter().map(NativeDecision::to_json).collect::<Vec<_>>(),
157        })
158    }
159}
160
161/// What a target's closure rests on once its leaves are re-derived.
162#[derive(Clone, Debug, Default, PartialEq, Eq)]
163pub struct Elimination {
164    /// The declarations of the closure that hold re-derived leaves, in export order.
165    pub rederivations: Vec<Rederivation>,
166    /// Whether the re-derived closure still mentions `Lean.ofReduceBool` outside every leaf.
167    pub escape_remains: bool,
168}
169
170impl Elimination {
171    /// Whether the re-derived proof no longer rests on [`ELIMINATED_AXIOM`] although Lean's did.
172    pub fn eliminates(&self) -> bool {
173        !self.rederivations.is_empty() && !self.escape_remains
174    }
175
176    /// Lean's `#print axioms` (Lean names) with the eliminated escape removed: the axioms the
177    /// re-derived proof must rest on. Lean's report must name the escape it says was eliminated.
178    pub fn lean_after_rederivation(
179        &self,
180        lean: &[String],
181    ) -> std::result::Result<Vec<String>, String> {
182        if !self.eliminates() {
183            return Ok(lean.to_vec());
184        }
185        if !lean.iter().any(|a| a == ELIMINATED_AXIOM) {
186            return Err(format!(
187                "the export's proof rests on {ELIMINATED_AXIOM} through native decisions ({}), but Lean's #print \
188                 axioms reports {lean:?}",
189                self.leaves().join(", ")
190            ));
191        }
192        Ok(lean
193            .iter()
194            .filter(|a| *a != ELIMINATED_AXIOM)
195            .cloned()
196            .collect())
197    }
198
199    /// The information changes a package states, one per declaration holding leaves.
200    pub fn information_changes(&self) -> Vec<(String, String)> {
201        if !self.eliminates() {
202            return Vec::new();
203        }
204        self.rederivations
205            .iter()
206            .map(|r| (INFORMATION_CATEGORY.to_string(), r.detail()))
207            .collect()
208    }
209
210    /// Whether `stated` (category, detail pairs) carries exactly this disclosure: every change it
211    /// must, and no change of the category it must not.
212    pub fn disclosed_by<'a>(
213        &self,
214        stated: impl IntoIterator<Item = (&'a str, &'a str)>,
215    ) -> std::result::Result<(), String> {
216        let stated: Vec<(String, String)> = stated
217            .into_iter()
218            .filter(|(category, _)| *category == INFORMATION_CATEGORY)
219            .map(|(category, detail)| (category.to_string(), detail.to_string()))
220            .collect();
221        let expected = self.information_changes();
222        if stated == expected {
223            return Ok(());
224        }
225        Err(format!(
226            "the manifest states {} {INFORMATION_CATEGORY} change(s), but the export's proof re-derives {}",
227            stated.len(),
228            if expected.is_empty() { "none".to_string() } else { self.leaves().join(", ") }
229        ))
230    }
231
232    fn leaves(&self) -> Vec<String> {
233        self.rederivations
234            .iter()
235            .flat_map(|r| r.decisions.iter().map(|d| d.decision.clone()))
236            .collect()
237    }
238}
239
240/// The native decisions of one export.
241pub struct NativeDecisions {
242    tables: Option<Tables>,
243    decisions: BTreeMap<usize, NativeDecision>,
244    /// Expressions whose subterm, as LOGOS reads it, holds a leaf.
245    holds_leaf: HashSet<usize>,
246}
247
248impl NativeDecisions {
249    /// Recognize every leaf of `export`. An export that never names `Lean.ofReduceBool` costs one
250    /// substring scan. Nothing is refused here: an export the tables cannot read has no leaves, and
251    /// the importer reports what is wrong with it.
252    pub fn read(export: &str) -> NativeDecisions {
253        let none = NativeDecisions {
254            tables: None,
255            decisions: BTreeMap::new(),
256            holds_leaf: HashSet::new(),
257        };
258        if !export.contains("\"ofReduceBool\"") {
259            return none;
260        }
261        let Some(tables) = Tables::read(export) else {
262            return none;
263        };
264        let decisions: BTreeMap<usize, NativeDecision> = if tables.canonical_environment() {
265            tables
266                .order
267                .iter()
268                .filter_map(|&e| tables.leaf(e).map(|d| (e, d)))
269                .collect()
270        } else {
271            BTreeMap::new()
272        };
273        let mut holds_leaf = HashSet::new();
274        for &e in &tables.order {
275            if decisions.contains_key(&e)
276                || tables
277                    .children(e, &decisions)
278                    .iter()
279                    .any(|c| holds_leaf.contains(c))
280            {
281                holds_leaf.insert(e);
282            }
283        }
284        NativeDecisions {
285            tables: Some(tables),
286            decisions,
287            holds_leaf,
288        }
289    }
290
291    /// Whether the export has no leaf.
292    pub fn is_empty(&self) -> bool {
293        self.decisions.is_empty()
294    }
295
296    /// Every leaf, in expression order.
297    pub fn decisions(&self) -> impl Iterator<Item = &NativeDecision> {
298        self.decisions.values()
299    }
300
301    /// The expression record LOGOS reads in place of `record`: a leaf's premise applied to Lean's
302    /// evaluation step itself, `of_decide_eq_true p inst (Eq.refl.{1} Bool Bool.true)`. Every other
303    /// record is returned as it is.
304    pub fn rederive(&self, record: Value) -> Value {
305        if self.decisions.is_empty() {
306            return record;
307        }
308        let Some(decision) = record
309            .get("ie")
310            .and_then(Value::as_u64)
311            .and_then(|e| self.decisions.get(&(e as usize)))
312        else {
313            return record;
314        };
315        json!({ "ie": decision.leaf, "app": { "fn": decision.premise, "arg": decision.evaluation } })
316    }
317
318    /// Every declaration whose type or value holds a leaf, in export order.
319    pub fn rederivations(&self) -> Vec<Rederivation> {
320        let Some(tables) = &self.tables else {
321            return Vec::new();
322        };
323        tables
324            .declared
325            .iter()
326            .filter_map(|&name| self.rederivation(tables, name))
327            .collect()
328    }
329
330    /// The closure of `target` (a kernel name) as Lean's `#print axioms` walks it — a definition,
331    /// theorem or opaque through its type and value, an inductive through its type and constructors,
332    /// a constructor or recursor through its type, an axiom not at all — with every leaf read as
333    /// its re-derivation.
334    pub fn closure_of(&self, target: &str) -> Elimination {
335        let Some(tables) = &self.tables else {
336            return Elimination::default();
337        };
338        let Some(&start) = tables.by_kernel_name.get(target) else {
339            return Elimination::default();
340        };
341        let mut elimination = Elimination::default();
342        let mut constants = vec![start];
343        let mut constants_seen = HashSet::new();
344        let mut expressions_seen = HashSet::new();
345        let mut rederived = BTreeSet::new();
346        while let Some(name) = constants.pop() {
347            if !constants_seen.insert(name) {
348                continue;
349            }
350            let Some(constant) = tables.constants.get(&name) else {
351                continue;
352            };
353            let roots: Vec<usize> = match &constant.kind {
354                Kind::Axiom | Kind::Quotient => Vec::new(),
355                Kind::Definition | Kind::Theorem | Kind::Opaque => {
356                    if self.rederivation(tables, name).is_some() {
357                        rederived.insert(name);
358                    }
359                    std::iter::once(constant.ty).chain(constant.value).collect()
360                }
361                Kind::Inductive { constructors } => {
362                    constants.extend(constructors.iter().copied());
363                    vec![constant.ty]
364                }
365                Kind::Constructor { .. } | Kind::Recursor => vec![constant.ty],
366            };
367            let mut work = roots;
368            while let Some(e) = work.pop() {
369                if !expressions_seen.insert(e) {
370                    continue;
371                }
372                if let Some((named, _)) = tables.constant(e) {
373                    if tables.name_is(named, OF_REDUCE_BOOL) {
374                        elimination.escape_remains = true;
375                    }
376                    constants.push(named);
377                }
378                work.extend(tables.children(e, &self.decisions));
379            }
380        }
381        elimination.rederivations = tables
382            .declared
383            .iter()
384            .filter(|name| rederived.contains(name))
385            .filter_map(|&name| self.rederivation(tables, name))
386            .collect();
387        elimination
388    }
389
390    /// `verdict`, saying which of the failing declaration's steps were re-derived: a native decision
391    /// the kernel refutes or cannot finish is named, never left to look like an unrelated mismatch.
392    pub fn explain(&self, verdict: Verdict, declaration: Option<usize>) -> Verdict {
393        let Some(tables) = &self.tables else {
394            return verdict;
395        };
396        let Some(rederivation) = declaration.and_then(|name| self.rederivation(tables, name))
397        else {
398            return verdict;
399        };
400        if !matches!(
401            verdict.reason().code(),
402            DiagnosticCode::InvalidProof | DiagnosticCode::ResourceLimit
403        ) {
404            return verdict;
405        }
406        let decisions: Vec<&str> = rederivation
407            .decisions
408            .iter()
409            .map(|d| d.decision.as_str())
410            .collect();
411        verdict.located(format!(
412            "{} re-derives its native decision(s) {} by kernel evaluation instead of {ELIMINATED_AXIOM}, and \
413             the kernel must reduce each Decidable.decide to Bool.true",
414            rederivation.lean,
415            decisions.join(", ")
416        ))
417    }
418
419    fn rederivation(&self, tables: &Tables, name: usize) -> Option<Rederivation> {
420        let constant = tables.constants.get(&name)?;
421        if !matches!(
422            constant.kind,
423            Kind::Definition | Kind::Theorem | Kind::Opaque
424        ) {
425            return None;
426        }
427        let mut leaves = BTreeSet::new();
428        let mut work: Vec<usize> = std::iter::once(constant.ty).chain(constant.value).collect();
429        let mut seen = HashSet::new();
430        while let Some(e) = work.pop() {
431            if !self.holds_leaf.contains(&e) || !seen.insert(e) {
432                continue;
433            }
434            if self.decisions.contains_key(&e) {
435                leaves.insert(e);
436            }
437            work.extend(tables.children(e, &self.decisions));
438        }
439        if leaves.is_empty() {
440            return None;
441        }
442        Some(Rederivation {
443            declaration: tables.kernel_name(name),
444            lean: intake::print_lean_name(&tables.names[&name]),
445            decisions: leaves
446                .into_iter()
447                .map(|e| self.decisions[&e].clone())
448                .collect(),
449        })
450    }
451}
452
453/// A universe level, as far as the recognizer compares levels.
454#[derive(Clone, Debug, PartialEq, Eq)]
455enum Level {
456    Zero,
457    Succ(Box<Level>),
458    Max(Box<Level>, Box<Level>),
459    IMax(Box<Level>, Box<Level>),
460    Param(Vec<Component>),
461}
462
463fn one() -> Level {
464    Level::Succ(Box::new(Level::Zero))
465}
466
467#[derive(Clone, Debug, PartialEq, Eq)]
468enum Kind {
469    Definition,
470    Theorem,
471    Opaque,
472    Axiom,
473    Quotient,
474    Inductive { constructors: Vec<usize> },
475    Constructor { inductive: usize },
476    Recursor,
477}
478
479/// A declared constant: its kind, universe parameter count, type and value.
480struct Constant {
481    kind: Kind,
482    level_params: usize,
483    ty: usize,
484    value: Option<usize>,
485}
486
487/// A structural pattern over export expressions, binder names and annotations aside.
488enum Shape {
489    Prop,
490    Constant(&'static [&'static str], Vec<Level>),
491    Bound(u64),
492    Apply(Box<Shape>, Box<Shape>),
493    Forall(Box<Shape>, Box<Shape>),
494}
495
496fn c(name: &'static [&'static str]) -> Shape {
497    Shape::Constant(name, Vec::new())
498}
499fn at(name: &'static [&'static str], levels: Vec<Level>) -> Shape {
500    Shape::Constant(name, levels)
501}
502fn b(index: u64) -> Shape {
503    Shape::Bound(index)
504}
505fn ap(f: Shape, args: impl IntoIterator<Item = Shape>) -> Shape {
506    args.into_iter()
507        .fold(f, |f, a| Shape::Apply(Box::new(f), Box::new(a)))
508}
509fn pi(domain: Shape, body: Shape) -> Shape {
510    Shape::Forall(Box::new(domain), Box::new(body))
511}
512fn eq_bool(lhs: Shape, rhs: Shape) -> Shape {
513    ap(at(EQ, vec![one()]), [c(BOOL), lhs, rhs])
514}
515
516/// The export, read once: names, levels, expressions and declared constants.
517struct Tables {
518    names: HashMap<usize, Vec<Component>>,
519    levels: HashMap<usize, Level>,
520    exprs: HashMap<usize, Value>,
521    /// Expression indices in export order: every child before its parents.
522    order: Vec<usize>,
523    constants: HashMap<usize, Constant>,
524    /// Declared name indices in export order.
525    declared: Vec<usize>,
526    by_kernel_name: HashMap<String, usize>,
527}
528
529impl Tables {
530    fn read(export: &str) -> Option<Tables> {
531        let mut tables = Tables {
532            names: HashMap::from([(0, Vec::new())]),
533            levels: HashMap::from([(0, Level::Zero)]),
534            exprs: HashMap::new(),
535            order: Vec::new(),
536            constants: HashMap::new(),
537            declared: Vec::new(),
538            by_kernel_name: HashMap::new(),
539        };
540        for line in export.lines().skip(1) {
541            let record: Value = serde_json::from_str(line).ok()?;
542            if let Some(i) = record.get("in") {
543                let i = index(i)?;
544                let (part, pre) = if let Some(x) = record.get("str") {
545                    (
546                        Component::Str(x["str"].as_str()?.to_string()),
547                        index(&x["pre"])?,
548                    )
549                } else {
550                    let x = record.get("num")?;
551                    (Component::Num(x["i"].as_u64()?), index(&x["pre"])?)
552                };
553                let mut name = tables.names.get(&pre)?.clone();
554                name.push(part);
555                tables.names.insert(i, name);
556            } else if let Some(i) = record.get("il") {
557                let i = index(i)?;
558                let level = if let Some(x) = record.get("succ") {
559                    Level::Succ(Box::new(tables.levels.get(&index(x)?)?.clone()))
560                } else if let Some(x) = record.get("param") {
561                    Level::Param(tables.names.get(&index(x)?)?.clone())
562                } else if let Some(pair) = record.get("max").or_else(|| record.get("imax")) {
563                    let pair = pair.as_array()?;
564                    let lhs = Box::new(tables.levels.get(&index(pair.first()?)?)?.clone());
565                    let rhs = Box::new(tables.levels.get(&index(pair.get(1)?)?)?.clone());
566                    if record.get("max").is_some() {
567                        Level::Max(lhs, rhs)
568                    } else {
569                        Level::IMax(lhs, rhs)
570                    }
571                } else {
572                    return None;
573                };
574                tables.levels.insert(i, level);
575            } else if let Some(i) = record.get("ie") {
576                let i = index(i)?;
577                tables.order.push(i);
578                tables.exprs.insert(i, record);
579            } else {
580                tables.declaration(&record)?;
581            }
582        }
583        Some(tables)
584    }
585
586    fn declaration(&mut self, record: &Value) -> Option<()> {
587        let (tag, body) = record.as_object()?.iter().next()?;
588        let simple = |kind: Kind, body: &Value| -> Option<(usize, Constant)> {
589            let name = index(&body["name"])?;
590            let value = if matches!(kind, Kind::Axiom | Kind::Quotient) {
591                None
592            } else {
593                Some(index(&body["value"])?)
594            };
595            let level_params = body["levelParams"].as_array()?.len();
596            Some((
597                name,
598                Constant {
599                    kind,
600                    level_params,
601                    ty: index(&body["type"])?,
602                    value,
603                },
604            ))
605        };
606        let mut declared = Vec::new();
607        match tag.as_str() {
608            "def" => declared.push(simple(Kind::Definition, body)?),
609            "thm" => declared.push(simple(Kind::Theorem, body)?),
610            "opaque" => declared.push(simple(Kind::Opaque, body)?),
611            "axiom" => declared.push(simple(Kind::Axiom, body)?),
612            "quot" => declared.push(simple(Kind::Quotient, body)?),
613            "inductive" => {
614                for ty in body["types"].as_array()? {
615                    let constructors = ty["ctors"]
616                        .as_array()?
617                        .iter()
618                        .map(index)
619                        .collect::<Option<Vec<_>>>()?;
620                    declared.push(simple_typed(Kind::Inductive { constructors }, ty)?);
621                }
622                for ctor in body["ctors"].as_array()? {
623                    declared.push(simple_typed(
624                        Kind::Constructor {
625                            inductive: index(&ctor["induct"])?,
626                        },
627                        ctor,
628                    )?);
629                }
630                for rec in body["recs"].as_array()? {
631                    declared.push(simple_typed(Kind::Recursor, rec)?);
632                }
633            }
634            _ => return None,
635        }
636        for (name, constant) in declared {
637            let kernel = self.kernel_name(name);
638            self.by_kernel_name.insert(kernel, name);
639            self.declared.push(name);
640            self.constants.insert(name, constant);
641        }
642        Some(())
643    }
644
645    fn kernel_name(&self, name: usize) -> String {
646        super::canonical_name(&intake::encode(&self.names[&name]))
647    }
648
649    fn name_is(&self, name: usize, expected: &[&str]) -> bool {
650        self.names.get(&name).is_some_and(|components| {
651            components.len() == expected.len()
652                && components
653                    .iter()
654                    .zip(expected)
655                    .all(|(c, e)| matches!(c, Component::Str(s) if s == e))
656        })
657    }
658
659    fn named(&self, expected: &[&str]) -> Option<(usize, &Constant)> {
660        self.declared
661            .iter()
662            .filter(|&&name| self.name_is(name, expected))
663            .map(|&name| (name, &self.constants[&name]))
664            .next()
665    }
666
667    fn app(&self, e: usize) -> Option<(usize, usize)> {
668        let x = self.exprs.get(&e)?.get("app")?;
669        Some((index(&x["fn"])?, index(&x["arg"])?))
670    }
671
672    fn constant(&self, e: usize) -> Option<(usize, Vec<Level>)> {
673        let x = self.exprs.get(&e)?.get("const")?;
674        let levels = x["us"]
675            .as_array()?
676            .iter()
677            .map(|u| self.levels.get(&index(u)?).cloned())
678            .collect::<Option<Vec<_>>>()?;
679        Some((index(&x["name"])?, levels))
680    }
681
682    fn is_constant(&self, e: usize, name: &[&str], levels: &[Level]) -> bool {
683        self.constant(e)
684            .is_some_and(|(n, us)| self.name_is(n, name) && us == levels)
685    }
686
687    /// The children of `e` as LOGOS reads it: a leaf's are its premise and its evaluation step.
688    fn children(&self, e: usize, leaves: &BTreeMap<usize, NativeDecision>) -> Vec<usize> {
689        if let Some(leaf) = leaves.get(&e) {
690            return vec![leaf.premise, leaf.evaluation];
691        }
692        let Some(record) = self.exprs.get(&e) else {
693            return Vec::new();
694        };
695        let fields: &[(&str, &[&str])] = &[
696            ("app", &["fn", "arg"]),
697            ("mdata", &["expr"]),
698            ("proj", &["struct"]),
699            ("letE", &["type", "value", "body"]),
700            ("lam", &["type", "body"]),
701            ("forallE", &["type", "body"]),
702        ];
703        for (tag, children) in fields {
704            if let Some(x) = record.get(*tag) {
705                return children.iter().filter_map(|c| index(&x[*c])).collect();
706            }
707        }
708        Vec::new()
709    }
710
711    /// Whether `e` has exactly the structure `shape`.
712    fn fits(&self, e: usize, shape: &Shape) -> bool {
713        let Some(record) = self.exprs.get(&e) else {
714            return false;
715        };
716        match shape {
717            Shape::Prop => record
718                .get("sort")
719                .and_then(index)
720                .is_some_and(|l| self.levels.get(&l) == Some(&Level::Zero)),
721            Shape::Constant(name, levels) => self.is_constant(e, name, levels),
722            Shape::Bound(i) => record.get("bvar").and_then(Value::as_u64) == Some(*i),
723            Shape::Apply(f, a) => self
724                .app(e)
725                .is_some_and(|(ef, ea)| self.fits(ef, f) && self.fits(ea, a)),
726            Shape::Forall(domain, body) => record.get("forallE").is_some_and(|x| {
727                index(&x["type"]).is_some_and(|t| self.fits(t, domain))
728                    && index(&x["body"]).is_some_and(|t| self.fits(t, body))
729            }),
730        }
731    }
732
733    fn declares(&self, name: &'static [&'static str], kind: fn(&Kind) -> bool, ty: Shape) -> bool {
734        self.named(name).is_some_and(|(_, constant)| {
735            kind(&constant.kind) && constant.level_params == 0 && self.fits(constant.ty, &ty)
736        })
737    }
738
739    /// The declarations a leaf's meaning depends on, in their canonical shapes.
740    fn canonical_environment(&self) -> bool {
741        let of_reduce_bool = pi(
742            c(BOOL),
743            pi(
744                c(BOOL),
745                pi(
746                    eq_bool(ap(c(REDUCE_BOOL), [b(1)]), b(0)),
747                    eq_bool(b(2), b(1)),
748                ),
749            ),
750        );
751        let of_decide_eq_true = pi(
752            Shape::Prop,
753            pi(
754                ap(c(DECIDABLE), [b(0)]),
755                pi(eq_bool(ap(c(DECIDE), [b(1), b(0)]), c(BOOL_TRUE)), b(2)),
756            ),
757        );
758        let decide = pi(Shape::Prop, pi(ap(c(DECIDABLE), [b(0)]), c(BOOL)));
759        self.declares(OF_REDUCE_BOOL, |k| *k == Kind::Axiom, of_reduce_bool)
760            && self.declares(REDUCE_BOOL, |k| *k == Kind::Opaque, pi(c(BOOL), c(BOOL)))
761            && self.declares(
762                OF_DECIDE_EQ_TRUE,
763                |k| *k == Kind::Theorem,
764                of_decide_eq_true,
765            )
766            && self.declares(DECIDE, |k| *k == Kind::Definition, decide)
767            && self.inductive_with_constructors(BOOL, &[BOOL_FALSE, BOOL_TRUE])
768            && self.inductive_with_constructors(EQ, &[EQ_REFL])
769    }
770
771    fn inductive_with_constructors(
772        &self,
773        name: &'static [&'static str],
774        constructors: &[&[&str]],
775    ) -> bool {
776        let Some((inductive, constant)) = self.named(name) else {
777            return false;
778        };
779        let Kind::Inductive {
780            constructors: declared,
781        } = &constant.kind
782        else {
783            return false;
784        };
785        declared.len() == constructors.len()
786            && declared.iter().zip(constructors).all(|(&ctor, expected)| {
787                self.name_is(ctor, expected)
788                    && self
789                        .constants
790                        .get(&ctor)
791                        .is_some_and(|k| k.kind == Kind::Constructor { inductive })
792            })
793    }
794
795    /// The leaf rooted at `e`, when `e` is one.
796    fn leaf(&self, e: usize) -> Option<NativeDecision> {
797        let (premise, appeal) = self.app(e)?;
798        let (partial, instance) = self.app(premise)?;
799        let (head, proposition) = self.app(partial)?;
800        if !self.is_constant(head, OF_DECIDE_EQ_TRUE, &[]) {
801            return None;
802        }
803        let (applied, evaluation) = self.app(appeal)?;
804        let (appealed, answer) = self.app(applied)?;
805        let (escape, decision) = self.app(appealed)?;
806        if !self.is_constant(escape, OF_REDUCE_BOOL, &[])
807            || !self.is_constant(answer, BOOL_TRUE, &[])
808        {
809            return None;
810        }
811        if !self.fits(
812            evaluation,
813            &ap(at(EQ_REFL, vec![one()]), [c(BOOL), c(BOOL_TRUE)]),
814        ) {
815            return None;
816        }
817        let (decision_name, levels) = self.constant(decision)?;
818        let constant = self.constants.get(&decision_name)?;
819        if !levels.is_empty()
820            || constant.kind != Kind::Definition
821            || constant.level_params != 0
822            || !self.is_constant(constant.ty, BOOL, &[])
823        {
824            return None;
825        }
826        let (decided, decided_instance) = self.app(constant.value?)?;
827        let (decide, decided_proposition) = self.app(decided)?;
828        if !self.is_constant(decide, DECIDE, &[])
829            || !self.same(decided_proposition, proposition)
830            || !self.same(decided_instance, instance)
831        {
832            return None;
833        }
834        Some(NativeDecision {
835            leaf: e,
836            premise,
837            appeal,
838            evaluation,
839            proposition,
840            instance,
841            decision: intake::print_lean_name(&self.names[&decision_name]),
842        })
843    }
844
845    /// Structural equality of two expressions, binder names and binder annotations aside.
846    fn same(&self, a: usize, b: usize) -> bool {
847        let mut work = vec![(a, b)];
848        let mut seen = HashSet::new();
849        while let Some((a, b)) = work.pop() {
850            if a == b || !seen.insert((a, b)) {
851                continue;
852            }
853            if self.same_node(a, b, &mut work).is_none() {
854                return false;
855            }
856        }
857        true
858    }
859
860    /// Whether the nodes `a` and `b` agree, their children queued on `work` for comparison.
861    fn same_node(&self, a: usize, b: usize, work: &mut Vec<(usize, usize)>) -> Option<()> {
862        let (x, y) = (self.exprs.get(&a)?, self.exprs.get(&b)?);
863        let mut pair = |field: &str, keys: &[&str]| -> Option<()> {
864            let (x, y) = (x.get(field)?, y.get(field)?);
865            for key in keys {
866                work.push((index(&x[*key])?, index(&y[*key])?));
867            }
868            Some(())
869        };
870        if x.get("app").is_some() {
871            pair("app", &["fn", "arg"])
872        } else if x.get("lam").is_some() {
873            pair("lam", &["type", "body"])
874        } else if x.get("forallE").is_some() {
875            pair("forallE", &["type", "body"])
876        } else if let Some(xl) = x.get("letE") {
877            (xl.get("nondep") == y.get("letE")?.get("nondep")).then_some(())?;
878            pair("letE", &["type", "value", "body"])
879        } else if let Some(xm) = x.get("mdata") {
880            (xm["data"] == y.get("mdata")?["data"]).then_some(())?;
881            pair("mdata", &["expr"])
882        } else if let Some(xp) = x.get("proj") {
883            let yp = y.get("proj")?;
884            let names = (
885                self.names.get(&index(&xp["typeName"])?),
886                self.names.get(&index(&yp["typeName"])?),
887            );
888            (xp["idx"] == yp["idx"] && names.0.is_some() && names.0 == names.1).then_some(())?;
889            pair("proj", &["struct"])
890        } else if x.get("const").is_some() {
891            let (xn, xl) = self.constant(a)?;
892            let (yn, yl) = self.constant(b)?;
893            (self.names.get(&xn).is_some()
894                && self.names.get(&xn) == self.names.get(&yn)
895                && xl == yl)
896                .then_some(())
897        } else if let Some(xs) = x.get("sort") {
898            let (xs, ys) = (
899                self.levels.get(&index(xs)?),
900                self.levels.get(&index(y.get("sort")?)?),
901            );
902            (xs.is_some() && xs == ys).then_some(())
903        } else {
904            let tag = ["bvar", "natVal", "strVal"]
905                .into_iter()
906                .find(|k| x.get(*k).is_some())?;
907            (y.get(tag).is_some() && x.get(tag) == y.get(tag)).then_some(())
908        }
909    }
910}
911
912fn simple_typed(kind: Kind, body: &Value) -> Option<(usize, Constant)> {
913    let name = index(&body["name"])?;
914    let level_params = body["levelParams"].as_array()?.len();
915    Some((
916        name,
917        Constant {
918            kind,
919            level_params,
920            ty: index(&body["type"])?,
921            value: None,
922        },
923    ))
924}
925
926fn index(v: &Value) -> Option<usize> {
927    v.as_u64().and_then(|n| usize::try_from(n).ok())
928}