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