Skip to main content

cml/
schedule.rs

1//! Base-k carry reduction schedule algorithm.
2
3/// Number of k-ary merges after appending an item at 0-based index `n`.
4///
5/// Controls the frontier stack reduction for a configured log arity `k >= 2`.
6///
7/// # Panics
8///
9/// Panics if `k < 2` as the arity must be at least 2.
10#[must_use]
11pub fn reduction_count(n: u64, k: u64) -> u32 {
12    assert!(k >= 2, "log arity k must be >= 2");
13    let mut count = 0;
14    let mut m = (n as u128) + 1; // 1-based index
15    let k_u128 = k as u128;
16    while m % k_u128 == 0 {
17        count += 1;
18        m /= k_u128;
19    }
20    count
21}
22
23#[cfg(test)]
24mod tests {
25    use super::*;
26
27    #[test]
28    fn test_reduction_count_k2() {
29        // Equivalent to EML's count_trailing_ones
30        let expected = vec![
31            0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0,
32            1, 0, 5,
33        ];
34        for (n, &exp) in expected.iter().enumerate() {
35            assert_eq!(reduction_count(n as u64, 2), exp, "failed for k=2, n={}", n);
36        }
37    }
38
39    #[test]
40    fn test_reduction_count_k3() {
41        let expected = vec![
42            0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 3, 0, 0,
43            1, 0, 0,
44        ];
45        for (n, &exp) in expected.iter().enumerate() {
46            assert_eq!(reduction_count(n as u64, 3), exp, "failed for k=3, n={}", n);
47        }
48    }
49
50    #[test]
51    fn test_reduction_count_k4() {
52        let expected = vec![
53            0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0,
54            0, 0, 2,
55        ];
56        for (n, &exp) in expected.iter().enumerate() {
57            assert_eq!(reduction_count(n as u64, 4), exp, "failed for k=4, n={}", n);
58        }
59    }
60
61    #[test]
62    #[should_panic(expected = "log arity k must be >= 2")]
63    fn test_reduction_count_invalid_k() {
64        let _ = reduction_count(0, 1);
65    }
66
67    /// Rust twin of every Lean `#guard` in `proofs/lean/EMLProof/Kary.lean`
68    /// (`section SanityChecks`).
69    ///
70    /// These are the *same vectors*, transcribed value-for-value, so that
71    /// definitional drift between the Lean topology model and the Rust source
72    /// breaks a build on **either** side rather than going silent. Each Lean
73    /// `#guard` is reproduced below in source order; if one is added there, add
74    /// its twin here. Argument order differs by design: Lean takes the arity
75    /// first (`frontierForSizeT k n`, `reductionCount k n`,
76    /// `inclusionSkeleton k treeSize index`), the Rust API takes it last
77    /// (`frontier_for_size(n, k)`, `reduction_count(n, k)`) or first
78    /// (`inclusion_skeleton(k, tree_size, index)`).
79    ///
80    /// This guard couples both crates: `reduction_count` lives here in CML
81    /// (the frontier carry); `frontier_for_size` lives in the `spine` core; the
82    /// concrete MMR `mountain_skeleton` lives in CML (the log owns its topology
83    /// now). It stays where it can reach all three.
84    ///
85    /// The `inclusionSkeleton` Lean guards move to N54's mountain re-spec; here
86    /// the structural `frontier_for_size`/`reduction_count` parity is pinned, plus
87    /// the mountain skeleton's leaf → peak → bag shape on small cases.
88    #[test]
89    fn lean_guard_parity() {
90        use spine::{SkeletonStep, frontier_for_size};
91
92        use crate::mountain::mountain_skeleton;
93
94        let step = |position: usize, sibling_count: usize| SkeletonStep {
95            position,
96            sibling_count,
97        };
98
99        // frontierForSizeT k n  ⟷  frontier_for_size(n, k)
100        assert_eq!(frontier_for_size(5, 2), vec![(0u64, 2u32), (4, 0)]);
101        assert_eq!(frontier_for_size(5, 3), vec![(0, 1), (3, 0), (4, 0)]);
102        assert_eq!(frontier_for_size(0, 2), Vec::<(u64, u32)>::new());
103
104        // (List.range _).map (reductionCount k)  ⟷  reduction_count(n, k)
105        let rc2: Vec<u32> = (0..8).map(|n| reduction_count(n, 2)).collect();
106        assert_eq!(rc2, vec![0, 1, 0, 2, 0, 1, 0, 3]);
107        let rc3: Vec<u32> = (0..9).map(|n| reduction_count(n, 3)).collect();
108        assert_eq!(rc3, vec![0, 0, 1, 0, 0, 1, 0, 0, 2]);
109
110        // The MMR mountain skeleton: leaf → peak (within-mountain base-k digits)
111        // then bagPath (peak → root). A singleton tree promotes to the root.
112        assert_eq!(mountain_skeleton(2, 1, 0), Some(vec![]));
113        // n=5, k=2: peaks = mountains of [4, 1]. Leaf 4 is the lone size-1
114        // mountain (peak 1); empty peakPath, one bag step (2 peaks → 1 bag node,
115        // peak 1 at position 1, one sibling).
116        assert_eq!(mountain_skeleton(2, 5, 4), Some(vec![step(1, 1)]));
117        // n=4, k=2: one perfect mountain (height 2), no bag. Leaf 1: two digit
118        // steps inside the mountain, empty bagPath.
119        assert_eq!(
120            mountain_skeleton(2, 4, 1),
121            Some(vec![step(1, 1), step(0, 1)])
122        );
123        // n=4, k=3: peaks = mountains of [3, 1]. Leaf 3 is the lone peak 1; one
124        // bag step (2 peaks → 1 bag node, position 1).
125        assert_eq!(mountain_skeleton(3, 4, 3), Some(vec![step(1, 1)]));
126        assert_eq!(mountain_skeleton(2, 4, 4), None); // index out of range
127        assert_eq!(mountain_skeleton(1, 4, 0), None); // arity out of range
128    }
129}