core: the last seven missing-portable vars — coverage gap closed (jolt-brh)

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.
This commit is contained in:
Yogthos 2026-06-10 17:22:28 -04:00
parent 3051f4264a
commit 6445f461bb
7 changed files with 167 additions and 26 deletions

View file

@ -3,16 +3,16 @@
Generated 2026-06-10 by `tools/spec_coverage.py` — do not edit by hand. Generated 2026-06-10 by `tools/spec_coverage.py` — do not edit by hand.
Surface: **694** clojure.core vars (ClojureDocs export; 648 with 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 | | Status | Count | Meaning |
|---|---|---| |---|---|---|
| implemented+tested | 415 | in jolt and exercised by spec/conformance | | implemented+tested | 433 | in jolt and exercised by spec/conformance |
| implemented-untested | 138 | in jolt, no direct test — spec entries will add them | | 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) | | 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 | | 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 | | agents-taps | 22 | out of scope pending concurrency design note |
| stm-refs | 11 | out of scope pending concurrency design note | | stm-refs | 11 | out of scope pending concurrency design note |
| jvm-specific | 53 | catalogued, not specified | | jvm-specific | 53 | catalogued, not specified |
@ -45,7 +45,7 @@ UNVERIFIED field; that column will be added as entries land.
| `*file*` | dynamic-var | ✓ | | `*file*` | dynamic-var | ✓ |
| `*flush-on-newline*` | dynamic-var | | | `*flush-on-newline*` | dynamic-var | |
| `*fn-loader*` | dynamic-var | | | `*fn-loader*` | dynamic-var | |
| `*in*` | dynamic-var | | | `*in*` | implemented-untested | |
| `*math-context*` | dynamic-var | | | `*math-context*` | dynamic-var | |
| `*ns*` | implemented-untested | ✓ | | `*ns*` | implemented-untested | ✓ |
| `*out*` | dynamic-var | ✓ | | `*out*` | dynamic-var | ✓ |
@ -170,8 +170,8 @@ UNVERIFIED field; that column will be added as entries land.
| `catch` | special-form | ✓ | | `catch` | special-form | ✓ |
| `char` | implemented+tested | ✓ | | `char` | implemented+tested | ✓ |
| `char-array` | implemented-untested | ✓ | | `char-array` | implemented-untested | ✓ |
| `char-escape-string` | implemented-untested | ✓ | | `char-escape-string` | implemented+tested | ✓ |
| `char-name-string` | implemented-untested | ✓ | | `char-name-string` | implemented+tested | ✓ |
| `char?` | implemented+tested | ✓ | | `char?` | implemented+tested | ✓ |
| `chars` | implemented-untested | ✓ | | `chars` | implemented-untested | ✓ |
| `chunk` | implemented-untested | ✓ | | `chunk` | implemented-untested | ✓ |
@ -185,7 +185,7 @@ UNVERIFIED field; that column will be added as entries land.
| `class` | implemented-untested | ✓ | | `class` | implemented-untested | ✓ |
| `class?` | jvm-specific | ✓ | | `class?` | jvm-specific | ✓ |
| `clear-agent-errors` | agents-taps | | | `clear-agent-errors` | agents-taps | |
| `clojure-version` | implemented-untested | ✓ | | `clojure-version` | implemented+tested | ✓ |
| `coll?` | implemented+tested | ✓ | | `coll?` | implemented+tested | ✓ |
| `comment` | implemented+tested | ✓ | | `comment` | implemented+tested | ✓ |
| `commute` | stm-refs | ✓ | | `commute` | stm-refs | ✓ |
@ -278,7 +278,7 @@ UNVERIFIED field; that column will be added as entries land.
| `extend` | implemented-untested | ✓ | | `extend` | implemented-untested | ✓ |
| `extend-protocol` | implemented+tested | ✓ | | `extend-protocol` | implemented+tested | ✓ |
| `extend-type` | implemented+tested | ✓ | | `extend-type` | implemented+tested | ✓ |
| `extenders` | missing-portable | ✓ | | `extenders` | implemented+tested | ✓ |
| `extends?` | implemented-untested | ✓ | | `extends?` | implemented-untested | ✓ |
| `false?` | implemented+tested | ✓ | | `false?` | implemented+tested | ✓ |
| `ffirst` | implemented+tested | ✓ | | `ffirst` | implemented+tested | ✓ |
@ -287,7 +287,7 @@ UNVERIFIED field; that column will be added as entries land.
| `filterv` | implemented+tested | ✓ | | `filterv` | implemented+tested | ✓ |
| `finally` | special-form | ✓ | | `finally` | special-form | ✓ |
| `find` | implemented+tested | ✓ | | `find` | implemented+tested | ✓ |
| `find-keyword` | missing-portable | ✓ | | `find-keyword` | implemented+tested | ✓ |
| `find-ns` | implemented+tested | ✓ | | `find-ns` | implemented+tested | ✓ |
| `find-protocol-impl` | jvm-specific | | | `find-protocol-impl` | jvm-specific | |
| `find-protocol-method` | jvm-specific | | | `find-protocol-method` | jvm-specific | |
@ -346,7 +346,7 @@ UNVERIFIED field; that column will be added as entries land.
| `infinite?` | implemented+tested | ✓ | | `infinite?` | implemented+tested | ✓ |
| `init-proxy` | implemented-untested | ✓ | | `init-proxy` | implemented-untested | ✓ |
| `inst-ms` | implemented+tested | ✓ | | `inst-ms` | implemented+tested | ✓ |
| `inst-ms*` | missing-portable | | | `inst-ms*` | implemented+tested | |
| `inst?` | implemented+tested | ✓ | | `inst?` | implemented+tested | ✓ |
| `instance?` | implemented+tested | ✓ | | `instance?` | implemented+tested | ✓ |
| `int` | implemented+tested | ✓ | | `int` | implemented+tested | ✓ |
@ -376,7 +376,7 @@ UNVERIFIED field; that column will be added as entries land.
| `lazy-seq` | implemented+tested | ✓ | | `lazy-seq` | implemented+tested | ✓ |
| `let` | implemented+tested | ✓ | | `let` | implemented+tested | ✓ |
| `letfn` | implemented+tested | ✓ | | `letfn` | implemented+tested | ✓ |
| `line-seq` | implemented-untested | ✓ | | `line-seq` | implemented+tested | ✓ |
| `list` | implemented+tested | ✓ | | `list` | implemented+tested | ✓ |
| `list*` | 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 | ✓ | | `mod` | implemented+tested | ✓ |
| `monitor-enter` | special-form | | | `monitor-enter` | special-form | |
| `monitor-exit` | special-form | | | `monitor-exit` | special-form | |
| `munge` | implemented-untested | ✓ | | `munge` | implemented+tested | ✓ |
| `name` | implemented+tested | ✓ | | `name` | implemented+tested | ✓ |
| `namespace` | implemented+tested | ✓ | | `namespace` | implemented+tested | ✓ |
| `namespace-munge` | 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-matches` | implemented+tested | ✓ |
| `re-pattern` | implemented+tested | ✓ | | `re-pattern` | implemented+tested | ✓ |
| `re-seq` | implemented+tested | ✓ | | `re-seq` | implemented+tested | ✓ |
| `read` | missing-portable | ✓ | | `read` | implemented+tested | ✓ |
| `read+string` | missing-portable | ✓ | | `read+string` | implemented+tested | ✓ |
| `read-line` | missing-portable | ✓ | | `read-line` | implemented+tested | ✓ |
| `read-string` | implemented+tested | ✓ | | `read-string` | implemented+tested | ✓ |
| `reader-conditional` | implemented-untested | ✓ | | `reader-conditional` | implemented-untested | ✓ |
| `reader-conditional?` | implemented+tested | ✓ | | `reader-conditional?` | implemented+tested | ✓ |
@ -623,7 +623,7 @@ UNVERIFIED field; that column will be added as entries land.
| `subs` | implemented+tested | ✓ | | `subs` | implemented+tested | ✓ |
| `subseq` | implemented+tested | ✓ | | `subseq` | implemented+tested | ✓ |
| `subvec` | implemented+tested | ✓ | | `subvec` | implemented+tested | ✓ |
| `supers` | implemented-untested | ✓ | | `supers` | implemented+tested | ✓ |
| `swap!` | implemented+tested | ✓ | | `swap!` | implemented+tested | ✓ |
| `swap-vals!` | implemented+tested | ✓ | | `swap-vals!` | implemented+tested | ✓ |
| `symbol` | implemented+tested | ✓ | | `symbol` | implemented+tested | ✓ |
@ -636,7 +636,7 @@ UNVERIFIED field; that column will be added as entries land.
| `take-nth` | implemented+tested | ✓ | | `take-nth` | implemented+tested | ✓ |
| `take-while` | implemented+tested | ✓ | | `take-while` | implemented+tested | ✓ |
| `tap>` | agents-taps | ✓ | | `tap>` | agents-taps | ✓ |
| `test` | implemented-untested | ✓ | | `test` | implemented+tested | ✓ |
| `the-ns` | implemented+tested | ✓ | | `the-ns` | implemented+tested | ✓ |
| `thread-bound?` | implemented+tested | ✓ | | `thread-bound?` | implemented+tested | ✓ |
| `throw` | special-form | ✓ | | `throw` | special-form | ✓ |
@ -671,7 +671,7 @@ UNVERIFIED field; that column will be added as entries land.
| `unchecked-short` | implemented-untested | ✓ | | `unchecked-short` | implemented-untested | ✓ |
| `unchecked-subtract` | implemented-untested | ✓ | | `unchecked-subtract` | implemented-untested | ✓ |
| `unchecked-subtract-int` | implemented-untested | ✓ | | `unchecked-subtract-int` | implemented-untested | ✓ |
| `underive` | implemented-untested | ✓ | | `underive` | implemented+tested | ✓ |
| `unquote` | jvm-specific | ✓ | | `unquote` | jvm-specific | ✓ |
| `unquote-splicing` | jvm-specific | ✓ | | `unquote-splicing` | jvm-specific | ✓ |
| `unreduced` | implemented+tested | ✓ | | `unreduced` | implemented+tested | ✓ |
@ -707,13 +707,13 @@ UNVERIFIED field; that column will be added as entries land.
| `while` | implemented+tested | ✓ | | `while` | implemented+tested | ✓ |
| `with-bindings` | implemented+tested | ✓ | | `with-bindings` | 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-loading-context` | jvm-specific | |
| `with-local-vars` | missing-portable | ✓ | | `with-local-vars` | implemented+tested | ✓ |
| `with-meta` | implemented+tested | ✓ | | `with-meta` | implemented+tested | ✓ |
| `with-open` | missing-portable | ✓ | | `with-open` | implemented+tested | ✓ |
| `with-out-str` | implemented+tested | ✓ | | `with-out-str` | implemented+tested | ✓ |
| `with-precision` | missing-portable | ✓ | | `with-precision` | implemented+tested | ✓ |
| `with-redefs` | implemented+tested | ✓ | | `with-redefs` | implemented+tested | ✓ |
| `with-redefs-fn` | implemented+tested | ✓ | | `with-redefs-fn` | implemented+tested | ✓ |
| `xml-seq` | implemented-untested | ✓ | | `xml-seq` | implemented-untested | ✓ |

View file

@ -809,3 +809,15 @@
(assoc m k (up (get m k) ks f args)) (assoc m k (up (get m k) ks f args))
(assoc m k (apply f (get m k) args)))))] (assoc m k (apply f (get m k) args)))))]
(up m ks f 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))

View file

@ -73,6 +73,29 @@
[] (partition 2 bindings))] [] (partition 2 bindings))]
`(with-redefs-fn (hash-map ~@pairs) (fn [] ~@body)))) `(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] (defmacro with-bindings [binding-map & body]
`(with-bindings* ~binding-map (fn [] ~@body))) `(with-bindings* ~binding-map (fn [] ~@body)))

View file

@ -19,7 +19,9 @@
"A reader over string s (the with-in-str expansion calls this)." "A reader over string s (the with-in-str expansion calls this)."
[s] [s]
(let [buf (atom s)] (let [buf (atom s)]
{:read-line-fn {:buf buf
:fill-fn nil
:read-line-fn
(fn [] (fn []
(let [cur @buf] (let [cur @buf]
(when (pos? (count cur)) (when (pos? (count cur))
@ -40,7 +42,13 @@
(def ^:private stdin-buf (atom "")) (def ^:private stdin-buf (atom ""))
(def ^:dynamic *in* (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 [] (fn []
(let [cur @stdin-buf] (let [cur @stdin-buf]
(if (pos? (count cur)) (if (pos? (count cur))
@ -87,6 +95,27 @@
`(binding [*in* (__string-reader ~s)] `(binding [*in* (__string-reader ~s)]
~@body)) ~@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 (defn line-seq
"Returns the lines of text from rdr as a lazy sequence of strings, as by "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 read-line. (Jolt extension kept from the old kernel stub: a plain string

View file

@ -2256,6 +2256,17 @@
(defn core-copy-var [sym & args] nil) (defn core-copy-var [sym & args] nil)
(defn core-macrofy [sym fn & more] fn) (defn core-macrofy [sym fn & more] fn)
(defn core-new-var [sym & args] nil) (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 # 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). # 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) {})) (defn core-avoid-method-too-large [& args] (if (> (length args) 0) (in args 0) {}))
@ -2949,6 +2960,8 @@
"copy-var" core-copy-var "copy-var" core-copy-var
"macrofy" core-macrofy "macrofy" core-macrofy
"new-var" core-new-var "new-var" core-new-var
"__local-var" core-local-var
"__close" core-close-resource
"avoid-method-too-large" core-avoid-method-too-large "avoid-method-too-large" core-avoid-method-too-large
"inst?" core-inst? "inst?" core-inst?
"inst-ms" core-inst-ms "inst-ms" core-inst-ms

View file

@ -951,6 +951,17 @@
(ns-intern core "protocol-dispatch" (ns-intern core "protocol-dispatch"
(fn [proto-name method-name obj rest-args] (fn [proto-name method-name obj rest-args]
(protocol-dispatch-impl ctx 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" (ns-intern core "register-method"
(fn [type-name proto-name method-name f] (fn [type-name proto-name method-name f]
(register-method-impl ctx type-name proto-name method-name f))) (register-method-impl ctx type-name proto-name method-name f)))

View file

@ -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)))))"])