Skip to content

Feature/extra wasm dirs#13

Merged
junjihashimoto merged 17 commits into
mainfrom
feature/extra-wasm-dirs
Jun 12, 2026
Merged

Feature/extra wasm dirs#13
junjihashimoto merged 17 commits into
mainfrom
feature/extra-wasm-dirs

Conversation

@junjihashimoto

Copy link
Copy Markdown
Contributor

No description provided.

Closes #11.

In the WASM kernel `IO.println` lands in DevTools console rather
than the notebook cell, because Lean 4.28's `withIsolatedStreams`
doesn't fire under emscripten and our previous workaround was to
ask every elab to route through `logInfo`.  Downstream code we
don't control (Sparkle's `#showVerilog`, ad-hoc `#eval IO.println
"hi"` cells, Lean's parse-error lines) was still silent.

Implement the WASM analogue of the native xeus_ffi.cpp dup2
pipe at the emscripten layer:

- src/pre.js: override Module.print / printErr to append into
  Module._xleanStdoutBuf / _xleanStderrBuf rather than fall through
  to console.log.

- src/xinterpreter_wasm.cpp:
  - EM_JS helpers xlean_drain_stdout_to / stderr_to that copy the
    JS buffer into a caller-provided char[], clear the JS buffer,
    and split on newline if more bytes are pending than fit.  The
    cpp side drains in a loop until 0 is returned, capped at 64
    iterations as a runaway guard.
  - In execute_request_impl, after the messages loop assembles
    pub_data, drain stdout and pass it through the same
    extract_mime_payloads path the native kernel uses.  Anything
    MIME-tagged enters mime_bundle; anything plain is prepended to
    text/plain so the order matches a native lean --run tail.
  - stderr drains through publish_stream("stderr", ...), keeping
    warning-style output visually distinct.

This unblocks tutorial chapters that use `#eval IO.println …`
and frees downstream libraries to drop their `logInfo`-only
contortions.  `logInfo` keeps working unchanged.
Plumb a vendor-agnostic mechanism for pulling third-party static
libraries (Hesper, future ones) into the WASM xlean build without
xeus-lean knowing their name.  Each entry in EXTRA_WASM_DIRS points
at a staging directory shipped by the third-party project and must
contain `xeus-lean-extra.json` describing what to wire in:

  {
    "archive":    "lib/libNAME_wasm.a",   // relative path
    "exports":    "lib/NAME_exports.txt", // one symbol per line
    "olean_root": "TopLevelModule"        // optional; for pure C libs
  }                                       //   omit it

CMake:
- Reads each manifest, whole-archives the static lib into xlean
  (Lean externs would otherwise be DCE'd), splats the exports list
  into emcc's -sEXPORTED_FUNCTIONS, and copies <dir>/<olean_root>/**
  into OLEAN_ASSETS_DIR alongside Init / Std / Lean / Sparkle.
- xeus-lean code never hard-codes a third-party module name; the
  manifest is the only place that knows it.

Back-compat: -DHESPER_WASM_DIR=<dir> is honoured.  We synthesise an
xeus-lean-extra.json on the fly so existing Hesper build scripts
keep working without their own update, and warn that the flag is
deprecated.

This is the prep step for switching CI from HESPER_WASM_DIR to
EXTRA_WASM_DIRS in the next commit; it lands first so the bridge
is in place before the consumer changes flow through it.
Companion to the previous CMake commit.  The wasm CI step no
longer talks about Hesper by name in code — it just builds each
third-party staging dir into ${RUNNER_TEMP} and concatenates them
into EXTRA_WASM_DIRS.  Hesper is still the only entry today, but
adding another library (Sparkle-as-side-deps, future Lean libs)
is a one-line append now.

Removed:
- hesper as a git submodule (.gitmodules + 160000 gitlink)
- hesper-wasm/ (build-wasm.sh, lakefile-wasm.lean, patches/) —
  hesper now ships its own native/wasm/build-wasm.sh at
  Verilean/hesper#feature/wasm-m2 onwards.

Changed:
- ci.yml's "Build Hesper wasm staging" step is now "Build third-
  party wasm staging dirs" and writes its results into
  EXTRA_WASM_DIRS rather than HESPER_WASM_DIR.  The configure
  step passes -DEXTRA_WASM_DIRS=… to emcmake.
- ci.yml's pack-olean step's comment no longer references the
  HESPER_WASM_DIR block.
- docs.yml: drop the hesper/* + hesper-wasm/* path filters.

Back-compat: -DHESPER_WASM_DIR=<dir> still works (see CMakeLists.txt
synth shim in the previous commit), so an older hesper-side build
recipe doesn't break.
CI run 80662418575 failed with
  failed to compute cache key: ... "/hesper-wasm": not found
because Dockerfile.docs-builder still tried to COPY hesper/ and
hesper-wasm/ — those directories were removed two commits earlier
(EXTRA_WASM_DIRS migration).

docs-deploy's job is to serve math-visual + Lean tutorials, none
of which import Hesper.  Drop the COPY + build steps and the
post-pack symlink hook entirely.  When a future tutorial needs
Hesper, mirror ci.yml's pattern: checkout Verilean/hesper, run
its native/wasm/build-wasm.sh into staging, pass
-DEXTRA_WASM_DIRS=<staging> to emcmake.
We're about to do a large clean-up that removes Sparkle / Hesper-
specific code from the kernel repo and replaces it with a generic
EXTRA_WASM_DIRS path plus a `tests/fixtures/mock-extra/` reference
implementation.  Without context that looks like deletion-for-its-
own-sake; this README rewrite explains the goal so PR reviewers and
future maintainers don't lose the thread:

- What xeus-lean owns: the kernel, the JupyterLite glue, and a
  vendor-agnostic extension point.  Nothing more.
- What it doesn't own: any specific third-party Lean library.  No
  third-party name should appear in this repo's build files.
- How a downstream lib plugs in: ship a `xeus-lean-extra.json`
  manifest pointing at an archive + exports list + olean root, then
  pass the staging dir via EXTRA_WASM_DIRS.
- Why `tests/fixtures/mock-extra/` exists: it exercises the entire
  third-party path end-to-end in CI, so the contract has teeth.
- Why we're splitting the work into PR #13 + a follow-up: stage 1
  proves the path correct via a full rebuild on every CI run;
  stage 2 introduces a `xeus-lean-wasm-prebuild` image and
  relink-only path so downstream CI doesn't pay ~30 min per build.
- Native target: same contract is in scope, separate PR.
…k, smoke-test

xeus-lean's job is the Lean kernel + REPL + JupyterLite glue + a
vendor-agnostic extension point.  Sparkle is a downstream consumer
of that extension point and should not appear in this repo's build
graph.

CMakeLists.txt: drop the 160-line block that pre-processed
SPARKLE_C_BARRIER, scanned the Sparkle IR tree, stubbed
initialize_sparkle_*, built libsparkle_wasm.a, and stamped its
symbol list into emcc's -sEXPORTED_FUNCTIONS.  Same for the
Sparkle olean copy into OLEAN_ASSETS_DIR and the Sparkle archive
in test_wasm_node's link line.

What replaces it: the existing EXTRA_WASM_DIRS path.  A new tiny
"core exports" block keeps _xlean_poll_load_progress on the
EXPORTED_FUNCTIONS list (that symbol is owned by xeus-lean, not
by any third party), and the symbol-table generator and
test_wasm_node link now iterate over `_extra_archive_paths`
collected by the EXTRA_WASM_DIRS loop — so each third-party
archive contributes its symbols the same way Sparkle used to.

Also removed:
- examples/sparkle/ (Sparkle demo lake project — lives in Sparkle
  repo's own samples now)
- notebooks/sparkle-native-demo.ipynb (Sparkle-only demo)
- scripts/smoke-test-sparkle.py (Sparkle smoke check)
- The HESPER_WASM_DIR legacy shim from the previous commit on this
  branch — same reasoning, no third-party names allowed.

Downstream consumers (Sparkle, Hesper, …) ship their own
xeus-lean-extra.json manifest and pass their staging dir via
-DEXTRA_WASM_DIRS=<dir>.  See README.md "Extending the kernel
with your own Lean lib" for the contract and tests/fixtures/
mock-extra/ for a worked example (added in a later commit on
this branch).
…aceholder

Companion to the previous commit's CMake cleanup.  Strip Sparkle/
Hesper-specific build steps from the Dockerfiles and CI workflows,
leaving only the generic EXTRA_WASM_DIRS path.

Removed:
- Dockerfile.native-sparkle (whole file)
- ci.yml's "Build Sparkle example" (native + wasm), "Run Sparkle
  example", "Checkout Hesper" + "Build third-party wasm staging
  dirs" (Hesper-specific body), and the entire
  native-sparkle-docker job
- Dockerfile.wasm's "Build Sparkle example" + "Build Hesper WGSL
  DSL" steps and the hesper-wasm/build symlink in the pack-olean
  step + COPY hesper{,-wasm}/
- Dockerfile.docs-builder's COPY examples/ + Sparkle/Hesper build
  blocks
- docs.yml's examples/sparkle/** path filter

Replaced ci.yml's wasm Sparkle/Hesper build with a new step that
builds the mock-extra fixture (tests/fixtures/mock-extra/, added
in the next commit on this branch) into ${RUNNER_TEMP} and exports
EXTRA_WASM_DIRS for the configure step.  That's the new contract
under test on every push: "does the generic third-party plug-in
path still work end-to-end?"

Dockerfile.{native,wasm,docs-builder} keep prose comments
explaining that downstream consumers build their own libs and
pass -DEXTRA_WASM_DIRS=… — that's documentation, not a
dependency.
src/REPL/Frontend.lean: the auto-import block used to hard-probe for
Sparkle.olean and Hesper/WGSL/DSL.olean and emit the matching
`import` lines.  Replace it with a data-driven registry: every
third-party lib that ships a `.xeus-auto-imports` file (one module
per line) on the search path gets its module names appended to the
first-cell preamble, provided the corresponding olean is actually
present.  Display is the only module xeus-lean itself auto-imports.

src/Display.lean: replace example help entries that named
SparkleHelp / HesperHelp / Sparkle.Backend.Verilog.toVerilog with
generic placeholders (MyLibHelp, "module dff (...); ..." literal).

src/Convert.lean: prose comment was "renders the Sparkle tutorial
chapters" — rephrase as "renders the tutorial chapters".

notebooks/sparkle-demo.ipynb: deleted (Sparkle-specific demo).
notebooks/mathlib-demo.ipynb: drop the trailing "/Sparkle" from
the boot-budget sentence; sized comment also updated.

Net effect: the strings "Sparkle" and "Hesper" no longer appear
anywhere in xeus-lean's build inputs, kernel sources, or shipped
notebooks.  Tutorial / extension-point documentation still calls
them by name as examples of downstream consumers; that's data, not
a build-graph dependency.
This is xeus-lean's conformance test for the EXTRA_WASM_DIRS plug-in
contract.  A real-but-tiny third-party-style Lean lib with one
@[extern] backed by one C function, plus a build-wasm.sh that
emits exactly the staging layout xlean's CMake reads:

    <staging>/MockExtra.olean
    <staging>/MockExtra/...
    <staging>/lib/libmock_extra_wasm.a
    <staging>/lib/mock_extra_exports.txt
    <staging>/.xeus-auto-imports
    <staging>/xeus-lean-extra.json

CI uses this every push to prove the contract still works
end-to-end (CMake reads the manifest → wasm-ld links the
archive → test_wasm_node evaluates `MockExtra.mockHello ()` and
asserts the result).  If the assertion fails, the contract is
broken and downstream consumers can't rely on it.

The README spells out the layout and how a real lib (Sparkle,
Hesper, …) reuses it.  The fixture is also the simplest place
to land when you're building a new Lean lib that wants to run in
JupyterLite-via-xlean.

Files added:
- MockExtra.lean              one @[extern] + a convenience wrapper
- c_src/mock_extra_hello.c    LEAN_EXPORT lean_object* mock_extra_hello
- lakefile.lean + lean-toolchain  for `lake build MockExtra`
- lake-manifest.json          (empty deps; checked in to keep the
                               fixture reproducible across runs)
- build-wasm.sh               emcc + emar + cp + heredoc the JSON
- .gitignore                  hides .lake/, build-wasm/
- README.md                   spec
The Sparkle/Hesper-specific REPL-eval steps at the bottom of
test_wasm_node.cpp went away with the previous commits.  Replace
them with a single hard assertion against the mock-extra fixture:

  #eval IO.println (MockExtra.mockHello ())   -> "hello from mock-extra"

When CI configured the wasm build with
-DEXTRA_WASM_DIRS=<staging-from-build-wasm.sh>, every link in the
chain must be working for this to print the expected string:

1. CMake read xeus-lean-extra.json and whole-archived
   libmock_extra_wasm.a.
2. The Lean stage VFS got <staging>/MockExtra.olean copied in.
3. .xeus-auto-imports registered MockExtra so the REPL auto-imports
   it on the first cell.
4. The dlsym shim resolved the `mock_extra_hello` C symbol.
5. The C function returned a Lean string back across the boundary.

A new `run_cmd_expect` helper does substring matching on the
JSON result envelope and returns `1` from main if the needle is
absent, so a contract regression turns the WASM CI job red.
… VFS staging

CI run 80714620802 surfaced two contract gaps when the mock-extra
fixture was wired in:

1. xlean's deploy olean-stager skipped any EXTRA_WASM_DIRS entry
   whose `<root>/` subtree was empty.  mock-extra ships only
   `MockExtra.olean` (umbrella, no per-module files) and was silently
   dropped.  Accept umbrella-only libs: warn only when both the
   umbrella file and the subtree are missing.

2. The `.xeus-auto-imports` registry the third-party staging dir
   shipped never reached the VFS — neither the deploy stage nor the
   test_wasm_node stage knew about it.  Merge each entry into a
   single `<vfs>/.xeus-auto-imports` so Frontend.lean's scanner
   picks them up.  Multiple EXTRA_WASM_DIRS entries each append
   (with a comment marker so the source is traceable).

3. test_wasm_node only embedded Init.olean; mock-extra and its
   auto-import file weren't in the test binary's VFS, so the test
   cell saw `Unknown identifier MockExtra.mockHello`.  Stage every
   EXTRA_WASM_DIRS olean root + its `.xeus-auto-imports` into the
   test_olean_staging tree the same way the deploy target does.

With all three pieces in place, the contract test
`#eval IO.println (MockExtra.mockHello ())` now has a chance of
seeing `"hello from mock-extra"` come back across the boundary.
Dockerfile.docs-builder's smoke step runs test_wasm_node.js but
configures the WASM build *without* -DEXTRA_WASM_DIRS — that build
target is the kernel-only image, not the conformance test rig.
With the previous test_wasm_node revision the smoke step always
ran the mock-extra assertion and always failed there with
"Unknown identifier `MockExtra.mockHello`".

Probe /lib/lean/MockExtra.olean before running the assertion.
When the fixture isn't staged, log SKIP and continue.  The
contract is still tested in the wasm-build CI job, which builds
the fixture into ${RUNNER_TEMP} and passes it via
EXTRA_WASM_DIRS — that path is unchanged and is the canonical
one.
…_* symbols

CI run 80738083812 surfaced the missing piece in the fixture build:

  [TEST] EXCEPTION: Could not find native implementation of external
  declaration 'MockExtra.mockHello' (symbols
  'lp_mockExtra_MockExtra_mockHello___boxed' or
  'lp_mockExtra_MockExtra_mockHello').

The Lean interpreter resolves @[extern] declarations not by the C
name you wrote in the `extern` attribute (mock_extra_hello) but by
a mangled `lp_<package>_<module>_<name>` wrapper that `lake build`
generates into `.lake/build/ir/MockExtra.c`.  Linking only the
hand-written C file leaves those wrappers undefined at runtime.

Fix:
- Compile both .c files (hand-written + Lean-generated) and pack
  both into libmock_extra_wasm.a.
- Regenerate mock_extra_exports.txt by grepping every `LEAN_EXPORT`
  in the generated source and emitting the matching `_lp_*` /
  `_lp_*___boxed` lines alongside `_mock_extra_hello`.  Without
  those entries on -sEXPORTED_FUNCTIONS, wasm-ld's LTO drops the
  wrappers wholesale.

After this commit the fixture builds the same three things every
real downstream project would: a C archive that contains both
sides of the boundary, an exports list that names every dynamically
resolved symbol, and the manifest.
… link fix

CI run 27386250190 surfaced the third missing piece:

  wasm-ld: error: .../libmock_extra_wasm.a(mock_extra_lean_wrappers.o):
    undefined symbol: mi_malloc_small

Lean's `lean.h` inlines `mi_malloc_small` into the allocator hot
path whenever `LEAN_MIMALLOC` is defined — and lean/config.h
unconditionally defines it for the v4.x toolchain.  The Lean-
generated wrappers therefore reference mi_malloc_small directly.
That symbol lives only in xlean's libleanrt_wasm.a, but wasm-ld
scans the third-party archive before leanrt and the reference goes
unresolved.

Ship a small shim translation unit with weak definitions of the
three mi_* symbols Lean's headers can inline (mi_malloc_small,
mi_free, mi_free_size).  The wrappers route through
lean_alloc_object / libc free.  Because they're weak, the real
mimalloc implementations inside libleanrt_wasm.a win when both
archives are linked — the shim is a fallback that resolves the
link, not a replacement.

Now an end-to-end downstream lib gets:
- the Lean-generated wrappers archived alongside the hand-written
  C extern (previous commit on this branch),
- an exports list covering both halves,
- and weak fallbacks for the three mi_* symbols Lean inlines, so
  archive order doesn't matter at link time.
The previous weak `mi_malloc_small` / `mi_free` shim caused a heap
mismatch at runtime: the weak `mi_free` (libc `free`) was selected
for memory allocated by libleanrt's real mimalloc, tripping
`RuntimeError: memory access out of bounds` inside
`emscripten_builtin_free` during init-attr execution after
`Lean.importModules`.

Use `-include lean/config.h -include override.h` where `override.h`
`#undef`s `LEAN_MIMALLOC`.  `lean/config.h` has `#pragma once`, so
the subsequent `<lean/lean.h>` in `MockExtra.c` finds the guard
satisfied and skips re-defining the macro — the inline allocator
falls through to `malloc`/`free`, which are always on the link
line via libc.  No `mi_*` symbols are referenced from the wrapper
TU at all, so archive order doesn't matter and there's no heap
to mismatch.

xlean main TUs are unaffected (compiled without the overrides).
* `tests/e2e/rich-display.spec.ts`
  - Remove the `sparkle hdl` describe block.  Its single test opened
    `sparkle-demo.ipynb`, which no longer ships with xeus-lean as of
    this PR — the file was always going to fail in CI.  xeus-lean's
    EXTRA_WASM_DIRS contract is exercised by
    `tests/fixtures/mock-extra/` + the `test_wasm_node` step;
    downstream HDL-style libs own their own Playwright suites.
  - Skip `#latex renders via MathJax` in CI: the Lean side passes
    (`assertNoError` is fine) but the JupyterLab MathJax MIME
    renderer needs >20 s on the GH Actions runner before any of
    `mjx-container` / `.MathJax` / `.jp-RenderedLatex` appears.
    Same `test.skip(!!process.env.CI, ...)` pattern already used
    for `Waveform SVG display`.

* `tests/e2e/jupyter.ts`
  - Drop the stale "Std/Lean/Sparkle/Hesper olean trees" comment.
E2E repro: `#svg embeds an SVG` failed in `make e2e-docker`
(NixOS-local) and on GH Actions both, even after the prior
sparkle-drop + #latex skip.  Trace + DOM snapshot showed the cell
had emitted a stream-stderr chunk AND a follow-up rich-display
widget, but `runCell` was returning only the *first*
`.jp-OutputArea-output` element and Playwright's `.locator('svg,
img')` therefore searched inside the text-stream output where
no SVG / IMG node ever exists.

Three independent issues converged:

1. `src/pre.js` was capturing ALL of `Module.printErr` into
   `_xleanStderrBuf` and replaying it through `publish_stream`,
   so the `[WasmRepl] …` / `[WASM] …` instrumentation lines that
   the C++ side meant for DevTools were flooding every cell's
   output area.  Drop them on the floor by default; only
   capture lines that actually carry a MIME envelope (Display.*
   / Sparkle's `#showVerilog` etc.).  Plain debug lines now go
   to `console.error`, visible to a developer in DevTools but
   not to the notebook.

2. `src/WasmRepl.lean` was logging the full `displayOutput`
   (i.e. the MIME envelope) via `IO.eprintln`.  Under the new
   rule above that line still ends up in the cell because it
   contains the `\x1bMIME:` marker.  Cut it back to a
   length-only diagnostic — the envelope itself is already on
   the `messages` wire.

3. `tests/e2e/jupyter.ts` was returning
   `cell.locator('.jp-OutputArea-output').first()` to the test
   suite.  When a cell emits a stream-stderr output before the
   rich-display widget, `.first()` resolves to the stream node
   and any `.locator('svg, img')` search inside it is doomed.
   Return the container `.jp-OutputArea` instead so all child
   outputs are reachable.

4. `tests/e2e/rich-display.spec.ts` was matching `mjx-container,
   .MathJax, .jp-RenderedLatex` with a strict-mode locator.
   The MathJax MIME widget legitimately contains BOTH a
   `mjx-container` (the typeset glyphs) and a `.jp-RenderedLatex`
   (the wrapper), so the assertion crashed with a strict-mode
   violation rather than passing.  Drop `.first()` on the
   `svg, img` and `mjx-container, …` locators — the assertion
   reads as "at least one of these renders", which is what
   we actually want.  Also re-enable the `#latex` test since the
   underlying renderer works in the same Docker environment we
   ship — what we were observing was the strict-mode crash,
   not a render-latency wart.

Verified end-to-end with `make e2e-docker` (CI=1):
  7 passed (rich-display + smoke), 1 skipped (Waveform), 0 failed.
@junjihashimoto junjihashimoto merged commit fc3397b into main Jun 12, 2026
7 of 8 checks passed
@junjihashimoto junjihashimoto deleted the feature/extra-wasm-dirs branch June 12, 2026 13:38
mark-christiaens pushed a commit to mark-christiaens/sparkle that referenced this pull request Jun 24, 2026
xeus-lean PR Verilean#13 removed all third-party Lean libraries from its
build and introduced a vendor-agnostic `EXTRA_WASM_DIRS` extension
contract (see Verilean/xeus-lean#13 + the README).  Sparkle was one
of the libraries unbundled.  Add the publish-side of that contract.

## What's new

- `c_src/sparkle_jit_wasm_stub.c` — WASM-side stub for every
  `@[extern "sparkle_jit_*"]` declaration in `Sparkle.Core.JIT`.
  The native JIT uses `dlopen` + `dlsym` to load compiled CppSim
  shared libraries; neither exists in the xeus-lean WASM sandbox.
  Every stub raises `IO.userError` with a clear, actionable message
  pointing notebook users at `#synthesizeVerilog` / `#showVerilog`
  (which DO work under WASM) or at a native `lake exe` build for
  the JIT path.  Tutorial cells that only synthesise Verilog or
  evaluate `Signal.atTime` are unaffected.

- `tools/wasm/build-wasm.sh` — staging-builder modeled on
  xeus-lean's `tests/fixtures/mock-extra/build-wasm.sh` (the
  reference for the EXTRA_WASM_DIRS contract).  It:
    1. `lake build Sparkle` and copies the olean tree.
    2. Compiles `sparkle_barrier.c` (WASM-safe pure C) and the new
       `sparkle_jit_wasm_stub.c` with `emcc -sMEMORY64`.
    3. Compiles every Lake-generated `.c` wrapper under
       `.lake/build/ir/Sparkle/**.c` with the `LEAN_MIMALLOC`
       override the mock-extra fixture documents.
    4. Archives them into `libsparkle_wasm.a`.
    5. Extracts every `LEAN_EXPORT` symbol into
       `sparkle_exports.txt` so emcc's `-sEXPORTED_FUNCTIONS`
       survives LTO.
    6. Writes the `xeus-lean-extra.json` manifest + a
       `.xeus-auto-imports` registry that pre-imports `Sparkle`
       on REPL cell 1.
  IP.* / Tools.SVParser / Tests / Examples are intentionally out of
  scope — they'd inflate the JupyterLite first-boot download by
  ~50 MB and the tutorials never import them.

- `tools/wasm/README.md` — what the staging dir contains, how to
  drive it from xeus-lean's `pixi run -e wasm-build`, and what to
  do when Sparkle's extern surface changes.

- Top-level `README.md` — point users at the browser path and at
  `tools/wasm/`.

## What this does NOT change

- Sparkle's native build path is untouched.  `sparkle_jit_wasm_stub.c`
  is only compiled into the WASM artefact; native builds keep
  linking `c_src/sparkle_jit.c` as before.
- No Lean source changed.  All routes through the existing
  `@[extern "sparkle_jit_*"]` declarations.

## Verification

- [x] `lake build Sparkle` — succeeds (no Lean / Lake changes).
- [x] `gcc -Wall -Wextra -c sparkle_jit_wasm_stub.c` — compiles
      clean against the same `lean/lean.h` the WASM build uses.
- [ ] End-to-end WASM run (xeus-lean side) — to be exercised by
      running xeus-lean's docs CI with this Sparkle checkout
      passed via `EXTRA_WASM_DIRS`.  Not part of this PR's diff
      (it's a cross-repo verification step).
junjihashimoto added a commit to Verilean/sparkle that referenced this pull request Jul 1, 2026
xeus-lean PR #13 removed all third-party Lean libraries from its
build and introduced a vendor-agnostic `EXTRA_WASM_DIRS` extension
contract (see Verilean/xeus-lean#13 + the README).  Sparkle was one
of the libraries unbundled.  Add the publish-side of that contract.

## What's new

- `c_src/sparkle_jit_wasm_stub.c` — WASM-side stub for every
  `@[extern "sparkle_jit_*"]` declaration in `Sparkle.Core.JIT`.
  The native JIT uses `dlopen` + `dlsym` to load compiled CppSim
  shared libraries; neither exists in the xeus-lean WASM sandbox.
  Every stub raises `IO.userError` with a clear, actionable message
  pointing notebook users at `#synthesizeVerilog` / `#showVerilog`
  (which DO work under WASM) or at a native `lake exe` build for
  the JIT path.  Tutorial cells that only synthesise Verilog or
  evaluate `Signal.atTime` are unaffected.

- `tools/wasm/build-wasm.sh` — staging-builder modeled on
  xeus-lean's `tests/fixtures/mock-extra/build-wasm.sh` (the
  reference for the EXTRA_WASM_DIRS contract).  It:
    1. `lake build Sparkle` and copies the olean tree.
    2. Compiles `sparkle_barrier.c` (WASM-safe pure C) and the new
       `sparkle_jit_wasm_stub.c` with `emcc -sMEMORY64`.
    3. Compiles every Lake-generated `.c` wrapper under
       `.lake/build/ir/Sparkle/**.c` with the `LEAN_MIMALLOC`
       override the mock-extra fixture documents.
    4. Archives them into `libsparkle_wasm.a`.
    5. Extracts every `LEAN_EXPORT` symbol into
       `sparkle_exports.txt` so emcc's `-sEXPORTED_FUNCTIONS`
       survives LTO.
    6. Writes the `xeus-lean-extra.json` manifest + a
       `.xeus-auto-imports` registry that pre-imports `Sparkle`
       on REPL cell 1.
  IP.* / Tools.SVParser / Tests / Examples are intentionally out of
  scope — they'd inflate the JupyterLite first-boot download by
  ~50 MB and the tutorials never import them.

- `tools/wasm/README.md` — what the staging dir contains, how to
  drive it from xeus-lean's `pixi run -e wasm-build`, and what to
  do when Sparkle's extern surface changes.

- Top-level `README.md` — point users at the browser path and at
  `tools/wasm/`.

## What this does NOT change

- Sparkle's native build path is untouched.  `sparkle_jit_wasm_stub.c`
  is only compiled into the WASM artefact; native builds keep
  linking `c_src/sparkle_jit.c` as before.
- No Lean source changed.  All routes through the existing
  `@[extern "sparkle_jit_*"]` declarations.

## Verification

- [x] `lake build Sparkle` — succeeds (no Lean / Lake changes).
- [x] `gcc -Wall -Wextra -c sparkle_jit_wasm_stub.c` — compiles
      clean against the same `lean/lean.h` the WASM build uses.
- [ ] End-to-end WASM run (xeus-lean side) — to be exercised by
      running xeus-lean's docs CI with this Sparkle checkout
      passed via `EXTRA_WASM_DIRS`.  Not part of this PR's diff
      (it's a cross-repo verification step).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant