feat(zig): native Arboricx bundle parser and C ABI
This commit is contained in:
36
ext/zig/src/arena.zig
Normal file
36
ext/zig/src/arena.zig
Normal 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
479
ext/zig/src/bundle.zig
Normal 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
183
ext/zig/src/c_abi.zig
Normal 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
205
ext/zig/src/codecs.zig
Normal 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
22
ext/zig/src/kernel.zig
Normal 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
235
ext/zig/src/main.zig
Normal 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
128
ext/zig/src/reduce.zig
Normal 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
27
ext/zig/src/ternary.zig
Normal 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
191
ext/zig/src/tree.zig
Normal 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(")");
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user