Library linking, the gas() oracle prerequisite, and what actually blocks the aave/uniswap fixtures - #141
Closed
leonardoalt wants to merge 4 commits into
Closed
Library linking, the gas() oracle prerequisite, and what actually blocks the aave/uniswap fixtures#141leonardoalt wants to merge 4 commits into
leonardoalt wants to merge 4 commits into
Conversation
Pinned yul-semantics now parameterizes `gas()` by an `ExternalGas` oracle (powdr-labs/yul-semantics#41) instead of hard-coding the unconstrained `∃ g` read. Carry that parameter through this repository. `ExternalModel` gains a `gas` field, defaulting to `ExternalGas.any` — exactly the oracle every theorem here was implicitly stated against — so no statement covers fewer source runs than before. `opTable` still does not map `.gas`, so every program that reads it is still rejected; this is the prerequisite step, not the feature. The one non-mechanical part is `GuardedExternals`, which gains `gas_insensitive : GasScratchInsensitive gasOracle base reserved`. The guarded spilling transport moves a built-in across two states that differ only in compiler-owned scratch bytes; with the oracle a parameter, what it reports must demonstrably not depend on those bytes, exactly as already required of the call and creation relations. `lake build` is clean and `lake env lean Checks.lean` still reports exactly `propext`, `Classical.choice`, `Quot.sound` for every headline theorem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
CI summary — 🔴 Soundness unverified (build failed)head 1. Parsing
2. Correctness
3. Gas
4. Compiler runtime (informational)
5. Soundness (formal guarantee)
6. Verdict🔴 Soundness unverified (build failed) See the failing job logs above for details. |
…e tree Layout entries are keyed by `litValue (.string ·)`, which keeps only the first 32 UTF-8 bytes of the name. At nesting depth >= 2 `shiftChildEntries` builds compiler-internal qualified names, so a grandchild contributes both `"parent.child"` and — because solc emits a `.metadata` segment in every object — `"parent.child..metadata"`. Once the two generated names total more than 32 bytes those share a 32-byte prefix, their keys alias, and `compileResolvedObject`'s `Nodup` guard fails, rejecting the **whole object tree** even though nothing references either name. This is not a corner case: it rejects `test/aave-v4/LiquidationLogic.sol` outright. Every one of its sub-objects compiles on its own, including the 190 KB runtime, yet the assembled tree did not. Reduced, the trigger is twelve lines, and it needs *two* overflowing keys rather than one — with one generated name shortened (keys 26 and 36 bytes) the same tree already compiled. Such a name is unusable anyway: `litWF (.string s)` requires `s.toUTF8.size <= 32`, so the validator rejects any `dataoffset`/`datasize` naming it. Dropping the entry removes only dead weight — a program that does reference the name still fails to resolve and is rejected, exactly as before, rather than being miscompiled. Direct data segments come from `dataEntries` and are untouched, so `Layout.Consistent`, which quantifies over an object's *direct* segments, is unaffected; the object proofs went through unchanged. Guards pin the reduced tree and the already-working short-name variant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
Author
|
Superseded by the split, per review-sequencing with
The remaining commit here — threading the The investigation write-up in this description (what actually blocks the four |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft. Depends on powdr-labs/yul-semantics#41 (this branch pins the commit from
that PR in
lakefile.toml/lake-manifest.json, which are normallyhuman-approval-only — please review that bump explicitly).
What this delivers
1. Live library linking (
linkersymbol) — complete.compileSourcetakes an optionalLinkEnv, the samefile.sol:Lib = 0xADDRinformation solc's
--librariescarries;yulcexposes it as--libraries=NAME=0xADDR[,…]. Resolution is a substitution on the sourceprogram before the optimizer or backend run, so the correctness statement does
not move — it is about the linked program, the way
dataoffset/datasizeresolution makes it about the concrete layout. A
#guardpins that the linkedprogram compiles to exactly the bytecode of the program with the address
written out by hand. Unresolved occurrences keep today's behavior: pruned when
provably dead, rejected otherwise — never given a default address.
2. The
gas()oracle, threaded — prerequisite only.ExternalModelgains agasfield defaulting toExternalGas.any, exactlythe oracle every theorem here was implicitly stated against, so nothing is
weakened. The one non-mechanical part:
GuardedExternalsgainsgas_insensitive, because guarded spilling transports a built-in across statesdiffering only in compiler-owned scratch bytes, and with the oracle a parameter
what it reports must demonstrably not depend on those bytes — exactly as
already required of the call and creation relations.
lake buildis clean;lake env lean Checks.leanstill reports exactlypropext,Classical.choice,Quot.soundfor every headline theorem. Nosorry, no new axiom.What this does not deliver, and why
The goal was to make
test/aave-v4/{HubOperations,LiquidationLogic,SpokeOperations}.soland
test/uniswap-v4/PoolManager.solcompile. Three findings changed that plan.Finding 1 — it is three features, not two
Extracting solc's
--irfor each fixture and mapping the object trees: everyone needs
gas()(5–147 uses) and immutables (setimmutablein eachconstructor object,
loadimmutablein the matching_deployedobject, in thetop-level pair and in nested CREATE'd objects) and, for two of them, live
linkersymbol.PoolManagerandHubOperationsturn out to be fully coveredby the existing dead-binding pruner; only
LiquidationLogic(2 sites) andSpokeOperations(11 sites) have genuinely live ones.Finding 2 —
gas()cannot enter the fragment under the current theoremThis is the blocker, and it needs a human decision.
Pinned yul-semantics modeled
gas()as an unconstrained oracle (∃ g).yul-semantics#41 makes that a parameter, which is necessary — but not
sufficient. The obligation a target realization has to discharge is: given a
source derivation that already picked
g, produce a target run yieldingg.The target's
GASpushess.gasAvailable - 2. Any oracle expressible as arelation on the source state cannot pin that, because
StateMatch yst sadmits many
swith different remaining gas. So aGasRealizedin the shapeof
CallsRealized/CreatesRealizedis satisfiable only byExternalGas.none,i.e. vacuously — which would silently hollow out
compile_correctrather thanextend it. I stopped rather than land that.
The routes I can see, none of them small:
EvmStatean abstract gas component soStateMatchcan relate it to the target. Contradicts yul-semanticsDESIGN.md§1 ("gas is not modeled anywhere") and makes every built-inresponsible for keeping it in sync.
gas-reading programs, where the target's actual gas is one of the source's
admitted choices. Fits
ExternalGas.anynaturally; a large theorem change.gas()occurrences acrossall four fixtures is directly the first argument of
call/staticcall/delegatecall— no exceptions. Socall(gas(), …)couldbe compiled as a unit under a
GasInsensitivehypothesis on the externalrelation (the response must not depend on the forwarded amount). The catch:
ANF normalization rewrites this to
let g := gas() … call(g, …)before thebackend sees it, so the pattern would have to be recognized after the
optimizer, or protected through it.
My preference is (c) if the ANF interaction can be contained, else (b). I did
not want to pick unilaterally — it moves the audited specification surface.
Finding 3 — the three features are not sufficient anyway
Control experiment: I textually stubbed all three features out of each
fixture's extracted IR (
gas()→0,linkersymbol(…)→0,loadimmutable(…)→0,setimmutable(…)deleted) and fed the result toyulc. Three of the four still fail (exit=2— parsed, unsupported feature):All four. So there is at least one further blocker per fixture, and "after this
PR everything compiles" was not reachable from immutables + linking + gas
alone, whatever we decide about
gas().Bisecting by sub-object, on the stubbed sources:
LiquidationLogic→PreviewHub_9270(+ its child)LiquidationLogic→AGasTest_9431_deployedPoolManager→TestToken_9344(+ its child)PoolManager→PoolRouter_9288(+ its child)PoolManager→AGasTest_8990_deployedThe two fixtures fail for different reasons.
LiquidationLogic: a layout-key collision (root-caused, 12-line reproducer)Every sub-object compiles, yet the assembled tree is rejected. Narrowing:
replacing the 190 KB runtime with
code { stop() }still rejects, in 0.53 sinstead of 21 minutes; the top-level creation code alone compiles; a minimal
Wraparound the realPreviewHub_9270subtree reproduces it.compileResolvedObjectkeys every layout entry bylitValue (.string name),which holds only 32 bytes. At nesting depth ≥ 2
shiftChildEntriesbuildscompiler-internal qualified names, so the grandchild and its data segment
become
which share the 32-byte prefix
PreviewHub_9270.PreviewHub_9270_. Their keyscollide,
(plan.entries.map entryKey).Nodupfails, and the whole tree isrejected. Pinned by a three-way discrimination — it needs two overflowing
keys, not one:
This is the name-aliasing caveat yul-semantics documents for string-literal
keying, reached by compiler-generated names rather than source ones — and it
fires on essentially every real contract, since solc always emits
.metadataand its object names (
Contract_1234/Contract_1234_deployed) exceed 32 bytesonce qualified.
Fixed (
shiftChildEntries): a qualified name whose key is notrepresentable is dropped rather than allowed to poison the tree. Such a name is
unusable anyway —
litWF (.string s)requiress.toUTF8.size ≤ 32, so thevalidator rejects any
dataoffset/datasizenaming it — so this removes onlydead weight. A program that does reference the name still fails to resolve and
is rejected, exactly as before, rather than being miscompiled. Direct data
segments come from
dataEntriesand are untouched, soLayout.Consistent(which quantifies over an object's direct segments) is unaffected and the
object proofs went through unchanged;
Checks.leanstill reports exactlythe three standard axioms.
The reduced tree and the already-working short-name variant are pinned as
#guards. Confirming the diagnosis: keeping the real 190 KB runtime andstubbing only the
PreviewHubsubtree already compiled (15304 bytes, 1081 s),so for
LiquidationLogicthis key collision was the sole blocker — there isno stack-pressure problem in that fixture at all.
PoolManager: a genuinely rejecting objectAGasTest_8990_deployedrejects on its own. Not recursion (zero call-graphcycles), not
msizeorverbatim(zero occurrences), not a missingreservation (it carries solc's
memoryguard(128)). The visible difference fromthe objects that compile is live-local count —
PoolRouter_9288_deployedhas310
lets and compiles, this has 5657 and does not — which points at stackpressure guarded spilling does not fully resolve. Whether the layout-key bug
also contributes here is untested.
Immutables
Not implemented. The design I had worked out, for the record: a dedicated
Asm.pushImmutableplaceholder (fixedPUSH32width, opaque to the peepholeand to source-level constant folding, which a magic-literal encoding would
not be), offsets collected from the child object's compiled layout in
planAttempt, andsetimmutable(base, k, v)expanded into onemstoreperrecorded offset in the parent's code. That much is ordinary Yul and reuses the
existing proofs; the new obligation is relating the returned patched deployed
bytes to the compilation of the program with
loadimmutable(k)replaced by thestored value.
Immutables are independent of Finding 3 — nothing about stack pressure
prevents implementing them. The only consequence of Finding 3 is that finishing
them will not by itself flip any fixture from REJECT to OK, since those same
objects are also rejected for pressure. (The one real interaction runs the
other way and is second-order: expanding
setimmutableinto onemstoreperrecorded offset adds a few locals to the constructor object.)
Suggested next steps
gas()route (a/b/c above) — that unblocks the largest piece.precisely, so we know the true size of "everything compiles".
🤖 Generated with Claude Code