324 lines
14 KiB
Python
324 lines
14 KiB
Python
from core.builtin_concepts import BuiltinConcepts
|
|
from core.builtin_helpers import expect_one, only_successful
|
|
from core.concept import Concept, DoNotResolve, ConceptParts, InfiniteRecursionResolved
|
|
|
|
CONCEPT_EVALUATION_STEPS = [
|
|
BuiltinConcepts.BEFORE_EVALUATION,
|
|
BuiltinConcepts.EVALUATION,
|
|
BuiltinConcepts.AFTER_EVALUATION]
|
|
|
|
|
|
class SheerkaEvaluateConcept:
|
|
def __init__(self, sheerka):
|
|
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):
|
|
"""
|
|
Updates the codes of the newly created concept
|
|
Basically, it runs the parsers on all parts
|
|
:param concept:
|
|
:param context:
|
|
:param logger:
|
|
:return:
|
|
"""
|
|
def is_only_successful(r):
|
|
# return False
|
|
return context.sheerka.isinstance(r, BuiltinConcepts.RETURN_VALUE) and \
|
|
context.sheerka.isinstance(r.body, BuiltinConcepts.ONLY_SUCCESSFUL)
|
|
|
|
steps = [BuiltinConcepts.BEFORE_PARSING, BuiltinConcepts.PARSING, BuiltinConcepts.AFTER_PARSING]
|
|
for part_key in ConceptParts:
|
|
if part_key in concept.compiled:
|
|
continue
|
|
|
|
source = getattr(concept.metadata, part_key.value)
|
|
if source is None or not isinstance(source, str):
|
|
continue
|
|
|
|
if source.strip() == "":
|
|
concept.compiled[part_key] = DoNotResolve(source)
|
|
else:
|
|
with context.push(desc=f"Initializing compiled for {part_key}") as sub_context:
|
|
sub_context.add_inputs(source=source)
|
|
to_parse = self.sheerka.ret(context.who, True,
|
|
self.sheerka.new(BuiltinConcepts.USER_INPUT, body=source))
|
|
res = self.sheerka.execute(sub_context, to_parse, steps)
|
|
only_success = only_successful(sub_context, res)
|
|
concept.compiled[part_key] = only_success.body.body if is_only_successful(only_success) else res
|
|
sub_context.add_values(return_values=res)
|
|
|
|
for var_name, default_value in concept.metadata.variables:
|
|
if var_name in concept.compiled:
|
|
continue
|
|
|
|
if default_value is None or not isinstance(default_value, str):
|
|
continue
|
|
|
|
if default_value.strip() == "":
|
|
concept.compiled[var_name] = DoNotResolve(default_value)
|
|
else:
|
|
with context.push(desc=f"Initializing AST for property {var_name}") as sub_context:
|
|
sub_context.add_inputs(source=default_value)
|
|
to_parse = self.sheerka.ret(context.who, True,
|
|
self.sheerka.new(BuiltinConcepts.USER_INPUT, body=default_value))
|
|
res = self.sheerka.execute(context, to_parse, steps)
|
|
only_success = only_successful(sub_context, res)
|
|
concept.compiled[var_name] = only_success.body.body if is_only_successful(only_success) else res
|
|
sub_context.add_values(return_values=res)
|
|
|
|
# Updates the cache of concepts when possible
|
|
if self.sheerka.has_id(concept.id):
|
|
self.sheerka.get_by_id(concept.id).compiled = concept.compiled
|
|
|
|
def resolve(self, context, to_resolve, current_prop, current_concept, force_evaluation):
|
|
|
|
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
|
|
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(desc, self.logger_name)
|
|
with context.push(desc=desc, obj=current_concept) as sub_context:
|
|
|
|
if force_evaluation:
|
|
sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
|
|
|
|
# when it's a concept, evaluate it
|
|
if isinstance(to_resolve, Concept) and \
|
|
not context.sheerka.isinstance(to_resolve, BuiltinConcepts.RETURN_VALUE):
|
|
evaluated = self.evaluate_concept(sub_context, to_resolve)
|
|
sub_context.add_values(return_values=evaluated)
|
|
if evaluated.key == to_resolve.key:
|
|
return evaluated
|
|
else:
|
|
error = evaluated
|
|
|
|
# otherwise, execute all return values to find out what is the value
|
|
else:
|
|
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)
|
|
|
|
one_r = expect_one(context, r)
|
|
sub_context.add_values(return_values=one_r)
|
|
if one_r.status:
|
|
return one_r.value
|
|
else:
|
|
error = one_r.value
|
|
|
|
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, force_evaluation):
|
|
"""When dealing with a list, there are two possibilities"""
|
|
# It may be a list of ReturnValueConcept to execute (always the case for metadata)
|
|
# or a list of single values (may be the case for properties)
|
|
# in this latter case, all values are to be processed one by one and a list should be returned
|
|
if len(list_to_resolve) == 0:
|
|
return []
|
|
|
|
if self.sheerka.isinstance(list_to_resolve[0], BuiltinConcepts.RETURN_VALUE):
|
|
return self.resolve(context, list_to_resolve, current_prop, current_concept, force_evaluation)
|
|
|
|
res = []
|
|
for to_resolve in list_to_resolve:
|
|
# sanity check
|
|
if self.sheerka.isinstance(to_resolve, BuiltinConcepts.RETURN_VALUE):
|
|
return self.sheerka.new(BuiltinConcepts.CONCEPT_EVAL_ERROR,
|
|
body="Mix between real values and return values",
|
|
concept=current_concept,
|
|
property_name=current_prop)
|
|
|
|
r = self.resolve(context, to_resolve, current_prop, current_concept, force_evaluation)
|
|
if self.sheerka.isinstance(r, BuiltinConcepts.CONCEPT_EVAL_ERROR):
|
|
return r
|
|
res.append(r)
|
|
|
|
return res
|
|
|
|
def evaluate_concept(self, context, concept: Concept):
|
|
"""
|
|
Evaluation a concept
|
|
It means that if the where clause is True, will evaluate the body
|
|
:param context:
|
|
:param concept:
|
|
:return: value of the evaluation or error
|
|
"""
|
|
|
|
if concept.metadata.is_evaluated:
|
|
return concept
|
|
|
|
self.initialize_concept_asts(context, concept)
|
|
|
|
# to make sure of the order, it don't use ConceptParts.get_parts()
|
|
# variables must be evaluated first, body must be evaluated before where
|
|
all_metadata_to_eval = self.choose_metadata_to_eval(context, concept)
|
|
|
|
for metadata_to_eval in all_metadata_to_eval:
|
|
if metadata_to_eval == "variables":
|
|
for var_name in (v for v in concept.variables() if v in concept.compiled):
|
|
prop_ast = concept.compiled[var_name]
|
|
|
|
if isinstance(prop_ast, list):
|
|
# Do not send the current concept for the properties
|
|
resolved = self.resolve_list(context, prop_ast, var_name, None, True)
|
|
else:
|
|
# Do not send the current concept for the properties
|
|
resolved = self.resolve(context, prop_ast, var_name, None, True)
|
|
|
|
if isinstance(resolved, Concept) and not context.sheerka.is_success(resolved):
|
|
resolved.set_value("concept", concept) # since current concept was not sent
|
|
return resolved
|
|
else:
|
|
concept.set_value(var_name, resolved)
|
|
else:
|
|
part_key = ConceptParts(metadata_to_eval)
|
|
|
|
# do not evaluate where when the body is a set
|
|
# Indeed, the way that the where clause is expressed is not a valid python or concept code
|
|
if part_key == ConceptParts.WHERE and self.sheerka.isaset(context, concept.body):
|
|
continue
|
|
|
|
if part_key in concept.compiled and concept.compiled[part_key] is not None:
|
|
metadata_ast = concept.compiled[part_key]
|
|
# if part_key is PRE, POST or WHERE, the concepts need to be evaluated
|
|
# otherwise the predicates cannot be resolved
|
|
force_concept_eval = False if part_key == ConceptParts.BODY else True
|
|
resolved = self.resolve(context, metadata_ast, part_key, concept, force_concept_eval)
|
|
if isinstance(resolved, Concept) and not context.sheerka.is_success(resolved):
|
|
return resolved
|
|
else:
|
|
concept.set_value(part_key, self.get_infinite_recursion_resolution(resolved) or resolved)
|
|
|
|
#
|
|
# TODO : Validate the PRE condition
|
|
#
|
|
|
|
# validate where clause
|
|
if ConceptParts.WHERE in concept.values:
|
|
where_value = concept.get_value(ConceptParts.WHERE)
|
|
if not (where_value is None or self.sheerka.objvalue(where_value)):
|
|
return self.sheerka.new(BuiltinConcepts.WHERE_CLAUSE_FAILED, body=concept)
|
|
|
|
#
|
|
# TODO : Validate the POST condition
|
|
#
|
|
|
|
concept.init_key() # only does it if needed
|
|
concept.metadata.is_evaluated = "body" in all_metadata_to_eval
|
|
return concept
|
|
|
|
def choose_metadata_to_eval(self, context, concept):
|
|
if context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED):
|
|
return ["pre", "post", "variables", "body", "where"]
|
|
|
|
metadata = ["pre", "post"]
|
|
if context.in_context(BuiltinConcepts.EVAL_WHERE_REQUESTED) or concept.metadata.need_validation:
|
|
needed = self.needed_metadata(concept, ConceptParts.WHERE)
|
|
for e in needed:
|
|
if e not in metadata:
|
|
metadata.append(e)
|
|
if "where" not in metadata:
|
|
metadata.append("where")
|
|
|
|
return metadata
|
|
|
|
def needed_metadata(self, concept, metadata):
|
|
"""
|
|
Tries to find out if the evaluation of the body is necessary
|
|
It's a very basic approach that will need to be improved
|
|
:param concept:
|
|
:param metadata:
|
|
:return:
|
|
"""
|
|
|
|
if metadata not in concept.compiled:
|
|
return []
|
|
|
|
return_values = concept.compiled[metadata]
|
|
if not isinstance(return_values, list):
|
|
return []
|
|
|
|
needed = []
|
|
for return_value in return_values:
|
|
if not self.sheerka.isinstance(return_value, BuiltinConcepts.RETURN_VALUE):
|
|
continue
|
|
|
|
if not return_value.status:
|
|
continue
|
|
|
|
if not self.sheerka.isinstance(return_value.body, BuiltinConcepts.PARSER_RESULT):
|
|
continue
|
|
|
|
if not isinstance(return_value.body.source, str):
|
|
continue
|
|
|
|
for var_name in (p[0] for p in concept.metadata.variables):
|
|
if var_name in return_value.body.source:
|
|
needed.append("variables")
|
|
break
|
|
|
|
if "self" in return_value.body.source:
|
|
needed.append("body")
|
|
|
|
return needed
|