Manual
ExprParsers
exports a constant EP
which is an alias for the package ExprParsers
itself. This comes in very handy when you use the custom parsers a lot.
The main interface encompass just three concepts, which seamlessly interact with oneanother
- macro
EP.@exprparser
: easily create definitions for highly flexible and nestable parsers - function
parse_expr
: compares a parser with a value, and returns a parsed result - function
to_expr
: transforms parsed values back toBase.Expr
Many parsers have already been defined, ready for use, and well documented. Take a look at Public API.
Extended Example: Part I Combining Parsers
To understand how to work with ExprParsers
in practice, it is best to see a full-fledged example.
One nice and complex task would be to implement support for the traitor-like-syntax introduced by the package Traitor.jl
. The syntax may no longer be the nicest, but still it is quite powerful and serves as a good example (I myself build another Traits package which extends where syntax instead, which many people may find more intuitive these days WhereTraits.jl
).
In Traitor syntax, you have a double type annotation argument::StandardType::TraitsType
. The StandardType
is a plain julia type like Int
or AbstractArray
. The TraitsType
is something similar to Base.HasEltype
or Base.HasLength
. For our goal it doesn't matter so much, we just want to parse the syntax.
Let's start with loading the package and creating a default parser for functions.
julia> using ExprParsers
julia> parser_function = EP.Function(name = EP.anysymbol)
EP.Function(
name = ExprParsers.Isa{Symbol}()
curlies = ExprParsers.Isa{Any}()
args = ExprParsers.Isa{Any}()
kwargs = ExprParsers.Isa{Any}()
wheres = ExprParsers.Isa{Any}()
body = ExprParsers.Isa{Any}()
)
As you can see, we only gave a subparser for the field name
which captures the function name. All other substructures default to be able to parse anything.
Now we parse our first function. Mind that we need to use :(...)
for constructing Expr, as quote...end
would introduce an additional Expr(:block, ...)
layer.
julia> parsed_function = parse_expr(parser_function, :(
function f(a, b::Int::HasSomeTrait)
"a = $a, b = $b, sometrait(b) = $(sometrait(b))"
end
))
EP.Function_Parsed(
name = :f
curlies = Any[]
args = Any[:a, :((b::Int)::HasSomeTrait)]
kwargs = Any[]
wheres = Any[]
body = quote
#= none:3 =#
"a = $(a), b = $(b), sometrait(b) = $(sometrait(b))"
end
)
We have our parsed function, but the arguments are still quite rough. Lets parse them further by using EP.Arg
. Conveniently, all ExprParsers support broadcasting syntax.
julia> parsed_args = parse_expr.(EP.Arg(), parsed_function.args)
2-element Array{ExprParsers.Arg_Parsed,1}:
EP.Arg_Parsed(name=:a, type=Any, default=ExprParsers.NoDefault())
EP.Arg_Parsed(name=:(b::Int), type=:HasSomeTrait, default=ExprParsers.NoDefault())
That is nice, we captured HasSomeTrait
in the type field. This is because argument::StandardType::TraitsType
is evaluated as (argument::StandardType)::TraitsType
. So we would like to get access to the b::Int
as well.
But now things start to get difficult, because there is also :a
with type=Any
, and somehow we need to separate both cases.
We can easily do this by defining more specific parsers. Lets start with the "standard" Parser.
julia> parser_standard_arg = EP.Arg(name=EP.anysymbol)
EP.Arg(
name = ExprParsers.Isa{Symbol}()
type = ExprParsers.Isa{Any}()
default = ExprParsers.Isa{Any}()
)
julia> parse_expr(parser_standard_arg, parsed_function.args[1])
EP.Arg_Parsed(
name = :a
type = Any
default = ExprParsers.NoDefault()
)
julia> parse_expr(parser_standard_arg, parsed_function.args[2])
ERROR: ParseError: Expected type `Symbol`, got `b::Int` of type `Expr`.
That looks very good: Our new parser only parsers standard arguments and fails on traitor-like syntax. In addition it super simple - we just required the name to be a Base.Symbol
.
Now let's define the traitor-argument. We know that the "name" field will be just another type-annotation. Let's use that knowledge straight away.
julia> parser_traitor_arg = EP.Arg(name=EP.TypeAnnotation())
EP.Arg(
name = EP.TypeAnnotation(name=ExprParsers.Isa{Any}(), type=ExprParsers.Isa{Any}())
type = ExprParsers.Isa{Any}()
default = ExprParsers.Isa{Any}()
)
julia> parse_expr(parser_traitor_arg, parsed_function.args[1])
ERROR: ParseError: ExprParsers.TypeAnnotation has no `parse_expr` method defined to capture Type `Symbol`. Got: `a`.
julia> parse_expr(parser_traitor_arg, parsed_function.args[2])
EP.Arg_Parsed(
name = EP.TypeAnnotation_Parsed(name=:b, type=:Int)
type = :HasSomeTrait
default = ExprParsers.NoDefault()
)
It worked. We only parsed traitor syntax, and now have all the components readily available.
We have all the pieces together, now we just need to combine them. EP.AnyOf
is the natural choice to combine multiple parsers by falling back to the next if the first fails.
julia> parser_arg = EP.AnyOf(parser_standard_arg, parser_traitor_arg)
ExprParsers.AnyOf{Tuple{ExprParsers.Arg,ExprParsers.Arg}}((EP.Arg(name=ExprParsers.Isa{Symbol}(), type=ExprParsers.Isa{Any}(), default=ExprParsers.Isa{Any}()), EP.Arg(name=EP.TypeAnnotation(name=ExprParsers.Isa{Any}(), type=ExprParsers.Isa{Any}()), type=ExprParsers.Isa{Any}(), default=ExprParsers.Isa{Any}())), "")
julia> parse_expr(parser_arg, parsed_function.args[1])
EP.Arg_Parsed(
name = :a
type = Any
default = ExprParsers.NoDefault()
)
julia> parse_expr(parser_arg, parsed_function.args[2])
EP.Arg_Parsed(
name = EP.TypeAnnotation_Parsed(name=:b, type=:Int)
type = :HasSomeTrait
default = ExprParsers.NoDefault()
)
julia> parse_expr.(parser_arg, parsed_function.args)
2-element Array{ExprParsers.Arg_Parsed,1}:
EP.Arg_Parsed(name=:a, type=Any, default=ExprParsers.NoDefault())
EP.Arg_Parsed(name=EP.TypeAnnotation_Parsed(name=:b, type=:Int), type=:HasSomeTrait, default=ExprParsers.NoDefault())
Here we are! We created our parser pipeline in a very simple and intuitive way and in the end we parsed all the information.
Extended Example: Part II Working with parsed results
One thing which is missing though, is to extract the information in a convenient way. In julia the most convenient way is to use dispatch. But how to dispatch in the example above? Both versions are of type ExprParsers.Arg_Parsed
.
We could have made the types more complicated, e.g. by including the types of all subparsers as well, however we decided against it and in favour for a simple meta-type EP.Named{Tag}
. Bringing this into a meta-expr-parser has the big advantage that you can easily create your own ExprParsers without bothering about the precise types you use.
EP.Named{Tag}(some_parser)
constructs a named version of your parser, which preserves the name when put through parse_expr
. That is it's key purpose. Let's see it it in action.
julia> parser_arg_named = EP.AnyOf(
EP.Named{:standard}(parser_standard_arg),
EP.Named{:traitor}(parser_traitor_arg)
)
ExprParsers.AnyOf{Tuple{ExprParsers.Named{:standard,ExprParsers.Arg},ExprParsers.Named{:traitor,ExprParsers.Arg}}}((ExprParsers.Named{:standard,ExprParsers.Arg}(EP.Arg(name=ExprParsers.Isa{Symbol}(), type=ExprParsers.Isa{Any}(), default=ExprParsers.Isa{Any}())), ExprParsers.Named{:traitor,ExprParsers.Arg}(EP.Arg(name=EP.TypeAnnotation(name=ExprParsers.Isa{Any}(), type=ExprParsers.Isa{Any}()), type=ExprParsers.Isa{Any}(), default=ExprParsers.Isa{Any}()))), "")
julia> parsed_args = parse_expr.(parser_arg_named, parsed_function.args)
2-element Array{ExprParsers.Named{Name,ExprParsers.Arg_Parsed} where Name,1}:
ExprParsers.Named{:standard,ExprParsers.Arg_Parsed}(EP.Arg_Parsed(name=:a, type=Any, default=ExprParsers.NoDefault()))
ExprParsers.Named{:traitor,ExprParsers.Arg_Parsed}(EP.Arg_Parsed(name=EP.TypeAnnotation_Parsed(name=:b, type=:Int), type=:HasSomeTrait, default=ExprParsers.NoDefault()))
We did the same as before, but added some type-tags to the results. You see that wrapping a parser into EP.Named{Tag}(...)
will seamlessly and intuitively pass on the Tag
to the parsed result. That is exactly what we need to simplify our dispatch.
Finally, let's write some utility which extracts all the Traitor information in a custom type.
julia> Base.@kwdef struct TraitorArg
name
type
traitstype
default
end
TraitorArg
julia> extract_traitsarg(parsed::EP.Named{:standard}) = TraitorArg(
name = parsed[].name,
type = parsed[].type,
traitstype = Any,
default = parsed[].default,
)
extract_traitsarg (generic function with 1 method)
julia> extract_traitsarg(parsed::EP.Named{:traitor}) = TraitorArg(
name = parsed[].name.name,
type = parsed[].name.type,
traitstype = parsed[].type,
default = parsed[].default,
)
extract_traitsarg (generic function with 2 methods)
julia> traitor_args = extract_traitsarg.(parse_expr.(parser_arg_named, parsed_function.args))
2-element Array{TraitorArg,1}:
TraitorArg(:a, Any, Any, ExprParsers.NoDefault())
TraitorArg(:b, :Int, :HasSomeTrait, ExprParsers.NoDefault())
Super clean result and intuitive Julian style of programming. You can see that the use of EP.Named
actually simplified the dispatch a lot and made it extremely readable.
From here on you can start to fill the traitor syntax with life. It turns out that we missed on one special Traitor-syntax case. Concretely, the argument style a::::TraitsType
is officially supported, but not captured by either of the above. If I got you motivated, try to implement this case yourself!
I hope you enjoyed this extended example and got a good feeling of how to work with ExprParsers
.
Extended Example: Part III creating Expr again
Almost forgot: When defining your own macro there will come the time when you want to construct Expr
objects again in order to return them to the user.
ExprParsers
are also made for this task. One way to do this is to manipulate the parsed results directly. Let's see what this means.
julia> using ExprParsers
julia> parsed = parse_expr(EP.Function(), :(f(x) = x))
EP.Function_Parsed(
name = :f
curlies = Any[]
args = Any[:x]
kwargs = Any[]
wheres = Any[]
body = quote
#= none:1 =#
x
end
)
julia> parsed.name = :anothername;
julia> parsed
EP.Function_Parsed(
name = :anothername
curlies = Any[]
args = Any[:x]
kwargs = Any[]
wheres = Any[]
body = quote
#= none:1 =#
x
end
)
julia> to_expr(parsed)
:(function anothername(x)
#= none:1 =#
x
end)
The key is that every parsed result can be converted to an Expr by using to_expr
.
The second way to construct Expr is by constructing parsed objects directly. You just have to use the EP.Function_Parsed
instead of EP.Function
, i.e. appending _Parsed
to your parser type and you get the constructor for the parsed object. It supports keyword assignment together with useful default values. In general it is very handy.
julia> using ExprParsers
julia> func = EP.Function_Parsed(
name = :myfunc,
args = [:(a::T), EP.Arg_Parsed(name = :b, default = 42)],
kwargs = [EP.Arg_Parsed(name = :c, default = :hi)],
wheres = [:T],
body = :(a + b)
)
EP.Function_Parsed(
name = :myfunc
curlies = Any[]
args = Any[:(a::T), EP.Arg_Parsed(name=:b, type=Any, default=42)]
kwargs = ExprParsers.Arg_Parsed[EP.Arg_Parsed(name=:c, type=Any, default=:hi)]
wheres = [:T]
body = :(a + b)
)
julia> to_expr(func)
:(function myfunc(a::T, b = 42; c = hi) where T
a + b
end)
For instance, for the Traitor syntax such manual construction would be needed, as quite some transformations need to happen to arrive at the traits semantics. As you can see, it is very easy to do this using ExprParsers
.
Extended Example: Part IV Defining your own ExprParser with EP.@exprparser
As a final little excurse about the Traitor syntax example, I would like to show you how you can easily build your own ExprParsers using the macro EP.@exprparsers
. It is also used internally to most of the parsers available.
For example, let us rewrite the previous code which we used to extract Traitor information. For easier reference, here a self-containing copy of what we did above:
julia> using ExprParsers
julia> parser_standard_arg = EP.Arg(name=EP.anysymbol);
julia> parser_traitor_arg = EP.Arg(name=EP.TypeAnnotation());
julia> parser_arg_named = EP.AnyOf(
EP.Named{:standard}(parser_standard_arg),
EP.Named{:traitor}(parser_traitor_arg),
);
julia> Base.@kwdef struct TraitorArg
name
type
traitstype
default
end
TraitorArg
julia> extract_traitsarg(parsed::EP.Named{:standard}) = TraitorArg(
name = parsed[].name,
type = parsed[].type,
traitstype = Any,
default = parsed[].default,
)
extract_traitsarg (generic function with 1 method)
julia> extract_traitsarg(parsed::EP.Named{:traitor}) = TraitorArg(
name = parsed[].name.name,
type = parsed[].name.type,
traitstype = parsed[].type,
default = parsed[].default,
)
extract_traitsarg (generic function with 2 methods)
julia> # which we then used for extraction
julia> parsed_function = parse_expr(EP.Function(), :(
function f(a, b::Int::HasSomeTrait)
"a = $a, b = $b, sometrait(b) = $(sometrait(b))"
end
));
julia> traitor_args = extract_traitsarg.(parse_expr.(parser_arg_named, parsed_function.args))
2-element Array{TraitorArg,1}:
TraitorArg(:a, Any, Any, ExprParsers.NoDefault())
TraitorArg(:b, :Int, :HasSomeTrait, ExprParsers.NoDefault())
We can easily transform this into a full-fledged ExprParser
by making using of the EP.@exprparser
macro.
julia> EP.@exprparser struct TraitorArgExprParser
name = EP.anything
type = EP.anything = Any
traitstype = EP.anything = Any
default = EP.anything = EP.nodefault
end;
julia> function EP.parse_expr(parser::TraitorArgExprParser, expr)
_parse_expr_traitor(parser, parse_expr(parser_arg_named, expr))
end;
julia> _parse_expr_traitor(parser, parsed::EP.Named{:standard}) = parse_expr(parser,
name = parsed[].name,
type = parsed[].type,
traitstype = Any,
default = parsed[].default,
)
_parse_expr_traitor (generic function with 1 method)
julia> _parse_expr_traitor(parser, parsed::EP.Named{:traitor}) = parse_expr(parser,
name = parsed[].name.name,
type = parsed[].name.type,
traitstype = parsed[].type,
default = parsed[].default,
)
_parse_expr_traitor (generic function with 2 methods)
julia> function EP.to_expr(parsed::TraitorArgExprParser_Parsed)
if parsed.default === EP.nodefault
if parsed.traitstype == Any
:($(parsed.name)::$(parsed.type))
else
:($(parsed.name)::$(parsed.type)::$(parsed.traitstype))
end
else
# Note that function keyword arguments are constructed using `Expr(:kw, ...)` and not plain `=`
if parsed.traitstype == Any
Expr(:kw, :($(parsed.name)::$(parsed.type)), parsed.default)
else
Expr(:kw, :($(parsed.name)::$(parsed.type)::$(parsed.traitstype)), parsed.default)
end
end
end;
That defines the ExprParser
as well as the key interface parse_expr
and to_expr
. Let's go through it one by one. First we defined the parser using EP.@exprparser
. It is very much identical to defining a simple struct with Base.kwdef
support, only that in addition you can make use of a neat double-default-syntax. default = EP.anything = EP.nodefault
means that the "first" default value EP.anything
is the default sub-parser which is used when constructing an TraitorArgExprParser
, while the "second" default value EP.nodefault
is the default value used when constructing the parsed result TraitorArgExprParser_Parsed
.
Then we defined parse_expr(...)
for our new type, which simply dispatches on the Named{Tag}
parsers within the sub-function _parse_expr_traitor
to actually construct the parsed results. One general feature we used here is that parse_expr(...)
by default always supports a generic keyword syntax parse_expr(parser, field1 = :value_to_be_parsed, field2 = ...)
which translates to constructing the parsed result, with all fields matched by the respective sub-parsers ..._Parsed(field1 = parse_expr(parser.field1, :value_to_be_parsed), field2 = ...)
. Using this syntax we make sure that always all sub-parsers are actually used and not overlooked accidentally.
Finally we also overloaded EP.to_expr
on top of the ..._Parsed
type so that we can easily convert traitor arguments back to proper Base.Expr
objects.
It is not much to define, and everything very straightforward. Now we have a fully flexible full-fledged TraitorArgument parser. Let's see it in action.
julia> parser_traitor_arg = TraitorArgExprParser(default = EP.Isa(Union{EP.NoDefault, Int}))
EP.TraitorArgExprParser(
name = ExprParsers.Isa{Any}()
type = ExprParsers.Isa{Any}()
traitstype = ExprParsers.Isa{Any}()
default = ExprParsers.Isa{Union{ExprParsers.NoDefault, Int64}}()
)
julia> traitor_args = parse_expr.(parser_traitor_arg, parsed_function.args)
2-element Array{TraitorArgExprParser_Parsed,1}:
EP.TraitorArgExprParser_Parsed(name=:a, type=Any, traitstype=Any, default=ExprParsers.NoDefault())
EP.TraitorArgExprParser_Parsed(name=:b, type=:Int, traitstype=:HasSomeTrait, default=ExprParsers.NoDefault())
julia> traitor_arg = parse_expr(parser_traitor_arg, Expr(:kw, :(a::Int::TraitsType), 4)) # Note that we need to use `Expr(:kw, ...)` to construct function keyword arguments
EP.TraitorArgExprParser_Parsed(
name = :a
type = :Int
traitstype = :TraitsType
default = 4
)
julia> to_expr(traitor_arg)
:($(Expr(:kw, :((a::Int)::TraitsType), 4)))
julia> parse_expr(parser_traitor_arg, Expr(:kw, :(a::String::TraitsType), "hi"))
ERROR: ParseError: Expected type `Union{ExprParsers.NoDefault, Int64}`, got `hi` of type `String`.
All works well, and also our example restriction to only accept Int
default values, or no default at all, works as intended. As you see, the ExprParsers
package even comes with pretty printing of your custom @exprparser
type.
The ExprParsers
package, with parse_expr
, to_expr
and @exprparser
at its core, provides an interface which is easy to understand, easy to work with and easy to extend. Having defined the interface, many tedious and repetitive Expr
-parsing tasks are well encapsulated and it becomes much easier to construct further macro-semantics on top of it.
I hope you enjoy the package.