Skip to main content

rssn_advanced/parallel/
simplify.rs

1//! Staged global simplification.
2//!
3//! `plan.md §4.1` calls for "stage-wise simplification" — running a
4//! lightweight local simplifier every N rounds during a long-running
5//! computation rather than only at the end. Per `parallel_review §1`
6//! the `SimplifyConfig` knob existed but had no implementation;
7//! [`run_staged_simplification`] supplies it.
8//!
9//! The loop is intentionally simple:
10//!
11//! 1. Caller passes a `DagBuilder` and a root `DagNodeId`.
12//! 2. We invoke the heuristic engine once per round, threading the
13//!    rewritten root through.
14//! 3. Each round goes through the full dedup-aware engine, so even
15//!    aggressive multi-round configurations cannot leak duplicates.
16
17use std::sync::atomic::{AtomicU64, Ordering};
18
19use crate::dag::builder::DagBuilder;
20use crate::dag::node::DagNodeId;
21use crate::heuristic::{HeuristicConfig, HeuristicEngine, SearchStrategy};
22
23/// Staged simplification configuration parameters.
24#[derive(Debug, Clone, Copy)]
25pub struct SimplifyConfig {
26    /// Number of intermediate local simplification rounds to perform.
27    pub intermediate_rounds: usize,
28    /// Whether to trigger global deduplication and constant folding.
29    pub enable_global_dedup: bool,
30}
31
32impl Default for SimplifyConfig {
33    fn default() -> Self {
34        Self {
35            intermediate_rounds: 3,
36            enable_global_dedup: true,
37        }
38    }
39}
40
41impl SimplifyConfig {
42    /// Creates a new configuration builder.
43    #[must_use]
44    pub const fn new() -> Self {
45        Self {
46            intermediate_rounds: 0,
47            enable_global_dedup: true,
48        }
49    }
50
51    /// Sets the number of intermediate local simplification rounds.
52    #[must_use]
53    pub const fn intermediate_rounds(mut self, rounds: usize) -> Self {
54        self.intermediate_rounds = rounds;
55        self
56    }
57}
58
59/// Per-thread telemetry for staged simplification.
60///
61/// Tracks three per-thread metrics (`parallel_review §3.1`):
62/// - `steps_count` — total simplification rounds performed by this thread
63/// - `nodes_visited` — total DAG nodes inspected during traversal
64/// - `rewrites_applied` — total rewrite rules that fired (pattern matches)
65///
66/// The 128-byte `repr(align)` was removed: cache-line padding guards against
67/// false-sharing only when *different threads* write to *adjacent* memory. A
68/// `thread_local!` value lives in per-thread storage — no other thread can
69/// touch it — so over-alignment is pure waste.
70#[derive(Debug)]
71pub struct ThreadLocalState {
72    /// Total simplification steps (one per [`run_staged_simplification`] round).
73    pub steps_count: AtomicU64,
74    /// Total DAG nodes visited across all steps. Useful for throughput profiling.
75    pub nodes_visited: AtomicU64,
76    /// Total rewrite rules that fired. Non-zero means the expression changed.
77    pub rewrites_applied: AtomicU64,
78}
79
80impl Default for ThreadLocalState {
81    fn default() -> Self {
82        Self {
83            steps_count: AtomicU64::new(0),
84            nodes_visited: AtomicU64::new(0),
85            rewrites_applied: AtomicU64::new(0),
86        }
87    }
88}
89
90impl ThreadLocalState {
91    /// Creates a new instance with all counters at zero.
92    #[must_use]
93    pub const fn new() -> Self {
94        Self {
95            steps_count: AtomicU64::new(0),
96            nodes_visited: AtomicU64::new(0),
97            rewrites_applied: AtomicU64::new(0),
98        }
99    }
100
101    /// Increments the simplification-step counter.
102    pub fn increment(&self) {
103        self.steps_count.fetch_add(1, Ordering::Release);
104    }
105
106    /// Retrieves the simplification-step count.
107    #[must_use]
108    pub fn get_count(&self) -> u64 {
109        self.steps_count.load(Ordering::Acquire)
110    }
111
112    /// Records that `n` additional DAG nodes were visited.
113    pub fn record_nodes_visited(&self, n: u64) {
114        self.nodes_visited.fetch_add(n, Ordering::Relaxed);
115    }
116
117    /// Records that `n` rewrite rules fired.
118    pub fn record_rewrites(&self, n: u64) {
119        self.rewrites_applied.fetch_add(n, Ordering::Relaxed);
120    }
121
122    /// Returns the total number of nodes visited.
123    #[must_use]
124    pub fn get_nodes_visited(&self) -> u64 {
125        self.nodes_visited.load(Ordering::Relaxed)
126    }
127
128    /// Returns the total number of rewrites applied.
129    #[must_use]
130    pub fn get_rewrites_applied(&self) -> u64 {
131        self.rewrites_applied.load(Ordering::Relaxed)
132    }
133
134    /// Resets all counters to zero.
135    pub fn reset(&self) {
136        self.steps_count.store(0, Ordering::Release);
137        self.nodes_visited.store(0, Ordering::Relaxed);
138        self.rewrites_applied.store(0, Ordering::Relaxed);
139    }
140}
141
142// =========================================================================
143// Staged simplification driver (T4.3)
144// =========================================================================
145
146/// Runs the heuristic engine `cfg.intermediate_rounds` times against
147/// `root`, threading each round's output back into the next.
148///
149/// The engine itself preserves the dedup invariant, so spinning the
150/// loop multiple times cannot accidentally fork the arena.
151///
152/// When `cfg.intermediate_rounds == 0` the function is a no-op pass-
153/// through, matching the previous behaviour exactly.
154///
155/// When `cfg.enable_global_dedup == false`, the heuristic still runs
156/// but only with a single final round — useful for low-overhead
157/// configurations that don't need the per-round local cleanup.
158#[must_use]
159pub fn run_staged_simplification(
160    builder: &mut DagBuilder,
161    root: DagNodeId,
162    cfg: SimplifyConfig,
163) -> DagNodeId {
164    if root.is_none() {
165        return root;
166    }
167
168    let rounds = if cfg.enable_global_dedup {
169        cfg.intermediate_rounds
170    } else {
171        // Even when global dedup is off, we still want at least one
172        // pass so callers see *some* simplification effect.
173        cfg.intermediate_rounds.max(1)
174    };
175
176    if rounds == 0 {
177        return root;
178    }
179
180    let mut engine = HeuristicEngine::new(HeuristicConfig::default(), SearchStrategy::Greedy);
181    let mut current = root;
182    let mut prev = DagNodeId::NONE;
183
184    for _ in 0..rounds {
185        if current == prev {
186            // Fixpoint reached — additional rounds are wasted work.
187            break;
188        }
189        prev = current;
190        current = engine.simplify(builder, current);
191    }
192    current
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn zero_rounds_is_passthrough() {
201        let mut b = DagBuilder::new();
202        let x = b.variable("x");
203        let zero = b.constant(0.0);
204        let expr = b.add(x, zero);
205        let cfg = SimplifyConfig {
206            intermediate_rounds: 0,
207            enable_global_dedup: true,
208        };
209        assert_eq!(run_staged_simplification(&mut b, expr, cfg), expr);
210    }
211
212    #[test]
213    fn single_round_folds_identity() {
214        let mut b = DagBuilder::new();
215        let x = b.variable("x");
216        let zero = b.constant(0.0);
217        let expr = b.add(x, zero);
218        let cfg = SimplifyConfig {
219            intermediate_rounds: 1,
220            enable_global_dedup: true,
221        };
222        assert_eq!(run_staged_simplification(&mut b, expr, cfg), x);
223    }
224
225    #[test]
226    fn multiple_rounds_reach_fixpoint() {
227        let mut b = DagBuilder::new();
228        let x = b.variable("x");
229        let zero = b.constant(0.0);
230        let one = b.constant(1.0);
231        // ((x + 0) * 1) — takes two passes through identities to
232        // collapse, but our iterative engine already does it in one.
233        let inner = b.add(x, zero);
234        let outer = b.mul(inner, one);
235        let cfg = SimplifyConfig {
236            intermediate_rounds: 5,
237            enable_global_dedup: true,
238        };
239        let out = run_staged_simplification(&mut b, outer, cfg);
240        assert_eq!(out, x);
241    }
242}