From cc6aabae22c720b02cbeab7ba558b49ea9719b5d Mon Sep 17 00:00:00 2001 From: Ben Arthur Date: Mon, 31 Aug 2026 16:30:47 -0400 Subject: [PATCH 1/5] Fix off-by-one in the overflow check of checked_mul for Normed The divisor 2^f-1 is odd, so a widened product equal to typemax(N).i * rawone(N) + (rawone(N) >> 1) still rounds to typemax(N) and must not throw. Previously such products (e.g. checked_mul(1.228N1f7, 1.638N1f7)) spuriously raised OverflowError, disagreeing with both wrapping_mul and the generic float fallback. Co-Authored-By: Claude Fable 5 --- src/normed.jl | 4 +++- test/normed.jl | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/normed.jl b/src/normed.jl index a8fae420..d8e95712 100644 --- a/src/normed.jl +++ b/src/normed.jl @@ -283,8 +283,10 @@ end function checked_mul(x::N, y::N) where {T <: Union{UInt8,UInt16,UInt32,UInt64}, f, N <: Normed{T,f}} f == bitwidth(T) && return wrapping_mul(x, y) z = widemul(x.i, y.i) + # `rawone(N)` is odd, so `z` rounds to `typemax(N).i` (no overflow) up to + # and including `m`; overflow starts at `m + 1`. m = widemul(typemax(N).i, rawone(N)) + (rawone(N) >> 0x1) - z < m || throw_overflowerror(:*, x, y) + z <= m || throw_overflowerror(:*, x, y) N(div_2fm1(z, Val(Int(f))) % T, 0) end diff --git a/test/normed.jl b/test/normed.jl index b6afb1d2..646b84b1 100644 --- a/test/normed.jl +++ b/test/normed.jl @@ -409,6 +409,10 @@ end @test_throws OverflowError checked_mul(typemax(N), typemax(N)) end end + # products whose exact value rounds to `typemax` must not throw + @test checked_mul(reinterpret(N1f7, 0x9c), reinterpret(N1f7, 0xd0)) === typemax(N1f7) + @test checked_mul(reinterpret(N7f9, 0x0201), reinterpret(N7f9, 0xff00)) === typemax(N7f9) + test_mul(Normed) end From 1b334184e6e3705addc4348ee207ae98dd2172ad Mon Sep 17 00:00:00 2001 From: Ben Arthur Date: Mon, 31 Aug 2026 16:32:07 -0400 Subject: [PATCH 2/5] Make rem of non-finite values platform-independent for small types _rem skipped the isfinite guard when bitwidth(T) < 32, leaving the result to unspecified float-to-int conversion behavior: Inf % N0f8 returned 1.0N0f8 on aarch64 but 0.0N0f8 on x86_64. Check isfinite unconditionally so NaN/Inf map to zero on all platforms, matching the wider types. This also makes saturating_fdiv(0, 0) well-defined. Co-Authored-By: Claude Fable 5 --- src/fixed.jl | 2 +- src/normed.jl | 2 +- test/common.jl | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/fixed.jl b/src/fixed.jl index 1abd7ff4..417a511a 100644 --- a/src/fixed.jl +++ b/src/fixed.jl @@ -103,10 +103,10 @@ function _rem(x::Fixed, ::Type{F}) where {T, f, F <: Fixed{T,f}} end _rem(x::Integer, ::Type{F}) where {T, f, F <: Fixed{T,f}} = F(_unsafe_trunc(T, x) << f, 0) function _rem(x::Real, ::Type{F}) where {T, f, F <: Fixed{T,f}} + isfinite(x) || return zero(F) if bitwidth(T) < 32 Ti = T else - isfinite(x) || return zero(F) Ti = promote_type(Int64, T) end Tf = floattype(F) diff --git a/src/normed.jl b/src/normed.jl index d8e95712..eec16735 100644 --- a/src/normed.jl +++ b/src/normed.jl @@ -112,7 +112,7 @@ _rem(x::N, ::Type{N}) where {N <: Normed} = x _rem(x::Normed, ::Type{N}) where {T, N <: Normed{T}} = reinterpret(N, _unsafe_trunc(T, round((rawone(N)/rawone(x))*reinterpret(x)))) function _rem(x::Real, ::Type{N}) where {T, N <: Normed{T}} - bitwidth(T) < 32 || isfinite(x) || return zero(N) + isfinite(x) || return zero(N) reinterpret(N, _unsafe_trunc(T, round(rawone(N) * x))) end _rem(x::Float16, ::Type{X}) where {X <: Normed} = _rem(Float32(x), X) # avoid overflow diff --git a/test/common.jl b/test/common.jl index fdd4cf7b..3a046080 100644 --- a/test/common.jl +++ b/test/common.jl @@ -162,9 +162,10 @@ function test_rem_type(TX::Type) end function test_rem_nan(TX::Type) - # TODO: avoid undefined behavior @testset "nan % $X" for X in target(TX, :i8, :i16, :i32, :i64; ex = :thin) @test NaN % X === NaN32 % X === NaN16 % X === zero(X) + @test Inf % X === Inf32 % X === Inf16 % X === zero(X) + @test -Inf % X === -Inf32 % X === -Inf16 % X === zero(X) end end From fbcd8ba663a0b5d5e97f0ff7ec6fc00aa59f8889 Mon Sep 17 00:00:00 2001 From: Ben Arthur Date: Mon, 31 Aug 2026 16:36:20 -0400 Subject: [PATCH 3/5] Relax Statistics compat to allow v1.11.2 and later The pin below v1.11.2 was awaiting the next Statistics.jl release (JuliaStats/Statistics.jl#165). Statistics v1.11.4 is now registered, still provides _mean_promote, and the extension already guards its use with isdefined; the full test suite passes against v1.11.4. Without this, environments requiring newer Statistics downgrade FixedPointNumbers to 0.8. Co-Authored-By: Claude Fable 5 --- Project.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index a76d1e6e..88767b48 100644 --- a/Project.toml +++ b/Project.toml @@ -17,9 +17,7 @@ Aqua = "0.8" Documenter = "0.27, 1" Random = "<0.0.1, 1" StableRNGs = "1" -# Update this version specifier when Statistics.jl v1.11.2 is released. -# https://github.com/JuliaStats/Statistics.jl/issues/165 -Statistics = "< 1.11.2" +Statistics = "1" Test = "1" julia = "1" From 34e1e9913e72c3b94ec70c73f9e502dee15212b3 Mon Sep 17 00:00:00 2001 From: Ben Arthur Date: Mon, 31 Aug 2026 16:38:41 -0400 Subject: [PATCH 4/5] bump patch version Co-Authored-By: Claude Fable 5 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 88767b48..d8abe474 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "FixedPointNumbers" uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" -version = "0.9.0" +version = "0.9.1" [deps] Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" From f24a6e0a3fe356b4b601b741a68ddc3bcb3f9a55 Mon Sep 17 00:00:00 2001 From: Ben Arthur Date: Mon, 31 Aug 2026 16:49:35 -0400 Subject: [PATCH 5/5] Fix version spoof in Invalidations workflow The workaround from #287 rewrote the literal line 'version = "0.9.0-dev"' to 0.8.4 so that the invalidations action can co-install SnoopCompile (whose dependency FlameGraphs pins FixedPointNumbers to <= 0.8). The literal no longer matches now that the version is 0.9.x, so the resolver hit unsatisfiable requirements. Match any version line instead. Co-Authored-By: Claude Fable 5 --- .github/workflows/Invalidations.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Invalidations.yml b/.github/workflows/Invalidations.yml index a582f3fa..d657cd16 100644 --- a/.github/workflows/Invalidations.yml +++ b/.github/workflows/Invalidations.yml @@ -27,7 +27,7 @@ jobs: lines = readlines("Project.toml") open("Project.toml", "w") do f for l in lines - if l == "version = \"0.9.0-dev\"" + if startswith(l, "version = ") l = "version = \"0.8.4\"" end println(f, l) @@ -46,7 +46,7 @@ jobs: lines = readlines("Project.toml") open("Project.toml", "w") do f for l in lines - if l == "version = \"0.9.0-dev\"" + if startswith(l, "version = ") l = "version = \"0.8.4\"" end println(f, l)