diff --git a/src/jolt/api.janet b/src/jolt/api.janet index 7683991..818f2ff 100644 --- a/src/jolt/api.janet +++ b/src/jolt/api.janet @@ -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-forms-positioned ctx (parse-all-positioned s) file)) diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index 34bfbf5..fab2990 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -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 "") (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. diff --git a/src/jolt/loader.janet b/src/jolt/loader.janet index fa3ea3d..2f5a77e 100644 --- a/src/jolt/loader.janet +++ b/src/jolt/loader.janet @@ -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,28 +112,21 @@ (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) - # Extract ns name from the first ns form - (when (and (nil? ns-name) - (array? form) - (> (length form) 0) - (and (struct? (first form)) - (= :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))))))) + (each [form _] pairs + # Extract ns name from the first ns form + (when (and (nil? ns-name) + (array? form) + (> (length form) 0) + (and (struct? (first form)) + (= :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)))))) (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) diff --git a/src/jolt/main.janet b/src/jolt/main.janet index 9ddbaa3..40637a1 100644 --- a/src/jolt/main.janet +++ b/src/jolt/main.janet @@ -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) + # :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) "") (= (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 "") (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] diff --git a/src/jolt/reader.janet b/src/jolt/reader.janet index 3bf04fa..9c7908a 100644 --- a/src/jolt/reader.janet +++ b/src/jolt/reader.janet @@ -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) diff --git a/test/integration/cli-test.janet b/test/integration/cli-test.janet index 8e23317..e7127b0 100644 --- a/test/integration/cli-test.janet +++ b/test/integration/cli-test.janet @@ -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 position on one-liners" + (run-err "-e" `(+ 1 "a")`) + (fn [s] (nil? (string/find "" 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!"))