Skip to content

A constant used as an array length must be declared before the array: valid contracts are rejected because of declaration order #16981

Description

@Lokkw0510

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions