feat: --yul-backend option to compile Yul with libyulc - #1
Conversation
Adds an alternative code generator for stand-alone Yul mode:
solc --strict-assembly --yul-backend yulc input.yul
`yulc` is libyulc, the Yul -> EVM compiler from
https://github.com/powdr-labs/yul-compiler, which is written and proved
correct in Lean. `solc`, the existing code generator, stays the default,
so nothing changes unless the option is passed.
The backend takes a whole Yul program -- block- or object-rooted, with
child objects and data segments resolved during compilation -- and
returns finished creation bytecode, so `assembleYulWithYulc()` hands the
source over verbatim and prints the result. It has no intermediate
representation to expose, so only --bin is available, and it optimizes
according to its own proofs, so --optimize and friends are rejected
rather than silently ignored. Unsupported programs are an error, not a
fallback to the default backend.
Linking libyulc is opt-in via -DUSE_YULC=ON, off by default. The library
is a Lean build and is not compiled from source here; a pinned release is
downloaded, or YULC_ROOT can point at a local yul-compiler build. The
merged static archive is used so solc stays a single self-contained
binary.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The release workflow derives the tarball name from the tag by stripping only the "libyulc-" prefix, so the published asset is libyulc-v0.0.1-x86_64-linux.tar.gz, with the "v" retained. Build the name accordingly and pin the checksum of the published tarball. Verified by configuring with the download path (no YULC_ROOT): the tarball is fetched, the hash matches and libyulc.a is found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pinning verified against the published release
Tarball downloaded, SHA-256 matched, bytecode identical to the On the binary size
plus 23.7 MB Mathlib is present because Lean's It traces to one line — |
A -DUSE_YULC=ON build of solc is 339 MiB, of which only 227 MiB is loadable content. The other 113 MiB is .symtab/.strtab: libyulc is a Lean build and Lean's mangled names are far bigger than the code they name -- a million of them in this binary. -DSOLC_STRIP_SYMBOLS=ON adds -s to the solc link, which brings it down to 226 MiB. (A plain build goes from 20 MiB to 17 MiB, so this is mostly about libyulc.) Off by default, and deliberately not tied to the build type. -s discards DWARF along with the symbol table, and CMAKE_BUILD_TYPE defaults to RelWithDebInfo for a git checkout, so making this automatic would silently take debug info away from everyone building from source. Note also that --strip-debug is not an alternative here: in a Release build there is no DWARF to remove and it saves only 0.3 MB. A link-time flag rather than a post-build strip command, so the executable is only ever written once and the build stays incremental. target_link_options() rather than the LINK_FLAGS property the neighbouring options use, so it composes with them instead of overwriting them. Solidity does not symbolize anything at runtime -- no crash handler, no boost::stacktrace anywhere in the tree -- so nothing solc prints changes. What is lost is gdb/perf/addr2line being able to name functions, which the documentation says. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Added A
All To enable: Notes on the design:
Documented as a new "Stripping Symbols" section under "CMake Options" in |
Correction to the size analysis aboveI claimed the Mathlib bloat traced to a single bare Narrowing Sizing the real prize, by narrowing the dependency locally and recomputing the closure:
That is roughly 31.6 MB (19%) off The |
Repinned to libyulc 0.0.2 — solc is now 144 MiB stripped, down from 339 MiBThe Mathlib work in yul-compiler has landed (powdr-labs/yul-compiler#137, argotorg#138, powdr-labs/yul-semantics#40 — all merged), and
End to end, Worth noting the reduction is ~3x what extrapolating from the Two mechanical notes:
|
What
Adds a CLI option that routes stand-alone Yul compilation through libyulc, our Yul → EVM compiler written and proved correct in Lean, instead of solc's own code generator:
--yul-backend=solcremains the default, so nothing changes for anyone who does not pass the option.Why this shape
libyulc's C entry point (
yulc_compile, seeyulc.h) takes a complete Yul program — block- or object-rooted, with child objects and data segments resolved during compilation — and returns finished creation bytecode. That is exactly the contract of--strict-assembly, so the integration is a single alternative branch inprocessInput(); the source is handed over verbatim and the resulting bytes are printed. No changes tolibyul,libevmasmor the Solidity pipeline.Because the backend has no intermediate representation to expose and optimizes according to its own proofs, the parser narrows what is accepted rather than silently doing something else:
--binis available (no--asm,--asm-json, AST, CFG, ethdebug or source maps);--optimize,--optimize-runs,--optimize-yul,--no-optimize-yul,--yul-optimizations,--via-ssa-cfgand--librariesare rejected;Build system
-DUSE_YULC=ON(off by default) linkssolcagainst libyulc and definesSOLC_HAVE_YULC. Without it the option still parses but reports that the binary was built without support, so the CLI surface stays identical across builds and the parser tests are configuration-independent.libyulc is a Lean build and is not compiled from source here.
cmake/yulc.cmakedownloads a pinned release tarball, or uses-DYULC_ROOT=<dir>to point at a localyul-compiler/.lake/build/c. The merged static archive is linked in, sosolcstays a single self-contained binary with no runtime library search path to arrange — at the cost of size (see below). This is only supported on x86-64 Linux, and is incompatible with-DSOLC_LINK_STATIC=ON.Verification
Built and exercised both configurations locally against the real
libyulc-v0.0.2release:solcsize+ SOLC_STRIP_SYMBOLS=ON--yul-backend yulcUSE_YULC=OFF(default)USE_YULC=ON602a5f55isPUSH1 0x2a; PUSH0; SSTORE— the two locals folded intosstore(0, 42). An object-rooted input produces creation bytecode whose runtime length matches itsdatasize.CommandLineParserTest/yul_backend_option{,_invalid}unit tests pass, and the fullCommandLineParserTestsuite (22 cases) is green;test/cmdlineTestsfixtures cover the rejection paths; all pass. They are all parser-level, so they behave the same with or without libyulc linked in.Notes for review
import Mathlibto the modules actually used yul-semantics#40 are all merged;libyulc-v0.0.2is cut from yul-compilermainand includes the Mathlib link-closure reduction (see the sizing comment below).docs/yul.rstand a build section indocs/installing-solidity.rst.🤖 Generated with Claude Code