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

36
ext/zig/src/arena.zig Normal file
View File

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

479
ext/zig/src/bundle.zig Normal file
View File

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

183
ext/zig/src/c_abi.zig Normal file
View File

@@ -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;
}

205
ext/zig/src/codecs.zig Normal file
View File

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

22
ext/zig/src/kernel.zig Normal file
View File

@@ -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];
}

235
ext/zig/src/main.zig Normal file
View File

@@ -0,0 +1,235 @@
const std = @import("std");
const tree = @import("tree.zig");
const Arena = @import("arena.zig").Arena;
const reduce = @import("reduce.zig");
const codecs = @import("codecs.zig");
const kernel = @import("kernel.zig");
const bundle = @import("bundle.zig");
fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, io: std.Io) !void {
const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
var current = term;
for (args_raw) |arg| {
const arg_tree = try parseArg(arena, arg);
current = try arena.alloc(.{ .app = .{ .func = current, .arg = arg_tree } });
}
const result = try reduce.reduce(current, arena, 1_000_000_000);
var stdout_buf: [4096]u8 = undefined;
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
switch (tag) {
codecs.HOST_STRING_TAG => {
const s = try codecs.toString(arena, result) orelse {
try stdout.interface.writeAll("Error: failed to decode string result\n");
try stdout.flush();
return error.DecodeFailed;
};
defer arena.allocator.free(s);
try stdout.interface.writeAll(s);
try stdout.interface.writeAll("\n");
},
codecs.HOST_NUMBER_TAG => {
const n = try codecs.toNumber(arena, result) orelse 0;
try stdout.interface.print("{d}\n", .{n});
},
codecs.HOST_BOOL_TAG => {
const b = try codecs.toBool(arena, result) orelse {
try stdout.interface.writeAll("Error: failed to decode bool result\n");
try stdout.flush();
return error.DecodeFailed;
};
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
},
codecs.HOST_TREE_TAG => {
try tree.formatTree(&stdout.interface, arena, result, 0);
try stdout.interface.writeAll("\n");
},
else => {
try stdout.interface.print("(tag={d}, payload=", .{tag});
try tree.formatTree(&stdout.interface, arena, result, 0);
try stdout.interface.writeAll(")\n");
},
}
try stdout.flush();
}
fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, io: std.Io) !void {
const kernel_root = try kernel.loadKernel(arena);
const tag_tree = try codecs.ofNumber(arena, tag);
const bundle_tree = try codecs.ofBytes(arena, bundle_bytes);
var arg_items = try arena.allocator.alloc(u32, args_raw.len);
defer arena.allocator.free(arg_items);
for (args_raw, 0..) |arg, i| {
arg_items[i] = try parseArg(arena, arg);
}
const args_tree = try codecs.ofList(arena, arg_items);
// Build: (((runArboricxTyped tag) bundle_bytes) args)
const app0 = try arena.alloc(.{ .app = .{ .func = kernel_root, .arg = tag_tree } });
const app1 = try arena.alloc(.{ .app = .{ .func = app0, .arg = bundle_tree } });
const app2 = try arena.alloc(.{ .app = .{ .func = app1, .arg = args_tree } });
const result = try reduce.reduce(app2, arena, 1_000_000_000);
const unwrapped = try codecs.unwrapResult(arena, result) orelse {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.writeAll("Error: result is not a valid ok/err pair\n");
try stderr.flush();
return error.InvalidResult;
};
if (!unwrapped.ok) {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
const code = try codecs.toNumber(arena, unwrapped.value) orelse 0;
try stderr.interface.print("Error: kernel returned err, code={d}\n", .{code});
try stderr.flush();
return error.KernelError;
}
const hv = try codecs.unwrapHostValue(arena, unwrapped.value) orelse {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.writeAll("Error: result is not a valid host ABI value\n");
try stderr.flush();
return error.InvalidHostValue;
};
var stdout_buf: [4096]u8 = undefined;
var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
switch (hv.tag) {
codecs.HOST_STRING_TAG => {
const s = try codecs.toString(arena, hv.payload) orelse {
try stdout.interface.writeAll("Error: failed to decode string payload\n");
try stdout.flush();
return error.DecodeFailed;
};
defer arena.allocator.free(s);
try stdout.interface.writeAll(s);
try stdout.interface.writeAll("\n");
},
codecs.HOST_NUMBER_TAG => {
const n = try codecs.toNumber(arena, hv.payload) orelse 0;
try stdout.interface.print("{d}\n", .{n});
},
codecs.HOST_BOOL_TAG => {
const b = try codecs.toBool(arena, hv.payload) orelse {
try stdout.interface.writeAll("Error: failed to decode bool payload\n");
try stdout.flush();
return error.DecodeFailed;
};
try stdout.interface.writeAll(if (b) "true\n" else "false\n");
},
codecs.HOST_TREE_TAG => {
try tree.formatTree(&stdout.interface, arena, hv.payload, 0);
try stdout.interface.writeAll("\n");
},
else => {
try stdout.interface.print("(tag={d}, payload=", .{hv.tag});
try tree.formatTree(&stdout.interface, arena, hv.payload, 0);
try stdout.interface.writeAll(")\n");
},
}
try stdout.flush();
}
fn parseArg(arena: *Arena, s: []const u8) !u32 {
if (std.fmt.parseInt(u64, s, 10)) |n| {
return try codecs.ofNumber(arena, n);
} else |_| {}
if (s.len >= 2 and s[0] == '"' and s[s.len - 1] == '"') {
return try codecs.ofString(arena, s[1 .. s.len - 1]);
}
return try codecs.ofString(arena, s);
}
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const io = init.io;
const args = try init.minimal.args.toSlice(init.arena.allocator());
if (args.len < 2) {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] <bundle.arboricx> [arg1 arg2 ...]\n");
try stderr.flush();
std.process.exit(1);
}
// Parse options before bundle path
var tag = codecs.HOST_STRING_TAG;
var bundle_idx: usize = 1;
var arg_start: usize = 2;
var use_kernel = false;
var i: usize = 1;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--type")) {
if (i + 1 >= args.len) {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.writeAll("Usage: tricu-zig --type <tree|number|bool|string|list|bytes> <bundle> [args...]\n");
try stderr.flush();
std.process.exit(1);
}
const type_str = args[i + 1];
tag = if (std.mem.eql(u8, type_str, "tree")) codecs.HOST_TREE_TAG
else if (std.mem.eql(u8, type_str, "number")) codecs.HOST_NUMBER_TAG
else if (std.mem.eql(u8, type_str, "bool")) codecs.HOST_BOOL_TAG
else if (std.mem.eql(u8, type_str, "string")) codecs.HOST_STRING_TAG
else if (std.mem.eql(u8, type_str, "list")) codecs.HOST_LIST_TAG
else if (std.mem.eql(u8, type_str, "bytes")) codecs.HOST_BYTES_TAG
else blk: {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.print("Unknown type: {s}\n", .{type_str});
try stderr.flush();
std.process.exit(1);
break :blk codecs.HOST_STRING_TAG;
};
i += 1;
} else if (std.mem.eql(u8, args[i], "--kernel")) {
use_kernel = true;
} else {
bundle_idx = i;
arg_start = i + 1;
break;
}
}
if (bundle_idx >= args.len) {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] <bundle.arboricx> [arg1 arg2 ...]\n");
try stderr.flush();
std.process.exit(1);
}
const bundle_path = args[bundle_idx];
const bundle_bytes = try std.Io.Dir.cwd().readFileAlloc(io, bundle_path, gpa, .limited(10 * 1024 * 1024));
defer gpa.free(bundle_bytes);
var arena = Arena.init(gpa);
defer arena.deinit();
const call_args = if (arg_start < args.len) args[arg_start..] else &[_][]const u8{};
if (use_kernel) {
runBundle(&arena, tag, bundle_bytes, call_args, io) catch |err| {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
try stderr.flush();
std.process.exit(1);
};
} else {
runNative(&arena, tag, bundle_bytes, call_args, io) catch |err| {
var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
try stderr.flush();
std.process.exit(1);
};
}
}

128
ext/zig/src/reduce.zig Normal file
View File

@@ -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,
}
},
}
}
}

27
ext/zig/src/ternary.zig Normal file
View File

@@ -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,
};
}

191
ext/zig/src/tree.zig Normal file
View File

@@ -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(")");
},
}
}