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
"""
@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
@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, _ ->
case input.chars do
[" "|_] -> parse(ignore_ws(parser, name1, skip_newlines), Input.drop(input), cache)
["\t"|_] -> parse(ignore_ws(parser, name1, skip_newlines), Input.drop(input), cache)
["\n"|_] when skip_newlines -> parse(ignore_ws(parser, name1, skip_newlines), Input.drop(input), cache)
_ -> parse(parser, input, cache)
end
end
)
end
@doc ~S"""
Unwrapps a wrapped parser and parses with it
"""
@spec lazy((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"""
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
@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
@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()), maybe(binary)) :: Parser.t()
def select(parsers, name \\ nil) do
name1 = name || "select"
new(
name1,
&_select(parsers, name, &1, &2, &3)
)
end
@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 _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
end
# SPDX-License-Identifier: Apache-2.0