Files
Sheerka-Old/src/evaluators/PythonEvaluator.py
T
kodjo 7dcaa9c111 Fixed #29: Parsers: Implement parsing memoization
Fixed #77 : Parser: ShortTermMemoryParser should be called separately
Fixed #78 : Remove VariableNode usage
Fixed #79 : ConceptManager: Implement compile caching
Fixed #80 : SheerkaExecute : parsers_key is not correctly computed
Fixed #81 : ValidateConceptEvaluator : Validate concept's where and pre clauses right after the parsing
Fixed #82 : SheerkaIsAManager: isa() failed when the set as a body
Fixed #83 : ValidateConceptEvaluator : Support BNF and SYA Concepts
Fixed #84 : ExpressionParser: Implement the parser as a standard parser
Fixed #85 : Services: Give order to services
Fixed #86 : cannot manage smart_get_attr(the short, color)
2021-06-07 21:14:03 +02:00

246 lines
10 KiB
Python

import ast
import copy
import traceback
from dataclasses import dataclass, field
import core.builtin_helpers
import core.utils
from core.ast_helpers import UnreferencedNamesVisitor, NamesWithAttributesVisitor
from core.builtin_concepts import BuiltinConcepts, ParserResultConcept
from core.concept import ConceptParts, Concept
from core.global_symbols import NotInit, ErrorObj
from core.sheerka.services.SheerkaMemory import SheerkaMemory
from evaluators.BaseEvaluator import OneReturnValueEvaluator
from parsers.PythonParser import PythonNode
from sheerkapython.python_wrapper import create_namespace, MethodAccessError
@dataclass
class PythonEvalError(ErrorObj):
error: Exception
source: str
traceback: str = field(repr=False)
concepts: dict = field(repr=False)
def __eq__(self, other):
if id(self) == id(other):
return True
if not isinstance(other, PythonEvalError):
return False
return isinstance(self.error, type(other.error)) and \
self.source == other.source and \
self.traceback == other.traceback and \
self.concepts == other.concepts
def __hash__(self):
return hash(self.error)
def get_error(self):
return self.error
class PythonEvaluator(OneReturnValueEvaluator):
NAME = "Python"
"""
Evaluate a Python node, ie, evaluate some Python code
"""
isinstance = None
def __init__(self):
super().__init__(self.NAME, [BuiltinConcepts.EVALUATION], 50)
self.isinstance = None
@staticmethod
def initialize(sheerka):
from core.sheerka.services.SheerkaAdmin import SheerkaAdmin
PythonEvaluator.isinstance = sheerka.services[SheerkaAdmin.NAME].extended_isinstance
def matches(self, context, return_value):
if not return_value.status or not isinstance(return_value.value, ParserResultConcept):
return False
body = return_value.value.value
return isinstance(body, PythonNode) or hasattr(body, "python_node")
def eval(self, context, return_value):
sheerka = context.sheerka
node = return_value.value.value.get_python_node()
debugger = context.get_debugger(PythonEvaluator.NAME, "eval")
debugger.debug_entering(node=node)
exception_debugger = context.get_debugger("Exceptions", PythonEvaluator.NAME + "-eval")
get_trace_back = exception_debugger.is_enabled()
context.log(f"Evaluating python node {node}.", self.name)
# If we evaluate a Concept metadata which is NOT the body ex (pre, post, where...)
# or if EVAL_QUESTION_REQUESTED is explicit
# We need to disable the functions that may alter the state
# It's a poor way to have source code security check
expression_only = context.in_context(BuiltinConcepts.EVAL_QUESTION_REQUESTED) or \
context.in_context(BuiltinConcepts.VALIDATION_ONLY_REQUESTED)
if not expression_only:
attr_under_eval = context.get_parents(lambda ec: ec.action == BuiltinConcepts.EVALUATING_ATTRIBUTE)
if attr_under_eval:
attr_under_eval = attr_under_eval[0]
expression_only = attr_under_eval.action_context != ConceptParts.BODY
# get globals
try:
my_globals = self.get_globals(context, node, expression_only)
debugger.debug_var("globals", my_globals)
except MethodAccessError as ex:
# Quick and dirty,
# When VALIDATION_ONLY_REQUESTED is enabled, it's normal to have some NameError exceptions
if context.in_context(BuiltinConcepts.VALIDATION_ONLY_REQUESTED):
return sheerka.ret(self.name, False, BuiltinConcepts.METHOD_ACCESS_ERROR, parents=[return_value])
eval_error = PythonEvalError(ex,
node.source,
traceback.format_exc() if get_trace_back else None,
None)
return sheerka.ret(self.name, False, sheerka.err(eval_error), parents=[return_value])
all_possible_globals = self.get_all_possible_globals(context, my_globals)
concepts_entries = None # entries in globals_ that refers to Concept objects
evaluated = NotInit
errors = []
expect_success = context.in_context(BuiltinConcepts.EVAL_UNTIL_SUCCESS_REQUESTED)
for globals_ in all_possible_globals:
try:
# eval
my_locals = {}
if isinstance(node.ast_, ast.Expression):
context.log("Evaluating using 'eval'.", self.name)
evaluated = eval(node.get_compiled(), globals_, my_locals)
else:
context.log("Evaluating using 'exec'.", self.name)
evaluated = self.exec_with_return(node.ast_, globals_, my_locals)
for k, v in my_locals.items():
sheerka.services[SheerkaMemory.NAME].add_to_memory(context, k, v)
if not expect_success or evaluated:
break # in this first version, we stop once a success is found
except Exception as ex:
if concepts_entries is None:
concepts_entries = self.get_concepts_entries_from_globals(my_globals)
eval_error = PythonEvalError(ex,
node.source,
traceback.format_exc() if get_trace_back else None,
self.get_concepts_values_from_globals(globals_, concepts_entries))
errors.append(eval_error)
exception_debugger.debug_var("exception", eval_error.error, is_error=True)
exception_debugger.debug_var("source", eval_error.source, is_error=True)
exception_debugger.debug_var("trace", eval_error.traceback, is_error=True)
if evaluated == NotInit:
if len(errors) == 1:
context.log_error(errors[0].error, who=self.name, exc=errors[0].traceback)
return sheerka.ret(self.name, False, sheerka.err(errors[0]), parents=[return_value])
if len(errors) > 1:
for eval_error in errors:
context.log_error(eval_error.error, who=self.name, exc=eval_error.traceback)
return sheerka.ret(self.name, False, sheerka.err(errors), parents=[return_value])
context.log(f"{evaluated=}", self.name)
debugger.debug_var("ret", evaluated)
if sheerka.isinstance(evaluated, BuiltinConcepts.RETURN_VALUE):
return sheerka.ret(self.name, evaluated.status, evaluated.body, parents=[return_value, evaluated])
else:
return sheerka.ret(self.name, True, evaluated, parents=[return_value])
def get_globals(self, context, node, expression_only):
"""
Creates the globals variables
:param context:
:param node:
:param expression_only:
:return:
"""
unreferenced_names_visitor = UnreferencedNamesVisitor(context)
names = unreferenced_names_visitor.get_names(node.ast_)
if "sheerka" in names:
sheerka_names = set()
visitor = NamesWithAttributesVisitor()
for sequence in visitor.get_sequences(node.ast_, "sheerka"):
if len(sequence) > 1:
sheerka_names.add(sequence[1])
else:
sheerka_names = None
return create_namespace(context, self.name, names, sheerka_names, node.objects, expression_only)
@staticmethod
def get_all_possible_globals(context, my_globals):
"""
From a dictionary of globals (str, obj)
Creates as many globals as there are combination between a concept and its body
Example:
if the entry 'foo': Concept("foo", body="something")
2 globals will be created
one with foo: Concept("foo") # we keep the concept as an object
one with foo: 'something' # we substitute its value
:param context:
:param my_globals:
:return:
"""
# first pass, get all the non concepts or concepts with no body
# Note that we consider that all concepts are evaluated
# In the future, it may be a good optimisation to defer the evaluation of the body
# until the python evaluation fails
fixed_values = {}
concepts_with_body = {}
for k, v in my_globals.items():
if not isinstance(v, Concept) or context.sheerka.objvalue(v) == v:
fixed_values[k] = v
else:
concepts_with_body[k] = v
# make the product the rest as cartesian product
res = [fixed_values]
for k, v in concepts_with_body.items():
res = core.utils.dict_product(res, [{k: v}, {k: context.sheerka.objvalue(v)}])
return res
@staticmethod
def get_concepts_entries_from_globals(my_globals):
return [k for k, v in my_globals.items() if isinstance(v, Concept)]
@staticmethod
def get_concepts_values_from_globals(my_globals, names):
return {name: my_globals[name] for name in names}
@staticmethod
def expr_to_expression(expr):
expr.lineno = 0
expr.col_offset = 0
result = ast.Expression(expr.value, lineno=0, col_offset=0)
return result
@staticmethod
def exec_with_return(code_ast, my_globals, my_locals):
init_ast = copy.deepcopy(code_ast)
init_ast.body = code_ast.body[:-1]
last_ast = copy.deepcopy(code_ast)
last_ast.body = code_ast.body[-1:]
exec(compile(init_ast, "<ast>", "exec"), my_globals, my_locals)
if type(last_ast.body[0]) == ast.Expr:
return eval(compile(PythonEvaluator.expr_to_expression(last_ast.body[0]), "<ast>", "eval"),
my_globals,
my_locals)
else:
exec(compile(last_ast, "<ast>", "exec"), my_globals, my_locals)