diff --git a/base/Base.jl b/base/Base.jl index cbfa5ede6aef9..54b4b8f1ae297 100644 --- a/base/Base.jl +++ b/base/Base.jl @@ -369,6 +369,8 @@ include("irrationals.jl") include("mathconstants.jl") using .MathConstants: ℯ, π, pi +include("staticint.jl") + # metaprogramming include("meta.jl") diff --git a/base/staticint.jl b/base/staticint.jl new file mode 100644 index 0000000000000..60a0102d9b5ad --- /dev/null +++ b/base/staticint.jl @@ -0,0 +1,357 @@ + +""" + StaticInt(N::Int) -> StaticInt{N}() + +A statically sized `Int`. Use `StaticInt(N)` instead of `Val(N)` when you want it to behave like a number. +""" +struct StaticInt{N} <: Integer + StaticInt{N}() where {N} = new{N::Int}() + StaticInt(N::Int) = StaticInt{N}() + StaticInt(N::StaticInt) = N + StaticInt(::Val{N}) where {N} = StaticInt(N) + StaticInt(N) = StaticInt(Int(N)) +end + +_dynamic_int(@nospecialize(x::StaticInt))::Int = _dynamic_int(typeof(x)) +_dynamic_int(@nospecialize(x::Type{<:StaticInt}))::Int = x.parameters[1] + +const Zero = StaticInt{0} +const One = StaticInt{1} +const IntType = Union{Int,StaticInt} +IntType(x::Integer) = Int(x) +IntType(x::IntType) = x + +convert(::Type{T}, @nospecialize(N::StaticInt)) where {T<:Number} = convert(T, Int(N)) +Bool(x::StaticInt{0}) = false +Bool(x::StaticInt{1}) = true + +BigInt(@nospecialize(x::StaticInt)) = BigInt(Int(x)) +Integer(x::StaticInt) = x +(::Type{T})(@nospecialize(x::StaticInt)) where {T<:Integer} = T(_dynamic_int(x)) +(::Type{T})(x::Int) where {T<:StaticInt} = StaticInt(x) +convert(::Type{StaticInt{N}}, ::StaticInt{N}) where {N} = StaticInt{N}() + +promote_rule(@nospecialize(T1::Type{<:StaticInt}), ::Type{T2}) where {T2<:Number} = promote_type(Int, T2) +promote_rule(@nospecialize(T1::Type{<:StaticInt}), ::Type{T2}) where {T2<:AbstractIrrational} = promote_type(Int, T2) +for (S, T) in [(:Complex, :Real), (:Rational, :Integer), (:(TwicePrecision), :Any)] + @eval function promote_rule(::Type{$S{T}}, @nospecialize(SI::Type{<:StaticInt})) where {T<:$T} + promote_type($S{T}, Int) + end +end +promote_rule(::Type{Union{Nothing,Missing}}, @nospecialize(T::Type{<:StaticInt})) = Union{Nothing,Missing,Int} +promote_rule(::Type{T1}, @nospecialize(T2::Type{<:StaticInt})) where {T1>:Union{Missing,Nothing}} = promote_type(T1, Int) +promote_rule(::Type{T1}, @nospecialize(T2::Type{<:StaticInt})) where {T1>:Nothing} = promote_type(T1, Int) +promote_rule(::Type{T1}, @nospecialize(T2::Type{<:StaticInt})) where {T1>:Missing} = promote_type(T1, Int) +for T in [:Bool, :Missing, :BigFloat, :BigInt, :Nothing, :Any] + # let S = :Any + @eval begin + promote_rule(@nospecialize(S::Type{<:StaticInt}), ::Type{$T}) = promote_type(Int, $T) + promote_rule(::Type{$T}, @nospecialize(S::Type{<:StaticInt})) = promote_type($T, Int) + end +end +promote_rule(@nospecialize(T1::Type{<:StaticInt}), @nospecialize(T2::Type{<:StaticInt})) = Int + +%(@nospecialize(n::StaticInt), ::Type{Integer}) = Int(n) + +eltype(@nospecialize(T::Type{<:StaticInt})) = Int +iszero(::Zero) = true +iszero(@nospecialize(x::StaticInt)) = false +isone(::One) = true +isone(@nospecialize(x::StaticInt)) = false +zero(@nospecialize(x::Type{<:StaticInt})) = Zero() +one(@nospecialize(x::Type{<:StaticInt})) = One() + +for T in [:Real, :Rational, :Integer] + for f in [:(-), :(+), :(*)] + @eval begin + $(f)(x::$T, @nospecialize(y::StaticInt)) = $(f)(x, Int(y)) + $(f)(@nospecialize(x::StaticInt), y::$T) = $(f)(Int(x), y) + end + end +end +@inline -(::StaticInt{M}) where {M} = StaticInt{-M}() + +for f in [:(+), :(-), :(*), :(÷), :(%), :(<<), :(>>), :(>>>), :(&), :(|), :(⊻)] + @eval begin + @inline $f(::StaticInt{M}, ::StaticInt{N}) where {M,N} = StaticInt{$f(M,N)}() + end +end +for f in [:(<<), :(>>), :(>>>)] + @eval begin + $f(@nospecialize(x::StaticInt), y::UInt) where {M} = $f(Int(x), y) + $f(x::Integer, @nospecialize(y::StaticInt)) = $f(x, Int(y)) + end +end +for f in [:(==), :(!=), :(<), :(≤), :(>), :(≥)] + @eval begin + $f(::StaticInt{M}, ::StaticInt{N}) where {M,N} = $f(M, N) + $f(@nospecialize(x::StaticInt), y::Int) where {M} = $f(Int(x), y) + $f(x::Int, @nospecialize(y::StaticInt)) = $f(x, Int(y)) + end +end + +widen(@nospecialize(x::StaticInt)) = Int(x) + +UnitRange{T}(@nospecialize(start::StaticInt), stop) where {T<:Real} = UnitRange{T}(T(start), T(stop)) +UnitRange{T}(start, @nospecialize(stop::StaticInt)) where {T<:Real} = UnitRange{T}(T(start), T(stop)) +function UnitRange{T}(@nospecialize(start::StaticInt), @nospecialize(stop::StaticInt)) where {T<:Real} + UnitRange{T}(T(start), T(stop)) +end +UnitRange(@nospecialize(start::StaticInt), stop) = UnitRange(Int(start), stop) +UnitRange(start, @nospecialize(stop::StaticInt)) = UnitRange(start, Int(stop)) +function UnitRange(@nospecialize(start::StaticInt), @nospecialize(stop::StaticInt)) + UnitRange(Int(start), Int(stop)) +end + +## ranges + +""" + OptionallyStaticUnitRange(start, stop) <: AbstractUnitRange{Int} + +Similar to `UnitRange` except each field may be an `Int` or `StaticInt`. An +`OptionallyStaticUnitRange` is intended to be constructed internally from other valid +indices. Therefore, users should not expect the same checks are used to ensure construction +of a valid `OptionallyStaticUnitRange` as a `UnitRange`. +""" +struct OptionallyStaticUnitRange{F<:IntType,L<:IntType} <: AbstractUnitRange{Int} + start::F + stop::L + + function OptionallyStaticUnitRange(start::IntType, stop::IntType) + new{typeof(start),typeof(stop)}(start, stop) + end + function OptionallyStaticUnitRange(start::Integer, stop::Integer) + OptionallyStaticUnitRange(IntType(start), IntType(stop)) + end + function OptionallyStaticUnitRange(x::AbstractRange) + step(x) == 1 && return OptionallyStaticUnitRange(maybe_static_first(x), maybe_static_last(x)) + + errmsg(x) = throw(ArgumentError("step must be 1, got $(step(x))")) # avoid GC frame + errmsg(x) + end + OptionallyStaticUnitRange{F,L}(x::AbstractRange) where {F,L} = OptionallyStaticUnitRange(x) + function OptionallyStaticUnitRange{StaticInt{F},StaticInt{L}}() where {F,L} + new{StaticInt{F},StaticInt{L}}() + end +end + +""" + OptionallyStaticStepRange(start, step, stop) <: OrdinalRange{Int,Int} + +Similarly to [`OptionallyStaticUnitRange`](@ref), `OptionallyStaticStepRange` permits +a combination of static and standard primitive `Int`s to construct a range. It +specifically enables the use of ranges without a step size of 1. It may be constructed +through the use of `OptionallyStaticStepRange` directly or using static integers with +the range operator (i.e., `:`). +""" +struct OptionallyStaticStepRange{F<:IntType,S<:IntType,L<:IntType} <: OrdinalRange{Int,Int} + start::F + step::S + stop::L + + function OptionallyStaticStepRange(start::IntType, step::IntType, stop::IntType) + lst = _steprange_last(start, step, stop) + new{typeof(start),typeof(step),typeof(lst)}(start, step, lst) + end + function OptionallyStaticStepRange(start::Integer, step::Integer, stop::Integer) + OptionallyStaticStepRange(IntType(start), IntType(step), IntType(stop)) + end + function OptionallyStaticStepRange(x::AbstractRange) + OptionallyStaticStepRange(maybe_static_first(x), maybe_static_step(x), maybe_static_last(x)) + end +end + +# to make StepRange constructor inlineable, so optimizer can see `step` value +@inline function _steprange_last(start::StaticInt, step::StaticInt, stop::StaticInt) + StaticInt(_steprange_last(Int(start), Int(step), Int(stop))) +end +@inline function _steprange_last(start::Integer, step::StaticInt, stop::StaticInt) + if step === one(step) + # we don't need to check the `stop` if we know it acts like a unit range + return stop + else + return _steprange_last(start, Int(step), Int(stop)) + end +end +@inline function _steprange_last(start::Integer, step::Integer, stop::Integer) + z = zero(step) + if step === z + throw(ArgumentError("step cannot be zero")) + else + if stop == start + return Int(stop) + else + if step > z + if stop > start + return stop - Int(unsigned(stop - start) % step) + else + return Int(start - one(start)) + end + else + if stop > start + return Int(start + one(start)) + else + return stop + Int(unsigned(start - stop) % -step) + end + end + end + end +end + +const OptionallyStaticRange = Union{<:OptionallyStaticUnitRange,<:OptionallyStaticStepRange} + +first(r::OptionallyStaticRange) = Int(r.start) + +step(r::OptionallyStaticStepRange)= Int(r.step) + +last(r::OptionallyStaticRange) = Int(r.stop) + +lastindex(x::OptionallyStaticRange) = length(x) +function length(r::OptionallyStaticUnitRange) + if isempty(r) + return 0 + else + return Int(r.stop - (r.start + One())) + end +end +length(r::OptionallyStaticStepRange) = _range_length(r.start, r.step, r.stop) +_range_length(start, s, stop) = nothing +@inline function _range_length(start::IntType, s::IntType, stop::IntType) + if s > 0 + if stop < start # isempty + return 0 + else + return Int(div(stop - start, s)) + 1 + end + else + if stop > start # isempty + return 0 + else + return Int(div(start - stop, -s)) + 1 + end + end +end + +AbstractUnitRange{Int}(r::OptionallyStaticUnitRange) = r +function AbstractUnitRange{T}(r::OptionallyStaticUnitRange) where {T} + if known_first(r) === 1 && T <: Integer + return OneTo{T}(last(r)) + else + return UnitRange{T}(first(r), last(r)) + end +end + +(:)(start::Integer, stop::StaticInt) = OptionallyStaticUnitRange(start, stop) +(:)(start::StaticInt, stop::Integer) = OptionallyStaticUnitRange(start, stop) +(:)(start::StaticInt, stop::StaticInt) = OptionallyStaticUnitRange(start, stop) +function (:)(start::StaticInt, step::StaticInt, stop::StaticInt) + OptionallyStaticStepRange(start, step, stop) +end +function (:)(start::Integer, step::StaticInt, stop::StaticInt) + OptionallyStaticStepRange(start, step, stop) +end +function (:)(start::StaticInt, step::StaticInt, stop::Integer) + OptionallyStaticStepRange(start, step, stop) +end +function (:)(start::StaticInt, step::Integer, stop::StaticInt) + OptionallyStaticStepRange(start, step, stop) +end +function (:)(start::Integer, step::Integer, stop::StaticInt) + OptionallyStaticStepRange(start, step, stop) +end +function (:)(start::Integer, step::StaticInt, stop::Integer) + OptionallyStaticStepRange(start, step, stop) +end +function (:)(start::StaticInt, step::Integer, stop::Integer) + OptionallyStaticStepRange(start, step, stop) +end +(:)(start::StaticInt, ::StaticInt{1}, stop::StaticInt) = start:stop +(:)(start::Integer, ::StaticInt{1}, stop::StaticInt) = start:stop +(:)(start::StaticInt, ::StaticInt{1}, stop::Integer) = start:stop +function (:)(start::Integer, step::StaticInt{1}, stop::Integer) + OptionallyStaticUnitRange(start, stop) +end +isempty(r::OptionallyStaticUnitRange) = r.start > r.stop +function isempty(r::OptionallyStaticStepRange) + (r.start != r.stop) & ((r.step > 0) != (r.stop > r.start)) +end + +function getindex(r::OptionallyStaticUnitRange, s::AbstractUnitRange{<:Integer}) + @boundscheck checkbounds(r, s) + f = r.start + fnew = f - one(f) + return (fnew+maybe_static_first(s)):(fnew+maybe_static_last(s)) +end + +function getindex(x::OptionallyStaticUnitRange, i::Int)::Int + val = (x.start - One()) + i + @boundscheck ((i < 1) || val > last(x)) && throw(BoundsError(x, i)) + val +end + +## traits +""" + known_first(::Type{T}) -> Union{Int,Nothing} + +If `first` of an instance of type `T` is known at compile time, return it. +Otherwise, return `nothing`. +""" +known_first(x) = known_first(typeof(x)) +known_first(::Type) = nothing +known_first(::Type{OneTo{T}}) where {T} = T(1) +known_first(::Type{<:OptionallyStaticUnitRange{StaticInt{F}}}) where {F} = F::Int +known_first(::Type{<:OptionallyStaticStepRange{StaticInt{F}}}) where {F} = F::Int +known_first(::Type{Slice{T}}) where {T} = known_first(T) +known_first(::Type{IdentityUnitRange{T}}) where {T} = known_first(T) + +""" + known_step(::Type{T}) -> Union{Int,Nothing} + +If `step` of an instance of type `T` is known at compile time, return it. +Otherwise, return `nothing`. +""" +known_step(x) = known_step(typeof(x)) +known_step(::Type) = nothing +known_step(T::Type{<:AbstractUnitRange}) = eltype(T)(1) +known_step(::Type{<:OptionallyStaticStepRange{<:Any,StaticInt{S}}}) where {S} = S::Int + +""" + known_last(::Type{T}) -> Union{Int,Nothing} + +If `last` of an instance of type `T` is known at compile time, return it. +Otherwise, return `nothing`. +""" +known_last(x) = known_last(typeof(x)) +known_last(::Type) = nothing +known_last(::Type{<:OptionallyStaticUnitRange{<:Any,StaticInt{L}}}) where {L} = L::Int +known_last(::Type{<:OptionallyStaticStepRange{<:Any,<:Any,StaticInt{L}}}) where {L} = L::Int +known_last(::Type{Slice{T}}) where {T} = known_last(T) +known_last(::Type{IdentityUnitRange{T}}) where {T} = known_last(T) + +""" + known_length(::Type{T}) -> Union{Int,Nothing} + +If `length` of an instance of type `T` is known at compile time, return it. +Otherwise, return `nothing`. +""" +known_length(x) = known_length(typeof(x)) +known_length(@nospecialize T::Type{<:Tuple}) = nfields(T) +known_length(@nospecialize T::Type{<:NamedTuple}) = fieldcount(T) +known_length(::Type{<:AbstractCartesianIndex{N}}) where {N} = N +function known_length(T::Type{<:AbstractRange}) + _range_length(known_first(T), known_step(T), known_last(T)) +end + +@inline maybe_static_length(x) = maybe_static(known_length, length, x) +@inline maybe_static_first(x) = maybe_static(known_first, first, x) +@inline maybe_static_last(x) = maybe_static(known_last, last, x) +@inline maybe_static_step(x) = maybe_static(known_step, step, x) +@inline function maybe_static(f::F, g::G, x) where {F,G} + L = f(x) + if L === nothing + return g(x) + else + return StaticInt(L) + end +end + diff --git a/test/staticint.jl b/test/staticint.jl new file mode 100644 index 0000000000000..754622ae78876 --- /dev/null +++ b/test/staticint.jl @@ -0,0 +1,78 @@ + +@testset "StaticInt" begin + @test static(UInt(8)) === StaticInt(UInt(8)) === StaticInt{8}() + @test iszero(StaticInt(0)) + @test !iszero(StaticInt(1)) + @test !isone(StaticInt(0)) + @test isone(StaticInt(1)) + @test @inferred(one(StaticInt(1))) === StaticInt(1) + @test @inferred(zero(StaticInt(1))) === StaticInt(0) + @test @inferred(one(StaticInt)) === StaticInt(1) + @test @inferred(zero(StaticInt)) === StaticInt(0) === StaticInt(StaticInt(Val(0))) + @test eltype(one(StaticInt)) <: Int + + x = StaticInt(1) + @test @inferred(Bool(x)) isa Bool + @test @inferred(BigInt(x)) isa BigInt + @test @inferred(Integer(x)) === x + @test @inferred(%(x, Integer)) === 1 + # test for ambiguities and correctness + for i ∈ Any[StaticInt(0), StaticInt(1), StaticInt(2), 3] + for j ∈ Any[StaticInt(0), StaticInt(1), StaticInt(2), 3] + i === j === 3 && continue + for f ∈ [+, -, *, ÷, %, <<, >>, >>>, &, |, ⊻, ==, ≤, ≥] + (iszero(j) && ((f === ÷) || (f === %))) && continue # integer division error + @test convert(Int, @inferred(f(i,j))) == f(convert(Int, i), convert(Int, j)) + end + end + i == 3 && break + for f ∈ [+, -, *, /, ÷, %, ==, ≤, ≥] + w = f(convert(Int, i), 1.4) + x = f(1.4, convert(Int, i)) + @test convert(typeof(w), @inferred(f(i, 1.4))) === w + @test convert(typeof(x), @inferred(f(1.4, i))) === x # if f is division and i === StaticInt(0), returns `NaN`; hence use of ==== in check. + (((f === ÷) || (f === %)) && (i === StaticInt(0))) && continue + y = f(convert(Int, i), 2 // 7) + z = f(2 // 7, convert(Int, i)) + @test convert(typeof(y), @inferred(f(i, 2 // 7))) === y + @test convert(typeof(z), @inferred(f(2 // 7, i))) === z + end + end + + @test UnitRange{Int16}(StaticInt(-9), 17) === Int16(-9):Int16(17) + @test UnitRange{Int16}(-7, StaticInt(19)) === Int16(-7):Int16(19) + @test UnitRange(-11, StaticInt(15)) === -11:15 + @test UnitRange(StaticInt(-11), 15) === -11:15 + @test UnitRange{Int}(StaticInt(-11), StaticInt(15)) === -11:15 + @test UnitRange(StaticInt(-11), StaticInt(15)) === -11:15 + @test float(StaticInt(8)) === static(8.0) + + # test specific promote rules to ensure we don't cause ambiguities + SI = StaticInt{1} + IR = typeof(1//1) + PI = typeof(pi) + @test @inferred(convert(SI, SI())) === SI() + @test @inferred(promote_rule(SI, PI)) <: promote_type(Int, PI) + @test @inferred(promote_rule(SI, IR)) <: promote_type(Int, IR) + @test @inferred(promote_rule(SI, SI)) <: Int + @test @inferred(promote_rule(Missing, SI)) <: promote_type(Missing, Int) + @test @inferred(promote_rule(Nothing, SI)) <: promote_type(Nothing, Int) + @test @inferred(promote_rule(SI, Missing)) <: promote_type(Int, Missing) + @test @inferred(promote_rule(SI, Nothing)) <: promote_type(Int, Nothing) + @test @inferred(promote_rule(Union{Missing,Int}, SI)) <: promote_type(Union{Missing,Int}, Int) + @test @inferred(promote_rule(Union{Nothing,Int}, SI)) <: promote_type(Union{Nothing,Int}, Int) + @test @inferred(promote_rule(Union{Nothing,Missing,Int}, SI)) <: Union{Nothing,Missing,Int} + @test @inferred(promote_rule(Union{Nothing,Missing}, SI)) <: promote_type(Union{Nothing,Missing}, Int) + @test @inferred(promote_rule(SI, Missing)) <: promote_type(Int, Missing) + @test @inferred(promote_rule(Base.TwicePrecision{Int}, StaticInt{1})) <: Base.TwicePrecision{Int} + + @test static(Int8(-18)) === static(-18) + @test static(0xef) === static(239) + @test static(Int16(-18)) === static(-18) + @test static(0xffef) === static(65519) + if sizeof(Int) == 8 + @test static(Int32(-18)) === static(-18) + @test static(0xffffffef) === static(4294967279) + end +end +