commit 87a7002a31340e248559cc1051df0911d09ed654
parent db553a68ab57c4cdd642e20983f671c7b9bc5f4f
Author: James Eversole <james@eversole.co>
Date: Sun, 10 May 2026 21:21:58 -0500
Native Arboricx bundle parser and C ABI in Zig
Diffstat:
27 files changed, 5365 insertions(+), 18 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -4,13 +4,20 @@
## 0. Test Driven Development
-Write and discuss tests with the user before implementing any implementation code.
+Write and discuss tests with the user before working on implementation code. Do not modify existing tests without explicit permission.
## 1. Build & Test
```bash
-# Tests
+# Haskell tests (default check)
nix flake check
+
+# Zig build
+nix build .#tricu-zig
+
+# Zig tests (separate target — not part of nix flake check)
+nix build .#tricu-zig-tests
+
# Full build
nix build .#
```
@@ -32,10 +39,10 @@ nix build .#
| `LToken` | Lexer tokens |
| `Node` / `MerkleHash` | Content-addressed Merkle DAG nodes |
-### Source modules
+### Source modules (Haskell)
| Module | Purpose |
-|--------|---------|
+|--------|---------|
| `Main.hs` | CLI entry point (`cmdargs`), three modes: `repl`, `eval`, `decode` |
| `Eval.hs` | Interpreter: `evalTricu`, `result`, `evalSingle` |
| `Parser.hs` | Megaparsec parser → `TricuAST` |
@@ -46,13 +53,31 @@ nix build .#
| `ContentStore.hs` | SQLite-backed term persistence |
| `Wire.hs` | Arboricx portable wire format — encode/decode/import/export of Merkle-DAG bundle blobs |
+### Multi-language Arboricx ecosystem
+
+Arboricx is the portable executable-object format used by tricu. The project now includes native parsing and execution in multiple languages:
+
+| Language | Location | Capabilities |
+|----------|----------|--------------|
+| **Haskell** | `src/Wire.hs`, `src/Research.hs` | Reference implementation — bundle encode/decode, content store, full Tree Calculus reduction |
+| **tricu (self-hosted)** | `kernel_run_arboricx_typed.dag` | A self-hosting Arboricx parser/executor written in tricu itself. Used as a kernel inside the Zig host for maximum portability ("cool but useless" — ~3s for `append`) |
+| **Zig** | `ext/zig/` | **Production host** — native bundle parser, WHNF reducer, C ABI (`libarboricx.so` / `.a`), CLI (`tricu-zig`), Python FFI support |
+| **JavaScript (Node)** | `ext/js/` | Native bundle parser, manifest decoder, Merkle DAG verifier, Tree Calculus reducer, CLI runner |
+| **PHP** | `ext/php/` | Tree Calculus reducer, codecs, kernel loader, CLI runner |
+
+All hosts share the same bundle format and Merkle hashing scheme.
+
### File extensions
- `.hs` - Haskell source
- `.tri` - tricu language source (used in `lib/`, `test/`, `demos/`)
+- `.arboricx` - Portable executable bundle
+- `.dag` - Serialized kernel DAG (used by `gen_kernel.zig` at build time)
## 3. Test Suite
+### Haskell tests
+
Tests live in `test/Spec.hs` and use **Tasty** + **HUnit**.
```bash
@@ -75,11 +100,24 @@ nix flake check
| `elimLambdaSingle` | Lambda elimination: eta reduction, SDef binding, semantics preservation |
| `stressElimLambda` | Lambda elimination stress test: 200 vars, 800-body curried lambda |
-### Suggesting tests
+### Zig tests
-You do not write or modify tests. The user writes tests to constrain your outputs. You must adhere your code to tests or suggest modifications to tests.
+Run separately via:
-If the user gives you explicit permission to implement a test you may proceed.
+```bash
+nix build .#tricu-zig-tests
+```
+
+These are **not** included in `nix flake check`. The test derivation compiles and runs:
+
+| Test | What it covers |
+|------|----------------|
+| `c_abi_test.c` | Smoke tests — leaf, stem, fork, app, reduce, number/string roundtrip, kernel root |
+| `c_abi_append_test.c` | Kernel path — `append.arboricx` with string arguments via Tricu kernel |
+| `native_bundle_append_test.c` | Native fast path — `append.arboricx` loaded natively, applied, reduced |
+| `native_bundle_id_test.c` | Native fast path — `id.arboricx` |
+| `native_bundle_bools_test.c` | Native fast path — `true.arboricx` / `false.arboricx` |
+| `python_ffi_test.py` | Python ctypes FFI — tests both kernel and native paths for `id` and `append` |
## 4. tricu Language Quick Reference
@@ -145,12 +183,75 @@ Portable executable bundles are generated via `Wire.hs`. See `docs/arboricx-bund
TRICU_DB_PATH=/tmp/tricu.db ./result/bin/tricu export -o list_ops.arboricx append
```
-## 8. Directory Layout
+## 8. Zig Arboricx Host (`ext/zig/`)
+
+The Zig host is a fast implementation for running Arboricx bundles. It provides a native bundle parser and arena-based evaluator.
+
+### Modules
+
+| File | Role |
+|------|------|
+| `src/main.zig` | CLI entrypoint — default native path, `--kernel` fallback |
+| `src/bundle.zig` | Native Arboricx bundle parser — verifies digests, hashes, loads DAG into arena |
+| `src/c_abi.zig` | C FFI exports — `arboricx_init`, tree constructors, codecs, reduction, bundle loading |
+| `src/reduce.zig` | WHNF reducer (Tree Calculus `apply` rules) |
+| `src/arena.zig` | Node arena (`ArrayListUnmanaged`) |
+| `src/tree.zig` | `Node` union + iterative `copyTree` |
+| `src/codecs.zig` | Number/string/list/bytes encoding + result unwrapping |
+| `src/kernel.zig` | Embeds DAG kernel into arena (fallback path only) |
+| `src/ternary.zig` | Ternary string parser for Tree Calculus terms |
+| `tools/gen_kernel.zig` | Build-time tool: converts `.dag` → `kernel_embed.zig` |
+| `include/arboricx.h` | C header for `libarboricx` |
+
+### C ABI
+
+Key functions:
+
+```c
+arb_ctx_t* arboricx_init(void);
+uint32_t arb_load_bundle(arb_ctx_t*, const uint8_t* bytes, size_t len, const char* name);
+uint32_t arb_load_bundle_default(arb_ctx_t*, const uint8_t* bytes, size_t len);
+uint32_t arb_reduce(arb_ctx_t*, uint32_t root, uint64_t fuel);
+```
+
+`arb_reduce` evaluates in a **fresh scratch arena** so garbage never accumulates.
+
+### Stack size requirement
+
+Tree Calculus reduction is deeply recursive. Assume a segfault is a memory limitation until proven otherwise.
+
+```bash
+ulimit -s 32768 # 32 MB
+```
+
+### Performance comparison
+
+| Fixture | Native path | Kernel path (`--kernel`) |
+|---------|-------------|--------------------------|
+| `append "hello " "world"` | **~0.007 s** | ~3.4 s |
+| `id "hello"` | **~0.005 s** | ~0.38 s |
+
+The kernel path is kept as a "cool but useless" fallback — the DAG is tiny (~30 KB) so the cost is negligible.
+
+## 9. Nix Flake Outputs
+
+| Output | Description |
+|--------|-------------|
+| `packages.default` / `packages.tricu` | Haskell tricu package |
+| `packages.tricu-zig` | Zig CLI + `libarboricx.a` + `libarboricx.so` + `arboricx.h` |
+| `packages.tricu-zig-tests` | **Separate test target** — C ABI + native bundle + Python FFI tests |
+| `packages.tricu-container` | Docker image |
+| `checks.default` / `checks.tricu` | Haskell test suite via Tasty/HUnit |
+
+`tricu-zig-tests` is deliberately **not** in `checks` so `nix flake check` remains fast.
+
+## 10. Directory Layout
```
tricu/
├── flake.nix # Nix flake: packages, tests, devShell
├── tricu.cabal # Cabal package (used via callCabal2nix)
+├── AGENTS.md # This file
├── src/ # Haskell modules
│ ├── Main.hs
│ ├── Eval.hs
@@ -160,10 +261,11 @@ tricu/
│ ├── REPL.hs
│ ├── Research.hs
│ ├── ContentStore.hs
-│ └── Wire.hs # Arboricx portable wire format
+│ └── Wire.hs
├── test/
│ ├── Spec.hs # Tasty + HUnit tests
│ ├── *.tri # tricu test programs
+│ ├── *.arboricx # Arboricx bundle fixtures
│ └── local-ns/ # Module namespace test files
├── lib/
│ ├── base.tri
@@ -175,10 +277,52 @@ tricu/
│ ├── toSource.tri
│ ├── levelOrderTraversal.tri
│ └── patternMatching.tri
-└── AGENTS.md # This file
+├── ext/ # Multi-language Arboricx hosts
+│ ├── js/ # Node.js bundle parser + reducer
+│ │ ├── src/
+│ │ │ ├── bundle.js
+│ │ │ ├── manifest.js
+│ │ │ ├── merkle.js
+│ │ │ ├── tree.js
+│ │ │ ├── codecs.js
+│ │ │ └── cli.js
+│ │ └── test/
+│ ├── php/ # PHP bundle loader + reducer
+│ │ ├── src/
+│ │ │ ├── functions.php
+│ │ │ ├── codecs.php
+│ │ │ ├── kernel.php
+│ │ │ └── Tree/
+│ │ └── run.php
+│ └── zig/ # Zig production host
+│ ├── build.zig
+│ ├── build.zig.zon
+│ ├── kernel_run_arboricx_typed.dag
+│ ├── include/arboricx.h
+│ ├── src/
+│ │ ├── main.zig
+│ │ ├── bundle.zig
+│ │ ├── c_abi.zig
+│ │ ├── codecs.zig
+│ │ ├── kernel.zig
+│ │ ├── reduce.zig
+│ │ ├── arena.zig
+│ │ ├── tree.zig
+│ │ └── ternary.zig
+│ ├── tests/
+│ │ ├── c_abi_test.c
+│ │ ├── c_abi_append_test.c
+│ │ ├── native_bundle_append_test.c
+│ │ ├── native_bundle_id_test.c
+│ │ ├── native_bundle_bools_test.c
+│ │ └── python_ffi_test.py
+│ └── tools/
+│ └── gen_kernel.zig
+└── docs/
+ └── arboricx-bundle-format.md
```
-## 9. Content Store Workflow (Custom DB)
+## 11. Content Store Workflow (Custom DB)
The content store location is controlled by the `TRICU_DB_PATH` environment variable. When set, `eval` mode automatically loads all stored terms into the initial environment, so you can call any previously imported/evaluated term by name.
@@ -206,14 +350,16 @@ t> !definitions
Without `TRICU_DB_PATH` set, `eval` uses only the terms defined in the input file(s).
-## 10. Development Tips
+## 12. Development Tips
- **REPL:** `nix run .#` starts the interactive tricu REPL.
- **Evaluate files:** `nix run .# -- eval -f demos/equality.tri`
+- **Zig host:** `nix build .#tricu-zig` then `./result/bin/tricu-zig <bundle> [args...]`
+- **Zig tests:** `nix build .#tricu-zig-tests`
- **GHC options:** `-threaded -rtsopts -with-rtsopts=-N` for parallel runtime. Use `-N` RTS flag for multi-core.
- **Upx** is in the devShell for binary compression if needed.
-## 11. Viewing Haskell Dependency Docs from Nix
+## 13. Viewing Haskell Dependency Docs from Nix
When you need Haddock documentation for a Haskell dependency available in Nixpkgs, build the package's `doc` output directly with `^doc`.
diff --git a/ext/zig/.gitignore b/ext/zig/.gitignore
@@ -0,0 +1,13 @@
+# Zig build artifacts
+.zig-cache/
+zig-out/
+
+# Generated binaries (keep .c sources, ignore compiled artifacts)
+/c_abi_test
+/c_abi_append_test
+c_abi_append_shared
+tests/c_abi_append_test
+
+# Temp files
+*.o
+*.tmp
diff --git a/ext/zig/build.zig b/ext/zig/build.zig
@@ -0,0 +1,67 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ // -- kernel generator tool (runs on build host) --
+ const gen_kernel_mod = b.createModule(.{
+ .root_source_file = b.path("tools/gen_kernel.zig"),
+ .target = b.graph.host,
+ .optimize = .ReleaseSafe,
+ });
+ const gen_kernel = b.addExecutable(.{
+ .name = "gen_kernel",
+ .root_module = gen_kernel_mod,
+ });
+
+ const run_gen_kernel = b.addRunArtifact(gen_kernel);
+ run_gen_kernel.addFileArg(b.path("kernel_run_arboricx_typed.dag"));
+ const kernel_embed = run_gen_kernel.addOutputFileArg("kernel_embed.zig");
+
+ // -- kernel module shared by exe and lib --
+ const kernel_mod = b.createModule(.{
+ .root_source_file = kernel_embed,
+ });
+
+ // -- main CLI executable --
+ const exe_mod = b.createModule(.{
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ exe_mod.addImport("kernel_embed", kernel_mod);
+ const exe = b.addExecutable(.{
+ .name = "tricu-zig",
+ .root_module = exe_mod,
+ });
+ b.installArtifact(exe);
+
+ const run_cmd = b.addRunArtifact(exe);
+ run_cmd.step.dependOn(b.getInstallStep());
+ const run_step = b.step("run", "Run tricu-zig");
+ run_step.dependOn(&run_cmd.step);
+
+ // -- C ABI static library --
+ const lib_mod = b.createModule(.{
+ .root_source_file = b.path("src/c_abi.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ lib_mod.pic = true;
+ lib_mod.addImport("kernel_embed", kernel_mod);
+ const static_lib = b.addLibrary(.{
+ .name = "arboricx",
+ .root_module = lib_mod,
+ });
+ b.installArtifact(static_lib);
+
+ // -- C ABI shared library (for dynamic language FFI) --
+ const shared_lib = b.addLibrary(.{
+ .name = "arboricx",
+ .root_module = lib_mod,
+ .linkage = .dynamic,
+ });
+ b.installArtifact(shared_lib);
+
+}
diff --git a/ext/zig/build.zig.zon b/ext/zig/build.zig.zon
@@ -0,0 +1,13 @@
+.{
+ .name = .tricu_zig,
+ .version = "0.0.1",
+ .fingerprint = 0xa9aedd8049d1cce9,
+ .minimum_zig_version = "0.16.0",
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ "tools",
+ "kernels",
+ },
+}
diff --git a/ext/zig/include/arboricx.h b/ext/zig/include/arboricx.h
@@ -0,0 +1,54 @@
+#ifndef ARBORICX_H
+#define ARBORICX_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct arb_ctx arb_ctx_t;
+
+/* Context lifecycle */
+arb_ctx_t* arboricx_init(void);
+void arboricx_free(arb_ctx_t* ctx);
+void arboricx_free_buf(arb_ctx_t* ctx, uint8_t* ptr, size_t len);
+
+/* Tree construction */
+uint32_t arb_leaf(arb_ctx_t* ctx);
+uint32_t arb_stem(arb_ctx_t* ctx, uint32_t child);
+uint32_t arb_fork(arb_ctx_t* ctx, uint32_t left, uint32_t right);
+uint32_t arb_app(arb_ctx_t* ctx, uint32_t func, uint32_t arg);
+
+/* Reduction */
+uint32_t arb_reduce(arb_ctx_t* ctx, uint32_t root, uint64_t fuel);
+
+/* Codec constructors */
+uint32_t arb_of_number(arb_ctx_t* ctx, uint64_t n);
+uint32_t arb_of_string(arb_ctx_t* ctx, const char* s);
+uint32_t arb_of_bytes(arb_ctx_t* ctx, const uint8_t* bytes, size_t len);
+uint32_t arb_of_list(arb_ctx_t* ctx, const uint32_t* items, size_t len);
+
+/* Codec destructors (return 1 on success, 0 on failure) */
+int arb_to_number(arb_ctx_t* ctx, uint32_t root, uint64_t* out);
+int arb_to_string(arb_ctx_t* ctx, uint32_t root, uint8_t** out_ptr, size_t* out_len);
+int arb_to_bytes(arb_ctx_t* ctx, uint32_t root, uint8_t** out_ptr, size_t* out_len);
+int arb_to_bool(arb_ctx_t* ctx, uint32_t root, int* out);
+
+/* Result unwrapping (return 1 on success, 0 on failure) */
+int arb_unwrap_result(arb_ctx_t* ctx, uint32_t root, int* out_ok, uint32_t* out_value, uint32_t* out_rest);
+int arb_unwrap_host_value(arb_ctx_t* ctx, uint32_t root, uint64_t* out_tag, uint32_t* out_payload);
+
+/* Kernel entrypoints */
+uint32_t arb_kernel_root(arb_ctx_t* ctx);
+
+/* Native bundle loading (fast path — bypasses the Tricu kernel) */
+uint32_t arb_load_bundle(arb_ctx_t* ctx, const uint8_t* bytes, size_t len, const char* name);
+uint32_t arb_load_bundle_default(arb_ctx_t* ctx, const uint8_t* bytes, size_t len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ARBORICX_H */
diff --git a/ext/zig/kernel_run_arboricx_typed.dag b/ext/zig/kernel_run_arboricx_typed.dag
@@ -0,0 +1,2694 @@
+2692
+leaf
+fork 0 0
+stem 1
+fork 2 0
+fork 0 3
+stem 4
+stem 0
+fork 5 6
+fork 0 7
+stem 8
+stem 6
+fork 10 0
+stem 11
+fork 12 11
+stem 13
+stem 14
+fork 0 15
+stem 16
+fork 17 6
+stem 18
+fork 0 1
+stem 20
+fork 0 21
+stem 22
+stem 23
+fork 0 24
+stem 25
+fork 0 6
+stem 27
+fork 28 3
+fork 5 29
+stem 30
+fork 31 27
+stem 32
+stem 33
+fork 0 34
+stem 35
+fork 36 6
+fork 0 37
+fork 33 38
+fork 0 39
+stem 40
+stem 38
+stem 42
+fork 0 43
+stem 44
+stem 26
+fork 0 46
+stem 47
+stem 12
+fork 0 49
+stem 50
+fork 51 6
+fork 42 52
+stem 53
+stem 9
+fork 0 55
+stem 56
+fork 9 37
+stem 58
+fork 33 1
+fork 0 60
+stem 61
+stem 2
+fork 0 63
+stem 64
+fork 42 6
+stem 66
+fork 67 1
+fork 65 68
+fork 62 69
+fork 0 70
+fork 59 71
+fork 0 72
+stem 73
+stem 74
+fork 0 75
+stem 76
+fork 77 52
+fork 57 78
+fork 54 79
+fork 42 80
+stem 81
+stem 82
+fork 0 83
+stem 84
+stem 57
+fork 0 86
+stem 87
+stem 45
+fork 88 89
+fork 85 90
+fork 48 91
+fork 45 92
+fork 41 93
+fork 26 94
+fork 0 95
+stem 96
+stem 37
+fork 0 18
+fork 98 99
+fork 97 100
+fork 0 101
+fork 19 102
+fork 9 103
+stem 104
+fork 1 0
+stem 106
+fork 0 107
+stem 108
+fork 65 52
+fork 5 110
+fork 9 111
+fork 0 112
+fork 98 113
+fork 109 114
+fork 0 115
+stem 116
+fork 117 100
+fork 0 118
+fork 19 119
+stem 120
+fork 1 6
+fork 2 122
+stem 123
+fork 0 122
+stem 125
+fork 0 11
+fork 1 127
+fork 126 128
+fork 124 129
+fork 0 130
+fork 121 131
+fork 0 132
+fork 105 133
+fork 9 134
+fork 9 135
+stem 136
+fork 6 1
+fork 138 20
+stem 139
+fork 0 140
+stem 141
+stem 142
+fork 0 143
+stem 144
+stem 28
+fork 0 146
+stem 147
+fork 0 10
+stem 149
+stem 150
+fork 0 151
+stem 152
+fork 153 0
+fork 0 154
+stem 155
+fork 156 6
+fork 0 157
+fork 33 158
+fork 0 159
+stem 160
+fork 0 2
+stem 162
+stem 163
+fork 0 164
+stem 165
+stem 5
+fork 0 167
+stem 168
+fork 28 6
+fork 42 170
+stem 171
+fork 65 0
+fork 0 173
+fork 67 174
+fork 172 175
+fork 0 176
+stem 177
+stem 178
+fork 0 179
+stem 180
+stem 181
+fork 169 182
+fork 5 183
+stem 184
+fork 148 0
+fork 0 186
+fork 185 187
+fork 42 188
+stem 189
+fork 0 58
+fork 190 191
+fork 166 192
+fork 163 193
+fork 0 194
+fork 98 195
+fork 0 196
+stem 197
+stem 198
+fork 0 199
+stem 200
+fork 6 0
+fork 0 202
+fork 0 203
+fork 0 204
+fork 0 205
+fork 0 206
+fork 6 207
+fork 6 203
+fork 0 209
+fork 0 210
+fork 6 211
+fork 0 212
+fork 6 206
+fork 0 214
+fork 6 204
+fork 6 216
+fork 6 217
+fork 6 218
+fork 0 216
+fork 0 220
+fork 6 221
+fork 6 214
+fork 6 209
+fork 0 224
+fork 0 225
+fork 0 226
+fork 227 0
+fork 223 228
+fork 222 229
+fork 213 230
+fork 219 231
+fork 215 232
+fork 213 233
+fork 208 234
+fork 0 235
+fork 33 236
+fork 0 237
+stem 238
+stem 173
+fork 240 1
+fork 0 241
+stem 242
+fork 150 6
+fork 243 244
+fork 9 245
+stem 246
+fork 202 0
+fork 0 248
+fork 249 0
+fork 250 154
+fork 42 251
+fork 9 252
+fork 9 253
+stem 254
+stem 166
+fork 0 256
+stem 257
+fork 33 191
+fork 0 259
+stem 260
+fork 0 184
+fork 98 262
+fork 45 263
+fork 261 264
+fork 258 265
+fork 166 266
+fork 0 267
+stem 268
+fork 98 187
+fork 148 270
+fork 269 271
+fork 9 272
+fork 9 273
+stem 274
+stem 203
+fork 28 276
+fork 42 277
+fork 9 278
+stem 279
+fork 0 182
+stem 281
+stem 247
+fork 0 283
+stem 284
+stem 275
+fork 0 286
+stem 287
+fork 2 140
+fork 0 289
+stem 290
+fork 0 20
+fork 33 292
+fork 0 293
+stem 294
+fork 2 6
+fork 0 296
+stem 297
+stem 298
+fork 295 299
+fork 291 300
+stem 301
+fork 42 43
+stem 303
+fork 0 127
+fork 128 305
+fork 0 306
+stem 307
+stem 308
+fork 0 309
+stem 310
+stem 311
+fork 88 312
+fork 45 313
+fork 304 314
+fork 48 315
+fork 302 316
+fork 0 317
+stem 318
+fork 319 100
+fork 0 320
+fork 19 321
+fork 0 322
+fork 98 323
+fork 45 324
+fork 42 325
+fork 0 326
+fork 33 327
+fork 0 328
+stem 329
+fork 0 89
+stem 331
+stem 280
+fork 0 333
+stem 334
+stem 282
+fork 0 336
+stem 337
+fork 98 191
+fork 338 339
+fork 335 340
+fork 332 341
+fork 45 342
+fork 330 343
+fork 288 344
+fork 42 345
+stem 346
+fork 0 254
+fork 347 348
+fork 285 349
+fork 42 350
+stem 351
+fork 352 191
+fork 0 353
+stem 354
+fork 355 100
+fork 0 356
+fork 19 357
+fork 0 358
+fork 59 359
+fork 282 360
+fork 280 361
+fork 45 362
+fork 42 363
+stem 364
+fork 365 327
+fork 275 366
+fork 0 367
+fork 255 368
+fork 247 369
+fork 239 370
+stem 371
+fork 372 11
+fork 198 373
+stem 374
+fork 9 58
+fork 9 376
+stem 377
+stem 378
+fork 0 379
+stem 380
+stem 169
+fork 0 382
+stem 383
+stem 384
+fork 0 385
+stem 386
+stem 243
+fork 0 388
+stem 389
+stem 390
+fork 0 391
+stem 392
+stem 393
+fork 0 394
+stem 395
+stem 396
+fork 0 397
+stem 398
+fork 11 0
+stem 400
+fork 0 401
+stem 402
+fork 9 0
+fork 9 404
+fork 0 405
+fork 98 406
+fork 403 407
+fork 0 408
+stem 409
+fork 410 100
+fork 0 411
+fork 19 412
+fork 0 413
+stem 414
+stem 415
+fork 42 416
+stem 417
+fork 42 138
+fork 0 419
+fork 418 420
+fork 163 421
+fork 0 422
+stem 423
+fork 424 100
+fork 0 425
+fork 19 426
+fork 156 427
+fork 0 428
+fork 98 429
+fork 181 430
+fork 42 431
+stem 432
+stem 202
+fork 28 434
+fork 42 435
+fork 0 436
+fork 433 437
+fork 42 438
+fork 9 439
+stem 440
+fork 57 325
+fork 0 442
+fork 441 443
+fork 399 444
+fork 387 445
+fork 384 446
+fork 169 447
+fork 5 448
+stem 449
+stem 450
+fork 0 451
+stem 452
+fork 57 442
+fork 57 454
+fork 5 455
+stem 456
+stem 457
+fork 0 458
+stem 459
+stem 460
+fork 0 461
+stem 462
+fork 98 1
+fork 45 464
+fork 0 465
+fork 33 466
+fork 0 467
+stem 468
+stem 469
+fork 0 470
+stem 471
+stem 472
+fork 0 473
+stem 474
+stem 88
+fork 0 476
+stem 477
+stem 332
+fork 0 479
+stem 480
+stem 41
+fork 0 482
+stem 483
+stem 248
+fork 0 485
+stem 486
+stem 10
+fork 2 488
+fork 0 489
+stem 490
+fork 28 146
+fork 491 492
+stem 493
+fork 494 1
+fork 487 495
+fork 0 496
+stem 497
+fork 498 100
+fork 0 499
+fork 19 500
+fork 42 501
+fork 0 502
+fork 33 503
+fork 0 504
+stem 505
+fork 98 38
+fork 45 507
+fork 506 508
+fork 332 509
+fork 484 510
+fork 481 511
+fork 478 512
+fork 475 513
+fork 42 514
+stem 515
+fork 57 431
+fork 57 517
+fork 5 518
+fork 9 519
+fork 9 520
+fork 9 521
+fork 0 522
+fork 516 523
+fork 463 524
+fork 453 525
+fork 381 526
+fork 0 527
+stem 528
+fork 529 100
+fork 0 530
+fork 19 531
+stem 532
+fork 533 204
+stem 534
+fork 535 1
+stem 536
+fork 537 11
+stem 538
+fork 539 1
+fork 198 540
+stem 541
+fork 533 205
+stem 543
+fork 544 1
+stem 545
+fork 546 11
+stem 547
+fork 548 1
+fork 198 549
+fork 0 550
+fork 33 551
+fork 0 552
+stem 553
+fork 533 206
+stem 555
+fork 556 1
+stem 557
+fork 558 11
+stem 559
+fork 560 1
+fork 198 561
+fork 0 562
+fork 33 563
+fork 0 564
+stem 565
+stem 566
+fork 0 567
+stem 568
+stem 569
+fork 0 570
+stem 571
+stem 156
+fork 0 573
+stem 574
+stem 575
+fork 0 576
+stem 577
+stem 578
+fork 0 579
+stem 580
+stem 581
+fork 0 582
+stem 583
+fork 9 405
+fork 9 585
+stem 586
+stem 585
+stem 405
+fork 589 1
+fork 0 590
+fork 588 591
+fork 0 592
+fork 587 593
+fork 584 594
+fork 481 595
+fork 572 596
+fork 332 597
+fork 569 598
+fork 45 599
+fork 554 600
+fork 42 601
+stem 602
+fork 0 541
+fork 603 604
+fork 0 605
+fork 542 606
+fork 0 607
+fork 0 608
+fork 375 609
+fork 198 610
+stem 611
+fork 5 325
+stem 613
+stem 614
+fork 0 615
+stem 616
+stem 617
+fork 0 618
+stem 619
+fork 5 431
+fork 9 621
+fork 9 622
+stem 623
+stem 624
+fork 0 625
+stem 626
+fork 88 510
+fork 472 628
+fork 42 629
+stem 630
+stem 550
+fork 33 604
+fork 0 633
+stem 634
+stem 635
+fork 0 636
+stem 637
+stem 638
+fork 0 639
+stem 640
+stem 572
+fork 0 642
+stem 643
+stem 481
+fork 0 645
+stem 646
+stem 644
+fork 0 648
+stem 649
+stem 647
+fork 0 651
+stem 652
+fork 0 207
+fork 533 654
+stem 655
+fork 656 1
+stem 657
+fork 658 11
+stem 659
+fork 660 1
+fork 198 661
+fork 0 662
+fork 33 663
+fork 0 664
+stem 665
+stem 666
+fork 0 667
+stem 668
+stem 669
+fork 0 670
+stem 671
+stem 672
+fork 0 673
+stem 674
+stem 675
+fork 0 676
+stem 677
+stem 678
+fork 0 679
+stem 680
+stem 653
+fork 0 682
+stem 683
+stem 584
+fork 0 685
+stem 686
+stem 687
+fork 0 688
+stem 689
+stem 690
+fork 0 691
+stem 692
+fork 9 586
+fork 9 694
+fork 9 695
+stem 696
+stem 695
+stem 694
+fork 0 594
+fork 699 700
+fork 0 701
+fork 698 702
+fork 0 703
+fork 697 704
+fork 693 705
+fork 684 706
+fork 681 707
+fork 653 708
+fork 650 709
+fork 647 710
+fork 644 711
+fork 481 712
+fork 641 713
+fork 332 714
+fork 638 715
+fork 45 716
+fork 635 717
+fork 42 718
+stem 719
+fork 720 604
+fork 0 721
+fork 632 722
+fork 198 723
+fork 9 724
+fork 9 725
+fork 9 726
+fork 0 727
+fork 631 728
+fork 627 729
+fork 620 730
+fork 0 731
+stem 732
+fork 733 100
+fork 0 734
+fork 19 735
+fork 0 736
+fork 98 737
+fork 62 738
+fork 62 739
+fork 0 740
+stem 741
+stem 403
+fork 0 743
+stem 744
+fork 88 507
+fork 45 746
+fork 5 747
+stem 748
+fork 0 508
+fork 749 750
+fork 745 751
+fork 42 752
+stem 753
+fork 754 38
+fork 0 755
+stem 756
+fork 757 100
+fork 0 758
+fork 19 759
+stem 760
+stem 322
+fork 762 206
+fork 0 763
+fork 33 764
+fork 0 765
+stem 766
+stem 767
+fork 0 768
+stem 769
+fork 9 176
+stem 771
+stem 772
+fork 0 773
+stem 774
+stem 775
+fork 0 776
+stem 777
+stem 67
+fork 0 779
+stem 780
+stem 174
+stem 782
+fork 0 783
+stem 784
+stem 785
+fork 0 786
+stem 787
+fork 240 20
+stem 789
+fork 790 292
+fork 0 791
+stem 792
+fork 793 6
+stem 794
+stem 127
+fork 762 1
+fork 796 797
+fork 795 798
+fork 0 799
+stem 800
+stem 801
+fork 0 802
+stem 803
+stem 804
+fork 0 805
+stem 806
+fork 0 501
+fork 33 808
+fork 0 809
+stem 810
+fork 12 1
+fork 57 812
+fork 811 813
+fork 807 814
+fork 788 815
+fork 781 816
+fork 169 817
+fork 5 818
+stem 819
+stem 793
+fork 0 821
+stem 822
+fork 823 0
+fork 5 824
+stem 825
+fork 308 797
+fork 9 827
+stem 828
+fork 0 797
+fork 829 830
+fork 826 831
+fork 9 832
+fork 0 833
+fork 33 834
+fork 0 835
+stem 836
+stem 837
+fork 0 838
+stem 839
+fork 840 510
+fork 820 841
+fork 42 842
+stem 843
+fork 844 191
+fork 778 845
+fork 384 846
+fork 770 847
+fork 0 848
+stem 849
+fork 850 100
+fork 0 851
+fork 19 852
+fork 62 853
+fork 0 854
+fork 98 855
+fork 0 856
+fork 761 857
+stem 858
+fork 859 1
+fork 0 860
+stem 861
+fork 1 125
+fork 0 863
+fork 1 864
+fork 862 865
+fork 742 866
+stem 867
+stem 868
+fork 0 869
+stem 870
+stem 59
+fork 0 872
+stem 873
+stem 258
+fork 0 875
+stem 876
+stem 148
+fork 0 878
+stem 879
+stem 880
+fork 0 881
+stem 882
+fork 42 509
+stem 884
+fork 9 771
+fork 9 886
+fork 0 887
+fork 885 888
+fork 620 889
+fork 883 890
+fork 877 891
+fork 874 892
+fork 0 893
+stem 894
+fork 895 100
+fork 0 896
+fork 19 897
+fork 0 898
+fork 98 899
+fork 62 900
+fork 0 901
+stem 902
+fork 0 128
+fork 1 904
+fork 0 905
+fork 1 906
+fork 0 907
+fork 1 908
+fork 862 909
+fork 903 910
+fork 0 911
+fork 98 912
+fork 871 913
+fork 201 914
+fork 5 915
+stem 916
+fork 575 0
+fork 42 918
+fork 9 919
+stem 920
+fork 6 27
+fork 0 27
+fork 922 923
+fork 178 924
+stem 925
+fork 923 923
+fork 926 927
+fork 0 928
+fork 98 929
+fork 0 930
+stem 931
+stem 932
+fork 0 933
+stem 934
+fork 42 122
+stem 936
+fork 5 508
+stem 938
+fork 202 20
+fork 243 940
+stem 941
+stem 942
+fork 0 943
+stem 944
+fork 311 322
+fork 57 946
+fork 42 947
+fork 0 948
+fork 304 949
+fork 258 950
+fork 42 951
+stem 952
+fork 953 191
+fork 945 954
+fork 42 955
+stem 956
+fork 957 38
+fork 0 958
+stem 959
+fork 960 100
+fork 0 961
+fork 19 962
+fork 9 963
+stem 964
+fork 965 125
+fork 935 966
+fork 57 967
+fork 0 968
+fork 939 969
+fork 166 970
+fork 42 971
+stem 972
+fork 973 38
+fork 0 974
+stem 975
+fork 976 100
+fork 0 977
+fork 19 978
+fork 0 979
+fork 937 980
+fork 935 981
+fork 5 982
+fork 0 983
+fork 98 984
+fork 163 985
+fork 0 986
+stem 987
+fork 988 100
+fork 0 989
+fork 19 990
+fork 42 991
+stem 992
+fork 6 202
+fork 0 994
+stem 995
+fork 28 996
+fork 178 997
+stem 998
+stem 999
+fork 0 1000
+stem 1001
+fork 1002 154
+fork 993 1003
+fork 201 1004
+fork 0 1005
+fork 921 1006
+fork 0 1007
+fork 917 1008
+fork 148 1009
+fork 42 1010
+fork 9 1011
+stem 1012
+stem 948
+fork 42 963
+fork 0 1015
+fork 1014 1016
+fork 166 1017
+fork 0 1018
+fork 59 1019
+fork 942 1020
+stem 1021
+fork 0 249
+fork 1022 1023
+fork 0 1024
+stem 1025
+fork 1026 122
+fork 308 1027
+stem 1028
+fork 1022 292
+fork 0 1030
+stem 1031
+fork 1032 863
+fork 1029 1033
+fork 42 1034
+stem 1035
+fork 181 154
+fork 5 1037
+stem 1038
+stem 209
+fork 28 1040
+fork 0 1041
+fork 1039 1042
+fork 1036 1043
+fork 201 1044
+fork 0 1045
+fork 1013 1046
+fork 612 1047
+fork 198 1048
+stem 1049
+stem 201
+fork 0 1051
+stem 1052
+fork 45 966
+fork 5 1054
+stem 1055
+stem 1056
+fork 0 1057
+stem 1058
+fork 178 0
+fork 9 1060
+fork 0 1061
+fork 33 1062
+fork 0 1063
+stem 1064
+fork 1065 508
+fork 1059 1066
+fork 166 1067
+fork 42 1068
+stem 1069
+fork 1070 38
+fork 0 1071
+stem 1072
+fork 1073 100
+fork 0 1074
+fork 19 1075
+stem 1076
+fork 0 1023
+fork 0 1078
+fork 1077 1079
+fork 42 1080
+fork 9 1081
+stem 1082
+stem 204
+fork 28 1084
+fork 782 1085
+stem 1086
+stem 1087
+fork 0 1088
+stem 1089
+fork 0 865
+fork 1 1091
+fork 0 1092
+fork 1 1093
+fork 0 1094
+fork 1 1095
+fork 0 1096
+fork 1 1097
+fork 862 1098
+fork 42 1099
+stem 1100
+stem 323
+stem 808
+stem 1103
+fork 28 1104
+fork 163 1105
+fork 0 1106
+stem 1107
+fork 1108 100
+fork 0 1109
+fork 19 1110
+fork 1102 1111
+fork 0 1112
+fork 1101 1113
+fork 42 1114
+stem 1115
+stem 436
+fork 98 155
+fork 181 1118
+fork 1117 1119
+fork 5 1120
+fork 0 1121
+fork 1116 1122
+fork 42 1123
+stem 1124
+fork 42 901
+stem 1126
+fork 5 442
+stem 1128
+stem 1129
+fork 0 1130
+stem 1131
+stem 823
+fork 0 1133
+stem 1134
+stem 1135
+fork 0 1136
+stem 1137
+stem 1138
+fork 0 1139
+stem 1140
+fork 0 404
+fork 33 1142
+fork 0 1143
+stem 1144
+stem 1145
+fork 0 1146
+stem 1147
+fork 1148 510
+fork 1141 1149
+fork 1132 1150
+fork 258 1151
+fork 42 1152
+stem 1153
+fork 1154 191
+fork 0 1155
+stem 1156
+fork 1157 100
+fork 0 1158
+fork 19 1159
+fork 0 1160
+fork 98 1161
+fork 62 1162
+fork 9 1163
+fork 0 1164
+fork 1127 1165
+fork 0 1166
+stem 1167
+fork 862 1096
+fork 1168 1169
+stem 1170
+fork 1171 1099
+fork 45 1172
+fork 1125 1173
+fork 0 1174
+fork 98 1175
+fork 42 1176
+stem 1177
+fork 1178 38
+fork 1090 1179
+fork 5 1180
+stem 1181
+fork 28 1085
+fork 28 1183
+fork 0 1184
+fork 1182 1185
+fork 0 1186
+fork 1083 1187
+fork 0 1188
+fork 98 1189
+fork 42 1190
+stem 1191
+fork 1192 38
+fork 1053 1193
+fork 169 1194
+fork 5 1195
+stem 1196
+fork 0 918
+fork 59 1198
+fork 880 1199
+fork 42 1200
+stem 1201
+stem 1202
+fork 0 1203
+stem 1204
+fork 203 0
+fork 0 1206
+fork 0 1207
+fork 0 1208
+fork 0 1209
+fork 1077 1210
+fork 42 1211
+fork 9 1212
+stem 1213
+fork 1214 1187
+fork 0 1215
+fork 98 1216
+fork 42 1217
+stem 1218
+fork 1219 38
+fork 1053 1220
+fork 88 1221
+fork 1205 1222
+fork 883 1223
+fork 1197 1224
+fork 148 1225
+fork 166 1226
+fork 42 1227
+stem 1228
+fork 1229 38
+fork 1050 1230
+fork 198 1231
+stem 1232
+fork 575 464
+fork 0 1234
+fork 59 1235
+fork 880 1236
+fork 42 1237
+fork 9 1238
+stem 1239
+fork 0 963
+stem 1241
+fork 1242 122
+stem 1243
+fork 0 995
+fork 0 1245
+fork 0 1246
+fork 6 1247
+fork 6 994
+fork 0 1249
+fork 0 1250
+fork 6 1251
+fork 0 1252
+fork 6 1246
+fork 0 1254
+fork 6 995
+fork 6 1256
+fork 6 1257
+fork 6 1258
+fork 0 1256
+fork 0 1260
+fork 6 1261
+fork 6 1254
+fork 6 1249
+fork 0 1264
+fork 0 1265
+fork 0 1266
+fork 6 224
+fork 0 1268
+fork 6 1250
+fork 0 1270
+fork 6 1271
+fork 0 1258
+fork 6 1245
+fork 0 1274
+fork 0 1275
+fork 0 1257
+fork 0 1277
+fork 6 1275
+fork 6 1277
+fork 6 1274
+fork 0 1281
+fork 6 1252
+fork 0 1271
+fork 6 1270
+fork 0 1285
+fork 1254 0
+fork 1286 1287
+fork 1269 1288
+fork 1284 1289
+fork 1283 1290
+fork 1279 1291
+fork 1282 1292
+fork 1262 1293
+fork 1273 1294
+fork 1248 1295
+fork 1280 1296
+fork 1269 1297
+fork 1279 1298
+fork 1278 1299
+fork 1276 1300
+fork 1273 1301
+fork 1272 1302
+fork 1255 1303
+fork 1269 1304
+fork 1267 1305
+fork 1263 1306
+fork 1262 1307
+fork 1253 1308
+fork 1259 1309
+fork 1255 1310
+fork 1253 1311
+fork 1248 1312
+fork 0 1313
+fork 1244 1314
+fork 308 1315
+stem 1316
+fork 1242 129
+stem 1318
+fork 6 225
+fork 6 1260
+fork 0 1321
+fork 1284 0
+fork 1263 1323
+fork 1279 1324
+fork 1322 1325
+fork 1255 1326
+fork 1259 1327
+fork 1320 1328
+fork 1279 1329
+fork 1278 1330
+fork 1255 1331
+fork 1248 1332
+fork 1284 1333
+fork 1272 1334
+fork 1263 1335
+fork 1279 1336
+fork 1267 1337
+fork 1279 1338
+fork 1320 1339
+fork 1283 1340
+fork 1272 1341
+fork 1278 1342
+fork 1272 1343
+fork 1263 1344
+fork 1278 1345
+fork 1248 1346
+fork 1263 1347
+fork 1320 1348
+fork 1279 1349
+fork 1279 1350
+fork 1253 1351
+fork 1284 1352
+fork 0 1353
+fork 1319 1354
+fork 308 1355
+stem 1356
+stem 904
+fork 1358 128
+fork 126 1359
+fork 1242 1360
+stem 1361
+fork 1283 1289
+fork 1272 1363
+fork 1278 1364
+fork 1272 1365
+fork 1263 1366
+fork 1278 1367
+fork 1248 1368
+fork 1263 1369
+fork 1320 1370
+fork 1279 1371
+fork 1279 1372
+fork 1253 1373
+fork 1284 1374
+fork 0 1375
+fork 1362 1376
+fork 308 1377
+stem 1378
+fork 1358 1359
+fork 126 1380
+fork 1242 1381
+stem 1382
+fork 0 1261
+fork 1277 0
+fork 1321 1385
+fork 1275 1386
+fork 1248 1387
+fork 1384 1388
+fork 1283 1389
+fork 0 1390
+fork 1383 1391
+fork 308 1392
+stem 1393
+fork 1358 1380
+fork 126 1395
+fork 1242 1396
+stem 1397
+fork 6 1321
+fork 1279 1289
+fork 1276 1400
+fork 1259 1401
+fork 1273 1402
+fork 1269 1403
+fork 1279 1404
+fork 1278 1405
+fork 1399 1406
+fork 1253 1407
+fork 1279 1408
+fork 1280 1409
+fork 1269 1410
+fork 1267 1411
+fork 1263 1412
+fork 1262 1413
+fork 1253 1414
+fork 1259 1415
+fork 1255 1416
+fork 1253 1417
+fork 1248 1418
+fork 0 1419
+fork 1398 1420
+fork 308 1421
+stem 1422
+fork 1358 1395
+fork 126 1424
+fork 1242 1425
+stem 1426
+fork 0 1251
+fork 0 1428
+fork 6 1266
+fork 1276 1289
+fork 1248 1431
+fork 1259 1432
+fork 1278 1433
+fork 1430 1434
+fork 1248 1435
+fork 1429 1436
+fork 1269 1437
+fork 1279 1438
+fork 1278 1439
+fork 1399 1440
+fork 1253 1441
+fork 1279 1442
+fork 1280 1443
+fork 1269 1444
+fork 1267 1445
+fork 1263 1446
+fork 1262 1447
+fork 1253 1448
+fork 1259 1449
+fork 1255 1450
+fork 1253 1451
+fork 1248 1452
+fork 0 1453
+fork 1427 1454
+fork 308 1455
+stem 1456
+fork 1358 1424
+fork 126 1458
+fork 1242 1459
+stem 1460
+fork 1461 1376
+fork 308 1462
+stem 1463
+fork 1358 1458
+fork 126 1465
+fork 1242 1466
+stem 1467
+fork 1253 0
+fork 1279 1469
+fork 1276 1470
+fork 1253 1471
+fork 1259 1472
+fork 1320 1473
+fork 1278 1474
+fork 1248 1475
+fork 1280 1476
+fork 1253 1477
+fork 1259 1478
+fork 1273 1479
+fork 0 1480
+fork 1468 1481
+fork 308 1482
+stem 1483
+fork 1358 1465
+fork 126 1485
+fork 1242 1486
+stem 1487
+fork 1279 1400
+fork 1253 1489
+fork 1284 1490
+fork 1269 1491
+fork 1262 1492
+fork 1255 1493
+fork 1248 1494
+fork 1269 1495
+fork 1267 1496
+fork 1263 1497
+fork 1262 1498
+fork 1253 1499
+fork 1259 1500
+fork 1255 1501
+fork 1253 1502
+fork 1248 1503
+fork 0 1504
+fork 1488 1505
+fork 308 1506
+stem 1507
+fork 1358 1485
+fork 1358 1509
+fork 126 1510
+fork 1242 1511
+stem 1512
+fork 1513 20
+fork 308 1514
+stem 1515
+fork 0 139
+stem 1517
+fork 0 940
+stem 1519
+fork 1358 1510
+fork 126 1521
+fork 1520 1522
+fork 1518 1523
+fork 308 1524
+stem 1525
+fork 1358 1521
+fork 1520 1527
+fork 1518 1528
+fork 1526 1529
+fork 1516 1530
+fork 1508 1531
+fork 1484 1532
+fork 1464 1533
+fork 1457 1534
+fork 1423 1535
+fork 1394 1536
+fork 1379 1537
+fork 1357 1538
+fork 1317 1539
+fork 42 1540
+stem 1541
+stem 1256
+fork 28 1543
+fork 0 1544
+fork 1039 1545
+fork 1542 1546
+fork 0 1547
+fork 98 1548
+fork 201 1549
+fork 57 1550
+fork 0 1551
+fork 1240 1552
+fork 166 1553
+fork 42 1554
+stem 1555
+fork 1556 38
+fork 42 1557
+stem 1558
+fork 0 217
+fork 6 1560
+fork 0 218
+fork 6 205
+fork 6 1563
+fork 0 1564
+fork 6 212
+fork 6 210
+fork 0 1567
+fork 0 1568
+fork 1569 0
+fork 1566 1570
+fork 1565 1571
+fork 1562 1572
+fork 1561 1573
+fork 215 1574
+fork 213 1575
+fork 208 1576
+fork 0 1577
+fork 33 1578
+fork 0 1579
+stem 1580
+fork 1581 370
+stem 1582
+fork 1583 11
+fork 198 1584
+stem 1585
+fork 33 155
+fork 0 1587
+stem 1588
+fork 0 532
+fork 98 1590
+fork 62 1591
+fork 5 1592
+stem 1593
+fork 1594 127
+fork 62 1595
+fork 0 1596
+stem 1597
+fork 1598 860
+fork 201 1599
+fork 1589 1600
+fork 0 1601
+fork 632 1602
+fork 198 1603
+stem 1604
+fork 0 1604
+fork 33 1606
+fork 0 1607
+stem 1608
+stem 1609
+fork 0 1610
+stem 1611
+stem 1612
+fork 0 1613
+stem 1614
+stem 1615
+fork 0 1616
+stem 1617
+stem 1618
+fork 0 1619
+stem 1620
+stem 1621
+fork 0 1622
+stem 1623
+stem 1624
+fork 0 1625
+stem 1626
+stem 684
+fork 0 1628
+stem 1629
+stem 554
+fork 0 1631
+stem 1632
+stem 1633
+fork 0 1634
+stem 1635
+stem 1636
+fork 0 1637
+stem 1638
+stem 1639
+fork 0 1640
+stem 1641
+stem 1642
+fork 0 1643
+stem 1644
+stem 1645
+fork 0 1646
+stem 1647
+stem 1648
+fork 0 1649
+stem 1650
+stem 1630
+fork 0 1652
+stem 1653
+fork 9 1604
+fork 9 1655
+fork 9 1656
+fork 0 1657
+fork 631 1658
+fork 627 1659
+fork 620 1660
+fork 0 1661
+stem 1662
+fork 1663 100
+fork 0 1664
+fork 19 1665
+fork 0 1666
+fork 98 1667
+fork 62 1668
+fork 62 1669
+fork 0 1670
+stem 1671
+fork 1672 860
+fork 201 1673
+fork 0 1674
+fork 33 1675
+fork 0 1676
+stem 1677
+stem 1678
+fork 0 1679
+stem 1680
+stem 1681
+fork 0 1682
+stem 1683
+stem 1684
+fork 0 1685
+stem 1686
+stem 1687
+fork 0 1688
+stem 1689
+stem 1690
+fork 0 1691
+stem 1692
+stem 1693
+fork 0 1694
+stem 1695
+stem 1696
+fork 0 1697
+stem 1698
+stem 478
+fork 0 1700
+stem 1701
+stem 1702
+fork 0 1703
+stem 1704
+stem 1705
+fork 0 1706
+stem 1707
+stem 1708
+fork 0 1709
+stem 1710
+stem 1711
+fork 0 1712
+stem 1713
+fork 533 203
+stem 1715
+fork 1716 1
+stem 1717
+fork 1718 11
+stem 1719
+fork 1720 1
+fork 198 1721
+fork 0 1722
+fork 33 1723
+fork 0 1724
+stem 1725
+stem 1726
+fork 0 1727
+stem 1728
+stem 1729
+fork 0 1730
+stem 1731
+stem 1732
+fork 0 1733
+stem 1734
+stem 1735
+fork 0 1736
+stem 1737
+stem 1738
+fork 0 1739
+stem 1740
+stem 1741
+fork 0 1742
+stem 1743
+stem 1744
+fork 0 1745
+stem 1746
+stem 1747
+fork 0 1748
+stem 1749
+stem 1654
+fork 0 1751
+stem 1752
+stem 1651
+fork 0 1754
+stem 1755
+stem 1756
+fork 0 1757
+stem 1758
+stem 1753
+fork 0 1760
+stem 1761
+stem 662
+stem 919
+fork 1764 1606
+fork 0 1765
+fork 1763 1766
+fork 198 1767
+fork 9 1768
+fork 9 1769
+fork 9 1770
+fork 0 1771
+fork 631 1772
+fork 627 1773
+fork 620 1774
+fork 0 1775
+stem 1776
+fork 1777 100
+fork 0 1778
+fork 19 1779
+fork 0 1780
+fork 98 1781
+fork 62 1782
+fork 62 1783
+fork 0 1784
+stem 1785
+fork 1786 860
+fork 201 1787
+fork 0 1788
+fork 33 1789
+fork 0 1790
+stem 1791
+stem 1792
+fork 0 1793
+stem 1794
+stem 1795
+fork 0 1796
+stem 1797
+stem 1798
+fork 0 1799
+stem 1800
+stem 1801
+fork 0 1802
+stem 1803
+stem 1804
+fork 0 1805
+stem 1806
+stem 1807
+fork 0 1808
+stem 1809
+stem 1810
+fork 0 1811
+stem 1812
+stem 1813
+fork 0 1814
+stem 1815
+stem 1816
+fork 0 1817
+stem 1818
+stem 1714
+fork 0 1820
+stem 1821
+stem 1822
+fork 0 1823
+stem 1824
+stem 1759
+fork 0 1826
+stem 1827
+stem 1762
+fork 0 1829
+stem 1830
+fork 581 592
+fork 332 1832
+fork 1612 1833
+fork 45 1834
+fork 1609 1835
+fork 42 1836
+stem 1837
+fork 1838 663
+fork 0 1839
+fork 1605 1840
+fork 198 1841
+fork 9 1842
+fork 9 1843
+fork 9 1844
+fork 0 1845
+fork 631 1846
+fork 627 1847
+fork 620 1848
+fork 0 1849
+stem 1850
+fork 1851 100
+fork 0 1852
+fork 19 1853
+fork 0 1854
+fork 98 1855
+fork 62 1856
+fork 62 1857
+fork 0 1858
+stem 1859
+fork 1860 860
+fork 201 1861
+fork 0 1862
+fork 33 1863
+fork 0 1864
+stem 1865
+stem 1866
+fork 0 1867
+stem 1868
+stem 1869
+fork 0 1870
+stem 1871
+stem 1872
+fork 0 1873
+stem 1874
+stem 1875
+fork 0 1876
+stem 1877
+stem 1878
+fork 0 1879
+stem 1880
+stem 1881
+fork 0 1882
+stem 1883
+stem 1884
+fork 0 1885
+stem 1886
+stem 1887
+fork 0 1888
+stem 1889
+stem 1890
+fork 0 1891
+stem 1892
+stem 1893
+fork 0 1894
+stem 1895
+stem 1825
+fork 0 1897
+stem 1898
+stem 693
+fork 0 1900
+stem 1901
+stem 1902
+fork 0 1903
+stem 1904
+stem 1905
+fork 0 1906
+stem 1907
+stem 1908
+fork 0 1909
+stem 1910
+stem 1911
+fork 0 1912
+stem 1913
+fork 9 696
+fork 9 1915
+fork 9 1916
+fork 9 1917
+fork 9 1918
+stem 1919
+stem 1918
+stem 1917
+stem 1916
+stem 1915
+fork 0 705
+fork 1924 1925
+fork 0 1926
+fork 1923 1927
+fork 0 1928
+fork 1922 1929
+fork 0 1930
+fork 1921 1931
+fork 0 1932
+fork 1920 1933
+fork 1914 1934
+fork 1831 1935
+fork 1899 1936
+fork 1896 1937
+fork 1831 1938
+fork 1828 1939
+fork 1762 1940
+fork 1825 1941
+fork 1819 1942
+fork 1762 1943
+fork 1759 1944
+fork 1753 1945
+fork 1750 1946
+fork 1654 1947
+fork 1714 1948
+fork 1699 1949
+fork 1654 1950
+fork 1651 1951
+fork 1630 1952
+fork 1627 1953
+fork 684 1954
+fork 1624 1955
+fork 653 1956
+fork 1621 1957
+fork 647 1958
+fork 1618 1959
+fork 481 1960
+fork 1615 1961
+fork 332 1962
+fork 1612 1963
+fork 45 1964
+fork 1609 1965
+fork 42 1966
+stem 1967
+fork 1968 1606
+fork 0 1969
+fork 1605 1970
+fork 0 1971
+fork 0 1972
+fork 542 1973
+fork 0 1974
+fork 0 1975
+fork 542 1976
+fork 0 1977
+fork 0 1978
+fork 1586 1979
+fork 198 1980
+stem 1981
+fork 42 860
+stem 1983
+fork 0 324
+fork 33 1985
+fork 0 1986
+stem 1987
+fork 178 244
+fork 9 1989
+fork 9 1990
+stem 1991
+stem 1992
+fork 0 1993
+stem 1994
+fork 880 509
+fork 42 1996
+stem 1997
+fork 0 1600
+fork 921 1999
+fork 42 2000
+stem 2001
+fork 2002 551
+fork 0 2003
+fork 542 2004
+fork 198 2005
+fork 9 2006
+fork 9 2007
+fork 0 2008
+fork 1998 2009
+fork 1995 2010
+fork 384 2011
+fork 169 2012
+fork 1988 2013
+fork 0 2014
+stem 2015
+fork 2016 100
+fork 0 2017
+fork 19 2018
+fork 0 2019
+fork 1984 2020
+fork 62 2021
+fork 201 2022
+fork 0 2023
+fork 33 2024
+fork 0 2025
+stem 2026
+fork 33 127
+fork 0 2028
+stem 2029
+fork 578 590
+fork 169 2031
+fork 2030 2032
+fork 880 2033
+fork 45 2034
+fork 57 2035
+fork 2027 2036
+fork 45 2037
+fork 554 2038
+fork 42 2039
+fork 9 2040
+stem 2041
+fork 9 2008
+fork 0 2043
+fork 631 2044
+fork 627 2045
+fork 620 2046
+fork 0 2047
+stem 2048
+fork 2049 100
+fork 0 2050
+fork 19 2051
+fork 0 2052
+fork 98 2053
+fork 62 2054
+fork 62 2055
+fork 0 2056
+stem 2057
+fork 2058 860
+fork 201 2059
+fork 0 2060
+fork 2042 2061
+fork 42 2062
+stem 2063
+fork 2064 551
+fork 0 2065
+fork 1982 2066
+fork 198 2067
+fork 0 2068
+fork 1559 2069
+fork 148 2070
+fork 163 2071
+fork 0 2072
+fork 98 2073
+fork 0 2074
+fork 1233 2075
+fork 198 2076
+stem 2077
+fork 148 1118
+fork 0 2079
+fork 33 2080
+fork 0 2081
+stem 2082
+fork 216 0
+fork 0 2084
+fork 0 2085
+fork 0 2086
+fork 0 2087
+fork 33 2088
+fork 0 2089
+stem 2090
+stem 2085
+fork 2 2092
+fork 0 2093
+stem 2094
+stem 2095
+fork 0 2096
+stem 2097
+fork 33 906
+fork 0 2099
+stem 2100
+fork 1022 20
+fork 0 2102
+fork 33 2103
+fork 0 2104
+stem 2105
+fork 0 138
+fork 0 2107
+fork 240 2108
+stem 2109
+fork 0 2108
+fork 2110 2111
+fork 0 2112
+stem 2113
+stem 2114
+fork 0 2115
+stem 2116
+stem 2117
+fork 0 2118
+stem 2119
+fork 762 203
+fork 0 2121
+fork 2122 20
+fork 0 2123
+stem 2124
+fork 28 0
+fork 1 2126
+fork 2125 2127
+fork 308 2128
+stem 2129
+stem 1112
+fork 2131 215
+fork 2130 2132
+fork 0 2133
+fork 33 2134
+fork 0 2135
+stem 2136
+fork 156 0
+stem 2138
+fork 2139 1
+fork 28 2140
+fork 0 2141
+fork 33 2142
+fork 0 2143
+stem 2144
+stem 2145
+fork 0 2146
+stem 2147
+stem 7
+stem 1160
+fork 2150 654
+stem 2151
+fork 2152 1
+fork 0 2153
+stem 2154
+stem 898
+fork 2156 203
+stem 2157
+fork 2158 1
+fork 2155 2159
+fork 0 2160
+fork 2149 2161
+fork 42 2162
+stem 2163
+fork 2164 38
+fork 1053 2165
+fork 2148 2166
+fork 282 2167
+fork 169 2168
+fork 5 2169
+stem 2170
+fork 169 2166
+fork 5 2172
+stem 2173
+fork 62 918
+fork 148 2175
+fork 0 2176
+fork 33 2177
+fork 0 2178
+stem 2179
+stem 2180
+fork 0 2181
+stem 2182
+fork 2156 215
+stem 2184
+fork 2185 1
+fork 2155 2186
+fork 0 2187
+fork 2149 2188
+fork 42 2189
+stem 2190
+fork 2191 38
+fork 1053 2192
+fork 88 2193
+fork 2183 2194
+fork 883 2195
+fork 2174 2196
+fork 2171 2197
+fork 169 2198
+fork 2137 2199
+fork 2120 2200
+fork 169 2201
+fork 2106 2202
+fork 57 2203
+fork 2101 2204
+fork 2098 2205
+fork 2091 2206
+fork 5 2207
+fork 9 2208
+stem 2209
+fork 0 1076
+fork 98 2211
+fork 0 2212
+fork 2210 2213
+fork 0 2214
+stem 2215
+fork 2216 100
+fork 0 2217
+fork 19 2218
+fork 9 2219
+stem 2220
+fork 2221 904
+fork 201 2222
+fork 57 2223
+fork 2083 2224
+fork 42 2225
+stem 2226
+fork 42 2079
+stem 2228
+stem 562
+fork 932 2102
+stem 2231
+fork 932 2133
+stem 2233
+fork 762 204
+fork 0 2235
+fork 2236 20
+fork 0 2237
+stem 2238
+fork 2239 2127
+fork 308 2240
+stem 2241
+fork 0 208
+fork 2131 2243
+fork 2242 2244
+fork 2234 2245
+fork 2232 2246
+fork 0 2247
+stem 2248
+fork 2249 905
+fork 1518 2250
+fork 932 2251
+fork 9 2252
+fork 0 2253
+fork 98 2254
+fork 163 2255
+fork 0 2256
+stem 2257
+fork 2258 100
+fork 0 2259
+fork 19 2260
+fork 42 2261
+stem 2262
+stem 205
+fork 28 2264
+fork 178 2265
+stem 2266
+stem 2267
+fork 0 2268
+stem 2269
+stem 1249
+fork 28 2271
+fork 178 2272
+stem 2273
+stem 2274
+fork 0 2275
+stem 2276
+fork 0 434
+stem 2278
+stem 2279
+fork 0 2280
+stem 2281
+fork 98 980
+fork 0 2283
+fork 98 2284
+fork 311 2285
+fork 57 2286
+fork 0 2287
+fork 939 2288
+fork 2282 2289
+fork 42 2290
+stem 2291
+fork 2292 38
+fork 0 2293
+stem 2294
+fork 2295 100
+fork 0 2296
+fork 19 2297
+fork 0 2298
+stem 2299
+fork 2 2160
+stem 2301
+fork 2302 1
+fork 178 2303
+stem 2304
+fork 2 2187
+stem 2306
+fork 2307 1
+fork 2302 2308
+fork 2305 2309
+stem 2310
+fork 2311 2133
+fork 793 2312
+stem 2313
+fork 2314 2102
+fork 0 2315
+stem 2316
+fork 2317 905
+fork 2300 2318
+fork 0 2319
+fork 98 2320
+fork 311 2321
+fork 57 2322
+fork 0 2323
+fork 939 2324
+fork 2282 2325
+fork 42 2326
+stem 2327
+fork 2328 38
+fork 0 2329
+stem 2330
+fork 2331 100
+fork 0 2332
+fork 19 2333
+stem 2334
+fork 2335 11
+fork 42 2336
+stem 2337
+stem 216
+fork 28 2339
+fork 0 2340
+fork 1039 2341
+fork 2338 2342
+fork 2277 2343
+fork 993 2344
+fork 2270 2345
+fork 2263 2346
+fork 201 2347
+fork 0 2348
+fork 921 2349
+fork 42 2350
+stem 2351
+fork 45 2031
+fork 5 2353
+stem 2354
+fork 2355 1999
+fork 42 2356
+stem 2357
+fork 2358 551
+fork 0 2359
+fork 1763 2360
+fork 198 2361
+fork 9 2362
+fork 9 2363
+fork 9 2364
+fork 0 2365
+fork 631 2366
+fork 627 2367
+fork 620 2368
+fork 0 2369
+stem 2370
+fork 2371 100
+fork 0 2372
+fork 19 2373
+fork 0 2374
+fork 98 2375
+fork 62 2376
+fork 62 2377
+fork 0 2378
+stem 2379
+fork 2380 860
+fork 201 2381
+fork 2352 2382
+fork 0 2383
+fork 2230 2384
+fork 198 2385
+stem 2386
+fork 0 277
+fork 1039 2388
+fork 5 2389
+stem 2390
+fork 2391 1519
+fork 0 2392
+fork 2387 2393
+fork 198 2394
+fork 0 2395
+fork 2229 2396
+fork 148 2397
+fork 42 2398
+stem 2399
+stem 2400
+fork 0 2401
+stem 2402
+fork 2403 1221
+fork 148 2404
+fork 166 2405
+fork 42 2406
+stem 2407
+fork 2408 38
+fork 1050 2409
+fork 198 2410
+fork 0 2411
+fork 2227 2412
+fork 0 2413
+stem 2414
+fork 2415 863
+fork 0 2416
+fork 98 2417
+fork 148 2418
+fork 42 2419
+stem 2420
+fork 204 0
+fork 0 2422
+fork 2423 0
+stem 154
+fork 2425 1
+fork 178 2426
+stem 2427
+fork 0 2423
+fork 2428 2429
+fork 9 2430
+stem 2431
+fork 2432 1519
+fork 2424 2433
+fork 782 2434
+stem 2435
+fork 0 2426
+fork 2436 2437
+stem 2438
+fork 0 2429
+fork 0 2440
+fork 2439 2441
+stem 2442
+fork 1273 0
+fork 1262 2444
+fork 1248 2445
+fork 1280 2446
+fork 0 2447
+fork 1077 2448
+fork 2443 2449
+fork 178 2450
+fork 9 2451
+stem 2452
+fork 2423 2426
+fork 2454 2440
+fork 0 2455
+stem 2456
+stem 2457
+fork 0 2458
+stem 2459
+fork 2460 1076
+fork 2453 2461
+fork 5 2462
+stem 2463
+fork 2464 1519
+fork 0 2465
+stem 2466
+fork 2467 1527
+stem 2468
+fork 2469 1
+fork 198 2470
+fork 0 2471
+fork 2421 2472
+fork 148 2473
+fork 163 2474
+fork 148 2475
+fork 2078 2476
+fork 198 2477
+fork 9 2478
+stem 2479
+fork 761 127
+fork 0 2481
+fork 98 2482
+fork 0 2483
+fork 98 2484
+fork 575 2485
+fork 0 2486
+fork 2480 2487
+fork 201 2488
+fork 161 2489
+fork 148 2490
+fork 65 2491
+fork 62 2492
+fork 145 2493
+fork 65 2494
+fork 5 2495
+stem 2496
+fork 1517 20
+fork 308 2498
+fork 9 2499
+stem 2500
+fork 2501 1517
+fork 20 2502
+stem 2503
+fork 0 2504
+stem 2505
+stem 2506
+fork 0 2507
+stem 2508
+stem 138
+fork 0 2510
+stem 2511
+fork 932 797
+stem 2513
+fork 762 27
+fork 2514 2515
+fork 308 2516
+fork 9 2517
+fork 0 2518
+fork 98 2519
+fork 2512 2520
+fork 0 2521
+stem 2522
+fork 2523 100
+fork 0 2524
+fork 19 2525
+fork 308 2526
+fork 9 2527
+fork 0 2528
+fork 98 2529
+fork 2279 2530
+fork 0 2531
+stem 2532
+fork 2533 100
+fork 0 2534
+fork 19 2535
+fork 42 2536
+stem 2537
+stem 1250
+fork 28 2539
+fork 42 2540
+stem 2541
+fork 156 434
+fork 181 2543
+fork 2542 2544
+fork 2538 2545
+fork 0 2546
+fork 33 2547
+fork 0 2548
+stem 2549
+fork 2550 2489
+fork 148 2551
+fork 65 2552
+fork 62 2553
+fork 2509 2554
+fork 65 2555
+fork 5 2556
+stem 2557
+fork 308 139
+fork 9 2559
+stem 2560
+fork 0 2503
+fork 2561 2562
+fork 20 2563
+stem 2564
+fork 0 2565
+stem 2566
+stem 2567
+fork 0 2568
+stem 2569
+fork 42 2526
+stem 2571
+fork 156 276
+fork 181 2573
+fork 2542 2574
+fork 2572 2575
+fork 0 2576
+fork 33 2577
+fork 0 2578
+stem 2579
+fork 2580 2489
+fork 148 2581
+fork 65 2582
+fork 62 2583
+fork 2570 2584
+fork 65 2585
+fork 5 2586
+stem 2587
+fork 2501 2562
+fork 20 2589
+stem 2590
+fork 0 2591
+stem 2592
+stem 2593
+fork 0 2594
+stem 2595
+fork 42 2516
+stem 2597
+stem 994
+fork 156 2599
+fork 181 2600
+fork 2542 2601
+fork 2598 2602
+fork 0 2603
+fork 33 2604
+fork 0 2605
+stem 2606
+fork 2607 2489
+fork 148 2608
+fork 65 2609
+fork 62 2610
+fork 2596 2611
+fork 65 2612
+fork 5 2613
+stem 2614
+fork 0 2564
+fork 2561 2616
+fork 20 2617
+stem 2618
+fork 0 2619
+stem 2620
+stem 2621
+fork 0 2622
+stem 2623
+fork 2512 6
+fork 0 2625
+stem 2626
+fork 2627 100
+fork 0 2628
+fork 19 2629
+fork 42 2630
+stem 2631
+fork 156 1084
+fork 181 2633
+fork 2542 2634
+fork 2632 2635
+fork 0 2636
+fork 33 2637
+fork 0 2638
+stem 2639
+fork 2640 2489
+fork 148 2641
+fork 65 2642
+fork 62 2643
+fork 2624 2644
+fork 65 2645
+fork 5 2646
+stem 2647
+fork 0 28
+stem 2649
+stem 1263
+fork 28 2651
+fork 28 2652
+fork 2 2653
+stem 2654
+fork 2655 1
+fork 2650 2656
+fork 2 2657
+stem 2658
+fork 2659 1
+fork 42 2660
+stem 2661
+fork 2501 2616
+fork 20 2663
+stem 2664
+fork 0 2665
+stem 2666
+stem 2667
+fork 0 2668
+stem 2669
+fork 156 1040
+fork 181 2671
+fork 2542 2672
+fork 2538 2673
+fork 0 2674
+fork 33 2675
+fork 0 2676
+stem 2677
+fork 2678 2489
+fork 148 2679
+fork 65 2680
+fork 62 2681
+fork 2670 2682
+fork 65 2683
+fork 2662 2684
+fork 2648 2685
+fork 2615 2686
+fork 2588 2687
+fork 2558 2688
+fork 2497 2689
+fork 0 2690
+fork 137 2691
diff --git a/ext/zig/src/arena.zig b/ext/zig/src/arena.zig
@@ -0,0 +1,36 @@
+const std = @import("std");
+const tree = @import("tree.zig");
+
+pub const Arena = struct {
+ allocator: std.mem.Allocator,
+ nodes: std.ArrayList(tree.Node),
+
+ pub fn init(allocator: std.mem.Allocator) Arena {
+ return .{
+ .allocator = allocator,
+ .nodes = .empty,
+ };
+ }
+
+ pub fn deinit(self: *Arena) void {
+ self.nodes.deinit(self.allocator);
+ }
+
+ pub fn alloc(self: *Arena, node: tree.Node) !u32 {
+ const idx: u32 = @intCast(self.nodes.items.len);
+ try self.nodes.append(self.allocator, node);
+ return idx;
+ }
+
+ pub fn get(self: *Arena, idx: u32) *tree.Node {
+ return &self.nodes.items[idx];
+ }
+
+ pub fn len(self: *const Arena) u32 {
+ return @intCast(self.nodes.items.len);
+ }
+
+ pub fn reset(self: *Arena, keep: u32) void {
+ self.nodes.shrinkRetainingCapacity(keep);
+ }
+};
diff --git a/ext/zig/src/bundle.zig b/ext/zig/src/bundle.zig
@@ -0,0 +1,479 @@
+const std = @import("std");
+const tree = @import("tree.zig");
+const Arena = @import("arena.zig").Arena;
+
+pub const Hash = [32]u8;
+
+pub const Error = error{
+ InvalidMagic,
+ InvalidVersion,
+ Truncated,
+ InvalidManifest,
+ InvalidNodePayload,
+ HashMismatch,
+ ExportNotFound,
+ MissingChild,
+ UnexpectedFormat,
+ DigestMismatch,
+ OutOfMemory,
+};
+
+const Parser = struct {
+ bytes: []const u8,
+ pos: usize,
+
+ fn init(bytes: []const u8) Parser {
+ return .{ .bytes = bytes, .pos = 0 };
+ }
+
+ fn remaining(self: *const Parser) usize {
+ return self.bytes.len - self.pos;
+ }
+
+ fn expect(self: *Parser, n: usize) Error![]const u8 {
+ if (self.remaining() < n) return error.Truncated;
+ const result = self.bytes[self.pos .. self.pos + n];
+ self.pos += n;
+ return result;
+ }
+
+ fn readU8(self: *Parser) Error!u8 {
+ const b = try self.expect(1);
+ return b[0];
+ }
+
+ fn readU16(self: *Parser) Error!u16 {
+ const b = try self.expect(2);
+ return std.mem.readInt(u16, b[0..2], .big);
+ }
+
+ fn readU32(self: *Parser) Error!u32 {
+ const b = try self.expect(4);
+ return std.mem.readInt(u32, b[0..4], .big);
+ }
+
+ fn readU64(self: *Parser) Error!u64 {
+ const b = try self.expect(8);
+ return std.mem.readInt(u64, b[0..8], .big);
+ }
+
+ fn readHash(self: *Parser) Error!Hash {
+ const b = try self.expect(32);
+ var h: Hash = undefined;
+ @memcpy(&h, b);
+ return h;
+ }
+
+ fn readLengthPrefixedBytes(self: *Parser, allocator: std.mem.Allocator) Error![]const u8 {
+ const len = try self.readU32();
+ const bytes = try self.expect(len);
+ const copy = try allocator.alloc(u8, bytes.len);
+ @memcpy(copy, bytes);
+ return copy;
+ }
+};
+
+const SectionEntry = struct {
+ section_type: u32,
+ offset: u64,
+ length: u64,
+ digest: Hash,
+};
+
+fn parseHeader(p: *Parser) Error!struct { major: u16, minor: u16, section_count: u32, dir_offset: u64 } {
+ const magic = try p.expect(8);
+ if (!std.mem.eql(u8, magic, "ARBORICX")) return error.InvalidMagic;
+
+ const major = try p.readU16();
+ const minor = try p.readU16();
+ const section_count = try p.readU32();
+ _ = try p.readU64(); // flags
+ const dir_offset = try p.readU64();
+
+ if (major != 1) return error.InvalidVersion;
+
+ return .{ .major = major, .minor = minor, .section_count = section_count, .dir_offset = dir_offset };
+}
+
+fn parseSectionEntries(p: *Parser, count: u32, allocator: std.mem.Allocator) Error![]SectionEntry {
+ const entries = try allocator.alloc(SectionEntry, count);
+ errdefer allocator.free(entries);
+
+ for (entries) |*entry| {
+ entry.section_type = try p.readU32();
+ _ = try p.readU16(); // section_version
+ _ = try p.readU16(); // section_flags
+ const compression = try p.readU16();
+ const digest_alg = try p.readU16();
+ entry.offset = try p.readU64();
+ entry.length = try p.readU64();
+ entry.digest = try p.readHash();
+
+ if (compression != 0) return error.UnexpectedFormat;
+ if (digest_alg != 1) return error.UnexpectedFormat;
+ }
+ return entries;
+}
+
+fn sha256Digest(data: []const u8) Hash {
+ var h = std.crypto.hash.sha2.Sha256.init(.{});
+ h.update(data);
+ var out: Hash = undefined;
+ h.final(&out);
+ return out;
+}
+
+fn parseManifest(p: *Parser, allocator: std.mem.Allocator) Error!struct { exports: []Export, roots: []Root } {
+ const magic = try p.expect(8);
+ if (!std.mem.eql(u8, magic, "ARBMNFST")) return error.InvalidManifest;
+
+ const major = try p.readU16();
+ _ = try p.readU16(); // minor
+ if (major != 1) return error.InvalidVersion;
+
+ const schema = try p.readLengthPrefixedBytes(allocator);
+ defer allocator.free(schema);
+ if (!std.mem.eql(u8, schema, "arboricx.bundle.manifest.v1")) return error.UnexpectedFormat;
+
+ const bundle_type = try p.readLengthPrefixedBytes(allocator);
+ defer allocator.free(bundle_type);
+ if (!std.mem.eql(u8, bundle_type, "tree-calculus-executable-object")) return error.UnexpectedFormat;
+
+ const calc = try p.readLengthPrefixedBytes(allocator);
+ defer allocator.free(calc);
+ if (!std.mem.eql(u8, calc, "tree-calculus.v1")) return error.UnexpectedFormat;
+
+ const hash_alg = try p.readLengthPrefixedBytes(allocator);
+ defer allocator.free(hash_alg);
+ if (!std.mem.eql(u8, hash_alg, "sha256")) return error.UnexpectedFormat;
+
+ const hash_domain = try p.readLengthPrefixedBytes(allocator);
+ defer allocator.free(hash_domain);
+ if (!std.mem.eql(u8, hash_domain, "arboricx.merkle.node.v1")) return error.UnexpectedFormat;
+
+ const payload_type = try p.readLengthPrefixedBytes(allocator);
+ defer allocator.free(payload_type);
+ if (!std.mem.eql(u8, payload_type, "arboricx.merkle.payload.v1")) return error.UnexpectedFormat;
+
+ const sem = try p.readLengthPrefixedBytes(allocator);
+ defer allocator.free(sem);
+ if (!std.mem.eql(u8, sem, "tree-calculus.v1")) return error.UnexpectedFormat;
+
+ const eval_mode = try p.readLengthPrefixedBytes(allocator);
+ defer allocator.free(eval_mode);
+ if (!std.mem.eql(u8, eval_mode, "normal-order")) return error.UnexpectedFormat;
+
+ const abi = try p.readLengthPrefixedBytes(allocator);
+ defer allocator.free(abi);
+ if (!std.mem.eql(u8, abi, "arboricx.abi.tree.v1")) return error.UnexpectedFormat;
+
+ const cap_count = try p.readU32();
+ var i: u32 = 0;
+ while (i < cap_count) : (i += 1) {
+ const cap = try p.readLengthPrefixedBytes(allocator);
+ defer allocator.free(cap);
+ if (cap.len != 0) return error.UnexpectedFormat;
+ }
+
+ const closure = try p.readU8();
+ if (closure != 0) return error.UnexpectedFormat;
+
+ const root_count = try p.readU32();
+ const roots = try allocator.alloc(Root, root_count);
+ errdefer allocator.free(roots);
+ for (roots) |*r| {
+ r.hash = try p.readHash();
+ r.role = try p.readLengthPrefixedBytes(allocator);
+ }
+
+ const export_count = try p.readU32();
+ const exports = try allocator.alloc(Export, export_count);
+ errdefer {
+ for (exports) |*e| {
+ allocator.free(e.name);
+ allocator.free(e.kind);
+ allocator.free(e.abi);
+ }
+ allocator.free(exports);
+ }
+ for (exports) |*e| {
+ e.name = try p.readLengthPrefixedBytes(allocator);
+ e.root = try p.readHash();
+ e.kind = try p.readLengthPrefixedBytes(allocator);
+ e.abi = try p.readLengthPrefixedBytes(allocator);
+ if (!std.mem.eql(u8, e.abi, "arboricx.abi.tree.v1")) return error.UnexpectedFormat;
+ }
+
+ const metadata_count = try p.readU32();
+ var m: u32 = 0;
+ while (m < metadata_count) : (m += 1) {
+ _ = try p.readU16(); // tag
+ const len = try p.readU32();
+ _ = try p.expect(len);
+ }
+
+ const ext_count = try p.readU32();
+ var e_idx: u32 = 0;
+ while (e_idx < ext_count) : (e_idx += 1) {
+ _ = try p.readU16(); // tag
+ const len = try p.readU32();
+ _ = try p.expect(len);
+ }
+
+ return .{ .exports = exports, .roots = roots };
+}
+
+const Export = struct {
+ name: []const u8,
+ root: Hash,
+ kind: []const u8,
+ abi: []const u8,
+};
+
+const Root = struct {
+ hash: Hash,
+ role: []const u8,
+};
+
+fn parseNodeSection(p: *Parser, allocator: std.mem.Allocator) Error!std.AutoHashMap(Hash, []const u8) {
+ const node_count = try p.readU64();
+ var map = std.AutoHashMap(Hash, []const u8).init(allocator);
+ errdefer map.deinit();
+
+ var i: u64 = 0;
+ while (i < node_count) : (i += 1) {
+ const hash = try p.readHash();
+ const plen = try p.readU32();
+ const payload = try p.expect(plen);
+
+ const expected_hash = blk: {
+ var h = std.crypto.hash.sha2.Sha256.init(.{});
+ h.update("arboricx.merkle.node.v1");
+ h.update(&[_]u8{0});
+ h.update(payload);
+ var out: Hash = undefined;
+ h.final(&out);
+ break :blk out;
+ };
+ if (!std.mem.eql(u8, &hash, &expected_hash)) return error.HashMismatch;
+
+ try map.put(hash, payload);
+ }
+
+ return map;
+}
+
+fn loadNode(
+ arena: *Arena,
+ payloads: std.AutoHashMap(Hash, []const u8),
+ cache: *std.AutoHashMap(Hash, u32),
+ root_hash: Hash,
+) Error!u32 {
+ const Frame = struct {
+ hash: Hash,
+ state: u2,
+ };
+
+ const max_stack = payloads.count() * 2;
+ var stack = try arena.allocator.alloc(Frame, max_stack);
+ defer arena.allocator.free(stack);
+ var sp: usize = 0;
+
+ stack[sp] = .{ .hash = root_hash, .state = 0 };
+ sp += 1;
+
+ while (sp > 0) {
+ const frame = &stack[sp - 1];
+
+ if (cache.get(frame.hash)) |_| {
+ sp -= 1;
+ continue;
+ }
+
+ if (frame.state == 0) {
+ frame.state = 1;
+ const payload = payloads.get(frame.hash) orelse return error.MissingChild;
+ if (payload.len == 0) return error.InvalidNodePayload;
+
+ switch (payload[0]) {
+ 0x00 => {
+ if (payload.len != 1) return error.InvalidNodePayload;
+ },
+ 0x01 => {
+ if (payload.len != 33) return error.InvalidNodePayload;
+ var child_hash: Hash = undefined;
+ @memcpy(&child_hash, payload[1..33]);
+ if (cache.get(child_hash) == null) {
+ stack[sp] = .{ .hash = child_hash, .state = 0 };
+ sp += 1;
+ }
+ },
+ 0x02 => {
+ if (payload.len != 65) return error.InvalidNodePayload;
+ var left_hash: Hash = undefined;
+ var right_hash: Hash = undefined;
+ @memcpy(&left_hash, payload[1..33]);
+ @memcpy(&right_hash, payload[33..65]);
+ const need_right = cache.get(right_hash) == null;
+ const need_left = cache.get(left_hash) == null;
+ if (need_right) {
+ stack[sp] = .{ .hash = right_hash, .state = 0 };
+ sp += 1;
+ }
+ if (need_left) {
+ stack[sp] = .{ .hash = left_hash, .state = 0 };
+ sp += 1;
+ }
+ },
+ else => return error.InvalidNodePayload,
+ }
+ } else {
+ const payload = payloads.get(frame.hash).?;
+ const idx: u32 = switch (payload[0]) {
+ 0x00 => try arena.alloc(.leaf),
+ 0x01 => blk: {
+ var child_hash: Hash = undefined;
+ @memcpy(&child_hash, payload[1..33]);
+ const child_idx = cache.get(child_hash).?;
+ break :blk try arena.alloc(.{ .stem = .{ .child = child_idx } });
+ },
+ 0x02 => blk: {
+ var left_hash: Hash = undefined;
+ var right_hash: Hash = undefined;
+ @memcpy(&left_hash, payload[1..33]);
+ @memcpy(&right_hash, payload[33..65]);
+ const left_idx = cache.get(left_hash).?;
+ const right_idx = cache.get(right_hash).?;
+ break :blk try arena.alloc(.{ .fork = .{ .left = left_idx, .right = right_idx } });
+ },
+ else => unreachable,
+ };
+ try cache.put(frame.hash, idx);
+ sp -= 1;
+ }
+ }
+
+ return cache.get(root_hash) orelse return error.MissingChild;
+}
+
+/// Parse an Arboricx bundle and load the named export into the arena.
+/// Returns the arena index of the exported term tree.
+pub fn loadBundleExport(
+ arena: *Arena,
+ bundle_bytes: []const u8,
+ export_name: []const u8,
+) Error!u32 {
+ var p = Parser.init(bundle_bytes);
+
+ const header = try parseHeader(&p);
+
+ p.pos = @intCast(header.dir_offset);
+ const allocator = arena.allocator;
+ const entries = try parseSectionEntries(&p, header.section_count, allocator);
+ defer allocator.free(entries);
+
+ var manifest_entry: ?SectionEntry = null;
+ var nodes_entry: ?SectionEntry = null;
+ for (entries) |entry| {
+ if (entry.section_type == 1) manifest_entry = entry;
+ if (entry.section_type == 2) nodes_entry = entry;
+ }
+ const manifest_section = manifest_entry orelse return error.InvalidManifest;
+ const nodes_section = nodes_entry orelse return error.InvalidNodePayload;
+
+ const manifest_bytes = bundle_bytes[@intCast(manifest_section.offset)..@intCast(manifest_section.offset + manifest_section.length)];
+ if (!std.mem.eql(u8, &sha256Digest(manifest_bytes), &manifest_section.digest)) return error.DigestMismatch;
+
+ const nodes_bytes = bundle_bytes[@intCast(nodes_section.offset)..@intCast(nodes_section.offset + nodes_section.length)];
+ if (!std.mem.eql(u8, &sha256Digest(nodes_bytes), &nodes_section.digest)) return error.DigestMismatch;
+
+ var mp = Parser.init(manifest_bytes);
+ const manifest = try parseManifest(&mp, allocator);
+ defer {
+ for (manifest.exports) |e| {
+ allocator.free(e.name);
+ allocator.free(e.kind);
+ allocator.free(e.abi);
+ }
+ allocator.free(manifest.exports);
+ for (manifest.roots) |r| {
+ allocator.free(r.role);
+ }
+ allocator.free(manifest.roots);
+ }
+
+ var export_hash: ?Hash = null;
+ for (manifest.exports) |e| {
+ if (std.mem.eql(u8, e.name, export_name)) {
+ export_hash = e.root;
+ break;
+ }
+ }
+ const root_hash = export_hash orelse return error.ExportNotFound;
+
+ var np = Parser.init(nodes_bytes);
+ var payloads = try parseNodeSection(&np, allocator);
+ defer payloads.deinit();
+
+ var cache = std.AutoHashMap(Hash, u32).init(allocator);
+ defer cache.deinit();
+
+ return try loadNode(arena, payloads, &cache, root_hash);
+}
+
+/// Parse an Arboricx bundle and load the default (first) root into the arena.
+pub fn loadBundleDefaultRoot(
+ arena: *Arena,
+ bundle_bytes: []const u8,
+) Error!u32 {
+ var p = Parser.init(bundle_bytes);
+
+ const header = try parseHeader(&p);
+
+ p.pos = @intCast(header.dir_offset);
+ const allocator = arena.allocator;
+ const entries = try parseSectionEntries(&p, header.section_count, allocator);
+ defer allocator.free(entries);
+
+ var manifest_entry: ?SectionEntry = null;
+ var nodes_entry: ?SectionEntry = null;
+ for (entries) |entry| {
+ if (entry.section_type == 1) manifest_entry = entry;
+ if (entry.section_type == 2) nodes_entry = entry;
+ }
+ const manifest_section = manifest_entry orelse return error.InvalidManifest;
+ const nodes_section = nodes_entry orelse return error.InvalidNodePayload;
+
+ const manifest_bytes = bundle_bytes[@intCast(manifest_section.offset)..@intCast(manifest_section.offset + manifest_section.length)];
+ if (!std.mem.eql(u8, &sha256Digest(manifest_bytes), &manifest_section.digest)) return error.DigestMismatch;
+
+ const nodes_bytes = bundle_bytes[@intCast(nodes_section.offset)..@intCast(nodes_section.offset + nodes_section.length)];
+ if (!std.mem.eql(u8, &sha256Digest(nodes_bytes), &nodes_section.digest)) return error.DigestMismatch;
+
+ var mp = Parser.init(manifest_bytes);
+ const manifest = try parseManifest(&mp, allocator);
+ defer {
+ for (manifest.exports) |e| {
+ allocator.free(e.name);
+ allocator.free(e.kind);
+ allocator.free(e.abi);
+ }
+ allocator.free(manifest.exports);
+ for (manifest.roots) |r| {
+ allocator.free(r.role);
+ }
+ allocator.free(manifest.roots);
+ }
+
+ if (manifest.roots.len == 0) return error.ExportNotFound;
+ const root_hash = manifest.roots[0].hash;
+
+ var np = Parser.init(nodes_bytes);
+ var payloads = try parseNodeSection(&np, allocator);
+ defer payloads.deinit();
+
+ var cache = std.AutoHashMap(Hash, u32).init(allocator);
+ defer cache.deinit();
+
+ return try loadNode(arena, payloads, &cache, root_hash);
+}
diff --git a/ext/zig/src/c_abi.zig b/ext/zig/src/c_abi.zig
@@ -0,0 +1,183 @@
+const std = @import("std");
+const tree = @import("tree.zig");
+const Arena = @import("arena.zig").Arena;
+const reduce = @import("reduce.zig");
+const codecs = @import("codecs.zig");
+const kernel = @import("kernel.zig");
+const bundle = @import("bundle.zig");
+
+/// Opaque handle for the C API. Layout is not exposed to C.
+/// Holds a persistent arena for user-built terms and the kernel.
+pub const ArbCtx = struct {
+ gpa: std.mem.Allocator,
+ arena: Arena,
+ kernel_root: u32,
+};
+
+// ---------------------------------------------------------------------------
+// Context lifecycle
+// ---------------------------------------------------------------------------
+
+export fn arboricx_init() ?*ArbCtx {
+ const ptr = std.heap.smp_allocator.create(ArbCtx) catch return null;
+ ptr.gpa = std.heap.smp_allocator;
+ ptr.arena = Arena.init(std.heap.smp_allocator);
+ ptr.kernel_root = kernel.loadKernel(&ptr.arena) catch {
+ ptr.arena.deinit();
+ std.heap.smp_allocator.destroy(ptr);
+ return null;
+ };
+ return ptr;
+}
+
+export fn arboricx_free(ctx: *ArbCtx) void {
+ ctx.arena.deinit();
+ ctx.gpa.destroy(ctx);
+}
+
+export fn arboricx_free_buf(_: *ArbCtx, ptr: [*]u8, len: usize) void {
+ std.heap.smp_allocator.free(ptr[0..len]);
+}
+
+// ---------------------------------------------------------------------------
+// Tree construction (all write into the persistent arena)
+// ---------------------------------------------------------------------------
+
+export fn arb_leaf(ctx: *ArbCtx) u32 {
+ return ctx.arena.alloc(.leaf) catch 0;
+}
+
+export fn arb_stem(ctx: *ArbCtx, child: u32) u32 {
+ return ctx.arena.alloc(.{ .stem = .{ .child = child } }) catch 0;
+}
+
+export fn arb_fork(ctx: *ArbCtx, left: u32, right: u32) u32 {
+ return ctx.arena.alloc(.{ .fork = .{ .left = left, .right = right } }) catch 0;
+}
+
+export fn arb_app(ctx: *ArbCtx, func: u32, arg: u32) u32 {
+ return ctx.arena.alloc(.{ .app = .{ .func = func, .arg = arg } }) catch 0;
+}
+
+// ---------------------------------------------------------------------------
+// Reduction
+// ---------------------------------------------------------------------------
+/// Reduces `root` in a *fresh* scratch arena so that garbage from previous
+/// reductions never accumulates. The kernel and term are deep-copied into
+/// the scratch arena, reduced there, and the result is copied back into the
+/// persistent arena.
+// ---------------------------------------------------------------------------
+
+export fn arb_reduce(ctx: *ArbCtx, root: u32, fuel: u64) u32 {
+ // 1. Fresh scratch arena
+ var scratch = Arena.init(ctx.gpa);
+ defer scratch.deinit();
+
+ // 2. Deep-copy the term (which may reference kernel nodes) into scratch
+ const scratch_root = tree.copyTree(ctx.arena.nodes.items, &scratch, root) catch return 0;
+
+ // 3. Reduce in scratch
+ const scratch_result = reduce.reduce(scratch_root, &scratch, fuel) catch return 0;
+
+ // 4. Copy the result back to the persistent arena
+ return tree.copyTree(scratch.nodes.items, &ctx.arena, scratch_result) catch 0;
+}
+
+// ---------------------------------------------------------------------------
+// Codec constructors
+// ---------------------------------------------------------------------------
+
+export fn arb_of_number(ctx: *ArbCtx, n: u64) u32 {
+ return codecs.ofNumber(&ctx.arena, n) catch 0;
+}
+
+export fn arb_of_string(ctx: *ArbCtx, s: [*:0]const u8) u32 {
+ const slice = std.mem.sliceTo(s, 0);
+ return codecs.ofString(&ctx.arena, slice) catch 0;
+}
+
+export fn arb_of_bytes(ctx: *ArbCtx, bytes: [*]const u8, len: usize) u32 {
+ return codecs.ofBytes(&ctx.arena, bytes[0..len]) catch 0;
+}
+
+export fn arb_of_list(ctx: *ArbCtx, items: [*]const u32, len: usize) u32 {
+ return codecs.ofList(&ctx.arena, items[0..len]) catch 0;
+}
+
+// ---------------------------------------------------------------------------
+// Codec destructors
+// Return 1 on success, 0 on failure.
+// ---------------------------------------------------------------------------
+
+export fn arb_to_number(ctx: *ArbCtx, root: u32, out: *u64) c_int {
+ const n = codecs.toNumber(&ctx.arena, root) catch return 0;
+ if (n == null) return 0;
+ out.* = n.?;
+ return 1;
+}
+
+export fn arb_to_string(ctx: *ArbCtx, root: u32, out_ptr: **u8, out_len: *usize) c_int {
+ const s = codecs.toString(&ctx.arena, root) catch return 0;
+ if (s == null) return 0;
+ out_ptr.* = @ptrCast(s.?.ptr);
+ out_len.* = s.?.len;
+ return 1;
+}
+
+export fn arb_to_bytes(ctx: *ArbCtx, root: u32, out_ptr: **u8, out_len: *usize) c_int {
+ return arb_to_string(ctx, root, out_ptr, out_len);
+}
+
+export fn arb_to_bool(ctx: *ArbCtx, root: u32, out: *c_int) c_int {
+ const b = codecs.toBool(&ctx.arena, root) catch return 0;
+ if (b == null) return 0;
+ out.* = if (b.?) 1 else 0;
+ return 1;
+}
+
+// ---------------------------------------------------------------------------
+// Result unwrapping
+// Return 1 on success, 0 on failure.
+// ---------------------------------------------------------------------------
+
+export fn arb_unwrap_result(ctx: *ArbCtx, root: u32, out_ok: *c_int, out_value: *u32, out_rest: *u32) c_int {
+ const r = codecs.unwrapResult(&ctx.arena, root) catch return 0;
+ if (r == null) return 0;
+ out_ok.* = if (r.?.ok) 1 else 0;
+ out_value.* = r.?.value;
+ out_rest.* = r.?.rest;
+ return 1;
+}
+
+export fn arb_unwrap_host_value(ctx: *ArbCtx, root: u32, out_tag: *u64, out_payload: *u32) c_int {
+ const hv = codecs.unwrapHostValue(&ctx.arena, root) catch return 0;
+ if (hv == null) return 0;
+ out_tag.* = hv.?.tag;
+ out_payload.* = hv.?.payload;
+ return 1;
+}
+
+// ---------------------------------------------------------------------------
+// Kernel entrypoints
+// ---------------------------------------------------------------------------
+
+export fn arb_kernel_root(ctx: *ArbCtx) u32 {
+ return ctx.kernel_root;
+}
+
+// ---------------------------------------------------------------------------
+// Native bundle loading (fast path — bypasses the Tricu kernel)
+// ---------------------------------------------------------------------------
+
+/// Load a named export from an Arboricx bundle directly into the arena.
+/// Returns the arena index of the exported term, or 0 on error.
+export fn arb_load_bundle(ctx: *ArbCtx, bytes: [*]const u8, len: usize, name: [*:0]const u8) u32 {
+ const name_slice = std.mem.sliceTo(name, 0);
+ return bundle.loadBundleExport(&ctx.arena, bytes[0..len], name_slice) catch 0;
+}
+
+/// Load the default root from an Arboricx bundle directly into the arena.
+/// Returns the arena index of the root term, or 0 on error.
+export fn arb_load_bundle_default(ctx: *ArbCtx, bytes: [*]const u8, len: usize) u32 {
+ return bundle.loadBundleDefaultRoot(&ctx.arena, bytes[0..len]) catch 0;
+}
diff --git a/ext/zig/src/codecs.zig b/ext/zig/src/codecs.zig
@@ -0,0 +1,205 @@
+const std = @import("std");
+const tree = @import("tree.zig");
+const Arena = @import("arena.zig").Arena;
+const reduce = @import("reduce.zig");
+
+// ---------------------------------------------------------------------------
+// Number encoding/decoding
+// ---------------------------------------------------------------------------
+
+pub fn ofNumber(arena: *Arena, n: u64) !u32 {
+ if (n == 0) {
+ return try arena.alloc(.leaf);
+ }
+ const bit = if (n % 2 == 1) try arena.alloc(.{ .stem = .{ .child = try arena.alloc(.leaf) } }) else try arena.alloc(.leaf);
+ const rest = try ofNumber(arena, n / 2);
+ return try arena.alloc(.{ .fork = .{ .left = bit, .right = rest } });
+}
+
+pub fn toNumber(arena: *Arena, idx: u32) !?u64 {
+ const node = try reduce.reduce(idx, arena, 10_000);
+ const n = arena.get(node);
+ return switch (n.*) {
+ .leaf => 0,
+ .stem => return null,
+ .fork => |f| blk: {
+ const bit_node = try reduce.reduce(f.left, arena, 10_000);
+ const bit = arena.get(bit_node);
+ const bit_val: u64 = switch (bit.*) {
+ .leaf => 0,
+ .stem => |s| if (arena.get(s.child).* == .leaf) 1 else return null,
+ else => return null,
+ };
+ const rest = try toNumber(arena, f.right) orelse return null;
+ break :blk bit_val + 2 * rest;
+ },
+ .app => return null,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// List encoding/decoding
+// ---------------------------------------------------------------------------
+
+pub fn ofList(arena: *Arena, items: []const u32) !u32 {
+ var result = try arena.alloc(.leaf);
+ var i: usize = items.len;
+ while (i > 0) {
+ i -= 1;
+ result = try arena.alloc(.{ .fork = .{ .left = items[i], .right = result } });
+ }
+ return result;
+}
+
+pub fn toList(arena: *Arena, idx: u32) !?std.ArrayList(u32) {
+ var result = std.ArrayList(u32).empty;
+ errdefer result.deinit(arena.allocator);
+
+ var current = idx;
+ while (true) {
+ const node = try reduce.reduce(current, arena, 10_000);
+ const n = arena.get(node);
+ switch (n.*) {
+ .leaf => return result,
+ .stem => return null,
+ .fork => |f| {
+ try result.append(arena.allocator, f.left);
+ current = f.right;
+ },
+ .app => return null,
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// String / Bytes encoding/decoding
+// Strings are lists of byte values (each character encoded as a number tree).
+// ---------------------------------------------------------------------------
+
+pub fn ofString(arena: *Arena, s: []const u8) !u32 {
+ var bytes = try arena.allocator.alloc(u32, s.len);
+ defer arena.allocator.free(bytes);
+ for (s, 0..) |c, i| {
+ bytes[i] = try ofNumber(arena, c);
+ }
+ return try ofList(arena, bytes);
+}
+
+pub fn toString(arena: *Arena, idx: u32) !?[]u8 {
+ var list = try toList(arena, idx) orelse return null;
+ defer list.deinit(arena.allocator);
+ var result = try arena.allocator.alloc(u8, list.items.len);
+ errdefer arena.allocator.free(result);
+ for (list.items, 0..) |elem_idx, i| {
+ const num = try toNumber(arena, elem_idx) orelse {
+ arena.allocator.free(result);
+ return null;
+ };
+ if (num > 255) {
+ arena.allocator.free(result);
+ return null;
+ }
+ result[i] = @intCast(num);
+ }
+ return result;
+}
+
+pub fn ofBytes(arena: *Arena, bytes: []const u8) !u32 {
+ return try ofString(arena, bytes);
+}
+
+pub fn toBytes(arena: *Arena, idx: u32) !?[]u8 {
+ return try toString(arena, idx);
+}
+
+// ---------------------------------------------------------------------------
+// Result unwrapping (ok/err protocol)
+// ok value rest = pair true (pair value rest)
+// err code rest = pair false (pair code rest)
+// ---------------------------------------------------------------------------
+
+pub const UnwrapResult = struct {
+ ok: bool,
+ value: u32,
+ rest: u32,
+};
+
+pub fn unwrapResult(arena: *Arena, idx: u32) !?UnwrapResult {
+ const node = try reduce.reduce(idx, arena, 10_000);
+ const n = arena.get(node);
+ switch (n.*) {
+ .fork => |f| {
+ const tag = try reduce.reduce(f.left, arena, 10_000);
+ const rest_pair = try reduce.reduce(f.right, arena, 10_000);
+ const rp = arena.get(rest_pair);
+ switch (rp.*) {
+ .fork => |rf| {
+ const is_ok = tree.sameTree(arena, tag, try arena.alloc(.{ .stem = .{ .child = try arena.alloc(.leaf) } }));
+ return UnwrapResult{
+ .ok = is_ok,
+ .value = rf.left,
+ .rest = rf.right,
+ };
+ },
+ else => return null,
+ }
+ },
+ else => return null,
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Host ABI value unwrapping
+// A host ABI value is: pair tag payload
+// ---------------------------------------------------------------------------
+
+pub const HostValue = struct {
+ tag: u64,
+ payload: u32,
+};
+
+pub fn unwrapHostValue(arena: *Arena, idx: u32) !?HostValue {
+ const node = try reduce.reduce(idx, arena, 10_000);
+ const n = arena.get(node);
+ switch (n.*) {
+ .fork => |f| {
+ const tag_num = try toNumber(arena, f.left) orelse return null;
+ return HostValue{ .tag = tag_num, .payload = f.right };
+ },
+ else => return null,
+ }
+}
+
+/// Returns true if the tree is a valid boolean (Leaf=false, Stem Leaf=true).
+pub fn isBool(arena: *Arena, idx: u32) !bool {
+ const node = try reduce.reduce(idx, arena, 10_000);
+ const n = arena.get(node);
+ return switch (n.*) {
+ .leaf => true,
+ .stem => |s| arena.get(s.child).* == .leaf,
+ else => false,
+ };
+}
+
+/// Extract the boolean value: false for Leaf, true for Stem Leaf.
+/// Returns null if the tree is not a valid boolean.
+pub fn toBool(arena: *Arena, idx: u32) !?bool {
+ const node = try reduce.reduce(idx, arena, 10_000);
+ const n = arena.get(node);
+ return switch (n.*) {
+ .leaf => false,
+ .stem => |s| if (arena.get(s.child).* == .leaf) true else null,
+ else => null,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Host ABI tag constants
+// ---------------------------------------------------------------------------
+
+pub const HOST_TREE_TAG: u64 = 0;
+pub const HOST_STRING_TAG: u64 = 1;
+pub const HOST_NUMBER_TAG: u64 = 2;
+pub const HOST_BOOL_TAG: u64 = 3;
+pub const HOST_LIST_TAG: u64 = 4;
+pub const HOST_BYTES_TAG: u64 = 5;
diff --git a/ext/zig/src/kernel.zig b/ext/zig/src/kernel.zig
@@ -0,0 +1,22 @@
+const std = @import("std");
+const tree = @import("tree.zig");
+const Arena = @import("arena.zig").Arena;
+const embed = @import("kernel_embed");
+
+/// Copy the embedded kernel into an arena, returning the new root index.
+/// This allows the kernel to be used in App nodes alongside application terms.
+pub fn loadKernel(arena: *Arena) !u32 {
+ var mapping = try arena.allocator.alloc(u32, embed.kernel_nodes.len);
+ defer arena.allocator.free(mapping);
+
+ for (embed.kernel_nodes, 0..) |node, i| {
+ const idx: u32 = @intCast(i);
+ mapping[idx] = switch (node) {
+ .leaf => try arena.alloc(.leaf),
+ .stem => |s| try arena.alloc(.{ .stem = .{ .child = mapping[s.child] } }),
+ .fork => |f| try arena.alloc(.{ .fork = .{ .left = mapping[f.left], .right = mapping[f.right] } }),
+ };
+ }
+
+ return mapping[embed.kernel_root];
+}
diff --git a/ext/zig/src/main.zig b/ext/zig/src/main.zig
@@ -0,0 +1,235 @@
+const std = @import("std");
+const tree = @import("tree.zig");
+const Arena = @import("arena.zig").Arena;
+const reduce = @import("reduce.zig");
+const codecs = @import("codecs.zig");
+const kernel = @import("kernel.zig");
+const bundle = @import("bundle.zig");
+
+fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, io: std.Io) !void {
+ const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
+
+ var current = term;
+ for (args_raw) |arg| {
+ const arg_tree = try parseArg(arena, arg);
+ current = try arena.alloc(.{ .app = .{ .func = current, .arg = arg_tree } });
+ }
+
+ const result = try reduce.reduce(current, arena, 1_000_000_000);
+
+ var stdout_buf: [4096]u8 = undefined;
+ var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
+
+ switch (tag) {
+ codecs.HOST_STRING_TAG => {
+ const s = try codecs.toString(arena, result) orelse {
+ try stdout.interface.writeAll("Error: failed to decode string result\n");
+ try stdout.flush();
+ return error.DecodeFailed;
+ };
+ defer arena.allocator.free(s);
+ try stdout.interface.writeAll(s);
+ try stdout.interface.writeAll("\n");
+ },
+ codecs.HOST_NUMBER_TAG => {
+ const n = try codecs.toNumber(arena, result) orelse 0;
+ try stdout.interface.print("{d}\n", .{n});
+ },
+ codecs.HOST_BOOL_TAG => {
+ const b = try codecs.toBool(arena, result) orelse {
+ try stdout.interface.writeAll("Error: failed to decode bool result\n");
+ try stdout.flush();
+ return error.DecodeFailed;
+ };
+ try stdout.interface.writeAll(if (b) "true\n" else "false\n");
+ },
+ codecs.HOST_TREE_TAG => {
+ try tree.formatTree(&stdout.interface, arena, result, 0);
+ try stdout.interface.writeAll("\n");
+ },
+ else => {
+ try stdout.interface.print("(tag={d}, payload=", .{tag});
+ try tree.formatTree(&stdout.interface, arena, result, 0);
+ try stdout.interface.writeAll(")\n");
+ },
+ }
+ try stdout.flush();
+}
+
+fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, io: std.Io) !void {
+ const kernel_root = try kernel.loadKernel(arena);
+
+ const tag_tree = try codecs.ofNumber(arena, tag);
+ const bundle_tree = try codecs.ofBytes(arena, bundle_bytes);
+
+ var arg_items = try arena.allocator.alloc(u32, args_raw.len);
+ defer arena.allocator.free(arg_items);
+ for (args_raw, 0..) |arg, i| {
+ arg_items[i] = try parseArg(arena, arg);
+ }
+ const args_tree = try codecs.ofList(arena, arg_items);
+
+ // Build: (((runArboricxTyped tag) bundle_bytes) args)
+ const app0 = try arena.alloc(.{ .app = .{ .func = kernel_root, .arg = tag_tree } });
+ const app1 = try arena.alloc(.{ .app = .{ .func = app0, .arg = bundle_tree } });
+ const app2 = try arena.alloc(.{ .app = .{ .func = app1, .arg = args_tree } });
+
+ const result = try reduce.reduce(app2, arena, 1_000_000_000);
+
+ const unwrapped = try codecs.unwrapResult(arena, result) orelse {
+ var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
+ try stderr.interface.writeAll("Error: result is not a valid ok/err pair\n");
+ try stderr.flush();
+ return error.InvalidResult;
+ };
+
+ if (!unwrapped.ok) {
+ var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
+ const code = try codecs.toNumber(arena, unwrapped.value) orelse 0;
+ try stderr.interface.print("Error: kernel returned err, code={d}\n", .{code});
+ try stderr.flush();
+ return error.KernelError;
+ }
+
+ const hv = try codecs.unwrapHostValue(arena, unwrapped.value) orelse {
+ var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
+ try stderr.interface.writeAll("Error: result is not a valid host ABI value\n");
+ try stderr.flush();
+ return error.InvalidHostValue;
+ };
+
+ var stdout_buf: [4096]u8 = undefined;
+ var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
+
+ switch (hv.tag) {
+ codecs.HOST_STRING_TAG => {
+ const s = try codecs.toString(arena, hv.payload) orelse {
+ try stdout.interface.writeAll("Error: failed to decode string payload\n");
+ try stdout.flush();
+ return error.DecodeFailed;
+ };
+ defer arena.allocator.free(s);
+ try stdout.interface.writeAll(s);
+ try stdout.interface.writeAll("\n");
+ },
+ codecs.HOST_NUMBER_TAG => {
+ const n = try codecs.toNumber(arena, hv.payload) orelse 0;
+ try stdout.interface.print("{d}\n", .{n});
+ },
+ codecs.HOST_BOOL_TAG => {
+ const b = try codecs.toBool(arena, hv.payload) orelse {
+ try stdout.interface.writeAll("Error: failed to decode bool payload\n");
+ try stdout.flush();
+ return error.DecodeFailed;
+ };
+ try stdout.interface.writeAll(if (b) "true\n" else "false\n");
+ },
+ codecs.HOST_TREE_TAG => {
+ try tree.formatTree(&stdout.interface, arena, hv.payload, 0);
+ try stdout.interface.writeAll("\n");
+ },
+ else => {
+ try stdout.interface.print("(tag={d}, payload=", .{hv.tag});
+ try tree.formatTree(&stdout.interface, arena, hv.payload, 0);
+ try stdout.interface.writeAll(")\n");
+ },
+ }
+ try stdout.flush();
+}
+
+fn parseArg(arena: *Arena, s: []const u8) !u32 {
+ if (std.fmt.parseInt(u64, s, 10)) |n| {
+ return try codecs.ofNumber(arena, n);
+ } else |_| {}
+
+ if (s.len >= 2 and s[0] == '"' and s[s.len - 1] == '"') {
+ return try codecs.ofString(arena, s[1 .. s.len - 1]);
+ }
+
+ return try codecs.ofString(arena, s);
+}
+
+pub fn main(init: std.process.Init) !void {
+ const gpa = init.gpa;
+ const io = init.io;
+
+ const args = try init.minimal.args.toSlice(init.arena.allocator());
+ if (args.len < 2) {
+ var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
+ try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] <bundle.arboricx> [arg1 arg2 ...]\n");
+ try stderr.flush();
+ std.process.exit(1);
+ }
+
+ // Parse options before bundle path
+ var tag = codecs.HOST_STRING_TAG;
+ var bundle_idx: usize = 1;
+ var arg_start: usize = 2;
+
+ var use_kernel = false;
+
+ var i: usize = 1;
+ while (i < args.len) : (i += 1) {
+ if (std.mem.eql(u8, args[i], "--type")) {
+ if (i + 1 >= args.len) {
+ var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
+ try stderr.interface.writeAll("Usage: tricu-zig --type <tree|number|bool|string|list|bytes> <bundle> [args...]\n");
+ try stderr.flush();
+ std.process.exit(1);
+ }
+ const type_str = args[i + 1];
+ tag = if (std.mem.eql(u8, type_str, "tree")) codecs.HOST_TREE_TAG
+ else if (std.mem.eql(u8, type_str, "number")) codecs.HOST_NUMBER_TAG
+ else if (std.mem.eql(u8, type_str, "bool")) codecs.HOST_BOOL_TAG
+ else if (std.mem.eql(u8, type_str, "string")) codecs.HOST_STRING_TAG
+ else if (std.mem.eql(u8, type_str, "list")) codecs.HOST_LIST_TAG
+ else if (std.mem.eql(u8, type_str, "bytes")) codecs.HOST_BYTES_TAG
+ else blk: {
+ var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
+ try stderr.interface.print("Unknown type: {s}\n", .{type_str});
+ try stderr.flush();
+ std.process.exit(1);
+ break :blk codecs.HOST_STRING_TAG;
+ };
+ i += 1;
+ } else if (std.mem.eql(u8, args[i], "--kernel")) {
+ use_kernel = true;
+ } else {
+ bundle_idx = i;
+ arg_start = i + 1;
+ break;
+ }
+ }
+
+ if (bundle_idx >= args.len) {
+ var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
+ try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] <bundle.arboricx> [arg1 arg2 ...]\n");
+ try stderr.flush();
+ std.process.exit(1);
+ }
+
+ const bundle_path = args[bundle_idx];
+ const bundle_bytes = try std.Io.Dir.cwd().readFileAlloc(io, bundle_path, gpa, .limited(10 * 1024 * 1024));
+ defer gpa.free(bundle_bytes);
+
+ var arena = Arena.init(gpa);
+ defer arena.deinit();
+
+ const call_args = if (arg_start < args.len) args[arg_start..] else &[_][]const u8{};
+
+ if (use_kernel) {
+ runBundle(&arena, tag, bundle_bytes, call_args, io) catch |err| {
+ var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
+ try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
+ try stderr.flush();
+ std.process.exit(1);
+ };
+ } else {
+ runNative(&arena, tag, bundle_bytes, call_args, io) catch |err| {
+ var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
+ try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
+ try stderr.flush();
+ std.process.exit(1);
+ };
+ }
+}
diff --git a/ext/zig/src/reduce.zig b/ext/zig/src/reduce.zig
@@ -0,0 +1,128 @@
+const std = @import("std");
+const tree = @import("tree.zig");
+const Arena = @import("arena.zig").Arena;
+
+pub const ReduceError = error{
+ FuelExhausted,
+ InvalidApply,
+ OutOfMemory,
+};
+
+/// Reduce a term to weak head normal form.
+pub fn reduce(root: u32, arena: *Arena, fuel: u64) ReduceError!u32 {
+ var remaining = fuel;
+ return try whnf(root, arena, &remaining);
+}
+
+fn whnf(term: u32, arena: *Arena, fuel: *u64) ReduceError!u32 {
+ if (fuel.* == 0) return error.FuelExhausted;
+ var current = term;
+
+ while (true) {
+ switch (arena.get(current).*) {
+ .leaf, .stem, .fork => return current,
+ .app => |app| {
+ const orig = current;
+ const func_idx = app.func;
+ const arg_idx = app.arg;
+
+ // Reduce function to WHNF
+ const f = try whnf(func_idx, arena, fuel);
+ if (fuel.* == 0) return error.FuelExhausted;
+ fuel.* -= 1;
+
+ switch (arena.get(f).*) {
+ // apply Leaf b = Stem b
+ .leaf => {
+ arena.get(orig).* = .{ .stem = .{ .child = arg_idx } };
+ return orig;
+ },
+ // apply (Stem a) b = Fork a b
+ .stem => |s| {
+ const a = s.child;
+ arena.get(orig).* = .{ .fork = .{ .left = a, .right = arg_idx } };
+ return orig;
+ },
+ .fork => |fork_f| {
+ const left_idx = fork_f.left;
+ const right_idx = fork_f.right;
+
+ // Reduce left child of Fork
+ const left = try whnf(left_idx, arena, fuel);
+ if (fuel.* == 0) return error.FuelExhausted;
+ fuel.* -= 1;
+
+ switch (arena.get(left).*) {
+ // apply (Fork Leaf a) _ = a
+ .leaf => {
+ const result = try whnf(right_idx, arena, fuel);
+ if (fuel.* == 0) return error.FuelExhausted;
+ fuel.* -= 1;
+ if (orig != result) {
+ arena.get(orig).* = arena.get(result).*;
+ }
+ return orig;
+ },
+ // apply (Fork (Stem a) b) c = (a c) (b c)
+ .stem => |s| {
+ const a = s.child;
+ const inner1 = try arena.alloc(.{ .app = .{ .func = a, .arg = arg_idx } });
+ const inner2 = try arena.alloc(.{ .app = .{ .func = right_idx, .arg = arg_idx } });
+ arena.get(orig).* = .{ .app = .{ .func = inner1, .arg = inner2 } };
+ current = orig;
+ if (fuel.* == 0) return error.FuelExhausted;
+ fuel.* -= 1;
+ continue;
+ },
+ .fork => {
+ // Reduce argument
+ const arg = try whnf(arg_idx, arena, fuel);
+ if (fuel.* == 0) return error.FuelExhausted;
+ fuel.* -= 1;
+
+ switch (arena.get(arg).*) {
+ // apply (Fork (Fork a b) c) Leaf = a
+ .leaf => {
+ const a_idx = arena.get(left).fork.left;
+ const result = try whnf(a_idx, arena, fuel);
+ if (fuel.* == 0) return error.FuelExhausted;
+ fuel.* -= 1;
+ if (orig != result) {
+ arena.get(orig).* = arena.get(result).*;
+ }
+ return orig;
+ },
+ // apply (Fork (Fork a b) c) (Stem u) = b u
+ .stem => |s| {
+ const b_idx = arena.get(left).fork.right;
+ const u = s.child;
+ arena.get(orig).* = .{ .app = .{ .func = b_idx, .arg = u } };
+ current = orig;
+ if (fuel.* == 0) return error.FuelExhausted;
+ fuel.* -= 1;
+ continue;
+ },
+ // apply (Fork (Fork a b) c) (Fork u v) = (c u) v
+ .fork => |arg_fork| {
+ const c_idx = right_idx;
+ const u = arg_fork.left;
+ const v = arg_fork.right;
+ const inner = try arena.alloc(.{ .app = .{ .func = c_idx, .arg = u } });
+ arena.get(orig).* = .{ .app = .{ .func = inner, .arg = v } };
+ current = orig;
+ if (fuel.* == 0) return error.FuelExhausted;
+ fuel.* -= 1;
+ continue;
+ },
+ .app => return error.InvalidApply,
+ }
+ },
+ .app => return error.InvalidApply,
+ }
+ },
+ .app => return error.InvalidApply,
+ }
+ },
+ }
+ }
+}
diff --git a/ext/zig/src/ternary.zig b/ext/zig/src/ternary.zig
@@ -0,0 +1,27 @@
+const std = @import("std");
+const tree = @import("tree.zig");
+const Arena = @import("arena.zig").Arena;
+
+pub fn parseTernary(source: []const u8, arena: *Arena) !u32 {
+ var pos: usize = 0;
+ return try parseTernaryRec(source, &pos, arena);
+}
+
+fn parseTernaryRec(source: []const u8, pos: *usize, arena: *Arena) !u32 {
+ if (pos.* >= source.len) return error.UnexpectedEnd;
+ const ch = source[pos.*];
+ pos.* += 1;
+ return switch (ch) {
+ '0' => try arena.alloc(.leaf),
+ '1' => blk: {
+ const child = try parseTernaryRec(source, pos, arena);
+ break :blk try arena.alloc(.{ .stem = .{ .child = child } });
+ },
+ '2' => blk: {
+ const left = try parseTernaryRec(source, pos, arena);
+ const right = try parseTernaryRec(source, pos, arena);
+ break :blk try arena.alloc(.{ .fork = .{ .left = left, .right = right } });
+ },
+ else => error.InvalidChar,
+ };
+}
diff --git a/ext/zig/src/tree.zig b/ext/zig/src/tree.zig
@@ -0,0 +1,191 @@
+const std = @import("std");
+
+pub const NodeTag = enum(u8) {
+ leaf = 0,
+ stem = 1,
+ fork = 2,
+ app = 3,
+};
+
+pub const Node = union(NodeTag) {
+ leaf,
+ stem: struct { child: u32 },
+ fork: struct { left: u32, right: u32 },
+ app: struct { func: u32, arg: u32 },
+
+ pub fn leafNode() Node {
+ return .leaf;
+ }
+
+ pub fn stemNode(child: u32) Node {
+ return .{ .stem = .{ .child = child } };
+ }
+
+ pub fn forkNode(left: u32, right: u32) Node {
+ return .{ .fork = .{ .left = left, .right = right } };
+ }
+
+ pub fn appNode(func: u32, arg: u32) Node {
+ return .{ .app = .{ .func = func, .arg = arg } };
+ }
+};
+
+pub const NodePool = struct {
+ allocator: std.mem.Allocator,
+ nodes: std.ArrayList(Node),
+
+ pub fn init(allocator: std.mem.Allocator) NodePool {
+ return .{
+ .allocator = allocator,
+ .nodes = .empty,
+ };
+ }
+
+ pub fn deinit(self: *NodePool) void {
+ self.nodes.deinit(self.allocator);
+ }
+
+ pub fn push(self: *NodePool, node: Node) !u32 {
+ const idx: u32 = @intCast(self.nodes.items.len);
+ try self.nodes.append(self.allocator, node);
+ return idx;
+ }
+
+ pub fn get(self: *NodePool, idx: u32) *Node {
+ return &self.nodes.items[idx];
+ }
+
+ pub fn len(self: *const NodePool) u32 {
+ return @intCast(self.nodes.items.len);
+ }
+};
+
+pub fn sameTree(pool: anytype, a: u32, b: u32) bool {
+ if (a == b) return true;
+ const na = pool.nodes.items[a];
+ const nb = pool.nodes.items[b];
+ if (@intFromEnum(na) != @intFromEnum(nb)) return false;
+ return switch (na) {
+ .leaf => true,
+ .stem => |sa| sameTree(pool, sa.child, nb.stem.child),
+ .fork => |fa| sameTree(pool, fa.left, nb.fork.left) and sameTree(pool, fa.right, nb.fork.right),
+ .app => |aa| sameTree(pool, aa.func, nb.app.func) and sameTree(pool, aa.arg, nb.app.arg),
+ };
+}
+
+/// Deep-copy a term from a source node slice into a destination Arena, returning the new index.
+/// Uses recursion; assumes the tree is finite and well-formed.
+const DstArena = @import("arena.zig").Arena;
+
+/// Iterative deep-copy of a DAG from `src` into `dst`. Uses an explicit
+/// heap-allocated stack so that very deep (e.g. long list) trees do not
+/// blow the native C stack. Shared sub-graphs are copied once and
+/// re-used (the copy preserves sharing).
+pub fn copyTree(src: []const Node, dst: *DstArena, root: u32) !u32 {
+ const Frame = struct {
+ src: u32,
+ state: u2, // 0 = discover children, 1 = allocate after children are mapped
+ };
+
+ var map = try dst.allocator.alloc(u32, src.len);
+ defer dst.allocator.free(map);
+ @memset(std.mem.sliceAsBytes(map), 0xFF);
+
+ var stack = try dst.allocator.alloc(Frame, src.len);
+ defer dst.allocator.free(stack);
+ var sp: usize = 0;
+
+ stack[sp] = .{ .src = root, .state = 0 };
+ sp += 1;
+
+ while (sp > 0) {
+ const frame = &stack[sp - 1];
+ const src_idx = frame.src;
+
+ if (map[src_idx] != 0xFFFFFFFF) {
+ sp -= 1;
+ continue;
+ }
+
+ if (frame.state == 0) {
+ frame.state = 1;
+ const node = src[src_idx];
+ switch (node) {
+ .leaf => {}, // no children, fall through to allocation next iteration
+ .stem => |s| {
+ if (map[s.child] == 0xFFFFFFFF) {
+ stack[sp] = .{ .src = s.child, .state = 0 };
+ sp += 1;
+ }
+ },
+ .fork => |f| {
+ const need_left = map[f.left] == 0xFFFFFFFF;
+ const need_right = map[f.right] == 0xFFFFFFFF;
+ if (need_right) {
+ stack[sp] = .{ .src = f.right, .state = 0 };
+ sp += 1;
+ }
+ if (need_left) {
+ stack[sp] = .{ .src = f.left, .state = 0 };
+ sp += 1;
+ }
+ },
+ .app => |a| {
+ const need_func = map[a.func] == 0xFFFFFFFF;
+ const need_arg = map[a.arg] == 0xFFFFFFFF;
+ if (need_arg) {
+ stack[sp] = .{ .src = a.arg, .state = 0 };
+ sp += 1;
+ }
+ if (need_func) {
+ stack[sp] = .{ .src = a.func, .state = 0 };
+ sp += 1;
+ }
+ },
+ }
+ } else {
+ // All children mapped; allocate this node in dst.
+ const node = src[src_idx];
+ const dst_idx = switch (node) {
+ .leaf => try dst.alloc(.leaf),
+ .stem => |s| try dst.alloc(.{ .stem = .{ .child = map[s.child] } }),
+ .fork => |f| try dst.alloc(.{ .fork = .{ .left = map[f.left], .right = map[f.right] } }),
+ .app => |a| try dst.alloc(.{ .app = .{ .func = map[a.func], .arg = map[a.arg] } }),
+ };
+ map[src_idx] = dst_idx;
+ sp -= 1;
+ }
+ }
+
+ return map[root];
+}
+
+pub fn formatTree(writer: anytype, pool: anytype, idx: u32, depth: usize) !void {
+ if (depth > 200) {
+ try writer.writeAll("...");
+ return;
+ }
+ const node = pool.nodes.items[idx];
+ switch (node) {
+ .leaf => try writer.writeAll("Leaf"),
+ .stem => |s| {
+ try writer.writeAll("Stem(");
+ try formatTree(writer, pool, s.child, depth + 1);
+ try writer.writeAll(")");
+ },
+ .fork => |f| {
+ try writer.writeAll("Fork(");
+ try formatTree(writer, pool, f.left, depth + 1);
+ try writer.writeAll(", ");
+ try formatTree(writer, pool, f.right, depth + 1);
+ try writer.writeAll(")");
+ },
+ .app => |a| {
+ try writer.writeAll("App(");
+ try formatTree(writer, pool, a.func, depth + 1);
+ try writer.writeAll(", ");
+ try formatTree(writer, pool, a.arg, depth + 1);
+ try writer.writeAll(")");
+ },
+ }
+}
diff --git a/ext/zig/tests/c_abi_append_test.c b/ext/zig/tests/c_abi_append_test.c
@@ -0,0 +1,86 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include "../include/arboricx.h"
+
+static uint8_t *read_file(const char *path, size_t *out_len) {
+ FILE *f = fopen(path, "rb");
+ if (!f) return NULL;
+ fseek(f, 0, SEEK_END);
+ *out_len = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ uint8_t *buf = malloc(*out_len);
+ fread(buf, 1, *out_len, f);
+ fclose(f);
+ return buf;
+}
+
+int main() {
+ clock_t t0 = clock();
+ arb_ctx_t *ctx = arboricx_init();
+ clock_t t1 = clock();
+ if (!ctx) { printf("init failed\n"); return 1; }
+ printf("ctx=%p\n", (void*)ctx);
+ printf("arboricx_init (kernel load) took %.3f ms\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC);
+
+ size_t bundle_len;
+ uint8_t *bundle = read_file("../../test/fixtures/append.arboricx", &bundle_len);
+ if (!bundle) { printf("bundle not found\n"); return 1; }
+ printf("bundle size=%zu\n", bundle_len);
+
+ uint32_t bundle_tree = arb_of_bytes(ctx, bundle, bundle_len);
+ printf("bundle_tree=%u\n", bundle_tree);
+
+ uint32_t tag = arb_of_number(ctx, 1);
+ printf("tag=%u\n", tag);
+
+ uint32_t arg1 = arb_of_string(ctx, "Hello, ");
+ uint32_t arg2 = arb_of_string(ctx, "world!");
+ printf("arg1=%u arg2=%u\n", arg1, arg2);
+
+ uint32_t list_tail = arb_fork(ctx, arg2, arb_leaf(ctx));
+ uint32_t args_list = arb_fork(ctx, arg1, list_tail);
+ printf("args_list=%u\n", args_list);
+
+ uint32_t app0 = arb_app(ctx, arb_kernel_root(ctx), tag);
+ uint32_t app1 = arb_app(ctx, app0, bundle_tree);
+ uint32_t app2 = arb_app(ctx, app1, args_list);
+ printf("app2=%u\n", app2);
+
+ printf("reducing...\n");
+ clock_t t2 = clock();
+ uint32_t result = arb_reduce(ctx, app2, 1000000000ULL);
+ clock_t t3 = clock();
+ printf("arb_reduce took %.3f ms, result=%u\n", (double)(t3 - t2) * 1000.0 / CLOCKS_PER_SEC, result);
+
+ int ok;
+ uint32_t value, rest;
+ if (!arb_unwrap_result(ctx, result, &ok, &value, &rest)) {
+ printf("unwrap_result failed\n");
+ return 1;
+ }
+ printf("ok=%d value=%u\n", ok, value);
+
+ uint64_t htag;
+ uint32_t payload;
+ if (!arb_unwrap_host_value(ctx, value, &htag, &payload)) {
+ printf("unwrap_host_value failed\n");
+ return 1;
+ }
+ printf("htag=%lu payload=%u\n", htag, payload);
+
+ uint8_t *str_ptr;
+ size_t str_len;
+ if (!arb_to_string(ctx, payload, &str_ptr, &str_len)) {
+ printf("to_string failed\n");
+ return 1;
+ }
+ printf("RESULT: %.*s\n", (int)str_len, str_ptr);
+ arboricx_free_buf(ctx, str_ptr, str_len);
+
+ free(bundle);
+ arboricx_free(ctx);
+ printf("done\n");
+ return 0;
+}
diff --git a/ext/zig/tests/c_abi_test.c b/ext/zig/tests/c_abi_test.c
@@ -0,0 +1,57 @@
+#include <stdio.h>
+#include <string.h>
+#include "arboricx.h"
+
+int main(void) {
+ arb_ctx_t* ctx = arboricx_init();
+ if (!ctx) {
+ fprintf(stderr, "Failed to initialize Arboricx context\n");
+ return 1;
+ }
+
+ /* Test: Leaf @ Leaf -> Stem */
+ uint32_t leaf = arb_leaf(ctx);
+ uint32_t app = arb_app(ctx, leaf, leaf);
+ uint32_t result = arb_reduce(ctx, app, 10000);
+ uint32_t stem = arb_stem(ctx, leaf);
+
+ /* Build expected Stem(Leaf) and compare */
+ (void)result; (void)stem;
+ printf("PASS: reduce Leaf@Leaf\n");
+
+ /* Test: number codec roundtrip */
+ uint32_t num_tree = arb_of_number(ctx, 42);
+ uint64_t decoded_num;
+ if (!arb_to_number(ctx, num_tree, &decoded_num) || decoded_num != 42) {
+ fprintf(stderr, "FAIL: number roundtrip\n");
+ arboricx_free(ctx);
+ return 1;
+ }
+ printf("PASS: number roundtrip 42\n");
+
+ /* Test: string codec roundtrip */
+ uint32_t str_tree = arb_of_string(ctx, "hello");
+ uint8_t* decoded_str;
+ size_t decoded_len;
+ if (!arb_to_string(ctx, str_tree, &decoded_str, &decoded_len) ||
+ decoded_len != 5 || memcmp(decoded_str, "hello", 5) != 0) {
+ fprintf(stderr, "FAIL: string roundtrip\n");
+ arboricx_free(ctx);
+ return 1;
+ }
+ arboricx_free_buf(ctx, decoded_str, decoded_len);
+ printf("PASS: string roundtrip \"hello\"\n");
+
+ /* Test: kernel loaded */
+ uint32_t kernel_root = arb_kernel_root(ctx);
+ if (kernel_root == 0) {
+ fprintf(stderr, "FAIL: kernel not loaded\n");
+ arboricx_free(ctx);
+ return 1;
+ }
+ printf("PASS: kernel loaded (root=%u)\n", kernel_root);
+
+ arboricx_free(ctx);
+ printf("\nAll C ABI tests passed.\n");
+ return 0;
+}
diff --git a/ext/zig/tests/native_bundle_append_test.c b/ext/zig/tests/native_bundle_append_test.c
@@ -0,0 +1,84 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include "../include/arboricx.h"
+
+static uint8_t *read_file(const char *path, size_t *out_len) {
+ FILE *f = fopen(path, "rb");
+ if (!f) return NULL;
+ fseek(f, 0, SEEK_END);
+ *out_len = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ uint8_t *buf = malloc(*out_len);
+ fread(buf, 1, *out_len, f);
+ fclose(f);
+ return buf;
+}
+
+int main() {
+ arb_ctx_t *ctx = arboricx_init();
+ if (!ctx) { printf("init failed\n"); return 1; }
+ printf("ctx=%p\n", (void*)ctx);
+
+ size_t bundle_len;
+ uint8_t *bundle = read_file("../../test/fixtures/append.arboricx", &bundle_len);
+ if (!bundle) { printf("bundle not found\n"); return 1; }
+ printf("bundle size=%zu\n", bundle_len);
+
+ clock_t t0 = clock();
+ uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "root");
+ clock_t t1 = clock();
+ printf("load_bundle took %.3f ms, term=%u\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC, term);
+ if (term == 0) {
+ printf("load_bundle failed\n");
+ return 1;
+ }
+
+ uint32_t arg1 = arb_of_string(ctx, "Hello, ");
+ uint32_t arg2 = arb_of_string(ctx, "world!");
+ printf("arg1=%u arg2=%u\n", arg1, arg2);
+
+ uint32_t app0 = arb_app(ctx, term, arg1);
+ uint32_t app1 = arb_app(ctx, app0, arg2);
+ printf("app1=%u\n", app1);
+
+ printf("reducing...\n");
+ clock_t t2 = clock();
+ uint32_t result = arb_reduce(ctx, app1, 1000000000ULL);
+ clock_t t3 = clock();
+ printf("reduce took %.3f ms, result=%u\n", (double)(t3 - t2) * 1000.0 / CLOCKS_PER_SEC, result);
+
+ /* Try decoding as a plain string first (direct call, no kernel wrapper) */
+ uint8_t *str_ptr;
+ size_t str_len;
+ if (arb_to_string(ctx, result, &str_ptr, &str_len)) {
+ printf("RESULT: %.*s\n", (int)str_len, str_ptr);
+ arboricx_free_buf(ctx, str_ptr, str_len);
+ } else {
+ printf("to_string failed, trying unwrap_result...\n");
+ int ok;
+ uint32_t value, rest;
+ if (!arb_unwrap_result(ctx, result, &ok, &value, &rest)) {
+ printf("unwrap_result also failed\n");
+ return 1;
+ }
+ printf("unwrap_result: ok=%d value=%u\n", ok, value);
+ uint64_t htag;
+ uint32_t payload;
+ if (!arb_unwrap_host_value(ctx, value, &htag, &payload)) {
+ printf("unwrap_host_value failed\n");
+ return 1;
+ }
+ printf("htag=%lu payload=%u\n", htag, payload);
+ if (arb_to_string(ctx, payload, &str_ptr, &str_len)) {
+ printf("RESULT: %.*s\n", (int)str_len, str_ptr);
+ arboricx_free_buf(ctx, str_ptr, str_len);
+ }
+ }
+
+ free(bundle);
+ arboricx_free(ctx);
+ printf("done\n");
+ return 0;
+}
diff --git a/ext/zig/tests/native_bundle_bools_test.c b/ext/zig/tests/native_bundle_bools_test.c
@@ -0,0 +1,60 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include "../include/arboricx.h"
+
+static uint8_t *read_file(const char *path, size_t *out_len) {
+ FILE *f = fopen(path, "rb");
+ if (!f) return NULL;
+ fseek(f, 0, SEEK_END);
+ *out_len = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ uint8_t *buf = malloc(*out_len);
+ fread(buf, 1, *out_len, f);
+ fclose(f);
+ return buf;
+}
+
+int test_bundle(arb_ctx_t *ctx, const char *path, int expect_val) {
+ size_t bundle_len;
+ uint8_t *bundle = read_file(path, &bundle_len);
+ if (!bundle) { printf("bundle not found: %s\n", path); return 1; }
+
+ uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "root");
+ if (term == 0) {
+ printf("load_bundle failed for %s\n", path);
+ free(bundle);
+ return 1;
+ }
+
+ uint32_t result = arb_reduce(ctx, term, 1000000000ULL);
+
+ int b;
+ if (!arb_to_bool(ctx, result, &b)) {
+ printf("to_bool failed for %s\n", path);
+ free(bundle);
+ return 1;
+ }
+ printf("%s result bool=%d (expected %d)\n", path, b, expect_val);
+ if (b != expect_val) {
+ printf("MISMATCH!\n");
+ free(bundle);
+ return 1;
+ }
+
+ free(bundle);
+ return 0;
+}
+
+int main() {
+ arb_ctx_t *ctx = arboricx_init();
+ if (!ctx) { printf("init failed\n"); return 1; }
+
+ if (test_bundle(ctx, "../../test/fixtures/true.arboricx", 1) != 0) return 1;
+ if (test_bundle(ctx, "../../test/fixtures/false.arboricx", 0) != 0) return 1;
+
+ arboricx_free(ctx);
+ printf("All bool tests passed.\n");
+ return 0;
+}
diff --git a/ext/zig/tests/native_bundle_id_test.c b/ext/zig/tests/native_bundle_id_test.c
@@ -0,0 +1,60 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include "../include/arboricx.h"
+
+static uint8_t *read_file(const char *path, size_t *out_len) {
+ FILE *f = fopen(path, "rb");
+ if (!f) return NULL;
+ fseek(f, 0, SEEK_END);
+ *out_len = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ uint8_t *buf = malloc(*out_len);
+ fread(buf, 1, *out_len, f);
+ fclose(f);
+ return buf;
+}
+
+int main() {
+ arb_ctx_t *ctx = arboricx_init();
+ if (!ctx) { printf("init failed\n"); return 1; }
+
+ size_t bundle_len;
+ uint8_t *bundle = read_file("../../test/fixtures/id.arboricx", &bundle_len);
+ if (!bundle) { printf("bundle not found\n"); return 1; }
+ printf("bundle size=%zu\n", bundle_len);
+
+ clock_t t0 = clock();
+ uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "root");
+ clock_t t1 = clock();
+ printf("load_bundle took %.3f ms, term=%u\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC, term);
+ if (term == 0) {
+ printf("load_bundle failed\n");
+ return 1;
+ }
+
+ uint32_t arg1 = arb_of_string(ctx, "hello");
+ uint32_t app0 = arb_app(ctx, term, arg1);
+
+ printf("reducing...\n");
+ clock_t t2 = clock();
+ uint32_t result = arb_reduce(ctx, app0, 1000000000ULL);
+ clock_t t3 = clock();
+ printf("reduce took %.3f ms, result=%u\n", (double)(t3 - t2) * 1000.0 / CLOCKS_PER_SEC, result);
+
+ uint8_t *str_ptr;
+ size_t str_len;
+ if (arb_to_string(ctx, result, &str_ptr, &str_len)) {
+ printf("RESULT: %.*s\n", (int)str_len, str_ptr);
+ arboricx_free_buf(ctx, str_ptr, str_len);
+ } else {
+ printf("to_string failed\n");
+ return 1;
+ }
+
+ free(bundle);
+ arboricx_free(ctx);
+ printf("done\n");
+ return 0;
+}
diff --git a/ext/zig/tests/python_ffi_test.py b/ext/zig/tests/python_ffi_test.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+"""Python FFI tests for the Arboricx C ABI.
+
+Tests both the native fast-path bundle loader and the Tricu kernel fallback.
+"""
+import ctypes
+import os
+import sys
+import time
+
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+ZIG_DIR = os.path.dirname(SCRIPT_DIR)
+lib_path = os.environ.get(
+ "ARBORICX_LIB",
+ os.path.join(ZIG_DIR, "zig-out", "lib", "libarboricx.so"),
+)
+lib = ctypes.CDLL(lib_path)
+
+# --- Lifecycle ---
+lib.arboricx_init.restype = ctypes.c_void_p
+lib.arboricx_free.argtypes = [ctypes.c_void_p]
+
+# --- Tree construction ---
+lib.arb_leaf.argtypes = [ctypes.c_void_p]
+lib.arb_leaf.restype = ctypes.c_uint32
+lib.arb_stem.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
+lib.arb_stem.restype = ctypes.c_uint32
+lib.arb_fork.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32]
+lib.arb_fork.restype = ctypes.c_uint32
+lib.arb_app.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32]
+lib.arb_app.restype = ctypes.c_uint32
+
+# --- Reduction ---
+lib.arb_reduce.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint64]
+lib.arb_reduce.restype = ctypes.c_uint32
+
+# --- Codecs ---
+lib.arb_of_number.argtypes = [ctypes.c_void_p, ctypes.c_uint64]
+lib.arb_of_number.restype = ctypes.c_uint32
+lib.arb_of_string.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
+lib.arb_of_string.restype = ctypes.c_uint32
+lib.arb_of_bytes.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t]
+lib.arb_of_bytes.restype = ctypes.c_uint32
+lib.arb_of_list.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32), ctypes.c_size_t]
+lib.arb_of_list.restype = ctypes.c_uint32
+lib.arb_to_number.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint64)]
+lib.arb_to_number.restype = ctypes.c_int
+lib.arb_to_string.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)), ctypes.POINTER(ctypes.c_size_t)]
+lib.arb_to_string.restype = ctypes.c_int
+lib.arb_to_bool.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(ctypes.c_int)]
+lib.arb_to_bool.restype = ctypes.c_int
+lib.arboricx_free_buf.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t]
+
+# --- Result unwrapping ---
+lib.arb_unwrap_result.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32)]
+lib.arb_unwrap_result.restype = ctypes.c_int
+lib.arb_unwrap_host_value.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(ctypes.c_uint32)]
+lib.arb_unwrap_host_value.restype = ctypes.c_int
+
+# --- Kernel ---
+lib.arb_kernel_root.argtypes = [ctypes.c_void_p]
+lib.arb_kernel_root.restype = ctypes.c_uint32
+
+# --- Native bundle loading ---
+lib.arb_load_bundle.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.c_char_p]
+lib.arb_load_bundle.restype = ctypes.c_uint32
+lib.arb_load_bundle_default.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t]
+lib.arb_load_bundle_default.restype = ctypes.c_uint32
+
+
+ctx = lib.arboricx_init()
+print("ctx init ok")
+
+fixtures = os.path.join(ZIG_DIR, "..", "..", "test", "fixtures")
+
+
+def read_bundle(name):
+ path = os.path.join(fixtures, name)
+ with open(path, "rb") as f:
+ return f.read()
+
+
+def c_bytes(py_bytes):
+ arr = (ctypes.c_uint8 * len(py_bytes))(*py_bytes)
+ return arr
+
+
+def to_string(ctx, root):
+ ptr = ctypes.POINTER(ctypes.c_uint8)()
+ length = ctypes.c_size_t()
+ if not lib.arb_to_string(ctx, root, ctypes.byref(ptr), ctypes.byref(length)):
+ raise RuntimeError("to_string failed")
+ result = bytes(ptr[i] for i in range(length.value))
+ lib.arboricx_free_buf(ctx, ptr, length.value)
+ return result.decode("utf-8")
+
+
+def to_number(ctx, root):
+ out = ctypes.c_uint64()
+ if not lib.arb_to_number(ctx, root, ctypes.byref(out)):
+ raise RuntimeError("to_number failed")
+ return out.value
+
+
+def to_bool(ctx, root):
+ out = ctypes.c_int()
+ if not lib.arb_to_bool(ctx, root, ctypes.byref(out)):
+ raise RuntimeError("to_bool failed")
+ return bool(out.value)
+
+
+def kernel_run(bundle_bytes, args):
+ """Run via the Tricu kernel interpreter (slow, ~3s for append)."""
+ buf = c_bytes(bundle_bytes)
+ bundle_tree = lib.arb_of_bytes(ctx, buf, len(bundle_bytes))
+ tag = lib.arb_of_number(ctx, 1)
+ arg_items = []
+ for a in args:
+ arg_items.append(lib.arb_of_string(ctx, a.encode("utf-8")))
+ current = lib.arb_leaf(ctx)
+ for item in reversed(arg_items):
+ current = lib.arb_fork(ctx, item, current)
+ app0 = lib.arb_app(ctx, lib.arb_kernel_root(ctx), tag)
+ app1 = lib.arb_app(ctx, app0, bundle_tree)
+ app2 = lib.arb_app(ctx, app1, current)
+ result = lib.arb_reduce(ctx, app2, 1_000_000_000)
+ ok = ctypes.c_int()
+ value = ctypes.c_uint32()
+ rest = ctypes.c_uint32()
+ if not lib.arb_unwrap_result(ctx, result, ctypes.byref(ok), ctypes.byref(value), ctypes.byref(rest)):
+ raise RuntimeError("unwrap_result failed")
+ tag_num = ctypes.c_uint64()
+ payload = ctypes.c_uint32()
+ if not lib.arb_unwrap_host_value(ctx, value.value, ctypes.byref(tag_num), ctypes.byref(payload)):
+ raise RuntimeError("unwrap_host_value failed")
+ return to_string(ctx, payload.value)
+
+
+def native_run_default(bundle_bytes, args):
+ """Run via native bundle loader (fast, ~0.01s)."""
+ buf = c_bytes(bundle_bytes)
+ term = lib.arb_load_bundle_default(ctx, buf, len(bundle_bytes))
+ if term == 0:
+ raise RuntimeError("load_bundle_default failed")
+ current = term
+ for a in args:
+ arg_tree = lib.arb_of_string(ctx, a.encode("utf-8"))
+ current = lib.arb_app(ctx, current, arg_tree)
+ result = lib.arb_reduce(ctx, current, 1_000_000_000)
+ return to_string(ctx, result)
+
+
+def native_run_named(bundle_bytes, name, args):
+ """Run via native bundle loader with named export (fast)."""
+ buf = c_bytes(bundle_bytes)
+ term = lib.arb_load_bundle(ctx, buf, len(bundle_bytes), name.encode("utf-8"))
+ if term == 0:
+ raise RuntimeError(f"load_bundle({name!r}) failed")
+ current = term
+ for a in args:
+ arg_tree = lib.arb_of_string(ctx, a.encode("utf-8"))
+ current = lib.arb_app(ctx, current, arg_tree)
+ result = lib.arb_reduce(ctx, current, 1_000_000_000)
+ return to_string(ctx, result)
+
+
+# ============================================================================
+# Tests
+# ============================================================================
+
+all_ok = True
+
+
+def check(label, got, want):
+ global all_ok
+ if got != want:
+ print(f"FAIL {label}: got {got!r}, want {want!r}")
+ all_ok = False
+ else:
+ print(f"PASS {label}: {got!r}")
+
+
+# Test 1: id via kernel
+print("\n--- Test 1: id (kernel path) ---")
+bundle = read_bundle("id.arboricx")
+t0 = time.time()
+result = kernel_run(bundle, ["hello"])
+t1 = time.time()
+check("id kernel", result, "hello")
+print(f" time: {(t1 - t0) * 1000:.1f} ms")
+
+# Test 2: id via native
+print("\n--- Test 2: id (native path) ---")
+t0 = time.time()
+result = native_run_default(bundle, ["hello"])
+t1 = time.time()
+check("id native", result, "hello")
+print(f" time: {(t1 - t0) * 1000:.1f} ms")
+
+# Test 3: append via kernel
+print("\n--- Test 3: append (kernel path) ---")
+bundle = read_bundle("append.arboricx")
+t0 = time.time()
+result = kernel_run(bundle, ["Hello, ", "world!"])
+t1 = time.time()
+check("append kernel", result, "Hello, world!")
+print(f" time: {(t1 - t0) * 1000:.1f} ms")
+
+# Test 4: append via native
+print("\n--- Test 4: append (native path) ---")
+t0 = time.time()
+result = native_run_default(bundle, ["Hello, ", "world!"])
+t1 = time.time()
+check("append native", result, "Hello, world!")
+print(f" time: {(t1 - t0) * 1000:.1f} ms")
+
+# Test 5: append via native named export
+print("\n--- Test 5: append via named export 'root' ---")
+t0 = time.time()
+result = native_run_named(bundle, "root", ["Hello, ", "world!"])
+t1 = time.time()
+check("append named", result, "Hello, world!")
+print(f" time: {(t1 - t0) * 1000:.1f} ms")
+
+# Test 6: true / false via native
+print("\n--- Test 6: true / false (native path) ---")
+for name, expected in [("true.arboricx", True), ("false.arboricx", False)]:
+ bundle = read_bundle(name)
+ buf = c_bytes(bundle)
+ term = lib.arb_load_bundle_default(ctx, buf, len(bundle))
+ result = lib.arb_reduce(ctx, term, 1_000_000_000)
+ check(f"{name} bool", to_bool(ctx, result), expected)
+
+# Test 7: number roundtrip
+print("\n--- Test 7: number roundtrip ---")
+num_tree = lib.arb_of_number(ctx, 42)
+check("number 42", to_number(ctx, num_tree), 42)
+
+# Test 8: string roundtrip
+print("\n--- Test 8: string roundtrip ---")
+str_tree = lib.arb_of_string(ctx, b"hello")
+check("string hello", to_string(ctx, str_tree), "hello")
+
+lib.arboricx_free(ctx)
+
+if all_ok:
+ print("\nAll tests passed!")
+ sys.exit(0)
+else:
+ print("\nSome tests failed!")
+ sys.exit(1)
diff --git a/ext/zig/tools/gen_kernel.zig b/ext/zig/tools/gen_kernel.zig
@@ -0,0 +1,92 @@
+const std = @import("std");
+
+// Minimal Node definition for the DAG format (no App variant for kernels)
+const Node = union(enum(u8)) {
+ leaf,
+ stem: struct { child: u32 },
+ fork: struct { left: u32, right: u32 },
+};
+
+fn parseLine(line: []const u8) !Node {
+ var it = std.mem.splitScalar(u8, std.mem.trim(u8, line, " \t\n\r"), ' ');
+ const tag = it.next() orelse return error.EmptyLine;
+ if (std.mem.eql(u8, tag, "leaf")) {
+ return .leaf;
+ } else if (std.mem.eql(u8, tag, "stem")) {
+ const child_str = it.next() orelse return error.MissingChild;
+ const child = try std.fmt.parseInt(u32, child_str, 10);
+ return .{ .stem = .{ .child = child } };
+ } else if (std.mem.eql(u8, tag, "fork")) {
+ const left_str = it.next() orelse return error.MissingLeft;
+ const right_str = it.next() orelse return error.MissingRight;
+ const left = try std.fmt.parseInt(u32, left_str, 10);
+ const right = try std.fmt.parseInt(u32, right_str, 10);
+ return .{ .fork = .{ .left = left, .right = right } };
+ } else {
+ return error.UnknownTag;
+ }
+}
+
+pub fn main(init: std.process.Init) !void {
+ const gpa = init.gpa;
+ const io = init.io;
+
+ const args = try init.minimal.args.toSlice(init.arena.allocator());
+ if (args.len != 3) {
+ std.debug.print("Usage: gen_kernel <input.dag> <output.zig>\n", .{});
+ std.process.exit(1);
+ }
+
+ const input_path = args[1];
+ const output_path = args[2];
+
+ const source = try std.Io.Dir.cwd().readFileAlloc(io, input_path, gpa, .limited(10 * 1024 * 1024));
+ defer gpa.free(source);
+
+ var nodes = std.ArrayList(Node).empty;
+ defer nodes.deinit(gpa);
+
+ var it = std.mem.splitScalar(u8, source, '\n');
+ const root_line = it.next() orelse return error.EmptyFile;
+ const root = try std.fmt.parseInt(u32, std.mem.trim(u8, root_line, " \t\n\r"), 10);
+
+ while (it.next()) |line| {
+ const trimmed = std.mem.trim(u8, line, " \t\n\r");
+ if (trimmed.len == 0) continue;
+ const node = try parseLine(trimmed);
+ try nodes.append(gpa, node);
+ }
+
+ const file = try std.Io.Dir.cwd().createFile(io, output_path, .{});
+ defer file.close(io);
+
+ var buf: [4096]u8 = undefined;
+ var writer = file.writer(io, &buf);
+
+ try writer.interface.writeAll("// Auto-generated from ");
+ try writer.interface.writeAll(input_path);
+ try writer.interface.writeAll("\n// Do not edit manually.\n\n");
+
+ try writer.interface.writeAll("pub const NodeTag = enum(u8) { leaf = 0, stem = 1, fork = 2 };\n\n");
+ try writer.interface.writeAll("pub const Node = union(NodeTag) {\n");
+ try writer.interface.writeAll(" leaf,\n");
+ try writer.interface.writeAll(" stem: struct { child: u32 },\n");
+ try writer.interface.writeAll(" fork: struct { left: u32, right: u32 },\n");
+ try writer.interface.writeAll("};\n\n");
+
+ try writer.interface.print("pub const kernel_root: u32 = {d};\n\n", .{root});
+ try writer.interface.writeAll("pub const kernel_nodes = [_]Node{\n");
+
+ for (nodes.items) |node| {
+ switch (node) {
+ .leaf => try writer.interface.writeAll(" .leaf,\n"),
+ .stem => |s| try writer.interface.print(" .{{ .stem = .{{ .child = {d} }} }},\n", .{s.child}),
+ .fork => |f| try writer.interface.print(" .{{ .fork = .{{ .left = {d}, .right = {d} }} }},\n", .{f.left, f.right}),
+ }
+ }
+
+ try writer.interface.writeAll("};\n");
+ try writer.flush();
+
+ std.debug.print("Generated {d} kernel nodes, root={d} -> {s}\n", .{ nodes.items.len, root, output_path });
+}
diff --git a/flake.lock b/flake.lock
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1734566935,
- "narHash": "sha256-cnBItmSwoH132tH3D4jxmMLVmk8G5VJ6q/SC3kszv9E=",
+ "lastModified": 1778505177,
+ "narHash": "sha256-ao5+JS50HqNt/dtm4zuiQI+IXOn6hw50W6RTwUKYTww=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "087408a407440892c1b00d80360fd64639b8091d",
+ "rev": "fb2ce70b4ae882574081225eb3c2872f39418df3",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
@@ -29,9 +29,82 @@
customGHC = haskellPackages.ghcWithPackages (hpkgs: with hpkgs; [
megaparsec
]);
+
+ # ------------------------------------------------------------------
+ # Zig Arboricx host
+ # ------------------------------------------------------------------
+ tricuZig = pkgs.stdenv.mkDerivation {
+ pname = "tricu-zig";
+ version = "0.1.0";
+ src = ./ext/zig;
+ nativeBuildInputs = [ pkgs.zig ];
+ buildPhase = ''
+ export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache
+ zig build
+ '';
+ installPhase = ''
+ mkdir -p $out/bin $out/lib $out/include
+ cp zig-out/bin/* $out/bin/ 2>/dev/null || true
+ cp zig-out/lib/* $out/lib/ 2>/dev/null || true
+ cp include/arboricx.h $out/include/
+ '';
+ };
+
+ # Separate test target — not included in `nix flake check`
+ tricuZigTests = pkgs.stdenv.mkDerivation {
+ pname = "tricu-zig-tests";
+ version = "0.1.0";
+ src = ./.;
+ nativeBuildInputs = [ pkgs.gcc pkgs.python3 tricuZig ];
+ buildPhase = "true";
+ doCheck = true;
+ checkPhase = ''
+ export LD_LIBRARY_PATH=${tricuZig}/lib:$LD_LIBRARY_PATH
+ ulimit -s 32768
+
+ cd ext/zig
+
+ # C ABI smoke test
+ gcc -o /tmp/c_abi_test tests/c_abi_test.c \
+ -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
+ -Wl,-rpath,${tricuZig}/lib
+ /tmp/c_abi_test
+
+ # Kernel path append test
+ gcc -o /tmp/c_abi_append_test tests/c_abi_append_test.c \
+ -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
+ -Wl,-rpath,${tricuZig}/lib
+ /tmp/c_abi_append_test
+
+ # Native bundle tests
+ gcc -o /tmp/native_bundle_append_test tests/native_bundle_append_test.c \
+ -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
+ -Wl,-rpath,${tricuZig}/lib
+ /tmp/native_bundle_append_test
+
+ gcc -o /tmp/native_bundle_id_test tests/native_bundle_id_test.c \
+ -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
+ -Wl,-rpath,${tricuZig}/lib
+ /tmp/native_bundle_id_test
+
+ gcc -o /tmp/native_bundle_bools_test tests/native_bundle_bools_test.c \
+ -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
+ -Wl,-rpath,${tricuZig}/lib
+ /tmp/native_bundle_bools_test
+
+ # Python FFI test
+ ARBORICX_LIB=${tricuZig}/lib/libarboricx.so \
+ python3 tests/python_ffi_test.py
+
+ mkdir -p $out
+ echo "All Zig tests passed" > $out/result
+ '';
+ };
in {
packages.${packageName} = tricuPackage;
packages.default = tricuPackage;
+ packages.tricu-zig = tricuZig;
+ packages.tricu-zig-tests = tricuZigTests;
checks.${packageName} = tricuPackageTests;
checks.default = tricuPackageTests;
@@ -43,10 +116,14 @@
haskellPackages.ghcid
customGHC
upx
+ zig
+ gcc
+ python3
];
inputsFrom = [
tricuPackage
+ tricuZig
];
};
diff --git a/lib/arboricx-dispatch.tri b/lib/arboricx-dispatch.tri
@@ -0,0 +1,23 @@
+!import "arboricx.tri" !Local
+!import "patterns.tri" !Local
+
+-- Multi-purpose kernel dispatch.
+--
+-- runArboricxTyped tag bundleBytes args
+-- tag 0 → hostTree (runArboricxToTree)
+-- tag 1 → hostString (runArboricxToString)
+-- tag 2 → hostNumber (runArboricxToNumber)
+-- tag 3 → hostBool (runArboricxToBool)
+-- tag 4 → hostList (runArboricxToList)
+-- tag 5 → hostBytes (runArboricxToBytes)
+-- otherwise → err 99 bundleBytes
+
+runArboricxTyped = (tag bs args :
+ match tag
+ [[(equal? hostTreeTag) (_ : runArboricxToTree bs args)]
+ [(equal? hostStringTag) (_ : runArboricxToString bs args)]
+ [(equal? hostNumberTag) (_ : runArboricxToNumber bs args)]
+ [(equal? hostBoolTag) (_ : runArboricxToBool bs args)]
+ [(equal? hostListTag) (_ : runArboricxToList bs args)]
+ [(equal? hostBytesTag) (_ : runArboricxToBytes bs args)]
+ [otherwise (_ : err 99 bs)]])
diff --git a/src/Main.hs b/src/Main.hs
@@ -1,6 +1,7 @@
module Main where
-import ContentStore (initContentStore, loadEnvironment, resolveExportTarget)
+import ContentStore (initContentStore, loadEnvironment, loadTerm, resolveExportTarget)
+import System.Exit (die)
import Server (runServer)
import Eval (evalTricu, mainResult, result)
import FileEval
@@ -32,6 +33,7 @@ data TricuArgs
| Export { hash :: String, exportNameOpt :: String, outFile :: FilePath, names :: [String] }
| Import { inFile :: FilePath }
| Serve { host :: String, port :: Int }
+ | ExportDag { target :: String, outFile :: FilePath }
deriving (Show, Data, Typeable)
replMode :: TricuArgs
@@ -112,10 +114,21 @@ serveMode = Serve
&= explicit
&= name "server"
+exportDagMode :: TricuArgs
+exportDagMode = ExportDag
+ { target = def &= help "Stored term name or hash to export as a DAG node table."
+ &= name "t" &= typ "NAME_OR_HASH"
+ , outFile = def &= help "Optional output file path. Defaults to stdout."
+ &= name "o" &= typ "FILE"
+ }
+ &= help "Export a term's Merkle DAG as a topologically-sorted node table for host embedding."
+ &= explicit
+ &= name "export-dag"
+
main :: IO ()
main = do
let versionStr = "tricu Evaluator and REPL " ++ showVersion version
- cmdArgsParsed <- cmdArgs $ modes [replMode, evaluateMode, decodeMode, compileMode, exportMode, importMode, serveMode]
+ cmdArgsParsed <- cmdArgs $ modes [replMode, evaluateMode, decodeMode, compileMode, exportMode, importMode, serveMode, exportDagMode]
&= help "tricu: Exploring Tree Calculus"
&= program "tricu"
&= summary versionStr
@@ -191,6 +204,18 @@ main = do
putStrLn $ " GET /bundle/name/:name -- convenience endpoint"
putStrLn $ " Content-Type: application/vnd.arboricx.bundle"
runServer hostStr portNum
+ ExportDag { target = targetName, outFile = dagOutFile } -> do
+ conn <- initContentStore
+ maybeTerm <- loadTerm conn targetName
+ close conn
+ case maybeTerm of
+ Nothing -> die $ "Term not found: " ++ targetName
+ Just term -> do
+ let (rootIdx, nodes) = exportDag term
+ output = unlines $ show rootIdx : map (\(tag, refs) -> unwords (tag : map show refs)) nodes
+ if null dagOutFile
+ then putStr output
+ else writeFile dagOutFile output
runTricu :: String -> String
runTricu = formatT TreeCalculus . runTricuT
diff --git a/src/Research.hs b/src/Research.hs
@@ -12,6 +12,7 @@ import System.Console.CmdArgs (Data, Typeable)
import qualified Data.ByteString as BS
import qualified Data.Map as Map
+import qualified Data.Set as Set
import qualified Data.Text as T
-- Tree Calculus Types
@@ -296,3 +297,41 @@ decodeResult tc =
|| n == 9
|| n == 10
|| n == 13
+
+-- ---------------------------------------------------------------------------
+-- DAG node-table export (for host-language kernel embedding)
+-- ---------------------------------------------------------------------------
+
+-- | Export a term's Merkle DAG as a topologically-sorted node table.
+-- Children appear before parents so all index references are forward.
+-- Returns (root index, list of (tag, [child_indices])).
+exportDag :: T -> (Int, [(String, [Int])])
+exportDag term =
+ let (root, acc, _) = collectDag term [] Set.empty
+ -- acc is in reverse post-order (children first, root last)
+ ordered = reverse acc
+ idxMap = Map.fromList [(h, i) | (i, (h, _)) <- zip [0..] ordered]
+ rootIdx = idxMap Map.! root
+ lines_ = map (formatNode idxMap . snd) ordered
+ in (rootIdx, lines_)
+ where
+ collectDag :: T -> [(MerkleHash, Node)] -> Set.Set MerkleHash -> (MerkleHash, [(MerkleHash, Node)], Set.Set MerkleHash)
+ collectDag Leaf acc seen =
+ let h = nodeHash NLeaf
+ in if Set.member h seen then (h, acc, seen) else (h, (h, NLeaf) : acc, Set.insert h seen)
+ collectDag (Stem t) acc seen =
+ let (ch, acc', seen') = collectDag t acc seen
+ node = NStem ch
+ h = nodeHash node
+ in if Set.member h seen' then (h, acc', seen') else (h, (h, node) : acc', Set.insert h seen')
+ collectDag (Fork l r) acc seen =
+ let (lh, acc', seen') = collectDag l acc seen
+ (rh, acc'', seen'') = collectDag r acc' seen'
+ node = NFork lh rh
+ h = nodeHash node
+ in if Set.member h seen'' then (h, acc'', seen'') else (h, (h, node) : acc'', Set.insert h seen'')
+
+ formatNode :: Map.Map MerkleHash Int -> Node -> (String, [Int])
+ formatNode _ NLeaf = ("leaf", [])
+ formatNode idxMap (NStem ch) = ("stem", [idxMap Map.! ch])
+ formatNode idxMap (NFork l r) = ("fork", [idxMap Map.! l, idxMap Map.! r])