fix: pr-str/str of a var, and nREPL namespace switching

- core: pr-render/str-render now render a Jolt var as #'ns/name instead of
  falling through to the generic table printer, which recursed into the var's
  cyclic :meta/:ns refs and looped forever. Adds name-of/var-display helpers.
- jolt.nrepl: capture the post-eval namespace *after* running the form — jolt
  evaluates map-literal values right-to-left, so {:val (eval form) :ns (the-ns)}
  read the ns before in-ns ran, pinning every session to 'user'. Now in-ns/ns
  switches persist across evals. render-value dropped: pr-str handles vars.
- specs: var printing (strings-spec) and nREPL in-ns/ns-persistence + explicit
  :ns override (nrepl-test).
This commit is contained in:
Yogthos 2026-06-05 22:05:45 -04:00
parent 5b66cfaa97
commit d00c3cc117
4 changed files with 38 additions and 7 deletions

View file

@ -1532,6 +1532,25 @@
(pr-render buf (in pair 1)))
(buffer/push-string buf "}"))
(defn- name-of
"Extract a plain name string from a string, symbol struct, or a namespace/var
table (reading its :name) — never recurses into the cyclic ns structure."
[x]
(cond
(nil? x) nil
(string? x) x
(and (struct? x) (= :symbol (get x :jolt/type))) (x :name)
(or (struct? x) (table? x)) (name-of (get x :name))
(string x)))
(defn- var-display
"Render a Jolt var as #'ns/name. A var's :meta/:ns refs are cyclic, so this
reads only its :name and :ns name — printing the var's pairs would loop."
[v]
(let [nm (name-of (v :name))
ns (name-of (v :ns))]
(if ns (string "#'" ns "/" nm) (string "#'" nm))))
(set pr-render
(fn [buf v]
(cond
@ -1551,6 +1570,7 @@
(number? v) (buffer/push-string buf (fmt-number v))
(and (struct? v) (= :symbol (v :jolt/type)))
(buffer/push-string buf (if (v :ns) (string (v :ns) "/" (v :name)) (v :name)))
(and (table? v) (= :jolt/var (get v :jolt/type))) (buffer/push-string buf (var-display v))
(core-sorted-map? v) (pr-render-pairs buf (sorted-map-entries v))
(core-sorted-set? v) (pr-render-seq buf (v :items) "#{" "}")
(lazy-seq? v) (pr-render-seq buf (realize-for-iteration v) "(" ")")
@ -1580,6 +1600,7 @@
(keyword? v) (string ":" (string v))
(and (struct? v) (= :symbol (v :jolt/type)))
(if (v :ns) (string (v :ns) "/" (v :name)) (v :name))
(and (table? v) (= :jolt/var (get v :jolt/type))) (var-display v)
(number? v) (fmt-number v)
(= true v) "true"
(= false v) "false"

View file

@ -90,16 +90,13 @@
; regardless of the ambient ns.
(defn- eval-in-ns [ns-str form]
(in-ns (symbol ns-str))
(let [result (try {:val (eval form) :ns (:name (the-ns))}
; Bind the value before reading the ns: jolt evaluates map-literal values
; right-to-left, so the result ns must be captured *after* eval runs any in-ns.
(let [result (try (let [v (eval form)] {:val v :ns (:name (the-ns))})
(catch Throwable e {:err e :ns (:name (the-ns))}))]
(in-ns 'jolt.nrepl)
result))
; pr-str on a var loops forever on its cyclic ns refs, so render vars (def/defn
; results) ourselves as #'ns/name rather than printing them.
(defn- render-value [v ns-str]
(if (var? v) (str "#'" ns-str "/" (get v :name)) (pr-str v)))
(defn- eval-handler [server msg send!]
; current-ns is global ctx state shared by all fibers, so set the eval ns
; explicitly each time: requested :ns, else the session's last ns, else user.
@ -122,7 +119,7 @@
(flush-out)
(swap! server assoc :eval-ns ns)
(when err (throw err))
(respond {"ns" ns "value" (render-value val ns)})
(respond {"ns" ns "value" (pr-str val)})
(recur (next forms) ns))))
(janet/setdyn :out old-out)
(respond {"status" ["done"]}))

View file

@ -64,6 +64,14 @@
(check "ns field reported"
"(some #(get % \"ns\") (jolt.nrepl/client-eval c \"(+ 1 1)\" s))" "user")
# in-ns switches the session ns, and it persists to the next eval
(check "in-ns switches ns"
"(some #(get % \"ns\") (jolt.nrepl/client-eval c \"(in-ns (quote foo.bar))\" s))" "foo.bar")
(check "ns persists across evals"
"(some #(get % \"ns\") (jolt.nrepl/client-eval c \"(+ 2 2)\" s))" "foo.bar")
# explicit :ns on the message overrides
(ev "(jolt.nrepl/request c {\"op\" \"eval\" \"code\" \"(+ 1 1)\" \"session\" s \"ns\" \"user\"})")
# eval error -> eval-error status, and the connection keeps working afterward
(check "eval error status"
"(boolean (some #(let [st (get % \"status\")] (and (sequential? st) (some (fn [x] (= \"eval-error\" x)) st))) (jolt.nrepl/client-eval c \"(/ 1 :z)\" s)))"

View file

@ -13,6 +13,11 @@
["string? true" "true" "(string? \"x\")"]
["pr-str vector" "\"[1 2 3]\"" "(pr-str [1 2 3])"]
["pr-str quotes str" "\"\\\"hi\\\"\"" "(pr-str \"hi\")"]
# a var's :meta/:ns refs are cyclic — pr-str/str render it as #'ns/name
# rather than recursing into (and looping on) the var's fields.
["pr-str of a var" "\"#'user/vv\"" "(pr-str (def vv 1))"]
["str of a var" "\"#'user/ww\"" "(str (def ww 2))"]
["pr-str of a defn" "\"#'user/gg\"" "(pr-str (defn gg [x] x))"]
["seq of string" "[\\a \\b]" "(seq \"ab\")"])
(defspec "clojure.string"