diff --git a/crates/codegen/src/expr_compiler.rs b/crates/codegen/src/expr_compiler.rs index 8c9f7da..79a2e28 100644 --- a/crates/codegen/src/expr_compiler.rs +++ b/crates/codegen/src/expr_compiler.rs @@ -1167,7 +1167,7 @@ impl<'a> ExprCompiler<'a> { match ty { EvmType::TupleT(elems) => elems.len(), EvmType::Base(EvmBaseType::UnitT) | EvmType::Base(EvmBaseType::StateT) => 0, - EvmType::Base(_) => 1, + EvmType::Base(_) | EvmType::ArrayT(..) => 1, } } diff --git a/crates/evm-tests/tests/main.rs b/crates/evm-tests/tests/main.rs index cae23c5..112ca81 100644 --- a/crates/evm-tests/tests/main.rs +++ b/crates/evm-tests/tests/main.rs @@ -1,5 +1,7 @@ #![allow(missing_docs)] +#[path = "suites/arrays.rs"] +mod arrays; #[path = "suites/checked_elision.rs"] mod checked_elision; #[path = "suites/counter.rs"] diff --git a/crates/evm-tests/tests/suites/arrays.rs b/crates/evm-tests/tests/suites/arrays.rs new file mode 100644 index 0000000..edadc75 --- /dev/null +++ b/crates/evm-tests/tests/suites/arrays.rs @@ -0,0 +1,316 @@ +//! Array feature tests: memory arrays, storage arrays, bounds checking, +//! iteration, mutation, and slices. Tests at O0, O1, and O2 to catch +//! optimization-related regressions. + +use alloy_primitives::U256; +use edge_evm_tests::{abi_decode_u256, abi_encode_u256, EvmTestHost}; + +const PATH: &str = "../../examples/tests/test_arrays.edge"; + +fn decode(data: &[u8]) -> U256 { + abi_decode_u256(data) +} + +fn u(val: u64) -> U256 { + U256::from(val) +} + +fn encode(val: u64) -> Vec { + abi_encode_u256(U256::from(val)) +} + +fn encode2(a: u64, b: u64) -> Vec { + let mut v = encode(a); + v.extend_from_slice(&encode(b)); + v +} + +fn deploy(opt: u8) -> EvmTestHost { + EvmTestHost::deploy_edge(PATH, opt) +} + +// ═══════════════════════════════════════════════════════════════════ +// Memory array: basic element access +// ═══════════════════════════════════════════════════════════════════ + +#[test] +fn element_access() { + for opt in 0..=2 { + let mut h = deploy(opt); + let r = h.call_fn("element_access()", &[]); + assert!(r.success, "O{opt}: element_access should succeed"); + assert_eq!(decode(&r.output), u(20), "O{opt}: arr[1] == 20"); + } +} + +#[test] +fn read_all() { + for opt in 0..=2 { + let mut h = deploy(opt); + let r = h.call_fn("read_all()", &[]); + assert!(r.success, "O{opt}: read_all should succeed"); + assert_eq!( + decode(&r.output), + u(1000), + "O{opt}: 100+200+300+400 == 1000" + ); + } +} + +// ═══════════════════════════════════════════════════════════════════ +// Memory array: write then read +// ═══════════════════════════════════════════════════════════════════ + +#[test] +fn write_then_read() { + for opt in 0..=2 { + let mut h = deploy(opt); + let r = h.call_fn("write_then_read()", &[]); + assert!(r.success, "O{opt}: write_then_read should succeed"); + // arr = [99, 2, 77], sum = 178 + assert_eq!(decode(&r.output), u(178), "O{opt}: 99+2+77 == 178"); + } +} + +// ═══════════════════════════════════════════════════════════════════ +// Memory array: iteration +// ═══════════════════════════════════════════════════════════════════ + +#[test] +fn sum_array() { + for opt in 0..=2 { + let mut h = deploy(opt); + let r = h.call_fn("sum_array()", &[]); + assert!(r.success, "O{opt}: sum_array should succeed"); + assert_eq!(decode(&r.output), u(100), "O{opt}: 10+20+30+40 == 100"); + } +} + +#[test] +fn loop_write_sum() { + for opt in 0..=2 { + let mut h = deploy(opt); + let r = h.call_fn("loop_write_sum()", &[]); + assert!(r.success, "O{opt}: loop_write_sum should succeed"); + // arr[i] = i*10: [0,10,20,30,40], sum = 100 + assert_eq!(decode(&r.output), u(100), "O{opt}: 0+10+20+30+40 == 100"); + } +} + +#[test] +fn find_max() { + for opt in 0..=2 { + let mut h = deploy(opt); + let r = h.call_fn("find_max()", &[]); + assert!(r.success, "O{opt}: find_max should succeed"); + assert_eq!( + decode(&r.output), + u(50), + "O{opt}: max of [30,10,50,20,40] == 50" + ); + } +} + +// ═══════════════════════════════════════════════════════════════════ +// Slice access +// ═══════════════════════════════════════════════════════════════════ + +#[test] +fn slice_sum() { + for opt in 0..=2 { + let mut h = deploy(opt); + let r = h.call_fn("slice_sum()", &[]); + assert!(r.success, "O{opt}: slice_sum should succeed"); + // arr[1:3] = [20, 30], sum = 50 + assert_eq!(decode(&r.output), u(50), "O{opt}: 20+30 == 50"); + } +} + +// ═══════════════════════════════════════════════════════════════════ +// Storage array: set/get round-trip +// ═══════════════════════════════════════════════════════════════════ + +#[test] +fn storage_set_get() { + for opt in 0..=2 { + let mut h = deploy(opt); + // Set values[0] = 42 + let r = h.call_fn("set(uint256,uint256)", &encode2(0, 42)); + assert!(r.success, "O{opt}: set(0, 42) should succeed"); + + // Get values[0] + let r = h.call_fn("get(uint256)", &encode(0)); + assert!(r.success, "O{opt}: get(0) should succeed"); + assert_eq!(decode(&r.output), u(42), "O{opt}: values[0] == 42"); + } +} + +#[test] +fn storage_multiple_slots() { + for opt in 0..=2 { + let mut h = deploy(opt); + // Set several slots + for i in 0..5u64 { + let r = h.call_fn("set(uint256,uint256)", &encode2(i, (i + 1) * 100)); + assert!( + r.success, + "O{opt}: set({i}, {}) should succeed", + (i + 1) * 100 + ); + } + // Read back + for i in 0..5u64 { + let r = h.call_fn("get(uint256)", &encode(i)); + assert!(r.success, "O{opt}: get({i}) should succeed"); + assert_eq!( + decode(&r.output), + u((i + 1) * 100), + "O{opt}: values[{i}] == {}", + (i + 1) * 100 + ); + } + } +} + +#[test] +fn storage_sum() { + for opt in 0..=2 { + let mut h = deploy(opt); + // Set values[0..5] = [10, 20, 30, 40, 50] + for i in 0..5u64 { + let r = h.call_fn("set(uint256,uint256)", &encode2(i, (i + 1) * 10)); + assert!(r.success, "O{opt}: set({i}) should succeed"); + } + let r = h.call_fn("storage_sum()", &[]); + assert!(r.success, "O{opt}: storage_sum should succeed"); + assert_eq!(decode(&r.output), u(150), "O{opt}: 10+20+30+40+50 == 150"); + } +} + +#[test] +fn storage_overwrite() { + for opt in 0..=2 { + let mut h = deploy(opt); + // Set then overwrite + let r = h.call_fn("set(uint256,uint256)", &encode2(2, 100)); + assert!(r.success, "O{opt}: initial set should succeed"); + let r = h.call_fn("set(uint256,uint256)", &encode2(2, 999)); + assert!(r.success, "O{opt}: overwrite should succeed"); + let r = h.call_fn("get(uint256)", &encode(2)); + assert!(r.success, "O{opt}: get after overwrite should succeed"); + assert_eq!(decode(&r.output), u(999), "O{opt}: values[2] == 999"); + } +} + +// ═══════════════════════════════════════════════════════════════════ +// Bounds checking: storage array OOB reverts +// ═══════════════════════════════════════════════════════════════════ + +#[test] +fn storage_get_oob_reverts() { + for opt in 0..=2 { + let mut h = deploy(opt); + // values has length 5, index 5 is OOB + let r = h.call_fn("get(uint256)", &encode(5)); + assert!( + !r.success, + "O{opt}: get(5) should revert (OOB on [u256; 5])" + ); + } +} + +#[test] +fn storage_get_large_index_reverts() { + for opt in 0..=2 { + let mut h = deploy(opt); + let r = h.call_fn("get(uint256)", &encode(100)); + assert!(!r.success, "O{opt}: get(100) should revert"); + } +} + +#[test] +fn storage_set_oob_reverts() { + for opt in 0..=2 { + let mut h = deploy(opt); + let r = h.call_fn("set(uint256,uint256)", &encode2(5, 42)); + assert!( + !r.success, + "O{opt}: set(5, 42) should revert (OOB on [u256; 5])" + ); + } +} + +#[test] +fn storage_boundary_index_succeeds() { + for opt in 0..=2 { + let mut h = deploy(opt); + // Index 4 is the last valid index for [u256; 5] + let r = h.call_fn("set(uint256,uint256)", &encode2(4, 777)); + assert!( + r.success, + "O{opt}: set(4, 777) should succeed (last valid index)" + ); + let r = h.call_fn("get(uint256)", &encode(4)); + assert!(r.success, "O{opt}: get(4) should succeed"); + assert_eq!(decode(&r.output), u(777), "O{opt}: values[4] == 777"); + } +} + +// ═══════════════════════════════════════════════════════════════════ +// Bounds checking: smaller storage array +// ═══════════════════════════════════════════════════════════════════ + +#[test] +fn small_storage_set_get() { + for opt in 0..=2 { + let mut h = deploy(opt); + // small is [u256; 3] + let r = h.call_fn("set_small(uint256,uint256)", &encode2(0, 11)); + assert!(r.success, "O{opt}: set_small(0, 11) should succeed"); + let r = h.call_fn("set_small(uint256,uint256)", &encode2(2, 33)); + assert!(r.success, "O{opt}: set_small(2, 33) should succeed"); + + let r = h.call_fn("get_small(uint256)", &encode(0)); + assert!(r.success, "O{opt}: get_small(0) should succeed"); + assert_eq!(decode(&r.output), u(11)); + + let r = h.call_fn("get_small(uint256)", &encode(2)); + assert!(r.success, "O{opt}: get_small(2) should succeed"); + assert_eq!(decode(&r.output), u(33)); + } +} + +#[test] +fn small_storage_oob_reverts() { + for opt in 0..=2 { + let mut h = deploy(opt); + // small is [u256; 3], index 3 is OOB + let r = h.call_fn("get_small(uint256)", &encode(3)); + assert!( + !r.success, + "O{opt}: get_small(3) should revert (OOB on [u256; 3])" + ); + + let r = h.call_fn("set_small(uint256,uint256)", &encode2(3, 42)); + assert!( + !r.success, + "O{opt}: set_small(3, 42) should revert (OOB on [u256; 3])" + ); + } +} + +// ═══════════════════════════════════════════════════════════════════ +// Bounds checking: index 0 always valid for non-empty arrays +// ═══════════════════════════════════════════════════════════════════ + +#[test] +fn index_zero_always_valid() { + for opt in 0..=2 { + let mut h = deploy(opt); + let r = h.call_fn("set(uint256,uint256)", &encode2(0, 1)); + assert!(r.success, "O{opt}: set(0, 1) should always succeed"); + let r = h.call_fn("get(uint256)", &encode(0)); + assert!(r.success, "O{opt}: get(0) should always succeed"); + assert_eq!(decode(&r.output), u(1)); + } +} diff --git a/crates/ir/src/costs.rs b/crates/ir/src/costs.rs index 56e6819..a45f41f 100644 --- a/crates/ir/src/costs.rs +++ b/crates/ir/src/costs.rs @@ -169,7 +169,7 @@ fn gas_cost_table() -> HashMap<&'static str, u32> { // -- EvmBaseType / EvmType variants -- for ty in &[ - "UIntT", "IntT", "BytesT", "AddrT", "BoolT", "UnitT", "StateT", "Base", "TupleT", + "UIntT", "IntT", "BytesT", "AddrT", "BoolT", "UnitT", "StateT", "Base", "TupleT", "ArrayT", ] { m.insert(*ty, 0); } diff --git a/crates/ir/src/optimizations/dead_code.egg b/crates/ir/src/optimizations/dead_code.egg index 2395f5f..cf73773 100644 --- a/crates/ir/src/optimizations/dead_code.egg +++ b/crates/ir/src/optimizations/dead_code.egg @@ -80,8 +80,8 @@ (rule ((= e (Concat a b)) (IsPure a) (IsPure b)) ((IsPure e)) :ruleset dead-code) -;; If with all-pure branches is pure -(rule ((= e (If cond t f ctx)) (IsPure cond) (IsPure t) (IsPure f)) +;; If with all-pure branches is pure (must check all 4 args including else) +(rule ((= e (If cond inputs then_b else_b)) (IsPure cond) (IsPure inputs) (IsPure then_b) (IsPure else_b)) ((IsPure e)) :ruleset dead-code) ;; ---- Empty elimination in Concat chains ---- diff --git a/crates/ir/src/pretty.rs b/crates/ir/src/pretty.rs index a336539..ce5e8eb 100644 --- a/crates/ir/src/pretty.rs +++ b/crates/ir/src/pretty.rs @@ -198,6 +198,7 @@ fn fmt_type(ty: &EvmType) -> String { let inner: Vec<_> = ts.iter().map(|t| format!("{t}")).collect(); format!("({})", inner.join(", ")) } + EvmType::ArrayT(elem, len) => format!("[{elem}; {len}]"), } } diff --git a/crates/ir/src/schema.egg b/crates/ir/src/schema.egg index 0bf71b0..d956858 100644 --- a/crates/ir/src/schema.egg +++ b/crates/ir/src/schema.egg @@ -31,7 +31,9 @@ ;; A primitive type (Base EvmBaseType) ;; A typed tuple - (TupleT EvmTypeList)) + (TupleT EvmTypeList) + ;; A fixed-size array type: (ArrayT element_type length) + (ArrayT EvmBaseType i64)) (constructor TLNil () EvmTypeList) (constructor TLCons (EvmBaseType EvmTypeList) EvmTypeList) diff --git a/crates/ir/src/schema.rs b/crates/ir/src/schema.rs index cb71b1c..b29d44a 100644 --- a/crates/ir/src/schema.rs +++ b/crates/ir/src/schema.rs @@ -52,6 +52,8 @@ pub enum EvmType { Base(EvmBaseType), /// A tuple type (flat — no nested tuples) TupleT(Vec), + /// A fixed-size array type: element type + length + ArrayT(EvmBaseType, usize), } // ============================================================ diff --git a/crates/ir/src/sexp.rs b/crates/ir/src/sexp.rs index ac296d4..2dc58ce 100644 --- a/crates/ir/src/sexp.rs +++ b/crates/ir/src/sexp.rs @@ -176,6 +176,9 @@ fn type_sexp(ty: &EvmType) -> String { }); format!("(TupleT {list})") } + EvmType::ArrayT(elem, len) => { + format!("(ArrayT {} {})", basetype_sexp(elem), len) + } } } @@ -587,6 +590,13 @@ fn sexp_to_type(sexp: &Sexp) -> Result { let types = sexp_to_type_list(&items[1])?; Ok(EvmType::TupleT(types)) } + "ArrayT" => { + let elem = sexp_to_basetype(&items[1])?; + let len = atom_str(&items[2])? + .parse::() + .map_err(|e| IrError::Extraction(format!("bad array length: {e}")))?; + Ok(EvmType::ArrayT(elem, len)) + } other => Err(IrError::Extraction(format!("unknown type: {other}"))), } } diff --git a/crates/ir/src/to_egglog/composite.rs b/crates/ir/src/to_egglog/composite.rs index 846126c..8dee104 100644 --- a/crates/ir/src/to_egglog/composite.rs +++ b/crates/ir/src/to_egglog/composite.rs @@ -5,7 +5,7 @@ use std::rc::Rc; use super::AstToEgglog; use crate::{ ast_helpers, - schema::{EvmBaseType, EvmType, RcExpr}, + schema::{EvmBaseType, EvmBinaryOp, EvmType, RcExpr}, IrError, }; @@ -445,6 +445,59 @@ impl AstToEgglog { None } + /// Extract the array length from a `__array__N` composite type string. + fn extract_array_len_from_composite(type_name: &str) -> Option { + type_name + .strip_prefix("__array__") + .and_then(|s| s.parse::().ok()) + } + + /// Try to extract a compile-time constant integer from an AST expression. + fn try_const_index(expr: &edge_ast::Expr) -> Option { + match expr { + edge_ast::Expr::Literal(lit) => match lit.as_ref() { + edge_ast::lit::Lit::Int(val, _, _) => Some(*val), + _ => None, + }, + edge_ast::Expr::Paren(inner, _) => Self::try_const_index(inner), + _ => None, + } + } + + /// Validate array bounds. For const indices, emits a compile error if out of bounds. + /// For non-const indices, emits a runtime bounds check that reverts on OOB. + fn check_array_bounds( + &mut self, + index: &edge_ast::Expr, + array_len: usize, + idx_ir: &RcExpr, + ) -> Result, IrError> { + if let Some(const_idx) = Self::try_const_index(index) { + if const_idx >= array_len as u64 { + return Err(IrError::LoweringSpanned { + message: format!( + "array index {const_idx} is out of bounds for array of length {array_len}" + ), + span: index.span(), + }); + } + // Const index is in bounds — no runtime check needed + return Ok(None); + } + + // Non-const index: emit `if (index >= len) { revert(0, 0) }` + let len_ir = ast_helpers::const_int(array_len as i64, self.current_ctx.clone()); + let in_bounds = ast_helpers::bop(EvmBinaryOp::Lt, Rc::clone(idx_ir), len_ir); + let zero = ast_helpers::const_int(0, self.current_ctx.clone()); + let revert = ast_helpers::revert(Rc::clone(&zero), zero, Rc::clone(&self.current_state)); + let empty = ast_helpers::empty(EvmType::Base(EvmBaseType::UnitT), self.current_ctx.clone()); + let inputs = + ast_helpers::empty(EvmType::Base(EvmBaseType::UnitT), self.current_ctx.clone()); + let bounds_check = ast_helpers::if_then_else(in_bounds, inputs, empty, revert); + self.current_state = Rc::clone(&bounds_check); + Ok(Some(bounds_check)) + } + /// Try to lower an array element read for memory-backed arrays. /// Returns None if the base is not a memory-backed array. pub(crate) fn try_lower_array_element_read( @@ -454,8 +507,18 @@ impl AstToEgglog { ) -> Result, IrError> { if let edge_ast::Expr::Ident(ident) = base { // Check for fixed-offset composite (array/struct with known base) - if let Some((_type_name, base_expr)) = self.lookup_composite_info(&ident.name) { + if let Some((type_name, base_expr)) = self.lookup_composite_info(&ident.name) { let idx_ir = self.lower_expr(index)?; + if let Some(array_len) = Self::extract_array_len_from_composite(&type_name) { + let check = self.check_array_bounds(index, array_len, &idx_ir)?; + if let Some(bounds_ir) = check { + let word_size = ast_helpers::const_int(32, self.current_ctx.clone()); + let offset = + ast_helpers::add(base_expr, ast_helpers::mul(idx_ir, word_size)); + let load = ast_helpers::mload(offset, Rc::clone(&self.current_state)); + return Ok(Some(ast_helpers::concat(bounds_ir, load))); + } + } let word_size = ast_helpers::const_int(32, self.current_ctx.clone()); let offset = ast_helpers::add(base_expr, ast_helpers::mul(idx_ir, word_size)); return Ok(Some(ast_helpers::mload( @@ -486,8 +549,23 @@ impl AstToEgglog { value: &RcExpr, ) -> Result, IrError> { if let edge_ast::Expr::Ident(ident) = base { - if let Some((_type_name, base_expr)) = self.lookup_composite_info(&ident.name) { + if let Some((type_name, base_expr)) = self.lookup_composite_info(&ident.name) { let idx_ir = self.lower_expr(index)?; + if let Some(array_len) = Self::extract_array_len_from_composite(&type_name) { + let check = self.check_array_bounds(index, array_len, &idx_ir)?; + if let Some(bounds_ir) = check { + let word_size = ast_helpers::const_int(32, self.current_ctx.clone()); + let offset = + ast_helpers::add(base_expr, ast_helpers::mul(idx_ir, word_size)); + let mstore = ast_helpers::mstore( + offset, + Rc::clone(value), + Rc::clone(&self.current_state), + ); + self.current_state = Rc::clone(&mstore); + return Ok(Some(ast_helpers::concat(bounds_ir, mstore))); + } + } let word_size = ast_helpers::const_int(32, self.current_ctx.clone()); let offset = ast_helpers::add(base_expr, ast_helpers::mul(idx_ir, word_size)); let mstore = @@ -517,12 +595,16 @@ impl AstToEgglog { index: &edge_ast::Expr, ) -> Result, IrError> { if let edge_ast::Expr::Ident(ident) = base { - if let Some(&(base_slot, _len)) = self.storage_array_fields.get(&ident.name) { + if let Some(&(base_slot, len)) = self.storage_array_fields.get(&ident.name) { let idx_ir = self.lower_expr(index)?; + let check = self.check_array_bounds(index, len, &idx_ir)?; let base_slot_ir = ast_helpers::const_int(base_slot as i64, self.current_ctx.clone()); let slot = ast_helpers::add(base_slot_ir, idx_ir); let load = ast_helpers::sload(slot, Rc::clone(&self.current_state)); + if let Some(bounds_ir) = check { + return Ok(Some(ast_helpers::concat(bounds_ir, load))); + } return Ok(Some(load)); } } @@ -538,14 +620,18 @@ impl AstToEgglog { value: &RcExpr, ) -> Result, IrError> { if let edge_ast::Expr::Ident(ident) = base { - if let Some(&(base_slot, _len)) = self.storage_array_fields.get(&ident.name) { + if let Some(&(base_slot, len)) = self.storage_array_fields.get(&ident.name) { let idx_ir = self.lower_expr(index)?; + let check = self.check_array_bounds(index, len, &idx_ir)?; let base_slot_ir = ast_helpers::const_int(base_slot as i64, self.current_ctx.clone()); let slot = ast_helpers::add(base_slot_ir, idx_ir); let store = ast_helpers::sstore(slot, Rc::clone(value), Rc::clone(&self.current_state)); self.current_state = Rc::clone(&store); + if let Some(bounds_ir) = check { + return Ok(Some(ast_helpers::concat(bounds_ir, store))); + } return Ok(Some(store)); } } diff --git a/crates/ir/src/to_egglog/types.rs b/crates/ir/src/to_egglog/types.rs index 7dff18d..d6cb6ab 100644 --- a/crates/ir/src/to_egglog/types.rs +++ b/crates/ir/src/to_egglog/types.rs @@ -139,7 +139,8 @@ impl AstToEgglog { /// Lower a type signature to an EVM IR type. pub(crate) fn lower_type_sig(&self, ty: &edge_ast::ty::TypeSig) -> EvmType { - match ty { + let resolved = self.resolve_type_alias(ty).clone(); + match &resolved { edge_ast::ty::TypeSig::Primitive(prim) => { EvmType::Base(self.lower_primitive_base_type(prim)) } @@ -149,12 +150,21 @@ impl AstToEgglog { .iter() .map(|t| match self.lower_type_sig(t) { EvmType::Base(b) => b, - EvmType::TupleT(_) => EvmBaseType::UIntT(256), // flatten nested tuples + EvmType::TupleT(_) | EvmType::ArrayT(..) => EvmBaseType::UIntT(256), }) .collect(); EvmType::TupleT(base_types) } - _ => EvmType::Base(EvmBaseType::UIntT(256)), // fallback for unhandled types + edge_ast::ty::TypeSig::Array(elem, len_expr) + | edge_ast::ty::TypeSig::PackedArray(elem, len_expr) => { + let elem_base = match self.lower_type_sig(elem) { + EvmType::Base(b) => b, + _ => EvmBaseType::UIntT(256), + }; + let len = Self::extract_array_length(len_expr).unwrap_or(0); + EvmType::ArrayT(elem_base, len) + } + _ => EvmType::Base(EvmBaseType::UIntT(256)), } } @@ -192,7 +202,7 @@ impl AstToEgglog { .iter() .map(|(_, ty)| match self.lower_type_sig(ty) { EvmType::Base(b) => b, - EvmType::TupleT(_) => EvmBaseType::UIntT(256), + EvmType::TupleT(_) | EvmType::ArrayT(..) => EvmBaseType::UIntT(256), }) .collect(); EvmType::TupleT(base_types) @@ -210,7 +220,7 @@ impl AstToEgglog { .iter() .map(|ty| match self.lower_type_sig(ty) { EvmType::Base(b) => b, - EvmType::TupleT(_) => EvmBaseType::UIntT(256), + EvmType::TupleT(_) | EvmType::ArrayT(..) => EvmBaseType::UIntT(256), }) .collect(); EvmType::TupleT(base_types) diff --git a/examples/tests/test_arrays.edge b/examples/tests/test_arrays.edge index 1a4e8d0..0ed942b 100644 --- a/examples/tests/test_arrays.edge +++ b/examples/tests/test_arrays.edge @@ -1,16 +1,33 @@ // test_arrays.edge — Execution tests for array features // -// A contract with public functions that exercise array instantiation, -// element access, iteration, and storage arrays. +// Covers: memory arrays, storage arrays, bounds checking (const + dynamic), +// element access, iteration, mutation, slices, and multi-word layouts. contract TestArrays { let values: &s [u256; 5]; + let small: &s [u256; 3]; + // ── Memory array: basic element access ────────────────────────── pub fn element_access() -> (u256) { let arr: [u256; 3] = [10, 20, 30]; return arr[1]; } + // ── Memory array: read all elements ───────────────────────────── + pub fn read_all() -> (u256) { + let arr: [u256; 4] = [100, 200, 300, 400]; + return arr[0] + arr[1] + arr[2] + arr[3]; + } + + // ── Memory array: write then read ─────────────────────────────── + pub fn write_then_read() -> (u256) { + let arr: [u256; 3] = [1, 2, 3]; + arr[0] = 99; + arr[2] = 77; + return arr[0] + arr[1] + arr[2]; + } + + // ── Memory array: iteration sum ───────────────────────────────── pub fn sum_array() -> (u256) { let arr: [u256; 4] = [10, 20, 30, 40]; let total: u256 = 0; @@ -20,17 +37,58 @@ contract TestArrays { return total; } - pub fn set(index: u256, value: u256) { - values[index] = value; + // ── Memory array: write via loop then sum ─────────────────────── + pub fn loop_write_sum() -> (u256) { + let arr: [u256; 5] = [0, 0, 0, 0, 0]; + for (let i: u256 = 0; i < 5; i = i + 1) { + arr[i] = i * 10; + } + return arr[0] + arr[1] + arr[2] + arr[3] + arr[4]; } - pub fn get(index: u256) -> (u256) { - return values[index]; + // ── Memory array: find max ────────────────────────────────────── + pub fn find_max() -> (u256) { + let arr: [u256; 5] = [30, 10, 50, 20, 40]; + let max_val: u256 = arr[0]; + for (let i: u256 = 1; i < 5; i = i + 1) { + if (arr[i] > max_val) { + max_val = arr[i]; + } + } + return max_val; } + // ── Slice access ──────────────────────────────────────────────── pub fn slice_sum() -> (u256) { let arr: [u256; 5] = [10, 20, 30, 40, 50]; let slice: [u256; 2] = arr[1:3]; return slice[0] + slice[1]; } + + // ── Storage array: set/get ────────────────────────────────────── + pub fn set(index: u256, value: u256) { + values[index] = value; + } + + pub fn get(index: u256) -> (u256) { + return values[index]; + } + + // ── Storage array: sum all ────────────────────────────────────── + pub fn storage_sum() -> (u256) { + let total: u256 = 0; + for (let i: u256 = 0; i < 5; i = i + 1) { + total = total + values[i]; + } + return total; + } + + // ── Storage array: set on smaller array ───────────────────────── + pub fn set_small(index: u256, value: u256) { + small[index] = value; + } + + pub fn get_small(index: u256) -> (u256) { + return small[index]; + } }