Added chicken and egg recursion detection
This commit is contained in:
@@ -51,6 +51,7 @@ class BuiltinConcepts(Enum):
|
||||
REDUCE_REQUESTED = "reduce requested" # remove meaningless error when possible
|
||||
NOT_A_SET = "not a set" # the concept has no entry in sets
|
||||
WHERE_CLAUSE_FAILED = "where clause failed" # failed to validate where clause during evaluation
|
||||
CHICKEN_AND_EGG = "chicken and egg" # infinite recursion when declaring concept
|
||||
|
||||
NODE = "node"
|
||||
GENERIC_NODE = "generic node"
|
||||
@@ -92,7 +93,8 @@ BuiltinErrors = [str(e) for e in {
|
||||
BuiltinConcepts.CONCEPT_EVAL_ERROR,
|
||||
BuiltinConcepts.CONCEPT_ALREADY_IN_SET,
|
||||
BuiltinConcepts.NOT_A_SET,
|
||||
BuiltinConcepts.WHERE_CLAUSE_FAILED
|
||||
BuiltinConcepts.WHERE_CLAUSE_FAILED,
|
||||
BuiltinConcepts.CHICKEN_AND_EGG
|
||||
}]
|
||||
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,6 @@ import logging
|
||||
import core.ast.nodes
|
||||
from core.ast.nodes import CallNodeConcept, GenericNodeConcept
|
||||
from core.ast.visitors import UnreferencedNamesVisitor
|
||||
|
||||
from core.builtin_concepts import BuiltinConcepts
|
||||
|
||||
|
||||
@@ -100,11 +99,22 @@ def expect_one(context, return_values, logger=None):
|
||||
return_values[0],
|
||||
parents=return_values)
|
||||
else:
|
||||
return sheerka.ret(
|
||||
context.who,
|
||||
False,
|
||||
sheerka.new(BuiltinConcepts.TOO_MANY_ERRORS, body=return_values),
|
||||
parents=return_values)
|
||||
# test if only one evaluator in error
|
||||
from evaluators.OneErrorEvaluator import OneErrorEvaluator
|
||||
one_error_evaluator = OneErrorEvaluator()
|
||||
reduce_requested = sheerka.ret(context.who, True, sheerka.new(BuiltinConcepts.REDUCE_REQUESTED))
|
||||
if one_error_evaluator.matches(context, return_values + [reduce_requested]):
|
||||
return sheerka.ret(
|
||||
context.who,
|
||||
False,
|
||||
one_error_evaluator.eval(context, return_values).body,
|
||||
parents=return_values)
|
||||
else:
|
||||
return sheerka.ret(
|
||||
context.who,
|
||||
False,
|
||||
sheerka.new(BuiltinConcepts.TOO_MANY_ERRORS, body=return_values),
|
||||
parents=return_values)
|
||||
|
||||
|
||||
def get_names(sheerka, concept_node):
|
||||
@@ -210,5 +220,3 @@ def _extract_predicates(sheerka, node, variables_to_include, variables_to_exclud
|
||||
predicates.append(res)
|
||||
|
||||
return predicates
|
||||
|
||||
|
||||
|
||||
+27
-18
@@ -242,24 +242,24 @@ class Concept:
|
||||
def body(self):
|
||||
return self.values[ConceptParts.BODY] if ConceptParts.BODY in self.values else None
|
||||
|
||||
def add_codes(self, codes):
|
||||
"""
|
||||
Gets the ASTs for 'where', 'pre', 'post' and 'body'
|
||||
There ASTs are know when the concept is freshly parsed.
|
||||
So the values are kept in cache.
|
||||
|
||||
For concepts loaded from sdp, these ASTs must be created again
|
||||
TODO : Seems to be a service method. Can be put somewhere else
|
||||
:param codes:
|
||||
:return:
|
||||
"""
|
||||
if codes is None:
|
||||
return
|
||||
|
||||
for key in codes:
|
||||
self.compiled[key] = codes[key]
|
||||
|
||||
return self
|
||||
# def add_codes(self, codes):
|
||||
# """
|
||||
# Gets the ASTs for 'where', 'pre', 'post' and 'body'
|
||||
# There ASTs are know when the concept is freshly parsed.
|
||||
# So the values are kept in cache.
|
||||
#
|
||||
# For concepts loaded from sdp, these ASTs must be created again
|
||||
# TODO : Seems to be a service method. Can be put somewhere else
|
||||
# :param codes:
|
||||
# :return:
|
||||
# """
|
||||
# if codes is None:
|
||||
# return
|
||||
#
|
||||
# for key in codes:
|
||||
# self.compiled[key] = codes[key]
|
||||
#
|
||||
# return self
|
||||
|
||||
def get_digest(self):
|
||||
"""
|
||||
@@ -406,3 +406,12 @@ class DoNotResolve:
|
||||
set concept.compiled[BODY] to DoNotResolve(value)
|
||||
"""
|
||||
value: object
|
||||
|
||||
|
||||
@dataclass()
|
||||
class InfiniteRecursionResolved:
|
||||
"""This class is used to when we managed to break an infinite recursion concept definition"""
|
||||
value: object
|
||||
|
||||
def get_value(self):
|
||||
return self.value
|
||||
|
||||
@@ -222,6 +222,8 @@ class ExecutionContext:
|
||||
to_str = self.return_value_to_str(r)
|
||||
logger.debug(f"[{self._id:2}]" + self._tab + "-> " + to_str)
|
||||
|
||||
def get_parent(self):
|
||||
return self._parent
|
||||
|
||||
@staticmethod
|
||||
def return_value_to_str(r):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from core.builtin_concepts import BuiltinConcepts
|
||||
from core.concept import Concept
|
||||
from sdp.sheerkaDataProvider import SheerkaDataProvider
|
||||
from core.sheerka.ExecutionContext import ExecutionContext
|
||||
from sdp.sheerkaDataProvider import SheerkaDataProvider, Event
|
||||
import pprint
|
||||
import os
|
||||
|
||||
@@ -28,7 +29,7 @@ class SheerkaDump:
|
||||
defs = self.sheerka.sdp.get(self.sheerka.CONCEPTS_DEFINITIONS_ENTRY)
|
||||
self.sheerka.log.info(defs)
|
||||
|
||||
def dump_desc(self, *concept_names):
|
||||
def dump_desc(self, *concept_names, eval=False):
|
||||
first = True
|
||||
for concept_name in concept_names:
|
||||
if isinstance(concept_name, Concept):
|
||||
@@ -43,13 +44,20 @@ class SheerkaDump:
|
||||
concepts = [concepts]
|
||||
|
||||
for c in concepts:
|
||||
if eval:
|
||||
event = Event(f"Evaluating {c}", "")
|
||||
context = ExecutionContext("dump_desc", event, self.sheerka)
|
||||
evaluated = self.sheerka.evaluate_concept(context, c)
|
||||
value = evaluated.body if evaluated.key == c.key else evaluated
|
||||
|
||||
if not first:
|
||||
self.sheerka.log.info("")
|
||||
self.sheerka.log.info(f"name : {c.name}")
|
||||
self.sheerka.log.info(f"bnf : {c.metadata.definition}")
|
||||
self.sheerka.log.info(f"key : {c.key}")
|
||||
self.sheerka.log.info(f"body : {c.metadata.body}")
|
||||
self.sheerka.log.info(f"value : {c.body}")
|
||||
if eval:
|
||||
self.sheerka.log.info(f"value : {value}")
|
||||
self.sheerka.log.info(f"digest : {c.get_digest()}")
|
||||
|
||||
if self.sheerka.isaset(c):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from core.builtin_concepts import BuiltinConcepts
|
||||
from core.concept import Concept, DoNotResolve, ConceptParts
|
||||
from core.concept import Concept, DoNotResolve, ConceptParts, InfiniteRecursionResolved
|
||||
import core.builtin_helpers
|
||||
|
||||
CONCEPT_EVALUATION_STEPS = [
|
||||
@@ -13,6 +13,55 @@ class SheerkaEvaluateConcept:
|
||||
self.sheerka = sheerka
|
||||
self.logger_name = self.evaluate_concept.__name__
|
||||
|
||||
@staticmethod
|
||||
def infinite_recursion_detected(context, concept):
|
||||
if concept is None:
|
||||
return False
|
||||
|
||||
parent = context.get_parent()
|
||||
while parent is not None:
|
||||
if parent.who == context.who and parent.obj == concept:
|
||||
return True
|
||||
|
||||
parent = parent.get_parent()
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_infinite_recursion_resolution(obj):
|
||||
if isinstance(obj, InfiniteRecursionResolved):
|
||||
return obj
|
||||
|
||||
if isinstance(obj, Concept) and isinstance(obj.body, InfiniteRecursionResolved):
|
||||
return obj.body
|
||||
|
||||
return None
|
||||
|
||||
def manage_infinite_recursion(self, context):
|
||||
"""
|
||||
We look for the fist parent that has a body that means something
|
||||
We use the eval function with no local or global
|
||||
:param context:
|
||||
:return:
|
||||
"""
|
||||
|
||||
parent = context
|
||||
concepts_found = set()
|
||||
while parent and parent.obj:
|
||||
if parent.who == context.who and parent.desc == context.desc:
|
||||
body = parent.obj.metadata.body
|
||||
try:
|
||||
return self.sheerka.ret(self.logger_name, True, InfiniteRecursionResolved(eval(body)))
|
||||
except Exception:
|
||||
pass
|
||||
concepts_found.add(parent.obj)
|
||||
parent = parent.get_parent()
|
||||
|
||||
return self.sheerka.ret(
|
||||
self.logger_name,
|
||||
False,
|
||||
self.sheerka.new(BuiltinConcepts.CHICKEN_AND_EGG, body=concepts_found))
|
||||
|
||||
def initialize_concept_asts(self, context, concept: Concept, logger=None):
|
||||
"""
|
||||
Updates the codes of the newly created concept
|
||||
@@ -75,6 +124,15 @@ class SheerkaEvaluateConcept:
|
||||
if isinstance(to_resolve, DoNotResolve):
|
||||
return to_resolve.value
|
||||
|
||||
# manage infinite loop
|
||||
if self.infinite_recursion_detected(context, current_concept):
|
||||
with context.push(desc="Infinite recursion detected", obj=current_concept) as sub_context:
|
||||
# I create a sub context in order to log what happened
|
||||
sub_context.log_new(logger)
|
||||
ret_val = self.manage_infinite_recursion(context)
|
||||
sub_context.add_values(return_values=ret_val)
|
||||
return ret_val.body
|
||||
|
||||
desc = f"Evaluating {current_prop} (concept={current_concept})"
|
||||
context.log(logger, desc, self.logger_name)
|
||||
with context.push(desc=desc, obj=current_concept) as sub_context:
|
||||
@@ -92,7 +150,8 @@ class SheerkaEvaluateConcept:
|
||||
|
||||
# otherwise, execute all return values to find out what is the value
|
||||
else:
|
||||
r = self.sheerka.execute(sub_context, to_resolve, CONCEPT_EVALUATION_STEPS, logger)
|
||||
use_copy = [r for r in to_resolve] if hasattr(to_resolve, "__iter__") else to_resolve
|
||||
r = self.sheerka.execute(sub_context, use_copy, CONCEPT_EVALUATION_STEPS, logger)
|
||||
one_r = core.builtin_helpers.expect_one(context, r)
|
||||
sub_context.add_values(return_values=one_r)
|
||||
if one_r.status:
|
||||
@@ -100,10 +159,11 @@ class SheerkaEvaluateConcept:
|
||||
else:
|
||||
error = one_r.value
|
||||
|
||||
return self.sheerka.new(BuiltinConcepts.CONCEPT_EVAL_ERROR,
|
||||
body=error,
|
||||
concept=current_concept,
|
||||
property_name=current_prop)
|
||||
return error if self.sheerka.isinstance(error, BuiltinConcepts.CHICKEN_AND_EGG) \
|
||||
else self.sheerka.new(BuiltinConcepts.CONCEPT_EVAL_ERROR,
|
||||
body=error,
|
||||
concept=current_concept,
|
||||
property_name=current_prop)
|
||||
|
||||
def resolve_list(self, context, list_to_resolve, current_prop, current_concept, logger):
|
||||
"""When dealing with a list, there are two possibilities"""
|
||||
@@ -168,7 +228,7 @@ class SheerkaEvaluateConcept:
|
||||
else:
|
||||
# Do not send the current concept for the properties
|
||||
resolved = self.resolve(context, prop_ast, prop_name, None, logger)
|
||||
if context.sheerka.isinstance(resolved, BuiltinConcepts.CONCEPT_EVAL_ERROR):
|
||||
if isinstance(resolved, Concept) and not context.sheerka.is_success(resolved):
|
||||
resolved.set_prop("concept", concept) # since current concept was not sent
|
||||
return resolved
|
||||
else:
|
||||
@@ -178,10 +238,10 @@ class SheerkaEvaluateConcept:
|
||||
if part_key in concept.compiled and concept.compiled[part_key] is not None:
|
||||
metadata_ast = concept.compiled[part_key]
|
||||
resolved = self.resolve(context, metadata_ast, part_key, concept, logger)
|
||||
if context.sheerka.isinstance(resolved, BuiltinConcepts.CONCEPT_EVAL_ERROR):
|
||||
if isinstance(resolved, Concept) and not context.sheerka.is_success(resolved):
|
||||
return resolved
|
||||
else:
|
||||
concept.values[part_key] = resolved
|
||||
concept.values[part_key] = self.get_infinite_recursion_resolution(resolved) or resolved
|
||||
|
||||
# validate where clause
|
||||
if concept.metadata.where is not None:
|
||||
|
||||
+31
-18
@@ -249,11 +249,12 @@ class Sheerka(Concept):
|
||||
if not self.skip_builtins_in_db:
|
||||
self.sdp.save_result(self, execution_context)
|
||||
|
||||
# hack to save valid concept definition
|
||||
if not self.during_restore:
|
||||
if len(ret) == 1 and ret[0].status and self.isinstance(ret[0].value, BuiltinConcepts.NEW_CONCEPT):
|
||||
with open(CONCEPTS_FILE, "a") as f:
|
||||
f.write(text + "\n")
|
||||
# hack to save valid concept definition
|
||||
# if not self.during_restore:
|
||||
# if len(ret) == 1 and ret[0].status and self.isinstance(ret[0].value, BuiltinConcepts.NEW_CONCEPT):
|
||||
# with open(CONCEPTS_FILE, "a") as f:
|
||||
# f.write(text + "\n")
|
||||
|
||||
return ret
|
||||
|
||||
def execute(self, execution_context, return_values, execution_steps, logger=None):
|
||||
@@ -364,19 +365,26 @@ class Sheerka(Concept):
|
||||
concept_key = str(concept_key)
|
||||
|
||||
# first search in cache
|
||||
result = self.cache_by_key[concept_key] if concept_key in self.cache_by_key else \
|
||||
self.sdp.get_safe(self.CONCEPTS_ENTRY, concept_key)
|
||||
if concept_key in self.cache_by_key:
|
||||
result = self.cache_by_key[concept_key]
|
||||
else:
|
||||
result = self.sdp.get_safe(self.CONCEPTS_ENTRY, concept_key)
|
||||
if result is None:
|
||||
metadata = [("key", concept_key), ("id", concept_id)] if concept_id else ("key", concept_key)
|
||||
result = self._get_unknown(metadata)
|
||||
|
||||
if result and (concept_id is None or not isinstance(result, list)):
|
||||
self.cache_by_key[concept_key] = result
|
||||
for r in (result if isinstance(result, list) else [result]):
|
||||
if r.id:
|
||||
self.cache_by_id[r.id] = r
|
||||
|
||||
if not (isinstance(result, list) and concept_id):
|
||||
return result
|
||||
|
||||
if isinstance(result, list):
|
||||
if concept_id:
|
||||
for c in result:
|
||||
if c.id == concept_id:
|
||||
return c
|
||||
else:
|
||||
return result
|
||||
# result is a list, but we have the concept_id to discriminate
|
||||
for c in result:
|
||||
if c.id == concept_id:
|
||||
return c
|
||||
|
||||
metadata = [("key", concept_key), ("id", concept_id)] if concept_id else ("key", concept_key)
|
||||
return self._get_unknown(metadata)
|
||||
@@ -386,10 +394,15 @@ class Sheerka(Concept):
|
||||
return ErrorConcept("Concept id is undefined.")
|
||||
|
||||
# first search in cache
|
||||
result = self.cache_by_id[concept_id] if concept_id in self.cache_by_id else \
|
||||
self.sdp.get_safe(self.CONCEPTS_BY_ID_ENTRY, concept_id)
|
||||
if concept_id in self.cache_by_id:
|
||||
result = self.cache_by_id[concept_id]
|
||||
else:
|
||||
result = self.sdp.get_safe(self.CONCEPTS_BY_ID_ENTRY, concept_id)
|
||||
if result is None:
|
||||
result = self._get_unknown(('id', concept_id))
|
||||
self.cache_by_id[concept_id] = result
|
||||
|
||||
return result or self._get_unknown(('id', concept_id))
|
||||
return result
|
||||
|
||||
def get_concepts_definitions(self, context):
|
||||
if self.concepts_definitions_cache:
|
||||
|
||||
@@ -6,8 +6,8 @@ from evaluators.BaseEvaluator import OneReturnValueEvaluator
|
||||
from parsers.BaseParser import NotInitializedNode
|
||||
from parsers.ConceptLexerParser import ParsingExpression, ParsingExpressionVisitor
|
||||
from parsers.DefaultParser import DefConceptNode
|
||||
|
||||
from parsers.PythonParser import PythonNode
|
||||
import core.utils
|
||||
|
||||
|
||||
class ConceptOrRuleNameVisitor(ParsingExpressionVisitor):
|
||||
@@ -66,7 +66,7 @@ class AddConceptEvaluator(OneReturnValueEvaluator):
|
||||
setattr(concept.metadata, prop, source)
|
||||
|
||||
# try to find what can be a property
|
||||
concept_name = [part.value for part in def_concept_node.name.tokens]
|
||||
concept_name = [part.value for part in core.utils.strip_tokens(def_concept_node.name.tokens, True)]
|
||||
for p in self.get_props(sheerka, part_ret_val, concept_name):
|
||||
props_found.add(p)
|
||||
|
||||
@@ -108,11 +108,12 @@ class AddConceptEvaluator(OneReturnValueEvaluator):
|
||||
# Case of python code
|
||||
#
|
||||
if isinstance(ret_value.value, ParserResultConcept) and isinstance(ret_value.value.value, PythonNode):
|
||||
python_node = ret_value.value.value
|
||||
as_concept_node = python_to_concept(python_node.ast_)
|
||||
variables = get_names(sheerka, as_concept_node)
|
||||
variables = filter(lambda x: x in concept_name, variables)
|
||||
return list(variables)
|
||||
if len(concept_name) > 1:
|
||||
python_node = ret_value.value.value
|
||||
as_concept_node = python_to_concept(python_node.ast_)
|
||||
variables = get_names(sheerka, as_concept_node)
|
||||
variables = filter(lambda x: x in concept_name, variables)
|
||||
return list(variables)
|
||||
|
||||
#
|
||||
# case of concept
|
||||
|
||||
Reference in New Issue
Block a user