From dd4100db73aaa6a7a5d116faed1c1fc758072038 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Mon, 15 Jun 2026 08:51:34 -0400 Subject: [PATCH] Refactor phase 5c: dispatch core collection ops on :jolt/type via one case (jolt-bvek) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core-count/core-seq/core-conj each walked a chain of (and (table? x) (= :jolt/X (get x :jolt/type))) predicates — re-fetching the type tag per predicate and, for conj, a 14-deep nested if. Replace with a single (case (get coll :jolt/type) ...) per op: the type is read once and the arm calls the concrete op directly. Host values (tuple/array/nil) and tuple-based shape-recs carry no :jolt/type and stay in a per-op fallback cond (shape-rec? kept before tuple? — shape-recs ARE tuples). Factor the two map-conj paths (phm vs struct) into one conj-into-map parameterized by the assoc fn. Behavior-preserving: the :jolt/type tags are disjoint, so the case is an exact re-expression of the predicate chain; the fallbacks reproduce the originals (incl. count erroring on a raw host table, which only seq ever handled). This is the perf-neutral realization of the planned collection vtable. A shared :jolt/type->{ops} table was tried first and REGRESSED core-bench ~2-4% (table lookup + indirect call on the hot path); the inline case instead runs ~1.7% FASTER than main (4864ms vs ~4950ms baseline) since one type-get + a jump beats the sequential predicate chain. Full gate green (suite >=4695/88, fixpoint). --- src/jolt/core_coll.janet | 196 +++++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 99 deletions(-) diff --git a/src/jolt/core_coll.janet b/src/jolt/core_coll.janet index 6ce4901..be732aa 100644 --- a/src/jolt/core_coll.janet +++ b/src/jolt/core_coll.janet @@ -36,70 +36,59 @@ [coll op] (get (coll :ops) op)) +# Merge conj items onto a map receiver. assoc1 is phm-assoc for a phm receiver, +# map-assoc1 for a struct/host-table receiver — the only thing that differs +# between the two map-conj paths. +(defn- conj-into-map [coll xs assoc1] + (var result coll) + (each x xs + (cond + # conj nil onto a map is a no-op (Clojure) + (nil? x) nil + # conj a map -> merge its entries + (map-value? x) + (each e (map-entries-of x) (set result (assoc1 result (in e 0) (in e 1)))) + # a [k v] entry: exactly a 2-element vector (Clojure throws otherwise — and + # merge inherits this strictness through conj) + (and (or (pvec? x) (tuple? x) (array? x)) (= 2 (vcount x))) + (set result (assoc1 result (vnth x 0) (vnth x 1))) + (error "Vector arg to map conj must be a pair"))) + result) + +# Dispatch is on :jolt/type via one case (the type is fetched once and the arm +# calls the concrete op directly) rather than a chain of (and (table? x) (= ..)) +# predicates — same hot-path cost as the predicate chain for the common types, +# one place per op. Host values (tuple/array/nil) and tuple-based shape-recs +# carry no :jolt/type and stay in the per-op fallback. (defn core-conj [& args] (if (= 0 (length args)) (make-vec @[]) # (conj) -> [] (let [coll (first args) xs (tuple/slice args 1)] - (if (nil? coll) - # conj onto nil builds a list (prepends): (conj nil 1 2) -> (2 1) - (do (var result nil) (each x xs (set result (pl-cons x result))) result) - (if (core-sorted? coll) - ((sorted-op coll :conj) coll xs) - (if (pvec? coll) - (do (var result coll) (each x xs (set result (pv-conj result x))) result) - (if (plist? coll) - # list: prepend, O(1) per element via structural sharing - (do (var result coll) (each x xs (set result (pl-cons x result))) result) - (if (tuple? coll) - (tuple/slice (tuple ;(array/concat (array/slice coll) xs))) - (if (array? coll) - (if mutable? - # mutable mode: arrays are vectors — append in place - (do (each x xs (array/push coll x)) coll) - # immutable mode: arrays are lists — prepend onto a persistent cons node, - # sharing the original array as the tail (O(1) per element, no copy) - (do (var result coll) (each x xs (set result (pl-cons x result))) result)) - (if (set? coll) - (apply phs-conj coll xs) - # conj onto a seq prepends (Clojure: a Cons cell). Without this branch a - # lazy-seq fell into the MAP fallback below — clojure.data/diff relies on - # (conj seq x) via set/union over (keys m), which is now a lazy seq. - (if (lazy-seq? coll) - (do (var result coll) - (each x xs (set result (pl-cons x (realize-for-iteration result)))) - result) - (if (phm? coll) - (do - (var result coll) - (each x xs - (cond - # conj nil onto a map is a no-op (Clojure) - (nil? x) nil - (map-value? x) - # conj a map -> merge its entries - (each e (map-entries-of x) - (set result (phm-assoc result (in e 0) (in e 1)))) - # a [k v] entry: exactly a 2-element vector (Clojure throws - # otherwise — and merge inherits this strictness through conj) - (and (or (pvec? x) (tuple? x) (array? x)) (= 2 (vcount x))) - (set result (phm-assoc result (vnth x 0) (vnth x 1))) - (error "Vector arg to map conj must be a pair"))) - result) - (do - (var result coll) - (each x xs - (cond - # conj nil onto a map is a no-op (Clojure) - (nil? x) nil - (map-value? x) - # conj a map -> merge its entries - (each e (map-entries-of x) - (set result (map-assoc1 result (in e 0) (in e 1)))) - # a [k v] entry: exactly a 2-element vector (Clojure throws - # otherwise — and merge inherits this strictness through conj) - (and (or (pvec? x) (tuple? x) (array? x)) (= 2 (vcount x))) - (set result (map-assoc1 result (vnth x 0) (vnth x 1))) - (error "Vector arg to map conj must be a pair"))) - result))))))))))))) + (if (table? coll) + (case (get coll :jolt/type) + :jolt/pvec (do (var r coll) (each x xs (set r (pv-conj r x))) r) + :jolt/phm (conj-into-map coll xs phm-assoc) + # list: prepend, O(1) per element via structural sharing + :jolt/plist (do (var r coll) (each x xs (set r (pl-cons x r))) r) + # conj onto a seq prepends (Clojure: a Cons cell) + :jolt/lazy-seq (do (var r coll) (each x xs (set r (pl-cons x (realize-for-iteration r)))) r) + :jolt/set (apply phs-conj coll xs) + :jolt/sorted-map ((sorted-op coll :conj) coll xs) + :jolt/sorted-set ((sorted-op coll :conj) coll xs) + # other tables (raw host table / deftype instance) conj like a map + (conj-into-map coll xs map-assoc1)) + (cond + # conj onto nil builds a list (prepends): (conj nil 1 2) -> (2 1) + (nil? coll) (do (var r nil) (each x xs (set r (pl-cons x r))) r) + (tuple? coll) (tuple/slice (tuple ;(array/concat (array/slice coll) xs))) + (array? coll) + (if mutable? + # mutable mode: arrays are vectors — append in place + (do (each x xs (array/push coll x)) coll) + # immutable mode: arrays are lists — prepend onto a persistent cons + # node, sharing the original array as the tail (O(1) per element) + (do (var r coll) (each x xs (set r (pl-cons x r))) r)) + # struct map literal: merge entries + (conj-into-map coll xs map-assoc1)))))) (defn core-assoc [m & kvs] (when (odd? (length kvs)) @@ -280,20 +269,26 @@ # the collection fns (conj/assoc/get/contains?/…) can branch on them. (defn core-count [coll] - (cond - (nil? coll) 0 - (shape-rec? coll) (shape-count coll) - (core-transient? coll) (length (if (= :vector (coll :kind)) (coll :arr) (coll :tbl))) - (core-sorted? coll) ((sorted-op coll :count) coll) - (lazy-seq? coll) (ls-count coll) - (pvec? coll) (pv-count coll) - (plist? coll) (pl-count coll) - (set? coll) (coll :cnt) - (phm? coll) (coll :cnt) - (and (table? coll) (get coll :jolt/deftype)) (- (length (keys coll)) 1) - (or (string? coll) (buffer? coll) (struct? coll) (tuple? coll) (array? coll)) (length coll) - # count is undefined on scalars (numbers/keywords/symbols/booleans/chars) - (error (string "count not supported on " (type coll))))) + (if (table? coll) + (case (get coll :jolt/type) + :jolt/pvec (pv-count coll) + :jolt/phm (coll :cnt) + :jolt/plist (pl-count coll) + :jolt/set (coll :cnt) + :jolt/lazy-seq (ls-count coll) + :jolt/sorted-map ((sorted-op coll :count) coll) + :jolt/sorted-set ((sorted-op coll :count) coll) + :jolt/transient (length (if (= :vector (coll :kind)) (coll :arr) (coll :tbl))) + # other tables: a deftype record instance counts its fields; a raw host + # table is unsupported (matches the original — seq handles it, count never did) + (if (get coll :jolt/deftype) (- (length (keys coll)) 1) + (error (string "count not supported on " (type coll))))) + (cond + (nil? coll) 0 + (shape-rec? coll) (shape-count coll) # shape-recs are tuples, not tables + (or (string? coll) (buffer? coll) (struct? coll) (tuple? coll) (array? coll)) (length coll) + # count is undefined on scalars (numbers/keywords/symbols/booleans/chars) + (error (string "count not supported on " (type coll)))))) (defn core-first [coll] (cond @@ -375,31 +370,34 @@ (pl-cons x (realize-for-iteration coll)))) (defn core-seq [coll] - (cond - (shape-rec? coll) (tuple ;(map (fn [k] (tuple k (shape-get coll k nil))) (shape-keys coll))) - (core-sorted? coll) ((sorted-op coll :seq) coll) - (or (nil? coll) (and (or (tuple? coll) (array? coll)) (= 0 (length coll)))) nil - # Cell-based emptiness, NOT (nil? (ls-first)): a lazy-seq whose first element - # is legitimately nil is non-empty, so (seq (cons nil ...)) must not be nil. - (lazy-seq? coll) (let [cell (realize-ls coll)] + (if (table? coll) + (case (get coll :jolt/type) + :jolt/pvec (if (= 0 (pv-count coll)) nil (tuple ;(pv->array coll))) + # empty maps/sets seq to nil, as in Clojure ((seq {}) is nil, not ()) + :jolt/phm (if (= 0 (coll :cnt)) nil (tuple ;(phm-entries coll))) + :jolt/plist (if (pl-empty? coll) nil (tuple ;(pl->array coll))) + # Cell-based emptiness, NOT (nil? (ls-first)): a lazy-seq whose first + # element is legitimately nil is non-empty, so (seq (cons nil ...)) is not nil. + :jolt/lazy-seq (let [cell (realize-ls coll)] (if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell))) nil coll)) - (pvec? coll) (if (= 0 (pv-count coll)) nil (tuple ;(pv->array coll))) - (plist? coll) (if (pl-empty? coll) nil (tuple ;(pl->array coll))) - (buffer? coll) (if (= 0 (length coll)) nil (let [a @[]] (each x coll (array/push a x)) (tuple ;a))) - # empty maps/sets seq to nil, as in Clojure ((seq {}) is nil, not ()) - (set? coll) (if (= 0 (coll :cnt)) nil (phs-seq coll)) - (phm? coll) (if (= 0 (coll :cnt)) nil (tuple ;(phm-entries coll))) - (tuple? coll) (tuple/slice coll) - (string? coll) (if (= 0 (length coll)) nil (tuple ;(map make-char (string/bytes coll)))) - (struct? coll) (if (= 0 (length coll)) nil (tuple ;(map (fn [k] (tuple k (get coll k))) (keys coll)))) - (array? coll) (tuple ;coll) - (and (table? coll) (get coll :jolt/deftype)) coll - # raw host table (System/getenv result) seqs like a map: kv entries - (and (table? coll) (nil? (get coll :jolt/type))) - (if (= 0 (length coll)) nil - (tuple ;(map (fn [k] (tuple k (get coll k))) (keys coll)))) - # scalars/functions aren't seqable - (error (string "seq not supported on " (type coll))))) + :jolt/set (if (= 0 (coll :cnt)) nil (phs-seq coll)) + :jolt/sorted-map ((sorted-op coll :seq) coll) + :jolt/sorted-set ((sorted-op coll :seq) coll) + # deftype instance seqs as itself; raw host table (System/getenv) as kv map + (if (get coll :jolt/deftype) coll + (if (= 0 (length coll)) nil + (tuple ;(map (fn [k] (tuple k (get coll k))) (keys coll)))))) + (cond + # shape-recs are tuples — must precede the tuple? branch + (shape-rec? coll) (tuple ;(map (fn [k] (tuple k (shape-get coll k nil))) (shape-keys coll))) + (nil? coll) nil + (buffer? coll) (if (= 0 (length coll)) nil (let [a @[]] (each x coll (array/push a x)) (tuple ;a))) + (tuple? coll) (if (= 0 (length coll)) nil (tuple/slice coll)) + (string? coll) (if (= 0 (length coll)) nil (tuple ;(map make-char (string/bytes coll)))) + (struct? coll) (if (= 0 (length coll)) nil (tuple ;(map (fn [k] (tuple k (get coll k))) (keys coll)))) + (array? coll) (if (= 0 (length coll)) nil (tuple ;coll)) + # scalars/functions aren't seqable + (error (string "seq not supported on " (type coll)))))) (defn core-vec [coll] (when (not (or (nil? coll) (core-coll? coll) (string? coll)))