Skip to main content

celox_slt/
scheduler.rs

1use crate::{
2    HashMap, HashSet, LogicPath, LogicPathTarget, NodeId, SLTNode, SLTNodeArena, SLTNodeFactsError,
3};
4use celox_analysis::dag_schedule::{
5    MappedGraphRows, MappedNodeRows, schedule_min_live_values_and_tokens_with_mapped_rows,
6};
7use celox_analysis::interval::{DisjointIntervalError, DisjointIntervalMap, ExactInterval};
8use celox_design::{BinaryOp, BitAccess, RuntimeErrorInfo, UnaryOp, VarAtomBase};
9use celox_sir::{
10    BlockId, ExecutionUnit, RegisterId, SIRBuilder, SIRInstruction, SIROffset, SIRTerminator,
11    SIRValue,
12};
13use std::collections::{BTreeMap, BTreeSet};
14use std::fmt::Debug;
15use std::fmt::Display;
16use std::hash::Hash;
17use thiserror::Error;
18
19/// Sparse scheduler-facing memory effects for one same-trigger FF group.
20#[derive(Debug, Clone, Default)]
21pub struct FfAccessSummary<A> {
22    pub reads: Vec<VarAtomBase<A>>,
23    pub writes: Vec<VarAtomBase<A>>,
24    pub dynamic_writes: HashSet<A>,
25}
26
27fn greedy_fas_sort(scc: &[usize], global_adj: &[Vec<usize>]) -> Vec<usize> {
28    let scc_set: HashSet<usize> = scc.iter().cloned().collect();
29    let mut local_adj: HashMap<usize, Vec<usize>> = HashMap::default();
30    let mut in_degree: HashMap<usize, usize> = HashMap::default();
31
32    for &u in scc {
33        in_degree.entry(u).or_insert(0);
34        let entries = local_adj.entry(u).or_default();
35        for &v in &global_adj[u] {
36            if scc_set.contains(&v) {
37                entries.push(v);
38                *in_degree.entry(v).or_insert(0) += 1;
39            }
40        }
41    }
42
43    let mut left = Vec::new();
44    let mut right = Vec::new();
45    let mut current_nodes: HashSet<usize> = scc.iter().cloned().collect();
46
47    while !current_nodes.is_empty() {
48        // 1. Sinks
49        while let Some(&u) = current_nodes
50            .iter()
51            .find(|&&u| local_adj.get(&u).is_none_or(|v| v.is_empty()))
52        {
53            right.push(u);
54            current_nodes.remove(&u);
55        }
56        // 2. Sources
57        while let Some(&u) = current_nodes
58            .iter()
59            .find(|&&u| in_degree.get(&u).is_none_or(|&d| d == 0))
60        {
61            left.push(u);
62            current_nodes.remove(&u);
63            if let Some(neighbors) = local_adj.remove(&u) {
64                for v in neighbors {
65                    if let Some(d) = in_degree.get_mut(&v) {
66                        *d -= 1;
67                    }
68                }
69            }
70        }
71        if current_nodes.is_empty() {
72            break;
73        }
74        // 3. Maximum Degree Difference
75        let &u = current_nodes
76            .iter()
77            .max_by_key(|&&u| {
78                let out_d = local_adj.get(&u).map_or(0, |v| v.len());
79                let in_d = in_degree.get(&u).cloned().unwrap_or(0);
80                out_d as i32 - in_d as i32
81            })
82            .unwrap();
83
84        left.push(u);
85        current_nodes.remove(&u);
86        if let Some(neighbors) = local_adj.remove(&u) {
87            for v in neighbors {
88                if let Some(d) = in_degree.get_mut(&v) {
89                    *d -= 1;
90                }
91            }
92        }
93    }
94    right.reverse();
95    left.extend(right);
96    left
97}
98fn calculate_required_iterations(adj: &[Vec<usize>], order: &[usize]) -> usize {
99    let pos: HashMap<usize, usize> = order.iter().enumerate().map(|(i, &n)| (n, i)).collect();
100    let scc_nodes: HashSet<usize> = order.iter().cloned().collect();
101
102    // Record already visited nodes to ensure a "simple path"
103    fn find_longest_backedge_path(
104        u: usize,
105        visited: &mut Vec<bool>,
106        adj: &[Vec<usize>],
107        pos: &HashMap<usize, usize>,
108        scc_nodes: &HashSet<usize>,
109    ) -> usize {
110        visited[u] = true;
111        let mut max_delay = 0;
112
113        for &v in &adj[u] {
114            if scc_nodes.contains(&v) && !visited[v] {
115                // 0 if forward direction, 1 if back-edge
116                let weight = if pos[&u] >= pos[&v] { 1 } else { 0 };
117                max_delay = max_delay
118                    .max(weight + find_longest_backedge_path(v, visited, adj, pos, scc_nodes));
119            }
120        }
121
122        visited[u] = false; // backtrack
123        max_delay
124    }
125
126    let mut overall_max_delay = 0;
127    let mut visited = vec![false; adj.len()];
128
129    // Search for the longest "waiting time (number of back-edges)" starting from each node
130    for &start_node in order {
131        overall_max_delay = overall_max_delay.max(find_longest_backedge_path(
132            start_node,
133            &mut visited,
134            adj,
135            &pos,
136            &scc_nodes,
137        ));
138    }
139
140    // Base execution (1) + number of times signals loop back (overall_max_delay)
141    overall_max_delay + 1
142}
143
144fn ranges_cover_access(ranges: &[BitAccess], access: BitAccess) -> bool {
145    let mut ranges = ranges.to_vec();
146    ranges.sort_unstable_by_key(|range| (range.lsb, range.msb));
147    let mut next = access.lsb;
148    for range in ranges {
149        if range.msb < next {
150            continue;
151        }
152        if range.lsb > next {
153            return false;
154        }
155        if range.msb >= access.msb {
156            return true;
157        }
158        let Some(after) = range.msb.checked_add(1) else {
159            return false;
160        };
161        next = after;
162    }
163    false
164}
165
166/// Conservatively check that every input of `address` reachable from `root`
167/// is supplied by the carried ranges. Dynamic inputs retain their complete
168/// declared access here; retaining an extra external dependency is safe.
169fn node_reads_only_covered_ranges<Addr: Clone + Eq + Hash>(
170    root: NodeId,
171    address: &Addr,
172    ranges: &[BitAccess],
173    arena: &SLTNodeArena<Addr>,
174) -> bool {
175    let mut visited = HashSet::default();
176    let mut work = vec![root];
177    while let Some(node) = work.pop() {
178        if !visited.insert(node) {
179            continue;
180        }
181        match arena.get(node) {
182            SLTNode::Input {
183                variable,
184                index,
185                access,
186                ..
187            } => {
188                if variable == address && !ranges_cover_access(ranges, *access) {
189                    return false;
190                }
191                work.extend(index.iter().map(|entry| entry.node));
192            }
193            SLTNode::Constant(..) => {}
194            SLTNode::Binary(lhs, _, rhs) => {
195                work.push(*lhs);
196                work.push(*rhs);
197            }
198            SLTNode::Unary(_, inner) | SLTNode::Capture { expr: inner, .. } => work.push(*inner),
199            SLTNode::Mux {
200                cond,
201                then_expr,
202                else_expr,
203            } => {
204                work.push(*cond);
205                work.push(*then_expr);
206                work.push(*else_expr);
207            }
208            SLTNode::Concat(parts) => work.extend(parts.iter().map(|(part, _)| *part)),
209            SLTNode::Slice { expr, .. } => work.push(*expr),
210            SLTNode::ForFold {
211                start,
212                end,
213                result,
214                initials,
215                updates,
216                effects,
217                continue_cond,
218                ..
219            } => {
220                if let crate::SLTLoopBound::Expr(node)
221                | crate::SLTLoopBound::TypedExpr { node, .. } = start
222                {
223                    work.push(*node);
224                }
225                if let crate::SLTLoopBound::Expr(node)
226                | crate::SLTLoopBound::TypedExpr { node, .. } = end
227                {
228                    work.push(*node);
229                }
230                if let crate::SLTForFoldResult::Transient { initial, update } = result {
231                    work.push(*initial);
232                    work.push(*update);
233                }
234                work.extend(initials.iter().map(|state| state.expr));
235                work.extend(updates.iter().map(|state| state.expr));
236                for effect in effects {
237                    match effect {
238                        crate::SLTForEffect::Event { guard, args, .. } => {
239                            work.extend(*guard);
240                            work.extend(args.iter().copied());
241                        }
242                        crate::SLTForEffect::Runner(runner) => work.push(*runner),
243                    }
244                }
245                work.push(*continue_cond);
246            }
247            SLTNode::ForFoldGroup {
248                entry_guard,
249                states,
250                ..
251            } => {
252                work.push(*entry_guard);
253                for state in states {
254                    work.push(state.initial);
255                    work.push(state.update);
256                }
257            }
258        }
259    }
260    true
261}
262
263fn collect_node_input_deps<Addr: Clone + Eq + Hash + Debug + Copy + Display>(
264    node: crate::NodeId,
265    arena: &SLTNodeArena<Addr>,
266    memo: &mut HashMap<crate::NodeId, HashSet<Addr>>,
267    inverse_memo: &mut HashMap<Addr, HashSet<crate::NodeId>>,
268) -> HashSet<Addr> {
269    if let Some(found) = memo.get(&node) {
270        return found.clone();
271    }
272
273    let deps = match arena.get(node) {
274        crate::SLTNode::Input {
275            variable, index, ..
276        } => {
277            let mut set = HashSet::default();
278            set.insert(*variable);
279            for idx in index {
280                set.extend(collect_node_input_deps(idx.node, arena, memo, inverse_memo));
281            }
282            set
283        }
284        crate::SLTNode::Slice { expr, .. } => {
285            collect_node_input_deps(*expr, arena, memo, inverse_memo)
286        }
287        crate::SLTNode::Concat(parts) => {
288            let mut set = HashSet::default();
289            for (part, _) in parts {
290                set.extend(collect_node_input_deps(*part, arena, memo, inverse_memo));
291            }
292            set
293        }
294        crate::SLTNode::Binary(lhs, _, rhs) => {
295            let mut set = collect_node_input_deps(*lhs, arena, memo, inverse_memo);
296            set.extend(collect_node_input_deps(*rhs, arena, memo, inverse_memo));
297            set
298        }
299        crate::SLTNode::Unary(_, inner) => {
300            collect_node_input_deps(*inner, arena, memo, inverse_memo)
301        }
302        crate::SLTNode::Capture { expr, .. } => {
303            collect_node_input_deps(*expr, arena, memo, inverse_memo)
304        }
305        crate::SLTNode::Mux {
306            cond,
307            then_expr,
308            else_expr,
309        } => {
310            let mut set = collect_node_input_deps(*cond, arena, memo, inverse_memo);
311            set.extend(collect_node_input_deps(
312                *then_expr,
313                arena,
314                memo,
315                inverse_memo,
316            ));
317            set.extend(collect_node_input_deps(
318                *else_expr,
319                arena,
320                memo,
321                inverse_memo,
322            ));
323            set
324        }
325        crate::SLTNode::ForFold {
326            loop_var,
327            start,
328            end,
329            result,
330            initials,
331            updates,
332            effects,
333            continue_cond,
334            ..
335        } => {
336            let mut set = HashSet::default();
337            match start {
338                crate::SLTLoopBound::Const(_) => {}
339                crate::SLTLoopBound::Expr(node) | crate::SLTLoopBound::TypedExpr { node, .. } => {
340                    set.extend(collect_node_input_deps(*node, arena, memo, inverse_memo));
341                }
342            }
343            match end {
344                crate::SLTLoopBound::Const(_) => {}
345                crate::SLTLoopBound::Expr(node) | crate::SLTLoopBound::TypedExpr { node, .. } => {
346                    set.extend(collect_node_input_deps(*node, arena, memo, inverse_memo));
347                }
348            }
349            if let crate::SLTForFoldResult::Transient { initial, update } = result {
350                set.extend(collect_node_input_deps(*initial, arena, memo, inverse_memo));
351                set.extend(collect_node_input_deps(*update, arena, memo, inverse_memo));
352            }
353            for init in initials {
354                set.extend(collect_node_input_deps(
355                    init.expr,
356                    arena,
357                    memo,
358                    inverse_memo,
359                ));
360            }
361            for update in updates {
362                set.extend(collect_node_input_deps(
363                    update.expr,
364                    arena,
365                    memo,
366                    inverse_memo,
367                ));
368            }
369            for effect in effects {
370                match effect {
371                    crate::SLTForEffect::Event { guard, args, .. } => {
372                        if let Some(guard) = guard {
373                            set.extend(collect_node_input_deps(*guard, arena, memo, inverse_memo));
374                        }
375                        for arg in args {
376                            set.extend(collect_node_input_deps(*arg, arena, memo, inverse_memo));
377                        }
378                    }
379                    crate::SLTForEffect::Runner(runner) => {
380                        set.extend(collect_node_input_deps(*runner, arena, memo, inverse_memo));
381                    }
382                }
383            }
384            set.remove(loop_var);
385            set.extend(collect_node_input_deps(
386                *continue_cond,
387                arena,
388                memo,
389                inverse_memo,
390            ));
391            set.remove(loop_var);
392            set
393        }
394        crate::SLTNode::ForFoldGroup {
395            loop_var,
396            entry_guard,
397            states,
398            ..
399        } => {
400            let mut set = collect_node_input_deps(*entry_guard, arena, memo, inverse_memo);
401            for state in states {
402                set.extend(collect_node_input_deps(
403                    state.initial,
404                    arena,
405                    memo,
406                    inverse_memo,
407                ));
408            }
409            let mut update_deps = HashSet::default();
410            for state in states {
411                update_deps.extend(collect_node_input_deps(
412                    state.update,
413                    arena,
414                    memo,
415                    inverse_memo,
416                ));
417            }
418            update_deps.remove(loop_var);
419
420            let mut state_ranges: HashMap<Addr, Vec<BitAccess>> = HashMap::default();
421            for state in states {
422                state_ranges
423                    .entry(state.target.id)
424                    .or_default()
425                    .push(state.target.access);
426            }
427            for (state_id, ranges) in state_ranges {
428                if states.iter().all(|state| {
429                    node_reads_only_covered_ranges(state.update, &state_id, &ranges, arena)
430                }) {
431                    update_deps.remove(&state_id);
432                }
433            }
434            set.extend(update_deps);
435            set
436        }
437        crate::SLTNode::Constant(_, _, _, _) => HashSet::default(),
438    };
439
440    for &addr in &deps {
441        inverse_memo.entry(addr).or_default().insert(node);
442    }
443    memo.insert(node, deps.clone());
444    deps
445}
446
447fn collect_logic_path_input_deps<Addr: Clone + Eq + Hash + Debug + Copy + Display>(
448    path: &LogicPath<Addr>,
449    arena: &SLTNodeArena<Addr>,
450    memo: &mut HashMap<NodeId, HashSet<Addr>>,
451    inverse_memo: &mut HashMap<Addr, HashSet<NodeId>>,
452) {
453    collect_node_input_deps(path.expr, arena, memo, inverse_memo);
454    for (_, node) in &path.local_inputs {
455        collect_node_input_deps(*node, arena, memo, inverse_memo);
456    }
457    for node in &path.pre_lower_nodes {
458        collect_node_input_deps(*node, arena, memo, inverse_memo);
459    }
460}
461
462struct TarjanContext {
463    index: usize,
464    stack: Vec<usize>,
465    on_stack: HashSet<usize>,
466    indices: Vec<Option<usize>>,
467    lowlink: Vec<Option<usize>>,
468    sccs: Vec<Vec<usize>>,
469}
470
471fn strong_connect(u: usize, adj: &Vec<Vec<usize>>, ctx: &mut TarjanContext) {
472    ctx.indices[u] = Some(ctx.index);
473    ctx.lowlink[u] = Some(ctx.index);
474    ctx.index += 1;
475    ctx.stack.push(u);
476    ctx.on_stack.insert(u);
477
478    for &v in &adj[u] {
479        if ctx.indices[v].is_none() {
480            strong_connect(v, adj, ctx);
481            ctx.lowlink[u] = Some(ctx.lowlink[u].unwrap().min(ctx.lowlink[v].unwrap()));
482        } else if ctx.on_stack.contains(&v) {
483            ctx.lowlink[u] = Some(ctx.lowlink[u].unwrap().min(ctx.indices[v].unwrap()));
484        }
485    }
486
487    if ctx.lowlink[u] == ctx.indices[u] {
488        let mut scc = Vec::new();
489        while let Some(w) = ctx.stack.pop() {
490            ctx.on_stack.remove(&w);
491            scc.push(w);
492            if w == u {
493                break;
494            }
495        }
496        ctx.sccs.push(scc);
497    }
498}
499
500fn component_map(adj: &Vec<Vec<usize>>) -> Vec<usize> {
501    let mut ctx = TarjanContext {
502        index: 0,
503        stack: Vec::new(),
504        on_stack: HashSet::default(),
505        indices: vec![None; adj.len()],
506        lowlink: vec![None; adj.len()],
507        sccs: Vec::new(),
508    };
509    for node in 0..adj.len() {
510        if ctx.indices[node].is_none() {
511            strong_connect(node, adj, &mut ctx);
512        }
513    }
514    let mut component_by_node = vec![usize::MAX; adj.len()];
515    for (component, nodes) in ctx.sccs.iter().enumerate() {
516        for &node in nodes {
517            component_by_node[node] = component;
518        }
519    }
520    component_by_node
521}
522
523/// Add old-state anti-dependencies which make a later FF write eligible for
524/// direct publication.  These edges are profitable ordering constraints, not
525/// RTL semantic requirements: WORKING staging is always a correct fallback.
526///
527/// Add all candidates once, identify the SCCs they create, then remove every
528/// newly-added edge internal to such an SCC.  Any remaining edge is acyclic in
529/// the resulting graph.  This keeps all old-state readers before a directly
530/// publishable writer without turning feedback pipelines into comb/FF loops.
531fn add_acyclic_ff_write_order_edges(
532    adj: &mut Vec<Vec<usize>>,
533    optional_edges: impl IntoIterator<Item = (usize, usize)>,
534) -> HashSet<(usize, usize)> {
535    let mut requested = HashSet::default();
536    let mut added = vec![Vec::<usize>::new(); adj.len()];
537    for (source, target) in optional_edges {
538        if source == target || source >= adj.len() || target >= adj.len() {
539            continue;
540        }
541        requested.insert((source, target));
542        if !adj[source].contains(&target) {
543            adj[source].push(target);
544            added[source].push(target);
545        }
546    }
547    if added.iter().all(Vec::is_empty) {
548        return requested;
549    }
550
551    let component_by_node = component_map(adj);
552    for (source, targets) in added.iter_mut().enumerate() {
553        targets.retain(|target| component_by_node[source] == component_by_node[*target]);
554        targets.sort_unstable();
555        if !targets.is_empty() {
556            adj[source].retain(|target| targets.binary_search(target).is_err());
557        }
558    }
559    requested.retain(|(source, target)| adj[*source].contains(target));
560    requested
561}
562
563/// Memory definitions and uses induced by one set of LogicPaths.
564///
565/// A variable target is one bit-range MemoryDef. A current-value source is a
566/// MemoryUse of every overlapping definition; a previous-value source is a
567/// live-on-entry use plus an anti-dependence which keeps that use before an
568/// overlapping definition. `dependencies` contains all semantic edges while
569/// `values` contains only Def-to-Use edges which can become a forwarded value
570/// during later lowering. Both relations retain their forward and reverse
571/// adjacency so regional scheduling can borrow either direction.
572struct LogicPathMemorySsa {
573    dependencies: LogicPathEdges,
574    values: LogicPathEdges,
575}
576
577struct LogicPathEdges {
578    users: Vec<Vec<usize>>,
579    predecessors: Vec<Vec<usize>>,
580}
581
582impl LogicPathEdges {
583    fn new(node_count: usize) -> Self {
584        Self {
585            users: vec![Vec::new(); node_count],
586            predecessors: vec![Vec::new(); node_count],
587        }
588    }
589
590    fn resize(&mut self, node_count: usize) {
591        self.users.resize_with(node_count, Vec::new);
592        self.predecessors.resize_with(node_count, Vec::new);
593    }
594
595    fn push(&mut self, predecessor: usize, user: usize) {
596        self.users[predecessor].push(user);
597        self.predecessors[user].push(predecessor);
598    }
599
600    fn canonicalize(&mut self) {
601        for row in self.users.iter_mut().chain(&mut self.predecessors) {
602            row.sort_unstable();
603            row.dedup();
604        }
605    }
606}
607
608#[derive(Debug, Clone, PartialEq, Eq)]
609pub(crate) struct FfCombSchedulePlan {
610    pub required_comb: Vec<bool>,
611    pub comb_value_predecessors: Vec<Vec<usize>>,
612    pub comb_before_direct_write: Vec<Vec<usize>>,
613    pub ff_before_direct_write: Vec<Vec<usize>>,
614}
615
616fn bit_interval(access: BitAccess) -> Option<(usize, usize)> {
617    Some((
618        access.lsb,
619        access.msb.checked_sub(access.lsb)?.checked_add(1)?,
620    ))
621}
622
623fn build_logic_path_memory_ssa<Addr>(
624    input: &[LogicPath<Addr>],
625) -> Result<LogicPathMemorySsa, SchedulerError<Addr>>
626where
627    Addr: Copy + Ord + Hash + Eq + Display + Debug,
628{
629    let mut definition_intervals = Vec::new();
630    for (path, logic_path) in input.iter().enumerate() {
631        let Some(target) = logic_path.target.var() else {
632            continue;
633        };
634        let Some((start, length)) = bit_interval(target.access) else {
635            return Err(SchedulerError::InvalidDependencyGraph);
636        };
637        definition_intervals.push(ExactInterval {
638            object: target.id,
639            start,
640            length,
641            value: path,
642        });
643    }
644    let definitions = match DisjointIntervalMap::try_new(definition_intervals) {
645        Ok(definitions) => definitions,
646        Err(DisjointIntervalError::Overlap { first, second }) => {
647            return Err(SchedulerError::MultipleDriver {
648                blocks: vec![input[first].clone(), input[second].clone()],
649            });
650        }
651        Err(DisjointIntervalError::Empty { .. } | DisjointIntervalError::Overflow { .. }) => {
652            return Err(SchedulerError::InvalidDependencyGraph);
653        }
654    };
655
656    let mut dependencies = LogicPathEdges::new(input.len());
657    let mut values = LogicPathEdges::new(input.len());
658    for (user, path) in input.iter().enumerate() {
659        for source in &path.sources {
660            let Some((start, length)) = bit_interval(source.access) else {
661                return Err(SchedulerError::InvalidDependencyGraph);
662            };
663            let reaching = definitions
664                .overlapping(&source.id, start, length)
665                .map_err(|_| SchedulerError::InvalidDependencyGraph)?;
666            for definition in reaching {
667                dependencies.push(definition, user);
668                values.push(definition, user);
669            }
670        }
671
672        // A previous-value source reads the live-on-entry version. Preserve
673        // that read before every overlapping definition. A self read/write is
674        // already one atomic LogicPath and needs no self anti-dependence.
675        for source in &path.previous_sources {
676            let Some((start, length)) = bit_interval(source.access) else {
677                return Err(SchedulerError::InvalidDependencyGraph);
678            };
679            let reaching = definitions
680                .overlapping(&source.id, start, length)
681                .map_err(|_| SchedulerError::InvalidDependencyGraph)?;
682            for definition in reaching.filter(|definition| *definition != user) {
683                dependencies.push(user, definition);
684            }
685        }
686        for target in &path.order_before {
687            if target.0 < input.len() && target.0 != user {
688                dependencies.push(user, target.0);
689            }
690        }
691    }
692    dependencies.canonicalize();
693    values.canonicalize();
694    Ok(LogicPathMemorySsa {
695        dependencies,
696        values,
697    })
698}
699
700/// Paths participating in a semantic dependency cycle, including self loops.
701/// Frontends can use this before scheduling to refine atomic pure expressions
702/// without perturbing the representation of already acyclic logic.
703pub fn cyclic_logic_paths<Addr>(
704    input: &[LogicPath<Addr>],
705) -> Result<Vec<usize>, SchedulerError<Addr>>
706where
707    Addr: Copy + Ord + Hash + Eq + Display + Debug,
708{
709    let memory = build_logic_path_memory_ssa(input)?;
710    let edges = &memory.dependencies.users;
711    let components = component_map(edges);
712    Ok(edges
713        .iter()
714        .enumerate()
715        .filter_map(|(source, targets)| {
716            targets
717                .iter()
718                .any(|&target| components[source] == components[target])
719                .then_some(source)
720        })
721        .collect())
722}
723
724/// Group bit ranges by destination object for overlap probes.
725///
726/// Overlap tests pair a read with the writes of the same object, or a write
727/// with the reads of the same object. Indexing one side by object id lets the
728/// probe scan only atoms that share the object instead of the whole access
729/// list, which keeps the FF/comb scheduling passes from nesting linearly.
730fn index_accesses<'a, Addr: Copy + Eq + Hash + 'a>(
731    accesses: impl IntoIterator<Item = &'a VarAtomBase<Addr>>,
732) -> HashMap<Addr, Vec<BitAccess>> {
733    let mut index = HashMap::<Addr, Vec<BitAccess>>::default();
734    for access in accesses {
735        index.entry(access.id).or_default().push(access.access);
736    }
737    index
738}
739
740/// Returns true when any indexed range overlaps `access`.
741fn indexed_overlaps<Addr: Eq + Hash>(
742    index: &HashMap<Addr, Vec<BitAccess>>,
743    access: &VarAtomBase<Addr>,
744) -> bool {
745    index
746        .get(&access.id)
747        .is_some_and(|ranges| ranges.iter().any(|range| range.overlaps(&access.access)))
748}
749
750pub(crate) fn plan_ff_comb_schedule<Addr>(
751    input: &[LogicPath<Addr>],
752    ff: &[FfAccessSummary<Addr>],
753) -> Result<FfCombSchedulePlan, SchedulerError<Addr>>
754where
755    Addr: Copy + Ord + Hash + Eq + Display + Debug,
756{
757    let memory = build_logic_path_memory_ssa(input)?;
758    let mut definition_intervals = Vec::new();
759    for (path, logic_path) in input.iter().enumerate() {
760        let Some(target) = logic_path.target.var() else {
761            continue;
762        };
763        let Some((start, length)) = bit_interval(target.access) else {
764            return Err(SchedulerError::InvalidDependencyGraph);
765        };
766        definition_intervals.push(ExactInterval {
767            object: target.id,
768            start,
769            length,
770            value: path,
771        });
772    }
773    let definitions = DisjointIntervalMap::try_new(definition_intervals)
774        .map_err(|_| SchedulerError::InvalidDependencyGraph)?;
775
776    let mut comb_value_predecessors = vec![Vec::new(); ff.len()];
777    let mut required_comb = vec![false; input.len()];
778    let mut work = Vec::new();
779    for (ff_index, summary) in ff.iter().enumerate() {
780        for read in &summary.reads {
781            let Some((start, length)) = bit_interval(read.access) else {
782                return Err(SchedulerError::InvalidDependencyGraph);
783            };
784            for definition in definitions
785                .overlapping(&read.id, start, length)
786                .map_err(|_| SchedulerError::InvalidDependencyGraph)?
787            {
788                comb_value_predecessors[ff_index].push(definition);
789                if !std::mem::replace(&mut required_comb[definition], true) {
790                    work.push(definition);
791                }
792            }
793        }
794    }
795    for (path, logic_path) in input.iter().enumerate() {
796        if logic_path_is_scheduling_barrier(logic_path)
797            && !std::mem::replace(&mut required_comb[path], true)
798        {
799            work.push(path);
800        }
801    }
802    for row in &mut comb_value_predecessors {
803        row.sort_unstable();
804        row.dedup();
805    }
806
807    while let Some(path) = work.pop() {
808        for &predecessor in &memory.dependencies.predecessors[path] {
809            if !std::mem::replace(&mut required_comb[predecessor], true) {
810                work.push(predecessor);
811            }
812        }
813    }
814
815    // Index each summary's writes by object once; both loops below probe the
816    // same write set repeatedly.
817    let ff_writes = ff
818        .iter()
819        .map(|summary| index_accesses(&summary.writes))
820        .collect::<Vec<_>>();
821
822    let mut comb_before_direct_write = vec![Vec::new(); ff.len()];
823    for (path_index, path) in input.iter().enumerate() {
824        if !required_comb[path_index] {
825            continue;
826        }
827        for (ff_index, writes) in ff_writes.iter().enumerate() {
828            if path
829                .sources
830                .iter()
831                .chain(&path.previous_sources)
832                .any(|read| indexed_overlaps(writes, read))
833            {
834                comb_before_direct_write[ff_index].push(path_index);
835            }
836        }
837    }
838
839    let mut ff_before_direct_write = vec![Vec::new(); ff.len()];
840    for (writer, writes) in ff_writes.iter().enumerate() {
841        for (reader, read_summary) in ff.iter().enumerate() {
842            if writer != reader
843                && read_summary
844                    .reads
845                    .iter()
846                    .any(|read| indexed_overlaps(writes, read))
847            {
848                ff_before_direct_write[writer].push(reader);
849            }
850        }
851    }
852    for row in comb_before_direct_write
853        .iter_mut()
854        .chain(&mut ff_before_direct_write)
855    {
856        row.sort_unstable();
857        row.dedup();
858    }
859
860    Ok(FfCombSchedulePlan {
861        required_comb,
862        comb_value_predecessors,
863        comb_before_direct_write,
864        ff_before_direct_write,
865    })
866}
867
868fn lower_logic_path_expr<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
869    lowerer: &crate::SLTToSIRLowerer,
870    builder: &mut SIRBuilder<Addr>,
871    path: &LogicPath<Addr>,
872    arena: &SLTNodeArena<Addr>,
873    lower_cache: &mut HashMap<NodeId, RegisterId>,
874) -> RegisterId {
875    if path.local_inputs.is_empty() {
876        return lowerer.lower(builder, path.expr, arena, lower_cache);
877    }
878
879    let mut env_inputs = HashMap::default();
880    for (addr, node) in &path.local_inputs {
881        let reg = lowerer.lower(builder, *node, arena, lower_cache);
882        let width = crate::get_width(*node, arena);
883        if width > 0 {
884            env_inputs.insert(VarAtomBase::new(*addr, 0, width - 1), reg);
885        }
886    }
887    lowerer.lower_with_inputs(builder, path.expr, arena, lower_cache, env_inputs)
888}
889
890fn lower_logic_path_node<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
891    lowerer: &crate::SLTToSIRLowerer,
892    builder: &mut SIRBuilder<Addr>,
893    path: &LogicPath<Addr>,
894    node: NodeId,
895    arena: &SLTNodeArena<Addr>,
896    lower_cache: &mut HashMap<NodeId, RegisterId>,
897) -> RegisterId {
898    if path.local_inputs.is_empty() {
899        return lowerer.lower(builder, node, arena, lower_cache);
900    }
901    // Unbound captures may already have been materialized before a later
902    // write. Do not rebuild them under the observer's unrelated local inputs.
903    if matches!(arena.get(node), crate::SLTNode::Capture { .. })
904        && let Some(reg) = lower_cache.get(&node)
905    {
906        return *reg;
907    }
908    let mut env_inputs = HashMap::default();
909    for (addr, local_node) in &path.local_inputs {
910        let reg = lowerer.lower(builder, *local_node, arena, lower_cache);
911        let width = crate::get_width(*local_node, arena);
912        if width > 0 {
913            env_inputs.insert(VarAtomBase::new(*addr, 0, width - 1), reg);
914        }
915    }
916    lowerer.lower_with_inputs(builder, node, arena, lower_cache, env_inputs)
917}
918
919fn pre_lower_logic_path_node<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
920    lowerer: &crate::SLTToSIRLowerer,
921    builder: &mut SIRBuilder<Addr>,
922    path: &LogicPath<Addr>,
923    node: NodeId,
924    arena: &SLTNodeArena<Addr>,
925    lower_cache: &mut HashMap<NodeId, RegisterId>,
926) {
927    if path.local_inputs.is_empty() {
928        lowerer.lower(builder, node, arena, lower_cache);
929    }
930}
931
932fn static_access_offset<Addr: Eq + Hash>(
933    addr: &Addr,
934    access: BitAccess,
935    unpacked_element_widths: &HashMap<Addr, usize>,
936) -> SIROffset {
937    let width = access.msb - access.lsb + 1;
938    match unpacked_element_widths.get(addr).copied() {
939        Some(element_width)
940            if element_width != 0
941                && width > element_width
942                && access.lsb.is_multiple_of(element_width)
943                && width.is_multiple_of(element_width) =>
944        {
945            SIROffset::PackedElements {
946                bit_offset: access.lsb,
947                element_width,
948            }
949        }
950        _ => SIROffset::Static(access.lsb),
951    }
952}
953
954fn emit_logic_path_store_with_result<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
955    lowerer: &crate::SLTToSIRLowerer,
956    builder: &mut SIRBuilder<Addr>,
957    path: &LogicPath<Addr>,
958    arena: &SLTNodeArena<Addr>,
959    lower_cache: &mut HashMap<NodeId, RegisterId>,
960    unpacked_element_widths: &HashMap<Addr, usize>,
961    prepared_result: Option<RegisterId>,
962) {
963    match &path.target {
964        LogicPathTarget::Var(target) => {
965            for node in &path.pre_lower_nodes {
966                pre_lower_logic_path_node(lowerer, builder, path, *node, arena, lower_cache);
967            }
968            let result_reg = match prepared_result {
969                Some(result) => result,
970                None => lower_logic_path_expr(lowerer, builder, path, arena, lower_cache),
971            };
972            let width = 1 + target.access.msb - target.access.lsb;
973            let offset = static_access_offset(&target.id, target.access, unpacked_element_widths);
974            let old_reg =
975                if path.comb_capture_enable_sites.is_empty() || path.comb_capture_enable_always {
976                    None
977                } else {
978                    let old_reg = builder.alloc_bit(width, false);
979                    builder.emit(SIRInstruction::Load(
980                        old_reg,
981                        target.id,
982                        offset.clone(),
983                        width,
984                    ));
985                    Some(old_reg)
986                };
987            builder.emit(SIRInstruction::Store(
988                target.id,
989                offset,
990                width,
991                result_reg,
992                Vec::new(),
993                Vec::new(),
994            ));
995            if !path.comb_capture_enable_sites.is_empty() {
996                let (old, new) = if path.comb_capture_enable_always {
997                    let old = builder.alloc_bit(1, false);
998                    let new = builder.alloc_bit(1, false);
999                    builder.emit(SIRInstruction::Imm(old, SIRValue::new(0u8)));
1000                    builder.emit(SIRInstruction::Imm(new, SIRValue::new(1u8)));
1001                    (old, new)
1002                } else {
1003                    (
1004                        old_reg.expect("changed capture enable loads the old value"),
1005                        result_reg,
1006                    )
1007                };
1008                builder.emit(SIRInstruction::CombCaptureEnableIfChanged {
1009                    old,
1010                    new,
1011                    sites: path.comb_capture_enable_sites.clone(),
1012                });
1013            }
1014        }
1015        LogicPathTarget::CombCaptureEvent {
1016            site_id,
1017            guard,
1018            emit_on_true,
1019            args,
1020            loop_runner,
1021            fatal_error_code,
1022            consume_enabled,
1023        } => {
1024            debug_assert!(prepared_result.is_none());
1025            if let Some(loop_runner) = loop_runner {
1026                lower_logic_path_node(lowerer, builder, path, *loop_runner, arena, lower_cache);
1027                return;
1028            }
1029            let emit = |builder: &mut SIRBuilder<Addr>,
1030                        lower_cache: &mut HashMap<NodeId, RegisterId>| {
1031                let regs = args
1032                    .iter()
1033                    .map(|arg| {
1034                        lower_logic_path_node(lowerer, builder, path, *arg, arena, lower_cache)
1035                    })
1036                    .collect();
1037                builder.emit(SIRInstruction::CombCaptureEvent {
1038                    site_id: *site_id,
1039                    args: regs,
1040                    fatal_error_code: *fatal_error_code,
1041                    consume_enabled: *consume_enabled,
1042                });
1043            };
1044            if let Some(guard) = guard {
1045                let cond =
1046                    lower_logic_path_node(lowerer, builder, path, *guard, arena, lower_cache);
1047                let branch_cond = if *emit_on_true {
1048                    cond
1049                } else {
1050                    let inverted = builder.alloc_bit(1, false);
1051                    builder.emit(SIRInstruction::Unary(inverted, UnaryOp::LogicNot, cond));
1052                    inverted
1053                };
1054                let event_block = builder.new_block();
1055                let done_block = builder.new_block();
1056                builder.seal_block(SIRTerminator::Branch {
1057                    cond: branch_cond,
1058                    true_block: (event_block, vec![]),
1059                    false_block: (done_block, vec![]),
1060                });
1061                builder.switch_to_block(event_block);
1062                emit(builder, lower_cache);
1063                builder.seal_block(SIRTerminator::Jump(done_block, vec![]));
1064                builder.switch_to_block(done_block);
1065            } else {
1066                emit(builder, lower_cache);
1067            }
1068        }
1069    }
1070}
1071
1072fn emit_logic_path_store<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
1073    lowerer: &crate::SLTToSIRLowerer,
1074    builder: &mut SIRBuilder<Addr>,
1075    path: &LogicPath<Addr>,
1076    arena: &SLTNodeArena<Addr>,
1077    lower_cache: &mut HashMap<NodeId, RegisterId>,
1078    unpacked_element_widths: &HashMap<Addr, usize>,
1079) {
1080    emit_logic_path_store_with_result(
1081        lowerer,
1082        builder,
1083        path,
1084        arena,
1085        lower_cache,
1086        unpacked_element_widths,
1087        None,
1088    );
1089}
1090
1091fn invalidate_logic_path_target<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
1092    path: &LogicPath<Addr>,
1093    inverse_dep_memo: &HashMap<Addr, HashSet<NodeId>>,
1094    lower_cache: &mut HashMap<NodeId, RegisterId>,
1095) {
1096    let Some(target) = path.target.var() else {
1097        return;
1098    };
1099    if let Some(to_remove) = inverse_dep_memo.get(&target.id) {
1100        for node in to_remove {
1101            if !path.pre_lower_nodes.contains(node) {
1102                lower_cache.remove(node);
1103            }
1104        }
1105    }
1106}
1107
1108#[allow(clippy::too_many_arguments)]
1109fn emit_scheduled_guard_region<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
1110    lowerer: &crate::SLTToSIRLowerer,
1111    builder: &mut SIRBuilder<Addr>,
1112    condition: NodeId,
1113    paths: &[usize],
1114    input: &[LogicPath<Addr>],
1115    arena: &SLTNodeArena<Addr>,
1116    lower_cache: &mut HashMap<NodeId, RegisterId>,
1117    dep_memo: &mut HashMap<NodeId, HashSet<Addr>>,
1118    inverse_dep_memo: &mut HashMap<Addr, HashSet<NodeId>>,
1119    unpacked_element_widths: &HashMap<Addr, usize>,
1120) {
1121    for &path in paths {
1122        collect_logic_path_input_deps(&input[path], arena, dep_memo, inverse_dep_memo);
1123    }
1124
1125    let condition_value = lowerer.lower(builder, condition, arena, lower_cache);
1126    let then_block = builder.new_block();
1127    let else_block = builder.new_block();
1128    let merge_block = builder.new_block();
1129    builder.seal_block(SIRTerminator::Branch {
1130        cond: condition_value,
1131        true_block: (then_block, Vec::new()),
1132        false_block: (else_block, Vec::new()),
1133    });
1134
1135    let mut affected = paths
1136        .iter()
1137        .filter_map(|path| input[*path].target.var())
1138        .filter_map(|target| inverse_dep_memo.get(&target.id))
1139        .flatten()
1140        .copied()
1141        .collect::<Vec<_>>();
1142    affected.sort_unstable();
1143    affected.dedup();
1144    let saved_cache = affected
1145        .iter()
1146        .filter_map(|node| lower_cache.get(node).copied().map(|value| (*node, value)))
1147        .collect::<Vec<_>>();
1148
1149    let emit_arm = |builder: &mut SIRBuilder<Addr>,
1150                    lower_cache: &mut HashMap<NodeId, RegisterId>,
1151                    take_true: bool| {
1152        let mut inserted = Vec::new();
1153        for &path in paths {
1154            let (_, then_expr, else_expr) = scheduled_root_mux(&input[path], arena)
1155                .expect("scheduled guard region must retain root Muxes");
1156            let root = if take_true { then_expr } else { else_expr };
1157            // Keep the existing per-LogicPath lowering model for nested
1158            // decisions. The scheduled region owns only the common outer
1159            // guard; changing fanout/cost facts for all nested Muxes at once
1160            // would silently replace the established branch decisions.
1161            let value = lowerer.lower(builder, root, arena, lower_cache);
1162            inserted.extend(lowerer.take_scheduled_region_insertions());
1163            emit_logic_path_store_with_result(
1164                lowerer,
1165                builder,
1166                &input[path],
1167                arena,
1168                lower_cache,
1169                unpacked_element_widths,
1170                Some(value),
1171            );
1172            invalidate_logic_path_target(&input[path], inverse_dep_memo, lower_cache);
1173        }
1174        builder.seal_block(SIRTerminator::Jump(merge_block, Vec::new()));
1175        for node in inserted {
1176            lower_cache.remove(&node);
1177        }
1178        for node in &affected {
1179            lower_cache.remove(node);
1180        }
1181        lower_cache.extend(saved_cache.iter().copied());
1182    };
1183
1184    builder.switch_to_block(then_block);
1185    emit_arm(builder, lower_cache, true);
1186    builder.switch_to_block(else_block);
1187    emit_arm(builder, lower_cache, false);
1188    builder.switch_to_block(merge_block);
1189
1190    // Both arms publish every path target, so cached expressions reading any
1191    // such target are invalid at the merge even when both arm caches happened
1192    // to contain the same SLT node identity.
1193    for node in affected {
1194        lower_cache.remove(&node);
1195    }
1196}
1197
1198fn projected_for_fold_group<Addr: Clone + Eq + Hash>(
1199    mut node: NodeId,
1200    arena: &SLTNodeArena<Addr>,
1201) -> Option<NodeId> {
1202    loop {
1203        match arena.get(node) {
1204            SLTNode::ForFoldGroup { .. } => return Some(node),
1205            SLTNode::Slice { expr, .. } => node = *expr,
1206            _ => return None,
1207        }
1208    }
1209}
1210
1211fn fold_group_projection_access<Addr: Clone + Eq + Hash>(
1212    node: NodeId,
1213    group: NodeId,
1214    arena: &SLTNodeArena<Addr>,
1215) -> Option<BitAccess> {
1216    if node == group {
1217        let width = crate::get_width(group, arena);
1218        return (width != 0).then(|| BitAccess::new(0, width - 1));
1219    }
1220    let SLTNode::Slice { expr, access } = arena.get(node) else {
1221        return None;
1222    };
1223    let parent = fold_group_projection_access(*expr, group, arena)?;
1224    let parent_width = parent.msb.checked_sub(parent.lsb)?.checked_add(1)?;
1225    if access.msb >= parent_width {
1226        return None;
1227    }
1228    Some(BitAccess::new(
1229        parent.lsb.checked_add(access.lsb)?,
1230        parent.lsb.checked_add(access.msb)?,
1231    ))
1232}
1233
1234fn packed_fold_group_state_accesses<Addr: Clone + Eq + Hash>(
1235    states: &[crate::SLTForFoldGroupState<Addr>],
1236) -> Option<Vec<BitAccess>> {
1237    let total_width = states.iter().try_fold(0usize, |total, state| {
1238        let width = state
1239            .target
1240            .access
1241            .msb
1242            .checked_sub(state.target.access.lsb)?
1243            .checked_add(1)?;
1244        total.checked_add(width)
1245    })?;
1246    if total_width == 0 {
1247        return None;
1248    }
1249
1250    let mut next_msb = total_width;
1251    let mut result = Vec::with_capacity(states.len());
1252    for state in states {
1253        let width = state.target.access.msb - state.target.access.lsb + 1;
1254        next_msb = next_msb.checked_sub(width)?;
1255        result.push(BitAccess::new(next_msb, next_msb + width - 1));
1256    }
1257    Some(result)
1258}
1259
1260fn push_scheduler_node_children<Addr: Clone + Eq + Hash>(
1261    node: NodeId,
1262    arena: &SLTNodeArena<Addr>,
1263    work: &mut Vec<NodeId>,
1264) {
1265    match arena.get(node) {
1266        SLTNode::Input { index, .. } => work.extend(index.iter().map(|entry| entry.node)),
1267        SLTNode::Constant(..) => {}
1268        SLTNode::Binary(lhs, _, rhs) => {
1269            work.push(*lhs);
1270            work.push(*rhs);
1271        }
1272        SLTNode::Unary(_, inner) | SLTNode::Capture { expr: inner, .. } => work.push(*inner),
1273        SLTNode::Mux {
1274            cond,
1275            then_expr,
1276            else_expr,
1277        } => {
1278            work.push(*cond);
1279            work.push(*then_expr);
1280            work.push(*else_expr);
1281        }
1282        SLTNode::Concat(parts) => work.extend(parts.iter().map(|(part, _)| *part)),
1283        SLTNode::Slice { expr, .. } => work.push(*expr),
1284        SLTNode::ForFold {
1285            start,
1286            end,
1287            result,
1288            initials,
1289            updates,
1290            effects,
1291            continue_cond,
1292            ..
1293        } => {
1294            if let crate::SLTLoopBound::Expr(node) | crate::SLTLoopBound::TypedExpr { node, .. } =
1295                start
1296            {
1297                work.push(*node);
1298            }
1299            if let crate::SLTLoopBound::Expr(node) | crate::SLTLoopBound::TypedExpr { node, .. } =
1300                end
1301            {
1302                work.push(*node);
1303            }
1304            if let crate::SLTForFoldResult::Transient { initial, update } = result {
1305                work.push(*initial);
1306                work.push(*update);
1307            }
1308            work.extend(initials.iter().map(|state| state.expr));
1309            work.extend(updates.iter().map(|state| state.expr));
1310            for effect in effects {
1311                match effect {
1312                    crate::SLTForEffect::Event { guard, args, .. } => {
1313                        work.extend(*guard);
1314                        work.extend(args.iter().copied());
1315                    }
1316                    crate::SLTForEffect::Runner(runner) => work.push(*runner),
1317                }
1318            }
1319            work.push(*continue_cond);
1320        }
1321        SLTNode::ForFoldGroup {
1322            entry_guard,
1323            states,
1324            ..
1325        } => {
1326            work.push(*entry_guard);
1327            for state in states {
1328                work.push(state.initial);
1329                work.push(state.update);
1330            }
1331        }
1332    }
1333}
1334
1335#[derive(Clone, PartialEq, Eq, Hash)]
1336enum NormalizedIndexExpr<Addr: Clone + Eq + Hash> {
1337    LoopValue {
1338        signed: bool,
1339        access: BitAccess,
1340    },
1341    Input {
1342        variable: Addr,
1343        signed: bool,
1344        access: BitAccess,
1345        index: Vec<(NormalizedIndexExpr<Addr>, usize)>,
1346    },
1347    Constant(num_bigint::BigUint, num_bigint::BigUint, usize, bool),
1348    Binary(
1349        Box<NormalizedIndexExpr<Addr>>,
1350        BinaryOp,
1351        Box<NormalizedIndexExpr<Addr>>,
1352    ),
1353    Unary(UnaryOp, Box<NormalizedIndexExpr<Addr>>),
1354    Mux {
1355        cond: Box<NormalizedIndexExpr<Addr>>,
1356        then_expr: Box<NormalizedIndexExpr<Addr>>,
1357        else_expr: Box<NormalizedIndexExpr<Addr>>,
1358    },
1359    Concat(Vec<(NormalizedIndexExpr<Addr>, usize)>),
1360    Slice(Box<NormalizedIndexExpr<Addr>>, BitAccess),
1361}
1362
1363impl<Addr: Clone + Eq + Hash> NormalizedIndexExpr<Addr> {
1364    fn contains_loop_value(&self) -> bool {
1365        match self {
1366            Self::LoopValue { .. } => true,
1367            Self::Input { index, .. } | Self::Concat(index) => {
1368                index.iter().any(|(expr, _)| expr.contains_loop_value())
1369            }
1370            Self::Constant(..) => false,
1371            Self::Binary(lhs, _, rhs) => lhs.contains_loop_value() || rhs.contains_loop_value(),
1372            Self::Unary(_, inner) | Self::Slice(inner, _) => inner.contains_loop_value(),
1373            Self::Mux {
1374                cond,
1375                then_expr,
1376                else_expr,
1377            } => {
1378                cond.contains_loop_value()
1379                    || then_expr.contains_loop_value()
1380                    || else_expr.contains_loop_value()
1381            }
1382        }
1383    }
1384
1385    fn operation_cost(&self) -> u128 {
1386        match self {
1387            Self::LoopValue { .. } | Self::Constant(..) => 0,
1388            Self::Input { index, .. } => 4u128.saturating_add(
1389                index
1390                    .iter()
1391                    .map(|(expr, _)| 2u128.saturating_add(expr.operation_cost()))
1392                    .sum(),
1393            ),
1394            Self::Binary(lhs, _, rhs) => 1u128
1395                .saturating_add(lhs.operation_cost())
1396                .saturating_add(rhs.operation_cost()),
1397            Self::Unary(_, inner) | Self::Slice(inner, _) => {
1398                1u128.saturating_add(inner.operation_cost())
1399            }
1400            Self::Mux {
1401                cond,
1402                then_expr,
1403                else_expr,
1404            } => 1u128
1405                .saturating_add(cond.operation_cost())
1406                .saturating_add(then_expr.operation_cost())
1407                .saturating_add(else_expr.operation_cost()),
1408            Self::Concat(parts) => parts.iter().fold(1u128, |cost, (part, _)| {
1409                cost.saturating_add(part.operation_cost())
1410            }),
1411        }
1412    }
1413}
1414
1415fn normalize_index_expr<Addr: Clone + Eq + Hash + Copy>(
1416    node: NodeId,
1417    loop_var: Addr,
1418    arena: &SLTNodeArena<Addr>,
1419    memo: &mut HashMap<NodeId, Option<NormalizedIndexExpr<Addr>>>,
1420) -> Option<NormalizedIndexExpr<Addr>> {
1421    if let Some(found) = memo.get(&node) {
1422        return found.clone();
1423    }
1424    let normalized = match arena.get(node) {
1425        SLTNode::Input {
1426            variable,
1427            signed,
1428            index,
1429            access,
1430        } if *variable == loop_var => {
1431            if !index.is_empty() {
1432                None
1433            } else {
1434                Some(NormalizedIndexExpr::LoopValue {
1435                    signed: *signed,
1436                    access: *access,
1437                })
1438            }
1439        }
1440        SLTNode::Input {
1441            variable,
1442            signed,
1443            index,
1444            access,
1445        } => Some(NormalizedIndexExpr::Input {
1446            variable: *variable,
1447            signed: *signed,
1448            access: *access,
1449            index: index
1450                .iter()
1451                .map(|entry| {
1452                    normalize_index_expr(entry.node, loop_var, arena, memo)
1453                        .map(|node| (node, entry.stride))
1454                })
1455                .collect::<Option<Vec<_>>>()?,
1456        }),
1457        SLTNode::Constant(payload, mask, width, signed) => Some(NormalizedIndexExpr::Constant(
1458            payload.clone(),
1459            mask.clone(),
1460            *width,
1461            *signed,
1462        )),
1463        SLTNode::Binary(lhs, op, rhs) => Some(NormalizedIndexExpr::Binary(
1464            Box::new(normalize_index_expr(*lhs, loop_var, arena, memo)?),
1465            *op,
1466            Box::new(normalize_index_expr(*rhs, loop_var, arena, memo)?),
1467        )),
1468        SLTNode::Unary(op, inner) => Some(NormalizedIndexExpr::Unary(
1469            *op,
1470            Box::new(normalize_index_expr(*inner, loop_var, arena, memo)?),
1471        )),
1472        SLTNode::Capture { expr, .. } => normalize_index_expr(*expr, loop_var, arena, memo),
1473        SLTNode::Mux {
1474            cond,
1475            then_expr,
1476            else_expr,
1477        } => Some(NormalizedIndexExpr::Mux {
1478            cond: Box::new(normalize_index_expr(*cond, loop_var, arena, memo)?),
1479            then_expr: Box::new(normalize_index_expr(*then_expr, loop_var, arena, memo)?),
1480            else_expr: Box::new(normalize_index_expr(*else_expr, loop_var, arena, memo)?),
1481        }),
1482        SLTNode::Concat(parts) => Some(NormalizedIndexExpr::Concat(
1483            parts
1484                .iter()
1485                .map(|(part, width)| {
1486                    normalize_index_expr(*part, loop_var, arena, memo).map(|part| (part, *width))
1487                })
1488                .collect::<Option<Vec<_>>>()?,
1489        )),
1490        SLTNode::Slice { expr, access } => Some(NormalizedIndexExpr::Slice(
1491            Box::new(normalize_index_expr(*expr, loop_var, arena, memo)?),
1492            *access,
1493        )),
1494        SLTNode::ForFold { .. } | SLTNode::ForFoldGroup { .. } => None,
1495    };
1496    memo.insert(node, normalized.clone());
1497    normalized
1498}
1499
1500#[derive(Clone, PartialEq, Eq, Hash)]
1501struct ExactIndexedLoadKey<Addr: Clone + Eq + Hash> {
1502    base: Addr,
1503    access: BitAccess,
1504    index: Vec<(NormalizedIndexExpr<Addr>, usize)>,
1505}
1506
1507impl<Addr: Clone + Eq + Hash> ExactIndexedLoadKey<Addr> {
1508    fn saved_runtime_cost(&self) -> u128 {
1509        let width = self.access.msb - self.access.lsb + 1;
1510        let chunks = width.div_ceil(64) as u128;
1511        let address_cost = self.index.iter().fold(1u128, |cost, (expr, _)| {
1512            cost.saturating_add(2).saturating_add(expr.operation_cost())
1513        });
1514        6u128.saturating_mul(chunks).saturating_add(address_cost)
1515    }
1516}
1517
1518#[derive(Clone)]
1519struct FoldGroupReadFacts<Addr: Clone + Eq + Hash> {
1520    loop_var: Addr,
1521    state_targets: Vec<VarAtomBase<Addr>>,
1522    guard_reads: Vec<SchedulerInputRead<Addr>>,
1523    initial_reads: Vec<SchedulerInputRead<Addr>>,
1524    update_reads: Vec<SchedulerInputRead<Addr>>,
1525    indexed_loads: HashSet<ExactIndexedLoadKey<Addr>>,
1526    carried_chunks: u128,
1527}
1528
1529#[derive(Clone)]
1530struct ExactFoldGroup<Addr: Clone + Eq + Hash> {
1531    root: NodeId,
1532    facts: FoldGroupReadFacts<Addr>,
1533}
1534
1535struct FoldGroupScheduleInfo<Addr: Clone + Eq + Hash> {
1536    projection_paths: Vec<usize>,
1537    read_facts: Option<FoldGroupReadFacts<Addr>>,
1538    exact_and_exclusive: bool,
1539}
1540
1541struct FoldGroupScheduleIndex<Addr: Clone + Eq + Hash> {
1542    groups: BTreeMap<NodeId, FoldGroupScheduleInfo<Addr>>,
1543    direct_group_by_path: Vec<Option<NodeId>>,
1544}
1545
1546fn collect_reachable_scheduled_groups<Addr: Clone + Eq + Hash>(
1547    root: NodeId,
1548    arena: &SLTNodeArena<Addr>,
1549    reaches_scheduled_group: &[bool],
1550    scheduled_groups: &HashSet<NodeId>,
1551    result: &mut HashSet<NodeId>,
1552) {
1553    if !reaches_scheduled_group[root.0] {
1554        return;
1555    }
1556    let mut visited = HashSet::default();
1557    let mut work = vec![root];
1558    let mut children = Vec::new();
1559    while let Some(node) = work.pop() {
1560        if !visited.insert(node) || !reaches_scheduled_group[node.0] {
1561            continue;
1562        }
1563        if scheduled_groups.contains(&node) {
1564            result.insert(node);
1565        }
1566        children.clear();
1567        push_scheduler_node_children(node, arena, &mut children);
1568        work.extend(
1569            children
1570                .iter()
1571                .copied()
1572                .filter(|child| reaches_scheduled_group[child.0]),
1573        );
1574    }
1575}
1576
1577fn exact_fold_group_paths<Addr: Clone + Eq + Hash + Copy>(
1578    group: NodeId,
1579    paths: &[usize],
1580    input: &[LogicPath<Addr>],
1581    arena: &SLTNodeArena<Addr>,
1582) -> bool {
1583    let SLTNode::ForFoldGroup { states, .. } = arena.get(group) else {
1584        return false;
1585    };
1586    let Some(packed_accesses) = packed_fold_group_state_accesses(states) else {
1587        return false;
1588    };
1589    let mut covered = vec![Vec::<BitAccess>::new(); states.len()];
1590
1591    for &path_index in paths {
1592        let path = &input[path_index];
1593        if !path.local_inputs.is_empty() || !path.pre_lower_nodes.is_empty() {
1594            return false;
1595        }
1596        let Some(target) = path.target.var() else {
1597            return false;
1598        };
1599        let Some(projection) = fold_group_projection_access(path.expr, group, arena) else {
1600            return false;
1601        };
1602        let matches = states
1603            .iter()
1604            .zip(&packed_accesses)
1605            .enumerate()
1606            .filter_map(|(state_index, (state, packed))| {
1607                if target.id != state.target.id
1608                    || target.access.lsb < state.target.access.lsb
1609                    || target.access.msb > state.target.access.msb
1610                {
1611                    return None;
1612                }
1613                let relative_lsb = target.access.lsb - state.target.access.lsb;
1614                let relative_msb = target.access.msb - state.target.access.lsb;
1615                let expected = BitAccess::new(
1616                    packed.lsb.checked_add(relative_lsb)?,
1617                    packed.lsb.checked_add(relative_msb)?,
1618                );
1619                (projection == expected).then_some(state_index)
1620            })
1621            .collect::<Vec<_>>();
1622        if matches.len() != 1 {
1623            return false;
1624        }
1625        covered[matches[0]].push(target.access);
1626    }
1627
1628    states.iter().zip(&mut covered).all(|(state, ranges)| {
1629        ranges.sort_unstable_by_key(|range| (range.lsb, range.msb));
1630        let mut next = state.target.access.lsb;
1631        for range in ranges.iter() {
1632            if range.lsb != next || range.msb > state.target.access.msb {
1633                return false;
1634            }
1635            let Some(after) = range.msb.checked_add(1) else {
1636                return range.msb == state.target.access.msb;
1637            };
1638            next = after;
1639        }
1640        next == state.target.access.msb.saturating_add(1)
1641    })
1642}
1643
1644fn build_fold_group_schedule_index<Addr: Clone + Eq + Ord + Hash + Copy>(
1645    input: &[LogicPath<Addr>],
1646    arena: &SLTNodeArena<Addr>,
1647) -> FoldGroupScheduleIndex<Addr> {
1648    let direct_group_by_path = input
1649        .iter()
1650        .map(|path| projected_for_fold_group(path.expr, arena))
1651        .collect::<Vec<_>>();
1652    let direct_roots = direct_group_by_path
1653        .iter()
1654        .flatten()
1655        .copied()
1656        .collect::<HashSet<_>>();
1657    if direct_roots.is_empty() {
1658        return FoldGroupScheduleIndex {
1659            groups: BTreeMap::new(),
1660            direct_group_by_path,
1661        };
1662    }
1663
1664    // SLT children always precede their owners. Compute one boolean per node,
1665    // then traverse only semantic roots that can actually reach a scheduled
1666    // fold. This avoids carrying a root set through the entire arena.
1667    let mut reaches_scheduled_group = Vec::<bool>::with_capacity(arena.len());
1668    let mut children = Vec::new();
1669    for raw in 0..arena.len() {
1670        let node = NodeId(raw);
1671        children.clear();
1672        push_scheduler_node_children(node, arena, &mut children);
1673        reaches_scheduled_group.push(
1674            direct_roots.contains(&node)
1675                || children
1676                    .iter()
1677                    .any(|child| reaches_scheduled_group[child.0]),
1678        );
1679    }
1680
1681    let mut groups = BTreeMap::<NodeId, FoldGroupScheduleInfo<Addr>>::new();
1682    for (path_index, path) in input.iter().enumerate() {
1683        let direct = direct_group_by_path[path_index];
1684        if let Some(root) = direct {
1685            groups
1686                .entry(root)
1687                .or_insert_with(|| FoldGroupScheduleInfo {
1688                    projection_paths: Vec::new(),
1689                    read_facts: None,
1690                    exact_and_exclusive: true,
1691                })
1692                .projection_paths
1693                .push(path_index);
1694        }
1695
1696        let mut reached = HashSet::default();
1697        collect_reachable_scheduled_groups(
1698            path.expr,
1699            arena,
1700            &reaches_scheduled_group,
1701            &direct_roots,
1702            &mut reached,
1703        );
1704        for root in reached.drain() {
1705            let info = groups.entry(root).or_insert_with(|| FoldGroupScheduleInfo {
1706                projection_paths: Vec::new(),
1707                read_facts: None,
1708                exact_and_exclusive: true,
1709            });
1710            if direct != Some(root) {
1711                info.exact_and_exclusive = false;
1712            }
1713        }
1714        let auxiliary_roots = path
1715            .local_inputs
1716            .iter()
1717            .map(|(_, node)| *node)
1718            .chain(path.pre_lower_nodes.iter().copied())
1719            .chain(match &path.target {
1720                LogicPathTarget::Var(_) => Vec::new(),
1721                LogicPathTarget::CombCaptureEvent {
1722                    guard,
1723                    args,
1724                    loop_runner,
1725                    ..
1726                } => guard
1727                    .iter()
1728                    .chain(args)
1729                    .chain(loop_runner)
1730                    .copied()
1731                    .collect(),
1732            });
1733        for node in auxiliary_roots {
1734            reached.clear();
1735            collect_reachable_scheduled_groups(
1736                node,
1737                arena,
1738                &reaches_scheduled_group,
1739                &direct_roots,
1740                &mut reached,
1741            );
1742            for root in reached.drain() {
1743                groups
1744                    .entry(root)
1745                    .or_insert_with(|| FoldGroupScheduleInfo {
1746                        projection_paths: Vec::new(),
1747                        read_facts: None,
1748                        exact_and_exclusive: true,
1749                    })
1750                    .exact_and_exclusive = false;
1751            }
1752        }
1753    }
1754
1755    for (&root, info) in &mut groups {
1756        info.exact_and_exclusive &= !info.projection_paths.is_empty()
1757            && exact_fold_group_paths(root, &info.projection_paths, input, arena);
1758        if info.exact_and_exclusive {
1759            info.read_facts = collect_fold_group_read_facts(root, arena);
1760            info.exact_and_exclusive &= info.read_facts.is_some();
1761        }
1762    }
1763
1764    FoldGroupScheduleIndex {
1765        groups,
1766        direct_group_by_path,
1767    }
1768}
1769
1770fn discover_exact_fold_groups<Addr: Clone + Eq + Ord + Hash + Copy>(
1771    indices: &[usize],
1772    schedule_index: &FoldGroupScheduleIndex<Addr>,
1773) -> Vec<ExactFoldGroup<Addr>> {
1774    let scheduled_indices = indices.iter().copied().collect::<HashSet<_>>();
1775    let mut roots = indices
1776        .iter()
1777        .filter_map(|&index| schedule_index.direct_group_by_path[index])
1778        .collect::<Vec<_>>();
1779    roots.sort_unstable();
1780    roots.dedup();
1781
1782    roots
1783        .into_iter()
1784        .filter_map(|root| {
1785            let info = schedule_index.groups.get(&root)?;
1786            if !info.exact_and_exclusive
1787                || info
1788                    .projection_paths
1789                    .iter()
1790                    .any(|index| !scheduled_indices.contains(index))
1791            {
1792                return None;
1793            }
1794            Some(ExactFoldGroup {
1795                root,
1796                facts: info.read_facts.clone()?,
1797            })
1798        })
1799        .collect()
1800}
1801
1802fn same_fold_group_domain<Addr: Clone + Eq + Hash>(
1803    lhs: NodeId,
1804    rhs: NodeId,
1805    arena: &SLTNodeArena<Addr>,
1806) -> bool {
1807    let SLTNode::ForFoldGroup {
1808        loop_width: lhs_width,
1809        loop_signed: lhs_signed,
1810        start: lhs_start,
1811        step: lhs_step,
1812        trip_count: lhs_count,
1813        entry_guard: lhs_guard,
1814        ..
1815    } = arena.get(lhs)
1816    else {
1817        return false;
1818    };
1819    let SLTNode::ForFoldGroup {
1820        loop_width: rhs_width,
1821        loop_signed: rhs_signed,
1822        start: rhs_start,
1823        step: rhs_step,
1824        trip_count: rhs_count,
1825        entry_guard: rhs_guard,
1826        ..
1827    } = arena.get(rhs)
1828    else {
1829        return false;
1830    };
1831    lhs_width == rhs_width
1832        && lhs_signed == rhs_signed
1833        && lhs_start == rhs_start
1834        && lhs_step == rhs_step
1835        && lhs_count == rhs_count
1836        && lhs_guard == rhs_guard
1837}
1838
1839#[derive(Clone, Copy)]
1840struct SchedulerInputRead<Addr> {
1841    id: Addr,
1842    access: BitAccess,
1843    indexed: bool,
1844}
1845
1846fn collect_scheduler_plain_reads<Addr: Clone + Eq + Hash + Copy>(
1847    root: NodeId,
1848    arena: &SLTNodeArena<Addr>,
1849    reads: &mut Vec<SchedulerInputRead<Addr>>,
1850) -> bool {
1851    let mut visited = HashSet::default();
1852    let mut work = vec![root];
1853    while let Some(node) = work.pop() {
1854        if !visited.insert(node) {
1855            continue;
1856        }
1857        match arena.get(node) {
1858            SLTNode::Input {
1859                variable,
1860                index,
1861                access,
1862                ..
1863            } => {
1864                reads.push(SchedulerInputRead {
1865                    id: *variable,
1866                    access: *access,
1867                    indexed: !index.is_empty(),
1868                });
1869                work.extend(index.iter().map(|entry| entry.node));
1870            }
1871            SLTNode::ForFold { .. } | SLTNode::ForFoldGroup { .. } => return false,
1872            _ => push_scheduler_node_children(node, arena, &mut work),
1873        }
1874    }
1875    true
1876}
1877
1878fn collect_fold_group_read_facts<Addr: Clone + Eq + Hash + Copy>(
1879    root: NodeId,
1880    arena: &SLTNodeArena<Addr>,
1881) -> Option<FoldGroupReadFacts<Addr>> {
1882    let SLTNode::ForFoldGroup {
1883        loop_var,
1884        entry_guard,
1885        states,
1886        ..
1887    } = arena.get(root)
1888    else {
1889        return None;
1890    };
1891
1892    let mut guard_reads = Vec::new();
1893    collect_scheduler_plain_reads(*entry_guard, arena, &mut guard_reads).then_some(())?;
1894    let mut initial_reads = Vec::new();
1895    let mut update_reads = Vec::new();
1896    for state in states {
1897        collect_scheduler_plain_reads(state.initial, arena, &mut initial_reads).then_some(())?;
1898        collect_scheduler_plain_reads(state.update, arena, &mut update_reads).then_some(())?;
1899    }
1900
1901    let state_ids = states
1902        .iter()
1903        .map(|state| state.target.id)
1904        .collect::<HashSet<_>>();
1905    let mut indexed_loads = HashSet::default();
1906    let mut normalize_memo = HashMap::default();
1907    let mut visited = HashSet::default();
1908    let mut work = states.iter().map(|state| state.update).collect::<Vec<_>>();
1909    while let Some(node) = work.pop() {
1910        if !visited.insert(node) {
1911            continue;
1912        }
1913        match arena.get(node) {
1914            SLTNode::Input {
1915                variable,
1916                index,
1917                access,
1918                ..
1919            } => {
1920                if *variable != *loop_var && !state_ids.contains(variable) && !index.is_empty() {
1921                    let normalized = index
1922                        .iter()
1923                        .map(|entry| {
1924                            normalize_index_expr(entry.node, *loop_var, arena, &mut normalize_memo)
1925                                .map(|node| (node, entry.stride))
1926                        })
1927                        .collect::<Option<Vec<_>>>();
1928                    if let Some(normalized) = normalized
1929                        && normalized
1930                            .iter()
1931                            .any(|(expr, _)| expr.contains_loop_value())
1932                    {
1933                        indexed_loads.insert(ExactIndexedLoadKey {
1934                            base: *variable,
1935                            access: *access,
1936                            index: normalized,
1937                        });
1938                    }
1939                }
1940                work.extend(index.iter().map(|entry| entry.node));
1941            }
1942            _ => push_scheduler_node_children(node, arena, &mut work),
1943        }
1944    }
1945
1946    let carried_chunks = states.iter().fold(0u128, |chunks, state| {
1947        let width = state.target.access.msb - state.target.access.lsb + 1;
1948        chunks.saturating_add(width.div_ceil(64) as u128)
1949    });
1950    let mut state_ranges: HashMap<Addr, Vec<BitAccess>> = HashMap::default();
1951    for state in states {
1952        if state.target.id == *loop_var
1953            || state_ranges
1954                .entry(state.target.id)
1955                .or_default()
1956                .iter()
1957                .any(|range| range.overlaps(&state.target.access))
1958        {
1959            return None;
1960        }
1961        state_ranges
1962            .entry(state.target.id)
1963            .or_default()
1964            .push(state.target.access);
1965    }
1966    if guard_reads.iter().any(|read| read.id == *loop_var)
1967        || initial_reads.iter().any(|read| read.id == *loop_var)
1968        || update_reads
1969            .iter()
1970            .any(|read| read.id == *loop_var && read.indexed)
1971    {
1972        return None;
1973    }
1974    Some(FoldGroupReadFacts {
1975        loop_var: *loop_var,
1976        state_targets: states.iter().map(|state| state.target).collect(),
1977        guard_reads,
1978        initial_reads,
1979        update_reads,
1980        indexed_loads,
1981        carried_chunks,
1982    })
1983}
1984
1985fn scheduler_read_overlaps_targets<Addr: Clone + Eq + Hash>(
1986    read: &SchedulerInputRead<Addr>,
1987    targets: &[VarAtomBase<Addr>],
1988) -> bool {
1989    targets.iter().any(|target| {
1990        target.id == read.id && (read.indexed || target.access.overlaps(&read.access))
1991    })
1992}
1993
1994fn fold_groups_are_pairwise_independent<Addr: Clone + Eq + Hash + Copy>(
1995    lhs: &FoldGroupReadFacts<Addr>,
1996    rhs: &FoldGroupReadFacts<Addr>,
1997) -> bool {
1998    if lhs.loop_var == rhs.loop_var
1999        || lhs
2000            .state_targets
2001            .iter()
2002            .any(|target| target.id == rhs.loop_var)
2003        || rhs
2004            .state_targets
2005            .iter()
2006            .any(|target| target.id == lhs.loop_var)
2007        || lhs.state_targets.iter().any(|left| {
2008            rhs.state_targets
2009                .iter()
2010                .any(|right| left.id == right.id && left.access.overlaps(&right.access))
2011        })
2012    {
2013        return false;
2014    }
2015
2016    let all_targets = lhs
2017        .state_targets
2018        .iter()
2019        .chain(&rhs.state_targets)
2020        .copied()
2021        .collect::<Vec<_>>();
2022    let guard_is_independent = lhs.guard_reads.iter().chain(&rhs.guard_reads).all(|read| {
2023        read.id != lhs.loop_var
2024            && read.id != rhs.loop_var
2025            && !scheduler_read_overlaps_targets(read, &all_targets)
2026    });
2027    let initials_are_independent = lhs
2028        .initial_reads
2029        .iter()
2030        .chain(&rhs.initial_reads)
2031        .all(|read| read.id != lhs.loop_var && read.id != rhs.loop_var);
2032    let lhs_update_is_independent = lhs.update_reads.iter().all(|read| {
2033        read.id != rhs.loop_var && !scheduler_read_overlaps_targets(read, &rhs.state_targets)
2034    });
2035    let rhs_update_is_independent = rhs.update_reads.iter().all(|read| {
2036        read.id != lhs.loop_var && !scheduler_read_overlaps_targets(read, &lhs.state_targets)
2037    });
2038    guard_is_independent
2039        && initials_are_independent
2040        && lhs_update_is_independent
2041        && rhs_update_is_independent
2042}
2043
2044fn fold_groups_share_exact_load<Addr: Clone + Eq + Hash>(
2045    lhs: &FoldGroupReadFacts<Addr>,
2046    rhs: &FoldGroupReadFacts<Addr>,
2047) -> bool {
2048    let (small, large) = if lhs.indexed_loads.len() <= rhs.indexed_loads.len() {
2049        (&lhs.indexed_loads, &rhs.indexed_loads)
2050    } else {
2051        (&rhs.indexed_loads, &lhs.indexed_loads)
2052    };
2053    small.iter().any(|key| large.contains(key))
2054}
2055
2056#[derive(Clone)]
2057struct WeightedFoldFamily {
2058    members: Vec<usize>,
2059    benefit: u128,
2060    pressure: u128,
2061}
2062
2063impl WeightedFoldFamily {
2064    fn is_positive(&self) -> bool {
2065        self.members.len() >= 2 && self.benefit > self.pressure
2066    }
2067
2068    fn cmp_net(&self, other: &Self) -> std::cmp::Ordering {
2069        self.benefit
2070            .saturating_add(other.pressure)
2071            .cmp(&other.benefit.saturating_add(self.pressure))
2072    }
2073}
2074
2075fn weighted_fold_family<Addr: Clone + Eq + Hash>(
2076    members: &[usize],
2077    candidates: &[ExactFoldGroup<Addr>],
2078    four_state: bool,
2079) -> WeightedFoldFamily {
2080    const SAVED_LOOP_CONTROL_COST: u128 = 6;
2081    const CARRIED_CHUNK_PRESSURE_COST: u128 = 4;
2082
2083    let mut users_by_load = HashMap::<ExactIndexedLoadKey<Addr>, usize>::default();
2084    let mut total_chunks = 0u128;
2085    let mut largest_separate_group = 0u128;
2086    for &member in members {
2087        let facts = &candidates[member].facts;
2088        total_chunks = total_chunks.saturating_add(facts.carried_chunks);
2089        largest_separate_group = largest_separate_group.max(facts.carried_chunks);
2090        for key in &facts.indexed_loads {
2091            *users_by_load.entry(key.clone()).or_insert(0) += 1;
2092        }
2093    }
2094    let load_benefit = users_by_load
2095        .into_iter()
2096        .filter(|(_, users)| *users >= 2)
2097        .fold(0u128, |benefit, (key, users)| {
2098            benefit.saturating_add(key.saved_runtime_cost().saturating_mul((users - 1) as u128))
2099        });
2100    let control_benefit =
2101        SAVED_LOOP_CONTROL_COST.saturating_mul(members.len().saturating_sub(1) as u128);
2102    let state_multiplier = if four_state { 2 } else { 1 };
2103    let pressure = total_chunks
2104        .saturating_sub(largest_separate_group)
2105        .saturating_mul(state_multiplier)
2106        .saturating_mul(CARRIED_CHUNK_PRESSURE_COST);
2107    WeightedFoldFamily {
2108        members: members.to_vec(),
2109        benefit: load_benefit.saturating_add(control_benefit),
2110        pressure,
2111    }
2112}
2113
2114fn family_signature<Addr: Clone + Eq + Hash>(
2115    family: &WeightedFoldFamily,
2116    candidates: &[ExactFoldGroup<Addr>],
2117) -> Vec<NodeId> {
2118    let mut roots = family
2119        .members
2120        .iter()
2121        .map(|member| candidates[*member].root)
2122        .collect::<Vec<_>>();
2123    roots.sort_unstable();
2124    roots
2125}
2126
2127fn better_weighted_family<Addr: Clone + Eq + Hash>(
2128    candidate: &WeightedFoldFamily,
2129    current: &WeightedFoldFamily,
2130    groups: &[ExactFoldGroup<Addr>],
2131) -> bool {
2132    candidate.cmp_net(current).is_gt()
2133        || candidate.cmp_net(current).is_eq()
2134            && family_signature(candidate, groups) < family_signature(current, groups)
2135}
2136
2137fn grow_weighted_family<Addr: Clone + Eq + Hash>(
2138    seed: usize,
2139    first: usize,
2140    candidates: &[ExactFoldGroup<Addr>],
2141    available: &[bool],
2142    compatible: &[Vec<bool>],
2143    shared_load: &[Vec<bool>],
2144    rejected: &HashSet<Vec<NodeId>>,
2145    four_state: bool,
2146) -> Option<WeightedFoldFamily> {
2147    let mut members = vec![seed, first];
2148    let mut best = None;
2149    loop {
2150        let weighted = weighted_fold_family(&members, candidates, four_state);
2151        let signature = family_signature(&weighted, candidates);
2152        if weighted.is_positive()
2153            && !rejected.contains(&signature)
2154            && best
2155                .as_ref()
2156                .is_none_or(|current| better_weighted_family(&weighted, current, candidates))
2157        {
2158            best = Some(weighted);
2159        }
2160
2161        let mut best_expansion: Option<(usize, WeightedFoldFamily)> = None;
2162        for candidate in 0..candidates.len() {
2163            if !available[candidate]
2164                || members.contains(&candidate)
2165                || !members.iter().all(|member| compatible[*member][candidate])
2166                || !members.iter().any(|member| shared_load[*member][candidate])
2167            {
2168                continue;
2169            }
2170            let mut expanded = members.clone();
2171            expanded.push(candidate);
2172            let weighted = weighted_fold_family(&expanded, candidates, four_state);
2173            if best_expansion
2174                .as_ref()
2175                .is_none_or(|(current_index, current)| {
2176                    better_weighted_family(&weighted, current, candidates)
2177                        || weighted.cmp_net(current).is_eq()
2178                            && candidates[candidate].root < candidates[*current_index].root
2179                })
2180            {
2181                best_expansion = Some((candidate, weighted));
2182            }
2183        }
2184        let Some((next, _)) = best_expansion else {
2185            break;
2186        };
2187        members.push(next);
2188    }
2189    best
2190}
2191
2192fn best_weighted_fold_family<Addr: Clone + Eq + Hash>(
2193    candidates: &[ExactFoldGroup<Addr>],
2194    available: &[bool],
2195    compatible: &[Vec<bool>],
2196    shared_load: &[Vec<bool>],
2197    rejected: &HashSet<Vec<NodeId>>,
2198    four_state: bool,
2199) -> Option<WeightedFoldFamily> {
2200    let mut best = None;
2201    for seed in 0..candidates.len() {
2202        if !available[seed] {
2203            continue;
2204        }
2205        // Force every compatible first edge from every seed. The subsequent
2206        // growth is weighted, so a low-root conflicting candidate cannot hide
2207        // a more profitable compatible partition.
2208        for first in 0..candidates.len() {
2209            if seed == first
2210                || !available[first]
2211                || !compatible[seed][first]
2212                || !shared_load[seed][first]
2213            {
2214                continue;
2215            }
2216            let Some(family) = grow_weighted_family(
2217                seed,
2218                first,
2219                candidates,
2220                available,
2221                compatible,
2222                shared_load,
2223                rejected,
2224                four_state,
2225            ) else {
2226                continue;
2227            };
2228            if best
2229                .as_ref()
2230                .is_none_or(|current| better_weighted_family(&family, current, candidates))
2231            {
2232                best = Some(family);
2233            }
2234        }
2235    }
2236    best
2237}
2238
2239fn jointly_lower_fold_group_families<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
2240    indices: &[usize],
2241    schedule_index: &FoldGroupScheduleIndex<Addr>,
2242    lowerer: &crate::SLTToSIRLowerer,
2243    builder: &mut SIRBuilder<Addr>,
2244    arena: &SLTNodeArena<Addr>,
2245    lower_cache: &mut HashMap<NodeId, RegisterId>,
2246    four_state: bool,
2247) -> HashSet<NodeId> {
2248    // `indices` is one bounded run of exact-fold roots. Direct dependencies
2249    // split runs before this point, and the fixed root window bounds the
2250    // pairwise compatibility matrix independently of design size.
2251    let candidates = discover_exact_fold_groups(indices, schedule_index);
2252    let mut compatible = vec![vec![false; candidates.len()]; candidates.len()];
2253    let mut shared_load = vec![vec![false; candidates.len()]; candidates.len()];
2254    for lhs in 0..candidates.len() {
2255        for rhs in lhs + 1..candidates.len() {
2256            let domains_match =
2257                same_fold_group_domain(candidates[lhs].root, candidates[rhs].root, arena);
2258            let independent = fold_groups_are_pairwise_independent(
2259                &candidates[lhs].facts,
2260                &candidates[rhs].facts,
2261            );
2262            let shares =
2263                fold_groups_share_exact_load(&candidates[lhs].facts, &candidates[rhs].facts);
2264            compatible[lhs][rhs] = domains_match && independent;
2265            compatible[rhs][lhs] = compatible[lhs][rhs];
2266            shared_load[lhs][rhs] = shares;
2267            shared_load[rhs][lhs] = shares;
2268        }
2269    }
2270
2271    let mut available = vec![true; candidates.len()];
2272    let mut rejected = HashSet::<Vec<NodeId>>::default();
2273    let mut lowered = HashSet::default();
2274    while let Some(family) = best_weighted_fold_family(
2275        &candidates,
2276        &available,
2277        &compatible,
2278        &shared_load,
2279        &rejected,
2280        four_state,
2281    ) {
2282        let roots = family
2283            .members
2284            .iter()
2285            .map(|member| candidates[*member].root)
2286            .collect::<Vec<_>>();
2287        if lowerer.lower_fold_groups_jointly(builder, &roots, arena, lower_cache) {
2288            for member in family.members {
2289                available[member] = false;
2290            }
2291            lowered.extend(roots);
2292        } else {
2293            rejected.insert(family_signature(&family, &candidates));
2294        }
2295    }
2296    lowered
2297}
2298
2299#[derive(Clone, Copy)]
2300struct PreparedFoldProjection {
2301    packed_result: RegisterId,
2302    access: BitAccess,
2303}
2304
2305/// Materialize each shared grouped fold once, but leave its projections
2306/// deferred.  A target Store may invalidate the ordinary lowering cache, so
2307/// the packed result is retained explicitly until every projection has been
2308/// consumed.  Each narrow projection is created immediately before its Store
2309/// instead of keeping all projected registers live simultaneously.
2310fn prepare_atomic_fold_group_results<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
2311    indices: &[usize],
2312    input: &[LogicPath<Addr>],
2313    fold_group_schedule_index: &FoldGroupScheduleIndex<Addr>,
2314    lowerer: &crate::SLTToSIRLowerer,
2315    builder: &mut SIRBuilder<Addr>,
2316    arena: &SLTNodeArena<Addr>,
2317    lower_cache: &mut HashMap<NodeId, RegisterId>,
2318    dep_memo: &mut HashMap<NodeId, HashSet<Addr>>,
2319    inverse_dep_memo: &mut HashMap<Addr, HashSet<NodeId>>,
2320    four_state: bool,
2321) -> HashMap<usize, PreparedFoldProjection> {
2322    let jointly_lowered = jointly_lower_fold_group_families(
2323        indices,
2324        fold_group_schedule_index,
2325        lowerer,
2326        builder,
2327        arena,
2328        lower_cache,
2329        four_state,
2330    );
2331    let mut counts = HashMap::default();
2332    for &idx in indices {
2333        let path = &input[idx];
2334        if path.target.var().is_none()
2335            || !path.local_inputs.is_empty()
2336            || !path.pre_lower_nodes.is_empty()
2337        {
2338            continue;
2339        }
2340        if let Some(group) = projected_for_fold_group(path.expr, arena) {
2341            *counts.entry(group).or_insert(0usize) += 1;
2342        }
2343    }
2344
2345    let mut prepared = HashMap::default();
2346    for &idx in indices {
2347        let path = &input[idx];
2348        if !path.local_inputs.is_empty() || !path.pre_lower_nodes.is_empty() {
2349            continue;
2350        }
2351        let Some(group) = projected_for_fold_group(path.expr, arena) else {
2352            continue;
2353        };
2354        if counts.get(&group).copied().unwrap_or(0) < 2 && !jointly_lowered.contains(&group) {
2355            continue;
2356        }
2357        let Some(access) = fold_group_projection_access(path.expr, group, arena) else {
2358            continue;
2359        };
2360        collect_logic_path_input_deps(path, arena, dep_memo, inverse_dep_memo);
2361        let packed_result = lower_cache
2362            .get(&group)
2363            .copied()
2364            .unwrap_or_else(|| lowerer.lower(builder, group, arena, lower_cache));
2365        prepared.insert(
2366            idx,
2367            PreparedFoldProjection {
2368                packed_result,
2369                access,
2370            },
2371        );
2372    }
2373    prepared
2374}
2375
2376#[derive(Error, Debug, PartialEq, Eq)]
2377pub enum SchedulerError<A: Display + Debug + Eq + Hash + Clone> {
2378    #[error("Combinational loop detected: {}", .blocks.iter().map(|v| format!("{}", v)).collect::<Vec<_>>().join(" -> "))]
2379    CombinationalLoop { blocks: Vec<LogicPath<A>> },
2380    #[error("Multiple driver detected: {}", .blocks.iter().map(|v| format!("{}", v)).collect::<Vec<_>>().join(","))]
2381    MultipleDriver { blocks: Vec<LogicPath<A>> },
2382    #[error("internal logic-path SCC condensation graph is invalid")]
2383    InvalidDependencyGraph,
2384}
2385
2386impl<A: Display + Debug + Eq + Hash + Clone> SchedulerError<A> {
2387    pub fn map_addr<B: Display + Debug + Eq + Hash + Clone, F>(
2388        self,
2389        arena: &SLTNodeArena<A>,
2390        target_arena: &mut SLTNodeArena<B>,
2391        f: &F,
2392    ) -> Result<SchedulerError<B>, SLTNodeFactsError>
2393    where
2394        F: Fn(&A) -> B,
2395    {
2396        let mut cache = HashMap::default();
2397        Ok(match self {
2398            SchedulerError::CombinationalLoop { blocks } => SchedulerError::CombinationalLoop {
2399                blocks: blocks
2400                    .into_iter()
2401                    .map(|b| b.map_addr(arena, target_arena, &mut cache, f))
2402                    .collect::<Result<Vec<_>, _>>()?,
2403            },
2404            SchedulerError::MultipleDriver { blocks } => SchedulerError::MultipleDriver {
2405                blocks: blocks
2406                    .into_iter()
2407                    .map(|b| b.map_addr(arena, target_arena, &mut cache, f))
2408                    .collect::<Result<Vec<_>, _>>()?,
2409            },
2410            SchedulerError::InvalidDependencyGraph => SchedulerError::InvalidDependencyGraph,
2411        })
2412    }
2413}
2414
2415pub struct ScheduleResult<Addr> {
2416    pub execution_units: Vec<ExecutionUnit<Addr>>,
2417    pub runtime_errors: HashMap<i64, RuntimeErrorInfo<Addr>>,
2418    /// Persistent-state ranges published directly by FF actions in the shared
2419    /// comb/FF schedule. These Stores are semantic state updates rather than
2420    /// disposable comb publications.
2421    pub direct_ff_writes: Vec<VarAtomBase<Addr>>,
2422}
2423
2424pub trait ClockFfLowering<Addr> {
2425    type Error;
2426
2427    fn summaries(&self) -> &[FfAccessSummary<Addr>];
2428    fn begin(
2429        &mut self,
2430        builder: &mut SIRBuilder<Addr>,
2431        direct_writes: &[Vec<VarAtomBase<Addr>>],
2432    ) -> Result<(), Self::Error>;
2433    fn lower(
2434        &mut self,
2435        index: usize,
2436        direct_writes: &[VarAtomBase<Addr>],
2437        builder: &mut SIRBuilder<Addr>,
2438    ) -> Result<(), Self::Error>;
2439    fn finish(
2440        &mut self,
2441        builder: &mut SIRBuilder<Addr>,
2442        direct_writes: &[Vec<VarAtomBase<Addr>>],
2443    ) -> Result<(), Self::Error>;
2444}
2445
2446pub enum ClockSortError<Addr: Display + Debug + Eq + Hash + Clone, E> {
2447    Scheduler(SchedulerError<Addr>),
2448    Lowering(E),
2449}
2450
2451enum ScheduledWork {
2452    CombPath(usize),
2453    CombScc(Vec<usize>),
2454    /// A dependency-ordered run of state publications selected by one SLT
2455    /// condition.  The path scheduler has already fixed the order; lowering
2456    /// only preserves the exclusivity which would otherwise become several
2457    /// independent Muxes and later need to be rediscovered from SIR.
2458    GuardedComb {
2459        condition: NodeId,
2460        paths: Vec<usize>,
2461    },
2462    Ff(usize),
2463}
2464
2465fn scheduled_root_mux<A: Clone + Eq + Hash>(
2466    path: &LogicPath<A>,
2467    arena: &SLTNodeArena<A>,
2468) -> Option<(NodeId, NodeId, NodeId)> {
2469    if path.target.var().is_none()
2470        || !path.local_inputs.is_empty()
2471        || !path.pre_lower_nodes.is_empty()
2472    {
2473        return None;
2474    }
2475    let SLTNode::Mux {
2476        cond,
2477        then_expr,
2478        else_expr,
2479    } = arena.get(path.expr)
2480    else {
2481        return None;
2482    };
2483    Some((*cond, *then_expr, *else_expr))
2484}
2485
2486fn collect_pure_scheduled_nodes<A: Clone + Eq + Hash>(
2487    root: NodeId,
2488    arena: &SLTNodeArena<A>,
2489    nodes: &mut HashSet<NodeId>,
2490) -> bool {
2491    if !nodes.insert(root) {
2492        return true;
2493    }
2494    match arena.get(root) {
2495        SLTNode::Input { index, .. } => index
2496            .iter()
2497            .all(|index| collect_pure_scheduled_nodes(index.node, arena, nodes)),
2498        SLTNode::Constant(..) => true,
2499        SLTNode::Binary(lhs, _, rhs) => {
2500            collect_pure_scheduled_nodes(*lhs, arena, nodes)
2501                && collect_pure_scheduled_nodes(*rhs, arena, nodes)
2502        }
2503        SLTNode::Unary(_, inner)
2504        | SLTNode::Capture { expr: inner, .. }
2505        | SLTNode::Slice { expr: inner, .. } => collect_pure_scheduled_nodes(*inner, arena, nodes),
2506        SLTNode::Mux {
2507            cond,
2508            then_expr,
2509            else_expr,
2510        } => {
2511            collect_pure_scheduled_nodes(*cond, arena, nodes)
2512                && collect_pure_scheduled_nodes(*then_expr, arena, nodes)
2513                && collect_pure_scheduled_nodes(*else_expr, arena, nodes)
2514        }
2515        SLTNode::Concat(parts) => parts
2516            .iter()
2517            .all(|(part, _)| collect_pure_scheduled_nodes(*part, arena, nodes)),
2518        SLTNode::ForFold { .. } | SLTNode::ForFoldGroup { .. } => false,
2519    }
2520}
2521
2522fn scheduled_node_cost<A: Clone + Eq + Hash>(node: NodeId, arena: &SLTNodeArena<A>) -> u128 {
2523    match arena.get(node) {
2524        SLTNode::Input { .. } | SLTNode::Constant(..) => 0,
2525        _ => crate::get_width(node, arena).div_ceil(64).max(1) as u128,
2526    }
2527}
2528
2529fn scheduled_condition_probability<A: Clone + Eq + Hash>(
2530    mut condition: NodeId,
2531    arena: &SLTNodeArena<A>,
2532) -> (u128, u128) {
2533    let mut inverted = false;
2534    loop {
2535        match arena.get(condition) {
2536            SLTNode::Unary(UnaryOp::Ident | UnaryOp::ToTwoState, inner) => condition = *inner,
2537            SLTNode::Unary(UnaryOp::LogicNot, inner) => {
2538                inverted = !inverted;
2539                condition = *inner;
2540            }
2541            _ => break,
2542        }
2543    }
2544    let constant = |node| matches!(arena.get(node), SLTNode::Constant(..));
2545    let equality = matches!(
2546        arena.get(condition),
2547        SLTNode::Binary(lhs, BinaryOp::Eq | BinaryOp::EqWildcard, rhs)
2548            if constant(*lhs) || constant(*rhs)
2549    );
2550    let inequality = matches!(
2551        arena.get(condition),
2552        SLTNode::Binary(lhs, BinaryOp::Ne | BinaryOp::NeWildcard, rhs)
2553            if constant(*lhs) || constant(*rhs)
2554    );
2555    let true_weight = if equality {
2556        1
2557    } else if inequality {
2558        4
2559    } else {
2560        return (1, 2);
2561    };
2562    if inverted {
2563        (5 - true_weight, 5)
2564    } else {
2565        (true_weight, 5)
2566    }
2567}
2568
2569fn scheduled_guard_region_is_profitable<A: Clone + Eq + Hash>(
2570    condition: NodeId,
2571    paths: &[usize],
2572    input: &[LogicPath<A>],
2573    arena: &SLTNodeArena<A>,
2574) -> bool {
2575    const CONTROL_COST: u128 = 2;
2576    const MISPREDICT_COST: u128 = 16;
2577
2578    let mut true_nodes = HashSet::default();
2579    let mut false_nodes = HashSet::default();
2580    for &path in paths {
2581        let Some((actual, then_expr, else_expr)) = scheduled_root_mux(&input[path], arena) else {
2582            return false;
2583        };
2584        if actual != condition
2585            || !collect_pure_scheduled_nodes(then_expr, arena, &mut true_nodes)
2586            || !collect_pure_scheduled_nodes(else_expr, arena, &mut false_nodes)
2587        {
2588            return false;
2589        }
2590    }
2591    let true_owned = true_nodes
2592        .difference(&false_nodes)
2593        .map(|node| scheduled_node_cost(*node, arena))
2594        .sum::<u128>();
2595    let false_owned = false_nodes
2596        .difference(&true_nodes)
2597        .map(|node| scheduled_node_cost(*node, arena))
2598        .sum::<u128>();
2599    let (true_weight, total_weight) = scheduled_condition_probability(condition, arena);
2600    let false_weight = total_weight - true_weight;
2601    let removed_mux_cost = total_weight.saturating_mul(paths.len() as u128);
2602    let saved = false_weight
2603        .saturating_mul(true_owned)
2604        .saturating_add(true_weight.saturating_mul(false_owned))
2605        .saturating_add(removed_mux_cost);
2606    let introduced = total_weight.saturating_mul(CONTROL_COST).saturating_add(
2607        true_weight
2608            .min(false_weight)
2609            .saturating_mul(MISPREDICT_COST),
2610    );
2611    saved > introduced
2612}
2613
2614fn condition_reads_target<A: Clone + Eq + Hash>(
2615    condition: NodeId,
2616    targets: &HashSet<A>,
2617    arena: &SLTNodeArena<A>,
2618) -> bool {
2619    let mut visited = HashSet::default();
2620    let mut work = vec![condition];
2621    while let Some(node) = work.pop() {
2622        if !visited.insert(node) {
2623            continue;
2624        }
2625        match arena.get(node) {
2626            SLTNode::Input {
2627                variable, index, ..
2628            } => {
2629                if targets.contains(variable) {
2630                    return true;
2631                }
2632                work.extend(index.iter().map(|index| index.node));
2633            }
2634            SLTNode::Constant(..) => {}
2635            SLTNode::Binary(lhs, _, rhs) => work.extend([*lhs, *rhs]),
2636            SLTNode::Unary(_, inner)
2637            | SLTNode::Capture { expr: inner, .. }
2638            | SLTNode::Slice { expr: inner, .. } => work.push(*inner),
2639            SLTNode::Mux {
2640                cond,
2641                then_expr,
2642                else_expr,
2643            } => work.extend([*cond, *then_expr, *else_expr]),
2644            SLTNode::Concat(parts) => work.extend(parts.iter().map(|(part, _)| *part)),
2645            SLTNode::ForFold { .. } | SLTNode::ForFoldGroup { .. } => return true,
2646        }
2647    }
2648    false
2649}
2650
2651fn form_scheduled_guard_regions<A: Clone + Eq + Hash>(
2652    work: Vec<ScheduledWork>,
2653    input: &[LogicPath<A>],
2654    arena: &SLTNodeArena<A>,
2655    four_state: bool,
2656) -> Vec<ScheduledWork> {
2657    if four_state {
2658        return work;
2659    }
2660    let mut result = Vec::with_capacity(work.len());
2661    let mut pending = work.into_iter().peekable();
2662    while let Some(item) = pending.next() {
2663        let ScheduledWork::CombPath(first_path) = item else {
2664            result.push(item);
2665            continue;
2666        };
2667        let Some((condition, _, _)) = scheduled_root_mux(&input[first_path], arena) else {
2668            result.push(ScheduledWork::CombPath(first_path));
2669            continue;
2670        };
2671
2672        let mut paths = vec![first_path];
2673        while let Some(ScheduledWork::CombPath(next_path)) = pending.peek() {
2674            if scheduled_root_mux(&input[*next_path], arena)
2675                .is_none_or(|(next_condition, _, _)| next_condition != condition)
2676            {
2677                break;
2678            }
2679            paths.push(*next_path);
2680            pending.next();
2681        }
2682
2683        let targets = paths
2684            .iter()
2685            .filter_map(|path| input[*path].target.var().map(|target| target.id.clone()))
2686            .collect::<HashSet<_>>();
2687        if paths.len() >= 2
2688            && !condition_reads_target(condition, &targets, arena)
2689            && scheduled_guard_region_is_profitable(condition, &paths, input, arena)
2690        {
2691            result.push(ScheduledWork::GuardedComb { condition, paths });
2692        } else {
2693            result.extend(paths.into_iter().map(ScheduledWork::CombPath));
2694        }
2695    }
2696    result
2697}
2698
2699/// Lower a consecutive set of exact grouped-fold paths.  The packed fold
2700/// results are computed atomically, then each projection is created and stored
2701/// in topological order.  Ordinary paths bypass this buffer entirely.
2702fn flush_pending_fold_paths<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
2703    pending: &mut Vec<usize>,
2704    input: &[LogicPath<Addr>],
2705    fold_group_schedule_index: &FoldGroupScheduleIndex<Addr>,
2706    lowerer: &crate::SLTToSIRLowerer,
2707    builder: &mut SIRBuilder<Addr>,
2708    arena: &SLTNodeArena<Addr>,
2709    lower_cache: &mut HashMap<NodeId, RegisterId>,
2710    dep_memo: &mut HashMap<NodeId, HashSet<Addr>>,
2711    inverse_dep_memo: &mut HashMap<Addr, HashSet<NodeId>>,
2712    unpacked_element_widths: &HashMap<Addr, usize>,
2713    four_state: bool,
2714) {
2715    if pending.is_empty() {
2716        return;
2717    }
2718
2719    let prepared_results = prepare_atomic_fold_group_results(
2720        pending,
2721        input,
2722        fold_group_schedule_index,
2723        lowerer,
2724        builder,
2725        arena,
2726        lower_cache,
2727        dep_memo,
2728        inverse_dep_memo,
2729        four_state,
2730    );
2731    for idx in pending.drain(..) {
2732        let path = &input[idx];
2733        collect_logic_path_input_deps(path, arena, dep_memo, inverse_dep_memo);
2734        let prepared_result = prepared_results.get(&idx).map(|projection| {
2735            lowerer.project_materialized(builder, projection.packed_result, projection.access)
2736        });
2737        emit_logic_path_store_with_result(
2738            lowerer,
2739            builder,
2740            path,
2741            arena,
2742            lower_cache,
2743            unpacked_element_widths,
2744            prepared_result,
2745        );
2746        invalidate_logic_path_target(path, inverse_dep_memo, lower_cache);
2747    }
2748}
2749
2750fn is_exact_fold_path<Addr: Clone + Eq + Hash + Copy>(
2751    path: usize,
2752    fold_groups: &FoldGroupScheduleIndex<Addr>,
2753) -> bool {
2754    if let Some(root) = fold_groups.direct_group_by_path[path]
2755        && fold_groups
2756            .groups
2757            .get(&root)
2758            .is_some_and(|info| info.exact_and_exclusive)
2759    {
2760        return true;
2761    }
2762    false
2763}
2764
2765/// Assign a scheduling domain to paths whose memory effects or packed fold
2766/// result benefit from staying adjacent.  A domain is only a ready-queue
2767/// preference: it never contracts paths into one graph node and therefore
2768/// cannot make a not-yet-ready path execute early.
2769fn logic_path_scheduling_domains<Addr: Clone + Eq + Ord + Hash + Copy>(
2770    input: &[LogicPath<Addr>],
2771    fold_groups: &FoldGroupScheduleIndex<Addr>,
2772) -> Vec<Option<usize>> {
2773    let mut next_domain = 0usize;
2774    let mut fold_domains = BTreeMap::<NodeId, usize>::new();
2775    let mut target_domains = BTreeMap::<Addr, usize>::new();
2776    let mut domains = Vec::with_capacity(input.len());
2777
2778    for (path, logic_path) in input.iter().enumerate() {
2779        let exact_fold_root = fold_groups.direct_group_by_path[path].filter(|root| {
2780            fold_groups
2781                .groups
2782                .get(root)
2783                .is_some_and(|info| info.exact_and_exclusive)
2784        });
2785        let domain = if let Some(root) = exact_fold_root {
2786            Some(*fold_domains.entry(root).or_insert_with(|| {
2787                let domain = next_domain;
2788                next_domain += 1;
2789                domain
2790            }))
2791        } else {
2792            logic_path.target.var().map(|target| {
2793                *target_domains.entry(target.id).or_insert_with(|| {
2794                    let domain = next_domain;
2795                    next_domain += 1;
2796                    domain
2797                })
2798            })
2799        };
2800        domains.push(domain);
2801    }
2802    domains
2803}
2804
2805/// Produce a deterministic topological order of the SCC condensation graph.
2806/// A ready effect domain is drained while possible, but each component remains
2807/// an independent queue item.  This preserves target/fold locality without
2808/// treating a layer or a complete ready frontier as a lowering batch.  Actual
2809/// register-pressure scheduling is performed later on MIR instructions and
2810/// their real VRegs.
2811fn stable_topological_sccs(
2812    sccs: Vec<Vec<usize>>,
2813    adj: &[Vec<usize>],
2814    path_domains: &[Option<usize>],
2815) -> Option<(Vec<Vec<usize>>, Vec<usize>)> {
2816    if path_domains.len() != adj.len() {
2817        return None;
2818    }
2819    let component_count = sccs.len();
2820    let mut component_by_path = vec![usize::MAX; adj.len()];
2821    let mut keys = Vec::with_capacity(component_count);
2822    let mut component_domains = Vec::with_capacity(component_count);
2823    for (component, scc) in sccs.iter().enumerate() {
2824        keys.push(*scc.iter().min()?);
2825        for &path in scc {
2826            if path >= adj.len() || component_by_path[path] != usize::MAX {
2827                return None;
2828            }
2829            component_by_path[path] = component;
2830        }
2831        let singleton = (scc.len() == 1).then_some(scc[0]);
2832        let acyclic = singleton.is_some_and(|path| !adj[path].contains(&path));
2833        component_domains.push(
2834            acyclic
2835                .then(|| path_domains[singleton.expect("acyclic SCC is a singleton")])
2836                .flatten(),
2837        );
2838    }
2839    if component_by_path.contains(&usize::MAX) {
2840        return None;
2841    }
2842
2843    let mut outgoing = vec![Vec::<usize>::new(); component_count];
2844    for (definition, users) in adj.iter().enumerate() {
2845        let source = component_by_path[definition];
2846        for &user in users {
2847            let target = *component_by_path.get(user)?;
2848            if source != target {
2849                outgoing[source].push(target);
2850            }
2851        }
2852    }
2853    let mut indegree = vec![0usize; component_count];
2854    for edges in &mut outgoing {
2855        edges.sort_unstable();
2856        edges.dedup();
2857        for &target in edges.iter() {
2858            indegree[target] = indegree[target].checked_add(1)?;
2859        }
2860    }
2861
2862    let mut ready = BTreeSet::new();
2863    let mut ready_by_domain = HashMap::<usize, BTreeSet<(usize, usize)>>::default();
2864    for (component, degree) in indegree.iter().enumerate() {
2865        if *degree == 0 {
2866            let entry = (keys[component], component);
2867            ready.insert(entry);
2868            if let Some(domain) = component_domains[component] {
2869                ready_by_domain.entry(domain).or_default().insert(entry);
2870            }
2871        }
2872    }
2873    let mut components = sccs.into_iter().map(Some).collect::<Vec<_>>();
2874    let mut ordered = Vec::with_capacity(component_count);
2875    let mut active_domain = None;
2876    while !ready.is_empty() {
2877        let selected = active_domain
2878            .and_then(|domain| ready_by_domain.get(&domain)?.iter().next().copied())
2879            .or_else(|| ready.iter().next().copied())?;
2880        let (_, component) = selected;
2881        ready.remove(&selected);
2882        if let Some(domain) = component_domains[component] {
2883            ready_by_domain.get_mut(&domain)?.remove(&selected);
2884        }
2885        active_domain = component_domains[component];
2886        ordered.push(components[component].take()?);
2887        for &target in &outgoing[component] {
2888            indegree[target] = indegree[target].checked_sub(1)?;
2889            if indegree[target] == 0 {
2890                let entry = (keys[target], target);
2891                ready.insert(entry);
2892                if let Some(domain) = component_domains[target] {
2893                    ready_by_domain.entry(domain).or_default().insert(entry);
2894                }
2895            }
2896        }
2897    }
2898    (ordered.len() == component_count).then_some((ordered, component_by_path))
2899}
2900
2901fn logic_path_is_scheduling_barrier<Addr: Clone + Eq + Hash>(path: &LogicPath<Addr>) -> bool {
2902    matches!(path.target, LogicPathTarget::CombCaptureEvent { .. })
2903        || !path.comb_capture_enable_sites.is_empty()
2904}
2905
2906fn cached_logic_path_roots<Addr: Clone + Eq + Hash>(path: &LogicPath<Addr>) -> Vec<NodeId> {
2907    let mut roots = path
2908        .local_inputs
2909        .iter()
2910        .map(|(_, node)| *node)
2911        .collect::<Vec<_>>();
2912    if path.local_inputs.is_empty() && matches!(path.target, LogicPathTarget::Var(_)) {
2913        roots.extend(path.pre_lower_nodes.iter().copied());
2914        roots.push(path.expr);
2915    }
2916    roots
2917}
2918
2919/// Build the sparse incidence relation between LogicPaths and SLT values
2920/// which can actually survive in `lower_cache` across path boundaries.
2921///
2922/// A tree-only node cannot be independently reused: a cached ancestor hides
2923/// it from later lowering.  Only DAG joins (including roots named by multiple
2924/// paths) therefore become materialization tokens.  Constants are deliberately
2925/// omitted because target lowering can rematerialize them without retaining a
2926/// source register.
2927fn logic_path_materialization_tokens<Addr: Clone + Eq + Hash>(
2928    input: &[LogicPath<Addr>],
2929    arena: &SLTNodeArena<Addr>,
2930) -> (Vec<Vec<usize>>, Vec<usize>) {
2931    let mut references = vec![0usize; arena.len()];
2932    let mut children = Vec::new();
2933    for raw in 0..arena.len() {
2934        children.clear();
2935        push_scheduler_node_children(NodeId(raw), arena, &mut children);
2936        children.sort_unstable();
2937        children.dedup();
2938        for &child in &children {
2939            references[child.0] = references[child.0].saturating_add(1);
2940        }
2941    }
2942    for path in input {
2943        let mut roots = cached_logic_path_roots(path);
2944        roots.sort_unstable();
2945        roots.dedup();
2946        for root in roots {
2947            references[root.0] = references[root.0].saturating_add(1);
2948        }
2949    }
2950
2951    let candidates = references
2952        .iter()
2953        .enumerate()
2954        .map(|(raw, references)| {
2955            *references > 1 && !matches!(arena.get(NodeId(raw)), SLTNode::Constant(..))
2956        })
2957        .collect::<Vec<_>>();
2958    let mut raw_tokens = vec![Vec::<usize>::new(); input.len()];
2959    let mut token_users = vec![0usize; arena.len()];
2960    let mut visited = vec![0usize; arena.len()];
2961    let mut epoch = 0usize;
2962    let mut work = Vec::new();
2963    for (path_index, path) in input.iter().enumerate() {
2964        epoch = epoch.wrapping_add(1);
2965        if epoch == 0 {
2966            visited.fill(0);
2967            epoch = 1;
2968        }
2969        work.extend(cached_logic_path_roots(path));
2970        while let Some(node) = work.pop() {
2971            if visited[node.0] == epoch {
2972                continue;
2973            }
2974            visited[node.0] = epoch;
2975            if candidates[node.0] {
2976                raw_tokens[path_index].push(node.0);
2977            }
2978            push_scheduler_node_children(node, arena, &mut work);
2979        }
2980        raw_tokens[path_index].sort_unstable();
2981        raw_tokens[path_index].dedup();
2982        for &token in &raw_tokens[path_index] {
2983            token_users[token] = token_users[token].saturating_add(1);
2984        }
2985    }
2986
2987    let mut dense_by_raw = vec![usize::MAX; arena.len()];
2988    let mut weights = Vec::new();
2989    for (raw, users) in token_users.into_iter().enumerate() {
2990        if users < 2 {
2991            continue;
2992        }
2993        dense_by_raw[raw] = weights.len();
2994        let width = crate::get_width(NodeId(raw), arena);
2995        weights.push(width.div_ceil(64).max(1));
2996    }
2997    for row in &mut raw_tokens {
2998        row.retain(|raw| dense_by_raw[*raw] != usize::MAX);
2999        for token in row.iter_mut() {
3000            *token = dense_by_raw[*token];
3001        }
3002    }
3003    (raw_tokens, weights)
3004}
3005
3006fn exact_native_storage_store(width: usize) -> bool {
3007    width != 0 && width <= 64 && matches!(width.div_ceil(8), 1 | 2 | 4 | 8)
3008}
3009
3010fn static_store_avoids_native_rmw<Addr: Copy + Eq + Hash>(
3011    write: &VarAtomBase<Addr>,
3012    var_widths: &HashMap<Addr, usize>,
3013    unpacked_element_widths: &HashMap<Addr, usize>,
3014) -> bool {
3015    let Some(width) = write
3016        .access
3017        .msb
3018        .checked_sub(write.access.lsb)
3019        .and_then(|width| width.checked_add(1))
3020    else {
3021        return false;
3022    };
3023
3024    if let Some(&element_width) = unpacked_element_widths.get(&write.id) {
3025        // Unpacked layout selection happens after scheduling. Only a complete
3026        // byte-aligned native-width element avoids RMW in both packed and
3027        // independently strided layouts.
3028        return element_width != 0
3029            && width == element_width
3030            && write.access.lsb.is_multiple_of(element_width)
3031            && matches!(width, 8 | 16 | 32 | 64);
3032    }
3033
3034    let whole_object = write.access.lsb == 0
3035        && var_widths.get(&write.id).copied() == Some(width)
3036        && exact_native_storage_store(width);
3037    let aligned_native_range =
3038        write.access.lsb.is_multiple_of(8) && matches!(width, 8 | 16 | 32 | 64);
3039    whole_object || aligned_native_range
3040}
3041
3042fn direct_ff_write_ranges<Addr: Copy + Eq + Hash>(
3043    input: &[LogicPath<Addr>],
3044    summaries: &[FfAccessSummary<Addr>],
3045    plan: &FfCombSchedulePlan,
3046    retained: &HashSet<(usize, usize)>,
3047    ff_node_base: usize,
3048    var_widths: &HashMap<Addr, usize>,
3049    unpacked_element_widths: &HashMap<Addr, usize>,
3050) -> Vec<Vec<VarAtomBase<Addr>>> {
3051    // Index reads by object so the overlap probes below only scan atoms that
3052    // share the object under test. Only the comb readers the plan actually
3053    // consults need an index; every other path would pay for an unused map.
3054    let mut consulted_comb_readers = vec![false; input.len()];
3055    for readers in &plan.comb_before_direct_write {
3056        for &reader in readers {
3057            consulted_comb_readers[reader] = true;
3058        }
3059    }
3060    let comb_reads = input
3061        .iter()
3062        .zip(&consulted_comb_readers)
3063        .map(|(path, &consulted)| {
3064            consulted.then(|| index_accesses(path.sources.iter().chain(&path.previous_sources)))
3065        })
3066        .collect::<Vec<_>>();
3067    let ff_reads = summaries
3068        .iter()
3069        .map(|summary| index_accesses(&summary.reads))
3070        .collect::<Vec<_>>();
3071
3072    summaries
3073        .iter()
3074        .enumerate()
3075        .map(|(writer, summary)| {
3076            let mut direct = summary
3077                .writes
3078                .iter()
3079                .map(|write| {
3080                    // Direct publication is only a throughput optimization.
3081                    // Dynamic destinations and static bitfields requiring a
3082                    // physical read-modify-write stay staged.
3083                    if summary.dynamic_writes.contains(&write.id)
3084                        || !static_store_avoids_native_rmw(
3085                            write,
3086                            var_widths,
3087                            unpacked_element_widths,
3088                        )
3089                    {
3090                        return false;
3091                    }
3092
3093                    // A read in the same always_ff action cannot be ordered
3094                    // before only one of its own Stores. Keep just the
3095                    // overlapping range staged so lowering reads pre-edge
3096                    // state without penalizing disjoint writes.
3097                    if indexed_overlaps(&ff_reads[writer], write) {
3098                        return false;
3099                    }
3100
3101                    let writer_node = ff_node_base + writer;
3102                    let comb_readers_proven = plan.comb_before_direct_write[writer]
3103                        .iter()
3104                        .filter(|&&reader| {
3105                            comb_reads[reader]
3106                                .as_ref()
3107                                .is_some_and(|reads| indexed_overlaps(reads, write))
3108                        })
3109                        .all(|&reader| retained.contains(&(reader, writer_node)));
3110                    let ff_readers_proven = plan.ff_before_direct_write[writer]
3111                        .iter()
3112                        .filter(|&&reader| indexed_overlaps(&ff_reads[reader], write))
3113                        .all(|&reader| retained.contains(&(ff_node_base + reader, writer_node)));
3114                    comb_readers_proven && ff_readers_proven
3115                })
3116                .collect::<Vec<_>>();
3117
3118            // Direct Stores publish immediately, while staged Stores publish
3119            // together in finish(). Mixing those destinations inside one
3120            // overlapping write component can reverse procedural last-write-
3121            // wins order. Propagate staging through the component while
3122            // leaving disjoint ranges eligible for direct publication.
3123            let mut staged = direct
3124                .iter()
3125                .enumerate()
3126                .filter_map(|(index, &direct)| (!direct).then_some(index))
3127                .collect::<Vec<_>>();
3128            while let Some(staged_index) = staged.pop() {
3129                let staged_write = summary.writes[staged_index];
3130                for (candidate, candidate_write) in summary.writes.iter().enumerate() {
3131                    if direct[candidate]
3132                        && staged_write.id == candidate_write.id
3133                        && staged_write.access.overlaps(&candidate_write.access)
3134                    {
3135                        direct[candidate] = false;
3136                        staged.push(candidate);
3137                    }
3138                }
3139            }
3140
3141            summary
3142                .writes
3143                .iter()
3144                .zip(direct)
3145                .filter_map(|(write, direct)| direct.then_some(write))
3146                .copied()
3147                .collect()
3148        })
3149        .collect()
3150}
3151
3152fn schedule_acyclic_path_region(
3153    paths: &[usize],
3154    dependencies: &LogicPathEdges,
3155    values: &LogicPathEdges,
3156    materialization_tokens: &[Vec<usize>],
3157    token_weights: &[usize],
3158    local_by_path: &mut [usize],
3159) -> Option<Vec<usize>> {
3160    for (local, &path) in paths.iter().enumerate() {
3161        if path >= local_by_path.len() || local_by_path[path] != usize::MAX {
3162            for &mapped in &paths[..local] {
3163                local_by_path[mapped] = usize::MAX;
3164            }
3165            return None;
3166        }
3167        local_by_path[path] = local;
3168    }
3169
3170    let result = (|| {
3171        let dependency_predecessors =
3172            MappedGraphRows::new(&dependencies.predecessors, paths, local_by_path).ok()?;
3173        let value_predecessors =
3174            MappedGraphRows::new(&values.predecessors, paths, local_by_path).ok()?;
3175        let tokens = MappedNodeRows::new(materialization_tokens, paths).ok()?;
3176        let successors = MappedGraphRows::new(&dependencies.users, paths, local_by_path).ok()?;
3177        let value_users = MappedGraphRows::new(&values.users, paths, local_by_path).ok()?;
3178        let order = schedule_min_live_values_and_tokens_with_mapped_rows(
3179            dependency_predecessors,
3180            value_predecessors,
3181            tokens,
3182            token_weights,
3183            successors,
3184            value_users,
3185        )
3186        .ok()?;
3187        Some(order.into_iter().map(|local| paths[local]).collect())
3188    })();
3189
3190    for &path in paths {
3191        local_by_path[path] = usize::MAX;
3192    }
3193    result
3194}
3195
3196/// Schedule maximal acyclic runs by MemoryDef/MemoryUse liveness. Loop SCCs
3197/// and observable events are synchronization boundaries, so no path is moved
3198/// across an iteration or externally visible effect.
3199fn schedule_logic_path_regions<Addr: Clone + Eq + Hash>(
3200    topological_sccs: Vec<Vec<usize>>,
3201    dependencies: &LogicPathEdges,
3202    values: &LogicPathEdges,
3203    materialization_tokens: &[Vec<usize>],
3204    token_weights: &[usize],
3205    input: &[LogicPath<Addr>],
3206) -> Option<Vec<ScheduledWork>> {
3207    if dependencies.users.len() != values.users.len()
3208        || dependencies.users.len() != materialization_tokens.len()
3209        || dependencies.users.len() < input.len()
3210    {
3211        return None;
3212    }
3213    let mut local_by_path = vec![usize::MAX; dependencies.users.len()];
3214    let mut result = Vec::with_capacity(topological_sccs.len());
3215    let mut pending = Vec::new();
3216    let flush = |pending: &mut Vec<usize>,
3217                 result: &mut Vec<ScheduledWork>,
3218                 local_by_path: &mut [usize]|
3219     -> Option<()> {
3220        if pending.is_empty() {
3221            return Some(());
3222        }
3223        let ordered = schedule_acyclic_path_region(
3224            pending,
3225            dependencies,
3226            values,
3227            materialization_tokens,
3228            token_weights,
3229            local_by_path,
3230        )?;
3231        result.extend(ordered.into_iter().map(|path| {
3232            if path < input.len() {
3233                ScheduledWork::CombPath(path)
3234            } else {
3235                ScheduledWork::Ff(path - input.len())
3236            }
3237        }));
3238        pending.clear();
3239        Some(())
3240    };
3241
3242    for scc in topological_sccs {
3243        if let [path] = scc.as_slice()
3244            && !dependencies.users[*path].contains(path)
3245            && (*path >= input.len() || !logic_path_is_scheduling_barrier(&input[*path]))
3246        {
3247            pending.push(*path);
3248        } else {
3249            flush(&mut pending, &mut result, &mut local_by_path)?;
3250            if let [path] = scc.as_slice() {
3251                if *path < input.len() {
3252                    result.push(ScheduledWork::CombPath(*path));
3253                } else {
3254                    result.push(ScheduledWork::Ff(*path - input.len()));
3255                }
3256            } else if scc.iter().all(|path| *path < input.len()) {
3257                result.push(ScheduledWork::CombScc(scc));
3258            } else {
3259                return None;
3260            }
3261        }
3262    }
3263    flush(&mut pending, &mut result, &mut local_by_path)?;
3264    Some(result)
3265}
3266
3267/// Schedules and transforms LogicPaths into Simulation Intermediate Representation (SIR).
3268///
3269/// This process performs:
3270/// 1. Dependency analysis to detect multiple drivers and combinational loops.
3271/// 2. SCC detection via Tarjan's algorithm.
3272/// 3. Scheduling based on two primary strategies:
3273///    - **Strategy A (Static Unrolling)**: For DAG parts or loops with small, predictable convergence bounds.
3274///    - **Strategy B (Dynamic Convergence)**: For complex SCCs or potential "True Loops", implementing
3275///      runtime oscillation detection and convergence-based repetition.
3276fn sort_impl<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display, E>(
3277    input: Vec<LogicPath<Addr>>,
3278    arena: &SLTNodeArena<Addr>,
3279    ignored_loops: &HashSet<(Addr, Addr)>,
3280    true_loops: &HashMap<(Addr, Addr), usize>,
3281    four_state: bool,
3282    var_widths: &HashMap<Addr, usize>,
3283    unpacked_element_widths: &HashMap<Addr, usize>,
3284    first_runtime_error_code: i64,
3285    mut ff: Option<&mut dyn ClockFfLowering<Addr, Error = E>>,
3286) -> Result<ScheduleResult<Addr>, ClockSortError<Addr, E>> {
3287    let (input, ff_plan) = if let Some(ff_lowering) = ff.as_deref() {
3288        let mut plan = plan_ff_comb_schedule(&input, ff_lowering.summaries())
3289            .map_err(ClockSortError::Scheduler)?;
3290        let mut old_to_new = vec![usize::MAX; input.len()];
3291        let mut filtered = Vec::with_capacity(
3292            plan.required_comb
3293                .iter()
3294                .filter(|required| **required)
3295                .count(),
3296        );
3297        for (old, path) in input.into_iter().enumerate() {
3298            if plan.required_comb[old] {
3299                old_to_new[old] = filtered.len();
3300                filtered.push(path);
3301            }
3302        }
3303        let remap_paths = |paths: &mut Vec<usize>| {
3304            for path in paths.iter_mut() {
3305                *path = old_to_new[*path];
3306            }
3307            paths.retain(|path| *path != usize::MAX);
3308            paths.sort_unstable();
3309            paths.dedup();
3310        };
3311        for paths in plan
3312            .comb_value_predecessors
3313            .iter_mut()
3314            .chain(&mut plan.comb_before_direct_write)
3315        {
3316            remap_paths(paths);
3317        }
3318        plan.required_comb = vec![true; filtered.len()];
3319        (filtered, Some(plan))
3320    } else {
3321        (input, None)
3322    };
3323
3324    // 1. Build bit-range MemoryDefs/MemoryUses and their semantic graph.
3325    // This is the source dataflow adapter; interval indexing and DAG
3326    // scheduling remain IR-independent in celox-analysis.
3327    let n = input.len();
3328    let (mut materialization_tokens, token_weights) =
3329        logic_path_materialization_tokens(&input, arena);
3330    let LogicPathMemorySsa {
3331        mut dependencies,
3332        mut values,
3333    } = build_logic_path_memory_ssa(&input).map_err(ClockSortError::Scheduler)?;
3334
3335    // FF actions are ordinary sinks in the existing comb dependency graph.
3336    // The added nodes go through the same SCC, topological, and ready-queue
3337    // scheduling as LogicPaths; there is no second clock-specific scheduler.
3338    let ff_count = ff
3339        .as_deref()
3340        .map_or(0, |lowering| lowering.summaries().len());
3341    let mut direct_ff_writes_by_action = vec![Vec::new(); ff_count];
3342    if let Some(plan) = &ff_plan {
3343        dependencies.resize(n + ff_count);
3344        values.resize(n + ff_count);
3345        materialization_tokens.resize_with(n + ff_count, Vec::new);
3346        for (ff_index, predecessors) in plan.comb_value_predecessors.iter().enumerate() {
3347            let ff_node = n + ff_index;
3348            for &definition in predecessors {
3349                dependencies.push(definition, ff_node);
3350                values.push(definition, ff_node);
3351            }
3352        }
3353        let write_order_edges = plan
3354            .comb_before_direct_write
3355            .iter()
3356            .zip(&plan.ff_before_direct_write)
3357            .enumerate()
3358            .map(|(writer, (comb_readers, ff_readers))| {
3359                comb_readers
3360                    .iter()
3361                    .copied()
3362                    .map(|reader| (reader, n + writer))
3363                    .chain(
3364                        ff_readers
3365                            .iter()
3366                            .copied()
3367                            .map(|reader| (n + reader, n + writer)),
3368                    )
3369                    .collect::<Vec<_>>()
3370            })
3371            .collect::<Vec<_>>();
3372        let retained = add_acyclic_ff_write_order_edges(
3373            &mut dependencies.users,
3374            write_order_edges.iter().flatten().copied(),
3375        );
3376        for &(predecessor, user) in &retained {
3377            dependencies.predecessors[user].push(predecessor);
3378        }
3379        direct_ff_writes_by_action = direct_ff_write_ranges(
3380            &input,
3381            ff.as_deref()
3382                .expect("an FF schedule plan requires an FF lowering callback")
3383                .summaries(),
3384            plan,
3385            &retained,
3386            n,
3387            var_widths,
3388            unpacked_element_widths,
3389        );
3390        dependencies.canonicalize();
3391        values.canonicalize();
3392    }
3393    let direct_ff_writes = direct_ff_writes_by_action
3394        .iter()
3395        .flatten()
3396        .copied()
3397        .collect();
3398
3399    // 2. SCC extraction identifies the synchronization boundaries at which
3400    // the dataflow equations must be iterated rather than freely reordered.
3401    let mut ctx = TarjanContext {
3402        index: 0,
3403        stack: Vec::new(),
3404        on_stack: HashSet::default(),
3405        indices: vec![None; n + ff_count],
3406        lowlink: vec![None; n + ff_count],
3407        sccs: Vec::new(),
3408    };
3409    for i in 0..(n + ff_count) {
3410        if ctx.indices[i].is_none() {
3411            strong_connect(i, &dependencies.users, &mut ctx);
3412        }
3413    }
3414    let fold_group_schedule_index = build_fold_group_schedule_index(&input, arena);
3415    let mut path_domains = logic_path_scheduling_domains(&input, &fold_group_schedule_index);
3416    path_domains.resize(n + ff_count, None);
3417    let (topological_sccs, component_by_path) =
3418        stable_topological_sccs(ctx.sccs, &dependencies.users, &path_domains).ok_or(
3419            ClockSortError::Scheduler(SchedulerError::InvalidDependencyGraph),
3420        )?;
3421    let scheduled_work = schedule_logic_path_regions(
3422        topological_sccs,
3423        &dependencies,
3424        &values,
3425        &materialization_tokens,
3426        &token_weights,
3427        &input,
3428    )
3429    .ok_or(ClockSortError::Scheduler(
3430        SchedulerError::InvalidDependencyGraph,
3431    ))?;
3432    let adj = dependencies.users;
3433    drop(dependencies.predecessors);
3434    drop(values);
3435    let scheduled_work = form_scheduled_guard_regions(scheduled_work, &input, arena, four_state);
3436
3437    let mut builder = SIRBuilder::new();
3438    if let Some(ff_lowering) = ff.as_deref_mut() {
3439        ff_lowering
3440            .begin(&mut builder, &direct_ff_writes_by_action)
3441            .map_err(ClockSortError::Lowering)?;
3442    }
3443    let lowerer = crate::SLTToSIRLowerer::new(four_state)
3444        .with_unpacked_input_types(arena, unpacked_element_widths);
3445
3446    let mut lower_cache = HashMap::default();
3447    let mut dep_memo = HashMap::default();
3448    let mut inverse_dep_memo = HashMap::default();
3449
3450    const UNROLL_THRESHOLD: usize = 32;
3451
3452    // Helper: Emits SIR for a logic path and manages the lowering cache.
3453    // lowerer.lower allocates registers and emits instructions for sub-expressions.
3454    let emit_node = |builder: &mut SIRBuilder<Addr>,
3455                     idx: usize,
3456                     lower_cache: &mut HashMap<NodeId, RegisterId>,
3457                     dep_memo: &mut HashMap<NodeId, HashSet<Addr>>,
3458                     inverse_dep_memo: &mut HashMap<Addr, HashSet<NodeId>>| {
3459        let path = &input[idx];
3460
3461        collect_logic_path_input_deps(path, arena, dep_memo, inverse_dep_memo);
3462        emit_logic_path_store(
3463            &lowerer,
3464            builder,
3465            path,
3466            arena,
3467            lower_cache,
3468            unpacked_element_widths,
3469        );
3470        invalidate_logic_path_target(path, inverse_dep_memo, lower_cache);
3471    };
3472    // Maximum blocks in a single EU before flushing to a new one.
3473    // This prevents Cranelift from choking on massive functions.
3474    const EU_BLOCK_LIMIT: usize = 20_000;
3475
3476    let mut result_eus: Vec<ExecutionUnit<Addr>> = Vec::new();
3477    let mut runtime_errors: HashMap<i64, RuntimeErrorInfo<Addr>> = HashMap::default();
3478    let mut next_runtime_error_code = first_runtime_error_code;
3479
3480    // Pairwise joint-fold profitability is intentionally local. Keep all
3481    // projections of one packed root together, but never build an unbounded
3482    // compatibility matrix for a long run of independent roots.
3483    const MAX_JOINT_FOLD_ROOTS: usize = 16;
3484    let mut pending_fold_indices: Vec<usize> = Vec::new();
3485    let mut pending_fold_roots = HashSet::default();
3486
3487    // 4. Lower each scheduled component, selecting static unrolling or
3488    // dynamic convergence for cyclic SCCs.
3489    for work in scheduled_work {
3490        let singleton;
3491        let scc = match &work {
3492            ScheduledWork::CombPath(path) => {
3493                singleton = [*path];
3494                singleton.as_slice()
3495            }
3496            ScheduledWork::CombScc(scc) => scc.as_slice(),
3497            ScheduledWork::GuardedComb { condition, paths } => {
3498                flush_pending_fold_paths(
3499                    &mut pending_fold_indices,
3500                    &input,
3501                    &fold_group_schedule_index,
3502                    &lowerer,
3503                    &mut builder,
3504                    arena,
3505                    &mut lower_cache,
3506                    &mut dep_memo,
3507                    &mut inverse_dep_memo,
3508                    unpacked_element_widths,
3509                    four_state,
3510                );
3511                pending_fold_roots.clear();
3512                emit_scheduled_guard_region(
3513                    &lowerer,
3514                    &mut builder,
3515                    *condition,
3516                    paths,
3517                    &input,
3518                    arena,
3519                    &mut lower_cache,
3520                    &mut dep_memo,
3521                    &mut inverse_dep_memo,
3522                    unpacked_element_widths,
3523                );
3524                continue;
3525            }
3526            ScheduledWork::Ff(index) => {
3527                flush_pending_fold_paths(
3528                    &mut pending_fold_indices,
3529                    &input,
3530                    &fold_group_schedule_index,
3531                    &lowerer,
3532                    &mut builder,
3533                    arena,
3534                    &mut lower_cache,
3535                    &mut dep_memo,
3536                    &mut inverse_dep_memo,
3537                    unpacked_element_widths,
3538                    four_state,
3539                );
3540                pending_fold_roots.clear();
3541                ff.as_deref_mut()
3542                    .ok_or(ClockSortError::Scheduler(
3543                        SchedulerError::InvalidDependencyGraph,
3544                    ))?
3545                    .lower(*index, &direct_ff_writes_by_action[*index], &mut builder)
3546                    .map_err(ClockSortError::Lowering)?;
3547                // Any directly published range is ordered after all of its
3548                // old-state readers. Other FF state stays invisible in
3549                // WORKING/SPARSE_WORKING until the final publish, so values
3550                // computed before the FF CFG keep the ordinary lowering cache
3551                // valid.
3552                continue;
3553            }
3554        };
3555        let component = component_by_path[scc[0]];
3556        let mut user_safety_limit = None;
3557        for &v_idx in scc {
3558            for &u_idx in &adj[v_idx] {
3559                if component_by_path[u_idx] == component {
3560                    if let (Some(v_target), Some(u_target)) =
3561                        (input[v_idx].target.var(), input[u_idx].target.var())
3562                    {
3563                        let edge = (v_target.id, u_target.id);
3564                        if let Some(&limit) = true_loops.get(&edge) {
3565                            user_safety_limit =
3566                                Some(user_safety_limit.map_or(limit, |l: usize| l.max(limit)));
3567                        }
3568                    }
3569                }
3570            }
3571        }
3572        let is_loop = scc.len() > 1 || (scc.len() == 1 && adj[scc[0]].contains(&scc[0]));
3573
3574        if is_loop {
3575            // Exact grouped folds are atomic with respect to a loop SCC.
3576            flush_pending_fold_paths(
3577                &mut pending_fold_indices,
3578                &input,
3579                &fold_group_schedule_index,
3580                &lowerer,
3581                &mut builder,
3582                arena,
3583                &mut lower_cache,
3584                &mut dep_memo,
3585                &mut inverse_dep_memo,
3586                unpacked_element_widths,
3587                four_state,
3588            );
3589            pending_fold_roots.clear();
3590            let mut authorized = user_safety_limit.is_some();
3591            'check_scc: for &v_idx in scc {
3592                for &u_idx in &adj[v_idx] {
3593                    if component_by_path[u_idx] == component
3594                        && input[v_idx]
3595                            .target
3596                            .var()
3597                            .zip(input[u_idx].target.var())
3598                            .is_some_and(|(v, u)| ignored_loops.contains(&(v.id, u.id)))
3599                    {
3600                        // Some loops are explicitly allowed by the user (e.g., false loops).
3601                        authorized = true;
3602                        break 'check_scc;
3603                    }
3604                }
3605            }
3606
3607            if !authorized {
3608                return Err(ClockSortError::Scheduler(
3609                    SchedulerError::CombinationalLoop {
3610                        blocks: scc.iter().map(|idx| input[*idx].clone()).collect(),
3611                    },
3612                ));
3613            }
3614
3615            // FAS Sort
3616            let optimized_scc_order = greedy_fas_sort(scc, &adj);
3617            let force_strategy_b = user_safety_limit.is_some();
3618            let iterations = calculate_required_iterations(&adj, &optimized_scc_order);
3619            let total_ops_estimate = optimized_scc_order.len().saturating_mul(iterations);
3620            if !force_strategy_b && total_ops_estimate <= UNROLL_THRESHOLD {
3621                // Strategy A: Static Unrolling
3622                // The loop is unrolled a fixed number of times based on structural dependency depth (iterations).
3623                for _ in 0..iterations {
3624                    for &idx in &optimized_scc_order {
3625                        emit_node(
3626                            &mut builder,
3627                            idx,
3628                            &mut lower_cache,
3629                            &mut dep_memo,
3630                            &mut inverse_dep_memo,
3631                        );
3632                    }
3633                }
3634            } else {
3635                let runtime_error_code = next_runtime_error_code;
3636                next_runtime_error_code += 1;
3637                let mut seen = HashSet::default();
3638                let sources = scc
3639                    .iter()
3640                    .filter_map(|idx| {
3641                        let addr = input[*idx].target.var()?.id;
3642                        seen.insert(addr).then_some(addr)
3643                    })
3644                    .collect::<Vec<_>>();
3645                runtime_errors.insert(
3646                    runtime_error_code,
3647                    RuntimeErrorInfo {
3648                        message: "Detected True Loop".to_string(),
3649                        signals: sources,
3650                    },
3651                );
3652
3653                // Strategy B: Dynamic Convergence
3654                // Implements a runtime loop that continues executing the SCC until all signals converge (dirty flag is false).
3655                // Includes a safety limit to detect non-converging "True Loops" and avoid infinite hang.
3656
3657                // 1. Determine the runtime repetition limit.
3658                let safety_limit = user_safety_limit.unwrap_or(iterations + 1);
3659
3660                // 2. Prepare Constants and Counters
3661                let zero_reg = builder.alloc_bit(64, false);
3662                builder.emit(SIRInstruction::Imm(zero_reg, SIRValue::new(0u64)));
3663
3664                let limit_reg = builder.alloc_bit(64, false);
3665                builder.emit(SIRInstruction::Imm(
3666                    limit_reg,
3667                    SIRValue::new(safety_limit as u64),
3668                ));
3669
3670                // 3. Blocks
3671                let current_counter = builder.alloc_bit(64, false);
3672                let header_block = builder.new_block_with(vec![current_counter]); // [counter]
3673                let body_block = builder.new_block();
3674                let exit_block = builder.new_block();
3675                let error_block = builder.new_block(); // For True Loop detection
3676
3677                // Start: Jump to header with counter = 0
3678                builder.seal_block(SIRTerminator::Jump(header_block, vec![zero_reg]));
3679
3680                // --- Header Block ---
3681                builder.switch_to_block(header_block);
3682
3683                // Check: counter < safety_limit
3684                let can_continue_reg = builder.alloc_bit(1, false);
3685                builder.emit(SIRInstruction::Binary(
3686                    can_continue_reg,
3687                    current_counter,
3688                    BinaryOp::LtU,
3689                    limit_reg,
3690                ));
3691
3692                // If counter exceeded limit, we might have an oscillating True Loop
3693                builder.seal_block(SIRTerminator::Branch {
3694                    cond: can_continue_reg,
3695                    true_block: (body_block, vec![]),
3696                    false_block: (error_block, vec![]),
3697                });
3698                builder.switch_to_block(body_block);
3699                let mut current_dirty_reg = builder.alloc_bit(1, false);
3700                builder.emit(SIRInstruction::Imm(current_dirty_reg, SIRValue::new(0u32)));
3701                for &idx in &optimized_scc_order {
3702                    let path = &input[idx];
3703                    let Some(target) = path.target.var() else {
3704                        emit_logic_path_store(
3705                            &lowerer,
3706                            &mut builder,
3707                            path,
3708                            arena,
3709                            &mut lower_cache,
3710                            unpacked_element_widths,
3711                        );
3712                        continue;
3713                    };
3714                    let width = 1 + target.access.msb - target.access.lsb;
3715                    let addr = target.id;
3716                    let offset =
3717                        static_access_offset(&target.id, target.access, unpacked_element_widths);
3718
3719                    // --- Dynamic Convergence Check Logic ---
3720                    // For each node in the SCC, we verify if its value changed after this iteration.
3721                    //
3722                    // a. Load the current value (pre-update benchmark)
3723                    let old_val_reg = builder.alloc_bit(width, false);
3724                    builder.emit(SIRInstruction::Load(
3725                        old_val_reg,
3726                        addr,
3727                        offset.clone(),
3728                        width,
3729                    ));
3730                    collect_logic_path_input_deps(
3731                        path,
3732                        arena,
3733                        &mut dep_memo,
3734                        &mut inverse_dep_memo,
3735                    );
3736                    // b. Compute the new value
3737                    let new_val_reg = lower_logic_path_expr(
3738                        &lowerer,
3739                        &mut builder,
3740                        path,
3741                        arena,
3742                        &mut lower_cache,
3743                    );
3744
3745                    // c. Compare: changed = (old != new)
3746                    let is_changed_reg = builder.alloc_bit(1, false);
3747                    builder.emit(SIRInstruction::Binary(
3748                        is_changed_reg,
3749                        old_val_reg,
3750                        BinaryOp::Ne, // Not Equal
3751                        new_val_reg,
3752                    ));
3753                    let new_dirty_reg = builder.alloc_bit(1, false);
3754
3755                    // d. Accumulate dirty flag: dirty = dirty | is_changed
3756                    // If any signal in the SCC changes, the entire SCC requires another iteration.
3757                    builder.emit(SIRInstruction::Binary(
3758                        new_dirty_reg,
3759                        current_dirty_reg,
3760                        BinaryOp::Or,
3761                        is_changed_reg,
3762                    ));
3763                    current_dirty_reg = new_dirty_reg;
3764                    // e. Store the new value
3765                    builder.emit(SIRInstruction::Store(
3766                        addr,
3767                        offset,
3768                        width,
3769                        new_val_reg,
3770                        Vec::new(),
3771                        Vec::new(),
3772                    ));
3773                    if !path.comb_capture_enable_sites.is_empty() {
3774                        let (old, new) = if path.comb_capture_enable_always {
3775                            let old = builder.alloc_bit(1, false);
3776                            let new = builder.alloc_bit(1, false);
3777                            builder.emit(SIRInstruction::Imm(old, SIRValue::new(0u8)));
3778                            builder.emit(SIRInstruction::Imm(new, SIRValue::new(1u8)));
3779                            (old, new)
3780                        } else {
3781                            (old_val_reg, new_val_reg)
3782                        };
3783                        builder.emit(SIRInstruction::CombCaptureEnableIfChanged {
3784                            old,
3785                            new,
3786                            sites: path.comb_capture_enable_sites.clone(),
3787                        });
3788                    }
3789                    if let Some(to_remove) = inverse_dep_memo.get(&addr) {
3790                        for node in to_remove {
3791                            lower_cache.remove(node);
3792                        }
3793                    }
3794                    // -------------------------------
3795                }
3796
3797                // 4. Branch: Loop if dirty
3798                let one_reg = builder.alloc_bit(64, false);
3799                builder.emit(SIRInstruction::Imm(one_reg, SIRValue::new(1u64)));
3800                let next_counter = builder.alloc_bit(64, false);
3801                builder.emit(SIRInstruction::Binary(
3802                    next_counter,
3803                    current_counter,
3804                    BinaryOp::Add,
3805                    one_reg,
3806                ));
3807
3808                // Increment the iteration counter and branch.
3809                // If 'dirty' is true, return to the header block; otherwise, exit the loop.
3810                builder.seal_block(SIRTerminator::Branch {
3811                    cond: current_dirty_reg,
3812                    true_block: (header_block, vec![next_counter]),
3813                    false_block: (exit_block, vec![]),
3814                });
3815
3816                // --- Error/Exit Blocks ---
3817                builder.switch_to_block(error_block);
3818                // Emit a trap or special instruction to indicate "Combinational Loop Oscillation"
3819                // builder.emit(SIRInstruction::Trap(1));
3820                builder.seal_block(SIRTerminator::Error(runtime_error_code));
3821
3822                // 5. Exit Block
3823                builder.switch_to_block(exit_block);
3824            }
3825        } else {
3826            // DAG Part — flush before emitting if the EU has grown too large
3827            if ff.is_none() && builder.block_count() >= EU_BLOCK_LIMIT {
3828                flush_pending_fold_paths(
3829                    &mut pending_fold_indices,
3830                    &input,
3831                    &fold_group_schedule_index,
3832                    &lowerer,
3833                    &mut builder,
3834                    arena,
3835                    &mut lower_cache,
3836                    &mut dep_memo,
3837                    &mut inverse_dep_memo,
3838                    unpacked_element_widths,
3839                    four_state,
3840                );
3841                pending_fold_roots.clear();
3842                if let Some(eu) = builder.flush_eu() {
3843                    result_eus.push(eu);
3844                    // Clear the lowering cache — register IDs are EU-scoped
3845                    lower_cache.clear();
3846                }
3847            }
3848
3849            let idx = scc[0];
3850            let exact_fold = is_exact_fold_path(idx, &fold_group_schedule_index);
3851            let fold_root = exact_fold
3852                .then_some(fold_group_schedule_index.direct_group_by_path[idx])
3853                .flatten();
3854            let depends_on_pending = pending_fold_indices
3855                .iter()
3856                .any(|pending| adj[*pending].binary_search(&idx).is_ok());
3857            let starts_new_root = fold_root.is_some_and(|root| !pending_fold_roots.contains(&root));
3858            let window_full = starts_new_root && pending_fold_roots.len() >= MAX_JOINT_FOLD_ROOTS;
3859            if exact_fold && !depends_on_pending && !window_full {
3860                pending_fold_indices.push(idx);
3861                pending_fold_roots.extend(fold_root);
3862            } else {
3863                flush_pending_fold_paths(
3864                    &mut pending_fold_indices,
3865                    &input,
3866                    &fold_group_schedule_index,
3867                    &lowerer,
3868                    &mut builder,
3869                    arena,
3870                    &mut lower_cache,
3871                    &mut dep_memo,
3872                    &mut inverse_dep_memo,
3873                    unpacked_element_widths,
3874                    four_state,
3875                );
3876                pending_fold_roots.clear();
3877                if exact_fold {
3878                    pending_fold_indices.push(idx);
3879                    pending_fold_roots.extend(fold_root);
3880                } else {
3881                    emit_node(
3882                        &mut builder,
3883                        idx,
3884                        &mut lower_cache,
3885                        &mut dep_memo,
3886                        &mut inverse_dep_memo,
3887                    );
3888                }
3889            }
3890        }
3891    }
3892
3893    // Flush the final exact grouped-fold run after the SCC loop.
3894    flush_pending_fold_paths(
3895        &mut pending_fold_indices,
3896        &input,
3897        &fold_group_schedule_index,
3898        &lowerer,
3899        &mut builder,
3900        arena,
3901        &mut lower_cache,
3902        &mut dep_memo,
3903        &mut inverse_dep_memo,
3904        unpacked_element_widths,
3905        four_state,
3906    );
3907    pending_fold_roots.clear();
3908
3909    if let Some(ff_lowering) = ff {
3910        ff_lowering
3911            .finish(&mut builder, &direct_ff_writes_by_action)
3912            .map_err(ClockSortError::Lowering)?;
3913    }
3914    builder.seal_block(SIRTerminator::Return);
3915    let (blocks, reg_map, _) = builder.drain();
3916    result_eus.push(ExecutionUnit {
3917        entry_block_id: BlockId(0),
3918        blocks,
3919        register_map: reg_map,
3920    });
3921    Ok(ScheduleResult {
3922        execution_units: result_eus,
3923        runtime_errors,
3924        direct_ff_writes,
3925    })
3926}
3927
3928#[cfg(test)]
3929pub fn sort<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
3930    input: Vec<LogicPath<Addr>>,
3931    arena: &SLTNodeArena<Addr>,
3932    ignored_loops: &HashSet<(Addr, Addr)>,
3933    true_loops: &HashMap<(Addr, Addr), usize>,
3934    four_state: bool,
3935    var_widths: &HashMap<Addr, usize>,
3936    first_runtime_error_code: i64,
3937) -> Result<ScheduleResult<Addr>, SchedulerError<Addr>> {
3938    sort_with_unpacked_element_widths(
3939        input,
3940        arena,
3941        ignored_loops,
3942        true_loops,
3943        four_state,
3944        var_widths,
3945        &HashMap::default(),
3946        first_runtime_error_code,
3947    )
3948}
3949
3950pub fn sort_with_unpacked_element_widths<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display>(
3951    input: Vec<LogicPath<Addr>>,
3952    arena: &SLTNodeArena<Addr>,
3953    ignored_loops: &HashSet<(Addr, Addr)>,
3954    true_loops: &HashMap<(Addr, Addr), usize>,
3955    four_state: bool,
3956    var_widths: &HashMap<Addr, usize>,
3957    unpacked_element_widths: &HashMap<Addr, usize>,
3958    first_runtime_error_code: i64,
3959) -> Result<ScheduleResult<Addr>, SchedulerError<Addr>> {
3960    match sort_impl::<Addr, std::convert::Infallible>(
3961        input,
3962        arena,
3963        ignored_loops,
3964        true_loops,
3965        four_state,
3966        var_widths,
3967        unpacked_element_widths,
3968        first_runtime_error_code,
3969        None,
3970    ) {
3971        Ok(result) => Ok(result),
3972        Err(ClockSortError::Scheduler(error)) => Err(error),
3973        Err(ClockSortError::Lowering(_)) => {
3974            unreachable!("ordinary comb scheduling has no FF lowering callback")
3975        }
3976    }
3977}
3978
3979pub fn sort_clock<Addr: Clone + Eq + Ord + Hash + Debug + Copy + Display, E>(
3980    input: Vec<LogicPath<Addr>>,
3981    arena: &SLTNodeArena<Addr>,
3982    ignored_loops: &HashSet<(Addr, Addr)>,
3983    true_loops: &HashMap<(Addr, Addr), usize>,
3984    four_state: bool,
3985    var_widths: &HashMap<Addr, usize>,
3986    unpacked_element_widths: &HashMap<Addr, usize>,
3987    first_runtime_error_code: i64,
3988    ff: &mut dyn ClockFfLowering<Addr, Error = E>,
3989) -> Result<ScheduleResult<Addr>, ClockSortError<Addr, E>> {
3990    sort_impl(
3991        input,
3992        arena,
3993        ignored_loops,
3994        true_loops,
3995        four_state,
3996        var_widths,
3997        unpacked_element_widths,
3998        first_runtime_error_code,
3999        Some(ff),
4000    )
4001}
4002
4003#[cfg(test)]
4004mod tests {
4005    use num_bigint::{BigInt, BigUint};
4006
4007    use super::{
4008        ExactFoldGroup, ExactIndexedLoadKey, FfAccessSummary, FoldGroupReadFacts,
4009        NormalizedIndexExpr, add_acyclic_ff_write_order_edges, best_weighted_fold_family,
4010        build_fold_group_schedule_index, build_logic_path_memory_ssa, collect_node_input_deps,
4011        direct_ff_write_ranges, index_accesses, indexed_overlaps, plan_ff_comb_schedule,
4012        prepare_atomic_fold_group_results, sort, stable_topological_sccs,
4013    };
4014    use crate::{HashMap, HashSet};
4015    use crate::{
4016        LogicPath, LogicPathTarget, SLTForFoldGroupState, SLTNode, SLTNodeArena, SLTToSIRLowerer,
4017    };
4018    use celox_design::{BinaryOp, BitAccess, VarAtomBase};
4019    use celox_sir::{SIRBuilder, SIRInstruction, SIRTerminator};
4020
4021    #[test]
4022    fn stable_scc_order_preserves_source_order_when_dependencies_allow_it() {
4023        // 0 and 1 are initially ready, both feed 2, and 2 feeds 3. Tarjan is
4024        // free to return components in any order; lowering remains stable.
4025        let adj = vec![vec![2], vec![2], vec![3], vec![]];
4026        let unordered = vec![vec![3], vec![1], vec![2], vec![0]];
4027
4028        let (ordered, component_by_path) =
4029            stable_topological_sccs(unordered, &adj, &[None; 4]).unwrap();
4030
4031        assert_eq!(ordered, vec![vec![0], vec![1], vec![2], vec![3]]);
4032        assert_eq!(component_by_path.len(), adj.len());
4033    }
4034
4035    #[test]
4036    fn stable_scc_order_drains_only_ready_members_of_an_effect_domain() {
4037        let adj = vec![vec![], vec![], vec![], vec![]];
4038        let unordered = vec![vec![3], vec![1], vec![2], vec![0]];
4039
4040        let (ordered, _) =
4041            stable_topological_sccs(unordered, &adj, &[Some(0), Some(1), Some(0), Some(1)])
4042                .unwrap();
4043
4044        assert_eq!(ordered, vec![vec![0], vec![2], vec![1], vec![3]]);
4045    }
4046
4047    #[test]
4048    fn effect_domain_preference_never_pulls_a_path_across_its_dependency() {
4049        // Path 1 shares path 0's destination domain, but it is not ready until
4050        // the independent path 2 has executed.
4051        let adj = vec![vec![], vec![], vec![1]];
4052        let unordered = vec![vec![1], vec![2], vec![0]];
4053
4054        let (ordered, _) =
4055            stable_topological_sccs(unordered, &adj, &[Some(0), Some(0), Some(1)]).unwrap();
4056
4057        assert_eq!(ordered, vec![vec![0], vec![2], vec![1]]);
4058    }
4059
4060    #[test]
4061    fn id_indexed_overlap_probes_match_a_naive_scan() {
4062        // The scheduling passes index one side of each overlap test by object
4063        // id. A probe must agree with scanning the whole access list, including
4064        // when objects are interleaved or a probe shares no object with it.
4065        let mut state = 0x9e37_79b9_7f4a_7c15u64;
4066        let mut next = move || {
4067            state ^= state << 13;
4068            state ^= state >> 7;
4069            state ^= state << 17;
4070            state
4071        };
4072        for _ in 0..256 {
4073            let accesses = (0..(next() % 12) as usize)
4074                .map(|_| {
4075                    let lsb = (next() % 32) as usize;
4076                    VarAtomBase::new((next() % 5) as u32, lsb, lsb + (next() % 8) as usize)
4077                })
4078                .collect::<Vec<_>>();
4079            let probe_lsb = (next() % 32) as usize;
4080            let probe = VarAtomBase::new(
4081                (next() % 5) as u32,
4082                probe_lsb,
4083                probe_lsb + (next() % 8) as usize,
4084            );
4085
4086            let index = index_accesses(&accesses);
4087            let naive = accesses
4088                .iter()
4089                .any(|access| access.id == probe.id && access.access.overlaps(&probe.access));
4090            assert_eq!(indexed_overlaps(&index, &probe), naive);
4091        }
4092    }
4093
4094    fn simple_path(
4095        arena: &mut SLTNodeArena<u32>,
4096        target: u32,
4097        source: Option<u32>,
4098    ) -> LogicPath<u32> {
4099        let expr = if let Some(source) = source {
4100            arena
4101                .alloc(SLTNode::Input {
4102                    variable: source,
4103                    signed: false,
4104                    index: Vec::new(),
4105                    access: BitAccess::new(0, 7),
4106                })
4107                .unwrap()
4108        } else {
4109            arena
4110                .alloc(SLTNode::Constant(
4111                    BigUint::from(target),
4112                    BigUint::from(0u8),
4113                    8,
4114                    false,
4115                ))
4116                .unwrap()
4117        };
4118        LogicPath {
4119            target: LogicPathTarget::Var(VarAtomBase::new(target, 0, 7)),
4120            sources: source
4121                .map(|source| [VarAtomBase::new(source, 0, 7)].into_iter().collect())
4122                .unwrap_or_default(),
4123            previous_sources: crate::HashSet::default(),
4124            address_sources: crate::HashSet::default(),
4125            local_inputs: Vec::new(),
4126            order_before: crate::HashSet::default(),
4127            comb_capture_enable_sites: Vec::new(),
4128            comb_capture_enable_always: false,
4129            pre_lower_nodes: Vec::new(),
4130            expr,
4131        }
4132    }
4133
4134    #[test]
4135    fn scheduled_paths_share_one_high_level_guard_without_changing_store_order() {
4136        let mut arena = SLTNodeArena::new();
4137        let condition = arena
4138            .alloc(SLTNode::Input {
4139                variable: 100,
4140                signed: false,
4141                index: Vec::new(),
4142                access: BitAccess::new(0, 0),
4143            })
4144            .unwrap();
4145        let mut paths = Vec::new();
4146        for target in 0u32..12 {
4147            let then_expr = arena
4148                .alloc(SLTNode::Constant(
4149                    BigUint::from(target),
4150                    BigUint::from(0u8),
4151                    8,
4152                    false,
4153                ))
4154                .unwrap();
4155            let else_expr = arena
4156                .alloc(SLTNode::Constant(
4157                    BigUint::from(target + 32),
4158                    BigUint::from(0u8),
4159                    8,
4160                    false,
4161                ))
4162                .unwrap();
4163            let expr = arena
4164                .alloc(SLTNode::Mux {
4165                    cond: condition,
4166                    then_expr,
4167                    else_expr,
4168                })
4169                .unwrap();
4170            paths.push(LogicPath {
4171                target: LogicPathTarget::Var(VarAtomBase::new(target, 0, 7)),
4172                sources: [VarAtomBase::new(100, 0, 0)].into_iter().collect(),
4173                previous_sources: crate::HashSet::default(),
4174                address_sources: crate::HashSet::default(),
4175                local_inputs: Vec::new(),
4176                order_before: crate::HashSet::default(),
4177                comb_capture_enable_sites: Vec::new(),
4178                comb_capture_enable_always: false,
4179                pre_lower_nodes: Vec::new(),
4180                expr,
4181            });
4182        }
4183
4184        let result = sort(
4185            paths,
4186            &arena,
4187            &crate::HashSet::default(),
4188            &crate::HashMap::default(),
4189            false,
4190            &crate::HashMap::default(),
4191            0,
4192        )
4193        .unwrap();
4194        let eu = &result.execution_units[0];
4195        let branches = eu
4196            .blocks
4197            .values()
4198            .filter(|block| matches!(block.terminator, SIRTerminator::Branch { .. }))
4199            .collect::<Vec<_>>();
4200        assert_eq!(branches.len(), 1);
4201        assert_eq!(
4202            eu.blocks
4203                .values()
4204                .flat_map(|block| &block.instructions)
4205                .filter(|instruction| matches!(instruction, SIRInstruction::Mux(..)))
4206                .count(),
4207            0
4208        );
4209
4210        let SIRTerminator::Branch {
4211            true_block,
4212            false_block,
4213            ..
4214        } = &branches[0].terminator
4215        else {
4216            unreachable!()
4217        };
4218        for block in [true_block.0, false_block.0] {
4219            let stores = eu.blocks[&block]
4220                .instructions
4221                .iter()
4222                .filter_map(|instruction| match instruction {
4223                    SIRInstruction::Store(target, ..) => Some(*target),
4224                    _ => None,
4225                })
4226                .collect::<Vec<_>>();
4227            assert_eq!(stores, (0..12).collect::<Vec<_>>());
4228        }
4229    }
4230
4231    #[test]
4232    fn scheduled_outer_guard_keeps_nested_mux_lowering_decisions() {
4233        let mut arena = SLTNodeArena::new();
4234        let outer_condition = arena
4235            .alloc(SLTNode::Input {
4236                variable: 100,
4237                signed: false,
4238                index: Vec::new(),
4239                access: BitAccess::new(0, 0),
4240            })
4241            .unwrap();
4242        let inner_condition = arena
4243            .alloc(SLTNode::Input {
4244                variable: 101,
4245                signed: false,
4246                index: Vec::new(),
4247                access: BitAccess::new(0, 0),
4248            })
4249            .unwrap();
4250        let input_value = arena
4251            .alloc(SLTNode::Input {
4252                variable: 102,
4253                signed: false,
4254                index: Vec::new(),
4255                access: BitAccess::new(0, 7),
4256            })
4257            .unwrap();
4258        let one = arena
4259            .alloc(SLTNode::Constant(
4260                BigUint::from(1u8),
4261                BigUint::from(0u8),
4262                8,
4263                false,
4264            ))
4265            .unwrap();
4266        let mut inner_true = input_value;
4267        let mut inner_false = input_value;
4268        for _ in 0..16 {
4269            inner_true = arena
4270                .alloc(SLTNode::Binary(inner_true, BinaryOp::Add, one))
4271                .unwrap();
4272            inner_false = arena
4273                .alloc(SLTNode::Binary(inner_false, BinaryOp::Xor, one))
4274                .unwrap();
4275        }
4276        let nested = arena
4277            .alloc(SLTNode::Mux {
4278                cond: inner_condition,
4279                then_expr: inner_true,
4280                else_expr: inner_false,
4281            })
4282            .unwrap();
4283
4284        let mut paths = Vec::new();
4285        for target in 0u32..12 {
4286            let then_expr = if target == 0 {
4287                nested
4288            } else {
4289                arena
4290                    .alloc(SLTNode::Constant(
4291                        BigUint::from(target),
4292                        BigUint::from(0u8),
4293                        8,
4294                        false,
4295                    ))
4296                    .unwrap()
4297            };
4298            let else_expr = arena
4299                .alloc(SLTNode::Constant(
4300                    BigUint::from(target + 32),
4301                    BigUint::from(0u8),
4302                    8,
4303                    false,
4304                ))
4305                .unwrap();
4306            let expr = arena
4307                .alloc(SLTNode::Mux {
4308                    cond: outer_condition,
4309                    then_expr,
4310                    else_expr,
4311                })
4312                .unwrap();
4313            paths.push(LogicPath {
4314                target: LogicPathTarget::Var(VarAtomBase::new(target, 0, 7)),
4315                sources: [
4316                    VarAtomBase::new(100, 0, 0),
4317                    VarAtomBase::new(101, 0, 0),
4318                    VarAtomBase::new(102, 0, 7),
4319                ]
4320                .into_iter()
4321                .collect(),
4322                previous_sources: crate::HashSet::default(),
4323                address_sources: crate::HashSet::default(),
4324                local_inputs: Vec::new(),
4325                order_before: crate::HashSet::default(),
4326                comb_capture_enable_sites: Vec::new(),
4327                comb_capture_enable_always: false,
4328                pre_lower_nodes: Vec::new(),
4329                expr,
4330            });
4331        }
4332
4333        let result = sort(
4334            paths,
4335            &arena,
4336            &crate::HashSet::default(),
4337            &crate::HashMap::default(),
4338            false,
4339            &crate::HashMap::default(),
4340            0,
4341        )
4342        .unwrap();
4343        let branch_count = result.execution_units[0]
4344            .blocks
4345            .values()
4346            .filter(|block| matches!(block.terminator, SIRTerminator::Branch { .. }))
4347            .count();
4348        assert_eq!(
4349            branch_count, 2,
4350            "outer region and nested Mux must both branch"
4351        );
4352    }
4353
4354    #[test]
4355    fn ff_plan_models_comb_and_ff_old_state_readers_before_writer() {
4356        let mut arena = SLTNodeArena::new();
4357        let comb_value = simple_path(&mut arena, 10, None);
4358        let comb_old_state_reader = simple_path(&mut arena, 11, Some(20));
4359        let ff = vec![
4360            FfAccessSummary {
4361                reads: vec![VarAtomBase::new(10, 0, 7), VarAtomBase::new(11, 0, 7)],
4362                writes: vec![VarAtomBase::new(20, 0, 7)],
4363                dynamic_writes: HashSet::default(),
4364            },
4365            FfAccessSummary {
4366                reads: vec![VarAtomBase::new(20, 0, 7)],
4367                writes: vec![VarAtomBase::new(30, 0, 7)],
4368                dynamic_writes: HashSet::default(),
4369            },
4370        ];
4371
4372        let plan = plan_ff_comb_schedule(&[comb_value, comb_old_state_reader], &ff).unwrap();
4373
4374        assert_eq!(plan.required_comb, vec![true, true]);
4375        assert_eq!(plan.comb_value_predecessors, vec![vec![0, 1], vec![]]);
4376        assert_eq!(plan.comb_before_direct_write, vec![vec![1], vec![]]);
4377        assert_eq!(plan.ff_before_direct_write, vec![vec![1], vec![]]);
4378    }
4379
4380    #[test]
4381    fn ff_plan_keeps_same_recipe_old_state_reads_local_to_lowering() {
4382        let summary = FfAccessSummary {
4383            reads: vec![VarAtomBase::new(20, 0, 7)],
4384            writes: vec![VarAtomBase::new(20, 0, 7)],
4385            dynamic_writes: HashSet::default(),
4386        };
4387
4388        let plan = plan_ff_comb_schedule(&[], &[summary]).unwrap();
4389
4390        assert_eq!(plan.ff_before_direct_write, vec![Vec::<usize>::new()]);
4391    }
4392
4393    #[test]
4394    fn ff_direct_write_proof_is_kept_per_bit_range() {
4395        let summaries = vec![
4396            FfAccessSummary {
4397                reads: vec![VarAtomBase::new(30, 0, 7)],
4398                writes: vec![VarAtomBase::new(20, 0, 7), VarAtomBase::new(20, 8, 15)],
4399                dynamic_writes: HashSet::default(),
4400            },
4401            FfAccessSummary {
4402                reads: vec![VarAtomBase::new(20, 0, 7)],
4403                writes: vec![VarAtomBase::new(30, 0, 7)],
4404                dynamic_writes: HashSet::default(),
4405            },
4406        ];
4407        let plan = plan_ff_comb_schedule(&[], &summaries).unwrap();
4408        // The FF0 -> FF1 edge is retained, while the inverse edge which only
4409        // protects FF0's low byte is dropped to break the cycle.
4410        let retained = [(0, 1)].into_iter().collect();
4411        let var_widths = [(20, 16), (30, 8)].into_iter().collect();
4412
4413        let direct = direct_ff_write_ranges(
4414            &[],
4415            &summaries,
4416            &plan,
4417            &retained,
4418            0,
4419            &var_widths,
4420            &HashMap::default(),
4421        );
4422
4423        assert_eq!(direct[0], vec![VarAtomBase::new(20, 8, 15)]);
4424        assert_eq!(direct[1], vec![VarAtomBase::new(30, 0, 7)]);
4425
4426        let local_old_read = vec![FfAccessSummary {
4427            reads: vec![VarAtomBase::new(20, 0, 7)],
4428            writes: vec![VarAtomBase::new(20, 0, 7), VarAtomBase::new(20, 8, 15)],
4429            dynamic_writes: HashSet::default(),
4430        }];
4431        let local_plan = plan_ff_comb_schedule(&[], &local_old_read).unwrap();
4432        let local_direct = direct_ff_write_ranges(
4433            &[],
4434            &local_old_read,
4435            &local_plan,
4436            &HashSet::default(),
4437            0,
4438            &var_widths,
4439            &HashMap::default(),
4440        );
4441
4442        assert_eq!(local_direct[0], vec![VarAtomBase::new(20, 8, 15)]);
4443    }
4444
4445    #[test]
4446    fn ff_direct_write_rejects_ranges_requiring_rmw() {
4447        let summary = FfAccessSummary {
4448            reads: Vec::new(),
4449            writes: vec![
4450                VarAtomBase::new(20, 0, 3),
4451                VarAtomBase::new(21, 0, 0),
4452                VarAtomBase::new(22, 0, 7),
4453                VarAtomBase::new(23, 6, 11),
4454                VarAtomBase::new(24, 8, 15),
4455            ],
4456            dynamic_writes: [22].into_iter().collect(),
4457        };
4458        let plan = plan_ff_comb_schedule(&[], std::slice::from_ref(&summary)).unwrap();
4459        let var_widths = [(20, 8), (21, 1), (22, 8), (23, 24), (24, 24)]
4460            .into_iter()
4461            .collect();
4462        let unpacked_element_widths = [(23, 6), (24, 8)].into_iter().collect();
4463
4464        let direct = direct_ff_write_ranges(
4465            &[],
4466            &[summary],
4467            &plan,
4468            &HashSet::default(),
4469            0,
4470            &var_widths,
4471            &unpacked_element_widths,
4472        );
4473
4474        assert_eq!(
4475            direct[0],
4476            vec![VarAtomBase::new(21, 0, 0), VarAtomBase::new(24, 8, 15)]
4477        );
4478    }
4479
4480    #[test]
4481    fn ff_direct_write_stages_complete_overlapping_components() {
4482        let summary = FfAccessSummary {
4483            reads: Vec::new(),
4484            writes: vec![
4485                VarAtomBase::new(20, 4, 19),
4486                VarAtomBase::new(20, 16, 31),
4487                VarAtomBase::new(20, 24, 31),
4488                VarAtomBase::new(20, 40, 47),
4489            ],
4490            dynamic_writes: HashSet::default(),
4491        };
4492        let plan = plan_ff_comb_schedule(&[], std::slice::from_ref(&summary)).unwrap();
4493        let var_widths = [(20, 64)].into_iter().collect();
4494
4495        let direct = direct_ff_write_ranges(
4496            &[],
4497            &[summary],
4498            &plan,
4499            &HashSet::default(),
4500            0,
4501            &var_widths,
4502            &HashMap::default(),
4503        );
4504
4505        // The unaligned first write is staged. Staging propagates through the
4506        // two transitively overlapping native ranges, but not the disjoint byte.
4507        assert_eq!(direct[0], vec![VarAtomBase::new(20, 40, 47)]);
4508    }
4509
4510    #[test]
4511    fn cyclic_direct_write_preference_falls_back_to_staging() {
4512        let mut adj = vec![vec![1], Vec::new(), Vec::new()];
4513
4514        add_acyclic_ff_write_order_edges(&mut adj, [(1, 0), (1, 2)]);
4515
4516        assert_eq!(adj, vec![vec![1], vec![2], Vec::new()]);
4517    }
4518
4519    #[test]
4520    fn memory_ssa_scheduler_lowers_independent_single_use_chains_contiguously() {
4521        let mut arena = SLTNodeArena::new();
4522        let paths = vec![
4523            simple_path(&mut arena, 10, None),
4524            simple_path(&mut arena, 20, None),
4525            simple_path(&mut arena, 11, Some(10)),
4526            simple_path(&mut arena, 21, Some(20)),
4527            simple_path(&mut arena, 12, Some(11)),
4528            simple_path(&mut arena, 22, Some(21)),
4529        ];
4530        let result = sort(
4531            paths,
4532            &arena,
4533            &crate::HashSet::default(),
4534            &crate::HashMap::default(),
4535            false,
4536            &crate::HashMap::default(),
4537            1,
4538        )
4539        .unwrap();
4540        let unit = &result.execution_units[0];
4541        let stores = unit.blocks[&unit.entry_block_id]
4542            .instructions
4543            .iter()
4544            .filter_map(|instruction| match instruction {
4545                SIRInstruction::Store(address, ..) => Some(*address),
4546                _ => None,
4547            })
4548            .collect::<Vec<_>>();
4549
4550        assert_eq!(stores, vec![10, 11, 12, 20, 21, 22]);
4551    }
4552
4553    #[test]
4554    fn previous_value_use_is_an_order_edge_not_a_forwarded_value() {
4555        let mut arena = SLTNodeArena::new();
4556        let mut previous_user = simple_path(&mut arena, 20, Some(10));
4557        previous_user.sources.clear();
4558        previous_user.previous_sources = [VarAtomBase::new(10, 0, 7)].into_iter().collect();
4559        let writer = simple_path(&mut arena, 10, None);
4560        let paths = vec![previous_user, writer];
4561
4562        let memory_ssa = build_logic_path_memory_ssa(&paths).unwrap();
4563        assert_eq!(memory_ssa.dependencies.users, vec![vec![1], vec![]]);
4564        assert_eq!(memory_ssa.dependencies.predecessors, vec![vec![], vec![0]]);
4565        assert_eq!(
4566            memory_ssa.values.users,
4567            vec![Vec::<usize>::new(), Vec::new()]
4568        );
4569        assert_eq!(
4570            memory_ssa.values.predecessors,
4571            vec![Vec::<usize>::new(), Vec::new()]
4572        );
4573
4574        let result = sort(
4575            paths,
4576            &arena,
4577            &crate::HashSet::default(),
4578            &crate::HashMap::default(),
4579            false,
4580            &crate::HashMap::default(),
4581            1,
4582        )
4583        .unwrap();
4584        let unit = &result.execution_units[0];
4585        let stores = unit.blocks[&unit.entry_block_id]
4586            .instructions
4587            .iter()
4588            .filter_map(|instruction| match instruction {
4589                SIRInstruction::Store(address, ..) => Some(*address),
4590                _ => None,
4591            })
4592            .collect::<Vec<_>>();
4593        assert_eq!(stores, vec![20, 10]);
4594    }
4595
4596    #[test]
4597    fn memory_use_depends_on_each_overlapping_bit_range_definition() {
4598        let mut arena = SLTNodeArena::new();
4599        let mut low = simple_path(&mut arena, 10, None);
4600        low.target = LogicPathTarget::Var(VarAtomBase::new(10, 0, 3));
4601        let mut high = simple_path(&mut arena, 10, None);
4602        high.target = LogicPathTarget::Var(VarAtomBase::new(10, 4, 7));
4603        let mut user = simple_path(&mut arena, 20, Some(10));
4604        user.sources = [VarAtomBase::new(10, 2, 5)].into_iter().collect();
4605
4606        let memory_ssa = build_logic_path_memory_ssa(&[low, high, user]).unwrap();
4607        assert_eq!(
4608            memory_ssa.dependencies.users,
4609            vec![vec![2], vec![2], vec![]]
4610        );
4611        assert_eq!(
4612            memory_ssa.dependencies.predecessors,
4613            vec![vec![], vec![], vec![0, 1]]
4614        );
4615        assert_eq!(memory_ssa.values.users, vec![vec![2], vec![2], vec![]]);
4616        assert_eq!(
4617            memory_ssa.values.predecessors,
4618            vec![vec![], vec![], vec![0, 1]]
4619        );
4620    }
4621
4622    fn fixed_group_path(
4623        arena: &mut SLTNodeArena<u32>,
4624        guard: crate::NodeId,
4625        loop_var: u32,
4626        target: u32,
4627        external: u32,
4628        trip_count: usize,
4629    ) -> LogicPath<u32> {
4630        fixed_group_path_with_index(
4631            arena,
4632            guard,
4633            loop_var,
4634            target,
4635            external,
4636            trip_count,
4637            1,
4638            BitAccess::new(0, 7),
4639        )
4640    }
4641
4642    #[allow(clippy::too_many_arguments)]
4643    fn fixed_group_path_with_index(
4644        arena: &mut SLTNodeArena<u32>,
4645        guard: crate::NodeId,
4646        loop_var: u32,
4647        target: u32,
4648        external: u32,
4649        trip_count: usize,
4650        index_scale: u8,
4651        load_access: BitAccess,
4652    ) -> LogicPath<u32> {
4653        let target = VarAtomBase::new(target, 0, 7);
4654        let initial = arena
4655            .alloc(SLTNode::Input {
4656                variable: target.id,
4657                signed: false,
4658                index: Vec::new(),
4659                access: target.access,
4660            })
4661            .unwrap();
4662        let loop_index = arena
4663            .alloc(SLTNode::Input {
4664                variable: loop_var,
4665                signed: false,
4666                index: Vec::new(),
4667                access: BitAccess::new(0, 7),
4668            })
4669            .unwrap();
4670        let loop_index = if index_scale == 1 {
4671            loop_index
4672        } else {
4673            let scale = arena
4674                .alloc(SLTNode::Constant(
4675                    BigUint::from(index_scale),
4676                    BigUint::from(0u8),
4677                    8,
4678                    false,
4679                ))
4680                .unwrap();
4681            arena
4682                .alloc(SLTNode::Binary(loop_index, BinaryOp::Mul, scale))
4683                .unwrap()
4684        };
4685        let update = arena
4686            .alloc(SLTNode::Input {
4687                variable: external,
4688                signed: false,
4689                index: vec![
4690                    serde_json::from_value(serde_json::json!({
4691                        "node": loop_index,
4692                        "stride": 8,
4693                        "kind": "Packed",
4694                    }))
4695                    .unwrap(),
4696                ],
4697                access: load_access,
4698            })
4699            .unwrap();
4700        let group = arena
4701            .alloc(SLTNode::ForFoldGroup {
4702                loop_var,
4703                loop_width: 8,
4704                loop_signed: false,
4705                start: BigInt::from(0),
4706                step: BigInt::from(1),
4707                trip_count,
4708                entry_guard: guard,
4709                states: vec![SLTForFoldGroupState {
4710                    target,
4711                    initial,
4712                    update,
4713                }],
4714            })
4715            .unwrap();
4716        LogicPath {
4717            target: LogicPathTarget::Var(target),
4718            sources: [VarAtomBase::new(external, 0, 63)].into_iter().collect(),
4719            previous_sources: crate::HashSet::default(),
4720            address_sources: crate::HashSet::default(),
4721            local_inputs: Vec::new(),
4722            order_before: crate::HashSet::default(),
4723            comb_capture_enable_sites: Vec::new(),
4724            comb_capture_enable_always: false,
4725            pre_lower_nodes: Vec::new(),
4726            expr: group,
4727        }
4728    }
4729
4730    fn fixed_group_fixture(
4731        left_trip_count: usize,
4732        right_trip_count: usize,
4733    ) -> (
4734        SLTNodeArena<u32>,
4735        Vec<LogicPath<u32>>,
4736        crate::HashMap<u32, usize>,
4737    ) {
4738        let mut arena = SLTNodeArena::new();
4739        let guard = arena
4740            .alloc(SLTNode::Constant(
4741                BigUint::from(1u8),
4742                BigUint::from(0u8),
4743                1,
4744                false,
4745            ))
4746            .unwrap();
4747        let paths = vec![
4748            fixed_group_path(&mut arena, guard, 100, 10, 50, left_trip_count),
4749            fixed_group_path(&mut arena, guard, 101, 11, 50, right_trip_count),
4750        ];
4751        let widths = [(10, 8), (11, 8)].into_iter().collect();
4752        (arena, paths, widths)
4753    }
4754
4755    fn schedule_branch_count(result: &super::ScheduleResult<u32>) -> usize {
4756        result
4757            .execution_units
4758            .iter()
4759            .flat_map(|unit| unit.blocks.values())
4760            .filter(|block| matches!(block.terminator, SIRTerminator::Branch { .. }))
4761            .count()
4762    }
4763
4764    fn synthetic_exact_load(base: u32) -> ExactIndexedLoadKey<u32> {
4765        ExactIndexedLoadKey {
4766            base,
4767            access: BitAccess::new(0, 7),
4768            index: vec![(
4769                NormalizedIndexExpr::LoopValue {
4770                    signed: false,
4771                    access: BitAccess::new(0, 7),
4772                },
4773                8,
4774            )],
4775        }
4776    }
4777
4778    fn synthetic_fold_group(
4779        root: usize,
4780        target: u32,
4781        loop_var: u32,
4782        indexed_loads: impl IntoIterator<Item = ExactIndexedLoadKey<u32>>,
4783        carried_chunks: u128,
4784    ) -> ExactFoldGroup<u32> {
4785        ExactFoldGroup {
4786            root: crate::NodeId(root),
4787            facts: FoldGroupReadFacts {
4788                loop_var,
4789                state_targets: vec![VarAtomBase::new(target, 0, 7)],
4790                guard_reads: Vec::new(),
4791                initial_reads: Vec::new(),
4792                update_reads: Vec::new(),
4793                indexed_loads: indexed_loads.into_iter().collect(),
4794                carried_chunks,
4795            },
4796        }
4797    }
4798
4799    #[test]
4800    fn independent_exact_fold_groups_lower_jointly_and_keep_store_order() {
4801        let (arena, paths, widths) = fixed_group_fixture(4, 4);
4802        let result = sort(
4803            paths,
4804            &arena,
4805            &crate::HashSet::default(),
4806            &crate::HashMap::default(),
4807            false,
4808            &widths,
4809            1,
4810        )
4811        .unwrap();
4812
4813        assert_eq!(schedule_branch_count(&result), 2);
4814        let store_block = result
4815            .execution_units
4816            .iter()
4817            .flat_map(|unit| unit.blocks.values())
4818            .find(|block| {
4819                block
4820                    .instructions
4821                    .iter()
4822                    .filter(|instruction| matches!(instruction, SIRInstruction::Store(..)))
4823                    .count()
4824                    == 2
4825            })
4826            .expect("joint results must be materialized before the ordered stores");
4827        let stores = store_block
4828            .instructions
4829            .iter()
4830            .filter_map(|instruction| match instruction {
4831                SIRInstruction::Store(address, _, _, _, _, _) => Some(*address),
4832                _ => None,
4833            })
4834            .collect::<Vec<_>>();
4835        assert_eq!(stores, vec![10, 11]);
4836    }
4837
4838    #[test]
4839    fn different_index_expression_or_load_slice_prevents_joint_lowering() {
4840        let mut arena = SLTNodeArena::new();
4841        let guard = arena
4842            .alloc(SLTNode::Constant(
4843                BigUint::from(1u8),
4844                BigUint::from(0u8),
4845                1,
4846                false,
4847            ))
4848            .unwrap();
4849        let paths = vec![
4850            fixed_group_path_with_index(&mut arena, guard, 100, 10, 50, 4, 1, BitAccess::new(0, 7)),
4851            fixed_group_path_with_index(&mut arena, guard, 101, 11, 50, 4, 2, BitAccess::new(0, 7)),
4852            fixed_group_path_with_index(
4853                &mut arena,
4854                guard,
4855                102,
4856                12,
4857                50,
4858                4,
4859                1,
4860                BitAccess::new(8, 15),
4861            ),
4862        ];
4863        let widths = [(10, 8), (11, 8), (12, 8)].into_iter().collect();
4864        let result = sort(
4865            paths,
4866            &arena,
4867            &crate::HashSet::default(),
4868            &crate::HashMap::default(),
4869            false,
4870            &widths,
4871            1,
4872        )
4873        .unwrap();
4874
4875        assert_eq!(schedule_branch_count(&result), 6);
4876    }
4877
4878    #[test]
4879    fn weighted_family_selection_ignores_conflicting_first_root() {
4880        let shared = synthetic_exact_load(50);
4881        let candidates = vec![
4882            synthetic_fold_group(0, 10, 100, [shared.clone()], 1),
4883            synthetic_fold_group(1, 11, 101, [shared.clone()], 1),
4884            synthetic_fold_group(2, 12, 102, [shared], 1),
4885        ];
4886        let available = vec![true; 3];
4887        let compatible = vec![
4888            vec![false, false, false],
4889            vec![false, false, true],
4890            vec![false, true, false],
4891        ];
4892        let shared_load = vec![
4893            vec![false, true, true],
4894            vec![true, false, true],
4895            vec![true, true, false],
4896        ];
4897
4898        let family = best_weighted_fold_family(
4899            &candidates,
4900            &available,
4901            &compatible,
4902            &shared_load,
4903            &crate::HashSet::default(),
4904            false,
4905        )
4906        .expect("the compatible B+C family has positive net benefit");
4907        let mut roots = family
4908            .members
4909            .iter()
4910            .map(|member| candidates[*member].root.0)
4911            .collect::<Vec<_>>();
4912        roots.sort_unstable();
4913        assert_eq!(roots, vec![1, 2]);
4914    }
4915
4916    #[test]
4917    fn weighted_family_selection_rejects_benefit_not_exceeding_pressure() {
4918        let shared = synthetic_exact_load(50);
4919        let candidates = vec![
4920            synthetic_fold_group(0, 10, 100, [shared.clone()], 8),
4921            synthetic_fold_group(1, 11, 101, [shared], 8),
4922        ];
4923        let available = vec![true; 2];
4924        let compatible = vec![vec![false, true], vec![true, false]];
4925        let shared_load = compatible.clone();
4926
4927        assert!(
4928            best_weighted_fold_family(
4929                &candidates,
4930                &available,
4931                &compatible,
4932                &shared_load,
4933                &crate::HashSet::default(),
4934                false,
4935            )
4936            .is_none()
4937        );
4938    }
4939
4940    #[test]
4941    fn dependency_edge_separates_otherwise_joint_fold_groups() {
4942        let (arena, mut paths, widths) = fixed_group_fixture(4, 4);
4943        paths[1].sources.insert(VarAtomBase::new(10, 0, 7));
4944        let result = sort(
4945            paths,
4946            &arena,
4947            &crate::HashSet::default(),
4948            &crate::HashMap::default(),
4949            false,
4950            &widths,
4951            1,
4952        )
4953        .unwrap();
4954
4955        assert_eq!(schedule_branch_count(&result), 4);
4956    }
4957
4958    #[test]
4959    fn different_fold_group_domains_do_not_lower_jointly() {
4960        let (arena, paths, widths) = fixed_group_fixture(4, 5);
4961        let result = sort(
4962            paths,
4963            &arena,
4964            &crate::HashSet::default(),
4965            &crate::HashMap::default(),
4966            false,
4967            &widths,
4968            1,
4969        )
4970        .unwrap();
4971
4972        assert_eq!(schedule_branch_count(&result), 4);
4973    }
4974
4975    #[test]
4976    fn joint_fold_preparation_does_not_mutate_logic_path_metadata() {
4977        let (arena, mut paths, _) = fixed_group_fixture(4, 4);
4978        paths[0].previous_sources = [VarAtomBase::new(60, 0, 7)].into_iter().collect();
4979        paths[0].sources.insert(VarAtomBase::new(61, 0, 7));
4980        paths[0].address_sources = [VarAtomBase::new(61, 0, 7)].into_iter().collect();
4981        paths[0].comb_capture_enable_sites = vec![3, 7];
4982        let snapshot = paths.clone();
4983        let mut builder = SIRBuilder::new();
4984        let mut cache = crate::HashMap::default();
4985        let mut dependencies = crate::HashMap::default();
4986        let mut inverse_dependencies = crate::HashMap::default();
4987        let schedule_index = build_fold_group_schedule_index(&paths, &arena);
4988
4989        let prepared = prepare_atomic_fold_group_results(
4990            &[0, 1],
4991            &paths,
4992            &schedule_index,
4993            &SLTToSIRLowerer::new(false),
4994            &mut builder,
4995            &arena,
4996            &mut cache,
4997            &mut dependencies,
4998            &mut inverse_dependencies,
4999            false,
5000        );
5001
5002        assert_eq!(prepared.len(), 2);
5003        assert_eq!(paths, snapshot);
5004    }
5005
5006    #[test]
5007    fn for_fold_group_dependencies_keep_initial_but_hide_loop_scoped_updates() {
5008        let mut arena = SLTNodeArena::<u32>::new();
5009        let input = |arena: &mut SLTNodeArena<u32>, variable| {
5010            arena
5011                .alloc(SLTNode::Input {
5012                    variable,
5013                    signed: false,
5014                    index: Vec::new(),
5015                    access: BitAccess::new(0, 7),
5016                })
5017                .unwrap()
5018        };
5019        let guard = arena
5020            .alloc(SLTNode::Constant(
5021                BigUint::from(1u8),
5022                BigUint::from(0u8),
5023                1,
5024                false,
5025            ))
5026            .unwrap();
5027        let initial = input(&mut arena, 2);
5028        let state_input = input(&mut arena, 2);
5029        let loop_input = input(&mut arena, 1);
5030        let external_input = input(&mut arena, 3);
5031        let scoped_sum = arena
5032            .alloc(SLTNode::Binary(state_input, BinaryOp::Add, loop_input))
5033            .unwrap();
5034        let update = arena
5035            .alloc(SLTNode::Binary(scoped_sum, BinaryOp::Add, external_input))
5036            .unwrap();
5037        let group = arena
5038            .alloc(SLTNode::ForFoldGroup {
5039                loop_var: 1,
5040                loop_width: 8,
5041                loop_signed: false,
5042                start: BigInt::from(0),
5043                step: BigInt::from(1),
5044                trip_count: 2,
5045                entry_guard: guard,
5046                states: vec![SLTForFoldGroupState {
5047                    target: VarAtomBase::new(2, 0, 7),
5048                    initial,
5049                    update,
5050                }],
5051            })
5052            .unwrap();
5053        let mut memo = crate::HashMap::default();
5054        let mut inverse_memo = crate::HashMap::default();
5055
5056        let dependencies = collect_node_input_deps(group, &arena, &mut memo, &mut inverse_memo);
5057
5058        assert!(
5059            dependencies.contains(&2),
5060            "initial state is an external dependency"
5061        );
5062        assert!(
5063            dependencies.contains(&3),
5064            "ordinary update input remains external"
5065        );
5066        assert!(
5067            !dependencies.contains(&1),
5068            "loop variable is supplied by the fold"
5069        );
5070    }
5071
5072    #[test]
5073    fn partial_for_fold_group_state_keeps_uncovered_variable_dependency() {
5074        let mut arena = SLTNodeArena::<u32>::new();
5075        let guard = arena
5076            .alloc(SLTNode::Constant(
5077                BigUint::from(1u8),
5078                BigUint::from(0u8),
5079                1,
5080                false,
5081            ))
5082            .unwrap();
5083        let initial = arena
5084            .alloc(SLTNode::Constant(
5085                BigUint::from(0u8),
5086                BigUint::from(0u8),
5087                8,
5088                false,
5089            ))
5090            .unwrap();
5091        let carried = arena
5092            .alloc(SLTNode::Input {
5093                variable: 2,
5094                signed: false,
5095                index: Vec::new(),
5096                access: BitAccess::new(0, 7),
5097            })
5098            .unwrap();
5099        let uncovered = arena
5100            .alloc(SLTNode::Input {
5101                variable: 2,
5102                signed: false,
5103                index: Vec::new(),
5104                access: BitAccess::new(8, 15),
5105            })
5106            .unwrap();
5107        let update = arena
5108            .alloc(SLTNode::Binary(carried, BinaryOp::Add, uncovered))
5109            .unwrap();
5110        let group = arena
5111            .alloc(SLTNode::ForFoldGroup {
5112                loop_var: 1,
5113                loop_width: 8,
5114                loop_signed: false,
5115                start: BigInt::from(0),
5116                step: BigInt::from(1),
5117                trip_count: 2,
5118                entry_guard: guard,
5119                states: vec![SLTForFoldGroupState {
5120                    target: VarAtomBase::new(2, 0, 7),
5121                    initial,
5122                    update,
5123                }],
5124            })
5125            .unwrap();
5126        let mut memo = crate::HashMap::default();
5127        let mut inverse_memo = crate::HashMap::default();
5128
5129        let dependencies = collect_node_input_deps(group, &arena, &mut memo, &mut inverse_memo);
5130        assert!(
5131            dependencies.contains(&2),
5132            "the uncovered high byte remains an external dependency"
5133        );
5134    }
5135
5136    #[test]
5137    fn shared_for_fold_group_projections_materialize_once_and_store_sequentially() {
5138        let mut arena = SLTNodeArena::<u32>::new();
5139        let guard = arena
5140            .alloc(SLTNode::Constant(
5141                BigUint::from(1u8),
5142                BigUint::from(0u8),
5143                1,
5144                false,
5145            ))
5146            .unwrap();
5147        let input = |arena: &mut SLTNodeArena<u32>, variable| {
5148            arena
5149                .alloc(SLTNode::Input {
5150                    variable,
5151                    signed: false,
5152                    index: Vec::new(),
5153                    access: BitAccess::new(0, 7),
5154                })
5155                .unwrap()
5156        };
5157        let initial_a = input(&mut arena, 1);
5158        let initial_b = input(&mut arena, 2);
5159        let previous_a = input(&mut arena, 1);
5160        let previous_b = input(&mut arena, 2);
5161        let group = arena
5162            .alloc(SLTNode::ForFoldGroup {
5163                loop_var: 3,
5164                loop_width: 8,
5165                loop_signed: false,
5166                start: BigInt::from(0),
5167                step: BigInt::from(1),
5168                trip_count: 3,
5169                entry_guard: guard,
5170                states: vec![
5171                    SLTForFoldGroupState {
5172                        target: VarAtomBase::new(1, 0, 7),
5173                        initial: initial_a,
5174                        update: previous_b,
5175                    },
5176                    SLTForFoldGroupState {
5177                        target: VarAtomBase::new(2, 0, 7),
5178                        initial: initial_b,
5179                        update: previous_a,
5180                    },
5181                ],
5182            })
5183            .unwrap();
5184        let high = arena
5185            .alloc(SLTNode::Slice {
5186                expr: group,
5187                access: BitAccess::new(8, 15),
5188            })
5189            .unwrap();
5190        let low = arena
5191            .alloc(SLTNode::Slice {
5192                expr: group,
5193                access: BitAccess::new(0, 7),
5194            })
5195            .unwrap();
5196        let path = |target, expr| LogicPath {
5197            target: LogicPathTarget::Var(VarAtomBase::new(target, 0, 7)),
5198            // Keep the scheduling graph acyclic in this focused cache test;
5199            // dependency memoization still sees both initial reads.
5200            sources: crate::HashSet::default(),
5201            previous_sources: crate::HashSet::default(),
5202            address_sources: crate::HashSet::default(),
5203            local_inputs: Vec::new(),
5204            order_before: crate::HashSet::default(),
5205            comb_capture_enable_sites: Vec::new(),
5206            comb_capture_enable_always: false,
5207            pre_lower_nodes: Vec::new(),
5208            expr,
5209        };
5210        let mut widths = crate::HashMap::default();
5211        widths.insert(1, 8);
5212        widths.insert(2, 8);
5213
5214        let result = sort(
5215            vec![path(1, high), path(2, low)],
5216            &arena,
5217            &crate::HashSet::default(),
5218            &crate::HashMap::default(),
5219            false,
5220            &widths,
5221            1,
5222        )
5223        .unwrap();
5224        assert_eq!(result.execution_units.len(), 1);
5225        let eu = &result.execution_units[0];
5226        assert_eq!(
5227            eu.blocks
5228                .values()
5229                .filter(|block| matches!(block.terminator, SIRTerminator::Branch { .. }))
5230                .count(),
5231            2,
5232            "one grouped fold has only its entry and counted-loop branches"
5233        );
5234        assert_eq!(
5235            eu.blocks
5236                .values()
5237                .flat_map(|block| &block.instructions)
5238                .filter(|instruction| matches!(instruction, SIRInstruction::Load(..)))
5239                .count(),
5240            2,
5241            "both initial values are loaded once, not once per projection"
5242        );
5243
5244        let store_block = eu
5245            .blocks
5246            .values()
5247            .find(|block| {
5248                block
5249                    .instructions
5250                    .iter()
5251                    .filter(|instruction| matches!(instruction, SIRInstruction::Store(..)))
5252                    .count()
5253                    == 2
5254            })
5255            .expect("both atomic projection stores share the materialization exit");
5256        let store_positions = store_block
5257            .instructions
5258            .iter()
5259            .enumerate()
5260            .filter_map(|(position, instruction)| {
5261                matches!(instruction, SIRInstruction::Store(..)).then_some(position)
5262            })
5263            .collect::<Vec<_>>();
5264        let stored_values = store_block
5265            .instructions
5266            .iter()
5267            .filter_map(|instruction| match instruction {
5268                SIRInstruction::Store(_, _, _, value, _, _) => Some(*value),
5269                _ => None,
5270            })
5271            .collect::<Vec<_>>();
5272        assert_eq!(store_positions.len(), stored_values.len());
5273        for (projection, value) in stored_values.into_iter().enumerate() {
5274            let definition = store_block
5275                .instructions
5276                .iter()
5277                .position(|instruction| match instruction {
5278                    SIRInstruction::Binary(dst, ..)
5279                    | SIRInstruction::Unary(dst, ..)
5280                    | SIRInstruction::Slice(dst, ..)
5281                    | SIRInstruction::Concat(dst, ..)
5282                    | SIRInstruction::Mux(dst, ..) => *dst == value,
5283                    _ => false,
5284                })
5285                .expect("stored projection has a local definition");
5286            assert!(definition < store_positions[projection]);
5287            if projection != 0 {
5288                assert!(
5289                    definition > store_positions[projection - 1],
5290                    "a later projection must not remain live across an earlier Store"
5291                );
5292            }
5293        }
5294    }
5295}