nandi/clonimpublic Fork
⑂ ad40357
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/clonim.git
git clone ssh://git@rickub.com/nandi/clonim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

perf: unbox scalars, so a sequence of integers stops allocating

Value was a ref, so every integer in a sequence was a heap object: a million
mkInt calls for `(range 1000000)`, each with its own allocation and refcount
traffic. Value is now a plain object carrying kind, a raw int64 payload and an
Obj pointer that only the kinds with a real payload populate. Nil, bool, int
and float never touch the heap.

Field accessors keep the call sites reading as they did -- `v.i`, `v.xs`,
`v.head` are inline procs now, with `lent` returns so strings and seqs are not
copied on access -- and isNil becomes a kind test, which is what every existing
`v.isNil` guard already meant.

The two things that made this look risky both hold, because the heap kinds
still live behind a shared ref:

  * lazy memoisation. force() mutates through `obj`, so a thunk is still
    forced once no matter how many Values name it. examples/lazy.clj counts
    calls and is unchanged.
  * chain teardown. The =destroy worklist that keeps a long cons chain from
    recursing a million frames deep at scope exit still applies, since it is
    defined on ValueObj. A 2M-element realized seq and a 500k-deep cons chain
    both tear down cleanly.

Dropping this during the port was the wrong call. It was redundant for fib and
loop, which is what I checked, but those specialise to raw int64 and never
build a Value at all. Sequence code is the opposite case: nothing specialises,
every element is a boxed Value moving through a cursor, and that is precisely
where the representation is the cost.

Compute excluding startup, against main before any of this work:

                      before   after
  seqs                884 ms   136 ms
  fib 30              755 ms     3 ms
  loop 10M           2474 ms     3 ms
  persistent-bench    122 ms    91 ms
  lazy-bench            0 ms     0 ms

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-20T19:29:40-07:00 Browse files
ad40357 parent: a6f57fd
modified src/runtime.nim +70 -41
@@ -56,13 +56,15 @@ type
5656 cnt*: int
5757 nextOrd*: int
5858
59- Value* = ref ValueObj
59+ Obj* = ref ValueObj
60+ ## Value itself is not a ref: nil, bool, int and float live entirely in the
61+ ## word below, so arithmetic and a numeric sequence never touch the heap. Only
62+ ## the kinds with a real payload allocate a ValueObj, which is also what keeps
63+ ## a lazy seq's memoisation working -- `force` mutates through `obj`, which is
64+ ## still shared by every Value that names it.
6065 ValueObj* = object
6166 case kind*: Kind
62- of kNil: discard
63- of kBool: b*: bool
64- of kInt: i*: int64
65- of kFloat: f*: float64
67+ of kNil, kBool, kInt, kFloat: discard
6668 of kStr, kKeyword, kSymbol: s*: string
6769 of kList: xs*: seq[Value]
6870 of kVector: vec*: PVec
@@ -84,8 +86,37 @@ type
8486 fn*: proc (args: openArray[Value]): Value {.closure.}
8587 name*: string
8688
89+ Value* = object
90+ kind*: Kind
91+ raw*: int64 ## int payload; a float is bit-cast into it; bool is 0/1
92+ obj*: Obj ## nil for the scalar kinds
93+
8794 CljError* = object of CatchableError
8895
96+# Field accessors, named so the rest of the codebase keeps reading `v.i`,
97+# `v.xs`, `v.head` exactly as it did when Value was a ref to the variant.
98+proc i*(v: Value): int64 {.inline.} = v.raw
99+proc b*(v: Value): bool {.inline.} = v.raw != 0
100+proc f*(v: Value): float64 {.inline.} = cast[float64](v.raw)
101+proc s*(v: Value): lent string {.inline.} = v.obj.s
102+proc xs*(v: Value): lent seq[Value] {.inline.} = v.obj.xs
103+proc vec*(v: Value): lent PVec {.inline.} = v.obj.vec
104+proc m*(v: Value): lent PMap {.inline.} = v.obj.m
105+proc head*(v: Value): lent Value {.inline.} = v.obj.head
106+proc tl*(v: Value): lent Value {.inline.} = v.obj.tl
107+proc chunk*(v: Value): lent seq[Value] {.inline.} = v.obj.chunk
108+proc coff*(v: Value): int {.inline.} = v.obj.coff
109+proc ctl*(v: Value): lent Value {.inline.} = v.obj.ctl
110+proc cached*(v: Value): lent Value {.inline.} = v.obj.cached
111+proc forced*(v: Value): bool {.inline.} = v.obj.forced
112+proc thunk*(v: Value): auto {.inline.} = v.obj.thunk
113+proc fn*(v: Value): auto {.inline.} = v.obj.fn
114+proc name*(v: Value): lent string {.inline.} = v.obj.name
115+
116+## A Value is never a nil pointer now, but every `v.isNil` in the codebase
117+## meant "is this the nil value", so keep the spelling working.
118+proc isNil*(v: Value): bool {.inline.} = v.kind == kNil
119+
89120 # ------------------------------------------------------------ teardown
90121 ## A realized lazy seq is a chain of `cons -> lazy -> cons -> …` refs, and ARC
91122 ## frees a chain by recursing into it — a million-element seq means a million
@@ -130,8 +161,8 @@ proc `=destroy`*(x: var ValueObj) =
130161 draining = false
131162
132163 let NilV* = Value(kind: kNil)
133-let TrueV* = Value(kind: kBool, b: true)
134-let FalseV* = Value(kind: kBool, b: false)
164+let TrueV* = Value(kind: kBool, raw: 1)
165+let FalseV* = Value(kind: kBool, raw: 0)
135166
136167 proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg)
137168
@@ -382,27 +413,27 @@ proc mapEntries*(m: PMap): seq[MEntry] =
382413
383414 # ------------------------------------------------------------ constructors
384415 proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV)
385-proc mkInt*(x: int64): Value = Value(kind: kInt, i: x)
386-proc mkFloat*(x: float64): Value = Value(kind: kFloat, f: x)
387-proc mkStr*(x: string): Value = Value(kind: kStr, s: x)
388-proc mkKeyword*(x: string): Value = Value(kind: kKeyword, s: x)
389-proc mkSymbol*(x: string): Value = Value(kind: kSymbol, s: x)
390-proc mkList*(xs: seq[Value]): Value = Value(kind: kList, xs: xs)
391-proc mkVector*(xs: seq[Value]): Value = Value(kind: kVector, vec: toPVec(xs))
392-proc mkVec*(v: PVec): Value = Value(kind: kVector, vec: v)
393-proc mkMapOf*(m: PMap): Value = Value(kind: kMap, m: m)
394-proc mkSetOf*(m: PMap): Value = Value(kind: kSet, m: m)
416+proc mkInt*(x: int64): Value {.inline.} = Value(kind: kInt, raw: x)
417+proc mkFloat*(x: float64): Value {.inline.} = Value(kind: kFloat, raw: cast[int64](x))
418+proc mkStr*(x: string): Value = Value(kind: kStr, obj: Obj(kind: kStr, s: x))
419+proc mkKeyword*(x: string): Value = Value(kind: kKeyword, obj: Obj(kind: kKeyword, s: x))
420+proc mkSymbol*(x: string): Value = Value(kind: kSymbol, obj: Obj(kind: kSymbol, s: x))
421+proc mkList*(xs: seq[Value]): Value = Value(kind: kList, obj: Obj(kind: kList, xs: xs))
422+proc mkVector*(xs: seq[Value]): Value = Value(kind: kVector, obj: Obj(kind: kVector, vec: toPVec(xs)))
423+proc mkVec*(v: PVec): Value = Value(kind: kVector, obj: Obj(kind: kVector, vec: v))
424+proc mkMapOf*(m: PMap): Value = Value(kind: kMap, obj: Obj(kind: kMap, m: m))
425+proc mkSetOf*(m: PMap): Value = Value(kind: kSet, obj: Obj(kind: kSet, m: m))
395426
396427 proc mkMap*(ps: seq[(Value, Value)]): Value =
397428 var m = emptyPMap()
398429 for (k, v) in ps: m = mapAssoc(m, k, v)
399- Value(kind: kMap, m: m)
430+ mkMapOf(m)
400431
401432 proc mkSet*(xs: seq[Value]): Value =
402433 var m = emptyPMap()
403434 for x in xs:
404435 if not mapContains(m, x): m = mapAssoc(m, x, x)
405- Value(kind: kSet, m: m)
436+ mkSetOf(m)
406437
407438 # openArray overloads, so a builtin can pass its argument list straight through
408439 # without first copying it into a seq.
@@ -411,7 +442,7 @@ proc mkVector*(xs: openArray[Value]): Value = mkVector(@xs)
411442 proc mkSet*(xs: openArray[Value]): Value = mkSet(@xs)
412443
413444 proc mkFn*(name: string, f: proc (args: openArray[Value]): Value {.closure.}): Value =
414- Value(kind: kFn, fn: f, name: name)
445+ Value(kind: kFn, obj: Obj(kind: kFn, fn: f, name: name))
415446
416447 # --------------------------------------------------------------- lazy seqs
417448 ## A lazy seq is a thunk that, when forced, yields either nil/`kNil` (the end)
@@ -421,7 +452,7 @@ proc mkFn*(name: string, f: proc (args: openArray[Value]): Value {.closure.}): V
421452 ## in core, which is what keeps `(nth (iterate inc 0) 1000000)` from blowing the
422453 ## stack.
423454
424-proc mkCons*(h, t: Value): Value = Value(kind: kCons, head: h, tl: t)
455+proc mkCons*(h, t: Value): Value = Value(kind: kCons, obj: Obj(kind: kCons, head: h, tl: t))
425456
426457 const ChunkSize* = 32
427458 ## Elements realized per step by a chunked producer. The same batch size
@@ -431,21 +462,21 @@ const ChunkSize* = 32
431462 proc mkChunk*(xs: sink seq[Value], off: int, rest: Value): Value =
432463 ## Normalizes an exhausted chunk away, so a cursor never sees an empty run.
433464 if off >= xs.len: return (if rest.isNil: NilV else: rest)
434- Value(kind: kChunk, chunk: xs, coff: off, ctl: rest)
465+ Value(kind: kChunk, obj: Obj(kind: kChunk, chunk: xs, coff: off, ctl: rest))
435466
436467 proc mkLazy*(f: proc (): Value {.closure.}): Value =
437- Value(kind: kLazy, thunk: f, cached: nil, forced: false)
468+ Value(kind: kLazy, obj: Obj(kind: kLazy, thunk: f, cached: NilV, forced: false))
438469
439470 proc force*(v: Value): Value =
440471 ## Realize one step: follow a chain of lazy seqs down to a cons, a concrete
441472 ## collection, or the end of the seq.
442473 var cur = v
443- while not cur.isNil and cur.kind == kLazy:
444- if not cur.forced:
445- cur.cached = cur.thunk()
446- cur.forced = true
447- cur.thunk = nil # drop the closure so its captures can be collected
448- cur = cur.cached
474+ while cur.kind == kLazy:
475+ if not cur.obj.forced:
476+ cur.obj.cached = cur.obj.thunk()
477+ cur.obj.forced = true
478+ cur.obj.thunk = nil # drop the closure so its captures can be collected
479+ cur = cur.obj.cached
449480 cur
450481
451482 proc isSeqNode(v: Value): bool =
@@ -471,17 +502,15 @@ proc cursor*(v: Value): Cursor =
471502
472503 proc hasNext*(c: var Cursor): bool =
473504 while true:
474- if not c.run.isNil:
505+ if c.run.kind == kChunk:
475506 if c.idx < c.run.chunk.len: return true
476- c.run = nil
507+ c.run = NilV
477508 c.idx = 0
478509 elif c.idx < c.backing.len:
479510 return true
480- if c.node.isNil: return false
511+ if c.node.kind == kNil: return false
481512 c.node = force(c.node)
482- if c.node.isNil or c.node.kind == kNil:
483- c.node = nil
484- return false
513+ if c.node.kind == kNil: return false
485514 case c.node.kind
486515 of kCons:
487516 return true # the element is c.node.head
@@ -495,10 +524,10 @@ proc hasNext*(c: var Cursor): bool =
495524 # does. Switch to indexing it rather than reporting the seq as finished.
496525 c.backing = toSeq(c.node)
497526 c.idx = 0
498- c.node = nil
527+ c.node = NilV
499528
500529 proc next*(c: var Cursor): Value =
501- if not c.run.isNil:
530+ if c.run.kind == kChunk:
502531 result = c.run.chunk[c.idx]
503532 inc c.idx
504533 elif c.idx < c.backing.len:
@@ -577,7 +606,7 @@ proc hashValue*(v: Value): uint32 =
577606 for e in mapEntries(v.m):
578607 h = h xor mixHash(hashValue(e.key), hashValue(e.val))
579608 h
580- of kFn: uint32(hash(cast[int](cast[pointer](v))))
609+ of kFn: uint32(hash(cast[int](cast[pointer](v.obj))))
581610
582611 # --------------------------------------------------------------- accessors
583612 proc items*(v: Value): seq[Value] =
@@ -620,7 +649,7 @@ proc count*(v: Value): int =
620649 else: err("Don't know how to count: " & prStr(v))
621650
622651 proc truthy*(v: Value): bool =
623- if v == nil: return false
652+ if v.kind == kNil: return false
624653 case v.kind
625654 of kNil: false
626655 of kBool: v.b
@@ -656,7 +685,7 @@ proc equals*(a, b: Value): bool =
656685 true
657686 of kMap:
658687 if a.m.cnt != b.m.cnt: return false
659- let missing = Value(kind: kKeyword, s: "%clonim-missing")
688+ let missing = mkKeyword("%clonim-missing")
660689 for e in mapEntries(a.m):
661690 if not equals(e.val, mapGet(b.m, e.key, missing)): return false
662691 true
@@ -746,7 +775,7 @@ proc hasVar*(name: string): bool =
746775 proc call*(f: Value, args: openArray[Value]): Value =
747776 if f.isNil: err("Can't call nil")
748777 case f.kind
749- of kFn: f.fn(args)
778+ of kFn: f.obj.fn(args)
750779 of kKeyword:
751780 # (:k m) => lookup
752781 if args.len == 0: err("Wrong number of args to keyword")
@@ -56,13 +56,15 @@ type
56 cnt*: int56 cnt*: int
57 nextOrd*: int57 nextOrd*: int
58 58
59- Value* = ref ValueObj59+ Obj* = ref ValueObj
60+ ## Value itself is not a ref: nil, bool, int and float live entirely in the
61+ ## word below, so arithmetic and a numeric sequence never touch the heap. Only
62+ ## the kinds with a real payload allocate a ValueObj, which is also what keeps
63+ ## a lazy seq's memoisation working -- `force` mutates through `obj`, which is
64+ ## still shared by every Value that names it.
60 ValueObj* = object65 ValueObj* = object
61 case kind*: Kind66 case kind*: Kind
62- of kNil: discard67+ of kNil, kBool, kInt, kFloat: discard
63- of kBool: b*: bool
64- of kInt: i*: int64
65- of kFloat: f*: float64
66 of kStr, kKeyword, kSymbol: s*: string68 of kStr, kKeyword, kSymbol: s*: string
67 of kList: xs*: seq[Value]69 of kList: xs*: seq[Value]
68 of kVector: vec*: PVec70 of kVector: vec*: PVec
@@ -84,8 +86,37 @@ type
84 fn*: proc (args: openArray[Value]): Value {.closure.}86 fn*: proc (args: openArray[Value]): Value {.closure.}
85 name*: string87 name*: string
86 88
89+ Value* = object
90+ kind*: Kind
91+ raw*: int64 ## int payload; a float is bit-cast into it; bool is 0/1
92+ obj*: Obj ## nil for the scalar kinds
93+
87 CljError* = object of CatchableError94 CljError* = object of CatchableError
88 95
96+# Field accessors, named so the rest of the codebase keeps reading `v.i`,
97+# `v.xs`, `v.head` exactly as it did when Value was a ref to the variant.
98+proc i*(v: Value): int64 {.inline.} = v.raw
99+proc b*(v: Value): bool {.inline.} = v.raw != 0
100+proc f*(v: Value): float64 {.inline.} = cast[float64](v.raw)
101+proc s*(v: Value): lent string {.inline.} = v.obj.s
102+proc xs*(v: Value): lent seq[Value] {.inline.} = v.obj.xs
103+proc vec*(v: Value): lent PVec {.inline.} = v.obj.vec
104+proc m*(v: Value): lent PMap {.inline.} = v.obj.m
105+proc head*(v: Value): lent Value {.inline.} = v.obj.head
106+proc tl*(v: Value): lent Value {.inline.} = v.obj.tl
107+proc chunk*(v: Value): lent seq[Value] {.inline.} = v.obj.chunk
108+proc coff*(v: Value): int {.inline.} = v.obj.coff
109+proc ctl*(v: Value): lent Value {.inline.} = v.obj.ctl
110+proc cached*(v: Value): lent Value {.inline.} = v.obj.cached
111+proc forced*(v: Value): bool {.inline.} = v.obj.forced
112+proc thunk*(v: Value): auto {.inline.} = v.obj.thunk
113+proc fn*(v: Value): auto {.inline.} = v.obj.fn
114+proc name*(v: Value): lent string {.inline.} = v.obj.name
115+
116+## A Value is never a nil pointer now, but every `v.isNil` in the codebase
117+## meant "is this the nil value", so keep the spelling working.
118+proc isNil*(v: Value): bool {.inline.} = v.kind == kNil
119+
89 # ------------------------------------------------------------ teardown120 # ------------------------------------------------------------ teardown
90 ## A realized lazy seq is a chain of `cons -> lazy -> cons -> …` refs, and ARC121 ## A realized lazy seq is a chain of `cons -> lazy -> cons -> …` refs, and ARC
91 ## frees a chain by recursing into it — a million-element seq means a million122 ## frees a chain by recursing into it — a million-element seq means a million
@@ -130,8 +161,8 @@ proc `=destroy`*(x: var ValueObj) =
130 draining = false161 draining = false
131 162
132 let NilV* = Value(kind: kNil)163 let NilV* = Value(kind: kNil)
133-let TrueV* = Value(kind: kBool, b: true)164+let TrueV* = Value(kind: kBool, raw: 1)
134-let FalseV* = Value(kind: kBool, b: false)165+let FalseV* = Value(kind: kBool, raw: 0)
135 166
136 proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg)167 proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg)
137 168
@@ -382,27 +413,27 @@ proc mapEntries*(m: PMap): seq[MEntry] =
382 413
383 # ------------------------------------------------------------ constructors414 # ------------------------------------------------------------ constructors
384 proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV)415 proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV)
385-proc mkInt*(x: int64): Value = Value(kind: kInt, i: x)416+proc mkInt*(x: int64): Value {.inline.} = Value(kind: kInt, raw: x)
386-proc mkFloat*(x: float64): Value = Value(kind: kFloat, f: x)417+proc mkFloat*(x: float64): Value {.inline.} = Value(kind: kFloat, raw: cast[int64](x))
387-proc mkStr*(x: string): Value = Value(kind: kStr, s: x)418+proc mkStr*(x: string): Value = Value(kind: kStr, obj: Obj(kind: kStr, s: x))
388-proc mkKeyword*(x: string): Value = Value(kind: kKeyword, s: x)419+proc mkKeyword*(x: string): Value = Value(kind: kKeyword, obj: Obj(kind: kKeyword, s: x))
389-proc mkSymbol*(x: string): Value = Value(kind: kSymbol, s: x)420+proc mkSymbol*(x: string): Value = Value(kind: kSymbol, obj: Obj(kind: kSymbol, s: x))
390-proc mkList*(xs: seq[Value]): Value = Value(kind: kList, xs: xs)421+proc mkList*(xs: seq[Value]): Value = Value(kind: kList, obj: Obj(kind: kList, xs: xs))
391-proc mkVector*(xs: seq[Value]): Value = Value(kind: kVector, vec: toPVec(xs))422+proc mkVector*(xs: seq[Value]): Value = Value(kind: kVector, obj: Obj(kind: kVector, vec: toPVec(xs)))
392-proc mkVec*(v: PVec): Value = Value(kind: kVector, vec: v)423+proc mkVec*(v: PVec): Value = Value(kind: kVector, obj: Obj(kind: kVector, vec: v))
393-proc mkMapOf*(m: PMap): Value = Value(kind: kMap, m: m)424+proc mkMapOf*(m: PMap): Value = Value(kind: kMap, obj: Obj(kind: kMap, m: m))
394-proc mkSetOf*(m: PMap): Value = Value(kind: kSet, m: m)425+proc mkSetOf*(m: PMap): Value = Value(kind: kSet, obj: Obj(kind: kSet, m: m))
395 426
396 proc mkMap*(ps: seq[(Value, Value)]): Value =427 proc mkMap*(ps: seq[(Value, Value)]): Value =
397 var m = emptyPMap()428 var m = emptyPMap()
398 for (k, v) in ps: m = mapAssoc(m, k, v)429 for (k, v) in ps: m = mapAssoc(m, k, v)
399- Value(kind: kMap, m: m)430+ mkMapOf(m)
400 431
401 proc mkSet*(xs: seq[Value]): Value =432 proc mkSet*(xs: seq[Value]): Value =
402 var m = emptyPMap()433 var m = emptyPMap()
403 for x in xs:434 for x in xs:
404 if not mapContains(m, x): m = mapAssoc(m, x, x)435 if not mapContains(m, x): m = mapAssoc(m, x, x)
405- Value(kind: kSet, m: m)436+ mkSetOf(m)
406 437
407 # openArray overloads, so a builtin can pass its argument list straight through438 # openArray overloads, so a builtin can pass its argument list straight through
408 # without first copying it into a seq.439 # without first copying it into a seq.
@@ -411,7 +442,7 @@ proc mkVector*(xs: openArray[Value]): Value = mkVector(@xs)
411 proc mkSet*(xs: openArray[Value]): Value = mkSet(@xs)442 proc mkSet*(xs: openArray[Value]): Value = mkSet(@xs)
412 443
413 proc mkFn*(name: string, f: proc (args: openArray[Value]): Value {.closure.}): Value =444 proc mkFn*(name: string, f: proc (args: openArray[Value]): Value {.closure.}): Value =
414- Value(kind: kFn, fn: f, name: name)445+ Value(kind: kFn, obj: Obj(kind: kFn, fn: f, name: name))
415 446
416 # --------------------------------------------------------------- lazy seqs447 # --------------------------------------------------------------- lazy seqs
417 ## A lazy seq is a thunk that, when forced, yields either nil/`kNil` (the end)448 ## A lazy seq is a thunk that, when forced, yields either nil/`kNil` (the end)
@@ -421,7 +452,7 @@ proc mkFn*(name: string, f: proc (args: openArray[Value]): Value {.closure.}): V
421 ## in core, which is what keeps `(nth (iterate inc 0) 1000000)` from blowing the452 ## in core, which is what keeps `(nth (iterate inc 0) 1000000)` from blowing the
422 ## stack.453 ## stack.
423 454
424-proc mkCons*(h, t: Value): Value = Value(kind: kCons, head: h, tl: t)455+proc mkCons*(h, t: Value): Value = Value(kind: kCons, obj: Obj(kind: kCons, head: h, tl: t))
425 456
426 const ChunkSize* = 32457 const ChunkSize* = 32
427 ## Elements realized per step by a chunked producer. The same batch size458 ## Elements realized per step by a chunked producer. The same batch size
@@ -431,21 +462,21 @@ const ChunkSize* = 32
431 proc mkChunk*(xs: sink seq[Value], off: int, rest: Value): Value =462 proc mkChunk*(xs: sink seq[Value], off: int, rest: Value): Value =
432 ## Normalizes an exhausted chunk away, so a cursor never sees an empty run.463 ## Normalizes an exhausted chunk away, so a cursor never sees an empty run.
433 if off >= xs.len: return (if rest.isNil: NilV else: rest)464 if off >= xs.len: return (if rest.isNil: NilV else: rest)
434- Value(kind: kChunk, chunk: xs, coff: off, ctl: rest)465+ Value(kind: kChunk, obj: Obj(kind: kChunk, chunk: xs, coff: off, ctl: rest))
435 466
436 proc mkLazy*(f: proc (): Value {.closure.}): Value =467 proc mkLazy*(f: proc (): Value {.closure.}): Value =
437- Value(kind: kLazy, thunk: f, cached: nil, forced: false)468+ Value(kind: kLazy, obj: Obj(kind: kLazy, thunk: f, cached: NilV, forced: false))
438 469
439 proc force*(v: Value): Value =470 proc force*(v: Value): Value =
440 ## Realize one step: follow a chain of lazy seqs down to a cons, a concrete471 ## Realize one step: follow a chain of lazy seqs down to a cons, a concrete
441 ## collection, or the end of the seq.472 ## collection, or the end of the seq.
442 var cur = v473 var cur = v
443- while not cur.isNil and cur.kind == kLazy:474+ while cur.kind == kLazy:
444- if not cur.forced:475+ if not cur.obj.forced:
445- cur.cached = cur.thunk()476+ cur.obj.cached = cur.obj.thunk()
446- cur.forced = true477+ cur.obj.forced = true
447- cur.thunk = nil # drop the closure so its captures can be collected478+ cur.obj.thunk = nil # drop the closure so its captures can be collected
448- cur = cur.cached479+ cur = cur.obj.cached
449 cur480 cur
450 481
451 proc isSeqNode(v: Value): bool =482 proc isSeqNode(v: Value): bool =
@@ -471,17 +502,15 @@ proc cursor*(v: Value): Cursor =
471 502
472 proc hasNext*(c: var Cursor): bool =503 proc hasNext*(c: var Cursor): bool =
473 while true:504 while true:
474- if not c.run.isNil:505+ if c.run.kind == kChunk:
475 if c.idx < c.run.chunk.len: return true506 if c.idx < c.run.chunk.len: return true
476- c.run = nil507+ c.run = NilV
477 c.idx = 0508 c.idx = 0
478 elif c.idx < c.backing.len:509 elif c.idx < c.backing.len:
479 return true510 return true
480- if c.node.isNil: return false511+ if c.node.kind == kNil: return false
481 c.node = force(c.node)512 c.node = force(c.node)
482- if c.node.isNil or c.node.kind == kNil:513+ if c.node.kind == kNil: return false
483- c.node = nil
484- return false
485 case c.node.kind514 case c.node.kind
486 of kCons:515 of kCons:
487 return true # the element is c.node.head516 return true # the element is c.node.head
@@ -495,10 +524,10 @@ proc hasNext*(c: var Cursor): bool =
495 # does. Switch to indexing it rather than reporting the seq as finished.524 # does. Switch to indexing it rather than reporting the seq as finished.
496 c.backing = toSeq(c.node)525 c.backing = toSeq(c.node)
497 c.idx = 0526 c.idx = 0
498- c.node = nil527+ c.node = NilV
499 528
500 proc next*(c: var Cursor): Value =529 proc next*(c: var Cursor): Value =
501- if not c.run.isNil:530+ if c.run.kind == kChunk:
502 result = c.run.chunk[c.idx]531 result = c.run.chunk[c.idx]
503 inc c.idx532 inc c.idx
504 elif c.idx < c.backing.len:533 elif c.idx < c.backing.len:
@@ -577,7 +606,7 @@ proc hashValue*(v: Value): uint32 =
577 for e in mapEntries(v.m):606 for e in mapEntries(v.m):
578 h = h xor mixHash(hashValue(e.key), hashValue(e.val))607 h = h xor mixHash(hashValue(e.key), hashValue(e.val))
579 h608 h
580- of kFn: uint32(hash(cast[int](cast[pointer](v))))609+ of kFn: uint32(hash(cast[int](cast[pointer](v.obj))))
581 610
582 # --------------------------------------------------------------- accessors611 # --------------------------------------------------------------- accessors
583 proc items*(v: Value): seq[Value] =612 proc items*(v: Value): seq[Value] =
@@ -620,7 +649,7 @@ proc count*(v: Value): int =
620 else: err("Don't know how to count: " & prStr(v))649 else: err("Don't know how to count: " & prStr(v))
621 650
622 proc truthy*(v: Value): bool =651 proc truthy*(v: Value): bool =
623- if v == nil: return false652+ if v.kind == kNil: return false
624 case v.kind653 case v.kind
625 of kNil: false654 of kNil: false
626 of kBool: v.b655 of kBool: v.b
@@ -656,7 +685,7 @@ proc equals*(a, b: Value): bool =
656 true685 true
657 of kMap:686 of kMap:
658 if a.m.cnt != b.m.cnt: return false687 if a.m.cnt != b.m.cnt: return false
659- let missing = Value(kind: kKeyword, s: "%clonim-missing")688+ let missing = mkKeyword("%clonim-missing")
660 for e in mapEntries(a.m):689 for e in mapEntries(a.m):
661 if not equals(e.val, mapGet(b.m, e.key, missing)): return false690 if not equals(e.val, mapGet(b.m, e.key, missing)): return false
662 true691 true
@@ -746,7 +775,7 @@ proc hasVar*(name: string): bool =
746 proc call*(f: Value, args: openArray[Value]): Value =775 proc call*(f: Value, args: openArray[Value]): Value =
747 if f.isNil: err("Can't call nil")776 if f.isNil: err("Can't call nil")
748 case f.kind777 case f.kind
749- of kFn: f.fn(args)778+ of kFn: f.obj.fn(args)
750 of kKeyword:779 of kKeyword:
751 # (:k m) => lookup780 # (:k m) => lookup
752 if args.len == 0: err("Wrong number of args to keyword")781 if args.len == 0: err("Wrong number of args to keyword")