Merge pull request #83 from jolt-lang/ring-core-host-shims

host: ring-core enablement — shims, class tokens, vendored spork/http
This commit is contained in:
Dmitri Sotnikov 2026-06-12 01:19:26 +00:00 committed by GitHub
commit 2009de2a76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 473 additions and 37 deletions

View file

@ -138,7 +138,13 @@ function expecting a tuple needs an explicit conversion. The `jolt.interop`,
This bridge is what makes networking (and everything else in Janet's stdlib) This bridge is what makes networking (and everything else in Janet's stdlib)
available to ordinary Clojure — for example, `jolt.nrepl` (below) is plain available to ordinary Clojure — for example, `jolt.nrepl` (below) is plain
Clojure over `janet.net/*`. Clojure over `janet.net/*`. It also reaches any **jpm-installed module**:
the first reference to `janet.<module>/<name>` requires the module from the
janet module path and caches its bindings — so after `jpm install spork`,
`janet.spork.http/*` just works. The `jolt.http` client and the Ring adapter
in [examples/ring-app](https://github.com/jolt-lang/examples/tree/main/ring-app)
are built on spork/http this way, and a project can declare the requirement
in `deps.edn` with a `:jpm/module` coordinate (see docs/tools-deps.md).
```clojure ```clojure
(require '[jolt.interop :as j]) (require '[jolt.interop :as j])

View file

@ -8,3 +8,7 @@ Libraries confirmed to load and pass their conformance checks on Jolt
* [Selmer](https://github.com/yogthos/Selmer) * [Selmer](https://github.com/yogthos/Selmer)
* [medley](https://github.com/weavejester/medley) * [medley](https://github.com/weavejester/medley)
* [cuerdas](https://github.com/funcool/cuerdas) * [cuerdas](https://github.com/funcool/cuerdas)
* [ring-core](https://github.com/ring-clojure/ring) — via `:deps/root "ring-core"`,
on the [ring-app example](https://github.com/jolt-lang/examples/tree/main/ring-app)'s
spork/http adapter
* [ring-codec](https://github.com/ring-clojure/ring-codec)

View file

@ -107,3 +107,23 @@ a regex feature like Unicode property classes (`\p{…}`).
- **Compiling deps into a binary image.** `uberscript` already produces a - **Compiling deps into a binary image.** `uberscript` already produces a
standalone `.clj`; baking a project's dependencies directly into a custom standalone `.clj`; baking a project's dependencies directly into a custom
executable image is a heavier variant that isn't implemented. executable image is a heavier variant that isn't implemented.
## Janet dependencies: `:jpm/module`
A jolt project can depend on janet libraries. jpm owns their installation;
`deps.edn` declares the requirement and `jolt-deps` verifies it at resolve
time:
```clojure
:deps {janet/spork-http {:jpm/module "spork/http"
:jpm/install "spork"}}
```
- `:jpm/module` — the janet module path that must be importable.
- `:jpm/install` (optional) — the jpm package to install when it isn't;
`jolt-deps` runs `jpm install <name>` once, then re-checks. Without it the
resolve fails with the install hint.
A `:jpm/module` dep contributes no source roots. At runtime the `janet.*`
interop bridge autoloads the module on first reference
(`janet.spork.http/server`, …), so nothing else is needed.

View file

@ -293,6 +293,13 @@
(defmacro fn [& raw] (defmacro fn [& raw]
(let [nm (if (symbol? (first raw)) (first raw) nil) (let [nm (if (symbol? (first raw)) (first raw) nil)
aftn (if nm (next raw) raw) aftn (if nm (next raw) raw)
;; a return-type hint (defn f ^bytes [x] ...) reaches us as a
;; (with-meta [x] {:tag ...}) FORM in params position — unwrap it
;; (the hint means nothing on jolt; ring-codec carries several).
unhint (fn* [x]
(if (if (seq? x) (= 'with-meta (first x)) false)
(nth x 1)
x))
md (fn* go [ps nps lets] md (fn* go [ps nps lets]
(if (seq ps) (if (seq ps)
(if (symbol? (first ps)) (if (symbol? (first ps))
@ -303,15 +310,21 @@
(go (next ps) (conj nps g) (conj (conj lets (first ps)) g)))) (go (next ps) (conj nps g) (conj (conj lets (first ps)) g))))
[nps lets])) [nps lets]))
mk (fn* [sig] mk (fn* [sig]
(let [r (md (seq (first sig)) [] [])] (let [ps (unhint (first sig))
(if (empty? (nth r 1)) hinted (not (= ps (first sig)))
r (md (seq ps) [] [])]
(if (if (empty? (nth r 1)) (not hinted) false)
sig sig
;; build the params/let vectors via [~@..] so they are tuple forms ;; build the params/let vectors via [~@..] so they are tuple forms
;; (the accumulators are plain seqs, the wrong representation). ;; (the accumulators are plain seqs, the wrong representation).
;; A hinted-but-undestructured arity also rebuilds, to shed the
;; with-meta wrapper without changing the clause representation.
(let [pv `[~@(nth r 0)] (let [pv `[~@(nth r 0)]
lv `[~@(nth r 1)]] lv `[~@(nth r 1)]]
`(~pv (let ~lv ~@(rest sig)))))))] (if (empty? (nth r 1))
(if (vector? (first aftn)) `(~pv ~@(rest sig))
`(~pv (let ~lv ~@(rest sig))))))))]
(if (vector? (unhint (first aftn)))
(let [a (mk aftn)] (let [a (mk aftn)]
(if nm `(fn* ~nm ~@a) `(fn* ~@a))) (if nm `(fn* ~nm ~@a) `(fn* ~@a)))
(let [as (vec (map mk aftn))] (let [as (vec (map mk aftn))]

View file

@ -252,8 +252,9 @@
;; Group a flat seq that starts with a head symbol followed by its list specs ;; Group a flat seq that starts with a head symbol followed by its list specs
;; into [[head spec spec ...] ...] runs. Used by extend-protocol and defrecord. ;; into [[head spec spec ...] ...] runs. Used by extend-protocol and defrecord.
(defn- group-by-head [items] (defn- group-by-head [items]
;; nil is a valid extension head (extend-protocol P ... nil (m [x] ...)).
(reduce (fn [acc x] (reduce (fn [acc x]
(if (symbol? x) (if (or (symbol? x) (nil? x))
(conj acc [x]) (conj acc [x])
(conj (pop acc) (conj (peek acc) x)))) (conj (pop acc) (conj (peek acc) x))))
[] items)) [] items))
@ -347,9 +348,10 @@
(defmacro extend-type [tsym psym & impls] (defmacro extend-type [tsym psym & impls]
;; register-method is a fn (clojure.core); pass type/protocol/method NAMES as ;; register-method is a fn (clojure.core); pass type/protocol/method NAMES as
;; strings (not the symbols) so the call compiles as a plain invoke. ;; strings (not the symbols) so the call compiles as a plain invoke. A nil
;; type extends on nil values (the host tag is the string "nil").
`(do ~@(map (fn [spec] `(do ~@(map (fn [spec]
`(register-method ~(name tsym) ~(name psym) ~(name (first spec)) `(register-method ~(if (nil? tsym) "nil" (name tsym)) ~(name psym) ~(name (first spec))
(fn* ~(nth spec 1) ~@(drop 2 spec)))) (fn* ~(nth spec 1) ~@(drop 2 spec))))
impls))) impls)))

View file

@ -281,7 +281,11 @@
# non-ctx value, so check the shape, not just the throw. # non-ctx value, so check the shape, not just the throw.
(when (and (r 0) (ctx? (r 1))) (when (and (r 0) (ctx? (r 1)))
(r 1))))] (r 1))))]
(or loaded (or (when loaded
# per-PROCESS wiring an image restore skips: the renderer's
# print-method hook lives in module state, not the marshaled ctx
(install-print-method-cb! loaded)
loaded)
(let [ctx (init opts) (let [ctx (init opts)
tmp (string path "." (os/getpid) ".tmp")] tmp (string path "." (os/getpid) ".tmp")]
# Atomic publish so concurrent cold starts never see a torn image. # Atomic publish so concurrent cold starts never see a torn image.

View file

@ -1108,7 +1108,13 @@
(if (= 0 (length c)) (f) (if (= 0 (length c)) (f)
(reduce-with-reduced f (in c 0) (array/slice c 1)))))) (reduce-with-reduced f (in c 0) (array/slice c 1))))))
3 (let [f (args 0) val (args 1) coll (args 2)] 3 (let [f (args 0) val (args 1) coll (args 2)]
(reduce-with-reduced f val coll)) # reify clojure.lang.IReduceInit: the reified value carries its own
# reduce — call it (ring.util.codec's tokenizer reduces this way)
(if-let [m (and (table? coll)
(get coll :jolt/protocol-methods)
(get (get coll :jolt/protocol-methods) :reduce))]
(m coll f val)
(reduce-with-reduced f val coll)))
(error "Wrong number of args passed to: reduce")))) (error "Wrong number of args passed to: reduce"))))
(defn core-take [n & rest] (defn core-take [n & rest]
@ -1723,7 +1729,17 @@
(defn core-current-time-ms [] (* 1000 (os/clock :monotonic))) (defn core-current-time-ms [] (* 1000 (os/clock :monotonic)))
# Host IO (host-classified in the spec): path-based slurp/spit, *out* flush. # Host IO (host-classified in the spec): path-based slurp/spit, *out* flush.
(defn core-slurp [path] (string (slurp path))) # Opts (:encoding ...) are accepted and ignored — everything is UTF-8 here.
# Reader shims (java.io.StringReader / PushbackReader / anything carrying
# :s + :pos) DRAIN instead of opening a file: Ring middleware slurps request
# bodies, and the jolt Ring adapter hands those over as StringReaders.
(defn core-slurp [src & opts]
(cond
(and (table? src) (string? (get src :s)) (number? (get src :pos)))
(let [s (src :s) p (src :pos)]
(put src :pos (length s))
(string/slice s p))
(string (slurp src))))
(defn core-spit [path content & opts] (defn core-spit [path content & opts]
(def append? (do (var a false) (var i 0) (def append? (do (var a false) (var i 0)

View file

@ -73,6 +73,26 @@
(get x :name)) (get x :name))
(string x))) (string x)))
(defn- ensure-jpm-dep
"A :jpm/module dep declares a janet module installed through jpm (e.g.
spork/http). jolt-deps doesn't manage janet packages — jpm does — so this
just verifies the module is importable, optionally running `jpm install
<:jpm/install>` once when it isn't, and fails with the install hint
otherwise. Contributes no source roots; the janet.* bridge autoloads the
module at first use."
[lib spec]
(def mod (get spec :jpm/module))
(defn importable? [] ((protect (require mod)) 0))
(unless (importable?)
(when-let [pkg (get spec :jpm/install)]
(eprintf "jolt-deps: %s: jpm module %s missing — running `jpm install %s`"
(sym-name lib) mod pkg)
(os/execute ["jpm" "install" pkg] :p))
(unless (importable?)
(errorf "%s: janet module %s is not importable. Install it with `jpm install %s` (jolt-deps leaves janet packages to jpm)."
(sym-name lib) mod (or (get spec :jpm/install) mod)))))
(defn- merge-by-name [a b] # union of symbol-keyed dictionaries, b wins (defn- merge-by-name [a b] # union of symbol-keyed dictionaries, b wins
(def out @{}) (def out @{})
(each m [a b] (when (dictionary? m) (eachp [k v] m (put out (sym-name k) v)))) (each m [a b] (when (dictionary? m) (eachp [k v] m (put out (sym-name k) v))))
@ -165,7 +185,8 @@
(and (deep= (get a :local/root) (get b :local/root)) (and (deep= (get a :local/root) (get b :local/root))
(deep= (get a :git/url) (get b :git/url)) (deep= (get a :git/url) (get b :git/url))
(deep= (get a :git/sha) (get b :git/sha)) (deep= (get a :git/sha) (get b :git/sha))
(deep= (get a :git/tag) (get b :git/tag)))) (deep= (get a :git/tag) (get b :git/tag))
(deep= (get a :jpm/module) (get b :jpm/module))))
(def queue @[]) (def queue @[])
(defn discover [lib spec base-dir] (defn discover [lib spec base-dir]
(def k (sym-name lib)) (def k (sym-name lib))
@ -175,9 +196,16 @@
k (coord-str prev) (coord-str spec))) k (coord-str prev) (coord-str spec)))
(do (do
(put seen k spec) (put seen k spec)
(when (and (dictionary? spec) (get spec :jpm/module))
(ensure-jpm-dep lib spec))
(def dir (def dir
(cond (cond
(and (dictionary? spec) (get spec :git/url)) (clone-git spec) (and (dictionary? spec) (get spec :git/url))
# :deps/root (tools.deps): the project lives in a subdirectory
# of the repo — monorepos like ring-clojure/ring.
(let [cloned (clone-git spec)
root (get spec :deps/root)]
(if root (string cloned "/" root) cloned))
(and (dictionary? spec) (get spec :local/root)) (and (dictionary? spec) (get spec :local/root))
(let [lr (get spec :local/root)] (let [lr (get spec :local/root)]
(if (string/has-prefix? "/" lr) lr (string base-dir "/" lr))) (if (string/has-prefix? "/" lr) lr (string base-dir "/" lr)))

View file

@ -34,13 +34,28 @@
(defn- roots [aliases] (defn- roots [aliases]
(if (os/stat "deps.edn") (deps/resolve-deps-cached "deps.edn" nil aliases) @[])) (if (os/stat "deps.edn") (deps/resolve-deps-cached "deps.edn" nil aliases) @[]))
(defn- jolt-bin
"The jolt executable: $JOLT_BIN, else the `jolt` sitting NEXT TO this
jolt-deps binary (the pair is built together — running a checkout's
build/jolt-deps by path must not pick up some other jolt, or fail when
none is on PATH), else `jolt` from PATH."
[]
(or (os/getenv "JOLT_BIN")
(let [self (or (first (dyn :args)) (dyn :executable))
slashes (when self (string/find-all "/" self))
dir (when (and slashes (> (length slashes) 0))
(string/slice self 0 (last slashes)))
sibling (when dir (string dir "/jolt"))]
(when (and sibling (os/stat sibling)) sibling))
"jolt"))
(defn- exec-jolt [aliases extra-args] (defn- exec-jolt [aliases extra-args]
# Set JOLT_PATH in our own env and let the child inherit it (os/execute's env # Set JOLT_PATH in our own env and let the child inherit it (os/execute's env
# arg isn't honored here; inheriting is reliable). # arg isn't honored here; inheriting is reliable).
(def rs (string/join (roots aliases) ":")) (def rs (string/join (roots aliases) ":"))
(def existing (os/getenv "JOLT_PATH")) (def existing (os/getenv "JOLT_PATH"))
(os/setenv "JOLT_PATH" (if (and existing (> (length existing) 0)) (string rs ":" existing) rs)) (os/setenv "JOLT_PATH" (if (and existing (> (length existing) 0)) (string rs ":" existing) rs))
(os/execute [(os/getenv "JOLT_BIN" "jolt") ;extra-args] :p)) (os/execute [(jolt-bin) ;extra-args] :p))
(defn- usage [] (defn- usage []
(print "usage: jolt-deps [-A:alias[:alias]] [path | run FILE [args] | repl | -e EXPR [args]]") (print "usage: jolt-deps [-A:alias[:alias]] [path | run FILE [args] | repl | -e EXPR [args]]")

View file

@ -13,6 +13,31 @@
# the janet/* interop bridge falls back to it inside env-less fibers. # the janet/* interop bridge falls back to it inside env-less fibers.
(def- module-load-env (fiber/getenv (fiber/current))) (def- module-load-env (fiber/getenv (fiber/current)))
# jpm-module autoload: a janet.<module>/<name> reference whose module isn't
# in the env is satisfied by requiring it from the jpm module path on first
# use — (janet.spork.http/server ...) just works when spork is installed,
# and the same goes for any jpm module. Loaded bindings are cached here
# (and failures negatively cached, so a missing module errors fast).
(def- janet-bridge-extras @{})
(def- janet-bridge-failed @{})
(defn- bridge-autoload
"jname is spork.http/server-shaped: require spork/http, cache its public
bindings under the dotted prefix, return the one asked for (nil when the
module is missing or has no such binding)."
[jname]
(def slash (string/find "/" jname))
(when slash
(def mod-ns (string/slice jname 0 slash))
(unless (get janet-bridge-failed mod-ns)
(def mod-path (string/replace-all "." "/" mod-ns))
(def r (protect (require mod-path)))
(if (r 0)
(eachp [sym entry] (r 1)
(when (and (symbol? sym) (table? entry) (not (get entry :private)))
(put janet-bridge-extras (string mod-ns "/" sym) (get entry :value))))
(put janet-bridge-failed mod-ns true))))
(in janet-bridge-extras jname))
(defn- sym-name? (defn- sym-name?
[sym-s name-str] [sym-s name-str]
(and (struct? sym-s) (= :symbol (sym-s :jolt/type)) (= name-str (sym-s :name)))) (and (struct? sym-s) (= :symbol (sym-s :jolt/type)) (= name-str (sym-s :name))))
@ -501,17 +526,60 @@
# ctor fns are interned as clojure.core vars at init (install-stateful-fns!). # ctor fns are interned as clojure.core vars at init (install-stateful-fns!).
(def class-ctors @{}) (def class-ctors @{})
(defn register-class-ctor! [nm f] (put class-ctors nm f)) (defn register-class-ctor! [nm f] (put class-ctors nm f))
# Class names evaluate to their CANONICAL NAME STRING — the same value
# core-class returns — so (defmethod m String ...) keys match a
# (defmulti m (comp class :body)) dispatch (ring.util.request does this).
# `new` resolves the actual constructor from class-ctors by short name.
(def- class-canonical-names
@{"String" "java.lang.String" "Number" "java.lang.Number"
"Boolean" "java.lang.Boolean" "Long" "java.lang.Long"
"Integer" "java.lang.Integer" "Double" "java.lang.Double"
"InputStream" "java.io.InputStream" "OutputStream" "java.io.OutputStream"
"File" "java.io.File" "Reader" "java.io.Reader" "Writer" "java.io.Writer"
"ISeq" "clojure.lang.ISeq" "Keyword" "clojure.lang.Keyword"
"Symbol" "clojure.lang.Symbol" "MapEntry" "clojure.lang.MapEntry"
"StringReader" "java.io.StringReader" "StringWriter" "java.io.StringWriter"
"StringBuilder" "java.lang.StringBuilder"
"StringTokenizer" "java.util.StringTokenizer"
"Charset" "java.nio.charset.Charset" "Base64" "java.util.Base64"})
(defn- class-value-for
"The value a class-name symbol evaluates to: its canonical name string."
[nm]
(or (get class-canonical-names nm)
# qualified already, or unknown: the name itself is the token
nm))
(defn- ctor-for-class-token
"Constructor fn for a class token (a canonical-name string): try the full
name, then the short name after the last dot."
[tok]
(or (in class-ctors tok)
(let [parts (string/split "." tok)]
(in class-ctors (last parts)))))
# java.lang.String method surface for clj-compat interop: (.toLowerCase s), # java.lang.String method surface for clj-compat interop: (.toLowerCase s),
# (.indexOf s x), ... — the methods portable cljc libraries actually call. # (.indexOf s x), ... — the methods portable cljc libraries actually call.
# Case mapping is ASCII (the whole engine is byte-based); indexOf returns -1 # Case mapping is ASCII (the whole engine is byte-based); indexOf returns -1
# on miss, as on the JVM. # on miss, as on the JVM.
(defn- str-needle [x] (defn- str-needle [x]
(if (and (struct? x) (= :jolt/char (get x :jolt/type))) (cond
(string/from-bytes (x :ch)) (and (struct? x) (= :jolt/char (get x :jolt/type))) (string/from-bytes (x :ch))
# (.indexOf s 61): an int needle is a char CODE on the JVM, not its decimal
# text (ring-codec splits k=v pairs this way)
(number? x) (string/from-bytes (math/trunc x))
(string x))) (string x)))
# java.lang.Number surface (ring-codec: (.byteValue (Integer/valueOf s 16))).
(def- number-methods
{"byteValue" (fn [n] (let [b (band (math/trunc n) 0xff)] (if (> b 127) (- b 256) b)))
"shortValue" (fn [n] (let [v (band (math/trunc n) 0xffff)] (if (> v 32767) (- v 65536) v)))
"intValue" (fn [n] (math/trunc n))
"longValue" (fn [n] (math/trunc n))
"floatValue" (fn [n] (* 1.0 n))
"doubleValue" (fn [n] (* 1.0 n))
"toString" (fn [n &opt radix] (if (= radix 16) (string/format "%x" (math/trunc n)) (string n)))})
(def- string-methods (def- string-methods
{"toString" (fn [s] s) {"getBytes" (fn [s &opt charset] (buffer s))
"toString" (fn [s] s)
"toLowerCase" (fn [s] (string/ascii-lower s)) "toLowerCase" (fn [s] (string/ascii-lower s))
"toUpperCase" (fn [s] (string/ascii-upper s)) "toUpperCase" (fn [s] (string/ascii-upper s))
"trim" (fn [s] (string/trim s)) "trim" (fn [s] (string/trim s))
@ -586,16 +654,24 @@
(let [jname (if (= ns "janet") name (string (string/slice ns 6) "/" name)) (let [jname (if (= ns "janet") name (string (string/slice ns 6) "/" name))
# worker fibers may carry no env (fiber/new without :e inherit) # worker fibers may carry no env (fiber/new without :e inherit)
# — fall back to the env captured at module load # — fall back to the env captured at module load
entry (in (or (fiber/getenv (fiber/current)) module-load-env) # four-step resolution: the runtime fiber's env (when it
(symbol jname))] # has one), the evaluator's module env (worker/connection
# fibers carry a foreign or empty env — net/server handler
# fibers resolve janet/struct through here), the autoload
# cache, then a jpm-module require on first miss
entry (or (when-let [fe (fiber/getenv (fiber/current))]
(in fe (symbol jname)))
(in module-load-env (symbol jname))
(in janet-bridge-extras jname)
(bridge-autoload jname))]
(if (not (nil? entry)) (if (not (nil? entry))
(if (table? entry) (entry :value) entry) (if (table? entry) (entry :value) entry)
(error (string "Unable to resolve Janet symbol: " jname)))) (error (string "Unable to resolve Janet symbol: " jname))))
# syntax-quote ns-qualifies bare class names inside macros # syntax-quote ns-qualifies bare class names inside macros
# (selmer.util/StringBuilder); class names never belong to an ns — # (selmer.util/StringBuilder); class names never belong to an ns —
# fall back to the constructor / statics shims before giving up. # fall back to the constructor / statics shims before giving up.
(if-let [ctor (in class-ctors name)] (if (or (in class-ctors name) (get class-canonical-names name))
ctor (class-value-for name)
(error (string "Unable to resolve symbol: " ns "/" name)))))) (error (string "Unable to resolve symbol: " ns "/" name))))))
# Use :jolt/not-found sentinel to distinguish nil binding from absent binding # Use :jolt/not-found sentinel to distinguish nil binding from absent binding
(let [local (get bindings name :jolt/not-found-1) (let [local (get bindings name :jolt/not-found-1)
@ -846,7 +922,10 @@
"Keyword" true "Symbol" true "Object" true "IFn" true "Fn" true "Keyword" true "Symbol" true "Object" true "IFn" true "Fn" true
"PersistentVector" true "PersistentList" true "PersistentHashMap" true "PersistentVector" true "PersistentList" true "PersistentHashMap" true
"PersistentHashSet" true "IPersistentMap" true "IPersistentVector" true "PersistentHashSet" true "IPersistentMap" true "IPersistentVector" true
"IPersistentSet" true "IPersistentCollection" true "ISeq" true "Atom" true "nil" true}) "IPersistentSet" true "IPersistentCollection" true "ISeq" true "Atom" true "nil" true
# java.util interfaces + seq types ring & friends extend on
"Map" true "Set" true "List" true "Collection" true "LazySeq" true
"APersistentMap" true})
(defn- canonical-host-tag (defn- canonical-host-tag
"If type-name names a host type (optionally java.*/clojure.lang.* qualified), "If type-name names a host type (optionally java.*/clojure.lang.* qualified),
@ -869,7 +948,17 @@
(keyword? obj) ["Keyword" "Object"] (keyword? obj) ["Keyword" "Object"]
(and (struct? obj) (= :jolt/char (get obj :jolt/type))) ["Character" "Object"] (and (struct? obj) (= :jolt/char (get obj :jolt/type))) ["Character" "Object"]
(and (struct? obj) (= :symbol (get obj :jolt/type))) ["Symbol" "Object"] (and (struct? obj) (= :symbol (get obj :jolt/type))) ["Symbol" "Object"]
(plist? obj) ["PersistentList" "IPersistentList" "IPersistentCollection" "ISeq" "Object"] (plist? obj) ["PersistentList" "IPersistentList" "IPersistentCollection" "ISeq" "List" "Collection" "Object"]
(lazy-seq? obj) ["LazySeq" "ISeq" "IPersistentCollection" "Collection" "Object"]
# maps: phm / plain struct / sorted / records — java.util.Map covers them
# all in ring-style extend-protocol clauses
(or (phm? obj)
(and (struct? obj) (nil? (get obj :jolt/type)))
(and (table? obj) (or (get obj :jolt/deftype)
(= :jolt/sorted-map (get obj :jolt/type)))))
["PersistentHashMap" "APersistentMap" "IPersistentMap" "Map" "IPersistentCollection" "Object"]
(or (set? obj) (and (table? obj) (= :jolt/sorted-set (get obj :jolt/type))))
["PersistentHashSet" "IPersistentSet" "Set" "IPersistentCollection" "Object"]
(or (tuple? obj) (array? obj) (pvec? obj)) ["PersistentVector" "IPersistentVector" "IPersistentCollection" "ISeq" "Object"] (or (tuple? obj) (array? obj) (pvec? obj)) ["PersistentVector" "IPersistentVector" "IPersistentCollection" "ISeq" "Object"]
(or (function? obj) (cfunction? obj)) ["IFn" "Fn" "Object"] (or (function? obj) (cfunction? obj)) ["IFn" "Fn" "Object"]
(nil? obj) ["nil" "Object"] (nil? obj) ["nil" "Object"]
@ -1054,7 +1143,8 @@
(def v-box @[nil]) (def v-box @[nil])
(def mm-fn (def mm-fn
(fn [& args] (fn [& args]
(let [dv (apply dispatch-fn args) (let [dv* (apply dispatch-fn args)
dv (if (nil? dv*) :jolt/nil-sentinel dv*)
method (get methods dv)] method (get methods dv)]
(if method (if method
(apply method args) (apply method args)
@ -1137,7 +1227,9 @@
(put v :jolt/methods @{}) (put v :jolt/methods @{})
v))) v)))
(def methods (or (get mm-var :jolt/methods) (let [m @{}] (put mm-var :jolt/methods m) m))) (def methods (or (get mm-var :jolt/methods) (let [m @{}] (put mm-var :jolt/methods m) m)))
(put methods dispatch-val impl) # nil is a legal dispatch value (ring's body-string keys a method on it);
# janet tables can't hold nil keys, so it rides the sentinel
(put methods (if (nil? dispatch-val) :jolt/nil-sentinel dispatch-val) impl)
(let [dc (get mm-var :jolt/dispatch-cache)] (let [dc (get mm-var :jolt/dispatch-cache)]
(when dc (each k (keys dc) (put dc k nil)))) (when dc (each k (keys dc) (put dc k nil))))
mm-var) mm-var)
@ -1294,6 +1386,7 @@
mm-var)) mm-var))
(ns-intern core "remove-method-setup" (ns-intern core "remove-method-setup"
(fn [mm-sym dval] (fn [mm-sym dval]
(def dval (if (nil? dval) :jolt/nil-sentinel dval))
(def mm-var (mm-var-of mm-sym false)) (def mm-var (mm-var-of mm-sym false))
(when mm-var (when mm-var
(let [methods (get mm-var :jolt/methods)] (let [methods (get mm-var :jolt/methods)]
@ -1316,6 +1409,7 @@
(or (and mm-var (get mm-var :jolt/prefers)) {}))) (or (and mm-var (get mm-var :jolt/prefers)) {})))
(ns-intern core "get-method-setup" (ns-intern core "get-method-setup"
(fn [mm-sym dval] (fn [mm-sym dval]
(def dval (if (nil? dval) :jolt/nil-sentinel dval))
(def mm-var (mm-var-of mm-sym false)) (def mm-var (mm-var-of mm-sym false))
(when mm-var (when mm-var
(let [methods (get mm-var :jolt/methods)] (let [methods (get mm-var :jolt/methods)]
@ -1408,8 +1502,13 @@
# read, with-in-str, and line-seq are Clojure over these (core/50-io.clj). # read, with-in-str, and line-seq are Clojure over these (core/50-io.clj).
# The loader's registered source roots (the closest thing to a classpath) — # The loader's registered source roots (the closest thing to a classpath) —
# io/resource searches these for relative resource paths. # io/resource searches these for relative resource paths.
# registered constructor shims (StringReader., StringBuilder., ...) # registered constructor shims: the NAME evaluates to the canonical class
(eachp [nm f] class-ctors (ns-intern core nm f)) # string (so class-dispatch defmultis match); `new` finds the ctor fn.
(eachp [nm f] class-ctors (ns-intern core nm (class-value-for nm)))
# dispatch-only type names (no ctor): InputStream, File, ISeq, ...
(eachp [nm canon] class-canonical-names
(unless (or (in class-ctors nm) (ns-find core nm))
(ns-intern core nm canon)))
(ns-intern core "__source-roots" (ns-intern core "__source-roots"
(fn [] (tuple ;(get (ctx :env) :source-paths)))) (fn [] (tuple ;(get (ctx :env) :source-paths))))
(ns-intern core "__stdin-read-line" (ns-intern core "__stdin-read-line"
@ -1975,7 +2074,8 @@
# compiles as a plain (do …); no special-form arm. # compiles as a plain (do …); no special-form arm.
"new" (let [type-sym (in form 1) "new" (let [type-sym (in form 1)
args (map |(eval-form ctx bindings $) (tuple/slice form 2)) args (map |(eval-form ctx bindings $) (tuple/slice form 2))
ctor (eval-form ctx bindings type-sym)] ctor (eval-form ctx bindings type-sym)
ctor (if (string? ctor) (or (ctor-for-class-token ctor) ctor) ctor)]
(apply ctor args)) (apply ctor args))
"." (let [target (eval-form ctx bindings (in form 1)) "." (let [target (eval-form ctx bindings (in form 1))
member-raw (in form 2) member-raw (in form 2)
@ -1996,6 +2096,8 @@
(if m (if m
(m (string target) ;args) (m (string target) ;args)
(error (string "Unsupported String method ." field-name)))) (error (string "Unsupported String method ." field-name))))
(if (and (number? target) (get number-methods field-name))
((get number-methods field-name) target ;args)
# registered shim objects (java.time etc.): tag-keyed method tables # registered shim objects (java.time etc.): tag-keyed method tables
(if (and (or (table? target) (struct? target)) (if (and (or (table? target) (struct? target))
(get tagged-methods (get target :jolt/type))) (get tagged-methods (get target :jolt/type)))
@ -2028,7 +2130,7 @@
(method-fn target ;args) (method-fn target ;args)
(error (string "Cannot call non-function " field-name " on " (type target))))) (error (string "Cannot call non-function " field-name " on " (type target)))))
(error (string "Cannot call non-function " field-name " on " (type target)))))) (error (string "Cannot call non-function " field-name " on " (type target))))))
(error (string "Cannot call method " field-name " on " (type target)))))))) (error (string "Cannot call method " field-name " on " (type target)))))))))
# (. obj member) with no extra args: a symbol member naming a # (. obj member) with no extra args: a symbol member naming a
# function is a zero-arg method call (receiver passed as self); # function is a zero-arg method call (receiver passed as self);
# a keyword or `-field` member is plain field access. Strings get # a keyword or `-field` member is plain field access. Strings get
@ -2038,6 +2140,8 @@
(if m (if m
(m (string target)) (m (string target))
(error (string "Unsupported String method ." field-name)))) (error (string "Unsupported String method ." field-name))))
(if (and (number? target) (get number-methods field-name))
((get number-methods field-name) target)
(if (and (or (table? target) (struct? target)) (if (and (or (table? target) (struct? target))
(get tagged-methods (get target :jolt/type)) (get tagged-methods (get target :jolt/type))
(get (get tagged-methods (get target :jolt/type)) field-name)) (get (get tagged-methods (get target :jolt/type)) field-name))
@ -2057,7 +2161,7 @@
(array? v) (let [f (eval-form ctx bindings v)] (array? v) (let [f (eval-form ctx bindings v)]
(if (or (function? f) (cfunction? f)) (f target) f)) (if (or (function? f) (cfunction? f)) (f target) f))
v) v)
v)))))) v)))))))
# default: function application — check for macros # default: function application — check for macros
(if (and (struct? first-form) (= :symbol (first-form :jolt/type))) (if (and (struct? first-form) (= :symbol (first-form :jolt/type)))
(let [sym-name (first-form :name)] (let [sym-name (first-form :name)]
@ -2086,6 +2190,9 @@
(let [type-name (string/slice sym-name 0 (- (length sym-name) 1)) (let [type-name (string/slice sym-name 0 (- (length sym-name) 1))
type-sym {:jolt/type :symbol :ns (first-form :ns) :name type-name} type-sym {:jolt/type :symbol :ns (first-form :ns) :name type-name}
ctor (eval-form ctx bindings type-sym) ctor (eval-form ctx bindings type-sym)
# class names evaluate to canonical-name STRINGS now; the
# constructor itself comes from the ctor registry
ctor (if (string? ctor) (or (ctor-for-class-token ctor) ctor) ctor)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))] args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(apply ctor args)) (apply ctor args))
(let [v (resolve-var ctx bindings first-form)] (let [v (resolve-var ctx bindings first-form)]

View file

@ -263,6 +263,131 @@
(register-class-ctor! nm string-builder)) (register-class-ctor! nm string-builder))
(each nm ["StringWriter" "java.io.StringWriter"] (each nm ["StringWriter" "java.io.StringWriter"]
(register-class-ctor! nm make-string-writer)) (register-class-ctor! nm make-string-writer))
# --- java.net / java.util surface for ring-codec (ring-core enablement) ---
# URLEncoder/URLDecoder: www-form-urlencoded (space <-> +, %XX bytes,
# [A-Za-z0-9.*_-] literal). Charset args are accepted and ignored beyond
# the name (everything is UTF-8 bytes here).
(defn- url-unreserved? [b]
(or (and (>= b 48) (<= b 57)) (and (>= b 65) (<= b 90))
(and (>= b 97) (<= b 122)) (= b 46) (= b 42) (= b 95) (= b 45)))
(defn- url-encode-www [s & _]
(def out @"")
(each b (string/bytes (string s))
(cond
(url-unreserved? b) (buffer/push-byte out b)
(= b 32) (buffer/push-string out "+")
(buffer/push-string out (string/format "%%%02X" b))))
(string out))
(defn- hexv [b]
(cond (and (>= b 48) (<= b 57)) (- b 48)
(and (>= b 65) (<= b 70)) (- b 55)
(and (>= b 97) (<= b 102)) (- b 87)
(error "URLDecoder: malformed escape")))
(defn- url-decode-www [s & _]
(def bytes (string/bytes (string s)))
(def n (length bytes))
(def out @"")
(var i 0)
(while (< i n)
(def b (in bytes i))
(cond
(= b 43) (do (buffer/push-string out " ") (++ i))
(= b 37) (if (< (+ i 2) n)
(do (buffer/push-byte out (+ (* 16 (hexv (in bytes (+ i 1)))) (hexv (in bytes (+ i 2)))))
(+= i 3))
(error "URLDecoder: incomplete escape"))
(do (buffer/push-byte out b) (++ i))))
(string out))
(each nm ["URLEncoder" "java.net.URLEncoder"]
(register-class-statics! nm @{"encode" url-encode-www}))
(each nm ["URLDecoder" "java.net.URLDecoder"]
(register-class-statics! nm @{"decode" url-decode-www}))
(each nm ["Charset" "java.nio.charset.Charset"]
(register-class-statics! nm @{"forName" (fn [nm*] @{:jolt/type :jolt/charset :name nm*})}))
# Base64 (RFC 4648): encoder/decoder singletons with encode/decode methods.
(def- b64-alphabet "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
(defn- b64-encode [bs]
(def bytes (if (bytes? bs) bs (string bs)))
(def n (length bytes))
(def out @"")
(var i 0)
(while (< i n)
(def b0 (in bytes i))
(def b1 (if (< (+ i 1) n) (in bytes (+ i 1))))
(def b2 (if (< (+ i 2) n) (in bytes (+ i 2))))
(buffer/push-byte out (in b64-alphabet (brshift b0 2)))
(buffer/push-byte out (in b64-alphabet (bor (blshift (band b0 3) 4) (brshift (or b1 0) 4))))
(buffer/push-string out (if (nil? b1) "=" (string/from-bytes (in b64-alphabet (bor (blshift (band b1 0xf) 2) (brshift (or b2 0) 6))))))
(buffer/push-string out (if (nil? b2) "=" (string/from-bytes (in b64-alphabet (band b2 0x3f)))))
(+= i 3))
(string out))
(def- b64-rev (do (def t @{}) (eachp [i c] b64-alphabet (put t c i)) t))
(defn- b64-decode [s]
(def cleaned (string/replace-all "=" "" (string s)))
(def out @"")
(var acc 0) (var bits 0)
(each c (string/bytes cleaned)
(def v (get b64-rev c))
(when (nil? v) (error "Base64: illegal character"))
(set acc (bor (blshift acc 6) v))
(+= bits 6)
(when (>= bits 8)
(-= bits 8)
(buffer/push-byte out (band (brshift acc bits) 0xff))))
out)
(register-tagged-methods! :jolt/base64-encoder @{"encode" (fn [self bs] (b64-encode bs)) "encodeToString" (fn [self bs] (b64-encode bs))})
(register-tagged-methods! :jolt/base64-decoder @{"decode" (fn [self s] (b64-decode s))})
(each nm ["Base64" "java.util.Base64"]
(register-class-statics! nm
@{"getEncoder" (fn [] @{:jolt/type :jolt/base64-encoder})
"getDecoder" (fn [] @{:jolt/type :jolt/base64-decoder})}))
# Integer statics: valueOf with optional radix. Returns a plain number —
# byteValue/intValue live on the number method surface in the evaluator.
(register-class-statics! "Integer"
@{"valueOf" (fn [x &opt radix]
(cond
(number? x) x
(nil? radix) (or (scan-number (string x)) (error (string "NumberFormatException: " x)))
(= radix 16) (or (scan-number (string "16r" x)) (error (string "NumberFormatException: " x)))
(= radix 8) (or (scan-number (string "8r" x)) (error (string "NumberFormatException: " x)))
(= radix 2) (or (scan-number (string "2r" x)) (error (string "NumberFormatException: " x)))
(error (string "Integer/valueOf: unsupported radix " radix))))
"parseInt" (fn [x &opt radix]
(or (scan-number (string (case radix 16 "16r" 8 "8r" 2 "2r" "") x))
(error (string "NumberFormatException: " x))))
"MAX_VALUE" 2147483647
"MIN_VALUE" -2147483648})
# StringTokenizer: eager split on any delimiter char, empty tokens skipped.
(defn- tokenize [s delims]
(def dset @{})
(each b (string/bytes delims) (put dset b true))
(def toks @[])
(def cur @"")
(each b (string/bytes (string s))
(if (get dset b)
(when (> (length cur) 0) (array/push toks (string cur)) (buffer/clear cur))
(buffer/push-byte cur b)))
(when (> (length cur) 0) (array/push toks (string cur)))
toks)
(register-tagged-methods! :jolt/string-tokenizer
@{"hasMoreTokens" (fn [self] (< (self :pos) (length (self :toks))))
"countTokens" (fn [self] (- (length (self :toks)) (self :pos)))
"nextToken" (fn [self]
(if (< (self :pos) (length (self :toks)))
(let [t (in (self :toks) (self :pos))]
(put self :pos (+ 1 (self :pos))) t)
(error "NoSuchElementException")))})
(each nm ["StringTokenizer" "java.util.StringTokenizer"]
(register-class-ctor! nm (fn [s &opt delims]
@{:jolt/type :jolt/string-tokenizer
:toks (tokenize s (or delims " \t\n\r\f"))
:pos 0})))
# clojure.lang.MapEntry: a 2-tuple, jolt's map-entry representation.
(each nm ["MapEntry" "clojure.lang.MapEntry"]
(register-class-ctor! nm (fn [k v] [k v])))
# (String. bytes) / (String. bytes charset): UTF-8 bytes to string.
(each nm ["String" "java.lang.String"]
(register-class-ctor! nm (fn [x &opt charset] (string x))))
# java.net.URL: enough for selmer's template cache — file: URLs only. # java.net.URL: enough for selmer's template cache — file: URLs only.
# A protocol-less spec throws (selmer catches MalformedURLException and # A protocol-less spec throws (selmer catches MalformedURLException and
# prepends file:///), and getPath hands back a stat-able filesystem path. # prepends file:///), and getPath hands back a stat-able filesystem path.

View file

@ -1,12 +1,25 @@
; Jolt Standard Library: jolt.http ; Jolt Standard Library: jolt.http
; HTTP client using Janet's net/ module. ; HTTP client over spork/http (janet.spork.http/*; requires `jpm install spork`).
; Responses come back as {:status int :headers map :body string}.
(defn- response->map [r]
;; clojure.core/get explicitly: this ns defines an http `get` that shadows it
{:status (clojure.core/get r :status)
:body (str (or (janet.spork.http/read-body r) ""))
:headers (reduce (fn [m kv] (assoc m (str (nth kv 0)) (str (nth kv 1))))
{}
(janet/pairs (or (clojure.core/get r :headers) (janet/struct))))})
(defn- header-struct [headers]
(apply janet/struct
(mapcat (fn [kv] [(str (key kv)) (str (val kv))]) (seq (or headers {})))))
(defn get (defn get
[url & {:keys [headers]}] [url & {:keys [headers]}]
(let [result (net/request url :get headers {})] (response->map
{:status (result :status) :body (result :body) :headers (result :headers)})) (janet.spork.http/request "GET" url :headers (header-struct headers))))
(defn post (defn post
[url body & {:keys [headers]}] [url body & {:keys [headers]}]
(let [result (net/request url :post headers body)] (response->map
{:status (result :status) :body (result :body) :headers (result :headers)})) (janet.spork.http/request "POST" url :body body :headers (header-struct headers))))

View file

@ -414,6 +414,17 @@
(do (buffer/push-string buf (string/from-bytes c)) (++ i))))) (do (buffer/push-string buf (string/from-bytes c)) (++ i)))))
(string buf)) (string buf))
(defn- replacement-for
"One match's replacement text. A string replacement gets $N expansion; a FN
replacement (Clojure: fn of the match — string, or [whole g1 ...] when the
pattern has groups) is called and its result used literally."
[replacement g ngroups]
(cond
(string? replacement) (expand-replacement replacement g)
(or (function? replacement) (cfunction? replacement))
(string (replacement (groups->result g ngroups)))
(string replacement)))
(defn re-replace-all [re s replacement] (defn re-replace-all [re s replacement]
(def re (re-pattern re)) (def re (re-pattern re))
(def buf @"") (var pos 0) (var last 0) (def buf @"") (var pos 0) (var last 0)
@ -421,7 +432,7 @@
(def g (match-at re s pos)) (def g (match-at re s pos))
(if (and g (> (length (in g 0)) 0)) (if (and g (> (length (in g 0)) 0))
(do (buffer/push-string buf (string/slice s last pos)) (do (buffer/push-string buf (string/slice s last pos))
(buffer/push-string buf (if (string? replacement) (expand-replacement replacement g) replacement)) (buffer/push-string buf (replacement-for replacement g (re :ngroups)))
(set pos (+ pos (length (in g 0)))) (set pos (+ pos (length (in g 0))))
(set last pos)) (set last pos))
(++ pos))) (++ pos)))
@ -439,6 +450,6 @@
(if done (if done
(let [[p g] done] (let [[p g] done]
(string (string/slice s 0 p) (string (string/slice s 0 p)
(if (string? replacement) (expand-replacement replacement g) replacement) (replacement-for replacement g (re :ngroups))
(string/slice s (+ p (length (in g 0)))))) (string/slice s (+ p (length (in g 0))))))
s)) s))

View file

@ -125,6 +125,32 @@
) )
(rmrf gbase) (rmrf gbase)
# --- :jpm/module deps: janet libraries installed through jpm -----------------
# Verification only (jpm owns installation): an importable module passes and
# contributes no roots; a missing one errors with the install hint. jpm/pm is
# always importable wherever jpm itself runs (CI included).
(do
(def jbase (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-deps-jpm-" (os/time)))
(mkdirs (string jbase "/src"))
(spit (string jbase "/deps.edn")
`{:paths ["src"] :deps {janet/jpm-pm {:jpm/module "jpm/pm"}}}`)
(def cwd (os/cwd))
(os/cd jbase)
(def roots (deps/resolve-deps "deps.edn" (string jbase "/jpm_tree")))
(os/cd cwd)
(check "jpm module dep resolves" true (not (nil? roots)))
(check "jpm module contributes no roots" 1 (length roots))
(spit (string jbase "/deps.edn")
`{:paths ["src"] :deps {janet/nope {:jpm/module "no/such-module-xyz"}}}`)
(os/cd jbase)
(def r (protect (deps/resolve-deps "deps.edn" (string jbase "/jpm_tree"))))
(os/cd cwd)
(check "missing jpm module errors" false (r 0))
(check "error carries the install hint" true
(not (nil? (string/find "jpm install" (string (r 1))))))
(rmrf jbase))
(if (> fails 0) (if (> fails 0)
(error (string "deps-resolve-test: " fails " failing check(s)")) (error (string "deps-resolve-test: " fails " failing check(s)"))
(print "\nAll deps-resolve tests passed!")) (print "\nAll deps-resolve tests passed!"))

View file

@ -140,3 +140,49 @@
"(string? (get (into {} (map (fn [[k v]] [k v]) (System/getenv))) \"HOME\"))"] "(string? (get (into {} (map (fn [[k v]] [k v]) (System/getenv))) \"HOME\"))"]
["System/getProperties" "true" ["System/getProperties" "true"
"(string? (get (System/getProperties) \"os.name\"))"]) "(string? (get (System/getProperties) \"os.name\"))"])
# ring-core enablement (host shims + protocol/reduce fixes): the java.net /
# java.util surface ring.util.codec needs, extend-protocol on Map and nil,
# and reduce over a reified clojure.lang.IReduceInit.
(defspec "host-interop / ring-codec surface"
["URLEncoder www form" "\"a+b%3Dc\"" "(URLEncoder/encode \"a b=c\")"]
["URLDecoder www form" "\"a b=c\"" "(URLDecoder/decode \"a+b%3Dc\" (Charset/forName \"UTF-8\"))"]
["url round trip" "\"x &=%?\"" "(URLDecoder/decode (URLEncoder/encode \"x &=%?\"))"]
["Base64 encode" "\"aGVsbG8=\"" "(String. (.encode (Base64/getEncoder) (.getBytes \"hello\")))"]
["Base64 round trip" "\"hello\"" "(String. (.decode (Base64/getDecoder) (String. (.encode (Base64/getEncoder) (.getBytes \"hello\")))))"]
["Integer radix + byteValue" "-1" "(.byteValue (Integer/valueOf \"ff\" 16))"]
["Integer parseInt" "255" "(Integer/parseInt \"ff\" 16)"]
["StringTokenizer" "[\"a=1\" \"b=2\"]" "(let [t (StringTokenizer. \"a=1&b=2\" \"&\")] [(.nextToken t) (.nextToken t)])"]
["MapEntry key/val" "[:a 1]" "(let [e (MapEntry. :a 1)] [(key e) (val e)])"]
["String ctor from bytes" "\"hi\"" "(String. (.getBytes \"hi\"))"]
["extend-protocol Map" ":map"
"(do (defprotocol Pe (pe [x])) (extend-protocol Pe Map (pe [m] :map) Object (pe [o] :obj)) (pe {:a 1}))"]
["extend-protocol nil" ":nil"
"(do (defprotocol Pn (pn [x])) (extend-protocol Pn nil (pn [n] :nil) Object (pn [o] :obj)) (pn nil))"]
["extend-protocol Map covers sorted" ":map"
"(do (defprotocol Ps (ps [x])) (extend-protocol Ps Map (ps [m] :map) Object (ps [o] :obj)) (ps (sorted-map 1 2)))"]
["reduce over reified IReduceInit" "42"
"(reduce + 0 (reify clojure.lang.IReduceInit (reduce [_ f init] (f (f init 40) 2))))"])
# ring-core enablement, part 2: class-name symbols evaluate to canonical
# class-name strings (so class-dispatch defmultis match), ctor sugar still
# constructs, type-hinted param vectors parse, slurp drains reader shims,
# str/replace takes fn replacements, and int needles are char codes.
(defspec "host-interop / class tokens & readers"
["class name evaluates to canonical string" "\"java.lang.String\"" "String"]
["dispatch-only class name" "\"java.io.InputStream\"" "InputStream"]
["(class x) matches the token" "true" "(= String (class \"abc\"))"]
["defmulti on class dispatches" ":str"
"(do (defmulti cm (fn [x] (class x))) (defmethod cm String [x] :str) (cm \"a\"))"]
["defmethod on nil dispatch value" ":nil"
"(do (defmulti cn (fn [x] (class x))) (defmethod cn nil [x] :nil) (defmethod cn String [x] :str) (cn nil))"]
["ctor sugar still constructs" "\"x\"" "(.toString (StringBuilder. \"x\"))"]
["return-hinted defn parses" "7" "(do (defn- hb ^bytes [b] b) (hb 7))"]
["hinted multi-arity parses" ":two" "((fn ([x] :one) (^String [x y] :two)) 1 2)"]
["slurp drains a StringReader" "\"a=1\"" "(slurp (StringReader. \"a=1\"))"]
["slurp accepts :encoding opts" "\"b\"" "(slurp (StringReader. \"b\") :encoding \"UTF-8\")"]
["replace with fn replacement is literal" "\"$0\""
"(do (require (quote [clojure.string :as s9])) (s9/replace \"x\" #\".\" (fn [m] \"$0\")))"]
["replace fn gets group vector" "\"v=k\""
"(do (require (quote [clojure.string :as s9])) (s9/replace \"k=v\" #\"(\\w+)=(\\w+)\" (fn [[_ k v]] (str v \"=\" k))))"]
["indexOf int needle is a char code" "1" "(.indexOf \"a=b\" 61)"])