core: edn :readers/:default opts; uncaught throws no longer leak the callee's ns
clojure.edn was nearly complete (sets, #uuid/#inst, :eof all landed earlier);
the :readers opt was ignored and :default missing. Both work now — the
reader stores a tag as a :#name keyword, so the lookup normalizes it to the
symbol Clojure keys :readers with; :default gets (tag value); built-in data
readers stay the fallback. 8 new edn spec rows. This was the last open item
of jolt-0mb (the vendored walk/zip/data/edn battery has been green for a
while: 34/33/61/50, all clean).
Chasing the probe cascade ('Unable to resolve symbol: edn/...' after one
error) found a real evaluator bug: an interpreted fn body runs with
current-ns rebound to its DEFINING ns and restores it with a plain trailing
call — an UNCAUGHT throw skips every restore on the way out, leaving the ctx
stuck in the deepest callee's ns, where alias-qualified lookups then fail
(the same cascade previously seen via sci). The repair lives at the
TOP-LEVEL boundary (loader/eval-toplevel saves the entry ns and restores it
on error before re-raising) — NOT per-call defer/try, which builds a fiber
per frame and blew the C stack on deep interpreted recursion (file-seq)
when tried first. Regression tests cover the cross-eval leak and that
aliases keep resolving.
This commit is contained in:
parent
fef99db6d8
commit
6260230231
6 changed files with 87 additions and 9 deletions
|
|
@ -8,20 +8,31 @@
|
|||
;; The reader yields set literals as a FORM ({:jolt/type :jolt/set :value [...]})
|
||||
;; rather than a constructed set, so build the actual values, recursing into
|
||||
;; maps/vectors/lists. (Lists stay lists — EDN never evaluates them as code.)
|
||||
(defn- edn->value [x]
|
||||
(defn- edn->value [opts x]
|
||||
(cond
|
||||
;; Reader FORMS are detected by :jolt/type tag, never by map? — strict map?
|
||||
;; (correctly) excludes tagged structs, so the old (and (map? x) ...) guard
|
||||
;; would skip them.
|
||||
(= :jolt/set (get x :jolt/type)) (set (map edn->value (get x :value)))
|
||||
;; EDN built-in tagged elements (#uuid/#inst, plus registered readers):
|
||||
;; apply the data reader to the read form (no evaluation involved).
|
||||
(= :jolt/set (get x :jolt/type)) (set (map (fn [v] (edn->value opts v)) (get x :value)))
|
||||
;; Tagged elements: a reader from the :readers opt wins, then the built-in
|
||||
;; data readers (#uuid/#inst + registered); an unknown tag falls to the
|
||||
;; :default opt fn (called with tag and value, as in Clojure) or throws.
|
||||
(= :jolt/tagged (get x :jolt/type))
|
||||
(__read-tagged (get x :tag) (edn->value (get x :form)))
|
||||
(let [tag (get x :tag)
|
||||
v (edn->value opts (get x :form))
|
||||
;; the reader stores the tag as a :#name keyword; :readers maps are
|
||||
;; keyed by the SYMBOL (Clojure's shape) — normalize for lookup
|
||||
tag-sym (let [n (name tag)]
|
||||
(symbol (if (= "#" (subs n 0 1)) (subs n 1) n)))
|
||||
custom (get (get opts :readers) tag-sym)]
|
||||
(cond
|
||||
custom (custom v)
|
||||
(get opts :default) ((get opts :default) tag v)
|
||||
:else (__read-tagged tag v)))
|
||||
(map? x)
|
||||
(into {} (map (fn [e] [(edn->value (key e)) (edn->value (val e))]) x))
|
||||
(vector? x) (mapv edn->value x)
|
||||
(seq? x) (map edn->value x)
|
||||
(into {} (map (fn [e] [(edn->value opts (key e)) (edn->value opts (val e))]) x))
|
||||
(vector? x) (mapv (fn [v] (edn->value opts v)) x)
|
||||
(seq? x) (map (fn [v] (edn->value opts v)) x)
|
||||
:else x))
|
||||
|
||||
;; Private helper, NOT named read-string: an unqualified (read-string …) call
|
||||
|
|
@ -30,7 +41,7 @@
|
|||
(defn- read-edn [opts s]
|
||||
(if (or (nil? s) (cstr/blank? s))
|
||||
(get opts :eof nil)
|
||||
(edn->value (clojure.core/read-string s))))
|
||||
(edn->value opts (clojure.core/read-string s))))
|
||||
|
||||
(defn read-string
|
||||
"Reads one object from the string s. Returns the :eof option value (default
|
||||
|
|
|
|||
|
|
@ -1341,6 +1341,10 @@
|
|||
# Use defining namespace for symbol resolution
|
||||
(def saved-ns (ctx-current-ns ctx))
|
||||
(ctx-set-current-ns ctx defining-ns)
|
||||
# Plain trailing restore (NOT defer/try — those build a fiber per
|
||||
# call and blow the C stack on deep interpreted recursion). An
|
||||
# unwinding throw is repaired once at the TOP-LEVEL boundary
|
||||
# (loader/eval-toplevel restores the ns on error).
|
||||
(var result nil)
|
||||
(each bf body
|
||||
(set result (eval-form ctx new-bindings bf)))
|
||||
|
|
@ -1417,6 +1421,10 @@
|
|||
# Use defining namespace for symbol resolution
|
||||
(def saved-ns (ctx-current-ns ctx))
|
||||
(ctx-set-current-ns ctx defining-ns)
|
||||
# Plain trailing restore (NOT defer/try — those build a fiber per
|
||||
# call and blow the C stack on deep interpreted recursion). An
|
||||
# unwinding throw is repaired once at the TOP-LEVEL boundary
|
||||
# (loader/eval-toplevel restores the ns on error).
|
||||
(var result nil)
|
||||
(each body-form body
|
||||
(set result (eval-form ctx fn-bindings body-form)))
|
||||
|
|
@ -1456,6 +1464,10 @@
|
|||
# Use defining namespace for symbol resolution
|
||||
(def saved-ns (ctx-current-ns ctx))
|
||||
(ctx-set-current-ns ctx defining-ns)
|
||||
# Plain trailing restore (NOT defer/try — those build a fiber per
|
||||
# call and blow the C stack on deep interpreted recursion). An
|
||||
# unwinding throw is repaired once at the TOP-LEVEL boundary
|
||||
# (loader/eval-toplevel restores the ns on error).
|
||||
(var result nil)
|
||||
(each body-form body
|
||||
(set result (eval-form ctx fn-bindings body-form)))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
# Supports in-memory bytecode caching when :compile? is enabled.
|
||||
|
||||
(use ./reader)
|
||||
(use ./types)
|
||||
(use ./evaluator)
|
||||
(import ./backend :as backend)
|
||||
|
||||
|
|
@ -30,6 +31,14 @@
|
|||
interpreter for forms it can't compile. Only the compile step is guarded —
|
||||
runtime errors in compiled code propagate (no double-eval, no hidden errors)."
|
||||
[ctx form]
|
||||
# Repair point for the interpreted-fn ns swap: a body runs with current-ns
|
||||
# rebound to its defining ns and restores it on normal return; an UNWINDING
|
||||
# throw skips those restores (they're plain trailing calls — defer/try per
|
||||
# call would cost a fiber per frame and blow the C stack on deep recursion).
|
||||
# So save here, and on error put the entry ns back before re-raising — the
|
||||
# ctx never leaks a callee's ns across top-level forms.
|
||||
(def entry-ns (ctx-current-ns ctx))
|
||||
(defn- run []
|
||||
(defn try-compile [] (backend/compile-and-eval ctx form))
|
||||
(if (get (ctx :env) :compile?)
|
||||
(if (array? form)
|
||||
|
|
@ -43,6 +52,11 @@
|
|||
(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)))))
|
||||
|
||||
(defn load-ns
|
||||
"Load a Clojure namespace from a .clj file. Per-form routing (compile-or-
|
||||
|
|
|
|||
|
|
@ -96,3 +96,20 @@
|
|||
(print " passed")
|
||||
|
||||
(print "\nAll namespace tests passed!")
|
||||
|
||||
# An UNCAUGHT throw inside an interpreted fn body must restore the caller's
|
||||
# current-ns (the body runs with current-ns rebound to the defining ns; the
|
||||
# restore is a defer now). Pre-fix, the ctx was left stuck in the defining ns
|
||||
# and every later alias-qualified lookup failed — the sci-bootstrap and
|
||||
# clojure.edn "Unable to resolve alias/..." cascades.
|
||||
(print "ns restored after uncaught throw...")
|
||||
(let [ctx (fresh-ctx)]
|
||||
(api/eval-string ctx "(in-ns (quote otherns))")
|
||||
(api/eval-string ctx "(clojure.core/refer-clojure)")
|
||||
(api/eval-string ctx "(defn boom [] (throw (ex-info \"x\" {})))")
|
||||
(api/eval-string ctx "(in-ns (quote user))")
|
||||
(assert (not ((protect (api/eval-string ctx "(otherns/boom)")) 0)) "boom throws")
|
||||
(assert (= "user" (api/eval-string ctx "(str *ns*)")) "*ns* restored after uncaught throw")
|
||||
(api/eval-string ctx "(require (quote [clojure.string :as s9]))")
|
||||
(assert (= "A" (api/eval-string ctx "(s9/upper-case \"a\")")) "aliases still resolve"))
|
||||
(print " ok")
|
||||
|
|
|
|||
|
|
@ -42,3 +42,14 @@
|
|||
"(do (require (quote clojure.string)) (alias (quote st2) (quote clojure.string)) (ns-unalias (quote user) (quote st2)) (nil? (get (ns-aliases (quote user)) (quote st2))))"]
|
||||
["ns-publics has var" "true" "(do (def npv 1) (some? (get (ns-publics (quote user)) (quote npv))))"]
|
||||
["newline returns nil" "nil" "(newline)"])
|
||||
|
||||
# A throw inside an interpreted fn body (or macro expander) must restore the
|
||||
# caller's current-ns: the body runs with current-ns rebound to the fn's
|
||||
# DEFINING ns, and an unwind that skipped the restore left the ctx stuck
|
||||
# there — every later alias-qualified lookup in the REPL ns then failed
|
||||
# ("Unable to resolve symbol: alias/...", seen via sci + clojure.edn).
|
||||
(defspec "namespaces / error inside a fn must not leak its defining ns"
|
||||
["alias survives a throwing stdlib call" "\"A\""
|
||||
"(do (require (quote [clojure.string :as s9])) (try (s9/join nil nil nil) (catch Exception e nil)) (s9/upper-case \"a\"))"]
|
||||
["*ns* restored after throw" "\"user\""
|
||||
"(do (require (quote [clojure.walk :as w9])) (try (w9/postwalk nil nil nil) (catch Exception e nil)) (str *ns*))"])
|
||||
|
|
|
|||
|
|
@ -37,3 +37,16 @@
|
|||
["load-string evals all" "3" "(load-string \"(def lsv 1) (+ lsv 2)\")"]
|
||||
["eval as value" "[2 3]" "(mapv eval [(quote (+ 1 1)) (quote (+ 1 2))])"]
|
||||
["eval special still works" "3" "(eval (quote (+ 1 2)))"])
|
||||
|
||||
# clojure.edn is complete (jolt-b7y / jolt-0mb): sets, #uuid/#inst, :eof,
|
||||
# and the :readers / :default opts (tag normalized from the reader's :#name
|
||||
# keyword to the symbol Clojure keys :readers with).
|
||||
(defspec "clojure.edn / opts"
|
||||
["set literal" "#{1 2}" "(do (require (quote [clojure.edn :as e0])) (e0/read-string \"#{1 2}\"))"]
|
||||
["uuid tag" "true" "(do (require (quote [clojure.edn :as e0])) (uuid? (e0/read-string \"#uuid \\\"550e8400-e29b-41d4-a716-446655440000\\\"\")))"]
|
||||
["inst tag" "true" "(do (require (quote [clojure.edn :as e0])) (inst? (e0/read-string \"#inst \\\"2020-01-01T00:00:00Z\\\"\")))"]
|
||||
[":eof on empty" ":end" "(do (require (quote [clojure.edn :as e0])) (e0/read-string {:eof :end} \"\"))"]
|
||||
[":readers custom tag" "[:custom 5]" "(do (require (quote [clojure.edn :as e0])) (e0/read-string {:readers {(quote custom) (fn [v] [:custom v])}} \"#custom 5\"))"]
|
||||
[":readers nested" "[6 8]" "(do (require (quote [clojure.edn :as e0])) (e0/read-string {:readers {(quote w) (fn [v] (* 2 v))}} \"[#w 3 #w 4]\"))"]
|
||||
[":default fn" "[:dflt 7]" "(do (require (quote [clojure.edn :as e0])) (e0/read-string {:default (fn [t v] [:dflt v])} \"#unknown 7\"))"]
|
||||
["unknown tag throws" :throws "(do (require (quote [clojure.edn :as e0])) (e0/read-string \"#nope 1\"))"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue