Pipeline divergence in tuple assignments aliasing storage with a memory LHS component
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
## Description
Tuple assignments where one LHS component is a memory variable receiving a storage reference type and another LHS component (at a higher tuple index) writes to the storage slot read by that first component produce different runtime results under the evmasm and IR pipelines.
The evmasm pipeline materializes the storage to memory copy before any LHS store, so the memory component captures a pre-write snapshot. The IR pipeline emits the storage to memory copy lazily at the per-component write site, after the aliased storage slot has already been overwritten, so the memory component captures post-write data.
The language documentation (see `docs/control-structures.rst`, section on destructuring assignments) currently states that
> "Tuples are not proper types in Solidity, they can only be used to form syntactic groupings of expressions."
So strictly speaking, `(a, b) = (c, d)` is a syntactic shorthand for component-level assignments whose interleaving is not pinned down. There is no "tuple value" on the RHS that could be snapshotted. Neither pipeline violates a written spec, but the silent divergence is unlikely to have been intentional.
## Steps to Reproduce
```solidity
contract C {
struct S { uint256 a; uint256 b; }
S current;
S pending;
function f() external returns (uint256 preA, uint256 preB, uint256 postA, uint256 postB) {
current = S(1, 2);
pending = S(10, 20);
S memory preSwap;
(preSwap, current) = (current, pending);
preA = preSwap.a;
preB = preSwap.b;
postA = current.a;
postB = current.b;
}
}
```
Calling `f()`:
- evmasm pipeline: returns `1, 2, 10, 20`
- IR pipeline: returns `10, 20, 10, 20`
Reproduces with and without `--optimize`; the divergence originates in IR generation, not in any optimizer pass.
## Mechanism
Both pipelines issue LHS stores right-to-left and both write `current` before `preSwap`. The difference is in *when* the storage→memory copy for `preSwap`'s RHS component is emitted:
- **evmasm pipeline:** `convertType` runs over the RHS tuple before any LHS store. For the storage→memory component this immediately emits "read slot, allocate memory, copy", so the snapshot is materialized before `current` is overwritten.
- **IR pipeline:** in `IRGeneratorForStatements::visit(Assignment)`, the eager RHS convert is skipped when the LHS is a tuple (not a value type), so reference-type RHS components stay as live slot references. `writeToLValue` for `IRLValue::Tuple` then emits the storage to memory copy at the per-component write site, which runs after the write to `current`, so it reads post-mutation storage.
Relevant locations:
- RHS convert is gated on `isValueType()` of the LHS in https://github.com/argotorg/solidity/blob/eccdf47e50a0fa547197b8949d9f7f9a4041224e/libsolidity/codegen/ir/IRGeneratorForStatements.cpp#L444-L450
- `IRLValue::Tuple` writes components right-to-left, calling `writeToLValue` per component in https://github.com/argotorg/solidity/blob/eccdf47e50a0fa547197b8949d9f7f9a4041224e/libsolidity/codegen/ir/IRGeneratorForStatements.cpp#L3256-L3264
## Some possible directions
1. Document the divergence in `docs/ir-breaking-changes.rst` and/or strengthen the existing warning in `docs/control-structures.rst` to explicitly call out the storage-aliasing-via-tuple case.
2. Emit a compile-time warning when a tuple assignment has a memory LHS component whose RHS reads a storage reference whose slot is also written by another LHS component of the same tuple.
3. Align the pipelines on one canonical behavior.
Contributor guide
Assessment
This issue has not been assessed yet.