diff --git a/README.md b/README.md index c285aaf..e5793eb 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,23 @@ REACTIONS: DO: @T_STATE2_EXIT ``` +#### Graph requirements + +* A `namespace` is required for the FSM model, when targeting a `graph` output. +* Below are the ways to specify the `namespace` for the graph output: + ```yaml + ns hello="https://example.com/fsm" + + NAME: (ns=hello) example-fsm + ``` + + or + + ```yaml + ns hello="https://example.com/fsm" + + NAME: hello.example-fsm + ##### States * States represent the different behaviors of an activity. @@ -115,8 +132,10 @@ REACTIONS: #### Code Generation ```bash -textx generate model.fsm --target cpp -o model.hpp -textx generate model.fsm --target py -o model.py +textx generate example.fsm --target cpp -o model.hpp +textx generate example.fsm --target py -o model.py +textx generate example.fsm --target graph --format json-ld --autocompact +textx generate example.fsm --target console --format ttl --autocompact ``` * Generates a C++ header file with the data structures required for the FSM, along with a sample implementation code. @@ -127,7 +146,13 @@ textx generate model.fsm --target py -o model.py - `cpp`: A C++ header file. - `py`: A Python module. - `json`: JSON file. + - graph: A graph representation of the FSM in formats [json-ld, ttl, xml]. - `console`: Console output. +* Available formats for graph and console targets through the `--format` option: + - `json-ld`: JSON-LD format. + - `ttl`: Turtle format. + - `xml`: XML format. +* The `--autocompact` option can be used to automatically compact the generated graph using the namespace defined in the FSM model. #### Execution diff --git a/examples/models/fsm/example-fsm.fsm.json b/examples/models/fsm/example-fsm.fsm.json new file mode 100644 index 0000000..751cd38 --- /dev/null +++ b/examples/models/fsm/example-fsm.fsm.json @@ -0,0 +1,371 @@ +{ + "@context": { + "el": "https://secorolab.github.io/metamodels/behaviour/event_loop#", + "fsm": "https://secorolab.github.io/metamodels/behaviour/fsm#", + "hello": "https://example.com/hello#", + "xsd": "http://www.w3.org/2001/XMLSchema#" + }, + "@graph": [ + { + "@id": "hello:S_CONFIGURE", + "@type": "fsm:State" + }, + { + "@id": "hello:E_EXECUTE_EXIT", + "@type": "el:Event" + }, + { + "@id": "hello:T_CONFIGURE_IDLE", + "@type": "fsm:Transition", + "fsm:transition-from": { + "@id": "hello:S_CONFIGURE" + }, + "fsm:transition-to": { + "@id": "hello:S_IDLE" + } + }, + { + "@id": "hello:R_E_STEP3", + "@type": "fsm:Reaction", + "fsm:do-transition": { + "@id": "hello:T_EXECUTE_EXECUTE" + }, + "fsm:when-event": { + "@id": "hello:E_STEP" + } + }, + { + "@id": "hello:T_IDLE_COMPILE", + "@type": "fsm:Transition", + "fsm:transition-from": { + "@id": "hello:S_IDLE" + }, + "fsm:transition-to": { + "@id": "hello:S_COMPILE" + } + }, + { + "@id": "hello:S_IDLE", + "@type": "fsm:State" + }, + { + "@id": "hello:E_IDLE_EXIT_COMPILE", + "@type": "el:Event" + }, + { + "@id": "hello:T_IDLE_IDLE", + "@type": "fsm:Transition", + "fsm:transition-from": { + "@id": "hello:S_IDLE" + }, + "fsm:transition-to": { + "@id": "hello:S_IDLE" + } + }, + { + "@id": "hello:E_CONFIGURE_EXIT", + "@type": "el:Event" + }, + { + "@id": "hello:R_E_IDLE_EXIT_EXECUTE", + "@type": "fsm:Reaction", + "fsm:do-transition": { + "@id": "hello:T_IDLE_EXECUTE" + }, + "fsm:fires-events": { + "@id": "hello:E_EXECUTE_ENTERED" + }, + "fsm:when-event": { + "@id": "hello:E_IDLE_EXIT_EXECUTE" + } + }, + { + "@id": "hello:S_EXECUTE", + "@type": "fsm:State" + }, + { + "@id": "hello:R_E_IDLE_EXIT_COMPILE", + "@type": "fsm:Reaction", + "fsm:do-transition": { + "@id": "hello:T_IDLE_COMPILE" + }, + "fsm:fires-events": { + "@id": "hello:E_COMPILE_ENTERED" + }, + "fsm:when-event": { + "@id": "hello:E_IDLE_EXIT_COMPILE" + } + }, + { + "@id": "hello:T_EXECUTE_EXECUTE", + "@type": "fsm:Transition", + "fsm:transition-from": { + "@id": "hello:S_EXECUTE" + }, + "fsm:transition-to": { + "@id": "hello:S_EXECUTE" + } + }, + { + "@id": "hello:T_COMPILE_EXECUTE", + "@type": "fsm:Transition", + "fsm:transition-from": { + "@id": "hello:S_COMPILE" + }, + "fsm:transition-to": { + "@id": "hello:S_EXECUTE" + } + }, + { + "@id": "hello:E_IDLE_ENTERED", + "@type": "el:Event" + }, + { + "@id": "hello:T_IDLE_EXECUTE", + "@type": "fsm:Transition", + "fsm:transition-from": { + "@id": "hello:S_IDLE" + }, + "fsm:transition-to": { + "@id": "hello:S_EXECUTE" + } + }, + { + "@id": "hello:E_CONFIGURE_ENTERED", + "@type": "el:Event" + }, + { + "@id": "hello:R_E_STEP2", + "@type": "fsm:Reaction", + "fsm:do-transition": { + "@id": "hello:T_IDLE_IDLE" + }, + "fsm:when-event": { + "@id": "hello:E_STEP" + } + }, + { + "@id": "hello:S_START", + "@type": "fsm:State" + }, + { + "@id": "hello:example_fsm", + "@type": "fsm:FSM", + "fsm:current-state": { + "@id": "hello:S_START" + }, + "fsm:description": "Example of a simple FSM", + "fsm:end-state": { + "@id": "hello:S_EXIT" + }, + "fsm:events": [ + { + "@id": "hello:E_CONFIGURE_ENTERED" + }, + { + "@id": "hello:E_CONFIGURE_EXIT" + }, + { + "@id": "hello:E_IDLE_ENTERED" + }, + { + "@id": "hello:E_IDLE_EXIT_EXECUTE" + }, + { + "@id": "hello:E_IDLE_EXIT_COMPILE" + }, + { + "@id": "hello:E_COMPILE_ENTERED" + }, + { + "@id": "hello:E_COMPILE_EXIT" + }, + { + "@id": "hello:E_EXECUTE_ENTERED" + }, + { + "@id": "hello:E_EXECUTE_EXIT" + }, + { + "@id": "hello:E_STEP" + } + ], + "fsm:name": "example-fsm", + "fsm:reactions": [ + { + "@id": "hello:R_E_CONFIGURE_EXIT" + }, + { + "@id": "hello:R_E_IDLE_EXIT_EXECUTE" + }, + { + "@id": "hello:R_E_IDLE_EXIT_COMPILE" + }, + { + "@id": "hello:R_E_COMPILE_EXIT" + }, + { + "@id": "hello:R_E_EXECUTE_EXIT" + }, + { + "@id": "hello:R_E_STEP1" + }, + { + "@id": "hello:R_E_STEP2" + }, + { + "@id": "hello:R_E_STEP3" + } + ], + "fsm:start-state": { + "@id": "hello:S_START" + }, + "fsm:states": [ + { + "@id": "hello:S_START" + }, + { + "@id": "hello:S_CONFIGURE" + }, + { + "@id": "hello:S_IDLE" + }, + { + "@id": "hello:S_COMPILE" + }, + { + "@id": "hello:S_EXECUTE" + }, + { + "@id": "hello:S_EXIT" + } + ], + "fsm:transitions": [ + { + "@id": "hello:T_START_CONFIGURE" + }, + { + "@id": "hello:T_CONFIGURE_IDLE" + }, + { + "@id": "hello:T_IDLE_IDLE" + }, + { + "@id": "hello:T_IDLE_EXECUTE" + }, + { + "@id": "hello:T_IDLE_COMPILE" + }, + { + "@id": "hello:T_COMPILE_EXECUTE" + }, + { + "@id": "hello:T_EXECUTE_EXECUTE" + }, + { + "@id": "hello:T_EXECUTE_IDLE" + } + ] + }, + { + "@id": "hello:E_STEP", + "@type": "el:Event" + }, + { + "@id": "hello:T_START_CONFIGURE", + "@type": "fsm:Transition", + "fsm:transition-from": { + "@id": "hello:S_START" + }, + "fsm:transition-to": { + "@id": "hello:S_CONFIGURE" + } + }, + { + "@id": "hello:R_E_STEP1", + "@type": "fsm:Reaction", + "fsm:do-transition": { + "@id": "hello:T_START_CONFIGURE" + }, + "fsm:fires-events": { + "@id": "hello:E_CONFIGURE_ENTERED" + }, + "fsm:when-event": { + "@id": "hello:E_STEP" + } + }, + { + "@id": "hello:E_COMPILE_ENTERED", + "@type": "el:Event" + }, + { + "@id": "hello:E_EXECUTE_ENTERED", + "@type": "el:Event" + }, + { + "@id": "hello:T_EXECUTE_IDLE", + "@type": "fsm:Transition", + "fsm:transition-from": { + "@id": "hello:S_EXECUTE" + }, + "fsm:transition-to": { + "@id": "hello:S_IDLE" + } + }, + { + "@id": "hello:S_EXIT", + "@type": "fsm:State" + }, + { + "@id": "hello:S_COMPILE", + "@type": "fsm:State" + }, + { + "@id": "hello:R_E_CONFIGURE_EXIT", + "@type": "fsm:Reaction", + "fsm:do-transition": { + "@id": "hello:T_CONFIGURE_IDLE" + }, + "fsm:fires-events": { + "@id": "hello:E_IDLE_ENTERED" + }, + "fsm:when-event": { + "@id": "hello:E_CONFIGURE_EXIT" + } + }, + { + "@id": "hello:E_IDLE_EXIT_EXECUTE", + "@type": "el:Event" + }, + { + "@id": "hello:E_COMPILE_EXIT", + "@type": "el:Event" + }, + { + "@id": "hello:R_E_EXECUTE_EXIT", + "@type": "fsm:Reaction", + "fsm:do-transition": { + "@id": "hello:T_EXECUTE_IDLE" + }, + "fsm:fires-events": { + "@id": "hello:E_IDLE_ENTERED" + }, + "fsm:when-event": { + "@id": "hello:E_EXECUTE_EXIT" + } + }, + { + "@id": "hello:R_E_COMPILE_EXIT", + "@type": "fsm:Reaction", + "fsm:do-transition": { + "@id": "hello:T_COMPILE_EXECUTE" + }, + "fsm:fires-events": { + "@id": "hello:E_EXECUTE_ENTERED" + }, + "fsm:when-event": { + "@id": "hello:E_COMPILE_EXIT" + } + } + ] +} \ No newline at end of file diff --git a/examples/models/fsm/example.fsm b/examples/models/fsm/example.fsm index dd536ab..e8989c3 100644 --- a/examples/models/fsm/example.fsm +++ b/examples/models/fsm/example.fsm @@ -1,10 +1,11 @@ -NAME: example +ns fsm = "http://example.org/fsm#" + +NAME: example-fsm DESCRIPTION: "Example of a simple FSM" STATES: S_START,S_CONFIGURE,S_IDLE,S_COMPILE,S_EXECUTE,S_EXIT START_STATE: @S_START -CURRENT_STATE: @S_START END_STATE: @S_EXIT EVENTS: E_CONFIGURE_ENTERED, @@ -69,7 +70,7 @@ REACTIONS: R_E_STEP1: WHEN: @E_STEP DO: @T_START_CONFIGURE - FIRES: @E_CONFIGURE_ENTERED + FIRES: @E_CONFIGURE_ENTERED,@E_STEP R_E_STEP2: WHEN: @E_STEP DO: @T_IDLE_IDLE diff --git a/pyproject.toml b/pyproject.toml index 37a3c06..c923dc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,10 +29,11 @@ where = ["src"] ] [project.entry-points."textx_languages"] -"fsm" = "coord_dsl:fsm_meta" +"fsm" = "coord_dsl.generators.registration:fsm_lang" [project.entry-points."textx_generators"] -"fsm_json" = "coord_dsl.generators.fsm_gen:fsm_json_gen" -"fsm_cpp" = "coord_dsl.generators.fsm_gen:fsm_cpp_gen" -"fsm_py" = "coord_dsl.generators.fsm_gen:fsm_py_gen" -"fsm_console" = "coord_dsl.generators.fsm_gen:fsm_console_gen" +"fsm_console" = "coord_dsl.generators.registration:fsm_console_gen" + +[ruff] +line-length = 100 + diff --git a/src/coord_dsl/__init__.py b/src/coord_dsl/__init__.py index b0a3541..8b13789 100644 --- a/src/coord_dsl/__init__.py +++ b/src/coord_dsl/__init__.py @@ -1,10 +1 @@ -from os.path import dirname, join -from textx import language, metamodel_from_file -fsm_meta_model = metamodel_from_file(join(dirname(__file__), "metamodels/fsm.tx")) - - -@language("fsm", "*.fsm") -def fsm_meta(): - "A language for describing finite state machines" - return fsm_meta_model diff --git a/src/coord_dsl/generators/classes.py b/src/coord_dsl/generators/classes.py new file mode 100644 index 0000000..2bf68f8 --- /dev/null +++ b/src/coord_dsl/generators/classes.py @@ -0,0 +1,63 @@ +from __future__ import annotations +from typing import Optional + + +class NamespaceDeclare: + def __init__(self, **kwargs): + self.name: str = kwargs.get("name", "") + self.uri: str = kwargs.get("uri", "") + + +class State: + def __init__(self, **kwargs): + self.name: str = kwargs.get("name", "") + self.uri: str = "" + + +class Event: + def __init__(self, **kwargs): + self.name: str = kwargs.get("name", "") + self.uri: str = "" + + +class Transition: + def __init__(self, **kwargs): + self.name: str = kwargs.get("name", "") + self.from_state: State = kwargs.get("from_state", State()) + self.to_state: State = kwargs.get("to_state", State()) + self.uri: str = "" + + +class FiredEvent: + def __init__(self, **kwargs): + self.event: Optional[Event] = kwargs.get("event") + + +class Reaction: + def __init__(self, **kwargs): + self.name: str = kwargs.get("name", "") + self.when: Event = kwargs.get("when", Event()) + self.do: Transition = kwargs.get("do", Transition()) + self.fires: list[FiredEvent] = kwargs.get("fires", []) + self.uri: str = "" + + @property + def fired_events(self) -> list[Event]: + return [f.event for f in self.fires if f.event is not None] + + +class FSM: + def __init__(self, **kwargs): + self.namespace: NamespaceDeclare = kwargs.get("namespace", NamespaceDeclare()) + self.name: str = kwargs.get("name", "") + self.description: Optional[str] = kwargs.get("description") + self.states: list[State] = kwargs.get("states", []) + self.start_state: State = kwargs.get("start_state", State()) + self.end_state: State = kwargs.get("end_state", State()) + self.events: list[Event] = kwargs.get("events", []) + self.transitions: list[Transition] = kwargs.get("transitions", []) + self.reactions: list[Reaction] = kwargs.get("reactions", []) + self.uri: str = "" + + def _all_entities(self) -> list: + return self.states + self.events + self.transitions + self.reactions diff --git a/src/coord_dsl/generators/fsm_gen.py b/src/coord_dsl/generators/fsm_gen.py deleted file mode 100644 index 51b978d..0000000 --- a/src/coord_dsl/generators/fsm_gen.py +++ /dev/null @@ -1,160 +0,0 @@ -import json -from dataclasses import dataclass, field -from textx import generator -from jinja2 import Environment, FileSystemLoader -from pathlib import Path - - -@dataclass -class FSMData: - name: str = "" - description: str = "" - states: list = field(default_factory=list) - start_state: str = "" - current_state: str = "" - end_state: str = "" - events: list = field(default_factory=list) - transitions: list = field(default_factory=list) - reactions: list = field(default_factory=list) - transitions_table: dict = field(default_factory=dict) - reactions_table: dict = field(default_factory=dict) - - def to_json(self): - return json.dumps( - { - "name": self.name, - "description": self.description, - "states": self.states, - "start_state": self.start_state, - "current_state": self.current_state, - "end_state": self.end_state, - "events": self.events, - "transitions": self.transitions, - "reactions": self.reactions, - "transitions_table": self.transitions_table, - "reactions_table": self.reactions_table, - }, - indent=4, - ) - - -def parse_fsm(model): - fsm = FSMData() - - fsm.name = model.name - fsm.description = model.description - - fsm.states = [state.name for state in model.states] - - fsm.start_state = model.start_state.name - fsm.current_state = model.current_state.name - fsm.end_state = model.end_state.name - - fsm.events = [event.name for event in model.events] - - for transition in model.transitions: - fsm.transitions.append(transition.name) - fsm.transitions_table[transition.name] = { - "from": transition.from_state.name, - "to": transition.to_state.name, - } - - for reaction in model.reactions: - fsm.reactions.append(reaction.name) - fsm.reactions_table[reaction.name] = { - "when": reaction.when.name, - "do": reaction.do.name, - "num_fires": len(reaction.fires), - "fires": [event.name for event in reaction.fires] - if len(reaction.fires) > 0 - else None, - } - - return fsm - - -@generator("fsm", "console") -def fsm_console_gen(metamodel, model, output_path, overwrite, debug, **custom_args): - """Prints the FSM datastructures to the console""" - print(f"Generating FSM: {model.name}") - - fsm = parse_fsm(model) - - print(fsm.to_json()) - - -@generator("fsm", "cpp") -def fsm_cpp_gen(metamodel, model, output_path, overwrite, debug, **custom_args): - """Generates a .hpp file with the FSM datastructures""" - - print(f"Generating C code for FSM: {model.name}") - - # get module path - module_path = Path(__file__).parent.parent - env = Environment(loader=FileSystemLoader(module_path / "templates")) - template = env.get_template("fsm.hpp.jinja2") - - fsm = parse_fsm(model) - - fsm_name = model.name - - output = template.render( - { - "data": fsm, - } - ) - - if not output_path: - model_path = Path(model._tx_filename).parent - output_path = f"{model_path}/{fsm_name}.fsm.hpp" - - with open(output_path, "w") as f: - f.write(output) - print(f"FSM C code generated at {output_path}") - - -@generator("fsm", "py") -def fsm_py_gen(metamodel, model, output_path, overwrite, debug, **custom_args): - """Generates a .py file with the FSM datastructures""" - - print(f"Generating Python code for FSM: {model.name}") - - # get module path - module_path = Path(__file__).parent.parent - env = Environment(loader=FileSystemLoader(module_path / "templates")) - template = env.get_template("fsm.py.jinja2") - - fsm = parse_fsm(model) - - fsm_name = model.name - - output = template.render( - { - "data": fsm, - } - ) - - if not output_path: - model_path = Path(model._tx_filename).parent - output_path = f"{model_path}/fsm_{fsm_name}.py" - - with open(output_path, "w") as f: - f.write(output) - print(f"FSM C code generated at {output_path}") - - -@generator("fsm", "json") -def fsm_json_gen(metamodel, model, output_path, overwrite, debug, **custom_args): - """Generates a .json file with the FSM datastructures""" - - print(f"Generating JSON for FSM: {model.name}") - - fsm = parse_fsm(model) - - if not output_path: - model_path = Path(model._tx_filename).parent - output_path = f"{model_path}/{model.name}.fsm.json" - - with open(output_path, "w") as f: - f.write(fsm.to_json()) - print(f"FSM JSON generated at {output_path}") diff --git a/src/coord_dsl/generators/fsm_graph.py b/src/coord_dsl/generators/fsm_graph.py new file mode 100644 index 0000000..126c4e9 --- /dev/null +++ b/src/coord_dsl/generators/fsm_graph.py @@ -0,0 +1,222 @@ +import json +from dataclasses import dataclass, field +from textx import generator +from jinja2 import Environment, FileSystemLoader +from pathlib import Path +from rdflib import Graph, Namespace, Literal, RDF, XSD, URIRef +from rdf_utils.uri import URL_SECORO_MM +from coord_dsl.generators.classes import * + + +def get_fsm_graph(model: FSM) -> Graph: + print(f"Generating FSM graph for {model.name}") + + assert model.namespace is not None and model.namespace.uri != "", ( + "Namespace is required for Graph generation" + ) + + URI_MM_FSM = f"{URL_SECORO_MM}/behaviour/fsm#" + URI_MM_EL = f"{URL_SECORO_MM}/behaviour/event_loop#" + + NS_FSM = Namespace(URI_MM_FSM) + NS_EL = Namespace(URI_MM_EL) + NS_MODEL = Namespace(model.namespace.uri) + + g = Graph() + g.bind("fsm", NS_FSM) + g.bind("el", NS_EL) + g.bind(model.name, NS_MODEL) + + g.add((URIRef(model.uri), RDF.type, NS_FSM.FSM)) + g.add((URIRef(model.uri), NS_FSM.name, Literal(model.name))) + if model.description is not None: + g.add((URIRef(model.uri), NS_FSM.description, Literal(model.description))) + + g.add((URIRef(model.uri), NS_FSM["start-state"], URIRef(model.start_state.uri))) + g.add((URIRef(model.uri), NS_FSM["end-state"], URIRef(model.end_state.uri))) + g.add((URIRef(model.uri), NS_FSM["current-state"], URIRef(model.start_state.uri))) + + for state in model.states: + g.add((URIRef(state.uri), RDF.type, NS_FSM.State)) + g.add((URIRef(model.uri), NS_FSM.states, URIRef(state.uri))) + + for event in model.events: + g.add((URIRef(event.uri), RDF.type, NS_EL.Event)) + g.add((URIRef(model.uri), NS_FSM.events, URIRef(event.uri))) + + for transition in model.transitions: + g.add((URIRef(transition.uri), RDF.type, NS_FSM.Transition)) + g.add((URIRef(model.uri), NS_FSM.transitions, URIRef(transition.uri))) + + from_state = transition.from_state.uri + to_state = transition.to_state.uri + + g.add((URIRef(transition.uri), NS_FSM["transition-from"], URIRef(from_state))) + g.add((URIRef(transition.uri), NS_FSM["transition-to"], URIRef(to_state))) + + for reaction in model.reactions: + g.add((URIRef(reaction.uri), RDF.type, NS_FSM.Reaction)) + g.add((URIRef(model.uri), NS_FSM.reactions, URIRef(reaction.uri))) + + when = reaction.when.uri + do = reaction.do.uri + fires = [f.event.uri for f in reaction.fires if f.event is not None] + + g.add((URIRef(reaction.uri), NS_FSM["when-event"], URIRef(when))) + g.add((URIRef(reaction.uri), NS_FSM["do-transition"], URIRef(do))) + for event_uri in fires: + g.add((URIRef(reaction.uri), NS_FSM["fires-events"], URIRef(event_uri))) + + return g + + + +# @generator("fsm", "graph") +# def fsm_graph_gen1(metamodel, model, output_path, overwrite, debug, **kwargs): +# """Generates a .jsonld file with the FSM datastructures""" +# +# assert model.name.ns is not None and model.name.ns != "", ( +# "Namespace is required for Graph generation" +# ) +# +# print(f"Generating JSON-LD for FSM: {model.name}") +# +# fsm = parse_fsm(model) +# fsm.ns = model.name.ns.name +# fsm.ns_uri = model.name.ns.uri +# +# g, context = get_fsm_graph(fsm, **kwargs) +# +# g_format = kwargs.get("format", "json-ld") +# +# assert g_format in __GRAPH_FORMAT_EXT, ( +# f"Unsupported graph format '{g_format}', supported formats are: {list(__GRAPH_FORMAT_EXT.keys())}" +# ) +# +# if not output_path: +# model_path = Path(model._tx_filename).parent +# output_path = f"{model_path}/{fsm.name}.fsm.{__GRAPH_FORMAT_EXT[g_format]}" +# +# ser_kwargs = { +# "destination": output_path, +# "format": g_format, +# "indent": 4, +# "context": context, +# } +# +# if "nocompact" in kwargs: +# ser_kwargs["auto_compact"] = False +# else: +# ser_kwargs["auto_compact"] = True +# +# g.serialize(**ser_kwargs) +# print(f"FSM JSON-LD generated at {output_path}") +# +# +# @generator("fsm", "console") +# def fsm_console_gen(metamodel, model, output_path, overwrite, debug, **kwargs): +# """Prints the FSM datastructures to the console""" +# print(f"Generating FSM: {model.name}") +# +# g_format = kwargs.get("format", "json") +# +# if g_format != "json": +# assert g_format in __GRAPH_FORMAT_EXT, ( +# f"Unsupported graph format '{g_format}', supported formats are: {list(__GRAPH_FORMAT_EXT.keys())}" +# ) +# +# fsm = parse_fsm(model) +# if g_format == "json": +# print(fsm.to_json()) +# else: +# assert model.name.ns is not None and model.name.ns != "", ( +# "Namespace is required for Graph generation" +# ) +# fsm.ns = model.name.ns.name +# fsm.ns_uri = model.name.ns.uri +# g, context = get_fsm_graph(fsm, **kwargs) +# print( +# g.serialize( +# format=g_format, +# indent=4, +# context=context, +# auto_compact=not kwargs.get("nocompact", False), +# ) +# ) +# +# +# @generator("fsm", "cpp") +# def fsm_cpp_gen(metamodel, model, output_path, overwrite, debug, **kwargs): +# """Generates a .hpp file with the FSM datastructures""" +# +# print(f"Generating C code for FSM: {model.name}") +# +# # get module path +# module_path = Path(__file__).parent.parent +# env = Environment(loader=FileSystemLoader(module_path / "templates")) +# template = env.get_template("fsm.hpp.jinja2") +# +# fsm = parse_fsm(model) +# +# fsm_name = model.name +# +# output = template.render( +# { +# "data": fsm, +# } +# ) +# +# if not output_path: +# model_path = Path(model._tx_filename).parent +# output_path = f"{model_path}/{fsm_name}.fsm.hpp" +# +# with open(output_path, "w") as f: +# f.write(output) +# print(f"FSM C code generated at {output_path}") +# +# +# @generator("fsm", "py") +# def fsm_py_gen(metamodel, model, output_path, overwrite, debug, **kwargs): +# """Generates a .py file with the FSM datastructures""" +# +# print(f"Generating Python code for FSM: {model.name}") +# +# # get module path +# module_path = Path(__file__).parent.parent +# env = Environment(loader=FileSystemLoader(module_path / "templates")) +# template = env.get_template("fsm.py.jinja2") +# +# fsm = parse_fsm(model) +# +# fsm_name = model.name +# +# output = template.render( +# { +# "data": fsm, +# } +# ) +# +# if not output_path: +# model_path = Path(model._tx_filename).parent +# output_path = f"{model_path}/fsm_{fsm_name}.py" +# +# with open(output_path, "w") as f: +# f.write(output) +# print(f"FSM C code generated at {output_path}") +# +# +# @generator("fsm", "json") +# def fsm_json_gen(metamodel, model, output_path, overwrite, debug, **kwargs): +# """Generates a .json file with the FSM datastructures""" +# +# print(f"Generating JSON for FSM: {model.name}") +# +# fsm = parse_fsm(model) +# +# if not output_path: +# model_path = Path(model._tx_filename).parent +# output_path = f"{model_path}/{model.name}.fsm.json" +# +# with open(output_path, "w") as f: +# f.write(fsm.to_json()) +# print(f"FSM JSON generated at {output_path}") diff --git a/src/coord_dsl/generators/registration.py b/src/coord_dsl/generators/registration.py new file mode 100644 index 0000000..6f1b485 --- /dev/null +++ b/src/coord_dsl/generators/registration.py @@ -0,0 +1,73 @@ +from textx import GeneratorDesc, LanguageDesc, metamodel_from_file +from textx.scoping import providers as scoping_providers +from coord_dsl.generators.classes import ( + NamespaceDeclare, + State, + Event, + Transition, + FiredEvent, + Reaction, + FSM, +) +from coord_dsl.generators.fsm_graph import get_fsm_graph +from importlib.resources import files + +GRAMMAR_PATH = str(files("coord_dsl.metamodels").joinpath("fsm.tx")) + +__SUPPORTED_GRAPH_FORMATS = ["ttl", "nt", "xml", "json-ld"] + +def fsm_metamodel(): + mm = metamodel_from_file( + GRAMMAR_PATH, + classes=[ + NamespaceDeclare, + State, + Event, + Transition, + FiredEvent, + Reaction, + FSM, + ], + ) + return mm + +def _expand_iris(fsm: FSM) -> None: + base = fsm.namespace.uri if fsm.namespace else "" + fsm.uri = base + fsm.name + for obj in fsm._all_entities(): + obj.uri = base + obj.name + +fsm_lang = LanguageDesc( + name="coord_dsl_fsm", + pattern="*.fsm", + description="Finite State Machine DSL", + metamodel=fsm_metamodel, +) + +def graph_gen_console(metamodel, model, output_path, overwrite, debug, **kwargs): + _expand_iris(model) + + g = get_fsm_graph(model) + + ser_args = {"indent": 2} + + if "autocompact" in kwargs: + ser_args["auto_compact"] = True + else: + ser_args["auto_compact"] = False + + format = kwargs.get("format", "json-ld") + if format not in __SUPPORTED_GRAPH_FORMATS: + raise ValueError(f"Unsupported graph format '{format}', supported formats are: {__SUPPORTED_GRAPH_FORMATS}") + + ser_args["format"] = format + + print(g.serialize(**ser_args)) + + +fsm_console_gen = GeneratorDesc( + language="coord_dsl_fsm", + target="console", + description="Prints the loaded model to console", + generator=graph_gen_console, +) diff --git a/src/coord_dsl/metamodels/fsm.tx b/src/coord_dsl/metamodels/fsm.tx index 1e21e18..3cd960e 100644 --- a/src/coord_dsl/metamodels/fsm.tx +++ b/src/coord_dsl/metamodels/fsm.tx @@ -1,19 +1,17 @@ -FSM: - "NAME" ":" name=ID - ( - "DESCRIPTION" ":" description=STRING - )? - "STATES" ":" states+=State[','] - "START_STATE" ":" "@"start_state=[State] - "CURRENT_STATE" ":" "@"current_state=[State] - "END_STATE" ":" "@"end_state=[State] - "EVENTS" ":" events+=Event[','] - "TRANSITIONS" ":" transitions+=Transition - "REACTIONS" ":" reactions+=Reaction +FSM: + namespace=NamespaceDeclare + "NAME" ":" name=Name + ("DESCRIPTION" ":" description=STRING)? + "STATES" ":" states+=State[','] + "START_STATE" ":" "@" start_state=[State] + "END_STATE" ":" "@" end_state=[State] + "EVENTS" ":" events+=Event[','] + "TRANSITIONS" ":" transitions+=Transition + "REACTIONS" ":" reactions+=Reaction ; State: - name=ID + name=ID ; Event: @@ -21,18 +19,22 @@ Event: ; Transition: - name=ID ":" - "FROM" ":" "@"from_state=[State] - "TO" ":" "@"to_state=[State] + name=ID ":" + "FROM" ":" "@" from_state=[State] + "TO" ":" "@" to_state=[State] ; Reaction: - name=ID ":" - "WHEN" ":" "@"when=[Event] - "DO" ":" "@"do=[Transition] - ("FIRES" ":" "@"fires=[Event] ("," "@"fires+=[Event])*)? + name=ID ":" + "WHEN" ":" "@" when=[Event] + "DO" ":" "@" do=[Transition] + ("FIRES" ":" fires+=FiredEvent[','])? +; + +FiredEvent: + "@" event=[Event] ; -Comment: - (/\/\/.*$/ | /(?ms)\/\*(.*?)\*\//) -; \ No newline at end of file +Comment: /\/\/.*$/; +NamespaceDeclare: 'ns' name=ID '=' uri=STRING; +Name: /[^\d\W][\w-]*/;