Current section

Files

Jump to
minipeg lib minipeg combinators.ex
Raw

lib/minipeg/combinators.ex

defmodule Minipeg.Combinators do
use Minipeg.Types
alias Minipeg.{Cache, Failure, Input, Parser, Success}
alias Minipeg.Combinators.{Base, Common, Convenience, Mapping, Tools}
import Failure, only: [fail: 4]
import Parser, only: [new: 2, parse: 3]
import Success, only: [succeed: 5]
@moduledoc ~S"""
All public functions in this module take a parser and potentially more parameters and
return a new parser
"""
@type wrapped_parser_t :: (-> Parser.t)
@typep upto_behavior_t :: :keep | :include | :discard
@doc ~S"""
Just parses with `parser` but displays, input and result to stderr
ALternatively, if a function is provided the function is called and
nothing is printed to stderr
"""
@spec debug(Parser.t, binary?, maybe((Input.t, result_t()->any()))) :: Parser.t
defdelegate debug(parser, name \\ nil, fun \\ nil), to: Tools
@doc ~S"""
This parser, iff it succeeds returns a special `Ignore` result that is ignored inside a `sequence` combinator
"""
@spec ignore(Parser.t(), maybe(binary)) :: Parser.t()
defdelegate ignore(parser, name \\ nil), to: Tools
@doc ~S"""
Parsers a string that would be parsed by parser but ignoring leading whitespace, if you also want to
ignore whitespace after the parsed string use `tokenized`
"""
@spec ignore_ws(Parser.t, binary?(), boolean()) :: Parser.t
defdelegate ignore_ws(parser, name \\ nil, skip_newlines \\ false), to: Common
@spec in_between_parser(Parser.t, Parser.t, Parser.t, binary?()) :: Parser.t
def in_between_parser(ignored_start_parser, inside_parser, ignored_end_parser, name \\ nil) do
name1 = name ||
"in_between_parser(#{ignored_start_parser.name}, #{inside_parser.name}, #{ignored_end_parser.name})"
sequence([
ignored_start_parser, inside_parser, ignored_end_parser
], name1)
# |> set_error(
|> map(fn [_, ast, _] -> ast end, name1)
end
@doc ~S"""
Unwrapps a wrapped parser and parses with it. This is used for recursive rules, e.g.
```elixir
def my_parser...
sequence([...
lazy(fn -> my_parser() end)
```
"""
@spec lazy(wrapped_parser_t(), binary?()) :: Parser.t
defdelegate lazy(wrapped_parser, name \\ nil), to: Base
@doc ~S"""
Parses a list defined by an `element_parser` and a `seperator_parser`
"""
@spec list_parser(Parser.t, Parser.t, binary?(), non_neg_integer()) :: Parser.t
def list_parser(element_parser, seperator_parser, name \\ nil, min_count \\ 0) do
name1 = name || "list_parser(#{element_parser.name}, #{seperator_parser.name})"
maybe(
sequence([
element_parser,
many(
sequence([
seperator_parser,
element_parser
])
)
])
)
|> map(&_flatten_list/1)
|> satisfy(fn list ->
if Enum.count(list) >= min_count do
{:ok, list}
else
{:error, "Parser #{name1} only parsed #{Enum.count(list)} element(s) but #{min_count} were needed"}
end
end)
end
@doc ~S"""
Applies a parser as many times as possible, can use `min_count` (defaulting to 0) to
fail unless the parser could parse `min_count` times
"""
@spec many(Parser.t(), maybe(binary), non_neg_integer()) :: Parser.t()
defdelegate many(parser, name \\ nil, min_count \\ 0), to: Base
@doc ~S"""
`many_sel([...])` is just a convenience for `many(select([...]))`
"""
@spec many_sel(list(Parser.t), binary?(), non_neg_integer()) :: Parser.t
defdelegate many_sel(parsers, name \\ nil, min_count \\ 0), to: Convenience
@doc ~S"""
`many_seq([...])` is just a convenience for `many(sequence([...]))`
"""
@spec many_seq(list(Parser.t()), binary?(), non_neg_integer()) :: Parser.t()
defdelegate many_seq(parsers, name \\ nil, min_count \\ 0), to: Convenience
@doc ~S"""
Succeeds if `parser` succeeds, but maps the ast with `mapper_fun`.
If `parser` fails, it fails
"""
@spec map(Parser.t(), (ast_t() -> ast_t()), maybe(binary)) :: Parser.t()
defdelegate map(parser, mapper_fun, name \\ nil), to: Mapping
@doc ~S"""
If `parser` fails, it fails with the error mapped by `error_fun`
If `parser` succeeds, it just passes the result along
"""
@spec map_error(Parser.t, (Failure.t -> Failure.t), binary?()) :: Parser.t
defdelegate map_error(parser, error_fun, name \\ nil), to: Mapping
@doc ~S"""
Like `map`, but this also provides a second argument, the position in form of a tuple `{col, lnb}`
to the mapper function
"""
@spec mapp(Parser.t(), (ast_t(), position_t() -> ast_t()), maybe(binary)) :: Parser.t()
defdelegate mapp(parser, mapper_fun, name \\ nil), to: Mapping
@doc ~S"""
parses iff `parser` parses then maps the ast to a string with `IO.chardata_to_string/1`
The ast must therefore be of type `IO data`
"""
@spec map_to_string(Parser.t, binary?()) :: Parser.t
defdelegate map_to_string(parser, name \\ nil), to: Mapping
@spec maybe(Parser.t(), binary?) :: Parser.t()
defdelegate maybe(parser, name \\ nil), to: Base
@spec option(list(Parser.t()), binary?()) :: Parser.t
defdelegate option(parsers, name \\ nil), to: __MODULE__, as: :select
@doc ~S"""
Returns a parser that only succeeds if the original parser succeeds **and**
the satisfier function that is called with the ast of the original result
returns {:ok, value}. It also changes the original ast with value in the
final result.
"""
@spec satisfy(Parser.t(), satisfier_t(), binary?) :: Parser.t()
defdelegate satisfy(parser, satisfier, name \\ nil), to: Base
@doc ~S"""
Looks into the cache (for this parsing position) before parsing,
if no result found parses and puts the result into the cache (for this parsing position)
"""
@spec savepoint(Parser.t(), binary?, binary?) :: Parser.t()
def savepoint(parser, sp_name \\ nil, name \\ nil) do
sp_name1 = sp_name || parser.name
name1 = name || parser.name
new(
name1,
&_savepoint(parser, sp_name1, &1, &2, &3)
)
end
@doc ~S"""
succeeds with the first parser that succeeds in `parsers`, otherwise fails
"""
@spec select(list(Parser.t()), binary?()) :: Parser.t()
defdelegate select(parsers, name \\ nil), to: Base
@spec sequence(list(Parser.t()), binary?) :: Parser.t()
defdelegate sequence(parsers, name \\ nil), to: Base
@spec set_error(Parser.t, binary?(), binary?()) :: Parser.t
def set_error(parser, reason, parser_name \\ nil) do
map_error(parser, &Failure.reset_error(&1, reason, parser_name))
end
@spec tokenize(Parser.t) :: Parser.t()
def tokenize(parser) do
in_between_parser(Minipeg.Parsers.ws_parser(), parser, Minipeg.Parsers.ws_parser())
end
@doc ~S"""
Consume chars until `parser` parses and return them as parsed ast, fails if parser never succeeds
can add the delimiter to the ast or leave it on the input stream
"""
@spec upto_parser_parser(Parser.t, binary?(), upto_behavior_t()) :: Parser.t
def upto_parser_parser(parser, name \\ nil, parse_behavior \\ :keep) do
name1 = name || "upto_parser_parser(#{parser.name}, #{parse_behavior})"
new(
name1,
&_upto_parser_parser(&1, &2, &3, name1, parse_behavior, parser)
)
end
@doc ~S"""
A shortcut for `mapp(parser, &{&1, &2}...`
"""
@spec with_pos(Parser.t, binary?()) :: Parser.t
defdelegate with_pos(parser, name \\ nil), to: Mapping
@spec _flatten_list(maybe(list(ast_t()))) :: list(ast_t())
defp _flatten_list(list_ast)
# The list_ast is of form [fst_element, [sep, snd_element], ... ] or nil
defp _flatten_list(nil), do: []
defp _flatten_list([fst_ele, rest_list]) do
[
fst_ele | rest_list |> Enum.map(&Enum.at(&1, 1))
]
end
@spec _savepoint(Parser.t(), binary(), Input.t(), Cache.t(), binary()) :: result_t
defp _savepoint(parser, sp_name, input, cache, _name) do
case Cache.lookup(cache, input, sp_name) do
nil ->
result = parse(parser, input, cache)
cache1 = Cache.update(cache, Input.position(input), sp_name, result)
%{result | cache: cache1}
result ->
result
end
end
@spec _upto_parser_parser(
Input.t(), Cache.t(), binary(), binary(), upto_behavior_t(), Parser.t(), ast_t()
) :: result_t()
defp _upto_parser_parser(input, cache, name, _name, parse_delim, parser, ast \\ [])
defp _upto_parser_parser(input, cache, name, _name, :discard, parser, ast) do
case parse(parser, input, cache) do
%Success{rest: new_input} -> succeed(ast|>Enum.reverse|>Enum.join, new_input, cache, Input.position(input), name)
_ -> _upto_parser_repeat(input, cache, name, parser, :discard, ast)
end
end
defp _upto_parser_parser(input, cache, name, _name, :include, parser, ast) do
case parse(parser, input, cache) do
%Success{ast: delim_ast, rest: new_input} -> succeed({ast|>Enum.reverse|>Enum.join, delim_ast}, new_input, cache, Input.position(input), name)
_ -> _upto_parser_repeat(input, cache, name, parser, :include, ast)
end
end
defp _upto_parser_parser(input, cache, name, _name, :keep, parser, ast) do
case parse(parser, input, cache) do
%Success{} -> succeed(ast|>Enum.reverse|>Enum.join, input, cache, Input.position(input), name)
_ -> _upto_parser_repeat(input, cache, name, parser, :keep, ast)
end
end
@spec _upto_parser_repeat(Input.t(), Cache.t(), binary(), Parser.t(), upto_behavior_t(), ast_t()) :: result_t()
defp _upto_parser_repeat(input, cache, name, parser, type, ast) do
{next, rest} = Input.take(input)
case next do
"" -> fail("encountered end of input", input, cache, name)
h -> _upto_parser_parser(rest, cache, name, name, type, parser, [h|ast])
end
end
end
# SPDX-License-Identifier: Apache-2.0