use crate::soliditylite::asm::{op, Asm, Label};
use crate::soliditylite::CompiledArtifact;
#[cfg(feature = "wallet")]
use crate::soliditylite::ast::{CmpOp, Expr, Facet, StateVarKind, Stmt, Ty};
#[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>),
}
#[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> },
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")]
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::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 },
}
#[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);
}
}
}
}
#[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::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 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,
)
})?;
if let StateVarKind::Mapping { .. } = self.facet.state_vars[idx].kind {
return Err(CompileError::at_code(
codes::TYPE_MISMATCH,
format!("`{name}` is a mapping; it must be indexed (`{name}[key]`)"),
span,
));
}
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,
)),
}
}
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) {
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 } => Ok(LoweredExpr::MapLoad {
base_slot: self.mapping_base_slot(base, *span)?,
key: Box::new(self.lower_expr(key)?),
}),
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, span } => LoweredStmt::Assign(LoweredAssign::Scalar {
slot: r.scalar_slot(name, *span)?,
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::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 body = match &func.body {
Stmt::Return(Expr::StrLit { value, span }) => {
if func.returns != Some(Ty::String) {
return Err(CompileError::at_code(
codes::TYPE_MISMATCH,
"a string literal can only be returned from a `returns (string)` function".to_string(),
*span,
));
}
Body::ConstString(value.clone())
}
_ if func.returns == Some(Ty::String) => {
return Err(CompileError::at_code(
codes::TYPE_MISMATCH,
"a `returns (string)` function must return a string literal 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")]
#[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()
}
}