Skip to main content

logicaffeine_kernel/
annotation.rs

1//! Annotations: what the source said about a subterm that no judgment of the kernel reads.
2//!
3//! [`Term::Annotated`] wraps one subterm with one [`Annotation`]:
4//!
5//! * [`Annotation::Binder`] — how the argument of the wrapped `fun` or `Π` is supplied, Lean's
6//!   `BinderInfo`, for a binder no declaration name records the kind of. `set_binder_kinds` keys a
7//!   declaration's telescope on its NAME; an anonymous `fun {α} a => a` passed as an argument, a
8//!   match case or a motive has no name, and this is where its kind lives.
9//! * [`Annotation::Source`] — where the subterm came from ([`Provenance`]): a position in a source
10//!   file, or one entry of a Lean `Expr.mdata` map, key and exported value.
11//!
12//! # Transparent
13//!
14//! An annotation changes no verdict and no result, and that is enforced at the representation
15//! rather than hoped for at each use:
16//!
17//! * `Term`'s `==` is the kernel's identity, so it does not see annotations: a term and its
18//!   annotated copy are EQUAL. [`Term::identical`] is the comparison that does see them.
19//! * The kernel's judgments read kernel meaning only. Each checking door — `infer_type`,
20//!   `is_subtype`, `def_eq`, the `normalize` family, `recheck`, `double_check` — erases the
21//!   annotations of the terms it is handed ONCE, at the outermost call, before the depth door
22//!   measures anything, and the [`Context`](crate::Context) stores every term erased: its locals,
23//!   and every definition, declaration, inductive, constructor, instance, coercion and default it
24//!   registers. Inside a kernel call no annotation exists; the arm every `match` on `Term` carries
25//!   for this variant sees through it all the same, and a walk that only collects —
26//!   `axiom_dependencies`, free variables, mentioned globals — reads straight through one.
27//! * The term store interns the ERASED term, so an annotation never splits sharing: a term and its
28//!   annotated copy intern to the same id.
29//! * A certificate carries kernel meaning only: the variant has no serialized form, so a capture
30//!   that forgot to erase fails loudly instead of widening the certificate language.
31//!
32//! What reads annotations is everything outside the kernel's judgments: the elaborator (a binder
33//! kind on an anonymous `fun` inserts implicit arguments exactly as a declaration's recorded kinds
34//! do) and the interface printer and parser (`@[…] t`, read back to the same annotated term).
35//!
36//! Every walk here runs on an explicit work stack: annotations sit on imported terms, which are
37//! deep, and asking whether a term carries one must never be what exhausts the stack.
38
39use crate::elaborate::ParamKind;
40use crate::term::Term;
41use std::fmt;
42
43/// One piece of information about how a subterm was written. The elaborator and the
44/// interface printer read it; every kernel judgment erases it first.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum Annotation {
47    /// How the argument of the wrapped `fun` or `Π` is supplied — Lean's `BinderInfo`.
48    Binder(ParamKind),
49    /// Where the wrapped subterm came from.
50    Source(Provenance),
51}
52
53/// Where a subterm came from.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum Provenance {
56    /// A position in a source text.
57    Position(SourcePosition),
58    /// One entry of a Lean `Expr.mdata` map: its key, and the exporter's rendering of its
59    /// `DataValue` (`Lean.DataValue.ofBool true`).
60    LeanMetadata {
61        /// The metadata key (`noImplicitLambda`, `_inaccessible`, `pp.analysis`, …).
62        key: String,
63        /// The value, as the exporter spells it.
64        value: String,
65    },
66}
67
68/// A position in a source text, in Lean's convention: the LINE is 1-based and the COLUMN is
69/// 0-based and counted in Unicode scalar values. Held in its canonical spelling, the one Lean
70/// prints a diagnostic's position in — `file:line:column` — so a position IS its text: two
71/// positions are equal exactly when they print the same, and reading one back needs no rendering.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct SourcePosition {
74    text: String,
75    file_len: usize,
76    line: u32,
77    column: u32,
78}
79
80impl SourcePosition {
81    /// The position `file:line:column`.
82    pub fn new(file: impl Into<String>, line: u32, column: u32) -> SourcePosition {
83        let file = file.into();
84        let file_len = file.len();
85        SourcePosition {
86            text: format!("{file}:{line}:{column}"),
87            file_len,
88            line,
89            column,
90        }
91    }
92
93    /// Read a position in its canonical spelling, `file:line:column`. The LAST two `:`-separated
94    /// fields are the line and the column, so a file name may itself contain `:`; the file is not
95    /// empty, the line is at least 1, and both numbers are written in canonical decimal (no sign,
96    /// no leading zero) so that reading and printing are inverse.
97    pub fn parse(text: &str) -> Option<SourcePosition> {
98        let mut fields = text.rsplitn(3, ':');
99        let column = canonical_number(fields.next()?)?;
100        let line = canonical_number(fields.next()?)?;
101        let file = fields.next()?;
102        if file.is_empty() || line == 0 {
103            return None;
104        }
105        Some(SourcePosition::new(file, line, column))
106    }
107
108    /// The file (or other source unit) the subterm was written in.
109    pub fn file(&self) -> &str {
110        &self.text[..self.file_len]
111    }
112
113    /// The 1-based line.
114    pub fn line(&self) -> u32 {
115        self.line
116    }
117
118    /// The 0-based column, in Unicode scalar values.
119    pub fn column(&self) -> u32 {
120        self.column
121    }
122
123    /// The canonical spelling, `file:line:column`.
124    pub fn as_str(&self) -> &str {
125        &self.text
126    }
127}
128
129/// A `u32` in canonical decimal: digits only, no leading zero unless it is `0`.
130fn canonical_number(digits: &str) -> Option<u32> {
131    let canonical = !digits.is_empty()
132        && digits.bytes().all(|b| b.is_ascii_digit())
133        && (digits == "0" || !digits.starts_with('0'));
134    canonical.then(|| digits.parse().ok()).flatten()
135}
136
137impl fmt::Display for SourcePosition {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        f.write_str(&self.text)
140    }
141}
142
143impl fmt::Display for Provenance {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Provenance::Position(position) => write!(f, "{position}"),
147            Provenance::LeanMetadata { key, value } => write!(f, "{key} := {value}"),
148        }
149    }
150}
151
152impl fmt::Display for Annotation {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            Annotation::Binder(kind) => write!(f, "binder {}", kind.lean_binder_info()),
156            Annotation::Source(Provenance::Position(position)) => {
157                write!(f, "source {:?}", position.as_str())
158            }
159            Annotation::Source(Provenance::LeanMetadata { key, value }) => {
160                write!(f, "mdata {key:?} {value:?}")
161            }
162        }
163    }
164}
165
166impl ParamKind {
167    /// The name of Lean's `BinderInfo` constructor for this kind — one each, a bijection.
168    pub fn lean_binder_info(self) -> &'static str {
169        match self {
170            ParamKind::Explicit => "default",
171            ParamKind::Implicit => "implicit",
172            ParamKind::StrictImplicit => "strictImplicit",
173            ParamKind::Instance => "instImplicit",
174        }
175    }
176
177    /// The kind a Lean `BinderInfo` constructor names, or `None` for a name Lean does not have.
178    pub fn from_lean_binder_info(name: &str) -> Option<ParamKind> {
179        match name {
180            "default" => Some(ParamKind::Explicit),
181            "implicit" => Some(ParamKind::Implicit),
182            "strictImplicit" => Some(ParamKind::StrictImplicit),
183            "instImplicit" => Some(ParamKind::Instance),
184            _ => None,
185        }
186    }
187}
188
189/// Push `t`'s immediate subterms, the one child enumeration every walk in this file shares.
190fn push_children<'t>(t: &'t Term, work: &mut Vec<&'t Term>) {
191    match t {
192        Term::Sort(_)
193        | Term::Var(_)
194        | Term::Global(_)
195        | Term::Const { .. }
196        | Term::Lit(_)
197        | Term::Hole => {}
198        Term::Pi {
199            param_type,
200            body_type: body,
201            ..
202        }
203        | Term::Lambda {
204            param_type, body, ..
205        } => {
206            work.push(param_type);
207            work.push(body);
208        }
209        Term::App(f, a) => {
210            work.push(f);
211            work.push(a);
212        }
213        Term::Match {
214            discriminant,
215            motive,
216            cases,
217        } => {
218            work.push(discriminant);
219            work.push(motive);
220            work.extend(cases.iter());
221        }
222        Term::Fix { body, .. } => work.push(body),
223        Term::MutualFix { defs, .. } => work.extend(defs.iter().map(|(_, body)| body)),
224        Term::Let {
225            ty, value, body, ..
226        } => {
227            work.push(ty);
228            work.push(value);
229            work.push(body);
230        }
231        Term::Annotated { term, .. } => work.push(term),
232    }
233}
234
235/// Replace every annotated node of `root` by the term it wraps, in place.
236fn erase_in_place(root: &mut Term) {
237    let mut work: Vec<&mut Term> = vec![root];
238    while let Some(node) = work.pop() {
239        if let Term::Annotated { term, .. } = node {
240            let inner = std::mem::replace(term.as_mut(), Term::Hole);
241            *node = inner;
242            work.push(node);
243            continue;
244        }
245        match node {
246            Term::Sort(_)
247            | Term::Var(_)
248            | Term::Global(_)
249            | Term::Const { .. }
250            | Term::Lit(_)
251            | Term::Hole => {}
252            Term::Pi {
253                param_type,
254                body_type: body,
255                ..
256            }
257            | Term::Lambda {
258                param_type, body, ..
259            } => {
260                work.push(param_type);
261                work.push(body);
262            }
263            Term::App(f, a) => {
264                work.push(f);
265                work.push(a);
266            }
267            Term::Match {
268                discriminant,
269                motive,
270                cases,
271            } => {
272                work.push(discriminant);
273                work.push(motive);
274                work.extend(cases.iter_mut());
275            }
276            Term::Fix { body, .. } => work.push(body),
277            Term::MutualFix { defs, .. } => work.extend(defs.iter_mut().map(|(_, body)| body)),
278            Term::Let {
279                ty, value, body, ..
280            } => {
281                work.push(ty);
282                work.push(value);
283                work.push(body);
284            }
285            Term::Annotated { term, .. } => work.push(term),
286        }
287    }
288}
289
290impl Term {
291    /// `term`, carrying `annotation`.
292    pub fn annotated(annotation: Annotation, term: Term) -> Term {
293        Term::Annotated {
294            annotation,
295            term: Box::new(term),
296        }
297    }
298
299    /// The term under every annotation at this term's head.
300    pub fn peel_annotations(&self) -> &Term {
301        let mut current = self;
302        while let Term::Annotated { term, .. } = current {
303            current = term;
304        }
305        current
306    }
307
308    /// The annotations at this term's head, outermost first.
309    pub fn head_annotations(&self) -> impl Iterator<Item = &Annotation> {
310        let mut current = self;
311        std::iter::from_fn(move || match current {
312            Term::Annotated { annotation, term } => {
313                current = term;
314                Some(annotation)
315            }
316            _ => None,
317        })
318    }
319
320    /// The kind recorded on this binder: the outermost [`Annotation::Binder`] at the head of a
321    /// `fun` or `Π`. `None` for a binder that records none, and for a term that is not a binder.
322    pub fn binder_kind(&self) -> Option<ParamKind> {
323        if !matches!(
324            self.peel_annotations(),
325            Term::Lambda { .. } | Term::Pi { .. }
326        ) {
327            return None;
328        }
329        self.head_annotations()
330            .find_map(|annotation| match annotation {
331                Annotation::Binder(kind) => Some(*kind),
332                Annotation::Source(_) => None,
333            })
334    }
335
336    /// Where this term came from: the outermost [`Annotation::Source`] at its head.
337    pub fn provenance(&self) -> Option<&Provenance> {
338        self.head_annotations()
339            .find_map(|annotation| match annotation {
340                Annotation::Source(provenance) => Some(provenance),
341                Annotation::Binder(_) => None,
342            })
343    }
344
345    /// Whether an annotation stands anywhere in this term.
346    pub fn has_annotations(&self) -> bool {
347        let mut work = vec![self];
348        while let Some(t) = work.pop() {
349            if matches!(t, Term::Annotated { .. }) {
350                return true;
351            }
352            push_children(t, &mut work);
353        }
354        false
355    }
356
357    /// This term with every annotation erased — its kernel meaning, the only thing a kernel
358    /// judgment reads. Erasing is idempotent, and `t.strip_annotations() == t` always holds.
359    pub fn strip_annotations(&self) -> Term {
360        self.clone().into_stripped()
361    }
362
363    /// [`strip_annotations`](Self::strip_annotations) on a term the caller owns: a term with no
364    /// annotation is handed back as it is, without a copy.
365    pub fn into_stripped(mut self) -> Term {
366        if self.has_annotations() {
367            erase_in_place(&mut self);
368        }
369        self
370    }
371
372    /// Structural equality that SEES annotations: same shape, same names, same annotations in
373    /// the same places. `==` is the kernel's identity and ignores them.
374    pub fn identical(&self, other: &Term) -> bool {
375        let mut work = vec![(self, other)];
376        while let Some(pair) = work.pop() {
377            match pair {
378                (
379                    Term::Annotated {
380                        annotation: a,
381                        term: s,
382                    },
383                    Term::Annotated {
384                        annotation: b,
385                        term: t,
386                    },
387                ) => {
388                    if a != b {
389                        return false;
390                    }
391                    work.push((s, t));
392                }
393                (Term::Sort(a), Term::Sort(b)) if a == b => {}
394                (Term::Var(a), Term::Var(b)) | (Term::Global(a), Term::Global(b)) if a == b => {}
395                (Term::Const { name: m, levels: a }, Term::Const { name: n, levels: b })
396                    if m == n && a == b => {}
397                (
398                    Term::Pi {
399                        param: p,
400                        param_type: s1,
401                        body_type: s2,
402                    },
403                    Term::Pi {
404                        param: q,
405                        param_type: t1,
406                        body_type: t2,
407                    },
408                )
409                | (
410                    Term::Lambda {
411                        param: p,
412                        param_type: s1,
413                        body: s2,
414                    },
415                    Term::Lambda {
416                        param: q,
417                        param_type: t1,
418                        body: t2,
419                    },
420                ) if p == q => {
421                    work.push((s1, t1));
422                    work.push((s2, t2));
423                }
424                (Term::App(f, a), Term::App(g, b)) => {
425                    work.push((f, g));
426                    work.push((a, b));
427                }
428                (
429                    Term::Match {
430                        discriminant: d,
431                        motive: m,
432                        cases: cs,
433                    },
434                    Term::Match {
435                        discriminant: e,
436                        motive: n,
437                        cases: ds,
438                    },
439                ) if cs.len() == ds.len() => {
440                    work.push((d, e));
441                    work.push((m, n));
442                    work.extend(cs.iter().zip(ds.iter()));
443                }
444                (Term::Fix { name: m, body: s }, Term::Fix { name: n, body: t }) if m == n => {
445                    work.push((s, t));
446                }
447                (Term::MutualFix { defs: a, index: i }, Term::MutualFix { defs: b, index: j })
448                    if i == j
449                        && a.len() == b.len()
450                        && a.iter().zip(b.iter()).all(|((m, _), (n, _))| m == n) =>
451                {
452                    work.extend(a.iter().zip(b.iter()).map(|((_, s), (_, t))| (s, t)));
453                }
454                (
455                    Term::Let {
456                        name: m,
457                        ty: s1,
458                        value: s2,
459                        body: s3,
460                    },
461                    Term::Let {
462                        name: n,
463                        ty: t1,
464                        value: t2,
465                        body: t3,
466                    },
467                ) if m == n => {
468                    work.push((s1, t1));
469                    work.push((s2, t2));
470                    work.push((s3, t3));
471                }
472                (Term::Lit(a), Term::Lit(b)) if a == b => {}
473                (Term::Hole, Term::Hole) => {}
474                _ => return false,
475            }
476        }
477        true
478    }
479}
480
481thread_local! {
482    /// Whether a kernel door on this thread has already erased the terms of the call under way.
483    static ERASED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
484}
485
486/// Holds [`ERASED`] for the call a door opened, restoring it on the way out — including on a
487/// panic, so the flag is never left stale.
488struct ErasedScope(bool);
489
490impl Drop for ErasedScope {
491    fn drop(&mut self) {
492        ERASED.with(|erased| erased.set(self.0));
493    }
494}
495
496impl ErasedScope {
497    fn open() -> ErasedScope {
498        ErasedScope(ERASED.with(|erased| erased.replace(true)))
499    }
500}
501
502/// Whether the call under way on this thread was entered through a kernel door, so every term it
503/// reads — its input and everything the context holds — is annotation-free.
504pub(crate) fn within_erased_call() -> bool {
505    ERASED.with(|erased| erased.get())
506}
507
508/// THE DOOR of a kernel judgment over one term, in the shape of `term_store::deepen_stack`:
509/// `Some(judge(erased))` at the OUTERMOST call, where the term is erased once (and not copied at
510/// all when it carries no annotation), and `None` on the re-entrant call inside `judge`, which then
511/// runs its own body. The scan is paid once per outermost call, before the depth door measures
512/// anything — an annotation adds nesting to the representation and none to the term, so the kernel
513/// must refuse exactly the terms it would refuse without it.
514pub(crate) fn erased<R>(term: &Term, judge: impl FnOnce(&Term) -> R) -> Option<R> {
515    if within_erased_call() {
516        return None;
517    }
518    let _scope = ErasedScope::open();
519    Some(if term.has_annotations() {
520        judge(&term.strip_annotations())
521    } else {
522        judge(term)
523    })
524}
525
526/// [`erased`] for a judgment over two terms.
527pub(crate) fn erased_pair<R>(
528    a: &Term,
529    b: &Term,
530    judge: impl FnOnce(&Term, &Term) -> R,
531) -> Option<R> {
532    if within_erased_call() {
533        return None;
534    }
535    let _scope = ErasedScope::open();
536    Some(match (a.has_annotations(), b.has_annotations()) {
537        (false, false) => judge(a, b),
538        (true, false) => judge(&a.strip_annotations(), b),
539        (false, true) => judge(a, &b.strip_annotations()),
540        (true, true) => judge(&a.strip_annotations(), &b.strip_annotations()),
541    })
542}
543
544/// A term as the [`Context`](crate::Context) stores it: erased, so a lookup never hands an
545/// annotation into a kernel call. A term stored from inside a kernel call is already erased, and
546/// is not scanned again.
547pub(crate) fn stored(term: Term) -> Term {
548    if within_erased_call() {
549        term
550    } else {
551        term.into_stripped()
552    }
553}
554
555#[cfg(test)]
556#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/annotation/tests.rs"]
557mod tests;