Attached runtimes
Part of the architecture notes: Jolt out of process, the shared attach lifecycle, and the evaluators that attach to a server you started. The dialect selection they plug into is in Dialects and evaluators.
The full design record, with the reasoning and what was ruled out, is kept in docs/history/architecture/attached-runtimes.md.
jonquil.nrepl.*: Jolt out of process
jonquil.nrepl.childlaunches Jolt's nREPL server in the project directory on a port it scans for. It writes no.nrepl-portand never reads the project's. Readiness comes from a log line the child prints, never from the connection alone. Ending a child kills only the pid Jonquil spawned. The child exits when its stdin pipe closes, so even a crash of Jonquil takes it along.jonquil.nrepl.clientkeeps one socket and one reader per child. The reader dispatches replies by:id, so an interrupt and the eval it stops share a connection.jonquil.nrepl.bencodeis the pure wire format, with a length cap so a hostile reply can't hang the process.jonquil.nrepl.opstranslates nREPL replies to Jonquil's shapes and builds the reset form, which appliesjonquil.session's reset rule over the wire. Bookkeeping evals go throughkeeping-history, so the user's*1/*2/*3stay put.
jonquil.nrepl.evaluator's NRepl record implements Evaluator on top: start! spawns and connects lazily (and refuses after stop!), eval! sends load-file for Run (with file-path only when the file on disk holds exactly the buffer's text, session/saved-file, since the child records it as the vars' :file; the same path binds *file*, so a Run of an unsaved or edited buffer has no *file* in the Jolt child, unlike the JVM child) or eval for the prompt, and stop! interrupts, then kills and restarts the child if that fails.
Every evaluator joins jonquil.runtime-registry when made. jonquil.main stops them all before exit, and stop-all! also runs from a shutdown hook for SIGTERM, SIGHUP and SIGINT. SIGKILL runs no hook; the stdin pipe covers that case.
jonquil.nrepl.connection: the shared attach lifecycle
This namespace holds what every attach evaluator needs: the Attached protocol (connected?, on-connection!), connect!, detach!, request!, stop-requested?, and the make-attached multimethod (see the diagram in Dialects and evaluators). Each dialect supplies a connect policy (console sentences and what :on-connect adds to :state) and keeps its own run worker. The window tests (satisfies? connection/Attached e), never a dialect's record.
jonquil.nrepl.attach: ClojureDart, attached
Attach never starts anything. On first use it reads NREPL.lock, connects, calls describe, and clones one session, kept for the life of the connection.
sequenceDiagram
participant W as Window
participant A as Attach
participant F as NREPL.lock
participant App as ClojureDart app (nREPL)
W->>A: eval! (Run, Cmd+E or Cmd+Enter)
A->>F: read the port (never written)
A->>App: connect, then describe
App-->>A: ops include eval and load-file
A->>App: clone
App-->>A: new session
A->>App: eval (+ 1 2), Jonquil's own wake-up probe
App-->>A: banner (out), then value 3
opt Run Selection or Run Form in another namespace
A->>App: eval the buffer's ns form
end
A->>App: load-file (Run) or eval (selection), sent once
App-->>A: out, value, ns, done
A-->>W: one result line, and the label reads attached
Note over W,App: Stop closes the socket. It never sends interrupt or close.
- Run uses
load-fileand prints one; Loaded <ns> (<n> forms)line. - The session namespace is tracked from replies'
:ns. If it differs from the buffer'snsform, Run Selection and Run Form evaluate that form first. - A fresh session gets a
(+ 1 2)probe (retried once on nil) before user code. User code is never sent twice. - Invariant: never send
interruptorclose; a client-side interrupt wedges the app's watcher. Stop closes the socket, even mid-handshake, and the next eval reconnects.
jonquil.cljd is the pure half: console sentences, the ns form, the lock-file port, the probe verdict and the load summary, with no socket or GTK.
jonquil.nrepl.jvm: JVM Clojure, attached
Jvm serves {:dialect :clj}. A JVM server honours :ns, so Run Selection and Run Form send it explicitly and Jonquil never sends the buffer's ns form; an unloaded namespace gets the not-loaded sentence. Run is a plain load-file, then the prompt moves to the buffer's namespace.
- Auto-require: on each fresh connection,
auto-require!sends one(require ...)for the:auto-requirelist from.jonquil.edn(kept for:cljand:jolt). A failure is reported and the user's request still goes out. Jolt does the same, out of process and in process. - Stop: sends
interruptif the server advertises it and waits up to:interrupt-timeout-ms(1 s); it detaches only if the evaluation hasn't ended by then, or the server lacks the op. - stdin:
send-eof!sends an emptystdinop only on nREPL 1.8.0 or later with a read waiting. Older servers get nothing, and the console says so once. - Help:
completemerges server and offline candidates.lookupuses the server's op and adds aDefined in <file>:<line>line.lookupandfull-docresolve:nsthroughresolve-lookup!(buffer namespace, then prompt namespace).full-docruns off the GTK thread with a longer:full-doc-timeout-ms, because the first call downloads the ClojureDocs export.
jonquil.clj is the pure half: console sentences, the ns form, the connect policy and the reply decisions.
jonquil.nrepl.generic: any nREPL server, attached
{:dialect :nrepl} reuses Jvm (make-attached :nrepl), because everything JVM-specific already degrades by the server's ops. jonquil.nrepl.generic holds the pure differences:
endpointandport-fileread.jonquil.edn's:nreplmap, andselection-keyincludes the endpoint, so a new host or port means a new connection.limitationsis derived from the server's ops once connected, andstdin-noticewarns before a Run that reads stdin on a server without the op.with-prefixtrims cidercompletereplies, andlocal-file?drops lookup paths that aren't on this disk.- Quick docs use
lookup, else cider'sinfo;resolve-lookup!retries a miss in the prompt namespace. ops/error-messageextracts an error line from servers that send noclojure.mainheader, falling through to the first-line rule.
jonquil.nrepl.shadow: ClojureScript, attached
Shadow serves {:dialect :cljs}. It connects to the shadow-cljs server you started (.shadow-cljs/nrepl.port) and never starts or signals it.
- A fresh session is Clojure, so the first eval after a connect sends
(shadow.cljs.devtools.api/nrepl-select :build). The build comes from:cljs :shadow-build, else the only build inshadow-cljs.edn. - Run evals the whole buffer with no
:ns. Shadow continues after a throw, so Run ends with a summary line, with errors already streamed. - Run Selection, Run Form and REPL entries send
:ns; a throw comes back as:repl/exception!. - Stop detaches, since
interruptcan't stop work in the JS runtime. completeandeldocuse the server's cider ops when listed, else the offline tables.
jonquil.cljs is the pure half.
This page is docs/guide/architecture/attached-runtimes.md in the Jonquil repository.