Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 130 additions & 50 deletions crates/sema/src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ const RECURSION_LIMIT: usize = 64;
pub fn eval_array_len(gcx: Gcx<'_>, size: &hir::Expr<'_>) -> Result<U256, ErrorGuaranteed> {
match ConstantEvaluator::new(gcx).eval(size) {
Ok(int) => {
if int.data.is_zero() {
if int.is_negative() {
let msg = "array length cannot be negative";
Err(gcx.dcx().err(msg).span(size.span).emit())
} else if int.data.is_zero() {
let msg = "array length must be greater than zero";
Err(gcx.dcx().err(msg).span(size.span).emit())
} else {
Expand Down Expand Up @@ -83,7 +86,7 @@ impl<'gcx> ConstantEvaluator<'gcx> {
hir::ExprKind::Binary(l, bin_op, r) => {
let l = self.try_eval(l)?;
let r = self.try_eval(r)?;
l.binop(&r, bin_op.kind).map_err(Into::into)
l.binop(r, bin_op.kind).map_err(Into::into)
}
// hir::ExprKind::Call(_, _) => unimplemented!(),
// hir::ExprKind::CallOptions(_, _) => unimplemented!(),
Expand Down Expand Up @@ -135,18 +138,50 @@ impl<'gcx> ConstantEvaluator<'gcx> {
}
}

/// Represents an integer value with an explicit sign for literal type tracking.
///
/// The `data` field always stores the absolute value of the number.
/// The `negative` field indicates whether the value is negative.
pub struct IntScalar {
/// The absolute value of the integer.
pub data: U256,
/// Whether the value is negative.
pub negative: bool,
}

impl IntScalar {
/// Creates a new non-negative integer value.
pub fn new(data: U256) -> Self {
Self { data }
Self { data, negative: false }
}

/// Creates a new integer value with the given sign.
pub fn new_signed(data: U256, negative: bool) -> Self {
// Zero is never negative.
Self { data, negative: negative && !data.is_zero() }
}

/// Returns the bit length of the integer value.
///
/// This is the number of bits needed to represent the value,
/// not including the sign bit.
pub fn bit_len(&self) -> u64 {
self.data.bit_len() as u64
}

/// Returns whether the value is negative.
pub fn is_negative(&self) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be is_signed and be a constant value. then data should be I256 and is_negative will be I256's is_negative (last bit set). this way all ops are easier to keep track of, since they become regular two's complement

self.negative
}

/// Returns the negation of this value.
pub fn negate(self) -> Self {
if self.data.is_zero() { self } else { Self::new_signed(self.data, !self.negative) }
}

/// Creates a new integer value from a boolean.
pub fn from_bool(value: bool) -> Self {
Self { data: U256::from(value as u8) }
Self::new(U256::from(value as u8))
}

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

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

/// Applies the given unary operation to this value.
pub fn unop(&self, op: hir::UnOpKind) -> Result<Self, EE> {
pub fn unop(self, op: hir::UnOpKind) -> Result<Self, EE> {
Ok(match op {
hir::UnOpKind::PreInc
| hir::UnOpKind::PreDec
| hir::UnOpKind::PostInc
| hir::UnOpKind::PostDec => return Err(EE::UnsupportedUnaryOp),
hir::UnOpKind::Not | hir::UnOpKind::BitNot => Self::new(!self.data),
hir::UnOpKind::Neg => Self::new(self.data.wrapping_neg()),
hir::UnOpKind::Neg => self.negate(),
})
}

/// Applies the given binary operation to this value.
pub fn binop(&self, r: &Self, op: hir::BinOpKind) -> Result<Self, EE> {
let l = self;
///
/// For signed arithmetic, this handles the sign tracking properly.
pub fn binop(self, r: Self, op: hir::BinOpKind) -> Result<Self, EE> {
use hir::BinOpKind::*;
Ok(match op {
// hir::BinOpKind::Lt => Self::from_bool(l.data < r.data),
// hir::BinOpKind::Le => Self::from_bool(l.data <= r.data),
// hir::BinOpKind::Gt => Self::from_bool(l.data > r.data),
// hir::BinOpKind::Ge => Self::from_bool(l.data >= r.data),
// hir::BinOpKind::Eq => Self::from_bool(l.data == r.data),
// hir::BinOpKind::Ne => Self::from_bool(l.data != r.data),
// hir::BinOpKind::Or => Self::from_bool(l.data != 0 || r.data != 0),
// hir::BinOpKind::And => Self::from_bool(l.data != 0 && r.data != 0),
hir::BinOpKind::BitOr => Self::new(l.data | r.data),
hir::BinOpKind::BitAnd => Self::new(l.data & r.data),
hir::BinOpKind::BitXor => Self::new(l.data ^ r.data),
hir::BinOpKind::Shr => {
Self::new(l.data.wrapping_shr(r.data.try_into().unwrap_or(usize::MAX)))
}
hir::BinOpKind::Shl => {
Self::new(l.data.wrapping_shl(r.data.try_into().unwrap_or(usize::MAX)))
}
hir::BinOpKind::Sar => {
Self::new(l.data.arithmetic_shr(r.data.try_into().unwrap_or(usize::MAX)))
}
hir::BinOpKind::Add => {
Self::new(l.data.checked_add(r.data).ok_or(EE::ArithmeticOverflow)?)
}
hir::BinOpKind::Sub => {
Self::new(l.data.checked_sub(r.data).ok_or(EE::ArithmeticOverflow)?)
}
hir::BinOpKind::Pow => {
Self::new(l.data.checked_pow(r.data).ok_or(EE::ArithmeticOverflow)?)
Add => self.checked_add(r).ok_or(EE::ArithmeticOverflow)?,
Sub => self.checked_sub(r).ok_or(EE::ArithmeticOverflow)?,
Mul => self.checked_mul(r).ok_or(EE::ArithmeticOverflow)?,
Div => self.checked_div(r).ok_or(EE::DivisionByZero)?,
Rem => self.checked_rem(r).ok_or(EE::DivisionByZero)?,
Pow => self.checked_pow(r).ok_or(EE::ArithmeticOverflow)?,
BitOr => Self::new(self.data | r.data),
BitAnd => Self::new(self.data & r.data),
BitXor => Self::new(self.data ^ r.data),
Shr => Self::new(self.data.wrapping_shr(r.data.try_into().unwrap_or(usize::MAX))),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be arithmetic_shr depending on sign

Shl => Self::new(self.data.wrapping_shl(r.data.try_into().unwrap_or(usize::MAX))),
Sar => Self::new(self.data.arithmetic_shr(r.data.try_into().unwrap_or(usize::MAX))),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is >>> which only works on fixed types, we should rename this to sometihng so we dont forget its useless

Lt | Le | Gt | Ge | Eq | Ne | Or | And => return Err(EE::UnsupportedBinaryOp),
})
}

/// Checked addition with sign handling.
fn checked_add(self, r: Self) -> Option<Self> {
match (self.negative, r.negative) {
// Both non-negative: simple add
(false, false) => Some(Self::new(self.data.checked_add(r.data)?)),
// Both negative: negate(|a| + |b|)
(true, true) => Some(Self::new_signed(self.data.checked_add(r.data)?, true)),
// Different signs: subtract the smaller absolute value from the larger
(false, true) => {
// a + (-b) = a - b
if self.data >= r.data {
Some(Self::new(self.data.checked_sub(r.data)?))
} else {
Some(Self::new_signed(r.data.checked_sub(self.data)?, true))
}
}
hir::BinOpKind::Mul => {
Self::new(l.data.checked_mul(r.data).ok_or(EE::ArithmeticOverflow)?)
(true, false) => {
// (-a) + b = b - a
if r.data >= self.data {
Some(Self::new(r.data.checked_sub(self.data)?))
} else {
Some(Self::new_signed(self.data.checked_sub(r.data)?, true))
}
}
hir::BinOpKind::Div => Self::new(l.data.checked_div(r.data).ok_or(EE::DivisionByZero)?),
hir::BinOpKind::Rem => Self::new(l.data.checked_rem(r.data).ok_or(EE::DivisionByZero)?),
hir::BinOpKind::Lt
| hir::BinOpKind::Le
| hir::BinOpKind::Gt
| hir::BinOpKind::Ge
| hir::BinOpKind::Eq
| hir::BinOpKind::Ne
| hir::BinOpKind::Or
| hir::BinOpKind::And => return Err(EE::UnsupportedBinaryOp),
})
}
}

/// Checked subtraction with sign handling.
fn checked_sub(self, r: Self) -> Option<Self> {
// a - b = a + (-b)
self.checked_add(r.negate())
}

/// Checked multiplication with sign handling.
fn checked_mul(self, r: Self) -> Option<Self> {
let result = self.data.checked_mul(r.data)?;
// Result is negative if exactly one operand is negative
Some(Self::new_signed(result, self.negative != r.negative))
}

/// Checked division with sign handling.
fn checked_div(self, r: Self) -> Option<Self> {
if r.data.is_zero() {
return None;
}
let result = self.data.checked_div(r.data)?;
// Result is negative if exactly one operand is negative
Some(Self::new_signed(result, self.negative != r.negative))
}

/// Checked remainder with sign handling.
fn checked_rem(self, r: Self) -> Option<Self> {
if r.data.is_zero() {
return None;
}
let result = self.data.checked_rem(r.data)?;
// Result has the sign of the dividend
Some(Self::new_signed(result, self.negative))
}

/// Checked exponentiation.
fn checked_pow(self, r: Self) -> Option<Self> {
// Exponent must be non-negative
if r.negative {
return None;
}
let result = self.data.checked_pow(r.data)?;
// Result is negative if base is negative and exponent is odd
let result_negative = self.negative && r.data.bit(0);
Some(Self::new_signed(result, result_negative))
}
}

Expand Down
21 changes: 21 additions & 0 deletions crates/sema/src/typeck/checker.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::{
builtins::Builtin,
eval::ConstantEvaluator,
hir::{self, Visit},
ty::{Gcx, Ty, TyKind},
};
Expand Down Expand Up @@ -134,6 +135,16 @@ impl<'gcx> TypeChecker<'gcx> {
hir::ExprKind::Binary(lhs_e, op, rhs_e) => {
let lhs = self.check_expr(lhs_e);
let rhs = self.check_expr(rhs_e);

// When both operands are IntLiteral, evaluate the expression to preserve
// literal type through binary operations (needed for -(1 + 2) to work).
if let (TyKind::IntLiteral(..), TyKind::IntLiteral(..)) = (lhs.kind, rhs.kind)
&& !op.kind.is_cmp()
&& let Some(lit_ty) = self.try_eval_int_literal_binop(expr)
{
return lit_ty;
}

self.check_binop(lhs_e, lhs, rhs_e, rhs, op, false)
}
hir::ExprKind::Call(callee, ref args, ref _opts) => {
Expand Down Expand Up @@ -472,6 +483,16 @@ impl<'gcx> TypeChecker<'gcx> {
let _ = (ty, expr);
}

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

fn check_binop(
&mut self,
lhs_e: &'gcx hir::Expr<'gcx>,
Expand Down
4 changes: 2 additions & 2 deletions tests/ui/typeck/eval.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ contract C {
function a2(uint[x / zeroPublic] memory) public {} //~ ERROR: failed to evaluate constant: attempted to divide by zero
function b(uint[x] memory) public {}
function c(uint[x * 2] memory) public {}
function d(uint[0 - 1] memory) public {} //~ ERROR: failed to evaluate constant: arithmetic overflow
function d2(uint[zeroPublic - 1] memory) public {} //~ ERROR: failed to evaluate constant: arithmetic overflow
function d(uint[0 - 1] memory) public {} //~ ERROR: array length cannot be negative
function d2(uint[zeroPublic - 1] memory) public {} //~ ERROR: array length cannot be negative
function e(uint[rec1] memory) public {} //~ ERROR: failed to evaluate constant: recursion limit reached
function f(uint[rec2] memory) public {} //~ ERROR: failed to evaluate constant: recursion limit reached

Expand Down
8 changes: 4 additions & 4 deletions tests/ui/typeck/eval.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ error: failed to evaluate constant: attempted to divide by zero
LL │ function a2(uint[x / zeroPublic] memory) public {}
╰╴ ━━━━━━━━━━━━━━ evaluation of constant value failed here

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

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

error: failed to evaluate constant: recursion limit reached
╭▸ ROOT/tests/ui/typeck/eval.sol:LL:CC
Expand Down
17 changes: 17 additions & 0 deletions tests/ui/typeck/implicit_int_literal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,21 @@ function f() {
// Negative literals cannot coerce to unsigned types
uint8 neg_to_uint8 = -1; //~ ERROR: mismatched types
uint256 neg_to_uint256 = -42; //~ ERROR: mismatched types

// === Edge cases: parenthesized and compound expressions ===
// Parentheses don't change the semantics - still a negative literal
int16 paren_neg = -(1);
int16 double_paren_neg = -((1));

// Double negation: negates a negative literal, result is non-negative
// -(-1) = 1, which is int_literal[1] non-negative
int16 double_neg = -(-1);

// Negation of binary expressions - binary ops on int literals are now
// evaluated during type checking to preserve the literal type
int16 neg_binop = -(1 + 2);

// More complex expressions with literals
int16 complex_lit = -(2 * 3 + 1);
int32 large_lit = -(256 + 1); // Result is -257, int_literal[2]
}
Loading