Skip to content

Commit 5b643de

Browse files
authored
Merge pull request #1 from invenia/ox/method
Add code to generate signature from Method
2 parents 3ccf6cf + 7819b56 commit 5b643de

File tree

11 files changed

+442
-12
lines changed

11 files changed

+442
-12
lines changed

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ExprTools"
22
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
3-
authors = ["Curtis Vogt <[email protected]>"]
4-
version = "0.1.0"
3+
authors = ["Invenia Technical Computing"]
4+
version = "0.1.1"
55

66
[compat]
77
julia = "1"

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ This package aims to provide light-weight performant tooling without requiring a
1010

1111
Alternatively see the [MacroTools](https://github.com/MikeInnes/MacroTools.jl) package for a more powerful set of tools.
1212

13-
Currently, this package provides the `splitdef` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions.
13+
Currently, this package provides the `splitdef`, `signature` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions.
14+
- `splitdef` works on a function definition expression and returns a `Dict` of its parts.
15+
- `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.
17+
1418

1519
e.g.
1620
```julia
@@ -40,6 +44,18 @@ julia> def[:head] = :(=);
4044

4145
julia> def[:body] = :(x * y);
4246

43-
julia> combinedef(def)
47+
julia> g_expr = combinedef(def)
4448
:((g(x::T, y::T) where T) = x * y)
49+
50+
julia> eval(g_expr)
51+
g (generic function with 1 method)
52+
53+
julia> g_method = first(methods(g))
54+
g(x::T, y::T) where T in Main
55+
56+
julia> signature(g_method)
57+
Dict{Symbol,Any} with 3 entries:
58+
:name => :g
59+
:args => Expr[:(x::T), :(y::T)]
60+
:whereparams => Any[:T]
4561
```

docs/src/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ CurrentModule = ExprTools
1010
```@docs
1111
splitdef
1212
combinedef
13+
signature
1314
```

docs/src/index.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ This package aims to provide light-weight performant tooling without requiring a
55

66
Alternatively see the [MacroTools](https://github.com/MikeInnes/MacroTools.jl) package for more powerful set of tools.
77

8-
Currently, this package provides the `splitdef` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions.
8+
9+
ExprTools provides tooling for working with Julia expressions during [metaprogramming](https://docs.julialang.org/en/v1/manual/metaprogramming/).
10+
This package aims to provide light-weight performant tooling without requiring additional package dependencies.
11+
12+
Alternatively see the [MacroTools](https://github.com/MikeInnes/MacroTools.jl) package for more powerful set of tools.
13+
14+
Currently, this package provides the `splitdef`, `signature` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions.
15+
- [`splitdef`](@ref) works on a function definition expression and returns a `Dict` of its parts.
16+
- [`combinedef`](@ref) takes `Dict` from `splitdef` and builds it into an expression.
17+
- [`signature`](@ref) works on a `Method` returning a similar `Dict` that holds the parts of the expressions that would form its signature.
918

1019
e.g.
1120
```jldoctest
@@ -35,6 +44,18 @@ julia> def[:head] = :(=);
3544
3645
julia> def[:body] = :(x * y);
3746
38-
julia> combinedef(def)
47+
julia> g_expr = combinedef(def)
3948
:((g(x::T, y::T) where T) = x * y)
40-
```
49+
50+
julia> eval(g_expr)
51+
g (generic function with 1 method)
52+
53+
julia> g_method = first(methods(g))
54+
g(x::T, y::T) where T in Main
55+
56+
julia> signature(g_method)
57+
Dict{Symbol,Any} with 3 entries:
58+
:name => :g
59+
:args => Expr[:(x::T), :(y::T)]
60+
:whereparams => Any[:T]
61+
```

src/ExprTools.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
module ExprTools
22

3-
export splitdef, combinedef
3+
export signature, splitdef, combinedef
44

55
include("function.jl")
6-
6+
include("method.jl")
7+
include("type_utils.jl")
78
end

src/function.jl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,13 @@ end
141141
Create a function definition expression from various components. Typically used to construct
142142
a function using the result of [`splitdef`](@ref).
143143
144+
If `def[:head]` is not provided it will default to `:function`.
145+
144146
For more details see the documentation on [`splitdef`](@ref).
145147
"""
146148
function combinedef(def::Dict{Symbol,Any})
149+
head = get(def, :head, :function)
150+
147151
# Determine the name of the function including parameterization
148152
name = if haskey(def, :params)
149153
Expr(:curly, def[:name], def[:params]...)
@@ -170,7 +174,7 @@ function combinedef(def::Dict{Symbol,Any})
170174
# Create a partial function signature including the name and arguments
171175
sig = if name !== nothing
172176
:($name($(args...))) # Equivalent to `Expr(:call, name, args...)` but faster
173-
elseif def[:head] === :(->) && length(args) == 1 && !haskey(def, :kwargs)
177+
elseif head === :(->) && length(args) == 1 && !haskey(def, :kwargs)
174178
args[1]
175179
else
176180
:(($(args...),)) # Equivalent to `Expr(:tuple, args...)` but faster
@@ -187,9 +191,9 @@ function combinedef(def::Dict{Symbol,Any})
187191
end
188192

189193
func = if haskey(def, :body)
190-
Expr(def[:head], sig, def[:body])
194+
Expr(head, sig, def[:body])
191195
else
192-
Expr(def[:head], name)
196+
Expr(head, name)
193197
end
194198

195199
return func

src/method.jl

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""
2+
signature(m::Method) -> Dict{Symbol,Any}
3+
4+
Finds the expression for a method's signature as broken up into its various components
5+
including:
6+
7+
- `:name`: Name of the function
8+
- `:params`: Parametric types defined on constructors
9+
- `:args`: Positional arguments of the function
10+
- `:whereparams`: Where parameters
11+
12+
All components listed above may not be present in the returned dictionary if they are
13+
not in the function definition.
14+
15+
Limited support for:
16+
- `:kwargs`: Keyword arguments of the function.
17+
Only the names will be included, not the default values or type constraints.
18+
19+
Unsupported:
20+
- `:rtype`: Return type of the function
21+
- `:body`: Function body0
22+
- `:head`: Expression head of the function definition (`:function`, `:(=)`, `:(->)`)
23+
24+
For more complete coverage, consider using [`splitdef`](@ref)
25+
with [`CodeTracking.definition`](https://github.com/timholy/CodeTracking.jl).
26+
27+
The dictionary of components returned by `signature` match those returned by
28+
[`splitdef`](@ref) and include all that are required by [`combinedef`](@ref), except for
29+
the `:body` component.
30+
"""
31+
function signature(m::Method)
32+
def = Dict{Symbol, Any}()
33+
def[:name] = m.name
34+
35+
def[:args] = arguments(m)
36+
def[:whereparams] = where_parameters(m)
37+
def[:params] = parameters(m)
38+
def[:kwargs] = kwargs(m)
39+
40+
return Dict(k => v for (k, v) in def if v !== nothing) # filter out nonfields.
41+
end
42+
43+
function slot_names(m::Method)
44+
ci = Base.uncompressed_ast(m)
45+
return ci.slotnames
46+
end
47+
48+
function argument_names(m::Method)
49+
slot_syms = slot_names(m)
50+
@assert slot_syms[1] === Symbol("#self#")
51+
arg_names = slot_syms[2:m.nargs] # nargs includes 1 for `#self#`
52+
return arg_names
53+
end
54+
55+
56+
function argument_types(m::Method)
57+
# First parameter of `sig` is the type of the function itself
58+
return parameters(m.sig)[2:end]
59+
end
60+
61+
name_of_type(x) = x
62+
name_of_type(tv::TypeVar) = tv.name
63+
function name_of_type(x::DataType)
64+
name_sym = Symbol(x.name)
65+
if isempty(x.parameters)
66+
return name_sym
67+
else
68+
parameter_names = name_of_type.(x.parameters)
69+
return :($(name_sym){$(parameter_names...)})
70+
end
71+
end
72+
function name_of_type(x::UnionAll)
73+
name = name_of_type(x.body)
74+
whereparam = where_parameters(x.var)
75+
return :($name where $whereparam)
76+
end
77+
78+
79+
function arguments(m::Method)
80+
arg_names = argument_names(m)
81+
arg_types = argument_types(m)
82+
map(arg_names, arg_types) do name, type
83+
has_name = name !== Symbol("#unused#")
84+
type_name = name_of_type(type)
85+
if type === Any && has_name
86+
name
87+
elseif has_name
88+
:($name::$type_name)
89+
else
90+
:(::$type_name)
91+
end
92+
end
93+
end
94+
95+
function where_parameters(x::TypeVar)
96+
if x.lb === Union{} && x.ub === Any
97+
return x.name
98+
elseif x.lb === Union{}
99+
return :($(x.name) <: $(Symbol(x.ub)))
100+
elseif x.ub === Any
101+
return :($(x.name) >: $(Symbol(x.lb)))
102+
else
103+
return :($(Symbol(x.lb)) <: $(x.name) <: $(Symbol(x.ub)))
104+
end
105+
end
106+
107+
function where_parameters(m::Method)
108+
m.sig isa UnionAll || return nothing
109+
110+
whereparams = []
111+
sig = m.sig
112+
while sig isa UnionAll
113+
push!(whereparams, where_parameters(sig.var))
114+
sig = sig.body
115+
end
116+
return whereparams
117+
end
118+
119+
function parameters(m::Method)
120+
typeof_type = first(parameters(m.sig)) # will be e.g Type{Foo{P}} if it has any parameters
121+
typeof_type <: Type{<:Any} || return nothing
122+
123+
function_type = first(parameters(typeof_type)) # will be e.g. Foo{P}
124+
parameter_types = parameters(function_type)
125+
return [name_of_type(type) for type in parameter_types]
126+
end
127+
128+
function kwargs(m::Method)
129+
names = kwarg_names(m)
130+
isempty(names) && return nothing # we know it has no keywords.
131+
# TODO: Enhance this to support more than just their names
132+
# see https://github.com/invenia/ExprTools.jl/issues/6
133+
return names
134+
end
135+
136+
function kwarg_names(m::Method)
137+
mt = Base.get_methodtable(m)
138+
!isdefined(mt, :kwsorter) && return [] # no kwsorter means no keywords for sure.
139+
return Base.kwarg_decl(m, typeof(mt.kwsorter))
140+
end

src/type_utils.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
parameters(type)
3+
4+
Extracts the type-parameters of the `type`.
5+
6+
e.g. `parameters(Foo{A, B, C}) == [A, B, C]`
7+
"""
8+
parameters(sig::UnionAll) = parameters(sig.body)
9+
parameters(sig::DataType) = sig.parameters

test/function.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,18 @@ function_form(short::Bool) = string(short ? "short" : "long", "-form")
825825
end
826826
end
827827

828+
@testset "combinedef with no `:head`" begin
829+
# should default to `:function`
830+
f, expr = @audit function f() end
831+
832+
d = splitdef(expr)
833+
delete!(d, :head)
834+
@assert !haskey(d, :head)
835+
836+
c_expr = combinedef(d)
837+
@test c_expr == expr
838+
end
839+
828840
@testset "invalid definitions" begin
829841
# Invalid function type
830842
@test_splitdef_invalid Expr(:block)

0 commit comments

Comments
 (0)