use super::{
ir::{
EvmIrBlock, EvmIrBlockId, EvmIrInstruction, EvmIrInstructionKind, EvmIrModule,
EvmIrOperand, EvmIrStackEffect, EvmIrStackOp, EvmIrTerminatorKind, EvmIrValueId,
default_instruction_stack_effect, is_encoded_push_instruction,
},
stack::SpillSlot,
};
use alloy_primitives::U256;
use solar_data_structures::map::FxHashMap;
const MAX_STACK_REACH: usize = 16;
pub(super) fn schedule_stack_ops(module: &mut EvmIrModule) -> bool {
EvmIrStackScheduler::new(module).run()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum ScheduledStackItem {
Value(EvmIrValueId),
Anonymous(u32),
}
struct EvmIrStackScheduler<'a> {
module: &'a mut EvmIrModule,
next_anonymous: u32,
changed: bool,
exit_stacks: FxHashMap<EvmIrBlockId, Vec<ScheduledStackItem>>,
spills: SpillState,
}
#[derive(Default)]
struct SpillState {
enabled: bool,
slots: FxHashMap<EvmIrValueId, SpillSlot>,
next_offset: u32,
}
impl SpillState {
fn reset(&mut self, enabled: bool) {
self.enabled = enabled;
self.slots.clear();
self.next_offset = 0;
}
fn allocate(&mut self, value: EvmIrValueId) -> SpillSlot {
if let Some(&slot) = self.slots.get(&value) {
return slot;
}
let slot = SpillSlot { offset: self.next_offset };
self.next_offset += 1;
self.slots.insert(value, slot);
slot
}
fn slot(&self, value: EvmIrValueId) -> Option<SpillSlot> {
self.slots.get(&value).copied()
}
fn mark_reloaded(&mut self, value: EvmIrValueId) {
self.slots.remove(&value);
}
fn is_spilled(&self, value: EvmIrValueId) -> bool {
self.slots.contains_key(&value)
}
}
impl<'a> EvmIrStackScheduler<'a> {
fn new(module: &'a mut EvmIrModule) -> Self {
Self {
module,
next_anonymous: 0,
changed: false,
exit_stacks: FxHashMap::default(),
spills: SpillState::default(),
}
}
fn run(mut self) -> bool {
let order = self.reverse_postorder();
for block_id in order {
self.schedule_block(block_id);
}
self.changed
}
fn reverse_postorder(&self) -> Vec<EvmIrBlockId> {
let mut postorder = Vec::with_capacity(self.module.blocks.len());
let mut visited = vec![false; self.module.blocks.len()];
if let Some(entry) = self.module.entry_block {
let mut work: Vec<(EvmIrBlockId, Vec<EvmIrBlockId>)> = Vec::new();
visited[entry.index()] = true;
work.push((entry, block_successors(&self.module.blocks[entry])));
while let Some((block_id, succs)) = work.last_mut() {
if let Some(succ) = succs.pop() {
if !visited[succ.index()] {
visited[succ.index()] = true;
let succs = block_successors(&self.module.blocks[succ]);
work.push((succ, succs));
}
} else {
postorder.push(*block_id);
work.pop();
}
}
}
postorder.reverse();
for block_id in self.module.blocks.indices() {
if !visited[block_id.index()] {
postorder.push(block_id);
}
}
postorder
}
fn schedule_block(&mut self, block_id: EvmIrBlockId) {
let Some(entry_stack) = self.infer_entry_stack(block_id) else {
return;
};
self.spills.reset(self.block_allows_spilling(block_id));
let mut stack: Vec<ScheduledStackItem> =
entry_stack.iter().copied().map(ScheduledStackItem::Value).collect();
let mut remaining = self.collect_value_uses(block_id);
let original = self.module.blocks[block_id].instructions.clone();
let working = std::mem::take(&mut self.module.blocks[block_id].instructions);
let mut out = Vec::with_capacity(working.len());
let mut changed = false;
for inst in working {
if !self.schedule_instruction(inst, &mut stack, &mut out, &mut remaining, &mut changed)
{
self.module.blocks[block_id].instructions = original;
return;
}
}
if !self.schedule_terminator(block_id, &mut stack, &mut out, &remaining, &mut changed) {
self.module.blocks[block_id].instructions = original;
return;
}
self.module.blocks[block_id].instructions = out;
self.module.blocks[block_id].entry_stack = entry_stack;
let exit = self.terminator_exit_stack(block_id, stack);
self.exit_stacks.insert(block_id, exit);
if changed {
self.changed = true;
}
}
fn infer_entry_stack(&self, block_id: EvmIrBlockId) -> Option<Vec<EvmIrValueId>> {
let declared = &self.module.blocks[block_id].entry_stack;
let inferred = if self.module.entry_block == Some(block_id) {
Vec::new()
} else {
let preds = self.predecessors(block_id);
if preds.is_empty() {
Vec::new()
} else {
let mut merged: Option<Vec<EvmIrValueId>> = None;
for pred in preds {
let exit = self.exit_stacks.get(&pred)?;
let flow = edge_entry_stack(&self.module.blocks[pred], exit);
match &merged {
Some(existing) if *existing != flow => return None,
Some(_) => {}
None => merged = Some(flow),
}
}
merged.unwrap_or_default()
}
};
if declared.is_empty() {
return Some(inferred);
}
if inferred.starts_with(declared) { Some(declared.clone()) } else { None }
}
fn predecessors(&self, block_id: EvmIrBlockId) -> Vec<EvmIrBlockId> {
let mut preds = Vec::new();
for (id, block) in self.module.blocks.iter_enumerated() {
if block_successors(block).contains(&block_id) {
preds.push(id);
}
}
preds
}
fn terminator_exit_stack(
&self,
block_id: EvmIrBlockId,
mut stack: Vec<ScheduledStackItem>,
) -> Vec<ScheduledStackItem> {
if let Some(term) = &self.module.blocks[block_id].terminator
&& matches!(
term.kind,
EvmIrTerminatorKind::Branch { .. } | EvmIrTerminatorKind::Switch { .. }
)
&& !stack.is_empty()
{
stack.remove(0);
}
stack
}
fn schedule_terminator(
&mut self,
block_id: EvmIrBlockId,
stack: &mut Vec<ScheduledStackItem>,
out: &mut Vec<EvmIrInstruction>,
remaining: &FxHashMap<EvmIrValueId, usize>,
changed: &mut bool,
) -> bool {
let Some(terminator) = self.module.blocks[block_id].terminator.as_ref() else {
return true;
};
let operands = terminator_arrange_operands(&terminator.kind);
if operands.is_empty() {
return true;
}
let target: Vec<ScheduledStackItem> =
operands.into_iter().map(ScheduledStackItem::Value).collect();
if !target.iter().all(|item| stack.contains(item)) {
return false;
}
self.arrange_stack_for(stack, &target, remaining, out, changed)
}
fn collect_value_uses(&self, block_id: EvmIrBlockId) -> FxHashMap<EvmIrValueId, usize> {
let block = &self.module.blocks[block_id];
let mut uses = FxHashMap::<EvmIrValueId, usize>::default();
let mut count = |operand: &EvmIrOperand| {
if let EvmIrOperand::Value(value) = operand {
*uses.entry(*value).or_default() += 1;
}
};
for inst in &block.instructions {
for operand in &inst.operands {
count(operand);
}
}
if let Some(terminator) = &block.terminator {
visit_terminator_value_operands(&terminator.kind, &mut count);
}
uses
}
fn schedule_instruction(
&mut self,
mut inst: EvmIrInstruction,
stack: &mut Vec<ScheduledStackItem>,
out: &mut Vec<EvmIrInstruction>,
remaining: &mut FxHashMap<EvmIrValueId, usize>,
changed: &mut bool,
) -> bool {
if let EvmIrInstructionKind::Stack(op) = inst.kind {
if !Self::apply_stack_op(stack, op) {
return false;
}
out.push(inst);
return true;
}
let effect = instruction_stack_effect(&inst);
let schedule_operands =
!inst.operands.is_empty() && !instruction_keeps_encoded_operands(&inst);
if schedule_operands {
if !self.prepare_operands_reachable(&inst.operands, stack, out, changed) {
return false;
}
let Some(target) = self.materialize_operand_stack(&inst.operands, stack, out, changed)
else {
return false;
};
if !self.arrange_stack_for(stack, &target, remaining, out, changed) {
return false;
}
if stack.len() < target.len() {
return false;
}
for item in &target {
if let ScheduledStackItem::Value(value) = item
&& let Some(count) = remaining.get_mut(value)
{
*count = count.saturating_sub(1);
}
}
stack.drain(0..target.len());
inst.operands.clear();
inst.metadata.stack.get_or_insert(effect);
*changed = true;
} else if (stack.len() as u64) < u64::from(effect.inputs) {
return false;
} else {
for _ in 0..effect.inputs {
stack.remove(0);
}
}
for index in 0..effect.outputs {
let item = if index == 0 {
inst.result.map(ScheduledStackItem::Value).unwrap_or_else(|| self.fresh_anonymous())
} else {
self.fresh_anonymous()
};
stack.insert(0, item);
}
out.push(inst);
true
}
fn block_allows_spilling(&self, block_id: EvmIrBlockId) -> bool {
!matches!(
self.module.blocks[block_id].terminator.as_ref().map(|term| &term.kind),
Some(EvmIrTerminatorKind::Jump(_) | EvmIrTerminatorKind::Fallthrough(_))
)
}
fn prepare_operands_reachable(
&mut self,
operands: &[EvmIrOperand],
stack: &mut Vec<ScheduledStackItem>,
out: &mut Vec<EvmIrInstruction>,
changed: &mut bool,
) -> bool {
for operand in operands {
if let EvmIrOperand::Value(value) = operand
&& self.spills.is_spilled(*value)
&& !self.reload_value(*value, stack, out, changed)
{
return false;
}
}
let needed: Vec<EvmIrValueId> = operands
.iter()
.filter_map(|operand| match operand {
EvmIrOperand::Value(value) => Some(*value),
_ => None,
})
.collect();
loop {
let deepest_needed = needed
.iter()
.filter_map(|value| {
stack.iter().position(|item| *item == ScheduledStackItem::Value(*value))
})
.max();
let Some(depth) = deepest_needed else { return true };
if depth < MAX_STACK_REACH {
return true;
}
if !self.spills.enabled {
return false;
}
let victim = (0..MAX_STACK_REACH.min(stack.len())).rev().find_map(|d| match stack[d] {
ScheduledStackItem::Value(value) if !needed.contains(&value) => Some((d, value)),
_ => None,
});
let Some((victim_depth, victim_value)) = victim else {
return false;
};
if !self.spill_value(victim_depth, victim_value, stack, out, changed) {
return false;
}
}
}
fn spill_value(
&mut self,
victim_depth: usize,
victim_value: EvmIrValueId,
stack: &mut Vec<ScheduledStackItem>,
out: &mut Vec<EvmIrInstruction>,
changed: &mut bool,
) -> bool {
if victim_depth >= MAX_STACK_REACH {
return false;
}
let copies =
stack.iter().filter(|item| **item == ScheduledStackItem::Value(victim_value)).count();
if copies != 1 {
return false;
}
let slot = self.spills.allocate(victim_value);
if victim_depth != 0
&& !self.emit_stack_op(EvmIrStackOp::Swap(victim_depth as u8), stack, out, changed)
{
return false;
}
debug_assert_eq!(stack.first().copied(), Some(ScheduledStackItem::Value(victim_value)));
self.emit_push_immediate(byte_offset(slot), stack, out, changed);
let mut mstore = EvmIrInstruction::new("mstore", Vec::new());
mstore.metadata.stack = Some(EvmIrStackEffect::new(2, 0));
out.push(mstore);
if stack.len() < 2 {
return false;
}
stack.drain(0..2);
*changed = true;
true
}
fn reload_value(
&mut self,
value: EvmIrValueId,
stack: &mut Vec<ScheduledStackItem>,
out: &mut Vec<EvmIrInstruction>,
changed: &mut bool,
) -> bool {
let Some(slot) = self.spills.slot(value) else {
return false;
};
self.emit_push_immediate(byte_offset(slot), stack, out, changed);
let mut mload = EvmIrInstruction::new("mload", Vec::new());
mload.metadata.stack = Some(EvmIrStackEffect::new(1, 1));
out.push(mload);
if stack.is_empty() {
return false;
}
stack[0] = ScheduledStackItem::Value(value);
self.spills.mark_reloaded(value);
*changed = true;
true
}
fn emit_push_immediate(
&mut self,
immediate: U256,
stack: &mut Vec<ScheduledStackItem>,
out: &mut Vec<EvmIrInstruction>,
changed: &mut bool,
) {
let item = self.fresh_anonymous();
let mut push = EvmIrInstruction::new("push", vec![EvmIrOperand::Immediate(immediate)]);
push.metadata.stack = Some(EvmIrStackEffect::new(0, 1));
out.push(push);
stack.insert(0, item);
*changed = true;
}
fn materialize_operand_stack(
&mut self,
operands: &[EvmIrOperand],
stack: &mut Vec<ScheduledStackItem>,
out: &mut Vec<EvmIrInstruction>,
changed: &mut bool,
) -> Option<Vec<ScheduledStackItem>> {
let mut target = Vec::with_capacity(operands.len());
for operand in operands {
match operand {
EvmIrOperand::Value(value) => target.push(ScheduledStackItem::Value(*value)),
EvmIrOperand::Immediate(_) | EvmIrOperand::Block(_) | EvmIrOperand::Symbol(_) => {
let item = self.fresh_anonymous();
let mut push = EvmIrInstruction::new("push", vec![operand.clone()]);
push.metadata.stack = Some(EvmIrStackEffect::new(0, 1));
out.push(push);
stack.insert(0, item);
target.push(item);
*changed = true;
}
}
}
if target.iter().all(|item| stack.contains(item)) { Some(target) } else { None }
}
fn arrange_stack_for(
&mut self,
stack: &mut Vec<ScheduledStackItem>,
target: &[ScheduledStackItem],
remaining: &FxHashMap<EvmIrValueId, usize>,
out: &mut Vec<EvmIrInstruction>,
changed: &mut bool,
) -> bool {
if !self.ensure_multiplicities(stack, target, remaining, out, changed) {
return false;
}
if stack.starts_with(target) {
return true;
}
for (target_depth, target_item) in target.iter().copied().enumerate() {
if stack.get(target_depth).copied() == Some(target_item) {
continue;
}
let Some(source_depth) = stack
.iter()
.enumerate()
.skip(target_depth)
.find_map(|(depth, item)| (*item == target_item).then_some(depth))
else {
return false;
};
if source_depth >= MAX_STACK_REACH {
return false;
}
if target_depth == 0 {
if !self.emit_stack_op(EvmIrStackOp::Swap(source_depth as u8), stack, out, changed)
{
return false;
}
} else if !self.shuffle_item_to_depth(target_depth, target_item, stack, out, changed) {
return false;
}
}
true
}
fn shuffle_item_to_depth(
&mut self,
target_depth: usize,
target_item: ScheduledStackItem,
stack: &mut Vec<ScheduledStackItem>,
out: &mut Vec<EvmIrInstruction>,
changed: &mut bool,
) -> bool {
if target_depth >= MAX_STACK_REACH {
return false;
}
if !self.emit_stack_op(EvmIrStackOp::Swap(target_depth as u8), stack, out, changed) {
return false;
}
let Some(new_depth) = stack.iter().position(|item| *item == target_item) else {
return false;
};
if new_depth == 0 || new_depth >= MAX_STACK_REACH {
return false;
}
if !self.emit_stack_op(EvmIrStackOp::Swap(new_depth as u8), stack, out, changed) {
return false;
}
self.emit_stack_op(EvmIrStackOp::Swap(target_depth as u8), stack, out, changed)
}
fn ensure_multiplicities(
&mut self,
stack: &mut Vec<ScheduledStackItem>,
target: &[ScheduledStackItem],
remaining: &FxHashMap<EvmIrValueId, usize>,
out: &mut Vec<EvmIrInstruction>,
changed: &mut bool,
) -> bool {
let mut target_counts = FxHashMap::<ScheduledStackItem, usize>::default();
for &item in target {
*target_counts.entry(item).or_default() += 1;
}
for (&item, &target_count) in &target_counts {
let needed = match item {
ScheduledStackItem::Value(value) => {
remaining.get(&value).copied().unwrap_or(target_count).max(target_count)
}
ScheduledStackItem::Anonymous(_) => target_count,
};
let mut have = stack.iter().filter(|&&stack_item| stack_item == item).count();
while have < needed {
let Some(depth) = stack.iter().position(|stack_item| *stack_item == item) else {
return false;
};
if depth >= MAX_STACK_REACH {
return false;
}
if !self.emit_stack_op(EvmIrStackOp::Dup((depth + 1) as u8), stack, out, changed) {
return false;
}
have += 1;
}
}
true
}
fn emit_stack_op(
&mut self,
op: EvmIrStackOp,
stack: &mut Vec<ScheduledStackItem>,
out: &mut Vec<EvmIrInstruction>,
changed: &mut bool,
) -> bool {
if !Self::apply_stack_op(stack, op) {
return false;
}
out.push(EvmIrInstruction::stack_op(op));
*changed = true;
true
}
fn apply_stack_op(stack: &mut Vec<ScheduledStackItem>, op: EvmIrStackOp) -> bool {
match op {
EvmIrStackOp::Dup(n) => {
let depth = usize::from(n - 1);
let Some(value) = stack.get(depth).copied() else {
return false;
};
stack.insert(0, value);
}
EvmIrStackOp::Swap(n) => {
let depth = usize::from(n);
if depth >= stack.len() {
return false;
}
stack.swap(0, depth);
}
EvmIrStackOp::Pop => {
if stack.is_empty() {
return false;
}
stack.remove(0);
}
}
true
}
fn fresh_anonymous(&mut self) -> ScheduledStackItem {
let id = self.next_anonymous;
self.next_anonymous += 1;
ScheduledStackItem::Anonymous(id)
}
}
fn byte_offset(slot: SpillSlot) -> U256 {
U256::from(slot.byte_offset())
}
fn block_successors(block: &EvmIrBlock) -> Vec<EvmIrBlockId> {
let mut targets = Vec::new();
let Some(term) = &block.terminator else {
return targets;
};
match &term.kind {
EvmIrTerminatorKind::Fallthrough(target) | EvmIrTerminatorKind::Jump(target) => {
targets.push(*target);
}
EvmIrTerminatorKind::Branch { then_block, else_block, .. } => {
targets.push(*then_block);
targets.push(*else_block);
}
EvmIrTerminatorKind::Switch { default, cases, .. } => {
targets.push(*default);
for (_, target) in cases {
targets.push(*target);
}
}
EvmIrTerminatorKind::Return { .. }
| EvmIrTerminatorKind::Revert { .. }
| EvmIrTerminatorKind::Stop
| EvmIrTerminatorKind::Invalid
| EvmIrTerminatorKind::SelfDestruct { .. }
| EvmIrTerminatorKind::RawOpcode(_) => {}
}
targets
}
fn edge_entry_stack(pred: &EvmIrBlock, exit: &[ScheduledStackItem]) -> Vec<EvmIrValueId> {
let linear = matches!(
pred.terminator.as_ref().map(|term| &term.kind),
Some(EvmIrTerminatorKind::Jump(_) | EvmIrTerminatorKind::Fallthrough(_))
);
if !linear {
return Vec::new();
}
exit.iter()
.map_while(|item| match item {
ScheduledStackItem::Value(value) => Some(*value),
ScheduledStackItem::Anonymous(_) => None,
})
.collect()
}
fn instruction_stack_effect(inst: &EvmIrInstruction) -> EvmIrStackEffect {
inst.metadata.stack.unwrap_or_else(|| default_instruction_stack_effect(inst))
}
fn instruction_keeps_encoded_operands(inst: &EvmIrInstruction) -> bool {
is_encoded_push_instruction(inst)
}
fn terminator_arrange_operands(kind: &EvmIrTerminatorKind) -> Vec<EvmIrValueId> {
let mut operands = Vec::new();
let mut push = |operand: &EvmIrOperand| {
if let EvmIrOperand::Value(value) = operand {
operands.push(*value);
}
};
match kind {
EvmIrTerminatorKind::Branch { condition, .. } => push(condition),
EvmIrTerminatorKind::Switch { value, .. } => push(value),
EvmIrTerminatorKind::Return { offset, size }
| EvmIrTerminatorKind::Revert { offset, size } => {
push(offset);
push(size);
}
EvmIrTerminatorKind::SelfDestruct { recipient } => push(recipient),
EvmIrTerminatorKind::Fallthrough(_)
| EvmIrTerminatorKind::Jump(_)
| EvmIrTerminatorKind::Stop
| EvmIrTerminatorKind::Invalid
| EvmIrTerminatorKind::RawOpcode(_) => {}
}
operands
}
fn visit_terminator_value_operands(
kind: &EvmIrTerminatorKind,
visit: &mut impl FnMut(&EvmIrOperand),
) {
match kind {
EvmIrTerminatorKind::Branch { condition, .. } => visit(condition),
EvmIrTerminatorKind::Switch { value, cases, .. } => {
visit(value);
for (case_value, _) in cases {
visit(case_value);
}
}
EvmIrTerminatorKind::Return { offset, size }
| EvmIrTerminatorKind::Revert { offset, size } => {
visit(offset);
visit(size);
}
EvmIrTerminatorKind::SelfDestruct { recipient } => visit(recipient),
EvmIrTerminatorKind::Fallthrough(_)
| EvmIrTerminatorKind::Jump(_)
| EvmIrTerminatorKind::Stop
| EvmIrTerminatorKind::Invalid
| EvmIrTerminatorKind::RawOpcode(_) => {}
}
}