diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e191b69..5fd8c2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Zig - uses: goto-bus-stop/setup-zig@v2 + uses: mlugg/setup-zig@v2 with: version: 0.15.0 diff --git a/.github/workflows/test-bindings.yml b/.github/workflows/test-bindings.yml index dc112b5..0bfea6d 100644 --- a/.github/workflows/test-bindings.yml +++ b/.github/workflows/test-bindings.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v4 - name: Install Zig - uses: goto-bus/setup-zig@v2 + uses: mlugg/setup-zig@v2 with: version: 0.15.2 diff --git a/Cargo.toml b/Cargo.toml index 7672adb..522d7fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ readme = "README.md" reqwest = { version = "0.12", features = ["blocking"] } [dependencies] -[target.'cfg(unix(all(not(target_os = "windows")))'.dependencies] +[target.'cfg(unix)'.dependencies] libc = "0.2" [[bin]] diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index c6464ca..7a38c72 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -22,6 +22,7 @@ endif() # Include directory for header-only wrapper include_directories( + "${CMAKE_SOURCE_DIR}/include" "${CMAKE_SOURCE_DIR}/../src/c" "${CMAKE_SOURCE_DIR}/../../src/c" ) diff --git a/go/goldenfloat/gf16.go b/go/goldenfloat/gf16.go index afbab25..90ccc8c 100644 --- a/go/goldenfloat/gf16.go +++ b/go/goldenfloat/gf16.go @@ -8,6 +8,7 @@ package goldenfloat /* #cgo LDFLAGS: -L../../zig-out/lib -lgoldenfloat #cgo CFLAGS: -I../../src/c +#include "gf16.h" */ import "C" diff --git a/go/goldenfloat/gf16_test.go b/go/goldenfloat/gf16_test.go index 396f7c1..0f10e56 100644 --- a/go/goldenfloat/gf16_test.go +++ b/go/goldenfloat/gf16_test.go @@ -1,417 +1,190 @@ -// Package goldenfloat provides Go bindings for GoldenFloat GF16 format. -// -// MIT License — Copyright (c) 2026 Trinity Project -// Repository: https://github.com/gHashTag/zig-golden-float - package goldenfloat import ( - "encoding/json" - "fmt" - "os" - "testing" + "encoding/json" + "fmt" + "os" + "testing" ) -/* -#cgo LDFLAGS: -L../../zig-out/lib -lgoldenfloat -#cgo CFLAGS: -I../../include -*/ -import "C" - -// ============================================================================ -// Test Helpers -// ============================================================================ - func loadVectors() (map[string]interface{}, error) { - vectorsPath := "../../conformance/vectors.json" - file, err := os.ReadFile(vectorsPath) - if err != nil { - return nil, fmt.Errorf("failed to read vectors.json: %w", err) - } - var data map[string]interface{} - err = json.Unmarshal(file, &data) - if err != nil { - return nil, fmt.Errorf("failed to parse vectors.json: %w", err) - } - vectors := data["vectors"].(map[string]interface{}) - return vectors, nil + vectorsPath := "../../conformance/vectors.json" + file, err := os.ReadFile(vectorsPath) + if err != nil { + return nil, fmt.Errorf("failed to read vectors.json: %w", err) + } + var data map[string]interface{} + err = json.Unmarshal(file, &data) + if err != nil { + return nil, fmt.Errorf("failed to parse vectors.json: %w", err) + } + vectors := data["vectors"].(map[string]interface{}) + return vectors, nil } func approxEqual(a, b, tolerance float32) bool { - // Check for inf/nan - if a == b { - return true - } - // Use relative error for finite values - diff := a - b - if diff < 0 { - diff = -diff - } - return diff <= tolerance || diff >= -tolerance + if a == b { + return true + } + diff := a - b + if diff < 0 { + diff = -diff + } + return diff <= tolerance } -// ============================================================================ -// Conversion Tests -// ============================================================================ - -func testConversions(t *testing.T) bool { - vectors, err := loadVectors() - if err != nil { - t.Fatal(err) - } - - conversions := vectors["conversions"].([]interface{}) - passed := 0 - failed := 0 - - for _, test := range conversions { - tc := test.(map[string]interface{}) - name := tc["name"].(string) - inputStr := tc["input"].(string) - - // Parse input - var inputVal float64 - switch inputStr { - case "inf": - inputVal = float64(1) - // In Go, positive infinity - inputVal /= 0 // Actually just set to +inf - inputVal *= float64(1) / float64(0) // But this gives NaN, so: - inputVal = float64(0x7FF0000000000000) - case "-inf": - inputVal = float64(1) - inputVal /= 0 - inputVal *= float64(1) / float64(0) - inputVal = float64(0xFFF00000000000000) - case "nan": - inputVal = float64(0x7FF8000000000000) - default: - var ok bool - inputVal, ok = tc["input"].(float64) - if !ok { - t.Fatalf("invalid input value for %s", name) - } - } - - gf := FromF32(float32(inputVal)) - back := gf.ToF32() - - var result bool - var expected bool - - if predicate, ok := tc["predicate"]; ok { - result = false // Default - expected = true - if predicate == "is_inf" { - result = gf.IsInf() - } else if predicate == "is_nan" { - result = gf.IsNaN() - } - } else if match, ok := tc["match"]; ok { - matchType := match.(string) - if matchType == "roundtrip" { - if inputStr == "inf" || inputStr == "-inf" || inputStr == "nan" { - result = true // Special values don't need roundtrip - } else { - // Allow 1% tolerance - relError := (back - inputVal) / inputVal - if inputVal == 0 { - result = (back == 0) || (back == inputVal) - } else { - result = relError <= 0.01 - } - } - expected = true - } else if matchType == "is_nan" { - result = gf.IsNaN() - expected = true - } else if matchType == "approximate" { - result = true // Just check conversion succeeds - expected = true - } else { - result = false - expected = true - } - } - - if result == expected { - passed++ - } else { - failed++ - t.Errorf("FAIL: %s - input=%s, got=%v, expected=%v", name, inputStr, back, expected) - } - } - - t.Logf("Conversions: %d/%d passed", passed, passed+failed) - return failed == 0 +func TestConversions(t *testing.T) { + vectors, err := loadVectors() + if err != nil { + t.Fatal(err) + } + + conversions := vectors["conversions"].([]interface{}) + for _, test := range conversions { + tc := test.(map[string]interface{}) + name := tc["name"].(string) + + gf := FromF32(float32(tc["input"].(float64))) + back := gf.ToF32() + + if predicate, ok := tc["predicate"]; ok { + var result bool + if predicate == "is_inf" { + result = gf.IsInf() + } else if predicate == "is_nan" { + result = gf.IsNaN() + } + if !result { + t.Errorf("FAIL: %s - predicate=%v not satisfied", name, predicate) + } + } else if match, ok := tc["match"]; ok { + matchType := match.(string) + switch matchType { + case "roundtrip": + inputVal := float32(tc["input"].(float64)) + if !approxEqual(back, inputVal, 0.01) && back != 0 { + t.Errorf("FAIL: %s - roundtrip got %v", name, back) + } + case "is_nan": + if !gf.IsNaN() { + t.Errorf("FAIL: %s - expected NaN", name) + } + case "approximate": + } + } + } } -// ============================================================================ -// Arithmetic Tests -// ============================================================================ - -func testArithmetic(t *testing.T) bool { - vectors, err := loadVectors() - if err != nil { - t.Fatal(err) - } - - arithmetic := vectors["arithmetic"].([]interface{}) - passed := 0 - failed := 0 - - for _, test := range arithmetic { - tc := test.(map[string]interface{}) - name := tc["name"].(string) - - a := FromF32(float32(tc["a"].(float64))) - b := FromF32(float32(tc["b"].(float64))) - expected := float32(tc["expected"].(float64)) - tolerance := float32(tc["tolerance"].(float64)) - - op := tc["op"].(string) - var result float32 - - switch op { - case "add": - result = (a + b).ToF32() - case "sub": - result = (a.Sub(b)).ToF32() - case "mul": - result = (a.Mul(b)).ToF32() - case "div": - result = (a.Div(b)).ToF32() - default: - t.Errorf("unknown op: %s", op) - failed++ - continue - } - - if approxEqual(result, expected, tolerance) { - passed++ - } else { - failed++ - t.Errorf("FAIL: %s - got %v, expected %v ±%v", name, result, expected, tolerance) - } - } - - t.Logf("Arithmetic: %d/%d passed", passed, passed+failed) - return failed == 0 -} - -// ============================================================================ -// Predicate Tests -// ============================================================================ - -func testPredicates(t *testing.T) bool { - vectors, err := loadVectors() - if err != nil { - t.Fatal(err) - } - - predicates := vectors["predicates"].([]interface{}) - passed := 0 - failed := 0 - - for _, test := range predicates { - tc := test.(map[string]interface{}) - name := tc["name"].(string) - - inputStr := tc["input"].(string) - var inputVal float64 - switch inputStr { - case "inf": - inputVal = float64(1) - inputVal /= 0 - inputVal *= float64(1) / float64(0) - inputVal = float64(0x7FF0000000000000) - case "-inf": - inputVal = float64(1) - inputVal /= 0 - inputVal *= float64(1) / float64(0) - inputVal = float64(0xFFF00000000000000) - case "nan": - inputVal = float64(0x7FF8000000000000) - default: - var ok bool - inputVal, ok = tc["input"].(float64) - if !ok { - t.Fatalf("invalid input value for %s", name) - } - } - - gf := FromF32(float32(inputVal)) - predicate := tc["predicate"].(string) - expected := tc["expected"].(bool) - - var result bool - switch predicate { - case "is_zero": - result = gf.IsZero() - case "is_nan": - result = gf.IsNaN() - case "is_inf": - result = gf.IsInf() - case "is_negative": - result = gf.IsNegative() - default: - t.Errorf("unknown predicate: %s", predicate) - failed++ - continue - } - - if result == expected { - passed++ - } else { - failed++ - t.Errorf("FAIL: %s - predicate=%s returned %v, expected %v", name, predicate, result, expected) - } - } - - t.Logf("Predicates: %d/%d passed", passed, passed+failed) - return failed == 0 +func TestArithmetic(t *testing.T) { + vectors, err := loadVectors() + if err != nil { + t.Fatal(err) + } + + arithmetic := vectors["arithmetic"].([]interface{}) + for _, test := range arithmetic { + tc := test.(map[string]interface{}) + name := tc["name"].(string) + + a := FromF32(float32(tc["a"].(float64))) + b := FromF32(float32(tc["b"].(float64))) + expected := float32(tc["expected"].(float64)) + tolerance := float32(tc["tolerance"].(float64)) + + op := tc["op"].(string) + var result float32 + + switch op { + case "add": + result = a.Add(b).ToF32() + case "sub": + result = a.Sub(b).ToF32() + case "mul": + result = a.Mul(b).ToF32() + case "div": + result = a.Div(b).ToF32() + default: + t.Errorf("unknown op: %s", op) + continue + } + + if !approxEqual(result, expected, tolerance) { + t.Errorf("FAIL: %s - got %v, expected %v +/- %v", name, result, expected, tolerance) + } + } } -// ============================================================================ -// phi-Math Tests -// ============================================================================ - -func testPhiMath(t *testing.T) bool { - passed := 0 - failed := 0 - - // Test phi constant - phi := Phi() - if approxEqual(float32(phi), 1.6180339887498948, 1e-10) { - passed++ - } else { - failed++ - t.Errorf("FAIL: phi - got %v, expected 1.6180339887498948", phi) - } - - // Test phi_sq - phiSq := PhiSq() - if approxEqual(float32(phiSq), 2.6180339887498948, 1e-10) { - passed++ - } else { - failed++ - t.Errorf("FAIL: phi_sq - got %v, expected 2.6180339887498948", phiSq) - } - - // Test phi_inv_sq - phiInvSq := PhiInvSq() - if approxEqual(float32(phiInvSq), 0.3819660112501051, 1e-10) { - passed++ - } else { - failed++ - t.Errorf("FAIL: phi_inv_sq - got %v, expected 0.3819660112501051", phiInvSq) - } - - // Test trinity - trinity := Trinity() - if approxEqual(float32(trinity), 3.0, 1e-10) { - passed++ - } else { - failed++ - t.Errorf("FAIL: trinity - got %v, expected 3.0", trinity) - } - - t.Logf("phi-Math: %d/%d passed", passed, passed+failed) - return failed == 0 +func TestPredicates(t *testing.T) { + vectors, err := loadVectors() + if err != nil { + t.Fatal(err) + } + + predicates := vectors["predicates"].([]interface{}) + for _, test := range predicates { + tc := test.(map[string]interface{}) + name := tc["name"].(string) + + gf := FromF32(float32(tc["input"].(float64))) + predicate := tc["predicate"].(string) + expected := tc["expected"].(bool) + + var result bool + switch predicate { + case "is_zero": + result = gf.IsZero() + case "is_nan": + result = gf.IsNaN() + case "is_inf": + result = gf.IsInf() + case "is_negative": + result = gf.IsNegative() + default: + t.Errorf("unknown predicate: %s", predicate) + continue + } + + if result != expected { + t.Errorf("FAIL: %s - %s returned %v, expected %v", name, predicate, result, expected) + } + } } -// ============================================================================ -// Constants Tests -// ============================================================================ - -func testConstants(t *testing.T) bool { - passed := 0 - failed := 0 - - // Test zero - if Zero.IsZero() { - passed++ - } else { - failed++ - t.Error("FAIL: zero constant") - } - - // Test one - one := One - if approxEqual(one.ToF32(), 1.0, 0.01) { - passed++ - } else { - failed++ - t.Errorf("FAIL: one constant - got %v, expected 1.0", one.ToF32()) - } - - // Test p_inf - pInf := PInf - if pInf.IsInf() && !pInf.IsNegative() { - passed++ - } else { - failed++ - t.Error("FAIL: p_inf constant") - } - - // Test n_inf - nInf := NInf - if nInf.IsInf() && nInf.IsNegative() { - passed++ - } else { - failed++ - t.Error("FAIL: n_inf constant") - } - - // Test nan - nan := NaN - if nan.IsNaN() { - passed++ - } else { - failed++ - t.Error("FAIL: nan constant") - } - - t.Logf("Constants: %d/%d passed", passed, passed+failed) - return failed == 0 +func TestPhiMath(t *testing.T) { + phi := Phi() + if !approxEqual(float32(phi), 1.6180339887498948, 1e-6) { + t.Errorf("FAIL: phi - got %v, expected 1.6180339887498948", phi) + } + + phiSq := PhiSq() + if !approxEqual(float32(phiSq), 2.6180339887498948, 1e-6) { + t.Errorf("FAIL: phi_sq - got %v, expected 2.6180339887498948", phiSq) + } + + trinity := Trinity() + if !approxEqual(float32(trinity), 3.0, 1e-6) { + t.Errorf("FAIL: trinity - got %v, expected 3.0", trinity) + } } -// ============================================================================ -// Benchmarks -// ============================================================================ +func TestConstants(t *testing.T) { + if !Zero.IsZero() { + t.Error("FAIL: zero constant") + } -func BenchmarkFromF32(b *testing.B) { - for i := 0; i < b.N; i++ { - b.ReportMetric(float64(i)) - _ = FromF32(float32(i)) - } -} + if !approxEqual(One.ToF32(), 1.0, 0.01) { + t.Errorf("FAIL: one constant - got %v", One.ToF32()) + } -func BenchmarkAdd(b *testing.B) { - a := FromF32(1.5) - b := FromF32(2.5) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = a.Add(b) - } - b.ReportMetric(float64(b.N)) -} + if !PInf.IsInf() || PInf.IsNegative() { + t.Error("FAIL: p_inf constant") + } -func BenchmarkMul(b *testing.B) { - a := FromF32(2.5) - b := FromF32(4.0) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = a.Mul(b) - } - b.ReportMetric(float64(b.N)) -} + if !NInf.IsInf() || !NInf.IsNegative() { + t.Error("FAIL: n_inf constant") + } -func BenchmarkPhiQuantize(b *testing.B) { - weights := []float32{1.0, 1.5, 2.0, 2.5, 3.0} - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = PhiQuantize(weights[i%len(weights)]) - } - b.ReportMetric(float64(b.N)) + if !NaN.IsNaN() { + t.Error("FAIL: nan constant") + } } diff --git a/python/goldenfloat/_binding.py b/python/goldenfloat/_binding.py index cade6dc..24de338 100644 --- a/python/goldenfloat/_binding.py +++ b/python/goldenfloat/_binding.py @@ -19,7 +19,9 @@ def _find_library(): # Check zig-out/lib first search_paths = [ os.path.join(os.path.dirname(__file__), "..", "..", "zig-out", "lib"), + os.path.join(os.path.dirname(__file__), "..", "..", "zig-out", "bin"), os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "zig-out", "lib"), + os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "zig-out", "bin"), ] # Add current directory for development diff --git a/rust/goldenfloat-sys/src/lib.rs b/rust/goldenfloat-sys/src/lib.rs index 91f642b..ee8cb31 100644 --- a/rust/goldenfloat-sys/src/lib.rs +++ b/rust/goldenfloat-sys/src/lib.rs @@ -18,6 +18,7 @@ #![no_std] #![allow(non_snake_case)] +#![allow(non_camel_case_types)] use core::ffi::c_char; diff --git a/src/formats/formats_root.zig b/src/formats/formats_root.zig index 2a4309d..8b443b7 100644 --- a/src/formats/formats_root.zig +++ b/src/formats/formats_root.zig @@ -173,63 +173,11 @@ fn fp16ToF32(x: u16) f32 { // Software bf16 encode/decode (Brain Float 16) fn f32ToBf16(a: f32) u16 { - if (a == 0) return 0; - if (std.math.isInf(a)) return 0x7F80; // Infinity (all ones) - if (std.math.isNan(a)) return 0x7FC0; // NaN - - const sign_bit: u16 = if (a < 0) 0x8000 else 0; - const abs_a = if (a < 0) -a else a; - - const frexp_result = std.math.frexp(abs_a); - const m_val = frexp_result.significand; - var e = frexp_result.exponent - 127; - - if (e < -7) { - // Denormalized range -> flush to zero - return sign_bit; - } - - e = @min(e, 7); - if (e <= 0 and m_val < 0.5) { - return sign_bit; // Subnormal -> zero - } - - const mant_f = (m_val - 1.0) * 256.0; // 2^8 - var mant_i = @as(i32, @intFromFloat(mant_f)); - - if (mant_i == 256) { - mant_i = 255; - e += 1; - if (e >= 7) return 0x7F80; // Overflow - } - - const mant_bits: u16 = @as(u16, @intCast(mant_i)) & 0x00FF; - const e_bits: u16 = @as(u16, @intCast(e)) << 7; - - return sign_bit | e_bits | mant_bits; + return @intCast(@as(u32, @bitCast(a)) >> 16); } fn bf16ToF32(x: u16) f32 { - if (x == 0) return 0.0; - if (x == 0x8000) return -0.0; - - const sign = @as(i32, (x >> 15) & 0x1); - const e = @as(i32, (x >> 7) & 0x7F); - const m = @as(i32, x & 0x00FF); - - if (e == 0) { - // Denormalized: value = m * 2^(-126) - const frac = @as(f32, @floatFromInt(m)) / 256.0; - const exp = @as(f32, @floatFromInt(e - 1 - 127)); - const val = frac * std.math.pow(f32, 2.0, exp); - return if (sign != 0) -val else val; - } else { - // Normal: value = (1 + m/256) * 2^(e-127) - const frac = @as(f32, @floatFromInt(m)) / 256.0; - const exp = @as(f32, @floatFromInt(e - 127)); - const val = (1.0 + frac) * std.math.pow(f32, 2.0, exp); - return if (sign != 0) -val else val; - } + return @bitCast(@as(u32, x) << 16); } // ═══════════════════════════════════════════════════════════════════ @@ -588,3 +536,42 @@ test "formatBytes" { try std.testing.expectEqual(@as(usize, 2), formatBytes(.gf16)); try std.testing.expectEqual(@as(usize, 1), formatBytes(.ternary)); } + +test "BF16: roundtrip 1.0" { + const bf16 = f32ToBf16(1.0); + try std.testing.expectEqual(@as(u16, 0x3F80), bf16); + const back = bf16ToF32(bf16); + try std.testing.expectEqual(@as(f32, 1.0), back); +} + +test "BF16: roundtrip 100.0" { + const bf16 = f32ToBf16(100.0); + const back = bf16ToF32(bf16); + const err = @abs(back - 100.0); + try std.testing.expect(err < 1.0); +} + +test "BF16: roundtrip 1e10" { + const bf16 = f32ToBf16(1e10); + const back = bf16ToF32(bf16); + const err = @abs(back - 1e10) / 1e10; + try std.testing.expect(err < 0.01); +} + +test "BF16: roundtrip small values" { + const values = [_]f32{ 0.5, -0.5, 2.0, -2.0, 3.14, -3.14, 1e-10, -1e-10 }; + for (values) |v| { + const bf16 = f32ToBf16(v); + const back = bf16ToF32(bf16); + const err = if (@abs(v) > 0.001) @abs(back - v) / @abs(v) else @abs(back - v); + try std.testing.expect(err < 0.01); + } +} + +test "BF16: special values" { + try std.testing.expectEqual(@as(u16, 0x3F80), f32ToBf16(1.0)); + try std.testing.expect(bf16ToF32(f32ToBf16(std.math.inf(f32))) > 1e30); + try std.testing.expect(std.math.isNan(bf16ToF32(f32ToBf16(std.math.nan(f32))))); + try std.testing.expectEqual(@as(u16, 0), f32ToBf16(0.0)); + try std.testing.expectEqual(@as(u16, 0x8000), f32ToBf16(-0.0)); +} diff --git a/src/formats/golden_float16.zig b/src/formats/golden_float16.zig index e695b24..3ad6920 100644 --- a/src/formats/golden_float16.zig +++ b/src/formats/golden_float16.zig @@ -89,6 +89,9 @@ pub const GF16 = packed struct(u16) { if (v == 0.0) return .{ .mant = 0, .exp = 0, .sign = 0 }; if (!std.math.isFinite(v)) { + if (std.math.isNan(v)) { + return .{ .mant = 1, .exp = 0x3F, .sign = 0 }; + } return .{ .mant = 0, .exp = 0x3F, .sign = @intFromBool(v < 0) }; } @@ -120,6 +123,7 @@ pub const GF16 = packed struct(u16) { return if (self.sign == 1) -0.0 else 0.0; } if (self.exp == 0x3F) { + if (self.mant != 0) return std.math.nan(f32); return if (self.sign == 1) -std.math.inf(f32) else std.math.inf(f32); }