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"))