Merge pull request #87 from jolt-lang/error-rounds-1-2
errors: user-readable messages and stack traces (rounds 1+2)
This commit is contained in:
commit
3fd0af33b6
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)
|
||||
body (if (and (seq body) (map? (first body)) (not (symbol? (first 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
|
||||
;; defn- delegates to defn with :private metadata).
|
||||
|
|
|
|||
|
|
@ -81,7 +81,12 @@
|
|||
;; Always a recur target, variadic included: the back end gives the rest
|
||||
;; param an ordinary positional slot (holding the collected seq), so recur
|
||||
;; 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))
|
||||
env* (-> (add-locals env names) (with-recur rname))
|
||||
arity {:params fixed :recur-name rname
|
||||
|
|
|
|||
|
|
@ -154,6 +154,11 @@
|
|||
(install-async! ctx)
|
||||
# Host contract (ns jolt.host): the seam the portable jolt-core compiler calls.
|
||||
(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,
|
||||
# register-method, …) — so the protocol macros compile to plain invokes. Must
|
||||
# precede the overlay (its defprotocol/extend-type expansions call these).
|
||||
|
|
|
|||
|
|
@ -353,13 +353,19 @@
|
|||
found))
|
||||
|
||||
(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]
|
||||
(def toplevel (get (ctx :env) :toplevel-eval))
|
||||
(var s src)
|
||||
(while (> (length (string/trim s)) 0)
|
||||
(def [f r] (parse-next s))
|
||||
(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
|
||||
"If namespace ns-name isn't populated yet, load its source — from a file on the
|
||||
|
|
|
|||
|
|
@ -52,11 +52,22 @@
|
|||
(try-compile)
|
||||
(eval-form ctx @{} form)))
|
||||
(eval-form ctx @{} form)))
|
||||
(def res (protect (run)))
|
||||
(if (res 0)
|
||||
(res 1)
|
||||
(do (ctx-set-current-ns ctx entry-ns)
|
||||
(error (res 1)))))
|
||||
(try
|
||||
(run)
|
||||
([err fib]
|
||||
(ctx-set-current-ns ctx entry-ns)
|
||||
# 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
|
||||
"Load a Clojure namespace from a .clj file. Per-form routing (compile-or-
|
||||
|
|
|
|||
|
|
@ -226,10 +226,100 @@
|
|||
(string? err) 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]
|
||||
(eprint "Error: " (err-message err))
|
||||
# Janet-level stack trace of where evaluation failed
|
||||
(when fib (debug/stacktrace fib "")))
|
||||
(eprint "Error: " (rewrite-message (err-message err)))
|
||||
(def stashed (get (ctx :env) :error-trace))
|
||||
(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 []
|
||||
(print "Jolt — Clojure on Janet")
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
(os/proc-wait p)
|
||||
(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)
|
||||
(defn check [label got pred]
|
||||
(if (pred got) (print " ok " label)
|
||||
|
|
@ -34,3 +40,28 @@
|
|||
(if (> fails 0)
|
||||
(error (string "cli-test: " fails " failing check(s)"))
|
||||
(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