Description
A constant state variable used as a fixed array length is only resolved if it appears earlier in
the source than the array declaration. Moving the two declarations past one another turns a
compiling contract into Error 5462, with no other change.
This contradicts the language rule the documentation states explicitly
(docs/control-structures.rst:447-449):
Variables and other items declared outside of a code block, for example functions, contracts,
user-defined types, etc., are visible even before they were declared. This means you can use state
variables before they are declared and call functions recursively.
The error message also points at the wrong thing: it reports the expression's form ("expected
integer literal or constant expression") when the expression is in fact a perfectly good constant
expression, and the real problem is that the analysis pass has not reached its definition yet.
Environment
- Compiler version: 0.8.36 (and 0.8.0 / 0.8.17 / 0.8.26 / 0.8.34)
- Rejection happens during analysis, so both pipelines are affected
Reproducer
Accepted:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
uint256 constant A = 2;
uint256[A] arr;
}
Rejected — the same two members, swapped:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
uint256[A] arr;
uint256 constant A = 2;
}
Error: Invalid array length, expected integer literal or constant expression.
--> order-rejected.sol:4:13:
|
4 | uint256[A] arr;
| ^
The same holds for a file-level constant declared after the contract that uses it, for arrays inside
a struct, and for array locals inside a function body.
The same constant, two uses, one contract
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
uint256[A] arr; // Error 5462
function g() public pure returns (uint256) { return A; } // fine
uint256 constant A = 2;
}
Only one error is reported, on line 4. A is usable in the function body and not as an array
length, in the same contract, with A declared after both.
The sibling context that gets it right
The custom storage-layout base slot is the other place a user constant is evaluated at compile time,
and it is order-independent:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.36;
contract C layout at A { uint256 x; }
uint256 constant A = 7; // declared AFTER the contract
solc --storage-layout layout-sibling.sol
# ... "label":"x", "slot":"7" -- resolved correctly
Same invariant ("a constant expression is usable here"), two enforcement sites, different answers.
Cause
DeclarationTypeChecker walks the AST in source order and resolves array lengths as it goes.
libsolidity/analysis/DeclarationTypeChecker.cpp:337-352 (endVisit(ArrayTypeName)):
if (Expression const* length = _typeName.length())
{
std::optional<rational> lengthValue;
if (length->annotation().type && length->annotation().type->category() == Type::Category::RationalNumber)
lengthValue = ...;
else if (ConstantEvaluator::TypedValue value = ConstantEvaluator::evaluate(m_errorReporter, *length);
std::holds_alternative<rational>(value.value)
)
lengthValue = std::get<rational>(value.value);
if (!lengthValue)
m_errorReporter.typeError(5462_error, length->location(),
"Invalid array length, expected integer literal or constant expression.");
ConstantEvaluator::evaluate then bails out at
libsolidity/analysis/ConstantEvaluator.cpp:319-324, and its own comment names the situation:
if (auto const* varDecl = dynamic_cast<VariableDeclaration const*>(&_node))
{
solAssert(varDecl->isConstant(), "");
// In some circumstances, we do not yet have a type for the variable.
if (!varDecl->value() || !varDecl->type())
m_values[&_node] = TypedValue{};
For a constant declared later in the source, DeclarationTypeChecker has not assigned its type yet,
so varDecl->type() is null, an empty TypedValue{} comes back, lengthValue stays unset, and
5462 is reported.
The layout specifier avoids this by being evaluated in a later pass —
libsolidity/analysis/PostTypeContractLevelChecker.cpp:87-101, after full type checking — where
every constant already has a type.
Relationship to #16055
#16055 ("Compile-time evaluation of member constants")
covers a different axis of the same ConstantEvaluator limitation: uint[D.LENGTH]
fails where uint[LENGTH] works, i.e. member access vs. standalone identifier. Its example
holds declaration order fixed (uint constant LENGTH = 42; uint[LENGTH] x; // OK, constant first),
so the ordering axis reported here is untouched by it. The two would likely be fixed together if the
fix is "evaluate array lengths in a later pass", but they are distinct symptoms and neither
reproducer implies the other.
Description
A
constantstate variable used as a fixed array length is only resolved if it appears earlier inthe source than the array declaration. Moving the two declarations past one another turns a
compiling contract into
Error 5462, with no other change.This contradicts the language rule the documentation states explicitly
(
docs/control-structures.rst:447-449):The error message also points at the wrong thing: it reports the expression's form ("expected
integer literal or constant expression") when the expression is in fact a perfectly good constant
expression, and the real problem is that the analysis pass has not reached its definition yet.
Environment
Reproducer
Accepted:
Rejected — the same two members, swapped:
The same holds for a file-level constant declared after the contract that uses it, for arrays inside
a
struct, and for array locals inside a function body.The same constant, two uses, one contract
Only one error is reported, on line 4.
Ais usable in the function body and not as an arraylength, in the same contract, with
Adeclared after both.The sibling context that gets it right
The custom storage-layout base slot is the other place a user constant is evaluated at compile time,
and it is order-independent:
Same invariant ("a constant expression is usable here"), two enforcement sites, different answers.
Cause
DeclarationTypeCheckerwalks the AST in source order and resolves array lengths as it goes.libsolidity/analysis/DeclarationTypeChecker.cpp:337-352(endVisit(ArrayTypeName)):ConstantEvaluator::evaluatethen bails out atlibsolidity/analysis/ConstantEvaluator.cpp:319-324, and its own comment names the situation:For a constant declared later in the source,
DeclarationTypeCheckerhas not assigned its type yet,so
varDecl->type()is null, an emptyTypedValue{}comes back,lengthValuestays unset, and5462 is reported.
The layout specifier avoids this by being evaluated in a later pass —
libsolidity/analysis/PostTypeContractLevelChecker.cpp:87-101, after full type checking — whereevery constant already has a type.
Relationship to #16055
#16055 ("Compile-time evaluation of member constants")
covers a different axis of the same
ConstantEvaluatorlimitation:uint[D.LENGTH]fails where
uint[LENGTH]works, i.e. member access vs. standalone identifier. Its exampleholds declaration order fixed (
uint constant LENGTH = 42; uint[LENGTH] x; // OK, constant first),so the ordering axis reported here is untouched by it. The two would likely be fixed together if the
fix is "evaluate array lengths in a later pass", but they are distinct symptoms and neither
reproducer implies the other.