use crate::soliditylite::asm::{op, Asm, Label};
use crate::soliditylite::CompiledArtifact;
#[cfg(feature = "wallet")]
use crate::soliditylite::ast::{CmpOp, Expr, Facet, StateVarKind, Stmt};
#[cfg(feature = "wallet")]
use crate::rustlite::CompileError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BodyValue {
Const([u8; 32]),
StorageSlot([u8; 32]),
}
#[cfg(feature = "wallet")]
enum Body {
View(BodyValue),
ViewExpr(LoweredExpr),
Mutating(Vec<LoweredStmt>),
ConstString(Vec<u8>),
DynamicStorageReturn { slot: [u8; 32] },
EchoParam { param_index: u64 },
}
#[cfg(feature = "wallet")]
enum LoweredStmt {
Require(LoweredExpr),
Assign(LoweredAssign),
Emit(LoweredEmit),
If {
cond: LoweredExpr,
then_body: Vec<LoweredStmt>,
else_body: Vec<LoweredStmt>,
},
}
#[cfg(feature = "wallet")]
struct LoweredEmit {
topic0: [u8; 32],
indexed: Vec<LoweredExpr>,
data: Vec<LoweredExpr>,
}
#[cfg(feature = "wallet")]
enum LoweredExpr {
Const([u8; 32]),
Load([u8; 32]),
Param(u64),
Caller,
Timestamp,
Number,
MapLoad { base_slot: [u8; 32], key: Box<LoweredExpr> },
ArrayLen { slot: [u8; 32] },
ArrayLoad { slot: [u8; 32], index: Box<LoweredExpr> },
Add(Box<LoweredExpr>, Box<LoweredExpr>),
Sub(Box<LoweredExpr>, Box<LoweredExpr>),
Mul(Box<LoweredExpr>, Box<LoweredExpr>),
Div(Box<LoweredExpr>, Box<LoweredExpr>),
Mod(Box<LoweredExpr>, Box<LoweredExpr>),
Cmp { op: CmpOp, lhs: Box<LoweredExpr>, rhs: Box<LoweredExpr> },
}
#[cfg(feature = "wallet")]
fn emit_map_slot(a: &mut Asm, base_slot: &[u8; 32], key: &LoweredExpr) {
key.emit(a);
a.push_u64(0x00).emit(op::MSTORE);
a.push32(base_slot).push_u64(0x20).emit(op::MSTORE);
a.push_u64(0x40).push_u64(0x00).emit(op::KECCAK256);
}
#[cfg(feature = "wallet")]
fn emit_array_slot(a: &mut Asm, slot: &[u8; 32], index: &LoweredExpr) {
a.push32(slot).push_u64(0x00).emit(op::MSTORE);
a.push_u64(0x20).push_u64(0x00).emit(op::KECCAK256);
index.emit(a);
a.emit(op::ADD);
}
#[cfg(feature = "wallet")]
impl LoweredExpr {
fn emit(&self, a: &mut Asm) {
match self {
LoweredExpr::Const(word) => {
a.push(word); }
LoweredExpr::Load(slot) => {
a.push32(slot).emit(op::SLOAD);
}
LoweredExpr::Param(index) => {
a.push_u64(4 + 32 * index).emit(op::CALLDATALOAD);
}
LoweredExpr::Caller => {
a.emit(op::CALLER);
}
LoweredExpr::Timestamp => {
a.emit(op::TIMESTAMP);
}
LoweredExpr::Number => {
a.emit(op::NUMBER);
}
LoweredExpr::MapLoad { base_slot, key } => {
emit_map_slot(a, base_slot, key);
a.emit(op::SLOAD);
}
LoweredExpr::ArrayLen { slot } => {
a.push32(slot).emit(op::SLOAD); }
LoweredExpr::ArrayLoad { slot, index } => {
emit_array_slot(a, slot, index);
a.emit(op::SLOAD);
}
LoweredExpr::Add(lhs, rhs) => {
lhs.emit(a);
rhs.emit(a);
a.emit(op::ADD);
}
LoweredExpr::Sub(lhs, rhs) => {
rhs.emit(a);
lhs.emit(a);
a.emit(op::SUB);
}
LoweredExpr::Mul(lhs, rhs) => {
lhs.emit(a);
rhs.emit(a);
a.emit(op::MUL); }
LoweredExpr::Div(lhs, rhs) => {
rhs.emit(a);
lhs.emit(a);
a.emit(op::DIV);
}
LoweredExpr::Mod(lhs, rhs) => {
rhs.emit(a);
lhs.emit(a);
a.emit(op::MOD);
}
LoweredExpr::Cmp { op: cmp, lhs, rhs } => {
rhs.emit(a);
lhs.emit(a);
match cmp {
CmpOp::Gt => {
a.emit(op::GT);
}
CmpOp::Lt => {
a.emit(op::LT);
}
CmpOp::Eq => {
a.emit(op::EQ);
}
CmpOp::Neq => {
a.emit(op::EQ).emit(op::ISZERO);
}
CmpOp::Le => {
a.emit(op::GT).emit(op::ISZERO);
}
CmpOp::Ge => {
a.emit(op::LT).emit(op::ISZERO);
}
}
}
}
}
}
#[cfg(feature = "wallet")]
enum LoweredAssign {
Scalar { slot: [u8; 32], value: LoweredExpr },
MapEntry { base_slot: [u8; 32], key: LoweredExpr, value: LoweredExpr },
ArrayElem { slot: [u8; 32], index: LoweredExpr, value: LoweredExpr },
ArrayPush { slot: [u8; 32], value: LoweredExpr },
ArrayPop { slot: [u8; 32] },
ArrayDelete { slot: [u8; 32], index: LoweredExpr },
ConstDynamicBytes { base_slot: [u8; 32], bytes: Vec<u8> },
}
#[cfg(feature = "wallet")]
impl LoweredAssign {
fn emit(&self, a: &mut Asm) {
match self {
LoweredAssign::Scalar { slot, value } => {
value.emit(a);
a.push32(slot).emit(op::SSTORE);
}
LoweredAssign::MapEntry { base_slot, key, value } => {
value.emit(a);
emit_map_slot(a, base_slot, key);
a.emit(op::SSTORE);
}
LoweredAssign::ArrayElem { slot, index, value } => {
value.emit(a);
emit_array_slot(a, slot, index);
a.emit(op::SSTORE);
}
LoweredAssign::ArrayPush { slot, value } => {
value.emit(a); emit_array_slot(a, slot, &LoweredExpr::ArrayLen { slot: *slot });
a.emit(op::SSTORE);
let mut one = [0u8; 32];
one[31] = 1;
a.push32(slot).emit(op::SLOAD).push(&one).emit(op::ADD);
a.push32(slot).emit(op::SSTORE);
}
LoweredAssign::ArrayPop { slot } => {
let new_len =
LoweredExpr::Sub(Box::new(LoweredExpr::ArrayLen { slot: *slot }), Box::new(one_word()));
LoweredExpr::Const([0u8; 32]).emit(a); emit_array_slot(a, slot, &new_len);
a.emit(op::SSTORE);
new_len.emit(a);
a.push32(slot).emit(op::SSTORE);
}
LoweredAssign::ArrayDelete { slot, index } => {
LoweredExpr::Const([0u8; 32]).emit(a); emit_array_slot(a, slot, index);
a.emit(op::SSTORE);
}
LoweredAssign::ConstDynamicBytes { base_slot, bytes } => {
emit_const_dynamic_bytes_store(a, base_slot, bytes);
}
}
}
}
#[cfg(feature = "wallet")]
fn emit_const_dynamic_bytes_store(a: &mut Asm, base_slot: &[u8; 32], bytes: &[u8]) {
let len = bytes.len();
if len <= 31 {
let mut word = [0u8; 32];
word[..len].copy_from_slice(bytes);
word[31] = (len as u8) * 2;
a.push32(&word).push32(base_slot).emit(op::SSTORE);
} else {
let mut header = [0u8; 32];
let marker = (len as u128) * 2 + 1;
header[16..].copy_from_slice(&marker.to_be_bytes());
a.push32(&header).push32(base_slot).emit(op::SSTORE);
let data_slot0 = dynamic_data_slot0(base_slot);
for (i, chunk) in bytes.chunks(32).enumerate() {
let mut word = [0u8; 32];
word[..chunk.len()].copy_from_slice(chunk); let slot = slot_at(data_slot0, i as u64);
a.push32(&word).push32(&slot).emit(op::SSTORE);
}
}
}
#[cfg(feature = "wallet")]
fn dynamic_data_slot0(slot: &[u8; 32]) -> [u8; 32] {
use sha3::{Digest, Keccak256};
Keccak256::digest(slot).into()
}
#[cfg(feature = "wallet")]
fn one_word() -> LoweredExpr {
let mut one = [0u8; 32];
one[31] = 1;
LoweredExpr::Const(one)
}
#[cfg(feature = "wallet")]
const LOG_DATA_BASE: u64 = 0x40;
#[cfg(feature = "wallet")]
impl LoweredEmit {
fn emit(&self, a: &mut Asm) {
for (i, word) in self.data.iter().enumerate() {
word.emit(a); a.push_u64(LOG_DATA_BASE + 0x20 * i as u64).emit(op::MSTORE);
}
for indexed in self.indexed.iter().rev() {
indexed.emit(a);
}
a.push32(&self.topic0);
let length = 0x20u64 * self.data.len() as u64;
a.push_u64(length).push_u64(LOG_DATA_BASE);
let n = 1 + self.indexed.len(); a.emit(log_op(n));
}
}
#[cfg(feature = "wallet")]
fn log_op(n: usize) -> u8 {
match n {
0 => op::LOG0,
1 => op::LOG1,
2 => op::LOG2,
3 => op::LOG3,
4 => op::LOG4,
_ => op::LOG4,
}
}
struct LoweredFn {
selector: [u8; 4],
value: BodyValue,
body_label: Label,
}
pub fn emit_dispatch_prelude(a: &mut Asm, fb: Label) {
a.push_u64(0x04)
.emit(op::CALLDATASIZE)
.emit(op::LT)
.push_label(fb)
.emit(op::JUMPI);
a.push_u64(0x00)
.emit(op::CALLDATALOAD)
.push_u64(0xE0)
.emit(op::SHR);
}
pub fn emit_dispatch_arm(a: &mut Asm, selector: [u8; 4], body: Label) {
a.emit(op::DUP1)
.push(&selector) .emit(op::EQ)
.push_label(body)
.emit(op::JUMPI);
}
pub fn emit_fallback(a: &mut Asm, fb: Label) {
a.jumpdest(fb).push_u64(0x00).push_u64(0x00).emit(op::REVERT);
}
pub fn emit_body(a: &mut Asm, body: Label, value: BodyValue) {
a.jumpdest(body);
match value {
BodyValue::Const(word) => {
a.push32(&word); }
BodyValue::StorageSlot(slot) => {
a.push32(&slot).emit(op::SLOAD); }
}
a.push_u64(0x00)
.emit(op::MSTORE)
.push_u64(0x20)
.push_u64(0x00)
.emit(op::RETURN);
}
#[cfg(feature = "wallet")]
fn emit_full_body(a: &mut Asm, body: Label, b: &Body) {
match b {
Body::View(value) => emit_body(a, body, *value),
Body::ViewExpr(expr) => {
a.jumpdest(body);
expr.emit(a);
a.push_u64(0x00)
.emit(op::MSTORE)
.push_u64(0x20)
.push_u64(0x00)
.emit(op::RETURN);
}
Body::ConstString(bytes) => {
a.jumpdest(body);
a.push_u64(0x20).push_u64(0x00).emit(op::MSTORE);
a.push_u64(bytes.len() as u64).push_u64(0x20).emit(op::MSTORE);
let mut off = 0x40u64;
for chunk in bytes.chunks(32) {
let mut word = [0u8; 32];
word[..chunk.len()].copy_from_slice(chunk); a.push32(&word).push_u64(off).emit(op::MSTORE);
off += 32;
}
let padded = bytes.len().div_ceil(32) * 32;
a.push_u64(0x40 + padded as u64).push_u64(0x00).emit(op::RETURN);
}
Body::DynamicStorageReturn { slot } => {
a.jumpdest(body);
emit_dynamic_storage_return(a, slot);
}
Body::EchoParam { param_index } => {
a.jumpdest(body);
emit_echo_param(a, *param_index);
}
Body::Mutating(stmts) => {
a.jumpdest(body);
let has_require = stmts.iter().any(stmt_has_require);
let revert = if has_require { Some(a.new_label()) } else { None };
emit_stmts(a, stmts, revert);
a.push_u64(0x00).push_u64(0x00).emit(op::RETURN);
if let Some(revert) = revert {
a.jumpdest(revert).push_u64(0x00).push_u64(0x00).emit(op::REVERT);
}
}
}
}
#[cfg(feature = "wallet")]
fn emit_param_tail_abs(a: &mut Asm, param_index: u64) {
a.push_u64(4 + 32 * param_index)
.emit(op::CALLDATALOAD) .push_u64(0x04)
.emit(op::ADD); }
#[cfg(feature = "wallet")]
fn emit_len_to_tail_copy_len(a: &mut Asm) {
a.push_u64(0x1f).emit(op::ADD); a.push_u64(0x05).emit(op::SHR); a.push_u64(0x20).emit(op::MUL); a.push_u64(0x20).emit(op::ADD); }
#[cfg(feature = "wallet")]
fn emit_echo_param(a: &mut Asm, param_index: u64) {
a.push_u64(0x20).push_u64(0x00).emit(op::MSTORE);
emit_param_tail_abs(a, param_index); a.emit(op::CALLDATALOAD); emit_len_to_tail_copy_len(a); emit_param_tail_abs(a, param_index); a.push_u64(0x20); a.emit(op::CALLDATACOPY); emit_param_tail_abs(a, param_index);
a.emit(op::CALLDATALOAD); emit_len_to_tail_copy_len(a); a.push_u64(0x20).emit(op::ADD); a.push_u64(0x00).emit(op::RETURN); }
#[cfg(feature = "wallet")]
fn emit_dynamic_storage_return(a: &mut Asm, slot: &[u8; 32]) {
let long_lbl = a.new_label();
a.push32(slot).emit(op::SLOAD); a.emit(op::DUP1).push_u64(0x01).emit(op::AND); a.push_label(long_lbl).emit(op::JUMPI);
a.push_u64(0x20).push_u64(0x00).emit(op::MSTORE); a.emit(op::DUP1).push_u64(0xff).emit(op::AND); a.push_u64(0x01).emit(op::SHR); a.push_u64(0x20).emit(op::MSTORE); a.push_u64(0x40).emit(op::MSTORE); a.push_u64(0x60).push_u64(0x00).emit(op::RETURN);
a.jumpdest(long_lbl);
a.push_u64(0x01).emit(op::SWAP1).emit(op::SUB); a.push_u64(0x01).emit(op::SHR); a.emit(op::DUP1).push_u64(0x20).emit(op::MSTORE); a.push_u64(0x1f).emit(op::ADD).push_u64(0x05).emit(op::SHR); a.push32(slot).push_u64(0x00).emit(op::MSTORE); a.push_u64(0x20).push_u64(0x00).emit(op::KECCAK256); a.push_u64(0x20).push_u64(0x00).emit(op::MSTORE); a.emit(op::SWAP1); a.push_u64(0x00);
let loop_head = a.new_label();
let loop_end = a.new_label();
a.jumpdest(loop_head);
a.emit(op::DUP1); a.emit(op::DUP3); a.emit(op::GT).emit(op::ISZERO); a.push_label(loop_end).emit(op::JUMPI); a.emit(op::DUP3); a.emit(op::DUP2); a.emit(op::ADD).emit(op::SLOAD); a.emit(op::DUP2); a.push_u64(0x20).emit(op::MUL); a.push_u64(0x40).emit(op::ADD); a.emit(op::MSTORE); a.push_u64(0x01).emit(op::ADD); a.push_label(loop_head).emit(op::JUMP);
a.jumpdest(loop_end);
a.emit(op::POP); a.push_u64(0x20).emit(op::MUL); a.push_u64(0x40).emit(op::ADD); a.push_u64(0x00); a.emit(op::RETURN); }
#[cfg(feature = "wallet")]
fn stmt_has_require(s: &LoweredStmt) -> bool {
match s {
LoweredStmt::Require(_) => true,
LoweredStmt::If { then_body, else_body, .. } => {
then_body.iter().any(stmt_has_require) || else_body.iter().any(stmt_has_require)
}
_ => false,
}
}
#[cfg(feature = "wallet")]
fn emit_stmts(a: &mut Asm, stmts: &[LoweredStmt], revert: Option<Label>) {
for stmt in stmts {
match stmt {
LoweredStmt::Require(cond) => {
cond.emit(a);
a.emit(op::ISZERO)
.push_label(revert.expect("revert label allocated when a require is present"))
.emit(op::JUMPI);
}
LoweredStmt::Assign(assign) => assign.emit(a),
LoweredStmt::Emit(ev) => ev.emit(a),
LoweredStmt::If { cond, then_body, else_body } => {
cond.emit(a);
a.emit(op::ISZERO);
if else_body.is_empty() {
let end = a.new_label();
a.push_label(end).emit(op::JUMPI);
emit_stmts(a, then_body, revert);
a.jumpdest(end);
} else {
let else_lbl = a.new_label();
let end = a.new_label();
a.push_label(else_lbl).emit(op::JUMPI);
emit_stmts(a, then_body, revert);
a.push_label(end).emit(op::JUMP);
a.jumpdest(else_lbl);
emit_stmts(a, else_body, revert);
a.jumpdest(end);
}
}
}
}
}
#[cfg(feature = "wallet")]
struct LoweredFnFull {
selector: [u8; 4],
body: Body,
body_label: Label,
}
#[cfg(feature = "wallet")]
fn assemble_full(functions: Vec<([u8; 4], Body)>) -> CompiledArtifact {
let mut a = Asm::new();
let fb = a.new_label();
let lowered: Vec<LoweredFnFull> = functions
.into_iter()
.map(|(selector, body)| LoweredFnFull { selector, body, body_label: a.new_label() })
.collect();
emit_dispatch_prelude(&mut a, fb);
for lf in &lowered {
emit_dispatch_arm(&mut a, lf.selector, lf.body_label);
}
emit_fallback(&mut a, fb);
for lf in &lowered {
emit_full_body(&mut a, lf.body_label, &lf.body);
}
let selectors = lowered.iter().map(|lf| lf.selector).collect();
let runtime = a.finish();
let init_code = crate::soliditylite::asm::init_wrapper(&runtime);
CompiledArtifact { init_code, runtime, selectors }
}
pub fn assemble(functions: &[([u8; 4], BodyValue)]) -> CompiledArtifact {
let mut a = Asm::new();
let fb = a.new_label();
let lowered: Vec<LoweredFn> = functions
.iter()
.map(|(selector, value)| LoweredFn { selector: *selector, value: *value, body_label: a.new_label() })
.collect();
emit_dispatch_prelude(&mut a, fb);
for lf in &lowered {
emit_dispatch_arm(&mut a, lf.selector, lf.body_label);
}
emit_fallback(&mut a, fb);
for lf in &lowered {
emit_body(&mut a, lf.body_label, lf.value);
}
let selectors = lowered.iter().map(|lf| lf.selector).collect();
let runtime = a.finish();
let init_code = crate::soliditylite::asm::init_wrapper(&runtime);
CompiledArtifact { init_code, runtime, selectors }
}
#[cfg(feature = "wallet")]
fn storage_base(facet_name: &str) -> [u8; 32] {
use sha3::{Digest, Keccak256};
let preimage = format!("localharness.{}.storage.v1", facet_name.to_ascii_lowercase());
let mut h = Keccak256::new();
h.update(preimage.as_bytes());
let digest = h.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
out
}
#[cfg(feature = "wallet")]
fn slot_at(base: [u8; 32], index: u64) -> [u8; 32] {
let mut out = base;
let mut carry = index as u128;
for byte in out.iter_mut().rev() {
if carry == 0 {
break;
}
let v = *byte as u128 + (carry & 0xFF);
*byte = (v & 0xFF) as u8;
carry = (carry >> 8) + (v >> 8);
}
out
}
#[cfg(feature = "wallet")]
struct Resolver<'a> {
facet: &'a Facet,
base: [u8; 32],
func: &'a crate::soliditylite::ast::Function,
}
#[cfg(feature = "wallet")]
impl Resolver<'_> {
fn state_var_index(&self, name: &str) -> Option<usize> {
self.facet.state_vars.iter().position(|sv| sv.name == name)
}
fn param_index(&self, name: &str) -> Option<usize> {
self.func.params.iter().position(|p| p.name == name)
}
fn scalar_slot(&self, name: &str, span: crate::rustlite::Span) -> Result<[u8; 32], CompileError> {
use crate::error_codes as codes;
let idx = self.state_var_index(name).ok_or_else(|| {
CompileError::at_code(
codes::UNDEFINED_VARIABLE,
format!("unknown state variable `{name}`"),
span,
)
})?;
match self.facet.state_vars[idx].kind {
StateVarKind::Mapping { .. } => {
return Err(CompileError::at_code(
codes::TYPE_MISMATCH,
format!("`{name}` is a mapping; it must be indexed (`{name}[key]`)"),
span,
))
}
StateVarKind::Array { .. } => {
return Err(CompileError::at_code(
codes::TYPE_MISMATCH,
format!("`{name}` is an array; index it (`{name}[i]`) or read `{name}.length`"),
span,
))
}
StateVarKind::DynamicBytes { .. } => {
return Err(CompileError::at_code(
codes::TYPE_MISMATCH,
format!("`{name}` is a dynamic `string`/`bytes`; it is not a single-word scalar"),
span,
))
}
StateVarKind::Scalar(_) => {}
}
Ok(slot_at(self.base, idx as u64))
}
fn mapping_base_slot(&self, name: &str, span: crate::rustlite::Span) -> Result<[u8; 32], CompileError> {
use crate::error_codes as codes;
let idx = self.state_var_index(name).ok_or_else(|| {
CompileError::at_code(
codes::UNDEFINED_VARIABLE,
format!("unknown state variable `{name}`"),
span,
)
})?;
match self.facet.state_vars[idx].kind {
StateVarKind::Mapping { .. } => Ok(slot_at(self.base, idx as u64)),
StateVarKind::Scalar(_) => Err(CompileError::at_code(
codes::TYPE_MISMATCH,
format!("`{name}` is not a mapping; it cannot be indexed"),
span,
)),
StateVarKind::Array { .. } => Err(CompileError::at_code(
codes::TYPE_MISMATCH,
format!("`{name}` is an array, not a mapping (indexing is array-shaped, handled separately)"),
span,
)),
StateVarKind::DynamicBytes { .. } => Err(CompileError::at_code(
codes::TYPE_MISMATCH,
format!("`{name}` is a dynamic `string`/`bytes`, not a mapping"),
span,
)),
}
}
fn array_base_slot(&self, name: &str, span: crate::rustlite::Span) -> Result<[u8; 32], CompileError> {
use crate::error_codes as codes;
let idx = self.state_var_index(name).ok_or_else(|| {
CompileError::at_code(
codes::UNDEFINED_VARIABLE,
format!("unknown state variable `{name}`"),
span,
)
})?;
match self.facet.state_vars[idx].kind {
StateVarKind::Array { .. } => Ok(slot_at(self.base, idx as u64)),
_ => Err(CompileError::at_code(
codes::TYPE_MISMATCH,
format!("`{name}` is not a dynamic array"),
span,
)),
}
}
fn is_array(&self, name: &str) -> bool {
self.state_var_index(name)
.map(|idx| matches!(self.facet.state_vars[idx].kind, StateVarKind::Array { .. }))
.unwrap_or(false)
}
fn is_dynamic_bytes_var(&self, name: &str) -> bool {
self.state_var_index(name)
.map(|idx| matches!(self.facet.state_vars[idx].kind, StateVarKind::DynamicBytes { .. }))
.unwrap_or(false)
}
fn dynamic_bytes_slot(&self, name: &str, span: crate::rustlite::Span) -> Result<[u8; 32], CompileError> {
use crate::error_codes as codes;
let idx = self.state_var_index(name).ok_or_else(|| {
CompileError::at_code(codes::UNDEFINED_VARIABLE, format!("unknown state variable `{name}`"), span)
})?;
match self.facet.state_vars[idx].kind {
StateVarKind::DynamicBytes { .. } => Ok(slot_at(self.base, idx as u64)),
_ => Err(CompileError::at_code(
codes::TYPE_MISMATCH,
format!("`{name}` is not a `string`/`bytes` state variable"),
span,
)),
}
}
fn dynamic_param_index(&self, name: &str) -> Option<u64> {
self.func
.params
.iter()
.position(|p| p.name == name && p.ty.is_dynamic())
.map(|i| i as u64)
}
fn lower_expr(&self, expr: &Expr) -> Result<LoweredExpr, CompileError> {
use crate::error_codes as codes;
match expr {
Expr::IntLit { value_be32, .. } => Ok(LoweredExpr::Const(*value_be32)),
Expr::StateVar { name, span } => {
if self.state_var_index(name).is_some() {
Ok(LoweredExpr::Load(self.scalar_slot(name, *span)?))
} else if let Some(p) = self.param_index(name) {
if self.func.params[p].ty.is_dynamic() {
return Err(CompileError::at_code(
codes::TYPE_MISMATCH,
format!(
"`{name}` is a dynamic `string`/`bytes` parameter; it is only \
supported as a whole `return {name};`, not inside an expression"
),
*span,
));
}
Ok(LoweredExpr::Param(p as u64))
} else {
Err(CompileError::at_code(
codes::UNDEFINED_VARIABLE,
format!("unknown variable `{name}`"),
*span,
))
}
}
Expr::MsgSender { .. } => Ok(LoweredExpr::Caller),
Expr::BlockTimestamp { .. } => Ok(LoweredExpr::Timestamp),
Expr::BlockNumber { .. } => Ok(LoweredExpr::Number),
Expr::Index { base, key, span } if self.is_array(base) => Ok(LoweredExpr::ArrayLoad {
slot: self.array_base_slot(base, *span)?,
index: Box::new(self.lower_expr(key)?),
}),
Expr::Index { base, key, span } => Ok(LoweredExpr::MapLoad {
base_slot: self.mapping_base_slot(base, *span)?,
key: Box::new(self.lower_expr(key)?),
}),
Expr::ArrayLen { base, span } => Ok(LoweredExpr::ArrayLen {
slot: self.array_base_slot(base, *span)?,
}),
Expr::Add { lhs, rhs, .. } => Ok(LoweredExpr::Add(
Box::new(self.lower_expr(lhs)?),
Box::new(self.lower_expr(rhs)?),
)),
Expr::Sub { lhs, rhs, .. } => Ok(LoweredExpr::Sub(
Box::new(self.lower_expr(lhs)?),
Box::new(self.lower_expr(rhs)?),
)),
Expr::Mul { lhs, rhs, .. } => Ok(LoweredExpr::Mul(
Box::new(self.lower_expr(lhs)?),
Box::new(self.lower_expr(rhs)?),
)),
Expr::Div { lhs, rhs, .. } => Ok(LoweredExpr::Div(
Box::new(self.lower_expr(lhs)?),
Box::new(self.lower_expr(rhs)?),
)),
Expr::Mod { lhs, rhs, .. } => Ok(LoweredExpr::Mod(
Box::new(self.lower_expr(lhs)?),
Box::new(self.lower_expr(rhs)?),
)),
Expr::Cmp { op, lhs, rhs, .. } => Ok(LoweredExpr::Cmp {
op: *op,
lhs: Box::new(self.lower_expr(lhs)?),
rhs: Box::new(self.lower_expr(rhs)?),
}),
Expr::StrLit { span, .. } => Err(CompileError::at_code(
crate::error_codes::UNSUPPORTED_FEATURE,
"a string literal is only supported as a whole `return` value in v1 (not in an \
assignment, comparison, arithmetic, or event argument)"
.to_string(),
*span,
)),
}
}
}
#[cfg(feature = "wallet")]
fn function_signature(func: &crate::soliditylite::ast::Function) -> String {
let types: Vec<&str> = func.params.iter().map(|p| p.ty.abi_name()).collect();
format!("{}({})", func.name, types.join(","))
}
#[cfg(feature = "wallet")]
fn event_signature(ev: &crate::soliditylite::ast::EventDecl) -> String {
let types: Vec<&str> = ev.args.iter().map(|arg| arg.ty.abi_name()).collect();
format!("{}({})", ev.name, types.join(","))
}
#[cfg(feature = "wallet")]
pub fn event_topic0(signature: &str) -> [u8; 32] {
use sha3::{Digest, Keccak256};
let mut h = Keccak256::new();
h.update(signature.as_bytes());
let mut out = [0u8; 32];
out.copy_from_slice(&h.finalize());
out
}
#[cfg(feature = "wallet")]
fn lower_emit(
facet: &Facet,
r: &Resolver,
ev_name: &str,
args: &[Expr],
span: crate::rustlite::Span,
) -> Result<LoweredEmit, CompileError> {
use crate::error_codes as codes;
let decl = facet.events.iter().find(|e| e.name == ev_name).ok_or_else(|| {
CompileError::at_code(
codes::UNKNOWN_FUNCTION,
format!("unknown event `{ev_name}` (no matching `event` declaration)"),
span,
)
})?;
if args.len() != decl.args.len() {
return Err(CompileError::at_code(
codes::ARITY_MISMATCH,
format!(
"event `{ev_name}` expects {} argument(s), got {}",
decl.args.len(),
args.len()
),
span,
));
}
let num_indexed = decl.args.iter().filter(|a| a.indexed).count();
if num_indexed > 3 {
return Err(CompileError::at_code(
codes::UNSUPPORTED_FEATURE,
format!("event `{ev_name}` has {num_indexed} indexed args; at most 3 are allowed (LOG topic cap)"),
span,
));
}
let topic0 = event_topic0(&event_signature(decl));
let mut indexed = Vec::with_capacity(num_indexed);
let mut data = Vec::with_capacity(decl.args.len() - num_indexed);
for (arg_decl, arg_expr) in decl.args.iter().zip(args) {
let lowered = r.lower_expr(arg_expr)?;
if arg_decl.indexed {
indexed.push(lowered);
} else {
data.push(lowered);
}
}
Ok(LoweredEmit { topic0, indexed, data })
}
#[cfg(feature = "wallet")]
fn lower_stmts(facet: &Facet, r: &Resolver, stmts: &[Stmt]) -> Result<Vec<LoweredStmt>, CompileError> {
stmts.iter().map(|s| lower_stmt(facet, r, s)).collect()
}
#[cfg(feature = "wallet")]
fn lower_stmt(facet: &Facet, r: &Resolver, stmt: &Stmt) -> Result<LoweredStmt, CompileError> {
use crate::error_codes as codes;
Ok(match stmt {
Stmt::Require { cond, .. } => LoweredStmt::Require(r.lower_expr(cond)?),
Stmt::Assign { name, value: Expr::StrLit { value: bytes, .. }, span }
if r.is_dynamic_bytes_var(name) =>
{
LoweredStmt::Assign(LoweredAssign::ConstDynamicBytes {
base_slot: r.dynamic_bytes_slot(name, *span)?,
bytes: bytes.clone(),
})
}
Stmt::Assign { name, span, .. } if r.is_dynamic_bytes_var(name) => {
return Err(CompileError::at_code(
codes::UNSUPPORTED_FEATURE,
format!("`{name}` is a dynamic `string`/`bytes`; v1 only supports assigning a string literal to it"),
*span,
))
}
Stmt::Assign { name, value, span } => LoweredStmt::Assign(LoweredAssign::Scalar {
slot: r.scalar_slot(name, *span)?,
value: r.lower_expr(value)?,
}),
Stmt::IndexAssign { base: idx_name, key, value, span } if r.is_array(idx_name) => {
LoweredStmt::Assign(LoweredAssign::ArrayElem {
slot: r.array_base_slot(idx_name, *span)?,
index: r.lower_expr(key)?,
value: r.lower_expr(value)?,
})
}
Stmt::IndexAssign { base: map_name, key, value, span } => LoweredStmt::Assign(LoweredAssign::MapEntry {
base_slot: r.mapping_base_slot(map_name, *span)?,
key: r.lower_expr(key)?,
value: r.lower_expr(value)?,
}),
Stmt::Push { base, value, span } => LoweredStmt::Assign(LoweredAssign::ArrayPush {
slot: r.array_base_slot(base, *span)?,
value: r.lower_expr(value)?,
}),
Stmt::Pop { base, span } => LoweredStmt::Assign(LoweredAssign::ArrayPop {
slot: r.array_base_slot(base, *span)?,
}),
Stmt::DeleteIndex { base, key, span } => LoweredStmt::Assign(LoweredAssign::ArrayDelete {
slot: r.array_base_slot(base, *span)?,
index: r.lower_expr(key)?,
}),
Stmt::Emit { name: ev_name, args, span } => LoweredStmt::Emit(lower_emit(facet, r, ev_name, args, *span)?),
Stmt::If { cond, then_body, else_body, .. } => LoweredStmt::If {
cond: r.lower_expr(cond)?,
then_body: lower_stmts(facet, r, then_body)?,
else_body: lower_stmts(facet, r, else_body)?,
},
other => {
return Err(CompileError::at_code(
codes::UNSUPPORTED_FEATURE,
format!("only `require`, `if`, `emit`, and assignments are supported in a mutating body, got {other:?}"),
r.func.span,
))
}
})
}
#[cfg(feature = "wallet")]
pub fn compile(facet: &Facet) -> Result<CompiledArtifact, CompileError> {
use crate::error_codes as codes;
let base = storage_base(&facet.name);
let mut lowered: Vec<([u8; 4], Body)> = Vec::with_capacity(facet.functions.len());
let mut seen_selectors: Vec<[u8; 4]> = Vec::new();
for func in &facet.functions {
let signature = function_signature(func);
let selector = crate::registry::selector(&signature);
if seen_selectors.contains(&selector) {
return Err(CompileError::at_code(
codes::UNSUPPORTED_FEATURE,
format!("selector collision: two functions hash to {selector:02x?}"),
func.span,
));
}
seen_selectors.push(selector);
let r = Resolver { facet, base, func };
let returns_dynamic = func.returns.map(|t| t.is_dynamic()).unwrap_or(false);
let body = match &func.body {
Stmt::Return(Expr::StrLit { value, span }) => {
if !returns_dynamic {
return Err(CompileError::at_code(
codes::TYPE_MISMATCH,
"a string literal can only be returned from a `returns (string)`/`returns (bytes)` function"
.to_string(),
*span,
));
}
Body::ConstString(value.clone())
}
Stmt::Return(Expr::StateVar { name, span })
if returns_dynamic && r.is_dynamic_bytes_var(name) =>
{
Body::DynamicStorageReturn { slot: r.dynamic_bytes_slot(name, *span)? }
}
Stmt::Return(Expr::StateVar { name, .. })
if returns_dynamic && r.dynamic_param_index(name).is_some() =>
{
Body::EchoParam { param_index: r.dynamic_param_index(name).unwrap() }
}
_ if returns_dynamic => {
return Err(CompileError::at_code(
codes::TYPE_MISMATCH,
"a `returns (string)`/`returns (bytes)` function must return a string literal, a \
`string`/`bytes` state variable, or a `string`/`bytes` parameter in v1"
.to_string(),
func.span,
))
}
Stmt::Return(Expr::IntLit { value_be32, .. }) => Body::View(BodyValue::Const(*value_be32)),
Stmt::Return(Expr::StateVar { name, span }) if r.state_var_index(name).is_some() => {
Body::View(BodyValue::StorageSlot(r.scalar_slot(name, *span)?))
}
Stmt::Return(expr) => Body::ViewExpr(r.lower_expr(expr)?),
Stmt::Block(stmts) => Body::Mutating(lower_stmts(facet, &r, stmts)?),
other => {
return Err(CompileError::at_code(
codes::UNSUPPORTED_FEATURE,
format!("unsupported function body {other:?}"),
func.span,
))
}
};
lowered.push((selector, body));
}
Ok(assemble_full(lowered))
}
#[cfg(test)]
mod tests {
#[test]
fn assemble_one_const_fn_round_trips() {
let mut w = [0u8; 32];
w[31] = 7;
let art = super::assemble(&[([0xaa, 0xbb, 0xcc, 0xdd], super::BodyValue::Const(w))]);
assert_eq!(art.init_code, crate::soliditylite::asm::init_wrapper(&art.runtime));
}
#[cfg(feature = "wallet")]
#[test]
fn slot_at_adds_index_to_base() {
let base = [0u8; 32]; assert_eq!(super::slot_at(base, 0), base);
let mut one = [0u8; 32];
one[31] = 1;
assert_eq!(super::slot_at(base, 1), one);
}
#[cfg(feature = "wallet")]
#[test]
fn slot_at_carries_across_a_byte_boundary() {
let mut base = [0u8; 32];
base[31] = 0xff;
let got = super::slot_at(base, 1);
let mut want = [0u8; 32];
want[30] = 0x01;
assert_eq!(got, want);
}
#[cfg(feature = "wallet")]
#[test]
fn tally_bump_emits_sload_add_sstore() {
use super::super::asm::op;
const SRC: &str = "facet Tally { uint256 n; \
function bump() external { n = n + 1; } \
function get() external view returns (uint256) { return n; } }";
let art = super::super::compile(SRC).expect("Tally must compile");
let rt = &art.runtime;
let base = super::storage_base("Tally");
let slot_n = super::slot_at(base, 0);
let sel_bump = crate::registry::selector("bump()");
let sel_get = crate::registry::selector("get()");
let push4 = |sel: [u8; 4]| -> Vec<u8> { std::iter::once(op::PUSH1 + 3).chain(sel).collect() };
assert!(
rt.windows(5).any(|w| w == push4(sel_bump)),
"bump() selector PUSH4 must be present"
);
assert!(
rt.windows(5).any(|w| w == push4(sel_get)),
"get() selector PUSH4 must be present"
);
let mut expected = Vec::new();
expected.push(op::PUSH1 + 31); expected.extend_from_slice(&slot_n);
expected.push(op::SLOAD);
expected.extend_from_slice(&[op::PUSH1, 0x01]);
expected.push(op::ADD);
expected.push(op::PUSH1 + 31); expected.extend_from_slice(&slot_n);
expected.push(op::SSTORE);
expected.extend_from_slice(&[op::PUSH1, 0x00, op::PUSH1, 0x00, op::RETURN]);
assert!(
rt.windows(expected.len()).any(|w| w == expected.as_slice()),
"bump() must emit SLOAD/PUSH1 0x01/ADD/SSTORE/RETURN(0,0) at slot n.\n\
expected window not found; runtime = {}",
to_hex(rt)
);
let arm = push4(sel_bump);
let arm_pos = rt.windows(arm.len()).position(|w| w == arm.as_slice()).unwrap();
let body_op = arm_pos + 5 + 1 + 1;
let body_off = u16::from_be_bytes([rt[body_op], rt[body_op + 1]]) as usize;
assert_eq!(rt[body_off], op::JUMPDEST, "bump() body must start with JUMPDEST");
assert_eq!(rt[body_off + 1], op::PUSH1 + 31, "first op after JUMPDEST is PUSH32");
assert_eq!(&rt[body_off + 2..body_off + 34], &slot_n, "PUSH32 pushes slot n");
assert_eq!(rt[body_off + 34], op::SLOAD, "then SLOAD");
assert_eq!(&rt[body_off + 35..body_off + 37], &[op::PUSH1, 0x01], "then PUSH1 1");
assert_eq!(rt[body_off + 37], op::ADD, "then ADD");
assert_eq!(rt[body_off + 38], op::PUSH1 + 31, "then PUSH32 (slot)");
assert_eq!(&rt[body_off + 39..body_off + 71], &slot_n, "the SSTORE slot");
assert_eq!(rt[body_off + 71], op::SSTORE, "then SSTORE");
assert_eq!(art.init_code, super::super::asm::init_wrapper(rt));
}
#[cfg(feature = "wallet")]
#[test]
fn add_expression_lowers_to_sload_push_add() {
use super::super::asm::op;
let art = super::super::compile(
"facet C { uint256 n; function bump() external { n = n + 1; } }",
)
.unwrap();
let rt = &art.runtime;
let base = super::storage_base("C");
let slot = super::slot_at(base, 0);
let pos = rt.iter().position(|&b| b == op::SLOAD).expect("an SLOAD must be present");
assert_eq!(&rt[pos + 1..pos + 3], &[op::PUSH1, 0x01], "SLOAD then PUSH1 1");
assert_eq!(rt[pos + 3], op::ADD, "then ADD");
let mut push32_slot = vec![op::PUSH1 + 31];
push32_slot.extend_from_slice(&slot);
assert!(
rt.windows(33).any(|w| w == push32_slot.as_slice()),
"the slot is PUSH32'd for the SSTORE"
);
assert!(rt.contains(&op::SSTORE), "an SSTORE must be present");
}
#[cfg(feature = "wallet")]
#[test]
fn assign_to_unknown_var_is_a_clean_error() {
let err = super::super::compile(
"facet C { function f() external { ghost = 1; } }",
)
.expect_err("assigning an undeclared var must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::UNDEFINED_VARIABLE));
assert!(err.to_string().starts_with("LH0"));
}
#[cfg(feature = "wallet")]
#[test]
fn read_unknown_var_in_add_is_a_clean_error() {
let err = super::super::compile(
"facet C { uint256 n; function f() external { n = n + missing; } }",
)
.expect_err("reading an undeclared var must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::UNDEFINED_VARIABLE));
}
#[cfg(feature = "wallet")]
fn map_entry_slot(key: &[u8; 32], base: &[u8; 32]) -> [u8; 32] {
use sha3::{Digest, Keccak256};
let mut h = Keccak256::new();
h.update(key); h.update(base); let mut out = [0u8; 32];
out.copy_from_slice(&h.finalize());
out
}
#[cfg(feature = "wallet")]
#[test]
fn add_loads_param_via_calldataload_and_writes_map_entry() {
use super::super::asm::op;
const SRC: &str = "facet Ledger { mapping(address => uint256) bal; \
function add(uint256 amt) external { bal[msg.sender] = bal[msg.sender] + amt; } }";
let art = super::super::compile(SRC).expect("Ledger add() must compile");
let rt = &art.runtime;
let base = super::storage_base("Ledger");
let map_base = super::slot_at(base, 0);
assert!(
rt.windows(3).any(|w| w == [op::PUSH1, 0x04, op::CALLDATALOAD]),
"amt must load via CALLDATALOAD(0x04); runtime = {}",
to_hex(rt)
);
assert!(rt.contains(&op::CALLER), "msg.sender must emit CALLER");
let mut derive = vec![op::CALLER, op::PUSH1, 0x00, op::MSTORE, op::PUSH1 + 31];
derive.extend_from_slice(&map_base);
derive.extend_from_slice(&[
op::PUSH1, 0x20, op::MSTORE, op::PUSH1, 0x40, op::PUSH1, 0x00, op::KECCAK256,
]);
assert!(
rt.windows(derive.len()).any(|w| w == derive.as_slice()),
"the bal[msg.sender] slot derivation (MSTORE key / MSTORE base / KECCAK256) \
must be present; runtime = {}",
to_hex(rt)
);
assert!(rt.contains(&op::SSTORE), "the map write must SSTORE");
assert!(rt.contains(&op::SLOAD), "the map read must SLOAD");
assert!(rt.contains(&op::ADD), "bal[..] + amt must ADD");
}
#[cfg(feature = "wallet")]
#[test]
fn balance_of_derives_slot_from_calldata_key_then_sloads() {
use super::super::asm::op;
const SRC: &str = "facet Ledger { mapping(address => uint256) bal; \
function balanceOf(address who) external view returns (uint256) { return bal[who]; } }";
let art = super::super::compile(SRC).expect("balanceOf must compile");
let rt = &art.runtime;
let base = super::storage_base("Ledger");
let map_base = super::slot_at(base, 0);
let mut expected = vec![
op::PUSH1, 0x04, op::CALLDATALOAD, op::PUSH1, 0x00, op::MSTORE, op::PUSH1 + 31,
];
expected.extend_from_slice(&map_base);
expected.extend_from_slice(&[
op::PUSH1, 0x20, op::MSTORE, op::PUSH1, 0x40, op::PUSH1, 0x00, op::KECCAK256, op::SLOAD,
]);
assert!(
rt.windows(expected.len()).any(|w| w == expected.as_slice()),
"balanceOf must derive bal[who]'s slot from calldata then SLOAD; runtime = {}",
to_hex(rt)
);
let sel = crate::registry::selector("balanceOf(address)");
let push4: Vec<u8> = std::iter::once(op::PUSH1 + 3).chain(sel).collect();
assert!(
rt.windows(5).any(|w| w == push4.as_slice()),
"balanceOf(address) selector must be dispatched"
);
}
#[cfg(feature = "wallet")]
#[test]
fn ledger_target_compiles_and_slot_matches_offchain_keccak() {
const SRC: &str = "facet Ledger { mapping(address => uint256) bal; \
function add(uint256 amt) external { bal[msg.sender] = bal[msg.sender] + amt; } \
function balanceOf(address who) external view returns (uint256) { return bal[who]; } }";
let art = super::super::compile(SRC).expect("the Ledger TARGET must compile");
assert_eq!(art.init_code, super::super::asm::init_wrapper(&art.runtime));
let base = super::storage_base("Ledger");
let map_base = super::slot_at(base, 0);
let mut key = [0u8; 32];
key[12..].copy_from_slice(&[0x11; 20]); let slot = map_entry_slot(&key, &map_base);
use sha3::{Digest, Keccak256};
let mut h = Keccak256::new();
h.update(key);
h.update(map_base);
let mut want = [0u8; 32];
want.copy_from_slice(&h.finalize());
assert_eq!(slot, want, "map entry slot = keccak256(key ++ base)");
}
#[cfg(feature = "wallet")]
#[test]
fn indexing_a_scalar_is_a_clean_error() {
let err = super::super::compile(
"facet C { uint256 n; function f() external view returns (uint256) { return n[0]; } }",
)
.expect_err("indexing a scalar must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::TYPE_MISMATCH));
}
#[cfg(feature = "wallet")]
#[test]
fn bare_mapping_reference_is_a_clean_error() {
let err = super::super::compile(
"facet C { mapping(address => uint256) m; \
function f() external view returns (uint256) { return m; } }",
)
.expect_err("a bare mapping reference must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::TYPE_MISMATCH));
}
#[cfg(feature = "wallet")]
#[test]
fn unknown_param_reference_is_a_clean_error() {
let err = super::super::compile(
"facet C { function f(uint256 a) external view returns (uint256) { return b; } }",
)
.expect_err("an unknown name must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::UNDEFINED_VARIABLE));
}
#[cfg(feature = "wallet")]
fn array_elem_slot(slot: &[u8; 32], index: u64) -> [u8; 32] {
use sha3::{Digest, Keccak256};
let base: [u8; 32] = Keccak256::digest(slot).into();
super::slot_at(base, index)
}
#[cfg(feature = "wallet")]
#[test]
fn array_length_reads_the_base_slot() {
use super::super::asm::op;
const SRC: &str = "facet C { uint256[] xs; \
function len() external view returns (uint256) { return xs.length; } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
let slot = super::slot_at(super::storage_base("C"), 0);
let mut want = vec![op::PUSH1 + 31];
want.extend_from_slice(&slot);
want.push(op::SLOAD);
assert!(
rt.windows(want.len()).any(|w| w == want.as_slice()),
"xs.length must be PUSH32 <baseSlot> SLOAD; runtime = {}",
to_hex(rt)
);
}
#[cfg(feature = "wallet")]
#[test]
fn array_index_read_derives_keccak_slot_plus_index() {
use super::super::asm::op;
const SRC: &str = "facet C { uint256[] xs; \
function at(uint256 i) external view returns (uint256) { return xs[i]; } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
let slot = super::slot_at(super::storage_base("C"), 0);
let mut want = vec![op::PUSH1 + 31];
want.extend_from_slice(&slot);
want.extend_from_slice(&[
op::PUSH1, 0x00, op::MSTORE, op::PUSH1, 0x20, op::PUSH1, 0x00, op::KECCAK256,
op::PUSH1, 0x04, op::CALLDATALOAD, op::ADD, op::SLOAD,
]);
assert!(
rt.windows(want.len()).any(|w| w == want.as_slice()),
"xs[i] must derive keccak256(slot)+i then SLOAD; runtime = {}",
to_hex(rt)
);
let sel = crate::registry::selector("at(uint256)");
let push4: Vec<u8> = std::iter::once(op::PUSH1 + 3).chain(sel).collect();
assert!(rt.windows(5).any(|w| w == push4.as_slice()), "at(uint256) dispatched");
}
#[cfg(feature = "wallet")]
#[test]
fn array_index_write_sstores_to_keccak_slot() {
use super::super::asm::op;
const SRC: &str = "facet C { uint256[] xs; \
function set(uint256 i, uint256 v) external { xs[i] = v; } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
let slot = super::slot_at(super::storage_base("C"), 0);
let mut want = vec![op::PUSH1, 0x24, op::CALLDATALOAD, op::PUSH1 + 31];
want.extend_from_slice(&slot);
want.extend_from_slice(&[
op::PUSH1, 0x00, op::MSTORE, op::PUSH1, 0x20, op::PUSH1, 0x00, op::KECCAK256,
op::PUSH1, 0x04, op::CALLDATALOAD, op::ADD, op::SSTORE,
]);
assert!(
rt.windows(want.len()).any(|w| w == want.as_slice()),
"xs[i] = v must push v, derive keccak256(slot)+i, SSTORE; runtime = {}",
to_hex(rt)
);
}
#[cfg(feature = "wallet")]
#[test]
fn array_push_stores_element_then_bumps_length() {
use super::super::asm::op;
const SRC: &str = "facet C { uint256[] xs; \
function add(uint256 v) external { xs.push(v); } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
let slot = super::slot_at(super::storage_base("C"), 0);
let mut elem = vec![op::PUSH1, 0x04, op::CALLDATALOAD, op::PUSH1 + 31];
elem.extend_from_slice(&slot);
elem.extend_from_slice(&[op::PUSH1, 0x00, op::MSTORE, op::PUSH1, 0x20, op::PUSH1, 0x00, op::KECCAK256]);
elem.push(op::PUSH1 + 31);
elem.extend_from_slice(&slot);
elem.extend_from_slice(&[op::SLOAD, op::ADD, op::SSTORE]);
assert!(
rt.windows(elem.len()).any(|w| w == elem.as_slice()),
"push must store v at keccak256(slot)+length; runtime = {}",
to_hex(rt)
);
let mut bump = vec![op::PUSH1 + 31];
bump.extend_from_slice(&slot);
bump.extend_from_slice(&[op::SLOAD, op::PUSH1, 0x01, op::ADD, op::PUSH1 + 31]);
bump.extend_from_slice(&slot);
bump.push(op::SSTORE);
assert!(
rt.windows(bump.len()).any(|w| w == bump.as_slice()),
"push must bump the length slot to length + 1; runtime = {}",
to_hex(rt)
);
}
#[cfg(feature = "wallet")]
#[test]
fn array_pop_zeroes_last_element_and_decrements_length() {
use super::super::asm::op;
const SRC: &str = "facet C { uint256[] xs; \
function pop() external { xs.pop(); } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
let slot = super::slot_at(super::storage_base("C"), 0);
let mut clear = vec![op::PUSH1, 0x00, op::PUSH1 + 31];
clear.extend_from_slice(&slot);
clear.extend_from_slice(&[op::PUSH1, 0x00, op::MSTORE, op::PUSH1, 0x20, op::PUSH1, 0x00, op::KECCAK256]);
assert!(
rt.windows(clear.len()).any(|w| w == clear.as_slice()),
"pop must clear the element with value 0 at keccak256(slot)+(len-1); runtime = {}",
to_hex(rt)
);
let mut len_minus_one = vec![op::PUSH1, 0x01, op::PUSH1 + 31];
len_minus_one.extend_from_slice(&slot);
len_minus_one.extend_from_slice(&[op::SLOAD, op::SUB]);
assert!(
rt.windows(len_minus_one.len()).any(|w| w == len_minus_one.as_slice()),
"pop must compute len - 1 (PUSH1 1, PUSH32 slot SLOAD, SUB); runtime = {}",
to_hex(rt)
);
let mut store_len = vec![op::PUSH1 + 31];
store_len.extend_from_slice(&slot);
store_len.push(op::SSTORE);
assert!(
rt.windows(store_len.len()).any(|w| w == store_len.as_slice()),
"pop must SSTORE the decremented length to the base slot"
);
}
#[cfg(feature = "wallet")]
#[test]
fn delete_index_zeroes_element_and_leaves_length() {
use super::super::asm::op;
const SRC: &str = "facet C { uint256[] xs; \
function clear(uint256 i) external { delete xs[i]; } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
let slot = super::slot_at(super::storage_base("C"), 0);
let mut want = vec![op::PUSH1, 0x00, op::PUSH1 + 31];
want.extend_from_slice(&slot);
want.extend_from_slice(&[
op::PUSH1, 0x00, op::MSTORE, op::PUSH1, 0x20, op::PUSH1, 0x00, op::KECCAK256,
op::PUSH1, 0x04, op::CALLDATALOAD, op::ADD, op::SSTORE,
]);
assert!(
rt.windows(want.len()).any(|w| w == want.as_slice()),
"delete xs[i] must push 0, derive keccak256(slot)+i, SSTORE; runtime = {}",
to_hex(rt)
);
let sstores = count_op(rt, op::SSTORE);
assert_eq!(sstores, 1, "delete xs[i] performs a single SSTORE (no length write)");
}
#[cfg(feature = "wallet")]
#[test]
fn pop_and_delete_on_non_arrays_are_clean_errors() {
let err = super::super::compile(
"facet C { uint256 n; function f() external { n.pop(); } }",
)
.expect_err("n.pop() on a scalar must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::TYPE_MISMATCH));
let err = super::super::compile(
"facet C { mapping(address => uint256) m; function f() external { delete m[msg.sender]; } }",
)
.expect_err("delete on a mapping must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::TYPE_MISMATCH));
}
#[cfg(feature = "wallet")]
#[test]
fn array_elem_slot_matches_independent_keccak() {
use sha3::{Digest, Keccak256};
let slot = super::slot_at(super::storage_base("C"), 0);
let base: [u8; 32] = Keccak256::digest(slot).into();
assert_eq!(array_elem_slot(&slot, 0), base);
assert_eq!(array_elem_slot(&slot, 3), super::slot_at(base, 3));
}
#[cfg(feature = "wallet")]
#[test]
fn array_target_facet_compiles_with_canonical_layout() {
use super::super::asm::op;
const SRC: &str = "facet Stack { uint256 total; uint256[] xs; \
function push(uint256 v) external { xs.push(v); total = total + 1; } \
function set(uint256 i, uint256 v) external { xs[i] = v; } \
function get(uint256 i) external view returns (uint256) { return xs[i]; } \
function size() external view returns (uint256) { return xs.length; } }";
let art = super::super::compile(SRC).expect("the array Stack TARGET must compile");
let rt = &art.runtime;
let xs_slot = super::slot_at(super::storage_base("Stack"), 1);
let mut len_read = vec![op::PUSH1 + 31];
len_read.extend_from_slice(&xs_slot);
len_read.push(op::SLOAD);
assert!(rt.windows(len_read.len()).any(|w| w == len_read.as_slice()), "size() reads slot 1");
for sig in ["push(uint256)", "set(uint256,uint256)", "get(uint256)", "size()"] {
let sel = crate::registry::selector(sig);
let push4: Vec<u8> = std::iter::once(op::PUSH1 + 3).chain(sel).collect();
assert!(rt.windows(5).any(|w| w == push4.as_slice()), "{sig} dispatched");
}
assert_eq!(art.init_code, super::super::asm::init_wrapper(rt));
}
#[cfg(feature = "wallet")]
#[test]
fn array_ops_on_non_arrays_are_clean_errors() {
let err = super::super::compile(
"facet C { uint256 n; function f() external view returns (uint256) { return n.length; } }",
)
.expect_err("n.length on a scalar must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::TYPE_MISMATCH));
let err = super::super::compile(
"facet C { uint256 n; function f() external { n.push(1); } }",
)
.expect_err("n.push on a scalar must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::TYPE_MISMATCH));
let err = super::super::compile(
"facet C { uint256[] xs; function f() external view returns (uint256) { return xs; } }",
)
.expect_err("a bare array reference must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::TYPE_MISMATCH));
}
#[cfg(feature = "wallet")]
#[test]
fn const_short_string_store_emits_one_packed_sstore() {
use super::super::asm::op;
const SRC: &str = "facet Note { string s; function set() external { s = \"hi\"; } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
let slot = super::slot_at(super::storage_base("Note"), 0);
let mut packed = [0u8; 32];
packed[..2].copy_from_slice(b"hi");
packed[31] = 4;
let mut want = vec![op::PUSH1 + 31];
want.extend_from_slice(&packed);
want.push(op::PUSH1 + 31);
want.extend_from_slice(&slot);
want.push(op::SSTORE);
assert!(
rt.windows(want.len()).any(|w| w == want.as_slice()),
"short store must be PUSH32 packed / PUSH32 slot / SSTORE; runtime = {}",
to_hex(rt)
);
assert_eq!(count_op(rt, op::SSTORE), 1, "short store is a single SSTORE");
}
#[cfg(feature = "wallet")]
#[test]
fn const_long_string_store_unrolls_header_plus_data_sstores() {
use super::super::asm::op;
const S: &str = "this string is forty bytes long, yes sir";
let src = format!("facet Note {{ string s; function set() external {{ s = \"{S}\"; }} }}");
let rt = &super::super::compile(&src).unwrap().runtime;
let slot = super::slot_at(super::storage_base("Note"), 0);
let mut header = [0u8; 32];
header[31] = 81;
let mut want_header = vec![op::PUSH1 + 31];
want_header.extend_from_slice(&header);
want_header.push(op::PUSH1 + 31);
want_header.extend_from_slice(&slot);
want_header.push(op::SSTORE);
assert!(
rt.windows(want_header.len()).any(|w| w == want_header.as_slice()),
"long store must SSTORE the len*2+1 header to the base slot; runtime = {}",
to_hex(rt)
);
assert_eq!(count_op(rt, op::SSTORE), 3, "header + 2 data chunks = 3 SSTOREs");
assert_eq!(count_op(rt, op::KECCAK256), 0, "data slots are precomputed (no runtime KECCAK256)");
use sha3::{Digest, Keccak256};
let data0: [u8; 32] = Keccak256::digest(slot).into();
for slot_i in [data0, super::slot_at(data0, 1)] {
let mut push_slot = vec![op::PUSH1 + 31];
push_slot.extend_from_slice(&slot_i);
assert!(
rt.windows(33).any(|w| w == push_slot.as_slice()),
"each data chunk SSTOREs to its precomputed keccak256(slot)+i slot"
);
}
}
#[cfg(feature = "wallet")]
#[test]
fn dynamic_storage_getter_emits_branch_and_copy_loop_opcodes() {
use super::super::asm::op;
const SRC: &str = "facet Note { string s; \
function get() external view returns (string) { return s; } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
assert!(
rt.windows(3).any(|w| w == [op::PUSH1, 0x01, op::AND]),
"the getter must test the slot's low bit via AND 1; runtime = {}",
to_hex(rt)
);
for o in [op::DUP2, op::DUP3, op::SWAP1, op::AND, op::KECCAK256] {
assert!(rt.contains(&o), "the getter must emit {o:#x}");
}
}
#[cfg(feature = "wallet")]
#[test]
fn dynamic_param_echo_emits_calldatacopy() {
use super::super::asm::op;
const SRC: &str =
"facet E { function echo(string s) external pure returns (string) { return s; } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
assert!(rt.contains(&op::CALLDATACOPY), "the echo must bulk-copy via CALLDATACOPY");
assert!(
rt.windows(5).any(|w| w == [op::PUSH1, 0x20, op::PUSH1, 0x00, op::MSTORE]),
"the echo must write the 0x20 ABI offset at mem[0]; runtime = {}",
to_hex(rt)
);
}
#[cfg(feature = "wallet")]
#[test]
fn bytes_param_uses_the_bytes_abi_selector() {
use super::super::asm::op;
const SRC: &str =
"facet E { function echo(bytes b) external pure returns (bytes) { return b; } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
let sel = crate::registry::selector("echo(bytes)");
let push4: Vec<u8> = std::iter::once(op::PUSH1 + 3).chain(sel).collect();
assert!(rt.windows(5).any(|w| w == push4.as_slice()), "echo(bytes) selector dispatched");
assert!(rt.contains(&op::CALLDATACOPY), "bytes echo uses the same CALLDATACOPY path");
}
#[cfg(feature = "wallet")]
#[test]
fn dynamic_value_in_word_context_is_a_clean_error() {
let err = super::super::compile(
"facet C { string s; function f() external view returns (uint256) { return s; } }",
)
.expect_err("a string state var is not a single word");
assert_eq!(err.code, Some(crate::error_codes::TYPE_MISMATCH));
let err = super::super::compile(
"facet C { function f(bytes b) external pure returns (uint256) { return b + 1; } }",
)
.expect_err("a bytes param is not a single word");
assert_eq!(err.code, Some(crate::error_codes::TYPE_MISMATCH));
let err = super::super::compile(
"facet C { string s; uint256 n; function f() external { s = n; } }",
)
.expect_err("a dynamic state var only accepts a string literal in v1");
assert_eq!(err.code, Some(crate::error_codes::UNSUPPORTED_FEATURE));
}
#[cfg(feature = "wallet")]
#[test]
fn comparison_lowers_to_gt_and_gt_iszero() {
use super::super::asm::op;
let art = super::super::compile(
"facet C { function f(uint256 n) external { require(n > 0, \"a\"); require(n <= 100, \"b\"); } }",
)
.unwrap();
let rt = &art.runtime;
assert!(rt.contains(&op::GT), "a `>` (and the `<=` inversion) must emit GT");
assert!(
rt.windows(2).any(|w| w == [op::GT, op::ISZERO]),
"`n <= 100` must emit GT then ISZERO; runtime = {}",
to_hex(rt)
);
}
#[cfg(feature = "wallet")]
#[test]
fn each_comparison_emits_its_opcodes() {
use super::super::asm::op;
let cases: &[(&str, &[u8])] = &[
(">", &[op::GT]),
("<", &[op::LT]),
("==", &[op::EQ]),
("<=", &[op::GT, op::ISZERO]), (">=", &[op::LT, op::ISZERO]), ];
for (src_op, want) in cases {
let src = format!(
"facet C {{ function f(uint256 n) external {{ require(n {src_op} 1, \"x\"); }} }}"
);
let rt = super::super::compile(&src).unwrap().runtime;
assert!(
rt.windows(want.len()).any(|w| w == *want),
"`{src_op}` must emit {want:02x?}; runtime = {}",
to_hex(&rt)
);
}
}
#[cfg(feature = "wallet")]
#[test]
fn require_emits_iszero_jumpi_to_a_revert_stub() {
use super::super::asm::op;
let art = super::super::compile(
"facet C { function f(uint256 n) external { require(n > 0, \"zero\"); } }",
)
.unwrap();
let rt = &art.runtime;
let pos = rt
.windows(5)
.position(|w| w[0] == op::ISZERO && w[1] == op::PUSH2 && w[4] == op::JUMPI)
.expect("require must emit ISZERO PUSH2 <revert> JUMPI");
let target = u16::from_be_bytes([rt[pos + 2], rt[pos + 3]]) as usize;
assert_eq!(rt[target], op::JUMPDEST, "the require target must be a JUMPDEST");
assert_eq!(
&rt[target..target + 6],
&[op::JUMPDEST, op::PUSH1, 0x00, op::PUSH1, 0x00, op::REVERT],
"the revert stub must be JUMPDEST PUSH1 0 PUSH1 0 REVERT"
);
}
#[cfg(feature = "wallet")]
#[test]
fn multiple_requires_share_one_revert_stub() {
use super::super::asm::op;
let art = super::super::compile(
"facet C { function f(uint256 n) external { require(n > 0, \"a\"); require(n <= 100, \"b\"); } }",
)
.unwrap();
let rt = &art.runtime;
let jumpis = rt
.windows(5)
.filter(|w| w[0] == op::ISZERO && w[1] == op::PUSH2 && w[4] == op::JUMPI)
.count();
assert_eq!(jumpis, 2, "two requires → two ISZERO/JUMPI branches");
let targets: Vec<usize> = rt
.windows(5)
.filter(|w| w[0] == op::ISZERO && w[1] == op::PUSH2 && w[4] == op::JUMPI)
.map(|w| u16::from_be_bytes([w[2], w[3]]) as usize)
.collect();
assert_eq!(targets[0], targets[1], "both requires share ONE revert stub");
let revert_stubs = rt
.windows(6)
.filter(|w| w == &[op::JUMPDEST, op::PUSH1, 0x00, op::PUSH1, 0x00, op::REVERT])
.count();
assert_eq!(revert_stubs, 2, "only the fallback + one shared require stub");
}
#[cfg(feature = "wallet")]
#[test]
fn require_free_body_has_no_extra_revert_stub() {
use super::super::asm::op;
let art = super::super::compile(
"facet C { uint256 n; function bump() external { n = n + 1; } }",
)
.unwrap();
let rt = &art.runtime;
let revert_stubs = rt
.windows(6)
.filter(|w| w == &[op::JUMPDEST, op::PUSH1, 0x00, op::PUSH1, 0x00, op::REVERT])
.count();
assert_eq!(revert_stubs, 1, "only the dispatcher fallback REVERT(0,0)");
}
#[cfg(feature = "wallet")]
#[test]
fn require_with_true_constant_compiles_and_does_not_take_the_branch() {
use super::super::asm::op;
let art = super::super::compile(
"facet C { function f() external { require(1 == 1, \"never\"); } }",
)
.unwrap();
let rt = &art.runtime;
assert!(rt.contains(&op::EQ), "1 == 1 emits EQ");
assert!(
rt.windows(5).any(|w| w[0] == op::ISZERO && w[1] == op::PUSH2 && w[4] == op::JUMPI),
"the require branch is well-formed"
);
assert_eq!(art.init_code, super::super::asm::init_wrapper(rt));
}
#[cfg(feature = "wallet")]
#[test]
fn malformed_require_is_a_clean_compile_error() {
let err = super::super::compile(
"facet C { function f(uint256 n) external { require(n > , \"x\"); } }",
)
.expect_err("a malformed comparison must fail cleanly");
assert!(err.code.is_some(), "carries an LH code");
assert!(err.to_string().starts_with("LH0"));
let err = super::super::compile(
"facet C { function f() external { require(ghost > 0, \"x\"); } }",
)
.expect_err("an unknown var in a require must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::UNDEFINED_VARIABLE));
}
#[cfg(feature = "wallet")]
#[test]
fn counter_target_facet_compiles_with_canonical_selectors() {
use super::super::asm::op;
const SRC: &str = "facet Counter { mapping(address => uint256) count; uint256 total; \
function increment() external { count[msg.sender] = count[msg.sender] + 1; total = total + 1; } \
function incrementBy(uint256 n) external { require(n > 0, \"zero\"); require(n <= 100, \"too big\"); \
count[msg.sender] = count[msg.sender] + n; total = total + n; } \
function countOf(address who) external view returns (uint256) { return count[who]; } \
function totalCount() external view returns (uint256) { return total; } }";
let art = super::super::compile(SRC).expect("the CounterFacet TARGET must compile");
let rt = &art.runtime;
let sels: [(&str, [u8; 4]); 4] = [
("increment()", [0xd0, 0x9d, 0xe0, 0x8a]),
("incrementBy(uint256)", [0x03, 0xdf, 0x17, 0x9c]),
("countOf(address)", [0xf8, 0x97, 0x7e, 0x96]),
("totalCount()", [0x34, 0xea, 0xfb, 0x11]),
];
for (sig, want) in sels {
assert_eq!(crate::registry::selector(sig), want, "selector pin for {sig}");
let push4: Vec<u8> = std::iter::once(op::PUSH1 + 3).chain(want).collect();
assert!(
rt.windows(5).any(|w| w == push4.as_slice()),
"{sig} selector {want:02x?} must be dispatched"
);
}
assert!(rt.contains(&op::GT), "incrementBy uses `>` / `<=` → GT");
assert!(rt.windows(2).any(|w| w == [op::GT, op::ISZERO]), "`<=` → GT ISZERO");
assert!(
rt.windows(5).any(|w| w[0] == op::ISZERO && w[1] == op::PUSH2 && w[4] == op::JUMPI),
"require → ISZERO/JUMPI"
);
assert_eq!(art.init_code, super::super::asm::init_wrapper(rt));
}
#[cfg(feature = "wallet")]
#[test]
fn event_topic0_is_full_keccak_of_the_signature() {
use sha3::{Digest, Keccak256};
const SIG: &str = "Incremented(address,uint256,uint256)";
let topic0 = super::event_topic0(SIG);
let mut want = [0u8; 32];
want.copy_from_slice(&Keccak256::digest(SIG.as_bytes()));
assert_eq!(topic0, want, "topic0 must be the full keccak of the event sig");
let sel = crate::registry::selector(SIG);
assert_eq!(&topic0[..4], &sel, "the first 4 bytes coincide with the selector");
assert!(topic0[4..].iter().any(|&b| b != 0), "topic0 is more than 4 bytes");
assert_eq!(to_hex(&topic0), TOPIC0_INCREMENTED, "Incremented topic0 drifted");
}
#[cfg(feature = "wallet")]
const TOPIC0_INCREMENTED: &str =
"0xcd5ad702c30bb253c9e421ea7f3e00faee62ce859708bfdaf949788e5ba0fdb5";
#[cfg(feature = "wallet")]
#[test]
fn event_signature_uses_types_only() {
use super::super::ast::*;
use crate::rustlite::Span;
let sp = Span { start: 0, end: 0 };
let ev = EventDecl {
name: "Incremented".into(),
args: vec![
EventArg { ty: Ty::Address, indexed: true, name: "who".into(), span: sp },
EventArg { ty: Ty::Uint256, indexed: false, name: "newCount".into(), span: sp },
EventArg { ty: Ty::Uint256, indexed: false, name: "newTotal".into(), span: sp },
],
span: sp,
};
assert_eq!(super::event_signature(&ev), "Incremented(address,uint256,uint256)");
}
#[cfg(feature = "wallet")]
#[test]
fn emit_lowers_to_log2_with_topic0_push32_and_data_mstores() {
use super::super::asm::op;
const SRC: &str = "facet C { mapping(address => uint256) count; uint256 total; \
event Incremented(address indexed who, uint256 newCount, uint256 newTotal); \
function increment() external { count[msg.sender] = count[msg.sender] + 1; \
total = total + 1; emit Incremented(msg.sender, count[msg.sender], total); } }";
let art = super::super::compile(SRC).expect("emit facet must compile");
let rt = &art.runtime;
assert_eq!(count_op(rt, op::LOG2), 1, "exactly one LOG2");
for other in [op::LOG0, op::LOG1, op::LOG3, op::LOG4] {
assert_eq!(count_op(rt, other), 0, "no other LOGn opcode, found {other:#x}");
}
let topic0 = super::event_topic0("Incremented(address,uint256,uint256)");
let mut push32_topic0 = vec![op::PUSH1 + 31];
push32_topic0.extend_from_slice(&topic0);
assert!(
rt.windows(33).any(|w| w == push32_topic0.as_slice()),
"topic0 must be PUSH32'd; runtime = {}",
to_hex(rt)
);
assert!(count_op(rt, op::MSTORE) >= 2, "the two data words are MSTORE'd into memory");
let log_pos = real_opcodes(rt)
.iter()
.find(|(_, o)| *o == op::LOG2)
.map(|(off, _)| *off)
.unwrap();
assert_eq!(
&rt[log_pos - 2..log_pos],
&[op::PUSH1, 0x40],
"LOG2 is preceded by PUSH1 0x40 (the data offset on top of the stack)"
);
assert_eq!(
&rt[log_pos - 4..log_pos - 2],
&[op::PUSH1, 0x40],
"the length (0x40 = two 32-byte data words) is pushed before the offset"
);
assert_eq!(art.init_code, super::super::asm::init_wrapper(rt));
}
#[cfg(feature = "wallet")]
#[test]
fn emit_stack_order_is_topic1_topic0_len_offset_logn() {
use super::super::asm::op;
const SRC: &str = "facet C { event E(address indexed who, uint256 amt); \
function f(uint256 n) external { emit E(msg.sender, n); } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
let topic0 = super::event_topic0("E(address,uint256)");
let mut tail = vec![op::CALLER, op::PUSH1 + 31];
tail.extend_from_slice(&topic0);
tail.extend_from_slice(&[op::PUSH1, 0x20, op::PUSH1, 0x40, op::LOG2]);
assert!(
rt.windows(tail.len()).any(|w| w == tail.as_slice()),
"emit tail must be CALLER, PUSH32 topic0, PUSH len, PUSH offset, LOG2 (in that \
order so the EVM pops offset, length, topic0, topic1); runtime = {}",
to_hex(rt)
);
assert!(
rt.windows(3).any(|w| w == [op::PUSH1, 0x40, op::MSTORE]),
"the data word is MSTORE'd at the data base mem[0x40]"
);
}
#[cfg(feature = "wallet")]
#[test]
fn indexed_only_event_has_zero_length_data() {
use super::super::asm::op;
const SRC: &str = "facet C { event Hit(address indexed who); \
function f() external { emit Hit(msg.sender); } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
assert_eq!(count_op(rt, op::LOG2), 1, "one LOG2 (topic0 + who)");
let log_pos = real_opcodes(rt)
.iter()
.find(|(_, o)| *o == op::LOG2)
.map(|(off, _)| *off)
.expect("a LOG2");
assert_eq!(&rt[log_pos - 2..log_pos], &[op::PUSH1, 0x40], "offset on top");
assert_eq!(&rt[log_pos - 4..log_pos - 2], &[op::PUSH1, 0x00], "zero length");
assert!(count_op(rt, op::CALLER) >= 1, "the indexed who emits CALLER");
}
#[cfg(feature = "wallet")]
#[test]
fn no_arg_event_lowers_to_log1() {
use super::super::asm::op;
const SRC: &str = "facet C { event Ping(); function f() external { emit Ping(); } }";
let rt = &super::super::compile(SRC).unwrap().runtime;
assert_eq!(count_op(rt, op::LOG1), 1, "one LOG1");
let topic0 = super::event_topic0("Ping()");
let mut push32 = vec![op::PUSH1 + 31];
push32.extend_from_slice(&topic0);
assert!(rt.windows(33).any(|w| w == push32.as_slice()), "topic0 PUSH32");
}
#[cfg(feature = "wallet")]
#[test]
fn emit_unknown_event_is_a_clean_error() {
let err = super::super::compile(
"facet C { function f() external { emit Ghost(1); } }",
)
.expect_err("emitting an undeclared event must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::UNKNOWN_FUNCTION));
assert!(err.to_string().starts_with("LH0"));
}
#[cfg(feature = "wallet")]
#[test]
fn emit_arg_count_mismatch_is_a_clean_error() {
let err = super::super::compile(
"facet C { event E(address indexed a, uint256 b); \
function f() external { emit E(msg.sender); } }",
)
.expect_err("an arg-count mismatch must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::ARITY_MISMATCH));
let err = super::super::compile(
"facet C { event E(uint256 a); function f(uint256 n) external { emit E(n, n); } }",
)
.expect_err("too many args must fail cleanly");
assert_eq!(err.code, Some(crate::error_codes::ARITY_MISMATCH));
}
#[cfg(feature = "wallet")]
#[test]
fn full_counter_facet_with_events_compiles() {
use super::super::asm::op;
const SRC: &str = "facet CounterFacet { mapping(address => uint256) count; uint256 total; \
event Incremented(address indexed who, uint256 newCount, uint256 newTotal); \
function increment() external { count[msg.sender] = count[msg.sender] + 1; total = total + 1; \
emit Incremented(msg.sender, count[msg.sender], total); } \
function incrementBy(uint256 n) external { require(n > 0, \"zero\"); require(n <= 100, \"too big\"); \
count[msg.sender] = count[msg.sender] + n; total = total + n; \
emit Incremented(msg.sender, count[msg.sender], total); } \
function countOf(address who) external view returns (uint256) { return count[who]; } \
function totalCount() external view returns (uint256) { return total; } }";
let art = super::super::compile(SRC).expect("the FULL CounterFacet must compile");
let rt = &art.runtime;
let sels: [(&str, [u8; 4]); 4] = [
("increment()", [0xd0, 0x9d, 0xe0, 0x8a]),
("incrementBy(uint256)", [0x03, 0xdf, 0x17, 0x9c]),
("countOf(address)", [0xf8, 0x97, 0x7e, 0x96]),
("totalCount()", [0x34, 0xea, 0xfb, 0x11]),
];
for (sig, want) in sels {
assert_eq!(crate::registry::selector(sig), want, "selector pin for {sig}");
let push4: Vec<u8> = std::iter::once(op::PUSH1 + 3).chain(want).collect();
assert!(rt.windows(5).any(|w| w == push4.as_slice()), "{sig} dispatched");
}
assert_eq!(count_op(rt, op::LOG2), 2, "two Incremented LOG2s");
let topic0 = super::event_topic0("Incremented(address,uint256,uint256)");
let mut push32_topic0 = vec![op::PUSH1 + 31];
push32_topic0.extend_from_slice(&topic0);
let occurrences = rt.windows(33).filter(|w| *w == push32_topic0.as_slice()).count();
assert_eq!(occurrences, 2, "topic0 PUSH32'd once per emit");
assert_eq!(art.init_code, super::super::asm::init_wrapper(rt));
}
#[allow(dead_code)]
fn to_hex(bytes: &[u8]) -> String {
use core::fmt::Write;
let mut s = String::with_capacity(2 + bytes.len() * 2);
s.push_str("0x");
for b in bytes {
let _ = write!(s, "{b:02x}");
}
s
}
#[cfg(feature = "wallet")]
fn real_opcodes(code: &[u8]) -> Vec<(usize, u8)> {
use super::super::asm::op;
let mut out = Vec::new();
let mut i = 0;
while i < code.len() {
let opc = code[i];
out.push((i, opc));
if (op::PUSH1..=op::PUSH1 + 31).contains(&opc) {
let n = (opc - op::PUSH1) as usize + 1;
i += 1 + n;
} else {
i += 1;
}
}
out
}
#[cfg(feature = "wallet")]
fn count_op(code: &[u8], opcode: u8) -> usize {
real_opcodes(code).iter().filter(|(_, o)| *o == opcode).count()
}
}