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#[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 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 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 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 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 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; max_delay
124 }
125
126 let mut overall_max_delay = 0;
127 let mut visited = vec![false; adj.len()];
128
129 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 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
166fn 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
523fn 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
563struct 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 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
700pub 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
724fn 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
740fn 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 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 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 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 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 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 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 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
2305fn 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 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 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
2699fn 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
2765fn 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
2805fn 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
2919fn 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 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 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 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 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 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
3196fn 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
3267fn 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 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 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 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 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 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 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 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 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 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 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 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 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 let safety_limit = user_safety_limit.unwrap_or(iterations + 1);
3659
3660 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 let current_counter = builder.alloc_bit(64, false);
3672 let header_block = builder.new_block_with(vec![current_counter]); let body_block = builder.new_block();
3674 let exit_block = builder.new_block();
3675 let error_block = builder.new_block(); builder.seal_block(SIRTerminator::Jump(header_block, vec![zero_reg]));
3679
3680 builder.switch_to_block(header_block);
3682
3683 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 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 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 let new_val_reg = lower_logic_path_expr(
3738 &lowerer,
3739 &mut builder,
3740 path,
3741 arena,
3742 &mut lower_cache,
3743 );
3744
3745 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, new_val_reg,
3752 ));
3753 let new_dirty_reg = builder.alloc_bit(1, false);
3754
3755 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 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 }
3796
3797 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 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 builder.switch_to_block(error_block);
3818 builder.seal_block(SIRTerminator::Error(runtime_error_code));
3821
3822 builder.switch_to_block(exit_block);
3824 }
3825 } else {
3826 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 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_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 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 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 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 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 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 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}