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

@ -43,7 +43,7 @@
# Raised 4004 -> 4034 / clean 66 -> 67 porting partition-all + repeatedly to the
# overlay, which required fixing two leniencies (a char is not callable; take
# validates its count) — correct beyond those fns, so the suite rose broadly.
(def baseline-pass 4532)
(def baseline-pass 4540)
# A file is "clean" when it ran with zero failures AND zero errors.
(def baseline-clean-files 86)
# Per-file wall-clock budget (seconds). Normal files finish in well under 1s, so

View file

@ -0,0 +1,39 @@
# Specification: Stage 3 turn 2b — host-classified IO fns, ns introspection,
# the thread-binding family, and load-string/eval as values. These were
# previously either missing or silently leaked from Janet's root environment.
(use ../support/harness)
(defspec "io / slurp, spit, printf, flush (host-classified)"
["slurp returns string" "true" "(string? (slurp \"project.janet\"))"]
["slurp content" "true" "(do (require (quote [clojure.string :as s])) (s/includes? (slurp \"project.janet\") \"jolt\"))"]
["spit + slurp round" "\"hello\"" "(do (spit \"/tmp/jolt-spit-test.txt\" \"hello\") (slurp \"/tmp/jolt-spit-test.txt\"))"]
["spit append" "\"ab\"" "(do (spit \"/tmp/jolt-spit-test.txt\" \"a\") (spit \"/tmp/jolt-spit-test.txt\" \"b\" :append true) (slurp \"/tmp/jolt-spit-test.txt\"))"]
["printf formats" "\"x=1 y=a\"" "(with-out-str (printf \"x=%d y=%s\" 1 \"a\"))"]
["printf no newline" "false" "(do (require (quote [clojure.string :as s])) (s/includes? (with-out-str (printf \"%d\" 1)) \"\\n\"))"]
["flush returns nil" "nil" "(flush)"]
["file-seq finds files" "true" "(do (require (quote [clojure.string :as s])) (boolean (some (fn [p] (s/ends-with? p \"project.janet\")) (file-seq \".\"))))"])
(defspec "ns / ns-map, ns-unmap, ns-refers"
["ns-map has var" "true" "(do (def nmv 1) (some? (get (ns-map (quote user)) (quote nmv))))"]
["ns-unmap removes" "nil" "(do (def nuv 1) (ns-unmap (quote user) (quote nuv)) (resolve (quote nuv)))"]
["ns-refers sees refer" "true" "(do (require (quote clojure.string)) (refer (quote clojure.string)) (some? (get (ns-refers (quote user)) (quote join))))"])
(defspec "vars / thread-binding family"
["bound? on def" "true" "(do (def bvv 1) (bound? (var bvv)))"]
["with-bindings* binds" "5"
"(do (def ^:dynamic dynv 1) (with-bindings* (array-map (var dynv) 5) (fn [] dynv)))"]
["with-bindings* restores" "1"
"(do (def ^:dynamic dynw 1) (with-bindings* (array-map (var dynw) 5) (fn [] nil)) dynw)"]
["with-bindings macro" "7"
"(do (def ^:dynamic dynx 1) (with-bindings (array-map (var dynx) 7) dynx))"]
["thread-bound? inside" "[true false]"
"(do (def ^:dynamic dyny 1) [(with-bindings* (array-map (var dyny) 2) (fn [] (thread-bound? (var dyny)))) (thread-bound? (var dyny))])"]
["bound-fn* conveys" "9"
"(do (def ^:dynamic dynz 1) (def f (with-bindings* (array-map (var dynz) 9) (fn [] (bound-fn* (fn [] dynz))))) (f))"]
["get-thread-bindings" "3"
"(do (def ^:dynamic dyng 1) (with-bindings* (array-map (var dyng) 3) (fn [] (get (get-thread-bindings) (var dyng)))))"])
(defspec "eval & load-string as values"
["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)))"])