Skip to content

Commit

Permalink
Refactor rounding (including bugfixes) and document API (#50812)
Browse files Browse the repository at this point in the history
  • Loading branch information
LilithHafner authored Aug 31, 2023
1 parent a3e2316 commit e0f5247
Show file tree
Hide file tree
Showing 15 changed files with 200 additions and 136 deletions.
1 change: 0 additions & 1 deletion base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ include(strcat((length(Core.ARGS)>=2 ? Core.ARGS[2] : ""), "version_git.jl")) #
# numeric operations
include("hashing.jl")
include("rounding.jl")
using .Rounding
include("div.jl")
include("rawbigints.jl")
include("float.jl")
Expand Down
10 changes: 5 additions & 5 deletions base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,8 @@ TypeError(where, @nospecialize(expected::Type), @nospecialize(got)) =
TypeError(Symbol(where), "", expected, got)
struct InexactError <: Exception
func::Symbol
T # Type
val
InexactError(f::Symbol, @nospecialize(T), @nospecialize(val)) = (@noinline; new(f, T, val))
args
InexactError(f::Symbol, @nospecialize(args...)) = (@noinline; new(f, args))
end
struct OverflowError <: Exception
msg::AbstractString
Expand Down Expand Up @@ -630,8 +629,6 @@ eval(Core, :(NamedTuple{names,T}(args::T) where {names, T <: Tuple} =

import .Intrinsics: eq_int, trunc_int, lshr_int, sub_int, shl_int, bitcast, sext_int, zext_int, and_int

throw_inexacterror(f::Symbol, ::Type{T}, val) where {T} = (@noinline; throw(InexactError(f, T, val)))

function is_top_bit_set(x)
@inline
eq_int(trunc_int(UInt8, lshr_int(x, sub_int(shl_int(sizeof(x), 3), 1))), trunc_int(UInt8, 1))
Expand All @@ -642,6 +639,9 @@ function is_top_bit_set(x::Union{Int8,UInt8})
eq_int(lshr_int(x, 7), trunc_int(typeof(x), 1))
end

#TODO delete this function (but see #48097):
throw_inexacterror(args...) = throw(InexactError(args...))

function check_top_bit(::Type{To}, x) where {To}
@inline
is_top_bit_set(x) && throw_inexacterror(:check_top_bit, To, x)
Expand Down
6 changes: 4 additions & 2 deletions base/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,10 @@ end

function showerror(io::IO, ex::InexactError)
print(io, "InexactError: ", ex.func, '(')
nameof(ex.T) === ex.func || print(io, ex.T, ", ")
print(io, ex.val, ')')
T = first(ex.args)
nameof(T) === ex.func || print(io, T, ", ")
join(io, ex.args[2:end], ", ")
print(io, ")")
Experimental.show_error_hints(io, ex)
end

Expand Down
32 changes: 13 additions & 19 deletions base/float.jl
Original file line number Diff line number Diff line change
Expand Up @@ -436,21 +436,15 @@ unsafe_trunc(::Type{UInt128}, x::Float16) = unsafe_trunc(UInt128, Float32(x))
unsafe_trunc(::Type{Int128}, x::Float16) = unsafe_trunc(Int128, Float32(x))

# matches convert methods
# also determines floor, ceil, round
trunc(::Type{Signed}, x::IEEEFloat) = trunc(Int,x)
trunc(::Type{Unsigned}, x::IEEEFloat) = trunc(UInt,x)
trunc(::Type{Integer}, x::IEEEFloat) = trunc(Int,x)

# Bool
trunc(::Type{Bool}, x::AbstractFloat) = (-1 < x < 2) ? 1 <= x : throw(InexactError(:trunc, Bool, x))
floor(::Type{Bool}, x::AbstractFloat) = (0 <= x < 2) ? 1 <= x : throw(InexactError(:floor, Bool, x))
ceil(::Type{Bool}, x::AbstractFloat) = (-1 < x <= 1) ? 0 < x : throw(InexactError(:ceil, Bool, x))
round(::Type{Bool}, x::AbstractFloat) = (-0.5 <= x < 1.5) ? 0.5 < x : throw(InexactError(:round, Bool, x))

round(x::IEEEFloat, r::RoundingMode{:ToZero}) = trunc_llvm(x)
round(x::IEEEFloat, r::RoundingMode{:Down}) = floor_llvm(x)
round(x::IEEEFloat, r::RoundingMode{:Up}) = ceil_llvm(x)
round(x::IEEEFloat, r::RoundingMode{:Nearest}) = rint_llvm(x)
# also determines trunc, floor, ceil
round(::Type{Signed}, x::IEEEFloat, r::RoundingMode) = round(Int, x, r)
round(::Type{Unsigned}, x::IEEEFloat, r::RoundingMode) = round(UInt, x, r)
round(::Type{Integer}, x::IEEEFloat, r::RoundingMode) = round(Int, x, r)

round(x::IEEEFloat, ::RoundingMode{:ToZero}) = trunc_llvm(x)
round(x::IEEEFloat, ::RoundingMode{:Down}) = floor_llvm(x)
round(x::IEEEFloat, ::RoundingMode{:Up}) = ceil_llvm(x)
round(x::IEEEFloat, ::RoundingMode{:Nearest}) = rint_llvm(x)

## floating point promotions ##
promote_rule(::Type{Float32}, ::Type{Float16}) = Float32
Expand Down Expand Up @@ -931,11 +925,11 @@ for Ti in (Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UIn
# directly. `Tf(typemax(Ti))+1` is either always exactly representable, or
# rounded to `Inf` (e.g. when `Ti==UInt128 && Tf==Float32`).
@eval begin
function trunc(::Type{$Ti},x::$Tf)
function round(::Type{$Ti},x::$Tf,::RoundingMode{:ToZero})
if $(Tf(typemin(Ti))-one(Tf)) < x < $(Tf(typemax(Ti))+one(Tf))
return unsafe_trunc($Ti,x)
else
throw(InexactError(:trunc, $Ti, x))
throw(InexactError(:round, $Ti, x, RoundToZero))
end
end
function (::Type{$Ti})(x::$Tf)
Expand All @@ -955,11 +949,11 @@ for Ti in (Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UIn
# be rounded up. This assumes that `Tf(typemin(Ti)) > -Inf`, which is true for
# these types, but not for `Float16` or larger integer types.
@eval begin
function trunc(::Type{$Ti},x::$Tf)
function round(::Type{$Ti},x::$Tf,::RoundingMode{:ToZero})
if $(Tf(typemin(Ti))) <= x < $(Tf(typemax(Ti)))
return unsafe_trunc($Ti,x)
else
throw(InexactError(:trunc, $Ti, x))
throw(InexactError(:round, $Ti, x, RoundToZero))
end
end
function (::Type{$Ti})(x::$Tf)
Expand Down
19 changes: 0 additions & 19 deletions base/floatfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@ isinteger(x::AbstractFloat) = (x - trunc(x) == 0)

# See rounding.jl for docstring.

function round(::Type{T}, x::AbstractFloat, r::RoundingMode) where {T<:Integer}
r != RoundToZero && (x = round(x,r))
trunc(T, x)
end

# NOTE: this relies on the current keyword dispatch behaviour (#9498).
function round(x::Real, r::RoundingMode=RoundNearest;
digits::Union{Nothing,Integer}=nothing, sigdigits::Union{Nothing,Integer}=nothing, base::Union{Nothing,Integer}=nothing)
Expand All @@ -77,20 +72,6 @@ function round(x::Real, r::RoundingMode=RoundNearest;
end
end

trunc(x::Real; kwargs...) = round(x, RoundToZero; kwargs...)
floor(x::Real; kwargs...) = round(x, RoundDown; kwargs...)
ceil(x::Real; kwargs...) = round(x, RoundUp; kwargs...)

# fallbacks
trunc(::Type{T}, x::Real; kwargs...) where {T} = round(T, x, RoundToZero; kwargs...)
floor(::Type{T}, x::Real; kwargs...) where {T} = round(T, x, RoundDown; kwargs...)
ceil(::Type{T}, x::Real; kwargs...) where {T} = round(T, x, RoundUp; kwargs...)
round(::Type{T}, x::Real; kwargs...) where {T} = round(T, x, RoundNearest; kwargs...)

round(::Type{T}, x::Real, r::RoundingMode) where {T} = convert(T, round(x, r))

round(x::Integer, r::RoundingMode) = x

# round x to multiples of 1/invstep
function _round_invstep(x, invstep, r::RoundingMode)
y = round(x * invstep, r) / invstep
Expand Down
2 changes: 1 addition & 1 deletion base/gmp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ function BigInt(x::Float64)
unsafe_trunc(BigInt,x)
end

function trunc(::Type{BigInt}, x::Union{Float16,Float32,Float64})
function round(::Type{BigInt}, x::Union{Float16,Float32,Float64}, r::RoundingMode{:ToZero})
isfinite(x) || throw(InexactError(:trunc, BigInt, x))
unsafe_trunc(BigInt,x)
end
Expand Down
64 changes: 0 additions & 64 deletions base/int.jl
Original file line number Diff line number Diff line change
Expand Up @@ -629,70 +629,6 @@ mod(x::Integer, ::Type{T}) where {T<:Integer} = rem(x, T)

unsafe_trunc(::Type{T}, x::Integer) where {T<:Integer} = rem(x, T)

"""
trunc([T,] x)
trunc(x; digits::Integer= [, base = 10])
trunc(x; sigdigits::Integer= [, base = 10])
`trunc(x)` returns the nearest integral value of the same type as `x` whose absolute value
is less than or equal to the absolute value of `x`.
`trunc(T, x)` converts the result to type `T`, throwing an `InexactError` if the truncated
value is not representable a `T`.
Keywords `digits`, `sigdigits` and `base` work as for [`round`](@ref).
See also: [`%`](@ref rem), [`floor`](@ref), [`unsigned`](@ref), [`unsafe_trunc`](@ref).
# Examples
```jldoctest
julia> trunc(2.22)
2.0
julia> trunc(-2.22, digits=1)
-2.2
julia> trunc(Int, -2.22)
-2
```
"""
function trunc end

"""
floor([T,] x)
floor(x; digits::Integer= [, base = 10])
floor(x; sigdigits::Integer= [, base = 10])
`floor(x)` returns the nearest integral value of the same type as `x` that is less than or
equal to `x`.
`floor(T, x)` converts the result to type `T`, throwing an `InexactError` if the floored
value is not representable a `T`.
Keywords `digits`, `sigdigits` and `base` work as for [`round`](@ref).
"""
function floor end

"""
ceil([T,] x)
ceil(x; digits::Integer= [, base = 10])
ceil(x; sigdigits::Integer= [, base = 10])
`ceil(x)` returns the nearest integral value of the same type as `x` that is greater than or
equal to `x`.
`ceil(T, x)` converts the result to type `T`, throwing an `InexactError` if the ceiled
value is not representable as a `T`.
Keywords `digits`, `sigdigits` and `base` work as for [`round`](@ref).
"""
function ceil end

round(::Type{T}, x::Integer) where {T<:Integer} = convert(T, x)
trunc(::Type{T}, x::Integer) where {T<:Integer} = convert(T, x)
floor(::Type{T}, x::Integer) where {T<:Integer} = convert(T, x)
ceil(::Type{T}, x::Integer) where {T<:Integer} = convert(T, x)

## integer construction ##

"""
Expand Down
13 changes: 0 additions & 13 deletions base/missing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -150,19 +150,6 @@ round(::Type{T}, x::Real, r::RoundingMode=RoundNearest) where {T>:Missing} = rou
round(::Type{T}, x::Rational{Tr}, r::RoundingMode=RoundNearest) where {T>:Missing,Tr} = round(nonmissingtype_checked(T), x, r)
round(::Type{T}, x::Rational{Bool}, r::RoundingMode=RoundNearest) where {T>:Missing} = round(nonmissingtype_checked(T), x, r)

# Handle ceil, floor, and trunc separately as they have no RoundingMode argument
for f in (:(ceil), :(floor), :(trunc))
@eval begin
($f)(::Missing; sigdigits::Integer=0, digits::Integer=0, base::Integer=0) = missing
($f)(::Type{>:Missing}, ::Missing) = missing
($f)(::Type{T}, ::Missing) where {T} = throw(MissingException(missing_conversion_msg(T)))
($f)(::Type{T}, x::Any) where {T>:Missing} = $f(nonmissingtype_checked(T), x)
# to fix ambiguities
($f)(::Type{T}, x::Rational) where {T>:Missing} = $f(nonmissingtype_checked(T), x)
($f)(::Type{T}, x::Real) where {T>:Missing} = $f(nonmissingtype_checked(T), x)
end
end

# to avoid ambiguity warnings
(^)(::Missing, ::Integer) = missing

Expand Down
11 changes: 4 additions & 7 deletions base/mpfr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -380,18 +380,15 @@ round(::Type{T}, x::BigFloat, r::RoundingMode) where T<:Union{Signed, Unsigned}
invoke(round, Tuple{Type{<:Union{Signed, Unsigned}}, BigFloat, Union{RoundingMode, MPFRRoundingMode}}, T, x, r)
round(::Type{BigInt}, x::BigFloat, r::RoundingMode) =
invoke(round, Tuple{Type{BigInt}, BigFloat, Union{RoundingMode, MPFRRoundingMode}}, BigInt, x, r)
round(::Type{<:Integer}, x::BigFloat, r::RoundingMode) = throw(MethodError(round, (Integer, x, r)))


unsafe_trunc(::Type{T}, x::BigFloat) where {T<:Integer} = unsafe_trunc(T, _unchecked_cast(T, x, RoundToZero))
unsafe_trunc(::Type{BigInt}, x::BigFloat) = _unchecked_cast(BigInt, x, RoundToZero)

# TODO: Ideally the base fallbacks for these would already exist
for (f, rnd) in zip((:trunc, :floor, :ceil, :round),
(RoundToZero, RoundDown, RoundUp, :(ROUNDING_MODE[])))
@eval $f(::Type{T}, x::BigFloat) where T<:Union{Unsigned, Signed, BigInt} = round(T, x, $rnd)
@eval $f(::Type{Integer}, x::BigFloat) = $f(BigInt, x)
end
round(::Type{T}, x::BigFloat) where T<:Integer = round(T, x, ROUNDING_MODE[])
# these two methods are split to increase their precedence in disambiguation:
round(::Type{Integer}, x::BigFloat, r::RoundingMode) = round(BigInt, x, r)
round(::Type{Integer}, x::BigFloat, r::MPFRRoundingMode) = round(BigInt, x, r)

function Bool(x::BigFloat)
iszero(x) && return false
Expand Down
85 changes: 83 additions & 2 deletions base/rounding.jl
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ function _convert_rounding(::Type{T}, x::Real, r::RoundingMode{:ToZero}) where T
end
end

# Default definitions

"""
set_zero_subnormals(yes::Bool) -> Bool
Expand Down Expand Up @@ -313,8 +315,8 @@ for IEEE arithmetic, and `true` if they might be converted to zeros.
get_zero_subnormals() = ccall(:jl_get_zero_subnormals,Int32,())!=0

end #module
using .Rounding

# Docstring listed here so it appears above the complex docstring.
"""
round([T,] x, [r::RoundingMode])
round(x, [r::RoundingMode]; digits::Integer=0, base = 10)
Expand Down Expand Up @@ -388,4 +390,83 @@ julia> round(357.913; sigdigits=4, base=2)
To extend `round` to new numeric types, it is typically sufficient to define `Base.round(x::NewType, r::RoundingMode)`.
"""
round(T::Type, x)
function round end

"""
trunc([T,] x)
trunc(x; digits::Integer= [, base = 10])
trunc(x; sigdigits::Integer= [, base = 10])
`trunc(x)` returns the nearest integral value of the same type as `x` whose absolute value
is less than or equal to the absolute value of `x`.
`trunc(T, x)` converts the result to type `T`, throwing an `InexactError` if the truncated
value is not representable a `T`.
Keywords `digits`, `sigdigits` and `base` work as for [`round`](@ref).
To support `trunc` for a new type, define `Base.round(x::NewType, ::RoundingMode{:ToZero})`.
See also: [`%`](@ref rem), [`floor`](@ref), [`unsigned`](@ref), [`unsafe_trunc`](@ref).
# Examples
```jldoctest
julia> trunc(2.22)
2.0
julia> trunc(-2.22, digits=1)
-2.2
julia> trunc(Int, -2.22)
-2
```
"""
function trunc end

"""
floor([T,] x)
floor(x; digits::Integer= [, base = 10])
floor(x; sigdigits::Integer= [, base = 10])
`floor(x)` returns the nearest integral value of the same type as `x` that is less than or
equal to `x`.
`floor(T, x)` converts the result to type `T`, throwing an `InexactError` if the floored
value is not representable a `T`.
Keywords `digits`, `sigdigits` and `base` work as for [`round`](@ref).
To support `floor` for a new type, define `Base.round(x::NewType, ::RoundingMode{:Down})`.
"""
function floor end

"""
ceil([T,] x)
ceil(x; digits::Integer= [, base = 10])
ceil(x; sigdigits::Integer= [, base = 10])
`ceil(x)` returns the nearest integral value of the same type as `x` that is greater than or
equal to `x`.
`ceil(T, x)` converts the result to type `T`, throwing an `InexactError` if the ceiled
value is not representable as a `T`.
Keywords `digits`, `sigdigits` and `base` work as for [`round`](@ref).
To support `ceil` for a new type, define `Base.round(x::NewType, ::RoundingMode{:Up})`.
"""
function ceil end

trunc(x; kws...) = round(x, RoundToZero; kws...)
floor(x; kws...) = round(x, RoundDown; kws...)
ceil(x; kws...) = round(x, RoundUp; kws...)
round(x; kws...) = round(x, RoundNearest; kws...)

trunc(::Type{T}, x) where T = round(T, x, RoundToZero)
floor(::Type{T}, x) where T = round(T, x, RoundDown)
ceil(::Type{T}, x) where T = round(T, x, RoundUp)
round(::Type{T}, x) where T = round(T, x, RoundNearest)

round(::Type{T}, x, r::RoundingMode) where T = convert(T, round(x, r))

round(x::Integer, r::RoundingMode) = x
2 changes: 1 addition & 1 deletion doc/src/base/math.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Base.exp10
Base.Math.ldexp
Base.Math.modf
Base.expm1
Base.round(::Type, ::Any)
Base.round
Base.Rounding.RoundingMode
Base.Rounding.RoundNearest
Base.Rounding.RoundNearestTiesAway
Expand Down
Loading

0 comments on commit e0f5247

Please sign in to comment.