Repository navigation
perf: resolve native symbols inside lean.wasm instead of exporting all of Lean - #16149
Draft
basvandijk wants to merge 18 commits into
Draft
basvandijk wants to merge 18 commits into
basvandijk wants to merge 18 commits into
Conversation
Contributor
Author
|
How this was tested:
|
Contributor
Author
|
changelog-no |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t `lean.js` links Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Emscripten the shared libraries are dummies: `lean.js` links the static archives (`LEAN_EXE_LINKER_FLAGS`), and `src/CMakeLists.txt` only touches `libInit_shared.so`, `libleanshared_1.so`, `libleanshared_2.so` and `libleanshared.so`. The `lean` rule in `stdlib.make` still depended on them, so once per build, after `lib/temp/empty.c` had been created, `make` ran their recipes, that is `emcc -shared`, which Emscripten before 6.0 emulated with a static object file and a warning and which Emscripten 6 turns into a real side-module link of all of Lean. And because the dummies are touched on every build, `make install` relinked `lean.js`, nine of the Web Assembly job's sixty minutes. `lean.js` now depends on `libInit.a`, `libStd.a`, `libLean.a`, `libleancpp.a`, `libleanrt.a`, `libleanshell.a` and `libleanmain.a`, with the Lake `.trace` files of the first three like the other binaries, and the dummy `leanshared` target depends on `leanrt`, which nothing ordered before `lean.js` so far. The shared-library recipes are no longer reachable on Emscripten; the native rule is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Since Emscripten 6.0.6, `emcc` links libc++ and libc++abi only when run as `em++` or with `-sDEFAULT_TO_CXX=1`, and it has always ignored `-lstdc++` for compatibility with GNU toolchains. `lean.js` is linked by `leanc.sh`, that is by `emcc`, with `ERROR_ON_UNDEFINED_SYMBOLS=0` and `MAIN_MODULE=1`, so on Emscripten 6 the link succeeds and `lean.js` fails when Node.js starts it. On Emscripten the C++ standard library flag is therefore `-sDEFAULT_TO_CXX=1`; before 6.0.6 the setting is on by default, so this changes nothing there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`-sALLOW_MEMORY_GROWTH=1` takes effect only when linking, but `EMSCRIPTEN_SETTINGS` passed it to every compilation as well, through `LEAN_EXTRA_CXX_FLAGS` and libuv's `CMAKE_C_FLAGS`, which emcc reports 147 times per build as `linker setting ignored during compilation`. The flags for compiling and linking, `-fwasm-exceptions -pthread -flto`, are now `EMSCRIPTEN_FLAGS` and the settings `EMSCRIPTEN_LINK_SETTINGS`; both go into `LEAN_EXTRA_LINKER_FLAGS`, which `leanc.sh` adds to every link, so `LEAN_EXE_LINKER_FLAGS` does not repeat them. Settings use the `-sKEY=VALUE` spelling that Emscripten documents. `LINKABLE=1` is dropped: `MAIN_MODULE=1` implies it in every Emscripten version from 3.1.44 to 6.0.13, and Emscripten 4.0.15 deprecated it, so newer versions warn about setting it explicitly. The comment quoting Emscripten's old advice to pass all `-s` flags at compile time went with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Since Emscripten 4.0.11 the `libclang_rt.builtins` system library bundles LLVM's profiling runtime, and a main module (`-sMAIN_MODULE=1`) links its system libraries as whole archives, so the runtime's static initializer runs in every `lean.js` process and its `atexit` hook writes `default.profraw` into the current directory: 128 bytes after `lean --version`, 640 bytes after a test. That fails CI's check that the test suite leaves the source tree clean and would litter every user's working directory. `lean_main` now points the runtime at `/dev/null`. The symbol is weak, so Emscripten versions without the runtime, such as 3.1.44, link and behave as before. Reported upstream as emscripten-core/emscripten#28123. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Emscripten 3.1.44 is from July 2023; 6.0.13 is the current release. `setup-emsdk` installs it directly and keys its cache on the version. The previous commits cover the two changes in Emscripten 6 that affect this build: `-shared` builds real side modules, and `emcc` no longer links the C++ runtime by default. `lean.js` still runs on Node.js 26; emcc uses the Node.js bundled with the SDK for its own passes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The page described the Lean 3 build (`emconfigure`, the `lean_js_js` and `lean_js_wasm` targets, lean-web-editor) and was linked from nowhere. It now describes the build as the Web Assembly CI job runs it: a native 32-bit stage 0 compiles the `.olean` files, Emscripten cross-compiles stage 1 into `lean.js` and `lean.wasm`, Node.js 24 or newer runs them through the `bin/lean` launcher, and the tests run one at a time. `ci.yml` and `build-template.yml` remain the authoritative source of the flags and the SDK version. `doc/make/index.md` links the page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The interpreter switches to native code by looking up a declaration's mangled symbol at run time. In `lean.js` that lookup went through Emscripten's `dlsym`, which enumerates every export of the module on each call (`Object.keys(lib.exports).indexOf(symbol)` in `_dlsym_js`, Emscripten 6.0.13); with about 250,000 exports one lookup costs 65 to 130 ms, and `lean` performs one per imported module and two per imported parser constant at startup, 2,630 of them for `Init`, which is most of the time a test takes on the Web Assembly CI job. `lean.js` now carries its own symbol table. `script/wasm_symbols.py` reads the global symbols of the Lean archives that `lean.js` links (`libInit.a`, `libStd.a`, `libLean.a`) from their wasm object files, which also give each function's type, and writes a sorted name/address table with a binary-search lookup, `lean_lookup_native_symbol`, which `lookup_symbol_in_cur_exe` consults on Emscripten; since the table holds every symbol of those archives, only the runtime's symbols are still looked up with `dlsym`. `stdlib.make` builds the table and links it into `lean.js`. It is about 28 MB of static data (the names alone are 25 MB), so the link's initial memory is raised from Emscripten's default 16 MB to 64 MB. The script also writes the list of the runtime archives' global symbols, which the next commit uses as the export list. On an AMD EPYC 7302 with Emscripten 6.0.13 and Node.js 26, `lean` on an empty file takes 21 s instead of 236 s (210 s of which were the 2,630 `dlsym` calls; now there are none), `tests/elab/Format.lean` 27 s instead of 193 s, and the six tests of the CI job 2 m 42 s instead of about 20 m. Lean's own profiler reports import 2.1 s instead of 57 s and interpretation 10 ms instead of 161 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With the interpreter's symbol table inside the module, `lean.wasm` no longer has to export all of Lean for `dlsym`. `lean.js` is now linked with `-sMAIN_MODULE=2` and the export list that `script/wasm_symbols.py` writes, the global symbols of the C and C++ runtime archives (600 names instead of 247,834 exports), so Emscripten removes unused code and the JavaScript glue no longer declares a variable per export, which was 76% of the 113 MB `lean.js`. Every pthread is a Node.js worker that loads that file, so each thread now starts in 0.5 s instead of 5 s and a `lean` process needs a third of the memory. Such a module includes only the JavaScript library functions the linked code references, so the helpers that Lean's `EM_ASM` blocks use are requested with `EXPORTED_RUNTIME_METHODS`. The Node.js heap override for emcc's JavaScript passes and the two `TODO(WN)` items in `src/CMakeLists.txt` about dead-code elimination and unused system libraries are obsolete and removed, and the comment on the CI job's Node.js step no longer cites the export count. On an AMD EPYC 7302 with Emscripten 6.0.13 and Node.js 26: `lean.js` is 201 KB instead of 113 MB, `lean.wasm` 131 MB instead of 163 MB with the table (135 MB before it), `lean` on an empty file takes 5.0 s instead of 21 s with the table alone (236 s before), `tests/elab/Format.lean` 6.3 s and 1.5 GB instead of 27 s and 5.5 GB (193 s and 5.3 GB before), and the six tests of the CI job 42 s instead of 2 m 42 s (about 20 m before). The eight `uv__*` symbols the link reported undefined disappear with the unused libuv code; the five `lean_uv_*` ones and `dl_iterate_phdr` remain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace the sentences in `doc/make/emscripten.md` about `lean.wasm` exporting about 250,000 symbols, the Node.js version that requires, `-sMAIN_MODULE=1 -sEXPORT_ALL=1` and the Node.js heap limit of the link with how the interpreter finds compiled code in `lean.js`: the table that `script/wasm_symbols.py` generates from the Lean archives and the export list of the runtime archives. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
basvandijk
force-pushed
the
wasm-symbols
branch
from
November 2, 2026 11:41
1062a54 to
cf685f6
Compare
basvandijk
commented
Nov 2, 2026
This branch has not been deployed
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.
This PR makes
lean.js, the WebAssembly build oflean, start about 50 times faster (5 s instead of 236 s on an empty file on a workstation), shrinks its JavaScript file from 113 MB to 201 KB and cuts the memory of aleanprocess by about two thirds.Background
The interpreter switches to native code by looking up a declaration's mangled symbol at run time, which is why
lean.wasmexported every symbol of Lean (247,834 of them) and whylean.jswas linked with-sMAIN_MODULE=1 -sEXPORT_ALL=1. Emscripten'sdlsymenumerates all exports of the module on every call (Object.keys(lib.exports).indexOf(symbol), emscripten-core/emscripten#28124), about 75 ms per call with that many exports, andleanperforms one lookup per imported module and two per imported parser constant at startup, 2,630 forInitalone; that was most of the time a test took on the Web Assembly CI job.Changes
script/wasm_symbols.pynow reads the global symbols of the Lean archives thatlean.jslinks (libInit.a,libStd.a,libLean.a) from their wasm object files, together with each function's type, and writes a sorted name/address table with a binary-search lookup,lean_lookup_native_symbol.lookup_symbol_in_cur_execonsults that new function on Emscripten instead ofdlsym;only the symbols of the C and C++ runtime archives, which the script writes as the export list for the link (600 names), are still resolved through
dlsym.lean.jsis linked with-sMAIN_MODULE=2, so unused code is removed and the JavaScript glue no longer declares a variable per export, which was three quarters of the 113 MB file;doc/make/emscripten.mddescribes the symbol table and the export list.Results
tests/elab/Format.leanpeaks at 1.5 GB instead of 5.3 GB.This PR is stacked on #16142 and carries its commits until that PR merges; it will be rebased onto master afterwards. Until then, the diff of this PR alone is basvandijk:wasm-emsdk-6...basvandijk:wasm-symbols.
Claude Code, an AI coding assistant, helped with the investigation, the changes, the testing and this description. I reviewed and checked them.
🤖 Generated with Claude Code