Skip to main content

oxilean_std/combinatory_logic/
functions.rs

1//! Functions for SKI combinatory logic: reduction, bracket abstraction, Church numerals.
2
3use super::types::{BracketMethod, CombRule, CombTerm, LambdaToComb, NormalForm, ReductionStep};
4
5// ── Reduction ─────────────────────────────────────────────────────────────────
6
7/// Perform one step of reduction on a combinator term (outermost-leftmost strategy).
8///
9/// Returns `Some((reduced_term, rule))` if a redex was found, or `None` if the
10/// term is already in normal form.
11pub fn reduce_step(t: &CombTerm) -> Option<(CombTerm, CombRule)> {
12    match t {
13        // I x → x
14        CombTerm::App(f, x) => {
15            if let CombTerm::I = f.as_ref() {
16                return Some((*x.clone(), CombRule::IRule));
17            }
18            // K x y → x
19            if let CombTerm::App(k, x_inner) = f.as_ref() {
20                if let CombTerm::K = k.as_ref() {
21                    return Some((*x_inner.clone(), CombRule::KRule));
22                }
23            }
24            // S x y z → x z (y z)
25            if let CombTerm::App(sx, y) = f.as_ref() {
26                if let CombTerm::App(s, x_inner) = sx.as_ref() {
27                    if let CombTerm::S = s.as_ref() {
28                        let xz = CombTerm::App(x_inner.clone(), x.clone());
29                        let yz = CombTerm::App(y.clone(), x.clone());
30                        let result = CombTerm::App(Box::new(xz), Box::new(yz));
31                        return Some((result, CombRule::SRule));
32                    }
33                }
34            }
35            // Try reducing left side first (outermost-leftmost)
36            if let Some((f2, rule)) = reduce_step(f) {
37                return Some((CombTerm::App(Box::new(f2), x.clone()), rule));
38            }
39            // Then try reducing right side
40            if let Some((x2, rule)) = reduce_step(x) {
41                return Some((CombTerm::App(f.clone(), Box::new(x2)), rule));
42            }
43            None
44        }
45        CombTerm::S | CombTerm::K | CombTerm::I | CombTerm::Var(_) | CombTerm::Const(_) => None,
46    }
47}
48
49/// Reduce a combinator term to normal form, up to `max_steps` reduction steps.
50///
51/// Returns `Ok(NormalForm(t))` if the term reached normal form within the step
52/// budget, or `Err(message)` if the budget was exhausted.
53pub fn reduce_to_normal(mut t: CombTerm, max_steps: usize) -> Result<NormalForm, String> {
54    for step in 0..max_steps {
55        match reduce_step(&t) {
56            None => return Ok(NormalForm(t)),
57            Some((next, _rule)) => {
58                t = next;
59                let _ = step; // used only for loop count
60            }
61        }
62    }
63    Err(format!(
64        "Term did not reach normal form within {} steps",
65        max_steps
66    ))
67}
68
69/// Collect the full reduction sequence from a term to its normal form.
70///
71/// Returns the steps taken and the final normal form (or an error if the budget
72/// is exhausted).
73pub fn reduce_with_trace(
74    mut t: CombTerm,
75    max_steps: usize,
76) -> Result<(Vec<ReductionStep>, NormalForm), String> {
77    let mut steps = Vec::new();
78    for _ in 0..max_steps {
79        match reduce_step(&t) {
80            None => return Ok((steps, NormalForm(t))),
81            Some((next, rule)) => {
82                steps.push(ReductionStep {
83                    from: t.clone(),
84                    to: next.clone(),
85                    rule_applied: rule,
86                });
87                t = next;
88            }
89        }
90    }
91    Err(format!(
92        "Term did not reach normal form within {} steps",
93        max_steps
94    ))
95}
96
97/// Check whether a combinator term is already in normal form (no applicable rules).
98pub fn is_normal_form(t: &CombTerm) -> bool {
99    reduce_step(t).is_none()
100}
101
102/// Check whether two combinator terms are beta-equal: reduce both to normal form
103/// and compare structurally. Returns `false` if either fails to normalize.
104pub fn beta_equal(t1: &CombTerm, t2: &CombTerm, max_steps: usize) -> bool {
105    let n1 = reduce_to_normal(t1.clone(), max_steps);
106    let n2 = reduce_to_normal(t2.clone(), max_steps);
107    match (n1, n2) {
108        (Ok(NormalForm(a)), Ok(NormalForm(b))) => a == b,
109        _ => false,
110    }
111}
112
113// ── Bracket abstraction ───────────────────────────────────────────────────────
114
115/// Naive bracket abstraction \[x\]M.
116///
117/// Implements the three standard rules:
118/// 1. \[x\]x     = I
119/// 2. \[x\]M     = K M   (when x ∉ FV(M))
120/// 3. \[x\](M N) = S (\[x\]M) (\[x\]N)
121pub fn lambda_to_ski_naive(var: &str, body: &CombTerm) -> CombTerm {
122    match body {
123        // Rule 1: [x]x = I
124        CombTerm::Var(v) if v == var => CombTerm::I,
125        // Rule 2: [x]M = K M when x ∉ FV(M)
126        t if !free_vars(t).contains(&var.to_string()) => {
127            CombTerm::App(Box::new(CombTerm::K), Box::new(t.clone()))
128        }
129        // Rule 3: [x](M N) = S ([x]M) ([x]N)
130        CombTerm::App(m, n) => {
131            let sm = lambda_to_ski_naive(var, m);
132            let sn = lambda_to_ski_naive(var, n);
133            CombTerm::App(
134                Box::new(CombTerm::App(Box::new(CombTerm::S), Box::new(sm))),
135                Box::new(sn),
136            )
137        }
138        // Rule 2 catch-all (S, K, I, Const that don't contain var)
139        t => CombTerm::App(Box::new(CombTerm::K), Box::new(t.clone())),
140    }
141}
142
143/// Optimized bracket abstraction \[x\]M with η-reduction.
144///
145/// Extends the naive algorithm with:
146/// 4. \[x\](M x) = M   (when x ∉ FV(M)) — η-reduction
147pub fn lambda_to_ski_optimized(var: &str, body: &CombTerm) -> CombTerm {
148    match body {
149        // Rule 1
150        CombTerm::Var(v) if v == var => CombTerm::I,
151        // Rule 4: η-reduction [x](M x) = M when x ∉ FV(M)
152        CombTerm::App(m, n) => {
153            if let CombTerm::Var(v) = n.as_ref() {
154                if v == var && !free_vars(m).contains(&var.to_string()) {
155                    return *m.clone();
156                }
157            }
158            // Rule 2
159            if !free_vars(body).contains(&var.to_string()) {
160                return CombTerm::App(Box::new(CombTerm::K), Box::new(body.clone()));
161            }
162            // Rule 3
163            let sm = lambda_to_ski_optimized(var, m);
164            let sn = lambda_to_ski_optimized(var, n);
165            CombTerm::App(
166                Box::new(CombTerm::App(Box::new(CombTerm::S), Box::new(sm))),
167                Box::new(sn),
168            )
169        }
170        // Rule 2
171        t if !free_vars(t).contains(&var.to_string()) => {
172            CombTerm::App(Box::new(CombTerm::K), Box::new(t.clone()))
173        }
174        t => CombTerm::App(Box::new(CombTerm::K), Box::new(t.clone())),
175    }
176}
177
178/// Apply bracket abstraction via the method stored in a `LambdaToComb` converter.
179pub fn convert_lambda(conv: &LambdaToComb, var: &str, body: &CombTerm) -> CombTerm {
180    match conv.bracket_abstraction {
181        BracketMethod::Naive => lambda_to_ski_naive(var, body),
182        BracketMethod::Optimized => lambda_to_ski_optimized(var, body),
183        BracketMethod::TurnerOptimized => lambda_to_ski_turner(var, body),
184    }
185}
186
187/// Turner's optimized bracket abstraction.
188///
189/// Uses auxiliary combinator patterns B, C, S' (simulated via S/K/I) to reduce
190/// term size when only one side of an application contains the variable.
191pub fn lambda_to_ski_turner(var: &str, body: &CombTerm) -> CombTerm {
192    match body {
193        CombTerm::Var(v) if v == var => CombTerm::I,
194        t if !free_vars(t).contains(&var.to_string()) => {
195            CombTerm::App(Box::new(CombTerm::K), Box::new(t.clone()))
196        }
197        CombTerm::App(m, n) => {
198            let m_has_var = free_vars(m).contains(&var.to_string());
199            let n_has_var = free_vars(n).contains(&var.to_string());
200            match (m_has_var, n_has_var) {
201                // η case: [x](M x) = M
202                (false, true) => {
203                    if let CombTerm::Var(v) = n.as_ref() {
204                        if v == var {
205                            return *m.clone();
206                        }
207                    }
208                    // B combinator pattern: [x](M (N x)) = B M ([x]N) where B = S(KS)K
209                    // Simulate B M N' = S (K M) N'
210                    let n2 = lambda_to_ski_turner(var, n);
211                    CombTerm::App(
212                        Box::new(CombTerm::App(
213                            Box::new(CombTerm::App(
214                                Box::new(CombTerm::S),
215                                Box::new(CombTerm::App(Box::new(CombTerm::K), m.clone())),
216                            )),
217                            Box::new(CombTerm::I),
218                        )),
219                        Box::new(n2),
220                    )
221                }
222                (true, false) => {
223                    // C combinator pattern: [x](M x N) = C ([x]M) N where C = S(BS)K
224                    // Simulate C M' N = S M' (K N)
225                    let m2 = lambda_to_ski_turner(var, m);
226                    CombTerm::App(
227                        Box::new(CombTerm::App(Box::new(CombTerm::S), Box::new(m2))),
228                        Box::new(CombTerm::App(Box::new(CombTerm::K), n.clone())),
229                    )
230                }
231                _ => {
232                    // Full S rule
233                    let sm = lambda_to_ski_turner(var, m);
234                    let sn = lambda_to_ski_turner(var, n);
235                    CombTerm::App(
236                        Box::new(CombTerm::App(Box::new(CombTerm::S), Box::new(sm))),
237                        Box::new(sn),
238                    )
239                }
240            }
241        }
242        t => CombTerm::App(Box::new(CombTerm::K), Box::new(t.clone())),
243    }
244}
245
246// ── Church numerals ───────────────────────────────────────────────────────────
247
248/// Build the Church numeral for `n` as a SKI combinator term.
249///
250/// Church numeral `n` = λf.λx. f^n x.
251/// In SKI: 0 = K I, 1 = I, 2 = S (S (K S) K) I, etc.
252/// We use the standard iterative construction.
253pub fn church_numeral_comb(n: u64) -> CombTerm {
254    // Church n = λf.λx. f^n x
255    // We build it via bracket abstraction applied to the body
256    // f^0 x = x,  f^(n+1) x = f (f^n x)
257    // Then [f]([x](f^n x))
258
259    // Build f^n x as an untyped term using Var("f") and Var("x")
260    let mut body = CombTerm::Var("x".to_string());
261    for _ in 0..n {
262        body = CombTerm::App(Box::new(CombTerm::Var("f".to_string())), Box::new(body));
263    }
264    // Abstract over x, then over f
265    let abs_x = lambda_to_ski_optimized("x", &body);
266    lambda_to_ski_optimized("f", &abs_x)
267}
268
269// ── Structural utilities ──────────────────────────────────────────────────────
270
271/// Build an n-ary application: f applied to each argument in turn.
272///
273/// `comb_app_n(f, \[a, b, c\])` = `((f a) b) c`
274pub fn comb_app_n(f: CombTerm, args: Vec<CombTerm>) -> CombTerm {
275    args.into_iter()
276        .fold(f, |acc, arg| CombTerm::App(Box::new(acc), Box::new(arg)))
277}
278
279/// Count the number of nodes in a combinator term tree.
280pub fn size(t: &CombTerm) -> usize {
281    match t {
282        CombTerm::App(f, x) => 1 + size(f) + size(x),
283        CombTerm::S | CombTerm::K | CombTerm::I | CombTerm::Var(_) | CombTerm::Const(_) => 1,
284    }
285}
286
287/// Collect all free variables in a combinator term (no duplicates, in order of first appearance).
288pub fn free_vars(t: &CombTerm) -> Vec<String> {
289    let mut seen = std::collections::HashSet::new();
290    let mut result = Vec::new();
291    collect_free_vars(t, &mut seen, &mut result);
292    result
293}
294
295fn collect_free_vars(
296    t: &CombTerm,
297    seen: &mut std::collections::HashSet<String>,
298    out: &mut Vec<String>,
299) {
300    match t {
301        CombTerm::Var(v) => {
302            if seen.insert(v.clone()) {
303                out.push(v.clone());
304            }
305        }
306        CombTerm::App(f, x) => {
307            collect_free_vars(f, seen, out);
308            collect_free_vars(x, seen, out);
309        }
310        CombTerm::S | CombTerm::K | CombTerm::I | CombTerm::Const(_) => {}
311    }
312}
313
314/// Substitute `val` for all free occurrences of `var` in `t`.
315pub fn substitute(t: &CombTerm, var: &str, val: &CombTerm) -> CombTerm {
316    match t {
317        CombTerm::Var(v) if v == var => val.clone(),
318        CombTerm::Var(_) | CombTerm::S | CombTerm::K | CombTerm::I | CombTerm::Const(_) => {
319            t.clone()
320        }
321        CombTerm::App(f, x) => CombTerm::App(
322            Box::new(substitute(f, var, val)),
323            Box::new(substitute(x, var, val)),
324        ),
325    }
326}
327
328// ── Tests ─────────────────────────────────────────────────────────────────────
329
330#[cfg(test)]
331mod tests {
332    use super::super::types::*;
333    use super::*;
334
335    fn app(f: CombTerm, x: CombTerm) -> CombTerm {
336        CombTerm::App(Box::new(f), Box::new(x))
337    }
338
339    fn var(s: &str) -> CombTerm {
340        CombTerm::Var(s.to_string())
341    }
342
343    // ── is_normal_form ────────────────────────────────────────────────────────
344
345    #[test]
346    fn test_normal_form_s() {
347        assert!(is_normal_form(&CombTerm::S));
348    }
349
350    #[test]
351    fn test_normal_form_k() {
352        assert!(is_normal_form(&CombTerm::K));
353    }
354
355    #[test]
356    fn test_normal_form_i() {
357        assert!(is_normal_form(&CombTerm::I));
358    }
359
360    #[test]
361    fn test_not_normal_form_i_x() {
362        let t = app(CombTerm::I, var("x"));
363        assert!(!is_normal_form(&t));
364    }
365
366    #[test]
367    fn test_not_normal_form_k_x_y() {
368        let t = app(app(CombTerm::K, var("x")), var("y"));
369        assert!(!is_normal_form(&t));
370    }
371
372    // ── reduce_step ───────────────────────────────────────────────────────────
373
374    #[test]
375    fn test_reduce_step_i_rule() {
376        let t = app(CombTerm::I, var("x"));
377        let result = reduce_step(&t);
378        match result {
379            Some((CombTerm::Var(v), CombRule::IRule)) => assert_eq!(v, "x"),
380            other => panic!("expected I-rule reduction to Var(x), got {:?}", other),
381        }
382    }
383
384    #[test]
385    fn test_reduce_step_k_rule() {
386        let t = app(app(CombTerm::K, var("x")), var("y"));
387        let result = reduce_step(&t);
388        match result {
389            Some((CombTerm::Var(v), CombRule::KRule)) => assert_eq!(v, "x"),
390            other => panic!("expected K-rule reduction to Var(x), got {:?}", other),
391        }
392    }
393
394    #[test]
395    fn test_reduce_step_s_rule() {
396        // S x y z → x z (y z)
397        let t = app(app(app(CombTerm::S, var("x")), var("y")), var("z"));
398        let (result, rule) = reduce_step(&t).expect("should reduce");
399        assert_eq!(rule, CombRule::SRule);
400        // result should be (x z) (y z)
401        let expected = app(app(var("x"), var("z")), app(var("y"), var("z")));
402        assert_eq!(result, expected);
403    }
404
405    #[test]
406    fn test_reduce_step_none_on_normal() {
407        assert!(reduce_step(&CombTerm::S).is_none());
408    }
409
410    // ── reduce_to_normal ──────────────────────────────────────────────────────
411
412    #[test]
413    fn test_reduce_to_normal_i_x() {
414        let t = app(CombTerm::I, var("x"));
415        let nf = reduce_to_normal(t, 100).expect("should normalize");
416        assert_eq!(nf.into_inner(), var("x"));
417    }
418
419    #[test]
420    fn test_reduce_to_normal_k_x_y() {
421        let t = app(app(CombTerm::K, var("x")), var("y"));
422        let nf = reduce_to_normal(t, 100).expect("should normalize");
423        assert_eq!(nf.into_inner(), var("x"));
424    }
425
426    #[test]
427    fn test_reduce_to_normal_already_normal() {
428        let nf = reduce_to_normal(CombTerm::S, 100).expect("should normalize");
429        assert_eq!(nf.into_inner(), CombTerm::S);
430    }
431
432    #[test]
433    fn test_reduce_to_normal_step_limit() {
434        // Build a term that needs many steps: I (I (I (I x)))
435        let t = app(CombTerm::I, app(CombTerm::I, app(CombTerm::I, var("x"))));
436        // Should succeed with enough steps
437        let nf = reduce_to_normal(t.clone(), 20).expect("should normalize");
438        assert_eq!(nf.into_inner(), var("x"));
439        // With only 1 step, the outermost I reduces but leaves I (I x)
440        let partial = reduce_to_normal(t, 1);
441        // 1 step is enough to reduce I (I (I x)) → I (I x), then 1 more is needed
442        // so it should fail or succeed depending on depth-first vs eager
443        // We just check it's not panic
444        let _ = partial;
445    }
446
447    // ── beta_equal ────────────────────────────────────────────────────────────
448
449    #[test]
450    fn test_beta_equal_same_normal() {
451        assert!(beta_equal(&CombTerm::S, &CombTerm::S, 100));
452    }
453
454    #[test]
455    fn test_beta_equal_different() {
456        assert!(!beta_equal(&CombTerm::S, &CombTerm::K, 100));
457    }
458
459    #[test]
460    fn test_beta_equal_reduces_to_same() {
461        // I x and K x (I y) both reduce to x
462        let t1 = app(CombTerm::I, var("x"));
463        let t2 = app(app(CombTerm::K, var("x")), app(CombTerm::I, var("y")));
464        assert!(beta_equal(&t1, &t2, 100));
465    }
466
467    // ── lambda_to_ski_naive ───────────────────────────────────────────────────
468
469    #[test]
470    fn test_naive_identity_lambda() {
471        // [x]x = I
472        let result = lambda_to_ski_naive("x", &var("x"));
473        assert_eq!(result, CombTerm::I);
474    }
475
476    #[test]
477    fn test_naive_constant_lambda() {
478        // [x]y = K y
479        let result = lambda_to_ski_naive("x", &var("y"));
480        let expected = app(CombTerm::K, var("y"));
481        assert_eq!(result, expected);
482    }
483
484    #[test]
485    fn test_naive_k_combinator() {
486        // [x]([y]x) = K
487        // [y]x = K x (since y ∉ FV(x))
488        let inner = lambda_to_ski_naive("y", &var("x")); // = K x
489        let outer = lambda_to_ski_naive("x", &inner); // = [x](K x)
490                                                      // [x](K x) = S ([x]K) ([x]x) = S (K K) I
491        let expected = app(app(CombTerm::S, app(CombTerm::K, CombTerm::K)), CombTerm::I);
492        assert_eq!(outer, expected);
493    }
494
495    // ── lambda_to_ski_optimized ───────────────────────────────────────────────
496
497    #[test]
498    fn test_optimized_identity() {
499        let result = lambda_to_ski_optimized("x", &var("x"));
500        assert_eq!(result, CombTerm::I);
501    }
502
503    #[test]
504    fn test_optimized_eta_reduction() {
505        // [x](M x) = M when x ∉ FV(M)
506        let body = app(var("f"), var("x"));
507        let result = lambda_to_ski_optimized("x", &body);
508        // η-reduces to f
509        assert_eq!(result, var("f"));
510    }
511
512    #[test]
513    fn test_optimized_constant() {
514        let result = lambda_to_ski_optimized("x", &CombTerm::K);
515        let expected = app(CombTerm::K, CombTerm::K);
516        assert_eq!(result, expected);
517    }
518
519    // ── church_numeral_comb ───────────────────────────────────────────────────
520
521    #[test]
522    fn test_church_zero_reduces_to_ki() {
523        // Church 0 = K I (applies f zero times to x, yielding x — the identity on x)
524        let zero = church_numeral_comb(0);
525        // Reduce: church_0 f x → x
526        let applied_f = app(zero.clone(), var("f"));
527        let applied_fx = app(applied_f, var("x"));
528        let nf = reduce_to_normal(applied_fx, 200).expect("should normalize");
529        assert_eq!(nf.into_inner(), var("x"));
530    }
531
532    #[test]
533    fn test_church_one_applies_once() {
534        // Church 1 f x → f x
535        let one = church_numeral_comb(1);
536        let applied = app(app(one, var("f")), var("x"));
537        let nf = reduce_to_normal(applied, 200).expect("should normalize");
538        assert_eq!(nf.into_inner(), app(var("f"), var("x")));
539    }
540
541    #[test]
542    fn test_church_two_applies_twice() {
543        // Church 2 f x → f (f x)
544        let two = church_numeral_comb(2);
545        let applied = app(app(two, var("f")), var("x"));
546        let nf = reduce_to_normal(applied, 1000).expect("should normalize");
547        assert_eq!(nf.into_inner(), app(var("f"), app(var("f"), var("x"))));
548    }
549
550    // ── comb_app_n ────────────────────────────────────────────────────────────
551
552    #[test]
553    fn test_comb_app_n_empty() {
554        let result = comb_app_n(var("f"), vec![]);
555        assert_eq!(result, var("f"));
556    }
557
558    #[test]
559    fn test_comb_app_n_one() {
560        let result = comb_app_n(var("f"), vec![var("x")]);
561        assert_eq!(result, app(var("f"), var("x")));
562    }
563
564    #[test]
565    fn test_comb_app_n_three() {
566        let result = comb_app_n(var("f"), vec![var("x"), var("y"), var("z")]);
567        let expected = app(app(app(var("f"), var("x")), var("y")), var("z"));
568        assert_eq!(result, expected);
569    }
570
571    // ── size ──────────────────────────────────────────────────────────────────
572
573    #[test]
574    fn test_size_atomic() {
575        assert_eq!(size(&CombTerm::S), 1);
576        assert_eq!(size(&CombTerm::K), 1);
577        assert_eq!(size(&var("x")), 1);
578    }
579
580    #[test]
581    fn test_size_app() {
582        let t = app(CombTerm::S, CombTerm::K);
583        assert_eq!(size(&t), 3); // App node + S + K
584    }
585
586    #[test]
587    fn test_size_nested() {
588        let t = app(app(CombTerm::S, CombTerm::K), CombTerm::I);
589        assert_eq!(size(&t), 5);
590    }
591
592    // ── free_vars ─────────────────────────────────────────────────────────────
593
594    #[test]
595    fn test_free_vars_combinator() {
596        assert!(free_vars(&CombTerm::S).is_empty());
597        assert!(free_vars(&CombTerm::K).is_empty());
598        assert!(free_vars(&CombTerm::I).is_empty());
599    }
600
601    #[test]
602    fn test_free_vars_var() {
603        assert_eq!(free_vars(&var("x")), vec!["x".to_string()]);
604    }
605
606    #[test]
607    fn test_free_vars_app() {
608        let t = app(var("x"), var("y"));
609        assert_eq!(free_vars(&t), vec!["x".to_string(), "y".to_string()]);
610    }
611
612    #[test]
613    fn test_free_vars_deduplication() {
614        let t = app(var("x"), var("x"));
615        assert_eq!(free_vars(&t), vec!["x".to_string()]);
616    }
617
618    // ── substitute ────────────────────────────────────────────────────────────
619
620    #[test]
621    fn test_substitute_var() {
622        let result = substitute(&var("x"), "x", &CombTerm::I);
623        assert_eq!(result, CombTerm::I);
624    }
625
626    #[test]
627    fn test_substitute_different_var() {
628        let result = substitute(&var("y"), "x", &CombTerm::I);
629        assert_eq!(result, var("y"));
630    }
631
632    #[test]
633    fn test_substitute_in_app() {
634        let t = app(var("x"), var("y"));
635        let result = substitute(&t, "x", &CombTerm::K);
636        assert_eq!(result, app(CombTerm::K, var("y")));
637    }
638
639    #[test]
640    fn test_substitute_combinator_unchanged() {
641        let result = substitute(&CombTerm::S, "x", &CombTerm::K);
642        assert_eq!(result, CombTerm::S);
643    }
644}