from fractions import Fraction
import importlib.resources as resources
from io import StringIO
import re
from typing import cast, Optional
from lark import v_args, Lark, Token, Transformer, UnexpectedInput
import amalgam.amalgams as am
GRAMMAR = resources.read_text(__package__, "grammar.lark")
[docs]@v_args(inline=True)
class Expression(Transformer):
"""
Transforms expressions in text into their respective
:class:`.amalgams.Amalgam` representations.
"""
[docs] def symbol(self, identifier: Token) -> am.Symbol:
return am.Symbol(str(identifier))
[docs] def atom(self, identifier: Token) -> am.Atom:
return am.Atom(str(identifier))
[docs] def integral(self, number: Token) -> am.Numeric:
return am.Numeric(int(number))
[docs] def floating(self, number: Token) -> am.Numeric:
return am.Numeric(float(number))
[docs] def fraction(self, number: Token) -> am.Numeric:
return am.Numeric(Fraction(number))
[docs] def string(self, *values: Token) -> am.String:
value = "".join(values)
value = re.sub(r"(?<!\\)\\([^\"\\])", r"\g<1>", value)
return am.String(value.strip("\""))
[docs] def s_expression(self, *expressions: am.Amalgam) -> am.SExpression:
return am.SExpression(*expressions)
[docs] def vector(self, *expressions: am.Amalgam) -> am.Vector:
return am.Vector(*expressions)
[docs] def quoted(self, expression: am.Amalgam) -> am.Quoted:
return am.Quoted(expression)
[docs]class ParsingError(Exception):
"""
Base exception for errors during parsing.
Attributes:
line (:class:`int`): the line number nearest to the error
column (:class:`int`): the column number nearest to the error
"""
def __init__(self, line: int, column: int):
self.line = line
self.column = column
def __str__(self): # pragma: no cover
return f"near line {self.line}, column {self.column}"
[docs]class ExpectedEOF(ParsingError):
"""Raised when multiple expressions are found."""
[docs]class ExpectedExpression(ParsingError):
"""Raised when no expressions are found."""
[docs]class MissingClosing(ParsingError):
"""Raised on missing closing parentheses or brackets."""
[docs]class MissingOpening(ParsingError):
"""Raised on missing opening parentheses or brackets."""
ERROR_EXAMPLES = {
ExpectedEOF: ("foo bar",),
ExpectedExpression: ("",),
MissingClosing: (
"(", "[", "(foo", "[foo", "(foo bar", "[foo bar", "\"foo", "\"foo bar",
),
MissingOpening: (")", "]", "[foo bar)", "(foo bar]"),
}
EXPR_PARSER = Lark(GRAMMAR, parser="lalr", transformer=Expression())
[docs]class Parser:
"""
Class that serves as the frontend for parsing text.
Attributes:
parse_buffer (:class:`StringIO`): The text buffer used within
:meth:`.Parser.repl_parse`.
"""
def __init__(self) -> None:
self.parse_buffer = StringIO()
[docs] def repl_parse(self, text: str) -> Optional[am.Amalgam]:
"""
Facilitates multi-line parsing for the REPL.
Writes the given `text` string to the :attr:`parse_buffer` and
attempts to parse `text`.
If :class:`MissingClosing` is raised, returns `None` to allow
for parsing to continue.
If another subclass of :class:`ParsingError` is raised, clears
the :attr:`parse_buffer` and re-raises the exception.
Otherwise, if parsing succeeds, clears the :attr:`parse_buffer`
and returns the parsed expression.
"""
self.parse_buffer.write(text)
self.parse_buffer.seek(0)
text = self.parse_buffer.read()
try:
expr = self.parse(text)
except MissingClosing:
return None
except (UnexpectedInput, ParsingError):
self.parse_buffer = StringIO()
raise
else:
self.parse_buffer = StringIO()
return expr
[docs] def parse(self, text: str) -> am.Amalgam:
"""Facilitates regular parsing that can fail."""
try:
return cast(am.Amalgam, EXPR_PARSER.parse(text))
except UnexpectedInput as u:
exc_cls = u.match_examples(
EXPR_PARSER.parse, ERROR_EXAMPLES.items(),
)
if exc_cls is None:
raise
raise exc_cls(u.line, u.column) from None