Skip to content

Commit 3b73f58

Browse files
authored
Merge pull request #58 from refcell/brock/array-bounds-check
feat(ir): ArrayT type, array bounds checking, IsPure fix
2 parents e66016d + 52e2271 commit 3b73f58

12 files changed

Lines changed: 508 additions & 21 deletions

File tree

crates/codegen/src/expr_compiler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1167,7 +1167,7 @@ impl<'a> ExprCompiler<'a> {
11671167
match ty {
11681168
EvmType::TupleT(elems) => elems.len(),
11691169
EvmType::Base(EvmBaseType::UnitT) | EvmType::Base(EvmBaseType::StateT) => 0,
1170-
EvmType::Base(_) => 1,
1170+
EvmType::Base(_) | EvmType::ArrayT(..) => 1,
11711171
}
11721172
}
11731173

crates/evm-tests/tests/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#![allow(missing_docs)]
22

3+
#[path = "suites/arrays.rs"]
4+
mod arrays;
35
#[path = "suites/checked_elision.rs"]
46
mod checked_elision;
57
#[path = "suites/counter.rs"]
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
//! Array feature tests: memory arrays, storage arrays, bounds checking,
2+
//! iteration, mutation, and slices. Tests at O0, O1, and O2 to catch
3+
//! optimization-related regressions.
4+
5+
use alloy_primitives::U256;
6+
use edge_evm_tests::{abi_decode_u256, abi_encode_u256, EvmTestHost};
7+
8+
const PATH: &str = "../../examples/tests/test_arrays.edge";
9+
10+
fn decode(data: &[u8]) -> U256 {
11+
abi_decode_u256(data)
12+
}
13+
14+
fn u(val: u64) -> U256 {
15+
U256::from(val)
16+
}
17+
18+
fn encode(val: u64) -> Vec<u8> {
19+
abi_encode_u256(U256::from(val))
20+
}
21+
22+
fn encode2(a: u64, b: u64) -> Vec<u8> {
23+
let mut v = encode(a);
24+
v.extend_from_slice(&encode(b));
25+
v
26+
}
27+
28+
fn deploy(opt: u8) -> EvmTestHost {
29+
EvmTestHost::deploy_edge(PATH, opt)
30+
}
31+
32+
// ═══════════════════════════════════════════════════════════════════
33+
// Memory array: basic element access
34+
// ═══════════════════════════════════════════════════════════════════
35+
36+
#[test]
37+
fn element_access() {
38+
for opt in 0..=2 {
39+
let mut h = deploy(opt);
40+
let r = h.call_fn("element_access()", &[]);
41+
assert!(r.success, "O{opt}: element_access should succeed");
42+
assert_eq!(decode(&r.output), u(20), "O{opt}: arr[1] == 20");
43+
}
44+
}
45+
46+
#[test]
47+
fn read_all() {
48+
for opt in 0..=2 {
49+
let mut h = deploy(opt);
50+
let r = h.call_fn("read_all()", &[]);
51+
assert!(r.success, "O{opt}: read_all should succeed");
52+
assert_eq!(
53+
decode(&r.output),
54+
u(1000),
55+
"O{opt}: 100+200+300+400 == 1000"
56+
);
57+
}
58+
}
59+
60+
// ═══════════════════════════════════════════════════════════════════
61+
// Memory array: write then read
62+
// ═══════════════════════════════════════════════════════════════════
63+
64+
#[test]
65+
fn write_then_read() {
66+
for opt in 0..=2 {
67+
let mut h = deploy(opt);
68+
let r = h.call_fn("write_then_read()", &[]);
69+
assert!(r.success, "O{opt}: write_then_read should succeed");
70+
// arr = [99, 2, 77], sum = 178
71+
assert_eq!(decode(&r.output), u(178), "O{opt}: 99+2+77 == 178");
72+
}
73+
}
74+
75+
// ═══════════════════════════════════════════════════════════════════
76+
// Memory array: iteration
77+
// ═══════════════════════════════════════════════════════════════════
78+
79+
#[test]
80+
fn sum_array() {
81+
for opt in 0..=2 {
82+
let mut h = deploy(opt);
83+
let r = h.call_fn("sum_array()", &[]);
84+
assert!(r.success, "O{opt}: sum_array should succeed");
85+
assert_eq!(decode(&r.output), u(100), "O{opt}: 10+20+30+40 == 100");
86+
}
87+
}
88+
89+
#[test]
90+
fn loop_write_sum() {
91+
for opt in 0..=2 {
92+
let mut h = deploy(opt);
93+
let r = h.call_fn("loop_write_sum()", &[]);
94+
assert!(r.success, "O{opt}: loop_write_sum should succeed");
95+
// arr[i] = i*10: [0,10,20,30,40], sum = 100
96+
assert_eq!(decode(&r.output), u(100), "O{opt}: 0+10+20+30+40 == 100");
97+
}
98+
}
99+
100+
#[test]
101+
fn find_max() {
102+
for opt in 0..=2 {
103+
let mut h = deploy(opt);
104+
let r = h.call_fn("find_max()", &[]);
105+
assert!(r.success, "O{opt}: find_max should succeed");
106+
assert_eq!(
107+
decode(&r.output),
108+
u(50),
109+
"O{opt}: max of [30,10,50,20,40] == 50"
110+
);
111+
}
112+
}
113+
114+
// ═══════════════════════════════════════════════════════════════════
115+
// Slice access
116+
// ═══════════════════════════════════════════════════════════════════
117+
118+
#[test]
119+
fn slice_sum() {
120+
for opt in 0..=2 {
121+
let mut h = deploy(opt);
122+
let r = h.call_fn("slice_sum()", &[]);
123+
assert!(r.success, "O{opt}: slice_sum should succeed");
124+
// arr[1:3] = [20, 30], sum = 50
125+
assert_eq!(decode(&r.output), u(50), "O{opt}: 20+30 == 50");
126+
}
127+
}
128+
129+
// ═══════════════════════════════════════════════════════════════════
130+
// Storage array: set/get round-trip
131+
// ═══════════════════════════════════════════════════════════════════
132+
133+
#[test]
134+
fn storage_set_get() {
135+
for opt in 0..=2 {
136+
let mut h = deploy(opt);
137+
// Set values[0] = 42
138+
let r = h.call_fn("set(uint256,uint256)", &encode2(0, 42));
139+
assert!(r.success, "O{opt}: set(0, 42) should succeed");
140+
141+
// Get values[0]
142+
let r = h.call_fn("get(uint256)", &encode(0));
143+
assert!(r.success, "O{opt}: get(0) should succeed");
144+
assert_eq!(decode(&r.output), u(42), "O{opt}: values[0] == 42");
145+
}
146+
}
147+
148+
#[test]
149+
fn storage_multiple_slots() {
150+
for opt in 0..=2 {
151+
let mut h = deploy(opt);
152+
// Set several slots
153+
for i in 0..5u64 {
154+
let r = h.call_fn("set(uint256,uint256)", &encode2(i, (i + 1) * 100));
155+
assert!(
156+
r.success,
157+
"O{opt}: set({i}, {}) should succeed",
158+
(i + 1) * 100
159+
);
160+
}
161+
// Read back
162+
for i in 0..5u64 {
163+
let r = h.call_fn("get(uint256)", &encode(i));
164+
assert!(r.success, "O{opt}: get({i}) should succeed");
165+
assert_eq!(
166+
decode(&r.output),
167+
u((i + 1) * 100),
168+
"O{opt}: values[{i}] == {}",
169+
(i + 1) * 100
170+
);
171+
}
172+
}
173+
}
174+
175+
#[test]
176+
fn storage_sum() {
177+
for opt in 0..=2 {
178+
let mut h = deploy(opt);
179+
// Set values[0..5] = [10, 20, 30, 40, 50]
180+
for i in 0..5u64 {
181+
let r = h.call_fn("set(uint256,uint256)", &encode2(i, (i + 1) * 10));
182+
assert!(r.success, "O{opt}: set({i}) should succeed");
183+
}
184+
let r = h.call_fn("storage_sum()", &[]);
185+
assert!(r.success, "O{opt}: storage_sum should succeed");
186+
assert_eq!(decode(&r.output), u(150), "O{opt}: 10+20+30+40+50 == 150");
187+
}
188+
}
189+
190+
#[test]
191+
fn storage_overwrite() {
192+
for opt in 0..=2 {
193+
let mut h = deploy(opt);
194+
// Set then overwrite
195+
let r = h.call_fn("set(uint256,uint256)", &encode2(2, 100));
196+
assert!(r.success, "O{opt}: initial set should succeed");
197+
let r = h.call_fn("set(uint256,uint256)", &encode2(2, 999));
198+
assert!(r.success, "O{opt}: overwrite should succeed");
199+
let r = h.call_fn("get(uint256)", &encode(2));
200+
assert!(r.success, "O{opt}: get after overwrite should succeed");
201+
assert_eq!(decode(&r.output), u(999), "O{opt}: values[2] == 999");
202+
}
203+
}
204+
205+
// ═══════════════════════════════════════════════════════════════════
206+
// Bounds checking: storage array OOB reverts
207+
// ═══════════════════════════════════════════════════════════════════
208+
209+
#[test]
210+
fn storage_get_oob_reverts() {
211+
for opt in 0..=2 {
212+
let mut h = deploy(opt);
213+
// values has length 5, index 5 is OOB
214+
let r = h.call_fn("get(uint256)", &encode(5));
215+
assert!(
216+
!r.success,
217+
"O{opt}: get(5) should revert (OOB on [u256; 5])"
218+
);
219+
}
220+
}
221+
222+
#[test]
223+
fn storage_get_large_index_reverts() {
224+
for opt in 0..=2 {
225+
let mut h = deploy(opt);
226+
let r = h.call_fn("get(uint256)", &encode(100));
227+
assert!(!r.success, "O{opt}: get(100) should revert");
228+
}
229+
}
230+
231+
#[test]
232+
fn storage_set_oob_reverts() {
233+
for opt in 0..=2 {
234+
let mut h = deploy(opt);
235+
let r = h.call_fn("set(uint256,uint256)", &encode2(5, 42));
236+
assert!(
237+
!r.success,
238+
"O{opt}: set(5, 42) should revert (OOB on [u256; 5])"
239+
);
240+
}
241+
}
242+
243+
#[test]
244+
fn storage_boundary_index_succeeds() {
245+
for opt in 0..=2 {
246+
let mut h = deploy(opt);
247+
// Index 4 is the last valid index for [u256; 5]
248+
let r = h.call_fn("set(uint256,uint256)", &encode2(4, 777));
249+
assert!(
250+
r.success,
251+
"O{opt}: set(4, 777) should succeed (last valid index)"
252+
);
253+
let r = h.call_fn("get(uint256)", &encode(4));
254+
assert!(r.success, "O{opt}: get(4) should succeed");
255+
assert_eq!(decode(&r.output), u(777), "O{opt}: values[4] == 777");
256+
}
257+
}
258+
259+
// ═══════════════════════════════════════════════════════════════════
260+
// Bounds checking: smaller storage array
261+
// ═══════════════════════════════════════════════════════════════════
262+
263+
#[test]
264+
fn small_storage_set_get() {
265+
for opt in 0..=2 {
266+
let mut h = deploy(opt);
267+
// small is [u256; 3]
268+
let r = h.call_fn("set_small(uint256,uint256)", &encode2(0, 11));
269+
assert!(r.success, "O{opt}: set_small(0, 11) should succeed");
270+
let r = h.call_fn("set_small(uint256,uint256)", &encode2(2, 33));
271+
assert!(r.success, "O{opt}: set_small(2, 33) should succeed");
272+
273+
let r = h.call_fn("get_small(uint256)", &encode(0));
274+
assert!(r.success, "O{opt}: get_small(0) should succeed");
275+
assert_eq!(decode(&r.output), u(11));
276+
277+
let r = h.call_fn("get_small(uint256)", &encode(2));
278+
assert!(r.success, "O{opt}: get_small(2) should succeed");
279+
assert_eq!(decode(&r.output), u(33));
280+
}
281+
}
282+
283+
#[test]
284+
fn small_storage_oob_reverts() {
285+
for opt in 0..=2 {
286+
let mut h = deploy(opt);
287+
// small is [u256; 3], index 3 is OOB
288+
let r = h.call_fn("get_small(uint256)", &encode(3));
289+
assert!(
290+
!r.success,
291+
"O{opt}: get_small(3) should revert (OOB on [u256; 3])"
292+
);
293+
294+
let r = h.call_fn("set_small(uint256,uint256)", &encode2(3, 42));
295+
assert!(
296+
!r.success,
297+
"O{opt}: set_small(3, 42) should revert (OOB on [u256; 3])"
298+
);
299+
}
300+
}
301+
302+
// ═══════════════════════════════════════════════════════════════════
303+
// Bounds checking: index 0 always valid for non-empty arrays
304+
// ═══════════════════════════════════════════════════════════════════
305+
306+
#[test]
307+
fn index_zero_always_valid() {
308+
for opt in 0..=2 {
309+
let mut h = deploy(opt);
310+
let r = h.call_fn("set(uint256,uint256)", &encode2(0, 1));
311+
assert!(r.success, "O{opt}: set(0, 1) should always succeed");
312+
let r = h.call_fn("get(uint256)", &encode(0));
313+
assert!(r.success, "O{opt}: get(0) should always succeed");
314+
assert_eq!(decode(&r.output), u(1));
315+
}
316+
}

crates/ir/src/costs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ fn gas_cost_table() -> HashMap<&'static str, u32> {
169169

170170
// -- EvmBaseType / EvmType variants --
171171
for ty in &[
172-
"UIntT", "IntT", "BytesT", "AddrT", "BoolT", "UnitT", "StateT", "Base", "TupleT",
172+
"UIntT", "IntT", "BytesT", "AddrT", "BoolT", "UnitT", "StateT", "Base", "TupleT", "ArrayT",
173173
] {
174174
m.insert(*ty, 0);
175175
}

crates/ir/src/optimizations/dead_code.egg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@
8080
(rule ((= e (Concat a b)) (IsPure a) (IsPure b))
8181
((IsPure e)) :ruleset dead-code)
8282

83-
;; If with all-pure branches is pure
84-
(rule ((= e (If cond t f ctx)) (IsPure cond) (IsPure t) (IsPure f))
83+
;; If with all-pure branches is pure (must check all 4 args including else)
84+
(rule ((= e (If cond inputs then_b else_b)) (IsPure cond) (IsPure inputs) (IsPure then_b) (IsPure else_b))
8585
((IsPure e)) :ruleset dead-code)
8686

8787
;; ---- Empty elimination in Concat chains ----

crates/ir/src/pretty.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ fn fmt_type(ty: &EvmType) -> String {
198198
let inner: Vec<_> = ts.iter().map(|t| format!("{t}")).collect();
199199
format!("({})", inner.join(", "))
200200
}
201+
EvmType::ArrayT(elem, len) => format!("[{elem}; {len}]"),
201202
}
202203
}
203204

crates/ir/src/schema.egg

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
;; A primitive type
3232
(Base EvmBaseType)
3333
;; A typed tuple
34-
(TupleT EvmTypeList))
34+
(TupleT EvmTypeList)
35+
;; A fixed-size array type: (ArrayT element_type length)
36+
(ArrayT EvmBaseType i64))
3537

3638
(constructor TLNil () EvmTypeList)
3739
(constructor TLCons (EvmBaseType EvmTypeList) EvmTypeList)

crates/ir/src/schema.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub enum EvmType {
5252
Base(EvmBaseType),
5353
/// A tuple type (flat — no nested tuples)
5454
TupleT(Vec<EvmBaseType>),
55+
/// A fixed-size array type: element type + length
56+
ArrayT(EvmBaseType, usize),
5557
}
5658

5759
// ============================================================

0 commit comments

Comments
 (0)