Current section
Files
Jump to
Current section
Files
lib/minipeg/combinators.ex
defmodule Minipeg.Combinators do
use Minipeg.Types
alias Minipeg.{Cache, Failure, Input, Parser, Success}
import Failure, only: [fail: 3]
import Parser, only: [new: 2, parse: 3]
import Success, only: [succeed: 3]
# import Success, only: [succeed: 3]
@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
"""
@spec debug(Parser.t, binary?) :: Parser.t
def debug(parser, name \\ nil) do
new(name||parser.name,
fn input, cache, _ ->
result = parse(parser, input, cache)
IO.puts(:stderr, inspect(%{name: name || parser.name, input: input, cache: cache}))
IO.puts(:stderr, inspect(result))
result
end
)
end
@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
def ignore_ws(parser, name \\ nil, skip_newlines \\ false) do
name1 = name || "ignore_ws"
new(
name1,
fn input, cache, _ ->
{next, input1} = Input.take(input)
case next do
"" -> parse(parser, input, cache)
" " -> parse(ignore_ws(parser, name1, skip_newlines), input1, cache)
"\t" -> parse(ignore_ws(parser, name1, skip_newlines), input1, cache)
"\n" when skip_newlines -> parse(ignore_ws(parser, name1, skip_newlines), input1, cache)
_ -> parse(parser, input, cache)
end
end
)
end
@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)
|> map(fn [_, ast, _] -> ast end, name1)
end
@doc ~S"""
Unwrapps a wrapped parser and parses with it
"""
@spec lazy(wrapped_parser_t(), binary?()) :: Parser.t
def lazy(wrapped_parser, name \\ nil) do
name1 = name || "lazy"
new(
name1,
fn input, cache, _ ->
parse(wrapped_parser.(), input, cache)
end
)
end
@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()
def many(parser, name \\ nil, min_count \\ 0) do
name1 = name || parser.name
new(
name1,
&_many(parser, min_count, &1, &1, &2, &3)
)
end
@spec many_sel(list(Parser.t), binary?(), non_neg_integer()) :: Parser.t
def many_sel(parsers, name \\ nil, min_count \\ 0) do
name1 = name || ( "many_sel:[ #{parsers |> Enum.map(&(&1.name)) |> Enum.join("|")} ]" )
many(select(parsers), name1, min_count)
end
@spec many_seq(list(Parser.t()), binary?(), non_neg_integer()) :: Parser.t()
def many_seq(parsers, name \\ nil, min_count \\ 0) do
name1 = name || ( "many_seq:[ #{parsers |> Enum.map(&(&1.name)) |> Enum.join("...")} ]" )
many(sequence(parsers), name1, min_count)
end
@doc ~S"""
Succeeds if `parser` succeeds, but maps the ast with `mapper_fun`.
If `parser` fails, it just fails
"""
@spec map(Parser.t(), (ast_t() -> ast_t()), maybe(binary)) :: Parser.t()
def map(parser, mapper_fun, name \\ nil) do
name1 = name || parser.name
new(
name1,
fn input, cache, _ ->
case parse(parser, input, cache) do
%Failure{} = f -> f
%Success{ast: ast} = s -> %{s | ast: mapper_fun.(ast)}
end
end
)
end
@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
def map_to_string(parser, name \\ nil) do
name1 = name || "map_to_string(#{parser.name})"
parser
|> map(&IO.chardata_to_string/1, name1)
end
@spec maybe(Parser.t(), binary?) :: Parser.t()
def maybe(parser, name \\ nil) do
name1 = name || "maybe(#{parser.name})"
new(
name1,
&_maybe(parser, &1, &2, &3)
)
end
@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()
def satisfy(parser, satisfier, name \\ nil) do
name1 = name || parser.name
new(
name1,
&_satisfy(parser, satisfier, &1, &2, &3)
)
end
@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
@spec select(list(Parser.t()), binary?()) :: Parser.t()
def select(parsers, name \\ nil) do
name1 = name || "select"
new(
name1,
&_select(parsers, name, &1, &2, &3)
)
end
@spec option(list(Parser.t()), binary?()) :: Parser.t
defdelegate option(parsers, name \\ nil), to: __MODULE__, as: :select
@spec sequence(list(Parser.t()), binary?) :: Parser.t()
def sequence(parsers, name \\ nil) do
name1 = name || "sequence"
new(
name1,
&_sequence(parsers, &1, &1, &2, &3)
)
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
@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 _many(Parser.t(), integer(), Input.t(), Input.t(), Cache.t(), binary(), list()) ::
result_t()
defp _many(parser, min_count, input, curr_input, cache, name, result \\ []) do
case parse(parser, curr_input, cache) do
%Success{} = s ->
_many(parser, min_count - 1, input, s.rest, s.cache, name, [s.ast | result])
%Failure{} = f ->
if min_count > 0 do
fail("Missing #{min_count} parses in many in #{name}", input, cache)
else
succeed(Enum.reverse(result), curr_input, f.cache)
end
end
end
@spec _maybe(Parser.t(), Input.t(), Cache.t(), binary()) :: result_t
defp _maybe(parser, input, cache, _name) do
case parse(parser, input, cache) do
%Success{} = s -> s
%Failure{} -> succeed(nil, input, cache)
end
end
@spec _satisfy(Parser.t(), satisfier_t(), Input.t(), Cache.t(), any()) :: result_t
defp _satisfy(parser, satisfier, input, cache, _name) do
case parse(parser, input, cache) do
%Failure{} = f ->
f
%Success{ast: ast} = s ->
case satisfier.(ast) do
{:ok, ast1} -> %{s | ast: ast1}
{:error, reason} -> fail(reason, input, cache)
end
end
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)
%{result | cache: Cache.update(cache, Input.position(input), sp_name, result)}
result ->
result
end
end
@spec _select(list(Parser.t()), binary(), Input.t(), Cache.t(), binary?) :: result_t
defp _select(parsers, name, input, cache, _name) do
case parsers do
[] ->
fail("no alternative could be parsed in #{name}", input, cache)
[parser | alt_parsers] ->
case parse(parser, input, cache) do
%Failure{cache: cache1} -> _select(alt_parsers, name, input, cache1, nil)
%Success{} = s -> s
end
end
end
@spec _sequence(list(Parser.t()), Input.t(), Input.t(), Cache.t(), binary(), list(ast_t())) ::
result_t
defp _sequence(parsers, input, current_input, cache, name, result \\ []) do
case parsers do
[] ->
succeed(Enum.reverse(result), current_input, cache)
[parser | other_parsers] ->
case parse(parser, current_input, cache) do
%Failure{reason: reason, cache: cache1} ->
fail("#{reason} in #{name}", input, cache1)
%Success{ast: ast, rest: rest, cache: cache2} ->
_sequence(other_parsers, input, rest, cache2, name, [ast | result])
end
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)
_ -> _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)
_ -> _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)
_ -> _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 in #{name}", input, cache)
h -> _upto_parser_parser(rest, cache, name, name, type, parser, [h|ast])
end
end
end
# SPDX-License-Identifier: Apache-2.0