Skip to main content

oxilean_kernel/def_eq/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6use crate::equiv_manager::EquivManager;
7use crate::expr_util::{
8    get_app_args, get_app_fn, get_app_fn_args, has_loose_bvar, has_loose_bvars, mk_app,
9};
10use crate::infer::proj_prop::{is_prop_sort, structure_type_sort};
11use crate::infer::proj_target::{proj_target, proj_type_params};
12use crate::instantiate::instantiate_type_lparams;
13use crate::level;
14use crate::reduce::{Reducer, ReducibilityHint, TransparencyMode};
15use crate::subst::instantiate;
16use crate::Node;
17use crate::{Environment, Expr, Level};
18use std::collections::HashMap;
19use std::rc::Rc;
20
21pub use super::support_types::*;
22
23/// A batch checker that can check multiple pairs efficiently.
24#[allow(dead_code)]
25pub struct BatchDefEqChecker<'env> {
26    checker: DefEqChecker<'env>,
27}
28impl<'env> BatchDefEqChecker<'env> {
29    /// Create a new batch checker.
30    #[allow(dead_code)]
31    pub fn new(env: &'env Environment) -> Self {
32        Self {
33            checker: DefEqChecker::new(env),
34        }
35    }
36    /// Check definitional equality for a single pair.
37    #[allow(dead_code)]
38    pub fn check(&mut self, t: &Expr, s: &Expr) -> bool {
39        self.checker.is_def_eq(t, s)
40    }
41    /// Check all pairs in a list; returns true iff all pairs are def-equal.
42    #[allow(dead_code)]
43    pub fn check_all(&mut self, pairs: &[(Expr, Expr)]) -> bool {
44        pairs.iter().all(|(t, s)| self.checker.is_def_eq(t, s))
45    }
46    /// Check any pair in a list; returns true iff at least one pair is def-equal.
47    #[allow(dead_code)]
48    pub fn check_any(&mut self, pairs: &[(Expr, Expr)]) -> bool {
49        pairs.iter().any(|(t, s)| self.checker.is_def_eq(t, s))
50    }
51    /// Count how many pairs in the list are definitionally equal.
52    #[allow(dead_code)]
53    pub fn count_equal(&mut self, pairs: &[(Expr, Expr)]) -> usize {
54        pairs
55            .iter()
56            .filter(|(t, s)| self.checker.is_def_eq(t, s))
57            .count()
58    }
59    /// Reset the checker (clears caches).
60    #[allow(dead_code)]
61    pub fn reset(&mut self) {
62        self.checker.cache.clear();
63        self.checker.equiv_manager.clear();
64    }
65}
66/// Statistics for a DefEq check run.
67#[allow(dead_code)]
68#[derive(Debug, Clone, Default)]
69pub struct DefEqStats {
70    /// Number of cache hits.
71    pub cache_hits: u64,
72    /// Number of cache misses.
73    pub cache_misses: u64,
74    /// Number of reduction steps taken.
75    pub reduction_steps: u64,
76    /// Number of successful delta reductions.
77    pub delta_reductions: u64,
78    /// Number of beta reductions.
79    pub beta_reductions: u64,
80    /// Number of eta expansions tried.
81    pub eta_attempts: u64,
82    /// Number of equiv-manager hits.
83    pub equiv_hits: u64,
84}
85impl DefEqStats {
86    /// Compute the cache hit rate in [0, 1].
87    #[allow(dead_code)]
88    pub fn cache_hit_rate(&self) -> f64 {
89        let total = self.cache_hits + self.cache_misses;
90        if total == 0 {
91            1.0
92        } else {
93            self.cache_hits as f64 / total as f64
94        }
95    }
96    /// Compute total number of cache accesses.
97    #[allow(dead_code)]
98    pub fn total_cache_accesses(&self) -> u64 {
99        self.cache_hits + self.cache_misses
100    }
101    /// Compute total number of reductions across all kinds.
102    #[allow(dead_code)]
103    pub fn total_reductions(&self) -> u64 {
104        self.reduction_steps + self.delta_reductions + self.beta_reductions
105    }
106}
107/// A reusable scratch buffer for path computations.
108#[allow(dead_code)]
109pub struct PathBuf {
110    components: Vec<String>,
111}
112#[allow(dead_code)]
113impl PathBuf {
114    /// Creates a new empty path buffer.
115    pub fn new() -> Self {
116        Self {
117            components: Vec::new(),
118        }
119    }
120    /// Pushes a component.
121    pub fn push(&mut self, comp: impl Into<String>) {
122        self.components.push(comp.into());
123    }
124    /// Pops the last component.
125    pub fn pop(&mut self) {
126        self.components.pop();
127    }
128    /// Returns the current path as a `/`-separated string.
129    pub fn as_str(&self) -> String {
130        self.components.join("/")
131    }
132    /// Returns the depth of the path.
133    pub fn depth(&self) -> usize {
134        self.components.len()
135    }
136    /// Clears the path.
137    pub fn clear(&mut self) {
138        self.components.clear();
139    }
140}
141/// A simple decision tree node for rule dispatching.
142#[allow(dead_code)]
143#[allow(missing_docs)]
144pub enum DecisionNode {
145    /// A leaf with an action string.
146    Leaf(String),
147    /// An interior node: check `key` equals `val` → `yes_branch`, else `no_branch`.
148    Branch {
149        key: String,
150        val: String,
151        yes_branch: Box<DecisionNode>,
152        no_branch: Box<DecisionNode>,
153    },
154}
155#[allow(dead_code)]
156impl DecisionNode {
157    /// Evaluates the decision tree with the given context.
158    pub fn evaluate(&self, ctx: &std::collections::HashMap<String, String>) -> &str {
159        match self {
160            DecisionNode::Leaf(action) => action.as_str(),
161            DecisionNode::Branch {
162                key,
163                val,
164                yes_branch,
165                no_branch,
166            } => {
167                let actual = ctx.get(key).map(|s| s.as_str()).unwrap_or("");
168                if actual == val.as_str() {
169                    yes_branch.evaluate(ctx)
170                } else {
171                    no_branch.evaluate(ctx)
172                }
173            }
174        }
175    }
176    /// Returns the depth of the decision tree.
177    pub fn depth(&self) -> usize {
178        match self {
179            DecisionNode::Leaf(_) => 0,
180            DecisionNode::Branch {
181                yes_branch,
182                no_branch,
183                ..
184            } => 1 + yes_branch.depth().max(no_branch.depth()),
185        }
186    }
187}
188/// A sparse vector: stores only non-default elements.
189#[allow(dead_code)]
190pub struct SparseVec<T: Default + Clone + PartialEq> {
191    entries: std::collections::HashMap<usize, T>,
192    default_: T,
193    logical_len: usize,
194}
195#[allow(dead_code)]
196impl<T: Default + Clone + PartialEq> SparseVec<T> {
197    /// Creates a new sparse vector with logical length `len`.
198    pub fn new(len: usize) -> Self {
199        Self {
200            entries: std::collections::HashMap::new(),
201            default_: T::default(),
202            logical_len: len,
203        }
204    }
205    /// Sets element at `idx`.
206    pub fn set(&mut self, idx: usize, val: T) {
207        if val == self.default_ {
208            self.entries.remove(&idx);
209        } else {
210            self.entries.insert(idx, val);
211        }
212    }
213    /// Gets element at `idx`.
214    pub fn get(&self, idx: usize) -> &T {
215        self.entries.get(&idx).unwrap_or(&self.default_)
216    }
217    /// Returns the logical length.
218    pub fn len(&self) -> usize {
219        self.logical_len
220    }
221    /// Returns whether the collection is empty.
222    pub fn is_empty(&self) -> bool {
223        self.len() == 0
224    }
225    /// Returns the number of non-default elements.
226    pub fn nnz(&self) -> usize {
227        self.entries.len()
228    }
229}
230/// A counter that can measure elapsed time between snapshots.
231#[allow(dead_code)]
232pub struct Stopwatch {
233    start: crate::wall_clock::Instant,
234    splits: Vec<f64>,
235}
236#[allow(dead_code)]
237impl Stopwatch {
238    /// Creates and starts a new stopwatch.
239    pub fn start() -> Self {
240        Self {
241            start: crate::wall_clock::Instant::now(),
242            splits: Vec::new(),
243        }
244    }
245    /// Records a split time (elapsed since start).
246    pub fn split(&mut self) {
247        self.splits.push(self.elapsed_ms());
248    }
249    /// Returns total elapsed milliseconds since start.
250    pub fn elapsed_ms(&self) -> f64 {
251        self.start.elapsed().as_secs_f64() * 1000.0
252    }
253    /// Returns all recorded split times.
254    pub fn splits(&self) -> &[f64] {
255        &self.splits
256    }
257    /// Returns the number of splits.
258    pub fn num_splits(&self) -> usize {
259        self.splits.len()
260    }
261}
262/// A label set for a graph node.
263#[allow(dead_code)]
264pub struct LabelSet {
265    labels: Vec<String>,
266}
267#[allow(dead_code)]
268impl LabelSet {
269    /// Creates a new empty label set.
270    pub fn new() -> Self {
271        Self { labels: Vec::new() }
272    }
273    /// Adds a label (deduplicates).
274    pub fn add(&mut self, label: impl Into<String>) {
275        let s = label.into();
276        if !self.labels.contains(&s) {
277            self.labels.push(s);
278        }
279    }
280    /// Returns `true` if `label` is present.
281    pub fn has(&self, label: &str) -> bool {
282        self.labels.iter().any(|l| l == label)
283    }
284    /// Returns the count of labels.
285    pub fn count(&self) -> usize {
286        self.labels.len()
287    }
288    /// Returns all labels.
289    pub fn all(&self) -> &[String] {
290        &self.labels
291    }
292}
293/// A pool of reusable string buffers.
294#[allow(dead_code)]
295pub struct StringPool {
296    free: Vec<String>,
297}
298#[allow(dead_code)]
299impl StringPool {
300    /// Creates a new empty string pool.
301    pub fn new() -> Self {
302        Self { free: Vec::new() }
303    }
304    /// Takes a string from the pool (may be empty).
305    pub fn take(&mut self) -> String {
306        self.free.pop().unwrap_or_default()
307    }
308    /// Returns a string to the pool.
309    pub fn give(&mut self, mut s: String) {
310        s.clear();
311        self.free.push(s);
312    }
313    /// Returns the number of free strings in the pool.
314    pub fn free_count(&self) -> usize {
315        self.free.len()
316    }
317}
318/// Status of a lazy delta reduction step.
319#[derive(Debug, PartialEq, Eq)]
320pub enum ReductionStatus {
321    /// Continue: reduced one or both sides.
322    Continue(Expr, Expr),
323    /// Definitely equal.
324    Equal,
325    /// Definitely not equal (both stuck).
326    Stuck,
327    /// Unknown: need more reduction.
328    Unknown,
329}
330/// A simple stack-based calculator for arithmetic expressions.
331#[allow(dead_code)]
332pub struct StackCalc {
333    stack: Vec<i64>,
334}
335#[allow(dead_code)]
336impl StackCalc {
337    /// Creates a new empty calculator.
338    pub fn new() -> Self {
339        Self { stack: Vec::new() }
340    }
341    /// Pushes an integer literal.
342    pub fn push(&mut self, n: i64) {
343        self.stack.push(n);
344    }
345    /// Replaces the top two values, `b` on top of `a`, with `op(a, b)` and
346    /// returns it.  Returns `None`, leaving the stack unchanged, if it holds
347    /// fewer than two values.
348    fn apply_binary(&mut self, op: impl FnOnce(i64, i64) -> i64) -> Option<i64> {
349        let (rest_len, a, b) = match self.stack[..] {
350            [ref rest @ .., a, b] => (rest.len(), a, b),
351            _ => return None,
352        };
353        let value = op(a, b);
354        self.stack.truncate(rest_len);
355        self.stack.push(value);
356        Some(value)
357    }
358    /// Adds the top two values and returns the sum, which replaces them.
359    /// Returns `None`, leaving the stack unchanged, if it holds fewer than
360    /// two values.
361    pub fn add(&mut self) -> Option<i64> {
362        self.apply_binary(|a, b| a + b)
363    }
364    /// Subtracts top from second and returns the difference, which replaces
365    /// them.  Returns `None`, leaving the stack unchanged, if it holds fewer
366    /// than two values.
367    pub fn sub(&mut self) -> Option<i64> {
368        self.apply_binary(|a, b| a - b)
369    }
370    /// Multiplies the top two values and returns the product, which replaces
371    /// them.  Returns `None`, leaving the stack unchanged, if it holds fewer
372    /// than two values.
373    pub fn mul(&mut self) -> Option<i64> {
374        self.apply_binary(|a, b| a * b)
375    }
376    /// Peeks the top value.
377    pub fn peek(&self) -> Option<i64> {
378        self.stack.last().copied()
379    }
380    /// Returns the stack depth.
381    pub fn depth(&self) -> usize {
382        self.stack.len()
383    }
384}
385/// Configuration for the definitional equality checker.
386#[allow(dead_code)]
387#[derive(Debug, Clone)]
388pub struct DefEqConfig {
389    /// Maximum number of reduction steps before giving up.
390    pub max_steps: u32,
391    /// Whether to use proof irrelevance (Props are equal).
392    pub proof_irrelevance: bool,
393    /// Whether to check eta-expansion.
394    pub eta: bool,
395    /// Whether to try lazy delta reduction.
396    pub lazy_delta: bool,
397    /// Transparency mode for unfolding.
398    pub transparency: TransparencyMode,
399}
400impl DefEqConfig {
401    /// Create a config for checking definitional equality with full unfolding.
402    #[allow(dead_code)]
403    pub fn full_transparency() -> Self {
404        Self {
405            transparency: TransparencyMode::All,
406            ..Self::default()
407        }
408    }
409    /// Create a config for opaque checking (no unfolding).
410    #[allow(dead_code)]
411    pub fn opaque() -> Self {
412        Self {
413            lazy_delta: false,
414            transparency: TransparencyMode::None,
415            ..Self::default()
416        }
417    }
418    /// Create a config with proof irrelevance disabled.
419    #[allow(dead_code)]
420    pub fn no_proof_irrelevance() -> Self {
421        Self {
422            proof_irrelevance: false,
423            ..Self::default()
424        }
425    }
426}
427/// A dependency closure builder (transitive closure via BFS).
428#[allow(dead_code)]
429pub struct TransitiveClosure {
430    adj: Vec<Vec<usize>>,
431    n: usize,
432}
433#[allow(dead_code)]
434impl TransitiveClosure {
435    /// Creates a transitive closure builder for `n` nodes.
436    pub fn new(n: usize) -> Self {
437        Self {
438            adj: vec![Vec::new(); n],
439            n,
440        }
441    }
442    /// Adds a direct edge.
443    pub fn add_edge(&mut self, from: usize, to: usize) {
444        if from < self.n {
445            self.adj[from].push(to);
446        }
447    }
448    /// Computes all nodes reachable from `start` (including `start`).
449    pub fn reachable_from(&self, start: usize) -> Vec<usize> {
450        let mut visited = vec![false; self.n];
451        let mut queue = std::collections::VecDeque::new();
452        queue.push_back(start);
453        while let Some(node) = queue.pop_front() {
454            if node >= self.n || visited[node] {
455                continue;
456            }
457            visited[node] = true;
458            for &next in &self.adj[node] {
459                queue.push_back(next);
460            }
461        }
462        (0..self.n).filter(|&i| visited[i]).collect()
463    }
464    /// Returns `true` if `from` can transitively reach `to`.
465    pub fn can_reach(&self, from: usize, to: usize) -> bool {
466        self.reachable_from(from).contains(&to)
467    }
468}
469/// Simple trie for efficient string prefix lookup.
470#[allow(dead_code)]
471#[derive(Debug, Clone, Default)]
472pub struct StringTrie {
473    pub(super) children: std::collections::HashMap<char, StringTrie>,
474    is_end: bool,
475    pub(super) value: Option<String>,
476}
477impl StringTrie {
478    /// Create a new empty trie.
479    #[allow(dead_code)]
480    pub fn new() -> Self {
481        Self::default()
482    }
483    /// Insert a string into the trie.
484    #[allow(dead_code)]
485    pub fn insert(&mut self, s: &str) {
486        let mut node = self;
487        for c in s.chars() {
488            node = node.children.entry(c).or_default();
489        }
490        node.is_end = true;
491        node.value = Some(s.to_string());
492    }
493    /// Check if a string is in the trie.
494    #[allow(dead_code)]
495    pub fn contains(&self, s: &str) -> bool {
496        let mut node = self;
497        for c in s.chars() {
498            match node.children.get(&c) {
499                Some(next) => node = next,
500                None => return false,
501            }
502        }
503        node.is_end
504    }
505    /// Find all strings with a given prefix.
506    #[allow(dead_code)]
507    pub fn starts_with(&self, prefix: &str) -> Vec<String> {
508        let mut node = self;
509        for c in prefix.chars() {
510            match node.children.get(&c) {
511                Some(next) => node = next,
512                None => return vec![],
513            }
514        }
515        let mut results = Vec::new();
516        collect_strings(node, &mut results);
517        results
518    }
519    /// Get the number of strings in the trie.
520    #[allow(dead_code)]
521    pub fn len(&self) -> usize {
522        let mut count = if self.is_end { 1 } else { 0 };
523        for child in self.children.values() {
524            count += child.len();
525        }
526        count
527    }
528    /// Check if the trie is empty.
529    #[allow(dead_code)]
530    pub fn is_empty(&self) -> bool {
531        self.len() == 0
532    }
533}
534/// A window iterator that yields overlapping windows of size `n`.
535#[allow(dead_code)]
536pub struct WindowIterator<'a, T> {
537    pub(super) data: &'a [T],
538    pub(super) pos: usize,
539    pub(super) window: usize,
540}
541#[allow(dead_code)]
542impl<'a, T> WindowIterator<'a, T> {
543    /// Creates a new window iterator.
544    pub fn new(data: &'a [T], window: usize) -> Self {
545        Self {
546            data,
547            pos: 0,
548            window,
549        }
550    }
551}
552/// A token bucket rate limiter.
553#[allow(dead_code)]
554pub struct TokenBucket {
555    capacity: u64,
556    tokens: u64,
557    refill_per_ms: u64,
558    last_refill: crate::wall_clock::Instant,
559}
560#[allow(dead_code)]
561impl TokenBucket {
562    /// Creates a new token bucket.
563    pub fn new(capacity: u64, refill_per_ms: u64) -> Self {
564        Self {
565            capacity,
566            tokens: capacity,
567            refill_per_ms,
568            last_refill: crate::wall_clock::Instant::now(),
569        }
570    }
571    /// Attempts to consume `n` tokens.  Returns `true` on success.
572    pub fn try_consume(&mut self, n: u64) -> bool {
573        self.refill();
574        if self.tokens >= n {
575            self.tokens -= n;
576            true
577        } else {
578            false
579        }
580    }
581    fn refill(&mut self) {
582        let now = crate::wall_clock::Instant::now();
583        let elapsed_ms = now.duration_since(self.last_refill).as_millis() as u64;
584        if elapsed_ms > 0 {
585            let new_tokens = elapsed_ms * self.refill_per_ms;
586            self.tokens = (self.tokens + new_tokens).min(self.capacity);
587            self.last_refill = now;
588        }
589    }
590    /// Returns the number of currently available tokens.
591    pub fn available(&self) -> u64 {
592        self.tokens
593    }
594    /// Returns the bucket capacity.
595    pub fn capacity(&self) -> u64 {
596        self.capacity
597    }
598}
599/// A set of rewrite rules.
600#[allow(dead_code)]
601pub struct RewriteRuleSet {
602    rules: Vec<RewriteRule>,
603}
604#[allow(dead_code)]
605impl RewriteRuleSet {
606    /// Creates an empty rule set.
607    pub fn new() -> Self {
608        Self { rules: Vec::new() }
609    }
610    /// Adds a rule.
611    pub fn add(&mut self, rule: RewriteRule) {
612        self.rules.push(rule);
613    }
614    /// Returns the number of rules.
615    pub fn len(&self) -> usize {
616        self.rules.len()
617    }
618    /// Returns `true` if the set is empty.
619    pub fn is_empty(&self) -> bool {
620        self.rules.is_empty()
621    }
622    /// Returns all conditional rules.
623    pub fn conditional_rules(&self) -> Vec<&RewriteRule> {
624        self.rules.iter().filter(|r| r.conditional).collect()
625    }
626    /// Returns all unconditional rules.
627    pub fn unconditional_rules(&self) -> Vec<&RewriteRule> {
628        self.rules.iter().filter(|r| !r.conditional).collect()
629    }
630    /// Looks up a rule by name.
631    pub fn get(&self, name: &str) -> Option<&RewriteRule> {
632        self.rules.iter().find(|r| r.name == name)
633    }
634}
635/// A simple directed acyclic graph.
636#[allow(dead_code)]
637pub struct SimpleDag {
638    /// `edges[i]` is the list of direct successors of node `i`.
639    edges: Vec<Vec<usize>>,
640}
641#[allow(dead_code)]
642impl SimpleDag {
643    /// Creates a DAG with `n` nodes and no edges.
644    pub fn new(n: usize) -> Self {
645        Self {
646            edges: vec![Vec::new(); n],
647        }
648    }
649    /// Adds an edge from `from` to `to`.
650    pub fn add_edge(&mut self, from: usize, to: usize) {
651        if from < self.edges.len() {
652            self.edges[from].push(to);
653        }
654    }
655    /// Returns the successors of `node`.
656    pub fn successors(&self, node: usize) -> &[usize] {
657        self.edges.get(node).map(|v| v.as_slice()).unwrap_or(&[])
658    }
659    /// Returns `true` if `from` can reach `to` via DFS.
660    pub fn can_reach(&self, from: usize, to: usize) -> bool {
661        let mut visited = vec![false; self.edges.len()];
662        self.dfs(from, to, &mut visited)
663    }
664    fn dfs(&self, cur: usize, target: usize, visited: &mut Vec<bool>) -> bool {
665        if cur == target {
666            return true;
667        }
668        if cur >= visited.len() || visited[cur] {
669            return false;
670        }
671        visited[cur] = true;
672        for &next in self.successors(cur) {
673            if self.dfs(next, target, visited) {
674                return true;
675            }
676        }
677        false
678    }
679    /// Returns the topological order of nodes, or `None` if a cycle is detected.
680    pub fn topological_sort(&self) -> Option<Vec<usize>> {
681        let n = self.edges.len();
682        let mut in_degree = vec![0usize; n];
683        for succs in &self.edges {
684            for &s in succs {
685                if s < n {
686                    in_degree[s] += 1;
687                }
688            }
689        }
690        let mut queue: std::collections::VecDeque<usize> =
691            (0..n).filter(|&i| in_degree[i] == 0).collect();
692        let mut order = Vec::new();
693        while let Some(node) = queue.pop_front() {
694            order.push(node);
695            for &s in self.successors(node) {
696                if s < n {
697                    in_degree[s] -= 1;
698                    if in_degree[s] == 0 {
699                        queue.push_back(s);
700                    }
701                }
702            }
703        }
704        if order.len() == n {
705            Some(order)
706        } else {
707            None
708        }
709    }
710    /// Returns the number of nodes.
711    pub fn num_nodes(&self) -> usize {
712        self.edges.len()
713    }
714}
715/// A flat list of substitution pairs `(from, to)`.
716#[allow(dead_code)]
717pub struct FlatSubstitution {
718    pairs: Vec<(String, String)>,
719}
720#[allow(dead_code)]
721impl FlatSubstitution {
722    /// Creates an empty substitution.
723    pub fn new() -> Self {
724        Self { pairs: Vec::new() }
725    }
726    /// Adds a pair.
727    pub fn add(&mut self, from: impl Into<String>, to: impl Into<String>) {
728        self.pairs.push((from.into(), to.into()));
729    }
730    /// Applies all substitutions to `s` (leftmost-first order).
731    pub fn apply(&self, s: &str) -> String {
732        let mut result = s.to_string();
733        for (from, to) in &self.pairs {
734            result = result.replace(from.as_str(), to.as_str());
735        }
736        result
737    }
738    /// Returns the number of pairs.
739    pub fn len(&self) -> usize {
740        self.pairs.len()
741    }
742    /// Returns `true` if empty.
743    pub fn is_empty(&self) -> bool {
744        self.pairs.is_empty()
745    }
746}
747/// Represents a rewrite rule `lhs → rhs`.
748#[allow(dead_code)]
749#[allow(missing_docs)]
750pub struct RewriteRule {
751    /// The name of the rule.
752    pub name: String,
753    /// A string representation of the LHS pattern.
754    pub lhs: String,
755    /// A string representation of the RHS.
756    pub rhs: String,
757    /// Whether this is a conditional rule (has side conditions).
758    pub conditional: bool,
759}
760#[allow(dead_code)]
761impl RewriteRule {
762    /// Creates an unconditional rewrite rule.
763    pub fn unconditional(
764        name: impl Into<String>,
765        lhs: impl Into<String>,
766        rhs: impl Into<String>,
767    ) -> Self {
768        Self {
769            name: name.into(),
770            lhs: lhs.into(),
771            rhs: rhs.into(),
772            conditional: false,
773        }
774    }
775    /// Creates a conditional rewrite rule.
776    pub fn conditional(
777        name: impl Into<String>,
778        lhs: impl Into<String>,
779        rhs: impl Into<String>,
780    ) -> Self {
781        Self {
782            name: name.into(),
783            lhs: lhs.into(),
784            rhs: rhs.into(),
785            conditional: true,
786        }
787    }
788    /// Returns a textual representation.
789    pub fn display(&self) -> String {
790        format!("{}: {} → {}", self.name, self.lhs, self.rhs)
791    }
792}
793/// A non-empty list (at least one element guaranteed).
794#[allow(dead_code)]
795pub struct NonEmptyVec<T> {
796    head: T,
797    tail: Vec<T>,
798}
799#[allow(dead_code)]
800impl<T> NonEmptyVec<T> {
801    /// Creates a non-empty vec with a single element.
802    pub fn singleton(val: T) -> Self {
803        Self {
804            head: val,
805            tail: Vec::new(),
806        }
807    }
808    /// Pushes an element.
809    pub fn push(&mut self, val: T) {
810        self.tail.push(val);
811    }
812    /// Returns a reference to the first element.
813    pub fn first(&self) -> &T {
814        &self.head
815    }
816    /// Returns a reference to the last element.
817    pub fn last(&self) -> &T {
818        self.tail.last().unwrap_or(&self.head)
819    }
820    /// Returns the number of elements.
821    pub fn len(&self) -> usize {
822        1 + self.tail.len()
823    }
824    /// Returns whether the collection is empty.
825    pub fn is_empty(&self) -> bool {
826        self.len() == 0
827    }
828    /// Returns all elements as a Vec.
829    pub fn to_vec(&self) -> Vec<&T> {
830        let mut v = vec![&self.head];
831        v.extend(self.tail.iter());
832        v
833    }
834}
835/// A fixed-size sliding window that computes a running sum.
836#[allow(dead_code)]
837pub struct SlidingSum {
838    window: Vec<f64>,
839    capacity: usize,
840    pos: usize,
841    sum: f64,
842    count: usize,
843}
844#[allow(dead_code)]
845impl SlidingSum {
846    /// Creates a sliding sum with the given window size.
847    pub fn new(capacity: usize) -> Self {
848        Self {
849            window: vec![0.0; capacity],
850            capacity,
851            pos: 0,
852            sum: 0.0,
853            count: 0,
854        }
855    }
856    /// Adds a value to the window, removing the oldest if full.
857    pub fn push(&mut self, val: f64) {
858        let oldest = self.window[self.pos];
859        self.sum -= oldest;
860        self.sum += val;
861        self.window[self.pos] = val;
862        self.pos = (self.pos + 1) % self.capacity;
863        if self.count < self.capacity {
864            self.count += 1;
865        }
866    }
867    /// Returns the current window sum.
868    pub fn sum(&self) -> f64 {
869        self.sum
870    }
871    /// Returns the window mean, or `None` if empty.
872    pub fn mean(&self) -> Option<f64> {
873        if self.count == 0 {
874            None
875        } else {
876            Some(self.sum / self.count as f64)
877        }
878    }
879    /// Returns the current window size (number of valid elements).
880    pub fn count(&self) -> usize {
881        self.count
882    }
883}
884/// Definitional equality checker with full optimizations.
885pub struct DefEqChecker<'env> {
886    env: &'env Environment,
887    reducer: Reducer,
888    cache: HashMap<(Expr, Expr), bool>,
889    equiv_manager: EquivManager,
890    /// Verdicts for the pairs too big, as trees, for `cache` and the
891    /// equivalence manager ([`DEF_EQ_CACHE_MAX_NODES`]) — by pair, compared
892    /// with the sharing-aware equality of `term_cache`. A heavily shared
893    /// term is big as a tree and small in memory; without this table every
894    /// path to such a pair compared it again.
895    big_pairs: crate::term_cache::PairCache<bool>,
896    /// The types `Self::quick_infer_type` has computed for terms with no
897    /// loose bound variable (their type does not depend on the binder
898    /// stack). Dropped with the other caches.
899    quick_types: crate::term_cache::TermCache<Expr>,
900    proof_irrelevance: bool,
901    /// Domain types of the binders the checker has descended under
902    /// (outermost first). Entry `i` is expressed in the context of entries
903    /// `0..i`, exactly like a de Bruijn telescope. This lets
904    /// `Self::quick_infer_type` type loose bound variables, which the
905    /// type-*dependent* rules (structure eta, unit-like equality, proof
906    /// irrelevance) need when comparing open `Pi`/`Lam` bodies — Lean's
907    /// kernel gets the same information from its local context.
908    binder_types: Vec<Expr>,
909    /// Types of the free variables in scope: those the surrounding
910    /// `TypeChecker` has opened (its local context) and those this checker
911    /// opened itself ([`Self::is_def_eq_under`]), keyed by `FVarId`. This
912    /// lets `Self::quick_infer_type` type FVars, which one-sided eta
913    /// expansion (`eta_expand_one`) needs when a Pi-bound function is
914    /// compared against a lambda — e.g. Lean core's `funext`, where `f` must
915    /// eta-expand to match `fun x => extfunApp (Quot.mk eqv f) x`. Lean's
916    /// kernel reads the same information from its local context. A variable
917    /// is removed when its binder closes ([`Self::forget_fvar_type`]).
918    fvar_types: crate::local_types::LocalTypes,
919    /// The next id for a local free variable minted by
920    /// [`Self::is_def_eq_under`] when opening a binder. Dispensed from a
921    /// dedicated high range ([`DEF_EQ_OPENED_FVAR_BASE`]) so ids never
922    /// collide with the surrounding `TypeChecker`'s (which are dispensed
923    /// sequentially from 0 — reaching this range would take 2^62 locals).
924    next_opened_fvar: u64,
925}
926/// Base id for binder-opening free variables minted by the def-eq checker.
927pub(crate) const DEF_EQ_OPENED_FVAR_BASE: u64 = 1 << 62;
928/// Maximum AST size (in nodes) of either side of a pair retained in the
929/// def-eq cache / equivalence manager. Oversized pairs are recomputed
930/// instead of retained; verdicts are unchanged.
931const DEF_EQ_CACHE_MAX_NODES: usize = 4096;
932impl<'env> DefEqChecker<'env> {
933    /// Create a new definitional equality checker.
934    pub fn new(env: &'env Environment) -> Self {
935        Self {
936            env,
937            reducer: Reducer::new(),
938            cache: HashMap::new(),
939            equiv_manager: EquivManager::new(),
940            big_pairs: crate::term_cache::PairCache::new(),
941            quick_types: crate::term_cache::TermCache::new(),
942            proof_irrelevance: true,
943            binder_types: Vec::new(),
944            fvar_types: crate::local_types::LocalTypes::new(),
945            next_opened_fvar: DEF_EQ_OPENED_FVAR_BASE,
946        }
947    }
948    /// Record the type of a free variable opened by the surrounding
949    /// type checker, so `Self::quick_infer_type` can type it.
950    ///
951    /// `FVarId`s are dispensed monotonically by the `TypeChecker` and never
952    /// reused, so recorded types are stable while their variable is in scope
953    /// and cached def-eq verdicts stay sound. Defensively, if a caller *does*
954    /// re-bind an id in a way that could make them unsound — an id in scope
955    /// given a different type, or an id whose binder was closed
956    /// ([`Self::forget_fvar_type`]) bound again; only possible through the
957    /// public `TypeChecker::push_local` — the caches are invalidated.
958    pub fn record_fvar_type(&mut self, id: crate::FVarId, ty: Expr) {
959        // Mirror into the reducer so K-like iota reduction can type recursor
960        // majors mentioning this free variable during WHNF.
961        self.reducer.record_fvar_type(id, ty.clone());
962        if self.fvar_types.bind(id, ty) {
963            self.cache.clear();
964            self.equiv_manager.clear();
965            self.big_pairs.clear();
966            self.quick_types.clear();
967        }
968    }
969    /// The binder of a free variable recorded with
970    /// [`Self::record_fvar_type`] has been closed: forget its type, here and
971    /// in the reducer's mirror.
972    ///
973    /// Nothing compared from here on mentions the variable, so nothing needs
974    /// its type; cached verdicts that mention it are never looked up again,
975    /// since its id is not dispensed again (and if a caller does bind it
976    /// again, [`Self::record_fvar_type`] invalidates the caches). Without
977    /// this both mirrors grow by one type per binder opened, for as long as
978    /// the checker lives, and every K-like or structure-eta attempt copies
979    /// all of them into a nested checker.
980    pub fn forget_fvar_type(&mut self, id: crate::FVarId) {
981        self.reducer.forget_fvar_type(id);
982        self.fvar_types.unbind(id);
983    }
984    /// This checker's mirror of the local context and its reducer's, and the
985    /// id the next binder it opens will get (for tests).
986    #[cfg(test)]
987    pub(crate) fn local_types(
988        &self,
989    ) -> (
990        &crate::local_types::LocalTypes,
991        &crate::local_types::LocalTypes,
992        u64,
993    ) {
994        (
995            &self.fvar_types,
996            self.reducer.local_types(),
997            self.next_opened_fvar,
998        )
999    }
1000    /// Keep every binder-opening free variable this checker mints from now
1001    /// on distinct from `id` (and from every smaller id of the opened range).
1002    ///
1003    /// A checker seeded with the locals of a SURROUNDING checker (see
1004    /// `TypeChecker::with_fvar_types`) compares terms that mention those
1005    /// locals, some of which the surrounding def-eq checker minted from this
1006    /// same range. Opening a binder with an id one of them already carries
1007    /// identifies the bound variable with that local: `fun y => g y c` and
1008    /// `fun y => g c y` would both open to `g c c` and compare equal — a
1009    /// wrong ACCEPT. The seeding code calls this with the largest seeded id
1010    /// of the opened range.
1011    pub(crate) fn reserve_opened_fvars_through(&mut self, id: u64) {
1012        if id >= self.next_opened_fvar {
1013            self.next_opened_fvar = id.saturating_add(1);
1014        }
1015    }
1016    /// Disable proof irrelevance for this checker.
1017    pub fn set_proof_irrelevance(&mut self, enabled: bool) {
1018        self.proof_irrelevance = enabled;
1019    }
1020    /// Set the transparency mode for this checker.
1021    pub fn set_transparency(&mut self, mode: TransparencyMode) {
1022        self.reducer.set_transparency(mode);
1023        self.cache.clear();
1024        self.equiv_manager.clear();
1025        self.big_pairs.clear();
1026        self.quick_types.clear();
1027    }
1028    /// Check if two expressions are definitionally equal.
1029    pub fn is_def_eq(&mut self, t: &Expr, s: &Expr) -> bool {
1030        // Syntactic equality, visiting a shared subterm once per pair (what
1031        // `==` decides; see `term_cache::same_term`).
1032        if crate::term_cache::same_term(t, s) {
1033            return true;
1034        }
1035        // Resource-fuel degradation (C16): with the budget exhausted only
1036        // syntactic equality (above) is decided — `false` here is always
1037        // conservative (never a wrong accept), and it breaks the
1038        // unfold/whnf/compare loops that would otherwise keep growing
1039        // terms. The caller reports the failure as a named resource limit.
1040        if crate::fuel::is_exhausted() {
1041            return false;
1042        }
1043        // Context-dependence guard: for OPEN terms under binders the verdict
1044        // can depend on the binder-type stack (structure eta / proof
1045        // irrelevance type loose BVars via `quick_infer_type`), so only pairs
1046        // that are closed — or compared at the top level, where loose BVars
1047        // cannot be typed and the rules degrade to structural ones — may
1048        // participate in the cache and the equivalence manager.
1049        //
1050        // Size: a pair of terms that are small as trees goes through the
1051        // equivalence manager and the pair cache, whose keys are compared
1052        // with `==`. A pair with a term that is big as a tree — which a
1053        // heavily shared term is, however little memory it takes — goes
1054        // through `big_pairs`, whose keys are compared once per shared
1055        // subterm. Neither changes a verdict.
1056        let context_free =
1057            self.binder_types.is_empty() || (!has_loose_bvars(t) && !has_loose_bvars(s));
1058        let small = context_free
1059            && crate::expr_util::expr_size_within(t, DEF_EQ_CACHE_MAX_NODES)
1060            && crate::expr_util::expr_size_within(s, DEF_EQ_CACHE_MAX_NODES);
1061        if small {
1062            if self.equiv_manager.is_equiv(t, s) {
1063                return true;
1064            }
1065            if self.equiv_manager.is_failure(t, s) {
1066                return false;
1067            }
1068            let key = (t.clone(), s.clone());
1069            if let Some(&result) = self.cache.get(&key) {
1070                return result;
1071            }
1072        } else if context_free {
1073            if let Some(&result) = self.big_pairs.get(t, s) {
1074                return result;
1075            }
1076        }
1077        let result = self.is_def_eq_core(t, s);
1078        if small {
1079            self.cache.insert((t.clone(), s.clone()), result);
1080            if result {
1081                self.equiv_manager.add_equiv(t, s);
1082            } else {
1083                self.equiv_manager.add_failure(t, s);
1084            }
1085        } else if context_free {
1086            self.big_pairs.insert(t, s, result);
1087        }
1088        result
1089    }
1090    /// Compare two binder bodies under a binder of domain type `dom` —
1091    /// Lean's `isDefEqBinding`: the binder is *opened* with a fresh, typed
1092    /// local free variable and the instantiated bodies are compared.
1093    ///
1094    /// Opening (rather than comparing the raw bodies with loose BVars under
1095    /// a binder-type stack) keeps every subterm locally closed, so every
1096    /// reduction step downstream can type what it needs *wherever the redex
1097    /// sits*. The decisive case is K-like iota during WHNF: in Lean core's
1098    /// `Std.IterStep.noConfusion` (new `ctorIdx`-based scheme, Lean ≥ 4.32)
1099    /// the term `(Eq.rec … (Eq.symm … : 0 = ctorIdx t) …).PULift.0 it out`
1100    /// is stuck unless the `Eq.rec` major — which mentions the Pi-bound
1101    /// `it`/`out` — can be typed; with loose BVars it cannot be (at any
1102    /// depth), and the declaration was wrongly rejected.
1103    ///
1104    /// The minted ids come from a dedicated high range and are never reused,
1105    /// so recorded types are stable and the def-eq/whnf caches stay sound.
1106    ///
1107    /// The variable is in scope for exactly this comparison: the opened
1108    /// bodies are the only terms that mention it, and they are dropped when
1109    /// it returns. Its type is forgotten then, so the mirrors hold the
1110    /// binders that are open, not every binder ever opened.
1111    fn is_def_eq_under(&mut self, dom: &Expr, b1: &Expr, b2: &Expr) -> bool {
1112        let id = crate::FVarId(self.next_opened_fvar);
1113        self.next_opened_fvar += 1;
1114        self.record_fvar_type(id, dom.clone());
1115        let fv = Expr::FVar(id);
1116        let b1_open = instantiate(b1, &fv);
1117        let b2_open = instantiate(b2, &fv);
1118        let equal = self.is_def_eq(&b1_open, &b2_open);
1119        self.forget_fvar_type(id);
1120        equal
1121    }
1122    fn is_def_eq_core(&mut self, t: &Expr, s: &Expr) -> bool {
1123        // Congruence before unfolding (the `f a =?= f b` step of Lean's lazy
1124        // delta reduction): two applications of the SAME definition to
1125        // pairwise def-eq arguments are def-eq, whatever the definition
1126        // unfolds to. (In the `congruence` module of this directory.)
1127        if self.same_definition_applied_to_equal_args(t, s) {
1128            return true;
1129        }
1130        let t_whnf = self.reducer.whnf_env(t, self.env);
1131        let s_whnf = self.reducer.whnf_env(s, self.env);
1132        if crate::term_cache::same_term(&t_whnf, &s_whnf) {
1133            return true;
1134        }
1135        if self.is_proof_irrelevant_eq(&t_whnf, &s_whnf) {
1136            return true;
1137        }
1138        let matched = match (&t_whnf, &s_whnf) {
1139            (Expr::Sort(l1), Expr::Sort(l2)) => level::is_equivalent(l1, l2),
1140            (Expr::BVar(i1), Expr::BVar(i2)) => i1 == i2,
1141            (Expr::FVar(id1), Expr::FVar(id2)) => id1 == id2,
1142            (Expr::Const(n1, ls1), Expr::Const(n2, ls2)) => {
1143                n1 == n2
1144                    && ls1.len() == ls2.len()
1145                    && ls1
1146                        .iter()
1147                        .zip(ls2.iter())
1148                        .all(|(l1, l2)| level::is_equivalent(l1, l2))
1149            }
1150            (Expr::App(f1, a1), Expr::App(f2, a2)) => {
1151                if self.is_def_eq_app(&t_whnf, &s_whnf) {
1152                    return true;
1153                }
1154                self.is_def_eq(f1, f2) && self.is_def_eq(a1, a2)
1155            }
1156            (Expr::Lam(_, _, ty1, b1), Expr::Lam(_, _, ty2, b2)) => {
1157                self.is_def_eq(ty1, ty2) && self.is_def_eq_under(ty1, b1, b2)
1158            }
1159            (Expr::Pi(_, _, ty1, b1), Expr::Pi(_, _, ty2, b2)) => {
1160                self.is_def_eq(ty1, ty2) && self.is_def_eq_under(ty1, b1, b2)
1161            }
1162            (Expr::Let(_, ty1, v1, b1), Expr::Let(_, ty2, v2, b2)) => {
1163                self.is_def_eq(ty1, ty2)
1164                    && self.is_def_eq(v1, v2)
1165                    && self.is_def_eq_under(ty1, b1, b2)
1166            }
1167            (Expr::Lit(l1), Expr::Lit(l2)) => l1 == l2,
1168            (Expr::Proj(n1, i1, e1), Expr::Proj(n2, i2, e2)) => {
1169                n1 == n2 && i1 == i2 && self.is_def_eq(e1, e2)
1170            }
1171            (Expr::Lam(_, _, _, _), _) => self.try_eta_lhs(&t_whnf, &s_whnf),
1172            (_, Expr::Lam(_, _, _, _)) => self.try_eta_rhs(&t_whnf, &s_whnf),
1173            // Literal ↔ constructor-form bridge (Lean's `natLitExt` /
1174            // `strLitExt`): a `Nat`/`String` literal is defeq to its
1175            // constructor-form denotation (`Nat.zero`/`Nat.succ …`,
1176            // `String.mk (List Char)`). Only one side is a literal here (both
1177            // were handled above). If the bridge applies it decides; otherwise
1178            // fall through to lazy delta so a constant that *unfolds* to a
1179            // constructor form is still reached.
1180            _ => match self.try_lit_ext(&t_whnf, &s_whnf) {
1181                Some(b) => b,
1182                None => self.try_lazy_delta(&t_whnf, &s_whnf),
1183            },
1184        };
1185        if matched {
1186            return true;
1187        }
1188        // K-like reduction that needs the *local* binder context to type the
1189        // major premise: the Reducer's own K path (`to_ctor_when_k`) uses a
1190        // fresh `TypeChecker` and cannot type loose bound variables, so e.g.
1191        // `Eq.rec ... h` with `h` a Pi-bound hypothesis stays stuck there.
1192        // Here the binder-type stack can type it (Lean's kernel has the same
1193        // information via its local context).
1194        if let Some(t_red) = self.try_k_whnf(&t_whnf) {
1195            return self.is_def_eq(&t_red, &s_whnf);
1196        }
1197        if let Some(s_red) = self.try_k_whnf(&s_whnf) {
1198            return self.is_def_eq(&t_whnf, &s_red);
1199        }
1200        // Lean kernel ordering (`isDefEqCore`): once the structural and
1201        // lazy-delta paths have failed, try definitional eta for structures
1202        // (`isDefEqEtaStruct`) in both orientations, then unit-like equality
1203        // (`isDefEqUnitLike`). Proof irrelevance was already tried above,
1204        // mirroring Lean's order, so Prop-valued structures are handled
1205        // there first.
1206        if self.try_eta_struct(&t_whnf, &s_whnf) || self.try_eta_struct(&s_whnf, &t_whnf) {
1207            return true;
1208        }
1209        self.is_def_eq_unit_like(&t_whnf, &s_whnf)
1210    }
1211    /// K-like iota reduction driven by the *def-eq-local* typing context
1212    /// (Lean `to_cnstr_when_K` executed with the kernel's local context).
1213    ///
1214    /// If `e` is a stuck application of a recursor with the K flag, type its
1215    /// major premise via `Self::quick_infer_type` (which can see the binder
1216    /// stack, unlike the Reducer's fresh `TypeChecker`), verify the major's
1217    /// type is the K-inductive applied to its parameters/indices, and replace
1218    /// the major with the canonical constructor application so iota can fire.
1219    /// Returns the WHNF of the rewritten application, or `None` when anything
1220    /// does not line up or no progress is made (never a wrong reduction).
1221    fn try_k_whnf(&mut self, e: &Expr) -> Option<Expr> {
1222        let (head, args) = get_app_fn_args(e);
1223        let Expr::Const(rec_name, rec_levels) = head else {
1224            return None;
1225        };
1226        let rec_val = self.env.get_recursor_val(rec_name)?.clone();
1227        if !rec_val.k {
1228            return None;
1229        }
1230        let major_idx = rec_val.get_major_idx() as usize;
1231        if args.len() <= major_idx {
1232            return None;
1233        }
1234        let major = args[major_idx].clone();
1235        let major_ty = self.quick_infer_type(&major)?;
1236        let major_ty = self.reducer.whnf_env(&major_ty, self.env);
1237        let (ty_head, ty_args) = get_app_fn_args(&major_ty);
1238        let Expr::Const(ind_name, ind_levels) = ty_head else {
1239            return None;
1240        };
1241        if ind_name != rec_val.all.first()? {
1242            return None;
1243        }
1244        let iv = self.env.get_inductive_val(ind_name)?.clone();
1245        if ty_args.len() != (iv.num_params + iv.num_indices) as usize {
1246            return None;
1247        }
1248        let ctor_name = iv.ctors.first()?.clone();
1249        let params: Vec<Expr> = ty_args[..iv.num_params as usize]
1250            .iter()
1251            .map(|e| (*e).clone())
1252            .collect();
1253        let new_ctor = mk_app(Expr::Const(ctor_name, ind_levels.clone()), &params);
1254        if crate::term_cache::same_term(&new_ctor, &major) {
1255            // Already canonical: the Reducer was stuck for a different
1256            // reason; nothing to gain (and rewriting would not terminate).
1257            return None;
1258        }
1259        let new_ty = self.quick_infer_type(&new_ctor)?;
1260        if !self.is_def_eq(&major_ty, &new_ty) {
1261            return None;
1262        }
1263        let mut new_args: Vec<Expr> = args.iter().map(|a| (*a).clone()).collect();
1264        new_args[major_idx] = new_ctor;
1265        let rebuilt = mk_app(Expr::Const(rec_name.clone(), rec_levels.clone()), &new_args);
1266        let reduced = self.reducer.whnf_env(&rebuilt, self.env);
1267        if crate::term_cache::same_term(&reduced, e) {
1268            return None;
1269        }
1270        Some(reduced)
1271    }
1272    /// Try lazy delta reduction: unfold one side at a time.
1273    ///
1274    /// When comparing `f a1 ... an` with `g b1 ... bm` where f and g
1275    /// are both definitions, unfold the one with lower height first.
1276    fn try_lazy_delta(&mut self, t: &Expr, s: &Expr) -> bool {
1277        let t_head = get_app_fn(t);
1278        let s_head = get_app_fn(s);
1279        let t_hint = self.get_hint(t_head);
1280        let s_hint = self.get_hint(s_head);
1281        match (t_hint, s_hint) {
1282            (Some(th), Some(sh)) => {
1283                if th.height() <= sh.height() {
1284                    if let Some(t_unfolded) = self.unfold_definition(t) {
1285                        let t_whnf = self.reducer.whnf_env(&t_unfolded, self.env);
1286                        return self.is_def_eq(&t_whnf, s);
1287                    }
1288                }
1289                if let Some(s_unfolded) = self.unfold_definition(s) {
1290                    let s_whnf = self.reducer.whnf_env(&s_unfolded, self.env);
1291                    return self.is_def_eq(t, &s_whnf);
1292                }
1293                false
1294            }
1295            (Some(_), None) => {
1296                if let Some(t_unfolded) = self.unfold_definition(t) {
1297                    let t_whnf = self.reducer.whnf_env(&t_unfolded, self.env);
1298                    return self.is_def_eq(&t_whnf, s);
1299                }
1300                false
1301            }
1302            (None, Some(_)) => {
1303                if let Some(s_unfolded) = self.unfold_definition(s) {
1304                    let s_whnf = self.reducer.whnf_env(&s_unfolded, self.env);
1305                    return self.is_def_eq(t, &s_whnf);
1306                }
1307                false
1308            }
1309            (None, None) => false,
1310        }
1311    }
1312    /// Literal ↔ constructor-form definitional-equality bridge — Lean's
1313    /// `natLitExt?` / `strLitExt?`.
1314    ///
1315    /// Exactly one of `t` / `s` is a `Lit` (the both-literal case is decided
1316    /// structurally before this is reached). A `Nat`/`String` literal is
1317    /// definitionally equal to its constructor-form denotation:
1318    ///
1319    /// * `NatLit 0`  ≡ `Nat.zero`;
1320    /// * `NatLit (n+1)` ≡ `Nat.succ e`  when `NatLit n ≡ e`;
1321    /// * `StrLit s` ≡ `String.mk l`  when `l` is the `List Char` of `s`.
1322    ///
1323    /// Returns `Some(true)` / `Some(false)` when the bridge conclusively
1324    /// decides, and `None` when it does not apply (the other side is neither
1325    /// `Nat.zero`/`Nat.succ`/`String.mk`-headed nor a literal) — the caller
1326    /// then falls through to lazy delta, so a *constant* that only unfolds to a
1327    /// constructor form is still reached.
1328    ///
1329    /// Termination / no blow-up: the literal is **never** materialised into a
1330    /// succ-tower. Instead the *constructor-form* side is peeled one layer at a
1331    /// time and the literal is decremented (`pred`) each step, so the number of
1332    /// steps is bounded by `min(literal, actual constructor depth)`. A large
1333    /// literal against a shallow succ-tower (e.g. `10^9` vs `succ (FVar)`)
1334    /// therefore stops after one layer with a fast mismatch, never eagerly
1335    /// expanding the literal.
1336    fn try_lit_ext(&mut self, t: &Expr, s: &Expr) -> Option<bool> {
1337        match (t, s) {
1338            (Expr::Lit(crate::Literal::Nat(n)), other)
1339            | (other, Expr::Lit(crate::Literal::Nat(n))) => self.nat_lit_eq_term(n, other),
1340            (Expr::Lit(crate::Literal::Str(str_lit)), other)
1341            | (other, Expr::Lit(crate::Literal::Str(str_lit))) => {
1342                self.str_lit_eq_term(str_lit, other)
1343            }
1344            _ => None,
1345        }
1346    }
1347    /// Decide `NatLit n ≡ term` via Lean's `natLitExt?`. `term` is already in
1348    /// WHNF and is *not* a `Nat` literal (that case is decided structurally).
1349    fn nat_lit_eq_term(&mut self, n: &crate::bignat::BigNat, term: &Expr) -> Option<bool> {
1350        let head = get_app_fn(term);
1351        let head_name = match head {
1352            Expr::Const(name, _) => name.to_string(),
1353            // Non-constructor head (FVar, Sort, …): the bridge cannot decide;
1354            // defer to lazy delta (which ultimately fails / stays stuck) so a
1355            // mismatch is never reported as equal.
1356            _ => return None,
1357        };
1358        if n.is_zero() {
1359            // `NatLit 0`: equal iff the term is `Nat.zero`; a `Nat.succ …`
1360            // head is a definite mismatch.
1361            if head_name == "Nat.zero" {
1362                Some(true)
1363            } else if head_name == "Nat.succ" {
1364                Some(false)
1365            } else {
1366                None
1367            }
1368        } else {
1369            // `NatLit (n>0)`: equal iff the term is `Nat.succ e` with
1370            // `NatLit (n-1) ≡ e`; a `Nat.zero` head is a definite mismatch.
1371            match head_name.as_str() {
1372                "Nat.succ" => {
1373                    let args = get_app_args(term);
1374                    // `Nat.succ` must be applied to exactly one argument.
1375                    if args.len() != 1 {
1376                        return None;
1377                    }
1378                    let pred_lit = Expr::Lit(crate::Literal::Nat(n.pred()));
1379                    // Recurse through `is_def_eq`: the argument is re-WHNF'd
1380                    // (idempotent, cheap) and the both-literal fast path or a
1381                    // further succ-peel applies. Each step strictly decrements
1382                    // the literal, bounding the recursion by the succ depth.
1383                    Some(self.is_def_eq(&pred_lit, args[0]))
1384                }
1385                "Nat.zero" => Some(false),
1386                _ => None,
1387            }
1388        }
1389    }
1390    /// Decide `StrLit s ≡ term` via Lean's `try_string_lit_expansion`
1391    /// (type_checker.cpp, v4.32). `term` is already in WHNF-core form and is
1392    /// *not* a `String` literal (that case is decided structurally).
1393    ///
1394    /// * **v4.32 model** (`String`'s constructor is `ofByteArray`): the
1395    ///   extension fires exactly when `term` is an application headed by the
1396    ///   `String.ofList` *function* — Lean checks `app_fn(s) == String.ofList`
1397    ///   syntactically, before lazy delta unfolds it. The literal expands to
1398    ///   `String.ofList <chars>` (`string_lit_to_constructor`), is WHNF'd,
1399    ///   and the two sides go through full definitional equality.
1400    /// * **Old model** (a one-field `String.mk : List Char → String`, e.g.
1401    ///   the builtin env): the constructor-form list is peeled one
1402    ///   `List.cons` layer at a time against `s.chars()` — bounded by the
1403    ///   string length, never eagerly materialising the whole list on a
1404    ///   length mismatch. Gated on `String.mk` really being the sole
1405    ///   one-field constructor of `String` in this environment, so an
1406    ///   unrelated definition of that name can never bridge.
1407    fn str_lit_eq_term(&mut self, s: &str, term: &Expr) -> Option<bool> {
1408        let (head, args) = crate::expr_util::get_app_fn_args(term);
1409        let head_name = match head {
1410            Expr::Const(name, _) => name.to_string(),
1411            _ => return None,
1412        };
1413        if head_name == "String.ofList" && args.len() == 1 {
1414            let expansion = crate::reduce::iota::str_lit_expansion(s, self.env)?;
1415            let expansion_whnf = self.reducer.whnf_env(&expansion, self.env);
1416            // Re-entry guard: with the real `String.ofList` the WHNF is a
1417            // constructor application (or a stuck `ofList` application, when
1418            // `ofList` is opaque in a synthetic env), never a literal. If an
1419            // adversarial environment defines `ofList` so the expansion
1420            // WHNF-folds back to a string literal, deciding here would
1421            // recurse into this same bridge forever — decline instead and
1422            // let lazy delta take over (safe incompleteness).
1423            if matches!(expansion_whnf, Expr::Lit(crate::Literal::Str(_))) {
1424                return None;
1425            }
1426            return Some(self.is_def_eq(&expansion_whnf, term));
1427        }
1428        if head_name == "String.mk" && args.len() == 1 {
1429            let cv = self.env.get_constructor_val(match head {
1430                Expr::Const(name, _) => name,
1431                _ => return None,
1432            })?;
1433            if cv.induct.to_string() != "String" || cv.num_fields != 1 || cv.num_params != 0 {
1434                return None;
1435            }
1436            return Some(self.char_list_eq_str(&s.chars().collect::<Vec<char>>(), args[0]));
1437        }
1438        None
1439    }
1440    /// Decide whether the `List Char` expression `list` denotes exactly the
1441    /// characters `chars` (in order). Peels one `List.cons` per character; a
1442    /// `List.nil` head must coincide with the end of `chars`. Bounded by
1443    /// `chars.len()`.
1444    ///
1445    /// Constructor names are matched by their full dotted string
1446    /// (`List.nil` / `List.cons` / `Char.ofNat`), which is identical for both
1447    /// the flat and hierarchical naming styles a `String` literal can expand to
1448    /// (see `reduce::iota::str_lit_to_ctor`), so the bridge is naming-style
1449    /// agnostic without risking a collision with an unrelated `Foo.cons`.
1450    fn char_list_eq_str(&mut self, chars: &[char], list: &Expr) -> bool {
1451        let list_whnf = self.reducer.whnf_env(list, self.env);
1452        let (head, args) = crate::expr_util::get_app_fn_args(&list_whnf);
1453        let head_name = match head {
1454            Expr::Const(name, _) => name.to_string(),
1455            _ => return false,
1456        };
1457        match head_name.as_str() {
1458            // `List.nil Char`: matches iff there are no characters left.
1459            "List.nil" => chars.is_empty(),
1460            // `List.cons Char c rest`: `Char` type arg, head char, tail list.
1461            "List.cons" => {
1462                if chars.is_empty() || args.len() != 3 {
1463                    return false;
1464                }
1465                self.char_eq(chars[0], args[1]) && self.char_list_eq_str(&chars[1..], args[2])
1466            }
1467            _ => false,
1468        }
1469    }
1470    /// Decide whether the WHNF'd expression `ch` denotes the character `c`,
1471    /// i.e. `ch` is `Char.ofNat <code>` with `<code>` the Unicode scalar value
1472    /// of `c`. The code is compared as a `Nat` literal via `is_def_eq`, so any
1473    /// def-eq encoding of the same scalar value is accepted.
1474    fn char_eq(&mut self, c: char, ch: &Expr) -> bool {
1475        let ch_whnf = self.reducer.whnf_env(ch, self.env);
1476        let (head, args) = crate::expr_util::get_app_fn_args(&ch_whnf);
1477        match head {
1478            Expr::Const(name, _) if name.to_string() == "Char.ofNat" && args.len() == 1 => {
1479                let code = Expr::Lit(crate::Literal::Nat(crate::bignat::BigNat::from(c as u32)));
1480                self.is_def_eq(&code, args[0])
1481            }
1482            _ => false,
1483        }
1484    }
1485    /// Get the reducibility hint for an expression head.
1486    pub(super) fn get_hint(&self, head: &Expr) -> Option<ReducibilityHint> {
1487        if let Expr::Const(name, _) = head {
1488            if let Some(ci) = self.env.find(name) {
1489                let hint = ci.reducibility_hint();
1490                if hint.should_unfold() {
1491                    return Some(hint);
1492                }
1493            }
1494        }
1495        None
1496    }
1497    /// Try to unfold a definition at the head of an expression.
1498    fn unfold_definition(&self, expr: &Expr) -> Option<Expr> {
1499        let head = get_app_fn(expr);
1500        if let Expr::Const(name, levels) = head {
1501            if let Some(ci) = self.env.find(name) {
1502                if let Some(val) = ci.value() {
1503                    let val_inst = if ci.level_params().is_empty() || levels.is_empty() {
1504                        val.clone()
1505                    } else {
1506                        crate::instantiate::instantiate_type_lparams(val, ci.level_params(), levels)
1507                    };
1508                    let args: Vec<Expr> = get_app_args(expr).into_iter().cloned().collect();
1509                    return Some(crate::expr_util::mk_app(val_inst, &args));
1510                }
1511            }
1512        }
1513        None
1514    }
1515    /// Try structural application equality.
1516    ///
1517    /// Compare `f a1 ... an` with `g b1 ... bn` by comparing
1518    /// the heads and all arguments.
1519    fn is_def_eq_app(&mut self, t: &Expr, s: &Expr) -> bool {
1520        let t_head = get_app_fn(t);
1521        let s_head = get_app_fn(s);
1522        let t_args = get_app_args(t);
1523        let s_args = get_app_args(s);
1524        if t_args.len() != s_args.len() {
1525            return false;
1526        }
1527        if !self.is_def_eq(t_head, s_head) {
1528            return false;
1529        }
1530        t_args
1531            .iter()
1532            .zip(s_args.iter())
1533            .all(|(a, b)| self.is_def_eq(a, b))
1534    }
1535    /// Quickly infer the type of a term without full elaboration.
1536    ///
1537    /// Handles the common structural cases needed for proof irrelevance:
1538    /// constants, applications of Pi-typed functions, Sort, and literals.
1539    /// Returns `None` for cases that require a full type checker (e.g. BVar, FVar).
1540    fn quick_infer_type(&mut self, expr: &Expr) -> Option<Expr> {
1541        // A term with no loose bound variable has the same type whatever
1542        // binders the checker is under, so its type can be kept. (Only a
1543        // type that WAS found is kept: a `None` may be a reduction cut short
1544        // at a limit rather than a property of the term.)
1545        let keep = expr.range() == 0
1546            && !matches!(
1547                expr,
1548                Expr::Sort(_) | Expr::Lit(_) | Expr::FVar(_) | Expr::BVar(_)
1549            );
1550        if keep {
1551            if let Some(ty) = self.quick_types.get(expr) {
1552                return Some(ty.clone());
1553            }
1554        }
1555        let ty = self.quick_infer_type_uncached(expr)?;
1556        if keep {
1557            self.quick_types.insert(expr, ty.clone());
1558        }
1559        Some(ty)
1560    }
1561    /// [`Self::quick_infer_type`] for a term whose type is not in the cache.
1562    fn quick_infer_type_uncached(&mut self, expr: &Expr) -> Option<Expr> {
1563        match expr {
1564            Expr::Sort(l) => Some(Expr::Sort(crate::level::Level::succ(l.clone()))),
1565            Expr::Lit(crate::Literal::Nat(_)) => Some(Expr::Const(crate::Name::str("Nat"), vec![])),
1566            Expr::Lit(crate::Literal::Str(_)) => {
1567                Some(Expr::Const(crate::Name::str("String"), vec![]))
1568            }
1569            Expr::Const(name, levels) => {
1570                let ci = self.env.find(name)?;
1571                let raw_ty = ci.ty().clone();
1572                let params = ci.level_params().to_vec();
1573                if params.is_empty() || levels.is_empty() {
1574                    Some(raw_ty)
1575                } else {
1576                    Some(crate::instantiate::instantiate_type_lparams(
1577                        &raw_ty, &params, levels,
1578                    ))
1579                }
1580            }
1581            Expr::App(f, a) => {
1582                let f_ty = self.quick_infer_type(f)?;
1583                let f_ty_whnf = self.reducer.whnf_env(&f_ty, self.env);
1584                if let Expr::Pi(_, _, _, body) = f_ty_whnf {
1585                    Some(crate::subst::instantiate(&body, a))
1586                } else {
1587                    None
1588                }
1589            }
1590            // A loose bound variable is typed from the binder-type stack the
1591            // checker maintains while descending under binders. Entry types
1592            // are expressed *outside* their binder, so the result is lifted
1593            // past the binders in between (idx + 1 of them).
1594            Expr::BVar(idx) => {
1595                let i = *idx as usize;
1596                let len = self.binder_types.len();
1597                if i < len {
1598                    let ty = self.binder_types[len - 1 - i].clone();
1599                    Some(crate::expr_util::lift_loose_bvars(&ty, *idx + 1, 0))
1600                } else {
1601                    None
1602                }
1603            }
1604            // A free variable is typed from the surrounding type checker's
1605            // local context (recorded via [`Self::record_fvar_type`]). FVar
1606            // types are closed with respect to the binder stack, so no
1607            // lifting is needed.
1608            Expr::FVar(id) => self.fvar_types.get(*id).cloned(),
1609            Expr::Lam(bi, name, dom, body) => {
1610                self.binder_types.push((**dom).clone());
1611                let body_ty = self.quick_infer_type(body);
1612                self.binder_types.pop();
1613                Some(Expr::Pi(
1614                    *bi,
1615                    name.clone(),
1616                    dom.clone(),
1617                    Node::new(body_ty?),
1618                ))
1619            }
1620            Expr::Pi(_, _, dom, cod) => {
1621                let dom_ty = self.quick_infer_type(dom)?;
1622                self.binder_types.push((**dom).clone());
1623                let cod_ty = self.quick_infer_type(cod);
1624                self.binder_types.pop();
1625                let cod_ty = cod_ty?;
1626                let dom_whnf = self.reducer.whnf_env(&dom_ty, self.env);
1627                let cod_whnf = self.reducer.whnf_env(&cod_ty, self.env);
1628                match (dom_whnf, cod_whnf) {
1629                    // The simplifying constructor, as in
1630                    // `TypeChecker::infer_type`: the same universe, in the
1631                    // shape Lean's kernel builds. A raw `imax` here nested
1632                    // once per binder of a telescope, and deciding whether
1633                    // such a level is `0` — which proof irrelevance asks of
1634                    // every pair compared — distributes over every nesting:
1635                    // exponential in the length of the telescope.
1636                    (Expr::Sort(l1), Expr::Sort(l2)) => {
1637                        Some(Expr::Sort(crate::level::mk_imax(l1, l2)))
1638                    }
1639                    _ => None,
1640                }
1641            }
1642            Expr::Let(_, _ty, val, body) => {
1643                let body_subst = crate::subst::instantiate(body, val);
1644                self.quick_infer_type(&body_subst)
1645            }
1646            Expr::Proj(struct_name, idx, inner) => {
1647                let inner_ty = self.quick_infer_type(inner)?;
1648                self.quick_proj_type(struct_name, *idx, inner, &inner_ty)
1649            }
1650        }
1651    }
1652    /// Infer the type of `Proj(struct_name, idx, inner)` given `inner`'s type.
1653    ///
1654    /// Mirrors `TypeChecker::infer_proj_field_type`: instantiates the
1655    /// constructor's type with the universe levels and inductive parameters
1656    /// read off `inner`'s (whnf'd) type, substitutes earlier fields with
1657    /// projections of `inner`, and returns the `idx`-th Pi domain. Which
1658    /// inductive type may be named is the `TypeChecker`'s rule
1659    /// (`infer::proj_target`: one constructor; recursion and indices are
1660    /// admitted). Returns `None` whenever anything does not line up — this
1661    /// is a best-effort helper; the full `TypeChecker` reports typed errors
1662    /// instead.
1663    fn quick_proj_type(
1664        &mut self,
1665        struct_name: &crate::Name,
1666        idx: u32,
1667        inner: &Expr,
1668        inner_ty: &Expr,
1669    ) -> Option<Expr> {
1670        let (ind_val, ctor_val) = proj_target(self.env, struct_name, idx).ok()?;
1671        let inner_ty_whnf = self.reducer.whnf_env(inner_ty, self.env);
1672        let levels: Vec<Level> = match get_app_fn(&inner_ty_whnf) {
1673            Expr::Const(name, lvls) if name == struct_name => lvls.clone(),
1674            _ => return None,
1675        };
1676        let ty_args: Vec<Expr> = get_app_args(&inner_ty_whnf).into_iter().cloned().collect();
1677        let params = proj_type_params(ind_val, &ty_args)?;
1678        let level_params = &ind_val.common.level_params;
1679        let mut cur_ty = if level_params.is_empty() || levels.is_empty() {
1680            ctor_val.common.ty.clone()
1681        } else {
1682            instantiate_type_lparams(&ctor_val.common.ty, level_params, &levels)
1683        };
1684        for param in params {
1685            match cur_ty {
1686                Expr::Pi(_, _, _, body) => cur_ty = instantiate(&body, param),
1687                _ => return None,
1688            }
1689        }
1690        // No data out of a proof, as in the `TypeChecker` (see
1691        // `infer::proj_prop`): a projection it refuses gets no type here.
1692        // The sort is that of `S ps is`: read past the indices too.
1693        let sort = structure_type_sort(ind_val, &levels, &ty_args, |e| {
1694            self.reducer.whnf_env(e, self.env)
1695        });
1696        let struct_is_prop = is_prop_sort(&sort)?;
1697        for j in 0..idx {
1698            match cur_ty {
1699                Expr::Pi(_, _, dom, body) => {
1700                    if struct_is_prop && has_loose_bvars(&body) && !self.quick_is_prop(&dom) {
1701                        return None;
1702                    }
1703                    let field_val = Expr::Proj(struct_name.clone(), j, Node::new(inner.clone()));
1704                    cur_ty = instantiate(&body, &field_val);
1705                }
1706                _ => return None,
1707            }
1708        }
1709        match cur_ty {
1710            Expr::Pi(_, _, dom, _) if !struct_is_prop || self.quick_is_prop(&dom) => {
1711                Some((*dom).clone())
1712            }
1713            _ => None,
1714        }
1715    }
1716    /// Whether the type `ty` is known to be a proposition (its sort is
1717    /// `Prop` up to universe equivalence); `false` when it cannot be typed.
1718    fn quick_is_prop(&mut self, ty: &Expr) -> bool {
1719        let Some(sort) = self.quick_infer_type(ty) else {
1720            return false;
1721        };
1722        is_prop_sort(&self.reducer.whnf_env(&sort, self.env)) == Some(true)
1723    }
1724    /// Check proof irrelevance: two proofs of the same Prop are definitionally equal.
1725    ///
1726    /// Two terms `t` and `s` are proof-irrelevantly equal when:
1727    /// - Both have types that reduce to `Sort 0` (i.e., they live in `Prop`)
1728    /// - Their types are definitionally equal (they prove the same proposition)
1729    fn is_proof_irrelevant_eq(&mut self, t: &Expr, s: &Expr) -> bool {
1730        if !self.proof_irrelevance {
1731            return false;
1732        }
1733        let ty_t = match self.quick_infer_type(t) {
1734            Some(ty) => ty,
1735            None => return false,
1736        };
1737        let ty_ty_t = match self.quick_infer_type(&ty_t) {
1738            Some(ty) => ty,
1739            None => return false,
1740        };
1741        let ty_ty_t_whnf = self.reducer.whnf_env(&ty_ty_t, self.env);
1742        // Prop test must be up to universe equivalence, not syntactic: a proof
1743        // whose sort level is e.g. `imax(u, 0)` normalizes to `0` (Prop) but is
1744        // not the literal `Zero`. Use the complete `is_equivalent` check.
1745        if !matches!(&ty_ty_t_whnf, Expr::Sort(l) if level::is_equivalent(l, &Level::zero())) {
1746            return false;
1747        }
1748        let ty_s = match self.quick_infer_type(s) {
1749            Some(ty) => ty,
1750            None => return false,
1751        };
1752        let ty_ty_s = match self.quick_infer_type(&ty_s) {
1753            Some(ty) => ty,
1754            None => return false,
1755        };
1756        let ty_ty_s_whnf = self.reducer.whnf_env(&ty_ty_s, self.env);
1757        if !matches!(&ty_ty_s_whnf, Expr::Sort(l) if level::is_equivalent(l, &Level::zero())) {
1758            return false;
1759        }
1760        let ty_t_whnf = self.reducer.whnf_env(&ty_t, self.env);
1761        let ty_s_whnf = self.reducer.whnf_env(&ty_s, self.env);
1762        self.is_def_eq(&ty_t_whnf, &ty_s_whnf)
1763    }
1764    /// Try function eta when the left side is a lambda and the right is not.
1765    ///
1766    /// Fast path (contraction): `λ x. f x` =?= `g` when `f` doesn't use `x`,
1767    /// then `f =?= g`. Fallback (Lean `tryEtaExpansionCore`): eta-expand the
1768    /// non-lambda side by one binder from its Pi type and recurse, which
1769    /// handles multi-binder telescopes such as `(fun x y => g x y) =?= g`.
1770    fn try_eta_lhs(&mut self, t: &Expr, s: &Expr) -> bool {
1771        if let Expr::Lam(_, _, _, body) = t {
1772            if let Expr::App(f, a) = body.as_ref() {
1773                if let Expr::BVar(0) = **a {
1774                    if !has_loose_bvar(f, 0) {
1775                        let f_shifted =
1776                            crate::subst::instantiate(f, &Expr::FVar(crate::FVarId(u64::MAX)));
1777                        if self.is_def_eq(&f_shifted, s) {
1778                            return true;
1779                        }
1780                    }
1781                }
1782            }
1783        }
1784        if let Some(s_expanded) = self.eta_expand_one(s) {
1785            return self.is_def_eq(t, &s_expanded);
1786        }
1787        false
1788    }
1789    /// Try function eta when the right side is a lambda and the left is not.
1790    ///
1791    /// Symmetric to [`Self::try_eta_lhs`].
1792    fn try_eta_rhs(&mut self, t: &Expr, s: &Expr) -> bool {
1793        if let Expr::Lam(_, _, _, body) = s {
1794            if let Expr::App(f, a) = body.as_ref() {
1795                if let Expr::BVar(0) = **a {
1796                    if !has_loose_bvar(f, 0) {
1797                        let f_shifted =
1798                            crate::subst::instantiate(f, &Expr::FVar(crate::FVarId(u64::MAX)));
1799                        if self.is_def_eq(t, &f_shifted) {
1800                            return true;
1801                        }
1802                    }
1803                }
1804            }
1805        }
1806        if let Some(t_expanded) = self.eta_expand_one(t) {
1807            return self.is_def_eq(&t_expanded, s);
1808        }
1809        false
1810    }
1811    /// Eta-expand a non-lambda expression by one binder (one step of Lean's
1812    /// `etaExpand`).
1813    ///
1814    /// Given `s` whose inferred type whnf's to `Pi (a : d), b`, returns
1815    /// `fun (a : d) => s a`, lifting loose bound variables in `s` under the
1816    /// new binder. Returns `None` if `s` is a lambda or its type cannot be
1817    /// determined to be a Pi.
1818    fn eta_expand_one(&mut self, s: &Expr) -> Option<Expr> {
1819        if matches!(s, Expr::Lam(_, _, _, _)) {
1820            return None;
1821        }
1822        let s_ty = self.quick_infer_type(s)?;
1823        let s_ty_whnf = self.reducer.whnf_env(&s_ty, self.env);
1824        if let Expr::Pi(bi, name, dom, _) = s_ty_whnf {
1825            let s_lifted = crate::expr_util::lift_loose_bvars(s, 1, 0);
1826            Some(Expr::Lam(
1827                bi,
1828                name,
1829                dom,
1830                Node::new(Expr::App(Node::new(s_lifted), Node::new(Expr::BVar(0)))),
1831            ))
1832        } else {
1833            None
1834        }
1835    }
1836    /// Definitional eta for structures (Lean `isDefEqEtaStruct`).
1837    ///
1838    /// If `s` is a fully applied constructor application `S.mk ps fs` of a
1839    /// structure-like inductive `S` (exactly one constructor, no indices,
1840    /// not recursive) and `t` is not itself a constructor application, then
1841    /// eta-expand `t` to `S.mk ps' t.0 ... t.(n-1)` — with parameters `ps'`
1842    /// and universe levels read off `t`'s (whnf'd) type — and recurse.
1843    /// Application congruence then compares the constructor levels, the
1844    /// parameters, and each field `f_i` against `Proj(S, i, t)`, which
1845    /// subsumes Lean's `isDefEq (inferType t) (inferType s)` gate.
1846    ///
1847    /// Only fires when `t`'s type whnf's to the structure type `S ps'`;
1848    /// otherwise the pair is left for other rules (never a wrong result).
1849    fn try_eta_struct(&mut self, t: &Expr, s: &Expr) -> bool {
1850        let (ctor_name, ctor_val) = match get_app_fn(s) {
1851            Expr::Const(name, _) => match self.env.get_constructor_val(name) {
1852                Some(cv) => (name.clone(), cv.clone()),
1853                None => return false,
1854            },
1855            _ => return false,
1856        };
1857        if get_app_args(s).len() != (ctor_val.num_params + ctor_val.num_fields) as usize {
1858            return false;
1859        }
1860        if !self.env.is_structure_like(&ctor_val.induct) {
1861            return false;
1862        }
1863        // Constructor-vs-constructor pairs are already handled by
1864        // application congruence; expanding here would be redundant.
1865        if let Expr::Const(t_head, _) = get_app_fn(t) {
1866            if self.env.is_constructor(t_head) {
1867                return false;
1868            }
1869        }
1870        let t_ty = match self.quick_infer_type(t) {
1871            Some(ty) => ty,
1872            None => return false,
1873        };
1874        let t_ty_whnf = self.reducer.whnf_env(&t_ty, self.env);
1875        let levels: Vec<Level> = match get_app_fn(&t_ty_whnf) {
1876            Expr::Const(ind_name, lvls) if *ind_name == ctor_val.induct => lvls.clone(),
1877            _ => return false,
1878        };
1879        let params: Vec<Expr> = get_app_args(&t_ty_whnf).into_iter().cloned().collect();
1880        if params.len() != ctor_val.num_params as usize {
1881            return false;
1882        }
1883        let mut expansion = Expr::Const(ctor_name, levels);
1884        for param in params {
1885            expansion = Expr::App(Node::new(expansion), Node::new(param));
1886        }
1887        for i in 0..ctor_val.num_fields {
1888            let field = Expr::Proj(ctor_val.induct.clone(), i, Node::new(t.clone()));
1889            expansion = Expr::App(Node::new(expansion), Node::new(field));
1890        }
1891        self.is_def_eq(s, &expansion)
1892    }
1893    /// Unit-like definitional equality (Lean `isDefEqUnitLike`).
1894    ///
1895    /// If `t`'s type whnf's to `S ps` where `S` is structure-like and its
1896    /// single constructor has no fields beyond the parameters, then any two
1897    /// elements of that type are definitionally equal: it suffices that the
1898    /// types agree, i.e. `t : S ps` and `s`'s type is def-eq to `S ps`.
1899    fn is_def_eq_unit_like(&mut self, t: &Expr, s: &Expr) -> bool {
1900        let t_ty = match self.quick_infer_type(t) {
1901            Some(ty) => ty,
1902            None => return false,
1903        };
1904        let t_ty_whnf = self.reducer.whnf_env(&t_ty, self.env);
1905        let ind_name = match get_app_fn(&t_ty_whnf) {
1906            Expr::Const(name, _) => name.clone(),
1907            _ => return false,
1908        };
1909        if !self.env.is_structure_like(&ind_name) {
1910            return false;
1911        }
1912        let ctor_name = match self
1913            .env
1914            .get_inductive_val(&ind_name)
1915            .and_then(|iv| iv.ctors.first())
1916        {
1917            Some(name) => name.clone(),
1918            None => return false,
1919        };
1920        match self.env.get_constructor_val(&ctor_name) {
1921            Some(cv) if cv.num_fields == 0 => {}
1922            _ => return false,
1923        }
1924        let s_ty = match self.quick_infer_type(s) {
1925            Some(ty) => ty,
1926            None => return false,
1927        };
1928        self.is_def_eq(&t_ty_whnf, &s_ty)
1929    }
1930}
1931/// A simple key-value store backed by a sorted Vec for small maps.
1932#[allow(dead_code)]
1933pub struct SmallMap<K: Ord + Clone, V: Clone> {
1934    entries: Vec<(K, V)>,
1935}
1936#[allow(dead_code)]
1937impl<K: Ord + Clone, V: Clone> SmallMap<K, V> {
1938    /// Creates a new empty small map.
1939    pub fn new() -> Self {
1940        Self {
1941            entries: Vec::new(),
1942        }
1943    }
1944    /// Inserts or replaces the value for `key`.
1945    pub fn insert(&mut self, key: K, val: V) {
1946        match self.entries.binary_search_by_key(&&key, |(k, _)| k) {
1947            Ok(i) => self.entries[i].1 = val,
1948            Err(i) => self.entries.insert(i, (key, val)),
1949        }
1950    }
1951    /// Returns the value for `key`, or `None`.
1952    pub fn get(&self, key: &K) -> Option<&V> {
1953        self.entries
1954            .binary_search_by_key(&key, |(k, _)| k)
1955            .ok()
1956            .map(|i| &self.entries[i].1)
1957    }
1958    /// Returns the number of entries.
1959    pub fn len(&self) -> usize {
1960        self.entries.len()
1961    }
1962    /// Returns `true` if empty.
1963    pub fn is_empty(&self) -> bool {
1964        self.entries.is_empty()
1965    }
1966    /// Returns all keys.
1967    pub fn keys(&self) -> Vec<&K> {
1968        self.entries.iter().map(|(k, _)| k).collect()
1969    }
1970    /// Returns all values.
1971    pub fn values(&self) -> Vec<&V> {
1972        self.entries.iter().map(|(_, v)| v).collect()
1973    }
1974}