Skip to main content

logicaffeine_kernel/
term.rs

1//! Unified term representation for the Calculus of Constructions.
2//!
3//! In CoC, there is no distinction between terms and types.
4//! Everything is a Term in an infinite hierarchy of universes.
5
6use std::fmt;
7
8/// Primitive literal values.
9///
10/// These are opaque values that compute via hardware ALU, not recursion.
11#[derive(Debug, Clone, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum Literal {
14    /// 64-bit signed integer
15    Int(i64),
16    /// 64-bit floating point
17    Float(f64),
18    /// UTF-8 string
19    Text(String),
20    /// Duration in nanoseconds (signed for negative offsets like "5 min early")
21    Duration(i64),
22    /// Calendar date as days since Unix epoch (i32 gives ±5.8 million year range)
23    Date(i32),
24    /// Instant in time as nanoseconds since Unix epoch (UTC)
25    Moment(i64),
26    /// Arbitrary-precision integer — the `Int` values that overflow `i64` (K6). This is a
27    /// PARALLEL representation, appended so existing certificates' `Int`/`Float`/… encoding
28    /// is byte-unchanged. A `BigInt` is CANONICAL: it holds only values `to_i64()` cannot,
29    /// so every integer has a unique `Literal` (small → `Int`, huge → `BigInt`) and
30    /// definitional equality stays sound. Serialized as a decimal string, stable across
31    /// `BigInt`'s internal limb layout. Produced by `int_lit`; never constructed directly
32    /// for a value that fits `i64`.
33    BigInt(
34        #[cfg_attr(feature = "serde", serde(with = "bigint_dec"))] logicaffeine_base::BigInt,
35    ),
36    /// Arbitrary-precision NATURAL-number literal — a compact, accelerated form of the
37    /// unary Peano numeral `Succ^n Zero` (K6). `Nat(n)` is DEFINITIONALLY EQUAL to `Succ`
38    /// applied `n` times to `Zero`: the kernel bridges the two in `extract_constructor`
39    /// (so a `match`/recursor computes on it, peeling one `Succ` per step) and in `def_eq`
40    /// (so `Nat(n)` and `Succ^n Zero` are interchangeable), in BOTH kernels. It stores the
41    /// count as one `BigInt` instead of `n` heap nodes. Serialized as a decimal string;
42    /// the value is non-negative.
43    Nat(#[cfg_attr(feature = "serde", serde(with = "bigint_dec"))] logicaffeine_base::BigInt),
44}
45
46/// The canonical `Literal` for an integer: `Int(i64)` when it fits (the fast, common path),
47/// otherwise the arbitrary-precision `BigInt`. This is the ONLY sanctioned way to build an
48/// integer literal from a `BigInt` result, guaranteeing the one-representation-per-value
49/// invariant on which definitional equality of literals rests.
50pub fn int_lit(n: logicaffeine_base::BigInt) -> Literal {
51    match n.to_i64() {
52        Some(x) => Literal::Int(x),
53        None => Literal::BigInt(n),
54    }
55}
56
57/// The `BigInt` value of an integer literal, promoting a machine `Int` — for arithmetic
58/// that must run in arbitrary precision. `None` for non-integer literals.
59pub fn lit_bigint(lit: &Literal) -> Option<logicaffeine_base::BigInt> {
60    match lit {
61        Literal::Int(x) => Some(logicaffeine_base::BigInt::from_i64(*x)),
62        Literal::BigInt(n) => Some(n.clone()),
63        _ => None,
64    }
65}
66
67/// Serialize a [`logicaffeine_base::BigInt`] as its decimal string — a representation
68/// independent of the internal limb layout, so certificates stay portable and stable.
69#[cfg(feature = "serde")]
70mod bigint_dec {
71    use logicaffeine_base::BigInt;
72
73    pub fn serialize<S: serde::Serializer>(v: &BigInt, s: S) -> Result<S::Ok, S::Error> {
74        s.serialize_str(&v.to_string())
75    }
76
77    pub fn deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result<BigInt, D::Error> {
78        let s = <String as serde::Deserialize>::deserialize(d)?;
79        BigInt::parse_decimal(&s).ok_or_else(|| serde::de::Error::custom("invalid BigInt literal"))
80    }
81}
82
83impl Eq for Literal {}
84
85impl fmt::Display for Literal {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        match self {
88            Literal::Int(n) => write!(f, "{}", n),
89            Literal::Float(x) => write!(f, "{}", x),
90            Literal::Text(s) => write!(f, "{:?}", s),
91            Literal::Duration(nanos) => {
92                // Display in most human-readable unit
93                let abs = nanos.unsigned_abs();
94                let sign = if *nanos < 0 { "-" } else { "" };
95                if abs >= 3_600_000_000_000 {
96                    write!(f, "{}{}h", sign, abs / 3_600_000_000_000)
97                } else if abs >= 60_000_000_000 {
98                    write!(f, "{}{}min", sign, abs / 60_000_000_000)
99                } else if abs >= 1_000_000_000 {
100                    write!(f, "{}{}s", sign, abs / 1_000_000_000)
101                } else if abs >= 1_000_000 {
102                    write!(f, "{}{}ms", sign, abs / 1_000_000)
103                } else if abs >= 1_000 {
104                    write!(f, "{}{}μs", sign, abs / 1_000)
105                } else {
106                    write!(f, "{}{}ns", sign, abs)
107                }
108            }
109            Literal::Date(days) => {
110                // Convert days since epoch to ISO-8601 date
111                // Unix epoch is 1970-01-01 (day 0)
112                // We use a simple algorithm for display purposes
113                let days = *days as i64;
114                let (year, month, day) = days_to_ymd(days);
115                write!(f, "{:04}-{:02}-{:02}", year, month, day)
116            }
117            Literal::Moment(nanos) => {
118                // Convert to ISO-8601 datetime
119                let secs = nanos / 1_000_000_000;
120                let days = secs / 86400;
121                let time_secs = secs % 86400;
122                let hours = time_secs / 3600;
123                let mins = (time_secs % 3600) / 60;
124                let secs_rem = time_secs % 60;
125                let (year, month, day) = days_to_ymd(days);
126                write!(f, "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
127                       year, month, day, hours, mins, secs_rem)
128            }
129            Literal::BigInt(n) => write!(f, "{}", n),
130            Literal::Nat(n) => write!(f, "{}", n),
131        }
132    }
133}
134
135/// Convert days since Unix epoch to (year, month, day).
136fn days_to_ymd(days: i64) -> (i64, u8, u8) {
137    // Civil date from days since epoch using the algorithm from Howard Hinnant
138    // https://howardhinnant.github.io/date_algorithms.html
139    let z = days + 719468;
140    let era = if z >= 0 { z / 146097 } else { (z - 146096) / 146097 };
141    let doe = (z - era * 146097) as u32;
142    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
143    let y = yoe as i64 + era * 400;
144    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
145    let mp = (5 * doy + 2) / 153;
146    let d = doy - (153 * mp + 2) / 5 + 1;
147    let m = if mp < 10 { mp + 3 } else { mp - 9 };
148    let year = if m <= 2 { y + 1 } else { y };
149    (year, m as u8, d as u8)
150}
151
152/// Universe levels in the type hierarchy — a level EXPRESSION, so the kernel can be
153/// universe-POLYMORPHIC (R3). The concrete hierarchy is `Prop : Type 1 : Type 2 : …`
154/// with `Prop ≤ Type i`; on top of it, a level may mention universe VARIABLES, so one
155/// definition (`id.{u} : Π(A : Sort u). A → A`) is reusable at every level instead of
156/// duplicated per level.
157///
158/// - `Prop` is the universe of propositions (the impredicative bottom; `Prop ≤` all)
159/// - `Type(n)` is the concrete universe at level n
160/// - `Var(u)` is a universe variable (ranges over `Type` levels, `≥ Type 0`)
161/// - `Succ(ℓ)` is `ℓ + 1`
162/// - `Max(ℓ₁, ℓ₂)` is the least upper bound (used in Π-type formation)
163///
164/// The algebra (`succ`/`max`/`equiv`/`is_subtype_of`) is decided over a canonical
165/// normal form, NOT by the derived structural equality — `max(u,u) ≡ u`,
166/// `max(succ u, u) ≡ succ u`, etc.
167#[derive(Debug, Clone, PartialEq, Eq)]
168#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
169pub enum Universe {
170    /// SProp — the DEFINITIONALLY-proof-irrelevant sort (S). The bottom of the hierarchy
171    /// (`SProp ≤ Prop ≤ Type n`): any two terms of a type in `SProp` are definitionally
172    /// equal, and it is impredicative (`Π` into `SProp` is `SProp`). It collapses out of
173    /// `max`/`imax`/`succ` so the `Prop=0` level encoding is never disturbed.
174    SProp,
175    /// Prop - the universe of propositions
176    Prop,
177    /// Type n - the universe of types at level n
178    Type(u32),
179    /// A universe variable (universe polymorphism).
180    Var(String),
181    /// The successor level `ℓ + 1`.
182    Succ(Box<Universe>),
183    /// The least upper bound of two levels.
184    Max(Box<Universe>, Box<Universe>),
185    /// The IMPREDICATIVE maximum, `imax(a, b)` = `b` if `b` is `Prop`, else
186    /// `max(a, b)`. It is the sort of `Π(x:A). B` where `A : Sort a`, `B : Sort b`:
187    /// a Π into a proposition is a proposition (Prop is impredicative), no matter
188    /// the domain. When `b` is a variable this stays symbolic — the level may be
189    /// `Prop` or not depending on the instantiation.
190    IMax(Box<Universe>, Box<Universe>),
191}
192
193impl Universe {
194    /// Get the successor universe: `Type n → Type (n+1)`, `Prop → Type 1`, and a
195    /// symbolic `Succ(ℓ)` for a level that mentions variables.
196    pub fn succ(&self) -> Universe {
197        match self {
198            Universe::SProp | Universe::Prop => Universe::Type(1),
199            Universe::Type(n) => Universe::Type(n + 1),
200            other => Universe::Succ(Box::new(other.clone())),
201        }
202    }
203
204    /// Get the maximum of two universes (for Pi type formation). Concrete operands
205    /// collapse immediately; otherwise a symbolic `Max(…)` is formed (its algebra is
206    /// resolved by `normalize`).
207    pub fn max(&self, other: &Universe) -> Universe {
208        match (self, other) {
209            // SProp and Prop are absorbed (they are `≤` everything), SProp first (it is `≤ Prop`).
210            (Universe::SProp, u) | (u, Universe::SProp) => u.clone(),
211            (Universe::Prop, u) | (u, Universe::Prop) => u.clone(),
212            (Universe::Type(a), Universe::Type(b)) => Universe::Type((*a).max(*b)),
213            _ => Universe::Max(Box::new(self.clone()), Box::new(other.clone())),
214        }
215    }
216
217    /// The impredicative maximum `imax(a, b)` — the sort of a `Π` whose codomain
218    /// lives in `b`. Collapses when `b`'s Prop-ness is known: `imax(a, Prop) = Prop`,
219    /// `imax(a, Type n) = max(a, Type n)`; `imax(a, a) = a`. Otherwise (a variable or
220    /// other symbolic `b`) it stays a symbolic `IMax`, whose algebra `equiv`/
221    /// `is_subtype_of` decide by case-splitting on whether `b` is `Prop`.
222    pub fn imax(&self, other: &Universe) -> Universe {
223        match other {
224            // A Π into an S-proposition is an S-proposition (impredicative SProp).
225            Universe::SProp => Universe::SProp,
226            // A Π into a proposition is a proposition.
227            Universe::Prop => Universe::Prop,
228            // A concrete non-Prop codomain: the Π is predicative.
229            Universe::Type(_) | Universe::Succ(_) => self.max(other),
230            _ => {
231                if self.equiv(other) {
232                    // imax(a, a) = a (a = 0 ⇒ 0; a ≥ 1 ⇒ max(a,a) = a).
233                    self.clone()
234                } else {
235                    Universe::IMax(Box::new(self.clone()), Box::new(other.clone()))
236                }
237            }
238        }
239    }
240
241    /// Cumulative subtyping `self ≤ other`, decided over the level normal form. Sound
242    /// for variables: `u ≤ u`, `Type 0 ≤ u`, `u ≤ succ u` hold, but `Type 1 ≤ u` and
243    /// `u ≤ v` do NOT (they fail for some instantiation).
244    pub fn is_subtype_of(&self, other: &Universe) -> bool {
245        level_leq(self, other)
246    }
247
248    /// Definitional equality of two level expressions, accounting for the algebra
249    /// (`max`, `imax`, `succ`, variables) — NOT the derived structural equality.
250    /// Decided as mutual subtyping over all variable instantiations.
251    pub fn equiv(&self, other: &Universe) -> bool {
252        level_leq(self, other) && level_leq(other, self)
253    }
254
255    /// Substitute universe variables (replacing each `Var(v)` by `subst[v]`) throughout
256    /// this level expression — the heart of instantiating a universe-polymorphic global.
257    pub fn substitute(&self, subst: &std::collections::HashMap<String, Universe>) -> Universe {
258        match self {
259            Universe::SProp | Universe::Prop | Universe::Type(_) => self.clone(),
260            Universe::Var(v) => subst.get(v).cloned().unwrap_or_else(|| self.clone()),
261            Universe::Succ(l) => l.substitute(subst).succ(),
262            Universe::Max(a, b) => a.substitute(subst).max(&b.substitute(subst)),
263            Universe::IMax(a, b) => a.substitute(subst).imax(&b.substitute(subst)),
264        }
265    }
266}
267
268/// A level in the ℕ-ENCODING used by the decision core: `Prop = 0`, `Type n = n+1`,
269/// and every universe VARIABLE ranges over all of ℕ (so a variable may be `Prop`,
270/// which is what closes the `Sort u := Nat` unsoundness — `Type 0 ≤ u` is false, since
271/// `u` could be `Prop`). This mirrors Lean's level model.
272#[derive(Clone, Debug)]
273enum LNat {
274    Const(u64),
275    Var(String),
276    Succ(Box<LNat>),
277    Max(Box<LNat>, Box<LNat>),
278    /// `imax(a, b) = 0` when `b = 0`, else `max(a, b)`.
279    IMax(Box<LNat>, Box<LNat>),
280}
281
282fn to_lnat(u: &Universe) -> LNat {
283    match u {
284        // SProp is handled directly in `level_leq` and never survives inside a compound
285        // level (max/imax/succ collapse it), so this is a never-hit fallback.
286        Universe::SProp => LNat::Const(0),
287        Universe::Prop => LNat::Const(0),
288        Universe::Type(n) => LNat::Const(*n as u64 + 1),
289        Universe::Var(v) => LNat::Var(v.clone()),
290        Universe::Succ(l) => LNat::Succ(Box::new(to_lnat(l))),
291        Universe::Max(a, b) => LNat::Max(Box::new(to_lnat(a)), Box::new(to_lnat(b))),
292        Universe::IMax(a, b) => LNat::IMax(Box::new(to_lnat(a)), Box::new(to_lnat(b))),
293    }
294}
295
296/// Simplify an [`LNat`], resolving every `imax` whose right argument's Prop-ness is
297/// determined and pushing `imax` down over `max`/`imax` until its right argument is a
298/// bare variable (or a constant). After this, an unresolved `imax` has the form
299/// `imax(_, Var v)`, so the remaining case analysis is on those `v`.
300fn simp_lnat(t: &LNat) -> LNat {
301    match t {
302        LNat::Const(_) | LNat::Var(_) => t.clone(),
303        LNat::Succ(l) => LNat::Succ(Box::new(simp_lnat(l))),
304        LNat::Max(a, b) => LNat::Max(Box::new(simp_lnat(a)), Box::new(simp_lnat(b))),
305        LNat::IMax(a, b) => {
306            let a = simp_lnat(a);
307            let b = simp_lnat(b);
308            match &b {
309                // b = 0 ⇒ imax = 0.
310                LNat::Const(0) => LNat::Const(0),
311                // b ≥ 1 (a positive constant or a successor) ⇒ imax = max(a, b).
312                LNat::Const(_) | LNat::Succ(_) => simp_lnat(&LNat::Max(Box::new(a), Box::new(b))),
313                // imax(a, max(b1,b2)) = max(imax(a,b1), imax(a,b2)).
314                LNat::Max(b1, b2) => simp_lnat(&LNat::Max(
315                    Box::new(LNat::IMax(Box::new(a.clone()), b1.clone())),
316                    Box::new(LNat::IMax(Box::new(a), b2.clone())),
317                )),
318                // imax(a, imax(b1,b2)) = imax(max(a,b1), b2).
319                LNat::IMax(b1, b2) => simp_lnat(&LNat::IMax(
320                    Box::new(LNat::Max(Box::new(a), b1.clone())),
321                    b2.clone(),
322                )),
323                // imax(a, v): stays symbolic (v may be 0 or ≥ 1).
324                LNat::Var(_) => LNat::IMax(Box::new(a), Box::new(b)),
325            }
326        }
327    }
328}
329
330/// A variable that still appears as the right argument of an `imax` — the pivot to
331/// case-split on. `None` when the term is imax-free (a pure `max`/`succ`/`const`/`var`).
332fn imax_pivot(t: &LNat) -> Option<String> {
333    match t {
334        LNat::Const(_) | LNat::Var(_) => None,
335        LNat::Succ(l) => imax_pivot(l),
336        LNat::Max(a, b) => imax_pivot(a).or_else(|| imax_pivot(b)),
337        LNat::IMax(a, b) => match &**b {
338            LNat::Var(v) => Some(v.clone()),
339            _ => imax_pivot(a).or_else(|| imax_pivot(b)),
340        },
341    }
342}
343
344/// Substitute `Var(v)` by `repl` throughout an [`LNat`].
345fn subst_lnat(t: &LNat, v: &str, repl: &LNat) -> LNat {
346    match t {
347        LNat::Const(_) => t.clone(),
348        LNat::Var(x) => {
349            if x == v {
350                repl.clone()
351            } else {
352                t.clone()
353            }
354        }
355        LNat::Succ(l) => LNat::Succ(Box::new(subst_lnat(l, v, repl))),
356        LNat::Max(a, b) => {
357            LNat::Max(Box::new(subst_lnat(a, v, repl)), Box::new(subst_lnat(b, v, repl)))
358        }
359        LNat::IMax(a, b) => {
360            LNat::IMax(Box::new(subst_lnat(a, v, repl)), Box::new(subst_lnat(b, v, repl)))
361        }
362    }
363}
364
365/// A flat atom of an imax-free level: `var + offset` (`var = None` ⇒ a constant
366/// `offset`). A level is a `max` of atoms.
367fn lnat_atoms(t: &LNat, off: u64, out: &mut Vec<(Option<String>, u64)>) {
368    match t {
369        LNat::Const(c) => out.push((None, c + off)),
370        LNat::Var(v) => out.push((Some(v.clone()), off)),
371        LNat::Succ(l) => lnat_atoms(l, off + 1, out),
372        LNat::Max(a, b) => {
373            lnat_atoms(a, off, out);
374            lnat_atoms(b, off, out);
375        }
376        // An imax-free term never contains IMax (guaranteed by the pivot loop).
377        LNat::IMax(..) => unreachable!("lnat_atoms on an unresolved imax"),
378    }
379}
380
381/// Decide `a ≤ b` for an IMAX-FREE pair, over ALL variable assignments (each variable
382/// ranges over ℕ ≥ 0). `max(A) ≤ max(B)` holds iff every atom of `A` is dominated by
383/// `B`: a constant `c` needs `c ≤ min max(B)` (all variables at 0); a `var+off` atom
384/// needs `B` to contain the SAME variable with offset `≥ off` (else driving it to ∞
385/// breaks the bound).
386fn leq_linear(a: &LNat, b: &LNat) -> bool {
387    let mut atoms_a = Vec::new();
388    lnat_atoms(a, 0, &mut atoms_a);
389    let mut atoms_b = Vec::new();
390    lnat_atoms(b, 0, &mut atoms_b);
391    // `min max(B)`: every variable at 0, so each atom contributes its offset.
392    let b_min = atoms_b.iter().map(|(_, off)| *off).max().unwrap_or(0);
393    atoms_a.iter().all(|(v, off)| match v {
394        None => *off <= b_min,
395        Some(name) => atoms_b
396            .iter()
397            .any(|(bv, boff)| bv.as_deref() == Some(name.as_str()) && *boff >= *off),
398    })
399}
400
401/// Decide cumulative `a ≤ b`, SOUND over all instantiations of the universe variables
402/// (each ranges over ℕ, `Prop = 0`). Unresolved `imax(_, v)` is handled by splitting
403/// `v` into the `Prop` case (`v := 0`) and the positive case (`v := succ v′`); each
404/// split removes a pivot, so the recursion terminates.
405fn level_leq(a: &Universe, b: &Universe) -> bool {
406    // SProp is the bottom sort — `SProp ≤ everything`, and only `SProp ≤ SProp`. Handled
407    // BEFORE the `Prop=0` level encoding, which never sees `SProp` (max/imax/succ collapse
408    // it away, so it only ever appears bare here).
409    if matches!(a, Universe::SProp) {
410        return true;
411    }
412    if matches!(b, Universe::SProp) {
413        return false;
414    }
415    lnat_leq(&simp_lnat(&to_lnat(a)), &simp_lnat(&to_lnat(b)))
416}
417
418fn lnat_leq(a: &LNat, b: &LNat) -> bool {
419    match imax_pivot(a).or_else(|| imax_pivot(b)) {
420        Some(v) => {
421            // v = Prop (0).
422            let zero = LNat::Const(0);
423            let a0 = simp_lnat(&subst_lnat(a, &v, &zero));
424            let b0 = simp_lnat(&subst_lnat(b, &v, &zero));
425            // v ≥ 1: v := succ(v′) for a fresh v′ (bakes the `≥ 1` into an offset).
426            let vpos = LNat::Succ(Box::new(LNat::Var(format!("{v}✦"))));
427            let ap = simp_lnat(&subst_lnat(a, &v, &vpos));
428            let bp = simp_lnat(&subst_lnat(b, &v, &vpos));
429            lnat_leq(&a0, &b0) && lnat_leq(&ap, &bp)
430        }
431        None => leq_linear(a, b),
432    }
433}
434
435/// Unified term representation.
436///
437/// Every expression in CoC is a Term:
438/// - `Sort(u)` - universes (Type 0, Type 1, Prop)
439/// - `Var(x)` - variables
440/// - `Pi` - dependent function types: Π(x:A). B
441/// - `Lambda` - functions: λ(x:A). t
442/// - `App` - application: f x
443#[derive(Debug, Clone, PartialEq, Eq)]
444#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
445pub enum Term {
446    /// Universe: Type n or Prop
447    Sort(Universe),
448
449    /// Local variable reference (bound by λ or Π)
450    Var(String),
451
452    /// Global definition (inductive type or constructor)
453    Global(String),
454
455    /// A universe-polymorphic global referenced at explicit levels — `name.{ℓ₀, ℓ₁, …}`.
456    /// `Global` is the monomorphic case (no level arguments); `Const` instantiates a
457    /// stored universe-polymorphic definition's universe parameters with `levels`.
458    Const { name: String, levels: Vec<Universe> },
459
460    /// Dependent function type: Π(x:A). B
461    ///
462    /// When B doesn't mention x, this is just A → B.
463    /// When B mentions x, this is a dependent type.
464    Pi {
465        param: String,
466        param_type: Box<Term>,
467        body_type: Box<Term>,
468    },
469
470    /// Lambda abstraction: λ(x:A). t
471    Lambda {
472        param: String,
473        param_type: Box<Term>,
474        body: Box<Term>,
475    },
476
477    /// Application: f x
478    App(Box<Term>, Box<Term>),
479
480    /// Pattern matching on inductive types.
481    ///
482    /// `match discriminant return motive with cases`
483    ///
484    /// - discriminant: the term being matched (must have inductive type)
485    /// - motive: λx:I. T — describes the return type
486    /// - cases: one case per constructor, in definition order
487    Match {
488        discriminant: Box<Term>,
489        motive: Box<Term>,
490        cases: Vec<Term>,
491    },
492
493    /// Fixpoint (recursive function).
494    ///
495    /// `fix name. body` binds `name` to itself within `body`.
496    /// Used for recursive definitions like addition.
497    Fix {
498        /// Name for self-reference within the body
499        name: String,
500        /// The body of the fixpoint (typically a lambda)
501        body: Box<Term>,
502    },
503
504    /// MUTUAL fixpoint — a block of mutually-recursive functions (K3).
505    ///
506    /// `mutualfix { f₀ := b₀, …, fₙ := bₙ }.index` binds ALL of `f₀ … fₙ` within EVERY
507    /// body `bᵢ` (that is the mutual part), and the whole term reduces to the
508    /// `index`-th definition. It is the body of a mutual inductive block's recursor:
509    /// `Even.rec`'s fixpoint calls `Odd.rec`'s on the smaller sub-proof and vice
510    /// versa. Each body's type is inferred structurally from its λ-telescope (like the
511    /// single `Fix`); termination is the MUTUAL Giménez guard — a call to ANY member
512    /// must pass a structurally-smaller argument at that member's recursive position.
513    MutualFix {
514        /// The mutually-recursive definitions, `(name, body)`, in block order. Every
515        /// name is in scope in every body.
516        defs: Vec<(String, Term)>,
517        /// Which definition this occurrence denotes (and reduces to).
518        index: usize,
519    },
520
521    /// Local definition: `let name : ty := value in body`.
522    ///
523    /// `name` is bound to `value` (of type `ty`) transparently within `body` —
524    /// so the body is type-checked and reduced with `name ≡ value` (ZETA), not
525    /// as an opaque hypothesis. The surface `let`; the sharing seam for the
526    /// elaborator.
527    Let {
528        name: String,
529        ty: Box<Term>,
530        value: Box<Term>,
531        body: Box<Term>,
532    },
533
534    /// Primitive literal value.
535    ///
536    /// Hardware-native values like i64, f64, String.
537    /// These compute via CPU ALU, not recursion.
538    Lit(Literal),
539
540    /// Hole (implicit argument).
541    ///
542    /// Represents an argument that should be inferred by the type checker.
543    /// Used in Literate syntax like `X equals Y` where the type of X/Y is implicit.
544    Hole,
545}
546
547impl fmt::Display for Universe {
548    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549        match self {
550            Universe::SProp => write!(f, "SProp"),
551            Universe::Prop => write!(f, "Prop"),
552            Universe::Type(n) => write!(f, "Type{}", n),
553            Universe::Var(v) => write!(f, "{}", v),
554            Universe::Succ(l) => write!(f, "({}+1)", l),
555            Universe::Max(a, b) => write!(f, "max({}, {})", a, b),
556            Universe::IMax(a, b) => write!(f, "imax({}, {})", a, b),
557        }
558    }
559}
560
561impl fmt::Display for Term {
562    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
563        match self {
564            Term::Sort(u) => write!(f, "{}", u),
565            Term::Var(name) => write!(f, "{}", name),
566            Term::Global(name) => write!(f, "{}", name),
567            Term::Const { name, levels } => {
568                write!(f, "{}.{{", name)?;
569                for (i, l) in levels.iter().enumerate() {
570                    if i > 0 {
571                        write!(f, ", ")?;
572                    }
573                    write!(f, "{}", l)?;
574                }
575                write!(f, "}}")
576            }
577            Term::Pi {
578                param,
579                param_type,
580                body_type,
581            } => {
582                // Use arrow notation for non-dependent functions (param = "_")
583                if param == "_" {
584                    write!(f, "{} -> {}", param_type, body_type)
585                } else {
586                    write!(f, "Π({}:{}). {}", param, param_type, body_type)
587                }
588            }
589            Term::Lambda {
590                param,
591                param_type,
592                body,
593            } => {
594                write!(f, "λ({}:{}). {}", param, param_type, body)
595            }
596            Term::App(func, arg) => {
597                // Arrow types (Pi with _) need inner parens when used as args
598                let arg_needs_inner_parens =
599                    matches!(arg.as_ref(), Term::Pi { param, .. } if param == "_");
600
601                if arg_needs_inner_parens {
602                    write!(f, "({} ({}))", func, arg)
603                } else {
604                    write!(f, "({} {})", func, arg)
605                }
606            }
607            Term::Match {
608                discriminant,
609                motive,
610                cases,
611            } => {
612                write!(f, "match {} return {} with ", discriminant, motive)?;
613                for (i, case) in cases.iter().enumerate() {
614                    if i > 0 {
615                        write!(f, " | ")?;
616                    }
617                    write!(f, "{}", case)?;
618                }
619                Ok(())
620            }
621            Term::Fix { name, body } => {
622                write!(f, "fix {}. {}", name, body)
623            }
624            Term::MutualFix { defs, index } => {
625                write!(f, "mutualfix {{ ")?;
626                for (i, (name, body)) in defs.iter().enumerate() {
627                    if i > 0 {
628                        write!(f, ", ")?;
629                    }
630                    write!(f, "{} := {}", name, body)?;
631                }
632                write!(f, " }}.{}", index)
633            }
634            Term::Let { name, ty, value, body } => {
635                write!(f, "let {}:{} := {} in {}", name, ty, value, body)
636            }
637            Term::Lit(lit) => {
638                write!(f, "{}", lit)
639            }
640            Term::Hole => {
641                write!(f, "_")
642            }
643        }
644    }
645}
646
647/// Instantiate universe variables throughout a term: substitute every `Sort`'s level by
648/// `subst`. This specializes a universe-POLYMORPHIC term (`λA:Sort u. …`) to a concrete
649/// level (`u := Type 0`), yielding an ordinary term the kernel checks as-is — so one
650/// definition is reused at every level instead of duplicated.
651pub fn instantiate_universes(
652    term: &Term,
653    subst: &std::collections::HashMap<String, Universe>,
654) -> Term {
655    match term {
656        Term::Sort(u) => Term::Sort(u.substitute(subst)),
657        Term::Const { name, levels } => Term::Const {
658            name: name.clone(),
659            levels: levels.iter().map(|l| l.substitute(subst)).collect(),
660        },
661        Term::Var(_) | Term::Global(_) | Term::Lit(_) | Term::Hole => term.clone(),
662        Term::Pi { param, param_type, body_type } => Term::Pi {
663            param: param.clone(),
664            param_type: Box::new(instantiate_universes(param_type, subst)),
665            body_type: Box::new(instantiate_universes(body_type, subst)),
666        },
667        Term::Lambda { param, param_type, body } => Term::Lambda {
668            param: param.clone(),
669            param_type: Box::new(instantiate_universes(param_type, subst)),
670            body: Box::new(instantiate_universes(body, subst)),
671        },
672        Term::App(f, a) => Term::App(
673            Box::new(instantiate_universes(f, subst)),
674            Box::new(instantiate_universes(a, subst)),
675        ),
676        Term::Match { discriminant, motive, cases } => Term::Match {
677            discriminant: Box::new(instantiate_universes(discriminant, subst)),
678            motive: Box::new(instantiate_universes(motive, subst)),
679            cases: cases.iter().map(|c| instantiate_universes(c, subst)).collect(),
680        },
681        Term::Fix { name, body } => Term::Fix {
682            name: name.clone(),
683            body: Box::new(instantiate_universes(body, subst)),
684        },
685        Term::MutualFix { defs, index } => Term::MutualFix {
686            defs: defs
687                .iter()
688                .map(|(n, b)| (n.clone(), instantiate_universes(b, subst)))
689                .collect(),
690            index: *index,
691        },
692        Term::Let { name, ty, value, body } => Term::Let {
693            name: name.clone(),
694            ty: Box::new(instantiate_universes(ty, subst)),
695            value: Box::new(instantiate_universes(value, subst)),
696            body: Box::new(instantiate_universes(body, subst)),
697        },
698    }
699}