josevalim
Yes, yet another parser combinator library!
Most of the parser combinators in the ecosystem are either compile-time, often using AST traversal and macros, which hurts composition, or are runtime based, which means it is slow when parsing. But more importantly, I haven’t found no library compiles parser combinator down to binary matches that rely on the VM optimizations.
So over the last 48h I built a yet another parsec combinator library for Elixir called NimbleParsec. The combinator composition happens fully at runtime which is then compiled down to binary matching. It works similar to quoted expressions in Elixir. The combinators build an AST which is then compiled down to binary match clauses. See the link above for an example and the code it compiles down to.
I have ran @OvermindDL1 benchmark scripts and I got these results:
$ mix bench
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Elixir 1.7.0-dev
Benchmark suite executing with the following configuration:
warmup: 2.0s
time: 3.0s
parallel: 1
inputs: parse_datetime, parse_int_10
Estimated total run time: 30.0s
Benchmarking with input parse_datetime:
Benchmarking combine...
Benchmarking ex_spirit...
Benchmarking nimble...
Warning: The function you are trying to benchmark is super fast, making measures more unreliable! See: https://github.com/PragTob/benchee/wiki/Benchee-Warnings#fast-execution-warning
Benchmarking with input parse_int_10:
Benchmarking combine...
Benchmarking ex_spirit...
Benchmarking nimble...
Warning: The function you are trying to benchmark is super fast, making measures more unreliable! See: https://github.com/PragTob/benchee/wiki/Benchee-Warnings#fast-execution-warning
##### With input parse_datetime #####
Name ips average deviation median
nimble 1425.75 K 0.70 μs ±437.03% 0.70 μs
ex_spirit 177.70 K 5.63 μs ±115.86% 5.00 μs
combine 95.83 K 10.44 μs ±93.60% 9.00 μs
Comparison:
nimble 1425.75 K
ex_spirit 177.70 K - 8.02x slower
combine 95.83 K - 14.88x slower
##### With input parse_int_10 #####
Name ips average deviation median
nimble 949.95 K 1.05 μs ±216.77% 1.00 μs
ex_spirit 463.71 K 2.16 μs ±950.25% 2.00 μs
combine 338.62 K 2.95 μs ±760.53% 2.00 μs
Comparison:
nimble 949.95 K
ex_spirit 463.71 K - 2.05x slower
combine 338.62 K - 2.81x slower
The above shows that for parsing datetimes, nimble is 8x faster than ex_spirit and 14x faster than combine. For the integer case, nimble is only twice faster, but it is worth noting Nimble’s integer parser is written on top of existing combinators while the integer parser for both ex_spirit and combine are written by hand. So nimble is beating hand-written code there.
I did not measure memory usage but that should also decrease wth nimble thanks to binary matching.
I have also benchmarked compilation times by compiling the same datetime parser 30 times. combine takes 1s, which makes sense as it is runtime based. nimble takes 2s and ex_spirit takes 6s.
While nimble is extremely new, I think most of the primitives are there, so you should be able to build almost anything. Improvements, PRs and feedback are very welcome, thanks!
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #security











Showing Posts 21 to 30- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
josevalim
I have added support for state. Here is how to implement a simple XML parser with it:
https://github.com/plataformatec/nimble_parsec/blob/master/examples/simple_xml.exs
We can support two new primitives here. One is
context_choicewhich allows you to choose which branch to process based on what is under a given key in the context.The other one is to allow context keys to be given to
times. So you can say “this integer min/max/length will be given by certain key in context”.Both should be straight-forward to add and are generalizations of primitives we currently have in place. I don’t plan to work on them now but if someone is working on a parser that needs it, please open up an issue with the use case, code examples and rationale and we can find out the best way to handle it.
Eiji
@josevalim: How about add optional
opts?I prefer to write:
rule(term, ignore: true, optional: true)instead ofoptional(ignore(rule(term))).For real world example simply compare my version with its original form from README.md:
I know that’s extra work for you, but at least for me code will look much cleaner.
mischov
Where has this been my whole life?
OvermindDL1
Yep, that’s the traditional way in the original Spirit. Though it’s syntax makes it shorter than the same in Elixir.
And the ability to pass state as arguments to the parsers… ^.^;
A few issues:
First, the length, all of this in Nimble:
Is this in ExSpirit:
And this in Nimble:
Is this in ExSpirit:
And this in Nimble:
Is just this in ExSpirit:
In addition, though this is an example, it was demonstrating features that, though easy to bypass in this example (you can do it in ExSpirit the same way you did in Nimble), is not bypassable in many other cases. Specifically this line in ExSpirit:
How would this be done in Nimble, your example does not demonstrate this thus your example is not a faithful recreation. This specific expression, though in this case can be done by comparing ‘later’, this shows that you can parameterize parsers based on prior parsed data, and this is what I’ve set to see how you could do it in Nimble as this is an absolutely mandatory capability that a lot of languages need to parse else the parser can become infinitely large in order to handle all possible variants.
Like to make it slightly more complex (though still not infinitely configurable as some languages get), how would you parse the xml example of
<foo><bar>one<baz>two</foo>as if it were<foo><bar>one<baz>two</baz></bar></foo>? In ExSpirit that would involve changing this:To be something like (although format it better of course):
And then you get nice auto-closing tags when left open, and yet it still fails if there is a mismatched closing tag.
Also, these two cases in your parse call:
How would you enforce those as part of the grammar? In ExSpirit for the first you just add an
eoi()(and an alternatefail("reason")if you want a customized message) at the end, and for the second, well, that’s already an error in ExSpirit, like why does that parse at all in Nimble as it really absolutely should not (not as big of an issue for something trivial like SimpleXML but a larger issue when you have complex grammars that need to be self-terminating based on state information).For comparison, here’s how you make SimpleAST in the original Spirit:
And yes, that is entire pure and valid C++, no preprocessor or anything of the sort needed, consequently @tmbb you see how the local state it uses has to be passed up and down through the parsers? And yes, it will generate code that is at least on par but usually better than hand-rolling your own parser in any language, even assembly, without substantial work (and even with that work you will only get on par with Spirit in speed, literally if you find any time where spirit is slower than anything else it can optimize for that and fix it, that is part of it’s design, which I did not follow in ExSpirit for a quick whipping up, though the expression version would be better at it).
That is not as useful though, need to pass state into the parser arguments to be able to parse many language grammars, else you end up with infinite parser variations required (or falling back to multiple passes and more code and mis-matched errors with the parsers and all that).
It would not just be
times, you’d have to allow passing state as an argument to an any parser to really allow it in full (which the original spirit allows for and ExSpirit allows for in ‘most’ cases, though not all…)Eh, it might be nicer but generalizing that to everything everyone might possibly make sounds a bit irritating since in his design they are all just functions and not a built expression tree like the C++ spirit is).
Lol! Just module attributes so you can do whatever. ^.^
OvermindDL1
Also another thing, how do you define a skip parser in Nimble? Or do you have to add the skip parser to every-single-possible-rule-and-skippable-location-which-is-utterly-ginormous-in-count?
josevalim
Not planned. You can easily wrap it though!
As the name of the library says, my plan is to stay nimble and not focus in providing high level constructs.
The fact it allows runtime composition and the focus on primitives mean you should be able to build
charsandtextyourself, like I did. Put those in a module as regular functions and you will be able to use them as if they were part of nimble.To clarify, I was not trying to recreate ex_spirit parser here. I haven’t checked its implementation. In any case, what you proposed can be implemented with
traverse, which receives the context and can return the accumulator and a possibly updated context.It can be done by using
traverseas well but it should be easy to add a lookahead construct too.I don’t think this will ever be possible. All combinators are compiled down to function clauses. A combinator cannot be used with state information unless the underlying combinator has been taught to read from state. Which goes back to the point I want to rather focus on the small set of primitives, making them powerful enough to build everything else on top of it.
I mentioned this in the issues tracker but getting a traditional parser combinator and translating it to
NimbleParsecis not going to work. I am pretty sure the C++ implementation is faster and more flexible thanNimbleParsecbut for nimble we need to stay within the rules of the Erlang VM. That’s why I am more interested in particular examples we cannot handle than a particular feature, because when all is said and done, the feature may need to be implemented in completely different way in nimble.josevalim
v0.2.0 is out with lookahead, user contexts, custom errors, byte offsets and
ascii_string/3andutf8_string/3. See the CHANGELOG:https://github.com/plataformatec/nimble_parsec/blob/master/CHANGELOG.md
tmbb
Cool. I’ll start rewriting my Elixir lexer directly in
nimble_parsec. Then, when I see anything worth refactoring into its own module, I’ll start porting Makeup to see if it can be based onnimble_parsec(Makeup is basically a library of useful combinators that can be reused between languages + some plumbing so that the entire chain from lexer to formatter works.This might take some time, especially because there isn’t a 1-to-1 mapping between ExSpirit combinators and NimbleParsec combinators.
josevalim
I will be very glad to help if you have any questions or you need help translating some constructs!
josevalim
Also, maybe a good suggestion is to start with the smaller language you currently handle. So we can directly compare compile and execution times.