diff --git a/base/util.jl b/base/util.jl index 80c790770af74..1fa0afd270a1b 100644 --- a/base/util.jl +++ b/base/util.jl @@ -633,6 +633,10 @@ expression. The default argument is supplied by declaring fields of the form `fi default` or `field = default`. If no default is provided then the keyword argument becomes a required keyword argument in the resulting type constructor. +Inner constructors can still be defined, but at least one should accept arguments in the +same form as the default inner constructor (i.e. one positional argument per field) in +order to function correctly with the keyword outer constructor. + # Examples ```jldoctest julia> Base.@kwdef struct Foo @@ -652,51 +656,82 @@ Stacktrace: """ macro kwdef(expr) expr = macroexpand(__module__, expr) # to expand @static + expr isa Expr && expr.head == :struct || error("Invalid usage of @kwdef") T = expr.args[2] - params_ex = Expr(:parameters) - call_ex = Expr(:call, T) - _kwdef!(expr.args[3], params_ex, call_ex) - ret = quote - Base.@__doc__($(esc(expr))) + if T isa Expr && T.head == :<: + T = T.args[1] end + + params_ex = Expr(:parameters) + call_args = Any[] + + _kwdef!(expr.args[3], params_ex.args, call_args) # Only define a constructor if the type has fields, otherwise we'll get a stack # overflow on construction if !isempty(params_ex.args) - push!(ret.args, :($(esc(Expr(:call, T, params_ex))) = $(esc(call_ex)))) + if T isa Symbol + kwdefs = :(($(esc(T)))($params_ex) = ($(esc(T)))($(call_args...))) + elseif T isa Expr && T.head == :curly + # if T == S{A<:AA,B<:BB}, define two methods + # S(...) = ... + # S{A,B}(...) where {A<:AA,B<:BB} = ... + S = T.args[1] + P = T.args[2:end] + Q = [U isa Expr && U.head == :<: ? U.args[1] : U for U in P] + SQ = :($S{$(Q...)}) + kwdefs = quote + ($(esc(S)))($params_ex) =($(esc(S)))($(call_args...)) + ($(esc(SQ)))($params_ex) where {$(esc.(P)...)} = + ($(esc(SQ)))($(call_args...)) + end + else + error("Invalid usage of @kwdef") + end + else + kwdefs = nothing + end + quote + Base.@__doc__($(esc(expr))) + $kwdefs end - ret end # @kwdef helper function # mutates arguments inplace -function _kwdef!(blk, params_ex, call_ex) +function _kwdef!(blk, params_args, call_args) for i in eachindex(blk.args) ei = blk.args[i] - if isa(ei, Symbol) - push!(params_ex.args, ei) - push!(call_ex.args, ei) - elseif !isa(ei, Expr) - continue - elseif ei.head == :(=) - # var::Typ = defexpr - dec = ei.args[1] # var::Typ - if isa(dec, Expr) && dec.head == :(::) - var = dec.args[1] - else - var = dec + if ei isa Symbol + # var + push!(params_args, ei) + push!(call_args, ei) + elseif ei isa Expr + if ei.head == :(=) + lhs = ei.args[1] + if lhs isa Symbol + # var = defexpr + var = lhs + elseif lhs isa Expr && lhs.head == :(::) && lhs.args[1] isa Symbol + # var::T = defexpr + var = lhs.args[1] + else + # something else, e.g. inline inner constructor + # F(...) = ... + continue + end + defexpr = ei.args[2] # defexpr + push!(params_args, Expr(:kw, var, esc(defexpr))) + push!(call_args, var) + blk.args[i] = lhs + elseif ei.head == :(::) && ei.args[1] isa Symbol + # var::Typ + var = ei.args[1] + push!(params_args, var) + push!(call_args, var) + elseif ei.head == :block + # can arise with use of @static inside type decl + _kwdef!(ei, params_args, call_args) end - def = ei.args[2] # defexpr - push!(params_ex.args, Expr(:kw, var, def)) - push!(call_ex.args, var) - blk.args[i] = dec - elseif ei.head == :(::) - dec = ei # var::Typ - var = dec.args[1] # var - push!(params_ex.args, var) - push!(call_ex.args, var) - elseif ei.head == :block - # can arise with use of @static inside type decl - _kwdef!(ei, params_ex, call_ex) end end blk diff --git a/contrib/generate_precompile.jl b/contrib/generate_precompile.jl index c3d52f4f65142..4691b86114b5a 100644 --- a/contrib/generate_precompile.jl +++ b/contrib/generate_precompile.jl @@ -163,6 +163,7 @@ function generate_precompile_statements() # println(statement) # Work around #28808 occursin("\"YYYY-mm-dd\\THH:MM:SS\"", statement) && continue + statement == "precompile(Tuple{typeof(Base.show), Base.IOContext{Base.TTY}, Type{Vararg{Any, N} where N}})" && continue try Base.include_string(PrecompileStagingArea, statement) catch ex diff --git a/test/misc.jl b/test/misc.jl index c280fc79d145e..defcba93c082a 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -679,6 +679,42 @@ end @test Test27970Empty() == Test27970Empty() end +abstract type AbstractTest29307 end +@kwdef struct Test29307{T<:Integer} <: AbstractTest29307 + a::T=2 +end + +@testset "subtyped @kwdef" begin + @test Test29307() == Test29307{Int}(2) + @test Test29307(a=0x03) == Test29307{UInt8}(0x03) + @test Test29307{UInt32}() == Test29307{UInt32}(2) + @test Test29307{UInt32}(a=0x03) == Test29307{UInt32}(0x03) +end + +@kwdef struct TestInnerConstructor + a = 1 + TestInnerConstructor(a::Int) = (@assert a>0; new(a)) + function TestInnerConstructor(a::String) + @assert length(a) > 0 + new(a) + end +end + +@testset "@kwdef inner constructor" begin + @test TestInnerConstructor() == TestInnerConstructor(1) + @test TestInnerConstructor(a=2) == TestInnerConstructor(2) + @test_throws AssertionError TestInnerConstructor(a=0) + @test TestInnerConstructor(a="2") == TestInnerConstructor("2") + @test_throws AssertionError TestInnerConstructor(a="") +end + +const outsidevar = 7 +@kwdef struct TestOutsideVar + a::Int=outsidevar +end +@test TestOutsideVar() == TestOutsideVar(7) + + @testset "exports of modules" begin for (_, mod) in Base.loaded_modules for v in names(mod)