diff --git a/README.md b/README.md index 46cb72b..2628688 100644 --- a/README.md +++ b/README.md @@ -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. - **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 (`(?…)`). - **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, `#"…"`). diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 12bdc63..56dc573 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -40,6 +40,11 @@ [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 # 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!. @@ -349,6 +354,11 @@ (defn core-get [m k &opt default] (default default nil) (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 (phm? m) (phm-get m k default) (if (pvec? m) @@ -358,7 +368,7 @@ (if (nil? v) default v)) (if (and (or (tuple? m) (array? m)) (number? k) (>= k 0) (< k (length m))) (in m k) - default))))))) + default)))))))) # Runtime invoke dispatch for COMPILED code (interpreter uses evaluator's # jolt-invoke). Handles real functions plus Clojure IFn collections. @@ -409,6 +419,10 @@ (if (nil? current) default current)) (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 (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))) @@ -416,7 +430,7 @@ (if (table? coll) (not (nil? (coll key))) (if (or (tuple? coll) (array? coll)) (and (number? key) (>= key 0) (< key (length coll))) - false))))))) + false)))))))) # 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. @@ -437,6 +451,7 @@ (defn core-count [coll] (cond (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-set? coll) (length (coll :items)) (lazy-seq? coll) (ls-count coll) @@ -945,6 +960,8 @@ (defn core-nth "Return the nth element of a sequential collection." [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) (let [a (pl->array coll)] (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 (nil? default) (error (string "Index " idx " out of bounds, length: " (length c))) - default))))))) + default)))))))) (defn core-sort "(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) "(" ")") (set? v) (pr-render-seq buf (phs-seq v) "#{" "}") (phm? v) (pr-render-pairs buf (phm-entries v)) + (core-transient? v) (buffer/push-string buf (string "#")) (pvec? v) (pr-render-seq buf (pv->array v) "[" "]") (plist? v) (pr-render-seq buf (pl->array v) "(" ")") (and (table? v) (get v :jolt/deftype)) (buffer/push-string buf (string v)) @@ -1591,7 +1609,6 @@ (case (length a) 0 (rf) 1 (rf (a 0)) (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-random-sample [prob & rest] (if (= 0 (length rest)) @@ -3048,14 +3065,73 @@ (rf (in a 0) (in a 1)))))))) (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- -# only aliases (no in-place optimization, but semantically correct). -(defn core-transient [coll] coll) -(defn core-persistent! [coll] coll) -(defn core-conj! [coll & xs] (apply core-conj coll xs)) -(defn core-assoc! [coll & kvs] (apply core-assoc coll kvs)) -(defn core-dissoc! [coll & ks] (apply core-dissoc coll ks)) -(defn core-pop! [coll] (core-pop coll)) +# Transients — real mutable scratch collections backed by Janet's native arrays +# and tables (host interop): O(1) conj!/assoc!/dissoc!/disj!/pop!, frozen back to +# a persistent value by persistent!. A transient is a tagged table holding either +# a Janet array (vectors) or a Janet table keyed by canonical key (maps/sets, so +# collection keys still compare by value). The mutating ops return the transient. +(defn core-transient [coll] + (cond + (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. (defn core-unchecked-add [a b] (+ a b)) @@ -3219,6 +3295,7 @@ "halt-when" core-halt-when "re-groups" core-re-groups "transient" core-transient + "transient?" core-transient? "persistent!" core-persistent! "conj!" core-conj! "assoc!" core-assoc! diff --git a/src/jolt/main.janet b/src/jolt/main.janet index 1bfd558..61f0d30 100644 --- a/src/jolt/main.janet +++ b/src/jolt/main.janet @@ -101,6 +101,9 @@ (write-value k buf)) (push-str buf "}")) + (and (table? v) (= :jolt/transient (v :jolt/type))) + (push-str buf (string "#")) + (phm? v) (do (push-str buf "{") diff --git a/test/spec/transients-spec.janet b/test/spec/transients-spec.janet new file mode 100644 index 0000000..c5017a2 --- /dev/null +++ b/test/spec/transients-spec.janet @@ -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}))"])