muMLE/transformation/schedule/rule_scheduler.py

338 lines
13 KiB
Python

from __future__ import annotations
import importlib.util
import io
import os
import re
import sys
from pathlib import Path
from time import time
from typing import cast, TYPE_CHECKING
from jinja2 import FileSystemLoader, Environment
from concrete_syntax.textual_od import parser as parser_od
from concrete_syntax.textual_cd import parser as parser_cd
from api.od import ODAPI
from bootstrap.scd import bootstrap_scd
from transformation.schedule.rule_executor import RuleExecutor
from transformation.schedule.generator import schedule_generator
from transformation.schedule.models.eval_context import mm_eval_context
from transformation.schedule.schedule_lib import ExecNode, Start
from framework.conformance import Conformance, render_conformance_check_result, eval_context_decorator
from state.devstate import DevState
from examples.petrinet.renderer import render_petri_net_to_dot
from drawio2py import parser
from drawio2py.abstract_syntax import DrawIOFile, Edge, Vertex, Cell
from icecream import ic
from transformation.schedule.schedule_lib.funcs import IdGenerator
if TYPE_CHECKING:
from transformation.schedule.schedule import Schedule
class RuleSchedular:
__slots__ = (
"rule_executor",
"schedule_main",
"loaded",
"out",
"verbose",
"conformance",
"directory",
"eval_context",
"_state",
"_mmm_cs",
"sub_schedules",
"end_time",
)
def __init__(
self,
state,
mm_rt,
mm_rt_ramified,
*,
outstream=sys.stdout,
verbose: bool = False,
conformance: bool = True,
directory: str = "",
eval_context: dict[str, any] = None,
):
self.rule_executor: RuleExecutor = RuleExecutor(state, mm_rt, mm_rt_ramified)
self.schedule_main: Schedule | None = None
self.out = outstream
self.verbose: bool = verbose
self.conformance: bool = conformance
self.directory: Path = Path.cwd() / directory
if eval_context is None:
eval_context = {}
self.eval_context: dict[str, any] = eval_context
self.loaded: dict[str, dict[str, any]] = {"od": {}, "py": {}, "drawio": {}, "rules": {}}
self._state = DevState()
self._mmm_cs = bootstrap_scd(self._state)
self.end_time = float("inf")
self.sub_schedules = float("inf")
def load_schedule(self, filename):
return self._load_schedule(filename, _main=True) is not None
def _load_schedule(self, filename: str, *, _main = True) -> Schedule | None:
if filename.endswith(".drawio"):
if (filename := self._generate_schedule_drawio(filename)) is None:
return None
if filename.endswith(".od"):
if (filename := self._generate_schedule_od(filename)) is None:
return None
if filename.endswith(".py"):
s = self._load_schedule_py(filename, _main=_main)
return s
raise Exception(f"Error unknown file: {filename}")
def _load_schedule_py(self, filename: str, *, _main = True) -> "Schedule":
if (s:= self.loaded["py"].get(filename, None)) is not None:
return s
spec = importlib.util.spec_from_file_location(filename, str(self.directory / filename))
schedule_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(schedule_module)
self.loaded["py"][filename] = (s:= schedule_module.Schedule())
if _main:
self.schedule_main = s
self.load_matchers(s)
return s
def _generate_schedule_od(self, filename: str) -> str | None:
if (s:= self.loaded.get(("od", filename), None)) is not None:
return s
file = str(self.directory / filename)
self._print("Generating schedule ...")
with open(f"{os.path.dirname(__file__)}/models/scheduling_MM.od", "r") as f_MM:
mm_cs = f_MM.read()
try:
with open(file, "r") as f_M:
m_cs = f_M.read()
except FileNotFoundError:
self._print(f"File not found: {file}")
return None
self._print("OK\n\nParsing models\n\tParsing meta model")
try:
scheduling_mm = parser_cd.parse_cd(
self._state,
m_text=mm_cs,
)
except Exception as e:
self._print(
f"Error while parsing meta-model: scheduling_MM.od\n\t{e}"
)
return None
self._print(f"\tParsing '{filename}' model")
try:
scheduling_m = parser_od.parse_od(
self._state, m_text=m_cs, mm=scheduling_mm
)
except Exception as e:
self._print(f"\033[91mError while parsing model: {filename}\n\t{e}\033[0m")
return None
if self.conformance:
success = True
self._print("OK\n\tmeta-meta-model a valid class diagram")
conf_err = Conformance(
self._state, self._mmm_cs, self._mmm_cs
).check_nominal()
b = len(conf_err)
success = success and not b
self._print(
f"\t\t{'\033[91m' if b else ''}{render_conformance_check_result(conf_err)}{'\033[0m' if b else ''}"
)
self._print(
f"Is our '{filename}' model a valid 'scheduling_MM.od' diagram?"
)
conf_err = Conformance(
self._state, scheduling_m, scheduling_mm, eval_context=mm_eval_context
).check_nominal()
b = len(conf_err)
success = success and not b
self._print(
f"\t\t{'\033[91m' if b else ''}{render_conformance_check_result(conf_err)}{'\033[0m' if b else ''}"
)
if not success:
return None
od = ODAPI(self._state, scheduling_m, scheduling_mm)
g = schedule_generator(od)
output_buffer = io.StringIO()
g.generate_schedule(output_buffer)
outfilename = f"{".".join(filename.split(".")[:-1])}.py"
open(self.directory / outfilename, "w", encoding='utf-8').write(output_buffer.getvalue())
self._print("Schedule generated")
self.loaded[("od", filename)] = outfilename
return outfilename
def _print(self, *args) -> None:
if self.verbose:
print(*args, file=self.out)
def load_matchers(self, schedule: "Schedule") -> None:
matchers = dict()
for file in schedule.get_matchers():
if (r:= self.loaded.get(("rule", file), None)) is None:
self.loaded[("rule", file)] = (r:= self.rule_executor.load_match(self.directory / file))
matchers[file] = r
schedule.init_schedule(self, self.rule_executor, matchers)
def generate_dot(self, file: str) -> None:
env = Environment(
loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), "templates")
)
)
env.trim_blocks = True
env.lstrip_blocks = True
template_dot = env.get_template("schedule_dot.j2")
nodes = []
edges = []
visit = set()
for schedule in self.loaded["py"].values():
schedule.generate_dot(nodes, edges, visit, template_dot)
with open(self.directory / file, "w") as f_dot:
f_dot.write(template_dot.render(nodes=nodes, edges=edges))
def run(self, model) -> tuple[int, str]:
self._print("Start simulation")
if 'pydevd' in sys.modules:
self.end_time = time() + 1000
else:
self.end_time = time() + 10000
return self._runner(model, self.schedule_main, "out", IdGenerator.generate_exec_id(), {})
def _runner(self, model, schedule: Schedule, exec_port: str, exec_id: int, data: dict[str, any]) -> tuple[int, any]:
self._generate_stackframe(schedule, exec_id)
cur_node = schedule.start
cur_node.run_init(exec_port, exec_id, data)
while self.end_time > time():
cur_node, port = cur_node.nextState(exec_id)
termination_reason = cur_node.execute(port, exec_id, model)
if termination_reason is not None:
self._delete_stackframe(schedule, exec_id)
return termination_reason
self._delete_stackframe(schedule, exec_id)
return -1, "limit reached"
def _generate_stackframe(self, schedule: Schedule, exec_id: int) -> None:
for node in schedule.nodes:
node.generate_stack_frame(exec_id)
def _delete_stackframe(self, schedule: Schedule, exec_id: int) -> None:
for node in schedule.nodes:
node.delete_stack_frame(exec_id)
def _generate_schedule_drawio(self, filename:str) -> str | None:
if (s:= self.loaded["drawio"].get(filename, None)) is not None:
return s
env = Environment(
loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), "templates")
)
)
env.trim_blocks = True
env.lstrip_blocks = True
template = env.get_template("schedule_muMLE.j2")
main: bool = False
node_map: dict[str, list[str | dict[str,str]]]
id_counter: int
def _get_node_id_map(elem: Cell) -> list[str | dict[str,str]]:
nonlocal node_map, id_counter
if (e_id := node_map.get(elem.id, None)) is None:
e_id = [f"{re.sub(r'[^a-zA-Z1-9_]', '', elem.properties["name"])}_{id_counter}", {}]
id_counter += 1
node_map[elem.id] = e_id
return e_id
edges: list[tuple[tuple[str, str, str, str], tuple[str,str,str,str]]] = []
def _parse_edge(elem: Edge):
nonlocal edges
try:
edges.append((
(
_get_node_id_map(elem.source.parent.parent.parent)[0],
elem.source.properties["label"],
elem.source.properties["type"],
elem.source.parent.value
),
(
_get_node_id_map(elem.target.parent.parent.parent)[0],
elem.target.properties["label"],
elem.target.properties["type"],
elem.target.parent.value
)
))
except AttributeError as e:
raise Exception(f"Missing attribute {e}")
return
def _parse_vertex(elem: Vertex):
nonlocal edges
try:
elem_map = _get_node_id_map(elem)
elem_map[1] = elem.properties
properties = elem_map[1]
properties.pop("label")
properties.pop("name")
properties.pop("placeholders")
if properties.get("type") == "Schedule":
if not re.search(r'\.(py|od)$', properties["file"]):
properties["file"] = f"{filename}/{properties["file"]}.od"
except AttributeError as e:
raise Exception(f"Missing attribute {e}")
return
abstract_syntax: DrawIOFile = parser.Parser.parse(str(self.directory / filename))
filename = filename.removesuffix(".drawio")
(self.directory / filename).mkdir(parents=False, exist_ok=True)
for page in abstract_syntax.pages:
if page.name == "main":
main = True
if len(page.root.children) != 1:
raise Exception(f"Only 1 layer allowed (keybind: ctr+shift+L)")
edges = []
id_counter = 1
node_map = {}
for element in page.root.children[0].children:
match element.__class__.__name__:
case "Edge":
_parse_edge(cast(Edge, element))
case "Vertex":
_parse_vertex(cast(Vertex, element))
for elem in element.children[0].children:
if elem.__class__.__name__ == "Edge":
_parse_edge(cast(Edge, elem))
continue
case _:
raise Exception(f"Unexpected element: {element}")
with open(self.directory / f"{filename}/{page.name}.od", "w", encoding="utf-8") as f:
f.write(template.render(nodes=node_map, edges=edges))
if main:
self.loaded["drawio"][filename] = (filename_out := f"{filename}/main.od")
return filename_out
self._print("drawio schedule requires main page to automatically load.")
return None