From 68c09a9d1d19c015e5759dcac3be5865c8a6bc7b Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Sat, 8 Aug 2026 02:26:07 +0700 Subject: [PATCH] fix(gf16): reconcile production codec to standard (1+M/512), reclaim mantissa bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit golden_float16.GF16 (the production gf16_* C-ABI codec used by every language binding + FPGA path) normalized to [0.5, 1.0) and stored (mant_f - 0.5)*512, so the top half of the 9-bit mantissa field (256..511) was never produced — ~8 effective mantissa bits instead of 9. It also diverged from every other GF16 in the repo (1.0 -> 0x4000 vs the standard 0x3E00): specs/gf16.tri from_f32_steps, docs/spec-gf16.md, gf_binary.GF16, and formats_root.f32ToGf16 all use the standard implicit-leading-1 form. specs/ops.tri was the only spec agreeing with the wasteful form, contradicting specs/gf16.tri. Owner decision: fix to the standard (1 + M/512)·2^(E-31) normalization. - golden_float16.GF16.fromF32/toF32 now delegate to gf_binary.GF16 — one codec, bit-identical by construction (guards against future re-drift). - Add gf16 exact-bit vectors to testdata/gf_conformance.csv (was excluded) and to gf_binary's encoding-regression guard; add reconciliation + exact-bit tests in golden_float16.zig. - Fix specs/ops.tri (0.5+M/512 -> 1+M/512) to match specs/gf16.tri; fix the bogus IEEE-f16 copy-paste test_vectors in gf16.tri; fix the value formula in src/c/gf16.h. - Reseal specs/TRI-HASHES.md for gf16.tri and ops.tri (shasum). WIRE FORMAT CHANGE: gf16_* now emits standard bits (1.0: 0x4000 -> 0x3E00). No RTL exists in-repo and FPGA docs already describe [1:6:9] b31, so this aligns the codec with the documented hardware convention. Serialized gf16 data matching the old output is incompatible — warrants a format-version bump on release. Verified: zig build + zig build test pass; the built libgoldenfloat.dylib emits every gf16 CSV row exactly via ctypes. Co-Authored-By: Claude Opus 4.8 --- specs/TRI-HASHES.md | 4 +- specs/gf16.tri | 10 ++--- specs/ops.tri | 4 +- src/c/gf16.h | 2 +- src/formats/gf_binary.zig | 5 +++ src/formats/golden_float16.zig | 82 ++++++++++++++++------------------ testdata/gf_conformance.csv | 7 +++ 7 files changed, 61 insertions(+), 53 deletions(-) diff --git a/specs/TRI-HASHES.md b/specs/TRI-HASHES.md index f56b792..868de6c 100644 --- a/specs/TRI-HASHES.md +++ b/specs/TRI-HASHES.md @@ -8,9 +8,9 @@ | Gene | Level | File | Version | SHA256 | Last Modified | |------|-------|------|---------|--------|---------------| -| **GF16** | 1 | `specs/gf16.tri` | 1 | `23f2353746e083ad236cd348ddc4152d5976cbbc20d35d23b27c5300ef07e70a` | 2026-03-31 | +| **GF16** | 1 | `specs/gf16.tri` | 1 | `ac35405ca9cb0b7389e21ce920392a379321b2716e667d65e47a31bbceaf7dc2` | 2026-08-08 | | **TF3** | 2 | `specs/tf3.tri` | 1 | `b482b1f829a8c856022077a26fd6bafa864eecda4c696da058083cea52a8e19c` | 2026-03-31 | -| **OPS** | 1 | `specs/ops.tri` | 1 | `750e57342e5ea8b52135c9cc36d9a70becba532566160be1dc825ffb5571ff95` | 2026-03-31 | +| **OPS** | 1 | `specs/ops.tri` | 1 | `43ccb0bda4d75de1edcdfc78e0d35a367b2902936a56aac3d344548a8f38702f` | 2026-08-08 | ## Hash Calculation diff --git a/specs/gf16.tri b/specs/gf16.tri index b911092..9e81080 100644 --- a/specs/gf16.tri +++ b/specs/gf16.tri @@ -152,13 +152,13 @@ test_vectors: raw_hex: "0000" - name: one f32: 1.0 - raw_hex: "3c00" + raw_hex: "3e00" - name: minus_one f32: -1.0 - raw_hex: "bc00" + raw_hex: "be00" - name: pi f32: 3.1415927 - raw_hex: "4248" + raw_hex: "4124" - name: max_pos - f32: 4.3e9 - raw_hex: "7bff" + f32: 4.29e9 + raw_hex: "7dff" diff --git a/specs/ops.tri b/specs/ops.tri index 1b636cc..679ab8f 100644 --- a/specs/ops.tri +++ b/specs/ops.tri @@ -20,7 +20,7 @@ gf16: intermediate_type: f32 steps: - extract_sign_bit - - normalize_to_0_5_to_1_range + - normalize_to_1_to_2_range - compute_exponent_with_bias_31 - round_mantissa_to_9_bits - pack_into_u16 @@ -34,7 +34,7 @@ gf16: - unpack_fields - check_special_cases - unbiased_exponent = exp - 31 - - mantissa_scaled = mant / 512 + 0.5 + - mantissa_scaled = 1 + mant / 512 - value = mantissa_scaled * 2^unbiased_exponent - apply_sign diff --git a/src/c/gf16.h b/src/c/gf16.h index d21c142..1052ab9 100644 --- a/src/c/gf16.h +++ b/src/c/gf16.h @@ -47,7 +47,7 @@ extern "C" { * [8:0] Mantissa (9 bits, fractional part) * * **Value Formula:** - * value = (-1)^sign × (0.5 + mant/512) × 2^(exp - 31) + * value = (-1)^sign × (1 + mant/512) × 2^(exp - 31) * * **Special Values:** * - exp=0, mant=0: Zero (signed by sign bit) diff --git a/src/formats/gf_binary.zig b/src/formats/gf_binary.zig index 55dd2e8..4a80b4b 100644 --- a/src/formats/gf_binary.zig +++ b/src/formats/gf_binary.zig @@ -252,6 +252,11 @@ test "GF ladder: exact-bit golden vectors (encoding regression guard)" { try E(@as(GF12.Repr, 0x3C0), GF12.fromF32(1.5).bits_()); try E(@as(GF12.Repr, 0x400), GF12.fromF32(2.0).bits_()); try E(@as(GF12.Repr, 0xC20), GF12.fromF32(-2.5).bits_()); + // gf16 [1:6:9] b31 — the primary production rung (shared with golden_float16.GF16) + try E(@as(GF16.Repr, 0x3E00), GF16.fromF32(1.0).bits_()); + try E(@as(GF16.Repr, 0x3F00), GF16.fromF32(1.5).bits_()); + try E(@as(GF16.Repr, 0x4000), GF16.fromF32(2.0).bits_()); + try E(@as(GF16.Repr, 0xC080), GF16.fromF32(-2.5).bits_()); // gf20 [1:7:12] b63 try E(@as(GF20.Repr, 0x3F000), GF20.fromF32(1.0).bits_()); try E(@as(GF20.Repr, 0x3F800), GF20.fromF32(1.5).bits_()); diff --git a/src/formats/golden_float16.zig b/src/formats/golden_float16.zig index d04d1fe..e793775 100644 --- a/src/formats/golden_float16.zig +++ b/src/formats/golden_float16.zig @@ -24,6 +24,7 @@ //! const std = @import("std"); +const gf_binary = @import("gf_binary.zig"); // ═════════════════════════════════════════════════════════════════════════ // TRINITY CONSTANTS @@ -84,53 +85,19 @@ pub const GF16 = packed struct(u16) { /// comptime calculation (0.049 for GF16) // pub const phi_distance: comptime_float = @import("std").math.fabs(6.0 / 9.0 - 1.0 / PHI); - /// Create GF16 from f32 with φ-optimized encoding + /// Create GF16 from f32. + /// + /// Delegates to the single normative codec `gf_binary.GF16` (the φ²-sized + /// binary rung factory) so GF16 has exactly ONE encoding across the repo: + /// the standard `(1 + M/512)·2^(E−31)` significand with the full 9-bit + /// mantissa (FORMAT-SPEC-001, specs/gf16.tri). e.g. 1.0 → 0x3E00. pub fn fromF32(v: f32) GF16 { - if (v == 0.0) return .{ .mant = 0, .exp = 0, .sign = 0 }; - - if (std.math.isNan(v)) { - return .{ .mant = 1, .exp = 0x3F, .sign = 0 }; - } - - if (std.math.isInf(v)) { - return .{ .mant = 0, .exp = 0x3F, .sign = @intFromBool(v < 0) }; - } - - const sign_bit: u1 = @intFromBool(v < 0); - const abs_v = @abs(v); - - // Find exponent (normalize to [0.5, 2)) - var exp: i8 = 0; - var mant_f = abs_v; - - while (mant_f >= 1.0 and exp < 31) : (exp += 1) mant_f /= 2.0; - while (mant_f < 0.5 and exp > -32) : (exp -= 1) mant_f *= 2.0; - - const exp_i8: i8 = exp; - const exp_bias: i8 = 31; - const exp_u6: u6 = @intCast(exp_bias + exp_i8); - const mant_u9: u9 = @intFromFloat((mant_f - 0.5) * 512.0); - - return .{ - .mant = @min(mant_u9, 511), - .exp = exp_u6, - .sign = sign_bit, - }; + return @bitCast(gf_binary.GF16.fromF32(v).bits_()); } - /// Convert GF16 to f32 + /// Convert GF16 to f32 (via the same normative `gf_binary.GF16` codec). pub fn toF32(self: GF16) f32 { - if (self.exp == 0 and self.mant == 0) { - return if (self.sign == 1) -0.0 else 0.0; - } - if (self.exp == 0x3F) { - return if (self.sign == 1) -std.math.inf(f32) else std.math.inf(f32); - } - - const exp_unbiased = @as(i32, self.exp) - 31; - const mant_f = 0.5 + @as(f32, @floatFromInt(self.mant)) / 512.0; - const value = mant_f * std.math.pow(f32, 2.0, @floatFromInt(exp_unbiased)); - return if (self.sign == 1) -value else value; + return gf_binary.GF16.fromBits(@bitCast(self)).toF32(); } /// GF16 addition (via f32 for precision) @@ -377,6 +344,35 @@ test "GF16 roundtrip negative" { } } +test "GF16 exact-bit encoding is the standard (1 + M/512) form" { + // Pin the wire format: 1.0 -> 0x3E00 (E=31, mantissa 0), NOT the old + // waste-a-bit 0x4000. Full 9-bit mantissa is now reachable. See specs/gf16.tri + // and testdata/gf_conformance.csv (gf16 rows). + const E = std.testing.expectEqual; + try E(@as(u16, 0x3E00), @as(u16, @bitCast(GF16.fromF32(1.0)))); + try E(@as(u16, 0x3F00), @as(u16, @bitCast(GF16.fromF32(1.5)))); + try E(@as(u16, 0x4000), @as(u16, @bitCast(GF16.fromF32(2.0)))); + try E(@as(u16, 0x4100), @as(u16, @bitCast(GF16.fromF32(3.0)))); + try E(@as(u16, 0x3C00), @as(u16, @bitCast(GF16.fromF32(0.5)))); + try E(@as(u16, 0xBE00), @as(u16, @bitCast(GF16.fromF32(-1.0)))); + try E(@as(u16, 0xC080), @as(u16, @bitCast(GF16.fromF32(-2.5)))); +} + +test "GF16 is bit-identical to the normative gf_binary.GF16 codec" { + // One implementation: golden_float16.GF16 delegates to gf_binary.GF16, so + // every value must encode to the exact same raw u16. Guards against re-drift. + const vals = [_]f32{ 0.0, 1.0, -1.0, 0.5, 1.5, 2.0, 3.0, -2.5, 3.14159, 100.0, 0.001, 12345.0, 1e30, -1e30 }; + for (vals) |v| { + const a: u16 = @bitCast(GF16.fromF32(v)); + const b: u16 = gf_binary.GF16.fromF32(v).bits_(); + try std.testing.expectEqual(b, a); + } + // NaN encodes consistently too (exp all-ones, mantissa != 0). + const na: u16 = @bitCast(GF16.fromF32(std.math.nan(f32))); + const nb: u16 = gf_binary.GF16.fromF32(std.math.nan(f32)).bits_(); + try std.testing.expectEqual(nb, na); +} + test "GF16 arithmetic" { const a = GF16.fromF32(1.5); const b = GF16.fromF32(2.5); diff --git a/testdata/gf_conformance.csv b/testdata/gf_conformance.csv index 371b723..ebefc96 100644 --- a/testdata/gf_conformance.csv +++ b/testdata/gf_conformance.csv @@ -17,6 +17,13 @@ gf12,3.0,0x440 gf12,0.5,0x300 gf12,-1.0,0xb80 gf12,-2.5,0xc20 +gf16,1.0,0x3e00 +gf16,1.5,0x3f00 +gf16,2.0,0x4000 +gf16,3.0,0x4100 +gf16,0.5,0x3c00 +gf16,-1.0,0xbe00 +gf16,-2.5,0xc080 gf20,1.0,0x3f000 gf20,1.5,0x3f800 gf20,2.0,0x40000