Fixed #48 : RelationalExpressionParser: Implement relational operator parser

Fixed #49 : ExpressionParser: Implement ExpressionParser
Fixed #50 : Implement ReteConditionExprVisitor
Fixed #51 : Implement PythonConditionExprVisitor
Fixed #52 : SheerkaConceptManager: I can get and set concept property
This commit is contained in:
2021-03-23 11:35:10 +01:00
parent f8e47e2b38
commit 6cda2686fb
25 changed files with 1083 additions and 978 deletions
+11
View File
@@ -113,6 +113,7 @@ class Sheerka(Concept):
"test_using_context": SheerkaMethod(self.test_using_context, False),
"test_dict": SheerkaMethod(self.test_dict, False),
"test_error": SheerkaMethod(self.test_error, False),
"is_sheerka": SheerkaMethod(self.is_sheerka, False),
}
self.concepts_ids = None
@@ -767,6 +768,16 @@ class Sheerka(Concept):
return a.key in b
def is_sheerka(self, obj):
if isinstance(obj, Concept) and obj.id == self.id:
return True
from evaluators.PythonEvaluator import Expando
if isinstance(obj, Expando) and obj.get_name() == "sheerka":
return True
return False
@staticmethod
def get_unknown(metadata):
"""
@@ -118,6 +118,8 @@ class SheerkaConceptManager(BaseService):
self.sheerka.bind_service_method(self.set_id_if_needed, True)
self.sheerka.bind_service_method(self.set_attr, True)
self.sheerka.bind_service_method(self.get_attr, False)
self.sheerka.bind_service_method(self.set_property, True, as_name="set_prop")
self.sheerka.bind_service_method(self.get_property, False, as_name="get_prop")
self.sheerka.bind_service_method(self.get_by_key, False, visible=False)
self.sheerka.bind_service_method(self.get_by_name, False, visible=False)
self.sheerka.bind_service_method(self.get_by_hash, False, visible=False)
@@ -285,15 +287,15 @@ class SheerkaConceptManager(BaseService):
# to_add is a dictionary
# to_add = {
# 'meta' : {<key, value>} of metadata to update,
# 'props' : {<key, value>} of properties to add/update,
# 'variables': {<key, value>} of variables to add/update,
# 'meta' : {<key>: <value>} of metadata to update,
# 'props' : {<key>: <value>} of properties to add/update,
# 'variables': {<key>: <value>} of variables to add/update,
# }
# if the <key> already exists, the entry is updated, otherwise a new value is created
# for props, if the <key> already exists, a new entry is added to the set
#
# to_remove = {
# 'props' : {<key, [value]>} entries to remove. 'value' can be a list or a single entry
# 'props' : {<key>: [value]} entries to remove. 'value' can be a list or a single entry
# 'variables': [<key>] list of keys to remove
# }
#
@@ -401,7 +403,7 @@ class SheerkaConceptManager(BaseService):
if (old_value := concept.get_value(attr)) is not NotInit:
if old_value == value:
return self.sheerka.ret(self.NAME, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
if isinstance(old_value, list):
old_value.append(value)
value = old_value
@@ -429,6 +431,49 @@ class SheerkaConceptManager(BaseService):
return self.sheerka.new(BuiltinConcepts.NOT_FOUND, body={"#concept": concept, "#attr": attribute})
return value
def get_property(self, concept, prop):
"""
Returns the value of a concept property
:param concept:
:param prop:
:return:
"""
ensure_concept()
if not self.sheerka.is_success(concept):
return concept
if isinstance(prop, Concept) and prop.get_metadata().is_builtin:
prop = prop.key
if (value := concept.get_prop(prop)) is None:
return self.sheerka.new(BuiltinConcepts.NOT_FOUND, body={"#concept": concept, "#prop": prop})
return value
def set_property(self, context, concept, prop, value, all_concepts=False):
"""
Set the value of a concept property
The concept is modified
:param context:
:param concept:
:param prop:
:param value:
:param all_concepts: if True, updates the definitions of the concept
:return:
"""
ensure_concept()
if not self.sheerka.is_success(concept):
return concept
if isinstance(prop, Concept) and prop.get_metadata().is_builtin:
prop = prop.key
if all_concepts:
return self.modify_concept(context, concept, to_add={'props': {prop: value}}, modify_source=True)
else:
concept.set_prop(prop, value)
return self.sheerka.ret(self.NAME, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
def set_id_if_needed(self, obj: Concept, is_builtin: bool):
"""
Set the key for the concept if needed
@@ -649,7 +694,7 @@ class SheerkaConceptManager(BaseService):
if "props" in to_add:
for k, v in to_add["props"].items():
concept.add_prop(k, v)
concept.set_prop(k, v)
if "variables" in to_add:
for k, v in to_add["variables"].items():
@@ -85,7 +85,7 @@ class SheerkaEvaluateRules(BaseService):
results.setdefault(LOW_PRIORITY_RULES, []).append(rule)
continue
res = self.evaluate_rule_old(sub_context, rule, bag)
res = self.evaluate_rule(sub_context, rule, bag)
ok = res.status and self.sheerka.is_success(self.sheerka.objvalue(res))
results.setdefault(ok, []).append(rule)
if ok and success_priority is None:
@@ -96,42 +96,6 @@ class SheerkaEvaluateRules(BaseService):
sub_context.add_values(rules_result=results)
return results
def evaluate_rule_old(self, context, rule, bag):
"""
Evaluate the conditions
:param context:
:param rule:
:param bag:
:return:
"""
results = []
for rule_predicate in rule.compiled_predicates:
if rule_predicate.source in bag:
# simple case where the rule is an item of the bag. No need of complicate evaluation
results.append(context.sheerka.ret(self.NAME, True, bag[rule_predicate.source]))
else:
# do not forget to reset the 'is_evaluated' in the case of a concept
if rule_predicate.evaluator == ConceptEvaluator.NAME:
rule_predicate.concept.get_metadata().is_evaluated = False
evaluator = self.evaluators_by_name[rule_predicate.evaluator]
res = evaluator.eval(context, rule_predicate.predicate)
if res.status and isinstance(res.body, bool) and res.body:
# one successful value found. No need to look any further
results = [res]
break
else:
results.append(res)
debugger = context.get_debugger(SheerkaEvaluateRules.NAME, "evaluate_rule", new_debug_id=False)
debugger.debug_rule(rule, results)
return expect_one(context, results)
def evaluate_rule(self, context, rule, bag):
"""
Evaluate the conditions
@@ -149,6 +113,9 @@ class SheerkaEvaluateRules(BaseService):
if compiled_condition.variables.intersection(bag_variables) != compiled_condition.variables:
continue
if compiled_condition.not_variables.intersection(bag_variables):
continue
if compiled_condition.return_value is None:
# We only want to test the existence of a data
results.append(context.sheerka.ret(self.NAME, True, True))
+2 -2
View File
@@ -786,6 +786,6 @@ class SheerkaExecute(BaseService):
desc = desc or f"Parsing expression '{source}'"
with context.push(BuiltinConcepts.PARSE_CODE, source, desc) as sub_context:
parser_input = context.sheerka.services[SheerkaExecute.NAME].get_parser_input(source)
from parsers.LogicalOperatorParser import LogicalOperatorParser
expr_parser = LogicalOperatorParser()
from parsers.ExpressionParser import ExpressionParser
expr_parser = ExpressionParser()
return expr_parser.parse(sub_context, parser_input)
@@ -1,6 +1,7 @@
from core.builtin_concepts import BuiltinConcepts
from core.builtin_helpers import ensure_concept
from core.sheerka.services.sheerka_service import BaseService
from core.utils import merge_sets
class SheerkaHasAManager(BaseService):
@@ -35,7 +36,8 @@ class SheerkaHasAManager(BaseService):
name=BuiltinConcepts.HASA,
concept=concept_a))
to_add = {"props": {BuiltinConcepts.HASA: concept_b}}
merged_concepts = merge_sets(concept_a.get_prop(BuiltinConcepts.HASA), {concept_b})
to_add = {"props": {BuiltinConcepts.HASA: merged_concepts}}
return self.sheerka.modify_concept(context, concept_a, to_add, modify_source=True)
def hasa(self, concept_a, concept_b):
@@ -7,6 +7,7 @@ from core.concept import Concept, ConceptParts, DEFINITION_TYPE_BNF
from core.global_symbols import NotFound
from core.sheerka.services.SheerkaConceptManager import SheerkaConceptManager
from core.sheerka.services.sheerka_service import BaseService
from core.utils import merge_sets
class SheerkaIsAManager(BaseService):
@@ -51,7 +52,8 @@ class SheerkaIsAManager(BaseService):
# KSI 20200709 add the concept, not its 'id' or 'key'
# It will allow conditions handling if concept set has its WHERE or PRE set to something
to_add = {"props": {BuiltinConcepts.ISA: concept_set}}
new_concept_set = merge_sets(concept.get_prop(BuiltinConcepts.ISA), {concept_set})
to_add = {"props": {BuiltinConcepts.ISA: new_concept_set}}
res = self.sheerka.modify_concept(context, concept, to_add, modify_source=True)
if not res.status:
return res
+335 -249
View File
@@ -2,32 +2,33 @@ import operator
import re
from dataclasses import dataclass
from itertools import product
from typing import Union, Set, List
from typing import Union, Set, List, Tuple
from cache.Cache import Cache
from cache.ListIfNeededCache import ListIfNeededCache
from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept
from core.builtin_helpers import is_a_question, ensure_evaluated, expect_one, evaluate
from core.builtin_helpers import ensure_evaluated, expect_one, evaluate
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
from core.rule import Rule, ACTION_TYPE_PRINT
from core.sheerka.Sheerka import RECOGNIZED_BY_NAME, RECOGNIZED_BY_ID
from core.sheerka.services.SheerkaExecute import ParserInput
from core.sheerka.services.sheerka_service import BaseService, FailedToCompileError
from core.tokenizer import Keywords, TokenKind, Token, IterParser
from core.utils import index_tokens, COLORS, get_text_from_tokens, merge_dictionaries, merge_sets
from evaluators.ConceptEvaluator import ConceptEvaluator
from evaluators.PythonEvaluator import PythonEvaluator, Expando
from parsers.BaseExpressionParser import AndNode, ExpressionVisitor, VariableNode, ComparisonNode, FunctionNode, \
ComparisonType
from parsers.BaseNodeParser import SourceCodeWithConceptNode, ConceptNode, SourceCodeNode
ComparisonType, NotNode, NameExprNode
from parsers.BaseNodeParser import ConceptNode
from parsers.LogicalOperatorParser import LogicalOperatorParser
from parsers.PythonParser import PythonNode
from parsers.PythonParser import PythonParser
from sheerkarete.common import V
from sheerkarete.conditions import AndConditions, Condition
from sheerkarete.conditions import AndConditions, Condition, NegatedCondition, NegatedConjunctiveConditions
from sheerkarete.network import FACT_NAME, FACT_SELF
CONCEPTS_ONLY_PARSERS = ["ExactConcept", "Bnf", "Sya", "Sequence"]
CONDITIONS_VISITOR_EVALUATORS = ["Python", "Concept"]
CONDITIONS_VISITOR_EVALUATORS = ["Python", "Concept", "LexerNode"]
identifier_regex = re.compile(r"[\w _.]+")
@@ -613,23 +614,6 @@ class NoConditionFound(ErrorObj):
return 0
@dataclass()
class RuleCompiledPredicate:
"""
The 'when' expression is parsed to have a ReturnValueConcept or a Concept that can then be evaluated
Depending on the evaluator, the 'predicate' attribute or the 'concept' attribute will be used
"""
source: str # what was compiled # DO NOT REMOVE
action: str # sheerka action when the rule must be executed # can be removed
# when used as a list of predicate to iterate thru
evaluator: str # evaluator to use when the rule will be evaluated
predicate: ReturnValueConcept # compiled source as ReturnValue
concept: Union[Concept, None] # compiled source as concept
variables: Set[str] = None # TODO: set of required variables
@dataclass()
class CompiledCondition:
"""
@@ -638,12 +622,13 @@ class CompiledCondition:
"""
evaluator_type: Union[str, None] # PythonEvaluator.NAME | ConceptEvaluator.NAME
return_value: Union[ReturnValueConcept, None] # compiled source as ReturnValue
variables: Set[str]
variables: Set[str] # variables that must be present in bag
not_variables: Set[str] # variables that must not be present in bag
concept: Union[Concept, None] = None # compiled source as concept
@dataclass
class CompiledWhenResult:
class ConditionCompilationResult:
"""
For a given source to compile (a given 'when')
List of RuleCompiledPredicate found
@@ -651,8 +636,8 @@ class CompiledWhenResult:
The two ways of evaluating a 'when' are used by Sheerka
"""
compiled_predicates: List[RuleCompiledPredicate]
rete_disjunctions: List[AndConditions]
python_conditions: List[CompiledCondition]
rete_conditions: List[AndConditions]
class SheerkaRuleManager(BaseService):
@@ -738,13 +723,13 @@ class SheerkaRuleManager(BaseService):
if rule.metadata.is_compiled:
return rule
if rule.compiled_predicates is None:
if rule.compiled_conditions is None:
try:
compiled_result = self.compile_when(context, self.NAME, rule.metadata.predicate)
rule.compiled_predicates = compiled_result.compiled_predicates
rule.rete_disjunctions = compiled_result.rete_disjunctions
compilation_result = self.compile_when(context, self.NAME, rule.metadata.predicate)
rule.compiled_conditions = compilation_result.python_conditions
rule.rete_disjunctions = compilation_result.rete_conditions
except FailedToCompileError as ex:
rule.compiled_predicates = None
rule.compiled_conditions = None
rule.rete_disjunctions = None
rule.error_sink = {"when": ex.cause}
@@ -774,48 +759,23 @@ class SheerkaRuleManager(BaseService):
:param source: what to compile
"""
# first, try to parse using expression parser
# -> Detect xxx and yyy or not zzz
action = None
parsed = []
errors = []
all_rete_disjunctions = []
parsed_expr_ret = context.sheerka.parse_expression(context, source)
parsed = parsed_expr_ret.body.body
rete_conditions, python_conditions = None, None
if parsed_expr_ret.status:
conjunctions = parsed_expr_ret.body.body.parts if isinstance(parsed_expr_ret.body.body, AndNode) else \
[parsed_expr_ret.body.body]
try:
rete_visitor = ReteConditionExprVisitor(context)
rete_conditions = rete_visitor.get_conditions(parsed)
except FailedToCompileError as err:
pass
# recognize __action == ''
if (action := self._recognized_action_definition(conjunctions[0].tokens)) is not None:
conjunctions = conjunctions[1:]
try:
python_visitor = PythonConditionExprVisitor(context)
python_conditions = python_visitor.get_conditions(parsed)
except FailedToCompileError as ex:
raise ex
if len(conjunctions) == 0:
errors.append(NoConditionFound())
else:
# compile conditions
try:
return_values, rete_disjunctions = self.expression_parser.compile_conjunctions(context,
conjunctions,
who)
parsed.extend(return_values)
all_rete_disjunctions.extend(rete_disjunctions)
except FailedToCompileError as ex:
errors.append(ex.cause)
if len(parsed) == 0:
raise FailedToCompileError(errors)
try:
compiled_predicates = self.add_evaluators(context,
source,
action,
parsed if hasattr(parsed, "__iter__") else [parsed])
return CompiledWhenResult(compiled_predicates, all_rete_disjunctions)
except EmitPythonCodeException as ex:
raise FailedToCompileError([ex.error])
return ConditionCompilationResult(python_conditions, rete_conditions)
def compile_print(self, context, source):
parser = FormatRuleActionParser(source)
@@ -859,7 +819,7 @@ class SheerkaRuleManager(BaseService):
# set id before saving in db
self.set_id_if_needed(rule)
if rule.compiled_predicates and rule.compiled_action:
if rule.compiled_conditions and rule.compiled_action:
rule.metadata.is_compiled = True
rule.metadata.is_enabled = True
@@ -918,7 +878,7 @@ class SheerkaRuleManager(BaseService):
# index=[2] in code, id=3 in debug
Rule("print", "Failed ReturnValue in red",
"__ret and not __ret.status",
"__ret and __ret.status == False",
"red(__ret)"),
# index=[3] in code, id=4 in debug
@@ -1015,44 +975,6 @@ class SheerkaRuleManager(BaseService):
reverse=True)
return self._exec_rules
def add_evaluators(self, context, source, action, ret_vals):
"""
Browse the ReturnValueConcepts to determine the evaluator to use
Returns a list of RulePredicate, basically a tuple (evaluator_name, return_value)
:param context:
:param source:
:param ret_vals:
:return:
"""
def get_rule_predicate_from_concept(c):
if is_a_question(context, c):
return RuleCompiledPredicate(source, action, ConceptEvaluator.NAME, r, c)
else:
to_parse = PythonCodeEmitter(context, "__ret.status").recognize_concept(c, "__ret.body").get_text()
return RuleCompiledPredicate(source,
action,
PythonEvaluator.NAME,
context.sheerka.parse_python(context, to_parse),
None)
res = []
for r in ret_vals:
underlying = self.sheerka.objvalue(r)
if isinstance(underlying, PythonNode):
res.append(RuleCompiledPredicate(source, action, PythonEvaluator.NAME, r, None))
elif isinstance(underlying, SourceCodeWithConceptNode):
res.append(RuleCompiledPredicate(source, action, PythonEvaluator.NAME, r, None))
elif isinstance(underlying, SourceCodeNode):
res.append(RuleCompiledPredicate(source, action, PythonEvaluator.NAME, r, None))
elif isinstance(underlying, Concept):
res.append(get_rule_predicate_from_concept(underlying))
elif hasattr(underlying, "__iter__") and len(underlying) == 1 and isinstance(underlying[0], ConceptNode):
res.append(get_rule_predicate_from_concept(underlying[0].concept))
else:
raise NotImplementedError(r)
return res
def resolve_rule(self, context, obj):
"""
Given obj, try to find the corresponding rule
@@ -1136,16 +1058,81 @@ class SheerkaRuleManager(BaseService):
return None
class ReteConditionExprVisitor(ExpressionVisitor):
"""
From an ExprNode, construct the list of Rete condition that can be used in the ReteNetwork
"""
class GetConditionExprVisitor(ExpressionVisitor):
def __init__(self, context):
self.context = context
self.var_counter = 0
self.variables = {}
def add_variable(self, target):
"""
Create a new variable
:param target:
:return:
"""
var_name = f"__x_{self.var_counter:02}__"
self.var_counter += 1
self.variables[target] = var_name
return var_name
def inner_unpack_variable(self, variable_path: List[str]) -> Tuple[str, str]:
"""
When variable_path = a.b.c.d
returns (x0, d) if x0 = a.b.c else (a, b.c.d)
:param variable_path:
:return:
"""
if len(variable_path) > 1:
left = variable_path[:-1]
right = [variable_path[-1]]
while left:
var_name = ".".join(left)
if var_name in self.variables:
return self.variables[var_name], ".".join(right)
right.insert(0, left.pop())
return variable_path[0], ".".join(variable_path[1:])
def inner_get_new_variable(self, variable_path: List[str]) -> Tuple[str, Tuple[str, str]]:
"""
Create a new variable if needed
:param variable_path:
:return: new variable name + how this variable is constructed
"""
path = ".".join(variable_path)
if path in self.variables:
return self.variables[path]
root, attr = self.inner_unpack_variable(variable_path)
var_name = self.add_variable(root + "." + attr)
return var_name, (root, attr)
def evaluate(self, source, eval_body):
res = evaluate(self.context,
source,
evaluators=CONDITIONS_VISITOR_EVALUATORS,
desc=None,
eval_body=eval_body,
eval_where=False,
is_question=False,
expect_success=False,
stm=None)
res = expect_one(self.context, res)
if not res.status:
raise FailedToCompileError([f"Failed to evaluate '{source}'"])
return res.value
class ReteConditionExprVisitor(GetConditionExprVisitor):
"""
From an ExprNode, construct the list of Rete condition that can be used in the ReteNetwork
"""
def get_conditions(self, expr_node):
self.var_counter = 0
self.variables.clear()
@@ -1153,12 +1140,6 @@ class ReteConditionExprVisitor(ExpressionVisitor):
conditions = self.visit(expr_node)
return [AndConditions(conditions)]
def add_variable(self, target):
var_name = f"__x_{self.var_counter:02}__"
self.var_counter += 1
self.variables[target] = var_name
return var_name
def init_or_get_variable_from_name(self, variable_path: List[str], conditions):
if len(variable_path) > 1:
@@ -1190,7 +1171,7 @@ class ReteConditionExprVisitor(ExpressionVisitor):
return variable
def visit_VariableNode(self, expr_node: VariableNode):
if expr_node.attributes_str is None and not expr_node.name.startswith("__"):
if expr_node.attributes_str is None:
# try to recognize a concept
res = evaluate(self.context,
expr_node.name,
@@ -1206,9 +1187,11 @@ class ReteConditionExprVisitor(ExpressionVisitor):
return self.recognize_concept(["__ret", "body"], res.value, {})
conditions = []
var_name, attr = self.init_or_get_variable_from_name(expr_node.unpack(), conditions)
if attr:
conditions.append(Condition(var_name, attr, True))
variable_name = expr_node.get_source()
if variable_name not in self.variables:
variable_ref = self.add_variable(variable_name)
conditions.append(Condition(V(variable_ref), "__name__", variable_name))
return conditions
def visit_AndNode(self, expr_node: AndNode):
@@ -1221,20 +1204,8 @@ class ReteConditionExprVisitor(ExpressionVisitor):
def visit_ComparisonNode(self, expr_node: ComparisonNode):
if isinstance(expr_node.left, VariableNode):
conditions = []
res = evaluate(self.context,
expr_node.right.get_source(),
evaluators=CONDITIONS_VISITOR_EVALUATORS,
desc=None,
eval_body=False,
eval_where=False,
is_question=False,
expect_success=False,
stm=None)
res = expect_one(self.context, res)
if not res.status:
return FailedToCompileError([f"Cannot recognize '{expr_node.right.get_source()}'"])
self.add_to_condition(expr_node.left.unpack(), res.value, conditions)
value = self.evaluate(expr_node.right.get_source(), True)
self.add_to_condition(expr_node.left.unpack(), value, conditions)
return conditions
else:
raise FailedToCompileError([expr_node])
@@ -1248,6 +1219,68 @@ class ReteConditionExprVisitor(ExpressionVisitor):
expr_node.parameters[1].value,
{})
def visit_NotNode(self, expr_node: NotNode):
def get_sub_conditions(conditions_):
"""
Looks for [(x, __name__, y), ...]
(x, __name__, y) -> exist_condition
... -> sub_conditions
:param conditions_:
:return:
"""
exists_condition_ = None
sub_conditions_ = []
for c in conditions_:
if c.attribute == FACT_NAME:
exists_condition_ = c
else:
sub_conditions_.append(c)
return exists_condition_, sub_conditions_
def negate_conditions(exists_condition_, conditions_):
res = [exists_condition_] if exists_condition_ else []
if len(conditions_) == 1:
c = conditions_[0]
if type(c) == Condition:
res.append(NegatedCondition(c.identifier, c.attribute, c.value))
elif type(c) == NegatedCondition:
res.append(Condition(c.identifier, c.attribute, c.value))
else:
raise NotImplementedError
else:
res.append(NegatedConjunctiveConditions(*conditions_))
return res
conditions = self.visit(expr_node.node)
if len(conditions) == 1:
cond = conditions[0]
if type(cond) == NegatedCondition and cond.attribute == FACT_NAME:
return [Condition(cond.identifier, cond.attribute, cond.value)]
elif type(cond) == Condition and cond.attribute == FACT_NAME:
return [NegatedCondition(cond.identifier, cond.attribute, cond.value)]
else:
return negate_conditions(None, conditions)
else:
exists_condition, sub_conditions = get_sub_conditions(conditions)
return negate_conditions(exists_condition, sub_conditions)
def visit_NameExprNode(self, expr_node: NameExprNode):
res = evaluate(self.context,
expr_node.get_source(),
evaluators=CONDITIONS_VISITOR_EVALUATORS,
desc=None,
eval_body=True,
eval_where=False,
is_question=False,
expect_success=False,
stm=None)
res = expect_one(self.context, res)
if res.status and isinstance(res.value, Concept):
return self.recognize_concept(["__ret", "body"], res.value, {})
def recognize_concept(self, variable_path, concept_to_recognize, concept_variables: dict):
"""
Creates Rete conditions to recognize a concept
@@ -1262,20 +1295,7 @@ class ReteConditionExprVisitor(ExpressionVisitor):
if not concept_as_str:
return FailedToCompileError([f"Missing concept in for {variable_path}"])
res = evaluate(self.context,
concept_as_str,
evaluators=CONDITIONS_VISITOR_EVALUATORS,
desc=None,
eval_body=True,
eval_where=False,
is_question=False,
expect_success=False,
stm=None)
res = expect_one(self.context, res)
if not res.status:
return FailedToCompileError([f"Unknown concept {concept_as_str}"])
concept = res.body
concept = self.evaluate(concept_as_str, True)
else:
concept = concept_to_recognize
@@ -1301,6 +1321,7 @@ class ReteConditionExprVisitor(ExpressionVisitor):
def add_to_condition(self, var_path, value, conditions):
left, attr = self.init_or_get_variable_from_name(var_path, conditions)
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):
conditions.append(Condition(left, attr, "__sheerka__"))
@@ -1317,11 +1338,10 @@ class PythonConditionExprVisitorObj:
source: Union[str, None] # python expression to compile
objects: dict
variables: set
not_variables: set
@staticmethod
def combine_with_and(left, right):
left_as_list = left if isinstance(left, list) else [left]
right_as_list = right if isinstance(right, list) else [right]
def create_and(a, b):
if a is None and b is None:
@@ -1333,22 +1353,43 @@ class PythonConditionExprVisitorObj:
return a
return a + " and " + b
left_as_list = left if isinstance(left, list) else [left]
right_as_list = right if isinstance(right, list) else [right]
left_right_product = list(product(left_as_list, right_as_list))
res = []
for left_obj, right_obj in left_right_product:
res.append(PythonConditionExprVisitorObj(create_and(left_obj.text, right_obj.text),
create_and(left_obj.source, right_obj.source),
merge_dictionaries(left_obj.objects, right_obj.objects),
merge_sets(left_obj.variables, right_obj.variables)))
merge_sets(left_obj.variables, right_obj.variables),
merge_sets(left_obj.not_variables, right_obj.not_variables)))
return res[0] if len(res) == 1 else res
@staticmethod
def combine_with_not(node):
def create_not(a):
return f"not ({a})"
node_as_list = node if isinstance(node, list) else [node]
res = []
for obj in node_as_list:
res.append(PythonConditionExprVisitorObj(create_not(obj.text),
create_not(obj.source),
obj.objects,
obj.variables,
obj.not_variables))
return res[0] if len(res) == 1 else res
class PythonConditionExprVisitor(ExpressionVisitor):
class PythonConditionExprVisitor(GetConditionExprVisitor):
def __init__(self, context):
self.context = context
self.var_counter = 0
self.variables = {}
super().__init__(context)
self.know_object_variables = {}
def get_conditions(self, expr_node):
self.var_counter = 0
@@ -1368,77 +1409,91 @@ class PythonConditionExprVisitor(ExpressionVisitor):
if ret.status:
ret.body.body.original_source = text
ret.body.body.objects = visitor_obj.objects
return [CompiledCondition(PythonEvaluator.NAME, ret, visitor_obj.variables)]
else:
return [CompiledCondition(None, None, visitor_obj.variables)]
return [CompiledCondition(PythonEvaluator.NAME, ret, visitor_obj.variables, visitor_obj.not_variables)]
def add_variable(self, target):
var_name = f"__x_{self.var_counter:02}__"
self.var_counter += 1
self.variables[target] = var_name
else:
errors = ret.body.reason if self.context.sheerka.isinstance(ret.body, BuiltinConcepts.NOT_FOR_ME) \
else ret.body.body
raise FailedToCompileError(errors)
else:
return [CompiledCondition(None, None, visitor_obj.variables, visitor_obj.not_variables)]
def get_variable(self, expr_node):
"""
From a ExprNode, try to know if it refers to a bag entry or it it's a python valid name
:param expr_node:
:return:
"""
if not isinstance(expr_node, VariableNode):
return None
var_root = expr_node.name
if var_root in self.know_object_variables:
return self.know_object_variables[var_root]
if self.context.sheerka.fast_resolve(var_root):
self.know_object_variables[var_root] = None
return None
python_parser = PythonParser()
ret = python_parser.parse(self.context, ParserInput(var_root))
if not ret.status:
self.know_object_variables[var_root] = var_root
return var_root
python_evaluator = PythonEvaluator()
ret = python_evaluator.eval(self.context, ret)
if not ret.status:
self.know_object_variables[var_root] = var_root
return var_root
self.know_object_variables[var_root] = None
return None
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 get_variable_from_name(self, variable_path: List[str]):
if len(variable_path) > 1:
left = variable_path[:-1]
right = [variable_path[-1]]
while left:
var_name = ".".join(left)
if var_name in self.variables:
return self.variables[var_name], ".".join(right)
right.insert(0, left.pop())
else:
return variable_path[0], ".".join(variable_path[1:])
return variable_path
def init_or_get_variable_from_name(self, variable_path: List[str], obj_variables):
var_root, var_attr = self.get_variable_from_name(variable_path)
if var_root != variable_path[0]:
return var_root, var_attr
if variable_path[0] not in self.variables:
self.add_variable(variable_path[0])
obj_variables.add(variable_path[0])
return self.variables[variable_path[0]], ".".join(variable_path[1:])
def init_or_get_variable_from_path(self, variable_path: List[str], obj_variables):
path = ".".join(variable_path)
if path in self.variables:
return self.variables[path]
def unpack_variable(self, variable_path: List[str], obj_variables):
obj_variables.add(variable_path[0])
return self.add_variable(path)
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):
# no evaluator to call, simply check that the variable is in the bag
if not expr_node.attributes and expr_node.name.startswith("__"):
return PythonConditionExprVisitorObj(None, None, {}, {expr_node.name})
# try to reconize a concept
if expr_node.attributes_str is None and not expr_node.name.startswith("__"):
# try to recognize a concept
res = evaluate(self.context,
expr_node.name,
evaluators=CONDITIONS_VISITOR_EVALUATORS,
desc=None,
eval_body=True,
eval_where=False,
is_question=False,
expect_success=False,
stm=None)
res = expect_one(self.context, res)
if res.status and isinstance(res.value, Concept):
return self.recognize_concept(["__ret", "body"], res.value, {})
source = expr_node.get_source() + " == True"
return PythonConditionExprVisitorObj(source, source, {}, {expr_node.name})
variable_name = expr_node.get_source()
return PythonConditionExprVisitorObj(None, None, {}, {variable_name}, set())
def visit_ComparisonNode(self, expr_node: ComparisonNode):
if not isinstance(expr_node.left, VariableNode):
raise FailedToCompileError([expr_node])
left = self.visit(expr_node.left)
source = expr_node.get_source()
return PythonConditionExprVisitorObj(source, source, {}, left.variables, left.not_variables)
res = evaluate(self.context,
expr_node.right.get_source(),
evaluators=CONDITIONS_VISITOR_EVALUATORS,
desc=None,
eval_body=False,
eval_where=False,
is_question=False,
expect_success=False,
stm=None)
res = expect_one(self.context, res)
if not res.status:
return FailedToCompileError([f"Cannot recognize '{expr_node.right.get_source()}'"])
return self.create_comparison_condition(expr_node.left.unpack(), expr_node.comp, res.value)
value = self.evaluate(expr_node.right.get_source(), True)
return self.create_comparison_condition(expr_node.left.unpack(), expr_node.comp, value)
def visit_AndNode(self, expr_node: AndNode):
current_visitor_obj = self.visit(expr_node.parts[0])
@@ -1456,6 +1511,34 @@ class PythonConditionExprVisitor(ExpressionVisitor):
return self.recognize_concept(expr_node.parameters[0].value.unpack(),
expr_node.parameters[1].value,
{})
else:
source = expr_node.get_source()
obj_variables = set()
for param in expr_node.parameters:
if (variable := self.get_variable(param.value)) is not None:
obj_variables.add(variable)
return PythonConditionExprVisitorObj(source, source, {}, obj_variables, set())
def visit_NotNode(self, expr_node: NotNode):
visitor_obj = self.visit(expr_node.node)
if visitor_obj.source is None:
return PythonConditionExprVisitorObj(None, None, {}, visitor_obj.not_variables, visitor_obj.variables)
return PythonConditionExprVisitorObj.combine_with_not(visitor_obj)
def visit_NameExprNode(self, expr_node: NameExprNode):
res = evaluate(self.context,
expr_node.get_source(),
evaluators=CONDITIONS_VISITOR_EVALUATORS,
desc=None,
eval_body=True,
eval_where=False,
is_question=False,
expect_success=False,
stm=None)
res = expect_one(self.context, res)
if res.status and isinstance(res.value, Concept):
return self.recognize_concept(["__ret", "body"], res.value, {})
def recognize_concept(self, variable_path, concept_to_recognize, concept_variables: dict):
if not isinstance(concept_to_recognize, Concept):
@@ -1463,47 +1546,50 @@ class PythonConditionExprVisitor(ExpressionVisitor):
if not concept_as_str:
return FailedToCompileError([f"Missing concept in for {variable_path}"])
res = evaluate(self.context,
concept_as_str,
evaluators=CONDITIONS_VISITOR_EVALUATORS,
desc=None,
eval_body=True,
eval_where=False,
is_question=False,
expect_success=False,
stm=None)
res = expect_one(self.context, res)
if not res.status:
return FailedToCompileError([f"Unknown concept {concept_as_str}"])
concept = res.body
concept = self.evaluate(concept_as_str, True)
else:
concept = concept_to_recognize
obj_variables = set()
variable = self.init_or_get_variable_from_path(variable_path, obj_variables)
var_name = self.get_new_variable(variable_path, obj_variables)
source = f"isinstance({variable}, Concept)"
source = f"isinstance({var_name}, Concept)"
if concept.get_hint(BuiltinConcepts.RECOGNIZED_BY) == RECOGNIZED_BY_NAME:
source += f" and {variable}.name == '{concept.name}'"
source += f" and {var_name}.name == '{concept.name}'"
elif concept.get_hint(BuiltinConcepts.RECOGNIZED_BY) == RECOGNIZED_BY_ID:
source += f" and {variable}.id == '{concept.id}'"
source += f" and {var_name}.id == '{concept.id}'"
else:
source += f" and {variable}.key == '{concept.key}'"
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})
return PythonConditionExprVisitorObj(source, source, {}, obj_variables)
for var_name, var_value in concept_variables.items():
new_var_path = variable_path.copy()
new_var_path.append(var_name)
variable_condition = self.create_comparison_condition(new_var_path, ComparisonType.EQUALS, var_value)
source += " and " + variable_condition.source
obj_variables.update(variable_condition.objects)
return PythonConditionExprVisitorObj(source, source, {}, obj_variables, set())
def create_comparison_condition(self, var_path, op, value):
var_root, var_attr = self.get_variable_from_name(var_path)
left = var_root + "." + var_attr
obj_variables = set()
if op == ComparisonType.EQUALS:
if isinstance(value, Expando):
source = f"isinstance({left}, Expando) and {left} == {value.get_name()}"
return PythonConditionExprVisitorObj(source, source, {}, set())
if self.context.sheerka.is_sheerka(value):
var_root, var_attr = self.unpack_variable(var_path, obj_variables)
source = f"is_sheerka({self.construct_variable(var_root, var_attr)})"
return PythonConditionExprVisitorObj(source, source, {}, obj_variables, set())
if isinstance(value, Concept):
return self.recognize_concept(var_path, value, {})
else:
source = ComparisonNode.rebuild_source(left, op, value)
return PythonConditionExprVisitorObj(source, source, {}, {var_path[0]})
if isinstance(value, str):
value = "'" + value + "'"
var_root, var_attr = self.unpack_variable(var_path, obj_variables)
source = ComparisonNode.rebuild_source(self.construct_variable(var_root, var_attr), op, value)
return PythonConditionExprVisitorObj(source, source, {}, obj_variables, set())
else:
if isinstance(value, str):
value = "'" + value + "'"
var_root, var_attr = self.unpack_variable(var_path, obj_variables)
source = ComparisonNode.rebuild_source(self.construct_variable(var_root, var_attr), op, value)
return PythonConditionExprVisitorObj(source, source, {}, obj_variables, set())