Skip to main content

solar_codegen/backend/evm/stack/
shuffler.rs

1//! Stack shuffler for optimal stack layout transitions.
2//!
3//! This module implements a greedy shuffler algorithm similar to solc's approach.
4//! The shuffler converts a source stack layout to a target layout using minimal
5//! DUP, SWAP, and POP operations.
6//!
7//! ## Algorithm Overview
8//!
9//! The shuffler uses a greedy approach with multiplicity tracking:
10//! 1. Count how many copies of each value are needed in the target
11//! 2. DUP values that need multiple copies
12//! 3. SWAP values to correct positions
13//! 4. POP excess values
14//!
15//! Key insight: Values that can be "freely generated" (literals, function labels)
16//! don't need to be in the source stack - they can be pushed when needed.
17
18use super::model::{MAX_STACK_ACCESS, StackModel, StackOp};
19use crate::mir::ValueId;
20use smallvec::{SmallVec, ToSmallVec};
21use solar_data_structures::map::FxHashMap;
22
23/// Result of a shuffle operation.
24#[derive(Clone, Debug, Default)]
25pub struct ShuffleResult {
26    /// The sequence of operations to perform.
27    pub ops: Vec<StackOp>,
28    /// Number of DUP operations.
29    pub dup_count: usize,
30    /// Number of SWAP operations.
31    pub swap_count: usize,
32    /// Number of POP operations.
33    pub pop_count: usize,
34}
35
36impl ShuffleResult {
37    /// Creates an empty shuffle result.
38    #[must_use]
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Returns true if no operations are needed.
44    #[must_use]
45    pub fn is_empty(&self) -> bool {
46        self.ops.is_empty()
47    }
48
49    /// Returns the total number of operations.
50    #[must_use]
51    pub fn op_count(&self) -> usize {
52        self.ops.len()
53    }
54
55    /// Estimated gas cost (DUP/SWAP = 3 gas each, POP = 2 gas).
56    #[must_use]
57    pub fn estimated_gas(&self) -> u64 {
58        (self.dup_count + self.swap_count) as u64 * 3 + self.pop_count as u64 * 2
59    }
60}
61
62/// Represents a slot in the target layout.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum TargetSlot {
65    /// A specific value must be in this slot.
66    Value(ValueId),
67    /// This slot should be empty (value will be popped).
68    Empty,
69    /// Any value is acceptable (don't care).
70    Any,
71}
72
73/// The stack shuffler transforms a source stack layout to a target layout.
74pub struct StackShuffler<'a> {
75    /// Current source stack (mutable during shuffling).
76    source: SmallVec<[Option<ValueId>; 16]>,
77    /// Target layout we're shuffling to.
78    target: &'a [TargetSlot],
79    /// Operations generated so far.
80    ops: Vec<StackOp>,
81    /// Multiplicity: how many copies of each value are needed.
82    multiplicities: FxHashMap<ValueId, usize>,
83}
84
85impl<'a> StackShuffler<'a> {
86    /// Creates a new shuffler to transform source to target layout.
87    pub fn new(source: &StackModel, target: &'a [TargetSlot]) -> Self {
88        let source_stack: SmallVec<[Option<ValueId>; 16]> =
89            source.as_slice().iter().copied().collect();
90
91        // Count multiplicities in target
92        let mut multiplicities = FxHashMap::default();
93        for slot in target {
94            if let TargetSlot::Value(v) = slot {
95                *multiplicities.entry(*v).or_insert(0) += 1;
96            }
97        }
98
99        Self { source: source_stack, target, ops: Vec::new(), multiplicities }
100    }
101
102    /// Performs the shuffle and returns the result.
103    pub fn shuffle(mut self) -> ShuffleResult {
104        // Phase 1: Ensure we have enough copies of each value
105        self.ensure_multiplicities();
106
107        // Phase 2: Arrange values to match target positions
108        self.arrange_positions();
109
110        // Phase 3: Pop excess values
111        self.pop_excess();
112
113        // Count operation types
114        let mut result = ShuffleResult::new();
115        for op in &self.ops {
116            match op {
117                StackOp::Dup(_) => result.dup_count += 1,
118                StackOp::Swap(_) => result.swap_count += 1,
119                StackOp::Pop => result.pop_count += 1,
120            }
121        }
122        result.ops = self.ops;
123        result
124    }
125
126    /// Phase 1: Ensure we have enough copies of each value in source.
127    fn ensure_multiplicities(&mut self) {
128        // For each value, check if we have enough copies
129        for (&value, &needed) in self.multiplicities.iter() {
130            let current_count = self.source.iter().filter(|&&v| v == Some(value)).count();
131            if current_count < needed {
132                // Need to DUP this value
133                if let Some(depth) = self.find_value(value)
134                    && depth < MAX_STACK_ACCESS
135                {
136                    for _ in current_count..needed {
137                        let dup_n = (self.find_value(value).unwrap_or(0) + 1) as u8;
138                        if dup_n <= 16 {
139                            self.ops.push(StackOp::Dup(dup_n));
140                            self.source.insert(0, Some(value));
141                        }
142                    }
143                }
144            }
145        }
146    }
147
148    /// Phase 2: Arrange values to match target positions using SWAPs.
149    fn arrange_positions(&mut self) {
150        // Work from top of stack downward
151        for target_depth in 0..self.target.len().min(self.source.len()) {
152            let target_slot = &self.target[target_depth];
153
154            match target_slot {
155                TargetSlot::Value(target_val) => {
156                    // Check if correct value is already at this position
157                    if self.source.get(target_depth) == Some(&Some(*target_val)) {
158                        continue;
159                    }
160
161                    // Find where the target value currently is
162                    if let Some(source_depth) = self.find_value_from(*target_val, target_depth)
163                        && source_depth != target_depth
164                        && source_depth < MAX_STACK_ACCESS
165                    {
166                        // Need to swap
167                        if target_depth == 0 {
168                            // Simple case: swap to top
169                            let swap_n = source_depth as u8;
170                            if (1..=16).contains(&swap_n) {
171                                self.ops.push(StackOp::Swap(swap_n));
172                                self.source.swap(0, source_depth);
173                            }
174                        } else {
175                            // Need to bring target value to position target_depth
176                            // First swap current top to target_depth, then bring value to
177                            // top, then swap back
178                            if target_depth < MAX_STACK_ACCESS && source_depth < MAX_STACK_ACCESS {
179                                // Swap top with target_depth position
180                                let swap1 = target_depth as u8;
181                                if (1..=16).contains(&swap1) {
182                                    self.ops.push(StackOp::Swap(swap1));
183                                    self.source.swap(0, target_depth);
184                                }
185
186                                // Now find where the value we need is
187                                if let Some(new_depth) = self.find_value(*target_val)
188                                    && new_depth > 0
189                                    && new_depth < MAX_STACK_ACCESS
190                                {
191                                    let swap2 = new_depth as u8;
192                                    if (1..=16).contains(&swap2) {
193                                        self.ops.push(StackOp::Swap(swap2));
194                                        self.source.swap(0, new_depth);
195                                    }
196                                }
197
198                                // Swap back to put the value at target_depth
199                                if (1..=16).contains(&swap1) {
200                                    self.ops.push(StackOp::Swap(swap1));
201                                    self.source.swap(0, target_depth);
202                                }
203                            }
204                        }
205                    }
206                }
207                TargetSlot::Empty | TargetSlot::Any => {
208                    // Don't care about this position
209                }
210            }
211        }
212    }
213
214    /// Phase 3: Pop excess values from the stack.
215    fn pop_excess(&mut self) {
216        // Count how many of each value we still need
217        let mut still_needed: FxHashMap<ValueId, usize> = FxHashMap::default();
218        for slot in self.target.iter() {
219            if let TargetSlot::Value(v) = slot {
220                *still_needed.entry(*v).or_insert(0) += 1;
221            }
222        }
223
224        // Pop values from top that are no longer needed
225        while !self.source.is_empty() {
226            if let Some(Some(top_val)) = self.source.first() {
227                let needed = still_needed.get(top_val).copied().unwrap_or(0);
228                let current = self.source.iter().filter(|&&v| v == Some(*top_val)).count();
229                if current > needed {
230                    self.ops.push(StackOp::Pop);
231                    self.source.remove(0);
232                } else {
233                    break;
234                }
235            } else if self.source.first() == Some(&None) {
236                // Unknown value on top - pop it if target is shorter
237                if self.source.len() > self.target.len() {
238                    self.ops.push(StackOp::Pop);
239                    self.source.remove(0);
240                } else {
241                    break;
242                }
243            } else {
244                break;
245            }
246        }
247    }
248
249    /// Find the depth of a value in source stack.
250    fn find_value(&self, value: ValueId) -> Option<usize> {
251        self.source.iter().position(|&v| v == Some(value))
252    }
253
254    /// Find a value starting from a minimum depth.
255    fn find_value_from(&self, value: ValueId, min_depth: usize) -> Option<usize> {
256        self.source
257            .iter()
258            .enumerate()
259            .skip(min_depth)
260            .find(|(_, v)| **v == Some(value))
261            .map(|(i, _)| i)
262    }
263}
264
265/// Computes the ideal entry layout for an instruction given the desired exit layout.
266///
267/// This is the core of backward layout analysis:
268/// - Given what we want after the instruction (exit layout)
269/// - Compute what we need before the instruction (entry layout)
270#[derive(Clone, Debug)]
271pub struct LayoutAnalysis {
272    /// Ideal entry layouts for each instruction (block_id, inst_idx) -> layout.
273    pub entry_layouts: FxHashMap<(usize, usize), Vec<TargetSlot>>,
274    /// Ideal exit layouts for each block.
275    pub block_exit_layouts: FxHashMap<usize, Vec<TargetSlot>>,
276}
277
278impl LayoutAnalysis {
279    /// Creates a new empty layout analysis.
280    #[must_use]
281    pub fn new() -> Self {
282        Self { entry_layouts: FxHashMap::default(), block_exit_layouts: FxHashMap::default() }
283    }
284
285    /// Analyzes a sequence of instructions backward to compute ideal layouts.
286    ///
287    /// Given:
288    /// - A sequence of (operands, result) pairs representing instructions
289    /// - The desired exit layout after the last instruction
290    ///
291    /// Returns the ideal entry layout before the first instruction.
292    pub fn analyze_backward(
293        instructions: &[(Vec<ValueId>, Option<ValueId>)],
294        exit_layout: &[TargetSlot],
295    ) -> Vec<TargetSlot> {
296        let mut current_layout = exit_layout.to_vec();
297
298        // Work backwards through instructions
299        for (operands, result) in instructions.iter().rev() {
300            current_layout =
301                Self::compute_entry_for_instruction(operands, *result, &current_layout);
302        }
303
304        current_layout
305    }
306
307    /// Computes the ideal entry layout for a single instruction.
308    fn compute_entry_for_instruction(
309        operands: &[ValueId],
310        result: Option<ValueId>,
311        exit_layout: &[TargetSlot],
312    ) -> Vec<TargetSlot> {
313        let mut entry = Vec::new();
314
315        // Operands should be on top of stack (first operand at depth 0)
316        for &op in operands {
317            entry.push(TargetSlot::Value(op));
318        }
319
320        // Rest of exit layout, excluding the result (which will be produced)
321        for slot in exit_layout {
322            match slot {
323                TargetSlot::Value(v) if Some(*v) == result => {
324                    // Skip - this will be produced by the instruction
325                }
326                _ => entry.push(*slot),
327            }
328        }
329
330        entry
331    }
332}
333
334impl Default for LayoutAnalysis {
335    fn default() -> Self {
336        Self::new()
337    }
338}
339
340/// Represents a stack layout for a block - the values that should be on the stack
341/// when entering or exiting a block, from top to bottom.
342#[derive(Clone, Debug, Default, PartialEq, Eq)]
343pub struct BlockStackLayout {
344    /// Stack slots from top (index 0) to bottom.
345    /// `None` means "don't care" / junk slot.
346    pub slots: SmallVec<[Option<ValueId>; 8]>,
347}
348
349impl BlockStackLayout {
350    /// Creates an empty layout.
351    #[must_use]
352    pub fn new() -> Self {
353        Self { slots: SmallVec::new() }
354    }
355
356    /// Creates a layout with the given values (first = top of stack).
357    #[must_use]
358    pub fn from_values(values: impl IntoIterator<Item = ValueId>) -> Self {
359        Self { slots: values.into_iter().map(Some).collect() }
360    }
361
362    /// Creates a layout from a StackModel.
363    #[must_use]
364    pub fn from_stack_model(model: &StackModel) -> Self {
365        Self { slots: model.as_slice().to_smallvec() }
366    }
367
368    /// Returns the depth (number of slots).
369    #[must_use]
370    pub fn depth(&self) -> usize {
371        self.slots.len()
372    }
373
374    /// Returns true if the layout is empty.
375    #[must_use]
376    pub fn is_empty(&self) -> bool {
377        self.slots.is_empty()
378    }
379
380    /// Gets the value at a given depth (0 = top).
381    #[must_use]
382    pub fn get(&self, depth: usize) -> Option<ValueId> {
383        self.slots.get(depth).copied().flatten()
384    }
385
386    /// Converts to a target layout for the shuffler.
387    #[must_use]
388    pub fn to_target_layout(&self) -> Vec<TargetSlot> {
389        self.slots
390            .iter()
391            .map(|&v| match v {
392                Some(val) => TargetSlot::Value(val),
393                None => TargetSlot::Any,
394            })
395            .collect()
396    }
397
398    /// Finds the position of a value in the layout.
399    #[must_use]
400    pub fn find(&self, value: ValueId) -> Option<usize> {
401        self.slots.iter().position(|&v| v == Some(value))
402    }
403
404    /// Returns true if the layout contains a value.
405    #[must_use]
406    pub fn contains(&self, value: ValueId) -> bool {
407        self.find(value).is_some()
408    }
409}
410
411/// Computes a common stack layout for multiple incoming edges at a merge point.
412///
413/// The algorithm:
414/// 1. Find values that appear in multiple incoming layouts at the same position
415/// 2. For values at different positions, choose the position with lowest total shuffle cost
416/// 3. Values only needed by one predecessor become "junk slots" for others
417///
418/// Returns `None` if the layouts are incompatible (too many different values).
419pub fn combine_stack_layouts(layouts: &[BlockStackLayout]) -> Option<BlockStackLayout> {
420    if layouts.is_empty() {
421        return Some(BlockStackLayout::new());
422    }
423
424    if layouts.len() == 1 {
425        return Some(layouts[0].clone());
426    }
427
428    // Find the maximum depth across all layouts
429    let max_depth = layouts.iter().map(|l| l.depth()).max().unwrap_or(0);
430    if max_depth == 0 {
431        return Some(BlockStackLayout::new());
432    }
433
434    // Collect all values that appear in any layout
435    let mut all_values: FxHashMap<ValueId, Vec<(usize, usize)>> = FxHashMap::default();
436    for (layout_idx, layout) in layouts.iter().enumerate() {
437        for (depth, slot) in layout.slots.iter().enumerate() {
438            if let Some(val) = slot {
439                all_values.entry(*val).or_default().push((layout_idx, depth));
440            }
441        }
442    }
443
444    // Build the combined layout
445    // Strategy: for each position, find the value that appears most often at that position
446    let mut combined = BlockStackLayout { slots: smallvec::smallvec![None; max_depth] };
447
448    // First pass: place values that are at the same position in all layouts
449    for depth in 0..max_depth {
450        let mut value_at_depth: Option<ValueId> = None;
451        let mut consistent = true;
452
453        for layout in layouts {
454            let val = layout.get(depth);
455            match (value_at_depth, val) {
456                (None, Some(v)) => value_at_depth = Some(v),
457                (Some(existing), Some(v)) if existing != v => {
458                    consistent = false;
459                    break;
460                }
461                _ => {}
462            }
463        }
464
465        if consistent {
466            combined.slots[depth] = value_at_depth;
467        }
468    }
469
470    // Second pass: for values not yet placed, find best position
471    for (&value, positions) in &all_values {
472        if combined.contains(value) {
473            continue; // Already placed
474        }
475
476        // Find the most common position for this value
477        let mut pos_counts: FxHashMap<usize, usize> = FxHashMap::default();
478        for &(_, depth) in positions {
479            *pos_counts.entry(depth).or_default() += 1;
480        }
481
482        // Pick the position with highest count, preferring lower depths on ties
483        if let Some((&best_pos, _)) = pos_counts.iter().max_by(|a, b| {
484            a.1.cmp(b.1).then_with(|| b.0.cmp(a.0)) // Higher count, then lower depth
485        }) && best_pos < combined.slots.len()
486            && combined.slots[best_pos].is_none()
487        {
488            combined.slots[best_pos] = Some(value);
489        }
490    }
491
492    // Trim trailing None values
493    while combined.slots.last() == Some(&None) {
494        combined.slots.pop();
495    }
496
497    Some(combined)
498}
499
500/// Computes the shuffle cost from a source layout to a target layout.
501/// This estimates the number of DUP/SWAP/POP operations needed.
502#[must_use]
503pub fn estimate_shuffle_cost(source: &BlockStackLayout, target: &BlockStackLayout) -> usize {
504    let mut cost = 0;
505
506    // Count mismatched positions (need SWAP)
507    for (depth, target_val) in
508        target.slots.iter().enumerate().filter_map(|(i, s)| s.map(|v| (i, v)))
509    {
510        match source.find(target_val) {
511            Some(src_depth) if src_depth != depth => {
512                // Need to move this value - costs at least 1 SWAP
513                cost += 1;
514            }
515            None => {
516                // Value not in source - need to push or reload
517                cost += 2; // Push is more expensive
518            }
519            _ => {} // Already in correct position
520        }
521    }
522
523    // Count values in source that aren't in target (need POP)
524    for val in source.slots.iter().flatten() {
525        if !target.contains(*val) {
526            cost += 1; // POP
527        }
528    }
529
530    // Count values that need DUP (appear more times in target than source)
531    for val in target.slots.iter().flatten() {
532        let source_count = source.slots.iter().filter(|&&v| v == Some(*val)).count();
533        let target_count = target.slots.iter().filter(|&&v| v == Some(*val)).count();
534        if target_count > source_count {
535            cost += target_count - source_count; // DUP operations
536        }
537    }
538
539    cost
540}
541
542/// Represents a "freely generable" value that can be pushed without being on the stack.
543/// These values don't need to be in the source layout when shuffling.
544#[derive(Clone, Copy, Debug, PartialEq, Eq)]
545pub enum FreelyGenerable {
546    /// An immediate value (literal).
547    Immediate,
548    /// A function argument (can be loaded from calldata).
549    Argument,
550    /// A function label (for internal calls).
551    FunctionLabel,
552}
553
554/// Checks if a value can be freely generated (doesn't need to be on stack).
555///
556/// Freely generable values include:
557/// - Literals/immediates
558/// - Function arguments (loaded from calldata)
559/// - Function labels (for internal calls)
560pub fn is_freely_generable(func: &crate::mir::Function, value: ValueId) -> bool {
561    matches!(func.value(value), crate::mir::Value::Immediate(_) | crate::mir::Value::Arg { .. })
562}
563
564/// Creates an ideal layout for preparing multiple operands.
565///
566/// Given operands [a, b, c] where we want a on top:
567/// - Returns [Value(a), Value(b), Value(c)]
568pub fn ideal_operand_layout(operands: &[ValueId]) -> Vec<TargetSlot> {
569    operands.iter().map(|&v| TargetSlot::Value(v)).collect()
570}
571
572/// Computes the ideal pre-layout for a binary operation.
573///
574/// For a binary op that consumes [a, b] (a on top, b below) and produces result:
575/// - If we want [result, ...rest] after
576/// - We need [a, b, ...rest] before (where rest doesn't contain a or b)
577pub fn ideal_binary_op_entry(
578    a: ValueId,
579    b: ValueId,
580    result: Option<ValueId>,
581    exit_layout: &[TargetSlot],
582) -> Vec<TargetSlot> {
583    let mut entry = Vec::with_capacity(exit_layout.len() + 1);
584
585    // The operands should be on top: a at depth 0, b at depth 1
586    entry.push(TargetSlot::Value(a));
587    entry.push(TargetSlot::Value(b));
588
589    // The rest of the exit layout (skipping the result position)
590    for slot in exit_layout.iter() {
591        match slot {
592            TargetSlot::Value(v) if Some(*v) == result => {
593                // Skip the result - it will be produced by the operation
594            }
595            _ => entry.push(*slot),
596        }
597    }
598
599    entry
600}
601
602/// Computes the ideal pre-layout for a unary operation.
603pub fn ideal_unary_op_entry(
604    operand: ValueId,
605    result: Option<ValueId>,
606    exit_layout: &[TargetSlot],
607) -> Vec<TargetSlot> {
608    let mut entry = Vec::with_capacity(exit_layout.len());
609
610    // The operand should be on top
611    entry.push(TargetSlot::Value(operand));
612
613    // The rest of the exit layout (skipping the result position)
614    for slot in exit_layout.iter() {
615        match slot {
616            TargetSlot::Value(v) if Some(*v) == result => {
617                // Skip the result - it will be produced by the operation
618            }
619            _ => entry.push(*slot),
620        }
621    }
622
623    entry
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    fn make_model(values: &[Option<ValueId>]) -> StackModel {
631        let mut model = StackModel::new();
632        // Push in reverse order so first element ends up on top
633        for &v in values.iter().rev() {
634            if let Some(val) = v {
635                model.push(val);
636            } else {
637                model.push_unknown();
638            }
639        }
640        model
641    }
642
643    #[test]
644    fn test_shuffle_already_correct() {
645        let v0 = ValueId::from_usize(0);
646        let v1 = ValueId::from_usize(1);
647
648        let source = make_model(&[Some(v0), Some(v1)]);
649        let target = [TargetSlot::Value(v0), TargetSlot::Value(v1)];
650
651        let result = StackShuffler::new(&source, &target).shuffle();
652        assert!(result.is_empty());
653    }
654
655    #[test]
656    fn test_shuffle_swap_needed() {
657        let v0 = ValueId::from_usize(0);
658        let v1 = ValueId::from_usize(1);
659
660        // Source: [v1, v0] (v1 on top)
661        let source = make_model(&[Some(v1), Some(v0)]);
662        // Target: [v0, v1] (v0 on top)
663        let target = [TargetSlot::Value(v0), TargetSlot::Value(v1)];
664
665        let result = StackShuffler::new(&source, &target).shuffle();
666        assert_eq!(result.swap_count, 1);
667        assert!(result.ops.contains(&StackOp::Swap(1)));
668    }
669
670    #[test]
671    fn test_shuffle_dup_needed() {
672        let v0 = ValueId::from_usize(0);
673
674        // Source: [v0]
675        let source = make_model(&[Some(v0)]);
676        // Target: [v0, v0] (need two copies)
677        let target = [TargetSlot::Value(v0), TargetSlot::Value(v0)];
678
679        let result = StackShuffler::new(&source, &target).shuffle();
680        assert_eq!(result.dup_count, 1);
681    }
682
683    #[test]
684    fn test_shuffle_pop_excess() {
685        let v0 = ValueId::from_usize(0);
686        let v1 = ValueId::from_usize(1);
687
688        // Source: [v0, v1] (v0 on top)
689        let source = make_model(&[Some(v0), Some(v1)]);
690        // Target: [v1] (only need v1)
691        let target = [TargetSlot::Value(v1)];
692
693        let result = StackShuffler::new(&source, &target).shuffle();
694        // Should swap v1 to top, then pop v0
695        assert!(result.pop_count >= 1 || result.swap_count >= 1);
696    }
697
698    #[test]
699    fn test_ideal_binary_op_entry() {
700        let a = ValueId::from_usize(0);
701        let b = ValueId::from_usize(1);
702        let result = ValueId::from_usize(2);
703        let extra = ValueId::from_usize(3);
704
705        // Exit layout: [result, extra]
706        let exit = [TargetSlot::Value(result), TargetSlot::Value(extra)];
707
708        let entry = ideal_binary_op_entry(a, b, Some(result), &exit);
709
710        // Entry should be: [a, b, extra]
711        assert_eq!(entry.len(), 3);
712        assert_eq!(entry[0], TargetSlot::Value(a));
713        assert_eq!(entry[1], TargetSlot::Value(b));
714        assert_eq!(entry[2], TargetSlot::Value(extra));
715    }
716
717    #[test]
718    fn test_backward_layout_analysis() {
719        let a = ValueId::from_usize(0);
720        let b = ValueId::from_usize(1);
721        let c = ValueId::from_usize(2);
722        let r1 = ValueId::from_usize(3);
723        let r2 = ValueId::from_usize(4);
724
725        // Two instructions:
726        // 1. r1 = ADD(a, b)
727        // 2. r2 = MUL(r1, c)
728        let instructions = vec![
729            (vec![a, b], Some(r1)),  // ADD(a, b) -> r1
730            (vec![r1, c], Some(r2)), // MUL(r1, c) -> r2
731        ];
732
733        // Desired exit: [r2]
734        let exit = vec![TargetSlot::Value(r2)];
735
736        let entry = LayoutAnalysis::analyze_backward(&instructions, &exit);
737
738        // Entry should be: [a, b, c]
739        // Because:
740        // - MUL needs [r1, c] and produces r2
741        // - ADD needs [a, b] and produces r1
742        assert_eq!(entry.len(), 3);
743        assert_eq!(entry[0], TargetSlot::Value(a));
744        assert_eq!(entry[1], TargetSlot::Value(b));
745        assert_eq!(entry[2], TargetSlot::Value(c));
746    }
747
748    #[test]
749    fn test_ideal_operand_layout() {
750        let a = ValueId::from_usize(0);
751        let b = ValueId::from_usize(1);
752        let c = ValueId::from_usize(2);
753
754        let layout = ideal_operand_layout(&[a, b, c]);
755
756        assert_eq!(layout.len(), 3);
757        assert_eq!(layout[0], TargetSlot::Value(a));
758        assert_eq!(layout[1], TargetSlot::Value(b));
759        assert_eq!(layout[2], TargetSlot::Value(c));
760    }
761
762    #[test]
763    fn test_shuffle_complex_rearrangement() {
764        let v0 = ValueId::from_usize(0);
765        let v1 = ValueId::from_usize(1);
766        let v2 = ValueId::from_usize(2);
767
768        // Source: [v0, v1, v2] (v0 on top)
769        let source = make_model(&[Some(v0), Some(v1), Some(v2)]);
770        // Target: [v2, v0, v1] (v2 on top)
771        let target = [TargetSlot::Value(v2), TargetSlot::Value(v0), TargetSlot::Value(v1)];
772
773        let result = StackShuffler::new(&source, &target).shuffle();
774
775        // Should use swaps to rearrange
776        assert!(result.swap_count > 0);
777    }
778
779    #[test]
780    fn test_block_stack_layout_basic() {
781        let v0 = ValueId::from_usize(0);
782        let v1 = ValueId::from_usize(1);
783
784        let layout = BlockStackLayout::from_values([v0, v1]);
785        assert_eq!(layout.depth(), 2);
786        assert_eq!(layout.get(0), Some(v0));
787        assert_eq!(layout.get(1), Some(v1));
788        assert!(layout.contains(v0));
789        assert!(layout.contains(v1));
790        assert_eq!(layout.find(v0), Some(0));
791        assert_eq!(layout.find(v1), Some(1));
792    }
793
794    #[test]
795    fn test_block_stack_layout_to_target() {
796        let v0 = ValueId::from_usize(0);
797        let v1 = ValueId::from_usize(1);
798
799        let layout = BlockStackLayout::from_values([v0, v1]);
800        let target = layout.to_target_layout();
801
802        assert_eq!(target.len(), 2);
803        assert_eq!(target[0], TargetSlot::Value(v0));
804        assert_eq!(target[1], TargetSlot::Value(v1));
805    }
806
807    #[test]
808    fn test_combine_stack_layouts_single() {
809        let v0 = ValueId::from_usize(0);
810        let v1 = ValueId::from_usize(1);
811
812        let layout = BlockStackLayout::from_values([v0, v1]);
813        let combined = combine_stack_layouts(std::slice::from_ref(&layout));
814
815        assert!(combined.is_some());
816        assert_eq!(combined.unwrap(), layout);
817    }
818
819    #[test]
820    fn test_combine_stack_layouts_identical() {
821        let v0 = ValueId::from_usize(0);
822        let v1 = ValueId::from_usize(1);
823
824        let layout1 = BlockStackLayout::from_values([v0, v1]);
825        let layout2 = BlockStackLayout::from_values([v0, v1]);
826        let combined = combine_stack_layouts(&[layout1, layout2]);
827
828        assert!(combined.is_some());
829        let result = combined.unwrap();
830        assert_eq!(result.get(0), Some(v0));
831        assert_eq!(result.get(1), Some(v1));
832    }
833
834    #[test]
835    fn test_combine_stack_layouts_different_values() {
836        let v0 = ValueId::from_usize(0);
837        let v1 = ValueId::from_usize(1);
838        let v2 = ValueId::from_usize(2);
839
840        // Layout1: [v0, v1]
841        // Layout2: [v0, v2]
842        // Combined should have v0 at position 0 (consistent)
843        let layout1 = BlockStackLayout::from_values([v0, v1]);
844        let layout2 = BlockStackLayout::from_values([v0, v2]);
845        let combined = combine_stack_layouts(&[layout1, layout2]);
846
847        assert!(combined.is_some());
848        let result = combined.unwrap();
849        assert_eq!(result.get(0), Some(v0)); // Consistent at position 0
850    }
851
852    #[test]
853    fn test_estimate_shuffle_cost_identical() {
854        let v0 = ValueId::from_usize(0);
855        let v1 = ValueId::from_usize(1);
856
857        let layout = BlockStackLayout::from_values([v0, v1]);
858        let cost = estimate_shuffle_cost(&layout, &layout);
859
860        assert_eq!(cost, 0);
861    }
862
863    #[test]
864    fn test_estimate_shuffle_cost_swap() {
865        let v0 = ValueId::from_usize(0);
866        let v1 = ValueId::from_usize(1);
867
868        let source = BlockStackLayout::from_values([v0, v1]);
869        let target = BlockStackLayout::from_values([v1, v0]);
870        let cost = estimate_shuffle_cost(&source, &target);
871
872        // Both values need to move positions
873        assert!(cost > 0);
874    }
875}