Skip to content

Commit da88ba7

Browse files
nsajkovtjnashaplavin
authored
replace constructorof with something not UB (#102)
* replace constructorof with something not UB It is confusing that `constructorof` is documented as returning the constructor of `T`, which is normally defined to be `T`, and then typically does not return `T`, and so does not usually construct `T`. However, this PR is intended just to remove the undefined behavior here of using an unsound generated function (examining mutable state with getfield). * Update ConstructionBase.jl * Update ConstructionBase.jl * Update ConstructionBase.jl * add pre to CI * upd ci * drop support for unmaintained versions of Julia * delete remaining branching for unsupported versions of Julia * remove pieces specific to old julia versions --------- Co-authored-by: Jameson Nash <vtjnash@gmail.com> Co-authored-by: Alexander Plavin <alexander@plav.in>
1 parent b987640 commit da88ba7

File tree

6 files changed

+78
-153
lines changed

6 files changed

+78
-153
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,13 @@ jobs:
1414
strategy:
1515
matrix:
1616
version:
17-
- '1.0'
18-
- '1.6'
1917
- '1.10'
2018
- '1'
19+
- 'pre'
2120
os:
2221
- ubuntu-latest
2322
- macOS-latest
2423
- windows-latest
25-
exclude:
26-
- os: macOS-latest # Apple Silicon
27-
version: '1.0'
28-
- os: macOS-latest # Apple Silicon
29-
version: '1.6'
3024
steps:
3125
- uses: actions/checkout@v4
3226
- uses: julia-actions/setup-julia@v2

.github/workflows/Downstream.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
fail-fast: false
1414
matrix:
15-
julia-version: ['1.6', '1.10', '1']
15+
julia-version: ['1.10', '1', 'pre']
1616
os: [ubuntu-latest]
1717
package:
1818
- Accessors
@@ -21,7 +21,7 @@ jobs:
2121
- Flatten
2222
steps:
2323
- uses: actions/checkout@v2
24-
- uses: julia-actions/setup-julia@v1
24+
- uses: julia-actions/setup-julia@v2
2525
with:
2626
version: ${{ matrix.julia-version }}
2727
arch: x64

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ ConstructionBaseStaticArraysExt = "StaticArrays"
1919
[compat]
2020
IntervalSets = "0.5, 0.6, 0.7"
2121
StaticArrays = "1"
22-
julia = "1"
22+
julia = "1.10"
2323
LinearAlgebra = "<0.0.1,1"
2424

2525
[extras]

src/ConstructionBase.jl

Lines changed: 16 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ for (name, path) in [
2626
end
2727
end
2828

29-
@generated function constructorof(::Type{T}) where T
30-
getfield(parentmodule(T), nameof(T))
31-
end
32-
29+
constructorof(T::Type) = Base.typename(T).wrapper
3330
constructorof(::Type{<:Tuple}) = tuple
3431
constructorof(::Type{<:NamedTuple{names}}) where names =
3532
NamedTupleConstructor{names}()
@@ -48,40 +45,14 @@ getfields(x::NamedTuple) = x
4845
getproperties(o::NamedTuple) = o
4946
getproperties(o::Tuple) = o
5047

51-
if VERSION >= v"1.7"
52-
function check_properties_are_fields(obj)
53-
# for ntuples of symbols `===` is semantically the same as `==`
54-
# but triple equals is easier for the compiler to optimize, see #82
55-
if propertynames(obj) !== fieldnames(typeof(obj))
56-
error("""
57-
The `$(nameof(typeof(obj)))` type defines custom properties: it has `propertynames` overloaded.
58-
Please define `ConstructionBase.setproperties(::$(nameof(typeof(obj))), ::NamedTuple)` to set its properties.
59-
""")
60-
end
61-
end
62-
else
63-
function is_propertynames_overloaded(T::Type)::Bool
64-
T <: NamedTuple && return false
65-
which(propertynames, Tuple{T}).sig !== Tuple{typeof(propertynames), Any}
66-
end
67-
68-
@generated function check_properties_are_fields(obj)
69-
if is_propertynames_overloaded(obj)
70-
return quote
71-
T = typeof(obj)
72-
msg = """
73-
The function `Base.propertynames` was overloaded for type `$T`.
74-
Please make sure the following methods are also overloaded for this type:
75-
```julia
76-
ConstructionBase.setproperties
77-
ConstructionBase.getproperties # optional in VERSION >= julia v1.7
78-
```
79-
"""
80-
error(msg)
81-
end
82-
else
83-
:(nothing)
84-
end
48+
function check_properties_are_fields(obj)
49+
# for ntuples of symbols `===` is semantically the same as `==`
50+
# but triple equals is easier for the compiler to optimize, see #82
51+
if propertynames(obj) !== fieldnames(typeof(obj))
52+
error("""
53+
The `$(nameof(typeof(obj)))` type defines custom properties: it has `propertynames` overloaded.
54+
Please define `ConstructionBase.setproperties(::$(nameof(typeof(obj))), ::NamedTuple)` to set its properties.
55+
""")
8556
end
8657
end
8758

@@ -102,27 +73,13 @@ end
10273
# otherwise: throw an error
10374
tuple_or_ntuple(::Type, names, vals) = error("Only Int and Symbol propertynames are supported")
10475

105-
if VERSION >= v"1.7"
106-
function getproperties(obj)
107-
fnames = propertynames(obj)
108-
tuple_or_ntuple(fnames, getproperty.((obj,), fnames))
109-
end
110-
function getfields(obj::T) where {T}
111-
fnames = fieldnames(T)
112-
NamedTuple{fnames}(getfield.((obj,), fnames))
113-
end
114-
else
115-
@generated function getfields(obj)
116-
fnames = fieldnames(obj)
117-
fvals = map(fnames) do fname
118-
Expr(:call, :getfield, :obj, QuoteNode(fname))
119-
end
120-
:(NamedTuple{$fnames}(($(fvals...),)))
121-
end
122-
function getproperties(obj)
123-
check_properties_are_fields(obj)
124-
getfields(obj)
125-
end
76+
function getproperties(obj)
77+
fnames = propertynames(obj)
78+
tuple_or_ntuple(fnames, getproperty.((obj,), fnames))
79+
end
80+
function getfields(obj::T) where {T}
81+
fnames = fieldnames(T)
82+
NamedTuple{fnames}(getfield.((obj,), fnames))
12683
end
12784

12885
################################################################################
@@ -211,8 +168,4 @@ end
211168
include("nonstandard.jl")
212169
include("functions.jl")
213170

214-
if !isdefined(Base, :get_extension)
215-
include("../ext/ConstructionBaseLinearAlgebraExt.jl")
216-
end
217-
218171
end # module

src/nonstandard.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@ linrange_constructor(start, stop, len, lendiv) = LinRange(start, stop, len)
3636
constructorof(::Type{<:LinRange}) = linrange_constructor
3737

3838
### Expr: args get splatted
39-
# ::Expr annotation is to make it type-stable on Julia 1.3-
39+
# ::Expr annotation is to make it type-stable on Julia 1.3-, probably not necessary anymore
4040
constructorof(::Type{<:Expr}) = (head, args) -> Expr(head, args...)::Expr

test/runtests.jl

Lines changed: 57 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -341,15 +341,7 @@ Base.getproperty(obj::FieldProps, name::Symbol) = getproperty(getfield(obj, :com
341341
@test occursin("setproperties", msg)
342342
@test occursin("FieldProps", msg)
343343
# == FieldProps((a="aaa", b=:b)
344-
if VERSION >= v"1.7"
345-
@test getproperties(x) == (a=1, b=:b)
346-
else
347-
res = @test_throws ErrorException getproperties(x)
348-
msg = sprint(showerror, res.value)
349-
@test occursin("overload", msg)
350-
@test occursin("getproperties", msg)
351-
@test occursin("FieldProps", msg)
352-
end
344+
@test getproperties(x) == (a=1, b=:b)
353345
end
354346

355347

@@ -361,24 +353,20 @@ Base.getproperty(s::SProp, prop::Symbol) = "ps$prop"
361353
Base.getproperty(s::SProp, prop::Int) = "pi$prop"
362354
Base.getproperty(s::SProp, prop::String) = "pstr$prop"
363355

364-
if VERSION >= v"1.7"
365-
# automatic getproperties() supported only on 1.7+
356+
@testset "properties can be numbered" begin
357+
@test getproperties(SProp((:a, :b))) === (a="psa", b="psb")
358+
@test getproperties(SProp((1, 2))) === ("pi1", "pi2")
359+
# what should it return?
360+
@test_broken getproperties(SProp(("a", "b")))
366361

367-
@testset "properties can be numbered" begin
368-
@test getproperties(SProp((:a, :b))) === (a="psa", b="psb")
369-
@test getproperties(SProp((1, 2))) === ("pi1", "pi2")
370-
# what should it return?
371-
@test_broken getproperties(SProp(("a", "b")))
372-
373-
@test_throws ErrorException getproperties(SProp((1, :a)))
374-
end
362+
@test_throws ErrorException getproperties(SProp((1, :a)))
363+
end
375364

376-
@testset "propertynames can be a vector" begin
377-
@test getproperties(SProp([:a, :b])) === (a="psa", b="psb")
378-
@test getproperties(SProp(Symbol[])) === (;)
379-
@test getproperties(SProp([1, 2])) === ("pi1", "pi2")
380-
@test getproperties(SProp(Int[])) === ()
381-
end
365+
@testset "propertynames can be a vector" begin
366+
@test getproperties(SProp([:a, :b])) === (a="psa", b="psb")
367+
@test getproperties(SProp(Symbol[])) === (;)
368+
@test getproperties(SProp([1, 2])) === ("pi1", "pi2")
369+
@test getproperties(SProp(Int[])) === ()
382370
end
383371

384372
function funny_numbers(::Type{Tuple}, n)::Tuple
@@ -484,11 +472,7 @@ end
484472
@testset "no allocs S2" begin
485473
obj = S2(3, UInt32(5))
486474
@test 0 == hot_loop_allocs(constructorof, typeof(obj))
487-
if VERSION < v"1.6"
488-
@test 32 hot_loop_allocs(setproperties, obj, (; a = nothing, b = Int32(6)))
489-
else
490-
@test 0 == hot_loop_allocs(setproperties, obj, (; a = nothing, b = Int32(6)))
491-
end
475+
@test 0 == hot_loop_allocs(setproperties, obj, (; a = nothing, b = Int32(6)))
492476
end
493477

494478
@testset "inference" begin
@@ -524,10 +508,8 @@ end
524508
@inferred getfields(nt)
525509

526510
@inferred constructorof(typeof(nt))
527-
if VERSION >= v"1.3"
528-
content = funny_numbers(NamedTuple,n)
529-
@inferred reconstruct(nt, content)
530-
end
511+
content = funny_numbers(NamedTuple,n)
512+
@inferred reconstruct(nt, content)
531513
#no_allocs_test(nt, content)
532514
for k in 0:n
533515
nt2 = funny_numbers(NamedTuple, k)
@@ -549,12 +531,10 @@ end
549531
@inferred constructorof(S1)
550532
@inferred constructorof(S20)
551533
@inferred constructorof(S40)
552-
if VERSION >= v"1.3"
553-
@inferred reconstruct(funny_numbers(S,0) , funny_numbers(Tuple,0))
554-
@inferred reconstruct(funny_numbers(S,1) , funny_numbers(Tuple,1))
555-
@inferred reconstruct(funny_numbers(S,20), funny_numbers(Tuple,20))
556-
@inferred reconstruct(funny_numbers(S,40), funny_numbers(Tuple,40))
557-
end
534+
@inferred reconstruct(funny_numbers(S,0) , funny_numbers(Tuple,0))
535+
@inferred reconstruct(funny_numbers(S,1) , funny_numbers(Tuple,1))
536+
@inferred reconstruct(funny_numbers(S,20), funny_numbers(Tuple,20))
537+
@inferred reconstruct(funny_numbers(S,40), funny_numbers(Tuple,40))
558538

559539
@inferred getfields(funny_numbers(S,0))
560540
@inferred getfields(funny_numbers(S,1))
@@ -569,45 +549,43 @@ end
569549

570550
using StaticArrays, IntervalSets
571551

572-
if isdefined(Base, :get_extension) # some 1.9 version
573-
@testset "staticarrays" begin
574-
sa = @SVector [2, 4, 6, 8]
575-
sa2 = ConstructionBase.constructorof(typeof(sa))((3.0, 5.0, 7.0, 9.0))
576-
@test sa2 === @SVector [3.0, 5.0, 7.0, 9.0]
577-
578-
ma = @MMatrix [2.0 4.0; 6.0 8.0]
579-
ma2 = @inferred ConstructionBase.constructorof(typeof(ma))((1, 2, 3, 4))
580-
@test ma2 isa MArray{Tuple{2,2},Int,2,4}
581-
@test all(ma2 .=== @MMatrix [1 3; 2 4])
582-
583-
sz = SizedArray{Tuple{2,2}}([1 2;3 4])
584-
sz2 = @inferred ConstructionBase.constructorof(typeof(sz))([:a :b; :c :d])
585-
@test sz2 == SizedArray{Tuple{2,2}}([:a :b; :c :d])
586-
@test typeof(sz2) <: SizedArray{Tuple{2,2},Symbol,2,2}
587-
588-
for T in (SVector, MVector)
589-
@test @inferred(ConstructionBase.constructorof(T)((1, 2, 3)))::T == T((1, 2, 3))
590-
@test @inferred(ConstructionBase.constructorof(T{3})((1, 2, 3)))::T == T((1, 2, 3))
591-
@test @inferred(ConstructionBase.constructorof(T{3})((1, 2)))::T == T((1, 2))
592-
@test @inferred(ConstructionBase.constructorof(T{3, Symbol})((1, 2, 3)))::T == T((1, 2, 3))
593-
@test @inferred(ConstructionBase.constructorof(T{3, Symbol})((1, 2)))::T == T((1, 2))
594-
@test @inferred(ConstructionBase.constructorof(T{3, X} where {X})((1, 2, 3)))::T == T((1, 2, 3))
595-
@test @inferred(ConstructionBase.constructorof(T{3, X} where {X})((1, 2)))::T == T((1, 2))
596-
@test @inferred(ConstructionBase.constructorof(T{X, Symbol} where {X})((1, 2, 3)))::T == T((1, 2, 3))
597-
end
598-
599-
sv = SVector(1, 2)
600-
@test SVector(3.0, 2.0) === @inferred setproperties(sv, x = 3.0)
601-
@test SVector(3.0, 5.0) === @inferred setproperties(sv, x = 3.0, y = 5.0)
602-
@test SVector(-1.0, -2.0) === @inferred setproperties(sv, data = (-1.0, -2))
603-
@test_throws "does not have properties z" setproperties(sv, z = 3.0)
604-
@test_throws "does not have properties z" setproperties(SVector(1, 2, 3, 4, 5), z = 3.0)
552+
@testset "staticarrays" begin
553+
sa = @SVector [2, 4, 6, 8]
554+
sa2 = ConstructionBase.constructorof(typeof(sa))((3.0, 5.0, 7.0, 9.0))
555+
@test sa2 === @SVector [3.0, 5.0, 7.0, 9.0]
556+
557+
ma = @MMatrix [2.0 4.0; 6.0 8.0]
558+
ma2 = @inferred ConstructionBase.constructorof(typeof(ma))((1, 2, 3, 4))
559+
@test ma2 isa MArray{Tuple{2,2},Int,2,4}
560+
@test all(ma2 .=== @MMatrix [1 3; 2 4])
561+
562+
sz = SizedArray{Tuple{2,2}}([1 2;3 4])
563+
sz2 = @inferred ConstructionBase.constructorof(typeof(sz))([:a :b; :c :d])
564+
@test sz2 == SizedArray{Tuple{2,2}}([:a :b; :c :d])
565+
@test typeof(sz2) <: SizedArray{Tuple{2,2},Symbol,2,2}
566+
567+
for T in (SVector, MVector)
568+
@test @inferred(ConstructionBase.constructorof(T)((1, 2, 3)))::T == T((1, 2, 3))
569+
@test @inferred(ConstructionBase.constructorof(T{3})((1, 2, 3)))::T == T((1, 2, 3))
570+
@test @inferred(ConstructionBase.constructorof(T{3})((1, 2)))::T == T((1, 2))
571+
@test @inferred(ConstructionBase.constructorof(T{3, Symbol})((1, 2, 3)))::T == T((1, 2, 3))
572+
@test @inferred(ConstructionBase.constructorof(T{3, Symbol})((1, 2)))::T == T((1, 2))
573+
@test @inferred(ConstructionBase.constructorof(T{3, X} where {X})((1, 2, 3)))::T == T((1, 2, 3))
574+
@test @inferred(ConstructionBase.constructorof(T{3, X} where {X})((1, 2)))::T == T((1, 2))
575+
@test @inferred(ConstructionBase.constructorof(T{X, Symbol} where {X})((1, 2, 3)))::T == T((1, 2, 3))
605576
end
606577

607-
@testset "intervalsets" begin
608-
@test constructorof(typeof(1..2))(0.5, 1.5) === 0.5..1.5
609-
@test constructorof(typeof(OpenInterval(1, 2)))(0.5, 1.5) === OpenInterval(0.5, 1.5)
610-
@test setproperties(1..2, left=0.0) === 0.0..2.0
611-
@test setproperties(OpenInterval(1.0, 2.0), left=1, right=5) === OpenInterval(1, 5)
612-
end
578+
sv = SVector(1, 2)
579+
@test SVector(3.0, 2.0) === @inferred setproperties(sv, x = 3.0)
580+
@test SVector(3.0, 5.0) === @inferred setproperties(sv, x = 3.0, y = 5.0)
581+
@test SVector(-1.0, -2.0) === @inferred setproperties(sv, data = (-1.0, -2))
582+
@test_throws "does not have properties z" setproperties(sv, z = 3.0)
583+
@test_throws "does not have properties z" setproperties(SVector(1, 2, 3, 4, 5), z = 3.0)
584+
end
585+
586+
@testset "intervalsets" begin
587+
@test constructorof(typeof(1..2))(0.5, 1.5) === 0.5..1.5
588+
@test constructorof(typeof(OpenInterval(1, 2)))(0.5, 1.5) === OpenInterval(0.5, 1.5)
589+
@test setproperties(1..2, left=0.0) === 0.0..2.0
590+
@test setproperties(OpenInterval(1.0, 2.0), left=1, right=5) === OpenInterval(1, 5)
613591
end

0 commit comments

Comments
 (0)