From 96f2583cd18a665aa404c1268c79bf926dd2a22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Sat, 17 Oct 2026 17:55:57 +0200 Subject: [PATCH 1/3] Make the package JET-clean and run JET on all reported code This seems to combine the best of both worlds [from the discussion](https://github.com/JuliaMath/FixedPointNumbers.jl/pull/358#discussion_r4292245389) of the feature. The JET workflow analyzed only FixedPointNumbers itself and printed an unrestricted report on the side, which can be confusing. Reports from Base code reached through FixedPointNumbers are worth catching as well, so the workflow now runs `test_package` without `target_modules` and fails on any report. This helps to find regressions both in future FPN and in future Julia versions. This required removing three causes of reports on Julia 1.10 and 1.13: - `signed` and `unsigned` for `FixedPoint` threw a `MethodError` only because `rem` has no method for a type argument. They now throw the `MethodError` explicitly. - `_trunc` for `FixedPoint{<:Unsigned}` also dispatched to the `Fixed` method of `_floor`, with an impossible `Fixed{Union{}}`. It is now defined for `Normed`, the only unsigned `FixedPoint` type. - `rand!` converted the array pointer with `reinterpret`, whose Base internals JET reports on Julia 1.10. The `Ptr{T}` constructor does the same conversion and is better style anyway. --- .github/workflows/JET.yml | 4 +--- src/FixedPointNumbers.jl | 16 +++++++--------- src/normed.jl | 2 ++ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/JET.yml b/.github/workflows/JET.yml index 3de143d..53e46bc 100644 --- a/.github/workflows/JET.yml +++ b/.github/workflows/JET.yml @@ -48,8 +48,6 @@ jobs: julia --project=. --color=yes -e ' using JET, Test using FixedPointNumbers - println("report_package(FixedPointNumbers, toplevel_logger=nothing)") - show(JET.report_package(FixedPointNumbers, toplevel_logger=nothing)) @testset "JET" begin - JET.test_package(FixedPointNumbers; target_modules=(FixedPointNumbers,)) + JET.test_package(FixedPointNumbers) end' diff --git a/src/FixedPointNumbers.jl b/src/FixedPointNumbers.jl index 7bafaca..cc3e154 100644 --- a/src/FixedPointNumbers.jl +++ b/src/FixedPointNumbers.jl @@ -364,12 +364,12 @@ clamp(x::X, lo::X, hi::X) where {X <: FixedPoint} = X(clamp(x.i, lo.i, hi.i), 0) clamp(x, ::Type{X}) where {X <: FixedPoint} = clamp(x, typemin(X), typemax(X)) % X # Since `FixedPoint` is not an integer type, it is not clear in what type -# `signed` and `unsigned` for `FixedPoint` should return values. They should -# currently throw errors in case we support "unsigned Fixed" or "signed Normed" -# in the future. The following "incomplete" code is necessary for Julia v1.0 -# etc. to prevent accidental conversion to an integer type. -signed(x::X) where {X <: FixedPoint} = signed(X)(signed(x.i), 0) -unsigned(x::X) where {X <: FixedPoint} = unsigned(X)(unsigned(x.i), 0) +# `signed` and `unsigned` for `FixedPoint` should return values. They throw a +# `MethodError` until "unsigned Fixed" or "signed Normed" are supported. The +# explicit methods keep the generic fallbacks from converting a `FixedPoint` to +# an integer type. +signed(x::FixedPoint) = throw(MethodError(signed, (x,))) +unsigned(x::FixedPoint) = throw(MethodError(unsigned, (x,))) function sign(x::X) where {T, X <: FixedPoint{T}} T <: Unsigned && return iszero(x) ? x : oneunit(X) @@ -450,8 +450,6 @@ for (m, f) in ((:(:Nearest), :round), end end end -_trunc(x::X) where {X <: FixedPoint{<:Unsigned}} = _floor(x) -_trunc(::Type{Ti}, x::X) where {X <: FixedPoint{<:Unsigned}, Ti <: Integer} = _floor(Ti, x) length(r::AbstractUnitRange{<:FixedPoint}) = _length(r) length(r::StepRange{<:FixedPoint}) = _length(r) @@ -657,7 +655,7 @@ function Random.rand(r::AbstractRNG, ::SamplerType{X}) where X <: FixedPoint end function rand!(r::AbstractRNG, A::Array{X}, ::SamplerType{X}) where {T, X <: FixedPoint{T}} - At = unsafe_wrap(Array, reinterpret(Ptr{T}, pointer(A)), size(A)) + At = unsafe_wrap(Array, Ptr{T}(pointer(A)), size(A)) Random.rand!(r, At, SamplerType{T}()) A end diff --git a/src/normed.jl b/src/normed.jl index 21ad969..20c7ccc 100644 --- a/src/normed.jl +++ b/src/normed.jl @@ -293,6 +293,8 @@ end # Functions _floor(x::N) where {N <: Normed} = reinterpret(N, x.i - x.i % rawone(N)) +_trunc(x::Normed) = _floor(x) +_trunc(::Type{Ti}, x::Normed) where {Ti <: Integer} = _floor(Ti, x) function _ceil(x::Normed{T,f}) where {T, f} f == 1 && return x if typemax(T) % rawone(x) != 0 From f873937b8a4e3bbbba1c0c06ffc41b465e77fbfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Sun, 18 Oct 2026 10:52:02 +0200 Subject: [PATCH 2/3] Handle `Fixed{Union{}}` in `_floor` instead of narrowing `_trunc` The generic `_trunc` for `FixedPoint{<:Unsigned}` stays available to unsigned `FixedPoint` types outside this package. JET reached the `Fixed` method of `_floor` through it, because `Union{}` is both `Signed` and `Unsigned`. Bounding that method by `T <: Signed` limits the overlap to `Fixed{Union{}, f}`, which has no values and now has its own method. The comment on `signed` and `unsigned` now only states why the methods exist. --- src/FixedPointNumbers.jl | 9 ++++----- src/fixed.jl | 5 ++++- src/normed.jl | 2 -- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/FixedPointNumbers.jl b/src/FixedPointNumbers.jl index cc3e154..448c3cc 100644 --- a/src/FixedPointNumbers.jl +++ b/src/FixedPointNumbers.jl @@ -363,11 +363,8 @@ clamp(x::X, lo::X, hi::X) where {X <: FixedPoint} = X(clamp(x.i, lo.i, hi.i), 0) clamp(x, ::Type{X}) where {X <: FixedPoint} = clamp(x, typemin(X), typemax(X)) % X -# Since `FixedPoint` is not an integer type, it is not clear in what type -# `signed` and `unsigned` for `FixedPoint` should return values. They throw a -# `MethodError` until "unsigned Fixed" or "signed Normed" are supported. The -# explicit methods keep the generic fallbacks from converting a `FixedPoint` to -# an integer type. +# Throw a `MethodError` so that the generic fallbacks don't convert a `FixedPoint` +# to an integer type. signed(x::FixedPoint) = throw(MethodError(signed, (x,))) unsigned(x::FixedPoint) = throw(MethodError(unsigned, (x,))) @@ -450,6 +447,8 @@ for (m, f) in ((:(:Nearest), :round), end end end +_trunc(x::X) where {X <: FixedPoint{<:Unsigned}} = _floor(x) +_trunc(::Type{Ti}, x::X) where {X <: FixedPoint{<:Unsigned}, Ti <: Integer} = _floor(Ti, x) length(r::AbstractUnitRange{<:FixedPoint}) = _length(r) length(r::StepRange{<:FixedPoint}) = _length(r) diff --git a/src/fixed.jl b/src/fixed.jl index 212a95f..7f6e476 100644 --- a/src/fixed.jl +++ b/src/fixed.jl @@ -189,10 +189,13 @@ function _trunc(x::Fixed{T,f}) where {T, f} _rawone = oneunit(T) << f reinterpret(Fixed{T,f}, (x.i < 0) & (r != 0) ? t + _rawone : t) end -function _floor(x::Fixed{T,f}) where {T, f} +# The bound `T <: Signed` limits the overlap with the generic `_trunc` for `FixedPoint{<:Unsigned}` to +# `Fixed{Union{}, f}`, which has no values and gets its own method. +function _floor(x::Fixed{T,f}) where {T <: Signed, f} f == bitwidth(T) && x.i < 0 && throw_converterror(Fixed{T,f}, -1) # TODO: remove this line Fixed{T,f}(x.i & intmask(x), 0) end +_floor(x::Fixed{Union{}, f}) where f = throw(MethodError(_floor, (x,))) function _ceil(x::Fixed{T,f}) where {T, f} f == 0 && return x upper = typemax(T) & intmask(x) diff --git a/src/normed.jl b/src/normed.jl index 20c7ccc..21ad969 100644 --- a/src/normed.jl +++ b/src/normed.jl @@ -293,8 +293,6 @@ end # Functions _floor(x::N) where {N <: Normed} = reinterpret(N, x.i - x.i % rawone(N)) -_trunc(x::Normed) = _floor(x) -_trunc(::Type{Ti}, x::Normed) where {Ti <: Integer} = _floor(Ti, x) function _ceil(x::Normed{T,f}) where {T, f} f == 1 && return x if typemax(T) % rawone(x) != 0 From 6a7e78f6e5bd5242e9d9fce5ce7ab32e55179455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Sun, 18 Oct 2026 16:41:18 +0200 Subject: [PATCH 3/3] Throw a plain `MethodError` for `Union{}` as type argument `Union{}` is a subtype of every type, so methods for `Type{X} where {X <: FixedPoint}` also match it. Calls like `floattype(Union{})` or `0.5 % Union{}` therefore ended in ambiguity errors that named internal functions, or in errors from inside unrelated methods. Since Julia 1.10, a method of the form `f(..., ::Type{Union{}}, slurp...)` takes precedence over all other methods of `f`, which is how Base handles the same case. The exported functions with a type argument get such a method. `*`, `rem` and the functions of `Base.Checked` belong to Base, so the internal `_convert` and `_rem` they call are guarded instead. On older Julia versions, these calls still throw an ambiguous `MethodError`. The `Fixed{Union{}}` method of `_floor` cannot be reached at runtime, because `Fixed{Union{}}` has no values, so it is excluded from coverage. --- src/FixedPointNumbers.jl | 13 +++++++++++++ src/fixed.jl | 2 +- test/traits.jl | 11 +++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/FixedPointNumbers.jl b/src/FixedPointNumbers.jl index 448c3cc..0b91cf7 100644 --- a/src/FixedPointNumbers.jl +++ b/src/FixedPointNumbers.jl @@ -67,6 +67,17 @@ wrapping_rem(x::Real, ::Type{X}) where {X <: FixedPoint} = _rem(x, X) saturating_rem(x::Real, ::Type{X}) where {X <: FixedPoint} = _rem(x, X) checked_rem(x::Real, ::Type{X}) where {X <: FixedPoint} = _rem(x, X) +# `Type{X} where {X <: FixedPoint}` also matches `Type{Union{}}`. Since Julia 1.10, a method of the form +# `f(..., ::Type{Union{}}, slurp...)` takes precedence over all other methods of `f`, so these calls throw a +# plain `MethodError` instead of an ambiguity error or an error from inside another method. +wrapping_mul(x, ::Type{Union{}}, slurp...) = throw(MethodError(wrapping_mul, (x, Union{}, slurp...))) +saturating_mul(x, ::Type{Union{}}, slurp...) = throw(MethodError(saturating_mul, (x, Union{}, slurp...))) +wrapping_rem(x, ::Type{Union{}}, slurp...) = throw(MethodError(wrapping_rem, (x, Union{}, slurp...))) +saturating_rem(x, ::Type{Union{}}, slurp...) = throw(MethodError(saturating_rem, (x, Union{}, slurp...))) +# `*`, `rem` and the functions of `Base.Checked` belong to Base, so the internal functions they call are guarded +_convert(::Type{Union{}}, slurp...) = throw(MethodError(convert, (Union{}, slurp...))) +_rem(x, ::Type{Union{}}, slurp...) = throw(MethodError(rem, (x, Union{}, slurp...))) + # constructor-style conversions (::Type{X})(x::X) where {X <: FixedPoint} = x (::Type{X})(x::Number) where {X <: FixedPoint} = _convert(X, x) @@ -204,6 +215,7 @@ floattype(::Type{X}) where {T <: LongInts, X <: FixedPoint{T}} = BigFloat floattype(::Type{Complex{T}}) where T = Complex{floattype(T)} floattype(::Type{Base.TwicePrecision{Float64}}) = Float64 # wider would be nice, but hardware support is paramount floattype(::Type{Base.TwicePrecision{T}}) where T<:Union{Float16,Float32} = widen(T) +floattype(::Type{Union{}}, slurp...) = throw(MethodError(floattype, (Union{}, slurp...))) float(x::FixedPoint) = convert(floattype(x), x) @@ -611,6 +623,7 @@ scaledual(::Type{Tdual}, x::Union{Number,AbstractArray{<:Number}}) where Tdual = scaledual(::Type{Tdual}, x::FixedPoint) where Tdual = convert(Tdual, 1/rawone(x)), reinterpret(x) scaledual(::Type{Tdual}, x::AbstractArray{T}) where {Tdual, T <: FixedPoint} = convert(Tdual, 1/rawone(T)), reinterpret(rawtype(T), x) +scaledual(::Type{Union{}}, slurp...) = throw(MethodError(scaledual, (Union{}, slurp...))) @noinline function throw_converterror(::Type{X}, @nospecialize(x)) where X <: FixedPoint nbits = bitwidth(rawtype(X)) diff --git a/src/fixed.jl b/src/fixed.jl index 7f6e476..ce931c4 100644 --- a/src/fixed.jl +++ b/src/fixed.jl @@ -195,7 +195,7 @@ function _floor(x::Fixed{T,f}) where {T <: Signed, f} f == bitwidth(T) && x.i < 0 && throw_converterror(Fixed{T,f}, -1) # TODO: remove this line Fixed{T,f}(x.i & intmask(x), 0) end -_floor(x::Fixed{Union{}, f}) where f = throw(MethodError(_floor, (x,))) +_floor(x::Fixed{Union{}, f}) where f = throw(MethodError(_floor, (x,))) # COV_EXCL_LINE function _ceil(x::Fixed{T,f}) where {T, f} f == 0 && return x upper = typemax(T) & intmask(x) diff --git a/test/traits.jl b/test/traits.jl index c248b4c..0d135a4 100644 --- a/test/traits.jl +++ b/test/traits.jl @@ -18,3 +18,14 @@ struct MyReal <: Real end @test_skip(@test_throws MethodError floattype(MyReal)) # TODO: eliminate `@test_skipped` when depwarn is eliminated. See #177. end + +@testset "`Union{}` as type argument" begin + @test_throws MethodError floattype(Union{}) + @test_throws MethodError scaledual(Union{}, 0.5N0f8) + @test_throws MethodError 0.5 * Union{} + @test_throws MethodError 0.5 % Union{} + for f in (wrapping_mul, saturating_mul, Base.Checked.checked_mul, + wrapping_rem, saturating_rem, Base.Checked.checked_rem) + @test_throws MethodError f(0.5, Union{}) + end +end