from fractions import Fraction
import importlib.resources as resources
import re
from typing import cast
from lark import v_args, Lark, 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):
return am.Symbol(str(identifier)).located_on(
lines=(identifier.line, identifier.end_line),
columns=(identifier.column, identifier.end_column),
)
[docs] def atom(self, colon, identifier):
return am.Atom(str(identifier)).located_on(
lines=(colon.line, identifier.end_line),
columns=(colon.column, identifier.end_column),
)
[docs] def integral(self, number):
return am.Numeric(int(number)).located_on(
lines=(number.line, number.end_line),
columns=(number.column, number.end_column),
)
[docs] def floating(self, number):
return am.Numeric(float(number)).located_on(
lines=(number.line, number.end_line),
columns=(number.column, number.end_column),
)
[docs] def fraction(self, number):
return am.Numeric(Fraction(number)).located_on(
lines=(number.line, number.end_line),
columns=(number.column, number.end_column),
)
[docs] def string(self, *values):
l_quote, text, r_quote = values
value = "".join(values)
value = re.sub(r"(?<!\\)\\([^\"\\])", r"\g<1>", value)
return am.String(value.strip("\"")).located_on(
lines=(l_quote.line, r_quote.line),
columns=(l_quote.column, r_quote.column),
)
[docs] def s_expression(self, *values):
l_paren, *expressions, r_paren = values
return am.SExpression(*expressions).located_on(
lines=(l_paren.line, r_paren.end_line),
columns=(l_paren.column, r_paren.end_column),
)
[docs] def vector(self, *values):
l_bracket, *expressions, r_bracket = values
return am.Vector(*expressions).located_on(
lines=(l_bracket.line, r_bracket.end_line),
columns=(l_bracket.column, r_bracket.end_column),
)
[docs] def quoted(self, quote, expression):
return am.Quoted(expression).located_on(
lines=(quote.line, expression.end_line),
columns=(quote.column, expression.end_column),
)
[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
text (:class:`str`): The original text being parsed
source (:class:`str`): The source of the original text
"""
def __init__(self, line: int, column: int, text: str, source: str):
self.line = line
self.column = column
self.text = text
self.source = source
def __str__(self): # pragma: no cover
return f"in {self.source}, 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]def parse(text: str, source: str = "<unknown>") -> 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, text, source) from None