diff --git a/src/codegen/cfg.rs b/src/codegen/cfg.rs index ac72b5f08..db6b40021 100644 --- a/src/codegen/cfg.rs +++ b/src/codegen/cfg.rs @@ -1667,7 +1667,7 @@ fn function_cfg( cfg.nonpayable = !func.is_payable(); // populate the argument variables - populate_arguments(func, &mut cfg, &mut vartab); + populate_arguments(func, &mut cfg, &mut vartab, ns); // Hold your breath, this is the trickest part of the codegen ahead. // For each contract, the top-level constructor calls the base constructors. The base @@ -1847,10 +1847,20 @@ pub(crate) fn populate_arguments( func: &T, cfg: &mut ControlFlowGraph, vartab: &mut Vartable, + ns: &Namespace, ) { for (i, arg) in func.get_symbol_table().arguments.iter().enumerate() { if let Some(pos) = arg { let var = &func.get_symbol_table().vars[pos]; + let mut runtime_ty = var.ty.clone(); + + if ns.target == Target::Soroban && cfg.public { + runtime_ty = soroban_runtime_arg_ty(&runtime_ty); + if let Some(slot) = vartab.vars.get_mut(pos) { + slot.ty = runtime_ty.clone(); + } + } + cfg.add( vartab, Instr::Set { @@ -1858,7 +1868,7 @@ pub(crate) fn populate_arguments( res: *pos, expr: Expression::FunctionArg { loc: var.id.loc, - ty: var.ty.clone(), + ty: runtime_ty, arg_no: i, }, }, @@ -1867,6 +1877,18 @@ pub(crate) fn populate_arguments( } } +fn soroban_runtime_arg_ty(ty: &Type) -> Type { + match ty { + Type::Array(elem_ty, dims) if dims.last() == Some(&ast::ArrayLength::Dynamic) => { + Type::Array( + Box::new(Type::SorobanHandle(Box::new(elem_ty.as_ref().clone()))), + dims.clone(), + ) + } + _ => ty.clone(), + } +} + /// Populate returns of functions that have named returns pub(crate) fn populate_named_returns( func: &T, diff --git a/src/codegen/dispatch/soroban.rs b/src/codegen/dispatch/soroban.rs index b3f0467d1..05bb648e3 100644 --- a/src/codegen/dispatch/soroban.rs +++ b/src/codegen/dispatch/soroban.rs @@ -52,23 +52,24 @@ pub fn function_dispatch( let mut params = Vec::new(); for p in cfg.params.as_ref() { - let type_ref = Type::Ref(Box::new(p.ty.clone())); - let mut param = ast::Parameter::new_default(type_ref); + let mut param = + ast::Parameter::new_default(Type::SorobanHandle(Box::new(p.ty.clone()))); param.id = p.id.clone(); params.push(param); } let mut returns = Vec::new(); for ret in cfg.returns.as_ref() { - let type_ref = Type::Ref(Box::new(ret.ty.clone())); - let ret = ast::Parameter::new_default(type_ref); + let ret = ast::Parameter::new_default(Type::SorobanHandle(Box::new(ret.ty.clone()))); returns.push(ret); } wrapper_cfg.params = Arc::new(params); if returns.is_empty() { - returns.push(ast::Parameter::new_default(Type::Ref(Box::new(Type::Void)))); + returns.push(ast::Parameter::new_default(Type::SorobanHandle(Box::new( + Type::Void, + )))); } wrapper_cfg.returns = Arc::new(returns); diff --git a/src/codegen/encoding/mod.rs b/src/codegen/encoding/mod.rs index dc48c819a..191738bcd 100644 --- a/src/codegen/encoding/mod.rs +++ b/src/codegen/encoding/mod.rs @@ -287,6 +287,7 @@ pub(crate) trait AbiEncoding { Type::InternalFunction { .. } | Type::Void | Type::BufferPointer + | Type::SorobanHandle(_) | Type::Mapping(..) => unreachable!("This type cannot be encoded"), } } @@ -901,6 +902,7 @@ pub(crate) trait AbiEncoding { | Type::Unreachable | Type::Void | Type::FunctionSelector + | Type::SorobanHandle(_) | Type::Mapping(..) => unreachable!("Type should not appear on an encoded buffer"), } } @@ -1447,6 +1449,7 @@ pub(crate) trait AbiEncoding { | Type::Void | Type::Unreachable | Type::BufferPointer + | Type::SorobanHandle(_) | Type::Mapping(..) => unreachable!("This type cannot be encoded"), Type::UserType(_) | Type::Unresolved | Type::Rational => { unreachable!("Type should not exist in codegen") diff --git a/src/codegen/encoding/soroban_encoding.rs b/src/codegen/encoding/soroban_encoding.rs index 4f6d1a038..2f33a484e 100644 --- a/src/codegen/encoding/soroban_encoding.rs +++ b/src/codegen/encoding/soroban_encoding.rs @@ -4,9 +4,9 @@ use crate::codegen::cfg::InternalCallTy; use crate::codegen::cfg::{ControlFlowGraph, Instr}; use crate::codegen::encoding::create_encoder; use crate::codegen::vartable::Vartable; -use crate::codegen::Expression; use crate::codegen::HostFunctions; -use crate::sema::ast::{Namespace, RetrieveType, StructType, Type, Type::Uint}; +use crate::codegen::{Builtin, Expression}; +use crate::sema::ast::{ArrayLength, Namespace, RetrieveType, StructType, Type, Type::Uint}; use num_bigint::BigInt; use num_traits::Zero; use solang_parser::helpers::CodeLocation; @@ -120,6 +120,10 @@ pub fn soroban_decode_arg( None => { if let Type::Ref(inner_ty) = arg.ty() { *inner_ty + } else if let Type::StorageRef(_, inner) = arg.ty() { + *inner + } else if let Type::SorobanHandle(inner) = arg.ty() { + *inner } else { arg.ty() } @@ -202,8 +206,15 @@ pub fn soroban_decode_arg( Type::Struct(StructType::UserDefined(n)) => { decode_struct(arg, wrapper_cfg, vartab, n, ns, ty) } + Type::Array(elem_ty, _) => { + if let Type::StorageRef(_, _) = arg.ty() { + arg.clone() + } else { + decode_vector(arg, &elem_ty, ns, wrapper_cfg, vartab) + } + } - _ => unimplemented!(), + _ => unimplemented!("unimplemented ty {:#?} in soroban decoder", ty), } } @@ -662,6 +673,17 @@ pub fn soroban_encode_arg( expr: buf, } } + Type::SorobanHandle(_) => Instr::Set { + loc: Loc::Codegen, + res: obj, + expr: item.clone(), + }, + Type::Array(_, _) => Instr::Set { + loc: Loc::Codegen, + res: obj, + expr: encode_vector(item.clone(), cfg, vartab), + }, + _ => todo!("Type not yet supported in soroban encoder: {:?}", item.ty()), }; @@ -856,10 +878,10 @@ fn encode_i256( } fn decode_i128(cfg: &mut ControlFlowGraph, vartab: &mut Vartable, arg: Expression) -> Expression { - let ty = if let Type::Ref(inner_ty) = arg.ty() { - *inner_ty.clone() - } else { - arg.ty() + let ty = match arg.ty() { + Type::Ref(inner_ty) => *inner_ty.clone(), + Type::SorobanHandle(inner_ty) => *inner_ty.clone(), + _ => arg.ty(), }; let ret_var = vartab.temp_anonymous(&ty); @@ -1055,10 +1077,10 @@ fn decode_i128(cfg: &mut ControlFlowGraph, vartab: &mut Vartable, arg: Expressio /// This function handles both Int256 and Uint256 types by retrieving /// the four 64-bit pieces from the host object. fn decode_i256(cfg: &mut ControlFlowGraph, vartab: &mut Vartable, arg: Expression) -> Expression { - let ty = if let Type::Ref(inner_ty) = arg.ty() { - *inner_ty.clone() - } else { - arg.ty() + let ty = match arg.ty() { + Type::Ref(inner_ty) => *inner_ty.clone(), + Type::SorobanHandle(inner_ty) => *inner_ty.clone(), + _ => arg.ty(), }; let ret_var = vartab.temp_anonymous(&ty); @@ -1342,6 +1364,47 @@ fn encode_struct( ret.0 } +/// Encode a linear-memory array into a Soroban VecObject using `VectorNewFromLinearMemory`. +fn encode_vector( + item: Expression, + cfg: &mut ControlFlowGraph, + vartab: &mut Vartable, +) -> Expression { + let len = Expression::Builtin { + loc: item.loc(), + tys: vec![Type::Uint(32)], + kind: Builtin::ArrayLength, + args: vec![item.clone()], + }; + + let data_ptr = Expression::VectorData { + pointer: Box::new(item.clone()), + }; + + // VectorNewFromLinearMemory expects (ptr_u32val, len_u32val). + let encoded_ptr = zext_shift_add(item.loc(), data_ptr, 32, 4); + let encoded_len = zext_shift_add(item.loc(), len, 32, 4); + + let obj = vartab.temp_name("vec_obj", &Type::Uint(64)); + cfg.add( + vartab, + Instr::Call { + res: vec![obj], + return_tys: vec![Type::Uint(64)], + call: InternalCallTy::HostFunction { + name: HostFunctions::VectorNewFromLinearMemory.name().to_string(), + }, + args: vec![encoded_ptr, encoded_len], + }, + ); + + Expression::Variable { + loc: item.loc(), + ty: Type::Uint(64), + var_no: obj, + } +} + /// Decode a struct from soroban encoding. Struct fields are laid out sequentially in a buffer, where each field is 64 bits long. fn decode_struct( mut item: Expression, @@ -1386,3 +1449,123 @@ fn decode_struct( values: members, } } + +fn zext_shift_add(loc: pt::Loc, value: Expression, shift: u64, tag: u64) -> Expression { + let shifted = Expression::ShiftLeft { + loc, + ty: Type::Uint(64), + left: Box::new(Expression::ZeroExt { + loc, + ty: Type::Uint(64), + expr: Box::new(value), + }), + right: Box::new(Expression::NumberLiteral { + loc, + ty: Type::Uint(64), + value: BigInt::from(shift), + }), + }; + + Expression::Add { + loc, + ty: Type::Uint(64), + left: Box::new(shifted), + right: Box::new(Expression::NumberLiteral { + loc, + ty: Type::Uint(64), + value: BigInt::from(tag), + }), + overflowing: false, + } +} +fn decode_vector( + vec_object: Expression, + elem_ty: &Type, + _ns: &Namespace, + cfg: &mut ControlFlowGraph, + vartab: &mut Vartable, +) -> Expression { + let vec_len = vartab.temp_name("vec_len", &Type::Uint(64)); + + // Get the length of the vector by VecLen (returns U32Val in a 64-bit host object). + let get_len_instr = Instr::Call { + res: vec![vec_len], + return_tys: vec![Type::Uint(64)], + call: crate::codegen::cfg::InternalCallTy::HostFunction { + name: HostFunctions::VecLen.name().to_string(), + }, + args: vec![vec_object.clone()], + }; + + cfg.add(vartab, get_len_instr); + + let len_var = Expression::Variable { + loc: pt::Loc::Codegen, + ty: Type::Uint(64), + var_no: vec_len, + }; + + // Decode vector length from U32Val payload. + let decoded_len_u64 = Expression::ShiftRight { + loc: pt::Loc::Codegen, + ty: Type::Uint(64), + left: Box::new(len_var.clone()), + right: Box::new(Expression::NumberLiteral { + loc: pt::Loc::Codegen, + ty: Type::Uint(64), + value: BigInt::from(32), + }), + signed: false, + }; + + let decoded_len_u32 = Expression::Trunc { + loc: Loc::Codegen, + ty: Type::Uint(32), + expr: Box::new(decoded_len_u64.clone()), + }; + + let decoded_array_ty = Type::Array( + Box::new(Type::SorobanHandle(Box::new(elem_ty.clone()))), + vec![ArrayLength::Dynamic], + ); + let decoded_buffer_var = vartab.temp_name("vector_data_decoded", &decoded_array_ty); + cfg.add( + vartab, + Instr::Set { + loc: Loc::Codegen, + res: decoded_buffer_var, + expr: Expression::AllocDynamicBytes { + loc: Loc::Codegen, + ty: decoded_array_ty.clone(), + size: Box::new(decoded_len_u32.clone()), + initializer: None, + }, + }, + ); + + let decoded_buffer = Expression::Variable { + loc: Loc::Codegen, + ty: decoded_array_ty, + var_no: decoded_buffer_var, + }; + + let data_location = Expression::VectorData { + pointer: decoded_buffer.clone().into(), + }; + + let data_location = zext_shift_add(Loc::Codegen, data_location, 32, 4); + let unused = vartab.temp_name("unused_void_return", &Type::Uint(64)); + // VecUnpack expects vector object, output pointer (U32Val), and element count (U32Val). + let unpack_instr = Instr::Call { + res: vec![unused], + return_tys: vec![Type::Uint(64)], + call: crate::codegen::cfg::InternalCallTy::HostFunction { + name: HostFunctions::VecUnpackToLinearMemory.name().to_string(), + }, + args: vec![vec_object.clone(), data_location, len_var], + }; + + cfg.add(vartab, unpack_instr); + + decoded_buffer +} diff --git a/src/codegen/expression.rs b/src/codegen/expression.rs index 76cfcec09..6dbed71ad 100644 --- a/src/codegen/expression.rs +++ b/src/codegen/expression.rs @@ -538,7 +538,7 @@ pub fn expression( }, Type::Array(_, dim) => match dim.last().unwrap() { ArrayLength::Dynamic => { - if ns.target == Target::Solana { + if ns.target == Target::Solana || ns.target == Target::Soroban { Expression::StorageArrayLength { loc: *loc, ty: ty.clone(), @@ -857,11 +857,30 @@ pub fn expression( from: from.clone(), expr: Box::new(expression(expr, cfg, contract_no, func, ns, vartab, opt)), }, - ast::Expression::Load { loc, ty, expr: e } => Expression::Load { - loc: *loc, - ty: ty.clone(), - expr: Box::new(expression(e, cfg, contract_no, func, ns, vartab, opt)), - }, + ast::Expression::Load { loc, ty, expr: e } => { + let expr = Box::new(expression(e, cfg, contract_no, func, ns, vartab, opt)); + + // Soroban lazy decode path: if memory contains encoded handles, decode on demand. + if ns.target == Target::Soroban { + if let Type::Ref(inner) = expr.ty() { + if matches!(inner.as_ref(), Type::SorobanHandle(_)) { + let load_handle = Expression::Load { + loc: *loc, + ty: inner.as_ref().clone(), + expr: expr.clone(), + }; + + return soroban_decode_arg(load_handle, cfg, vartab, ns, None); + } + } + } + + Expression::Load { + loc: *loc, + ty: ty.clone(), + expr, + } + } // for some built-ins, we have to inline special case code ast::Expression::Builtin { kind: ast::Builtin::UserTypeWrap, @@ -1143,7 +1162,11 @@ pub fn expression( } ast::Expression::Variable { loc, ty, var_no } => Expression::Variable { loc: *loc, - ty: ty.clone(), + ty: vartab + .vars + .get(var_no) + .map(|v| v.ty.clone()) + .unwrap_or_else(|| ty.clone()), var_no: *var_no, }, ast::Expression::GetRef { loc, ty, expr: exp } => Expression::GetRef { @@ -2464,7 +2487,7 @@ fn expr_builtin( // }; // let auth_context = auth::InvokerContractAuthEntry::Contract(x); // Most of the logic done here is just to encode the above struct as the host expects it. - // FIXME: This uses a series of MapNewFromLinearMemory, and multiple inserts to create the struct. + // FIXME: This uses a series of MapNew, and multiple inserts to create the struct. // This is not efficient and should be optimized. // Instead, we should use MapNewFromLinearMemory to create the struct in one go. ast::Builtin::AuthAsCurrContract => { @@ -3308,17 +3331,30 @@ pub fn assign_single( ); } Type::Ref(_) => { - cfg.add( - vartab, - Instr::Store { - dest, - data: Expression::Variable { + let data = if ns.target == Target::Soroban + && matches!( + dest.ty(), + Type::Ref(inner) if matches!(inner.as_ref(), Type::SorobanHandle(_)) + ) { + soroban_encode_arg( + Expression::Variable { loc: Loc::Codegen, ty: ty.clone(), var_no: pos, }, - }, - ); + cfg, + vartab, + ns, + ) + } else { + Expression::Variable { + loc: Loc::Codegen, + ty: ty.clone(), + var_no: pos, + } + }; + + cfg.add(vartab, Instr::Store { dest, data }); } _ => unreachable!(), } @@ -3858,7 +3894,7 @@ fn array_subscript( Type::Array(..) => match array_ty.array_length() { None => { if let Type::StorageRef(..) = array_ty { - if ns.target == Target::Solana { + if ns.target == Target::Solana || ns.target == Target::Soroban { Expression::StorageArrayLength { loc: *loc, ty: ns.storage_type(), @@ -3866,22 +3902,21 @@ fn array_subscript( elem_ty: array_ty.storage_array_elem().deref_into(), } } else { - // TODO(Soroban): Storage type here is None, since arrays are not yet supported in Soroban - let array_length = load_storage( - loc, - &Type::Uint(256), - array.clone(), - cfg, - vartab, - None, - ns, - ); - - array = Expression::Keccak256 { - loc: *loc, - ty: Type::Uint(256), - exprs: vec![array], + let ty = if ns.target == Target::Soroban { + Type::Uint(64) + } else { + ns.storage_type() }; + // TODO(Soroban): Storage type here is None, since arrays are not yet supported in Soroban + let array_length = + load_storage(loc, &ty, array.clone(), cfg, vartab, None, ns); + if ns.target != Target::Soroban { + array = Expression::Keccak256 { + loc: *loc, + ty: Type::Uint(256), + exprs: vec![array], + }; + } array_length } @@ -4062,6 +4097,25 @@ fn array_subscript( let elem_ty = ty.storage_array_elem(); let slot_ty = ns.storage_type(); + if ns.target == Target::Soroban { + let index = index.cast(&Type::Uint(64), ns); + + let index = if elem_ty.is_reference_type(ns) { + soroban_encode_arg(index, cfg, vartab, ns) + } else { + index + }; + + let val = Expression::Subscript { + loc: *loc, + ty: elem_ty.clone(), + array_ty: array_ty.clone(), + expr: Box::new(array), + index: Box::new(index), + }; + return val; + } + if ns.target == Target::Solana { if ty.array_length().is_some() && ty.is_sparse_solana(ns) { let index = Expression::Variable { @@ -4172,11 +4226,29 @@ fn array_subscript( ) } } else { - match array_ty.deref_memory() { + // Use runtime array type on Soroban so lowered wrapper args can carry + // Array(SorobanHandle(_), ..) representation. + let mut effective_array_ty = array_ty.clone(); + let mut effective_elem_ty = elem_ty.clone(); + + if ns.target == Target::Soroban { + if let Type::Array(runtime_elem_ty, runtime_dims) = array.ty().deref_any() { + if matches!(runtime_elem_ty.as_ref(), Type::SorobanHandle(_)) { + effective_array_ty = Type::Array(runtime_elem_ty.clone(), runtime_dims.clone()); + effective_elem_ty = if matches!(elem_ty, Type::Ref(_)) { + Type::Ref(runtime_elem_ty.clone()) + } else { + runtime_elem_ty.as_ref().clone() + }; + } + } + } + + match effective_array_ty.deref_memory() { Type::DynamicBytes | Type::Array(..) | Type::Slice(_) => Expression::Subscript { loc: *loc, - ty: elem_ty.clone(), - array_ty: array_ty.clone(), + ty: effective_elem_ty, + array_ty: effective_array_ty, expr: Box::new(array), index: Box::new(Expression::Variable { loc: index_loc, diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 284d4b232..2192348f8 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -14,6 +14,7 @@ mod reaching_definitions; pub mod revert; mod solana_accounts; mod solana_deploy; +mod soroban; mod statements; mod storage; mod strength_reduce; @@ -99,12 +100,15 @@ pub enum HostFunctions { PutContractData, GetContractData, HasContractData, + DeleteContractData, ExtendContractDataTtl, ExtendCurrentContractInstanceAndCodeTtl, LogFromLinearMemory, SymbolNewFromLinearMemory, VectorNew, VectorNewFromLinearMemory, + VecUnpackToLinearMemory, + VecLen, MapNewFromLinearMemory, Call, ObjToU64, @@ -130,6 +134,9 @@ pub enum HostFunctions { MapNew, MapPut, VecPushBack, + VecPopBack, + VecGet, + VecPut, StringNewFromLinearMemory, StrKeyToAddr, GetCurrentContractAddress, @@ -144,12 +151,14 @@ impl HostFunctions { HostFunctions::PutContractData => "l._", HostFunctions::GetContractData => "l.1", HostFunctions::HasContractData => "l.0", + HostFunctions::DeleteContractData => "l.2", HostFunctions::ExtendContractDataTtl => "l.7", HostFunctions::ExtendCurrentContractInstanceAndCodeTtl => "l.8", HostFunctions::LogFromLinearMemory => "x._", HostFunctions::SymbolNewFromLinearMemory => "b.j", HostFunctions::VectorNew => "v._", HostFunctions::VectorNewFromLinearMemory => "v.g", + HostFunctions::VecUnpackToLinearMemory => "v.h", HostFunctions::Call => "d._", HostFunctions::ObjToU64 => "i.0", HostFunctions::ObjFromU64 => "i._", @@ -181,6 +190,10 @@ impl HostFunctions { HostFunctions::BytesNewFromLinearMemory => "b.3", HostFunctions::BytesLen => "b.8", HostFunctions::BytesCopyToLinearMemory => "b.1", + HostFunctions::VecLen => "v.3", + HostFunctions::VecPopBack => "v.7", + HostFunctions::VecGet => "v.1", + HostFunctions::VecPut => "v.0", } } } @@ -360,31 +373,37 @@ fn storage_initializer(contract_no: usize, ns: &mut Namespace, opt: &Options) -> for layout in &ns.contracts[contract_no].layout { let var = &ns.contracts[layout.contract_no].variables[layout.var_no]; - if let Some(init) = &var.initializer { - let storage = ns.contracts[contract_no].get_storage_slot( - pt::Loc::Codegen, - layout.contract_no, - layout.var_no, - ns, - None, - ); + let mut value = if let Some(init) = &var.initializer { + expression(init, &mut cfg, contract_no, None, ns, &mut vartab, opt) + } else if ns.target == Target::Soroban && var.ty.is_dynamic_memory() { + soroban::soroban_vec_new(&var.loc, &var.ty, &mut cfg, &mut vartab) + } else { + continue; + }; - let mut value = expression(init, &mut cfg, contract_no, None, ns, &mut vartab, opt); + let storage = ns.contracts[contract_no].get_storage_slot( + pt::Loc::Codegen, + layout.contract_no, + layout.var_no, + ns, + None, + ); - if ns.target == Target::Soroban { - value = soroban_encode_arg(value, &mut cfg, &mut vartab, ns); - } + //let mut value = expression(init, &mut cfg, contract_no, None, ns, &mut vartab, opt); - cfg.add( - &mut vartab, - Instr::SetStorage { - value, - ty: var.ty.clone(), - storage, - storage_type: var.storage_type.clone(), - }, - ); + if ns.target == Target::Soroban { + value = soroban_encode_arg(value, &mut cfg, &mut vartab, ns); } + + cfg.add( + &mut vartab, + Instr::SetStorage { + value, + ty: var.ty.clone(), + storage, + storage_type: var.storage_type.clone(), + }, + ); } cfg.add(&mut vartab, Instr::Return { value: Vec::new() }); diff --git a/src/codegen/soroban.rs b/src/codegen/soroban.rs new file mode 100644 index 000000000..62037c46a --- /dev/null +++ b/src/codegen/soroban.rs @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: Apache-2.0 + +use super::cfg::{ControlFlowGraph, Instr, InternalCallTy}; +use super::encoding::soroban_encoding::soroban_encode_arg; +use super::expression::{expression, load_storage}; +use super::vartable::Vartable; +use super::Options; +use crate::codegen::{Expression, HostFunctions}; +use crate::sema::ast; +use crate::sema::ast::{Function, Namespace, RetrieveType, Type}; +use solang_parser::pt; + +fn soroban_vec_handle_ty(vec_ty: &Type) -> Type { + let inner_ty = if let Type::StorageRef(_, inner) = vec_ty { + inner.as_ref().clone() + } else { + vec_ty.clone() + }; + + Type::SorobanHandle(Box::new(inner_ty)) +} + +pub(super) fn soroban_vec_new( + loc: &pt::Loc, + vec_ty: &Type, + cfg: &mut ControlFlowGraph, + vartab: &mut Vartable, +) -> Expression { + let handle_ty = soroban_vec_handle_ty(vec_ty); + let empty_vec_no = vartab.temp_name("soroban_vec_new", &handle_ty); + + let empty_vec_var = Expression::Variable { + loc: *loc, + ty: handle_ty.clone(), + var_no: empty_vec_no, + }; + + cfg.add( + vartab, + Instr::Call { + call: InternalCallTy::HostFunction { + name: HostFunctions::VectorNew.name().to_string(), + }, + args: vec![], + return_tys: vec![handle_ty], + res: vec![empty_vec_no], + }, + ); + + empty_vec_var +} + +fn soroban_vec_push_back( + loc: &pt::Loc, + vec_obj: Expression, + vec_ty: &Type, + value: Expression, + cfg: &mut ControlFlowGraph, + ns: &Namespace, + vartab: &mut Vartable, +) -> Expression { + let value_encoded = soroban_encode_arg(value, cfg, vartab, ns); + let handle_ty = soroban_vec_handle_ty(vec_ty); + + let new_vec_no = vartab.temp_name("soroban_vec_push", &handle_ty); + + let new_vec_var = Expression::Variable { + loc: *loc, + ty: handle_ty.clone(), + var_no: new_vec_no, + }; + + let instr = Instr::Call { + res: vec![new_vec_no], + return_tys: vec![handle_ty], + call: InternalCallTy::HostFunction { + name: HostFunctions::VecPushBack.name().to_string(), + }, + args: vec![vec_obj, value_encoded], + }; + + cfg.add(vartab, instr); + + new_vec_var +} + +fn soroban_vec_pop_back( + loc: &pt::Loc, + vec_obj: Expression, + vec_ty: &Type, + cfg: &mut ControlFlowGraph, + vartab: &mut Vartable, +) -> Expression { + let handle_ty = soroban_vec_handle_ty(vec_ty); + let new_vec_no = vartab.temp_name("soroban_vec_pop", &handle_ty); + + let new_vec_var = Expression::Variable { + loc: *loc, + ty: handle_ty.clone(), + var_no: new_vec_no, + }; + + let instr = Instr::Call { + res: vec![new_vec_no], + return_tys: vec![handle_ty], + call: InternalCallTy::HostFunction { + name: HostFunctions::VecPopBack.name().to_string(), + }, + args: vec![vec_obj], + }; + + cfg.add(vartab, instr); + + new_vec_var +} + +pub(super) fn soroban_storage_push( + loc: &pt::Loc, + args: &[ast::Expression], + cfg: &mut ControlFlowGraph, + contract_no: usize, + func: Option<&Function>, + ns: &Namespace, + vartab: &mut Vartable, + opt: &Options, +) -> Expression { + // Storage wrapper: evaluate storage key/value and load vec object from storage. + let var_expr = expression(&args[0], cfg, contract_no, func, ns, vartab, opt); + let value = expression(&args[1], cfg, contract_no, func, ns, vartab, opt); + let vec_ty = args[0].ty(); + + let old_vec_obj = load_storage(loc, &vec_ty, var_expr.clone(), cfg, vartab, None, ns); + let new_vec_var = soroban_vec_push_back(loc, old_vec_obj, &vec_ty, value, cfg, ns, vartab); + + // Storage wrapper: store updated vec object. + let store_instr = Instr::SetStorage { + ty: vec_ty, + value: new_vec_var.clone(), + storage: var_expr.clone(), + storage_type: None, + }; + + cfg.add(vartab, store_instr); + + var_expr +} + +pub(super) fn soroban_storage_pop( + loc: &pt::Loc, + args: &[ast::Expression], + return_ty: &Type, + cfg: &mut ControlFlowGraph, + contract_no: usize, + func: Option<&Function>, + ns: &Namespace, + vartab: &mut Vartable, + opt: &Options, +) -> Expression { + // Storage wrapper: evaluate storage key and load vec object from storage. + let var_expr = expression(&args[0], cfg, contract_no, func, ns, vartab, opt); + let vec_ty = args[0].ty(); + + let old_vec_obj = load_storage(loc, &vec_ty, var_expr.clone(), cfg, vartab, None, ns); + let new_vec_var = soroban_vec_pop_back(loc, old_vec_obj, &vec_ty, cfg, vartab); + let new_vec_no = match &new_vec_var { + Expression::Variable { var_no, .. } => *var_no, + _ => unreachable!(), + }; + + // Storage wrapper: store updated vec object. + let store_instr = Instr::SetStorage { + ty: vec_ty, + value: new_vec_var.clone(), + storage: var_expr.clone(), + storage_type: None, + }; + + cfg.add(vartab, store_instr); + + Expression::Variable { + loc: *loc, + ty: return_ty.clone(), + var_no: new_vec_no, + } +} diff --git a/src/codegen/storage.rs b/src/codegen/storage.rs index ae06ab7ca..bb477c36c 100644 --- a/src/codegen/storage.rs +++ b/src/codegen/storage.rs @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +use crate::codegen::encoding::soroban_encoding::soroban_encode_arg; use crate::codegen::Expression; use crate::sema::ast; +use crate::Target; use num_bigint::BigInt; use num_traits::FromPrimitive; use num_traits::One; @@ -13,6 +15,7 @@ use super::revert::SolidityError; use super::Options; use super::{ cfg::{ControlFlowGraph, Instr}, + soroban::{soroban_storage_pop, soroban_storage_push}, vartable::Vartable, }; use crate::codegen::revert::{assert_failure, log_runtime_error}; @@ -92,6 +95,20 @@ pub fn storage_slots_array_push( vartab: &mut Vartable, opt: &Options, ) -> Expression { + let inner_ty = if let Type::StorageRef(_, inner) = args[0].ty() { + if let Type::Array(elem_ty, _) = inner.deref_any() { + elem_ty.clone() + } else { + panic!("expected storage array type"); + } + } else { + panic!("expected storage reference type"); + }; + + if ns.target == Target::Soroban && !inner_ty.is_reference_type(ns) { + return soroban_storage_push(loc, args, cfg, contract_no, func, ns, vartab, opt); + } + // set array+length to val_expr let slot_ty = ns.storage_type(); let length_pos = vartab.temp_anonymous(&slot_ty); @@ -114,31 +131,55 @@ pub fn storage_slots_array_push( let entry_pos = vartab.temp_anonymous(&slot_ty); + let array_offset = if ns.target == Target::Soroban { + let index = Expression::Variable { + loc: *loc, + ty: slot_ty.clone(), + var_no: length_pos, + }; + + let index_encoded = soroban_encode_arg(index, cfg, vartab, ns); + + Expression::Subscript { + loc: *loc, + ty: elem_ty.clone(), + array_ty: Type::StorageRef(false, Box::new(elem_ty.clone())), + expr: Box::new(var_expr.clone()), + index: Box::new(index_encoded), + } + } else { + array_offset( + loc, + Expression::Keccak256 { + loc: *loc, + ty: slot_ty.clone(), + exprs: vec![var_expr.clone()], + }, + Expression::Variable { + loc: *loc, + ty: slot_ty.clone(), + var_no: length_pos, + }, + elem_ty.clone(), + ns, + ) + }; + cfg.add( vartab, Instr::Set { loc: pt::Loc::Codegen, res: entry_pos, - expr: array_offset( - loc, - Expression::Keccak256 { - loc: *loc, - ty: slot_ty.clone(), - exprs: vec![var_expr.clone()], - }, - Expression::Variable { - loc: *loc, - ty: slot_ty.clone(), - var_no: length_pos, - }, - elem_ty.clone(), - ns, - ), + expr: array_offset, }, ); if args.len() == 2 { - let value = expression(&args[1], cfg, contract_no, func, ns, vartab, opt); + let mut value = expression(&args[1], cfg, contract_no, func, ns, vartab, opt); + + if ns.target == Target::Soroban { + value = soroban_encode_arg(value, cfg, vartab, ns); + } cfg.add( vartab, @@ -156,7 +197,7 @@ pub fn storage_slots_array_push( } // increase length - let new_length = Expression::Add { + let mut new_length = Expression::Add { loc: *loc, ty: slot_ty.clone(), overflowing: true, @@ -172,6 +213,10 @@ pub fn storage_slots_array_push( }), }; + if ns.target == Target::Soroban { + new_length = soroban_encode_arg(new_length, cfg, vartab, ns); + } + cfg.add( vartab, Instr::SetStorage { @@ -205,6 +250,20 @@ pub fn storage_slots_array_pop( vartab: &mut Vartable, opt: &Options, ) -> Expression { + if ns.target == Target::Soroban { + return soroban_storage_pop( + loc, + args, + return_ty, + cfg, + contract_no, + func, + ns, + vartab, + opt, + ); + } + // set array+length to val_expr let slot_ty = ns.storage_type(); let length_ty = ns.storage_type(); @@ -263,26 +322,32 @@ pub fn storage_slots_array_pop( cfg.set_basic_block(has_elements); let new_length = vartab.temp_anonymous(&slot_ty); + let mut subtract = Expression::Subtract { + loc: *loc, + ty: length_ty.clone(), + overflowing: true, + left: Box::new(Expression::Variable { + loc: *loc, + ty: length_ty.clone(), + var_no: length_pos, + }), + right: Box::new(Expression::NumberLiteral { + loc: *loc, + ty: length_ty.clone(), + value: BigInt::one(), + }), + }; + + if ns.target == Target::Soroban { + subtract = soroban_encode_arg(subtract, cfg, vartab, ns); + } + cfg.add( vartab, Instr::Set { loc: pt::Loc::Codegen, res: new_length, - expr: Expression::Subtract { - loc: *loc, - ty: length_ty.clone(), - overflowing: true, - left: Box::new(Expression::Variable { - loc: *loc, - ty: length_ty.clone(), - var_no: length_pos, - }), - right: Box::new(Expression::NumberLiteral { - loc: *loc, - ty: length_ty, - value: BigInt::one(), - }), - }, + expr: subtract, }, ); @@ -291,26 +356,46 @@ pub fn storage_slots_array_pop( let elem_ty = ty.storage_array_elem().deref_any().clone(); let entry_pos = vartab.temp_anonymous(&slot_ty); + let array_offset_expr = if ns.target == Target::Soroban { + let index = Expression::Variable { + loc: *loc, + ty: slot_ty.clone(), + var_no: length_pos, + }; + + let index_encoded = soroban_encode_arg(index, cfg, vartab, ns); + + Expression::Subscript { + loc: *loc, + ty: elem_ty.clone(), + array_ty: Type::StorageRef(false, Box::new(elem_ty.clone())), + expr: Box::new(var_expr.clone()), + index: Box::new(index_encoded), + } + } else { + array_offset( + loc, + Expression::Keccak256 { + loc: *loc, + ty: slot_ty.clone(), + exprs: vec![var_expr.clone()], + }, + Expression::Variable { + loc: *loc, + ty: slot_ty.clone(), + var_no: new_length, + }, + elem_ty.clone(), + ns, + ) + }; + cfg.add( vartab, Instr::Set { loc: pt::Loc::Codegen, res: entry_pos, - expr: array_offset( - loc, - Expression::Keccak256 { - loc: *loc, - ty: slot_ty.clone(), - exprs: vec![var_expr.clone()], - }, - Expression::Variable { - loc: *loc, - ty: slot_ty.clone(), - var_no: new_length, - }, - elem_ty.clone(), - ns, - ), + expr: array_offset_expr, }, ); @@ -391,6 +476,10 @@ pub fn array_push( vartab: &mut Vartable, opt: &Options, ) -> Expression { + if ns.target == Target::Soroban { + return soroban_storage_push(loc, args, cfg, contract_no, func, ns, vartab, opt); + } + let storage = expression(&args[0], cfg, contract_no, func, ns, vartab, opt); let mut ty = args[0].ty().storage_array_elem(); diff --git a/src/codegen/yul/mod.rs b/src/codegen/yul/mod.rs index 6c7314431..dc4dacf04 100644 --- a/src/codegen/yul/mod.rs +++ b/src/codegen/yul/mod.rs @@ -74,7 +74,7 @@ fn yul_function_cfg( cfg.nonpayable = true; // populate the arguments - populate_arguments(yul_func, &mut cfg, &mut vartab); + populate_arguments(yul_func, &mut cfg, &mut vartab, ns); // populate the returns, if any populate_named_returns(yul_func, ns, &mut cfg, &mut vartab); diff --git a/src/emit/binary.rs b/src/emit/binary.rs index 071aa53d4..fa28f823d 100644 --- a/src/emit/binary.rs +++ b/src/emit/binary.rs @@ -886,23 +886,25 @@ impl<'a> Binary<'a> { .add_function(&name, ret_ty.fn_type(&[ty.into(), ty.into()], false), None) } - /// Return the llvm type for a variable holding the type, not the type itself - pub(crate) fn llvm_var_ty(&self, ty: &Type) -> BasicTypeEnum<'a> { - if self.ns.target == Target::Soroban { - return self.llvm_type(ty); - } - - let llvm_ty = self.llvm_type(ty); + fn var_ty_uses_pointer_storage(&self, ty: &Type) -> bool { match ty.deref_memory() { Type::Struct(_) | Type::Array(..) | Type::DynamicBytes - | Type::String - | Type::ExternalFunction { .. } => self - .context + | Type::ExternalFunction { .. } => true, + Type::String => self.ns.target != Target::Soroban, + _ => false, + } + } + + /// Return the llvm type for a variable holding the type, not the type itself + pub(crate) fn llvm_var_ty(&self, ty: &Type) -> BasicTypeEnum<'a> { + if self.var_ty_uses_pointer_storage(ty) { + self.context .ptr_type(AddressSpace::default()) - .as_basic_type_enum(), - _ => llvm_ty, + .as_basic_type_enum() + } else { + self.llvm_type(ty) } } @@ -999,15 +1001,10 @@ impl<'a> Binary<'a> { ) .as_basic_type_enum(), Type::Mapping(..) => self.llvm_type(&self.ns.storage_type()), - Type::Ref(..) => { - if self.ns.target == Target::Soroban { - return BasicTypeEnum::IntType(self.context.i64_type()); - } - - self.context - .ptr_type(AddressSpace::default()) - .as_basic_type_enum() - } + Type::Ref(..) => self + .context + .ptr_type(AddressSpace::default()) + .as_basic_type_enum(), Type::StorageRef(..) => self.llvm_type(&self.ns.storage_type()), Type::InternalFunction { .. } => { BasicTypeEnum::PointerType(self.context.ptr_type(AddressSpace::default())) @@ -1038,6 +1035,7 @@ impl<'a> Binary<'a> { Type::FunctionSelector => { self.llvm_type(&Type::Bytes(self.ns.target.selector_length())) } + Type::SorobanHandle(_) => BasicTypeEnum::IntType(self.context.i64_type()), // Soroban functions always return a 64 bit value. Type::Void => { if self.ns.target == Target::Soroban { diff --git a/src/emit/instructions.rs b/src/emit/instructions.rs index c212377b0..96dfef521 100644 --- a/src/emit/instructions.rs +++ b/src/emit/instructions.rs @@ -115,9 +115,14 @@ pub(super) fn process_instruction<'a, T: TargetRuntime<'a> + ?Sized>( storage_type, } => { let mut slot = expression(target, bin, storage, &w.vars, function).into_int_value(); + let slot_ty = if let Expression::Subscript { array_ty, .. } = storage { + Some(array_ty) + } else { + None + }; w.vars.get_mut(res).unwrap().value = - target.storage_load(bin, ty, &mut slot, function, storage_type); + target.storage_load(bin, ty, &mut slot, slot_ty, function, storage_type); } Instr::ClearStorage { ty, storage } => { let mut slot = expression(target, bin, storage, &w.vars, function).into_int_value(); @@ -133,8 +138,22 @@ pub(super) fn process_instruction<'a, T: TargetRuntime<'a> + ?Sized>( let value = expression(target, bin, value, &w.vars, function); let mut slot = expression(target, bin, storage, &w.vars, function).into_int_value(); + let slot_ty = if let Expression::Subscript { array_ty, .. } = storage { + Some(array_ty) + } else { + None + }; - target.storage_store(bin, ty, true, &mut slot, value, function, storage_type); + target.storage_store( + bin, + ty, + true, + &mut slot, + slot_ty, + value, + function, + storage_type, + ); } Instr::SetStorageBytes { storage, diff --git a/src/emit/mod.rs b/src/emit/mod.rs index 04e9cc266..2665b0f34 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -83,6 +83,7 @@ pub trait TargetRuntime<'a> { bin: &Binary<'a>, ty: &ast::Type, slot: &mut IntValue<'a>, + slot_ty: Option<&ast::Type>, function: FunctionValue<'a>, storage_type: &Option, ) -> BasicValueEnum<'a>; @@ -91,9 +92,10 @@ pub trait TargetRuntime<'a> { fn storage_store( &self, bin: &Binary<'a>, - ty: &ast::Type, + elem_ty: &ast::Type, existing: bool, slot: &mut IntValue<'a>, + slot_ty: Option<&ast::Type>, dest: BasicValueEnum<'a>, function: FunctionValue<'a>, storage_type: &Option, diff --git a/src/emit/polkadot/target.rs b/src/emit/polkadot/target.rs index 5de4f6291..17de405bc 100644 --- a/src/emit/polkadot/target.rs +++ b/src/emit/polkadot/target.rs @@ -1440,6 +1440,7 @@ impl<'a> TargetRuntime<'a> for PolkadotTarget { bin: &Binary<'a>, ty: &Type, slot: &mut IntValue<'a>, + _slot_ty: Option<&Type>, function: FunctionValue, _storage_type: &Option, ) -> BasicValueEnum<'a> { @@ -1456,6 +1457,7 @@ impl<'a> TargetRuntime<'a> for PolkadotTarget { ty: &Type, _existing: bool, slot: &mut IntValue<'a>, + _slot_ty: Option<&Type>, dest: BasicValueEnum<'a>, function: FunctionValue<'a>, _: &Option, diff --git a/src/emit/solana/target.rs b/src/emit/solana/target.rs index 545d228b0..289dd653e 100644 --- a/src/emit/solana/target.rs +++ b/src/emit/solana/target.rs @@ -390,7 +390,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget { .unwrap(); if let Some(val) = val { - self.storage_store(bin, ty, false, &mut new_offset, val, function, &None); + self.storage_store(bin, ty, false, &mut new_offset, None, val, function, &None); } if ty.is_reference_type(bin.ns) { @@ -480,7 +480,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget { let mut old_elem_offset = bin.builder.build_int_add(offset, new_length, "").unwrap(); let val = if load { - Some(self.storage_load(bin, ty, &mut old_elem_offset, function, &None)) + Some(self.storage_load(bin, ty, &mut old_elem_offset, None, function, &None)) } else { None }; @@ -568,6 +568,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget { bin: &Binary<'a>, ty: &ast::Type, slot: &mut IntValue<'a>, + _slot_ty: Option<&ast::Type>, function: FunctionValue<'a>, _storage_type: &Option, ) -> BasicValueEnum<'a> { @@ -661,7 +662,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget { ) .unwrap(); - let val = self.storage_load(bin, &field.ty, &mut offset, function, &None); + let val = self.storage_load(bin, &field.ty, &mut offset, None, function, &None); let elem = unsafe { bin.builder @@ -772,6 +773,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget { bin, elem_ty.deref_memory(), &mut offset_val, + None, function, &None, ); @@ -817,6 +819,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget { ty: &ast::Type, existing: bool, offset: &mut IntValue<'a>, + _slot_ty: Option<&ast::Type>, val: BasicValueEnum<'a>, function: FunctionValue<'a>, _: &Option, @@ -1103,6 +1106,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget { elem_ty.deref_any(), false, // storage already freed with storage_free &mut offset_val, + None, if elem_ty.deref_memory().is_fixed_reference_type(bin.ns) { elem.into() } else { @@ -1173,6 +1177,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget { &field.ty, existing, &mut offset, + None, if field.ty.is_fixed_reference_type(bin.ns) { elem.into() } else { diff --git a/src/emit/soroban/mod.rs b/src/emit/soroban/mod.rs index 52686f19e..97375a893 100644 --- a/src/emit/soroban/mod.rs +++ b/src/emit/soroban/mod.rs @@ -13,7 +13,7 @@ use inkwell::{ }; use soroban_sdk::xdr::{ Limited, Limits, ScEnvMetaEntry, ScEnvMetaEntryInterfaceVersion, ScSpecEntry, - ScSpecFunctionInputV0, ScSpecFunctionV0, ScSpecTypeDef, StringM, WriteXdr, + ScSpecFunctionInputV0, ScSpecFunctionV0, ScSpecTypeDef, ScSpecTypeVec, StringM, WriteXdr, }; const SOROBAN_ENV_INTERFACE_VERSION: ScEnvMetaEntryInterfaceVersion = @@ -38,6 +38,11 @@ impl HostFunctions { .context .i64_type() .fn_type(&[ty.into(), ty.into()], false), + + HostFunctions::DeleteContractData => bin + .context + .i64_type() + .fn_type(&[ty.into(), ty.into()], false), // https://github.com/stellar/stellar-protocol/blob/2fdc77302715bc4a31a784aef1a797d466965024/core/cap-0046-03.md#ledger-host-functions-mod-l // ;; If the entry's TTL is below `threshold` ledgers, extend `live_until_ledger_seq` such that TTL == `extend_to`, where TTL is defined as live_until_ledger_seq - current ledger. // (func $extend_contract_data_ttl (param $k_val i64) (param $t_storage_type i64) (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64)) @@ -60,6 +65,16 @@ impl HostFunctions { .i64_type() .fn_type(&[ty.into(), ty.into()], false), HostFunctions::VectorNew => bin.context.i64_type().fn_type(&[], false), + HostFunctions::VecPopBack => bin.context.i64_type().fn_type(&[ty.into()], false), + HostFunctions::VecGet => bin + .context + .i64_type() + .fn_type(&[ty.into(), ty.into()], false), + + HostFunctions::VecPut => bin + .context + .i64_type() + .fn_type(&[ty.into(), ty.into(), ty.into()], false), HostFunctions::Call => bin .context .i64_type() @@ -68,6 +83,10 @@ impl HostFunctions { .context .i64_type() .fn_type(&[ty.into(), ty.into()], false), + HostFunctions::VecUnpackToLinearMemory => bin + .context + .i64_type() + .fn_type(&[ty.into(), ty.into(), ty.into()], false), HostFunctions::ObjToU64 => bin.context.i64_type().fn_type(&[ty.into()], false), HostFunctions::ObjFromU64 => bin.context.i64_type().fn_type(&[ty.into()], false), HostFunctions::RequireAuth => bin.context.i64_type().fn_type(&[ty.into()], false), @@ -91,6 +110,8 @@ impl HostFunctions { .i64_type() .fn_type(&[ty.into(), ty.into()], false), + HostFunctions::VecLen => bin.context.i64_type().fn_type(&[ty.into()], false), + HostFunctions::StringNewFromLinearMemory => bin .context .i64_type() @@ -141,6 +162,30 @@ impl HostFunctions { pub struct SorobanTarget; impl SorobanTarget { + fn vec_spec_type(ty: &ast::Type) -> ScSpecTypeDef { + match ty { + ast::Type::Array(nested, _) => { + let nested = Self::vec_spec_type(nested.as_ref()); + ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec { + element_type: Box::new(nested), + })) + } + ast::Type::Uint(32) => ScSpecTypeDef::U32, + ast::Type::Int(32) => ScSpecTypeDef::I32, + ast::Type::Uint(64) => ScSpecTypeDef::U64, + ast::Type::Int(64) => ScSpecTypeDef::I64, + ast::Type::Int(128) => ScSpecTypeDef::I128, + ast::Type::Uint(128) => ScSpecTypeDef::U128, + ast::Type::Bool => ScSpecTypeDef::Bool, + ast::Type::Address(_) => ScSpecTypeDef::Address, + ast::Type::Bytes(_) => ScSpecTypeDef::Bytes, + ast::Type::String => ScSpecTypeDef::String, + ast::Type::Ref(inner) => Self::vec_spec_type(inner.as_ref()), + ast::Type::SorobanHandle(inner) => Self::vec_spec_type(inner.as_ref()), + _ => panic!("unsupported array element type {ty:?}"), + } + } + pub fn build<'a>( context: &'a Context, std_lib: &Module<'a>, @@ -274,10 +319,9 @@ impl SorobanTarget { .try_into() .expect("function input name exceeds limit"), type_: { - let ty = if let ast::Type::Ref(ty) = &p.ty { - ty.as_ref() - } else { - &p.ty + let ty = match &p.ty { + ast::Type::Ref(ty) | ast::Type::SorobanHandle(ty) => ty.as_ref(), + _ => &p.ty, }; match ty { @@ -293,6 +337,13 @@ impl SorobanTarget { ast::Type::Address(_) => ScSpecTypeDef::Address, ast::Type::Bytes(_) => ScSpecTypeDef::Bytes, ast::Type::String => ScSpecTypeDef::String, + ast::Type::Array(ty, _) => { + let element = Self::vec_spec_type(ty.as_ref()); + + ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec { + element_type: Box::new(element), + })) + } _ => panic!("unsupported input type {:?}", p.ty), } }, // TODO: Map type. @@ -306,10 +357,9 @@ impl SorobanTarget { .iter() .map(|return_type| { let ret_type = return_type.ty.clone(); - let ty = if let ast::Type::Ref(ty) = ret_type { - *ty - } else { - ret_type + let ty = match ret_type { + ast::Type::Ref(ty) | ast::Type::SorobanHandle(ty) => *ty, + _ => ret_type, }; match ty { ast::Type::Uint(32) => ScSpecTypeDef::U32, @@ -370,6 +420,7 @@ impl SorobanTarget { HostFunctions::PutContractData, HostFunctions::GetContractData, HostFunctions::HasContractData, + HostFunctions::DeleteContractData, HostFunctions::ExtendContractDataTtl, HostFunctions::ExtendCurrentContractInstanceAndCodeTtl, HostFunctions::LogFromLinearMemory, @@ -377,6 +428,7 @@ impl SorobanTarget { HostFunctions::VectorNew, HostFunctions::Call, HostFunctions::VectorNewFromLinearMemory, + HostFunctions::VecUnpackToLinearMemory, HostFunctions::ObjToU64, HostFunctions::ObjFromU64, HostFunctions::PutContractData, @@ -402,12 +454,15 @@ impl SorobanTarget { HostFunctions::MapNew, HostFunctions::MapPut, HostFunctions::VecPushBack, + HostFunctions::VecGet, + HostFunctions::VecPut, HostFunctions::StringNewFromLinearMemory, HostFunctions::StrKeyToAddr, HostFunctions::GetCurrentContractAddress, HostFunctions::BytesNewFromLinearMemory, - HostFunctions::BytesLen, HostFunctions::BytesCopyToLinearMemory, + HostFunctions::VecLen, + HostFunctions::VecPopBack, ]; for func in &host_functions { diff --git a/src/emit/soroban/target.rs b/src/emit/soroban/target.rs index 106781f32..a8838574e 100644 --- a/src/emit/soroban/target.rs +++ b/src/emit/soroban/target.rs @@ -42,9 +42,17 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { bin: &Binary<'a>, ty: &ast::Type, slot: &mut IntValue<'a>, + slot_ty: Option<&ast::Type>, function: FunctionValue<'a>, storage_type: &Option, ) -> BasicValueEnum<'a> { + if let Some(Type::StorageRef(_, inner)) = slot_ty { + if let Type::Array(inner_ty, _) = inner.as_ref() { + if !is_reference_type(inner_ty) { + return get_storage_vec_subscript(bin, function, *slot); + } + } + } let storage_type = storage_type_to_int(storage_type); emit_context!(bin); @@ -133,10 +141,19 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { ty: &ast::Type, existing: bool, slot: &mut IntValue<'a>, + slot_ty: Option<&ast::Type>, dest: BasicValueEnum<'a>, function: FunctionValue<'a>, storage_type: &Option, ) { + if let Some(Type::StorageRef(_, inner)) = slot_ty { + if let Type::Array(inner_ty, _) = inner.as_ref() { + if !is_reference_type(inner_ty) { + return set_storage_vec_subscript(bin, function, *slot, dest.into_int_value()); + } + } + } + emit_context!(bin); let storage_type = storage_type_to_int(storage_type); @@ -155,7 +172,15 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { // In case of struct, we receive a buffer in that format: [ field1, field2, ... ] where each field is a Soroban tagged value of type i64 // therefore, for each field, we need to extract it from the buffer and call PutContractData for each field separately - if let Type::Struct(ast::StructType::UserDefined(n)) = ty { + + // This check is added to handle the case we are stroing a struct in storage + let inner_ty = if let Type::StorageRef(mutable, inner) = ty { + inner + } else { + ty + }; + + if let Type::Struct(ast::StructType::UserDefined(n)) = inner_ty { let field_count = &bin.ns.structs[*n].fields.len(); let data_ptr = bin.vector_bytes(dest); @@ -198,7 +223,23 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { slot: &mut IntValue<'a>, function: FunctionValue<'a>, ) { - unimplemented!() + let storage_type = storage_type_to_int(&None); + + let type_int = bin.context.i64_type().const_int(storage_type, false); + + let function_value = bin + .module + .get_function(HostFunctions::DeleteContractData.name()) + .unwrap(); + + let call = bin + .builder + .build_call( + function_value, + &[slot.as_basic_value_enum().into(), type_int.into()], + "del_contract_data", + ) + .unwrap(); } // Bytes and string have special storage layout @@ -272,6 +313,50 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { slot: IntValue<'a>, index: BasicValueEnum<'a>, ) -> IntValue<'a> { + if let Type::StorageRef(_, ty) = ty { + if let Type::Array(inner, _) = *ty.clone() { + if !is_reference_type(&inner) { + // here, we return a memory array with the following format: [ slot, index ] + + let arr = bin + .builder + .build_array_alloca( + bin.context.i64_type(), + bin.context.i64_type().const_int(2, false), + "array_subscript", + ) + .unwrap(); + + bin.builder.build_store(arr, slot).unwrap(); + + // advance pointer to index + + let index_ptr = unsafe { + bin.builder + .build_gep( + bin.context.i64_type().array_type(1), + arr, + &[ + bin.context.i64_type().const_zero(), + bin.context.i64_type().const_int(1, false), + ], + "index_ptr", + ) + .unwrap() + }; + + bin.builder.build_store(index_ptr, index).unwrap(); + + // now return the pointer as int value + let arr_ptr_as_int = bin + .builder + .build_ptr_to_int(arr, bin.context.i64_type(), "array_ptr_as_int") + .unwrap(); + return arr_ptr_as_int; + } + } + } + let vec_new = bin .builder .build_call( @@ -358,12 +443,83 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { fn storage_array_length( &self, - _bin: &Binary<'a>, + bin: &Binary<'a>, _function: FunctionValue, - _slot: IntValue<'a>, - _elem_ty: &Type, + slot: IntValue<'a>, + elem_ty: &Type, ) -> IntValue<'a> { - unimplemented!() + if !is_reference_type(elem_ty) { + // Native arrays use VecObject layout: load vec object then call VecLen. + let load_storage = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::GetContractData.name()) + .unwrap(), + &[ + slot.into(), + bin.context.i64_type().const_int(1, false).into(), // persistent storage + ], + "load_storage", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let u32_val = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::VecLen.name()) + .unwrap(), + &[load_storage.into()], + "vec_len", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + // VecLen returns U32Val => payload in top 32 bits. + return bin + .builder + .build_right_shift( + u32_val, + bin.context.i64_type().const_int(32, false), + false, + "length", + ) + .unwrap(); + } + // Reference arrays keep old layout: length encoded in slot as U64Small. + let storage_ty = bin.context.i64_type().const_int(1, false); + let loaded_len = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::GetContractData.name()) + .unwrap(), + &[slot.into(), storage_ty.into()], + "get_len", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + // U64Small payload is shifted by 8 bits. + bin.builder + .build_right_shift( + loaded_len, + bin.context.i64_type().const_int(8, false), + false, + "length", + ) + .unwrap() } /// keccak256 hash @@ -823,6 +979,145 @@ fn encode_value<'a>( .unwrap() } +fn load_slot_index_from_key_ptr<'a>( + bin: &Binary<'a>, + key_ptr: PointerValue<'a>, +) -> (IntValue<'a>, IntValue<'a>) { + let slot_val = bin + .builder + .build_load(bin.context.i64_type(), key_ptr, "key_slot") + .unwrap() + .into_int_value(); // slot loaded from key array + let index_ptr = unsafe { + bin.builder + .build_gep( + bin.context.i64_type(), + key_ptr, + &[bin.context.i64_type().const_int(1, false)], + "key_index_ptr", + ) + .unwrap() + }; // pointer to index element + let index_val = bin + .builder + .build_load(bin.context.i64_type(), index_ptr, "key_index") + .unwrap() + .into_int_value(); // index loaded from key array + (slot_val, index_val) +} + +fn get_storage_vec_subscript<'a>( + bin: &Binary<'a>, + _function: FunctionValue<'a>, + key_vec: IntValue<'a>, +) -> BasicValueEnum<'a> { + let key_ptr = bin + .builder + .build_int_to_ptr(key_vec, bin.context.ptr_type(Default::default()), "key_ptr") + .unwrap(); // pointer to key array + let (slot_val, index_val) = load_slot_index_from_key_ptr(bin, key_ptr); // slot/index from key array + + let vec_obj = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::GetContractData.name()) + .unwrap(), + &[ + slot_val.into(), + bin.context.i64_type().const_int(1, false).into(), + ], + "load_storage", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); // vec object from storage + let index_val = encode_value(index_val, 32, 4, bin); // index encoded as u32 val + let elem_val = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::VecGet.name()) + .unwrap(), + &[vec_obj.into(), index_val.into()], + "vec_get", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); // element from vec + elem_val.as_basic_value_enum() +} + +fn set_storage_vec_subscript<'a>( + bin: &Binary<'a>, + _function: FunctionValue<'a>, + key_vec: IntValue<'a>, + value: IntValue<'a>, +) { + let key_ptr = bin + .builder + .build_int_to_ptr(key_vec, bin.context.ptr_type(Default::default()), "key_ptr") + .unwrap(); // pointer to key array + let (slot_val, index_val) = load_slot_index_from_key_ptr(bin, key_ptr); // slot/index from key array + + // encode index as u32 val + let index_val = encode_value(index_val, 32, 4, bin); // index encoded as u32 val + + let vec_obj = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::GetContractData.name()) + .unwrap(), + &[ + slot_val.into(), + bin.context.i64_type().const_int(1, false).into(), + ], + "load_storage", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); // vec object from storage + let new_vec_obj = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::VecPut.name()) + .unwrap(), + &[vec_obj.into(), index_val.into(), value.into()], + "vec_put", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); // updated vec object + let _store_storage = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::PutContractData.name()) + .unwrap(), + &[ + slot_val.into(), + new_vec_obj.into(), + bin.context.i64_type().const_int(1, false).into(), + ], + "store_storage", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); // store updated vec +} + fn is_val_true<'ctx>(bin: &Binary<'ctx>, val: IntValue<'ctx>) -> IntValue<'ctx> { let tag_mask = bin.context.i64_type().const_int(0xff, false); let tag_true = bin.context.i64_type().const_int(1, false); @@ -1055,3 +1350,23 @@ pub fn soroban_get_fields_to_val_buffer<'a>( //bin.vector_bytes( vec_ptr.as_basic_value_enum()) vec_ptr } + +fn is_reference_type(ty: &Type) -> bool { + match ty { + Type::Bool => false, + Type::Address(_) => false, + Type::Int(_) => false, + Type::Uint(_) => false, + Type::Rational => false, + Type::Bytes(_) => false, + Type::Enum(_) => false, + Type::Struct(_) => true, + Type::Array(..) => true, + Type::DynamicBytes => true, + Type::String => true, + Type::Mapping(..) => true, + Type::Contract(_) => false, + Type::InternalFunction { .. } => false, + _ => false, + } +} diff --git a/src/lir/converter/lir_type.rs b/src/lir/converter/lir_type.rs index fda2cd9e0..0c5c11abd 100644 --- a/src/lir/converter/lir_type.rs +++ b/src/lir/converter/lir_type.rs @@ -85,6 +85,7 @@ impl Converter<'_> { self.wrap_ptr_by_depth(Type::Slice(Box::new(ty)), depth) } ast::Type::FunctionSelector => Type::Uint(self.fn_selector_length() as u16 * 8), + ast::Type::SorobanHandle(_) => Type::Uint(64), ast::Type::Rational => unreachable!(), ast::Type::Void => unreachable!(), ast::Type::Unreachable => unreachable!(), diff --git a/src/sema/ast.rs b/src/sema/ast.rs index 0d09dfc9f..470759af6 100644 --- a/src/sema/ast.rs +++ b/src/sema/ast.rs @@ -74,6 +74,8 @@ pub enum Type { BufferPointer, /// The function selector (or discriminator) type is 4 bytes on Polkadot and 8 bytes on Solana FunctionSelector, + /// Soroban wrapper ABI handle around a concrete Solidity type. + SorobanHandle(Box), } #[derive(Eq, Clone, Debug)] diff --git a/src/sema/types.rs b/src/sema/types.rs index 7cccbf633..e9ecfbb6f 100644 --- a/src/sema/types.rs +++ b/src/sema/types.rs @@ -1303,6 +1303,7 @@ impl Type { Type::Unresolved => "unresolved".into(), Type::BufferPointer => "buffer_pointer".into(), Type::FunctionSelector => "function_selector".into(), + Type::SorobanHandle(inner) => format!("soroban_handle({})", inner.to_string(ns)), } } diff --git a/tests/soroban_testcases/array_args.rs b/tests/soroban_testcases/array_args.rs new file mode 100644 index 000000000..ab36a2a26 --- /dev/null +++ b/tests/soroban_testcases/array_args.rs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::build_solidity; +use soroban_sdk::{IntoVal, Val}; + +#[test] +fn array_argument_ops() { + let runtime = build_solidity( + r#" + contract array_arg_ops { + function sum_len(uint64[] memory arr) public returns (uint64) { + uint64 sum = 0; + + for (uint64 i = 0; i < arr.length; i++) { + sum += arr[i]; + } + + return sum + arr.length; + } + + function mutate_and_read(uint64[] memory arr) public returns (uint64) { + arr[0] = arr[0] + 10; + arr[1] = arr[1] * 2; + + return arr[0] + arr[1] + arr.length; + } + + function pair_sum_at(uint64[] memory arr, uint64 i) public returns (uint64) { + return arr[i] + arr[i + 1]; + } + } + "#, + |_| {}, + ); + + let addr = runtime.contracts.last().unwrap(); + + let arr: soroban_sdk::Vec = soroban_sdk::vec![&runtime.env, 1_u64, 2_u64, 3_u64]; + + // sum_len([1,2,3]) => 1 + 2 + 3 + len(3) = 9 + let expected: Val = 9_u64.into_val(&runtime.env); + let res = runtime.invoke_contract(addr, "sum_len", vec![arr.clone().into_val(&runtime.env)]); + assert!(expected.shallow_eq(&res)); + + // mutate_and_read([1,2,3]) => (1+10) + (2*2) + 3 = 18 + let expected: Val = 18_u64.into_val(&runtime.env); + let res = runtime.invoke_contract( + addr, + "mutate_and_read", + vec![arr.clone().into_val(&runtime.env)], + ); + assert!(expected.shallow_eq(&res)); + + // pair_sum_at([1,2,3], 1) => 2 + 3 = 5 + let expected: Val = 5_u64.into_val(&runtime.env); + let res = runtime.invoke_contract( + addr, + "pair_sum_at", + vec![arr.into_val(&runtime.env), 1_u64.into_val(&runtime.env)], + ); + assert!(expected.shallow_eq(&res)); +} + +#[test] +fn array_argument_storage_roundtrip() { + let runtime = build_solidity( + r#" + contract array_arg_ops { + uint64[] public stored_array; + + function store_and_get(uint64[] memory arr, uint64 index) public returns (uint64) { + stored_array = arr; + return stored_array[index]; + } + + function store_and_weighted_sum(uint64[] memory arr) public returns (uint64) { + stored_array = arr; + + uint64 acc = 0; + for (uint64 i = 0; i < stored_array.length; i++) { + acc += stored_array[i] * (i + 1); + } + + return acc; + } + + function probe() public returns (uint64) { + return stored_array[0] + stored_array[stored_array.length - 1] + stored_array.length; + } + } + "#, + |_| {}, + ); + + let addr = runtime.contracts.last().unwrap(); + let arr1: soroban_sdk::Vec = soroban_sdk::vec![&runtime.env, 11_u64, 22_u64, 33_u64]; + let arr2: soroban_sdk::Vec = soroban_sdk::vec![&runtime.env, 2_u64, 4_u64, 6_u64, 8_u64]; + + // store_and_get([11,22,33], 1) => 22 + let expected: Val = 22_u64.into_val(&runtime.env); + let res = runtime.invoke_contract( + addr, + "store_and_get", + vec![arr1.into_val(&runtime.env), 1_u64.into_val(&runtime.env)], + ); + assert!(expected.shallow_eq(&res)); + + // store_and_weighted_sum([2,4,6,8]) => 2*1 + 4*2 + 6*3 + 8*4 = 60 + let expected: Val = 60_u64.into_val(&runtime.env); + let res = runtime.invoke_contract( + addr, + "store_and_weighted_sum", + vec![arr2.into_val(&runtime.env)], + ); + assert!(expected.shallow_eq(&res)); + + // probe() uses persisted storage from previous call: 2 + 8 + 4 = 14 + let expected: Val = 14_u64.into_val(&runtime.env); + let res = runtime.invoke_contract(addr, "probe", vec![]); + assert!(expected.shallow_eq(&res)); +} diff --git a/tests/soroban_testcases/mod.rs b/tests/soroban_testcases/mod.rs index 935358a71..348301489 100644 --- a/tests/soroban_testcases/mod.rs +++ b/tests/soroban_testcases/mod.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 mod alloc; +mod array_args; mod auth; mod constructor; mod cross_contract_calls; @@ -10,6 +11,7 @@ mod mappings; mod math; mod print; mod storage; +mod storage_array; mod structs; mod token; mod ttl; diff --git a/tests/soroban_testcases/storage_array.rs b/tests/soroban_testcases/storage_array.rs new file mode 100644 index 000000000..15723d46f --- /dev/null +++ b/tests/soroban_testcases/storage_array.rs @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::build_solidity; +use soroban_sdk::{IntoVal, Val}; + +#[test] +fn storage_array_ops_test() { + let contract_src = r#" + contract storage_array { + uint64[] mylist; + uint64 normal = 20; + + function push_pop() public returns (uint64) { + mylist.push(5); + + mylist[0] = 15; + + mylist.push(5); + + return mylist[0] + mylist[1]; + } + + function loop() public returns (uint64) { + uint64 sum = 0; + + mylist.push(5); + mylist.push(10); + mylist.push(15); + + for (uint64 i = 0; i < mylist.length; i++) { + sum += mylist[i]; + } + + return sum; + } + + function random_access(uint64 index) public returns (uint64) { + uint64 sum = 0; + + mylist.push(5); + mylist.push(10); + mylist.push(15); + + sum += mylist[index]; + sum += mylist[index + 1]; + + return sum; + } + + function pop_len() public returns (uint64) { + mylist.push(1); + mylist.push(2); + mylist.push(3); + + mylist.pop(); + mylist.pop(); + + return mylist.length; + } + + // Copy a memory array into storage using push + function mem_to_storage() public returns (uint64) { + uint64[] memory tmp = new uint64[](3); + tmp[0] = 1; + tmp[1] = 2; + tmp[2] = 3; + + for (uint64 i = 0; i < tmp.length; i++) { + mylist.push(tmp[i]); + } + + uint64 sum = 0; + for (uint64 i = 0; i < mylist.length; i++) { + sum += mylist[i]; + } + return sum; // 1+2+3 = 6 + } + + // Copy a storage array into memory and sum + function storage_to_mem() public returns (uint64) { + mylist.push(7); + mylist.push(9); + mylist.push(11); + + uint64[] memory tmp = new uint64[](mylist.length); + for (uint64 i = 0; i < mylist.length; i++) { + tmp[i] = mylist[i]; + } + + uint64 sum = 0; + for (uint64 i = 0; i < tmp.length; i++) { + sum += tmp[i]; + } + return sum; // 7+9+11 = 27 + } + } + "#; + + // Build once; deploy fresh instances for each scenario to avoid state carryover. + let mut runtime = build_solidity(contract_src, |_| {}); + + // 1) push_pop(): after operations -> [15, 5]; return 15 + 5 = 20 + let addr = runtime.contracts.last().unwrap(); + let expected: Val = 20_u64.into_val(&runtime.env); + let res = runtime.invoke_contract(addr, "push_pop", vec![]); + println!("res: {res:?}"); + println!("expected: {expected:?}"); + assert!(expected.shallow_eq(&res)); + + // 2) loop(): new instance, pushes 5,10,15 and sums => 30 + let addr2 = runtime.deploy_contract(contract_src); + let expected: Val = 30_u64.into_val(&runtime.env); + let res = runtime.invoke_contract(&addr2, "loop", vec![]); + assert!(expected.shallow_eq(&res)); + + // 3) random_access(index): new instance + let addr3 = runtime.deploy_contract(contract_src); + + // index 0: 5 + 10 = 15 + let expected: Val = 15_u64.into_val(&runtime.env); + let args = vec![0_u64.into_val(&runtime.env)]; + let res = runtime.invoke_contract(&addr3, "random_access", args); + assert!(expected.shallow_eq(&res)); + + // index 1: 10 + 15 = 25 + let expected: Val = 25_u64.into_val(&runtime.env); + let args = vec![1_u64.into_val(&runtime.env)]; + let res = runtime.invoke_contract(&addr3, "random_access", args); + assert!(expected.shallow_eq(&res)); + + // 4) pop_len(): start with [], push 3 items then pop 2 => length = 1 + let addr4 = runtime.deploy_contract(contract_src); + let expected: Val = 1_u64.into_val(&runtime.env); + let res = runtime.invoke_contract(&addr4, "pop_len", vec![]); + assert!(expected.shallow_eq(&res)); + + // 5) mem_to_storage(): copy [1,2,3] into storage and sum => 6 + let addr5 = runtime.deploy_contract(contract_src); + let expected: Val = 6_u64.into_val(&runtime.env); + let res = runtime.invoke_contract(&addr5, "mem_to_storage", vec![]); + assert!(expected.shallow_eq(&res)); + + // 6) storage_to_mem(): start storage [7,9,11], copy to memory and sum => 27 + let addr6 = runtime.deploy_contract(contract_src); + let expected: Val = 27_u64.into_val(&runtime.env); + let res = runtime.invoke_contract(&addr6, "storage_to_mem", vec![]); + assert!(expected.shallow_eq(&res)); +} + +#[test] +fn storage_array_of_structs_test() { + let contract_src = r#" + contract storage_struct_vec { + struct Pair { + uint64 a; + uint64 b; + } + + Pair[] items; + + function push_pair_len() public returns (uint64) { + Pair memory p1 = Pair({a: 1, b: 2}); + Pair memory p2 = Pair({a: 3, b: 4}); + items.push(p1); + items.push(p2); + return items.length; // 2 + } + + function write_then_read() public returns (uint64) { + items.push(); // append empty slot + items[0] = Pair({a: 9, b: 11}); + return items[0].a + items[0].b; // 20 + } + + function iter_sum() public returns (uint64) { + items.push(Pair({a: 1, b: 2})); + items.push(Pair({a: 3, b: 4})); + items.push(Pair({a: 5, b: 6})); + uint64 s = 0; + for (uint64 i = 0; i < items.length; i++) { + s += items[i].a + items[i].b; + } + return s; // (1+2)+(3+4)+(5+6) = 21 + } + } + "#; + + let mut runtime = build_solidity(contract_src, |_| {}); + + // 1) push_pair_len => 2 + let addr1 = runtime.contracts.last().unwrap(); + let expected: Val = 2_u64.into_val(&runtime.env); + let res = runtime.invoke_contract(addr1, "push_pair_len", vec![]); + assert!(expected.shallow_eq(&res)); + + // 2) write_then_read => 20 + let addr2 = runtime.deploy_contract(contract_src); + let expected: Val = 20_u64.into_val(&runtime.env); + let res = runtime.invoke_contract(&addr2, "write_then_read", vec![]); + assert!(expected.shallow_eq(&res)); + + // 3) iter_sum => 21 + let addr3 = runtime.deploy_contract(contract_src); + let expected: Val = 21_u64.into_val(&runtime.env); + let res = runtime.invoke_contract(&addr3, "iter_sum", vec![]); + println!("res: {res:?}"); + assert!(expected.shallow_eq(&res)); +}