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
|
||||
default trace shows only the non-tail spine.
|
||||
|
||||
Set `JOLT_TRACE=1` to opt into a fuller **tail-frame history**. Each compiled fn
|
||||
then records itself on entry into a bounded ring-of-rings buffer, so the trace
|
||||
recovers TCO-elided frames (including the immediate error site) while a tight tail
|
||||
loop stays bounded and its non-tail caller context is preserved. It costs a small
|
||||
per-call overhead, so it is off by default and never emitted into a built binary.
|
||||
A fuller **tail-frame history** recovers the frames TCO erases: each compiled fn
|
||||
records itself on entry into a bounded ring-of-rings buffer, so the trace shows
|
||||
TCO-elided frames (including the immediate error site) while a tight tail loop
|
||||
stays bounded and its non-tail caller context is preserved.
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -110,13 +110,24 @@
|
|||
;; older seed during the first re-mint pass.
|
||||
(let ((scv (var-deref "jolt.backend-scheme" "set-var-cache!")))
|
||||
(when (procedure? scv) (scv #t)))
|
||||
;; JOLT_TRACE opts into tail-frame history: the emitter prepends a frame-recording
|
||||
;; push to every runtime-compiled fn, and the ring buffer is allocated for this
|
||||
;; thread. Off by default, so a normal run emits and pays exactly as before.
|
||||
(when (getenv "JOLT_TRACE")
|
||||
(let ((stf (var-deref "jolt.backend-scheme" "set-trace-frames!")))
|
||||
(when (procedure? stf) (stf #t)))
|
||||
(jolt-trace-enable!))
|
||||
;; Tail-frame history. Turning it on makes the emitter add a per-fn history push to
|
||||
;; every fn compiled AFTERWARD, and allocates this thread's ring. Suppressed when
|
||||
;; JOLT_TRACE is explicitly falsey, so JOLT_TRACE=0 disables it even in dev mode.
|
||||
(define (jolt-enable-trace!)
|
||||
(let ((e (getenv "JOLT_TRACE")))
|
||||
(unless (and e (member e '("0" "false" "no")))
|
||||
(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
|
||||
;; reader metadata as a with-meta form; strip it to read the bare ns symbol.
|
||||
|
|
|
|||
|
|
@ -224,5 +224,25 @@ else
|
|||
fails=$((fails + 1))
|
||||
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"
|
||||
[ "$fails" -eq 0 ]
|
||||
|
|
|
|||
|
|
@ -145,10 +145,11 @@
|
|||
;; TCO-truncated non-tail spine.
|
||||
;; 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).
|
||||
(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))
|
||||
;; 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 '()))
|
||||
(if (null? ns)
|
||||
(reverse acc)
|
||||
|
|
@ -156,12 +157,23 @@
|
|||
(loop (cdr ns)
|
||||
(if (or src (not (srcreg-plumbing-name? nm)))
|
||||
(cons (cons nm src) acc) acc)))))))
|
||||
(if (pair? recs)
|
||||
(jolt-render-recs recs)
|
||||
(let ((k (jolt-error-continuation v)))
|
||||
(and k
|
||||
(let ((recs (jolt-frame-records k)))
|
||||
(and (pair? recs) (jolt-render-recs recs))))))))
|
||||
(and (pair? recs) (jolt-render-recs recs))))
|
||||
|
||||
(define (jolt-backtrace-string v)
|
||||
(or (jolt-history-backtrace)
|
||||
(let ((k (jolt-error-continuation v)))
|
||||
(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:
|
||||
;; 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.
|
||||
(try (apply-project! (deps/resolve-project (project-dir)))
|
||||
(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"))
|
||||
(loop []
|
||||
(let [form (repl-read-form)]
|
||||
|
|
@ -160,7 +163,9 @@
|
|||
(catch :default e
|
||||
(println "error:" (or (ex-message e)
|
||||
(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)))))))
|
||||
|
||||
;; 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)))
|
||||
(in-ns (symbol ns-str)))
|
||||
(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))
|
||||
:out out
|
||||
:ns (str (ns-name *ns*))
|
||||
|
|
@ -277,6 +280,11 @@
|
|||
no-op."
|
||||
([port] (start port nil))
|
||||
([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 [])))
|
||||
fd (listen-socket port) ; throws on bind/listen failure
|
||||
stopped (atom false)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue