From 95819a9cce13cc9ffc16f687495341399c61706f Mon Sep 17 00:00:00 2001 From: JobyGitGud <119866693+JobyGitGud@users.noreply.github.com> Date: Wed, 20 May 2026 22:33:08 -0500 Subject: [PATCH] Support tuple parameters in external ABI calls --- codegen/src/codegen/function.rs | 31 ++++++- codegen/src/masm/memory.rs | 51 ++++++++--- codegen/src/visitor/local.rs | 32 ++++++- codegen/src/visitor/mod.rs | 54 +++++++++++- evm/abi/src/arg.rs | 144 +++++++++++++++++++++++++++++++- zink/examples/struct_param.rs | 42 ++++++++++ 6 files changed, 336 insertions(+), 18 deletions(-) create mode 100644 zink/examples/struct_param.rs diff --git a/codegen/src/codegen/function.rs b/codegen/src/codegen/function.rs index 3b5596adb..09d288bee 100644 --- a/codegen/src/codegen/function.rs +++ b/codegen/src/codegen/function.rs @@ -6,7 +6,7 @@ use crate::{ local::{LocalSlot, LocalSlotType, Locals}, masm::MacroAssembler, validator::ValidateThenVisit, - wasm::Env, + wasm::{Env, ToLSBytes}, Buffer, Error, Result, }; use opcodes::ShangHai as OpCode; @@ -60,6 +60,7 @@ impl Function { // codegen.masm.increment_sp(1)?; tracing::debug!(""); codegen.masm._jumpdest()?; + codegen.hydrate_tuple_params()?; } else { // Mock the stack frame for the callee function // @@ -72,6 +73,34 @@ impl Function { Ok(codegen) } + /// Copy tuple ABI parameters from calldata into EVM memory. + fn hydrate_tuple_params(&mut self) -> Result<()> { + let Some(abi) = self.abi.clone() else { + return Ok(()); + }; + + let mut calldata_slot = 0; + for (local_index, input) in abi.inputs.iter().enumerate() { + if let Some(fields) = input.ty.tuple_field_offsets() { + let base = (self.env.reserved() + local_index as u32) as usize * 0x20; + + for (field_offset, field_slot) in fields.into_iter().rev() { + let calldata_offset = 4 + (calldata_slot + field_slot) * 32; + let memory_offset = base + field_offset; + + self.masm.push(&calldata_offset.to_ls_bytes())?; + self.masm._calldataload()?; + self.masm.push(&memory_offset.to_ls_bytes())?; + self.masm._mstore()?; + } + } + + calldata_slot += input.ty.calldata_slots(); + } + + Ok(()) + } + /// Emit function locals /// /// 1. the function parameters. diff --git a/codegen/src/masm/memory.rs b/codegen/src/masm/memory.rs index 66bc8629a..ab9769b3c 100644 --- a/codegen/src/masm/memory.rs +++ b/codegen/src/masm/memory.rs @@ -1,57 +1,82 @@ //! Memory Instructions -use crate::{MacroAssembler, Result}; +use crate::{wasm::ToLSBytes, MacroAssembler, Result}; +use wasmparser::MemArg; impl MacroAssembler { /// Load n bytes to extend self as another number type. /// /// Just for adapting the WASM instructions, this method makes /// no sense for EVM since all of the numbers as U256. - pub(crate) fn _load(&mut self) -> Result<()> { - Ok(()) + pub(crate) fn _load(&mut self, arg: MemArg) -> Result<()> { + self._load_bytes(arg, 4) + } + + /// Load 8 bytes. + pub(crate) fn _load64(&mut self, arg: MemArg) -> Result<()> { + self._load_bytes(arg, 8) + } + + fn _load_bytes(&mut self, arg: MemArg, bytes: usize) -> Result<()> { + if arg.offset > 0 { + self.push(&arg.offset.to_ls_bytes())?; + self._add()?; + } + + self._mload()?; + self.mask_low_bytes(bytes) + } + + fn mask_low_bytes(&mut self, bytes: usize) -> Result<()> { + if bytes >= 32 { + return Ok(()); + } + + self.push(&vec![0xff; bytes])?; + self._and() } /// Load 1 byte to extend self as another number type. /// /// Just for adapting the WASM instructions, this method makes /// no sense for EVM since all of the numbers as U256. - pub(crate) fn _load8(&mut self) -> Result<()> { - Ok(()) + pub(crate) fn _load8(&mut self, arg: MemArg) -> Result<()> { + self._load_bytes(arg, 1) } /// Load 2 bytes to extend self as another number type. /// /// Just for adapting the WASM instructions, this method makes /// no sense for EVM since all of the numbers as U256. - pub(crate) fn _load16(&mut self) -> Result<()> { - Ok(()) + pub(crate) fn _load16(&mut self, arg: MemArg) -> Result<()> { + self._load_bytes(arg, 2) } /// Load 4 bytes to extend self as another number type. /// /// Just for adapting the WASM instructions, this method makes /// no sense for EVM since all of the numbers as U256. - pub(crate) fn _load32(&mut self) -> Result<()> { - Ok(()) + pub(crate) fn _load32(&mut self, arg: MemArg) -> Result<()> { + self._load_bytes(arg, 4) } /// Store n bytes in memory. - pub fn _store(&mut self) -> Result<()> { + pub fn _store(&mut self, _: MemArg) -> Result<()> { todo!() } /// Wrap self to i8 and store 1 byte - pub fn _store8(&mut self) -> Result<()> { + pub fn _store8(&mut self, _: MemArg) -> Result<()> { todo!() } /// Wrap self to i16 and store 2 bytes - pub fn _store16(&mut self) -> Result<()> { + pub fn _store16(&mut self, _: MemArg) -> Result<()> { todo!() } /// Wrap self to i32 and store 4 bytes - pub fn _store32(&mut self) -> Result<()> { + pub fn _store32(&mut self, _: MemArg) -> Result<()> { todo!() } diff --git a/codegen/src/visitor/local.rs b/codegen/src/visitor/local.rs index 63e71a4d3..79e1a44cf 100644 --- a/codegen/src/visitor/local.rs +++ b/codegen/src/visitor/local.rs @@ -45,7 +45,17 @@ impl Function { fn _local_get_calldata(&mut self, local_index: usize) -> Result<()> { let mut offset = self.locals.offset_of(local_index)?; if self.abi.is_some() { - offset = (4 + local_index * 32).to_ls_bytes().to_vec().into(); + if self.is_tuple_abi_param(local_index) { + offset = ((self.env.reserved() + local_index as u32) * 0x20) + .to_ls_bytes() + .to_vec() + .into(); + self.masm.push(&offset)?; + return Ok(()); + } + + let calldata_slot = self.abi_calldata_slot(local_index); + offset = (4 + calldata_slot * 32).to_ls_bytes().to_vec().into(); } self.masm.push(&offset)?; @@ -54,6 +64,26 @@ impl Function { Ok(()) } + fn abi_calldata_slot(&self, local_index: usize) -> usize { + let Some(abi) = &self.abi else { + return local_index; + }; + + abi.inputs + .iter() + .take(local_index) + .map(|input| input.ty.calldata_slots()) + .sum() + } + + fn is_tuple_abi_param(&self, local_index: usize) -> bool { + self.abi + .as_ref() + .and_then(|abi| abi.inputs.get(local_index)) + .and_then(|input| input.ty.tuple_field_offsets()) + .is_some() + } + /// Local get for variables. fn _local_get_var(&mut self, local_index: usize) -> Result<()> { tracing::debug!("Local get variable: {local_index}"); diff --git a/codegen/src/visitor/mod.rs b/codegen/src/visitor/mod.rs index 0081e8634..bc8ae52f0 100644 --- a/codegen/src/visitor/mod.rs +++ b/codegen/src/visitor/mod.rs @@ -37,13 +37,65 @@ macro_rules! impl_visit_operator { /// Implement arithmetic operators for types. macro_rules! map_wasm_operators { + (@basic i32, load, load $arg:ident: MemArg) => { + fn visit_i32_load(&mut self, $arg: MemArg) -> Self::Output { + trace!("i32.load"); + + let before = self.masm.buffer().len(); + self.masm._load32($arg)?; + + let instr = self.masm.buffer()[before..].to_vec(); + self.backtrace.push(instr); + + Ok(()) + } + }; + (@basic i64, load, load $arg:ident: MemArg) => { + fn visit_i64_load(&mut self, $arg: MemArg) -> Self::Output { + trace!("i64.load"); + + let before = self.masm.buffer().len(); + self.masm._load64($arg)?; + + let instr = self.masm.buffer()[before..].to_vec(); + self.backtrace.push(instr); + + Ok(()) + } + }; + (@basic f32, load, load $arg:ident: MemArg) => { + fn visit_f32_load(&mut self, $arg: MemArg) -> Self::Output { + trace!("f32.load"); + + let before = self.masm.buffer().len(); + self.masm._load32($arg)?; + + let instr = self.masm.buffer()[before..].to_vec(); + self.backtrace.push(instr); + + Ok(()) + } + }; + (@basic f64, load, load $arg:ident: MemArg) => { + fn visit_f64_load(&mut self, $arg: MemArg) -> Self::Output { + trace!("f64.load"); + + let before = self.masm.buffer().len(); + self.masm._load64($arg)?; + + let instr = self.masm.buffer()[before..].to_vec(); + self.backtrace.push(instr); + + Ok(()) + } + }; (@basic $ty:tt, $wasm:tt, $evm:tt $($arg:ident: $argty:ty),*) => { paste! { fn [< visit_ $ty _ $wasm >](&mut self $(,$arg: $argty),*) -> Self::Output { trace!("{}.{}", stringify!($ty), stringify!($evm)); let before = self.masm.buffer().len(); - self.masm.[< _ $evm >]()?; + self.masm.[< _ $evm >]($($arg),*)?; let instr = self.masm.buffer()[before..].to_vec(); self.backtrace.push(instr); diff --git a/evm/abi/src/arg.rs b/evm/abi/src/arg.rs index 01f9617e8..69024d74c 100644 --- a/evm/abi/src/arg.rs +++ b/evm/abi/src/arg.rs @@ -111,12 +111,152 @@ impl fmt::Display for Param { } } +impl Param { + /// Number of 32-byte ABI calldata slots used by this parameter. + pub fn calldata_slots(&self) -> usize { + self.tuple_fields().map_or(1, |fields| fields.len()) + } + + /// Byte offsets of tuple fields paired with their ABI calldata slot index. + pub fn tuple_field_offsets(&self) -> Option> { + let fields = self.tuple_fields()?; + let mut offset = 0; + + Some( + fields + .iter() + .enumerate() + .map(|(slot, field)| { + let size = field.abi_byte_size(); + offset = align_to(offset, size); + let field_offset = offset; + offset += size; + (field_offset, slot) + }) + .collect(), + ) + } + + fn signature_type(&self) -> String { + match self { + Param::Unknown(ty) => ty.clone(), + _ => self.as_ref().to_string(), + } + } + + fn tuple_fields(&self) -> Option> { + let Param::Unknown(ty) = self else { + return None; + }; + + let ty = ty.trim(); + if !(ty.starts_with('(') && ty.ends_with(')')) { + return None; + } + + let fields = split_tuple_fields(&ty[1..ty.len() - 1]); + if fields.is_empty() { + return None; + } + + Some( + fields + .into_iter() + .map(|field| Param::from(field.as_str())) + .collect(), + ) + } + + fn abi_byte_size(&self) -> usize { + match self { + Param::Int8 | Param::UInt8 | Param::Bool => 1, + Param::Int16 | Param::UInt16 => 2, + Param::Int32 | Param::UInt32 => 4, + Param::Int64 | Param::UInt64 => 8, + Param::Address => 20, + Param::UInt256 | Param::Bytes | Param::String | Param::Unknown(_) => 32, + } + } +} + +fn align_to(offset: usize, alignment: usize) -> usize { + if alignment == 0 { + return offset; + } + + (offset + alignment - 1) & !(alignment - 1) +} + +fn split_tuple_fields(fields: &str) -> Vec { + let mut depth = 0; + let mut start = 0; + let mut output = Vec::new(); + + for (index, ch) in fields.char_indices() { + match ch { + '(' => depth += 1, + ')' => depth -= 1, + ',' if depth == 0 => { + let field = fields[start..index].trim(); + if !field.is_empty() { + output.push(field.to_string()); + } + start = index + 1; + } + _ => {} + } + } + + let field = fields[start..].trim(); + if !field.is_empty() { + output.push(field.to_string()); + } + + output +} + #[cfg(feature = "syn")] impl From<&Box> for Param { fn from(ty: &Box) -> Self { + Self::from(ty.as_ref()) + } +} + +#[cfg(feature = "syn")] +impl From<&syn::Type> for Param { + fn from(ty: &syn::Type) -> Self { use quote::ToTokens; - let ident = ty.into_token_stream().to_string(); - Self::from(ident.as_str()) + match ty { + syn::Type::Tuple(tuple) => { + let fields = tuple + .elems + .iter() + .map(|field| Param::from(field).signature_type()) + .collect::>() + .join(","); + + Param::Unknown(format!("({fields})")) + } + _ => { + let ident = ty.into_token_stream().to_string().replace(' ', ""); + Self::from(ident.as_str()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::Param; + + #[test] + fn tuple_param_uses_canonical_signature_types() { + let ty: syn::Type = syn::parse_str("(i32, u64)").unwrap(); + let param = Param::from(&ty); + + assert_eq!(param.as_ref(), "(int32,uint64)"); + assert_eq!(param.calldata_slots(), 2); + assert_eq!(param.tuple_field_offsets(), Some(vec![(0, 0), (8, 1)])); } } diff --git a/zink/examples/struct_param.rs b/zink/examples/struct_param.rs new file mode 100644 index 000000000..71613f03e --- /dev/null +++ b/zink/examples/struct_param.rs @@ -0,0 +1,42 @@ +//! Struct parameter example. +#![cfg_attr(target_arch = "wasm32", no_std)] +#![cfg_attr(target_arch = "wasm32", no_main)] + +extern crate zink; + +#[zink::external] +pub fn sum(pair: (i32, i32)) -> i32 { + pair.0 + pair.1 +} + +#[zink::external] +pub fn sum_with_extra(pair: (i32, i32), extra: i32) -> i32 { + pair.0 + pair.1 + extra +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() {} + +#[test] +fn struct_parameter() -> anyhow::Result<()> { + use zint::{Bytes32, Contract}; + + let mut contract = Contract::search("struct_param")?.compile()?; + let info = contract.execute([ + b"sum((int32,int32))".to_vec(), + 1i32.to_bytes32().to_vec(), + 2i32.to_bytes32().to_vec(), + ])?; + + assert_eq!(info.ret, 3i32.to_bytes32()); + + let info = contract.execute([ + b"sum_with_extra((int32,int32),int32)".to_vec(), + 1i32.to_bytes32().to_vec(), + 2i32.to_bytes32().to_vec(), + 4i32.to_bytes32().to_vec(), + ])?; + + assert_eq!(info.ret, 7i32.to_bytes32()); + Ok(()) +}