Point yul-semantics at the Mathlib-narrowed rev; 29 MB off yulc - #138
Conversation
A Lean module's `initialize_M` calls the initializer of every module `M`
imports. That is a real symbol reference, so a bare `import Mathlib` anywhere in
the runtime closure links all of compiled Mathlib into `yulc` and `libyulc`
(and hence into any solc statically linked against `libyulc`).
`canon-narrow-mathlib-import` removed this repo's only bare `import Mathlib`
and measured no effect, because the pinned `yul-semantics` had two of its own —
`YulSemantics/Dialect/EVM.lean` (which most of `YulEvmCompiler` imports) and
`YulSemantics/Basic.lean`. Both are now narrowed upstream; this commit points
`rev` at that work and fixes the downstream cascade.
Measured on `.lake/build/bin/yulc`, ordinary runnable link, no
`--unresolved-symbols` games:
before 165,277,392 bytes 8,724 objects in yulc.rsp
after 134,841,520 bytes 1,112 objects in yulc.rsp
delta -30,435,872 bytes (-29.0 MiB, -18.4%)
The `.c.o.export` input to the link is 19.7 MB after. The resulting binary was
run: it compiles a small Yul object to the same bytecode as the baseline binary.
## Downstream cascade
Modules that were getting Mathlib transitively through `YulSemantics.Dialect.EVM`
now import what they use:
* `YulEvmCompiler/Value.lean` — `Mathlib.Data.BitVec` (`BitVec.toNat_injective`),
`Mathlib.Algebra.Divisibility.Basic` (`Dvd.dvd.mul_left`),
`Mathlib.Algebra.Order.GroupWithZero.Basic` (`pow_lt_pow_right₀`), and the
`ring` / `positivity` / `norm_num` tactics.
* `YulEvmCompiler/Optimizer/Implementation/Simplify.lean` — `Mathlib.Data.BitVec`,
`Batteries.Data.BitVec.Lemmas` (`BitVec.toNat_pow`, `BitVec.ofNat_pow`), and the
monoid `pow` lemmas.
* `YulEvmCompiler/Decode.lean` — `fin_cases`, `interval_cases`.
* `YulEvmCompiler/Asm.lean` — `norm_num` (`.Basic` + `.Ineq`).
* `YulEvmCompiler/ObjectResolve.lean` — `by_contra`, `push`, and
`Mathlib.Data.Nat.SuccPred` for `simp_wf`, exactly as `YulParser/Canon.lean`
needed on the parent commit.
* `YulIR/FrameCongr.lean` — `Batteries.Data.List.Basic` (`List.Forall₂`); no
Mathlib needed.
## Explicit dialect annotations (72 sites, no proof content changed)
`Step`, `EquivExpr`, `VEnv.set_keys` and friends take `[DecidableEq D.Value]`,
and these terms are elaborated before the expected type fixes `D`. With Mathlib
in scope, `DecidableEq (Dialect.Value ?D)` was *inconclusive* rather than
failing — `LinearOrder.toDecidableEq` and the long chain below it leave the
search stuck on metavariables — so Lean postponed it until `?D` was known.
Without Mathlib there are no candidate instances at all, so it is a hard
failure. The fix is to name the dialect: `(«D» := D)`. The guillemets are needed
because these files declare `local notation "D" => evmWithExternal calls creates`,
which makes bare `D` a token rather than an identifier, so the parameter name
has to be escaped. The proof terms are otherwise untouched.
## Tactic-syntax changes, called out
Six `decreasing_by` blocks changed. `all_goals simp_wf <;> omega` became
`all_goals simp_wf; omega` in five places (`ObjectResolve`, `InlineHelpers` x3,
and the Batteries `unnecessarySeqFocus` linter's own recommendation — it fires
because `simp_wf` now leaves one goal per invocation instead of several), and
`DeadPureResolve`'s bare `all_goals simp_wf` gained `; omega` because `simp_wf`
alone no longer closes `2 * sizeOf body ≤ 2 * (1 + sizeOf body)`.
## Audited spec surface: one hash moved, deliberately
`SpecClosure.lean`'s pin for `YulEvmCompiler.opTable` changed from
`e1b0c299397baebd` to `58d5b129f84835ed`, and `SPEC.md` is regenerated. The
*value is identical*; only the elaboration of the five `Fin 5` literals in the
LOG rows changed. With all of Mathlib in scope they were going through
`SimplexCategory.instOfNatToTypeOrderHomFinHAddNatLenOfNat` — the `OfNat`
instance on the simplicial-category object `[4]`, which happens to be defeq to
`Fin 5`. They now use core's `Fin.instOfNat`. Verified by diffing the fully
elaborated `opTable` term across the two trees (that instance is the only
difference) and by evaluating the table on `log0..log4` in both: both give
`Operation.Log { topics := 0..4 }`. The other 88 audited hashes and the
155-declaration external boundary hash are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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. Soundness (formal guarantee)
5. Verdict✅ All good |
powdr-labs/yul-semantics#40 is merged. Move the pin off the unmerged branch commit and onto 4c6f9753 on that repository's main. The two commits have the identical tree (6f559621), so this changes the recorded SHA and nothing else; the build and corpus results measured against the branch commit carry over unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Removes Mathlib from the
yulc/libyulclink closure. A Lean module'sinitialize_Mcalls the initializer of every moduleMimports, which is a real symbol reference the linker cannot discard, so one bareimport Mathlibanywhere in the runtime closure links all of compiled Mathlib into the binary.PR #137 (this PR's base) removed this repo's only bare
import Mathliband measured no effect, because the pinnedyul-semanticshad two of its own. Those are narrowed in powdr-labs/yul-semantics#40; this PR pointsrevat that work and fixes the downstream cascade. The two changes are mutually blocking: neither one alone does anything.Measured on
.lake/build/bin/yulc, an ordinary runnable link (no--unresolved-symbols=ignore-all):yulc.rspcanon-narrow-mathlib-import)The
.c.o.exportinput to the link is 19.7 MB after. The resulting binary was run and compiles a small Yul object to byte-identical output vs the baseline binary.Changes
lakefile.toml+lake-manifest.json:yul-semanticsrev81b9e3e->4c6f975. Nothing else inlakefile.tomlis touched.YulSemantics.Dialect.EVM(Value.lean,Simplify.lean,Decode.lean,Asm.lean,ObjectResolve.lean,YulIR/FrameCongr.lean).FrameCongrneeded only Batteries.(«D» := D)dialect annotations across the optimizer proofs.Step,EquivExpr,VEnv.set_keysand friends take[DecidableEq D.Value], and these terms elaborate before the expected type fixesD. With Mathlib in scope,DecidableEq (Dialect.Value ?D)was inconclusive rather than failing (LinearOrder.toDecidableEqand the chain below it leave the search stuck on metavariables), so Lean postponed it until?Dwas known. Without Mathlib there are no candidate instances at all, so it is a hard failure. The guillemets are required because these files declarelocal notation "D" => evmWithExternal calls creates, which makes bareDa token rather than an identifier. No proof term content changed.Two things a reviewer should look at closely
1. Six
decreasing_byblocks changed.all_goals simp_wf <;> omega->all_goals simp_wf; omegain five places, which is literally what the BatteriesunnecessarySeqFocuslinter demands (it fires becausesimp_wfnow leaves one goal per invocation rather than several).DeadPureResolve's bareall_goals simp_wfgained; omega, becausesimp_wfalone no longer closes2 * sizeOf body ≤ 2 * (1 + sizeOf body).2. One audited spec hash moved.
SpecClosure.lean's pin forYulEvmCompiler.opTablechanged frome1b0c299397baebdto58d5b129f84835ed, andSPEC.mdis regenerated. The value is identical; only the elaboration of the fiveFin 5literals in the LOG rows changed. With all of Mathlib in scope they were resolving throughSimplexCategory.instOfNatToTypeOrderHomFinHAddNatLenOfNat, theOfNatinstance on the simplicial-category object[4], which happens to be defeq toFin 5. They now use core'sFin.instOfNat. Verified by diffing the fully elaboratedopTableterm between the two trees (that instance is the only difference anywhere in the term) and by evaluating the table onlog0..log4in both trees: both giveOperation.Log { topics := 0..4 }. The other 88 audited hashes and the 155-declaration external boundary hash are unchanged. The rest of theSPEC.mddiff is line numbers shifted by the added imports.Testing
Everything CI's
buildjob runs, all against Solidity develop510b2a1:lake build(all default targets): passlake build yulc: pass; binary runslake env lean Checks.lean(axiom footprint): passlake env lean SpecClosure.lean: pass after theopTablere-pin above;SPEC.mdcommitted and up to datelake env lean YulIR/CheckBaseline.lean:YulIR OK: 59 programs319 tests: 106 expected successes, 213 expected failures, 0 known parser mismatches53 tests: 28 passed, 25 failed (25 known); skipped 1643 tests: 593 compiled, 50 failed (50 known); skipped 836 tests: 30 compiled, 6 failed (6 known); skipped 047 tests: 43 compiled, 4 failed (4 known); skipped 0scripts/YulIRCorpus.lean check:gated 43 categories, 0 drifted/new, 0 regressionsNot run: the
gas-runner/solidity-gas/solc-differentiallegs (they need the pinned solc and hours of corpus execution). Nothing here changes emitted bytecode, and the compile corpora above cover the compile path.Notes
Both blockers are cleared:
import Mathlibto the modules actually used yul-semantics#40 is merged.revnow points at4c6f9753cc0e5850f6bdaf6fb0df76457204699aon that repository'smain. That merge commit has the identical tree (6f559621) to the branch commit everything above was measured and tested against, so every result in this PR carries over unchanged -- the repoint moved a SHA, not a source byte.import Mathlibto the two modules it needs #137 is merged, so this PR is now based onmain.🤖 Generated with Claude Code