Skip to content

xlean-mcp + JIT-in-kernel + Real-Time Collaboration#53

Merged
junjihashimoto merged 5 commits into
mainfrom
feature/xlean-mcp-jit-integration
Jun 12, 2026
Merged

xlean-mcp + JIT-in-kernel + Real-Time Collaboration#53
junjihashimoto merged 5 commits into
mainfrom
feature/xlean-mcp-jit-integration

Conversation

@junjihashimoto

Copy link
Copy Markdown
Contributor

Summary

End-to-end enablement of the tutorial Docker image as an MCP-driven Sparkle workspace:

  • Bundle xlean-mcp in the image (11bab79). The stdio MCP binary now ships preinstalled so MCP hosts (Claude Code, Cursor, …) can spawn it via docker exec -i sparkle xlean-mcp instead of building inside the container. Depends on MCP server improvements: NDJSON transport + kernel_execute + md⇄ipynb sync xeus-lean#14.
  • Enable Jupyter Real-Time Collaboration (51a793e). Install jupyter-collaboration and add --collaborative to the jupyter lab CMD. With this, an external edit to a .ipynb (e.g. the MCP server's markdown_to_notebook regenerating Ch03 from its .md) propagates to every open JupyterLab tab via Y.js / CRDT without a manual reload dialog.
  • Link Sparkle FFI into the xlean kernel binary (a15560e). Pre-build c_src/sparkle_jit.c and c_src/sparkle_barrier.c with -fvisibility=default, force the xlean relink to actually consume them via XEUS_LEAN_EXTRA_LIBS wrapped in --whole-archive + --undefined=<sym> to defeat the Lean linker tail's -Wl,--gc-sections. nm --dynamic xlean | grep sparkle_jit_load now shows T sparkle_jit_load instead of empty.
  • JIT-in-kernel: precompileModules + LEAN_DYNLIB_PATH wiring (b8a6bc7). precompileModules := true on the Sparkle lean_lib so Lake emits a sparkle_Sparkle_Core_JIT.so next to the olean with all lp_* boxing wrappers; the kernelspec patch sets LEAN_DYNLIB_PATH= to that .so so xlean's existing Lean.loadDynlib boot loop picks it up. #eval Sparkle.Core.JIT.JIT.compileAndLoad ... now succeeds in a notebook cell instead of throwing "Could not find native implementation".
  • Three new tutorial cells (d069c61) that demonstrate the workflows above are working end-to-end inside JupyterLab:
    • Ch03 §3.0 Variant — 4-bit Gray-code counter + JIT sim
    • Ch05 §5.5e Worked example — read Verilog via verilog!, prove three theorems about nextState
    • Ch08 §8.2/§8.3 — in-notebook Yosys run via #bash

Test plan

  • docker build -t sparkle-tutorial:latest -f docker/tutorial/Dockerfile . succeeds end-to-end
  • docker run --rm -d --name sparkle -p 8888:8888 sparkle-tutorial:latest and Ch03/Ch05/Ch08 cells execute green
  • nm --dynamic /usr/local/bin/xlean | grep sparkle_jit_load shows T sparkle_jit_load
  • #eval Sparkle.Core.JIT.JIT.compileAndLoad "/tmp/foo_jit.cpp" returns a handle (does not throw)
  • markdown_to_notebook from xlean-mcp regenerates the Ch03 .ipynb and the open tab updates without a reload prompt (RTC working)
  • docker exec -i sparkle xlean-mcp speaks NDJSON and tools/list returns 9 tools including kernel_execute / markdown_to_notebook

Depends on

Known follow-ups

  • Sparkle.Backend.CppSim emits a _tmp_loop_body_* variable usage before its assignment for the circuit do { reg + …; return reg } register pattern, so the JIT sim of grayCtr / counter8 returns wrong runtime values (the JIT mechanism itself works, the synth output is buggy). Not addressed here — separate PR.

Add `xlean-mcp` to the `lake build` line and install it onto
`/usr/local/bin` alongside `xlean` / `xlean-convert`.  The source
already ships in xeus-lean (`src/MCPMain.lean` +
`src/XLean/MCP/*.lean`), but the binary wasn't being built, so MCP
hosts (Claude Code, Cursor, the MCP inspector) had no way to drive
the kernel without `lake build`ing inside the running container.

With this in the image, the canonical hookup is

    "command": "docker",
    "args": ["exec", "-i", "<container>", "xlean-mcp"]

— the stdio JSON-RPC server speaks directly to the host.
Install `jupyter-collaboration` (Y.js / CRDT backend, the
official Jupyter RTC extension) and pass `--collaborative` on
the `jupyter lab` CMD.  Without this, external edits to a
`.ipynb` — e.g. the `markdown_to_notebook` MCP tool regenerating
a chapter from its `.md` source — only land in the browser after
a manual "File changed on disk" reload dialog.  With it, Jupyter
Server watches the file, computes a CRDT update, and pushes the
diff to every connected client, so the cells refresh in place.
Make `Sparkle.Core.JIT.JIT.{load,eval,tick,…}` callable from a
notebook `#eval` cell.  Until now any cell that touched the JIT
threw

  lean::exception: Could not find native implementation of
  external declaration 'Sparkle.Core.JIT.JIT.load' (symbols
  'lp_sparkle_..._load___boxed' or 'lp_sparkle_..._load')

inside the kernel.  Four interlocking pieces are now in place:

1. Pre-build `c_src/sparkle_jit.c` and `c_src/sparkle_barrier.c`
   as static archives via `leanc -c -O2 -fvisibility=default`.
   `leanc` defaults to `-fvisibility=hidden`, which would mark
   every symbol local; overriding to `default` is what lets
   `dlsym(RTLD_DEFAULT, ...)` find them later.

2. Force the `lake build xlean` line to rebuild by `rm -f`'ing
   the binary first.  Lake's build cache doesn't hash
   `moreLinkArgs` or env vars, so without this the prior xlean
   from cmake's `buildXlean` script is treated as up-to-date and
   the extra archives never reach the link.

3. Pass the archives through `XEUS_LEAN_EXTRA_LIBS` wrapped in
   `-Wl,--whole-archive ... -Wl,--no-whole-archive`.  Without
   whole-archive the linker drops every TU because xlean itself
   references nothing in them at static-link time.

4. Pin each FFI entry point with `-Wl,--undefined=<sym>`.  Even
   with whole-archive the Lean linker tail appends
   `-Wl,--gc-sections`, which would otherwise strip the symbols
   at output time as unreachable; `--undefined=` marks them as
   roots so gc-sections keeps the containing sections.

The end state: `nm --dynamic /usr/local/bin/xlean | grep sparkle_jit_load`
shows `T sparkle_jit_load` (uppercase, in `.dynsym`), and the
interpreter's `dlsym(RTLD_DEFAULT, ...)` lookup at `#eval` time
resolves cleanly.

(There's a companion sparkle `lakefile.lean` change setting
`precompileModules := true` on the Sparkle lib so the Lean-side
`lp_*` wrapper symbols land in a per-module `.so`; the kernel
loads those at boot via `LEAN_DYNLIB_PATH`.)
Make `@[extern]` Sparkle declarations callable from the xeus-lean
kernel's `#eval` path.  The previous commit (`a15560e`) made the
C-level FFI symbols (`sparkle_jit_load`, `sparkle_jit_eval`, …)
resolvable via `dlsym` against xlean's own dynsym.  That gets us
half of the way: at `#eval` time the Lean interpreter actually
wants to call the *Lean-side* boxing wrapper
`lp_sparkle_Sparkle_Core_JIT_JIT_load___boxed`, not the raw C
function, and that wrapper only exists when the surrounding Lean
module is compiled to native code (it lives in the `.c.o` Lake
emits per module, not in the `.olean`).

Two changes pin the wrappers down:

- `lakefile.lean`: `precompileModules := true` on the `Sparkle`
  lean_lib.  Lake now produces a per-module shared library
  (e.g. `sparkle_Sparkle_Core_JIT.so`) alongside each
  `.olean`, with all the `lp_*` symbols exported.

- `docker/tutorial/Dockerfile`: extend the Stage-5 kernelspec
  patch to set `LEAN_DYNLIB_PATH=<sparkle_Sparkle_Core_JIT.so>`.
  `XeusKernel.lean` already has a loop that reads this env var
  and calls `Lean.loadDynlib` on each entry at boot, so the
  precompiled module's symbols become part of `RTLD_DEFAULT`'s
  search space.  We list only `JIT.so` because the other
  Sparkle modules cross-reference each other and would need a
  topo-sorted load order; `JIT.so` is self-contained except for
  the C-level `sparkle_jit_*` symbols already in xlean's dynsym.

After this, `#eval JIT.compileAndLoad "..."` returns a valid
handle instead of crashing the kernel with "Could not find
native implementation".
Three new tutorial cells that exercise paths the harness now
supports end-to-end inside JupyterLab:

- **Ch03 §3.0 Variant — 4-bit Gray-code counter + JIT sim.**
  Introduces the `bin XOR (bin >> 1)` trick as a stepping stone
  to the rest of the chapter, then shows the same design driven
  through `Sparkle.Core.JIT.JIT.{compileAndLoad,eval,tick,
  getOutput,destroy}` so the cell renders the expected Gray
  sequence inline.  Hits the new `LEAN_DYNLIB_PATH`-driven
  `lp_*` resolution path.

- **Ch05 §5.5e Worked example — read Verilog, then prove it.**
  Inline `verilog!` macro materialises `nextState` for a one-
  register synchronous XOR, then proves three theorems about
  it via `simp [nextState] ; bv_decide`.  Demonstrates that the
  Verilog frontend isn't just for type-checking imported code —
  the resulting `State`/`Input`/`nextState` shape is reasonable
  to prove against directly.

- **Ch08 §8.2/§8.3 in-notebook Yosys run.**  Replaces the
  illustrative `lake exe sparkle-emit > /tmp/counter8.sv` line
  with a runnable `#writeVerilogDesign` cell, then converts the
  `yosys -p '...'` block from a `bash` text block to xeus-lean's
  `#bash` magic so the synthesis stat output appears below the
  cell with no host-side step.
@junjihashimoto junjihashimoto merged commit 26e510d into main Jun 12, 2026
4 of 6 checks passed
@junjihashimoto junjihashimoto deleted the feature/xlean-mcp-jit-integration branch June 12, 2026 16:26
mark-christiaens pushed a commit to mark-christiaens/sparkle that referenced this pull request Jun 24, 2026
PR Verilean#53's `precompileModules := true` on `lean_lib Sparkle` made Lake
build `libsparkle_Sparkle.so` alongside the oleans.  That .so was
linked against the C extern_lib archives (`libsparkle_barrier.a`,
`libsparkle_jit.a`) but the linker's default `--as-needed` pass
discarded every symbol no Lean wrapper currently *calls* directly —
including `sparkle_jit_load`, which the dlsym-loaded
`JIT.load` boxing wrapper looks up at run time.

Tutorial-notebooks CI consequently broke with:

  symbol lookup error: .lake/build/lib/libsparkle_Sparkle.so:
    undefined symbol: sparkle_jit_load

while building `Notebooks.Gen.Ch03_Sequential` (the new chapter
that exercises JIT.load).

Fix: pass `-Wl,--whole-archive` around the two extern archives in
`moreLinkArgs`, so the linker retains every symbol from them.  The
`-L ./.lake/build/c_src` path points at the directory the
`extern_lib` blocks emit those archives into.

`lake build Sparkle`, `lake build TutorialNotebooks`, and
`lake exe svparser-test` (42/0) all succeed locally with this
change.
junjihashimoto added a commit that referenced this pull request Jul 1, 2026
xlean-mcp + JIT-in-kernel + Real-Time Collaboration
junjihashimoto added a commit that referenced this pull request Jul 1, 2026
PR #53's `precompileModules := true` on `lean_lib Sparkle` made Lake
build `libsparkle_Sparkle.so` alongside the oleans.  That .so was
linked against the C extern_lib archives (`libsparkle_barrier.a`,
`libsparkle_jit.a`) but the linker's default `--as-needed` pass
discarded every symbol no Lean wrapper currently *calls* directly —
including `sparkle_jit_load`, which the dlsym-loaded
`JIT.load` boxing wrapper looks up at run time.

Tutorial-notebooks CI consequently broke with:

  symbol lookup error: .lake/build/lib/libsparkle_Sparkle.so:
    undefined symbol: sparkle_jit_load

while building `Notebooks.Gen.Ch03_Sequential` (the new chapter
that exercises JIT.load).

Fix: pass `-Wl,--whole-archive` around the two extern archives in
`moreLinkArgs`, so the linker retains every symbol from them.  The
`-L ./.lake/build/c_src` path points at the directory the
`extern_lib` blocks emit those archives into.

`lake build Sparkle`, `lake build TutorialNotebooks`, and
`lake exe svparser-test` (42/0) all succeed locally with this
change.
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