Skip to main content

logicaffeine_kernel/dag/
relevance.rs

1//! Argument relevance: which arguments of a definition cannot change what it computes.
2//!
3//! When two applications of the same definition are compared, Lean's algorithm first tries the
4//! arguments pairwise and only unfolds on failure. Comparing an argument the definition never
5//! uses can cost arbitrarily much (Arena `perf/discarded-argument`: two numerals evaluated in
6//! Θ(n²) only to be thrown away). An argument position `i` is *ignorable* when either
7//!
8//! * the definition's value, after its leading `fun`s, never mentions parameter `i`, and the rest
9//!   of its type does not depend on it — unfolding substitutes into a body with no occurrence of
10//!   the parameter, so `c … aᵢ …` and `c … bᵢ …` unfold to the same term whenever the other
11//!   arguments agree; or
12//! * the parameter's type is a proposition — two proofs of definitionally equal propositions are
13//!   definitionally equal (proof irrelevance), and the propositions are equal whenever the earlier
14//!   arguments are.
15//!
16//! Both facts are properties of the settled constant (and, for proof-ness, its universe
17//! arguments), so they are computed once and shared by every declaration.
18
19use super::error::KResult;
20use super::expr::{ExprId, Node};
21use super::level::LevelsId;
22use super::name::NameId;
23use super::tc::Kernel;
24use rustc_hash::FxHashMap as FxMap;
25
26/// Positions beyond this are always compared.
27pub const MAX_TRACKED: usize = 64;
28
29/// Relevance facts, keyed on settled constants only.
30#[derive(Clone, Default)]
31pub struct Cache {
32    absent: FxMap<NameId, u64>,
33    proof: FxMap<(NameId, LevelsId), u64>,
34    signatures: FxMap<(NameId, LevelsId), Signature>,
35    /// Signatures being computed: one asked for again while its own computation runs (a
36    /// reduction inside the telescope's walk that compares applications of the constant) is
37    /// unknown for that inner question.
38    pub(crate) computing: rustc_hash::FxHashSet<(NameId, LevelsId)>,
39}
40
41/// What a settled constant's type says about its applications, at one universe instantiation.
42///
43/// The type's telescope `Π (x₀ : A₀) … (xₙ₋₁ : Aₙ₋₁), B`, walked once under fresh variables,
44/// gives the universe `uᵢ` of every binder type and `v` of the result. Universes do not depend on
45/// terms, so these are the universes of every application's parts too: an argument at position
46/// `i` is a proof exactly when `uᵢ` is zero, and an application to `k ≤ n` arguments, whose type
47/// is `Π (xₖ : Aₖ) … , B`, lives in `imax(uₖ, … imax(uₙ₋₁, v))` — zero exactly when `v` is. So
48/// whether an application is a proof is read off its head and its argument count, without
49/// inferring a type. Past the telescope — a result type that becomes a Π only once arguments are
50/// substituted — nothing is known.
51#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
52pub struct Signature {
53    proof_args: u64,
54    /// Bit `k`: an application to `k` arguments is a proof (meaningful where `known` has bit `k`).
55    proofs: u64,
56    known: u64,
57    /// The telescope's length when it ends within tracking, and — when its result `B` is a sort —
58    /// whether that sort is `Prop` (the full application is then a proposition).
59    full: Option<(u32, Option<bool>)>,
60}
61
62impl Signature {
63    pub(crate) fn new(
64        proof_args: u64,
65        proofs: u64,
66        known: u64,
67        full: Option<(u32, Option<bool>)>,
68    ) -> Signature {
69        Signature {
70            proof_args,
71            proofs,
72            known,
73            full,
74        }
75    }
76
77    /// The argument positions (below [`MAX_TRACKED`]) whose binder type is a proposition.
78    pub fn proof_args(&self) -> u64 {
79        self.proof_args
80    }
81
82    /// Whether an application to `k` arguments is a proof, if the telescope says.
83    #[inline]
84    pub fn is_proof(&self, k: usize) -> Option<bool> {
85        (k < MAX_TRACKED && (self.known >> k) & 1 == 1).then(|| (self.proofs >> k) & 1 == 1)
86    }
87
88    /// Whether an application to `k` arguments is a proposition, if the telescope says: only a
89    /// full application whose result is a sort is a type at all.
90    #[inline]
91    pub fn is_proposition(&self, k: usize) -> Option<bool> {
92        match self.full {
93            Some((n, p)) if n as usize == k => p,
94            _ => None,
95        }
96    }
97}
98
99impl Kernel {
100    /// The argument positions of `c.{ls}` whose arguments need not be compared (see the module
101    /// documentation). Zero for anything that is not a settled, unfoldable definition.
102    pub fn ignorable_args(&mut self, c: NameId, ls: LevelsId) -> KResult<u64> {
103        let Some(absent) = self.absent_mask(c, ls) else {
104            return Ok(0);
105        };
106        let proof = match self.relevance.proof.get(&(c, ls)) {
107            Some(m) => *m,
108            None => {
109                let k = self.env.get(c).cloned().expect("an unfoldable definition");
110                let ty = self.instantiate_type_lparams(&k, ls);
111                let m = self.proof_args(ty)?;
112                self.relevance.proof.insert((c, ls), m);
113                m
114            }
115        };
116        Ok(absent | proof)
117    }
118
119    /// The positions `c`'s value never mentions, or `None` when `c.{ls}` is not a settled,
120    /// unfoldable definition (whose arguments must then all be compared).
121    pub fn absent_mask(&mut self, c: NameId, ls: LevelsId) -> Option<u64> {
122        if !self.env.is_settled(c) {
123            return None;
124        }
125        let k = self.env.get(c)?.clone();
126        let value = k.delta_value()?;
127        if self.s.levels.get_list(ls).len() != k.lparams.len() {
128            return None;
129        }
130        Some(match self.relevance.absent.get(&c) {
131            Some(m) => *m,
132            None => {
133                let m = self.absent_args(k.ty, value);
134                self.relevance.absent.insert(c, m);
135                m
136            }
137        })
138    }
139
140    /// The signature of `c.{ls}`, if some engine has already computed it.
141    #[inline]
142    pub(crate) fn known_signature(&self, c: NameId, ls: LevelsId) -> Option<Signature> {
143        self.relevance.signatures.get(&(c, ls)).copied()
144    }
145
146    /// Records the signature of the settled constant `c.{ls}` (and its proof positions).
147    pub(crate) fn note_signature(&mut self, c: NameId, ls: LevelsId, s: Signature) {
148        self.relevance.signatures.insert((c, ls), s);
149        self.relevance.proof.entry((c, ls)).or_insert(s.proof_args);
150    }
151
152    /// Parameters the value never mentions and the rest of the type does not depend on.
153    fn absent_args(&mut self, ty: ExprId, value: ExprId) -> u64 {
154        let mut body = value;
155        let mut arity = 0usize;
156        while let Node::Lam(_, b) = self.s.node(body) {
157            if arity == MAX_TRACKED {
158                break;
159            }
160            body = b;
161            arity += 1;
162        }
163        if arity == 0 {
164            return 0;
165        }
166        let used = loose_mask(self, body, 0, &mut FxMap::default());
167        let mut absent = 0u64;
168        let mut rest = ty;
169        for i in 0..arity {
170            let Node::Pi(_, cod) = self.s.node(rest) else {
171                break;
172            };
173            let unused = (used >> (arity - 1 - i)) & 1 == 0;
174            let independent = !mentions_bvar0(self, cod);
175            if unused && independent {
176                absent |= 1 << i;
177            }
178            rest = cod;
179        }
180        absent
181    }
182
183    /// Parameters whose type is a proposition, over the instantiated type's telescope.
184    fn proof_args(&mut self, ty: ExprId) -> KResult<u64> {
185        let mut mask = 0u64;
186        let mut cur = ty;
187        for i in 0..MAX_TRACKED {
188            let w = self.whnf(cur)?;
189            let Node::Pi(d, cod) = self.s.node(w) else {
190                break;
191            };
192            if self.is_prop(d)? {
193                mask |= 1 << i;
194            }
195            let fv = self.push_local(d, None);
196            cur = self.s.instantiate1(cod, fv);
197        }
198        Ok(mask)
199    }
200}
201
202/// The loose de Bruijn indices (relative to `depth`, below 64) occurring in `e`.
203fn loose_mask(k: &Kernel, e: ExprId, depth: u32, memo: &mut FxMap<(ExprId, u32), u64>) -> u64 {
204    if k.s.loose_bvar_range(e) <= depth {
205        return 0;
206    }
207    if let Some(m) = memo.get(&(e, depth)) {
208        return *m;
209    }
210    let m = match k.s.node(e) {
211        Node::BVar(i) => {
212            let j = i - depth;
213            if (j as usize) < MAX_TRACKED {
214                1u64 << j
215            } else {
216                0
217            }
218        }
219        Node::App(a, b) => loose_mask(k, a, depth, memo) | loose_mask(k, b, depth, memo),
220        Node::Lam(a, b) | Node::Pi(a, b) => {
221            loose_mask(k, a, depth, memo) | loose_mask(k, b, depth + 1, memo)
222        }
223        Node::Let(a, b, c) => {
224            loose_mask(k, a, depth, memo)
225                | loose_mask(k, b, depth, memo)
226                | loose_mask(k, c, depth + 1, memo)
227        }
228        Node::Proj(_, _, a) => loose_mask(k, a, depth, memo),
229        _ => 0,
230    };
231    memo.insert((e, depth), m);
232    m
233}
234
235/// Does `e` mention the innermost enclosing binder (`BVar(0)` at depth 0)?
236fn mentions_bvar0(k: &Kernel, e: ExprId) -> bool {
237    loose_mask(k, e, 0, &mut FxMap::default()) & 1 == 1
238}