ityonemo
Pegasus - PEG grammar nimbleparsec generator
Pegasus - Nimbleparsec parser generator
https://hexdocs.pm/pegasus/Pegasus.html
This library converts a PEG parser library to nimbleparsec parsers. You can “hook” extra functions on to the combinators generated by the PEG language.
The PEG language is here:
https://www.piumarta.com/software/peg/peg.1.html
Unlike yecc/leex, This PEG grammar is extremely easy to read and write, given ABNF descriptions given in most RFCs or other standards documents (e.g. ECMA). This also leverages the extremely effective compile-time nature of the NimbleParsec library.
Most Liked
tj0
Wow, I didn’t realize I needed this library. Much, much easier to use than regular expressions. PEG seems like it would be an excellent addition to the core library as an alternative to Regex.
The following code is me just messing around trying to get things to work, but I hope it helps someone.
defmodule PegasusExample do
import NimbleParsec
require Pegasus
@moduledoc """
Examples for PEG parsing.
PegasusExample.get_pairs("grass=4,horse=1, star=2")
From https://github.com/xored/peg/blob/master/docs/grammar-examples.md
PegasusExample.get_timestamp("2009-09-22T06:59:28")
PegasusExample.get_timestamp("2009-09-22 06:59:28")
PegasusExample.get_timestamp("Fri Jun 17 03:50:56 PDT 2011")
PegasusExample.get_timestamp("2010-10-26 10:00:53.360")
"""
@parser_options [
Pair: [tag: :pair],
Word: [tag: :word],
Number: [tag: :number],
Space: [ignore: true],
Separator: [ignore: true],
Equals: [ignore: true],
]
Pegasus.parser_from_string(
"""
List <- Pair (Space* Separator Space* Pair)*
Pair <- Word Equals Number
Word <- [A-Za-z0-9_]+
Number <- [0-9]+
Space <- ' ' / '\t' / EndOfLine
EndOfLine <- '\r\n' / '\n' / '\r'
EndOfFile <- !.
Separator <- ','
Equals <- '='
""",
@parser_options
)
defparsec :get_pairs, parsec(:List)
@parser_timestamp [
Hour: [tag: :hour],
Minute: [tag: :minute],
Second: [tag: :second],
Year: [tag: :year],
Month: [tag: :month],
Day: [tag: :day],
TZ: [tag: :tz],
Space: [ignore: true],
Separator: [ignore: true],
Equals: [ignore: true],
]
Pegasus.parser_from_string(
"""
Timestamp <- DateTime / FreeDateTime
# Times
Hour <- [0-1] [0-9] / '2' [0-4]
Minute <- [0-5] [0-9]
Second <- [0-5] [0-9] / '60'
Fraction <- ('.' / ',') [0-9]+
IsoTz <- 'Z' / ('+' / '-') Hour (':'? Minute)?
TzL <- [A-Z]
TzAbbr <- TzL TzL (TzL (TzL TzL?)?)?
TZ <- IsoTz / TzAbbr
HM <- Hour ':' Minute Fraction?
HMS <- Hour ':' Minute ':' Second Fraction?
Time <- ('T' ' '?)? (HMS / HM) (' '? TZ)?
# Dates
Year <- [0-9] [0-9] [0-9] [0-9]
Month <- '0' [1-9] / '1' [0-2]
Day <- '0' [1-9] / [1-2] [0-9] / '3' [0-1]
Date <- Year '-' Month ('-' Day)?
# Combined
DateTime <- Date ' '? Time
# Free style
MonthAbbr <- 'Jan' / 'Feb' / 'Mar' / 'Apr' / 'May' / 'Jun' / 'Jul' / 'Aug' / 'Sep' / 'Sept' / 'Oct' / 'Nov' / 'Dec'
WeekDayAbbr <- 'Mon' / 'Tu' / 'Tue' / 'Tues' / 'Wed' / 'Th' / 'Thu' / 'Thur' / 'Thurs' / 'Fri' / 'Sat' / 'Sun'
FreeDateTime <- WeekDayAbbr ' ' MonthAbbr ' ' Day ' ' Time ' ' Year
""",
@parser_timestamp)
defparsec :get_timestamp, parsec(:Timestamp)
end
iex> PegasusExample.get_pairs("grass=4,horse=1, star=2")
{:ok,
[
pair: [word: ~c"grass", number: ~c"4"],
pair: [word: ~c"horse", number: ~c"1"],
pair: [word: ~c"star", number: ~c"2"]
], "", %{}, {1, 0}, 23}
TwistingTwists
Wow. Combining PEG grammars with Nimbleparsec is :chef_kiss: ![]()
ityonemo
Most of these:
And this:
ityonemo
well, to be fair Regex is supported at a low level in the VM (effectively a nif).
And there is a PEG in the stdlib ![]()
tj0
Ha, I wonder what that makes the rest of us.
It seems even the creator of Python had the same commentary. PEP 617 – New PEG parser for CPython | peps.python.org .
Looks like python is changing their internal parser to PEG. From some cursory research, https://janet-lang.org/ has PEG by default instead of PCRE/Regex in the standard library.
After all this exploration, I’m surprised Pegasus isn’t more popular. I’m guessing it might be because people don’t quite understand how to use it despite its awesomeness (or they don’t need to write grammars).Or it might be because it’s not so easy to do a mental-model replacement of Regex. A ton of people use nimble_parsec though it looks like, so it’s probably just a ergonomics thing. Here are some of my first impressions here while I was trying to get it working.
From the docs:
- parser options [:collect, :token, :tag, :post_traverse, :ignore] work on elements of a PEG, but I didn’t figure that out until I read the code from the other repos.
- parser options [:start_position, :export, :parser, :alias] seem to operate on the data and I’m not sure what they are for.
I was just trying to figure out how to turn the functions in Pegasus into something I understood from other languages aka:
- Regex.capture
- Regex.match
So I had to go thru your other packages to understand that I actually wanted defparsec because I first tried using [parser: true, export: true].
Regarding captures, I think the issues I had are just documentation related. I ended up going thru your other codebases to figure out that I needed to use [:tag, :collect, :post_traverse]. There’s a little more boilerplate than regex, but I don’t think that can be gotten rid of. But perhaps the default should be [collect: true]? It was very confusing to see a series of characters.
Regarding match, I added a bit of extra boilerplate to get the equivalent functionality of Regex.match.
Definitely, would be nice to have both a match and capture setup that worked cleanly after putting a grammar string in.
@email_options [
Name: [tag: :name],
Domain: [tag: :domain],
At: [ignore: true],
TLD: [tag: :tld],
Dot: [ignore: true]
]
Pegasus.parser_from_string(
"""
Email <- Name At (Domain)+ TLD
Name <- ([a-zA-Z0-9_\.\\-]+)
Domain <- ([A-Za-z0-9\\-]+ Dot)+
TLD <- ([A-Za-z\.]) ([A-Za-z\.])+
Dot <- '.'
At <- '@'
""",
@email_options
)
defparsec :parse_email, parsec(:Email)
def peg_match_email(email) do
case parse_email(email) do
{:ok, result, "", _, _, _} -> :ok
_ -> :error
end
end
Anyway, thank you and great work. I’m halfway done writing a parser for semi-structured text, if it’s useful, I could take some notes on what was tough to figure out.







