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}