Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/EmulatedBitIntegers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions src/reductions.jl
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions test/sums.jl
Original file line number Diff line number Diff line change
@@ -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