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]
  )
  ```

  ## 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