errors: load errors carry file:line and the require chain (jolt-2o7.4)
A failing top-level form now reports where it lives:
Error: Cannot add 1 and "boom" — + expects numbers
at /path/src/app/broken.clj:3
while loading /path/src/app/mid.clj
while loading /path/src/app/top.clj
The reader has no per-form positions (round 5), but the loaders know
exactly which slice of source each form came from: parse-all-positioned
(reader) counts newlines around parse-next and returns [form line]
pairs; load-ns, load-string and load-ns-source evaluate through a
positioned loop that on error stashes the innermost form's {:file
:line} on the env and appends each file unwound through to a loading
chain. report-error prints both, suppressing the synthetic <eval>
strings the CLI feeds itself (the require/apply one-liners).
load-string takes an optional file arg; run-file passes the script
path so script errors name the script. cli-test rows cover the
3-requires-deep case, script files, and that one-line -e output stays
clean. Gate green, suite 4718 steady, bench within noise.
This commit is contained in:
parent
32f733b6f6
commit
6d082e9b1d
6 changed files with 133 additions and 43 deletions
|
|
@ -324,13 +324,7 @@
|
|||
"Evaluate all forms from a Clojure source string.
|
||||
Uses parse-next to load every top-level form in sequence.
|
||||
Returns the result of the last form evaluated."
|
||||
[ctx s]
|
||||
(var cur s)
|
||||
(var result nil)
|
||||
(while (> (length (string/trim cur)) 0)
|
||||
(def [form rest] (parse-next cur))
|
||||
(set cur rest)
|
||||
(when (not (nil? form))
|
||||
(set result (eval-one ctx form))))
|
||||
result)
|
||||
[ctx s &opt file]
|
||||
(default file "<eval>")
|
||||
(eval-forms-positioned ctx (parse-all-positioned s) file))
|
||||
|
||||
|
|
|
|||
|
|
@ -358,14 +358,23 @@
|
|||
(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 &opt file]
|
||||
(default file "<source>")
|
||||
(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))
|
||||
(if toplevel (toplevel ctx f) (eval-form ctx @{} f)))))
|
||||
(each [f line] (parse-all-positioned src)
|
||||
(try
|
||||
(if toplevel (toplevel ctx f) (eval-form ctx @{} f))
|
||||
([err fib]
|
||||
# innermost failing form wins; files unwound through form the
|
||||
# 'while loading …' chain (mirrors loader/eval-forms-positioned,
|
||||
# which this can't import — circularity) (jolt-2o7.4)
|
||||
(def env (ctx :env))
|
||||
(when (nil? (get env :error-pos))
|
||||
(put env :error-pos {:file file :line line}))
|
||||
(when (nil? (get env :error-loading)) (put env :error-loading @[]))
|
||||
(def chain (get env :error-loading))
|
||||
(when (not= (last chain) file) (array/push chain file))
|
||||
(propagate err fib)))))
|
||||
|
||||
(defn- maybe-require-ns
|
||||
"If namespace ns-name isn't populated yet, load its source — from a file on the
|
||||
|
|
@ -395,7 +404,9 @@
|
|||
# Stdlib files have no `ns` form, so switch into the target ns first
|
||||
# (their defs intern there); a library's own `ns` form overrides this.
|
||||
(ctx-set-current-ns ctx ns-name)
|
||||
(if path (load-ns-source ctx (slurp path)) (load-ns-source ctx embedded))
|
||||
(if path
|
||||
(load-ns-source ctx (slurp path) path)
|
||||
(load-ns-source ctx embedded (string ns-name " (stdlib)")))
|
||||
# Record load order for tooling (uberscript): a dependency finishes
|
||||
# loading before its requirer, so this is topological. Skip the
|
||||
# baked-in stdlib — it's part of the runtime, not something to bundle.
|
||||
|
|
|
|||
|
|
@ -84,6 +84,27 @@
|
|||
res)
|
||||
(eval-toplevel-1 ctx form)))
|
||||
|
||||
(defn eval-forms-positioned
|
||||
"Evaluate parsed [form line] pairs, recording WHERE an error happened: the
|
||||
innermost failing form's {:file :line} goes to (env :error-pos) and each
|
||||
file unwound through joins the (env :error-loading) chain — the CLI's
|
||||
report-error prints 'at file:line' and 'while loading …' from these.
|
||||
(jolt-2o7.4)"
|
||||
[ctx pairs file]
|
||||
(var res nil)
|
||||
(each [form line] pairs
|
||||
(try
|
||||
(set res (eval-toplevel ctx form))
|
||||
([err fib]
|
||||
(def env (ctx :env))
|
||||
(when (nil? (get env :error-pos))
|
||||
(put env :error-pos {:file file :line line}))
|
||||
(when (nil? (get env :error-loading)) (put env :error-loading @[]))
|
||||
(def chain (get env :error-loading))
|
||||
(when (not= (last chain) file) (array/push chain file))
|
||||
(propagate err fib))))
|
||||
res)
|
||||
|
||||
(defn load-ns
|
||||
"Load a Clojure namespace from a .clj file. Per-form routing (compile-or-
|
||||
interpret, stateful forms interpret) is shared with eval-one via eval-toplevel.
|
||||
|
|
@ -91,16 +112,9 @@
|
|||
(load-ns ctx filepath) → namespace symbol string"
|
||||
[ctx filepath]
|
||||
(def source (slurp filepath))
|
||||
(def pairs (parse-all-positioned source))
|
||||
(var ns-name nil)
|
||||
(var remaining source)
|
||||
(var forms @[])
|
||||
|
||||
# Parse all forms
|
||||
(while (> (length (string/trim remaining)) 0)
|
||||
(def [form rest] (parse-next remaining))
|
||||
(set remaining rest)
|
||||
(when (not (nil? form))
|
||||
(array/push forms form)
|
||||
(each [form _] pairs
|
||||
# Extract ns name from the first ns form
|
||||
(when (and (nil? ns-name)
|
||||
(array? form)
|
||||
|
|
@ -109,10 +123,10 @@
|
|||
(= :symbol ((first form) :jolt/type))
|
||||
(= "ns" ((first form) :name))))
|
||||
(let [name-form (in form 1)]
|
||||
(set ns-name (if (struct? name-form) (name-form :name) (string name-form)))))))
|
||||
(set ns-name (if (struct? name-form) (name-form :name) (string name-form))))))
|
||||
|
||||
(when (nil? ns-name)
|
||||
(error (string "No ns form found in " filepath)))
|
||||
|
||||
(each form forms (eval-toplevel ctx form))
|
||||
(eval-forms-positioned ctx pairs filepath)
|
||||
ns-name)
|
||||
|
|
|
|||
|
|
@ -313,13 +313,28 @@
|
|||
|
||||
(defn- report-error [err fib]
|
||||
(eprint "Error: " (rewrite-message (err-message err)))
|
||||
(def stashed (get (ctx :env) :error-trace))
|
||||
(put (ctx :env) :error-trace nil)
|
||||
(def env (ctx :env))
|
||||
(def stashed (get env :error-trace))
|
||||
(def pos (get env :error-pos))
|
||||
(def chain (get env :error-loading))
|
||||
(put env :error-trace nil)
|
||||
(put env :error-pos nil)
|
||||
(put env :error-loading nil)
|
||||
# <eval>:1 is the synthetic require/apply string the CLI feeds itself (and
|
||||
# any one-line -e) — no information there
|
||||
(when (and pos (not (and (= (pos :file) "<eval>") (= (pos :line) 1))))
|
||||
(eprint " at " (pos :file) ":" (pos :line)))
|
||||
(cond
|
||||
(os/getenv "JOLT_DEBUG")
|
||||
(if stashed (eprin stashed) (when fib (debug/stacktrace fib "")))
|
||||
stashed (print-user-trace stashed)
|
||||
fib (when fib nil)))
|
||||
fib (when fib nil))
|
||||
# requires unwound through, innermost first; the failing file itself is
|
||||
# already on the 'at' line
|
||||
(when chain
|
||||
(each f chain
|
||||
(unless (or (= f "<eval>") (and pos (= f (pos :file))))
|
||||
(eprint " while loading " f)))))
|
||||
|
||||
(defn- run-repl []
|
||||
(print "Jolt — Clojure on Janet")
|
||||
|
|
@ -360,7 +375,7 @@
|
|||
(do (eprint "Error: file not found: " path) (os/exit 1))
|
||||
(let [src (slurp path)]
|
||||
(try
|
||||
(load-string ctx src)
|
||||
(load-string ctx src path)
|
||||
([err fib] (report-error err fib) (os/exit 1))))))
|
||||
|
||||
(defn- run-eval [expr argv]
|
||||
|
|
|
|||
|
|
@ -651,3 +651,35 @@
|
|||
(parse-next-loop new-pos)
|
||||
[form (string/slice s new-pos)])))
|
||||
(parse-next-loop 0))
|
||||
|
||||
(defn parse-all-positioned
|
||||
"Parse every top-level form of source, returning an array of [form line]
|
||||
where line is the 1-based source line the form starts on. parse-next eats
|
||||
leading trivia itself, so the form's start line is the running newline
|
||||
count plus the newlines in the trivia (whitespace, commas, ; comments)
|
||||
ahead of it. (jolt-2o7.4)"
|
||||
[source]
|
||||
(def out @[])
|
||||
(var s source)
|
||||
(var line 1)
|
||||
(while (> (length (string/trim s)) 0)
|
||||
# newlines in the leading trivia belong BEFORE the form's line
|
||||
(var i 0)
|
||||
(def n (length s))
|
||||
(var scanning true)
|
||||
(while (and scanning (< i n))
|
||||
(def c (in s i))
|
||||
(cond
|
||||
(= c (chr "\n")) (do (++ line) (++ i))
|
||||
(or (= c (chr " ")) (= c (chr "\t")) (= c (chr "\r")) (= c (chr ","))) (++ i)
|
||||
(= c (chr ";")) (while (and (< i n) (not= (in s i) (chr "\n"))) (++ i))
|
||||
(set scanning false)))
|
||||
(def [form rest*] (parse-next s))
|
||||
(def consumed (- (length s) (length rest*)))
|
||||
(def form-line line)
|
||||
# count newlines inside the consumed chunk past the trivia
|
||||
(loop [j :range [i consumed]]
|
||||
(when (= (in s j) (chr "\n")) (++ line)))
|
||||
(set s rest*)
|
||||
(when (not (nil? form)) (array/push out [form form-line])))
|
||||
out)
|
||||
|
|
|
|||
|
|
@ -37,9 +37,7 @@
|
|||
(os/setenv "JOLT_PATH" nil)
|
||||
(os/rm (string tmp "/app/core.clj")) (os/rmdir (string tmp "/app")) (os/rmdir tmp)
|
||||
|
||||
(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
|
||||
|
|
@ -66,9 +64,35 @@
|
|||
(check "no jolt-internal frames in user errors"
|
||||
(run-err "-e" `(+ 1 "a")`)
|
||||
(fn [s] (nil? (string/find "src/jolt/" s))))
|
||||
# --- round 4: load errors carry file:line + the require chain ---------------
|
||||
(def r4 (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-cli-r4-" (os/time)))
|
||||
(os/mkdir r4) (os/mkdir (string r4 "/app"))
|
||||
(spit (string r4 "/app/broken.clj") "(ns app.broken)\n\n(def config\n (+ 1 \"boom\"))\n")
|
||||
(spit (string r4 "/app/mid.clj") "(ns app.mid (:require [app.broken :as b]))\n")
|
||||
(spit (string r4 "/app/top.clj") "(ns app.top (:require [app.mid :as m]))\n(defn -main [& a] nil)\n")
|
||||
(os/setenv "JOLT_PATH" r4)
|
||||
(def deep-err (run-err "-m" "app.top"))
|
||||
(check "load error names the failing file:line" deep-err
|
||||
(has "at " ))
|
||||
(check "load error points into broken.clj line 3" deep-err
|
||||
(has "/app/broken.clj:3"))
|
||||
(check "require chain shows the loading path" deep-err
|
||||
(fn [s] (and (string/find "while loading" s) (string/find "/app/mid.clj" s) (string/find "/app/top.clj" s))))
|
||||
(check "script errors name the script file"
|
||||
(do (spit (string r4 "/scr.clj") "(ns scr)\n\n(+ 1 \"x\")\n")
|
||||
(run-err (string r4 "/scr.clj")))
|
||||
(has "/scr.clj:3"))
|
||||
(check "no synthetic <eval> position on one-liners"
|
||||
(run-err "-e" `(+ 1 "a")`)
|
||||
(fn [s] (nil? (string/find "<eval>" 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"))
|
||||
|
||||
(if (> fails 0)
|
||||
(error (string "cli-test: " fails " failing check(s)"))
|
||||
(print "\nAll CLI tests passed!"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue