Skip to main content

solar_codegen/backend/evm/stack/
scheduler.rs

1//! Stack scheduler for generating DUP/SWAP sequences.
2//!
3//! The scheduler takes operands needed for an instruction and generates
4//! the sequence of stack operations to arrange them on the stack.
5//!
6//! ## Backward Layout Optimization
7//!
8//! This scheduler now supports backward layout analysis:
9//! 1. Define the desired exit layout for each instruction
10//! 2. Compute the ideal entry layout that minimizes shuffling
11//! 3. Use the shuffler to generate optimal DUP/SWAP/POP sequences
12
13use super::{
14    model::{MAX_STACK_ACCESS, StackModel, StackOp},
15    shuffler::{BlockStackLayout, ShuffleResult, StackShuffler, TargetSlot, combine_stack_layouts},
16    spill::{SpillManager, SpillSlot},
17};
18use crate::{
19    analysis::Liveness,
20    mir::{BlockId, Function, ValueId},
21};
22use solar_data_structures::map::FxHashMap;
23
24/// Stack scheduler that generates stack manipulation operations.
25pub struct StackScheduler {
26    /// Current stack state.
27    pub stack: StackModel,
28    /// Spill manager for values beyond stack depth 16.
29    pub spills: SpillManager,
30    /// Operations to emit.
31    ops: Vec<ScheduledOp>,
32    /// Entry layouts for each block (computed from merge points).
33    block_entry_layouts: FxHashMap<BlockId, BlockStackLayout>,
34    /// Exit layouts for each block (computed after generating block).
35    block_exit_layouts: FxHashMap<BlockId, BlockStackLayout>,
36}
37
38/// A scheduled operation to emit.
39#[derive(Clone, Debug)]
40pub enum ScheduledOp {
41    /// Stack manipulation (DUP, SWAP, POP).
42    Stack(StackOp),
43    /// Push an immediate value.
44    PushImmediate(alloy_primitives::U256),
45    /// Load a spilled value from memory.
46    LoadSpill(SpillSlot),
47    /// Spill a value to memory.
48    SaveSpill(SpillSlot),
49    /// Load a function argument from calldata.
50    /// Contains the argument index (0-based).
51    LoadArg(u32),
52}
53
54impl StackScheduler {
55    /// Creates a new stack scheduler.
56    #[must_use]
57    pub fn new() -> Self {
58        Self {
59            stack: StackModel::new(),
60            spills: SpillManager::new(),
61            ops: Vec::new(),
62            block_entry_layouts: FxHashMap::default(),
63            block_exit_layouts: FxHashMap::default(),
64        }
65    }
66
67    /// Clears the scheduled operations (after emitting them).
68    pub fn clear_ops(&mut self) {
69        self.ops.clear();
70    }
71
72    /// Takes the scheduled operations.
73    pub fn take_ops(&mut self) -> Vec<ScheduledOp> {
74        std::mem::take(&mut self.ops)
75    }
76
77    /// Ensures a value is on top of the stack.
78    /// Returns the operations needed to achieve this.
79    pub fn ensure_on_top(&mut self, value: ValueId, func: &Function) -> &[ScheduledOp] {
80        self.ensure_on_top_impl(value, func, true)
81    }
82
83    /// Emits a fresh operand occurrence for a consuming instruction.
84    ///
85    /// If `value` is already on top, `ensure_on_top` can claim that existing stack item. That is
86    /// correct for a single use, but wrong for instructions that consume the same MIR value more
87    /// than once, such as `revert(x, x)` or `log1(x, x, x)`. In those cases every operand
88    /// occurrence needs its own stack item, so a top-of-stack value must be duplicated.
89    pub fn ensure_operand_on_top(&mut self, value: ValueId, func: &Function) -> &[ScheduledOp] {
90        self.ensure_on_top_impl(value, func, false)
91    }
92
93    fn ensure_on_top_impl(
94        &mut self,
95        value: ValueId,
96        func: &Function,
97        claim_top: bool,
98    ) -> &[ScheduledOp] {
99        self.ops.clear();
100
101        if self.stack.is_on_top(value) {
102            if !claim_top {
103                self.ops.push(ScheduledOp::Stack(StackOp::Dup(1)));
104                self.stack.dup(1);
105            }
106            return &self.ops;
107        }
108
109        if let Some(depth) = self.stack.find(value) {
110            if depth < MAX_STACK_ACCESS {
111                // Value is accessible via DUP
112                let dup_n = (depth + 1) as u8;
113                self.ops.push(ScheduledOp::Stack(StackOp::Dup(dup_n)));
114                self.stack.dup(dup_n);
115                return &self.ops;
116            }
117            // Value is too deep for DUP. It must either be reloadable from a spill slot or
118            // re-emittable below.
119            if self.spills.is_reloadable(value)
120                && let Some(slot) = self.spills.get(value)
121            {
122                self.ops.push(ScheduledOp::LoadSpill(slot));
123                self.stack.push(value);
124                return &self.ops;
125            }
126        } else if self.spills.is_reloadable(value)
127            && let Some(slot) = self.spills.get(value)
128        {
129            // Value is spilled, load it
130            self.ops.push(ScheduledOp::LoadSpill(slot));
131            self.stack.push(value);
132            return &self.ops;
133        }
134
135        match func.value(value) {
136            crate::mir::Value::Immediate(imm) => {
137                // It's an immediate, push it directly
138                if let Some(u256) = imm.as_u256() {
139                    self.ops.push(ScheduledOp::PushImmediate(u256));
140                    self.stack.push(value);
141                }
142            }
143            crate::mir::Value::Arg { index, .. } => {
144                // It's a function argument, load from calldata
145                self.ops.push(ScheduledOp::LoadArg(*index));
146                self.stack.push(value);
147            }
148            other => {
149                panic!(
150                    "Value {value:?} is not on stack, not spilled, and not an immediate/arg. \
151                         This usually means a cross-block value wasn't spilled before the block exit. \
152                         Stack: {:?}, Spills: {:?}. \
153                         Value kind: {other:?}",
154                    self.stack, self.spills
155                );
156            }
157        }
158
159        &self.ops
160    }
161
162    /// Checks if we can emit a value (it's an immediate, arg, on stack, or spilled).
163    /// Returns false for instruction results that aren't tracked.
164    pub fn can_emit_value(&self, value: ValueId, func: &Function) -> bool {
165        // Check if on stack and reachable by DUP.
166        if let Some(depth) = self.stack.find(value) {
167            return depth < MAX_STACK_ACCESS || self.spills.is_reloadable(value);
168        }
169        // Check if spilled
170        if self.spills.is_reloadable(value) {
171            return true;
172        }
173        // Check value type
174        matches!(func.value(value), crate::mir::Value::Immediate(_) | crate::mir::Value::Arg { .. })
175    }
176
177    /// Ensures multiple values are on top of the stack in order.
178    /// The first value will be at the top, second below it, etc.
179    pub fn ensure_on_top_many(&mut self, values: &[ValueId], func: &Function) -> Vec<ScheduledOp> {
180        let mut all_ops = Vec::new();
181
182        // Push in reverse order so first value ends up on top
183        for &value in values.iter().rev() {
184            self.ensure_operand_on_top(value, func);
185            all_ops.append(&mut self.ops);
186        }
187
188        all_ops
189    }
190
191    /// Brings a specific value to the top of the stack using SWAP.
192    /// The value must already be on the stack within accessible range.
193    pub fn bring_to_top(&mut self, value: ValueId) -> Option<StackOp> {
194        if self.stack.is_on_top(value) {
195            return None;
196        }
197
198        if let Some(depth) = self.stack.find(value)
199            && depth < MAX_STACK_ACCESS
200            && depth > 0
201        {
202            let swap_n = depth as u8;
203            self.stack.swap(swap_n);
204            return Some(StackOp::Swap(swap_n));
205        }
206
207        None
208    }
209
210    /// Records that an instruction consumed its operands and produced a result.
211    /// This updates the stack model accordingly.
212    pub fn instruction_executed(&mut self, consumed: usize, produced: Option<ValueId>) {
213        // Pop consumed values
214        for _ in 0..consumed {
215            self.stack.pop();
216        }
217
218        // Push produced value
219        if let Some(val) = produced {
220            self.stack.push(val);
221        }
222
223        debug_assert!(self.stack.depth() <= 1024, "Stack overflow: depth {}", self.stack.depth());
224    }
225
226    /// Records that an instruction consumed inputs and produced an untracked output.
227    /// The output is on the EVM stack but we don't track which ValueId it corresponds to.
228    /// This is used for MLOAD where the value may become stale in loops.
229    pub fn instruction_executed_untracked(&mut self, consumed: usize) {
230        // Pop consumed values
231        for _ in 0..consumed {
232            self.stack.pop();
233        }
234        // Push an unknown value to keep stack depth correct
235        self.stack.push_unknown();
236    }
237
238    /// Checks if there's an untracked value on top of the stack.
239    pub fn has_untracked_on_top(&self) -> bool {
240        self.stack.depth() > 0 && self.stack.top().is_none()
241    }
242
243    /// Checks if there's an untracked value at a specific depth.
244    pub fn has_untracked_at_depth(&self, depth: usize) -> bool {
245        self.stack.depth() > depth && self.stack.peek(depth).is_none()
246    }
247
248    /// Records that a SWAP1 was executed, updating the stack model.
249    pub fn stack_swapped(&mut self) {
250        self.stack.swap(1);
251    }
252
253    /// Drops dead values from the stack.
254    /// Returns operations (SWAPs and POPs) to remove dead values.
255    pub fn drop_dead_values(
256        &mut self,
257        liveness: &Liveness,
258        block: BlockId,
259        inst_idx: usize,
260    ) -> Vec<StackOp> {
261        let mut ops = Vec::new();
262
263        // First, pop dead values from the top
264        while let Some(top_val) = self.stack.top() {
265            if liveness.is_dead_after(top_val, block, inst_idx) {
266                self.stack.pop();
267                ops.push(StackOp::Pop);
268            } else {
269                break;
270            }
271        }
272
273        // Then, look for dead values deeper in the stack (up to depth 16)
274        // and swap them to the top to pop them
275        let mut depth = 1usize;
276        while depth < self.stack.depth().min(MAX_STACK_ACCESS) {
277            if let Some(val) = self.stack.peek(depth)
278                && liveness.is_dead_after(val, block, inst_idx)
279            {
280                // Swap this dead value to the top and pop it
281                let swap_n = depth as u8;
282                ops.push(StackOp::Swap(swap_n));
283                self.stack.swap(swap_n);
284                ops.push(StackOp::Pop);
285                self.stack.pop();
286                // Don't increment depth since we removed an element
287                continue;
288            }
289            depth += 1;
290        }
291
292        ops
293    }
294
295    /// Spills values to memory to make room on the stack.
296    /// This is needed when stack depth exceeds 16.
297    pub fn spill_excess_values(&mut self) -> Vec<ScheduledOp> {
298        let mut ops = Vec::new();
299
300        if self.stack.depth() > MAX_STACK_ACCESS {
301            // Find a value deep in the stack to spill
302            if let Some(value) = self.stack.peek(MAX_STACK_ACCESS - 1) {
303                let slot = self.spills.allocate(value);
304                ops.push(ScheduledOp::SaveSpill(slot));
305            }
306        }
307
308        ops
309    }
310
311    /// Returns the current stack depth.
312    #[must_use]
313    pub fn stack_depth(&self) -> usize {
314        self.stack.depth()
315    }
316
317    /// Returns the current stack depth (alias for `stack_depth`).
318    #[must_use]
319    pub fn depth(&self) -> usize {
320        self.stack.depth()
321    }
322
323    /// Clears the stack model (used at block boundaries).
324    pub fn clear_stack(&mut self) {
325        self.stack.clear();
326    }
327
328    /// Shuffles the current stack to match the target layout.
329    ///
330    /// This uses the backward layout optimization approach:
331    /// - Given a target layout (what we want the stack to look like)
332    /// - Generate the minimal sequence of DUP/SWAP/POP operations
333    ///
334    /// Returns the shuffle result containing the operations to emit.
335    pub fn shuffle_to_layout(&mut self, target: &[TargetSlot]) -> ShuffleResult {
336        let shuffler = StackShuffler::new(&self.stack, target);
337        let result = shuffler.shuffle();
338
339        // Apply the operations to our stack model
340        for op in &result.ops {
341            match op {
342                StackOp::Dup(n) => self.stack.dup(*n),
343                StackOp::Swap(n) => self.stack.swap(*n),
344                StackOp::Pop => {
345                    self.stack.pop();
346                }
347            }
348        }
349
350        result
351    }
352
353    /// Prepares the stack for a binary operation.
354    ///
355    /// Given operands (a, b) where a should be on top and b below:
356    /// - Computes the target layout [a, b, ...rest]
357    /// - Shuffles current stack to match
358    /// - Returns operations to emit
359    pub fn prepare_binary_op(&mut self, a: ValueId, b: ValueId, _func: &Function) -> ShuffleResult {
360        // Build target layout: [a, b]
361        let target = [TargetSlot::Value(a), TargetSlot::Value(b)];
362
363        // Check if we need to push values that aren't on stack
364        let a_on_stack = self.stack.find(a).is_some();
365        let b_on_stack = self.stack.find(b).is_some();
366
367        if !a_on_stack || !b_on_stack {
368            // Can't shuffle - values need to be pushed first
369            // Fall back to regular ensure_on_top behavior
370            return ShuffleResult::new();
371        }
372
373        self.shuffle_to_layout(&target)
374    }
375
376    /// Prepares the stack for a unary operation.
377    ///
378    /// Given operand that should be on top:
379    /// - Computes the target layout [operand, ...rest]
380    /// - Shuffles current stack to match
381    /// - Returns operations to emit
382    pub fn prepare_unary_op(&mut self, operand: ValueId, _func: &Function) -> ShuffleResult {
383        let target = [TargetSlot::Value(operand)];
384
385        if self.stack.find(operand).is_none() {
386            // Can't shuffle - value needs to be pushed first
387            return ShuffleResult::new();
388        }
389
390        self.shuffle_to_layout(&target)
391    }
392
393    /// Computes the ideal entry layout for a binary operation given the exit layout.
394    ///
395    /// For ADD(a, b) -> result, if exit layout is [result, x, y]:
396    /// - Entry layout should be [a, b, x, y]
397    pub fn compute_binary_entry_layout(
398        a: ValueId,
399        b: ValueId,
400        result: Option<ValueId>,
401        exit_layout: &[TargetSlot],
402    ) -> Vec<TargetSlot> {
403        super::shuffler::ideal_binary_op_entry(a, b, result, exit_layout)
404    }
405
406    /// Computes the ideal entry layout for a unary operation given the exit layout.
407    pub fn compute_unary_entry_layout(
408        operand: ValueId,
409        result: Option<ValueId>,
410        exit_layout: &[TargetSlot],
411    ) -> Vec<TargetSlot> {
412        super::shuffler::ideal_unary_op_entry(operand, result, exit_layout)
413    }
414
415    // ==================== Block Layout Management ====================
416    //
417    // These methods support phi node handling via stack layout merging.
418    // Instead of always spilling values at block boundaries, we can pass
419    // values through the stack by agreeing on a common layout at merge points.
420
421    /// Sets the entry layout for a block.
422    ///
423    /// This is used to specify what the stack should look like when entering
424    /// a block. Predecessors will shuffle their stacks to match this layout.
425    pub fn set_block_entry_layout(&mut self, block: BlockId, layout: BlockStackLayout) {
426        self.block_entry_layouts.insert(block, layout);
427    }
428
429    /// Gets the entry layout for a block, if one has been set.
430    #[must_use]
431    pub fn get_block_entry_layout(&self, block: BlockId) -> Option<&BlockStackLayout> {
432        self.block_entry_layouts.get(&block)
433    }
434
435    /// Records the exit layout for a block (current stack state).
436    ///
437    /// This is called after generating a block's instructions, before the terminator.
438    /// The layout is used to determine what values are on the stack when exiting.
439    pub fn record_block_exit_layout(&mut self, block: BlockId) {
440        let layout = BlockStackLayout::from_stack_model(&self.stack);
441        self.block_exit_layouts.insert(block, layout);
442    }
443
444    /// Gets the exit layout for a block, if one has been recorded.
445    #[must_use]
446    pub fn get_block_exit_layout(&self, block: BlockId) -> Option<&BlockStackLayout> {
447        self.block_exit_layouts.get(&block)
448    }
449
450    /// Computes the entry layout for a merge block from its predecessors' exit layouts.
451    ///
452    /// This is the key function for phi node handling. It finds a common stack layout
453    /// that all predecessors can shuffle to with minimal cost.
454    ///
455    /// The function also considers the live-in values for the block to ensure
456    /// all needed values are on the stack.
457    pub fn compute_merge_layout(
458        &self,
459        predecessors: &[BlockId],
460        live_in: impl IntoIterator<Item = ValueId>,
461    ) -> BlockStackLayout {
462        // Collect exit layouts from predecessors
463        let mut pred_layouts: Vec<BlockStackLayout> = Vec::new();
464        for &pred in predecessors {
465            if let Some(layout) = self.block_exit_layouts.get(&pred) {
466                pred_layouts.push(layout.clone());
467            }
468        }
469
470        // If we have no predecessor layouts, create a layout from live-in values
471        if pred_layouts.is_empty() {
472            let live_in_values: Vec<_> = live_in.into_iter().collect();
473            if live_in_values.is_empty() {
474                return BlockStackLayout::new();
475            }
476            return BlockStackLayout::from_values(live_in_values);
477        }
478
479        // Combine the layouts to find a common one
480        combine_stack_layouts(&pred_layouts).unwrap_or_default()
481    }
482
483    /// Shuffles the current stack to match a block's entry layout.
484    ///
485    /// Returns the shuffle operations needed. The caller is responsible for
486    /// emitting the actual opcodes.
487    pub fn shuffle_to_block_entry(&mut self, target_block: BlockId) -> ShuffleResult {
488        if let Some(target_layout) = self.block_entry_layouts.get(&target_block) {
489            let target_slots = target_layout.to_target_layout();
490            self.shuffle_to_layout(&target_slots)
491        } else {
492            ShuffleResult::new()
493        }
494    }
495
496    /// Initializes the stack from a block's entry layout.
497    ///
498    /// This is called when starting to generate a block that has an entry layout.
499    /// It sets up the stack model to match the expected entry state.
500    pub fn init_from_block_entry_layout(&mut self, block: BlockId) {
501        self.stack.clear();
502        if let Some(layout) = self.block_entry_layouts.get(&block).cloned() {
503            // Push values in reverse order so first slot ends up on top
504            for slot in layout.slots.iter().rev() {
505                if let Some(val) = slot {
506                    self.stack.push(*val);
507                } else {
508                    self.stack.push_unknown();
509                }
510            }
511        }
512    }
513
514    /// Clears all block layout information.
515    ///
516    /// Called when starting to generate a new function.
517    pub fn clear_block_layouts(&mut self) {
518        self.block_entry_layouts.clear();
519        self.block_exit_layouts.clear();
520    }
521
522    /// Returns true if a block has an entry layout set.
523    #[must_use]
524    pub fn has_block_entry_layout(&self, block: BlockId) -> bool {
525        self.block_entry_layouts.contains_key(&block)
526    }
527
528    /// Checks if the current stack matches a block's entry layout.
529    ///
530    /// Returns true if the stack already matches the target layout (no shuffling needed).
531    #[must_use]
532    pub fn stack_matches_entry_layout(&self, target_block: BlockId) -> bool {
533        if let Some(target_layout) = self.block_entry_layouts.get(&target_block) {
534            let current = BlockStackLayout::from_stack_model(&self.stack);
535            current == *target_layout
536        } else {
537            true // No layout specified, so any stack is fine
538        }
539    }
540}
541
542impl Default for StackScheduler {
543    fn default() -> Self {
544        Self::new()
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551    use crate::mir::{Function, Immediate, InstKind, Instruction, MirType, Value};
552    use solar_interface::Ident;
553
554    fn make_test_func() -> Function {
555        let name = Ident::DUMMY;
556        let mut func = Function::new(name);
557
558        // Add some values
559        func.alloc_value(Value::Immediate(Immediate::uint256(alloy_primitives::U256::from(42))));
560        func.alloc_value(Value::Immediate(Immediate::uint256(alloy_primitives::U256::from(100))));
561
562        func
563    }
564
565    #[test]
566    fn test_ensure_on_top_already_there() {
567        let func = make_test_func();
568        let mut scheduler = StackScheduler::new();
569
570        let v0 = ValueId::from_usize(0);
571        scheduler.stack.push(v0);
572
573        let ops = scheduler.ensure_on_top(v0, &func);
574        assert!(ops.is_empty());
575    }
576
577    #[test]
578    fn test_ensure_on_top_dup() {
579        let func = make_test_func();
580        let mut scheduler = StackScheduler::new();
581
582        let v0 = ValueId::from_usize(0);
583        let v1 = ValueId::from_usize(1);
584
585        scheduler.stack.push(v0);
586        scheduler.stack.push(v1);
587        // Stack: [v1, v0]
588
589        let ops = scheduler.ensure_on_top(v0, &func);
590        // Should emit DUP2 to get v0 on top
591
592        assert_eq!(ops.len(), 1);
593        if let ScheduledOp::Stack(StackOp::Dup(n)) = &ops[0] {
594            assert_eq!(*n, 2);
595        } else {
596            panic!("Expected DUP operation");
597        }
598    }
599
600    #[test]
601    fn test_deep_unspilled_inst_result_is_not_emittable() {
602        let mut func = make_test_func();
603        let v0 = ValueId::from_usize(0);
604        let v1 = ValueId::from_usize(1);
605        let inst =
606            func.alloc_inst(Instruction::new(InstKind::Add(v0, v1), Some(MirType::uint256())));
607        let deep = func.alloc_value(Value::Inst(inst));
608        let mut scheduler = StackScheduler::new();
609
610        scheduler.stack.push(deep);
611        for i in 0..MAX_STACK_ACCESS {
612            scheduler.stack.push(ValueId::from_usize(100 + i));
613        }
614
615        assert_eq!(scheduler.stack.find(deep), Some(MAX_STACK_ACCESS));
616        assert!(!scheduler.can_emit_value(deep, &func));
617
618        scheduler.spills.allocate(deep);
619        assert!(!scheduler.can_emit_value(deep, &func));
620
621        scheduler.spills.mark_reloadable(deep);
622        assert!(scheduler.can_emit_value(deep, &func));
623    }
624
625    #[test]
626    fn test_block_layout_set_and_get() {
627        let mut scheduler = StackScheduler::new();
628        let block_id = BlockId::from_usize(0);
629        let v0 = ValueId::from_usize(0);
630        let v1 = ValueId::from_usize(1);
631
632        let layout = BlockStackLayout::from_values([v0, v1]);
633        scheduler.set_block_entry_layout(block_id, layout.clone());
634
635        assert!(scheduler.has_block_entry_layout(block_id));
636        assert_eq!(scheduler.get_block_entry_layout(block_id), Some(&layout));
637    }
638
639    #[test]
640    fn test_record_block_exit_layout() {
641        let mut scheduler = StackScheduler::new();
642        let block_id = BlockId::from_usize(0);
643        let v0 = ValueId::from_usize(0);
644        let v1 = ValueId::from_usize(1);
645
646        scheduler.stack.push(v0);
647        scheduler.stack.push(v1);
648        // Stack: [v1, v0]
649
650        scheduler.record_block_exit_layout(block_id);
651
652        let exit_layout = scheduler.get_block_exit_layout(block_id);
653        assert!(exit_layout.is_some());
654        let layout = exit_layout.unwrap();
655        assert_eq!(layout.get(0), Some(v1)); // v1 is on top
656        assert_eq!(layout.get(1), Some(v0));
657    }
658
659    #[test]
660    fn test_init_from_block_entry_layout() {
661        let mut scheduler = StackScheduler::new();
662        let block_id = BlockId::from_usize(0);
663        let v0 = ValueId::from_usize(0);
664        let v1 = ValueId::from_usize(1);
665
666        let layout = BlockStackLayout::from_values([v0, v1]);
667        scheduler.set_block_entry_layout(block_id, layout);
668
669        // Clear and reinitialize
670        scheduler.stack.push(ValueId::from_usize(99)); // Put something on stack
671        scheduler.init_from_block_entry_layout(block_id);
672
673        // Stack should now match the entry layout
674        assert_eq!(scheduler.stack.top(), Some(v0));
675        assert_eq!(scheduler.stack.peek(1), Some(v1));
676        assert_eq!(scheduler.stack.depth(), 2);
677    }
678
679    #[test]
680    fn test_stack_matches_entry_layout() {
681        let mut scheduler = StackScheduler::new();
682        let block_id = BlockId::from_usize(0);
683        let v0 = ValueId::from_usize(0);
684        let v1 = ValueId::from_usize(1);
685
686        let layout = BlockStackLayout::from_values([v0, v1]);
687        scheduler.set_block_entry_layout(block_id, layout);
688
689        // Stack doesn't match yet
690        assert!(!scheduler.stack_matches_entry_layout(block_id));
691
692        // Set up stack to match
693        scheduler.stack.push(v1);
694        scheduler.stack.push(v0);
695        // Stack: [v0, v1]
696
697        assert!(scheduler.stack_matches_entry_layout(block_id));
698    }
699
700    #[test]
701    fn test_clear_block_layouts() {
702        let mut scheduler = StackScheduler::new();
703        let block_id = BlockId::from_usize(0);
704        let v0 = ValueId::from_usize(0);
705
706        let layout = BlockStackLayout::from_values([v0]);
707        scheduler.set_block_entry_layout(block_id, layout);
708
709        assert!(scheduler.has_block_entry_layout(block_id));
710
711        scheduler.clear_block_layouts();
712
713        assert!(!scheduler.has_block_entry_layout(block_id));
714    }
715}