lib/pegasus.ex

defmodule Pegasus do
  @moduledoc """
  converts `peg` files into `NimbleParsec` parsers.

  For documentation on this peg format:  https://www.piumarta.com/software/peg/peg.1.html

  To use, drop this in your model:

  ```
  defmodule MyModule
    require Pegasus

    Pegasus.parser_from_string(\"""
    foo <- "foo" "bar"
    \""")
  end
  ```

  See `NimbleParsec` for the description of the output.

  ```
  MyModule.foo("foobar") # ==> {:ok, ["foo", "bar"], ...}
  ```

  > #### Capitalized Identifiers {: .warning}
  >
  > for capitalized identifiers, you will have to use `apply/3` to call the
  > function, or you may wrap it in another combinator like so:
  >
  > ```elixir
  > defmodule Capitalized do
  >   require Pegasus
  >   import NimbleParsec
  >
  >   Pegasus.parser_from_string("Foo <- 'foo'")
  >
  >   defparsec :parse, parsec(:Foo)
  > end
  > ```

  You may also load a parser from a file using `parser_from_file/2`.

  ## Parser Options

  Parser options are passed as a keyword list after the parser defintion
  string (or file).  The keys for the options are the names of the combinators,
  followed by a keyword list of supplied options, which are applied in the
  specified order:

  ### `:start_position`

  When true, drops a map `%{line: <line>, column: <column>, offset: <offset>}` into
  the arguments for this keyword at the front of its list.

  ### `:collect`

  You may collect the contents of a combinator using the `collect: true` option.
  If this combinator calls other combinators, they must leave only iodata (no
  tags, no tokens) in the arguments list.

  ### `:token`

  You may substitute the contents of any combinator with a token (usually an atom).
  The following conditions apply:

  - `token: false` - no token (default)
  - `token: true` - token is set to the atom name of the combinator
  - `token: <value>` - token is set to the value of setting

  ### `:tag`

  You may tag the contents of your combinator using the `:tag` option.  The
  following conditions apply:

  - `tag: false` - No tag (default)
  - `tag: true` - Use the combinator name as the tag.
  - `tag: <atom>` - Use the supplied atom as the tag.

  ### `:post_traverse`

  You may supply a post_traversal for any parser.  See `NimbleParsec` for how to
  implement post-traversal functions.  These are defined by passing a keyword list
  to the `parser_from_file/2` or `parser_from_string/2` function.

  ### `:ignore`

  If true, clears the arguments from the list.

  #### Example

  ```
  Pegasus.parser_from_string(\"""
    foo <- "foo" "bar"
    \""",
    foo: [post_traverse: {:some_function, []}]
  )

  defp foo(rest, ["bar", "foo"], context, {_line, _col}, _bytes) do
    {rest, [:parsed], context}
  end
  ```

  ### `:parser`

  You may sepecify to export a combinator as a parser by specifying `parser: true`.
  By default, only a combinator will be generated.  See `NimbleParsec.defparsec/3`
  to understand the difference.

  #### Example

  ```
  Pegasus.parser_from_string(\"""
    foo <- "foo" "bar"
    \""", foo: [parser: true]
  )
  ```

  ### `:export`

  You may sepecify to export a combinator as a public function by specifying `export: true`.
  By default, the combinators are private functions.

  #### Example

  ```
  Pegasus.parser_from_string(\"""
    foo <- "foo" "bar"
    \""", foo: [export: true]
  )
  ```

  ### `:alias`

  You may specify your own combinators to be run in place of what's in the grammar.
  This is useful if the grammar is wrong or contains content that can't be run for
  some reason.

  #### Example

  ```
  Pegasus.parser_from_string(\"""
    foo <- "foo"
  \""", foo: [alias: :my_combinator])
  ```

  ## Not implemented features

  Actions, which imply the use of C code, are not implemented.  These currently fail to parse
  but in the future they may silently do nothing.
  """

  import NimbleParsec

  defparsec(:parse, Pegasus.Grammar.parser())

  defmacro parser_from_string(string, opts \\ []) do
    quote bind_quoted: [string: string, opts: opts] do
      string
      |> Pegasus.parse()
      |> Pegasus.parser_from_ast(opts)
    end
  end

  defmacro parser_from_file(file, opts \\ []) do
    quote bind_quoted: [file: file, opts: opts] do
      file
      |> File.read!()
      |> Pegasus.parse()
      |> Pegasus.parser_from_ast(opts)
    end
  end

  defmacro parser_from_ast(ast, opts) do
    quote bind_quoted: [ast: ast, opts: opts] do
      require NimbleParsec
      require Pegasus.Ast

      for ast = %{name: name, parsec: parsec} <- Pegasus.Ast.to_nimble_parsec(ast, opts) do
        name_opts = Keyword.get(opts, name, [])
        exported = !!Keyword.get(name_opts, :export)
        parser = Keyword.get(name_opts, :parser, false)

        Pegasus.Ast.traversals(ast)

        case {exported, parser} do
          {false, false} ->
            NimbleParsec.defcombinatorp(name, parsec)

          {false, true} ->
            NimbleParsec.defparsecp(name, parsec)

          {false, parser_name} ->
            NimbleParsec.defparsecp(parser_name, parsec)

          {true, false} ->
            NimbleParsec.defcombinator(name, parsec)

          {true, true} ->
            NimbleParsec.defparsec(name, parsec)

          {true, parser_name} ->
            NimbleParsec.defparsec(parser_name, parsec)
        end
      end
    end
  end
end