argotorg / argotorg/solidity

Pathological compile time under `--via-ir` (and `--via-ssa-cfg`) on deeply nested `while (j < ++j)` with shadowed loop variables

Open
#16,748 4 comments 0 reactions 1 assignee Claimed by @nikola-matic View on GitHub
performance :racehorse:
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
1d 11h
Merged PRs (30d)
21

Description

## Summary

A ~9.6 KB / 834-line Solidity file with one function whose body is 190 nested `while (j < ++j)` loops (each re-declaring `uint j;` in its body, shadowing the outer one) causes `solc`'s via-IR pipelines to consume the full 12 s budget under `--via-ir` (with and without `--optimize`) and under `--experimental --via-ssa-cfg --optimize`. The legacy backend compiles the same file in <165 ms either way.

All three timing-out configs spend most of their time in `StackLayoutGenerator::reportStackTooDeep` → `processEntryPoint` → `combineStack`, with `StackCompressor::run` (43–59 %), `StackLimitEvader::run` (32–36 %) and `Multiplicity::operator[]` (~28–34 %, ~20–23 % self) as the dominant hot spots. Peak RSS stays modest (~42–53 MB) — this is CPU-bound rather than memory-bound.

Found via AFL fuzzing of `solc`.

## Environment

- Compiler: `0.8.36-develop.2026.5.18+commit.090bc8ff` (solidity submodule at `090bc8ff2`, top of `develop` after PR #16709), Release build.
- OS: `Linux 7.0.5-arch1-1`, AMD Ryzen 9 3900.

## Steps to reproduce

Reproducer (9563 bytes, 834 lines):
[`source.sol`](https://gist.github.com/msooseth/da80c2e5ffba316e1b3f1afe39e3494a).

Shape:

```solidity
contract ERC20 is Context {
function approve() public virtual {
uint x;
uint i;
while (i < 3) {
++i;
uint j;
while (j < ++j) {
++i;
uint j; // shadows the outer j
while (j < ++j) {
++i;
uint j; // shadows again
while (j < ++j) {
// … ~190 nested while-loops total, all sharing the same
// `uint j; while (j < ++j) { ... }` pattern,
// with occasional `}while(j < ++j) { ... }` re-opens
}
}
}
}
assert(x == 3);
}
}
```

Quantitatively, the file contains:

- **190** `while (j < ++j)` loops
- **173** `uint j;` redeclarations inside loop bodies (shadowing)
- a few `}while (j < ++j) { … }` sequences that re-open a new loop at the same brace depth (AFL-generated structure)

```bash
# Times measured with a 12 s wall-clock cap per invocation:
solc --bin --evm-version osaka --via-ir source.sol # >12 s (timeout), peak RSS 53 MB
solc --bin --evm-version osaka --via-ir --optimize source.sol # >12 s (timeout), peak RSS 42 MB
solc --bin --evm-version osaka source.sol # 161 ms
solc --bin --evm-version osaka --optimize source.sol # 142 ms
solc --bin --evm-version osaka --via-ir --optimize --experimental --via-ssa-cfg source.sol # >12 s (timeout), peak RSS 45 MB
```

## Profile — `solc --bin --via-ir source.sol` (no `--optimize`, truncated at 12 s)

| Incl. % | Self % | Symbol |
|---|---|---|
| 97.46 | 0.00 | `frontend::CompilerStack::generateIR` |
| 96.83 | 0.00 | `yul::YulStack::optimize` |
| 95.75 | 0.00 | `yul::OptimiserSuite::run` |
| **94.89** | 0.00 | **`yul::StackLayoutGenerator::reportStackTooDeep`** |
| 94.52 | 1.27 | `yul::StackLayoutGenerator::processEntryPoint` |
| **68.82** | 0.05 | **`yul::StackLayoutGenerator::combineStack`** |
| **59.20** | 0.00 | **`yul::StackCompressor::run`** |
| **36.25** | 0.00 | **`yul::StackLimitEvader::run`** |
| **33.93** | **23.05** | **`yul::Multiplicity::operator[]`** |
| 11.85 | 0.41 | `yul::Multiplicity::~Multiplicity` |
| 11.56 | 0.87 | `(anonymous namespace)::findStackTooDeep` |
| 10.67 | 0.88 | `StackLayoutGenerator::propagateStackThroughOperation` |
| 7.39 | 7.39 | `malloc` (libc) |
| 6.28 | 6.28 | `std::_Rb_tree_insert_and_rebalance` |

## Profile — `solc --bin --via-ir --optimize source.sol` (truncated at 12 s)

| Incl. % | Self % | Symbol |
|---|---|---|
| 92.61 | 0.00 | `CompilerStack::generateIR` |
| 92.10 | 0.00 | `yul::YulStack::optimize` |
| 91.63 | 0.00 | `yul::OptimiserSuite::run` |
| **74.90** | 0.00 | **`yul::StackLayoutGenerator::reportStackTooDeep`** |
| 74.49 | 1.10 | `yul::StackLayoutGenerator::processEntryPoint` |
| **64.66** | 0.02 | **`yul::StackLayoutGenerator::combineStack`** |
| **43.32** | 0.00 | **`yul::StackCompressor::run`** |
| **31.80** | 0.00 | **`yul::StackLimitEvader::run`** |
| **28.44** | **19.58** | **`yul::Multiplicity::operator[]`** |
| 16.75 | 0.00 | `yul::OptimiserSuite::runSequence` |
| 9.44 | 0.41 | `yul::UnusedAssignEliminator::operator()(yul::Block&)` |
| 9.42 | 0.06 | `UnusedStoreBase::operator()(yul::ForLoop&)` |
| 9.14 | 0.04 | `yul::ASTModifier::operator()(yul::Block&)` |
| 8.60 | 0.00 | `yul::DataFlowAnalyzer::operator()(yul::Block&)` |

The `--via-ssa-cfg --optimize` profile matches `--via-ir --optimize` essentially line-for-line: `reportStackTooDeep` 74.6 %, `processEntryPoint` 73.8 %, `combineStack` 64.2 %, `StackCompressor::run` 42.9 %, `StackLimitEvader::run` 31.9 %, `Multiplicity::operator[]` 28.4 % (19.8 % self), `UnusedAssignEliminator`/`ForLoop` ~9.7 %.

## Notes / possibly related

- #16745, #16746, #16747 — other `StackLayoutGenerator` / `StackCompressor` hot-spot reports from the same fuzzing campaign. NEW-B is distinct in shape: nested control-flow rather than a single long expression, and `StackLimitEvader::run` shows up as a major contributor (~32–36 %) which is not the case in those other reports. The `UnusedStoreBase<…>::operator()(ForLoop&)` contribution (~9 %) under `--optimize` also hints that the for-loop visitor cost scales with the nesting depth here.
- #14885 — `StackCompressor` infinite loop on assembly input; same component is hot, but different trigger (handwritten assembly there, deeply nested Solidity `while` loops with shadowed loop variables here).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.