1use crate::{NodeId, SLTForFoldGroupState, SLTLoopBound, SLTNode, SLTNodeArena, SLTStepOp};
2use celox_design::{BinaryOp, BitAccess, UnaryOp, VarAtomBase};
3use celox_sir::{
4 RegisterId, RegisterType, SIRBuilder, SIRInstruction, SIROffset, SIRTerminator, SIRValue,
5};
6use num_bigint::{BigInt, BigUint};
7use num_traits::{ToPrimitive, Zero};
8use std::cell::RefCell;
9use std::hash::Hash;
10
11fn slt_value_mask(width: usize) -> BigUint {
12 (BigUint::from(1u8) << width) - BigUint::from(1u8)
13}
14
15fn try_const_eval<A: Hash + Eq + Clone>(
18 node_id: NodeId,
19 arena: &SLTNodeArena<A>,
20) -> Option<(BigUint, BigUint)> {
21 match arena.get(node_id) {
22 SLTNode::Constant(val, mask, width, _signed) => {
23 let width_mask = slt_value_mask(*width);
24 Some((val & &width_mask, mask & width_mask))
25 }
26 SLTNode::Binary(lhs, op, rhs) => {
27 let (lv, lm) = try_const_eval(*lhs, arena)?;
28 let (rv, rm) = try_const_eval(*rhs, arena)?;
29 if lm != BigUint::from(0u32) || rm != BigUint::from(0u32) {
31 return None;
32 }
33 let width = crate::get_width(node_id, arena);
34 let width_mask = slt_value_mask(width);
35 let result = match op {
36 BinaryOp::And => &lv & &rv,
37 BinaryOp::Or => &lv | &rv,
38 BinaryOp::Xor => &lv ^ &rv,
39 BinaryOp::Add => &lv + &rv,
40 BinaryOp::Sub => {
41 let modulus = BigUint::from(1u8) << width;
42 (&lv + modulus - &rv) & &width_mask
43 }
44 _ => return None,
45 };
46 Some((result & width_mask, BigUint::from(0u32)))
47 }
48 SLTNode::Unary(_, _) => None,
49 SLTNode::Concat(parts) => {
50 let mut combined_val = BigUint::from(0u32);
51 let mut total_width = 0usize;
52 for (part_node, part_width) in parts.iter().rev() {
53 let (v, m) = try_const_eval(*part_node, arena)?;
54 if m != BigUint::from(0u32) {
55 return None;
56 }
57 let width_mask = if *part_width >= 64 {
58 (BigUint::from(1u64) << part_width) - 1u64
59 } else {
60 BigUint::from((1u64 << part_width) - 1)
61 };
62 combined_val |= (&v & &width_mask) << total_width;
63 total_width += part_width;
64 }
65 Some((combined_val, BigUint::from(0u32)))
66 }
67 SLTNode::Slice { expr, access } => {
68 let (v, m) = try_const_eval(*expr, arena)?;
69 if m != BigUint::from(0u32) {
70 return None;
71 }
72 let width = access.msb - access.lsb + 1;
73 let shifted = &v >> access.lsb;
74 let width_mask = if width >= 64 {
75 (BigUint::from(1u64) << width) - 1u64
76 } else {
77 BigUint::from((1u64 << width) - 1)
78 };
79 Some((shifted & width_mask, BigUint::from(0u32)))
80 }
81 _ => None, }
83}
84
85#[derive(Clone)]
86enum SLTBitOrigin<A: Hash + Eq + Clone> {
87 Node(NodeId),
88 Input {
89 node: NodeId,
93 variable: A,
94 signed: bool,
95 index: Vec<crate::SLTIndex>,
96 },
97}
98
99impl<A: Hash + Eq + Clone> PartialEq for SLTBitOrigin<A> {
100 fn eq(&self, other: &Self) -> bool {
101 match (self, other) {
102 (Self::Node(lhs), Self::Node(rhs)) => lhs == rhs,
103 (
104 Self::Input {
105 variable: lhs_variable,
106 signed: lhs_signed,
107 index: lhs_index,
108 ..
109 },
110 Self::Input {
111 variable: rhs_variable,
112 signed: rhs_signed,
113 index: rhs_index,
114 ..
115 },
116 ) => lhs_variable == rhs_variable && lhs_signed == rhs_signed && lhs_index == rhs_index,
117 _ => false,
118 }
119 }
120}
121
122impl<A: Hash + Eq + Clone> Eq for SLTBitOrigin<A> {}
123
124impl<A: Hash + Eq + Clone> Hash for SLTBitOrigin<A> {
125 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
126 match self {
127 Self::Node(node) => {
128 0u8.hash(state);
129 node.hash(state);
130 }
131 Self::Input {
132 variable,
133 signed,
134 index,
135 ..
136 } => {
137 1u8.hash(state);
138 variable.hash(state);
139 signed.hash(state);
140 index.hash(state);
141 }
142 }
143 }
144}
145
146#[derive(Clone)]
147struct SLTBitTerm<A: Hash + Eq + Clone> {
148 predicate: NodeId,
149 origin: Option<(SLTBitOrigin<A>, usize)>,
150}
151
152enum SLTCountPredicate {
153 Node(NodeId),
154 And(NodeId, NodeId),
155}
156
157enum SLTVectorExpr<A: Hash + Eq + Clone> {
158 Origin(SLTBitOrigin<A>),
159 StaticInput {
163 variable: A,
164 access: BitAccess,
165 unpacked_element_width: Option<usize>,
166 },
167 Broadcast(NodeId),
168 LowOnes {
169 bound: NodeId,
170 },
171 Not(Box<SLTVectorExpr<A>>),
172 Binary {
173 lhs: Box<SLTVectorExpr<A>>,
174 op: BinaryOp,
175 rhs: Box<SLTVectorExpr<A>>,
176 },
177}
178
179enum SLTCountInput<A: Hash + Eq + Clone> {
180 Origin(SLTBitOrigin<A>),
181 Vector(SLTVectorExpr<A>),
182 Predicates(Vec<SLTCountPredicate>),
183}
184
185struct SLTCountPlan<A: Hash + Eq + Clone> {
186 op: UnaryOp,
187 input_width: usize,
188 input: SLTCountInput<A>,
189 post: SLTCountPost,
190}
191
192enum SLTCountPost {
193 Direct,
194 AddTo(NodeId),
197 SubtractFrom(u64),
200 ReplaceZeroInputCount(u64),
203 Select {
205 cond: NodeId,
206 false_value: NodeId,
207 },
208}
209
210fn slt_const_u64<A: Hash + Eq + Clone>(node: NodeId, arena: &SLTNodeArena<A>) -> Option<u64> {
211 let (value, mask) = try_const_eval(node, arena)?;
212 if mask != BigUint::from(0u8) {
213 return None;
214 }
215 match value.to_u64_digits().as_slice() {
216 [] => Some(0),
217 [value] => Some(*value),
218 _ => None,
219 }
220}
221
222fn slt_literal_u64<A: Hash + Eq + Clone>(node: NodeId, arena: &SLTNodeArena<A>) -> Option<u64> {
223 let SLTNode::Constant(value, mask, _, _) = arena.get(node) else {
224 return None;
225 };
226 if mask != &BigUint::from(0u8) {
227 return None;
228 }
229 match value.to_u64_digits().as_slice() {
230 [] => Some(0),
231 [value] => Some(*value),
232 _ => None,
233 }
234}
235
236fn slt_width<A: Hash + Eq + Clone>(node: NodeId, arena: &SLTNodeArena<A>) -> usize {
237 crate::get_width(node, arena)
238}
239
240fn unwrap_slt_one_bit_procedural_truth<A: Hash + Eq + Clone>(
244 node: NodeId,
245 arena: &SLTNodeArena<A>,
246) -> NodeId {
247 if let SLTNode::Unary(UnaryOp::ToTwoState, truth) = arena.get(node)
248 && let SLTNode::Unary(UnaryOp::Or, inner) = arena.get(*truth)
249 && slt_width(*inner, arena) == 1
250 {
251 *inner
252 } else {
253 node
254 }
255}
256
257fn slt_literal_zero_of_width<A: Hash + Eq + Clone>(
258 node: NodeId,
259 width: usize,
260 arena: &SLTNodeArena<A>,
261) -> bool {
262 slt_width(node, arena) == width && slt_literal_u64(node, arena) == Some(0)
263}
264
265fn slt_width_can_represent(width: usize, maximum: usize) -> bool {
266 width >= usize::BITS as usize || maximum < (1usize << width)
267}
268
269fn resolve_slt_bit_origin<A: Hash + Eq + Clone>(
270 node: NodeId,
271 arena: &SLTNodeArena<A>,
272) -> Option<(SLTBitOrigin<A>, usize)> {
273 let node = unwrap_slt_one_bit_procedural_truth(node, arena);
274 match arena.get(node) {
275 SLTNode::Input {
276 variable,
277 signed,
278 index,
279 access,
280 } if access.msb == access.lsb => Some((
281 SLTBitOrigin::Input {
282 node,
283 variable: variable.clone(),
284 signed: *signed,
285 index: index.clone(),
286 },
287 access.lsb,
288 )),
289 SLTNode::Slice { expr, access } if access.msb == access.lsb => {
290 Some((SLTBitOrigin::Node(*expr), access.lsb))
291 }
292 SLTNode::Unary(UnaryOp::Ident, inner) => resolve_slt_bit_origin(*inner, arena),
293 SLTNode::Binary(lhs, BinaryOp::Eq, rhs) => {
294 if slt_const_u64(*lhs, arena) == Some(1) {
295 resolve_slt_bit_origin(*rhs, arena)
296 } else if slt_const_u64(*rhs, arena) == Some(1) {
297 resolve_slt_bit_origin(*lhs, arena)
298 } else {
299 None
300 }
301 }
302 SLTNode::Binary(lhs, BinaryOp::And, rhs) => {
303 let shifted = if slt_const_u64(*lhs, arena) == Some(1) {
304 *rhs
305 } else if slt_const_u64(*rhs, arena) == Some(1) {
306 *lhs
307 } else {
308 return None;
309 };
310 match arena.get(shifted) {
311 SLTNode::Binary(source, BinaryOp::Shr, amount) => Some((
312 SLTBitOrigin::Node(*source),
313 slt_const_u64(*amount, arena)? as usize,
314 )),
315 _ if slt_width(shifted, arena) == 1 => Some((SLTBitOrigin::Node(shifted), 0)),
316 _ => None,
317 }
318 }
319 _ => None,
320 }
321}
322
323fn resolve_slt_extended_bit<A: Hash + Eq + Clone>(
324 node: NodeId,
325 arena: &SLTNodeArena<A>,
326) -> Option<SLTBitTerm<A>> {
327 let node = unwrap_slt_one_bit_procedural_truth(node, arena);
328 if slt_width(node, arena) == 1 {
329 return Some(SLTBitTerm {
330 predicate: node,
331 origin: resolve_slt_bit_origin(node, arena),
332 });
333 }
334 match arena.get(node) {
335 SLTNode::Unary(UnaryOp::Ident, inner) => resolve_slt_extended_bit(*inner, arena),
336 SLTNode::Concat(parts) => {
337 let (least_significant, leading) = parts.split_last()?;
341 if leading
342 .iter()
343 .any(|(part, _)| slt_literal_u64(*part, arena) != Some(0))
344 {
345 return None;
346 }
347 resolve_slt_extended_bit(least_significant.0, arena)
348 }
349 _ => None,
350 }
351}
352
353fn common_complete_slt_origin<A: Hash + Eq + Clone>(
354 terms: &[SLTBitTerm<A>],
355 arena: &SLTNodeArena<A>,
356) -> Option<SLTBitOrigin<A>> {
357 let (origin, _) = terms.first()?.origin.clone()?;
358 let width = terms.len();
359 if let SLTBitOrigin::Node(node) = origin
360 && slt_width(node, arena) != width
361 {
362 return None;
363 }
364 let mut seen = vec![false; width];
365 for term in terms {
366 let (term_origin, bit) = term.origin.as_ref()?;
367 if *term_origin != origin || *bit >= width || seen[*bit] {
368 return None;
369 }
370 seen[*bit] = true;
371 }
372 Some(origin)
373}
374
375fn normalized_slt_lane_op(op: BinaryOp) -> Option<BinaryOp> {
376 match op {
377 BinaryOp::And | BinaryOp::LogicAnd => Some(BinaryOp::And),
378 BinaryOp::Or | BinaryOp::LogicOr => Some(BinaryOp::Or),
379 BinaryOp::Xor => Some(BinaryOp::Xor),
380 _ => None,
381 }
382}
383
384fn compact_slt_predicate_nodes<A: Hash + Eq + Clone>(
385 nodes: &[NodeId],
386 arena: &SLTNodeArena<A>,
387) -> Option<SLTVectorExpr<A>> {
388 let width = nodes.len();
389 if width == 0 || nodes.iter().any(|node| slt_width(*node, arena) != 1) {
390 return None;
391 }
392
393 if nodes.iter().all(|node| *node == nodes[0]) {
394 return Some(SLTVectorExpr::Broadcast(nodes[0]));
395 }
396
397 let mut common_origin = None;
398 let mut origin_matches = true;
399 for (concat_index, node) in nodes.iter().copied().enumerate() {
400 let Some((origin, bit)) = resolve_slt_bit_origin(node, arena) else {
401 origin_matches = false;
402 break;
403 };
404 if bit != width - 1 - concat_index {
405 origin_matches = false;
406 break;
407 }
408 if let Some(previous) = &common_origin {
409 if previous != &origin {
410 origin_matches = false;
411 break;
412 }
413 } else {
414 common_origin = Some(origin);
415 }
416 }
417 if origin_matches {
418 let origin = common_origin?;
419 if !matches!(&origin, SLTBitOrigin::Node(node) if slt_width(*node, arena) != width) {
420 return Some(SLTVectorExpr::Origin(origin));
421 }
422 }
423
424 let mut bound = None;
428 let mut is_low_ones = true;
429 for (concat_index, node) in nodes.iter().copied().enumerate() {
430 let SLTNode::Binary(index, BinaryOp::LtU, lane_bound) = arena.get(node) else {
431 is_low_ones = false;
432 break;
433 };
434 if slt_const_u64(*index, arena) != Some((width - 1 - concat_index) as u64)
435 || slt_width(*index, arena) != slt_width(*lane_bound, arena)
436 {
437 is_low_ones = false;
438 break;
439 }
440 if bound.is_some_and(|previous| previous != *lane_bound) {
441 is_low_ones = false;
442 break;
443 }
444 bound = Some(*lane_bound);
445 }
446 if is_low_ones {
447 return Some(SLTVectorExpr::LowOnes { bound: bound? });
448 }
449
450 let mut op = None;
451 let mut lhs_nodes = Vec::with_capacity(width);
452 let mut rhs_nodes = Vec::with_capacity(width);
453 for node in nodes {
454 let SLTNode::Binary(lhs, lane_op, rhs) = arena.get(*node) else {
455 return None;
456 };
457 let lane_op = normalized_slt_lane_op(*lane_op)?;
458 if op.is_some_and(|previous| previous != lane_op) {
459 return None;
460 }
461 op = Some(lane_op);
462 lhs_nodes.push(*lhs);
463 rhs_nodes.push(*rhs);
464 }
465 Some(SLTVectorExpr::Binary {
466 lhs: Box::new(compact_slt_predicate_nodes(&lhs_nodes, arena)?),
467 op: op?,
468 rhs: Box::new(compact_slt_predicate_nodes(&rhs_nodes, arena)?),
469 })
470}
471
472fn compact_slt_predicates<A: Hash + Eq + Clone>(
473 predicates: &[SLTCountPredicate],
474 arena: &SLTNodeArena<A>,
475) -> Option<SLTVectorExpr<A>> {
476 if predicates
477 .iter()
478 .all(|predicate| matches!(predicate, SLTCountPredicate::Node(_)))
479 {
480 let nodes = predicates
481 .iter()
482 .map(|predicate| match predicate {
483 SLTCountPredicate::Node(node) => *node,
484 SLTCountPredicate::And(..) => unreachable!(),
485 })
486 .collect::<Vec<_>>();
487 return compact_slt_predicate_nodes(&nodes, arena);
488 }
489 if predicates
490 .iter()
491 .all(|predicate| matches!(predicate, SLTCountPredicate::And(..)))
492 {
493 let mut lhs = Vec::with_capacity(predicates.len());
494 let mut rhs = Vec::with_capacity(predicates.len());
495 for predicate in predicates {
496 let SLTCountPredicate::And(lane_lhs, lane_rhs) = predicate else {
497 unreachable!();
498 };
499 lhs.push(*lane_lhs);
500 rhs.push(*lane_rhs);
501 }
502 return Some(SLTVectorExpr::Binary {
503 lhs: Box::new(compact_slt_predicate_nodes(&lhs, arena)?),
504 op: BinaryOp::And,
505 rhs: Box::new(compact_slt_predicate_nodes(&rhs, arena)?),
506 });
507 }
508 None
509}
510
511fn match_slt_increment<A: Hash + Eq + Clone>(
512 value: NodeId,
513 accumulator: NodeId,
514 arena: &SLTNodeArena<A>,
515) -> bool {
516 let SLTNode::Binary(lhs, BinaryOp::Add, rhs) = arena.get(value) else {
517 return false;
518 };
519 *lhs == accumulator && slt_literal_u64(*rhs, arena) == Some(1)
520 || *rhs == accumulator && slt_literal_u64(*lhs, arena) == Some(1)
521}
522
523fn collect_slt_conditional_increments<A: Hash + Eq + Clone>(
524 mut cursor: NodeId,
525 accumulator_width: usize,
526 arena: &SLTNodeArena<A>,
527 materialized: Option<&crate::HashMap<NodeId, RegisterId>>,
528) -> Option<(Vec<SLTBitTerm<A>>, Option<NodeId>)> {
529 let mut terms = Vec::new();
530 loop {
531 if terms.len() == 1
535 && materialized.is_some_and(|cache| cache.contains_key(&cursor))
536 && slt_width(cursor, arena) == accumulator_width
537 {
538 return Some((terms, Some(cursor)));
539 }
540 let SLTNode::Mux {
541 cond,
542 then_expr,
543 else_expr,
544 } = arena.get(cursor)
545 else {
546 return None;
547 };
548 if slt_width(cursor, arena) != accumulator_width
549 || !match_slt_increment(*then_expr, *else_expr, arena)
550 || slt_width(*cond, arena) != 1
551 {
552 return None;
553 }
554 let cond = unwrap_slt_one_bit_procedural_truth(*cond, arena);
555 terms.push(SLTBitTerm {
556 predicate: cond,
557 origin: resolve_slt_bit_origin(cond, arena),
558 });
559 cursor = *else_expr;
560 if slt_literal_zero_of_width(cursor, accumulator_width, arena) {
561 break;
562 }
563 }
564 Some((terms, None))
565}
566
567fn collect_slt_additive_bits<A: Hash + Eq + Clone>(
568 mut cursor: NodeId,
569 accumulator_width: usize,
570 arena: &SLTNodeArena<A>,
571 materialized: Option<&crate::HashMap<NodeId, RegisterId>>,
572) -> Option<(Vec<SLTBitTerm<A>>, Option<NodeId>)> {
573 let mut terms = Vec::new();
574 loop {
575 if terms.len() == 1
578 && materialized.is_some_and(|cache| cache.contains_key(&cursor))
579 && slt_width(cursor, arena) == accumulator_width
580 {
581 return Some((terms, Some(cursor)));
582 }
583 if slt_literal_zero_of_width(cursor, accumulator_width, arena) {
584 break;
585 }
586 let SLTNode::Binary(lhs, BinaryOp::Add, rhs) = arena.get(cursor) else {
587 return None;
588 };
589 if slt_width(cursor, arena) != accumulator_width {
590 return None;
591 }
592 let lhs_term = resolve_slt_extended_bit(*lhs, arena);
593 let rhs_term = resolve_slt_extended_bit(*rhs, arena);
594 match (lhs_term, rhs_term) {
595 (Some(term), None) => {
596 terms.push(term);
597 cursor = *rhs;
598 }
599 (None, Some(term)) => {
600 terms.push(term);
601 cursor = *lhs;
602 }
603 _ => return None,
604 }
605 }
606 Some((terms, None))
607}
608
609fn match_slt_popcount<A: Hash + Eq + Clone>(
610 root: NodeId,
611 arena: &SLTNodeArena<A>,
612 materialized: Option<&crate::HashMap<NodeId, RegisterId>>,
613) -> Option<SLTCountPlan<A>> {
614 let result_width = slt_width(root, arena);
615 let (terms, base) = match arena.get(root) {
616 SLTNode::Mux { .. } => {
617 collect_slt_conditional_increments(root, result_width, arena, materialized)?
618 }
619 SLTNode::Binary(_, BinaryOp::Add, _) => {
620 collect_slt_additive_bits(root, result_width, arena, materialized)?
621 }
622 _ => return None,
623 };
624 let minimum_terms = if base.is_some() { 1 } else { 4 };
625 if terms.len() < minimum_terms || !slt_width_can_represent(result_width, terms.len()) {
626 return None;
627 }
628 let input_width = terms.len();
629 let input = if let Some(origin) = common_complete_slt_origin(&terms, arena) {
630 SLTCountInput::Origin(origin)
631 } else {
632 let predicates = terms
633 .into_iter()
634 .map(|term| term.predicate)
635 .collect::<Vec<_>>();
636 compact_slt_predicate_nodes(&predicates, arena)
637 .map(SLTCountInput::Vector)
638 .unwrap_or_else(|| {
639 SLTCountInput::Predicates(
640 predicates
641 .into_iter()
642 .map(SLTCountPredicate::Node)
643 .collect(),
644 )
645 })
646 };
647 Some(SLTCountPlan {
648 op: UnaryOp::PopCount,
649 input_width,
650 input,
651 post: base.map_or(SLTCountPost::Direct, SLTCountPost::AddTo),
652 })
653}
654
655fn match_slt_boolean_not<A: Hash + Eq + Clone>(
656 node: NodeId,
657 arena: &SLTNodeArena<A>,
658) -> Option<NodeId> {
659 let node = unwrap_slt_one_bit_procedural_truth(node, arena);
660 match arena.get(node) {
661 SLTNode::Unary(UnaryOp::LogicNot, inner) => Some(*inner),
662 SLTNode::Binary(lhs, BinaryOp::Eq, rhs) if slt_const_u64(*lhs, arena) == Some(0) => {
663 Some(*rhs)
664 }
665 SLTNode::Binary(lhs, BinaryOp::Eq, rhs) if slt_const_u64(*rhs, arena) == Some(0) => {
666 Some(*lhs)
667 }
668 _ => None,
669 }
670}
671
672fn match_slt_sets_found<A: Hash + Eq + Clone>(
673 node: NodeId,
674 previous: NodeId,
675 arena: &SLTNodeArena<A>,
676) -> bool {
677 if slt_const_u64(node, arena) == Some(1) {
678 return true;
679 }
680 let SLTNode::Mux {
681 cond,
682 then_expr,
683 else_expr,
684 } = arena.get(node)
685 else {
686 return false;
687 };
688 *else_expr == previous
689 && slt_const_u64(*then_expr, arena) == Some(1)
690 && match_slt_boolean_not(*cond, arena) == Some(previous)
691}
692
693fn match_slt_found_update<A: Hash + Eq + Clone>(
694 next: NodeId,
695 previous: NodeId,
696 predicate: NodeId,
697 arena: &SLTNodeArena<A>,
698) -> bool {
699 let predicate = unwrap_slt_one_bit_procedural_truth(predicate, arena);
700 match arena.get(next) {
701 SLTNode::Binary(lhs, BinaryOp::Or | BinaryOp::LogicOr, rhs) => {
702 (*lhs == previous && *rhs == predicate) || (*rhs == previous && *lhs == predicate)
703 }
704 SLTNode::Mux {
705 cond,
706 then_expr,
707 else_expr,
708 } => {
709 unwrap_slt_one_bit_procedural_truth(*cond, arena) == predicate
710 && *else_expr == previous
711 && match_slt_sets_found(*then_expr, previous, arena)
712 }
713 _ => false,
714 }
715}
716
717fn match_slt_found_reduction<A: Hash + Eq + Clone>(
718 root: NodeId,
719 arena: &SLTNodeArena<A>,
720) -> Option<SLTCountPlan<A>> {
721 if slt_width(root, arena) != 1 {
722 return None;
723 }
724 let mut cursor = root;
725 let mut predicates = Vec::new();
726 loop {
727 let SLTNode::Mux {
728 cond,
729 then_expr,
730 else_expr: previous,
731 } = arena.get(cursor)
732 else {
733 return None;
734 };
735 if slt_width(*cond, arena) != 1 || slt_width(*previous, arena) != 1 {
736 return None;
737 }
738 let cond = unwrap_slt_one_bit_procedural_truth(*cond, arena);
739 let predicate = if match_slt_sets_found(*then_expr, *previous, arena) {
740 SLTCountPredicate::Node(cond)
741 } else {
742 let SLTNode::Binary(lhs, BinaryOp::Or | BinaryOp::LogicOr, rhs) = arena.get(*then_expr)
743 else {
744 return None;
745 };
746 let lane = if *lhs == *previous {
747 *rhs
748 } else if *rhs == *previous {
749 *lhs
750 } else {
751 return None;
752 };
753 if slt_width(lane, arena) != 1 {
754 return None;
755 }
756 SLTCountPredicate::And(cond, lane)
757 };
758 predicates.push(predicate);
759 cursor = *previous;
760 if slt_const_u64(cursor, arena) == Some(0) && slt_width(cursor, arena) == 1 {
761 break;
762 }
763 }
764 if predicates.len() < 4 {
765 return None;
766 }
767 let input_width = predicates.len();
768 let input = compact_slt_predicates(&predicates, arena)
769 .map(SLTCountInput::Vector)
770 .unwrap_or(SLTCountInput::Predicates(predicates));
771 Some(SLTCountPlan {
772 op: UnaryOp::Or,
773 input_width,
774 input,
775 post: SLTCountPost::Direct,
776 })
777}
778
779fn nested_first_write_predicates<A: Hash + Eq + Clone>(
780 items: &[(usize, SLTCountPredicate, Option<(SLTBitOrigin<A>, usize)>)],
781 arena: &SLTNodeArena<A>,
782) -> Option<Vec<NodeId>> {
783 let ordered = items.iter().rev().map(|(_, predicate, _)| {
784 let SLTCountPredicate::And(outer, inner) = predicate else {
785 return None;
786 };
787 Some((*outer, *inner, match_slt_boolean_not(*inner, arena)?))
788 });
789 let ordered = ordered.collect::<Option<Vec<_>>>()?;
790 let &(_, _, first_state) = ordered.first()?;
791 if slt_const_u64(first_state, arena) != Some(0) {
792 return None;
793 }
794 for pair in ordered.windows(2) {
795 let (predicate, _, previous) = pair[0];
796 let (_, _, next) = pair[1];
797 if !match_slt_found_update(next, previous, predicate, arena) {
798 return None;
799 }
800 }
801 Some(
802 ordered
803 .into_iter()
804 .rev()
805 .map(|(outer, _, _)| outer)
806 .collect(),
807 )
808}
809
810fn split_slt_priority_condition<A: Hash + Eq + Clone>(
811 cond: NodeId,
812 accumulator: NodeId,
813 arena: &SLTNodeArena<A>,
814) -> (bool, NodeId, Option<NodeId>) {
815 let cond = unwrap_slt_one_bit_procedural_truth(cond, arena);
816 let SLTNode::Binary(lhs, BinaryOp::And | BinaryOp::LogicAnd, rhs) = arena.get(cond) else {
817 return (false, cond, None);
818 };
819 if let Some(default) = match_slt_accumulator_default(*lhs, accumulator, arena) {
820 (true, *rhs, Some(default))
821 } else if let Some(default) = match_slt_accumulator_default(*rhs, accumulator, arena) {
822 (true, *lhs, Some(default))
823 } else {
824 (false, cond, None)
825 }
826}
827
828fn match_slt_accumulator_default<A: Hash + Eq + Clone>(
829 candidate: NodeId,
830 accumulator: NodeId,
831 arena: &SLTNodeArena<A>,
832) -> Option<NodeId> {
833 let SLTNode::Binary(lhs, BinaryOp::Eq, rhs) = arena.get(candidate) else {
834 return None;
835 };
836 if *lhs == accumulator && slt_const_u64(*rhs, arena).is_some() {
837 Some(*rhs)
838 } else if *rhs == accumulator && slt_const_u64(*lhs, arena).is_some() {
839 Some(*lhs)
840 } else {
841 None
842 }
843}
844
845fn match_slt_priority_count<A: Hash + Eq + Clone>(
846 root: NodeId,
847 arena: &SLTNodeArena<A>,
848) -> Option<SLTCountPlan<A>> {
849 let mut cursor = root;
850 let mut items = Vec::new();
851 let mut default_node = None;
852 let mut default_value = None;
853 let mut guarded = None;
854 let mut conditional_gate = None;
855 loop {
856 let SLTNode::Mux {
857 cond,
858 then_expr,
859 else_expr,
860 } = arena.get(cursor)
861 else {
862 return None;
863 };
864 let cond = unwrap_slt_one_bit_procedural_truth(*cond, arena);
865 let mut value_node = *then_expr;
866 let mut predicate = SLTCountPredicate::Node(cond);
867 let mut origin_guard = Some(cond);
868 let (is_guarded, guard, matched_default) =
869 split_slt_priority_condition(cond, *else_expr, arena);
870
871 let nested_write = if slt_const_u64(value_node, arena).is_none()
876 && let SLTNode::Mux {
877 cond: inner_cond,
878 then_expr: inner_then,
879 else_expr: inner_else,
880 } = arena.get(value_node)
881 && *inner_else == *else_expr
882 && slt_const_u64(*inner_then, arena).is_some()
883 && slt_width(cond, arena) == 1
884 && (is_guarded || slt_width(*inner_cond, arena) == 1)
885 {
886 let inner_cond = unwrap_slt_one_bit_procedural_truth(*inner_cond, arena);
887 value_node = *inner_then;
888 if is_guarded {
889 if conditional_gate.is_some_and(|previous| previous != inner_cond) {
890 return None;
891 }
892 conditional_gate = Some(inner_cond);
893 predicate = SLTCountPredicate::Node(guard);
894 origin_guard = Some(guard);
895 } else {
896 predicate = SLTCountPredicate::And(cond, inner_cond);
897 origin_guard = None;
898 }
899 true
900 } else {
901 false
902 };
903 if conditional_gate.is_some() && !nested_write {
904 return None;
905 }
906 if guarded.is_some_and(|previous| previous != is_guarded) {
907 return None;
908 }
909 guarded = Some(is_guarded);
910 if let Some(matched_default) = matched_default {
911 if default_node.is_some_and(|previous| previous != matched_default) {
912 return None;
913 }
914 default_node = Some(matched_default);
915 default_value = slt_const_u64(matched_default, arena);
916 }
917 let value = slt_const_u64(value_node, arena)? as usize;
918 let origin = origin_guard.and_then(|_| resolve_slt_bit_origin(guard, arena));
919 items.push((value, predicate, origin));
920 cursor = *else_expr;
921 if let (Some(gate), Some(default)) = (conditional_gate, default_node)
922 && matches!(
923 arena.get(cursor),
924 SLTNode::Mux {
925 cond,
926 then_expr,
927 ..
928 } if unwrap_slt_one_bit_procedural_truth(*cond, arena) == gate
929 && *then_expr == default
930 )
931 {
932 break;
933 }
934 if !matches!(arena.get(cursor), SLTNode::Mux { .. }) {
935 break;
936 }
937 }
938 if guarded == Some(false) {
939 default_node = Some(cursor);
940 default_value = slt_const_u64(cursor, arena);
941 }
942 let default_value = default_value?;
943 let result_width = slt_width(root, arena);
944
945 let all_ones_default = match result_width {
952 1..=63 => default_value == (1u64 << result_width) - 1,
953 64 => default_value == u64::MAX,
954 _ => false,
955 };
956 if guarded == Some(false)
957 && items.len() >= 4
958 && all_ones_default
959 && slt_width_can_represent(result_width, items.len().saturating_sub(1))
960 && items
961 .iter()
962 .enumerate()
963 .all(|(stage, (value, predicate, _))| {
964 *value == items.len() - 1 - stage
965 && match predicate {
966 SLTCountPredicate::Node(node) => slt_width(*node, arena) == 1,
967 SLTCountPredicate::And(lhs, rhs) => {
968 slt_width(*lhs, arena) == 1 && slt_width(*rhs, arena) == 1
969 }
970 }
971 })
972 {
973 let input_width = items.len();
974 if slt_width_can_represent(result_width, input_width)
975 && let Some(predicates) = nested_first_write_predicates(&items, arena)
976 {
977 let input = compact_slt_predicate_nodes(&predicates, arena)
978 .map(SLTCountInput::Vector)
979 .unwrap_or_else(|| {
980 SLTCountInput::Predicates(
981 predicates
982 .into_iter()
983 .map(SLTCountPredicate::Node)
984 .collect(),
985 )
986 });
987 return Some(SLTCountPlan {
988 op: UnaryOp::CountTrailingZeros,
989 input_width,
990 input,
991 post: SLTCountPost::ReplaceZeroInputCount(default_value),
992 });
993 }
994 return Some(SLTCountPlan {
995 op: UnaryOp::CountLeadingZeros,
996 input_width,
997 input: SLTCountInput::Predicates(
998 items
999 .into_iter()
1000 .map(|(_, predicate, _)| predicate)
1001 .collect(),
1002 ),
1003 post: SLTCountPost::SubtractFrom((input_width - 1) as u64),
1004 });
1005 }
1006
1007 let conditional_fallback = conditional_gate.and_then(|gate| {
1008 let default = default_node?;
1009 let SLTNode::Mux {
1010 cond,
1011 then_expr,
1012 else_expr,
1013 } = arena.get(cursor)
1014 else {
1015 return None;
1016 };
1017 (unwrap_slt_one_bit_procedural_truth(*cond, arena) == gate && *then_expr == default)
1018 .then_some(*else_expr)
1019 });
1020 let base_matches = Some(cursor) == default_node || conditional_fallback.is_some();
1021 let width = default_value as usize;
1022 if items.len() < 4
1023 || items.len() != width
1024 || !base_matches
1025 || !slt_width_can_represent(slt_width(root, arena), width)
1026 {
1027 return None;
1028 }
1029 let origin = items.first()?.2.clone()?.0;
1030 if let SLTBitOrigin::Node(node) = origin
1031 && slt_width(node, arena) != width
1032 {
1033 return None;
1034 }
1035 if items.iter().any(|item| {
1036 item.2
1037 .as_ref()
1038 .is_none_or(|(item_origin, _)| *item_origin != origin)
1039 }) {
1040 return None;
1041 }
1042
1043 let op = if guarded == Some(true)
1044 && items.iter().enumerate().all(|(j, (value, _, origin))| {
1045 *value == width - 1 - j && origin.as_ref().is_some_and(|(_, bit)| *bit == j)
1046 }) {
1047 UnaryOp::CountLeadingZeros
1048 } else if guarded == Some(true)
1049 && items.iter().enumerate().all(|(j, (value, _, origin))| {
1050 *value == width - 1 - j
1051 && origin
1052 .as_ref()
1053 .is_some_and(|(_, bit)| *bit == width - 1 - j)
1054 })
1055 {
1056 UnaryOp::CountTrailingZeros
1057 } else if guarded == Some(false)
1058 && items.iter().enumerate().all(|(j, (value, _, origin))| {
1059 *value == j
1060 && origin
1061 .as_ref()
1062 .is_some_and(|(_, bit)| *bit == width - 1 - j)
1063 })
1064 {
1065 UnaryOp::CountLeadingZeros
1066 } else if guarded == Some(false)
1067 && items.iter().enumerate().all(|(j, (value, _, origin))| {
1068 *value == j && origin.as_ref().is_some_and(|(_, bit)| *bit == j)
1069 })
1070 {
1071 UnaryOp::CountTrailingZeros
1072 } else {
1073 return None;
1074 };
1075 Some(SLTCountPlan {
1076 op,
1077 input_width: width,
1078 input: SLTCountInput::Origin(origin),
1079 post: if let (Some(cond), Some(false_value)) = (conditional_gate, conditional_fallback) {
1080 SLTCountPost::Select { cond, false_value }
1081 } else {
1082 SLTCountPost::Direct
1083 },
1084 })
1085}
1086
1087fn match_slt_count_idiom<A: Hash + Eq + Clone>(
1088 root: NodeId,
1089 arena: &SLTNodeArena<A>,
1090) -> Option<SLTCountPlan<A>> {
1091 match_slt_found_reduction(root, arena)
1092 .or_else(|| match_slt_priority_count(root, arena))
1093 .or_else(|| match_slt_popcount(root, arena, None))
1094}
1095
1096fn match_slt_count_idiom_with_materialized<A: Hash + Eq + Clone>(
1097 root: NodeId,
1098 arena: &SLTNodeArena<A>,
1099 materialized: &crate::HashMap<NodeId, RegisterId>,
1100) -> Option<SLTCountPlan<A>> {
1101 match_slt_found_reduction(root, arena)
1102 .or_else(|| match_slt_priority_count(root, arena))
1103 .or_else(|| match_slt_popcount(root, arena, Some(materialized)))
1104}
1105
1106pub fn matches_slt_count_idiom<A: Hash + Eq + Clone>(
1110 root: NodeId,
1111 arena: &SLTNodeArena<A>,
1112) -> bool {
1113 match_slt_count_idiom(root, arena).is_some()
1114}
1115
1116#[derive(Default)]
1117struct LoweringCostCache {
1118 tree_costs: Vec<Option<u128>>,
1119 contains_div_rem: Vec<Option<bool>>,
1120 fanout: Vec<usize>,
1121 initially_materialized: Vec<bool>,
1122 owned_costs: Vec<Option<u128>>,
1123 owned_slice_lower_costs: Vec<Option<u128>>,
1124 contains_shared_nontrivial: Vec<Option<bool>>,
1125 is_speculatable_pure: Vec<Option<bool>>,
1126 traversal_seen: Vec<bool>,
1127 traversal_work: Vec<NodeId>,
1128 #[cfg(test)]
1129 analysis_node_visits: usize,
1130}
1131
1132#[derive(Clone, Copy, PartialEq, Eq)]
1133struct StaticBranchProbability {
1134 true_weight: u128,
1135 total_weight: u128,
1136}
1137
1138impl StaticBranchProbability {
1139 const EVEN: Self = Self {
1140 true_weight: 1,
1141 total_weight: 2,
1142 };
1143
1144 fn inverted(self) -> Self {
1145 Self {
1146 true_weight: self.total_weight - self.true_weight,
1147 total_weight: self.total_weight,
1148 }
1149 }
1150
1151 fn conjunction(self, rhs: Self) -> Self {
1152 let Some(true_weight) = self.true_weight.checked_mul(rhs.true_weight) else {
1153 return Self::EVEN;
1154 };
1155 let Some(total_weight) = self.total_weight.checked_mul(rhs.total_weight) else {
1156 return Self::EVEN;
1157 };
1158 Self {
1159 true_weight,
1160 total_weight,
1161 }
1162 }
1163}
1164
1165struct MuxCfgPlan {
1166 shared_nodes: Vec<NodeId>,
1169}
1170
1171#[derive(Clone, Default)]
1172struct ZeroControllerFacts {
1173 unconditional_zero: bool,
1177 guards: crate::HashSet<NodeId>,
1180}
1181
1182#[derive(Clone, Copy)]
1183struct GuardedConcatPlan {
1184 guard: NodeId,
1185 net_benefit_scaled: u128,
1186}
1187
1188#[derive(Default)]
1189struct MuxLowerStats {
1190 normal_seen: usize,
1191 slice_seen: usize,
1192 constant_folded: usize,
1193 cfg_cost: usize,
1194 cfg_div_rem: usize,
1195 cfg_slice_cost: usize,
1196 cfg_slice_div_rem: usize,
1197 shared_nodes_hoisted: usize,
1198 kept_four_state: usize,
1199 kept_impure: usize,
1200 kept_dynamic_env: usize,
1201 kept_unprofitable: usize,
1202 kept_deep_shared: usize,
1203 biased_conditions: usize,
1204 owned_cost_sum: u128,
1205 owned_cost_max: u128,
1206 unprofitable_cost_buckets: [usize; 7],
1207}
1208
1209impl MuxLowerStats {
1210 fn record_cost(&mut self, then_cost: u128, else_cost: u128) {
1211 let total = then_cost.saturating_add(else_cost);
1212 self.owned_cost_sum = self.owned_cost_sum.saturating_add(total);
1213 self.owned_cost_max = self.owned_cost_max.max(total);
1214 }
1215
1216 fn record_unprofitable(&mut self, then_cost: u128, else_cost: u128) {
1217 self.kept_unprofitable += 1;
1218 let total = then_cost.saturating_add(else_cost);
1219 let bucket = match total {
1220 0..=7 => 0,
1221 8..=15 => 1,
1222 16..=31 => 2,
1223 32..=63 => 3,
1224 64..=127 => 4,
1225 128..=255 => 5,
1226 _ => 6,
1227 };
1228 self.unprofitable_cost_buckets[bucket] += 1;
1229 }
1230}
1231
1232pub struct SLTToSIRLowerer {
1233 four_state: bool,
1234 unpacked_input_element_widths: crate::HashMap<NodeId, usize>,
1235 cost_cache: RefCell<LoweringCostCache>,
1236 cache_insert_log: RefCell<Vec<NodeId>>,
1237 region_slice_cache: RefCell<crate::HashMap<(NodeId, BitAccess), RegisterId>>,
1238 region_slice_cache_insert_log: RefCell<Vec<(NodeId, BitAccess)>>,
1239 mux_stats: Option<RefCell<MuxLowerStats>>,
1240}
1241
1242#[derive(Clone, Copy)]
1243struct LowerCacheTransaction {
1244 node_insertions: usize,
1245 region_slice_insertions: usize,
1246}
1247
1248struct LowerEnv<'parent, A: Hash + Eq + Clone> {
1249 inputs: crate::HashMap<VarAtomBase<A>, RegisterId>,
1250 parent: Option<&'parent LowerEnv<'parent, A>>,
1255}
1256
1257#[derive(Clone, Copy)]
1258struct FoldGroupLowerSpec<'arena, A: Hash + Eq + Clone> {
1259 loop_var: &'arena A,
1260 loop_width: usize,
1261 loop_signed: bool,
1262 start: &'arena BigInt,
1263 step: &'arena BigInt,
1264 trip_count: usize,
1265 entry_guard: NodeId,
1266 states: &'arena [SLTForFoldGroupState<A>],
1267}
1268
1269impl<'arena, A: Hash + Eq + Clone> FoldGroupLowerSpec<'arena, A> {
1270 fn from_root(root: NodeId, arena: &'arena SLTNodeArena<A>) -> Option<Self> {
1271 let SLTNode::ForFoldGroup {
1272 loop_var,
1273 loop_width,
1274 loop_signed,
1275 start,
1276 step,
1277 trip_count,
1278 entry_guard,
1279 states,
1280 } = arena.get_checked(root)?
1281 else {
1282 return None;
1283 };
1284 Some(Self {
1285 loop_var,
1286 loop_width: *loop_width,
1287 loop_signed: *loop_signed,
1288 start,
1289 step,
1290 trip_count: *trip_count,
1291 entry_guard: *entry_guard,
1292 states,
1293 })
1294 }
1295}
1296
1297struct SLTOrScanPlan<A: Hash + Eq + Clone> {
1302 vector_state: usize,
1303 found_state: usize,
1304 width: usize,
1305 active: SLTVectorExpr<A>,
1306 source: SLTVectorExpr<A>,
1307 select_before: NodeId,
1308 select_first: NodeId,
1309}
1310
1311fn slt_tree_reads_any_variable<A: Hash + Eq + Clone>(
1312 root: NodeId,
1313 variables: &[&A],
1314 arena: &SLTNodeArena<A>,
1315) -> bool {
1316 let mut visited = crate::HashSet::default();
1317 let mut work = vec![root];
1318 while let Some(node) = work.pop() {
1319 if !visited.insert(node) {
1320 continue;
1321 }
1322 match arena.get(node) {
1323 SLTNode::Input {
1324 variable, index, ..
1325 } => {
1326 if variables.contains(&variable) {
1327 return true;
1328 }
1329 work.extend(index.iter().map(|entry| entry.node));
1330 }
1331 SLTNode::Constant(..) => {}
1332 SLTNode::Binary(lhs, _, rhs) => work.extend([*lhs, *rhs]),
1333 SLTNode::Unary(_, inner)
1334 | SLTNode::Capture { expr: inner, .. }
1335 | SLTNode::Slice { expr: inner, .. } => {
1336 work.push(*inner);
1337 }
1338 SLTNode::Mux {
1339 cond,
1340 then_expr,
1341 else_expr,
1342 } => work.extend([*cond, *then_expr, *else_expr]),
1343 SLTNode::Concat(parts) => work.extend(parts.iter().map(|(part, _)| *part)),
1344 SLTNode::ForFold { .. } | SLTNode::ForFoldGroup { .. } => return true,
1345 }
1346 }
1347 false
1348}
1349
1350fn slt_is_exact_state_input<A: Hash + Eq + Clone>(
1351 node: NodeId,
1352 state: &SLTForFoldGroupState<A>,
1353 arena: &SLTNodeArena<A>,
1354) -> bool {
1355 match arena.get(node) {
1356 SLTNode::Input {
1357 variable,
1358 index,
1359 access,
1360 ..
1361 } => variable == &state.target.id && index.is_empty() && access == &state.target.access,
1362 SLTNode::Unary(UnaryOp::Ident, inner) => slt_is_exact_state_input(*inner, state, arena),
1363 _ => false,
1364 }
1365}
1366
1367fn slt_scan_lane_bits(trip_count: usize) -> usize {
1368 let maximum = trip_count.saturating_sub(1);
1369 (usize::BITS as usize - maximum.leading_zeros() as usize).max(1)
1370}
1371
1372fn slt_scan_domain_preserves_identity<A: Hash + Eq + Clone>(
1373 spec: &FoldGroupLowerSpec<'_, A>,
1374) -> bool {
1375 if spec.loop_width == 0
1376 || spec.start != &BigInt::from(0u8)
1377 || spec.step != &BigInt::from(1u8)
1378 || spec.trip_count == 0
1379 {
1380 return false;
1381 }
1382
1383 let maximum = spec.trip_count - 1;
1387 let required_bits = usize::BITS as usize - maximum.leading_zeros() as usize;
1388 let available_value_bits = spec.loop_width - usize::from(spec.loop_signed);
1389 required_bits <= available_value_bits
1390}
1391
1392fn slt_scan_low_mask_preserves_domain<A: Hash + Eq + Clone>(
1393 node: NodeId,
1394 trip_count: usize,
1395 arena: &SLTNodeArena<A>,
1396) -> bool {
1397 let required_bits = slt_scan_lane_bits(trip_count);
1398 let required_mask = if required_bits >= 64 {
1399 u64::MAX
1400 } else {
1401 (1u64 << required_bits) - 1
1402 };
1403 slt_const_u64(node, arena).is_some_and(|mask| mask & required_mask == required_mask)
1404}
1405
1406fn slt_is_scan_loop_value<A: Hash + Eq + Clone>(
1407 node: NodeId,
1408 spec: &FoldGroupLowerSpec<'_, A>,
1409 arena: &SLTNodeArena<A>,
1410) -> bool {
1411 match arena.get(node) {
1412 SLTNode::Input {
1413 variable,
1414 index,
1415 access,
1416 ..
1417 } => {
1418 variable == spec.loop_var
1419 && index.is_empty()
1420 && access.lsb == 0
1421 && access.msb + 1 == spec.loop_width
1422 }
1423 SLTNode::Unary(UnaryOp::Ident, inner) => slt_is_scan_loop_value(*inner, spec, arena),
1424 SLTNode::Slice { expr, access }
1425 if access.lsb == 0 && access.msb + 1 >= slt_scan_lane_bits(spec.trip_count) =>
1426 {
1427 slt_is_scan_loop_value(*expr, spec, arena)
1428 }
1429 SLTNode::Concat(parts) if !parts.is_empty() => {
1430 let (low, low_width) = parts.last().copied().expect("non-empty concat");
1431 low_width >= slt_scan_lane_bits(spec.trip_count)
1432 && slt_is_scan_loop_value(low, spec, arena)
1433 && parts[..parts.len() - 1]
1434 .iter()
1435 .all(|(part, _)| slt_const_u64(*part, arena) == Some(0))
1436 }
1437 SLTNode::Binary(lhs, BinaryOp::Add, rhs) => {
1438 slt_const_u64(*lhs, arena) == Some(0) && slt_is_scan_loop_value(*rhs, spec, arena)
1439 || slt_const_u64(*rhs, arena) == Some(0)
1440 && slt_is_scan_loop_value(*lhs, spec, arena)
1441 }
1442 SLTNode::Binary(lhs, BinaryOp::Mul, rhs) => {
1443 slt_const_u64(*lhs, arena) == Some(1) && slt_is_scan_loop_value(*rhs, spec, arena)
1444 || slt_const_u64(*rhs, arena) == Some(1)
1445 && slt_is_scan_loop_value(*lhs, spec, arena)
1446 }
1447 SLTNode::Binary(lhs, BinaryOp::And, rhs) => {
1452 slt_scan_low_mask_preserves_domain(*lhs, spec.trip_count, arena)
1453 && slt_is_scan_loop_value(*rhs, spec, arena)
1454 || slt_scan_low_mask_preserves_domain(*rhs, spec.trip_count, arena)
1455 && slt_is_scan_loop_value(*lhs, spec, arena)
1456 }
1457 _ => false,
1458 }
1459}
1460
1461fn match_slt_scan_indexed_input<A: Hash + Eq + Clone>(
1462 variable: &A,
1463 index: &[crate::SLTIndex],
1464 input_access: BitAccess,
1465 spec: &FoldGroupLowerSpec<'_, A>,
1466 state_variables: &[&A],
1467 arena: &SLTNodeArena<A>,
1468) -> Option<SLTVectorExpr<A>> {
1469 let [entry] = index else {
1470 return None;
1471 };
1472 if entry.stride != 1
1473 || variable == spec.loop_var
1474 || state_variables.contains(&variable)
1475 || !slt_is_scan_loop_value(entry.node, spec, arena)
1476 {
1477 return None;
1478 }
1479 let packed_access = if input_access == BitAccess::new(0, 0) {
1480 BitAccess::new(0, spec.trip_count - 1)
1483 } else if input_access.lsb == 0 && input_access.msb + 1 == spec.trip_count {
1484 input_access
1485 } else {
1486 return None;
1487 };
1488 let unpacked_element_width = match entry.kind {
1489 crate::SLTIndexKind::Unpacked { element_width } => Some(element_width),
1490 crate::SLTIndexKind::Packed => None,
1491 };
1492 Some(SLTVectorExpr::StaticInput {
1493 variable: variable.clone(),
1494 access: packed_access,
1495 unpacked_element_width,
1496 })
1497}
1498
1499fn match_slt_scan_indexed_bit<A: Hash + Eq + Clone>(
1500 node: NodeId,
1501 spec: &FoldGroupLowerSpec<'_, A>,
1502 state_variables: &[&A],
1503 arena: &SLTNodeArena<A>,
1504) -> Option<SLTVectorExpr<A>> {
1505 match arena.get(node) {
1506 SLTNode::Unary(UnaryOp::Ident, inner) => {
1507 match_slt_scan_indexed_bit(*inner, spec, state_variables, arena)
1508 }
1509 SLTNode::Input {
1510 variable,
1511 index,
1512 access,
1513 ..
1514 } if *access == BitAccess::new(0, 0) => {
1515 match_slt_scan_indexed_input(variable, index, *access, spec, state_variables, arena)
1516 }
1517 SLTNode::Slice { expr, access } if *access == BitAccess::new(0, 0) => {
1518 let SLTNode::Input {
1519 variable,
1520 index,
1521 access: input_access,
1522 ..
1523 } = arena.get(*expr)
1524 else {
1525 return None;
1526 };
1527 match_slt_scan_indexed_input(
1528 variable,
1529 index,
1530 *input_access,
1531 spec,
1532 state_variables,
1533 arena,
1534 )
1535 }
1536 _ => None,
1537 }
1538}
1539
1540fn lift_slt_scan_lane_expr<A: Hash + Eq + Clone>(
1541 node: NodeId,
1542 spec: &FoldGroupLowerSpec<'_, A>,
1543 state_variables: &[&A],
1544 arena: &SLTNodeArena<A>,
1545) -> Option<SLTVectorExpr<A>> {
1546 if let Some(input) = match_slt_scan_indexed_bit(node, spec, state_variables, arena) {
1547 return Some(input);
1548 }
1549 if let SLTNode::Unary(UnaryOp::ToTwoState, truth) = arena.get(node)
1554 && let SLTNode::Unary(UnaryOp::Or, inner) = arena.get(*truth)
1555 && slt_width(*inner, arena) == 1
1556 {
1557 return lift_slt_scan_lane_expr(*inner, spec, state_variables, arena);
1558 }
1559 let mut forbidden = Vec::with_capacity(state_variables.len() + 1);
1560 forbidden.push(spec.loop_var);
1561 forbidden.extend_from_slice(state_variables);
1562 if slt_width(node, arena) == 1 && !slt_tree_reads_any_variable(node, &forbidden, arena) {
1563 return Some(SLTVectorExpr::Broadcast(node));
1564 }
1565 match arena.get(node) {
1566 SLTNode::Binary(index, BinaryOp::LtU, bound)
1567 if slt_is_scan_loop_value(*index, spec, arena)
1568 && !slt_tree_reads_any_variable(*bound, &forbidden, arena) =>
1569 {
1570 Some(SLTVectorExpr::LowOnes { bound: *bound })
1571 }
1572 SLTNode::Binary(lhs, op, rhs) => {
1573 let op = normalized_slt_lane_op(*op)?;
1574 Some(SLTVectorExpr::Binary {
1575 lhs: Box::new(lift_slt_scan_lane_expr(*lhs, spec, state_variables, arena)?),
1576 op,
1577 rhs: Box::new(lift_slt_scan_lane_expr(*rhs, spec, state_variables, arena)?),
1578 })
1579 }
1580 SLTNode::Unary(UnaryOp::LogicNot | UnaryOp::BitNot, inner) => {
1581 Some(SLTVectorExpr::Not(Box::new(lift_slt_scan_lane_expr(
1582 *inner,
1583 spec,
1584 state_variables,
1585 arena,
1586 )?)))
1587 }
1588 _ => None,
1589 }
1590}
1591
1592fn slt_binary_operands<A: Hash + Eq + Clone>(
1593 node: NodeId,
1594 op: BinaryOp,
1595 arena: &SLTNodeArena<A>,
1596) -> Option<(NodeId, NodeId)> {
1597 let SLTNode::Binary(lhs, actual, rhs) = arena.get(node) else {
1598 return None;
1599 };
1600 (*actual == op).then_some((*lhs, *rhs))
1601}
1602
1603fn slt_matches_commutative_pair<A: Hash + Eq + Clone>(
1604 node: NodeId,
1605 ops: &[BinaryOp],
1606 lhs: NodeId,
1607 rhs: NodeId,
1608 arena: &SLTNodeArena<A>,
1609) -> bool {
1610 matches!(
1611 arena.get(node),
1612 SLTNode::Binary(actual_lhs, op, actual_rhs)
1613 if ops.contains(op)
1614 && ((*actual_lhs == lhs && *actual_rhs == rhs)
1615 || (*actual_lhs == rhs && *actual_rhs == lhs))
1616 )
1617}
1618
1619fn match_slt_scan_found_update<A: Hash + Eq + Clone>(
1620 state: &SLTForFoldGroupState<A>,
1621 arena: &SLTNodeArena<A>,
1622) -> Option<(NodeId, NodeId, NodeId)> {
1623 if state.target.access != BitAccess::new(0, 0) || slt_const_u64(state.initial, arena) != Some(0)
1624 {
1625 return None;
1626 }
1627 let SLTNode::Mux {
1628 cond,
1629 then_expr,
1630 else_expr,
1631 } = arena.get(state.update)
1632 else {
1633 return None;
1634 };
1635 if !slt_is_exact_state_input(*else_expr, state, arena) {
1636 return None;
1637 }
1638 let SLTNode::Binary(lhs, BinaryOp::Or | BinaryOp::LogicOr, rhs) = arena.get(*then_expr) else {
1639 return None;
1640 };
1641 let source = if slt_is_exact_state_input(*lhs, state, arena) {
1642 *rhs
1643 } else if slt_is_exact_state_input(*rhs, state, arena) {
1644 *lhs
1645 } else {
1646 return None;
1647 };
1648 (slt_width(source, arena) == 1).then_some((*cond, source, *else_expr))
1649}
1650
1651fn match_slt_scan_offset<A: Hash + Eq + Clone>(
1652 node: NodeId,
1653 spec: &FoldGroupLowerSpec<'_, A>,
1654 arena: &SLTNodeArena<A>,
1655) -> bool {
1656 slt_is_scan_loop_value(node, spec, arena)
1657}
1658
1659fn match_slt_scan_zext_bit<A: Hash + Eq + Clone>(
1660 node: NodeId,
1661 width: usize,
1662 arena: &SLTNodeArena<A>,
1663) -> Option<NodeId> {
1664 if width == 1 && slt_width(node, arena) == 1 {
1665 return Some(node);
1666 }
1667 let SLTNode::Concat(parts) = arena.get(node) else {
1668 return None;
1669 };
1670 let (bit, bit_width) = parts.last().copied()?;
1671 (bit_width == 1
1672 && slt_width(bit, arena) == 1
1673 && parts
1674 .iter()
1675 .map(|(_, part_width)| *part_width)
1676 .sum::<usize>()
1677 == width
1678 && parts[..parts.len() - 1]
1679 .iter()
1680 .all(|(part, _)| slt_const_u64(*part, arena) == Some(0)))
1681 .then_some(bit)
1682}
1683
1684fn match_slt_scan_insert<A: Hash + Eq + Clone>(
1685 node: NodeId,
1686 old: NodeId,
1687 width: usize,
1688 spec: &FoldGroupLowerSpec<'_, A>,
1689 arena: &SLTNodeArena<A>,
1690) -> Option<NodeId> {
1691 let (lhs, rhs) = slt_binary_operands(node, BinaryOp::Or, arena)?;
1692 for (old_masked, new_masked) in [(lhs, rhs), (rhs, lhs)] {
1693 let (old_lhs, old_rhs) = slt_binary_operands(old_masked, BinaryOp::And, arena)?;
1694 let inverted_mask = if old_lhs == old {
1695 old_rhs
1696 } else if old_rhs == old {
1697 old_lhs
1698 } else {
1699 continue;
1700 };
1701 let SLTNode::Unary(UnaryOp::BitNot, mask) = arena.get(inverted_mask) else {
1702 continue;
1703 };
1704 let SLTNode::Binary(one, BinaryOp::Shl, offset) = arena.get(*mask) else {
1705 continue;
1706 };
1707 if slt_width(*mask, arena) != width
1708 || slt_const_u64(*one, arena) != Some(1)
1709 || slt_width(*one, arena) != width
1710 || !match_slt_scan_offset(*offset, spec, arena)
1711 {
1712 continue;
1713 }
1714 let (new_lhs, new_rhs) = slt_binary_operands(new_masked, BinaryOp::And, arena)?;
1715 let shifted = if new_lhs == *mask {
1716 new_rhs
1717 } else if new_rhs == *mask {
1718 new_lhs
1719 } else {
1720 continue;
1721 };
1722 let SLTNode::Binary(value, BinaryOp::Shl, value_offset) = arena.get(shifted) else {
1723 continue;
1724 };
1725 if value_offset != offset {
1726 continue;
1727 }
1728 if let Some(bit) = match_slt_scan_zext_bit(*value, width, arena) {
1729 return Some(bit);
1730 }
1731 }
1732 None
1733}
1734
1735fn match_slt_scan_mode_test<A: Hash + Eq + Clone>(
1736 node: NodeId,
1737 expected: u64,
1738 forbidden: &[&A],
1739 arena: &SLTNodeArena<A>,
1740) -> Option<NodeId> {
1741 let SLTNode::Binary(lhs, BinaryOp::Eq | BinaryOp::EqWildcard, rhs) = arena.get(node) else {
1742 return None;
1743 };
1744 let mode = if slt_const_u64(*lhs, arena) == Some(expected) {
1745 *rhs
1746 } else if slt_const_u64(*rhs, arena) == Some(expected) {
1747 *lhs
1748 } else {
1749 return None;
1750 };
1751 (slt_width(mode, arena) == 2 && !slt_tree_reads_any_variable(mode, forbidden, arena))
1752 .then_some(mode)
1753}
1754
1755fn match_slt_scan_selected_bit<A: Hash + Eq + Clone>(
1756 node: NodeId,
1757 found: NodeId,
1758 source: NodeId,
1759 forbidden: &[&A],
1760 arena: &SLTNodeArena<A>,
1761) -> Option<(NodeId, NodeId)> {
1762 let not_found = match_slt_boolean_not(node, arena).filter(|inner| *inner == found);
1763 if not_found.is_some() {
1764 return None;
1765 }
1766 let SLTNode::Mux {
1767 cond: before_cond,
1768 then_expr: before,
1769 else_expr,
1770 } = arena.get(node)
1771 else {
1772 return None;
1773 };
1774 let SLTNode::Mux {
1775 cond: first_cond,
1776 then_expr: first,
1777 else_expr: through,
1778 } = arena.get(*else_expr)
1779 else {
1780 return None;
1781 };
1782 let not_found = match_slt_boolean_not(*through, arena)?;
1783 if not_found != found {
1784 return None;
1785 }
1786 let before_matches = match arena.get(*before) {
1787 SLTNode::Binary(lhs, BinaryOp::And | BinaryOp::LogicAnd, rhs) => {
1788 (match_slt_boolean_not(*lhs, arena) == Some(found)
1789 && match_slt_boolean_not(*rhs, arena) == Some(source))
1790 || (match_slt_boolean_not(*rhs, arena) == Some(found)
1791 && match_slt_boolean_not(*lhs, arena) == Some(source))
1792 }
1793 _ => false,
1794 };
1795 if !before_matches
1796 || !slt_matches_commutative_pair(
1797 *first,
1798 &[BinaryOp::And, BinaryOp::LogicAnd],
1799 *through,
1800 source,
1801 arena,
1802 )
1803 {
1804 return None;
1805 }
1806 let before_mode = match_slt_scan_mode_test(*before_cond, 1, forbidden, arena)?;
1807 let first_mode = match_slt_scan_mode_test(*first_cond, 2, forbidden, arena)?;
1808 (before_mode == first_mode).then_some((*before_cond, *first_cond))
1809}
1810
1811fn match_slt_or_scan_plan<A: Hash + Eq + Clone>(
1812 spec: &FoldGroupLowerSpec<'_, A>,
1813 arena: &SLTNodeArena<A>,
1814) -> Option<SLTOrScanPlan<A>> {
1815 if !slt_scan_domain_preserves_identity(spec) || spec.states.len() != 2 {
1816 return None;
1817 }
1818 let state_variables = spec
1819 .states
1820 .iter()
1821 .map(|state| &state.target.id)
1822 .collect::<Vec<_>>();
1823 for (found_state, found) in spec.states.iter().enumerate() {
1824 let Some((active, source, old_found)) = match_slt_scan_found_update(found, arena) else {
1825 continue;
1826 };
1827 let vector_state = 1 - found_state;
1828 let vector = &spec.states[vector_state];
1829 let width = vector.target.access.msb - vector.target.access.lsb + 1;
1830 if vector.target.access.lsb != 0
1831 || width != spec.trip_count
1832 || slt_width(vector.initial, arena) != width
1833 {
1834 continue;
1835 }
1836 let SLTNode::Mux {
1837 cond,
1838 then_expr,
1839 else_expr,
1840 } = arena.get(vector.update)
1841 else {
1842 continue;
1843 };
1844 if *cond != active || !slt_is_exact_state_input(*else_expr, vector, arena) {
1845 continue;
1846 }
1847 let Some(new_bit) = match_slt_scan_insert(*then_expr, *else_expr, width, spec, arena)
1848 else {
1849 continue;
1850 };
1851 let mut forbidden = Vec::with_capacity(state_variables.len() + 1);
1852 forbidden.push(spec.loop_var);
1853 forbidden.extend(state_variables.iter().copied());
1854 let Some((select_before, select_first)) =
1855 match_slt_scan_selected_bit(new_bit, old_found, source, &forbidden, arena)
1856 else {
1857 continue;
1858 };
1859 let active = lift_slt_scan_lane_expr(active, spec, &state_variables, arena)?;
1860 let source = match_slt_scan_indexed_bit(source, spec, &state_variables, arena)?;
1861 return Some(SLTOrScanPlan {
1862 vector_state,
1863 found_state,
1864 width,
1865 active,
1866 source,
1867 select_before,
1868 select_first,
1869 });
1870 }
1871 None
1872}
1873
1874pub fn matches_slt_or_scan_group<A: Hash + Eq + Clone>(
1875 root: NodeId,
1876 arena: &SLTNodeArena<A>,
1877) -> bool {
1878 FoldGroupLowerSpec::from_root(root, arena)
1879 .and_then(|spec| match_slt_or_scan_plan(&spec, arena))
1880 .is_some()
1881}
1882
1883impl SLTToSIRLowerer {
1884 pub fn new(four_state: bool) -> Self {
1885 Self {
1886 four_state,
1887 unpacked_input_element_widths: crate::HashMap::default(),
1888 cost_cache: RefCell::new(LoweringCostCache::default()),
1889 cache_insert_log: RefCell::new(Vec::new()),
1890 region_slice_cache: RefCell::new(crate::HashMap::default()),
1891 region_slice_cache_insert_log: RefCell::new(Vec::new()),
1892 mux_stats: tracing::enabled!(tracing::Level::DEBUG)
1893 .then(|| RefCell::new(MuxLowerStats::default())),
1894 }
1895 }
1896
1897 pub fn with_unpacked_input_types<A: Hash + Eq + Clone>(
1898 mut self,
1899 arena: &SLTNodeArena<A>,
1900 element_widths: &crate::HashMap<A, usize>,
1901 ) -> Self {
1902 for (index, node) in arena.iter().enumerate() {
1903 let SLTNode::Input { variable, .. } = node else {
1904 continue;
1905 };
1906 if let Some(&element_width) = element_widths.get(variable) {
1907 self.unpacked_input_element_widths
1908 .insert(NodeId(index), element_width);
1909 }
1910 }
1911 self
1912 }
1913
1914 fn lower_compacted_input<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
1915 &self,
1916 builder: &mut SIRBuilder<A>,
1917 node: NodeId,
1918 variable: &A,
1919 index: &[crate::SLTIndex],
1920 width: usize,
1921 arena: &SLTNodeArena<A>,
1922 cache: &mut crate::HashMap<NodeId, RegisterId>,
1923 ) -> RegisterId {
1924 if index.is_empty()
1925 && let Some(&element_width) = self.unpacked_input_element_widths.get(&node)
1926 && width.is_multiple_of(element_width)
1927 {
1928 let destination = builder.alloc_logic(width);
1929 builder.emit(SIRInstruction::Load(
1930 destination,
1931 variable.clone(),
1932 SIROffset::PackedElements {
1933 bit_offset: 0,
1934 element_width,
1935 },
1936 width,
1937 ));
1938 destination
1939 } else {
1940 self.lower_input_for_node(
1941 builder,
1942 node,
1943 variable,
1944 index,
1945 &BitAccess::new(0, width - 1),
1946 arena,
1947 cache,
1948 None,
1949 )
1950 }
1951 }
1952
1953 fn lower_input_for_node<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
1954 &self,
1955 builder: &mut SIRBuilder<A>,
1956 node: NodeId,
1957 id: &A,
1958 index: &[crate::SLTIndex],
1959 access: &BitAccess,
1960 arena: &SLTNodeArena<A>,
1961 cache: &mut crate::HashMap<NodeId, RegisterId>,
1962 env: Option<&LowerEnv<'_, A>>,
1963 ) -> RegisterId {
1964 let width = access.msb - access.lsb + 1;
1965 if index.is_empty()
1966 && let Some(&element_width) = self.unpacked_input_element_widths.get(&node)
1967 && element_width != 0
1968 && width > element_width
1969 && access.lsb.is_multiple_of(element_width)
1970 && width.is_multiple_of(element_width)
1971 {
1972 let destination = builder.alloc_logic(width);
1973 builder.emit(SIRInstruction::Load(
1974 destination,
1975 id.clone(),
1976 SIROffset::PackedElements {
1977 bit_offset: access.lsb,
1978 element_width,
1979 },
1980 width,
1981 ));
1982 destination
1983 } else {
1984 self.lower_input(builder, id, index, access, arena, cache, env)
1985 }
1986 }
1987
1988 #[inline(always)]
1989 fn with_mux_stats(&self, update: impl FnOnce(&mut MuxLowerStats)) {
1990 if let Some(stats) = &self.mux_stats {
1991 update(&mut stats.borrow_mut());
1992 }
1993 }
1994
1995 pub fn lower<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
1997 &self,
1998 builder: &mut SIRBuilder<A>,
1999 node: NodeId,
2000 arena: &SLTNodeArena<A>,
2001 cache: &mut crate::HashMap<NodeId, RegisterId>,
2002 ) -> RegisterId {
2003 self.reset_cost_cache(node, arena, cache, true);
2004 self.lower_inner(builder, node, arena, cache, None, true)
2005 }
2006
2007 pub fn lower_fold_groups_jointly<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
2015 &self,
2016 builder: &mut SIRBuilder<A>,
2017 roots: &[NodeId],
2018 arena: &SLTNodeArena<A>,
2019 cache: &mut crate::HashMap<NodeId, RegisterId>,
2020 ) -> bool {
2021 if roots.is_empty()
2022 || roots.iter().any(|root| cache.contains_key(root))
2023 || roots.iter().copied().collect::<crate::HashSet<_>>().len() != roots.len()
2024 {
2025 return false;
2026 }
2027 let Some(specs) = roots
2028 .iter()
2029 .copied()
2030 .map(|root| FoldGroupLowerSpec::from_root(root, arena))
2031 .collect::<Option<Vec<_>>>()
2032 else {
2033 return false;
2034 };
2035 if !self.four_state
2040 && specs
2041 .iter()
2042 .any(|spec| match_slt_or_scan_plan(spec, arena).is_some())
2043 {
2044 return false;
2045 }
2046 if !Self::joint_fold_group_specs_are_legal(&specs, arena) {
2047 return false;
2048 }
2049
2050 self.reset_cost_cache_roots(roots, arena, cache, true);
2051 let results = self.lower_fold_group_specs(builder, arena, cache, &specs, None, true);
2052 debug_assert_eq!(results.len(), roots.len());
2053 for (&root, result) in roots.iter().zip(results) {
2054 let previous = cache.insert(root, result);
2055 debug_assert!(previous.is_none());
2056 self.cache_insert_log.borrow_mut().push(root);
2057 }
2058 true
2059 }
2060
2061 fn joint_fold_group_specs_are_legal<A: Hash + Eq + Clone>(
2062 specs: &[FoldGroupLowerSpec<'_, A>],
2063 arena: &SLTNodeArena<A>,
2064 ) -> bool {
2065 let Some(first) = specs.first() else {
2066 return false;
2067 };
2068 if specs.iter().any(|spec| {
2069 spec.loop_width != first.loop_width
2070 || spec.loop_signed != first.loop_signed
2071 || spec.start != first.start
2072 || spec.step != first.step
2073 || spec.trip_count != first.trip_count
2074 || spec.entry_guard != first.entry_guard
2075 || spec.loop_width == 0
2076 || spec.trip_count == 0
2077 || spec.states.is_empty()
2078 }) {
2079 return false;
2080 }
2081
2082 let mut state_owners: crate::HashMap<A, Vec<(usize, BitAccess)>> =
2083 crate::HashMap::default();
2084 for (owner, spec) in specs.iter().enumerate() {
2085 for state in spec.states {
2086 if specs
2087 .iter()
2088 .any(|candidate| *candidate.loop_var == state.target.id)
2089 {
2090 return false;
2091 }
2092 let owners = state_owners.entry(state.target.id.clone()).or_default();
2093 if owners
2094 .iter()
2095 .any(|(_, access)| access.overlaps(&state.target.access))
2096 {
2097 return false;
2098 }
2099 owners.push((owner, state.target.access));
2100 }
2101 }
2102
2103 let mut preheader_visited = crate::HashSet::default();
2104 if !Self::joint_fold_tree_is_legal(
2105 first.entry_guard,
2106 None,
2107 specs,
2108 &state_owners,
2109 arena,
2110 &mut preheader_visited,
2111 ) {
2112 return false;
2113 }
2114 let mut update_visited = (0..specs.len())
2115 .map(|_| crate::HashSet::default())
2116 .collect::<Vec<_>>();
2117 for (owner, spec) in specs.iter().enumerate() {
2118 for state in spec.states {
2119 if !Self::joint_fold_tree_is_legal(
2120 state.initial,
2121 None,
2122 specs,
2123 &state_owners,
2124 arena,
2125 &mut preheader_visited,
2126 ) || !Self::joint_fold_tree_is_legal(
2127 state.update,
2128 Some(owner),
2129 specs,
2130 &state_owners,
2131 arena,
2132 &mut update_visited[owner],
2133 ) {
2134 return false;
2135 }
2136 }
2137 }
2138 true
2139 }
2140
2141 fn joint_fold_tree_is_legal<A: Hash + Eq + Clone>(
2142 root: NodeId,
2143 update_owner: Option<usize>,
2144 specs: &[FoldGroupLowerSpec<'_, A>],
2145 state_owners: &crate::HashMap<A, Vec<(usize, BitAccess)>>,
2146 arena: &SLTNodeArena<A>,
2147 visited: &mut crate::HashSet<NodeId>,
2148 ) -> bool {
2149 let mut work = vec![root];
2150 while let Some(node) = work.pop() {
2151 if !visited.insert(node) {
2152 continue;
2153 }
2154 match arena.get(node) {
2155 SLTNode::Input {
2156 variable,
2157 index,
2158 access,
2159 ..
2160 } => {
2161 if let (Some(owner), Some(owners)) = (update_owner, state_owners.get(variable))
2162 {
2163 let overlaps = owners
2164 .iter()
2165 .filter(|(_, target)| !index.is_empty() || target.overlaps(access));
2166 for (state_owner, _) in overlaps {
2167 if owner != *state_owner {
2168 return false;
2169 }
2170 }
2171 }
2172 if let Some(owner) = update_owner {
2173 for spec in specs {
2174 if *spec.loop_var == *variable
2175 && (*spec.loop_var != *specs[owner].loop_var || !index.is_empty())
2176 {
2177 return false;
2178 }
2179 }
2180 }
2181 work.extend(index.iter().map(|entry| entry.node));
2182 }
2183 SLTNode::Constant(..) => {}
2184 SLTNode::Binary(lhs, _, rhs) => {
2185 work.push(*lhs);
2186 work.push(*rhs);
2187 }
2188 SLTNode::Unary(_, inner) | SLTNode::Capture { expr: inner, .. } => {
2189 work.push(*inner)
2190 }
2191 SLTNode::Mux {
2192 cond,
2193 then_expr,
2194 else_expr,
2195 } => {
2196 work.push(*cond);
2197 work.push(*then_expr);
2198 work.push(*else_expr);
2199 }
2200 SLTNode::Concat(parts) => {
2201 work.extend(parts.iter().map(|(part, _)| *part));
2202 }
2203 SLTNode::Slice { expr, .. } => work.push(*expr),
2204 SLTNode::ForFold { .. } | SLTNode::ForFoldGroup { .. } => return false,
2205 }
2206 }
2207 true
2208 }
2209
2210 pub fn lower_with_inputs<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
2211 &self,
2212 builder: &mut SIRBuilder<A>,
2213 node: NodeId,
2214 arena: &SLTNodeArena<A>,
2215 cache: &mut crate::HashMap<NodeId, RegisterId>,
2216 inputs: crate::HashMap<VarAtomBase<A>, RegisterId>,
2217 ) -> RegisterId {
2218 self.reset_cost_cache(node, arena, cache, false);
2219 let env = LowerEnv {
2220 inputs,
2221 parent: None,
2222 };
2223 self.lower_inner(builder, node, arena, cache, Some(&env), false)
2224 }
2225
2226 fn lower_slt_vector_expr<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
2227 &self,
2228 builder: &mut SIRBuilder<A>,
2229 expr: SLTVectorExpr<A>,
2230 width: usize,
2231 arena: &SLTNodeArena<A>,
2232 cache: &mut crate::HashMap<NodeId, RegisterId>,
2233 allow_cache: bool,
2234 ) -> RegisterId {
2235 match expr {
2236 SLTVectorExpr::Origin(SLTBitOrigin::Node(source)) => {
2237 self.lower_inner(builder, source, arena, cache, None, allow_cache)
2238 }
2239 SLTVectorExpr::Origin(SLTBitOrigin::Input {
2240 node,
2241 variable,
2242 signed: _,
2243 index,
2244 }) => self.lower_compacted_input(builder, node, &variable, &index, width, arena, cache),
2245 SLTVectorExpr::StaticInput {
2246 variable,
2247 access,
2248 unpacked_element_width,
2249 } => match unpacked_element_width {
2250 Some(element_width) => {
2251 let width = access.msb - access.lsb + 1;
2252 let destination = builder.alloc_logic(width);
2253 builder.emit(SIRInstruction::Load(
2254 destination,
2255 variable,
2256 SIROffset::PackedElements {
2257 bit_offset: access.lsb,
2258 element_width,
2259 },
2260 width,
2261 ));
2262 destination
2263 }
2264 None => self.lower_input(builder, &variable, &[], &access, arena, cache, None),
2265 },
2266 SLTVectorExpr::Broadcast(bit) => {
2267 let bit = self.lower_inner(builder, bit, arena, cache, None, allow_cache);
2268 if width == 1 {
2269 return bit;
2270 }
2271 let padding = builder.alloc_bit(width - 1, false);
2272 builder.emit(SIRInstruction::Imm(padding, SIRValue::new(0u8)));
2273 let extended = builder.alloc_bit(width, false);
2274 builder.emit(SIRInstruction::Concat(extended, vec![padding, bit]));
2275 let zero = builder.alloc_bit(width, false);
2276 builder.emit(SIRInstruction::Imm(zero, SIRValue::new(0u8)));
2277 let result = builder.alloc_bit(width, false);
2278 builder.emit(SIRInstruction::Binary(
2279 result,
2280 zero,
2281 BinaryOp::Sub,
2282 extended,
2283 ));
2284 result
2285 }
2286 SLTVectorExpr::LowOnes { bound } => {
2287 let bound = self.lower_inner(builder, bound, arena, cache, None, allow_cache);
2288 let one = builder.alloc_bit(width, false);
2289 builder.emit(SIRInstruction::Imm(one, SIRValue::new(1u8)));
2290 let shifted = builder.alloc_bit(width, false);
2291 builder.emit(SIRInstruction::Binary(shifted, one, BinaryOp::Shl, bound));
2292 let low_ones = builder.alloc_bit(width, false);
2293 builder.emit(SIRInstruction::Binary(
2294 low_ones,
2295 shifted,
2296 BinaryOp::Sub,
2297 one,
2298 ));
2299
2300 let bound_width = builder.register(&bound).width();
2305 let width_bits = (usize::BITS as usize - width.leading_zeros() as usize).max(1);
2306 if bound_width < width_bits {
2307 return low_ones;
2308 }
2309 let compare_width = bound_width.max(width_bits);
2310 let extended_bound = self.cast_reg_width_ext(builder, bound, compare_width, false);
2311 let width_value = builder.alloc_bit(compare_width, false);
2312 builder.emit(SIRInstruction::Imm(
2313 width_value,
2314 SIRValue::new(BigUint::from(width)),
2315 ));
2316 let saturated = builder.alloc_bit(1, false);
2317 builder.emit(SIRInstruction::Binary(
2318 saturated,
2319 extended_bound,
2320 BinaryOp::GeU,
2321 width_value,
2322 ));
2323 let all_ones = builder.alloc_bit(width, false);
2324 builder.emit(SIRInstruction::Imm(
2325 all_ones,
2326 SIRValue::new((BigUint::from(1u8) << width) - BigUint::from(1u8)),
2327 ));
2328 let result = builder.alloc_bit(width, false);
2329 builder.emit(SIRInstruction::Mux(result, saturated, all_ones, low_ones));
2330 result
2331 }
2332 SLTVectorExpr::Not(inner) => {
2333 let inner =
2334 self.lower_slt_vector_expr(builder, *inner, width, arena, cache, allow_cache);
2335 let result = builder.alloc_bit(width, false);
2336 builder.emit(SIRInstruction::Unary(result, UnaryOp::BitNot, inner));
2337 result
2338 }
2339 SLTVectorExpr::Binary { lhs, op, rhs } => {
2340 let lhs =
2341 self.lower_slt_vector_expr(builder, *lhs, width, arena, cache, allow_cache);
2342 let rhs =
2343 self.lower_slt_vector_expr(builder, *rhs, width, arena, cache, allow_cache);
2344 let result = builder.alloc_bit(width, false);
2345 builder.emit(SIRInstruction::Binary(result, lhs, op, rhs));
2346 result
2347 }
2348 }
2349 }
2350
2351 fn try_lower_count_idiom<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
2352 &self,
2353 builder: &mut SIRBuilder<A>,
2354 node: NodeId,
2355 arena: &SLTNodeArena<A>,
2356 cache: &mut crate::HashMap<NodeId, RegisterId>,
2357 allow_cache: bool,
2358 ) -> Option<RegisterId> {
2359 if self.four_state {
2360 return None;
2361 }
2362 let plan = if allow_cache {
2363 match_slt_count_idiom_with_materialized(node, arena, cache)?
2364 } else {
2365 match_slt_count_idiom(node, arena)?
2366 };
2367 if let SLTCountPost::AddTo(base) = &plan.post {
2368 let base = cache.get(base)?;
2369 if builder.register(base).width() != self.get_width(node, arena) {
2370 return None;
2371 }
2372 }
2373 let source = match plan.input {
2374 SLTCountInput::Origin(SLTBitOrigin::Node(source)) => {
2375 self.lower_inner(builder, source, arena, cache, None, allow_cache)
2376 }
2377 SLTCountInput::Origin(SLTBitOrigin::Input {
2378 node,
2379 variable,
2380 signed: _,
2381 index,
2382 }) => self.lower_compacted_input(
2383 builder,
2384 node,
2385 &variable,
2386 &index,
2387 plan.input_width,
2388 arena,
2389 cache,
2390 ),
2391 SLTCountInput::Vector(expr) => self.lower_slt_vector_expr(
2392 builder,
2393 expr,
2394 plan.input_width,
2395 arena,
2396 cache,
2397 allow_cache,
2398 ),
2399 SLTCountInput::Predicates(predicates) => {
2400 let args = predicates
2401 .into_iter()
2402 .map(|predicate| match predicate {
2403 SLTCountPredicate::Node(predicate) => {
2404 self.lower_inner(builder, predicate, arena, cache, None, allow_cache)
2405 }
2406 SLTCountPredicate::And(lhs, rhs) => {
2407 let lhs =
2408 self.lower_inner(builder, lhs, arena, cache, None, allow_cache);
2409 let rhs =
2410 self.lower_inner(builder, rhs, arena, cache, None, allow_cache);
2411 let predicate = builder.alloc_bit(1, false);
2412 builder.emit(SIRInstruction::Binary(
2413 predicate,
2414 lhs,
2415 BinaryOp::LogicAnd,
2416 rhs,
2417 ));
2418 predicate
2419 }
2420 })
2421 .collect();
2422 let source = builder.alloc_bit(plan.input_width, false);
2423 builder.emit(SIRInstruction::Concat(source, args));
2424 source
2425 }
2426 };
2427 let result_width = self.get_width(node, arena);
2428 match plan.post {
2429 SLTCountPost::Direct => {
2430 let result = builder.alloc_logic(result_width);
2431 builder.emit(SIRInstruction::Unary(result, plan.op, source));
2432 Some(result)
2433 }
2434 SLTCountPost::AddTo(base) => {
2435 let delta = if plan.op == UnaryOp::PopCount && plan.input_width == 1 {
2436 source
2437 } else {
2438 let delta = builder.alloc_logic(result_width);
2439 builder.emit(SIRInstruction::Unary(delta, plan.op, source));
2440 delta
2441 };
2442 let delta = self.cast_reg_width_ext(builder, delta, result_width, false);
2443 let base = *cache
2444 .get(&base)
2445 .expect("validated materialized count base must remain cached");
2446 let result = builder.alloc_logic(result_width);
2447 builder.emit(SIRInstruction::Binary(result, base, BinaryOp::Add, delta));
2448 Some(result)
2449 }
2450 SLTCountPost::SubtractFrom(minuend) => {
2451 let count = builder.alloc_logic(result_width);
2452 builder.emit(SIRInstruction::Unary(count, plan.op, source));
2453 let base = builder.alloc_logic(result_width);
2454 builder.emit(SIRInstruction::Imm(base, SIRValue::new(minuend)));
2455 let result = builder.alloc_logic(result_width);
2456 builder.emit(SIRInstruction::Binary(result, base, BinaryOp::Sub, count));
2457 Some(result)
2458 }
2459 SLTCountPost::ReplaceZeroInputCount(sentinel) => {
2460 let count = builder.alloc_logic(result_width);
2461 builder.emit(SIRInstruction::Unary(count, plan.op, source));
2462 let zero_count = builder.alloc_logic(result_width);
2463 builder.emit(SIRInstruction::Imm(
2464 zero_count,
2465 SIRValue::new(plan.input_width as u64),
2466 ));
2467 let is_zero_input = builder.alloc_bit(1, false);
2468 builder.emit(SIRInstruction::Binary(
2469 is_zero_input,
2470 count,
2471 BinaryOp::Eq,
2472 zero_count,
2473 ));
2474 let default = builder.alloc_logic(result_width);
2475 builder.emit(SIRInstruction::Imm(default, SIRValue::new(sentinel)));
2476 let result = builder.alloc_logic(result_width);
2477 builder.emit(SIRInstruction::Mux(result, is_zero_input, default, count));
2478 Some(result)
2479 }
2480 SLTCountPost::Select { cond, false_value } => {
2481 let count = builder.alloc_logic(result_width);
2482 builder.emit(SIRInstruction::Unary(count, plan.op, source));
2483 let cond = self.lower_inner(builder, cond, arena, cache, None, allow_cache);
2484 let false_value =
2485 self.lower_inner(builder, false_value, arena, cache, None, allow_cache);
2486 let result = builder.alloc_logic(result_width);
2487 builder.emit(SIRInstruction::Mux(result, cond, count, false_value));
2488 Some(result)
2489 }
2490 }
2491 }
2492
2493 pub fn project_materialized<A>(
2497 &self,
2498 builder: &mut SIRBuilder<A>,
2499 value: RegisterId,
2500 access: BitAccess,
2501 ) -> RegisterId {
2502 let source_width = builder.register(&value).width();
2503 debug_assert!(access.msb < source_width);
2504 if access.lsb == 0 && access.msb + 1 == source_width {
2505 value
2506 } else {
2507 self.slice_reg(builder, value, &access)
2508 }
2509 }
2510
2511 fn lower_inner<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
2512 &self,
2513 builder: &mut SIRBuilder<A>,
2514 node: NodeId,
2515 arena: &SLTNodeArena<A>,
2516 cache: &mut crate::HashMap<NodeId, RegisterId>,
2517 env: Option<&LowerEnv<'_, A>>,
2518 allow_cache: bool,
2519 ) -> RegisterId {
2520 if allow_cache {
2521 if let Some(reg) = cache.get(&node) {
2522 return *reg;
2523 }
2524 }
2525
2526 if env.is_none()
2527 && let Some(reg) = self.try_lower_count_idiom(builder, node, arena, cache, allow_cache)
2528 {
2529 if allow_cache {
2530 let previous = cache.insert(node, reg);
2531 debug_assert!(previous.is_none());
2532 self.cache_insert_log.borrow_mut().push(node);
2533 }
2534 return reg;
2535 }
2536
2537 let reg = match arena.get(node) {
2538 SLTNode::Input {
2539 variable: id,
2540 index,
2541 access,
2542 ..
2543 } => {
2544 if let Some(env) = env
2545 && let Some(reg) =
2546 self.lookup_override(builder, node, arena, cache, env, id, index, access)
2547 {
2548 reg
2549 } else {
2550 self.lower_input_for_node(builder, node, id, index, access, arena, cache, env)
2551 }
2552 }
2553 SLTNode::Constant(val, mask, width, _signed) => {
2554 let reg = if mask.is_zero() {
2555 builder.alloc_bit(*width, false)
2556 } else {
2557 builder.alloc_logic(*width)
2558 };
2559 builder.emit(SIRInstruction::Imm(
2560 reg,
2561 SIRValue::new_four_state(val.clone(), mask.clone()),
2562 ));
2563 reg
2564 }
2565 SLTNode::Binary(lhs, op, rhs) => {
2566 let mut l = self.lower_inner(builder, *lhs, arena, cache, env, allow_cache);
2567 let mut r = self.lower_inner(builder, *rhs, arena, cache, env, allow_cache);
2568 let width = self.get_width(node, arena);
2569 if matches!(
2570 op,
2571 BinaryOp::Eq
2572 | BinaryOp::Ne
2573 | BinaryOp::EqCase
2574 | BinaryOp::NeCase
2575 | BinaryOp::LtU
2576 | BinaryOp::LtS
2577 | BinaryOp::LeU
2578 | BinaryOp::LeS
2579 | BinaryOp::GtU
2580 | BinaryOp::GtS
2581 | BinaryOp::GeU
2582 | BinaryOp::GeS
2583 | BinaryOp::EqWildcard
2584 | BinaryOp::NeWildcard
2585 ) {
2586 let operand_width = builder
2587 .register(&l)
2588 .width()
2589 .max(builder.register(&r).width());
2590 let signed = matches!(
2591 op,
2592 BinaryOp::LtS | BinaryOp::LeS | BinaryOp::GtS | BinaryOp::GeS
2593 ) || matches!(
2594 op,
2595 BinaryOp::Eq
2596 | BinaryOp::Ne
2597 | BinaryOp::EqCase
2598 | BinaryOp::NeCase
2599 | BinaryOp::EqWildcard
2600 | BinaryOp::NeWildcard
2601 ) && self.get_bound_signed(*lhs, arena)
2602 && self.get_bound_signed(*rhs, arena);
2603 l = self.cast_reg_width_ext(builder, l, operand_width, signed);
2604 r = self.cast_reg_width_ext(builder, r, operand_width, signed);
2605 } else if matches!(
2606 op,
2607 BinaryOp::DivU | BinaryOp::DivS | BinaryOp::RemU | BinaryOp::RemS
2608 ) {
2609 let signed = matches!(op, BinaryOp::DivS | BinaryOp::RemS);
2610 l = self.cast_reg_width_ext(builder, l, width, signed);
2611 r = self.cast_reg_width_ext(builder, r, width, signed);
2612 }
2613 let dest = builder.alloc_logic(width);
2614 builder.emit(SIRInstruction::Binary(dest, l, *op, r));
2615 dest
2616 }
2617 SLTNode::Unary(op, inner) => {
2618 let i = self.lower_inner(builder, *inner, arena, cache, env, allow_cache);
2619 let width = self.get_width(node, arena);
2620 let dest = if matches!(op, UnaryOp::ToTwoState) {
2621 builder.alloc_bit(width, self.get_bound_signed(node, arena))
2622 } else {
2623 builder.alloc_logic(width)
2624 };
2625 builder.emit(SIRInstruction::Unary(dest, *op, i));
2626 dest
2627 }
2628 SLTNode::Capture { expr, .. } => {
2629 self.lower_inner(builder, *expr, arena, cache, env, allow_cache)
2630 }
2631 SLTNode::Slice { expr, access } => {
2632 self.lower_slice_inner(builder, *expr, access, arena, cache, env, allow_cache)
2633 }
2634 SLTNode::Concat(parts) => {
2635 self.lower_concat_inner(builder, node, parts, arena, cache, env, allow_cache)
2636 }
2637 SLTNode::Mux {
2638 cond,
2639 then_expr,
2640 else_expr,
2641 } => self.lower_mux_inner(
2642 builder,
2643 *cond,
2644 *then_expr,
2645 *else_expr,
2646 arena,
2647 cache,
2648 env,
2649 allow_cache,
2650 ),
2651 SLTNode::ForFold {
2652 loop_var,
2653 loop_width,
2654 loop_signed,
2655 start,
2656 end,
2657 inclusive,
2658 step,
2659 step_op,
2660 reverse,
2661 result,
2662 initials,
2663 updates,
2664 effects,
2665 continue_cond,
2666 } => self.lower_for_fold(
2667 builder,
2668 arena,
2669 cache,
2670 loop_var,
2671 *loop_width,
2672 *loop_signed,
2673 start,
2674 end,
2675 *inclusive,
2676 *step,
2677 *step_op,
2678 *reverse,
2679 result,
2680 initials,
2681 updates,
2682 effects,
2683 *continue_cond,
2684 env,
2685 ),
2686 SLTNode::ForFoldGroup { .. } => {
2687 let spec = FoldGroupLowerSpec::from_root(node, arena)
2688 .expect("matched ForFoldGroup must remain present in its arena");
2689 self.lower_fold_group_specs(
2690 builder,
2691 arena,
2692 cache,
2693 std::slice::from_ref(&spec),
2694 env,
2695 allow_cache,
2696 )[0]
2697 }
2698 };
2699
2700 if allow_cache {
2701 let previous = cache.insert(node, reg);
2702 debug_assert!(previous.is_none());
2703 self.cache_insert_log.borrow_mut().push(node);
2704 }
2705 reg
2706 }
2707
2708 fn lower_input<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
2709 &self,
2710 builder: &mut SIRBuilder<A>,
2711 id: &A,
2712 index: &[crate::SLTIndex],
2713 access: &BitAccess,
2714 arena: &SLTNodeArena<A>,
2715 cache: &mut crate::HashMap<NodeId, RegisterId>,
2716 env: Option<&LowerEnv<'_, A>>,
2717 ) -> RegisterId {
2718 let width = access.msb - access.lsb + 1;
2719 let dest = builder.alloc_logic(width);
2720
2721 if !index.is_empty() {
2722 let static_offset = index.iter().try_fold(access.lsb, |offset, entry| {
2729 let (value, mask) = try_const_eval(entry.node, arena)?;
2730 if !mask.is_zero() {
2731 return None;
2732 }
2733 let value = value.to_usize()?;
2734 offset.checked_add(value.checked_mul(entry.stride)?)
2735 });
2736 if let Some(static_offset) = static_offset {
2737 builder.emit(SIRInstruction::Load(
2738 dest,
2739 id.clone(),
2740 SIROffset::Static(static_offset),
2741 width,
2742 ));
2743 return dest;
2744 }
2745
2746 let element_width = index.iter().find_map(|entry| match entry.kind {
2747 crate::SLTIndexKind::Unpacked { element_width } => Some(element_width),
2748 crate::SLTIndexKind::Packed => None,
2749 });
2750 let element_access = element_width.filter(|element_width| {
2751 access.msb < *element_width
2752 && index.iter().all(|entry| match entry.kind {
2753 crate::SLTIndexKind::Unpacked {
2754 element_width: width,
2755 } => width == *element_width && entry.stride % *element_width == 0,
2756 crate::SLTIndexKind::Packed => entry.stride < *element_width,
2757 })
2758 });
2759 let mut element_dynamic = None;
2760 let mut packed_dynamic = None;
2761 let mut logical_dynamic = None;
2762 for idx_entry in index {
2763 let mut idx_val =
2764 self.lower_inner(builder, idx_entry.node, arena, cache, env, env.is_none());
2765
2766 let (stride, accumulator) = if let Some(element_width) = element_access {
2767 match idx_entry.kind {
2768 crate::SLTIndexKind::Unpacked { .. } => {
2769 (idx_entry.stride / element_width, &mut element_dynamic)
2770 }
2771 crate::SLTIndexKind::Packed => (idx_entry.stride, &mut packed_dynamic),
2772 }
2773 } else {
2774 (idx_entry.stride, &mut logical_dynamic)
2775 };
2776 if stride > 1 {
2777 let stride_reg = builder.alloc_bit(64, false);
2778 builder.emit(SIRInstruction::Imm(
2779 stride_reg,
2780 SIRValue::new(stride as u64),
2781 ));
2782 let stepped_idx = builder.alloc_bit(64, false);
2783 builder.emit(SIRInstruction::Binary(
2784 stepped_idx,
2785 idx_val,
2786 BinaryOp::Mul,
2787 stride_reg,
2788 ));
2789 idx_val = stepped_idx;
2790 }
2791
2792 if let Some(acc) = *accumulator {
2793 let new_acc = builder.alloc_bit(64, false);
2794 builder.emit(SIRInstruction::Binary(new_acc, acc, BinaryOp::Add, idx_val));
2795 *accumulator = Some(new_acc);
2796 } else {
2797 *accumulator = Some(idx_val);
2798 }
2799 }
2800
2801 let offset = if let Some(element_width) = element_access {
2802 if let Some(element_index) = element_dynamic {
2803 SIROffset::Element {
2804 index: element_index,
2805 element_width,
2806 bit_offset: access.lsb,
2807 dynamic_bit_offset: packed_dynamic,
2808 }
2809 } else {
2810 unreachable!("an unpacked element access has an unpacked index")
2811 }
2812 } else if let Some(dynamic_off) = logical_dynamic {
2813 if access.lsb == 0 {
2814 SIROffset::Dynamic(dynamic_off)
2815 } else {
2816 let static_off = builder.alloc_bit(64, false);
2817 builder.emit(SIRInstruction::Imm(
2818 static_off,
2819 SIRValue::new(access.lsb as u64),
2820 ));
2821 let final_off = builder.alloc_bit(64, false);
2822 builder.emit(SIRInstruction::Binary(
2823 final_off,
2824 static_off,
2825 BinaryOp::Add,
2826 dynamic_off,
2827 ));
2828 SIROffset::Dynamic(final_off)
2829 }
2830 } else {
2831 SIROffset::Static(access.lsb)
2832 };
2833 builder.emit(SIRInstruction::Load(dest, id.clone(), offset, width));
2834 } else {
2835 builder.emit(SIRInstruction::Load(
2836 dest,
2837 id.clone(),
2838 SIROffset::Static(access.lsb),
2839 width,
2840 ));
2841 }
2842
2843 dest
2844 }
2845
2846 fn build_dynamic_offset<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
2847 &self,
2848 builder: &mut SIRBuilder<A>,
2849 arena: &SLTNodeArena<A>,
2850 cache: &mut crate::HashMap<NodeId, RegisterId>,
2851 env: Option<&LowerEnv<'_, A>>,
2852 index: &[crate::SLTIndex],
2853 access: &BitAccess,
2854 ) -> RegisterId {
2855 let off_reg = builder.alloc_bit(64, false);
2856 builder.emit(SIRInstruction::Imm(
2857 off_reg,
2858 SIRValue::new(access.lsb as u64),
2859 ));
2860
2861 let mut total_dynamic = None;
2862 for idx_entry in index {
2863 let mut idx_val =
2864 self.lower_inner(builder, idx_entry.node, arena, cache, env, env.is_none());
2865
2866 if idx_entry.stride > 1 {
2867 let stride_reg = builder.alloc_bit(64, false);
2868 builder.emit(SIRInstruction::Imm(
2869 stride_reg,
2870 SIRValue::new(idx_entry.stride as u64),
2871 ));
2872 let stepped_idx = builder.alloc_bit(64, false);
2873 builder.emit(SIRInstruction::Binary(
2874 stepped_idx,
2875 idx_val,
2876 BinaryOp::Mul,
2877 stride_reg,
2878 ));
2879 idx_val = stepped_idx;
2880 }
2881
2882 if let Some(acc) = total_dynamic {
2883 let new_acc = builder.alloc_bit(64, false);
2884 builder.emit(SIRInstruction::Binary(new_acc, acc, BinaryOp::Add, idx_val));
2885 total_dynamic = Some(new_acc);
2886 } else {
2887 total_dynamic = Some(idx_val);
2888 }
2889 }
2890
2891 if let Some(dynamic_off) = total_dynamic {
2892 let final_off = builder.alloc_bit(64, false);
2893 builder.emit(SIRInstruction::Binary(
2894 final_off,
2895 off_reg,
2896 BinaryOp::Add,
2897 dynamic_off,
2898 ));
2899 final_off
2900 } else {
2901 off_reg
2902 }
2903 }
2904
2905 fn rebuild_override_range<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
2906 &self,
2907 builder: &mut SIRBuilder<A>,
2908 node: NodeId,
2909 arena: &SLTNodeArena<A>,
2910 cache: &mut crate::HashMap<NodeId, RegisterId>,
2911 env: &LowerEnv<'_, A>,
2912 id: &A,
2913 index: &[crate::SLTIndex],
2914 access: &BitAccess,
2915 ) -> Option<RegisterId> {
2916 let exact = VarAtomBase::new(id.clone(), access.lsb, access.msb);
2917 let mut higher_priority_overlap = false;
2918 let mut layer = Some(env);
2919 while let Some(current) = layer {
2920 if !higher_priority_overlap && let Some(reg) = current.inputs.get(&exact) {
2921 return Some(*reg);
2922 }
2923 for (target, reg) in ¤t.inputs {
2924 if target.id != *id {
2925 continue;
2926 }
2927 if !higher_priority_overlap
2928 && target.access.lsb <= access.lsb
2929 && access.msb <= target.access.msb
2930 {
2931 let rel = BitAccess::new(
2932 access.lsb - target.access.lsb,
2933 access.msb - target.access.lsb,
2934 );
2935 return Some(self.slice_reg(builder, *reg, &rel));
2936 }
2937 }
2938 higher_priority_overlap |= current.inputs.keys().any(|target| {
2939 target.id == *id
2940 && target.access.lsb <= access.msb
2941 && access.lsb <= target.access.msb
2942 });
2943 layer = current.parent;
2944 }
2945
2946 let mut cut_points = vec![access.lsb, access.msb + 1];
2947 let mut layer = Some(env);
2948 while let Some(current) = layer {
2949 for target in current.inputs.keys() {
2950 if target.id != *id {
2951 continue;
2952 }
2953 if target.access.msb < access.lsb || access.msb < target.access.lsb {
2954 continue;
2955 }
2956 cut_points.push(target.access.lsb.max(access.lsb));
2957 cut_points.push((target.access.msb + 1).min(access.msb + 1));
2958 }
2959 layer = current.parent;
2960 }
2961 cut_points.sort_unstable();
2962 cut_points.dedup();
2963 if cut_points.len() <= 2 {
2964 return None;
2965 }
2966
2967 let mut part_regs = Vec::new();
2968 for window in cut_points.windows(2).rev() {
2969 let part_access = BitAccess::new(window[0], window[1] - 1);
2970 let mut part_reg = None;
2971 let mut layer = Some(env);
2972 'layers: while let Some(current) = layer {
2973 for (target, reg) in ¤t.inputs {
2974 if target.id != *id {
2975 continue;
2976 }
2977 if target.access.lsb <= part_access.lsb && part_access.msb <= target.access.msb
2978 {
2979 let rel = BitAccess::new(
2980 part_access.lsb - target.access.lsb,
2981 part_access.msb - target.access.lsb,
2982 );
2983 part_reg = Some(self.slice_reg(builder, *reg, &rel));
2984 break 'layers;
2985 }
2986 }
2987 layer = current.parent;
2988 }
2989 let reg = part_reg.unwrap_or_else(|| {
2990 self.lower_input_for_node(
2991 builder,
2992 node,
2993 id,
2994 index,
2995 &part_access,
2996 arena,
2997 cache,
2998 None,
2999 )
3000 });
3001 part_regs.push(reg);
3002 }
3003
3004 if part_regs.len() == 1 {
3005 part_regs.into_iter().next()
3006 } else {
3007 let result = builder.alloc_logic(access.msb - access.lsb + 1);
3008 builder.emit(SIRInstruction::Concat(result, part_regs));
3009 Some(result)
3010 }
3011 }
3012
3013 fn lookup_override<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
3014 &self,
3015 builder: &mut SIRBuilder<A>,
3016 node: NodeId,
3017 arena: &SLTNodeArena<A>,
3018 cache: &mut crate::HashMap<NodeId, RegisterId>,
3019 env: &LowerEnv<'_, A>,
3020 id: &A,
3021 index: &[crate::SLTIndex],
3022 access: &BitAccess,
3023 ) -> Option<RegisterId> {
3024 if !index.is_empty() {
3025 let mut layer = Some(env);
3026 let mut has_override = false;
3027 while let Some(current) = layer {
3028 has_override |= current.inputs.keys().any(|target| target.id == *id);
3029 layer = current.parent;
3030 }
3031 if !has_override {
3032 return Some(self.lower_input(builder, id, index, access, arena, cache, Some(env)));
3033 }
3034
3035 let dynamic_off =
3036 self.build_dynamic_offset(builder, arena, cache, Some(env), index, access);
3037 let mut result = self.lower_input(builder, id, index, access, arena, cache, Some(env));
3038 let result_width = access.msb - access.lsb + 1;
3039 let mut layers = Vec::new();
3040 let mut layer = Some(env);
3041 while let Some(current) = layer {
3042 layers.push(current);
3043 layer = current.parent;
3044 }
3045 for current in layers.into_iter().rev() {
3048 for (target, reg) in ¤t.inputs {
3049 if target.id != *id {
3050 continue;
3051 }
3052 let range_lo = target.access.lsb;
3053 let Some(range_hi) = target
3054 .access
3055 .msb
3056 .checked_sub(result_width.saturating_sub(1))
3057 else {
3058 continue;
3059 };
3060 if range_lo > range_hi {
3061 continue;
3062 }
3063
3064 let lo_reg = builder.alloc_bit(64, false);
3065 builder.emit(SIRInstruction::Imm(lo_reg, SIRValue::new(range_lo as u64)));
3066 let hi_reg = builder.alloc_bit(64, false);
3067 builder.emit(SIRInstruction::Imm(hi_reg, SIRValue::new(range_hi as u64)));
3068
3069 let ge_lo = builder.alloc_bit(1, false);
3070 builder.emit(SIRInstruction::Binary(
3071 ge_lo,
3072 dynamic_off,
3073 BinaryOp::GeU,
3074 lo_reg,
3075 ));
3076 let le_hi = builder.alloc_bit(1, false);
3077 builder.emit(SIRInstruction::Binary(
3078 le_hi,
3079 dynamic_off,
3080 BinaryOp::LeU,
3081 hi_reg,
3082 ));
3083 let in_range = builder.alloc_bit(1, false);
3084 builder.emit(SIRInstruction::Binary(
3085 in_range,
3086 ge_lo,
3087 BinaryOp::And,
3088 le_hi,
3089 ));
3090
3091 let rel_off = if range_lo == 0 {
3092 dynamic_off
3093 } else {
3094 let rel = builder.alloc_bit(64, false);
3095 builder.emit(SIRInstruction::Binary(
3096 rel,
3097 dynamic_off,
3098 BinaryOp::Sub,
3099 lo_reg,
3100 ));
3101 rel
3102 };
3103
3104 let shifted = builder.alloc_logic(target.access.msb - target.access.lsb + 1);
3105 builder.emit(SIRInstruction::Binary(
3106 shifted,
3107 *reg,
3108 BinaryOp::Shr,
3109 rel_off,
3110 ));
3111 let candidate = self.cast_reg_width(builder, shifted, result_width);
3112 let merged = builder.alloc_logic(result_width);
3113 builder.emit(SIRInstruction::Mux(merged, in_range, candidate, result));
3114 result = merged;
3115 }
3116 }
3117 return Some(result);
3118 }
3119 self.rebuild_override_range(builder, node, arena, cache, env, id, index, access)
3120 }
3121
3122 fn get_width<A: Hash + Eq + Clone + std::fmt::Debug>(
3124 &self,
3125 node: NodeId,
3126 arena: &SLTNodeArena<A>,
3127 ) -> usize {
3128 crate::get_width(node, arena)
3129 }
3130
3131 fn get_bound_signed<A: Hash + Eq + Clone + std::fmt::Debug>(
3132 &self,
3133 node: NodeId,
3134 arena: &SLTNodeArena<A>,
3135 ) -> bool {
3136 match arena.get(node) {
3137 SLTNode::Input { signed, .. } => *signed,
3138 SLTNode::Constant(_, _, _, signed) => *signed,
3139 SLTNode::Binary(lhs, op, rhs) => match op {
3140 BinaryOp::Eq
3141 | BinaryOp::Ne
3142 | BinaryOp::EqCase
3143 | BinaryOp::NeCase
3144 | BinaryOp::LtU
3145 | BinaryOp::LtS
3146 | BinaryOp::LeU
3147 | BinaryOp::LeS
3148 | BinaryOp::GtU
3149 | BinaryOp::GtS
3150 | BinaryOp::GeU
3151 | BinaryOp::GeS
3152 | BinaryOp::LogicAnd
3153 | BinaryOp::LogicOr
3154 | BinaryOp::EqWildcard
3155 | BinaryOp::NeWildcard
3156 | BinaryOp::DivU
3157 | BinaryOp::RemU => false,
3158 BinaryOp::DivS | BinaryOp::RemS => true,
3159 BinaryOp::Shl | BinaryOp::Shr | BinaryOp::Sar => self.get_bound_signed(*lhs, arena),
3160 BinaryOp::Add
3161 | BinaryOp::Sub
3162 | BinaryOp::Mul
3163 | BinaryOp::And
3164 | BinaryOp::Or
3165 | BinaryOp::Xor => {
3166 self.get_bound_signed(*lhs, arena) && self.get_bound_signed(*rhs, arena)
3167 }
3168 },
3169 SLTNode::Unary(
3170 UnaryOp::LogicNot
3171 | UnaryOp::And
3172 | UnaryOp::Or
3173 | UnaryOp::Xor
3174 | UnaryOp::PopCount
3175 | UnaryOp::CountLeadingZeros
3176 | UnaryOp::CountTrailingZeros,
3177 _,
3178 ) => false,
3179 SLTNode::Unary(
3180 UnaryOp::Ident | UnaryOp::ToTwoState | UnaryOp::Minus | UnaryOp::BitNot,
3181 inner,
3182 ) => self.get_bound_signed(*inner, arena),
3183 SLTNode::Capture { expr, .. } => self.get_bound_signed(*expr, arena),
3184 SLTNode::Mux {
3185 then_expr,
3186 else_expr,
3187 ..
3188 } => {
3189 self.get_bound_signed(*then_expr, arena) && self.get_bound_signed(*else_expr, arena)
3190 }
3191 SLTNode::ForFold {
3192 loop_signed,
3193 result,
3194 ..
3195 } => match result {
3196 crate::SLTForFoldResult::State(_) => *loop_signed,
3197 crate::SLTForFoldResult::Transient { .. } => false,
3198 },
3199 SLTNode::ForFoldGroup { .. } => false,
3202 SLTNode::Slice { .. } => false,
3205 SLTNode::Concat(_) => false,
3206 }
3207 }
3208
3209 fn lower_slice_inner<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
3210 &self,
3211 builder: &mut SIRBuilder<A>,
3212 expr: NodeId,
3213 access: &BitAccess,
3214 arena: &SLTNodeArena<A>,
3215 cache: &mut crate::HashMap<NodeId, RegisterId>,
3216 env: Option<&LowerEnv<'_, A>>,
3217 allow_cache: bool,
3218 ) -> RegisterId {
3219 self.lower_region_slice_inner(builder, expr, access, arena, cache, env, allow_cache)
3220 }
3221
3222 fn lower_region_slice_inner<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
3223 &self,
3224 builder: &mut SIRBuilder<A>,
3225 expr: NodeId,
3226 access: &BitAccess,
3227 arena: &SLTNodeArena<A>,
3228 cache: &mut crate::HashMap<NodeId, RegisterId>,
3229 env: Option<&LowerEnv<'_, A>>,
3230 allow_cache: bool,
3231 ) -> RegisterId {
3232 if allow_cache && let Some(&full_value) = cache.get(&expr) {
3236 if access.lsb == 0 && access.msb + 1 == self.get_width(expr, arena) {
3237 return full_value;
3238 }
3239 return self.slice_reg(builder, full_value, access);
3240 }
3241
3242 if let SLTNode::Binary(lhs, BinaryOp::And, rhs) = arena.get(expr)
3247 && (slt_const_u64(*lhs, arena) == Some(0) || slt_const_u64(*rhs, arena) == Some(0))
3248 {
3249 let width = access.msb - access.lsb + 1;
3250 let result = builder.alloc_bit(width, false);
3251 builder.emit(SIRInstruction::Imm(result, SIRValue::new(0u8)));
3252 return result;
3253 }
3254
3255 let cache_projection = allow_cache
3259 && env.is_none()
3260 && self
3261 .cost_cache
3262 .borrow()
3263 .fanout
3264 .get(expr.0)
3265 .is_some_and(|fanout| *fanout > 1)
3266 && Self::is_nontrivial_node(expr, arena);
3267 let projection_key = (expr, *access);
3268 if cache_projection
3269 && let Some(&projected) = self.region_slice_cache.borrow().get(&projection_key)
3270 {
3271 return projected;
3272 }
3273
3274 let result = match arena.get(expr) {
3275 SLTNode::Input {
3276 variable,
3277 index,
3278 access: input_access,
3279 ..
3280 } if access.msb <= input_access.msb - input_access.lsb => {
3281 let composed =
3282 BitAccess::new(input_access.lsb + access.lsb, input_access.lsb + access.msb);
3283 if let Some(env) = env
3284 && let Some(reg) = self.lookup_override(
3285 builder, expr, arena, cache, env, variable, index, &composed,
3286 )
3287 {
3288 return reg;
3289 }
3290 self.lower_input_for_node(
3291 builder, expr, variable, index, &composed, arena, cache, env,
3292 )
3293 }
3294 SLTNode::Slice {
3295 expr: inner,
3296 access: inner_access,
3297 } if access.msb <= inner_access.msb - inner_access.lsb => {
3298 let composed =
3299 BitAccess::new(inner_access.lsb + access.lsb, inner_access.lsb + access.msb);
3300 self.lower_region_slice_inner(
3301 builder,
3302 *inner,
3303 &composed,
3304 arena,
3305 cache,
3306 env,
3307 allow_cache,
3308 )
3309 }
3310 SLTNode::Binary(lhs, op @ (BinaryOp::And | BinaryOp::Or | BinaryOp::Xor), rhs)
3311 if access.msb < self.get_width(*lhs, arena)
3312 && access.msb < self.get_width(*rhs, arena) =>
3313 {
3314 let lhs_val = self.lower_region_slice_inner(
3315 builder,
3316 *lhs,
3317 access,
3318 arena,
3319 cache,
3320 env,
3321 allow_cache,
3322 );
3323 let rhs_val = self.lower_region_slice_inner(
3324 builder,
3325 *rhs,
3326 access,
3327 arena,
3328 cache,
3329 env,
3330 allow_cache,
3331 );
3332 let result = builder.alloc_logic(access.msb - access.lsb + 1);
3333 builder.emit(SIRInstruction::Binary(result, lhs_val, *op, rhs_val));
3334 result
3335 }
3336 SLTNode::Binary(lhs, op @ (BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul), rhs)
3337 if access.lsb == 0
3338 && access.msb < self.get_width(*lhs, arena)
3339 && access.msb < self.get_width(*rhs, arena) =>
3340 {
3341 let lhs_val = self.lower_region_slice_inner(
3342 builder,
3343 *lhs,
3344 access,
3345 arena,
3346 cache,
3347 env,
3348 allow_cache,
3349 );
3350 let rhs_val = self.lower_region_slice_inner(
3351 builder,
3352 *rhs,
3353 access,
3354 arena,
3355 cache,
3356 env,
3357 allow_cache,
3358 );
3359 let result = builder.alloc_logic(access.msb + 1);
3360 builder.emit(SIRInstruction::Binary(result, lhs_val, *op, rhs_val));
3361 result
3362 }
3363 SLTNode::Unary(
3364 op @ (UnaryOp::Ident | UnaryOp::ToTwoState | UnaryOp::BitNot),
3365 inner,
3366 ) if access.msb < self.get_width(*inner, arena) => {
3367 let input = self.lower_region_slice_inner(
3368 builder,
3369 *inner,
3370 access,
3371 arena,
3372 cache,
3373 env,
3374 allow_cache,
3375 );
3376 let width = access.msb - access.lsb + 1;
3377 let result = if matches!(op, UnaryOp::ToTwoState) {
3378 builder.alloc_bit(width, self.get_bound_signed(expr, arena))
3379 } else {
3380 builder.alloc_logic(width)
3381 };
3382 builder.emit(SIRInstruction::Unary(result, *op, input));
3383 result
3384 }
3385 SLTNode::Mux {
3386 cond,
3387 then_expr,
3388 else_expr,
3389 } if access.msb < self.get_width(*then_expr, arena)
3390 && access.msb < self.get_width(*else_expr, arena) =>
3391 {
3392 self.lower_region_slice_mux_inner(
3393 builder,
3394 *cond,
3395 *then_expr,
3396 *else_expr,
3397 access,
3398 arena,
3399 cache,
3400 env,
3401 allow_cache,
3402 )
3403 }
3404 _ => {
3405 let inner = self.lower_inner(builder, expr, arena, cache, env, allow_cache);
3406 self.slice_reg(builder, inner, access)
3407 }
3408 };
3409
3410 if cache_projection {
3411 let previous = self
3412 .region_slice_cache
3413 .borrow_mut()
3414 .insert(projection_key, result);
3415 debug_assert!(previous.is_none());
3416 if previous.is_none() {
3417 self.region_slice_cache_insert_log
3418 .borrow_mut()
3419 .push(projection_key);
3420 }
3421 }
3422 result
3423 }
3424
3425 fn zero_required_from_both(
3426 lhs: &ZeroControllerFacts,
3427 rhs: &ZeroControllerFacts,
3428 ) -> ZeroControllerFacts {
3429 match (lhs.unconditional_zero, rhs.unconditional_zero) {
3430 (true, true) => ZeroControllerFacts {
3431 unconditional_zero: true,
3432 guards: crate::HashSet::default(),
3433 },
3434 (true, false) => rhs.clone(),
3435 (false, true) => lhs.clone(),
3436 (false, false) => {
3437 let (smaller, larger) = if lhs.guards.len() <= rhs.guards.len() {
3438 (&lhs.guards, &rhs.guards)
3439 } else {
3440 (&rhs.guards, &lhs.guards)
3441 };
3442 ZeroControllerFacts {
3443 unconditional_zero: false,
3444 guards: smaller
3445 .iter()
3446 .copied()
3447 .filter(|guard| larger.contains(guard))
3448 .collect(),
3449 }
3450 }
3451 }
3452 }
3453
3454 fn zero_from_either(
3455 lhs: &ZeroControllerFacts,
3456 rhs: &ZeroControllerFacts,
3457 ) -> ZeroControllerFacts {
3458 if lhs.unconditional_zero || rhs.unconditional_zero {
3459 return ZeroControllerFacts {
3460 unconditional_zero: true,
3461 guards: crate::HashSet::default(),
3462 };
3463 }
3464 let mut guards = lhs.guards.clone();
3465 guards.extend(rhs.guards.iter().copied());
3466 ZeroControllerFacts {
3467 unconditional_zero: false,
3468 guards,
3469 }
3470 }
3471
3472 fn zero_controller_facts<A: Hash + Eq + Clone + std::fmt::Debug>(
3476 &self,
3477 root: NodeId,
3478 arena: &SLTNodeArena<A>,
3479 memo: &mut crate::HashMap<NodeId, ZeroControllerFacts>,
3480 ) -> ZeroControllerFacts {
3481 if let Some(facts) = memo.get(&root) {
3482 return facts.clone();
3483 }
3484
3485 let mut stack = vec![(root, false)];
3488 while let Some((node, expanded)) = stack.pop() {
3489 if memo.contains_key(&node) {
3490 continue;
3491 }
3492 if !expanded {
3493 stack.push((node, true));
3494 for child in Self::node_children(node, arena).into_iter().rev() {
3495 if !memo.contains_key(&child) {
3496 stack.push((child, false));
3497 }
3498 }
3499 continue;
3500 }
3501
3502 let child = |node: NodeId| {
3503 memo.get(&node)
3504 .cloned()
3505 .expect("zero-controller postorder must analyze children first")
3506 };
3507 let mut facts = match arena.get(node) {
3508 SLTNode::Constant(value, mask, _, _) => ZeroControllerFacts {
3509 unconditional_zero: value.is_zero() && mask.is_zero(),
3510 guards: crate::HashSet::default(),
3511 },
3512 SLTNode::Input { .. } => ZeroControllerFacts::default(),
3513 SLTNode::Binary(lhs, op, rhs) => {
3514 let lhs = child(*lhs);
3515 let rhs = child(*rhs);
3516 match op {
3517 BinaryOp::And | BinaryOp::LogicAnd | BinaryOp::Mul => {
3518 Self::zero_from_either(&lhs, &rhs)
3519 }
3520 BinaryOp::Or
3521 | BinaryOp::Xor
3522 | BinaryOp::Add
3523 | BinaryOp::Sub
3524 | BinaryOp::LogicOr => Self::zero_required_from_both(&lhs, &rhs),
3525 BinaryOp::Shl | BinaryOp::Shr | BinaryOp::Sar => lhs,
3526 BinaryOp::Eq
3527 | BinaryOp::Ne
3528 | BinaryOp::EqCase
3529 | BinaryOp::NeCase
3530 | BinaryOp::EqWildcard
3531 | BinaryOp::NeWildcard
3532 | BinaryOp::LtU
3533 | BinaryOp::LtS
3534 | BinaryOp::LeU
3535 | BinaryOp::LeS
3536 | BinaryOp::GtU
3537 | BinaryOp::GtS
3538 | BinaryOp::GeU
3539 | BinaryOp::GeS
3540 | BinaryOp::DivU
3541 | BinaryOp::DivS
3542 | BinaryOp::RemU
3543 | BinaryOp::RemS => ZeroControllerFacts::default(),
3544 }
3545 }
3546 SLTNode::Unary(op, inner) => match op {
3547 UnaryOp::Ident
3548 | UnaryOp::ToTwoState
3549 | UnaryOp::Minus
3550 | UnaryOp::And
3551 | UnaryOp::Or
3552 | UnaryOp::Xor
3553 | UnaryOp::PopCount => child(*inner),
3554 UnaryOp::LogicNot
3555 | UnaryOp::BitNot
3556 | UnaryOp::CountLeadingZeros
3557 | UnaryOp::CountTrailingZeros => ZeroControllerFacts::default(),
3558 },
3559 SLTNode::Capture { expr, .. } => child(*expr),
3560 SLTNode::Slice { expr, .. } => child(*expr),
3561 SLTNode::Concat(parts) => {
3562 let mut combined = ZeroControllerFacts {
3563 unconditional_zero: true,
3564 guards: crate::HashSet::default(),
3565 };
3566 for (part, _) in parts {
3567 combined = Self::zero_required_from_both(&combined, &child(*part));
3568 }
3569 combined
3570 }
3571 SLTNode::Mux {
3572 then_expr,
3573 else_expr,
3574 ..
3575 } => Self::zero_required_from_both(&child(*then_expr), &child(*else_expr)),
3576 SLTNode::ForFold { .. } | SLTNode::ForFoldGroup { .. } => {
3577 ZeroControllerFacts::default()
3578 }
3579 };
3580
3581 let shared = self.cost_cache.borrow().fanout[node.0] > 1;
3582 if !facts.unconditional_zero
3583 && shared
3584 && self.get_width(node, arena) == 1
3585 && !matches!(arena.get(node), SLTNode::Constant(..))
3586 {
3587 facts.guards.clear();
3592 facts.guards.insert(node);
3593 }
3594 memo.insert(node, facts);
3595 }
3596
3597 memo.get(&root)
3598 .cloned()
3599 .expect("zero-controller root must be produced by its postorder")
3600 }
3601
3602 fn guarded_concat_root_is_supported<A: Hash + Eq + Clone>(
3603 root: NodeId,
3604 arena: &SLTNodeArena<A>,
3605 ) -> bool {
3606 let mut visited = crate::HashSet::default();
3607 let mut work = vec![root];
3608 while let Some(node) = work.pop() {
3609 if !visited.insert(node) {
3610 continue;
3611 }
3612 match arena.get(node) {
3613 SLTNode::Binary(
3614 _,
3615 BinaryOp::DivU | BinaryOp::DivS | BinaryOp::RemU | BinaryOp::RemS,
3616 _,
3617 )
3618 | SLTNode::ForFold { .. }
3619 | SLTNode::ForFoldGroup { .. } => return false,
3620 _ => work.extend(Self::node_children(node, arena)),
3621 }
3622 }
3623 true
3624 }
3625
3626 fn guarded_concat_region_cost<A: Hash + Eq + Clone + std::fmt::Debug>(
3627 &self,
3628 root: NodeId,
3629 guard: NodeId,
3630 arena: &SLTNodeArena<A>,
3631 materialized: &crate::HashMap<NodeId, RegisterId>,
3632 ) -> (u128, u128) {
3633 let mut guard_closure = crate::HashSet::default();
3634 let mut guard_work = vec![guard];
3635 while let Some(node) = guard_work.pop() {
3636 if guard_closure.insert(node) {
3637 guard_work.extend(Self::node_children(node, arena));
3638 }
3639 }
3640
3641 let mut visited = crate::HashSet::default();
3642 let mut live_through = crate::HashSet::default();
3643 let mut work = vec![root];
3644 let mut cost = 0u128;
3645 while let Some(node) = work.pop() {
3646 if !visited.insert(node) {
3647 continue;
3648 }
3649 if guard_closure.contains(&node) || materialized.contains_key(&node) {
3653 live_through.insert(node);
3654 continue;
3655 }
3656
3657 cost = cost.saturating_add(self.intrinsic_node_cost(node, arena));
3658 if !matches!(arena.get(node), SLTNode::Mux { .. }) {
3662 work.extend(Self::node_children(node, arena));
3663 }
3664 }
3665 let live_through_cost = live_through
3666 .into_iter()
3667 .map(|node| Self::chunks(self.get_width(node, arena)))
3668 .fold(0u128, u128::saturating_add);
3669 (cost, live_through_cost)
3670 }
3671
3672 fn guarded_concat_net_benefit<A: Hash + Eq + Clone + std::fmt::Debug>(
3673 &self,
3674 root: NodeId,
3675 guard: NodeId,
3676 arena: &SLTNodeArena<A>,
3677 materialized: &crate::HashMap<NodeId, RegisterId>,
3678 ) -> Option<u128> {
3679 const CONTROL_COST: u128 = 3;
3680 const MISPREDICT_COST: u128 = 16;
3681 const PHI_COPY_COST_PER_CHUNK: u128 = 2;
3682 const LIVE_THROUGH_COST_PER_CHUNK: u128 = 1;
3683
3684 let probability = Self::guarded_true_probability(guard, arena);
3685 let false_weight = probability.total_weight - probability.true_weight;
3686 let (skippable_cost, live_through_chunks) =
3687 self.guarded_concat_region_cost(root, guard, arena, materialized);
3688 let result_chunks = Self::chunks(self.get_width(root, arena));
3689 let saved_scaled = false_weight.saturating_mul(skippable_cost);
3690 let introduced_scaled = probability
3691 .total_weight
3692 .saturating_mul(
3693 CONTROL_COST
3694 .saturating_add(result_chunks.saturating_mul(PHI_COPY_COST_PER_CHUNK))
3695 .saturating_add(
3696 live_through_chunks.saturating_mul(LIVE_THROUGH_COST_PER_CHUNK),
3697 ),
3698 )
3699 .saturating_add(false_weight.saturating_mul(result_chunks))
3700 .saturating_add(
3701 probability
3702 .true_weight
3703 .min(false_weight)
3704 .saturating_mul(MISPREDICT_COST),
3705 );
3706 (saved_scaled > introduced_scaled).then(|| saved_scaled - introduced_scaled)
3707 }
3708
3709 fn guarded_concat_plan<A: Hash + Eq + Clone + std::fmt::Debug>(
3710 &self,
3711 root: NodeId,
3712 arena: &SLTNodeArena<A>,
3713 materialized: &crate::HashMap<NodeId, RegisterId>,
3714 ) -> Option<GuardedConcatPlan> {
3715 if self.four_state || !Self::guarded_concat_root_is_supported(root, arena) {
3716 return None;
3717 }
3718
3719 let mut memo = crate::HashMap::default();
3720 let facts = self.zero_controller_facts(root, arena, &mut memo);
3721 if facts.unconditional_zero {
3722 return None;
3723 }
3724 let mut guards = facts.guards.into_iter().collect::<Vec<_>>();
3725 guards.sort_unstable();
3726
3727 let mut best = None;
3728 for guard in guards {
3729 if guard == root || self.get_width(guard, arena) != 1 {
3730 continue;
3731 }
3732 let Some(net_benefit_scaled) =
3733 self.guarded_concat_net_benefit(root, guard, arena, materialized)
3734 else {
3735 continue;
3736 };
3737 let candidate = GuardedConcatPlan {
3738 guard,
3739 net_benefit_scaled,
3740 };
3741 let replace = best.as_ref().is_none_or(|current: &GuardedConcatPlan| {
3742 candidate.net_benefit_scaled > current.net_benefit_scaled
3743 || candidate.net_benefit_scaled == current.net_benefit_scaled
3744 && candidate.guard < current.guard
3745 });
3746 if replace {
3747 best = Some(candidate);
3748 }
3749 }
3750 best
3751 }
3752
3753 #[allow(clippy::too_many_arguments)]
3754 fn lower_guarded_concat_cfg<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
3755 &self,
3756 builder: &mut SIRBuilder<A>,
3757 plan: GuardedConcatPlan,
3758 parts: &[(NodeId, usize)],
3759 arena: &SLTNodeArena<A>,
3760 cache: &mut crate::HashMap<NodeId, RegisterId>,
3761 ) -> RegisterId {
3762 let guard = self.lower_inner(builder, plan.guard, arena, cache, None, true);
3763 let width = parts.iter().map(|(_, width)| *width).sum();
3764 let result = builder.alloc_logic(width);
3765 let true_block = builder.new_block();
3766 let false_block = builder.new_block();
3767 let merge_block = builder.new_block_with(vec![result]);
3768 builder.seal_block(SIRTerminator::Branch {
3769 cond: guard,
3770 true_block: (true_block, Vec::new()),
3771 false_block: (false_block, Vec::new()),
3772 });
3773
3774 let true_transaction = self.cache_transaction();
3775 builder.switch_to_block(true_block);
3776 let true_value = self.lower_concat_eager_inner(builder, parts, arena, cache, None, true);
3777 builder.seal_block(SIRTerminator::Jump(merge_block, vec![true_value]));
3778 self.rollback_cache(cache, true_transaction);
3779
3780 builder.switch_to_block(false_block);
3781 let zero = builder.alloc_logic(width);
3782 builder.emit(SIRInstruction::Imm(zero, SIRValue::new(0u64)));
3783 builder.seal_block(SIRTerminator::Jump(merge_block, vec![zero]));
3784
3785 builder.switch_to_block(merge_block);
3786 result
3787 }
3788
3789 fn lower_concat_inner<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
3790 &self,
3791 builder: &mut SIRBuilder<A>,
3792 node: NodeId,
3793 parts: &[(NodeId, usize)],
3794 arena: &SLTNodeArena<A>,
3795 cache: &mut crate::HashMap<NodeId, RegisterId>,
3796 env: Option<&LowerEnv<'_, A>>,
3797 allow_cache: bool,
3798 ) -> RegisterId {
3799 if env.is_none()
3801 && let Some(reg) = self.try_fold_const_concat(builder, parts, arena)
3802 {
3803 return reg;
3804 }
3805
3806 if env.is_none()
3807 && allow_cache
3808 && let Some(plan) = self.guarded_concat_plan(node, arena, cache)
3809 {
3810 return self.lower_guarded_concat_cfg(builder, plan, parts, arena, cache);
3811 }
3812
3813 self.lower_concat_eager_inner(builder, parts, arena, cache, env, allow_cache)
3814 }
3815
3816 fn lower_concat_eager_inner<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
3817 &self,
3818 builder: &mut SIRBuilder<A>,
3819 parts: &[(NodeId, usize)],
3820 arena: &SLTNodeArena<A>,
3821 cache: &mut crate::HashMap<NodeId, RegisterId>,
3822 env: Option<&LowerEnv<'_, A>>,
3823 allow_cache: bool,
3824 ) -> RegisterId {
3825 let total_width: usize = parts.iter().map(|(_, w)| w).sum();
3829 let part_regs: Vec<RegisterId> = parts
3830 .iter()
3831 .map(|(node, width)| {
3832 let reg = self.lower_inner(builder, *node, arena, cache, env, allow_cache);
3833 self.cast_reg_width(builder, reg, *width)
3834 })
3835 .collect();
3836 let result = builder.alloc_logic(total_width);
3837 builder.emit(SIRInstruction::Concat(result, part_regs));
3838 result
3839 }
3840
3841 fn try_fold_const_concat<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
3844 &self,
3845 builder: &mut SIRBuilder<A>,
3846 parts: &[(NodeId, usize)],
3847 arena: &SLTNodeArena<A>,
3848 ) -> Option<RegisterId> {
3849 let mut const_parts: Vec<(BigUint, BigUint, usize)> = Vec::with_capacity(parts.len());
3850 for (node_id, width) in parts {
3851 let (val, mask) = try_const_eval(*node_id, arena)?;
3852 const_parts.push((val, mask, *width));
3853 }
3854
3855 let mut combined_val = BigUint::from(0u32);
3857 let mut combined_mask = BigUint::from(0u32);
3858 let mut total_width = 0usize;
3859 for (val, mask, width) in const_parts.iter().rev() {
3860 let width_mask = if *width >= 64 {
3861 (BigUint::from(1u64) << width) - 1u64
3862 } else {
3863 BigUint::from((1u64 << width) - 1)
3864 };
3865 combined_val |= (val & &width_mask) << total_width;
3866 combined_mask |= (mask & &width_mask) << total_width;
3867 total_width += *width;
3868 }
3869
3870 let reg = if combined_mask.is_zero() {
3871 builder.alloc_bit(total_width, false)
3872 } else {
3873 builder.alloc_logic(total_width)
3874 };
3875 builder.emit(SIRInstruction::Imm(
3876 reg,
3877 SIRValue::new_four_state(combined_val, combined_mask),
3878 ));
3879 Some(reg)
3880 }
3881
3882 fn reset_cost_cache<A: Hash + Eq + Clone>(
3883 &self,
3884 root: NodeId,
3885 arena: &SLTNodeArena<A>,
3886 materialized: &crate::HashMap<NodeId, RegisterId>,
3887 honor_materialized: bool,
3888 ) {
3889 self.reset_cost_cache_roots(
3890 std::slice::from_ref(&root),
3891 arena,
3892 materialized,
3893 honor_materialized,
3894 );
3895 }
3896
3897 fn reset_cost_cache_roots<A: Hash + Eq + Clone>(
3898 &self,
3899 roots: &[NodeId],
3900 arena: &SLTNodeArena<A>,
3901 materialized: &crate::HashMap<NodeId, RegisterId>,
3902 honor_materialized: bool,
3903 ) {
3904 let node_count = arena.len();
3905 let mut cache = self.cost_cache.borrow_mut();
3906 cache.tree_costs.clear();
3907 cache.tree_costs.resize(node_count, None);
3908 cache.contains_div_rem.clear();
3909 cache.contains_div_rem.resize(node_count, None);
3910 cache.fanout.clear();
3911 cache.fanout.resize(node_count, 0);
3912 cache.initially_materialized.clear();
3913 cache.initially_materialized.resize(node_count, false);
3914 cache.owned_costs.clear();
3915 cache.owned_costs.resize(node_count, None);
3916 cache.owned_slice_lower_costs.clear();
3917 cache.owned_slice_lower_costs.resize(node_count, None);
3918 cache.contains_shared_nontrivial.clear();
3919 cache.contains_shared_nontrivial.resize(node_count, None);
3920 cache.is_speculatable_pure.clear();
3921 cache.is_speculatable_pure.resize(node_count, None);
3922 cache.traversal_seen.clear();
3923 cache.traversal_seen.resize(node_count, false);
3924 cache.traversal_work.clear();
3925 cache.traversal_work.extend_from_slice(roots);
3926 #[cfg(test)]
3927 {
3928 cache.analysis_node_visits = 0;
3929 }
3930
3931 while let Some(node) = cache.traversal_work.pop() {
3932 if cache.traversal_seen[node.0] {
3933 continue;
3934 }
3935 cache.traversal_seen[node.0] = true;
3936 #[cfg(test)]
3937 {
3938 cache.analysis_node_visits += 1;
3939 }
3940 if honor_materialized && materialized.contains_key(&node) {
3941 cache.initially_materialized[node.0] = true;
3942 continue;
3943 }
3944 for child in Self::node_children(node, arena) {
3945 cache.fanout[child.0] = cache.fanout[child.0].saturating_add(1);
3946 cache.traversal_work.push(child);
3947 }
3948 }
3949 self.cache_insert_log.borrow_mut().clear();
3950 self.region_slice_cache.borrow_mut().clear();
3951 self.region_slice_cache_insert_log.borrow_mut().clear();
3952 }
3953
3954 fn cache_transaction(&self) -> LowerCacheTransaction {
3955 LowerCacheTransaction {
3956 node_insertions: self.cache_insert_log.borrow().len(),
3957 region_slice_insertions: self.region_slice_cache_insert_log.borrow().len(),
3958 }
3959 }
3960
3961 #[cfg(test)]
3962 fn note_analysis_visits(&self, visits: usize) {
3963 let mut cache = self.cost_cache.borrow_mut();
3964 cache.analysis_node_visits = cache.analysis_node_visits.saturating_add(visits);
3965 }
3966
3967 #[cfg(not(test))]
3968 #[inline(always)]
3969 fn note_analysis_visits(&self, _visits: usize) {}
3970
3971 #[cfg(test)]
3972 fn analysis_node_visits(&self) -> usize {
3973 self.cost_cache.borrow().analysis_node_visits
3974 }
3975
3976 fn rollback_cache(
3977 &self,
3978 cache: &mut crate::HashMap<NodeId, RegisterId>,
3979 transaction: LowerCacheTransaction,
3980 ) {
3981 let mut log = self.cache_insert_log.borrow_mut();
3982 for node in log.drain(transaction.node_insertions..) {
3983 cache.remove(&node);
3984 }
3985 let mut region_log = self.region_slice_cache_insert_log.borrow_mut();
3986 let mut region_cache = self.region_slice_cache.borrow_mut();
3987 for key in region_log.drain(transaction.region_slice_insertions..) {
3988 region_cache.remove(&key);
3989 }
3990 }
3991
3992 pub(crate) fn take_scheduled_region_insertions(&self) -> Vec<NodeId> {
3996 std::mem::take(&mut *self.cache_insert_log.borrow_mut())
3997 }
3998
3999 fn prepare_cost_cache<A: Hash + Eq + Clone>(&self, arena: &SLTNodeArena<A>) {
4000 let mut cache = self.cost_cache.borrow_mut();
4001 if cache.tree_costs.len() < arena.len() {
4002 cache.tree_costs.resize(arena.len(), None);
4003 cache.contains_div_rem.resize(arena.len(), None);
4004 cache.fanout.resize(arena.len(), 0);
4005 cache.initially_materialized.resize(arena.len(), false);
4006 cache.owned_costs.resize(arena.len(), None);
4007 cache.owned_slice_lower_costs.resize(arena.len(), None);
4008 cache.contains_shared_nontrivial.resize(arena.len(), None);
4009 cache.is_speculatable_pure.resize(arena.len(), None);
4010 }
4011 }
4012
4013 fn node_children<A: Hash + Eq + Clone>(node: NodeId, arena: &SLTNodeArena<A>) -> Vec<NodeId> {
4014 match arena.get(node) {
4015 SLTNode::Input { index, .. } => index.iter().map(|entry| entry.node).collect(),
4016 SLTNode::Constant(..) => Vec::new(),
4017 SLTNode::Binary(lhs, _, rhs) => vec![*lhs, *rhs],
4018 SLTNode::Unary(_, inner) => vec![*inner],
4019 SLTNode::Capture { expr, .. } => vec![*expr],
4020 SLTNode::Mux {
4021 cond,
4022 then_expr,
4023 else_expr,
4024 } => vec![*cond, *then_expr, *else_expr],
4025 SLTNode::Concat(parts) => parts.iter().map(|(part, _)| *part).collect(),
4026 SLTNode::Slice { expr, .. } => vec![*expr],
4027 SLTNode::ForFold {
4028 start,
4029 end,
4030 result,
4031 initials,
4032 updates,
4033 effects,
4034 continue_cond,
4035 ..
4036 } => {
4037 let mut children = Vec::new();
4038 if let SLTLoopBound::Expr(node) = start {
4039 children.push(*node);
4040 }
4041 if let SLTLoopBound::Expr(node) = end {
4042 children.push(*node);
4043 }
4044 if let crate::SLTForFoldResult::Transient { initial, update } = result {
4045 children.push(*initial);
4046 children.push(*update);
4047 }
4048 children.extend(initials.iter().map(|update| update.expr));
4049 children.extend(updates.iter().map(|update| update.expr));
4050 for effect in effects {
4051 match effect {
4052 crate::SLTForEffect::Event { guard, args, .. } => {
4053 children.extend(*guard);
4054 children.extend(args.iter().copied());
4055 }
4056 crate::SLTForEffect::Runner(runner) => children.push(*runner),
4057 }
4058 }
4059 children.push(*continue_cond);
4060 children
4061 }
4062 SLTNode::ForFoldGroup {
4063 entry_guard,
4064 states,
4065 ..
4066 } => std::iter::once(*entry_guard)
4067 .chain(
4068 states
4069 .iter()
4070 .flat_map(|state| [state.initial, state.update]),
4071 )
4072 .collect(),
4073 }
4074 }
4075
4076 fn chunks(width: usize) -> u128 {
4077 width.div_ceil(64).max(1) as u128
4078 }
4079
4080 fn binary_operation_cost(op: BinaryOp, width: usize) -> u128 {
4081 let chunks = Self::chunks(width);
4082 match op {
4083 BinaryOp::And
4084 | BinaryOp::Or
4085 | BinaryOp::Xor
4086 | BinaryOp::LogicAnd
4087 | BinaryOp::LogicOr => chunks,
4088 BinaryOp::Add | BinaryOp::Sub => 3 * chunks,
4089 BinaryOp::Mul => 5 * chunks.saturating_mul(chunks),
4090 BinaryOp::DivU | BinaryOp::DivS | BinaryOp::RemU | BinaryOp::RemS => {
4091 12 * chunks.saturating_mul(chunks)
4092 }
4093 BinaryOp::Shl | BinaryOp::Shr | BinaryOp::Sar => 4 * chunks,
4094 BinaryOp::Eq
4095 | BinaryOp::Ne
4096 | BinaryOp::EqCase
4097 | BinaryOp::NeCase
4098 | BinaryOp::EqWildcard
4099 | BinaryOp::NeWildcard
4100 | BinaryOp::LtU
4101 | BinaryOp::LtS
4102 | BinaryOp::LeU
4103 | BinaryOp::LeS
4104 | BinaryOp::GtU
4105 | BinaryOp::GtS
4106 | BinaryOp::GeU
4107 | BinaryOp::GeS => 3 * chunks,
4108 }
4109 }
4110
4111 fn intrinsic_node_cost<A: Hash + Eq + Clone + std::fmt::Debug>(
4114 &self,
4115 node: NodeId,
4116 arena: &SLTNodeArena<A>,
4117 ) -> u128 {
4118 match arena.get(node) {
4119 SLTNode::Input { access, index, .. } => {
4120 let chunks = Self::chunks(access.msb - access.lsb + 1);
4121 3 * chunks + u128::from(!index.is_empty()) * 3
4122 }
4123 SLTNode::Constant(_, _, width, _) => Self::chunks(*width),
4124 SLTNode::Binary(lhs, op, rhs) => {
4125 let width = self.get_width(*lhs, arena).max(self.get_width(*rhs, arena));
4126 Self::binary_operation_cost(*op, width)
4127 }
4128 SLTNode::Unary(op, inner) => {
4129 let chunks = Self::chunks(self.get_width(*inner, arena));
4130 match op {
4131 UnaryOp::PopCount => 2 * chunks + 1,
4132 UnaryOp::CountLeadingZeros | UnaryOp::CountTrailingZeros => 3 * chunks + 1,
4133 _ => 2 * chunks,
4134 }
4135 }
4136 SLTNode::Capture { .. } => 0,
4137 SLTNode::Mux {
4138 then_expr,
4139 else_expr,
4140 ..
4141 } => Self::chunks(
4142 self.get_width(*then_expr, arena)
4143 .max(self.get_width(*else_expr, arena)),
4144 ),
4145 SLTNode::Concat(parts) => {
4146 let width = parts.iter().map(|(_, width)| *width).sum();
4147 Self::chunks(width) + parts.len() as u128
4148 }
4149 SLTNode::Slice { access, .. } => 2 * Self::chunks(access.msb - access.lsb + 1),
4150 SLTNode::ForFold { updates, .. } => 8 + 2 * updates.len() as u128,
4155 SLTNode::ForFoldGroup { states, .. } => 6 + 2 * states.len() as u128,
4156 }
4157 }
4158
4159 fn estimated_tree_cost<A: Hash + Eq + Clone + std::fmt::Debug>(
4163 &self,
4164 node: NodeId,
4165 arena: &SLTNodeArena<A>,
4166 ) -> u128 {
4167 self.prepare_cost_cache(arena);
4168 if let Some(cost) = self.cost_cache.borrow().tree_costs[node.0] {
4169 return cost;
4170 }
4171 self.note_analysis_visits(1);
4172 let mut cost = self.intrinsic_node_cost(node, arena);
4173 for child in Self::node_children(node, arena) {
4174 cost = cost.saturating_add(self.estimated_tree_cost(child, arena));
4175 }
4176 self.cost_cache.borrow_mut().tree_costs[node.0] = Some(cost);
4177 cost
4178 }
4179
4180 fn is_nontrivial_node<A: Hash + Eq + Clone>(node: NodeId, arena: &SLTNodeArena<A>) -> bool {
4181 !matches!(
4182 arena.get(node),
4183 SLTNode::Input { .. } | SLTNode::Constant(..)
4184 )
4185 }
4186
4187 fn owned_tree_cost<A: Hash + Eq + Clone + std::fmt::Debug>(
4193 &self,
4194 node: NodeId,
4195 arena: &SLTNodeArena<A>,
4196 ) -> u128 {
4197 self.prepare_cost_cache(arena);
4198 if let Some(cost) = self.cost_cache.borrow().owned_costs[node.0] {
4199 return cost;
4200 }
4201 self.note_analysis_visits(1);
4202 let excluded = {
4203 let cache = self.cost_cache.borrow();
4204 cache.initially_materialized[node.0] || cache.fanout[node.0] > 1
4205 };
4206 let mut cost = if excluded {
4207 0
4208 } else {
4209 self.intrinsic_node_cost(node, arena)
4210 };
4211 if !excluded {
4212 for child in Self::node_children(node, arena) {
4213 cost = cost.saturating_add(self.owned_tree_cost(child, arena));
4214 }
4215 }
4216 self.cost_cache.borrow_mut().owned_costs[node.0] = Some(cost);
4217 cost
4218 }
4219
4220 fn owned_slice_lower_cost<A: Hash + Eq + Clone + std::fmt::Debug>(
4224 &self,
4225 node: NodeId,
4226 arena: &SLTNodeArena<A>,
4227 ) -> u128 {
4228 self.prepare_cost_cache(arena);
4229 if let Some(cost) = self.cost_cache.borrow().owned_slice_lower_costs[node.0] {
4230 return cost;
4231 }
4232 self.note_analysis_visits(1);
4233 let excluded = {
4234 let cache = self.cost_cache.borrow();
4235 cache.initially_materialized[node.0] || cache.fanout[node.0] > 1
4236 };
4237 let mut cost = if excluded {
4238 0
4239 } else {
4240 match arena.get(node) {
4241 SLTNode::Slice { .. } => 0,
4242 SLTNode::Binary(_, op, _) => Self::binary_operation_cost(*op, 1),
4243 SLTNode::Unary(..) => 1,
4244 SLTNode::Capture { .. } => 0,
4245 SLTNode::ForFold { updates, .. } => 8 + 2 * updates.len() as u128,
4246 SLTNode::ForFoldGroup { states, .. } => 6 + 2 * states.len() as u128,
4247 SLTNode::Input { .. }
4248 | SLTNode::Constant(..)
4249 | SLTNode::Mux { .. }
4250 | SLTNode::Concat(..) => 1,
4251 }
4252 };
4253 if !excluded {
4254 for child in Self::node_children(node, arena) {
4255 cost = cost.saturating_add(self.owned_slice_lower_cost(child, arena));
4256 }
4257 }
4258 self.cost_cache.borrow_mut().owned_slice_lower_costs[node.0] = Some(cost);
4259 cost
4260 }
4261
4262 fn contains_shared_nontrivial<A: Hash + Eq + Clone>(
4263 &self,
4264 node: NodeId,
4265 arena: &SLTNodeArena<A>,
4266 ) -> bool {
4267 self.prepare_cost_cache(arena);
4268 if let Some(result) = self.cost_cache.borrow().contains_shared_nontrivial[node.0] {
4269 return result;
4270 }
4271 self.note_analysis_visits(1);
4272 let (materialized, fanout) = {
4273 let cache = self.cost_cache.borrow();
4274 (cache.initially_materialized[node.0], cache.fanout[node.0])
4275 };
4276 let result = !materialized
4277 && ((fanout > 1 && Self::is_nontrivial_node(node, arena))
4278 || Self::node_children(node, arena)
4279 .into_iter()
4280 .any(|child| self.contains_shared_nontrivial(child, arena)));
4281 self.cost_cache.borrow_mut().contains_shared_nontrivial[node.0] = Some(result);
4282 result
4283 }
4284
4285 fn direct_shared_candidates<A: Hash + Eq + Clone>(
4286 &self,
4287 node: NodeId,
4288 arena: &SLTNodeArena<A>,
4289 materialized: &crate::HashMap<NodeId, RegisterId>,
4290 ) -> crate::HashSet<NodeId> {
4291 let candidates = std::iter::once(node)
4292 .chain(Self::node_children(node, arena))
4293 .collect::<Vec<_>>();
4294 self.note_analysis_visits(candidates.len());
4295 candidates
4296 .into_iter()
4297 .filter(|candidate| {
4298 !materialized.contains_key(candidate)
4299 && self.cost_cache.borrow().fanout[candidate.0] > 1
4300 && Self::is_nontrivial_node(*candidate, arena)
4301 })
4302 .collect()
4303 }
4304
4305 fn arm_has_only_direct_shared<A: Hash + Eq + Clone>(
4306 &self,
4307 node: NodeId,
4308 arena: &SLTNodeArena<A>,
4309 materialized: &crate::HashMap<NodeId, RegisterId>,
4310 allowed_shared: &crate::HashSet<NodeId>,
4311 ) -> bool {
4312 if materialized.contains_key(&node) || allowed_shared.contains(&node) {
4313 return true;
4314 }
4315 let node_is_shared =
4316 self.cost_cache.borrow().fanout[node.0] > 1 && Self::is_nontrivial_node(node, arena);
4317 if node_is_shared {
4318 return false;
4319 }
4320 let children = Self::node_children(node, arena);
4321 self.note_analysis_visits(children.len().max(1));
4322 children.into_iter().all(|child| {
4323 materialized.contains_key(&child)
4324 || allowed_shared.contains(&child)
4325 || !self.contains_shared_nontrivial(child, arena)
4326 })
4327 }
4328
4329 fn shared_mux_nodes<A: Hash + Eq + Clone>(
4334 &self,
4335 then_expr: NodeId,
4336 else_expr: NodeId,
4337 arena: &SLTNodeArena<A>,
4338 materialized: &crate::HashMap<NodeId, RegisterId>,
4339 ) -> Option<Vec<NodeId>> {
4340 let then_candidates = self.direct_shared_candidates(then_expr, arena, materialized);
4341 let else_candidates = self.direct_shared_candidates(else_expr, arena, materialized);
4342 let shared = then_candidates
4343 .intersection(&else_candidates)
4344 .copied()
4345 .collect::<crate::HashSet<_>>();
4346 if !self.arm_has_only_direct_shared(then_expr, arena, materialized, &shared)
4347 || !self.arm_has_only_direct_shared(else_expr, arena, materialized, &shared)
4348 {
4349 return None;
4350 }
4351 let mut shared = shared.into_iter().collect::<Vec<_>>();
4352 shared.sort_unstable_by_key(|node| std::cmp::Reverse(node.0));
4353 Some(shared)
4354 }
4355
4356 fn is_speculatable_pure<A: Hash + Eq + Clone>(
4357 &self,
4358 node: NodeId,
4359 arena: &SLTNodeArena<A>,
4360 ) -> bool {
4361 self.prepare_cost_cache(arena);
4362 if let Some(result) = self.cost_cache.borrow().is_speculatable_pure[node.0] {
4363 return result;
4364 }
4365 self.note_analysis_visits(1);
4366 let result = !matches!(
4371 arena.get(node),
4372 SLTNode::ForFold { .. } | SLTNode::ForFoldGroup { .. }
4373 ) && Self::node_children(node, arena)
4374 .into_iter()
4375 .all(|child| self.is_speculatable_pure(child, arena));
4376 self.cost_cache.borrow_mut().is_speculatable_pure[node.0] = Some(result);
4377 result
4378 }
4379
4380 fn fold_node_is_invariant<A: Hash + Eq + Clone>(
4381 node: NodeId,
4382 rebound_variables: &crate::HashSet<&A>,
4383 arena: &SLTNodeArena<A>,
4384 memo: &mut crate::HashMap<NodeId, bool>,
4385 ) -> bool {
4386 if let Some(&invariant) = memo.get(&node) {
4387 return invariant;
4388 }
4389 let invariant = match arena.get(node) {
4390 SLTNode::Input {
4391 variable, index, ..
4392 } => {
4393 !rebound_variables.contains(variable)
4394 && index.iter().all(|entry| {
4395 Self::fold_node_is_invariant(entry.node, rebound_variables, arena, memo)
4396 })
4397 }
4398 SLTNode::Constant(..) => true,
4399 SLTNode::Binary(lhs, _, rhs) => {
4400 Self::fold_node_is_invariant(*lhs, rebound_variables, arena, memo)
4401 && Self::fold_node_is_invariant(*rhs, rebound_variables, arena, memo)
4402 }
4403 SLTNode::Unary(_, inner)
4404 | SLTNode::Capture { expr: inner, .. }
4405 | SLTNode::Slice { expr: inner, .. } => {
4406 Self::fold_node_is_invariant(*inner, rebound_variables, arena, memo)
4407 }
4408 SLTNode::Mux {
4409 cond,
4410 then_expr,
4411 else_expr,
4412 } => {
4413 Self::fold_node_is_invariant(*cond, rebound_variables, arena, memo)
4414 && Self::fold_node_is_invariant(*then_expr, rebound_variables, arena, memo)
4415 && Self::fold_node_is_invariant(*else_expr, rebound_variables, arena, memo)
4416 }
4417 SLTNode::Concat(parts) => parts.iter().all(|(part, _)| {
4418 Self::fold_node_is_invariant(*part, rebound_variables, arena, memo)
4419 }),
4420 SLTNode::ForFold { .. } | SLTNode::ForFoldGroup { .. } => false,
4421 };
4422 memo.insert(node, invariant);
4423 invariant
4424 }
4425
4426 fn fold_capture_is_total<A: Hash + Eq + Clone>(
4427 node: NodeId,
4428 arena: &SLTNodeArena<A>,
4429 memo: &mut crate::HashMap<NodeId, bool>,
4430 ) -> bool {
4431 if let Some(&total) = memo.get(&node) {
4432 return total;
4433 }
4434 let total = match arena.get(node) {
4435 SLTNode::Binary(
4436 _,
4437 BinaryOp::DivU | BinaryOp::DivS | BinaryOp::RemU | BinaryOp::RemS,
4438 _,
4439 )
4440 | SLTNode::ForFold { .. }
4441 | SLTNode::ForFoldGroup { .. } => false,
4442 _ => Self::node_children(node, arena)
4443 .into_iter()
4444 .all(|child| Self::fold_capture_is_total(child, arena, memo)),
4445 };
4446 memo.insert(node, total);
4447 total
4448 }
4449
4450 fn fold_invariant_capture_frontier<A: Hash + Eq + Clone>(
4451 specs: &[FoldGroupLowerSpec<'_, A>],
4452 arena: &SLTNodeArena<A>,
4453 ) -> Vec<NodeId> {
4454 let mut rebound_variables = crate::HashSet::default();
4455 for spec in specs {
4456 rebound_variables.insert(spec.loop_var);
4457 rebound_variables.extend(spec.states.iter().map(|state| &state.target.id));
4458 }
4459 let mut invariant_memo = crate::HashMap::default();
4460 let mut total_memo = crate::HashMap::default();
4461 let mut captures = crate::HashSet::default();
4462 let mut pending = specs
4463 .iter()
4464 .flat_map(|spec| spec.states.iter().map(|state| state.update))
4465 .collect::<Vec<_>>();
4466 let mut visited = crate::HashSet::default();
4467 while let Some(node) = pending.pop() {
4468 if !visited.insert(node) {
4469 continue;
4470 }
4471 let invariant =
4472 Self::fold_node_is_invariant(node, &rebound_variables, arena, &mut invariant_memo);
4473 if invariant
4474 && Self::fold_capture_is_total(node, arena, &mut total_memo)
4475 && !matches!(arena.get(node), SLTNode::Constant(..))
4476 {
4477 captures.insert(node);
4478 continue;
4479 }
4480 pending.extend(Self::node_children(node, arena));
4481 }
4482 let mut captures = captures.into_iter().collect::<Vec<_>>();
4483 captures.sort_unstable();
4484 captures
4485 }
4486
4487 fn contains_div_rem<A: Hash + Eq + Clone + std::fmt::Debug>(
4488 &self,
4489 node: NodeId,
4490 arena: &SLTNodeArena<A>,
4491 ) -> bool {
4492 self.prepare_cost_cache(arena);
4493 if let Some(result) = self.cost_cache.borrow().contains_div_rem[node.0] {
4494 return result;
4495 }
4496 self.note_analysis_visits(1);
4497 let excluded = {
4498 let cache = self.cost_cache.borrow();
4499 cache.initially_materialized[node.0] || cache.fanout[node.0] > 1
4500 };
4501 let result = !excluded
4502 && (matches!(
4503 arena.get(node),
4504 SLTNode::Binary(
4505 _,
4506 BinaryOp::DivU | BinaryOp::DivS | BinaryOp::RemU | BinaryOp::RemS,
4507 _,
4508 )
4509 ) || Self::node_children(node, arena)
4510 .into_iter()
4511 .any(|child| self.contains_div_rem(child, arena)));
4512 self.cost_cache.borrow_mut().contains_div_rem[node.0] = Some(result);
4513 result
4514 }
4515
4516 fn static_true_probability<A: Hash + Eq + Clone>(
4517 cond: NodeId,
4518 arena: &SLTNodeArena<A>,
4519 ) -> StaticBranchProbability {
4520 match arena.get(cond) {
4521 SLTNode::Unary(UnaryOp::LogicNot, inner) => {
4522 Self::static_true_probability(*inner, arena).inverted()
4523 }
4524 SLTNode::Unary(UnaryOp::Ident, inner) => Self::static_true_probability(*inner, arena),
4525 SLTNode::Binary(
4526 lhs,
4527 op @ (BinaryOp::Eq | BinaryOp::Ne | BinaryOp::EqWildcard | BinaryOp::NeWildcard),
4528 rhs,
4529 ) if try_const_eval(*lhs, arena).is_some() || try_const_eval(*rhs, arena).is_some() => {
4530 let equality = StaticBranchProbability {
4537 true_weight: 1,
4538 total_weight: 5,
4539 };
4540 if matches!(*op, BinaryOp::Eq | BinaryOp::EqWildcard) {
4541 equality
4542 } else {
4543 equality.inverted()
4544 }
4545 }
4546 _ => StaticBranchProbability::EVEN,
4547 }
4548 }
4549
4550 fn guarded_true_probability<A: Hash + Eq + Clone>(
4551 guard: NodeId,
4552 arena: &SLTNodeArena<A>,
4553 ) -> StaticBranchProbability {
4554 let mut probability = StaticBranchProbability {
4555 true_weight: 1,
4556 total_weight: 1,
4557 };
4558 let mut visited = crate::HashSet::default();
4559 let mut work = vec![guard];
4560 while let Some(node) = work.pop() {
4561 if !visited.insert(node) {
4562 continue;
4563 }
4564 match arena.get(node) {
4565 SLTNode::Binary(lhs, BinaryOp::LogicAnd, rhs) => {
4566 work.extend([*lhs, *rhs]);
4567 }
4568 SLTNode::Unary(UnaryOp::Ident, inner) => work.push(*inner),
4569 _ => {
4570 probability =
4571 probability.conjunction(Self::static_true_probability(node, arena));
4572 }
4573 }
4574 }
4575 probability
4576 }
4577
4578 fn mux_cfg_is_profitable(
4579 then_cost: u128,
4580 else_cost: u128,
4581 result_width: usize,
4582 probability: StaticBranchProbability,
4583 ) -> bool {
4584 Self::mux_cfg_is_profitable_with_extra_cost(
4585 then_cost,
4586 else_cost,
4587 result_width,
4588 probability,
4589 0,
4590 )
4591 }
4592
4593 fn mux_cfg_is_profitable_with_extra_cost(
4594 then_cost: u128,
4595 else_cost: u128,
4596 result_width: usize,
4597 probability: StaticBranchProbability,
4598 extra_always_executed_cost: u128,
4599 ) -> bool {
4600 const CONTROL_COST: u128 = 3;
4606 const MISPREDICT_COST: u128 = 16;
4607 const PHI_COPY_COST_PER_CHUNK: u128 = 2;
4608
4609 let false_weight = probability.total_weight - probability.true_weight;
4610 let select_cost = Self::chunks(result_width);
4611 let skipped_cost = false_weight
4612 .saturating_mul(then_cost)
4613 .saturating_add(probability.true_weight.saturating_mul(else_cost))
4614 .saturating_add(probability.total_weight.saturating_mul(select_cost));
4615 let predictable_misses = probability.true_weight.min(false_weight);
4616 let introduced_cost = probability
4617 .total_weight
4618 .saturating_mul(
4619 CONTROL_COST
4620 .saturating_add(
4621 PHI_COPY_COST_PER_CHUNK.saturating_mul(Self::chunks(result_width)),
4622 )
4623 .saturating_add(extra_always_executed_cost),
4624 )
4625 .saturating_add(predictable_misses.saturating_mul(MISPREDICT_COST));
4626 skipped_cost > introduced_cost
4627 }
4628
4629 fn mux_cfg_plan<A: Hash + Eq + Clone + std::fmt::Debug>(
4630 &self,
4631 cond: NodeId,
4632 then_expr: NodeId,
4633 else_expr: NodeId,
4634 result_width: usize,
4635 arena: &SLTNodeArena<A>,
4636 materialized: &crate::HashMap<NodeId, RegisterId>,
4637 allow_cache: bool,
4638 ) -> Option<MuxCfgPlan> {
4639 let empty_materialized = crate::HashMap::default();
4640 let materialized = if allow_cache {
4641 materialized
4642 } else {
4643 &empty_materialized
4644 };
4645 if self.four_state {
4649 self.with_mux_stats(|stats| stats.kept_four_state += 1);
4650 return None;
4651 }
4652 if !self.is_speculatable_pure(then_expr, arena)
4653 || !self.is_speculatable_pure(else_expr, arena)
4654 {
4655 self.with_mux_stats(|stats| stats.kept_impure += 1);
4656 return None;
4657 }
4658 let forced =
4659 self.contains_div_rem(then_expr, arena) || self.contains_div_rem(else_expr, arena);
4660 if !forced && !allow_cache {
4661 self.with_mux_stats(|stats| stats.kept_dynamic_env += 1);
4662 return None;
4663 }
4664
4665 let probability = Self::static_true_probability(cond, arena);
4666 let then_cost = self.owned_tree_cost(then_expr, arena);
4667 let else_cost = self.owned_tree_cost(else_expr, arena);
4668 self.with_mux_stats(|stats| {
4669 stats.record_cost(then_cost, else_cost);
4670 stats.biased_conditions += usize::from(probability != StaticBranchProbability::EVEN);
4671 });
4672 if !forced && !Self::mux_cfg_is_profitable(then_cost, else_cost, result_width, probability)
4673 {
4674 self.with_mux_stats(|stats| stats.record_unprofitable(then_cost, else_cost));
4675 return None;
4676 }
4677
4678 let shared_nodes = match self.shared_mux_nodes(then_expr, else_expr, arena, materialized) {
4679 Some(shared) => shared,
4680 None if forced => Vec::new(),
4681 None => {
4682 self.with_mux_stats(|stats| stats.kept_deep_shared += 1);
4683 return None;
4684 }
4685 };
4686 self.with_mux_stats(|stats| {
4687 if forced {
4688 stats.cfg_div_rem += 1;
4689 } else {
4690 stats.cfg_cost += 1;
4691 }
4692 });
4693 Some(MuxCfgPlan { shared_nodes })
4694 }
4695
4696 fn mux_slice_cfg_plan<A: Hash + Eq + Clone + std::fmt::Debug>(
4697 &self,
4698 cond: NodeId,
4699 then_expr: NodeId,
4700 else_expr: NodeId,
4701 access: &BitAccess,
4702 arena: &SLTNodeArena<A>,
4703 materialized: &crate::HashMap<NodeId, RegisterId>,
4704 allow_cache: bool,
4705 ) -> Option<MuxCfgPlan> {
4706 if self.four_state {
4707 self.with_mux_stats(|stats| stats.kept_four_state += 1);
4708 return None;
4709 }
4710 if !self.is_speculatable_pure(then_expr, arena)
4711 || !self.is_speculatable_pure(else_expr, arena)
4712 {
4713 self.with_mux_stats(|stats| stats.kept_impure += 1);
4714 return None;
4715 }
4716 let empty_materialized = crate::HashMap::default();
4717 let materialized = if allow_cache {
4718 materialized
4719 } else {
4720 &empty_materialized
4721 };
4722 let forced =
4723 self.contains_div_rem(then_expr, arena) || self.contains_div_rem(else_expr, arena);
4724 let shared_nodes = match self.shared_mux_nodes(then_expr, else_expr, arena, materialized) {
4725 Some(shared) => shared,
4726 None if forced => Vec::new(),
4727 None => {
4728 self.with_mux_stats(|stats| stats.kept_deep_shared += 1);
4729 return None;
4730 }
4731 };
4732 if !forced {
4733 let then_cost = self.owned_slice_lower_cost(then_expr, arena);
4734 let else_cost = self.owned_slice_lower_cost(else_expr, arena);
4735 let probability = Self::static_true_probability(cond, arena);
4736 self.with_mux_stats(|stats| {
4737 stats.record_cost(then_cost, else_cost);
4738 stats.biased_conditions +=
4739 usize::from(probability != StaticBranchProbability::EVEN);
4740 });
4741 let shared_hoist_cost = shared_nodes
4746 .iter()
4747 .map(|node| self.estimated_tree_cost(*node, arena))
4748 .fold(0u128, u128::saturating_add);
4749 if !Self::mux_cfg_is_profitable_with_extra_cost(
4750 then_cost,
4751 else_cost,
4752 access.msb - access.lsb + 1,
4753 probability,
4754 shared_hoist_cost,
4755 ) {
4756 self.with_mux_stats(|stats| stats.record_unprofitable(then_cost, else_cost));
4757 return None;
4758 }
4759 }
4760 self.with_mux_stats(|stats| {
4761 if forced {
4762 stats.cfg_slice_div_rem += 1;
4763 } else {
4764 stats.cfg_slice_cost += 1;
4765 }
4766 });
4767 Some(MuxCfgPlan { shared_nodes })
4768 }
4769
4770 fn constant_condition<A: Hash + Eq + Clone>(
4771 cond: NodeId,
4772 arena: &SLTNodeArena<A>,
4773 ) -> Option<bool> {
4774 let (value, mask) = try_const_eval(cond, arena)?;
4775 (mask == BigUint::from(0u8)).then(|| value != BigUint::from(0u8))
4776 }
4777
4778 fn hoist_shared_mux_nodes<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
4779 &self,
4780 builder: &mut SIRBuilder<A>,
4781 plan: &MuxCfgPlan,
4782 arena: &SLTNodeArena<A>,
4783 cache: &mut crate::HashMap<NodeId, RegisterId>,
4784 env: Option<&LowerEnv<'_, A>>,
4785 allow_cache: bool,
4786 ) {
4787 if !allow_cache {
4788 return;
4789 }
4790 self.with_mux_stats(|stats| stats.shared_nodes_hoisted += plan.shared_nodes.len());
4791 for &node in &plan.shared_nodes {
4792 self.lower_inner(builder, node, arena, cache, env, true);
4793 }
4794 }
4795
4796 fn lower_mux_inner<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
4804 &self,
4805 builder: &mut SIRBuilder<A>,
4806 cond: NodeId,
4807 then_expr: NodeId,
4808 else_expr: NodeId,
4809 arena: &SLTNodeArena<A>,
4810 cache: &mut crate::HashMap<NodeId, RegisterId>,
4811 env: Option<&LowerEnv<'_, A>>,
4812 allow_cache: bool,
4813 ) -> RegisterId {
4814 self.with_mux_stats(|stats| stats.normal_seen += 1);
4815 let then_width = self.get_width(then_expr, arena);
4816 let else_width = self.get_width(else_expr, arena);
4817 let res_width = then_width.max(else_width);
4818
4819 if let Some(take_then) = Self::constant_condition(cond, arena) {
4820 self.with_mux_stats(|stats| stats.constant_folded += 1);
4821 let selected = if take_then { then_expr } else { else_expr };
4822 let value = self.lower_inner(builder, selected, arena, cache, env, allow_cache);
4823 return self.cast_reg_width(builder, value, res_width);
4824 }
4825
4826 let cond_reg = self.lower_inner(builder, cond, arena, cache, env, allow_cache);
4827 if let Some(plan) = self.mux_cfg_plan(
4828 cond,
4829 then_expr,
4830 else_expr,
4831 res_width,
4832 arena,
4833 cache,
4834 allow_cache,
4835 ) {
4836 self.hoist_shared_mux_nodes(builder, &plan, arena, cache, env, allow_cache);
4837 return self.lower_mux_cfg(
4838 builder,
4839 cond_reg,
4840 then_expr,
4841 else_expr,
4842 res_width,
4843 arena,
4844 cache,
4845 env,
4846 allow_cache,
4847 );
4848 }
4849
4850 let then_val = self.lower_inner(builder, then_expr, arena, cache, env, allow_cache);
4851 let else_val = self.lower_inner(builder, else_expr, arena, cache, env, allow_cache);
4852
4853 let result = builder.alloc_logic(res_width);
4856 builder.emit(SIRInstruction::Mux(result, cond_reg, then_val, else_val));
4857
4858 result
4859 }
4860
4861 #[allow(clippy::too_many_arguments)]
4862 fn lower_mux_cfg<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
4863 &self,
4864 builder: &mut SIRBuilder<A>,
4865 cond_reg: RegisterId,
4866 then_expr: NodeId,
4867 else_expr: NodeId,
4868 result_width: usize,
4869 arena: &SLTNodeArena<A>,
4870 cache: &mut crate::HashMap<NodeId, RegisterId>,
4871 env: Option<&LowerEnv<'_, A>>,
4872 allow_cache: bool,
4873 ) -> RegisterId {
4874 let result = builder.alloc_logic(result_width);
4875 let then_block = builder.new_block();
4876 let else_block = builder.new_block();
4877 let merge_block = builder.new_block_with(vec![result]);
4878
4879 builder.seal_block(SIRTerminator::Branch {
4880 cond: cond_reg,
4881 true_block: (then_block, vec![]),
4882 false_block: (else_block, vec![]),
4883 });
4884
4885 let then_transaction = self.cache_transaction();
4886 builder.switch_to_block(then_block);
4887 let then_val = self.lower_inner(builder, then_expr, arena, cache, env, allow_cache);
4888 let then_val = self.cast_reg_width(builder, then_val, result_width);
4889 builder.seal_block(SIRTerminator::Jump(merge_block, vec![then_val]));
4890 if allow_cache {
4891 self.rollback_cache(cache, then_transaction);
4892 }
4893
4894 let else_transaction = self.cache_transaction();
4895 builder.switch_to_block(else_block);
4896 let else_val = self.lower_inner(builder, else_expr, arena, cache, env, allow_cache);
4897 let else_val = self.cast_reg_width(builder, else_val, result_width);
4898 builder.seal_block(SIRTerminator::Jump(merge_block, vec![else_val]));
4899 if allow_cache {
4900 self.rollback_cache(cache, else_transaction);
4901 }
4902
4903 builder.switch_to_block(merge_block);
4904 result
4905 }
4906
4907 fn lower_region_slice_mux_inner<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
4908 &self,
4909 builder: &mut SIRBuilder<A>,
4910 cond: NodeId,
4911 then_expr: NodeId,
4912 else_expr: NodeId,
4913 access: &BitAccess,
4914 arena: &SLTNodeArena<A>,
4915 cache: &mut crate::HashMap<NodeId, RegisterId>,
4916 env: Option<&LowerEnv<'_, A>>,
4917 allow_cache: bool,
4918 ) -> RegisterId {
4919 self.with_mux_stats(|stats| stats.slice_seen += 1);
4920 let result_width = access.msb - access.lsb + 1;
4921 if let Some(take_then) = Self::constant_condition(cond, arena) {
4922 self.with_mux_stats(|stats| stats.constant_folded += 1);
4923 return self.lower_region_slice_inner(
4924 builder,
4925 if take_then { then_expr } else { else_expr },
4926 access,
4927 arena,
4928 cache,
4929 env,
4930 allow_cache,
4931 );
4932 }
4933
4934 let cond_reg = self.lower_inner(builder, cond, arena, cache, env, allow_cache);
4935 if let Some(plan) = self.mux_slice_cfg_plan(
4936 cond,
4937 then_expr,
4938 else_expr,
4939 access,
4940 arena,
4941 cache,
4942 allow_cache,
4943 ) {
4944 self.hoist_shared_mux_nodes(builder, &plan, arena, cache, env, allow_cache);
4945 let result = builder.alloc_logic(result_width);
4946 let then_block = builder.new_block();
4947 let else_block = builder.new_block();
4948 let merge_block = builder.new_block_with(vec![result]);
4949
4950 builder.seal_block(SIRTerminator::Branch {
4951 cond: cond_reg,
4952 true_block: (then_block, vec![]),
4953 false_block: (else_block, vec![]),
4954 });
4955
4956 let then_transaction = self.cache_transaction();
4957 builder.switch_to_block(then_block);
4958 let then_value = self.lower_region_slice_inner(
4959 builder,
4960 then_expr,
4961 access,
4962 arena,
4963 cache,
4964 env,
4965 allow_cache,
4966 );
4967 builder.seal_block(SIRTerminator::Jump(merge_block, vec![then_value]));
4968 if allow_cache {
4969 self.rollback_cache(cache, then_transaction);
4970 }
4971
4972 let else_transaction = self.cache_transaction();
4973 builder.switch_to_block(else_block);
4974 let else_value = self.lower_region_slice_inner(
4975 builder,
4976 else_expr,
4977 access,
4978 arena,
4979 cache,
4980 env,
4981 allow_cache,
4982 );
4983 builder.seal_block(SIRTerminator::Jump(merge_block, vec![else_value]));
4984 if allow_cache {
4985 self.rollback_cache(cache, else_transaction);
4986 }
4987
4988 builder.switch_to_block(merge_block);
4989 return result;
4990 }
4991
4992 let then_value = self.lower_region_slice_inner(
4993 builder,
4994 then_expr,
4995 access,
4996 arena,
4997 cache,
4998 env,
4999 allow_cache,
5000 );
5001 let else_value = self.lower_region_slice_inner(
5002 builder,
5003 else_expr,
5004 access,
5005 arena,
5006 cache,
5007 env,
5008 allow_cache,
5009 );
5010 let result = builder.alloc_logic(result_width);
5011 builder.emit(SIRInstruction::Mux(
5012 result, cond_reg, then_value, else_value,
5013 ));
5014 result
5015 }
5016
5017 fn slice_reg<A>(
5018 &self,
5019 builder: &mut SIRBuilder<A>,
5020 reg: RegisterId,
5021 access: &BitAccess,
5022 ) -> RegisterId {
5023 let width = access.msb - access.lsb + 1;
5024 let shift_amt = builder.alloc_bit(64, false);
5025 builder.emit(SIRInstruction::Imm(
5026 shift_amt,
5027 SIRValue::new(access.lsb as u64),
5028 ));
5029
5030 let shifted = builder.alloc_logic(width);
5031 builder.emit(SIRInstruction::Binary(
5032 shifted,
5033 reg,
5034 BinaryOp::Shr,
5035 shift_amt,
5036 ));
5037
5038 let mask_val = (BigUint::from(1u64) << width) - BigUint::from(1u64);
5039 let mask_reg = builder.alloc_bit(width, false);
5040 builder.emit(SIRInstruction::Imm(mask_reg, SIRValue::new(mask_val)));
5041
5042 let dest = builder.alloc_logic(width);
5043 builder.emit(SIRInstruction::Binary(
5044 dest,
5045 shifted,
5046 BinaryOp::And,
5047 mask_reg,
5048 ));
5049 dest
5050 }
5051
5052 fn cast_reg_width<A>(
5053 &self,
5054 builder: &mut SIRBuilder<A>,
5055 reg: RegisterId,
5056 width: usize,
5057 ) -> RegisterId {
5058 self.cast_reg_width_ext(builder, reg, width, false)
5059 }
5060
5061 fn cast_reg_width_ext<A>(
5062 &self,
5063 builder: &mut SIRBuilder<A>,
5064 reg: RegisterId,
5065 width: usize,
5066 signed: bool,
5067 ) -> RegisterId {
5068 let source_type = builder.register(®).clone();
5069 let current_width = source_type.width();
5070 let alloc_like_source = |builder: &mut SIRBuilder<A>, width, signed| match &source_type {
5071 RegisterType::Logic { .. } => builder.alloc_logic(width),
5072 RegisterType::Bit { .. } => builder.alloc_bit(width, signed),
5073 };
5074 if current_width == width {
5075 return reg;
5076 }
5077 if current_width < width {
5078 let pad_width = width - current_width;
5079 let pad = if signed {
5080 let sign = self.slice_reg(
5081 builder,
5082 reg,
5083 &BitAccess::new(current_width - 1, current_width - 1),
5084 );
5085 if pad_width == 1 {
5086 sign
5087 } else {
5088 let ext = alloc_like_source(builder, pad_width, true);
5089 builder.emit(SIRInstruction::Concat(
5090 ext,
5091 std::iter::repeat_n(sign, pad_width).collect(),
5092 ));
5093 ext
5094 }
5095 } else {
5096 let zero = builder.alloc_bit(pad_width, false);
5097 builder.emit(SIRInstruction::Imm(zero, SIRValue::new(0u64)));
5098 zero
5099 };
5100 let dest = alloc_like_source(builder, width, signed);
5101 builder.emit(SIRInstruction::Concat(dest, vec![pad, reg]));
5102 return dest;
5103 }
5104
5105 let mask_val = (BigUint::from(1u64) << width) - BigUint::from(1u64);
5106 let mask_reg = builder.alloc_bit(current_width, false);
5107 builder.emit(SIRInstruction::Imm(mask_reg, SIRValue::new(mask_val)));
5108 let masked = alloc_like_source(builder, current_width, signed);
5109 builder.emit(SIRInstruction::Binary(masked, reg, BinaryOp::And, mask_reg));
5110 let sliced = self.slice_reg(builder, masked, &BitAccess::new(0, width - 1));
5111 let dest = alloc_like_source(builder, width, signed);
5112 builder.emit(SIRInstruction::Unary(dest, UnaryOp::Ident, sliced));
5113 dest
5114 }
5115
5116 fn lower_bound<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
5117 &self,
5118 builder: &mut SIRBuilder<A>,
5119 bound: &SLTLoopBound,
5120 _canonical_width: usize,
5121 width: usize,
5122 signed: bool,
5123 arena: &SLTNodeArena<A>,
5124 cache: &mut crate::HashMap<NodeId, RegisterId>,
5125 env: Option<&LowerEnv<'_, A>>,
5126 ) -> RegisterId {
5127 match bound {
5128 SLTLoopBound::Const(v) => {
5129 let reg = builder.alloc_bit(width, signed);
5130 builder.emit(SIRInstruction::Imm(reg, SIRValue::new(*v as u64)));
5131 reg
5132 }
5133 SLTLoopBound::Expr(node) => {
5134 let reg = self.lower_inner(builder, *node, arena, cache, env, env.is_none());
5135 let source_signed = self.get_bound_signed(*node, arena);
5136 let extend_signed = source_signed && signed;
5137 let sized = self.cast_reg_width_ext(builder, reg, width, extend_signed);
5138 if extend_signed == signed {
5139 sized
5140 } else {
5141 let dest = builder.alloc_bit(width, signed);
5142 builder.emit(SIRInstruction::Unary(dest, UnaryOp::Ident, sized));
5143 dest
5144 }
5145 }
5146 }
5147 }
5148
5149 fn bound_width(bound: &SLTLoopBound) -> usize {
5150 match bound {
5151 SLTLoopBound::Const(v) => {
5152 let bits = usize::BITS as usize - v.leading_zeros() as usize;
5153 bits.max(1)
5154 }
5155 SLTLoopBound::Expr(_) => 0,
5156 }
5157 }
5158
5159 fn step_math_width(base_width: usize, step_op: SLTStepOp, step: usize) -> usize {
5160 match step_op {
5161 SLTStepOp::Add => {
5162 let step_bits = (usize::BITS as usize - step.leading_zeros() as usize).max(1);
5163 base_width.saturating_add(step_bits)
5164 }
5165 SLTStepOp::Mul => {
5166 let step_bits = (usize::BITS as usize - step.leading_zeros() as usize).max(1);
5167 base_width.saturating_add(step_bits)
5168 }
5169 SLTStepOp::Shl => base_width.saturating_add(step.max(1)),
5170 SLTStepOp::BitOr | SLTStepOp::BitXor => base_width,
5171 }
5172 }
5173
5174 fn truncate_usize_to_width(value: usize, width: usize) -> usize {
5175 if width >= usize::BITS as usize {
5176 value
5177 } else if width == 0 {
5178 0
5179 } else {
5180 value & ((1usize << width) - 1)
5181 }
5182 }
5183
5184 fn bigint_payload(value: &BigInt, width: usize) -> BigUint {
5185 let modulus = BigInt::from(1u8) << width;
5186 let mut wrapped = value % &modulus;
5187 if wrapped < BigInt::from(0u8) {
5188 wrapped += modulus;
5189 }
5190 wrapped
5191 .to_biguint()
5192 .expect("a modulo-reduced loop value must be non-negative")
5193 }
5194
5195 fn pack_fold_group_states<A>(
5196 &self,
5197 builder: &mut SIRBuilder<A>,
5198 states: &[RegisterId],
5199 ) -> RegisterId {
5200 debug_assert!(!states.is_empty());
5201 let width = states
5202 .iter()
5203 .map(|state| builder.register(state).width())
5204 .sum();
5205 let packed = builder.alloc_logic(width);
5206 builder.emit(SIRInstruction::Concat(packed, states.to_vec()));
5207 packed
5208 }
5209
5210 fn lower_or_scan_plan<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
5211 &self,
5212 builder: &mut SIRBuilder<A>,
5213 arena: &SLTNodeArena<A>,
5214 cache: &mut crate::HashMap<NodeId, RegisterId>,
5215 spec: &FoldGroupLowerSpec<'_, A>,
5216 plan: SLTOrScanPlan<A>,
5217 allow_cache: bool,
5218 ) -> RegisterId {
5219 debug_assert!(!self.four_state);
5220 let initial_states = spec
5221 .states
5222 .iter()
5223 .map(|state| {
5224 let initial =
5225 self.lower_inner(builder, state.initial, arena, cache, None, allow_cache);
5226 self.cast_reg_width(
5227 builder,
5228 initial,
5229 state.target.access.msb - state.target.access.lsb + 1,
5230 )
5231 })
5232 .collect::<Vec<_>>();
5233 let guard = self.lower_inner(builder, spec.entry_guard, arena, cache, None, allow_cache);
5234 let active =
5235 self.lower_slt_vector_expr(builder, plan.active, plan.width, arena, cache, allow_cache);
5236 let source =
5237 self.lower_slt_vector_expr(builder, plan.source, plan.width, arena, cache, allow_cache);
5238
5239 let hits = builder.alloc_bit(plan.width, false);
5240 builder.emit(SIRInstruction::Binary(hits, active, BinaryOp::And, source));
5241 let zero = builder.alloc_bit(plan.width, false);
5242 builder.emit(SIRInstruction::Imm(zero, SIRValue::new(0u8)));
5243 let negated_hits = builder.alloc_bit(plan.width, false);
5244 builder.emit(SIRInstruction::Binary(
5245 negated_hits,
5246 zero,
5247 BinaryOp::Sub,
5248 hits,
5249 ));
5250 let first = builder.alloc_bit(plan.width, false);
5251 builder.emit(SIRInstruction::Binary(
5252 first,
5253 hits,
5254 BinaryOp::And,
5255 negated_hits,
5256 ));
5257 let one = builder.alloc_bit(plan.width, false);
5258 builder.emit(SIRInstruction::Imm(one, SIRValue::new(1u8)));
5259 let before = builder.alloc_bit(plan.width, false);
5260 builder.emit(SIRInstruction::Binary(before, first, BinaryOp::Sub, one));
5261 let through = builder.alloc_bit(plan.width, false);
5264 builder.emit(SIRInstruction::Binary(through, before, BinaryOp::Or, first));
5265
5266 let not_source = builder.alloc_bit(plan.width, false);
5267 builder.emit(SIRInstruction::Unary(not_source, UnaryOp::BitNot, source));
5268 let before_bits = builder.alloc_bit(plan.width, false);
5269 builder.emit(SIRInstruction::Binary(
5270 before_bits,
5271 through,
5272 BinaryOp::And,
5273 not_source,
5274 ));
5275 let first_bits = builder.alloc_bit(plan.width, false);
5276 builder.emit(SIRInstruction::Binary(
5277 first_bits,
5278 through,
5279 BinaryOp::And,
5280 source,
5281 ));
5282 let select_first =
5283 self.lower_inner(builder, plan.select_first, arena, cache, None, allow_cache);
5284 let first_or_through = builder.alloc_bit(plan.width, false);
5285 builder.emit(SIRInstruction::Mux(
5286 first_or_through,
5287 select_first,
5288 first_bits,
5289 through,
5290 ));
5291 let select_before =
5292 self.lower_inner(builder, plan.select_before, arena, cache, None, allow_cache);
5293 let selected = builder.alloc_bit(plan.width, false);
5294 builder.emit(SIRInstruction::Mux(
5295 selected,
5296 select_before,
5297 before_bits,
5298 first_or_through,
5299 ));
5300
5301 let not_active = builder.alloc_bit(plan.width, false);
5302 builder.emit(SIRInstruction::Unary(not_active, UnaryOp::BitNot, active));
5303 let preserved = builder.alloc_bit(plan.width, false);
5304 builder.emit(SIRInstruction::Binary(
5305 preserved,
5306 initial_states[plan.vector_state],
5307 BinaryOp::And,
5308 not_active,
5309 ));
5310 let replaced = builder.alloc_bit(plan.width, false);
5311 builder.emit(SIRInstruction::Binary(
5312 replaced,
5313 selected,
5314 BinaryOp::And,
5315 active,
5316 ));
5317 let vector_result = builder.alloc_bit(plan.width, false);
5318 builder.emit(SIRInstruction::Binary(
5319 vector_result,
5320 preserved,
5321 BinaryOp::Or,
5322 replaced,
5323 ));
5324 let found_result = builder.alloc_bit(1, false);
5325 builder.emit(SIRInstruction::Unary(found_result, UnaryOp::Or, hits));
5326
5327 let mut candidates = initial_states.clone();
5328 candidates[plan.vector_state] = vector_result;
5329 candidates[plan.found_state] = found_result;
5330 let final_states = candidates
5331 .into_iter()
5332 .zip(initial_states)
5333 .zip(spec.states)
5334 .map(
5335 |((candidate, initial), state)| match slt_const_u64(spec.entry_guard, arena) {
5336 Some(0) => initial,
5337 Some(_) => candidate,
5338 None => {
5339 let result = builder
5340 .alloc_logic(state.target.access.msb - state.target.access.lsb + 1);
5341 builder.emit(SIRInstruction::Mux(result, guard, candidate, initial));
5342 result
5343 }
5344 },
5345 )
5346 .collect::<Vec<_>>();
5347 self.pack_fold_group_states(builder, &final_states)
5348 }
5349
5350 fn lower_fold_group_specs<'env, A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
5357 &self,
5358 builder: &mut SIRBuilder<A>,
5359 arena: &SLTNodeArena<A>,
5360 cache: &mut crate::HashMap<NodeId, RegisterId>,
5361 specs: &[FoldGroupLowerSpec<'_, A>],
5362 outer_env: Option<&'env LowerEnv<'env, A>>,
5363 allow_cache: bool,
5364 ) -> Vec<RegisterId> {
5365 let first = specs
5366 .first()
5367 .expect("joint fold lowering requires at least one group");
5368 debug_assert!(first.loop_width > 0);
5369 debug_assert!(first.trip_count > 0);
5370 debug_assert!(specs.iter().all(|spec| !spec.states.is_empty()));
5371 debug_assert!(specs.iter().all(|spec| {
5372 spec.loop_width == first.loop_width
5373 && spec.loop_signed == first.loop_signed
5374 && spec.start == first.start
5375 && spec.step == first.step
5376 && spec.trip_count == first.trip_count
5377 && spec.entry_guard == first.entry_guard
5378 }));
5379
5380 if !self.four_state
5381 && outer_env.is_none()
5382 && specs.len() == 1
5383 && let Some(plan) = match_slt_or_scan_plan(first, arena)
5384 {
5385 return vec![self.lower_or_scan_plan(builder, arena, cache, first, plan, allow_cache)];
5386 }
5387
5388 let guard = self.lower_inner(
5389 builder,
5390 first.entry_guard,
5391 arena,
5392 cache,
5393 outer_env,
5394 allow_cache,
5395 );
5396 let mut group_ranges = Vec::with_capacity(specs.len());
5397 let mut state_count = 0usize;
5398 for spec in specs {
5399 let start = state_count;
5400 state_count = state_count
5401 .checked_add(spec.states.len())
5402 .expect("verified joint fold state count must fit usize");
5403 group_ranges.push(start..state_count);
5404 }
5405 let initial_states: Vec<_> = specs
5406 .iter()
5407 .flat_map(|spec| spec.states)
5408 .map(|state| {
5409 let initial =
5410 self.lower_inner(builder, state.initial, arena, cache, outer_env, allow_cache);
5411 self.cast_reg_width(
5412 builder,
5413 initial,
5414 state.target.access.msb - state.target.access.lsb + 1,
5415 )
5416 })
5417 .collect();
5418 let initial_packed = if self.four_state {
5419 group_ranges
5420 .iter()
5421 .map(|range| {
5422 Some(self.pack_fold_group_states(builder, &initial_states[range.clone()]))
5423 })
5424 .collect::<Vec<_>>()
5425 } else {
5426 vec![None; specs.len()]
5427 };
5428 let remaining_width =
5429 (usize::BITS as usize - first.trip_count.leading_zeros() as usize).max(1);
5430 let initial_remaining = builder.alloc_bit(remaining_width, false);
5431 let zero = builder.alloc_bit(remaining_width, false);
5432 let one = builder.alloc_bit(remaining_width, false);
5433 let initial_loop_value = builder.alloc_bit(first.loop_width, first.loop_signed);
5434 let step_value = builder.alloc_bit(first.loop_width, first.loop_signed);
5435 let body_remaining = builder.alloc_bit(remaining_width, false);
5436 let body_loop_value = builder.alloc_bit(first.loop_width, first.loop_signed);
5437 let body_states: Vec<_> = specs
5438 .iter()
5439 .flat_map(|spec| spec.states)
5440 .map(|state| builder.alloc_logic(state.target.access.msb - state.target.access.lsb + 1))
5441 .collect();
5442 let exit_states: Vec<_> = specs
5443 .iter()
5444 .flat_map(|spec| spec.states)
5445 .map(|state| builder.alloc_logic(state.target.access.msb - state.target.access.lsb + 1))
5446 .collect();
5447 let body = builder.new_block_with(
5448 std::iter::once(body_remaining)
5449 .chain(std::iter::once(body_loop_value))
5450 .chain(body_states.iter().copied())
5451 .collect(),
5452 );
5453 let exit = builder.new_block_with(exit_states.clone());
5454
5455 let capture_nodes = Self::fold_invariant_capture_frontier(specs, arena);
5456 let needs_capture_block = !capture_nodes.is_empty()
5457 && (!allow_cache || capture_nodes.iter().any(|node| !cache.contains_key(node)));
5458 let enter = needs_capture_block.then(|| builder.new_block());
5459 let emit_loop_setup = |builder: &mut SIRBuilder<A>| {
5460 builder.emit(SIRInstruction::Imm(
5461 initial_remaining,
5462 SIRValue::new(BigUint::from(first.trip_count)),
5463 ));
5464 builder.emit(SIRInstruction::Imm(zero, SIRValue::new(0u8)));
5465 builder.emit(SIRInstruction::Imm(one, SIRValue::new(1u8)));
5466 builder.emit(SIRInstruction::Imm(
5467 initial_loop_value,
5468 SIRValue::new(Self::bigint_payload(first.start, first.loop_width)),
5469 ));
5470 builder.emit(SIRInstruction::Imm(
5471 step_value,
5472 SIRValue::new(Self::bigint_payload(first.step, first.loop_width)),
5473 ));
5474 };
5475 let initial_body_args = || {
5476 std::iter::once(initial_remaining)
5477 .chain(std::iter::once(initial_loop_value))
5478 .chain(initial_states.iter().copied())
5479 .collect::<Vec<_>>()
5480 };
5481 let mut captured_values = crate::HashMap::default();
5482 if let Some(enter) = enter {
5483 builder.seal_block(SIRTerminator::Branch {
5484 cond: guard,
5485 true_block: (enter, Vec::new()),
5486 false_block: (exit, initial_states.clone()),
5487 });
5488 builder.switch_to_block(enter);
5489 let capture_transaction = self.cache_transaction();
5490 if allow_cache {
5491 for node in capture_nodes {
5492 let value = cache.get(&node).copied().unwrap_or_else(|| {
5493 self.lower_inner(builder, node, arena, cache, outer_env, true)
5494 });
5495 captured_values.insert(node, value);
5496 }
5497 self.rollback_cache(cache, capture_transaction);
5498 } else {
5499 let mut capture_cache = crate::HashMap::default();
5500 for node in capture_nodes {
5501 let value =
5502 self.lower_inner(builder, node, arena, &mut capture_cache, outer_env, true);
5503 captured_values.insert(node, value);
5504 }
5505 self.rollback_cache(&mut capture_cache, capture_transaction);
5506 }
5507 emit_loop_setup(builder);
5508 builder.seal_block(SIRTerminator::Jump(body, initial_body_args()));
5509 } else {
5510 for node in capture_nodes {
5511 let value = *cache
5512 .get(&node)
5513 .expect("a capture without an enter block must already dominate the loop");
5514 captured_values.insert(node, value);
5515 }
5516 emit_loop_setup(builder);
5517 builder.seal_block(SIRTerminator::Branch {
5518 cond: guard,
5519 true_block: (body, initial_body_args()),
5520 false_block: (exit, initial_states.clone()),
5521 });
5522 }
5523
5524 builder.switch_to_block(body);
5525 let mut env_inputs = crate::HashMap::default();
5526 for (state, value) in specs
5527 .iter()
5528 .flat_map(|spec| spec.states)
5529 .zip(body_states.iter().copied())
5530 {
5531 env_inputs.insert(state.target.clone(), value);
5532 }
5533 for spec in specs {
5534 env_inputs.insert(
5535 VarAtomBase::new(spec.loop_var.clone(), 0, first.loop_width - 1),
5536 body_loop_value,
5537 );
5538 }
5539 let env = LowerEnv {
5540 inputs: env_inputs,
5541 parent: outer_env,
5542 };
5543 let mut local_cache = captured_values;
5544 let local_cache_transaction = self.cache_transaction();
5545 let next_states: Vec<_> = specs
5546 .iter()
5547 .flat_map(|spec| spec.states)
5548 .map(|state| {
5549 self.reset_cost_cache(state.update, arena, &local_cache, true);
5557 let next = self.lower_inner(
5558 builder,
5559 state.update,
5560 arena,
5561 &mut local_cache,
5562 Some(&env),
5563 true,
5564 );
5565 self.cast_reg_width(
5566 builder,
5567 next,
5568 state.target.access.msb - state.target.access.lsb + 1,
5569 )
5570 })
5571 .collect();
5572 self.cache_insert_log
5576 .borrow_mut()
5577 .truncate(local_cache_transaction.node_insertions);
5578 self.region_slice_cache_insert_log
5579 .borrow_mut()
5580 .truncate(local_cache_transaction.region_slice_insertions);
5581
5582 let next_remaining = builder.alloc_bit(remaining_width, false);
5583 builder.emit(SIRInstruction::Binary(
5584 next_remaining,
5585 body_remaining,
5586 BinaryOp::Sub,
5587 one,
5588 ));
5589 let has_more = builder.alloc_bit(1, false);
5590 builder.emit(SIRInstruction::Binary(
5591 has_more,
5592 next_remaining,
5593 BinaryOp::Ne,
5594 zero,
5595 ));
5596 let next_loop_value = builder.alloc_bit(first.loop_width, first.loop_signed);
5601 builder.emit(SIRInstruction::Binary(
5602 next_loop_value,
5603 body_loop_value,
5604 BinaryOp::Add,
5605 step_value,
5606 ));
5607 builder.seal_block(SIRTerminator::Branch {
5608 cond: has_more,
5609 true_block: (
5610 body,
5611 std::iter::once(next_remaining)
5612 .chain(std::iter::once(next_loop_value))
5613 .chain(next_states.iter().copied())
5614 .collect(),
5615 ),
5616 false_block: (exit, next_states.clone()),
5617 });
5618
5619 builder.switch_to_block(exit);
5620 let mut results = Vec::with_capacity(specs.len());
5621 for (range, initial) in group_ranges.iter().zip(initial_packed) {
5622 let candidate = self.pack_fold_group_states(builder, &exit_states[range.clone()]);
5623 if let Some(initial) = initial {
5624 let result = builder.alloc_logic(builder.register(&candidate).width());
5625 builder.emit(SIRInstruction::Mux(result, guard, candidate, initial));
5626 results.push(result);
5627 } else {
5628 results.push(candidate);
5629 }
5630 }
5631 results
5632 }
5633
5634 #[allow(clippy::too_many_arguments)]
5635 fn lower_for_fold<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
5636 &self,
5637 builder: &mut SIRBuilder<A>,
5638 arena: &SLTNodeArena<A>,
5639 cache: &mut crate::HashMap<NodeId, RegisterId>,
5640 loop_var: &A,
5641 loop_width: usize,
5642 loop_signed: bool,
5643 start: &SLTLoopBound,
5644 end: &SLTLoopBound,
5645 inclusive: bool,
5646 step: usize,
5647 step_op: SLTStepOp,
5648 reverse: bool,
5649 result: &crate::SLTForFoldResult<A>,
5650 initials: &[crate::SLTForUpdate<A>],
5651 updates: &[crate::SLTForUpdate<A>],
5652 effects: &[crate::SLTForEffect],
5653 continue_cond: NodeId,
5654 parent_env: Option<&LowerEnv<'_, A>>,
5655 ) -> RegisterId {
5656 let mut counter_width = loop_width.max(1);
5657 counter_width = counter_width.max(Self::bound_width(start));
5658 counter_width = counter_width.max(Self::bound_width(end));
5659 if let SLTLoopBound::Expr(node) = start {
5660 counter_width = counter_width.max(self.get_width(*node, arena));
5661 }
5662 if let SLTLoopBound::Expr(node) = end {
5663 counter_width = counter_width.max(self.get_width(*node, arena));
5664 }
5665
5666 let widen_inclusive = inclusive && !loop_signed;
5667 let compare_width = if widen_inclusive {
5668 counter_width + 1
5669 } else {
5670 counter_width
5671 };
5672
5673 let start_reg = self.lower_bound(
5674 builder,
5675 start,
5676 loop_width,
5677 compare_width,
5678 loop_signed,
5679 arena,
5680 cache,
5681 parent_env,
5682 );
5683 let end_reg = self.lower_bound(
5684 builder,
5685 end,
5686 loop_width,
5687 compare_width,
5688 loop_signed,
5689 arena,
5690 cache,
5691 parent_env,
5692 );
5693 let one_reg = builder.alloc_bit(compare_width, loop_signed);
5694 builder.emit(SIRInstruction::Imm(one_reg, SIRValue::new(1u64)));
5695 let end_limit = if widen_inclusive {
5696 let reg = builder.alloc_bit(compare_width, loop_signed);
5697 builder.emit(SIRInstruction::Binary(reg, end_reg, BinaryOp::Add, one_reg));
5698 reg
5699 } else {
5700 end_reg
5701 };
5702
5703 let init_source = if reverse && !inclusive {
5704 let reg = builder.alloc_bit(compare_width, loop_signed);
5705 builder.emit(SIRInstruction::Binary(reg, end_reg, BinaryOp::Sub, one_reg));
5706 reg
5707 } else if reverse {
5708 end_reg
5709 } else {
5710 start_reg
5711 };
5712 let init_visible = self.cast_reg_width_ext(builder, init_source, loop_width, loop_signed);
5715 let init_counter =
5716 self.cast_reg_width_ext(builder, init_visible, compare_width, loop_signed);
5717
5718 let mut initial_states: Vec<RegisterId> = initials
5719 .iter()
5720 .zip(updates.iter())
5721 .map(|(init, update)| {
5722 let reg = self.lower_inner(
5723 builder,
5724 init.expr,
5725 arena,
5726 cache,
5727 parent_env,
5728 parent_env.is_none(),
5729 );
5730 let width = update.target.access.msb - update.target.access.lsb + 1;
5731 self.cast_reg_width(builder, reg, width)
5732 })
5733 .collect();
5734 if let crate::SLTForFoldResult::Transient { initial, update } = result {
5735 let reg = self.lower_inner(
5736 builder,
5737 *initial,
5738 arena,
5739 cache,
5740 parent_env,
5741 parent_env.is_none(),
5742 );
5743 initial_states.push(self.cast_reg_width(builder, reg, self.get_width(*update, arena)));
5744 }
5745
5746 let transient_width = match result {
5747 crate::SLTForFoldResult::State(_) => None,
5748 crate::SLTForFoldResult::Transient { update, .. } => {
5749 Some(self.get_width(*update, arena))
5750 }
5751 };
5752
5753 let header_counter = builder.alloc_bit(compare_width, loop_signed);
5754 let mut header_states: Vec<_> = updates
5755 .iter()
5756 .map(|update| {
5757 let width = update.target.access.msb - update.target.access.lsb + 1;
5758 builder.alloc_logic(width)
5759 })
5760 .collect();
5761 if let Some(width) = transient_width {
5762 header_states.push(builder.alloc_logic(width));
5763 }
5764 let body_counter = builder.alloc_bit(compare_width, loop_signed);
5765 let mut body_states: Vec<_> = updates
5766 .iter()
5767 .map(|update| {
5768 let width = update.target.access.msb - update.target.access.lsb + 1;
5769 builder.alloc_logic(width)
5770 })
5771 .collect();
5772 if let Some(width) = transient_width {
5773 body_states.push(builder.alloc_logic(width));
5774 }
5775 let mut exit_states: Vec<_> = updates
5776 .iter()
5777 .map(|update| {
5778 let width = update.target.access.msb - update.target.access.lsb + 1;
5779 builder.alloc_logic(width)
5780 })
5781 .collect();
5782 if let Some(width) = transient_width {
5783 exit_states.push(builder.alloc_logic(width));
5784 }
5785
5786 let header_params = std::iter::once(header_counter)
5787 .chain(header_states.iter().copied())
5788 .collect();
5789 let body_params = std::iter::once(body_counter)
5790 .chain(body_states.iter().copied())
5791 .collect();
5792 let header_block = builder.new_block_with(header_params);
5793 let body_block = builder.new_block_with(body_params);
5794 let exit_block = builder.new_block_with(exit_states.clone());
5795
5796 builder.seal_block(SIRTerminator::Jump(
5797 header_block,
5798 std::iter::once(init_counter)
5799 .chain(initial_states.iter().copied())
5800 .collect(),
5801 ));
5802
5803 builder.switch_to_block(header_block);
5804 if reverse {
5805 let in_range = builder.alloc_bit(1, false);
5806 builder.emit(SIRInstruction::Binary(
5807 in_range,
5808 header_counter,
5809 if loop_signed {
5810 BinaryOp::GeS
5811 } else {
5812 BinaryOp::GeU
5813 },
5814 start_reg,
5815 ));
5816 builder.seal_block(SIRTerminator::Branch {
5817 cond: in_range,
5818 true_block: (
5819 body_block,
5820 std::iter::once(header_counter)
5821 .chain(header_states.iter().copied())
5822 .collect(),
5823 ),
5824 false_block: (exit_block, header_states.clone()),
5825 });
5826 } else {
5827 let cond = builder.alloc_bit(1, false);
5828 builder.emit(SIRInstruction::Binary(
5829 cond,
5830 header_counter,
5831 if loop_signed {
5832 if inclusive {
5833 BinaryOp::LeS
5834 } else {
5835 BinaryOp::LtS
5836 }
5837 } else {
5838 BinaryOp::LtU
5839 },
5840 end_limit,
5841 ));
5842 builder.seal_block(SIRTerminator::Branch {
5843 cond,
5844 true_block: (
5845 body_block,
5846 std::iter::once(header_counter)
5847 .chain(header_states.iter().copied())
5848 .collect(),
5849 ),
5850 false_block: (exit_block, header_states.clone()),
5851 });
5852 }
5853
5854 builder.switch_to_block(body_block);
5855 let loop_value = body_counter;
5856 let loop_value_trunc =
5857 self.cast_reg_width_ext(builder, loop_value, loop_width, loop_signed);
5858
5859 let mut env_inputs = crate::HashMap::default();
5860 for (update, state_reg) in updates.iter().zip(body_states.iter().copied()) {
5861 env_inputs.insert(update.target.clone(), state_reg);
5862 }
5863 env_inputs.insert(
5864 VarAtomBase::new(loop_var.clone(), 0, loop_width - 1),
5865 loop_value_trunc,
5866 );
5867 let env = LowerEnv {
5868 inputs: env_inputs,
5869 parent: parent_env,
5870 };
5871 let mut local_cache = crate::HashMap::default();
5872 self.lower_for_effects(builder, arena, &mut local_cache, &env, effects);
5873 let mut next_states: Vec<_> = updates
5874 .iter()
5875 .map(|update| {
5876 let reg = self.lower_inner(
5877 builder,
5878 update.expr,
5879 arena,
5880 &mut local_cache,
5881 Some(&env),
5882 false,
5883 );
5884 let width = update.target.access.msb - update.target.access.lsb + 1;
5885 self.cast_reg_width(builder, reg, width)
5886 })
5887 .collect();
5888 if let crate::SLTForFoldResult::Transient { update, .. } = result {
5889 let reg =
5890 self.lower_inner(builder, *update, arena, &mut local_cache, Some(&env), false);
5891 next_states.push(self.cast_reg_width(builder, reg, self.get_width(*update, arena)));
5892 }
5893
5894 let continue_reg = self.lower_inner(
5895 builder,
5896 continue_cond,
5897 arena,
5898 &mut local_cache,
5899 Some(&env),
5900 false,
5901 );
5902
5903 let progress_block = builder.new_block();
5904 builder.seal_block(SIRTerminator::Branch {
5905 cond: continue_reg,
5906 true_block: (progress_block, vec![]),
5907 false_block: (exit_block, next_states.clone()),
5908 });
5909 builder.switch_to_block(progress_block);
5910
5911 if reverse {
5912 let reverse_width = Self::step_math_width(compare_width, SLTStepOp::Add, step);
5913 let current_math =
5914 self.cast_reg_width_ext(builder, body_counter, reverse_width, loop_signed);
5915 let start_math =
5916 self.cast_reg_width_ext(builder, start_reg, reverse_width, loop_signed);
5917 let reverse_step = builder.alloc_bit(reverse_width, loop_signed);
5918 builder.emit(SIRInstruction::Imm(
5919 reverse_step,
5920 SIRValue::new(step as u64),
5921 ));
5922 let next_raw = builder.alloc_bit(reverse_width, loop_signed);
5923 builder.emit(SIRInstruction::Binary(
5924 next_raw,
5925 current_math,
5926 BinaryOp::Sub,
5927 reverse_step,
5928 ));
5929 let next_visible = self.cast_reg_width_ext(builder, next_raw, loop_width, loop_signed);
5930 let next_math =
5931 self.cast_reg_width_ext(builder, next_visible, reverse_width, loop_signed);
5932 let decreasing = builder.alloc_bit(1, false);
5933 builder.emit(SIRInstruction::Binary(
5934 decreasing,
5935 next_math,
5936 if loop_signed {
5937 BinaryOp::LtS
5938 } else {
5939 BinaryOp::LtU
5940 },
5941 current_math,
5942 ));
5943 let range_check_block = builder.new_block();
5944 let stall_block = builder.new_block();
5945 builder.seal_block(SIRTerminator::Branch {
5946 cond: decreasing,
5947 true_block: (range_check_block, vec![]),
5948 false_block: (stall_block, vec![]),
5949 });
5950 builder.switch_to_block(range_check_block);
5951 let in_range = builder.alloc_bit(1, false);
5952 builder.emit(SIRInstruction::Binary(
5953 in_range,
5954 next_math,
5955 if loop_signed {
5956 BinaryOp::GeS
5957 } else {
5958 BinaryOp::GeU
5959 },
5960 start_math,
5961 ));
5962 let next_counter =
5963 self.cast_reg_width_ext(builder, next_math, compare_width, loop_signed);
5964 builder.seal_block(SIRTerminator::Branch {
5965 cond: in_range,
5966 true_block: (
5967 header_block,
5968 std::iter::once(next_counter)
5969 .chain(next_states.iter().copied())
5970 .collect(),
5971 ),
5972 false_block: (exit_block, next_states.clone()),
5973 });
5974 builder.switch_to_block(stall_block);
5975 builder.seal_block(SIRTerminator::Error(1));
5976 } else {
5977 let math_width = Self::step_math_width(compare_width, step_op, step);
5978 let step_width = if matches!(step_op, SLTStepOp::BitOr | SLTStepOp::BitXor) {
5979 loop_width
5980 } else {
5981 math_width
5982 };
5983 let current_step =
5984 self.cast_reg_width_ext(builder, body_counter, step_width, loop_signed);
5985 let step_reg = builder.alloc_bit(step_width, loop_signed);
5986 let step_value = Self::truncate_usize_to_width(step, step_width);
5987 builder.emit(SIRInstruction::Imm(
5988 step_reg,
5989 SIRValue::new(step_value as u64),
5990 ));
5991 let next_step = builder.alloc_bit(step_width, loop_signed);
5992 let op = match step_op {
5993 SLTStepOp::Add => BinaryOp::Add,
5994 SLTStepOp::Mul => BinaryOp::Mul,
5995 SLTStepOp::Shl => BinaryOp::Shl,
5996 SLTStepOp::BitOr => BinaryOp::Or,
5997 SLTStepOp::BitXor => BinaryOp::Xor,
5998 };
5999 builder.emit(SIRInstruction::Binary(
6000 next_step,
6001 current_step,
6002 op,
6003 step_reg,
6004 ));
6005 let current_math =
6006 self.cast_reg_width_ext(builder, current_step, math_width, loop_signed);
6007 let next_visible = self.cast_reg_width_ext(builder, next_step, loop_width, loop_signed);
6013 let next_math = self.cast_reg_width_ext(builder, next_visible, math_width, loop_signed);
6014
6015 let progress = builder.alloc_bit(1, false);
6016 builder.emit(SIRInstruction::Binary(
6017 progress,
6018 next_math,
6019 BinaryOp::Ne,
6020 current_math,
6021 ));
6022 let check_block = builder.new_block();
6023 let stall_block = builder.new_block();
6024 builder.seal_block(SIRTerminator::Branch {
6025 cond: progress,
6026 true_block: (check_block, vec![]),
6027 false_block: (stall_block, vec![]),
6028 });
6029
6030 builder.switch_to_block(check_block);
6031 let increasing = builder.alloc_bit(1, false);
6032 builder.emit(SIRInstruction::Binary(
6033 increasing,
6034 next_math,
6035 if loop_signed {
6036 BinaryOp::GtS
6037 } else {
6038 BinaryOp::GtU
6039 },
6040 current_math,
6041 ));
6042 let range_check_block = builder.new_block();
6043 builder.seal_block(SIRTerminator::Branch {
6044 cond: increasing,
6045 true_block: (range_check_block, vec![]),
6046 false_block: (stall_block, vec![]),
6047 });
6048
6049 builder.switch_to_block(range_check_block);
6050 let end_math = self.cast_reg_width_ext(builder, end_limit, math_width, loop_signed);
6051 let in_range = builder.alloc_bit(1, false);
6052 builder.emit(SIRInstruction::Binary(
6053 in_range,
6054 next_math,
6055 if loop_signed {
6056 if inclusive {
6057 BinaryOp::LeS
6058 } else {
6059 BinaryOp::LtS
6060 }
6061 } else {
6062 BinaryOp::LtU
6063 },
6064 end_math,
6065 ));
6066 let next_counter =
6067 self.cast_reg_width_ext(builder, next_math, compare_width, loop_signed);
6068 builder.seal_block(SIRTerminator::Branch {
6069 cond: in_range,
6070 true_block: (
6071 header_block,
6072 std::iter::once(next_counter)
6073 .chain(next_states.iter().copied())
6074 .collect(),
6075 ),
6076 false_block: (exit_block, next_states.clone()),
6077 });
6078
6079 builder.switch_to_block(stall_block);
6080 builder.seal_block(SIRTerminator::Error(1));
6081 }
6082
6083 builder.switch_to_block(exit_block);
6084 let result_idx = match result {
6085 crate::SLTForFoldResult::State(result) => updates
6086 .iter()
6087 .position(|update| update.target == *result)
6088 .expect("ForFold result target must be present in updates"),
6089 crate::SLTForFoldResult::Transient { .. } => updates.len(),
6090 };
6091 exit_states[result_idx]
6092 }
6093
6094 fn lower_for_effects<A: Hash + Eq + Clone + std::fmt::Debug + std::fmt::Display>(
6095 &self,
6096 builder: &mut SIRBuilder<A>,
6097 arena: &SLTNodeArena<A>,
6098 cache: &mut crate::HashMap<NodeId, RegisterId>,
6099 env: &LowerEnv<'_, A>,
6100 effects: &[crate::SLTForEffect],
6101 ) {
6102 for effect in effects {
6103 let crate::SLTForEffect::Event {
6104 site_id,
6105 guard,
6106 emit_on_true,
6107 args,
6108 fatal_error_code,
6109 } = effect
6110 else {
6111 let crate::SLTForEffect::Runner(runner) = effect else {
6112 unreachable!()
6113 };
6114 self.lower_inner(builder, *runner, arena, cache, Some(env), false);
6115 continue;
6116 };
6117 let emit = |builder: &mut SIRBuilder<A>,
6118 this: &Self,
6119 cache: &mut crate::HashMap<NodeId, RegisterId>| {
6120 let args = args
6121 .iter()
6122 .map(|arg| this.lower_inner(builder, *arg, arena, cache, Some(env), false))
6123 .collect();
6124 builder.emit(SIRInstruction::CombCaptureEvent {
6125 site_id: *site_id,
6126 args,
6127 fatal_error_code: *fatal_error_code,
6128 consume_enabled: false,
6129 });
6130 };
6131 if let Some(guard) = guard {
6132 let cond = self.lower_inner(builder, *guard, arena, cache, Some(env), false);
6133 let branch_cond = if *emit_on_true {
6134 cond
6135 } else {
6136 let inverted = builder.alloc_bit(1, false);
6137 builder.emit(SIRInstruction::Unary(inverted, UnaryOp::LogicNot, cond));
6138 inverted
6139 };
6140 let event_block = builder.new_block();
6141 let done_block = builder.new_block();
6142 builder.seal_block(SIRTerminator::Branch {
6143 cond: branch_cond,
6144 true_block: (event_block, vec![]),
6145 false_block: (done_block, vec![]),
6146 });
6147 builder.switch_to_block(event_block);
6148 emit(builder, self, cache);
6149 builder.seal_block(SIRTerminator::Jump(done_block, vec![]));
6150 builder.switch_to_block(done_block);
6151 } else {
6152 emit(builder, self, cache);
6153 }
6154 }
6155 }
6156}
6157
6158impl Drop for SLTToSIRLowerer {
6159 fn drop(&mut self) {
6160 let Some(stats) = &self.mux_stats else {
6161 return;
6162 };
6163 let stats = stats.borrow();
6164 tracing::debug!(
6165 "[mux-lower-stats] normal_seen={} slice_seen={} constant_folded={} cfg_cost={} cfg_div_rem={} cfg_slice_cost={} cfg_slice_div_rem={} shared_nodes_hoisted={} kept_four_state={} kept_impure={} kept_dynamic_env={} kept_unprofitable={} kept_deep_shared={} biased_conditions={} owned_cost_sum={} owned_cost_max={} unprofitable_buckets_0_7_15_31_63_127_255_inf={:?}",
6166 stats.normal_seen,
6167 stats.slice_seen,
6168 stats.constant_folded,
6169 stats.cfg_cost,
6170 stats.cfg_div_rem,
6171 stats.cfg_slice_cost,
6172 stats.cfg_slice_div_rem,
6173 stats.shared_nodes_hoisted,
6174 stats.kept_four_state,
6175 stats.kept_impure,
6176 stats.kept_dynamic_env,
6177 stats.kept_unprofitable,
6178 stats.kept_deep_shared,
6179 stats.biased_conditions,
6180 stats.owned_cost_sum,
6181 stats.owned_cost_max,
6182 stats.unprofitable_cost_buckets,
6183 );
6184 }
6185}
6186
6187#[cfg(test)]
6188mod tests {
6189 use super::*;
6190 use crate::SLTNodeArena;
6191 use celox_design::BitAccess;
6192 use celox_sir::{BlockId, ExecutionUnit};
6193
6194 fn input(arena: &mut SLTNodeArena<u32>, variable: u32, width: usize) -> NodeId {
6195 arena
6196 .alloc(SLTNode::Input {
6197 variable,
6198 signed: false,
6199 index: vec![],
6200 access: BitAccess::new(0, width - 1),
6201 })
6202 .unwrap()
6203 }
6204
6205 fn input_bit(arena: &mut SLTNodeArena<u32>, variable: u32, bit: usize) -> NodeId {
6206 arena
6207 .alloc(SLTNode::Input {
6208 variable,
6209 signed: false,
6210 index: vec![],
6211 access: BitAccess::new(bit, bit),
6212 })
6213 .unwrap()
6214 }
6215
6216 fn constant(arena: &mut SLTNodeArena<u32>, value: u64, width: usize) -> NodeId {
6217 arena
6218 .alloc(SLTNode::Constant(value.into(), 0u8.into(), width, false))
6219 .unwrap()
6220 }
6221
6222 fn operation_chain(
6223 arena: &mut SLTNodeArena<u32>,
6224 mut value: NodeId,
6225 op: BinaryOp,
6226 operations: usize,
6227 constant_base: u64,
6228 width: usize,
6229 ) -> NodeId {
6230 for index in 0..operations {
6231 let rhs = constant(arena, constant_base + index as u64, width);
6232 value = arena.alloc(SLTNode::Binary(value, op, rhs)).unwrap();
6233 }
6234 value
6235 }
6236
6237 fn guarded_lane_concat(
6238 arena: &mut SLTNodeArena<u32>,
6239 lanes: usize,
6240 ungated_lane: Option<usize>,
6241 ) -> (NodeId, NodeId, NodeId) {
6242 let valid = input(arena, 10_000, 1);
6243 let is_store = input(arena, 10_001, 1);
6244 let guard = arena
6245 .alloc(SLTNode::Binary(valid, BinaryOp::LogicAnd, is_store))
6246 .unwrap();
6247 let threshold = constant(arena, 0x8000_0000_0000_0000, 64);
6248 let mut parts = Vec::with_capacity(lanes);
6249 let mut first_predicate = None;
6250 for lane in 0..lanes {
6251 let source = input(arena, 11_000 + lane as u32, 64);
6252 let expensive = operation_chain(arena, source, BinaryOp::Add, 6, 3, 64);
6253 let predicate = arena
6254 .alloc(SLTNode::Binary(expensive, BinaryOp::GeU, threshold))
6255 .unwrap();
6256 first_predicate.get_or_insert(predicate);
6257 let value = if ungated_lane == Some(lane) {
6258 predicate
6259 } else {
6260 arena
6261 .alloc(SLTNode::Binary(guard, BinaryOp::LogicAnd, predicate))
6262 .unwrap()
6263 };
6264 parts.push((value, 1));
6265 }
6266 let root = arena.alloc(SLTNode::Concat(parts)).unwrap();
6267 (root, guard, first_predicate.unwrap())
6268 }
6269
6270 fn finish_lowering(mut builder: SIRBuilder<u32>) -> ExecutionUnit<u32> {
6271 builder.seal_block(SIRTerminator::Return);
6272 let (blocks, register_map, _) = builder.drain();
6273 let eu = ExecutionUnit {
6274 entry_block_id: BlockId(0),
6275 blocks,
6276 register_map,
6277 };
6278 eu.verify_result()
6279 .unwrap_or_else(|error| panic!("{error}\n{eu}"));
6280 eu
6281 }
6282
6283 #[test]
6284 fn static_input_slice_lowers_to_an_exact_range_load() {
6285 let mut arena = SLTNodeArena::new();
6286 let packed = arena
6287 .alloc(SLTNode::Input {
6288 variable: 10,
6289 signed: false,
6290 index: Vec::new(),
6291 access: BitAccess::new(100, 938),
6292 })
6293 .unwrap();
6294 let field = arena
6295 .alloc(SLTNode::Slice {
6296 expr: packed,
6297 access: BitAccess::new(33, 37),
6298 })
6299 .unwrap();
6300
6301 let mut builder = SIRBuilder::new();
6302 SLTToSIRLowerer::new(false).lower(
6303 &mut builder,
6304 field,
6305 &arena,
6306 &mut crate::HashMap::default(),
6307 );
6308 let eu = finish_lowering(builder);
6309 let instructions = &eu.blocks[&eu.entry_block_id].instructions;
6310
6311 assert!(matches!(
6312 instructions.as_slice(),
6313 [SIRInstruction::Load(_, 10, SIROffset::Static(133), 5)]
6314 ));
6315 }
6316
6317 #[test]
6318 fn pointwise_slice_lowers_only_requested_input_ranges() {
6319 let mut arena = SLTNodeArena::new();
6320 let lhs = arena
6321 .alloc(SLTNode::Input {
6322 variable: 10,
6323 signed: false,
6324 index: Vec::new(),
6325 access: BitAccess::new(100, 115),
6326 })
6327 .unwrap();
6328 let rhs = arena
6329 .alloc(SLTNode::Input {
6330 variable: 20,
6331 signed: false,
6332 index: Vec::new(),
6333 access: BitAccess::new(200, 215),
6334 })
6335 .unwrap();
6336 let bitwise = arena
6337 .alloc(SLTNode::Binary(lhs, BinaryOp::And, rhs))
6338 .unwrap();
6339 let field = arena
6340 .alloc(SLTNode::Slice {
6341 expr: bitwise,
6342 access: BitAccess::new(4, 7),
6343 })
6344 .unwrap();
6345
6346 let mut builder = SIRBuilder::new();
6347 SLTToSIRLowerer::new(false).lower(
6348 &mut builder,
6349 field,
6350 &arena,
6351 &mut crate::HashMap::default(),
6352 );
6353 let eu = finish_lowering(builder);
6354 let instructions = &eu.blocks[&eu.entry_block_id].instructions;
6355
6356 assert!(instructions.iter().any(|instruction| matches!(
6357 instruction,
6358 SIRInstruction::Load(_, 10, SIROffset::Static(104), 4)
6359 )));
6360 assert!(instructions.iter().any(|instruction| matches!(
6361 instruction,
6362 SIRInstruction::Load(_, 20, SIROffset::Static(204), 4)
6363 )));
6364 assert!(instructions.iter().all(|instruction| !matches!(
6365 instruction,
6366 SIRInstruction::Load(_, 10 | 20, _, width) if *width != 4
6367 )));
6368 }
6369
6370 #[test]
6371 fn pointwise_slice_does_not_lower_an_input_annihilated_by_zero() {
6372 let mut arena = SLTNodeArena::new();
6373 let input = arena
6374 .alloc(SLTNode::Input {
6375 variable: 10,
6376 signed: false,
6377 index: Vec::new(),
6378 access: BitAccess::new(100, 115),
6379 })
6380 .unwrap();
6381 let zero = arena
6382 .alloc(SLTNode::Constant(0u8.into(), 0u8.into(), 16, false))
6383 .unwrap();
6384 let bitwise = arena
6385 .alloc(SLTNode::Binary(input, BinaryOp::And, zero))
6386 .unwrap();
6387 let field = arena
6388 .alloc(SLTNode::Slice {
6389 expr: bitwise,
6390 access: BitAccess::new(4, 7),
6391 })
6392 .unwrap();
6393
6394 let mut builder = SIRBuilder::new();
6395 SLTToSIRLowerer::new(true).lower(
6396 &mut builder,
6397 field,
6398 &arena,
6399 &mut crate::HashMap::default(),
6400 );
6401 let eu = finish_lowering(builder);
6402 let instructions = &eu.blocks[&eu.entry_block_id].instructions;
6403
6404 assert!(matches!(
6405 instructions.as_slice(),
6406 [SIRInstruction::Imm(_, value)] if value.payload.is_zero() && value.mask.is_zero()
6407 ));
6408 }
6409
6410 #[test]
6411 fn static_input_slice_preserves_a_cached_snapshot() {
6412 let mut arena = SLTNodeArena::new();
6413 let packed = input(&mut arena, 10, 839);
6414 let field = arena
6415 .alloc(SLTNode::Slice {
6416 expr: packed,
6417 access: BitAccess::new(133, 133),
6418 })
6419 .unwrap();
6420
6421 let lowerer = SLTToSIRLowerer::new(false);
6422 let mut builder = SIRBuilder::new();
6423 let mut cache = crate::HashMap::default();
6424 let snapshot = lowerer.lower(&mut builder, packed, &arena, &mut cache);
6425 lowerer.lower(&mut builder, field, &arena, &mut cache);
6426 let eu = finish_lowering(builder);
6427 let instructions = &eu.blocks[&eu.entry_block_id].instructions;
6428
6429 assert_eq!(
6430 instructions
6431 .iter()
6432 .filter(|instruction| matches!(instruction, SIRInstruction::Load(..)))
6433 .count(),
6434 1
6435 );
6436 assert!(instructions.iter().any(|instruction| matches!(
6437 instruction,
6438 SIRInstruction::Load(_, 10, SIROffset::Static(0), 839)
6439 )));
6440 assert!(instructions.iter().any(|instruction| matches!(
6441 instruction,
6442 SIRInstruction::Binary(_, source, BinaryOp::Shr, _) if *source == snapshot
6443 )));
6444 }
6445
6446 #[test]
6447 fn region_slice_caches_shared_rmw_projections_once() {
6448 const UPDATES: usize = 18;
6449
6450 for width in [8, 65] {
6451 let mut arena = SLTNodeArena::new();
6452 let mut previous = input(&mut arena, 10, width);
6453 for update in 0..UPDATES {
6454 let condition = input(&mut arena, 100 + update as u32, 1);
6455 let payload = constant(&mut arena, 1 << (update % 8), width);
6456 let modified = arena
6457 .alloc(SLTNode::Binary(previous, BinaryOp::Or, payload))
6458 .unwrap();
6459 previous = arena
6460 .alloc(SLTNode::Mux {
6461 cond: condition,
6462 then_expr: modified,
6463 else_expr: previous,
6464 })
6465 .unwrap();
6466 }
6467 let bit = arena
6468 .alloc(SLTNode::Slice {
6469 expr: previous,
6470 access: BitAccess::new(0, 0),
6471 })
6472 .unwrap();
6473
6474 for four_state in [false, true] {
6475 let mut builder = SIRBuilder::new();
6476 SLTToSIRLowerer::new(four_state).lower(
6477 &mut builder,
6478 bit,
6479 &arena,
6480 &mut crate::HashMap::default(),
6481 );
6482 let eu = finish_lowering(builder);
6483 let instructions = eu
6484 .blocks
6485 .values()
6486 .map(|block| block.instructions.len())
6487 .sum::<usize>();
6488
6489 assert!(
6490 instructions < 256,
6491 "{width}-bit shared RMW DAG expanded to {instructions} instructions in four_state={four_state}"
6492 );
6493 }
6494 }
6495 }
6496
6497 #[test]
6498 fn region_slice_keeps_wide_shared_multiply_narrow() {
6499 const WIDTH: usize = 4096;
6500
6501 let mut arena = SLTNodeArena::new();
6502 let lhs = input(&mut arena, 10, WIDTH);
6503 let rhs = input(&mut arena, 20, WIDTH);
6504 let shared = arena
6505 .alloc(SLTNode::Binary(lhs, BinaryOp::Mul, rhs))
6506 .unwrap();
6507 let ones = constant(&mut arena, 1, WIDTH);
6508 let first_user = arena
6509 .alloc(SLTNode::Binary(shared, BinaryOp::And, ones))
6510 .unwrap();
6511 let second_user = arena
6512 .alloc(SLTNode::Binary(shared, BinaryOp::Xor, ones))
6513 .unwrap();
6514 let first_bit = arena
6515 .alloc(SLTNode::Slice {
6516 expr: first_user,
6517 access: BitAccess::new(0, 0),
6518 })
6519 .unwrap();
6520 let second_bit = arena
6521 .alloc(SLTNode::Slice {
6522 expr: second_user,
6523 access: BitAccess::new(0, 0),
6524 })
6525 .unwrap();
6526 let root = arena
6527 .alloc(SLTNode::Concat(vec![(first_bit, 1), (second_bit, 1)]))
6528 .unwrap();
6529
6530 for four_state in [false, true] {
6531 let mut builder = SIRBuilder::new();
6532 SLTToSIRLowerer::new(four_state).lower(
6533 &mut builder,
6534 root,
6535 &arena,
6536 &mut crate::HashMap::default(),
6537 );
6538 let eu = finish_lowering(builder);
6539
6540 let multiply_widths = eu
6541 .blocks
6542 .values()
6543 .flat_map(|block| &block.instructions)
6544 .filter_map(|instruction| match instruction {
6545 SIRInstruction::Binary(dst, _, BinaryOp::Mul, _) => {
6546 Some(eu.register_map[dst].width())
6547 }
6548 _ => None,
6549 })
6550 .collect::<Vec<_>>();
6551 assert_eq!(
6552 multiply_widths,
6553 [1],
6554 "wide multiply was materialized in four_state={four_state}"
6555 );
6556 }
6557 }
6558
6559 #[test]
6560 fn region_slice_caches_shared_rmw_with_arithmetic_payloads() {
6561 const UPDATES: usize = 18;
6562 const WIDTH: usize = 4096;
6563
6564 let mut arena = SLTNodeArena::new();
6565 let mut previous = input(&mut arena, 10, WIDTH);
6566 let one = constant(&mut arena, 1, WIDTH);
6567 for update in 0..UPDATES {
6568 let condition = input(&mut arena, 100 + update as u32, 1);
6569 let payload_input = input(&mut arena, 200 + update as u32, WIDTH);
6570 let payload = arena
6571 .alloc(SLTNode::Binary(payload_input, BinaryOp::Add, one))
6572 .unwrap();
6573 let modified = arena
6574 .alloc(SLTNode::Binary(previous, BinaryOp::Or, payload))
6575 .unwrap();
6576 previous = arena
6577 .alloc(SLTNode::Mux {
6578 cond: condition,
6579 then_expr: modified,
6580 else_expr: previous,
6581 })
6582 .unwrap();
6583 }
6584 let bit = arena
6585 .alloc(SLTNode::Slice {
6586 expr: previous,
6587 access: BitAccess::new(0, 0),
6588 })
6589 .unwrap();
6590
6591 for four_state in [false, true] {
6592 let mut builder = SIRBuilder::new();
6593 SLTToSIRLowerer::new(four_state).lower(
6594 &mut builder,
6595 bit,
6596 &arena,
6597 &mut crate::HashMap::default(),
6598 );
6599 let eu = finish_lowering(builder);
6600 let instructions = eu
6601 .blocks
6602 .values()
6603 .flat_map(|block| &block.instructions)
6604 .collect::<Vec<_>>();
6605
6606 assert!(
6607 instructions.len() < 512,
6608 "arithmetic RMW DAG expanded to {} instructions in four_state={four_state}",
6609 instructions.len()
6610 );
6611 assert!(instructions.iter().all(|instruction| !matches!(
6612 instruction,
6613 SIRInstruction::Binary(dst, _, BinaryOp::Add, _)
6614 if eu.register_map[dst].width() != 1
6615 )));
6616 }
6617 }
6618
6619 #[test]
6620 fn region_slice_annihilates_shared_zero_before_expensive_operands() {
6621 const WIDTH: usize = 4096;
6622
6623 let mut arena = SLTNodeArena::new();
6624 let dividend = input(&mut arena, 10, WIDTH);
6625 let divisor = input(&mut arena, 20, WIDTH);
6626 let division = arena
6627 .alloc(SLTNode::Binary(dividend, BinaryOp::DivU, divisor))
6628 .unwrap();
6629 let zero = constant(&mut arena, 0, WIDTH);
6630 let dead = arena
6631 .alloc(SLTNode::Binary(division, BinaryOp::And, zero))
6632 .unwrap();
6633 let one = constant(&mut arena, 1, WIDTH);
6634 let first_user = arena
6635 .alloc(SLTNode::Binary(dead, BinaryOp::Or, one))
6636 .unwrap();
6637 let second_user = arena
6638 .alloc(SLTNode::Binary(dead, BinaryOp::Xor, one))
6639 .unwrap();
6640 let first_bit = arena
6641 .alloc(SLTNode::Slice {
6642 expr: first_user,
6643 access: BitAccess::new(0, 0),
6644 })
6645 .unwrap();
6646 let second_bit = arena
6647 .alloc(SLTNode::Slice {
6648 expr: second_user,
6649 access: BitAccess::new(0, 0),
6650 })
6651 .unwrap();
6652 let root = arena
6653 .alloc(SLTNode::Concat(vec![(first_bit, 1), (second_bit, 1)]))
6654 .unwrap();
6655
6656 for four_state in [false, true] {
6657 let mut builder = SIRBuilder::new();
6658 SLTToSIRLowerer::new(four_state).lower(
6659 &mut builder,
6660 root,
6661 &arena,
6662 &mut crate::HashMap::default(),
6663 );
6664 let eu = finish_lowering(builder);
6665
6666 assert!(
6667 eu.blocks
6668 .values()
6669 .all(
6670 |block| block.instructions.iter().all(|instruction| !matches!(
6671 instruction,
6672 SIRInstruction::Binary(_, _, BinaryOp::DivU, _)
6673 | SIRInstruction::Load(_, 10 | 20, _, _)
6674 ))
6675 )
6676 );
6677 }
6678 }
6679
6680 #[test]
6681 fn region_slice_skips_deep_dead_constant_mux_arm() {
6682 const DEPTH: usize = 20_000;
6683 const WIDTH: usize = 65;
6684
6685 let mut arena = SLTNodeArena::new();
6686 let condition = constant(&mut arena, 0, 1);
6687 let live = input(&mut arena, 10, WIDTH);
6688 let dead_input = input(&mut arena, 20, WIDTH);
6689 let dead = operation_chain(&mut arena, dead_input, BinaryOp::Xor, DEPTH, 100, WIDTH);
6690 let shared = arena
6691 .alloc(SLTNode::Mux {
6692 cond: condition,
6693 then_expr: dead,
6694 else_expr: live,
6695 })
6696 .unwrap();
6697 let one = constant(&mut arena, 1, WIDTH);
6698 let first_user = arena
6699 .alloc(SLTNode::Binary(shared, BinaryOp::And, one))
6700 .unwrap();
6701 let second_user = arena
6702 .alloc(SLTNode::Binary(shared, BinaryOp::Xor, one))
6703 .unwrap();
6704 let first_bit = arena
6705 .alloc(SLTNode::Slice {
6706 expr: first_user,
6707 access: BitAccess::new(0, 0),
6708 })
6709 .unwrap();
6710 let second_bit = arena
6711 .alloc(SLTNode::Slice {
6712 expr: second_user,
6713 access: BitAccess::new(0, 0),
6714 })
6715 .unwrap();
6716 let root = arena
6717 .alloc(SLTNode::Concat(vec![(first_bit, 1), (second_bit, 1)]))
6718 .unwrap();
6719
6720 let mut builder = SIRBuilder::new();
6721 SLTToSIRLowerer::new(false).lower(
6722 &mut builder,
6723 root,
6724 &arena,
6725 &mut crate::HashMap::default(),
6726 );
6727 let eu = finish_lowering(builder);
6728 let instructions = eu
6729 .blocks
6730 .values()
6731 .flat_map(|block| &block.instructions)
6732 .collect::<Vec<_>>();
6733
6734 assert!(instructions.len() < 32);
6735 assert!(
6736 instructions
6737 .iter()
6738 .all(|instruction| !matches!(instruction, SIRInstruction::Load(_, 20, _, _)))
6739 );
6740 }
6741
6742 #[test]
6743 fn static_input_slice_uses_an_exact_override_range() {
6744 let mut arena = SLTNodeArena::new();
6745 let packed = input(&mut arena, 10, 16);
6746 let field = arena
6747 .alloc(SLTNode::Slice {
6748 expr: packed,
6749 access: BitAccess::new(4, 7),
6750 })
6751 .unwrap();
6752
6753 let mut builder = SIRBuilder::new();
6754 let materialized = builder.alloc_logic(16);
6755 builder.emit(SIRInstruction::Imm(materialized, SIRValue::new(0xabcdu16)));
6756 let mut inputs = crate::HashMap::default();
6757 inputs.insert(VarAtomBase::new(10, 0, 15), materialized);
6758 SLTToSIRLowerer::new(false).lower_with_inputs(
6759 &mut builder,
6760 field,
6761 &arena,
6762 &mut crate::HashMap::default(),
6763 inputs,
6764 );
6765 let eu = finish_lowering(builder);
6766 let instructions = &eu.blocks[&eu.entry_block_id].instructions;
6767
6768 assert!(
6769 instructions
6770 .iter()
6771 .all(|instruction| !matches!(instruction, SIRInstruction::Load(..)))
6772 );
6773 assert!(instructions.iter().any(|instruction| matches!(
6774 instruction,
6775 SIRInstruction::Binary(_, source, BinaryOp::Shr, _) if *source == materialized
6776 )));
6777 }
6778
6779 fn instruction_count(
6780 eu: &ExecutionUnit<u32>,
6781 predicate: impl Fn(&SIRInstruction<u32>) -> bool,
6782 ) -> usize {
6783 eu.blocks
6784 .values()
6785 .flat_map(|block| &block.instructions)
6786 .filter(|instruction| predicate(instruction))
6787 .count()
6788 }
6789
6790 fn branch_count(eu: &ExecutionUnit<u32>) -> usize {
6791 eu.blocks
6792 .values()
6793 .filter(|block| matches!(block.terminator, SIRTerminator::Branch { .. }))
6794 .count()
6795 }
6796
6797 #[derive(Clone, Debug, PartialEq, Eq)]
6798 struct TestSIRValue {
6799 payload: BigUint,
6800 mask: BigUint,
6801 }
6802
6803 fn width_mask(width: usize) -> BigUint {
6804 (BigUint::from(1u8) << width) - BigUint::from(1u8)
6805 }
6806
6807 fn execute_fold_group_sir(eu: &ExecutionUnit<u32>) -> crate::HashMap<RegisterId, TestSIRValue> {
6812 execute_fold_group_sir_with_memory(eu, &crate::HashMap::default())
6813 }
6814
6815 fn execute_fold_group_sir_with_memory(
6816 eu: &ExecutionUnit<u32>,
6817 memory: &crate::HashMap<u32, TestSIRValue>,
6818 ) -> crate::HashMap<RegisterId, TestSIRValue> {
6819 let mut values = crate::HashMap::default();
6820 let mut current = eu.entry_block_id;
6821
6822 for _ in 0..100 {
6823 let block = &eu.blocks[¤t];
6824 for instruction in &block.instructions {
6825 match instruction {
6826 SIRInstruction::Imm(dst, value) => {
6827 values.insert(
6828 *dst,
6829 TestSIRValue {
6830 payload: value.payload.clone(),
6831 mask: value.mask.clone(),
6832 },
6833 );
6834 }
6835 SIRInstruction::Binary(dst, lhs, op, rhs) => {
6836 let lhs_reg = *lhs;
6837 let rhs_reg = *rhs;
6838 let lhs = &values[&lhs_reg];
6839 let rhs = &values[&rhs_reg];
6840 let width = eu.register_map[dst].width();
6841 let modulus = BigUint::from(1u8) << width;
6842 let (payload, mask) = match op {
6843 BinaryOp::LogicAnd | BinaryOp::LogicOr => {
6844 let truth = |reg: RegisterId, value: &TestSIRValue| {
6845 let known =
6846 width_mask(eu.register_map[®].width()) ^ &value.mask;
6847 if (&value.payload & known) != BigUint::from(0u8) {
6848 Some(true)
6849 } else if value.mask.is_zero() {
6850 Some(false)
6851 } else {
6852 None
6853 }
6854 };
6855 let lhs_truth = truth(lhs_reg, lhs);
6856 let rhs_truth = truth(rhs_reg, rhs);
6857 let known = match op {
6858 BinaryOp::LogicAnd => {
6859 if lhs_truth == Some(false) || rhs_truth == Some(false) {
6860 Some(false)
6861 } else if lhs_truth == Some(true) && rhs_truth == Some(true)
6862 {
6863 Some(true)
6864 } else {
6865 None
6866 }
6867 }
6868 BinaryOp::LogicOr => {
6869 if lhs_truth == Some(true) || rhs_truth == Some(true) {
6870 Some(true)
6871 } else if lhs_truth == Some(false)
6872 && rhs_truth == Some(false)
6873 {
6874 Some(false)
6875 } else {
6876 None
6877 }
6878 }
6879 _ => unreachable!(),
6880 };
6881 match known {
6882 Some(value) => (BigUint::from(value), BigUint::from(0u8)),
6883 None => (BigUint::from(0u8), BigUint::from(1u8)),
6884 }
6885 }
6886 _ => {
6887 assert_eq!(lhs.mask, BigUint::from(0u8));
6888 assert_eq!(rhs.mask, BigUint::from(0u8));
6889 let payload = match op {
6890 BinaryOp::Add => (&lhs.payload + &rhs.payload) % &modulus,
6891 BinaryOp::Mul => (&lhs.payload * &rhs.payload) % &modulus,
6892 BinaryOp::Sub => {
6893 (&lhs.payload + &modulus - &rhs.payload) % &modulus
6894 }
6895 BinaryOp::And => &lhs.payload & &rhs.payload,
6896 BinaryOp::Or => &lhs.payload | &rhs.payload,
6897 BinaryOp::Shl => {
6898 let shift = rhs
6899 .payload
6900 .to_u64_digits()
6901 .first()
6902 .copied()
6903 .unwrap_or(0);
6904 if shift > usize::MAX as u64 {
6905 BigUint::from(0u8)
6906 } else {
6907 (&lhs.payload << shift as usize) % &modulus
6908 }
6909 }
6910 BinaryOp::Shr => {
6911 let shift = rhs
6912 .payload
6913 .to_u64_digits()
6914 .first()
6915 .copied()
6916 .unwrap_or(0);
6917 if shift > usize::MAX as u64 {
6918 BigUint::from(0u8)
6919 } else {
6920 &lhs.payload >> shift as usize
6921 }
6922 }
6923 BinaryOp::Eq | BinaryOp::EqWildcard => {
6924 BigUint::from(lhs.payload == rhs.payload)
6925 }
6926 BinaryOp::Ne => BigUint::from(lhs.payload != rhs.payload),
6927 BinaryOp::GeU => BigUint::from(lhs.payload >= rhs.payload),
6928 other => {
6929 panic!("unexpected grouped-fold binary op {other:?}")
6930 }
6931 };
6932 (payload, BigUint::from(0u8))
6933 }
6934 };
6935 values.insert(*dst, TestSIRValue { payload, mask });
6936 }
6937 SIRInstruction::Unary(dst, op, src) => {
6938 let width = eu.register_map[dst].width();
6939 let value = &values[src];
6940 let (payload, mask) = match op {
6941 UnaryOp::Ident => (value.payload.clone(), value.mask.clone()),
6942 UnaryOp::ToTwoState => {
6943 let known = width_mask(width) ^ &value.mask;
6944 (&value.payload & known, BigUint::from(0u8))
6945 }
6946 UnaryOp::BitNot => {
6947 (&width_mask(width) ^ &value.payload, value.mask.clone())
6948 }
6949 UnaryOp::LogicNot => (
6950 BigUint::from(value.payload == BigUint::from(0u8)),
6951 value.mask.clone(),
6952 ),
6953 UnaryOp::Or => (
6954 BigUint::from(value.payload != BigUint::from(0u8)),
6955 value.mask.clone(),
6956 ),
6957 UnaryOp::PopCount => (
6958 BigUint::from(
6959 value
6960 .payload
6961 .to_u64_digits()
6962 .iter()
6963 .map(|word| word.count_ones() as u64)
6964 .sum::<u64>(),
6965 ),
6966 value.mask.clone(),
6967 ),
6968 other => panic!("unexpected grouped-fold unary op {other:?}"),
6969 };
6970 values.insert(*dst, TestSIRValue { payload, mask });
6971 }
6972 SIRInstruction::Load(dst, address, offset, width) => {
6973 let offset = match offset {
6974 SIROffset::Static(offset)
6975 | SIROffset::PackedElements {
6976 bit_offset: offset, ..
6977 } => *offset,
6978 SIROffset::Dynamic(offset) => values[offset]
6979 .payload
6980 .to_u64_digits()
6981 .first()
6982 .copied()
6983 .unwrap_or(0)
6984 as usize,
6985 SIROffset::Element {
6986 index,
6987 element_width,
6988 bit_offset,
6989 dynamic_bit_offset,
6990 } => {
6991 let element = values[index]
6992 .payload
6993 .to_u64_digits()
6994 .first()
6995 .copied()
6996 .unwrap_or(0)
6997 as usize;
6998 let dynamic_bit_offset = dynamic_bit_offset
6999 .map(|register| {
7000 values[®ister]
7001 .payload
7002 .to_u64_digits()
7003 .first()
7004 .copied()
7005 .unwrap_or(0)
7006 as usize
7007 })
7008 .unwrap_or(0);
7009 element * element_width + bit_offset + dynamic_bit_offset
7010 }
7011 };
7012 let source = memory
7013 .get(address)
7014 .unwrap_or_else(|| panic!("missing test memory value at {address}"));
7015 let mask = width_mask(*width);
7016 values.insert(
7017 *dst,
7018 TestSIRValue {
7019 payload: (&source.payload >> offset) & &mask,
7020 mask: (&source.mask >> offset) & mask,
7021 },
7022 );
7023 }
7024 SIRInstruction::Concat(dst, args) => {
7025 let mut payload = BigUint::from(0u8);
7026 let mut mask = BigUint::from(0u8);
7027 for arg in args {
7028 let width = eu.register_map[arg].width();
7029 payload = (payload << width) | &values[arg].payload;
7030 mask = (mask << width) | &values[arg].mask;
7031 }
7032 values.insert(*dst, TestSIRValue { payload, mask });
7033 }
7034 SIRInstruction::Slice(dst, src, bit_offset, width) => {
7035 let mask = width_mask(*width);
7036 values.insert(
7037 *dst,
7038 TestSIRValue {
7039 payload: (&values[src].payload >> *bit_offset) & &mask,
7040 mask: (&values[src].mask >> *bit_offset) & mask,
7041 },
7042 );
7043 }
7044 SIRInstruction::Mux(dst, cond, then_value, else_value) => {
7045 let cond = &values[cond];
7046 let selected = if cond.payload == BigUint::from(0u8) {
7047 &values[else_value]
7048 } else {
7049 &values[then_value]
7050 };
7051 let width = eu.register_map[dst].width();
7052 values.insert(
7053 *dst,
7054 TestSIRValue {
7055 payload: &selected.payload & width_mask(width),
7056 mask: if cond.mask == BigUint::from(0u8) {
7057 &selected.mask & width_mask(width)
7058 } else {
7059 width_mask(width)
7060 },
7061 },
7062 );
7063 }
7064 other => panic!("unexpected grouped-fold instruction {other:?}"),
7065 }
7066 }
7067
7068 let (next, args) = match &block.terminator {
7069 SIRTerminator::Jump(target, args) => (*target, args),
7070 SIRTerminator::Branch {
7071 cond,
7072 true_block,
7073 false_block,
7074 } => {
7075 if values[cond].payload == BigUint::from(0u8) {
7076 (false_block.0, &false_block.1)
7077 } else {
7078 (true_block.0, &true_block.1)
7079 }
7080 }
7081 SIRTerminator::Switch { .. } => {
7082 panic!("unexpected Switch in grouped-fold lowering test")
7083 }
7084 SIRTerminator::Return => return values,
7085 SIRTerminator::Error(code) => panic!("unexpected Error({code})"),
7086 };
7087 let arguments = args
7088 .iter()
7089 .map(|argument| values[argument].clone())
7090 .collect::<Vec<_>>();
7091 for (¶meter, argument) in eu.blocks[&next].params.iter().zip(arguments) {
7092 values.insert(parameter, argument);
7093 }
7094 current = next;
7095 }
7096 panic!("grouped fold did not terminate at its exact trip count")
7097 }
7098
7099 const SCAN_VECTOR_STATE: u32 = 100;
7100 const SCAN_FOUND_STATE: u32 = 101;
7101 const SCAN_SOURCE: u32 = 102;
7102 const SCAN_MASK: u32 = 103;
7103 const SCAN_BOUND: u32 = 104;
7104 const SCAN_UNMASKED: u32 = 105;
7105 const SCAN_MODE: u32 = 106;
7106 const SCAN_GUARD: u32 = 107;
7107 const SCAN_LOOP: u32 = 108;
7108
7109 #[derive(Clone, Copy)]
7110 enum ScanMutation {
7111 None,
7112 OverflowFalseGuard,
7113 DifferentActive,
7114 NonIdentityOffset,
7115 NonIdentityInputStride,
7116 NarrowLoopMask,
7117 WrongBeforeValue,
7118 }
7119
7120 fn scan_dynamic_bit(
7121 arena: &mut SLTNodeArena<u32>,
7122 variable: u32,
7123 loop_value: NodeId,
7124 width: usize,
7125 stride: usize,
7126 unpacked: bool,
7127 ) -> NodeId {
7128 let _ = width;
7129 arena
7130 .alloc(SLTNode::Input {
7131 variable,
7132 signed: false,
7133 index: vec![crate::SLTIndex {
7134 node: loop_value,
7135 stride,
7136 kind: if unpacked {
7137 crate::SLTIndexKind::Unpacked { element_width: 1 }
7138 } else {
7139 crate::SLTIndexKind::Packed
7140 },
7141 }],
7142 access: BitAccess::new(0, 0),
7143 })
7144 .unwrap()
7145 }
7146
7147 fn synthetic_or_scan_group(
7148 width: usize,
7149 mutation: ScanMutation,
7150 ) -> (SLTNodeArena<u32>, NodeId) {
7151 synthetic_or_scan_group_with_layout(width, mutation, false)
7152 }
7153
7154 fn synthetic_or_scan_group_with_layout(
7155 width: usize,
7156 mutation: ScanMutation,
7157 unpacked: bool,
7158 ) -> (SLTNodeArena<u32>, NodeId) {
7159 let mut arena = SLTNodeArena::new();
7160 let loop_value = input(&mut arena, SCAN_LOOP, 64);
7161 let old_vector = input(&mut arena, SCAN_VECTOR_STATE, width);
7162 let old_found = input(&mut arena, SCAN_FOUND_STATE, 1);
7163 let source = scan_dynamic_bit(
7164 &mut arena,
7165 SCAN_SOURCE,
7166 loop_value,
7167 width,
7168 if matches!(mutation, ScanMutation::NonIdentityInputStride) {
7169 2
7170 } else {
7171 1
7172 },
7173 unpacked,
7174 );
7175 let mask = scan_dynamic_bit(&mut arena, SCAN_MASK, loop_value, width, 1, unpacked);
7176 let bound = input(&mut arena, SCAN_BOUND, 8);
7177 let unmasked = input(&mut arena, SCAN_UNMASKED, 1);
7178 let mode = input(&mut arena, SCAN_MODE, 2);
7179 let guard = if matches!(mutation, ScanMutation::OverflowFalseGuard) {
7180 let one = constant(&mut arena, 1, 1);
7181 arena
7182 .alloc(SLTNode::Binary(one, BinaryOp::Add, one))
7183 .unwrap()
7184 } else {
7185 input(&mut arena, SCAN_GUARD, 1)
7186 };
7187
7188 let lane_bits = slt_scan_lane_bits(width);
7189 let valid_lane_mask = (1u64 << lane_bits) - 1;
7190 let lane_mask = constant(
7191 &mut arena,
7192 if matches!(mutation, ScanMutation::NarrowLoopMask) {
7193 valid_lane_mask >> 1
7194 } else {
7195 0xff
7196 },
7197 64,
7198 );
7199 let truncated_loop = arena
7200 .alloc(SLTNode::Binary(loop_value, BinaryOp::And, lane_mask))
7201 .unwrap();
7202 let in_range = arena
7203 .alloc(SLTNode::Binary(truncated_loop, BinaryOp::LtU, bound))
7204 .unwrap();
7205 let enabled = arena
7206 .alloc(SLTNode::Binary(unmasked, BinaryOp::LogicOr, mask))
7207 .unwrap();
7208 let active = arena
7209 .alloc(SLTNode::Binary(in_range, BinaryOp::LogicAnd, enabled))
7210 .unwrap();
7211 let found_next = arena
7212 .alloc(SLTNode::Binary(old_found, BinaryOp::LogicOr, source))
7213 .unwrap();
7214 let found_update = arena
7215 .alloc(SLTNode::Mux {
7216 cond: active,
7217 then_expr: found_next,
7218 else_expr: old_found,
7219 })
7220 .unwrap();
7221
7222 let not_found = arena
7223 .alloc(SLTNode::Unary(UnaryOp::LogicNot, old_found))
7224 .unwrap();
7225 let not_source = arena
7226 .alloc(SLTNode::Unary(UnaryOp::LogicNot, source))
7227 .unwrap();
7228 let before = arena
7229 .alloc(SLTNode::Binary(
7230 not_found,
7231 BinaryOp::LogicAnd,
7232 if matches!(mutation, ScanMutation::WrongBeforeValue) {
7233 source
7234 } else {
7235 not_source
7236 },
7237 ))
7238 .unwrap();
7239 let first = arena
7240 .alloc(SLTNode::Binary(not_found, BinaryOp::LogicAnd, source))
7241 .unwrap();
7242 let one_mode = constant(&mut arena, 1, 2);
7243 let two_mode = constant(&mut arena, 2, 2);
7244 let is_before = arena
7245 .alloc(SLTNode::Binary(mode, BinaryOp::EqWildcard, one_mode))
7246 .unwrap();
7247 let is_first = arena
7248 .alloc(SLTNode::Binary(mode, BinaryOp::EqWildcard, two_mode))
7249 .unwrap();
7250 let first_or_through = arena
7251 .alloc(SLTNode::Mux {
7252 cond: is_first,
7253 then_expr: first,
7254 else_expr: not_found,
7255 })
7256 .unwrap();
7257 let selected = arena
7258 .alloc(SLTNode::Mux {
7259 cond: is_before,
7260 then_expr: before,
7261 else_expr: first_or_through,
7262 })
7263 .unwrap();
7264
7265 let zero64 = constant(&mut arena, 0, 64);
7266 let one64 = constant(&mut arena, 1, 64);
7267 let scaled = arena
7268 .alloc(SLTNode::Binary(loop_value, BinaryOp::Mul, one64))
7269 .unwrap();
7270 let identity_offset = arena
7271 .alloc(SLTNode::Binary(zero64, BinaryOp::Add, scaled))
7272 .unwrap();
7273 let offset = if matches!(mutation, ScanMutation::NonIdentityOffset) {
7274 arena
7275 .alloc(SLTNode::Binary(identity_offset, BinaryOp::Add, one64))
7276 .unwrap()
7277 } else {
7278 identity_offset
7279 };
7280 let one = constant(&mut arena, 1, width);
7281 let bit_mask = arena
7282 .alloc(SLTNode::Binary(one, BinaryOp::Shl, offset))
7283 .unwrap();
7284 let inverted_mask = arena
7285 .alloc(SLTNode::Unary(UnaryOp::BitNot, bit_mask))
7286 .unwrap();
7287 let preserved = arena
7288 .alloc(SLTNode::Binary(old_vector, BinaryOp::And, inverted_mask))
7289 .unwrap();
7290 let extended = if width == 1 {
7291 selected
7292 } else {
7293 let zero = constant(&mut arena, 0, width - 1);
7294 arena
7295 .alloc(SLTNode::Concat(vec![(zero, width - 1), (selected, 1)]))
7296 .unwrap()
7297 };
7298 let shifted = arena
7299 .alloc(SLTNode::Binary(extended, BinaryOp::Shl, offset))
7300 .unwrap();
7301 let inserted_bit = arena
7302 .alloc(SLTNode::Binary(shifted, BinaryOp::And, bit_mask))
7303 .unwrap();
7304 let inserted = arena
7305 .alloc(SLTNode::Binary(preserved, BinaryOp::Or, inserted_bit))
7306 .unwrap();
7307 let vector_update = arena
7308 .alloc(SLTNode::Mux {
7309 cond: if matches!(mutation, ScanMutation::DifferentActive) {
7310 source
7311 } else {
7312 active
7313 },
7314 then_expr: inserted,
7315 else_expr: old_vector,
7316 })
7317 .unwrap();
7318 let initial_vector = input(&mut arena, SCAN_VECTOR_STATE, width);
7319 let initial_found = constant(&mut arena, 0, 1);
7320 let group = arena
7321 .alloc(SLTNode::ForFoldGroup {
7322 loop_var: SCAN_LOOP,
7323 loop_width: 64,
7324 loop_signed: false,
7325 start: BigInt::from(0u8),
7326 step: BigInt::from(1u8),
7327 trip_count: width,
7328 entry_guard: guard,
7329 states: vec![
7330 SLTForFoldGroupState {
7331 target: VarAtomBase::new(SCAN_VECTOR_STATE, 0, width - 1),
7332 initial: initial_vector,
7333 update: vector_update,
7334 },
7335 SLTForFoldGroupState {
7336 target: VarAtomBase::new(SCAN_FOUND_STATE, 0, 0),
7337 initial: initial_found,
7338 update: found_update,
7339 },
7340 ],
7341 })
7342 .unwrap();
7343 (arena, group)
7344 }
7345
7346 fn lower_synthetic_scan(
7347 width: usize,
7348 mutation: ScanMutation,
7349 four_state: bool,
7350 ) -> (ExecutionUnit<u32>, RegisterId) {
7351 let (arena, group) = synthetic_or_scan_group(width, mutation);
7352 let mut builder = SIRBuilder::new();
7353 let result = SLTToSIRLowerer::new(four_state).lower(
7354 &mut builder,
7355 group,
7356 &arena,
7357 &mut crate::HashMap::default(),
7358 );
7359 (finish_lowering(builder), result)
7360 }
7361
7362 fn scan_reference(
7363 width: usize,
7364 source: u64,
7365 mask: u64,
7366 old: u64,
7367 bound: u64,
7368 unmasked: bool,
7369 mode: u64,
7370 guard: bool,
7371 ) -> (u64, bool) {
7372 if !guard {
7373 return (old, false);
7374 }
7375 let mut result = old;
7376 let mut found = false;
7377 for lane in 0..width {
7378 let active = (lane as u64) < bound && (unmasked || (mask >> lane) & 1 != 0);
7379 if active {
7380 let bit = (source >> lane) & 1 != 0;
7381 let selected = match mode {
7382 1 => !found && !bit,
7383 2 => !found && bit,
7384 _ => !found,
7385 };
7386 let lane_mask = 1u64 << lane;
7387 result = if selected {
7388 result | lane_mask
7389 } else {
7390 result & !lane_mask
7391 };
7392 found |= bit;
7393 }
7394 }
7395 (result, found)
7396 }
7397
7398 #[test]
7399 fn exact_two_state_or_scan_lowers_without_a_runtime_loop() {
7400 let (eu, _) = lower_synthetic_scan(8, ScanMutation::None, false);
7401 assert_eq!(branch_count(&eu), 0);
7402 assert_eq!(
7403 instruction_count(&eu, |instruction| matches!(
7404 instruction,
7405 SIRInstruction::Unary(_, UnaryOp::Or, _)
7406 )),
7407 1
7408 );
7409 }
7410
7411 #[test]
7412 fn unpacked_bit_scan_uses_explicit_packed_elements_loads() {
7413 let width = 32;
7414 let (arena, group) = synthetic_or_scan_group_with_layout(width, ScanMutation::None, true);
7415 let mut builder = SIRBuilder::new();
7416 SLTToSIRLowerer::new(false).lower(
7417 &mut builder,
7418 group,
7419 &arena,
7420 &mut crate::HashMap::default(),
7421 );
7422 let eu = finish_lowering(builder);
7423 let packed_loads = eu
7424 .blocks
7425 .values()
7426 .flat_map(|block| &block.instructions)
7427 .filter(|instruction| {
7428 matches!(
7429 instruction,
7430 SIRInstruction::Load(
7431 _,
7432 SCAN_SOURCE | SCAN_MASK,
7433 SIROffset::PackedElements {
7434 bit_offset: 0,
7435 element_width: 1
7436 },
7437 32
7438 )
7439 )
7440 })
7441 .count();
7442
7443 assert_eq!(packed_loads, 2);
7444 }
7445
7446 #[test]
7447 fn scan_entry_guard_constant_evaluation_uses_bitvector_width() {
7448 let width = 4;
7449 let old = 0b1010u64;
7450 let (eu, result) = lower_synthetic_scan(width, ScanMutation::OverflowFalseGuard, false);
7451 let memory = crate::HashMap::from_iter([
7452 (
7453 SCAN_VECTOR_STATE,
7454 TestSIRValue {
7455 payload: old.into(),
7456 mask: 0u8.into(),
7457 },
7458 ),
7459 (
7460 SCAN_SOURCE,
7461 TestSIRValue {
7462 payload: 0b1111u8.into(),
7463 mask: 0u8.into(),
7464 },
7465 ),
7466 (
7467 SCAN_MASK,
7468 TestSIRValue {
7469 payload: 0b1111u8.into(),
7470 mask: 0u8.into(),
7471 },
7472 ),
7473 (
7474 SCAN_BOUND,
7475 TestSIRValue {
7476 payload: width.into(),
7477 mask: 0u8.into(),
7478 },
7479 ),
7480 (
7481 SCAN_UNMASKED,
7482 TestSIRValue {
7483 payload: 1u8.into(),
7484 mask: 0u8.into(),
7485 },
7486 ),
7487 (
7488 SCAN_MODE,
7489 TestSIRValue {
7490 payload: 2u8.into(),
7491 mask: 0u8.into(),
7492 },
7493 ),
7494 ]);
7495
7496 assert_eq!(branch_count(&eu), 0);
7497 assert_eq!(
7498 execute_fold_group_sir_with_memory(&eu, &memory)[&result].payload,
7499 BigUint::from(old << 1)
7500 );
7501 }
7502
7503 #[test]
7504 fn word_scan_matches_the_sequential_first_true_semantics_exhaustively() {
7505 for width in 1..=4 {
7506 let (eu, result) = lower_synthetic_scan(width, ScanMutation::None, false);
7507 let values = 1u64 << width;
7508 for source in 0..values {
7509 for mask in 0..values {
7510 for old in 0..values {
7511 for bound in 0..=width as u64 {
7512 for unmasked in [false, true] {
7513 for mode in 1..=3 {
7514 for guard in [false, true] {
7515 let memory = crate::HashMap::from_iter([
7516 (
7517 SCAN_VECTOR_STATE,
7518 TestSIRValue {
7519 payload: old.into(),
7520 mask: 0u8.into(),
7521 },
7522 ),
7523 (
7524 SCAN_SOURCE,
7525 TestSIRValue {
7526 payload: source.into(),
7527 mask: 0u8.into(),
7528 },
7529 ),
7530 (
7531 SCAN_MASK,
7532 TestSIRValue {
7533 payload: mask.into(),
7534 mask: 0u8.into(),
7535 },
7536 ),
7537 (
7538 SCAN_BOUND,
7539 TestSIRValue {
7540 payload: bound.into(),
7541 mask: 0u8.into(),
7542 },
7543 ),
7544 (
7545 SCAN_UNMASKED,
7546 TestSIRValue {
7547 payload: u8::from(unmasked).into(),
7548 mask: 0u8.into(),
7549 },
7550 ),
7551 (
7552 SCAN_MODE,
7553 TestSIRValue {
7554 payload: mode.into(),
7555 mask: 0u8.into(),
7556 },
7557 ),
7558 (
7559 SCAN_GUARD,
7560 TestSIRValue {
7561 payload: u8::from(guard).into(),
7562 mask: 0u8.into(),
7563 },
7564 ),
7565 ]);
7566 let actual =
7567 &execute_fold_group_sir_with_memory(&eu, &memory)
7568 [&result]
7569 .payload;
7570 let (expected_vector, expected_found) = scan_reference(
7571 width, source, mask, old, bound, unmasked, mode, guard,
7572 );
7573 let expected =
7574 (expected_vector << 1) | u64::from(expected_found);
7575 assert_eq!(
7576 actual,
7577 &BigUint::from(expected),
7578 "width={width} source={source:#x} mask={mask:#x} old={old:#x} bound={bound} unmasked={unmasked} mode={mode} guard={guard}",
7579 );
7580 }
7581 }
7582 }
7583 }
7584 }
7585 }
7586 }
7587 }
7588 }
7589
7590 #[test]
7591 fn or_scan_matcher_rejects_every_near_miss_and_four_state_mode() {
7592 for mutation in [
7593 ScanMutation::DifferentActive,
7594 ScanMutation::NonIdentityOffset,
7595 ScanMutation::NonIdentityInputStride,
7596 ScanMutation::NarrowLoopMask,
7597 ScanMutation::WrongBeforeValue,
7598 ] {
7599 let (eu, _) = lower_synthetic_scan(4, mutation, false);
7600 assert!(branch_count(&eu) > 0);
7601 }
7602 let (eu, _) = lower_synthetic_scan(4, ScanMutation::None, true);
7603 assert!(branch_count(&eu) > 0);
7604 }
7605
7606 #[test]
7607 fn common_zero_controller_guards_rob_sized_lane_concat() {
7608 let mut arena = SLTNodeArena::new();
7609 let (root, guard, first_predicate) = guarded_lane_concat(&mut arena, 32, None);
7610 let lowerer = SLTToSIRLowerer::new(false);
7611 let mut builder = SIRBuilder::new();
7612 let mut cache = crate::HashMap::default();
7613 lowerer.reset_cost_cache(root, &arena, &cache, true);
7614 assert_eq!(
7615 lowerer
7616 .guarded_concat_plan(root, &arena, &cache)
7617 .expect("ROB-sized guarded scan must be profitable")
7618 .guard,
7619 guard,
7620 "the maximal compound guard must dominate its individual leaves"
7621 );
7622 let result = lowerer.lower(&mut builder, root, &arena, &mut cache);
7623
7624 assert_eq!(cache.get(&root), Some(&result));
7625 assert!(
7626 cache.contains_key(&guard),
7627 "the compound valid/store guard must dominate the outlined region"
7628 );
7629 assert!(
7630 !cache.contains_key(&first_predicate),
7631 "true-only values must be rolled back at the merge"
7632 );
7633
7634 let eu = finish_lowering(builder);
7635 assert_eq!(branch_count(&eu), 1, "one guard, not one branch per lane");
7636 assert_eq!(eu.blocks.len(), 4);
7637 let entry = &eu.blocks[&BlockId(0)];
7638 let SIRTerminator::Branch {
7639 true_block,
7640 false_block,
7641 ..
7642 } = &entry.terminator
7643 else {
7644 panic!("guarded concat entry must branch")
7645 };
7646 assert_eq!(
7647 entry
7648 .instructions
7649 .iter()
7650 .filter(|inst| matches!(inst, SIRInstruction::Load(..)))
7651 .count(),
7652 2,
7653 "only valid and is_store may be loaded before the branch"
7654 );
7655 assert_eq!(
7656 eu.blocks[&true_block.0]
7657 .instructions
7658 .iter()
7659 .filter(|inst| matches!(inst, SIRInstruction::Load(..)))
7660 .count(),
7661 32,
7662 "all lane inputs belong to the selected arm"
7663 );
7664 let false_instructions = &eu.blocks[&false_block.0].instructions;
7665 assert_eq!(false_instructions.len(), 1);
7666 assert!(matches!(
7667 &false_instructions[0],
7668 SIRInstruction::Imm(_, value) if value.payload.is_zero() && value.mask.is_zero()
7669 ));
7670 let merge = eu
7671 .blocks
7672 .values()
7673 .find(|block| block.params.contains(&result))
7674 .expect("guarded concat result must be a merge parameter");
7675 assert_eq!(eu.register_map[&merge.params[0]].width(), 32);
7676 }
7677
7678 #[test]
7679 fn guarded_concat_cfg_matches_eager_two_state_truth_table() {
7680 const LANES: usize = 4;
7681 let mut arena = SLTNodeArena::new();
7682 let (root, _, _) = guarded_lane_concat(&mut arena, LANES, None);
7683 let mut builder = SIRBuilder::new();
7684 let result = SLTToSIRLowerer::new(false).lower(
7685 &mut builder,
7686 root,
7687 &arena,
7688 &mut crate::HashMap::default(),
7689 );
7690 let eu = finish_lowering(builder);
7691 assert_eq!(branch_count(&eu), 1);
7692
7693 for valid in [false, true] {
7694 for is_store in [false, true] {
7695 for predicates in 0u8..(1 << LANES) {
7696 let mut memory = crate::HashMap::from_iter([
7697 (
7698 10_000,
7699 TestSIRValue {
7700 payload: u8::from(valid).into(),
7701 mask: 0u8.into(),
7702 },
7703 ),
7704 (
7705 10_001,
7706 TestSIRValue {
7707 payload: u8::from(is_store).into(),
7708 mask: 0u8.into(),
7709 },
7710 ),
7711 ]);
7712 for lane in 0..LANES {
7713 let predicate = predicates & (1 << lane) != 0;
7714 memory.insert(
7715 11_000 + lane as u32,
7716 TestSIRValue {
7717 payload: if predicate {
7718 BigUint::from(0x8000_0000_0000_0000u64)
7719 } else {
7720 BigUint::from(0u8)
7721 },
7722 mask: BigUint::from(0u8),
7723 },
7724 );
7725 }
7726 let actual = &execute_fold_group_sir_with_memory(&eu, &memory)[&result];
7727 let mut expected = 0u8;
7728 for lane in 0..LANES {
7729 expected <<= 1;
7730 expected |= u8::from(valid && is_store && predicates & (1 << lane) != 0);
7731 }
7732 assert_eq!(actual.payload, BigUint::from(expected));
7733 assert!(actual.mask.is_zero());
7734 }
7735 }
7736 }
7737 }
7738
7739 #[test]
7740 fn common_zero_controller_rejects_one_ungated_lane() {
7741 let mut arena = SLTNodeArena::new();
7742 let (root, _, _) = guarded_lane_concat(&mut arena, 32, Some(17));
7743 let mut builder = SIRBuilder::new();
7744 SLTToSIRLowerer::new(false).lower(
7745 &mut builder,
7746 root,
7747 &arena,
7748 &mut crate::HashMap::default(),
7749 );
7750 let eu = finish_lowering(builder);
7751
7752 assert_eq!(branch_count(&eu), 0);
7753 assert_eq!(eu.blocks.len(), 1);
7754 }
7755
7756 #[test]
7757 fn guarded_concat_recomputes_true_only_value_after_merge() {
7758 let mut arena = SLTNodeArena::new();
7759 let (root, guard, first_predicate) = guarded_lane_concat(&mut arena, 32, None);
7760 let lowerer = SLTToSIRLowerer::new(false);
7761 let mut builder = SIRBuilder::new();
7762 let mut cache = crate::HashMap::default();
7763 lowerer.lower(&mut builder, root, &arena, &mut cache);
7764 assert!(cache.contains_key(&guard));
7765 assert!(!cache.contains_key(&first_predicate));
7766
7767 let merge_block = builder.current_block();
7768 let predicate = lowerer.lower(&mut builder, first_predicate, &arena, &mut cache);
7769 assert_eq!(cache.get(&first_predicate), Some(&predicate));
7770 let eu = finish_lowering(builder);
7771 assert!(eu.verify_result().is_ok());
7772 assert!(eu.blocks[&merge_block].instructions.iter().any(|inst| {
7773 matches!(
7774 inst,
7775 SIRInstruction::Binary(dst, _, BinaryOp::GeU, _) if *dst == predicate
7776 )
7777 }));
7778 }
7779
7780 #[test]
7781 fn four_state_common_zero_controller_stays_eager() {
7782 let mut arena = SLTNodeArena::new();
7783 let (root, _, first_predicate) = guarded_lane_concat(&mut arena, 32, None);
7784 let mut builder = SIRBuilder::new();
7785 let mut cache = crate::HashMap::default();
7786 SLTToSIRLowerer::new(true).lower(&mut builder, root, &arena, &mut cache);
7787 assert!(cache.contains_key(&first_predicate));
7788 let eu = finish_lowering(builder);
7789
7790 assert_eq!(branch_count(&eu), 0);
7791 assert_eq!(eu.blocks.len(), 1);
7792 assert_eq!(
7793 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Load(..))),
7794 34
7795 );
7796 }
7797
7798 #[test]
7799 fn four_state_guarded_concat_retains_logical_and_mask_semantics() {
7800 const LANES: usize = 4;
7801 let mut arena = SLTNodeArena::new();
7802 let (root, _, _) = guarded_lane_concat(&mut arena, LANES, None);
7803 let mut builder = SIRBuilder::new();
7804 let result = SLTToSIRLowerer::new(true).lower(
7805 &mut builder,
7806 root,
7807 &arena,
7808 &mut crate::HashMap::default(),
7809 );
7810 let eu = finish_lowering(builder);
7811 assert_eq!(branch_count(&eu), 0);
7812
7813 for (guard_payload, guard_mask) in [(0u8, 0u8), (1, 0), (0, 1), (1, 1)] {
7815 for predicates in 0u8..(1 << LANES) {
7816 let mut memory = crate::HashMap::from_iter([
7817 (
7818 10_000,
7819 TestSIRValue {
7820 payload: guard_payload.into(),
7821 mask: guard_mask.into(),
7822 },
7823 ),
7824 (
7825 10_001,
7826 TestSIRValue {
7827 payload: 1u8.into(),
7828 mask: 0u8.into(),
7829 },
7830 ),
7831 ]);
7832 for lane in 0..LANES {
7833 let predicate = predicates & (1 << lane) != 0;
7834 memory.insert(
7835 11_000 + lane as u32,
7836 TestSIRValue {
7837 payload: if predicate {
7838 BigUint::from(0x8000_0000_0000_0000u64)
7839 } else {
7840 BigUint::from(0u8)
7841 },
7842 mask: BigUint::from(0u8),
7843 },
7844 );
7845 }
7846
7847 let actual = &execute_fold_group_sir_with_memory(&eu, &memory)[&result];
7848 let mut expected_payload = 0u8;
7849 let mut expected_mask = 0u8;
7850 for lane in 0..LANES {
7851 expected_payload <<= 1;
7852 expected_mask <<= 1;
7853 let predicate = predicates & (1 << lane) != 0;
7854 if guard_mask == 0 {
7855 expected_payload |= u8::from(guard_payload != 0 && predicate);
7856 } else if predicate {
7857 expected_mask |= 1;
7858 }
7859 }
7860 assert_eq!(actual.payload, BigUint::from(expected_payload));
7861 assert_eq!(actual.mask, BigUint::from(expected_mask));
7862 }
7863 }
7864 }
7865
7866 #[test]
7867 fn cheap_common_zero_controller_stays_eager() {
7868 let mut arena = SLTNodeArena::new();
7869 let valid = input(&mut arena, 0, 1);
7870 let is_store = input(&mut arena, 1, 1);
7871 let guard = arena
7872 .alloc(SLTNode::Binary(valid, BinaryOp::LogicAnd, is_store))
7873 .unwrap();
7874 let payload = input(&mut arena, 2, 1);
7875 let lane = arena
7876 .alloc(SLTNode::Binary(guard, BinaryOp::LogicAnd, payload))
7877 .unwrap();
7878 let root = arena.alloc(SLTNode::Concat(vec![(lane, 1)])).unwrap();
7879 let mut builder = SIRBuilder::new();
7880 SLTToSIRLowerer::new(false).lower(
7881 &mut builder,
7882 root,
7883 &arena,
7884 &mut crate::HashMap::default(),
7885 );
7886 let eu = finish_lowering(builder);
7887
7888 assert_eq!(branch_count(&eu), 0);
7889 }
7890
7891 #[test]
7892 fn zero_controller_analysis_uses_iterative_postorder_on_deep_dag() {
7893 let mut arena = SLTNodeArena::new();
7894 let valid = input(&mut arena, 0, 1);
7895 let is_store = input(&mut arena, 1, 1);
7896 let guard = arena
7897 .alloc(SLTNode::Binary(valid, BinaryOp::LogicAnd, is_store))
7898 .unwrap();
7899 let mut parts = Vec::new();
7900 for lane in 0..2 {
7901 let mut payload = input(&mut arena, 100 + lane, 1);
7902 for _ in 0..20_000 {
7903 payload = arena
7904 .alloc(SLTNode::Unary(UnaryOp::Ident, payload))
7905 .unwrap();
7906 }
7907 let gated = arena
7908 .alloc(SLTNode::Binary(guard, BinaryOp::LogicAnd, payload))
7909 .unwrap();
7910 parts.push((gated, 1));
7911 }
7912 let root = arena.alloc(SLTNode::Concat(parts)).unwrap();
7913 let lowerer = SLTToSIRLowerer::new(false);
7914 let cache = crate::HashMap::default();
7915 lowerer.reset_cost_cache(root, &arena, &cache, true);
7916
7917 let plan = lowerer
7918 .guarded_concat_plan(root, &arena, &cache)
7919 .expect("deep guarded concat must be analyzed without recursion");
7920 assert_eq!(plan.guard, guard);
7921 }
7922
7923 #[test]
7924 fn cheap_mux_stays_branchless() {
7925 let mut arena = SLTNodeArena::new();
7926 let cond = input(&mut arena, 0, 1);
7927 let then_expr = input(&mut arena, 1, 8);
7928 let else_expr = input(&mut arena, 2, 8);
7929 let mux = arena
7930 .alloc(SLTNode::Mux {
7931 cond,
7932 then_expr,
7933 else_expr,
7934 })
7935 .unwrap();
7936 let mut builder = SIRBuilder::new();
7937 SLTToSIRLowerer::new(false).lower(
7938 &mut builder,
7939 mux,
7940 &arena,
7941 &mut crate::HashMap::default(),
7942 );
7943 let eu = finish_lowering(builder);
7944
7945 assert_eq!(branch_count(&eu), 0);
7946 assert_eq!(
7947 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
7948 1
7949 );
7950 }
7951
7952 #[test]
7953 fn sequential_priority_index_becomes_clz_and_subtract() {
7954 let mut arena = SLTNodeArena::new();
7955 let mut acc = constant(&mut arena, u64::MAX, 64);
7956 for index in 0..8 {
7957 let cond = input(&mut arena, index, 1);
7958 let value = constant(&mut arena, index as u64, 64);
7959 acc = arena
7960 .alloc(SLTNode::Mux {
7961 cond,
7962 then_expr: value,
7963 else_expr: acc,
7964 })
7965 .unwrap();
7966 }
7967
7968 let mut builder = SIRBuilder::new();
7969 SLTToSIRLowerer::new(false).lower(
7970 &mut builder,
7971 acc,
7972 &arena,
7973 &mut crate::HashMap::default(),
7974 );
7975 let eu = finish_lowering(builder);
7976
7977 assert_eq!(
7978 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
7979 0
7980 );
7981 assert_eq!(
7982 instruction_count(&eu, |inst| matches!(
7983 inst,
7984 SIRInstruction::Unary(_, UnaryOp::CountLeadingZeros, _)
7985 )),
7986 1
7987 );
7988 assert_eq!(
7989 instruction_count(&eu, |inst| matches!(
7990 inst,
7991 SIRInstruction::Binary(_, _, BinaryOp::Sub, _)
7992 )),
7993 1
7994 );
7995 assert_eq!(
7996 instruction_count(&eu, |inst| matches!(
7997 inst,
7998 SIRInstruction::Concat(_, args) if args.len() == 8
7999 )),
8000 1
8001 );
8002 }
8003
8004 #[test]
8005 fn nested_conditional_priority_writes_use_combined_predicates() {
8006 let mut arena = SLTNodeArena::new();
8007 let mut acc = constant(&mut arena, u64::MAX, 64);
8008 for index in 0..8 {
8009 let outer = input(&mut arena, index * 2, 1);
8010 let inner = input(&mut arena, index * 2 + 1, 1);
8011 let value = constant(&mut arena, index as u64, 64);
8012 let write = arena
8013 .alloc(SLTNode::Mux {
8014 cond: inner,
8015 then_expr: value,
8016 else_expr: acc,
8017 })
8018 .unwrap();
8019 acc = arena
8020 .alloc(SLTNode::Mux {
8021 cond: outer,
8022 then_expr: write,
8023 else_expr: acc,
8024 })
8025 .unwrap();
8026 }
8027
8028 let mut builder = SIRBuilder::new();
8029 SLTToSIRLowerer::new(false).lower(
8030 &mut builder,
8031 acc,
8032 &arena,
8033 &mut crate::HashMap::default(),
8034 );
8035 let eu = finish_lowering(builder);
8036
8037 assert_eq!(
8038 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
8039 0
8040 );
8041 assert_eq!(
8042 instruction_count(&eu, |inst| matches!(
8043 inst,
8044 SIRInstruction::Binary(_, _, BinaryOp::LogicAnd, _)
8045 )),
8046 8
8047 );
8048 assert_eq!(
8049 instruction_count(&eu, |inst| matches!(
8050 inst,
8051 SIRInstruction::Unary(_, UnaryOp::CountLeadingZeros, _)
8052 )),
8053 1
8054 );
8055 }
8056
8057 #[test]
8058 fn first_write_found_recurrence_uses_outer_predicates_and_ctz() {
8059 let mut arena = SLTNodeArena::new();
8060 let mut acc = constant(&mut arena, u64::MAX, 64);
8061 let mut found = constant(&mut arena, 0, 1);
8062 let one = constant(&mut arena, 1, 1);
8063 for index in 0..8 {
8064 let outer = input(&mut arena, index, 1);
8065 let not_found = arena
8066 .alloc(SLTNode::Unary(UnaryOp::LogicNot, found))
8067 .unwrap();
8068 let value = constant(&mut arena, index as u64, 64);
8069 let write = arena
8070 .alloc(SLTNode::Mux {
8071 cond: not_found,
8072 then_expr: value,
8073 else_expr: acc,
8074 })
8075 .unwrap();
8076 acc = arena
8077 .alloc(SLTNode::Mux {
8078 cond: outer,
8079 then_expr: write,
8080 else_expr: acc,
8081 })
8082 .unwrap();
8083
8084 let set_found = arena
8085 .alloc(SLTNode::Mux {
8086 cond: not_found,
8087 then_expr: one,
8088 else_expr: found,
8089 })
8090 .unwrap();
8091 found = arena
8092 .alloc(SLTNode::Mux {
8093 cond: outer,
8094 then_expr: set_found,
8095 else_expr: found,
8096 })
8097 .unwrap();
8098 }
8099
8100 let mut builder = SIRBuilder::new();
8101 SLTToSIRLowerer::new(false).lower(
8102 &mut builder,
8103 acc,
8104 &arena,
8105 &mut crate::HashMap::default(),
8106 );
8107 let eu = finish_lowering(builder);
8108
8109 assert_eq!(
8110 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
8111 1,
8112 "only the zero-input sentinel select should remain"
8113 );
8114 assert_eq!(
8115 instruction_count(&eu, |inst| matches!(
8116 inst,
8117 SIRInstruction::Unary(_, UnaryOp::CountTrailingZeros, _)
8118 )),
8119 1
8120 );
8121 assert_eq!(
8122 instruction_count(&eu, |inst| matches!(
8123 inst,
8124 SIRInstruction::Concat(_, args) if args.len() == 8
8125 )),
8126 1
8127 );
8128 assert_eq!(
8129 instruction_count(&eu, |inst| matches!(
8130 inst,
8131 SIRInstruction::Unary(_, UnaryOp::LogicNot, _)
8132 )),
8133 0,
8134 "the found prefix recurrence must not be lowered"
8135 );
8136 }
8137
8138 #[test]
8139 fn conditionally_seeded_priority_count_preserves_fallback() {
8140 let width = 8;
8141 let result_width = UnaryOp::CountLeadingZeros.result_width(width);
8142 let mut arena = SLTNodeArena::new();
8143 let source = input(&mut arena, 0, width);
8144 let gate = input(&mut arena, 1, 1);
8145 let fallback = input(&mut arena, 2, result_width);
8146 let sentinel = constant(&mut arena, width as u64, result_width);
8147 let mut acc = arena
8148 .alloc(SLTNode::Mux {
8149 cond: gate,
8150 then_expr: sentinel,
8151 else_expr: fallback,
8152 })
8153 .unwrap();
8154
8155 for value in 0..width {
8156 let bit = arena
8157 .alloc(SLTNode::Slice {
8158 expr: source,
8159 access: BitAccess::new(width - 1 - value, width - 1 - value),
8160 })
8161 .unwrap();
8162 let unmatched = arena
8163 .alloc(SLTNode::Binary(acc, BinaryOp::Eq, sentinel))
8164 .unwrap();
8165 let write = arena
8166 .alloc(SLTNode::Binary(bit, BinaryOp::LogicAnd, unmatched))
8167 .unwrap();
8168 let value = constant(&mut arena, value as u64, result_width);
8169 let candidate = arena
8170 .alloc(SLTNode::Mux {
8171 cond: gate,
8172 then_expr: value,
8173 else_expr: acc,
8174 })
8175 .unwrap();
8176 acc = arena
8177 .alloc(SLTNode::Mux {
8178 cond: write,
8179 then_expr: candidate,
8180 else_expr: acc,
8181 })
8182 .unwrap();
8183 }
8184
8185 let mut builder = SIRBuilder::new();
8186 SLTToSIRLowerer::new(false).lower(
8187 &mut builder,
8188 acc,
8189 &arena,
8190 &mut crate::HashMap::default(),
8191 );
8192 let eu = finish_lowering(builder);
8193
8194 assert_eq!(
8195 instruction_count(&eu, |inst| matches!(
8196 inst,
8197 SIRInstruction::Unary(_, UnaryOp::CountLeadingZeros, _)
8198 )),
8199 1
8200 );
8201 assert_eq!(
8202 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
8203 1,
8204 "only gate ? clz(source) : fallback should remain"
8205 );
8206 }
8207
8208 #[derive(Clone, Copy, Debug)]
8209 enum ConditionalPriorityCorruption {
8210 StageGate(usize),
8211 SeedGate,
8212 SeedDefault,
8213 StageFallback(usize),
8214 BitOrder(usize),
8215 ValueOrder(usize),
8216 }
8217
8218 fn corrupted_conditional_priority(
8219 corruption: ConditionalPriorityCorruption,
8220 ) -> (SLTNodeArena<u32>, NodeId) {
8221 let width = 8;
8222 let result_width = UnaryOp::CountLeadingZeros.result_width(width);
8223 let mut arena = SLTNodeArena::new();
8224 let source = input(&mut arena, 0, width);
8225 let gate = input(&mut arena, 1, 1);
8226 let other_gate = input(&mut arena, 2, 1);
8227 let fallback = input(&mut arena, 3, result_width);
8228 let other_fallback = input(&mut arena, 4, result_width);
8229 let sentinel = constant(&mut arena, width as u64, result_width);
8230 let other_default = constant(&mut arena, width as u64 - 1, result_width);
8231 let seed_gate = if matches!(corruption, ConditionalPriorityCorruption::SeedGate) {
8232 other_gate
8233 } else {
8234 gate
8235 };
8236 let seed_default = if matches!(corruption, ConditionalPriorityCorruption::SeedDefault) {
8237 other_default
8238 } else {
8239 sentinel
8240 };
8241 let mut acc = arena
8242 .alloc(SLTNode::Mux {
8243 cond: seed_gate,
8244 then_expr: seed_default,
8245 else_expr: fallback,
8246 })
8247 .unwrap();
8248
8249 for stage in 0..width {
8250 let source_bit = if matches!(
8251 corruption,
8252 ConditionalPriorityCorruption::BitOrder(corrupt_stage)
8253 if corrupt_stage == stage
8254 ) {
8255 width - 1 - ((stage + 1) % width)
8256 } else {
8257 width - 1 - stage
8258 };
8259 let bit = arena
8260 .alloc(SLTNode::Slice {
8261 expr: source,
8262 access: BitAccess::new(source_bit, source_bit),
8263 })
8264 .unwrap();
8265 let unmatched = arena
8266 .alloc(SLTNode::Binary(acc, BinaryOp::Eq, sentinel))
8267 .unwrap();
8268 let write = arena
8269 .alloc(SLTNode::Binary(bit, BinaryOp::LogicAnd, unmatched))
8270 .unwrap();
8271 let selected_value = if matches!(
8272 corruption,
8273 ConditionalPriorityCorruption::ValueOrder(corrupt_stage)
8274 if corrupt_stage == stage
8275 ) {
8276 (stage + 1) % width
8277 } else {
8278 stage
8279 };
8280 let value = constant(&mut arena, selected_value as u64, result_width);
8281 let stage_gate = if matches!(
8282 corruption,
8283 ConditionalPriorityCorruption::StageGate(corrupt_stage)
8284 if corrupt_stage == stage
8285 ) {
8286 other_gate
8287 } else {
8288 gate
8289 };
8290 let stage_fallback = if matches!(
8291 corruption,
8292 ConditionalPriorityCorruption::StageFallback(corrupt_stage)
8293 if corrupt_stage == stage
8294 ) {
8295 other_fallback
8296 } else {
8297 acc
8298 };
8299 let candidate = arena
8300 .alloc(SLTNode::Mux {
8301 cond: stage_gate,
8302 then_expr: value,
8303 else_expr: stage_fallback,
8304 })
8305 .unwrap();
8306 acc = arena
8307 .alloc(SLTNode::Mux {
8308 cond: write,
8309 then_expr: candidate,
8310 else_expr: acc,
8311 })
8312 .unwrap();
8313 }
8314 (arena, acc)
8315 }
8316
8317 fn assert_conditional_priority_rejected(corruption: ConditionalPriorityCorruption) {
8318 let (arena, root) = corrupted_conditional_priority(corruption);
8319 assert!(
8320 match_slt_priority_count(root, &arena).is_none(),
8321 "conditionally seeded priority chain with {corruption:?} must not match"
8322 );
8323 }
8324
8325 #[test]
8326 fn conditional_priority_rejects_mismatched_per_stage_gate() {
8327 assert_conditional_priority_rejected(ConditionalPriorityCorruption::StageGate(3));
8328 }
8329
8330 #[test]
8331 fn conditional_priority_rejects_mismatched_seed_gate_and_default() {
8332 assert_conditional_priority_rejected(ConditionalPriorityCorruption::SeedGate);
8333 assert_conditional_priority_rejected(ConditionalPriorityCorruption::SeedDefault);
8334 }
8335
8336 #[test]
8337 fn conditional_priority_rejects_mismatched_stage_fallback() {
8338 assert_conditional_priority_rejected(ConditionalPriorityCorruption::StageFallback(3));
8339 }
8340
8341 #[test]
8342 fn conditional_priority_rejects_reordered_bit_or_value() {
8343 assert_conditional_priority_rejected(ConditionalPriorityCorruption::BitOrder(3));
8344 assert_conditional_priority_rejected(ConditionalPriorityCorruption::ValueOrder(3));
8345 }
8346
8347 #[test]
8348 fn additive_popcount_accepts_only_lsb_zero_extension() {
8349 let mut arena = SLTNodeArena::new();
8350 let bit = input(&mut arena, 0, 1);
8351 let zero3 = constant(&mut arena, 0, 3);
8352 let lsb_extended = arena
8353 .alloc(SLTNode::Concat(vec![(zero3, 3), (bit, 1)]))
8354 .unwrap();
8355 let msb_shifted = arena
8356 .alloc(SLTNode::Concat(vec![(bit, 1), (zero3, 3)]))
8357 .unwrap();
8358 let wide = input(&mut arena, 1, 4);
8359 let multi_bit_slice = arena
8360 .alloc(SLTNode::Slice {
8361 expr: wide,
8362 access: BitAccess::new(0, 1),
8363 })
8364 .unwrap();
8365
8366 assert!(resolve_slt_extended_bit(lsb_extended, &arena).is_some());
8367 assert!(resolve_slt_extended_bit(msb_shifted, &arena).is_none());
8368 assert!(resolve_slt_extended_bit(multi_bit_slice, &arena).is_none());
8369 }
8370
8371 #[test]
8372 fn procedural_truth_unwrap_keeps_wide_reduction() {
8373 let mut arena = SLTNodeArena::new();
8374 let wide = input(&mut arena, 0, 4);
8375 let truth = arena.alloc(SLTNode::Unary(UnaryOp::Or, wide)).unwrap();
8376 let normalized = arena
8377 .alloc(SLTNode::Unary(UnaryOp::ToTwoState, truth))
8378 .unwrap();
8379
8380 assert_eq!(
8381 unwrap_slt_one_bit_procedural_truth(normalized, &arena),
8382 normalized,
8383 "a wide reduction is a real booleanization, not an identity"
8384 );
8385 }
8386
8387 #[test]
8388 fn cached_popcount_accumulator_only_lowers_new_increment() {
8389 let width = 8;
8390 let result_width = 4;
8391 let mut arena = SLTNodeArena::new();
8392 let source = input(&mut arena, 0, width);
8393 let one = constant(&mut arena, 1, result_width);
8394 let mut base = constant(&mut arena, 0, result_width);
8395 for bit in 0..width {
8396 let predicate = arena
8397 .alloc(SLTNode::Slice {
8398 expr: source,
8399 access: BitAccess::new(bit, bit),
8400 })
8401 .unwrap();
8402 let incremented = arena
8403 .alloc(SLTNode::Binary(base, BinaryOp::Add, one))
8404 .unwrap();
8405 base = arena
8406 .alloc(SLTNode::Mux {
8407 cond: predicate,
8408 then_expr: incremented,
8409 else_expr: base,
8410 })
8411 .unwrap();
8412 }
8413
8414 let delta = input(&mut arena, 1, 1);
8415 let incremented = arena
8416 .alloc(SLTNode::Binary(base, BinaryOp::Add, one))
8417 .unwrap();
8418 let root = arena
8419 .alloc(SLTNode::Mux {
8420 cond: delta,
8421 then_expr: incremented,
8422 else_expr: base,
8423 })
8424 .unwrap();
8425
8426 let mut builder = SIRBuilder::new();
8427 let mut cache = crate::HashMap::default();
8428 let lowerer = SLTToSIRLowerer::new(false);
8429 let base_reg = lowerer.lower(&mut builder, base, &arena, &mut cache);
8430 let root_reg = lowerer.lower(&mut builder, root, &arena, &mut cache);
8431 let eu = finish_lowering(builder);
8432
8433 assert_eq!(
8434 instruction_count(&eu, |instruction| matches!(
8435 instruction,
8436 SIRInstruction::Unary(_, UnaryOp::PopCount, _)
8437 )),
8438 1,
8439 "the already materialized population count must not be rebuilt"
8440 );
8441 assert_eq!(
8442 instruction_count(&eu, |instruction| matches!(
8443 instruction,
8444 SIRInstruction::Concat(_, arguments) if arguments.len() == width + 1
8445 )),
8446 0
8447 );
8448 assert!(
8449 eu.blocks
8450 .values()
8451 .flat_map(|block| &block.instructions)
8452 .any(|instruction| matches!(
8453 instruction,
8454 SIRInstruction::Binary(dst, lhs, BinaryOp::Add, _)
8455 if *dst == root_reg && *lhs == base_reg
8456 ))
8457 );
8458 }
8459
8460 #[test]
8461 fn cached_additive_popcount_accumulator_only_lowers_new_bit() {
8462 let width = 8;
8463 let result_width = 4;
8464 let mut arena = SLTNodeArena::new();
8465 let source = input(&mut arena, 0, width);
8466 let zero = constant(&mut arena, 0, result_width);
8467 let padding = constant(&mut arena, 0, result_width - 1);
8468 let mut base = zero;
8469 for bit in 0..width {
8470 let predicate = arena
8471 .alloc(SLTNode::Slice {
8472 expr: source,
8473 access: BitAccess::new(bit, bit),
8474 })
8475 .unwrap();
8476 let extended = arena
8477 .alloc(SLTNode::Concat(vec![
8478 (padding, result_width - 1),
8479 (predicate, 1),
8480 ]))
8481 .unwrap();
8482 base = arena
8483 .alloc(SLTNode::Binary(base, BinaryOp::Add, extended))
8484 .unwrap();
8485 }
8486
8487 let delta = input(&mut arena, 1, 1);
8488 let extended_delta = arena
8489 .alloc(SLTNode::Concat(vec![
8490 (padding, result_width - 1),
8491 (delta, 1),
8492 ]))
8493 .unwrap();
8494 let root = arena
8495 .alloc(SLTNode::Binary(base, BinaryOp::Add, extended_delta))
8496 .unwrap();
8497
8498 let mut builder = SIRBuilder::new();
8499 let mut cache = crate::HashMap::default();
8500 let lowerer = SLTToSIRLowerer::new(false);
8501 let base_reg = lowerer.lower(&mut builder, base, &arena, &mut cache);
8502 let root_reg = lowerer.lower(&mut builder, root, &arena, &mut cache);
8503 let eu = finish_lowering(builder);
8504
8505 assert_eq!(
8506 instruction_count(&eu, |instruction| matches!(
8507 instruction,
8508 SIRInstruction::Unary(_, UnaryOp::PopCount, _)
8509 )),
8510 1
8511 );
8512 assert!(
8513 eu.blocks
8514 .values()
8515 .flat_map(|block| &block.instructions)
8516 .any(|instruction| matches!(
8517 instruction,
8518 SIRInstruction::Binary(dst, lhs, BinaryOp::Add, _)
8519 if *dst == root_reg && *lhs == base_reg
8520 ))
8521 );
8522 }
8523
8524 #[test]
8525 fn cached_popcount_delta_preserves_wrapping_semantics() {
8526 let width = 7;
8527 let result_width = 3;
8528 let mut arena = SLTNodeArena::new();
8529 let source = input(&mut arena, 0, width);
8530 let one = constant(&mut arena, 1, result_width);
8531 let mut base = constant(&mut arena, 0, result_width);
8532 for bit in 0..width {
8533 let predicate = arena
8534 .alloc(SLTNode::Slice {
8535 expr: source,
8536 access: BitAccess::new(bit, bit),
8537 })
8538 .unwrap();
8539 let incremented = arena
8540 .alloc(SLTNode::Binary(base, BinaryOp::Add, one))
8541 .unwrap();
8542 base = arena
8543 .alloc(SLTNode::Mux {
8544 cond: predicate,
8545 then_expr: incremented,
8546 else_expr: base,
8547 })
8548 .unwrap();
8549 }
8550 let delta = input(&mut arena, 1, 1);
8551 let incremented = arena
8552 .alloc(SLTNode::Binary(base, BinaryOp::Add, one))
8553 .unwrap();
8554 let root = arena
8555 .alloc(SLTNode::Mux {
8556 cond: delta,
8557 then_expr: incremented,
8558 else_expr: base,
8559 })
8560 .unwrap();
8561
8562 let mut builder = SIRBuilder::new();
8563 let mut cache = crate::HashMap::default();
8564 let lowerer = SLTToSIRLowerer::new(false);
8565 lowerer.lower(&mut builder, base, &arena, &mut cache);
8566 let result = lowerer.lower(&mut builder, root, &arena, &mut cache);
8567 let eu = finish_lowering(builder);
8568
8569 assert_eq!(
8570 instruction_count(&eu, |instruction| matches!(
8571 instruction,
8572 SIRInstruction::Unary(_, UnaryOp::PopCount, _)
8573 )),
8574 1
8575 );
8576
8577 for source_value in 0u64..(1 << width) {
8578 for delta_value in 0u64..=1 {
8579 let mut memory = crate::HashMap::default();
8580 memory.insert(
8581 0u32,
8582 TestSIRValue {
8583 payload: source_value.into(),
8584 mask: 0u8.into(),
8585 },
8586 );
8587 memory.insert(
8588 1u32,
8589 TestSIRValue {
8590 payload: delta_value.into(),
8591 mask: 0u8.into(),
8592 },
8593 );
8594 let actual = &execute_fold_group_sir_with_memory(&eu, &memory)[&result].payload;
8595 let expected =
8596 (source_value.count_ones() as u64 + delta_value) & ((1 << result_width) - 1);
8597 assert_eq!(actual, &BigUint::from(expected));
8598 }
8599 }
8600 }
8601
8602 #[test]
8603 fn cached_accumulator_is_not_reused_for_non_unit_update() {
8604 let width = 8;
8605 let result_width = 4;
8606 let mut arena = SLTNodeArena::new();
8607 let source = input(&mut arena, 0, width);
8608 let one = constant(&mut arena, 1, result_width);
8609 let two = constant(&mut arena, 2, result_width);
8610 let mut base = constant(&mut arena, 0, result_width);
8611 for bit in 0..width {
8612 let predicate = arena
8613 .alloc(SLTNode::Slice {
8614 expr: source,
8615 access: BitAccess::new(bit, bit),
8616 })
8617 .unwrap();
8618 let incremented = arena
8619 .alloc(SLTNode::Binary(base, BinaryOp::Add, one))
8620 .unwrap();
8621 base = arena
8622 .alloc(SLTNode::Mux {
8623 cond: predicate,
8624 then_expr: incremented,
8625 else_expr: base,
8626 })
8627 .unwrap();
8628 }
8629 let delta = input(&mut arena, 1, 1);
8630 let incremented = arena
8631 .alloc(SLTNode::Binary(base, BinaryOp::Add, two))
8632 .unwrap();
8633 let root = arena
8634 .alloc(SLTNode::Mux {
8635 cond: delta,
8636 then_expr: incremented,
8637 else_expr: base,
8638 })
8639 .unwrap();
8640
8641 let mut builder = SIRBuilder::new();
8642 let mut cache = crate::HashMap::default();
8643 let lowerer = SLTToSIRLowerer::new(false);
8644 lowerer.lower(&mut builder, base, &arena, &mut cache);
8645 lowerer.lower(&mut builder, root, &arena, &mut cache);
8646 let eu = finish_lowering(builder);
8647
8648 assert_eq!(
8649 instruction_count(&eu, |instruction| matches!(
8650 instruction,
8651 SIRInstruction::Mux(..)
8652 )),
8653 1,
8654 "only an exact conditional +1 update may become a count delta"
8655 );
8656 }
8657
8658 #[test]
8659 fn active_bit_predicate_family_becomes_one_wide_expression() {
8660 let width = 8;
8661 let mut arena = SLTNodeArena::new();
8662 let bound = input(&mut arena, 0, 4);
8663 let vm = input(&mut arena, 1, 1);
8664 let zero = constant(&mut arena, 0, 4);
8665 let one = constant(&mut arena, 1, 4);
8666 let mut acc = zero;
8667
8668 for index in 0..width {
8669 let index_value = constant(&mut arena, index as u64, 4);
8670 let in_range = arena
8671 .alloc(SLTNode::Binary(index_value, BinaryOp::LtU, bound))
8672 .unwrap();
8673 let mask = input_bit(&mut arena, 2, index);
8674 let enabled = arena
8675 .alloc(SLTNode::Binary(vm, BinaryOp::LogicOr, mask))
8676 .unwrap();
8677 let eligible = arena
8678 .alloc(SLTNode::Binary(in_range, BinaryOp::LogicAnd, enabled))
8679 .unwrap();
8680 let source = input_bit(&mut arena, 3, index);
8681 let active = arena
8682 .alloc(SLTNode::Binary(eligible, BinaryOp::LogicAnd, source))
8683 .unwrap();
8684 let incremented = arena
8685 .alloc(SLTNode::Binary(acc, BinaryOp::Add, one))
8686 .unwrap();
8687 acc = arena
8688 .alloc(SLTNode::Mux {
8689 cond: active,
8690 then_expr: incremented,
8691 else_expr: acc,
8692 })
8693 .unwrap();
8694 }
8695
8696 let mut builder = SIRBuilder::new();
8697 SLTToSIRLowerer::new(false).lower(
8698 &mut builder,
8699 acc,
8700 &arena,
8701 &mut crate::HashMap::default(),
8702 );
8703 let eu = finish_lowering(builder);
8704
8705 assert_eq!(
8706 instruction_count(&eu, |inst| matches!(
8707 inst,
8708 SIRInstruction::Unary(_, UnaryOp::PopCount, _)
8709 )),
8710 1
8711 );
8712 assert_eq!(
8713 instruction_count(&eu, |inst| matches!(
8714 inst,
8715 SIRInstruction::Binary(_, _, BinaryOp::LtU, _)
8716 )),
8717 0,
8718 "the ordered comparison ladder must become a low-ones mask"
8719 );
8720 assert_eq!(
8721 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
8722 1,
8723 "the saturated low-ones mask needs one word-level select"
8724 );
8725 assert_eq!(
8726 instruction_count(&eu, |inst| matches!(
8727 inst,
8728 SIRInstruction::Concat(_, args) if args.len() == width
8729 )),
8730 0,
8731 "the scalar active predicates must not be reassembled one bit at a time"
8732 );
8733 }
8734
8735 #[test]
8736 fn low_ones_saturates_when_only_a_wide_bound_high_limb_is_set() {
8737 let mut arena = SLTNodeArena::new();
8738 let bound = arena
8739 .alloc(SLTNode::Constant(
8740 BigUint::from(1u8) << 64,
8741 BigUint::from(0u8),
8742 128,
8743 false,
8744 ))
8745 .unwrap();
8746 let mut builder = SIRBuilder::new();
8747 let result = SLTToSIRLowerer::new(false).lower_slt_vector_expr(
8748 &mut builder,
8749 SLTVectorExpr::LowOnes { bound },
8750 8,
8751 &arena,
8752 &mut crate::HashMap::default(),
8753 true,
8754 );
8755 let eu = finish_lowering(builder);
8756
8757 assert_eq!(
8758 execute_fold_group_sir(&eu)[&result].payload,
8759 BigUint::from(0xffu8)
8760 );
8761 assert_eq!(
8762 instruction_count(&eu, |instruction| matches!(
8763 instruction,
8764 SIRInstruction::Binary(_, _, BinaryOp::GeU, _)
8765 )),
8766 1
8767 );
8768 }
8769
8770 #[test]
8771 fn compacted_unpacked_input_preserves_packed_elements_provenance() {
8772 let variable = 42u32;
8773 let mut arena = SLTNodeArena::new();
8774 let representative = arena
8775 .alloc(SLTNode::Input {
8776 variable,
8777 signed: false,
8778 index: Vec::new(),
8779 access: BitAccess::new(0, 0),
8780 })
8781 .unwrap();
8782 let element_widths = crate::HashMap::from_iter([(variable, 1)]);
8783 let lowerer =
8784 SLTToSIRLowerer::new(false).with_unpacked_input_types(&arena, &element_widths);
8785 let mut builder = SIRBuilder::new();
8786 lowerer.lower_slt_vector_expr(
8787 &mut builder,
8788 SLTVectorExpr::Origin(SLTBitOrigin::Input {
8789 node: representative,
8790 variable,
8791 signed: false,
8792 index: Vec::new(),
8793 }),
8794 32,
8795 &arena,
8796 &mut crate::HashMap::default(),
8797 true,
8798 );
8799 let eu = finish_lowering(builder);
8800
8801 assert!(matches!(
8802 eu.blocks[&eu.entry_block_id].instructions.as_slice(),
8803 [SIRInstruction::Load(
8804 _,
8805 42,
8806 SIROffset::PackedElements {
8807 bit_offset: 0,
8808 element_width: 1,
8809 },
8810 32,
8811 )]
8812 ));
8813 }
8814
8815 #[test]
8816 fn masked_found_recurrence_becomes_wide_or_reduction() {
8817 let width = 8;
8818 let mut arena = SLTNodeArena::new();
8819 let bound = input(&mut arena, 0, 4);
8820 let vm = input(&mut arena, 1, 1);
8821 let mut found = constant(&mut arena, 0, 1);
8822
8823 for index in 0..width {
8824 let index_value = constant(&mut arena, index as u64, 4);
8825 let in_range = arena
8826 .alloc(SLTNode::Binary(index_value, BinaryOp::LtU, bound))
8827 .unwrap();
8828 let mask = input_bit(&mut arena, 2, index);
8829 let enabled = arena
8830 .alloc(SLTNode::Binary(vm, BinaryOp::LogicOr, mask))
8831 .unwrap();
8832 let eligible = arena
8833 .alloc(SLTNode::Binary(in_range, BinaryOp::LogicAnd, enabled))
8834 .unwrap();
8835 let source = input_bit(&mut arena, 3, index);
8836 let set = arena
8837 .alloc(SLTNode::Binary(found, BinaryOp::LogicOr, source))
8838 .unwrap();
8839 found = arena
8840 .alloc(SLTNode::Mux {
8841 cond: eligible,
8842 then_expr: set,
8843 else_expr: found,
8844 })
8845 .unwrap();
8846 }
8847
8848 let mut builder = SIRBuilder::new();
8849 SLTToSIRLowerer::new(false).lower(
8850 &mut builder,
8851 found,
8852 &arena,
8853 &mut crate::HashMap::default(),
8854 );
8855 let eu = finish_lowering(builder);
8856
8857 assert_eq!(
8858 instruction_count(&eu, |inst| matches!(
8859 inst,
8860 SIRInstruction::Unary(_, UnaryOp::Or, _)
8861 )),
8862 1
8863 );
8864 assert_eq!(
8865 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
8866 1,
8867 "the saturated low-ones mask needs one word-level select"
8868 );
8869 assert_eq!(
8870 instruction_count(&eu, |inst| matches!(
8871 inst,
8872 SIRInstruction::Binary(_, _, BinaryOp::LtU, _)
8873 )),
8874 0
8875 );
8876 }
8877
8878 #[test]
8879 fn expected_cost_uses_static_equality_probability() {
8880 let even = StaticBranchProbability::EVEN;
8881 let equality = StaticBranchProbability {
8882 true_weight: 1,
8883 total_weight: 5,
8884 };
8885
8886 assert!(!SLTToSIRLowerer::mux_cfg_is_profitable(10, 0, 64, even));
8890 assert!(SLTToSIRLowerer::mux_cfg_is_profitable(10, 0, 64, equality));
8891 assert!(!SLTToSIRLowerer::mux_cfg_is_profitable(
8892 10,
8893 0,
8894 64,
8895 equality.inverted(),
8896 ));
8897 }
8898
8899 #[test]
8900 fn wildcard_equality_uses_the_decoder_bias() {
8901 let mut arena = SLTNodeArena::new();
8902 let selector = input(&mut arena, 0, 8);
8903 let opcode = constant(&mut arena, 0x13, 8);
8904 let eq = arena
8905 .alloc(SLTNode::Binary(selector, BinaryOp::EqWildcard, opcode))
8906 .unwrap();
8907 let ne = arena
8908 .alloc(SLTNode::Binary(selector, BinaryOp::NeWildcard, opcode))
8909 .unwrap();
8910
8911 let eq_probability = SLTToSIRLowerer::static_true_probability(eq, &arena);
8912 let ne_probability = SLTToSIRLowerer::static_true_probability(ne, &arena);
8913 assert_eq!(
8914 (eq_probability.true_weight, eq_probability.total_weight),
8915 (1, 5)
8916 );
8917 assert_eq!(
8918 (ne_probability.true_weight, ne_probability.total_weight),
8919 (4, 5)
8920 );
8921 }
8922
8923 #[test]
8924 fn expensive_mux_preserves_control_flow_and_verifies() {
8925 let mut arena = SLTNodeArena::new();
8926 let cond = input(&mut arena, 0, 1);
8927 let then_input = input(&mut arena, 1, 64);
8928 let else_input = input(&mut arena, 2, 64);
8929 let then_expr = operation_chain(&mut arena, then_input, BinaryOp::Add, 8, 10, 64);
8930 let else_expr = operation_chain(&mut arena, else_input, BinaryOp::Xor, 12, 100, 64);
8931 let mux = arena
8932 .alloc(SLTNode::Mux {
8933 cond,
8934 then_expr,
8935 else_expr,
8936 })
8937 .unwrap();
8938 let mut builder = SIRBuilder::new();
8939 SLTToSIRLowerer::new(false).lower(
8940 &mut builder,
8941 mux,
8942 &arena,
8943 &mut crate::HashMap::default(),
8944 );
8945 let eu = finish_lowering(builder);
8946
8947 assert_eq!(branch_count(&eu), 1);
8948 assert_eq!(eu.blocks.len(), 4);
8949 assert_eq!(
8950 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
8951 0
8952 );
8953 }
8954
8955 #[test]
8956 fn shared_arm_dag_is_hoisted_once() {
8957 let mut arena = SLTNodeArena::new();
8958 let cond = input(&mut arena, 0, 1);
8959 let source = input(&mut arena, 1, 64);
8960 let shared = operation_chain(&mut arena, source, BinaryOp::Mul, 3, 3, 64);
8961 let then_source = input(&mut arena, 2, 64);
8962 let else_source = input(&mut arena, 3, 64);
8963 let then_unique = operation_chain(&mut arena, then_source, BinaryOp::Add, 5, 20, 64);
8964 let else_unique = operation_chain(&mut arena, else_source, BinaryOp::Sub, 5, 40, 64);
8965 let then_expr = arena
8966 .alloc(SLTNode::Binary(shared, BinaryOp::Add, then_unique))
8967 .unwrap();
8968 let else_expr = arena
8969 .alloc(SLTNode::Binary(shared, BinaryOp::Sub, else_unique))
8970 .unwrap();
8971 let mux = arena
8972 .alloc(SLTNode::Mux {
8973 cond,
8974 then_expr,
8975 else_expr,
8976 })
8977 .unwrap();
8978 let mut builder = SIRBuilder::new();
8979 SLTToSIRLowerer::new(false).lower(
8980 &mut builder,
8981 mux,
8982 &arena,
8983 &mut crate::HashMap::default(),
8984 );
8985 let eu = finish_lowering(builder);
8986
8987 assert_eq!(branch_count(&eu), 1);
8988 assert_eq!(
8989 instruction_count(&eu, |inst| matches!(
8990 inst,
8991 SIRInstruction::Binary(_, _, BinaryOp::Mul, _)
8992 )),
8993 3,
8994 );
8995 let entry = &eu.blocks[&BlockId(0)];
8996 assert_eq!(
8997 entry
8998 .instructions
8999 .iter()
9000 .filter(|inst| matches!(inst, SIRInstruction::Binary(_, _, BinaryOp::Mul, _)))
9001 .count(),
9002 3,
9003 );
9004 }
9005
9006 #[test]
9007 fn nested_cost_directed_muxes_form_valid_ssa() {
9008 let mut arena = SLTNodeArena::new();
9009 let outer_cond = input(&mut arena, 0, 1);
9010 let inner_cond = input(&mut arena, 1, 1);
9011 let a = input(&mut arena, 2, 64);
9012 let b = input(&mut arena, 3, 64);
9013 let c = input(&mut arena, 4, 64);
9014 let inner_then = operation_chain(&mut arena, a, BinaryOp::Add, 8, 10, 64);
9015 let inner_else = operation_chain(&mut arena, b, BinaryOp::Sub, 8, 30, 64);
9016 let inner = arena
9017 .alloc(SLTNode::Mux {
9018 cond: inner_cond,
9019 then_expr: inner_then,
9020 else_expr: inner_else,
9021 })
9022 .unwrap();
9023 let outer_else = operation_chain(&mut arena, c, BinaryOp::Xor, 16, 70, 64);
9024 let outer = arena
9025 .alloc(SLTNode::Mux {
9026 cond: outer_cond,
9027 then_expr: inner,
9028 else_expr: outer_else,
9029 })
9030 .unwrap();
9031 let mut builder = SIRBuilder::new();
9032 SLTToSIRLowerer::new(false).lower(
9033 &mut builder,
9034 outer,
9035 &arena,
9036 &mut crate::HashMap::default(),
9037 );
9038 let eu = finish_lowering(builder);
9039
9040 assert_eq!(branch_count(&eu), 2);
9041 }
9042
9043 #[test]
9044 fn deep_division_forces_cfg_and_casts_merge_width() {
9045 let mut arena = SLTNodeArena::new();
9046 let cond = input(&mut arena, 0, 1);
9047 let narrow = input(&mut arena, 1, 8);
9048 let numerator = input(&mut arena, 2, 16);
9049 let denominator = input(&mut arena, 3, 16);
9050 let quotient = arena
9051 .alloc(SLTNode::Binary(numerator, BinaryOp::DivU, denominator))
9052 .unwrap();
9053 let one = constant(&mut arena, 1, 16);
9054 let deep_division = arena
9055 .alloc(SLTNode::Binary(quotient, BinaryOp::Add, one))
9056 .unwrap();
9057 let mux = arena
9058 .alloc(SLTNode::Mux {
9059 cond,
9060 then_expr: narrow,
9061 else_expr: deep_division,
9062 })
9063 .unwrap();
9064 let mut builder = SIRBuilder::new();
9065 SLTToSIRLowerer::new(false).lower(
9066 &mut builder,
9067 mux,
9068 &arena,
9069 &mut crate::HashMap::default(),
9070 );
9071 let eu = finish_lowering(builder);
9072
9073 assert_eq!(branch_count(&eu), 1);
9074 let merge = eu
9075 .blocks
9076 .values()
9077 .find(|block| !block.params.is_empty())
9078 .unwrap();
9079 assert_eq!(eu.register_map[&merge.params[0]].width(), 16);
9080 }
9081
9082 #[test]
9083 fn four_state_expensive_mux_keeps_xz_select_semantics() {
9084 let mut arena = SLTNodeArena::new();
9085 let cond = input(&mut arena, 0, 1);
9086 let then_input = input(&mut arena, 1, 64);
9087 let else_input = input(&mut arena, 2, 64);
9088 let then_expr = operation_chain(&mut arena, then_input, BinaryOp::Add, 10, 10, 64);
9089 let else_expr = operation_chain(&mut arena, else_input, BinaryOp::Sub, 10, 30, 64);
9090 let mux = arena
9091 .alloc(SLTNode::Mux {
9092 cond,
9093 then_expr,
9094 else_expr,
9095 })
9096 .unwrap();
9097 let mut builder = SIRBuilder::new();
9098 SLTToSIRLowerer::new(true).lower(&mut builder, mux, &arena, &mut crate::HashMap::default());
9099 let eu = finish_lowering(builder);
9100
9101 assert_eq!(branch_count(&eu), 0);
9102 assert_eq!(
9103 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
9104 1
9105 );
9106 }
9107
9108 #[test]
9109 fn for_fold_is_not_a_pure_mux_arm() {
9110 let mut arena = SLTNodeArena::new();
9111 let initial = input(&mut arena, 0, 8);
9112 let update = input(&mut arena, 1, 8);
9113 let continue_cond = constant(&mut arena, 1, 1);
9114 let target = VarAtomBase::new(2, 0, 7);
9115 let fold = arena
9116 .alloc(SLTNode::ForFold {
9117 loop_var: 3,
9118 loop_width: 8,
9119 loop_signed: false,
9120 start: SLTLoopBound::Const(0),
9121 end: SLTLoopBound::Const(2),
9122 inclusive: false,
9123 step: 1,
9124 step_op: SLTStepOp::Add,
9125 reverse: false,
9126 result: crate::SLTForFoldResult::State(target),
9127 initials: vec![crate::SLTForUpdate {
9128 target,
9129 expr: initial,
9130 }],
9131 updates: vec![crate::SLTForUpdate {
9132 target,
9133 expr: update,
9134 }],
9135 effects: vec![crate::SLTForEffect::Event {
9136 site_id: 1,
9137 guard: None,
9138 emit_on_true: true,
9139 args: vec![update],
9140 fatal_error_code: None,
9141 }],
9142 continue_cond,
9143 })
9144 .unwrap();
9145
9146 assert!(!SLTToSIRLowerer::new(false).is_speculatable_pure(fold, &arena));
9147 }
9148
9149 #[test]
9150 fn joint_fold_groups_share_one_counted_backedge() {
9151 let mut arena = SLTNodeArena::new();
9152 let guard = constant(&mut arena, 1, 1);
9153 let initial = constant(&mut arena, 0, 8);
9154 let one = constant(&mut arena, 1, 8);
9155 let previous_a = input(&mut arena, 10, 8);
9156 let previous_b = input(&mut arena, 11, 8);
9157 let update_a = arena
9158 .alloc(SLTNode::Binary(previous_a, BinaryOp::Add, one))
9159 .unwrap();
9160 let update_b = arena
9161 .alloc(SLTNode::Binary(previous_b, BinaryOp::Add, one))
9162 .unwrap();
9163 let group_a = arena
9164 .alloc(SLTNode::ForFoldGroup {
9165 loop_var: 20,
9166 loop_width: 8,
9167 loop_signed: false,
9168 start: BigInt::from(0),
9169 step: BigInt::from(1),
9170 trip_count: 3,
9171 entry_guard: guard,
9172 states: vec![SLTForFoldGroupState {
9173 target: VarAtomBase::new(10, 0, 7),
9174 initial,
9175 update: update_a,
9176 }],
9177 })
9178 .unwrap();
9179 let group_b = arena
9180 .alloc(SLTNode::ForFoldGroup {
9181 loop_var: 21,
9182 loop_width: 8,
9183 loop_signed: false,
9184 start: BigInt::from(0),
9185 step: BigInt::from(1),
9186 trip_count: 3,
9187 entry_guard: guard,
9188 states: vec![SLTForFoldGroupState {
9189 target: VarAtomBase::new(11, 0, 7),
9190 initial,
9191 update: update_b,
9192 }],
9193 })
9194 .unwrap();
9195
9196 let mut builder = SIRBuilder::new();
9197 let mut cache = crate::HashMap::default();
9198 assert!(SLTToSIRLowerer::new(false).lower_fold_groups_jointly(
9199 &mut builder,
9200 &[group_a, group_b],
9201 &arena,
9202 &mut cache,
9203 ));
9204 let result_a = cache[&group_a];
9205 let result_b = cache[&group_b];
9206 let eu = finish_lowering(builder);
9207
9208 assert_eq!(branch_count(&eu), 2, "one entry branch and one backedge");
9209 assert_eq!(
9210 eu.blocks
9211 .values()
9212 .filter(|block| matches!(
9213 &block.terminator,
9214 SIRTerminator::Branch { true_block, .. } if true_block.0 == block.id
9215 ))
9216 .count(),
9217 1,
9218 );
9219 let values = execute_fold_group_sir(&eu);
9220 assert_eq!(values[&result_a].payload, BigUint::from(3u8));
9221 assert_eq!(values[&result_b].payload, BigUint::from(3u8));
9222 }
9223
9224 #[test]
9225 fn joint_fold_groups_keep_all_updates_simultaneous() {
9226 let mut arena = SLTNodeArena::new();
9227 let guard = constant(&mut arena, 1, 1);
9228 let initial_a = constant(&mut arena, 0x12, 8);
9229 let initial_b = constant(&mut arena, 0x34, 8);
9230 let initial_c = constant(&mut arena, 7, 8);
9231 let previous_a = input(&mut arena, 10, 8);
9232 let previous_b = input(&mut arena, 11, 8);
9233 let previous_c = input(&mut arena, 12, 8);
9234 let one = constant(&mut arena, 1, 8);
9235 let update_c = arena
9236 .alloc(SLTNode::Binary(previous_c, BinaryOp::Add, one))
9237 .unwrap();
9238 let swap_group = arena
9239 .alloc(SLTNode::ForFoldGroup {
9240 loop_var: 20,
9241 loop_width: 8,
9242 loop_signed: false,
9243 start: BigInt::from(0),
9244 step: BigInt::from(1),
9245 trip_count: 3,
9246 entry_guard: guard,
9247 states: vec![
9248 SLTForFoldGroupState {
9249 target: VarAtomBase::new(10, 0, 7),
9250 initial: initial_a,
9251 update: previous_b,
9252 },
9253 SLTForFoldGroupState {
9254 target: VarAtomBase::new(11, 0, 7),
9255 initial: initial_b,
9256 update: previous_a,
9257 },
9258 ],
9259 })
9260 .unwrap();
9261 let increment_group = arena
9262 .alloc(SLTNode::ForFoldGroup {
9263 loop_var: 21,
9264 loop_width: 8,
9265 loop_signed: false,
9266 start: BigInt::from(0),
9267 step: BigInt::from(1),
9268 trip_count: 3,
9269 entry_guard: guard,
9270 states: vec![SLTForFoldGroupState {
9271 target: VarAtomBase::new(12, 0, 7),
9272 initial: initial_c,
9273 update: update_c,
9274 }],
9275 })
9276 .unwrap();
9277
9278 let mut builder = SIRBuilder::new();
9279 let mut cache = crate::HashMap::default();
9280 assert!(SLTToSIRLowerer::new(false).lower_fold_groups_jointly(
9281 &mut builder,
9282 &[swap_group, increment_group],
9283 &arena,
9284 &mut cache,
9285 ));
9286 let swap_result = cache[&swap_group];
9287 let increment_result = cache[&increment_group];
9288 let values = execute_fold_group_sir(&finish_lowering(builder));
9289 assert_eq!(values[&swap_result].payload, BigUint::from(0x3412u16));
9290 assert_eq!(values[&increment_result].payload, BigUint::from(10u8));
9291 }
9292
9293 #[test]
9294 fn joint_fold_groups_reject_mismatched_domains_atomically() {
9295 let mut arena = SLTNodeArena::new();
9296 let guard = constant(&mut arena, 1, 1);
9297 let initial = constant(&mut arena, 0, 8);
9298 let update_a = input(&mut arena, 10, 8);
9299 let update_b = input(&mut arena, 11, 8);
9300 let make_group = |arena: &mut SLTNodeArena<u32>, loop_var, target, update, trip_count| {
9301 arena
9302 .alloc(SLTNode::ForFoldGroup {
9303 loop_var,
9304 loop_width: 8,
9305 loop_signed: false,
9306 start: BigInt::from(0),
9307 step: BigInt::from(1),
9308 trip_count,
9309 entry_guard: guard,
9310 states: vec![SLTForFoldGroupState {
9311 target: VarAtomBase::new(target, 0, 7),
9312 initial,
9313 update,
9314 }],
9315 })
9316 .unwrap()
9317 };
9318 let group_a = make_group(&mut arena, 20, 10, update_a, 3);
9319 let group_b = make_group(&mut arena, 21, 11, update_b, 4);
9320
9321 let mut builder = SIRBuilder::new();
9322 let mut cache = crate::HashMap::default();
9323 assert!(!SLTToSIRLowerer::new(false).lower_fold_groups_jointly(
9324 &mut builder,
9325 &[group_a, group_b],
9326 &arena,
9327 &mut cache,
9328 ));
9329 assert!(cache.is_empty());
9330 assert_eq!(builder.block_count(), 1);
9331 let value = builder.alloc_bit(1, false);
9332 builder.emit(SIRInstruction::Imm(value, SIRValue::new(1u8)));
9333 let eu = finish_lowering(builder);
9334 assert_eq!(branch_count(&eu), 0);
9335 assert_eq!(eu.blocks[&BlockId(0)].instructions.len(), 1);
9336 }
9337
9338 #[test]
9339 fn joint_fold_groups_accept_pre_loop_target_initials() {
9340 let mut arena = SLTNodeArena::new();
9341 let guard = constant(&mut arena, 1, 1);
9342 let previous_a = input(&mut arena, 10, 8);
9343 let previous_b = input(&mut arena, 11, 8);
9344 let make_group = |arena: &mut SLTNodeArena<u32>, loop_var, target, previous| {
9345 arena
9346 .alloc(SLTNode::ForFoldGroup {
9347 loop_var,
9348 loop_width: 8,
9349 loop_signed: false,
9350 start: BigInt::from(0),
9351 step: BigInt::from(1),
9352 trip_count: 2,
9353 entry_guard: guard,
9354 states: vec![SLTForFoldGroupState {
9355 target: VarAtomBase::new(target, 0, 7),
9356 initial: previous,
9357 update: previous,
9358 }],
9359 })
9360 .unwrap()
9361 };
9362 let group_a = make_group(&mut arena, 20, 10, previous_a);
9363 let group_b = make_group(&mut arena, 21, 11, previous_b);
9364
9365 let mut builder = SIRBuilder::new();
9366 let mut cache = crate::HashMap::default();
9367 assert!(SLTToSIRLowerer::new(false).lower_fold_groups_jointly(
9368 &mut builder,
9369 &[group_a, group_b],
9370 &arena,
9371 &mut cache,
9372 ));
9373 assert!(cache.contains_key(&group_a));
9374 assert!(cache.contains_key(&group_b));
9375 let eu = finish_lowering(builder);
9376 assert_eq!(branch_count(&eu), 2);
9377 }
9378
9379 #[test]
9380 fn fold_body_cost_analysis_uses_the_environment_scoped_cache() {
9381 let mut arena = SLTNodeArena::new();
9382 let guard = constant(&mut arena, 1, 1);
9383 let initial = constant(&mut arena, 8, 8);
9384 let previous = input(&mut arena, 10, 8);
9385 let cond = input_bit(&mut arena, 10, 0);
9386 let divisor = constant(&mut arena, 2, 8);
9387 let quotient = arena
9388 .alloc(SLTNode::Binary(previous, BinaryOp::DivU, divisor))
9389 .unwrap();
9390 let update = arena
9391 .alloc(SLTNode::Mux {
9392 cond,
9393 then_expr: quotient,
9394 else_expr: previous,
9395 })
9396 .unwrap();
9397 let group = arena
9398 .alloc(SLTNode::ForFoldGroup {
9399 loop_var: 20,
9400 loop_width: 8,
9401 loop_signed: false,
9402 start: BigInt::from(0),
9403 step: BigInt::from(1),
9404 trip_count: 2,
9405 entry_guard: guard,
9406 states: vec![SLTForFoldGroupState {
9407 target: VarAtomBase::new(10, 0, 7),
9408 initial,
9409 update,
9410 }],
9411 })
9412 .unwrap();
9413
9414 let mut builder = SIRBuilder::new();
9415 let unavailable_outer_value = builder.alloc_logic(8);
9416 builder.emit(SIRInstruction::Imm(
9417 unavailable_outer_value,
9418 SIRValue::new(0u8),
9419 ));
9420 let mut cache = crate::HashMap::default();
9421 cache.insert(quotient, unavailable_outer_value);
9422 SLTToSIRLowerer::new(false).lower(&mut builder, group, &arena, &mut cache);
9423 let eu = finish_lowering(builder);
9424
9425 assert_eq!(
9426 branch_count(&eu),
9427 3,
9428 "the body-local division arm must retain its mandatory lazy branch"
9429 );
9430 assert_eq!(
9431 instruction_count(&eu, |inst| matches!(
9432 inst,
9433 SIRInstruction::Binary(_, _, BinaryOp::DivU, _)
9434 )),
9435 1,
9436 );
9437 eu.verify_result().unwrap();
9438 }
9439
9440 #[test]
9441 fn joint_fold_groups_reject_cross_group_carried_reads() {
9442 let mut arena = SLTNodeArena::new();
9443 let guard = constant(&mut arena, 1, 1);
9444 let initial = constant(&mut arena, 0, 8);
9445 let previous_a = input(&mut arena, 10, 8);
9446 let group_a = arena
9447 .alloc(SLTNode::ForFoldGroup {
9448 loop_var: 20,
9449 loop_width: 8,
9450 loop_signed: false,
9451 start: BigInt::from(0),
9452 step: BigInt::from(1),
9453 trip_count: 2,
9454 entry_guard: guard,
9455 states: vec![SLTForFoldGroupState {
9456 target: VarAtomBase::new(10, 0, 7),
9457 initial,
9458 update: previous_a,
9459 }],
9460 })
9461 .unwrap();
9462 let group_b = arena
9463 .alloc(SLTNode::ForFoldGroup {
9464 loop_var: 21,
9465 loop_width: 8,
9466 loop_signed: false,
9467 start: BigInt::from(0),
9468 step: BigInt::from(1),
9469 trip_count: 2,
9470 entry_guard: guard,
9471 states: vec![SLTForFoldGroupState {
9472 target: VarAtomBase::new(11, 0, 7),
9473 initial,
9474 update: previous_a,
9475 }],
9476 })
9477 .unwrap();
9478
9479 let mut builder = SIRBuilder::new();
9480 let mut cache = crate::HashMap::default();
9481 assert!(!SLTToSIRLowerer::new(false).lower_fold_groups_jointly(
9482 &mut builder,
9483 &[group_a, group_b],
9484 &arena,
9485 &mut cache,
9486 ));
9487 assert!(cache.is_empty());
9488 assert_eq!(builder.block_count(), 1);
9489 }
9490
9491 #[test]
9492 fn four_state_joint_fold_groups_preserve_unknown_guard_per_result() {
9493 let mut arena = SLTNodeArena::new();
9494 let guard = arena
9495 .alloc(SLTNode::Constant(
9496 BigUint::from(0u8),
9497 BigUint::from(1u8),
9498 1,
9499 false,
9500 ))
9501 .unwrap();
9502 let initial_a = constant(&mut arena, 0x12, 8);
9503 let initial_b = constant(&mut arena, 0x34, 8);
9504 let update_a = input(&mut arena, 10, 8);
9505 let update_b = input(&mut arena, 11, 8);
9506 let make_group = |arena: &mut SLTNodeArena<u32>, loop_var, target, initial, update| {
9507 arena
9508 .alloc(SLTNode::ForFoldGroup {
9509 loop_var,
9510 loop_width: 8,
9511 loop_signed: false,
9512 start: BigInt::from(0),
9513 step: BigInt::from(1),
9514 trip_count: 2,
9515 entry_guard: guard,
9516 states: vec![SLTForFoldGroupState {
9517 target: VarAtomBase::new(target, 0, 7),
9518 initial,
9519 update,
9520 }],
9521 })
9522 .unwrap()
9523 };
9524 let group_a = make_group(&mut arena, 20, 10, initial_a, update_a);
9525 let group_b = make_group(&mut arena, 21, 11, initial_b, update_b);
9526
9527 let mut builder = SIRBuilder::new();
9528 let mut cache = crate::HashMap::default();
9529 assert!(SLTToSIRLowerer::new(true).lower_fold_groups_jointly(
9530 &mut builder,
9531 &[group_a, group_b],
9532 &arena,
9533 &mut cache,
9534 ));
9535 let result_a = cache[&group_a];
9536 let result_b = cache[&group_b];
9537 let eu = finish_lowering(builder);
9538 assert_eq!(branch_count(&eu), 2);
9539 assert_eq!(
9540 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
9541 2,
9542 );
9543 let values = execute_fold_group_sir(&eu);
9544 assert_eq!(values[&result_a].mask, BigUint::from(0xffu8));
9545 assert_eq!(values[&result_b].mask, BigUint::from(0xffu8));
9546 }
9547
9548 #[test]
9549 fn for_fold_group_lowers_swap_updates_as_one_counted_cfg() {
9550 let mut arena = SLTNodeArena::new();
9551 let guard = constant(&mut arena, 1, 1);
9552 let initial_a = constant(&mut arena, 0x12, 8);
9553 let initial_b = constant(&mut arena, 0x34, 8);
9554 let previous_a = input(&mut arena, 10, 8);
9555 let previous_b = input(&mut arena, 11, 8);
9556 let target_a = VarAtomBase::new(10, 0, 7);
9557 let target_b = VarAtomBase::new(11, 0, 7);
9558 let group = arena
9559 .alloc(SLTNode::ForFoldGroup {
9560 loop_var: 20,
9561 loop_width: 8,
9562 loop_signed: false,
9563 start: BigInt::from(2),
9564 step: BigInt::from(3),
9565 trip_count: 3,
9566 entry_guard: guard,
9567 states: vec![
9568 SLTForFoldGroupState {
9569 target: target_a,
9570 initial: initial_a,
9571 update: previous_b,
9572 },
9573 SLTForFoldGroupState {
9574 target: target_b,
9575 initial: initial_b,
9576 update: previous_a,
9577 },
9578 ],
9579 })
9580 .unwrap();
9581
9582 let mut builder = SIRBuilder::new();
9583 let result = SLTToSIRLowerer::new(false).lower(
9584 &mut builder,
9585 group,
9586 &arena,
9587 &mut crate::HashMap::default(),
9588 );
9589 let eu = finish_lowering(builder);
9590
9591 assert_eq!(eu.register_map[&result].width(), 16);
9592 assert_eq!(
9593 execute_fold_group_sir(&eu)[&result].payload,
9594 BigUint::from(0x3412u16),
9595 "three simultaneous swaps leave the first state in the MSBs"
9596 );
9597 assert_eq!(branch_count(&eu), 2, "entry guard plus counted backedge");
9598 assert_eq!(
9599 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Concat(..))),
9600 1,
9601 "the final states must be packed once at the common exit"
9602 );
9603 assert_eq!(
9604 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
9605 0
9606 );
9607 assert!(
9608 eu.blocks
9609 .values()
9610 .all(|block| !matches!(block.terminator, SIRTerminator::Error(_)))
9611 );
9612
9613 let body = eu
9614 .blocks
9615 .values()
9616 .find(|block| {
9617 block.params.len() == 4 && matches!(block.terminator, SIRTerminator::Branch { .. })
9618 })
9619 .expect("counted body block");
9620 assert_eq!(eu.register_map[&body.params[0]].width(), 2);
9621 let SIRTerminator::Branch { true_block, .. } = &body.terminator else {
9622 unreachable!()
9623 };
9624 assert_eq!(true_block.0, body.id);
9625 let backedge_args = &true_block.1;
9626 assert_eq!(backedge_args[2], body.params[3]);
9627 assert_eq!(backedge_args[3], body.params[2]);
9628
9629 let exit = eu
9630 .blocks
9631 .values()
9632 .find(|block| {
9633 block.params.len() == 2
9634 && block
9635 .instructions
9636 .iter()
9637 .any(|inst| matches!(inst, SIRInstruction::Concat(..)))
9638 })
9639 .expect("packed common exit");
9640 let packed_args = exit
9641 .instructions
9642 .iter()
9643 .find_map(|inst| match inst {
9644 SIRInstruction::Concat(_, args) => Some(args),
9645 _ => None,
9646 })
9647 .unwrap();
9648 assert_eq!(packed_args, &exit.params, "state zero occupies the MSBs");
9649 }
9650
9651 #[test]
9652 fn for_fold_group_executes_exactly_one_and_three_iterations() {
9653 for (trip_count, expected) in [(1usize, 1u8), (3, 3)] {
9654 let mut arena = SLTNodeArena::new();
9655 let guard = constant(&mut arena, 1, 1);
9656 let initial = constant(&mut arena, 0, 8);
9657 let previous = input(&mut arena, 10, 8);
9658 let one = constant(&mut arena, 1, 8);
9659 let update = arena
9660 .alloc(SLTNode::Binary(previous, BinaryOp::Add, one))
9661 .unwrap();
9662 let group = arena
9663 .alloc(SLTNode::ForFoldGroup {
9664 loop_var: 20,
9665 loop_width: 8,
9666 loop_signed: false,
9667 start: BigInt::from(0),
9668 step: BigInt::from(1),
9669 trip_count,
9670 entry_guard: guard,
9671 states: vec![SLTForFoldGroupState {
9672 target: VarAtomBase::new(10, 0, 7),
9673 initial,
9674 update,
9675 }],
9676 })
9677 .unwrap();
9678
9679 let mut builder = SIRBuilder::new();
9680 let result = SLTToSIRLowerer::new(false).lower(
9681 &mut builder,
9682 group,
9683 &arena,
9684 &mut crate::HashMap::default(),
9685 );
9686 let eu = finish_lowering(builder);
9687
9688 assert_eq!(
9689 execute_fold_group_sir(&eu)[&result].payload,
9690 BigUint::from(expected),
9691 "trip_count={trip_count}"
9692 );
9693 assert!(
9694 eu.blocks
9695 .values()
9696 .all(|block| !matches!(block.terminator, SIRTerminator::Error(_)))
9697 );
9698 }
9699 }
9700
9701 #[test]
9702 fn for_fold_group_inherits_outer_inputs_with_inner_partial_state_priority() {
9703 let mut arena = SLTNodeArena::new();
9704 let guard = input(&mut arena, 20, 1);
9705 let initial = constant(&mut arena, 0x12, 8);
9706 let outer_wide = input(&mut arena, 10, 16);
9707 let update = arena
9708 .alloc(SLTNode::Slice {
9709 expr: outer_wide,
9710 access: BitAccess::new(0, 7),
9711 })
9712 .unwrap();
9713 let group = arena
9714 .alloc(SLTNode::ForFoldGroup {
9715 loop_var: 30,
9716 loop_width: 8,
9717 loop_signed: false,
9718 start: BigInt::from(0),
9719 step: BigInt::from(1),
9720 trip_count: 2,
9721 entry_guard: guard,
9722 states: vec![SLTForFoldGroupState {
9723 target: VarAtomBase::new(10, 0, 7),
9724 initial,
9725 update,
9726 }],
9727 })
9728 .unwrap();
9729
9730 let mut builder = SIRBuilder::new();
9731 let outer_value = builder.alloc_logic(16);
9732 builder.emit(SIRInstruction::Imm(outer_value, SIRValue::new(0xabcdu16)));
9733 let outer_guard = builder.alloc_logic(1);
9734 builder.emit(SIRInstruction::Imm(outer_guard, SIRValue::new(1u8)));
9735 let mut inputs = crate::HashMap::default();
9736 inputs.insert(VarAtomBase::new(10, 0, 15), outer_value);
9737 inputs.insert(VarAtomBase::new(20, 0, 0), outer_guard);
9738
9739 let result = SLTToSIRLowerer::new(false).lower_with_inputs(
9740 &mut builder,
9741 group,
9742 &arena,
9743 &mut crate::HashMap::default(),
9744 inputs,
9745 );
9746 let eu = finish_lowering(builder);
9747
9748 assert_eq!(
9749 execute_fold_group_sir(&eu)[&result].payload,
9750 BigUint::from(0x12u8),
9751 "the inner carried low byte must override the overlapping outer full value"
9752 );
9753 assert!(eu.blocks.values().all(|block| {
9754 block
9755 .instructions
9756 .iter()
9757 .all(|instruction| !matches!(instruction, SIRInstruction::Load(..)))
9758 }));
9759 }
9760
9761 #[test]
9762 fn for_fold_group_dynamic_array_read_stays_a_narrow_load_under_env() {
9763 let mut arena = SLTNodeArena::new();
9764 let guard = constant(&mut arena, 1, 1);
9765 let initial = constant(&mut arena, 0, 8);
9766 let loop_index = input(&mut arena, 20, 8);
9767 let raw_array = arena
9768 .alloc(SLTNode::Input {
9769 variable: 30,
9770 signed: false,
9771 index: vec![crate::SLTIndex {
9772 node: loop_index,
9773 stride: 8,
9774 kind: crate::SLTIndexKind::Packed,
9775 }],
9776 access: BitAccess::new(0, 255),
9777 })
9778 .unwrap();
9779 let update = arena
9780 .alloc(SLTNode::Slice {
9781 expr: raw_array,
9782 access: BitAccess::new(0, 7),
9783 })
9784 .unwrap();
9785 let group = arena
9786 .alloc(SLTNode::ForFoldGroup {
9787 loop_var: 20,
9788 loop_width: 8,
9789 loop_signed: false,
9790 start: BigInt::from(0),
9791 step: BigInt::from(1),
9792 trip_count: 3,
9793 entry_guard: guard,
9794 states: vec![SLTForFoldGroupState {
9795 target: VarAtomBase::new(10, 0, 7),
9796 initial,
9797 update,
9798 }],
9799 })
9800 .unwrap();
9801
9802 let mut builder = SIRBuilder::new();
9803 SLTToSIRLowerer::new(false).lower(
9804 &mut builder,
9805 group,
9806 &arena,
9807 &mut crate::HashMap::default(),
9808 );
9809 let eu = finish_lowering(builder);
9810 let load_widths = eu
9811 .blocks
9812 .values()
9813 .flat_map(|block| &block.instructions)
9814 .filter_map(|instruction| match instruction {
9815 SIRInstruction::Load(_, variable, SIROffset::Dynamic(_), width)
9816 if *variable == 30 =>
9817 {
9818 Some(*width)
9819 }
9820 _ => None,
9821 })
9822 .collect::<Vec<_>>();
9823 assert_eq!(load_widths, vec![8]);
9824 }
9825
9826 #[test]
9827 fn constant_unpacked_index_lowers_to_a_direct_logical_offset() {
9828 let mut arena = SLTNodeArena::new();
9829 let index = constant(&mut arena, 3, 8);
9830 let element = arena
9831 .alloc(SLTNode::Input {
9832 variable: 30,
9833 signed: false,
9834 index: vec![crate::SLTIndex {
9835 node: index,
9836 stride: 14,
9837 kind: crate::SLTIndexKind::Unpacked { element_width: 14 },
9838 }],
9839 access: BitAccess::new(4, 7),
9840 })
9841 .unwrap();
9842
9843 let mut builder = SIRBuilder::new();
9844 SLTToSIRLowerer::new(false).lower(
9845 &mut builder,
9846 element,
9847 &arena,
9848 &mut crate::HashMap::default(),
9849 );
9850 let eu = finish_lowering(builder);
9851
9852 assert!(eu.blocks.values().any(|block| {
9853 block.instructions.iter().any(|instruction| {
9854 matches!(
9855 instruction,
9856 SIRInstruction::Load(_, 30, SIROffset::Static(46), 4)
9857 )
9858 })
9859 }));
9860 assert!(eu.blocks.values().all(|block| {
9861 block.instructions.iter().all(|instruction| {
9862 !matches!(
9863 instruction,
9864 SIRInstruction::Load(
9865 _,
9866 30,
9867 SIROffset::Dynamic(_) | SIROffset::Element { .. },
9868 _
9869 )
9870 )
9871 })
9872 }));
9873 }
9874
9875 #[test]
9876 fn for_fold_group_captures_invariant_work_on_the_true_entry_edge() {
9877 let mut arena = SLTNodeArena::new();
9878 let guard = input(&mut arena, 40, 1);
9879 let initial = constant(&mut arena, 0, 8);
9880 let previous = input(&mut arena, 10, 8);
9881 let external = input(&mut arena, 30, 8);
9882 let two = constant(&mut arena, 2, 8);
9883 let invariant = arena
9884 .alloc(SLTNode::Binary(external, BinaryOp::Add, two))
9885 .unwrap();
9886 let update = arena
9887 .alloc(SLTNode::Binary(previous, BinaryOp::Add, invariant))
9888 .unwrap();
9889 let group = arena
9890 .alloc(SLTNode::ForFoldGroup {
9891 loop_var: 20,
9892 loop_width: 8,
9893 loop_signed: false,
9894 start: BigInt::from(0),
9895 step: BigInt::from(1),
9896 trip_count: 3,
9897 entry_guard: guard,
9898 states: vec![SLTForFoldGroupState {
9899 target: VarAtomBase::new(10, 0, 7),
9900 initial,
9901 update,
9902 }],
9903 })
9904 .unwrap();
9905
9906 let mut builder = SIRBuilder::new();
9907 SLTToSIRLowerer::new(false).lower(
9908 &mut builder,
9909 group,
9910 &arena,
9911 &mut crate::HashMap::default(),
9912 );
9913 let eu = finish_lowering(builder);
9914 let SIRTerminator::Branch { true_block, .. } = &eu.blocks[&BlockId(0)].terminator else {
9915 panic!("entry must branch around the recovered loop")
9916 };
9917 assert!(
9918 true_block.1.is_empty(),
9919 "the true edge must enter the capture block"
9920 );
9921 let enter = &eu.blocks[&true_block.0];
9922 let SIRTerminator::Jump(body, _) = &enter.terminator else {
9923 panic!("capture block must jump to the counted body")
9924 };
9925 let body = *body;
9926 assert!(enter.instructions.iter().any(|instruction| matches!(
9927 instruction,
9928 SIRInstruction::Load(_, variable, SIROffset::Static(0), 8) if *variable == 30
9929 )));
9930 assert!(
9931 eu.blocks[&body]
9932 .instructions
9933 .iter()
9934 .all(|instruction| !matches!(
9935 instruction,
9936 SIRInstruction::Load(_, variable, _, _) if *variable == 30
9937 ))
9938 );
9939 assert!(matches!(
9940 &eu.blocks[&body].terminator,
9941 SIRTerminator::Branch { true_block: (target, _), .. } if *target == body
9942 ));
9943 }
9944
9945 #[test]
9946 fn for_fold_group_does_not_capture_invariant_division() {
9947 let mut arena = SLTNodeArena::new();
9948 let guard = input(&mut arena, 40, 1);
9949 let initial = constant(&mut arena, 0, 8);
9950 let previous = input(&mut arena, 10, 8);
9951 let numerator = input(&mut arena, 30, 8);
9952 let denominator = input(&mut arena, 31, 8);
9953 let quotient = arena
9954 .alloc(SLTNode::Binary(numerator, BinaryOp::DivU, denominator))
9955 .unwrap();
9956 let update = arena
9957 .alloc(SLTNode::Binary(previous, BinaryOp::Add, quotient))
9958 .unwrap();
9959 let group = arena
9960 .alloc(SLTNode::ForFoldGroup {
9961 loop_var: 20,
9962 loop_width: 8,
9963 loop_signed: false,
9964 start: BigInt::from(0),
9965 step: BigInt::from(1),
9966 trip_count: 3,
9967 entry_guard: guard,
9968 states: vec![SLTForFoldGroupState {
9969 target: VarAtomBase::new(10, 0, 7),
9970 initial,
9971 update,
9972 }],
9973 })
9974 .unwrap();
9975
9976 let mut builder = SIRBuilder::new();
9977 SLTToSIRLowerer::new(false).lower(
9978 &mut builder,
9979 group,
9980 &arena,
9981 &mut crate::HashMap::default(),
9982 );
9983 let eu = finish_lowering(builder);
9984 assert_eq!(
9985 instruction_count(&eu, |instruction| matches!(
9986 instruction,
9987 SIRInstruction::Binary(_, _, BinaryOp::DivU, _)
9988 )),
9989 1,
9990 );
9991 let division_block = eu
9992 .blocks
9993 .values()
9994 .find(|block| {
9995 block.instructions.iter().any(|instruction| {
9996 matches!(instruction, SIRInstruction::Binary(_, _, BinaryOp::DivU, _))
9997 })
9998 })
9999 .unwrap();
10000 assert!(matches!(
10001 &division_block.terminator,
10002 SIRTerminator::Branch { true_block: (target, _), .. }
10003 if *target == division_block.id
10004 ));
10005 }
10006
10007 #[test]
10008 fn false_for_fold_group_entry_guard_skips_all_updates() {
10009 let mut arena = SLTNodeArena::new();
10010 let guard = constant(&mut arena, 0, 1);
10011 let initial = constant(&mut arena, 0x5a, 8);
10012 let previous = input(&mut arena, 10, 8);
10013 let one = constant(&mut arena, 1, 8);
10014 let update = arena
10015 .alloc(SLTNode::Binary(previous, BinaryOp::Add, one))
10016 .unwrap();
10017 let group = arena
10018 .alloc(SLTNode::ForFoldGroup {
10019 loop_var: 20,
10020 loop_width: 8,
10021 loop_signed: false,
10022 start: BigInt::from(0),
10023 step: BigInt::from(1),
10024 trip_count: 3,
10025 entry_guard: guard,
10026 states: vec![SLTForFoldGroupState {
10027 target: VarAtomBase::new(10, 0, 7),
10028 initial,
10029 update,
10030 }],
10031 })
10032 .unwrap();
10033
10034 let mut builder = SIRBuilder::new();
10035 let result = SLTToSIRLowerer::new(false).lower(
10036 &mut builder,
10037 group,
10038 &arena,
10039 &mut crate::HashMap::default(),
10040 );
10041 let eu = finish_lowering(builder);
10042
10043 assert_eq!(branch_count(&eu), 2);
10044 assert_eq!(
10045 execute_fold_group_sir(&eu)[&result].payload,
10046 BigUint::from(0x5au8)
10047 );
10048 let SIRTerminator::Branch { false_block, .. } = &eu.blocks[&BlockId(0)].terminator else {
10049 panic!("entry must branch around the loop")
10050 };
10051 assert_eq!(false_block.1.len(), 1);
10052 let skipped_to = &eu.blocks[&false_block.0];
10053 assert_eq!(skipped_to.params.len(), 1);
10054 assert!(
10055 skipped_to
10056 .instructions
10057 .iter()
10058 .any(|inst| matches!(inst, SIRInstruction::Concat(..)))
10059 );
10060 }
10061
10062 #[test]
10063 fn four_state_for_fold_group_branches_then_applies_one_packed_mux() {
10064 let mut arena = SLTNodeArena::new();
10065 let guard = arena
10066 .alloc(SLTNode::Constant(
10067 BigUint::from(0u8),
10068 BigUint::from(1u8),
10069 1,
10070 false,
10071 ))
10072 .unwrap();
10073 let initial_a = constant(&mut arena, 1, 8);
10074 let initial_b = constant(&mut arena, 2, 8);
10075 let previous_a = input(&mut arena, 10, 8);
10076 let previous_b = input(&mut arena, 11, 8);
10077 let group = arena
10078 .alloc(SLTNode::ForFoldGroup {
10079 loop_var: 20,
10080 loop_width: 8,
10081 loop_signed: false,
10082 start: BigInt::from(0),
10083 step: BigInt::from(1),
10084 trip_count: 2,
10085 entry_guard: guard,
10086 states: vec![
10087 SLTForFoldGroupState {
10088 target: VarAtomBase::new(10, 0, 7),
10089 initial: initial_a,
10090 update: previous_b,
10091 },
10092 SLTForFoldGroupState {
10093 target: VarAtomBase::new(11, 0, 7),
10094 initial: initial_b,
10095 update: previous_a,
10096 },
10097 ],
10098 })
10099 .unwrap();
10100
10101 let mut builder = SIRBuilder::new();
10102 let result = SLTToSIRLowerer::new(true).lower(
10103 &mut builder,
10104 group,
10105 &arena,
10106 &mut crate::HashMap::default(),
10107 );
10108 let eu = finish_lowering(builder);
10109
10110 assert_eq!(
10111 execute_fold_group_sir(&eu)[&result].mask,
10112 BigUint::from(0xffffu16),
10113 "an unknown entry guard must make the packed result all-X"
10114 );
10115
10116 assert_eq!(
10117 branch_count(&eu),
10118 2,
10119 "the guard value plane controls loop entry"
10120 );
10121 assert_eq!(
10122 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Concat(..))),
10123 2,
10124 "initial and branch-selected candidates are each packed once"
10125 );
10126 assert_eq!(
10127 instruction_count(&eu, |inst| matches!(inst, SIRInstruction::Mux(..))),
10128 1,
10129 "one packed Mux must restore X/Z guard semantics"
10130 );
10131
10132 let entry_guard = match &eu.blocks[&BlockId(0)].terminator {
10133 SIRTerminator::Branch { cond, .. } => *cond,
10134 other => panic!("expected entry guard branch, got {other:?}"),
10135 };
10136 let mux_guard = eu
10137 .blocks
10138 .values()
10139 .flat_map(|block| &block.instructions)
10140 .find_map(|inst| match inst {
10141 SIRInstruction::Mux(_, cond, _, _) => Some(*cond),
10142 _ => None,
10143 })
10144 .unwrap();
10145 assert!(eu.blocks[&BlockId(0)].instructions.iter().any(|inst| {
10146 matches!(
10147 inst,
10148 SIRInstruction::Unary(dst, UnaryOp::ToTwoState, src)
10149 if *dst == entry_guard && *src == mux_guard
10150 )
10151 }));
10152 }
10153
10154 #[test]
10155 fn signed_for_fold_group_uses_a_direct_conditional_backedge() {
10156 let mut arena = SLTNodeArena::new();
10157 let guard = constant(&mut arena, 1, 1);
10158 let initial = constant(&mut arena, 0, 8);
10159 let previous = input(&mut arena, 10, 8);
10160 let loop_value = arena
10161 .alloc(SLTNode::Input {
10162 variable: 20,
10163 signed: true,
10164 index: vec![],
10165 access: BitAccess::new(0, 7),
10166 })
10167 .unwrap();
10168 let update = arena
10169 .alloc(SLTNode::Binary(previous, BinaryOp::Add, loop_value))
10170 .unwrap();
10171 let group = arena
10172 .alloc(SLTNode::ForFoldGroup {
10173 loop_var: 20,
10174 loop_width: 8,
10175 loop_signed: true,
10176 start: BigInt::from(-1),
10177 step: BigInt::from(-2),
10178 trip_count: 3,
10179 entry_guard: guard,
10180 states: vec![SLTForFoldGroupState {
10181 target: VarAtomBase::new(10, 0, 7),
10182 initial,
10183 update,
10184 }],
10185 })
10186 .unwrap();
10187
10188 let mut builder = SIRBuilder::new();
10189 let result = SLTToSIRLowerer::new(false).lower(
10190 &mut builder,
10191 group,
10192 &arena,
10193 &mut crate::HashMap::default(),
10194 );
10195 let eu = finish_lowering(builder);
10196
10197 assert_eq!(
10198 execute_fold_group_sir(&eu)[&result].payload,
10199 BigUint::from(0xf7u16),
10200 "three iterations must observe -1, -3, and -5 exactly"
10201 );
10202
10203 let body = eu
10204 .blocks
10205 .values()
10206 .find(|block| {
10207 block.params.len() == 3 && matches!(block.terminator, SIRTerminator::Branch { .. })
10208 })
10209 .expect("counted body block");
10210 assert!(body.instructions.iter().any(|inst| matches!(
10211 inst,
10212 SIRInstruction::Binary(_, lhs, BinaryOp::Add, rhs)
10213 if *lhs == body.params[2] && *rhs == body.params[1]
10214 )));
10215
10216 let step_reg = body
10217 .instructions
10218 .iter()
10219 .find_map(|inst| match inst {
10220 SIRInstruction::Binary(_, lhs, BinaryOp::Add, rhs) if *lhs == body.params[1] => {
10221 Some(*rhs)
10222 }
10223 _ => None,
10224 })
10225 .expect("loop-value step");
10226 let step_payload = eu
10227 .blocks
10228 .values()
10229 .flat_map(|block| &block.instructions)
10230 .find_map(|inst| match inst {
10231 SIRInstruction::Imm(dst, value) if *dst == step_reg => Some(&value.payload),
10232 _ => None,
10233 })
10234 .unwrap();
10235 assert_eq!(step_payload, &BigUint::from(0xfeu16));
10236 let SIRTerminator::Branch { true_block, .. } = &body.terminator else {
10237 unreachable!()
10238 };
10239 assert_eq!(true_block.0, body.id);
10240 assert!(eu.blocks.values().all(|block| {
10241 !matches!(&block.terminator, SIRTerminator::Jump(target, _) if *target == body.id)
10242 }));
10243 assert!(
10244 eu.blocks
10245 .values()
10246 .all(|block| !matches!(block.terminator, SIRTerminator::Error(_)))
10247 );
10248 }
10249
10250 #[test]
10251 fn slice_uses_slice_aware_cfg_cost_and_verifies() {
10252 let mut arena = SLTNodeArena::new();
10253 let cond = input(&mut arena, 0, 1);
10254 let then_input = input(&mut arena, 1, 256);
10255 let else_input = input(&mut arena, 2, 256);
10256 let then_expr = operation_chain(&mut arena, then_input, BinaryOp::And, 12, 10, 256);
10257 let else_expr = operation_chain(&mut arena, else_input, BinaryOp::Xor, 12, 100, 256);
10258 let mux = arena
10259 .alloc(SLTNode::Mux {
10260 cond,
10261 then_expr,
10262 else_expr,
10263 })
10264 .unwrap();
10265 let slice = arena
10266 .alloc(SLTNode::Slice {
10267 expr: mux,
10268 access: BitAccess::new(0, 63),
10269 })
10270 .unwrap();
10271 let mut builder = SIRBuilder::new();
10272 SLTToSIRLowerer::new(false).lower(
10273 &mut builder,
10274 slice,
10275 &arena,
10276 &mut crate::HashMap::default(),
10277 );
10278 let eu = finish_lowering(builder);
10279
10280 assert_eq!(branch_count(&eu), 1);
10281 }
10282
10283 #[test]
10284 fn nested_mux_analysis_is_linear_in_dag_size() {
10285 let mut arena = SLTNodeArena::new();
10286 let mut value = input(&mut arena, 0, 64);
10287 for depth in 0..256u32 {
10288 let cond = input(&mut arena, 1 + depth * 2, 1);
10289 let arm_input = input(&mut arena, 2 + depth * 2, 64);
10290 let arm = operation_chain(
10291 &mut arena,
10292 arm_input,
10293 BinaryOp::Add,
10294 4,
10295 1_000 + u64::from(depth) * 8,
10296 64,
10297 );
10298 value = arena
10299 .alloc(SLTNode::Mux {
10300 cond,
10301 then_expr: arm,
10302 else_expr: value,
10303 })
10304 .unwrap();
10305 }
10306
10307 let lowerer = SLTToSIRLowerer::new(false);
10308 let mut builder = SIRBuilder::new();
10309 lowerer.lower(&mut builder, value, &arena, &mut crate::HashMap::default());
10310 let visits = lowerer.analysis_node_visits();
10311 let node_count = arena.len();
10312 finish_lowering(builder);
10313
10314 assert!(
10315 visits <= node_count * 20,
10316 "analysis revisited {visits} nodes for a {node_count}-node nested mux DAG",
10317 );
10318 }
10319
10320 #[test]
10321 fn unrelated_global_cache_does_not_enter_mux_analysis() {
10322 let mut arena = SLTNodeArena::new();
10323 let cond = input(&mut arena, 0, 1);
10324 let then_input = input(&mut arena, 1, 64);
10325 let else_input = input(&mut arena, 2, 64);
10326 let then_expr = operation_chain(&mut arena, then_input, BinaryOp::Add, 8, 10, 64);
10327 let else_expr = operation_chain(&mut arena, else_input, BinaryOp::Sub, 8, 100, 64);
10328 let mux = arena
10329 .alloc(SLTNode::Mux {
10330 cond,
10331 then_expr,
10332 else_expr,
10333 })
10334 .unwrap();
10335
10336 let empty_lowerer = SLTToSIRLowerer::new(false);
10337 let mut empty_builder = SIRBuilder::new();
10338 empty_lowerer.lower(
10339 &mut empty_builder,
10340 mux,
10341 &arena,
10342 &mut crate::HashMap::default(),
10343 );
10344 let empty_visits = empty_lowerer.analysis_node_visits();
10345 finish_lowering(empty_builder);
10346
10347 let mut large_cache = crate::HashMap::default();
10348 for index in 0..20_000usize {
10349 large_cache.insert(NodeId(arena.len() + index), RegisterId(index));
10350 }
10351 let cached_lowerer = SLTToSIRLowerer::new(false);
10352 let mut cached_builder = SIRBuilder::new();
10353 cached_lowerer.lower(&mut cached_builder, mux, &arena, &mut large_cache);
10354 let cached_visits = cached_lowerer.analysis_node_visits();
10355 finish_lowering(cached_builder);
10356
10357 assert_eq!(cached_visits, empty_visits);
10358 }
10359
10360 #[test]
10361 fn signed_inputs_report_signedness() {
10362 let mut arena = SLTNodeArena::<u32>::new();
10363 let node = arena
10364 .alloc(SLTNode::Input {
10365 variable: 0,
10366 signed: true,
10367 index: vec![],
10368 access: BitAccess::new(0, 7),
10369 })
10370 .unwrap();
10371 let lowerer = SLTToSIRLowerer::new(false);
10372 assert!(lowerer.get_bound_signed(node, &arena));
10373 }
10374
10375 #[test]
10376 fn unsigned_inputs_report_unsignedness() {
10377 let mut arena = SLTNodeArena::<u32>::new();
10378 let node = arena
10379 .alloc(SLTNode::Input {
10380 variable: 0,
10381 signed: false,
10382 index: vec![],
10383 access: BitAccess::new(0, 7),
10384 })
10385 .unwrap();
10386 let lowerer = SLTToSIRLowerer::new(false);
10387 assert!(!lowerer.get_bound_signed(node, &arena));
10388 }
10389
10390 #[test]
10391 fn bit_count_results_are_unsigned_even_for_signed_inputs() {
10392 let mut arena = SLTNodeArena::<u32>::new();
10393 let input = arena
10394 .alloc(SLTNode::Input {
10395 variable: 0,
10396 signed: true,
10397 index: vec![],
10398 access: BitAccess::new(0, 7),
10399 })
10400 .unwrap();
10401 let lowerer = SLTToSIRLowerer::new(false);
10402
10403 for op in [
10404 UnaryOp::PopCount,
10405 UnaryOp::CountLeadingZeros,
10406 UnaryOp::CountTrailingZeros,
10407 ] {
10408 let node = arena.alloc(SLTNode::Unary(op, input)).unwrap();
10409 assert!(!lowerer.get_bound_signed(node, &arena));
10410 }
10411 }
10412
10413 #[test]
10414 fn unary_value_operators_preserve_operand_expression_signedness() {
10415 let mut arena = SLTNodeArena::<u32>::new();
10416 let signed = arena
10417 .alloc(SLTNode::Input {
10418 variable: 0,
10419 signed: true,
10420 index: vec![],
10421 access: BitAccess::new(0, 7),
10422 })
10423 .unwrap();
10424 let unsigned = arena
10425 .alloc(SLTNode::Input {
10426 variable: 1,
10427 signed: false,
10428 index: vec![],
10429 access: BitAccess::new(0, 7),
10430 })
10431 .unwrap();
10432 let lowerer = SLTToSIRLowerer::new(false);
10433
10434 for op in [
10435 UnaryOp::Ident,
10436 UnaryOp::ToTwoState,
10437 UnaryOp::Minus,
10438 UnaryOp::BitNot,
10439 ] {
10440 let signed_result = arena.alloc(SLTNode::Unary(op, signed)).unwrap();
10441 let unsigned_result = arena.alloc(SLTNode::Unary(op, unsigned)).unwrap();
10442 assert!(lowerer.get_bound_signed(signed_result, &arena), "{op}");
10443 assert!(!lowerer.get_bound_signed(unsigned_result, &arena), "{op}");
10444 }
10445 }
10446
10447 #[test]
10448 fn width_materialization_preserves_four_state_register_kind() {
10449 let lowerer = SLTToSIRLowerer::new(true);
10450 let mut builder = SIRBuilder::<usize>::new();
10451 let source = builder.alloc_logic(5);
10452 builder.emit(SIRInstruction::Imm(
10453 source,
10454 SIRValue::new_four_state(0x11u8, 0x10u8),
10455 ));
10456
10457 let widened = lowerer.cast_reg_width_ext(&mut builder, source, 8, true);
10458 let narrowed = lowerer.cast_reg_width_ext(&mut builder, widened, 4, true);
10459
10460 assert!(matches!(
10461 builder.register(&widened),
10462 RegisterType::Logic { width: 8 }
10463 ));
10464 assert!(matches!(
10465 builder.register(&narrowed),
10466 RegisterType::Logic { width: 4 }
10467 ));
10468 }
10469
10470 #[test]
10471 fn mixed_sign_subtraction_bound_is_unsigned() {
10472 let mut arena = SLTNodeArena::<u32>::new();
10473 let lhs = arena
10474 .alloc(SLTNode::Constant(1u8.into(), 0u8.into(), 8, false))
10475 .unwrap();
10476 let rhs = arena
10477 .alloc(SLTNode::Input {
10478 variable: 0,
10479 signed: true,
10480 index: vec![],
10481 access: BitAccess::new(0, 7),
10482 })
10483 .unwrap();
10484 let node = arena
10485 .alloc(SLTNode::Binary(lhs, BinaryOp::Sub, rhs))
10486 .unwrap();
10487 let lowerer = SLTToSIRLowerer::new(false);
10488 assert!(!lowerer.get_bound_signed(node, &arena));
10489 }
10490
10491 #[test]
10492 fn mixed_sign_mux_bound_is_unsigned() {
10493 let mut arena = SLTNodeArena::<u32>::new();
10494 let cond = arena
10495 .alloc(SLTNode::Constant(1u8.into(), 0u8.into(), 1, false))
10496 .unwrap();
10497 let then_expr = arena
10498 .alloc(SLTNode::Input {
10499 variable: 0,
10500 signed: true,
10501 index: vec![],
10502 access: BitAccess::new(0, 7),
10503 })
10504 .unwrap();
10505 let else_expr = arena
10506 .alloc(SLTNode::Input {
10507 variable: 1,
10508 signed: false,
10509 index: vec![],
10510 access: BitAccess::new(0, 7),
10511 })
10512 .unwrap();
10513 let node = arena
10514 .alloc(SLTNode::Mux {
10515 cond,
10516 then_expr,
10517 else_expr,
10518 })
10519 .unwrap();
10520 let lowerer = SLTToSIRLowerer::new(false);
10521 assert!(!lowerer.get_bound_signed(node, &arena));
10522 }
10523
10524 #[test]
10525 fn comparison_bound_is_not_signed() {
10526 let mut arena = SLTNodeArena::<u32>::new();
10527 let lhs = arena
10528 .alloc(SLTNode::Input {
10529 variable: 0,
10530 signed: false,
10531 index: vec![],
10532 access: BitAccess::new(0, 7),
10533 })
10534 .unwrap();
10535 let rhs = arena
10536 .alloc(SLTNode::Input {
10537 variable: 1,
10538 signed: true,
10539 index: vec![],
10540 access: BitAccess::new(0, 7),
10541 })
10542 .unwrap();
10543 let node = arena
10544 .alloc(SLTNode::Binary(lhs, BinaryOp::LtS, rhs))
10545 .unwrap();
10546 let lowerer = SLTToSIRLowerer::new(false);
10547 assert!(!lowerer.get_bound_signed(node, &arena));
10548 }
10549
10550 #[test]
10551 fn unsigned_target_bound_zero_extends_signed_slice_without_losing_state_kind() {
10552 let mut arena = SLTNodeArena::<u32>::new();
10553 let inner = arena
10554 .alloc(SLTNode::Input {
10555 variable: 0,
10556 signed: true,
10557 index: vec![],
10558 access: BitAccess::new(0, 15),
10559 })
10560 .unwrap();
10561 let casted = arena
10562 .alloc(SLTNode::Slice {
10563 expr: inner,
10564 access: BitAccess::new(0, 7),
10565 })
10566 .unwrap();
10567 let mut builder = SIRBuilder::<u32>::new();
10568 let mut cache = crate::HashMap::default();
10569 let lowerer = SLTToSIRLowerer::new(false);
10570 let reg = lowerer.lower_bound(
10571 &mut builder,
10572 &SLTLoopBound::Expr(casted),
10573 8,
10574 9,
10575 false,
10576 &arena,
10577 &mut cache,
10578 None,
10579 );
10580 assert!(matches!(
10581 builder.register(®),
10582 RegisterType::Logic { width: 9 }
10583 ));
10584 assert!(!lowerer.get_bound_signed(casted, &arena));
10585 }
10586}