Report source location on uncaught errors

Each top-level form records its source position (thread-local) before it
compiles+evals, and cli.ss jolt-report-uncaught appends 'at file:line:col'
when an error propagates out. Covers joltc -e, joltc run <file>, and
load-string — every interpreted path. Top-level granularity, one set per
form; deeper frames come from the Phase 2 frame walk.

Runtime .ss only, no re-mint.
This commit is contained in:
Yogthos 2026-06-25 21:18:51 -04:00
parent 824fb347a3
commit 9bf3d1c80e
3 changed files with 43 additions and 0 deletions

View file

@ -53,6 +53,9 @@
(display "Unhandled exception: " port)
(display (if (condition? v) (with-output-to-string (lambda () (display-condition v))) (jolt-pr-str v)) port)
(newline port)))
;; The top-level form that was evaluating when this propagated (file:line:col).
(let ((loc (jolt-current-source-string)))
(when loc (display " at " port) (display loc port) (newline port)))
(exit 1)))
(guard (v (#t (jolt-report-uncaught v)))

View file

@ -20,6 +20,29 @@
;; whose data conversion would turn those into real sets.
(define jolt-ce-read jolt-read-form-raw)
;; --- current source location ------------------------------------------------
;; The position of the top-level form currently compiling/evaluating, so an
;; uncaught error can report where it came from (cli.ss jolt-report-uncaught).
;; Thread-local: a future/agent worker tracks its own form. Holds #f or a
;; {:line :column :file?} position map (jolt.host/form-position's shape).
;; Top-level granularity — one set per top-level form, nothing per call.
(define jolt-current-source (make-thread-parameter #f))
(define (jolt-enter-form! form)
(let ((p (hc-form-position form)))
(when (pmap? p) (jolt-current-source p))))
;; "file:line:col" / "line:col" for the current form, or #f when none is set.
(define (jolt-current-source-string)
(let ((p (jolt-current-source)))
(and (pmap? p)
(let ((line (jolt-get p hc-kw-line jolt-nil))
(col (jolt-get p hc-kw-column jolt-nil))
(file (jolt-get p hc-kw-file jolt-nil)))
(string-append
(if (jolt-nil? file) "" (string-append file ":"))
(if (jolt-nil? line) "?" (number->string line)) ":"
(if (jolt-nil? col) "?" (number->string col)))))))
;; The spine ALWAYS runs with the full clojure.core prelude loaded, so a clojure.*
;; ref must lower to var-deref (resolved from the prelude), not trip the emitter's
;; "unsupported stdlib fn (no core on Chez yet)" out-of-subset guard — that guard
@ -124,6 +147,9 @@
;; of the expander fn + (mark-macro! …) so subsequent forms expand it. One
;; macro-expansion path (no separate spine interception).
(else
;; record this form's source location first, so a compile- or run-time error
;; in it reports the right place.
(jolt-enter-form! form)
(eval (read (open-input-string (jolt-analyze-emit-form form ns)))
(interaction-environment)))))

View file

@ -18,6 +18,18 @@ check() {
}
pass=0
# An uncaught error reports the source location of the top-level form (stderr).
check_loc() {
err="$(bin/joltc -e "$1" 2>&1 >/dev/null)"
if printf '%s' "$err" | grep -q "$2"; then
pass=$((pass + 1))
else
echo " FAIL (loc): $1"
echo " want stderr to contain \`$2\`, got \`$err\`"
fails=$((fails + 1))
fi
}
check '(+ 1 2)' '3'
check '(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) (fib 15)' '610'
check '(->> (range 10) (filter even?) (map (fn [x] (* x x))) (reduce +))' '120'
@ -31,6 +43,8 @@ check '(deref (future (+ 1 2)))' '3'
check '(/ 1 2)' '1/2'
check '(= 3 3.0)' 'false'
check '(== 3 3.0)' 'true'
check_loc '(throw (ex-info "boom" {}))' ' at 1:'
check_loc '(do (+ 1 1) (/ 1 0))' ' at 1:'
echo "cli smoke: $pass passed, $fails failed"
[ "$fails" -eq 0 ]