diff --git a/.github/workflows/reproduce.yml b/.github/workflows/reproduce.yml index 62732a8537..3c8fd1778e 100644 --- a/.github/workflows/reproduce.yml +++ b/.github/workflows/reproduce.yml @@ -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 diff --git a/build.zig b/build.zig index 06977c4102..65599f7651 100644 --- a/build.zig +++ b/build.zig @@ -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", ); @@ -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", ); @@ -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 }, }, @@ -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 }}, }), }); @@ -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); @@ -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 }}, }), }); diff --git a/specs/reproduce/headless.t27 b/specs/reproduce/headless.t27 index 5b2ccc7cee..f6ae81f147 100644 --- a/specs/reproduce/headless.t27 +++ b/specs/reproduce/headless.t27 @@ -4,15 +4,19 @@ ; 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; @@ -20,7 +24,7 @@ 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 @@ -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", @@ -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/.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; @@ -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; } diff --git a/src/c_api.zig b/src/c_api.zig index 970ddbfed8..ed5caa7c49 100644 --- a/src/c_api.zig +++ b/src/c_api.zig @@ -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; @@ -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); @@ -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); } @@ -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); } @@ -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); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -240,11 +236,7 @@ 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) @@ -252,14 +244,7 @@ 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; } @@ -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; } diff --git a/src/e2e_test.zig b/src/e2e_test.zig index a95aa821dd..2bd7b4924f 100644 --- a/src/e2e_test.zig +++ b/src/e2e_test.zig @@ -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 diff --git a/src/jit.zig b/src/jit.zig index 04fc8c17c1..bbf64ccb7b 100644 --- a/src/jit.zig +++ b/src/jit.zig @@ -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 @@ -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; @@ -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 @@ -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, }; @@ -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; } diff --git a/src/science.zig b/src/science.zig index 704f91da19..e25f1941eb 100644 --- a/src/science.zig +++ b/src/science.zig @@ -50,18 +50,16 @@ pub fn computeStats(hv: *Hypervector) VectorStats { var sum_sq: i64 = 0; for (0..hv.data.trit_len) |i| { - if (hv.data.unpacked_cache) |cache| { - const t = cache[i]; - if (t > 0) { - positive += 1; - } else if (t < 0) { - negative += 1; - } else { - zero += 1; - } - sum += t; - sum_sq += @as(i64, t) * @as(i64, t); + const t = hv.data.unpacked_cache[i]; + if (t > 0) { + positive += 1; + } else if (t < 0) { + negative += 1; + } else { + zero += 1; } + sum += t; + sum_sq += @as(i64, t) * @as(i64, t); } const n = hv.data.trit_len; @@ -140,8 +138,8 @@ pub fn euclideanDistance(a: *Hypervector, b: *Hypervector) f64 { const len = @max(a.data.trit_len, b.data.trit_len); for (0..len) |i| { - const a_t: i64 = if (i < a.data.trit_len) a.data.unpacked_cache.?[i] else 0; - const b_t: i64 = if (i < b.data.trit_len) b.data.unpacked_cache.?[i] else 0; + const a_t: i64 = if (i < a.data.trit_len) a.data.unpacked_cache[i] else 0; + const b_t: i64 = if (i < b.data.trit_len) b.data.unpacked_cache[i] else 0; const diff = a_t - b_t; sum_sq += diff * diff; } @@ -158,8 +156,8 @@ pub fn manhattanDistance(a: *Hypervector, b: *Hypervector) f64 { const len = @max(a.data.trit_len, b.data.trit_len); for (0..len) |i| { - const a_t: i64 = if (i < a.data.trit_len) a.data.unpacked_cache.?[i] else 0; - const b_t: i64 = if (i < b.data.trit_len) b.data.unpacked_cache.?[i] else 0; + const a_t: i64 = if (i < a.data.trit_len) a.data.unpacked_cache[i] else 0; + const b_t: i64 = if (i < b.data.trit_len) b.data.unpacked_cache[i] else 0; sum += @abs(a_t - b_t); } @@ -176,8 +174,8 @@ pub fn jaccardDistance(a: *Hypervector, b: *Hypervector) f64 { const len = @max(a.data.trit_len, b.data.trit_len); for (0..len) |i| { - const a_t = if (i < a.data.trit_len) a.data.unpacked_cache.?[i] else 0; - const b_t = if (i < b.data.trit_len) b.data.unpacked_cache.?[i] else 0; + const a_t = if (i < a.data.trit_len) a.data.unpacked_cache[i] else 0; + const b_t = if (i < b.data.trit_len) b.data.unpacked_cache[i] else 0; const a_nz = a_t != 0; const b_nz = b_t != 0; @@ -201,8 +199,8 @@ pub fn diceDistance(a: *Hypervector, b: *Hypervector) f64 { const len = @max(a.data.trit_len, b.data.trit_len); for (0..len) |i| { - const a_t = if (i < a.data.trit_len) a.data.unpacked_cache.?[i] else 0; - const b_t = if (i < b.data.trit_len) b.data.unpacked_cache.?[i] else 0; + const a_t = if (i < a.data.trit_len) a.data.unpacked_cache[i] else 0; + const b_t = if (i < b.data.trit_len) b.data.unpacked_cache[i] else 0; if (a_t != 0) a_count += 1; if (b_t != 0) b_count += 1; @@ -450,9 +448,7 @@ pub const SparseHypervector = struct { for (0..self.indices.items.len) |i| { const idx = self.indices.items[i]; const val = self.values.items[i]; - if (hv.data.unpacked_cache) |cache| { - cache[idx] = val; - } + hv.data.unpacked_cache[idx] = val; } hv.data.dirty = true; diff --git a/src/vm.zig b/src/vm.zig index 00a5f68dcc..24e31b66b1 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -1146,11 +1146,10 @@ test "VSA VM f16: v_f16_load quantizes correctly" { try std.testing.expectEqual(@as(usize, 16), v0.trit_len); // All values should be in {-1, 0, +1} + v0.ensureUnpacked(); for (0..16) |i| { - if (v0.unpacked_cache) |cache| { - const val = cache[i]; - try std.testing.expect(val == -1 or val == 0 or val == 1); - } + const val = v0.unpacked_cache[i]; + try std.testing.expect(val == -1 or val == 0 or val == 1); } } diff --git a/src/vsa_jit.zig b/src/vsa_jit.zig index d2fbcfb9a1..2ee8736e69 100644 --- a/src/vsa_jit.zig +++ b/src/vsa_jit.zig @@ -114,7 +114,7 @@ pub const JitVSAEngine = struct { /// Fallback to non-JIT dot product (for comparison) pub fn dotProductFallback(a: *HybridBigInt, b: *HybridBigInt) i64 { - return @intCast(a.dotProduct(b, std.heap.page_allocator)); + return @intCast(a.dotProduct(b)); } // ═══════════════════════════════════════════════════════════════════════════ diff --git a/tools/reproduce.py b/tools/reproduce.py index 01a9e5c4a7..0f2b4f50e4 100644 --- a/tools/reproduce.py +++ b/tools/reproduce.py @@ -16,13 +16,15 @@ -------------- specs/reproduce/headless.t27 is the declared profile: the toolchain, the two commands, the 46 artifacts the build installs, the test roots the test step compiles, the floors the graph may not drop below, and -the test roots known not to compile, each with its reason and the capability card it blocks. The tool -runs the commands and judges what they did: +the test roots allowed not to compile, each with its reason and the capability card it blocks (none +since the change after #1207 fixed the five that did not). The tool runs the commands and judges what +they did: * the exit code of each command is read from the process, never through a pipe; * a compile failure must be a BLOCKED root, and a BLOCKED root must still fail (a fixed one is reported, so the ledger cannot outlive the defect); - * a failed test, a leak, or a failure the output does not name fails; + * a failed test, a leak, more skipped tests than the spec knows, or a failure the output does not + name fails; * a missing install fails; every install is hashed into the evidence; * the number of run steps, of tests passed, and (with fresh caches) the set of compiled test roots may not drop below the spec -- a graph that shrank is a partial graph; @@ -121,12 +123,29 @@ def parse_summary(text: str) -> dict: "tests_leaked": num(r"(\d+) leaked")} +def summary_tree(text: str) -> str: + """The step tree zig prints after its Build Summary line. The same step lines also head each + error report above it, so reading the whole output counts every failed step twice.""" + lines = text.split("\n") + at = next((i for i, x in enumerate(lines) if x.startswith("Build Summary:")), None) + return "\n".join(lines[at + 1:]) if at is not None else "" + + def parse_run_steps(text: str) -> dict: - """The run steps of the summary tree: how many, how many ran a test, how many ran none.""" - counted = [int(m.group(1)) for m in re.finditer(r"run test (\d+) passed", text)] - empty = len(re.findall(r"run test success\b", text)) - failed = len(re.findall(r"run test (?:\d+/\d+ passed, )?\d+ failed", text)) - return {"with_tests": len(counted), "empty": empty, "with_failures": failed, "tests_in_run_steps": sum(counted)} + """The run steps of the summary tree: how many ran a test (a step whose tests failed ran them + too), how many had a failure, how many ran none. 'run test 78 passed 4 skipped', + 'run test 1/3 passed, 1 failed, 1 skipped', 'run test success'.""" + steps = re.findall(r"run test (\d+)(?:/\d+)? passed(, \d+ failed)?", summary_tree(text)) + empty = len(re.findall(r"run test success\b", summary_tree(text))) + return {"with_tests": len(steps), "empty": empty, "with_failures": sum(1 for _, f in steps if f), + "tests_in_run_steps": sum(int(p) for p, _ in steps)} + + +def parse_failed_tests(text: str) -> list: + """"error: 'a.test.a fails' failed: expected 1, found -1" -- the test, and the first line of its + report. That line is whatever the test wrote first, which for a test that prints is its own + output and not the reason; the whole report is in the printed output and the log.""" + return [{"test": m.group(1), "first_line": m.group(2).strip()[:200]} for m in re.finditer(r"^error: '([^']+)' failed:(.*)$", text, re.M)] def rel(path: str, workspace: str) -> str: @@ -162,7 +181,7 @@ def read_pins(zon: str) -> dict: def first_errors(text: str, n=3) -> list: - return [x.strip()[:200] for x in text.split("\n") if ": error:" in x][:n] + return [x.strip()[:200] for x in text.split("\n") if ": error:" in x or x.startswith("error:")][:n] # =========================================================================================== @@ -221,9 +240,11 @@ def measure(sp: dict, zig: str, reuse_cache: bool, build: bool) -> dict: t = run(test_cmd, env, 7200) doc["test"] = {"command": t["command"], "rc": t["rc"], "seconds": t["seconds"], "summary": parse_summary(t["output"]), "run_steps": parse_run_steps(t["output"]), "compile_failures": parse_compile_failures(t["output"], str(ROOT)), + "failed_tests": parse_failed_tests(t["output"]), "compiled_roots": parse_compiled_roots(t["output"], str(ROOT)) if not reuse_cache else None, "leak_lines": [x.strip()[:200] for x in t["output"].split("\n") if re.search(r"\bleak", x, re.I)][:5], "errors": first_errors(t["output"])} + doc["outputs"] = {"test": t["output"]} | ({"build": b["output"]} if build else {}) if scratch: shutil.rmtree(scratch, ignore_errors=True) return doc @@ -276,11 +297,13 @@ def bad(code, text): bad("UNNAMED_FAILURE", "a compile failure whose root the output does not name") if s.get("found"): if s["tests_failed"] or rs["with_failures"]: - bad("TEST_FAILED", f"{s['line']}") + bad("TEST_FAILED", f"{s['line']}; {[x['test'] for x in t.get('failed_tests', [])][:8]}") if s["tests_leaked"] or t["leak_lines"]: bad("LEAK", f"{s['tests_leaked']} leaked; {t['leak_lines'][:2]}") if s["steps_failed"] != len(t["compile_failures"]) + rs["with_failures"]: bad("UNEXPLAINED_STEP_FAILURE", f"{s['steps_failed']} failed step(s), {len(t['compile_failures'])} compile failure(s) and {rs['with_failures']} failed run step(s) named") + if s["tests_skipped"] > sp["TESTS_SKIPPED_MAX"]: + bad("SKIPPED", f"{s['tests_skipped']} tests skipped, at most {sp['TESTS_SKIPPED_MAX']} are known") if s["tests_passed"] < sp["TESTS_PASSED_MIN"]: bad("PARTIAL_GRAPH", f"{s['tests_passed']} tests passed, the floor is {sp['TESTS_PASSED_MIN']}") if s["steps_total"] < sp["TEST_STEPS_MIN"]: @@ -308,6 +331,7 @@ def bad(code, text): # =========================================================================================== def self_check() -> int: ok = True + seen = set() def expect(cond, what): nonlocal ok @@ -315,18 +339,24 @@ def expect(cond, what): ok = ok and bool(cond) sp = load_spec(SPEC) blocked = sp["BLOCKED_ROOTS"] - fail_block = "".join(f"error: the following command failed with 1 compilation errors:\n/zig/zig test -ODebug -Mroot=/work/{r} --name test\n" for r in blocked) - steps_total = sp["TEST_STEPS_MIN"] - good_out = (fail_block + f"Build Summary: {steps_total - len(blocked)}/{steps_total} steps succeeded; {len(blocked)} failed; " - f"{sp['TESTS_PASSED_MIN']}/{sp['TESTS_PASSED_MIN']} tests passed\n" - + "".join("+- run test 7 passed 1ms MaxRSS:1M\n" for _ in range(sp["RUN_STEPS_WITH_TESTS_MIN"])) - + "".join("+- run test success 1ms MaxRSS:1M\n" for _ in range(sp["EMPTY_RUN_STEPS_MAX"])) - + "".join(f"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/zig/zig test -ODebug -Mroot=/work/{r} --listen=-\n" for r in sp["TEST_ROOTS"])) + steps_total, passed, skipped = sp["TEST_STEPS_MIN"], sp["TESTS_PASSED_MIN"], sp["TESTS_SKIPPED_MAX"] + + def fails(roots): + return "".join(f"error: the following command failed with 1 compilation errors:\n/zig/zig test -ODebug -Mroot=/work/{r} --name test\n" for r in roots) + + def summary(steps_failed=len(blocked), ok_tests=passed, failed_tests=0, skipped_tests=skipped): + # The shape zig 0.15 prints: a count that is zero is left out. + line = f"Build Summary: {steps_total - steps_failed}/{steps_total} steps succeeded" + (f"; {steps_failed} failed" if steps_failed else "") + line += f"; {ok_tests}/{ok_tests + failed_tests + skipped_tests} tests passed" + return line + (f"; {skipped_tests} skipped" if skipped_tests else "") + (f"; {failed_tests} failed" if failed_tests else "") + "\n" + tree = ("".join("+- run test 7 passed 1ms MaxRSS:1M\n" for _ in range(sp["RUN_STEPS_WITH_TESTS_MIN"])) + + "".join("+- run test success 1ms MaxRSS:1M\n" for _ in range(sp["EMPTY_RUN_STEPS_MAX"])) + + "".join(f"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/zig/zig test -ODebug -Mroot=/work/{r} --listen=-\n" for r in sp["TEST_ROOTS"])) + good_out, good_rc = fails(blocked) + summary() + tree, 1 if blocked else 0 good_build = f"Build Summary: {sp['BUILD_STEPS_MIN']}/{sp['BUILD_STEPS_MIN']} steps succeeded\n" - good_deps = {n: [u, h] for n, u, h in zip(sp["DEPENDENCIES"], sp["DEPENDENCY_URLS"], sp["DEPENDENCY_HASHES"])} - def record(test_out=good_out, test_rc=1, build_out=good_build, build_rc=0, installs=None, zig=sp["ZIG"], deps=None): + def record(test_out=good_out, test_rc=good_rc, build_out=good_build, build_rc=0, installs=None, zig=sp["ZIG"], deps=None): return {"zig": zig, "head": "0" * 40, "tracked_changes": [], "dependencies": deps if deps is not None else good_deps, "build": {"command": sp["BUILD"], "rc": build_rc, "summary": parse_summary(build_out), "compile_failures": parse_compile_failures(build_out, "/work"), "errors": []}, @@ -334,29 +364,62 @@ def record(test_out=good_out, test_rc=1, build_out=good_build, build_rc=0, insta "test": {"command": sp["TEST"], "rc": test_rc, "summary": parse_summary(test_out), "run_steps": parse_run_steps(test_out), "compile_failures": parse_compile_failures(test_out, "/work"), "compiled_roots": parse_compiled_roots(test_out, "/work"), "leak_lines": [x for x in test_out.split("\n") if re.search(r"\bleak", x, re.I)], "errors": []}} - codes = lambda rec: {x.split(":")[0] for x in judge(sp, rec)} + + def codes(rec, spec=sp): + got = {x.split(":")[0] for x in judge(spec, rec)} + seen.update(got) + return got + # What zig 0.15 prints, verbatim (a three-root project with a failed, a skipped and a passed test): + # every failed step appears twice, above its error report and in the tree after the summary. + real = ( + "test\n+- run test 1/2 passed, 1 failed\n" + "error: 'b.test.b fails too' failed: /zig/lib/std/testing.zig:607:14: 0x104992b in expect (test)\n" + " if (!ok) return error.TestUnexpectedResult;\n" + "error: while executing test 'b.test.b fails too', the following test command failed:\n" + "./.zig-cache/o/6e4f9704b668314d6fccf4525ed6b11b/test --cache-dir=./.zig-cache --seed=0x931f3af4 --listen=-\n" + "test\n+- run test 1/3 passed, 1 failed, 1 skipped\n" + "error: 'a.test.a fails' failed: expected 1, found -1\n" + "/zig/lib/std/testing.zig:110:17: 0x1049d57 in expectEqualInner__anon_470 (test)\n" + "error: while executing test 'a.test.a skips', the following test command failed:\n" + "./.zig-cache/o/803ae009b6bc47690f50ac5cf0b206e3/test --cache-dir=./.zig-cache --seed=0x931f3af4 --listen=-\n\n" + "Build Summary: 4/7 steps succeeded; 2 failed; 3/6 tests passed; 1 skipped; 2 failed\n" + "test transitive failure\n" + "+- run test 1/3 passed, 1 failed, 1 skipped\n| +- compile test Debug native success 1s MaxRSS:228M\n" + "+- run test 1/2 passed, 1 failed\n| +- compile test Debug native success 1s MaxRSS:236M\n" + "+- run test 1 passed 947us MaxRSS:1M\n +- compile test Debug native success 1s MaxRSS:227M\n") + rs, sm = parse_run_steps(real), parse_summary(real) + expect(rs == {"with_tests": 3, "empty": 0, "with_failures": 2, "tests_in_run_steps": 3} + and (sm["steps_failed"], sm["tests_passed"], sm["tests_skipped"], sm["tests_failed"]) == (2, 3, 1, 2), + f"real zig output: each step counted once, a step whose test failed counted as one that ran tests ({rs})") + expect([x["test"] for x in parse_failed_tests(real)] == ["b.test.b fails too", "a.test.a fails"], "real zig output: the failed tests are named") expect(not judge(sp, record()), f"a record that is what the spec says is silent ({judge(sp, record())[:2]})") - expect("EXIT_SWALLOWED" in codes(record(test_rc=0)), "planted: the test step's failures with exit 0 (the tee of #616)") - extra = "error: the following command failed with 2 compilation errors:\n/zig/zig test -ODebug -Mroot=/work/src/new_broken.zig --name test\n" - out = good_out.replace(f"{len(blocked)} failed;", f"{len(blocked) + 1} failed;") - expect("NEW_COMPILE_FAILURE" in codes(record(test_out=extra + out)), "planted: a test root that stops compiling") - first = f"error: the following command failed with 1 compilation errors:\n/zig/zig test -ODebug -Mroot=/work/{blocked[0]} --name test\n" - out = good_out.replace(first, "", 1).replace(f"{len(blocked)} failed;", f"{len(blocked) - 1} failed;") - expect("BLOCKED_NOW_COMPILES" in codes(record(test_out=out)), "planted: a blocked root that compiles again (the ledger would outlive the defect)") - out = good_out.replace(f"{sp['TESTS_PASSED_MIN']}/{sp['TESTS_PASSED_MIN']} tests passed", f"{sp['TESTS_PASSED_MIN'] - 1}/{sp['TESTS_PASSED_MIN']} tests passed; 1 failed") - expect("TEST_FAILED" in codes(record(test_out=out)), "planted: a failed assertion") - expect("LEAK" in codes(record(test_out=good_out + "error: 'x.test.y' leaked: [gpa] Memory leak detected\n")), "planted: a leak") - out = good_out.replace(f"{sp['TESTS_PASSED_MIN']}/{sp['TESTS_PASSED_MIN']} tests passed", f"{sp['TESTS_PASSED_MIN'] - 400}/{sp['TESTS_PASSED_MIN'] - 400} tests passed") - expect("PARTIAL_GRAPH" in codes(record(test_out=out)), "planted: 400 tests fewer (a partial graph)") + broken = fails(blocked + ["src/new_broken.zig"]) + summary(len(blocked) + 1) + tree + expect("EXIT_SWALLOWED" in codes(record(test_out=broken, test_rc=0)), "planted: the test step's failures with exit 0 (the tee of #616)") + expect("NEW_COMPILE_FAILURE" in codes(record(test_out=broken, test_rc=1)), "planted: a test root that stops compiling") + # The ledger, whatever the real list holds: a spec naming one more blocked root. + ledger = dict(sp, BLOCKED_ROOTS=blocked + ["src/planted_blocked.zig"]) + expect("BLOCKED_NOW_COMPILES" in codes(record(test_rc=1), ledger), "planted: a blocked root that compiles again (the ledger would outlive the defect)") + named = fails(ledger["BLOCKED_ROOTS"]) + summary(len(ledger["BLOCKED_ROOTS"])) + tree + expect(not codes(record(test_out=named, test_rc=1), ledger), "a blocked root that still fails, as its line says, is accepted") + expect("UNNAMED_FAILURE" in codes(record(test_out=good_out + "error: the following command failed with 1 compilation errors:\n/zig/zig test -ODebug --name test\n", test_rc=1)), + "planted: a compile failure whose root the output does not name") + expect("UNEXPLAINED_STEP_FAILURE" in codes(record(test_out=fails(blocked) + summary(len(blocked) + 1) + tree, test_rc=1)), "planted: a failed step the output does not explain") + out = fails(blocked) + summary(len(blocked) + 1, ok_tests=passed - 1, failed_tests=1) + tree + "+- run test 6/7 passed, 1 failed 1ms MaxRSS:1M\n" + expect("TEST_FAILED" in codes(record(test_out=out, test_rc=1)), "planted: a failed assertion") + expect("LEAK" in codes(record(test_out=good_out + "error: 'x.test.y' leaked: [gpa] Memory leak detected\n", test_rc=1)), "planted: a leak") + expect("SKIPPED" in codes(record(test_out=fails(blocked) + summary(skipped_tests=skipped + 1) + tree)), "planted: one more skipped test") + expect("PARTIAL_GRAPH" in codes(record(test_out=fails(blocked) + summary(ok_tests=passed - 400) + tree)), "planted: 400 tests fewer (a partial graph)") out = good_out.replace(f"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/zig/zig test -ODebug -Mroot=/work/{sp['TEST_ROOTS'][0]} --listen=-\n", "", 1) expect("PARTIAL_GRAPH" in codes(record(test_out=out)), "planted: a declared test root that is no longer compiled") expect("EMPTY_STEP" in codes(record(test_out=good_out + "+- run test success 1ms MaxRSS:1M\n")), "planted: one more run step that runs no test") + expect("UNEXPLAINED_EXIT" in codes(record(test_out=summary(0) + tree, test_rc=1)), "planted: a non-zero exit with no failure in the output") + expect("NO_SUMMARY" in codes(record(test_out=tree, test_rc=1)), "planted: a test step that printed no summary") + expect("TIMEOUT" in codes(record(test_rc=None)), "planted: a test step that did not finish") inst = {k: "0" * 64 for k in sp["INSTALLS"]} inst[sp["INSTALLS"][0]] = None expect("MISSING_OUTPUT" in codes(record(installs=inst)), "planted: a declared install that is not there") - expect("BUILD_FAILED" in codes(record(build_rc=1, build_out="Build Summary: 1/2 steps succeeded; 1 failed\n")), "planted: a build that fails") - expect("UNEXPLAINED_EXIT" in codes(record(test_out=good_out.replace(fail_block, "").replace(f"{len(blocked)} failed;", "0 failed;"), test_rc=1)) or - "BLOCKED_NOW_COMPILES" in codes(record(test_out=good_out.replace(fail_block, ""), test_rc=1)), "planted: a non-zero exit with no failure in the output") + expect({"BUILD_FAILED", "BUILD_STEP_FAILED"} <= codes(record(build_rc=1, build_out="Build Summary: 1/2 steps succeeded; 1 failed\n")), "planted: a build that fails") + expect("EXIT_SWALLOWED" in codes(record(build_rc=0, build_out="Build Summary: 1/2 steps succeeded; 1 failed\n")), "planted: a failed build step with exit 0") expect("TOOLCHAIN" in codes(record(zig="0.16.0")), "planted: another zig") rec = record() rec["head"] = "unknown" @@ -368,6 +431,9 @@ def record(test_out=good_out, test_rc=1, build_out=good_build, build_rc=0, insta expect(read_pins(zon) == good_deps, "the pins build.zig.zon carries are the spec's") moved = zon.replace(sp["DEPENDENCY_URLS"][2], sp["DEPENDENCY_URLS"][2].replace("b73b2fa2", "0000fa2a"), 1) expect("DEPENDENCY_DRIFT" in codes(record(deps=read_pins(moved))), "planted: zig_hdc pinned to another revision") + # Every code the judge can give has been seen to fire, so a rule added without a plant is caught here. + rules = set(re.findall(r'bad\("([A-Z_]+)"', pathlib.Path(__file__).read_text(encoding="utf-8").split("def judge(", 1)[1].split("def self_check(", 1)[0])) + expect(rules <= seen, f"every rule of the judge fired at least once ({len(rules & seen)}/{len(rules)}; never: {sorted(rules - seen)})") print("reproduce --self-check:", "ok" if ok else "FAILED") return 0 if ok else 1 @@ -397,6 +463,16 @@ def main() -> int: doc["violations"] = judge(sp, doc) out = pathlib.Path(a.out) if a.out else ROOT / "zig-out" / "reproduce" / f"{a.command}.json" out.parent.mkdir(parents=True, exist_ok=True) + # The whole output of each command goes beside the record, and a command that failed has it + # printed here too: a wrapper that keeps a tool's output must show it when the tool fails. + outputs = doc.pop("outputs") + doc["logs"] = {} + for name, text in outputs.items(): + log = out.with_name(f"{out.stem}.{name}.log") + log.write_text(text, encoding="utf-8") + doc["logs"][name] = log.name + if doc[name]["rc"] != 0: + print(f"---- the output of `{doc[name]['command']}` (exit {doc[name]['rc']}) ----\n{text.rstrip()}\n---- end of the output ----") out.write_text(json.dumps(doc, indent=1, sort_keys=True) + "\n", encoding="utf-8") t = doc["test"] print(f"reproduce {a.command}: zig {doc['zig']}, {'fresh' if doc['fresh_caches'] else 'reused'} caches, head {doc['head'][:12]}") @@ -405,6 +481,8 @@ def main() -> int: f"{sum(1 for h in doc['installs'].values() if h)}/{len(doc['installs'])} installs") print(f" test: exit {t['rc']}, {t['summary'].get('line', 'no summary')}") print(f" compile failures {[f['root'] for f in t['compile_failures']]}, run steps {t['run_steps']}") + for x in t["failed_tests"]: + print(f" failed: {x['test']} (first line of its report: {x['first_line']})") for x in doc["violations"]: print("VIOLATION:", x) print(f" evidence: {out}")