From 6cda2686fb05d25e5cab0dba1dc2597c3a7b2fa5 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Tue, 23 Mar 2021 11:35:10 +0100 Subject: [PATCH] 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 --- _concepts_default.txt | 11 +- src/core/global_symbols.py | 10 + src/core/rule.py | 5 +- src/core/sheerka/Sheerka.py | 11 + .../sheerka/services/SheerkaConceptManager.py | 57 +- .../sheerka/services/SheerkaEvaluateRules.py | 41 +- src/core/sheerka/services/SheerkaExecute.py | 4 +- .../sheerka/services/SheerkaHasAManager.py | 4 +- .../sheerka/services/SheerkaIsAManager.py | 4 +- .../sheerka/services/SheerkaRuleManager.py | 584 +++++---- src/evaluators/DefRuleEvaluator.py | 2 +- src/evaluators/PythonEvaluator.py | 1 + src/parsers/BaseExpressionParser.py | 9 +- src/parsers/BaseNodeParser.py | 10 - src/parsers/DefRuleParser.py | 16 +- src/parsers/SyaNodeParser.py | 6 +- src/sheerkarete/network.py | 45 +- tests/core/test_SheerkaConceptManager.py | 46 +- tests/core/test_SheerkaEvaluateRules.py | 9 +- tests/core/test_SheerkaRuleManager.py | 1087 ++++++++--------- tests/evaluators/test_DefRuleEvaluator.py | 2 +- tests/parsers/parsers_utils.py | 35 +- tests/parsers/test_DefRuleParser.py | 36 +- tests/parsers/test_ExpressionParser.py | 5 +- tests/sheerkarete/test_network.py | 21 +- 25 files changed, 1083 insertions(+), 978 deletions(-) diff --git a/_concepts_default.txt b/_concepts_default.txt index fabeebb..5bc717f 100644 --- a/_concepts_default.txt +++ b/_concepts_default.txt @@ -7,9 +7,9 @@ set_auto_eval(c:q:) def concept "x is a concept" as isinstance(x, Concept) pre is_question() # is a -def concept x is a y as set_isa(x, y) +def concept x is a y as set_isa(x, y) ret x set_auto_eval(c:x is a y:) -def concept x is an y as set_isa(x, y) +def concept x is an y as set_isa(x, y) ret x set_auto_eval(c:x is an y:) def concept x is a y as isa(x,y) pre is_question() # no need to auto eval as it's a question @@ -18,9 +18,9 @@ def concept x is an y as isa(x,y) pre is_question() # has a -def concept x has a y as set_hasa(x, y) +def concept x has a y as set_hasa(x, y) ret x set_auto_eval(c:x has a y:) -def concept x has an y as set_hasa(x, y) +def concept x has an y as set_hasa(x, y) ret x set_auto_eval(c:x has an y:) def concept x has a y as hasa(x,y) pre is_question() # no need to auto eval as it's a question @@ -39,6 +39,8 @@ set_is_greater_than(__PRECEDENCE, c:x and y:, c:x or y:, 'Sya') set_is_less_than(__PRECEDENCE, c:q:, c:x or y:, 'Sya') def concept the x ret memory(x) +def concept a x where 'x is a concept' ret x +def concept an x where 'x is a concept' ret x # default def concept male @@ -54,6 +56,7 @@ def concept boy def concept boys def concept girl def concept girls +def concept shirt # days of the week def concept monday diff --git a/src/core/global_symbols.py b/src/core/global_symbols.py index 5485ca4..d05d8dc 100644 --- a/src/core/global_symbols.py +++ b/src/core/global_symbols.py @@ -1,4 +1,5 @@ # events +from enum import Enum EVENT_CONCEPT_PRECEDENCE_MODIFIED = "evt_cp_m" EVENT_RULE_PRECEDENCE_MODIFIED = "evt_rp_m" @@ -69,3 +70,12 @@ class ErrorObj: CURRENT_OBJ = "__obj" + + +class SyaAssociativity(Enum): + Left = "left" + Right = "right" + No = "No" + + def __repr__(self): + return self.value diff --git a/src/core/rule.py b/src/core/rule.py index 90409ae..2dd953c 100644 --- a/src/core/rule.py +++ b/src/core/rule.py @@ -31,8 +31,7 @@ class Rule: rule_id=None, is_enabled=None): self.metadata = RuleMetadata(action_type, name, predicate, action, id=rule_id, is_enabled=is_enabled) - self.compiled_predicates = None # old version (to remove when possible) - self.compiled_conditions = None # new version + self.compiled_conditions = None self.compiled_action = None from core.sheerka.services.SheerkaComparisonManager import SheerkaComparisonManager self.priority = priority if priority is not None else SheerkaComparisonManager.DEFAULT_COMPARISON_VALUE @@ -78,7 +77,7 @@ class Rule: self.id, self.metadata.is_enabled) - copy.compiled_predicates = self.compiled_predicates + copy.compiled_conditions = self.compiled_conditions copy.compiled_action = self.compiled_action copy.metadata.is_compiled = self.metadata.is_compiled copy.metadata.id_is_unresolved = self.metadata.id_is_unresolved diff --git a/src/core/sheerka/Sheerka.py b/src/core/sheerka/Sheerka.py index 1c6d047..fa24db1 100644 --- a/src/core/sheerka/Sheerka.py +++ b/src/core/sheerka/Sheerka.py @@ -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): """ diff --git a/src/core/sheerka/services/SheerkaConceptManager.py b/src/core/sheerka/services/SheerkaConceptManager.py index 2e0732e..057deb4 100644 --- a/src/core/sheerka/services/SheerkaConceptManager.py +++ b/src/core/sheerka/services/SheerkaConceptManager.py @@ -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' : {} of metadata to update, - # 'props' : {} of properties to add/update, - # 'variables': {} of variables to add/update, + # 'meta' : {: } of metadata to update, + # 'props' : {: } of properties to add/update, + # 'variables': {: } of variables to add/update, # } # if the already exists, the entry is updated, otherwise a new value is created # for props, if the already exists, a new entry is added to the set # # to_remove = { - # 'props' : {} entries to remove. 'value' can be a list or a single entry + # 'props' : {: [value]} entries to remove. 'value' can be a list or a single entry # 'variables': [] 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(): diff --git a/src/core/sheerka/services/SheerkaEvaluateRules.py b/src/core/sheerka/services/SheerkaEvaluateRules.py index 465b72e..773cde7 100644 --- a/src/core/sheerka/services/SheerkaEvaluateRules.py +++ b/src/core/sheerka/services/SheerkaEvaluateRules.py @@ -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)) diff --git a/src/core/sheerka/services/SheerkaExecute.py b/src/core/sheerka/services/SheerkaExecute.py index 2f181a1..28550e1 100644 --- a/src/core/sheerka/services/SheerkaExecute.py +++ b/src/core/sheerka/services/SheerkaExecute.py @@ -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) diff --git a/src/core/sheerka/services/SheerkaHasAManager.py b/src/core/sheerka/services/SheerkaHasAManager.py index 5da6b8c..16d29e9 100644 --- a/src/core/sheerka/services/SheerkaHasAManager.py +++ b/src/core/sheerka/services/SheerkaHasAManager.py @@ -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): diff --git a/src/core/sheerka/services/SheerkaIsAManager.py b/src/core/sheerka/services/SheerkaIsAManager.py index 8bbd0cd..54350a6 100644 --- a/src/core/sheerka/services/SheerkaIsAManager.py +++ b/src/core/sheerka/services/SheerkaIsAManager.py @@ -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 diff --git a/src/core/sheerka/services/SheerkaRuleManager.py b/src/core/sheerka/services/SheerkaRuleManager.py index 5fb7a0e..250bf22 100644 --- a/src/core/sheerka/services/SheerkaRuleManager.py +++ b/src/core/sheerka/services/SheerkaRuleManager.py @@ -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()) diff --git a/src/evaluators/DefRuleEvaluator.py b/src/evaluators/DefRuleEvaluator.py index 8e6a9cd..1a804ff 100644 --- a/src/evaluators/DefRuleEvaluator.py +++ b/src/evaluators/DefRuleEvaluator.py @@ -45,7 +45,7 @@ class DefRuleEvaluator(OneReturnValueEvaluator): compiled_action = rule_definition.then rule = Rule(action_type, name, predicate, action) - rule.compiled_predicates = rule_definition.when + rule.compiled_conditions = rule_definition.python rule.rete_disjunctions = rule_definition.rete rule.compiled_action = compiled_action diff --git a/src/evaluators/PythonEvaluator.py b/src/evaluators/PythonEvaluator.py index 6515caf..acf640f 100644 --- a/src/evaluators/PythonEvaluator.py +++ b/src/evaluators/PythonEvaluator.py @@ -207,6 +207,7 @@ class PythonEvaluator(OneReturnValueEvaluator): "Expando": Expando, "ExecutionContext": ExecutionContext, "in_context": context.in_context, + "SyaAssociativity": core.global_symbols.SyaAssociativity } for name in names: diff --git a/src/parsers/BaseExpressionParser.py b/src/parsers/BaseExpressionParser.py index e211db4..44373d2 100644 --- a/src/parsers/BaseExpressionParser.py +++ b/src/parsers/BaseExpressionParser.py @@ -313,9 +313,6 @@ class ComparisonNode(ExprNode): @staticmethod def rebuild_source(left, op, right): - if isinstance(right, str): - right = f"'{right}'" - if op == ComparisonType.EQUALS: return f"{left} == {right}" @@ -600,6 +597,12 @@ class IsAQuestionVisitor(ExpressionVisitor): return True return None + def visit_FunctionNode(self, expr_node: FunctionNode): + if tokens_are_matching(expr_node.tokens, is_question_tokens) or \ + tokens_are_matching(expr_node.tokens, eval_question_requested_in_context): + return True + return None + def visit_AndNode(self, expr_node): """ AND | True | False | None diff --git a/src/parsers/BaseNodeParser.py b/src/parsers/BaseNodeParser.py index a12cb56..3443ea8 100644 --- a/src/parsers/BaseNodeParser.py +++ b/src/parsers/BaseNodeParser.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from enum import Enum import core.utils from core.tokenizer import TokenKind, Token @@ -452,15 +451,6 @@ class NoMatchingTokenError(ParsingError): pos: int -class SyaAssociativity(Enum): - Left = "left" - Right = "right" - No = "No" - - def __repr__(self): - return self.value - - class BaseNodeParser(BaseParserInputParser): """ Parser that return LexerNode diff --git a/src/parsers/DefRuleParser.py b/src/parsers/DefRuleParser.py index c85ae36..365547b 100644 --- a/src/parsers/DefRuleParser.py +++ b/src/parsers/DefRuleParser.py @@ -6,20 +6,20 @@ from core.builtin_concepts import ReturnValueConcept from core.builtin_concepts_ids import BuiltinConcepts from core.global_symbols import NotInit from core.sheerka.services.SheerkaExecute import ParserInput -from core.sheerka.services.SheerkaRuleManager import SheerkaRuleManager, FormatAstNode, RuleCompiledPredicate +from core.sheerka.services.SheerkaRuleManager import SheerkaRuleManager, FormatAstNode, CompiledCondition from core.sheerka.services.sheerka_service import FailedToCompileError from core.tokenizer import Keywords, TokenKind from parsers.BaseCustomGrammarParser import BaseCustomGrammarParser, NameNode, KeywordNotFound, SyntaxErrorNode from parsers.BaseParser import Node, UnexpectedEofParsingError -from sheerkarete.conditions import Condition +from sheerkarete.conditions import AndConditions @dataclass() class DefRuleNode(Node): tokens: dict name: NameNode = NotInit - when: List[RuleCompiledPredicate] = NotInit - rete: List[List[Condition]] = NotInit + python: List[CompiledCondition] = NotInit + rete: List[AndConditions] = NotInit @dataclass() @@ -143,8 +143,8 @@ class DefRuleParser(BaseCustomGrammarParser): compiled_result = self.get_when(parts[Keywords.WHEN]) if compiled_result is None: return node - node.when = compiled_result.compiled_predicates - node.rete = compiled_result.rete_disjunctions + node.python = compiled_result.python_conditions + node.rete = compiled_result.rete_conditions parsed = self.get_then(parts[Keywords.THEN]) if parsed is None: @@ -162,8 +162,8 @@ class DefRuleParser(BaseCustomGrammarParser): compiled_result = self.get_when(parts[Keywords.WHEN]) if compiled_result is None: return node - node.when = compiled_result.compiled_predicates - node.rete = compiled_result.rete_disjunctions + node.python = compiled_result.python_conditions + node.rete = compiled_result.rete_conditions parsed = self.get_print(parts[Keywords.PRINT]) if parsed is None: diff --git a/src/parsers/SyaNodeParser.py b/src/parsers/SyaNodeParser.py index d29d150..08f5489 100644 --- a/src/parsers/SyaNodeParser.py +++ b/src/parsers/SyaNodeParser.py @@ -6,12 +6,12 @@ from typing import List from core import builtin_helpers from core.builtin_concepts import BuiltinConcepts from core.concept import Concept, DEFINITION_TYPE_BNF -from core.global_symbols import CONCEPT_COMPARISON_CONTEXT +from core.global_symbols import CONCEPT_COMPARISON_CONTEXT, SyaAssociativity from core.sheerka.services.SheerkaComparisonManager import SheerkaComparisonManager from core.sheerka.services.SheerkaExecute import ParserInput from core.tokenizer import Token, TokenKind, Tokenizer from core.utils import get_n_clones, get_text_from_tokens, NextIdManager -from parsers.BaseNodeParser import UnrecognizedTokensNode, ConceptNode, SourceCodeNode, SyaAssociativity, \ +from parsers.BaseNodeParser import UnrecognizedTokensNode, ConceptNode, SourceCodeNode, \ SourceCodeWithConceptNode, BaseNodeParser, VariableNode from parsers.BaseParser import ParsingError @@ -40,7 +40,7 @@ class DebugInfo: """ pos: int = -1 # position of the parser input token: Token = None # current token - concept: Concept = None # current concept if ay + concept: Concept = None # current concept if any action: str = None # action taken level: str = None diff --git a/src/sheerkarete/network.py b/src/sheerkarete/network.py index 2bb39fe..583b8c6 100644 --- a/src/sheerkarete/network.py +++ b/src/sheerkarete/network.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from itertools import product from typing import TYPE_CHECKING, Generator, Union @@ -30,6 +31,27 @@ if TYPE_CHECKING: # pragma: no cover from typing import Hashable FACT_ID = "##fact_id##" +FACT_NAME = "__name__" +FACT_SHEERKA = "__sheerka__" +FACT_IS_CONCEPT = "__is_concept__" +FACT_SELF = "__self__" + + +@dataclass +class FactObj: + """ + Wrapper for primitive objects + """ + value: object + + def __eq__(self, other): + if not isinstance(other, FactObj): + return False + + return self.value == other.value + + def __hash__(self): + return hash(self.value) class ReteNetwork: @@ -235,7 +257,7 @@ class ReteNetwork: if isinstance(cond, Condition): # Manage list of requested attributes when using __name__ indirection - if isinstance(cond.identifier, V) and cond.attribute == "__name__": + if isinstance(cond.identifier, V) and cond.attribute == FACT_NAME: vars_ids_mappings[cond.identifier] = cond.value # Manage list of requested attributes when bounding a new variable @@ -396,7 +418,11 @@ class ReteNetwork: raise ValueError("Object already has an id, cannot add") fact_id = f"f-{self.fact_counter:05}" - setattr(obj, FACT_ID, fact_id) + try: + setattr(obj, FACT_ID, fact_id) + except AttributeError: + obj = FactObj(obj) + self.facts[fact_id] = obj self.fact_counter += 1 @@ -409,16 +435,21 @@ class ReteNetwork: bag = as_bag(obj) for k, v in bag.items(): inner_add_vme(name, fact_id, k, v) - elif attribute == "__name__": - self.add_wme(WME(fact_id, "__name__", name)) - elif attribute == "__is_concept__": - self.add_wme(WME(fact_id, "__is_concept__", isinstance(obj, Concept))) + elif attribute == FACT_NAME: + self.add_wme(WME(fact_id, FACT_NAME, name)) + elif attribute == FACT_IS_CONCEPT: + self.add_wme(WME(fact_id, FACT_IS_CONCEPT, isinstance(obj, Concept))) + elif attribute == FACT_SELF: + if isinstance(obj, FactObj): + self.add_wme(WME(fact_id, FACT_SELF, obj.value)) + else: + self.add_wme(WME(fact_id, FACT_SELF, obj)) else: try: value = getattr(obj, attribute) if (isinstance(value, Concept) and value.key == BuiltinConcepts.SHEERKA or isinstance(value, Expando) and value.get_name() == "sheerka"): - value = "__sheerka__" + value = FACT_SHEERKA if is_primitive(value): self.add_wme(WME(fact_id, attribute, value)) else: diff --git a/tests/core/test_SheerkaConceptManager.py b/tests/core/test_SheerkaConceptManager.py index 850afcd..6bbcdc2 100644 --- a/tests/core/test_SheerkaConceptManager.py +++ b/tests/core/test_SheerkaConceptManager.py @@ -5,7 +5,7 @@ from core.builtin_concepts import BuiltinConcepts from core.builtin_helpers import ensure_bnf from core.concept import PROPERTIES_TO_SERIALIZE, Concept, DEFINITION_TYPE_DEF, get_concept_attrs, \ DEFINITION_TYPE_BNF -from core.global_symbols import NotInit, NotFound +from core.global_symbols import NotInit, NotFound, SyaAssociativity from core.sheerka.services.SheerkaConceptManager import SheerkaConceptManager, NoModificationFound, ForbiddenAttribute, \ UnknownAttribute, CannotRemoveMeta, ValueNotFound, ConceptIsReferenced, NoFirstTokenError from parsers.BnfNodeParser import Sequence, StrMatch, ConceptExpression, OrderedChoice, Optional, ZeroOrMore, OneOrMore, \ @@ -320,13 +320,13 @@ class TestSheerkaConceptManager(TestUsingMemoryBasedSheerka): def test_i_can_modify_add_a_property(self): sheerka, context, one, foo = self.init_concepts("one", Concept("foo", props={BuiltinConcepts.ISA: {"value"}})) - res = sheerka.modify_concept(context, foo, to_add={"props": {BuiltinConcepts.ISA: "value2", + res = sheerka.modify_concept(context, foo, to_add={"props": {BuiltinConcepts.ISA: {"value2"}, BuiltinConcepts.HASA: one}}) assert res.status assert sheerka.isinstance(res.body, BuiltinConcepts.NEW_CONCEPT) - assert res.body.body.get_prop(BuiltinConcepts.ISA) == {"value", "value2"} - assert res.body.body.get_prop(BuiltinConcepts.HASA) == {sheerka.new("one")} + assert res.body.body.get_prop(BuiltinConcepts.ISA) == {"value2"} + assert res.body.body.get_prop(BuiltinConcepts.HASA) == sheerka.new("one") def test_i_can_modify_remove_a_property(self): sheerka, context, foo = self.init_concepts( @@ -382,7 +382,7 @@ class TestSheerkaConceptManager(TestUsingMemoryBasedSheerka): to_add = {"meta": {"body": "metadata value"}, "variables": {"var_name": "default value"}, - "props": {BuiltinConcepts.ISA: bar}} + "props": {BuiltinConcepts.ISA: {bar}}} res = sheerka.modify_concept(context, foo, to_add) new_concept = res.body.body @@ -656,7 +656,7 @@ class TestSheerkaConceptManager(TestUsingMemoryBasedSheerka): to_add = { "meta": {"body": "a body"}, - "props": {BuiltinConcepts.ISA: "bar"}, + "props": {BuiltinConcepts.ISA: {"bar"}}, "variables": {"c": "value"} } to_remove = { @@ -826,6 +826,40 @@ class TestSheerkaConceptManager(TestUsingMemoryBasedSheerka): assert sheerka.isinstance(res.body, BuiltinConcepts.SUCCESS) assert sheerka.get_attr(foo, prop) == [bar, baz, qux] + def test_i_can_get_and_set_property(self): + sheerka, context, foo, prop, bar = self.init_concepts("foo", "property", "bar") + + foo_instance = sheerka.new(foo) + res = sheerka.set_property(context, foo_instance, prop, bar) + assert res.status + assert sheerka.isinstance(res.body, BuiltinConcepts.SUCCESS) + assert sheerka.get_property(foo_instance, prop) == bar + + res = sheerka.set_property(context, foo_instance, BuiltinConcepts.ASSOCIATIVITY, SyaAssociativity.Left) + assert res.status + assert sheerka.isinstance(res.body, BuiltinConcepts.SUCCESS) + assert sheerka.get_property(foo_instance, BuiltinConcepts.ASSOCIATIVITY) == SyaAssociativity.Left + + res = sheerka.set_property(context, foo_instance, sheerka.new(BuiltinConcepts.ASSOCIATIVITY), SyaAssociativity.Right) + assert res.status + assert sheerka.isinstance(res.body, BuiltinConcepts.SUCCESS) + assert sheerka.get_property(foo_instance, BuiltinConcepts.ASSOCIATIVITY) == SyaAssociativity.Right + + # by default, the concept itself is not modified + new_foo = sheerka.new(foo) + not_found = sheerka.get_property(new_foo, prop) + assert sheerka.isinstance(not_found, BuiltinConcepts.NOT_FOUND) + assert not_found.body == {"#concept": new_foo, "#prop": prop} + + # I can modify the concept itself + another_foo_instance = sheerka.new(foo) + res = sheerka.set_property(context, another_foo_instance, prop, bar, all_concepts=True) + assert res.status + assert sheerka.get_property(another_foo_instance, prop) == bar + + new_foo = sheerka.new(foo) + assert sheerka.get_property(new_foo, prop) == bar + def test_i_cannot_remove_a_concept_which_has_reference(self): sheerka, context, one, twenty_one = self.init_test().with_concepts( Concept("one"), diff --git a/tests/core/test_SheerkaEvaluateRules.py b/tests/core/test_SheerkaEvaluateRules.py index 21ca0b6..820bc18 100644 --- a/tests/core/test_SheerkaEvaluateRules.py +++ b/tests/core/test_SheerkaEvaluateRules.py @@ -8,7 +8,7 @@ from core.rule import Rule, ACTION_TYPE_EXEC from core.sheerka.Sheerka import RECOGNIZED_BY_ID, RECOGNIZED_BY_NAME from core.sheerka.services.SheerkaEvaluateRules import SheerkaEvaluateRules, LOW_PRIORITY_RULES, DISABLED_RULES from core.sheerka.services.SheerkaExecute import ParserInput -from core.sheerka.services.SheerkaRuleManager import RuleCompiledPredicate, SheerkaRuleManager +from core.sheerka.services.SheerkaRuleManager import SheerkaRuleManager, CompiledCondition from evaluators.PythonEvaluator import PythonEvaluator, Expando from parsers.PythonParser import PythonParser from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka @@ -39,6 +39,7 @@ class TestSheerkaEvaluateRules(TestUsingMemoryBasedSheerka): DISABLED_RULES: [r7] } + @pytest.mark.skip("Not ready for that") def test_i_can_evaluate_question_concept_rules(self): sheerka, context, concept, r1, r2, r3, r4, r5, r6, r7, r8, r9 = self.init_test().with_concepts( Concept("x equals y", body="x == y", pre="is_question()").def_var("x").def_var("y"), @@ -82,8 +83,8 @@ class TestSheerkaEvaluateRules(TestUsingMemoryBasedSheerka): # create fake compiled predicates parser = PythonParser() - my_rule.compiled_predicates = [ - RuleCompiledPredicate("my rule", None, PythonEvaluator.NAME, parser.parse(context, ParserInput(exp)), None) + my_rule.compiled_conditions = [ + CompiledCondition(PythonEvaluator.NAME, parser.parse(context, ParserInput(exp)), set(), set(), None) for exp in predicates] my_rule.metadata.is_compiled = True my_rule.metadata.is_enabled = True @@ -94,6 +95,7 @@ class TestSheerkaEvaluateRules(TestUsingMemoryBasedSheerka): True: [my_rule], } + @pytest.mark.skip("Not ready for that") def test_i_can_evaluate_rules_when_concepts_are_questions(self): sheerka, context, isa, cat, crocodile, pet, r1, r2, r3 = self.init_test().with_concepts( Concept("x is a y", body="isa(x,y)", pre="is_question()").def_var("x").def_var("y"), @@ -188,6 +190,7 @@ class TestSheerkaEvaluateRules(TestUsingMemoryBasedSheerka): res = service.evaluate_rules(context, [rule], {"__ret": ret}, set()) assert res == {True: [rule]} + @pytest.mark.skip("Not ready for that") def test_i_can_evaluate_concept_rules_when_same_name(self): sheerka, context, g1, g2, rule = self.init_test().with_concepts( Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), diff --git a/tests/core/test_SheerkaRuleManager.py b/tests/core/test_SheerkaRuleManager.py index dc30eae..ea7911d 100644 --- a/tests/core/test_SheerkaRuleManager.py +++ b/tests/core/test_SheerkaRuleManager.py @@ -3,7 +3,7 @@ import ast import pytest from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept -from core.concept import Concept, DEFINITION_TYPE_DEF, DoNotResolve +from core.concept import Concept, DEFINITION_TYPE_DEF from core.global_symbols import RULE_COMPARISON_CONTEXT, NotFound, EVENT_RULE_DELETED from core.rule import Rule, ACTION_TYPE_PRINT, ACTION_TYPE_EXEC from core.sheerka.Sheerka import RECOGNIZED_BY_ID, RECOGNIZED_BY_NAME @@ -11,22 +11,22 @@ from core.sheerka.services.SheerkaEvaluateRules import SheerkaEvaluateRules from core.sheerka.services.SheerkaExecute import ParserInput from core.sheerka.services.SheerkaRuleManager import SheerkaRuleManager, FormatRuleActionParser, \ FormatAstRawText, FormatAstVariable, FormatAstSequence, FormatAstFunction, \ - FormatRuleSyntaxError, FormatAstList, UnexpectedEof, FormatAstColor, RuleCompiledPredicate, FormatAstDict, \ + FormatRuleSyntaxError, FormatAstList, UnexpectedEof, FormatAstColor, FormatAstDict, \ FormatAstMulti, \ - PythonCodeEmitter, NoConditionFound, FormatAstNode, ReteConditionExprVisitor, PythonConditionExprVisitor, \ + PythonCodeEmitter, FormatAstNode, ReteConditionExprVisitor, PythonConditionExprVisitor, \ CompiledCondition -from core.sheerka.services.sheerka_service import FailedToCompileError from core.tokenizer import Token, TokenKind -from parsers.BaseNodeParser import SourceCodeWithConceptNode, SourceCodeNode +from evaluators.PythonEvaluator import Expando from parsers.BaseParser import ErrorSink from parsers.ExpressionParser import ExpressionParser from parsers.PythonParser import PythonNode from sheerkarete.common import V -from sheerkarete.conditions import Condition, AndConditions +from sheerkarete.conditions import Condition, AndConditions, FilterCondition from sheerkarete.network import ReteNetwork from tests.TestUsingFileBasedSheerka import TestUsingFileBasedSheerka from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka -from tests.parsers.parsers_utils import CMV, CC, compare_with_test_object, get_test_obj, get_rete_conditions +from tests.parsers.parsers_utils import get_rete_conditions, NEGCOND, \ + NCCOND seq = FormatAstSequence raw = FormatAstRawText @@ -143,7 +143,7 @@ class TestSheerkaRuleManager(TestUsingMemoryBasedSheerka): assert rule.metadata.is_compiled assert rule.metadata.is_enabled - assert len(rule.compiled_predicates) == 1 + assert len(rule.compiled_conditions) == 1 assert len(rule.rete_disjunctions) == 1 assert sheerka.isinstance(rule.compiled_action, BuiltinConcepts.RETURN_VALUE) assert rule.compiled_action.status @@ -158,7 +158,7 @@ class TestSheerkaRuleManager(TestUsingMemoryBasedSheerka): assert rule.metadata.is_compiled assert rule.metadata.is_enabled - assert len(rule.compiled_predicates) == 1 + assert len(rule.compiled_conditions) == 1 assert len(rule.rete_disjunctions) == 1 assert isinstance(rule.compiled_action, FormatAstNode) assert rule.error_sink is None @@ -183,7 +183,7 @@ class TestSheerkaRuleManager(TestUsingMemoryBasedSheerka): sheerka, context = self.init_test(cache_only=False).unpack() service = sheerka.services[SheerkaRuleManager.NAME] - rule = Rule(ACTION_TYPE_EXEC, "name", "cannot build", "'Hello back at you !'") + rule = Rule(ACTION_TYPE_EXEC, "name", "cannot build = False", "'Hello back at you !'") rule.metadata.is_enabled = True # it should be disabled rule = service.init_rule(context, rule) @@ -193,7 +193,7 @@ class TestSheerkaRuleManager(TestUsingMemoryBasedSheerka): assert "then" not in rule.error_sink assert rule.metadata.is_compiled assert not rule.metadata.is_enabled - assert rule.compiled_predicates is None + assert rule.compiled_conditions is None assert rule.rete_disjunctions is None @pytest.mark.parametrize("action_type, action", [ @@ -282,518 +282,30 @@ class TestSheerkaRuleManager(TestUsingMemoryBasedSheerka): assert parser.error_sink == expected_error - @pytest.mark.parametrize("text", [ - "__action == 'some_action' and True", - "__action == 'some_action' and not a question", - "__action == 'some_action' and is a question", + @pytest.mark.parametrize("text, compiled_text", [ + ("a == 5", "a == 5"), + ("foo > 5", "foo > 5"), + ("func() == 5", "func() == 5"), + ("not a == 5", "not (a == 5)"), + ("not foo > 5", "not (foo > 5)"), + ("not func() == 5", "not (func() == 5)"), ]) - def test_i_can_compile_predicate_when_action_is_provided(self, text): - sheerka, context, *concepts = self.init_test().with_concepts( - "a question", - Concept("is a question", pre='is_question()'), - create_new=True).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].action == 'some_action' - - def test_i_can_compile___action_is_not_part_of_the_predicates(self): + def test_i_can_compile_predicate_when_pure_python(self, text, compiled_text): sheerka, context, *concepts = self.init_concepts("foo") service = sheerka.services[SheerkaRuleManager.NAME] - ast_ = ast.parse("a == 5", "", 'eval') - expected_python_node = PythonNode('a == 5', ast_) - - compiled_result = service.compile_when(context, "test", "__action == 'some action' and a == 5") - res = compiled_result.compiled_predicates - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert sheerka.objvalue(res[0].predicate) == expected_python_node - assert res[0].concept is None - - @pytest.mark.parametrize("text", [ - "a == 5", - "foo > 5", - "func() == 5", - "not a == 5", - "not foo > 5", - "not func() == 5", - ]) - def test_i_can_compile_predicate_when_pure_python(self, text): - sheerka, context, *concepts = self.init_concepts("foo") - service = sheerka.services[SheerkaRuleManager.NAME] - ast_ = ast.parse(text, "", 'eval') - expected_python_node = PythonNode(text, ast_) - - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert sheerka.objvalue(res[0].predicate) == expected_python_node - assert res[0].concept is None - - @pytest.mark.parametrize("text, expected_type", [ - ("isinstance(a, int)", SourceCodeWithConceptNode), - ("func()", SourceCodeNode), - ("not isinstance(a, int)", PythonNode), - ("not func()", PythonNode), - ]) - def test_i_can_compile_predicates_that_resolve_to_python(self, text, expected_type): - sheerka, context, *concepts = self.init_concepts() - service = sheerka.services[SheerkaRuleManager.NAME] - ast_ = ast.parse(text, "", 'eval') - expected_python_node = PythonNode(text, ast_) - - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert isinstance(sheerka.objvalue(res[0].predicate), expected_type) - assert sheerka.objvalue(res[0].predicate).get_python_node() == expected_python_node - assert res[0].concept is None - - @pytest.mark.parametrize("text, expected_variables", [ - ("cat is an animal", ["cat", "animal"]), - ("a is an animal", ["a", "animal"]), - ("cat is an b", ["cat", "b"]), - ]) - def test_i_can_compile_predicate_when_exact_concept(self, text, expected_variables): - sheerka, context, *concepts = self.init_test().with_concepts( - Concept("x is an y", pre="is_question()", body="isinstance(x, y)").def_var("x").def_var("y"), - Concept("cat"), - Concept("animal"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - expected = concepts[0] - expected.get_metadata().variables = [('x', expected_variables[0]), ('y', expected_variables[1])] - - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == CONCEPT_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert sheerka.objvalue(res[0].predicate) == expected - assert res[0].concept == expected - - @pytest.mark.parametrize("text, text_to_compile, expected_variables", [ - ("not cat is an animal", "not __C__00var0000is0an000var001__1001__C__", ["cat", "animal"]), - ("not a is an animal", "not __C__00var0000is0an000var001__1001__C__", ["a", "animal"]), - ("not cat is an b", "not __C__00var0000is0an000var001__1001__C__", ["cat", "b"]), - ]) - def test_i_can_compile_negative_predicate_when_exact_concept(self, text, text_to_compile, expected_variables): - sheerka, context, *concepts = self.init_test().with_concepts( - Concept("x is an y", pre="is_question()", body="isinstance(x, y)").def_var("x").def_var("y"), - Concept("cat"), - Concept("animal"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - expected_concept = concepts[0] - expected_concept.get_metadata().variables = [('x', expected_variables[0]), ('y', expected_variables[1])] - ast_ = ast.parse(text_to_compile, "", 'eval') - expected_python_node = PythonNode(text_to_compile, ast_) - expected_python_node.original_source = text - expected_python_node.objects = {"__C__00var0000is0an000var001__1001__C__": expected_concept} - res = service.compile_when(context, "test", text) - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert isinstance(sheerka.objvalue(res[0].predicate), PythonNode) - - python_node = sheerka.objvalue(res[0].predicate).get_python_node() - assert python_node == expected_python_node - assert len(python_node.objects) == 1 - assert python_node.objects["__C__00var0000is0an000var001__1001__C__"] == expected_concept - assert res[0].concept is None - - @pytest.mark.skip("Not managed yet") - @pytest.mark.parametrize("text, text_to_compile, expected_variables", [ - ("not cat is an animal", "__C__00var0000is0an000var001__1001__C__", ["not cat", "animal"]) - ]) - def test_i_can_compile_negative_predicate_when_exact_concept(self, text, text_to_compile, expected_variables): - sheerka, context, *concepts = self.init_test().with_concepts( - Concept("x is an y", pre="is_question()", body="isinstance(x, y)").def_var("x").def_var("y"), - Concept("cat"), - Concept("animal"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - expected_concept = concepts[0] - expected_concept.get_metadata().variables = [('x', expected_variables[0]), ('y', expected_variables[1])] - ast_ = ast.parse(text_to_compile, "", 'eval') - expected_python_node = PythonNode(text_to_compile, ast_) - expected_python_node.original_source = text - expected_python_node.objects = {"__C__00var0000is0an000var001__1001__C__": expected_concept} - res = service.compile_when(context, "test", text) - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert isinstance(sheerka.objvalue(res[0].predicate), PythonNode) - - python_node = sheerka.objvalue(res[0].predicate).get_python_node() - assert python_node == expected_python_node - assert len(python_node.objects) == 1 - assert python_node.objects["__C__00var0000is0an000var001__1001__C__"] == expected_concept - assert res[0].concept is None - - @pytest.mark.parametrize("text, text_to_compile", [ - ("foo bar == 5", "__C__foo0bar__1001__C__ == 5"), - ("not foo bar == 5", "not __C__foo0bar__1001__C__ == 5"), - ]) - def test_i_can_compile_predicate_when_python_and_concept(self, text, text_to_compile): - sheerka, context, *concepts = self.init_test().with_concepts(Concept("foo bar"), create_new=True).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - ast_ = ast.parse(text_to_compile, "", 'eval') - resolved_expected = PythonNode(text_to_compile, ast_, text) - - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - - python_node = sheerka.objvalue(res[0].predicate).get_python_node() - assert python_node == resolved_expected - assert python_node.objects == {'__C__foo0bar__1001__C__': concepts[0]} - assert res[0].concept is None - - @pytest.mark.parametrize("text, expected_variables", [ - ("a cat is an animal", ["a cat", "animal"]), - ("a cat is an b", ["a cat", "b"]), - ]) - def test_i_can_compile_predicate_when_sya_node_parser(self, text, expected_variables): - sheerka, context, *concepts = self.init_test().with_concepts( - Concept("x is an y", pre="is_question()", body="isinstance(x, y)").def_var("x").def_var("y"), - Concept("a cat"), - Concept("animal"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - expected = CMV(concepts[0], x=expected_variables[0], y=expected_variables[1]) - - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == CONCEPT_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - compare_with_test_object(sheerka.objvalue(res[0].predicate)[0].concept, expected) - compare_with_test_object(res[0].concept, expected) - - def test_i_can_compile_predicate_when_bnf_node_parser(self): - sheerka, context, *concepts = self.init_test().with_concepts( - Concept("animal"), - Concept("x is an y", pre="is_question()", definition="('cat'|'bird')=x 'is an' animal").def_var("x"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - expected = concepts[1] - - compiled_result = service.compile_when(context, "test", "cat is an animal") - res = compiled_result.compiled_predicates - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == CONCEPT_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert sheerka.objvalue(res[0].predicate)[0].concept == expected - assert res[0].concept == expected - - def test_i_can_compile_predicate_when_mix_of_concepts_and_python(self): - sheerka, context, animal, cat, dog, pet, is_a, is_an = self.init_test().with_concepts( - Concept("animal"), - Concept("a cat"), - Concept("dog"), - Concept("pet"), - Concept("x is a y", pre="is_question()", body="isinstance(x, y)").def_var("x").def_var("y"), - Concept("x is an y", pre="is_question()", definition="('cat'|'bird')=x 'is an' animal").def_var("x"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - text = "a cat is a pet and bird is an animal and x > 5 and dog is a pet" - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - - to_compile = '__C__00var0000is0a000var001__1005__C__ and __C__00var0000is0an0y__1006__C__ and x > 5 and __C__00var0000is0a000var001__1005_1__C__' - ast_ = ast.parse(to_compile, "", 'eval') - expected_python_node = PythonNode(to_compile, ast_, text) - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - python_node = res[0].predicate.body.body - assert python_node == expected_python_node - expected = { - "__C__00var0000is0a000var001__1005__C__": CC(is_a, x=cat, y=pet), - "__C__00var0000is0an0y__1006__C__": CC(is_an, exclude_body=True, x=DoNotResolve("bird"), animal=animal), - "__C__00var0000is0a000var001__1005_1__C__": CMV(is_a, x="dog", y="pet"), - } - transformed = get_test_obj(python_node.objects, expected) - assert transformed == expected - - @pytest.mark.parametrize("text", [ - "a and not b", - "not b and a", - "__ret and not __ret.status", - ]) - def test_i_can_compile_negative_conjunctions_when_pure_python(self, text): - sheerka, context, *concepts = self.init_concepts("foo") - service = sheerka.services[SheerkaRuleManager.NAME] - - ast_ = ast.parse(text, "", 'eval') - expected_python_node = PythonNode(text, ast_) - - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert sheerka.objvalue(res[0].predicate) == expected_python_node - assert res[0].concept is None - - def test_i_can_compile_negative_conjunction_of_mix_of_concepts_and_python(self): - sheerka, context, animal, cat, dog, pet, is_a, is_an = self.init_test().with_concepts( - Concept("animal"), - Concept("a cat"), - Concept("dog"), - Concept("pet"), - Concept("x is a y", pre="is_question()", body="isinstance(x, y)").def_var("x").def_var("y"), - Concept("x is an y", pre="is_question()", definition="('cat'|'bird')=x 'is an' animal").def_var("x"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - text = "not a cat is a pet and not bird is an animal and not x > 5 and not dog is a pet" - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - - to_compile = 'not __C__00var0000is0a000var001__1005__C__' - to_compile += ' and not __C__00var0000is0an0y__1006__C__' - to_compile += ' and not x > 5' - to_compile += ' and not __C__00var0000is0a000var001__1005_1__C__' - ast_ = ast.parse(to_compile, "", 'eval') - expected_python_node = PythonNode(to_compile, ast_, text) - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - python_node = res[0].predicate.body.body - assert python_node == expected_python_node - compare_with_test_object(python_node.objects, { - "__C__00var0000is0a000var001__1005__C__": CC(is_a, x=cat, y=pet), - "__C__00var0000is0an0y__1006__C__": CC(is_an, exclude_body=True, x=DoNotResolve("bird"), animal=animal), - "__C__00var0000is0a000var001__1005_1__C__": CMV(is_a, x="dog", y="pet"), - }) - - def test_i_can_compile_predicate_when_multiple_choices(self): - sheerka, context, *concepts = self.init_test().with_concepts( - Concept("x is a y", pre="is_question()", body="isinstance(x, y)").def_var("x").def_var("y"), - Concept("x is a y", pre="is_question()", body="isa(x, y)").def_var("x").def_var("y"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - compiled_result = service.compile_when(context, "test", "a is a b") - res = compiled_result.compiled_predicates - - assert len(res) == 2 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == CONCEPT_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - compare_with_test_object(sheerka.objvalue(res[0].predicate)[0].concept, CMV(concepts[0], x="a", y="b")) - compare_with_test_object(res[0].concept, CMV(concepts[0], x="a", y="b")) - - assert isinstance(res[1], RuleCompiledPredicate) - assert res[1].evaluator == CONCEPT_EVALUATOR_NAME - assert sheerka.isinstance(res[1].predicate, BuiltinConcepts.RETURN_VALUE) - compare_with_test_object(sheerka.objvalue(res[1].predicate)[0].concept, CMV(concepts[1], x="a", y="b")) - compare_with_test_object(res[1].concept, CMV(concepts[1], x="a", y="b")) - - def test_i_can_compile_predicate_when_mix_and_multiple_choices(self): - sheerka, context, *concepts = self.init_test().with_concepts( - Concept("animal"), - Concept("a cat"), - Concept("dog"), - Concept("pet"), - Concept("x is a y", pre="is_question()", body="isinstance(x, y)").def_var("x").def_var("y"), - Concept("x is a y", pre="is_question()", body="isa(x, y)").def_var("x").def_var("y"), - Concept("x is an y", pre="is_question()", definition="('cat'|'bird')=x 'is an' animal").def_var("x"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - text = "__action == 'value' and a cat is a pet and bird is an animal and x > 5 and dog is a pet" - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - - assert len(res) == 4 - trimmed_source = "a cat is a pet and bird is an animal and x > 5 and dog is a pet" - - current_res = res[0] - assert isinstance(current_res, RuleCompiledPredicate) - assert current_res.evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(current_res.predicate, BuiltinConcepts.RETURN_VALUE) - python_source = "__C__00var0000is0a000var001__1005__C__ and __C__00var0000is0an0y__1007__C__ and x > 5 and __C__00var0000is0a000var001__1005_1__C__" - ast_ = ast.parse(python_source, "", 'eval') - resolved_expected = PythonNode(python_source, ast_, trimmed_source) - assert sheerka.objvalue(current_res.predicate) == resolved_expected - assert current_res.concept is None - - current_res = res[1] - assert isinstance(current_res, RuleCompiledPredicate) - assert current_res.evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(current_res.predicate, BuiltinConcepts.RETURN_VALUE) - python_source = "__C__00var0000is0a000var001__1005__C__ and __C__00var0000is0an0y__1007__C__ and x > 5 and __C__00var0000is0a000var001__1006__C__" - ast_ = ast.parse(python_source, "", 'eval') - resolved_expected = PythonNode(python_source, ast_, trimmed_source) - assert sheerka.objvalue(current_res.predicate) == resolved_expected - assert current_res.concept is None - - current_res = res[2] - assert isinstance(current_res, RuleCompiledPredicate) - assert current_res.evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(current_res.predicate, BuiltinConcepts.RETURN_VALUE) - python_source = "__C__00var0000is0a000var001__1006__C__ and __C__00var0000is0an0y__1007__C__ and x > 5 and __C__00var0000is0a000var001__1005__C__" - ast_ = ast.parse(python_source, "", 'eval') - resolved_expected = PythonNode(python_source, ast_, trimmed_source) - assert sheerka.objvalue(current_res.predicate) == resolved_expected - assert current_res.concept is None - - current_res = res[3] - assert isinstance(current_res, RuleCompiledPredicate) - assert current_res.evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(current_res.predicate, BuiltinConcepts.RETURN_VALUE) - python_source = "__C__00var0000is0a000var001__1006__C__ and __C__00var0000is0an0y__1007__C__ and x > 5 and __C__00var0000is0a000var001__1006_1__C__" - ast_ = ast.parse(python_source, "", 'eval') - resolved_expected = PythonNode(python_source, ast_, trimmed_source) - assert sheerka.objvalue(current_res.predicate) == resolved_expected - assert current_res.concept is None - - @pytest.mark.parametrize("text, mode, compiled_text", [ - ("greetings", "eval", f"__ret.status and isinstance(__ret.body, Concept) and __ret.body.name == 'greetings'"), - ("c:|1001:", "eval", f"__ret.status and isinstance(__ret.body, Concept) and __ret.body.id == '1001'"), - ("hello 'there'", "eval", - f"__ret.status and isinstance(__ret.body, Concept) and __ret.body.key == 'hello __var__0' and __ret.body.get_value('a') == 'there'"), - ("hello there", "exec", - f"__x_00__ = __ret.body.get_value('a')\n__ret.status and isinstance(__ret.body, Concept) and __ret.body.key == 'hello __var__0' and isinstance(__x_00__, Concept) and __x_00__.key == 'there'"), - ("hello my friend", "exec", - f"__x_00__ = __ret.body.get_value('a')\n__ret.status and isinstance(__ret.body, Concept) and __ret.body.key == 'hello __var__0' and isinstance(__x_00__, Concept) and __x_00__.key == 'my friend'"), - ]) - def test_i_can_compile_predicate_when_concept_is_not_a_question(self, text, mode, compiled_text): - sheerka, context, greetings, there, my_friend = self.init_test().with_concepts( - Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), - Concept("there"), - Concept("my friend"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - ast_ = ast.parse(compiled_text, "", mode) + ast_ = ast.parse(compiled_text, "", 'eval') expected_python_node = PythonNode(compiled_text, ast_) - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates + compilation_result = service.compile_when(context, "test", text) + res = compilation_result.python_conditions assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert sheerka.objvalue(res[0].predicate) == expected_python_node + assert isinstance(res[0], CompiledCondition) + assert res[0].evaluator_type == PYTHON_EVALUATOR_NAME + assert sheerka.isinstance(res[0].return_value, BuiltinConcepts.RETURN_VALUE) + assert sheerka.objvalue(res[0].return_value) == expected_python_node assert res[0].concept is None - def test_i_can_compile_predicate_when_concept_is_not_a_question_and_involves_sheerka(self): - sheerka, context, greetings = self.init_test().with_concepts( - Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - compiled_text = "__x_00__ = __ret.body.get_value('a')\n" - compiled_text += "__ret.status" - compiled_text += " and isinstance(__ret.body, Concept) and __ret.body.key == 'hello __var__0'" - compiled_text += " and isinstance(__x_00__, Expando) and __x_00__.get_name() == 'sheerka'" - ast_ = ast.parse(compiled_text, "", "exec") - expected_python_node = PythonNode(compiled_text, ast_) - - compiled_result = service.compile_when(context, "test", "hello sheerka") - res = compiled_result.compiled_predicates - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == PYTHON_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert sheerka.objvalue(res[0].predicate) == expected_python_node - assert res[0].concept is None - - @pytest.mark.skip("Not managed yet") - def test_i_can_compile_when_concept_starts_with_not(self): - sheerka, context, *concepts = self.init_test().with_concepts( - Concept("not a cheesecake", pre="is_question()"), - create_new=True).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - text = "not a cheesecake" - expected = concepts[0] - - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.compiled_predicates - - assert len(res) == 1 - assert isinstance(res[0], RuleCompiledPredicate) - assert res[0].evaluator == CONCEPT_EVALUATOR_NAME - assert sheerka.isinstance(res[0].predicate, BuiltinConcepts.RETURN_VALUE) - assert sheerka.objvalue(res[0].predicate) == expected - assert res[0].concept == expected - - def test_i_cannot_compile_when_concept_is_not_a_question_and_has_unknown_variable(self): - sheerka, context, greetings = self.init_test().with_concepts( - Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), - create_new=True - ).unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - with pytest.raises(FailedToCompileError) as ex: - service.compile_when(context, "test", "hello there") - - assert sheerka.isinstance(ex.value.cause[0], BuiltinConcepts.CONCEPT_EVAL_ERROR) - - @pytest.mark.parametrize("text, expected_error", [ - ("__action == 'some_action'", NoConditionFound()) - ]) - def test_i_cannot_compile_when_error(self, text, expected_error): - sheerka, context = self.init_test().unpack() - service = sheerka.services[SheerkaRuleManager.NAME] - - with pytest.raises(FailedToCompileError) as ex: - service.compile_when(context, "test", text) - - assert ex.value.cause == [expected_error] - def test_i_can_get_rule_priorities(self): sheerka, context, rule_true, rule_false = self.init_test().with_format_rules(("True", "True"), ("False", "False")).unpack() @@ -994,8 +506,8 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ text = "__ret" - compiled_result = service.compile_when(context, "test", text) - res = compiled_result.rete_disjunctions + compilation_result = service.compile_when(context, "test", text) + res = compilation_result.rete_disjunctions assert len(res) == 1 assert isinstance(res[0], AndConditions) @@ -1013,25 +525,34 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ for k, v in vars(rule).items(): assert getattr(clone, k) == getattr(rule, k) - @pytest.mark.parametrize("expression, expected_as_str", [ + @pytest.mark.parametrize("expression, expected_as_str, expected_variables", [ ( "__ret", ["#__x_00__|__name__|'__ret'"], + {"__ret"} ), ( "__ret.status == True", ["#__x_00__|__name__|'__ret'", "#__x_00__|status|True"], + {"__ret"} ), ( "__ret.status", - ["#__x_00__|__name__|'__ret'", "#__x_00__|status|True"], + ["#__x_00__|__name__|'__ret.status'"], + {"__ret.status"} + ), + ( + "body", + ["#__x_00__|__name__|'body'"], + {"body"} ), ( "__ret and __ret.status", - ["#__x_00__|__name__|'__ret'", "#__x_00__|status|True"], + ["#__x_00__|__name__|'__ret'", "#__x_01__|__name__|'__ret.status'"], + {"__ret", "__ret.status"} ), ]) - def test_i_can_get_rete_conditions(self, expression, expected_as_str): + def test_i_can_get_rete_conditions(self, expression, expected_as_str, expected_variables): sheerka, context = self.init_test().unpack() parser = ExpressionParser() expected = get_rete_conditions(*expected_as_str) @@ -1055,10 +576,82 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ rule.rete_disjunctions = conditions network.add_rule(rule) - network.add_obj("__ret", ReturnValueConcept("Test", True, None)) + return_value = ReturnValueConcept("Test", True, None) + if "__ret" in expected_variables: + network.add_obj("__ret", return_value) + if "__ret.status" in expected_variables: + network.add_obj("__ret.status", return_value.status) + if "body" in expected_variables: + network.add_obj("body", return_value.body) + matches = list(network.matches) assert len(matches) == 1 + def test_i_can_get_rete_conditions_when_no_attribute(self): + sheerka, context = self.init_test().unpack() + expression = "a == 10" + expected_as_str = ["#__x_00__|__name__|'a'", "#__x_00__|__self__|10"] + parser = ExpressionParser() + expected = get_rete_conditions(*expected_as_str) + + error_sink = ErrorSink() + parser_input = ParserInput(expression) + parser.reset_parser_input(parser_input, error_sink) + parsed = parser.parse_input(context, parser_input, error_sink) + + visitor = ReteConditionExprVisitor(context) + conditions = visitor.get_conditions(parsed) + + assert conditions == [expected] + + # check against a Rete network + network = ReteNetwork() + rule = Rule("test", expression, None) + rule.metadata.id = 9999 + rule.metadata.is_compiled = True + rule.metadata.is_enabled = True + rule.rete_disjunctions = conditions + network.add_rule(rule) + + network.add_obj("a", 10) + matches = list(network.matches) + assert len(matches) == 1 + + @pytest.mark.skip("No ready yet for SheerkaFilterCondition") + def test_i_can_get_rete_conditions_when_function(self): + sheerka, context, greetings = self.init_test().with_concepts( + Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), + ).unpack() + expression = "isinstance(a, greetings)" + expected = get_rete_conditions(["#__x_00__|__name__|'a'", + FilterCondition(lambda a: isinstance(a, greetings))]) + + parser = ExpressionParser() + error_sink = ErrorSink() + parser_input = ParserInput(expression) + parser.reset_parser_input(parser_input, error_sink) + parsed = parser.parse_input(context, parser_input, error_sink) + + visitor = ReteConditionExprVisitor(context) + conditions = visitor.get_conditions(parsed) + + assert conditions == [expected] + + # check against a Rete network + network = ReteNetwork() + rule = Rule("test", expression, None) + rule.metadata.id = 9999 + rule.metadata.is_compiled = True + rule.metadata.is_enabled = True + rule.rete_disjunctions = conditions + network.add_rule(rule) + + # variable = foo if variable_name == "foo" else sheerka if variable_name == "sheerka" else variable_name + # to_recognize = sheerka.new_from_template(greetings, greetings.key, a=variable) + # network.add_obj("__ret", ReturnValueConcept("Test", True, to_recognize)) + # matches = list(network.matches) + # assert len(matches) == 1 + @pytest.mark.parametrize("test_name, expression, variable_name, expected_as_str", [ ( "recognize by name", @@ -1152,12 +745,26 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ "#__x_02__|key|'foo'", ] ), - + ( + "recognize by instance when long concept", + "recognize(__ret.body, hello my best friend)", + "my best friend", + ["#__x_00__|__name__|'__ret'", + "#__x_00__|body|#__x_01__", + "#__x_01__|__is_concept__|True", + "#__x_01__|key|'hello __var__0'", + "#__x_01__|a|#__x_02__", + "#__x_02__|__is_concept__|True", + "#__x_02__|key|'my best friend'", + ] + ), ]) def test_i_can_get_rete_using_recognize_function(self, test_name, expression, variable_name, expected_as_str): - sheerka, context, greetings, foo = self.init_test().with_concepts( + sheerka, context, greetings, foo, my_best_friend = self.init_test().with_concepts( Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), Concept("foo"), + Concept("my best friend"), + create_new=True ).unpack() parser = ExpressionParser() expected = get_rete_conditions(*expected_as_str) @@ -1181,7 +788,12 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ rule.rete_disjunctions = conditions network.add_rule(rule) - variable = foo if variable_name == "foo" else sheerka if variable_name == "sheerka" else variable_name + variable_map = { + "foo": foo, + "my best friend": my_best_friend, + "sheerka": sheerka + } + variable = variable_map.get(variable_name, variable_name) to_recognize = sheerka.new_from_template(greetings, greetings.key, a=variable) network.add_obj("__ret", ReturnValueConcept("Test", True, to_recognize)) matches = list(network.matches) @@ -1196,6 +808,14 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ "#__x_01__|__is_concept__|True", "#__x_01__|name|'greetings'"] ), + ( + "c:|1001:", + None, + ["#__x_00__|__name__|'__ret'", + "#__x_00__|body|#__x_01__", + "#__x_01__|__is_concept__|True", + "#__x_01__|id|'1001'"] + ), ( "hello foo", "foo", @@ -1208,11 +828,25 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ "#__x_02__|key|'foo'", ] ), + ( + "hello my best friend", + "my best friend", + ["#__x_00__|__name__|'__ret'", + "#__x_00__|body|#__x_01__", + "#__x_01__|__is_concept__|True", + "#__x_01__|key|'hello __var__0'", + "#__x_01__|a|#__x_02__", + "#__x_02__|__is_concept__|True", + "#__x_02__|key|'my best friend'", + ] + ), ]) def test_i_can_get_rete_when_a_concept_is_recognized(self, expression, variable_name, expected_as_str): - sheerka, context, greetings, foo = self.init_test().with_concepts( + sheerka, context, greetings, foo, my_best_friend = self.init_test().with_concepts( Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), Concept("foo"), + Concept("my best friend"), + create_new=True ).unpack() parser = ExpressionParser() expected = get_rete_conditions(*expected_as_str) @@ -1236,12 +870,49 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ rule.rete_disjunctions = conditions network.add_rule(rule) - variable = foo if variable_name == "foo" else sheerka if variable_name == "sheerka" else variable_name + variable_map = { + "foo": foo, + "my best friend": my_best_friend, + "sheerka": sheerka + } + variable = variable_map.get(variable_name, variable_name) to_recognize = sheerka.new_from_template(greetings, greetings.key, a=variable) network.add_obj("__ret", ReturnValueConcept("Test", True, to_recognize)) matches = list(network.matches) assert len(matches) == 1 + @pytest.mark.parametrize("expression, bag_key, expected", [ + ("not __ret", "__other", [NEGCOND("#__x_00__|__name__|'__ret'")]), + ("not not __ret", "__ret", ["#__x_00__|__name__|'__ret'"]), + ("not __ret.status == True", "__ret", ["#__x_00__|__name__|'__ret'", NEGCOND("#__x_00__|status|True")]), + ("not __ret.status", "__ret", [NEGCOND("#__x_00__|__name__|'__ret.status'")]), + ("__ret and not __ret.status", "__ret", + ["#__x_00__|__name__|'__ret'", NEGCOND("#__x_01__|__name__|'__ret.status'")]), + ("not recognize(__ret.body, hello sheerka)", "__ret", ["#__x_00__|__name__|'__ret'", + NCCOND(["#__x_00__|body|#__x_01__", + "#__x_01__|__is_concept__|True", + "#__x_01__|key|'hello __var__0'", + "#__x_01__|a|'__sheerka__'"])]), + ("__ret and not __error", "__ret", ["#__x_00__|__name__|'__ret'", NEGCOND("#__x_01__|__name__|'__error'")]), + ]) + def test_i_can_get_rete_using_not(self, expression, bag_key, expected): + sheerka, context, greetings, foo = self.init_test().with_concepts( + Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), + Concept("foo"), + ).unpack() + parser = ExpressionParser() + expected_conditions = get_rete_conditions(*expected) + + error_sink = ErrorSink() + parser_input = ParserInput(expression) + parser.reset_parser_input(parser_input, error_sink) + parsed = parser.parse_input(context, parser_input, error_sink) + + visitor = ReteConditionExprVisitor(context) + conditions = visitor.get_conditions(parsed) + + assert conditions == [expected_conditions] + @pytest.mark.skip("I am not sure yet of what I want to get") @pytest.mark.parametrize("expression, expected_as_str", [ ( @@ -1290,13 +961,14 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ matches = list(network.matches) assert len(matches) == 1 - @pytest.mark.parametrize("expression, expected_compiled", [ - ("__ret", None), - ("__ret.status == True", "__ret.status == True"), - ("__ret.status", "__ret.status == True"), - ("__ret and __ret.status", "__ret.status == True") + @pytest.mark.parametrize("expression, expected_compiled, expected_variables", [ + ("__ret", None, {"__ret"}), + ("__ret.status == True", "__ret.status == True", {"__ret"}), + ("__ret.status", None, {"__ret.status"}), + ("body", None, {"body"}), + ("__ret and __ret.status", None, {"__ret", "__ret.status"}) ]) - def test_i_can_get_compiled_conditions(self, expression, expected_compiled): + def test_i_can_get_compiled_conditions(self, expression, expected_compiled, expected_variables): sheerka, context = self.init_test().unpack() parser = ExpressionParser() @@ -1320,11 +992,90 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ assert conditions[0].evaluator_type is None assert conditions[0].return_value is None assert conditions[0].concept is None - assert conditions[0].variables == {"__ret"} + assert conditions[0].variables == expected_variables # check against SheerkaEvaluateRules evaluate_rules_service = sheerka.services[SheerkaEvaluateRules.NAME] - bag = {"__ret": ReturnValueConcept("Test", True, None)} + return_value = ReturnValueConcept("Test", True, None) + bag = {} + if "__ret" in expected_variables: + bag["__ret"] = return_value + if "__ret.status" in expected_variables: + bag["__ret.status"] = return_value.status + if "body" in expected_variables: + bag["body"] = return_value.body + with context.push(BuiltinConcepts.RULES_EVALUATION, bag, desc="Evaluating rules...") as sub_context: + sub_context.sheerka.add_many_to_short_term_memory(sub_context, bag) + rule = Rule(name="test_i_can_get_compiled_conditions", predicate=expression) + rule.compiled_conditions = conditions + res = evaluate_rules_service.evaluate_rule(sub_context, rule, bag) + assert res.status + assert self.sheerka.is_success(self.sheerka.objvalue(res)) + + def test_i_can_get_compiled_conditions_when_no_attribute(self): + sheerka, context = self.init_test().unpack() + expression = "a == 10" + expected_compiled = "a == 10" + + parser = ExpressionParser() + error_sink = ErrorSink() + parser_input = ParserInput(expression) + parser.reset_parser_input(parser_input, error_sink) + parsed = parser.parse_input(context, parser_input, error_sink) + + visitor = PythonConditionExprVisitor(context) + conditions = visitor.get_conditions(parsed) + + assert len(conditions) == 1 + assert isinstance(conditions[0], CompiledCondition) + ast_ = ast.parse(expected_compiled, "", 'eval') + expected_python_node = PythonNode(expected_compiled, ast_) + assert conditions[0].evaluator_type == PYTHON_EVALUATOR_NAME + assert sheerka.isinstance(conditions[0].return_value, BuiltinConcepts.RETURN_VALUE) + assert sheerka.objvalue(conditions[0].return_value) == expected_python_node + assert conditions[0].concept is None + assert conditions[0].variables == {"a"} + + # check against SheerkaEvaluateRules + evaluate_rules_service = sheerka.services[SheerkaEvaluateRules.NAME] + bag = {"a": 10} + with context.push(BuiltinConcepts.RULES_EVALUATION, bag, desc="Evaluating rules...") as sub_context: + sub_context.sheerka.add_many_to_short_term_memory(sub_context, bag) + rule = Rule(name="test_i_can_get_compiled_conditions", predicate=expression) + rule.compiled_conditions = conditions + res = evaluate_rules_service.evaluate_rule(sub_context, rule, bag) + assert res.status + assert self.sheerka.is_success(self.sheerka.objvalue(res)) + + def test_i_can_get_compiled_conditions_when_function(self): + sheerka, context, greetings = self.init_test().with_concepts( + Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), + ).unpack() + expression = "isinstance(a, greetings)" + expected_compiled = "isinstance(a, greetings)" + + parser = ExpressionParser() + error_sink = ErrorSink() + parser_input = ParserInput(expression) + parser.reset_parser_input(parser_input, error_sink) + parsed = parser.parse_input(context, parser_input, error_sink) + + visitor = PythonConditionExprVisitor(context) + conditions = visitor.get_conditions(parsed) + + assert len(conditions) == 1 + assert isinstance(conditions[0], CompiledCondition) + ast_ = ast.parse(expected_compiled, "", 'eval') + expected_python_node = PythonNode(expected_compiled, ast_) + assert conditions[0].evaluator_type == PYTHON_EVALUATOR_NAME + assert sheerka.isinstance(conditions[0].return_value, BuiltinConcepts.RETURN_VALUE) + assert sheerka.objvalue(conditions[0].return_value) == expected_python_node + assert conditions[0].concept is None + assert conditions[0].variables == {"a"} + + # check against SheerkaEvaluateRules + evaluate_rules_service = sheerka.services[SheerkaEvaluateRules.NAME] + bag = {"a": sheerka.new(greetings, a="my friend")} with context.push(BuiltinConcepts.RULES_EVALUATION, bag, desc="Evaluating rules...") as sub_context: sub_context.sheerka.add_many_to_short_term_memory(sub_context, bag) rule = Rule(name="test_i_can_get_compiled_conditions", predicate=expression) @@ -1334,80 +1085,73 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ assert self.sheerka.is_success(self.sheerka.objvalue(res)) @pytest.mark.parametrize("expression, variable_name, expected_compiled", [ - # ( - # "recognize(__ret.body, greetings)", - # None, - # "__x_00__ = __ret.body\nisinstance(__x_00__, Concept) and __x_00__.name == 'greetings'" - # ), - # ( - # "recognize(__ret.body, c:|1001:)", - # None, - # "__x_00__ = __ret.body\nisinstance(__x_00__, Concept) and __x_00__.id == '1001'" - # ), - # ( - # "recognize(__ret.body, c:greetings:)", - # None, - # "__x_00__ = __ret.body\nisinstance(__x_00__, Concept) and __x_00__.name == 'greetings'" - # ), - # ( - # "recognize(__ret.body, greetings) and __ret.body.a == 'my friend'", - # "my friend", - # "__x_00__ = __ret.body\nisinstance(__x_00__, Concept) and __x_00__.name == 'greetings' and __x_00__.a == 'my friend'" - # ), + ( + "recognize(__ret.body, greetings)", + None, + "__x_00__ = __ret.body\nisinstance(__x_00__, Concept) and __x_00__.name == 'greetings'" + ), + ( + "recognize(__ret.body, c:|1001:)", + None, + "__x_00__ = __ret.body\nisinstance(__x_00__, Concept) and __x_00__.id == '1001'" + ), + ( + "recognize(__ret.body, c:greetings:)", + None, + "__x_00__ = __ret.body\nisinstance(__x_00__, Concept) and __x_00__.name == 'greetings'" + ), + ( + "recognize(__ret.body, greetings) and __ret.body.a == 'my friend'", + "my friend", + "__x_00__ = __ret.body\nisinstance(__x_00__, Concept) and __x_00__.name == 'greetings' and __x_00__.a == 'my friend'" + ), ( "recognize(__ret.body, greetings) and __ret.body.a == sheerka", "sheerka", """__x_00__ = __ret.body -__x_01__ = __x_00__.a -isinstance(__x_00__, Concept) and __x_00__.name == 'greetings' and isinstance(__x_01__, Expando) and __x_01__.name == 'sheerka'""" +isinstance(__x_00__, Concept) and __x_00__.name == 'greetings' and is_sheerka(__x_00__.a)""" + ), + ( + "recognize(__ret.body, greetings) and __ret.body.a == foo", + "foo", + """__x_00__ = __ret.body +__x_01__ = __x_00__.a +isinstance(__x_00__, Concept) and __x_00__.name == 'greetings' and isinstance(__x_01__, Concept) and __x_01__.key == 'foo'""" + ), + ( + "recognize(__ret.body, hello sheerka)", + "sheerka", + """__x_00__ = __ret.body +isinstance(__x_00__, Concept) and __x_00__.key == 'hello __var__0' and is_sheerka(__x_00__.a)""" + ), + ( + "recognize(__ret.body, hello 'my friend')", + "my friend", + """__x_00__ = __ret.body +isinstance(__x_00__, Concept) and __x_00__.key == 'hello __var__0' and __x_00__.a == 'my friend'""" + ), + ( + "recognize(__ret.body, hello foo)", + "foo", + """__x_00__ = __ret.body +__x_01__ = __x_00__.a +isinstance(__x_00__, Concept) and __x_00__.key == 'hello __var__0' and isinstance(__x_01__, Concept) and __x_01__.key == 'foo'""" + ), + ( + "recognize(__ret.body, hello my best friend)", + "my best friend", + """__x_00__ = __ret.body +__x_01__ = __x_00__.a +isinstance(__x_00__, Concept) and __x_00__.key == 'hello __var__0' and isinstance(__x_01__, Concept) and __x_01__.key == 'my best friend'""" ), - # ( - # "recognize(__ret.body, greetings) and __ret.body.a == foo", - # "foo", - # ["#__x_00__|__name__|'__ret'", - # "#__x_00__|body|#__x_01__", - # "#__x_01__|__is_concept__|True", - # "#__x_01__|name|'greetings'", - # "#__x_01__|a|#__x_02__", - # "#__x_02__|__is_concept__|True", - # "#__x_02__|key|'foo'"] - # ), - # ( - # "recognize(__ret.body, hello sheerka)", - # "sheerka", - # ["#__x_00__|__name__|'__ret'", - # "#__x_00__|body|#__x_01__", - # "#__x_01__|__is_concept__|True", - # "#__x_01__|key|'hello __var__0'", - # "#__x_01__|a|'__sheerka__'"] - # ), - # ( - # "recognize(__ret.body, hello 'my friend')", - # "my friend", - # ["#__x_00__|__name__|'__ret'", - # "#__x_00__|body|#__x_01__", - # "#__x_01__|__is_concept__|True", - # "#__x_01__|key|'hello __var__0'", - # "#__x_01__|a|'my friend'"] - # ), - # ( - # "recognize(__ret.body, hello foo)", - # "foo", - # ["#__x_00__|__name__|'__ret'", - # "#__x_00__|body|#__x_01__", - # "#__x_01__|__is_concept__|True", - # "#__x_01__|key|'hello __var__0'", - # "#__x_01__|a|#__x_02__", - # "#__x_02__|__is_concept__|True", - # "#__x_02__|key|'foo'", - # ] - # ), ]) def test_i_can_get_compiled_using_recognize_function(self, expression, variable_name, expected_compiled): - sheerka, context, greetings, foo = self.init_test().with_concepts( + sheerka, context, greetings, foo, my_best_friend = self.init_test().with_concepts( Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), Concept("foo"), + Concept("my best friend"), + create_new=True ).unpack() parser = ExpressionParser() @@ -1435,7 +1179,12 @@ isinstance(__x_00__, Concept) and __x_00__.name == 'greetings' and isinstance(__ # check against SheerkaEvaluateRules evaluate_rules_service = sheerka.services[SheerkaEvaluateRules.NAME] - variable = foo if variable_name == "foo" else sheerka if variable_name == "sheerka" else variable_name + variable_map = { + "foo": foo, + "my best friend": my_best_friend, + "sheerka": Expando("sheerka", {}) + } + variable = variable_map.get(variable_name, variable_name) to_recognize = sheerka.new_from_template(greetings, greetings.key, a=variable) bag = {"__ret": ReturnValueConcept("Test", True, to_recognize)} with context.push(BuiltinConcepts.RULES_EVALUATION, bag, desc="Evaluating rules...") as sub_context: @@ -1446,15 +1195,147 @@ isinstance(__x_00__, Concept) and __x_00__.name == 'greetings' and isinstance(__ assert res.status assert self.sheerka.is_success(self.sheerka.objvalue(res)) + @pytest.mark.parametrize("expression, variable_name, expected_compiled", [ + ( + "greetings", + None, + "__x_00__ = __ret.body\nisinstance(__x_00__, Concept) and __x_00__.name == 'greetings'" + ), + ( + "c:|1001:", + None, + "__x_00__ = __ret.body\nisinstance(__x_00__, Concept) and __x_00__.id == '1001'" + ), + ( + "hello foo", + "foo", + """__x_00__ = __ret.body +__x_01__ = __x_00__.a +isinstance(__x_00__, Concept) and __x_00__.key == 'hello __var__0' and isinstance(__x_01__, Concept) and __x_01__.key == 'foo'""" + ), + ( + "hello my best friend", + "my best friend", + """__x_00__ = __ret.body +__x_01__ = __x_00__.a +isinstance(__x_00__, Concept) and __x_00__.key == 'hello __var__0' and isinstance(__x_01__, Concept) and __x_01__.key == 'my best friend'""" + ), + ]) + def test_i_can_get_compiled_when_a_concept_is_recognized(self, expression, variable_name, expected_compiled): + sheerka, context, greetings, foo, my_best_friend = self.init_test().with_concepts( + Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), + Concept("foo"), + Concept("my best friend"), + create_new=True + ).unpack() + + parser = ExpressionParser() + error_sink = ErrorSink() + parser_input = ParserInput(expression) + parser.reset_parser_input(parser_input, error_sink) + parsed = parser.parse_input(context, parser_input, error_sink) + + visitor = PythonConditionExprVisitor(context) + conditions = visitor.get_conditions(parsed) + + assert len(conditions) == 1 + assert isinstance(conditions[0], CompiledCondition) + if expected_compiled: + ast_ = ast.parse(expected_compiled, "", 'exec') + expected_python_node = PythonNode(expected_compiled, ast_, expected_compiled) + assert conditions[0].evaluator_type == PYTHON_EVALUATOR_NAME + assert sheerka.isinstance(conditions[0].return_value, BuiltinConcepts.RETURN_VALUE) + assert sheerka.objvalue(conditions[0].return_value) == expected_python_node + else: + assert conditions[0].evaluator_type is None + assert conditions[0].return_value is None + assert conditions[0].concept is None + assert conditions[0].variables == {"__ret"} + + # check against SheerkaEvaluateRules + evaluate_rules_service = sheerka.services[SheerkaEvaluateRules.NAME] + variable_map = { + "foo": foo, + "my best friend": my_best_friend, + "sheerka": Expando("sheerka", {}) + } + variable = variable_map.get(variable_name, variable_name) + to_recognize = sheerka.new_from_template(greetings, greetings.key, a=variable) + bag = {"__ret": ReturnValueConcept("Test", True, to_recognize)} + with context.push(BuiltinConcepts.RULES_EVALUATION, bag, desc="Evaluating rules...") as sub_context: + sub_context.sheerka.add_many_to_short_term_memory(sub_context, bag) + rule = Rule(name="test_i_can_get_compiled_using_recognize_function", predicate=expression) + rule.compiled_conditions = conditions + res = evaluate_rules_service.evaluate_rule(sub_context, rule, bag) + assert res.status + assert self.sheerka.is_success(self.sheerka.objvalue(res)) + + @pytest.mark.parametrize("expression, expected_compiled, variables, not_variables", [ + ("not __ret", None, set(), {"__ret"}), + ("not not __ret", None, {"__ret"}, set()), + ("not __ret.status == True", "not (__ret.status == True)", {"__ret"}, set()), + ("not __ret.status", None, set(), {"__ret.status"},), + ("__ret and not __ret.status", None, {"__ret"}, {"__ret.status"}), + ("not recognize(__ret.body, hello sheerka)", """__x_00__ = __ret.body +not (isinstance(__x_00__, Concept) and __x_00__.key == 'hello __var__0' and is_sheerka(__x_00__.a))""", {"__ret"}, + set()), + ("__ret and not __error", None, {"__ret"}, {"__error"}), + ]) + def test_i_can_get_compiled_condition_using_not(self, expression, expected_compiled, variables, not_variables): + sheerka, context, greetings, foo = self.init_test().with_concepts( + Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a"), + Concept("foo"), + ).unpack() + + parser = ExpressionParser() + error_sink = ErrorSink() + parser_input = ParserInput(expression) + parser.reset_parser_input(parser_input, error_sink) + parsed = parser.parse_input(context, parser_input, error_sink) + + visitor = PythonConditionExprVisitor(context) + conditions = visitor.get_conditions(parsed) + + assert len(conditions) == 1 + assert isinstance(conditions[0], CompiledCondition) + if expected_compiled: + if "\n" in expected_compiled: + ast_ = ast.parse(expected_compiled, "", 'exec') + else: + ast_ = ast.parse(expected_compiled, "", 'eval') + expected_python_node = PythonNode(expected_compiled, ast_) + assert conditions[0].evaluator_type == PYTHON_EVALUATOR_NAME + assert sheerka.isinstance(conditions[0].return_value, BuiltinConcepts.RETURN_VALUE) + assert sheerka.objvalue(conditions[0].return_value) == expected_python_node + + else: + assert conditions[0].evaluator_type is None + assert conditions[0].return_value is None + assert conditions[0].variables == variables + assert conditions[0].not_variables == not_variables + assert conditions[0].concept is None + + # check against SheerkaEvaluateRules + evaluate_rules_service = sheerka.services[SheerkaEvaluateRules.NAME] + ret_val_key = "__ret" if "__ret" in conditions[0].variables else "__other" + bag = {ret_val_key: ReturnValueConcept("Test", False, None)} + with context.push(BuiltinConcepts.RULES_EVALUATION, bag, desc="Evaluating rules...") as sub_context: + sub_context.sheerka.add_many_to_short_term_memory(sub_context, bag) + rule = Rule(name="test_i_can_get_compiled_conditions", predicate=expression) + rule.compiled_conditions = conditions + res = evaluate_rules_service.evaluate_rule(sub_context, rule, bag) + assert res.status + assert self.sheerka.is_success(self.sheerka.objvalue(res)) + class TestSheerkaRuleManagerUsingFileBasedSheerka(TestUsingFileBasedSheerka): def test_rules_are_initialized_at_startup(self): sheerka, context, *rules = self.init_test().with_rules( None, Rule("print", "name1", "True", "Hello world"), - Rule("print", "name2", "value() is __EXPLANATION", "list(value())"), + Rule("print", "name2", "__rets", "list(rets)"), Rule("exec", "name3", "True", "'Hello world'"), - Rule("exec", "name4", "value() is __EXPLANATION", "list(value())"), + Rule("exec", "name4", "__ret.body", "list(__ret.body)"), ).unpack() sheerka.set_is_greater_than(context, BuiltinConcepts.PRECEDENCE, rules[0], @@ -1486,7 +1367,7 @@ class TestSheerkaRuleManagerUsingFileBasedSheerka(TestUsingFileBasedSheerka): assert rule.metadata.is_compiled assert rule.metadata.is_enabled assert rule.compiled_action == expected.compiled_action - assert rule.compiled_predicates == expected.compiled_predicates + assert rule.compiled_conditions == expected.compiled_conditions assert rule.priority is not None assert rule.priority == expected.priority @@ -1519,6 +1400,6 @@ class TestSheerkaRuleManagerUsingFileBasedSheerka(TestUsingFileBasedSheerka): assert rule.metadata.is_compiled == expected.metadata.is_compiled assert rule.metadata.is_enabled == expected.metadata.is_enabled assert rule.compiled_action == expected.compiled_action - assert rule.compiled_predicates == expected.compiled_predicates + assert rule.compiled_conditions == expected.compiled_conditions assert rule.priority is not None assert rule.priority == expected.priority diff --git a/tests/evaluators/test_DefRuleEvaluator.py b/tests/evaluators/test_DefRuleEvaluator.py index 4f7e29c..a7e0e1d 100644 --- a/tests/evaluators/test_DefRuleEvaluator.py +++ b/tests/evaluators/test_DefRuleEvaluator.py @@ -55,6 +55,6 @@ class TestDefRuleEvaluator(TestUsingMemoryBasedSheerka): is_compiled=True, is_enabled=True) rule = res.body.body - assert rule.compiled_predicates is not None + assert rule.compiled_conditions is not None assert rule.compiled_action is not None assert rule.rete_disjunctions is not None diff --git a/tests/parsers/parsers_utils.py b/tests/parsers/parsers_utils.py index 2341508..b0ecad7 100644 --- a/tests/parsers/parsers_utils.py +++ b/tests/parsers/parsers_utils.py @@ -1,7 +1,6 @@ import ast -import re from dataclasses import dataclass -from typing import Union +from typing import Union, List from core.builtin_concepts import ReturnValueConcept from core.builtin_helpers import CreateObjectIdentifiers @@ -18,7 +17,7 @@ from parsers.FunctionParser import FunctionNode from parsers.PythonParser import PythonNode from parsers.SyaNodeParser import SyaConceptParserHelper from sheerkarete.common import V -from sheerkarete.conditions import Condition, AndConditions +from sheerkarete.conditions import Condition, AndConditions, NegatedCondition, NegatedConjunctiveConditions @dataclass @@ -933,6 +932,16 @@ class FN: raise NotImplementedError(f"FN, {other=}") +@dataclass() +class NEGCOND: + condition: str + + +@dataclass() +class NCCOND: + conditions: List[str] + + comparison_type_mapping = { "EQ": ComparisonType.EQUALS, "NEQ": ComparisonType.NOT_EQUAlS, @@ -1295,10 +1304,10 @@ def resolve_test_concept(concept_map, hint): raise NotImplementedError() -def get_rete_conditions(*conditions_as_string): +def get_rete_conditions(*conditions): """ Transform a list of string into a list of Condition (Rete conditions) - :param conditions_as_string: conditions in the form 'identifier|attribute|value' + :param conditions: conditions in the form 'identifier|attribute|value' when one argument starts with "#" it means that it's a variables ex : "#__x_00__|__name__|'__ret'" -> Condition(V('#__x_00__'), '__name__', '__ret') @@ -1317,13 +1326,17 @@ def get_rete_conditions(*conditions_as_string): return int(obj) res = [] - for as_string in conditions_as_string: - if as_string.startswith("$"): - fn_match = re.match(r"(?P\w+)\s?\((?P.+)\)", as_string[1:]) - as_dict = fn_match.groupdict() - pass + for cond in conditions: + if isinstance(cond, Condition): + res.append(cond) + elif isinstance(cond, NEGCOND): + inner_cond = get_rete_conditions(cond.condition).conditions[0] + res.append(NegatedCondition(inner_cond.identifier, inner_cond.attribute, inner_cond.value)) + elif isinstance(cond, NCCOND): + inner_conds = get_rete_conditions(*cond.conditions).conditions + res.append(NegatedConjunctiveConditions(*inner_conds)) else: - parts = as_string.split("|") + parts = cond.split("|") identifier = get_value(parts[0]) attribute = parts[1] value = get_value(parts[2]) diff --git a/tests/parsers/test_DefRuleParser.py b/tests/parsers/test_DefRuleParser.py index 3718237..087e6f1 100644 --- a/tests/parsers/test_DefRuleParser.py +++ b/tests/parsers/test_DefRuleParser.py @@ -3,7 +3,7 @@ import pytest from core.builtin_concepts_ids import BuiltinConcepts from core.concept import Concept from core.sheerka.services.SheerkaExecute import ParserInput -from core.sheerka.services.SheerkaRuleManager import RuleCompiledPredicate, FormatAstNode +from core.sheerka.services.SheerkaRuleManager import FormatAstNode, CompiledCondition from core.tokenizer import Tokenizer, Keywords from core.utils import tokens_are_matching from parsers.BaseCustomGrammarParser import KeywordNotFound, NameNode, SyntaxErrorNode @@ -80,9 +80,9 @@ class TestDefRuleParser(TestUsingMemoryBasedSheerka): assert len(parsed.tokens) == 2 assert tokens_are_matching(parsed.tokens[Keywords.WHEN], Tokenizer("when True")) assert tokens_are_matching(parsed.tokens[Keywords.THEN], Tokenizer("then answer('that is true')")) - assert isinstance(parsed.when, list) - assert len(parsed.when) == 1 - assert isinstance(parsed.when[0], RuleCompiledPredicate) + assert isinstance(parsed.python, list) + assert len(parsed.python) == 1 + assert isinstance(parsed.python[0], CompiledCondition) assert sheerka.isinstance(parsed.then, BuiltinConcepts.RETURN_VALUE) def test_i_can_parse_simple_format_rule_definition(self): @@ -100,9 +100,9 @@ class TestDefRuleParser(TestUsingMemoryBasedSheerka): assert len(parsed.tokens) == 2 assert tokens_are_matching(parsed.tokens[Keywords.WHEN], Tokenizer("when True")) assert tokens_are_matching(parsed.tokens[Keywords.PRINT], Tokenizer("print hello world!")) - assert isinstance(parsed.when, list) - assert len(parsed.when) == 1 - assert isinstance(parsed.when[0], RuleCompiledPredicate) + assert isinstance(parsed.python, list) + assert len(parsed.python) == 1 + assert isinstance(parsed.python[0], CompiledCondition) assert isinstance(parsed.print, FormatAstNode) def test_i_can_parse_exec_rule_with_name(self): @@ -121,36 +121,38 @@ class TestDefRuleParser(TestUsingMemoryBasedSheerka): assert len(parsed.tokens) == 2 assert tokens_are_matching(parsed.tokens[Keywords.WHEN], Tokenizer("when True")) assert tokens_are_matching(parsed.tokens[Keywords.THEN], Tokenizer("then answer('that is true')")) - assert isinstance(parsed.when, list) - assert len(parsed.when) == 1 - assert isinstance(parsed.when[0], RuleCompiledPredicate) + assert isinstance(parsed.python, list) + assert len(parsed.python) == 1 + assert isinstance(parsed.python[0], CompiledCondition) assert sheerka.isinstance(parsed.then, BuiltinConcepts.RETURN_VALUE) + @pytest.mark.skip("Not ready for that") def test_when_is_parsed_in_the_context_of_a_question(self): sheerka, context, parser = self.init_parser() text = "when foo is a bar print hello world" res = parser.parse(context, ParserInput(text)) format_rule = res.body.body - rules = format_rule.when + rules = format_rule.python assert res.status assert len(rules) == 1 - assert isinstance(rules[0], RuleCompiledPredicate) - assert rules[0].predicate.body.body.get_metadata().pre == "is_question()" + assert isinstance(rules[0], CompiledCondition) + assert rules[0].return_value.body.body.get_metadata().pre == "is_question()" + @pytest.mark.skip("Not ready for that") def test_when_can_support_multiple_possibilities_when_question_only(self): sheerka, context, parser = self.init_parser() text = "when foo is good print hello world" res = parser.parse(context, ParserInput(text)) format_rule = res.body.body - rules = format_rule.when + rules = format_rule.python assert res.status assert len(rules) == 2 - assert rules[0].predicate.body.body.get_metadata().name == "a is good" - assert rules[1].predicate.body.body.get_metadata().name == "b is good" + assert rules[0].return_value.body.body.get_metadata().name == "a is good" + assert rules[1].return_value.body.body.get_metadata().name == "b is good" @pytest.mark.parametrize("text, error", [ ("def", [KeywordNotFound(None, keywords=['rule'])]), @@ -175,7 +177,7 @@ class TestDefRuleParser(TestUsingMemoryBasedSheerka): assert not_for_me.reason == error @pytest.mark.parametrize("text, expected_error", [ - ("when x x print 'hello world'", BuiltinConcepts.TOO_MANY_ERRORS), + ("when x x = False print 'hello world'", BuiltinConcepts.ERROR), ]) def test_i_can_detect_errors(self, text, expected_error): diff --git a/tests/parsers/test_ExpressionParser.py b/tests/parsers/test_ExpressionParser.py index 41ca5b8..2dbc669 100644 --- a/tests/parsers/test_ExpressionParser.py +++ b/tests/parsers/test_ExpressionParser.py @@ -95,10 +95,13 @@ class TestExpressionParser(TestUsingMemoryBasedSheerka): @pytest.mark.parametrize("expression, expected", [ ("ret.status in ('a', 1 , func())", "new_var in ('a', 1 , func())"), ("ret.status not in ('a', 1 , func())", "new_var not in ('a', 1 , func())"), + ("ret.status == 10", "new_var == 10"), + ("ret.status == 'a'", "new_var == 'a'"), ]) def test_i_can_rebuild_source(self, expression, expected): sheerka, context, parser, parser_input, error_sink = self.init_parser_with_source(expression) parsed = parser.parse_input(context, parser_input, error_sink) - assert ComparisonNode.rebuild_source("new_var", parsed.comp, parsed.right.get_source()) == expected + new_source = ComparisonNode.rebuild_source("new_var", parsed.comp, parsed.right.get_source()) + assert new_source == expected diff --git a/tests/sheerkarete/test_network.py b/tests/sheerkarete/test_network.py index c41eb65..12b6f22 100644 --- a/tests/sheerkarete/test_network.py +++ b/tests/sheerkarete/test_network.py @@ -10,7 +10,7 @@ from sheerkarete.common import V, WME, ReteToken from sheerkarete.conditions import Condition, NegatedCondition, AndConditions from sheerkarete.join_node import JoinNode from sheerkarete.negative_node import NegativeNode -from sheerkarete.network import ReteNetwork, FACT_ID +from sheerkarete.network import ReteNetwork, FACT_ID, FACT_NAME, FACT_SELF, FactObj from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka from tests.sheerkarete.RuleForTestingRete import RuleForTestingRete @@ -247,6 +247,25 @@ class TestReteNetwork(TestUsingMemoryBasedSheerka): assert matches[0].pnode.rules == [rule] assert network.facts == {'f-00000': ret} + def test_i_can_add_primitive(self): + network = ReteNetwork() + rule = RuleForTestingRete(AndConditions([Condition(V("a"), FACT_NAME, "a"), + Condition(V("a"), FACT_SELF, 10), + ])) + network.add_rule(rule) + network.add_obj("a", 10) + + assert network.working_memory == { + WME("f-00000", "__name__", "a"), + WME("f-00000", "__self__", 10), + } + + # sanity check that the WME produced match the condition + matches = list(network.matches) + assert len(matches) == 1 + assert matches[0].pnode.rules == [rule] + assert network.facts == {'f-00000': FactObj(10)} + def test_i_can_distinguish_objects_with_different_value(self): network = ReteNetwork() rule = RuleForTestingRete(AndConditions([