Skip to content

Commit

Permalink
Merge pull request #1 from invenia/ox/method
Browse files Browse the repository at this point in the history
Add code to generate signature from Method
  • Loading branch information
oxinabox authored Apr 14, 2020
2 parents 3ccf6cf + 7819b56 commit 5b643de
Show file tree
Hide file tree
Showing 11 changed files with 442 additions and 12 deletions.
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "ExprTools"
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
authors = ["Curtis Vogt <[email protected]>"]
version = "0.1.0"
authors = ["Invenia Technical Computing"]
version = "0.1.1"

[compat]
julia = "1"
Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ This package aims to provide light-weight performant tooling without requiring a

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

Currently, this package provides the `splitdef` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions.
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.


e.g.
```julia
Expand Down Expand Up @@ -40,6 +44,18 @@ julia> def[:head] = :(=);

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

julia> combinedef(def)
julia> g_expr = combinedef(def)
:((g(x::T, y::T) where T) = x * y)

julia> eval(g_expr)
g (generic function with 1 method)

julia> g_method = first(methods(g))
g(x::T, y::T) where T in Main

julia> signature(g_method)
Dict{Symbol,Any} with 3 entries:
:name => :g
:args => Expr[:(x::T), :(y::T)]
:whereparams => Any[:T]
```
1 change: 1 addition & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ CurrentModule = ExprTools
```@docs
splitdef
combinedef
signature
```
27 changes: 24 additions & 3 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ This package aims to provide light-weight performant tooling without requiring a

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

Currently, this package provides the `splitdef` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions.

ExprTools provides tooling for working with Julia expressions during [metaprogramming](https://docs.julialang.org/en/v1/manual/metaprogramming/).
This package aims to provide light-weight performant tooling without requiring additional package dependencies.

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

Currently, this package provides the `splitdef`, `signature` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions.
- [`splitdef`](@ref) works on a function definition expression and returns a `Dict` of its parts.
- [`combinedef`](@ref) takes `Dict` from `splitdef` and builds it into an expression.
- [`signature`](@ref) works on a `Method` returning a similar `Dict` that holds the parts of the expressions that would form its signature.

e.g.
```jldoctest
Expand Down Expand Up @@ -35,6 +44,18 @@ julia> def[:head] = :(=);
julia> def[:body] = :(x * y);
julia> combinedef(def)
julia> g_expr = combinedef(def)
:((g(x::T, y::T) where T) = x * y)
```
julia> eval(g_expr)
g (generic function with 1 method)
julia> g_method = first(methods(g))
g(x::T, y::T) where T in Main
julia> signature(g_method)
Dict{Symbol,Any} with 3 entries:
:name => :g
:args => Expr[:(x::T), :(y::T)]
:whereparams => Any[:T]
```
5 changes: 3 additions & 2 deletions src/ExprTools.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module ExprTools

export splitdef, combinedef
export signature, splitdef, combinedef

include("function.jl")

include("method.jl")
include("type_utils.jl")
end
10 changes: 7 additions & 3 deletions src/function.jl
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,13 @@ end
Create a function definition expression from various components. Typically used to construct
a function using the result of [`splitdef`](@ref).
If `def[:head]` is not provided it will default to `:function`.
For more details see the documentation on [`splitdef`](@ref).
"""
function combinedef(def::Dict{Symbol,Any})
head = get(def, :head, :function)

# Determine the name of the function including parameterization
name = if haskey(def, :params)
Expr(:curly, def[:name], def[:params]...)
Expand All @@ -170,7 +174,7 @@ function combinedef(def::Dict{Symbol,Any})
# Create a partial function signature including the name and arguments
sig = if name !== nothing
:($name($(args...))) # Equivalent to `Expr(:call, name, args...)` but faster
elseif def[:head] === :(->) && length(args) == 1 && !haskey(def, :kwargs)
elseif head === :(->) && length(args) == 1 && !haskey(def, :kwargs)
args[1]
else
:(($(args...),)) # Equivalent to `Expr(:tuple, args...)` but faster
Expand All @@ -187,9 +191,9 @@ function combinedef(def::Dict{Symbol,Any})
end

func = if haskey(def, :body)
Expr(def[:head], sig, def[:body])
Expr(head, sig, def[:body])
else
Expr(def[:head], name)
Expr(head, name)
end

return func
Expand Down
140 changes: 140 additions & 0 deletions src/method.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""
signature(m::Method) -> Dict{Symbol,Any}
Finds the expression for a method's signature as broken up into its various components
including:
- `:name`: Name of the function
- `:params`: Parametric types defined on constructors
- `:args`: Positional arguments of the function
- `:whereparams`: Where parameters
All components listed above may not be present in the returned dictionary if they are
not in the function definition.
Limited support for:
- `:kwargs`: Keyword arguments of the function.
Only the names will be included, not the default values or type constraints.
Unsupported:
- `:rtype`: Return type of the function
- `:body`: Function body0
- `:head`: Expression head of the function definition (`:function`, `:(=)`, `:(->)`)
For more complete coverage, consider using [`splitdef`](@ref)
with [`CodeTracking.definition`](https://github.com/timholy/CodeTracking.jl).
The dictionary of components returned by `signature` match those returned by
[`splitdef`](@ref) and include all that are required by [`combinedef`](@ref), except for
the `:body` component.
"""
function signature(m::Method)
def = Dict{Symbol, Any}()
def[:name] = m.name

def[:args] = arguments(m)
def[:whereparams] = where_parameters(m)
def[:params] = parameters(m)
def[:kwargs] = kwargs(m)

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

function slot_names(m::Method)
ci = Base.uncompressed_ast(m)
return ci.slotnames
end

function argument_names(m::Method)
slot_syms = slot_names(m)
@assert slot_syms[1] === Symbol("#self#")
arg_names = slot_syms[2:m.nargs] # nargs includes 1 for `#self#`
return arg_names
end


function argument_types(m::Method)
# First parameter of `sig` is the type of the function itself
return parameters(m.sig)[2:end]
end

name_of_type(x) = x
name_of_type(tv::TypeVar) = tv.name
function name_of_type(x::DataType)
name_sym = Symbol(x.name)
if isempty(x.parameters)
return name_sym
else
parameter_names = name_of_type.(x.parameters)
return :($(name_sym){$(parameter_names...)})
end
end
function name_of_type(x::UnionAll)
name = name_of_type(x.body)
whereparam = where_parameters(x.var)
return :($name where $whereparam)
end


function arguments(m::Method)
arg_names = argument_names(m)
arg_types = argument_types(m)
map(arg_names, arg_types) do name, type
has_name = name !== Symbol("#unused#")
type_name = name_of_type(type)
if type === Any && has_name
name
elseif has_name
:($name::$type_name)
else
:(::$type_name)
end
end
end

function where_parameters(x::TypeVar)
if x.lb === Union{} && x.ub === Any
return x.name
elseif x.lb === Union{}
return :($(x.name) <: $(Symbol(x.ub)))
elseif x.ub === Any
return :($(x.name) >: $(Symbol(x.lb)))
else
return :($(Symbol(x.lb)) <: $(x.name) <: $(Symbol(x.ub)))
end
end

function where_parameters(m::Method)
m.sig isa UnionAll || return nothing

whereparams = []
sig = m.sig
while sig isa UnionAll
push!(whereparams, where_parameters(sig.var))
sig = sig.body
end
return whereparams
end

function parameters(m::Method)
typeof_type = first(parameters(m.sig)) # will be e.g Type{Foo{P}} if it has any parameters
typeof_type <: Type{<:Any} || return nothing

function_type = first(parameters(typeof_type)) # will be e.g. Foo{P}
parameter_types = parameters(function_type)
return [name_of_type(type) for type in parameter_types]
end

function kwargs(m::Method)
names = kwarg_names(m)
isempty(names) && return nothing # we know it has no keywords.
# TODO: Enhance this to support more than just their names
# see https://github.com/invenia/ExprTools.jl/issues/6
return names
end

function kwarg_names(m::Method)
mt = Base.get_methodtable(m)
!isdefined(mt, :kwsorter) && return [] # no kwsorter means no keywords for sure.
return Base.kwarg_decl(m, typeof(mt.kwsorter))
end
9 changes: 9 additions & 0 deletions src/type_utils.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
parameters(type)
Extracts the type-parameters of the `type`.
e.g. `parameters(Foo{A, B, C}) == [A, B, C]`
"""
parameters(sig::UnionAll) = parameters(sig.body)
parameters(sig::DataType) = sig.parameters
12 changes: 12 additions & 0 deletions test/function.jl
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,18 @@ function_form(short::Bool) = string(short ? "short" : "long", "-form")
end
end

@testset "combinedef with no `:head`" begin
# should default to `:function`
f, expr = @audit function f() end

d = splitdef(expr)
delete!(d, :head)
@assert !haskey(d, :head)

c_expr = combinedef(d)
@test c_expr == expr
end

@testset "invalid definitions" begin
# Invalid function type
@test_splitdef_invalid Expr(:block)
Expand Down
Loading

2 comments on commit 5b643de

@oxinabox
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/12951

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.1 -m "<description of version>" 5b643de90f46adb2505f30454dc5c3d5993218e2
git push origin v0.1.1

Please sign in to comment.