Trace by default in REPL-driven development
A repl or nREPL session now turns tail-frame tracing on, so an uncaught error in evaluated/reloaded code shows a tail-frame backtrace with no JOLT_TRACE set. The REPL and nREPL catch errors themselves rather than going through the uncaught reporter, so they now print the history backtrace via a new jolt.host/backtrace- string (history-only — the live continuation in a REPL is just REPL machinery). Because the recording is baked in at compile time, only code compiled while a session is live is traced; reload a namespace to trace already-loaded code. JOLT_TRACE=1 still forces it on for a whole run (a plain -M:run traces its own load); JOLT_TRACE=0 forces it off even in a session. No seed change — jolt.main/jolt.nrepl are runtime-loaded and compile-eval.ss / source-registry.ss are host files.
This commit is contained in:
parent
a3e2365217
commit
7167af4830
6 changed files with 89 additions and 23 deletions
|
|
@ -190,11 +190,21 @@ binary the frames map to `ns/name (file:line)`; on the runtime eval path they ar
|
||||||
the surviving fn names. Tail-call optimization erases tail-called frames, so the
|
the surviving fn names. Tail-call optimization erases tail-called frames, so the
|
||||||
default trace shows only the non-tail spine.
|
default trace shows only the non-tail spine.
|
||||||
|
|
||||||
Set `JOLT_TRACE=1` to opt into a fuller **tail-frame history**. Each compiled fn
|
A fuller **tail-frame history** recovers the frames TCO erases: each compiled fn
|
||||||
then records itself on entry into a bounded ring-of-rings buffer, so the trace
|
records itself on entry into a bounded ring-of-rings buffer, so the trace shows
|
||||||
recovers TCO-elided frames (including the immediate error site) while a tight tail
|
TCO-elided frames (including the immediate error site) while a tight tail loop
|
||||||
loop stays bounded and its non-tail caller context is preserved. It costs a small
|
stays bounded and its non-tail caller context is preserved.
|
||||||
per-call overhead, so it is off by default and never emitted into a built binary.
|
|
||||||
|
It is **on by default in REPL-driven development** — a `repl` or nREPL session
|
||||||
|
turns it on, so an error in code you evaluate or reload shows a tail-frame trace
|
||||||
|
with no setup. Because the recording is baked in at compile time, only code
|
||||||
|
compiled while a session is live is traced; reload a namespace to trace code that
|
||||||
|
was already loaded (e.g. an app's initial `-M:run` load before its nREPL started).
|
||||||
|
|
||||||
|
Elsewhere it is off (a small per-call cost, and never emitted into a `jolt build`
|
||||||
|
binary). Override with the environment: `JOLT_TRACE=1` forces it on for a whole
|
||||||
|
run — including a plain `-M:run`, so the app's own load is traced — and
|
||||||
|
`JOLT_TRACE=0` forces it off, even in a REPL/nREPL session.
|
||||||
|
|
||||||
## Conformance
|
## Conformance
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,13 +110,24 @@
|
||||||
;; older seed during the first re-mint pass.
|
;; older seed during the first re-mint pass.
|
||||||
(let ((scv (var-deref "jolt.backend-scheme" "set-var-cache!")))
|
(let ((scv (var-deref "jolt.backend-scheme" "set-var-cache!")))
|
||||||
(when (procedure? scv) (scv #t)))
|
(when (procedure? scv) (scv #t)))
|
||||||
;; JOLT_TRACE opts into tail-frame history: the emitter prepends a frame-recording
|
;; Tail-frame history. Turning it on makes the emitter add a per-fn history push to
|
||||||
;; push to every runtime-compiled fn, and the ring buffer is allocated for this
|
;; every fn compiled AFTERWARD, and allocates this thread's ring. Suppressed when
|
||||||
;; thread. Off by default, so a normal run emits and pays exactly as before.
|
;; JOLT_TRACE is explicitly falsey, so JOLT_TRACE=0 disables it even in dev mode.
|
||||||
(when (getenv "JOLT_TRACE")
|
(define (jolt-enable-trace!)
|
||||||
(let ((stf (var-deref "jolt.backend-scheme" "set-trace-frames!")))
|
(let ((e (getenv "JOLT_TRACE")))
|
||||||
(when (procedure? stf) (stf #t)))
|
(unless (and e (member e '("0" "false" "no")))
|
||||||
(jolt-trace-enable!))
|
(let ((stf (var-deref "jolt.backend-scheme" "set-trace-frames!")))
|
||||||
|
(when (procedure? stf) (stf #t)))
|
||||||
|
(jolt-trace-enable!))))
|
||||||
|
;; Exposed so the REPL / nREPL entrypoints (jolt.main, jolt.nrepl) can turn tracing
|
||||||
|
;; on for REPL-driven development without the user setting JOLT_TRACE. Because the
|
||||||
|
;; push is baked in at compile time, only code compiled after this call is traced —
|
||||||
|
;; which is exactly the code you eval / reload in a live session.
|
||||||
|
(def-var! "jolt.host" "enable-trace!" jolt-enable-trace!)
|
||||||
|
;; Explicit opt-in for a whole run (JOLT_TRACE=1): enable at load, BEFORE any app
|
||||||
|
;; namespace is compiled, so a plain `-M:run` traces the app's own code too.
|
||||||
|
(let ((e (getenv "JOLT_TRACE")))
|
||||||
|
(when (and e (not (member e '("0" "false" "no" "")))) (jolt-enable-trace!)))
|
||||||
|
|
||||||
;; (with-meta sym m) -> sym, else x — an (ns ^:no-doc name …) yields the name with
|
;; (with-meta sym m) -> sym, else x — an (ns ^:no-doc name …) yields the name with
|
||||||
;; reader metadata as a with-meta form; strip it to read the bare ns symbol.
|
;; reader metadata as a with-meta form; strip it to read the bare ns symbol.
|
||||||
|
|
|
||||||
|
|
@ -224,5 +224,25 @@ else
|
||||||
fails=$((fails + 1))
|
fails=$((fails + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# REPL-driven development traces by default: an error in an evaluated form shows a
|
||||||
|
# tail-frame backtrace with no JOLT_TRACE set. rb tail-calls ra tail-calls +, all
|
||||||
|
# TCO-elided from the continuation — only the history recovers them.
|
||||||
|
repl_err="$(printf '(defn ra [x] (+ x 1))\n(defn rb [x] (ra x))\n(rb :nan)\n:exit\n' | bin/joltc repl 2>&1)"
|
||||||
|
if printf '%s' "$repl_err" | grep -q ' trace:' && printf '%s' "$repl_err" | grep -q 'rb'; then
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: a REPL error should show a tail-frame trace by default"
|
||||||
|
printf '%s\n' "$repl_err" | sed 's/^/ | /'
|
||||||
|
fails=$((fails + 1))
|
||||||
|
fi
|
||||||
|
# JOLT_TRACE=0 opts out — no trace in the REPL.
|
||||||
|
repl_off="$(printf '(defn ra [x] (+ x 1))\n(defn rb [x] (ra x))\n(rb :nan)\n:exit\n' | JOLT_TRACE=0 bin/joltc repl 2>&1)"
|
||||||
|
if printf '%s' "$repl_off" | grep -q ' trace:'; then
|
||||||
|
echo " FAIL: JOLT_TRACE=0 should suppress the REPL trace"
|
||||||
|
fails=$((fails + 1))
|
||||||
|
else
|
||||||
|
pass=$((pass + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
echo "cli smoke: $pass passed, $fails failed"
|
echo "cli smoke: $pass passed, $fails failed"
|
||||||
[ "$fails" -eq 0 ]
|
[ "$fails" -eq 0 ]
|
||||||
|
|
|
||||||
|
|
@ -145,10 +145,11 @@
|
||||||
;; TCO-truncated non-tail spine.
|
;; TCO-truncated non-tail spine.
|
||||||
;; Each frame maps to "ns/name (file:line)" when registered, else its bare name.
|
;; Each frame maps to "ns/name (file:line)" when registered, else its bare name.
|
||||||
;; #f when neither source yields a frame (the caller then prints just the location).
|
;; #f when neither source yields a frame (the caller then prints just the location).
|
||||||
(define (jolt-backtrace-string v)
|
;; The tail-frame history ring rendered as a backtrace, or #f when tracing is off /
|
||||||
|
;; empty. A mapped frame is kept; else drop plumbing (same rule as the continuation
|
||||||
|
;; path) so the two sources read consistently.
|
||||||
|
(define (jolt-history-backtrace)
|
||||||
(let* ((hist (jolt-trace-snapshot))
|
(let* ((hist (jolt-trace-snapshot))
|
||||||
;; a mapped frame is always kept; else drop plumbing (same rule as the
|
|
||||||
;; continuation path) so the two backtrace sources read consistently
|
|
||||||
(recs (let loop ((ns hist) (acc '()))
|
(recs (let loop ((ns hist) (acc '()))
|
||||||
(if (null? ns)
|
(if (null? ns)
|
||||||
(reverse acc)
|
(reverse acc)
|
||||||
|
|
@ -156,12 +157,23 @@
|
||||||
(loop (cdr ns)
|
(loop (cdr ns)
|
||||||
(if (or src (not (srcreg-plumbing-name? nm)))
|
(if (or src (not (srcreg-plumbing-name? nm)))
|
||||||
(cons (cons nm src) acc) acc)))))))
|
(cons (cons nm src) acc) acc)))))))
|
||||||
(if (pair? recs)
|
(and (pair? recs) (jolt-render-recs recs))))
|
||||||
(jolt-render-recs recs)
|
|
||||||
(let ((k (jolt-error-continuation v)))
|
(define (jolt-backtrace-string v)
|
||||||
(and k
|
(or (jolt-history-backtrace)
|
||||||
(let ((recs (jolt-frame-records k)))
|
(let ((k (jolt-error-continuation v)))
|
||||||
(and (pair? recs) (jolt-render-recs recs))))))))
|
(and k
|
||||||
|
(let ((recs (jolt-frame-records k)))
|
||||||
|
(and (pair? recs) (jolt-render-recs recs)))))))
|
||||||
|
|
||||||
|
;; Exposed for the REPL / nREPL error paths, which catch errors themselves instead
|
||||||
|
;; of going through the uncaught reporter. Returns the " trace:\n<frames>" block
|
||||||
|
;; from the tail-frame HISTORY only — the live continuation in a REPL is just the
|
||||||
|
;; REPL's own machinery — or nil when tracing is off (so a caller can when-let).
|
||||||
|
(def-var! "jolt.host" "backtrace-string"
|
||||||
|
(lambda ()
|
||||||
|
(let ((bt (jolt-history-backtrace)))
|
||||||
|
(if bt (string-append " trace:\n" bt) jolt-nil))))
|
||||||
|
|
||||||
;; Render an uncaught jolt throw (any value, not just a Chez condition) to a port:
|
;; Render an uncaught jolt throw (any value, not just a Chez condition) to a port:
|
||||||
;; an ex-info shows its message + ex-data (+ a host cause); anything else is
|
;; an ex-info shows its message + ex-data (+ a host cause); anything else is
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,9 @@
|
||||||
;; loaded — same context a run gets, so (require '[some.lib]) works in the REPL.
|
;; loaded — same context a run gets, so (require '[some.lib]) works in the REPL.
|
||||||
(try (apply-project! (deps/resolve-project (project-dir)))
|
(try (apply-project! (deps/resolve-project (project-dir)))
|
||||||
(catch :default _ nil))
|
(catch :default _ nil))
|
||||||
|
;; REPL-driven development: trace by default so an uncaught error in evaluated
|
||||||
|
;; code shows a tail-frame backtrace, no JOLT_TRACE needed (JOLT_TRACE=0 opts out).
|
||||||
|
(jolt.host/enable-trace!)
|
||||||
(println (str ";; jolt " (version) " repl — :repl/quit or ^D to exit"))
|
(println (str ";; jolt " (version) " repl — :repl/quit or ^D to exit"))
|
||||||
(loop []
|
(loop []
|
||||||
(let [form (repl-read-form)]
|
(let [form (repl-read-form)]
|
||||||
|
|
@ -160,7 +163,9 @@
|
||||||
(catch :default e
|
(catch :default e
|
||||||
(println "error:" (or (ex-message e)
|
(println "error:" (or (ex-message e)
|
||||||
(try ((resolve 'jolt.host/condition-message) e) (catch :default _ nil))
|
(try ((resolve 'jolt.host/condition-message) e) (catch :default _ nil))
|
||||||
(pr-str e)))))
|
(pr-str e)))
|
||||||
|
(when-let [bt (jolt.host/backtrace-string)]
|
||||||
|
(print bt))))
|
||||||
(recur)))))))
|
(recur)))))))
|
||||||
|
|
||||||
;; A deps.edn :tasks entry: a string is a shell command; a map is {:main-opts …}.
|
;; A deps.edn :tasks entry: a string is a shell command; a map is {:main-opts …}.
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,10 @@
|
||||||
(try (when (and ns-str (not (str/blank? ns-str)) (find-ns (symbol ns-str)))
|
(try (when (and ns-str (not (str/blank? ns-str)) (find-ns (symbol ns-str)))
|
||||||
(in-ns (symbol ns-str)))
|
(in-ns (symbol ns-str)))
|
||||||
(reset! result (load-string code))
|
(reset! result (load-string code))
|
||||||
(catch :default e (reset! err (err-msg e)))))]
|
(catch :default e
|
||||||
|
(reset! err (str (err-msg e)
|
||||||
|
(when-let [bt (jolt.host/backtrace-string)]
|
||||||
|
(str "\n" bt)))))))]
|
||||||
{:value (when (nil? @err) (pr-str @result))
|
{:value (when (nil? @err) (pr-str @result))
|
||||||
:out out
|
:out out
|
||||||
:ns (str (ns-name *ns*))
|
:ns (str (ns-name *ns*))
|
||||||
|
|
@ -277,6 +280,11 @@
|
||||||
no-op."
|
no-op."
|
||||||
([port] (start port nil))
|
([port] (start port nil))
|
||||||
([port middleware]
|
([port middleware]
|
||||||
|
;; An nREPL session is REPL-driven development: trace by default so an uncaught
|
||||||
|
;; error in code evaluated over the connection shows a tail-frame backtrace, with
|
||||||
|
;; no JOLT_TRACE needed. Covers both `--nrepl-server` and an app that starts its
|
||||||
|
;; own server under `-M:run` (reload a namespace to trace already-loaded code).
|
||||||
|
(jolt.host/enable-trace!)
|
||||||
(let [handler (build-handler (resolve-middleware (or middleware [])))
|
(let [handler (build-handler (resolve-middleware (or middleware [])))
|
||||||
fd (listen-socket port) ; throws on bind/listen failure
|
fd (listen-socket port) ; throws on bind/listen failure
|
||||||
stopped (atom false)]
|
stopped (atom false)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue