|
| 1 | +# 0029 — The nREPL entry point: the compiler is the server, the engine is a child |
| 2 | + |
| 3 | +**Status:** proposed · 2026-07-30 · adversarially reviewed the same day, |
| 4 | +before implementation. The review inspected the actual nrepl 1.4.0 |
| 5 | +stack and changed five things, marked *(review)*: the handler is the |
| 6 | +default middleware stack with eval intercepted, not four hand-rolled |
| 7 | +ops; the session analysis API is a separate pair the corpus never |
| 8 | +imports; the child protocol's two framing pins; the `def` printing |
| 9 | +decision (a stable `user` presentation); and §6's hole is closed in |
| 10 | +this unit's own commit, because `0026`'s recorded trigger for closing |
| 11 | +it *is* this unit. |
| 12 | + |
| 13 | +## The question |
| 14 | + |
| 15 | +S2's remaining shape (`doc/roadmap.md`): the compiler runs on the JVM |
| 16 | +and *is itself* the nREPL server, so CIDER/Calva connect with no extra |
| 17 | +machinery. Every prerequisite is now decided — the assembler sits next |
| 18 | +to the engine (`0026`), forms link over one runtime (`0028`), throws |
| 19 | +classify (`0027`) — but nothing serves the protocol. What does the v0 |
| 20 | +server do on `eval`, who holds which half of the session, and what is |
| 21 | +honestly out of scope? |
| 22 | + |
| 23 | +## The decision |
| 24 | + |
| 25 | +1. **One JVM process, `cljwit.repl`, serving nREPL as the default |
| 26 | + middleware stack with `eval` intercepted** *(review — the first |
| 27 | + draft said "a minimal clone/describe/eval/close handler", which |
| 28 | + hand-rolls exactly the parts that break: `clone` is session |
| 29 | + middleware behavior, not an op you implement, and a handler that |
| 30 | + does not answer unknown ops with `unknown-op`+`done` hangs CIDER on |
| 31 | + its first `lookup`)*. A `wrap-cljwit-eval` middleware declares it |
| 32 | + handles `"eval"` ahead of `interruptible-eval` — piggieback's exact |
| 33 | + shape — so session bookkeeping, `describe`, and unknown-op |
| 34 | + semantics come from `nrepl/nrepl` protocol-correct and for free. |
| 35 | + `clj -M:repl` starts it; the `-X` coordinate `0021` called |
| 36 | + provisional is superseded by this. Verified with the nrepl client; |
| 37 | + an editor connection is a manual check until someone performs it |
| 38 | + *(review)*. |
| 39 | +2. **One long-lived node child, `dev/repl_engine.mjs`** — started by |
| 40 | + the server, speaking one JSON line per message on stdio, strict |
| 41 | + request→response alternation. Two framing pins *(review)*: **the |
| 42 | + child's stdout carries protocol lines exclusively** — diagnostics go |
| 43 | + to stderr, which the server inherits — and **the child exits on |
| 44 | + stdin EOF**, which is also the whole orphaning story when the JVM |
| 45 | + dies. It loads binaryen.js once (~170 ms, `0026`'s measured session |
| 46 | + cost), instantiates the session runtime once, holds the var ledger |
| 47 | + (`0028`'s instantiator half), and per form: assemble (explicit |
| 48 | + features, never `Features.All`), instantiate, merge exports — |
| 49 | + rejecting duplicate var exports loudly — call `entry`, answer |
| 50 | + `{"tag":"result"|"exn"|"trap"|"error", …}` in the corpus vocabulary. |
| 51 | + Crash recovery beyond EOF-exit is a named v0 hole. |
| 52 | +3. **The compiler half of the session lives in a separate session API** |
| 53 | + *(review)*: `open-session`/`analyze-in-session!`/`close-session!` |
| 54 | + keep one analysis namespace alive across eval messages, so form 2's |
| 55 | + analysis resolves form 1's vars. **`analyze-forms` keeps its |
| 56 | + create-and-discard semantics unchanged, and the corpus lanes use |
| 57 | + only it** — the oracle's isolation rule (`0022` B.2) depends on the |
| 58 | + discard, and routing corpus entries through the session API would |
| 59 | + be the analysis-side version of the leak `0028`'s ledger makes |
| 60 | + loud. The contract is in both docstrings. |
| 61 | +4. **What the REPL prints, v0**: an expression's value is a scalar and |
| 62 | + prints as its number (`0022` B.3's boundary, surfaced honestly); a |
| 63 | + `def` prints **`#'user/name`** — a *presentation* *(review)*: the |
| 64 | + session's analysis namespace is internal and gensym'd, the user |
| 65 | + never sees it, and the printed var is analysis state, not a JVM var |
| 66 | + anyone can deref. Namespaces are global across nREPL sessions, |
| 67 | + Clojure's own answer and the cheap parity choice. A classified |
| 68 | + `exn`/`trap` prints as an error with the trap-table row's class |
| 69 | + name. Anything the slice cannot express is the loud out-of-slice |
| 70 | + compile error it already is. |
| 71 | +5. **Scope, named**: no watcher, no browser transport, no interrupt, no |
| 72 | + pretty-printing, no wasmtime session lane (deferred in `0028` §5). |
| 73 | + The engine child is node-only v0; the browser session reuses the |
| 74 | + same protocol the day a transport exists for it. |
| 75 | +6. **The dev assembler enters the toolchain in this unit's commit** |
| 76 | + *(review — `0026` recorded exactly this trigger: "`tools.json` |
| 77 | + gains the dev assembler's version when the nREPL unit lands", and |
| 78 | + this is that unit; deferring again would be two notes pointing at |
| 79 | + each other forever)*. `tools.json` gains `binaryen-js` with a |
| 80 | + node-side probe, and `bb check` materializes `node_modules` with a |
| 81 | + guarded `npm ci` when it is absent — a network step only on first |
| 82 | + run or lock change, the same class of need as `bb ref`. The |
| 83 | + integration test still skips with a printed line if binaryen is |
| 84 | + somehow absent (the FFM tests' trade), but on CI it runs: server |
| 85 | + in-process, real nREPL client, eval through the whole pipe. |
| 86 | + |
| 87 | +## Why |
| 88 | + |
| 89 | +- The server/child split is `0026`'s decision made operational: |
| 90 | + assembly *must* live engine-side for the browser anyway, so the node |
| 91 | + child doing its own assembly keeps one code path for both targets. |
| 92 | +- The JSON-line protocol is the session runner (`corpus/session.mjs`) |
| 93 | + turned resident — same instantiation, same ledger, same |
| 94 | + classification, one loop around it. |
| 95 | +- nREPL rather than a bespoke socket because the roadmap's whole point |
| 96 | + for S2 is that editors connect with zero new machinery. |
| 97 | + |
| 98 | +## Alternatives rejected |
| 99 | + |
| 100 | +- **Evaluating on the JVM and cross-compiling later** (a normal Clojure |
| 101 | + nREPL now, wasm someday). Rejected because it is not this project: a |
| 102 | + REPL whose semantics are the JVM's is `0002`'s incident waiting to |
| 103 | + recur at the DX layer — every difference would be invisible until |
| 104 | + batch compilation. |
| 105 | +- **piggieback middleware inside a host JVM REPL.** Right shape for |
| 106 | + nesting, heavier dependency surface, and nothing nests yet; the |
| 107 | + minimal handler serves the same editors today and does not preclude |
| 108 | + it. |
| 109 | +- **JVM-side assembly (wasm-tools spawn) for the v0 child.** Works |
| 110 | + everywhere the gate runs, but contradicts `0026`'s decided placement |
| 111 | + and forks the code path the browser target needs; the 23 ms spawn is |
| 112 | + also above the budget `0026` bought for 1.3 ms. |
| 113 | +- **A bespoke wire protocol to the editor.** nREPL exists; the child's |
| 114 | + JSON lines are internal plumbing, not an editor surface. |
| 115 | + |
| 116 | +## What would falsify this |
| 117 | + |
| 118 | +- **An editor round-trip measurably dominated by something other than |
| 119 | + the measured pieces** (analysis, 1.3 ms assembly, instantiation) — |
| 120 | + the v0 makes no latency claim, but the first complaint gets measured |
| 121 | + against `0026`'s numbers before anything is redesigned. |
| 122 | +- **Session growth**: hundreds of forms → hundreds of live instances |
| 123 | + and an ever-longer ledger (`0028`'s named soak question). The nREPL |
| 124 | + server is where a soak test becomes runnable at all. |
| 125 | +- **The CI hole biting** — a protocol regression landing while CI |
| 126 | + cannot run the pipe. If that happens once, the assembler's |
| 127 | + `tools.json` entry is due immediately, not eventually. |
0 commit comments