Skip to content

Commit 03cfeed

Browse files
committed
fix: Catch ndarray version + float16 incompatibility
1 parent 9b8a75f commit 03cfeed

4 files changed

Lines changed: 98 additions & 8 deletions

File tree

src/ASDF.jl

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,28 @@ function asdf_datatype_yaml(dt::StructuredDatatype)
617617
end
618618
end
619619

620+
"""
621+
uses_float16(dt) -> Bool
622+
623+
Whether the datatype `dt` (including any nested field of a [`StructuredDatatype`](@ref))
624+
is or contains a `Float16`. The `float16` scalar datatype was only added to the ndarray
625+
schema in version 1.1.0, so such arrays must be tagged `!core/ndarray-1.1.0` rather than
626+
`!core/ndarray-1.0.0`. `complex32` is `Complex{Float16}`, so it counts too.
627+
"""
628+
uses_float16(dt::Datatype) = dt === Datatype_float16 || dt === Datatype_complex32
629+
uses_float16(::Union{AsciiDatatype, Ucs4Datatype}) = false
630+
uses_float16(dt::StructuredDatatype) = any(uses_float16(f.datatype) for f in dt.fields)
631+
632+
"""
633+
min_ndarray_version(datatype) -> VersionNumber
634+
635+
Lowest `!core/ndarray-X.Y.Z` schema version that can represent `datatype`: `float16` (and
636+
`complex32`, which contains it) require 1.1.0; every other datatype is valid from 1.0.0. This
637+
is the single source of truth shared by the reader (which rejects too-old tags) and the writer
638+
(which picks the tag version).
639+
"""
640+
min_ndarray_version(datatype) = uses_float16(datatype) ? v"1.1.0" : v"1.0.0"
641+
620642
################################################################################
621643

622644
"""
@@ -781,7 +803,16 @@ function make_construct_yaml_ndarray(block_headers::LazyBlockHeaders)
781803
byteorder = get(mapping, "byteorder", nothing)::Union{Nothing,AbstractString}
782804
offset = get(mapping, "offset", nothing)::Union{Nothing,Integer}
783805
strides = get(mapping, "strides", nothing)::Union{Nothing,AbstractVector{<:Integer}}
784-
return NDArray(block_headers, source, data, shape, datatype, byteorder, offset, strides)
806+
ndarray = NDArray(block_headers, source, data, shape, datatype, byteorder, offset, strides)
807+
# The datatype must be representable by the tag's ndarray schema version. `float16`
808+
# requires >= 1.1.0; `min_ndarray_version` is the single source of truth, shared with
809+
# the writer.
810+
required = min_ndarray_version(ndarray.datatype)
811+
m = match(r"ndarray-(\d+\.\d+\.\d+)$", node.tag)
812+
if m !== nothing && VersionNumber(m[1]) < required
813+
error("`float16` datatype requires `!core/ndarray-$required` or newer, but tag is `$(node.tag)`")
814+
end
815+
return ndarray
785816
end
786817
return construct_yaml_ndarray
787818
end
@@ -1387,8 +1418,11 @@ function load_file(filename::AbstractString; extensions = false, validate_checks
13871418
construct_yaml_ndarray_chunk = make_construct_yaml_ndarray_chunk(lazy_block_headers)
13881419

13891420
asdf_constructors′ = copy(asdf_constructors)
1390-
asdf_constructors′["tag:stsci.edu:asdf/core/ndarray-1.0.0"] = construct_yaml_ndarray
1391-
asdf_constructors′["tag:stsci.edu:asdf/core/ndarray-1.1.0"] = construct_yaml_ndarray
1421+
# One constructor handles every ndarray schema version we accept; the per-datatype minimum
1422+
# is enforced inside it via `min_ndarray_version`.
1423+
for v in (v"1.0.0", v"1.1.0")
1424+
asdf_constructors′["tag:stsci.edu:asdf/core/ndarray-$v"] = construct_yaml_ndarray
1425+
end
13921426
asdf_constructors′["tag:stsci.edu:asdf/core/ndarray-chunk-1.0.0"] = construct_yaml_ndarray_chunk
13931427
asdf_constructors′["tag:stsci.edu:asdf/core/chunked-ndarray-1.0.0"] = construct_yaml_chunked_ndarray
13941428

@@ -1573,7 +1607,9 @@ function YAML._print(io::IO, val::NDArrayWrapper, level::Int=0, ignore_level::Bo
15731607
)
15741608
end
15751609
# println(io, YAML._indent("-\n", level), "!core/chunked-ndarray-1.0.0")
1576-
println(io, "!core/ndarray-1.0.0")
1610+
# Emit the lowest ndarray schema version that can represent this datatype (the reader's
1611+
# matching check in `make_construct_yaml_ndarray` uses the same `min_ndarray_version`).
1612+
println(io, "!core/ndarray-$(min_ndarray_version(datatype))")
15771613
YAML._print(io, ndarray, level, ignore_level)
15781614
end
15791615

0 Bytes
Binary file not shown.

test/data/chunking.asdf

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,25 @@ chunky: !core/chunked-ndarray-1.0.0
1616
chunks:
1717
- !core/ndarray-chunk-1.0.0
1818
start: [0, 0]
19-
ndarray: !core/ndarray-1.0.0
19+
ndarray: !core/ndarray-1.1.0
2020
data: [[11, 12, 13], [21, 22, 23]]
2121
datatype: float16
2222
shape: [2, 3]
2323
- !core/ndarray-chunk-1.0.0
2424
start: [2, 0]
25-
ndarray: !core/ndarray-1.0.0
25+
ndarray: !core/ndarray-1.1.0
2626
data: [[31, 32, 33], [41, 42, 43]]
2727
datatype: float16
2828
shape: [2, 3]
2929
- !core/ndarray-chunk-1.0.0
3030
start: [0, 3]
31-
ndarray: !core/ndarray-1.0.0
31+
ndarray: !core/ndarray-1.1.0
3232
data: [[14], [24]]
3333
datatype: float16
3434
shape: [2, 1]
3535
- !core/ndarray-chunk-1.0.0
3636
start: [2, 3]
37-
ndarray: !core/ndarray-1.0.0
37+
ndarray: !core/ndarray-1.1.0
3838
data: [[34], [44]]
3939
datatype: float16
4040
shape: [2, 1]

test/test-datatypes.jl

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,60 @@ end
6868
@test ASDF.infer_asdf_datatype(Int64) == ASDF.Datatype_int64
6969
end
7070

71+
@testset "float16 requires ndarray-1.1.0" begin
72+
# A `float16` ndarray tagged with the older 1.0.0 schema must be rejected on load,
73+
# since the `float16` scalar datatype only exists from ndarray-1.1.0 onward.
74+
body = """
75+
arr: !core/ndarray-1.0.0
76+
data: [1.0, 2.0]
77+
datatype: float16
78+
shape: [2]
79+
"""
80+
mktempdir() do dir
81+
path = joinpath(dir, "f16.asdf")
82+
open(path, "w") do io
83+
print(io, """
84+
#ASDF 1.0.0
85+
#ASDF_STANDARD 1.6.0
86+
%YAML 1.1
87+
%TAG ! tag:stsci.edu:asdf/
88+
---
89+
!core/asdf-1.1.0
90+
$(body)
91+
...
92+
""")
93+
end
94+
@test_throws "`float16` datatype requires `!core/ndarray-1.1.0` or newer" begin
95+
ASDF.load_file(path)
96+
end
97+
end
98+
99+
# Tagged with 1.1.0, the same array loads cleanly and materializes as `Float16`.
100+
mktempdir() do dir
101+
path = joinpath(dir, "f16ok.asdf")
102+
open(path, "w") do io
103+
print(io, """
104+
#ASDF 1.0.0
105+
#ASDF_STANDARD 1.6.0
106+
%YAML 1.1
107+
%TAG ! tag:stsci.edu:asdf/
108+
---
109+
!core/asdf-1.1.0
110+
arr: !core/ndarray-1.1.0
111+
data: [1.0, 2.0]
112+
datatype: float16
113+
shape: [2]
114+
...
115+
""")
116+
end
117+
af = ASDF.load_file(path)
118+
arr = af["arr"][]
119+
# `==` promotes by value, so pin the element type explicitly too.
120+
@test eltype(arr) == Float16
121+
@test arr == Float16[1.0, 2.0]
122+
end
123+
end
124+
71125
@testset "tagged node accessors" begin
72126
# The `Tagged*` wrappers delegate their collection / string interfaces to the wrapped
73127
# value. Loading round-trips them (see test-read_fallbacks), but the individual delegated

0 commit comments

Comments
 (0)