Skip to content

Commit

Permalink
at-kwdef support for parametric types and subtypes (#29316)
Browse files Browse the repository at this point in the history
* at-kwdef support for parametric types and subtypes

Fixes #29307.
  • Loading branch information
simonbyrne authored Oct 5, 2018
1 parent 8ff75ad commit 0210b1d
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 32 deletions.
99 changes: 67 additions & 32 deletions base/util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions contrib/generate_precompile.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions test/misc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 0210b1d

Please sign in to comment.