Added chicken and egg recursion detection

This commit is contained in:
2020-02-06 17:50:14 +01:00
parent afc1e22949
commit 7481b458e1
16 changed files with 358 additions and 77 deletions
+3 -1
View File
@@ -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
}]
"""
+16 -8
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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):
+11 -3
View File
@@ -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
View File
@@ -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: