Make the REPL read multi-line forms and render real error messages

The REPL evaluated one line at a time, so a form split across lines
(e.g. `(+` then `1 2)`) raised instead of waiting. The read loop now
accumulates lines until delimiters are balanced — skipping string,
char, regex and comment context — printing a `... ` continuation prompt
for each extra line.

Reader/runtime errors rendered as Chez's "attempt to apply
non-procedure #[chez-pmap...]" instead of their real message. Two causes:

jolt-throw raised the thrown value raw. When a throw crossed the host
`eval` boundary, Chez re-wrapped the non-condition into a compound
condition whose message extraction applies the value, losing the message
and crashing on ex-info's empty-map :data. jolt-throw now raises a
&jolt-throw condition wrapping the value; catch (lowered to `guard`),
jolt-report-uncaught and jolt-render-throwable unwrap it back via
jolt-unwrap-throw, so ex-data/ex-message and the backtrace tag survive.

Every reader/post-prelude EOF-throw site used `(empty-pmap)` (with
parens), applying the empty-map value as a procedure and crashing during
ex-info construction before jolt-throw ran. Fixed to `empty-pmap`.

Re-minted the seed; smoke 23/23, unit 574/574.
This commit is contained in:
Yogthos 2026-06-30 20:36:06 -04:00
parent a58bca3bee
commit 240458d994
10 changed files with 225 additions and 132 deletions

View file

@ -258,6 +258,7 @@
(let [n {:op :try :body (analyze-seq ctx @body env)}
n (if (seq @catches)
(let [evar-name (gen-name "catch")
raw-name (gen-name "catch-raw")
evar (symbol evar-name)
dispatch
(reduce
@ -278,6 +279,7 @@
(list 'throw evar)
(reverse @catches))]
(assoc n :catch-sym evar-name
:catch-raw-sym raw-name
:catch-body (analyze-seq ctx (list dispatch)
(add-locals env [evar-name]))))
n)

View file

@ -666,14 +666,18 @@
:else
(invoke))))
;; try/catch/finally. throw raises the jolt value RAW (jolt-throw =
;; Scheme `raise`); catch lowers to `guard` with an `else` clause (the IR drops
;; the class), finally to `dynamic-wind`'s after-thunk (runs on success, catch and
;; escape — Clojure finally semantics). Both keys optional on the node.
;; try/catch/finally. throw raises a Chez condition wrapping the jolt value
;; (jolt-throw = Scheme `raise` of a &jolt-throw condition); catch lowers to
;; `guard`, whose raw binding is unwrapped via jolt-unwrap-throw so the catch var
;; receives the jolt value (preserving ex-data/ex-message and the backtrace
;; identity tag). finally lowers to `dynamic-wind`'s after-thunk (runs on
;; success, catch and escape — Clojure finally semantics). Both keys optional.
(defn- emit-try [node]
(let [core (if-let [cs (:catch-sym node)]
(str "(guard (" (munge-name cs) " (else " (emit (:catch-body node)) ")) "
(emit (:body node)) ")")
(let [raw (munge-name (:catch-raw-sym node))]
(str "(guard (" raw " (else (let ((" (munge-name cs) " (jolt-unwrap-throw " raw "))) "
(emit (:catch-body node)) "))) "
(emit (:body node)) ")"))
(emit (:body node)))]
(if-let [fin (:finally node)]
(str "(dynamic-wind (lambda () #f) (lambda () " core ") (lambda () " (emit fin) "))")

View file

@ -88,6 +88,53 @@
(let [{:keys [roots]} (deps/resolve-project (project-dir))]
(println (str/join ":" roots))))
(defn- repl-form-complete?
"True when `s` has balanced ()/[]/{}, no open string/char/regex, and at most
a trailing comment past the last form. Drives the REPL's read-until-complete
decision so a form split across lines is accumulated, not evaluated half-read."
[s]
(let [n (count s)]
(loop [i 0 depth 0 state :code] ; state: :code :string :regex :comment
(if (>= i n)
(and (<= depth 0) (#{:code :comment} state))
(let [c (get s i)]
(case state
:code (cond
(= c \;) (recur (inc i) depth :comment)
(= c \\) (recur (+ i 2) depth :code) ; char literal: \(
(= c \") (recur (inc i) depth :string)
(= c \#) (recur (inc i) depth
(if (= (get s (inc i)) \") :regex :code))
(#{\( \[ \{} c) (recur (inc i) (inc depth) :code)
(#{\) \] \}} c) (recur (inc i) (dec depth) :code)
:else (recur (inc i) depth :code))
:string (cond
(= c \\) (recur (+ i 2) depth :string) ; escaped char
(= c \") (recur (inc i) depth :code)
:else (recur (inc i) depth :string))
:regex (cond
(= c \\) (recur (+ i 2) depth :regex)
(= c \") (recur (inc i) depth :code)
:else (recur (inc i) depth :regex))
:comment (recur (inc i) depth
(if (#{\newline \return} c) :code :comment))))))))
(defn- repl-read-form []
;; Read lines — printing a secondary prompt for continuations — until the
;; accumulated buffer is a complete form. Returns the (possibly multi-line)
;; buffer, or nil on EOF at the primary prompt.
(loop [buf nil]
(print (if buf "... " "user=> ")) (flush)
(let [line (read-line)]
(cond
(nil? line) buf ; EOF: nil at primary, partial mid-form
(nil? buf) (cond
(str/blank? line) (recur nil) ; skip a blank first line
(repl-form-complete? line) line
:else (recur line))
:else (let [nb (str buf "\n" line)]
(if (repl-form-complete? nb) nb (recur nb)))))))
(defn- repl []
;; resolve the project so deps (git libs) are on the roots and native libs are
;; loaded — same context a run gets, so (require '[some.lib]) works in the REPL.
@ -95,15 +142,14 @@
(catch :default _ nil))
(println ";; jolt repl — :repl/quit or ^D to exit")
(loop []
(print "user=> ") (flush)
(let [line (read-line)]
(when line
(let [form (repl-read-form)]
(when form
;; :repl/quit / :exit exit the loop — a reliable gesture that works in any
;; terminal, unlike ^D (some terminals/editors don't deliver it as EOF).
(if (#{:repl/quit :exit} (try (read-string line) (catch :default _ nil)))
(if (#{:repl/quit :exit} (try (read-string form) (catch :default _ nil)))
nil
(do
(try (println (pr-str (load-string line)))
(try (println (pr-str (load-string form)))
(catch :default e
(println "error:" (or (ex-message e)
(try ((resolve 'jolt.host/condition-message) e) (catch :default _ nil))