errors: user-readable messages and stack traces (jolt-2o7 rounds 1+2)
Before: (+ 1 "a") printed 'could not find method :+ for 1 or :r+ for "a"'
followed by three janet frames pointing at jolt internals. After:
Error: Cannot add 1 and "a" — + expects numbers
at app.deep/level3
Round 1 — compiled fns carry their Clojure identity:
- The analyzer's recur target (which doubles as the compiled janet fn's
name) is now ns/fn-name (_r$app.deep/level3--N), so janet stack traces
name the user's fns; defn passes the self-name through to fn.
- eval-toplevel re-raises with propagate instead of protect+error — the
failing fiber's stack was being discarded, which is why every trace began
at eval-toplevel.
- require/maybe-require-ns route loaded namespaces through the loader's
compile-or-interpret eval-toplevel via a ctx hook (the evaluator can't
import the loader). Previously REQUIRED namespaces always ran interpreted:
slower, and their fns were anonymous in traces.
Round 2 — report-error presents for users (rephrase-inspired):
- The full trace text is stashed at the innermost eval-toplevel boundary
(janet's debug/stacktrace walks the fiber propagation chain; debug/stack
cannot), then filtered: _r$ frames demangled to ns/fn-name, jolt-internal
and [eval] frames dropped. JOLT_DEBUG=1 restores the raw janet trace.
- Message rewrites: janet arithmetic dispatch -> 'Cannot add X and Y — +
expects numbers'; compiled arity -> Clojure's 'Wrong number of args (N)
passed to: ns/fn'; nil-call gets an undefined-symbol hint (round 3 will
fix resolution properly).
6 cli-test rows assert the exact user-visible output. Gate green, suite
4718 steady, bench within noise.
This commit is contained in:
parent
a40c715ade
commit
e0442f84e4
7 changed files with 162 additions and 12 deletions
|
|
@ -342,7 +342,9 @@
|
||||||
(let [body (if (and (seq body) (string? (first body))) (rest body) body)
|
(let [body (if (and (seq body) (string? (first body))) (rest body) body)
|
||||||
body (if (and (seq body) (map? (first body)) (not (symbol? (first body))))
|
body (if (and (seq body) (map? (first body)) (not (symbol? (first body))))
|
||||||
(rest body) body)]
|
(rest body) body)]
|
||||||
`(def ~fn-name (fn ~@body))))
|
;; pass the name through to fn: the compiled fn's janet name carries it,
|
||||||
|
;; so stack traces read app.deep/level3 instead of a gensym (jolt-2o7.1)
|
||||||
|
`(def ~fn-name (fn ~fn-name ~@body))))
|
||||||
|
|
||||||
;; Jolt doesn't enforce privacy, so defn- is just defn (matching how Clojure's own
|
;; Jolt doesn't enforce privacy, so defn- is just defn (matching how Clojure's own
|
||||||
;; defn- delegates to defn with :private metadata).
|
;; defn- delegates to defn with :private metadata).
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,12 @@
|
||||||
;; Always a recur target, variadic included: the back end gives the rest
|
;; Always a recur target, variadic included: the back end gives the rest
|
||||||
;; param an ordinary positional slot (holding the collected seq), so recur
|
;; param an ordinary positional slot (holding the collected seq), so recur
|
||||||
;; is a self-call carrying the rest seq directly — Clojure semantics.
|
;; is a self-call carrying the rest seq directly — Clojure semantics.
|
||||||
rname (gen-name "arity")
|
;; The recur target doubles as the COMPILED FN'S NAME, which is what a
|
||||||
|
;; janet stack trace shows — so carry the Clojure ns/fn-name (jolt-2o7.1):
|
||||||
|
;; an error inside app.deep/level3 traces as _r$app.deep/level3--N
|
||||||
|
;; (report-error demangles the _r$/--N wrapper). gen-name's counter
|
||||||
|
;; keeps recur targets unique per compilation unit.
|
||||||
|
rname (gen-name (str (compile-ns ctx) "/" (or fn-name "fn") "--"))
|
||||||
names (cond-> (vec fixed) rst (conj rst) fn-name (conj fn-name))
|
names (cond-> (vec fixed) rst (conj rst) fn-name (conj fn-name))
|
||||||
env* (-> (add-locals env names) (with-recur rname))
|
env* (-> (add-locals env names) (with-recur rname))
|
||||||
arity {:params fixed :recur-name rname
|
arity {:params fixed :recur-name rname
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,11 @@
|
||||||
(install-async! ctx)
|
(install-async! ctx)
|
||||||
# Host contract (ns jolt.host): the seam the portable jolt-core compiler calls.
|
# Host contract (ns jolt.host): the seam the portable jolt-core compiler calls.
|
||||||
(host/install! ctx)
|
(host/install! ctx)
|
||||||
|
# require/maybe-require-ns route loaded namespaces through the loader's
|
||||||
|
# compile-or-interpret eval-toplevel (the evaluator can't import the loader
|
||||||
|
# — that would be circular — so it reads this hook). Without it, required
|
||||||
|
# namespaces ran interpreted-only.
|
||||||
|
(put (ctx :env) :toplevel-eval eval-toplevel)
|
||||||
# Stateful primitives as ctx-capturing clojure.core fns (protocol-dispatch,
|
# Stateful primitives as ctx-capturing clojure.core fns (protocol-dispatch,
|
||||||
# register-method, …) — so the protocol macros compile to plain invokes. Must
|
# register-method, …) — so the protocol macros compile to plain invokes. Must
|
||||||
# precede the overlay (its defprotocol/extend-type expansions call these).
|
# precede the overlay (its defprotocol/extend-type expansions call these).
|
||||||
|
|
|
||||||
|
|
@ -353,13 +353,19 @@
|
||||||
found))
|
found))
|
||||||
|
|
||||||
(defn- load-ns-source
|
(defn- load-ns-source
|
||||||
"Parse and evaluate every form of a namespace's source in the given context."
|
"Parse and evaluate every form of a namespace's source in the given context.
|
||||||
|
Routes through the loader's eval-toplevel when the api has installed it
|
||||||
|
(the :toplevel-eval hook) so REQUIRED namespaces compile like everything
|
||||||
|
else — without it they ran interpreted-only: slower, and their fns were
|
||||||
|
anonymous closures in stack traces (jolt-2o7.1)."
|
||||||
[ctx src]
|
[ctx src]
|
||||||
|
(def toplevel (get (ctx :env) :toplevel-eval))
|
||||||
(var s src)
|
(var s src)
|
||||||
(while (> (length (string/trim s)) 0)
|
(while (> (length (string/trim s)) 0)
|
||||||
(def [f r] (parse-next s))
|
(def [f r] (parse-next s))
|
||||||
(set s r)
|
(set s r)
|
||||||
(when (not (nil? f)) (eval-form ctx @{} f))))
|
(when (not (nil? f))
|
||||||
|
(if toplevel (toplevel ctx f) (eval-form ctx @{} f)))))
|
||||||
|
|
||||||
(defn- maybe-require-ns
|
(defn- maybe-require-ns
|
||||||
"If namespace ns-name isn't populated yet, load its source — from a file on the
|
"If namespace ns-name isn't populated yet, load its source — from a file on the
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,22 @@
|
||||||
(try-compile)
|
(try-compile)
|
||||||
(eval-form ctx @{} form)))
|
(eval-form ctx @{} form)))
|
||||||
(eval-form ctx @{} form)))
|
(eval-form ctx @{} form)))
|
||||||
(def res (protect (run)))
|
(try
|
||||||
(if (res 0)
|
(run)
|
||||||
(res 1)
|
([err fib]
|
||||||
(do (ctx-set-current-ns ctx entry-ns)
|
(ctx-set-current-ns ctx entry-ns)
|
||||||
(error (res 1)))))
|
# Stash the full trace TEXT at this innermost boundary: janet's
|
||||||
|
# debug/stacktrace walks the propagation chain (fiber->child, no public
|
||||||
|
# accessor), so this is the only place the user's compiled frames
|
||||||
|
# (in _r$ns/fn--N ...) are reachable. Innermost capture wins; the CLI's
|
||||||
|
# report-error filters + demangles it. (jolt-2o7.1/2)
|
||||||
|
(when (nil? (get (ctx :env) :error-trace))
|
||||||
|
(def buf @"")
|
||||||
|
(with-dyns [:err buf] (debug/stacktrace fib err ""))
|
||||||
|
(put (ctx :env) :error-trace (string buf)))
|
||||||
|
# propagate (not error): re-raising with `error` discards the failing
|
||||||
|
# fiber's stack
|
||||||
|
(propagate err fib))))
|
||||||
|
|
||||||
(defn load-ns
|
(defn load-ns
|
||||||
"Load a Clojure namespace from a .clj file. Per-form routing (compile-or-
|
"Load a Clojure namespace from a .clj file. Per-form routing (compile-or-
|
||||||
|
|
|
||||||
|
|
@ -226,10 +226,100 @@
|
||||||
(string? err) err
|
(string? err) err
|
||||||
(string/format "%q" err)))
|
(string/format "%q" err)))
|
||||||
|
|
||||||
|
# --- error presentation (jolt-2o7.2, rephrase-inspired) ----------------------
|
||||||
|
# Host messages rewritten into Clojure-shaped ones; stack frames filtered to
|
||||||
|
# the USER'S code (compiled jolt fns carry _r$ns/name--N janet names — round
|
||||||
|
# 2o7.1). JOLT_DEBUG=1 restores the raw janet trace for jolt development.
|
||||||
|
|
||||||
|
(defn- demangle
|
||||||
|
"_r$app.deep/level3--105 -> app.deep/level3 (nil for non-jolt names)."
|
||||||
|
[nm]
|
||||||
|
(when (string/has-prefix? "_r$" nm)
|
||||||
|
(def s (string/slice nm 3))
|
||||||
|
# the counter suffix is the LAST --N run
|
||||||
|
(var cut (length s))
|
||||||
|
(var i (string/find "--" s))
|
||||||
|
(while (not (nil? i))
|
||||||
|
(set cut i)
|
||||||
|
(set i (string/find "--" s (+ i 1))))
|
||||||
|
(string/slice s 0 cut)))
|
||||||
|
|
||||||
|
(defn- fmt-val [x]
|
||||||
|
(cond
|
||||||
|
(string? x) (string `"` x `"`)
|
||||||
|
(nil? x) "nil"
|
||||||
|
(string x)))
|
||||||
|
|
||||||
|
(def- op-words
|
||||||
|
{"+" "add" "-" "subtract" "*" "multiply" "/" "divide"
|
||||||
|
"<" "compare" ">" "compare" "<=" "compare" ">=" "compare"})
|
||||||
|
|
||||||
|
(defn- rewrite-message
|
||||||
|
"Host/janet error text -> a user-facing message. Unknown messages verbatim."
|
||||||
|
[msg]
|
||||||
|
(def msg (string msg))
|
||||||
|
(cond
|
||||||
|
# janet polymorphic arithmetic: could not find method :+ for 1 or :r+ for "a"
|
||||||
|
(string/has-prefix? "could not find method :" msg)
|
||||||
|
(let [rest* (string/slice msg (length "could not find method :"))
|
||||||
|
sp (string/find " " rest*)
|
||||||
|
op (string/slice rest* 0 sp)
|
||||||
|
tail (string/slice rest* (+ sp (length " for ")))
|
||||||
|
orpos (string/find " or :r" tail)
|
||||||
|
a (string/slice tail 0 orpos)
|
||||||
|
forpos (string/find " for " tail (+ orpos 1))
|
||||||
|
b (string/slice tail (+ forpos 5))]
|
||||||
|
(string "Cannot " (get op-words op op) " " a " and " b
|
||||||
|
" — " op " expects numbers"))
|
||||||
|
# janet fixed-arity: <function _r$ns/f--N> called with 2 arguments, expected 1
|
||||||
|
(and (string/has-prefix? "<function " msg) (string/find "> called with " msg))
|
||||||
|
(let [nm-end (string/find ">" msg)
|
||||||
|
nm (string/slice msg (length "<function ") nm-end)
|
||||||
|
pretty (or (demangle nm) nm)
|
||||||
|
tail (string/slice msg (+ nm-end (length "> called with ")))
|
||||||
|
n-end (string/find " " tail)
|
||||||
|
n (string/slice tail 0 n-end)
|
||||||
|
exp (if-let [i (string/find "expected " tail)]
|
||||||
|
(string " (expected " (string/slice tail (+ i 9)) ")") "")]
|
||||||
|
(string "Wrong number of args (" n ") passed to: " pretty exp))
|
||||||
|
# a typo'd symbol compiles to nil today (round 2o7.3 will fix resolution)
|
||||||
|
(= msg "Cannot call nil as a function")
|
||||||
|
(string msg " — often an undefined (misspelled?) symbol")
|
||||||
|
msg))
|
||||||
|
|
||||||
|
(defn- print-user-trace
|
||||||
|
"Filter the stashed janet trace down to frames a jolt user can act on:
|
||||||
|
compiled jolt fns (the _r$ns/name--N janet names, demangled) and frames
|
||||||
|
from non-jolt source files. Internal janet/jolt frames are dropped."
|
||||||
|
[trace]
|
||||||
|
(var shown 0)
|
||||||
|
(each line (string/split "\n" trace)
|
||||||
|
(def t (string/trim line))
|
||||||
|
(when (string/has-prefix? "in " t)
|
||||||
|
(def rest* (string/slice t 3))
|
||||||
|
(def sp (or (string/find " " rest*) (length rest*)))
|
||||||
|
(def nm (string/slice rest* 0 sp))
|
||||||
|
(cond
|
||||||
|
(string/has-prefix? "_r$" nm)
|
||||||
|
(do (eprint " at " (demangle nm)) (++ shown))
|
||||||
|
# a frame from a real source file outside jolt internals
|
||||||
|
(and (string/find "[" rest*)
|
||||||
|
(not (string/find "src/jolt/" rest*))
|
||||||
|
(not (string/find "boot.janet" rest*))
|
||||||
|
(not (string/find "[eval]" rest*)))
|
||||||
|
(do (eprint " " t) (++ shown))
|
||||||
|
nil)))
|
||||||
|
shown)
|
||||||
|
|
||||||
(defn- report-error [err fib]
|
(defn- report-error [err fib]
|
||||||
(eprint "Error: " (err-message err))
|
(eprint "Error: " (rewrite-message (err-message err)))
|
||||||
# Janet-level stack trace of where evaluation failed
|
(def stashed (get (ctx :env) :error-trace))
|
||||||
(when fib (debug/stacktrace fib "")))
|
(put (ctx :env) :error-trace nil)
|
||||||
|
(cond
|
||||||
|
(os/getenv "JOLT_DEBUG")
|
||||||
|
(if stashed (eprin stashed) (when fib (debug/stacktrace fib "")))
|
||||||
|
stashed (print-user-trace stashed)
|
||||||
|
fib (when fib nil)))
|
||||||
|
|
||||||
(defn- run-repl []
|
(defn- run-repl []
|
||||||
(print "Jolt — Clojure on Janet")
|
(print "Jolt — Clojure on Janet")
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@
|
||||||
(os/proc-wait p)
|
(os/proc-wait p)
|
||||||
(string (or out "")))
|
(string (or out "")))
|
||||||
|
|
||||||
|
(defn- run-err [& args]
|
||||||
|
(def p (os/spawn ["janet" "src/jolt/main.janet" ;args] :p {:out :pipe :err :pipe}))
|
||||||
|
(def err (:read (p :err) :all))
|
||||||
|
(os/proc-wait p)
|
||||||
|
(string (or err "")))
|
||||||
|
|
||||||
(var fails 0)
|
(var fails 0)
|
||||||
(defn check [label got pred]
|
(defn check [label got pred]
|
||||||
(if (pred got) (print " ok " label)
|
(if (pred got) (print " ok " label)
|
||||||
|
|
@ -34,3 +40,28 @@
|
||||||
(if (> fails 0)
|
(if (> fails 0)
|
||||||
(error (string "cli-test: " fails " failing check(s)"))
|
(error (string "cli-test: " fails " failing check(s)"))
|
||||||
(print "\nAll CLI tests passed!"))
|
(print "\nAll CLI tests passed!"))
|
||||||
|
|
||||||
|
# --- user-facing error output (jolt-2o7 rounds 1+2) --------------------------
|
||||||
|
# Messages are Clojure-shaped; traces show the USER'S fns (compiled fn names
|
||||||
|
# carry ns/fn-name, demangled by report-error) and never jolt internals.
|
||||||
|
(check "arith error message rewritten"
|
||||||
|
(run-err "-e" `(+ 1 "a")`)
|
||||||
|
(has `Cannot add 1 and "a"`))
|
||||||
|
(check "arity error names the fn"
|
||||||
|
(run-err "-e" "(defn afn [x] x) (afn 1 2)")
|
||||||
|
(has "Wrong number of args (2) passed to: user/afn"))
|
||||||
|
(check "nil-call hints at undefined symbol"
|
||||||
|
(run-err "-e" "(undefined-fn 1)")
|
||||||
|
(has "undefined (misspelled?) symbol"))
|
||||||
|
(check "trace shows the user's call chain"
|
||||||
|
(run-err "-e" "(defn inner [x] (let [r (+ x :k)] r)) (defn outer [x] (let [v (inner x)] v)) (outer 1)")
|
||||||
|
(fn [s] (and (string/find "at user/inner" s) (string/find "at user/outer" s))))
|
||||||
|
(check "no jolt-internal frames in user errors"
|
||||||
|
(run-err "-e" `(+ 1 "a")`)
|
||||||
|
(fn [s] (nil? (string/find "src/jolt/" s))))
|
||||||
|
(check "JOLT_DEBUG restores the raw trace"
|
||||||
|
(do (os/setenv "JOLT_DEBUG" "1")
|
||||||
|
(def r (run-err "-e" `(+ 1 "a")`))
|
||||||
|
(os/setenv "JOLT_DEBUG" nil)
|
||||||
|
r)
|
||||||
|
(has "could not find method"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue