perf: chunk the lazy seq producers
A lazy seq cost a cons cell, a memoizing thunk and a closure per element, which
put `(count (range 1000000))` at 299 ms where the eager version had been 23 ms.
Chunking amortizes that over a batch: range, map, filter/remove and take now
realize up to 32 elements per step and hand back one kChunk instead of 32
cons cells.
A new kChunk value kind holds the realized run, an offset into it, and the rest
of the seq. Cursor indexes a chunk where it lies rather than copying its
elements out, and still follows cons/lazy links one at a time, so chunked and
unchunked seqs compose freely -- `(concat [1 2] (range))`, `(cons 0 (range 10))`
and `(map f (range) (cycle [10 20]))` all work unchanged.
The visible consequence is that demand rounds up to a batch: taking 3 elements
of a mapped seq runs the fn 32 times. That is what Clojure does, and every case
in examples/chunking.clj was checked against a real Clojure run -- the 32/64
call counts, the interop cases and the infinite-source cases all agree. It does
mean map's fn must not be relied on for per-element side effects; doseq is the
tool for that. examples/lazy.clj says so where it counts calls.
filter draws up to 32 source elements per step and emits whichever pass, rather
than pulling until 32 have passed, so a rare match in a long or infinite source
still costs a bounded amount per step: `(first (filter #(= % 5000) (range)))`
does not realize 32 matches.
reduce also stops materializing its source through toSeq and streams it
instead, which is what lets a fold over a lazy seq hold a chunk at a time.
Compute excluding startup, best-of-N round-robin:
before after
seqs 853 ms 342 ms
lazy-bench 0 ms 0 ms (early exit unaffected)
persistent-bench 120 ms 120 ms
Two things measured and rejected rather than kept: copying a chunk's elements
into the cursor was worth ~30 ms of the total, and skipping the destructor's
drain worklist for kinds that never queue anything was worth nothing at all --
not enough to justify touching the mechanism that keeps a long chain from
overflowing the stack at teardown.
What remains between this and the eager numbers is the boxed Value: every
mkInt still allocates, which a separate measurement puts at 16 ms per million.
Folding a long seq also still retains its head through the argument that holds
it, so `(reduce + (map inc (filter even? (range 5000000))))` peaks at 713 MB.
Both are their own piece of work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>75fc33e parent: 017649f added
examples/chunking.clj +24 -0 | new file mode 100644 | ||
| @@ -0,0 +1,24 @@ | ||
| 1 | +;; chunking realizes in batches of 32: demand is rounded up, never exceeded | |
| 2 | +;; beyond a batch, and nothing is recomputed | |
| 3 | +(def calls (atom 0)) | |
| 4 | +(def xs (map (fn [x] (reset! calls (inc (deref calls))) x) (range 1000))) | |
| 5 | +(println "built:" (deref calls)) ; 0 | |
| 6 | +(println (doall (take 3 xs)) "calls:" (deref calls)) ; (0 1 2) 32 | |
| 7 | +(println (doall (take 3 xs)) "calls:" (deref calls)) ; memoized, still 32 | |
| 8 | +(println (doall (take 40 xs)) "calls:" (deref calls)) ; 64 | |
| 9 | + | |
| 10 | +;; infinite sources stay usable | |
| 11 | +(println (take 5 (filter even? (range)))) | |
| 12 | +(println (first (filter (fn [x] (> x 100)) (range)))) | |
| 13 | +(println (take 3 (map inc (iterate inc 0)))) | |
| 14 | +(println (nth (range) 1000) (take 4 (drop 10 (range)))) | |
| 15 | +(println (count (take 100 (cycle [1 2 3])))) | |
| 16 | + | |
| 17 | +;; rare matches over a long source must not pull a whole chunk of matches | |
| 18 | +(println (first (filter (fn [x] (= x 5000)) (range)))) | |
| 19 | + | |
| 20 | +;; chunked and unchunked seqs interoperate | |
| 21 | +(println (take 4 (concat [1 2] (range)))) | |
| 22 | +(println (count (cons 0 (range 10)))) | |
| 23 | +(println (reduce + (cons 100 (map inc (range 5))))) | |
| 24 | +(println (take 3 (map (fn [a b] (+ a b)) (range) (cycle [10 20])))) | |
| new file mode 100644 | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | +;; chunking realizes in batches of 32: demand is rounded up, never exceeded | ||
| 2 | +;; beyond a batch, and nothing is recomputed | ||
| 3 | +(def calls (atom 0)) | ||
| 4 | +(def xs (map (fn [x] (reset! calls (inc (deref calls))) x) (range 1000))) | ||
| 5 | +(println "built:" (deref calls)) ; 0 | ||
| 6 | +(println (doall (take 3 xs)) "calls:" (deref calls)) ; (0 1 2) 32 | ||
| 7 | +(println (doall (take 3 xs)) "calls:" (deref calls)) ; memoized, still 32 | ||
| 8 | +(println (doall (take 40 xs)) "calls:" (deref calls)) ; 64 | ||
| 9 | + | ||
| 10 | +;; infinite sources stay usable | ||
| 11 | +(println (take 5 (filter even? (range)))) | ||
| 12 | +(println (first (filter (fn [x] (> x 100)) (range)))) | ||
| 13 | +(println (take 3 (map inc (iterate inc 0)))) | ||
| 14 | +(println (nth (range) 1000) (take 4 (drop 10 (range)))) | ||
| 15 | +(println (count (take 100 (cycle [1 2 3])))) | ||
| 16 | + | ||
| 17 | +;; rare matches over a long source must not pull a whole chunk of matches | ||
| 18 | +(println (first (filter (fn [x] (= x 5000)) (range)))) | ||
| 19 | + | ||
| 20 | +;; chunked and unchunked seqs interoperate | ||
| 21 | +(println (take 4 (concat [1 2] (range)))) | ||
| 22 | +(println (count (cons 0 (range 10)))) | ||
| 23 | +(println (reduce + (cons 100 (map inc (range 5))))) | ||
| 24 | +(println (take 3 (map (fn [a b] (+ a b)) (range) (cycle [10 20])))) | ||
modified
examples/lazy.clj +4 -1 | @@ -13,7 +13,10 @@ | ||
| 13 | 13 | (println (take 5 (concat [1 2] (range)))) |
| 14 | 14 | (println (first (map inc (range))) (second (range)) (nth (range) 1000)) |
| 15 | 15 | |
| 16 | -;; nothing beyond the demand is computed, and each cell is computed once | |
| 16 | +;; nothing beyond the demand is computed, and each cell is computed once. | |
| 17 | +;; Seqs are chunked, so demand is rounded up to a batch of 32: taking 3 runs | |
| 18 | +;; the mapped fn 32 times, exactly as it does in Clojure. Construction still | |
| 19 | +;; runs it zero times, and a second pass recomputes nothing. | |
| 17 | 20 | (def calls (atom 0)) |
| 18 | 21 | (def xs (map (fn [x] (reset! calls (inc (deref calls))) x) (range 1000))) |
| 19 | 22 | (println "built, calls so far:" (deref calls)) |
| @@ -13,7 +13,10 @@ | |||
| 13 | (println (take 5 (concat [1 2] (range)))) | 13 | (println (take 5 (concat [1 2] (range)))) |
| 14 | (println (first (map inc (range))) (second (range)) (nth (range) 1000)) | 14 | (println (first (map inc (range))) (second (range)) (nth (range) 1000)) |
| 15 | 15 | ||
| 16 | -;; nothing beyond the demand is computed, and each cell is computed once | 16 | +;; nothing beyond the demand is computed, and each cell is computed once. |
| 17 | +;; Seqs are chunked, so demand is rounded up to a batch of 32: taking 3 runs | ||
| 18 | +;; the mapped fn 32 times, exactly as it does in Clojure. Construction still | ||
| 19 | +;; runs it zero times, and a second pass recomputes nothing. | ||
| 17 | (def calls (atom 0)) | 20 | (def calls (atom 0)) |
| 18 | (def xs (map (fn [x] (reset! calls (inc (deref calls))) x) (range 1000))) | 21 | (def xs (map (fn [x] (reset! calls (inc (deref calls))) x) (range 1000))) |
| 19 | (println "built, calls so far:" (deref calls)) | 22 | (println "built, calls so far:" (deref calls)) |
modified
src/compiler.nim +3 -3 | @@ -186,7 +186,7 @@ proc quoteLit(v: Value): string = | ||
| 186 | 186 | var parts: seq[string] = @[] |
| 187 | 187 | for (k, val) in v.pairs: parts.add "(" & quoteLit(k) & ", " & quoteLit(val) & ")" |
| 188 | 188 | "mkMap(@[" & parts.join(", ") & "])" |
| 189 | - of kCons, kLazy: err("Can't quote a lazy seq") | |
| 189 | + of kCons, kChunk, kLazy: err("Can't quote a lazy seq") | |
| 190 | 190 | of kFn: err("Can't quote a function") |
| 191 | 191 | |
| 192 | 192 | proc emptySeqFix(s: string, elemType: string): string = |
| @@ -775,7 +775,7 @@ proc tryExpr(f: Value, env: Env, c: Ctx): string = | ||
| 775 | 775 | if hv.len == 0: return "" |
| 776 | 776 | "call(" & hv & ", " & |
| 777 | 777 | (if ids.len == 0: "emptyArgs" else: "@[" & ids.join(", ") & "]") & ")" |
| 778 | - of kFn, kCons, kLazy: | |
| 778 | + of kFn, kCons, kChunk, kLazy: | |
| 779 | 779 | "" |
| 780 | 780 | |
| 781 | 781 | proc genInto(f: Value, dst: string, env: Env, c: Ctx) = |
| @@ -811,7 +811,7 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | ||
| 811 | 811 | (if parts.len == 0: "newSeq[(Value, Value)]()" else: "@[" & parts.join(", ") & "]") & ")") |
| 812 | 812 | of kFn: |
| 813 | 813 | err("Can't emit a function literal") |
| 814 | - of kCons, kLazy: | |
| 814 | + of kCons, kChunk, kLazy: | |
| 815 | 815 | err("Can't emit a lazy seq literal") |
| 816 | 816 | of kList: |
| 817 | 817 | if f.items.len == 0: |
| @@ -186,7 +186,7 @@ proc quoteLit(v: Value): string = | |||
| 186 | var parts: seq[string] = @[] | 186 | var parts: seq[string] = @[] |
| 187 | for (k, val) in v.pairs: parts.add "(" & quoteLit(k) & ", " & quoteLit(val) & ")" | 187 | for (k, val) in v.pairs: parts.add "(" & quoteLit(k) & ", " & quoteLit(val) & ")" |
| 188 | "mkMap(@[" & parts.join(", ") & "])" | 188 | "mkMap(@[" & parts.join(", ") & "])" |
| 189 | - of kCons, kLazy: err("Can't quote a lazy seq") | 189 | + of kCons, kChunk, kLazy: err("Can't quote a lazy seq") |
| 190 | of kFn: err("Can't quote a function") | 190 | of kFn: err("Can't quote a function") |
| 191 | 191 | ||
| 192 | proc emptySeqFix(s: string, elemType: string): string = | 192 | proc emptySeqFix(s: string, elemType: string): string = |
| @@ -775,7 +775,7 @@ proc tryExpr(f: Value, env: Env, c: Ctx): string = | |||
| 775 | if hv.len == 0: return "" | 775 | if hv.len == 0: return "" |
| 776 | "call(" & hv & ", " & | 776 | "call(" & hv & ", " & |
| 777 | (if ids.len == 0: "emptyArgs" else: "@[" & ids.join(", ") & "]") & ")" | 777 | (if ids.len == 0: "emptyArgs" else: "@[" & ids.join(", ") & "]") & ")" |
| 778 | - of kFn, kCons, kLazy: | 778 | + of kFn, kCons, kChunk, kLazy: |
| 779 | "" | 779 | "" |
| 780 | 780 | ||
| 781 | proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | 781 | proc genInto(f: Value, dst: string, env: Env, c: Ctx) = |
| @@ -811,7 +811,7 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | |||
| 811 | (if parts.len == 0: "newSeq[(Value, Value)]()" else: "@[" & parts.join(", ") & "]") & ")") | 811 | (if parts.len == 0: "newSeq[(Value, Value)]()" else: "@[" & parts.join(", ") & "]") & ")") |
| 812 | of kFn: | 812 | of kFn: |
| 813 | err("Can't emit a function literal") | 813 | err("Can't emit a function literal") |
| 814 | - of kCons, kLazy: | 814 | + of kCons, kChunk, kLazy: |
| 815 | err("Can't emit a lazy seq literal") | 815 | err("Can't emit a lazy seq literal") |
| 816 | of kList: | 816 | of kList: |
| 817 | if f.items.len == 0: | 817 | if f.items.len == 0: |
modified
src/core.nim +44 -17 | @@ -198,12 +198,17 @@ proc lazyOf(c: Cursor): Value = | ||
| 198 | 198 | mkCons(x, lazyOf(cc))) |
| 199 | 199 | |
| 200 | 200 | proc lazyMap(f: Value, c: Cursor): Value = |
| 201 | + ## Chunked, which means f runs for up to ChunkSize elements as soon as the | |
| 202 | + ## first is demanded -- the same trade Clojure makes. f must not be relied on | |
| 203 | + ## for per-element side effects; `doseq` and `run!` are the tools for that. | |
| 201 | 204 | let cur = c |
| 202 | 205 | mkLazy(proc (): Value = |
| 203 | 206 | var cc = cur |
| 204 | - if not hasNext(cc): return NilV | |
| 205 | - let x = next(cc) | |
| 206 | - mkCons(call(f, @[x]), lazyMap(f, cc))) | |
| 207 | + var xs = newSeqOfCap[Value](ChunkSize) | |
| 208 | + while xs.len < ChunkSize and hasNext(cc): | |
| 209 | + xs.add call(f, @[next(cc)]) | |
| 210 | + if xs.len == 0: return NilV | |
| 211 | + mkChunk(xs, 0, lazyMap(f, cc))) | |
| 207 | 212 | |
| 208 | 213 | proc lazyMapN(f: Value, cs: seq[Cursor]): Value = |
| 209 | 214 | let curs = cs |
| @@ -224,23 +229,32 @@ proc lazyMapIndexed(f: Value, i: int64, c: Cursor): Value = | ||
| 224 | 229 | mkCons(call(f, @[mkInt(i), x]), lazyMapIndexed(f, i + 1, cc))) |
| 225 | 230 | |
| 226 | 231 | proc lazyFilter(pred: Value, c: Cursor, keep: bool): Value = |
| 232 | + ## Draws up to ChunkSize source elements per step and emits whichever pass, | |
| 233 | + ## rather than pulling until ChunkSize have passed: that keeps the work per | |
| 234 | + ## step bounded when matches are rare in a long or infinite source. | |
| 227 | 235 | let cur = c |
| 228 | 236 | mkLazy(proc (): Value = |
| 229 | 237 | var cc = cur |
| 230 | - while hasNext(cc): | |
| 231 | - let x = next(cc) | |
| 232 | - if truthy(call(pred, @[x])) == keep: | |
| 233 | - return mkCons(x, lazyFilter(pred, cc, keep)) | |
| 234 | - NilV) | |
| 238 | + while true: | |
| 239 | + var xs = newSeqOfCap[Value](ChunkSize) | |
| 240 | + var drawn = 0 | |
| 241 | + while drawn < ChunkSize and hasNext(cc): | |
| 242 | + let x = next(cc) | |
| 243 | + if truthy(call(pred, @[x])) == keep: xs.add x | |
| 244 | + inc drawn | |
| 245 | + if xs.len > 0: return mkChunk(xs, 0, lazyFilter(pred, cc, keep)) | |
| 246 | + if drawn == 0: return NilV) | |
| 235 | 247 | |
| 236 | 248 | proc lazyTake(n: int, c: Cursor): Value = |
| 237 | 249 | if n <= 0: return mkList(@[]) |
| 238 | 250 | let cur = c |
| 239 | 251 | mkLazy(proc (): Value = |
| 240 | 252 | var cc = cur |
| 241 | - if not hasNext(cc): return NilV | |
| 242 | - let x = next(cc) | |
| 243 | - mkCons(x, lazyTake(n - 1, cc))) | |
| 253 | + var want = min(n, ChunkSize) | |
| 254 | + var xs = newSeqOfCap[Value](want) | |
| 255 | + while xs.len < want and hasNext(cc): xs.add next(cc) | |
| 256 | + if xs.len == 0: return NilV | |
| 257 | + mkChunk(xs, 0, lazyTake(n - xs.len, cc))) | |
| 244 | 258 | |
| 245 | 259 | proc lazyDrop(n: int, c: Cursor): Value = |
| 246 | 260 | let cur = c |
| @@ -271,9 +285,18 @@ proc lazyDropWhile(pred: Value, c: Cursor): Value = | ||
| 271 | 285 | cc = peek) |
| 272 | 286 | |
| 273 | 287 | proc lazyRange(i, hi, step: int64, bounded: bool): Value = |
| 288 | + ## Chunked: one thunk and one chunk per ChunkSize elements rather than a | |
| 289 | + ## cons and a thunk each. An unbounded range stays lazy -- it just realizes | |
| 290 | + ## a bounded batch at a time. | |
| 274 | 291 | mkLazy(proc (): Value = |
| 275 | - if bounded and ((step > 0 and i >= hi) or (step < 0 and i <= hi)): return NilV | |
| 276 | - mkCons(mkInt(i), lazyRange(i + step, hi, step, bounded))) | |
| 292 | + var cur = i | |
| 293 | + var xs = newSeqOfCap[Value](ChunkSize) | |
| 294 | + while xs.len < ChunkSize: | |
| 295 | + if bounded and ((step > 0 and cur >= hi) or (step < 0 and cur <= hi)): break | |
| 296 | + xs.add mkInt(cur) | |
| 297 | + cur += step | |
| 298 | + if xs.len == 0: return NilV | |
| 299 | + mkChunk(xs, 0, lazyRange(cur, hi, step, bounded))) | |
| 277 | 300 | |
| 278 | 301 | proc lazyIterate(f, x: Value): Value = |
| 279 | 302 | mkLazy(proc (): Value = mkCons(x, lazyIterate(f, call(f, @[x])))) |
| @@ -636,12 +659,16 @@ proc registerCore*() = | ||
| 636 | 659 | def "remove", proc (a: seq[Value]): Value = |
| 637 | 660 | lazyFilter(a[0], cursor(a[1]), keep = false) |
| 638 | 661 | def "reduce", proc (a: seq[Value]): Value = |
| 662 | + ## Streams the source rather than materializing it, so folding a lazy seq | |
| 663 | + ## holds one chunk at a time instead of the whole sequence. | |
| 639 | 664 | let f = a[0] |
| 640 | 665 | if a.len == 2: |
| 641 | - let s = toSeq(a[1]) | |
| 642 | - if s.len == 0: return call(f, @[]) | |
| 643 | - var acc = s[0] | |
| 644 | - for i in 1 ..< s.len: acc = call(f, @[acc, s[i]]) | |
| 666 | + var acc = NilV | |
| 667 | + var first = true | |
| 668 | + for x in elems(a[1]): | |
| 669 | + if first: acc = x; first = false | |
| 670 | + else: acc = call(f, @[acc, x]) | |
| 671 | + if first: return call(f, @[]) | |
| 645 | 672 | return acc |
| 646 | 673 | var acc = a[1] |
| 647 | 674 | for x in elems(a[2]): acc = call(f, @[acc, x]) |
| @@ -198,12 +198,17 @@ proc lazyOf(c: Cursor): Value = | |||
| 198 | mkCons(x, lazyOf(cc))) | 198 | mkCons(x, lazyOf(cc))) |
| 199 | 199 | ||
| 200 | proc lazyMap(f: Value, c: Cursor): Value = | 200 | proc lazyMap(f: Value, c: Cursor): Value = |
| 201 | + ## Chunked, which means f runs for up to ChunkSize elements as soon as the | ||
| 202 | + ## first is demanded -- the same trade Clojure makes. f must not be relied on | ||
| 203 | + ## for per-element side effects; `doseq` and `run!` are the tools for that. | ||
| 201 | let cur = c | 204 | let cur = c |
| 202 | mkLazy(proc (): Value = | 205 | mkLazy(proc (): Value = |
| 203 | var cc = cur | 206 | var cc = cur |
| 204 | - if not hasNext(cc): return NilV | 207 | + var xs = newSeqOfCap[Value](ChunkSize) |
| 205 | - let x = next(cc) | 208 | + while xs.len < ChunkSize and hasNext(cc): |
| 206 | - mkCons(call(f, @[x]), lazyMap(f, cc))) | 209 | + xs.add call(f, @[next(cc)]) |
| 210 | + if xs.len == 0: return NilV | ||
| 211 | + mkChunk(xs, 0, lazyMap(f, cc))) | ||
| 207 | 212 | ||
| 208 | proc lazyMapN(f: Value, cs: seq[Cursor]): Value = | 213 | proc lazyMapN(f: Value, cs: seq[Cursor]): Value = |
| 209 | let curs = cs | 214 | let curs = cs |
| @@ -224,23 +229,32 @@ proc lazyMapIndexed(f: Value, i: int64, c: Cursor): Value = | |||
| 224 | mkCons(call(f, @[mkInt(i), x]), lazyMapIndexed(f, i + 1, cc))) | 229 | mkCons(call(f, @[mkInt(i), x]), lazyMapIndexed(f, i + 1, cc))) |
| 225 | 230 | ||
| 226 | proc lazyFilter(pred: Value, c: Cursor, keep: bool): Value = | 231 | proc lazyFilter(pred: Value, c: Cursor, keep: bool): Value = |
| 232 | + ## Draws up to ChunkSize source elements per step and emits whichever pass, | ||
| 233 | + ## rather than pulling until ChunkSize have passed: that keeps the work per | ||
| 234 | + ## step bounded when matches are rare in a long or infinite source. | ||
| 227 | let cur = c | 235 | let cur = c |
| 228 | mkLazy(proc (): Value = | 236 | mkLazy(proc (): Value = |
| 229 | var cc = cur | 237 | var cc = cur |
| 230 | - while hasNext(cc): | 238 | + while true: |
| 231 | - let x = next(cc) | 239 | + var xs = newSeqOfCap[Value](ChunkSize) |
| 232 | - if truthy(call(pred, @[x])) == keep: | 240 | + var drawn = 0 |
| 233 | - return mkCons(x, lazyFilter(pred, cc, keep)) | 241 | + while drawn < ChunkSize and hasNext(cc): |
| 234 | - NilV) | 242 | + let x = next(cc) |
| 243 | + if truthy(call(pred, @[x])) == keep: xs.add x | ||
| 244 | + inc drawn | ||
| 245 | + if xs.len > 0: return mkChunk(xs, 0, lazyFilter(pred, cc, keep)) | ||
| 246 | + if drawn == 0: return NilV) | ||
| 235 | 247 | ||
| 236 | proc lazyTake(n: int, c: Cursor): Value = | 248 | proc lazyTake(n: int, c: Cursor): Value = |
| 237 | if n <= 0: return mkList(@[]) | 249 | if n <= 0: return mkList(@[]) |
| 238 | let cur = c | 250 | let cur = c |
| 239 | mkLazy(proc (): Value = | 251 | mkLazy(proc (): Value = |
| 240 | var cc = cur | 252 | var cc = cur |
| 241 | - if not hasNext(cc): return NilV | 253 | + var want = min(n, ChunkSize) |
| 242 | - let x = next(cc) | 254 | + var xs = newSeqOfCap[Value](want) |
| 243 | - mkCons(x, lazyTake(n - 1, cc))) | 255 | + while xs.len < want and hasNext(cc): xs.add next(cc) |
| 256 | + if xs.len == 0: return NilV | ||
| 257 | + mkChunk(xs, 0, lazyTake(n - xs.len, cc))) | ||
| 244 | 258 | ||
| 245 | proc lazyDrop(n: int, c: Cursor): Value = | 259 | proc lazyDrop(n: int, c: Cursor): Value = |
| 246 | let cur = c | 260 | let cur = c |
| @@ -271,9 +285,18 @@ proc lazyDropWhile(pred: Value, c: Cursor): Value = | |||
| 271 | cc = peek) | 285 | cc = peek) |
| 272 | 286 | ||
| 273 | proc lazyRange(i, hi, step: int64, bounded: bool): Value = | 287 | proc lazyRange(i, hi, step: int64, bounded: bool): Value = |
| 288 | + ## Chunked: one thunk and one chunk per ChunkSize elements rather than a | ||
| 289 | + ## cons and a thunk each. An unbounded range stays lazy -- it just realizes | ||
| 290 | + ## a bounded batch at a time. | ||
| 274 | mkLazy(proc (): Value = | 291 | mkLazy(proc (): Value = |
| 275 | - if bounded and ((step > 0 and i >= hi) or (step < 0 and i <= hi)): return NilV | 292 | + var cur = i |
| 276 | - mkCons(mkInt(i), lazyRange(i + step, hi, step, bounded))) | 293 | + var xs = newSeqOfCap[Value](ChunkSize) |
| 294 | + while xs.len < ChunkSize: | ||
| 295 | + if bounded and ((step > 0 and cur >= hi) or (step < 0 and cur <= hi)): break | ||
| 296 | + xs.add mkInt(cur) | ||
| 297 | + cur += step | ||
| 298 | + if xs.len == 0: return NilV | ||
| 299 | + mkChunk(xs, 0, lazyRange(cur, hi, step, bounded))) | ||
| 277 | 300 | ||
| 278 | proc lazyIterate(f, x: Value): Value = | 301 | proc lazyIterate(f, x: Value): Value = |
| 279 | mkLazy(proc (): Value = mkCons(x, lazyIterate(f, call(f, @[x])))) | 302 | mkLazy(proc (): Value = mkCons(x, lazyIterate(f, call(f, @[x])))) |
| @@ -636,12 +659,16 @@ proc registerCore*() = | |||
| 636 | def "remove", proc (a: seq[Value]): Value = | 659 | def "remove", proc (a: seq[Value]): Value = |
| 637 | lazyFilter(a[0], cursor(a[1]), keep = false) | 660 | lazyFilter(a[0], cursor(a[1]), keep = false) |
| 638 | def "reduce", proc (a: seq[Value]): Value = | 661 | def "reduce", proc (a: seq[Value]): Value = |
| 662 | + ## Streams the source rather than materializing it, so folding a lazy seq | ||
| 663 | + ## holds one chunk at a time instead of the whole sequence. | ||
| 639 | let f = a[0] | 664 | let f = a[0] |
| 640 | if a.len == 2: | 665 | if a.len == 2: |
| 641 | - let s = toSeq(a[1]) | 666 | + var acc = NilV |
| 642 | - if s.len == 0: return call(f, @[]) | 667 | + var first = true |
| 643 | - var acc = s[0] | 668 | + for x in elems(a[1]): |
| 644 | - for i in 1 ..< s.len: acc = call(f, @[acc, s[i]]) | 669 | + if first: acc = x; first = false |
| 670 | + else: acc = call(f, @[acc, x]) | ||
| 671 | + if first: return call(f, @[]) | ||
| 645 | return acc | 672 | return acc |
| 646 | var acc = a[1] | 673 | var acc = a[1] |
| 647 | for x in elems(a[2]): acc = call(f, @[acc, x]) | 674 | for x in elems(a[2]): acc = call(f, @[acc, x]) |
modified
src/runtime.nim +79 -34 | @@ -17,7 +17,7 @@ const | ||
| 17 | 17 | type |
| 18 | 18 | Kind* = enum |
| 19 | 19 | kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol, |
| 20 | - kList, kVector, kMap, kSet, kCons, kLazy, kFn | |
| 20 | + kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn | |
| 21 | 21 | |
| 22 | 22 | VNode* = ref object |
| 23 | 23 | ## A trie node: leaves hold values, internal nodes hold children. |
| @@ -70,6 +70,12 @@ type | ||
| 70 | 70 | of kCons: |
| 71 | 71 | head*: Value |
| 72 | 72 | tl*: Value ## rest of the seq: a cons, a lazy seq, a coll, or nil |
| 73 | + of kChunk: | |
| 74 | + ## A run of elements already realized together, plus the rest of the seq. | |
| 75 | + ## One of these per 32 elements replaces 32 cons cells and 32 thunks. | |
| 76 | + chunk*: seq[Value] | |
| 77 | + coff*: int ## index of the first element not yet consumed | |
| 78 | + ctl*: Value ## rest of the seq beyond this run | |
| 73 | 79 | of kLazy: |
| 74 | 80 | thunk*: proc (): Value {.closure.} |
| 75 | 81 | cached*: Value |
| @@ -107,6 +113,10 @@ proc `=destroy`*(x: var ValueObj) = | ||
| 107 | 113 | `=destroy`(x.head) |
| 108 | 114 | if not x.tl.isNil: pendingFree.add x.tl # +1, outlives the release below |
| 109 | 115 | `=destroy`(x.tl) |
| 116 | + of kChunk: | |
| 117 | + `=destroy`(x.chunk) | |
| 118 | + if not x.ctl.isNil: pendingFree.add x.ctl | |
| 119 | + `=destroy`(x.ctl) | |
| 110 | 120 | of kLazy: |
| 111 | 121 | `=destroy`(x.thunk) |
| 112 | 122 | if not x.cached.isNil: pendingFree.add x.cached |
| @@ -407,6 +417,16 @@ proc mkFn*(name: string, f: proc (args: seq[Value]): Value {.closure.}): Value = | ||
| 407 | 417 | |
| 408 | 418 | proc mkCons*(h, t: Value): Value = Value(kind: kCons, head: h, tl: t) |
| 409 | 419 | |
| 420 | +const ChunkSize* = 32 | |
| 421 | + ## Elements realized per step by a chunked producer. The same batch size | |
| 422 | + ## Clojure uses: big enough to amortize the per-step allocation, small enough | |
| 423 | + ## that an infinite seq still costs a bounded amount to look at. | |
| 424 | + | |
| 425 | +proc mkChunk*(xs: sink seq[Value], off: int, rest: Value): Value = | |
| 426 | + ## Normalizes an exhausted chunk away, so a cursor never sees an empty run. | |
| 427 | + if off >= xs.len: return (if rest.isNil: NilV else: rest) | |
| 428 | + Value(kind: kChunk, chunk: xs, coff: off, ctl: rest) | |
| 429 | + | |
| 410 | 430 | proc mkLazy*(f: proc (): Value {.closure.}): Value = |
| 411 | 431 | Value(kind: kLazy, thunk: f, cached: nil, forced: false) |
| 412 | 432 | |
| @@ -423,43 +443,64 @@ proc force*(v: Value): Value = | ||
| 423 | 443 | cur |
| 424 | 444 | |
| 425 | 445 | proc isSeqNode(v: Value): bool = |
| 426 | - not v.isNil and v.kind in {kCons, kLazy} | |
| 446 | + not v.isNil and v.kind in {kCons, kChunk, kLazy} | |
| 427 | 447 | |
| 428 | 448 | type Cursor* = object |
| 429 | - ## Walks any seqable value without materializing it. Cons/lazy chains are | |
| 430 | - ## followed link by link; concrete collections are indexed. | |
| 431 | - node: Value | |
| 432 | - backing: seq[Value] | |
| 449 | + ## Walks any seqable value without materializing it. A chunk is indexed | |
| 450 | + ## where it lies -- copying its elements into the cursor would undo the point | |
| 451 | + ## of chunking -- a concrete collection is indexed through `backing`, and | |
| 452 | + ## cons/lazy links are followed one at a time. `node` is the unrealized rest. | |
| 453 | + run: Value ## kChunk currently being indexed, else nil | |
| 454 | + backing: seq[Value] ## concrete collection being indexed, else empty | |
| 433 | 455 | idx: int |
| 434 | - isNode: bool | |
| 456 | + node: Value | |
| 435 | 457 | |
| 436 | 458 | proc cursor*(v: Value): Cursor = |
| 437 | - ## Forces nothing: a cons/lazy value is walked link by link, and `hasNext` | |
| 438 | - ## is the only thing that ever forces. So building `(take 3 (map f xs))` | |
| 439 | - ## runs `f` zero times until something asks for an element. | |
| 440 | - if v.isNil: return Cursor(isNode: false) | |
| 441 | - if v.kind in {kCons, kLazy, kNil}: Cursor(isNode: true, node: v) | |
| 442 | - else: Cursor(isNode: false, backing: toSeq(v)) | |
| 459 | + ## Forces nothing: `hasNext` is the only thing that ever forces, so building | |
| 460 | + ## `(take 3 (map f xs))` runs `f` zero times until something asks for an | |
| 461 | + ## element. | |
| 462 | + if v.isNil: return Cursor() | |
| 463 | + if v.kind in {kCons, kChunk, kLazy, kNil}: Cursor(node: v) | |
| 464 | + else: Cursor(backing: toSeq(v)) | |
| 443 | 465 | |
| 444 | 466 | proc hasNext*(c: var Cursor): bool = |
| 445 | - if not c.isNode: return c.idx < c.backing.len | |
| 446 | - c.node = force(c.node) | |
| 447 | - if c.node.isNil or c.node.kind == kNil: return false | |
| 448 | - if c.node.kind == kCons: return true | |
| 449 | - # The tail bottomed out in a concrete collection, as `(cons x [1 2])` does. | |
| 450 | - # Switch to indexing it rather than reporting the seq as finished. | |
| 451 | - c.isNode = false | |
| 452 | - c.backing = toSeq(c.node) | |
| 453 | - c.idx = 0 | |
| 454 | - c.idx < c.backing.len | |
| 467 | + while true: | |
| 468 | + if not c.run.isNil: | |
| 469 | + if c.idx < c.run.chunk.len: return true | |
| 470 | + c.run = nil | |
| 471 | + c.idx = 0 | |
| 472 | + elif c.idx < c.backing.len: | |
| 473 | + return true | |
| 474 | + if c.node.isNil: return false | |
| 475 | + c.node = force(c.node) | |
| 476 | + if c.node.isNil or c.node.kind == kNil: | |
| 477 | + c.node = nil | |
| 478 | + return false | |
| 479 | + case c.node.kind | |
| 480 | + of kCons: | |
| 481 | + return true # the element is c.node.head | |
| 482 | + of kChunk: | |
| 483 | + c.run = c.node | |
| 484 | + c.idx = c.node.coff | |
| 485 | + c.backing = @[] | |
| 486 | + c.node = c.node.ctl | |
| 487 | + else: | |
| 488 | + # The tail bottomed out in a concrete collection, as `(cons x [1 2])` | |
| 489 | + # does. Switch to indexing it rather than reporting the seq as finished. | |
| 490 | + c.backing = toSeq(c.node) | |
| 491 | + c.idx = 0 | |
| 492 | + c.node = nil | |
| 455 | 493 | |
| 456 | 494 | proc next*(c: var Cursor): Value = |
| 457 | - if c.isNode: | |
| 458 | - result = c.node.head | |
| 459 | - c.node = c.node.tl | |
| 460 | - else: | |
| 495 | + if not c.run.isNil: | |
| 496 | + result = c.run.chunk[c.idx] | |
| 497 | + inc c.idx | |
| 498 | + elif c.idx < c.backing.len: | |
| 461 | 499 | result = c.backing[c.idx] |
| 462 | 500 | inc c.idx |
| 501 | + else: | |
| 502 | + result = c.node.head # valid only just after hasNext saw a cons | |
| 503 | + c.node = c.node.tl | |
| 463 | 504 | |
| 464 | 505 | iterator elems*(v: Value): Value = |
| 465 | 506 | ## The one way to walk a collection in core: works for lists, vectors, maps, |
| @@ -472,6 +513,7 @@ proc seqFirst*(v: Value): Value = | ||
| 472 | 513 | let f = force(v) |
| 473 | 514 | if f.isNil: return NilV |
| 474 | 515 | if f.kind == kCons: return f.head |
| 516 | + if f.kind == kChunk: return f.chunk[f.coff] | |
| 475 | 517 | var c = cursor(f) |
| 476 | 518 | (if hasNext(c): next(c) else: NilV) |
| 477 | 519 | |
| @@ -481,6 +523,9 @@ proc seqRest*(v: Value): Value = | ||
| 481 | 523 | let f = force(v) |
| 482 | 524 | if f.isNil or f.kind == kNil: return mkList(@[]) |
| 483 | 525 | if f.kind == kCons: return (if f.tl.isNil: mkList(@[]) else: f.tl) |
| 526 | + if f.kind == kChunk: | |
| 527 | + let r = mkChunk(f.chunk, f.coff + 1, f.ctl) | |
| 528 | + return (if r.isNil or r.kind == kNil: mkList(@[]) else: r) | |
| 484 | 529 | let xs = toSeq(f) |
| 485 | 530 | (if xs.len <= 1: mkList(@[]) else: mkList(xs[1 .. ^1])) |
| 486 | 531 | |
| @@ -512,7 +557,7 @@ proc hashValue*(v: Value): uint32 = | ||
| 512 | 557 | of kStr: mixHash(1'u32, uint32(hash(v.s))) |
| 513 | 558 | of kKeyword: mixHash(2'u32, uint32(hash(v.s))) |
| 514 | 559 | of kSymbol: mixHash(3'u32, uint32(hash(v.s))) |
| 515 | - of kList, kVector, kCons, kLazy: | |
| 560 | + of kList, kVector, kCons, kChunk, kLazy: | |
| 516 | 561 | # sequentials are `=` when their elements are, so they hash alike |
| 517 | 562 | var h = 7'u32 |
| 518 | 563 | for x in elems(v): h = mixHash(h, hashValue(x)) |
| @@ -540,7 +585,7 @@ proc items*(v: Value): seq[Value] = | ||
| 540 | 585 | var r = newSeqOfCap[Value](v.m.cnt) |
| 541 | 586 | for e in mapEntries(v.m): r.add e.key |
| 542 | 587 | r |
| 543 | - of kCons, kLazy: | |
| 588 | + of kCons, kChunk, kLazy: | |
| 544 | 589 | var r: seq[Value] = @[] |
| 545 | 590 | var c = cursor(v) |
| 546 | 591 | while hasNext(c): r.add next(c) |
| @@ -560,7 +605,7 @@ proc count*(v: Value): int = | ||
| 560 | 605 | of kVector: v.vec.cnt |
| 561 | 606 | of kMap, kSet: v.m.cnt |
| 562 | 607 | of kStr: v.s.len |
| 563 | - of kCons, kLazy: | |
| 608 | + of kCons, kChunk, kLazy: | |
| 564 | 609 | # realizes the whole seq, which is the honest cost of counting one |
| 565 | 610 | var n = 0 |
| 566 | 611 | var c = cursor(v) |
| @@ -582,7 +627,7 @@ proc equals*(a, b: Value): bool = | ||
| 582 | 627 | if a.kind == kInt and b.kind == kFloat: return float64(a.i) == b.f |
| 583 | 628 | if a.kind == kFloat and b.kind == kInt: return a.f == float64(b.i) |
| 584 | 629 | # every sequential thing is `=` to every other with the same elements |
| 585 | - const Seqs = {kList, kVector, kCons, kLazy} | |
| 630 | + const Seqs = {kList, kVector, kCons, kChunk, kLazy} | |
| 586 | 631 | if a.kind in Seqs and b.kind in Seqs: |
| 587 | 632 | var ca = cursor(a) |
| 588 | 633 | var cb = cursor(b) |
| @@ -610,7 +655,7 @@ proc equals*(a, b: Value): bool = | ||
| 610 | 655 | if not equals(e.val, mapGet(b.m, e.key, missing)): return false |
| 611 | 656 | true |
| 612 | 657 | of kFn: a == b |
| 613 | - of kList, kVector, kCons, kLazy: false # handled above | |
| 658 | + of kList, kVector, kCons, kChunk, kLazy: false # handled above | |
| 614 | 659 | # ---------------------------------------------------------------- printing |
| 615 | 660 | proc escapeStr(s: string): string = |
| 616 | 661 | result = "\"" |
| @@ -637,7 +682,7 @@ proc toStr*(v: Value, readable: bool): string = | ||
| 637 | 682 | of kStr: (if readable: escapeStr(v.s) else: v.s) |
| 638 | 683 | of kKeyword: ":" & v.s |
| 639 | 684 | of kSymbol: v.s |
| 640 | - of kList, kCons, kLazy: | |
| 685 | + of kList, kCons, kChunk, kLazy: | |
| 641 | 686 | # printing a lazy seq realizes it, exactly as in Clojure |
| 642 | 687 | var parts: seq[string] = @[] |
| 643 | 688 | for x in elems(v): parts.add toStr(x, readable) |
| @@ -726,7 +771,7 @@ proc toSeq*(v: Value): seq[Value] = | ||
| 726 | 771 | if v.isNil: return @[] |
| 727 | 772 | case v.kind |
| 728 | 773 | of kNil: @[] |
| 729 | - of kList, kVector, kSet, kCons, kLazy: v.items | |
| 774 | + of kList, kVector, kSet, kCons, kChunk, kLazy: v.items | |
| 730 | 775 | of kStr: |
| 731 | 776 | var r: seq[Value] = @[] |
| 732 | 777 | for c in v.s: r.add mkStr($c) |
| @@ -17,7 +17,7 @@ const | |||
| 17 | type | 17 | type |
| 18 | Kind* = enum | 18 | Kind* = enum |
| 19 | kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol, | 19 | kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol, |
| 20 | - kList, kVector, kMap, kSet, kCons, kLazy, kFn | 20 | + kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn |
| 21 | 21 | ||
| 22 | VNode* = ref object | 22 | VNode* = ref object |
| 23 | ## A trie node: leaves hold values, internal nodes hold children. | 23 | ## A trie node: leaves hold values, internal nodes hold children. |
| @@ -70,6 +70,12 @@ type | |||
| 70 | of kCons: | 70 | of kCons: |
| 71 | head*: Value | 71 | head*: Value |
| 72 | tl*: Value ## rest of the seq: a cons, a lazy seq, a coll, or nil | 72 | tl*: Value ## rest of the seq: a cons, a lazy seq, a coll, or nil |
| 73 | + of kChunk: | ||
| 74 | + ## A run of elements already realized together, plus the rest of the seq. | ||
| 75 | + ## One of these per 32 elements replaces 32 cons cells and 32 thunks. | ||
| 76 | + chunk*: seq[Value] | ||
| 77 | + coff*: int ## index of the first element not yet consumed | ||
| 78 | + ctl*: Value ## rest of the seq beyond this run | ||
| 73 | of kLazy: | 79 | of kLazy: |
| 74 | thunk*: proc (): Value {.closure.} | 80 | thunk*: proc (): Value {.closure.} |
| 75 | cached*: Value | 81 | cached*: Value |
| @@ -107,6 +113,10 @@ proc `=destroy`*(x: var ValueObj) = | |||
| 107 | `=destroy`(x.head) | 113 | `=destroy`(x.head) |
| 108 | if not x.tl.isNil: pendingFree.add x.tl # +1, outlives the release below | 114 | if not x.tl.isNil: pendingFree.add x.tl # +1, outlives the release below |
| 109 | `=destroy`(x.tl) | 115 | `=destroy`(x.tl) |
| 116 | + of kChunk: | ||
| 117 | + `=destroy`(x.chunk) | ||
| 118 | + if not x.ctl.isNil: pendingFree.add x.ctl | ||
| 119 | + `=destroy`(x.ctl) | ||
| 110 | of kLazy: | 120 | of kLazy: |
| 111 | `=destroy`(x.thunk) | 121 | `=destroy`(x.thunk) |
| 112 | if not x.cached.isNil: pendingFree.add x.cached | 122 | if not x.cached.isNil: pendingFree.add x.cached |
| @@ -407,6 +417,16 @@ proc mkFn*(name: string, f: proc (args: seq[Value]): Value {.closure.}): Value = | |||
| 407 | 417 | ||
| 408 | proc mkCons*(h, t: Value): Value = Value(kind: kCons, head: h, tl: t) | 418 | proc mkCons*(h, t: Value): Value = Value(kind: kCons, head: h, tl: t) |
| 409 | 419 | ||
| 420 | +const ChunkSize* = 32 | ||
| 421 | + ## Elements realized per step by a chunked producer. The same batch size | ||
| 422 | + ## Clojure uses: big enough to amortize the per-step allocation, small enough | ||
| 423 | + ## that an infinite seq still costs a bounded amount to look at. | ||
| 424 | + | ||
| 425 | +proc mkChunk*(xs: sink seq[Value], off: int, rest: Value): Value = | ||
| 426 | + ## Normalizes an exhausted chunk away, so a cursor never sees an empty run. | ||
| 427 | + if off >= xs.len: return (if rest.isNil: NilV else: rest) | ||
| 428 | + Value(kind: kChunk, chunk: xs, coff: off, ctl: rest) | ||
| 429 | + | ||
| 410 | proc mkLazy*(f: proc (): Value {.closure.}): Value = | 430 | proc mkLazy*(f: proc (): Value {.closure.}): Value = |
| 411 | Value(kind: kLazy, thunk: f, cached: nil, forced: false) | 431 | Value(kind: kLazy, thunk: f, cached: nil, forced: false) |
| 412 | 432 | ||
| @@ -423,43 +443,64 @@ proc force*(v: Value): Value = | |||
| 423 | cur | 443 | cur |
| 424 | 444 | ||
| 425 | proc isSeqNode(v: Value): bool = | 445 | proc isSeqNode(v: Value): bool = |
| 426 | - not v.isNil and v.kind in {kCons, kLazy} | 446 | + not v.isNil and v.kind in {kCons, kChunk, kLazy} |
| 427 | 447 | ||
| 428 | type Cursor* = object | 448 | type Cursor* = object |
| 429 | - ## Walks any seqable value without materializing it. Cons/lazy chains are | 449 | + ## Walks any seqable value without materializing it. A chunk is indexed |
| 430 | - ## followed link by link; concrete collections are indexed. | 450 | + ## where it lies -- copying its elements into the cursor would undo the point |
| 431 | - node: Value | 451 | + ## of chunking -- a concrete collection is indexed through `backing`, and |
| 432 | - backing: seq[Value] | 452 | + ## cons/lazy links are followed one at a time. `node` is the unrealized rest. |
| 453 | + run: Value ## kChunk currently being indexed, else nil | ||
| 454 | + backing: seq[Value] ## concrete collection being indexed, else empty | ||
| 433 | idx: int | 455 | idx: int |
| 434 | - isNode: bool | 456 | + node: Value |
| 435 | 457 | ||
| 436 | proc cursor*(v: Value): Cursor = | 458 | proc cursor*(v: Value): Cursor = |
| 437 | - ## Forces nothing: a cons/lazy value is walked link by link, and `hasNext` | 459 | + ## Forces nothing: `hasNext` is the only thing that ever forces, so building |
| 438 | - ## is the only thing that ever forces. So building `(take 3 (map f xs))` | 460 | + ## `(take 3 (map f xs))` runs `f` zero times until something asks for an |
| 439 | - ## runs `f` zero times until something asks for an element. | 461 | + ## element. |
| 440 | - if v.isNil: return Cursor(isNode: false) | 462 | + if v.isNil: return Cursor() |
| 441 | - if v.kind in {kCons, kLazy, kNil}: Cursor(isNode: true, node: v) | 463 | + if v.kind in {kCons, kChunk, kLazy, kNil}: Cursor(node: v) |
| 442 | - else: Cursor(isNode: false, backing: toSeq(v)) | 464 | + else: Cursor(backing: toSeq(v)) |
| 443 | 465 | ||
| 444 | proc hasNext*(c: var Cursor): bool = | 466 | proc hasNext*(c: var Cursor): bool = |
| 445 | - if not c.isNode: return c.idx < c.backing.len | 467 | + while true: |
| 446 | - c.node = force(c.node) | 468 | + if not c.run.isNil: |
| 447 | - if c.node.isNil or c.node.kind == kNil: return false | 469 | + if c.idx < c.run.chunk.len: return true |
| 448 | - if c.node.kind == kCons: return true | 470 | + c.run = nil |
| 449 | - # The tail bottomed out in a concrete collection, as `(cons x [1 2])` does. | 471 | + c.idx = 0 |
| 450 | - # Switch to indexing it rather than reporting the seq as finished. | 472 | + elif c.idx < c.backing.len: |
| 451 | - c.isNode = false | 473 | + return true |
| 452 | - c.backing = toSeq(c.node) | 474 | + if c.node.isNil: return false |
| 453 | - c.idx = 0 | 475 | + c.node = force(c.node) |
| 454 | - c.idx < c.backing.len | 476 | + if c.node.isNil or c.node.kind == kNil: |
| 477 | + c.node = nil | ||
| 478 | + return false | ||
| 479 | + case c.node.kind | ||
| 480 | + of kCons: | ||
| 481 | + return true # the element is c.node.head | ||
| 482 | + of kChunk: | ||
| 483 | + c.run = c.node | ||
| 484 | + c.idx = c.node.coff | ||
| 485 | + c.backing = @[] | ||
| 486 | + c.node = c.node.ctl | ||
| 487 | + else: | ||
| 488 | + # The tail bottomed out in a concrete collection, as `(cons x [1 2])` | ||
| 489 | + # does. Switch to indexing it rather than reporting the seq as finished. | ||
| 490 | + c.backing = toSeq(c.node) | ||
| 491 | + c.idx = 0 | ||
| 492 | + c.node = nil | ||
| 455 | 493 | ||
| 456 | proc next*(c: var Cursor): Value = | 494 | proc next*(c: var Cursor): Value = |
| 457 | - if c.isNode: | 495 | + if not c.run.isNil: |
| 458 | - result = c.node.head | 496 | + result = c.run.chunk[c.idx] |
| 459 | - c.node = c.node.tl | 497 | + inc c.idx |
| 460 | - else: | 498 | + elif c.idx < c.backing.len: |
| 461 | result = c.backing[c.idx] | 499 | result = c.backing[c.idx] |
| 462 | inc c.idx | 500 | inc c.idx |
| 501 | + else: | ||
| 502 | + result = c.node.head # valid only just after hasNext saw a cons | ||
| 503 | + c.node = c.node.tl | ||
| 463 | 504 | ||
| 464 | iterator elems*(v: Value): Value = | 505 | iterator elems*(v: Value): Value = |
| 465 | ## The one way to walk a collection in core: works for lists, vectors, maps, | 506 | ## The one way to walk a collection in core: works for lists, vectors, maps, |
| @@ -472,6 +513,7 @@ proc seqFirst*(v: Value): Value = | |||
| 472 | let f = force(v) | 513 | let f = force(v) |
| 473 | if f.isNil: return NilV | 514 | if f.isNil: return NilV |
| 474 | if f.kind == kCons: return f.head | 515 | if f.kind == kCons: return f.head |
| 516 | + if f.kind == kChunk: return f.chunk[f.coff] | ||
| 475 | var c = cursor(f) | 517 | var c = cursor(f) |
| 476 | (if hasNext(c): next(c) else: NilV) | 518 | (if hasNext(c): next(c) else: NilV) |
| 477 | 519 | ||
| @@ -481,6 +523,9 @@ proc seqRest*(v: Value): Value = | |||
| 481 | let f = force(v) | 523 | let f = force(v) |
| 482 | if f.isNil or f.kind == kNil: return mkList(@[]) | 524 | if f.isNil or f.kind == kNil: return mkList(@[]) |
| 483 | if f.kind == kCons: return (if f.tl.isNil: mkList(@[]) else: f.tl) | 525 | if f.kind == kCons: return (if f.tl.isNil: mkList(@[]) else: f.tl) |
| 526 | + if f.kind == kChunk: | ||
| 527 | + let r = mkChunk(f.chunk, f.coff + 1, f.ctl) | ||
| 528 | + return (if r.isNil or r.kind == kNil: mkList(@[]) else: r) | ||
| 484 | let xs = toSeq(f) | 529 | let xs = toSeq(f) |
| 485 | (if xs.len <= 1: mkList(@[]) else: mkList(xs[1 .. ^1])) | 530 | (if xs.len <= 1: mkList(@[]) else: mkList(xs[1 .. ^1])) |
| 486 | 531 | ||
| @@ -512,7 +557,7 @@ proc hashValue*(v: Value): uint32 = | |||
| 512 | of kStr: mixHash(1'u32, uint32(hash(v.s))) | 557 | of kStr: mixHash(1'u32, uint32(hash(v.s))) |
| 513 | of kKeyword: mixHash(2'u32, uint32(hash(v.s))) | 558 | of kKeyword: mixHash(2'u32, uint32(hash(v.s))) |
| 514 | of kSymbol: mixHash(3'u32, uint32(hash(v.s))) | 559 | of kSymbol: mixHash(3'u32, uint32(hash(v.s))) |
| 515 | - of kList, kVector, kCons, kLazy: | 560 | + of kList, kVector, kCons, kChunk, kLazy: |
| 516 | # sequentials are `=` when their elements are, so they hash alike | 561 | # sequentials are `=` when their elements are, so they hash alike |
| 517 | var h = 7'u32 | 562 | var h = 7'u32 |
| 518 | for x in elems(v): h = mixHash(h, hashValue(x)) | 563 | for x in elems(v): h = mixHash(h, hashValue(x)) |
| @@ -540,7 +585,7 @@ proc items*(v: Value): seq[Value] = | |||
| 540 | var r = newSeqOfCap[Value](v.m.cnt) | 585 | var r = newSeqOfCap[Value](v.m.cnt) |
| 541 | for e in mapEntries(v.m): r.add e.key | 586 | for e in mapEntries(v.m): r.add e.key |
| 542 | r | 587 | r |
| 543 | - of kCons, kLazy: | 588 | + of kCons, kChunk, kLazy: |
| 544 | var r: seq[Value] = @[] | 589 | var r: seq[Value] = @[] |
| 545 | var c = cursor(v) | 590 | var c = cursor(v) |
| 546 | while hasNext(c): r.add next(c) | 591 | while hasNext(c): r.add next(c) |
| @@ -560,7 +605,7 @@ proc count*(v: Value): int = | |||
| 560 | of kVector: v.vec.cnt | 605 | of kVector: v.vec.cnt |
| 561 | of kMap, kSet: v.m.cnt | 606 | of kMap, kSet: v.m.cnt |
| 562 | of kStr: v.s.len | 607 | of kStr: v.s.len |
| 563 | - of kCons, kLazy: | 608 | + of kCons, kChunk, kLazy: |
| 564 | # realizes the whole seq, which is the honest cost of counting one | 609 | # realizes the whole seq, which is the honest cost of counting one |
| 565 | var n = 0 | 610 | var n = 0 |
| 566 | var c = cursor(v) | 611 | var c = cursor(v) |
| @@ -582,7 +627,7 @@ proc equals*(a, b: Value): bool = | |||
| 582 | if a.kind == kInt and b.kind == kFloat: return float64(a.i) == b.f | 627 | if a.kind == kInt and b.kind == kFloat: return float64(a.i) == b.f |
| 583 | if a.kind == kFloat and b.kind == kInt: return a.f == float64(b.i) | 628 | if a.kind == kFloat and b.kind == kInt: return a.f == float64(b.i) |
| 584 | # every sequential thing is `=` to every other with the same elements | 629 | # every sequential thing is `=` to every other with the same elements |
| 585 | - const Seqs = {kList, kVector, kCons, kLazy} | 630 | + const Seqs = {kList, kVector, kCons, kChunk, kLazy} |
| 586 | if a.kind in Seqs and b.kind in Seqs: | 631 | if a.kind in Seqs and b.kind in Seqs: |
| 587 | var ca = cursor(a) | 632 | var ca = cursor(a) |
| 588 | var cb = cursor(b) | 633 | var cb = cursor(b) |
| @@ -610,7 +655,7 @@ proc equals*(a, b: Value): bool = | |||
| 610 | if not equals(e.val, mapGet(b.m, e.key, missing)): return false | 655 | if not equals(e.val, mapGet(b.m, e.key, missing)): return false |
| 611 | true | 656 | true |
| 612 | of kFn: a == b | 657 | of kFn: a == b |
| 613 | - of kList, kVector, kCons, kLazy: false # handled above | 658 | + of kList, kVector, kCons, kChunk, kLazy: false # handled above |
| 614 | # ---------------------------------------------------------------- printing | 659 | # ---------------------------------------------------------------- printing |
| 615 | proc escapeStr(s: string): string = | 660 | proc escapeStr(s: string): string = |
| 616 | result = "\"" | 661 | result = "\"" |
| @@ -637,7 +682,7 @@ proc toStr*(v: Value, readable: bool): string = | |||
| 637 | of kStr: (if readable: escapeStr(v.s) else: v.s) | 682 | of kStr: (if readable: escapeStr(v.s) else: v.s) |
| 638 | of kKeyword: ":" & v.s | 683 | of kKeyword: ":" & v.s |
| 639 | of kSymbol: v.s | 684 | of kSymbol: v.s |
| 640 | - of kList, kCons, kLazy: | 685 | + of kList, kCons, kChunk, kLazy: |
| 641 | # printing a lazy seq realizes it, exactly as in Clojure | 686 | # printing a lazy seq realizes it, exactly as in Clojure |
| 642 | var parts: seq[string] = @[] | 687 | var parts: seq[string] = @[] |
| 643 | for x in elems(v): parts.add toStr(x, readable) | 688 | for x in elems(v): parts.add toStr(x, readable) |
| @@ -726,7 +771,7 @@ proc toSeq*(v: Value): seq[Value] = | |||
| 726 | if v.isNil: return @[] | 771 | if v.isNil: return @[] |
| 727 | case v.kind | 772 | case v.kind |
| 728 | of kNil: @[] | 773 | of kNil: @[] |
| 729 | - of kList, kVector, kSet, kCons, kLazy: v.items | 774 | + of kList, kVector, kSet, kCons, kChunk, kLazy: v.items |
| 730 | of kStr: | 775 | of kStr: |
| 731 | var r: seq[Value] = @[] | 776 | var r: seq[Value] = @[] |
| 732 | for c in v.s: r.add mkStr($c) | 777 | for c in v.s: r.add mkStr($c) |
added
tests/chunking.expected +14 -0 | new file mode 100644 | ||
| @@ -0,0 +1,14 @@ | ||
| 1 | +built: 0 | |
| 2 | +(0 1 2) calls: 32 | |
| 3 | +(0 1 2) calls: 32 | |
| 4 | +(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39) calls: 64 | |
| 5 | +(0 2 4 6 8) | |
| 6 | +101 | |
| 7 | +(1 2 3) | |
| 8 | +1000 (10 11 12 13) | |
| 9 | +100 | |
| 10 | +5000 | |
| 11 | +(1 2 0 1) | |
| 12 | +11 | |
| 13 | +115 | |
| 14 | +(10 21 12) | |
| new file mode 100644 | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | +built: 0 | ||
| 2 | +(0 1 2) calls: 32 | ||
| 3 | +(0 1 2) calls: 32 | ||
| 4 | +(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39) calls: 64 | ||
| 5 | +(0 2 4 6 8) | ||
| 6 | +101 | ||
| 7 | +(1 2 3) | ||
| 8 | +1000 (10 11 12 13) | ||
| 9 | +100 | ||
| 10 | +5000 | ||
| 11 | +(1 2 0 1) | ||
| 12 | +11 | ||
| 13 | +115 | ||
| 14 | +(10 21 12) | ||
modified
tests/lazy.expected +2 -2 | @@ -10,8 +10,8 @@ | ||
| 10 | 10 | (1 2 0 1 2) |
| 11 | 11 | 1 1 1000 |
| 12 | 12 | built, calls so far: 0 |
| 13 | -took (0 1 2) - calls: 3 | |
| 14 | -took (0 1 2) - calls: 3 (memoized) | |
| 13 | +took (0 1 2) - calls: 32 | |
| 14 | +took (0 1 2) - calls: 32 (memoized) | |
| 15 | 15 | 1330 |
| 16 | 16 | 0 1 (2 3 4) |
| 17 | 17 | (2 3 4) (2 4) |
| @@ -10,8 +10,8 @@ | |||
| 10 | (1 2 0 1 2) | 10 | (1 2 0 1 2) |
| 11 | 1 1 1000 | 11 | 1 1 1000 |
| 12 | built, calls so far: 0 | 12 | built, calls so far: 0 |
| 13 | -took (0 1 2) - calls: 3 | 13 | +took (0 1 2) - calls: 32 |
| 14 | -took (0 1 2) - calls: 3 (memoized) | 14 | +took (0 1 2) - calls: 32 (memoized) |
| 15 | 1330 | 15 | 1330 |
| 16 | 0 1 (2 3 4) | 16 | 0 1 (2 3 4) |
| 17 | (2 3 4) (2 4) | 17 | (2 3 4) (2 4) |