advent-2022-python/advent/parser/parser.py
2022-12-12 18:28:16 +01:00

442 lines
14 KiB
Python

from __future__ import annotations
from functools import reduce
from itertools import chain
from typing import Any, Callable, Generic, Iterator, Self, TypeVar, overload
import unicodedata
from advent.parser.parser_input import AllowedParserInput, ParserInput, create_parser_input
from .result import Result
T = TypeVar('T')
T1 = TypeVar('T1')
T2 = TypeVar('T2')
T3 = TypeVar('T3')
T4 = TypeVar('T4')
T5 = TypeVar('T5')
TR = TypeVar('TR')
ParserResult = Iterator[tuple[ParserInput, T]]
ParserFunc = Callable[[ParserInput], ParserResult[T]]
class P(Generic[T]):
def __init__(self, func: ParserFunc[T]):
self.func = func
def parse(self, input: AllowedParserInput) -> Result[T]:
parser_input = create_parser_input(input)
all_results = self.func(parser_input)
try:
_, result = next(all_results)
return Result.of(result)
except StopIteration:
return Result.fail("No result")
def parse_multi(self, input: AllowedParserInput) -> Iterator[T]:
parser_input = create_parser_input(input)
all_results = self.func(parser_input)
return (v for _, v in all_results)
@classmethod
def pure(cls, value: T) -> P[T]:
return P(lambda pp: iter([(pp, value)]))
@classmethod
def fail(cls) -> P[Any]:
return P(lambda _: iter([]))
@classmethod
def _fix(cls, p1: Callable[[P[Any]], P[T]]) -> P[T]:
""" Not really nice helper function, but it works"""
return [p._forward(q.func) for p in [P(None)] for q in [p1(p)]][0] # type: ignore
def _forward(self, func: ParserFunc[T]) -> Self:
self.func = func
return self
def bind(self, bind_func: Callable[[T], P[TR]]) -> P[TR]:
def inner(parserPos: ParserInput) -> ParserResult[TR]:
return (r for rs in (bind_func(v).func(pp)
for pp, v in self.func(parserPos)) for r in rs)
return P(inner)
def fmap(self, map_func: Callable[[T], TR]) -> P[TR]:
def inner(parserPos: ParserInput) -> ParserResult[TR]:
return ((pp, map_func(v)) for pp, v in self.func(parserPos))
return P(inner)
def safe_fmap(self, map_func: Callable[[T], TR]) -> P[TR]:
def inner(parserPos: ParserInput) -> ParserResult[TR]:
for pp, v in self.func(parserPos):
try:
yield pp, map_func(v)
except Exception:
pass
return P(inner)
def replace(self, value: TR) -> P[TR]:
return self.fmap(lambda _: value)
def ignore(self) -> P[tuple[()]]:
return self.fmap(lambda _: ())
def apply(self, p2: P[Callable[[T], TR]]) -> P[TR]:
return self.bind(lambda x: p2.bind(lambda y: P.pure(y(x))))
@classmethod
def first(cls, p1: P[T1], p2: P[Any]) -> P[T1]:
return p1.bind(lambda v1: p2.fmap(lambda _: v1))
@classmethod
def second(cls, p1: P[Any], p2: P[T2]) -> P[T2]:
return p1.bind(lambda _: p2)
def between(self, pre: P[Any], post: P[Any]) -> P[T]:
return P.map3(pre, self, post, lambda _1, v, _2: v)
def some(self) -> P[list[T]]:
return P._fix(lambda p: self.bind(
lambda x: P.either(p, P.pure([])).fmap(lambda ys: [x] + ys)))
def some_lazy(self) -> P[list[T]]:
return P._fix(lambda p: self.bind(
lambda x: P.either(P.pure([]), p).fmap(lambda ys: [x] + ys)))
def many(self) -> P[list[T]]:
return P.either(self.some(), P.pure([]))
def many_lazy(self) -> P[list[T]]:
return P.either(P.pure([]), self.some_lazy())
def satisfies(self, pred: Callable[[T], bool]) -> P[T]:
return self.bind(lambda v: P.pure(v) if pred(v) else P.fail())
def optional(self) -> P[T | None]:
return P.either(self, P.pure(None))
def optional_lazy(self) -> P[T | None]:
return P.either(P.pure(None), self)
@overload
def times(self, *, exact: int) -> P[list[T]]:
...
@overload
def times(self, *, min: int) -> P[list[T]]:
...
@overload
def times(self, *, max: int) -> P[list[T]]:
...
@overload
def times(self, *, min: int, max: int) -> P[list[T]]:
...
def times(self, *, max: int | None = None, min: int | None = None,
exact: int | None = None) -> P[list[T]]:
match (exact, min, max):
case (int(e), None, None):
return self.many().satisfies(lambda lst: len(lst) == e)
case (None, int(mn), None):
return self.many().satisfies(lambda lst: len(lst) >= mn)
case (None, None, int(mx)):
return self.many().satisfies(lambda lst: len(lst) <= mx)
case (None, int(mn), int(mx)):
return self.many().satisfies(lambda lst: mn <= len(lst) <= mx)
case _:
raise Exception("Illegal combination of parameters")
@overload
def times_lazy(self, *, exact: int) -> P[list[T]]:
...
@overload
def times_lazy(self, *, min: int) -> P[list[T]]:
...
@overload
def times_lazy(self, *, max: int) -> P[list[T]]:
...
@overload
def times_lazy(self, *, min: int, max: int) -> P[list[T]]:
...
def times_lazy(self, *, max: int | None = None, min: int | None = None,
exact: int | None = None) -> P[list[T]]:
match (exact, min, max):
case (int(e), None, None):
return self.many_lazy().satisfies(lambda lst: len(lst) == e)
case (None, int(mn), None):
return self.many_lazy().satisfies(lambda lst: len(lst) >= mn)
case (None, None, int(mx)):
return self.many_lazy().satisfies(lambda lst: len(lst) <= mx)
case (None, int(mn), int(mx)):
return self.many_lazy().satisfies(lambda lst: mn <= len(lst) <= mx)
case _:
raise Exception("Illegal combination of parameters")
def sep_by(self, sep: P[Any]) -> P[list[T]]:
return P.map2(self, P.second(sep, self).many(), lambda f, r: [f] + r)
def sep_by_lazy(self, sep: P[Any]) -> P[list[T]]:
return P.map2(self, P.second(sep, self).many_lazy(), lambda f, r: [f] + r)
def no_match(self) -> P[tuple[()]]:
def inner(parserPos: ParserInput) -> ParserResult[tuple[()]]:
result = self.func(parserPos)
try:
next(result)
# Silently yields nothing so is an empty Generator
except StopIteration:
yield (parserPos, ())
return P(inner)
@classmethod
def map2(cls, p1: P[T1], p2: P[T2], func: Callable[[T1, T2], TR]) -> P[TR]:
return p1.bind(lambda v1: p2.fmap(lambda v2: func(v1, v2)))
@classmethod
def map3(cls, p1: P[T1], p2: P[T2], p3: P[T3], func: Callable[[T1, T2, T3], TR]) -> P[TR]:
return p1.bind(
lambda v1: p2.bind(
lambda v2: p3.fmap(
lambda v3: func(v1, v2, v3))))
@classmethod
def map4(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
func: Callable[[T1, T2, T3, T4], TR]) -> P[TR]:
return p1.bind(
lambda v1: p2.bind(
lambda v2: p3.bind(
lambda v3: p4.fmap(
lambda v4: func(v1, v2, v3, v4)))))
@classmethod
def map5(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], p5: P[T5],
func: Callable[[T1, T2, T3, T4, T5], TR]) -> P[TR]:
return p1.bind(
lambda v1: p2.bind(
lambda v2: p3.bind(
lambda v3: p4.bind(
lambda v4: p5.fmap(
lambda v5: func(v1, v2, v3, v4, v5))))))
@classmethod
@overload
def seq(cls, p1: P[T1], p2: P[T2], /) -> P[tuple[T1, T2]]:
...
@classmethod
@overload
def seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[tuple[T1, T2, T3]]:
...
@classmethod
@overload
def seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[tuple[T1, T2, T3, T4]]:
...
@classmethod
@overload
def seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
p5: P[T5], /) -> P[tuple[T1, T2, T3, T4, T5]]:
...
@classmethod
def seq(cls, *ps: P[Any]) -> P[tuple[Any, ...]]:
return reduce(lambda p, x: x.bind(
lambda a: p.fmap(lambda b: chain([a], b))),
list(ps)[::-1], P.pure(iter([]))).fmap(tuple)
@classmethod
@overload
def sep_seq(cls, p1: P[T1], p2: P[T2], /, *, sep: P[Any]) -> P[tuple[T1, T2]]:
...
@classmethod
@overload
def sep_seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3]]:
...
@classmethod
@overload
def sep_seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /,
*, sep: P[Any]) -> P[tuple[T1, T2, T3, T4]]:
...
@classmethod
@overload
def sep_seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
p5: P[T5], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3, T4, T5]]:
...
@classmethod
def sep_seq(cls, *ps: P[Any], sep: P[Any]) -> P[tuple[Any, ...]]:
first, *rest = list(ps)
return P.map2(first,
reduce(lambda p, x: P.second(sep, x.bind(
lambda a: p.fmap(lambda b: chain([a], b)))),
rest[::-1], P.pure(iter([]))),
lambda f, r: (f,) + tuple(r))
@classmethod
def either(cls, p1: P[T1], p2: P[T2], /) -> P[T1 | T2]:
def inner(parserPos: ParserInput):
yield from p1.func(parserPos)
yield from p2.func(parserPos)
return P(inner)
@classmethod
@overload
def choice(cls, p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[T1 | T2 | T3]:
...
@classmethod
@overload
def choice(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[T1 | T2 | T3 | T4]:
...
@classmethod
@overload
def choice(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
p5: P[T5], /) -> P[T1 | T2 | T3 | T4 | T5]:
...
@classmethod
def choice(cls, *ps: P[Any]) -> P[Any]:
def inner(parserPos: ParserInput) -> Iterator[Any]:
for p in ps:
yield from p.func(parserPos)
return P(inner)
@classmethod
def choice2(cls, *ps: P[T]) -> P[T]:
return P.choice(*ps)
# Start of String functions
@classmethod
def any_char(cls) -> P[str]:
def inner(parserPos: ParserInput) -> ParserResult[str]:
if parserPos.has_data():
yield parserPos.step()
return P(inner)
@classmethod
def eof(cls) -> P[tuple[()]]:
return P.any_char().no_match()
@classmethod
def char_func(cls, cmp: Callable[[str], bool]) -> P[str]:
return P.any_char().satisfies(cmp)
@classmethod
def char(cls, cmp: str) -> P[str]:
return P.char_func(lambda c: c == cmp)
@classmethod
def string(cls, cmp: str) -> P[str]:
return P.seq(*map(P.char, cmp)).replace(cmp)
@classmethod
def tchar(cls, cmp: str) -> P[str]:
return P.char(cmp).trim()
@classmethod
def tstring(cls, cmp: str) -> P[str]:
return P.string(cmp).trim()
@classmethod
def one_of(cls, s: str) -> P[str]:
return P.char_func(lambda c: c in s)
@classmethod
def any_decimal(cls) -> P[str]:
return P.char_func(lambda c: c.isdecimal())
@classmethod
def is_decimal(cls, num: int) -> P[str]:
return P.any_decimal().satisfies(lambda c: unicodedata.decimal(c) == num)
@classmethod
def is_not_decimal(cls, num: int) -> P[str]:
return P.any_decimal().satisfies(lambda c: unicodedata.decimal(c) != num)
@classmethod
def lower(cls) -> P[str]:
return P.char_func(lambda c: c.islower())
@classmethod
def upper(cls) -> P[str]:
return P.char_func(lambda c: c.isupper())
@classmethod
def eol(cls) -> P[tuple[()]]:
return P.char('\n').ignore()
@classmethod
def space(cls) -> P[str]:
return P.char_func(lambda c: c.isspace())
def word(self) -> P[str]:
return P.first(self.many().fmap(lambda cs: ''.join(str(c) for c in cs)), self.no_match())
@classmethod
def unsigned(cls) -> P[int]:
return P.either(P.first(P.is_decimal(0), P.any_decimal().no_match()).replace(0),
P.map2(P.is_not_decimal(0), P.any_decimal().word(),
lambda f, s: int(f + s)))
@classmethod
def signed(cls) -> P[int]:
return P.map2(P.one_of('+-').optional(), P.unsigned(),
lambda sign, num: num if sign != '-' else -num)
@classmethod
def tunsigned(cls) -> P[int]:
return P.unsigned().trim()
@classmethod
def tsigned(cls) -> P[int]:
return P.signed().trim()
def line(self) -> P[T]:
return P.first(self, P.eol())
def tline(self) -> P[T]:
return P.first(self.trim(), P.eol())
def in_parens(self) -> P[T]:
return self.between(P.char('('), P.char(')'))
def in_angles(self) -> P[T]:
return self.between(P.char('<'), P.char('>'))
def in_brackets(self) -> P[T]:
return self.between(P.char('['), P.char(']'))
def in_curleys(self) -> P[T]:
return self.between(P.char('{'), P.char('}'))
def trim_left(self) -> P[T]:
return P.second(WHITE_SPACE, self)
def trim_right(self) -> P[T]:
return P.first(self, WHITE_SPACE)
def trim(self) -> P[T]:
return self.between(WHITE_SPACE, WHITE_SPACE)
WHITE_SPACE: P[tuple[()]] = P.space().many().ignore()
SEP_SPACE: P[tuple[()]] = P.space().some().ignore()