diff --git a/src/jolt/core.janet b/src/jolt/core.janet index ce85ae6..953adbc 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -3,6 +3,7 @@ (use ./types) (use ./phm) +(use ./lazyseq) (use ./regex) (use ./config) (use ./pv) diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index 57c96ff..fa65bee 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -3,6 +3,7 @@ (use ./types) (use ./phm) +(use ./lazyseq) (use ./pv) (use ./plist) (use ./config) diff --git a/src/jolt/host_interop.janet b/src/jolt/host_interop.janet index 064c472..9038837 100644 --- a/src/jolt/host_interop.janet +++ b/src/jolt/host_interop.janet @@ -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 diff --git a/src/jolt/lazyseq.janet b/src/jolt/lazyseq.janet new file mode 100644 index 0000000..6ad5ead --- /dev/null +++ b/src/jolt/lazyseq.janet @@ -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]))) diff --git a/src/jolt/main.janet b/src/jolt/main.janet index 48224fc..750ba14 100644 --- a/src/jolt/main.janet +++ b/src/jolt/main.janet @@ -4,6 +4,7 @@ (use ./api) (use ./types) (use ./phm) +(use ./lazyseq) (use ./pv) (use ./plist) (use ./config) diff --git a/src/jolt/phm.janet b/src/jolt/phm.janet index 4d581ba..cbd4c3f 100644 --- a/src/jolt/phm.janet +++ b/src/jolt/phm.janet @@ -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 # ============================================================