Added first implementation of concepts ambiguity resolution + Jenkins file test
This commit is contained in:
@@ -2,6 +2,7 @@ from core.builtin_concepts import BuiltinConcepts
|
||||
from core.builtin_helpers import expect_one, only_successful
|
||||
from core.concept import Concept, DoNotResolve, ConceptParts, InfiniteRecursionResolved
|
||||
from core.sheerka.services.sheerka_service import BaseService
|
||||
from core.tokenizer import Tokenizer
|
||||
from core.utils import unstr_concept
|
||||
|
||||
CONCEPT_EVALUATION_STEPS = [
|
||||
@@ -17,16 +18,23 @@ class SheerkaEvaluateConcept(BaseService):
|
||||
super().__init__(sheerka)
|
||||
|
||||
def initialize(self):
|
||||
self.sheerka.bind_service_method(self.evaluate_concept)
|
||||
self.sheerka.bind_service_method(self.evaluate_concept, True)
|
||||
|
||||
@staticmethod
|
||||
def infinite_recursion_detected(context, concept):
|
||||
"""
|
||||
Browse the parents, looking for another evaluation of the same concept
|
||||
:param context:
|
||||
:param concept:
|
||||
:return:
|
||||
"""
|
||||
if concept is None:
|
||||
return False
|
||||
|
||||
parent = context.get_parent()
|
||||
while parent is not None:
|
||||
if parent.who == context.who and parent.obj == concept and parent.obj.compiled == concept.compiled:
|
||||
if parent.who == context.who and parent.action == BuiltinConcepts.EVALUATING_CONCEPT and \
|
||||
parent.obj == concept and parent.obj.compiled == concept.compiled:
|
||||
return True
|
||||
|
||||
parent = parent.get_parent()
|
||||
@@ -64,7 +72,7 @@ class SheerkaEvaluateConcept(BaseService):
|
||||
parent = context
|
||||
concepts_found = set()
|
||||
while parent and parent.obj:
|
||||
if parent.who == context.who and parent.desc == context.desc:
|
||||
if parent.who == context.who and parent.action == BuiltinConcepts.EVALUATING_CONCEPT:
|
||||
body = parent.obj.metadata.body
|
||||
try:
|
||||
return self.sheerka.ret(self.NAME, True, InfiniteRecursionResolved(eval(body)))
|
||||
@@ -180,8 +188,7 @@ class SheerkaEvaluateConcept(BaseService):
|
||||
|
||||
path = get_path(context, current_prop)
|
||||
desc = f"Evaluating {path} (concept={current_concept})"
|
||||
context.log(desc, self.NAME)
|
||||
with context.push(BuiltinConcepts.EVALUATING_CONCEPT,
|
||||
with context.push(BuiltinConcepts.EVALUATING_ATTRIBUTE,
|
||||
current_prop,
|
||||
desc=desc,
|
||||
obj=current_concept,
|
||||
@@ -191,14 +198,14 @@ class SheerkaEvaluateConcept(BaseService):
|
||||
sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
|
||||
|
||||
if expect_success:
|
||||
sub_context.local_hints.add(BuiltinConcepts.EVAL_SUCCESS_REQUESTED)
|
||||
sub_context.local_hints.add(BuiltinConcepts.EVAL_UNTIL_SUCCESS_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:
|
||||
if evaluated.key == to_resolve.key: # quicker (and dirtier) than sheerka.is_success()
|
||||
return self.apply_ret(evaluated)
|
||||
else:
|
||||
error = evaluated
|
||||
@@ -253,51 +260,78 @@ class SheerkaEvaluateConcept(BaseService):
|
||||
|
||||
return res
|
||||
|
||||
def evaluate_concept(self, context, concept: Concept):
|
||||
def evaluate_concept(self, context, concept: Concept, eval_body=False, metadata=None):
|
||||
"""
|
||||
Evaluation a concept
|
||||
It means that if the where clause is True, will evaluate the body
|
||||
:param context:
|
||||
:param concept:
|
||||
:param eval_body:
|
||||
:param metadata: list of metadata to evaluate ('pre', 'post'...)
|
||||
:return: value of the evaluation or error
|
||||
"""
|
||||
|
||||
if concept.metadata.is_evaluated:
|
||||
return concept
|
||||
|
||||
self.initialize_concept_asts(context, concept)
|
||||
# I cannot use cache because of concept like 'number'.
|
||||
# They don't have variables, but their values change every time they are instanciated
|
||||
# TODO: Need to find a way to cache despite of them
|
||||
# need_body = eval_body or context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED)
|
||||
# if need_body and len(concept.metadata.variables) == 0 and context.sheerka.has_id(concept.id):
|
||||
# from_cache = context.sheerka.get_by_id(concept.id)
|
||||
# if from_cache.metadata.is_evaluated:
|
||||
# concept.set_value(ConceptParts.BODY, from_cache.body)
|
||||
# concept.metadata.is_evaluated = True
|
||||
# return 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)
|
||||
desc = f"Evaluating concept {concept}"
|
||||
with context.push(BuiltinConcepts.EVALUATING_CONCEPT, concept, desc=desc, eval_body=eval_body) as sub_context:
|
||||
|
||||
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 eval_body:
|
||||
# ask for body evaluation
|
||||
sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
|
||||
|
||||
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, False)
|
||||
else:
|
||||
# Do not send the current concept for the properties
|
||||
resolved = self.resolve(context, prop_ast, var_name, None, True, False)
|
||||
# auto evaluate commands
|
||||
if context.sheerka.isa(concept, context.sheerka.new(BuiltinConcepts.COMMAND)):
|
||||
sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
|
||||
|
||||
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)
|
||||
self.initialize_concept_asts(sub_context, concept)
|
||||
|
||||
# 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
|
||||
# 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 = metadata or self.compute_metadata_to_eval(sub_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(sub_context, prop_ast, var_name, None, True, False)
|
||||
else:
|
||||
# Do not send the current concept for the properties
|
||||
resolved = self.resolve(sub_context, prop_ast, var_name, None, True, False)
|
||||
|
||||
if isinstance(resolved, Concept) and not sub_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(sub_context, concept.body):
|
||||
continue
|
||||
|
||||
if part_key not in concept.compiled or concept.compiled[part_key] is None:
|
||||
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 concept need to be evaluated
|
||||
# if we want the predicates to be resolved => so force_eval = True
|
||||
# otherwise no need to force
|
||||
@@ -306,91 +340,111 @@ class SheerkaEvaluateConcept(BaseService):
|
||||
# when resolving predicate (where or pre), we need to make sure that PythonEvaluator
|
||||
# will try every possibilities before returning False
|
||||
expect_success = part_key in (ConceptParts.WHERE, ConceptParts.PRE)
|
||||
resolved = self.resolve(context,
|
||||
|
||||
# resolve
|
||||
resolved = self.resolve(sub_context,
|
||||
metadata_ast,
|
||||
part_key,
|
||||
concept,
|
||||
force_concept_eval,
|
||||
expect_success)
|
||||
if isinstance(resolved, Concept) and not context.sheerka.is_success(resolved):
|
||||
|
||||
# 'FATAL' error is detected, let's stop
|
||||
if isinstance(resolved, Concept) and not sub_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
|
||||
#
|
||||
concept.set_value(part_key, self.get_infinite_recursion_resolution(resolved) or resolved)
|
||||
|
||||
# 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)
|
||||
# validate PRE and WHERE condition
|
||||
if part_key in (ConceptParts.PRE, ConceptParts.WHERE) and not self.sheerka.objvalue(resolved):
|
||||
return self.sheerka.new(BuiltinConcepts.CONDITION_FAILED,
|
||||
body=getattr(concept.metadata, metadata_to_eval),
|
||||
concept=concept,
|
||||
prop=part_key)
|
||||
|
||||
#
|
||||
# TODO : Validate the POST condition
|
||||
#
|
||||
#
|
||||
# TODO : Validate the POST condition
|
||||
#
|
||||
|
||||
concept.init_key() # only does it if needed
|
||||
concept.metadata.is_evaluated = "body" in all_metadata_to_eval
|
||||
concept.init_key() # Necessary for old unit tests. To remove someday
|
||||
|
||||
# update the cache for concepts with no variable
|
||||
if len(concept.metadata.variables) == 0:
|
||||
self.sheerka.cache_manager.put(self.sheerka.CONCEPTS_BY_ID_ENTRY, concept.id, concept)
|
||||
if "body" in all_metadata_to_eval:
|
||||
concept.metadata.is_evaluated = True
|
||||
|
||||
return concept
|
||||
# # update the cache for concepts with no variables
|
||||
# Cannot use cache. See the comment at the beginning of this method
|
||||
# if len(concept.metadata.variables) == 0:
|
||||
# self.sheerka.cache_manager.put(self.sheerka.CONCEPTS_BY_ID_ENTRY, concept.id, concept)
|
||||
|
||||
def choose_metadata_to_eval(self, context, concept):
|
||||
if context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED):
|
||||
return ["pre", "post", "variables", "body", "where", "ret"]
|
||||
return concept
|
||||
|
||||
def compute_metadata_to_eval(self, context, concept):
|
||||
to_eval = []
|
||||
|
||||
needed, variables, body = self.get_needed_metadata(concept, ConceptParts.PRE, True, True)
|
||||
to_eval.extend(needed)
|
||||
|
||||
metadata = ["pre", "post", "ret"]
|
||||
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")
|
||||
# What are the cases where we do not need a validation ?
|
||||
# see test_sheerka_non_reg::test_i_can_evaluate_bnf_concept_with_where_clause()
|
||||
# res = sheerka.evaluate_user_input("foobar")
|
||||
needed, v, b = self.get_needed_metadata(concept, ConceptParts.WHERE, not variables, not body)
|
||||
variables |= v
|
||||
body |= b
|
||||
to_eval.extend(needed)
|
||||
|
||||
return metadata
|
||||
needed, v, b = self.get_needed_metadata(concept, ConceptParts.RET, not variables, not body)
|
||||
variables |= v
|
||||
body |= b
|
||||
to_eval.extend(needed)
|
||||
|
||||
def needed_metadata(self, concept, metadata):
|
||||
needed, v, b = self.get_needed_metadata(concept, ConceptParts.POST, not variables, not body)
|
||||
variables |= v
|
||||
body |= b
|
||||
to_eval.extend(needed)
|
||||
|
||||
if context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED):
|
||||
if not variables:
|
||||
to_eval.append('variables')
|
||||
|
||||
if not body:
|
||||
to_eval.append("body")
|
||||
|
||||
return to_eval
|
||||
|
||||
@staticmethod
|
||||
def get_needed_metadata(concept, concept_part, check_vars, check_body):
|
||||
"""
|
||||
Tries to find out if the evaluation of the body is necessary
|
||||
It's a very basic approach that will need to be improved
|
||||
Check if the concept_part has to be evaluated
|
||||
It also checks if the variables and the body need to be evaluated prior to it
|
||||
:param concept:
|
||||
:param metadata:
|
||||
:param concept_part:
|
||||
:param check_vars:
|
||||
:param check_body:
|
||||
:return:
|
||||
"""
|
||||
ret = []
|
||||
vars_needed = False
|
||||
body_needed = False
|
||||
|
||||
if metadata not in concept.compiled:
|
||||
return []
|
||||
if concept_part in concept.compiled and concept.compiled[concept_part] is not None:
|
||||
concept_part_source = getattr(concept.metadata, concept_part.value)
|
||||
|
||||
return_values = concept.compiled[metadata]
|
||||
if not isinstance(return_values, list):
|
||||
return []
|
||||
assert concept_part_source is not None
|
||||
|
||||
needed = []
|
||||
for return_value in return_values:
|
||||
if not self.sheerka.isinstance(return_value, BuiltinConcepts.RETURN_VALUE):
|
||||
continue
|
||||
tokens = [t.str_value for t in Tokenizer(concept_part_source)]
|
||||
|
||||
if not return_value.status:
|
||||
continue
|
||||
if check_vars:
|
||||
for var_name in (v[0] for v in concept.metadata.variables):
|
||||
if var_name in tokens:
|
||||
vars_needed = True
|
||||
ret.append("variables")
|
||||
break
|
||||
|
||||
if not self.sheerka.isinstance(return_value.body, BuiltinConcepts.PARSER_RESULT):
|
||||
continue
|
||||
if check_body and "self" in tokens:
|
||||
body_needed = True
|
||||
ret.append("body")
|
||||
|
||||
if not isinstance(return_value.body.source, str):
|
||||
continue
|
||||
ret.append(concept_part.value)
|
||||
|
||||
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
|
||||
return ret, vars_needed, body_needed
|
||||
|
||||
Reference in New Issue
Block a user