solar_codegen/backend/evm/stack/
scheduler.rs1use 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
24pub struct StackScheduler {
26 pub stack: StackModel,
28 pub spills: SpillManager,
30 ops: Vec<ScheduledOp>,
32 block_entry_layouts: FxHashMap<BlockId, BlockStackLayout>,
34 block_exit_layouts: FxHashMap<BlockId, BlockStackLayout>,
36}
37
38#[derive(Clone, Debug)]
40pub enum ScheduledOp {
41 Stack(StackOp),
43 PushImmediate(alloy_primitives::U256),
45 LoadSpill(SpillSlot),
47 SaveSpill(SpillSlot),
49 LoadArg(u32),
52}
53
54impl StackScheduler {
55 #[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 pub fn clear_ops(&mut self) {
69 self.ops.clear();
70 }
71
72 pub fn take_ops(&mut self) -> Vec<ScheduledOp> {
74 std::mem::take(&mut self.ops)
75 }
76
77 pub fn ensure_on_top(&mut self, value: ValueId, func: &Function) -> &[ScheduledOp] {
80 self.ensure_on_top_impl(value, func, true)
81 }
82
83 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 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 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 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 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 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 pub fn can_emit_value(&self, value: ValueId, func: &Function) -> bool {
165 if let Some(depth) = self.stack.find(value) {
167 return depth < MAX_STACK_ACCESS || self.spills.is_reloadable(value);
168 }
169 if self.spills.is_reloadable(value) {
171 return true;
172 }
173 matches!(func.value(value), crate::mir::Value::Immediate(_) | crate::mir::Value::Arg { .. })
175 }
176
177 pub fn ensure_on_top_many(&mut self, values: &[ValueId], func: &Function) -> Vec<ScheduledOp> {
180 let mut all_ops = Vec::new();
181
182 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 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 pub fn instruction_executed(&mut self, consumed: usize, produced: Option<ValueId>) {
213 for _ in 0..consumed {
215 self.stack.pop();
216 }
217
218 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 pub fn instruction_executed_untracked(&mut self, consumed: usize) {
230 for _ in 0..consumed {
232 self.stack.pop();
233 }
234 self.stack.push_unknown();
236 }
237
238 pub fn has_untracked_on_top(&self) -> bool {
240 self.stack.depth() > 0 && self.stack.top().is_none()
241 }
242
243 pub fn has_untracked_at_depth(&self, depth: usize) -> bool {
245 self.stack.depth() > depth && self.stack.peek(depth).is_none()
246 }
247
248 pub fn stack_swapped(&mut self) {
250 self.stack.swap(1);
251 }
252
253 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 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 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 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 continue;
288 }
289 depth += 1;
290 }
291
292 ops
293 }
294
295 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 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 #[must_use]
313 pub fn stack_depth(&self) -> usize {
314 self.stack.depth()
315 }
316
317 #[must_use]
319 pub fn depth(&self) -> usize {
320 self.stack.depth()
321 }
322
323 pub fn clear_stack(&mut self) {
325 self.stack.clear();
326 }
327
328 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 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 pub fn prepare_binary_op(&mut self, a: ValueId, b: ValueId, _func: &Function) -> ShuffleResult {
360 let target = [TargetSlot::Value(a), TargetSlot::Value(b)];
362
363 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 return ShuffleResult::new();
371 }
372
373 self.shuffle_to_layout(&target)
374 }
375
376 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 return ShuffleResult::new();
388 }
389
390 self.shuffle_to_layout(&target)
391 }
392
393 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 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 pub fn set_block_entry_layout(&mut self, block: BlockId, layout: BlockStackLayout) {
426 self.block_entry_layouts.insert(block, layout);
427 }
428
429 #[must_use]
431 pub fn get_block_entry_layout(&self, block: BlockId) -> Option<&BlockStackLayout> {
432 self.block_entry_layouts.get(&block)
433 }
434
435 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 #[must_use]
446 pub fn get_block_exit_layout(&self, block: BlockId) -> Option<&BlockStackLayout> {
447 self.block_exit_layouts.get(&block)
448 }
449
450 pub fn compute_merge_layout(
458 &self,
459 predecessors: &[BlockId],
460 live_in: impl IntoIterator<Item = ValueId>,
461 ) -> BlockStackLayout {
462 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 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_stack_layouts(&pred_layouts).unwrap_or_default()
481 }
482
483 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 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 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 pub fn clear_block_layouts(&mut self) {
518 self.block_entry_layouts.clear();
519 self.block_exit_layouts.clear();
520 }
521
522 #[must_use]
524 pub fn has_block_entry_layout(&self, block: BlockId) -> bool {
525 self.block_entry_layouts.contains_key(&block)
526 }
527
528 #[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 }
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 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 let ops = scheduler.ensure_on_top(v0, &func);
590 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 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)); 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 scheduler.stack.push(ValueId::from_usize(99)); scheduler.init_from_block_entry_layout(block_id);
672
673 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 assert!(!scheduler.stack_matches_entry_layout(block_id));
691
692 scheduler.stack.push(v1);
694 scheduler.stack.push(v0);
695 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}