Resolve live linkersymbol against a supplied library link map - #145
Conversation
CI summary — ✅ All goodhead 1. Parsing
2. CorrectnessCompilation (positive corpora):
Behaviour differential vs solc:
3. Gasa) This compiler vs solc's optimized output — we compile solc's unoptimized
ours/solc > 100% is expected: this compiler has no Yul optimizer yet, so it spends more gas than solc's optimized output. This number is the size of that gap. It does not fail CI; only a regression above the pinned baseline does. b) Backend codegen parity — both this compiler and solc assemble the same, unoptimized Yul (solc
Here ours/solc near 100% is expected — neither side optimizes, so this compares raw code generation on identical input, not optimizer quality. 4. Compiler runtime (informational)Both columns measure the same job on the same input: unoptimized Yul → EVM bytecode, no optimizer on either side, over the same fixtures — only those both compilers finished are counted, on either side. solc's Solidity→Yul front-end is charged to neither: it runs once, before both, and its output is what each then compiles. a) Solidity corpora — both compile the unoptimized
Charged to neither column: 32.5 s of solc b) Yul corpora — the fixtures are already Yul, so both compile it directly; there is no front-end on either side.
Excluded from both columns: 1.4 min this compiler spent on 303 fixture(s) it then rejected. solc is not asked for those. Each figure is the sum of that suite's per-fixture compile spans, added across shards — independent of worker count and sharding, but measured on shared CI runners under saturated parallelism. Treat single-digit percentage moves as noise. Nothing here affects the verdict. 5. Soundness (formal guarantee)
6. Verdict✅ All good |
Scope correction: this moves zero test results, and cannot until
|
| compiles | |
|---|---|
without --libraries |
1 / 44 |
with --libraries |
1 / 44 |
The one that compiles does so either way. Linking unlocks nothing.
The reason is structural, not incidental: an external library call is
delegatecall(gas(), lib, …). A program that needs a live linkersymbol is
calling a public/external library function, which forwards gas — so it
necessarily also uses gas(), which this compiler rejects. 18 of the first 20
link-needing fixtures use gas(); the same coupling holds for the two in-repo
aave fixtures with live linker symbols (LiquidationLogic, SpokeOperations),
which need immutables as well.
So this is a prerequisite, not a win. It is small, sound and guarded, and it
is genuinely required before any linked program can compile — but it pays off
only jointly with gas() (powdr-labs/yul-semantics#41, currently parked). It
should be reviewed on that basis, and it may make more sense to land it
alongside the gas work than ahead of it.
The harness work (discover library names, deploy the libraries, pass matching
--libraries to solc so its reference bytecode links too) is still needed
eventually, but it is pointless until gas() is supported.
A *used* `linkersymbol("file.sol:Lib")` is solc's placeholder for the address a
linker substitutes — the `delegatecall` target of a public/external library
function. With no linker there is no sound value for it, so `compileSource`
pruned the provably dead bindings and rejected everything else. Real Solidity
that calls an external library therefore could not be compiled at all.
Supply the addresses instead. `compileSource` takes an optional `LinkEnv`,
exactly the `file.sol:Lib = 0xADDR` information solc's own `--libraries` flag
carries, and `yulc` exposes it as `--libraries=NAME=0xADDR[,…]`.
Resolution is a **substitution on the source program**, run before the
optimizer or the backend see anything: afterwards `linkersymbol` no longer
occurs and what is compiled is ordinary Yul. So the correctness statement does
not move — it is about the *linked* program, the same way `dataoffset`/
`datasize` resolution makes it about the concrete layout, and a different link
map is a different program. An `#guard` pins that equivalence: the linked
program compiles to exactly the bytecode of the program with the address
written out by hand.
Unresolved occurrences keep the previous behavior: pruned when provably dead,
rejected otherwise. No program is ever given a default address.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`linkStmt`/`linkObject` are an expensive identity on an empty `LinkEnv`, and the corpus runners feed this entry point megabytes of generated Yul (single fixtures already take minutes). Guard both paths on `libraries.isEmpty` so the default configuration rebuilds nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
06bf754 to
33a35ac
Compare
Standalone. No dependency on any other PR and no change to the pinned semantics.
Why
A used
linkersymbol("file.sol:Lib")is solc's placeholder for the address alinker substitutes — the
delegatecalltarget of a public/external libraryfunction. With no linker there is no sound value for it, so
compileSourcepruned the provably dead bindings and rejected everything else. Real Solidity
that calls an external library therefore could not be compiled at all.
In the in-repo fixtures this is not hypothetical:
test/aave-v4/SpokeOperations.solhas 11 live occurrences and
test/aave-v4/LiquidationLogic.solhas 2. (Theexisting pruner already covers
PoolManagerandHubOperationsentirely — allof their occurrences are dead placeholders.)
What
compileSourcetakes an optionalLinkEnv— exactly thefile.sol:Lib = 0xADDRinformation solc's own
--librariesflag carries — andyulcexposes it as--libraries=NAME=0xADDR[,…](splitting on the last=, since a library namemay itself contain
:).Resolution is a substitution on the source program, run before the optimizer
or the backend see anything: afterwards
linkersymbolno longer occurs and whatis compiled is ordinary Yul. So the correctness statement does not move — it is
about the linked program, the same way
dataoffset/datasizeresolutionmakes it about the concrete layout, and a different link map is a different
program.
Unresolved occurrences keep today's behavior exactly: pruned when provably dead,
rejected otherwise. No program is ever given a default address.
The second commit guards both paths on
libraries.isEmpty, because thesubstitution is an expensive identity on the default configuration and the
corpus runners feed this entry point megabytes of generated Yul.
Verification
lake buildclean;lake env lean Checks.leanstill reports exactlypropext,Classical.choice,Quot.sound.#guards pin: the used form compiles when linked, a different library isstill rejected, and — the important one — the linked program compiles to
exactly the bytecode of the program with the address written out by hand.
yulc --libraries=file.sol:L=0xdead…beefon adelegatecallthrough a library emits
…73 deadbeef…beef …f4…(PUSH20,DELEGATECALL),while the same file without the flag is still rejected.
🤖 Generated with Claude Code