lib/gcode/parser/engine.ex

defmodule Gcode.Parser.Engine do
  import NimbleParsec
  use Gcode.Result

  @moduledoc """
  A parser for G-code programs using Parsec.
  """

  defcombinatorp(
    :whitespace,
    [
      string(" "),
      string("\t")
    ]
    |> choice()
    |> repeat()
    |> ignore()
  )

  defcombinatorp(
    :whitespace?,
    optional(parsec(:whitespace))
  )

  defcombinatorp(
    :newline,
    [string("\r"), string("\n")] |> choice() |> times(min: 1) |> tag(:newline)
  )

  defcombinatorp(
    :eol,
    empty()
    |> parsec(:whitespace?)
    |> choice([parsec(:newline), eos()])
  )

  defcombinatorp(
    :tape,
    empty()
    |> ignore(string("%"))
    |> optional(
      parsec(:whitespace?)
      |> repeat(utf8_char([]))
    )
    |> tag(:tape)
  )

  defcombinatorp(
    :braces_comment,
    empty()
    |> ignore(
      string("(")
      |> parsec(:whitespace?)
    )
    |> repeat(
      lookahead_not(
        choice([
          string(")"),
          parsec(:whitespace)
          |> string(")")
        ])
      )
      |> utf8_char([])
    )
    |> tag(:comment)
    |> ignore(
      parsec(:whitespace?)
      |> string(")")
    )
  )

  defcombinatorp(
    :semi_comment,
    empty()
    |> ignore(
      string(";")
      |> parsec(:whitespace?)
    )
    |> repeat(
      lookahead_not(parsec(:eol))
      |> utf8_char([])
    )
    |> tag(:comment)
  )

  defcombinatorp(
    :comment,
    choice([
      parsec(:braces_comment),
      parsec(:semi_comment)
    ])
  )

  defcombinatorp(
    :integer,
    times(
      utf8_char([?0..?9]),
      min: 1
    )
    |> tag(:integer)
  )

  defcombinatorp(
    :float,
    times(utf8_char([?0..?9]), min: 1)
    |> utf8_char([?.])
    |> times(utf8_char([?0..?9]), min: 1)
    |> tag(:float)
  )

  defcombinatorp(
    :number,
    choice([
      parsec(:float),
      parsec(:integer)
    ])
  )

  defcombinator(
    :constant,
    choice([
      choice([
        string("true"),
        string("false")
      ])
      |> tag(:boolean),
      string("iterations")
      |> tag(:iterations),
      string("line")
      |> tag(:line),
      string("pi")
      |> tag(:pi),
      string("result")
      |> tag(:result)
    ])
    |> tag(:constant)
  )

  defcombinator(
    :prefix,
    choice([
      ignore(string("!") |> parsec(:whitespace?)) |> tag(parsec(:expression), :!),
      ignore(string("+") |> parsec(:whitespace?)) |> tag(parsec(:expression), :+),
      ignore(string("-") |> parsec(:whitespace?)) |> tag(parsec(:expression), :-),
      ignore(string("#") |> parsec(:whitespace?)) |> tag(parsec(:expression), :"#")
    ])
  )

  defcombinator(
    :expression,
    choice([
      parsec(:prefix),
      parsec(:number),
      parsec(:constant)
    ])
  )

  defcombinatorp(
    :bare_string,
    times(
      lookahead_not(
        parsec(:whitespace?)
        |> choice([
          parsec(:newline),
          parsec(:comment)
        ])
      )
      |> utf8_char([]),
      min: 1
    )
    |> tag(:string)
  )

  defcombinatorp(
    :quoted_string,
    ignore(string("\""))
    |> repeat(
      lookahead_not(
        choice([
          parsec(:newline),
          string("\"")
        ])
      )
      |> utf8_char([])
    )
    |> tag(:string)
    |> ignore(optional(string("\"")))
  )

  defcombinatorp(:string, choice([parsec(:quoted_string), parsec(:bare_string)]))

  defcombinatorp(
    :word,
    empty()
    |> utf8_char([?A..?Z])
    |> tag(:command)
    |> parsec(:whitespace?)
    |> tag(
      parsec(:expression),
      :address
    )
    |> tag(:word)
  )

  defcombinatorp(
    :block,
    empty()
    |> times(
      choice([
        parsec(:word),
        parsec(:string)
      ])
      |> parsec(:whitespace?),
      min: 1
    )
    |> optional(parsec(:comment))
    |> tag(:block)
  )

  defcombinatorp(
    :line,
    choice([
      parsec(:tape),
      parsec(:comment),
      parsec(:block)
    ])
  )

  defparsecp(:program, times(parsec(:line) |> parsec(:eol), min: 1))

  @spec parse(String.t()) :: Result.t(keyword)
  def parse(program) do
    case program(program) do
      {:ok, tokens, _, _, _, _} -> ok(tokens)
      {:error, a, b, c, d, e} -> error({a, b, c, d, e})
    end
  end
end