`findStackTooDeep` aborts with "Could not create stack layout after 1000 iterations" on long chained library-call expressions
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
## Summary
A Solidity program that chains ≥998 internal-library calls into one expression aborts compilation with a `YulAssertion` inside the legacy stack shuffler:
```
Yul assertion failed:
libyul/backends/evm/StackHelpers.h(122): Throw in function
static void solidity::yul::Shuffler::shuffle(...)
std::exception::what: Could not create stack layout after 1000 iterations.
```
The user-visible error should be "stack too deep" (or some other surface diagnostic). Instead, the diagnostic pass itself crashes via `yulAssert`, which surfaces as an `InternalCompilerError`.
## Reproducer
Reproducer files (source + captured stacks): https://gist.github.com/msooseth/1acfe98e798799ccc94bd6f28e915e4b
```solidity
library L {
function double(bytes memory a) internal pure returns (bytes memory) {
return bytes.concat(a, a);
}
}
contract C {
using L for *;
function f() public returns (bytes memory) {
return "abc".double().double()… // 998 chained calls
}
}
```
Threshold: **998 chained `.double()` calls** is the minimum that triggers the assertion. 997 compiles (surfaces stack-too-deep, as expected); 998+ trips the iteration cap.
Affected compile modes (all the via-IR paths):
- `solc --via-ir source.sol` — crash
- `solc --via-ir --optimize source.sol` — crash
- `solc --experimental --via-ssa-cfg source.sol` — crash
- `solc source.sol` (no IR) — OK
- `solc --optimize source.sol` (no IR) — OK
## What the shuffler was actually trying to do
I instrumented `findStackTooDeep` in `libyul/backends/evm/StackLayoutGenerator.cpp` to dump `_source` and `_target` to a file when the underlying `Shuffler::shuffle` throws. Captured input:
```
reachableStackDepth: 16
source.size: 1
target.size: 1237
source: [FRET] # FunctionReturnLabelSlot of f()
target: [RET₁, RET₂, …, RET₁₂₃₆, T@?#0] # 1236 distinct FunctionCallReturnLabelSlots
# followed by 1 TemporarySlot
```
So the diagnostic is asked to walk an abstract shuffle from a 1-slot source `[FRET]` to a 1237-slot target consisting of 1236 deferred-call return labels plus a single temporary result slot.
## Root cause
`libyul/backends/evm/StackHelpers.h:117-123`:
```cpp
size_t iterationCount = 0;
while (iterationCount < 1000 && (needsMoreShuffling = shuffleStep(std::forward(args)...)))
++iterationCount;
yulAssert(!needsMoreShuffling, "Could not create stack layout after 1000 iterations.");
```
Each call to `shuffleStep` performs **at most one** stack operation (swap, dup, push, or pop). Reaching a target of size 1237 from a source of size 1 requires *at least* 1236 push/dup operations. With the cap hardcoded at 1000, convergence is mathematically impossible whenever `|target − source| > 1000`, independent of the cleverness of the shuffler.
This isn't (necessarily) a non-termination bug in the algorithm — it's a hard cap that wasn't scaled to input size. Whether the algorithm would actually converge in `O(|target|)` ops or get stuck in a swap-loop, the cap masks the distinction.
## Why \`--via-ssa-cfg\` still hits the *legacy* shuffler
The assertion comes from \`Shuffler::shuffle\` in \`StackHelpers.h\` (legacy), not the SSA \`StackShuffler\` in \`libyul/backends/evm/ssa/StackShuffler.h\`. Path:
```
EVMObjectCompiler::run // chooses ssa::CodeTransform when --via-ssa-cfg
└── (optimizer ran first) Yul optimizer Suite
└── StackCompressor::run
└── StackLayoutGenerator::reportStackTooDeep // legacy
└── findStackTooDeep // legacy, anonymous-namespace
└── ::createStackLayout // StackHelpers.h
└── Shuffler<…>::shuffle // ASSERT FIRES HERE
```
So the SSA codegen flag selects a different codegen *backend*, but the Yul **optimizer's \`StackCompressor\` pass** runs first and still goes through the legacy stack-layout analysis.
## Expected behaviour
\`findStackTooDeep\` is a *diagnostic*: it should be able to *report* that the layout is impossibly deep, not abort with an internal-compiler-error. The user should get a "Stack too deep" surface error.
Contributor guide
Assessment
This issue has not been assessed yet.