sane-date-math: use Productions as objects

This commit is contained in:
colin 2022-12-24 01:17:19 +00:00
parent 7b01822ee7
commit 51a96525d9

View File

@ -183,12 +183,12 @@ class ProductionContext(ParserContext):
and then parse "all in one go", sealing away incomplete state, and converting and then parse "all in one go", sealing away incomplete state, and converting
the parsed tokens into actually useful abstractions (like signed numbers). the parsed tokens into actually useful abstractions (like signed numbers).
""" """
def __init__(self, production_cls: type, grammar: ParserContext): def __init__(self, production: 'Production', context: ParserContext = None):
self.production_cls = production_cls self.production = production
self.context = grammar self.context = context if context is not None else production.grammar()
def __repr__(self) -> str: def __repr__(self) -> str:
return f"ProductionContext({self.production_cls.__name__!r}, {self.context!r})" return f"ProductionContext({self.production!r}, {self.context!r})"
def __str__(self) -> str: def __str__(self) -> str:
return str(self.context) return str(self.context)
@ -209,23 +209,20 @@ class ProductionContext(ParserContext):
def reduce(self) -> object: def reduce(self) -> object:
# XXX this ends up being a leaf -> root reduction, # XXX this ends up being a leaf -> root reduction,
# which generally makes it harder to achieve detailed control when nesting. # which generally makes it harder to achieve detailed control when nesting.
return self.production_cls.reduce(self.reduce_inner(self.context)) return self.production.reduce(self.reduce_inner(self.context))
class Production: class Production:
""" """
non-generic, likely multi-token productions, non-generic, likely multi-token productions,
specified in terms of other Productions and the above primitives specified in terms of other Productions and the above primitives
""" """
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
raise NotImplementedError() raise NotImplementedError()
@classmethod def context(self) -> ParserContext:
def context(cls) -> ParserContext: return ProductionContext(self)
return ProductionContext(cls, cls.grammar())
@classmethod def reduce(self, inner: object) -> object:
def reduce(cls, inner: object) -> object:
""" """
use to construct the outer types out of already-converted inner types. use to construct the outer types out of already-converted inner types.
e.g. Number = Then([optional(Minus), Digits, optional(Suffix)]) e.g. Number = Then([optional(Minus), Digits, optional(Suffix)])
@ -236,25 +233,22 @@ class Production:
class DigitProduction(Production): class DigitProduction(Production):
""" one digit token """ """ one digit token """
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
return WantToken(DIGITS) return WantToken(DIGITS)
@staticmethod def reduce(self, inner: Token) -> int:
def reduce(inner: Token) -> int:
return int(inner.c) return int(inner.c)
class IntProduction(Production): class IntProduction(Production):
""" multi-digit integer """ """ multi-digit integer """
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
return Then([ return Then([
DigitProduction, DigitProduction(),
optional(IntProduction), optional(IntProduction()),
]) ])
@staticmethod def reduce(self, inner: list) -> int:
def reduce(inner: list) -> int: # TODO: wrong associativity
leading, trailing = inner leading, trailing = inner
if trailing is None: if trailing is None:
return leading return leading
@ -264,14 +258,13 @@ class IntProduction(Production):
class DurationOrIntProduction(Production): class DurationOrIntProduction(Production):
# due to a lack of lookahead, we combine duration and int parsing into one production # due to a lack of lookahead, we combine duration and int parsing into one production
# because a duration shares a complete int as prefix # because a duration shares a complete int as prefix
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
return Then([ return Then([
IntProduction, IntProduction(),
optional(WantToken(Token('d'))), optional(WantToken(Token('d'))),
]) ])
def reduce(inner: list) -> 'Literal': def reduce(self, inner: list) -> 'Literal':
value, suffix = inner value, suffix = inner
if suffix is None: if suffix is None:
return Literal(value) return Literal(value)
@ -279,46 +272,41 @@ class DurationOrIntProduction(Production):
return Literal(timedelta(value)) return Literal(timedelta(value))
class Whitespace(Production): class Whitespace(Production):
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
return Then([ return Then([
WantToken(SPACE), WantToken(SPACE),
optional(Whitespace), optional(Whitespace()),
]) ])
class ParenthesizedExpr(Production): class ParenthesizedExpr(Production):
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
return Then([ return Then([
WantToken(OPEN_PAREN), WantToken(OPEN_PAREN),
Expr, Expr(),
WantToken(CLOSE_PAREN), WantToken(CLOSE_PAREN),
]) ])
def reduce(inner: list) -> object: def reduce(self, inner: list) -> 'AstItem':
open, expr, close = inner open, expr, close = inner
return expr return expr
class IdentifierTail(Production): class IdentifierTail(Production):
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
return Then([ return Then([
WantToken(ALPHA_NUM_UNDER), WantToken(ALPHA_NUM_UNDER),
optional(IdentifierTail), optional(IdentifierTail()),
]) ])
class Identifier(Production): class Identifier(Production):
""" variable-style identifier, e.g. 'TODAY' """ """ variable-style identifier, e.g. 'TODAY' """
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
return Then([ return Then([
WantToken(ALPHA_UNDER), WantToken(ALPHA_UNDER),
optional(IdentifierTail), optional(IdentifierTail()),
]) ])
@staticmethod def reduce(self, inner: list) -> 'Literal':
def reduce(inner: list) -> 'Literal':
# fold the tokens into a string # fold the tokens into a string
first, rest = inner first, rest = inner
head = first.c head = first.c
@ -329,46 +317,41 @@ class Identifier(Production):
class UnaryExpr(Production): class UnaryExpr(Production):
""" some expression which does not invoke any operators at the outermost level """ """ some expression which does not invoke any operators at the outermost level """
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
return Then([ return Then([
optional(Whitespace), optional(Whitespace()),
Choice([ Choice([
DurationOrIntProduction, DurationOrIntProduction(),
Identifier, Identifier(),
ParenthesizedExpr, ParenthesizedExpr(),
]), ]),
optional(Whitespace), optional(Whitespace()),
]) ])
@staticmethod def reduce(self, inner: list):
def reduce(inner: list):
# drop the whitespace # drop the whitespace
leading, primary, trailing = inner leading, primary, trailing = inner
return primary return primary
class ExprRHS(Production): class ExprRHS(Production):
""" right hand side of a binary operation """ """ right hand side of a binary operation """
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
return Then([ return Then([
Choice([WantToken(ASTERISK), WantToken(PLUS), WantToken(MINUS)]), Choice([WantToken(ASTERISK), WantToken(PLUS), WantToken(MINUS)]),
# remaining, is just another `Expr`, but we need to keep the fields expanded here to control precedence. # remaining, is just another `Expr`, but we need to keep the fields expanded here to control precedence.
UnaryExpr, UnaryExpr(),
Choice([ExprRHS, Empty()]), Choice([ExprRHS(), Empty()]),
]) ])
class Expr(Production): class Expr(Production):
""" this is the top-level production """ """ this is the top-level production """
@staticmethod def grammar(self) -> ParserContext:
def grammar() -> ParserContext:
return Then([ return Then([
UnaryExpr, UnaryExpr(),
Choice([ExprRHS, Empty()]) Choice([ExprRHS(), Empty()])
]) ])
@staticmethod def reduce(self, inner: list):
def reduce(inner: list):
lhs, rhs = inner lhs, rhs = inner
if rhs is None: return lhs if rhs is None: return lhs
@ -378,11 +361,11 @@ class Expr(Production):
# multiplication has high precedence and we grab the adjacent token ASAP # multiplication has high precedence and we grab the adjacent token ASAP
lhs = MulOp(lhs, rhs) lhs = MulOp(lhs, rhs)
if rhs_next is not None: if rhs_next is not None:
lhs = Expr.reduce([lhs, rhs_next]) lhs = self.reduce([lhs, rhs_next])
else: else:
# reduce the rhs and *then* apply this operator # reduce the rhs and *then* apply this operator
if rhs_next is not None: if rhs_next is not None:
rhs = Expr.reduce([rhs, rhs_next]) rhs = self.reduce([rhs, rhs_next])
if oper == PLUS: if oper == PLUS:
lhs = AddOp(lhs, rhs) lhs = AddOp(lhs, rhs)
@ -449,7 +432,7 @@ class MulOp(BinaryOp):
def tokenize(stream: str) -> list: def tokenize(stream: str) -> list:
return [Token(char) for char in stream] return [Token(char) for char in stream]
def parse(ty: type, tokens: list) -> AstItem: def parse(ty: Production, tokens: list) -> AstItem:
ctx = Then([ty, Empty()]) ctx = Then([ty, Empty()])
for i, t in enumerate(tokens): for i, t in enumerate(tokens):
result = ctx.feed(t) result = ctx.feed(t)
@ -466,7 +449,7 @@ def parse(ty: type, tokens: list) -> AstItem:
def evaluate(expr: str) -> object: def evaluate(expr: str) -> object:
tok = tokenize(expr) tok = tokenize(expr)
expr = parse(Expr, tok) expr = parse(Expr(), tok)
print(expr) print(expr)
env = dict( env = dict(
today=datetime.now() today=datetime.now()