Skip to content
Draft
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
28 changes: 28 additions & 0 deletions crates/sema/src/ty/ty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,9 @@ impl<'gcx> Ty<'gcx> {
/// Checks if the type is explicitly convertible to the given type.
///
/// See: <https://docs.soliditylang.org/en/latest/types.html#explicit-conversions>
///
/// Follows Solidity 0.8.0+ rules where explicit conversions are as strict as implicit.
/// See: <https://docs.soliditylang.org/en/latest/080-breaking-changes.html>
#[allow(clippy::result_unit_err)]
pub fn try_convert_explicit_to(self, other: Self) -> Result<(), ()> {
use ElementaryType::*;
Expand Down Expand Up @@ -504,6 +507,31 @@ impl<'gcx> Ty<'gcx> {

// address -> address payable.
(Elementary(Address(false)), Elementary(Address(true))) => Ok(()),
// IntLiteral -> IntLiteral: always allowed.
// The restriction on negative -> unsigned applies when converting to concrete types,
// not between literals.
(IntLiteral(_, _), IntLiteral(_, _)) => Ok(()),

// Int <-> Int: any size allowed (only width changes, sign stays same).
(Elementary(Int(_)), Elementary(Int(_))) => Ok(()),

// UInt <-> UInt: any size allowed (only width changes, sign stays same).
(Elementary(UInt(_)), Elementary(UInt(_))) => Ok(()),

// Int <-> UInt: same size only (prevents multi-aspect conversion).
// This enforces the Solidity 0.8.0+ restriction: cannot change both sign and width.
(Elementary(Int(size_from)), Elementary(UInt(size_to)))
| (Elementary(UInt(size_from)), Elementary(Int(size_to)))
if size_from == size_to =>
{
Ok(())
}

// IntLiteral(positive) -> Int or UInt: always allowed.
(IntLiteral(false, _), Elementary(Int(_) | UInt(_))) => Ok(()),

// IntLiteral(negative) -> Int: allowed.
(IntLiteral(true, _), Elementary(Int(_))) => Ok(()),

_ => Result::Err(()),
}
Expand Down
97 changes: 97 additions & 0 deletions tests/ui/typeck/integer_explicit_conversions.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//@compile-flags: -Ztypeck

contract IntegerConversions {
// Int <-> Int (any size - only width changes)
function validIntToInt(int8 i8, int256 i256) public pure {
int256 a = int256(i8);
int8 b = int8(i256);
int128 c = int128(i8);
int64 d = int64(i8);
int16 e = int16(i256);
}

// UInt <-> UInt (any size - only width changes)
function validUintToUint(uint8 u8, uint256 u256) public pure {
uint256 a = uint256(u8);
uint8 b = uint8(u256);
uint128 c = uint128(u8);
uint64 d = uint64(u8);
uint16 e = uint16(u256);
}

// Int <-> UInt (same size only)
function validIntUintSameSize(int8 i8, uint8 u8, int128 i128, uint128 u128) public pure {
uint8 a = uint8(i8);
int8 b = int8(u8);
uint128 c = uint128(i128);
int128 d = int128(u128);
}

// Int <-> UInt (different sizes - multi-aspect conversion)
// Cannot change both sign and width in one conversion
function invalidIntUintDifferentSize(int8 i8, uint256 u256, int16 i16, uint32 u32) public pure {
uint256 a = uint256(i8); //~ ERROR: cannot convert
int8 b = int8(u256); //~ ERROR: cannot convert
uint32 c = uint32(i16); //~ ERROR: cannot convert
int16 d = int16(u32); //~ ERROR: cannot convert
uint128 e = uint128(i8); //~ ERROR: cannot convert
}

// IntLiteral (positive) -> Int/UInt (any size)
function validPositiveLiteralToInt() public pure {
uint8 a = uint8(42);
uint16 b = uint16(256);
uint256 c = uint256(12345);
int8 d = int8(42);
int16 e = int16(100);
int256 f = int256(12345);
}

// Positive literals with various sizes
function validPositiveLiteralVariousSizes() public pure {
uint128 a = uint128(0);
uint32 b = uint32(0xFFFFFFFF);
int64 c = int64(9223372036854775807); // max int64
Copy link
Member

Choose a reason for hiding this comment

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

can you also check overflow fails? same line but with 9223372036854775808

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right, mb. The check on int literal is not sufficient btw. I'll fix it to comply w/:

  1. Explicit conversions between literals and an integer type T are only allowed if the literal lies between type(T).min and type(T).max. [...]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's actually blocked for the moment (maybe solved by #566)

In fact we need to check the value and the sign of the literal to make this assertion : type(T).min <= literal_value < type(T).max.

So, mapping from/to Ty objects is not enough, as we need the value from LitKind::Number.

Copy link
Member

Choose a reason for hiding this comment

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

would it be possible with TypeSize storing bits? that way min..=max for intX is X-1 bits and uintX is X bits

Copy link
Contributor Author

@mablr mablr Dec 14, 2025

Choose a reason for hiding this comment

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

That's what i tried initially, but it seems to be to restrictive given the granularity of TypeSize (byte).

IntLiteral(positive) -> Uint: OK

(IntLiteral(false, size_from), Elementary(UInt(size_to))) => { if size_from.bits() <= size_to.bits() { Ok(()) } else { Result::Err(()) }},

IntLiteral(negative) -> Int: I guess OK (but can't test it without #566)

(IntLiteral(true, size_from), Elementary(Int(size_to))) => { if size_from.bits() <= size_to.bits() { Ok(()) } else { Result::Err(()) } },

IntLiteral(positive) -> Int: too restrictive

(IntLiteral(false, size_from), Elementary(Int(size_to))) => { if size_from.bits() < size_to.bits() { Ok(()) } else { Result::Err(()) } },

i.e int8 d = int8(0xF0); is rejected because size_from.bits() == 8 and size_to.bits() == 8 which doesn't reflect the value bounds (−2^(n−1) to 2^(n−1) − 1)

Copy link
Member

@DaniPopes DaniPopes Dec 15, 2025

Choose a reason for hiding this comment

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

right, what if typesize had bit granularity for literals? so that intX::MIN is typesize(X-1 : u16) instead of typesize(X/8 : u8)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what if typesize had bit granularity for literals?

That would be great!

Correct me if I’m wrong, i think to produce Ty IntLiteral objects, it will be necessary anyway to hold the sign in the ast/hir number (the value as u256 and boolean for the sign).

Copy link
Contributor

@dipanshuhappy dipanshuhappy Dec 17, 2025

Choose a reason for hiding this comment

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

what if typesize had bit granularity for literals?

That would be great!

Correct me if I’m wrong, i think to produce Ty IntLiteral objects, it will be necessary anyway to hold the sign in the ast/hir number (the value as u256 and boolean for the sign).

Yep it seems so, looking at the expression enum here
Lit only stores the numerical value, while sign is held by Unary

All these are translated in the typechecker to Ty::IntLiteral here

}

// IntLiteral (negative) -> Int
// Note: unary minus on literals requires special handling in parser
function validNegativeLiteralToInt() public pure {
int8 a = int8(42);
int16 b = int16(256);
int256 c = int256(12345);
int128 d = int128(1);
}

// IntLiteral -> IntLiteral (allowed)
function validLiteralToLiteral() public pure {
int256 a = int256(uint256(42));
uint256 b = uint256(uint128(100));
int64 c = int64(int32(42));
}

// UInt -> FixedBytes (same size)
function validUintToBytes(uint8 u8, uint32 u32, uint256 u256) public pure {
bytes1 b1 = bytes1(u8);
bytes4 b4 = bytes4(u32);
bytes32 b32 = bytes32(u256);
}

// UInt -> FixedBytes (different size)
function invalidUintToBytesDifferentSize(uint8 u8, uint32 u32) public pure {
bytes2 b2 = bytes2(u8); //~ ERROR: cannot convert
bytes8 b8 = bytes8(u32); //~ ERROR: cannot convert
}

// Nested type conversions
function validComplexConversions() public pure {
uint256 a = uint256(int256(42));
int128 b = int128(int64(100));
uint8 c = uint8(uint16(255));
}

function validNestedConversions(int8 i8) public pure {
uint16 a = uint16(int16(i8));
uint32 b = uint32(int32(i8));
}
}
44 changes: 44 additions & 0 deletions tests/ui/typeck/integer_explicit_conversions.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
error: cannot convert `int8` to `uint256`
╭▸ ROOT/tests/ui/typeck/integer_explicit_conversions.sol:LL:CC
LL │ uint256 a = uint256(i8);
╰╴ ━━━━━━━━━━━ invalid explicit type conversion

error: cannot convert `uint256` to `int8`
╭▸ ROOT/tests/ui/typeck/integer_explicit_conversions.sol:LL:CC
LL │ int8 b = int8(u256);
╰╴ ━━━━━━━━━━ invalid explicit type conversion

error: cannot convert `int16` to `uint32`
╭▸ ROOT/tests/ui/typeck/integer_explicit_conversions.sol:LL:CC
LL │ uint32 c = uint32(i16);
╰╴ ━━━━━━━━━━━ invalid explicit type conversion

error: cannot convert `uint32` to `int16`
╭▸ ROOT/tests/ui/typeck/integer_explicit_conversions.sol:LL:CC
LL │ int16 d = int16(u32);
╰╴ ━━━━━━━━━━ invalid explicit type conversion

error: cannot convert `int8` to `uint128`
╭▸ ROOT/tests/ui/typeck/integer_explicit_conversions.sol:LL:CC
LL │ uint128 e = uint128(i8);
╰╴ ━━━━━━━━━━━ invalid explicit type conversion

error: cannot convert `uint8` to `bytes2`
╭▸ ROOT/tests/ui/typeck/integer_explicit_conversions.sol:LL:CC
LL │ bytes2 b2 = bytes2(u8);
╰╴ ━━━━━━━━━━ invalid explicit type conversion

error: cannot convert `uint32` to `bytes8`
╭▸ ROOT/tests/ui/typeck/integer_explicit_conversions.sol:LL:CC
LL │ bytes8 b8 = bytes8(u32);
╰╴ ━━━━━━━━━━━ invalid explicit type conversion

error: aborting due to 7 previous errors

Loading