jolt/src/jolt/core_coll.janet
Dmitri Sotnikov 91e246c682
Persistent hash map: HAMT instead of O(n) copy-on-write (jolt-684u) (#136)
* Add benchmark suite for alloc/dispatch/collection workloads (jolt-1r86)

The ray tracer is float-compute-bound (devirt, alloc removal, type-proving all
measured flat on it), so it can't validate the optimization passes. Add a small
cross-language suite (AWFY + CLBG style, portable Clojure) isolating the axes it
misses:

  binary-trees  allocation / GC pressure (escaping short-lived records)
  dispatch      megamorphic protocol dispatch (~1M dispatches/s; WP can't devirt)
  collections   persistent map/vector churn

bench/run.sh runs them; bench/README.md maps each to the pass it exercises.

collections immediately surfaced jolt-684u: the persistent hash map is O(n) per
assoc (flat copy-on-write bucket array, not a HAMT) — n=4000 assocs take 50s.
Invisible to the ray tracer (no maps).

* Persistent hash map: HAMT instead of O(n) copy-on-write (jolt-684u)

The map was a flat bucket array whose assoc copied the whole array every insert
(O(n)/assoc, O(n^2) to build). Compounding it, small maps are Janet structs that
only promoted to phm for collection keys — never for size — so a scalar-key map
stayed an O(n)-copy struct forever. Building a 4000-entry map took ~50s.

Two fixes, following ClojureScript's design:

- phm.janet is now a HAMT (hash array mapped trie): BitmapIndexedNode /
  ArrayNode / HashCollisionNode, 32-way, 5 hash bits per level, structural
  sharing — assoc/dissoc/get are O(log32 n). Translated from cljs.core, adapted
  to Janet's 32-bit bit-ops (the hash is carried unsigned, the level index is
  extracted with arithmetic, and bits are tested with band against 1<<i since
  brushift rejects negative bitmaps). The public phm-* API and the value shape
  (:jolt/type :jolt/phm, :cnt) are unchanged; transients are a separate rep and
  untouched.

- core_coll promotes a struct map to a phm past 8 entries (not only for
  collection keys), mirroring cljs PersistentArrayMap -> PersistentHashMap, so
  incremental building isn't O(n^2).

20000 raw assocs: 7.1s -> 0.105s. The collections benchmark: 16.7s -> 0.2s.
Correctness covered by test/unit/phm-hamt-test.janet (oracle vs a Janet table,
nil keys, dissoc, a real hash-collision pair, and a sub-linear-assoc guard);
full gate green.

---------

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-16 05:01:22 +00:00

1145 lines
52 KiB
Text

# Jolt Core — collections, transducers, seqs, HOFs, constructors
# Extracted from core.janet (jolt-nma8, phase 2b split).
#
# REP vs API: this file holds the Clojure-facing collection ops and dispatches
# on `:jolt/type` over the internal persistent structures, whose representations
# live elsewhere: persistent vector → pv.janet, list → plist.janet, hash map →
# phm.janet, set → phs.janet, lazy seq → lazyseq.janet. Grep a structure's file
# header for the primitive (pv-*/pl-*/phm-*/phs-*/ls-*) it exposes.
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./regex)
(use ./config)
(use ./pv)
(use ./plist)
(use ./core_types)
# Collections
# ============================================================
# Small maps are Janet structs (native, O(1) get) but assoc copies them whole
# (O(n)); past this many entries a map promotes to the phm HAMT (O(log n) assoc)
# so incremental building isn't O(n^2). Mirrors cljs PersistentArrayMap's
# HASHMAP-THRESHOLD (jolt-684u).
(def- map-array-threshold 8)
# Is x a map value (for conj/merge semantics: conj-ing a map merges its entries)?
(defn- map-value? [x]
(or (phm? x) (and (struct? x) (nil? (get x :jolt/type)))))
# --- Sorted collections (sorted-map / sorted-set) -------------------------------
# Pure Clojure now (stage 3, jolt-0lj — jolt-core/clojure/core/25-sorted.clj).
# A sorted coll is a tagged table {:jolt/type .. :entries SORTED-VECTOR :cmp
# :ops {kw fn}} whose ops travel WITH the value, so the seed's dispatch
# branches below are each a one-line call through (coll :ops) — no module-level
# hooks, correct across contexts/forks/AOT images. The tag predicates and the
# entries view live near the top of this module (canon-key/empty?/equality
# need them); only this dispatch accessor is left here.
(defn sorted-op
"The overlay-attached implementation of `op` for sorted coll `coll`."
[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 (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))
(error "assoc expects an even number of key/value arguments"))
# assoc is defined on maps, vectors and nil; reject other shapes
(when (or (number? m) (string? m) (buffer? m) (keyword? m) (boolean? m)
(plist? m) (set? m) (core-transient? m) (core-sorted-set? m)
(and (struct? m) (get m :jolt/type)))
(error (string "assoc requires a map or vector, got " (type m))))
(cond
(shape-rec? m)
(do (var result m) (var i 0)
(while (< i (length kvs)) (set result (shape-assoc result (kvs i) (kvs (+ i 1)))) (+= i 2))
result)
(core-sorted-map? m) ((sorted-op m :assoc) m kvs)
(phm? m)
(do (var result m) (var i 0) (while (< i (length kvs)) (set result (phm-assoc result (kvs i) (kvs (+ i 1)))) (+= i 2)) result)
(pvec? m)
(do (var result m) (var i 0)
(while (< i (length kvs))
(let [idx (kvs i)]
(when (not (and (number? idx) (= idx (math/floor idx)) (>= idx 0) (<= idx (pv-count result))))
(error (string "Index " idx " out of bounds for assoc on a vector of length " (pv-count result))))
(set result (pv-assoc result idx (kvs (+ i 1)))))
(+= i 2)) result)
# vector: assoc by integer index (appending at count is allowed); stays a vector
(or (tuple? m) (array? m))
(do (var result (array/slice m)) (var i 0)
(while (< i (length kvs))
(let [idx (kvs i) v (kvs (+ i 1))]
(when (not (and (number? idx) (= idx (math/floor idx)) (>= idx 0) (<= idx (length result))))
(error (string "Index " idx " out of bounds for assoc on a vector of length " (length result))))
(if (= idx (length result)) (array/push result v) (put result idx v)))
(+= i 2))
(if (tuple? m) (tuple/slice (tuple ;result)) result))
# map (struct/table). Promote to a phm when (a) any new key is a collection
# (a Janet struct/table would key it by identity) or any new key/value is nil
# (a struct drops nil; phm preserves it), or (b) the result would exceed the
# small-map threshold — a Janet struct copies wholesale on assoc (O(n)), so a
# growing map must ride the phm HAMT (O(log n)) past ~8 entries. Mirrors cljs
# PersistentArrayMap -> PersistentHashMap (jolt-684u). m is a struct here
# (phm handled above), so only the current size + new kvs matter.
(let [coll-key (do (var c false) (var i 0)
(while (< i (length kvs))
(let [k (in kvs i) v (in kvs (+ i 1))]
(when (or (table? k) (array? k) (nil? k) (nil? v)) (set c true)))
(+= i 2)) c)
promote (or coll-key
(> (+ (if m (length m) 0) (/ (length kvs) 2)) map-array-threshold))]
(if promote
(do (var result (make-phm))
(when m (each k (keys m) (set result (phm-assoc result k (get m k)))))
(var i 0) (while (< i (length kvs)) (set result (phm-assoc result (in kvs i) (in kvs (+ i 1)))) (+= i 2))
result)
(do (var result @{}) (when m (each k (keys m) (put result k (get m k))))
(var i 0) (while (< i (length kvs)) (let [k (kvs i) v (kvs (+ i 1))] (put result k v) (+= i 2)))
# nil assocs to a fresh immutable map ((assoc nil :a 1) => {:a 1}); a
# raw table here would not count?/seq like a Clojure map (assoc-in into
# an absent key recurses through nil — migratus's migration maps).
(if (or (struct? m) (nil? m)) (table/to-struct result) result))))))
(defn core-dissoc [m & ks]
(cond
(nil? m) nil
# dissoc loses a key -> the shape changes; bridge through a struct (cold op)
(shape-rec? m) (core-dissoc (shape->struct m) ;ks)
(core-sorted-map? m) ((sorted-op m :dissoc) m ks)
(phm? m) (do (var result m) (each k ks (set result (phm-dissoc result k))) result)
# reject clearly non-map values (scalars, sequences, sets, symbol/char structs)
(or (number? m) (string? m) (buffer? m) (keyword? m) (boolean? m)
(pvec? m) (plist? m) (tuple? m) (array? m) (set? m) (core-transient? m)
(and (struct? m) (get m :jolt/type)))
(error (string "dissoc requires a map, got " (type m)))
# struct map / sorted-map / record / meta-wrapped map
(do (var result @{}) (each k (keys m) (var in-ks false) (each k2 ks (if (deep= k k2) (do (set in-ks true) (break)))) (if (not in-ks) (put result k (m k))))
(if (struct? m) (table/to-struct result) result))))
(defn core-get [m k &opt default]
(default default nil)
(if (nil? m) default
# inline the shape check (no fn call) so non-shape gets pay only a tuple? test
(if (and (tuple? m) (> (length m) 0) (struct? (in m 0)) (not (nil? (in (in m 0) :jolt/shape))))
(shape-get m k default)
(if (core-sorted? m) ((sorted-op m :get) m k 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)
(if (and (number? k) (>= k 0) (< k (pv-count m))) (pv-nth m k) default)
(if (or (struct? m) (table? m))
(let [v (m k)]
(if (nil? v) default v))
(if (and (or (tuple? m) (array? m)) (number? k) (>= k 0) (< k (length m)))
(in m k)
# Clojure's get indexes strings too (returns the char) — reitit's path
# parser relies on (get path i). nth already did; get did not, so
# (get "a:b" 1) was nil.
(if (and (or (string? m) (buffer? m)) (number? k) (>= k 0) (< k (length m)))
(make-char (in m k))
default)))))))))))
# Runtime invoke dispatch for COMPILED code (interpreter uses evaluator's
# jolt-invoke). Handles real functions plus Clojure IFn collections.
(defn jolt-call [f & args]
(cond
(or (function? f) (cfunction? f)) (apply f args)
(shape-rec? f) (core-get f (get args 0) (get args 1))
(keyword? f) (core-get (get args 0) f (get args 1))
(and (struct? f) (= :symbol (f :jolt/type))) (core-get (get args 0) f (get args 1))
(core-sorted? f) ((sorted-op f :get) f (get args 0) (get args 1))
(phm? f) (phm-get f (get args 0) (get args 1))
(set? f) (if (phs-contains? f (get args 0)) (get args 0) (get args 1))
(pvec? f)
(let [k (get args 0)]
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (pv-count f)))
(pv-nth f k)
(error (string "Index " k " out of bounds for vector of length " (pv-count f)))))
(or (tuple? f) (array? f))
(let [k (get args 0)]
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length f)))
(in f k)
(error (string "Index " k " out of bounds for vector of length " (length f)))))
# Map literal (struct with no :jolt/type marker) or a record: callable as a
# key lookup. A TAGGED struct (char/etc.) is NOT a fn — symbols are handled
# above; everything else with a :jolt/type falls through to the error.
(or (and (struct? f) (nil? (get f :jolt/type))) (and (table? f) (get f :jolt/deftype)))
(let [v (get f (get args 0) :jolt/not-found)]
(if (= v :jolt/not-found) (get args 1) v))
(error (string "Cannot call " (type f) " as a function"))))
(defn core-apply
"(apply f a b ... coll) — call f with the leading args plus the elements of
the final collection spliced in. Materializes pvec/lazy-seq/set tails."
[f & args]
(let [n (length args)]
(if (= n 0)
(jolt-call f)
(let [fixed (array/slice args 0 (- n 1))
t (in args (- n 1))
tail (cond (nil? t) [] # (apply f x nil) == (f x), as in Clojure
(set? t) (phs-seq t) (phm? t) (tuple ;(phm-entries t))
(realize-for-iteration t))]
(jolt-call f ;fixed ;tail)))))
# get-in now lives in the Clojure collection tier (core/20-coll.clj).
(defn core-contains? [coll key]
(if (shape-rec? coll) (shape-contains? coll key)
(if (core-sorted? coll) (if ((sorted-op coll :contains) coll key) true false)
(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) (phm-contains? coll key)
(if (pvec? coll) (and (number? key) (>= key 0) (< key (pv-count coll)))
(if (struct? coll) (not (nil? (coll key)))
(if (table? coll) (not (nil? (coll key)))
(if (or (tuple? coll) (array? coll))
(and (number? key) (>= key 0) (< key (length coll)))
false))))))))))
# Coerce a Clojure IFn value to a Janet-callable fn for higher-order fns
# (map/filter/sort-by/group-by/...). Janet functions pass through; a keyword or
# symbol becomes a key lookup, a map a key lookup, a set a membership test — so
# (map :k coll), (sort-by :k coll), (filter a-set coll) work.
(defn- as-fn [f]
(cond
(or (function? f) (cfunction? f)) f
(keyword? f) (fn [x &opt d] (core-get x f d))
(core-symbol? f) (fn [x &opt d] (core-get x f d))
(phm? f) (fn [k &opt d] (core-get f k d))
(set? f) (fn [x &opt d] (if (core-contains? f x) x d))
true f))
# 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.
# sorted-map/sorted-set predicates, constructors and ops live ABOVE core-conj so
# the collection fns (conj/assoc/get/contains?/…) can branch on them.
(defn core-count [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
(shape-rec? coll) (core-first (shape->struct coll))
(core-sorted? coll) ((sorted-op coll :first) coll)
(lazy-seq? coll) (ls-first coll)
(pvec? coll) (if (= 0 (pv-count coll)) nil (pv-nth coll 0))
(plist? coll) (if (pl-empty? coll) nil (pl-first coll))
# maps and sets: first of their seq (an entry / element)
(phm? coll) (let [e (phm-entries coll)] (if (= 0 (length e)) nil (in e 0)))
(set? coll) (let [s (phs-seq coll)] (if (= 0 (length s)) nil (in s 0)))
(and (struct? coll) (nil? (get coll :jolt/type)))
(let [ks (keys coll)] (if (= 0 (length ks)) nil (tuple (in ks 0) (get coll (in ks 0)))))
(nil? coll) nil
(string? coll) (if (= 0 (length coll)) nil (make-char (in coll 0)))
# scalars aren't seqable
(or (number? coll) (boolean? coll) (keyword? coll) (and (struct? coll) (get coll :jolt/type)))
(error (string "first not supported on " (type coll)))
(= 0 (length coll)) nil
(in coll 0)))
(defn- seq-done?
"True when cursor c (a lazy-seq or a concrete collection) is exhausted.
Uses cell realization for lazy-seqs so nil elements don't end the seq early."
[c]
(if (lazy-seq? c)
(let [cell (realize-ls c)]
(or (nil? cell) (= :jolt/pending cell) (= 0 (length cell))))
(or (nil? c) (= 0 (length c)))))
(defn core-rest [coll]
(cond
# rest never returns nil — Clojure's rest yields () on an exhausted seq.
(lazy-seq? coll) (let [r (ls-rest coll)] (if (nil? r) @[] r))
(plist? coll) (pl-rest coll)
# Indexed collections: an O(1) lazy view from index 1 (Clojure: rest of a
# vector is a seq, not a vector). Slicing per step made first/rest loops
# over concrete collections O(n^2) — a 20k rest-loop took two seconds.
# These stay ABOVE the set/map branches: rest-of-vector is every seq loop's
# hot path and must not pay the wrapper-tag checks.
(pvec? coll) (let [a (pv->array coll)]
(if (<= (length a) 1) @[]
(make-lazy-seq (fn [] (indexed-cells a 1)))))
(or (nil? coll) (= 0 (length coll))) @[]
(string? coll) (tuple ;(map make-char (string/bytes (string/slice coll 1))))
(tuple? coll) (if (<= (length coll) 1) @[]
(make-lazy-seq (fn [] (indexed-cells coll 1))))
# Sets, maps and sorted colls rest via their seq. Without these branches
# they fell into the indexed fall-through, which walked the wrapper table's
# INTERNAL fields — (next #{1 2}) was (nil nil) until the canonical every?
# started seq-walking sets (seed-shrink round 4).
(set? coll) (if (= 0 (coll :cnt)) @[] (core-rest (phs-seq coll)))
(phm? coll) (if (= 0 (coll :cnt)) @[] (core-rest (tuple ;(phm-entries coll))))
(core-sorted? coll) (core-rest ((sorted-op coll :seq) coll))
# plain struct maps (untagged literals) rest via entries too
(and (struct? coll) (nil? (get coll :jolt/type)))
(core-rest (tuple ;(map-entries-of coll)))
(if (<= (length coll) 1) @[]
(make-lazy-seq (fn [] (indexed-cells coll 1))))))
(defn core-next [coll]
# next is rest, but nil when the rest is empty. seq-done? realizes one lazy
# cell so a lazy rest that turns out empty (length on the table won't tell us)
# collapses to nil, matching Clojure.
(let [r (core-rest coll)]
(if (seq-done? r) nil r)))
(defn core-cons [x coll]
"Prepend x onto coll. For concrete collections this is an O(1) persistent cons
node; for lazy-seqs it stays a lazy cell so laziness is preserved."
(cond
# Lazy tail: return a LazySeq (NOT a bare cell), so a cons-of-a-cons stays a
# proper lazy-seq and the rest-thunk never leaks as a plain array element.
(lazy-seq? coll) (make-lazy-seq (fn [] @[x (fn [] coll)]))
(or (nil? coll) (plist? coll) (array? coll) (tuple? coll)) (pl-cons x coll)
# second arg must be seqable (a collection or string); reject scalars
(not (or (core-coll? coll) (string? coll)))
(error (string "Don't know how to create ISeq from: " (type coll)))
(pl-cons x (realize-for-iteration coll))))
(defn core-seq [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))
: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)))
(error (string "Don't know how to create a vector from " (type coll))))
(let [coll (realize-for-iteration coll)]
(cond
(array? coll) (make-vec coll)
(tuple? coll) (make-vec coll)
(struct? coll) (make-vec (map |(in (kvs coll) (+ (* $ 2) 1)) (range (/ (length (kvs coll)) 2))))
(string? coll) (make-vec (map |(string/from-bytes $) (string/bytes coll)))
(make-vec @[]))))
(defn- into-conj [to items]
(cond
(or (phm? to) (struct? to) (and (table? to) (get to :jolt/deftype)))
(do (var result to)
(each item items (set result (core-assoc result (vnth item 0) (vnth item 1))))
result)
(pvec? to) (do (var result to) (each x items (set result (pv-conj result x))) result)
(array? to) (if mutable?
(do (each x items (array/push to x)) to) # vector: append
(do (var result (array/slice to)) (each x items (array/insert result 0 x)) result)) # list: prepend
(tuple? to) (tuple/slice (tuple ;(array/concat (array/slice to) (array/slice items))))
# everything else conj-able (sets, sorted colls): fold conj — previously
# this fell through to `to` unchanged, silently dropping all elements
# ((into #{} [:a :b]) was #{}, jolt-h86)
(do (var result to) (each x items (set result (core-conj result x))) result)))
# merge now lives in the Clojure collection tier (core/20-coll.clj).
# merge-with now lives in the Clojure collection tier (core/20-coll.clj).
# keys / vals now live in the syntax tier (core/00-syntax.clj) — canonical
# projections of (seq m), so sorted maps come back in comparator order.
# select-keys now lives in the Clojure collection tier (core/20-coll.clj).
# zipmap now lives in the Clojure collection tier (core/20-coll.clj).
# ============================================================
# Transducers
# ============================================================
# A transducer is (fn [rf] rf') where rf' is a reducing fn with arities
# []=init, [acc]=complete, [acc x]=step. map/filter/take/... return a
# transducer when called with no collection.
(defn core-reduced [x] @{:jolt/type :jolt/reduced :val x})
(defn core-reduced? [x] (and (table? x) (= :jolt/reduced (x :jolt/type))))
# unreduced lives in the syntax tier (core/00-syntax.clj) over reduced?/deref.
(defn- ensure-reduced [x] (if (core-reduced? x) x (core-reduced x)))
(defn td-map [f]
(fn [rf] (fn [& a] (case (length a) 0 (rf) 1 (rf (a 0)) (rf (a 0) (f (a 1)))))))
(defn td-filter [pred]
(fn [rf] (fn [& a] (case (length a) 0 (rf) 1 (rf (a 0))
(if (truthy? (pred (a 1))) (rf (a 0) (a 1)) (a 0))))))
(defn td-remove [pred] (td-filter (fn [x] (not (pred x)))))
# td-keep removed: keep (incl its transducer arity) lives in core/40-lazy.clj.
(defn td-take [n]
(fn [rf]
(var left n)
(fn [& a] (case (length a) 0 (rf) 1 (rf (a 0))
(if (<= left 0) (core-reduced (a 0))
(let [r (rf (a 0) (a 1))] (set left (dec left))
(if (<= left 0) (ensure-reduced r) r)))))))
(defn td-drop [n]
(fn [rf]
(var left n)
(fn [& a] (case (length a) 0 (rf) 1 (rf (a 0))
(if (> left 0) (do (set left (dec left)) (a 0)) (rf (a 0) (a 1)))))))
(defn td-take-while [pred]
(fn [rf]
(fn [& a] (case (length a) 0 (rf) 1 (rf (a 0))
(if (truthy? (pred (a 1))) (rf (a 0) (a 1)) (core-reduced (a 0)))))))
(defn td-drop-while [pred]
(fn [rf]
(var dropping true)
(fn [& a] (case (length a) 0 (rf) 1 (rf (a 0))
(do (when (and dropping (not (truthy? (pred (a 1))))) (set dropping false))
(if dropping (a 0) (rf (a 0) (a 1))))))))
# td-map-indexed removed: map-indexed (incl transducer arity) lives in core/40-lazy.clj.
# Stateful windowing transducers. The 1-arg (completion) arity flushes a partial
# trailing window before delegating to rf's completion; matches Clojure.
# td-partition-all removed: partition-all (incl transducer arity) lives in core/40-lazy.clj.
# partition-by's transducer arity lives with its (lazy) collection arity in the
# overlay (10-seq tier), written in Clojure with volatiles.
(defn- reduce-with-reduced
"Reduce coll with reducing fn rf and seed init, honoring `reduced`. Steps lazy
seqs one cell at a time so a reducing fn that returns `reduced` (e.g. the
`take`/`take-while` transducers) can short-circuit over an INFINITE seq instead
of realizing it eagerly. Returns the final (unwrapped) accumulator."
[rf init coll]
(var acc init)
(if (lazy-seq? coll)
(do
(var cur coll) (var go true)
(while go
(let [cell (realize-ls cur)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))
(set go false)
(do
(set acc (rf acc (in cell 0)))
(if (core-reduced? acc)
(do (set acc (acc :val)) (set go false))
(let [rt (in cell 1)]
(if (nil? rt) (set go false) (set cur (ls-rest-cached cur rt))))))))))
(do
(var stop false)
(cond
# Indexed colls iterate in place — realize-for-iteration would copy a
# pvec into a fresh array (alloc + pv-nth per element) on EVERY
# reduce call, which dominates tight reduce-over-vector loops
# (jolt-4vr). Also breaks at `reduced` instead of scanning the tail.
(pvec? coll)
(do (def n (pv-count coll)) (var i 0)
(while (and (< i n) (not stop))
(set acc (rf acc (pv-nth coll i)))
(when (core-reduced? acc) (set acc (acc :val)) (set stop true))
(++ i)))
(or (tuple? coll) (array? coll))
(do (def n (length coll)) (var i 0)
(while (and (< i n) (not stop))
(set acc (rf acc (in coll i)))
(when (core-reduced? acc) (set acc (acc :val)) (set stop true))
(++ i)))
(each x (if (set? coll) (phs-seq coll) (realize-for-iteration coll))
(when (not stop)
(set acc (rf acc x))
(when (core-reduced? acc) (set acc (acc :val)) (set stop true)))))))
acc)
(defn- transduce-reduce
"Reduce coll with reducing fn rf and seed init, honoring `reduced`."
[rf init coll]
(reduce-with-reduced rf init coll))
# SEED-TWIN: transduce is overlay-public (jolt-core/clojure/core/20-coll.clj);
# this seed copy is NOT registered in core-bindings. It survives only as the
# helper core-into calls below — user `transduce` resolves to the overlay. The
# asymmetry with `into` (seed-public) is intentional; docs/seed-overlay-registry.md.
(defn core-transduce
"(transduce xform f coll) or (transduce xform f init coll)."
[xform f & rest]
(let [has-init (= 2 (length rest))
init (if has-init (in rest 0) (f))
coll (if has-init (in rest 1) (in rest 0))
rf (xform f)]
(rf (transduce-reduce rf init coll))))
(defn core-into
"(into to from) or (into to xform from)."
[to & rest]
(if (= 2 (length rest))
(let [xform (in rest 0) from (in rest 1)]
(core-transduce xform (fn [& a] (case (length a) 0 to 1 (a 0) (core-conj (a 0) (a 1)))) to from))
(into-conj to (realize-for-iteration (in rest 0)))))
(defn core-sequence
"(sequence coll) -> a seq of coll. (sequence xform coll) -> a LAZY seq of coll
transformed by xform: elements are pulled and pushed through the transducer one
at a time, with outputs buffered and emitted lazily — so it works over infinite
input (matching Clojure). Honors `reduced` (early stop) and runs the completion
arity to flush stateful transducers (e.g. partition-all)."
[a & rest]
(if (= 0 (length rest))
(core-seq a)
(let [xform a
coll (in rest 0)
buf @[]
state @{:stopped false :completed false}
rf (fn [& args]
(case (length args)
0 buf
1 (in args 0)
(do (array/push (in args 0) (in args 1)) (in args 0))))
xf (xform rf)]
# Pull/complete until buf holds an output or the source is fully drained.
(defn ensure-buf [src]
(var s src)
(while (and (= 0 (length buf)) (not (state :stopped)) (not (seq-done? s)))
(let [r (xf buf (core-first s))]
(set s (core-rest s))
(when (core-reduced? r) (put state :stopped true))))
(when (and (= 0 (length buf)) (not (state :completed))
(or (state :stopped) (seq-done? s)))
(put state :completed true)
(xf buf)) # completion arity — flushes any buffered state
s)
(defn gen [src]
(fn []
(let [s (ensure-buf src)]
(if (= 0 (length buf)) nil
(let [val (in buf 0)]
(array/remove buf 0 1)
@[val (gen s)])))))
# core-seq normalizes to a tuple / lazy-seq / nil — all walkable by
# core-first/rest/seq-done?. (Walking a raw pvec/set would misfire:
# seq-done? uses length, which counts a pvec table's KEYS, not elements.)
(make-lazy-seq (gen (core-seq coll))))))
(defn coll->cells [c]
"Convert a seqable to a lazy-seq cell chain: nil or [first, rest-thunk].
A cons cell is a MUTABLE array `@[val rest-thunk]` (produced by `cons`/the lazy
transformers); user collections (tuples, pvecs, lists) are immutable. We rely
on that distinction: only a mutable 2-array whose tail is a function is treated
as an already-built cell — a user vector like `[first last]` (tail is the fn
`last`) is data and must NOT be misread as a cell. User data is recursed through
immutable tuples so its tails never reach the cell-detection branch."
(if (nil? c) nil
(if (pvec? c) (coll->cells (tuple ;(pv->array c)))
(if (plist? c) (coll->cells (tuple ;(pl->array c)))
(if (function? c)
(let [r (c)]
(if (and (array? r) (= 2 (length r)) (function? (in r 1)))
r
(coll->cells r)))
(if (lazy-seq? c)
(let [cell (realize-ls c)]
(if (= :jolt/pending cell) nil cell))
(if (tuple? c)
# user sequential data: every element is a value, no cell-detection.
# indexed-cells walks by INDEX — the old (tuple/slice c 1) per cell
# made any walk over a concrete collection O(n^2).
(if (= 0 (length c)) nil (indexed-cells c 0))
(if (array? c)
# mutable array: a genuine cons cell, or an eager seq result.
(if (= 0 (length c)) nil
(if (and (= 2 (length c)) (function? (in c 1)))
c # already a cell [val, rest-thunk]
(indexed-cells c 0)))
# Other concrete seqables (set/map/sorted coll/string/buffer): coerce
# to a tuple seq via core-seq, then recurse. (lazy/indexed above.)
(if (or (set? c) (phm? c) (buffer? c) (string? c) (core-sorted? c)
(and (struct? c) (nil? (get c :jolt/type)))
# raw host table (System/getenv) — a map: kv entries
(and (table? c) (nil? (get c :jolt/type))
(nil? (get c :jolt/deftype))))
(coll->cells (core-seq c))
nil)))))))))
(defn lazy-from
"Coerce any seqable to a uniform lazy view without forcing.
Returns nil if coll is nil or empty, the LazySeq unchanged if already lazy,
or a new LazySeq that walks element by element."
[coll]
(if (nil? coll) nil
(if (lazy-seq? coll) coll
(do
# Reject non-seqable scalars (number/boolean/keyword, and tagged structs
# like char/symbol) so a lazy transformer over bad input throws when
# realized — matching Clojure — instead of silently yielding empty.
(when (or (number? coll) (boolean? coll) (keyword? coll)
(and (struct? coll) (not (nil? (get coll :jolt/type)))))
(error (string "Don't know how to create ISeq from: " (type coll))))
(let [cell (coll->cells coll)]
(if (nil? cell) nil
(make-lazy-seq (fn [] cell))))))))
(defn core-map [f & colls]
(def f (as-fn f))
(if (= 0 (length colls))
(td-map f) # transducer arity
(if (= 1 (length colls))
(let [coll (colls 0)]
# Option A: always lazy, even over concrete collections (matches Clojure —
# map returns a seq, not a vector).
(do
(defn mstep [c]
(fn []
(if (seq-done? c) nil
@[(f (core-first c)) (mstep (core-rest c))])))
(make-lazy-seq (mstep (lazy-from coll)))))
# Multi-collection: lazy-seq with per-element independent state
(let [init-cs (array/new-filled (length colls) nil)
init-idxs (array/new-filled (length colls) 0)
init-reals (array/new-filled (length colls) nil)
_ (do
(var i 0)
(while (< i (length colls))
(let [c (in colls i)]
(if (lazy-seq? c)
(put init-cs i c)
(do (put init-cs i nil)
(put init-reals i (if (set? c) (phs-seq c) (realize-for-iteration c))))))
(++ i))
nil)]
(defn step [cs idxs reals]
"cs: current lazy-seq cursors, idxs: indices, reals: realized colls"
(fn []
(var args @[])
(var next-cs (array/new-filled (length cs) nil))
(var next-idxs (array/new-filled (length idxs) 0))
(var next-reals (array/new-filled (length reals) nil))
(var ok true)
(var i 0)
(while (< i (length cs))
(let [cur (in cs i) ridx (in idxs i) real (in reals i)]
(if (not (nil? cur))
# Detect exhaustion with seq-done?, NOT (nil? (ls-first)): a
# lazy-seq can legitimately contain nil elements, and treating the
# first nil as end-of-seq truncates (e.g. mapping over a previous
# map result that holds nils).
(if (seq-done? cur) (do (set ok false) (break))
(do (array/push args (ls-first cur))
(put next-cs i (ls-rest cur))
(put next-idxs i (+ ridx 1))
(put next-reals i nil)))
(let [c (if (nil? real)
(let [rc (realize-for-iteration (in colls i))]
(put next-reals i rc) rc)
real)]
(if (>= ridx (length c)) (do (set ok false) (break))
(do (array/push args (in c ridx))
(put next-cs i nil)
(put next-idxs i (+ ridx 1))
(put next-reals i c))))))
(++ i))
(if (and ok (= (length args) (length cs)))
@[(apply f args) (step next-cs next-idxs next-reals)]
nil)))
(make-lazy-seq (step init-cs init-idxs init-reals))))))
(defn core-filter [pred & rest]
(def pred (as-fn pred))
(if (= 0 (length rest)) (td-filter pred)
(let [coll (in rest 0)]
# Option A: always lazy (matches Clojure — filter returns a seq).
(do
(defn fstep [c]
(fn []
(var cur c) (var hit nil) (var found false)
(while (and (not found) (not (seq-done? cur)))
(let [x (core-first cur)]
(if (pred x) (do (set hit @[x (core-rest cur)]) (set found true))
(set cur (core-rest cur)))))
(if found @[(in hit 0) (fstep (in hit 1))] nil)))
(make-lazy-seq (fstep (lazy-from coll)))))))
(defn core-remove [pred & rest]
(def pred (as-fn pred))
(if (= 0 (length rest)) (td-remove pred)
(core-filter (fn [x] (not (pred x))) (in rest 0))))
(def core-reduce
(fn [& args]
(case (length args)
# 2-arg: seed is the first element; reduce over the rest. Lazy seqs are
# stepped incrementally (via reduce-with-reduced) so `reduced` can
# short-circuit an infinite seq rather than realizing it.
2 (let [f (args 0) coll (args 1)]
(if (lazy-seq? coll)
(let [cell (realize-ls coll)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))
(f)
(let [rt (in cell 1)]
(if (nil? rt) (in cell 0)
(reduce-with-reduced f (in cell 0) (ls-rest-cached coll rt))))))
(let [c (if (set? coll) (phs-seq coll) (realize-for-iteration coll))]
(if (= 0 (length c)) (f)
(reduce-with-reduced f (in c 0) (array/slice c 1))))))
3 (let [f (args 0) val (args 1) coll (args 2)]
# 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"))))
(defn core-take [n & rest]
# n is a count — reject non-numbers (e.g. a char/string) like Clojure, rather
# than letting Janet's >= silently compare mixed types.
(unless (number? n) (error (string "take: n must be a number, got " (type n))))
(if (= 0 (length rest)) (td-take n)
(let [coll (in rest 0)]
# Option A: lazy take (returns a seq, not a vector, even over a vector).
(defn tstep [c i]
(fn []
(if (or (>= i n) (seq-done? c)) nil
@[(core-first c) (tstep (core-rest c) (+ i 1))])))
(make-lazy-seq (tstep (lazy-from coll) 0)))))
(defn core-drop [n & rest]
(if (= 0 (length rest)) (td-drop n)
(let [coll (in rest 0)]
# Option A: lazy drop — skip n (forcing only those), return the lazy tail.
(make-lazy-seq
(fn []
(var cur (lazy-from coll))
(var i 0)
(while (and (< i n) (not (seq-done? cur)))
(set cur (core-rest cur))
(++ i))
(coll->cells cur))))))
# ffirst/nfirst/fnext/nnext/last/butlast (seq tier) and second/peek/subvec/mapv/
# update (kernel tier) now live in the Clojure clojure.core tiers under
# jolt-core/clojure/core/. The kernel tier is bootstrap-compiled before the
# self-hosted analyzer is built, so the structural fns the analyzer uses come
# from Clojure, not Janet — see api/load-core-overlay! and core/00-kernel.clj.
(defn core-take-while [pred & rest]
(def pred (as-fn pred))
(if (= 0 (length rest)) (td-take-while pred)
(let [coll (in rest 0)]
# Option A: lazy take-while.
(defn twstep [c]
(fn []
(if (seq-done? c) nil
(let [x (core-first c)]
(if (pred x) @[x (twstep (core-rest c))] nil)))))
(make-lazy-seq (twstep (lazy-from coll))))))
(defn core-drop-while [pred & rest]
(def pred (as-fn pred))
(if (= 0 (length rest)) (td-drop-while pred)
(let [coll (in rest 0)]
(if (lazy-seq? coll)
(do
(defn dwstep [c]
(fn []
(var cur c)
(while (and (not (seq-done? cur)) (pred (ls-first cur)))
(set cur (ls-rest cur)))
(if (seq-done? cur) nil (realize-ls cur))))
(make-lazy-seq (dwstep coll)))
# A string iterates as a seq of chars in Clojure; realize-for-iteration
# passes strings through, so char-seq it here (as take-while/remove do) —
# otherwise pred sees raw bytes and array/slice rejects the string.
(let [c0 (realize-for-iteration coll)
c (if (string? c0) (map make-char (string/bytes c0)) c0)]
(var start 0)
(while (and (< start (length c)) (pred (c start)))
(++ start))
(if (tuple? c)
(tuple/slice c start)
(array/slice c start)))))))
(defn core-concat [& colls]
"Truly lazy concatenation. `step` returns a 0-arg thunk that is only forced
when the consumer asks for the next cell, so nothing in `colls` is realized at
construction time. This is essential for self-referential lazy seqs (e.g.
(def fib (lazy-cat [0 1] (map + (rest fib) fib)))): the later colls must not be
forced until after the surrounding `def` has bound the var."
(if (= 0 (length colls)) @[]
(let [colls (if (tuple? colls) (array/slice colls) colls)]
(defn step [cs]
(fn []
(if (= 0 (length cs))
nil
(let [c (in cs 0)
remaining (array/slice cs 1)
cell (coll->cells c)]
(if (nil? cell)
# current coll is empty: advance to the next one
((step remaining))
(let [val (in cell 0)
rest-fn (in cell 1)]
@[val (step (if (nil? rest-fn)
remaining
(array/insert remaining 0 rest-fn)))]))))))
(make-lazy-seq (step colls)))))
(defn core-mapcat
"(mapcat f & colls) — map then concat. (mapcat f) returns a transducer."
[f & colls]
(if (= 0 (length colls))
# transducer: map f over each input, then splice (cat) the result
(fn [rf]
(fn [& a]
(case (length a)
0 (rf)
1 (rf (a 0))
(do (var acc (a 0))
(each x (realize-for-iteration (f (a 1)))
(set acc (rf acc x)))
acc))))
# collection arity: direct lazy implementation. Pull one element
# from each input coll, apply f, then yield elements from f's result.
# No apply-forcing — walk input colls lazily element-by-element.
(do
(var n (length colls))
(var init-cs @[])
(var i 0)
(while (< i n)
(array/push init-cs (lazy-from (in colls i)))
(++ i))
(defn step [cs res]
(fn []
(var cursors cs) (var cur-res res) (var hit nil) (var ok false)
(while (not ok)
(if (nil? cur-res)
(do
(var args @[]) (var next-cs @[]) (var exhausted false) (var j 0)
(while (and (< j n) (not exhausted))
(let [c (in cursors j)]
(if (seq-done? c) (set exhausted true)
(do
(array/push args (ls-first c))
(array/push next-cs (ls-rest c)))))
(++ j))
(if exhausted (break))
(let [r (apply f args)]
(set cursors next-cs)
(set cur-res (if (or (nil? r) (tuple? r) (array? r)
(lazy-seq? r) (pvec? r) (set? r) (plist? r))
(lazy-from r)
(lazy-from (tuple r))))))
(if (seq-done? cur-res)
(set cur-res nil)
(let [val (ls-first cur-res) rest (ls-rest cur-res)]
(set hit @[val (step cursors rest)])
(set ok true)))))
(if ok hit nil)))
(make-lazy-seq (step init-cs nil)))))
# reverse now lives in the Clojure collection tier ((reduce conj () coll)).
(defn core-nth
"Return the nth element of a sequential collection. With a not-found arg, return
it when idx is out of bounds (even if it's nil); without one, throw — matching
Clojure, where (nth coll i nil) returns nil rather than throwing."
[coll idx & rest]
(def has-default (> (length rest) 0))
(def default (if has-default (in rest 0) nil))
(defn oob [n] (if has-default default (error (string "Index " idx " out of bounds, length: " n))))
(if (nil? coll) default # (nth nil i) -> nil / default, never throws
(if (core-transient? coll)
(let [a (coll :arr)] (if (and (>= idx 0) (< idx (length a))) (in a idx) (oob (length a))))
(if (plist? coll)
(let [a (pl->array coll)]
(if (and (>= idx 0) (< idx (length a))) (in a idx) (oob (length a))))
(if (pvec? coll)
(if (and (>= idx 0) (< idx (pv-count coll)))
(pv-nth coll idx)
(oob (pv-count coll)))
(if (lazy-seq? coll)
# Walk with seq-done?, NOT (ls-first cur): a lazy element may legitimately be
# false or nil, which truthiness would mistake for end-of-seq.
(if (< idx 0) (oob 0)
(do
(var cur coll)
(var i 0)
(while (and (< i idx) (not (seq-done? cur)))
(set cur (core-rest cur))
(++ i))
(if (seq-done? cur) (oob i) (core-first cur))))
(do
(var c (realize-for-iteration coll))
(if (and (>= idx 0) (< idx (length c)))
(if (string? c) (make-char (in c idx)) (in c idx))
(oob (length c))))))))))
(defn core-sort
"(sort coll) or (sort comparator coll). Comparator may return a boolean or a
Clojure-style negative/zero/positive number."
[a & rest]
(let [has-cmp (> (length rest) 0)
cmp (if has-cmp a nil)
coll (if has-cmp (first rest) a)]
(if (nil? coll) @[]
(let [arr (array/slice (realize-for-iteration coll))]
(if has-cmp
(sort arr (fn [x y] (let [r (cmp x y)] (if (number? r) (< r 0) (truthy? r)))))
(sort arr))
(tuple/slice (tuple ;arr))))))
# (sort-by keyfn coll) or (sort-by keyfn comparator coll). The comparator (when
# given) compares the KEYS and may return a boolean or a Clojure-style number.
# sort-by now lives in the Clojure collection tier — canonical: compare-
# defaulted (nil sorts first), comparator over KEYS, via the host sort seam.
# distinct now lives in the Clojure lazy tier (core/40-lazy.clj).
# group-by / frequencies now live in the Clojure collection tier
# (core/20-coll.clj).
(defn core-partition
"(partition n coll), (partition n step coll), or (partition n step pad coll).
Only complete partitions of size n are kept; with pad, the final partial
partition is padded from pad (possibly to fewer than n if pad runs out)."
[n & rest]
(let [argc (length rest)
step (if (>= argc 2) (first rest) n)
pad (if (>= argc 3) (in rest 1) nil)
has-pad (>= argc 3)
coll (case argc 1 (first rest) 2 (in rest 1) 3 (in rest 2))]
# Option A: always lazy.
(defn pstep [c]
(fn []
(if (seq-done? c) nil
(do
(var part @[]) (var cur c) (var i 0)
(while (and (< i n) (not (seq-done? cur)))
(array/push part (core-first cur))
(set cur (core-rest cur))
(++ i))
(cond
(= i n)
(let [next-cur (if (= step n) cur (lazy-from (core-drop (- step n) cur)))]
@[(tuple/slice (tuple ;part)) (pstep next-cur)])
# partial final partition: pad it (last partition, then stop)
(and has-pad (> i 0))
(do
(each x (realize-for-iteration pad)
(when (< (length part) n) (array/push part x)))
@[(tuple/slice (tuple ;part)) (fn [] nil)])
nil)))))
(make-lazy-seq (pstep (lazy-from coll)))))
# partition-by now lives in the Clojure seq tier (core/10-seq.clj).
# partition-all now lives in the Clojure lazy tier (core/40-lazy.clj).
# keep-indexed / map-indexed / cycle now live in the Clojure lazy tier
# (core/40-lazy.clj).
# reduce-kv now lives in the Clojure collection tier (core/20-coll.clj).
# pop is defined only on stacks (vectors -> last end, lists -> front); Clojure
# throws on sets/maps/seqs/strings/scalars. (peek lives in the Clojure kernel
# tier — core/00-kernel.clj.)
# subvec lives in the Clojure kernel tier — core/00-kernel.clj.
# trampoline now lives in the Clojure collection tier (core/20-coll.clj).
(def core-format (fn [fmt & args] (string/format fmt ;args)))
# ============================================================
# Sequence generators
# ============================================================
(def core-range
(fn [& args]
(if (= 0 (length args))
# (range) — infinite lazy sequence 0, 1, 2, ...
(do
(defn rstep [i] (fn [] @[i (rstep (+ i 1))]))
(make-lazy-seq (rstep 0)))
(let [start (if (> (length args) 1) (args 0) 0)
end (if (> (length args) 1) (args 1) (args 0))
step (if (> (length args) 2) (args 2) 1)]
(var result @[])
(var i start)
(while (if (pos? step) (< i end) (> i end))
(array/push result i)
(+= i step))
(tuple/slice (tuple ;result))))))
# repeat / iterate now live in the Clojure lazy tier (core/40-lazy.clj).
# repeatedly now lives in the Clojure lazy tier (core/40-lazy.clj).
# ============================================================
# Higher-order functions
# ============================================================
# identity / constantly live in the Clojure collection tier (core/20-coll.clj).
# complement now lives in the Clojure collection tier (core/20-coll.clj).
# inst?/inst-ms live in the Clojure collection tier (core/20-coll.clj).
# Jolt has no uri host type, so uri? is always false.
# uri? lives in the Clojure collection tier (no uri host type: always false).
# uuid? now lives in the Clojure collection tier (tagged-value predicate).
(defn core-bytes? [x] (buffer? x))
# tagged-literal? now lives in the Clojure collection tier (tagged-value predicate).
(defn core-meta [x]
"Returns the metadata of x, or nil."
(cond
(var? x) (var-meta x)
# symbols carry reader metadata (type hints etc.) in a :meta field
(and (struct? x) (= :symbol (get x :jolt/type))) (get x :meta)
(table? x) (or (get x :jolt/meta) (get x :meta))
nil))
# every-pred now lives in the Clojure collection tier (core/20-coll.clj).
# Public comp lives in the overlay now (20-coll) — its stages can be any jolt
# IFn (keyword/map/set/vector), which raw Janet calls mishandle ((comp seq
# :content) returned nil: janet keyword-apply is not jolt invoke). This
# private composer remains ONLY for the transducer machinery below, where the
# stages are always real fns.
# (td-comp is gone: eduction — its last caller — lives in the overlay now.)
# partial now lives in the Clojure collection tier (canonical arities).
# juxt now lives in the Clojure collection tier (core/20-coll.clj).
# memoize now lives in the Clojure collection tier — find-based, so it
# caches nil results too (this kernel fn re-computed them).
# ============================================================
# Collection constructors
# ============================================================
(defn core-vector [& xs] (make-vec xs))
(defn core-hash-map [& kvs] (make-phm kvs))
(defn core-array-map [& kvs]
(var result @{})
(var i 0)
(while (< i (length kvs))
(put result (kvs i) (kvs (+ i 1)))
(+= i 2))
(table/to-struct result))
(defn core-hash-set [& xs]
(apply make-phs xs))
# sorted sets are tagged tables the host set? predicate misses (jolt-dpn)
(defn core-set? [x] (or (set? x) (core-sorted-set? x)))
(defn core-disj [s & ks]
(cond
(core-sorted-set? s) ((sorted-op s :disj) s ks)
(set? s) (apply phs-disj s ks)
(error "disj expects a set")))
(defn core-set [coll]
(apply core-hash-set (realize-for-iteration coll)))
(defn core-list [& xs]
(array ;xs))
# ============================================================