Fixed #131 : Implement ExprToConditions

Fixed #130 : ArithmeticOperatorParser
Fixed #129 : python_wrapper : create_namespace
Fixed #128 : ExpressionParser: Cannot parse func(x) infixed concept 'xxx'
This commit is contained in:
2021-10-13 16:06:57 +02:00
parent a61a1c0d2b
commit 89e1f20975
76 changed files with 5867 additions and 3206 deletions
@@ -0,0 +1,89 @@
import core.builtin_helpers
import core.utils
from core.builtin_concepts_ids import BuiltinConcepts, BuiltinErrors
from core.concept import Concept
from core.global_symbols import ErrorObj, NotInit
from core.sheerka.services.sheerka_service import BaseService
class ErrorItem:
def __init__(self, level: int, parent, error: object):
self.level = level
self.parent = parent
self.error = error
class SheerkaErrorManager(BaseService):
NAME = "Error"
def __init__(self, sheerka):
super().__init__(sheerka, order=1)
def initialize(self):
self.sheerka.bind_service_method(self.NAME, self.get_errors, False)
self.sheerka.bind_service_method(self.NAME, self.has_error, False)
self.sheerka.bind_service_method(self.NAME, self.get_error_cause, False)
def get_errors(self, context, obj, **kwargs):
"""
Browse obj, looking for error
:param context:
:param obj:
:param kwargs: if defined, specialize the error (example __type="PythonEvalError")
:return:
"""
def is_error(_obj):
if isinstance(_obj, (ErrorObj, Exception)):
return True
if isinstance(_obj, Concept) and _obj.get_metadata().is_builtin and _obj.key in BuiltinErrors:
return True
return False
def inner_get_errors(_obj, level, parent):
if self.sheerka.isinstance(_obj, BuiltinConcepts.RETURN_VALUE) and _obj.status:
return []
if isinstance(_obj, (list, set, tuple)):
return core.utils.flatten([inner_get_errors(o, level, parent) for o in _obj])
if is_error(_obj):
#error_item = ErrorItem(level, parent, _obj)
error_item = _obj
if isinstance(_obj, Concept) and _obj.body not in (NotInit, None):
return [error_item] + inner_get_errors(_obj.body, level + 1, error_item)
if isinstance(_obj, ErrorObj) and hasattr(_obj, "get_error"):
return [error_item] + inner_get_errors(_obj.get_error(), level + 1, error_item)
else:
return [error_item]
if self.sheerka.isinstance(_obj, BuiltinConcepts.FILTERED):
return inner_get_errors(_obj.reason, level, parent)
if isinstance(_obj, Concept) and _obj.body != NotInit:
return inner_get_errors(_obj.body, level, parent)
return []
errors = inner_get_errors(obj, 0, None)
#return self.sheerka.filter_objects(context, errors, mapping=lambda o: o.error, **kwargs)
return self.sheerka.filter_objects(context, errors, **kwargs)
def has_error(self, context, obj, **kwargs):
errors = self.get_errors(context, obj, **kwargs)
return len(errors) > 0
def get_error_cause(self, obj):
if self.sheerka.isinstance(obj, BuiltinConcepts.NOT_FOR_ME):
res = obj.reason
elif self.sheerka.isinstance(obj, BuiltinConcepts.ERROR):
res = obj.body
else:
res = None
if isinstance(res, list) and len(res) == 1:
return res[0]
else:
return res
@@ -1,22 +1,20 @@
from dataclasses import dataclass
from core.builtin_concepts import BuiltinConcepts
from core.builtin_helpers import ensure_bnf, ensure_concept, expect_one, is_only_successful, only_successful
from core.builtin_helpers import ensure_bnf, ensure_concept, expect_one, get_parsed_concept, is_only_successful, \
only_successful, variables_in_context
from core.concept import AllConceptParts, Concept, ConceptParts, DoNotResolve, InfiniteRecursionResolved, \
concept_part_value
from core.global_symbols import CURRENT_OBJ, INIT_AST_PARSERS, INIT_AST_QUESTION_PARSERS, NotInit
from core.rule import Rule
from core.sheerka.services.SheerkaEvaluateRules import SheerkaEvaluateRules
from core.sheerka.services.SheerkaExecute import ParserInput, SheerkaExecute
from core.sheerka.services.SheerkaRuleManager import PythonConditionExprVisitor
from core.sheerka.services.sheerka_service import BaseService, ChickenAndEggException, FailedToCompileError
from core.tokenizer import Tokenizer
from core.utils import unstr_concept
from parsers.BaseExpressionParser import TrueifyVisitor
from parsers.BaseNodeParser import ConceptNode
from parsers.BnfNodeParser import BnfNodeConceptExpressionVisitor
from parsers.ExpressionParser import ExpressionParser
from parsers.LogicalOperatorParser import LogicalOperatorParser
from sheerkapython.ExprToConditions import ExprToConditionsVisitor
CONCEPT_EVALUATION_STEPS = [
BuiltinConcepts.BEFORE_EVALUATION,
@@ -156,6 +154,10 @@ class SheerkaEvaluateConcept(BaseService):
:param var_name:
:return:
"""
from parsers.LogicalOperatorParser import LogicalOperatorParser
from parsers.ExpressionParser import ExpressionParser
if concept.get_metadata().where is None or concept.get_metadata().where.strip() == "":
return None
@@ -171,24 +173,14 @@ class SheerkaEvaluateConcept(BaseService):
try:
parser_input = context.sheerka.services[SheerkaExecute.NAME].get_parser_input(trueified_where)
parsed = ExpressionParser(auto_compile=False).parse(context, parser_input)
python_visitor = PythonConditionExprVisitor(context)
conditions = python_visitor.get_conditions(parsed.body.body)
expr_to_cond_python_visitor = ExprToConditionsVisitor(context)
conditions = expr_to_cond_python_visitor.get_conditions(parsed.body.body)
return WhereClauseDef(concept, concept.get_metadata().where, trueified_where, var_name, conditions)
except FailedToCompileError:
# TODO: manage invalid where clause
return None
# tokens = [t.str_value for t in Tokenizer(trueified_where)]
# if var_name in tokens:
# compiled = None
# try:
# compiled = compile(trueified_where, "<where clause>", "eval")
# except Exception:
# pass
# return WhereClauseDef(concept, concept.get_metadata().where, trueified_where, var_name, compiled)
# else:
# return None
@staticmethod
def get_recursive_definitions(context, concept, return_values):
"""
@@ -230,6 +222,7 @@ class SheerkaEvaluateConcept(BaseService):
:param possible_variables: concepts that must be considered as variables
:return:
"""
def get_filtered_by_prevent_circular_reference(return_values):
for ret_val in return_values:
from evaluators.PreventCircularReferenceEvaluator import PreventCircularReferenceEvaluator
@@ -256,16 +249,18 @@ class SheerkaEvaluateConcept(BaseService):
:param p: part of the concept being parsed
:return:
"""
parsers_to_use = INIT_AST_QUESTION_PARSERS if p in [ConceptParts.WHERE,
ConceptParts.PRE] else INIT_AST_PARSERS
evaluating_question = p in [ConceptParts.WHERE, ConceptParts.PRE]
parsers_to_use = INIT_AST_QUESTION_PARSERS if evaluating_question else INIT_AST_PARSERS
recursion_detected = False
while True:
return_value = current_context.sheerka.parse_unrecognized(current_context,
s,
parsers=parsers_to_use,
parsers_to_use,
who=who,
concept=c,
prop=p,
filter_func=only_successful,
is_question=evaluating_question,
possible_variables=possible_variables)
if not return_value.status:
@@ -318,7 +313,17 @@ class SheerkaEvaluateConcept(BaseService):
return concept_found
else:
# ...or a list of ReturnValueConcept to resolve
return get_return_value(context, concept, source, part_key)
ret_values = get_return_value(context, concept, source, part_key)
for ret_val in ret_values:
# correct when the concept is incorrectly detected as definition
# if its parameters can be resolved, it means that it was a concept instance
if ((inner_concept := get_parsed_concept(ret_val)) is not None and
not inner_concept.get_hints().is_instance and
variables_in_context(context, inner_concept.get_metadata().parameters)):
inner_concept.get_hints().is_instance = True
inner_concept.get_hints().is_evaluated = False
return ret_values
def apply_where_clause(self, context, where_clause_def, return_values):
"""
@@ -431,6 +436,7 @@ class SheerkaEvaluateConcept(BaseService):
ensure_bnf(context, concept)
if concept.get_bnf():
from parsers.BnfNodeParser import BnfNodeConceptExpressionVisitor
visitor = BnfNodeConceptExpressionVisitor()
visitor.visit(concept.get_bnf())
possible_variables = [c.name if isinstance(c, Concept) else c for c in visitor.references]
@@ -810,20 +816,20 @@ class SheerkaEvaluateConcept(BaseService):
return self.apply_ret(concept, sub_context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED))
def call_concept(self, context, concept, *args, **kwargs):
return self.call_concept_with_args(context,
concept,
hints=EvaluationHints(eval_body=True, eval_question=False),
*args,
**kwargs)
return self._inner_call_concept_with_hint_and_args(context,
concept,
hints=EvaluationHints(eval_body=True, eval_question=False),
*args,
**kwargs)
def evaluate_question(self, context, concept, *args, **kwargs):
return self.call_concept_with_args(context,
concept,
hints=EvaluationHints(eval_body=True, eval_question=True),
*args,
**kwargs)
return self._inner_call_concept_with_hint_and_args(context,
concept,
hints=EvaluationHints(eval_body=True, eval_question=True),
*args,
**kwargs)
def call_concept_with_args(self, context, concept, hints, *args, **kwargs):
def _inner_call_concept_with_hint_and_args(self, context, concept, hints, *args, **kwargs):
"""
call the concept using either args or kwargs (not both)
:param context:
@@ -120,8 +120,12 @@ class SheerkaEvaluateRules(BaseService):
:param missing_vars: if initialized to a set, keeps tracks of the missing variables
:return:
"""
bag_variables = set(bag.keys())
for k, v in bag.items():
context.add_to_short_term_memory(k, v)
results = []
for compiled_condition in conditions:
+72 -71
View File
@@ -24,81 +24,89 @@ class ParserInput:
Helper class that tokenizes the input once for all
"""
def __init__(self, text, tokens=None, length=None, start=None, end=None, yield_oef=True):
def __init__(self, text, tokens=None, start=None, end=None, yield_oef=True):
# Tokenizing the text may raise an exception
# But we do not want it to be thrown in the initializer (because it's bad !)
# So let's keep it somewhere and raise it as soon as we can
self.exception = None
self.text = text
self.tokens = tokens or None
if self.tokens:
if tokens is None:
try:
# the eof if forced, but will not be yield if not set to.
self.tokens = list(Tokenizer(self.text, yield_eof=True))
except Exception as ex:
self.tokens = None
self.exception = ex
else:
# make sure tokens ends with EOF token
# and do not modify the original token list
if len(self.tokens) == 0:
if len(tokens) == 0:
self.tokens = [Token(TokenKind.EOF, "", 0, 1, 1)]
elif (last_token := tokens[-1]).type != TokenKind.EOF:
self.tokens = tokens + [Token(TokenKind.EOF,
"",
last_token.index + 1,
last_token.line,
last_token.column + 1)]
else:
self.tokens = tokens
elif (last_token := self.tokens[-1]).type != TokenKind.EOF:
self.tokens = self.tokens + [Token(TokenKind.EOF,
"",
last_token.index + 1,
last_token.line,
last_token.column + 1)]
self.length = length # to be computed (again) in reset()
self.length = len(self.tokens) if self.tokens else -1
self.yield_oef = yield_oef
self.start = start or 0
self.original_end = end
if end is not None:
self.original_end = end # forced index of the last token
self.end = self.original_end # index of the last token => len(tokens) - 1 if full tokens
max_end = self.get_end_from_yield_eof(self.length, self.yield_oef)
self.end = end if end < max_end else max_end
else:
self.original_end = self.end = None
self.end = self.get_end_from_yield_eof(self.length, self.yield_oef)
self.sub_text = None
self.sub_tokens = None
self.pos = None
self.token = None
self.from_tokens = tokens is not None
self.from_tokens = text is None and tokens is not None
def __repr__(self):
from_tokens = "from_tokens" if self.from_tokens else ""
return f"ParserInput({from_tokens}'{self.text}')"
return f"ParserInput({from_tokens}'{self.as_text()}')"
@staticmethod
def get_end_from_yield_eof(length, yield_oef):
return length - 1 if yield_oef else length - 2
def reset(self, yield_oef=None):
def _get_end_from_yield_eof(_length, _yield_oef):
return _length - 1 if _yield_oef else _length - 2
if self.exception:
raise self.exception
if yield_oef is None:
yield_oef = self.yield_oef
# make sure tokens is correctly initialized
if self.tokens is None:
# the eof if forced, but will not be yield if not set to.
self.tokens = list(Tokenizer(self.text, yield_eof=True))
self.length = len(self.tokens)
if self.original_end is None:
self.end = _get_end_from_yield_eof(self.length, yield_oef)
self.end = self.get_end_from_yield_eof(self.length, yield_oef)
else:
self.end = self.original_end if self.original_end < self.length else \
_get_end_from_yield_eof(self.length, yield_oef)
max_end = self.get_end_from_yield_eof(self.length, yield_oef)
self.end = self.original_end if self.original_end < max_end else max_end
self.pos = self.start - 1
self.token = None
return self
def as_text(self, custom_switcher=None, tracker=None):
if not self.tokens or self.end is None:
# as_text is requested before reset().
# It means that we want the original text
if not self.tokens:
return self.text
if custom_switcher is None:
if self.sub_text:
return self.sub_text
if self.start == 0 and self.end == self.length - 1:
if self.start == 0 and self.end == self.length - 1 and self.text:
self.sub_text = self.text
return self.sub_text
self.sub_text = core.utils.get_text_from_tokens(self.tokens[self.start:self.end + 1])
return self.sub_text
else:
@@ -128,6 +136,11 @@ class ParserInput:
while self.token.type in (TokenKind.WHITESPACE, TokenKind.NEWLINE):
self.pos += 1
if self.pos > self.end:
self.token = Token(TokenKind.EOF,
"",
self.pos,
self.token.line,
self.token.column + 1)
return False
self.token = self.tokens[self.pos]
@@ -175,10 +188,7 @@ class ParserInput:
if self.from_tokens and len(self.tokens) == 0:
return True
if self.end == self.start:
return True
if self.end and self.end == self.start + 1 and self.tokens[self.start].type == TokenKind.WHITESPACE:
if self.end and self.end == self.start and self.tokens[self.start].type == TokenKind.WHITESPACE:
return True
return False
@@ -198,7 +208,11 @@ class ParserInput:
return clone
def sub_part(self, start, end, yield_oef=None):
return ParserInput(self.text, self.tokens, self.length, start, end, yield_oef)
return ParserInput(self.text, self.tokens, start, end, yield_oef)
@property
def effective_length(self):
return self.end - self.start + 1
class SheerkaExecute(BaseService):
@@ -426,12 +440,12 @@ class SheerkaExecute(BaseService):
pi = self.pi_cache.get(text)
if pi is NotFound: # when CacheManager.cache_only is True
pi = ParserInput(text)
self.pi_cache.put(text, pi)
return ParserInput(text, tokens=pi.tokens,
length=pi.length) # new instance, but no need to tokenize the text again
if pi.tokens:
self.pi_cache.put(text, pi)
return ParserInput(text, tokens=pi.tokens) # new instance, but no need to tokenize the text again
key = text or core.utils.get_text_from_tokens(tokens)
pi = ParserInput(key, tokens=tokens, length=len(tokens))
pi = ParserInput(key, tokens=tokens)
self.pi_cache.put(key, pi)
return pi
@@ -491,6 +505,12 @@ class SheerkaExecute(BaseService):
:return:
"""
def _compute_key_for_parser_input(_input):
if isinstance(_input.body, ParserInput) and _input.body.from_tokens:
return "".join(repr(t) for t in _input.body.tokens)
return self.get_input_as_text(_input.body)
# return_values must be a list
if not isinstance(return_values, list):
return_values = [return_values]
@@ -545,9 +565,7 @@ class SheerkaExecute(BaseService):
if self.sheerka.enable_parser_caching and to_process and parsers_key:
processed = []
for return_value in to_process:
to_parse_as_str = self.get_input_as_text(return_value.body.body) \
if self.sheerka.isinstance(return_value.body, BuiltinConcepts.USER_INPUT) \
else return_value.body.source
to_parse_as_str = _compute_key_for_parser_input(return_value.body)
key_to_use = (parsers_key, to_parse_as_str)
if key_to_use in self.parsers_cache:
@@ -822,6 +840,7 @@ class SheerkaExecute(BaseService):
def parse_unrecognized(self, context, source, parsers,
who=None,
concept=None,
prop=None,
filter_func=None,
is_question=False,
@@ -832,31 +851,23 @@ class SheerkaExecute(BaseService):
:param source: ParserInput if possible
:param parsers:
:param who: who is asking the parsing ?
:param concept: concept being compiled
:param prop: Extra info, when parsing a property
:param filter_func: Once the result are found, call this function to filter them
:param is_question: Force EVAL_QUESTION_REQUESTED
:param possible_variables: concepts that must be considered as variables
:return:
"""
# def filter_for_circular_reference(return_values, concept):
# for r in return_values:
# if r.status and context.sheerka.isinstance(r.body, BuiltinConcepts.PARSER_RESULT):
# body = r.body.body[0] if isinstance(r.body.body, list) and len(r.body.body) == 1 else r.body.body
# if hasattr(body, "get_concept") and body.get_concept().id == concept.id:
# continue
# yield r
sheerka = context.sheerka
if prop:
action_context = {"prop": prop, "source": source}
action_context = {"concept": concept, "prop": prop, "source": source}
desc = f"Parsing attribute '{prop}'"
else:
action_context = source
desc = f"Parsing '{source}'"
with context.push(BuiltinConcepts.PARSING, action_context, who=who, desc=desc) as sub_context:
with context.push(BuiltinConcepts.PARSE_CODE, action_context, who=who, desc=desc) as sub_context:
if (prop in (Keywords.WHERE, Keywords.PRE, ConceptParts.WHERE, ConceptParts.PRE, Keywords.WHEN) or
is_question):
@@ -876,14 +887,6 @@ class SheerkaExecute(BaseService):
sheerka.new(BuiltinConcepts.USER_INPUT, body=source))
res = sheerka.execute(sub_context, to_parse, PARSE_STEPS)
# # before user defined filtering, remove the return values that may lead to circular reference
# in_recursion = list(context.search(predicate=lambda c: c.action == BuiltinConcepts.EVALUATING_CONCEPT,
# get_obj=lambda c: c.action_context,
# only_first=True))
#
# if in_recursion:
# res = list(filter_for_circular_reference(res, in_recursion[0]))
if filter_func:
res = filter_func(sub_context, res)
@@ -902,8 +905,8 @@ class SheerkaExecute(BaseService):
from parsers.BaseNodeParser import SourceCodeWithConceptNode
sheerka = context.sheerka
from parsers.FunctionParser import FunctionParser
parser = FunctionParser()
from parsers.FunctionParserOld import FunctionParserOld
parser = FunctionParserOld()
desc = f"Parsing function '{source}'"
with context.push(BuiltinConcepts.PARSE_CODE, source, desc=desc) as sub_context:
sheerka_execution = sheerka.services[SheerkaExecute.NAME]
@@ -933,9 +936,7 @@ class SheerkaExecute(BaseService):
from parsers.PythonParser import PythonParser
desc = desc or f"Compiling python '{source}'"
with context.push(BuiltinConcepts.PARSE_CODE,
{"language": "Python", "source": source},
desc) as sub_context:
with context.push(BuiltinConcepts.PARSE_CODE, source, desc) as sub_context:
parser_input = context.sheerka.services[SheerkaExecute.NAME].get_parser_input(source)
python_parser = PythonParser()
return python_parser.parse(sub_context, parser_input)
@@ -256,7 +256,7 @@ class SheerkaIsAManager(BaseService):
return self.sheerka.new(BuiltinConcepts.CONDITION_FAILED, body=concept)
predicate = concept.get_metadata().where.replace(possibles_variables[0], "sheerka.objvalue(self)")
res = self.sheerka.filter_objects(context, results, predicate)
res = self.sheerka.filter_objects(context, results, predicate=predicate)
return res
def _get_concepts(self, context, ids, evaluate):
+1 -1
View File
@@ -257,7 +257,7 @@ class SheerkaMemory(BaseService):
current_list = sorted(current_list, key=lambda o: o.timestamp, reverse=True)
current_objects = [o.obj for o in current_list]
res = self.sheerka.filter_objects(context, current_objects, name_to_use)
res = self.sheerka.filter_objects(context, current_objects, predicate=name_to_use)
if len(res) > 0:
return res[0] # only the first, as it should have a better timestamp
@@ -1,11 +1,11 @@
from cache.FastCache import FastCache
from core.builtin_concepts_ids import BuiltinContainers, BuiltinConcepts
from core.builtin_concepts_ids import BuiltinConcepts, BuiltinContainers
from core.concept import Concept, ConceptParts
from core.sheerka.services.SheerkaEvaluateRules import SheerkaEvaluateRules
from core.sheerka.services.sheerka_service import BaseService
from core.tokenizer import Tokenizer, TokenKind
from core.tokenizer import TokenKind, Tokenizer
from core.utils import as_bag
from sheerkapython.python_wrapper import create_namespace, ObjectContainer, get_type
from sheerkapython.python_wrapper import ObjectContainer, create_namespace, get_type
from sheerkaql.lexer import Lexer
from sheerkaql.parser import Parser
@@ -17,6 +17,7 @@ class SheerkaQueryManager(BaseService):
NAME = "QueryManager"
OBJECTS_ROOT_ALIAS = "__xxx__objects__xx__"
QUERY_PARAMETER_PREFIX = "__xxx__query_parameter__xx__"
MAPPING_PREFIX = "__xxx__map__xx__"
def __init__(self, sheerka):
super().__init__(sheerka, order=16)
@@ -39,16 +40,19 @@ class SheerkaQueryManager(BaseService):
def initialize_deferred(self, context, is_first_time):
self.rule_evaluator = self.sheerka.services[SheerkaEvaluateRules.NAME]
def get_query_by_kwargs(self, local_namespace, **kwargs):
def get_query_by_kwargs(self, local_namespace, use_mapping, **kwargs):
"""
Create a predicate using kwargs and filter the result
:param local_namespace:
:param use_mapping: True if a mapping is to be used
:param kwargs:
:return:
"""
if not kwargs:
return None
self_ident = f"{self.MAPPING_PREFIX}(self)" if use_mapping else "self"
objects_in_context_index = 0
conditions = []
for k, v in kwargs.items():
@@ -57,28 +61,39 @@ class SheerkaQueryManager(BaseService):
local_namespace[current_variable_name] = v
if k == "__type":
conditions.append(f"get_type(self) == {current_variable_name}")
conditions.append(f"get_type({self_ident}) == {current_variable_name}")
elif k in ("__self", "_"):
conditions.append(f"self == {current_variable_name}")
conditions.append(f"{self_ident} == {current_variable_name}")
elif k.endswith("_contains"):
prop_name = k[:-9]
conditions.append(f"{current_variable_name} in self.{prop_name}")
conditions.append(f"{current_variable_name} in {self_ident}.{prop_name}")
else:
conditions.append(f"self.{k} == {current_variable_name}")
conditions.append(f"{self_ident}.{k} == {current_variable_name}")
return ' and '.join(conditions)
def filter_objects(self, context, objects, predicate=None, **kwargs):
def get_updated_predicate(self, predicate, use_mapping):
if predicate is None:
return None
if use_mapping:
predicate = predicate.replace("self", f"{self.MAPPING_PREFIX}(self)")
return predicate
def filter_objects(self, context, objects, mapping=None, predicate=None, **kwargs):
"""
filter the given objects using the conditions from kwargs
for each k,v in kwargs, the equality k == v is added
for k starting with a double underscore '__', a special treatment may be done
__type : get the type of object (in Sheerka world)
:param context:
:param objects:
:param mapping: mapping to execute on each object before applying the predicate (lambda obj:obj)
:param predicate:
:param kwargs:
:return:
@@ -93,8 +108,9 @@ class SheerkaQueryManager(BaseService):
debugger.debug_entering(nb_objects=len(objects), predicate=predicate, **kwargs)
local_namespace = {}
query_by_kwargs = self.get_query_by_kwargs(local_namespace, **kwargs)
local_namespace = {} if mapping is None else {self.MAPPING_PREFIX: mapping}
query_by_kwargs = self.get_query_by_kwargs(local_namespace, mapping is not None, **kwargs)
predicate = self.get_updated_predicate(predicate, mapping is not None)
if predicate is not None and query_by_kwargs is not None:
query = f"({predicate}) and ({query_by_kwargs})"
+19 -445
View File
@@ -1,27 +1,24 @@
import operator
from dataclasses import dataclass
from typing import Union, Set, List, Tuple
from typing import List, Set, Tuple, Union
from cache.Cache import Cache
from cache.ListIfNeededCache import ListIfNeededCache
from core.ast_helpers import UnreferencedNamesVisitor
from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept
from core.builtin_helpers import ensure_evaluated, expect_one, evaluate_from_source, \
get_possible_variables_from_concept, is_a_question, only_successful, is_only_successful, evaluate_return_values
from core.builtin_helpers import evaluate_from_source, expect_one
from core.concept import Concept
from core.global_symbols import EVENT_RULE_PRECEDENCE_MODIFIED, RULE_COMPARISON_CONTEXT, NotFound, ErrorObj, \
EVENT_RULE_CREATED, EVENT_RULE_DELETED, NotInit, INIT_AST_PARSERS
from core.rule import Rule, ACTION_TYPE_PRINT
from core.sheerka.Sheerka import RECOGNIZED_BY_NAME, RECOGNIZED_BY_ID
from core.global_symbols import EVENT_RULE_CREATED, EVENT_RULE_DELETED, EVENT_RULE_PRECEDENCE_MODIFIED, ErrorObj, \
NotFound, NotInit, RULE_COMPARISON_CONTEXT
from core.rule import ACTION_TYPE_PRINT, Rule
from core.sheerka.Sheerka import RECOGNIZED_BY_ID, RECOGNIZED_BY_NAME
from core.sheerka.services.sheerka_service import BaseService, FailedToCompileError, UnknownVariableError
from core.tokenizer import Keywords, TokenKind, Token
from core.utils import merge_dicts, merge_sets, get_safe_str_value
from evaluators.PythonEvaluator import PythonEvaluator
from parsers.BaseExpressionParser import AndNode, ExpressionVisitor, VariableNode, ComparisonNode, FunctionNode, \
ComparisonType, NotNode, NameExprNode
from core.tokenizer import Token, TokenKind
from parsers.BaseExpressionParser import AndNode, ComparisonNode, ExpressionVisitor, \
FunctionNodeOld, NameExprNode, NotNode, VariableNode
from parsers.BaseNodeParser import ConceptNode
from parsers.FormatRuleActionParser import FormatRuleActionParser
from parsers.LogicalOperatorParser import LogicalOperatorParser
from sheerkapython.ExprToConditions import ExprToConditionsVisitor
from sheerkapython.python_wrapper import Expando, sheerka_globals
from sheerkarete.common import V
from sheerkarete.conditions import AndConditions, Condition, NegatedCondition, NegatedConjunctiveConditions
@@ -199,11 +196,10 @@ class SheerkaRuleManager(BaseService):
pass
# get the conditions as sheerka conditions
try:
python_visitor = PythonConditionExprVisitor(context)
python_conditions = python_visitor.get_conditions(parsed)
except FailedToCompileError as ex:
raise ex
python_visitor = ExprToConditionsVisitor(context)
python_conditions = python_visitor.get_conditions(parsed)
if python_visitor.errors:
raise FailedToCompileError(python_visitor.errors)
return ConditionCompilationResult(python_conditions, rete_conditions)
@@ -220,9 +216,8 @@ class SheerkaRuleManager(BaseService):
def compile_exec(self, context, source):
parsed = context.sheerka.parse_unrecognized(context,
source,
parsers="all",
"all",
who=self.NAME,
prop=Keywords.THEN,
filter_func=expect_one)
return parsed
@@ -425,13 +420,13 @@ class SheerkaRuleManager(BaseService):
return rule
# try via indirection
if (rule_id := self.sheerka.get_from_short_term_memory(context, obj.value[1])) is not NotFound and \
(rule := self._inner_get_by_id(str(rule_id))) is not None:
(rule := self._inner_get_by_id(str(rule_id))) is not None:
return rule
elif isinstance(obj, Rule):
if obj.metadata.id_is_unresolved:
if (rule_id := self.sheerka.get_from_short_term_memory(context, obj.id)) is not NotFound and \
(rule := self._inner_get_by_id(str(rule_id))) is not None:
(rule := self._inner_get_by_id(str(rule_id))) is not None:
return rule
else:
return self._inner_get_by_id(obj.id)
@@ -711,7 +706,7 @@ class ReteConditionExprVisitor(GetConditionExprVisitor):
else:
raise FailedToCompileError([expr_node])
def visit_FunctionNode(self, expr_node: FunctionNode):
def visit_FunctionNodeOld(self, expr_node: FunctionNodeOld):
if expr_node.first.value == "recognize(":
if not isinstance(expr_node.parameters[0].value, VariableNode):
return FailedToCompileError([f"Cannot recognize '{expr_node.parameters[0].value}'"])
@@ -823,7 +818,7 @@ class ReteConditionExprVisitor(GetConditionExprVisitor):
attr = attr or FACT_SELF
if (isinstance(value, Expando) and value.get_name() == "sheerka" or
isinstance(value, Concept) and value.id == self.context.sheerka.id):
isinstance(value, Concept) and value.id == self.context.sheerka.id):
conditions.append(Condition(left, attr, "__sheerka__"))
elif isinstance(value, Concept):
res = self.recognize_concept(var_path, value, {})
@@ -833,424 +828,3 @@ class ReteConditionExprVisitor(GetConditionExprVisitor):
conditions.append(Condition(left, attr, var_root))
else:
conditions.append(Condition(left, attr, value))
@dataclass()
class PythonConditionExprVisitorObj:
text: Union[str, None] # human readable
source: Union[str, None] # python expression to compile
objects: dict
variables: set
not_variables: set
@staticmethod
def create_function(first, last, parameters):
def get_function_as_text(parameter):
if parameter is None:
return f"{first}{last}"
else:
return f"{first}{parameter}{last}"
if parameters is None:
source = get_function_as_text(None)
return PythonConditionExprVisitorObj(source, source, {}, set(), set())
parameters_as_list = parameters if isinstance(parameters, list) else [parameters]
res = []
for obj in parameters_as_list:
res.append(PythonConditionExprVisitorObj(get_function_as_text(obj.text),
get_function_as_text(obj.source),
obj.objects,
obj.variables,
obj.not_variables))
return res[0] if len(res) == 1 else res
@staticmethod
def create_and(left, right):
def get_source(a, b):
if a is None and b is None:
return None
if a is None or a == "":
return b
if b is None or b == "":
return a
return a + " and " + b # no need to protect with parenthesis
return PythonConditionExprVisitorObj(get_source(left.text, right.text),
get_source(left.source, right.source),
merge_dicts(left.objects, right.objects),
merge_sets(left.variables, right.variables),
merge_sets(left.not_variables, right.not_variables))
@staticmethod
def combine_with_comma(left, right):
def get_source(a, b):
if a is None and b is None:
return None
if a is None or a == "":
return b
if b is None or b == "":
return a
return a + ", " + b # no need to protect with parenthesis
if left is None:
return right
return PythonConditionExprVisitorObj(get_source(left.text, right.text),
get_source(left.source, right.source),
merge_dicts(left.objects, right.objects),
merge_sets(left.variables, right.variables),
merge_sets(left.not_variables, right.not_variables))
@staticmethod
def create_not(node):
def get_source(a):
return f"not ({a})"
return PythonConditionExprVisitorObj(get_source(node.text),
get_source(node.source),
node.objects,
node.variables,
node.not_variables)
@staticmethod
def create_condition(text, op, left, right):
def get_source(a, b):
if op == ComparisonType.EQUALS and b == "sheerka":
return f"is_sheerka({a})"
else:
return ComparisonNode.rebuild_source(a, op, b)
return PythonConditionExprVisitorObj(text,
get_source(left.source, right.source),
merge_dicts(left.objects, right.objects),
merge_sets(left.variables, right.variables),
merge_sets(left.not_variables, right.not_variables))
class PythonConditionExprVisitor(GetConditionExprVisitor):
def __init__(self, context):
super().__init__(context)
self.check_variable_existence_only = True
self.concepts_to_reset = set()
def get_conditions(self, expr_node):
self.check_variable_existence_only = True
self.var_counter = 0
self.variables.clear()
self.concepts_to_reset.clear()
visitor_obj = self.visit(expr_node)
if self.check_variable_existence_only:
return [CompiledCondition(None,
None,
visitor_obj.variables,
visitor_obj.not_variables,
visitor_obj.objects,
self.concepts_to_reset)]
else:
if self.variables:
variables_definitions = "\n".join([f"{v} = {k}" for k, v in self.variables.items()])
source = variables_definitions + "\n" + visitor_obj.source
text = variables_definitions + "\n" + visitor_obj.text
else:
source = visitor_obj.source
text = visitor_obj.text
ret = self.context.sheerka.parse_python(self.context, source)
if ret.status:
ret.body.body.original_source = text
ret.body.body.objects = visitor_obj.objects
return [CompiledCondition(PythonEvaluator.NAME,
ret,
visitor_obj.variables,
visitor_obj.not_variables,
visitor_obj.objects,
self.concepts_to_reset)]
else:
errors = ret.body.reason if self.context.sheerka.isinstance(ret.body, BuiltinConcepts.NOT_FOR_ME) \
else ret.body.body
raise FailedToCompileError(errors)
def get_new_variable(self, variable_path: List[str], obj_variables):
obj_variables.add(variable_path[0])
var_name, var_def = self.inner_get_new_variable(variable_path)
return var_name
def unpack_variable(self, variable_path: List[str], obj_variables):
if self.is_a_possible_variable(variable_path[0]):
obj_variables.add(variable_path[0])
return self.inner_unpack_variable(variable_path)
@staticmethod
def construct_variable(root, attribute):
if attribute is None or attribute.strip() == "":
return root
return root + "." + attribute
def visit_VariableNode(self, expr_node: VariableNode):
if expr_node.attributes_str is None and not expr_node.name.startswith("__"):
# try to recognize a concept
res = self.evaluate_from_source(expr_node.name, is_question=True)
if res.status and isinstance(res.value, Concept):
if self.context.possible_variables and res.value.name in self.context.possible_variables:
variable_name = expr_node.get_source()
return PythonConditionExprVisitorObj(variable_name, variable_name, {}, {variable_name}, set())
# else / otherwise
self.check_variable_existence_only = False
return self.manage_concept(expr_node.get_source(), res.value)
else:
if self.context.sheerka.has_error(self.context, res, __type=BuiltinConcepts.TOO_MANY_SUCCESS):
raise FailedToCompileError([res])
variable_name = expr_node.get_source()
if res.status:
variables = set()
self.check_variable_existence_only = False
else:
variables = {variable_name}
return PythonConditionExprVisitorObj(variable_name, variable_name, {}, variables, set())
if self.check_variable_existence_only:
# special case where we want to check the existence of the whole string
variable_name = expr_node.get_source()
possible_variables = {variable_name} if self.is_a_possible_variable(variable_name) else set()
else:
# try to detect the variable
possible_variables = set()
var_root, var_attr = self.unpack_variable(expr_node.unpack(), possible_variables)
variable_name = self.construct_variable(var_root, var_attr)
return PythonConditionExprVisitorObj(variable_name, variable_name, {}, possible_variables, set())
def visit_ComparisonNode(self, expr_node: ComparisonNode):
self.check_variable_existence_only = False
left = self.visit(expr_node.left)
right = self.visit(expr_node.right)
# special case when we call recognize concept when an equality with a concept is found
right_value = right.objects[right.source] if right.source in right.objects else None
if expr_node.comp == ComparisonType.EQUALS and isinstance(right_value, Concept):
res = self.recognize_concept(expr_node.left.unpack(), right_value, {}, expr_node.get_source())
else:
res = PythonConditionExprVisitorObj.create_condition(expr_node.get_source(), expr_node.comp, left, right)
return res
def visit_AndNode(self, expr_node: AndNode):
current_visitor_obj = self.visit(expr_node.parts[0])
for node in expr_node.parts[1:]:
visitor_obj = self.visit(node)
current_visitor_obj = PythonConditionExprVisitorObj.create_and(current_visitor_obj, visitor_obj)
return current_visitor_obj
def visit_FunctionNode(self, expr_node: FunctionNode):
self.check_variable_existence_only = False
if expr_node.first.value == "recognize(":
if not isinstance(expr_node.parameters[0].value, VariableNode):
return FailedToCompileError([f"Cannot recognize '{expr_node.parameters[0].value}'"])
return self.recognize_concept(expr_node.parameters[0].value.unpack(),
expr_node.parameters[1].value,
{})
else:
params_as_visitor_obj = None
for param in expr_node.parameters:
current_visitor_obj = self.visit(param.value)
params_as_visitor_obj = PythonConditionExprVisitorObj.combine_with_comma(params_as_visitor_obj,
current_visitor_obj)
return PythonConditionExprVisitorObj.create_function(expr_node.first, expr_node.last, params_as_visitor_obj)
def visit_NotNode(self, expr_node: NotNode):
visitor_obj = self.visit(expr_node.node)
if self.check_variable_existence_only:
return PythonConditionExprVisitorObj(visitor_obj.text,
visitor_obj.source,
visitor_obj.objects,
visitor_obj.not_variables,
visitor_obj.variables)
return PythonConditionExprVisitorObj.create_not(visitor_obj)
def visit_NameExprNode(self, expr_node: NameExprNode):
self.check_variable_existence_only = False
source = expr_node.get_source()
res = self.context.sheerka.parse_unrecognized(self.context,
source,
INIT_AST_PARSERS,
filter_func=only_successful,
is_question=True)
if not res.status:
raise FailedToCompileError([expr_node])
return_values = res.body.body if is_only_successful(self.context.sheerka, res) else [res]
# if the result is a concept, create an object for it, otherwise just leave the source as it is
res = evaluate_return_values(self.context, source, return_values, is_question=True)
if res.status:
if isinstance(res.value, Concept):
return self.manage_concept(expr_node.get_source(), res.value)
else:
obj_name, objects = self.get_object_name(res.value)
return PythonConditionExprVisitorObj(source, obj_name, objects, set(), set())
else:
if len(return_values) == 1:
body = return_values[0].body.body
if hasattr(body, "get_python_node"):
python_node = return_values[0].body.body.get_python_node()
unreferenced_names_visitor = UnreferencedNamesVisitor(self.context)
variables = unreferenced_names_visitor.get_names(python_node.ast_)
variables = variables - set(python_node.objects.keys())
return PythonConditionExprVisitorObj(python_node.original_source,
python_node.source,
python_node.objects,
variables,
set())
elif isinstance(body, Concept):
return self.manage_concept(expr_node.get_source(), body)
else:
raise FailedToCompileError([expr_node])
def recognize_concept(self, variable_path, concept_to_recognize, concept_variables: dict, original_source=None):
if not isinstance(concept_to_recognize, Concept):
concept_as_str = concept_to_recognize.get_source()
if not concept_as_str:
return FailedToCompileError([f"Missing concept in for {variable_path}"])
concept = self.evaluate_from_source(concept_as_str, return_body=True)
else:
concept = concept_to_recognize
obj_variables = set()
var_name = self.get_new_variable(variable_path, obj_variables)
objects = {}
source = f"isinstance({var_name}, Concept)"
if concept.get_hints().recognized_by == RECOGNIZED_BY_NAME:
source += f" and {var_name}.name == '{concept.name}'"
elif concept.get_hints().recognized_by == RECOGNIZED_BY_ID:
source += f" and {var_name}.id == '{concept.id}'"
else:
source += f" and {var_name}.key == '{concept.key}'"
concept_variables.update({k: v for k, v in concept.variables().items() if v is not NotInit})
text = source
for var_name, var_value in concept_variables.items():
new_var_path = variable_path.copy()
new_var_path.append(var_name)
obj_name, objects = self.get_object_name(var_value, objects)
variable_condition = self.create_comparison_condition(new_var_path,
ComparisonType.EQUALS,
obj_name,
var_value,
objects,
set())
source += " and " + variable_condition.source
text += " and " + variable_condition.text
return PythonConditionExprVisitorObj(original_source or text, source, objects, obj_variables, set())
def manage_concept(self, source, concept):
if is_a_question(self.context, concept):
return self.evaluate_concept_as_question(source, concept)
else:
return self.evaluate_concept(source, concept)
def evaluate_concept_as_question(self, original_text, concept):
concept_var_name, objects = self.get_object_name(concept)
source = f"evaluate_question({concept_var_name})"
variables = get_possible_variables_from_concept(self.context, concept)
self.concepts_to_reset.update(self.get_concepts_to_reset(concept))
return PythonConditionExprVisitorObj(original_text, source, objects, variables, set())
def evaluate_concept(self, original_text, concept):
ensure_evaluated(self.context, concept, eval_body=True)
source, objects = self.get_object_name(concept)
return PythonConditionExprVisitorObj(original_text, source, objects, set(), set())
def get_concepts_to_reset(self, concept):
"""
Returns all the concept that might be reset before a second evaluation
The algo is empirical, there must be a theory to make sure that no concept is missed
:param concept:
:return:
"""
res = set()
def _inner_get_concept_to_reset(_concept):
if _concept in res: # prevent circular references
return
res.add(_concept)
assert not _concept.get_hints().use_copy
for part_name, asts in _concept.get_compiled().items():
if isinstance(asts, Concept):
if is_a_question(self.context, asts):
res.update(self.get_concepts_to_reset(asts))
else:
for ret_val in asts:
body_as_list = ret_val.body.body # go through the ParserResult
if not isinstance(body_as_list, list):
body_as_list = [body_as_list]
for body in body_as_list:
if hasattr(body, "get_concept"): # to manage Concept and ConceptNode
c = body.get_concept()
if is_a_question(self.context, c):
res.update(self.get_concepts_to_reset(c))
elif hasattr(body, "get_python_node"): # to manage PythonNode and SourceCodeNode like
python_node = body.get_python_node()
for obj in python_node.objects.values():
if isinstance(obj, Concept) and is_a_question(self.context, obj):
res.update(self.get_concepts_to_reset(obj))
_inner_get_concept_to_reset(concept)
return res
def create_comparison_condition(self,
left_path,
op,
right_name,
right_value,
objects,
variables,
original_source=None):
possible_variables = variables.copy()
var_root, var_attr = self.unpack_variable(left_path, possible_variables)
left = self.construct_variable(var_root, var_attr)
if original_source is None:
right = get_safe_str_value(right_value or right_name)
original_source = ComparisonNode.rebuild_source(left, op, right)
if op == ComparisonType.EQUALS:
if self.context.sheerka.is_sheerka(right_value):
source = f"is_sheerka({left})"
return PythonConditionExprVisitorObj(original_source, source, objects, possible_variables, set())
if isinstance(right_value, Concept):
return self.recognize_concept(left_path, right_value, {}, original_source)
else:
source = ComparisonNode.rebuild_source(left, op, right_name)
return PythonConditionExprVisitorObj(original_source, source, objects, possible_variables, set())
else:
source = ComparisonNode.rebuild_source(left, op, right_name)
return PythonConditionExprVisitorObj(original_source, source, objects, possible_variables, set())
+2 -1
View File
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import Dict, List, Union
from core.concept import Concept
from core.global_symbols import NotFound, ErrorObj
@@ -52,7 +53,7 @@ class BaseService:
@dataclass()
class FailedToCompileError(Exception, ErrorObj):
cause: list
cause: Union[List, Dict]
@dataclass()