argotorg / argotorg/solidity

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

Open
#16,981 0 comments 0 reactions 0 assignees View on GitHub
bug :bug:
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
2d 19h
Merged PRs (30d)
29

Description

## 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:

```solidity
// 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:

```solidity
// 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

```solidity
// 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**:

```solidity
// 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)`):

```cpp
if (Expression const* length = _typeName.length())
{
std::optional 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(value.value)
)
lengthValue = std::get(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:

```cpp
if (auto const* varDecl = dynamic_cast(&_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](https://github.com/argotorg/solidity/issues/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.

Contributor guide

Open the contributing guide

Research direction

Reproduce the two contract examples, then read DeclarationTypeChecker.cpp around endVisit(ArrayTypeName) and ConstantEvaluator.cpp around the VariableDeclaration handling. Compare this with PostTypeContractLevelChecker.cpp, which evaluates the layout specifier after type checking, and consult docs/control-structures.rst:447-449. Done means constants used as array lengths resolve regardless of declaration order in the reported contract, file-level, struct, and local-array cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, solidity
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.