Refactor: extract LazySeq from phm.janet into lazyseq.janet (#108)

phm.janet held the PersistentHashMap, the PersistentHashSet, AND the LazySeq
primitives — a lazy sequence has nothing to do with hash maps; both were just
tagged tables, which is why they shared a file (jolt-bvek). An agent looking for
lazy-seq realization would never grep phm.janet.

Move the LazySeq section (lazy-seq?/make-lazy-seq/realize-ls/ls-first/ls-rest/
ls-rest-cached/ls-seq/ls-count/lazy-cons) to a new self-contained lazyseq.janet
(janet builtins only, no jolt deps). Importers that used the fns through
(use ./phm) add (use ./lazyseq); host_interop's one phm/lazy-seq? becomes
lazy-seq?. Behaviour unchanged (covered by test/unit/lazy-seq-test.janet + the
full gate). phs split is a follow-up.

Co-authored-by: Yogthos <yogthos@gmail.com>
This commit is contained in:
Dmitri Sotnikov 2026-06-15 04:41:39 +00:00 committed by GitHub
parent 62fe11fa0d
commit 7cffe85298
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 87 additions and 85 deletions

View file

@ -3,6 +3,7 @@
(use ./types)
(use ./phm)
(use ./lazyseq)
(use ./regex)
(use ./config)
(use ./pv)

View file

@ -3,6 +3,7 @@
(use ./types)
(use ./phm)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)

View file

@ -15,6 +15,7 @@
(use ./pv)
(use ./plist)
(use ./types)
(use ./lazyseq)
(import ./phm)
(defn- chr [s] (get s 0))
@ -791,7 +792,7 @@
# clojure.core equivalent. :jolt/ci-none means "not a collection method here".
(set-coll-interop!
(fn [target name args]
(if-not (or (pvec? target) (phm/phm? target) (plist? target) (phm/lazy-seq? target)
(if-not (or (pvec? target) (phm/phm? target) (plist? target) (lazy-seq? target)
(and (table? target) (= :jolt/set (get target :jolt/type)))
(shape-rec? target) # map-as-tuple record
(and (struct? target) (nil? (get target :jolt/type)))) # plain map literal

82
src/jolt/lazyseq.janet Normal file
View file

@ -0,0 +1,82 @@
# LazySeq — cell-by-cell lazy sequence (Clojure-compatible)
#
# Model: a thunk returns nil (empty) or a [first-val, rest-thunk] pair; each
# step produces one element + a thunk for the rest. Supports self-referencing
# sequences like fib-seq. Self-contained (janet builtins only) — the Clojure
# seq layer (core.janet) and the interpreter build on these primitives.
#
# Extracted from phm.janet (jolt-bvek): a lazy sequence has nothing to do with
# hash maps; both were tagged tables, which is why they shared a file.
(defn lazy-seq?
"Check if x is a LazySeq."
[x]
(and (table? x) (= :jolt/lazy-seq (x :jolt/type))))
(defn make-lazy-seq [thunk]
@{:jolt/type :jolt/lazy-seq :fn thunk :realized false :val nil})
(defn realize-ls
"Force a LazySeq cell. Returns nil (empty) or [first-val, rest-thunk].
If the thunk returns another lazy-seq, recursively realize it.
Uses :jolt/pending sentinel to detect self-referencing cycles."
[ls]
(if (get ls :realized)
(ls :val)
(do
(put ls :val :jolt/pending)
(put ls :realized true)
(let [raw ((ls :fn))
v (if (lazy-seq? raw) (realize-ls raw) raw)]
(put ls :val v)
v))))
(defn ls-first [ls]
(let [cell (realize-ls ls)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell))) nil (in cell 0))))
# The memoized rest wrapper for a node whose cell yielded rest-thunk rt.
# EVERY walk must go through this (not a fresh make-lazy-seq) or independent
# walks re-run the shared thunks and side effects duplicate.
(defn ls-rest-cached [ls rt]
(or (get ls :rest-ls)
(let [w (make-lazy-seq rt)]
(put ls :rest-ls w)
w)))
(defn ls-rest [ls]
(let [cell (realize-ls ls)]
(if (or (nil? cell) (= 0 (length cell))) nil
(let [rt (in cell 1)]
(if (nil? rt) nil
# Memoized wrapper (see ls-rest-cached): a fresh table per call gave
# every independent walk its own realization state, so the shared
# rest-thunks re-ran — duplicating side effects (a doall'd seq of
# futures re-spawned them on the deref walk, serializing pmap).
(ls-rest-cached ls rt))))))
(defn ls-seq [ls]
(var result @[])
(var cur ls)
(while (not (nil? cur))
(let [cell (realize-ls cur)]
(if (nil? cell) (break))
(array/push result (in cell 0))
(set cur (ls-rest cur))))
(if (= 0 (length result)) nil result))
(defn ls-count [ls]
(var cnt 0)
(var cur ls)
(while (not (nil? cur))
(let [cell (realize-ls cur)]
(if (nil? cell) (break))
(++ cnt)
(set cur (ls-rest cur))))
cnt)
(defn lazy-cons
"Returns a LazySeq whose first element is x and whose rest is produced by
rest-thunk (a 0-arg function returning nil or a LazySeq)."
[x rest-thunk]
(make-lazy-seq (fn [] @[x rest-thunk])))

View file

@ -4,6 +4,7 @@
(use ./api)
(use ./types)
(use ./phm)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)

View file

@ -174,90 +174,6 @@
(while (< i n) (set m (phm-assoc m (kvs i) (kvs (+ i 1)))) (+= i 2)))
m)
# ============================================================
# LazySeq — cell-by-cell lazy sequence (Clojure-compatible)
# ============================================================
# Model: thunk returns nil (empty) or [first-val, rest-thunk] pair.
# Each step produces one element + thunk for the rest.
# Supports self-referencing sequences like fib-seq.
(defn lazy-seq?
"Check if x is a LazySeq."
[x]
(and (table? x) (= :jolt/lazy-seq (x :jolt/type))))
(defn make-lazy-seq [thunk]
@{:jolt/type :jolt/lazy-seq :fn thunk :realized false :val nil})
(defn realize-ls
"Force a LazySeq cell. Returns nil (empty) or [first-val, rest-thunk].
If the thunk returns another lazy-seq, recursively realize it.
Uses :jolt/pending sentinel to detect self-referencing cycles."
[ls]
(if (get ls :realized)
(ls :val)
(do
(put ls :val :jolt/pending)
(put ls :realized true)
(let [raw ((ls :fn))
v (if (lazy-seq? raw) (realize-ls raw) raw)]
(put ls :val v)
v))))
(defn ls-first [ls]
(let [cell (realize-ls ls)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell))) nil (in cell 0))))
# The memoized rest wrapper for a node whose cell yielded rest-thunk rt.
# EVERY walk must go through this (not a fresh make-lazy-seq) or independent
# walks re-run the shared thunks and side effects duplicate.
(defn ls-rest-cached [ls rt]
(or (get ls :rest-ls)
(let [w (make-lazy-seq rt)]
(put ls :rest-ls w)
w)))
(defn ls-rest [ls]
(let [cell (realize-ls ls)]
(if (or (nil? cell) (= 0 (length cell))) nil
(let [rt (in cell 1)]
(if (nil? rt) nil
# Memoized wrapper (see ls-rest-cached): a fresh table per call gave
# every independent walk its own realization state, so the shared
# rest-thunks re-ran — duplicating side effects (a doall'd seq of
# futures re-spawned them on the deref walk, serializing pmap).
(ls-rest-cached ls rt))))))
(defn ls-seq [ls]
(var result @[])
(var cur ls)
(while (not (nil? cur))
(let [cell (realize-ls cur)]
(if (nil? cell) (break))
(array/push result (in cell 0))
(set cur (ls-rest cur))))
(if (= 0 (length result)) nil result))
(defn ls-count [ls]
(var cnt 0)
(var cur ls)
(while (not (nil? cur))
(let [cell (realize-ls cur)]
(if (nil? cell) (break))
(++ cnt)
(set cur (ls-rest cur))))
cnt)
# ============================================================
# Lazy combinator — primitive for building lazy sequences
# ============================================================
(defn lazy-cons
"Returns a LazySeq whose first element is x and whose rest is produced
by rest-thunk (a 0-arg function returning nil or a LazySeq)."
[x rest-thunk]
(make-lazy-seq (fn [] @[x rest-thunk])))
# ============================================================
# PersistentHashSet — backed by PersistentHashMap
# ============================================================