View Source Grammar to parse a comma separated list of numbers
Mix.install([:grammar])
Local setup
To use this livebook within a local clone the project, use the following setup, and launch the livebook from it's directory.
Mix.install(
[
{:grammar, path: Path.join(__DIR__, ".."), env: :dev}
],
lockfile: :grammar
)
Grammar with a dedicated callback module
defmodule CB do
def list(["[", nil, "]"]), do: []
def list(["[", elements, "]"]), do: elements
def elements?([element, nil]), do: [element]
def elements?([element, tail]), do: [element | tail]
def elements_tail?([",", element, nil]), do: [element]
def elements_tail?([",", element, tail]), do: [element | tail]
def element([number]), do: number
def number([number_str]), do: String.to_integer(number_str)
end
grammar =
Grammar.new()
|> Grammar.add_clause(:list, ["[", :elements?, "]"], &CB.list/1)
|> Grammar.add_clause(:elements?, [:element, :elements_tail?], &CB.elements?/1, true)
|> Grammar.add_clause(:elements_tail?, [",", :element, :elements_tail?], &CB.elements_tail?/1, true)
|> Grammar.add_clause(:element, [:number], &CB.element/1)
|> Grammar.add_clause(:number, [~r/[0-9]+/], &CB.number/1)
|> Grammar.prepare!()
|> Grammar.start(:list)
Parse some valid inputs
# Empty list
Grammar.loop(grammar, Grammar.Tokenizer.new("[]"))
# Single item list
Grammar.loop(grammar, Grammar.Tokenizer.new("[12]"))
# Regular list
Grammar.loop(grammar, Grammar.Tokenizer.new("[42, 53, 453]"))
Try parse invalid inputs
Grammar.loop(grammar, Grammar.Tokenizer.new("ident"))
Grammar.loop(grammar, Grammar.Tokenizer.new("[12,"))
Grammar.loop(grammar, Grammar.Tokenizer.new("[12,]"))
Changing the start rule
Changing the starting rule allows for testing a sub-part of a grammar.
Change grammar
to start at rule :element
Only a number can be extracted from the input
grammar = Grammar.start(grammar, :element)
Grammar.loop(grammar, Grammar.Tokenizer.new("12"))
Trying to reading anything else will fail
Grammar.loop(grammar, Grammar.Tokenizer.new("[12]"))
Change grammar
to start at rule :elements_tail?
The parser can now extract "nothing" (as :elements_tail?
is an epsilon
rule), or a a list of , <number>
.
grammar = Grammar.start(grammar, :elements_tail?)
Grammar.loop(grammar, Grammar.Tokenizer.new(""))
Grammar.loop(grammar, Grammar.Tokenizer.new(", 122, 134"))
Again, trying to reading anything else will fail
Grammar.loop(grammar, Grammar.Tokenizer.new("122, 134"))