Skip to content

Commit aaa617d

Browse files
authored
Merge pull request #18 from invenia/ox/sigtup
add signature from type tuple
2 parents 30c1a58 + 62f2fe9 commit aaa617d

File tree

3 files changed

+135
-2
lines changed

3 files changed

+135
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Alternatively see the [MacroTools](https://github.com/MikeInnes/MacroTools.jl) p
1313
Currently, this package provides the `splitdef`, `signature` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions.
1414
- `splitdef` works on a function definition expression and returns a `Dict` of its parts.
1515
- `combinedef` takes a `Dict` from `splitdef` and builds it into an expression.
16-
- `signature` works on a `Method` returning a similar `Dict` that holds the parts of the expressions that would form its signature.
16+
- `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.
1717

1818
As well as several helpers that are useful in combination with them.
1919
- `args_tuple_expr` applies to a `Dict` from `splitdef` or `signature` to generate an expression for a tuple of its arguments.

src/method.jl

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,63 @@ function signature(m::Method)
3737
def[:params] = type_parameters(m)
3838
def[:kwargs] = kwargs(m)
3939

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

43+
44+
"""
45+
signature(sig::Type{<:Tuple})
46+
47+
Like `ExprTools.signature(::Method)` but on the underlying signature type-tuple, rather than
48+
the Method`.
49+
For `sig` being a tuple-type representing a methods type signature, this generates a
50+
dictionary that can be passes to `ExprTools.combinedef` to define that function,
51+
Provided that you assign the `:body` key on the dictionary first.
52+
53+
The quality of the output, in terms of matching names etc is not as high as for the
54+
`signature(::Method)`, but all the key information is present; and the type-tuple is for
55+
other purposes generally easier to manipulate.
56+
57+
Examples
58+
```julia
59+
julia> signature(Tuple{typeof(identity), Any})
60+
Dict{Symbol, Any} with 2 entries:
61+
:name => :(op::typeof(identity))
62+
:args => Expr[:(x1::Any)]
63+
64+
julia> signature(Tuple{typeof(+), Vector{T}, Vector{T}} where T<:Number)
65+
Dict{Symbol, Any} with 3 entries:
66+
:name => :(op::typeof(+))
67+
:args => Expr[:(x1::Array{var"##T#5492", 1}), :(x2::Array{var"##T#5492", 1})]
68+
:whereparams => Any[:(var"##T#5492" <: Number)]
69+
```
70+
71+
# keywords
72+
73+
- `extra_hygiene=false`: if set to `true` this forces name-hygine on the `TypeVar`s in
74+
`UnionAll`s, regenerating each with a unique name via `gensym`. This shouldn't actually
75+
be required as they are scoped such that they are not supposed to leak. However, there is
76+
a long-standing [julia bug](https://github.com/JuliaLang/julia/issues/39876) that means
77+
they do leak if they clash with function type-vars.
78+
"""
79+
function signature(orig_sig::Type{<:Tuple}; extra_hygiene=false)
80+
sig = extra_hygiene ? _truly_rename_unionall(orig_sig) : orig_sig
81+
def = Dict{Symbol, Any}()
82+
83+
opT = parameters(sig)[1]
84+
def[:name] = :(op::$opT)
85+
86+
arg_types = name_of_type.(argument_types(sig))
87+
arg_names = [Symbol(:x, ii) for ii in eachindex(arg_types)]
88+
def[:args] = Expr.(:(::), arg_names, arg_types)
89+
def[:whereparams] = where_parameters(sig)
90+
91+
filter!(kv->last(kv)!==nothing, def) # filter out nonfields.
92+
return def
93+
end
94+
95+
96+
4397
function slot_names(m::Method)
4498
ci = Base.uncompressed_ast(m)
4599
return ci.slotnames
@@ -178,3 +232,39 @@ function kwarg_names(m::Method)
178232
!isdefined(mt, :kwsorter) && return [] # no kwsorter means no keywords for sure.
179233
return Base.kwarg_decl(m, typeof(mt.kwsorter))
180234
end
235+
236+
237+
238+
"""
239+
_truly_rename_unionall(@nospecialize(u))
240+
241+
For `u` being a `UnionAll` this replaces every `TypeVar` with a new one with a `gensym`ed
242+
names.
243+
This shouldn't actually be required as they are scoped such that they are not supposed to leak. However, there is
244+
a long standing [julia bug](https://github.com/JuliaLang/julia/issues/39876) that means
245+
they do leak if they clash with function type-vars.
246+
247+
Example:
248+
```julia
249+
julia> _truly_rename_unionall(Array{T, N} where {T<:Number, N})
250+
Array{var"##T#2881", var"##N#2880"} where var"##N#2880" where var"##T#2881"<:Number
251+
```
252+
253+
Note that the similar `Base.rename_unionall`, though `Base.rename_unionall` does not
254+
`gensym` the names just replaces the instances with new instances with identical names.
255+
"""
256+
function _truly_rename_unionall(@nospecialize(u))
257+
# This works by recursively unwrapping UnionAlls to seperate the TypeVars from body
258+
# changing the name in the TypeVar, and then rewrapping it back up.
259+
# The code is basically the same as `Base.rename_unionall`, but with gensym added
260+
isa(u, UnionAll) || return u
261+
body = _truly_rename_unionall(u.body)
262+
if body === u.body
263+
body = u
264+
else
265+
body = UnionAll(u.var, body)
266+
end
267+
var = u.var::TypeVar
268+
nv = TypeVar(gensym(var.name), var.lb, var.ub)
269+
return UnionAll(nv, body{nv})
270+
end

test/method.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,47 @@ end
234234
only_method(OneParamStruct{Float32}, Tuple{Float32, Bool})
235235
)
236236
end
237+
238+
@testset "signature(type_tuple)" begin
239+
# our tests here are much less comprehensive than for `signature(::Method)`
240+
# but that is OK, as most of the code is shared between the two
241+
242+
@test signature(Tuple{typeof(+), Float32, Float32}) == Dict(
243+
# Notice the type of the function object is actually interpolated in to the Expr
244+
# This is useful because it bypasses julia's pretection for overloading things
245+
# which Nabla (and probably others generating overloads) depends upon
246+
:name => :(op::$(typeof(+))),
247+
:args => Expr[:(x1::Float32), :(x2::Float32)],
248+
)
249+
250+
@test signature(Tuple{typeof(+), Array}) == Dict(
251+
:name => :(op::$(typeof(+))),
252+
:args => Expr[:(x1::(Array{T, N} where {T, N}))],
253+
)
254+
255+
@test signature(Tuple{typeof(+), Vector{T}, Matrix{T}} where T<:Real) == Dict(
256+
:name => :(op::$(typeof(+))),
257+
:args => Expr[:(x1::Array{T, 1}), :(x2::Array{T, 2})],
258+
:whereparams => Any[:(T <: Real)],
259+
)
260+
261+
@testset "extra_hygiene" begin
262+
no_hygiene = signature(Tuple{typeof(+),T,Array} where T)
263+
@test no_hygiene == Dict(
264+
:name => :(op::$(typeof(+))),
265+
:args => Expr[:(x1::T), :(x2::(Array{T, N} where {T, N}))],
266+
:whereparams => Any[:T],
267+
)
268+
hygiene = signature(Tuple{typeof(+),T,Array} where T; extra_hygiene=true)
269+
@test no_hygiene[:name] == hygiene[:name]
270+
@test length(no_hygiene[:args]) == 2
271+
@test no_hygiene[:args][1] != hygiene[:args][1] # different Symbols
272+
@test no_hygiene[:args][2] == hygiene[:args][2]
273+
274+
@test length(no_hygiene[:whereparams]) == 1
275+
@test no_hygiene[:whereparams] != hygiene[:whereparams] # different Symbols
276+
# very coarse test to make sure the renamed arg is in the expression it should be
277+
@test occursin(string(no_hygiene[:whereparams][1]), string(no_hygiene[:args][1]))
278+
end
279+
end
237280
end

0 commit comments

Comments
 (0)