Skip to content

Commit 76aa326

Browse files
authored
Merge pull request #55 from refcell/nox/int-literals-bug
fix: preserve full 256-bit precision for integer literals
2 parents 2b6379c + 1e4a3d7 commit 76aa326

12 files changed

Lines changed: 379 additions & 55 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ resolver = "2"
44

55
[workspace.package]
66
description = "Edge Language Compiler Workspace"
7-
version = "0.1.19"
7+
version = "0.1.20"
88
edition = "2021"
99
rust-version = "1.85"
1010
license = "MIT"

crates/ast/src/lit.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::ty::PrimitiveType;
1010
#[derive(Debug, Clone, PartialEq, Eq)]
1111
pub enum Lit {
1212
/// Integer literal: 42, 42u8, etc.
13-
Int(u64, Option<PrimitiveType>, Span),
13+
Int([u8; 32], Option<PrimitiveType>, Span),
1414
/// String literal: "hello"
1515
Str(String, Span),
1616
/// Boolean literal: true or false

crates/e2e/tests/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ mod types_exec;
4949
mod utils_exec;
5050
#[path = "suites/warnings.rs"]
5151
mod warnings;
52+
53+
#[path = "suites/large_int_literals.rs"]
54+
mod large_int_literals;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#![allow(missing_docs)]
2+
3+
use edge_driver::compiler::Compiler;
4+
5+
fn compile_source(src: &str) -> Vec<u8> {
6+
let mut compiler = Compiler::from_source(src);
7+
let output = compiler.compile().expect("compile failed");
8+
output.bytecode.expect("no bytecode produced")
9+
}
10+
11+
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
12+
haystack.windows(needle.len()).any(|w| w == needle)
13+
}
14+
15+
fn to_hex(bytes: &[u8]) -> String {
16+
bytes.iter().map(|b| format!("{b:02x}")).collect()
17+
}
18+
19+
fn from_hex(s: &str) -> Vec<u8> {
20+
(0..s.len())
21+
.step_by(2)
22+
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
23+
.collect()
24+
}
25+
26+
#[test]
27+
fn large_literal_two_pow_64() {
28+
let src = r#"
29+
contract Test {
30+
pub fn get() -> u256 {
31+
return 18446744073709551616;
32+
}
33+
}
34+
"#;
35+
let bytecode = compile_source(src);
36+
let expected: [u8; 9] = [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
37+
assert!(
38+
contains_bytes(&bytecode, &expected),
39+
"bytecode should contain 2^64: {}",
40+
to_hex(&bytecode)
41+
);
42+
}
43+
44+
#[test]
45+
fn large_literal_10_pow_21() {
46+
let src = r#"
47+
contract Test {
48+
pub fn get() -> u256 {
49+
return 1000000000000000000000;
50+
}
51+
}
52+
"#;
53+
let bytecode = compile_source(src);
54+
let expected = from_hex("3635c9adc5dea00000");
55+
assert!(
56+
contains_bytes(&bytecode, &expected),
57+
"bytecode should contain 10^21: {}",
58+
to_hex(&bytecode)
59+
);
60+
}
61+
62+
#[test]
63+
fn large_literal_u256_max() {
64+
let src = r#"
65+
contract Test {
66+
pub fn get() -> u256 {
67+
return 115792089237316195423570985008687907853269984665640564039457584007913129639935;
68+
}
69+
}
70+
"#;
71+
let bytecode = compile_source(src);
72+
let expected = [0xffu8; 32];
73+
assert!(
74+
contains_bytes(&bytecode, &expected),
75+
"bytecode should contain 32 bytes of 0xff for MAX_UINT: {}",
76+
to_hex(&bytecode)
77+
);
78+
}
79+
80+
#[test]
81+
fn small_literal_not_regressed() {
82+
let src = r#"
83+
contract Test {
84+
pub fn get() -> u256 {
85+
return 42;
86+
}
87+
}
88+
"#;
89+
let bytecode = compile_source(src);
90+
assert!(
91+
contains_bytes(&bytecode, &[0x60, 0x2a]),
92+
"bytecode should contain PUSH1 42: {}",
93+
to_hex(&bytecode)
94+
);
95+
}

crates/ir/src/to_egglog/composite.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,9 @@ impl AstToEgglog {
456456
fn try_const_index(expr: &edge_ast::Expr) -> Option<u64> {
457457
match expr {
458458
edge_ast::Expr::Literal(lit) => match lit.as_ref() {
459-
edge_ast::lit::Lit::Int(val, _, _) => Some(*val),
459+
edge_ast::lit::Lit::Int(bytes, _, _) => {
460+
Some(u64::from_be_bytes(bytes[24..32].try_into().unwrap()))
461+
}
460462
_ => None,
461463
},
462464
edge_ast::Expr::Paren(inner, _) => Self::try_const_index(inner),

crates/ir/src/to_egglog/expr.rs

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -316,20 +316,28 @@ impl AstToEgglog {
316316
{
317317
// Evaluate start index (must be a constant for now)
318318
if let edge_ast::Expr::Literal(lit) = index.as_ref() {
319-
if let edge_ast::Lit::Int(start, _, _) = lit.as_ref() {
319+
if let edge_ast::Lit::Int(start_bytes, _, _) = lit.as_ref() {
320+
let start =
321+
u64::from_be_bytes(start_bytes[24..32].try_into().unwrap())
322+
as usize;
320323
let new_base = ast_helpers::add(
321324
base_expr,
322325
ast_helpers::const_int(
323-
(*start as usize * 32) as i64,
326+
(start * 32) as i64,
324327
self.current_ctx.clone(),
325328
),
326329
);
327330
// Compute slice length from end index
328331
let slice_len = end_index.as_ref().map_or(0, |end_idx| {
329332
if let edge_ast::Expr::Literal(end_lit) = end_idx.as_ref() {
330-
if let edge_ast::Lit::Int(end, _, _) = end_lit.as_ref()
333+
if let edge_ast::Lit::Int(end_bytes, _, _) =
334+
end_lit.as_ref()
331335
{
332-
(*end as usize) - (*start as usize)
336+
let end = u64::from_be_bytes(
337+
end_bytes[24..32].try_into().unwrap(),
338+
)
339+
as usize;
340+
end - start
333341
} else {
334342
0
335343
}
@@ -464,16 +472,40 @@ impl AstToEgglog {
464472
/// Lower a literal value.
465473
pub(crate) fn lower_literal(&self, lit: &edge_ast::Lit) -> Result<RcExpr, IrError> {
466474
match lit {
467-
edge_ast::Lit::Int(val, maybe_ty, _span) => {
475+
edge_ast::Lit::Int(bytes, maybe_ty, _span) => {
468476
let ty = maybe_ty
469477
.as_ref()
470478
.map(|pt| self.lower_primitive_type(pt))
471479
.unwrap_or(EvmType::Base(EvmBaseType::UIntT(256)));
472-
Ok(Rc::new(EvmExpr::Const(
473-
EvmConstant::SmallInt(*val as i64),
474-
ty,
475-
self.current_ctx.clone(),
476-
)))
480+
// Check if value fits in SmallInt (first 24 bytes are zero and high bit of remaining 8 is not set)
481+
let is_small = bytes[..24].iter().all(|&b| b == 0) && (bytes[24] & 0x80) == 0;
482+
if is_small {
483+
let mut val: u64 = 0;
484+
for &b in &bytes[24..] {
485+
val = (val << 8) | (b as u64);
486+
}
487+
Ok(Rc::new(EvmExpr::Const(
488+
EvmConstant::SmallInt(val as i64),
489+
ty,
490+
self.current_ctx.clone(),
491+
)))
492+
} else {
493+
let hex_str: String = bytes
494+
.iter()
495+
.skip_while(|&&b| b == 0)
496+
.map(|b| format!("{b:02x}"))
497+
.collect();
498+
let hex_str = if hex_str.is_empty() {
499+
"00".to_string()
500+
} else {
501+
hex_str
502+
};
503+
Ok(Rc::new(EvmExpr::Const(
504+
EvmConstant::LargeInt(hex_str),
505+
ty,
506+
self.current_ctx.clone(),
507+
)))
508+
}
477509
}
478510
edge_ast::Lit::Bool(val, _span) => {
479511
Ok(ast_helpers::const_bool(*val, self.current_ctx.clone()))

crates/ir/src/to_egglog/types.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ impl AstToEgglog {
6060
/// Extract fixed array length from a type expression (literal integer).
6161
pub(crate) fn extract_array_length(len_expr: &edge_ast::Expr) -> Option<usize> {
6262
if let edge_ast::Expr::Literal(lit) = len_expr {
63-
if let edge_ast::Lit::Int(n, _, _) = lit.as_ref() {
64-
return Some(*n as usize);
63+
if let edge_ast::Lit::Int(bytes, _, _) = lit.as_ref() {
64+
let n = u64::from_be_bytes(bytes[24..32].try_into().unwrap());
65+
return Some(n as usize);
6566
}
6667
}
6768
None

crates/parser/src/parser.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,12 +1678,10 @@ impl Parser {
16781678
match kind {
16791679
TokenKind::Literal(lit_bytes) => {
16801680
let token = self.advance();
1681-
// Extract the actual integer value from the literal bytes
1682-
let mut value: u128 = 0;
1683-
for byte in &lit_bytes {
1684-
value = (value << 8) | (*byte as u128);
1685-
}
1686-
let lit = Lit::Int(value as u64, None, token.span);
1681+
let mut bytes = [0u8; 32];
1682+
let len = lit_bytes.len().min(32);
1683+
bytes[32 - len..].copy_from_slice(&lit_bytes[lit_bytes.len() - len..]);
1684+
let lit = Lit::Int(bytes, None, token.span);
16871685
Ok(Expr::Literal(Box::new(lit)))
16881686
}
16891687
TokenKind::StringLiteral(s) => {

crates/parser/tests/parser_tests.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Integration tests for the Edge language parser.
22
3+
use edge_ast::Lit;
34
use edge_parser::parse;
45

56
// ─── Empty Program ──────────────────────────────────────────────────
@@ -461,3 +462,49 @@ fn parse_match_statement() {
461462
edge_ast::Stmt::Match(..)
462463
));
463464
}
465+
466+
// ─── Large Integer Literals (Issue #40) ─────────────────────────────
467+
468+
/// Helper: parse "let x: u256 = <literal>;" and extract the Lit from the initializer.
469+
fn parse_int_literal(literal: &str) -> [u8; 32] {
470+
let src = format!("let x: u256 = {literal};");
471+
let program = parse(&src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
472+
assert_eq!(program.stmts.len(), 1);
473+
if let edge_ast::Stmt::VarDecl(_, _, Some(init), _) = &program.stmts[0] {
474+
{
475+
if let edge_ast::Expr::Literal(lit) = init.as_ref() {
476+
if let Lit::Int(bytes, _, _) = lit.as_ref() {
477+
return *bytes;
478+
}
479+
}
480+
}
481+
}
482+
panic!("expected Lit::Int in initializer");
483+
}
484+
485+
#[test]
486+
fn int_literal_two_pow_64_not_truncated() {
487+
// 2^64 = 18446744073709551616; must NOT be truncated to 0
488+
let bytes = parse_int_literal("18446744073709551616");
489+
// 2^64 in big-endian [u8;32]: byte[23] = 0x01, rest zeros
490+
let mut expected = [0u8; 32];
491+
expected[23] = 1;
492+
assert_eq!(bytes, expected, "2^64 was truncated!");
493+
}
494+
495+
#[test]
496+
fn int_literal_u256_max() {
497+
// 2^256 - 1: all bytes 0xff
498+
let bytes = parse_int_literal(
499+
"115792089237316195423570985008687907853269984665640564039457584007913129639935",
500+
);
501+
assert_eq!(bytes, [0xffu8; 32], "2^256-1 bytes should all be 0xff");
502+
}
503+
504+
#[test]
505+
fn int_literal_small_value_preserved() {
506+
let bytes = parse_int_literal("42");
507+
let mut expected = [0u8; 32];
508+
expected[31] = 42;
509+
assert_eq!(bytes, expected);
510+
}

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]

0 commit comments

Comments
 (0)