diff --git a/README.md b/README.md index b602636..acabfd4 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,29 @@ If you `@emulate` an already defined type, nothing will be done (not even any ou Zero-bit types are rejected: `@emulate Int0` and `@emulate UInt0` throw an `ArgumentError`, since a 0-bit integer carries no information. +## Summation + +General `sum` reductions widen emulated integers whose logical width is less +than the machine word size to `Int` or `UInt`, according to signedness. This +also applies to types with explicitly oversized storage. Wider emulated +integers retain their type and modular arithmetic. + +```julia +@emulate Int3 +sum(Int3[3, 3]) # 6 :: Int, without overflowing Int3 +``` + +This behavior supports arrays, mapped sums, and iterators, including nonempty +containers with abstract or union element types. Empty same-signed unions of +small emulated types also produce a machine-word zero. Containers without a +suitable empty identity still require `init`. Specialized range sums retain +Base's range behavior. + +Support on current Julia releases uses private Base reduction hooks. A +compatibility check verifies their availability and built-in widening behavior +before installing the extensions during precompilation. Incompatible hooks +cause an explicit error rather than silently disabling widening. + ## Querying and converting types The package exposes a few helpers usable on both emulated and standard integers. diff --git a/src/EmulatedBitIntegers.jl b/src/EmulatedBitIntegers.jl index 4afe5d8..8564c3b 100644 --- a/src/EmulatedBitIntegers.jl +++ b/src/EmulatedBitIntegers.jl @@ -40,6 +40,7 @@ include("IntegerType.jl") include("interface.jl") include("methods.jl") include("emulate.jl") +include("reductions.jl") # `emulate` only builds an `Expr` (no `eval`), so calling it here exercises the macro pipeline without defining real types or polluting any module. Cover the four distinct branches: unsigned default-storage, signed default-storage, explicit non-default storage, and `redundant_storage_request` (suffix matches the default → produces a `const` alias). @compile_workload begin diff --git a/src/reductions.jl b/src/reductions.jl new file mode 100644 index 0000000..3d530a1 --- /dev/null +++ b/src/reductions.jl @@ -0,0 +1,46 @@ +""" + sum_hooks_compatible() + +Check the private reduction hooks required for small-integer sum widening. + +This check runs before installing the extensions during package precompilation. +Integration tests verify that `sum` actually uses these hooks. +""" +function sum_hooks_compatible() + all(name -> isdefined(Base, name) && getfield(Base, name) isa Function, + (:add_sum, :reduce_first, :reduce_empty)) || return false + add = Base.add_sum + first = Base.reduce_first + empty = Base.reduce_empty + for (Small, Accumulator, left, right) in ((Int8, Int, 100, 28), (UInt8, UInt, 200, 100)) + applicable(add, Small(left), Small(right)) || return false + applicable(first, add, Small(1)) || return false + applicable(empty, add, Small) || return false + add(Small(left), Small(right)) === Accumulator(left + right) || return false + first(add, Small(1)) === Accumulator(1) || return false + empty(add, Small) === zero(Accumulator) || return false + end + return true +end + +function sumtype(::Type{T}) where T<:EmulatedInteger + if isconcretetype(T) + return bits(T) < bits(Int) ? (T <: Signed ? Int : UInt) : T + elseif T isa Union + targets = map(sumtype, Base.uniontypes(T)) + allequal(targets) && return first(targets) + end + return T +end +sumvalue(x::Number) = x +sumvalue(x::EmulatedInteger) = sumtype(typeof(x))(x) + +if sum_hooks_compatible() + Base.add_sum(x::EmulatedInteger, y::Real) = sumvalue(x) + sumvalue(y) + Base.add_sum(x::Real, y::EmulatedInteger) = sumvalue(x) + sumvalue(y) + Base.add_sum(x::EmulatedInteger, y::EmulatedInteger) = sumvalue(x) + sumvalue(y) + Base.reduce_first(::typeof(Base.add_sum), x::EmulatedInteger) = sumvalue(x) + Base.reduce_empty(::typeof(Base.add_sum), ::Type{T}) where T<:EmulatedInteger = zero(sumtype(T)) +else + error("Unsupported Base sum internals for Julia $VERSION") +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 8a838ab..1b5bcd4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,6 +16,7 @@ values(x) = x |> fieldvalues |> collect @emulate(UInt1_64, UInt3_64, Int4_8, Int4_16, Int7_16, Int20_32) include("comparisons.jl") +include("sums.jl") # ============================================================================ # Static analysis diff --git a/test/sums.jl b/test/sums.jl new file mode 100644 index 0000000..8c13f16 --- /dev/null +++ b/test/sums.jl @@ -0,0 +1,57 @@ +@testset "sum widening" begin + @emulate Int3 UInt3 Int9 UInt9 Int3_128 UInt3_128 Int63 UInt63 Int65 UInt65 + + @test EmulatedBitIntegers.sum_hooks_compatible() + + for Element in (Int3, UInt3, Int9, UInt9, Int3_128, UInt3_128, Int63, UInt63, Int65, UInt65) + Accumulator = bits(Element) < bits(Int) ? Element <: Signed ? Int : UInt : Element + for count in (0, 1, 2, 17, 2048) + values = fill(Element(1), count) + expected = count % Accumulator + @test sum(values) === expected + @test sum(identity, values) === expected + @test sum(value for value in values) === expected + @test sum(Iterators.filter(_ -> true, values)) === expected + @test sum(values; init=zero(Accumulator)) === expected + @test sum(reshape(values, count, 1); dims=1) == fill(expected, 1, 1) + @test eltype(sum(reshape(values, count, 1); dims=1)) === Accumulator + if count > 0 + @test sum(Integer[values...]) === expected + @test sum(Any[values...]) === expected + end + end + end + + @test sum(Int3[3, 3]) === 6 + @test sum(UInt3[7, 7]) === UInt(14) + @test (@inferred sum(Int3[3, 3])) === 6 + @test (@inferred sum(UInt3[7, 7])) === UInt(14) + @test sum(fill(Int3(3), 2, 3); dims=2) == fill(9, 2, 1) + @test eltype(sum(fill(Int3(3), 2, 3); dims=2)) === Int + @test sum(fill(UInt3(7), 2, 3); dims=2) == fill(UInt(21), 2, 1) + @test eltype(sum(fill(UInt3(7), 2, 3); dims=2)) === UInt + @test sum(Union{Int3,Int9}[Int3(3) Int9(4)]; dims=2) == fill(7, 1, 1) + @test eltype(sum(Union{Int3,Int9}[Int3(3) Int9(4)]; dims=2)) === Int + @test sum(Integer[Int3(3) Int9(4)]; dims=2) == fill(7, 1, 1) + @test sum(Any[Int3(3) Int9(4)]; dims=2) == fill(7, 1, 1) + @test sum(Union{Int3,Int9}[Int3(3), Int9(4)]) === 7 + @test sum(Union{Int,Int3}[Int3(3), 4]) === 7 + @test sum(Union{UInt3,UInt9}[UInt3(7), UInt9(8)]) === UInt(15) + @test sum(Union{Int3,Int9}[]) === 0 + @test sum(Union{UInt3,UInt9}[]) === UInt(0) + @test sum(Union{Int,Int3}[]) === 0 + @test_throws MethodError sum(Union{Int3,UInt3}[]) + @test_throws MethodError sum(EmulatedBitIntegers.EmulatedInteger[]) + @test sum(Int3[1, 2]; init=0.5) === 3.5 + @test sum(Int3[1, 2]; init=Int3(0)) === 3 + @test sum(Int3[]; init=Int3(0)) === Int3(0) + @test sum(Int3[1, 2]; init=big(0)) == big(3) + @test sum(Int3[1, 2]; init=0//1) === 3//1 + @test sum(Int3[1, 2]; init=0 + 1im) === 3 + 1im + @test_throws MethodError sum(Any[]) + @test sum(Any[]; init=0) === 0 + @test sum(Integer[]) === sum(Integer[]; init=0) + @test sum(value -> Int3(value), 1:3) === 6 + @test sum(Int3(1):Int3(3)) === 6 + @test sum(UInt3(1):UInt3(3)) === sum(UInt8(1):UInt8(3)) +end \ No newline at end of file