defmodule Xpeg do
require Record
Record.defrecord(:state,
func: nil,
userdata: nil,
back_stack: [],
ret_stack: [],
captures: [],
cap_stack: [],
status: :running
)
@moduledoc """
XPeg is a pure Elixir pattern matching library. It provides macros to compile
patterns and grammars (PEGs) to Elixir function which will parse a string and
collect selected parts of the input. PEGs are not unlike regular expressions,
but offer more power and flexibility, and have less ambiguities. (More about
PEGs on [Wikipedia](https://en.wikipedia.org/wiki/Parsing_expression_grammar))
Some use cases where XPeg is useful are configuration or data file parsers,
robust protocol implementations, input validation, lexing of programming
languages or domain specific languages.
p = Xpeg.peg :dict do
:dict <- :pair * star("," * :pair) * !1
:pair <- :word * "=" * :number * fn [a,b|cs] -> [{b,a}|cs] end
:word <- cap(+{'a'..'z'})
:number <- cap(+{'0'..'9'}) * fn [v|cs] -> [String.to_integer(v) | cs] end
end
Xpeg.match(p, "grass=4,horse=1,star=2")
"""
defp collect_captures(stack, acc, caps) do
case {stack, acc} do
{[{:open, s, si} | stack], _} ->
collect_captures(stack, [{:open, s, si} | acc], caps)
{[{:close, _sc, sic} | stack], [{:open, so, sio} | acc]} ->
len = sic - sio
cap = to_string(Enum.take(so, len))
collect_captures(stack, acc, [cap | caps])
{_, acc} ->
{acc, caps}
end
end
def collect_captures(ctx) do
{cap_stack, captures} =
state(ctx, :cap_stack)
|> Enum.reverse()
|> collect_captures([], [])
state(ctx, cap_stack: cap_stack, captures: captures ++ state(ctx, :captures))
end
def trace(ip, cmd, s) do
ip = to_string(ip) |> String.pad_leading(4, " ")
s = inspect(s) |> String.slice(0, 20) |> String.pad_trailing(20, " ")
IO.puts(" #{ip} | #{s} | #{cmd} ")
end
defp make(start, rules, options) do
%{start: start, rules: rules}
|> Linker.link_grammar(options)
|> Codegen.emit(options)
end
defmacro peg(start, [{:do, v}]) do
make(start, Parser.parse(v), [])
end
defmacro peg(start, options, [{:do, v}]) do
make(start, Parser.parse(v), options)
end
defmacro patt(v) do
make(:anon, %{anon: Parser.parse(%{}, v) ++ [{:return}]}, [])
end
def match(func, s, data \\ :nodata) do
ctx = state(func: func, userdata: data)
{time, {ctx, match_len}} = :timer.tc(fn ->
func.(ctx, String.to_charlist(s), 0, 0)
end)
ctx = collect_captures(ctx)
%{
captures: state(ctx, :captures),
status: state(ctx, :status),
time: time / 1.0e6,
match_len: match_len,
userdata: state(ctx, :userdata),
}
end
end
# set ft=elixir