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