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