Skip to content

Fix floattype() for non-concrete types while preserving the depwarn - #362

Open
kimikage wants to merge 1 commit into
JuliaMath:masterfrom
kimikage:fix_floattype_re
Open

kimikage wants to merge 1 commit into
JuliaMath:masterfrom
kimikage:fix_floattype_re

Conversation

@kimikage

Copy link
Copy Markdown
Collaborator

This is a variant of PR #354 with deprecated fallback method retained.

While it would be hypocritical not to call this a breaking change, it does allow us to avoid a direct violation of SemVer.
In other words, I plan to release this in the v0.9 series.

@codecov

codecov Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.74%. Comparing base (d62abf3) to head (bd93684).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #362      +/-   ##
==========================================
- Coverage   96.75%   96.74%   -0.01%     
==========================================
  Files           7        7              
  Lines         800      799       -1     
==========================================
- Hits          774      773       -1     
  Misses         26       26              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@PatrickHaecker PatrickHaecker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would currently violate the promised interface, but the fix seems plausible. LGTM with the fix.

I still haven't understood why this is better than just doing a 0.10 release in the next days, but that is irrelevant in terms of correctness.

Comment thread src/FixedPointNumbers.jl
floattype(::Type{X}) where {T <: ShortInts, X <: FixedPoint{T}} = Float32
floattype(::Type{X}) where {T <: Integer, X <: FixedPoint{T}} = Float64
floattype(::Type{X}) where {T <: LongInts, X <: FixedPoint{T}} = BigFloat
floattype(::Type{<:FixedPoint}) = BigFloat # fallback for `FixedPoint`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your proposal would return BigFloat for Union{N0f8, N0f16}, which in my read violates the docstring: "In general the returned type should have the minimum bitwidth needed to encode the full precision of the input type."

Not perfect, but I think better:

Suggested change
floattype(::Type{<:FixedPoint}) = BigFloat # fallback for `FixedPoint`
floattype(::Type{T}) where {T <: FixedPoint} =
T isa Union ? promote_type(map(floattype, Base.uniontypes(T))...) : BigFloat

@kimikage kimikage Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as non-concrete types are concerned, I don't think that document makes much sense.
BTW, have you read this comment? #343 (comment)
As you've probably noticed, we can create various edge cases (e.g., Union{N0f8, Float32}).

Comment thread test/traits.jl
Comment on lines +8 to +17
@test floattype(FixedPoint) === BigFloat
@test floattype(Normed) === BigFloat
@test floattype(Fixed) === BigFloat
@test floattype(FixedPoint{Int8}) === Float32
@test floattype(Normed{UInt32}) === Float64
@test floattype(Fixed{Int64}) === BigFloat
@test floattype(FixedPoint{MyInteger}) === Float64
@test floattype(FixedPoint{Union{},1}) === BigFloat
@test floattype(Normed{Unsigned,2}) === BigFloat
@test floattype(Fixed{T,3} where {T<:Signed}) === BigFloat

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add few non-trivial Union cases, too.

Comment thread src/FixedPointNumbers.jl
Comment on lines 151 to 192
"""
floattype(::Type{T})::Type{<:AbstractFloat}

Return a minimal type suitable for performing computations with instances of type `T` without integer overflow.

The fallback definition of `floattype(T)` applies only to `T<:AbstractFloat`.
However, it is permissible to extend `floattype` to return types that are not subtypes of
`AbstractFloat`; the key characteristic is that the return type should support computation without integer overflow.

In general the returned type should have the minimum bitwidth needed to encode the full precision of the input type.
however, a priority should be placed on computational efficiency; consequently, types like `Float16` should be avoided
except in scenarios where they are guaranteed to have hardware support.

# Examples

A classic usage is to avoid overflow behavior by promoting `FixedPoint` to `AbstractFloat`

```jldoctest; setup = :(using FixedPointNumbers)
julia> x = N0f8(1.0)
1.0N0f8

julia> x + x # overflow
0.996N0f8

julia> T = floattype(x)
Float32

julia> T(x) + T(x)
2.0f0
```

The following represents a valid extension of `floattype` to non-AbstractFloats:

```julia
julia> using FixedPointNumbers, ColorTypes

julia> floattype(RGB{N0f8})
RGB{Float32}
```

`RGB` itself is not a subtype of `AbstractFloat`, but unlike `RGB{N0f8}` operations with `RGB{Float32}` are not subject to integer overflow.
"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring had some weaknesses and should describe the new situation explicitly:

Suggested change
"""
floattype(::Type{T})
floattype(x::FixedPoint)
Return a minimal type suitable for performing computations with instances of type `T` without integer overflow.
For a `Real` type `T`, the returned type is a subtype of `AbstractFloat`. In general it has the minimum bitwidth
needed to encode the full precision of `T`. However, computational efficiency takes priority, so the returned type
may be wider. For example, `floattype(N0f8)` is currently `Float32` rather than `Float16`.
For a non-concrete `FixedPoint` type, the returned type suits all of its concrete subtypes. A `Union` of `FixedPoint`
types gives the promoted type of its members. Any other non-concrete `FixedPoint` type gives `floattype(R)` if its raw
type `R` is concrete, and `BigFloat` otherwise.
Methods are defined for `AbstractFloat`, `Integer`, `Rational`, `AbstractIrrational` and `FixedPoint` types. Other
`Real` types currently return `T` itself with a deprecation warning, and will throw a `MethodError` in a future
release. Packages may extend `floattype` to types that are not `Real`, as long as the returned type supports
computation without integer overflow.
# Examples
A classic usage is to avoid overflow by converting `FixedPoint` values to a floating-point type:
```jldoctest; setup = :(using FixedPointNumbers)
julia> x = N0f8(1.0)
1.0N0f8
julia> x + x # overflow
0.996N0f8
julia> T = floattype(x)
Float32
julia> T(x) + T(x)
2.0f0

Non-concrete FixedPoint types:

julia> floattype(Normed{UInt8})
Float32

julia> floattype(Union{N0f8, N0f16})
Float32

julia> floattype(Normed)
BigFloat

The following represents a valid extension of floattype to types that are not subtypes of AbstractFloat:

julia> using FixedPointNumbers, ColorTypes

julia> floattype(RGB{N0f8})
RGB{Float32}

RGB itself is not a subtype of AbstractFloat, but unlike RGB{N0f8}, operations with RGB{Float32} are not subject
to integer overflow.
"""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from the fact that it's long, I think the proposal isn't bad.
However, I don't understand exactly what purpose it serves.
This document is primarily intended for package developers. I don't know of any examples where floattype is actually being used outside of ColorTypes and its downstream packages.

I think the top-down definition of floattype should be discussed in JuliaLang/julia rather than in this package.

@kimikage

Copy link
Copy Markdown
Collaborator Author

@PatrickHaecker
Before we get into the implementation discussion, please let me know where you're willing to compromise.
I'm fine as long as an AbstractFloat tyoe is returned and it doesn't conflict with the current implementation for concrete types.

@PatrickHaecker

Copy link
Copy Markdown
Collaborator

@kimikage
The problem is that as long as the call fails, we can later define whatever we want without a breaking release. As long as it is deprecated, we can at least do it in a nicer way with a (single) major release. As soon as we commit to BigFloat, that ship has sailed (and deprecating and changing it should be two major releases). So I'd like each result we introduce now to be the one we want to keep.

Union{N0f8, Float32} is not a FixedPoint, so it stay in my proposal on the deprecated Real fallback.

The promote decision is one of the most important one, as this is so often called implicitly. BigFloat really should be the last resort, as it's immediately allocating and therefore at least of the kind of real-time programming I am mostly involved in a direct show-stopper. As FixedPointNumber is especially useful when performance matters, that is a hard sell.

Base only seems to promote to BigFloat if either BigInt or BigFloat are already involved. So even

julia> promote_type(Int128, Float16)
Float16

That seems a bit extreme to me, but that is what we should go for if we want to be as compatible as possible to Base.

Base is indeed very peculiar:

julia> promote_type(Union{Int8, Int16}, Float32)
Real

So we probably don't want to strive for maximum Base compatibility here, to not reproduce the bugs Base has:

w = vcat(Int8[100], Int32[3])
w[1] + w[1]                  # 200: the Int8 was converted to Int32

w = vcat(Int16[100], Int32[3])
w[1] + w[1]                  # 200: the Int16 was converted to Int32

a = Union{Int8, Int16}[Int8(100)]
v = vcat(a, Int32[3])        # eltype Signed, elements Int8, Int16, Int32
v[1] + v[1]                  # -56: Int8 arithmetic overflows

I reported that. Should we maybe see whether that report provides any insights?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants