Files
Sheerka-Old/src/parsers/ExpressionParser.py
T

368 lines
9.9 KiB
Python

from dataclasses import dataclass
from typing import List, Tuple, Callable
from core.builtin_concepts import BuiltinConcepts
from core.concept import Concept
from core.sheerka.services.SheerkaExecute import ParserInput
from core.tokenizer import LexerError, TokenKind, Token
from parsers.BaseParser import Node, BaseParser, UnexpectedTokenErrorNode, UnexpectedEofNode, ErrorNode
class ExprNode(Node):
"""
Base ExprNode
eval() must be overridden
"""
def eval(self, obj):
return True
@dataclass()
class LeftPartNotFoundError(ErrorNode):
"""
When the expression starts with 'or' or 'and'
"""
pass
class NameExprNode(ExprNode):
def __init__(self, tokens):
self.tokens = tokens
self.value = "".join([t.str_value for t in self.tokens])
def eval(self, obj):
return self.value
def __repr__(self):
return f"NameExprNode('{self.value}')"
def __str__(self):
return self.value
@dataclass
class PropertyEqualsNode(ExprNode):
prop: str
value: object
def eval(self, obj):
if hasattr(obj, self.prop):
return str(getattr(obj, self.prop)) == self.value
return False
@dataclass()
class PropertyContainsNode(ExprNode):
prop: str
value: object
def eval(self, obj):
if hasattr(obj, self.prop):
return self.value in str(getattr(obj, self.prop))
return False
@dataclass
class PropertyEqualsSequenceNode(ExprNode):
"""
To use when the test must be done across parent and child
"""
props: List[str]
values: List[object]
def eval(self, obj):
index = len(self.props) - 1
while True:
if not hasattr(obj, self.props[index]) or getattr(obj, self.props[index]) != self.values[index]:
return False
if index == 0:
break
index -= 1
obj = obj.get_parent() if hasattr(obj, "get_parent") else obj.parent
if obj is None:
return False
return True
@dataclass()
class IsaNode(ExprNode):
"""
To use to replicate instanceof, sheerka.instanceof,
"""
obj_class: object
def eval(self, obj):
if isinstance(self.obj_class, type):
return isinstance(obj, self.obj_class)
if isinstance(self.obj_class, (BuiltinConcepts, str)):
return isinstance(obj, Concept) and str(self.obj_class) == obj.key
return False
@dataclass()
class LambdaNode(ExprNode):
"""
Generic expression to ease the tests
"""
lambda_exp: Callable[[object], bool]
def eval(self, obj):
try:
return self.lambda_exp(obj)
except Exception:
pass
@dataclass(init=False)
class AndNode(ExprNode):
parts: Tuple[ExprNode]
def __init__(self, *parts: ExprNode):
self.parts = parts
def eval(self, obj):
res = self.parts[0].eval(obj) and self.parts[1].eval(obj)
for part in self.parts[2:]:
res &= part.eval(obj)
return res
def __repr__(self):
return f"AndNode(" + ", ".join([repr(p) for p in self.parts]) + ")"
def __str__(self):
return " and ".join([str(p) for p in self.parts])
@dataclass(init=False)
class OrNode(ExprNode):
parts: Tuple[ExprNode]
def __init__(self, *parts: ExprNode):
self.parts = parts
def eval(self, obj):
res = self.parts[0].eval(obj) or self.parts[1].eval(obj)
for part in self.parts[2:]:
res |= part.eval(obj)
return res
def __repr__(self):
return f"OrNode(" + ", ".join([repr(p) for p in self.parts]) + ")"
def __str__(self):
return " or ".join([str(p) for p in self.parts])
@dataclass()
class NotNode(ExprNode):
node: ExprNode
def eval(self, obj):
return not self.node.eval(obj)
class FalseNode(ExprNode):
def eval(self, obj):
return False
class TrueNode(ExprNode):
def eval(self, obj):
return True
class ExpressionParser(BaseParser):
"""
will parser logic expression
like not (a and b or c)
The nodes can be used for custom filtering (ex with ExplanationConcept)
Or to help to understand why a python expression returns True or False
"""
def __init__(self, **kwargs):
super().__init__("Expression", 50, False, yield_eof=True)
def parse(self, context, parser_input: ParserInput):
"""
:param context:
:param parser_input:
:return:
"""
if not isinstance(parser_input, ParserInput):
return None
context.log(f"Parsing '{parser_input}' with ExpressionParser", self.name)
sheerka = context.sheerka
if parser_input.is_empty():
return context.sheerka.ret(self.name,
False,
sheerka.new(BuiltinConcepts.IS_EMPTY))
if not self.reset_parser(context, parser_input):
return self.sheerka.ret(
self.name,
False,
context.sheerka.new(BuiltinConcepts.ERROR, body=self.error_sink))
self.parser_input.next_token()
tree = self.parse_or()
token = self.parser_input.token
if token and token.type != TokenKind.EOF:
self.add_error(UnexpectedTokenErrorNode(f"Unexpected token '{token}'", token, []))
value = self.get_return_value_body(context.sheerka, self.parser_input.as_text(), tree, tree)
ret = self.sheerka.ret(
self.name,
not self.has_error,
value)
return ret
def parse_or(self):
expr = self.parse_and()
token = self.parser_input.token
if token.type != TokenKind.IDENTIFIER or token.value != "or":
return expr
parts = [expr]
while token.type == TokenKind.IDENTIFIER and token.value == "or":
self.parser_input.next_token()
expr = self.parse_and()
if expr is None:
self.add_error(UnexpectedEofNode("When parsing 'or'"))
return OrNode(*parts)
parts.append(expr)
token = self.parser_input.token
return OrNode(*parts)
def parse_and(self):
expr = self.parse_names()
token = self.parser_input.token
if token.type != TokenKind.IDENTIFIER or token.value != "and":
return expr
parts = [expr]
while token.type == TokenKind.IDENTIFIER and token.value == "and":
self.parser_input.next_token()
expr = self.parse_names()
if expr is None:
self.add_error(UnexpectedEofNode("When parsing 'and'"))
return AndNode(*parts)
parts.append(expr)
token = self.parser_input.token
return AndNode(*parts)
def parse_names(self):
def stop():
return token.type == TokenKind.EOF or \
paren_count == 0 and token.type == TokenKind.RPAR or \
token.type == TokenKind.IDENTIFIER and token.value in ("and", "or")
token = self.parser_input.token
if token.type == TokenKind.EOF:
return None
if token.type == TokenKind.LPAR:
self.parser_input.next_token()
expr = self.parse_or()
token = self.parser_input.token
if token.type != TokenKind.RPAR:
self.error_sink.append(UnexpectedTokenErrorNode(f"Unexpected token '{token}'", token, [TokenKind.RPAR]))
return expr
self.parser_input.next_token()
return expr
buffer = []
paren_count = 0
while not stop():
buffer.append(token)
if token.type == TokenKind.LPAR:
paren_count += 1
if token.type == TokenKind.RPAR:
paren_count -= 1
self.parser_input.next_token(False)
token = self.parser_input.token
if len(buffer) == 0:
if token.type != TokenKind.RPAR:
self.error_sink.append(LeftPartNotFoundError())
return None
if buffer[-1].type == TokenKind.WHITESPACE:
buffer.pop()
return NameExprNode(buffer)
class ExpressionVisitor:
"""
Pyhtonic implementation of visitors for ExprNode
"""
def visit(self, expr_node):
name = expr_node.__class__.__name__
method = 'visit_' + name
visitor = getattr(self, method, self.generic_visit)
return visitor(expr_node)
def generic_visit(self, expr_node):
"""Called if no explicit visitor function exists for a node."""
for field, value in expr_node.__dict__.items():
if isinstance(value, (list, tuple)):
for item in value:
if isinstance(item, ExprNode):
self.visit(item)
elif isinstance(value, ExprNode):
self.visit(value)
class TrueifyVisitor(ExpressionVisitor):
"""
Visit an ExprNode
replace all the nodes containing a variable to 'trueify' with True
The node containing both variables to trueify and to skip are skipped
"""
def __init__(self, to_trueify, to_skip):
self.to_trueify = to_trueify
self.to_skip = to_skip
def visit_AndNode(self, expr_node):
parts = []
for part in expr_node.parts:
parts.append(self.visit(part))
return AndNode(*parts)
def visit_OrNode(self, expr_node):
parts = []
for part in expr_node.parts:
parts.append(self.visit(part))
return OrNode(*parts)
def visit_NameExprNode(self, expr_node):
return_true = False
for t in expr_node.tokens:
if t.type == TokenKind.IDENTIFIER:
if t.value in self.to_skip:
return expr_node
if t.value in self.to_trueify:
return_true = True
return NameExprNode([Token(TokenKind.IDENTIFIER, "True", -1, -1, -1)]) if return_true else expr_node