Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Round-trip reintrepretation of all bits types #47116

Merged
merged 25 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
898dc8f
WIP: make `reinterpret` work on structs
Jul 23, 2019
dc41f6e
Reinterpret packed bits
BioTurboNick Jun 17, 2022
d3ceb24
Fixed nested padding calc
BioTurboNick Nov 13, 2021
26a768a
Reorganize and remove allocations from nested
BioTurboNick Nov 13, 2021
088b6db
Add `assume_effects`
BioTurboNick Oct 10, 2022
1016e8c
Add tests, fix tests, change to ArgumentError
BioTurboNick Oct 12, 2022
3823876
Merge branch 'master' into reinterpret-all-ext
BioTurboNick Dec 20, 2022
26ccc19
Export PaddingException
BioTurboNick Jun 22, 2023
fd2019a
Merge branch 'master' into reinterpret-all-ext
BioTurboNick Jun 22, 2023
fe923ec
Replace calls to `_memcpy!` with `memcpy` to reflect #49550
BioTurboNick Jun 22, 2023
c2a765a
Return SimpleVector to ensure the function really is `:foldable`
BioTurboNick Jun 22, 2023
5fb9c1e
Refine docstring
BioTurboNick Jun 22, 2023
f7af68d
Narrow signature to proper types
BioTurboNick Jun 22, 2023
122c82b
Simplify packing test
BioTurboNick Jun 22, 2023
ecfb138
Back to UInt - needed because fieldoffset returns UInt
BioTurboNick Jun 22, 2023
706fccb
Back to UInt
BioTurboNick Jun 22, 2023
9dee0c6
UInt(0) specifically
BioTurboNick Jun 22, 2023
8b35d55
Error, not Exception
BioTurboNick Jun 23, 2023
d74d153
More specific exception
BioTurboNick Jun 23, 2023
de70127
Okay actually ArgumentError
BioTurboNick Jun 23, 2023
2cad1b9
Fix tests
BioTurboNick Jun 23, 2023
afea3ab
Merge branch 'master' into reinterpret-all-ext
BioTurboNick Jun 23, 2023
4e4d13b
Add docstring
BioTurboNick Jun 23, 2023
16e83ed
Merge branch 'master' into reinterpret-all-ext
BioTurboNick Jun 27, 2023
e1d2413
Remove export, back to `Int` as better API
BioTurboNick Jun 28, 2023
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
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export
InvalidStateException,
KeyError,
MissingException,
PaddingError,
BioTurboNick marked this conversation as resolved.
Show resolved Hide resolved
ProcessFailedException,
TaskFailedException,
SystemError,
Expand Down
140 changes: 131 additions & 9 deletions base/reinterpretarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -651,8 +651,8 @@ end

# Padding
struct Padding
offset::Int
size::Int
offset::Int # 0-indexed offset of the next valid byte; sizeof(T) indicates trailing padding
size::Int # bytes of padding before a valid byte
end
function intersect(p1::Padding, p2::Padding)
start = max(p1.offset, p2.offset)
Expand Down Expand Up @@ -696,20 +696,24 @@ function iterate(cp::CyclePadding, state::Tuple)
end

"""
Compute the location of padding in a type.
Compute the location of padding in an isbits datatype. Recursive over the fields of that type.
"""
function padding(T)
padding = Padding[]
last_end::Int = 0
@assume_effects :foldable function padding(T::DataType, baseoffset::UInt = UInt(0))
BioTurboNick marked this conversation as resolved.
Show resolved Hide resolved
pads = Padding[]
last_end::Int = baseoffset
for i = 1:fieldcount(T)
offset = fieldoffset(T, i)
offset = baseoffset + fieldoffset(T, i)
BioTurboNick marked this conversation as resolved.
Show resolved Hide resolved
fT = fieldtype(T, i)
append!(pads, padding(fT, offset))
if offset != last_end
push!(padding, Padding(offset, offset-last_end))
push!(pads, Padding(offset, offset-last_end))
end
last_end = offset + sizeof(fT)
end
padding
if 0 < last_end - baseoffset < sizeof(T)
push!(pads, Padding(baseoffset + sizeof(T), sizeof(T) - last_end + baseoffset))
end
return Core.svec(pads...)
end

function CyclePadding(T::DataType)
Expand Down Expand Up @@ -748,6 +752,124 @@ end
return true
end

@assume_effects :foldable function struct_subpadding(::Type{Out}, ::Type{In}) where {Out, In}
padding(Out) == padding(In)
end

@assume_effects :foldable function packedsize(::Type{T}) where T
pads = padding(T)
return sizeof(T) - sum((p.size for p ∈ pads), init = 0)
end

@assume_effects :foldable ispacked(::Type{T}) where T = isempty(padding(T))

function _copytopacked!(ptr_out::Ptr{Out}, ptr_in::Ptr{In}) where {Out, In}
writeoffset = 0
for i ∈ 1:fieldcount(In)
readoffset = fieldoffset(In, i)
fT = fieldtype(In, i)
if ispacked(fT)
readsize = sizeof(fT)
memcpy(ptr_out + writeoffset, ptr_in + readoffset, readsize)
writeoffset += readsize
else # nested padded type
_copytopacked!(ptr_out + writeoffset, Ptr{fT}(ptr_in + readoffset))
writeoffset += packedsize(fT)
end
end
end

function _copyfrompacked!(ptr_out::Ptr{Out}, ptr_in::Ptr{In}) where {Out, In}
readoffset = 0
for i ∈ 1:fieldcount(Out)
writeoffset = fieldoffset(Out, i)
fT = fieldtype(Out, i)
if ispacked(fT)
writesize = sizeof(fT)
memcpy(ptr_out + writeoffset, ptr_in + readoffset, writesize)
readoffset += writesize
else # nested padded type
_copyfrompacked!(Ptr{fT}(ptr_out + writeoffset), ptr_in + readoffset)
readoffset += packedsize(fT)
end
end
end

"""
reinterpret(::Type{Out}, x::In)

Reinterpret the valid non-padding bytes of an isbits value `x` as isbits type `Out`.

Both types must have the same amount of non-padding bytes. This operation is guaranteed
to be reversible.

```jldoctest
julia> reinterpret(NTuple{2, UInt8}, 0x1234)
(0x34, 0x12)

julia> reinterpret(UInt16, (0x34, 0x12))
0x1234

julia> reinterpret(Tuple{UInt16, UInt8}, (0x01, 0x0203))
(0x0301, 0x02)
```

!!! warning

Use caution if some combinations of bits in `Out` are not considered valid and would
otherwise be prevented by the type's constructors and methods. Unexpected behavior
may result without additional validation.
"""
@inline function reinterpret(::Type{Out}, x::In) where {Out, In}
isbitstype(Out) || throw(ArgumentError("Target type for `reinterpret` must be isbits"))
isbitstype(In) || throw(ArgumentError("Source type for `reinterpret` must be isbits"))
if isprimitivetype(Out) && isprimitivetype(In)
outsize = sizeof(Out)
insize = sizeof(In)
outsize == insize ||
throw(ArgumentError("Sizes of types $Out and $In do not match; got $outsize \
and $insize, respectively."))
return bitcast(Out, x)
end
inpackedsize = packedsize(In)
outpackedsize = packedsize(Out)
inpackedsize == outpackedsize ||
throw(ArgumentError("Packed sizes of types $Out and $In do not match; got $outpackedsize \
and $inpackedsize, respectively."))
in = Ref{In}(x)
out = Ref{Out}()
if struct_subpadding(Out, In)
# if packed the same, just copy
GC.@preserve in out begin
ptr_in = unsafe_convert(Ptr{In}, in)
ptr_out = unsafe_convert(Ptr{Out}, out)
memcpy(ptr_out, ptr_in, sizeof(Out))
end
return out[]
else
# mismatched padding
GC.@preserve in out begin
ptr_in = unsafe_convert(Ptr{In}, in)
ptr_out = unsafe_convert(Ptr{Out}, out)

if fieldcount(In) > 0 && ispacked(Out)
_copytopacked!(ptr_out, ptr_in)
elseif fieldcount(Out) > 0 && ispacked(In)
_copyfrompacked!(ptr_out, ptr_in)
else
packed = Ref{NTuple{inpackedsize, UInt8}}()
GC.@preserve packed begin
ptr_packed = unsafe_convert(Ptr{NTuple{inpackedsize, UInt8}}, packed)
_copytopacked!(ptr_packed, ptr_in)
_copyfrompacked!(ptr_out, ptr_packed)
end
end
end
return out[]
end
end


# Reductions with IndexSCartesian2

function _mapreduce(f::F, op::OP, style::IndexSCartesian2{K}, A::AbstractArrayOrBroadcasted) where {F,OP,K}
Expand Down
2 changes: 1 addition & 1 deletion test/core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1897,7 +1897,7 @@ function f4528(A, B)
end
end
@test f4528(false, Int32(12)) === nothing
@test_throws ErrorException f4528(true, Int32(12))
@test_throws ArgumentError f4528(true, Int32(12))

# issue #4518
f4518(x, y::Union{Int32,Int64}) = 0
Expand Down
8 changes: 3 additions & 5 deletions test/numbers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2216,13 +2216,11 @@ end
@test round(Int16, -32768.1) === Int16(-32768)
end
# issue #7508
@test_throws ErrorException reinterpret(Int, 0x01)
@test_throws ArgumentError reinterpret(Int, 0x01)

@testset "issue #12832" begin
@test_throws ErrorException reinterpret(Float64, Complex{Int64}(1))
@test_throws ErrorException reinterpret(Float64, ComplexF32(1))
@test_throws ErrorException reinterpret(ComplexF32, Float64(1))
@test_throws ErrorException reinterpret(Int32, false)
@test_throws ArgumentError reinterpret(Float64, Complex{Int64}(1))
@test_throws ArgumentError reinterpret(Int32, false)
end
# issue #41
ndigf(n) = Float64(log(Float32(n)))
Expand Down
30 changes: 26 additions & 4 deletions test/reinterpretarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -450,10 +450,10 @@ end
SomeSingleton(x) = new()
end

@test_throws ErrorException reinterpret(Int, nothing)
@test_throws ErrorException reinterpret(Missing, 3)
@test_throws ErrorException reinterpret(Missing, NotASingleton())
@test_throws ErrorException reinterpret(NotASingleton, ())
@test_throws ArgumentError reinterpret(Int, nothing)
@test_throws ArgumentError reinterpret(Missing, 3)
@test_throws ArgumentError reinterpret(Missing, NotASingleton())
@test_throws ArgumentError reinterpret(NotASingleton, ())

@test_throws ArgumentError reinterpret(NotASingleton, fill(nothing, ()))
@test_throws ArgumentError reinterpret(reshape, NotASingleton, fill(missing, 3))
Expand Down Expand Up @@ -513,3 +513,25 @@ end
@test setindex!(x, SomeSingleton(:), 3, 5) == x2
@test_throws MethodError x[2,4] = nothing
end

# reinterpret of arbitrary bitstypes
@testset "Reinterpret arbitrary bitstypes" begin
struct Bytes15
a::Int8
b::Int16
c::Int32
d::Int64
end

@test reinterpret(Float64, ComplexF32(1, 1)) === 0.007812501848093234
@test reinterpret(ComplexF32, 0.007812501848093234) === ComplexF32(1, 1)
@test reinterpret(Tuple{Float64, Float64}, ComplexF64(1, 1)) === (1.0, 1.0)
@test reinterpret(ComplexF64, (1.0, 1.0)) === ComplexF64(1, 1)
@test reinterpret(Tuple{Int8, Int16, Int32, Int64}, (Int64(1), Int32(2), Int16(3), Int8(4))) === (Int8(1), Int16(0), Int32(0), 288233674686595584)
@test reinterpret(Tuple{Int8, Int16, Tuple{Int32, Int64}}, (Int64(1), Int32(2), Int16(3), Int8(4))) === (Int8(1), Int16(0), (Int32(0), 288233674686595584))
@test reinterpret(Tuple{Int64, Int32, Int16, Int8}, (Int8(1), Int16(0), (Int32(0), 288233674686595584))) === (Int64(1), Int32(2), Int16(3), Int8(4))
@test reinterpret(Tuple{Int8, Int16, Int32, Int64}, Bytes15(Int8(1), Int16(2), Int32(3), Int64(4))) === (Int8(1), Int16(2), Int32(3), Int64(4))
@test reinterpret(Bytes15, (Int8(1), Int16(2), Int32(3), Int64(4))) == Bytes15(Int8(1), Int16(2), Int32(3), Int64(4))

@test_throws ArgumentError reinterpret(Tuple{Int32, Int64}, (Int16(1), Int64(4)))
end