Skip to content

Commit aed4cf3

Browse files
committed
Merge branch 'master' into fieldvalues
2 parents c769603 + 518a6b8 commit aed4cf3

File tree

5 files changed

+230
-37
lines changed

5 files changed

+230
-37
lines changed

.github/workflows/CI.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ jobs:
1010
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
1111
runs-on: ${{ matrix.os }}
1212
strategy:
13-
fail-fast: true
13+
fail-fast: false
1414
matrix:
1515
version:
1616
- '1.0'
1717
- '1.3'
1818
- '1.5'
19+
- '1.6'
20+
- '1'
1921
- 'nightly'
2022
os:
2123
- ubuntu-latest

.github/workflows/Downstream.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: IntegrationTest
2+
on:
3+
push:
4+
pull_request:
5+
6+
jobs:
7+
test:
8+
name: ${{ matrix.package.repo }}/${{ matrix.julia-version }}
9+
runs-on: ${{ matrix.os }}
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
julia-version: [1.6, 1, nightly]
14+
os: [ubuntu-latest]
15+
package:
16+
- {repo: JuliaObjects/Accessors.jl}
17+
- {repo: JuliaFolds/BangBang.jl}
18+
- {repo: jw3126/Setfield.jl}
19+
- {repo: rafaqz/Flatten.jl}
20+
steps:
21+
- uses: actions/checkout@v2
22+
- uses: julia-actions/setup-julia@v1
23+
with:
24+
version: ${{ matrix.julia-version }}
25+
arch: x64
26+
- uses: julia-actions/julia-buildpkg@latest
27+
- name: Clone Downstream
28+
uses: actions/checkout@v2
29+
with:
30+
repository: ${{ matrix.package.repo }}
31+
path: downstream
32+
- name: Load this and run the downstream tests
33+
shell: julia --color=yes --project=downstream {0}
34+
run: |
35+
using Pkg
36+
try
37+
# force it to use this PR's version of the package
38+
Pkg.develop(PackageSpec(path=".")) # resolver may fail with main deps
39+
Pkg.update()
40+
Pkg.test(coverage=true) # resolver may fail with test time deps
41+
catch err
42+
err isa Pkg.Resolve.ResolverError || rethrow()
43+
# If we can't resolve that means this is incompatible by SemVer and this is fine
44+
# It means we marked this as a breaking change, so we don't need to worry about
45+
# Mistakenly introducing a breaking change, as we have intentionally made one
46+
@info "Not compatible with this release. No problem." exception=err
47+
exit(0) # Exit immediately, as a success
48+
end
49+
- uses: julia-actions/julia-processcoverage@v1
50+
- uses: codecov/codecov-action@v1
51+
with:
52+
file: lcov.info

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ConstructionBase"
22
uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
33
authors = ["Takafumi Arakaki", "Rafael Schouten", "Jan Weidner"]
4-
version = "1.3.0"
4+
version = "1.3.1"
55

66
[deps]
77
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

src/ConstructionBase.jl

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,10 @@ constructorof(::Type{<:NamedTuple{names}}) where names =
3737

3838
struct NamedTupleConstructor{names} end
3939

40-
@generated function (::NamedTupleConstructor{names})(args...) where names
41-
quote
42-
Base.@_inline_meta
43-
$(NamedTuple{names, Tuple{args...}})(args)
44-
end
40+
@inline function (::NamedTupleConstructor{names})(args...) where names
41+
NamedTuple{names}(args)
4542
end
4643

47-
4844
################################################################################
4945
#### getfields
5046
################################################################################
@@ -53,13 +49,40 @@ getfields(x::NamedTuple) = x
5349
@generated function getfields(obj)
5450
fnames = fieldnames(obj)
5551
fvals = map(fnames) do fname
56-
Expr(:call, :getfield, :obj, QuoteNode(fname))
52+
Expr(:call, :getfield, :obj, QuoteNode(fname))
53+
end
54+
:(NamedTuple{$fnames}(($(fvals...),)))
55+
end
56+
getproperties(o::NamedTuple) = o
57+
getproperties(o::Tuple) = o
58+
if VERSION >= v"1.7"
59+
function getproperties(obj)
60+
fnames = propertynames(obj)
61+
NamedTuple{fnames}(getproperty.(Ref(obj), fnames))
62+
end
63+
else
64+
function getproperties(obj)
65+
check_properties_are_fields(obj)
66+
getfields(obj)
67+
end
68+
@generated function check_properties_are_fields(obj)
69+
if which(propertynames, Tuple{obj}).sig != Tuple{typeof(propertynames), Any}
70+
# custom propertynames defined for this type
71+
return quote
72+
msg = """
73+
Different fieldnames and propertynames are only supported on Julia v1.7+.
74+
For older julia versions, consider overloading
75+
`ConstructionBase.getproperties(obj::$(typeof(obj))`.
76+
See also https://github.com/JuliaObjects/ConstructionBase.jl/pull/60.
77+
"""
78+
error(msg)
79+
end
80+
else
81+
:()
82+
end
5783
end
58-
fvals = Expr(:tuple, fvals...)
59-
:(NamedTuple{$fnames}($fvals))
6084
end
6185

62-
getproperties(o) = getfields(o)
6386
################################################################################
6487
##### setproperties
6588
################################################################################
@@ -94,9 +117,8 @@ function validate_setproperties_result(
94117
end
95118
@noinline function validate_setproperties_result(nt_new, nt_old, obj, patch)
96119
O = typeof(obj)
97-
P = typeof(patch)
98120
msg = """
99-
Failed to assign properties $(fieldnames(P)) to object with fields $(fieldnames(O)).
121+
Failed to assign properties $(propertynames(patch)) to object with properties $(propertynames(obj)).
100122
You may want to overload
101123
ConstructionBase.setproperties(obj::$O, patch::NamedTuple)
102124
ConstructionBase.getproperties(obj::$O)
@@ -148,6 +170,7 @@ setproperties_object(obj, patch::Tuple{}) = obj
148170
obj = $obj
149171
patch = $patch
150172
"""
173+
throw(ArgumentError(msg))
151174
end
152175
setproperties_object(obj, patch::NamedTuple{()}) = obj
153176
function setproperties_object(obj, patch)

test/runtests.jl

Lines changed: 139 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ end
6969
@test setproperties((a=1,), ()) === (a=1,)
7070
@test setproperties((a=1,), NamedTuple()) === (a=1,)
7171
@test setproperties(AB(1,2), ()) === AB(1,2)
72+
@test_throws ArgumentError setproperties(AB(1,2), (10,))
7273
@test setproperties(AB(1,2), NamedTuple()) === AB(1,2)
7374

7475
@test setproperties(AB(1,2), (a=2, b=3)) === AB(2,3)
@@ -262,7 +263,31 @@ end
262263
end
263264
end
264265

265-
function funny_numbers(n)::Tuple
266+
# example of a struct with different fields and properties
267+
struct FieldProps{NT <: NamedTuple{(:a, :b)}}
268+
components::NT
269+
end
270+
271+
Base.propertynames(obj::FieldProps) = (:a, :b)
272+
Base.getproperty(obj::FieldProps, name::Symbol) = getproperty(getfield(obj, :components), name)
273+
ConstructionBase.constructorof(::Type{<:FieldProps}) = (a, b) -> FieldProps((a=a, b=b))
274+
275+
@testset "use properties, not fields" begin
276+
x = FieldProps((a=1, b=:b))
277+
if VERSION >= v"1.7"
278+
@test getproperties(x) == (a=1, b=:b)
279+
@test setproperties(x, a="aaa") == FieldProps((a="aaa", b=:b))
280+
VERSION >= v"1.8-dev" ?
281+
(@test_throws "Failed to assign properties (:c,) to object with properties (:a, :b)" setproperties(x, c=0)) :
282+
(@test_throws ArgumentError setproperties(x, c=0))
283+
else
284+
@test_throws ErrorException getproperties(x)
285+
@test_throws ErrorException setproperties(x, a="aaa")
286+
@test_throws ErrorException setproperties(x, c=0)
287+
end
288+
end
289+
290+
function funny_numbers(::Type{Tuple}, n)::Tuple
266291
types = [
267292
Int128, Int16, Int32, Int64, Int8,
268293
UInt128, UInt16, UInt32, UInt64, UInt8,
@@ -272,47 +297,129 @@ function funny_numbers(n)::Tuple
272297
end
273298

274299
function funny_numbers(::Type{NamedTuple}, n)::NamedTuple
275-
t = funny_numbers(n)
300+
t = funny_numbers(Tuple,n)
276301
pairs = map(1:n) do i
277302
Symbol("a$i") => t[i]
278303
end
279304
(;pairs...)
280305
end
281306

282-
for n in [0,1,20,40]
307+
abstract type S end
308+
Sn_from_n = Dict{Int,Type}()
309+
for n in [0,1,10,20,40]
283310
Sn = Symbol("S$n")
284311
types = [Symbol("A$i") for i in 1:n]
285312
fields = [Symbol("a$i") for i in 1:n]
286313
typed_fields = [:($ai::$Ai) for (ai,Ai) in zip(fields, types)]
287-
@eval struct $(Sn){$(types...)}
314+
@eval struct $(Sn){$(types...)} <: S
288315
$(typed_fields...)
289316
end
290-
@eval funny_numbers(::Type{$Sn}) = ($Sn)(funny_numbers($n)...)
317+
@eval Sn_from_n[$n] = $Sn
318+
end
319+
function funny_numbers(::Type{S}, n)::S
320+
fields = funny_numbers(Tuple, n)
321+
Sn_from_n[n](fields...)
322+
end
323+
324+
reconstruct(obj, content) = constructorof(typeof(obj))(content...)
325+
326+
function write_output_to_ref!(f, out_ref::Ref, arg_ref::Ref)
327+
arg = arg_ref[]
328+
out_ref[] = f(arg)
329+
out_ref
330+
end
331+
function write_output_to_ref!(f, out_ref::Ref, arg_ref1::Ref, arg_ref2::Ref)
332+
arg1 = arg_ref1[]
333+
arg2 = arg_ref2[]
334+
out_ref[] = f(arg1,arg2)
335+
out_ref
336+
end
337+
function hot_loop_allocs(f::F, args...) where {F}
338+
# we want to test that f(args...) does not allocate
339+
# when used in hot loops
340+
# however a naive @allocated f(args...)
341+
# will not be representative of what happens in an inner loop
342+
# Instead it will sometimes box inputs/outputs
343+
# and report too many allocations
344+
# so we use Refs to minimize inputs and outputs
345+
out_ref = Ref(f(args...))
346+
arg_refs = map(Ref, args)
347+
write_output_to_ref!(f, out_ref, arg_refs...)
348+
out_ref = typeof(out_ref)() # erase out_ref so we can assert work was done later
349+
# Avoid splatting args... which also results in undesired allocs
350+
allocs = if length(arg_refs) == 1
351+
r1, = arg_refs
352+
@allocated write_output_to_ref!(f, out_ref, r1)
353+
elseif length(arg_refs) == 2
354+
r1,r2 = arg_refs
355+
@allocated write_output_to_ref!(f, out_ref, r1, r2)
356+
else
357+
error("TODO too many args")
358+
end
359+
@assert out_ref[] == f(args...)
360+
return allocs
361+
end
362+
363+
@testset "no allocs $T" for T in [Tuple, NamedTuple, S]
364+
@testset "n = $n" for n in [0,1,10,20]
365+
obj = funny_numbers(T, n)
366+
new_content = funny_numbers(Tuple, n)
367+
@test 0 == hot_loop_allocs(constructorof, typeof(obj))
368+
@test 0 == hot_loop_allocs(reconstruct, obj, new_content)
369+
@test 0 == hot_loop_allocs(getproperties, obj)
370+
@test 0 == hot_loop_allocs(getfields, obj)
371+
patch_sizes = [0,1,n÷3,n÷2,n]
372+
patch_sizes = min.(patch_sizes, n)
373+
patch_sizes = unique(patch_sizes)
374+
for k in patch_sizes
375+
patch = if T === Tuple
376+
funny_numbers(Tuple, k)
377+
else
378+
funny_numbers(NamedTuple, k)
379+
end
380+
@test 0 == hot_loop_allocs(setproperties, obj, patch)
381+
end
382+
end
291383
end
292384

293385
@testset "inference" begin
294386
@testset "Tuple n=$n" for n in [0,1,2,3,4,5,10,20,30,40]
295-
t = funny_numbers(n)
387+
t = funny_numbers(Tuple,n)
296388
@test length(t) == n
297389
@test getproperties(t) === t
298390
@inferred getproperties(t)
391+
@test getfields(t) === t
392+
@inferred getfields(t)
393+
@inferred constructorof(typeof(t))
394+
content = funny_numbers(Tuple,n)
395+
@inferred reconstruct(t, content)
299396
for k in 0:n
300-
t2 = funny_numbers(k)
301-
@inferred setproperties(t, t2)
397+
t2 = funny_numbers(Tuple,k)
302398
@test setproperties(t, t2)[1:k] === t2
303399
@test setproperties(t, t2) isa Tuple
304400
@test length(setproperties(t, t2)) == n
305401
@test setproperties(t, t2)[k+1:n] === t[k+1:n]
402+
@inferred setproperties(t, t2)
306403
end
307404
end
308-
@inferred getproperties(funny_numbers(100))
309-
@inferred setproperties(funny_numbers(100), funny_numbers(90))
405+
@inferred getproperties(funny_numbers(Tuple,100))
406+
@inferred setproperties(funny_numbers(Tuple,100), funny_numbers(Tuple,90))
407+
310408
@testset "NamedTuple n=$n" for n in [0,1,2,3,4,5,10,20,30,40]
311409
nt = funny_numbers(NamedTuple, n)
312410
@test nt isa NamedTuple
313411
@test length(nt) == n
314412
@test getproperties(nt) === nt
315413
@inferred getproperties(nt)
414+
@test getfields(nt) === nt
415+
@inferred getfields(nt)
416+
417+
@inferred constructorof(typeof(nt))
418+
if VERSION >= v"1.3"
419+
content = funny_numbers(NamedTuple,n)
420+
@inferred reconstruct(nt, content)
421+
end
422+
#no_allocs_test(nt, content)
316423
for k in 0:n
317424
nt2 = funny_numbers(NamedTuple, k)
318425
@inferred setproperties(nt, nt2)
@@ -325,18 +432,27 @@ end
325432
@inferred getproperties(funny_numbers(NamedTuple, 100))
326433
@inferred setproperties(funny_numbers(NamedTuple, 100), funny_numbers(NamedTuple, 90))
327434

435+
@inferred setproperties(funny_numbers(S,0), funny_numbers(NamedTuple, 0))
436+
@inferred setproperties(funny_numbers(S,1), funny_numbers(NamedTuple, 1))
437+
@inferred setproperties(funny_numbers(S,20), funny_numbers(NamedTuple, 18))
438+
@inferred setproperties(funny_numbers(S,40), funny_numbers(NamedTuple, 38))
439+
@inferred constructorof(S0)
440+
@inferred constructorof(S1)
441+
@inferred constructorof(S20)
442+
@inferred constructorof(S40)
443+
if VERSION >= v"1.3"
444+
@inferred reconstruct(funny_numbers(S,0) , funny_numbers(Tuple,0))
445+
@inferred reconstruct(funny_numbers(S,1) , funny_numbers(Tuple,1))
446+
@inferred reconstruct(funny_numbers(S,20), funny_numbers(Tuple,20))
447+
@inferred reconstruct(funny_numbers(S,40), funny_numbers(Tuple,40))
448+
end
328449

329-
@inferred setproperties(funny_numbers(S0), funny_numbers(NamedTuple, 0))
330-
@inferred setproperties(funny_numbers(S1), funny_numbers(NamedTuple, 1))
331-
@inferred setproperties(funny_numbers(S20), funny_numbers(NamedTuple, 18))
332-
@inferred setproperties(funny_numbers(S40), funny_numbers(NamedTuple, 38))
333-
@inferred getproperties(funny_numbers(S0))
334-
@inferred getproperties(funny_numbers(S1))
335-
@inferred getproperties(funny_numbers(S20))
336-
@inferred getproperties(funny_numbers(S40))
337-
338-
@inferred getfields(funny_numbers(S0))
339-
@inferred getfields(funny_numbers(S1))
340-
@inferred getfields(funny_numbers(S20))
341-
@inferred getfields(funny_numbers(S40))
450+
@inferred getfields(funny_numbers(S,0))
451+
@inferred getfields(funny_numbers(S,1))
452+
@inferred getfields(funny_numbers(S,20))
453+
@inferred getfields(funny_numbers(S,40))
454+
@inferred getproperties(funny_numbers(S,0))
455+
@inferred getproperties(funny_numbers(S,1))
456+
@inferred getproperties(funny_numbers(S,20))
457+
@inferred getproperties(funny_numbers(S,40))
342458
end

0 commit comments

Comments
 (0)