nandi/clonimpublic Fork
⑂ f880f22
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

release: ship a private static runtime

nandithebull committed 2026-09-20T21:36:26-07:00 Browse files
f880f22 parent: ee111f4
added .github/workflows/release.yml +105 -0
new file mode 100644
@@ -0,0 +1,105 @@
1+name: Release
2+
3+on:
4+ push:
5+ tags:
6+ - "v*"
7+ workflow_dispatch:
8+ inputs:
9+ tag:
10+ description: Existing version tag to publish
11+ required: true
12+ type: string
13+
14+permissions:
15+ contents: write
16+
17+concurrency:
18+ group: release-${{ github.ref }}
19+ cancel-in-progress: false
20+
21+jobs:
22+ release:
23+ name: Build and publish Linux x86_64
24+ runs-on: ubuntu-24.04
25+
26+ steps:
27+ - name: Resolve release tag
28+ id: release
29+ env:
30+ EVENT_NAME: ${{ github.event_name }}
31+ INPUT_TAG: ${{ inputs.tag }}
32+ REF_NAME: ${{ github.ref_name }}
33+ run: |
34+ if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
35+ tag="$INPUT_TAG"
36+ else
37+ tag="$REF_NAME"
38+ fi
39+ if [[ ! "$tag" =~ ^v[0-9] ]]; then
40+ echo "Release tags must start with a version such as v0.1.0" >&2
41+ exit 1
42+ fi
43+ echo "tag=$tag" >> "$GITHUB_OUTPUT"
44+
45+ - name: Check out release tag
46+ uses: actions/checkout@v4
47+ with:
48+ ref: ${{ steps.release.outputs.tag }}
49+
50+ - name: Install Nim
51+ uses: jiro4989/setup-nim-action@v2
52+ with:
53+ nim-version: "2.2.12"
54+ repo-token: ${{ secrets.GITHUB_TOKEN }}
55+
56+ - name: Run test suite
57+ run: ./run-tests.sh
58+
59+ - name: Build optimized compiler
60+ run: |
61+ mkdir -p lib
62+ nim c --app:staticlib --nimMainPrefix:ClonimRuntime \
63+ --hints:off --warnings:off --nimcache:lib/nimcache-debug \
64+ -o:lib/libclonim_runtime_debug.a src/runtime_lib.nim
65+ nim c -d:release --app:staticlib --nimMainPrefix:ClonimRuntime \
66+ --hints:off --warnings:off --nimcache:lib/nimcache-release \
67+ -o:lib/libclonim_runtime.a src/runtime_lib.nim
68+ nim c -d:release --hints:off --warnings:off \
69+ -o:bin/clonim src/clonim.nim
70+
71+ - name: Package release
72+ env:
73+ ARCHIVE_ROOT: clonim-linux-x86_64
74+ run: |
75+ install -Dm755 bin/clonim "dist/$ARCHIVE_ROOT/bin/clonim"
76+ install -Dm644 lib/libclonim_runtime.a \
77+ "dist/$ARCHIVE_ROOT/lib/libclonim_runtime.a"
78+ install -Dm644 lib/libclonim_runtime_debug.a \
79+ "dist/$ARCHIVE_ROOT/lib/libclonim_runtime_debug.a"
80+ install -Dm644 src/app_runtime.nim \
81+ "dist/$ARCHIVE_ROOT/src/app_runtime.nim"
82+ install -Dm644 src/runtime_types.nim \
83+ "dist/$ARCHIVE_ROOT/src/runtime_types.nim"
84+ cp -R stdlib "dist/$ARCHIVE_ROOT/stdlib"
85+ cp README.md "dist/$ARCHIVE_ROOT/README.md"
86+ tar -C dist -czf "dist/$ARCHIVE_ROOT.tar.gz" "$ARCHIVE_ROOT"
87+ cd dist
88+ sha256sum "$ARCHIVE_ROOT.tar.gz" > "$ARCHIVE_ROOT.tar.gz.sha256"
89+
90+ - name: Publish GitHub release
91+ env:
92+ GH_TOKEN: ${{ github.token }}
93+ TAG: ${{ steps.release.outputs.tag }}
94+ run: |
95+ assets=(
96+ dist/clonim-linux-x86_64.tar.gz
97+ dist/clonim-linux-x86_64.tar.gz.sha256
98+ )
99+ if gh release view "$TAG" >/dev/null 2>&1; then
100+ gh release upload "$TAG" "${assets[@]}" --clobber
101+ else
102+ gh release create "$TAG" "${assets[@]}" \
103+ --verify-tag \
104+ --generate-notes \
105+ --title "$TAG"
new file mode 100644
@@ -0,0 +1,105 @@
1+name: Release
2+
3+on:
4+ push:
5+ tags:
6+ - "v*"
7+ workflow_dispatch:
8+ inputs:
9+ tag:
10+ description: Existing version tag to publish
11+ required: true
12+ type: string
13+
14+permissions:
15+ contents: write
16+
17+concurrency:
18+ group: release-${{ github.ref }}
19+ cancel-in-progress: false
20+
21+jobs:
22+ release:
23+ name: Build and publish Linux x86_64
24+ runs-on: ubuntu-24.04
25+
26+ steps:
27+ - name: Resolve release tag
28+ id: release
29+ env:
30+ EVENT_NAME: ${{ github.event_name }}
31+ INPUT_TAG: ${{ inputs.tag }}
32+ REF_NAME: ${{ github.ref_name }}
33+ run: |
34+ if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
35+ tag="$INPUT_TAG"
36+ else
37+ tag="$REF_NAME"
38+ fi
39+ if [[ ! "$tag" =~ ^v[0-9] ]]; then
40+ echo "Release tags must start with a version such as v0.1.0" >&2
41+ exit 1
42+ fi
43+ echo "tag=$tag" >> "$GITHUB_OUTPUT"
44+
45+ - name: Check out release tag
46+ uses: actions/checkout@v4
47+ with:
48+ ref: ${{ steps.release.outputs.tag }}
49+
50+ - name: Install Nim
51+ uses: jiro4989/setup-nim-action@v2
52+ with:
53+ nim-version: "2.2.12"
54+ repo-token: ${{ secrets.GITHUB_TOKEN }}
55+
56+ - name: Run test suite
57+ run: ./run-tests.sh
58+
59+ - name: Build optimized compiler
60+ run: |
61+ mkdir -p lib
62+ nim c --app:staticlib --nimMainPrefix:ClonimRuntime \
63+ --hints:off --warnings:off --nimcache:lib/nimcache-debug \
64+ -o:lib/libclonim_runtime_debug.a src/runtime_lib.nim
65+ nim c -d:release --app:staticlib --nimMainPrefix:ClonimRuntime \
66+ --hints:off --warnings:off --nimcache:lib/nimcache-release \
67+ -o:lib/libclonim_runtime.a src/runtime_lib.nim
68+ nim c -d:release --hints:off --warnings:off \
69+ -o:bin/clonim src/clonim.nim
70+
71+ - name: Package release
72+ env:
73+ ARCHIVE_ROOT: clonim-linux-x86_64
74+ run: |
75+ install -Dm755 bin/clonim "dist/$ARCHIVE_ROOT/bin/clonim"
76+ install -Dm644 lib/libclonim_runtime.a \
77+ "dist/$ARCHIVE_ROOT/lib/libclonim_runtime.a"
78+ install -Dm644 lib/libclonim_runtime_debug.a \
79+ "dist/$ARCHIVE_ROOT/lib/libclonim_runtime_debug.a"
80+ install -Dm644 src/app_runtime.nim \
81+ "dist/$ARCHIVE_ROOT/src/app_runtime.nim"
82+ install -Dm644 src/runtime_types.nim \
83+ "dist/$ARCHIVE_ROOT/src/runtime_types.nim"
84+ cp -R stdlib "dist/$ARCHIVE_ROOT/stdlib"
85+ cp README.md "dist/$ARCHIVE_ROOT/README.md"
86+ tar -C dist -czf "dist/$ARCHIVE_ROOT.tar.gz" "$ARCHIVE_ROOT"
87+ cd dist
88+ sha256sum "$ARCHIVE_ROOT.tar.gz" > "$ARCHIVE_ROOT.tar.gz.sha256"
89+
90+ - name: Publish GitHub release
91+ env:
92+ GH_TOKEN: ${{ github.token }}
93+ TAG: ${{ steps.release.outputs.tag }}
94+ run: |
95+ assets=(
96+ dist/clonim-linux-x86_64.tar.gz
97+ dist/clonim-linux-x86_64.tar.gz.sha256
98+ )
99+ if gh release view "$TAG" >/dev/null 2>&1; then
100+ gh release upload "$TAG" "${assets[@]}" --clobber
101+ else
102+ gh release create "$TAG" "${assets[@]}" \
103+ --verify-tag \
104+ --generate-notes \
105+ --title "$TAG"
modified .gitignore +1 -0
@@ -1,4 +1,5 @@
11 bin/
2+lib/
23 nimcache/
34 .clj-kondo/.cache/
45 .lsp/
@@ -1,4 +1,5 @@
1 bin/1 bin/
2+lib/
2 nimcache/3 nimcache/
3 .clj-kondo/.cache/4 .clj-kondo/.cache/
4 .lsp/5 .lsp/
modified README.md +9 -1
@@ -23,6 +23,14 @@ and flags slowdowns above 10% versus the latest compatible run. Requires Jolt
2323 and a clean working tree. Use `just bench-store 15` to change the threshold;
2424 see [benchmark history](bench/README.md) for metrics and exit codes.
2525
26+`just release` also builds clonim's private static runtime archives. Generated
27+programs link the matching debug or release archive, so their finished binaries
28+are standalone. Version tags matching `v*` trigger the release workflow, which
29+runs the test suite and publishes a Linux x86_64 bundle plus its SHA-256 file to
30+GitHub Releases. The bundle contains the compiler, both internal runtime
31+archives, their small Nim interface, and the clonim source-level standard
32+library; users still need Nim installed as the native-code backend.
33+
2634 Source-level libraries are loaded explicitly. Use
2735 `(require '[clonim.core :refer [now-ms]])` to load `stdlib/clonim/core.clj`. Host-dependent operations remain small runtime primitives; for
2836 example, the stdlib `now-ms` function wraps the `*epoch-time-ms*` primitive.
@@ -62,7 +70,7 @@ Missing dependencies, missing vars, and dependency cycles produce errors.
6270 |---|---|---|
6371 | reader | `src/reader.nim` | text → data. Forms *are* runtime values (homoiconic), as in Clojure |
6472 | analyzer + codegen | `src/compiler.nim` | expands the macro set to core special forms, emits Nim statements |
65-| runtime | `src/runtime.nim` | the `Value` tagged union, the persistent vector/map, equality, printing, var cells, `call` |
73+| runtime | `src/runtime.nim` | the `Value` tagged union, the persistent vector/map, equality, printing, var cells, `call`; precompiled into a private static archive |
6674 | core | `src/core.nim` | ~140 `clojure.core` builtins as Nim closures |
6775 | stdlib | `stdlib/clonim/core.clj` | source-level helpers loaded by explicit require |
6876 | driver | `src/clonim.nim` | shells out to `nim c`, times each phase |
@@ -23,6 +23,14 @@ and flags slowdowns above 10% versus the latest compatible run. Requires Jolt
23 and a clean working tree. Use `just bench-store 15` to change the threshold;23 and a clean working tree. Use `just bench-store 15` to change the threshold;
24 see [benchmark history](bench/README.md) for metrics and exit codes.24 see [benchmark history](bench/README.md) for metrics and exit codes.
25 25
26+`just release` also builds clonim's private static runtime archives. Generated
27+programs link the matching debug or release archive, so their finished binaries
28+are standalone. Version tags matching `v*` trigger the release workflow, which
29+runs the test suite and publishes a Linux x86_64 bundle plus its SHA-256 file to
30+GitHub Releases. The bundle contains the compiler, both internal runtime
31+archives, their small Nim interface, and the clonim source-level standard
32+library; users still need Nim installed as the native-code backend.
33+
26 Source-level libraries are loaded explicitly. Use34 Source-level libraries are loaded explicitly. Use
27 `(require '[clonim.core :refer [now-ms]])` to load `stdlib/clonim/core.clj`. Host-dependent operations remain small runtime primitives; for35 `(require '[clonim.core :refer [now-ms]])` to load `stdlib/clonim/core.clj`. Host-dependent operations remain small runtime primitives; for
28 example, the stdlib `now-ms` function wraps the `*epoch-time-ms*` primitive.36 example, the stdlib `now-ms` function wraps the `*epoch-time-ms*` primitive.
@@ -62,7 +70,7 @@ Missing dependencies, missing vars, and dependency cycles produce errors.
62 |---|---|---|70 |---|---|---|
63 | reader | `src/reader.nim` | text → data. Forms *are* runtime values (homoiconic), as in Clojure |71 | reader | `src/reader.nim` | text → data. Forms *are* runtime values (homoiconic), as in Clojure |
64 | analyzer + codegen | `src/compiler.nim` | expands the macro set to core special forms, emits Nim statements |72 | analyzer + codegen | `src/compiler.nim` | expands the macro set to core special forms, emits Nim statements |
65-| runtime | `src/runtime.nim` | the `Value` tagged union, the persistent vector/map, equality, printing, var cells, `call` |73+| runtime | `src/runtime.nim` | the `Value` tagged union, the persistent vector/map, equality, printing, var cells, `call`; precompiled into a private static archive |
66 | core | `src/core.nim` | ~140 `clojure.core` builtins as Nim closures |74 | core | `src/core.nim` | ~140 `clojure.core` builtins as Nim closures |
67 | stdlib | `stdlib/clonim/core.clj` | source-level helpers loaded by explicit require |75 | stdlib | `stdlib/clonim/core.clj` | source-level helpers loaded by explicit require |
68 | driver | `src/clonim.nim` | shells out to `nim c`, times each phase |76 | driver | `src/clonim.nim` | shells out to `nim c`, times each phase |
modified justfile +7 -1
@@ -6,8 +6,14 @@ bin := "bin/clonim"
66 build:
77 nim c --hints:off --warnings:off -o:{{bin}} src/clonim.nim
88
9+# Build the private static runtime in both modes used by the driver.
10+runtime-libs:
11+ mkdir -p lib
12+ nim c --app:staticlib --nimMainPrefix:ClonimRuntime --hints:off --warnings:off --nimcache:lib/nimcache-debug -o:lib/libclonim_runtime_debug.a src/runtime_lib.nim
13+ nim c -d:release --app:staticlib --nimMainPrefix:ClonimRuntime --hints:off --warnings:off --nimcache:lib/nimcache-release -o:lib/libclonim_runtime.a src/runtime_lib.nim
14+
915 # Build with optimisations on (compiler and, via -d:release, the programs it emits)
10-release:
16+release: runtime-libs
1117 nim c -d:release --hints:off --warnings:off -o:{{bin}} src/clonim.nim
1218
1319 # Run every example against tests/<name>.expected
@@ -6,8 +6,14 @@ bin := "bin/clonim"
6 build:6 build:
7 nim c --hints:off --warnings:off -o:{{bin}} src/clonim.nim7 nim c --hints:off --warnings:off -o:{{bin}} src/clonim.nim
8 8
9+# Build the private static runtime in both modes used by the driver.
10+runtime-libs:
11+ mkdir -p lib
12+ nim c --app:staticlib --nimMainPrefix:ClonimRuntime --hints:off --warnings:off --nimcache:lib/nimcache-debug -o:lib/libclonim_runtime_debug.a src/runtime_lib.nim
13+ nim c -d:release --app:staticlib --nimMainPrefix:ClonimRuntime --hints:off --warnings:off --nimcache:lib/nimcache-release -o:lib/libclonim_runtime.a src/runtime_lib.nim
14+
9 # Build with optimisations on (compiler and, via -d:release, the programs it emits)15 # Build with optimisations on (compiler and, via -d:release, the programs it emits)
10-release:16+release: runtime-libs
11 nim c -d:release --hints:off --warnings:off -o:{{bin}} src/clonim.nim17 nim c -d:release --hints:off --warnings:off -o:{{bin}} src/clonim.nim
12 18
13 # Run every example against tests/<name>.expected19 # Run every example against tests/<name>.expected
added src/app_runtime.nim +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+## Private interface to the precompiled clonim runtime. Generated programs
2+## import this module; implementations live in libclonim_runtime.a.
3+import runtime_types
4+export runtime_types
5+
6+proc mkFn*(name: string,
7+ f: proc (args: openArray[Value]): Value {.closure.}): Value
8+ {.cdecl, importc: "clonim_mk_fn".}
9+
10+{.push cdecl.}
11+proc initClonimRuntime*() {.importc: "ClonimRuntimeNimMain".}
12+proc registerCore*() {.importc: "clonim_register_core".}
13+proc registerNamespaceCore*() {.importc: "clonim_register_namespace_core".}
14+
15+proc err*(msg: string) {.noreturn, importc: "clonim_err".}
16+proc varCell*(name: string): VarCell {.importc: "clonim_var_cell".}
17+proc setVar*(name: string, v: Value): Value {.discardable, importc: "clonim_set_var".}
18+proc cellGet*(c: VarCell): Value {.importc: "clonim_cell_get".}
19+proc cellIs*(c: VarCell, v: Value): bool {.importc: "clonim_cell_is".}
20+proc getVar*(name: string): Value {.importc: "clonim_get_var".}
21+proc call*(f: Value, args: openArray[Value]): Value {.importc: "clonim_call".}
22+proc argAt*(args: openArray[Value], i: int): Value {.importc: "clonim_arg_at".}
23+proc restArgs*(args: openArray[Value], i: int): Value {.importc: "clonim_rest_args".}
24+proc seqDrop*(v: Value, n: int): Value {.importc: "clonim_seq_drop".}
25+proc toSeq*(v: Value): seq[Value] {.importc: "clonim_to_seq".}
26+proc truthy*(v: Value): bool {.importc: "clonim_truthy".}
27+
28+iterator elems*(v: Value): Value =
29+ for x in toSeq(v): yield x
30+
31+proc mkInt*(x: int64): Value {.importc: "clonim_mk_int".}
32+proc mkFloat*(x: float64): Value {.importc: "clonim_mk_float".}
33+proc mkStr*(x: string): Value {.importc: "clonim_mk_str".}
34+proc mkKeyword*(x: string): Value {.importc: "clonim_mk_keyword".}
35+proc mkSymbol*(x: string): Value {.importc: "clonim_mk_symbol".}
36+proc mkList*(xs: openArray[Value]): Value {.importc: "clonim_mk_list".}
37+proc mkVector*(xs: openArray[Value]): Value {.importc: "clonim_mk_vector".}
38+proc mkSet*(xs: openArray[Value]): Value {.importc: "clonim_mk_set".}
39+proc mkMap*(ps: seq[(Value, Value)]): Value {.importc: "clonim_mk_map".}
40+
41+proc idiv*(a, b: int64): int64 {.importc: "clonim_idiv".}
42+proc irem*(a, b: int64): int64 {.importc: "clonim_irem".}
43+proc add2*(a, b: Value): Value {.importc: "clonim_add2".}
44+proc sub2*(a, b: Value): Value {.importc: "clonim_sub2".}
45+proc mul2*(a, b: Value): Value {.importc: "clonim_mul2".}
46+proc lt2*(a, b: Value): Value {.importc: "clonim_lt2".}
47+proc gt2*(a, b: Value): Value {.importc: "clonim_gt2".}
48+proc le2*(a, b: Value): Value {.importc: "clonim_le2".}
49+proc ge2*(a, b: Value): Value {.importc: "clonim_ge2".}
50+proc eq2*(a, b: Value): Value {.importc: "clonim_eq2".}
51+proc ne2*(a, b: Value): Value {.importc: "clonim_ne2".}
52+proc inc1*(a: Value): Value {.importc: "clonim_inc1".}
53+proc dec1*(a: Value): Value {.importc: "clonim_dec1".}
54+proc fusedReduce*(f, init: Value, hasInit: bool, base: Value,
55+ ops: openArray[FusedOp]): Value {.importc: "clonim_fused_reduce".}
56+proc fusedCount*(base: Value, ops: openArray[FusedOp]): Value
57+ {.importc: "clonim_fused_count".}
58+{.pop.}
new file mode 100644
@@ -0,0 +1,58 @@
1+## Private interface to the precompiled clonim runtime. Generated programs
2+## import this module; implementations live in libclonim_runtime.a.
3+import runtime_types
4+export runtime_types
5+
6+proc mkFn*(name: string,
7+ f: proc (args: openArray[Value]): Value {.closure.}): Value
8+ {.cdecl, importc: "clonim_mk_fn".}
9+
10+{.push cdecl.}
11+proc initClonimRuntime*() {.importc: "ClonimRuntimeNimMain".}
12+proc registerCore*() {.importc: "clonim_register_core".}
13+proc registerNamespaceCore*() {.importc: "clonim_register_namespace_core".}
14+
15+proc err*(msg: string) {.noreturn, importc: "clonim_err".}
16+proc varCell*(name: string): VarCell {.importc: "clonim_var_cell".}
17+proc setVar*(name: string, v: Value): Value {.discardable, importc: "clonim_set_var".}
18+proc cellGet*(c: VarCell): Value {.importc: "clonim_cell_get".}
19+proc cellIs*(c: VarCell, v: Value): bool {.importc: "clonim_cell_is".}
20+proc getVar*(name: string): Value {.importc: "clonim_get_var".}
21+proc call*(f: Value, args: openArray[Value]): Value {.importc: "clonim_call".}
22+proc argAt*(args: openArray[Value], i: int): Value {.importc: "clonim_arg_at".}
23+proc restArgs*(args: openArray[Value], i: int): Value {.importc: "clonim_rest_args".}
24+proc seqDrop*(v: Value, n: int): Value {.importc: "clonim_seq_drop".}
25+proc toSeq*(v: Value): seq[Value] {.importc: "clonim_to_seq".}
26+proc truthy*(v: Value): bool {.importc: "clonim_truthy".}
27+
28+iterator elems*(v: Value): Value =
29+ for x in toSeq(v): yield x
30+
31+proc mkInt*(x: int64): Value {.importc: "clonim_mk_int".}
32+proc mkFloat*(x: float64): Value {.importc: "clonim_mk_float".}
33+proc mkStr*(x: string): Value {.importc: "clonim_mk_str".}
34+proc mkKeyword*(x: string): Value {.importc: "clonim_mk_keyword".}
35+proc mkSymbol*(x: string): Value {.importc: "clonim_mk_symbol".}
36+proc mkList*(xs: openArray[Value]): Value {.importc: "clonim_mk_list".}
37+proc mkVector*(xs: openArray[Value]): Value {.importc: "clonim_mk_vector".}
38+proc mkSet*(xs: openArray[Value]): Value {.importc: "clonim_mk_set".}
39+proc mkMap*(ps: seq[(Value, Value)]): Value {.importc: "clonim_mk_map".}
40+
41+proc idiv*(a, b: int64): int64 {.importc: "clonim_idiv".}
42+proc irem*(a, b: int64): int64 {.importc: "clonim_irem".}
43+proc add2*(a, b: Value): Value {.importc: "clonim_add2".}
44+proc sub2*(a, b: Value): Value {.importc: "clonim_sub2".}
45+proc mul2*(a, b: Value): Value {.importc: "clonim_mul2".}
46+proc lt2*(a, b: Value): Value {.importc: "clonim_lt2".}
47+proc gt2*(a, b: Value): Value {.importc: "clonim_gt2".}
48+proc le2*(a, b: Value): Value {.importc: "clonim_le2".}
49+proc ge2*(a, b: Value): Value {.importc: "clonim_ge2".}
50+proc eq2*(a, b: Value): Value {.importc: "clonim_eq2".}
51+proc ne2*(a, b: Value): Value {.importc: "clonim_ne2".}
52+proc inc1*(a: Value): Value {.importc: "clonim_inc1".}
53+proc dec1*(a: Value): Value {.importc: "clonim_dec1".}
54+proc fusedReduce*(f, init: Value, hasInit: bool, base: Value,
55+ ops: openArray[FusedOp]): Value {.importc: "clonim_fused_reduce".}
56+proc fusedCount*(base: Value, ops: openArray[FusedOp]): Value
57+ {.importc: "clonim_fused_count".}
58+{.pop.}
modified src/clonim.nim +46 -9
@@ -3,7 +3,7 @@
33 ## clonim run foo.clj compile and run
44 ## clonim build foo.clj [-o bin] compile to a native binary
55 ## clonim emit foo.clj print the generated Nim
6-import std/[hashes, os, osproc, strutils, times]
6+import std/[hashes, os, osproc, sequtils, strutils, times]
77 import runtime, compiler
88
99 proc usage() =
@@ -22,14 +22,43 @@ options:
2222 quit(1)
2323
2424 proc srcDir(): string =
25- ## where runtime.nim / core.nim live, so generated code can import them
25+ ## Where generated programs find the private runtime interface.
2626 for cand in [getAppDir(), getAppDir().parentDir / "src",
2727 getAppDir().parentDir.parentDir / "src"]:
28- if fileExists(cand / "runtime.nim"): return cand
28+ if fileExists(cand / "app_runtime.nim"): return cand
2929 getAppDir()
3030
31+proc runtimeLib(release: bool): string =
32+ ## Release archives place the private runtime beside bin/. A source checkout
33+ ## builds the same archive into lib/ on first use.
34+ let libName = (if release: "libclonim_runtime.a" else: "libclonim_runtime_debug.a")
35+ let installed = getAppDir().parentDir / "lib" / libName
36+ let source = srcDir() / "runtime_lib.nim"
37+ if fileExists(installed):
38+ if not fileExists(source): return installed
39+ var stale = false
40+ for f in walkFiles(srcDir() / "*.nim"):
41+ if getLastModificationTime(f) > getLastModificationTime(installed):
42+ stale = true
43+ break
44+ if not stale: return installed
45+ if not fileExists(source):
46+ raise newException(IOError, "missing private runtime library: " & installed)
47+ createDir(installed.parentDir)
48+ var args = @["nim", "c", "--app:staticlib", "--nimMainPrefix:ClonimRuntime",
49+ "--hints:off", "--warnings:off", "--path:" & srcDir(),
50+ "--nimcache:" & installed.parentDir /
51+ (if release: "nimcache-release" else: "nimcache-debug"),
52+ "-o:" & installed]
53+ if release: args.add "-d:release"
54+ args.add source
55+ let (output, code) = execCmdEx(args.mapIt(quoteShell(it)).join(" "))
56+ if code != 0:
57+ raise newException(IOError, "failed to build private runtime library\n" & output)
58+ installed
59+
3160
32-proc buildKey(nimSrc: string, release: bool): string =
61+proc buildKey(nimSrc: string, release: bool, rtLib: string): string =
3362 ## Identifies everything the produced binary depends on. Any change here
3463 ## invalidates the cached binary for a source file.
3564 var h: Hash = hash(nimSrc) !& hash(release)
@@ -39,6 +68,7 @@ proc buildKey(nimSrc: string, release: bool): string =
3968 let nimExe = findExe("nim")
4069 if nimExe.len > 0:
4170 h = h !& hash($getLastModificationTime(nimExe))
71+ h = h !& hash($getLastModificationTime(rtLib))
4272 $(!$h)
4373
4474 proc main() =
@@ -111,15 +141,22 @@ proc main() =
111141
112142 # Even a warm nimcache costs a second or so of semantic checking and linking.
113143 # `run` skips the backend entirely when nothing that feeds the binary has
114- # changed: the generated Nim, the runtime/core sources it imports, the build
115- # flags, and the Nim compiler itself.
144+ # changed: the generated Nim, private runtime archive/interface, build flags,
145+ # and the Nim compiler itself.
116146 let stamp = work / "stamp"
147+ var rtLib = ""
148+ try:
149+ rtLib = runtimeLib(release)
150+ except IOError, OSError:
151+ stderr.writeLine("clonim: " & getCurrentExceptionMsg())
152+ quit(1)
117153 let cached = cmd == "run" and fileExists(outBin) and fileExists(stamp) and
118- readFile(stamp) == buildKey(nimSrc, release)
154+ readFile(stamp) == buildKey(nimSrc, release, rtLib)
119155
120156 var nimCmd = @["nim", "c", "--hints:off", "--warnings:off",
121157 "--path:" & srcDir(), "--nimcache:" & (work / "cache"),
122- "-o:" & outBin]
158+ "--passL:-Wl,--allow-multiple-definition", "--passL:" & rtLib,
159+ "--passL:-lm", "-o:" & outBin]
123160 if release: nimCmd.add "-d:release"
124161 nimCmd.add nimFile
125162 if verbose and not cached: echo "clonim: " & nimCmd.join(" ")
@@ -129,7 +166,7 @@ proc main() =
129166 var code = 0
130167 if not cached:
131168 (output, code) = execCmdEx(nimCmd.join(" "))
132- if code == 0: writeFile(stamp, buildKey(nimSrc, release))
169+ if code == 0: writeFile(stamp, buildKey(nimSrc, release, rtLib))
133170 let tBuild = epochTime() - t1
134171 if code != 0:
135172 stderr.writeLine("clonim: Nim backend failed\n" & output)
@@ -3,7 +3,7 @@
3 ## clonim run foo.clj compile and run3 ## clonim run foo.clj compile and run
4 ## clonim build foo.clj [-o bin] compile to a native binary4 ## clonim build foo.clj [-o bin] compile to a native binary
5 ## clonim emit foo.clj print the generated Nim5 ## clonim emit foo.clj print the generated Nim
6-import std/[hashes, os, osproc, strutils, times]6+import std/[hashes, os, osproc, sequtils, strutils, times]
7 import runtime, compiler7 import runtime, compiler
8 8
9 proc usage() =9 proc usage() =
@@ -22,14 +22,43 @@ options:
22 quit(1)22 quit(1)
23 23
24 proc srcDir(): string =24 proc srcDir(): string =
25- ## where runtime.nim / core.nim live, so generated code can import them25+ ## Where generated programs find the private runtime interface.
26 for cand in [getAppDir(), getAppDir().parentDir / "src",26 for cand in [getAppDir(), getAppDir().parentDir / "src",
27 getAppDir().parentDir.parentDir / "src"]:27 getAppDir().parentDir.parentDir / "src"]:
28- if fileExists(cand / "runtime.nim"): return cand28+ if fileExists(cand / "app_runtime.nim"): return cand
29 getAppDir()29 getAppDir()
30 30
31+proc runtimeLib(release: bool): string =
32+ ## Release archives place the private runtime beside bin/. A source checkout
33+ ## builds the same archive into lib/ on first use.
34+ let libName = (if release: "libclonim_runtime.a" else: "libclonim_runtime_debug.a")
35+ let installed = getAppDir().parentDir / "lib" / libName
36+ let source = srcDir() / "runtime_lib.nim"
37+ if fileExists(installed):
38+ if not fileExists(source): return installed
39+ var stale = false
40+ for f in walkFiles(srcDir() / "*.nim"):
41+ if getLastModificationTime(f) > getLastModificationTime(installed):
42+ stale = true
43+ break
44+ if not stale: return installed
45+ if not fileExists(source):
46+ raise newException(IOError, "missing private runtime library: " & installed)
47+ createDir(installed.parentDir)
48+ var args = @["nim", "c", "--app:staticlib", "--nimMainPrefix:ClonimRuntime",
49+ "--hints:off", "--warnings:off", "--path:" & srcDir(),
50+ "--nimcache:" & installed.parentDir /
51+ (if release: "nimcache-release" else: "nimcache-debug"),
52+ "-o:" & installed]
53+ if release: args.add "-d:release"
54+ args.add source
55+ let (output, code) = execCmdEx(args.mapIt(quoteShell(it)).join(" "))
56+ if code != 0:
57+ raise newException(IOError, "failed to build private runtime library\n" & output)
58+ installed
59+
31 60
32-proc buildKey(nimSrc: string, release: bool): string =61+proc buildKey(nimSrc: string, release: bool, rtLib: string): string =
33 ## Identifies everything the produced binary depends on. Any change here62 ## Identifies everything the produced binary depends on. Any change here
34 ## invalidates the cached binary for a source file.63 ## invalidates the cached binary for a source file.
35 var h: Hash = hash(nimSrc) !& hash(release)64 var h: Hash = hash(nimSrc) !& hash(release)
@@ -39,6 +68,7 @@ proc buildKey(nimSrc: string, release: bool): string =
39 let nimExe = findExe("nim")68 let nimExe = findExe("nim")
40 if nimExe.len > 0:69 if nimExe.len > 0:
41 h = h !& hash($getLastModificationTime(nimExe))70 h = h !& hash($getLastModificationTime(nimExe))
71+ h = h !& hash($getLastModificationTime(rtLib))
42 $(!$h)72 $(!$h)
43 73
44 proc main() =74 proc main() =
@@ -111,15 +141,22 @@ proc main() =
111 141
112 # Even a warm nimcache costs a second or so of semantic checking and linking.142 # Even a warm nimcache costs a second or so of semantic checking and linking.
113 # `run` skips the backend entirely when nothing that feeds the binary has143 # `run` skips the backend entirely when nothing that feeds the binary has
114- # changed: the generated Nim, the runtime/core sources it imports, the build144+ # changed: the generated Nim, private runtime archive/interface, build flags,
115- # flags, and the Nim compiler itself.145+ # and the Nim compiler itself.
116 let stamp = work / "stamp"146 let stamp = work / "stamp"
147+ var rtLib = ""
148+ try:
149+ rtLib = runtimeLib(release)
150+ except IOError, OSError:
151+ stderr.writeLine("clonim: " & getCurrentExceptionMsg())
152+ quit(1)
117 let cached = cmd == "run" and fileExists(outBin) and fileExists(stamp) and153 let cached = cmd == "run" and fileExists(outBin) and fileExists(stamp) and
118- readFile(stamp) == buildKey(nimSrc, release)154+ readFile(stamp) == buildKey(nimSrc, release, rtLib)
119 155
120 var nimCmd = @["nim", "c", "--hints:off", "--warnings:off",156 var nimCmd = @["nim", "c", "--hints:off", "--warnings:off",
121 "--path:" & srcDir(), "--nimcache:" & (work / "cache"),157 "--path:" & srcDir(), "--nimcache:" & (work / "cache"),
122- "-o:" & outBin]158+ "--passL:-Wl,--allow-multiple-definition", "--passL:" & rtLib,
159+ "--passL:-lm", "-o:" & outBin]
123 if release: nimCmd.add "-d:release"160 if release: nimCmd.add "-d:release"
124 nimCmd.add nimFile161 nimCmd.add nimFile
125 if verbose and not cached: echo "clonim: " & nimCmd.join(" ")162 if verbose and not cached: echo "clonim: " & nimCmd.join(" ")
@@ -129,7 +166,7 @@ proc main() =
129 var code = 0166 var code = 0
130 if not cached:167 if not cached:
131 (output, code) = execCmdEx(nimCmd.join(" "))168 (output, code) = execCmdEx(nimCmd.join(" "))
132- if code == 0: writeFile(stamp, buildKey(nimSrc, release))169+ if code == 0: writeFile(stamp, buildKey(nimSrc, release, rtLib))
133 let tBuild = epochTime() - t1170 let tBuild = epochTime() - t1
134 if code != 0:171 if code != 0:
135 stderr.writeLine("clonim: Nim backend failed\n" & output)172 stderr.writeLine("clonim: Nim backend failed\n" & output)
modified src/compiler.nim +2 -2
@@ -1134,7 +1134,7 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) =
11341134 # ------------------------------------------------------------- entry point
11351135 const preamble = """
11361136 ## Generated by clonim. Do not edit.
1137-import runtime, core, namespaces
1137+import app_runtime
11381138
11391139 proc cljMain() =
11401140 """
@@ -1165,7 +1165,7 @@ proc compileForms*(forms: seq[Value]): string =
11651165 c.line("var " & t & ": Value = NilV")
11661166 genInto(f, t, env, c)
11671167 c.line("discard " & t)
1168- var src = preamble & " registerCore()\n registerNamespaceCore()\n" & c.prelude.join("\n") & "\n" &
1168+ var src = preamble & " initClonimRuntime()\n registerCore()\n registerNamespaceCore()\n" & c.prelude.join("\n") & "\n" &
11691169 c.body.join("\n") & "\n\n"
11701170 src &= """
11711171 when isMainModule:
@@ -1134,7 +1134,7 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) =
1134 # ------------------------------------------------------------- entry point1134 # ------------------------------------------------------------- entry point
1135 const preamble = """1135 const preamble = """
1136 ## Generated by clonim. Do not edit.1136 ## Generated by clonim. Do not edit.
1137-import runtime, core, namespaces1137+import app_runtime
1138 1138
1139 proc cljMain() =1139 proc cljMain() =
1140 """1140 """
@@ -1165,7 +1165,7 @@ proc compileForms*(forms: seq[Value]): string =
1165 c.line("var " & t & ": Value = NilV")1165 c.line("var " & t & ": Value = NilV")
1166 genInto(f, t, env, c)1166 genInto(f, t, env, c)
1167 c.line("discard " & t)1167 c.line("discard " & t)
1168- var src = preamble & " registerCore()\n registerNamespaceCore()\n" & c.prelude.join("\n") & "\n" &1168+ var src = preamble & " initClonimRuntime()\n registerCore()\n registerNamespaceCore()\n" & c.prelude.join("\n") & "\n" &
1169 c.body.join("\n") & "\n\n"1169 c.body.join("\n") & "\n\n"
1170 src &= """1170 src &= """
1171 when isMainModule:1171 when isMainModule:
modified src/core.nim +0 -5
@@ -348,11 +348,6 @@ proc def(name: string, f: proc (args: openArray[Value]): Value {.closure.}) =
348348 ## Each base element is pushed through the stages and whatever survives is
349349 ## folded, so no chunk, cursor or intermediate seq is built between stages.
350350
351-type FusedOp* = object
352- isMap*: bool ## map when true, filter when false
353- fn*: Value
354- keep*: bool ## filter: keep matches, or drop them
355-
356351 template pushThrough(ops: openArray[FusedOp], x0: Value, emit: untyped) =
357352 ## Stages arrive outermost-first, so they apply in reverse.
358353 var it {.inject.} = x0
@@ -348,11 +348,6 @@ proc def(name: string, f: proc (args: openArray[Value]): Value {.closure.}) =
348 ## Each base element is pushed through the stages and whatever survives is348 ## Each base element is pushed through the stages and whatever survives is
349 ## folded, so no chunk, cursor or intermediate seq is built between stages.349 ## folded, so no chunk, cursor or intermediate seq is built between stages.
350 350
351-type FusedOp* = object
352- isMap*: bool ## map when true, filter when false
353- fn*: Value
354- keep*: bool ## filter: keep matches, or drop them
355-
356 template pushThrough(ops: openArray[FusedOp], x0: Value, emit: untyped) =351 template pushThrough(ops: openArray[FusedOp], x0: Value, emit: untyped) =
357 ## Stages arrive outermost-first, so they apply in reverse.352 ## Stages arrive outermost-first, so they apply in reverse.
358 var it {.inject.} = x0353 var it {.inject.} = x0
modified src/runtime.nim +2 -175
@@ -7,6 +7,8 @@
77 ## entry carries an `ord` stamp and iteration sorts by it — so printing and
88 ## `keys`/`vals` stay predictable the way Clojure's small array-maps are.
99 import std/[tables, strutils, hashes, bitops, algorithm]
10+import runtime_types
11+export runtime_types
1012
1113 const
1214 Bits = 5
@@ -14,174 +16,6 @@ const
1416 Mask = Width - 1
1517 MaxShift = 30 # beyond this a HAMT runs out of hash bits
1618
17-type
18- Kind* = enum
19- kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol,
20- kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn
21-
22- VNode* = ref object
23- ## A trie node: leaves hold values, internal nodes hold children.
24- case leaf*: bool
25- of true: vals*: seq[Value]
26- of false: kids*: seq[VNode]
27-
28- PVec* = object
29- cnt*: int ## total element count
30- shift*: int ## bit offset of the root level
31- root*: VNode ## internal node (never nil)
32- tail*: seq[Value] ## up to 32 trailing elements, not yet in the trie
33-
34- MEntry* = object
35- key*, val*: Value
36- ord*: int ## insertion stamp, for stable iteration order
37-
38- MSlotKind* = enum msEntry, msNode
39- MSlot* = object
40- case sk*: MSlotKind
41- of msEntry: e*: MEntry
42- of msNode: node*: MNode
43-
44- MNode* = ref object
45- ## Bitmap-indexed node, or — once the hash bits run out — a linear
46- ## collision bucket.
47- case collision*: bool
48- of false:
49- bitmap*: uint32
50- slots*: seq[MSlot]
51- of true:
52- kvs*: seq[MEntry]
53-
54- PMap* = object
55- root*: MNode ## nil when empty
56- cnt*: int
57- nextOrd*: int
58-
59- RecipeKind* = enum rkRange, rkMap, rkFilter
60-
61- Recipe* = ref object
62- ## What a lazy seq would produce, described rather than built. A consumer
63- ## that is going to realize everything anyway (reduce, count) can read this
64- ## and run the whole pipeline as a single loop, allocating no intermediate
65- ## chunks at all. Consumers that stop early ignore it and take the ordinary
66- ## lazy path, so laziness is unaffected.
67- case rk*: RecipeKind
68- of rkRange:
69- lo*, hi*, step*: int64
70- bounded*: bool
71- of rkMap, rkFilter:
72- rfn*: Value
73- keep*: bool ## rkFilter: keep matches, or drop them
74- src*: Value
75-
76- Obj* = ref ValueObj
77- ## Value itself is not a ref: nil, bool, int and float live entirely in the
78- ## word below, so arithmetic and a numeric sequence never touch the heap. Only
79- ## the kinds with a real payload allocate a ValueObj, which is also what keeps
80- ## a lazy seq's memoisation working -- `force` mutates through `obj`, which is
81- ## still shared by every Value that names it.
82- ValueObj* = object
83- case kind*: Kind
84- of kNil, kBool, kInt, kFloat: discard
85- of kStr, kKeyword, kSymbol: s*: string
86- of kList: xs*: seq[Value]
87- of kVector: vec*: PVec
88- of kMap, kSet: m*: PMap
89- of kCons:
90- head*: Value
91- tl*: Value ## rest of the seq: a cons, a lazy seq, a coll, or nil
92- of kChunk:
93- ## A run of elements already realized together, plus the rest of the seq.
94- ## One of these per 32 elements replaces 32 cons cells and 32 thunks.
95- chunk*: seq[Value]
96- coff*: int ## index of the first element not yet consumed
97- ctl*: Value ## rest of the seq beyond this run
98- of kLazy:
99- thunk*: proc (): Value {.closure.}
100- cached*: Value
101- forced*: bool
102- rec*: Recipe ## nil unless this seq is a describable pipeline stage
103- of kFn:
104- fn*: proc (args: openArray[Value]): Value {.closure.}
105- name*: string
106-
107- Value* = object
108- kind*: Kind
109- raw*: int64 ## int payload; a float is bit-cast into it; bool is 0/1
110- obj*: Obj ## nil for the scalar kinds
111-
112- CljError* = object of CatchableError
113-
114-# Field accessors, named so the rest of the codebase keeps reading `v.i`,
115-# `v.xs`, `v.head` exactly as it did when Value was a ref to the variant.
116-proc i*(v: Value): int64 {.inline.} = v.raw
117-proc b*(v: Value): bool {.inline.} = v.raw != 0
118-proc f*(v: Value): float64 {.inline.} = cast[float64](v.raw)
119-proc s*(v: Value): lent string {.inline.} = v.obj.s
120-proc xs*(v: Value): lent seq[Value] {.inline.} = v.obj.xs
121-proc vec*(v: Value): lent PVec {.inline.} = v.obj.vec
122-proc m*(v: Value): lent PMap {.inline.} = v.obj.m
123-proc head*(v: Value): lent Value {.inline.} = v.obj.head
124-proc tl*(v: Value): lent Value {.inline.} = v.obj.tl
125-proc chunk*(v: Value): lent seq[Value] {.inline.} = v.obj.chunk
126-proc coff*(v: Value): int {.inline.} = v.obj.coff
127-proc ctl*(v: Value): lent Value {.inline.} = v.obj.ctl
128-proc cached*(v: Value): lent Value {.inline.} = v.obj.cached
129-proc forced*(v: Value): bool {.inline.} = v.obj.forced
130-proc thunk*(v: Value): auto {.inline.} = v.obj.thunk
131-proc fn*(v: Value): auto {.inline.} = v.obj.fn
132-proc name*(v: Value): lent string {.inline.} = v.obj.name
133-
134-## A Value is never a nil pointer now, but every `v.isNil` in the codebase
135-## meant "is this the nil value", so keep the spelling working.
136-proc isNil*(v: Value): bool {.inline.} = v.kind == kNil
137-
138-# ------------------------------------------------------------ teardown
139-## A realized lazy seq is a chain of `cons -> lazy -> cons -> …` refs, and ARC
140-## frees a chain by recursing into it — a million-element seq means a million
141-## destructor frames, i.e. a segfault at scope exit. So `kCons`/`kLazy` hand
142-## their tail to a worklist instead of letting the field drop inline, and the
143-## outermost destructor drains it in a loop. Nothing shared is mutated: a node
144-## another seq still holds simply survives with its refcount intact.
145-##
146-## Defining `=destroy` means the compiler stops generating field teardown for
147-## `ValueObj`, so every branch below has to release its own fields.
148-
149-var pendingFree: seq[Value] = @[]
150-var draining = false
151-
152-proc `=destroy`*(x: var ValueObj) =
153- case x.kind
154- of kStr, kKeyword, kSymbol: `=destroy`(x.s)
155- of kList: `=destroy`(x.xs)
156- of kVector: `=destroy`(x.vec)
157- of kMap, kSet: `=destroy`(x.m)
158- of kFn:
159- `=destroy`(x.fn)
160- `=destroy`(x.name)
161- of kCons:
162- `=destroy`(x.head)
163- if not x.tl.isNil: pendingFree.add x.tl # +1, outlives the release below
164- `=destroy`(x.tl)
165- of kChunk:
166- `=destroy`(x.chunk)
167- if not x.ctl.isNil: pendingFree.add x.ctl
168- `=destroy`(x.ctl)
169- of kLazy:
170- `=destroy`(x.thunk)
171- if not x.cached.isNil: pendingFree.add x.cached
172- `=destroy`(x.cached)
173- of kNil, kBool, kInt, kFloat: discard
174- if draining: return
175- draining = true
176- while pendingFree.len > 0:
177- let v = pendingFree.pop()
178- discard v # dies here: its own tail is queued, not recursed into
179- draining = false
180-
181-let NilV* = Value(kind: kNil)
182-let TrueV* = Value(kind: kBool, raw: 1)
183-let FalseV* = Value(kind: kBool, raw: 0)
184-
18519 proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg)
18620
18721 proc equals*(a, b: Value): bool
@@ -769,11 +603,6 @@ proc str*(v: Value): string = toStr(v, false)
769603 ## Vars are cells, as in Clojure: compiled code resolves the cell once and
770604 ## reads through it, so a call site costs one pointer deref, not a hash lookup,
771605 ## while `def` can still rebind the var later.
772-type VarCell* = ref object
773- name*: string
774- bound*: bool
775- v*: Value
776-
777606 var globals*: Table[string, VarCell] = initTable[string, VarCell]()
778607
779608 proc varCell*(name: string): VarCell =
@@ -843,8 +672,6 @@ proc toSeq*(v: Value): seq[Value] =
843672 r
844673 else: err("Don't know how to create seq from: " & prStr(v))
845674
846-let emptyArgs*: array[0, Value] = []
847-
848675 ## True while a var still holds the exact fn a call site was compiled against.
849676 ## Call sites that bind a known-arity fn or an inlined primitive directly guard
850677 ## on this, so a later `def` that rebinds the name still takes effect.
@@ -7,6 +7,8 @@
7 ## entry carries an `ord` stamp and iteration sorts by it — so printing and7 ## entry carries an `ord` stamp and iteration sorts by it — so printing and
8 ## `keys`/`vals` stay predictable the way Clojure's small array-maps are.8 ## `keys`/`vals` stay predictable the way Clojure's small array-maps are.
9 import std/[tables, strutils, hashes, bitops, algorithm]9 import std/[tables, strutils, hashes, bitops, algorithm]
10+import runtime_types
11+export runtime_types
10 12
11 const13 const
12 Bits = 514 Bits = 5
@@ -14,174 +16,6 @@ const
14 Mask = Width - 116 Mask = Width - 1
15 MaxShift = 30 # beyond this a HAMT runs out of hash bits17 MaxShift = 30 # beyond this a HAMT runs out of hash bits
16 18
17-type
18- Kind* = enum
19- kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol,
20- kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn
21-
22- VNode* = ref object
23- ## A trie node: leaves hold values, internal nodes hold children.
24- case leaf*: bool
25- of true: vals*: seq[Value]
26- of false: kids*: seq[VNode]
27-
28- PVec* = object
29- cnt*: int ## total element count
30- shift*: int ## bit offset of the root level
31- root*: VNode ## internal node (never nil)
32- tail*: seq[Value] ## up to 32 trailing elements, not yet in the trie
33-
34- MEntry* = object
35- key*, val*: Value
36- ord*: int ## insertion stamp, for stable iteration order
37-
38- MSlotKind* = enum msEntry, msNode
39- MSlot* = object
40- case sk*: MSlotKind
41- of msEntry: e*: MEntry
42- of msNode: node*: MNode
43-
44- MNode* = ref object
45- ## Bitmap-indexed node, or — once the hash bits run out — a linear
46- ## collision bucket.
47- case collision*: bool
48- of false:
49- bitmap*: uint32
50- slots*: seq[MSlot]
51- of true:
52- kvs*: seq[MEntry]
53-
54- PMap* = object
55- root*: MNode ## nil when empty
56- cnt*: int
57- nextOrd*: int
58-
59- RecipeKind* = enum rkRange, rkMap, rkFilter
60-
61- Recipe* = ref object
62- ## What a lazy seq would produce, described rather than built. A consumer
63- ## that is going to realize everything anyway (reduce, count) can read this
64- ## and run the whole pipeline as a single loop, allocating no intermediate
65- ## chunks at all. Consumers that stop early ignore it and take the ordinary
66- ## lazy path, so laziness is unaffected.
67- case rk*: RecipeKind
68- of rkRange:
69- lo*, hi*, step*: int64
70- bounded*: bool
71- of rkMap, rkFilter:
72- rfn*: Value
73- keep*: bool ## rkFilter: keep matches, or drop them
74- src*: Value
75-
76- Obj* = ref ValueObj
77- ## Value itself is not a ref: nil, bool, int and float live entirely in the
78- ## word below, so arithmetic and a numeric sequence never touch the heap. Only
79- ## the kinds with a real payload allocate a ValueObj, which is also what keeps
80- ## a lazy seq's memoisation working -- `force` mutates through `obj`, which is
81- ## still shared by every Value that names it.
82- ValueObj* = object
83- case kind*: Kind
84- of kNil, kBool, kInt, kFloat: discard
85- of kStr, kKeyword, kSymbol: s*: string
86- of kList: xs*: seq[Value]
87- of kVector: vec*: PVec
88- of kMap, kSet: m*: PMap
89- of kCons:
90- head*: Value
91- tl*: Value ## rest of the seq: a cons, a lazy seq, a coll, or nil
92- of kChunk:
93- ## A run of elements already realized together, plus the rest of the seq.
94- ## One of these per 32 elements replaces 32 cons cells and 32 thunks.
95- chunk*: seq[Value]
96- coff*: int ## index of the first element not yet consumed
97- ctl*: Value ## rest of the seq beyond this run
98- of kLazy:
99- thunk*: proc (): Value {.closure.}
100- cached*: Value
101- forced*: bool
102- rec*: Recipe ## nil unless this seq is a describable pipeline stage
103- of kFn:
104- fn*: proc (args: openArray[Value]): Value {.closure.}
105- name*: string
106-
107- Value* = object
108- kind*: Kind
109- raw*: int64 ## int payload; a float is bit-cast into it; bool is 0/1
110- obj*: Obj ## nil for the scalar kinds
111-
112- CljError* = object of CatchableError
113-
114-# Field accessors, named so the rest of the codebase keeps reading `v.i`,
115-# `v.xs`, `v.head` exactly as it did when Value was a ref to the variant.
116-proc i*(v: Value): int64 {.inline.} = v.raw
117-proc b*(v: Value): bool {.inline.} = v.raw != 0
118-proc f*(v: Value): float64 {.inline.} = cast[float64](v.raw)
119-proc s*(v: Value): lent string {.inline.} = v.obj.s
120-proc xs*(v: Value): lent seq[Value] {.inline.} = v.obj.xs
121-proc vec*(v: Value): lent PVec {.inline.} = v.obj.vec
122-proc m*(v: Value): lent PMap {.inline.} = v.obj.m
123-proc head*(v: Value): lent Value {.inline.} = v.obj.head
124-proc tl*(v: Value): lent Value {.inline.} = v.obj.tl
125-proc chunk*(v: Value): lent seq[Value] {.inline.} = v.obj.chunk
126-proc coff*(v: Value): int {.inline.} = v.obj.coff
127-proc ctl*(v: Value): lent Value {.inline.} = v.obj.ctl
128-proc cached*(v: Value): lent Value {.inline.} = v.obj.cached
129-proc forced*(v: Value): bool {.inline.} = v.obj.forced
130-proc thunk*(v: Value): auto {.inline.} = v.obj.thunk
131-proc fn*(v: Value): auto {.inline.} = v.obj.fn
132-proc name*(v: Value): lent string {.inline.} = v.obj.name
133-
134-## A Value is never a nil pointer now, but every `v.isNil` in the codebase
135-## meant "is this the nil value", so keep the spelling working.
136-proc isNil*(v: Value): bool {.inline.} = v.kind == kNil
137-
138-# ------------------------------------------------------------ teardown
139-## A realized lazy seq is a chain of `cons -> lazy -> cons -> …` refs, and ARC
140-## frees a chain by recursing into it — a million-element seq means a million
141-## destructor frames, i.e. a segfault at scope exit. So `kCons`/`kLazy` hand
142-## their tail to a worklist instead of letting the field drop inline, and the
143-## outermost destructor drains it in a loop. Nothing shared is mutated: a node
144-## another seq still holds simply survives with its refcount intact.
145-##
146-## Defining `=destroy` means the compiler stops generating field teardown for
147-## `ValueObj`, so every branch below has to release its own fields.
148-
149-var pendingFree: seq[Value] = @[]
150-var draining = false
151-
152-proc `=destroy`*(x: var ValueObj) =
153- case x.kind
154- of kStr, kKeyword, kSymbol: `=destroy`(x.s)
155- of kList: `=destroy`(x.xs)
156- of kVector: `=destroy`(x.vec)
157- of kMap, kSet: `=destroy`(x.m)
158- of kFn:
159- `=destroy`(x.fn)
160- `=destroy`(x.name)
161- of kCons:
162- `=destroy`(x.head)
163- if not x.tl.isNil: pendingFree.add x.tl # +1, outlives the release below
164- `=destroy`(x.tl)
165- of kChunk:
166- `=destroy`(x.chunk)
167- if not x.ctl.isNil: pendingFree.add x.ctl
168- `=destroy`(x.ctl)
169- of kLazy:
170- `=destroy`(x.thunk)
171- if not x.cached.isNil: pendingFree.add x.cached
172- `=destroy`(x.cached)
173- of kNil, kBool, kInt, kFloat: discard
174- if draining: return
175- draining = true
176- while pendingFree.len > 0:
177- let v = pendingFree.pop()
178- discard v # dies here: its own tail is queued, not recursed into
179- draining = false
180-
181-let NilV* = Value(kind: kNil)
182-let TrueV* = Value(kind: kBool, raw: 1)
183-let FalseV* = Value(kind: kBool, raw: 0)
184-
185 proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg)19 proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg)
186 20
187 proc equals*(a, b: Value): bool21 proc equals*(a, b: Value): bool
@@ -769,11 +603,6 @@ proc str*(v: Value): string = toStr(v, false)
769 ## Vars are cells, as in Clojure: compiled code resolves the cell once and603 ## Vars are cells, as in Clojure: compiled code resolves the cell once and
770 ## reads through it, so a call site costs one pointer deref, not a hash lookup,604 ## reads through it, so a call site costs one pointer deref, not a hash lookup,
771 ## while `def` can still rebind the var later.605 ## while `def` can still rebind the var later.
772-type VarCell* = ref object
773- name*: string
774- bound*: bool
775- v*: Value
776-
777 var globals*: Table[string, VarCell] = initTable[string, VarCell]()606 var globals*: Table[string, VarCell] = initTable[string, VarCell]()
778 607
779 proc varCell*(name: string): VarCell =608 proc varCell*(name: string): VarCell =
@@ -843,8 +672,6 @@ proc toSeq*(v: Value): seq[Value] =
843 r672 r
844 else: err("Don't know how to create seq from: " & prStr(v))673 else: err("Don't know how to create seq from: " & prStr(v))
845 674
846-let emptyArgs*: array[0, Value] = []
847-
848 ## True while a var still holds the exact fn a call site was compiled against.675 ## True while a var still holds the exact fn a call site was compiled against.
849 ## Call sites that bind a known-arity fn or an inlined primitive directly guard676 ## Call sites that bind a known-arity fn or an inlined primitive directly guard
850 ## on this, so a later `def` that rebinds the name still takes effect.677 ## on this, so a later `def` that rebinds the name still takes effect.
added src/runtime_lib.nim +53 -0
new file mode 100644
@@ -0,0 +1,53 @@
1+## C-named entry points for clonim's private static runtime archive.
2+import runtime, core, namespaces
3+
4+proc clonim_mk_fn(name: string,
5+ f: proc (args: openArray[Value]): Value {.closure.}): Value
6+ {.cdecl, exportc.} =
7+ mkFn(name, f)
8+
9+{.push cdecl, exportc.}
10+proc clonim_register_core() = registerCore()
11+proc clonim_register_namespace_core() = registerNamespaceCore()
12+proc clonim_err(msg: string) {.noreturn.} = err(msg)
13+proc clonim_var_cell(name: string): VarCell = varCell(name)
14+proc clonim_set_var(name: string, v: Value): Value = setVar(name, v)
15+proc clonim_cell_get(c: VarCell): Value = cellGet(c)
16+proc clonim_cell_is(c: VarCell, v: Value): bool = cellIs(c, v)
17+proc clonim_get_var(name: string): Value = getVar(name)
18+proc clonim_call(f: Value, args: openArray[Value]): Value = call(f, args)
19+proc clonim_arg_at(args: openArray[Value], i: int): Value = argAt(args, i)
20+proc clonim_rest_args(args: openArray[Value], i: int): Value = restArgs(args, i)
21+proc clonim_seq_drop(v: Value, n: int): Value = seqDrop(v, n)
22+proc clonim_to_seq(v: Value): seq[Value] = toSeq(v)
23+proc clonim_truthy(v: Value): bool = truthy(v)
24+
25+proc clonim_mk_int(x: int64): Value = mkInt(x)
26+proc clonim_mk_float(x: float64): Value = mkFloat(x)
27+proc clonim_mk_str(x: string): Value = mkStr(x)
28+proc clonim_mk_keyword(x: string): Value = mkKeyword(x)
29+proc clonim_mk_symbol(x: string): Value = mkSymbol(x)
30+proc clonim_mk_list(xs: openArray[Value]): Value = mkList(xs)
31+proc clonim_mk_vector(xs: openArray[Value]): Value = mkVector(xs)
32+proc clonim_mk_set(xs: openArray[Value]): Value = mkSet(xs)
33+proc clonim_mk_map(ps: seq[(Value, Value)]): Value = mkMap(ps)
34+
35+proc clonim_idiv(a, b: int64): int64 = idiv(a, b)
36+proc clonim_irem(a, b: int64): int64 = irem(a, b)
37+proc clonim_add2(a, b: Value): Value = add2(a, b)
38+proc clonim_sub2(a, b: Value): Value = sub2(a, b)
39+proc clonim_mul2(a, b: Value): Value = mul2(a, b)
40+proc clonim_lt2(a, b: Value): Value = lt2(a, b)
41+proc clonim_gt2(a, b: Value): Value = gt2(a, b)
42+proc clonim_le2(a, b: Value): Value = le2(a, b)
43+proc clonim_ge2(a, b: Value): Value = ge2(a, b)
44+proc clonim_eq2(a, b: Value): Value = eq2(a, b)
45+proc clonim_ne2(a, b: Value): Value = ne2(a, b)
46+proc clonim_inc1(a: Value): Value = inc1(a)
47+proc clonim_dec1(a: Value): Value = dec1(a)
48+proc clonim_fused_reduce(f, init: Value, hasInit: bool, base: Value,
49+ ops: openArray[FusedOp]): Value =
50+ fusedReduce(f, init, hasInit, base, ops)
51+proc clonim_fused_count(base: Value, ops: openArray[FusedOp]): Value =
52+ fusedCount(base, ops)
53+{.pop.}
new file mode 100644
@@ -0,0 +1,53 @@
1+## C-named entry points for clonim's private static runtime archive.
2+import runtime, core, namespaces
3+
4+proc clonim_mk_fn(name: string,
5+ f: proc (args: openArray[Value]): Value {.closure.}): Value
6+ {.cdecl, exportc.} =
7+ mkFn(name, f)
8+
9+{.push cdecl, exportc.}
10+proc clonim_register_core() = registerCore()
11+proc clonim_register_namespace_core() = registerNamespaceCore()
12+proc clonim_err(msg: string) {.noreturn.} = err(msg)
13+proc clonim_var_cell(name: string): VarCell = varCell(name)
14+proc clonim_set_var(name: string, v: Value): Value = setVar(name, v)
15+proc clonim_cell_get(c: VarCell): Value = cellGet(c)
16+proc clonim_cell_is(c: VarCell, v: Value): bool = cellIs(c, v)
17+proc clonim_get_var(name: string): Value = getVar(name)
18+proc clonim_call(f: Value, args: openArray[Value]): Value = call(f, args)
19+proc clonim_arg_at(args: openArray[Value], i: int): Value = argAt(args, i)
20+proc clonim_rest_args(args: openArray[Value], i: int): Value = restArgs(args, i)
21+proc clonim_seq_drop(v: Value, n: int): Value = seqDrop(v, n)
22+proc clonim_to_seq(v: Value): seq[Value] = toSeq(v)
23+proc clonim_truthy(v: Value): bool = truthy(v)
24+
25+proc clonim_mk_int(x: int64): Value = mkInt(x)
26+proc clonim_mk_float(x: float64): Value = mkFloat(x)
27+proc clonim_mk_str(x: string): Value = mkStr(x)
28+proc clonim_mk_keyword(x: string): Value = mkKeyword(x)
29+proc clonim_mk_symbol(x: string): Value = mkSymbol(x)
30+proc clonim_mk_list(xs: openArray[Value]): Value = mkList(xs)
31+proc clonim_mk_vector(xs: openArray[Value]): Value = mkVector(xs)
32+proc clonim_mk_set(xs: openArray[Value]): Value = mkSet(xs)
33+proc clonim_mk_map(ps: seq[(Value, Value)]): Value = mkMap(ps)
34+
35+proc clonim_idiv(a, b: int64): int64 = idiv(a, b)
36+proc clonim_irem(a, b: int64): int64 = irem(a, b)
37+proc clonim_add2(a, b: Value): Value = add2(a, b)
38+proc clonim_sub2(a, b: Value): Value = sub2(a, b)
39+proc clonim_mul2(a, b: Value): Value = mul2(a, b)
40+proc clonim_lt2(a, b: Value): Value = lt2(a, b)
41+proc clonim_gt2(a, b: Value): Value = gt2(a, b)
42+proc clonim_le2(a, b: Value): Value = le2(a, b)
43+proc clonim_ge2(a, b: Value): Value = ge2(a, b)
44+proc clonim_eq2(a, b: Value): Value = eq2(a, b)
45+proc clonim_ne2(a, b: Value): Value = ne2(a, b)
46+proc clonim_inc1(a: Value): Value = inc1(a)
47+proc clonim_dec1(a: Value): Value = dec1(a)
48+proc clonim_fused_reduce(f, init: Value, hasInit: bool, base: Value,
49+ ops: openArray[FusedOp]): Value =
50+ fusedReduce(f, init, hasInit, base, ops)
51+proc clonim_fused_count(base: Value, ops: openArray[FusedOp]): Value =
52+ fusedCount(base, ops)
53+{.pop.}
added src/runtime_types.nim +147 -0
new file mode 100644
@@ -0,0 +1,147 @@
1+## ABI shared by generated clonim programs and the private static runtime.
2+## This is not a public interface: it is built and shipped as one versioned
3+## unit with the matching compiler and runtime archive.
4+
5+type
6+ Kind* = enum
7+ kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol,
8+ kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn
9+
10+ VNode* = ref object
11+ case leaf*: bool
12+ of true: vals*: seq[Value]
13+ of false: kids*: seq[VNode]
14+
15+ PVec* = object
16+ cnt*, shift*: int
17+ root*: VNode
18+ tail*: seq[Value]
19+
20+ MEntry* = object
21+ key*, val*: Value
22+ ord*: int
23+
24+ MSlotKind* = enum msEntry, msNode
25+ MSlot* = object
26+ case sk*: MSlotKind
27+ of msEntry: e*: MEntry
28+ of msNode: node*: MNode
29+
30+ MNode* = ref object
31+ case collision*: bool
32+ of false:
33+ bitmap*: uint32
34+ slots*: seq[MSlot]
35+ of true:
36+ kvs*: seq[MEntry]
37+
38+ PMap* = object
39+ root*: MNode
40+ cnt*, nextOrd*: int
41+
42+ RecipeKind* = enum rkRange, rkMap, rkFilter
43+ Recipe* = ref object
44+ case rk*: RecipeKind
45+ of rkRange:
46+ lo*, hi*, step*: int64
47+ bounded*: bool
48+ of rkMap, rkFilter:
49+ rfn*: Value
50+ keep*: bool
51+ src*: Value
52+
53+ Obj* = ref ValueObj
54+ ValueObj* = object
55+ case kind*: Kind
56+ of kNil, kBool, kInt, kFloat: discard
57+ of kStr, kKeyword, kSymbol: s*: string
58+ of kList: xs*: seq[Value]
59+ of kVector: vec*: PVec
60+ of kMap, kSet: m*: PMap
61+ of kCons:
62+ head*, tl*: Value
63+ of kChunk:
64+ chunk*: seq[Value]
65+ coff*: int
66+ ctl*: Value
67+ of kLazy:
68+ thunk*: proc (): Value {.closure.}
69+ cached*: Value
70+ forced*: bool
71+ rec*: Recipe
72+ of kFn:
73+ fn*: proc (args: openArray[Value]): Value {.closure.}
74+ name*: string
75+
76+ Value* = object
77+ kind*: Kind
78+ raw*: int64
79+ obj*: Obj
80+
81+ CljError* = object of CatchableError
82+
83+ VarCell* = ref object
84+ name*: string
85+ bound*: bool
86+ v*: Value
87+
88+ FusedOp* = object
89+ isMap*: bool
90+ fn*: Value
91+ keep*: bool
92+
93+proc i*(v: Value): int64 {.inline.} = v.raw
94+proc b*(v: Value): bool {.inline.} = v.raw != 0
95+proc f*(v: Value): float64 {.inline.} = cast[float64](v.raw)
96+proc s*(v: Value): lent string {.inline.} = v.obj.s
97+proc xs*(v: Value): lent seq[Value] {.inline.} = v.obj.xs
98+proc vec*(v: Value): lent PVec {.inline.} = v.obj.vec
99+proc m*(v: Value): lent PMap {.inline.} = v.obj.m
100+proc head*(v: Value): lent Value {.inline.} = v.obj.head
101+proc tl*(v: Value): lent Value {.inline.} = v.obj.tl
102+proc chunk*(v: Value): lent seq[Value] {.inline.} = v.obj.chunk
103+proc coff*(v: Value): int {.inline.} = v.obj.coff
104+proc ctl*(v: Value): lent Value {.inline.} = v.obj.ctl
105+proc cached*(v: Value): lent Value {.inline.} = v.obj.cached
106+proc forced*(v: Value): bool {.inline.} = v.obj.forced
107+proc thunk*(v: Value): auto {.inline.} = v.obj.thunk
108+proc fn*(v: Value): auto {.inline.} = v.obj.fn
109+proc name*(v: Value): lent string {.inline.} = v.obj.name
110+proc isNil*(v: Value): bool {.inline.} = v.kind == kNil
111+
112+var pendingFree: seq[Value] = @[]
113+var draining = false
114+
115+proc `=destroy`*(x: var ValueObj) =
116+ case x.kind
117+ of kStr, kKeyword, kSymbol: `=destroy`(x.s)
118+ of kList: `=destroy`(x.xs)
119+ of kVector: `=destroy`(x.vec)
120+ of kMap, kSet: `=destroy`(x.m)
121+ of kFn:
122+ `=destroy`(x.fn)
123+ `=destroy`(x.name)
124+ of kCons:
125+ `=destroy`(x.head)
126+ if not x.tl.isNil: pendingFree.add x.tl
127+ `=destroy`(x.tl)
128+ of kChunk:
129+ `=destroy`(x.chunk)
130+ if not x.ctl.isNil: pendingFree.add x.ctl
131+ `=destroy`(x.ctl)
132+ of kLazy:
133+ `=destroy`(x.thunk)
134+ if not x.cached.isNil: pendingFree.add x.cached
135+ `=destroy`(x.cached)
136+ of kNil, kBool, kInt, kFloat: discard
137+ if draining: return
138+ draining = true
139+ while pendingFree.len > 0:
140+ let v = pendingFree.pop()
141+ discard v
142+ draining = false
143+
144+let NilV* = Value(kind: kNil)
145+let TrueV* = Value(kind: kBool, raw: 1)
146+let FalseV* = Value(kind: kBool, raw: 0)
147+let emptyArgs*: array[0, Value] = []
new file mode 100644
@@ -0,0 +1,147 @@
1+## ABI shared by generated clonim programs and the private static runtime.
2+## This is not a public interface: it is built and shipped as one versioned
3+## unit with the matching compiler and runtime archive.
4+
5+type
6+ Kind* = enum
7+ kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol,
8+ kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn
9+
10+ VNode* = ref object
11+ case leaf*: bool
12+ of true: vals*: seq[Value]
13+ of false: kids*: seq[VNode]
14+
15+ PVec* = object
16+ cnt*, shift*: int
17+ root*: VNode
18+ tail*: seq[Value]
19+
20+ MEntry* = object
21+ key*, val*: Value
22+ ord*: int
23+
24+ MSlotKind* = enum msEntry, msNode
25+ MSlot* = object
26+ case sk*: MSlotKind
27+ of msEntry: e*: MEntry
28+ of msNode: node*: MNode
29+
30+ MNode* = ref object
31+ case collision*: bool
32+ of false:
33+ bitmap*: uint32
34+ slots*: seq[MSlot]
35+ of true:
36+ kvs*: seq[MEntry]
37+
38+ PMap* = object
39+ root*: MNode
40+ cnt*, nextOrd*: int
41+
42+ RecipeKind* = enum rkRange, rkMap, rkFilter
43+ Recipe* = ref object
44+ case rk*: RecipeKind
45+ of rkRange:
46+ lo*, hi*, step*: int64
47+ bounded*: bool
48+ of rkMap, rkFilter:
49+ rfn*: Value
50+ keep*: bool
51+ src*: Value
52+
53+ Obj* = ref ValueObj
54+ ValueObj* = object
55+ case kind*: Kind
56+ of kNil, kBool, kInt, kFloat: discard
57+ of kStr, kKeyword, kSymbol: s*: string
58+ of kList: xs*: seq[Value]
59+ of kVector: vec*: PVec
60+ of kMap, kSet: m*: PMap
61+ of kCons:
62+ head*, tl*: Value
63+ of kChunk:
64+ chunk*: seq[Value]
65+ coff*: int
66+ ctl*: Value
67+ of kLazy:
68+ thunk*: proc (): Value {.closure.}
69+ cached*: Value
70+ forced*: bool
71+ rec*: Recipe
72+ of kFn:
73+ fn*: proc (args: openArray[Value]): Value {.closure.}
74+ name*: string
75+
76+ Value* = object
77+ kind*: Kind
78+ raw*: int64
79+ obj*: Obj
80+
81+ CljError* = object of CatchableError
82+
83+ VarCell* = ref object
84+ name*: string
85+ bound*: bool
86+ v*: Value
87+
88+ FusedOp* = object
89+ isMap*: bool
90+ fn*: Value
91+ keep*: bool
92+
93+proc i*(v: Value): int64 {.inline.} = v.raw
94+proc b*(v: Value): bool {.inline.} = v.raw != 0
95+proc f*(v: Value): float64 {.inline.} = cast[float64](v.raw)
96+proc s*(v: Value): lent string {.inline.} = v.obj.s
97+proc xs*(v: Value): lent seq[Value] {.inline.} = v.obj.xs
98+proc vec*(v: Value): lent PVec {.inline.} = v.obj.vec
99+proc m*(v: Value): lent PMap {.inline.} = v.obj.m
100+proc head*(v: Value): lent Value {.inline.} = v.obj.head
101+proc tl*(v: Value): lent Value {.inline.} = v.obj.tl
102+proc chunk*(v: Value): lent seq[Value] {.inline.} = v.obj.chunk
103+proc coff*(v: Value): int {.inline.} = v.obj.coff
104+proc ctl*(v: Value): lent Value {.inline.} = v.obj.ctl
105+proc cached*(v: Value): lent Value {.inline.} = v.obj.cached
106+proc forced*(v: Value): bool {.inline.} = v.obj.forced
107+proc thunk*(v: Value): auto {.inline.} = v.obj.thunk
108+proc fn*(v: Value): auto {.inline.} = v.obj.fn
109+proc name*(v: Value): lent string {.inline.} = v.obj.name
110+proc isNil*(v: Value): bool {.inline.} = v.kind == kNil
111+
112+var pendingFree: seq[Value] = @[]
113+var draining = false
114+
115+proc `=destroy`*(x: var ValueObj) =
116+ case x.kind
117+ of kStr, kKeyword, kSymbol: `=destroy`(x.s)
118+ of kList: `=destroy`(x.xs)
119+ of kVector: `=destroy`(x.vec)
120+ of kMap, kSet: `=destroy`(x.m)
121+ of kFn:
122+ `=destroy`(x.fn)
123+ `=destroy`(x.name)
124+ of kCons:
125+ `=destroy`(x.head)
126+ if not x.tl.isNil: pendingFree.add x.tl
127+ `=destroy`(x.tl)
128+ of kChunk:
129+ `=destroy`(x.chunk)
130+ if not x.ctl.isNil: pendingFree.add x.ctl
131+ `=destroy`(x.ctl)
132+ of kLazy:
133+ `=destroy`(x.thunk)
134+ if not x.cached.isNil: pendingFree.add x.cached
135+ `=destroy`(x.cached)
136+ of kNil, kBool, kInt, kFloat: discard
137+ if draining: return
138+ draining = true
139+ while pendingFree.len > 0:
140+ let v = pendingFree.pop()
141+ discard v
142+ draining = false
143+
144+let NilV* = Value(kind: kNil)
145+let TrueV* = Value(kind: kBool, raw: 1)
146+let FalseV* = Value(kind: kBool, raw: 0)
147+let emptyArgs*: array[0, Value] = []