Skip to main content

logicaffeine_kernel/dag/sem/
mod.rs

1//! The value engine: type checking and definitional equality by evaluation into closures.
2//!
3//! The substitution checker ([`crate::dag::tc`]) crosses a binder by substituting a free variable
4//! into the body and β-reduces by substituting an argument into a body — each a copy of
5//! everything the variable is under. Here nothing is ever substituted. A term is *evaluated* in
6//! an environment that gives each bound variable a value: β extends the environment, a binder
7//! is crossed by binding a fresh variable, δ evaluates a definition's body once per universe
8//! instantiation and applies it to the arguments. Evaluation performs β only; δ and ι wait in
9//! *neutral* values (a head and its arguments) until conversion or inference demands a weak head,
10//! and each such step is remembered on the value that asked for it.
11//!
12//! Types are values too: inferring an application instantiates its function type's closure with
13//! the argument's value, and a lambda's type is a closure whose codomain is inferred only when
14//! someone asks for it. The judgment is the substitution checker's rule for rule, and conversion
15//! is Lean's algorithm (lazy δ by definitional height, proof irrelevance, η, struct-η, unit-like
16//! types, K, quotients, `Nat`/`String` literals) — only the objects differ.
17//!
18//! Values live for a *session* of many declarations: a constant's type, its unfolded body, the
19//! weak-head normal form of a closed application are computed once and shared by every later
20//! declaration, until the session's arena outgrows its budget or the environment changes the
21//! meaning of something already evaluated (see [`Kernel::sem_epoch`]).
22//!
23//! A variable is identified by its binder depth and its type, so the variable a check binds and
24//! the variable a later comparison binds at the same depth with the same type are one value, and
25//! everything known about one is known about the other. That identification is sound exactly
26//! when no two variables in scope share a depth — the one invariant `Sem::bind` keeps.
27
28pub mod conv;
29pub mod eval;
30pub mod infer;
31pub mod table;
32pub mod types;
33pub mod value;
34pub mod whnf;
35
36use super::error::{KResult, KernelFailure};
37use super::expr::ExprId;
38use super::level::{LevelId, SubstId};
39use super::tc::Kernel;
40use table::{StampedTable, Table};
41use value::{Arena, CtxId, EnvId, V};
42
43/// Recursion bound for evaluation, reduction, inference and conversion (the checker thread's
44/// stack is sized for it).
45const MAX_DEPTH: u32 = 200_000;
46
47/// A session ends, and every value is dropped, once the arena holds this many values.
48const SESSION_VALUES: usize = 1 << 23;
49
50/// One session's values and caches.
51#[derive(Clone)]
52pub struct State {
53    pub arena: Arena,
54    epoch: u64,
55    const_type: Table<(u32, u32)>,
56    const_body: Table<(u32, u32)>,
57    rule_rhs: Table<(u32, u32, u32)>,
58    eval_cache: Table<(u32, u32)>,
59    prune_cache: Table<(u32, u64)>,
60    /// Pruning for expressions beyond the 64-bit masks, keyed on (environment, expression).
61    wide_prune_cache: Table<(u32, u32)>,
62    /// Exact loose-variable sets of such expressions. A function of the (persistent) expression
63    /// alone, so it outlives sessions.
64    loose_sets: rustc_hash::FxHashMap<ExprId, std::rc::Rc<[u32]>>,
65    /// Types inferred without checking, keyed on the environment and the expression. A checked
66    /// inference is also an inferred one, so check results land here too.
67    infer_cache: Table<(u32, u32)>,
68    /// Types inferred WITH checking. Checking depends on the declaration's universe parameters
69    /// and safety, so this is cleared at every declaration.
70    check_cache: StampedTable<(u32, u32)>,
71    /// Types inferred with checking in a SAFE declaration, of expressions that mention no
72    /// universe parameter: such a check holds in every later declaration (no level parameter
73    /// can be undeclared, and safe is the strictest safety), so these outlive the declaration.
74    safe_checked: Table<(u32, u32)>,
75    /// Types inferred with checking in a SAFE declaration, of expressions that do mention
76    /// universe parameters, keyed also on the declaration's parameter list (`scope`): the check
77    /// holds in every later safe-or-weaker declaration with exactly those parameters.
78    scoped_checked: Table<(u32, u32, u32)>,
79    /// The current declaration's universe parameters, as an interned list.
80    scope: u32,
81    /// Pairs of values proven definitionally equal.
82    proven: Table<(u32, u32)>,
83    /// Pairs of applications of one definition whose arguments were found to differ.
84    failure: Table<(u32, u32)>,
85    depth: u32,
86    /// The binder depth: the level the next bound variable gets.
87    level: u32,
88    /// Lean's `eagerReduce`.
89    eager: bool,
90    /// The conversion steps a speculative probe may still take (conv.rs), while one runs.
91    pub(crate) probe: Option<u32>,
92    /// A value or cache key of this session refers to a term of the current declaration's
93    /// scratch region (ids recycled when the declaration ends), or to a declaration that is only
94    /// provisionally in the environment: the session must not outlive the declaration.
95    scratch_seen: bool,
96    pub counters: Counters,
97    /// The values of recently evaluated constants and sorts, by expression id and the universe
98    /// substitution they were read under, direct-mapped: a hash-consed constant is one id, so its
99    /// value is found without substituting its levels or hashing its head. Only persistent ids: a
100    /// scratch id names another term in the next declaration.
101    atoms: Vec<(u32, u32, u32)>,
102}
103
104const ATOM_MEMO: usize = 1 << 16;
105
106/// How the session's caches are doing (reported by `logos_kernel --stats`).
107#[derive(Default, Clone, Debug)]
108pub struct Counters {
109    pub sessions: u64,
110    pub resets_epoch: u64,
111    pub resets_scratch: u64,
112    pub resets_budget: u64,
113    pub scratch_unsettled: u64,
114    pub scratch_instance: u64,
115    pub scratch_expr: u64,
116    /// Values whose proof-ness neither their shape nor their head's signature decided, so that
117    /// their type was inferred.
118    pub proof_probes: u64,
119}
120
121impl Default for State {
122    fn default() -> Self {
123        State {
124            arena: Arena::new(),
125            epoch: 0,
126            const_type: Table::new(),
127            const_body: Table::new(),
128            rule_rhs: Table::new(),
129            eval_cache: Table::new(),
130            prune_cache: Table::new(),
131            wide_prune_cache: Table::new(),
132            loose_sets: Default::default(),
133            infer_cache: Table::new(),
134            check_cache: StampedTable::new(),
135            safe_checked: Table::new(),
136            scoped_checked: Table::new(),
137            scope: 0,
138            proven: Table::new(),
139            failure: Table::new(),
140            depth: 0,
141            level: 0,
142            eager: false,
143            probe: None,
144            scratch_seen: false,
145            counters: Counters::default(),
146            atoms: vec![(u32::MAX, u32::MAX, u32::MAX); ATOM_MEMO],
147        }
148    }
149}
150
151impl State {
152    fn begin_decl(&mut self, epoch: u64) {
153        if self.epoch != epoch || self.scratch_seen || self.arena.len() > SESSION_VALUES {
154            let c = &mut self.counters;
155            c.sessions += 1;
156            c.resets_epoch += (self.epoch != epoch) as u64;
157            c.resets_scratch += self.scratch_seen as u64;
158            c.resets_budget += (self.arena.len() > SESSION_VALUES) as u64;
159            self.end_session(epoch);
160        }
161        self.check_cache.clear();
162        self.depth = 0;
163        self.level = 0;
164        self.eager = false;
165        self.probe = None;
166    }
167
168    #[inline]
169    fn atom_slot(e: ExprId, s: SubstId) -> usize {
170        (e.0 ^ s.0.wrapping_mul(0x9E37_79B9)) as usize & (ATOM_MEMO - 1)
171    }
172
173    #[inline]
174    pub(crate) fn atom_memo(&self, e: ExprId, s: SubstId) -> Option<V> {
175        let (k, ks, v) = self.atoms[Self::atom_slot(e, s)];
176        (k == e.0 && ks == s.0).then_some(V(v))
177    }
178
179    #[inline]
180    pub(crate) fn note_atom(&mut self, e: ExprId, s: SubstId, v: V) {
181        if !e.is_scratch() {
182            self.atoms[Self::atom_slot(e, s)] = (e.0, s.0, v.0);
183        }
184    }
185
186    /// Drops every value and everything remembered about one, keeping the tables' storage.
187    fn end_session(&mut self, epoch: u64) {
188        self.arena.reset();
189        self.atoms.fill((u32::MAX, u32::MAX, u32::MAX));
190        self.epoch = epoch;
191        self.const_type.clear();
192        self.const_body.clear();
193        self.rule_rhs.clear();
194        self.eval_cache.clear();
195        self.prune_cache.clear();
196        self.wide_prune_cache.clear();
197        self.infer_cache.clear();
198        self.check_cache.clear();
199        self.safe_checked.clear();
200        self.scoped_checked.clear();
201        self.proven.clear();
202        self.failure.clear();
203        self.scratch_seen = false;
204    }
205
206    /// Bytes held by each part of the session (capacity: what the process pays).
207    pub fn memory_report(&self) -> Vec<(&'static str, usize)> {
208        let mut r = self.arena.memory_report();
209        r.push(("value engine: evaluation cache", self.eval_cache.bytes()));
210        r.push((
211            "value engine: pruning cache",
212            self.prune_cache.bytes() + self.wide_prune_cache.bytes(),
213        ));
214        r.push((
215            "value engine: inference caches",
216            self.infer_cache.bytes()
217                + self.check_cache.bytes()
218                + self.safe_checked.bytes()
219                + self.scoped_checked.bytes(),
220        ));
221        r.push((
222            "value engine: conversion caches",
223            self.proven.bytes() + self.failure.bytes(),
224        ));
225        r.push((
226            "value engine: constant caches",
227            self.const_type.bytes() + self.const_body.bytes() + self.rule_rhs.bytes(),
228        ));
229        r
230    }
231
232    pub fn bytes(&self) -> usize {
233        self.memory_report().iter().map(|(_, b)| b).sum()
234    }
235}
236
237/// The engine: the kernel (environment, stores, well-known names) and the session state.
238pub struct Sem<'a> {
239    pub k: &'a mut Kernel,
240    pub st: &'a mut State,
241}
242
243fn reject(m: impl Into<String>) -> KernelFailure {
244    KernelFailure::reject(m)
245}
246
247impl<'a> Sem<'a> {
248    pub fn new(k: &'a mut Kernel, st: &'a mut State) -> Sem<'a> {
249        Sem { k, st }
250    }
251
252    #[inline]
253    pub(crate) fn enter(&mut self) -> KResult<()> {
254        if self.st.depth >= MAX_DEPTH {
255            return Err(KernelFailure::resource("recursion depth limit reached"));
256        }
257        self.st.depth += 1;
258        Ok(())
259    }
260
261    #[inline]
262    pub(crate) fn leave(&mut self) {
263        self.st.depth -= 1;
264    }
265
266    /// A fresh variable of type `ty` at the current binder depth, which is entered: every use
267    /// must be matched by [`Sem::unbind`] before a sibling binder is crossed.
268    #[inline]
269    pub(crate) fn bind(&mut self, ty: V) -> V {
270        let level = self.st.level;
271        self.st.level += 1;
272        self.st
273            .arena
274            .head_value(value::Kind::Local, value::Head::Local { level, ty })
275    }
276
277    #[inline]
278    pub(crate) fn unbind(&mut self) {
279        self.st.level -= 1;
280    }
281
282    /// Runs `f` under a fresh variable of type `ty`.
283    pub fn under<T>(&mut self, ty: V, f: impl FnOnce(&mut Self, V) -> KResult<T>) -> KResult<T> {
284        let x = self.bind(ty);
285        let r = f(self, x);
286        self.unbind();
287        r
288    }
289
290    // ---- declaration entry points ----
291
292    /// Checks that the closed term `ty` is a type; its universe level.
293    pub fn check_type(&mut self, ty: ExprId) -> KResult<LevelId> {
294        let t = self.infer(EnvId::EMPTY, CtxId::EMPTY, ty, true)?;
295        match self.sort_of(t)? {
296            Some(l) => Ok(l),
297            None => Err(reject(format!("type expected, found {}", self.k.show(ty)))),
298        }
299    }
300
301    /// Checks the closed term `value` against the closed, already checked type `ty`.
302    pub fn check_value(&mut self, value: ExprId, ty: ExprId) -> KResult<()> {
303        let vt = self.infer(EnvId::EMPTY, CtxId::EMPTY, value, true)?;
304        let tv = self.eval(EnvId::EMPTY, ty)?;
305        if !self.is_def_eq(vt, tv)? {
306            return Err(reject("declaration type mismatch").with_detail(format!(
307                "the value has type\n    {}\n  but is declared with type\n    {}",
308                self.show(vt),
309                self.show(tv)
310            )));
311        }
312        Ok(())
313    }
314
315    /// The level of a sort value after weak-head normalisation.
316    pub(crate) fn sort_of(&mut self, t: V) -> KResult<Option<LevelId>> {
317        let w = self.whnf(t)?;
318        Ok(match self.st.arena.get(w) {
319            value::Val::Sort(l) => Some(l),
320            _ => None,
321        })
322    }
323
324    /// A one-line description of a value, for diagnostics.
325    pub fn show(&mut self, v: V) -> String {
326        use value::{Head, Val};
327        match self.st.arena.get(v) {
328            Val::Sort(l) => format!("Sort {l:?}"),
329            Val::Nat(n) => self.k.s.nat(n).to_decimal(),
330            Val::Str(s) => format!("{:?}", self.k.s.string(s)),
331            Val::Lam { body, .. } => format!("fun _ => {}", self.k.show(body)),
332            Val::Pi { dom, env, body } => match self.k.s.node(body) {
333                // A codomain read from the closure's environment: show the value it reads.
334                crate::dag::expr::Node::BVar(i) if i > 0 => {
335                    match self.st.arena.lookup(env, i - 1) {
336                        Some(cod) => format!("({}) → {}", self.show(dom), self.show(cod)),
337                        None => format!("({}) → {}", self.show(dom), self.k.show(body)),
338                    }
339                }
340                _ => format!("({}) → {}", self.show(dom), self.k.show(body)),
341            },
342            Val::PiLam { dom, body, .. } => format!("({}) → {}", self.show(dom), self.k.show(body)),
343            Val::Neu { head, spine, .. } => {
344                let h = match self.st.arena.head(head) {
345                    Head::Local { level, .. } => format!("x{level}"),
346                    Head::Const { name, .. } => self.k.s.names.display(name),
347                    Head::Proj { idx, x, .. } => format!("({}).{}", self.show(x), idx + 1),
348                };
349                let n = self.st.arena.spine_len(spine);
350                let args: Vec<String> = self
351                    .st
352                    .arena
353                    .args(spine)
354                    .iter()
355                    .take(6)
356                    .map(|a| self.show_atom(*a))
357                    .collect();
358                if n == 0 {
359                    h
360                } else if n > 6 {
361                    format!("{h} {} …", args.join(" "))
362                } else {
363                    format!("{h} {}", args.join(" "))
364                }
365            }
366        }
367    }
368
369    fn show_atom(&mut self, v: V) -> String {
370        use value::{Head, Val};
371        match self.st.arena.get(v) {
372            Val::Neu { head, spine, .. } if spine == value::Sp::NIL => {
373                match self.st.arena.head(head) {
374                    Head::Local { level, .. } => format!("x{level}"),
375                    Head::Const { name, .. } => self.k.s.names.display(name),
376                    Head::Proj { idx, .. } => format!("_.{}", idx + 1),
377                }
378            }
379            Val::Nat(n) => self.k.s.nat(n).to_decimal(),
380            Val::Sort(_) => "Sort _".into(),
381            _ => "(…)".into(),
382        }
383    }
384}
385
386impl Kernel {
387    /// Runs the value engine on this kernel with its session state.
388    pub fn with_sem<T>(&mut self, f: impl FnOnce(&mut Sem) -> KResult<T>) -> KResult<T> {
389        let mut st = self.sem.take().unwrap_or_default();
390        st.begin_decl(self.sem_epoch());
391        let ps = self.lparams.clone();
392        st.scope = self.const_levels_of(&ps).0;
393        let r = {
394            let mut sem = Sem::new(self, &mut st);
395            f(&mut sem)
396        };
397        self.sem = Some(st);
398        r
399    }
400
401    /// Changes whenever something already evaluated may now mean something else: the quotient
402    /// reductions were switched on, or a primitive was validated (its literal extension switched
403    /// on). The value engine's session never spans two epochs.
404    ///
405    /// A withdrawn declaration needs no epoch: only a provisional declaration is ever withdrawn,
406    /// and a session that touches one is marked (`scratch_seen`) and ends with its declaration.
407    pub fn sem_epoch(&self) -> u64 {
408        ((self.validated_primitives.len() as u64) << 1) | self.env.quot_init as u64
409    }
410}