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