Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/reproduce.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,8 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: reproduce-headless
path: reproduce-headless.json
# The record and, beside it, the whole output of each command.
path: |
reproduce-headless.json
reproduce-headless.*.log
if-no-files-found: error
31 changes: 28 additions & 3 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ pub fn build(b: *std.Build) void {

// Install C header
const install_header = b.addInstallHeaderFile(
b.path("libs/c/libtrinityvsa/include/trinity_vsa.h"),
b.path("src/libs/c/libtrinityvsa/include/trinity_vsa.h"),
"trinity_vsa.h",
);

Expand Down Expand Up @@ -160,7 +160,7 @@ pub fn build(b: *std.Build) void {
const install_queen_static = b.addInstallArtifact(libqueen_static, .{});

const install_queen_header = b.addInstallHeaderFile(
b.path("libs/c/libtrinityvsa/include/trinity_queen.h"),
b.path("src/libs/c/libtrinityvsa/include/trinity_queen.h"),
"trinity_queen.h",
);

Expand Down Expand Up @@ -219,7 +219,12 @@ pub fn build(b: *std.Build) void {
// ahead of it had been failing, so CI Runner reported the build and
// never reached the tests to report this.
.link_libc = true,
// src/trinity.zig asks for `hdc_vsa`; src/hybrid.zig and src/vsa.zig,
// which it re-exports, ask for `zig-hdc-vsa`. Both names, one module,
// as `trinity_mod` above already does -- with only one of them this
// root did not compile (specs/reproduce/headless.t27 recorded it).
.imports = &.{
.{ .name = "zig-hdc-vsa", .module = hdc_vsa_mod },
.{ .name = "hdc_vsa", .module = hdc_vsa_mod },
.{ .name = "golden_float", .module = gf_mod },
},
Expand Down Expand Up @@ -260,6 +265,9 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/queen_api.zig"),
.target = target,
.optimize = optimize,
// queen_api reaches std.c and the C allocator; without libc the test
// root never compiled (specs/reproduce/headless.t27 recorded it blocked).
.link_libc = true,
.imports = &.{.{ .name = "zig-hdc-vsa", .module = hdc_vsa_mod }},
}),
});
Expand Down Expand Up @@ -293,11 +301,25 @@ pub fn build(b: *std.Build) void {

// E2E + Benchmarks + Verdict tests (Phase 4)
const e2e_tests = b.addTest(.{
// This root asserts wall-clock thresholds (1 ms per 1024-trit operation, and a
// VERDICT that scores them). Zig 0.15 builds Debug for x86_64 with its own backend,
// which compiles the VSA kernels of zig-golden-float -- 71 KB HybridBigInt values
// made and returned by value -- into code 185 to 1680 times slower than LLVM's
// Debug output on aarch64 (cosine 19.4 ms against 11.6 us per op on CI), so the
// thresholds measured the backend. Built with LLVM on every target, they measure
// the same code everywhere; the other roots keep the default backend.
.use_llvm = true,
.root_module = b.createModule(.{
.root_source_file = b.path("src/e2e_test.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "zig-hdc-vsa", .module = hdc_vsa_mod }},
// e2e_test.zig reaches src/trinity.zig, which imports `hdc_vsa` and
// `golden_float`; the module offered neither.
.imports = &.{
.{ .name = "zig-hdc-vsa", .module = hdc_vsa_mod },
.{ .name = "hdc_vsa", .module = hdc_vsa_mod },
.{ .name = "golden_float", .module = gf_mod },
},
}),
});
const run_e2e_tests = b.addRunArtifact(e2e_tests);
Expand All @@ -311,6 +333,9 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/c_api.zig"),
.target = target,
.optimize = optimize,
// The C API allocates with std.heap.c_allocator; both libraries it
// builds call linkLibC(), and its tests need libc for the same reason.
.link_libc = true,
.imports = &.{.{ .name = "zig-hdc-vsa", .module = hdc_vsa_mod }},
}),
});
Expand Down
65 changes: 36 additions & 29 deletions specs/reproduce/headless.t27
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@
; a clean checkout builds and tests the declared profile, and a failure anywhere fails the gate. This module is
; that profile, measured; tools/reproduce.py runs it and judges every run against it (the command is
; `python3 tools/reproduce.py headless`, the CI job is .github/workflows/reproduce.yml).
; MEASURED at gHashTag/trinity main afc9d38435ad01c48af6c33c311da422c38f046e (2026-09-30) with Zig 0.15.2, twice:
; Linux aarch64 in a container with empty caches, and the Build & Test job of CI run 36875247882 (Linux x86_64).
; Both print the same test summary, "124/135 steps succeeded; 5 failed; 2510/2510 tests passed", and until
; this module CI reported that summary as a success: its test step was `zig build test -Dci=true 2>&1 | tee`
; under bash without pipefail, so the step's exit code was tee's (gHashTag/trinity#616, the tee fix alone is
; gHashTag/trinity#987). Five test roots do not compile, the library's own src/trinity.zig among them, and
; their tests have not run since; they are BLOCKED below, each with the error and the capability card it
; holds back, and the gate fails the day one of them compiles until it is taken off the list.
; Not a claim about any profile but headless; not a claim that the 2510 tests that ran test enough.
; MEASURED twice with Zig 0.15.2 and empty caches: Linux aarch64 in a container, and the jobs of the change that
; brought this version. At afc9d38435ad01c48af6c33c311da422c38f046e (2026-09-30) the test step printed
; "124/135 steps succeeded; 5 failed; 2510/2510 tests passed", and CI reported that summary as a success: its
; test step was `zig build test -Dci=true 2>&1 | tee` under bash without pipefail, so the step's exit code was
; tee's (gHashTag/trinity#616; the tee fix alone is gHashTag/trinity#987). gHashTag/trinity#1207 put this
; module and its judge in front of CI with the five roots that did not compile named as BLOCKED. The next
; change fixed all five on main 3c33adbdb7b0b70a8485fc2efc0429ec1c239c5e -- two module names and libc in
; build.zig, a duplicated `if` in src/c_api.zig, an import of a deleted file in src/e2e_test.zig, and the
; HybridBigInt API of zig-golden-float (the unpacked cache is an array, not an optional; dotProduct, bundle2
; and bundle3 take no allocator) in src/science.zig, src/vm.zig, src/vsa_jit.zig and src/c_api.zig -- and the
; graph now runs whole: "135/135 steps succeeded; 2760/2768 tests passed; 8 skipped" on aarch64, where the
; eight tests of src/jit.zig that execute x86-64 machine code skip themselves; on x86_64 they run.
; Not a claim about any profile but headless; not a claim that the tests that ran test enough.
; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY

module reproduce_headless;

pub const KIND : str = "reproduce-profile";
pub const ID : str = "reproduce/headless";
pub const PROFILE : str = "headless";
pub const MEASURED_AT : str = "afc9d38435ad01c48af6c33c311da422c38f046e";
pub const MEASURED_AT : str = "3c33adbdb7b0b70a8485fc2efc0429ec1c239c5e";
pub const ZIG : str = "0.15.2";
; The documented clean-clone commands. tools/reproduce.py replaces the leading zig with the binary it is given
; and, unless asked to reuse caches, points ZIG_GLOBAL_CACHE_DIR and ZIG_LOCAL_CACHE_DIR at a new empty
Expand Down Expand Up @@ -52,13 +56,18 @@ pub const INSTALLS : [46]str = [
];
; The test graph: the floors it may not drop below. A graph that shrank is a partial graph, and a run step
; that ran no test is not a passing one: one does today, the step of external/zig-hslm/src/root.zig, the only
; compiled test root without a test declaration.
; compiled test root without a test declaration. A skipped test is not a passing one either: the eight that
; skip are the four x86-64 JIT tests of src/jit.zig, compiled into src/trinity.zig and src/e2e_test.zig, and
; only off x86_64. The floor of passed tests is the aarch64 count, the lower of the two. Not every pass tested
; something: 23 of the 24 tests of src/jit_arm64.zig, and 4 of the 5 of src/jit_x86_64.zig, return early off
; their architecture and are counted as passed there.
pub const TEST_STEPS_MIN : u32 = 135;
pub const TESTS_PASSED_MIN : u32 = 2510;
pub const RUN_STEPS_WITH_TESTS_MIN : u32 = 61;
pub const TESTS_PASSED_MIN : u32 = 2760;
pub const TESTS_SKIPPED_MAX : u32 = 8;
pub const RUN_STEPS_WITH_TESTS_MIN : u32 = 66;
pub const EMPTY_RUN_STEPS_MAX : u32 = 1;
; Every root the test step compiles, read from `--verbose` with empty caches. The five BLOCKED ones are here too:
; the step still tries them, and a root that leaves this list is a test that left the graph.
; Every root the test step compiles, read from `--verbose` with empty caches; a root that leaves this list is
; a test that left the graph.
pub const TEST_ROOTS : [67]str = [
"external/zig-hslm/src/root.zig", "src/agent_mu/production_hardening_test.zig",
"src/agent_mu/production_test.zig", "src/agent_mu/swarm_collaboration.zig",
Expand Down Expand Up @@ -95,17 +104,13 @@ pub const TEST_ROOTS : [67]str = [
"src/vibeec/sym_005_demo.zig", "src/vibeec/triples_parser.zig",
"src/vm.zig"
];
; The roots that do not compile at MEASURED_AT, what the compiler says, and the capability card of the S01
; manifest each one holds back. Exactly these may fail; one more fails the gate, and one fewer fails it too.
pub const BLOCKED_ROOTS : [5]str = ["src/c_api.zig", "src/e2e_test.zig", "src/queen_api.zig", "src/trinity.zig", "src/vm.zig"];
pub const BLOCKED_REASONS : [5]str = [
"src/c_api.zig:270:1: expected statement, found 'a document comment' -- the file does not parse; S05 of the epic recorded the same at the S01 pin",
"src/packed_trit.zig:1:1: unable to load 'packed_trit.zig': FileNotFound -- src/e2e_test.zig:21 imports a file removed in 42490a222 (#517)",
"lib/std/c.zig and lib/std/heap.zig:128: the C allocator without libc (4 errors) -- the queen_api test module does not set link_libc, as the main test module does",
"src/hybrid.zig:4 and src/vsa.zig:7: no module named 'zig-hdc-vsa' (the test module provides it as hdc_vsa); src/science.zig:53, 143, 161, 179, 204: expected optional type, found '[59049]i8' (7 errors)",
"src/vm.zig:1150:15: expected optional type, found '[59049]i8'; src/vsa_jit.zig:117:26: member function expected 1 argument(s), found 2"
];
pub const BLOCKED_CAPABILITIES : [5]str = ["trinity/abi.c-api", "trinity/examples.zig", "trinity/queen.lib", "trinity/lib.trinity", "trinity/lib.trinity"];
; The roots that may fail to compile, each with what the compiler says and the capability card of the S01
; manifest it holds back. None today. A root that stops compiling and cannot be fixed in the change that
; breaks it is named here, and only then may it fail; a named root that compiles again fails the gate until
; its line is removed, so the list cannot outlive the defect. At afc9d384 it held five (see the header).
pub const BLOCKED_ROOTS : [0]str = [];
pub const BLOCKED_REASONS : [0]str = [];
pub const BLOCKED_CAPABILITIES : [0]str = [];
pub const EVIDENCE : str = "tools/reproduce.py writes zig-out/reproduce/<command>.json: toolchain, head, cache freshness, each command with its exit code and summary, the compile failures by root, the run steps, the compiled roots, every install with its sha256, and the violations";
pub const ENABLED : bool = true;

Expand All @@ -116,11 +121,13 @@ test the_profile_is_headless_and_pinned {
}

test the_floors_hold_together {
assert TESTS_PASSED_MIN == 2510;
assert RUN_STEPS_WITH_TESTS_MIN + EMPTY_RUN_STEPS_MAX + 5 == 67;
assert TESTS_PASSED_MIN == 2760;
assert TESTS_SKIPPED_MAX == 8;
assert RUN_STEPS_WITH_TESTS_MIN + EMPTY_RUN_STEPS_MAX == 67;
assert TEST_STEPS_MIN > RUN_STEPS_WITH_TESTS_MIN;
}

invariant blocked_roots_are_few {
invariant every_root_runs {
assert EMPTY_RUN_STEPS_MAX <= 1;
assert TESTS_SKIPPED_MAX <= 8;
}
35 changes: 10 additions & 25 deletions src/c_api.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
const std = @import("std");
const vsa = @import("vsa.zig");
const hybrid = @import("hybrid.zig");
const encoding = @import("vsa/gen_encoding.zig");
// src/vsa/gen_encoding.zig moved to gHashTag/zig-hdc with the rest of src/vsa/
// (42490a222, #517); the facade in vsa.zig re-exports it as `encoding`.
const encoding = vsa.encoding;

const HybridBigInt = hybrid.HybridBigInt;
const Trit = hybrid.Trit;
Expand Down Expand Up @@ -80,13 +82,7 @@ export fn trinity_vsa_from_array(data: [*]const i8, dim: usize) ?*anyopaque {

for (0..actual_dim) |i| {
const val = data[i];
if (val > 0) {
if (ptr.unpacked_cache) |cache| cache[i] = 1;
} else if (val < 0) {
if (ptr.unpacked_cache) |cache| cache[i] = -1;
} else {
if (ptr.unpacked_cache) |cache| cache[i] = 0;
}
ptr.unpacked_cache[i] = if (val > 0) 1 else if (val < 0) -1 else 0;
}

return toOpaque(ptr);
Expand Down Expand Up @@ -135,7 +131,7 @@ export fn trinity_vsa_bundle2(a: ?*anyopaque, b: ?*anyopaque) ?*anyopaque {
const ha = toHybrid(a orelse return null);
const hb = toHybrid(b orelse return null);
const ptr = heapAlloc() orelse return null;
ptr.* = vsa.bundle2(ha, hb, std.heap.c_allocator);
ptr.* = vsa.bundle2(ha, hb);
return toOpaque(ptr);
}

Expand All @@ -145,7 +141,7 @@ export fn trinity_vsa_bundle3(a: ?*anyopaque, b: ?*anyopaque, c: ?*anyopaque) ?*
const hb = toHybrid(b orelse return null);
const hc = toHybrid(c orelse return null);
const ptr = heapAlloc() orelse return null;
ptr.* = vsa.bundle3(ha, hb, hc, std.heap.c_allocator);
ptr.* = vsa.bundle3(ha, hb, hc);
return toOpaque(ptr);
}

Expand Down Expand Up @@ -179,7 +175,7 @@ export fn trinity_vsa_hamming_distance(a: ?*anyopaque, b: ?*anyopaque) usize {
export fn trinity_vsa_dot_product(a: ?*anyopaque, b: ?*anyopaque) i64 {
const ha = toHybrid(a orelse return 0);
const hb = toHybrid(b orelse return 0);
return ha.dotProduct(hb, std.heap.c_allocator);
return ha.dotProduct(hb);
}

// ═══════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -240,26 +236,15 @@ export fn trinity_vsa_get_trit(v: ?*anyopaque, index: usize) i8 {
const hv = toHybrid(v orelse return 0);
hv.ensureUnpacked();
if (index >= hv.trit_len) return 0;
if (hv.unpacked_cache) |cache| {
return cache[index];
} else {
return 0;
}
return hv.unpacked_cache[index];
}

/// Set trit value at index (value clamped to -1, 0, +1)
export fn trinity_vsa_set_trit(v: ?*anyopaque, index: usize, value: i8) void {
const hv = toHybrid(v orelse return);
hv.ensureUnpacked();
if (index >= hv.trit_len) return;
if (value > 0) {
if (value > 0) {
if (hv.unpacked_cache) |cache| cache[index] = 1;
} else if (value < 0) {
if (hv.unpacked_cache) |cache| cache[index] = -1;
} else {
if (hv.unpacked_cache) |cache| cache[index] = 0;
}
hv.unpacked_cache[index] = if (value > 0) 1 else if (value < 0) -1 else 0;
hv.dirty = true;
}

Expand All @@ -274,7 +259,7 @@ export fn trinity_vsa_to_array(v: ?*anyopaque, out: [*]i8, max_len: usize) usize
hv.ensureUnpacked();
const copy_len = @min(hv.trit_len, max_len);
for (0..copy_len) |i| {
if (hv.unpacked_cache) |cache| out[i] = cache[i];
out[i] = hv.unpacked_cache[i];
}
return copy_len;
}
Expand Down
1 change: 0 additions & 1 deletion src/e2e_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const vsa = @import("vsa.zig");
const vm = @import("vm.zig");
const sdk = @import("sdk.zig");
const hybrid = @import("hybrid.zig");
const packed_trit = @import("packed_trit.zig");

// ═══════════════════════════════════════════════════════════════════════════════
// E2E TEST 1: VSA → VM → SDK Full Pipeline
Expand Down
15 changes: 11 additions & 4 deletions src/jit.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ pub const JitVsaFn = *const fn (*HybridBigInt, *HybridBigInt) void;
/// Takes two vector pointers and returns f64 similarity
pub const JitSimilarityFn = *const fn (*HybridBigInt, *HybridBigInt) f64;

/// What `finalize` returns: the generated code reads its two pointers from rdi and rsi, so it must
/// be called with the C calling convention. Without `callconv(.c)` the pointer used Zig's own
/// convention, which nothing promises to pass them there -- and the x86-64 backend that builds Debug
/// does not: the bind and bundle tests read the wrong memory on x86_64 (dot product, which casts
/// to `callconv(.c)` itself, passed). src/jit_arm64.zig already declares it.
pub const JitBinaryFn = *const fn (*anyopaque, *anyopaque) callconv(.c) void;

/// JIT Compiler for VSA operations
pub const JitCompiler = struct {
/// Code buffer for generated machine code
Expand Down Expand Up @@ -257,7 +264,7 @@ pub const JitCompiler = struct {
// ═══════════════════════════════════════════════════════════════════════════

/// Make code executable and return function pointer
pub fn finalize(self: *Self) !*const fn (*anyopaque, *anyopaque) void {
pub fn finalize(self: *Self) !JitBinaryFn {
const code_size = self.code.items.len;
if (code_size == 0) return error.EmptyCode;

Expand Down Expand Up @@ -471,7 +478,7 @@ pub const JitCompiler = struct {
/// Cache for JIT-compiled functions
pub const JitCache = struct {
/// Cached bind functions by dimension
bind_cache: std.AutoHashMap(usize, *const fn (*anyopaque, *anyopaque) void),
bind_cache: std.AutoHashMap(usize, JitBinaryFn),
/// Compiler instance
compiler: JitCompiler,
/// Allocator
Expand All @@ -481,7 +488,7 @@ pub const JitCache = struct {

pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.bind_cache = std.AutoHashMap(usize, *const fn (*anyopaque, *anyopaque) void).init(allocator),
.bind_cache = std.AutoHashMap(usize, JitBinaryFn).init(allocator),
.compiler = JitCompiler.init(allocator),
.allocator = allocator,
};
Expand All @@ -493,7 +500,7 @@ pub const JitCache = struct {
}

/// Get or compile bind function for dimension
pub fn getBind(self: *Self, dimension: usize) !*const fn (*anyopaque, *anyopaque) void {
pub fn getBind(self: *Self, dimension: usize) !JitBinaryFn {
if (self.bind_cache.get(dimension)) |func| {
return func;
}
Expand Down
Loading
Loading