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
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Language Features:
Compiler Features:

Bugfixes:
* TypeChecker: Allow assignment of `string.concat` or `bytes.concat` to constant variables.


### 0.8.31 (2025-12-03)
Expand Down
10 changes: 10 additions & 0 deletions libsolidity/analysis/TypeChecker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3271,11 +3271,21 @@ bool TypeChecker::visit(MemberAccess const& _memberAccess)
// TODO some members might be pure, but for example `address(0x123).balance` is not pure
// although every subexpression is, so leaving this limited for now.
if (auto tt = dynamic_cast<TypeType const*>(exprType))
{
if (
tt->actualType()->category() == Type::Category::Enum ||
tt->actualType()->category() == Type::Category::UserDefinedValueType
)
annotation.isPure = true;

// `concat` purity depends also on its arguments, but this is checked later, in visit(FunctionCall...)
if (
// This covers `bytes.concat` and `string.concat`.
tt->actualType()->category() == Type::Category::Array &&
memberName == "concat"
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be safer to check for FunctionType::Kind::StringConcat/FunctionType::Kind::BytesConcat rather than function name.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would also add an assert at the end of this function that for function types the annotation matches the result of FunctionType::isPure().

)
annotation.isPure = true;
}
if (
auto const* functionType = dynamic_cast<FunctionType const*>(exprType);
functionType &&
Expand Down
4 changes: 3 additions & 1 deletion libsolidity/ast/Types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3713,7 +3713,9 @@ bool FunctionType::isPure() const
m_kind == Kind::ABIDecode ||
m_kind == Kind::MetaType ||
m_kind == Kind::Wrap ||
m_kind == Kind::Unwrap;
m_kind == Kind::Unwrap ||
m_kind == Kind::BytesConcat ||
m_kind == Kind::StringConcat;
}

TypePointers FunctionType::parseElementaryTypeVector(strings const& _types)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
contract A {
}

contract B {
bytes constant creationCode = type(A).creationCode;
bytes constant runtimeCode = type(A).runtimeCode;

function isEmptyCode() public pure returns (bool) {
return creationCode.length > 0 && runtimeCode.length > 0;
Comment on lines +8 to +9
Copy link
Collaborator

Choose a reason for hiding this comment

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

The name does not match the condition.

Suggested change
function isEmptyCode() public pure returns (bool) {
return creationCode.length > 0 && runtimeCode.length > 0;
function nonEmptyCode() public pure returns (bool) {
return creationCode.length > 0 && runtimeCode.length > 0;

}
}
// ----
// isEmptyCode() -> true
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
contract C {
function f() public pure {
bytes.concat(
function f() public pure returns (bytes memory) {
return bytes.concat(
hex"00",
hex"aabbcc",
unicode"abc",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
contract C {
function f() public pure {
bytes.concat(hex"", unicode"", "");
function f() public pure returns (bytes memory) {
return bytes.concat(hex"", unicode"", "");
}
}
// ----
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bytes constant aEncoded = abi.encode(
hex"aaaa"
);

contract A {
bytes constant a = abi.decode(aEncoded, (bytes));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
contract A {
function encoded() private view returns (bytes memory) {
return abi.encode(hex"aaaa");
}

bytes constant a = abi.decode(encoded(), (bytes));
}
// ----
// TypeError 8349: (142-172): Initial value for constant variable has to be compile-time constant.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
contract A {
function f() external {}
bytes constant fCallA = abi.encodeCall(A.f, ());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
contract A {
function f(uint a) external {}

function getA() private view returns(uint) {
return 1;
}

bytes constant fCallA = abi.encodeCall(A.f, (getA()));
}
// ----
// TypeError 8349: (151-180): Initial value for constant variable has to be compile-time constant.
Copy link
Collaborator

@cameel cameel Dec 11, 2025

Choose a reason for hiding this comment

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

It would be good to use a more regular naming scheme for these tests. Checking if we already have tests covering something is always a challenge and this would make it a bit easier. Here's how I'd name these:

abi_decode_pure_assignment.sol                       -> assign_abi_decode_non_const_args.sol
abi_decode_pure_assignment_error.sol                 -> assign_abi_decode_const_args.sol
abi_encoding_constant_error.sol                      -> assign_abi_encoding_builtin_non_const_args.sol
abi_encode_call_pure_assignment.sol                  -> assign_abi_encode_const_args.sol
abi_encode_call_pure_assignment_error.sol            -> assign_abi_encode_non_const_args.sol
block_and_tx_properties_pure_assignment_error.sol    -> assign_block_tx_msg_property.sol
bytes_concat_pure_assignment.sol                     -> assign_bytes_concat_const_args.sol
bytes_concat_pure_assignment_error.sol               -> assign_bytes_concat_non_const_args.sol
string_concat_pure_assignment.sol                    -> assign_string_concat_const_args.sol
string_concat_pure_assignment_error.sol              -> assign_string_concat_non_const_args.sol
math_functions_opcodes_pure_assignment_error.sol     -> assign_math_builtin_opcode_based_non_const_args.sol
math_functions_precompiles_pure_assignment_error.sol -> assign_math_builtin_precompile_based_non_const_args.sol
math_functions_pure_assignment.sol                   -> assign_math_builtin_const_args.sol
type_information_pure_assignment.sol                 -> assign_type_info.sol

General remarks:

  • Instead of appending _error I'd rather have the test name say what exactly makes the test different. It makes it easier to see what the test is about just from the name and sometimes these errors are there only temporarily, because some features is not yet implemented.
  • Instead of the assign_ prefix I suggested above you could also group them by moving them into a subdir, e.g. initialization/.
  • The names of existing tests use pure and constant interchangeably. That's because I think initially pure was meant to be a compile-time constant. With time they have diverged and pure will likely remain its own thing. It's fine to stick to original naming if it's consistent, but otherwise we should distinguish these.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
contract C {
uint k = 1;

bytes32 constant a = keccak256(abi.encode(1, k));
bytes32 constant b = keccak256(abi.encodePacked(uint(1), k));
bytes32 constant c = keccak256(abi.encodeWithSelector(0x12345678, k, 2));
bytes32 constant d = keccak256(abi.encodeWithSignature("f()", 1, k));
}
// ----
// TypeError 8349: (55-82): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (109-148): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (175-226): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (253-300): Initial value for constant variable has to be compile-time constant.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
bytes32 constant blockhGlobal = blockhash(1);
bytes32 constant blobhGlobal = blobhash(1);
uint constant bfGlobal = block.basefee;
uint constant blobbfGlobal = block.blobbasefee;
uint constant chainIdGlobal = block.chainid;
address constant coinbaseGlobal = block.coinbase;
uint constant diffGlobal = block.difficulty;
uint constant gaslimitGlobal = block.gaslimit;
uint constant numberGlobal = block.number;
uint constant prevrandaoGlobal = block.prevrandao;
uint constant timestampGlobal = block.timestamp;
uint constant gGlobal = gasleft();
bytes constant dataGlobal = msg.data;
address constant senderGlobal = msg.sender;
bytes4 constant sigGlobal = msg.sig;
uint constant valueGlobal = msg.value;
uint constant gaspriceGlobal = tx.gasprice;
address constant originGlobal = tx.origin;

contract A {
bytes32 constant blockh = blockhash(1);
bytes32 constant blobh = blobhash(1);
uint constant bf = block.basefee;
uint constant blobbf = block.blobbasefee;
uint constant chainId = block.chainid;
address constant coinbase = block.coinbase;
uint constant diff = block.difficulty;
uint constant gaslimit = block.gaslimit;
uint constant number = block.number;
uint constant prevrandao = block.prevrandao;
uint constant timestamp = block.timestamp;
uint constant g = gasleft();
bytes constant data = msg.data;
address constant sender = msg.sender;
bytes4 constant sig = msg.sig;
uint constant value = msg.value;
uint constant gasprice = tx.gasprice;
address constant origin = tx.origin;
}
// ====
// EVMVersion: >=cancun
// ----
// TypeError 8349: (32-44): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (77-88): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (115-128): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (159-176): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (208-221): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (257-271): Initial value for constant variable has to be compile-time constant.
// Warning 8417: (300-316): Since the VM version paris, "difficulty" was replaced by "prevrandao", which now returns a random number based on the beacon chain.
// TypeError 8349: (300-316): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (349-363): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (394-406): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (441-457): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (491-506): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (532-541): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (571-579): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (613-623): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (653-660): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (690-699): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (732-743): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (777-786): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (832-844): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (875-886): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (911-924): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (953-970): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1000-1013): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1047-1061): Initial value for constant variable has to be compile-time constant.
// Warning 8417: (1088-1104): Since the VM version paris, "difficulty" was replaced by "prevrandao", which now returns a random number based on the beacon chain.
// TypeError 8349: (1088-1104): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1135-1149): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1178-1190): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1223-1239): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1271-1286): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1310-1319): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1347-1355): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1387-1397): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1425-1432): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1460-1469): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1500-1511): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (1543-1552): Initial value for constant variable has to be compile-time constant.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
bytes constant abcGlobal = bytes.concat(
hex"aaaa",
hex"bbbb",
hex"cccc"
);
Comment on lines +1 to +5
Copy link
Collaborator

Choose a reason for hiding this comment

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

For completeness I'd also make one of the arguments a constant.

And in bytes_concat_pure_assignment_error.sol I'd add a call to a function that is marked pure.


contract A {
bytes public constant abc = bytes.concat(
hex"aaaa",
hex"bbbb",
hex"cccc"
);

bytes public constant abcCopy = abc;
bytes public constant abcGlobalCopy = abcGlobal;
bytes public constant abcabc = bytes.concat(abc, abcGlobal);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
contract A {
function getData() public view returns (bytes memory) {
return msg.data;
}

bytes constant abData = bytes.concat(
hex"aaaa",
hex"bbbb",
msg.data
);
Comment on lines +6 to +10
Copy link
Collaborator

Choose a reason for hiding this comment

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

The args in these tests are pretty short, so I'd put them on a single line to make these it less verbose.

Suggested change
bytes constant abData = bytes.concat(
hex"aaaa",
hex"bbbb",
msg.data
);
bytes constant abData = bytes.concat(hex"aaaa", hex"bbbb", msg.data);


bytes constant abgetData = bytes.concat(
hex"aaaa",
hex"bbbb",
getData()
);
}
// ----
// TypeError 8349: (133-207): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (241-316): Initial value for constant variable has to be compile-time constant.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
contract A {
uint256 k = 7;
uint256 constant amod = addmod(1, 8, k);
uint256 constant mmod = mulmod(1, 8, k);
Comment on lines +2 to +4
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we have a variant of this test but with constant args?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same for math_functions_precompiles_pure_assignment_error.sol


bytes data = hex"ffff";
bytes32 constant keccak = keccak256(data);
}
// ----
// TypeError 8349: (60-75): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (105-120): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (181-196): Initial value for constant variable has to be compile-time constant.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
contract A {
bytes data = hex"ffff";
bytes32 constant sha = sha256(data);
bytes20 constant ripemd = ripemd160(data);
address constant addr = ecrecover("1234", 1, "0", abi.decode(data, (bytes2)));
}
// ----
// TypeError 8349: (68-80): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (112-127): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (157-210): Initial value for constant variable has to be compile-time constant.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bytes32 constant sGlobal = sha256(hex"ffff");
address constant addrGlobal = ecrecover("1234", 1, "0", abi.decode("", (bytes2)));
Comment on lines +1 to +2
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add ripemd160() for completeness.


contract A {
bytes32 constant s = sha256(hex"ffff");
address constant addr = ecrecover("1234", 1, "0", abi.decode("", (bytes2)));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
string constant abcGlobal = string.concat(
"aaaa",
"bbbb",
"cccc"
);

contract A {
string public constant abc = string.concat(
"aaaa",
"bbbb",
"cccc"
);

string public constant abcCopy = abc;
string public constant abcGlobalCopy = abcGlobal;
string public constant abcabc = string.concat(abc, abcGlobal);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
contract A {
string name = "name";

function getName() public view returns (string memory) {
return name;
}

string public constant abName = string.concat(
"aaaa",
"bbbb",
name
);

string public constant abgetName = string.concat(
"aaaa",
"bbbb",
getName()
);
}
// ----
// TypeError 8349: (165-230): Initial value for constant variable has to be compile-time constant.
// TypeError 8349: (272-342): Initial value for constant variable has to be compile-time constant.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
string constant aNameGlobal = type(A).name;
bytes4 constant iNameGlobal = type(I).interfaceId;
uint256 constant minGlobal = type(uint256).min;
uint256 constant maxGlobal = type(uint256).max;

contract B {
}

interface I {
function hello() external pure;
function world(int) external pure;
}

contract A {
string constant aName = type(A).name;
bytes4 constant iName = type(I).interfaceId;
uint256 constant min = type(uint256).min;
uint256 constant max = type(uint256).max;
bytes creationCodeB = type(B).creationCode;
bytes runtimeCodeB = type(B).runtimeCode;
Comment on lines +19 to +20
Copy link
Collaborator

Choose a reason for hiding this comment

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

These are not marked constant:

Suggested change
bytes creationCodeB = type(B).creationCode;
bytes runtimeCodeB = type(B).runtimeCode;
bytes constant creationCodeB = type(B).creationCode;
bytes constant runtimeCodeB = type(B).runtimeCode;

And I'd add them at file level too.

}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
contract C layout at uint64(bytes8(bytes.concat("ABCD", "EFGH"))) {}
// ----
// TypeError 1139: (21-65): The base slot of the storage layout must be a compile-time constant expression.
// TypeError 1505: (21-65): The base slot expression contains elements that are not yet supported by the internal constant evaluator and therefore cannot be evaluated at compilation time.
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
contract C layout at uint(keccak256(bytes.concat("ABCD"))) {}
// ----
// TypeError 1139: (21-58): The base slot of the storage layout must be a compile-time constant expression.
// TypeError 1505: (21-58): The base slot expression contains elements that are not yet supported by the internal constant evaluator and therefore cannot be evaluated at compilation time.