From 5fe5c99294972eea4989c677a8cb93c325af481b Mon Sep 17 00:00:00 2001 From: Sean Billig Date: Wed, 20 Nov 2024 20:02:28 -0800 Subject: [PATCH 1/4] Liveness analysis --- .gitignore | 4 +- crates/codegen/Cargo.toml | 4 + crates/codegen/src/bitset.rs | 131 ++++++++++++++ crates/codegen/src/lib.rs | 2 + crates/codegen/src/liveness.rs | 263 +++++++++++++++++++++++++++++ crates/ir/src/dfg.rs | 4 + crates/ir/src/inst/control_flow.rs | 4 + crates/parser/src/lib.rs | 8 +- 8 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 crates/codegen/src/bitset.rs create mode 100644 crates/codegen/src/liveness.rs diff --git a/.gitignore b/.gitignore index 6c916fb9..be965dea 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,6 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk - - */target +.DS_Store +.zed/* diff --git a/crates/codegen/Cargo.toml b/crates/codegen/Cargo.toml index dc98b1e5..3d7812d7 100644 --- a/crates/codegen/Cargo.toml +++ b/crates/codegen/Cargo.toml @@ -21,3 +21,7 @@ rustc-hash = "2.0.0" sonatina-ir = { path = "../ir", version = "0.0.3-alpha" } sonatina-triple = { path = "../triple", version = "0.0.3-alpha" } sonatina-macros = { path = "../macros", version = "0.0.3-alpha" } +bit-set = "0.8.0" + +[dev-dependencies] +sonatina-parser = { path = "../parser", version = "0.0.3-alpha" } diff --git a/crates/codegen/src/bitset.rs b/crates/codegen/src/bitset.rs new file mode 100644 index 00000000..e79252e9 --- /dev/null +++ b/crates/codegen/src/bitset.rs @@ -0,0 +1,131 @@ +use bit_set::BitSet as Bs; +use cranelift_entity::EntityRef; +use std::fmt; +use std::marker::PhantomData; + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct BitSet { + bs: Bs, + marker: PhantomData, +} + +impl BitSet { + pub fn new() -> Self { + Self { + bs: Bs::new(), + marker: PhantomData, + } + } + + pub fn is_empty(&self) -> bool { + self.bs.is_empty() + } + + pub fn len(&self) -> usize { + self.bs.len() + } + + pub fn union_with(&mut self, other: &Self) { + self.bs.union_with(&other.bs) + } + + pub fn intersect_with(&mut self, other: &Self) { + self.bs.intersect_with(&other.bs) + } + + pub fn difference_with(&mut self, other: &Self) { + self.bs.difference_with(&other.bs) + } + + pub fn symmetric_difference_with(&mut self, other: &Self) { + self.bs.symmetric_difference_with(&other.bs) + } + + pub fn is_subset(&self, other: &Self) -> bool { + self.bs.is_subset(&other.bs) + } + + pub fn is_superset(&self, other: &Self) -> bool { + self.bs.is_superset(&other.bs) + } + + pub fn is_disjoint(&self, other: &Self) -> bool { + self.bs.is_disjoint(&other.bs) + } + + pub fn clear(&mut self) { + self.bs.clear() + } +} + +impl BitSet +where + T: EntityRef, +{ + pub fn difference(a: &Self, b: &Self) -> Self { + let mut d = a.clone(); + d.difference_with(b); + d + } + + pub fn insert(&mut self, elem: T) -> bool { + self.bs.insert(elem.index()) + } + pub fn remove(&mut self, elem: T) -> bool { + self.bs.remove(elem.index()) + } + pub fn contains(&self, elem: T) -> bool { + self.bs.contains(elem.index()) + } + pub fn iter(&self) -> impl Iterator + '_ { + self.bs.iter().map(|v| T::new(v)) + } +} + +impl Default for BitSet { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Debug for BitSet +where + T: EntityRef + fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_set().entries(self.bs.iter()).finish() + } +} + +impl From<&[T]> for BitSet { + fn from(elems: &[T]) -> Self { + let mut bs = BitSet::new(); + for e in elems { + bs.insert(*e); + } + bs + } +} + +impl From<[T; N]> for BitSet { + fn from(elems: [T; N]) -> Self { + let mut bs = BitSet::new(); + for e in elems { + bs.insert(e); + } + bs + } +} + +impl FromIterator for BitSet { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + let mut bs = BitSet::new(); + for e in iter { + bs.insert(e); + } + bs + } +} diff --git a/crates/codegen/src/lib.rs b/crates/codegen/src/lib.rs index eaaa2a70..3a3d2bf7 100644 --- a/crates/codegen/src/lib.rs +++ b/crates/codegen/src/lib.rs @@ -2,8 +2,10 @@ // See and #![allow(clippy::needless_collect)] +mod bitset; pub mod critical_edge; pub mod domtree; +pub mod liveness; pub mod loop_analysis; pub mod optim; pub mod post_domtree; diff --git a/crates/codegen/src/liveness.rs b/crates/codegen/src/liveness.rs new file mode 100644 index 00000000..f39d564c --- /dev/null +++ b/crates/codegen/src/liveness.rs @@ -0,0 +1,263 @@ +//! Compute the "liveness" of values in a control flow graph. +//! +//! This is an implementation of "Liveness Sets using Path Exploration", +//! as described in https://hal.inria.fr/hal-00761555/file/habilitation.pdf +//! Section 2.5.1: +//! +//! > a variable is live at a program point p, if p belongs to a path of the CFG +//! > leading from a definition of that variable to one of its uses without +//! > passing through the definition. Therefore, the live-range of a variable can +//! > be computed using a backward traversal starting at its uses and stopping +//! > when reaching its (unique) definition. This idea was first proposed by +//! > Appel in his “Tiger” book... +//! > +//! > Starting from a use of a variable, all paths where that variable is live +//! > are followed by traversing the CFG backwards until the variable’s +//! > definition is reached. Along the encountered paths, the variable is added +//! > to the live-in and live-out sets of the respective basic blocks. +//! +//! +//! Note that the arguments to a phi instruction are considered to be used by +//! their associated predecessor blocks, *not* by the block containing the phi. +//! The result of a phi instruction is live-in for the block containing the phi, +//! but *not* live-out for the predecessor blocks (so no block defines the result). + +use std::collections::BTreeMap; + +use crate::bitset::BitSet; +use cranelift_entity::SecondaryMap; +use sonatina_ir::{cfg::ControlFlowGraph, BlockId, Function, ValueId}; + +#[derive(Default)] +pub struct Liveness { + /// block => (live_ins, live_outs) + live_ins: SecondaryMap>, + live_outs: SecondaryMap>, + + /// value => (defining block, is_defined_by_phi) + defs: SecondaryMap>, + pub val_live_blocks: SecondaryMap>, + pub val_use_blocks: SecondaryMap>, + pub val_use_count: SecondaryMap, +} + +impl Liveness { + pub fn new() -> Self { + Self::default() + } + + pub fn compute(&mut self, func: &Function, cfg: &ControlFlowGraph) { + self.clear(); + + for arg in &func.arg_values { + self.defs[*arg] = Some(ValDef::FnArg); + } + for block in cfg.post_order() { + for_each_def(func, block, |val, is_phi_def| { + self.defs[val] = if is_phi_def { + Some(ValDef::Phi(block)) + } else { + Some(ValDef::Normal(block)) + } + }); + } + + for block in cfg.post_order() { + for_each_use(func, block, |val, phi_source_block| { + if func.dfg.value_is_imm(val) { + self.mark_use(val, block); + } else if let Some(pred_block) = phi_source_block { + // A phi input is considered to be a use by the associated + // predecessor block + self.mark_use(val, pred_block); + self.up_and_mark(cfg, pred_block, val); + } else { + self.mark_use(val, block); + self.up_and_mark(cfg, block, val); + } + }); + } + } + + // XXX better api + pub fn block_live_ins(&self, block: BlockId) -> &BitSet { + &self.live_ins[block] + } + pub fn block_live_outs(&self, block: BlockId) -> &BitSet { + &self.live_outs[block] + } + + /// Propagate liveness of `val` "upward" from its use in `block` + fn up_and_mark(&mut self, cfg: &ControlFlowGraph, block: BlockId, val: ValueId) { + let def = self.defs[val].unwrap(); + + self.val_live_blocks[val].insert(block); + + // If `val` is defined in this block, there's nothing to do. + if def == ValDef::Normal(block) { + return; + } + + if self.live_ins[block].contains(val) { + // Already marked, so propagation to preds already done + return; + } + self.live_ins[block].insert(val); + + // If `val` is the result of a phi, then it's live-in for the block + // containing the phi, but not live-out for any predecessor block. + if def == ValDef::Phi(block) { + return; + } + + for pred in cfg.preds_of(block) { + self.live_outs[*pred].insert(val); + self.up_and_mark(cfg, *pred, val); + } + } + + fn mark_use(&mut self, val: ValueId, block: BlockId) { + self.val_use_blocks[val].insert(block); + self.val_use_count[val] += 1; + } + + /// Reset the `Liveness` struct so that it can be reused. + pub fn clear(&mut self) { + self.live_ins.clear(); + self.live_outs.clear(); + self.defs.clear(); + self.val_live_blocks.clear(); + } +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum ValDef { + FnArg, + Normal(BlockId), + Phi(BlockId), +} + +pub fn value_uses_in_block_matching_predicate( + func: &Function, + block: BlockId, + pred: impl Fn(ValueId) -> bool, +) -> BTreeMap { + let mut counts = BTreeMap::new(); + for_each_use(func, block, |val, _| { + if pred(val) { + counts.entry(val).and_modify(|n| *n += 1).or_insert(1); + } + }); + counts +} + +fn for_each_use(func: &Function, block: BlockId, mut f: impl FnMut(ValueId, Option)) { + for inst in func.layout.iter_inst(block) { + if let Some(phi) = func.dfg.cast_phi(inst) { + for (val, block) in phi.iter_args() { + f(*val, Some(*block)) + } + } else { + func.dfg.inst(inst).visit_values(&mut |v| f(v, None)); + } + } +} + +fn for_each_def(func: &Function, block: BlockId, mut f: impl FnMut(ValueId, bool)) { + for inst in func.layout.iter_inst(block) { + if let Some(val) = func.dfg.inst_result(inst) { + f(val, func.dfg.is_phi(inst)) + } + } +} + +#[cfg(test)] +mod tests { + use super::Liveness; + use sonatina_ir::{cfg::ControlFlowGraph, BlockId}; + use sonatina_parser::parse_module; + + const SRC: &str = r#" +target = "evm-ethereum-london" +func public %complex_loop(v0.i8, v20.i8) -> i8 { + block1: + v1.i8 = sub v0 1.i8; + jump block2; + + block2: + v2.i8 = phi (v8 block7) (v0 block1); + v3.i8 = phi (v9 block7) (v1 block1); + v4.i1 = lt v3 100.i8; + br v4 block3 block4; + + block3: + v5.i1 = lt v2 20.i8; + br v5 block5 block6; + + block4: + return v2; + + block5: + v6.i8 = add 1.i8 v3; + jump block7; + + block6: + v7.i8 = add v3 2.i8; + jump block7; + + block7: + v8.i8 = phi (v0 block5) (v3 block6); + v9.i8 = phi (v6 block5) (v7 block6); + jump block2; +} +"#; + + #[test] + fn test() { + let parsed = parse_module(SRC).unwrap(); + let (funcref, function) = parsed.module.funcs.iter().next().unwrap(); + let mut cfg = ControlFlowGraph::new(); + cfg.compute(function); + + let v = |n: usize| parsed.debug.value(funcref, &format!("v{n}")).unwrap(); + + let mut live = Liveness::default(); + live.compute(function, &cfg); + + assert_eq!(live.block_live_ins(BlockId(1)), &[v(0)].as_slice().into()); + assert_eq!(live.block_live_outs(BlockId(1)), &[v(0)].as_slice().into()); + + assert_eq!( + live.block_live_ins(BlockId(2)), + &[v(0), v(2), v(3)].as_slice().into() + ); + assert_eq!( + live.block_live_outs(BlockId(2)), + &[v(0), v(2), v(3)].as_slice().into() + ); + + assert_eq!( + live.block_live_ins(BlockId(3)), + &[v(0), v(2), v(3)].as_slice().into() + ); + assert_eq!( + live.block_live_outs(BlockId(3)), + &[v(0), v(3)].as_slice().into() + ); + + assert_eq!(live.block_live_ins(BlockId(4)), &[v(2)].as_slice().into()); + assert_eq!(live.block_live_outs(BlockId(4)), &[].as_slice().into()); + + assert_eq!( + live.block_live_ins(BlockId(5)), + &[v(0), v(3)].as_slice().into() + ); + assert_eq!(live.block_live_outs(BlockId(5)), &[v(0)].as_slice().into()); + + assert_eq!( + live.block_live_ins(BlockId(6)), + &[v(0), v(3)].as_slice().into() + ); + assert_eq!(live.block_live_outs(BlockId(6)), &[v(0)].as_slice().into()); + } +} diff --git a/crates/ir/src/dfg.rs b/crates/ir/src/dfg.rs index 2a009bb3..28f26dff 100644 --- a/crates/ir/src/dfg.rs +++ b/crates/ir/src/dfg.rs @@ -147,6 +147,10 @@ impl DataFlowGraph { } } + pub fn value_is_imm(&self, value_id: ValueId) -> bool { + matches!(self.values[value_id], Value::Immediate { .. }) + } + pub fn attach_user(&mut self, inst_id: InstId) { let inst = &self.insts[inst_id]; inst.visit_values(&mut |value| { diff --git a/crates/ir/src/inst/control_flow.rs b/crates/ir/src/inst/control_flow.rs index 2b2ffffd..a7fba2a9 100644 --- a/crates/ir/src/inst/control_flow.rs +++ b/crates/ir/src/inst/control_flow.rs @@ -36,6 +36,10 @@ pub struct Phi { } impl Phi { + pub fn iter_args(&self) -> impl Iterator { + self.args.iter() + } + pub fn append_phi_arg(&mut self, value: ValueId, block: BlockId) { self.args.push((value, block)) } diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index 0f2b0090..825a53f6 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -126,6 +126,12 @@ pub struct DebugInfo { pub value_names: FxHashMap>, } +impl DebugInfo { + pub fn value(&self, func: FuncRef, name: &str) -> Option { + self.value_names.get(&func)?.get_by_right(name).copied() + } +} + impl DebugProvider for DebugInfo { fn value_name(&self, _func: &Function, func_ref: FuncRef, value: ir::ValueId) -> Option<&str> { let names = self.value_names.get(&func_ref)?; @@ -444,7 +450,7 @@ mod tests { use super::*; #[test] fn foo() { - let s = " + let s = " target = \"evm-ethereum-london\" # sameln: func public %simple(v0.i8) -> i8 { From 9c560c06acd627cc6703ea083e9537a04c4944bd Mon Sep 17 00:00:00 2001 From: Sean Billig Date: Sat, 23 Nov 2024 19:56:02 -0800 Subject: [PATCH 2/4] wip update old code for new insts --- crates/codegen/Cargo.toml | 6 + crates/codegen/src/bitset.rs | 3 +- crates/codegen/src/isa/evm/mod.rs | 527 +++++++++++++++ crates/codegen/src/isa/evm/opcode.rs | 161 +++++ crates/codegen/src/isa/mod.rs | 1 + crates/codegen/src/lib.rs | 3 + crates/codegen/src/liveness.rs | 19 +- crates/codegen/src/machinst/assemble.rs | 240 +++++++ crates/codegen/src/machinst/lower.rs | 139 ++++ crates/codegen/src/machinst/mod.rs | 3 + crates/codegen/src/machinst/vcode.rs | 167 +++++ crates/codegen/src/optim/adce.rs | 2 +- crates/codegen/src/stackalloc/local_stack.rs | 608 ++++++++++++++++++ crates/codegen/src/stackalloc/mod.rs | 36 ++ crates/codegen/src/stackalloc/simple.rs | 401 ++++++++++++ crates/codegen/test_files/evm/add.snap | 41 ++ crates/codegen/test_files/evm/add.sntn | 19 + crates/codegen/test_files/evm/call.snap | 147 +++++ crates/codegen/test_files/evm/call.sntn | 33 + crates/codegen/test_files/evm/const_loop.snap | 67 ++ crates/codegen/test_files/evm/const_loop.sntn | 15 + crates/codegen/test_files/evm/spill.snap | 270 ++++++++ crates/codegen/test_files/evm/spill.sntn | 39 ++ crates/codegen/tests/build.rs | 4 + crates/codegen/tests/evm.rs | 240 +++++++ crates/ir/src/dfg.rs | 15 + crates/ir/src/inst/control_flow.rs | 22 +- crates/ir/src/inst/mod.rs | 4 +- crates/ir/src/ir_writer.rs | 39 +- crates/parser/src/lib.rs | 46 +- 30 files changed, 3256 insertions(+), 61 deletions(-) create mode 100644 crates/codegen/src/isa/evm/mod.rs create mode 100644 crates/codegen/src/isa/evm/opcode.rs create mode 100644 crates/codegen/src/isa/mod.rs create mode 100644 crates/codegen/src/machinst/assemble.rs create mode 100644 crates/codegen/src/machinst/lower.rs create mode 100644 crates/codegen/src/machinst/mod.rs create mode 100644 crates/codegen/src/machinst/vcode.rs create mode 100644 crates/codegen/src/stackalloc/local_stack.rs create mode 100644 crates/codegen/src/stackalloc/mod.rs create mode 100644 crates/codegen/src/stackalloc/simple.rs create mode 100644 crates/codegen/test_files/evm/add.snap create mode 100644 crates/codegen/test_files/evm/add.sntn create mode 100644 crates/codegen/test_files/evm/call.snap create mode 100644 crates/codegen/test_files/evm/call.sntn create mode 100644 crates/codegen/test_files/evm/const_loop.snap create mode 100644 crates/codegen/test_files/evm/const_loop.sntn create mode 100644 crates/codegen/test_files/evm/spill.snap create mode 100644 crates/codegen/test_files/evm/spill.sntn create mode 100644 crates/codegen/tests/build.rs create mode 100644 crates/codegen/tests/evm.rs diff --git a/crates/codegen/Cargo.toml b/crates/codegen/Cargo.toml index 3d7812d7..e5d0936a 100644 --- a/crates/codegen/Cargo.toml +++ b/crates/codegen/Cargo.toml @@ -22,6 +22,12 @@ sonatina-ir = { path = "../ir", version = "0.0.3-alpha" } sonatina-triple = { path = "../triple", version = "0.0.3-alpha" } sonatina-macros = { path = "../macros", version = "0.0.3-alpha" } bit-set = "0.8.0" +indexmap = "2.6.0" +if_chain = "1.0.2" [dev-dependencies] +dir-test = "0.4.0" +hex = "0.4.3" +insta = "1.41.1" +revm = { version = "18.0.0", default-features = false, features = ["std"] } sonatina-parser = { path = "../parser", version = "0.0.3-alpha" } diff --git a/crates/codegen/src/bitset.rs b/crates/codegen/src/bitset.rs index e79252e9..36c6d875 100644 --- a/crates/codegen/src/bitset.rs +++ b/crates/codegen/src/bitset.rs @@ -1,7 +1,6 @@ use bit_set::BitSet as Bs; use cranelift_entity::EntityRef; -use std::fmt; -use std::marker::PhantomData; +use std::{fmt, marker::PhantomData}; #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct BitSet { diff --git a/crates/codegen/src/isa/evm/mod.rs b/crates/codegen/src/isa/evm/mod.rs new file mode 100644 index 00000000..aa948116 --- /dev/null +++ b/crates/codegen/src/isa/evm/mod.rs @@ -0,0 +1,527 @@ +#![allow(unused_variables, dead_code)] // XXX + +pub mod opcode; +use opcode::OpCode; + +use crate::{ + machinst::{ + lower::{Lower, LowerBackend}, + vcode::Label, + }, + stackalloc::{Action, Allocator}, +}; +use smallvec::{smallvec, SmallVec}; +use sonatina_ir::{ + inst::evm::inst_set::EvmInstKind, + isa::{evm::Evm, Isa}, + BlockId, Function, Immediate, InstId, InstSetExt, U256, +}; + +const FRAME_POINTER_OFFSET: u8 = 0x0; + +pub struct EvmBackend { + isa: Evm, +} +impl EvmBackend { + pub fn new(isa: Evm) -> Self { + Self { isa } + } +} + +impl LowerBackend for EvmBackend { + type MInst = OpCode; + + fn enter_function( + &self, + ctx: &mut Lower, + alloc: &mut dyn Allocator, + function: &Function, + ) { + perform_actions(ctx, &alloc.enter_function(function)); + } + + fn enter_block( + &self, + ctx: &mut Lower, + _alloc: &mut dyn Allocator, + block: BlockId, + ) { + // Every block start is a jumpdest unless + // - all incoming edges are fallthroughs (TODO) + // - it's the entry block of the main fn (TODO) + ctx.push(OpCode::JUMPDEST); + } + + fn lower(&self, ctx: &mut Lower, alloc: &mut dyn Allocator, insn: InstId) { + let result = ctx.insn_result(insn); + let args = ctx.insn_data(insn).collect_values(); + let data = self.isa.inst_set().resolve_inst(ctx.insn_data(insn)); + + let basic_op = |ctx: &mut Lower, ops: &[OpCode]| { + perform_actions(ctx, &alloc.read(insn, &args)); + for op in ops { + ctx.push(*op); + } + perform_actions(ctx, &alloc.write(insn, result)); + }; + + match &data { + EvmInstKind::Neg(_) => basic_op(ctx, &[OpCode::PUSH0, OpCode::SUB]), + EvmInstKind::Add(_) => basic_op(ctx, &[OpCode::ADD]), + EvmInstKind::Mul(_) => basic_op(ctx, &[OpCode::MUL]), + EvmInstKind::Sub(_) => basic_op(ctx, &[OpCode::SUB]), + EvmInstKind::Shl(_) => basic_op(ctx, &[OpCode::SHL]), + EvmInstKind::Shr(_) => basic_op(ctx, &[OpCode::SHR]), + EvmInstKind::Sar(_) => basic_op(ctx, &[OpCode::SAR]), + EvmInstKind::Sext(_) => todo!(), + EvmInstKind::Zext(_) => todo!(), + EvmInstKind::Trunc(_) => todo!(), + EvmInstKind::Bitcast(_) => todo!(), + EvmInstKind::IntToPtr(_) => todo!(), + EvmInstKind::PtrToInt(_) => todo!(), + EvmInstKind::Lt(_) => basic_op(ctx, &[OpCode::LT]), + EvmInstKind::Gt(_) => basic_op(ctx, &[OpCode::GT]), + EvmInstKind::Slt(_) => basic_op(ctx, &[OpCode::SLT]), + EvmInstKind::Sgt(_) => basic_op(ctx, &[OpCode::SGT]), + EvmInstKind::Le(_) => basic_op(ctx, &[OpCode::GT, OpCode::ISZERO]), + EvmInstKind::Ge(_) => basic_op(ctx, &[OpCode::LT, OpCode::ISZERO]), + EvmInstKind::Sge(_) => basic_op(ctx, &[OpCode::SLT, OpCode::ISZERO]), + EvmInstKind::Eq(_) => basic_op(ctx, &[OpCode::EQ]), + EvmInstKind::Ne(_) => basic_op(ctx, &[OpCode::EQ, OpCode::ISZERO]), + EvmInstKind::IsZero(_) => basic_op(ctx, &[OpCode::ISZERO]), + + EvmInstKind::Not(_) => basic_op(ctx, &[OpCode::NOT]), + EvmInstKind::And(_) => basic_op(ctx, &[OpCode::AND]), + EvmInstKind::Or(_) => basic_op(ctx, &[OpCode::OR]), + EvmInstKind::Xor(_) => basic_op(ctx, &[OpCode::XOR]), + + EvmInstKind::Jump(jump) => { + let dest = *jump.dest(); + perform_actions(ctx, &alloc.read(insn, &[])); + + if !ctx.is_next_block(dest) { + let push_op = ctx.push(OpCode::PUSH1); + ctx.add_label_reference(push_op, Label::Block(dest)); + ctx.push(OpCode::JUMP); + } + } + EvmInstKind::Br(br) => { + let nz_dest = *br.nz_dest(); + let z_dest = *br.z_dest(); + + // JUMPI: dest is top of stack, bool val is next + perform_actions(ctx, &alloc.read(insn, &args)); + + ctx.push_jump_target(OpCode::PUSH1, Label::Block(nz_dest)); + ctx.push(OpCode::JUMPI); + + if !ctx.is_next_block(z_dest) { + ctx.push_jump_target(OpCode::PUSH1, Label::Block(z_dest)); + ctx.push(OpCode::JUMP); + } + } + EvmInstKind::Phi(_) => {} + + EvmInstKind::BrTable(br) => { + let table = br.table(); + let lhs = *br.scrutinee(); + let default = *br.default(); + + for (arg, dest) in table.clone().iter() { + perform_actions(ctx, &alloc.read(insn, &[*arg])); + ctx.push(OpCode::EQ); + + let p = ctx.push(OpCode::PUSH1); + ctx.add_label_reference(p, Label::Block(*dest)); + ctx.push(OpCode::JUMPI); + } + + if let Some(dest) = default { + if !ctx.is_next_block(dest) { + let p = ctx.push(OpCode::PUSH1); + ctx.add_label_reference(p, Label::Block(dest)); + ctx.push(OpCode::JUMP); + } + } + } + + EvmInstKind::Call(call) => { + // xxx if func uses memory, store new fp + + let callee = *call.callee(); + let mut actions = alloc.read(insn, &args); + + assert_eq!(actions.remove(0), Action::PushContinuationOffset); + let push_callback = ctx.push(OpCode::PUSH1); + + // Move fn args onto stack + perform_actions(ctx, &actions); + + // Push fn address onto stack and jump + let p = ctx.push(OpCode::PUSH1); + ctx.add_label_reference(p, Label::Function(callee)); + ctx.push(OpCode::JUMP); + + // Mark return pc as jumpdest + let jumpdest_op = ctx.push(OpCode::JUMPDEST); + ctx.add_label_reference(push_callback, Label::Insn(jumpdest_op)); + } + + EvmInstKind::Return(_) => { + perform_actions(ctx, &alloc.read(insn, &args)); + + // Caller pushes return location onto stack prior to call. + if !args.is_empty() { + // Swap the return loc to the top. + ctx.push(OpCode::SWAP1); + } + ctx.push(OpCode::JUMP); + } + EvmInstKind::Mload(_) => basic_op(ctx, &[OpCode::MLOAD]), + EvmInstKind::Mstore(mstore) => { + let ty_size = self + .isa + .type_layout() + .size_of(*mstore.ty(), ctx.module) + .unwrap(); + + perform_actions(ctx, &alloc.read(insn, &args)); + if ty_size == 0 { + // TODO: optimize away mstores of size 0 + // Pop the args, and don't do an mstore. + ctx.push(OpCode::POP); + ctx.push(OpCode::POP); + } else if ty_size < 32 { + // Value to write to mem is in stack slot 1. + ctx.push(OpCode::SWAP1); + // NOTE: This assumes that the useful bytes are on the right. + ctx.push_with_imm(OpCode::PUSH1, &[((32 - ty_size) * 8) as u8]); + ctx.push(OpCode::SHL); + ctx.push(OpCode::SWAP1); + } + + ctx.push(OpCode::MSTORE); + } + EvmInstKind::EvmMcopy(_) => todo!(), + EvmInstKind::Gep(_) => todo!(), + EvmInstKind::Alloca(_) => todo!(), + + EvmInstKind::EvmStop(_) => basic_op(ctx, &[OpCode::STOP]), + + EvmInstKind::EvmSdiv(_) => basic_op(ctx, &[OpCode::SDIV]), + EvmInstKind::EvmUdiv(_) => basic_op(ctx, &[OpCode::DIV]), + EvmInstKind::EvmUmod(_) => basic_op(ctx, &[OpCode::MOD]), + EvmInstKind::EvmSmod(_) => basic_op(ctx, &[OpCode::SMOD]), + EvmInstKind::EvmAddMod(_) => basic_op(ctx, &[OpCode::ADDMOD]), + EvmInstKind::EvmMulMod(_) => basic_op(ctx, &[OpCode::MULMOD]), + EvmInstKind::EvmExp(_) => basic_op(ctx, &[OpCode::EXP]), + EvmInstKind::EvmByte(_) => basic_op(ctx, &[OpCode::BYTE]), + EvmInstKind::EvmKeccak256(_) => basic_op(ctx, &[OpCode::KECCAK256]), + EvmInstKind::EvmAddress(_) => basic_op(ctx, &[OpCode::ADDRESS]), + EvmInstKind::EvmBalance(_) => basic_op(ctx, &[OpCode::BALANCE]), + EvmInstKind::EvmOrigin(_) => basic_op(ctx, &[OpCode::ORIGIN]), + EvmInstKind::EvmCaller(_) => basic_op(ctx, &[OpCode::CALLER]), + EvmInstKind::EvmCallValue(_) => basic_op(ctx, &[OpCode::CALLVALUE]), + EvmInstKind::EvmCallDataLoad(_) => basic_op(ctx, &[OpCode::CALLDATALOAD]), + EvmInstKind::EvmCallDataCopy(_) => todo!(), + EvmInstKind::EvmCallDataSize(_) => basic_op(ctx, &[OpCode::CALLDATALOAD]), + EvmInstKind::EvmCodeSize(_) => basic_op(ctx, &[OpCode::CODESIZE]), + EvmInstKind::EvmCodeCopy(_) => todo!(), + EvmInstKind::EvmExtCodeCopy(_) => todo!(), + EvmInstKind::EvmReturnDataSize(_) => basic_op(ctx, &[OpCode::RETURNDATASIZE]), + EvmInstKind::EvmReturnDataCopy(_) => todo!(), + EvmInstKind::EvmExtCodeHash(_) => basic_op(ctx, &[OpCode::EXTCODEHASH]), + EvmInstKind::EvmBlockHash(_) => basic_op(ctx, &[OpCode::BLOCKHASH]), + EvmInstKind::EvmCoinBase(_) => basic_op(ctx, &[OpCode::COINBASE]), + EvmInstKind::EvmTimestamp(_) => basic_op(ctx, &[OpCode::TIMESTAMP]), + EvmInstKind::EvmNumber(_) => basic_op(ctx, &[OpCode::NUMBER]), + EvmInstKind::EvmPrevRandao(_) => basic_op(ctx, &[OpCode::PREVRANDAO]), + EvmInstKind::EvmGasLimit(_) => basic_op(ctx, &[OpCode::GASLIMIT]), + EvmInstKind::EvmChainId(_) => basic_op(ctx, &[OpCode::CHAINID]), + EvmInstKind::EvmSelfBalance(_) => basic_op(ctx, &[OpCode::SELFBALANCE]), + EvmInstKind::EvmBaseFee(_) => basic_op(ctx, &[OpCode::BASEFEE]), + EvmInstKind::EvmBlobHash(_) => basic_op(ctx, &[OpCode::BLOBHASH]), + EvmInstKind::EvmBlobBaseFee(_) => basic_op(ctx, &[OpCode::BLOBBASEFEE]), + EvmInstKind::EvmMstore8(_) => basic_op(ctx, &[OpCode::MSTORE8]), + EvmInstKind::EvmSload(_) => basic_op(ctx, &[OpCode::SLOAD]), + EvmInstKind::EvmSstore(_) => basic_op(ctx, &[OpCode::SSTORE]), + EvmInstKind::EvmMsize(_) => basic_op(ctx, &[OpCode::MSIZE]), + EvmInstKind::EvmGas(_) => basic_op(ctx, &[OpCode::GAS]), + EvmInstKind::EvmTload(_) => basic_op(ctx, &[OpCode::TLOAD]), + EvmInstKind::EvmTstore(_) => basic_op(ctx, &[OpCode::TSTORE]), + EvmInstKind::EvmLog0(_) => basic_op(ctx, &[OpCode::LOG0]), + EvmInstKind::EvmLog1(_) => basic_op(ctx, &[OpCode::LOG1]), + EvmInstKind::EvmLog2(_) => basic_op(ctx, &[OpCode::LOG2]), + EvmInstKind::EvmLog3(_) => basic_op(ctx, &[OpCode::LOG3]), + EvmInstKind::EvmLog4(_) => basic_op(ctx, &[OpCode::LOG4]), + EvmInstKind::EvmCreate(_) => todo!(), + EvmInstKind::EvmCall(_) => todo!(), + EvmInstKind::EvmCallCode(_) => todo!(), + EvmInstKind::EvmReturn(_) => basic_op(ctx, &[OpCode::RETURN]), + EvmInstKind::EvmDelegateCall(_) => todo!(), + EvmInstKind::EvmCreate2(_) => todo!(), + EvmInstKind::EvmStaticCall(_) => todo!(), + EvmInstKind::EvmRevert(_) => todo!(), + EvmInstKind::EvmSelfDestruct(_) => todo!(), + + EvmInstKind::EvmMalloc(_) => todo!(), + EvmInstKind::InsertValue(_) => todo!(), + EvmInstKind::ExtractValue(_) => todo!(), + EvmInstKind::GetFunctionPtr(_) => todo!(), + EvmInstKind::EvmContractSize(_) => todo!(), + EvmInstKind::EvmInvalid(_) => todo!(), + } + } + + fn update_opcode_with_immediate_bytes( + &self, + opcode: &mut OpCode, + bytes: &mut SmallVec<[u8; 8]>, + ) { + while bytes.first() == Some(&0) { + bytes.pop(); + } + *opcode = push_op(bytes.len()); + } + + fn update_opcode_with_label( + &self, + opcode: &mut OpCode, + label_offset: u32, + ) -> SmallVec<[u8; 4]> { + let bytes = label_offset + .to_be_bytes() + .into_iter() + .skip_while(|b| *b == 0) + .collect::>(); + + *opcode = push_op(bytes.len()); + bytes + } + + fn emit_opcode(&self, opcode: &OpCode, buf: &mut Vec) { + buf.push(*opcode as u8); + } + + fn emit_immediate_bytes(&self, bytes: &[u8], buf: &mut Vec) { + buf.extend_from_slice(bytes); + } + fn emit_label(&self, address: u32, buf: &mut Vec) { + buf.extend(address.to_be_bytes().into_iter().skip_while(|b| *b == 0)); + } +} + +fn perform_actions(ctx: &mut Lower, actions: &[Action]) { + for action in actions { + match action { + Action::StackDup(slot) => { + ctx.push(dup_op(*slot)); + } + Action::StackSwap(n) => { + ctx.push(swap_op(*n)); + } + Action::Push(imm) => { + if imm.is_zero() { + ctx.push(OpCode::PUSH0); + } else { + let bytes = match imm { + Immediate::I1(v) => smallvec![*v as u8], + Immediate::I8(v) => SmallVec::from_slice(&v.to_be_bytes()), + Immediate::I16(v) => shrink_bytes(&v.to_be_bytes()), + Immediate::I32(v) => shrink_bytes(&v.to_be_bytes()), + Immediate::I64(v) => shrink_bytes(&v.to_be_bytes()), + Immediate::I128(v) => shrink_bytes(&v.to_be_bytes()), + Immediate::I256(v) => todo!(), + }; + push_bytes(ctx, &bytes); + + // Sign-extend negative numbers to 32 bytes + // TODO: signextend isn't always needed (eg push then mstore8) + if imm.is_negative() && bytes.len() < 32 { + push_bytes(ctx, &u32_to_be((bytes.len() - 1) as u32)); + ctx.push(OpCode::SIGNEXTEND); + } + } + } + Action::Pop => { + ctx.push(OpCode::POP); + } + Action::MemLoadAbs(offset) => { + let bytes = u32_to_be(*offset); + push_bytes(ctx, &bytes); + ctx.push(OpCode::MLOAD); + } + Action::MemLoadFrameSlot(offset) => { + push_bytes(ctx, &[FRAME_POINTER_OFFSET]); + ctx.push(OpCode::MLOAD); + if *offset != 0 { + let bytes = u32_to_be(*offset); + push_bytes(ctx, &bytes); + ctx.push(OpCode::ADD); + } + ctx.push(OpCode::MLOAD); + } + Action::MemStoreAbs(offset) => { + let bytes = u32_to_be(*offset); + push_bytes(ctx, &bytes); + ctx.push(OpCode::MSTORE); + } + Action::MemStoreFrameSlot(offset) => { + push_bytes(ctx, &[FRAME_POINTER_OFFSET]); + ctx.push(OpCode::MLOAD); + if *offset != 0 { + let bytes = u32_to_be(*offset); + push_bytes(ctx, &bytes); + ctx.push(OpCode::ADD); + } + ctx.push(OpCode::MSTORE); + } + Action::PushContinuationOffset => { + panic!("handle PushContinuationOffset elsewhere"); + } + } + } +} + +fn push_bytes(ctx: &mut Lower, bytes: &[u8]) { + assert!(!bytes.is_empty()); + if bytes == [0] { + ctx.push(OpCode::PUSH0); + } else { + ctx.push_with_imm(push_op(bytes.len()), bytes); + } +} + +/// Remove unnecessary leading bytes of the big-endian two's complement +/// representation of a number. +fn shrink_bytes(bytes: &[u8]) -> SmallVec<[u8; 8]> { + assert!(!bytes.is_empty()); + + let is_neg = bytes[0].leading_ones() > 0; + let skip = if is_neg { 0xff } else { 0x00 }; + let mut bytes = bytes + .iter() + .copied() + .skip_while(|b| *b == skip) + .collect::>(); + + // Negative numbers need a leading 1 bit for sign-extension + if is_neg && bytes.first().map(|&b| b < 0x80).unwrap_or(true) { + bytes.insert(0, 0xff); + } + bytes +} + +fn imm_to_be_bytes(imm: &Immediate) -> SmallVec<[u8; 4]> { + match imm { + Immediate::I1(v) => smallvec![*v as u8], + Immediate::I8(v) => smallvec![*v as u8], + Immediate::I16(v) => todo!(), + Immediate::I32(v) => todo!(), + Immediate::I64(v) => todo!(), + Immediate::I128(v) => todo!(), + Immediate::I256(v) => todo!(), + } +} +fn u32_to_be(num: u32) -> SmallVec<[u8; 4]> { + if num == 0 { + smallvec![0] + } else { + num.to_be_bytes() + .into_iter() + .skip_while(|b| *b == 0) + .collect() + } +} + +fn u256_to_be(num: &U256) -> SmallVec<[u8; 8]> { + if num.is_zero() { + smallvec![0] + } else { + let b = num.to_big_endian(); + b.into_iter().skip_while(|b| *b == 0).collect() + } +} + +fn dup_op(n: u8) -> OpCode { + match n + 1 { + 1 => OpCode::DUP1, + 2 => OpCode::DUP2, + 3 => OpCode::DUP3, + 4 => OpCode::DUP4, + 5 => OpCode::DUP5, + 6 => OpCode::DUP6, + 7 => OpCode::DUP7, + 8 => OpCode::DUP8, + 9 => OpCode::DUP9, + 10 => OpCode::DUP10, + 11 => OpCode::DUP11, + 12 => OpCode::DUP12, + 13 => OpCode::DUP13, + 14 => OpCode::DUP14, + 15 => OpCode::DUP15, + 16 => OpCode::DUP16, + _ => unreachable!(), + } +} + +fn swap_op(n: u8) -> OpCode { + match n { + 1 => OpCode::SWAP1, + 2 => OpCode::SWAP2, + 3 => OpCode::SWAP3, + 4 => OpCode::SWAP4, + 5 => OpCode::SWAP5, + 6 => OpCode::SWAP6, + 7 => OpCode::SWAP7, + 8 => OpCode::SWAP8, + 9 => OpCode::SWAP9, + 10 => OpCode::SWAP10, + 11 => OpCode::SWAP11, + 12 => OpCode::SWAP12, + 13 => OpCode::SWAP13, + 14 => OpCode::SWAP14, + 15 => OpCode::SWAP15, + 16 => OpCode::SWAP16, + _ => unreachable!(), + } +} + +fn push_op(bytes: usize) -> OpCode { + match bytes { + 0 => OpCode::PUSH0, + 1 => OpCode::PUSH1, + 2 => OpCode::PUSH2, + 3 => OpCode::PUSH3, + 4 => OpCode::PUSH4, + 5 => OpCode::PUSH5, + 6 => OpCode::PUSH6, + 7 => OpCode::PUSH7, + 8 => OpCode::PUSH8, + 9 => OpCode::PUSH9, + 10 => OpCode::PUSH10, + 11 => OpCode::PUSH11, + 12 => OpCode::PUSH12, + 13 => OpCode::PUSH13, + 14 => OpCode::PUSH14, + 15 => OpCode::PUSH15, + 16 => OpCode::PUSH16, + 17 => OpCode::PUSH17, + 18 => OpCode::PUSH18, + 19 => OpCode::PUSH19, + 20 => OpCode::PUSH20, + 21 => OpCode::PUSH21, + 22 => OpCode::PUSH22, + 23 => OpCode::PUSH23, + 24 => OpCode::PUSH24, + 25 => OpCode::PUSH25, + 26 => OpCode::PUSH26, + 27 => OpCode::PUSH27, + 28 => OpCode::PUSH28, + 29 => OpCode::PUSH29, + 30 => OpCode::PUSH30, + 31 => OpCode::PUSH31, + 32 => OpCode::PUSH32, + _ => unreachable!(), + } +} diff --git a/crates/codegen/src/isa/evm/opcode.rs b/crates/codegen/src/isa/evm/opcode.rs new file mode 100644 index 00000000..7632e8fe --- /dev/null +++ b/crates/codegen/src/isa/evm/opcode.rs @@ -0,0 +1,161 @@ +#[derive(Debug, Default, Clone, Copy)] +#[repr(u8)] +pub enum OpCode { + STOP = 0x00, + ADD = 0x01, + MUL = 0x02, + SUB = 0x03, + DIV = 0x04, + SDIV = 0x05, + MOD = 0x06, + SMOD = 0x07, + ADDMOD = 0x08, + MULMOD = 0x09, + EXP = 0x0A, + SIGNEXTEND = 0x0B, + // 0C-0F unused + LT = 0x10, + GT = 0x11, + SLT = 0x12, + SGT = 0x13, + EQ = 0x14, + ISZERO = 0x15, + AND = 0x16, + OR = 0x17, + XOR = 0x18, + NOT = 0x19, + BYTE = 0x1A, + SHL = 0x1B, + SHR = 0x1C, + SAR = 0x1D, + // 1E-1F unused + KECCAK256 = 0x20, + // 21-2F unused + ADDRESS = 0x30, + BALANCE = 0x31, + ORIGIN = 0x32, + CALLER = 0x33, + CALLVALUE = 0x34, + CALLDATALOAD = 0x35, + CALLDATASIZE = 0x36, + CALLDATACOPY = 0x37, + CODESIZE = 0x38, + CODECOPY = 0x39, + GASPRICE = 0x3A, + EXTCODESIZE = 0x3B, + EXTCODECOPY = 0x3C, + RETURNDATASIZE = 0x3D, + RETURNDATACOPY = 0x3E, + EXTCODEHASH = 0x3F, + BLOCKHASH = 0x40, + COINBASE = 0x41, + TIMESTAMP = 0x42, + NUMBER = 0x43, + PREVRANDAO = 0x44, + GASLIMIT = 0x45, + CHAINID = 0x46, + SELFBALANCE = 0x47, + BASEFEE = 0x48, + BLOBHASH = 0x49, + BLOBBASEFEE = 0x4A, + // 4B-4F unused + POP = 0x50, + MLOAD = 0x51, + MSTORE = 0x52, + MSTORE8 = 0x53, + SLOAD = 0x54, + SSTORE = 0x55, + JUMP = 0x56, + JUMPI = 0x57, + PC = 0x58, + MSIZE = 0x59, + GAS = 0x5A, + JUMPDEST = 0x5B, + TLOAD = 0x5C, + TSTORE = 0x5D, + MCOPY = 0x5E, + PUSH0 = 0x5F, + PUSH1 = 0x60, + PUSH2 = 0x61, + PUSH3 = 0x62, + PUSH4 = 0x63, + PUSH5 = 0x64, + PUSH6 = 0x65, + PUSH7 = 0x66, + PUSH8 = 0x67, + PUSH9 = 0x68, + PUSH10 = 0x69, + PUSH11 = 0x6A, + PUSH12 = 0x6B, + PUSH13 = 0x6C, + PUSH14 = 0x6D, + PUSH15 = 0x6E, + PUSH16 = 0x6F, + PUSH17 = 0x70, + PUSH18 = 0x71, + PUSH19 = 0x72, + PUSH20 = 0x73, + PUSH21 = 0x74, + PUSH22 = 0x75, + PUSH23 = 0x76, + PUSH24 = 0x77, + PUSH25 = 0x78, + PUSH26 = 0x79, + PUSH27 = 0x7A, + PUSH28 = 0x7B, + PUSH29 = 0x7C, + PUSH30 = 0x7D, + PUSH31 = 0x7E, + PUSH32 = 0x7F, + DUP1 = 0x80, + DUP2 = 0x81, + DUP3 = 0x82, + DUP4 = 0x83, + DUP5 = 0x84, + DUP6 = 0x85, + DUP7 = 0x86, + DUP8 = 0x87, + DUP9 = 0x88, + DUP10 = 0x89, + DUP11 = 0x8A, + DUP12 = 0x8B, + DUP13 = 0x8C, + DUP14 = 0x8D, + DUP15 = 0x8E, + DUP16 = 0x8F, + SWAP1 = 0x90, + SWAP2 = 0x91, + SWAP3 = 0x92, + SWAP4 = 0x93, + SWAP5 = 0x94, + SWAP6 = 0x95, + SWAP7 = 0x96, + SWAP8 = 0x97, + SWAP9 = 0x98, + SWAP10 = 0x99, + SWAP11 = 0x9A, + SWAP12 = 0x9B, + SWAP13 = 0x9C, + SWAP14 = 0x9D, + SWAP15 = 0x9E, + SWAP16 = 0x9F, + LOG0 = 0xA0, + LOG1 = 0xA1, + LOG2 = 0xA2, + LOG3 = 0xA3, + LOG4 = 0xA4, + // A5-EF unused + CREATE = 0xF0, + CALL = 0xF1, + CALLCODE = 0xF2, + RETURN = 0xF3, + DELEGATECALL = 0xF4, + CREATE2 = 0xF5, + // F6-F9 unused + STATICCALL = 0xFA, + // FB-FC unused + REVERT = 0xFD, + #[default] + INVALID = 0xFE, + SELFDESTRUCT = 0xFF, +} diff --git a/crates/codegen/src/isa/mod.rs b/crates/codegen/src/isa/mod.rs new file mode 100644 index 00000000..c469d0c8 --- /dev/null +++ b/crates/codegen/src/isa/mod.rs @@ -0,0 +1 @@ +pub mod evm; diff --git a/crates/codegen/src/lib.rs b/crates/codegen/src/lib.rs index 3a3d2bf7..e354e36d 100644 --- a/crates/codegen/src/lib.rs +++ b/crates/codegen/src/lib.rs @@ -5,7 +5,10 @@ mod bitset; pub mod critical_edge; pub mod domtree; +pub mod isa; pub mod liveness; pub mod loop_analysis; +pub mod machinst; pub mod optim; pub mod post_domtree; +pub mod stackalloc; diff --git a/crates/codegen/src/liveness.rs b/crates/codegen/src/liveness.rs index f39d564c..8690c7ed 100644 --- a/crates/codegen/src/liveness.rs +++ b/crates/codegen/src/liveness.rs @@ -1,7 +1,7 @@ //! Compute the "liveness" of values in a control flow graph. //! //! This is an implementation of "Liveness Sets using Path Exploration", -//! as described in https://hal.inria.fr/hal-00761555/file/habilitation.pdf +//! as described in //! Section 2.5.1: //! //! > a variable is live at a program point p, if p belongs to a path of the CFG @@ -154,7 +154,7 @@ pub fn value_uses_in_block_matching_predicate( fn for_each_use(func: &Function, block: BlockId, mut f: impl FnMut(ValueId, Option)) { for inst in func.layout.iter_inst(block) { if let Some(phi) = func.dfg.cast_phi(inst) { - for (val, block) in phi.iter_args() { + for (val, block) in phi.args().iter() { f(*val, Some(*block)) } } else { @@ -215,14 +215,17 @@ func public %complex_loop(v0.i8, v20.i8) -> i8 { #[test] fn test() { let parsed = parse_module(SRC).unwrap(); - let (funcref, function) = parsed.module.funcs.iter().next().unwrap(); - let mut cfg = ControlFlowGraph::new(); - cfg.compute(function); + let funcref = *parsed.module.funcs().first().unwrap(); - let v = |n: usize| parsed.debug.value(funcref, &format!("v{n}")).unwrap(); + let live = parsed.module.func_store.view(funcref, |function| { + let mut cfg = ControlFlowGraph::new(); + cfg.compute(function); + let mut live = Liveness::default(); + live.compute(function, &cfg); + live + }); - let mut live = Liveness::default(); - live.compute(function, &cfg); + let v = |n: usize| parsed.debug.value(funcref, &format!("v{n}")).unwrap(); assert_eq!(live.block_live_ins(BlockId(1)), &[v(0)].as_slice().into()); assert_eq!(live.block_live_outs(BlockId(1)), &[v(0)].as_slice().into()); diff --git a/crates/codegen/src/machinst/assemble.rs b/crates/codegen/src/machinst/assemble.rs new file mode 100644 index 00000000..ff63112f --- /dev/null +++ b/crates/codegen/src/machinst/assemble.rs @@ -0,0 +1,240 @@ +use std::{io, ops::IndexMut}; + +use cranelift_entity::SecondaryMap; +use indexmap::IndexMap; +use sonatina_ir::{ + ir_writer::{DebugProvider, FuncWriteCtx, FunctionSignature, InstStatement, IrWrite}, + module::FuncRef, + BlockId, Module, U256, +}; + +use super::{ + lower::LowerBackend, + vcode::{Label, LabelId, VCode, VCodeInst}, +}; + +pub struct ObjectLayout { + // TODO: data, suffix (solc metadata) + _offset: u32, + _size: u32, + functions: IndexMap>, + func_offsets: SecondaryMap, +} + +impl ObjectLayout { + pub fn new(funcs: Vec<(FuncRef, VCode, Vec)>, mut offset: u32) -> Self { + let start = offset; + + let mut func_offsets = SecondaryMap::with_capacity(funcs.len()); + let functions = funcs + .into_iter() + .map(|(f, vcode, block_order)| { + func_offsets[f] = offset; + let layout = FuncLayout::new(vcode, block_order, offset); + offset += layout.size; + (f, layout) + }) + .collect(); + + Self { + _offset: start, + _size: offset - start, + functions, + func_offsets, + } + } + + pub fn resize(&mut self, backend: &impl LowerBackend, mut offset: u32) -> bool { + let mut did_change = false; + for (funcref, layout) in self.functions.iter_mut() { + did_change |= layout.resize(backend, offset, &self.func_offsets); + self.func_offsets[*funcref] = offset; + offset += layout.size; + } + did_change + } + + pub fn emit(&self, backend: &impl LowerBackend, buf: &mut Vec) { + for layout in self.functions.values() { + layout.emit(backend, buf); + } + } +} + +pub struct FuncLayout { + offset: u32, + size: u32, + vcode: VCode, + block_order: Vec, + block_offsets: SecondaryMap, + insn_offsets: SecondaryMap, + label_targets: SecondaryMap, +} + +impl FuncLayout { + fn new(vcode: VCode, block_order: Vec, mut offset: u32) -> Self { + let start = offset; + + // Rough block offsets, ignoring immediates and labels + let mut block_offsets = SecondaryMap::default(); + for b in &block_order { + block_offsets[*b] = offset; + let block_size = vcode.blocks.get(*b).unwrap().len(&vcode.insts_pool) as u32; + offset += block_size; + } + + let imm_bytes: u32 = vcode + .inst_imm_bytes + .values() + .map(|(_, bytes)| bytes.len() as u32) + .sum(); + + // Guess that each label is 2 bytes + let label_bytes = 2 * vcode.labels.len() as u32; + + let size = offset - start + imm_bytes + label_bytes; + + Self { + offset, + size, + vcode, + block_order, + block_offsets, + insn_offsets: SecondaryMap::default(), + label_targets: SecondaryMap::default(), + } + } + + fn resize( + &mut self, + backend: &impl LowerBackend, + mut offset: u32, + fn_offsets: &SecondaryMap, + ) -> bool { + let mut did_change = update(&mut self.offset, offset); + + for block in self.block_order.iter().copied() { + did_change |= update(self.block_offsets.index_mut(block), offset); + + let block_insts = self.vcode.blocks[block].as_slice(&self.vcode.insts_pool); + for insn in block_insts { + did_change |= update(self.insn_offsets.index_mut(*insn), offset); + offset += std::mem::size_of::() as u32; + + if let Some((_, label)) = self.vcode.label_uses.get(*insn) { + let address = match self.vcode.labels[*label] { + Label::Block(b) => self.block_offsets[b], + Label::Function(f) => fn_offsets[f], + Label::Insn(i) => self.insn_offsets[i], + }; + did_change |= update(self.label_targets.index_mut(*label), address); + + let label_bytes = + backend.update_opcode_with_label(&mut self.vcode.insts[*insn], address); + offset += label_bytes.len() as u32; + } + + if let Some((_, bytes)) = self.vcode.inst_imm_bytes.get_mut(*insn) { + backend.update_opcode_with_immediate_bytes(&mut self.vcode.insts[*insn], bytes); + offset += bytes.len() as u32; + } + } + } + did_change |= update(&mut self.size, offset - self.offset); + did_change + } + + fn emit(&self, backend: &impl LowerBackend, buf: &mut Vec) { + for block in self.block_order.iter().copied() { + for insn in self.vcode.block_insns(block) { + backend.emit_opcode(&self.vcode.insts[insn], buf); + if let Some((_, label)) = self.vcode.label_uses.get(insn) { + let address = self.label_targets[*label]; + backend.emit_label(address, buf); // xxx emit_address_bytes + } + + if let Some((_, bytes)) = self.vcode.inst_imm_bytes.get(insn) { + backend.emit_immediate_bytes(bytes, buf); + } + } + } + } +} + +impl ObjectLayout +where + Op: std::fmt::Debug, +{ + pub fn print( + &self, + w: &mut impl std::io::Write, + module: &Module, + dbg: &dyn DebugProvider, + ) -> std::io::Result<()> { + for (funcref, layout) in self.functions.iter() { + module.func_store.view(*funcref, |function| { + let ctx = FuncWriteCtx::with_debug_provider(function, *funcref, dbg); + layout.write(w, &ctx) + })?; + } + Ok(()) + } +} + +impl IrWrite> for FuncLayout +where + Op: std::fmt::Debug, +{ + fn write(&self, w: &mut W, ctx: &FuncWriteCtx) -> io::Result<()> + where + W: io::Write, + { + // Mostly copied from VCode::print + + write!(w, "// ")?; + FunctionSignature.write(w, ctx)?; + writeln!(w)?; + writeln!(w, "{}:", ctx.func.sig.name())?; + + let mut cur_ir = None; + for block in self.block_order.iter().copied() { + writeln!(w, " block{}:", block.0)?; + for insn in self.vcode.block_insns(block) { + write!( + w, + "{: >5} {:?}", + self.insn_offsets[insn], self.vcode.insts[insn], + )?; + if let Some((_, bytes)) = self.vcode.inst_imm_bytes.get(insn) { + let mut be = [0; 32]; + be[32 - bytes.len()..].copy_from_slice(bytes); + let imm = U256::from_big_endian(&be); + write!(w, " 0x{imm:x} ({imm})")?; + } else if let Some((_, label)) = self.vcode.label_uses.get(insn) { + write!(w, " {}", self.label_targets[*label])?; + match self.vcode.labels[*label] { + Label::Block(BlockId(n)) => write!(w, " (block{n})")?, + Label::Insn(_) => {} + Label::Function(func) => write!(w, " ({func:?})")?, + }; + } + + if let Some(ir) = self.vcode.inst_ir[insn].expand() { + if cur_ir != Some(ir) { + cur_ir = Some(ir); + write!(w, " // ")?; + InstStatement(ir).write(w, ctx)?; + } + } + writeln!(w)?; + } + } + Ok(()) + } +} + +fn update(val: &mut u32, to: u32) -> bool { + let did_change = *val != to; + *val = to; + did_change +} diff --git a/crates/codegen/src/machinst/lower.rs b/crates/codegen/src/machinst/lower.rs new file mode 100644 index 00000000..3df34b42 --- /dev/null +++ b/crates/codegen/src/machinst/lower.rs @@ -0,0 +1,139 @@ +use super::vcode::{Label, VCode, VCodeInst}; +use crate::stackalloc::Allocator; +use smallvec::SmallVec; +use sonatina_ir::{module::ModuleCtx, BlockId, Function, Immediate, Inst, InstId, ValueId}; + +pub trait LowerBackend { + type MInst; + + fn lower(&self, ctx: &mut Lower, alloc: &mut dyn Allocator, inst: InstId); + // -> Option == SmallVec<[ValueRegs; 2]> + + fn enter_function( + &self, + ctx: &mut Lower, + alloc: &mut dyn Allocator, + function: &Function, + ); + fn enter_block(&self, ctx: &mut Lower, alloc: &mut dyn Allocator, block: BlockId); + + fn update_opcode_with_immediate_bytes( + &self, + opcode: &mut Self::MInst, + bytes: &mut SmallVec<[u8; 8]>, + ); + + fn update_opcode_with_label( + &self, + opcode: &mut Self::MInst, + label_offset: u32, + ) -> SmallVec<[u8; 4]>; + + fn emit_opcode(&self, opcode: &Self::MInst, buf: &mut Vec); + fn emit_immediate_bytes(&self, bytes: &[u8], buf: &mut Vec); + fn emit_label(&self, address: u32, buf: &mut Vec); +} + +#[derive(Debug)] +pub struct CodegenError {} +pub type CodegenResult = Result; + +pub struct Lower<'a, Op> { + pub module: &'a ModuleCtx, + function: &'a Function, + vcode: VCode, + + cur_insn: Option, + cur_block: Option, +} + +impl<'a, Op: Default> Lower<'a, Op> { + pub fn new(module: &'a ModuleCtx, function: &'a Function) -> Self { + Lower { + module, + function, + vcode: VCode::default(), + cur_insn: None, + cur_block: None, + } + } + + pub fn lower>( + mut self, + backend: &B, + alloc: &mut dyn Allocator, + ) -> CodegenResult> { + self.cur_block = self.function.layout.entry_block(); + let f = self.function; + backend.enter_function(&mut self, alloc, f); + + for block in self.function.layout.iter_block() { + self.cur_block = Some(block); + backend.enter_block(&mut self, alloc, block); + + for insn in self.function.layout.iter_inst(block) { + self.cur_insn = Some(insn); + backend.lower(&mut self, alloc, insn); + } + } + + Ok(self.vcode) + } + + pub fn push(&mut self, op: Op) -> VCodeInst { + self.vcode + .add_inst_to_block(op, self.cur_insn, self.cur_block.unwrap()) + } + + pub fn push_with_imm(&mut self, op: Op, bytes: &[u8]) { + let i = self.push(op); + self.add_immediate(i, bytes); + } + + pub fn push_jump_target(&mut self, op: Op, dest: Label) { + let op = self.push(op); + self.add_label_reference(op, dest); + } + + pub fn next_insn(&self) -> VCodeInst { + self.vcode.insts.next_key() + } + + pub fn add_immediate(&mut self, inst: VCodeInst, bytes: &[u8]) { + self.vcode.inst_imm_bytes.insert((inst, bytes.into())); + } + + pub fn add_label_reference(&mut self, inst: VCodeInst, dest: Label) { + let label = self.vcode.labels.push(dest); + self.vcode.label_uses.insert((inst, label)); + } + + pub fn insn_data(&self, inst: InstId) -> &dyn Inst { + self.function.dfg.inst(inst) + } + + pub fn value_imm(&self, value: ValueId) -> Option { + self.function.dfg.value_imm(value) + } + + pub fn insn_result(&self, inst: InstId) -> Option { + self.function.dfg.inst_result(inst) + } + + pub fn insn_block(&self, inst: InstId) -> BlockId { + self.function.layout.inst_block(inst) + } + + pub fn is_entry(&self, block: BlockId) -> bool { + self.function.layout.entry_block() == Some(block) + } + + /// Check if the given `BlockId` is next in the layout. + /// Used for avoiding unnecessary `jump` operations. + pub fn is_next_block(&self, block: BlockId) -> bool { + let Some(cur) = self.cur_block else { + return false; + }; + self.function.layout.next_block_of(cur) == Some(block) + } +} diff --git a/crates/codegen/src/machinst/mod.rs b/crates/codegen/src/machinst/mod.rs new file mode 100644 index 00000000..758fad00 --- /dev/null +++ b/crates/codegen/src/machinst/mod.rs @@ -0,0 +1,3 @@ +pub mod assemble; +pub mod lower; +pub mod vcode; diff --git a/crates/codegen/src/machinst/vcode.rs b/crates/codegen/src/machinst/vcode.rs new file mode 100644 index 00000000..af8890d6 --- /dev/null +++ b/crates/codegen/src/machinst/vcode.rs @@ -0,0 +1,167 @@ +use cranelift_entity::{ + entity_impl, + packed_option::{PackedOption, ReservedValue}, + EntityList, EntityRef, ListPool, PrimaryMap, SecondaryMap, SparseMap, SparseMapValue, +}; +use smallvec::SmallVec; +use sonatina_ir::{ + ir_writer::{FuncWriteCtx, FunctionSignature, InstStatement, IrWrite}, + module::FuncRef, + BlockId, InstId, U256, +}; +use std::{fmt, io}; + +// xxx offset reference graph? +// fn call graph + +#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)] +pub struct LabelId(pub u32); +entity_impl!(LabelId); + +#[derive(Debug, Copy, Clone)] +pub enum Label { + Insn(VCodeInst), + Block(BlockId), + Function(FuncRef), +} + +#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)] +pub struct VCodeInst(pub u32); +entity_impl!(VCodeInst); + +pub struct VCode { + pub insts: PrimaryMap, + pub inst_ir: SecondaryMap>, + + /// Immediate bytes for PUSH* ops + pub inst_imm_bytes: SparseMap)>, + + pub labels: PrimaryMap, + /// Instructions that contain label offsets that will need to updated + /// when the bytecode is emitted + pub label_uses: SparseMap, + + pub blocks: SecondaryMap>, + + pub insts_pool: ListPool, +} + +impl VCode { + pub fn add_inst_to_block( + &mut self, + op: Op, + source_insn: Option, + block: BlockId, + ) -> VCodeInst { + let inst = self.insts.push(op); + self.inst_ir[inst] = source_insn.into(); + self.blocks[block].push(inst, &mut self.insts_pool); + inst + } + + pub fn block_insns(&self, block: BlockId) -> impl Iterator + '_ { + EntityIter { + list: &self.blocks[block], + pool: &self.insts_pool, + next: 0, + } + } + + // pub fn emit(self, alloc: &sonatina_stackalloc::Output) +} + +impl IrWrite> for VCode +where + Op: fmt::Debug, +{ + fn write(&self, w: &mut W, ctx: &FuncWriteCtx) -> std::io::Result<()> + where + W: io::Write, + { + write!(w, "// ")?; + FunctionSignature.write(w, ctx)?; + writeln!(w)?; + writeln!(w, "{}:", ctx.func.sig.name())?; + + let mut cur_ir = None; + for block in self.blocks.keys() { + if self.block_insns(block).count() > 0 { + writeln!(w, " block{}:", block.0)?; + } + for (idx, insn) in self.block_insns(block).enumerate() { + write!(w, " {:?}", self.insts[insn])?; + if let Some((_, bytes)) = self.inst_imm_bytes.get(insn) { + let mut be = [0; 32]; + be[32 - bytes.len()..].copy_from_slice(bytes); + let imm = U256::from_big_endian(&be); + write!(w, " 0x{imm:x} ({imm})")?; + } else if let Some((_, label)) = self.label_uses.get(insn) { + match self.labels[*label] { + Label::Block(BlockId(n)) => write!(w, " block{n}"), + Label::Insn(insn) => { + let pos = self + .block_insns(block) + .position(|i| i == insn) + .expect("Label::Insn must be in same block"); + let offset: i32 = pos as i32 - idx as i32; + write!(w, " `pc + ({offset})`") + } + Label::Function(func) => write!(w, " {func:?}"), + }?; + } + + if let Some(ir) = self.inst_ir[insn].expand() { + if cur_ir != Some(ir) { + cur_ir = Some(ir); + write!(w, " // ")?; + InstStatement(ir).write(w, ctx)?; + } + } + writeln!(w)?; + } + } + Ok(()) + } +} + +impl Default for VCode { + fn default() -> Self { + Self { + insts: Default::default(), + inst_ir: Default::default(), + inst_imm_bytes: SparseMap::new(), // no `Default` impl + labels: PrimaryMap::default(), + label_uses: SparseMap::new(), + blocks: Default::default(), + insts_pool: Default::default(), + } + } +} + +impl SparseMapValue for (VCodeInst, T) { + fn key(&self) -> VCodeInst { + self.0 + } +} + +struct EntityIter<'a, T> +where + T: EntityRef + ReservedValue, +{ + list: &'a EntityList, + pool: &'a ListPool, + next: usize, +} + +impl<'a, T> Iterator for EntityIter<'a, T> +where + T: EntityRef + ReservedValue, +{ + type Item = T; + + fn next(&mut self) -> Option { + let next = self.list.get(self.next, self.pool)?; + self.next += 1; + Some(next) + } +} diff --git a/crates/codegen/src/optim/adce.rs b/crates/codegen/src/optim/adce.rs index 449f4b12..e635b356 100644 --- a/crates/codegen/src/optim/adce.rs +++ b/crates/codegen/src/optim/adce.rs @@ -196,7 +196,7 @@ impl AdceSolver { }; inserter.set_location(CursorLocation::At(last_inst)); - let dests: Vec<_> = func + let dests = func .dfg .branch_info(last_inst) .map(|bi| bi.dests()) diff --git a/crates/codegen/src/stackalloc/local_stack.rs b/crates/codegen/src/stackalloc/local_stack.rs new file mode 100644 index 00000000..cbc58339 --- /dev/null +++ b/crates/codegen/src/stackalloc/local_stack.rs @@ -0,0 +1,608 @@ +use super::{Action, Actions}; +use crate::{bitset::BitSet, liveness::Liveness}; +use if_chain::if_chain; +use smallvec::{smallvec, SmallVec}; +use sonatina_ir::{DataFlowGraph, Immediate, ValueId}; +use std::{collections::VecDeque, fmt}; + +pub type MemSlot = SmallVec<[ValueId; 4]>; + +const REACH: usize = 16; + +// xxx rename +#[derive(Clone, Default)] +pub struct LocalStack { + stack: VecDeque, + dead: BitSet, + + /// `usize` is reverse index (position relative to bottom of stack). + /// Eg. [a, b, c] freeze(a) + /// [2, 1, 0] ridxs + /// is_frozen(0) => len - 1 - 0 == 3 + frozen: SmallVec<[(usize, ValueId); 2]>, +} +impl LocalStack { + /// Initialize stack with given `vals`, in order (ie, first element of `vals` + /// is on top of stack). + pub fn with_values(vals: &[ValueId]) -> Self { + Self { + stack: VecDeque::from_iter(vals.iter().copied()), + dead: BitSet::new(), + frozen: SmallVec::default(), + } + } + + pub fn force_pop(&mut self) { + self.stack.pop_front(); + } + + /// "Rename" the value in a given stack slot; for phi values. + /// Panics if slot >= stack.len(). + pub fn rename_slot(&mut self, slot: usize, val: ValueId) { + debug_assert!(!self.is_frozen(slot)); + self.stack[slot] = val; + } + + pub fn freeze(&mut self, mut values: BitSet) { + if values.is_empty() { + self.unfreeze(); + return; + } + + // We only freeze the bottommost instance of each value. + for (ridx, val) in self.stack.iter_mut().rev().enumerate() { + if values.remove(*val) { + self.frozen.push((ridx, *val)); + } + } + } + + pub fn unfreeze(&mut self) { + self.frozen.clear(); + } + + fn is_frozen(&self, slot: usize) -> bool { + debug_assert!(self.stack.len() > slot); + + self.frozen + .iter() + .any(|(ridx, _)| *ridx == self.stack.len() - 1 - slot) + } + + fn top(&self) -> Option { + self.stack.front().copied() + } + + fn top_n(&self, n: usize) -> impl Iterator + '_ { + self.stack.iter().take(n) + } + + fn first_reachable_dead(&self) -> Option { + self.position_within_reach(|v| self.dead.contains(v)) + } + + fn push(&mut self, act: &mut Actions, val: ValueId, imm: Immediate) { + self.stack.push_front(val); + act.push(Action::Push(imm)); + } + + /// Exchange 0th and nth items + fn swap(&mut self, act: &mut Actions, n: usize) { + if n == 0 { + return; // No-op + } + debug_assert!(n <= REACH); + // xxx relax this (for temporary swaps) + debug_assert!(!self.is_frozen(0)); + debug_assert!(!self.is_frozen(n)); + + act.push(Action::StackSwap(n as u8)); + self.stack.swap(0, n); + } + + // xxx remove? + // fn swap_m_n(&mut self, act: &mut Actions, m: usize, n: usize) { + // assert!(m <= REACH && n <= REACH); + // if m == 0 { + // self.swap(act, n); + // } else if n == 0 { + // self.swap(act, m); + // } else { + // self.swap(act, n); + // self.swap(act, m); + // self.swap(act, n); + // } + // } + + // xxx 0 vs 1 indexing of swap and dup + /// Push a copy of the n-1th item onto the stack + fn dup(&mut self, act: &mut Actions, n: usize) { + debug_assert!(n <= REACH); + let v = self.stack[n]; + self.stack.push_front(v); + act.push(Action::StackDup(n as u8)); + } + + /// Panics if stack is empty, or if top entry is frozen. + fn pop(&mut self, act: &mut Actions) { + debug_assert!(!self.is_frozen(0)); + self.stack.pop_front().unwrap(); + act.push(Action::Pop); + } + + fn pop_while(&mut self, act: &mut Actions, pred: F) + where + F: Fn(ValueId, &Self) -> bool, + { + while let Some(val) = self.stack.front() { + if pred(*val, self) { + self.pop(act) + } else { + break; + } + } + } + + fn pop_all(&mut self, act: &mut Actions) { + while !self.stack.is_empty() { + self.pop(act); + } + } + + /// Ensure `out` is atop an otherwise empty stack. // xxx name + pub fn ret(&mut self, act: &mut Actions, memory: &[MemSlot], return_val: Option) { + let Some(return_val) = return_val else { + self.pop_all(act); + return; + }; + + while let Some(val) = self.stack.front() { + if return_val != *val { + self.pop(act); + } else if self.stack.len() > 1 { + // If there are vals below the return val, + // swap the return val downward and pop the rest off. + self.swap(act, REACH.min(self.stack.len() - 1)); + } else { + break; + } + } + if self.stack.is_empty() { + for (idx, slot) in memory.iter().enumerate() { + if slot.contains(&return_val) { + act.push(Action::MemLoadFrameSlot(idx as u32)); + return; + } + } + panic!("return value not found"); + } + } + + fn clean_top_of_stack(&mut self, act: &mut Actions, pred: F) + where + F: Fn(ValueId, &Self) -> bool, + { + self.pop_while(act, &pred); + + if !self.stack.is_empty() && self.is_frozen(0) { + return; + } + + // if there are deads following the top val, swap it down and pop them off + // xxx only within REACH + let swap_to = self + .stack + .iter() + .skip(1) + .take_while(|v| pred(**v, self)) + .count(); + + if swap_to > 0 { + self.swap(act, swap_to); + self.pop_while(act, pred); + } + } + + pub fn xxx_branch_prep( + &mut self, + arg: ValueId, + last_use: bool, + act: &mut Actions, + memory: &mut Vec, + dfg: &DataFlowGraph, + liveness: &Liveness, + ) { + // xxx is `dead` complete here? + self.clean_top_of_stack(act, |val, zelf| zelf.dead.contains(val)); + + let consumable = last_use + || self.stack.iter().filter(|v| **v == arg).count() > 1 + || memory_slot_of(arg, memory).is_some() + || dfg.value_is_imm(arg); + + match self.location_of(arg, memory, dfg) { + ValueLocation::Stack(pos) => { + if pos >= REACH { + // ValueId is unreachable in the stack, so we allocate + // a slot for the value in memory. At the val def + // point, we'll duplicate it and store it in this + // slot, while leaving a copy on the stack. + let slot = allocate_slot(memory, arg, liveness); + act.push(Action::MemLoadFrameSlot(slot as u32)); + self.stack.push_front(arg); + } else if !consumable { + self.dup(act, pos); + } else if !self.is_frozen(0) { + // if pos is 0, no swap op is emitted + self.swap(act, pos); + } else { + self.dup(act, pos) + } + } + ValueLocation::Immediate(imm) => { + self.push(act, arg, imm); + } + ValueLocation::Memory(pos) => { + act.push(Action::MemLoadFrameSlot(pos as u32)); + self.stack.push_front(arg); + } + ValueLocation::Nowhere => { + panic!("{arg:?} not found on stack or in memory"); + } + } + } + + pub fn move_to_top( + &mut self, + args: &[ValueId], + last_use: &BitSet, + act: &mut Actions, + memory: &mut Vec, + dfg: &DataFlowGraph, + liveness: &Liveness, + ) { + self.clean_top_of_stack(act, |val, zelf| zelf.dead.contains(val)); + + let consumable: BitSet = args + .iter() + .copied() + .filter(|a| { + last_use.contains(*a) + // We allow a val to be consumed if the stack contains + // any additional copies, even if buried. + || self.stack.iter().filter(|v| *v == a).count() > 1 + || memory_slot_of(*a, memory).is_some() + || dfg.value_is_imm(*a) + }) + .collect(); + + // if the top val isn't used by this insn... + if let Some(top) = self.stack.front() { + if !args.contains(top) { + if_chain! { + if let Some(last) = args.last(); + if consumable.contains(*last); + if let ValueLocation::Stack(pos) = self.location_of(*last, memory, dfg); + then { + // ... swap it with the last arg value. + self.swap(act, pos); + } else { + if let Some(pos) = self.first_reachable_dead() { + // ... or swap it down to a dead slot. + self.swap(act, pos); + self.pop(act); + } + } + } + } + } + + let all_consumable = args.iter().all(|a| consumable.contains(*a)); + if all_consumable && args.iter().eq(self.top_n(args.len())) { + // happy case: all vals are at top of stack, in order + } else if all_consumable && args.iter().rev().eq(self.top_n(args.len())) { + // all vals are at top of stack, but in reverse order + for swap in reverse(args.len() as u8) { + self.swap(act, swap as usize); + } + + // TODO: other cases where we can do better than duping all args + } else { + // Default behavior: dupe all args to the top. + + // If any args are unreachable in the stack, try to remove enough + // unnecessary values from the stack to make them reachable. + + while args.iter().any(|&v| self.is_buried(v, memory, dfg)) { + if !self.shrink_stack(act, memory, dfg, |v| !args.contains(&v)) { + break; + } + } + + // TODO: duplicate any immediates that are used again this block + for (idx, val) in args.iter().copied().rev().enumerate() { + match self.location_of(val, memory, dfg) { + ValueLocation::Immediate(imm) => { + self.push(act, val, imm); + } + ValueLocation::Stack(pos) => { + if pos == 0 && consumable.contains(val) { + // do nothing + } else if idx == 0 && consumable.contains(val) { + // last arg is consumable; swap it up. + self.swap(act, pos) + } else if pos == 1 + && args.last().copied() == self.top() + && consumable.contains(val) + { + // stack is [prev_arg, this_arg, ..]; swap. + self.swap(act, 1); + } else if pos >= REACH { + // ValueId is unreachable in the stack, so we allocate + // a slot for the value in the stack. At the val def + // point, we'll duplicate it and store it in this + // slot, while leaving a copy on the stack. + let slot = allocate_slot(memory, val, liveness); + act.push(Action::MemLoadFrameSlot(slot as u32)); + self.stack.push_front(val); + } else { + self.dup(act, pos); + } + } + ValueLocation::Memory(pos) => { + act.push(Action::MemLoadFrameSlot(pos as u32)); + self.stack.push_front(val); + } + ValueLocation::Nowhere => { + panic!("{val:?} not found on stack or in memory"); + } + } + } + } + } + + #[allow(clippy::too_many_arguments)] + pub fn step( + &mut self, + args: &[ValueId], + last_use: &BitSet, + result: Option, + act: &mut Actions, + memory: &mut Vec, + dfg: &DataFlowGraph, + liveness: &Liveness, + ) { + self.move_to_top(args, last_use, act, memory, dfg, liveness); + + // pop consumed args off stack, and mark other copies as dead + for _ in args { + self.stack.pop_front(); + } + self.dead.union_with(last_use); + + if let Some(res) = result { + self.stack.push_front(res); + } + } + + /// Remove all dead values from unfrozen portion of stack, + /// leaving any `args` values at the top, and any + /// `live_out` values at the bottom. + /// Args must be moved to the top before calling `retain`. + pub fn retain(&mut self, act: &mut Actions, args: &[ValueId], live_out: &BitSet) { + eprintln!("retain {args:?} in {:?}", &self); + debug_assert!(self.top_n(args.len()).eq(args.iter())); + + eprintln!("retain 1 {:?}", &self); + + self.clean_top_of_stack(act, |val, _| { + !args.contains(&val) && !live_out.contains(val) + }); + + // xxx + // while let Some(mut pos) = self + // .stack + // .iter() + // .take(REACH) + // .skip(args.len()) + // .position(|e| !live_out.contains(e.val)) + // { + // pos += args.len(); + + // self.swap(act, pos); + // eprintln!("swapped {:?} {:?}", &self, act); + + // self.remove_leading_deads(act); + // eprintln!("popped {:?} {:?}", &self, act); + + // if args.len() > 1 { + // // Swap first arg back to the top, so the args don't get shuffled + // debug_assert_eq!(self.stack.get(pos - 1).map(|e| &e.val), args.first()); + // self.swap(act, pos - 1); + // } + // } + + eprintln!("retain 3 {:?} {:?}", &self, act); + + // xxx remove + // if let Some(arg) = args.first() { + // let pos = self + // .position_within_reach(|v| v == *arg) + // .expect("`retain` arg must be reachable"); + // self.swap(act, pos); + // } + } + + fn position_within_reach(&self, predicate: F) -> Option + where + F: FnMut(ValueId) -> bool, + { + self.top_n(REACH).copied().position(predicate) + } + + fn shrink_stack( + &mut self, + act: &mut Actions, + memory: &[MemSlot], + dfg: &DataFlowGraph, + poppable: F, + ) -> bool + where + F: Fn(ValueId) -> bool, + { + // TODO: prioritize popping deads + let Some(pos) = self.position_within_reach(|v| { + poppable(v) + && (self.dead.contains(v) + || dfg.value_is_imm(v) + || memory_slot_of(v, memory).is_some()) + }) else { + return false; + }; + + self.swap(act, pos); + self.pop(act); + true + } + + fn is_buried(&self, val: ValueId, memory: &[MemSlot], dfg: &DataFlowGraph) -> bool { + if let ValueLocation::Stack(pos) = self.location_of(val, memory, dfg) { + pos >= REACH + } else { + false + } + } + + fn location_of(&self, val: ValueId, memory: &[MemSlot], dfg: &DataFlowGraph) -> ValueLocation { + if let Some(pos) = self.stack.iter().position(|v| *v == val) { + // If item is buried, check if it's imm or in memory + if pos >= REACH { + if let Some(imm) = dfg.value_imm(val) { + return ValueLocation::Immediate(imm); + } else if let Some(pos) = memory_slot_of(val, memory) { + return ValueLocation::Memory(pos); + } + } + ValueLocation::Stack(pos) + } else if let Some(imm) = dfg.value_imm(val) { + ValueLocation::Immediate(imm) + } else if let Some(pos) = memory_slot_of(val, memory) { + ValueLocation::Memory(pos) + } else { + ValueLocation::Nowhere + } + } +} + +enum ValueLocation { + Immediate(Immediate), + Stack(usize), + Memory(usize), + Nowhere, +} + +pub fn memory_slot_of(val: ValueId, memory: &[MemSlot]) -> Option { + memory.iter().position(|vs| vs.contains(&val)) +} + +fn allocate_slot(mem: &mut Vec, val: ValueId, liveness: &Liveness) -> usize { + for (i, slot) in mem.iter_mut().enumerate() { + if slot_can_hold(slot, val, liveness) { + slot.push(val); + return i; + } + } + mem.push(smallvec![val]); + mem.len() - 1 +} + +fn slot_can_hold(slot: &[ValueId], val: ValueId, liveness: &Liveness) -> bool { + // xxx use insn-level liveness for single-block conflict case + let live = &liveness.val_live_blocks[val]; + slot.iter() + .all(|v| live.is_disjoint(&liveness.val_live_blocks[*v])) +} + +/// Generate SWAP operations required to reverse the top `n` items of the stack +fn reverse(n: u8) -> SmallVec<[u8; 8]> { + let mut swaps = smallvec![]; + for i in 1..n / 2 + 1 { + swaps.push(n - i); + if i < n - i - 1 { + swaps.push(i); + } + } + for i in (1..n / 2).rev() { + swaps.push(i); + } + swaps +} + +impl fmt::Debug for LocalStack { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[")?; + let mut pref = ""; + for (idx, val) in self.stack.iter().enumerate() { + write!(f, "{pref}")?; + if self.dead.contains(*val) { + write!(f, "dead({})", val.as_u32())?; + } else if self.is_frozen(idx) { + write!(f, "frozen({})", val.as_u32())?; + } else { + write!(f, "{}", val.as_u32())?; + } + pref = ", "; + } + write!(f, "]") + } +} + +#[cfg(test)] +mod tests { + use std::collections::VecDeque; + + use super::BitSet; + use sonatina_ir::ValueId; + + use crate::stackalloc::Actions; + + use super::{reverse, LocalStack}; + + #[test] + fn test_reverse() { + assert_eq!(reverse(2).as_slice(), [1].as_slice()); + assert_eq!(reverse(3).as_slice(), [2].as_slice()); + assert_eq!(reverse(4).as_slice(), [3, 1, 2, 1].as_slice()); + assert_eq!(reverse(5).as_slice(), [4, 1, 3, 1].as_slice()); + assert_eq!(reverse(6).as_slice(), [5, 1, 4, 2, 3, 2, 1].as_slice()); + assert_eq!(reverse(7).as_slice(), [6, 1, 5, 2, 4, 2, 1].as_slice()); + assert_eq!( + reverse(8).as_slice(), + [7, 1, 6, 2, 5, 3, 4, 3, 2, 1].as_slice() + ); + assert_eq!( + reverse(9).as_slice(), + [8, 1, 7, 2, 6, 3, 5, 3, 2, 1].as_slice() + ); + assert_eq!( + reverse(10).as_slice(), + [9, 1, 8, 2, 7, 3, 6, 4, 5, 4, 3, 2, 1].as_slice() + ); + assert_eq!( + reverse(11).as_slice(), + [10, 1, 9, 2, 8, 3, 7, 4, 6, 4, 3, 2, 1].as_slice() + ); + } + + #[test] + fn clean_top_of_stack() { + let mut stack = + LocalStack::with_values(&[ValueId(1), ValueId(2), ValueId(3), ValueId(4), ValueId(5)]); + let live = BitSet::from([ValueId(2), ValueId(5)]); + + let mut act = Actions::default(); + stack.clean_top_of_stack(&mut act, |val, _| !live.contains(val)); + + assert_eq!(stack.stack, VecDeque::from([ValueId(2), ValueId(5)])); + } +} diff --git a/crates/codegen/src/stackalloc/mod.rs b/crates/codegen/src/stackalloc/mod.rs new file mode 100644 index 00000000..f27b4a8f --- /dev/null +++ b/crates/codegen/src/stackalloc/mod.rs @@ -0,0 +1,36 @@ +use smallvec::SmallVec; +use sonatina_ir::{BlockId, Function, Immediate, InstId, ValueId}; + +mod local_stack; +mod simple; +pub use simple::SimpleAlloc; + +pub type Actions = SmallVec<[Action; 2]>; + +pub trait Allocator { + fn enter_function(&self, function: &Function) -> Actions; + + // xxx rename these to make it clear that these are pre- and post-insn operations + /// Return the actions required to place `vals` on the stack, + /// in the specified order. I.e. the first `Value` in `vals` + /// will be on the top of the stack. + fn read(&self, inst: InstId, vals: &[ValueId]) -> Actions; + fn write(&self, inst: InstId, val: Option) -> Actions; + + fn traverse_edge(&self, from: BlockId, to: BlockId) -> Actions; +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Action { + StackDup(u8), + StackSwap(u8), + Push(Immediate), + /// For CALL: Push code offset that callee should jump to upon return + PushContinuationOffset, + Pop, + MemLoadAbs(u32), + /// Relative to `LowerBackend`-defined frame pointer + MemLoadFrameSlot(u32), + MemStoreAbs(u32), + MemStoreFrameSlot(u32), +} diff --git a/crates/codegen/src/stackalloc/simple.rs b/crates/codegen/src/stackalloc/simple.rs new file mode 100644 index 00000000..d442bdea --- /dev/null +++ b/crates/codegen/src/stackalloc/simple.rs @@ -0,0 +1,401 @@ +use super::{ + local_stack::{memory_slot_of, LocalStack, MemSlot}, + Action, Actions, Allocator, +}; +use crate::{ + bitset::BitSet, + domtree::{DFSet, DomTree}, + liveness, + liveness::Liveness, +}; +use cranelift_entity::SecondaryMap; +use smallvec::{smallvec, SmallVec}; +use sonatina_ir::{ + cfg::ControlFlowGraph, inst::control_flow::BranchKind, BlockId, Function, InstId, ValueId, +}; +use std::collections::BTreeMap; + +// xxx notes +// +// stack layout constraints: +// - if any values escape block dominance region: +// - those values must exist on the stack on block entry +// - they must remain in the same position, relative to one another +// +// - if next block is not dominated by current block: +// - there can be no entries at the top of the stack above the live-out values +// (which must be in their respective locked positions relative to each other) +// - xxx consider live-in status of next blocks, not live-out of current block +// +// - values defined in a block can out-live the block, but can't escape the dominance zone +// +// - if block is inside of a loop: +// - it can't increase the stack depth (except for live-out values and phi args) +// +// - function return blocks: +// - stack must be empty, except return val +// - TODO: leave junk on stack when it doesn't matter +// eg main fn, tail call from main fn, ... +// + +#[derive(Default)] +pub struct SimpleAlloc { + /// The actions that must be performed to arrange the stack prior to the + /// execution of each instruction. + actions: SecondaryMap, + brtable_actions: BTreeMap<(InstId, ValueId), Actions>, + edge_actions: BTreeMap<(BlockId, BlockId), Actions>, + /// Memory slot assignments are fn-global. + /// Slots can be shared by values with non-overlapping liveness. + memory: Vec, +} + +impl SimpleAlloc { + pub fn for_function( + func: &Function, + cfg: &ControlFlowGraph, + dom: &DomTree, + liveness: &Liveness, + _stack_size: u8, // xxx + ) -> Self { + let g = SimpleAllocBuilder::new(func, cfg, dom, liveness); + g.compute() + } +} + +pub struct SimpleAllocBuilder<'a> { + func: &'a Function, + cfg: &'a ControlFlowGraph, + dom: &'a DomTree, + liveness: &'a Liveness, + + alloc: SimpleAlloc, +} +impl<'a> SimpleAllocBuilder<'a> { + pub fn new( + func: &'a Function, + cfg: &'a ControlFlowGraph, + dom: &'a DomTree, + liveness: &'a Liveness, + ) -> Self { + Self { + func, + cfg, + dom, + liveness, + // block_val_live_range: BTreeMap::new(), + alloc: SimpleAlloc::default(), + } + } + + fn frozen_values_for_block(&self, block: BlockId, dom_frontiers: &DFSet) -> BitSet { + // ValueIds that escape into the block's dominance frontier must + // remain in their current positions relative to each other. + let mut escapees = self.liveness.block_live_outs(block).clone(); + for frontier_block in dom_frontiers.frontiers(block) { + escapees.difference_with(self.liveness.block_live_ins(*frontier_block)); + } + + // If there's only one value that escapes, it can be moved around freely. + if escapees.len() > 1 { + escapees + } else { + BitSet::default() + } + } + + pub fn compute(mut self) -> SimpleAlloc { + let Some(entry) = self.cfg.entry() else { + return self.alloc; + }; + // xxx ensure entry block can't have preds + assert_eq!(self.cfg.pred_num_of(entry), 0); + + let dom_frontiers = self.dom.compute_df(self.cfg); + + // The inherited stack of the entry block contains the fn args. + // xxx store SplitStack + let mut inherited_stack = BTreeMap::new(); + inherited_stack.insert(entry, LocalStack::with_values(&self.func.arg_values)); + + for block in self.dom.rpo().iter().copied() { + // xxx allow crit edges, but no actions on crit edge + debug_assert!( + !has_critical_edge(self.func, self.cfg, block), + "stack_alloc assumes no critical edges. \ + Please run `CriticalEdgeSplitter` prior to `Liveness` analysis" + ); + + // xxx if block has incoming backedge we should keep a copy + // of the inherited stack so we can verify that the stacks match + let mut stack = inherited_stack.remove(&block).unwrap_or_else(|| { + // We must have visited a pred block and stored an inherited stack + // if there are live-in values or phi arguments. + debug_assert!(self.liveness.block_live_ins(block).is_empty()); + debug_assert!(!self + .func + .layout + .first_inst_of(block) + .map(|insn| self.func.dfg.is_phi(insn)) + .unwrap_or_default()); + + LocalStack::default() + }); + + stack.freeze(self.frozen_values_for_block(block, &dom_frontiers)); + + let live_out = self.liveness.block_live_outs(block); + let die_set = BitSet::difference(self.liveness.block_live_ins(block), live_out); + let mut remaining_uses = + liveness::value_uses_in_block_matching_predicate(self.func, block, |val| { + die_set.contains(val) + }); + + if block == entry { + for arg in &self.func.arg_values { + if !live_out.contains(*arg) { + // xxx check and remove this block + debug_assert!(remaining_uses.contains_key(arg)); + } + } + } + + for (idx, inst_id) in self.func.layout.iter_inst(block).enumerate() { + let result = self.func.dfg.inst_result(inst_id); + let inst = self.func.dfg.inst(inst_id); + + if self.func.dfg.is_phi(inst_id) { + let r = result.unwrap(); + stack.rename_slot(idx, result.unwrap()); + if !live_out.contains(r) { + // xxx check and remove + debug_assert!(remaining_uses.contains_key(&r)); + } + continue; + } else if self.func.dfg.is_return(inst_id) { + let arg = inst.collect_values().first().copied(); + stack.ret(&mut self.alloc.actions[inst_id], &self.alloc.memory, arg); + break; + } else if let Some(branch) = self.func.dfg.branch_info(inst_id) { + let act = &mut self.alloc.actions[inst_id]; + + match branch.branch_kind() { + BranchKind::Jump(_) => { + let dest = branch.dests()[0]; + let args = phi_args_for_edge(self.func, block, dest); + + if !args.is_empty() { + stack.move_to_top( + &args, + &dead_set(&args, live_out), + act, + &mut self.alloc.memory, + &self.func.dfg, + self.liveness, + ); + } + eprintln!("JUMP {block:?}->{dest:?}, args: {args:?}, stack: {stack:?}, live_out: {live_out:?}"); + // xxx allow phi vals to move into dead xfer slots + stack.retain(act, &args, live_out); + eprintln!("after retain: {:?}", &stack); + // xxx debug_assert inherited stacks are equal + inherited_stack.insert(dest, stack); + } + BranchKind::Br(_) => { + let cond = inst.collect_values()[0]; + let [nz_dest, z_dest] = branch.dests().as_slice().try_into().unwrap(); + + // These things only happen on critical edges, which + // must be split before stack alloc is performed. + debug_assert!(!inherited_stack.contains_key(&nz_dest)); + debug_assert!(!inherited_stack.contains_key(&z_dest)); + debug_assert!(phi_args_for_edge(self.func, block, nz_dest).is_empty()); + debug_assert!(phi_args_for_edge(self.func, block, z_dest).is_empty()); + + let act = &mut self.alloc.actions[inst_id]; + stack.xxx_branch_prep( + cond, + !live_out.contains(cond), + act, + &mut self.alloc.memory, + &self.func.dfg, + self.liveness, + ); + eprintln!( + "BRANCH {block:?}->{nz_dest:?} | {z_dest:?}, {cond:?}, {:?}", + &stack + ); + + // "Consume" branch condition + stack.force_pop(); + + inherited_stack.insert(nz_dest, stack.clone()); + inherited_stack.insert(z_dest, stack); + } + BranchKind::BrTable(br) => { + let args = inst.collect_values(); + let table = br.table(); + let default = br.default(); + + // Remove all dead locals + let mut live = BitSet::from(args.as_slice()); + live.union_with(live_out); + eprintln!("BRTABLE {args:?}, {:?}", &stack); + stack.retain(&mut self.alloc.actions[inst_id], &[], &live); + eprintln!("after retain: {:?}", &stack); + + let mut args_iter = args.iter(); + let comp = args_iter.next().unwrap(); + + for (idx, (arg, dest)) in br.table().iter().enumerate() { + let case_args = &[*arg, *comp]; + let mut dead = dead_set(case_args, live_out); + // The first arg (`comp`) needs to be available + // for each case. The last case can consume it. + if idx != table.len() - 1 { + dead.remove(*comp); + } + let mut act = Actions::new(); + stack.step( + case_args, + &dead, + None, + &mut act, + &mut self.alloc.memory, + &self.func.dfg, + self.liveness, + ); + + // Assert that this is not a critical edge + debug_assert!(!inherited_stack.contains_key(dest)); + debug_assert!(phi_args_for_edge(self.func, block, *dest).is_empty()); + + inherited_stack.insert(*dest, stack.clone()); + self.alloc.brtable_actions.insert((inst_id, *arg), act); + } + + if let Some(dest) = default { + debug_assert!(!inherited_stack.contains_key(dest)); + debug_assert!(phi_args_for_edge(self.func, block, *dest).is_empty()); + inherited_stack.insert(*dest, stack); + } + } + } + break; + } else { + let args = &inst.collect_values(); + let consumable = args + .iter() + .copied() + .filter(|v| { + if let Some(count) = remaining_uses.get_mut(v) { + *count -= 1; + *count == 0 + } else { + false + } + }) + .collect::>(); + + let act = &mut self.alloc.actions[inst_id]; + + if self.func.dfg.is_call(inst_id) { + act.push(Action::PushContinuationOffset); + } + + stack.step( + args, + &consumable, + result, + act, + &mut self.alloc.memory, + &self.func.dfg, + self.liveness, + ); + + if let Some(val) = result { + if !live_out.contains(val) { + remaining_uses.insert(val, self.liveness.val_use_count[val]); + } + } + } + } + } + + self.alloc + } +} + +fn phi_args_for_edge(func: &Function, from: BlockId, to: BlockId) -> SmallVec<[ValueId; 2]> { + func.layout + .iter_inst(to) + .map_while(|inst| { + func.dfg.cast_phi(inst).and_then(|phi| { + phi.args() + .iter() + .find_map(|(val, block)| (*block == from).then_some(*val)) + }) + }) + .collect() +} + +fn dead_set(args: &[ValueId], live: &BitSet) -> BitSet { + args.iter() + .copied() + .filter(|a| !live.contains(*a)) + .collect() +} + +fn has_critical_edge(func: &Function, cfg: &ControlFlowGraph, block: BlockId) -> bool { + let Some(branch) = func + .layout + .last_inst_of(block) + .and_then(|i| func.dfg.branch_info(i)) + else { + return false; + }; + + if branch.num_dests() > 2 { + branch.dests().iter().any(|d| cfg.pred_num_of(*d) > 1) + } else { + false + } +} + +impl Allocator for SimpleAlloc { + fn enter_function(&self, function: &Function) -> Actions { + let mut act = Actions::new(); + for (i, arg) in function.arg_values.iter().enumerate() { + if let Some(slot) = memory_slot_of(*arg, &self.memory) { + act.push(Action::StackDup(i as u8)); + act.push(Action::MemStoreFrameSlot(slot as u32)); + } + } + act + } + + fn read(&self, insn: InstId, vals: &[ValueId]) -> Actions { + if let [val] = vals { + if let Some(act) = self.brtable_actions.get(&(insn, *val)) { + return act.clone(); + } + } + self.actions[insn].clone() + } + fn write(&self, _insn: InstId, val: Option) -> Actions { + let Some(val) = val else { + return smallvec![]; + }; + if let Some(pos) = memory_slot_of(val, &self.memory) { + return smallvec![Action::StackDup(0), Action::MemStoreFrameSlot(pos as u32)]; + } + smallvec![] + } + + fn traverse_edge(&self, from: BlockId, to: BlockId) -> Actions { + self.edge_actions + .get(&(from, to)) + .cloned() + .unwrap_or_default() + } +} diff --git a/crates/codegen/test_files/evm/add.snap b/crates/codegen/test_files/evm/add.snap new file mode 100644 index 00000000..ea85cc50 --- /dev/null +++ b/crates/codegen/test_files/evm/add.snap @@ -0,0 +1,41 @@ +--- +source: crates/codegen/tests/evm.rs +expression: "String::from_utf8(v).unwrap()" +input_file: test_files/evm/add.sntn +--- +// func public %add() -> i32 +add: + block0: + JUMPDEST + PUSH1 0x9c (156) // v1.i32 = add 100000.i32 -100.i32; + PUSH0 + SIGNEXTEND + PUSH3 0x186a0 (100000) + ADD + PUSH2 0x384 (900) // v2.i32 = sub v1 900.i32; + SWAP1 + SUB + PUSH0 // mstore v2 0.i32 i32; + MSTORE + + + +--------------- + +// func public %add() -> i32 +add: + block0: + 0 JUMPDEST + 1 PUSH1 0x9c (156) // v1.i32 = add 100000.i32 -100.i32; + 3 PUSH0 + 4 SIGNEXTEND + 5 PUSH3 0x186a0 (100000) + 9 ADD + 10 PUSH2 0x384 (900) // v2.i32 = sub v1 900.i32; + 13 SWAP1 + 14 SUB + 15 PUSH0 // mstore v2 0.i32 i32; + 16 MSTORE + + +0x5b609c5f0b620186a00161038490035f52 diff --git a/crates/codegen/test_files/evm/add.sntn b/crates/codegen/test_files/evm/add.sntn new file mode 100644 index 00000000..56ef3162 --- /dev/null +++ b/crates/codegen/test_files/evm/add.sntn @@ -0,0 +1,19 @@ +target = "evm-ethereum-london" + +func public %entry() { + block0: + v0.i256 = evm_call_data_load 0.i32; + v1.i32 = shr 224.i32 v0; + v2.i32 = shl 32.i32 v0; + v3.i32 = shr 224.i32 v2; + v4.i32 = call %add v1 v3; + + mstore 0.i32 v4 i32; + evm_return 0.i8 4.i8; +} + +func public %add(v0.i32, v1.i32) -> i32 { + block0: + v2.i32 = add v0 v1; + return v2; +} diff --git a/crates/codegen/test_files/evm/call.snap b/crates/codegen/test_files/evm/call.snap new file mode 100644 index 00000000..1ccc7e62 --- /dev/null +++ b/crates/codegen/test_files/evm/call.snap @@ -0,0 +1,147 @@ +--- +source: crates/codegen/tests/evm.rs +expression: "String::from_utf8(v).unwrap()" +input_file: test_files/evm/call.sntn +--- +// func private %square(v0.i64) -> i64 +square: + block0: + JUMPDEST + MUL // v1.i64 = mul v0 v0; + SWAP1 // return v1; + JUMP + +// func private %invert(v0.i64) -> i64 +invert: + block0: + JUMPDEST + PUSH1 0xff (255) // v1.i64 = mul v0 -1.i64; + PUSH0 + SIGNEXTEND + SWAP1 + MUL + SWAP1 // return v1; + JUMP + +// func public %main(v0.i8, v1.i64) -> i64 +main: + block0: + JUMPDEST + DUP1 // br_table v0 block3 (0.i8 block1) (1.i8 block2); + PUSH0 + EQ + PUSH1 block1 + JUMPI + DUP1 + PUSH1 0x1 (1) + EQ + PUSH1 block2 + JUMPI + PUSH1 block3 + JUMP + block1: + JUMPDEST + PUSH1 `pc + (4)` // v2.i64 = call %square v1; + SWAP1 + PUSH1 FuncRef(0) + JUMP + JUMPDEST + SWAP1 // jump block4; + POP + PUSH1 block4 + JUMP + block2: + JUMPDEST + PUSH1 `pc + (4)` // v3.i64 = call %invert v1; + SWAP1 + PUSH1 FuncRef(1) + JUMP + JUMPDEST + SWAP1 // jump block4; + POP + PUSH1 block4 + JUMP + block3: + JUMPDEST + SWAP1 // jump block4; + POP + PUSH1 block4 + JUMP + block4: + JUMPDEST + SWAP1 // return v4; + JUMP + + + +--------------- + +// func private %square(v0.i64) -> i64 +square: + block0: + 0 JUMPDEST + 1 MUL // v1.i64 = mul v0 v0; + 2 SWAP1 // return v1; + 3 JUMP +// func private %invert(v0.i64) -> i64 +invert: + block0: + 4 JUMPDEST + 5 PUSH1 0xff (255) // v1.i64 = mul v0 -1.i64; + 7 PUSH0 + 8 SIGNEXTEND + 9 SWAP1 + 10 MUL + 11 SWAP1 // return v1; + 12 JUMP +// func public %main(v0.i8, v1.i64) -> i64 +main: + block0: + 13 JUMPDEST + 14 DUP1 // br_table v0 block3 (0.i8 block1) (1.i8 block2); + 15 PUSH0 + 16 EQ + 17 PUSH1 30 (block1) + 19 JUMPI + 20 DUP1 + 21 PUSH1 0x1 (1) + 23 EQ + 24 PUSH1 42 (block2) + 26 JUMPI + 27 PUSH1 55 (block3) + 29 JUMP + block1: + 30 JUMPDEST + 31 PUSH1 36 // v2.i64 = call %square v1; + 33 SWAP1 + 34 PUSH0 0 (FuncRef(0)) + 35 JUMP + 36 JUMPDEST + 37 SWAP1 // jump block4; + 38 POP + 39 PUSH1 61 (block4) + 41 JUMP + block2: + 42 JUMPDEST + 43 PUSH1 49 // v3.i64 = call %invert v1; + 45 SWAP1 + 46 PUSH1 4 (FuncRef(1)) + 48 JUMP + 49 JUMPDEST + 50 SWAP1 // jump block4; + 51 POP + 52 PUSH1 61 (block4) + 54 JUMP + block3: + 55 JUMPDEST + 56 SWAP1 // jump block4; + 57 POP + 58 PUSH1 61 (block4) + 60 JUMP + block4: + 61 JUMPDEST + 62 SWAP1 // return v4; + 63 JUMP + + +0x5b0290565b60ff5f0b900290565b805f14601e5780600114602a576037565b6024905f565b9050603d565b6031906004565b9050603d565b9050603d565b9056 diff --git a/crates/codegen/test_files/evm/call.sntn b/crates/codegen/test_files/evm/call.sntn new file mode 100644 index 00000000..4a7b0162 --- /dev/null +++ b/crates/codegen/test_files/evm/call.sntn @@ -0,0 +1,33 @@ +target = "evm-ethereum-london" + +func private %square(v0.i64) -> i64 { + block0: + v1.i64 = mul v0 v0; + return v1; +} + +func private %invert(v0.i64) -> i64 { + block0: + v1.i64 = mul v0 -1.i64; + return v1; +} + +func public %main(v0.i8, v1.i64) -> i64 { + block0: + br_table v0 block3 (0.i8 block1) (1.i8 block2); + + block1: + v2.i64 = call %square v1; + jump block4; + + block2: + v3.i64 = call %invert v1; + jump block4; + + block3: + jump block4; + + block4: + v4.i64 = phi (v2 block1) (v3 block2) (v0 block3); + return v4; +} diff --git a/crates/codegen/test_files/evm/const_loop.snap b/crates/codegen/test_files/evm/const_loop.snap new file mode 100644 index 00000000..7844f504 --- /dev/null +++ b/crates/codegen/test_files/evm/const_loop.snap @@ -0,0 +1,67 @@ +--- +source: crates/codegen/tests/evm.rs +expression: "String::from_utf8(v).unwrap()" +input_file: test_files/evm/const_loop.sntn +--- +// func public %const_loop() -> i8 +const_loop: + block1: + JUMPDEST + PUSH1 0x1 (1) // jump block2; + PUSH1 block2 + JUMP + block2: + JUMPDEST + PUSH1 0xa (10) // v2.i8 = add v1 10.i8; + DUP2 + ADD + SWAP1 // v3.i1 = gt v2 v1; + DUP2 + GT + PUSH1 block3 // br v3 block3 block4; + JUMPI + PUSH1 block4 + JUMP + block3: + JUMPDEST + SWAP1 // return v2; + JUMP + block4: + JUMPDEST + PUSH1 block2 // jump block2; + JUMP + + + +--------------- + +// func public %const_loop() -> i8 +const_loop: + block1: + 0 JUMPDEST + 1 PUSH1 0x1 (1) // jump block2; + 3 PUSH1 6 (block2) + 5 JUMP + block2: + 6 JUMPDEST + 7 PUSH1 0xa (10) // v2.i8 = add v1 10.i8; + 9 DUP2 + 10 ADD + 11 SWAP1 // v3.i1 = gt v2 v1; + 12 DUP2 + 13 GT + 14 PUSH1 20 (block3) // br v3 block3 block4; + 16 JUMPI + 17 PUSH1 23 (block4) + 19 JUMP + block3: + 20 JUMPDEST + 21 SWAP1 // return v2; + 22 JUMP + block4: + 23 JUMPDEST + 24 PUSH1 6 (block2) // jump block2; + 26 JUMP + + +0x5b60016006565b600a81019081116014576017565b90565b600656 diff --git a/crates/codegen/test_files/evm/const_loop.sntn b/crates/codegen/test_files/evm/const_loop.sntn new file mode 100644 index 00000000..453b4732 --- /dev/null +++ b/crates/codegen/test_files/evm/const_loop.sntn @@ -0,0 +1,15 @@ +target = "evm-ethereum-london" + +func public %const_loop() -> i8 { + block1: + jump block2; + + block2: + v1.i8 = phi (1.i8 block1) (v2 block2); + v2.i8 = add v1 10.i8; + v3.i1 = gt v2 v1; + br v3 block3 block2; + + block3: + return v2; +} diff --git a/crates/codegen/test_files/evm/spill.snap b/crates/codegen/test_files/evm/spill.snap new file mode 100644 index 00000000..e346663c --- /dev/null +++ b/crates/codegen/test_files/evm/spill.snap @@ -0,0 +1,270 @@ +--- +source: crates/codegen/tests/evm.rs +expression: "String::from_utf8(v).unwrap()" +input_file: test_files/evm/spill.sntn +--- +// func public %sum8(v0.i64, v1.i64, v2.i64, v3.i64, v4.i64, v5.i64, v6.i64, v7.i64) -> i64 +sum8: + block0: + JUMPDEST + ADD // v8.i64 = add v0 v1; + SWAP2 // v9.i64 = add v2 v3; + SWAP1 + ADD + SWAP3 // v10.i64 = add v4 v5; + DUP3 + ADD + SWAP5 // v11.i64 = add v6 v7; + DUP5 + ADD + SWAP3 // v12.i64 = add v8 v9; + SWAP1 + ADD + SWAP1 // v13.i64 = add v10 v11; + POP + SWAP1 + DUP4 + ADD + SWAP1 // v14.i64 = add v12 v13; + ADD + SWAP2 // return v14; + POP + POP + SWAP1 + JUMP + +// func public %spill(v0.i64, v1.i64, v2.i64, v3.i64, v4.i64, v5.i64, v6.i64, v7.i64) -> i64 +spill: + block1: + DUP8 + PUSH0 + MLOAD + MSTORE + JUMPDEST + DUP2 // v8.i64 = add v0 v1; + DUP2 + ADD + DUP5 // v9.i64 = add v2 v3; + DUP5 + ADD + DUP8 // v10.i64 = add v4 v5; + DUP8 + ADD + DUP11 // v11.i64 = add v6 v7; + DUP11 + ADD + DUP3 // v12.i64 = add v8 v9; + DUP5 + ADD + DUP2 // v13.i64 = add v10 v11; + DUP4 + ADD + DUP1 // v14.i64 = add v12 v13; + DUP3 + ADD + DUP1 // jump block2; + PUSH1 block2 + JUMP + block2: + JUMPDEST + PUSH1 `pc + (11)` // v16.i64 = call %sum8 v0 v1 v2 v3 v4 v5 v6 v7; + DUP16 + DUP16 + DUP16 + DUP16 + DUP16 + DUP16 + DUP16 + DUP16 + PUSH1 FuncRef(0) + JUMP + JUMPDEST + PUSH1 `pc + (11)` // v17.i64 = call %sum8 v8 v9 v10 v11 v12 v13 v14 v15; + SWAP1 + DUP3 + DUP5 + DUP7 + DUP9 + DUP11 + DUP13 + DUP15 + PUSH1 FuncRef(0) + JUMP + JUMPDEST + SWAP1 // v18.i64 = add v16 v17; + ADD + DUP1 // v19.i64 = add v7 v18; + PUSH0 + MLOAD + MLOAD + ADD + PUSH2 0x3e8 (1000) // v20.i1 = gt v19 1000.i64; + DUP2 + GT + PUSH1 block3 // br v20 block3 block4; + JUMPI + PUSH1 block4 + JUMP + block3: + JUMPDEST + POP // return v18; + SWAP15 + POP + POP + POP + POP + POP + POP + POP + POP + POP + POP + POP + POP + POP + POP + POP + SWAP1 + JUMP + block4: + JUMPDEST + SWAP1 // jump block2; + POP + PUSH1 block2 + JUMP + + + +--------------- + +// func public %sum8(v0.i64, v1.i64, v2.i64, v3.i64, v4.i64, v5.i64, v6.i64, v7.i64) -> i64 +sum8: + block0: + 0 JUMPDEST + 1 ADD // v8.i64 = add v0 v1; + 2 SWAP2 // v9.i64 = add v2 v3; + 3 SWAP1 + 4 ADD + 5 SWAP3 // v10.i64 = add v4 v5; + 6 DUP3 + 7 ADD + 8 SWAP5 // v11.i64 = add v6 v7; + 9 DUP5 + 10 ADD + 11 SWAP3 // v12.i64 = add v8 v9; + 12 SWAP1 + 13 ADD + 14 SWAP1 // v13.i64 = add v10 v11; + 15 POP + 16 SWAP1 + 17 DUP4 + 18 ADD + 19 SWAP1 // v14.i64 = add v12 v13; + 20 ADD + 21 SWAP2 // return v14; + 22 POP + 23 POP + 24 SWAP1 + 25 JUMP +// func public %spill(v0.i64, v1.i64, v2.i64, v3.i64, v4.i64, v5.i64, v6.i64, v7.i64) -> i64 +spill: + block1: + 26 DUP8 + 27 PUSH0 + 28 MLOAD + 29 MSTORE + 30 JUMPDEST + 31 DUP2 // v8.i64 = add v0 v1; + 32 DUP2 + 33 ADD + 34 DUP5 // v9.i64 = add v2 v3; + 35 DUP5 + 36 ADD + 37 DUP8 // v10.i64 = add v4 v5; + 38 DUP8 + 39 ADD + 40 DUP11 // v11.i64 = add v6 v7; + 41 DUP11 + 42 ADD + 43 DUP3 // v12.i64 = add v8 v9; + 44 DUP5 + 45 ADD + 46 DUP2 // v13.i64 = add v10 v11; + 47 DUP4 + 48 ADD + 49 DUP1 // v14.i64 = add v12 v13; + 50 DUP3 + 51 ADD + 52 DUP1 // jump block2; + 53 PUSH1 56 (block2) + 55 JUMP + block2: + 56 JUMPDEST + 57 PUSH1 69 // v16.i64 = call %sum8 v0 v1 v2 v3 v4 v5 v6 v7; + 59 DUP16 + 60 DUP16 + 61 DUP16 + 62 DUP16 + 63 DUP16 + 64 DUP16 + 65 DUP16 + 66 DUP16 + 67 PUSH0 0 (FuncRef(0)) + 68 JUMP + 69 JUMPDEST + 70 PUSH1 82 // v17.i64 = call %sum8 v8 v9 v10 v11 v12 v13 v14 v15; + 72 SWAP1 + 73 DUP3 + 74 DUP5 + 75 DUP7 + 76 DUP9 + 77 DUP11 + 78 DUP13 + 79 DUP15 + 80 PUSH0 0 (FuncRef(0)) + 81 JUMP + 82 JUMPDEST + 83 SWAP1 // v18.i64 = add v16 v17; + 84 ADD + 85 DUP1 // v19.i64 = add v7 v18; + 86 PUSH0 + 87 MLOAD + 88 MLOAD + 89 ADD + 90 PUSH2 0x3e8 (1000) // v20.i1 = gt v19 1000.i64; + 93 DUP2 + 94 GT + 95 PUSH1 101 (block3) // br v20 block3 block4; + 97 JUMPI + 98 PUSH1 121 (block4) + 100 JUMP + block3: + 101 JUMPDEST + 102 POP // return v18; + 103 SWAP15 + 104 POP + 105 POP + 106 POP + 107 POP + 108 POP + 109 POP + 110 POP + 111 POP + 112 POP + 113 POP + 114 POP + 115 POP + 116 POP + 117 POP + 118 POP + 119 SWAP1 + 120 JUMP + block4: + 121 JUMPDEST + 122 SWAP1 // jump block2; + 123 POP + 124 PUSH1 56 (block2) + 126 JUMP + + +0x5b01919001928201948401929001905090830190019150509056875f51525b8181018484018787018a8a01828401818301808201806038565b60458f8f8f8f8f8f8f8f5f565b605290828486888a8c8e5f565b9001805f5151016103e881116065576079565b509e50505050505050505050505050505090565b9050603856 diff --git a/crates/codegen/test_files/evm/spill.sntn b/crates/codegen/test_files/evm/spill.sntn new file mode 100644 index 00000000..56586b6d --- /dev/null +++ b/crates/codegen/test_files/evm/spill.sntn @@ -0,0 +1,39 @@ +target = "evm-ethereum-london" + +func public %sum8(v0.i64, v1.i64, v2.i64, v3.i64, v4.i64, v5.i64, v6.i64, v7.i64) -> i64 { + block0: + v8.i64 = add v0 v1; + v9.i64 = add v2 v3; + v10.i64 = add v4 v5; + v11.i64 = add v6 v7; + v12.i64 = add v8 v9; + v13.i64 = add v10 v11; + v14.i64 = add v12 v13; + return v14; +} + +func public %spill(v0.i64, v1.i64, v2.i64, v3.i64, v4.i64, v5.i64, v6.i64, v7.i64) -> i64 { + block1: + v8.i64 = add v0 v1; + v9.i64 = add v2 v3; + v10.i64 = add v4 v5; + v11.i64 = add v6 v7; + v12.i64 = add v8 v9; + v13.i64 = add v10 v11; + v14.i64 = add v12 v13; + + jump block2; + block2: + v15.i64 = phi (v14 block1) (v19 block2); + + v16.i64 = call %sum8 v0 v1 v2 v3 v4 v5 v6 v7; + v17.i64 = call %sum8 v8 v9 v10 v11 v12 v13 v14 v15; + v18.i64 = add v16 v17; + v19.i64 = add v7 v18; + + v20.i1 = gt v19 1000.i64; + br v20 block3 block2; + + block3: + return v18; +} diff --git a/crates/codegen/tests/build.rs b/crates/codegen/tests/build.rs new file mode 100644 index 00000000..8e048f92 --- /dev/null +++ b/crates/codegen/tests/build.rs @@ -0,0 +1,4 @@ +fn main() { + #[cfg(test)] + println!("cargo:rerun-if-changed=./test_files"); +} diff --git a/crates/codegen/tests/evm.rs b/crates/codegen/tests/evm.rs new file mode 100644 index 00000000..6c250843 --- /dev/null +++ b/crates/codegen/tests/evm.rs @@ -0,0 +1,240 @@ +use dir_test::{dir_test, Fixture}; + +use hex::ToHex; +use revm::{ + inspector_handle_register, + interpreter::Interpreter, + primitives::{ + AccountInfo, Address, Bytecode, Bytes, CancunSpec, Env, ExecutionResult, TransactTo, U256, + }, + Context, EvmContext, Handler, +}; + +use sonatina_codegen::{ + critical_edge::CriticalEdgeSplitter, + domtree::DomTree, + isa::evm::{opcode::OpCode, EvmBackend}, + liveness::Liveness, + machinst::{assemble::ObjectLayout, lower::Lower, vcode::VCode}, + stackalloc::SimpleAlloc, +}; +use sonatina_ir::{ + cfg::ControlFlowGraph, + ir_writer::{FuncWriteCtx, IrWrite}, + isa::evm::Evm, + module::ModuleCtx, + BlockId, Function, +}; +use sonatina_parser::{parse_module, ParsedModule}; +use sonatina_triple::{Architecture, OperatingSystem, Vendor}; +use std::io::{stderr, Write}; + +// XXX copied from fe test-utils +#[macro_export] +macro_rules! snap_test { + ($value:expr, $fixture_path: expr) => { + let mut settings = insta::Settings::new(); + let fixture_path = ::std::path::Path::new($fixture_path); + let fixture_dir = fixture_path.parent().unwrap(); + let fixture_name = fixture_path.file_stem().unwrap().to_str().unwrap(); + + settings.set_snapshot_path(fixture_dir); + settings.set_input_file($fixture_path); + settings.set_prepend_module_to_snapshot(false); + settings.bind(|| { + insta::_macro_support::assert_snapshot( + (insta::_macro_support::AutoName, $value.as_str()).into(), + std::path::Path::new(env!("CARGO_MANIFEST_DIR")), + fixture_name, + module_path!(), + file!(), + line!(), + stringify!($value), + ) + .unwrap() + }) + }; +} + +fn parse_sona(content: &str) -> ParsedModule { + match parse_module(content) { + Ok(module) => module, + Err(errs) => { + let mut w = stderr(); + for err in errs { + err.print(&mut w, "[test]", content, true).unwrap(); + } + panic!("Failed to parse test file. See errors above.") + } + } +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/test_files/evm", + glob: "*.sntn" +)] +fn test_evm(fixture: Fixture<&str>) { + let parsed = parse_sona(fixture.content()); + + let backend = EvmBackend::new(Evm::new(sonatina_triple::TargetTriple { + architecture: Architecture::Evm, + vendor: Vendor::Ethereum, + operating_system: OperatingSystem::Evm(sonatina_triple::EvmVersion::Cancun), + })); + + let mut v = Vec::new(); + let funcs = parsed + .debug + .func_order + .iter() + .map(|fref| { + let (vcode, block_order) = parsed.module.func_store.modify(*fref, |function| { + let (vcode, block_order) = vcode_for_fn(function, &parsed.module.ctx, &backend); + let ctx = FuncWriteCtx::with_debug_provider(function, *fref, &parsed.debug); + vcode.write(&mut v, &ctx).unwrap(); + (vcode, block_order) + }); + writeln!(v).unwrap(); + (*fref, vcode, block_order) + }) + .collect(); + + write!(&mut v, "\n\n---------------\n\n").unwrap(); + + let mut layout = ObjectLayout::new(funcs, 0); + let mut i = 0; + while layout.resize(&backend, 0) { + i += 1; + println!("resize iteration {i}"); + } + layout.print(&mut v, &parsed.module, &parsed.debug).unwrap(); + + let mut bytecode = Vec::new(); + layout.emit(&backend, &mut bytecode); + let hex = bytecode.encode_hex::(); + + writeln!(&mut v, "\n0x{hex}").unwrap(); + + let (res, trace) = run_on_evm(&bytecode); + + writeln!(&mut v, "\n{trace}").unwrap(); + writeln!(&mut v, "\n{res:?}").unwrap(); + + snap_test!(String::from_utf8(v).unwrap(), fixture.path()); +} + +fn run_on_evm(bytecode: &[u8]) -> (ExecutionResult, String) { + let mut db = revm::InMemoryDB::default(); + let revm_bytecode = Bytecode::new_raw(Bytes::copy_from_slice(bytecode)); + let test_address = Address::repeat_byte(0x12); + db.insert_account_info( + test_address, + AccountInfo { + balance: U256::ZERO, + nonce: 0, + code_hash: revm_bytecode.hash_slow(), + code: Some(revm_bytecode), + }, + ); + + let mut env = Env::default(); + env.tx.clear(); + env.tx.transact_to = TransactTo::Call(test_address); + env.tx.data = Bytes::copy_from_slice(&[0, 0, 0, 11, 0, 0, 0, 22]); + + let context = Context::new( + EvmContext::new_with_env(db, Box::new(env)), + // TracingInspector::new(TracingInspectorConfig::all()); + TestInspector::new(vec![]), + ); + + let mut evm = revm::Evm::new(context, Handler::mainnet::()); + evm = evm + .modify() + .append_handler_register(inspector_handle_register) + .build(); + + let res = evm.transact_commit(); + + // let mut w = TraceWriter::new(&mut v).use_colors(ColorChoice::Never); + // w.write_arena(evm.context.external.traces()).unwrap(); + + match res.clone() { + Ok(r) => (r, String::from_utf8(evm.context.external.w).unwrap()), + Err(e) => panic!("evm failure: {e}"), + } +} + +struct TestInspector { + w: W, +} + +impl TestInspector { + fn new(w: W) -> TestInspector { + Self { w } + } +} + +impl revm::Inspector for TestInspector { + fn initialize_interp(&mut self, _interp: &mut Interpreter, _context: &mut EvmContext) { + writeln!( + self.w, + "{:>6} {:<17} input (stack grows to the right)", + "pc", "opcode" + ) + .unwrap(); + } + + fn step(&mut self, interp: &mut Interpreter, _context: &mut EvmContext) { + // xxx tentatively writing input stack; clean up + let pc = interp.program_counter(); + + let op = interp.current_opcode(); + let code = unsafe { std::mem::transmute::(op) }; + + let stack = interp.stack().data(); + + write!( + self.w, + "{:>6} {:0>2} {:<12} ", + pc, + format!("{op:x}"), + code.info().name(), + ) + .unwrap(); + let imm_size = code.info().immediate_size() as usize; + if imm_size > 0 { + let imm_bytes = interp.bytecode.slice((pc + 1)..(pc + 1 + imm_size)); + writeln!(self.w, "{}", imm_bytes).unwrap(); + } else { + writeln!(self.w, "{stack:?}").unwrap(); + } + } + + fn step_end(&mut self, _interp: &mut Interpreter, _context: &mut EvmContext) { + // NOTE: annoying revm behavior: `interp.current_opcode()` now returns the next opcode. + } +} + +fn vcode_for_fn( + function: &mut Function, + module: &ModuleCtx, + backend: &EvmBackend, +) -> (VCode, Vec) { + let mut cfg = ControlFlowGraph::new(); + cfg.compute(function); + let mut splitter = CriticalEdgeSplitter::new(); + splitter.run(function, &mut cfg); + + let mut liveness = Liveness::new(); + liveness.compute(function, &cfg); + let mut dom = DomTree::new(); + dom.compute(&cfg); + let mut alloc = SimpleAlloc::for_function(function, &cfg, &dom, &liveness, 16); + let lower = Lower::new(module, function); + + match lower.lower(backend, &mut alloc) { + Ok(vcode) => (vcode, dom.rpo().to_owned()), + Err(err) => panic!("{:?}", err), + } +} diff --git a/crates/ir/src/dfg.rs b/crates/ir/src/dfg.rs index 28f26dff..d9bd5b24 100644 --- a/crates/ir/src/dfg.rs +++ b/crates/ir/src/dfg.rs @@ -232,6 +232,12 @@ impl DataFlowGraph { InstDowncastMut::downcast_mut(is, inst) } + pub fn cast_call(&self, inst_id: InstId) -> Option<&control_flow::Call> { + let inst = self.inst(inst_id); + let is = self.inst_set(); + InstDowncast::downcast(is, inst) + } + pub fn make_phi(&self, args: Vec<(ValueId, BlockId)>) -> Phi { Phi::new(self.inst_set().phi(), args) } @@ -264,6 +270,15 @@ impl DataFlowGraph { self.cast_phi(inst).is_some() } + pub fn is_call(&self, inst: InstId) -> bool { + self.cast_call(inst).is_some() + } + + pub fn is_return(&self, inst: InstId) -> bool { + <&control_flow::Return as InstDowncast>::downcast(self.inst_set(), self.inst(inst)) + .is_some() + } + pub fn rewrite_branch_dest(&mut self, inst: InstId, from: BlockId, to: BlockId) { let inst_set = self.ctx.inst_set; let Some(branch) = self.branch_info(inst) else { diff --git a/crates/ir/src/inst/control_flow.rs b/crates/ir/src/inst/control_flow.rs index a7fba2a9..e6142113 100644 --- a/crates/ir/src/inst/control_flow.rs +++ b/crates/ir/src/inst/control_flow.rs @@ -1,5 +1,5 @@ use macros::{inst_prop, Inst}; -use smallvec::SmallVec; +use smallvec::{smallvec, SmallVec}; use crate::{module::FuncRef, BlockId, Inst, InstSetBase, ValueId}; @@ -36,10 +36,6 @@ pub struct Phi { } impl Phi { - pub fn iter_args(&self) -> impl Iterator { - self.args.iter() - } - pub fn append_phi_arg(&mut self, value: ValueId, block: BlockId) { self.args.push((value, block)) } @@ -81,7 +77,7 @@ pub struct Return { #[inst_prop] pub trait Branch { - fn dests(&self) -> Vec; + fn dests(&self) -> SmallVec<[BlockId; 2]>; fn num_dests(&self) -> usize; fn remove_dest(&self, isb: &dyn InstSetBase, dest: BlockId) -> Box; fn rewrite_dest(&self, isb: &dyn InstSetBase, from: BlockId, to: BlockId) -> Box; @@ -91,8 +87,8 @@ pub trait Branch { } impl Branch for Jump { - fn dests(&self) -> Vec { - vec![self.dest] + fn dests(&self) -> SmallVec<[BlockId; 2]> { + smallvec![self.dest] } fn num_dests(&self) -> usize { @@ -118,8 +114,8 @@ impl Branch for Jump { } impl Branch for Br { - fn dests(&self) -> Vec { - vec![self.nz_dest, self.z_dest] + fn dests(&self) -> SmallVec<[BlockId; 2]> { + smallvec![self.nz_dest, self.z_dest] } fn num_dests(&self) -> usize { @@ -153,11 +149,11 @@ impl Branch for Br { } impl Branch for BrTable { - fn dests(&self) -> Vec { + fn dests(&self) -> SmallVec<[BlockId; 2]> { let mut dests = if let Some(default) = self.default { - vec![default] + smallvec![default] } else { - vec![] + smallvec![] }; dests.extend(self.table.iter().map(|(_, block)| *block)); diff --git a/crates/ir/src/inst/mod.rs b/crates/ir/src/inst/mod.rs index 8dba2fc2..6b3d0301 100644 --- a/crates/ir/src/inst/mod.rs +++ b/crates/ir/src/inst/mod.rs @@ -35,8 +35,8 @@ pub trait Inst: inst_set::sealed::Registered + DynClone + Any + Send + Sync { fn as_text(&self) -> &'static str; fn is_terminator(&self) -> bool; - fn collect_values(&self) -> Vec { - let mut vs = Vec::new(); + fn collect_values(&self) -> SmallVec<[ValueId; 2]> { + let mut vs = SmallVec::new(); self.visit_values(&mut |v| vs.push(v)); vs diff --git a/crates/ir/src/ir_writer.rs b/crates/ir/src/ir_writer.rs index 951feafe..498467b3 100644 --- a/crates/ir/src/ir_writer.rs +++ b/crates/ir/src/ir_writer.rs @@ -133,21 +133,7 @@ impl<'a> FuncWriter<'a> { } pub fn write(&mut self, w: &mut impl io::Write) -> io::Result<()> { - let func = &self.ctx.func; - write!(w, "func ")?; - func.sig.linkage().write(w, &self.ctx)?; - write!(w, " %{}(", func.sig.name())?; - let arg_values: SmallVec<[ValueWithTy; 8]> = self - .ctx - .func - .arg_values - .iter() - .map(|value| ValueWithTy(*value)) - .collect(); - arg_values.write_with_delim(w, ", ", &self.ctx)?; - - write!(w, ") -> ")?; - func.sig.ret_ty().write(w, &self.ctx)?; + FunctionSignature.write(w, &self.ctx)?; writeln!(w, " {{")?; self.level += 1; @@ -362,6 +348,29 @@ where } } +#[derive(Clone, Copy)] +pub struct FunctionSignature; +impl IrWrite> for FunctionSignature { + fn write(&self, w: &mut W, ctx: &FuncWriteCtx) -> io::Result<()> + where + W: io::Write, + { + write!(w, "func ")?; + ctx.func.sig.linkage().write(w, &ctx)?; + write!(w, " %{}(", ctx.func.sig.name())?; + let arg_values: SmallVec<[ValueWithTy; 8]> = ctx + .func + .arg_values + .iter() + .map(|value| ValueWithTy(*value)) + .collect(); + arg_values.write_with_delim(w, ", ", ctx)?; + + write!(w, ") -> ")?; + ctx.func.sig.ret_ty().write(w, &ctx) + } +} + #[derive(Clone, Copy)] pub struct ValueWithTy(pub ValueId); impl IrWrite> for ValueWithTy { diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index 825a53f6..351a9f30 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -74,27 +74,31 @@ pub fn parse_module(input: &str) -> Result> { builder.declare_function(sig); } - for func in ast.functions.iter() { - if !ctx.check_duplicated_func(&builder, &func.signature.name) { - continue; - } - - let sig = &func.signature; - let args = sig - .params - .iter() - .map(|decl| ctx.type_(&builder, &decl.1)) - .collect::>(); - - let ret_ty = sig - .ret_type - .as_ref() - .map(|t| ctx.type_(&builder, t)) - .unwrap_or(ir::Type::Unit); - let sig = Signature::new(&sig.name.name, sig.linkage, &args, ret_ty); + let func_order = ast + .functions + .iter() + .flat_map(|func| { + if !ctx.check_duplicated_func(&builder, &func.signature.name) { + return None; + } - builder.declare_function(sig); - } + let sig = &func.signature; + let args = sig + .params + .iter() + .map(|decl| ctx.type_(&builder, &decl.1)) + .collect::>(); + + let ret_ty = sig + .ret_type + .as_ref() + .map(|t| ctx.type_(&builder, t)) + .unwrap_or(ir::Type::Unit); + let sig = Signature::new(&sig.name.name, sig.linkage, &args, ret_ty); + + Some(builder.declare_function(sig)) + }) + .collect(); let mut func_comments = SecondaryMap::default(); @@ -110,6 +114,7 @@ pub fn parse_module(input: &str) -> Result> { Ok(ParsedModule { module, debug: DebugInfo { + func_order, module_comments: ast.comments, func_comments, value_names: ctx.value_names, @@ -121,6 +126,7 @@ pub fn parse_module(input: &str) -> Result> { } pub struct DebugInfo { + pub func_order: Vec, pub module_comments: Vec, pub func_comments: SecondaryMap>, pub value_names: FxHashMap>, From 24f72e78b0b0872e9a40cadc8b8259757a65ab23 Mon Sep 17 00:00:00 2001 From: Sean Billig Date: Sat, 23 Nov 2024 20:53:00 -0800 Subject: [PATCH 3/4] Don't emit jump op for fallthroughs --- crates/codegen/test_files/evm/call.snap | 16 ++- crates/codegen/test_files/evm/const_loop.snap | 38 +++---- crates/codegen/test_files/evm/spill.snap | 100 +++++++++--------- 3 files changed, 71 insertions(+), 83 deletions(-) diff --git a/crates/codegen/test_files/evm/call.snap b/crates/codegen/test_files/evm/call.snap index 1ccc7e62..b77c850a 100644 --- a/crates/codegen/test_files/evm/call.snap +++ b/crates/codegen/test_files/evm/call.snap @@ -65,8 +65,6 @@ main: JUMPDEST SWAP1 // jump block4; POP - PUSH1 block4 - JUMP block4: JUMPDEST SWAP1 // return v4; @@ -119,7 +117,7 @@ main: 36 JUMPDEST 37 SWAP1 // jump block4; 38 POP - 39 PUSH1 61 (block4) + 39 PUSH1 58 (block4) 41 JUMP block2: 42 JUMPDEST @@ -130,18 +128,16 @@ main: 49 JUMPDEST 50 SWAP1 // jump block4; 51 POP - 52 PUSH1 61 (block4) + 52 PUSH1 58 (block4) 54 JUMP block3: 55 JUMPDEST 56 SWAP1 // jump block4; 57 POP - 58 PUSH1 61 (block4) - 60 JUMP block4: - 61 JUMPDEST - 62 SWAP1 // return v4; - 63 JUMP + 58 JUMPDEST + 59 SWAP1 // return v4; + 60 JUMP -0x5b0290565b60ff5f0b900290565b805f14601e5780600114602a576037565b6024905f565b9050603d565b6031906004565b9050603d565b9050603d565b9056 +0x5b0290565b60ff5f0b900290565b805f14601e5780600114602a576037565b6024905f565b9050603a565b6031906004565b9050603a565b90505b9056 diff --git a/crates/codegen/test_files/evm/const_loop.snap b/crates/codegen/test_files/evm/const_loop.snap index 7844f504..e1ba2991 100644 --- a/crates/codegen/test_files/evm/const_loop.snap +++ b/crates/codegen/test_files/evm/const_loop.snap @@ -8,8 +8,6 @@ const_loop: block1: JUMPDEST PUSH1 0x1 (1) // jump block2; - PUSH1 block2 - JUMP block2: JUMPDEST PUSH1 0xa (10) // v2.i8 = add v1 10.i8; @@ -40,28 +38,26 @@ const_loop: block1: 0 JUMPDEST 1 PUSH1 0x1 (1) // jump block2; - 3 PUSH1 6 (block2) - 5 JUMP block2: - 6 JUMPDEST - 7 PUSH1 0xa (10) // v2.i8 = add v1 10.i8; + 3 JUMPDEST + 4 PUSH1 0xa (10) // v2.i8 = add v1 10.i8; + 6 DUP2 + 7 ADD + 8 SWAP1 // v3.i1 = gt v2 v1; 9 DUP2 - 10 ADD - 11 SWAP1 // v3.i1 = gt v2 v1; - 12 DUP2 - 13 GT - 14 PUSH1 20 (block3) // br v3 block3 block4; - 16 JUMPI - 17 PUSH1 23 (block4) - 19 JUMP + 10 GT + 11 PUSH1 17 (block3) // br v3 block3 block4; + 13 JUMPI + 14 PUSH1 20 (block4) + 16 JUMP block3: - 20 JUMPDEST - 21 SWAP1 // return v2; - 22 JUMP + 17 JUMPDEST + 18 SWAP1 // return v2; + 19 JUMP block4: - 23 JUMPDEST - 24 PUSH1 6 (block2) // jump block2; - 26 JUMP + 20 JUMPDEST + 21 PUSH1 3 (block2) // jump block2; + 23 JUMP -0x5b60016006565b600a81019081116014576017565b90565b600656 +0x5b60015b600a81019081116011576014565b90565b600356 diff --git a/crates/codegen/test_files/evm/spill.snap b/crates/codegen/test_files/evm/spill.snap index e346663c..74075d61 100644 --- a/crates/codegen/test_files/evm/spill.snap +++ b/crates/codegen/test_files/evm/spill.snap @@ -63,8 +63,6 @@ spill: DUP3 ADD DUP1 // jump block2; - PUSH1 block2 - JUMP block2: JUMPDEST PUSH1 `pc + (11)` // v16.i64 = call %sum8 v0 v1 v2 v3 v4 v5 v6 v7; @@ -196,52 +194,53 @@ spill: 50 DUP3 51 ADD 52 DUP1 // jump block2; - 53 PUSH1 56 (block2) - 55 JUMP block2: - 56 JUMPDEST - 57 PUSH1 69 // v16.i64 = call %sum8 v0 v1 v2 v3 v4 v5 v6 v7; + 53 JUMPDEST + 54 PUSH1 66 // v16.i64 = call %sum8 v0 v1 v2 v3 v4 v5 v6 v7; + 56 DUP16 + 57 DUP16 + 58 DUP16 59 DUP16 60 DUP16 61 DUP16 62 DUP16 63 DUP16 - 64 DUP16 - 65 DUP16 - 66 DUP16 - 67 PUSH0 0 (FuncRef(0)) - 68 JUMP - 69 JUMPDEST - 70 PUSH1 82 // v17.i64 = call %sum8 v8 v9 v10 v11 v12 v13 v14 v15; - 72 SWAP1 - 73 DUP3 - 74 DUP5 - 75 DUP7 - 76 DUP9 - 77 DUP11 - 78 DUP13 - 79 DUP15 - 80 PUSH0 0 (FuncRef(0)) - 81 JUMP - 82 JUMPDEST - 83 SWAP1 // v18.i64 = add v16 v17; - 84 ADD - 85 DUP1 // v19.i64 = add v7 v18; - 86 PUSH0 - 87 MLOAD - 88 MLOAD - 89 ADD - 90 PUSH2 0x3e8 (1000) // v20.i1 = gt v19 1000.i64; - 93 DUP2 - 94 GT - 95 PUSH1 101 (block3) // br v20 block3 block4; - 97 JUMPI - 98 PUSH1 121 (block4) - 100 JUMP + 64 PUSH0 0 (FuncRef(0)) + 65 JUMP + 66 JUMPDEST + 67 PUSH1 79 // v17.i64 = call %sum8 v8 v9 v10 v11 v12 v13 v14 v15; + 69 SWAP1 + 70 DUP3 + 71 DUP5 + 72 DUP7 + 73 DUP9 + 74 DUP11 + 75 DUP13 + 76 DUP15 + 77 PUSH0 0 (FuncRef(0)) + 78 JUMP + 79 JUMPDEST + 80 SWAP1 // v18.i64 = add v16 v17; + 81 ADD + 82 DUP1 // v19.i64 = add v7 v18; + 83 PUSH0 + 84 MLOAD + 85 MLOAD + 86 ADD + 87 PUSH2 0x3e8 (1000) // v20.i1 = gt v19 1000.i64; + 90 DUP2 + 91 GT + 92 PUSH1 98 (block3) // br v20 block3 block4; + 94 JUMPI + 95 PUSH1 118 (block4) + 97 JUMP block3: - 101 JUMPDEST - 102 POP // return v18; - 103 SWAP15 + 98 JUMPDEST + 99 POP // return v18; + 100 SWAP15 + 101 POP + 102 POP + 103 POP 104 POP 105 POP 106 POP @@ -254,17 +253,14 @@ spill: 113 POP 114 POP 115 POP - 116 POP - 117 POP - 118 POP - 119 SWAP1 - 120 JUMP + 116 SWAP1 + 117 JUMP block4: - 121 JUMPDEST - 122 SWAP1 // jump block2; - 123 POP - 124 PUSH1 56 (block2) - 126 JUMP + 118 JUMPDEST + 119 SWAP1 // jump block2; + 120 POP + 121 PUSH1 53 (block2) + 123 JUMP -0x5b01919001928201948401929001905090830190019150509056875f51525b8181018484018787018a8a01828401818301808201806038565b60458f8f8f8f8f8f8f8f5f565b605290828486888a8c8e5f565b9001805f5151016103e881116065576079565b509e50505050505050505050505050505090565b9050603856 +0x5b01919001928201948401929001905090830190019150509056875f51525b8181018484018787018a8a01828401818301808201805b60428f8f8f8f8f8f8f8f5f565b604f90828486888a8c8e5f565b9001805f5151016103e881116062576076565b509e50505050505050505050505050505090565b9050603556 From 15af0a13158867bdcf0ee238954102f058f85d6f Mon Sep 17 00:00:00 2001 From: Sean Billig Date: Sun, 1 Dec 2024 10:41:46 -0800 Subject: [PATCH 4/4] wip revm and fixes --- crates/codegen/src/stackalloc/local_stack.rs | 138 +++++++++++++------ crates/codegen/src/stackalloc/simple.rs | 9 +- crates/codegen/test_files/evm/add.snap | 133 ++++++++++++++---- crates/codegen/tests/evm.rs | 1 + crates/ir/src/inst/evm/mod.rs | 1 + 5 files changed, 210 insertions(+), 72 deletions(-) diff --git a/crates/codegen/src/stackalloc/local_stack.rs b/crates/codegen/src/stackalloc/local_stack.rs index cbc58339..4b4ee93c 100644 --- a/crates/codegen/src/stackalloc/local_stack.rs +++ b/crates/codegen/src/stackalloc/local_stack.rs @@ -9,10 +9,18 @@ pub type MemSlot = SmallVec<[ValueId; 4]>; const REACH: usize = 16; +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum StackEntry { + Live(ValueId), + // xxx Frozen(ValueId), + ReturnJumpTarget, + Junk, +} + // xxx rename #[derive(Clone, Default)] pub struct LocalStack { - stack: VecDeque, + stack: VecDeque, dead: BitSet, /// `usize` is reverse index (position relative to bottom of stack). @@ -26,7 +34,7 @@ impl LocalStack { /// is on top of stack). pub fn with_values(vals: &[ValueId]) -> Self { Self { - stack: VecDeque::from_iter(vals.iter().copied()), + stack: VecDeque::from_iter(vals.iter().copied().map(StackEntry::Live)), dead: BitSet::new(), frozen: SmallVec::default(), } @@ -40,7 +48,8 @@ impl LocalStack { /// Panics if slot >= stack.len(). pub fn rename_slot(&mut self, slot: usize, val: ValueId) { debug_assert!(!self.is_frozen(slot)); - self.stack[slot] = val; + debug_assert!(matches!(self.stack[slot], StackEntry::Live(_))); + self.stack[slot] = StackEntry::Live(val); } pub fn freeze(&mut self, mut values: BitSet) { @@ -51,8 +60,10 @@ impl LocalStack { // We only freeze the bottommost instance of each value. for (ridx, val) in self.stack.iter_mut().rev().enumerate() { - if values.remove(*val) { - self.frozen.push((ridx, *val)); + if let StackEntry::Live(val) = val { + if values.remove(*val) { + self.frozen.push((ridx, *val)); + } } } } @@ -69,20 +80,28 @@ impl LocalStack { .any(|(ridx, _)| *ridx == self.stack.len() - 1 - slot) } - fn top(&self) -> Option { + fn top(&self) -> Option { self.stack.front().copied() } - fn top_n(&self, n: usize) -> impl Iterator + '_ { + fn top_n(&self, n: usize) -> impl Iterator + '_ { self.stack.iter().take(n) } + fn top_n_is(&self, n: usize, iter: &mut dyn Iterator) -> bool { + self.stack + .iter() + .take(n) + .copied() + .eq(iter.map(|v| StackEntry::Live(*v))) + } + fn first_reachable_dead(&self) -> Option { self.position_within_reach(|v| self.dead.contains(v)) } fn push(&mut self, act: &mut Actions, val: ValueId, imm: Immediate) { - self.stack.push_front(val); + self.stack.push_front(StackEntry::Live(val)); act.push(Action::Push(imm)); } @@ -135,8 +154,12 @@ impl LocalStack { F: Fn(ValueId, &Self) -> bool, { while let Some(val) = self.stack.front() { - if pred(*val, self) { - self.pop(act) + if match val { + StackEntry::Live(val) => pred(*val, self), + StackEntry::Junk => true, + StackEntry::ReturnJumpTarget => false, + } { + self.pop(act); } else { break; } @@ -149,6 +172,14 @@ impl LocalStack { } } + // xxx rename/remove? + pub fn push_jump_target(&mut self) { + self.stack.push_front(StackEntry::ReturnJumpTarget); + } + pub fn pop_jump_target(&mut self) { + assert_eq!(self.stack.pop_front(), Some(StackEntry::ReturnJumpTarget)); + } + /// Ensure `out` is atop an otherwise empty stack. // xxx name pub fn ret(&mut self, act: &mut Actions, memory: &[MemSlot], return_val: Option) { let Some(return_val) = return_val else { @@ -157,7 +188,7 @@ impl LocalStack { }; while let Some(val) = self.stack.front() { - if return_val != *val { + if *val != StackEntry::Live(return_val) { self.pop(act); } else if self.stack.len() > 1 { // If there are vals below the return val, @@ -194,7 +225,11 @@ impl LocalStack { .stack .iter() .skip(1) - .take_while(|v| pred(**v, self)) + .take_while(|e| match e { + StackEntry::Live(v) => pred(*v, self), + StackEntry::ReturnJumpTarget => false, + StackEntry::Junk => true, + }) .count(); if swap_to > 0 { @@ -216,7 +251,12 @@ impl LocalStack { self.clean_top_of_stack(act, |val, zelf| zelf.dead.contains(val)); let consumable = last_use - || self.stack.iter().filter(|v| **v == arg).count() > 1 + || self + .stack + .iter() + .filter(|v| **v == StackEntry::Live(arg)) + .count() + > 1 || memory_slot_of(arg, memory).is_some() || dfg.value_is_imm(arg); @@ -229,7 +269,7 @@ impl LocalStack { // slot, while leaving a copy on the stack. let slot = allocate_slot(memory, arg, liveness); act.push(Action::MemLoadFrameSlot(slot as u32)); - self.stack.push_front(arg); + self.stack.push_front(StackEntry::Live(arg)); } else if !consumable { self.dup(act, pos); } else if !self.is_frozen(0) { @@ -244,7 +284,7 @@ impl LocalStack { } ValueLocation::Memory(pos) => { act.push(Action::MemLoadFrameSlot(pos as u32)); - self.stack.push_front(arg); + self.stack.push_front(StackEntry::Live(arg)); } ValueLocation::Nowhere => { panic!("{arg:?} not found on stack or in memory"); @@ -270,14 +310,15 @@ impl LocalStack { last_use.contains(*a) // We allow a val to be consumed if the stack contains // any additional copies, even if buried. - || self.stack.iter().filter(|v| *v == a).count() > 1 + || self.stack.iter().filter(|e| **e == StackEntry::Live(*a)).count() > 1 || memory_slot_of(*a, memory).is_some() || dfg.value_is_imm(*a) }) .collect(); // if the top val isn't used by this insn... - if let Some(top) = self.stack.front() { + if let Some(StackEntry::Live(top)) = self.stack.front() { + // xxx consider case where top entry is a jump target we just pushed on if !args.contains(top) { if_chain! { if let Some(last) = args.last(); @@ -298,9 +339,10 @@ impl LocalStack { } let all_consumable = args.iter().all(|a| consumable.contains(*a)); - if all_consumable && args.iter().eq(self.top_n(args.len())) { + if all_consumable && self.top_n_is(args.len(), &mut args.iter()) { // happy case: all vals are at top of stack, in order - } else if all_consumable && args.iter().rev().eq(self.top_n(args.len())) { + } else if all_consumable && self.top_n_is(args.len(), &mut args.iter().rev()) { + // xxx if args.len() == 2 and insn commutes, do nothing // all vals are at top of stack, but in reverse order for swap in reverse(args.len() as u8) { self.swap(act, swap as usize); @@ -332,7 +374,7 @@ impl LocalStack { // last arg is consumable; swap it up. self.swap(act, pos) } else if pos == 1 - && args.last().copied() == self.top() + && self.top() == args.last().map(|a| StackEntry::Live(*a)) && consumable.contains(val) { // stack is [prev_arg, this_arg, ..]; swap. @@ -344,14 +386,14 @@ impl LocalStack { // slot, while leaving a copy on the stack. let slot = allocate_slot(memory, val, liveness); act.push(Action::MemLoadFrameSlot(slot as u32)); - self.stack.push_front(val); + self.stack.push_front(StackEntry::Live(val)); } else { self.dup(act, pos); } } ValueLocation::Memory(pos) => { act.push(Action::MemLoadFrameSlot(pos as u32)); - self.stack.push_front(val); + self.stack.push_front(StackEntry::Live(val)); } ValueLocation::Nowhere => { panic!("{val:?} not found on stack or in memory"); @@ -380,8 +422,13 @@ impl LocalStack { } self.dead.union_with(last_use); + // xxx assert that the current insn is a call + if self.stack.front() == Some(&StackEntry::ReturnJumpTarget) { + self.stack.pop_front(); + } + if let Some(res) = result { - self.stack.push_front(res); + self.stack.push_front(StackEntry::Live(res)); } } @@ -389,11 +436,9 @@ impl LocalStack { /// leaving any `args` values at the top, and any /// `live_out` values at the bottom. /// Args must be moved to the top before calling `retain`. + /// xxx rework this. don't require args to be at top pub fn retain(&mut self, act: &mut Actions, args: &[ValueId], live_out: &BitSet) { - eprintln!("retain {args:?} in {:?}", &self); - debug_assert!(self.top_n(args.len()).eq(args.iter())); - - eprintln!("retain 1 {:?}", &self); + debug_assert!(self.top_n_is(args.len(), &mut args.iter())); self.clean_top_of_stack(act, |val, _| { !args.contains(&val) && !live_out.contains(val) @@ -422,8 +467,6 @@ impl LocalStack { // } // } - eprintln!("retain 3 {:?} {:?}", &self, act); - // xxx remove // if let Some(arg) = args.first() { // let pos = self @@ -433,11 +476,17 @@ impl LocalStack { // } } - fn position_within_reach(&self, predicate: F) -> Option + fn position_within_reach(&self, mut predicate: F) -> Option where F: FnMut(ValueId) -> bool, { - self.top_n(REACH).copied().position(predicate) + self.top_n(REACH).copied().position(|e| { + if let StackEntry::Live(v) = e { + predicate(v) + } else { + false + } + }) } fn shrink_stack( @@ -474,7 +523,7 @@ impl LocalStack { } fn location_of(&self, val: ValueId, memory: &[MemSlot], dfg: &DataFlowGraph) -> ValueLocation { - if let Some(pos) = self.stack.iter().position(|v| *v == val) { + if let Some(pos) = self.stack.iter().position(|e| *e == StackEntry::Live(val)) { // If item is buried, check if it's imm or in memory if pos >= REACH { if let Some(imm) = dfg.value_imm(val) { @@ -544,12 +593,18 @@ impl fmt::Debug for LocalStack { let mut pref = ""; for (idx, val) in self.stack.iter().enumerate() { write!(f, "{pref}")?; - if self.dead.contains(*val) { - write!(f, "dead({})", val.as_u32())?; - } else if self.is_frozen(idx) { - write!(f, "frozen({})", val.as_u32())?; - } else { - write!(f, "{}", val.as_u32())?; + match val { + StackEntry::Live(val) => { + if self.dead.contains(*val) { + write!(f, "dead({})", val.as_u32())?; + } else if self.is_frozen(idx) { + write!(f, "frozen({})", val.as_u32())?; + } else { + write!(f, "{}", val.as_u32())?; + } + } + StackEntry::ReturnJumpTarget => write!(f, "jump target")?, + StackEntry::Junk => write!(f, "junk!")?, } pref = ", "; } @@ -564,7 +619,7 @@ mod tests { use super::BitSet; use sonatina_ir::ValueId; - use crate::stackalloc::Actions; + use crate::stackalloc::{local_stack::StackEntry, Actions}; use super::{reverse, LocalStack}; @@ -603,6 +658,9 @@ mod tests { let mut act = Actions::default(); stack.clean_top_of_stack(&mut act, |val, _| !live.contains(val)); - assert_eq!(stack.stack, VecDeque::from([ValueId(2), ValueId(5)])); + assert_eq!( + stack.stack, + VecDeque::from([StackEntry::Live(ValueId(2)), StackEntry::Live(ValueId(5))]) + ); } } diff --git a/crates/codegen/src/stackalloc/simple.rs b/crates/codegen/src/stackalloc/simple.rs index d442bdea..bae814ab 100644 --- a/crates/codegen/src/stackalloc/simple.rs +++ b/crates/codegen/src/stackalloc/simple.rs @@ -194,10 +194,8 @@ impl<'a> SimpleAllocBuilder<'a> { self.liveness, ); } - eprintln!("JUMP {block:?}->{dest:?}, args: {args:?}, stack: {stack:?}, live_out: {live_out:?}"); // xxx allow phi vals to move into dead xfer slots stack.retain(act, &args, live_out); - eprintln!("after retain: {:?}", &stack); // xxx debug_assert inherited stacks are equal inherited_stack.insert(dest, stack); } @@ -221,10 +219,6 @@ impl<'a> SimpleAllocBuilder<'a> { &self.func.dfg, self.liveness, ); - eprintln!( - "BRANCH {block:?}->{nz_dest:?} | {z_dest:?}, {cond:?}, {:?}", - &stack - ); // "Consume" branch condition stack.force_pop(); @@ -240,9 +234,7 @@ impl<'a> SimpleAllocBuilder<'a> { // Remove all dead locals let mut live = BitSet::from(args.as_slice()); live.union_with(live_out); - eprintln!("BRTABLE {args:?}, {:?}", &stack); stack.retain(&mut self.alloc.actions[inst_id], &[], &live); - eprintln!("after retain: {:?}", &stack); let mut args_iter = args.iter(); let comp = args_iter.next().unwrap(); @@ -300,6 +292,7 @@ impl<'a> SimpleAllocBuilder<'a> { let act = &mut self.alloc.actions[inst_id]; if self.func.dfg.is_call(inst_id) { + stack.push_jump_target(); act.push(Action::PushContinuationOffset); } diff --git a/crates/codegen/test_files/evm/add.snap b/crates/codegen/test_files/evm/add.snap index ea85cc50..f9ae43ac 100644 --- a/crates/codegen/test_files/evm/add.snap +++ b/crates/codegen/test_files/evm/add.snap @@ -3,39 +3,124 @@ source: crates/codegen/tests/evm.rs expression: "String::from_utf8(v).unwrap()" input_file: test_files/evm/add.sntn --- -// func public %add() -> i32 -add: +// func public %entry() -> unit +entry: block0: JUMPDEST - PUSH1 0x9c (156) // v1.i32 = add 100000.i32 -100.i32; + PUSH0 // v0.i256 = evm_call_data_load 0.i32; + CALLDATALOAD + DUP1 // v1.i32 = shr 224.i32 v0; + PUSH1 0xe0 (224) + SHR + SWAP1 // v2.i32 = shl 32.i32 v0; + PUSH1 0x20 (32) + SHL + PUSH1 0xe0 (224) // v3.i32 = shr 224.i32 v2; + SHR + PUSH1 `pc + (5)` // v4.i32 = call %add v1 v3; + SWAP1 + DUP3 + PUSH1 FuncRef(1) + JUMP + JUMPDEST + SWAP1 // mstore 0.i32 v4 i32; + POP PUSH0 - SIGNEXTEND - PUSH3 0x186a0 (100000) - ADD - PUSH2 0x384 (900) // v2.i32 = sub v1 900.i32; SWAP1 - SUB - PUSH0 // mstore v2 0.i32 i32; + PUSH1 0xe0 (224) + SHL + SWAP1 MSTORE + PUSH1 0x4 (4) // evm_return 0.i8 4.i8; + PUSH0 + RETURN + +// func public %add(v0.i32, v1.i32) -> i32 +add: + block0: + JUMPDEST + ADD // v2.i32 = add v0 v1; + SWAP1 // return v2; + JUMP --------------- -// func public %add() -> i32 -add: +// func public %entry() -> unit +entry: block0: 0 JUMPDEST - 1 PUSH1 0x9c (156) // v1.i32 = add 100000.i32 -100.i32; - 3 PUSH0 - 4 SIGNEXTEND - 5 PUSH3 0x186a0 (100000) - 9 ADD - 10 PUSH2 0x384 (900) // v2.i32 = sub v1 900.i32; - 13 SWAP1 - 14 SUB - 15 PUSH0 // mstore v2 0.i32 i32; - 16 MSTORE - - -0x5b609c5f0b620186a00161038490035f52 + 1 PUSH0 // v0.i256 = evm_call_data_load 0.i32; + 2 CALLDATALOAD + 3 DUP1 // v1.i32 = shr 224.i32 v0; + 4 PUSH1 0xe0 (224) + 6 SHR + 7 SWAP1 // v2.i32 = shl 32.i32 v0; + 8 PUSH1 0x20 (32) + 10 SHL + 11 PUSH1 0xe0 (224) // v3.i32 = shr 224.i32 v2; + 13 SHR + 14 PUSH1 21 // v4.i32 = call %add v1 v3; + 16 SWAP1 + 17 DUP3 + 18 PUSH1 35 (FuncRef(1)) + 20 JUMP + 21 JUMPDEST + 22 SWAP1 // mstore 0.i32 v4 i32; + 23 POP + 24 PUSH0 + 25 SWAP1 + 26 PUSH1 0xe0 (224) + 28 SHL + 29 SWAP1 + 30 MSTORE + 31 PUSH1 0x4 (4) // evm_return 0.i8 4.i8; + 33 PUSH0 + 34 RETURN +// func public %add(v0.i32, v1.i32) -> i32 +add: + block0: + 35 JUMPDEST + 36 ADD // v2.i32 = add v0 v1; + 37 SWAP1 // return v2; + 38 JUMP + +0x5b5f358060e01c9060201b60e01c601590826023565b90505f9060e01b905260045ff35b019056 + + pc opcode input (stack grows to the right) + 0 5b JUMPDEST [] + 1 5f PUSH0 [] + 2 35 CALLDATALOAD [0] + 3 80 DUP1 [296559413476753275919844142761603304720577249802198117070348472025088] + 4 60 PUSH1 0xe0 + 6 1c SHR [296559413476753275919844142761603304720577249802198117070348472025088, 296559413476753275919844142761603304720577249802198117070348472025088, 224] + 7 90 SWAP1 [296559413476753275919844142761603304720577249802198117070348472025088, 11] + 8 60 PUSH1 0x20 + 10 1b SHL [11, 296559413476753275919844142761603304720577249802198117070348472025088, 32] + 11 60 PUSH1 0xe0 + 13 1c SHR [11, 593118826677314075482674331914431874820017177295892594584279425482752, 224] + 14 60 PUSH1 0x15 + 16 90 SWAP1 [11, 22, 21] + 17 82 DUP3 [11, 21, 22] + 18 60 PUSH1 0x23 + 20 56 JUMP [11, 21, 22, 11, 35] + 35 5b JUMPDEST [11, 21, 22, 11] + 36 01 ADD [11, 21, 22, 11] + 37 90 SWAP1 [11, 21, 33] + 38 56 JUMP [11, 33, 21] + 21 5b JUMPDEST [11, 33] + 22 90 SWAP1 [11, 33] + 23 50 POP [33, 11] + 24 5f PUSH0 [33] + 25 90 SWAP1 [33, 0] + 26 60 PUSH1 0xe0 + 28 1b SHL [0, 33, 224] + 29 90 SWAP1 [0, 889678240015971113224011497871647812230025765943838891876419138224128] + 30 52 MSTORE [889678240015971113224011497871647812230025765943838891876419138224128, 0] + 31 60 PUSH1 0x04 + 33 5f PUSH0 [4] + 34 f3 RETURN [4, 0] + + +Success { reason: Return, gas_used: 21152, gas_refunded: 0, logs: [], output: Call(0x00000021) } diff --git a/crates/codegen/tests/evm.rs b/crates/codegen/tests/evm.rs index 6c250843..1aa1dc57 100644 --- a/crates/codegen/tests/evm.rs +++ b/crates/codegen/tests/evm.rs @@ -94,6 +94,7 @@ fn test_evm(fixture: Fixture<&str>) { vcode.write(&mut v, &ctx).unwrap(); (vcode, block_order) }); + writeln!(v).unwrap(); (*fref, vcode, block_order) }) diff --git a/crates/ir/src/inst/evm/mod.rs b/crates/ir/src/inst/evm/mod.rs index d42af28e..6af7d657 100644 --- a/crates/ir/src/inst/evm/mod.rs +++ b/crates/ir/src/inst/evm/mod.rs @@ -115,6 +115,7 @@ pub struct EvmCallValue {} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Inst)] #[inst(side_effect(crate::inst::SideEffect::Read))] pub struct EvmCallDataLoad { + #[inst(value)] data_offset: ValueId, }