Added first implementation of concepts ambiguity resolution + Jenkins file test

This commit is contained in:
2020-07-15 18:29:43 +02:00
parent b768eaa95d
commit e84b394da2
42 changed files with 1130 additions and 313 deletions
+73 -30
View File
@@ -15,7 +15,15 @@ class BuiltinConcepts(Enum):
"""
SHEERKA = "sheerka"
# Execution context actions
# processing instructions during sheerka.execute()
EVAL_BODY_REQUESTED = "eval body" # to evaluate the body
EVAL_WHERE_REQUESTED = "eval where" # to evaluate the where clause
RETURN_VALUE_REQUESTED = "return value" # returns the body of the concept instead of the concept itself
REDUCE_REQUESTED = "reduce" # remove meaningless error when possible
EVAL_UNTIL_SUCCESS_REQUESTED = "eval until success" # PythonEvaluator tries combination until True is found
QUESTION_REQUESTED = "question" # a question is asked
# possible actions during sheerka.execute()
INIT_SHEERKA = "init sheerka" #
PROCESS_INPUT = "process input" # Processing user input or other input
PROCESSING = "processing input" # Processing user input or other input
@@ -30,22 +38,28 @@ class BuiltinConcepts(Enum):
AFTER_RENDERING = "after rendering" # rendering the response from sheerka
EVALUATE_CONCEPT = "evaluate concept" # a concept will be evaluated
EVALUATING_CONCEPT = "evaluating concept" # a concept will be evaluated
EVALUATING_ATTRIBUTE = "evaluating concept attribute" #
VALIDATE_CONCEPT = "validate concept"
VALIDATING_CONCEPT = "validating concept"
INIT_COMPILED = "initializing concept compiled"
INIT_BNF = "ensure bnf"
INIT_BNF = "initialize bnf"
MANAGE_INFINITE_RECURSION = "manage infinite recursion"
PARSE_CODE = "execute source code"
EXEC_CODE = "execute source code"
TESTING = "testing"
USER_INPUT = "user input" # represent an input from an user
SUCCESS = "success"
ERROR = "error"
# builtin attributes
ISA = "is a" # when a concept is an instance of another one
COMMAND = "command" # when the concept must be auto evaluated
# object
USER_INPUT = "user input concept" # represent an input from an user
SUCCESS = "success concept"
ERROR = "error concept"
UNKNOWN_CONCEPT = "unknown concept" # the request concept is not recognized
CANNOT_RESOLVE_CONCEPT = "cannot resolve concept" # when too many concepts with the same name
RETURN_VALUE = "return value" # a value is returned
CONCEPT_TOO_LONG = "concept too long" # concept cannot be processed by exactConcept parser
RETURN_VALUE = "return value concept" # a value is returned
CONCEPT_TOO_LONG = "concept too long concept" # concept cannot be processed by exactConcept parser
NEW_CONCEPT = "new concept" # when a new concept is added
UNKNOWN_PROPERTY = "unknown property" # when requesting for a unknown property
PARSER_RESULT = "parser result"
@@ -64,15 +78,9 @@ class BuiltinConcepts(Enum):
FILTERED = "filtered" # represents the result of a filtering
CONCEPT_ALREADY_IN_SET = "concept already in set"
EVALUATOR_PRE_PROCESS = "evaluator pre process" # used modify / tweak behaviour of evaluators
EVAL_BODY_REQUESTED = "eval body requested" # to evaluate the body
EVAL_WHERE_REQUESTED = "eval where requested" # to evaluate the where clause
CONCEPT_VALUE_REQUESTED = "concept value requested" # returns the body of the concept instead of the concept itself
REDUCE_REQUESTED = "reduce requested" # remove meaningless error when possible
EVAL_SUCCESS_REQUESTED = "Try to find a successful evaluation" # PyhtonEvaluator tries combination until True is found
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
CONDITION_FAILED = "where clause failed" # failed to validate where clause during evaluation
CHICKEN_AND_EGG = "chicken and egg" # infinite recursion when declaring concept
ISA = "is a" # builtin concept to express that a concept is an instance of another one
EXPLANATION = "explanation"
PRECEDENCE = "precedence" # use to set priority among concepts when parsing
ASSOCIATIVITY = "associativity" # use to set priority among concepts when parsing
@@ -80,6 +88,7 @@ class BuiltinConcepts(Enum):
NOT_FOUND = "not found" # when the wanted resource is not found
FORMAT_INSTRUCTIONS = "format instructions" # to express how to print the concept
NOT_IMPLEMENTED = "not implemented" # instead of raise an error
PYTHON_SECURITY_ERROR = "security error" # when trying to execute statement when only expression is allowed
NODE = "node"
GENERIC_NODE = "generic node"
@@ -108,6 +117,16 @@ class BuiltinConcepts(Enum):
BuiltinUnique = [
BuiltinConcepts.EVAL_BODY_REQUESTED,
BuiltinConcepts.EVAL_WHERE_REQUESTED,
BuiltinConcepts.RETURN_VALUE_REQUESTED,
BuiltinConcepts.REDUCE_REQUESTED,
BuiltinConcepts.EVAL_UNTIL_SUCCESS_REQUESTED,
BuiltinConcepts.QUESTION_REQUESTED,
BuiltinConcepts.INIT_SHEERKA,
BuiltinConcepts.PROCESS_INPUT,
BuiltinConcepts.PROCESSING,
BuiltinConcepts.BEFORE_PARSING,
BuiltinConcepts.PARSING,
BuiltinConcepts.AFTER_PARSING,
@@ -117,10 +136,20 @@ BuiltinUnique = [
BuiltinConcepts.BEFORE_RENDERING,
BuiltinConcepts.RENDERING,
BuiltinConcepts.AFTER_RENDERING,
BuiltinConcepts.SUCCESS,
BuiltinConcepts.NOP,
BuiltinConcepts.EVAL_BODY_REQUESTED,
BuiltinConcepts.REDUCE_REQUESTED,
BuiltinConcepts.EVALUATE_CONCEPT,
BuiltinConcepts.EVALUATING_CONCEPT,
BuiltinConcepts.EVALUATING_ATTRIBUTE,
BuiltinConcepts.VALIDATE_CONCEPT,
BuiltinConcepts.VALIDATING_CONCEPT,
BuiltinConcepts.INIT_COMPILED,
BuiltinConcepts.INIT_BNF,
BuiltinConcepts.MANAGE_INFINITE_RECURSION,
BuiltinConcepts.PARSE_CODE,
BuiltinConcepts.EXEC_CODE,
BuiltinConcepts.TESTING,
BuiltinConcepts.ISA,
BuiltinConcepts.COMMAND,
]
BuiltinErrors = [str(e) for e in {
@@ -137,7 +166,7 @@ BuiltinErrors = [str(e) for e in {
BuiltinConcepts.CONCEPT_EVAL_ERROR,
BuiltinConcepts.CONCEPT_ALREADY_IN_SET,
BuiltinConcepts.NOT_A_SET,
BuiltinConcepts.WHERE_CLAUSE_FAILED,
BuiltinConcepts.CONDITION_FAILED,
BuiltinConcepts.CHICKEN_AND_EGG,
BuiltinConcepts.NOT_INITIALIZED,
BuiltinConcepts.NOT_FOUND
@@ -194,7 +223,7 @@ class ReturnValueConcept(Concept):
It's the main input for the evaluators
"""
def __init__(self, who=None, status=None, value=None, message=None, parents=None):
def __init__(self, who=None, status=None, value=None, message=None, parents=None, concept_id=None):
super().__init__(BuiltinConcepts.RETURN_VALUE, True, False, BuiltinConcepts.RETURN_VALUE)
self.set_value(ConceptParts.BODY, value)
self.set_value("who", who)
@@ -202,6 +231,7 @@ class ReturnValueConcept(Concept):
self.set_value("message", message)
self.set_value("parents", parents)
self.metadata.is_evaluated = True
self.metadata.id = concept_id
@property
def who(self):
@@ -429,21 +459,19 @@ class ConceptAlreadyInSet(Concept):
return self.get_value("concept_set")
class WhereClauseFailed(Concept):
def __init__(self, concept=None):
super().__init__(BuiltinConcepts.WHERE_CLAUSE_FAILED,
class ConditionFailed(Concept):
def __init__(self, condition=None, concept=None, prop=None):
super().__init__(BuiltinConcepts.CONDITION_FAILED,
True,
False,
BuiltinConcepts.WHERE_CLAUSE_FAILED)
self.set_value(ConceptParts.BODY, concept)
BuiltinConcepts.CONDITION_FAILED)
self.set_value(ConceptParts.BODY, condition)
self.set_value("concept", concept)
self.set_value("prop", prop)
self.metadata.is_evaluated = True
def __repr__(self):
return f"WhereClauseFailed(concept={self.concept})"
@property
def concept(self):
return self.body
return f"ConditionFailed(condition='{self.body}', concept='{self.concept}', prop='{self.prop}')"
class NotForMeConcept(Concept):
@@ -472,3 +500,18 @@ class ExplanationConcept(Concept):
self.set_value("instructions", instructions) # instructions for SheerkaPrint
self.set_value(ConceptParts.BODY, execution_result) # list of results
self.metadata.is_evaluated = True
class PythonSecurityError(Concept):
def __init__(self, prop=None, source_code=None, source=None, line=None, column=None):
super().__init__(BuiltinConcepts.PYTHON_SECURITY_ERROR,
True,
False,
BuiltinConcepts.PYTHON_SECURITY_ERROR)
self.set_value("prop", prop) # property or variable that was evaluated
self.set_value("source", source) # origin of the source code (eg. file name)
self.set_value("line", line) # line number
self.set_value("column", column) # column number
self.set_value(ConceptParts.BODY, source_code) # code being executed
self.metadata.is_evaluated = True
+133 -16
View File
@@ -5,7 +5,7 @@ import core.ast.nodes
from core.ast.nodes import CallNodeConcept
from core.ast.visitors import UnreferencedNamesVisitor
from core.builtin_concepts import BuiltinConcepts
from core.concept import Concept, NotInit
from core.concept import Concept, NotInit, ConceptParts
from parsers.BaseNodeParser import SourceCodeNode, ConceptNode, UnrecognizedTokensNode
from parsers.BaseParser import BaseParser, ErrorNode
@@ -25,13 +25,10 @@ def is_same_success(context, return_values):
if isinstance(ret_val.body, Concept):
if not ret_val.body.metadata.is_evaluated:
with context.push(BuiltinConcepts.EVALUATE_CONCEPT,
ret_val.body,
desc=f"Evaluating concept '{ret_val.body}'") as sub_context:
sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
evaluated = context.sheerka.evaluate_concept(sub_context, ret_val.body)
if evaluated.key != ret_val.body.key:
raise Exception("Failed to evaluate evaluate")
evaluated = context.sheerka.evaluate_concept(context, ret_val.body, eval_body=True)
if not context.sheerka.is_success(evaluated):
raise Exception("Failed to evaluate evaluate")
return context.sheerka.objvalue(evaluated)
else:
return context.sheerka.objvalue(ret_val.body)
@@ -173,6 +170,129 @@ def only_successful(context, return_values):
sheerka.new(BuiltinConcepts.ONLY_SUCCESSFUL, body=successful_results),
parents=return_values)
#
# def remove_ambiguity(context, return_values):
# """
# When multiple concepts are matching, try to find the one(s) which is/are the more accurate
# :param context:
# :param return_values:
# :return:
# """
# # example :
# # Concept("x is a y", PRE=QUESTION)
# # Concept("x is a y")
# # Concept("x is a command")
# # with the source 'foo is a command'
# # The first one do not respect the pre condition (assuming that QUESTION is not in context)
# # The second one is Ok, but the third is more specific (as it's a concept that explicitly asks for 'command')
# # The third one will remain
# if not return_values:
# return return_values
#
# sheerka = context.sheerka
# by_number_of_vars = {} # sorted by number of variables
# to_keep = [] # return values values that are not eligible (ex non parser result)
# passed_validation = []
#
# # sort by number of variables
# # The less variables there are, the more accurate is the concept
# for r in return_values:
# if (r.status and
# sheerka.isinstance(r.body, BuiltinConcepts.PARSER_RESULT) and
# isinstance(r.body.body, Concept)):
# by_number_of_vars.setdefault(len(r.body.body.metadata.variables), []).append(r)
# else:
# to_keep.append(r)
#
# # Check the concepts, starting with the ones that have the less variables
# # Once a concept with a given number of variables is found, we do not process the concepts with an higher
# # number of variables
# for nb_vars in sorted(by_number_of_vars.keys()):
# for r in by_number_of_vars[nb_vars]:
# concept = r.body.body
# if concept.metadata.pre is None or concept.metadata.pre.strip() == "":
# passed_validation.append(r)
# else:
# evaluated = context.sheerka.evaluate_concept(context, concept, metadata=["pre"])
# if evaluated.key == concept.key and evaluated.get_value(ConceptParts.PRE):
# passed_validation.append(r)
# if len(passed_validation) > 0:
# break
#
# # Nothing was filtered, return the original return values
# if len(passed_validation) + len(to_keep) == len(return_values):
# return return_values # nothing to filter
#
# # All return_values fail, return empty list
# if len(passed_validation) == 0:
# return sheerka.ret(
# context.who,
# True,
# sheerka.new(BuiltinConcepts.FILTERED,
# body=to_keep,
# iterable=return_values,
# predicate="remove_ambiguity(context, iterable)"),
# parents=return_values)
#
# # Final check, we consider that the concepts that match a PRE condition are better than concepts with no condition
# by_condition_complexity = {}
# for r in passed_validation:
# by_condition_complexity.setdefault(get_condition_complexity(r.body.body, "pre"), []).append(r)
#
# return sheerka.ret(
# context.who,
# True,
# sheerka.new(BuiltinConcepts.FILTERED,
# body=to_keep + by_condition_complexity[max(by_condition_complexity.keys())],
# iterable=return_values,
# predicate="remove_ambiguity(context, iterable)"),
# parents=return_values)
def resolve_ambiguity(context, concepts):
"""
From the list of concept, elect the one(s) that best suit(s) the context
:param context:
:param concepts:
:return:
"""
# we first sort by condition complexity. The more complex is the PRE condition, the more likely
# the concept matches the context
by_complexity = {}
for c in concepts:
by_complexity.setdefault(get_condition_complexity(c, "pre"), []).append(c)
remaining_concepts = []
for complexity in sorted(by_complexity.keys(), reverse=True):
for c in by_complexity[complexity]:
evaluated = context.sheerka.evaluate_concept(context, c, metadata=["pre"])
if evaluated.key == c.key:
remaining_concepts.append(c)
if len(remaining_concepts) > 0:
break # no need to check concept with lower complexity
if len(remaining_concepts) in (0, 1):
return remaining_concepts # they all failed the pre conditions or one champ is found
# for concepts with the same condition complexity, we choose the one that has the less number of variables
# We consider that Concept("hello world") is more specific than Concept("hello a").def_var("a")
# when the input is "hello world"
by_number_of_vars = {}
for c in remaining_concepts:
by_number_of_vars.setdefault(len(c.metadata.variables), []).append(c)
return by_number_of_vars[min(by_number_of_vars.keys())]
def get_condition_complexity(concept, concept_part_str):
concept_part_value = getattr(concept.metadata, concept_part_str)
if concept_part_value is None or concept_part_value.strip() == 0:
return 0
return 1 # no real computing as of now
def only_parsers_results(context, return_values):
"""
@@ -197,7 +317,8 @@ def only_parsers_results(context, return_values):
sheerka.new(BuiltinConcepts.IS_EMPTY, body=return_values),
parents=return_values)
return_values_ok = [item for item in return_values if sheerka.isinstance(item.body, BuiltinConcepts.PARSER_RESULT)]
return_values_ok = [item for item in return_values if
sheerka.isinstance(item.body, BuiltinConcepts.PARSER_RESULT)]
# hack because some parsers don't follow the NOT_FOR_ME rule
temp_ret_val = []
@@ -291,7 +412,8 @@ def get_lexer_nodes(return_values, start, tokens):
continue
end = start + len(tokens) - 1
lexer_nodes.append([SourceCodeNode(ret_val.body.body, start, end, tokens, ret_val.body.source, ret_val)])
lexer_nodes.append(
[SourceCodeNode(ret_val.body.body, start, end, tokens, ret_val.body.source, ret_val)])
elif ret_val.who == "parsers.ExactConcept":
concepts = ret_val.body.body if hasattr(ret_val.body.body, "__iter__") else [ret_val.body.body]
@@ -333,12 +455,7 @@ def ensure_evaluated(context, concept, eval_body=True):
(var[0] not in concept.values or concept.get_value(var[0]) == NotInit):
return concept
with context.push(BuiltinConcepts.EVALUATE_CONCEPT, concept, desc=f"Evaluating concept {concept}") as sub_context:
if eval_body:
sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
evaluated = context.sheerka.evaluate_concept(sub_context, concept)
sub_context.add_values(return_values=evaluated)
evaluated = context.sheerka.evaluate_concept(context, concept, eval_body=eval_body)
return evaluated
+45 -11
View File
@@ -1,5 +1,6 @@
import inspect
import logging
from dataclasses import dataclass
import core.builtin_helpers
import core.utils
@@ -22,6 +23,15 @@ BASE_NODE_PARSER_CLASS = "parsers.BaseNodeParser.BaseNodeParser"
EXIT_COMMANDS = ("quit", "exit", "bye")
@dataclass
class SheerkaMethod:
"""
Wrapper to sheerka method, to indicate if it's safe to call
"""
method: object
has_side_effect: bool
class Sheerka(Concept):
"""
Main controller for the project
@@ -55,6 +65,7 @@ class Sheerka(Concept):
self.log.debug("Starting Sheerka.")
self.bnp = None # reference to the BaseNodeParser class (to compute first keyword token)
self.return_value_concept_id = None
# a concept can be instantiated
# ex: File is a concept, but File('foo.txt') is an instance
@@ -84,10 +95,10 @@ class Sheerka(Concept):
self.save_execution_context = True
self.methods_with_context = {"test_using_context"}
self.methods_with_context = {"test_using_context"} # only the names, the method is defined in sheerka_methods
self.sheerka_methods = {
"test": self.test,
"test_using_context": self.test_using_context
"test": SheerkaMethod(self.test, False),
"test_using_context": SheerkaMethod(self.test_using_context, False)
}
self.sheerka_pipeables = {}
@@ -119,10 +130,11 @@ class Sheerka(Concept):
def chicken_and_eggs(self):
return self.cache_manager.caches[self.CONCEPTS_GRAMMARS_ENTRY].cache
def bind_service_method(self, bound_method, as_name=None):
def bind_service_method(self, bound_method, has_side_effect, as_name=None):
"""
Bind service method to sheerka instance for ease of use ?
:param bound_method:
:param has_side_effect: False if the method is safe
:param as_name:
:return:
"""
@@ -132,18 +144,19 @@ class Sheerka(Concept):
signature = inspect.signature(bound_method)
if len(signature.parameters) > 0 and list(signature.parameters.keys())[0] == "context":
self.methods_with_context.add(as_name)
self.sheerka_methods[as_name] = bound_method
self.sheerka_methods[as_name] = SheerkaMethod(bound_method, has_side_effect)
setattr(self, as_name, bound_method)
def add_pipeable(self, func_name, function):
def add_pipeable(self, func_name, function, has_side_effect):
"""
Adds a function that can bu used with pipe '|'
:param func_name:
:param function:
:param has_side_effect:
:return:
"""
self.sheerka_pipeables[func_name] = function
self.sheerka_pipeables[func_name] = SheerkaMethod(function, has_side_effect)
def initialize(self, root_folder: str = None, save_execution_context=True):
"""
@@ -292,6 +305,10 @@ class Sheerka(Concept):
self.init_log.debug(f"'{concept.name}' concept is not found in db. Adding.")
self.set_id_if_needed(concept, True)
self.cache_manager.add_concept(concept)
if key == BuiltinConcepts.RETURN_VALUE:
self.return_value_concept_id = concept.id
else:
self.init_log.debug(f"Found concept '{from_db}' in db. Updating.")
concept.update_from(from_db)
@@ -595,6 +612,8 @@ class Sheerka(Concept):
:param concept_id:
:return:
"""
if concept_id is None:
return False
return self.cache_manager.has(self.CONCEPTS_BY_ID_ENTRY, concept_id)
def has_key(self, concept_key):
@@ -652,6 +671,7 @@ class Sheerka(Concept):
return self.new_from_template(template, concept_key, **kwargs)
def new_from_template(self, template, key, **kwargs):
# core.utils.my_debug(f"Created {template}, {key=}, {kwargs=}")
# manage singleton
if template.metadata.is_unique:
return template
@@ -677,7 +697,7 @@ class Sheerka(Concept):
return self.new(BuiltinConcepts.UNKNOWN_PROPERTY, body=k, concept=concept)
# TODO : add the concept to the list of known concepts (self.instances)
concept.metadata.is_evaluated = True # because we have manually set the variables
concept.metadata.is_evaluated = True # because we have manually set the variables
return concept
def ret(self, who: str, status: bool, value, message=None, parents=None):
@@ -690,13 +710,23 @@ class Sheerka(Concept):
:param parents:
:return:
"""
return self.new(
BuiltinConcepts.RETURN_VALUE,
# 1 second saved every twenty seconds in unit tests
return ReturnValueConcept(
who=who,
status=status,
value=value,
message=message,
parents=parents)
parents=parents,
concept_id=self.return_value_concept_id
)
# return self.new(
# BuiltinConcepts.RETURN_VALUE,
# who=who,
# status=status,
# value=value,
# message=message,
# parents=parents)
def objvalue(self, obj, reduce_simple_list=False):
if obj is None:
@@ -812,6 +842,10 @@ class Sheerka(Concept):
if isinstance(obj, ReturnValueConcept):
return obj.status
# other cases ?
# ...
# manage internal errors
if isinstance(obj, Concept) and obj.metadata.is_builtin and obj.key in BuiltinErrors:
return False
+5 -5
View File
@@ -14,11 +14,11 @@ class SheerkaAdmin(BaseService):
super().__init__(sheerka)
def initialize(self):
self.sheerka.bind_service_method(self.caches_names)
self.sheerka.bind_service_method(self.cache)
self.sheerka.bind_service_method(self.restore)
self.sheerka.bind_service_method(self.concepts)
self.sheerka.bind_service_method(self.last_created_concept)
self.sheerka.bind_service_method(self.caches_names, False)
self.sheerka.bind_service_method(self.cache, False)
self.sheerka.bind_service_method(self.restore, True)
self.sheerka.bind_service_method(self.concepts, False)
self.sheerka.bind_service_method(self.last_created_concept, False)
def caches_names(self):
"""
@@ -90,10 +90,10 @@ class SheerkaComparisonManager(BaseService):
cache = Cache()
self.sheerka.cache_manager.register_cache(self.RESOLVED_COMPARISON_ENTRY, cache, persist=False)
self.sheerka.bind_service_method(self.set_is_greater_than)
self.sheerka.bind_service_method(self.set_is_less_than)
self.sheerka.bind_service_method(self.get_partition)
self.sheerka.bind_service_method(self.get_concepts_weights)
self.sheerka.bind_service_method(self.set_is_greater_than, True)
self.sheerka.bind_service_method(self.set_is_less_than, True)
self.sheerka.bind_service_method(self.get_partition, False)
self.sheerka.bind_service_method(self.get_concepts_weights, False)
def set_is_greater_than(self, context, prop_name, concept_a, concept_b, comparison_context="#"):
"""
@@ -20,7 +20,7 @@ class SheerkaCreateNewConcept(BaseService):
self.bnp = core.utils.get_class(BASE_NODE_PARSER_CLASS) # BaseNodeParser
def initialize(self):
self.sheerka.bind_service_method(self.create_new_concept)
self.sheerka.bind_service_method(self.create_new_concept, True)
def create_new_concept(self, context, concept: Concept):
"""
+3 -3
View File
@@ -21,8 +21,8 @@ class SheerkaDump(BaseService):
super().__init__(sheerka)
def initialize(self):
self.sheerka.bind_service_method(self.dump_desc, "desc")
self.sheerka.bind_service_method(self.dump_sdp, "dump_sdp")
self.sheerka.bind_service_method(self.dump_desc, True, "desc") # because concept is evaluated
self.sheerka.bind_service_method(self.dump_sdp, False, "dump_sdp")
def dump_desc(self, *concept_names, eval=False):
first = True
@@ -42,7 +42,7 @@ class SheerkaDump(BaseService):
for c in concepts:
if eval:
evaluated = self.sheerka.evaluate_concept(context, c)
evaluated = self.sheerka.evaluate_concept(context, c, eval_body=eval)
value = evaluated.body if evaluated.key == c.key else evaluated
if not first:
@@ -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
+3 -2
View File
@@ -144,7 +144,7 @@ class SheerkaExecute(BaseService):
self.pi_cache = Cache(default=lambda key: ParserInput(key), max_size=20)
def initialize(self):
self.sheerka.bind_service_method(self.execute)
self.sheerka.bind_service_method(self.execute, True)
self.sheerka.cache_manager.register_cache(self.PARSERS_INPUTS_ENTRY, self.pi_cache, False)
@@ -362,7 +362,8 @@ class SheerkaExecute(BaseService):
if not isinstance(results, list):
results = [results]
for result in results:
evaluated_items.append(result)
if result.body:
evaluated_items.append(result)
to_delete.extend(result.parents)
sub_context.add_values(return_values=results)
else:
+2 -2
View File
@@ -96,9 +96,9 @@ class SheerkaFilter(BaseService):
for k, v in SheerkaFilter.__dict__.items():
if k.startswith("pipe_"):
if isinstance(v, staticmethod):
self.sheerka.add_pipeable(k[5:], v.__func__)
self.sheerka.add_pipeable(k[5:], v.__func__, True)
else:
self.sheerka.add_pipeable(k[5:], v.__get__(self, self.__class__))
self.sheerka.add_pipeable(k[5:], v.__get__(self, self.__class__), True)
self.sheerka.cache_manager.register_cache(self.PREDICATES_ENTRY, self.cache, False, False)
@@ -57,7 +57,7 @@ class SheerkaHistoryManager(BaseService):
super().__init__(sheerka)
def initialize(self):
self.sheerka.bind_service_method(self.history)
self.sheerka.bind_service_method(self.history, False)
def history(self, depth=10, start=0):
"""
@@ -10,7 +10,7 @@ class SheerkaModifyConcept(BaseService):
super().__init__(sheerka)
def initialize(self):
self.sheerka.bind_service_method(self.modify_concept)
self.sheerka.bind_service_method(self.modify_concept, True)
def modify_concept(self, context, concept):
old_version = self.sheerka.get_by_id(concept.id)
@@ -10,10 +10,10 @@ class SheerkaResultConcept(BaseService):
self.page_size = page_size
def initialize(self):
self.sheerka.bind_service_method(self.get_results_by_digest)
self.sheerka.bind_service_method(self.get_results_by_command)
self.sheerka.bind_service_method(self.get_last_results)
self.sheerka.bind_service_method(self.get_results)
self.sheerka.bind_service_method(self.get_results_by_digest, True) # digest is recorded
self.sheerka.bind_service_method(self.get_results_by_command, True) # digest is recorded
self.sheerka.bind_service_method(self.get_last_results, True) # digest is recorded
self.sheerka.bind_service_method(self.get_results, False)
def get_results_by_digest(self, context, digest, record_digest=True):
"""
@@ -13,7 +13,7 @@ GROUP_PREFIX = 'All_'
class SheerkaSetsManager(BaseService):
NAME = "SetsManager"
CONCEPTS_GROUPS_ENTRY = "SetsManager:Concepts_Groups"
CONCEPTS_IN_GROUPS_ENTRY = "SetsManager:Concepts_In_Groups" # cache for get_set_elements()
CONCEPTS_IN_GROUPS_ENTRY = "SetsManager:Concepts_In_Groups" # cache for get_set_elements()
def __init__(self, sheerka):
super().__init__(sheerka)
@@ -21,12 +21,12 @@ class SheerkaSetsManager(BaseService):
self.concepts_in_set = Cache()
def initialize(self):
self.sheerka.bind_service_method(self.set_isa)
self.sheerka.bind_service_method(self.get_set_elements)
self.sheerka.bind_service_method(self.add_concept_to_set)
self.sheerka.bind_service_method(self.isinset)
self.sheerka.bind_service_method(self.isa)
self.sheerka.bind_service_method(self.isaset)
self.sheerka.bind_service_method(self.set_isa, True)
self.sheerka.bind_service_method(self.get_set_elements, True) # concepts are evaluated
self.sheerka.bind_service_method(self.add_concept_to_set, True)
self.sheerka.bind_service_method(self.isinset, False)
self.sheerka.bind_service_method(self.isa, False)
self.sheerka.bind_service_method(self.isaset, True) # concept is evaluated, need to change the code
self.sheerka.cache_manager.register_cache(self.CONCEPTS_GROUPS_ENTRY, self.sets)
self.sheerka.cache_manager.register_cache(self.CONCEPTS_IN_GROUPS_ENTRY, self.concepts_in_set, persist=False)
@@ -49,6 +49,8 @@ class SheerkaSetsManager(BaseService):
False,
self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET, body=concept, concept_set=concept_set))
# 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
concept.add_prop(BuiltinConcepts.ISA, concept_set)
res = self.sheerka.modify_concept(context, concept)
@@ -141,7 +143,7 @@ class SheerkaSetsManager(BaseService):
if sub_concept.metadata.where:
new_condition = self._validate_where_clause(sub_concept)
if not new_condition:
return self.sheerka.new(BuiltinConcepts.WHERE_CLAUSE_FAILED, body=sub_concept)
return self.sheerka.new(BuiltinConcepts.CONDITION_FAILED, body=sub_concept)
# This methods sucks, but I don't have enough tools (like proper AST manipulation functions)
# to do it properly now. It will be enhanced later
@@ -206,7 +208,7 @@ class SheerkaSetsManager(BaseService):
if not (isinstance(concept, Concept) and concept.id):
return False
# KSI 29062020
# KSI 20200629
# To resolve infinite recursion between group concepts and BNF concepts
if concept.metadata.definition_type == DEFINITION_TYPE_BNF:
return False
@@ -27,11 +27,11 @@ class SheerkaVariableManager(BaseService):
super().__init__(sheerka)
def initialize(self):
self.sheerka.bind_service_method(self.record)
self.sheerka.bind_service_method(self.load)
self.sheerka.bind_service_method(self.delete)
self.sheerka.bind_service_method(self.set)
self.sheerka.bind_service_method(self.get)
self.sheerka.bind_service_method(self.record, True)
self.sheerka.bind_service_method(self.load, False)
self.sheerka.bind_service_method(self.delete, True)
self.sheerka.bind_service_method(self.set, True)
self.sheerka.bind_service_method(self.get, False)
cache = Cache(default=lambda k: self.sheerka.sdp.get(self.VARIABLES_ENTRY, k))
self.sheerka.cache_manager.register_cache(self.VARIABLES_ENTRY, cache, True, True)
+5
View File
@@ -487,6 +487,11 @@ class Tokenizer:
return result, lines_count, column_index
def eat_word(self, start):
"""
Word is an alphanum (no space)
:param start:
:return:
"""
result = self.text[start]
i = start + 1
while i < self.text_len: