Skip to main content

oxilean_kernel/reduction/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use crate::Node;
6use crate::{Expr, Reducer};
7use std::rc::Rc;
8
9use super::types::{
10    ConfigNode, DecisionNode, Either2, Fixture, FlatSubstitution, FocusStack, HeadForm, LabelSet,
11    MinHeap, NonEmptyVec, PathBuf, PrefixCounter, RedexInfo, RedexKind, ReductionBound,
12    ReductionMemo, ReductionResult, ReductionStats, ReductionStep, ReductionStrategy,
13    ReductionTrace, RewriteRule, RewriteRuleSet, SimpleDag, SlidingSum, SmallMap, SparseVec,
14    StackCalc, StatSummary, Stopwatch, StringPool, TokenBucket, TransformStat, TransitiveClosure,
15    VersionedRecord, WindowIterator, WriteOnce,
16};
17
18/// Reduce an expression using a specific strategy.
19///
20/// This is the primary entry point for expression reduction. It dispatches
21/// to the appropriate reduction algorithm based on the strategy.
22pub fn reduce_with_strategy(expr: &Expr, strategy: ReductionStrategy) -> Expr {
23    let mut reducer = Reducer::new();
24    match strategy {
25        ReductionStrategy::WHNF => reducer.whnf(expr),
26        ReductionStrategy::NF => reduce_to_nf(expr, &mut reducer, 500),
27        ReductionStrategy::OneStep => one_step_reduce(expr).unwrap_or_else(|| expr.clone()),
28        ReductionStrategy::CBV => reduce_cbv(expr, &mut reducer),
29        ReductionStrategy::CBN => reduce_cbn(expr, &mut reducer),
30        ReductionStrategy::HeadOnly => reduce_head(expr),
31    }
32}
33/// Reduce an expression to full normal form.
34///
35/// After reducing to WHNF, recursively reduce all sub-expressions.
36/// The fuel parameter limits total recursion to avoid infinite loops.
37pub(super) fn reduce_to_nf(expr: &Expr, reducer: &mut Reducer, fuel: u32) -> Expr {
38    if fuel == 0 {
39        return expr.clone();
40    }
41    let whnf = reducer.whnf(expr);
42    match whnf {
43        Expr::Sort(_) | Expr::BVar(_) | Expr::FVar(_) | Expr::Lit(_) | Expr::Const(_, _) => whnf,
44        Expr::Lam(bi, name, ty, body) => {
45            let ty_nf = reduce_to_nf(&ty, reducer, fuel - 1);
46            let body_nf = reduce_to_nf(&body, reducer, fuel - 1);
47            Expr::Lam(bi, name, Node::new(ty_nf), Node::new(body_nf))
48        }
49        Expr::Pi(bi, name, ty, body) => {
50            let ty_nf = reduce_to_nf(&ty, reducer, fuel - 1);
51            let body_nf = reduce_to_nf(&body, reducer, fuel - 1);
52            Expr::Pi(bi, name, Node::new(ty_nf), Node::new(body_nf))
53        }
54        Expr::App(f, a) => {
55            let f_nf = reduce_to_nf(&f, reducer, fuel - 1);
56            let a_nf = reduce_to_nf(&a, reducer, fuel - 1);
57            let rebuilt = Expr::App(Node::new(f_nf), Node::new(a_nf));
58            let whnf2 = reducer.whnf(&rebuilt);
59            if whnf2 == rebuilt {
60                rebuilt
61            } else {
62                reduce_to_nf(&whnf2, reducer, fuel.saturating_sub(1))
63            }
64        }
65        Expr::Let(_name, _ty, val, body) => {
66            let reduced = crate::subst::instantiate(&body, &val);
67            reduce_to_nf(&reduced, reducer, fuel - 1)
68        }
69        Expr::Proj(struct_name, idx, e) => {
70            let e_nf = reduce_to_nf(&e, reducer, fuel - 1);
71            Expr::Proj(struct_name, idx, Node::new(e_nf))
72        }
73    }
74}
75/// Apply exactly one reduction step (outermost-leftmost).
76///
77/// Returns `Some(reduced)` if a redex was found, `None` if already in NF.
78#[allow(clippy::only_used_in_recursion)]
79pub(super) fn one_step_reduce(expr: &Expr) -> Option<Expr> {
80    match expr {
81        Expr::App(f, a) => {
82            if let Expr::Lam(_, _, _, body) = f.as_ref() {
83                let reduced = crate::subst::instantiate(body, a);
84                return Some(reduced);
85            }
86            if let Some(f_reduced) = one_step_reduce(f) {
87                return Some(Expr::App(Node::new(f_reduced), a.clone()));
88            }
89            if let Some(a_reduced) = one_step_reduce(a) {
90                return Some(Expr::App(f.clone(), Node::new(a_reduced)));
91            }
92            None
93        }
94        Expr::Let(_, _, val, body) => {
95            let reduced = crate::subst::instantiate(body, val);
96            Some(reduced)
97        }
98        Expr::Lam(bi, name, ty, body) => {
99            if let Some(ty_r) = one_step_reduce(ty) {
100                return Some(Expr::Lam(*bi, name.clone(), Node::new(ty_r), body.clone()));
101            }
102            if let Some(body_r) = one_step_reduce(body) {
103                return Some(Expr::Lam(*bi, name.clone(), ty.clone(), Node::new(body_r)));
104            }
105            None
106        }
107        Expr::Pi(bi, name, ty, body) => {
108            if let Some(ty_r) = one_step_reduce(ty) {
109                return Some(Expr::Pi(*bi, name.clone(), Node::new(ty_r), body.clone()));
110            }
111            if let Some(body_r) = one_step_reduce(body) {
112                return Some(Expr::Pi(*bi, name.clone(), ty.clone(), Node::new(body_r)));
113            }
114            None
115        }
116        Expr::Proj(struct_name, idx, e) => {
117            if let Some(e_r) = one_step_reduce(e) {
118                return Some(Expr::Proj(struct_name.clone(), *idx, Node::new(e_r)));
119            }
120            None
121        }
122        Expr::Sort(_) | Expr::BVar(_) | Expr::FVar(_) | Expr::Lit(_) | Expr::Const(_, _) => None,
123    }
124}
125/// Call-by-value reduction: evaluate arguments to WHNF before applying.
126///
127/// In CBV, all arguments are reduced to WHNF (values) before the function
128/// body is entered. This is strict/eager evaluation.
129pub(super) fn reduce_cbv(expr: &Expr, reducer: &mut Reducer) -> Expr {
130    reduce_cbv_impl(expr, reducer, 500)
131}
132#[allow(clippy::only_used_in_recursion)]
133pub(super) fn reduce_cbv_impl(expr: &Expr, reducer: &mut Reducer, fuel: u32) -> Expr {
134    if fuel == 0 {
135        return expr.clone();
136    }
137    match expr {
138        Expr::App(f, a) => {
139            let a_val = reducer.whnf(a);
140            let f_val = reduce_cbv_impl(f, reducer, fuel - 1);
141            match f_val {
142                Expr::Lam(_, _, _, body) => {
143                    let reduced = crate::subst::instantiate(&body, &a_val);
144                    reduce_cbv_impl(&reduced, reducer, fuel - 1)
145                }
146                _ => Expr::App(Node::new(f_val), Node::new(a_val)),
147            }
148        }
149        Expr::Let(_, _, val, body) => {
150            let val_whnf = reducer.whnf(val);
151            let reduced = crate::subst::instantiate(body, &val_whnf);
152            reduce_cbv_impl(&reduced, reducer, fuel - 1)
153        }
154        Expr::Lam(_, _, _, _) | Expr::Pi(_, _, _, _) => reducer.whnf(expr),
155        _ => reducer.whnf(expr),
156    }
157}
158/// Call-by-name reduction: reduce function before arguments, pass args unevaluated.
159///
160/// In CBN, arguments are substituted without first being evaluated.
161/// The function is reduced to a lambda, then the body is evaluated with
162/// the argument passed as-is.
163pub(super) fn reduce_cbn(expr: &Expr, reducer: &mut Reducer) -> Expr {
164    reduce_cbn_impl(expr, reducer, 500)
165}
166#[allow(clippy::only_used_in_recursion)]
167pub(super) fn reduce_cbn_impl(expr: &Expr, reducer: &mut Reducer, fuel: u32) -> Expr {
168    if fuel == 0 {
169        return expr.clone();
170    }
171    match expr {
172        Expr::App(f, a) => {
173            let f_val = reduce_cbn_impl(f, reducer, fuel - 1);
174            match f_val {
175                Expr::Lam(_, _, _, body) => {
176                    let reduced = crate::subst::instantiate(&body, a);
177                    reduce_cbn_impl(&reduced, reducer, fuel - 1)
178                }
179                _ => Expr::App(Node::new(f_val), a.clone()),
180            }
181        }
182        Expr::Let(_, _, val, body) => {
183            let reduced = crate::subst::instantiate(body, val);
184            reduce_cbn_impl(&reduced, reducer, fuel - 1)
185        }
186        _ => reducer.whnf(expr),
187    }
188}
189/// Reduce only the head of an expression (not arguments).
190///
191/// For an application `f a1 ... an`, reduces `f` to WHNF without
192/// reducing the arguments.
193pub fn reduce_head(expr: &Expr) -> Expr {
194    let mut reducer = Reducer::new();
195    reducer.whnf(expr)
196}
197/// Check if an expression is in normal form.
198///
199/// An expression is in normal form if no reduction rules apply. This
200/// conservatively checks syntactic normal form without performing reductions.
201pub fn is_normal_form(expr: &Expr) -> bool {
202    match expr {
203        Expr::Sort(_) | Expr::BVar(_) | Expr::FVar(_) => true,
204        Expr::Const(_, _) => true,
205        Expr::Lit(_) => true,
206        Expr::Lam(_, _, ty, body) => is_normal_form(ty) && is_normal_form(body),
207        Expr::Pi(_, _, ty, body) => is_normal_form(ty) && is_normal_form(body),
208        Expr::App(f, a) => {
209            if matches!(f.as_ref(), Expr::Lam(_, _, _, _)) {
210                return false;
211            }
212            is_normal_form(f) && is_normal_form(a)
213        }
214        Expr::Let(_, _, _, _) => false,
215        Expr::Proj(_, _, e) => is_normal_form(e),
216    }
217}
218/// Count the number of reduction steps needed.
219///
220/// Iteratively reduces the expression until it reaches a fixed point,
221/// counting the steps taken. Capped at 10,000 steps to prevent infinite loops.
222pub fn count_reduction_steps(expr: &Expr, _strategy: ReductionStrategy) -> usize {
223    let mut steps = 0;
224    let mut current = expr.clone();
225    loop {
226        let next = reduce_with_strategy(&current, ReductionStrategy::OneStep);
227        if next == current {
228            break;
229        }
230        steps += 1;
231        current = next;
232        if steps > 10000 {
233            break;
234        }
235    }
236    steps
237}
238/// Check if two expressions are convertible (definitionally equal).
239///
240/// Reduces both expressions to normal form and checks for syntactic equality.
241/// This is a coarse approximation; the kernel's definitional equality checker
242/// is more precise.
243pub fn are_convertible(e1: &Expr, e2: &Expr) -> bool {
244    let nf1 = reduce_with_strategy(e1, ReductionStrategy::NF);
245    let nf2 = reduce_with_strategy(e2, ReductionStrategy::NF);
246    nf1 == nf2
247}
248/// Attempt a single reduction step, returning a `ReductionResult`.
249///
250/// Unlike `count_reduction_steps`, this function performs at most one step
251/// and clearly indicates whether the expression changed.
252pub fn try_reduce_step(expr: &Expr) -> ReductionResult {
253    let reduced = reduce_with_strategy(expr, ReductionStrategy::OneStep);
254    if reduced == *expr {
255        ReductionResult::Normal(reduced)
256    } else {
257        ReductionResult::Reduced(reduced)
258    }
259}
260/// Build a reduction trace for an expression.
261///
262/// Reduces the expression step-by-step, recording each intermediate form.
263/// Stops after `max_steps` steps (default 100).
264pub fn build_reduction_trace(expr: &Expr, max_steps: usize) -> ReductionTrace {
265    let mut trace = ReductionTrace::new();
266    let mut current = expr.clone();
267    for _ in 0..max_steps {
268        let next = reduce_with_strategy(&current, ReductionStrategy::OneStep);
269        if next == current {
270            trace.reached_normal = true;
271            break;
272        }
273        trace.steps.push(ReductionStep {
274            before: current.clone(),
275            after: next.clone(),
276            rule: "beta/delta".to_string(),
277        });
278        current = next;
279    }
280    if !trace.reached_normal && trace.len() >= max_steps {
281        trace.truncated = true;
282    }
283    trace
284}
285/// Compute the size (number of nodes) of an expression.
286///
287/// Used as a heuristic for reduction complexity.
288pub fn expr_size(expr: &Expr) -> usize {
289    match expr {
290        Expr::Sort(_) | Expr::BVar(_) | Expr::FVar(_) | Expr::Const(_, _) | Expr::Lit(_) => 1,
291        Expr::App(f, a) => 1 + expr_size(f) + expr_size(a),
292        Expr::Lam(_, _, ty, body) | Expr::Pi(_, _, ty, body) => 1 + expr_size(ty) + expr_size(body),
293        Expr::Let(_, ty, val, body) => 1 + expr_size(ty) + expr_size(val) + expr_size(body),
294        Expr::Proj(_, _, e) => 1 + expr_size(e),
295    }
296}
297/// Compute the depth of an expression tree.
298pub fn expr_depth(expr: &Expr) -> usize {
299    match expr {
300        Expr::Sort(_) | Expr::BVar(_) | Expr::FVar(_) | Expr::Const(_, _) | Expr::Lit(_) => 1,
301        Expr::App(f, a) => 1 + expr_depth(f).max(expr_depth(a)),
302        Expr::Lam(_, _, ty, body) | Expr::Pi(_, _, ty, body) => {
303            1 + expr_depth(ty).max(expr_depth(body))
304        }
305        Expr::Let(_, ty, val, body) => {
306            1 + expr_depth(ty).max(expr_depth(val)).max(expr_depth(body))
307        }
308        Expr::Proj(_, _, e) => 1 + expr_depth(e),
309    }
310}
311/// Count the number of beta-redexes in an expression.
312///
313/// A beta-redex is an application of the form `(λx. e) a`.
314pub fn count_beta_redexes(expr: &Expr) -> usize {
315    match expr {
316        Expr::Sort(_) | Expr::BVar(_) | Expr::FVar(_) | Expr::Const(_, _) | Expr::Lit(_) => 0,
317        Expr::App(f, a) => {
318            let is_redex = matches!(f.as_ref(), Expr::Lam(_, _, _, _));
319            let base = if is_redex { 1 } else { 0 };
320            base + count_beta_redexes(f) + count_beta_redexes(a)
321        }
322        Expr::Lam(_, _, ty, body) | Expr::Pi(_, _, ty, body) => {
323            count_beta_redexes(ty) + count_beta_redexes(body)
324        }
325        Expr::Let(_, ty, val, body) => {
326            1 + count_beta_redexes(ty) + count_beta_redexes(val) + count_beta_redexes(body)
327        }
328        Expr::Proj(_, _, e) => count_beta_redexes(e),
329    }
330}
331/// Classify the head form of an expression.
332pub fn classify_head(expr: &Expr) -> HeadForm {
333    match expr {
334        Expr::BVar(i) => HeadForm::BVar(*i),
335        Expr::FVar(_) => HeadForm::FVar,
336        Expr::Const(name, _) => HeadForm::Const(name.clone()),
337        Expr::Sort(_) => HeadForm::Sort,
338        Expr::Lam(_, _, _, _) => HeadForm::Lambda,
339        Expr::Pi(_, _, _, _) => HeadForm::Pi,
340        Expr::Lit(_) => HeadForm::Lit,
341        Expr::Let(_, _, _, _) => HeadForm::Let,
342        Expr::Proj(_, _, _) => HeadForm::Proj,
343        Expr::App(f, _) => {
344            if matches!(f.as_ref(), Expr::Lam(_, _, _, _)) {
345                HeadForm::BetaRedex
346            } else {
347                classify_head(f)
348            }
349        }
350    }
351}
352/// Find all redexes in an expression.
353///
354/// Returns information about each reducible sub-expression.
355pub fn find_redexes(expr: &Expr) -> Vec<RedexInfo> {
356    let mut redexes = Vec::new();
357    find_redexes_rec(expr, 0, &mut redexes);
358    redexes
359}
360pub(super) fn find_redexes_rec(expr: &Expr, depth: usize, redexes: &mut Vec<RedexInfo>) {
361    match expr {
362        Expr::App(f, a) => {
363            if matches!(f.as_ref(), Expr::Lam(_, _, _, _)) {
364                redexes.push(RedexInfo {
365                    kind: RedexKind::Beta,
366                    depth,
367                    size: expr_size(expr),
368                });
369            }
370            find_redexes_rec(f, depth + 1, redexes);
371            find_redexes_rec(a, depth + 1, redexes);
372        }
373        Expr::Let(_, ty, val, body) => {
374            redexes.push(RedexInfo {
375                kind: RedexKind::Let,
376                depth,
377                size: expr_size(expr),
378            });
379            find_redexes_rec(ty, depth + 1, redexes);
380            find_redexes_rec(val, depth + 1, redexes);
381            find_redexes_rec(body, depth + 1, redexes);
382        }
383        Expr::Proj(_, _, e) => {
384            find_redexes_rec(e, depth + 1, redexes);
385        }
386        Expr::Lam(_, _, ty, body) | Expr::Pi(_, _, ty, body) => {
387            find_redexes_rec(ty, depth + 1, redexes);
388            find_redexes_rec(body, depth + 1, redexes);
389        }
390        _ => {}
391    }
392}
393/// Reduce an expression with a bound on the number of steps.
394///
395/// Returns the reduced expression and statistics.
396pub fn reduce_bounded(
397    expr: &Expr,
398    strategy: ReductionStrategy,
399    bound: ReductionBound,
400) -> (Expr, ReductionStats) {
401    let mut stats = ReductionStats::new();
402    let mut current = expr.clone();
403    loop {
404        if bound.exceeded(stats.total_steps, expr_size(&current)) {
405            stats.aborted = true;
406            break;
407        }
408        let next = reduce_with_strategy(&current, strategy);
409        if next == current {
410            break;
411        }
412        stats.total_steps += 1;
413        let head = classify_head(&current);
414        match head {
415            HeadForm::BetaRedex => stats.beta_steps += 1,
416            HeadForm::Let => stats.let_steps += 1,
417            _ => stats.delta_steps += 1,
418        }
419        current = next;
420    }
421    (current, stats)
422}
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use crate::{BinderInfo, Level, Literal, Name};
427    fn mk_nat_lit(n: u64) -> Expr {
428        Expr::Lit(Literal::nat(n))
429    }
430    fn mk_lam(body: Expr) -> Expr {
431        Expr::Lam(
432            BinderInfo::Default,
433            Name::str("x"),
434            Node::new(Expr::Sort(Level::zero())),
435            Node::new(body),
436        )
437    }
438    fn mk_app(f: Expr, a: Expr) -> Expr {
439        Expr::App(Node::new(f), Node::new(a))
440    }
441    fn mk_let(val: Expr, body: Expr) -> Expr {
442        Expr::Let(
443            Name::str("x"),
444            Node::new(Expr::Sort(Level::zero())),
445            Node::new(val),
446            Node::new(body),
447        )
448    }
449    #[test]
450    fn test_reduce_whnf() {
451        let expr = Expr::Sort(Level::zero());
452        let result = reduce_with_strategy(&expr, ReductionStrategy::WHNF);
453        assert_eq!(result, expr);
454    }
455    #[test]
456    fn test_is_normal_form_sort() {
457        let expr = Expr::Sort(Level::zero());
458        assert!(is_normal_form(&expr));
459    }
460    #[test]
461    fn test_is_normal_form_lit() {
462        let expr = Expr::Lit(Literal::nat(42));
463        assert!(is_normal_form(&expr));
464    }
465    #[test]
466    fn test_not_normal_form_beta() {
467        let lam = mk_lam(Expr::BVar(0));
468        let app = mk_app(lam, mk_nat_lit(42));
469        assert!(!is_normal_form(&app));
470    }
471    #[test]
472    fn test_not_normal_form_let() {
473        let let_expr = mk_let(mk_nat_lit(42), Expr::BVar(0));
474        assert!(!is_normal_form(&let_expr));
475    }
476    #[test]
477    fn test_are_convertible_same() {
478        let e1 = mk_nat_lit(42);
479        let e2 = mk_nat_lit(42);
480        assert!(are_convertible(&e1, &e2));
481    }
482    #[test]
483    fn test_count_reduction_steps_zero() {
484        let expr = mk_nat_lit(42);
485        let steps = count_reduction_steps(&expr, ReductionStrategy::WHNF);
486        assert_eq!(steps, 0);
487    }
488    #[test]
489    fn test_reduction_strategy_name() {
490        assert_eq!(ReductionStrategy::WHNF.name(), "whnf");
491        assert_eq!(ReductionStrategy::NF.name(), "nf");
492        assert_eq!(ReductionStrategy::CBV.name(), "cbv");
493        assert_eq!(ReductionStrategy::HeadOnly.name(), "head-only");
494    }
495    #[test]
496    fn test_reduction_strategy_lazy_eager() {
497        assert!(ReductionStrategy::WHNF.is_lazy());
498        assert!(ReductionStrategy::CBV.is_eager());
499        assert!(!ReductionStrategy::WHNF.is_eager());
500    }
501    #[test]
502    fn test_reduction_strategy_complete() {
503        assert!(ReductionStrategy::NF.is_complete());
504        assert!(!ReductionStrategy::WHNF.is_complete());
505    }
506    #[test]
507    fn test_try_reduce_step_normal() {
508        let expr = mk_nat_lit(5);
509        let result = try_reduce_step(&expr);
510        assert!(result.is_normal());
511    }
512    #[test]
513    fn test_try_reduce_step_reduces() {
514        let lam = mk_lam(Expr::BVar(0));
515        let app = mk_app(lam, mk_nat_lit(0));
516        let result = try_reduce_step(&app);
517        assert!(result.was_reduced());
518    }
519    #[test]
520    fn test_classify_head_sort() {
521        let e = Expr::Sort(Level::zero());
522        assert_eq!(classify_head(&e), HeadForm::Sort);
523    }
524    #[test]
525    fn test_classify_head_lambda() {
526        let e = mk_lam(Expr::BVar(0));
527        assert_eq!(classify_head(&e), HeadForm::Lambda);
528    }
529    #[test]
530    fn test_classify_head_beta_redex() {
531        let lam = mk_lam(Expr::BVar(0));
532        let app = mk_app(lam, mk_nat_lit(42));
533        assert_eq!(classify_head(&app), HeadForm::BetaRedex);
534    }
535    #[test]
536    fn test_classify_head_let() {
537        let e = mk_let(mk_nat_lit(1), Expr::BVar(0));
538        assert_eq!(classify_head(&e), HeadForm::Let);
539    }
540    #[test]
541    fn test_expr_size_lit() {
542        assert_eq!(expr_size(&mk_nat_lit(0)), 1);
543    }
544    #[test]
545    fn test_expr_size_app() {
546        let app = mk_app(mk_nat_lit(1), mk_nat_lit(2));
547        assert_eq!(expr_size(&app), 3);
548    }
549    #[test]
550    fn test_expr_depth() {
551        let lit = mk_nat_lit(0);
552        assert_eq!(expr_depth(&lit), 1);
553        let app = mk_app(lit.clone(), lit.clone());
554        assert_eq!(expr_depth(&app), 2);
555    }
556    #[test]
557    fn test_count_beta_redexes_none() {
558        let expr = mk_nat_lit(42);
559        assert_eq!(count_beta_redexes(&expr), 0);
560    }
561    #[test]
562    fn test_count_beta_redexes_one() {
563        let lam = mk_lam(Expr::BVar(0));
564        let app = mk_app(lam, mk_nat_lit(5));
565        assert_eq!(count_beta_redexes(&app), 1);
566    }
567    #[test]
568    fn test_count_beta_redexes_let() {
569        let e = mk_let(mk_nat_lit(1), Expr::BVar(0));
570        assert_eq!(count_beta_redexes(&e), 1);
571    }
572    #[test]
573    fn test_find_redexes_empty() {
574        let expr = mk_nat_lit(42);
575        let redexes = find_redexes(&expr);
576        assert!(redexes.is_empty());
577    }
578    #[test]
579    fn test_find_redexes_beta() {
580        let lam = mk_lam(Expr::BVar(0));
581        let app = mk_app(lam, mk_nat_lit(5));
582        let redexes = find_redexes(&app);
583        assert!(!redexes.is_empty());
584        assert!(redexes.iter().any(|r| r.kind == RedexKind::Beta));
585    }
586    #[test]
587    fn test_find_redexes_let() {
588        let e = mk_let(mk_nat_lit(1), Expr::BVar(0));
589        let redexes = find_redexes(&e);
590        assert!(redexes.iter().any(|r| r.kind == RedexKind::Let));
591    }
592    #[test]
593    fn test_reduction_stats_summary() {
594        let stats = ReductionStats {
595            beta_steps: 3,
596            delta_steps: 1,
597            let_steps: 0,
598            proj_steps: 0,
599            total_steps: 4,
600            aborted: false,
601        };
602        let s = stats.summary();
603        assert!(s.contains("β:3"));
604        assert!(s.contains("total:4"));
605    }
606    #[test]
607    fn test_reduction_bound_exceeded() {
608        let bound = ReductionBound::Steps(5);
609        assert!(!bound.exceeded(4, 100));
610        assert!(bound.exceeded(5, 100));
611    }
612    #[test]
613    fn test_reduce_bounded_normal() {
614        let expr = mk_nat_lit(42);
615        let (result, stats) =
616            reduce_bounded(&expr, ReductionStrategy::WHNF, ReductionBound::Steps(100));
617        assert_eq!(result, expr);
618        assert!(!stats.any_reductions());
619    }
620    #[test]
621    fn test_build_reduction_trace_normal() {
622        let expr = mk_nat_lit(1);
623        let trace = build_reduction_trace(&expr, 10);
624        assert!(trace.reached_normal);
625        assert!(trace.is_empty());
626    }
627    #[test]
628    fn test_head_form_is_neutral() {
629        assert!(HeadForm::BVar(0).is_neutral());
630        assert!(HeadForm::FVar.is_neutral());
631        assert!(HeadForm::Const(Name::str("foo")).is_neutral());
632        assert!(!HeadForm::Lambda.is_neutral());
633    }
634    #[test]
635    fn test_redex_kind_description() {
636        assert!(RedexKind::Beta.description().contains('β'));
637        assert!(RedexKind::Let.description().contains("let"));
638        let delta = RedexKind::Delta(Name::str("foo"));
639        assert!(delta.description().contains("foo"));
640    }
641    #[test]
642    fn test_reduction_result_accessors() {
643        let e = mk_nat_lit(0);
644        let r = ReductionResult::Normal(e.clone());
645        assert!(r.is_normal());
646        assert!(!r.was_reduced());
647        assert_eq!(r.into_expr(), e);
648    }
649}
650/// Check whether two expressions are alpha-equivalent (same up to variable renaming).
651///
652/// Uses a structural comparison, treating `BVar` indices as-is.
653/// This is a lightweight check — it does not perform reduction.
654pub fn alpha_equiv(e1: &Expr, e2: &Expr) -> bool {
655    match (e1, e2) {
656        (Expr::BVar(i), Expr::BVar(j)) => i == j,
657        (Expr::FVar(a), Expr::FVar(b)) => a == b,
658        (Expr::Sort(l1), Expr::Sort(l2)) => l1 == l2,
659        (Expr::Lit(l1), Expr::Lit(l2)) => l1 == l2,
660        (Expr::Const(n1, _), Expr::Const(n2, _)) => n1 == n2,
661        (Expr::App(f1, a1), Expr::App(f2, a2)) => alpha_equiv(f1, f2) && alpha_equiv(a1, a2),
662        (Expr::Lam(_, _, t1, b1), Expr::Lam(_, _, t2, b2)) => {
663            alpha_equiv(t1, t2) && alpha_equiv(b1, b2)
664        }
665        (Expr::Pi(_, _, t1, b1), Expr::Pi(_, _, t2, b2)) => {
666            alpha_equiv(t1, t2) && alpha_equiv(b1, b2)
667        }
668        (Expr::Let(_, ty1, v1, b1), Expr::Let(_, ty2, v2, b2)) => {
669            alpha_equiv(ty1, ty2) && alpha_equiv(v1, v2) && alpha_equiv(b1, b2)
670        }
671        _ => false,
672    }
673}
674/// Compute a fingerprint (hash) for an expression.
675///
676/// Used as a quick non-equality check before full comparison.
677pub fn expr_fingerprint(expr: &Expr) -> u64 {
678    use std::collections::hash_map::DefaultHasher;
679    use std::hash::{Hash, Hasher};
680    let mut h = DefaultHasher::new();
681    format!("{:?}", expr).hash(&mut h);
682    h.finish()
683}
684#[cfg(test)]
685mod extra_reduction_tests {
686    use super::*;
687    use crate::{Level, Literal, Name};
688    fn mk_nat_lit(n: u64) -> Expr {
689        Expr::Lit(Literal::nat(n))
690    }
691    fn mk_sort() -> Expr {
692        Expr::Sort(Level::zero())
693    }
694    #[test]
695    fn test_reduction_memo_insert_get() {
696        let mut memo = ReductionMemo::new();
697        let expr = mk_nat_lit(5);
698        let result = mk_nat_lit(5);
699        memo.insert(ReductionStrategy::WHNF, &expr, result.clone());
700        let got = memo.get(ReductionStrategy::WHNF, &expr);
701        assert_eq!(got, Some(&result));
702    }
703    #[test]
704    fn test_reduction_memo_miss() {
705        let mut memo = ReductionMemo::new();
706        let expr = mk_nat_lit(42);
707        assert_eq!(memo.get(ReductionStrategy::NF, &expr), None);
708    }
709    #[test]
710    fn test_reduction_memo_hit_rate() {
711        let mut memo = ReductionMemo::new();
712        let expr = mk_nat_lit(1);
713        memo.insert(ReductionStrategy::WHNF, &expr, expr.clone());
714        let _ = memo.get(ReductionStrategy::WHNF, &expr);
715        let _ = memo.get(ReductionStrategy::NF, &expr);
716        let rate = memo.hit_rate();
717        assert!(rate > 0.0 && rate <= 1.0);
718    }
719    #[test]
720    fn test_reduction_memo_clear() {
721        let mut memo = ReductionMemo::new();
722        let expr = mk_nat_lit(1);
723        memo.insert(ReductionStrategy::WHNF, &expr, expr.clone());
724        memo.clear();
725        assert!(memo.is_empty());
726    }
727    #[test]
728    fn test_alpha_equiv_bvar() {
729        assert!(alpha_equiv(&Expr::BVar(0), &Expr::BVar(0)));
730        assert!(!alpha_equiv(&Expr::BVar(0), &Expr::BVar(1)));
731    }
732    #[test]
733    fn test_alpha_equiv_sort() {
734        assert!(alpha_equiv(&mk_sort(), &mk_sort()));
735    }
736    #[test]
737    fn test_alpha_equiv_app() {
738        let f = Expr::Const(Name::str("f"), vec![]);
739        let a = mk_nat_lit(1);
740        let e1 = Expr::App(Node::new(f.clone()), Node::new(a.clone()));
741        let e2 = Expr::App(Node::new(f), Node::new(a));
742        assert!(alpha_equiv(&e1, &e2));
743    }
744    #[test]
745    fn test_alpha_equiv_different_constructors() {
746        let e1 = mk_nat_lit(1);
747        let e2 = mk_sort();
748        assert!(!alpha_equiv(&e1, &e2));
749    }
750    #[test]
751    fn test_expr_fingerprint_same() {
752        let e = mk_nat_lit(7);
753        assert_eq!(expr_fingerprint(&e), expr_fingerprint(&e));
754    }
755    #[test]
756    fn test_expr_fingerprint_different() {
757        let e1 = mk_nat_lit(1);
758        let e2 = mk_nat_lit(2);
759        assert_ne!(expr_fingerprint(&e1), expr_fingerprint(&e2));
760    }
761    #[test]
762    fn test_reduction_memo_strategy_separation() {
763        let mut memo = ReductionMemo::new();
764        let expr = mk_nat_lit(3);
765        memo.insert(ReductionStrategy::WHNF, &expr, mk_nat_lit(10));
766        memo.insert(ReductionStrategy::NF, &expr, mk_nat_lit(20));
767        assert_eq!(
768            memo.get(ReductionStrategy::WHNF, &expr),
769            Some(&mk_nat_lit(10))
770        );
771        assert_eq!(
772            memo.get(ReductionStrategy::NF, &expr),
773            Some(&mk_nat_lit(20))
774        );
775    }
776    #[test]
777    fn test_reduction_memo_len() {
778        let mut memo = ReductionMemo::new();
779        let e = mk_nat_lit(0);
780        memo.insert(ReductionStrategy::WHNF, &e, e.clone());
781        assert_eq!(memo.len(), 1);
782    }
783}
784#[cfg(test)]
785mod tests_padding_infra {
786    use super::*;
787    #[test]
788    fn test_stat_summary() {
789        let mut ss = StatSummary::new();
790        ss.record(10.0);
791        ss.record(20.0);
792        ss.record(30.0);
793        assert_eq!(ss.count(), 3);
794        assert!((ss.mean().expect("mean should succeed") - 20.0).abs() < 1e-9);
795        assert_eq!(ss.min().expect("min should succeed") as i64, 10);
796        assert_eq!(ss.max().expect("max should succeed") as i64, 30);
797    }
798    #[test]
799    fn test_transform_stat() {
800        let mut ts = TransformStat::new();
801        ts.record_before(100.0);
802        ts.record_after(80.0);
803        let ratio = ts.mean_ratio().expect("ratio should be present");
804        assert!((ratio - 0.8).abs() < 1e-9);
805    }
806    #[test]
807    fn test_small_map() {
808        let mut m: SmallMap<u32, &str> = SmallMap::new();
809        m.insert(3, "three");
810        m.insert(1, "one");
811        m.insert(2, "two");
812        assert_eq!(m.get(&2), Some(&"two"));
813        assert_eq!(m.len(), 3);
814        let keys = m.keys();
815        assert_eq!(*keys[0], 1);
816        assert_eq!(*keys[2], 3);
817    }
818    #[test]
819    fn test_label_set() {
820        let mut ls = LabelSet::new();
821        ls.add("foo");
822        ls.add("bar");
823        ls.add("foo");
824        assert_eq!(ls.count(), 2);
825        assert!(ls.has("bar"));
826        assert!(!ls.has("baz"));
827    }
828    #[test]
829    fn test_config_node() {
830        let mut root = ConfigNode::section("root");
831        let child = ConfigNode::leaf("key", "value");
832        root.add_child(child);
833        assert_eq!(root.num_children(), 1);
834    }
835    #[test]
836    fn test_versioned_record() {
837        let mut vr = VersionedRecord::new(0u32);
838        vr.update(1);
839        vr.update(2);
840        assert_eq!(*vr.current(), 2);
841        assert_eq!(vr.version(), 2);
842        assert!(vr.has_history());
843        assert_eq!(*vr.at_version(0).expect("value should be present"), 0);
844    }
845    #[test]
846    fn test_simple_dag() {
847        let mut dag = SimpleDag::new(4);
848        dag.add_edge(0, 1);
849        dag.add_edge(1, 2);
850        dag.add_edge(2, 3);
851        assert!(dag.can_reach(0, 3));
852        assert!(!dag.can_reach(3, 0));
853        let order = dag.topological_sort().expect("order should be present");
854        assert_eq!(order, vec![0, 1, 2, 3]);
855    }
856    #[test]
857    fn test_focus_stack() {
858        let mut fs: FocusStack<&str> = FocusStack::new();
859        fs.focus("a");
860        fs.focus("b");
861        assert_eq!(fs.current(), Some(&"b"));
862        assert_eq!(fs.depth(), 2);
863        fs.blur();
864        assert_eq!(fs.current(), Some(&"a"));
865    }
866}
867#[cfg(test)]
868mod tests_extra_iterators {
869    use super::*;
870    #[test]
871    fn test_window_iterator() {
872        let data = vec![1u32, 2, 3, 4, 5];
873        let windows: Vec<_> = WindowIterator::new(&data, 3).collect();
874        assert_eq!(windows.len(), 3);
875        assert_eq!(windows[0], &[1, 2, 3]);
876        assert_eq!(windows[2], &[3, 4, 5]);
877    }
878    #[test]
879    fn test_non_empty_vec() {
880        let mut nev = NonEmptyVec::singleton(10u32);
881        nev.push(20);
882        nev.push(30);
883        assert_eq!(nev.len(), 3);
884        assert_eq!(*nev.first(), 10);
885        assert_eq!(*nev.last(), 30);
886    }
887}
888#[cfg(test)]
889mod tests_padding2 {
890    use super::*;
891    #[test]
892    fn test_sliding_sum() {
893        let mut ss = SlidingSum::new(3);
894        ss.push(1.0);
895        ss.push(2.0);
896        ss.push(3.0);
897        assert!((ss.sum() - 6.0).abs() < 1e-9);
898        ss.push(4.0);
899        assert!((ss.sum() - 9.0).abs() < 1e-9);
900        assert_eq!(ss.count(), 3);
901    }
902    #[test]
903    fn test_path_buf() {
904        let mut pb = PathBuf::new();
905        pb.push("src");
906        pb.push("main");
907        assert_eq!(pb.as_str(), "src/main");
908        assert_eq!(pb.depth(), 2);
909        pb.pop();
910        assert_eq!(pb.as_str(), "src");
911    }
912    #[test]
913    fn test_string_pool() {
914        let mut pool = StringPool::new();
915        let s = pool.take();
916        assert!(s.is_empty());
917        pool.give("hello".to_string());
918        let s2 = pool.take();
919        assert!(s2.is_empty());
920        assert_eq!(pool.free_count(), 0);
921    }
922    #[test]
923    fn test_transitive_closure() {
924        let mut tc = TransitiveClosure::new(4);
925        tc.add_edge(0, 1);
926        tc.add_edge(1, 2);
927        tc.add_edge(2, 3);
928        assert!(tc.can_reach(0, 3));
929        assert!(!tc.can_reach(3, 0));
930        let r = tc.reachable_from(0);
931        assert_eq!(r.len(), 4);
932    }
933    #[test]
934    fn test_token_bucket() {
935        let mut tb = TokenBucket::new(100, 0);
936        assert_eq!(tb.available(), 100);
937        assert!(tb.try_consume(50));
938        assert_eq!(tb.available(), 50);
939        assert!(!tb.try_consume(60));
940        assert_eq!(tb.capacity(), 100);
941    }
942    #[test]
943    fn test_rewrite_rule_set() {
944        let mut rrs = RewriteRuleSet::new();
945        rrs.add(RewriteRule::unconditional(
946            "beta",
947            "App(Lam(x, b), v)",
948            "b[x:=v]",
949        ));
950        rrs.add(RewriteRule::conditional("comm", "a + b", "b + a"));
951        assert_eq!(rrs.len(), 2);
952        assert_eq!(rrs.unconditional_rules().len(), 1);
953        assert_eq!(rrs.conditional_rules().len(), 1);
954        assert!(rrs.get("beta").is_some());
955        let disp = rrs
956            .get("beta")
957            .expect("element at \'beta\' should exist")
958            .display();
959        assert!(disp.contains("→"));
960    }
961}
962#[cfg(test)]
963mod tests_padding3 {
964    use super::*;
965    #[test]
966    fn test_decision_node() {
967        let tree = DecisionNode::Branch {
968            key: "x".into(),
969            val: "1".into(),
970            yes_branch: Box::new(DecisionNode::Leaf("yes".into())),
971            no_branch: Box::new(DecisionNode::Leaf("no".into())),
972        };
973        let mut ctx = std::collections::HashMap::new();
974        ctx.insert("x".into(), "1".into());
975        assert_eq!(tree.evaluate(&ctx), "yes");
976        ctx.insert("x".into(), "2".into());
977        assert_eq!(tree.evaluate(&ctx), "no");
978        assert_eq!(tree.depth(), 1);
979    }
980    #[test]
981    fn test_flat_substitution() {
982        let mut sub = FlatSubstitution::new();
983        sub.add("foo", "bar");
984        sub.add("baz", "qux");
985        assert_eq!(sub.apply("foo and baz"), "bar and qux");
986        assert_eq!(sub.len(), 2);
987    }
988    #[test]
989    fn test_stopwatch() {
990        let mut sw = Stopwatch::start();
991        sw.split();
992        sw.split();
993        assert_eq!(sw.num_splits(), 2);
994        assert!(sw.elapsed_ms() >= 0.0);
995        for &s in sw.splits() {
996            assert!(s >= 0.0);
997        }
998    }
999    #[test]
1000    fn test_either2() {
1001        let e: Either2<i32, &str> = Either2::First(42);
1002        assert!(e.is_first());
1003        let mapped = e.map_first(|x| x * 2);
1004        assert_eq!(mapped.first(), Some(84));
1005        let e2: Either2<i32, &str> = Either2::Second("hello");
1006        assert!(e2.is_second());
1007        assert_eq!(e2.second(), Some("hello"));
1008    }
1009    #[test]
1010    fn test_write_once() {
1011        let wo: WriteOnce<u32> = WriteOnce::new();
1012        assert!(!wo.is_written());
1013        assert!(wo.write(42));
1014        assert!(!wo.write(99));
1015        assert_eq!(wo.read(), Some(42));
1016    }
1017    #[test]
1018    fn test_sparse_vec() {
1019        let mut sv: SparseVec<i32> = SparseVec::new(100);
1020        sv.set(5, 10);
1021        sv.set(50, 20);
1022        assert_eq!(*sv.get(5), 10);
1023        assert_eq!(*sv.get(50), 20);
1024        assert_eq!(*sv.get(0), 0);
1025        assert_eq!(sv.nnz(), 2);
1026        sv.set(5, 0);
1027        assert_eq!(sv.nnz(), 1);
1028    }
1029    #[test]
1030    fn test_stack_calc() {
1031        let mut calc = StackCalc::new();
1032        calc.push(3);
1033        calc.push(4);
1034        assert_eq!(calc.add(), Some(7));
1035        assert_eq!(calc.peek(), Some(7));
1036        calc.push(2);
1037        assert_eq!(calc.mul(), Some(14));
1038        assert_eq!(calc.peek(), Some(14));
1039    }
1040}
1041#[cfg(test)]
1042mod tests_final_padding {
1043    use super::*;
1044    #[test]
1045    fn test_min_heap() {
1046        let mut h = MinHeap::new();
1047        h.push(5u32);
1048        h.push(1u32);
1049        h.push(3u32);
1050        assert_eq!(h.peek(), Some(&1));
1051        assert_eq!(h.pop(), Some(1));
1052        assert_eq!(h.pop(), Some(3));
1053        assert_eq!(h.pop(), Some(5));
1054        assert!(h.is_empty());
1055    }
1056    #[test]
1057    fn test_prefix_counter() {
1058        let mut pc = PrefixCounter::new();
1059        pc.record("hello");
1060        pc.record("help");
1061        pc.record("world");
1062        assert_eq!(pc.count_with_prefix("hel"), 2);
1063        assert_eq!(pc.count_with_prefix("wor"), 1);
1064        assert_eq!(pc.count_with_prefix("xyz"), 0);
1065    }
1066    #[test]
1067    fn test_fixture() {
1068        let mut f = Fixture::new();
1069        f.set("key1", "val1");
1070        f.set("key2", "val2");
1071        assert_eq!(f.get("key1"), Some("val1"));
1072        assert_eq!(f.get("key3"), None);
1073        assert_eq!(f.len(), 2);
1074    }
1075}