feat(zig): native Arboricx bundle parser and C ABI

This commit is contained in:
2026-05-10 21:21:58 -05:00
parent 8a673e282d
commit d7a7a8134c
27 changed files with 5365 additions and 18 deletions

View File

@@ -0,0 +1,86 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../include/arboricx.h"
static uint8_t *read_file(const char *path, size_t *out_len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
*out_len = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *buf = malloc(*out_len);
fread(buf, 1, *out_len, f);
fclose(f);
return buf;
}
int main() {
clock_t t0 = clock();
arb_ctx_t *ctx = arboricx_init();
clock_t t1 = clock();
if (!ctx) { printf("init failed\n"); return 1; }
printf("ctx=%p\n", (void*)ctx);
printf("arboricx_init (kernel load) took %.3f ms\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC);
size_t bundle_len;
uint8_t *bundle = read_file("../../test/fixtures/append.arboricx", &bundle_len);
if (!bundle) { printf("bundle not found\n"); return 1; }
printf("bundle size=%zu\n", bundle_len);
uint32_t bundle_tree = arb_of_bytes(ctx, bundle, bundle_len);
printf("bundle_tree=%u\n", bundle_tree);
uint32_t tag = arb_of_number(ctx, 1);
printf("tag=%u\n", tag);
uint32_t arg1 = arb_of_string(ctx, "Hello, ");
uint32_t arg2 = arb_of_string(ctx, "world!");
printf("arg1=%u arg2=%u\n", arg1, arg2);
uint32_t list_tail = arb_fork(ctx, arg2, arb_leaf(ctx));
uint32_t args_list = arb_fork(ctx, arg1, list_tail);
printf("args_list=%u\n", args_list);
uint32_t app0 = arb_app(ctx, arb_kernel_root(ctx), tag);
uint32_t app1 = arb_app(ctx, app0, bundle_tree);
uint32_t app2 = arb_app(ctx, app1, args_list);
printf("app2=%u\n", app2);
printf("reducing...\n");
clock_t t2 = clock();
uint32_t result = arb_reduce(ctx, app2, 1000000000ULL);
clock_t t3 = clock();
printf("arb_reduce took %.3f ms, result=%u\n", (double)(t3 - t2) * 1000.0 / CLOCKS_PER_SEC, result);
int ok;
uint32_t value, rest;
if (!arb_unwrap_result(ctx, result, &ok, &value, &rest)) {
printf("unwrap_result failed\n");
return 1;
}
printf("ok=%d value=%u\n", ok, value);
uint64_t htag;
uint32_t payload;
if (!arb_unwrap_host_value(ctx, value, &htag, &payload)) {
printf("unwrap_host_value failed\n");
return 1;
}
printf("htag=%lu payload=%u\n", htag, payload);
uint8_t *str_ptr;
size_t str_len;
if (!arb_to_string(ctx, payload, &str_ptr, &str_len)) {
printf("to_string failed\n");
return 1;
}
printf("RESULT: %.*s\n", (int)str_len, str_ptr);
arboricx_free_buf(ctx, str_ptr, str_len);
free(bundle);
arboricx_free(ctx);
printf("done\n");
return 0;
}

View File

@@ -0,0 +1,57 @@
#include <stdio.h>
#include <string.h>
#include "arboricx.h"
int main(void) {
arb_ctx_t* ctx = arboricx_init();
if (!ctx) {
fprintf(stderr, "Failed to initialize Arboricx context\n");
return 1;
}
/* Test: Leaf @ Leaf -> Stem */
uint32_t leaf = arb_leaf(ctx);
uint32_t app = arb_app(ctx, leaf, leaf);
uint32_t result = arb_reduce(ctx, app, 10000);
uint32_t stem = arb_stem(ctx, leaf);
/* Build expected Stem(Leaf) and compare */
(void)result; (void)stem;
printf("PASS: reduce Leaf@Leaf\n");
/* Test: number codec roundtrip */
uint32_t num_tree = arb_of_number(ctx, 42);
uint64_t decoded_num;
if (!arb_to_number(ctx, num_tree, &decoded_num) || decoded_num != 42) {
fprintf(stderr, "FAIL: number roundtrip\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: number roundtrip 42\n");
/* Test: string codec roundtrip */
uint32_t str_tree = arb_of_string(ctx, "hello");
uint8_t* decoded_str;
size_t decoded_len;
if (!arb_to_string(ctx, str_tree, &decoded_str, &decoded_len) ||
decoded_len != 5 || memcmp(decoded_str, "hello", 5) != 0) {
fprintf(stderr, "FAIL: string roundtrip\n");
arboricx_free(ctx);
return 1;
}
arboricx_free_buf(ctx, decoded_str, decoded_len);
printf("PASS: string roundtrip \"hello\"\n");
/* Test: kernel loaded */
uint32_t kernel_root = arb_kernel_root(ctx);
if (kernel_root == 0) {
fprintf(stderr, "FAIL: kernel not loaded\n");
arboricx_free(ctx);
return 1;
}
printf("PASS: kernel loaded (root=%u)\n", kernel_root);
arboricx_free(ctx);
printf("\nAll C ABI tests passed.\n");
return 0;
}

View File

@@ -0,0 +1,84 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../include/arboricx.h"
static uint8_t *read_file(const char *path, size_t *out_len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
*out_len = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *buf = malloc(*out_len);
fread(buf, 1, *out_len, f);
fclose(f);
return buf;
}
int main() {
arb_ctx_t *ctx = arboricx_init();
if (!ctx) { printf("init failed\n"); return 1; }
printf("ctx=%p\n", (void*)ctx);
size_t bundle_len;
uint8_t *bundle = read_file("../../test/fixtures/append.arboricx", &bundle_len);
if (!bundle) { printf("bundle not found\n"); return 1; }
printf("bundle size=%zu\n", bundle_len);
clock_t t0 = clock();
uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "root");
clock_t t1 = clock();
printf("load_bundle took %.3f ms, term=%u\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC, term);
if (term == 0) {
printf("load_bundle failed\n");
return 1;
}
uint32_t arg1 = arb_of_string(ctx, "Hello, ");
uint32_t arg2 = arb_of_string(ctx, "world!");
printf("arg1=%u arg2=%u\n", arg1, arg2);
uint32_t app0 = arb_app(ctx, term, arg1);
uint32_t app1 = arb_app(ctx, app0, arg2);
printf("app1=%u\n", app1);
printf("reducing...\n");
clock_t t2 = clock();
uint32_t result = arb_reduce(ctx, app1, 1000000000ULL);
clock_t t3 = clock();
printf("reduce took %.3f ms, result=%u\n", (double)(t3 - t2) * 1000.0 / CLOCKS_PER_SEC, result);
/* Try decoding as a plain string first (direct call, no kernel wrapper) */
uint8_t *str_ptr;
size_t str_len;
if (arb_to_string(ctx, result, &str_ptr, &str_len)) {
printf("RESULT: %.*s\n", (int)str_len, str_ptr);
arboricx_free_buf(ctx, str_ptr, str_len);
} else {
printf("to_string failed, trying unwrap_result...\n");
int ok;
uint32_t value, rest;
if (!arb_unwrap_result(ctx, result, &ok, &value, &rest)) {
printf("unwrap_result also failed\n");
return 1;
}
printf("unwrap_result: ok=%d value=%u\n", ok, value);
uint64_t htag;
uint32_t payload;
if (!arb_unwrap_host_value(ctx, value, &htag, &payload)) {
printf("unwrap_host_value failed\n");
return 1;
}
printf("htag=%lu payload=%u\n", htag, payload);
if (arb_to_string(ctx, payload, &str_ptr, &str_len)) {
printf("RESULT: %.*s\n", (int)str_len, str_ptr);
arboricx_free_buf(ctx, str_ptr, str_len);
}
}
free(bundle);
arboricx_free(ctx);
printf("done\n");
return 0;
}

View File

@@ -0,0 +1,60 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../include/arboricx.h"
static uint8_t *read_file(const char *path, size_t *out_len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
*out_len = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *buf = malloc(*out_len);
fread(buf, 1, *out_len, f);
fclose(f);
return buf;
}
int test_bundle(arb_ctx_t *ctx, const char *path, int expect_val) {
size_t bundle_len;
uint8_t *bundle = read_file(path, &bundle_len);
if (!bundle) { printf("bundle not found: %s\n", path); return 1; }
uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "root");
if (term == 0) {
printf("load_bundle failed for %s\n", path);
free(bundle);
return 1;
}
uint32_t result = arb_reduce(ctx, term, 1000000000ULL);
int b;
if (!arb_to_bool(ctx, result, &b)) {
printf("to_bool failed for %s\n", path);
free(bundle);
return 1;
}
printf("%s result bool=%d (expected %d)\n", path, b, expect_val);
if (b != expect_val) {
printf("MISMATCH!\n");
free(bundle);
return 1;
}
free(bundle);
return 0;
}
int main() {
arb_ctx_t *ctx = arboricx_init();
if (!ctx) { printf("init failed\n"); return 1; }
if (test_bundle(ctx, "../../test/fixtures/true.arboricx", 1) != 0) return 1;
if (test_bundle(ctx, "../../test/fixtures/false.arboricx", 0) != 0) return 1;
arboricx_free(ctx);
printf("All bool tests passed.\n");
return 0;
}

View File

@@ -0,0 +1,60 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../include/arboricx.h"
static uint8_t *read_file(const char *path, size_t *out_len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
*out_len = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *buf = malloc(*out_len);
fread(buf, 1, *out_len, f);
fclose(f);
return buf;
}
int main() {
arb_ctx_t *ctx = arboricx_init();
if (!ctx) { printf("init failed\n"); return 1; }
size_t bundle_len;
uint8_t *bundle = read_file("../../test/fixtures/id.arboricx", &bundle_len);
if (!bundle) { printf("bundle not found\n"); return 1; }
printf("bundle size=%zu\n", bundle_len);
clock_t t0 = clock();
uint32_t term = arb_load_bundle(ctx, bundle, bundle_len, "root");
clock_t t1 = clock();
printf("load_bundle took %.3f ms, term=%u\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC, term);
if (term == 0) {
printf("load_bundle failed\n");
return 1;
}
uint32_t arg1 = arb_of_string(ctx, "hello");
uint32_t app0 = arb_app(ctx, term, arg1);
printf("reducing...\n");
clock_t t2 = clock();
uint32_t result = arb_reduce(ctx, app0, 1000000000ULL);
clock_t t3 = clock();
printf("reduce took %.3f ms, result=%u\n", (double)(t3 - t2) * 1000.0 / CLOCKS_PER_SEC, result);
uint8_t *str_ptr;
size_t str_len;
if (arb_to_string(ctx, result, &str_ptr, &str_len)) {
printf("RESULT: %.*s\n", (int)str_len, str_ptr);
arboricx_free_buf(ctx, str_ptr, str_len);
} else {
printf("to_string failed\n");
return 1;
}
free(bundle);
arboricx_free(ctx);
printf("done\n");
return 0;
}

View File

@@ -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)