Yul optimiser: switch cases are ordered by literal *kind* but compared by *value*, so equivalent functions are never merged
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
## Description
Two functions that are identical except that one writes `case 0` and the other writes
`case false` are never recognised as equivalent, so `EquivalentFunctionCombiner` does not merge
them and both are emitted. The two switches are semantically the same — a Yul switch dispatches on
the numeric value, and `false` and `0` are the same value.
The cause is that the ordering relation and the equality relation applied to the same set of cases
disagree about whether `LiteralKind` matters.
`libyul/optimiser/SyntacticalEquality.cpp:126-138` puts both switches' cases into a sorted set and
then walks the two sets pairwise:
```cpp
std::set lhsCases;
std::set rhsCases;
...
util::containerEqual(lhsCases, rhsCases, [this](Case const* _lhsCase, Case const* _rhsCase) {
return this->switchCaseEqual(*_lhsCase, *_rhsCase);
});
```
- The **ordering**, `SwitchCaseCompareByLiteralValue` (`libyul/Utilities.cpp:239`), delegates to
`Less` (`libyul/Utilities.cpp:224`), which orders **`kind` first, then `value`**:
```cpp
if (_lhs.kind != _rhs.kind)
return _lhs.kind < _rhs.kind;
```
- The **equality** it is paired with, `expressionEqual(Literal, Literal)`
(`SyntacticalEquality.cpp:74-79`), compares **value only**:
```cpp
return _lhs.value == _rhs.value;
```
So the ordering is strictly finer than the equality applied to the same containers. Cases that the
equality considers identical are sorted into different positions, the pairwise walk lines up
mismatched cases, and the switches compare unequal.
`libyul/optimiser/BlockHasher.cpp:154` uses the same comparator to order cases before hashing, so
the two functions also hash differently and are not even considered as merge candidates.
Note that the comparator is named `SwitchCaseCompareByLiteralValue` and its declaration comment
speaks about cases within one switch — the name says *value*, while the body orders by kind first.
That is what makes me think this is an oversight rather than a deliberate choice.
**Expected:** the two functions below are merged, as they are when both switches write `case 0`.
**Actual:** both are emitted.
## Environment
- Compiler version: 0.8.36+commit.8a079791 (cited lines byte-identical on current `develop`)
- Compilation pipeline (legacy, IR, SSA CFG): Yul optimiser — affects `--strict-assembly` and
`--via-ir`
- Target EVM version (as per compiler settings): default; not EVM-version dependent
- Framework/IDE (e.g. Foundry, Hardhat, Remix): `solc` command line directly
- EVM execution environment / backend / blockchain client: n/a — compile-time only
- Operating system: macOS 26.5.2
## Steps to Reproduce
`trigger.yul` — `f` and `g` differ only in `case false` vs `case 0`:
```yul
object "C" {
code {
function f(a) -> r {
r := 0
switch a
case false { r := add(mul(a,3),7) sstore(0,r) sstore(1,add(r,1)) sstore(2,add(r,2)) sstore(3,add(r,3)) }
case 2 { r := add(mul(a,5),8) sstore(4,r) sstore(5,add(r,1)) sstore(6,add(r,2)) sstore(7,add(r,3)) }
default { r := 99 sstore(8,r) sstore(9,add(r,1)) }
}
function g(b) -> r {
r := 0
switch b
case 0 { r := add(mul(b,3),7) sstore(0,r) sstore(1,add(r,1)) sstore(2,add(r,2)) sstore(3,add(r,3)) }
case 2 { r := add(mul(b,5),8) sstore(4,r) sstore(5,add(r,1)) sstore(6,add(r,2)) sstore(7,add(r,3)) }
default { r := 99 sstore(8,r) sstore(9,add(r,1)) }
}
mstore(0, f(calldataload(0)))
mstore(32, g(calldataload(32)))
mstore(64, f(calldataload(64)))
mstore(96, g(calldataload(96)))
return(0, 128)
}
}
```
`control.yul` is the same file with `case false` changed to `case 0`.
```
$ solc --strict-assembly --optimize control.yul | grep -c '^\s*function '
1
$ solc --strict-assembly --optimize trigger.yul | grep -c '^\s*function '
2
```
The bodies are otherwise byte-identical. This is with the **default** optimiser sequence, not a
hand-picked one.
## Notes
- The failure direction is always *false unequal* — equivalent things compare unequal, never the
reverse — so this is a missed optimisation and not a correctness problem. Both functions are
compiled correctly; the output is just larger than it needs to be. I would triage it as
optimiser quality rather than as a bug affecting generated code.
- `solc`'s own generated IR does not appear to emit boolean-kind case literals, so this is only
reachable from hand-written Yul or from inline assembly in a Solidity source file
(`assembly { switch a case false { ... } }`, which compiles). That further limits the impact.
- I searched the tracker ("switch case literal kind equality optimizer", "EquivalentFunctionCombiner
not merging", "boolean literal switch case yul") and found nothing on point, but I may have
missed prior art — please close as a duplicate if so.
Contributor guide
Assessment
This issue has not been assessed yet.