Skip to main content

logicaffeine_kernel/dag/
logos_tc.rs

1//! LOGOS's Calculus of Inductive Constructions in the core: the rules [`Theory::Logos`] adds to
2//! the substitution checker.
3//!
4//! The judgments are the `Term` kernel's (`type_checker.rs`, `reduction.rs`), stated over the
5//! core's hash-consed DAG:
6//!
7//! * a literal's type is LOGOS's carrier (`Int`, `Nat`, `Float`, `Text`, `Duration`, `Date`,
8//!   `Moment`);
9//! * `match d return m with cs` takes the inductive family from `d`'s type, accepts a motive that
10//!   is a family over that type and its indices (or a bare type, read as the constant family),
11//!   owes a case for every constructor or for exactly those `d`'s indices allow, checks each case
12//!   against the constructor's fields, refuses an elimination out of `SProp` into a relevant sort
13//!   and a large elimination of a family that is a proposition at some instance of its levels
14//!   (unless it is a subsingleton), and has the motive applied to `d`'s indices and `d` as its
15//!   type. It ι-reduces once `d` is in constructor form (a `Nat` literal is `Zero` or `Succ`, an
16//!   `Int` literal `Int_ofNat` or `Int_negSucc`), and a single-case match on a proof of a K family
17//!   or a value of a unit-like type reads its scrutinee as the constructor;
18//! * `fix f => body` has the type read off `body`'s binders and the motive of its innermost
19//!   `match`; in a checking position it is checked against the expected type instead, with `f`
20//!   bound at that type. It unrolls only when the argument at the structural column the guard
21//!   chose is in constructor form;
22//! * conversion relates stuck `match`es, `fix`es and mutual `fix`es by congruence, an `Int`
23//!   literal with its constructor form, and two instances of a cumulative inductive that differ
24//!   only in irrelevant universe parameters;
25//! * a term of type `A` stands where `B` is expected when `A ≤ B` (cumulative universes,
26//!   covariant in a function type's codomain).
27
28//!
29//! The declaration-time checks of LOGOS's own design (the termination guard, strict positivity)
30//! and the computation rules of LOGOS's builtins (integer and text operations, the validated
31//! `natLe`/`natLt`, the reflection embedding) are the `Term` kernel's, applied to [`lift`]ed
32//! terms through `LogosState::ctx`, and so are the certificates of the programs computed natively
33//! on literals (an arithmetic operation, a `Nat` order or equality, a division program such as
34//! Lean's well-founded `Nat.div._unary`): one implementation of each, so the two kernels cannot
35//! disagree about them.
36
37use super::env::Kind;
38use super::error::{KResult, KernelFailure};
39use super::expr::{ExprId, Lit, Node};
40use super::level::LevelsId;
41use super::logos::{lift, lower_open};
42use super::tc::{Kernel, Theory};
43use crate::context::Context;
44use crate::reduction::{CertifiedProgram, ProgramShape};
45use crate::term::Term;
46use rustc_hash::FxHashMap as FxMap;
47use std::rc::Rc;
48
49/// What checking LOGOS's theory needs beyond the store and the environment.
50#[derive(Clone)]
51pub struct LogosState {
52    /// The LOGOS context the kernel's declarations come from.
53    pub(crate) ctx: Context,
54    /// The structural column the guard chose for each fixpoint body (`None`: no column, so the
55    /// fixpoint never unrolls).
56    columns: FxMap<ExprId, Option<usize>>,
57    /// The builtin computation rules' answers, by the application they rewrote.
58    builtins: FxMap<ExprId, Option<ExprId>>,
59    /// Whether `Nat` and `Int` are the canonical families, so that a literal is read as its
60    /// constructor form (`Context::canonical_numerals`).
61    numerals: crate::context::CanonicalNumerals,
62    /// Each inductive's and constructor's irrelevant universe parameters, as a mask over its
63    /// level list (see `logos_env::irrelevant_levels`).
64    irrelevant: FxMap<super::name::NameId, u64>,
65    /// The context's certified observational equality, by the names its rules fire for.
66    pub(super) certified_obs: Option<super::logos_obs::Certified>,
67    /// What the `Term` kernel's evaluator certifies each constant to compute at an arity, and how
68    /// its operands are passed (`reduction::program_shape`, `reduction::certified_program`): asked
69    /// once per context, `None` for a constant that computes nothing natively.
70    programs: FxMap<(super::name::NameId, usize), Option<Rc<(ProgramShape, CertifiedProgram)>>>,
71}
72
73impl LogosState {
74    /// A declaration is done: what its scratch region held is gone, so are the answers about it.
75    pub(crate) fn end_decl(&mut self) {
76        self.columns.retain(|k, _| !k.is_scratch());
77        self.builtins
78            .retain(|k, v| !k.is_scratch() && !v.is_some_and(ExprId::is_scratch));
79    }
80}
81
82macro_rules! bail {
83    ($($arg:tt)*) => {
84        return Err(KernelFailure::reject(format!($($arg)*)))
85    };
86}
87
88impl Kernel {
89    /// A kernel for LOGOS's theory over the declarations of `ctx`.
90    pub fn logos(ctx: &Context) -> Kernel {
91        let mut k = Kernel::new();
92        k.theory = Theory::Logos;
93        k.wk = super::wellknown::WellKnown::logos(&mut k.s.names);
94        let certified_obs = super::logos_obs::Certified::of(ctx, &mut k.s.names);
95        k.logos = Some(Box::new(LogosState {
96            ctx: ctx.clone(),
97            columns: FxMap::default(),
98            builtins: FxMap::default(),
99            numerals: ctx.canonical_numerals(),
100            irrelevant: FxMap::default(),
101            certified_obs,
102            programs: FxMap::default(),
103        }));
104        k
105    }
106
107    /// Move a LOGOS kernel to `ctx`, a later state of the lineage its declarations came from: the
108    /// guard, positivity, index coverage and the builtin evaluator read the context they check in.
109    pub(crate) fn follow(&mut self, ctx: &Context) {
110        let certified_obs = super::logos_obs::Certified::of(ctx, &mut self.s.names);
111        let state = self
112            .logos
113            .as_mut()
114            .expect("a LOGOS kernel carries its context");
115        state.ctx = ctx.clone();
116        state.certified_obs = certified_obs;
117        state.programs.clear();
118    }
119
120    /// Whether this kernel's `Nat` and `Int` are the canonical families its literal rules read.
121    pub(crate) fn numerals(&self) -> crate::context::CanonicalNumerals {
122        self.logos
123            .as_ref()
124            .expect("a LOGOS kernel carries its context")
125            .numerals
126    }
127
128    /// Whether the `Nat` literal `n` reads as the constructor it names (`int_inductive::reads_nat_literal`):
129    /// always in Lean's theory, whose `Nat` the primitive checks hold to its canonical form; in
130    /// LOGOS's, `0` as `Zero` while `Zero : Nat` is `Nat`'s constructor and `n + 1` as `Succ n`
131    /// while `Succ : Nat → Nat` is too.
132    pub(crate) fn reads_nat_literal(&self, n: &super::nat::Nat) -> bool {
133        match &self.logos {
134            None => true,
135            Some(state) if n.is_zero() => state.numerals.zero,
136            Some(state) => state.numerals.succ,
137        }
138    }
139
140    /// Whether `Succ` of a literal is the next one and the arithmetic on literals computes: the
141    /// successor reading, [`Kernel::reads_nat_literal`] past `0`.
142    pub(crate) fn nat_literal_rules(&self) -> bool {
143        self.logos
144            .as_ref()
145            .map_or(true, |state| state.numerals.succ)
146    }
147
148    fn logos_ctx(&self) -> &Context {
149        &self
150            .logos
151            .as_ref()
152            .expect("a LOGOS kernel carries its context")
153            .ctx
154    }
155
156    fn global(&mut self, name: &str) -> ExprId {
157        let n = self.s.names.str(super::name::NameId::ANON, name);
158        self.s.cnst(n, LevelsId::EMPTY)
159    }
160
161    pub(crate) fn infer_logos(&mut self, e: ExprId, infer_only: bool) -> KResult<ExprId> {
162        if self.theory != Theory::Logos {
163            bail!("a LOGOS construct outside LOGOS's theory");
164        }
165        match self.s.node(e) {
166            Node::Lit(l) => {
167                let carrier = match self.s.lit_value(l) {
168                    Lit::Int(_) | Lit::BigInt(_) => "Int",
169                    Lit::Float(_) => "Float",
170                    Lit::Duration(_) => "Duration",
171                    Lit::Date(_) => "Date",
172                    Lit::Moment(_) => "Moment",
173                };
174                Ok(self.global(carrier))
175            }
176            Node::NatLit(_) => Ok(self.global("Nat")),
177            Node::StrLit(_) => Ok(self.global("Text")),
178            Node::Match(d, m, cs) => self.infer_match(d, m, cs, infer_only),
179            Node::Fix(body) => self.infer_fix(e, body, infer_only),
180            Node::MutualFix(ds, i) => self.infer_mutual_fix(ds, i, infer_only),
181            other => unreachable!("not a LOGOS construct: {other:?}"),
182        }
183    }
184
185    /// The inductive family a type is an application of, with the application's arguments.
186    fn inductive_app(
187        &mut self,
188        ty: ExprId,
189    ) -> KResult<(super::name::NameId, LevelsId, Vec<ExprId>)> {
190        let w = self.whnf(ty)?;
191        let (head, args) = self.s.app_args(w);
192        if let Node::Const(n, ls) = self.s.node(head) {
193            if let Some(c) = self.env.get(n) {
194                if matches!(c.kind, Kind::Induct(_)) {
195                    return Ok((n, ls, args.to_vec()));
196                }
197            }
198        }
199        bail!(
200            "match on a value whose type {} is not an inductive family",
201            self.show(ty)
202        )
203    }
204
205    fn infer_match(
206        &mut self,
207        d: ExprId,
208        m: ExprId,
209        cs: ExprId,
210        infer_only: bool,
211    ) -> KResult<ExprId> {
212        let dt = self.infer(d, infer_only)?;
213        let (ind, ls, args) = self.inductive_app(dt)?;
214        let info = match &self.env.get(ind).expect("checked above").kind {
215            Kind::Induct(i) => i.clone(),
216            _ => unreachable!(),
217        };
218        let dtw = self.whnf(dt)?;
219        if info.num_indices > 0 {
220            return self.infer_indexed_match(d, m, cs, ind, ls, &args, &info, dtw, infer_only);
221        }
222        let mt = self.infer(m, infer_only)?;
223        let mtw = self.whnf(mt)?;
224        let motive = match self.s.node(mtw) {
225            // Inferring only, the type is the motive at the discriminant, as for the indexed
226            // `match`; the motive is held to the discriminant's type where the `match` is checked.
227            Node::Pi(..) if infer_only => m,
228            Node::Pi(dom, cod) => {
229                if !self.is_def_eq(dom, dtw)? {
230                    bail!(
231                        "motive parameter {} doesn't match discriminant type {}",
232                        self.show(dom),
233                        self.show(dtw)
234                    );
235                }
236                let x = self.push_local(dom, None);
237                let codw = self.s.instantiate1(cod, x);
238                let codw = self.whnf(codw)?;
239                if !matches!(self.s.node(codw), Node::Sort(_)) {
240                    bail!("motive type codomain is not a sort");
241                }
242                m
243            }
244            Node::Sort(_) => {
245                let body = self.s.lift_loose(m, 0, 1);
246                self.s.lam(dtw, body)
247            }
248            _ => bail!("motive {} is not a function or type", self.show(m)),
249        };
250        let cases = self.s.seq_items(cs);
251        if cases.len() != info.ctors.len() {
252            bail!(
253                "wrong number of cases: expected {}, found {}",
254                info.ctors.len(),
255                cases.len()
256            );
257        }
258        let params = &args[..info.num_params as usize];
259        for (case, ctor) in cases.iter().zip(info.ctors.iter()) {
260            if infer_only {
261                continue;
262            }
263            let expected = self.case_type(*ctor, ls, params, motive)?;
264            if !self.check_fix_against(*case, expected)? {
265                let ct = self.infer(*case, false)?;
266                if !self.conforms(ct, expected)? {
267                    bail!(
268                        "case for '{}' has type {} where {} is expected",
269                        self.s.names.display(*ctor),
270                        self.show(ct),
271                        self.show(expected)
272                    );
273                }
274            }
275        }
276        let applied = self.s.app(motive, d);
277        let ret = self.whnf_core(applied, false)?;
278        if !infer_only {
279            self.check_elimination(ind, dtw, ret)?;
280        }
281        Ok(ret)
282    }
283
284    /// `Π fields. motive (C params fields)` for constructor `ctor` of a family at `params`.
285    fn case_type(
286        &mut self,
287        ctor: super::name::NameId,
288        ls: LevelsId,
289        params: &[ExprId],
290        motive: ExprId,
291    ) -> KResult<ExprId> {
292        let c = self
293            .env
294            .get(ctor)
295            .expect("an inductive's constructor is declared")
296            .clone();
297        let us: Vec<_> = self.s.levels.get_list(ls).to_vec();
298        let mut ty = self.s.instantiate_lparams(c.ty, &c.lparams, &us);
299        for p in params {
300            let w = self.whnf(ty)?;
301            let Node::Pi(_, b) = self.s.node(w) else {
302                bail!("constructor type has fewer parameters than its family")
303            };
304            ty = self.s.instantiate1(b, *p);
305        }
306        let head = self.s.cnst(ctor, ls);
307        let head = self.s.mk_app_n(head, params);
308        let mut fields = Vec::new();
309        loop {
310            let w = self.whnf(ty)?;
311            match self.s.node(w) {
312                Node::Pi(a, b) => {
313                    let x = self.push_local(a, None);
314                    fields.push(x);
315                    ty = self.s.instantiate1(b, x);
316                }
317                _ => break,
318            }
319        }
320        let value = self.s.mk_app_n(head, &fields);
321        let w = self.whnf(ty)?;
322        let (_, result_args) = self.s.app_args(w);
323        let indices: Vec<ExprId> = result_args.iter().skip(params.len()).copied().collect();
324        let body = self.s.mk_app_n(motive, &indices);
325        let body = self.s.app(body, value);
326        let body = self.whnf_core(body, false)?;
327        Ok(self.mk_pi(&fields, body))
328    }
329
330    /// `match` on an indexed family: the motive abstracts the indices and then the scrutinee; a
331    /// case is owed for every constructor, or exactly for the constructors the scrutinee's indices
332    /// allow (`type_checker::constructors_the_indices_allow`); each case is the motive at the
333    /// constructor's result indices and value; the result is the motive at the scrutinee's indices
334    /// and the scrutinee.
335    #[allow(clippy::too_many_arguments)]
336    fn infer_indexed_match(
337        &mut self,
338        d: ExprId,
339        m: ExprId,
340        cs: ExprId,
341        ind: super::name::NameId,
342        ls: LevelsId,
343        args: &[ExprId],
344        info: &super::env::InductiveInfo,
345        dtw: ExprId,
346        infer_only: bool,
347    ) -> KResult<ExprId> {
348        let np = info.num_params as usize;
349        let (params, indices) = args.split_at(np);
350        if !infer_only {
351            self.validate_indexed_motive(ind, ls, params, indices.len(), m)?;
352        }
353        let cases = self.s.seq_items(cs);
354        let owed: Vec<super::name::NameId> = if cases.len() == info.ctors.len() {
355            info.ctors.to_vec()
356        } else {
357            let allowed = self.allowed_constructors(ind, np, args);
358            if cases.len() != allowed.len() {
359                bail!(
360                    "wrong number of cases: expected {}, found {}",
361                    allowed.len(),
362                    cases.len()
363                );
364            }
365            allowed.iter().map(|i| info.ctors[*i]).collect()
366        };
367        for (case, ctor) in cases.iter().zip(owed.iter()) {
368            if infer_only {
369                continue;
370            }
371            let expected = self.case_type(*ctor, ls, params, m)?;
372            if !self.check_fix_against(*case, expected)? {
373                let ct = self.infer(*case, false)?;
374                if !self.conforms(ct, expected)? {
375                    bail!(
376                        "case for '{}' has type {} where {} is expected",
377                        self.s.names.display(*ctor),
378                        self.show(ct),
379                        self.show(expected)
380                    );
381                }
382            }
383        }
384        let ret = self.s.mk_app_n(m, indices);
385        let ret = self.s.app(ret, d);
386        let ret = self.whnf_core(ret, false)?;
387        if !infer_only {
388            let rs = self.infer(ret, true)?;
389            let rs = self.whnf(rs)?;
390            if !matches!(self.s.node(rs), Node::Sort(_)) {
391                bail!("indexed match on '{}' has a non-type result: the motive is not a family into a sort", self.s.names.display(ind));
392            }
393            self.check_elimination(ind, dtw, ret)?;
394        }
395        Ok(ret)
396    }
397
398    /// The motive's telescope is the family's index telescope (at the scrutinee's parameters), then
399    /// the family at those indices, then a sort.
400    fn validate_indexed_motive(
401        &mut self,
402        ind: super::name::NameId,
403        ls: LevelsId,
404        params: &[ExprId],
405        num_indices: usize,
406        m: ExprId,
407    ) -> KResult<()> {
408        let c = self.env.get(ind).expect("an inductive").clone();
409        let us: Vec<_> = self.s.levels.get_list(ls).to_vec();
410        let mut arity = self.s.instantiate_lparams(c.ty, &c.lparams, &us);
411        for p in params {
412            let w = self.whnf(arity)?;
413            let Node::Pi(_, b) = self.s.node(w) else {
414                bail!("inductive has fewer parameters than its discriminant")
415            };
416            arity = self.s.instantiate1(b, *p);
417        }
418        let head = self.s.cnst(ind, ls);
419        let mut family = self.s.mk_app_n(head, params);
420        let mut mt = self.infer(m, false)?;
421        for _ in 0..num_indices {
422            let aw = self.whnf(arity)?;
423            let Node::Pi(ad, ab) = self.s.node(aw) else {
424                bail!("inductive index telescope is shorter than its discriminant")
425            };
426            let mw = self.whnf(mt)?;
427            let Node::Pi(md, mb) = self.s.node(mw) else {
428                bail!("indexed motive lacks an index binder")
429            };
430            if !self.is_def_eq(md, ad)? {
431                bail!("indexed motive index domain differs from the inductive telescope");
432            }
433            let x = self.push_local(ad, None);
434            arity = self.s.instantiate1(ab, x);
435            mt = self.s.instantiate1(mb, x);
436            family = self.s.app(family, x);
437        }
438        let aw = self.whnf(arity)?;
439        if !matches!(self.s.node(aw), Node::Sort(_)) {
440            bail!("discriminant does not instantiate the full inductive index telescope");
441        }
442        let mw = self.whnf(mt)?;
443        let Node::Pi(md, mb) = self.s.node(mw) else {
444            bail!("indexed motive lacks its major-premise binder")
445        };
446        if !self.is_def_eq(md, family)? {
447            bail!("indexed motive major domain differs from the generic indexed family");
448        }
449        let x = self.push_local(family, None);
450        let cod = self.s.instantiate1(mb, x);
451        let cod = self.whnf(cod)?;
452        if !matches!(self.s.node(cod), Node::Sort(_)) {
453            bail!("indexed motive must return a sort after its major-premise binder");
454        }
455        Ok(())
456    }
457
458    /// The positions of the constructors whose declared result indices can equal the indices among
459    /// `args`, the family's type arguments with its `np` parameters first.
460    fn allowed_constructors(
461        &mut self,
462        ind: super::name::NameId,
463        np: usize,
464        args: &[ExprId],
465    ) -> Vec<usize> {
466        let name = self.s.names.display(ind);
467        let lifted: Vec<Term> = args.iter().map(|i| lift(&self.s, *i)).collect();
468        crate::type_checker::constructors_the_indices_allow(self.logos_ctx(), &name, np, &lifted)
469    }
470
471    /// The elimination restrictions of `match` (`type_checker::check_elimination`'s rule): an
472    /// `SProp` family eliminates only into `SProp`, and a family that is a proposition at some
473    /// instance of its levels eliminates into a larger sort only when it is a subsingleton or, for
474    /// a family whose level may be `Prop`, the result's level is at most its own.
475    fn check_elimination(
476        &mut self,
477        ind: super::name::NameId,
478        disc_ty: ExprId,
479        ret: ExprId,
480    ) -> KResult<()> {
481        let ds = self.infer(disc_ty, true)?;
482        let ds = self.whnf(ds)?;
483        let rs = self.infer(ret, true)?;
484        let rs = self.whnf(rs)?;
485        let (Node::Sort(dl), Node::Sort(rl)) = (self.s.node(ds), self.s.node(rs)) else {
486            bail!("a match whose scrutinee or result is not a type");
487        };
488        let levels = &mut self.s.levels;
489        if levels.is_sprop(dl) && !levels.is_sprop(rl) {
490            bail!(
491                "elimination of the definitionally-irrelevant proposition '{}' into a relevant sort is not allowed",
492                self.s.names.display(ind)
493            );
494        }
495        let zero = super::level::LevelId::ZERO;
496        let one = levels.succ(zero);
497        let large = if levels.is_sprop(dl) {
498            false
499        } else if levels.equiv(dl, zero) {
500            !levels.equiv(rl, zero) && !levels.is_sprop(rl)
501        } else if !levels.leq(one, dl) {
502            !levels.leq(rl, dl)
503        } else {
504            false
505        };
506        if large {
507            let name = self.s.names.display(ind);
508            if !crate::type_checker::is_subsingleton_prop(self.logos_ctx(), &name).unwrap_or(false)
509            {
510                bail!(
511                    "large elimination of proposition '{name}' into a larger sort is not allowed"
512                );
513            }
514        }
515        Ok(())
516    }
517
518    /// A fixpoint in a checking position — a definition's value, an argument, a `let` value, a
519    /// `match` case — checked against the type that position expects: bound at that type, a
520    /// terminating fixpoint's body must have it (`type_checker::check_type`'s rule). It types a recursion whose result type no `match` motive states,
521    /// such as a `match` applied to further arguments. `false` hands the value to inference;
522    /// `expected` is a type, as every caller has already established.
523    pub(crate) fn check_fix_against(&mut self, value: ExprId, expected: ExprId) -> KResult<bool> {
524        let Node::Fix(body) = self.s.node(value) else {
525            return Ok(false);
526        };
527        let Term::Fix { name, body: b } = lift(&self.s, value) else {
528            unreachable!()
529        };
530        if crate::termination::check_termination(self.logos_ctx(), &name, &b).is_err() {
531            return Ok(false);
532        }
533        let x = self.push_local(expected, None);
534        let opened = self.s.instantiate1(body, x);
535        match self.infer(opened, false) {
536            Ok(got) => self.conforms(got, expected),
537            Err(_) => Ok(false),
538        }
539    }
540
541    /// The type of `fix f => body`, read off `body` as the `Term` kernel reads it; checked, the
542    /// guard admits the recursion and `body`, with `f` bound at that type, has it.
543    fn infer_fix(&mut self, fix: ExprId, body: ExprId, infer_only: bool) -> KResult<ExprId> {
544        let ty = self.fix_type(body)?;
545        if !infer_only {
546            let Term::Fix { name, body: b } = lift(&self.s, fix) else {
547                unreachable!()
548            };
549            crate::termination::check_termination(self.logos_ctx(), &name, &b)
550                .map_err(|e| KernelFailure::reject(e.to_string()))?;
551            let x = self.push_local(ty, None);
552            let opened = self.s.instantiate1(body, x);
553            let got = self.infer(opened, false)?;
554            if !self.is_def_eq(got, ty)? {
555                bail!(
556                    "a fixpoint's body has type {} where its binders and motive read {}",
557                    self.show(got),
558                    self.show(ty)
559                );
560            }
561        }
562        Ok(ty)
563    }
564
565    fn infer_mutual_fix(&mut self, ds: ExprId, index: u32, infer_only: bool) -> KResult<ExprId> {
566        let bodies = self.s.seq_items(ds);
567        if bodies.is_empty() || index as usize >= bodies.len() {
568            bail!("mutual fixpoint with an empty block or out-of-range index");
569        }
570        // A member's type is read off its own binders and motive with the block's names unbound,
571        // as the `Term` kernel reads it: a sibling mentioned there is a loose variable, and refused.
572        let mut types = Vec::with_capacity(bodies.len());
573        let n = bodies.len() as u32;
574        for b in bodies.iter() {
575            if self.s.loose_bvar_range(*b) > n {
576                bail!("a mutual fixpoint member mentions a variable outside its block");
577            }
578            types.push(self.fix_type(*b)?);
579        }
580        if !infer_only {
581            let whole = self.s.intern(Node::MutualFix(ds, index));
582            let Term::MutualFix { defs, .. } = lift(&self.s, whole) else {
583                unreachable!()
584            };
585            crate::termination::check_termination_mutual(self.logos_ctx(), &defs)
586                .map_err(|e| KernelFailure::reject(e.to_string()))?;
587            let xs: Vec<ExprId> = types.iter().map(|t| self.push_local(*t, None)).collect();
588            for b in bodies.iter() {
589                let opened = self.s.instantiate_rev(*b, &xs);
590                self.infer(opened, false)?;
591            }
592        }
593        Ok(types[index as usize])
594    }
595
596    /// `infer_fix_type_structurally`: a fixpoint's type from its body's binders and the motive of
597    /// the innermost `match` (applied to the discriminant's indices and the discriminant), read
598    /// through an application spine, so a convoy `(match v … ) w` has the motive's type applied to
599    /// `w`. The fixpoint's own binders stay loose in `body`, so a type that mentions one is refused.
600    fn fix_type(&mut self, body: ExprId) -> KResult<ExprId> {
601        let mut locals = Vec::new();
602        let mut cur = body;
603        while let Node::Lam(a, b) = self.s.node(cur) {
604            let a = self.s.instantiate_rev(a, &locals);
605            self.infer(a, true)?;
606            let x = self.push_local(a, None);
607            locals.push(x);
608            cur = b;
609        }
610        let inner = self.s.instantiate_rev(cur, &locals);
611        let (head, args) = self.s.app_args(inner);
612        let result = match self.s.node(head) {
613            Node::Match(d, m, _) => {
614                let mut ty = self.match_result_type(d, m)?;
615                for a in args.iter() {
616                    let w = self.whnf(ty)?;
617                    let Node::Pi(_, b) = self.s.node(w) else {
618                        bail!(
619                            "a convoy applies a match whose type {} is not a function",
620                            self.show(w)
621                        )
622                    };
623                    ty = self.s.instantiate1(b, *a);
624                }
625                ty
626            }
627            _ => self.infer(inner, true)?,
628        };
629        Ok(self.mk_pi(&locals, result))
630    }
631
632    /// A `match`'s type as a fixpoint's type is read off it: a motive that is itself a type, or
633    /// the motive applied to the scrutinee's indices and the scrutinee.
634    fn match_result_type(&mut self, d: ExprId, m: ExprId) -> KResult<ExprId> {
635        let mt = self.infer(m, true)?;
636        let mtw = self.whnf(mt)?;
637        if matches!(self.s.node(mtw), Node::Sort(_)) {
638            return Ok(m);
639        }
640        let dt = self.infer(d, true)?;
641        let indices = match self.inductive_app(dt) {
642            Ok((ind, _, args)) => match &self.env.get(ind).expect("an inductive").kind {
643                Kind::Induct(i) => args[i.num_params as usize..].to_vec(),
644                _ => Vec::new(),
645            },
646            Err(_) => Vec::new(),
647        };
648        let applied = self.s.mk_app_n(m, &indices);
649        let applied = self.s.app(applied, d);
650        self.whnf_core(applied, false)
651    }
652
653    /// ι for `match`: the case of the discriminant's constructor, applied to its fields.
654    pub(crate) fn reduce_match(&mut self, e: ExprId) -> KResult<Option<ExprId>> {
655        if self.theory != Theory::Logos {
656            return Ok(None);
657        }
658        let Node::Match(d, m, cs) = self.s.node(e) else {
659            unreachable!()
660        };
661        let dw = self.whnf(d)?;
662        let cases = self.s.seq_items(cs);
663        let Some((ctor, fields)) = self.constructor_view(dw)? else {
664            if cases.len() == 1 {
665                if let Some(k) = self.k_constructor_form(dw)? {
666                    return Ok(Some(self.s.intern(Node::Match(k, m, cs))));
667                }
668            }
669            return Ok(None);
670        };
671        let Some(pos) = self.case_position(dw, ctor, cases.len())? else {
672            return Ok(None);
673        };
674        let Some(case) = cases.get(pos).copied() else {
675            return Ok(None);
676        };
677        Ok(Some(self.s.mk_app_n(case, &fields)))
678    }
679
680    /// Which case a constructor's value takes: its constructor's position, or, in a case list
681    /// pruned to the constructors the scrutinee's indices allow, its position among those.
682    fn case_position(
683        &mut self,
684        v: ExprId,
685        ctor: super::name::NameId,
686        num_cases: usize,
687    ) -> KResult<Option<usize>> {
688        let Some(c) = self.env.get(ctor).cloned() else {
689            return Ok(None);
690        };
691        let Kind::Ctor(ci) = &c.kind else {
692            return Ok(None);
693        };
694        let Some(ic) = self.env.get(ci.induct).cloned() else {
695            return Ok(None);
696        };
697        let Kind::Induct(info) = &ic.kind else {
698            return Ok(None);
699        };
700        if num_cases == info.ctors.len() {
701            return Ok(Some(ci.cidx as usize));
702        }
703        // The scrutinee's indices: the constructor's declared result at its actual arguments.
704        let (head, actual) = self.s.app_args(v);
705        let Node::Const(_, ls) = self.s.node(head) else {
706            return Ok(None);
707        };
708        let us: Vec<_> = self.s.levels.get_list(ls).to_vec();
709        let mut ty = self.s.instantiate_lparams(c.ty, &c.lparams, &us);
710        for a in actual.iter() {
711            let w = self.whnf(ty)?;
712            let Node::Pi(_, b) = self.s.node(w) else {
713                return Ok(None);
714            };
715            ty = self.s.instantiate1(b, *a);
716        }
717        let w = self.whnf(ty)?;
718        let (_, result) = self.s.app_args(w);
719        let np = info.num_params as usize;
720        let allowed = self.allowed_constructors(ci.induct, np, &result);
721        if allowed.len() != num_cases {
722            return Ok(None);
723        }
724        Ok(allowed.iter().position(|i| *i == ci.cidx as usize))
725    }
726
727    /// The K-like rule of LOGOS's ι: a proof of a K family (`Context::k_constructor`, a
728    /// one-constructor proposition whose constructor fixes its indices) or a value of a unit-like
729    /// type (`Context::unit_constructor`) is its constructor at the parameters, when that
730    /// constructor has the proof's type.
731    fn k_constructor_form(&mut self, proof: ExprId) -> KResult<Option<ExprId>> {
732        let pt = match self.infer(proof, true) {
733            Ok(t) => self.whnf(t)?,
734            Err(_) => return Ok(None),
735        };
736        let (head, args) = self.s.app_args(pt);
737        let Node::Const(ind, ls) = self.s.node(head) else {
738            return Ok(None);
739        };
740        let name = self.s.names.display(ind);
741        let ctx = self.logos_ctx();
742        let unit = ctx.unit_constructor(&name).map(str::to_string);
743        let Some(ctor) = ctx
744            .k_constructor(&name)
745            .map(str::to_string)
746            .or(unit.clone())
747        else {
748            return Ok(None);
749        };
750        let np = ctx.inductive_num_params(&name);
751        if unit.is_none() {
752            let sort = self.infer(pt, true)?;
753            let sort = self.whnf(sort)?;
754            let Node::Sort(l) = self.s.node(sort) else {
755                return Ok(None);
756            };
757            if self.s.levels.is_sprop(l) || l != super::level::LevelId::ZERO {
758                return Ok(None);
759            }
760        }
761        let c = self.s.names.str(super::name::NameId::ANON, &ctor);
762        let value = self.s.cnst(c, ls);
763        let value = self.s.mk_app_n(value, &args[..np.min(args.len())]);
764        let vt = match self.infer(value, true) {
765            Ok(t) => t,
766            Err(_) => return Ok(None),
767        };
768        Ok(self.is_def_eq(pt, vt)?.then_some(value))
769    }
770
771    /// A value in constructor form: the constructor and its fields (parameters dropped). A `Nat`
772    /// literal is `Zero` or `Succ` of the literal below it.
773    fn constructor_view(
774        &mut self,
775        v: ExprId,
776    ) -> KResult<Option<(super::name::NameId, Vec<ExprId>)>> {
777        if let Some(c) = self.int_constructor_form(v) {
778            return self.constructor_view(c);
779        }
780        if let Node::NatLit(n) = self.s.node(v) {
781            let n = self.s.nat(n).clone();
782            if !self.reads_nat_literal(&n) {
783                return Ok(None);
784            }
785            if n.is_zero() {
786                let z = self.s.names.str(super::name::NameId::ANON, "Zero");
787                return Ok(Some((z, Vec::new())));
788            }
789            let pred = self.s.nat_lit(n.sub(&super::nat::Nat::from_u64(1)));
790            let succ = self.s.names.str(super::name::NameId::ANON, "Succ");
791            return Ok(Some((succ, vec![pred])));
792        }
793        let (head, args) = self.s.app_args(v);
794        let Node::Const(n, _) = self.s.node(head) else {
795            return Ok(None);
796        };
797        let Some(c) = self.env.get(n) else {
798            return Ok(None);
799        };
800        let Kind::Ctor(ci) = &c.kind else {
801            return Ok(None);
802        };
803        let np = ci.num_params as usize;
804        if args.len() < np {
805            return Ok(None);
806        }
807        Ok(Some((n, args[np..].to_vec())))
808    }
809
810    /// `whnf_core` of a stuck application under LOGOS's ι, out of line: the Lean theory's
811    /// `whnf_core` reaches it only through a test of [`Theory`] and keeps its own recursor step
812    /// inline.
813    #[inline(never)]
814    pub(crate) fn whnf_core_logos_app(&mut self, e: ExprId, cheap_proj: bool) -> KResult<ExprId> {
815        match self.reduce_logos_app(e)? {
816            Some(r) => self.whnf_core(r, cheap_proj),
817            None => Ok(e),
818        }
819    }
820
821    /// The application rules of LOGOS's ι: the quotient's (`Quot_lift`/`Quot_ind` of a
822    /// `Quot_mk`), and `fix` unrolling — an application of a fixpoint whose structural argument
823    /// is in constructor form becomes its body with the fixpoint substituted, applied to the
824    /// arguments.
825    pub(crate) fn reduce_logos_app(&mut self, e: ExprId) -> KResult<Option<ExprId>> {
826        if self.env.quot_init {
827            if let Some(r) = self.quot_reduce_rec(e)? {
828                return Ok(Some(r));
829            }
830        }
831        if let Some(r) = self.reduce_observational(e)? {
832            return Ok(Some(r));
833        }
834        let (head, args) = self.s.app_args(e);
835        match self.s.node(head) {
836            Node::Fix(body) => {
837                let Some(col) = self.structural_column(head)? else {
838                    return Ok(None);
839                };
840                let Some(arg) = args.get(col).copied() else {
841                    return Ok(None);
842                };
843                let aw = self.whnf(arg)?;
844                if self.constructor_view(aw)?.is_none() {
845                    return Ok(None);
846                }
847                let unrolled = self.s.instantiate1(body, head);
848                Ok(Some(self.s.mk_app_n(unrolled, &args)))
849            }
850            Node::MutualFix(ds, index) => {
851                // A mutual block unrolls on its first argument, as the `Term` kernel's does.
852                let Some(arg) = args.first().copied() else {
853                    return Ok(None);
854                };
855                let aw = self.whnf(arg)?;
856                if self.constructor_view(aw)?.is_none() {
857                    return Ok(None);
858                }
859                let bodies = self.s.seq_items(ds);
860                let members: Vec<ExprId> = (0..bodies.len() as u32)
861                    .map(|i| self.s.intern(Node::MutualFix(ds, i)))
862                    .collect();
863                let unrolled = self.s.instantiate_rev(bodies[index as usize], &members);
864                Ok(Some(self.s.mk_app_n(unrolled, &args)))
865            }
866            _ => Ok(None),
867        }
868    }
869
870    /// The column the guard recursed on for this fixpoint.
871    fn structural_column(&mut self, fix: ExprId) -> KResult<Option<usize>> {
872        if let Some(c) = self.logos.as_ref().and_then(|l| l.columns.get(&fix)) {
873            return Ok(*c);
874        }
875        let Term::Fix { body, .. } = lift(&self.s, fix) else {
876            unreachable!("a single fixpoint")
877        };
878        let col = crate::termination::structural_columns(self.logos_ctx(), &body)
879            .first()
880            .copied();
881        self.logos
882            .as_mut()
883            .expect("LOGOS state")
884            .columns
885            .insert(fix, col);
886        Ok(col)
887    }
888
889    /// A builtin computation rule of LOGOS's (an integer or text operation, the validated `Nat`
890    /// order, the reflection embedding), applied by the `Term` kernel's reducer to the lifted
891    /// application.
892    pub(crate) fn reduce_builtin(&mut self, e: ExprId) -> KResult<Option<ExprId>> {
893        let Some((n, _)) = self.s.const_head(e) else {
894            return Ok(None);
895        };
896        if self.s.app_num_args(e) == 0 {
897            return Ok(None);
898        }
899        let name = self.s.names.display(n);
900        let ctx = self.logos_ctx();
901        if !(crate::reduction::computes_by_name(&name)
902            || crate::reduction::accelerates_nat_order(ctx, &name))
903        {
904            return Ok(None);
905        }
906        if let Some(r) = self.logos.as_ref().and_then(|l| l.builtins.get(&e)) {
907            return Ok(*r);
908        }
909        let term = lift(&self.s, e);
910        let reduced = crate::reduction::normalize(self.logos_ctx(), &term);
911        let r = if reduced == term {
912            None
913        } else {
914            Some(lower_open(&mut self.s, &reduced).map_err(|e| {
915                KernelFailure::reject(format!("a builtin's answer is not a kernel term: {e:?}"))
916            })?)
917        };
918        self.logos
919            .as_mut()
920            .expect("LOGOS state")
921            .builtins
922            .insert(e, r);
923        Ok(r)
924    }
925
926    /// A program the `Term` kernel's evaluator certifies — an arithmetic operation, a `Nat` order
927    /// or equality, a division program on a pair (`reduction::certified_program`) — applied to
928    /// operands whose weak-head normal forms are literals is its value, computed by the evaluator's
929    /// own arithmetic (`reduction::certified_application`). The operands are this kernel's to read;
930    /// the certificate is the evaluator's, so the two kernels compute exactly the same programs. A
931    /// program is certified only over the canonical `Nat` (`reduction::program_shape`), so the
932    /// literals read here are its numerals.
933    pub(crate) fn reduce_certified_program(&mut self, e: ExprId) -> KResult<Option<ExprId>> {
934        let Some((name, levels)) = self.s.const_head(e) else {
935            return Ok(None);
936        };
937        let arity = self.s.app_num_args(e);
938        if levels != LevelsId::EMPTY || !(1..=2).contains(&arity) {
939            return Ok(None);
940        }
941        let Some(certified) = self.certified_program_of(name, arity) else {
942            return Ok(None);
943        };
944        let (shape, program) = &*certified;
945        let (_, args) = self.s.app_args(e);
946        let (x, y) = match shape {
947            ProgramShape::Binary => (args[0], args[1]),
948            ProgramShape::Pair { pair, parameters } => {
949                let parameters = *parameters;
950                let observed = self.whnf(args[0])?;
951                let (constructor, fields) = self.s.app_args(observed);
952                let Node::Const(c, _) = self.s.node(constructor) else {
953                    return Ok(None);
954                };
955                if fields.len() != parameters + 2 || self.s.names.display(c) != *pair {
956                    return Ok(None);
957                }
958                (fields[parameters], fields[parameters + 1])
959            }
960        };
961        let Some((x, y)) = self.nat_bin_args(x, y)? else {
962            return Ok(None);
963        };
964        let Some(value) =
965            crate::reduction::certified_application(program, &x.to_bigint(), &y.to_bigint())
966        else {
967            return Ok(None);
968        };
969        lower_open(&mut self.s, &value).map(Some).map_err(|e| {
970            KernelFailure::reject(format!(
971                "a certified program's value is not a kernel term: {e:?}"
972            ))
973        })
974    }
975
976    /// What `name` applied to `arity` operands is certified to compute, and how its operands are
977    /// passed: a division program takes one pair, every other program two literals.
978    fn certified_program_of(
979        &mut self,
980        name: super::name::NameId,
981        arity: usize,
982    ) -> Option<Rc<(ProgramShape, CertifiedProgram)>> {
983        if let Some(known) = self
984            .logos
985            .as_ref()
986            .and_then(|l| l.programs.get(&(name, arity)))
987        {
988            return known.clone();
989        }
990        let spelled = self.s.names.display(name);
991        let ctx = self.logos_ctx();
992        let known = crate::reduction::program_shape(ctx, &spelled, arity).and_then(|shape| {
993            let program = crate::reduction::certified_program(ctx, &spelled)?;
994            let division = matches!(program, CertifiedProgram::Division { .. });
995            (division == matches!(shape, ProgramShape::Pair { .. }))
996                .then(|| Rc::new((shape, program)))
997        });
998        self.logos
999            .as_mut()
1000            .expect("LOGOS state")
1001            .programs
1002            .insert((name, arity), known.clone());
1003        known
1004    }
1005
1006    pub(crate) fn record_irrelevant_levels(&mut self, name: super::name::NameId, mask: u64) {
1007        if mask != 0 {
1008            self.logos
1009                .as_mut()
1010                .expect("LOGOS state")
1011                .irrelevant
1012                .insert(name, mask);
1013        }
1014    }
1015
1016    /// Cumulative inductive types: two applications of one inductive or constructor, its
1017    /// parameters supplied, at instances that differ only in irrelevant universe parameters,
1018    /// are equal when their arguments are.
1019    fn cumulative_instances(&mut self, t: ExprId, s: ExprId) -> KResult<bool> {
1020        let (Some((n, tl)), Some((m, sl))) = (self.s.const_head(t), self.s.const_head(s)) else {
1021            return Ok(false);
1022        };
1023        if n != m || tl == sl {
1024            return Ok(false);
1025        }
1026        let Some(mask) = self
1027            .logos
1028            .as_ref()
1029            .and_then(|l| l.irrelevant.get(&n))
1030            .copied()
1031        else {
1032            return Ok(false);
1033        };
1034        let num_params = match self.env.get(n).map(|c| &c.kind) {
1035            Some(Kind::Induct(info)) => info.num_params,
1036            Some(Kind::Ctor(ci)) => ci.num_params,
1037            _ => return Ok(false),
1038        };
1039        let (a, b) = (
1040            self.s.levels.get_list(tl).to_vec(),
1041            self.s.levels.get_list(sl).to_vec(),
1042        );
1043        if a.len() != b.len() {
1044            return Ok(false);
1045        }
1046        for (i, (u, v)) in a.iter().zip(&b).enumerate() {
1047            let irrelevant = i < 64 && (mask >> i) & 1 == 1;
1048            if !irrelevant && !self.s.levels.equiv(*u, *v) {
1049                return Ok(false);
1050            }
1051        }
1052        let (_, targs) = self.s.app_args(t);
1053        let (_, sargs) = self.s.app_args(s);
1054        if targs.len() != sargs.len() || targs.len() < num_params as usize {
1055            return Ok(false);
1056        }
1057        for (x, y) in targs.iter().zip(&sargs) {
1058            if !self.is_def_eq(*x, *y)? {
1059                return Ok(false);
1060            }
1061        }
1062        Ok(true)
1063    }
1064
1065    /// An `Int` literal's constructor form, `Int_ofNat n` or `Int_negSucc n`, when `Int` is the
1066    /// canonical family: a literal is a compact spelling of a constructor value, not a second
1067    /// carrier.
1068    pub(super) fn int_constructor_form(&mut self, e: ExprId) -> Option<ExprId> {
1069        use logicaffeine_base::BigInt;
1070        let Node::Lit(l) = self.s.node(e) else {
1071            return None;
1072        };
1073        let value = match self.s.lit_value(l) {
1074            Lit::Int(i) => BigInt::from_i64(*i),
1075            Lit::BigInt(b) => b.clone(),
1076            _ => return None,
1077        };
1078        if !self.logos.as_ref()?.numerals.int {
1079            return None;
1080        }
1081        let (ctor, field) = if value.is_negative() {
1082            ("Int_negSucc", value.abs().sub(&BigInt::from_i64(1)))
1083        } else {
1084            ("Int_ofNat", value)
1085        };
1086        let c = self.global(ctor);
1087        let n = self.s.nat_lit(super::nat::Nat::from_bigint(&field));
1088        Some(self.s.app(c, n))
1089    }
1090
1091    /// LOGOS's conversion rules beyond the shared algorithm, on two weak-head normal forms it
1092    /// could not relate: an `Int` literal is its constructor form, and a stuck `match`, a `fix`
1093    /// and a mutual `fix` are related by congruence. `false` is "not shown equal", never a
1094    /// refutation.
1095    /// Structure η as LOGOS states it (`type_checker::try_structure_eta`): `s`, a fully applied
1096    /// constructor of a structure the context registers, against `t`, not headed by that constructor,
1097    /// field by field against the structure's projection applied to the parameters and `t`. LOGOS
1098    /// writes a projection as a definition by `match`, never as a projection node.
1099    pub(crate) fn logos_eta_struct(&mut self, t: ExprId, s: ExprId) -> KResult<bool> {
1100        let Some((f, ls)) = self.s.const_head(s) else {
1101            return Ok(false);
1102        };
1103        if self.s.const_head(t).is_some_and(|(g, _)| g == f) {
1104            return Ok(false);
1105        }
1106        let constructor = super::logos::component(&self.s, f);
1107        let Some(state) = self.logos.as_ref() else {
1108            return Ok(false);
1109        };
1110        let Some((_, info)) = state.ctx.struct_of_constructor(&constructor) else {
1111            return Ok(false);
1112        };
1113        let (num_params, projections) = (info.num_params, info.projections.clone());
1114        let (_, args) = self.s.app_args(s);
1115        if args.len() != num_params + projections.len() {
1116            return Ok(false);
1117        }
1118        for (i, projection) in projections.iter().enumerate() {
1119            let p = self.s.names.str(super::name::NameId::ANON, projection);
1120            if self.env.get(p).is_none() {
1121                return Ok(false);
1122            }
1123            let head = self.s.cnst(p, ls);
1124            let applied = self.s.mk_app_n(head, &args[..num_params]);
1125            let applied = self.s.app(applied, t);
1126            if !self.is_def_eq(args[num_params + i], applied)? {
1127                return Ok(false);
1128            }
1129        }
1130        Ok(true)
1131    }
1132
1133    pub(crate) fn logos_def_eq(&mut self, t: ExprId, s: ExprId) -> KResult<bool> {
1134        if self.cumulative_instances(t, s)? {
1135            return Ok(true);
1136        }
1137        if let Some(tc) = self.int_constructor_form(t) {
1138            let sc = self.int_constructor_form(s).unwrap_or(s);
1139            return self.is_def_eq(tc, sc);
1140        }
1141        if let Some(sc) = self.int_constructor_form(s) {
1142            return self.is_def_eq(t, sc);
1143        }
1144        match (self.s.node(t), self.s.node(s)) {
1145            (Node::Match(d1, m1, c1), Node::Match(d2, m2, c2)) => {
1146                let (cs1, cs2) = (self.s.seq_items(c1), self.s.seq_items(c2));
1147                if cs1.len() != cs2.len() || !self.is_def_eq(d1, d2)? || !self.is_def_eq(m1, m2)? {
1148                    return Ok(false);
1149                }
1150                for (a, b) in cs1.iter().zip(&cs2) {
1151                    if !self.is_def_eq(*a, *b)? {
1152                        return Ok(false);
1153                    }
1154                }
1155                Ok(true)
1156            }
1157            (Node::Fix(b1), Node::Fix(b2)) => {
1158                let ty = self.infer(t, true)?;
1159                let x = self.push_local(ty, None);
1160                let (o1, o2) = (self.s.instantiate1(b1, x), self.s.instantiate1(b2, x));
1161                self.is_def_eq(o1, o2)
1162            }
1163            (Node::MutualFix(ds1, i1), Node::MutualFix(ds2, i2)) if i1 == i2 => {
1164                let (bs1, bs2) = (self.s.seq_items(ds1), self.s.seq_items(ds2));
1165                if bs1.len() != bs2.len() {
1166                    return Ok(false);
1167                }
1168                let mut xs = Vec::with_capacity(bs1.len());
1169                for b in bs1.iter() {
1170                    let ty = self.fix_type(*b)?;
1171                    xs.push(self.push_local(ty, None));
1172                }
1173                for (a, b) in bs1.iter().zip(&bs2) {
1174                    let (oa, ob) = (
1175                        self.s.instantiate_rev(*a, &xs),
1176                        self.s.instantiate_rev(*b, &xs),
1177                    );
1178                    if !self.is_def_eq(oa, ob)? {
1179                        return Ok(false);
1180                    }
1181                }
1182                Ok(true)
1183            }
1184            _ => Ok(false),
1185        }
1186    }
1187
1188    /// `a ≤ b`: cumulative universes, covariant in a function type's codomain, otherwise
1189    /// definitional equality.
1190    pub(crate) fn is_subtype(&mut self, a: ExprId, b: ExprId) -> KResult<bool> {
1191        if a == b {
1192            return Ok(true);
1193        }
1194        let (aw, bw) = (self.whnf(a)?, self.whnf(b)?);
1195        match (self.s.node(aw), self.s.node(bw)) {
1196            (Node::Sort(u), Node::Sort(v)) => Ok(self.s.levels.leq(u, v)),
1197            (Node::Pi(d1, c1), Node::Pi(d2, c2)) => {
1198                if !self.is_def_eq(d1, d2)? {
1199                    return Ok(false);
1200                }
1201                let x = self.push_local(d2, None);
1202                let (c1, c2) = (self.s.instantiate1(c1, x), self.s.instantiate1(c2, x));
1203                self.is_subtype(c1, c2)
1204            }
1205            _ => self.is_def_eq(aw, bw),
1206        }
1207    }
1208}