diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 19369b6..f70b68b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -16,6 +16,9 @@ jobs: - '1.0' - '1.3' - '1.5' + - '1.6' + - '1.7' + - '1' - 'nightly' os: - ubuntu-latest @@ -31,8 +34,3 @@ jobs: arch: ${{ matrix.arch }} - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-runtest@latest - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 - with: - file: lcov.info - fail_ci_if_error: true diff --git a/.github/workflows/Documenter.yml b/.github/workflows/Documenter.yml index 172a79b..a9db185 100644 --- a/.github/workflows/Documenter.yml +++ b/.github/workflows/Documenter.yml @@ -5,6 +5,7 @@ on: tags: [v*] pull_request: branches: [master] + workflow_dispatch: jobs: Documenter: name: Documentation diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml new file mode 100644 index 0000000..805c696 --- /dev/null +++ b/.github/workflows/Downstream.yml @@ -0,0 +1,52 @@ +name: IntegrationTest +on: + push: + pull_request: + +jobs: + test: + name: ${{ matrix.package.repo }}/${{ matrix.julia-version }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + julia-version: [1.6, 1, nightly] + os: [ubuntu-latest] + package: + - {repo: JuliaObjects/Accessors.jl} + - {repo: JuliaFolds/BangBang.jl} + - {repo: jw3126/Setfield.jl} + - {repo: rafaqz/Flatten.jl} + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.julia-version }} + arch: x64 + - uses: julia-actions/julia-buildpkg@latest + - name: Clone Downstream + uses: actions/checkout@v2 + with: + repository: ${{ matrix.package.repo }} + path: downstream + - name: Load this and run the downstream tests + shell: julia --color=yes --project=downstream {0} + run: | + using Pkg + try + # force it to use this PR's version of the package + Pkg.develop(PackageSpec(path=".")) # resolver may fail with main deps + Pkg.update() + Pkg.test(coverage=true) # resolver may fail with test time deps + catch err + err isa Pkg.Resolve.ResolverError || rethrow() + # If we can't resolve that means this is incompatible by SemVer and this is fine + # It means we marked this as a breaking change, so we don't need to worry about + # Mistakenly introducing a breaking change, as we have intentionally made one + @info "Not compatible with this release. No problem." exception=err + exit(0) # Exit immediately, as a success + end + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info diff --git a/.github/workflows/Invalidations.yml b/.github/workflows/Invalidations.yml new file mode 100644 index 0000000..4d0004e --- /dev/null +++ b/.github/workflows/Invalidations.yml @@ -0,0 +1,40 @@ +name: Invalidations + +on: + pull_request: + +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: always. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + evaluate: + # Only run on PRs to the default branch. + # In the PR trigger above branches can be specified only explicitly whereas this check should work for master, main, or any other default branch + if: github.base_ref == github.event.repository.default_branch + runs-on: ubuntu-latest + steps: + - uses: julia-actions/setup-julia@v1 + with: + version: '1' + - uses: actions/checkout@v3 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-invalidations@v1 + id: invs_pr + + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.repository.default_branch }} + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-invalidations@v1 + id: invs_default + + - name: Report invalidation counts + run: | + echo "Invalidations on default branch: ${{ steps.invs_default.outputs.total }} (${{ steps.invs_default.outputs.deps }} via deps)" >> $GITHUB_STEP_SUMMARY + echo "This branch: ${{ steps.invs_pr.outputs.total }} (${{ steps.invs_pr.outputs.deps }} via deps)" >> $GITHUB_STEP_SUMMARY + - name: Check if the PR does increase number of invalidations + if: steps.invs_pr.outputs.total > steps.invs_default.outputs.total + run: exit 1 diff --git a/.github/workflows/RunTestsNoIncrementalPrecompile.yml b/.github/workflows/RunTestsNoIncrementalPrecompile.yml new file mode 100644 index 0000000..3f831d8 --- /dev/null +++ b/.github/workflows/RunTestsNoIncrementalPrecompile.yml @@ -0,0 +1,30 @@ +name: CI +on: + - push + - pull_request +defaults: + run: + shell: bash +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + version: + - '1' + - 'nightly' + os: + - ubuntu-latest + arch: + - x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/julia-buildpkg@latest + - name: Run tests without incremental precompilation + run: julia --compiled-modules=no --project -e "using Pkg; Pkg.test()" diff --git a/Project.toml b/Project.toml index 9375eb9..7611b5c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,16 +1,28 @@ name = "ConstructionBase" uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" authors = ["Takafumi Arakaki", "Rafael Schouten", "Jan Weidner"] -version = "1.3.0" +version = "1.5.1" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +[weakdeps] +IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[extensions] +IntervalSetsExt = "IntervalSets" +StaticArraysExt = "StaticArrays" + [compat] +IntervalSets = "0.5, 0.6, 0.7" +StaticArrays = "1" julia = "1" [extras] +IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["IntervalSets","StaticArrays","Test"] diff --git a/README.md b/README.md index 3ad6f61..892bc0d 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,9 @@ [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaObjects.github.io/ConstructionBase.jl/stable) [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaObjects.github.io/ConstructionBase.jl/dev) [![Build Status](https://github.com/JuliaObjects/ConstructionBase.jl/workflows/CI/badge.svg)](https://github.com/JuliaObjects/ConstructionBase.jl/actions?query=workflow%3ACI) -[![Codecov](https://codecov.io/gh/JuliaObjects/ConstructionBase.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaObjects/ConstructionBase.jl) [![GitHub stars](https://img.shields.io/github/stars/JuliaObjects/ConstructionBase.jl?style=social)](https://github.com/JuliaObjects/ConstructionBase.jl) -ConstructionBase is a very lightwight package, that provides primitive functions for construction of objects: +ConstructionBase is a very lightweight package, that provides primitive functions for construction of objects: ```julia setproperties(obj::MyType, patch::NamedTuple) constructorof(MyType) diff --git a/docs/Manifest.toml b/docs/Manifest.toml index fe71747..ae8bbc4 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,89 +1,149 @@ # This file is machine-generated - editing it directly is not advised -[[Base64]] +julia_version = "1.7.0" +manifest_format = "2.0" + +[[deps.ANSIColoredPrinters]] +git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" +uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" +version = "0.0.1" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" + +[[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -[[Dates]] +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" + +[[deps.ConstructionBase]] +deps = ["LinearAlgebra"] +path = ".." +uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" +version = "1.3.0" + +[[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -[[Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" - -[[DocStringExtensions]] -deps = ["LibGit2", "Markdown", "Pkg", "Test"] -git-tree-sha1 = "0513f1a8991e9d83255e0140aace0d0fc4486600" +[[deps.DocStringExtensions]] +deps = ["LibGit2"] +git-tree-sha1 = "b19534d1895d702889b219c382a6e18010797f0b" uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.8.0" +version = "0.8.6" -[[Documenter]] -deps = ["Base64", "DocStringExtensions", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] -git-tree-sha1 = "1b6ae3796f60311e39cd1770566140d2c056e87f" +[[deps.Documenter]] +deps = ["ANSIColoredPrinters", "Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] +git-tree-sha1 = "7d9a46421aef53cbd6b8ecc40c3dcbacbceaf40e" uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "0.23.3" +version = "0.27.15" + +[[deps.Future]] +deps = ["Random"] +uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" -[[InteractiveUtils]] +[[deps.IOCapture]] +deps = ["Logging", "Random"] +git-tree-sha1 = "f7be53659ab06ddc986428d3a9dcc95f6fa6705a" +uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" +version = "0.2.2" + +[[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -[[JSON]] +[[deps.JSON]] deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" +git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.0" +version = "0.21.3" -[[LibGit2]] +[[deps.LibGit2]] +deps = ["Base64", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" -[[Logging]] +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[deps.LinearAlgebra]] +deps = ["Libdl", "libblastrampoline_jll"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" -[[Markdown]] +[[deps.MacroTools]] +deps = ["Markdown", "Random"] +git-tree-sha1 = "3d3e902b31198a27340d0bf00d6ac452866021cf" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.9" + +[[deps.Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -[[Mmap]] +[[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" -[[Parsers]] -deps = ["Dates", "Test"] -git-tree-sha1 = "ef0af6c8601db18c282d092ccbd2f01f3f0cd70b" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "0.3.7" +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" + +[[deps.OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -[[Pkg]] -deps = ["Dates", "LibGit2", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +[[deps.Parsers]] +deps = ["Dates"] +git-tree-sha1 = "85b5da0fa43588c75bb1ff986493443f821c70b7" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.2.3" -[[Printf]] +[[deps.Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -[[REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets"] +[[deps.REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -[[Random]] -deps = ["Serialization"] +[[deps.Random]] +deps = ["SHA", "Serialization"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -[[SHA]] +[[deps.Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.3.0" + +[[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -[[Serialization]] +[[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -[[Sockets]] +[[deps.Setfield]] +deps = ["ConstructionBase", "Future", "MacroTools", "Requires"] +git-tree-sha1 = "38d88503f695eb0301479bc9b0d4320b378bafe5" +uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" +version = "0.8.2" + +[[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -[[Test]] -deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +[[deps.Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -[[UUIDs]] +[[deps.UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -[[Unicode]] +[[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[deps.libblastrampoline_jll]] +deps = ["Artifacts", "Libdl", "OpenBLAS_jll"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" diff --git a/docs/Project.toml b/docs/Project.toml index 13764a2..33447c3 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,4 @@ [deps] +ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" diff --git a/docs/src/index.md b/docs/src/index.md index 0411784..8eb1022 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,13 +1,25 @@ # ConstructionBase.jl -```@index -``` +[`ConstructionBase`](@ref) allows flexible construction and destructuring of objects. +There are two levels of under which this can be done: +### [The raw level](@id the-raw-level) +This is where `Base.fieldnames`, `Base.getfield`, `Base.setfield!` live. +This level is what an object is ultimately composed of including all private details. +At the raw level [`ConstructionBase`](@ref) adds [`constructorof`](@ref) and [`getfields`](@ref). +### [The semantic level](@id the-semantic-level) +This is where `Base.propertynames`, `Base.getproperty` and `Base.setproperty!` live. This level is typically the public interface of a type, it may hide private details and do magic tricks. +At the semantic level [`ConstructionBase`](@ref) adds [`setproperties`](@ref) and [`getproperties`](@ref). + ## Interface +```@index +``` + ```@docs ConstructionBase ConstructionBase.constructorof +ConstructionBase.getfields ConstructionBase.getproperties ConstructionBase.setproperties ``` diff --git a/ext/IntervalSetsExt.jl b/ext/IntervalSetsExt.jl new file mode 100644 index 0000000..1208d8e --- /dev/null +++ b/ext/IntervalSetsExt.jl @@ -0,0 +1,8 @@ +module IntervalSetsExt + +using ConstructionBase +using IntervalSets + +ConstructionBase.constructorof(::Type{<:Interval{L, R}}) where {L, R} = Interval{L, R} + +end diff --git a/ext/StaticArraysExt.jl b/ext/StaticArraysExt.jl new file mode 100644 index 0000000..6bcc490 --- /dev/null +++ b/ext/StaticArraysExt.jl @@ -0,0 +1,31 @@ +module StaticArraysExt + +using ConstructionBase +using StaticArrays + +# general static arrays need to keep the size parameter +ConstructionBase.constructorof(sa::Type{<:SArray{S}}) where {S} = SArray{S} +ConstructionBase.constructorof(sa::Type{<:MArray{S}}) where {S} = MArray{S} +ConstructionBase.constructorof(sa::Type{<:SizedArray{S}}) where {S} = SizedArray{S} + +# static vectors don't even need the explicit size specification +ConstructionBase.constructorof(::Type{<:SVector}) = SVector +ConstructionBase.constructorof(::Type{<:MVector}) = MVector + +# set properties by name: x, y, z, w +@generated function ConstructionBase.setproperties(obj::Union{SVector{N}, MVector{N}}, patch::NamedTuple{KS}) where {N, KS} + if KS == (:data,) + :( constructorof(typeof(obj))(only(patch)) ) + else + N <= 4 || error("type $obj does not have properties $(join(KS, ", "))") + propnames = (:x, :y, :z, :w)[1:N] + KS ⊆ propnames || error("type $obj does not have properties $(join(KS, ", "))") + field_exprs = map(enumerate(propnames)) do (i, p) + from = p ∈ KS ? :patch : :obj + :( $from.$p ) + end + :( constructorof(typeof(obj))($(field_exprs...)) ) + end +end + +end diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 0302fc0..6174be0 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -3,11 +3,13 @@ module ConstructionBase export getproperties export setproperties export constructorof +export getfields # Use markdown files as docstring: for (name, path) in [ :ConstructionBase => joinpath(dirname(@__DIR__), "README.md"), :constructorof => joinpath(@__DIR__, "constructorof.md"), + :getfields => joinpath(@__DIR__, "getfields.md"), :getproperties => joinpath(@__DIR__, "getproperties.md"), :setproperties => joinpath(@__DIR__, "setproperties.md"), ] @@ -34,22 +36,83 @@ constructorof(::Type{<:NamedTuple{names}}) where names = struct NamedTupleConstructor{names} end -@generated function (::NamedTupleConstructor{names})(args...) where names - quote - Base.@_inline_meta - $(NamedTuple{names, Tuple{args...}})(args) - end +@inline function (::NamedTupleConstructor{names})(args...) where names + NamedTuple{names}(args) end +################################################################################ +#### getfields +################################################################################ +getfields(x::Tuple) = x +getfields(x::NamedTuple) = x getproperties(o::NamedTuple) = o getproperties(o::Tuple) = o -@generated function getproperties(obj) - fnames = fieldnames(obj) - fvals = map(fnames) do fname - Expr(:call, :getproperty, :obj, QuoteNode(fname)) + +if VERSION >= v"1.7" + function check_properties_are_fields(obj) + if propertynames(obj) != fieldnames(typeof(obj)) + error(""" + The function `Base.propertynames` was overloaded for type `$(typeof(obj))`. + Please make sure `ConstructionBase.setproperties` is also overloaded for this type. + """) + end + end +else + function is_propertynames_overloaded(T::Type)::Bool + which(propertynames, Tuple{T}).sig !== Tuple{typeof(propertynames), Any} + end + + @generated function check_properties_are_fields(obj) + if is_propertynames_overloaded(obj) + return quote + T = typeof(obj) + msg = """ + The function `Base.propertynames` was overloaded for type `$T`. + Please make sure the following methods are also overloaded for this type: + ```julia + ConstructionBase.setproperties + ConstructionBase.getproperties # optional in VERSION >= julia v1.7 + ``` + """ + error(msg) + end + else + :(nothing) + end + end +end + +# names are consecutive integers: return tuple +# names are symbols: return namedtuple +# names are empty (object has no properties): also return namedtuple, for backwards compat and generally makes more sense +@inline tuple_or_ntuple(names::Tuple{}, vals::Tuple) = NamedTuple{names}(vals) +@inline tuple_or_ntuple(names::Tuple{Vararg{Symbol}}, vals::Tuple) = NamedTuple{names}(vals) +@inline function tuple_or_ntuple(names::Tuple{Vararg{Int}}, vals::Tuple) + @assert names === ntuple(identity, length(names)) + vals +end + +if VERSION >= v"1.7" + function getproperties(obj) + fnames = propertynames(obj) + tuple_or_ntuple(fnames, getproperty.(Ref(obj), fnames)) + end + function getfields(obj::T) where {T} + fnames = fieldnames(T) + NamedTuple{fnames}(getfield.(Ref(obj), fnames)) + end +else + @generated function getfields(obj) + fnames = fieldnames(obj) + fvals = map(fnames) do fname + Expr(:call, :getfield, :obj, QuoteNode(fname)) + end + :(NamedTuple{$fnames}(($(fvals...),))) + end + function getproperties(obj) + check_properties_are_fields(obj) + getfields(obj) end - fvals = Expr(:tuple, fvals...) - :(NamedTuple{$fnames}($fvals)) end ################################################################################ @@ -77,21 +140,17 @@ setproperties_namedtuple(obj, patch::Tuple{}) = obj end function setproperties_namedtuple(obj, patch) res = merge(obj, patch) - validate_setproperties_result(res, obj, obj, patch) + check_patch_properties_exist(res, obj, obj, patch) res end -function validate_setproperties_result( +function check_patch_properties_exist( nt_new::NamedTuple{fields}, nt_old::NamedTuple{fields}, obj, patch) where {fields} nothing end -@noinline function validate_setproperties_result(nt_new, nt_old, obj, patch) +@noinline function check_patch_properties_exist(nt_new, nt_old, obj, patch) O = typeof(obj) - P = typeof(patch) msg = """ - Failed to assign properties $(fieldnames(P)) to object with fields $(fieldnames(O)). - You may want to overload - ConstructionBase.setproperties(obj::$O, patch::NamedTuple) - ConstructionBase.getproperties(obj::$O) + Failed to assign properties $(propertynames(patch)) to object with properties $(propertynames(obj)). """ throw(ArgumentError(msg)) end @@ -140,13 +199,17 @@ setproperties_object(obj, patch::Tuple{}) = obj obj = $obj patch = $patch """ + throw(ArgumentError(msg)) end setproperties_object(obj, patch::NamedTuple{()}) = obj + function setproperties_object(obj, patch) + check_properties_are_fields(obj) nt = getproperties(obj) nt_new = merge(nt, patch) - validate_setproperties_result(nt_new, nt, obj, patch) - constructorof(typeof(obj))(Tuple(nt_new)...) + check_patch_properties_exist(nt_new, nt, obj, patch) + args = Tuple(nt_new) # old julia inference prefers if we wrap in Tuple + constructorof(typeof(obj))(args...) end include("nonstandard.jl") diff --git a/src/constructorof.md b/src/constructorof.md index 4c88f81..42fef1e 100644 --- a/src/constructorof.md +++ b/src/constructorof.md @@ -32,17 +32,16 @@ julia> constructorof(S)(1,2,4) ERROR: AssertionError: a + b == checksum ``` Instead `constructor` can be any object that satisfies the following properties: -* It must be possible to reconstruct an object from the `NamedTuple` returned by -`getproperties`: +* It must be possible to reconstruct an object from the elements of [`getfields`](@ref): ```julia ctor = constructorof(typeof(obj)) -@assert obj == ctor(getproperties(obj)...) -@assert typeof(obj) == typeof(ctor(getproperties(obj)...)) +@assert obj == ctor(getfields(obj)...) +@assert typeof(obj) == typeof(ctor(getfields(obj)...)) ``` * The other direction should hold for as many values of `args` as possible: ```julia ctor = constructorof(T) -getproperties(ctor(args...)) == args +getfields(ctor(args...)) == args ``` For instance given a suitable parametric type it should be possible to change the type of its fields: @@ -61,7 +60,7 @@ T{Float64, Int64}(1.0, 2) julia> constructorof(typeof(t))(10, 2) T{Int64, Int64}(10, 2) ``` - +`constructorof` belongs to [the raw level](@ref the-raw-level). `constructorof` is generated for all anonymous `Function`s lacking constructors, identified as having `gensym` `#` in their names. A custom struct `<: Function` with a `gensym` name may need to define `constructorof` manually. diff --git a/src/functions.jl b/src/functions.jl index 5c88b47..739aacd 100644 --- a/src/functions.jl +++ b/src/functions.jl @@ -3,6 +3,7 @@ # one for them based on the types of args passed to FunctionConstructor struct FunctionConstructor{F} end +_isgensym(s::Symbol) = occursin("#", string(s)) @generated function (fc::FunctionConstructor{F})(args...) where F T = getfield(parentmodule(F), nameof(F)) @@ -20,4 +21,3 @@ function ConstructionBase.constructorof(f::Type{F}) where F <: Function FunctionConstructor{F}() end -_isgensym(s::Symbol) = occursin("#", string(s)) diff --git a/src/getfields.md b/src/getfields.md new file mode 100644 index 0000000..d4f0d89 --- /dev/null +++ b/src/getfields.md @@ -0,0 +1,53 @@ + getfields(obj) -> NamedTuple + getfields(obj::Tuple) -> Tuple + +Return a `NamedTuple` containing the fields of `obj`. On `Tuples` `getfields` is +the identity function instead, since `Tuple` fields have no symbolic names. + +# Examples +```jldoctest +julia> using ConstructionBase + +julia> struct S{A,B} + a::A + b::B + end + +julia> getfields(S(1,2)) +(a = 1, b = 2) + +julia> getfields((a=10,b=20)) +(a = 10, b = 20) + +julia> getfields((4,5,6)) +(4, 5, 6) +``` + +# Specification + +`getfields` belongs to the [the raw level](@ref the-raw-level). +Semantically `getfields` boils down to `getfield` and `fieldnames`: +```julia +function getfields(obj::T) where {T} + fnames = fieldnames(T) + NamedTuple{fnames}(getfield.(Ref(obj), fnames)) +end +``` +However the actual implementation can be more optimized. For builtin types, there can also be deviations from this semantics: +* `getfields(::Tuple)::Tuple` since `Tuples` don't have symbolic fieldnames +* There are some types in `Base` that have `undef` fields. Since accessing these results in an error, `getfields` instead just omits these. + +# Implementation + +The semantics of `getfields` should not be changed for user defined types. It should +return the raw fields as a `NamedTuple` in the struct order. In other words it should be +equivalent to +```julia +function getfields(obj::T) where {T} + fnames = fieldnames(T) + NamedTuple{fnames}(getfield.(Ref(obj), fnames)) +end +``` +even if that includes private fields of `obj`. +If a change of semantics is desired, consider overloading [`getproperties`](@ref) instead. +See also [`getproperties`](@ref), [`constructorof`](@ref) diff --git a/src/getproperties.md b/src/getproperties.md index b70d009..61a6501 100644 --- a/src/getproperties.md +++ b/src/getproperties.md @@ -1,6 +1,8 @@ - getproperties(obj) + getproperties(obj)::NamedTuple + getproperties(obj::Tuple)::Tuple -Return the fields of `obj` as a `NamedTuple`. +Return the properties of `obj` as a `NamedTuple`. Since `Tuple` don't have symbolic properties, +`getproperties` is the identity function on tuples. # Examples ```jldoctest @@ -17,26 +19,31 @@ S(1, 2, 3) julia> getproperties(s) (a = 1, b = 2, c = 3) -``` - -# Implementation -`getproperties` is defined by default for all objects. However for a custom type `MyType`, -`getproperties(obj::MyType)` may be defined when objects may have undefined fields, -when it has calculated fields that should not be accessed or set manually, or -other conditions that do not meet the specification with the default implementation. +julia> getproperties((10,20)) +(10, 20) +``` ## Specification +`getproperties` belongs to [the semantic level](@ref the-semantic-level). `getproperties` guarantees a couple of invariants. When overloading it, the user is responsible for ensuring them: -1. Relation to `propertynames` and `fieldnames`: `getproperties` relates to `propertynames` and `getproperty`, not to `fieldnames` and `getfield`. - This means that any series `p₁, p₂, ..., pₙ` of `propertynames(obj)` that is not undefined should be returned by `getproperties`. -2. `getproperties` is defined in relation to `constructorof` so that: - ```julia - obj == constructorof(obj)(getproperties(obj)...) - ``` +1. `getproperties` should be consistent with `Base.propertynames`, `Base.getproperty`, `Base.setproperty!`. + Semantically it should be equivalent to: + ```julia + function getproperties(obj) + fnames = propertynames(obj) + NamedTuple{fnames}(getproperty.(Ref(obj), fnames)) + end + ``` 2. `getproperties` is defined in relation to `setproperties` so that: ```julia obj == setproperties(obj, getproperties(obj)) ``` + The only exception from this semantics is that undefined properties may be avoided + in the return value of `getproperties`. + +# Implementation + +`getproperties` is defined by default for all objects. It should be very rare that a custom type `MyType`, has to implement `getproperties(obj::MyType)`. Reasons to do so are undefined fields or performance considerations. diff --git a/src/nonstandard.jl b/src/nonstandard.jl index 8ca9104..2ba642d 100644 --- a/src/nonstandard.jl +++ b/src/nonstandard.jl @@ -36,8 +36,8 @@ function tridiagonal_constructor(dl::V, d::V, du::V, du2::V) where {V<:AbstractV Tridiagonal{T,V}(dl, d, du, du2) end -# `du2` may be undefined, so we need a custom `getproperties` that checks `isdefined` -function getproperties(o::Tridiagonal) +# `du2` may be undefined, so we need a custom `getfields` that checks `isdefined` +function getfields(o::Tridiagonal) if isdefined(o, :du2) (dl=o.dl, d=o.d, du=o.du, du2=o.du2) else @@ -52,3 +52,7 @@ constructorof(::Type{<:LinearAlgebra.Tridiagonal}) = tridiagonal_constructor linrange_constructor(start, stop, len, lendiv) = LinRange(start, stop, len) constructorof(::Type{<:LinRange}) = linrange_constructor + +### Expr: args get splatted +# ::Expr annotation is to make it type-stable on Julia 1.3- +constructorof(::Type{<:Expr}) = (head, args) -> Expr(head, args...)::Expr diff --git a/src/setproperties.md b/src/setproperties.md index 11faa4b..aa85248 100644 --- a/src/setproperties.md +++ b/src/setproperties.md @@ -1,6 +1,7 @@ setproperties(obj, patch::NamedTuple) -Return a copy of `obj` with attributes updates according to `patch`. + +Return a copy of `obj` with properties updated according to `patch`. # Examples ```jldoctest @@ -44,23 +45,9 @@ julia> setproperties(o, a="A", c="cc") S("A", 2, "cc") ``` -# Implementation - -For a custom type `MyType`, a method `setproperties(obj::MyType, patch::NamedTuple)` -may be defined. - -* Prefer to overload [`constructorof`](@ref) whenever makes sense (e.g., no `getproperty` - method is defined). Default `setproperties` is defined in terms of `constructorof`. - -* If `getproperty` is customized, it may be a good idea to define `setproperties`. - -!!! warning - The signature `setproperties(obj::MyType; kw...)` should never be overloaded. - Instead `setproperties(obj::MyType, patch::NamedTuple)` should be overloaded. - ## Specification -`setproperties` guarantees a couple of invariants. When overloading it, the user is responsible for ensuring them: +`setproperties` belongs to [the semantic level](@ref the-semantic-level). If satisfies the following invariants: 1. Purity: `setproperties` is supposed to have no side effects. In particular `setproperties(obj, patch::NamedTuple)` may not mutate `obj`. 2. Relation to `propertynames` and `fieldnames`: `setproperties` relates to `propertynames` and `getproperty`, not to `fieldnames` and `getfield`. @@ -100,3 +87,18 @@ let obj′ = setproperties(obj, ($p₁=v₁, $p₂=v₂, ..., $pₙ=vₙ)), @assert obj′′.$pₙ == wₙ end ``` + +# Implementation + +For a custom type `MyType`, a method `setproperties(obj::MyType, patch::NamedTuple)` +may be defined. When doing so it is important to ensure compliance with the specification. + +* Prefer to overload [`constructorof`](@ref) whenever makes sense (e.g., no `getproperty` + method is defined). Default `setproperties` is defined in terms of `constructorof` and `getproperties`. + +* If `getproperty` is customized, it may be a good idea to define `setproperties`. + +!!! warning + The signature `setproperties(obj::MyType; kw...)` should never be overloaded. + Instead `setproperties(obj::MyType, patch::NamedTuple)` should be overloaded. + diff --git a/test/runtests.jl b/test/runtests.jl index b3b0198..0bd249a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,6 +19,30 @@ end @test constructorof(Tuple{Nothing, Missing})(1.0, 2) === (1.0, 2) end +@testset "getfields" begin + @test getfields(()) === () + @test getfields([]) === NamedTuple() + @test getfields(Empty()) === NamedTuple() + @test getfields(NamedTuple()) === NamedTuple() + @test getfields((10,20,30)) === (10,20,30) + @test getfields((a=10,b=20f0,c=true)) === (a=10,b=20f0,c=true) + @test getfields(AB(1, 10)) === (a=1, b=10) + adder(a) = x -> x + a + @test getfields(adder(1)) === (a=1,) +end + +struct DontTouchProperties + a + b +end +Base.propertynames(::DontTouchProperties) = error() +Base.getproperty(::DontTouchProperties, ::Symbol) = error() +ConstructionBase.getproperties(::DontTouchProperties) = error() +@testset "getfields does not depend on properties" begin + @test getfields(DontTouchProperties(1,2)) === (a=1, b=2) + @test constructorof(DontTouchProperties) === DontTouchProperties +end + @testset "getproperties" begin o = AB(1, 2) @test getproperties(o) === (a=1, b=2) @@ -45,6 +69,7 @@ end @test setproperties((a=1,), ()) === (a=1,) @test setproperties((a=1,), NamedTuple()) === (a=1,) @test setproperties(AB(1,2), ()) === AB(1,2) + @test_throws ArgumentError setproperties(AB(1,2), (10,)) @test setproperties(AB(1,2), NamedTuple()) === AB(1,2) @test setproperties(AB(1,2), (a=2, b=3)) === AB(2,3) @@ -54,14 +79,10 @@ end res = @test_throws ArgumentError setproperties(AB(1,2), (a=2, this_field_does_not_exist=3.0)) msg = sprint(showerror, res.value) @test occursin("this_field_does_not_exist", msg) - @test occursin("overload", msg) - @test occursin("ConstructionBase.setproperties", msg) res = @test_throws ArgumentError setproperties(AB(1,2), a=2, this_field_does_not_exist=3.0) msg = sprint(showerror, res.value) @test occursin("this_field_does_not_exist", msg) - @test occursin("overload", msg) - @test occursin("ConstructionBase.setproperties", msg) @test setproperties(42, NamedTuple()) === 42 @test setproperties(42) === 42 @@ -120,29 +141,29 @@ end @testset "SubArray" begin subarray = view(A1, 1:2, 3:4) - @test constructorof(typeof(subarray))(getproperties(subarray)...) === subarray + @test constructorof(typeof(subarray))(getfields(subarray)...) === subarray @test all(constructorof(typeof(subarray))(A2, (Base.OneTo(2), 3:4), 0, 0) .== Float32[1 1; 1 1]) - @inferred constructorof(typeof(subarray))(getproperties(subarray)...) + @inferred constructorof(typeof(subarray))(getfields(subarray)...) @inferred constructorof(typeof(subarray))(A2, (Base.OneTo(2), 3:4), 0, 0) end @testset "ReinterpretArray" begin ra1 = reinterpret(Float16, A1) @test constructorof(typeof(ra1))(A1) === ra1 - @test constructorof(typeof(ra1))(getproperties(ra1)...) === ra1 + @test constructorof(typeof(ra1))(getfields(ra1)...) === ra1 ra2 = constructorof(typeof(ra1))(A2) @test size(ra2) == (10, 6) @test eltype(ra2) == Float16 - @inferred constructorof(typeof(ra1))(getproperties(ra1)...) + @inferred constructorof(typeof(ra1))(getfields(ra1)...) @inferred constructorof(typeof(ra1))(A2) end @testset "PermutedDimsArray" begin pda1 = PermutedDimsArray(A1, (2, 1)) @test constructorof(typeof(pda1))(A1) === pda1 - @test constructorof(typeof(pda1))(getproperties(pda1)...) === pda1 + @test constructorof(typeof(pda1))(getfields(pda1)...) === pda1 @test eltype(constructorof(typeof(pda1))(A2)) == Float32 - @inferred constructorof(typeof(pda1))(getproperties(pda1)...) + @inferred constructorof(typeof(pda1))(getfields(pda1)...) @inferred constructorof(typeof(pda1))(A2) end @@ -153,26 +174,30 @@ end tda = Tridiagonal(dl, d, du) @test isdefined(tda, :du2) == false @test constructorof(typeof(tda))(dl, d, du) === tda - @test constructorof(typeof(tda))(getproperties(tda)...) === tda + @test constructorof(typeof(tda))(getfields(tda)...) === tda # lu factorization defines du2 tda_lu = lu!(tda).factors @test isdefined(tda_lu, :du2) == true - @test constructorof(typeof(tda_lu))(getproperties(tda_lu)...) === tda_lu - @test constructorof(typeof(tda_lu))(getproperties(tda)...) !== tda_lu - @test constructorof(typeof(tda_lu))(getproperties(tda)...) === tda - @inferred constructorof(typeof(tda))(getproperties(tda)...) - @inferred constructorof(typeof(tda))(getproperties(tda_lu)...) + @test constructorof(typeof(tda_lu))(getfields(tda_lu)...) === tda_lu + @test constructorof(typeof(tda_lu))(getfields(tda)...) !== tda_lu + @test constructorof(typeof(tda_lu))(getfields(tda)...) === tda + @inferred constructorof(typeof(tda))(getfields(tda)...) + @inferred constructorof(typeof(tda))(getfields(tda_lu)...) end @testset "LinRange" begin lr1 = LinRange(1, 7, 10) lr2 = LinRange(1.0f0, 7.0f0, 10) @test constructorof(typeof(lr1))(1, 7, 10, nothing) === lr1 - @test constructorof(typeof(lr1))(getproperties(lr2)...) === lr2 - @inferred constructorof(typeof(lr1))(getproperties(lr1)...) - @inferred constructorof(typeof(lr1))(getproperties(lr2)...) + @test constructorof(typeof(lr1))(getfields(lr2)...) === lr2 + @inferred constructorof(typeof(lr1))(getfields(lr1)...) + @inferred constructorof(typeof(lr1))(getfields(lr2)...) end + @testset "Expr" begin + e = :(a + b) + @test e == @inferred constructorof(typeof(e))(getfields(e)...) + end end @testset "Anonymous function constructors" begin @@ -238,7 +263,66 @@ end end end -function funny_numbers(n)::Tuple +# example of a struct with different fields and properties +struct FieldProps{NT <: NamedTuple{(:a, :b)}} + components::NT +end + +Base.propertynames(::FieldProps) = (:a, :b) +Base.getproperty(obj::FieldProps, name::Symbol) = getproperty(getfield(obj, :components), name) + +@testset "use properties, not fields" begin + x = FieldProps((a=1, b=:b)) + @test constructorof(typeof(x)) === FieldProps + @test getfields(x) === (components=(a=1, b=:b),) + res = @test_throws ErrorException setproperties(x, c=0) + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("setproperties", msg) + @test occursin("FieldProps", msg) + @test_throws ErrorException setproperties(x, components=(a=1,b=:b)) + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("setproperties", msg) + @test occursin("FieldProps", msg) + @test_throws ErrorException setproperties(x, a="aaa") + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("setproperties", msg) + @test occursin("FieldProps", msg) + # == FieldProps((a="aaa", b=:b) + if VERSION >= v"1.7" + @test getproperties(x) == (a=1, b=:b) + else + res = @test_throws ErrorException getproperties(x) + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("getproperties", msg) + @test occursin("FieldProps", msg) + end +end + + +struct SProp + names +end +Base.propertynames(s::SProp) = getfield(s, :names) +Base.getproperty(s::SProp, prop::Symbol) = "ps$prop" +Base.getproperty(s::SProp, prop::Int) = "pi$prop" +Base.getproperty(s::SProp, prop::String) = "pstr$prop" + +if VERSION >= v"1.7" + # automatic getproperties() supported only on 1.7+ + + @testset "properties can be numbered" begin + @test getproperties(SProp((:a, :b))) === (a="psa", b="psb") + @test getproperties(SProp((1, 2))) === ("pi1", "pi2") + # what should it return? + @test_broken getproperties(SProp(("a", "b"))) + end +end + +function funny_numbers(::Type{Tuple}, n)::Tuple types = [ Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8, @@ -248,47 +332,129 @@ function funny_numbers(n)::Tuple end function funny_numbers(::Type{NamedTuple}, n)::NamedTuple - t = funny_numbers(n) + t = funny_numbers(Tuple,n) pairs = map(1:n) do i Symbol("a$i") => t[i] end (;pairs...) end -for n in [0,1,20,40] +abstract type S end +Sn_from_n = Dict{Int,Type}() +for n in [0,1,10,20,40] Sn = Symbol("S$n") types = [Symbol("A$i") for i in 1:n] fields = [Symbol("a$i") for i in 1:n] typed_fields = [:($ai::$Ai) for (ai,Ai) in zip(fields, types)] - @eval struct $(Sn){$(types...)} + @eval struct $(Sn){$(types...)} <: S $(typed_fields...) end - @eval funny_numbers(::Type{$Sn}) = ($Sn)(funny_numbers($n)...) + @eval Sn_from_n[$n] = $Sn +end +function funny_numbers(::Type{S}, n)::S + fields = funny_numbers(Tuple, n) + Sn_from_n[n](fields...) +end + +reconstruct(obj, content) = constructorof(typeof(obj))(content...) + +function write_output_to_ref!(f, out_ref::Ref, arg_ref::Ref) + arg = arg_ref[] + out_ref[] = f(arg) + out_ref +end +function write_output_to_ref!(f, out_ref::Ref, arg_ref1::Ref, arg_ref2::Ref) + arg1 = arg_ref1[] + arg2 = arg_ref2[] + out_ref[] = f(arg1,arg2) + out_ref +end +function hot_loop_allocs(f::F, args...) where {F} + # we want to test that f(args...) does not allocate + # when used in hot loops + # however a naive @allocated f(args...) + # will not be representative of what happens in an inner loop + # Instead it will sometimes box inputs/outputs + # and report too many allocations + # so we use Refs to minimize inputs and outputs + out_ref = Ref(f(args...)) + arg_refs = map(Ref, args) + write_output_to_ref!(f, out_ref, arg_refs...) + out_ref = typeof(out_ref)() # erase out_ref so we can assert work was done later + # Avoid splatting args... which also results in undesired allocs + allocs = if length(arg_refs) == 1 + r1, = arg_refs + @allocated write_output_to_ref!(f, out_ref, r1) + elseif length(arg_refs) == 2 + r1,r2 = arg_refs + @allocated write_output_to_ref!(f, out_ref, r1, r2) + else + error("TODO too many args") + end + @assert out_ref[] == f(args...) + return allocs +end + +@testset "no allocs $T" for T in [Tuple, NamedTuple, S] + @testset "n = $n" for n in [0,1,10,20] + obj = funny_numbers(T, n) + new_content = funny_numbers(Tuple, n) + @test 0 == hot_loop_allocs(constructorof, typeof(obj)) + @test 0 == hot_loop_allocs(reconstruct, obj, new_content) + @test 0 == hot_loop_allocs(getproperties, obj) + @test 0 == hot_loop_allocs(getfields, obj) + patch_sizes = [0,1,n÷3,n÷2,n] + patch_sizes = min.(patch_sizes, n) + patch_sizes = unique(patch_sizes) + for k in patch_sizes + patch = if T === Tuple + funny_numbers(Tuple, k) + else + funny_numbers(NamedTuple, k) + end + @test 0 == hot_loop_allocs(setproperties, obj, patch) + end + end end @testset "inference" begin @testset "Tuple n=$n" for n in [0,1,2,3,4,5,10,20,30,40] - t = funny_numbers(n) + t = funny_numbers(Tuple,n) @test length(t) == n @test getproperties(t) === t @inferred getproperties(t) + @test getfields(t) === t + @inferred getfields(t) + @inferred constructorof(typeof(t)) + content = funny_numbers(Tuple,n) + @inferred reconstruct(t, content) for k in 0:n - t2 = funny_numbers(k) - @inferred setproperties(t, t2) + t2 = funny_numbers(Tuple,k) @test setproperties(t, t2)[1:k] === t2 @test setproperties(t, t2) isa Tuple @test length(setproperties(t, t2)) == n @test setproperties(t, t2)[k+1:n] === t[k+1:n] + @inferred setproperties(t, t2) end end - @inferred getproperties(funny_numbers(100)) - @inferred setproperties(funny_numbers(100), funny_numbers(90)) + @inferred getproperties(funny_numbers(Tuple,100)) + @inferred setproperties(funny_numbers(Tuple,100), funny_numbers(Tuple,90)) + @testset "NamedTuple n=$n" for n in [0,1,2,3,4,5,10,20,30,40] nt = funny_numbers(NamedTuple, n) @test nt isa NamedTuple @test length(nt) == n @test getproperties(nt) === nt @inferred getproperties(nt) + @test getfields(nt) === nt + @inferred getfields(nt) + + @inferred constructorof(typeof(nt)) + if VERSION >= v"1.3" + content = funny_numbers(NamedTuple,n) + @inferred reconstruct(nt, content) + end + #no_allocs_test(nt, content) for k in 0:n nt2 = funny_numbers(NamedTuple, k) @inferred setproperties(nt, nt2) @@ -301,13 +467,73 @@ end @inferred getproperties(funny_numbers(NamedTuple, 100)) @inferred setproperties(funny_numbers(NamedTuple, 100), funny_numbers(NamedTuple, 90)) + @inferred setproperties(funny_numbers(S,0), funny_numbers(NamedTuple, 0)) + @inferred setproperties(funny_numbers(S,1), funny_numbers(NamedTuple, 1)) + @inferred setproperties(funny_numbers(S,20), funny_numbers(NamedTuple, 18)) + @inferred setproperties(funny_numbers(S,40), funny_numbers(NamedTuple, 38)) + @inferred constructorof(S0) + @inferred constructorof(S1) + @inferred constructorof(S20) + @inferred constructorof(S40) + if VERSION >= v"1.3" + @inferred reconstruct(funny_numbers(S,0) , funny_numbers(Tuple,0)) + @inferred reconstruct(funny_numbers(S,1) , funny_numbers(Tuple,1)) + @inferred reconstruct(funny_numbers(S,20), funny_numbers(Tuple,20)) + @inferred reconstruct(funny_numbers(S,40), funny_numbers(Tuple,40)) + end + + @inferred getfields(funny_numbers(S,0)) + @inferred getfields(funny_numbers(S,1)) + @inferred getfields(funny_numbers(S,20)) + @inferred getfields(funny_numbers(S,40)) + @inferred getproperties(funny_numbers(S,0)) + @inferred getproperties(funny_numbers(S,1)) + @inferred getproperties(funny_numbers(S,20)) + @inferred getproperties(funny_numbers(S,40)) +end - @inferred setproperties(funny_numbers(S0), funny_numbers(NamedTuple, 0)) - @inferred setproperties(funny_numbers(S1), funny_numbers(NamedTuple, 1)) - @inferred setproperties(funny_numbers(S20), funny_numbers(NamedTuple, 18)) - @inferred setproperties(funny_numbers(S40), funny_numbers(NamedTuple, 38)) - @inferred getproperties(funny_numbers(S0)) - @inferred getproperties(funny_numbers(S1)) - @inferred getproperties(funny_numbers(S20)) - @inferred getproperties(funny_numbers(S40)) + +using StaticArrays, IntervalSets + +if isdefined(Base, :get_extension) # some 1.9 version + @testset "staticarrays" begin + sa = @SVector [2, 4, 6, 8] + sa2 = ConstructionBase.constructorof(typeof(sa))((3.0, 5.0, 7.0, 9.0)) + @test sa2 === @SVector [3.0, 5.0, 7.0, 9.0] + + ma = @MMatrix [2.0 4.0; 6.0 8.0] + ma2 = @inferred ConstructionBase.constructorof(typeof(ma))((1, 2, 3, 4)) + @test ma2 isa MArray{Tuple{2,2},Int,2,4} + @test all(ma2 .=== @MMatrix [1 3; 2 4]) + + sz = SizedArray{Tuple{2,2}}([1 2;3 4]) + sz2 = @inferred ConstructionBase.constructorof(typeof(sz))([:a :b; :c :d]) + @test sz2 == SizedArray{Tuple{2,2}}([:a :b; :c :d]) + @test typeof(sz2) <: SizedArray{Tuple{2,2},Symbol,2,2} + + for T in (SVector, MVector) + @test @inferred(ConstructionBase.constructorof(T)((1, 2, 3)))::T == T((1, 2, 3)) + @test @inferred(ConstructionBase.constructorof(T{3})((1, 2, 3)))::T == T((1, 2, 3)) + @test @inferred(ConstructionBase.constructorof(T{3})((1, 2)))::T == T((1, 2)) + @test @inferred(ConstructionBase.constructorof(T{3, Symbol})((1, 2, 3)))::T == T((1, 2, 3)) + @test @inferred(ConstructionBase.constructorof(T{3, Symbol})((1, 2)))::T == T((1, 2)) + @test @inferred(ConstructionBase.constructorof(T{3, X} where {X})((1, 2, 3)))::T == T((1, 2, 3)) + @test @inferred(ConstructionBase.constructorof(T{3, X} where {X})((1, 2)))::T == T((1, 2)) + @test @inferred(ConstructionBase.constructorof(T{X, Symbol} where {X})((1, 2, 3)))::T == T((1, 2, 3)) + end + + sv = SVector(1, 2) + @test SVector(3.0, 2.0) === @inferred setproperties(sv, x = 3.0) + @test SVector(3.0, 5.0) === @inferred setproperties(sv, x = 3.0, y = 5.0) + @test SVector(-1.0, -2.0) === @inferred setproperties(sv, data = (-1.0, -2)) + @test_throws "does not have properties z" setproperties(sv, z = 3.0) + @test_throws "does not have properties z" setproperties(SVector(1, 2, 3, 4, 5), z = 3.0) + end + + @testset "intervalsets" begin + @test constructorof(typeof(1..2))(0.5, 1.5) === 0.5..1.5 + @test constructorof(typeof(OpenInterval(1, 2)))(0.5, 1.5) === OpenInterval(0.5, 1.5) + @test setproperties(1..2, left=0.0) === 0.0..2.0 + @test setproperties(OpenInterval(1.0, 2.0), left=1, right=5) === OpenInterval(1, 5) + end end