diff --git a/crates/ast/src/expr.rs b/crates/ast/src/expr.rs index 1fbd9cb..074da7b 100644 --- a/crates/ast/src/expr.rs +++ b/crates/ast/src/expr.rs @@ -90,6 +90,9 @@ pub enum Expr { /// Inline assembly block: asm(inputs...) -> (outputs...) { opcodes... } /// Fields: inputs, output names ("_" for discarded), asm ops, span InlineAsm(Vec, Vec>, Vec, Span), + + /// Type cast: expr as Type + Cast(Box, crate::ty::TypeSig, Span), } impl Expr { @@ -118,6 +121,7 @@ impl Expr { Self::At(_, _, span) => span.clone(), Self::Assign(_, _, span) => span.clone(), Self::InlineAsm(_, _, _, span) => span.clone(), + Self::Cast(_, _, span) => span.clone(), } } } diff --git a/crates/e2e/tests/main.rs b/crates/e2e/tests/main.rs index 00a6c04..aae1e77 100644 --- a/crates/e2e/tests/main.rs +++ b/crates/e2e/tests/main.rs @@ -54,5 +54,9 @@ mod utils_exec; #[path = "suites/warnings.rs"] mod warnings; +#[path = "suites/int_widths_exec.rs"] +mod int_widths_exec; #[path = "suites/large_int_literals.rs"] mod large_int_literals; +#[path = "suites/signed_widths_exec.rs"] +mod signed_widths_exec; diff --git a/crates/e2e/tests/suites/int_widths_exec.rs b/crates/e2e/tests/suites/int_widths_exec.rs new file mode 100644 index 0000000..b7e00c0 --- /dev/null +++ b/crates/e2e/tests/suites/int_widths_exec.rs @@ -0,0 +1,466 @@ +#![allow(missing_docs)] + +//! Execution-level tests for sub-256-bit integer width semantics. +//! +//! Verifies that: +//! - u8/u128 arithmetic truncates correctly +//! - Overflow/underflow at sub-256-bit widths is caught and reverts +//! - Bitwise ops and division work correctly at narrow widths +//! - Compile-time constant overflow is rejected +//! - Mixed-width arithmetic is rejected (requires explicit casts) +//! - Unsuffixed literals adopt the type of the other operand + +use edge_driver::compiler::Compiler; + +use crate::helpers::*; + +// ============================================================================= +// u8 checked addition +// ============================================================================= + +#[test] +fn test_u8_add_ok() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("u8_add_ok(uint8,uint8)"), + &[encode_u256(100), encode_u256(50)], + )); + assert!(r.success, "u8_add_ok(100, 50) reverted unexpectedly"); + assert_eq!(decode_u256(&r.output), 150); +} + +#[test] +fn test_u8_add_overflow_reverts() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("u8_add_overflow(uint8,uint8)"), + &[encode_u256(200), encode_u256(100)], + )); + assert!(!r.success, "u8_add_overflow(200, 100) should revert"); +} + +#[test] +fn test_u8_add_boundary_ok() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 255 + 0 = 255, should succeed + let r = evm.call(calldata( + selector("u8_add_ok(uint8,uint8)"), + &[encode_u256(255), encode_u256(0)], + )); + assert!(r.success, "u8_add_ok(255, 0) reverted"); + assert_eq!(decode_u256(&r.output), 255); +} + +#[test] +fn test_u8_add_boundary_overflow() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 255 + 1 = 256 > 255 → revert + let r = evm.call(calldata( + selector("u8_add_overflow(uint8,uint8)"), + &[encode_u256(255), encode_u256(1)], + )); + assert!(!r.success, "u8_add_overflow(255, 1) should revert"); +} + +// ============================================================================= +// u8 checked subtraction +// ============================================================================= + +#[test] +fn test_u8_sub_ok() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("u8_sub_ok(uint8,uint8)"), + &[encode_u256(100), encode_u256(50)], + )); + assert!(r.success, "u8_sub_ok(100, 50) reverted"); + assert_eq!(decode_u256(&r.output), 50); +} + +#[test] +fn test_u8_sub_underflow_reverts() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("u8_sub_underflow(uint8,uint8)"), + &[encode_u256(50), encode_u256(100)], + )); + assert!(!r.success, "u8_sub_underflow(50, 100) should revert"); +} + +// ============================================================================= +// u8 checked multiplication +// ============================================================================= + +#[test] +fn test_u8_mul_ok() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("u8_mul_ok(uint8,uint8)"), + &[encode_u256(10), encode_u256(20)], + )); + assert!(r.success, "u8_mul_ok(10, 20) reverted"); + assert_eq!(decode_u256(&r.output), 200); +} + +#[test] +fn test_u8_mul_overflow_reverts() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("u8_mul_overflow(uint8,uint8)"), + &[encode_u256(20), encode_u256(20)], + )); + assert!( + !r.success, + "u8_mul_overflow(20, 20) should revert (400 > 255)" + ); +} + +// ============================================================================= +// u8 bitwise and division (always safe) +// ============================================================================= + +#[test] +fn test_u8_and() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("u8_and(uint8,uint8)"), + &[encode_u256(0xAB), encode_u256(0x0F)], + )); + assert!(r.success, "u8_and reverted"); + assert_eq!(decode_u256(&r.output), 0x0B); +} + +#[test] +fn test_u8_div() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("u8_div(uint8,uint8)"), + &[encode_u256(200), encode_u256(10)], + )); + assert!(r.success, "u8_div reverted"); + assert_eq!(decode_u256(&r.output), 20); +} + +// ============================================================================= +// u128 overflow +// ============================================================================= + +#[test] +fn test_u128_add_ok() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("u128_add_ok(uint128,uint128)"), + &[encode_u256(1000), encode_u256(2000)], + )); + assert!(r.success, "u128_add_ok(1000, 2000) reverted"); + assert_eq!(decode_u256(&r.output), 3000); +} + +// ============================================================================= +// u8 truncation on OR and SHL +// ============================================================================= + +#[test] +fn test_u8_or_truncate() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("u8_or_truncate(uint8,uint8)"), + &[encode_u256(0xF0), encode_u256(0x0F)], + )); + assert!(r.success, "u8_or_truncate reverted"); + assert_eq!(decode_u256(&r.output), 0xFF); +} + +#[test] +fn test_u8_shl() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 3 << 2 = 12, fits in u8 + let r = evm.call(calldata( + selector("u8_shl(uint8,uint8)"), + &[encode_u256(3), encode_u256(2)], + )); + assert!(r.success, "u8_shl(3, 2) reverted"); + assert_eq!(decode_u256(&r.output), 12); +} + +#[test] +fn test_u8_shl_truncates() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 0x80 << 1 = 0x100 → truncated to 0x00 for u8 + let r = evm.call(calldata( + selector("u8_shl(uint8,uint8)"), + &[encode_u256(0x80), encode_u256(1)], + )); + assert!(r.success, "u8_shl(0x80, 1) reverted"); + assert_eq!(decode_u256(&r.output), 0); +} + +// ============================================================================= +// Chained u8 operations +// ============================================================================= + +#[test] +fn test_u8_chain() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + // (10 + 20) * 2 = 60, fits in u8 + let r = evm.call(calldata( + selector("u8_chain(uint8,uint8)"), + &[encode_u256(10), encode_u256(20)], + )); + assert!(r.success, "u8_chain(10, 20) reverted"); + assert_eq!(decode_u256(&r.output), 60); +} + +#[test] +fn test_u8_chain_overflow_reverts() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + // (100 + 100) * 2 = 400 > 255 → revert (overflow on mul) + let r = evm.call(calldata( + selector("u8_chain(uint8,uint8)"), + &[encode_u256(100), encode_u256(100)], + )); + assert!(!r.success, "u8_chain(100, 100) should revert"); +} + +// ============================================================================= +// Literal type suffix +// ============================================================================= + +#[test] +fn test_literal_u8_suffix() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata(selector("literal_u8_add()"), &[])); + assert!(r.success, "literal_u8_add() reverted"); + assert_eq!(decode_u256(&r.output), 52); // 42 + 10 = 52 +} + +#[test] +fn test_literal_u256_suffix() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata(selector("literal_u256_add()"), &[])); + assert!(r.success, "literal_u256_add() reverted"); + assert_eq!(decode_u256(&r.output), 1001); +} + +// ============================================================================= +// Type casting +// ============================================================================= + +#[test] +fn test_cast_u256_to_u8() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 300 as u8 = 300 & 0xFF = 44 + let r = evm.call(calldata( + selector("cast_u256_to_u8(uint256)"), + &[encode_u256(300)], + )); + assert!(r.success, "cast_u256_to_u8(300) reverted"); + assert_eq!(decode_u256(&r.output), 44); // 300 % 256 = 44 +} + +#[test] +fn test_cast_u8_to_u256() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + let r = evm.call(calldata( + selector("cast_u8_to_u256(uint8)"), + &[encode_u256(42)], + )); + assert!(r.success, "cast_u8_to_u256(42) reverted"); + assert_eq!(decode_u256(&r.output), 42); +} + +#[test] +fn test_cast_and_add() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 300 as u8 = 44, 44 + 1 = 45 + let r = evm.call(calldata( + selector("cast_and_add(uint256)"), + &[encode_u256(300)], + )); + assert!(r.success, "cast_and_add(300) reverted"); + assert_eq!(decode_u256(&r.output), 45); +} + +#[test] +fn test_cast_and_add_overflow() { + let bc = compile_contract("examples/tests/test_int_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 255 as u8 = 255, 255 + 1 = 256 > u8 → revert + let r = evm.call(calldata( + selector("cast_and_add(uint256)"), + &[encode_u256(255)], + )); + assert!(!r.success, "cast_and_add(255) should revert on u8 overflow"); +} + +// ============================================================================= +// Compile-time constant overflow rejection +// ============================================================================= + +fn assert_compile_error(source: &str, expected_messages: &[&str], expected_rendered: &[&str]) { + let mut compiler = Compiler::from_source(source); + let result = compiler.compile(); + assert!( + result.is_err(), + "Expected compilation to fail, but it succeeded.\nSource:\n{source}" + ); + + let messages = compiler.diagnostic_messages(); + let all_messages = messages.join("\n"); + for exp in expected_messages { + assert!( + all_messages.contains(exp), + "Expected message containing '{exp}', got:\n{all_messages}\nSource:\n{source}" + ); + } + + let rendered = compiler.render_diagnostics(); + for exp in expected_rendered { + assert!( + rendered.contains(exp), + "Expected rendered output containing '{exp}', got:\n{rendered}\nSource:\n{source}" + ); + } +} + +fn assert_compiles(source: &str) { + let mut compiler = Compiler::from_source(source); + let result = compiler.compile(); + assert!( + result.is_ok(), + "Expected compilation to succeed, but it failed.\nSource:\n{source}\nError: {:?}", + result.unwrap_err() + ); +} + +#[test] +fn test_const_overflow_u8_add() { + assert_compile_error( + "contract T { pub fn f() -> (u256) { return 250u8 + 250u8; } }", + &["attempt to compute", "250_u8 + 250_u8", "would overflow"], + &["overflows `u8`"], + ); +} + +#[test] +fn test_const_overflow_u8_mul() { + assert_compile_error( + "contract T { pub fn f() -> (u256) { return 20u8 * 20u8; } }", + &["attempt to compute", "20_u8 * 20_u8", "would overflow"], + &["overflows `u8`"], + ); +} + +#[test] +fn test_const_overflow_u8_sub() { + assert_compile_error( + "contract T { pub fn f() -> (u256) { return 0u8 - 1u8; } }", + &["attempt to compute", "would overflow"], + &["overflows `u8`"], + ); +} + +#[test] +fn test_const_no_overflow_u8_add() { + assert_compiles("contract T { pub fn f() -> (u256) { return 100u8 + 50u8; } }"); +} + +#[test] +fn test_const_no_overflow_u8_boundary() { + assert_compiles("contract T { pub fn f() -> (u256) { return 200u8 + 55u8; } }"); +} + +#[test] +fn test_const_overflow_u8_boundary() { + assert_compile_error( + "contract T { pub fn f() -> (u256) { return 200u8 + 56u8; } }", + &["attempt to compute", "200_u8 + 56_u8", "would overflow"], + &["overflows `u8`"], + ); +} + +#[test] +fn test_const_overflow_u128_mul() { + assert_compile_error( + "contract T { pub fn f() -> (u256) { let x: u128 = 340282366920938463463374607431768211455u128 * 2u128; return x; } }", + &["attempt to compute", "would overflow"], + &["overflows `u128`"], + ); +} + +// ============================================================================= +// Mixed-width rejection +// ============================================================================= + +#[test] +fn test_mixed_width_u8_u256_rejected() { + assert_compile_error( + "contract T { pub fn f(a: u8, b: u256) -> (u256) { return a + b; } }", + &["mismatched types", "u8", "u256"], + &["use an explicit cast"], + ); +} + +#[test] +fn test_mixed_width_u8_u128_rejected() { + assert_compile_error( + "contract T { pub fn f(a: u8, b: u128) -> (u256) { return a + b; } }", + &["mismatched types", "u8", "u128"], + &["use an explicit cast"], + ); +} + +// ============================================================================= +// Unsuffixed literal type adoption +// ============================================================================= + +#[test] +fn test_unsuffixed_literal_adopts_u8() { + // `x * 2` where x: u8 — the `2` should be treated as u8 + assert_compiles("contract T { pub fn f(x: u8) -> (u256) { return x * 2; } }"); +} + +#[test] +fn test_unsuffixed_literal_adopts_addr() { + // `a == 0` where a: addr — the `0` should be treated as addr + assert_compiles( + "contract T { pub fn f(a: addr) -> (u256) { if (a == 0) { return 1; } return 0; } }", + ); +} + +#[test] +fn test_unsuffixed_literal_adopts_bool() { + // `b == true` where b: bool — both are bool, should work + assert_compiles( + "contract T { pub fn f(b: bool) -> (u256) { if (b == true) { return 1; } return 0; } }", + ); +} + +#[test] +fn test_explicit_cast_fixes_mismatch() { + // Cast makes mixed-width work + assert_compiles( + "contract T { pub fn f(a: u8, b: u256) -> (u256) { return (a as u256) + b; } }", + ); +} diff --git a/crates/e2e/tests/suites/signed_widths_exec.rs b/crates/e2e/tests/suites/signed_widths_exec.rs new file mode 100644 index 0000000..3ddee7b --- /dev/null +++ b/crates/e2e/tests/suites/signed_widths_exec.rs @@ -0,0 +1,573 @@ +#![allow(missing_docs)] + +//! Execution-level tests for signed sub-256-bit integer width semantics. +//! +//! Verifies that: +//! - i8 checked arithmetic reverts on overflow/underflow +//! - Signed division uses SDIV +//! - Signed comparisons use SLT/SGT +//! - Unary negation works +//! - UnsafeAdd/Sub/Mul wrap without reverting +//! - Casts between signed and unsigned preserve/reinterpret bits correctly + +use edge_driver::compiler::Compiler; + +use crate::helpers::*; + +/// Encode a signed i64 value as a 32-byte two's complement ABI encoding. +/// Positive values are zero-padded, negative values are sign-extended (0xFF padded). +fn encode_i256(val: i64) -> [u8; 32] { + let mut out = if val < 0 { [0xFFu8; 32] } else { [0u8; 32] }; + out[24..].copy_from_slice(&val.to_be_bytes()); + out +} + +/// Decode a 32-byte two's complement return value as a signed i64. +fn decode_i256(output: &[u8]) -> i64 { + assert!( + output.len() >= 32, + "return value too short: {} bytes", + output.len() + ); + // Check if negative: top byte has high bit set + let negative = output[0] & 0x80 != 0; + if negative { + // All upper bytes should be 0xFF for values fitting in i64 + assert!( + output[0..24].iter().all(|&b| b == 0xFF), + "signed value too large for i64" + ); + } else { + assert_eq!(&output[0..24], &[0u8; 24], "signed value too large for i64"); + } + i64::from_be_bytes(output[24..32].try_into().unwrap()) +} + +// ============================================================================= +// i8 checked addition +// ============================================================================= + +#[test] +fn test_i8_add_ok() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 50 + 30 = 80, fits in i8 (-128..127) + let r = evm.call(calldata( + selector("i8_add_ok(int8,int8)"), + &[encode_i256(50), encode_i256(30)], + )); + assert!(r.success, "i8_add_ok(50, 30) reverted unexpectedly"); + assert_eq!(decode_i256(&r.output), 80); +} + +#[test] +fn test_i8_add_negative_ok() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -50 + 30 = -20, fits in i8 + let r = evm.call(calldata( + selector("i8_add_ok(int8,int8)"), + &[encode_i256(-50), encode_i256(30)], + )); + assert!(r.success, "i8_add_ok(-50, 30) reverted unexpectedly"); + assert_eq!(decode_i256(&r.output), -20); +} + +#[test] +fn test_i8_add_both_negative_ok() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -30 + (-40) = -70, fits in i8 + let r = evm.call(calldata( + selector("i8_add_ok(int8,int8)"), + &[encode_i256(-30), encode_i256(-40)], + )); + assert!(r.success, "i8_add_ok(-30, -40) reverted unexpectedly"); + assert_eq!(decode_i256(&r.output), -70); +} + +#[test] +fn test_i8_add_overflow_reverts() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 100 + 100 = 200 > 127 → revert + let r = evm.call(calldata( + selector("i8_add_overflow(int8,int8)"), + &[encode_i256(100), encode_i256(100)], + )); + assert!(!r.success, "i8_add_overflow(100, 100) should revert"); +} + +#[test] +fn test_i8_add_boundary_ok() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 127 + 0 = 127, exact max + let r = evm.call(calldata( + selector("i8_add_ok(int8,int8)"), + &[encode_i256(127), encode_i256(0)], + )); + assert!(r.success, "i8_add_ok(127, 0) reverted"); + assert_eq!(decode_i256(&r.output), 127); +} + +#[test] +fn test_i8_add_boundary_overflow() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 127 + 1 = 128 > 127 → revert + let r = evm.call(calldata( + selector("i8_add_overflow(int8,int8)"), + &[encode_i256(127), encode_i256(1)], + )); + assert!(!r.success, "i8_add_overflow(127, 1) should revert"); +} + +#[test] +fn test_i8_add_negative_boundary_ok() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -128 + 0 = -128, exact min + let r = evm.call(calldata( + selector("i8_add_ok(int8,int8)"), + &[encode_i256(-128), encode_i256(0)], + )); + assert!(r.success, "i8_add_ok(-128, 0) reverted"); + assert_eq!(decode_i256(&r.output), -128); +} + +// ============================================================================= +// i8 checked subtraction +// ============================================================================= + +#[test] +fn test_i8_sub_ok() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 50 - 30 = 20 + let r = evm.call(calldata( + selector("i8_sub_ok(int8,int8)"), + &[encode_i256(50), encode_i256(30)], + )); + assert!(r.success, "i8_sub_ok(50, 30) reverted"); + assert_eq!(decode_i256(&r.output), 20); +} + +#[test] +fn test_i8_sub_negative_result_ok() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 30 - 50 = -20, fits in i8 + let r = evm.call(calldata( + selector("i8_sub_ok(int8,int8)"), + &[encode_i256(30), encode_i256(50)], + )); + assert!(r.success, "i8_sub_ok(30, 50) reverted"); + assert_eq!(decode_i256(&r.output), -20); +} + +#[test] +fn test_i8_sub_underflow_reverts() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -100 - 100 = -200 < -128 → revert + let r = evm.call(calldata( + selector("i8_sub_underflow(int8,int8)"), + &[encode_i256(-100), encode_i256(100)], + )); + assert!(!r.success, "i8_sub_underflow(-100, 100) should revert"); +} + +// ============================================================================= +// i8 checked multiplication +// ============================================================================= + +#[test] +fn test_i8_mul_ok() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 10 * 10 = 100, fits in i8 + let r = evm.call(calldata( + selector("i8_mul_ok(int8,int8)"), + &[encode_i256(10), encode_i256(10)], + )); + assert!(r.success, "i8_mul_ok(10, 10) reverted"); + assert_eq!(decode_i256(&r.output), 100); +} + +#[test] +fn test_i8_mul_negative_ok() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -10 * 10 = -100, fits in i8 + let r = evm.call(calldata( + selector("i8_mul_ok(int8,int8)"), + &[encode_i256(-10), encode_i256(10)], + )); + assert!(r.success, "i8_mul_ok(-10, 10) reverted"); + assert_eq!(decode_i256(&r.output), -100); +} + +#[test] +fn test_i8_mul_overflow_reverts() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 20 * 20 = 400 > 127 → revert + let r = evm.call(calldata( + selector("i8_mul_overflow(int8,int8)"), + &[encode_i256(20), encode_i256(20)], + )); + assert!(!r.success, "i8_mul_overflow(20, 20) should revert"); +} + +// ============================================================================= +// i8 signed division (SDIV) +// ============================================================================= + +#[test] +fn test_i8_sdiv_positive() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 100 / 10 = 10 + let r = evm.call(calldata( + selector("i8_sdiv(int8,int8)"), + &[encode_i256(100), encode_i256(10)], + )); + assert!(r.success, "i8_sdiv(100, 10) reverted"); + assert_eq!(decode_i256(&r.output), 10); +} + +#[test] +fn test_i8_sdiv_negative_dividend() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -100 / 10 = -10 (signed division) + let r = evm.call(calldata( + selector("i8_sdiv(int8,int8)"), + &[encode_i256(-100), encode_i256(10)], + )); + assert!(r.success, "i8_sdiv(-100, 10) reverted"); + assert_eq!(decode_i256(&r.output), -10); +} + +#[test] +fn test_i8_sdiv_negative_divisor() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 100 / -10 = -10 (signed division) + let r = evm.call(calldata( + selector("i8_sdiv(int8,int8)"), + &[encode_i256(100), encode_i256(-10)], + )); + assert!(r.success, "i8_sdiv(100, -10) reverted"); + assert_eq!(decode_i256(&r.output), -10); +} + +#[test] +fn test_i8_sdiv_both_negative() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -100 / -10 = 10 (negative / negative = positive) + let r = evm.call(calldata( + selector("i8_sdiv(int8,int8)"), + &[encode_i256(-100), encode_i256(-10)], + )); + assert!(r.success, "i8_sdiv(-100, -10) reverted"); + assert_eq!(decode_i256(&r.output), 10); +} + +// ============================================================================= +// i8 negation +// ============================================================================= + +#[test] +fn test_i8_negate_positive() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -42 (negate 42) + let r = evm.call(calldata(selector("i8_negate(int8)"), &[encode_i256(42)])); + assert!(r.success, "i8_negate(42) reverted"); + assert_eq!(decode_i256(&r.output), -42); +} + +#[test] +fn test_i8_negate_negative() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -(-42) = 42 + let r = evm.call(calldata(selector("i8_negate(int8)"), &[encode_i256(-42)])); + assert!(r.success, "i8_negate(-42) reverted"); + assert_eq!(decode_i256(&r.output), 42); +} + +#[test] +fn test_i8_negate_zero() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -0 = 0 + let r = evm.call(calldata(selector("i8_negate(int8)"), &[encode_i256(0)])); + assert!(r.success, "i8_negate(0) reverted"); + assert_eq!(decode_i256(&r.output), 0); +} + +// ============================================================================= +// i8 signed comparisons (SLT / SGT) +// ============================================================================= + +#[test] +fn test_i8_slt_true() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -50 < 50 → 1 + let r = evm.call(calldata( + selector("i8_slt(int8,int8)"), + &[encode_i256(-50), encode_i256(50)], + )); + assert!(r.success, "i8_slt(-50, 50) reverted"); + assert_eq!(decode_u256(&r.output), 1); +} + +#[test] +fn test_i8_slt_false() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 50 < -50 → 0 + let r = evm.call(calldata( + selector("i8_slt(int8,int8)"), + &[encode_i256(50), encode_i256(-50)], + )); + assert!(r.success, "i8_slt(50, -50) reverted"); + assert_eq!(decode_u256(&r.output), 0); +} + +#[test] +fn test_i8_sgt_true() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 50 > -50 → 1 + let r = evm.call(calldata( + selector("i8_sgt(int8,int8)"), + &[encode_i256(50), encode_i256(-50)], + )); + assert!(r.success, "i8_sgt(50, -50) reverted"); + assert_eq!(decode_u256(&r.output), 1); +} + +#[test] +fn test_i8_sgt_false() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -50 > 50 → 0 + let r = evm.call(calldata( + selector("i8_sgt(int8,int8)"), + &[encode_i256(-50), encode_i256(50)], + )); + assert!(r.success, "i8_sgt(-50, 50) reverted"); + assert_eq!(decode_u256(&r.output), 0); +} + +// ============================================================================= +// Unsafe (unchecked) signed arithmetic +// ============================================================================= + +#[test] +fn test_i8_unsafe_add_ok() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // unsafe_add(50, 30) = 80, no revert + let r = evm.call(calldata( + selector("i8_unsafe_add(int8,int8)"), + &[encode_i256(50), encode_i256(30)], + )); + assert!(r.success, "i8_unsafe_add(50, 30) reverted"); + assert_eq!(decode_i256(&r.output), 80); +} + +#[test] +fn test_i8_unsafe_add_wraps() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // unsafe_add(100, 100) = 200 — UnsafeAdd bypasses all truncation, + // result is raw u256 add (200), returned without sign-extend/mask + let r = evm.call(calldata( + selector("i8_unsafe_add(int8,int8)"), + &[encode_i256(100), encode_i256(100)], + )); + assert!( + r.success, + "i8_unsafe_add(100, 100) should NOT revert (unsafe)" + ); + assert_eq!(decode_i256(&r.output), 200); +} + +#[test] +fn test_i8_unsafe_sub_wraps() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // unsafe_sub(-100, 100) = -200 in u256 two's complement — no truncation + let r = evm.call(calldata( + selector("i8_unsafe_sub(int8,int8)"), + &[encode_i256(-100), encode_i256(100)], + )); + assert!( + r.success, + "i8_unsafe_sub(-100, 100) should NOT revert (unsafe)" + ); + assert_eq!(decode_i256(&r.output), -200); +} + +#[test] +fn test_i8_unsafe_mul_wraps() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // unsafe_mul(20, 20) = 400 — raw u256 mul, no truncation + let r = evm.call(calldata( + selector("i8_unsafe_mul(int8,int8)"), + &[encode_i256(20), encode_i256(20)], + )); + assert!( + r.success, + "i8_unsafe_mul(20, 20) should NOT revert (unsafe)" + ); + assert_eq!(decode_i256(&r.output), 400); +} + +// ============================================================================= +// Casts between signed and unsigned +// ============================================================================= + +#[test] +fn test_cast_i8_to_u8_positive() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 42i8 as u8 = 42 + let r = evm.call(calldata( + selector("cast_i8_to_u8(int8)"), + &[encode_i256(42)], + )); + assert!(r.success, "cast_i8_to_u8(42) reverted"); + assert_eq!(decode_u256(&r.output), 42); +} + +#[test] +fn test_cast_i8_to_u8_negative() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // -1i8 as u8 = 255 (0xFF) + let r = evm.call(calldata( + selector("cast_i8_to_u8(int8)"), + &[encode_i256(-1)], + )); + assert!(r.success, "cast_i8_to_u8(-1) reverted"); + assert_eq!(decode_u256(&r.output), 255); +} + +#[test] +fn test_cast_u8_to_i8_small() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 42u8 as i8 = 42 (fits in positive range) + let r = evm.call(calldata( + selector("cast_u8_to_i8(uint8)"), + &[encode_u256(42)], + )); + assert!(r.success, "cast_u8_to_i8(42) reverted"); + assert_eq!(decode_i256(&r.output), 42); +} + +#[test] +fn test_cast_u8_to_i8_high() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 200u8 as i8 = -56 (0xC8 sign-extended) + let r = evm.call(calldata( + selector("cast_u8_to_i8(uint8)"), + &[encode_u256(200)], + )); + assert!(r.success, "cast_u8_to_i8(200) reverted"); + assert_eq!(decode_i256(&r.output), -56); +} + +#[test] +fn test_cast_i8_to_u256_positive() { + let bc = compile_contract("examples/tests/test_signed_widths.edge"); + let mut evm = EvmHandle::new(bc); + // 42i8 as u256 = 42 + let r = evm.call(calldata( + selector("cast_i8_to_u256(int8)"), + &[encode_i256(42)], + )); + assert!(r.success, "cast_i8_to_u256(42) reverted"); + assert_eq!(decode_u256(&r.output), 42); +} + +// ============================================================================= +// Compile-time constant overflow rejection (signed) +// ============================================================================= + +fn assert_compile_error(source: &str, expected_messages: &[&str], expected_rendered: &[&str]) { + let mut compiler = Compiler::from_source(source); + let result = compiler.compile(); + assert!( + result.is_err(), + "Expected compilation to fail, but it succeeded.\nSource:\n{source}" + ); + + let messages = compiler.diagnostic_messages(); + let all_messages = messages.join("\n"); + for exp in expected_messages { + assert!( + all_messages.contains(exp), + "Expected message containing '{exp}', got:\n{all_messages}\nSource:\n{source}" + ); + } + + let rendered = compiler.render_diagnostics(); + for exp in expected_rendered { + assert!( + rendered.contains(exp), + "Expected rendered output containing '{exp}', got:\n{rendered}\nSource:\n{source}" + ); + } +} + +fn assert_compiles(source: &str) { + let mut compiler = Compiler::from_source(source); + let result = compiler.compile(); + assert!( + result.is_ok(), + "Expected compilation to succeed, but it failed.\nSource:\n{source}\nError: {:?}", + result.unwrap_err() + ); +} + +#[test] +fn test_mixed_width_i8_i256_rejected() { + assert_compile_error( + "contract T { pub fn f(a: i8, b: i256) -> (i256) { return a + b; } }", + &["mismatched types", "i8", "i256"], + &["use an explicit cast"], + ); +} + +#[test] +fn test_mixed_width_i8_u8_rejected() { + assert_compile_error( + "contract T { pub fn f(a: i8, b: u8) -> (u256) { return a + b; } }", + &["mismatched types", "i8", "u8"], + &["use an explicit cast"], + ); +} + +#[test] +fn test_unsuffixed_literal_adopts_i8() { + // `x + 2` where x: i8 — the `2` should be treated as i8 + assert_compiles("contract T { pub fn f(x: i8) -> (i8) { return x + 2; } }"); +} + +#[test] +fn test_explicit_cast_fixes_signed_mismatch() { + assert_compiles( + "contract T { pub fn f(a: i8, b: i256) -> (i256) { return (a as i256) + b; } }", + ); +} diff --git a/crates/ir/src/ast_helpers.rs b/crates/ir/src/ast_helpers.rs index 63ea4f8..51d91ac 100644 --- a/crates/ir/src/ast_helpers.rs +++ b/crates/ir/src/ast_helpers.rs @@ -112,6 +112,11 @@ pub fn shr(shift_amount: RcExpr, value: RcExpr) -> RcExpr { bop(EvmBinaryOp::Shr, shift_amount, value) } +/// Shorthand: arithmetic shift right (`SAR` `shift_amount`, `value` — EVM operand order) +pub fn sar(shift_amount: RcExpr, value: RcExpr) -> RcExpr { + bop(EvmBinaryOp::Sar, shift_amount, value) +} + /// Shorthand: bitwise AND pub fn bitand(lhs: RcExpr, rhs: RcExpr) -> RcExpr { bop(EvmBinaryOp::And, lhs, rhs) @@ -277,3 +282,81 @@ pub fn keccak256(offset: RcExpr, size: RcExpr, state: RcExpr) -> RcExpr { pub fn mem_region(region_id: i64, size_words: i64) -> RcExpr { Rc::new(EvmExpr::MemRegion(region_id, size_words)) } + +// ---- Integer width helpers ---- + +/// Create a mask constant for the given bit width: `(1 << bit_width) - 1`. +/// For `bit_width` >= 256, returns all-ones (no-op mask). +pub fn width_mask(bit_width: u16, ctx: EvmContext) -> RcExpr { + if bit_width >= 256 { + const_bigint( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string(), + ctx, + ) + } else if bit_width <= 63 { + let mask_val = (1i64 << bit_width) - 1; + const_int(mask_val, ctx) + } else { + // Build hex mask string: (1 << N) - 1 + let full_nibbles = (bit_width / 4) as usize; + let remainder = bit_width % 4; + let mut hex = String::with_capacity(full_nibbles + 1); + if remainder > 0 { + hex.push(char::from_digit((1u32 << remainder) - 1, 16).unwrap()); + } + for _ in 0..full_nibbles { + hex.push('f'); + } + const_bigint(hex, ctx) + } +} + +/// Mask a value to the given unsigned bit width: `value & ((1 << bits) - 1)`. +/// No-op for 256-bit types. +pub fn mask_to_width(value: RcExpr, bit_width: u16, ctx: EvmContext) -> RcExpr { + if bit_width >= 256 { + value + } else { + bitand(value, width_mask(bit_width, ctx)) + } +} + +/// Sign-extend a value from the given bit width to 256 bits. +/// +/// Uses the branchless XOR-SUB trick: +/// `sign_bit = 1 << (bits - 1)` +/// `result = ((value & mask) ^ sign_bit) - sign_bit` +/// +/// This correctly sign-extends without needing conditional logic. +/// The SUB is unchecked (wrapping) since it intentionally underflows for negative values. +/// No-op for 256-bit types. +pub fn sign_extend(value: RcExpr, bit_width: u16, ctx: EvmContext) -> RcExpr { + if bit_width >= 256 { + return value; + } + let mask = width_mask(bit_width, ctx.clone()); + let sign_bit = sign_bit_const(bit_width, ctx); + let truncated = bitand(value, mask); + // (truncated ^ sign_bit) - sign_bit (wrapping SUB) + let xored = bop(EvmBinaryOp::Xor, truncated, Rc::clone(&sign_bit)); + bop(EvmBinaryOp::Sub, xored, sign_bit) +} + +/// Create a constant for the sign bit position: `1 << (bit_width - 1)`. +fn sign_bit_const(bit_width: u16, ctx: EvmContext) -> RcExpr { + let shift = bit_width - 1; + if shift <= 62 { + const_int(1i64 << shift, ctx) + } else { + // Build hex: "1" followed by (shift/4) zeros, with leading nibble adjustment + let full_nibbles = (shift / 4) as usize; + let remainder = shift % 4; + let mut hex = String::with_capacity(full_nibbles + 1); + let lead = 1u32 << remainder; + hex.push(char::from_digit(lead, 16).unwrap()); + for _ in 0..full_nibbles { + hex.push('0'); + } + const_bigint(hex, ctx) + } +} diff --git a/crates/ir/src/lib.rs b/crates/ir/src/lib.rs index 1fad318..040854f 100644 --- a/crates/ir/src/lib.rs +++ b/crates/ir/src/lib.rs @@ -39,6 +39,7 @@ pub mod var_opt; use std::rc::Rc; pub use costs::OptimizeFor; +use schema::{EvmBaseType, EvmConstant, EvmType}; pub use schema::{EvmContract, EvmExpr, EvmProgram, RcExpr}; /// Errors that can occur during IR lowering or optimization. @@ -220,6 +221,21 @@ pub fn lower_and_optimize( let mut optimized_runtime = sexp::sexp_to_expr(extracted_sexp)?; + // Check for compile-time-detectable constant overflows in narrow types. + // This catches overflow revealed by egglog const-folding (e.g. through + // inlined constants). The lowering-time check catches literal cases with + // source spans; this is the fallback for optimization-revealed cases. + let overflow_errors = check_const_overflow(&optimized_runtime); + if !overflow_errors.is_empty() { + let mut diag = edge_diagnostics::Diagnostic::error( + "arithmetic overflow detected after optimization", + ); + for err in &overflow_errors { + diag = diag.with_note(err.clone()); + } + return Err(IrError::Diagnostic(diag)); + } + // Post-egglog cleanup: simplify state params and remove dead code optimized_runtime = cleanup::cleanup_expr_pub(&optimized_runtime); @@ -380,6 +396,139 @@ fn collect_call_names_rec(expr: &schema::RcExpr, names: &mut std::collections::H } } +/// Check for constant values that overflow their declared narrow type. +/// +/// After egglog optimization + extraction, walk the IR tree and look for +/// `Const(val, UIntT(N))` where `N < 256` and `val >= 2^N`. This detects +/// compile-time-provable overflows like `250u8 + 250u8`. +fn check_const_overflow(expr: &schema::RcExpr) -> Vec { + let mut errors = Vec::new(); + check_const_overflow_rec(expr, &mut errors); + errors +} + +fn check_const_overflow_rec(expr: &schema::RcExpr, errors: &mut Vec) { + match expr.as_ref() { + EvmExpr::Const(val, EvmType::Base(EvmBaseType::UIntT(width)), _) if *width < 256 => { + let max_val = if *width == 0 { + 0u128 + } else { + (1u128 << *width) - 1 + }; + let exceeds = match val { + EvmConstant::SmallInt(n) => { + if *n < 0 { + true // negative value in unsigned type + } else { + *n as u128 > max_val + } + } + EvmConstant::LargeInt(hex) => { + // LargeInt always exceeds narrow types (it's > i64::MAX) + // unless the hex happens to be small. Parse and check. + ruint::aliases::U256::from_str_radix(hex, 16) + .map(|v| v > ruint::aliases::U256::from(max_val)) + .unwrap_or(false) + } + _ => false, + }; + if exceeds { + errors.push(format!( + "constant value {} overflows u{} (max {})", + match val { + EvmConstant::SmallInt(n) => format!("{n}"), + EvmConstant::LargeInt(hex) => format!("0x{hex}"), + _ => "?".to_string(), + }, + width, + max_val + )); + } + } + EvmExpr::Const(val, EvmType::Base(EvmBaseType::IntT(width)), _) if *width < 256 => { + let half = if *width <= 1 { + 1i128 + } else { + 1i128 << (*width - 1) + }; + let min_val = -half; + let max_val = half - 1; + let exceeds = match val { + EvmConstant::SmallInt(n) => (*n as i128) < min_val || (*n as i128) > max_val, + _ => false, + }; + if exceeds { + errors.push(format!( + "constant value {} overflows i{} (range {}..={})", + match val { + EvmConstant::SmallInt(n) => format!("{n}"), + _ => "?".to_string(), + }, + width, + min_val, + max_val + )); + } + } + _ => {} + } + + // Recurse into children + match expr.as_ref() { + EvmExpr::Concat(a, b) | EvmExpr::Bop(_, a, b) | EvmExpr::DoWhile(a, b) => { + check_const_overflow_rec(a, errors); + check_const_overflow_rec(b, errors); + } + EvmExpr::Uop(_, a) | EvmExpr::VarStore(_, a) | EvmExpr::Get(a, _) => { + check_const_overflow_rec(a, errors); + } + EvmExpr::If(c, i, t, e) => { + check_const_overflow_rec(c, errors); + check_const_overflow_rec(i, errors); + check_const_overflow_rec(t, errors); + check_const_overflow_rec(e, errors); + } + EvmExpr::LetBind(_, init, body) => { + check_const_overflow_rec(init, errors); + check_const_overflow_rec(body, errors); + } + EvmExpr::Top(_, a, b, c) | EvmExpr::Revert(a, b, c) | EvmExpr::ReturnOp(a, b, c) => { + check_const_overflow_rec(a, errors); + check_const_overflow_rec(b, errors); + check_const_overflow_rec(c, errors); + } + EvmExpr::Log(_, topics, d, s, st) => { + for t in topics { + check_const_overflow_rec(t, errors); + } + check_const_overflow_rec(d, errors); + check_const_overflow_rec(s, errors); + check_const_overflow_rec(st, errors); + } + EvmExpr::ExtCall(a, b, c, d, e, f, g) => { + check_const_overflow_rec(a, errors); + check_const_overflow_rec(b, errors); + check_const_overflow_rec(c, errors); + check_const_overflow_rec(d, errors); + check_const_overflow_rec(e, errors); + check_const_overflow_rec(f, errors); + check_const_overflow_rec(g, errors); + } + EvmExpr::EnvRead(_, s) => check_const_overflow_rec(s, errors), + EvmExpr::EnvRead1(_, a, s) => { + check_const_overflow_rec(a, errors); + check_const_overflow_rec(s, errors); + } + EvmExpr::Function(_, _, _, body) => check_const_overflow_rec(body, errors), + EvmExpr::Call(_, args) => { + for a in args { + check_const_overflow_rec(a, errors); + } + } + _ => {} + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ir/src/optimizations/u256_const_fold.egg b/crates/ir/src/optimizations/u256_const_fold.egg index 045c97d..e1240b8 100644 --- a/crates/ir/src/optimizations/u256_const_fold.egg +++ b/crates/ir/src/optimizations/u256_const_fold.egg @@ -223,6 +223,44 @@ (Base (UIntT 256)) (InFunction "__opt__"))))) :ruleset u256-const-fold) +;; ---- Narrow-type-preserving constant folding ---- +;; When both operands are Const with the same narrow type UIntT(w) where +;; w < 256, produce a result with UIntT(w) so that post-extraction overflow +;; detection can catch compile-time-detectable overflows like 250u8 + 250u8. + +;; ADD (narrow type preserving) +(rule ((= e (Bop (OpAdd) a b)) + (= a (Const ca (Base (UIntT w)) ctxa)) + (= b (Const cb (Base (UIntT w)) ctxb)) + (< w 256) + (= va (const-u256-val a)) + (= vb (const-u256-val b))) + ((union e (Const (LargeInt (u256-to-hex (u256-add va vb))) + (Base (UIntT w)) (InFunction "__opt__")))) + :ruleset u256-const-fold) + +;; SUB (narrow type preserving) +(rule ((= e (Bop (OpSub) a b)) + (= a (Const ca (Base (UIntT w)) ctxa)) + (= b (Const cb (Base (UIntT w)) ctxb)) + (< w 256) + (= va (const-u256-val a)) + (= vb (const-u256-val b))) + ((union e (Const (LargeInt (u256-to-hex (u256-sub va vb))) + (Base (UIntT w)) (InFunction "__opt__")))) + :ruleset u256-const-fold) + +;; MUL (narrow type preserving) +(rule ((= e (Bop (OpMul) a b)) + (= a (Const ca (Base (UIntT w)) ctxa)) + (= b (Const cb (Base (UIntT w)) ctxb)) + (< w 256) + (= va (const-u256-val a)) + (= vb (const-u256-val b))) + ((union e (Const (LargeInt (u256-to-hex (u256-mul va vb))) + (Base (UIntT w)) (InFunction "__opt__")))) + :ruleset u256-const-fold) + ;; ---- Normalization: small LargeInt → SmallInt ---- ;; When a LargeInt hex result fits in i64, convert back to SmallInt for ;; better extraction cost and compatibility with existing i64 rules. diff --git a/crates/ir/src/to_egglog/calls.rs b/crates/ir/src/to_egglog/calls.rs index cb23ffd..cfe579d 100644 --- a/crates/ir/src/to_egglog/calls.rs +++ b/crates/ir/src/to_egglog/calls.rs @@ -477,6 +477,7 @@ impl AstToEgglog { match expr { edge_ast::Expr::Literal(lit) => match lit.as_ref() { edge_ast::Lit::Bool(_, _) => EvmType::Base(EvmBaseType::BoolT), + edge_ast::Lit::Int(_, Some(pt), _) => self.lower_primitive_type(pt), _ => EvmType::Base(EvmBaseType::UIntT(256)), }, edge_ast::Expr::Ident(ident) => { @@ -487,6 +488,12 @@ impl AstToEgglog { } EvmType::Base(EvmBaseType::UIntT(256)) } + edge_ast::Expr::Cast(_, target_type, _) => self.lower_type_sig(target_type), + edge_ast::Expr::Paren(inner, _) => self.infer_expr_type(inner), + edge_ast::Expr::At(name, _, _) => match name.name.as_str() { + "caller" | "origin" | "coinbase" | "address" => EvmType::Base(EvmBaseType::AddrT), + _ => EvmType::Base(EvmBaseType::UIntT(256)), + }, _ => EvmType::Base(EvmBaseType::UIntT(256)), } } diff --git a/crates/ir/src/to_egglog/expr.rs b/crates/ir/src/to_egglog/expr.rs index 213f6f0..8d5011a 100644 --- a/crates/ir/src/to_egglog/expr.rs +++ b/crates/ir/src/to_egglog/expr.rs @@ -12,6 +12,89 @@ use crate::{ IrError, }; +/// Check if an expression is an untyped (unsuffixed) literal. +/// Unsuffixed int literals and bool literals are considered untyped and +/// will adopt the type of the other operand in a binary expression. +fn is_untyped_literal(expr: &edge_ast::Expr) -> bool { + match expr { + edge_ast::Expr::Literal(lit) => match lit.as_ref() { + // no suffix + edge_ast::Lit::Int(_, None, _) | edge_ast::Lit::Bool(_, _) => true, + _ => false, + }, + _ => false, + } +} + +/// Format an `EvmBaseType` as a user-facing type name. +fn fmt_base_type(bt: &EvmBaseType) -> String { + match bt { + EvmBaseType::UIntT(n) => format!("u{n}"), + EvmBaseType::IntT(n) => format!("i{n}"), + EvmBaseType::BytesT(n) => format!("bytes{n}"), + EvmBaseType::AddrT => "addr".to_string(), + EvmBaseType::BoolT => "bool".to_string(), + EvmBaseType::UnitT => "()".to_string(), + EvmBaseType::StateT => "state".to_string(), + } +} + +/// Extract the source text for an AST expression from the file source. +fn expr_src_text(expr: &edge_ast::Expr) -> String { + let span = expr.span(); + // Walk up: try the expression's own file, which should have the source + if let Some(ref file) = span.file { + if let Some(ref src) = file.source { + if let Some(text) = src.get(span.start..=span.end) { + return text.trim().to_string(); + } + } + } + // Fallback: extract name/value from common leaf nodes + match expr { + edge_ast::Expr::Ident(id) => id.name.clone(), + edge_ast::Expr::Literal(lit) => match lit.as_ref() { + edge_ast::Lit::Int(bytes, _, _) => { + let val = ruint::aliases::U256::from_be_bytes::<32>(*bytes); + format!("{val}") + } + edge_ast::Lit::Bool(b, _) => format!("{b}"), + edge_ast::Lit::Str(s, _) => format!("\"{s}\""), + edge_ast::Lit::Hex(bytes, _) => { + format!( + "0x{}", + bytes.iter().map(|b| format!("{b:02x}")).collect::() + ) + } + edge_ast::Lit::Bin(bytes, _) => { + format!( + "0b{}", + bytes.iter().map(|b| format!("{b:08b}")).collect::() + ) + } + }, + _ => "expr".to_string(), + } +} + +/// Extract a constant U256 value from an IR Const node, if it is one. +fn const_value(expr: &RcExpr) -> Option { + match expr.as_ref() { + EvmExpr::Const(EvmConstant::SmallInt(n), _, _) => { + if *n >= 0 { + Some(ruint::aliases::U256::from(*n as u64)) + } else { + // Negative values wrap around in U256 + Some(ruint::aliases::U256::MAX - ruint::aliases::U256::from((-*n - 1) as u64)) + } + } + EvmExpr::Const(EvmConstant::LargeInt(hex), _, _) => { + ruint::aliases::U256::from_str_radix(hex, 16).ok() + } + _ => None, + } +} + impl AstToEgglog { /// Lower a statement. pub(crate) fn lower_stmt(&mut self, stmt: &edge_ast::Stmt) -> Result { @@ -253,14 +336,52 @@ impl AstToEgglog { if let Some(result) = self.try_operator_overload(lhs, op, rhs, span)? { return Ok(result); } + let lhs_ty = self.infer_expr_type(lhs); + let rhs_ty = self.infer_expr_type(rhs); + + // Unsuffixed literals adopt the type of the other operand. + // e.g. `x: u8 = 3; x - 2;` → the `2` becomes u8. + let lhs_untyped = is_untyped_literal(lhs); + let rhs_untyped = is_untyped_literal(rhs); + let operand_ty = if lhs_untyped && !rhs_untyped { + rhs_ty + } else if rhs_untyped && !lhs_untyped { + lhs_ty + } else { + // Both typed (or both untyped) — check for mismatches + if let (EvmType::Base(ref lbt), EvmType::Base(ref rbt)) = (&lhs_ty, &rhs_ty) { + let lw = lbt.bit_width(); + let rw = rbt.bit_width(); + let l_signed = matches!(lbt, EvmBaseType::IntT(_)); + let r_signed = matches!(rbt, EvmBaseType::IntT(_)); + if (lw != rw || l_signed != r_signed) && lw > 0 && rw > 0 { + let lty_name = fmt_base_type(lbt); + let rty_name = fmt_base_type(rbt); + let lhs_src = expr_src_text(lhs); + let rhs_src = expr_src_text(rhs); + return Err(IrError::Diagnostic( + edge_diagnostics::Diagnostic::error(format!( + "mismatched types: `{lty_name}` and `{rty_name}`" + )) + .with_label(span.clone(), format!("cannot apply `{op:?}` to `{lty_name}` and `{rty_name}`")) + .with_note(format!( + "use an explicit cast: `{lhs_src} as {rty_name}` or `{rhs_src} as {lty_name}`" + )), + )); + } + } + lhs_ty + }; + let lhs_ir = self.lower_expr(lhs)?; let rhs_ir = self.lower_expr(rhs)?; - self.lower_binary_op(op, lhs_ir, rhs_ir) + self.lower_binary_op(op, lhs_ir, rhs_ir, &operand_ty, span) } edge_ast::Expr::Unary(op, expr, _span) => { + let operand_ty = self.infer_expr_type(expr); let expr_ir = self.lower_expr(expr)?; - self.lower_unary_op(op, expr_ir) + self.lower_unary_op(op, expr_ir, &operand_ty) } edge_ast::Expr::Ternary(cond, true_expr, false_expr, _span) => { @@ -462,6 +583,8 @@ impl AstToEgglog { self.lower_inline_asm(inputs, outputs, ops, span) } + edge_ast::Expr::Cast(expr, target_type, _span) => self.lower_cast(expr, target_type), + // TODO: implement remaining expression types other => Err(IrError::Unsupported(format!( "Expression type not yet supported: {other:?}" @@ -704,19 +827,78 @@ impl AstToEgglog { } } - /// Lower a binary operator. + /// Lower a binary operator with type-aware operator selection and width truncation. + /// + /// For signed integer types (`IntT`), uses signed EVM opcodes (SDIV, SMOD, SLT, SGT, SAR). + /// For sub-256-bit types, truncates results to the correct width after arithmetic. pub(crate) fn lower_binary_op( &self, op: &edge_ast::BinOp, lhs: RcExpr, rhs: RcExpr, + operand_ty: &EvmType, + span: &edge_types::span::Span, ) -> Result { + let is_signed = matches!(operand_ty, EvmType::Base(EvmBaseType::IntT(_))); + let bit_width = match operand_ty { + EvmType::Base(bt) => bt.bit_width(), + _ => 256, + }; + let ir_op = match op { - edge_ast::BinOp::Add | edge_ast::BinOp::AddAssign => EvmBinaryOp::CheckedAdd, - edge_ast::BinOp::Sub | edge_ast::BinOp::SubAssign => EvmBinaryOp::CheckedSub, - edge_ast::BinOp::Mul | edge_ast::BinOp::MulAssign => EvmBinaryOp::CheckedMul, - edge_ast::BinOp::Div | edge_ast::BinOp::DivAssign => EvmBinaryOp::Div, - edge_ast::BinOp::Mod | edge_ast::BinOp::ModAssign => EvmBinaryOp::Mod, + edge_ast::BinOp::Add | edge_ast::BinOp::AddAssign => { + if bit_width < 256 { + return self.width_checked_op( + EvmBinaryOp::Add, + lhs, + rhs, + bit_width, + is_signed, + span, + ); + } + EvmBinaryOp::CheckedAdd + } + edge_ast::BinOp::Sub | edge_ast::BinOp::SubAssign => { + if bit_width < 256 { + return self.width_checked_op( + EvmBinaryOp::Sub, + lhs, + rhs, + bit_width, + is_signed, + span, + ); + } + EvmBinaryOp::CheckedSub + } + edge_ast::BinOp::Mul | edge_ast::BinOp::MulAssign => { + if bit_width < 256 { + return self.width_checked_op( + EvmBinaryOp::Mul, + lhs, + rhs, + bit_width, + is_signed, + span, + ); + } + EvmBinaryOp::CheckedMul + } + edge_ast::BinOp::Div | edge_ast::BinOp::DivAssign => { + if is_signed { + EvmBinaryOp::SDiv + } else { + EvmBinaryOp::Div + } + } + edge_ast::BinOp::Mod | edge_ast::BinOp::ModAssign => { + if is_signed { + EvmBinaryOp::SMod + } else { + EvmBinaryOp::Mod + } + } edge_ast::BinOp::Exp | edge_ast::BinOp::ExpAssign => EvmBinaryOp::Exp, edge_ast::BinOp::BitwiseAnd | edge_ast::BinOp::BitwiseAndAssign => EvmBinaryOp::And, edge_ast::BinOp::BitwiseOr | edge_ast::BinOp::BitwiseOrAssign => EvmBinaryOp::Or, @@ -724,12 +906,19 @@ impl AstToEgglog { edge_ast::BinOp::Shl | edge_ast::BinOp::ShlAssign => { // IR convention: Bop(Shl, shift_amount, value) // AST: value << shift → swap to (shift, value) - return Ok(ast_helpers::bop(EvmBinaryOp::Shl, rhs, lhs)); + let result = ast_helpers::bop(EvmBinaryOp::Shl, rhs, lhs); + return Ok(self.truncate_to_width(result, bit_width, is_signed)); } edge_ast::BinOp::Shr | edge_ast::BinOp::ShrAssign => { // IR convention: Bop(Shr, shift_amount, value) // AST: value >> shift → swap to (shift, value) - return Ok(ast_helpers::bop(EvmBinaryOp::Shr, rhs, lhs)); + // Signed: use SAR (arithmetic shift right) + let shr_op = if is_signed { + EvmBinaryOp::Sar + } else { + EvmBinaryOp::Shr + }; + return Ok(ast_helpers::bop(shr_op, rhs, lhs)); } edge_ast::BinOp::LogicalAnd => EvmBinaryOp::LogAnd, edge_ast::BinOp::LogicalOr => EvmBinaryOp::LogOr, @@ -739,20 +928,203 @@ impl AstToEgglog { let eq_expr = ast_helpers::eq(lhs, rhs); return Ok(ast_helpers::iszero(eq_expr)); } - edge_ast::BinOp::Lt => EvmBinaryOp::Lt, + edge_ast::BinOp::Lt => { + if is_signed { + EvmBinaryOp::SLt + } else { + EvmBinaryOp::Lt + } + } edge_ast::BinOp::Lte => { // a <= b -> IsZero(Gt(a, b)) - let gt_expr = ast_helpers::bop(EvmBinaryOp::Gt, lhs, rhs); + let gt_op = if is_signed { + EvmBinaryOp::SGt + } else { + EvmBinaryOp::Gt + }; + let gt_expr = ast_helpers::bop(gt_op, lhs, rhs); return Ok(ast_helpers::iszero(gt_expr)); } - edge_ast::BinOp::Gt => EvmBinaryOp::Gt, + edge_ast::BinOp::Gt => { + if is_signed { + EvmBinaryOp::SGt + } else { + EvmBinaryOp::Gt + } + } edge_ast::BinOp::Gte => { // a >= b -> IsZero(Lt(a, b)) - let lt_expr = ast_helpers::bop(EvmBinaryOp::Lt, lhs, rhs); + let lt_op = if is_signed { + EvmBinaryOp::SLt + } else { + EvmBinaryOp::Lt + }; + let lt_expr = ast_helpers::bop(lt_op, lhs, rhs); return Ok(ast_helpers::iszero(lt_expr)); } }; - Ok(ast_helpers::bop(ir_op, lhs, rhs)) + + let result = ast_helpers::bop(ir_op, lhs, rhs); + + // Truncate arithmetic results to the correct width for sub-256-bit types. + // Comparisons and logical ops return booleans — no truncation needed. + let needs_truncation = matches!( + op, + edge_ast::BinOp::Add + | edge_ast::BinOp::AddAssign + | edge_ast::BinOp::Sub + | edge_ast::BinOp::SubAssign + | edge_ast::BinOp::Mul + | edge_ast::BinOp::MulAssign + | edge_ast::BinOp::Div + | edge_ast::BinOp::DivAssign + | edge_ast::BinOp::Mod + | edge_ast::BinOp::ModAssign + | edge_ast::BinOp::Exp + | edge_ast::BinOp::ExpAssign + | edge_ast::BinOp::BitwiseOr + | edge_ast::BinOp::BitwiseOrAssign + | edge_ast::BinOp::BitwiseXor + | edge_ast::BinOp::BitwiseXorAssign + ); + + if needs_truncation { + Ok(self.truncate_to_width(result, bit_width, is_signed)) + } else { + Ok(result) + } + } + + /// Truncate a value to the given integer width. + /// For unsigned: AND with mask. For signed: sign-extend. No-op for 256-bit. + fn truncate_to_width(&self, value: RcExpr, bit_width: u16, is_signed: bool) -> RcExpr { + if bit_width >= 256 { + return value; + } + if is_signed { + ast_helpers::sign_extend(value, bit_width, self.current_ctx.clone()) + } else { + ast_helpers::mask_to_width(value, bit_width, self.current_ctx.clone()) + } + } + + /// Emit a width-specific checked arithmetic operation for sub-256-bit types. + /// + /// For sub-256-bit integers, the u256-level `CheckedAdd`/`CheckedSub`/`CheckedMul` + /// never trigger (inputs are too small to overflow u256). Instead, we: + /// 1. Compute the full u256 result with an unchecked op + /// 2. Truncate to the target width (mask for unsigned, sign-extend for signed) + /// 3. Compare truncated vs full — if different, the result overflowed the width + /// 4. Revert on overflow + fn width_checked_op( + &self, + op: EvmBinaryOp, + lhs: RcExpr, + rhs: RcExpr, + bit_width: u16, + is_signed: bool, + span: &edge_types::span::Span, + ) -> Result { + // Compile-time overflow detection: if both operands are constants, + // compute the result and check if it fits in the target width. + if let (Some(lv), Some(rv)) = (const_value(&lhs), const_value(&rhs)) { + let result = match op { + EvmBinaryOp::Add => lv.wrapping_add(rv), + EvmBinaryOp::Sub => lv.wrapping_sub(rv), + EvmBinaryOp::Mul => lv.wrapping_mul(rv), + _ => lv, // won't happen for checked ops + }; + let max_val = if bit_width == 0 { + ruint::aliases::U256::ZERO + } else { + (ruint::aliases::U256::from(1u64) << bit_width) - ruint::aliases::U256::from(1u64) + }; + if result > max_val { + let op_name = match op { + EvmBinaryOp::Add => "+", + EvmBinaryOp::Sub => "-", + EvmBinaryOp::Mul => "*", + _ => "?", + }; + let ty_name = if is_signed { + format!("i{bit_width}") + } else { + format!("u{bit_width}") + }; + return Err(IrError::Diagnostic( + edge_diagnostics::Diagnostic::error(format!( + "attempt to compute `{lv}_{ty_name} {op_name} {rv}_{ty_name}`, which would overflow" + )) + .with_label(span.clone(), format!("overflows `{ty_name}` (max {max_val})")) + )); + } + } + + let ctx = self.current_ctx.clone(); + + // Step 1: unchecked full-width result + let full = ast_helpers::bop(op, lhs, rhs); + + // Step 2: truncate to target width + let truncated = if is_signed { + ast_helpers::sign_extend(Rc::clone(&full), bit_width, ctx.clone()) + } else { + ast_helpers::mask_to_width(Rc::clone(&full), bit_width, ctx.clone()) + }; + + // Step 3: overflow check — if truncated != full, width overflow occurred + let is_overflow = ast_helpers::iszero(ast_helpers::eq(Rc::clone(&truncated), full)); + + // Step 4: revert on overflow, otherwise return truncated value + let empty = ast_helpers::empty(EvmType::Base(EvmBaseType::UnitT), ctx.clone()); + let revert_expr = ast_helpers::revert( + ast_helpers::const_int(0, ctx.clone()), + ast_helpers::const_int(0, ctx), + Rc::clone(&empty), + ); + + // If(overflow?, inputs, then=revert, else=truncated_value) + Ok(ast_helpers::if_then_else( + is_overflow, + empty, + revert_expr, + truncated, + )) + } + + /// Lower a type cast: `expr as TargetType`. + /// + /// Casts between integer types: + /// - Narrowing unsigned: mask to target width + /// - Widening unsigned: no-op (already zero-extended in u256) + /// - Narrowing signed: mask then sign-extend to target width + /// - Widening signed → unsigned: mask to remove sign extension + /// - Unsigned → signed: sign-extend to target width + /// - Same width: no-op (or reinterpret signedness) + fn lower_cast( + &mut self, + expr: &edge_ast::Expr, + target_type: &edge_ast::ty::TypeSig, + ) -> Result { + let val = self.lower_expr(expr)?; + let target_evm_ty = self.lower_type_sig(target_type); + let ctx = self.current_ctx.clone(); + + let target_bits = match &target_evm_ty { + EvmType::Base(bt) => bt.bit_width(), + _ => return Ok(val), // Non-integer cast: no-op + }; + let target_signed = matches!(target_evm_ty, EvmType::Base(EvmBaseType::IntT(_))); + + if target_signed { + // Cast to signed: sign-extend from target width + // First mask to target width, then sign-extend + let masked = ast_helpers::mask_to_width(val, target_bits, ctx.clone()); + Ok(ast_helpers::sign_extend(masked, target_bits, ctx)) + } else { + // Cast to unsigned: mask to target width + Ok(ast_helpers::mask_to_width(val, target_bits, ctx)) + } } /// Map a binary operator to its corresponding trait name and method. @@ -863,18 +1235,28 @@ impl AstToEgglog { } } - /// Lower a unary operator. + /// Lower a unary operator with type-aware width truncation. pub(crate) fn lower_unary_op( &self, op: &edge_ast::UnaryOp, expr: RcExpr, + operand_ty: &EvmType, ) -> Result { + let is_signed = matches!(operand_ty, EvmType::Base(EvmBaseType::IntT(_))); + let bit_width = match operand_ty { + EvmType::Base(bt) => bt.bit_width(), + _ => 256, + }; + let ir_op = match op { edge_ast::UnaryOp::Neg => EvmUnaryOp::Neg, edge_ast::UnaryOp::BitwiseNot => EvmUnaryOp::Not, - edge_ast::UnaryOp::LogicalNot => EvmUnaryOp::IsZero, + edge_ast::UnaryOp::LogicalNot => return Ok(ast_helpers::iszero(expr)), }; - Ok(ast_helpers::uop(ir_op, expr)) + let result = ast_helpers::uop(ir_op, expr); + + // Neg and Not can produce values outside the width — truncate + Ok(self.truncate_to_width(result, bit_width, is_signed)) } /// Resolve a multi-component path to a name. diff --git a/crates/ir/src/to_egglog/function.rs b/crates/ir/src/to_egglog/function.rs index cd42086..48fd62a 100644 --- a/crates/ir/src/to_egglog/function.rs +++ b/crates/ir/src/to_egglog/function.rs @@ -81,18 +81,21 @@ impl AstToEgglog { ast_helpers::const_int(calldata_offset as i64, self.current_ctx.clone()), Rc::clone(&self.current_state), )); - // Mask address-typed params to 20 bytes to clean dirty upper bits - let param_val = if ty == EvmType::Base(EvmBaseType::AddrT) { - Rc::new(EvmExpr::Bop( - EvmBinaryOp::And, - raw_val, - ast_helpers::const_bigint( - "ffffffffffffffffffffffffffffffffffffffff".to_owned(), - self.current_ctx.clone(), - ), - )) - } else { - raw_val + // Mask/sign-extend sub-256-bit params to clean dirty upper bits + let param_val = match &ty { + EvmType::Base(EvmBaseType::AddrT) => { + ast_helpers::mask_to_width(raw_val, 160, self.current_ctx.clone()) + } + EvmType::Base(EvmBaseType::UIntT(bits)) if *bits < 256 => { + ast_helpers::mask_to_width(raw_val, *bits, self.current_ctx.clone()) + } + EvmType::Base(EvmBaseType::IntT(bits)) if *bits < 256 => { + ast_helpers::sign_extend(raw_val, *bits, self.current_ctx.clone()) + } + EvmType::Base(EvmBaseType::BoolT) => { + ast_helpers::mask_to_width(raw_val, 8, self.current_ctx.clone()) + } + _ => raw_val, }; let binding = VarBinding { value: param_val, diff --git a/crates/ir/src/u256_sort.rs b/crates/ir/src/u256_sort.rs index 5d10dfa..56b4960 100644 --- a/crates/ir/src/u256_sort.rs +++ b/crates/ir/src/u256_sort.rs @@ -93,6 +93,8 @@ impl Sort for U256Sort { add_primitives!(eg, "u256-gt" = |a: W, b: W| -> Opt { (a.0 > b.0).then_some(()) }); add_primitives!(eg, "u256-eq" = |a: W, b: W| -> Opt { (a.0 == b.0).then_some(()) }); add_primitives!(eg, "u256-ne" = |a: W, b: W| -> Opt { (a.0 != b.0).then_some(()) }); + add_primitives!(eg, "u256-le" = |a: W, b: W| -> Opt { (a.0 <= b.0).then_some(()) }); + add_primitives!(eg, "u256-ge" = |a: W, b: W| -> Opt { (a.0 >= b.0).then_some(()) }); add_primitives!(eg, "u256-is-zero" = |a: W| -> Opt { a.0.is_zero().then_some(()) }); add_primitives!(eg, "u256-nonzero" = |a: W| -> Opt { (!a.0.is_zero()).then_some(()) }); diff --git a/crates/lexer/src/lexer.rs b/crates/lexer/src/lexer.rs index 97ae974..f727b11 100644 --- a/crates/lexer/src/lexer.rs +++ b/crates/lexer/src/lexer.rs @@ -129,7 +129,28 @@ impl<'a> Lexer<'a> { span.clone(), ) })?; - let kind = TokenKind::Literal(literal); + + // Check for optional type suffix (e.g. 0xffu8, 0x100i16) + let saved_pos = self.position; + if let Some(ch) = self.peek() { + if ch.is_ascii_alphabetic() && !ch.is_ascii_hexdigit() { + let (suffix_word, _, end_pos) = self.eat_while(None, |c| c.is_ascii_alphanumeric()); + if let Some(ty) = Self::parse_evm_type(&suffix_word) { + return Ok(Token { + kind: TokenKind::Literal(literal, Some(ty)), + span: Span { + start: span.start, + end: end_pos as usize, + file: None, + }, + }); + } else { + self.position = saved_pos; + } + } + } + + let kind = TokenKind::Literal(literal, None); Ok(Token { kind, span }) } @@ -208,8 +229,8 @@ impl<'a> Lexer<'a> { if !suffix_word.is_empty() { // We have a potential type suffix - if Self::parse_evm_type(&suffix_word).is_some() { - // Valid type suffix, consume it + if let Some(suffix_ty) = Self::parse_evm_type(&suffix_word) { + // Valid type suffix, consume it and carry the type info let span = Span { start: start as usize, end: suffix_end as usize, @@ -217,7 +238,7 @@ impl<'a> Lexer<'a> { }; let literal = decimal_to_bytes32(integer_str.replace('_', "").as_ref()); return Ok(Token { - kind: TokenKind::Literal(literal.into()), + kind: TokenKind::Literal(literal.into(), Some(suffix_ty)), span, }); } else { @@ -233,7 +254,7 @@ impl<'a> Lexer<'a> { }; let literal = decimal_to_bytes32(integer_str.replace('_', "").as_ref()); Ok(Token { - kind: TokenKind::Literal(literal.into()), + kind: TokenKind::Literal(literal.into(), None), span, }) } @@ -249,11 +270,15 @@ impl<'a> Lexer<'a> { let (suffix_word, _, suffix_end) = self.eat_while(None, |c| c.is_alphanumeric() || c == '_'); - let end_pos = if !suffix_word.is_empty() && Self::parse_evm_type(&suffix_word).is_some() { - suffix_end + let (end_pos, suffix_ty) = if !suffix_word.is_empty() { + if let Some(ty) = Self::parse_evm_type(&suffix_word) { + (suffix_end, Some(ty)) + } else { + self.position = suffix_start; + (end, None) + } } else { - self.position = suffix_start; - end + (end, None) }; let span = Span { @@ -268,7 +293,7 @@ impl<'a> Lexer<'a> { ) })?; Ok(Token { - kind: TokenKind::Literal(literal), + kind: TokenKind::Literal(literal, suffix_ty), span, }) } @@ -488,6 +513,7 @@ impl<'a> Lexer<'a> { found_kind = Some(TokenKind::Literal( str_to_bytes32(if word.as_str() == "true" { "1" } else { "0" }) .expect("single hex digit is always valid"), + None, )); self.eat_while(None, |c| c.is_alphanumeric()); } diff --git a/crates/lexer/tests/suites/operators.rs b/crates/lexer/tests/suites/operators.rs index f78f9aa..de40c96 100644 --- a/crates/lexer/tests/suites/operators.rs +++ b/crates/lexer/tests/suites/operators.rs @@ -201,13 +201,13 @@ fn lex_string_literal_with_escape() { #[test] fn lex_hex_literal() { let tok = Lexer::new("0xff").next().unwrap().unwrap(); - assert!(matches!(tok.kind, TokenKind::Literal(_))); + assert!(matches!(tok.kind, TokenKind::Literal(_, _))); } #[test] fn lex_hex_literal_single_digit() { let tok = Lexer::new("0x01").next().unwrap().unwrap(); - assert!(matches!(tok.kind, TokenKind::Literal(_))); + assert!(matches!(tok.kind, TokenKind::Literal(_, _))); } // ─── Decimal Literals ─────────────────────────────────────────────── @@ -215,11 +215,11 @@ fn lex_hex_literal_single_digit() { #[test] fn lex_decimal_literal() { let tok = Lexer::new("42").next().unwrap().unwrap(); - assert!(matches!(tok.kind, TokenKind::Literal(_))); + assert!(matches!(tok.kind, TokenKind::Literal(_, _))); // Decimal 42 = 0x2a assert_eq!( tok.kind, - TokenKind::Literal(decimal_to_bytes32("42").into()) + TokenKind::Literal(decimal_to_bytes32("42").into(), None) ); } @@ -392,8 +392,8 @@ fn lex_assignment_operator() { fn lex_bool_literals() { // true and false are syntax sugar for literals let kinds = lex_non_ws("true"); - assert!(matches!(kinds[0], TokenKind::Literal(_))); + assert!(matches!(kinds[0], TokenKind::Literal(_, _))); let kinds = lex_non_ws("false"); - assert!(matches!(kinds[0], TokenKind::Literal(_))); + assert!(matches!(kinds[0], TokenKind::Literal(_, _))); } diff --git a/crates/parser/src/parser.rs b/crates/parser/src/parser.rs index 7f3c227..175e86f 100644 --- a/crates/parser/src/parser.rs +++ b/crates/parser/src/parser.rs @@ -22,6 +22,23 @@ pub struct Parser { } impl Parser { + /// Convert a token-level `PrimitiveType` to an AST-level `PrimitiveType`. + /// Returns `None` for types that don't have AST equivalents (e.g. Pointer). + const fn token_prim_to_ast_prim( + tok_prim: &edge_types::tokens::PrimitiveType, + ) -> Option { + use edge_types::tokens::PrimitiveType as TP; + match tok_prim { + TP::UInt(bits) => Some(edge_ast::ty::PrimitiveType::UInt(*bits)), + TP::Int(bits) => Some(edge_ast::ty::PrimitiveType::Int(*bits)), + TP::FixedBytes(bytes) => Some(edge_ast::ty::PrimitiveType::FixedBytes(*bytes)), + TP::Address => Some(edge_ast::ty::PrimitiveType::Address), + TP::Bool => Some(edge_ast::ty::PrimitiveType::Bool), + TP::Bit => Some(edge_ast::ty::PrimitiveType::Bit), + TP::Pointer(_) => None, + } + } + /// Create a new parser from source code pub fn new(source: &str) -> ParseResult { let mut lexer = Lexer::new(source); @@ -1646,7 +1663,7 @@ impl Parser { expr = Expr::FieldAccess(Box::new(expr), field, span); } - TokenKind::Literal(lit_bytes) => { + TokenKind::Literal(lit_bytes, _) => { // Tuple field access: expr.0, expr.1, etc. let mut value: u64 = 0; for byte in lit_bytes { @@ -1663,6 +1680,16 @@ impl Parser { _ => {} } } + TokenKind::Keyword(Keyword::As) => { + self.advance(); + let target_type = self.parse_type_sig()?; + let span = Span { + start: expr.span().start, + end: self.tokens[self.cursor - 1].span.end, + file: expr.span().file.clone(), + }; + expr = Expr::Cast(Box::new(expr), target_type, span); + } _ => break, } } @@ -1676,12 +1703,14 @@ impl Parser { let kind = self.peek().kind.clone(); match kind { - TokenKind::Literal(lit_bytes) => { + TokenKind::Literal(lit_bytes, suffix_ty) => { let token = self.advance(); let mut bytes = [0u8; 32]; let len = lit_bytes.len().min(32); bytes[32 - len..].copy_from_slice(&lit_bytes[lit_bytes.len() - len..]); - let lit = Lit::Int(bytes, None, token.span); + // Convert token-level PrimitiveType to AST-level PrimitiveType + let ast_ty = suffix_ty.as_ref().and_then(Self::token_prim_to_ast_prim); + let lit = Lit::Int(bytes, ast_ty, token.span); Ok(Expr::Literal(Box::new(lit))) } TokenKind::StringLiteral(s) => { @@ -2052,10 +2081,10 @@ impl Parser { ops.push(AsmOp::Ident(name.clone(), tok.span)); } } - TokenKind::Literal(_) => { + TokenKind::Literal(_, _) => { self.advance(); let hex = match &tok.kind { - TokenKind::Literal(bytes) => { + TokenKind::Literal(bytes, _) => { let mut val: u128 = 0; for b in bytes { val = (val << 8) | (*b as u128); diff --git a/crates/types/src/tokens.rs b/crates/types/src/tokens.rs index 1c7e126..77ea12e 100644 --- a/crates/types/src/tokens.rs +++ b/crates/types/src/tokens.rs @@ -53,8 +53,8 @@ pub enum TokenKind { DocComment(String), /// Whitespace Whitespace, - /// A numeric literal - Literal(B256), + /// A numeric literal with optional type suffix (e.g. `42u8`, `0xffi16`) + Literal(B256, Option), /// A string literal StringLiteral(String), /// An Identifier @@ -120,7 +120,7 @@ impl fmt::Display for TokenKind { TokenKind::DocComment(s) => return write!(f, "{s}"), TokenKind::Keyword(k) => return write!(f, "{k}"), TokenKind::Pointer(l) => return write!(f, "{l}"), - TokenKind::Literal(l) => { + TokenKind::Literal(l, _suffix) => { let mut s = String::new(); for b in l.iter() { let _ = write!(&mut s, "{b:02x}"); diff --git a/crates/types/src/tokens/keywords.rs b/crates/types/src/tokens/keywords.rs index d5eeab3..cf4f811 100644 --- a/crates/types/src/tokens/keywords.rs +++ b/crates/types/src/tokens/keywords.rs @@ -107,6 +107,9 @@ pub enum Keyword { /// Inline assembly block #[display("asm")] Asm, + /// Type cast operator + #[display("as")] + As, } impl Keyword { @@ -148,6 +151,7 @@ impl Keyword { "super" => Some(Keyword::Super), "emit" => Some(Keyword::Emit), "asm" => Some(Keyword::Asm), + "as" => Some(Keyword::As), _ => None, } } @@ -189,6 +193,7 @@ impl Keyword { Keyword::Super, Keyword::Emit, Keyword::Asm, + Keyword::As, ] } } diff --git a/examples/tests/test_int_widths.edge b/examples/tests/test_int_widths.edge new file mode 100644 index 0000000..0361997 --- /dev/null +++ b/examples/tests/test_int_widths.edge @@ -0,0 +1,112 @@ +// test_int_widths.edge — Tests for sub-256-bit integer width semantics + +contract TestIntWidths { + // ── u8 arithmetic ── + + // u8 addition that fits: 100 + 50 = 150 + pub fn u8_add_ok(a: u8, b: u8) -> (u8) { + return a + b; + } + + // u8 addition that overflows: 200 + 100 = 300 > 255 → revert + pub fn u8_add_overflow(a: u8, b: u8) -> (u8) { + return a + b; + } + + // u8 subtraction that fits: 100 - 50 = 50 + pub fn u8_sub_ok(a: u8, b: u8) -> (u8) { + return a - b; + } + + // u8 subtraction that underflows: 50 - 100 → revert + pub fn u8_sub_underflow(a: u8, b: u8) -> (u8) { + return a - b; + } + + // u8 multiplication that fits: 10 * 20 = 200 + pub fn u8_mul_ok(a: u8, b: u8) -> (u8) { + return a * b; + } + + // u8 multiplication that overflows: 20 * 20 = 400 > 255 → revert + pub fn u8_mul_overflow(a: u8, b: u8) -> (u8) { + return a * b; + } + + // u8 bitwise AND: always safe, no truncation needed + pub fn u8_and(a: u8, b: u8) -> (u8) { + return a & b; + } + + // u8 division: always fits (result <= dividend) + pub fn u8_div(a: u8, b: u8) -> (u8) { + return a / b; + } + + // ── u128 arithmetic ── + + // u128 addition that fits + pub fn u128_add_ok(a: u128, b: u128) -> (u128) { + return a + b; + } + + // u128 addition that would overflow at u128 max + pub fn u128_add_overflow(a: u128, b: u128) -> (u128) { + return a + b; + } + + // ── Truncation semantics ── + + // u8 truncation on bitwise OR + pub fn u8_or_truncate(a: u8, b: u8) -> (u8) { + return a | b; + } + + // u8 shift left — truncates result + pub fn u8_shl(a: u8, b: u8) -> (u8) { + return a << b; + } + + // ── Mixed operations ── + + // Chain of u8 ops + pub fn u8_chain(a: u8, b: u8) -> (u8) { + let x: u8 = a + b; + let y: u8 = x * 2; + return y; + } + + // ── Literal suffix tests ── + + // Literal with u8 suffix: 42u8 should produce a u8-typed constant + pub fn literal_u8_add() -> (u256) { + let x: u8 = 42u8; + let y: u8 = 10u8; + return x + y; + } + + // Literal with u256 suffix (default behavior) + pub fn literal_u256_add() -> (u256) { + let x: u256 = 1000u256; + return x + 1u256; + } + + // ── Cast tests ── + + // Cast u256 down to u8 (narrowing) + pub fn cast_u256_to_u8(x: u256) -> (u256) { + let y: u8 = x as u8; + return y; + } + + // Cast u8 up to u256 (widening, no-op) + pub fn cast_u8_to_u256(x: u8) -> (u256) { + return x as u256; + } + + // Cast then arithmetic: narrowed value used in u8 add + pub fn cast_and_add(x: u256) -> (u256) { + let narrow: u8 = x as u8; + return narrow + 1u8; + } +} diff --git a/examples/tests/test_signed_widths.edge b/examples/tests/test_signed_widths.edge new file mode 100644 index 0000000..3ef58b5 --- /dev/null +++ b/examples/tests/test_signed_widths.edge @@ -0,0 +1,109 @@ +// test_signed_widths.edge — Tests for signed sub-256-bit integer width semantics + +use std::ops::UnsafeAdd; +use std::ops::UnsafeSub; +use std::ops::UnsafeMul; + +contract TestSignedWidths { + // ── i8 checked addition ── + + // i8 addition that fits: 50 + 30 = 80 (within -128..127) + pub fn i8_add_ok(a: i8, b: i8) -> (i8) { + return a + b; + } + + // i8 addition that overflows: 100 + 100 = 200 > 127 → revert + pub fn i8_add_overflow(a: i8, b: i8) -> (i8) { + return a + b; + } + + // ── i8 checked subtraction ── + + // i8 subtraction that fits: 50 - 30 = 20 + pub fn i8_sub_ok(a: i8, b: i8) -> (i8) { + return a - b; + } + + // i8 subtraction that underflows: -100 - 100 = -200 < -128 → revert + pub fn i8_sub_underflow(a: i8, b: i8) -> (i8) { + return a - b; + } + + // ── i8 checked multiplication ── + + // i8 multiplication that fits: 10 * 10 = 100 (within -128..127) + pub fn i8_mul_ok(a: i8, b: i8) -> (i8) { + return a * b; + } + + // i8 multiplication that overflows: 20 * 20 = 400 > 127 → revert + pub fn i8_mul_overflow(a: i8, b: i8) -> (i8) { + return a * b; + } + + // ── i8 signed division ── + + // Signed division: 100 / 10 = 10 (positive / positive) + pub fn i8_sdiv(a: i8, b: i8) -> (i8) { + return a / b; + } + + // ── i8 negation ── + + // Unary negation: -x + pub fn i8_negate(a: i8) -> (i8) { + return -a; + } + + // ── i8 comparisons ── + + // Signed less-than + pub fn i8_slt(a: i8, b: i8) -> (u256) { + if (a < b) { + return 1; + } + return 0; + } + + // Signed greater-than + pub fn i8_sgt(a: i8, b: i8) -> (u256) { + if (a > b) { + return 1; + } + return 0; + } + + // ── Unsafe (unchecked) signed arithmetic ── + + // Unsafe add: wraps without reverting + pub fn i8_unsafe_add(a: i8, b: i8) -> (i8) { + return UnsafeAdd::unsafe_add(a, b); + } + + // Unsafe sub: wraps without reverting + pub fn i8_unsafe_sub(a: i8, b: i8) -> (i8) { + return UnsafeSub::unsafe_sub(a, b); + } + + // Unsafe mul: wraps without reverting + pub fn i8_unsafe_mul(a: i8, b: i8) -> (i8) { + return UnsafeMul::unsafe_mul(a, b); + } + + // ── Cast signed ↔ unsigned ── + + // Cast i8 to u8 (reinterpret bits) + pub fn cast_i8_to_u8(a: i8) -> (u8) { + return a as u8; + } + + // Cast u8 to i8 (reinterpret bits, sign-extend) + pub fn cast_u8_to_i8(a: u8) -> (i8) { + return a as i8; + } + + // Cast i8 to u256 (sign-extend to 256) + pub fn cast_i8_to_u256(a: i8) -> (u256) { + return a as u256; + } +} diff --git a/examples/types/comptime.edge b/examples/types/comptime.edge index f96641d..7f24913 100644 --- a/examples/types/comptime.edge +++ b/examples/types/comptime.edge @@ -82,7 +82,7 @@ contract ComptimeExample { // Storage that changes behavior based on hard fork target. let value: &s u256; - fn base_fee2(x: u8, y: u8) -> u8 { + fn base_fee2(x: u256, y: u256) -> u256 { return x + y; }