Skip to content

Commit 33d4257

Browse files
author
tars-swarm[bot]
committed
feat(typeck): upgrade const eval to full U256 precision
1 parent 8437a5a commit 33d4257

3 files changed

Lines changed: 180 additions & 40 deletions

File tree

crates/typeck/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ indexmap = { workspace = true }
1616
tiny-keccak = { workspace = true }
1717
alloy-primitives = { workspace = true }
1818
serde = { workspace = true, features = ["derive"] }
19+
ruint = { workspace = true }
1920

2021
[dev-dependencies]
2122
edge-driver = { workspace = true }
23+
edge-lexer = { workspace = true }
24+
edge-parser = { workspace = true }
2225
serde_json = { workspace = true }
2326

2427
[lints]

crates/typeck/src/checker.rs

Lines changed: 90 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use edge_ast::{
1414
Program,
1515
};
1616
use indexmap::IndexMap;
17+
use ruint::aliases::U256;
1718
use tiny_keccak::{Hasher, Keccak};
1819

1920
/// Storage slot assignment for contract fields
@@ -28,8 +29,8 @@ pub struct StorageLayout {
2829
pub struct ConstValue {
2930
/// Constant name
3031
pub name: String,
31-
/// Resolved u256 value (fits in u64 for simple literals)
32-
pub value: u64,
32+
/// Resolved value as big-endian 256-bit bytes
33+
pub value: [u8; 32],
3334
}
3435

3536
/// A function's type information
@@ -179,12 +180,12 @@ impl TypeChecker {
179180
.collect();
180181

181182
// Process constants (evaluate simple literal/arithmetic expressions)
182-
let mut const_env: IndexMap<String, u64> = IndexMap::new();
183+
let mut const_env: IndexMap<String, [u8; 32]> = IndexMap::new();
183184
let consts = contract
184185
.consts
185186
.iter()
186187
.map(|(decl, expr)| {
187-
let value = Self::eval_const_expr(expr, &const_env).unwrap_or(0);
188+
let value = Self::eval_const_expr(expr, &const_env).unwrap_or([0u8; 32]);
188189
const_env.insert(decl.name.name.clone(), value);
189190
ConstValue {
190191
name: decl.name.name.clone(),
@@ -293,54 +294,103 @@ impl TypeChecker {
293294
}
294295
}
295296

296-
/// Evaluate a constant expression to a u64 value.
297+
/// Evaluate a constant expression to a 256-bit value.
297298
/// Supports literals and simple arithmetic over previously-defined constants.
298-
fn eval_const_expr(expr: &Expr, env: &IndexMap<String, u64>) -> Option<u64> {
299+
/// Uses U256 arithmetic to match EVM semantics (256-bit wrapping).
300+
fn eval_const_expr(expr: &Expr, env: &IndexMap<String, [u8; 32]>) -> Option<[u8; 32]> {
299301
match expr {
300302
Expr::Literal(lit) => match lit.as_ref() {
301-
Lit::Int(bytes, _, _) => {
302-
// Values >u64 can't be evaluated here yet — return None
303-
// rather than silently truncating. Full U256 const eval
304-
// is planned as part of issue #43.
305-
if bytes[..24].iter().any(|&b| b != 0) {
306-
return None;
303+
Lit::Int(bytes, _, _) => Some(*bytes),
304+
Lit::Bool(b, _) => {
305+
let mut result = [0u8; 32];
306+
if *b {
307+
result[31] = 1;
307308
}
308-
Some(u64::from_be_bytes(bytes[24..32].try_into().unwrap()))
309+
Some(result)
309310
}
310-
Lit::Bool(b, _) => Some(if *b { 1 } else { 0 }),
311311
Lit::Hex(bytes, _) | Lit::Bin(bytes, _) => {
312-
let mut v = 0u64;
313-
for &b in bytes.iter().take(8) {
314-
v = (v << 8) | (b as u64);
315-
}
316-
Some(v)
312+
let mut result = [0u8; 32];
313+
let len = bytes.len().min(32);
314+
result[32 - len..].copy_from_slice(&bytes[..len]);
315+
Some(result)
317316
}
318317
Lit::Str(_, _) => None,
319318
},
320319
Expr::Ident(id) => env.get(&id.name).copied(),
321320
Expr::Paren(inner, _) => Self::eval_const_expr(inner, env),
322321
Expr::Binary(lhs, op, rhs, _) => {
323-
let l = Self::eval_const_expr(lhs, env)?;
324-
let r = Self::eval_const_expr(rhs, env)?;
325-
match op {
326-
BinOp::Add => Some(l.wrapping_add(r)),
327-
BinOp::Sub => Some(l.wrapping_sub(r)),
328-
BinOp::Mul => Some(l.wrapping_mul(r)),
329-
BinOp::Div if r != 0 => Some(l / r),
330-
BinOp::Mod if r != 0 => Some(l % r),
331-
BinOp::BitwiseAnd => Some(l & r),
332-
BinOp::BitwiseOr => Some(l | r),
333-
BinOp::BitwiseXor => Some(l ^ r),
334-
BinOp::Shl => Some(l << (r & 63)),
335-
BinOp::Shr => Some(l >> (r & 63)),
336-
BinOp::Eq => Some(if l == r { 1 } else { 0 }),
337-
BinOp::Neq => Some(if l != r { 1 } else { 0 }),
338-
BinOp::Lt => Some(if l < r { 1 } else { 0 }),
339-
BinOp::Gt => Some(if l > r { 1 } else { 0 }),
340-
BinOp::Lte => Some(if l <= r { 1 } else { 0 }),
341-
BinOp::Gte => Some(if l >= r { 1 } else { 0 }),
342-
_ => None,
343-
}
322+
let l_bytes = Self::eval_const_expr(lhs, env)?;
323+
let r_bytes = Self::eval_const_expr(rhs, env)?;
324+
let l = U256::from_be_bytes(l_bytes);
325+
let r = U256::from_be_bytes(r_bytes);
326+
let result = match op {
327+
BinOp::Add => l.wrapping_add(r),
328+
BinOp::Sub => l.wrapping_sub(r),
329+
BinOp::Mul => l.wrapping_mul(r),
330+
BinOp::Div if !r.is_zero() => l / r,
331+
BinOp::Mod if !r.is_zero() => l % r,
332+
BinOp::BitwiseAnd => l & r,
333+
BinOp::BitwiseOr => l | r,
334+
BinOp::BitwiseXor => l ^ r,
335+
BinOp::Shl => {
336+
if r >= U256::from(256) {
337+
U256::ZERO
338+
} else {
339+
l << r.to::<usize>()
340+
}
341+
}
342+
BinOp::Shr => {
343+
if r >= U256::from(256) {
344+
U256::ZERO
345+
} else {
346+
l >> r.to::<usize>()
347+
}
348+
}
349+
BinOp::Eq => {
350+
if l == r {
351+
U256::from(1)
352+
} else {
353+
U256::ZERO
354+
}
355+
}
356+
BinOp::Neq => {
357+
if l != r {
358+
U256::from(1)
359+
} else {
360+
U256::ZERO
361+
}
362+
}
363+
BinOp::Lt => {
364+
if l < r {
365+
U256::from(1)
366+
} else {
367+
U256::ZERO
368+
}
369+
}
370+
BinOp::Gt => {
371+
if l > r {
372+
U256::from(1)
373+
} else {
374+
U256::ZERO
375+
}
376+
}
377+
BinOp::Lte => {
378+
if l <= r {
379+
U256::from(1)
380+
} else {
381+
U256::ZERO
382+
}
383+
}
384+
BinOp::Gte => {
385+
if l >= r {
386+
U256::from(1)
387+
} else {
388+
U256::ZERO
389+
}
390+
}
391+
_ => return None,
392+
};
393+
Some(result.to_be_bytes())
344394
}
345395
_ => None,
346396
}

crates/typeck/tests/const_eval.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#![allow(missing_docs)]
2+
3+
use std::path::PathBuf;
4+
5+
use edge_typeck::{ConstValue, TypeChecker};
6+
7+
/// Helper: parse + typecheck Edge source and return const values.
8+
fn compile_consts(source: &str) -> Vec<ConstValue> {
9+
let ast = edge_parser::parse(source).expect("parse failed");
10+
let checked = TypeChecker::new().check(&ast).expect("typecheck failed");
11+
12+
checked
13+
.contracts
14+
.into_iter()
15+
.flat_map(|c| c.consts)
16+
.collect()
17+
}
18+
19+
/// Convert a [u8; 32] big-endian value to a hex string for easier assertions.
20+
fn to_hex(bytes: &[u8; 32]) -> String {
21+
bytes.iter().map(|b| format!("{b:02x}")).collect()
22+
}
23+
24+
#[test]
25+
fn large_literal_above_u64_max() {
26+
// 10^21 = 0x3635C9ADC5DEA00000 — larger than u64::MAX
27+
let consts = compile_consts(
28+
r#"contract Test {
29+
const X: u256 = 1000000000000000000000;
30+
}"#,
31+
);
32+
assert_eq!(consts.len(), 1);
33+
assert_eq!(consts[0].name, "X");
34+
35+
let hex = to_hex(&consts[0].value);
36+
assert!(
37+
hex.ends_with("3635c9adc5dea00000"),
38+
"expected 10^21, got 0x{hex}"
39+
);
40+
}
41+
42+
#[test]
43+
fn const_arithmetic_multiplication() {
44+
let consts = compile_consts(
45+
r#"contract Test {
46+
const A: u256 = 100;
47+
const B: u256 = A * A;
48+
}"#,
49+
);
50+
assert_eq!(consts.len(), 2);
51+
assert_eq!(consts[1].name, "B");
52+
// 100 * 100 = 10000 = 0x2710
53+
assert_eq!(consts[1].value[31], 0x10);
54+
assert_eq!(consts[1].value[30], 0x27);
55+
assert!(consts[1].value[..30].iter().all(|&b| b == 0));
56+
}
57+
58+
#[test]
59+
fn shift_left_128_not_zero() {
60+
let consts = compile_consts(
61+
r#"contract Test {
62+
const S: u256 = 1 << 128;
63+
}"#,
64+
);
65+
assert_eq!(consts.len(), 1);
66+
assert_eq!(consts[0].name, "S");
67+
assert!(
68+
consts[0].value.iter().any(|&b| b != 0),
69+
"1 << 128 should not be zero"
70+
);
71+
// Byte at index 15 (big-endian): bit 128 = byte 16 from end = index 15
72+
assert_eq!(consts[0].value[15], 1, "bit 128 should be set");
73+
assert!(consts[0].value[16..].iter().all(|&b| b == 0));
74+
}
75+
76+
#[test]
77+
fn small_value_u8() {
78+
let consts = compile_consts(
79+
r#"contract Test {
80+
const X: u8 = 42;
81+
}"#,
82+
);
83+
assert_eq!(consts.len(), 1);
84+
assert_eq!(consts[0].name, "X");
85+
assert_eq!(consts[0].value[31], 42);
86+
assert!(consts[0].value[..31].iter().all(|&b| b == 0));
87+
}

0 commit comments

Comments
 (0)