368 lines
9.9 KiB
Python
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
|