diff --git a/README.md b/README.md index 8ff4402..15d01e8 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ zig fetch --save https://github.com/gHashTag/zig-golden-float/archive/refs/tags/ ``` ```zig -const gf = @import("golden_float"); +const gf = @import("golden-float"); const x = gf.GF16.fromF32(3.14); const y = gf.GF16.fromF32(2.71); diff --git a/build.zig b/build.zig index a746228..42f6b6f 100644 --- a/build.zig +++ b/build.zig @@ -234,7 +234,22 @@ pub fn build(b: *std.Build) void { }); const run_jepa_t_tests = b.addRunArtifact(jepa_t_tests); + // The public module root. Nothing else compiles it, which is how thirteen + // broken import paths shipped invisibly: every consumer of + // @import("golden-float") failed while the suite stayed green. + const root_api_tests_root = b.createModule(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + const root_api_tests = b.addTest(.{ + .name = "root-api-tests", + .root_module = root_api_tests_root, + }); + const run_root_api_tests = b.addRunArtifact(root_api_tests); + const test_step = b.step("test", "Run all tests"); + test_step.dependOn(&run_root_api_tests.step); test_step.dependOn(&run_tests.step); test_step.dependOn(&run_gft_tests.step); test_step.dependOn(&run_gf_binary_tests.step); diff --git a/docs/AUDIT_2026-08-20.md b/docs/AUDIT_2026-08-20.md new file mode 100644 index 0000000..7210833 --- /dev/null +++ b/docs/AUDIT_2026-08-20.md @@ -0,0 +1,147 @@ +# zig-golden-float — Number Audit (README + top-level docs) + +**Date:** 2026-08-20 · **Repo:** `/Users/ssdm4/Desktop/PROJECTS/CLAUDE/zig-golden-float` (read-only, HEAD `ba724e8`, tag v2.1.0 present) · **Toolchain:** zig 0.16.0 +**Method:** number-audit checklist (`~/.claude/skills/number-audit/SKILL.md`) — two-phase (classify → verify claims only); enumerate/run instead of deriving; a "measured" number with no in-repo script/record is a rumor; fixed constants grepped repo-wide; negative-existence backed by a search transcript. Naming: the ternary-exponent family is **GF-T** (this repo's own name). + +## Instruments run + +| Instrument | Result | +|---|---| +| `zig build test --summary all` | **21/21 steps, 163/163 tests PASS, 0 fail** (formats 27, gft 14, gf-binary 11, transcendental 14, c-abi 58, trinity-constants 6, phi-attention 10, trinity-init 10, jepa-t 11, tri-reader 2) | +| `zig test src/formats/gf8.zig` (standalone; **not** in the build's test step) | 8/8 PASS | +| Consumer compile against the public module (`-Mgolden-float=src/root.zig`) | **FAILS** — `src/root.zig` imports `"src/formats/…"` relative to `src/` → `src/src/…` FileNotFound (×3) | +| `zig run examples/gft_usage.zig` (the file's own documented command) | **FAILS** — `import of file outside module path` | +| `probe.zig` (direct imports of `gf_binary.zig`/`gft.zig`/`gf8.zig`/`formats_root.zig`) | codec oracle: bit patterns, ranges, ties, MSE reruns — details inline below | +| `probe_rne.zig` (tie case 1+2.5/512) | GF16 mant=3, GFT16 mant=3 → rounding is half-**away-from-zero**, not ties-to-even | + +Record files present: `.trinity/results/{bench_007b,bench_008,bench_009_transformer_attention,bench_010}.log`, `tables/*.csv`, `docs/whitepaper/bench_001_results.csv`. Not present (search transcripts in rows): any FPGA-timing record, BENCH-002 record, `zig-float-audit.md`, a binary-ladder catalog past GF32. + +**Instrument caveat discovered:** `benches/bench_007b_extended_range.rs` (source of `bench_007b.log`, i.e. BENCH-007/007b) quantizes "GF" formats with a **base-φ model** (`phi_quantize`: exponent in base φ, GF8 max = φ³ ≈ 4.236, GFTernary = {−φ, 0, +φ}). The shipped Zig codecs are **base-2** (`(1+M/2^m)·2^e`). The two instruments disagree: at uniform ±100 the log has GF16 MSE 0.0474 > bf16 0.0390, while the shipped codec (probe + `bench_010.log`) has GF16 16× **better** than bf16. Several doc claims trace to the φ-model, not to the shipped code — flagged per row. + +## Verdicts + +Legend: **R** = REPRODUCES · **DNR** = DOES-NOT-REPRODUCE (claimed → actual) · **U** = UNCHECKED (reason). + +### README.md + +| # | Loc | Claim | Verdict | +|---|-----|-------|---------| +| R1 | :17 | GF16 `[s:1][e:6][m:9]`, bias 31 | **R** — code constants + in-suite golden vectors + probe (1.0 → 0x3E00) | +| R2 | :17 | GF16 range **~±65504** | **DNR** — actual max finite ±4.29e9 (probe: `maxFinite`=4.2908e9; 70000 encodes finite). 65504 is **fp16's** max; spec-gf16.md itself says ±4.3e9 | +| R3 | :17 | GF16 "no subnormals" | **R** — probe: fromF32(1e-10) → 0 (flush). Contradicts docs/spec-gf16.md §2 (see S4) | +| R4 | :18 | fp16 bias 15, ±65504, full subnormal support | **R** (standard params; repo's fp16 decode handles subnormals). Note: repo's fp16 *encoder* truncates mantissa — see R8 | +| R5 | :19 | bf16 bias 127, ~±3.4e38, canonical `(bits +| 0x7FFF) >> 16` | **R (approx)** — actual encoder is RNE: `(bits +| ((bits>>16)&1) + 0x7FFF) >> 16`; README drops the tie bit | +| R6 | :20 | GF8 `[1:3:4]` bias 7, range **~±4.24**, "saturates outside φ³" | **DNR** — no shipped codec does this: standalone `gf8.zig` (bias 7) clamps at **1.9375** (probe); ladder `gf_binary.GF8` (bias 3) max **15.5**. ±4.24 = φ³ exists only in the Rust base-φ model (`phi_quantize`, max φ^(2^2−1)) | +| R7 | :21 | GFTernary {−1, 0, +1}, ±0.5 threshold, range ±1 | **R** vs `formats_root.zig` (probe: 0.6→1, 0.4→0). But whitepaper §9.1 + the Rust bench use **{−φ, 0, +φ}** — cross-doc contradiction (the format's values differ by φ between docs) | +| R8 | :23 | "All formats use **round-to-nearest-even** via quantizeValue()" | **DNR** — probe_rne: GF16/GFT tie 1+2.5/512 → mant 3 (half-away-from-zero; RNE ⇒ 2). `f32ToFp16` **truncates** (`mant >> 13`, no rounding). Only bf16 is genuine RNE | +| R9 | :32–43 | Ladder rule `e=round((N−1)/φ²)`, `m=N−1−e`, `bias=2^(e−1)−1` and the GF4…GF32 table (`[1:1:2]`/0, `[1:3:4]`/3, `[1:4:7]`/7, `[1:6:9]`/31, `[1:7:12]`/63, `[1:9:14]`/255, `[1:12:19]`/2047) | **R** — recomputed independently for all 7 widths (e.g. round(31/2.618)=12, 2^11−1=2047); code constants match; in-suite rule-conformance test re-derives them; 163/163 green | +| R10 | :45 | "ladder continues to **GF1024 (17 binary rungs total)**" | **U** — no in-repo catalog: grep `GF1024` hits only `specs/gft.tri` (GF-**T**1024, ternary ladder, 9 rungs) and this README line; FORMAT-SPEC-001 is not in this repo | +| R11 | :40 | GF16 "**FPGA 35/35 @ 323 MHz Artix-7**" | **U (rumor)** — no script, log, test, or `MHz`/`Artix`/`35/35` hit anywhere in the repo outside this line (grep transcript over src/ benches/ tests/ specs/ docs/ tables/ .trinity/). Per the skill: a measured number with no record is a rumor | +| R12 | :57–60 | GF8 bias discrepancy: normative `2^(e−1)−1 = 3`, standalone gf8.zig encodes bias 7 | **R** — `specs/gf8.tri` bias: 3; `gf8.zig` `Bias = 7`. The README's self-disclosure is accurate | +| R13 | :69–74 | GF-T table: EXP_OFFSET 4/13/40/364, special row 8/26/80/728 | **R** — code constants + in-suite test; recomputed (3^E−1)/2 and 3^E−1 for E=2,3,4,6 | +| R14 | :69–74 | GF-T "Exponent range **±4 / ±13 / ±40 / ±364**" | **DNR** — max finite e = EXP_OFFSET−1 (top offset row reserved). Probe: GFT16 holds 5.4e11 (2^39·x) but 1.2e12 (needs e=40) → Inf; GFT4: 16.0 → Inf. Code doc itself says e∈[−40,+39]. Actual ranges: [−4,+3], [−13,+12], [−40,+39], [−364,+363]. `specs/gft.tri` `exp_range` carries the same +1 overstatement | +| R15 | :69–74 | Dynamic range ~2.4 / ~8 / ~24 / ~219 decades | **R** (convention: log10(2^OFFSET_MAX) = max_finite/min_normal) — 2.41 / 7.83 / 24.08 / 219.15 recomputed | +| R16 | :76 | GF-T16 keeps the 9-bit mantissa across its whole range | **R** — fixed mantissa field by construction (no regime/taper) | +| R17 | :77 | "tekum16 tapers to ~4 bits at the extremes" | **U** — no tekum oracle/record in this repo (external claim) | +| R18 | :91 | GFT16 mul example prints "~8.539" | **R (approx)** — actual output **8.53125** (probe); 8.5397 is the exact product, 9-bit mantissa lands 0.1% below | +| R19 | :100–105 | GFT16: 1e30→Inf, 1e-30→0; GFT32 holds ~219 decades incl. 6.022e23 at 25-bit precision | **R** — probe: GFT32(6.022e23) round-trips with 0 rel-err in f32; 1e30 finite; GFT16 overflow/underflow as claimed (also in-suite tests) | +| R20 | :109–113 | Factory `GFT(5,12)`; constants EXP_TRITS/MANT_BITS/EXP_OFFSET/OFFSET_MAX/BITS/Repr exposed | **R** in code (probe compiled against `gft.zig`) — but unreachable through the public module (R22) | +| R21 | :114 | "A **runnable** copy lives in examples/gft_usage.zig" | **DNR** — its own documented command `zig run examples/gft_usage.zig` fails: `import of file outside module path` (imports `../src/formats/gft.zig`) | +| R22 | :50–55, 82–106, 122–129 | All library usage examples: `@import("golden-float")` / Quick Start `@import("golden_float")` | **DNR** — the public module **cannot compile**: `src/root.zig` imports `"src/formats/…"` relative to `src/` (→ `src/src/…` FileNotFound, verified by consumer compile). Quick Start additionally uses the wrong module name (`golden_float` vs registered `golden-float`). Invisible to CI: no build step compiles root.zig — the green check does not exist for the public API | +| R23 | :119 | `zig fetch …/tags/v2.1.0.tar.gz` | **R** — tag v2.1.0 exists in the repo | +| R24 | :161–174 | Bindings coverage matrix (GF16 rich + ladder + GF-T across C/C++/Rust/Python/Go; GF-T4 C-ABI only) | **U (partial)** — wrapper files exist for every claimed cell (go/: gf16, gf_ladder, gft8/16/32; cpp/, rust/goldenfloat-sys/, python/goldenfloat/; `gft4_*` ×11 in c_abi.zig); the language test suites were not run in this audit | +| R25 | :168,174 | GF4 degenerate — "no normal values" (zero/Inf/NaN only) | **R** — 1-bit exponent: E=0 → zero, E=1=EXP_MAX → Inf/NaN; MIN_E(1) > MAX_E(0) | +| R26 | :197–206 | φ-FMA API (`gf16_fma/fms/fnma/phi_fma/phi_dot`) | **R** — all exported in `src/c_abi.zig`, declared in `src/c/gf16.h`; covered by the 58 c-abi tests | +| R27 | :215 | Fibonacci mask `{1,2,3,5,8,13,21,34,55,89,144}` → 2.15% sparsity | **R** — `FIB_VISIBLE` is exactly that 11-element set; 11/512 = 2.148% | +| R28 | :217 | Warmup "Fib(7)=21 steps" | **R** for the operative number — `LR_WARMUP_STEPS = 21`. "Fib(7)=21" holds only under 0-based indexing of the code's table (1,1,2,3,5,8,13,21); classic 1-based gives Fib(8)=21 — reading-dependent label, correct constant | +| R29 | :218 | JEPA-T Encoder 6 + Predictor 3 layers | **R** — `EncoderLayers=6`, `PredictorLayers=3` | +| R30 | :224 | GF16 accuracy vs fp32 (σ=1.0) **> 99.99%** | **U (unfalsifiable as stated)** — no record defines the metric (no σ=1.0 accuracy line in any log; grep "99.99" hits only READMEs/CHANGELOG). Probe under a 1−mean-rel-err reading: **99.965%** — fails ">99.99%"; under an MSE reading (MSE ~1.7e-11 at σ=0.01-1) it trivially passes. The sentence needs its reading named | +| R31 | :225 | GF16 vs bf16 MSE ratio (uniform ±100) **16.2× better** | **R** — record `bench_010.log` UNIFORM_100: bf16 9.37e-3 / gf16 5.77e-4 = **16.2**; independent probe on the shipped codec: 16.0× (±100), 16.0× (±1). (The φ-model log `bench_007b.log` says the opposite — see instrument caveat) | +| R32 | :226 | GF16 sparsity at [−10,10]: 0% (no saturation) | **R** — codec max 4.29e9 ≫ 10; no clipping possible | +| R33 | :227 | GFTernary sparsity (He init σ=0.05) 100% | **R** — record `bench_008.log`: 100.0%; consistent with threshold 0.5 = 10σ | +| R34 | :228 | Pearson r(φ-distance, MSE) = **−0.34** | **R (approx, ambiguous source)** — `bench_008.log`: r = −0.3494 (weight MSE; rounds to −0.35, truncates to −0.34). `bench_007b.log` has a different r = −0.4200 for the same-named quantity. README doesn't name which experiment | +| R35 | :230 | "Full results in `.trinity/results/`" | **R (partial)** — 4 logs exist there (007b/008/009/010); BENCH-001–006 records live in `tables/` and `docs/whitepaper/` instead | +| R36 | :242–243 | `goldenfloat_phi()` = 1.6180339887…, `goldenfloat_trinity()` = φ²+φ⁻² = 3 | **R** — exported + asserted in the c-abi suite (green) | +| R37 | :256 | Version 2.1.0 | **R** — build.zig.zon 2.1.0, shared-lib version 2.1.0, git tag v2.1.0, CHANGELOG 2.1.0 | + +### docs/spec-gf16.md + +| # | Loc | Claim | Verdict | +|---|-----|-------|---------| +| S1 | §1 | Layout [1:6:9], bias 31, bit masks | **R** | +| S2 | Abstract vs §3 vs App. B | GF16 φ-distance = **0.049** (abstract) vs **0.018** (§3 table, f16=0.049 there) vs **0.018** with f16=**0.082** (App. B) | **DNR (internally contradictory)** — three conventions in one document. §3 is self-consistent under m/(e+m); App. B mixes e/m ratios with φ-distances matching neither column; the abstract's 0.049 comes from the |e/m−1/φ| convention used elsewhere. The same number migrates between formats depending on section | +| S3 | §2 | Normal range 2^-30…2^31, ≈ ±4.3e9 | **R** — probe max 4.2908e9, min pos normal 9.31e-10 = 2^-30 | +| S4 | §2 | Subnormals: E=0, M≠0 → value; min subnormal ≈ **4.7e-10** | **DNR twice** — (a) the shipped codec has **no subnormals** (probe: 1e-10 → 0; decode E=0 → 0); (b) 4.7e-10 = 2^-31 doesn't match the section's own formula 2^-30/512 = 1.8e-12. The 2^-31 figure also appears in `bench_001_results.csv` / `gf16_comparison.md` as "min positive" — 2× below the true min normal 2^-30 | +| S5 | §8 | Test vectors: 1.0→0x3C00, 2.0→0x3D00, π→0x3E23, ∞→0x7C00, NaN→0x7C01 | **DNR** — actual codec (probe + in-suite golden vectors): 1.0→**0x3E00**, 2.0→**0x4000**, π→**0x4124**, ∞→**0x7E00**. The table's codes are IEEE-fp16-shaped, not GF16 | +| S6 | App. A | k_optimal = 16×0.382 = 6.11 → 6:9 split | **R** (arithmetic) | +| S7 | App. B | Max 4.3e9 ✓; min 4.7e-10; precision 2.8 digits | max **R**; min **DNR** (see S4); 2.8 digits ≈ 9·log10 2 = 2.71 — **R (approx)** | +| S8 | §3 | "GF16 has the smallest φ-distance of any practical 16-bit format" | **R under both conventions** vs fp16/bf16 (0.018<0.049<0.151 and 0.049<0.118<0.525) — though the attached value wobbles (S2) | + +### docs/whitepaper.md + +| # | Loc | Claim | Verdict | +|---|-----|-------|---------| +| W1 | §1.2 | BENCH-004b: GF16 97.67% = f32 (0.00% gap); bf16/ternary 9.80% (−87.87%) | **R vs record** — `tables/cpu_accuracy.csv` matches digit-for-digit. **U vs experiment** — no MNIST training/eval script in the repo to re-run | +| W2 | §1.4 | BENCH-005: add 118 LUT (59×), mul 94 LUT + 1 DSP (47×), ternary 2 LUT | **R vs record** — `tables/fpga_unit_level.csv` matches; ratios recomputed (118/2, 94/2). No synthesis scripts in repo to re-run | +| W3 | §1.4 | BENCH-006: ternary 52 LUT / GF16 71 LUT + 16 DSP, 1.37× | **R vs record** — `tables/fpga_mac_level.csv`; 71/52 = 1.365 | +| W4 | §1.4 | Capacity: 1219 / 893 / 15 units on XC7A100T (63400 LUT, 240 DSP) | **R vs record + arithmetic** — 63400/52=1219.2, 63400/71=892.9, 240/16=15 | +| W5 | §1.1 | BENCH-002: GF16 add **7.2 ns/op, 15% faster** than soft-fp16 | **U (rumor)** — grep: "7.2 ns" appears only in this claim; no bench log or record anywhere in the repo | +| W6 | §1.3 | BENCH-007 φ-distance column (GFTernary 0.000, GF16 0.049, fp16 0.118, GF8 0.132, GF64 0.264, GF32 0.340, bf16 0.525) | **DNR as a measurement** — the values are **hard-coded inputs** in `bench_007b_extended_range.rs` (`FormatSpec.phi_distance`, comment "from BENCH-007"), not computed. No single convention reproduces the column: GF16/GF8/fp16/bf16 fit \|e/m−φ⁻¹\|; GF32 0.340 needs \|13/18−φ⁻²\|; GF64 0.264 fits **no** tried convention (21/42=0.5 → 0.118 either way) | +| W7 | §9.1 vs §11.1 vs code | GF32 split: 1:13:18 (§9.1) vs 1:8:23 bias 127 (§11.1 — that's IEEE f32) vs shipped `gf_binary.GF32` = **1:12:19 bias 2047** | **DNR (three-way contradiction)** — only 12:19 exists in code and follows the repo's own φ² rule (round(31/2.618)=12) | +| W8 | §9.1 vs §12 | GF64 = 21:42 (φ-dist 0.264) | **DNR vs repo's own rule** — round(63/φ²) = **24** → 24:39; §12 of the same document uses GF64 (24:39) with φ-dist 0.003 (=\|24/39−φ⁻¹\|, checked). Same doc, two GF64s, 0.264 vs 0.003 | +| W9 | §9.3, §11.5.2 | Lucas closure: 3, 7, 18, 47; φ¹²+φ⁻¹²=322, φ¹⁸+φ⁻¹⁸=5778; log₂ 5.5/8.3/12.5 | **R** — recomputed (Lucas L8=47, L12=322, L18=5778) | +| W10 | §9.3 vs §11.5.2 | GF16 safe MAC depth: **256** (=2^9/2, §9.3) vs **≤ 8.3** (§11.5.2) | **DNR (internal contradiction)** — two incompatible theories for the same quantity, 30× apart | +| W11 | §8.6, §9.2 | lr = 0.004 = α_φ/φ³ | **DNR (arithmetic)** — 0.118034/4.2361 = **0.0279**, not 0.004 | +| W12 | §11.5.3 vs §11 init | α_φ = φ⁻³/2 = (√5−2)/2 ≈ 0.118034 vs σ_gauge = α_φ = **(φ−1)/2** ≈ 0.118034 | first two forms **R** (0.11803); "(φ−1)/2" **DNR** — that's 0.309. Code `ALPHA_PHI = φ−1.5` = 0.118034 (**R**) | +| W13 | §11.8 | LR table: e.g. τ=1000 → 0.014421 labeled "α_φ·φ^(−1)" | **DNR (internal + vs code)** — α_φ·φ⁻¹ = 0.0730 ≠ 0.014421; the Formula and Value columns disagree on every non-zero row; the shipped `phiLrSchedule` implements a third law (φ^(−t·total/τ²), τ=228.9) matching neither | +| W14 | §11.9 | φ-power ladder: φ⁵ = **10.944272** | **DNR** — φ⁵ = 11.090170. (All other rows φ²…φ¹¹ recompute correctly) | +| W15 | §11.9 | "Attention reduction 78.5% = 1 − (5632/262144)"; "reducing complexity by 78.5%" | **DNR (own formula)** — 1 − 5632/262144 = **97.85%**. The visible fraction is 2.15%; 78.5% reproduces under no reading of the given numbers | +| W16 | §11.2 | GF8 range [~0.0078, 1.9375]; "8/8 tests pass" | max **R** (probe clamp 1.9375); 8/8 **R** (`zig test src/formats/gf8.zig` — but this file is **not** in `zig build test`); lower bound **DNR (approx)** — min normal 2^-6 = 0.0156, min denormal 2^-10 = 9.77e-4; 0.0078 = 2^-7 matches neither | +| W17 | §9.1 vs gf8.zig | GF8 φ-distance 0.132 vs gf8.zig header "0.047" | **DNR (cross-doc)** — 0.132 = \|3/4−φ⁻¹\|, 0.047 = \|4/7−φ⁻¹\| (m/(e+m)); same format, two published values | +| W18 | Abstract | GF16 "requiring **47–59× fewer** hardware resources (unit-level)… compared to minimal ternary logic" | **DNR (direction inverted)** — the paper's own §1.4/§2.2: GF16 requires 47–59× **more** LUT than ternary | +| W19 | §6.1, §10 | "GF16 achieves **10× energy savings** vs FP32" | **DNR (own table)** — §6.1 lists GF16 at 2× savings (1.56× total vs 2.0×); the 10× row is **Ternary**. Projection table itself is labeled "Estimated" (U as measurement) | +| W20 | §6.2 | Hybrid 18.4 GOPS = 1.44× baseline; 44% of FP32 throughput | 18.4/12.8 = 1.44 **R (arithmetic)**; "44%" is inconsistent with 144% in the same row — reading unclear; projections have no record (**U**) | +| W21 | §12 | Catalog φ-distances: fp32 0.270, fp64 0.406, fp128 0.484, binary256 0.538 | **R** under \|e/m−φ⁻¹\| (recomputed all four) — but the headline "0.003 (GF64)" contradicts §9.1's 0.264 (see W8) | +| W22 | §1.1 | BENCH-001 "GF16 ≈ fp16, 2× better than bf16" | **U (record defective)** — `tables/quantization_results.csv` is malformed (9-column header over 7/8-field rows; columns misaligned); `docs/whitepaper/bench_001_results.csv` has format params but its GF16 min_positive repeats the 2×-off 4.66e-10 (see S4) | + +### docs/multi-language-audit.md + +| # | Claim | Verdict | +|---|-------|---------| +| M1 | 84 = 62+6+5+3+4+4 issues; urgent 32; filed 17 | **R (internal sums)** — all three totals re-add correctly. The external issue counts themselves: **U** | +| M2 | "45 test vectors (f32 → GF16 → f32)" | **R (count)** — docs/test-vectors.csv has exactly 45 data rows. Content: see T1 — the vectors are wrong | +| M3 | "zig-float-audit.md ✅ 62 Zig issues documented" | **DNR (existence)** — `find . -name "zig-float-audit*"` (excluding .git/worktrees): no file in the repo | +| M4 | "c/gf16.h 80 lines, c/gf16.c 300 lines" | **DNR** — no `c/` directory; `src/c/gf16.h` is 405 lines; no gf16.c exists | +| M5 | 70B model RAM: GF16 **14 GB**, "10× vs FP16" | **DNR (arithmetic + own table)** — the same table says GF16 = 16 bits = same memory as FP16 → 70B×2B = **140 GB**. 14 GB contradicts the row two lines above it | +| M6 | Compute "**Add only**", 10× vs FP16/FP32 | **DNR vs shipped code** — every shipped GF16/GF-T multiply routes through f32 multiplication (`fromF32(a.toF32()*b.toF32())`); no add-only/log-domain implementation exists in the repo | +| M7 | Rust example: 3.14159 → 3.14062 "(**0.003%** error)" | **DNR (arithmetic)** — (3.14159−3.140625)/3.14159 = **0.031%** (10× understated). The value 3.140625 itself: **R** (probe) | +| M8 | SIMD 56 vs 2304 = 41× | 2304/56 = 41.1 **R (arithmetic)**; provenance of both counts: **U** (no record) | +| M9 | arXiv:2602.15266 quote | **U** — external source, not fetched in this audit | + +### docs/test-vectors.csv + +| # | Claim | Verdict | +|---|-------|---------| +| T1 | The 45-vector table as the GF16 conformance record | **DNR wholesale** — codes are fp16-shaped: 1.0→0x3C00 claimed vs **0x3E00** actual; 1.5→0x3E00 claimed (collides with actual 1.0); rows 33/38 assign the same 0x7BFF to both 4.29e9 and 65504; row 40 claims 100.0→Infinity (GF16 max is 4.29e9); ∞→0x7C00 vs actual 0x7E00. The real, codec-matching vectors live in the in-suite golden-vector tests (`gf_binary.zig`, `gft.zig`) and `testdata/gf_conformance.csv` | + +### docs/gft-spec-first-plan.md · CHANGELOG.md · CLAUDE.md · build.zig + +| # | Claim | Verdict | +|---|-------|---------| +| G1 | tri_gen is a template copier: all four emitters `_ = spec;` + copy | **R** — grep: 4× `_ = spec;` in tools/gen/tri_gen.zig. (This doc is itself a correction and survives audit) | +| C1 | CHANGELOG v2.1.0 summary: GF16 MSE ±100 = 2.3e-3, bf16 3.8e-2, "16.3× lower" | **DNR vs its own record** — `bench_010.log` (which this very release "committed") says 5.77e-4 / 9.37e-3 / 16.2×; also internally 3.8e-2/2.3e-3 = 16.5, not 16.3 | +| C2 | CHANGELOG: jepa total ≈16.4 MB, relaxed to 17 MB | **R** — jepa-t test asserts < 17 MB, green | +| C3 | CHANGELOG: "GF8 saturates at [−10,10] (φ³ ≈ 4.24)" | **DNR vs shipped codecs** — 1.9375 (standalone) / 15.5 (ladder); φ³ is the Rust φ-model only (see instrument caveat) | +| C4 | CHANGELOG v2.0.0 summary: Pearson −0.42 | **R** — matches bench_007b.log (−0.4200) | +| CL1 | CLAUDE.md φ-distance table: TF3-9 0.018, GF16 0.049, f16 0.118 | **R (self-consistent** under \|e/m−φ⁻¹\|**)** — but assigns 0.018/0.049 to different formats than spec-gf16.md §3 does (see S2): the cross-doc contradiction stands | +| CL2 | CLAUDE.md constants PHI/PHI_SQ/PHI_INV_SQ/TRINITY | **R** — match code to printed digits | +| B1 | build.zig: lib version 2.1.0; step list in header comment | **R** — matches zon/tag; steps exist. **Meta-finding:** the `test` step never compiles `src/root.zig` (public API), `src/formats/gf8.zig`, or `tests/*.zig` (orphaned: `tests/gf8_max_value.zig` — 7 tests, `tests/whitepaper_results.zig` — asserts whitepaper constants **against themselves**, a tautological instrument), so "zig build test green" does not cover the public API surface (see R22) | + +## Headline findings + +1. **The public API does not compile** (R22, R21): `src/root.zig`'s imports are path-broken, every README usage example and the "runnable" GF-T example fail, and no CI/test step would ever notice — the check that would catch it does not exist. +2. **The GF16 test-vector record is for the wrong format** (S5, T1): docs vectors are fp16-shaped codes; the actual codec (0x3E00 for 1.0) is pinned only by in-suite golden vectors. +3. **Two GF quantizers coexist** (instrument caveat): the shipped base-2 Zig codecs vs the Rust base-φ model behind BENCH-007/007b. Claims sourced from the φ-model (GF8 ±4.24/φ³, GFTernary ±φ, GF16-worse-than-bf16-at-±100, the φ-dist column) contradict the shipped code; claims sourced from bench_010/bench_008 (16.2×, 100% sparsity, −0.3494) reproduce. +4. **Wandering constants**: φ-distance for GF16 is 0.018/0.049/0.0486 and for fp16 0.049/0.082/0.118 depending on the file/section; GF8 is 0.132 or 0.047; GF32 has three published splits, GF64 two (whitepaper §9.1 disagreeing with §12 of the same paper and with the repo's own sizing rule). +5. **No record exists** for the two flashiest performance numbers: "35/35 @ 323 MHz Artix-7" (README) and "7.2 ns/op, 15% faster" (whitepaper BENCH-002). +6. **Format-parameter core is solid**: the φ² ladder rule, all GF4–GF32 and GF-T4–GF-T32 constants, dynamic ranges, specials, and the FPGA/accuracy CSV-backed tables reproduce exactly; 163/163 in-suite tests pass — with the one systematic off-by-one that GF-T "±N" exponent ranges are really [−N, N−1], and rounding is half-away-from-zero despite three docs claiming ties-to-even. diff --git a/src/root.zig b/src/root.zig index f39e861..6a34f79 100644 --- a/src/root.zig +++ b/src/root.zig @@ -116,7 +116,9 @@ test "every public declaration of this module is analysed" { // That is not hypothetical. The same omission in gHashTag/zig-hdc hid five // distinct API-drift errors against the version of this package it pins, // while its CI stayed green throughout (zig-hdc#2). - @import("std").testing.refAllDeclsRecursive(@This()); + // refAllDeclsRecursive was removed by zig 0.16; refAllDecls references + // every top-level export, which is what forces each module file to load. + @import("std").testing.refAllDecls(@This()); } // vsa_jit was never exported, so nothing ever compiled it, so nobody found diff --git a/src/ternary/bigint.zig b/src/ternary/bigint.zig index aaf1f30..0c2a74f 100644 --- a/src/ternary/bigint.zig +++ b/src/ternary/bigint.zig @@ -544,11 +544,14 @@ pub const TVCBigInt = struct { const cmp = simdCompareTrits(a_vec, b_vec); // Check from most significant position in chunk + // A @Vector cannot be indexed at runtime (zig 0.16); coerce to + // an array first. + const cmp_arr: [32]i8 = cmp; var pos: usize = 32; while (pos > 0) { pos -= 1; - if (cmp[pos] != 0) { - return cmp[pos]; + if (cmp_arr[pos] != 0) { + return cmp_arr[pos]; } } } diff --git a/src/vm/jit_arm64.zig b/src/vm/jit_arm64.zig index 84949d3..bdbb096 100644 --- a/src/vm/jit_arm64.zig +++ b/src/vm/jit_arm64.zig @@ -7,6 +7,32 @@ // φ² + 1/φ² = 3 const std = @import("std"); + +/// PROT flags are decl-constants on 0.15.x and a packed struct on 0.16; +/// mprotect left std.posix in 0.16. Both paths compile because the condition +/// is comptime-known and Zig skips the untaken branch. +const zig_016_mem = @import("builtin").zig_version.major == 0 and + @import("builtin").zig_version.minor >= 16; + +/// zig 0.15.x and 0.16 disagree on several std APIs this file uses. The +/// repository declares minimum_zig_version 0.15.0 and CI runs 0.15.2, while +/// development happens on 0.16 — so both must compile. Zig does not analyse +/// the untaken branch of a comptime-known `if`, which is what makes this work. +const zig_016 = @import("builtin").zig_version.major == 0 and + @import("builtin").zig_version.minor >= 16; + +/// Monotonic nanoseconds. std.time.Timer/nanoTimestamp left std by 0.16. +fn monotonicNs() u64 { + if (comptime zig_016) { + var ts: std.c.timespec = undefined; + _ = std.c.clock_gettime(.MONOTONIC, &ts); + return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec)); + } else { + return @intCast(std.time.nanoTimestamp()); + } +} + + const builtin = @import("builtin"); // ═══════════════════════════════════════════════════════════════════════════════ @@ -26,7 +52,7 @@ pub const Arm64JitCompiler = struct { pub fn init(allocator: std.mem.Allocator) Self { return Self{ - .code = .{}, + .code = .empty, .allocator = allocator, }; } @@ -1426,7 +1452,7 @@ pub const Arm64JitCompiler = struct { const mem = try std.posix.mmap( null, alloc_size, - std.posix.PROT.READ | std.posix.PROT.WRITE, + if (comptime zig_016_mem) .{ .READ = true, .WRITE = true } else std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, -1, 0, @@ -1434,7 +1460,12 @@ pub const Arm64JitCompiler = struct { @memcpy(mem[0..code_size], self.code.items); - try std.posix.mprotect(mem, std.posix.PROT.READ | std.posix.PROT.EXEC); + if (comptime zig_016_mem) { + if (std.c.mprotect(@ptrCast(mem.ptr), mem.len, .{ .READ = true, .EXEC = true }) != 0) + return error.MprotectFailed; + } else { + try std.posix.mprotect(mem, std.posix.PROT.READ | std.posix.PROT.EXEC); + } self.exec_mem = mem; @@ -1642,20 +1673,20 @@ test "ARM64 NEON SIMD benchmark vs scalar" { const simd_func = try simd_compiler.finalize(); // Benchmark scalar - var timer = try std.time.Timer.start(); + var t_mark = monotonicNs(); var scalar_result: i64 = 0; for (0..iterations) |_| { scalar_result = scalar_func(@ptrCast(&a), @ptrCast(&b)); } - const scalar_ns = timer.read(); + const scalar_ns = monotonicNs() - t_mark; // Benchmark SIMD - timer.reset(); + t_mark = monotonicNs(); var simd_result: i64 = 0; for (0..iterations) |_| { simd_result = simd_func(@ptrCast(&a), @ptrCast(&b)); } - const simd_ns = timer.read(); + const simd_ns = monotonicNs() - t_mark; // Verify results match try std.testing.expectEqual(scalar_result, simd_result); @@ -1832,20 +1863,20 @@ test "ARM64 hybrid benchmark vs pure scalar" { const hybrid_func = try hybrid_compiler.finalize(); // Benchmark scalar - var timer = try std.time.Timer.start(); + var t_mark = monotonicNs(); var scalar_result: i64 = 0; for (0..iterations) |_| { scalar_result = scalar_func(@ptrCast(&a), @ptrCast(&b)); } - const scalar_ns = timer.read(); + const scalar_ns = monotonicNs() - t_mark; // Benchmark hybrid - timer.reset(); + t_mark = monotonicNs(); var hybrid_result: i64 = 0; for (0..iterations) |_| { hybrid_result = hybrid_func(@ptrCast(&a), @ptrCast(&b)); } - const hybrid_ns = timer.read(); + const hybrid_ns = monotonicNs() - t_mark; // Verify results match try std.testing.expectEqual(scalar_result, hybrid_result); @@ -2015,7 +2046,7 @@ test "ARM64 SIMD bind benchmark vs scalar" { const scalar_func = try scalar_compiler.finalize(); // Benchmark SIMD - var timer = try std.time.Timer.start(); + var t_mark = monotonicNs(); for (0..iterations) |_| { // Reset a for fair comparison for (0..dim) |i| { @@ -2023,17 +2054,17 @@ test "ARM64 SIMD bind benchmark vs scalar" { } _ = simd_func(@ptrCast(&a_simd), @ptrCast(&b)); } - const simd_ns = timer.read(); + const simd_ns = monotonicNs() - t_mark; // Benchmark scalar - timer.reset(); + t_mark = monotonicNs(); for (0..iterations) |_| { for (0..dim) |i| { a_scalar[i] = @intCast(@as(i32, @intCast(i % 3)) - 1); } _ = scalar_func(@ptrCast(&a_scalar), @ptrCast(&b)); } - const scalar_ns = timer.read(); + const scalar_ns = monotonicNs() - t_mark; const simd_ms = @as(f64, @floatFromInt(simd_ns)) / 1_000_000.0; const scalar_ms = @as(f64, @floatFromInt(scalar_ns)) / 1_000_000.0; @@ -2106,16 +2137,16 @@ test "ARM64 fused cosine benchmark vs 3x dot" { } // Benchmark fused - var timer = try std.time.Timer.start(); + var t_mark = monotonicNs(); var fused_result: f64 = 0; for (0..iterations) |_| { const bits = fused_func(@ptrCast(&a), @ptrCast(&b)); fused_result = @bitCast(bits); } - const fused_ns = timer.read(); + const fused_ns = monotonicNs() - t_mark; // Benchmark 3x dot - timer.reset(); + t_mark = monotonicNs(); var dot_result: f64 = 0; for (0..iterations) |_| { const dot_ab = dot_func(@ptrCast(&a), @ptrCast(&b)); @@ -2124,7 +2155,7 @@ test "ARM64 fused cosine benchmark vs 3x dot" { const norm = @sqrt(@as(f64, @floatFromInt(dot_aa)) * @as(f64, @floatFromInt(dot_bb))); dot_result = @as(f64, @floatFromInt(dot_ab)) / norm; } - const dot_ns = timer.read(); + const dot_ns = monotonicNs() - t_mark; const fused_ms = @as(f64, @floatFromInt(fused_ns)) / 1_000_000.0; const dot_ms = @as(f64, @floatFromInt(dot_ns)) / 1_000_000.0; diff --git a/src/vm/jit_unified.zig b/src/vm/jit_unified.zig index 45ac5c6..8cfed38 100644 --- a/src/vm/jit_unified.zig +++ b/src/vm/jit_unified.zig @@ -7,6 +7,26 @@ // φ² + 1/φ² = 3 const std = @import("std"); + +/// zig 0.15.x and 0.16 disagree on several std APIs this file uses. The +/// repository declares minimum_zig_version 0.15.0 and CI runs 0.15.2, while +/// development happens on 0.16 — so both must compile. Zig does not analyse +/// the untaken branch of a comptime-known `if`, which is what makes this work. +const zig_016 = @import("builtin").zig_version.major == 0 and + @import("builtin").zig_version.minor >= 16; + +/// Monotonic nanoseconds. std.time.Timer/nanoTimestamp left std by 0.16. +fn monotonicNs() u64 { + if (comptime zig_016) { + var ts: std.c.timespec = undefined; + _ = std.c.clock_gettime(.MONOTONIC, &ts); + return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec)); + } else { + return @intCast(std.time.nanoTimestamp()); + } +} + + const builtin = @import("builtin"); // Import architecture-specific backends @@ -409,12 +429,12 @@ test "Unified JIT benchmark" { b[i] = @intCast(@as(i32, @intCast((i + 1) % 3)) - 1); } - var timer = try std.time.Timer.start(); + const t_mark = monotonicNs(); var result: i64 = 0; for (0..iterations) |_| { result = func(@ptrCast(&a), @ptrCast(&b)); } - const ns = timer.read(); + const ns = monotonicNs() - t_mark; const ms = @as(f64, @floatFromInt(ns)) / 1_000_000.0; const ns_per_iter = @as(f64, @floatFromInt(ns)) / @as(f64, iterations); diff --git a/src/vm/jit_x86_64.zig b/src/vm/jit_x86_64.zig index 104242a..e606d4f 100644 --- a/src/vm/jit_x86_64.zig +++ b/src/vm/jit_x86_64.zig @@ -7,6 +7,12 @@ // φ² + 1/φ² = 3 const std = @import("std"); + +/// PROT flags are decl-constants on 0.15.x and a packed struct on 0.16; +/// mprotect left std.posix in 0.16. Both paths compile because the condition +/// is comptime-known and Zig skips the untaken branch. +const zig_016_mem = @import("builtin").zig_version.major == 0 and + @import("builtin").zig_version.minor >= 16; const builtin = @import("builtin"); // ═══════════════════════════════════════════════════════════════════════════════ @@ -26,7 +32,7 @@ pub const X86_64JitCompiler = struct { pub fn init(allocator: std.mem.Allocator) Self { return Self{ - .code = .{}, + .code = .empty, .allocator = allocator, }; } @@ -354,7 +360,7 @@ pub const X86_64JitCompiler = struct { const mem = try std.posix.mmap( null, alloc_size, - std.posix.PROT.READ | std.posix.PROT.WRITE, + if (comptime zig_016_mem) .{ .READ = true, .WRITE = true } else std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, -1, 0, @@ -364,7 +370,12 @@ pub const X86_64JitCompiler = struct { @memcpy(mem[0..code_size], self.code.items); // Change to PROT_READ | PROT_EXEC - try std.posix.mprotect(mem, std.posix.PROT.READ | std.posix.PROT.EXEC); + if (comptime zig_016_mem) { + if (std.c.mprotect(@ptrCast(mem.ptr), mem.len, .{ .READ = true, .EXEC = true }) != 0) + return error.MprotectFailed; + } else { + try std.posix.mprotect(mem, std.posix.PROT.READ | std.posix.PROT.EXEC); + } self.exec_mem = mem; diff --git a/src/vm/vm.zig b/src/vm/vm.zig index b5b48a4..a6759cc 100644 --- a/src/vm/vm.zig +++ b/src/vm/vm.zig @@ -3,7 +3,7 @@ // ⲤⲀⲔⲢⲀ ⲪⲞⲢⲘⲨⲖⲀ: V = n × 3^k × π^m × φ^p × e^q const std = @import("std"); -const tvc_hybrid = @import("hybrid.zig"); +const tvc_hybrid = @import("../ternary/hybrid.zig"); const tvc_vsa = @import("vsa.zig"); const gf = @import("golden-float"); diff --git a/src/vsa/10k_vsa.zig b/src/vsa/10k_vsa.zig index cbbe2ff..b5f1d8e 100644 --- a/src/vsa/10k_vsa.zig +++ b/src/vsa/10k_vsa.zig @@ -12,6 +12,24 @@ // ╚════════════════════════════════════════════════════════════════════════════╝ const std = @import("std"); + +/// zig 0.15.x and 0.16 disagree on several std APIs this file uses. The +/// repository declares minimum_zig_version 0.15.0 and CI runs 0.15.2, while +/// development happens on 0.16 — so both must compile. Zig does not analyse +/// the untaken branch of a comptime-known `if`, which is what makes this work. +const zig_016 = @import("builtin").zig_version.major == 0 and + @import("builtin").zig_version.minor >= 16; + +/// Monotonic nanoseconds. std.time.Timer/nanoTimestamp left std by 0.16. +fn monotonicNs() u64 { + if (comptime zig_016) { + var ts: std.c.timespec = undefined; + _ = std.c.clock_gettime(.MONOTONIC, &ts); + return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec)); + } else { + return @intCast(std.time.nanoTimestamp()); + } +} const builtin = @import("builtin"); const common = @import("common.zig"); @@ -274,9 +292,12 @@ pub const BenchmarkResult = struct { dimensions: usize = DIM_10K, }; + /// Run 10K VSA benchmark pub fn benchmark(_: std.mem.Allocator, iterations: usize) !BenchmarkResult { - var rng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp())); + // std.time.timestamp left std by zig 0.16; a fixed seed also makes the + // benchmark deterministic, like every other test in this repository. + var rng = std.Random.DefaultPrng.init(0x10AD5EED); // Create test vectors const vec_a = try HyperVector10K.random(&rng); @@ -288,30 +309,30 @@ pub fn benchmark(_: std.mem.Allocator, iterations: usize) !BenchmarkResult { _ = try HyperVector10K.cosineSimilarity(&vec_a, &vec_b); // Benchmark bind - const bind_start = std.time.nanoTimestamp(); + const bind_start = monotonicNs(); var i: usize = 0; while (i < iterations) : (i += 1) { _ = HyperVector10K.bind(&vec_a, &vec_b); } - const bind_end = std.time.nanoTimestamp(); + const bind_end = monotonicNs(); const bind_ns = @as(f64, @floatFromInt(bind_end - bind_start)) / @as(f64, @floatFromInt(iterations)); // Benchmark bundle - const bundle_start = std.time.nanoTimestamp(); + const bundle_start = monotonicNs(); i = 0; while (i < iterations) : (i += 1) { _ = try HyperVector10K.bundle(&vec_a, &vec_b); } - const bundle_end = std.time.nanoTimestamp(); + const bundle_end = monotonicNs(); const bundle_ns = @as(f64, @floatFromInt(bundle_end - bundle_start)) / @as(f64, @floatFromInt(iterations)); // Benchmark similarity - const sim_start = std.time.nanoTimestamp(); + const sim_start = monotonicNs(); i = 0; while (i < iterations) : (i += 1) { _ = try HyperVector10K.cosineSimilarity(&vec_a, &vec_b); } - const sim_end = std.time.nanoTimestamp(); + const sim_end = monotonicNs(); const sim_ns = @as(f64, @floatFromInt(sim_end - sim_start)) / @as(f64, @floatFromInt(iterations)); return BenchmarkResult{ diff --git a/src/vsa/gen_core.zig b/src/vsa/gen_core.zig index 12d27fc..e569b57 100644 --- a/src/vsa/gen_core.zig +++ b/src/vsa/gen_core.zig @@ -5,7 +5,7 @@ // φ² + 1/φ² = 3 | TRINITY const std = @import("std"); -const hybrid = @import("hybrid.zig"); +const hybrid = @import("../ternary/hybrid.zig"); const HybridBigInt = hybrid.HybridBigInt; const Trit = hybrid.Trit; const Vec32i8 = hybrid.Vec32i8; diff --git a/src/vsa/hrr.zig b/src/vsa/hrr.zig index 6457ed1..95c0c5a 100644 --- a/src/vsa/hrr.zig +++ b/src/vsa/hrr.zig @@ -23,7 +23,9 @@ const std = @import("std"); const math = std.math; const Allocator = std.mem.Allocator; -const random = std.crypto.random; +// std.crypto.random left the std namespace by zig 0.16; a seeded PRNG also +// matches the determinism the rest of this repository's tests rely on. +var hrr_prng = std.Random.DefaultPrng.init(0x48525244); /// ═══════════════════════════════════════════════════════════════════════════════ /// SACRED CONSTANTS FOR HRR @@ -73,8 +75,8 @@ pub const HRR = struct { var i: usize = 0; while (i < self.dim) : (i += 2) { // Generate uniform random floats in (0, 1] - const u1_raw: f32 = random.float(f32); - const u2_raw: f32 = random.float(f32); + const u1_raw: f32 = hrr_prng.random().float(f32); + const u2_raw: f32 = hrr_prng.random().float(f32); // Avoid log(0) and ensure valid range const u1_safe = if (u1_raw <= 0.0) 1.0e-6 else if (u1_raw >= 1.0) 0.999999 else u1_raw; diff --git a/src/vsa/packed_vsa.zig b/src/vsa/packed_vsa.zig index dcdcd6f..7f656b8 100644 --- a/src/vsa/packed_vsa.zig +++ b/src/vsa/packed_vsa.zig @@ -8,6 +8,24 @@ // φ² + 1/φ² = 3 const std = @import("std"); + +/// zig 0.15.x and 0.16 disagree on several std APIs this file uses. The +/// repository declares minimum_zig_version 0.15.0 and CI runs 0.15.2, while +/// development happens on 0.16 — so both must compile. Zig does not analyse +/// the untaken branch of a comptime-known `if`, which is what makes this work. +const zig_016 = @import("builtin").zig_version.major == 0 and + @import("builtin").zig_version.minor >= 16; + +/// Monotonic nanoseconds. std.time.Timer/nanoTimestamp left std by 0.16. +fn monotonicNs() u64 { + if (comptime zig_016) { + var ts: std.c.timespec = undefined; + _ = std.c.clock_gettime(.MONOTONIC, &ts); + return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec)); + } else { + return @intCast(std.time.nanoTimestamp()); + } +} const packed_trit = @import("../ternary/packed_trit.zig"); const hybrid = @import("../ternary/hybrid.zig"); // There is no vsa.zig; bind, bundle2 and randomVector are all in the @@ -365,6 +383,7 @@ test "packed unbind retrieval" { // from a knowledge-graph consumer. The three tests below used Entity only // for djb2 over a string, to derive a seed. That function is reproduced // here verbatim so the seeds — and therefore the tests — are unchanged. + fn hashString(s: []const u8) u64 { var hash: u64 = 5381; for (s) |c| { @@ -461,20 +480,20 @@ test "benchmark Packed vs Unpacked" { const p_b = fromHybrid(&h_b); // Benchmark Unpacked (vsa.bind) - var timer = std.time.Timer.start() catch unreachable; + const t0 = monotonicNs(); for (0..iterations) |_| { const result = vsa.bind(&h_a, &h_b); std.mem.doNotOptimizeAway(&result); } - const unpacked_ns = timer.read(); + const unpacked_ns = monotonicNs() - t0; // Benchmark Packed - timer.reset(); + const t1 = monotonicNs(); for (0..iterations) |_| { const result = packedBind(&p_a, &p_b); std.mem.doNotOptimizeAway(&result); } - const packed_ns = timer.read(); + const packed_ns = monotonicNs() - t1; const unpacked_us = @as(f64, @floatFromInt(unpacked_ns)) / 1000.0 / @as(f64, @floatFromInt(iterations)); const packed_us = @as(f64, @floatFromInt(packed_ns)) / 1000.0 / @as(f64, @floatFromInt(iterations)); diff --git a/src/vsa_jit.zig b/src/vsa_jit.zig index 819cb7c..492780d 100644 --- a/src/vsa_jit.zig +++ b/src/vsa_jit.zig @@ -7,6 +7,26 @@ // φ² + 1/φ² = 3 const std = @import("std"); + +/// zig 0.15.x and 0.16 disagree on several std APIs this file uses. The +/// repository declares minimum_zig_version 0.15.0 and CI runs 0.15.2, while +/// development happens on 0.16 — so both must compile. Zig does not analyse +/// the untaken branch of a comptime-known `if`, which is what makes this work. +const zig_016 = @import("builtin").zig_version.major == 0 and + @import("builtin").zig_version.minor >= 16; + +/// Monotonic nanoseconds. std.time.Timer/nanoTimestamp left std by 0.16. +fn monotonicNs() u64 { + if (comptime zig_016) { + var ts: std.c.timespec = undefined; + _ = std.c.clock_gettime(.MONOTONIC, &ts); + return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec)); + } else { + return @intCast(std.time.nanoTimestamp()); + } +} + + const builtin = @import("builtin"); const jit_unified = @import("vm/jit_unified.zig"); const hybrid = @import("ternary/hybrid.zig"); @@ -48,7 +68,7 @@ pub const JitVSAEngine = struct { .hamming_cache = std.AutoHashMap(usize, jit_unified.JitDotFn).init(allocator), .cosine_cache = std.AutoHashMap(usize, jit_unified.JitDotFn).init(allocator), .bundle_cache = std.AutoHashMap(usize, jit_unified.JitDotFn).init(allocator), - .compilers = .{}, + .compilers = .empty, }; } @@ -517,20 +537,20 @@ test "JitVSAEngine benchmark vs fallback" { _ = try engine.dotProduct(&a, &b); // Benchmark JIT - var timer = try std.time.Timer.start(); + var t_mark = monotonicNs(); var jit_result: i64 = 0; for (0..iterations) |_| { jit_result = try engine.dotProduct(&a, &b); } - const jit_ns = timer.read(); + const jit_ns = monotonicNs() - t_mark; // Benchmark fallback - timer.reset(); + t_mark = monotonicNs(); var fallback_result: i64 = 0; for (0..iterations) |_| { fallback_result = JitVSAEngine.dotProductFallback(&a, &b); } - const fallback_ns = timer.read(); + const fallback_ns = monotonicNs() - t_mark; // Results should match try std.testing.expectEqual(jit_result, fallback_result);