core: Stage 3 turn 2b — host IO, ns introspection, thread-binding family

The names turn 2a's leak removal exposed as honestly missing, now proper:

- slurp/spit/flush (host-classified): path-based IO over Janet files; spit
  takes :append; flush flushes *out*. printf prints formatted (no newline)
  over the existing format. file-seq walks paths via two host dir primitives
  through the overlay's tree-seq.
- ns-map / ns-unmap / ns-refers (ctx fns). ns-refers required fixing the
  refer MODEL: refer/use/:refer now map the SOURCE VAR into the target ns
  (the Clojure model) instead of copying its value into a new var — so
  source-ns redefinitions propagate, the :macro flag travels for free, and
  refers are identifiable by the var's home :ns.
- Thread-binding family: with-bindings*/with-bindings, bound-fn*/bound-fn,
  bound?, thread-bound?, get-thread-bindings. The captured binding map is a
  Janet struct keyed by the var tables — the exact frame representation
  var-get reads — so it re-pushes correctly (a phm frame is invisible to
  var lookup).
- load-string and eval interned as VALUES at the api layer (they need the
  loader's compile-or-interpret routing); the eval special form still
  handles direct calls.

Suite 4532 -> 4572 (baseline floor 4540 across timeout variance, clean 86),
conformance 326x3, stdlib battery, all specs+unit (+21 turn-2b rows).
Coverage: missing-portable 27 -> 10 (left: the *in*-model readers, the
with-local-vars/with-precision/extenders tail).
This commit is contained in:
Yogthos 2026-06-10 12:53:47 -04:00
parent a75a26860b
commit d61c86a068
8 changed files with 197 additions and 34 deletions

View file

@ -145,6 +145,21 @@
# clojure.core and compiled by the self-hosted pipeline (or interpreted when
# :compile? is off). Phase 4 kernel-shrink seam — see that file.
(load-core-overlay! ctx)
# load-string and eval as VALUES need the loader's compile-or-interpret
# routing, which lives above the evaluator — intern them here. (The eval
# special form still handles direct calls; this covers value position,
# e.g. (map eval forms).)
(let [core (ctx-find-ns ctx "clojure.core")]
(ns-intern core "load-string"
(fn [s]
(var cur s)
(var result nil)
(while (> (length (string/trim cur)) 0)
(def [form rest-src] (parse-next cur))
(set cur rest-src)
(when (not (nil? form)) (set result (eval-toplevel ctx form))))
result))
(ns-intern core "eval" (fn [form] (eval-toplevel ctx form))))
ctx))
# --- Context snapshot/fork (cheap isolated copies) --------------------------

View file

@ -1832,6 +1832,53 @@
# Host time source for the `time` macro (monotonic, milliseconds).
(defn core-current-time-ms [] (* 1000 (os/clock :monotonic)))
# Host IO (host-classified in the spec): path-based slurp/spit, *out* flush.
(defn core-slurp [path] (string (slurp path)))
(defn core-spit [path content & opts]
(def append? (do (var a false) (var i 0)
(while (< i (length opts))
(when (and (= :append (in opts i)) (in opts (+ i 1))) (set a true))
(+= i 2))
a))
(def f (file/open path (if append? :a :w)))
(file/write f (str-render-one content))
(file/close f)
nil)
(defn core-flush []
(def out (dyn :out))
(when out (file/flush out))
nil)
# Thread-binding introspection over the frame stack (types/cur-binding-stack).
(defn core-get-thread-bindings []
# Innermost frame wins: merge frames oldest-first. The result is a Janet
# STRUCT keyed by the var tables themselves — the exact frame representation
# var-get reads (identity-keyed get) — so the map can be re-pushed by
# with-bindings*/bound-fn* and remains lookup-able with (get m the-var).
(def acc @{})
(each frame (snapshot-bindings)
(each entry (realize-for-iteration frame)
(put acc (in entry 0) (in entry 1))))
(table/to-struct acc))
(defn core-thread-bound?* [v]
(var found false)
(each frame (snapshot-bindings)
(each entry (realize-for-iteration frame)
(when (= (in entry 0) v) (set found true))))
found)
# Directory primitives for file-seq (paths, not File objects — host-classified).
(defn core-dir? [path]
(def st (os/stat path))
(and st (= :directory (st :mode))))
(defn core-list-dir [path]
(def entries (os/dir path))
(map (fn [e] (string path "/" e)) (sort entries)))
# Clojure compare: a total order over comparable values. nil sorts first;
# numbers numerically; strings/keywords lexically; symbols by ns then name;
# booleans false<true; chars by codepoint; vectors by length then elementwise;
@ -3071,6 +3118,13 @@
"int?" core-integer?
"compare" core-compare
"type" core-type
"slurp" core-slurp
"spit" core-spit
"flush" core-flush
"get-thread-bindings" core-get-thread-bindings
"__thread-bound?" core-thread-bound?*
"__dir?" core-dir?
"__list-dir" core-list-dir
"parse-long" core-parse-long
"parse-double" core-parse-double
"parse-boolean" core-parse-boolean

View file

@ -382,10 +382,9 @@
(let [name (if (struct? refer-sym) (refer-sym :name) refer-sym)
v (ns-find source-ns name)]
(when v
# Preserve macro-ness: a referred macro must stay a macro, so copy
# the :macro flag onto the interned var (not just its value).
(let [nv (ns-intern target-ns name (var-get v))]
(when (get v :macro) (put nv :macro true))))))))
# Share the SOURCE var (the Clojure model): macro-ness travels with
# it and source-ns redefinitions propagate to the referer.
(put (target-ns :mappings) name v))))))
nil))
(defn- bind-put
@ -813,9 +812,11 @@
src-name (sym-name-str ns-sym)]
(maybe-require-ns ctx src-name)
(let [source-ns (ctx-find-ns ctx src-name)]
# Refer maps the SOURCE VAR itself (the Clojure model): redefinitions in
# the source ns propagate, the :macro flag travels for free, and
# ns-refers can identify refers by the var's home :ns.
(loop [[sym v] :pairs (source-ns :mappings)]
(let [nv (ns-intern target-ns sym (var-get v))]
(when (get v :macro) (put nv :macro true)))))))
(put (target-ns :mappings) sym v)))))
nil)
(defn import-impl
@ -1144,13 +1145,32 @@
nil))
# ns-publics: {symbol -> var} (jolt has no private vars, so publics = interns).
# Keys are symbol structs (value-hashed), matching Clojure's symbol keys.
(def mappings->symbol-map (fn [ns pred]
(var m (make-phm))
(loop [[nm v] :pairs (ns :mappings)]
(when (pred nm v)
(set m (phm-assoc m {:jolt/type :symbol :ns nil :name nm} v))))
m))
(ns-intern core "ns-publics"
(fn [&opt ns-d]
(mappings->symbol-map (ns-or-current ns-d) (fn [nm v] true))))
# ns-map: all mappings (interns + refers; jolt has no class imports in maps).
(ns-intern core "ns-map"
(fn [&opt ns-d]
(mappings->symbol-map (ns-or-current ns-d) (fn [nm v] true))))
# ns-refers: mappings whose var's HOME ns differs from this ns (copied in by
# refer/use/require :refer).
(ns-intern core "ns-refers"
(fn [&opt ns-d]
(def ns (ns-or-current ns-d))
(var m (make-phm))
(loop [[nm v] :pairs (ns :mappings)]
(set m (phm-assoc m {:jolt/type :symbol :ns nil :name nm} v)))
m))
(def my-name (ns :name))
(mappings->symbol-map ns (fn [nm v]
(and (table? v) (not= (get v :ns) my-name))))))
(ns-intern core "ns-unmap"
(fn [ns-d sym]
(def ns (ns-or-current ns-d))
(put (ns :mappings) (if (struct? sym) (sym :name) (string sym)) nil)
nil))
core)
# Dispatch a special form by its string name.