Skip to content

Commit 0210b1d

Browse files
authored
at-kwdef support for parametric types and subtypes (#29316)
* at-kwdef support for parametric types and subtypes Fixes #29307.
1 parent 8ff75ad commit 0210b1d

File tree

3 files changed

+104
-32
lines changed

3 files changed

+104
-32
lines changed

base/util.jl

+67-32
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,10 @@ expression. The default argument is supplied by declaring fields of the form `fi
633633
default` or `field = default`. If no default is provided then the keyword argument becomes
634634
a required keyword argument in the resulting type constructor.
635635
636+
Inner constructors can still be defined, but at least one should accept arguments in the
637+
same form as the default inner constructor (i.e. one positional argument per field) in
638+
order to function correctly with the keyword outer constructor.
639+
636640
# Examples
637641
```jldoctest
638642
julia> Base.@kwdef struct Foo
@@ -652,51 +656,82 @@ Stacktrace:
652656
"""
653657
macro kwdef(expr)
654658
expr = macroexpand(__module__, expr) # to expand @static
659+
expr isa Expr && expr.head == :struct || error("Invalid usage of @kwdef")
655660
T = expr.args[2]
656-
params_ex = Expr(:parameters)
657-
call_ex = Expr(:call, T)
658-
_kwdef!(expr.args[3], params_ex, call_ex)
659-
ret = quote
660-
Base.@__doc__($(esc(expr)))
661+
if T isa Expr && T.head == :<:
662+
T = T.args[1]
661663
end
664+
665+
params_ex = Expr(:parameters)
666+
call_args = Any[]
667+
668+
_kwdef!(expr.args[3], params_ex.args, call_args)
662669
# Only define a constructor if the type has fields, otherwise we'll get a stack
663670
# overflow on construction
664671
if !isempty(params_ex.args)
665-
push!(ret.args, :($(esc(Expr(:call, T, params_ex))) = $(esc(call_ex))))
672+
if T isa Symbol
673+
kwdefs = :(($(esc(T)))($params_ex) = ($(esc(T)))($(call_args...)))
674+
elseif T isa Expr && T.head == :curly
675+
# if T == S{A<:AA,B<:BB}, define two methods
676+
# S(...) = ...
677+
# S{A,B}(...) where {A<:AA,B<:BB} = ...
678+
S = T.args[1]
679+
P = T.args[2:end]
680+
Q = [U isa Expr && U.head == :<: ? U.args[1] : U for U in P]
681+
SQ = :($S{$(Q...)})
682+
kwdefs = quote
683+
($(esc(S)))($params_ex) =($(esc(S)))($(call_args...))
684+
($(esc(SQ)))($params_ex) where {$(esc.(P)...)} =
685+
($(esc(SQ)))($(call_args...))
686+
end
687+
else
688+
error("Invalid usage of @kwdef")
689+
end
690+
else
691+
kwdefs = nothing
692+
end
693+
quote
694+
Base.@__doc__($(esc(expr)))
695+
$kwdefs
666696
end
667-
ret
668697
end
669698

670699
# @kwdef helper function
671700
# mutates arguments inplace
672-
function _kwdef!(blk, params_ex, call_ex)
701+
function _kwdef!(blk, params_args, call_args)
673702
for i in eachindex(blk.args)
674703
ei = blk.args[i]
675-
if isa(ei, Symbol)
676-
push!(params_ex.args, ei)
677-
push!(call_ex.args, ei)
678-
elseif !isa(ei, Expr)
679-
continue
680-
elseif ei.head == :(=)
681-
# var::Typ = defexpr
682-
dec = ei.args[1] # var::Typ
683-
if isa(dec, Expr) && dec.head == :(::)
684-
var = dec.args[1]
685-
else
686-
var = dec
704+
if ei isa Symbol
705+
# var
706+
push!(params_args, ei)
707+
push!(call_args, ei)
708+
elseif ei isa Expr
709+
if ei.head == :(=)
710+
lhs = ei.args[1]
711+
if lhs isa Symbol
712+
# var = defexpr
713+
var = lhs
714+
elseif lhs isa Expr && lhs.head == :(::) && lhs.args[1] isa Symbol
715+
# var::T = defexpr
716+
var = lhs.args[1]
717+
else
718+
# something else, e.g. inline inner constructor
719+
# F(...) = ...
720+
continue
721+
end
722+
defexpr = ei.args[2] # defexpr
723+
push!(params_args, Expr(:kw, var, esc(defexpr)))
724+
push!(call_args, var)
725+
blk.args[i] = lhs
726+
elseif ei.head == :(::) && ei.args[1] isa Symbol
727+
# var::Typ
728+
var = ei.args[1]
729+
push!(params_args, var)
730+
push!(call_args, var)
731+
elseif ei.head == :block
732+
# can arise with use of @static inside type decl
733+
_kwdef!(ei, params_args, call_args)
687734
end
688-
def = ei.args[2] # defexpr
689-
push!(params_ex.args, Expr(:kw, var, def))
690-
push!(call_ex.args, var)
691-
blk.args[i] = dec
692-
elseif ei.head == :(::)
693-
dec = ei # var::Typ
694-
var = dec.args[1] # var
695-
push!(params_ex.args, var)
696-
push!(call_ex.args, var)
697-
elseif ei.head == :block
698-
# can arise with use of @static inside type decl
699-
_kwdef!(ei, params_ex, call_ex)
700735
end
701736
end
702737
blk

contrib/generate_precompile.jl

+1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ function generate_precompile_statements()
163163
# println(statement)
164164
# Work around #28808
165165
occursin("\"YYYY-mm-dd\\THH:MM:SS\"", statement) && continue
166+
statement == "precompile(Tuple{typeof(Base.show), Base.IOContext{Base.TTY}, Type{Vararg{Any, N} where N}})" && continue
166167
try
167168
Base.include_string(PrecompileStagingArea, statement)
168169
catch ex

test/misc.jl

+36
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,42 @@ end
679679
@test Test27970Empty() == Test27970Empty()
680680
end
681681

682+
abstract type AbstractTest29307 end
683+
@kwdef struct Test29307{T<:Integer} <: AbstractTest29307
684+
a::T=2
685+
end
686+
687+
@testset "subtyped @kwdef" begin
688+
@test Test29307() == Test29307{Int}(2)
689+
@test Test29307(a=0x03) == Test29307{UInt8}(0x03)
690+
@test Test29307{UInt32}() == Test29307{UInt32}(2)
691+
@test Test29307{UInt32}(a=0x03) == Test29307{UInt32}(0x03)
692+
end
693+
694+
@kwdef struct TestInnerConstructor
695+
a = 1
696+
TestInnerConstructor(a::Int) = (@assert a>0; new(a))
697+
function TestInnerConstructor(a::String)
698+
@assert length(a) > 0
699+
new(a)
700+
end
701+
end
702+
703+
@testset "@kwdef inner constructor" begin
704+
@test TestInnerConstructor() == TestInnerConstructor(1)
705+
@test TestInnerConstructor(a=2) == TestInnerConstructor(2)
706+
@test_throws AssertionError TestInnerConstructor(a=0)
707+
@test TestInnerConstructor(a="2") == TestInnerConstructor("2")
708+
@test_throws AssertionError TestInnerConstructor(a="")
709+
end
710+
711+
const outsidevar = 7
712+
@kwdef struct TestOutsideVar
713+
a::Int=outsidevar
714+
end
715+
@test TestOutsideVar() == TestOutsideVar(7)
716+
717+
682718
@testset "exports of modules" begin
683719
for (_, mod) in Base.loaded_modules
684720
for v in names(mod)

0 commit comments

Comments
 (0)