From d7a7a8134c1fb81f1ca03a81f5d06632f78ad342 Mon Sep 17 00:00:00 2001 From: James Eversole Date: Sun, 10 May 2026 21:21:58 -0500 Subject: [PATCH] feat(zig): native Arboricx bundle parser and C ABI --- AGENTS.md | 172 +- ext/zig/.gitignore | 13 + ext/zig/build.zig | 67 + ext/zig/build.zig.zon | 13 + ext/zig/include/arboricx.h | 54 + ext/zig/kernel_run_arboricx_typed.dag | 2694 +++++++++++++++++++++ ext/zig/src/arena.zig | 36 + ext/zig/src/bundle.zig | 479 ++++ ext/zig/src/c_abi.zig | 183 ++ ext/zig/src/codecs.zig | 205 ++ ext/zig/src/kernel.zig | 22 + ext/zig/src/main.zig | 235 ++ ext/zig/src/reduce.zig | 128 + ext/zig/src/ternary.zig | 27 + ext/zig/src/tree.zig | 191 ++ ext/zig/tests/c_abi_append_test.c | 86 + ext/zig/tests/c_abi_test.c | 57 + ext/zig/tests/native_bundle_append_test.c | 84 + ext/zig/tests/native_bundle_bools_test.c | 60 + ext/zig/tests/native_bundle_id_test.c | 60 + ext/zig/tests/python_ffi_test.py | 251 ++ ext/zig/tools/gen_kernel.zig | 92 + flake.lock | 6 +- flake.nix | 77 + lib/arboricx-dispatch.tri | 23 + src/Main.hs | 29 +- src/Research.hs | 39 + 27 files changed, 5365 insertions(+), 18 deletions(-) create mode 100644 ext/zig/.gitignore create mode 100644 ext/zig/build.zig create mode 100644 ext/zig/build.zig.zon create mode 100644 ext/zig/include/arboricx.h create mode 100644 ext/zig/kernel_run_arboricx_typed.dag create mode 100644 ext/zig/src/arena.zig create mode 100644 ext/zig/src/bundle.zig create mode 100644 ext/zig/src/c_abi.zig create mode 100644 ext/zig/src/codecs.zig create mode 100644 ext/zig/src/kernel.zig create mode 100644 ext/zig/src/main.zig create mode 100644 ext/zig/src/reduce.zig create mode 100644 ext/zig/src/ternary.zig create mode 100644 ext/zig/src/tree.zig create mode 100644 ext/zig/tests/c_abi_append_test.c create mode 100644 ext/zig/tests/c_abi_test.c create mode 100644 ext/zig/tests/native_bundle_append_test.c create mode 100644 ext/zig/tests/native_bundle_bools_test.c create mode 100644 ext/zig/tests/native_bundle_id_test.c create mode 100644 ext/zig/tests/python_ffi_test.py create mode 100644 ext/zig/tools/gen_kernel.zig create mode 100644 lib/arboricx-dispatch.tri diff --git a/AGENTS.md b/AGENTS.md index 0ed865c..2efdced 100644 --- 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 [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 new file mode 100644 index 0000000..69f30d3 --- /dev/null +++ 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 new file mode 100644 index 0000000..bf7b58c --- /dev/null +++ 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 new file mode 100644 index 0000000..8dd1b23 --- /dev/null +++ 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 new file mode 100644 index 0000000..9e0e264 --- /dev/null +++ b/ext/zig/include/arboricx.h @@ -0,0 +1,54 @@ +#ifndef ARBORICX_H +#define ARBORICX_H + +#include +#include + +#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 new file mode 100644 index 0000000..22b0a64 --- /dev/null +++ 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 new file mode 100644 index 0000000..da4fac0 --- /dev/null +++ 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 new file mode 100644 index 0000000..399997b --- /dev/null +++ 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 new file mode 100644 index 0000000..6e9a5b1 --- /dev/null +++ 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 new file mode 100644 index 0000000..b8ee8da --- /dev/null +++ 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 new file mode 100644 index 0000000..5da116f --- /dev/null +++ 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 new file mode 100644 index 0000000..6a9d0ba --- /dev/null +++ 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] [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 [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] [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 new file mode 100644 index 0000000..9626587 --- /dev/null +++ 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 new file mode 100644 index 0000000..698a95e --- /dev/null +++ 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 new file mode 100644 index 0000000..8a9ee52 --- /dev/null +++ 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 new file mode 100644 index 0000000..1db6184 --- /dev/null +++ b/ext/zig/tests/c_abi_append_test.c @@ -0,0 +1,86 @@ +#include +#include +#include +#include +#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 new file mode 100644 index 0000000..5752af0 --- /dev/null +++ b/ext/zig/tests/c_abi_test.c @@ -0,0 +1,57 @@ +#include +#include +#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 new file mode 100644 index 0000000..fec94c1 --- /dev/null +++ b/ext/zig/tests/native_bundle_append_test.c @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#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 new file mode 100644 index 0000000..d6834bf --- /dev/null +++ b/ext/zig/tests/native_bundle_bools_test.c @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#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 new file mode 100644 index 0000000..c71eb5c --- /dev/null +++ b/ext/zig/tests/native_bundle_id_test.c @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#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 new file mode 100644 index 0000000..a5bfdfe --- /dev/null +++ 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 new file mode 100644 index 0000000..a753ca1 --- /dev/null +++ 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 \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 index 4e16d41..eabbce4 100644 --- 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 index cd2a501..6de6997 100644 --- 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 new file mode 100644 index 0000000..bddc1cb --- /dev/null +++ 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 index 6138a3a..61537ff 100644 --- 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 index 3a0eebb..7427395 100644 --- 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])