Skip to main content

logicaffeine_kernel/
observational.rs

1//! Observational equality: `Obs.{u} A a b : SProp`, an equality over every type that computes on
2//! the weak-head normal form of `A`, with `Obs_refl`, `Obs_transp` (its elimination, into `SProp` only)
3//! and `Obs_cast` (a value carried along an equality of types), after Pujet and Tabareau's CC^obs.
4//!
5//! [`register`](crate::observational::register) installs them beside the standard library, with the `SProp` connectives the rules
6//! produce (`STrue`, `SFalse`, `SAnd`, `SSigma`, `SBox` and their projections) and `EqvGen`, the
7//! equivalence closure of a relation, and certifies them: the kernels' computation rules fire only
8//! for the constants the certificate names, and displacing any name it rests on revokes it, as the
9//! standard quotient's certificate is.
10//!
11//! `Obs.{u} A a b`, by the weak-head normal form of `A`:
12//!
13//! | `A` | `a ~ b` is |
14//! |---|---|
15//! | `Π (x : D). C` | `Π (x : D). a x ~[C] b x` |
16//! | `SProp` | `SAnd (a → b) (b → a)` |
17//! | `Prop` | `SAnd (SBox (a → b)) (SBox (b → a))` |
18//! | another universe, `a` and `b` types that are not propositions | by their formers: two universes equal when their levels are, two function types `SSigma (D' ~ D) (λ e. Π (x' : D'). C[Obs_cast D' D e x'] ~ C'[x'])`, one inductive family or quotient former the conjunction of its arguments' equalities, two different formers `SFalse` |
19//! | an inductive family that is not a proposition, `a` and `b` constructors | `SFalse` for two constructors, else the conjunction of the fields' equalities |
20//! | a quotient that is not a proposition, `a` and `b` classes | `SBox (EqvGen A r x y)` of their representatives |
21//!
22//! A conjunction is `STrue` for none and right-nested `SAnd` otherwise; a position whose type mentions
23//! an earlier position compares heterogeneously, `SSigma (T ~ T') (λ e. Obs_cast T T' e x ~ y)`, which
24//! positions are dependent read off the telescope. Three guards keep the rules sound, each locked by
25//! a test that derives `SFalse` without it: two proofs are never compared by their constructors (they
26//! are one by irrelevance); a proposition seen as a type is never compared by its former (`Nat →
27//! False` and `Bool → False` are one proposition); and a family's arguments are compared only when
28//! its universe can store them (Hur: an injective `I : (Type → Type) → Type` refutes excluded
29//! middle). Everything else is stuck.
30//!
31//! `Obs_cast.{u} A B e t` computes where `A ~ B` does: along one universe to `t`; along two function
32//! types to `λ (x' : D'). Obs_cast C[back] C'[x'] (SSigma_snd … e x') (t back)` with `back` the argument
33//! carried back along the domains' equality; along one family without indices whose parameters and
34//! fields depend on no earlier one to the same constructor at the target parameters, each field
35//! carried along an equality of its types built by `Obs_transp`; along one quotient former to the class
36//! of the carried representative. A cast between propositions, along evidence of `SFalse`, along a
37//! stuck type or of a value that is not yet a constructor is stuck.
38
39use crate::context::{Context, MutualInductive};
40use crate::error::KernelResult;
41use crate::term::{Term, Universe};
42use std::collections::BTreeSet;
43
44/// What a certified constant is.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub(crate) enum Kind {
47    Obs,
48    Refl,
49    Transp,
50    Cast,
51}
52
53/// The constants the computation rules fire for, and every name they rest on.
54#[derive(Debug)]
55pub(crate) struct Certificate {
56    pub(crate) constants: Vec<(String, Kind)>,
57    pub(crate) dependencies: BTreeSet<String>,
58}
59
60impl Certificate {
61    pub(crate) fn names(&self) -> impl Iterator<Item = &str> {
62        self.constants.iter().map(|(n, _)| n.as_str())
63    }
64}
65
66/// What `name` is to the certified observational equality of `ctx`, if anything.
67pub(crate) fn kind(ctx: &Context, name: &str) -> Option<Kind> {
68    ctx.observational
69        .as_ref()?
70        .constants
71        .iter()
72        .find(|(n, _)| n == name)
73        .map(|(_, k)| *k)
74}
75
76fn g(name: &str) -> Term {
77    Term::Global(name.to_string())
78}
79
80fn v(name: &str) -> Term {
81    Term::Var(name.to_string())
82}
83
84fn app(f: Term, args: impl IntoIterator<Item = Term>) -> Term {
85    args.into_iter()
86        .fold(f, |f, a| Term::App(Box::new(f), Box::new(a)))
87}
88
89fn pi(param: &str, ty: Term, body: Term) -> Term {
90    Term::Pi {
91        param: param.to_string(),
92        param_type: Box::new(ty),
93        body_type: Box::new(body),
94    }
95}
96
97fn sort(u: Universe) -> Term {
98    Term::Sort(u)
99}
100
101fn obs_at(u: Universe) -> Term {
102    Term::Const {
103        name: "Obs".to_string(),
104        levels: vec![u],
105    }
106}
107
108/// The `SProp` connectives and the four constants, certified.
109pub fn register(ctx: &mut Context) -> KernelResult<()> {
110    let certificate = declare(ctx)?;
111    ctx.certify_observational(certificate);
112    Ok(())
113}
114
115/// The connectives, their projections and the four constants, declared; the certificate that would
116/// make them compute, not yet installed.
117fn declare(ctx: &mut Context) -> KernelResult<Certificate> {
118    let sprop = || sort(Universe::SProp);
119    let u = || Universe::Var("u".to_string());
120    ctx.add_mutual_inductives(&[MutualInductive {
121        name: "STrue".to_string(),
122        sort: sprop(),
123        num_params: 0,
124        constructors: vec![("sI".to_string(), g("STrue"))],
125    }])?;
126    ctx.add_mutual_inductives(&[MutualInductive {
127        name: "SFalse".to_string(),
128        sort: sprop(),
129        num_params: 0,
130        constructors: vec![],
131    }])?;
132    ctx.add_mutual_inductives(&[MutualInductive {
133        name: "SAnd".to_string(),
134        sort: pi("A", sprop(), pi("B", sprop(), sprop())),
135        num_params: 2,
136        constructors: vec![(
137            "spair".to_string(),
138            pi(
139                "A",
140                sprop(),
141                pi(
142                    "B",
143                    sprop(),
144                    pi(
145                        "a",
146                        v("A"),
147                        pi("b", v("B"), app(g("SAnd"), [v("A"), v("B")])),
148                    ),
149                ),
150            ),
151        )],
152    }])?;
153    ctx.add_mutual_inductives(&[MutualInductive {
154        name: "SSigma".to_string(),
155        sort: pi("A", sprop(), pi("B", pi("_", v("A"), sprop()), sprop())),
156        num_params: 2,
157        constructors: vec![(
158            "sexists".to_string(),
159            pi(
160                "A",
161                sprop(),
162                pi(
163                    "B",
164                    pi("_", v("A"), sprop()),
165                    pi(
166                        "a",
167                        v("A"),
168                        pi(
169                            "b",
170                            app(v("B"), [v("a")]),
171                            app(g("SSigma"), [v("A"), v("B")]),
172                        ),
173                    ),
174                ),
175            ),
176        )],
177    }])?;
178    let sbox = || Term::Const {
179        name: "SBox".to_string(),
180        levels: vec![u()],
181    };
182    ctx.add_universe_poly_inductives(
183        &["u".to_string()],
184        &[MutualInductive {
185            name: "SBox".to_string(),
186            sort: pi("A", sort(u()), sprop()),
187            num_params: 1,
188            constructors: vec![(
189                "sbox".to_string(),
190                pi("A", sort(u()), pi("a", v("A"), app(sbox(), [v("A")]))),
191            )],
192        }],
193    )?;
194    register_projections(ctx)?;
195    register_closure(ctx)?;
196    let levels = vec!["u".to_string()];
197    ctx.add_universe_poly_declaration(
198        "Obs",
199        levels.clone(),
200        pi("A", sort(u()), pi("a", v("A"), pi("b", v("A"), sprop()))),
201    )?;
202    let obs = |ty: Term, a: Term, b: Term| app(obs_at(u()), [ty, a, b]);
203    ctx.add_universe_poly_declaration(
204        "Obs_refl",
205        levels.clone(),
206        pi("A", sort(u()), pi("a", v("A"), obs(v("A"), v("a"), v("a")))),
207    )?;
208    let refl = Term::Const {
209        name: "Obs_refl".to_string(),
210        levels: vec![u()],
211    };
212    ctx.add_universe_poly_declaration(
213        "Obs_transp",
214        levels.clone(),
215        pi(
216            "A",
217            sort(u()),
218            pi(
219                "a",
220                v("A"),
221                pi(
222                    "P",
223                    pi("b", v("A"), pi("_", obs(v("A"), v("a"), v("b")), sprop())),
224                    pi(
225                        "h",
226                        app(v("P"), [v("a"), app(refl, [v("A"), v("a")])]),
227                        pi(
228                            "b",
229                            v("A"),
230                            pi(
231                                "e",
232                                obs(v("A"), v("a"), v("b")),
233                                app(v("P"), [v("b"), v("e")]),
234                            ),
235                        ),
236                    ),
237                ),
238            ),
239        ),
240    )?;
241    let types_equal = app(obs_at(u().succ()), [sort(u()), v("A"), v("B")]);
242    ctx.add_universe_poly_declaration(
243        "Obs_cast",
244        levels,
245        pi(
246            "A",
247            sort(u()),
248            pi(
249                "B",
250                sort(u()),
251                pi("e", types_equal, pi("_", v("A"), v("B"))),
252            ),
253        ),
254    )?;
255    let constants = vec![
256        ("Obs".to_string(), Kind::Obs),
257        ("Obs_refl".to_string(), Kind::Refl),
258        ("Obs_transp".to_string(), Kind::Transp),
259        ("Obs_cast".to_string(), Kind::Cast),
260    ];
261    let mut dependencies: BTreeSet<String> = constants.iter().map(|(n, _)| n.clone()).collect();
262    for name in [
263        "STrue", "sI", "SFalse", "SAnd", "spair", "SSigma", "sexists", "SBox", "sbox",
264    ]
265    .into_iter()
266    .chain(PROJECTIONS)
267    .chain(CLOSURE)
268    {
269        dependencies.insert(name.to_string());
270    }
271    Ok(Certificate {
272        constants,
273        dependencies,
274    })
275}
276
277/// The projections out of the `SProp` pairs, which the rules for `Obs_cast` read an equality's
278/// components with.
279const PROJECTIONS: [&str; 4] = ["SAnd_left", "SAnd_right", "SSigma_fst", "SSigma_snd"];
280
281fn register_projections(ctx: &mut Context) -> KernelResult<()> {
282    let sprop = || sort(Universe::SProp);
283    let and = || app(g("SAnd"), [v("A"), v("B")]);
284    let family = || pi("_", v("A"), sprop());
285    let sigma = || app(g("SSigma"), [v("A"), v("B")]);
286    let out_of = |discriminant: &str, owner: Term, motive: Term, case: Term| Term::Match {
287        discriminant: Box::new(v(discriminant)),
288        motive: Box::new(lam(discriminant, owner, motive)),
289        cases: vec![case],
290    };
291    for (name, field, ty) in [("SAnd_left", "a", v("A")), ("SAnd_right", "b", v("B"))] {
292        let body = out_of(
293            "p",
294            and(),
295            ty.clone(),
296            lam("a", v("A"), lam("b", v("B"), v(field))),
297        );
298        ctx.declare(
299            name,
300            pi("A", sprop(), pi("B", sprop(), pi("p", and(), ty))),
301            lam("A", sprop(), lam("B", sprop(), lam("p", and(), body))),
302        )?;
303    }
304    let fields = |chosen: &str| lam("a", v("A"), lam("b", app(v("B"), [v("a")]), v(chosen)));
305    let fst = out_of("s", sigma(), v("A"), fields("a"));
306    ctx.declare(
307        "SSigma_fst",
308        pi("A", sprop(), pi("B", family(), pi("s", sigma(), v("A")))),
309        lam("A", sprop(), lam("B", family(), lam("s", sigma(), fst))),
310    )?;
311    let second = || app(v("B"), [app(g("SSigma_fst"), [v("A"), v("B"), v("s")])]);
312    let snd = out_of("s", sigma(), second(), fields("b"));
313    ctx.declare(
314        "SSigma_snd",
315        pi("A", sprop(), pi("B", family(), pi("s", sigma(), second()))),
316        lam("A", sprop(), lam("B", family(), lam("s", sigma(), snd))),
317    )
318}
319
320/// The equivalence closure of a relation, which the equality of two quotient classes is.
321const CLOSURE: [&str; 5] = [
322    "EqvGen",
323    "EqvGen_rel",
324    "EqvGen_refl",
325    "EqvGen_symm",
326    "EqvGen_trans",
327];
328
329/// `EqvGen.{u} (A : Sort u) (r : A → A → Prop) : A → A → Prop`, the least equivalence containing `r`.
330fn register_closure(ctx: &mut Context) -> KernelResult<()> {
331    let u = || Universe::Var("u".to_string());
332    let relation = || pi("_", v("A"), pi("_", v("A"), sort(Universe::Prop)));
333    let closure = |x: &str, y: &str| {
334        app(
335            Term::Const {
336                name: "EqvGen".to_string(),
337                levels: vec![u()],
338            },
339            [v("A"), v("r"), v(x), v(y)],
340        )
341    };
342    let over = |body: Term| pi("A", sort(u()), pi("r", relation(), body));
343    let point = |name: &str, body: Term| pi(name, v("A"), body);
344    ctx.add_universe_poly_inductives(
345        &["u".to_string()],
346        &[MutualInductive {
347            name: "EqvGen".to_string(),
348            sort: over(point("x", point("y", sort(Universe::Prop)))),
349            num_params: 2,
350            constructors: vec![
351                (
352                    "EqvGen_rel".to_string(),
353                    over(point(
354                        "x",
355                        point(
356                            "y",
357                            pi("_", app(v("r"), [v("x"), v("y")]), closure("x", "y")),
358                        ),
359                    )),
360                ),
361                (
362                    "EqvGen_refl".to_string(),
363                    over(point("x", closure("x", "x"))),
364                ),
365                (
366                    "EqvGen_symm".to_string(),
367                    over(point(
368                        "x",
369                        point("y", pi("_", closure("x", "y"), closure("y", "x"))),
370                    )),
371                ),
372                (
373                    "EqvGen_trans".to_string(),
374                    over(point(
375                        "x",
376                        point(
377                            "y",
378                            point(
379                                "z",
380                                pi(
381                                    "_",
382                                    closure("x", "y"),
383                                    pi("_", closure("y", "z"), closure("x", "z")),
384                                ),
385                            ),
386                        ),
387                    )),
388                ),
389            ],
390        }],
391    )
392}
393
394/// One step of a certified constant applied to all its arguments: `Obs` on the shape of its type,
395/// `Obs_cast` on the shapes of its two types and its argument. The reducer brings the arguments to
396/// their normal forms, so a rule that does not fire yet fires once they are reduced.
397pub(crate) fn reduce(ctx: &Context, head: &Term, args: &[&Term]) -> Option<Term> {
398    let Term::Const { name, levels } = head else {
399        return None;
400    };
401    if levels.len() != 1 {
402        return None;
403    }
404    match (kind(ctx, name)?, args) {
405        (Kind::Obs, [ty, a, b]) => equality(ctx, ty, a, b),
406        (Kind::Cast, [from, to, proof, value]) => carry(ctx, from, to, proof, value),
407        _ => None,
408    }
409}
410
411/// `Obs.{u} A a b`, by `A`:
412///
413/// - a function type: pointwise equality (function extensionality);
414/// - `SProp` or `Prop`: logical equivalence (propositional extensionality), squashed into `SProp`
415///   for `Prop`;
416/// - any other universe: equality of types that are not propositions, by their formers;
417/// - an inductive family that holds no proposition: equality of values, by their constructors.
418///
419/// Every other `A` is stuck, among them a family that may be a proposition and any type that is
420/// not yet a former; so is an equality of propositions at a universe that may be `Prop`.
421fn equality(ctx: &Context, ty: &Term, a: &Term, b: &Term) -> Option<Term> {
422    match ty {
423        Term::Pi {
424            param,
425            param_type,
426            body_type,
427        } => pointwise(ctx, param, param_type, body_type, a, b),
428        Term::Sort(Universe::SProp) => Some(app(g("SAnd"), [arrow(a, b), arrow(b, a)])),
429        Term::Sort(level) if level.equiv(&Universe::Prop) => {
430            let squash = |p: Term| {
431                app(
432                    Term::Const {
433                        name: "SBox".to_string(),
434                        levels: vec![Universe::Prop],
435                    },
436                    [p],
437                )
438            };
439            Some(app(g("SAnd"), [squash(arrow(a, b)), squash(arrow(b, a))]))
440        }
441        Term::Sort(_) => type_equality(ctx, a, b).map(TypeEquality::into_term),
442        _ => values(ctx, ty, a, b),
443    }
444}
445
446/// `Π (x : D). Obs.{v} (C x) (a x) (b x)`, `v` the level of the codomain.
447fn pointwise(
448    ctx: &Context,
449    param: &str,
450    domain: &Term,
451    codomain: &Term,
452    a: &Term,
453    b: &Term,
454) -> Option<Term> {
455    let x = fresh(param, &[a, b, domain, &scoped(param, codomain)]);
456    let codomain = if x == param {
457        codomain.clone()
458    } else {
459        substitute(codomain, param, &v(&x))
460    };
461    let level = level_of(&ctx.extend(&x, domain.clone()), &codomain)?;
462    let at = |t: &Term| Term::App(Box::new(t.clone()), Box::new(v(&x)));
463    Some(pi(&x, domain.clone(), obs(level, codomain, at(a), at(b))))
464}
465
466/// What the equality of two types that are not propositions computes to, read on their formers.
467enum TypeEquality {
468    /// One universe.
469    Holds,
470    /// Two different formers, or two universes no instantiation makes one.
471    Fails,
472    /// Two function types: `SSigma domains codomains`, `domains` the equality of the domains at
473    /// level `domain` and `codomains` the family over it.
474    Products {
475        domain: Universe,
476        domains: Term,
477        codomains: Term,
478    },
479    /// One family: its arguments' equalities, and whether any is heterogeneous.
480    Family { parts: Vec<Term>, dependent: bool },
481    /// One quotient former: the carriers' equality first, then the relations'.
482    Quotient { parts: Vec<Term> },
483}
484
485impl TypeEquality {
486    fn into_term(self) -> Term {
487        match self {
488            TypeEquality::Holds => g("STrue"),
489            TypeEquality::Fails => g("SFalse"),
490            TypeEquality::Products {
491                domains, codomains, ..
492            } => app(g("SSigma"), [domains, codomains]),
493            TypeEquality::Family { parts, .. } | TypeEquality::Quotient { parts } => {
494                conjunction(parts)
495            }
496        }
497    }
498}
499
500/// Equality of two types that are not propositions, by their formers: universes by their levels,
501/// function types by their domains and codomains, the same inductive family by its arguments, and
502/// two different formers never. A side whose own sort may be `Prop` is stuck: a proposition seen
503/// as a type is compared by `Prop`'s rule, never by its former (`Nat → False` and `Bool → False`
504/// are the same proposition).
505fn type_equality(ctx: &Context, a: &Term, b: &Term) -> Option<TypeEquality> {
506    let (left, right) = (former(ctx, a)?, former(ctx, b)?);
507    for side in [a, b] {
508        if !crate::type_checker::infer_sort(ctx, side)
509            .ok()?
510            .is_never_prop()
511        {
512            return None;
513        }
514    }
515    match (left, right) {
516        (Former::Sort(s), Former::Sort(t)) => {
517            if s.equiv(t) {
518                Some(TypeEquality::Holds)
519            } else if distinct_levels(s, t) {
520                Some(TypeEquality::Fails)
521            } else {
522                None
523            }
524        }
525        (Former::Pi(p, d, c), Former::Pi(q, e, k)) => products(ctx, (p, d, c), (q, e, k)),
526        (Former::Family(i, li, xs), Former::Family(j, lj, ys)) => {
527            if i != j {
528                return Some(TypeEquality::Fails);
529            }
530            families(ctx, a, (i, li, &xs), (lj, &ys))
531        }
532        (Former::Quotient(h, xs), Former::Quotient(k, ys)) => {
533            if !same_quotient(h, k)? {
534                return Some(TypeEquality::Fails);
535            }
536            quotients(ctx, h, &xs, &ys)
537        }
538        _ => Some(TypeEquality::Fails),
539    }
540}
541
542/// What a type is built by, read on its head.
543enum Former<'a> {
544    Sort(&'a Universe),
545    Pi(&'a str, &'a Term, &'a Term),
546    Family(&'a str, &'a [Universe], Vec<&'a Term>),
547    /// A certified quotient former (the prelude's or a kernel-installed one) and its arguments.
548    Quotient(&'a Term, Vec<&'a Term>),
549}
550
551fn former<'a>(ctx: &Context, t: &'a Term) -> Option<Former<'a>> {
552    match t {
553        Term::Sort(level) => Some(Former::Sort(level)),
554        Term::Pi {
555            param,
556            param_type,
557            body_type,
558        } => Some(Former::Pi(param, param_type, body_type)),
559        _ => {
560            let (head, args) = spine(t);
561            match head {
562                Term::Global(name) if ctx.is_inductive(name) => {
563                    Some(Former::Family(name, &[], args))
564                }
565                Term::Const { name, levels } if ctx.is_inductive(name) => {
566                    Some(Former::Family(name, levels, args))
567                }
568                _ if quotient_part(ctx, head) == Some(QuotientPart::Former) => {
569                    Some(Former::Quotient(head, args))
570                }
571                _ => None,
572            }
573        }
574    }
575}
576
577#[derive(PartialEq)]
578enum QuotientPart {
579    Former,
580    Class,
581}
582
583/// What a certified quotient constant `head` is, if it is one.
584fn quotient_part(ctx: &Context, head: &Term) -> Option<QuotientPart> {
585    use crate::quotient::{Kind, PreludeKind};
586    match head {
587        Term::Global(name) => match crate::quotient::prelude_kind(ctx, name)? {
588            PreludeKind::Type => Some(QuotientPart::Former),
589            PreludeKind::Constructor => Some(QuotientPart::Class),
590            _ => None,
591        },
592        Term::Const { name, levels } => match crate::quotient::kind(ctx, name, levels)? {
593            Kind::Type => Some(QuotientPart::Former),
594            Kind::Constructor => Some(QuotientPart::Class),
595            _ => None,
596        },
597        _ => None,
598    }
599}
600
601/// `Quot_mk A r x ~[Quot A r] Quot_mk A r y` is `SBox (EqvGen A r x y)`: two classes are one exactly
602/// when their representatives are related by the equivalence `r` generates.
603fn classes(ctx: &Context, ty: &Term, carrier: &[&Term], a: &Term, b: &Term) -> Option<Term> {
604    if carrier.len() != 2
605        || !crate::type_checker::infer_sort(ctx, ty)
606            .ok()?
607            .is_never_prop()
608    {
609        return None;
610    }
611    let ((c, xs), (d, ys)) = (spine(a), spine(b));
612    if quotient_part(ctx, c) != Some(QuotientPart::Class)
613        || quotient_part(ctx, d) != Some(QuotientPart::Class)
614    {
615        return None;
616    }
617    let ([_, _, x], [_, _, y]) = (xs.as_slice(), ys.as_slice()) else {
618        return None;
619    };
620    let level = level_of(ctx, carrier[0])?;
621    let closure = app(
622        Term::Const {
623            name: "EqvGen".to_string(),
624            levels: vec![level],
625        },
626        [
627            carrier[0].clone(),
628            carrier[1].clone(),
629            (*x).clone(),
630            (*y).clone(),
631        ],
632    );
633    Some(app(
634        Term::Const {
635            name: "SBox".to_string(),
636            levels: vec![Universe::Prop],
637        },
638        [closure],
639    ))
640}
641
642/// Two quotients by one former: the equalities of their carriers and relations, as a family's
643/// arguments compare (the relation's type mentions the carrier, so it compares heterogeneously).
644fn quotients(ctx: &Context, head: &Term, xs: &[&Term], ys: &[&Term]) -> Option<TypeEquality> {
645    if xs.len() != 2 || ys.len() != 2 {
646        return None;
647    }
648    let level = crate::type_checker::infer_sort(ctx, xs[0]).ok()?;
649    let arity = match head {
650        Term::Global(name) => {
651            let header = ctx.get_global(name)?;
652            let [u] = crate::term::free_level_vars(header).try_into().ok()?;
653            crate::term::instantiate_universes(header, &[(u, level)].into_iter().collect())
654        }
655        _ => crate::type_checker::infer_type(ctx, head).ok()?,
656    };
657    // `Quot.{u} : Π (A : Sort u). (A → A → Prop) → Sort u`: in a quotient that is not a proposition
658    // (`u ≥ 1`), the carrier is an element of the universe and the relation a family of propositions
659    // over it, both small, so no argument is vetted.
660    let (parts, _) = componentwise(ctx, arity, xs, ys, &|_| true)?;
661    Some(TypeEquality::Quotient { parts })
662}
663
664/// One certified quotient head and another: the same constant at levels that agree.
665fn same_quotient(h: &Term, k: &Term) -> Option<bool> {
666    match (h, k) {
667        (Term::Global(m), Term::Global(n)) => Some(m == n),
668        (Term::Const { name: m, levels: l }, Term::Const { name: n, levels: k }) => {
669            if m != n {
670                return Some(false);
671            }
672            (l.len() == k.len() && l.iter().zip(k).all(|(a, b)| a.equiv(b))).then_some(true)
673        }
674        _ => Some(false),
675    }
676}
677
678/// Two levels no instantiation of their variables makes equal: one strictly below the other
679/// everywhere, or two different closed levels.
680fn distinct_levels(s: &Universe, t: &Universe) -> bool {
681    s.succ().is_subtype_of(t)
682        || t.succ().is_subtype_of(s)
683        || (closed(s) && closed(t) && !s.equiv(t))
684}
685
686fn closed(level: &Universe) -> bool {
687    match level {
688        Universe::SProp | Universe::Prop | Universe::Type(_) => true,
689        Universe::Var(_) => false,
690        Universe::Succ(l) => closed(l),
691        Universe::Max(a, b) | Universe::IMax(a, b) => closed(a) && closed(b),
692    }
693}
694
695/// `(Π (x : D). C) ~ (Π (x' : D'). C')` is
696/// `SSigma (D' ~ D) (λ e. Π (x' : D'). C[x := Obs_cast D' D e x'] ~ C'[x'])`: the domains are equal,
697/// and the codomains are equal at every argument carried across.
698fn products(
699    ctx: &Context,
700    (p, d, c): (&str, &Term, &Term),
701    (q, e, k): (&str, &Term, &Term),
702) -> Option<TypeEquality> {
703    let domain = level_of(ctx, d)?;
704    if !domain.equiv(&level_of(ctx, e)?) {
705        return None;
706    }
707    let (c_scoped, k_scoped) = (scoped(p, c), scoped(q, k));
708    let proof = fresh("e", &[d, e, &c_scoped, &k_scoped]);
709    let x = fresh(q, &[d, e, &c_scoped, &k_scoped, &v(&proof)]);
710    let domains = obs(
711        domain.succ(),
712        Term::Sort(domain.clone()),
713        e.clone(),
714        d.clone(),
715    );
716    let carried = app(
717        cast_at(domain.clone()),
718        [e.clone(), d.clone(), v(&proof), v(&x)],
719    );
720    let left = substitute(c, p, &carried);
721    let right = if x == q {
722        k.clone()
723    } else {
724        substitute(k, q, &v(&x))
725    };
726    let inside = ctx.extend(&proof, domains.clone()).extend(&x, e.clone());
727    let codomain = level_of(&inside, &left)?;
728    if !codomain.equiv(&level_of(&inside, &right)?) {
729        return None;
730    }
731    let codomains_equal = pi(
732        &x,
733        e.clone(),
734        obs(codomain.succ(), Term::Sort(codomain), left, right),
735    );
736    let codomains = lam(&proof, domains.clone(), codomains_equal);
737    Some(TypeEquality::Products {
738        domain,
739        domains,
740        codomains,
741    })
742}
743
744/// `I p̄ ~ I q̄` is the conjunction of the arguments' equalities, when every argument is small
745/// enough to be stored in the family's own universe: an element of a type in it, a universe it
746/// contains, or a family of those over an element type. A family over a larger argument (Hur's
747/// `I : (Type → Type) → Type`) would make `I` an injection from that larger type into its
748/// universe, which classical logic refutes; such an equality is stuck.
749fn families(
750    ctx: &Context,
751    a: &Term,
752    (family, levels, xs): (&str, &[Universe], &[&Term]),
753    (other_levels, ys): (&[Universe], &[&Term]),
754) -> Option<TypeEquality> {
755    if levels.len() != other_levels.len()
756        || levels.iter().zip(other_levels).any(|(l, m)| !l.equiv(m))
757    {
758        return None;
759    }
760    let universe = crate::type_checker::infer_sort(ctx, a).ok()?;
761    let arity = crate::type_checker::infer_type(ctx, &family_head(family, levels)).ok()?;
762    let small = |t: &Term| small(ctx, t, &universe);
763    let (parts, dependent) = componentwise(ctx, arity, xs, ys, &small)?;
764    Some(TypeEquality::Family { parts, dependent })
765}
766
767fn family_head(family: &str, levels: &[Universe]) -> Term {
768    if levels.is_empty() {
769        g(family)
770    } else {
771        Term::Const {
772            name: family.to_string(),
773            levels: levels.to_vec(),
774        }
775    }
776}
777
778/// An element of a type in `universe`, a universe below it, or a family of those over an element
779/// type.
780fn small(ctx: &Context, t: &Term, universe: &Universe) -> bool {
781    if crate::type_checker::infer_sort(ctx, t).is_ok_and(|sort| sort.is_subtype_of(universe)) {
782        return true;
783    }
784    match crate::reduction::normalize(ctx, t) {
785        Term::Sort(level) => level.is_subtype_of(universe),
786        Term::Pi {
787            param,
788            param_type,
789            body_type,
790        } => {
791            crate::type_checker::infer_sort(ctx, &param_type)
792                .is_ok_and(|sort| sort.is_subtype_of(universe))
793                && small(&ctx.extend(&param, *param_type), &body_type, universe)
794        }
795        _ => false,
796    }
797}
798
799/// Equality of two values of a family that is not a proposition: two different constructors
800/// never, the same constructor by its fields. A numeral is read as its constructor.
801fn values(ctx: &Context, ty: &Term, a: &Term, b: &Term) -> Option<Term> {
802    if let Some(Former::Quotient(_, carrier)) = former(ctx, ty) {
803        return classes(ctx, ty, &carrier, a, b);
804    }
805    let (family, arguments) = spine(ty);
806    let (Term::Global(family) | Term::Const { name: family, .. }) = family else {
807        return None;
808    };
809    if !ctx.is_inductive(family)
810        || !crate::type_checker::infer_sort(ctx, ty)
811            .ok()?
812            .is_never_prop()
813    {
814        return None;
815    }
816    let (a, b) = (
817        crate::reduction::constructor_form(ctx, a)?,
818        crate::reduction::constructor_form(ctx, b)?,
819    );
820    let ((left, xs), (right, ys)) = (spine(&a), spine(&b));
821    let (Term::Global(c) | Term::Const { name: c, .. }) = left else {
822        return None;
823    };
824    let (Term::Global(d) | Term::Const { name: d, .. }) = right else {
825        return None;
826    };
827    if ctx.constructor_inductive(c) != Some(family) || ctx.constructor_inductive(d) != Some(family)
828    {
829        return None;
830    }
831    if c != d {
832        return Some(g("SFalse"));
833    }
834    let parameters = ctx.inductive_num_params(family);
835    if arguments.len() < parameters || xs.len() < parameters || xs.len() != ys.len() {
836        return None;
837    }
838    let telescope = peel(
839        crate::type_checker::infer_type(ctx, left).ok()?,
840        &arguments[..parameters],
841    )?;
842    let (parts, _) = componentwise(
843        ctx,
844        telescope,
845        &xs[parameters..],
846        &ys[parameters..],
847        &|_| true,
848    )?;
849    Some(conjunction(parts))
850}
851
852/// The equalities of `xs` and `ys` read along one telescope, each at its own type, and whether any
853/// is heterogeneous: a position whose type mentions no earlier position compares at that type;
854/// one whose type does compares heterogeneously, `SSigma (T ~ T') (λ e. Obs_cast T T' e x ~ y)`, its
855/// type taken at each side's own earlier positions. Which positions are dependent is read from
856/// the telescope, never from the two instances, so the answer does not change as the arguments
857/// reduce. `admit` vets each position's type on both sides; the telescope must be used up exactly.
858fn componentwise(
859    ctx: &Context,
860    telescope: Term,
861    xs: &[&Term],
862    ys: &[&Term],
863    admit: &dyn Fn(&Term) -> bool,
864) -> Option<(Vec<Term>, bool)> {
865    if xs.len() != ys.len() {
866        return None;
867    }
868    let (mut generic, mut left, mut right) = (telescope.clone(), telescope.clone(), telescope);
869    let mut earlier: Vec<String> = Vec::new();
870    let mut parts = Vec::with_capacity(xs.len());
871    let mut any_dependent = false;
872    for (x, y) in xs.iter().zip(ys) {
873        let Term::Pi {
874            param: name,
875            param_type: shape,
876            body_type: rest,
877        } = generic
878        else {
879            return None;
880        };
881        let Term::Pi {
882            param: lp,
883            param_type: lt,
884            body_type: lb,
885        } = left
886        else {
887            return None;
888        };
889        let Term::Pi {
890            param: rp,
891            param_type: rt,
892            body_type: rb,
893        } = right
894        else {
895            return None;
896        };
897        if !admit(&lt) || !admit(&rt) {
898            return None;
899        }
900        let dependent = earlier
901            .iter()
902            .any(|n| crate::type_checker::occurs_free(&shape, n));
903        any_dependent |= dependent;
904        parts.push(if dependent {
905            heterogeneous(ctx, &lt, &rt, x, y)?
906        } else {
907            obs(level_of(ctx, &lt)?, *lt, (*x).clone(), (*y).clone())
908        });
909        earlier.push(name);
910        generic = *rest;
911        left = substitute(&lb, &lp, x);
912        right = substitute(&rb, &rp, y);
913    }
914    if matches!(left, Term::Pi { .. }) || matches!(right, Term::Pi { .. }) {
915        return None;
916    }
917    Some((parts, any_dependent))
918}
919
920/// `SSigma (T ~ T') (λ e. Obs_cast T T' e x ~[T'] y)`.
921fn heterogeneous(ctx: &Context, from: &Term, to: &Term, x: &Term, y: &Term) -> Option<Term> {
922    let level = level_of(ctx, from)?;
923    if !level.equiv(&level_of(ctx, to)?) {
924        return None;
925    }
926    let types_equal = obs(
927        level.succ(),
928        Term::Sort(level.clone()),
929        from.clone(),
930        to.clone(),
931    );
932    let e = fresh("e", &[from, to, x, y]);
933    let carried = app(
934        cast_at(level.clone()),
935        [from.clone(), to.clone(), v(&e), x.clone()],
936    );
937    Some(app(
938        g("SSigma"),
939        [
940            types_equal.clone(),
941            lam(&e, types_equal, obs(level, to.clone(), carried, y.clone())),
942        ],
943    ))
944}
945
946/// `Obs_cast.{u} A B e t`, for two types that are not propositions whose equality computes:
947///
948/// - along one universe, `t`;
949/// - along two function types, the function that carries its argument back along the domains'
950///   equality and its result forth along the codomains';
951/// - along one family that has no indices and whose parameters depend on no earlier parameter,
952///   with `t` a constructor whose fields depend on no earlier field, the same constructor at
953///   `B`'s parameters with each field carried.
954///
955/// Every other cast is stuck: a cast along an equality of types that does not compute, along
956/// evidence of `SFalse`, between propositions (a proof needs no carrying), or of a value that is
957/// not yet a constructor.
958fn carry(ctx: &Context, from: &Term, to: &Term, proof: &Term, value: &Term) -> Option<Term> {
959    match type_equality(ctx, from, to)? {
960        TypeEquality::Holds => Some(value.clone()),
961        TypeEquality::Products {
962            domain,
963            domains,
964            codomains,
965        } => {
966            let (
967                Term::Pi {
968                    param: p,
969                    param_type: d,
970                    body_type: c,
971                },
972                Term::Pi {
973                    param: q,
974                    param_type: e,
975                    body_type: k,
976                },
977            ) = (from, to)
978            else {
979                return None;
980            };
981            let x = fresh(
982                q,
983                &[
984                    d,
985                    e,
986                    &scoped(p, c),
987                    &scoped(q, k),
988                    proof,
989                    value,
990                    &domains,
991                    &codomains,
992                ],
993            );
994            let first = app(
995                g("SSigma_fst"),
996                [domains.clone(), codomains.clone(), proof.clone()],
997            );
998            let back = app(
999                cast_at(domain),
1000                [(**e).clone(), (**d).clone(), first, v(&x)],
1001            );
1002            let left = substitute(c, p, &back);
1003            let right = if x == *q {
1004                (**k).clone()
1005            } else {
1006                substitute(k, q, &v(&x))
1007            };
1008            let inside = ctx.extend(&x, (**e).clone());
1009            let codomain = level_of(&inside, &left)?;
1010            if !codomain.equiv(&level_of(&inside, &right)?) {
1011                return None;
1012            }
1013            let second = app(g("SSigma_snd"), [domains, codomains, proof.clone(), v(&x)]);
1014            let image = Term::App(Box::new(value.clone()), Box::new(back));
1015            Some(lam(
1016                &x,
1017                (**e).clone(),
1018                app(cast_at(codomain), [left, right, second, image]),
1019            ))
1020        }
1021        TypeEquality::Family {
1022            parts,
1023            dependent: false,
1024        } => carry_value(ctx, from, to, &parts, proof, value),
1025        TypeEquality::Quotient { parts } => carry_class(ctx, from, to, &parts, proof, value),
1026        TypeEquality::Fails | TypeEquality::Family { .. } => None,
1027    }
1028}
1029
1030/// `Obs_cast (Quot X R) (Quot Y S) e (Quot_mk X R x)` is `Quot_mk Y S (Obs_cast X Y e₀ x)`, `e₀` the carriers'
1031/// equality: the class of the carried representative.
1032fn carry_class(
1033    ctx: &Context,
1034    from: &Term,
1035    to: &Term,
1036    parts: &[Term],
1037    proof: &Term,
1038    value: &Term,
1039) -> Option<Term> {
1040    let (Former::Quotient(_, xs), Former::Quotient(_, ys)) = (former(ctx, from)?, former(ctx, to)?)
1041    else {
1042        return None;
1043    };
1044    let (constructor, arguments) = spine(value);
1045    let [_, _, x] = arguments.as_slice() else {
1046        return None;
1047    };
1048    if quotient_part(ctx, constructor) != Some(QuotientPart::Class) {
1049        return None;
1050    }
1051    let carriers = components(parts, proof).into_iter().next()?;
1052    let representative = app(
1053        cast_at(level_of(ctx, xs[0])?),
1054        [xs[0].clone(), ys[0].clone(), carriers, (*x).clone()],
1055    );
1056    Some(app(
1057        constructor.clone(),
1058        [ys[0].clone(), ys[1].clone(), representative],
1059    ))
1060}
1061
1062/// `Obs_cast (I p̄) (I q̄) e (C p̄ x̄)` is `C q̄ (Obs_cast T₀[p̄] T₀[q̄] h₀ x₀) …`, each `hₖ : Tₖ[p̄] ~ Tₖ[q̄]`
1063/// built from `e`'s components by one `Obs_transp` per parameter, moving `Tₖ`'s parameters from `p̄` to
1064/// `q̄` one at a time.
1065fn carry_value(
1066    ctx: &Context,
1067    from: &Term,
1068    to: &Term,
1069    parts: &[Term],
1070    proof: &Term,
1071    value: &Term,
1072) -> Option<Term> {
1073    let (Former::Family(family, levels, ps), Former::Family(_, _, qs)) =
1074        (former(ctx, from)?, former(ctx, to)?)
1075    else {
1076        return None;
1077    };
1078    if ctx.inductive_num_indices(family) != 0 || ps.len() != parts.len() || qs.len() != ps.len() {
1079        return None;
1080    }
1081    let value = crate::reduction::constructor_form(ctx, value)?;
1082    let (constructor, arguments) = spine(&value);
1083    let (Term::Global(c) | Term::Const { name: c, .. }) = constructor else {
1084        return None;
1085    };
1086    if ctx.constructor_inductive(c) != Some(family) || arguments.len() < ps.len() {
1087        return None;
1088    }
1089    let fields = &arguments[ps.len()..];
1090    let telescope = crate::type_checker::infer_type(ctx, constructor).ok()?;
1091    if !fields_independent(&telescope, ps.len()) {
1092        return None;
1093    }
1094    let arity = crate::type_checker::infer_type(ctx, &family_head(family, levels)).ok()?;
1095    let parameter_types = telescope_types(arity, &ps)?;
1096    let components = components(parts, proof);
1097    let owned = |xs: &[&Term]| xs.iter().map(|x| (*x).clone()).collect::<Vec<Term>>();
1098    let (ps, qs) = (owned(&ps), owned(&qs));
1099    let mut carried = Vec::with_capacity(fields.len());
1100    for k in 0..fields.len() {
1101        let source = field_type(&telescope, &ps, fields, k)?;
1102        let target = field_type(&telescope, &qs, fields, k)?;
1103        let level = level_of(ctx, &source)?;
1104        if !level.equiv(&level_of(ctx, &target)?) {
1105            return None;
1106        }
1107        let mut evidence = app(
1108            Term::Const {
1109                name: "Obs_refl".to_string(),
1110                levels: vec![level.succ()],
1111            },
1112            [Term::Sort(level.clone()), source.clone()],
1113        );
1114        for (i, parameter_type) in parameter_types.iter().enumerate() {
1115            let mut avoid: Vec<&Term> =
1116                ps.iter().chain(&qs).chain(fields.iter().copied()).collect();
1117            avoid.push(&telescope);
1118            let b = fresh("b", &avoid);
1119            let h = fresh("h", &[&avoid[..], &[&v(&b)]].concat());
1120            let mixed: Vec<Term> = qs[..i]
1121                .iter()
1122                .cloned()
1123                .chain([v(&b)])
1124                .chain(ps[i + 1..].iter().cloned())
1125                .collect();
1126            let moved = field_type(&telescope, &mixed, fields, k)?;
1127            let parameter = level_of(ctx, parameter_type)?;
1128            let motive = lam(
1129                &b,
1130                parameter_type.clone(),
1131                lam(
1132                    &h,
1133                    obs(
1134                        parameter.clone(),
1135                        parameter_type.clone(),
1136                        ps[i].clone(),
1137                        v(&b),
1138                    ),
1139                    obs(
1140                        level.succ(),
1141                        Term::Sort(level.clone()),
1142                        source.clone(),
1143                        moved,
1144                    ),
1145                ),
1146            );
1147            evidence = app(
1148                Term::Const {
1149                    name: "Obs_transp".to_string(),
1150                    levels: vec![parameter],
1151                },
1152                [
1153                    parameter_type.clone(),
1154                    ps[i].clone(),
1155                    motive,
1156                    evidence,
1157                    qs[i].clone(),
1158                    components[i].clone(),
1159                ],
1160            );
1161        }
1162        carried.push(app(
1163            cast_at(level),
1164            [source, target, evidence, fields[k].clone()],
1165        ));
1166    }
1167    Some(app(constructor.clone(), qs.into_iter().chain(carried)))
1168}
1169
1170/// Whether no field of a constructor's telescope (after its `parameters`) mentions an earlier field.
1171fn fields_independent(telescope: &Term, parameters: usize) -> bool {
1172    let mut cur = telescope;
1173    for _ in 0..parameters {
1174        let Term::Pi { body_type, .. } = cur else {
1175            return false;
1176        };
1177        cur = body_type;
1178    }
1179    let mut earlier: Vec<&str> = Vec::new();
1180    while let Term::Pi {
1181        param,
1182        param_type,
1183        body_type,
1184    } = cur
1185    {
1186        if earlier
1187            .iter()
1188            .any(|n| crate::type_checker::occurs_free(param_type, n))
1189        {
1190            return false;
1191        }
1192        earlier.push(param);
1193        cur = body_type;
1194    }
1195    true
1196}
1197
1198/// The types of the first `values.len()` positions of `telescope`, each at the earlier values.
1199fn telescope_types(telescope: Term, values: &[&Term]) -> Option<Vec<Term>> {
1200    let mut cur = telescope;
1201    let mut types = Vec::with_capacity(values.len());
1202    for value in values {
1203        let Term::Pi {
1204            param,
1205            param_type,
1206            body_type,
1207        } = cur
1208        else {
1209            return None;
1210        };
1211        types.push(*param_type);
1212        cur = substitute(&body_type, &param, value);
1213    }
1214    Some(types)
1215}
1216
1217/// `telescope` past its first positions, instantiated at `values`.
1218fn peel(telescope: Term, values: &[&Term]) -> Option<Term> {
1219    let mut cur = telescope;
1220    for value in values {
1221        let Term::Pi {
1222            param, body_type, ..
1223        } = cur
1224        else {
1225            return None;
1226        };
1227        cur = substitute(&body_type, &param, value);
1228    }
1229    Some(cur)
1230}
1231
1232/// The type of field `k` of a constructor `telescope`, at `parameters` and the earlier `fields`.
1233fn field_type(telescope: &Term, parameters: &[Term], fields: &[&Term], k: usize) -> Option<Term> {
1234    let parameters: Vec<&Term> = parameters.iter().collect();
1235    let cur = peel(peel(telescope.clone(), &parameters)?, &fields[..k])?;
1236    let Term::Pi { param_type, .. } = cur else {
1237        return None;
1238    };
1239    Some(*param_type)
1240}
1241
1242/// Each component of `proof : SAnd parts₀ (SAnd parts₁ …)`, read out by `SAnd_left`/`SAnd_right`.
1243fn components(parts: &[Term], proof: &Term) -> Vec<Term> {
1244    let mut out = Vec::with_capacity(parts.len());
1245    let mut rest = proof.clone();
1246    for (j, part) in parts.iter().enumerate() {
1247        if j + 1 == parts.len() {
1248            out.push(rest);
1249            break;
1250        }
1251        let tail = conjunction(parts[j + 1..].to_vec());
1252        out.push(app(
1253            g("SAnd_left"),
1254            [part.clone(), tail.clone(), rest.clone()],
1255        ));
1256        rest = app(g("SAnd_right"), [part.clone(), tail, rest]);
1257    }
1258    out
1259}
1260
1261/// `STrue` for none, the one for one, else right-nested `SAnd`.
1262fn conjunction(mut parts: Vec<Term>) -> Term {
1263    let Some(last) = parts.pop() else {
1264        return g("STrue");
1265    };
1266    parts
1267        .into_iter()
1268        .rev()
1269        .fold(last, |rest, part| app(g("SAnd"), [part, rest]))
1270}
1271
1272/// The level a type lives at, as a universe argument (`SProp` read as `Prop`).
1273fn level_of(ctx: &Context, ty: &Term) -> Option<Universe> {
1274    crate::type_checker::infer_sort(ctx, ty)
1275        .ok()
1276        .map(|sort| sort.least_level())
1277}
1278
1279fn obs(level: Universe, ty: Term, a: Term, b: Term) -> Term {
1280    app(obs_at(level), [ty, a, b])
1281}
1282
1283fn cast_at(level: Universe) -> Term {
1284    Term::Const {
1285        name: "Obs_cast".to_string(),
1286        levels: vec![level],
1287    }
1288}
1289
1290fn arrow(a: &Term, b: &Term) -> Term {
1291    pi(&fresh("_", &[b]), a.clone(), b.clone())
1292}
1293
1294fn lam(param: &str, ty: Term, body: Term) -> Term {
1295    Term::Lambda {
1296        param: param.to_string(),
1297        param_type: Box::new(ty),
1298        body: Box::new(body),
1299    }
1300}
1301
1302fn substitute(body: &Term, var: &str, replacement: &Term) -> Term {
1303    crate::type_checker::substitute(body, var, replacement)
1304}
1305
1306/// The head and argument spine of an application.
1307fn spine(t: &Term) -> (&Term, Vec<&Term>) {
1308    let mut args = Vec::new();
1309    let mut cur = t;
1310    while let Term::App(f, a) = cur {
1311        args.push(a.as_ref());
1312        cur = f.as_ref();
1313    }
1314    args.reverse();
1315    (cur, args)
1316}
1317
1318/// `body` under a binder for `param`, so a fresh-name search sees every name free in `body` except
1319/// `param` itself.
1320fn scoped(param: &str, body: &Term) -> Term {
1321    lam(param, Term::Sort(Universe::Prop), body.clone())
1322}
1323
1324/// `base`, or `base` with a numeral, free in none of `terms`.
1325fn fresh(base: &str, terms: &[&Term]) -> String {
1326    let taken = |n: &str| terms.iter().any(|t| crate::type_checker::occurs_free(t, n));
1327    if !taken(base) {
1328        return base.to_string();
1329    }
1330    (1..)
1331        .map(|i| format!("{base}{i}"))
1332        .find(|n| !taken(n))
1333        .expect("a fresh name")
1334}
1335
1336#[cfg(test)]
1337#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/observational/tests.rs"]
1338pub(crate) mod tests;