use super::model::{MAX_STACK_ACCESS, StackModel, StackOp};
use crate::mir::ValueId;
use smallvec::{SmallVec, ToSmallVec};
use solar_data_structures::map::FxHashMap;
#[derive(Clone, Debug, Default)]
pub struct ShuffleResult {
pub ops: Vec<StackOp>,
pub dup_count: usize,
pub swap_count: usize,
pub pop_count: usize,
}
impl ShuffleResult {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ops.is_empty()
}
#[must_use]
pub fn op_count(&self) -> usize {
self.ops.len()
}
#[must_use]
pub fn estimated_gas(&self) -> u64 {
(self.dup_count + self.swap_count) as u64 * 3 + self.pop_count as u64 * 2
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TargetSlot {
Value(ValueId),
Empty,
Any,
}
pub struct StackShuffler<'a> {
source: SmallVec<[Option<ValueId>; 16]>,
target: &'a [TargetSlot],
ops: Vec<StackOp>,
multiplicities: FxHashMap<ValueId, usize>,
}
impl<'a> StackShuffler<'a> {
pub fn new(source: &StackModel, target: &'a [TargetSlot]) -> Self {
let source_stack: SmallVec<[Option<ValueId>; 16]> =
source.as_slice().iter().copied().collect();
let mut multiplicities = FxHashMap::default();
for slot in target {
if let TargetSlot::Value(v) = slot {
*multiplicities.entry(*v).or_insert(0) += 1;
}
}
Self { source: source_stack, target, ops: Vec::new(), multiplicities }
}
pub fn shuffle(mut self) -> ShuffleResult {
self.ensure_multiplicities();
self.arrange_positions();
self.pop_excess();
let mut result = ShuffleResult::new();
for op in &self.ops {
match op {
StackOp::Dup(_) => result.dup_count += 1,
StackOp::Swap(_) => result.swap_count += 1,
StackOp::Pop => result.pop_count += 1,
}
}
result.ops = self.ops;
result
}
fn ensure_multiplicities(&mut self) {
for (&value, &needed) in self.multiplicities.iter() {
let current_count = self.source.iter().filter(|&&v| v == Some(value)).count();
if current_count < needed {
if let Some(depth) = self.find_value(value)
&& depth < MAX_STACK_ACCESS
{
for _ in current_count..needed {
let dup_n = (self.find_value(value).unwrap_or(0) + 1) as u8;
if dup_n <= 16 {
self.ops.push(StackOp::Dup(dup_n));
self.source.insert(0, Some(value));
}
}
}
}
}
}
fn arrange_positions(&mut self) {
for target_depth in 0..self.target.len().min(self.source.len()) {
let target_slot = &self.target[target_depth];
match target_slot {
TargetSlot::Value(target_val) => {
if self.source.get(target_depth) == Some(&Some(*target_val)) {
continue;
}
if let Some(source_depth) = self.find_value_from(*target_val, target_depth)
&& source_depth != target_depth
&& source_depth < MAX_STACK_ACCESS
{
if target_depth == 0 {
let swap_n = source_depth as u8;
if (1..=16).contains(&swap_n) {
self.ops.push(StackOp::Swap(swap_n));
self.source.swap(0, source_depth);
}
} else {
if target_depth < MAX_STACK_ACCESS && source_depth < MAX_STACK_ACCESS {
let swap1 = target_depth as u8;
if (1..=16).contains(&swap1) {
self.ops.push(StackOp::Swap(swap1));
self.source.swap(0, target_depth);
}
if let Some(new_depth) = self.find_value(*target_val)
&& new_depth > 0
&& new_depth < MAX_STACK_ACCESS
{
let swap2 = new_depth as u8;
if (1..=16).contains(&swap2) {
self.ops.push(StackOp::Swap(swap2));
self.source.swap(0, new_depth);
}
}
if (1..=16).contains(&swap1) {
self.ops.push(StackOp::Swap(swap1));
self.source.swap(0, target_depth);
}
}
}
}
}
TargetSlot::Empty | TargetSlot::Any => {
}
}
}
}
fn pop_excess(&mut self) {
let mut still_needed: FxHashMap<ValueId, usize> = FxHashMap::default();
for slot in self.target.iter() {
if let TargetSlot::Value(v) = slot {
*still_needed.entry(*v).or_insert(0) += 1;
}
}
while !self.source.is_empty() {
if let Some(Some(top_val)) = self.source.first() {
let needed = still_needed.get(top_val).copied().unwrap_or(0);
let current = self.source.iter().filter(|&&v| v == Some(*top_val)).count();
if current > needed {
self.ops.push(StackOp::Pop);
self.source.remove(0);
} else {
break;
}
} else if self.source.first() == Some(&None) {
if self.source.len() > self.target.len() {
self.ops.push(StackOp::Pop);
self.source.remove(0);
} else {
break;
}
} else {
break;
}
}
}
fn find_value(&self, value: ValueId) -> Option<usize> {
self.source.iter().position(|&v| v == Some(value))
}
fn find_value_from(&self, value: ValueId, min_depth: usize) -> Option<usize> {
self.source
.iter()
.enumerate()
.skip(min_depth)
.find(|(_, v)| **v == Some(value))
.map(|(i, _)| i)
}
}
#[derive(Clone, Debug)]
pub struct LayoutAnalysis {
pub entry_layouts: FxHashMap<(usize, usize), Vec<TargetSlot>>,
pub block_exit_layouts: FxHashMap<usize, Vec<TargetSlot>>,
}
impl LayoutAnalysis {
#[must_use]
pub fn new() -> Self {
Self { entry_layouts: FxHashMap::default(), block_exit_layouts: FxHashMap::default() }
}
pub fn analyze_backward(
instructions: &[(Vec<ValueId>, Option<ValueId>)],
exit_layout: &[TargetSlot],
) -> Vec<TargetSlot> {
let mut current_layout = exit_layout.to_vec();
for (operands, result) in instructions.iter().rev() {
current_layout =
Self::compute_entry_for_instruction(operands, *result, ¤t_layout);
}
current_layout
}
fn compute_entry_for_instruction(
operands: &[ValueId],
result: Option<ValueId>,
exit_layout: &[TargetSlot],
) -> Vec<TargetSlot> {
let mut entry = Vec::new();
for &op in operands {
entry.push(TargetSlot::Value(op));
}
for slot in exit_layout {
match slot {
TargetSlot::Value(v) if Some(*v) == result => {
}
_ => entry.push(*slot),
}
}
entry
}
}
impl Default for LayoutAnalysis {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct BlockStackLayout {
pub slots: SmallVec<[Option<ValueId>; 8]>,
}
impl BlockStackLayout {
#[must_use]
pub fn new() -> Self {
Self { slots: SmallVec::new() }
}
#[must_use]
pub fn from_values(values: impl IntoIterator<Item = ValueId>) -> Self {
Self { slots: values.into_iter().map(Some).collect() }
}
#[must_use]
pub fn from_stack_model(model: &StackModel) -> Self {
Self { slots: model.as_slice().to_smallvec() }
}
#[must_use]
pub fn depth(&self) -> usize {
self.slots.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.slots.is_empty()
}
#[must_use]
pub fn get(&self, depth: usize) -> Option<ValueId> {
self.slots.get(depth).copied().flatten()
}
#[must_use]
pub fn to_target_layout(&self) -> Vec<TargetSlot> {
self.slots
.iter()
.map(|&v| match v {
Some(val) => TargetSlot::Value(val),
None => TargetSlot::Any,
})
.collect()
}
#[must_use]
pub fn find(&self, value: ValueId) -> Option<usize> {
self.slots.iter().position(|&v| v == Some(value))
}
#[must_use]
pub fn contains(&self, value: ValueId) -> bool {
self.find(value).is_some()
}
}
pub fn combine_stack_layouts(layouts: &[BlockStackLayout]) -> Option<BlockStackLayout> {
if layouts.is_empty() {
return Some(BlockStackLayout::new());
}
if layouts.len() == 1 {
return Some(layouts[0].clone());
}
let max_depth = layouts.iter().map(|l| l.depth()).max().unwrap_or(0);
if max_depth == 0 {
return Some(BlockStackLayout::new());
}
let mut all_values: FxHashMap<ValueId, Vec<(usize, usize)>> = FxHashMap::default();
for (layout_idx, layout) in layouts.iter().enumerate() {
for (depth, slot) in layout.slots.iter().enumerate() {
if let Some(val) = slot {
all_values.entry(*val).or_default().push((layout_idx, depth));
}
}
}
let mut combined = BlockStackLayout { slots: smallvec::smallvec![None; max_depth] };
for depth in 0..max_depth {
let mut value_at_depth: Option<ValueId> = None;
let mut consistent = true;
for layout in layouts {
let val = layout.get(depth);
match (value_at_depth, val) {
(None, Some(v)) => value_at_depth = Some(v),
(Some(existing), Some(v)) if existing != v => {
consistent = false;
break;
}
_ => {}
}
}
if consistent {
combined.slots[depth] = value_at_depth;
}
}
for (&value, positions) in &all_values {
if combined.contains(value) {
continue; }
let mut pos_counts: FxHashMap<usize, usize> = FxHashMap::default();
for &(_, depth) in positions {
*pos_counts.entry(depth).or_default() += 1;
}
if let Some((&best_pos, _)) = pos_counts.iter().max_by(|a, b| {
a.1.cmp(b.1).then_with(|| b.0.cmp(a.0)) }) && best_pos < combined.slots.len()
&& combined.slots[best_pos].is_none()
{
combined.slots[best_pos] = Some(value);
}
}
while combined.slots.last() == Some(&None) {
combined.slots.pop();
}
Some(combined)
}
#[must_use]
pub fn estimate_shuffle_cost(source: &BlockStackLayout, target: &BlockStackLayout) -> usize {
let mut cost = 0;
for (depth, target_val) in
target.slots.iter().enumerate().filter_map(|(i, s)| s.map(|v| (i, v)))
{
match source.find(target_val) {
Some(src_depth) if src_depth != depth => {
cost += 1;
}
None => {
cost += 2; }
_ => {} }
}
for val in source.slots.iter().flatten() {
if !target.contains(*val) {
cost += 1; }
}
for val in target.slots.iter().flatten() {
let source_count = source.slots.iter().filter(|&&v| v == Some(*val)).count();
let target_count = target.slots.iter().filter(|&&v| v == Some(*val)).count();
if target_count > source_count {
cost += target_count - source_count; }
}
cost
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FreelyGenerable {
Immediate,
Argument,
FunctionLabel,
}
pub fn is_freely_generable(func: &crate::mir::Function, value: ValueId) -> bool {
matches!(func.value(value), crate::mir::Value::Immediate(_) | crate::mir::Value::Arg { .. })
}
pub fn ideal_operand_layout(operands: &[ValueId]) -> Vec<TargetSlot> {
operands.iter().map(|&v| TargetSlot::Value(v)).collect()
}
pub fn ideal_binary_op_entry(
a: ValueId,
b: ValueId,
result: Option<ValueId>,
exit_layout: &[TargetSlot],
) -> Vec<TargetSlot> {
let mut entry = Vec::with_capacity(exit_layout.len() + 1);
entry.push(TargetSlot::Value(a));
entry.push(TargetSlot::Value(b));
for slot in exit_layout.iter() {
match slot {
TargetSlot::Value(v) if Some(*v) == result => {
}
_ => entry.push(*slot),
}
}
entry
}
pub fn ideal_unary_op_entry(
operand: ValueId,
result: Option<ValueId>,
exit_layout: &[TargetSlot],
) -> Vec<TargetSlot> {
let mut entry = Vec::with_capacity(exit_layout.len());
entry.push(TargetSlot::Value(operand));
for slot in exit_layout.iter() {
match slot {
TargetSlot::Value(v) if Some(*v) == result => {
}
_ => entry.push(*slot),
}
}
entry
}
#[cfg(test)]
mod tests {
use super::*;
fn make_model(values: &[Option<ValueId>]) -> StackModel {
let mut model = StackModel::new();
for &v in values.iter().rev() {
if let Some(val) = v {
model.push(val);
} else {
model.push_unknown();
}
}
model
}
#[test]
fn test_shuffle_already_correct() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let source = make_model(&[Some(v0), Some(v1)]);
let target = [TargetSlot::Value(v0), TargetSlot::Value(v1)];
let result = StackShuffler::new(&source, &target).shuffle();
assert!(result.is_empty());
}
#[test]
fn test_shuffle_swap_needed() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let source = make_model(&[Some(v1), Some(v0)]);
let target = [TargetSlot::Value(v0), TargetSlot::Value(v1)];
let result = StackShuffler::new(&source, &target).shuffle();
assert_eq!(result.swap_count, 1);
assert!(result.ops.contains(&StackOp::Swap(1)));
}
#[test]
fn test_shuffle_dup_needed() {
let v0 = ValueId::from_usize(0);
let source = make_model(&[Some(v0)]);
let target = [TargetSlot::Value(v0), TargetSlot::Value(v0)];
let result = StackShuffler::new(&source, &target).shuffle();
assert_eq!(result.dup_count, 1);
}
#[test]
fn test_shuffle_pop_excess() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let source = make_model(&[Some(v0), Some(v1)]);
let target = [TargetSlot::Value(v1)];
let result = StackShuffler::new(&source, &target).shuffle();
assert!(result.pop_count >= 1 || result.swap_count >= 1);
}
#[test]
fn test_ideal_binary_op_entry() {
let a = ValueId::from_usize(0);
let b = ValueId::from_usize(1);
let result = ValueId::from_usize(2);
let extra = ValueId::from_usize(3);
let exit = [TargetSlot::Value(result), TargetSlot::Value(extra)];
let entry = ideal_binary_op_entry(a, b, Some(result), &exit);
assert_eq!(entry.len(), 3);
assert_eq!(entry[0], TargetSlot::Value(a));
assert_eq!(entry[1], TargetSlot::Value(b));
assert_eq!(entry[2], TargetSlot::Value(extra));
}
#[test]
fn test_backward_layout_analysis() {
let a = ValueId::from_usize(0);
let b = ValueId::from_usize(1);
let c = ValueId::from_usize(2);
let r1 = ValueId::from_usize(3);
let r2 = ValueId::from_usize(4);
let instructions = vec![
(vec![a, b], Some(r1)), (vec![r1, c], Some(r2)), ];
let exit = vec![TargetSlot::Value(r2)];
let entry = LayoutAnalysis::analyze_backward(&instructions, &exit);
assert_eq!(entry.len(), 3);
assert_eq!(entry[0], TargetSlot::Value(a));
assert_eq!(entry[1], TargetSlot::Value(b));
assert_eq!(entry[2], TargetSlot::Value(c));
}
#[test]
fn test_ideal_operand_layout() {
let a = ValueId::from_usize(0);
let b = ValueId::from_usize(1);
let c = ValueId::from_usize(2);
let layout = ideal_operand_layout(&[a, b, c]);
assert_eq!(layout.len(), 3);
assert_eq!(layout[0], TargetSlot::Value(a));
assert_eq!(layout[1], TargetSlot::Value(b));
assert_eq!(layout[2], TargetSlot::Value(c));
}
#[test]
fn test_shuffle_complex_rearrangement() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let v2 = ValueId::from_usize(2);
let source = make_model(&[Some(v0), Some(v1), Some(v2)]);
let target = [TargetSlot::Value(v2), TargetSlot::Value(v0), TargetSlot::Value(v1)];
let result = StackShuffler::new(&source, &target).shuffle();
assert!(result.swap_count > 0);
}
#[test]
fn test_block_stack_layout_basic() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let layout = BlockStackLayout::from_values([v0, v1]);
assert_eq!(layout.depth(), 2);
assert_eq!(layout.get(0), Some(v0));
assert_eq!(layout.get(1), Some(v1));
assert!(layout.contains(v0));
assert!(layout.contains(v1));
assert_eq!(layout.find(v0), Some(0));
assert_eq!(layout.find(v1), Some(1));
}
#[test]
fn test_block_stack_layout_to_target() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let layout = BlockStackLayout::from_values([v0, v1]);
let target = layout.to_target_layout();
assert_eq!(target.len(), 2);
assert_eq!(target[0], TargetSlot::Value(v0));
assert_eq!(target[1], TargetSlot::Value(v1));
}
#[test]
fn test_combine_stack_layouts_single() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let layout = BlockStackLayout::from_values([v0, v1]);
let combined = combine_stack_layouts(std::slice::from_ref(&layout));
assert!(combined.is_some());
assert_eq!(combined.unwrap(), layout);
}
#[test]
fn test_combine_stack_layouts_identical() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let layout1 = BlockStackLayout::from_values([v0, v1]);
let layout2 = BlockStackLayout::from_values([v0, v1]);
let combined = combine_stack_layouts(&[layout1, layout2]);
assert!(combined.is_some());
let result = combined.unwrap();
assert_eq!(result.get(0), Some(v0));
assert_eq!(result.get(1), Some(v1));
}
#[test]
fn test_combine_stack_layouts_different_values() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let v2 = ValueId::from_usize(2);
let layout1 = BlockStackLayout::from_values([v0, v1]);
let layout2 = BlockStackLayout::from_values([v0, v2]);
let combined = combine_stack_layouts(&[layout1, layout2]);
assert!(combined.is_some());
let result = combined.unwrap();
assert_eq!(result.get(0), Some(v0)); }
#[test]
fn test_estimate_shuffle_cost_identical() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let layout = BlockStackLayout::from_values([v0, v1]);
let cost = estimate_shuffle_cost(&layout, &layout);
assert_eq!(cost, 0);
}
#[test]
fn test_estimate_shuffle_cost_swap() {
let v0 = ValueId::from_usize(0);
let v1 = ValueId::from_usize(1);
let source = BlockStackLayout::from_values([v0, v1]);
let target = BlockStackLayout::from_values([v1, v0]);
let cost = estimate_shuffle_cost(&source, &target);
assert!(cost > 0);
}
}