feat: real transients backed by Janet arrays/tables (interop)
Replace the correctness-only transient aliases with real mutable scratch collections via host interop: - transient vector -> a Janet array; conj!/assoc!/pop! mutate in place - transient map -> a Janet table keyed by canonical key (collection keys still compare by value); assoc!/dissoc!/conj! mutate in place - transient set -> a Janet table; conj!/disj! mutate in place - persistent! freezes back to a pvec / phm / phs - count/nth/get/contains? work on transients; transient? predicate added Building a map/set this way avoids the persistent path's per-step bucket-array copying (transient map build ~35% faster at 20k here); vectors are comparable since pvec conj is already ~O(1). The mutating ops return the transient and the source collection is untouched. spec/transients-spec (34 cases). conformance 218/218, jpm test green.
This commit is contained in:
parent
a0943cb944
commit
f38d402445
4 changed files with 140 additions and 13 deletions
|
|
@ -80,7 +80,8 @@ Jolt targets Clojure semantics but runs on Janet, not the JVM. The notable diver
|
||||||
- **Concurrency / STM.** Single-threaded. No refs, `dosync`, agents, or `send`; `locking` evaluates its body without real locking. Atoms, volatiles, and delays are supported.
|
- **Concurrency / STM.** Single-threaded. No refs, `dosync`, agents, or `send`; `locking` evaluates its body without real locking. Atoms, volatiles, and delays are supported.
|
||||||
- **Regex.** Compiled to Janet's PEG engine (Janet has no regex). Supported: capturing groups (`[whole g1 …]`), greedy and lazy quantifiers with backtracking, `(?:…)`, lookahead `(?=…)`/`(?!…)`, alternation, anchors `^ $ \b \B`, character classes, and the `(?i)` flag. Not supported: lookbehind, backreferences (`\1`), and named groups (`(?<name>…)`).
|
- **Regex.** Compiled to Janet's PEG engine (Janet has no regex). Supported: capturing groups (`[whole g1 …]`), greedy and lazy quantifiers with backtracking, `(?:…)`, lookahead `(?=…)`/`(?!…)`, alternation, anchors `^ $ \b \B`, character classes, and the `(?i)` flag. Not supported: lookbehind, backreferences (`\1`), and named groups (`(?<name>…)`).
|
||||||
- **Arrays.** Java-style arrays map onto Janet's native types: `byte-array` is a Janet buffer (contiguous, C-backed); `object-array`/`int-array`/`double-array`/etc. are Janet arrays. `aget`/`aset`/`alength`/`aclone` work over both.
|
- **Arrays.** Java-style arrays map onto Janet's native types: `byte-array` is a Janet buffer (contiguous, C-backed); `object-array`/`int-array`/`double-array`/etc. are Janet arrays. `aget`/`aset`/`alength`/`aclone` work over both.
|
||||||
- **Not implemented.** JVM reflection, `proxy`, and the `clojure.repl`/`clojure.template` namespaces. Transients (`transient`/`conj!`/`persistent!`) work but are correctness-only aliases over the persistent collections (no in-place speedup).
|
- **Transients.** `transient`/`conj!`/`assoc!`/`dissoc!`/`disj!`/`pop!`/`persistent!` are real mutable scratch collections backed by Janet's native arrays and tables (vectors → arrays, maps/sets → tables), so building a collection with them avoids the per-step copying of the persistent path (notably for maps/sets). `persistent!` freezes back to a persistent value.
|
||||||
|
- **Not implemented.** JVM reflection, `proxy`, and the `clojure.repl`/`clojure.template` namespaces.
|
||||||
|
|
||||||
Supported and Clojure-compatible: chars as a distinct type, lazy/infinite sequences, transducers, destructuring, multimethods with hierarchies, protocols/records (`deftype`/`defrecord`/`reify`/`extend-protocol`), metadata, namespaces, and the reader (`#()`, `#_`, `#?`, tagged literals, `#"…"`).
|
Supported and Clojure-compatible: chars as a distinct type, lazy/infinite sequences, transducers, destructuring, multimethods with hierarchies, protocols/records (`deftype`/`defrecord`/`reify`/`extend-protocol`), metadata, namespaces, and the reader (`#()`, `#_`, `#?`, tagged literals, `#"…"`).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@
|
||||||
[xs]
|
[xs]
|
||||||
(if mutable? (array ;xs) (pv-from-indexed xs)))
|
(if mutable? (array ;xs) (pv-from-indexed xs)))
|
||||||
|
|
||||||
|
(defn core-transient?
|
||||||
|
"True when x is a transient (a mutable scratch collection). See `transient`."
|
||||||
|
[x]
|
||||||
|
(and (table? x) (= :jolt/transient (get x :jolt/type))))
|
||||||
|
|
||||||
# Canonicalize a collection key/element to a value-hashable Janet struct/tuple so
|
# Canonicalize a collection key/element to a value-hashable Janet struct/tuple so
|
||||||
# the PHM/PHS treat value-equal maps/vectors as the same key (Janet hashes tables
|
# the PHM/PHS treat value-equal maps/vectors as the same key (Janet hashes tables
|
||||||
# by identity otherwise). Installed into phm via set-canonicalize-key!.
|
# by identity otherwise). Installed into phm via set-canonicalize-key!.
|
||||||
|
|
@ -349,6 +354,11 @@
|
||||||
(defn core-get [m k &opt default]
|
(defn core-get [m k &opt default]
|
||||||
(default default nil)
|
(default default nil)
|
||||||
(if (nil? m) default
|
(if (nil? m) default
|
||||||
|
(if (core-transient? m)
|
||||||
|
(case (m :kind)
|
||||||
|
:vector (if (and (number? k) (>= k 0) (< k (length (m :arr)))) (in (m :arr) k) default)
|
||||||
|
:map (let [p (get (m :tbl) (canon-key k))] (if p (in p 1) default))
|
||||||
|
:set (if (nil? (get (m :tbl) (canon-key k))) default k))
|
||||||
(if (set? m) (phs-get m k default)
|
(if (set? m) (phs-get m k default)
|
||||||
(if (phm? m) (phm-get m k default)
|
(if (phm? m) (phm-get m k default)
|
||||||
(if (pvec? m)
|
(if (pvec? m)
|
||||||
|
|
@ -358,7 +368,7 @@
|
||||||
(if (nil? v) default v))
|
(if (nil? v) default v))
|
||||||
(if (and (or (tuple? m) (array? m)) (number? k) (>= k 0) (< k (length m)))
|
(if (and (or (tuple? m) (array? m)) (number? k) (>= k 0) (< k (length m)))
|
||||||
(in m k)
|
(in m k)
|
||||||
default)))))))
|
default))))))))
|
||||||
|
|
||||||
# Runtime invoke dispatch for COMPILED code (interpreter uses evaluator's
|
# Runtime invoke dispatch for COMPILED code (interpreter uses evaluator's
|
||||||
# jolt-invoke). Handles real functions plus Clojure IFn collections.
|
# jolt-invoke). Handles real functions plus Clojure IFn collections.
|
||||||
|
|
@ -409,6 +419,10 @@
|
||||||
(if (nil? current) default current))
|
(if (nil? current) default current))
|
||||||
|
|
||||||
(defn core-contains? [coll key]
|
(defn core-contains? [coll key]
|
||||||
|
(if (core-transient? coll)
|
||||||
|
(case (coll :kind)
|
||||||
|
:vector (and (number? key) (>= key 0) (< key (length (coll :arr))))
|
||||||
|
(not (nil? (get (coll :tbl) (canon-key key)))))
|
||||||
(if (set? coll) (phs-contains? coll key)
|
(if (set? coll) (phs-contains? coll key)
|
||||||
(if (phm? coll) (let [b (get (coll :buckets) (phm-hash-key key))] (if b (phm-bucket-contains? b key) false))
|
(if (phm? coll) (let [b (get (coll :buckets) (phm-hash-key key))] (if b (phm-bucket-contains? b key) false))
|
||||||
(if (pvec? coll) (and (number? key) (>= key 0) (< key (pv-count coll)))
|
(if (pvec? coll) (and (number? key) (>= key 0) (< key (pv-count coll)))
|
||||||
|
|
@ -416,7 +430,7 @@
|
||||||
(if (table? coll) (not (nil? (coll key)))
|
(if (table? coll) (not (nil? (coll key)))
|
||||||
(if (or (tuple? coll) (array? coll))
|
(if (or (tuple? coll) (array? coll))
|
||||||
(and (number? key) (>= key 0) (< key (length coll)))
|
(and (number? key) (>= key 0) (< key (length coll)))
|
||||||
false)))))))
|
false))))))))
|
||||||
|
|
||||||
# Sorted collections — minimal: backed by a struct (map) / sorted array (set),
|
# Sorted collections — minimal: backed by a struct (map) / sorted array (set),
|
||||||
# ordered by key/element on read. Defined early so seq/count/get can dispatch.
|
# ordered by key/element on read. Defined early so seq/count/get can dispatch.
|
||||||
|
|
@ -437,6 +451,7 @@
|
||||||
(defn core-count [coll]
|
(defn core-count [coll]
|
||||||
(cond
|
(cond
|
||||||
(nil? coll) 0
|
(nil? coll) 0
|
||||||
|
(core-transient? coll) (length (if (= :vector (coll :kind)) (coll :arr) (coll :tbl)))
|
||||||
(core-sorted-map? coll) (length (keys (coll :map)))
|
(core-sorted-map? coll) (length (keys (coll :map)))
|
||||||
(core-sorted-set? coll) (length (coll :items))
|
(core-sorted-set? coll) (length (coll :items))
|
||||||
(lazy-seq? coll) (ls-count coll)
|
(lazy-seq? coll) (ls-count coll)
|
||||||
|
|
@ -945,6 +960,8 @@
|
||||||
(defn core-nth
|
(defn core-nth
|
||||||
"Return the nth element of a sequential collection."
|
"Return the nth element of a sequential collection."
|
||||||
[coll idx &opt default]
|
[coll idx &opt default]
|
||||||
|
(if (core-transient? coll)
|
||||||
|
(let [a (coll :arr)] (if (and (>= idx 0) (< idx (length a))) (in a idx) default))
|
||||||
(if (plist? coll)
|
(if (plist? coll)
|
||||||
(let [a (pl->array coll)]
|
(let [a (pl->array coll)]
|
||||||
(if (and (>= idx 0) (< idx (length a))) (in a idx)
|
(if (and (>= idx 0) (< idx (length a))) (in a idx)
|
||||||
|
|
@ -970,7 +987,7 @@
|
||||||
(if (string? c) (make-char (in c idx)) (in c idx))
|
(if (string? c) (make-char (in c idx)) (in c idx))
|
||||||
(if (nil? default)
|
(if (nil? default)
|
||||||
(error (string "Index " idx " out of bounds, length: " (length c)))
|
(error (string "Index " idx " out of bounds, length: " (length c)))
|
||||||
default)))))))
|
default))))))))
|
||||||
|
|
||||||
(defn core-sort
|
(defn core-sort
|
||||||
"(sort coll) or (sort comparator coll). Comparator may return a boolean or a
|
"(sort coll) or (sort comparator coll). Comparator may return a boolean or a
|
||||||
|
|
@ -1374,6 +1391,7 @@
|
||||||
(lazy-seq? v) (pr-render-seq buf (realize-for-iteration v) "(" ")")
|
(lazy-seq? v) (pr-render-seq buf (realize-for-iteration v) "(" ")")
|
||||||
(set? v) (pr-render-seq buf (phs-seq v) "#{" "}")
|
(set? v) (pr-render-seq buf (phs-seq v) "#{" "}")
|
||||||
(phm? v) (pr-render-pairs buf (phm-entries v))
|
(phm? v) (pr-render-pairs buf (phm-entries v))
|
||||||
|
(core-transient? v) (buffer/push-string buf (string "#<transient " (v :kind) ">"))
|
||||||
(pvec? v) (pr-render-seq buf (pv->array v) "[" "]")
|
(pvec? v) (pr-render-seq buf (pv->array v) "[" "]")
|
||||||
(plist? v) (pr-render-seq buf (pl->array v) "(" ")")
|
(plist? v) (pr-render-seq buf (pl->array v) "(" ")")
|
||||||
(and (table? v) (get v :jolt/deftype)) (buffer/push-string buf (string v))
|
(and (table? v) (get v :jolt/deftype)) (buffer/push-string buf (string v))
|
||||||
|
|
@ -1591,7 +1609,6 @@
|
||||||
(case (length a)
|
(case (length a)
|
||||||
0 (rf) 1 (rf (a 0))
|
0 (rf) 1 (rf (a 0))
|
||||||
(do (var acc (a 0)) (each x (realize-for-iteration (a 1)) (set acc (rf acc x))) acc))))
|
(do (var acc (a 0)) (each x (realize-for-iteration (a 1)) (set acc (rf acc x))) acc))))
|
||||||
(defn core-disj! [s & ks] (apply core-disj s ks))
|
|
||||||
(defn core-rationalize [x] x)
|
(defn core-rationalize [x] x)
|
||||||
(defn core-random-sample [prob & rest]
|
(defn core-random-sample [prob & rest]
|
||||||
(if (= 0 (length rest))
|
(if (= 0 (length rest))
|
||||||
|
|
@ -3048,14 +3065,73 @@
|
||||||
(rf (in a 0) (in a 1))))))))
|
(rf (in a 0) (in a 1))))))))
|
||||||
(defn core-re-groups [m] (error "re-groups: stateful matchers are not supported in Jolt"))
|
(defn core-re-groups [m] (error "re-groups: stateful matchers are not supported in Jolt"))
|
||||||
|
|
||||||
# Transients — Jolt's collections are persistent, so transients are correctness-
|
# Transients — real mutable scratch collections backed by Janet's native arrays
|
||||||
# only aliases (no in-place optimization, but semantically correct).
|
# and tables (host interop): O(1) conj!/assoc!/dissoc!/disj!/pop!, frozen back to
|
||||||
(defn core-transient [coll] coll)
|
# a persistent value by persistent!. A transient is a tagged table holding either
|
||||||
(defn core-persistent! [coll] coll)
|
# a Janet array (vectors) or a Janet table keyed by canonical key (maps/sets, so
|
||||||
(defn core-conj! [coll & xs] (apply core-conj coll xs))
|
# collection keys still compare by value). The mutating ops return the transient.
|
||||||
(defn core-assoc! [coll & kvs] (apply core-assoc coll kvs))
|
(defn core-transient [coll]
|
||||||
(defn core-dissoc! [coll & ks] (apply core-dissoc coll ks))
|
(cond
|
||||||
(defn core-pop! [coll] (core-pop coll))
|
(pvec? coll)
|
||||||
|
@{:jolt/type :jolt/transient :kind :vector :arr (pv->array coll)}
|
||||||
|
(set? coll)
|
||||||
|
(let [t @{}] (each e (phs-seq coll) (put t (canon-key e) e))
|
||||||
|
@{:jolt/type :jolt/transient :kind :set :tbl t})
|
||||||
|
(or (phm? coll) (and (struct? coll) (nil? (get coll :jolt/type))))
|
||||||
|
(let [t @{}]
|
||||||
|
(each pair (realize-for-iteration coll)
|
||||||
|
(put t (canon-key (in pair 0)) @[(in pair 0) (in pair 1)]))
|
||||||
|
@{:jolt/type :jolt/transient :kind :map :tbl t})
|
||||||
|
# mutable-build arrays (vectors/lists) — copy into a transient vector
|
||||||
|
(array? coll) @{:jolt/type :jolt/transient :kind :vector :arr (array/slice coll)}
|
||||||
|
(error (string "Don't know how to create a transient from " (type coll)))))
|
||||||
|
|
||||||
|
(defn- tr-conj! [t x]
|
||||||
|
(case (t :kind)
|
||||||
|
:vector (array/push (t :arr) x)
|
||||||
|
:set (put (t :tbl) (canon-key x) x)
|
||||||
|
:map (let [k (vnth x 0)] (put (t :tbl) (canon-key k) @[k (vnth x 1)])))
|
||||||
|
t)
|
||||||
|
|
||||||
|
(defn- tr-assoc! [t k v]
|
||||||
|
(case (t :kind)
|
||||||
|
:vector (let [a (t :arr)] (if (= k (length a)) (array/push a v) (put a k v)))
|
||||||
|
:map (put (t :tbl) (canon-key k) @[k v])
|
||||||
|
(error "assoc! expects a transient vector or map"))
|
||||||
|
t)
|
||||||
|
|
||||||
|
(defn core-conj! [t & xs]
|
||||||
|
(if (core-transient? t)
|
||||||
|
(do (each x xs (tr-conj! t x)) t)
|
||||||
|
(apply core-conj t xs))) # lenient fallback for a persistent coll
|
||||||
|
|
||||||
|
(defn core-assoc! [t & kvs]
|
||||||
|
(if (core-transient? t)
|
||||||
|
(do (var i 0) (while (< i (length kvs)) (tr-assoc! t (in kvs i) (in kvs (+ i 1))) (+= i 2)) t)
|
||||||
|
(apply core-assoc t kvs)))
|
||||||
|
|
||||||
|
(defn core-dissoc! [t & ks]
|
||||||
|
(if (core-transient? t)
|
||||||
|
(do (each k ks (put (t :tbl) (canon-key k) nil)) t)
|
||||||
|
(apply core-dissoc t ks)))
|
||||||
|
|
||||||
|
(defn core-disj! [t & xs]
|
||||||
|
(if (core-transient? t)
|
||||||
|
(do (each x xs (put (t :tbl) (canon-key x) nil)) t)
|
||||||
|
(apply core-disj t xs)))
|
||||||
|
|
||||||
|
(defn core-pop! [t]
|
||||||
|
(if (core-transient? t)
|
||||||
|
(do (array/pop (t :arr)) t)
|
||||||
|
(core-pop t)))
|
||||||
|
|
||||||
|
(defn core-persistent! [t]
|
||||||
|
(if (core-transient? t)
|
||||||
|
(case (t :kind)
|
||||||
|
:vector (make-vec (t :arr))
|
||||||
|
:set (do (var s (make-phs)) (each [_ e] (pairs (t :tbl)) (set s (phs-conj s e))) s)
|
||||||
|
:map (do (var m (make-phm)) (each [_ pair] (pairs (t :tbl)) (set m (phm-assoc m (in pair 0) (in pair 1)))) m))
|
||||||
|
t))
|
||||||
|
|
||||||
# Unchecked arithmetic — Jolt numbers don't overflow, so these are plain ops.
|
# Unchecked arithmetic — Jolt numbers don't overflow, so these are plain ops.
|
||||||
(defn core-unchecked-add [a b] (+ a b))
|
(defn core-unchecked-add [a b] (+ a b))
|
||||||
|
|
@ -3219,6 +3295,7 @@
|
||||||
"halt-when" core-halt-when
|
"halt-when" core-halt-when
|
||||||
"re-groups" core-re-groups
|
"re-groups" core-re-groups
|
||||||
"transient" core-transient
|
"transient" core-transient
|
||||||
|
"transient?" core-transient?
|
||||||
"persistent!" core-persistent!
|
"persistent!" core-persistent!
|
||||||
"conj!" core-conj!
|
"conj!" core-conj!
|
||||||
"assoc!" core-assoc!
|
"assoc!" core-assoc!
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,9 @@
|
||||||
(write-value k buf))
|
(write-value k buf))
|
||||||
(push-str buf "}"))
|
(push-str buf "}"))
|
||||||
|
|
||||||
|
(and (table? v) (= :jolt/transient (v :jolt/type)))
|
||||||
|
(push-str buf (string "#<transient " (v :kind) ">"))
|
||||||
|
|
||||||
(phm? v)
|
(phm? v)
|
||||||
(do
|
(do
|
||||||
(push-str buf "{")
|
(push-str buf "{")
|
||||||
|
|
|
||||||
46
test/spec/transients-spec.janet
Normal file
46
test/spec/transients-spec.janet
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Specification: transients (mutable scratch collections frozen by persistent!).
|
||||||
|
(use ../support/harness)
|
||||||
|
|
||||||
|
(defspec "transient / vector"
|
||||||
|
["conj! then persistent!" "[1 2]" "(persistent! (conj! (conj! (transient []) 1) 2))"]
|
||||||
|
["reduce conj!" "[0 1 2 3 4]" "(persistent! (reduce conj! (transient []) (range 5)))"]
|
||||||
|
["conj! many args" "[1 2 3]" "(persistent! (conj! (transient [1]) 2 3))"]
|
||||||
|
["assoc! existing" "[1 9 3]" "(persistent! (assoc! (transient [1 2 3]) 1 9))"]
|
||||||
|
["assoc! at count grows" "[1 2 3]" "(persistent! (assoc! (transient [1 2]) 2 3))"]
|
||||||
|
["pop!" "[1 2]" "(persistent! (pop! (transient [1 2 3])))"]
|
||||||
|
["from existing vector" "[1 2 3 4]" "(persistent! (conj! (transient [1 2 3]) 4))"]
|
||||||
|
["count" "3" "(count (transient [1 2 3]))"]
|
||||||
|
["nth" "2" "(nth (transient [1 2 3]) 1)"]
|
||||||
|
["get" "2" "(get (transient [1 2 3]) 1)"]
|
||||||
|
["persistent! is a vector" "true" "(vector? (persistent! (transient [1])))"]
|
||||||
|
["transient? true" "true" "(transient? (transient []))"]
|
||||||
|
["transient? false" "false" "(transient? [1 2])"])
|
||||||
|
|
||||||
|
(defspec "transient / map"
|
||||||
|
["assoc! then persistent!" "{:a 1, :b 2}" "(persistent! (assoc! (assoc! (transient {}) :a 1) :b 2))"]
|
||||||
|
["assoc! many" "{:a 1, :b 2}" "(persistent! (assoc! (transient {}) :a 1 :b 2))"]
|
||||||
|
["dissoc!" "{:b 2}" "(persistent! (dissoc! (transient {:a 1 :b 2}) :a))"]
|
||||||
|
["conj! map entry" "{:a 1}" "(persistent! (conj! (transient {}) [:a 1]))"]
|
||||||
|
["from existing map" "{:a 1, :b 2}" "(persistent! (assoc! (transient {:a 1}) :b 2))"]
|
||||||
|
["get" "1" "(get (transient {:a 1}) :a)"]
|
||||||
|
["get missing default" ":x" "(get (transient {:a 1}) :z :x)"]
|
||||||
|
["contains?" "true" "(contains? (transient {:a 1}) :a)"]
|
||||||
|
["count" "2" "(count (transient {:a 1 :b 2}))"]
|
||||||
|
["collection key by value" ":v" "(get (persistent! (assoc! (transient {}) [1 2] :v)) [1 2])"]
|
||||||
|
["persistent! is a map" "true" "(map? (persistent! (transient {:a 1})))"]
|
||||||
|
["reduce build" "{0 0, 1 1, 2 2}" "(persistent! (reduce (fn [t i] (assoc! t i i)) (transient {}) (range 3)))"])
|
||||||
|
|
||||||
|
(defspec "transient / set"
|
||||||
|
["conj! dedups" "#{1 2 3}" "(persistent! (conj! (transient #{}) 1 2 2 3))"]
|
||||||
|
["disj!" "#{1 3}" "(persistent! (disj! (transient #{1 2 3}) 2))"]
|
||||||
|
["from existing set" "#{1 2 3}" "(persistent! (conj! (transient #{1 2}) 3))"]
|
||||||
|
["contains?" "true" "(contains? (transient #{1 2}) 1)"]
|
||||||
|
["count" "2" "(count (transient #{1 2}))"]
|
||||||
|
["persistent! is a set" "true" "(set? (persistent! (transient #{1})))"]
|
||||||
|
["map elements by value" "1" "(count (persistent! (conj! (transient #{}) {:a 1} (hash-map :a 1))))"])
|
||||||
|
|
||||||
|
(defspec "transient / immutability of source"
|
||||||
|
["source vector unchanged" "true"
|
||||||
|
"(let [v [1 2 3] _ (persistent! (conj! (transient v) 4))] (= v [1 2 3]))"]
|
||||||
|
["source map unchanged" "true"
|
||||||
|
"(let [m {:a 1} _ (persistent! (assoc! (transient m) :b 2))] (= m {:a 1}))"])
|
||||||
Loading…
Add table
Add a link
Reference in a new issue