diff --git a/src/codegen/expression.rs b/src/codegen/expression.rs index 76cfcec09..594f41e6b 100644 --- a/src/codegen/expression.rs +++ b/src/codegen/expression.rs @@ -3866,22 +3866,22 @@ 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 { + Type::Uint(256) }; + // TODO(Soroban): Storage type here is None, it should be the same type as the array + 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 +4062,20 @@ 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_encoded = soroban_encode_arg(index, cfg, vartab, ns); + + return Expression::Subscript { + loc: *loc, + ty: elem_ty, + array_ty: array_ty.clone(), + expr: Box::new(array), + index: Box::new(index_encoded), + }; + } + if ns.target == Target::Solana { if ty.array_length().is_some() && ty.is_sparse_solana(ns) { let index = Expression::Variable { diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 284d4b232..966d7d4fd 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -99,6 +99,7 @@ pub enum HostFunctions { PutContractData, GetContractData, HasContractData, + DeleteContractData, ExtendContractDataTtl, ExtendCurrentContractInstanceAndCodeTtl, LogFromLinearMemory, @@ -144,6 +145,7 @@ 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._", diff --git a/src/codegen/storage.rs b/src/codegen/storage.rs index ae06ab7ca..b205af51c 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; @@ -114,31 +116,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 +182,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 +198,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 { @@ -263,26 +293,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 +327,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, }, ); diff --git a/src/emit/soroban/mod.rs b/src/emit/soroban/mod.rs index 52686f19e..ca63114cb 100644 --- a/src/emit/soroban/mod.rs +++ b/src/emit/soroban/mod.rs @@ -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)) @@ -370,6 +375,7 @@ impl SorobanTarget { HostFunctions::PutContractData, HostFunctions::GetContractData, HostFunctions::HasContractData, + HostFunctions::DeleteContractData, HostFunctions::ExtendContractDataTtl, HostFunctions::ExtendCurrentContractInstanceAndCodeTtl, HostFunctions::LogFromLinearMemory, diff --git a/src/emit/soroban/target.rs b/src/emit/soroban/target.rs index 106781f32..dbd0023b0 100644 --- a/src/emit/soroban/target.rs +++ b/src/emit/soroban/target.rs @@ -155,7 +155,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 +206,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 diff --git a/tests/soroban_testcases/mod.rs b/tests/soroban_testcases/mod.rs index 935358a71..1a29a8cfa 100644 --- a/tests/soroban_testcases/mod.rs +++ b/tests/soroban_testcases/mod.rs @@ -10,6 +10,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..84744185a --- /dev/null +++ b/tests/soroban_testcases/storage_array.rs @@ -0,0 +1,205 @@ +// 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![]); + 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![]); + assert!(expected.shallow_eq(&res)); +}