use alloy_primitives::U256;
use solar_data_structures::{fmt, index::IndexVec, newtype_index};
mod display;
mod parse;
mod passes;
mod verify;
pub use parse::{EvmIrParseError, parse_evm_ir_module};
pub use passes::{EVM_IR_PASSES, EvmIrPass};
pub use verify::{EvmIrVerifyError, verify_evm_ir_module};
newtype_index! {
pub struct EvmIrBlockId;
}
newtype_index! {
pub struct EvmIrValueId;
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EvmIrModule {
pub name: String,
pub blocks: IndexVec<EvmIrBlockId, EvmIrBlock>,
pub entry_block: Option<EvmIrBlockId>,
pub values: IndexVec<EvmIrValueId, EvmIrValue>,
}
impl EvmIrModule {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
assert!(is_valid_ident(&name), "invalid EVM IR program name `{name}`");
Self { name, blocks: IndexVec::new(), entry_block: None, values: IndexVec::new() }
}
pub fn add_block(&mut self, block: EvmIrBlock) -> EvmIrBlockId {
let id = self.blocks.push(block);
if self.entry_block.is_none() {
self.entry_block = Some(id);
}
id
}
pub fn add_value(&mut self, name: impl Into<String>) -> EvmIrValueId {
let name = name.into();
assert!(is_valid_value_name(&name), "invalid EVM IR value name `%{name}`");
self.values.push(EvmIrValue { name })
}
#[must_use]
pub fn block(&self, id: EvmIrBlockId) -> &EvmIrBlock {
&self.blocks[id]
}
pub fn block_mut(&mut self, id: EvmIrBlockId) -> &mut EvmIrBlock {
&mut self.blocks[id]
}
#[must_use]
pub fn value(&self, id: EvmIrValueId) -> &EvmIrValue {
&self.values[id]
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EvmIrBlock {
pub label: String,
pub metadata: EvmIrBlockMetadata,
pub instructions: Vec<EvmIrInstruction>,
pub terminator: Option<EvmIrTerminator>,
pub entry_stack: Vec<EvmIrValueId>,
}
impl EvmIrBlock {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
let label = label.into();
assert!(is_valid_block_label(&label), "invalid EVM IR block label `{label}`");
Self {
label,
metadata: EvmIrBlockMetadata::default(),
instructions: Vec::new(),
terminator: None,
entry_stack: Vec::new(),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct EvmIrBlockMetadata {
pub hotness: EvmIrBlockHotness,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum EvmIrBlockHotness {
#[default]
Hot,
Cold,
}
impl EvmIrBlockHotness {
fn parse(value: &str) -> Option<Self> {
Some(match value {
"hot" => Self::Hot,
"cold" => Self::Cold,
_ => return None,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EvmIrValue {
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EvmIrInstruction {
pub result: Option<EvmIrValueId>,
pub kind: EvmIrInstructionKind,
pub operands: Vec<EvmIrOperand>,
pub metadata: EvmIrMetadata,
}
impl EvmIrInstruction {
#[must_use]
pub fn new(mnemonic: impl Into<String>, operands: Vec<EvmIrOperand>) -> Self {
Self {
result: None,
kind: EvmIrInstructionKind::Operation(mnemonic.into()),
operands,
metadata: EvmIrMetadata::default(),
}
}
#[must_use]
pub fn stack_op(op: EvmIrStackOp) -> Self {
Self {
result: None,
kind: EvmIrInstructionKind::Stack(op),
operands: Vec::new(),
metadata: EvmIrMetadata::default(),
}
}
#[must_use]
pub fn mnemonic(&self) -> impl fmt::Display + '_ {
fmt::from_fn(move |f| match &self.kind {
EvmIrInstructionKind::Operation(mnemonic) => write!(f, "{mnemonic}"),
EvmIrInstructionKind::Stack(op) => write!(f, "{}", op.mnemonic()),
})
}
#[must_use]
pub const fn is_physical_stack_op(&self) -> bool {
matches!(self.kind, EvmIrInstructionKind::Stack(_))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum EvmIrInstructionKind {
Operation(String),
Stack(EvmIrStackOp),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EvmIrStackOp {
Dup(u8),
Swap(u8),
Pop,
}
impl EvmIrStackOp {
#[must_use]
pub const fn dup(n: u8) -> Option<Self> {
if n >= 1 && n <= 16 { Some(Self::Dup(n)) } else { None }
}
#[must_use]
pub const fn swap(n: u8) -> Option<Self> {
if n >= 1 && n <= 16 { Some(Self::Swap(n)) } else { None }
}
#[must_use]
pub const fn stack_effect(self) -> EvmIrStackEffect {
match self {
Self::Dup(_) => EvmIrStackEffect::new(0, 1),
Self::Swap(_) => EvmIrStackEffect::new(0, 0),
Self::Pop => EvmIrStackEffect::new(1, 0),
}
}
fn parse(mnemonic: &str) -> Option<Self> {
if mnemonic == "pop" {
return Some(Self::Pop);
}
if let Some(n) = mnemonic.strip_prefix("dup").and_then(|s| s.parse::<u8>().ok()) {
return Self::dup(n);
}
if let Some(n) = mnemonic.strip_prefix("swap").and_then(|s| s.parse::<u8>().ok()) {
return Self::swap(n);
}
None
}
fn mnemonic(self) -> impl fmt::Display {
fmt::from_fn(move |f| match self {
Self::Dup(n) => write!(f, "dup{n}"),
Self::Swap(n) => write!(f, "swap{n}"),
Self::Pop => write!(f, "pop"),
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EvmIrTerminator {
pub kind: EvmIrTerminatorKind,
pub metadata: EvmIrMetadata,
}
impl EvmIrTerminator {
#[must_use]
pub const fn new(kind: EvmIrTerminatorKind) -> Self {
Self { kind, metadata: EvmIrMetadata::EMPTY }
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EvmIrTerminatorKind {
Fallthrough(EvmIrBlockId),
Jump(EvmIrBlockId),
Branch {
condition: EvmIrOperand,
then_block: EvmIrBlockId,
else_block: EvmIrBlockId,
},
Switch {
value: EvmIrOperand,
default: EvmIrBlockId,
cases: Vec<(EvmIrOperand, EvmIrBlockId)>,
},
Return {
offset: EvmIrOperand,
size: EvmIrOperand,
},
Revert {
offset: EvmIrOperand,
size: EvmIrOperand,
},
Stop,
Invalid,
SelfDestruct {
recipient: EvmIrOperand,
},
RawOpcode(u8),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum EvmIrOperand {
Value(EvmIrValueId),
Immediate(U256),
Block(EvmIrBlockId),
Symbol(String),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EvmIrMetadata {
pub stack: Option<EvmIrStackEffect>,
pub attrs: Vec<EvmIrMetadataItem>,
}
impl EvmIrMetadata {
pub const EMPTY: Self = Self { stack: None, attrs: Vec::new() };
#[must_use]
pub fn is_empty(&self) -> bool {
self.stack.is_none() && self.attrs.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EvmIrMetadataItem {
pub key: String,
pub value: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct EvmIrStackEffect {
pub inputs: u16,
pub outputs: u16,
}
impl EvmIrStackEffect {
#[must_use]
pub const fn new(inputs: u16, outputs: u16) -> Self {
Self { inputs, outputs }
}
}
pub(super) fn default_instruction_stack_effect(inst: &EvmIrInstruction) -> EvmIrStackEffect {
match &inst.kind {
EvmIrInstructionKind::Stack(op) => op.stack_effect(),
EvmIrInstructionKind::Operation(_) if is_encoded_push_instruction(inst) => {
EvmIrStackEffect::new(0, 1)
}
EvmIrInstructionKind::Operation(_) => EvmIrStackEffect::new(
inst.operands.len().try_into().unwrap_or(u16::MAX),
u16::from(inst.result.is_some()),
),
}
}
fn default_terminator_stack_effect(kind: &EvmIrTerminatorKind) -> EvmIrStackEffect {
match kind {
EvmIrTerminatorKind::Branch { .. } => EvmIrStackEffect::new(1, 0),
EvmIrTerminatorKind::Switch { .. } => EvmIrStackEffect::new(1, 0),
EvmIrTerminatorKind::Return { .. } | EvmIrTerminatorKind::Revert { .. } => {
EvmIrStackEffect::new(2, 0)
}
EvmIrTerminatorKind::SelfDestruct { .. } => EvmIrStackEffect::new(1, 0),
EvmIrTerminatorKind::Fallthrough(_)
| EvmIrTerminatorKind::Jump(_)
| EvmIrTerminatorKind::Stop
| EvmIrTerminatorKind::Invalid
| EvmIrTerminatorKind::RawOpcode(_) => EvmIrStackEffect::new(0, 0),
}
}
pub(super) fn is_encoded_push_instruction(inst: &EvmIrInstruction) -> bool {
matches!(
&inst.kind,
EvmIrInstructionKind::Operation(mnemonic)
if matches!(mnemonic.as_str(), "push" | "push_deferred" | "push_immutable")
)
}
fn is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_' || c == '$' || c == '.'
}
fn is_ident_continue(c: char) -> bool {
is_ident_start(c) || c.is_ascii_digit()
}
fn is_valid_ident(name: &str) -> bool {
let mut chars = name.chars();
matches!(chars.next(), Some(c) if is_ident_start(c)) && chars.all(is_ident_continue)
}
fn is_valid_value_name(name: &str) -> bool {
let mut chars = name.chars();
matches!(chars.next(), Some(c) if is_ident_start(c) || c.is_ascii_digit())
&& chars.all(is_ident_continue)
}
fn is_valid_block_label(label: &str) -> bool {
let Some(digits) = label.strip_prefix("bb") else {
return false;
};
!digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())
}