Skip to content

Commit 546a5a9

Browse files
authored
feat: nice exception printing (#14)
1 parent d5d494a commit 546a5a9

File tree

2 files changed

+109
-37
lines changed

2 files changed

+109
-37
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ It is built on top of the Clojure JVM runtime, but the parts that need dynamic c
77
**Features**
88

99
- Starts quickly (it is compiled with GraalVM native-image)
10-
- Small (1K SLOC)
10+
- Small (<1K SLOC)
1111
- Out of the Box [core.async](https://github.com/clojure/core.async) support and also [many other core libraries](https://github.com/erdos/uclj/blob/master/src/uclj/core.clj#L10)
1212

1313
## Usage

src/uclj/core.clj

+108-36
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
[clojure.pprint :as pprint :refer [pprint pp]]
2626
[clojure.set :as set]
2727
[clojure.spec.alpha]
28+
[clojure.stacktrace :as stacktrace]
2829
[clojure.string :as s]
2930
[clojure.test :refer [deftest testing is are]]
3031
[clojure.test.check :as check]
@@ -177,7 +178,7 @@
177178
(meta exp)))
178179

179180
;; else
180-
(map (partial iter &env) expanded))
181+
(with-meta (map (partial iter &env) expanded) (meta exp)))
181182

182183
(or (vector? expanded) (set? expanded) (map? expanded))
183184
(map-coll #(iter &env %) expanded)
@@ -199,6 +200,15 @@
199200
java.util.regex.Pattern (evalme [t _] t)
200201
clojure.lang.Keyword (evalme [t _] t))
201202

203+
(def ^java.util.Map exception-stack (java.util.Collections/synchronizedMap (new java.util.WeakHashMap)))
204+
205+
(defmacro with-err-report [loc expr]
206+
`(try ~expr
207+
(catch Throwable t#
208+
(when-let [loc# ~loc]
209+
(.compute exception-stack t# (reify java.util.function.BiFunction (apply [_# _# v#] (cons loc# v#)))))
210+
(throw t#))))
211+
202212
;; TODO: test with interfaces instead of protocols!
203213
(defmacro gen-eval-node
204214
([m body] `(with-meta (gen-eval-node ~body) ~m))
@@ -232,8 +242,8 @@
232242

233243
(custom-var! #'clojure.core/load-file
234244
(fn [fname]
235-
(binding [*file* (io/file fname)]
236-
((@custom-var-impls #'clojure.core/load-reader) (io/reader *file*)))))
245+
(binding [*file* fname]
246+
((@custom-var-impls #'clojure.core/load-reader) (io/reader (io/file fname))))))
237247

238248
(custom-var! #'clojure.core/load
239249
(fn [& bodies] (throw (new RuntimeException "UCLJ does not yet support clojure.core/load!")))
@@ -265,6 +275,16 @@
265275
(alter-meta! (var *ns*) assoc :dynamic true)
266276
(var-set-reset! (var *ns*) (create-ns new-ns))))
267277

278+
;; Emits a clojure (let[]) expression in which values in let-vals are bound to new local vars
279+
;; and body is result of calling body-fn with the generated var names.
280+
(defn- gen-let-form [let-vals body-fn]
281+
(assert (seq? let-vals))
282+
(let [let-vars (repeatedly (count let-vals) gensym)]
283+
(list 'let*
284+
(vec (interleave let-vars let-vals))
285+
(apply body-fn let-vars))))
286+
287+
;; invocation of core functions is inlined for all arities
268288
(def clojure-core-inlined-fns
269289
(template
270290
(hash-map
@@ -276,30 +296,37 @@
276296
#'clojure.core/alength #'clojure.core/aset} v))
277297
:when (not (@custom-var-impls v))
278298
:when (not (:macro (meta v)))
279-
:let [arglists (var->arglists v)]
299+
:let [arglists (var->arglists v)
300+
+meta (gensym)] ;; symbol will reference metadata of original form
280301
:when arglists]
281302
[v (list* 'fn*
282303
(symbol (str (name (symbol v)) "-inlined"))
283304
(for [args arglists]
284305
(if (= 'variadic (last args))
285-
(list (vec args) (list 'gen-eval-node (concat ['clojure.core/apply (symbol v)]
306+
;; TODO: also add to variadic calls!
307+
(list (vec (cons +meta args)) (list 'gen-eval-node (concat ['clojure.core/apply (symbol v)]
286308
(for [a (butlast (butlast args))] (list 'evalme a '&b))
287309
[(list 'clojure.core/for [(last args) (last args)] (list 'evalme (last args) '&b))])))
288-
(list (vec args) (list 'gen-eval-node (list* (symbol v) (for [a args] (list 'evalme a '&b))))))))])))))
310+
(list (vec (cons +meta args)) (list 'gen-eval-node
311+
(gen-let-form (for [a args] (list 'evalme a '&b))
312+
(fn [& arg-forms] (list 'with-err-report +meta (list* (symbol v) arg-forms)))))))))])))))
289313

290314
(defmethod seq->eval-node ::default seq-eval-call [&a _ s]
291315
(if (empty? s)
292316
(gen-eval-node ())
293-
(let [[f & args] (map (partial ->eval-node &a nil) s)
294-
[a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15] args] ;; TODO: unroll with template!
317+
(let [[f & args :as f+arglists] (map (partial ->eval-node &a nil) s)
318+
[a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15] args ;; TODO: unroll with template!
319+
form-meta (::source-meta (meta s))
320+
form-meta (when form-meta (assoc form-meta :file *file*))]
295321
(dorun args)
296322
(if-let [call-factory (clojure-core-inlined-fns (::var (meta f)))]
297-
(apply call-factory args)
323+
(apply call-factory form-meta args)
298324
(template [a-symbol #(symbol (str 'a %))]
299325
(case (count args)
300326
~@(mapcat seq (for [i (range 16)]
301-
[i (list 'gen-eval-node (list* '.invoke (quote ^clojure.lang.IFn (evalme f &b))
302-
(for [j (range 1 (inc i))] (list 'evalme (a-symbol j) '&b))))]))
327+
[i (list 'gen-eval-node
328+
(gen-let-form (cons (with-meta '(evalme f &b) {:tag 'clojure.lang.IFn}) (for [j (range 1 (inc i))] (list 'evalme (a-symbol j) '&b)))
329+
(fn [f & as] (list 'with-err-report 'form-meta (list* '.invoke f as)))))]))
303330
;; else
304331
(gen-eval-node (apply (evalme f &b) (for [e args] (evalme e &b))))))))))
305332

@@ -407,6 +434,7 @@
407434
`(let [~'enclosed-array-size (int (if ~fname (inc (count ~symbol-used)) (count ~symbol-used)))
408435
body-vararg# (:variadic ~arity->body-node)
409436
body-vararg-symbols# (:variadic ~arity->symbols-introduced)
437+
~'err-meta {:fn (or ~fname "fn$anonymous") :ns *ns*}
410438
[~@(for [i (range (inc max-arity))] (symbol (str 'body i)))] (map ~arity->body-node (range))
411439
[~@(for [i (range (inc max-arity))] (symbol (str 'body i '-symbols)))] (map ~arity->symbols-introduced (range))]
412440
(gen-eval-node
@@ -421,22 +449,24 @@
421449
~'enclosed-array (+ (count ~(symbol (str 'body i '-symbols))) ~'enclosed-array-size))]
422450
~@(for [j (range i)]
423451
(list 'aset 'invocation-array (list '+ j 'enclosed-array-size) (nth arg-symbols j)))
424-
(loop []
425-
(let [result# (evalme ~(symbol (str 'body i)) ~'invocation-array)]
426-
(if (identical? ::recur result#)
427-
(recur)
428-
result#))))))
452+
(with-err-report ~'err-meta
453+
(loop []
454+
(let [result# (evalme ~(symbol (str 'body i)) ~'invocation-array)]
455+
(if (identical? ::recur result#)
456+
(recur)
457+
result#)))))))
429458
([~@(for [i (range max-arity)] (symbol (str 'arg- i))) ~'& arg-rest#]
430459
(assert body-vararg-symbols# "Called with too many parameters!")
431460
(let [~'invocation-array (java.util.Arrays/copyOf ~'enclosed-array (+ (count body-vararg-symbols#) ~'enclosed-array-size))]
432461
~@(for [j (range (+ max-arity))]
433462
(list 'aset 'invocation-array (list '+ j 'enclosed-array-size) (symbol (str 'arg- j))))
434463
(aset ~'invocation-array (+ ~max-arity ~'enclosed-array-size) arg-rest#)
435-
(loop []
436-
(let [result# (evalme body-vararg# ~'invocation-array)]
437-
(if (identical? ::recur result#)
438-
(recur)
439-
result#))))))
464+
(with-err-report ~'err-meta
465+
(loop []
466+
(let [result# (evalme body-vararg# ~'invocation-array)]
467+
(if (identical? ::recur result#)
468+
(recur)
469+
result#)))))))
440470
(cond->> ~fname (aset #^objects ~'enclosed-array (dec ~'enclosed-array-size))))))))
441471

442472

@@ -562,6 +592,7 @@
562592
(assert (= 2 (count form)))
563593
(assert (or (symbol? e) (seq? e)))
564594
(let [e (->eval-node &a nil e)]
595+
;; TODO: mark exception as rethrown!
565596
(gen-eval-node (throw (evalme e &b)))))
566597

567598
(defmethod seq->eval-node 'var [&a _ [_ v]]
@@ -683,6 +714,7 @@
683714
;; method calls
684715
(let [bodies (doall (for [b v] (enhance-code sym->iden b)))]
685716
(with-meta bodies {::symbol-used (set (mapcat (comp ::symbol-used meta) bodies))
717+
::source-meta (meta v)
686718
::symbol-introduced (set (mapcat (comp ::symbol-introduced meta) bodies))}))
687719
;; scalar values: string, numbers, etc.
688720
v))
@@ -831,22 +863,64 @@
831863
(some (comp :test meta) (vals (ns-interns ns))))]
832864
ns))
833865

866+
(alter-var-root #'stacktrace/print-trace-element
867+
(fn [print-trace-element]
868+
(fn stack-trace-element-2 [e]
869+
(if (map? e)
870+
(print (str (:ns e) "/" (:fn e) "(" (:file e) ":" (:line e) ":" (:column e) ")"))
871+
(print-trace-element e)))))
872+
873+
(alter-var-root #'stacktrace/print-stack-trace
874+
(fn [old-print-stack-trace]
875+
(fn print-stack-trace-2 [^Throwable tr & n]
876+
(println :print-stack-trace)
877+
(if-let [st (first (reduce (fn [[xs ns fn] entry]
878+
(if (:ns entry)
879+
[xs (:ns entry) (:fn entry)]
880+
[(cons (assoc entry :ns ns :fn fn) xs) ns fn]))
881+
[nil nil nil] (.get exception-stack tr)))]
882+
(do (stacktrace/print-throwable tr)
883+
(newline)
884+
(print " at ")
885+
(if-let [e (first st)]
886+
(stacktrace/print-trace-element e)
887+
(print "[empty stack trace]"))
888+
(newline)
889+
(doseq [e (rest st)]
890+
(print " ")
891+
(stacktrace/print-trace-element e)
892+
(newline)))
893+
(old-print-stack-trace tr n)))))
894+
895+
(defmacro ^:private try-catch-error [exit-code body]
896+
`(try ~body
897+
(catch Throwable ~'t
898+
(binding [*out* *err*]
899+
(stacktrace/print-stack-trace ~'t)
900+
(flush))
901+
~(if (int? exit-code) `(System/exit ~exit-code) exit-code))))
902+
903+
(Thread/setDefaultUncaughtExceptionHandler
904+
(reify Thread$UncaughtExceptionHandler
905+
(uncaughtException [_ thread ex] (try-catch-error nil (throw ex)))))
906+
834907
(defn -main [& args]
835908
(evaluator '(in-ns 'user))
836909
(evaluator '(run! require uclj.core/namespaces-to-require))
837910
(cond
838911
(and (first args) (.startsWith (str (first args)) "("))
839-
(binding [*command-line-args* (second args)]
840-
(println (evaluator (read-string (first args)))))
912+
(binding [*command-line-args* (second args)]
913+
(try-catch-error 1 (println (evaluator (read-string (first args))))))
841914

842915
(and (first args) (.exists (io/file (first args))))
843916
(let [test? (= "--test" (second args))]
844-
(binding [*command-line-args* (if test? (nnext args) (next args))]
845-
(evaluator `(load-file ~(first args))))
917+
(try-catch-error 1 (binding [*command-line-args* (if test? (nnext args) (next args))]
918+
(evaluator `(load-file ~(first args)))) )
846919
(when test?
847-
(let [test-result (apply clojure.test/run-tests (all-test-namespaces))]
848-
(when-not (zero? (:fail test-result))
849-
(System/exit 1)))))
920+
(try-catch-error 2
921+
(let [test-result (apply clojure.test/run-tests (all-test-namespaces))]
922+
(when-not (zero? (:fail test-result))
923+
(System/exit 1))))))
850924

851925
:else ;; interactive mode
852926
(do (println "Welcome to the small interpreter!")
@@ -856,14 +930,12 @@
856930
(print (str (ns-name *ns*) "=> ")) (flush)
857931
(let [read (read {:eof ::eof} *in*)]
858932
(when-not (= ::eof read)
859-
(try (let [e (evaluator read)]
860-
(var-set-reset! #'*3 *2)
861-
(var-set-reset! #'*2 *1)
862-
(var-set-reset! #'*1 e)
863-
(println e))
864-
(catch Throwable t
865-
(.printStackTrace t)
866-
(var-set-reset! #'*e t)))
933+
(try-catch-error (var-set-reset! #'*e t)
934+
(let [e (evaluator read)]
935+
(var-set-reset! #'*3 *2)
936+
(var-set-reset! #'*2 *1)
937+
(var-set-reset! #'*1 e)
938+
(println e)))
867939
(recur))))
868940
(println "EOF, bye!"))))
869941

0 commit comments

Comments
 (0)