From 6445f461bb4dd6266610539069e11d292120d6d8 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Wed, 10 Jun 2026 17:22:28 -0400 Subject: [PATCH] =?UTF-8?q?core:=20the=20last=20seven=20missing-portable?= =?UTF-8?q?=20vars=20=E2=80=94=20coverage=20gap=20closed=20(jolt-brh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard's missing-portable category is now EMPTY (was 35 when the issue was filed; this session's io/leaf work had already landed most of them). The final seven: - extenders — ctx-capturing clojure.core fn over the protocol type-registry: the type-tags implementing a protocol, as symbols; nil when none - find-keyword — keyword: jolt keywords have no intern table, so it always finds (babashka makes the same call) - inst-ms* — the raw Inst method; one inst representation, so = inst-ms - read+string — over the 50-io readers, which now expose :buf and :fill-fn; returns [form exact-text-consumed], EOF throws or yields [eof-value ""] with the 3-arity, works for string AND stdin readers - with-local-vars — fresh free-standing var cells (__local-var seam) bound as locals; var-get/var-set work on any cell - with-open — canonical recursive expansion closing through the __close seam: a map-like value's :close fn or a host file (no .close interop here); nested closes run inner-first, finally runs on throw - with-precision — body evaluates with precision/:rounding accepted and ignored (doubles, no BigDecimal context) — documented divergence 30 new spec rows (test/spec/missing-vars-spec.janet); coverage.md regenerated: implemented+tested 426 -> 433, missing-portable 7 -> 0. Gate: jpm exit 0, all tests passed. --- docs/spec/coverage.md | 48 ++++++++++++------------- jolt-core/clojure/core/20-coll.clj | 12 +++++++ jolt-core/clojure/core/30-macros.clj | 23 ++++++++++++ jolt-core/clojure/core/50-io.clj | 33 +++++++++++++++-- src/jolt/core.janet | 13 +++++++ src/jolt/evaluator.janet | 11 ++++++ test/spec/missing-vars-spec.janet | 53 ++++++++++++++++++++++++++++ 7 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 test/spec/missing-vars-spec.janet diff --git a/docs/spec/coverage.md b/docs/spec/coverage.md index 64be83c..7783481 100644 --- a/docs/spec/coverage.md +++ b/docs/spec/coverage.md @@ -3,16 +3,16 @@ Generated 2026-06-10 by `tools/spec_coverage.py` — do not edit by hand. Surface: **694** clojure.core vars (ClojureDocs export; 648 with -community examples). jolt interns 553 of them. +community examples). jolt interns 564 of them. | Status | Count | Meaning | |---|---|---| -| implemented+tested | 415 | in jolt and exercised by spec/conformance | -| implemented-untested | 138 | in jolt, no direct test — spec entries will add them | +| implemented+tested | 433 | in jolt and exercised by spec/conformance | +| implemented-untested | 131 | in jolt, no direct test — spec entries will add them | | resolvable-not-interned | 2 | works in code but invisible to ns introspection (conformance finding) | -| missing-portable | 10 | portable semantics, jolt lacks it — implementation gap | +| missing-portable | 0 | portable semantics, jolt lacks it — implementation gap | | special-form | 15 | specified in §3, not a library var | -| dynamic-var | 30 | classification needed: portable default vs host-dependent | +| dynamic-var | 29 | classification needed: portable default vs host-dependent | | agents-taps | 22 | out of scope pending concurrency design note | | stm-refs | 11 | out of scope pending concurrency design note | | jvm-specific | 53 | catalogued, not specified | @@ -45,7 +45,7 @@ UNVERIFIED field; that column will be added as entries land. | `*file*` | dynamic-var | ✓ | | `*flush-on-newline*` | dynamic-var | | | `*fn-loader*` | dynamic-var | | -| `*in*` | dynamic-var | | +| `*in*` | implemented-untested | | | `*math-context*` | dynamic-var | | | `*ns*` | implemented-untested | ✓ | | `*out*` | dynamic-var | ✓ | @@ -170,8 +170,8 @@ UNVERIFIED field; that column will be added as entries land. | `catch` | special-form | ✓ | | `char` | implemented+tested | ✓ | | `char-array` | implemented-untested | ✓ | -| `char-escape-string` | implemented-untested | ✓ | -| `char-name-string` | implemented-untested | ✓ | +| `char-escape-string` | implemented+tested | ✓ | +| `char-name-string` | implemented+tested | ✓ | | `char?` | implemented+tested | ✓ | | `chars` | implemented-untested | ✓ | | `chunk` | implemented-untested | ✓ | @@ -185,7 +185,7 @@ UNVERIFIED field; that column will be added as entries land. | `class` | implemented-untested | ✓ | | `class?` | jvm-specific | ✓ | | `clear-agent-errors` | agents-taps | | -| `clojure-version` | implemented-untested | ✓ | +| `clojure-version` | implemented+tested | ✓ | | `coll?` | implemented+tested | ✓ | | `comment` | implemented+tested | ✓ | | `commute` | stm-refs | ✓ | @@ -278,7 +278,7 @@ UNVERIFIED field; that column will be added as entries land. | `extend` | implemented-untested | ✓ | | `extend-protocol` | implemented+tested | ✓ | | `extend-type` | implemented+tested | ✓ | -| `extenders` | missing-portable | ✓ | +| `extenders` | implemented+tested | ✓ | | `extends?` | implemented-untested | ✓ | | `false?` | implemented+tested | ✓ | | `ffirst` | implemented+tested | ✓ | @@ -287,7 +287,7 @@ UNVERIFIED field; that column will be added as entries land. | `filterv` | implemented+tested | ✓ | | `finally` | special-form | ✓ | | `find` | implemented+tested | ✓ | -| `find-keyword` | missing-portable | ✓ | +| `find-keyword` | implemented+tested | ✓ | | `find-ns` | implemented+tested | ✓ | | `find-protocol-impl` | jvm-specific | | | `find-protocol-method` | jvm-specific | | @@ -346,7 +346,7 @@ UNVERIFIED field; that column will be added as entries land. | `infinite?` | implemented+tested | ✓ | | `init-proxy` | implemented-untested | ✓ | | `inst-ms` | implemented+tested | ✓ | -| `inst-ms*` | missing-portable | | +| `inst-ms*` | implemented+tested | | | `inst?` | implemented+tested | ✓ | | `instance?` | implemented+tested | ✓ | | `int` | implemented+tested | ✓ | @@ -376,7 +376,7 @@ UNVERIFIED field; that column will be added as entries land. | `lazy-seq` | implemented+tested | ✓ | | `let` | implemented+tested | ✓ | | `letfn` | implemented+tested | ✓ | -| `line-seq` | implemented-untested | ✓ | +| `line-seq` | implemented+tested | ✓ | | `list` | implemented+tested | ✓ | | `list*` | implemented+tested | ✓ | | `list?` | implemented+tested | ✓ | @@ -415,7 +415,7 @@ UNVERIFIED field; that column will be added as entries land. | `mod` | implemented+tested | ✓ | | `monitor-enter` | special-form | | | `monitor-exit` | special-form | | -| `munge` | implemented-untested | ✓ | +| `munge` | implemented+tested | ✓ | | `name` | implemented+tested | ✓ | | `namespace` | implemented+tested | ✓ | | `namespace-munge` | implemented+tested | ✓ | @@ -517,9 +517,9 @@ UNVERIFIED field; that column will be added as entries land. | `re-matches` | implemented+tested | ✓ | | `re-pattern` | implemented+tested | ✓ | | `re-seq` | implemented+tested | ✓ | -| `read` | missing-portable | ✓ | -| `read+string` | missing-portable | ✓ | -| `read-line` | missing-portable | ✓ | +| `read` | implemented+tested | ✓ | +| `read+string` | implemented+tested | ✓ | +| `read-line` | implemented+tested | ✓ | | `read-string` | implemented+tested | ✓ | | `reader-conditional` | implemented-untested | ✓ | | `reader-conditional?` | implemented+tested | ✓ | @@ -623,7 +623,7 @@ UNVERIFIED field; that column will be added as entries land. | `subs` | implemented+tested | ✓ | | `subseq` | implemented+tested | ✓ | | `subvec` | implemented+tested | ✓ | -| `supers` | implemented-untested | ✓ | +| `supers` | implemented+tested | ✓ | | `swap!` | implemented+tested | ✓ | | `swap-vals!` | implemented+tested | ✓ | | `symbol` | implemented+tested | ✓ | @@ -636,7 +636,7 @@ UNVERIFIED field; that column will be added as entries land. | `take-nth` | implemented+tested | ✓ | | `take-while` | implemented+tested | ✓ | | `tap>` | agents-taps | ✓ | -| `test` | implemented-untested | ✓ | +| `test` | implemented+tested | ✓ | | `the-ns` | implemented+tested | ✓ | | `thread-bound?` | implemented+tested | ✓ | | `throw` | special-form | ✓ | @@ -671,7 +671,7 @@ UNVERIFIED field; that column will be added as entries land. | `unchecked-short` | implemented-untested | ✓ | | `unchecked-subtract` | implemented-untested | ✓ | | `unchecked-subtract-int` | implemented-untested | ✓ | -| `underive` | implemented-untested | ✓ | +| `underive` | implemented+tested | ✓ | | `unquote` | jvm-specific | ✓ | | `unquote-splicing` | jvm-specific | ✓ | | `unreduced` | implemented+tested | ✓ | @@ -707,13 +707,13 @@ UNVERIFIED field; that column will be added as entries land. | `while` | implemented+tested | ✓ | | `with-bindings` | implemented+tested | ✓ | | `with-bindings*` | implemented+tested | ✓ | -| `with-in-str` | missing-portable | ✓ | +| `with-in-str` | implemented+tested | ✓ | | `with-loading-context` | jvm-specific | | -| `with-local-vars` | missing-portable | ✓ | +| `with-local-vars` | implemented+tested | ✓ | | `with-meta` | implemented+tested | ✓ | -| `with-open` | missing-portable | ✓ | +| `with-open` | implemented+tested | ✓ | | `with-out-str` | implemented+tested | ✓ | -| `with-precision` | missing-portable | ✓ | +| `with-precision` | implemented+tested | ✓ | | `with-redefs` | implemented+tested | ✓ | | `with-redefs-fn` | implemented+tested | ✓ | | `xml-seq` | implemented-untested | ✓ | diff --git a/jolt-core/clojure/core/20-coll.clj b/jolt-core/clojure/core/20-coll.clj index ae9ea60..ef03133 100644 --- a/jolt-core/clojure/core/20-coll.clj +++ b/jolt-core/clojure/core/20-coll.clj @@ -809,3 +809,15 @@ (assoc m k (up (get m k) ks f args)) (assoc m k (apply f (get m k) args)))))] (up m ks f args))) + +;; --- jolt-brh: the last missing-portable vars -------------------------------- + +;; jolt keywords have no intern table (any keyword "exists"), so find-keyword +;; always finds — babashka makes the same call. +(defn find-keyword + ([nm] (keyword nm)) + ([ns nm] (keyword ns nm))) + +;; The raw Inst protocol method; jolt insts have one representation, so it is +;; inst-ms itself. +(defn inst-ms* [i] (inst-ms i)) diff --git a/jolt-core/clojure/core/30-macros.clj b/jolt-core/clojure/core/30-macros.clj index 18610eb..9d4364e 100644 --- a/jolt-core/clojure/core/30-macros.clj +++ b/jolt-core/clojure/core/30-macros.clj @@ -73,6 +73,29 @@ [] (partition 2 bindings))] `(with-redefs-fn (hash-map ~@pairs) (fn [] ~@body)))) +;; Fresh free-standing var cells bound as locals; read/write with +;; var-get/var-set. The cells come from the host seam __local-var. +(defmacro with-local-vars [bindings & body] + (let [binds (reduce (fn [acc p] (conj (conj acc (first p)) `(__local-var ~(second p)))) + [] (partition 2 bindings))] + `(let [~@binds] ~@body))) + +;; Canonical recursive expansion; closing goes through the host seam __close +;; (a map-like value's :close fn or a host file — no .close interop here). +(defmacro with-open [bindings & body] + (if (zero? (count bindings)) + `(do ~@body) + `(let [~(first bindings) ~(second bindings)] + (try + (with-open ~(vec (drop 2 bindings)) ~@body) + (finally (__close ~(first bindings))))))) + +;; jolt numbers are doubles — there is no BigDecimal math context, so the +;; precision (and optional :rounding mode) is accepted and ignored. +(defmacro with-precision [precision & exprs] + (let [body (if (= :rounding (first exprs)) (drop 2 exprs) exprs)] + `(do ~@body))) + (defmacro with-bindings [binding-map & body] `(with-bindings* ~binding-map (fn [] ~@body))) diff --git a/jolt-core/clojure/core/50-io.clj b/jolt-core/clojure/core/50-io.clj index ccc6e0b..c1cc90d 100644 --- a/jolt-core/clojure/core/50-io.clj +++ b/jolt-core/clojure/core/50-io.clj @@ -19,7 +19,9 @@ "A reader over string s (the with-in-str expansion calls this)." [s] (let [buf (atom s)] - {:read-line-fn + {:buf buf + :fill-fn nil + :read-line-fn (fn [] (let [cur @buf] (when (pos? (count cur)) @@ -40,7 +42,13 @@ (def ^:private stdin-buf (atom "")) (def ^:dynamic *in* - {:read-line-fn + {:buf stdin-buf + :fill-fn (fn [] + (let [line (__stdin-read-line)] + (if (nil? line) + false + (do (swap! stdin-buf (fn [b] (str b line "\n"))) true)))) + :read-line-fn (fn [] (let [cur @stdin-buf] (if (pos? (count cur)) @@ -87,6 +95,27 @@ `(binding [*in* (__string-reader ~s)] ~@body)) +;; Like read, and also returns the exact text consumed for the form (leading +;; whitespace included). On EOF: throws, or returns [eof-value ""] when +;; eof-error? is false. +(defn read+string + ([] (read+string *in*)) + ([stream] (read+string stream true nil)) + ([stream eof-error? eof-value] + (let [buf (get stream :buf) + fill (get stream :fill-fn)] + (loop [] + (let [s (deref buf) + r (__parse-next s)] + (if (nil? r) + (if (and fill (fill)) + (recur) + (if eof-error? + (throw (ex-info "EOF while reading" {})) + [eof-value ""])) + (do (reset! buf (nth r 1)) + [(nth r 0) (subs s 0 (- (count s) (count (nth r 1))))]))))))) + (defn line-seq "Returns the lines of text from rdr as a lazy sequence of strings, as by read-line. (Jolt extension kept from the old kernel stub: a plain string diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 9ca5964..50aac6b 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -2256,6 +2256,17 @@ (defn core-copy-var [sym & args] nil) (defn core-macrofy [sym fn & more] fn) (defn core-new-var [sym & args] nil) +# A free-standing var cell (not interned anywhere): with-local-vars binds +# these as locals; var-get/var-set work on any cell. +(defn core-local-var [&opt val] + @{:jolt/type :jolt/var :name "local" :ns nil :root val :gen 0}) +# with-open's close seam: a map-like value closes via its :close fn, a host +# file via file/close. No .close interop on the Janet host. +(defn core-close-resource [x] + (cond + (and (or (table? x) (struct? x)) (function? (get x :close))) ((get x :close)) + (= :core/file (type x)) (file/close x) + (error (string "with-open: don't know how to close " (type x))))) # sci stub: pass the registry map through (it was @{} — a raw host table that # strict map-conj rightly rejects; identity also keeps sci's registry intact). (defn core-avoid-method-too-large [& args] (if (> (length args) 0) (in args 0) {})) @@ -2949,6 +2960,8 @@ "copy-var" core-copy-var "macrofy" core-macrofy "new-var" core-new-var + "__local-var" core-local-var + "__close" core-close-resource "avoid-method-too-large" core-avoid-method-too-large "inst?" core-inst? "inst-ms" core-inst-ms diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index 39d339f..8f7c43b 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -951,6 +951,17 @@ (ns-intern core "protocol-dispatch" (fn [proto-name method-name obj rest-args] (protocol-dispatch-impl ctx proto-name method-name obj rest-args))) + (ns-intern core "extenders" + (fn [proto] + # All type-tags whose registry entry implements this protocol, as symbols + # (closest analog to Clojure's class list); nil when none. + (let [pname (get (get proto :name) :name) + registry (get (ctx :env) :type-registry) + out @[]] + (each tag (keys registry) + (when (get (get registry tag) pname) + (array/push out {:jolt/type :symbol :ns nil :name tag}))) + (if (empty? out) nil (tuple ;out))))) (ns-intern core "register-method" (fn [type-name proto-name method-name f] (register-method-impl ctx type-name proto-name method-name f))) diff --git a/test/spec/missing-vars-spec.janet b/test/spec/missing-vars-spec.janet new file mode 100644 index 0000000..0690976 --- /dev/null +++ b/test/spec/missing-vars-spec.janet @@ -0,0 +1,53 @@ +# Specification: the last missing-portable core vars (jolt-brh): +# extenders, find-keyword, inst-ms*, read+string, with-local-vars, with-open, +# with-precision. Documented jolt divergences: find-keyword always finds (jolt +# keywords have no intern table — babashka does the same); with-precision +# evaluates its body with the precision ignored (numbers are doubles, no +# BigDecimal context); with-open closes via the value's :close fn or a host +# file (no .close interop on the Janet host). +(use ../support/harness) + +(defspec "core / find-keyword + inst-ms*" + ["find-keyword" ":a" "(find-keyword \"a\")"] + ["find-keyword 2-arity" ":n/a" "(find-keyword \"n\" \"a\")"] + ["find-keyword = keyword" "true" "(= (find-keyword \"x\") :x)"] + ["inst-ms*" "true" "(= (inst-ms* #inst \"2020-01-01T00:00:00Z\") (inst-ms #inst \"2020-01-01T00:00:00Z\"))"] + ["inst-ms* value" "0" "(inst-ms* #inst \"1970-01-01T00:00:00Z\")"]) + +(defspec "core / with-local-vars" + ["var-get initial" "1" "(with-local-vars [x 1] (var-get x))"] + ["var-set" "2" "(with-local-vars [x 1] (var-set x 2) (var-get x))"] + ["two vars" "[1 2]" "(with-local-vars [a 1 b 2] [(var-get a) (var-get b)])"] + ["vars are values" "5" "(with-local-vars [x 0] (let [bump (fn [v] (var-set v (+ 5 (var-get v))))] (bump x) (var-get x)))"] + ["init sees outer" "3" "(let [y 3] (with-local-vars [x y] (var-get x)))"] + ["body result" ":done" "(with-local-vars [x 1] :done)"]) + +(defspec "core / with-open" + ["body result" ":r" "(let [log (atom [])] (with-open [c {:close (fn [] (swap! log conj :closed))}] :r))"] + ["close runs" "[:closed]" "(let [log (atom [])] (with-open [c {:close (fn [] (swap! log conj :closed))}] :r) (deref log))"] + ["close on throw" "[:closed]" "(let [log (atom [])] (try (with-open [c {:close (fn [] (swap! log conj :closed))}] (throw (ex-info \"boom\" {}))) (catch Exception e nil)) (deref log))"] + ["nested close order" "[:inner :outer]" "(let [log (atom [])] (with-open [a {:close (fn [] (swap! log conj :outer))} b {:close (fn [] (swap! log conj :inner))}] :r) (deref log))"] + ["zero bindings" ":r" "(with-open [] :r)"] + ["binding visible" "5" "(with-open [c {:close (fn [] nil) :v 5}] (:v c))"]) + +(defspec "core / with-precision" + ["body evaluates" "3.14" "(with-precision 3 3.14)"] + ["multiple body forms" "2" "(with-precision 10 1 2)"] + ["rounding arg accepted" "1.5" "(with-precision 4 :rounding :half-up 1.5)"] + ["arithmetic" "2" "(with-precision 5 (+ 1 1))"]) + +(defspec "core / read+string" + ["form and text" "true" "(let [[v s] (with-in-str \"42 rest\" (read+string))] (and (= v 42) (string? s)))"] + ["form value" "(quote (+ 1 2))" "(first (with-in-str \"(+ 1 2)\" (read+string)))"] + ["text covers the form" "true" "(let [[v s] (with-in-str \" [1 2] tail\" (read+string))] (and (= v [1 2]) (> (count s) 3)))"] + ["advances the stream" "[1 2]" "(with-in-str \"1 2\" [(first (read+string)) (first (read+string))])"] + ["EOF throws" :throws "(with-in-str \"\" (read+string))"] + ["eof-value arity" ":done" "(first (with-in-str \"\" (read+string *in* false :done)))"]) + +(defspec "core / extenders" + ["lists extended type" "[\"user.Rx\"]" + "(do (defprotocol Px (pm [x])) (defrecord Rx [] Px (pm [x] 1)) (mapv str (extenders Px)))"] + ["nil when none" "nil" + "(do (defprotocol Py (pn [x])) (extenders Py))"] + ["seq of tags" "true" + "(do (defprotocol Pz (pz [x])) (defrecord Rz [] Pz (pz [x] 1)) (and (seq (extenders Pz)) (= 1 (count (extenders Pz)))))"])