Fixed #48 : RelationalExpressionParser: Implement relational operator parser

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