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