Skip to content

Commit d3a3036

Browse files
committed
feat(typeck): implement constant folding for integer literal type preservation
When both operands of a binary operation are IntLiteral, evaluate the expression at compile time to preserve literal type semantics. This allows expressions like `-(1 + 2)` to work correctly, since the result remains an IntLiteral that can be negated. Changes: - Extended IntScalar with explicit sign tracking and signed arithmetic - Added try_eval_int_literal_binop to evaluate literal binary expressions - Updated eval_array_len to reject negative array lengths
1 parent addc964 commit d3a3036

File tree

5 files changed

+174
-56
lines changed

5 files changed

+174
-56
lines changed

crates/sema/src/eval.rs

Lines changed: 130 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ const RECURSION_LIMIT: usize = 64;
1212
pub fn eval_array_len(gcx: Gcx<'_>, size: &hir::Expr<'_>) -> Result<U256, ErrorGuaranteed> {
1313
match ConstantEvaluator::new(gcx).eval(size) {
1414
Ok(int) => {
15-
if int.data.is_zero() {
15+
if int.is_negative() {
16+
let msg = "array length cannot be negative";
17+
Err(gcx.dcx().err(msg).span(size.span).emit())
18+
} else if int.data.is_zero() {
1619
let msg = "array length must be greater than zero";
1720
Err(gcx.dcx().err(msg).span(size.span).emit())
1821
} else {
@@ -83,7 +86,7 @@ impl<'gcx> ConstantEvaluator<'gcx> {
8386
hir::ExprKind::Binary(l, bin_op, r) => {
8487
let l = self.try_eval(l)?;
8588
let r = self.try_eval(r)?;
86-
l.binop(&r, bin_op.kind).map_err(Into::into)
89+
l.binop(r, bin_op.kind).map_err(Into::into)
8790
}
8891
// hir::ExprKind::Call(_, _) => unimplemented!(),
8992
// hir::ExprKind::CallOptions(_, _) => unimplemented!(),
@@ -135,18 +138,50 @@ impl<'gcx> ConstantEvaluator<'gcx> {
135138
}
136139
}
137140

141+
/// Represents an integer value with an explicit sign for literal type tracking.
142+
///
143+
/// The `data` field always stores the absolute value of the number.
144+
/// The `negative` field indicates whether the value is negative.
138145
pub struct IntScalar {
146+
/// The absolute value of the integer.
139147
pub data: U256,
148+
/// Whether the value is negative.
149+
pub negative: bool,
140150
}
141151

142152
impl IntScalar {
153+
/// Creates a new non-negative integer value.
143154
pub fn new(data: U256) -> Self {
144-
Self { data }
155+
Self { data, negative: false }
156+
}
157+
158+
/// Creates a new integer value with the given sign.
159+
pub fn new_signed(data: U256, negative: bool) -> Self {
160+
// Zero is never negative.
161+
Self { data, negative: negative && !data.is_zero() }
162+
}
163+
164+
/// Returns the bit length of the integer value.
165+
///
166+
/// This is the number of bits needed to represent the value,
167+
/// not including the sign bit.
168+
pub fn bit_len(&self) -> u64 {
169+
self.data.bit_len() as u64
170+
}
171+
172+
/// Returns whether the value is negative.
173+
pub fn is_negative(&self) -> bool {
174+
self.negative
175+
}
176+
177+
/// Returns the negation of this value.
178+
pub fn negate(self) -> Self {
179+
if self.data.is_zero() { self } else { Self::new_signed(self.data, !self.negative) }
145180
}
146181

147182
/// Creates a new integer value from a boolean.
148183
pub fn from_bool(value: bool) -> Self {
149-
Self { data: U256::from(value as u8) }
184+
Self::new(U256::from(value as u8))
150185
}
151186

152187
/// Creates a new integer value from big-endian bytes.
@@ -155,7 +190,7 @@ impl IntScalar {
155190
///
156191
/// Panics if `bytes` is empty or has a length greater than 32.
157192
pub fn from_be_bytes(bytes: &[u8]) -> Self {
158-
Self { data: U256::from_be_slice(bytes) }
193+
Self::new(U256::from_be_slice(bytes))
159194
}
160195

161196
/// Converts the integer value to a boolean.
@@ -164,64 +199,109 @@ impl IntScalar {
164199
}
165200

166201
/// Applies the given unary operation to this value.
167-
pub fn unop(&self, op: hir::UnOpKind) -> Result<Self, EE> {
202+
pub fn unop(self, op: hir::UnOpKind) -> Result<Self, EE> {
168203
Ok(match op {
169204
hir::UnOpKind::PreInc
170205
| hir::UnOpKind::PreDec
171206
| hir::UnOpKind::PostInc
172207
| hir::UnOpKind::PostDec => return Err(EE::UnsupportedUnaryOp),
173208
hir::UnOpKind::Not | hir::UnOpKind::BitNot => Self::new(!self.data),
174-
hir::UnOpKind::Neg => Self::new(self.data.wrapping_neg()),
209+
hir::UnOpKind::Neg => self.negate(),
175210
})
176211
}
177212

178213
/// Applies the given binary operation to this value.
179-
pub fn binop(&self, r: &Self, op: hir::BinOpKind) -> Result<Self, EE> {
180-
let l = self;
214+
///
215+
/// For signed arithmetic, this handles the sign tracking properly.
216+
pub fn binop(self, r: Self, op: hir::BinOpKind) -> Result<Self, EE> {
217+
use hir::BinOpKind::*;
181218
Ok(match op {
182-
// hir::BinOpKind::Lt => Self::from_bool(l.data < r.data),
183-
// hir::BinOpKind::Le => Self::from_bool(l.data <= r.data),
184-
// hir::BinOpKind::Gt => Self::from_bool(l.data > r.data),
185-
// hir::BinOpKind::Ge => Self::from_bool(l.data >= r.data),
186-
// hir::BinOpKind::Eq => Self::from_bool(l.data == r.data),
187-
// hir::BinOpKind::Ne => Self::from_bool(l.data != r.data),
188-
// hir::BinOpKind::Or => Self::from_bool(l.data != 0 || r.data != 0),
189-
// hir::BinOpKind::And => Self::from_bool(l.data != 0 && r.data != 0),
190-
hir::BinOpKind::BitOr => Self::new(l.data | r.data),
191-
hir::BinOpKind::BitAnd => Self::new(l.data & r.data),
192-
hir::BinOpKind::BitXor => Self::new(l.data ^ r.data),
193-
hir::BinOpKind::Shr => {
194-
Self::new(l.data.wrapping_shr(r.data.try_into().unwrap_or(usize::MAX)))
195-
}
196-
hir::BinOpKind::Shl => {
197-
Self::new(l.data.wrapping_shl(r.data.try_into().unwrap_or(usize::MAX)))
198-
}
199-
hir::BinOpKind::Sar => {
200-
Self::new(l.data.arithmetic_shr(r.data.try_into().unwrap_or(usize::MAX)))
201-
}
202-
hir::BinOpKind::Add => {
203-
Self::new(l.data.checked_add(r.data).ok_or(EE::ArithmeticOverflow)?)
204-
}
205-
hir::BinOpKind::Sub => {
206-
Self::new(l.data.checked_sub(r.data).ok_or(EE::ArithmeticOverflow)?)
207-
}
208-
hir::BinOpKind::Pow => {
209-
Self::new(l.data.checked_pow(r.data).ok_or(EE::ArithmeticOverflow)?)
219+
Add => self.checked_add(r).ok_or(EE::ArithmeticOverflow)?,
220+
Sub => self.checked_sub(r).ok_or(EE::ArithmeticOverflow)?,
221+
Mul => self.checked_mul(r).ok_or(EE::ArithmeticOverflow)?,
222+
Div => self.checked_div(r).ok_or(EE::DivisionByZero)?,
223+
Rem => self.checked_rem(r).ok_or(EE::DivisionByZero)?,
224+
Pow => self.checked_pow(r).ok_or(EE::ArithmeticOverflow)?,
225+
BitOr => Self::new(self.data | r.data),
226+
BitAnd => Self::new(self.data & r.data),
227+
BitXor => Self::new(self.data ^ r.data),
228+
Shr => Self::new(self.data.wrapping_shr(r.data.try_into().unwrap_or(usize::MAX))),
229+
Shl => Self::new(self.data.wrapping_shl(r.data.try_into().unwrap_or(usize::MAX))),
230+
Sar => Self::new(self.data.arithmetic_shr(r.data.try_into().unwrap_or(usize::MAX))),
231+
Lt | Le | Gt | Ge | Eq | Ne | Or | And => return Err(EE::UnsupportedBinaryOp),
232+
})
233+
}
234+
235+
/// Checked addition with sign handling.
236+
fn checked_add(self, r: Self) -> Option<Self> {
237+
match (self.negative, r.negative) {
238+
// Both non-negative: simple add
239+
(false, false) => Some(Self::new(self.data.checked_add(r.data)?)),
240+
// Both negative: negate(|a| + |b|)
241+
(true, true) => Some(Self::new_signed(self.data.checked_add(r.data)?, true)),
242+
// Different signs: subtract the smaller absolute value from the larger
243+
(false, true) => {
244+
// a + (-b) = a - b
245+
if self.data >= r.data {
246+
Some(Self::new(self.data.checked_sub(r.data)?))
247+
} else {
248+
Some(Self::new_signed(r.data.checked_sub(self.data)?, true))
249+
}
210250
}
211-
hir::BinOpKind::Mul => {
212-
Self::new(l.data.checked_mul(r.data).ok_or(EE::ArithmeticOverflow)?)
251+
(true, false) => {
252+
// (-a) + b = b - a
253+
if r.data >= self.data {
254+
Some(Self::new(r.data.checked_sub(self.data)?))
255+
} else {
256+
Some(Self::new_signed(self.data.checked_sub(r.data)?, true))
257+
}
213258
}
214-
hir::BinOpKind::Div => Self::new(l.data.checked_div(r.data).ok_or(EE::DivisionByZero)?),
215-
hir::BinOpKind::Rem => Self::new(l.data.checked_rem(r.data).ok_or(EE::DivisionByZero)?),
216-
hir::BinOpKind::Lt
217-
| hir::BinOpKind::Le
218-
| hir::BinOpKind::Gt
219-
| hir::BinOpKind::Ge
220-
| hir::BinOpKind::Eq
221-
| hir::BinOpKind::Ne
222-
| hir::BinOpKind::Or
223-
| hir::BinOpKind::And => return Err(EE::UnsupportedBinaryOp),
224-
})
259+
}
260+
}
261+
262+
/// Checked subtraction with sign handling.
263+
fn checked_sub(self, r: Self) -> Option<Self> {
264+
// a - b = a + (-b)
265+
self.checked_add(r.negate())
266+
}
267+
268+
/// Checked multiplication with sign handling.
269+
fn checked_mul(self, r: Self) -> Option<Self> {
270+
let result = self.data.checked_mul(r.data)?;
271+
// Result is negative if exactly one operand is negative
272+
Some(Self::new_signed(result, self.negative != r.negative))
273+
}
274+
275+
/// Checked division with sign handling.
276+
fn checked_div(self, r: Self) -> Option<Self> {
277+
if r.data.is_zero() {
278+
return None;
279+
}
280+
let result = self.data.checked_div(r.data)?;
281+
// Result is negative if exactly one operand is negative
282+
Some(Self::new_signed(result, self.negative != r.negative))
283+
}
284+
285+
/// Checked remainder with sign handling.
286+
fn checked_rem(self, r: Self) -> Option<Self> {
287+
if r.data.is_zero() {
288+
return None;
289+
}
290+
let result = self.data.checked_rem(r.data)?;
291+
// Result has the sign of the dividend
292+
Some(Self::new_signed(result, self.negative))
293+
}
294+
295+
/// Checked exponentiation.
296+
fn checked_pow(self, r: Self) -> Option<Self> {
297+
// Exponent must be non-negative
298+
if r.negative {
299+
return None;
300+
}
301+
let result = self.data.checked_pow(r.data)?;
302+
// Result is negative if base is negative and exponent is odd
303+
let result_negative = self.negative && r.data.bit(0);
304+
Some(Self::new_signed(result, result_negative))
225305
}
226306
}
227307

crates/sema/src/typeck/checker.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{
22
builtins::Builtin,
3+
eval::ConstantEvaluator,
34
hir::{self, Visit},
45
ty::{Gcx, Ty, TyKind},
56
};
@@ -134,6 +135,16 @@ impl<'gcx> TypeChecker<'gcx> {
134135
hir::ExprKind::Binary(lhs_e, op, rhs_e) => {
135136
let lhs = self.check_expr(lhs_e);
136137
let rhs = self.check_expr(rhs_e);
138+
139+
// When both operands are IntLiteral, evaluate the expression to preserve
140+
// literal type through binary operations (needed for -(1 + 2) to work).
141+
if let (TyKind::IntLiteral(..), TyKind::IntLiteral(..)) = (lhs.kind, rhs.kind)
142+
&& !op.kind.is_cmp()
143+
&& let Some(lit_ty) = self.try_eval_int_literal_binop(expr)
144+
{
145+
return lit_ty;
146+
}
147+
137148
self.check_binop(lhs_e, lhs, rhs_e, rhs, op, false)
138149
}
139150
hir::ExprKind::Call(callee, ref args, ref _opts) => {
@@ -472,6 +483,16 @@ impl<'gcx> TypeChecker<'gcx> {
472483
let _ = (ty, expr);
473484
}
474485

486+
/// Tries to evaluate a binary expression where both operands are int literals.
487+
///
488+
/// Returns the resulting IntLiteral type if successful, or None if evaluation fails.
489+
/// This is used to preserve literal type through binary operations.
490+
fn try_eval_int_literal_binop(&self, expr: &'gcx hir::Expr<'gcx>) -> Option<Ty<'gcx>> {
491+
let mut evaluator = ConstantEvaluator::new(self.gcx);
492+
let result = evaluator.try_eval(expr).ok()?;
493+
self.gcx.mk_ty_int_literal(result.is_negative(), result.bit_len())
494+
}
495+
475496
fn check_binop(
476497
&mut self,
477498
lhs_e: &'gcx hir::Expr<'gcx>,

tests/ui/typeck/eval.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ contract C {
2525
function a2(uint[x / zeroPublic] memory) public {} //~ ERROR: failed to evaluate constant: attempted to divide by zero
2626
function b(uint[x] memory) public {}
2727
function c(uint[x * 2] memory) public {}
28-
function d(uint[0 - 1] memory) public {} //~ ERROR: failed to evaluate constant: arithmetic overflow
29-
function d2(uint[zeroPublic - 1] memory) public {} //~ ERROR: failed to evaluate constant: arithmetic overflow
28+
function d(uint[0 - 1] memory) public {} //~ ERROR: array length cannot be negative
29+
function d2(uint[zeroPublic - 1] memory) public {} //~ ERROR: array length cannot be negative
3030
function e(uint[rec1] memory) public {} //~ ERROR: failed to evaluate constant: recursion limit reached
3131
function f(uint[rec2] memory) public {} //~ ERROR: failed to evaluate constant: recursion limit reached
3232

tests/ui/typeck/eval.stderr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,17 @@ error: failed to evaluate constant: attempted to divide by zero
1919
LL │ function a2(uint[x / zeroPublic] memory) public {}
2020
╰╴ ━━━━━━━━━━━━━━ evaluation of constant value failed here
2121

22-
error: failed to evaluate constant: arithmetic overflow
22+
error: array length cannot be negative
2323
╭▸ ROOT/tests/ui/typeck/eval.sol:LL:CC
2424
2525
LL │ function d(uint[0 - 1] memory) public {}
26-
╰╴ ━━━━━ evaluation of constant value failed here
26+
╰╴ ━━━━━
2727

28-
error: failed to evaluate constant: arithmetic overflow
28+
error: array length cannot be negative
2929
╭▸ ROOT/tests/ui/typeck/eval.sol:LL:CC
3030
3131
LL │ function d2(uint[zeroPublic - 1] memory) public {}
32-
╰╴ ━━━━━━━━━━━━━━ evaluation of constant value failed here
32+
╰╴ ━━━━━━━━━━━━━━
3333

3434
error: failed to evaluate constant: recursion limit reached
3535
╭▸ ROOT/tests/ui/typeck/eval.sol:LL:CC

tests/ui/typeck/implicit_int_literal.sol

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,21 @@ function f() {
6262
// Negative literals cannot coerce to unsigned types
6363
uint8 neg_to_uint8 = -1; //~ ERROR: mismatched types
6464
uint256 neg_to_uint256 = -42; //~ ERROR: mismatched types
65+
66+
// === Edge cases: parenthesized and compound expressions ===
67+
// Parentheses don't change the semantics - still a negative literal
68+
int16 paren_neg = -(1);
69+
int16 double_paren_neg = -((1));
70+
71+
// Double negation: negates a negative literal, result is non-negative
72+
// -(-1) = 1, which is int_literal[1] non-negative
73+
int16 double_neg = -(-1);
74+
75+
// Negation of binary expressions - binary ops on int literals are now
76+
// evaluated during type checking to preserve the literal type
77+
int16 neg_binop = -(1 + 2);
78+
79+
// More complex expressions with literals
80+
int16 complex_lit = -(2 * 3 + 1);
81+
int32 large_lit = -(256 + 1); // Result is -257, int_literal[2]
6582
}

0 commit comments

Comments
 (0)