646c428edb
Fixed #31 : Add regex support in BNF Concept Fixed #33 : Do not memorize object during restore
229 lines
9.5 KiB
Python
229 lines
9.5 KiB
Python
from dataclasses import dataclass
|
|
|
|
import core.utils
|
|
from core.ast_helpers import UnreferencedVariablesVisitor
|
|
from core.builtin_concepts import ParserResultConcept, ReturnValueConcept, BuiltinConcepts
|
|
from core.concept import Concept, DEFINITION_TYPE_BNF, DEFINITION_TYPE_DEF
|
|
from core.global_symbols import NotInit
|
|
from core.sheerka.services.SheerkaExecute import ParserInput
|
|
from core.tokenizer import TokenKind, Tokenizer
|
|
from evaluators.BaseEvaluator import OneReturnValueEvaluator
|
|
from parsers.BnfNodeParser import ParsingExpression, ParsingExpressionVisitor
|
|
from parsers.DefConceptParser import DefConceptNode, NameNode
|
|
from parsers.PythonParser import get_python_node
|
|
|
|
|
|
@dataclass(eq=True, frozen=True)
|
|
class MandatoryVariable:
|
|
"""
|
|
When we are searching for variables, we are searching for potential variable
|
|
So if the variable found has no match in the concept definition, it's not a problem
|
|
for example:
|
|
def concept foo x as isinstance(x, str)
|
|
{x, str} will be detected as potential variable, but 'str' will find no match.
|
|
|
|
But there are cases where the variable found must exist, otherwise, it's an error
|
|
example:
|
|
def concept foo from bnf xxx
|
|
'xxx' is detected as a variable (assuming that there is no concept named 'xxx' and a match must be
|
|
found in the the name of the variable
|
|
|
|
To distinguish between mandatory and not mandatory variable, we use MandatoryVariable
|
|
"""
|
|
name: str
|
|
|
|
def __hash__(self):
|
|
return hash(("MandatoryVariable", self.name))
|
|
|
|
|
|
class ConceptOrRuleNameVisitor(ParsingExpressionVisitor):
|
|
"""
|
|
Gets the concepts referenced by BNF
|
|
If a rule_name is given, it will also be considered as a potential property
|
|
"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.names = set()
|
|
|
|
def visit_ConceptExpression(self, node):
|
|
if node.rule_name:
|
|
self.names.add(node.rule_name)
|
|
elif isinstance(node.concept, Concept):
|
|
self.names.add(node.concept.name)
|
|
else:
|
|
self.names.add(node.concept)
|
|
|
|
def visit_VariableExpression(self, node):
|
|
self.names.add(MandatoryVariable(node.rule_name))
|
|
|
|
def visit_all(self, node):
|
|
if node.rule_name:
|
|
self.names.add(node.rule_name)
|
|
|
|
|
|
class DefConceptEvaluator(OneReturnValueEvaluator):
|
|
"""
|
|
Used to add a new concept
|
|
"""
|
|
NAME = "DefConcept"
|
|
|
|
def __init__(self):
|
|
super().__init__(self.NAME, [BuiltinConcepts.EVALUATION], 50)
|
|
|
|
def matches(self, context, return_value):
|
|
debugger = context.get_debugger(self.NAME, "matches")
|
|
debugger.debug_entering(return_value=return_value)
|
|
return return_value.status and \
|
|
isinstance(return_value.value, ParserResultConcept) and \
|
|
isinstance(return_value.value.value, DefConceptNode)
|
|
|
|
def eval(self, context, return_value):
|
|
context.log("Adding a new concept", self.name)
|
|
def_concept_node = return_value.value.value
|
|
sheerka = context.sheerka
|
|
|
|
debugger = context.get_debugger(self.NAME, "eval")
|
|
debugger.debug_entering(def_concept=def_concept_node)
|
|
|
|
# validate the node
|
|
variables_found = set()
|
|
mandatory_variables = set() # these variable MUST have a match in the name (if the name is not None)
|
|
|
|
concept = Concept(str(def_concept_node.name))
|
|
concept.get_metadata().definition_type = def_concept_node.definition_type
|
|
name_to_use = self.get_name_to_use(def_concept_node)
|
|
|
|
# get variables
|
|
for prop in ("definition", "where", "pre", "post", "body", "ret"):
|
|
|
|
part_ret_val = getattr(def_concept_node, prop)
|
|
|
|
# put back the sources
|
|
if part_ret_val is NotInit:
|
|
continue
|
|
elif isinstance(part_ret_val, NameNode):
|
|
source = str(part_ret_val)
|
|
elif isinstance(part_ret_val, ReturnValueConcept) and part_ret_val.status:
|
|
source = part_ret_val.value.source.as_text() if isinstance(part_ret_val.value.source,
|
|
ParserInput) else part_ret_val.value.source
|
|
else:
|
|
raise Exception("Unexpected")
|
|
setattr(concept.get_metadata(), prop, source)
|
|
|
|
# Do not try to resolve variables from itself
|
|
if prop == "definition" and concept.get_metadata().definition_type == DEFINITION_TYPE_DEF:
|
|
continue
|
|
|
|
# try to find what can be a property
|
|
for p in self.get_variables(context, part_ret_val, name_to_use):
|
|
if isinstance(p, MandatoryVariable):
|
|
variables_found.add(p.name)
|
|
mandatory_variables.add(p.name)
|
|
else:
|
|
variables_found.add(p)
|
|
|
|
# add variables by order of appearance when possible
|
|
for name_part in name_to_use:
|
|
if name_part in variables_found:
|
|
concept.def_var(name_part, None)
|
|
|
|
# check that all mandatory variables are defined in the name
|
|
# KSI: 2021-02-17
|
|
# The mandatory variables come for bnf definition where it was not possible to resolve to a concept
|
|
# So rather that issuing a 'UnresolvedVariableError' I prefer UNKNOWN_CONCEPT
|
|
if (diff := mandatory_variables.difference(set(name_to_use))) != set():
|
|
unknown_concepts = [sheerka.new(BuiltinConcepts.UNKNOWN_CONCEPT, body={"name": c}) for c in sorted(diff)]
|
|
error = sheerka.new(BuiltinConcepts.ERROR, body=unknown_concepts)
|
|
return sheerka.ret(self.name, False, error, parents=[return_value])
|
|
|
|
# add the remaining properties
|
|
# They mainly come from BNF definition
|
|
for p in variables_found:
|
|
if p not in concept.values():
|
|
concept.def_var(p, None)
|
|
|
|
# initialize the key
|
|
key_source = def_concept_node.definition.tokens if \
|
|
def_concept_node.definition_type == DEFINITION_TYPE_DEF else \
|
|
def_concept_node.name.tokens
|
|
concept.init_key(key_source)
|
|
|
|
# update the bnf definition if needed
|
|
if def_concept_node.definition is not NotInit and \
|
|
def_concept_node.definition_type == DEFINITION_TYPE_BNF:
|
|
concept.set_bnf(def_concept_node.definition.value.value)
|
|
|
|
ret = sheerka.create_new_concept(context, concept)
|
|
if not ret.status:
|
|
error_cause = sheerka.objvalue(ret.body)
|
|
context.log(f"Failed to add concept '{concept.name}'. Reason: {error_cause}", self.name)
|
|
return sheerka.ret(self.name, ret.status, ret.value, parents=[return_value])
|
|
|
|
@staticmethod
|
|
def get_name_to_use(node):
|
|
source = node.definition if node.definition_type == DEFINITION_TYPE_DEF else node.name
|
|
return [part.str_value for part in core.utils.strip_tokens(source.tokens, True)]
|
|
|
|
@staticmethod
|
|
def get_variables(context, ret_value, concept_name):
|
|
"""
|
|
Try to find out the variables
|
|
This function can only be a draft, as there may be tons of different situations
|
|
I guess that it can only be complete when will we have access to Sheerka memory
|
|
"""
|
|
debugger = context.get_debugger(DefConceptEvaluator.NAME, "get_variables")
|
|
#
|
|
# Case of NameNode
|
|
#
|
|
if isinstance(ret_value, NameNode):
|
|
names = [str(t.value) for t in ret_value.tokens if t.type in (
|
|
TokenKind.IDENTIFIER, TokenKind.STRING, TokenKind.KEYWORD)]
|
|
debugger.debug_var("names", names, hint="from NameNode")
|
|
return set(filter(lambda x: x in concept_name and context.sheerka.is_not_a_variable(x), names))
|
|
|
|
#
|
|
# case of BNF
|
|
#
|
|
if isinstance(ret_value.value, ParserResultConcept) and isinstance(ret_value.value.value, ParsingExpression):
|
|
visitor = ConceptOrRuleNameVisitor()
|
|
visitor.visit(ret_value.value.value)
|
|
debugger.debug_var("names", visitor.names, hint="from BNF")
|
|
return set(visitor.names)
|
|
|
|
#
|
|
# Case of python code
|
|
#
|
|
if (python_node := get_python_node(ret_value.value.value)) is not None:
|
|
if len(concept_name) > 1:
|
|
visitor = UnreferencedVariablesVisitor(context)
|
|
names = visitor.get_names(python_node.ast_)
|
|
debugger.debug_var("names", names, hint="from python node")
|
|
return set(filter(lambda x: x in concept_name and context.sheerka.is_not_a_variable(x), names))
|
|
else:
|
|
return set()
|
|
|
|
#
|
|
# Concept
|
|
#
|
|
if isinstance(ret_value.value, ParserResultConcept) and len(concept_name) > 1:
|
|
variables = set()
|
|
source = ret_value.value.source.as_text() if isinstance(ret_value.value.source,
|
|
ParserInput) else ret_value.value.source
|
|
tokens = ret_value.value.tokens or list(Tokenizer(source, yield_eof=False))
|
|
possible_vars = set()
|
|
for t in tokens:
|
|
if t.type == TokenKind.RULE:
|
|
for v in [v for v in t.value if v is not None]:
|
|
possible_vars.add(v)
|
|
else:
|
|
possible_vars.add(t.str_value)
|
|
|
|
for identifier in [i for i in concept_name if str(i).isalnum()]:
|
|
if identifier in possible_vars:
|
|
variables.add(identifier)
|
|
debugger.debug_var("names", variables, hint="from concept")
|
|
return variables
|
|
|
|
return set()
|