feat(nrepl): nREPL server + client in Clojure on a Janet interop bridge

Add a general Janet interop bridge and an nREPL implemented on top of it.

Interop bridge (evaluator):
- A qualified symbol whose namespace is `janet` or `janet.<module>` resolves
  against Janet's environment: `janet/<name>` -> root binding (janet/slurp),
  `janet.<module>/<name>` -> module binding (janet.net/server, janet.os/clock).
  The explicit `janet` segment marks every crossing into host code (where
  Clojure semantics, e.g. collection representation, no longer hold). This makes
  the whole Janet stdlib — networking included — reachable from Clojure.

jolt.nrepl (Clojure, src/jolt/jolt/nrepl.clj):
- bencode codec (encode + streaming decode), ported from nrepl.bencode.
- server: accept loop via janet.net/accept + janet.ev/call (Janet's built-in
  handler arity-checks Jolt closures, so we drive accept ourselves); ops clone/
  describe/eval/load-file/close/ls-sessions/interrupt/eldoc following
  babashka.nrepl response shapes. eval captures *out* by rebinding Janet's :out,
  reports ns, streams out, and isolates the eval namespace (current-ns is global
  ctx state) restoring it afterward. Vars are rendered as #'ns/name (pr-str
  loops on a var's cyclic ns refs).
- client: connect / request / client-eval / client-clone / client-close.

CLI: `jolt nrepl [port]` starts the server and writes .nrepl-port; the Clojure
source is embedded at build time so the binary is self-contained from any cwd.

Tests: test/spec/nrepl-spec.janet (bencode), test/integration/nrepl-test.janet
(server+client over a real TCP/bencode wire, server in a subprocess).
This commit is contained in:
Yogthos 2026-06-05 21:25:23 -04:00
parent 0f12598b06
commit 8cbc695f99
6 changed files with 457 additions and 7 deletions

View file

@ -0,0 +1,27 @@
# Specification: jolt.nrepl bencode codec (pure, no networking).
# The server/client wire behavior is covered by test/integration/nrepl-test.janet.
(use ../support/harness)
(defn- b [body]
(string "(do (require '[jolt.nrepl :as nr]) " body ")"))
(defn- rt [body]
# round-trip a value through encode -> decode
(b (string "(nr/decode (nr/reader nil (nr/encode " body ")))")))
(defspec "jolt.nrepl / bencode round-trip"
["integer" "42" (rt "42")]
["negative" "-7" (rt "-7")]
["string" "\"hello\"" (rt "\"hello\"")]
["empty string" "\"\"" (rt "\"\"")]
["list" "[\"a\" 1 \"b\"]" (rt "[\"a\" 1 \"b\"]")]
["nested list" "[1 [2 3]]" (rt "[1 [2 3]]")]
["dict" "{\"op\" \"eval\" \"id\" \"7\"}" (rt "{\"op\" \"eval\" \"id\" \"7\"}")]
["dict with list" "{\"status\" [\"done\"]}" (rt "{\"status\" [\"done\"]}")]
["nested dict" "{\"a\" {\"b\" 1}}" (rt "{\"a\" {\"b\" 1}}")])
(defspec "jolt.nrepl / bencode encode shape"
["int" "\"i42e\"" (b "(nr/encode 42)")]
["string" "\"5:hello\"" (b "(nr/encode \"hello\")")]
["list" "\"li1ei2ee\"" (b "(nr/encode [1 2])")]
# dict keys are sorted lexicographically
["dict sorted keys" "\"d1:ai1e1:bi2ee\"" (b "(nr/encode {\"b\" 2 \"a\" 1})")])