Skip to content

Commit

Permalink
Merge pull request #18 from invenia/ox/sigtup
Browse files Browse the repository at this point in the history
add signature from type tuple
  • Loading branch information
oxinabox authored Jul 1, 2021
2 parents 30c1a58 + 62f2fe9 commit aaa617d
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Alternatively see the [MacroTools](https://github.com/MikeInnes/MacroTools.jl) p
Currently, this package provides the `splitdef`, `signature` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions.
- `splitdef` works on a function definition expression and returns a `Dict` of its parts.
- `combinedef` takes a `Dict` from `splitdef` and builds it into an expression.
- `signature` works on a `Method` returning a similar `Dict` that holds the parts of the expressions that would form its signature.
- `signature` works on a `Method`, or the type-tuple `sig` field of a method, returning a similar `Dict` that holds the parts of the expressions that would form its signature.

As well as several helpers that are useful in combination with them.
- `args_tuple_expr` applies to a `Dict` from `splitdef` or `signature` to generate an expression for a tuple of its arguments.
Expand Down
92 changes: 91 additions & 1 deletion src/method.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,63 @@ function signature(m::Method)
def[:params] = type_parameters(m)
def[:kwargs] = kwargs(m)

return Dict(k => v for (k, v) in def if v !== nothing) # filter out nonfields.
return filter!(kv->last(kv)!==nothing, def) # filter out nonfields.
end


"""
signature(sig::Type{<:Tuple})
Like `ExprTools.signature(::Method)` but on the underlying signature type-tuple, rather than
the Method`.
For `sig` being a tuple-type representing a methods type signature, this generates a
dictionary that can be passes to `ExprTools.combinedef` to define that function,
Provided that you assign the `:body` key on the dictionary first.
The quality of the output, in terms of matching names etc is not as high as for the
`signature(::Method)`, but all the key information is present; and the type-tuple is for
other purposes generally easier to manipulate.
Examples
```julia
julia> signature(Tuple{typeof(identity), Any})
Dict{Symbol, Any} with 2 entries:
:name => :(op::typeof(identity))
:args => Expr[:(x1::Any)]
julia> signature(Tuple{typeof(+), Vector{T}, Vector{T}} where T<:Number)
Dict{Symbol, Any} with 3 entries:
:name => :(op::typeof(+))
:args => Expr[:(x1::Array{var"##T#5492", 1}), :(x2::Array{var"##T#5492", 1})]
:whereparams => Any[:(var"##T#5492" <: Number)]
```
# keywords
- `extra_hygiene=false`: if set to `true` this forces name-hygine on the `TypeVar`s in
`UnionAll`s, regenerating each with a unique name via `gensym`. This shouldn't actually
be required as they are scoped such that they are not supposed to leak. However, there is
a long-standing [julia bug](https://github.com/JuliaLang/julia/issues/39876) that means
they do leak if they clash with function type-vars.
"""
function signature(orig_sig::Type{<:Tuple}; extra_hygiene=false)
sig = extra_hygiene ? _truly_rename_unionall(orig_sig) : orig_sig
def = Dict{Symbol, Any}()

opT = parameters(sig)[1]
def[:name] = :(op::$opT)

arg_types = name_of_type.(argument_types(sig))
arg_names = [Symbol(:x, ii) for ii in eachindex(arg_types)]
def[:args] = Expr.(:(::), arg_names, arg_types)
def[:whereparams] = where_parameters(sig)

filter!(kv->last(kv)!==nothing, def) # filter out nonfields.
return def
end



function slot_names(m::Method)
ci = Base.uncompressed_ast(m)
return ci.slotnames
Expand Down Expand Up @@ -178,3 +232,39 @@ function kwarg_names(m::Method)
!isdefined(mt, :kwsorter) && return [] # no kwsorter means no keywords for sure.
return Base.kwarg_decl(m, typeof(mt.kwsorter))
end



"""
_truly_rename_unionall(@nospecialize(u))
For `u` being a `UnionAll` this replaces every `TypeVar` with a new one with a `gensym`ed
names.
This shouldn't actually be required as they are scoped such that they are not supposed to leak. However, there is
a long standing [julia bug](https://github.com/JuliaLang/julia/issues/39876) that means
they do leak if they clash with function type-vars.
Example:
```julia
julia> _truly_rename_unionall(Array{T, N} where {T<:Number, N})
Array{var"##T#2881", var"##N#2880"} where var"##N#2880" where var"##T#2881"<:Number
```
Note that the similar `Base.rename_unionall`, though `Base.rename_unionall` does not
`gensym` the names just replaces the instances with new instances with identical names.
"""
function _truly_rename_unionall(@nospecialize(u))
# This works by recursively unwrapping UnionAlls to seperate the TypeVars from body
# changing the name in the TypeVar, and then rewrapping it back up.
# The code is basically the same as `Base.rename_unionall`, but with gensym added
isa(u, UnionAll) || return u
body = _truly_rename_unionall(u.body)
if body === u.body
body = u
else
body = UnionAll(u.var, body)
end
var = u.var::TypeVar
nv = TypeVar(gensym(var.name), var.lb, var.ub)
return UnionAll(nv, body{nv})
end
43 changes: 43 additions & 0 deletions test/method.jl
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,47 @@ end
only_method(OneParamStruct{Float32}, Tuple{Float32, Bool})
)
end

@testset "signature(type_tuple)" begin
# our tests here are much less comprehensive than for `signature(::Method)`
# but that is OK, as most of the code is shared between the two

@test signature(Tuple{typeof(+), Float32, Float32}) == Dict(
# Notice the type of the function object is actually interpolated in to the Expr
# This is useful because it bypasses julia's pretection for overloading things
# which Nabla (and probably others generating overloads) depends upon
:name => :(op::$(typeof(+))),
:args => Expr[:(x1::Float32), :(x2::Float32)],
)

@test signature(Tuple{typeof(+), Array}) == Dict(
:name => :(op::$(typeof(+))),
:args => Expr[:(x1::(Array{T, N} where {T, N}))],
)

@test signature(Tuple{typeof(+), Vector{T}, Matrix{T}} where T<:Real) == Dict(
:name => :(op::$(typeof(+))),
:args => Expr[:(x1::Array{T, 1}), :(x2::Array{T, 2})],
:whereparams => Any[:(T <: Real)],
)

@testset "extra_hygiene" begin
no_hygiene = signature(Tuple{typeof(+),T,Array} where T)
@test no_hygiene == Dict(
:name => :(op::$(typeof(+))),
:args => Expr[:(x1::T), :(x2::(Array{T, N} where {T, N}))],
:whereparams => Any[:T],
)
hygiene = signature(Tuple{typeof(+),T,Array} where T; extra_hygiene=true)
@test no_hygiene[:name] == hygiene[:name]
@test length(no_hygiene[:args]) == 2
@test no_hygiene[:args][1] != hygiene[:args][1] # different Symbols
@test no_hygiene[:args][2] == hygiene[:args][2]

@test length(no_hygiene[:whereparams]) == 1
@test no_hygiene[:whereparams] != hygiene[:whereparams] # different Symbols
# very coarse test to make sure the renamed arg is in the expression it should be
@test occursin(string(no_hygiene[:whereparams][1]), string(no_hygiene[:args][1]))
end
end
end

0 comments on commit aaa617d

Please sign in to comment.