From ae8661aecf58719be8ed1900bbc3467986cb33cf Mon Sep 17 00:00:00 2001 From: Jennifer Conner <2819667+phiryll@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:57:02 -0700 Subject: [PATCH 1/8] Fix *big.Float zero detection to use value, not precision bigFloatCodec classified a value as zero using `prec == 0`, but a big.Float's precision is independent of its value. Only new(big.Float) and var-declared zeros have precision 0; zeros from big.NewFloat(0) or from arithmetic carry a nonzero precision (53 for NewFloat). Those zeros fell through to the finite-number path and were encoded with exponent 0, sorting them as though they were large finite values. Concretely, big.NewFloat(0) encoded greater than 0.25 and 1e-300, and -1e-300 encoded greater than -0.0, breaking the codec's ordering guarantee. Detect zero with value.Sign() == 0 instead. The existing tests missed this because every zero they construct uses var/new(big.Float), which has precision 0. Add TestBigFloatZeroPrecision covering precision-bearing signed zeros against small finite neighbors. Co-Authored-By: Claude Opus 4.8 (1M context) --- big.go | 8 ++++++-- big_test.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/big.go b/big.go index d813a8c..bcbc25a 100644 --- a/big.go +++ b/big.go @@ -213,7 +213,9 @@ func (c bigFloatCodec) Append(buf []byte, value *big.Float) []byte { shift := computeShift(exp, prec) isInf := value.IsInf() - isZero := prec == 0 + // A big.Float's precision is independent of its value; a zero can have any + // precision (e.g. big.NewFloat(0) has precision 53), so test the value. + isZero := value.Sign() == 0 var kind int8 switch { @@ -284,7 +286,9 @@ func (c bigFloatCodec) Put(buf []byte, value *big.Float) []byte { shift := computeShift(exp, prec) isInf := value.IsInf() - isZero := prec == 0 + // A big.Float's precision is independent of its value; a zero can have any + // precision (e.g. big.NewFloat(0) has precision 53), so test the value. + isZero := value.Sign() == 0 var kind int8 switch { diff --git a/big_test.go b/big_test.go index 9c50365..756b1bd 100644 --- a/big_test.go +++ b/big_test.go @@ -1,6 +1,7 @@ package lexy_test import ( + "bytes" "math/big" "testing" @@ -235,6 +236,59 @@ func TestBigFloatOrdering(t *testing.T) { }) } +// A big.Float's precision is independent of its value, so a zero can have any +// precision. big.NewFloat(0) and any zero produced by arithmetic have nonzero +// precision, unlike the var/new(big.Float) zeros used elsewhere in these tests. +// This regression test guards against classifying such zeros as finite numbers, +// which encoded them out of order (as though they were large finite values). +func TestBigFloatZeroPrecision(t *testing.T) { + t.Parallel() + codec := lexy.BigFloat() + enc := func(f *big.Float) []byte { return codec.Append(nil, f) } + + posZeroPrec := big.NewFloat(0) // sign 0, precision 53 + negZeroPrec := new(big.Float).Neg(posZeroPrec) + assert.True(t, negZeroPrec.Signbit()) + assert.Equal(t, uint(53), posZeroPrec.Prec()) + + tiny := big.NewFloat(1e-300) + negTiny := big.NewFloat(-1e-300) + half := big.NewFloat(0.5) + negHalf := big.NewFloat(-0.5) + + // A precision-bearing zero must encode identically to the var-declared zero, + // and must sort between the smallest negative and smallest positive values. + var plainPosZero, plainNegZero big.Float + plainNegZero.Neg(&plainNegZero) + assert.Equal(t, enc(&plainPosZero), enc(posZeroPrec)) + assert.Equal(t, enc(&plainNegZero), enc(negZeroPrec)) + + // Encoded order is strictly ascending, including -0.0 < +0.0 (the codec + // distinguishes signed zeros even though they are numerically equal). + ordered := []struct { + name string + f *big.Float + }{ + {"-0.5", negHalf}, + {"-1e-300", negTiny}, + {"-0.0 (prec 53)", negZeroPrec}, + {"+0.0 (prec 53)", posZeroPrec}, + {"+1e-300", tiny}, + {"+0.5", half}, + } + for i := 1; i < len(ordered); i++ { + a, b := ordered[i-1], ordered[i] + cmp := bytes.Compare(enc(a.f), enc(b.f)) + assert.Negativef(t, cmp, "%s should encode before %s (encoded cmp=%d)", a.name, b.name, cmp) + } + + // Round trips still work for precision-bearing zeros. + for _, o := range ordered { + got, _ := codec.Get(enc(o.f)) + assert.Zerof(t, got.Cmp(o.f), "round trip %s: got %v", o.name, got) + } +} + func TestBigFloatNilsLast(t *testing.T) { t.Parallel() var negInf, posInf, posZero big.Float From 5ca6b574c89eb4357114c8719693d71fa2480a7a Mon Sep 17 00:00:00 2001 From: Jennifer Conner <2819667+phiryll@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:58:03 -0700 Subject: [PATCH 2/8] Comment out some unused constants. --- float_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/float_test.go b/float_test.go index 9072e13..85069ac 100644 --- a/float_test.go +++ b/float_test.go @@ -12,10 +12,10 @@ import ( // Bit masks for the sign, exponent, and matissa of the // IEEE 754 32- and 64- floating point representations. const ( - maskSign32 uint32 = 0x80_00_00_00 + // maskSign32 uint32 = 0x80_00_00_00 maskExp32 uint32 = 0x7F_80_00_00 maskMant32 uint32 = 0x00_7F_FF_FF - maskSign64 uint64 = 0x80_00_00_00_00_00_00_00 + // maskSign64 uint64 = 0x80_00_00_00_00_00_00_00 maskExp64 uint64 = 0x7F_F0_00_00_00_00_00_00 maskMant64 uint64 = 0x00_0F_FF_FF_FF_FF_FF_FF ) From 684f7b6ec724f35e4045f31b8e5e71704ab60618 Mon Sep 17 00:00:00 2001 From: Jennifer Conner <2819667+phiryll@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:13:11 -0700 Subject: [PATCH 3/8] Disable timeout when fuzz testing. --- fuzz-all.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz-all.sh b/fuzz-all.sh index 67499bd..495fefd 100755 --- a/fuzz-all.sh +++ b/fuzz-all.sh @@ -16,6 +16,6 @@ do do echo "Fuzzing $func in $file" parentDir=$(dirname $file) - go test $parentDir -run=$func -fuzz=$func -fuzztime=${fuzzTime}s + go test $parentDir -run=$func -fuzz=$func -fuzztime=${fuzzTime}s -timeout 0 done done From ddc91699fc44d496190c084dbf9cd108b91c0bc9 Mon Sep 17 00:00:00 2001 From: Jennifer Conner <2819667+phiryll@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:10:06 -0700 Subject: [PATCH 4/8] Remove claude's test, as the fix is not correct. It is a partial fix, but zeroes with different precisions need to be encoded differently. The existing tests should be modified to include new test cases instead of adding a new test here. --- big.go | 4 ---- big_test.go | 54 ----------------------------------------------------- 2 files changed, 58 deletions(-) diff --git a/big.go b/big.go index bcbc25a..4414d5d 100644 --- a/big.go +++ b/big.go @@ -213,8 +213,6 @@ func (c bigFloatCodec) Append(buf []byte, value *big.Float) []byte { shift := computeShift(exp, prec) isInf := value.IsInf() - // A big.Float's precision is independent of its value; a zero can have any - // precision (e.g. big.NewFloat(0) has precision 53), so test the value. isZero := value.Sign() == 0 var kind int8 @@ -286,8 +284,6 @@ func (c bigFloatCodec) Put(buf []byte, value *big.Float) []byte { shift := computeShift(exp, prec) isInf := value.IsInf() - // A big.Float's precision is independent of its value; a zero can have any - // precision (e.g. big.NewFloat(0) has precision 53), so test the value. isZero := value.Sign() == 0 var kind int8 diff --git a/big_test.go b/big_test.go index 756b1bd..9c50365 100644 --- a/big_test.go +++ b/big_test.go @@ -1,7 +1,6 @@ package lexy_test import ( - "bytes" "math/big" "testing" @@ -236,59 +235,6 @@ func TestBigFloatOrdering(t *testing.T) { }) } -// A big.Float's precision is independent of its value, so a zero can have any -// precision. big.NewFloat(0) and any zero produced by arithmetic have nonzero -// precision, unlike the var/new(big.Float) zeros used elsewhere in these tests. -// This regression test guards against classifying such zeros as finite numbers, -// which encoded them out of order (as though they were large finite values). -func TestBigFloatZeroPrecision(t *testing.T) { - t.Parallel() - codec := lexy.BigFloat() - enc := func(f *big.Float) []byte { return codec.Append(nil, f) } - - posZeroPrec := big.NewFloat(0) // sign 0, precision 53 - negZeroPrec := new(big.Float).Neg(posZeroPrec) - assert.True(t, negZeroPrec.Signbit()) - assert.Equal(t, uint(53), posZeroPrec.Prec()) - - tiny := big.NewFloat(1e-300) - negTiny := big.NewFloat(-1e-300) - half := big.NewFloat(0.5) - negHalf := big.NewFloat(-0.5) - - // A precision-bearing zero must encode identically to the var-declared zero, - // and must sort between the smallest negative and smallest positive values. - var plainPosZero, plainNegZero big.Float - plainNegZero.Neg(&plainNegZero) - assert.Equal(t, enc(&plainPosZero), enc(posZeroPrec)) - assert.Equal(t, enc(&plainNegZero), enc(negZeroPrec)) - - // Encoded order is strictly ascending, including -0.0 < +0.0 (the codec - // distinguishes signed zeros even though they are numerically equal). - ordered := []struct { - name string - f *big.Float - }{ - {"-0.5", negHalf}, - {"-1e-300", negTiny}, - {"-0.0 (prec 53)", negZeroPrec}, - {"+0.0 (prec 53)", posZeroPrec}, - {"+1e-300", tiny}, - {"+0.5", half}, - } - for i := 1; i < len(ordered); i++ { - a, b := ordered[i-1], ordered[i] - cmp := bytes.Compare(enc(a.f), enc(b.f)) - assert.Negativef(t, cmp, "%s should encode before %s (encoded cmp=%d)", a.name, b.name, cmp) - } - - // Round trips still work for precision-bearing zeros. - for _, o := range ordered { - got, _ := codec.Get(enc(o.f)) - assert.Zerof(t, got.Cmp(o.f), "round trip %s: got %v", o.name, got) - } -} - func TestBigFloatNilsLast(t *testing.T) { t.Parallel() var negInf, posInf, posZero big.Float From c56e5a4197f3f7e8d1d419913dab2eeb4d9a7e25 Mon Sep 17 00:00:00 2001 From: Jennifer Conner <2819667+phiryll@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:11:39 -0700 Subject: [PATCH 5/8] Remove commented out code. --- float_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/float_test.go b/float_test.go index 85069ac..e9e803d 100644 --- a/float_test.go +++ b/float_test.go @@ -12,10 +12,8 @@ import ( // Bit masks for the sign, exponent, and matissa of the // IEEE 754 32- and 64- floating point representations. const ( - // maskSign32 uint32 = 0x80_00_00_00 maskExp32 uint32 = 0x7F_80_00_00 maskMant32 uint32 = 0x00_7F_FF_FF - // maskSign64 uint64 = 0x80_00_00_00_00_00_00_00 maskExp64 uint64 = 0x7F_F0_00_00_00_00_00_00 maskMant64 uint64 = 0x00_0F_FF_FF_FF_FF_FF_FF ) From 27fcb031bbaa4534f23c7077a57e254aee450a8e Mon Sep 17 00:00:00 2001 From: Jennifer Conner <2819667+phiryll@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:55:39 -0700 Subject: [PATCH 6/8] Add test functions to create zeroes with non-zero precision. --- big_test.go | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/big_test.go b/big_test.go index 9c50365..c51f14b 100644 --- a/big_test.go +++ b/big_test.go @@ -102,6 +102,19 @@ func newBigFloat64(f float64, shift int, prec uint) *big.Float { return value } +func newBigFloatPosZero(prec uint) *big.Float { + var value big.Float + value.SetPrec(prec) + return &value +} + +func newBigFloatNegZero(prec uint) *big.Float { + var value big.Float + value.SetPrec(prec) + value.Neg(&value) + return &value +} + func newBigFloat(s string) *big.Float { var value big.Float // Parse truncates to 64 bits if precision is currently 0. @@ -114,14 +127,9 @@ func newBigFloat(s string) *big.Float { func TestBigFloat(t *testing.T) { t.Parallel() - var negInf, posInf, negZero, posZero big.Float + var negInf, posInf big.Float negInf.SetInf(true) posInf.SetInf(false) - negZero.Neg(&negZero) - assert.True(t, negZero.Signbit()) - assert.False(t, posZero.Signbit()) - assert.Equal(t, 0, negZero.Cmp(&posZero)) - assert.NotEqual(t, &negZero, &posZero) wholeNumber := newBigFloat(manyDigits + manyDigits) mixedNumber := newBigFloat(manyDigits + "." + manyDigits) @@ -148,8 +156,8 @@ func TestBigFloat(t *testing.T) { {"-Inf", &negInf, nil}, {"+Inf", &posInf, nil}, - {"-0", &negZero, nil}, - {"+0", &posZero, nil}, + {"-0", newBigFloatNegZero(0), nil}, + {"+0", newBigFloatPosZero(0), nil}, {"long whole", wholeNumber, nil}, {"long mixed", mixedNumber, nil}, @@ -159,14 +167,9 @@ func TestBigFloat(t *testing.T) { func TestBigFloatOrdering(t *testing.T) { t.Parallel() - var negInf, posInf, negZero, posZero big.Float + var negInf, posInf big.Float negInf.SetInf(true) posInf.SetInf(false) - negZero.Neg(&negZero) - assert.True(t, negZero.Signbit()) - assert.False(t, posZero.Signbit()) - assert.Equal(t, 0, negZero.Cmp(&posZero)) - assert.NotEqual(t, &negZero, &posZero) // For the same matissa, a higher exponent (first) or precision is closer to infinity. testOrdering(t, lexy.BigFloat(), []testCase[*big.Float]{ @@ -201,8 +204,8 @@ func TestBigFloatOrdering(t *testing.T) { {"-12345.0 * 2^-10000 (19)", newBigFloat64(-12345.0, -10000, 19), nil}, // zeros - {"-0.0", &negZero, nil}, - {"+0.0", &posZero, nil}, + {"-0.0", newBigFloatNegZero(0), nil}, + {"+0.0", newBigFloatPosZero(0), nil}, // very small positive numbers {"12345.0 * 2^-10000 (19)", newBigFloat64(12345.0, -10000, 19), nil}, From 5ed35d4014af1c74dfa0d8ee28d7701c5fa7ffb8 Mon Sep 17 00:00:00 2001 From: Jennifer Conner <2819667+phiryll@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:19:40 -0700 Subject: [PATCH 7/8] Add failing test cases for zero big.Floats with non-zero precision. --- big_test.go | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/big_test.go b/big_test.go index c51f14b..33c4072 100644 --- a/big_test.go +++ b/big_test.go @@ -140,12 +140,12 @@ func TestBigFloat(t *testing.T) { testCodec(t, codec, fillTestData(codec, []testCase[*big.Float]{ {"nil", nil, nil}, // example in implementation comments - {"seven(3)", newBigFloat64(7.0, 0, 3), nil}, - {"seven(4)", newBigFloat64(7.0, 0, 4), nil}, - {"seven(10)", newBigFloat64(7.0, 0, 10), nil}, - {"-seven(3)", newBigFloat64(-7.0, 0, 3), nil}, - {"-seven(4)", newBigFloat64(-7.0, 0, 4), nil}, - {"-seven(10)", newBigFloat64(-7.0, 0, 10), nil}, + {"seven (3)", newBigFloat64(7.0, 0, 3), nil}, + {"seven (4)", newBigFloat64(7.0, 0, 4), nil}, + {"seven (10)", newBigFloat64(7.0, 0, 10), nil}, + {"-seven (3)", newBigFloat64(-7.0, 0, 3), nil}, + {"-seven (4)", newBigFloat64(-7.0, 0, 4), nil}, + {"-seven (10)", newBigFloat64(-7.0, 0, 10), nil}, {"tiny", newBigFloat64(12345.0, -100, 20), nil}, {"mixed", newBigFloat64(12345.0, -10, 20), nil}, @@ -156,8 +156,13 @@ func TestBigFloat(t *testing.T) { {"-Inf", &negInf, nil}, {"+Inf", &posInf, nil}, - {"-0", newBigFloatNegZero(0), nil}, - {"+0", newBigFloatPosZero(0), nil}, + + {"-0.0 (0)", newBigFloatNegZero(0), nil}, + {"-0.0 (10)", newBigFloatNegZero(10), nil}, + {"-0.0 (20)", newBigFloatNegZero(20), nil}, + {"+0.0 (0)", newBigFloatPosZero(0), nil}, + {"+0.0 (10)", newBigFloatPosZero(10), nil}, + {"+0.0 (20)", newBigFloatPosZero(20), nil}, {"long whole", wholeNumber, nil}, {"long mixed", mixedNumber, nil}, @@ -204,8 +209,12 @@ func TestBigFloatOrdering(t *testing.T) { {"-12345.0 * 2^-10000 (19)", newBigFloat64(-12345.0, -10000, 19), nil}, // zeros - {"-0.0", newBigFloatNegZero(0), nil}, - {"+0.0", newBigFloatPosZero(0), nil}, + {"-0.0 (20)", newBigFloatNegZero(20), nil}, + {"-0.0 (10)", newBigFloatNegZero(10), nil}, + {"-0.0 (0)", newBigFloatNegZero(0), nil}, + {"+0.0 (0)", newBigFloatPosZero(0), nil}, + {"+0.0 (10)", newBigFloatPosZero(10), nil}, + {"+0.0 (20)", newBigFloatPosZero(20), nil}, // very small positive numbers {"12345.0 * 2^-10000 (19)", newBigFloat64(12345.0, -10000, 19), nil}, From 865b9a51e81ff74b8e78da8ac181279232eefafd Mon Sep 17 00:00:00 2001 From: Jennifer Conner <2819667+phiryll@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:37:44 -0700 Subject: [PATCH 8/8] Fix encoding for big.Float zero values with non-zero precision. --- big.go | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/big.go b/big.go index 4414d5d..440a964 100644 --- a/big.go +++ b/big.go @@ -110,7 +110,7 @@ func (bigIntCodec) nilsLast() Codec[*big.Int] { // the standard library just doesn't expose that information. // A description of the encoding and why it does what it does follows. // -// Shift a copy of the big.Float so that: +// If not infinite or zero, shift a copy of the big.Float so that: // // all significant bits are to the left of the point, // the high bit of the high byte is 1, and @@ -156,7 +156,12 @@ func (bigIntCodec) nilsLast() Codec[*big.Int] { // write prefixNilFirst/Last if value is nil and return immediately // write prefixNonNil // write int8: negInf/negFinite/negZero/posZero/posFinite/posInf -// if infinite or zero, we're done +// if infinite, we're done +// if zero +// write int32 precision +// negate precision first if Float is negative +// write uint8 rounding mode +// we're done // write int32 exponent // negate exponent first if Float is negative // write the (big-endian) bytes of the big.Int of the shifted mantissa @@ -231,9 +236,17 @@ func (c bigFloatCodec) Append(buf []byte, value *big.Float) []byte { kind = posFinite } buf = stdInt8.Append(buf, kind) - if isInf || isZero { + if isInf { return buf } + if isZero { + if signbit { + prec = -prec + } + buf = stdInt32.Append(buf, int32(prec)) + return modeCodec.Append(buf, mode) + } + mantSize := numBytes(prec) start := len(buf) // 9 = 4 (exp) + 4 (prec) + 1 (mode) @@ -269,7 +282,7 @@ func (c bigFloatCodec) Append(buf []byte, value *big.Float) []byte { return buf } -//nolint:cyclop +//nolint:cyclop,funlen func (c bigFloatCodec) Put(buf []byte, value *big.Float) []byte { done, buf := c.prefix.Put(buf, value == nil) if done { @@ -302,11 +315,18 @@ func (c bigFloatCodec) Put(buf []byte, value *big.Float) []byte { kind = posFinite } buf = stdInt8.Put(buf, kind) - if isInf || isZero { + if isInf { return buf } - mantSize := numBytes(prec) + if isZero { + if signbit { + prec = -prec + } + buf = stdInt32.Put(buf, int32(prec)) + return modeCodec.Put(buf, mode) + } + mantSize := numBytes(prec) var tmp big.Float tmp.SetMantExp(value, shift) mantInt, acc := tmp.Int(nil) @@ -343,10 +363,16 @@ func (c bigFloatCodec) Get(buf []byte) (*big.Float, []byte) { return value.SetInf(signbit), buf } if kind == negZero || kind == posZero { + //nolint:govet + prec, buf := stdInt32.Get(buf) + mode, buf := modeCodec.Get(buf) var value big.Float if signbit { + prec = -prec value.Neg(&value) } + value.SetPrec(uint(prec)) + value.SetMode(mode) return &value, buf }