Skip to main content

celox_slt/
lower.rs

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
15/// Try to evaluate an SLT node as a compile-time constant.
16/// Returns `Some((value, mask))` if the entire subtree is constant, `None` otherwise.
17fn 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            // Only fold 2-state (no X/Z) constants for safety.
30            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, // Input, Mux — not constant
82    }
83}
84
85#[derive(Clone)]
86enum SLTBitOrigin<A: Hash + Eq + Clone> {
87    Node(NodeId),
88    Input {
89        /// One representative input node. This is provenance for memory type
90        /// lookup and deliberately does not participate in origin identity:
91        /// unrolled lanes have distinct nodes but the same logical input.
92        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    /// A packed input reconstructed from a proven identity-indexed bit read.
160    /// Unlike `SLTBitOrigin::Input`, this deliberately drops the dynamic lane
161    /// index and loads the complete packed word at its static base offset.
162    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    /// Add a newly recovered population-count delta to an exact accumulator
195    /// value that already dominates the current lowering point.
196    AddTo(NodeId),
197    /// Turn `clz(predicates)` into the selected last-write index.  With an
198    /// all-ones default, `N - 1 - clz(0)` naturally wraps to that sentinel.
199    SubtractFrom(u64),
200    /// Map the count operation's zero-input result (`input_width`) back to the
201    /// sentinel used by the procedural priority encoder.
202    ReplaceZeroInputCount(u64),
203    /// Preserve a conditional accumulator seed around the recovered count.
204    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
240/// Procedural control represents truth as `ToTwoState(Or(cond))`. Count-idiom
241/// lowering is enabled only in two-state mode, where that exact pair is an
242/// identity for a one-bit `cond`. Do not look through a real wide reduction.
243fn 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            // A numeric conditional increment must be exactly a zero-extended
338            // bit in the LSB position.  Merely finding one nonzero one-bit
339            // part is insufficient: `{bit, 0...}` contributes 2^K, not 1.
340            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    // `{(W-1 < bound), ..., (0 < bound)}` is the saturated low-ones mask
425    // `(1_W << bound) - 1`.  Native shift legalization defines shifts by W or
426    // more as zero, so the expression also produces all ones for bound >= W.
427    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        // Only reuse the immediate predecessor.  A longer partial suffix can
532        // destroy a profitable whole-vector count shape; one exact +1 delta
533        // is always the recurrence edge we are replacing.
534        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        // See the conditional form above: a materialized immediate
576        // predecessor is an exact delta edge, not an arbitrary split point.
577        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        // Procedural `if outer { if inner { acc = constant; } }` expands to
872        // two muxes with the same else accumulator.  Treat it as one write
873        // guarded by `outer && inner`; this preserves the exact mux semantics
874        // while avoiding dependence on source-level loop structure.
875        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    // A last-write mux chain with values 0, 1, ..., N-1 is a priority
946    // encoder over its conditions.  Collecting from the root visits the
947    // highest-priority condition first, so `N - 1 - clz(conditions)` yields
948    // the selected value.  This is exact for arbitrary predicates; no claim
949    // about how those predicates were produced is required.  For no match,
950    // clz is N and the subtraction wraps to the original all-ones sentinel.
951    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
1106/// Whether the ordinary expanded SLT already matches a native count idiom.
1107/// Loop recovery uses this as a semantic priority check so it does not replace
1108/// an exact PopCount/CLZ/CTZ plan with a slower counted loop.
1109pub 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    /// Nodes used by both arms which were not already materialized.  They must
1167    /// be evaluated once in the dominator before the control-flow split.
1168    shared_nodes: Vec<NodeId>,
1169}
1170
1171#[derive(Clone, Default)]
1172struct ZeroControllerFacts {
1173    /// The expression is the known two-state value zero independently of any
1174    /// runtime predicate.  Such a child imposes no constraint when an outer
1175    /// operation requires all of its children to be zero.
1176    unconditional_zero: bool,
1177    /// One-bit descendants whose false value is sufficient to prove this
1178    /// expression is all zero.
1179    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    /// Lower-priority bindings from an enclosing lowering scope.  Keeping the
1251    /// layers separate is important for partial state targets: flattening the
1252    /// maps would make overlapping inner/outer ranges depend on HashMap
1253    /// iteration order.
1254    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
1297/// A proven fixed-width first-true scan.  This is deliberately a transient
1298/// lowering plan rather than another SLT node: `ForFoldGroup` remains the
1299/// semantic representation and every near miss uses its generic counted-loop
1300/// lowering.
1301struct 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    // The matcher treats the induction value as the unsigned lane number.
1384    // A signed counter is equivalent on this finite domain only while every
1385    // value 0..trip_count-1 remains in its non-negative representable range.
1386    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        // Analyzer casts of a non-negative unrolled IV commonly survive as
1448        // `iv & low_mask`.  It is still the identity over this exact finite
1449        // trip domain iff every bit needed to represent `0..trip_count` is
1450        // retained.  Reject masks that drop even one such bit.
1451        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        // A direct narrow indexed input denotes `variable[iv]`; the complete
1481        // identity traversal therefore reconstructs bits `0..trip_count-1`.
1482        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    // Procedural control normalizes a condition as ToTwoState(|cond).  The
1550    // word-scan plan is emitted only in two-state mode, so that pair is an
1551    // identity when the original condition is already one bit.  Look through
1552    // exactly that shape; a reduction of a wider condition is not lane-wise.
1553    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    /// Recursively expand SLT nodes into SIR instructions
1996    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    /// Lower several independent recovered folds as one counted loop.
2008    ///
2009    /// This entry point is intentionally transactional: a rejected family
2010    /// leaves both the builder and the materialization cache unchanged.  The
2011    /// scheduler remains responsible for proving that the roots are mutually
2012    /// unordered in its dependency graph; this method rechecks every local
2013    /// property needed by the joint loop itself.
2014    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        // A word-level scan is strictly cheaper than putting this group back
2036        // into a shared counted loop.  Leave it for ordinary single-root
2037        // lowering, which can apply the algebraic plan without weakening the
2038        // joint-lowering transaction.
2039        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                // A shift count wider than the host word may have non-zero
2301                // high limbs even when its low limb is zero.  Saturate from a
2302                // full-width unsigned comparison instead of relying on the
2303                // legalized shift to distinguish that case.
2304                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    /// Project a value which has already been lowered without retaining every
2494    /// projection at once.  Grouped folds use this after the packed result has
2495    /// been computed, immediately before the corresponding state Store.
2496    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            // Analyzer-unrolled array accesses retain their index syntax even
2723            // when every index expression is a compile-time constant.  Fold
2724            // those accesses back to one logical static bit offset here.  In
2725            // particular, this lets the native element-strided layout choose
2726            // a direct address instead of materializing a fake dynamic index
2727            // for every unrolled lane.
2728            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 &current.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 &current.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            // Apply outer bindings first and inner bindings last so an inner
3046            // loop-carried range wins whenever scopes overlap.
3047            for current in layers.into_iter().rev() {
3048                for (target, reg) in &current.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    /// Get the width recorded by frontend construction.
3123    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            // The grouped result has concat layout and is therefore unsigned,
3200            // independently of the loop counter's signedness.
3201            SLTNode::ForFoldGroup { .. } => false,
3202            // Verilog/Veryl bit- and part-select expressions are unsigned even when
3203            // the source signal is signed.
3204            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        // A cached value is a snapshot at the point where the scheduler first
3233        // lowered this node. Preserve that snapshot instead of introducing a
3234        // later memory read which could cross an intervening Store.
3235        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        // Eliminate an annihilated expression before consulting the shared
3243        // projection cache. Neither operand may be visited: it can contain a
3244        // dead wide division or a loop-carried Input which would otherwise be
3245        // rebuilt as a dynamic read from the current state version.
3246        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        // Cache the projection rather than materializing the full expression.
3256        // Repeated references in a read-modify-write DAG then stay linear while
3257        // wide arithmetic continues to operate on only the requested region.
3258        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    /// Compute exact two-state zero controllers for the small algebra used by
3473    /// guarded lane vectors. A controller `g` is present only when assuming
3474    /// the one-bit value `g == 0` proves every bit of `node` is zero.
3475    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        // Explicit postorder avoids consuming the native stack on procedural
3486        // expression chains. Each node in this concat cone is analyzed once.
3487        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                // This shared compound value covers every zero case of its
3588                // descendant controllers and possibly more. Keeping only the
3589                // maximal value prevents a leaf from winning on a tiny local
3590                // cost difference and keeps deep conjunction sets linear.
3591                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            // The guard and its dependencies are evaluated in the dominator.
3650            // If one is also reached outside the guard expression, the true
3651            // arm consumes that already-materialized value as a live-through.
3652            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            // A nested Mux may already lower to control flow. Counting only
3659            // the Mux itself and none of its condition/arms is a conservative
3660            // lower bound on work skipped by the new outer branch.
3661            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        // Fast path: if all parts are constants, fold into a single wide Imm.
3800        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        // Use SIR Concat instruction directly. This preserves Z bits in 4-state
3826        // mode (unlike the Shl+Or pattern which converts Z to X through Binary Or
3827        // normalization). Concat args are [MSB, ..., LSB] — same order as `parts`.
3828        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    /// Try to fold a Concat of all-constant parts into a single wide Imm.
3842    /// Recursively evaluates each part to check if it's a compile-time constant.
3843    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        // Build the combined value and mask (parts are MSB-first, reverse for LSB-first).
3856        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    /// Return the cache entries created by the most recent top-level `lower`
3993    /// call. A scheduler-owned control arm keeps them available to subsequent
3994    /// paths in that arm, then removes them before lowering the sibling arm.
3995    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    /// Runtime work introduced by this node itself.  Child work is accounted
4112    /// separately so hash-consed descendants can be counted exactly once.
4113    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            // A fold contains at least a loop test, a backedge, loop-carried
4151            // values, and an exit edge.  Its child DAG is still counted below;
4152            // this fixed cost represents the control operation itself rather
4153            // than an input-size or iteration cap.
4154            SLTNode::ForFold { updates, .. } => 8 + 2 * updates.len() as u128,
4155            SLTNode::ForFoldGroup { states, .. } => 6 + 2 * states.len() as u128,
4156        }
4157    }
4158
4159    /// Cheap, memoized upper bound used only to avoid building reachability
4160    /// sets for muxes that cannot possibly pay for a branch.  It may count a
4161    /// shared descendant more than once; the final decision below never does.
4162    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    /// Cost which is provably owned by this node in the current top-level DAG.
4188    /// A node with more than one incoming DAG edge is excluded together with
4189    /// its descendants: charging it to either mux arm could mistake shared CSE
4190    /// work for conditionally skippable work.  The memo makes all nested mux
4191    /// queries constant-time after one traversal of the top-level DAG.
4192    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    /// Width-independent lower bound for region-slice lowering.  A Slice node
4221    /// may compose into its child without emitting an instruction, while every
4222    /// other non-materialized node emits at least its one-chunk operation.
4223    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    /// Find shared expressions without walking either entire arm.  Only a
4330    /// common root or direct operand is hoisted.  If a deeper shared expression
4331    /// exists, the mux remains a Select; this conservative rule preserves CSE
4332    /// and keeps analysis linear for long nested priority-mux chains.
4333    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        // Fold nodes carry a scoped loop environment and lower to CFG.  They
4367        // must not be hoisted or cloned as an ordinary mux-arm expression;
4368        // ForFold can additionally emit effects and Error exits.  All other
4369        // SLT nodes lower to read-only/value instructions.
4370        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                // Ball and Larus, "Branch Prediction for Free" (PLDI 1993),
4531                // predict equality-to-constant tests false.  Their complete
4532                // static heuristic reports a 20% average miss rate; use that
4533                // measured uncertainty as the 20/80 local prior.  This affects
4534                // expected executed cost, never whether analysis is allowed to
4535                // stop or how large a CFG may become.
4536                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        // Native and Cranelift both pay for a conditional transfer, the taken
4601        // arm's merge transfer, and a result phi copy.  With no dynamic profile,
4602        // predict the more likely edge and charge a 16-cycle x86 branch miss on
4603        // the less likely edge.  All terms are scaled by total_weight, so this
4604        // remains exact integer expected-cost arithmetic.
4605        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        // Control flow selects one arm, while a four-state Mux bitwise-merges
4646        // both arms for X/Z conditions. No expression shape may bypass this
4647        // semantic policy.
4648        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            // Slice lowering can be cheaper than computing the corresponding
4742            // full shared node.  Charge the entire full hoist as additional
4743            // always-executed work; this deliberately underestimates the
4744            // transformation's benefit and prevents optimistic branchification.
4745            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    /// Cost-directed reverse if-conversion for symbolic expression DAGs.
4797    ///
4798    /// Cheap pure muxes remain `SIRInstruction::Mux`.  When the expected work
4799    /// skipped by preserving control exceeds branch, prediction, and phi-copy
4800    /// costs, the arms are lowered into separate CFG blocks.  Division and
4801    /// remainder remain a correctness case: an unselected zero divisor must
4802    /// never reach a native divide instruction.
4803    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        // Use Mux instruction: preserves Z in 4-state, branchless select in 2-state.
4854        // Backends handle value and mask selection independently.
4855        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(&reg).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        // `before | first` is true exactly through the first hit.  It is all
5262        // ones when `hits` is zero, matching the sequential `!found` state.
5263        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    /// Lower one or more independent, fixed-trip-count multi-state folds.
5351    ///
5352    /// The loop body sees one immutable set of block parameters, so every
5353    /// update is computed from the previous iteration and the backedge applies
5354    /// all updates simultaneously.  The counter is a remaining-iteration
5355    /// count: it cannot stall and needs neither a safety cap nor an Error exit.
5356    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                // The loop body uses its own environment-scoped cache.  A
5550                // child that was materialized while lowering a guard or an
5551                // initial value is not reusable here: its value may depend on
5552                // the loop variable or an old carried state.  Rebuild the cost
5553                // model from the cache that lower_inner will actually use so
5554                // mux profitability and mandatory lazy Div/Rem lowering do not
5555                // mistake an unavailable outer value for a body-local one.
5556                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        // These entries are valid only under this loop body's state/counter
5573        // environment.  Keep the local CSE results, but remove their tracking
5574        // records before returning to the caller's global cache transaction.
5575        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        // The final value of this addition is unobserved when `has_more` is
5597        // false. Computing it eagerly lets the conditional edge carry the next
5598        // loop parameters directly, avoiding an extra hot advance block and
5599        // unconditional jump on every taken iteration.
5600        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        // SystemVerilog declares the induction variable as a fixed-width
5713        // `int`, so initialization is an assignment to that visible width.
5714        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            // The emitted SystemVerilog loop variable has `loop_width` bits
6008            // (`int`/i32 for Veryl). Apply the compound assignment at that
6009            // width before checking progress or the loop bound; otherwise a
6010            // widened host counter can step through values the emitted loop
6011            // can never represent and incorrectly terminate.
6012            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    /// Execute the small, value-only SIR subset emitted by ForFoldGroup.
6808    /// Keeping this interpreter local to the lowering tests lets the tests pin
6809    /// exact iteration and four-state merge semantics without adding a second
6810    /// production execution path.
6811    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[&current];
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[&reg].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[&register]
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 (&parameter, 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        // Veryl encodes X=(payload 0, mask 1), Z=(payload 1, mask 1).
7814        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        // With a 50/50 prior, ten units in the true arm cannot repay the
8887        // expected branch miss.  When equality is predicted false, 80% of that
8888        // arm is skipped and the same transformation is profitable.
8889        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(&reg),
10582            RegisterType::Logic { width: 9 }
10583        ));
10584        assert!(!lowerer.get_bound_signed(casted, &arena));
10585    }
10586}