From 52ded8af772e35edd33a60a20231e5e05cafdfab Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 9 Oct 2024 15:09:16 +0200 Subject: [PATCH] Add concrete syntax for class diagrams + example (woods2.py) --- concrete_syntax/common.py | 41 +++++++++ concrete_syntax/textual_cd/parser.py | 130 +++++++++++++++++++++++++-- concrete_syntax/textual_od/parser.py | 41 +-------- examples/conformance/woods.py | 1 + examples/conformance/woods2.py | 130 +++++++++++++++++++++++++++ services/scd.py | 26 +++--- 6 files changed, 311 insertions(+), 58 deletions(-) create mode 100644 examples/conformance/woods2.py diff --git a/concrete_syntax/common.py b/concrete_syntax/common.py index d3ba5d0..3b29228 100644 --- a/concrete_syntax/common.py +++ b/concrete_syntax/common.py @@ -1,3 +1,5 @@ +from lark import Transformer + def indent(multiline_string, how_much): lines = multiline_string.split('\n') return '\n'.join([' '*how_much+l for l in lines]) @@ -14,3 +16,42 @@ def display_value(val: any, type_name: str, indentation=0): return str(val) else: raise Exception("don't know how to display value" + type_name) + + +# internal use only +# just a dumb wrapper to distinguish between code and string +class _Code: + def __init__(self, code): + self.code = code + +class TBase(Transformer): + + def IDENTIFIER(self, token): + return str(token) + + def INT(self, token): + return int(token) + + def BOOL(self, token): + return token == "True" + + def STR(self, token): + return str(token[1:-1]) # strip the "" or '' + + def CODE(self, token): + return _Code(str(token[1:-1])) # strip the `` + + def INDENTED_CODE(self, token): + skip = 4 # strip the ``` and the following newline character + space_count = 0 + while token[skip+space_count] == " ": + space_count += 1 + lines = token.split('\n')[1:-1] + for line in lines: + if len(line) >= space_count and line[0:space_count] != ' '*space_count: + raise Exception("wrong indentation of INDENTED_CODE") + unindented_lines = [l[space_count:] for l in lines] + return _Code('\n'.join(unindented_lines)) + + def literal(self, el): + return el[0] diff --git a/concrete_syntax/textual_cd/parser.py b/concrete_syntax/textual_cd/parser.py index c5952d7..7f8b36e 100644 --- a/concrete_syntax/textual_cd/parser.py +++ b/concrete_syntax/textual_cd/parser.py @@ -1,26 +1,142 @@ +from lark import Lark, logger +from concrete_syntax.common import _Code, TBase +from uuid import UUID +from services.scd import SCD +from services.od import OD + grammar = r""" %import common.WS %ignore WS %ignore COMMENT -?start: object* +?start: (class_ | association | global_constraint)* IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/ -COMMENT: /#[^\n]*\n/ -# newline -_NL: /(\r?\n[\t ]*)+/ +COMMENT: /#[^\n]*\n/ literal: INT | STR | BOOL + | CODE + | INDENTED_CODE INT: /[0-9]+/ STR: /"[^"]*"/ | /'[^']*'/ BOOL: "True" | "False" +CODE: /`[^`]*`/ +INDENTED_CODE: /```[^`]*```/ -object: [IDENTIFIER] ":" IDENTIFIER [link] _NL [_INDENT slot+ _DEDENT] -link: "(" IDENTIFIER "->" IDENTIFIER ")" -slot: IDENTIFIER "=" literal _NL +INT_OR_INF: INT | "*" + +multiplicity: "[" INT ".." INT_OR_INF "]" + +ABSTRACT: "abstract" + +superclasses: IDENTIFIER ("," IDENTIFIER)* + +attrs: attr* + +constraint: CODE | INDENTED_CODE + +class_: [ABSTRACT] "class" IDENTIFIER [multiplicity] ["(" superclasses ")"] ["{" attrs [constraint] "}"] + +association: "association" IDENTIFIER [multiplicity] IDENTIFIER "->" IDENTIFIER [multiplicity] ["{" [constraint] "}"] + +OPTIONAL: "optional" + +attr: [OPTIONAL] IDENTIFIER IDENTIFIER [constraint] ";" + +global_constraint: "global" IDENTIFIER constraint """ + +parser = Lark(grammar, parser='lalr') + +def _handle_missing_multiplicity(multiplicity): + if multiplicity != None: + return multiplicity + else: + return (None, None) + + +def parse_cd(state, m_text): + type_model_id = state.read_dict(state.read_root(), "SCD") + scd_mmm = UUID(state.read_value(type_model_id)) + + m = state.create_node() + cd = SCD(m, state) + od = OD(scd_mmm, m, state) + + def _add_constraint_to_obj(obj_name, constraint): + c = od.create_actioncode_value(f"{obj_name}.constraint", constraint.code) + od.create_slot("constraint", obj_name, c) + + primitive_types = { + type_name : UUID(state.read_value(state.read_dict(state.read_root(), type_name))) + for type_name in ["Integer", "String", "Boolean"] + } + + class T(TBase): + def __init__(self, visit_tokens): + super().__init__(visit_tokens) + self.obj_counter = 0 + + def ABSTRACT(self, el): + return True + + def INT_OR_INF(self, el): + return float('inf') if el == "*" else int(el) + + def multiplicity(self, el): + [lower, upper] = el + return (lower, upper) + + def superclasses(self, el): + return list(el) + + def attrs(self, el): + return list(el) + + def constraint(self, el): + return el[0] + + def attr(self, el): + [optional, attr_type, attr_name, constraint] = el + return (optional == "optional", attr_type, attr_name, constraint) + + def global_constraint(self, el): + [name, constraint] = el + od.create_object(name, "GlobalConstraint") + _add_constraint_to_obj(name, constraint) + + def class_(self, el): + [abstract, class_name, multiplicity, super_classes, attrs, constraint] = el + (lower, upper) = _handle_missing_multiplicity(multiplicity) + cd.create_class(class_name, abstract, lower, upper) + if super_classes != None: + for super_class in super_classes: + cd.create_inheritance(class_name, super_class) + if constraint != None: + _add_constraint_to_obj(class_name, constraint) + if attrs != None: + for attr in attrs: + (optional, attr_type, attr_name, constraint) = attr + # TODO: only create type ref if it doesn't exist yet + cd.create_model_ref(attr_type, primitive_types[attr_type]) + cd.create_attribute_link(class_name, attr_type, attr_name, optional) + if constraint != None: + _add_constraint_to_obj(f"{class_name}_{attr_name}", constraint) + + def association(self, el): + [assoc_name, src_multiplicity, src_name, tgt_name, tgt_multiplicity, constraint] = el + (src_lower, src_upper) = _handle_missing_multiplicity(src_multiplicity) + (tgt_lower, tgt_upper) = _handle_missing_multiplicity(tgt_multiplicity) + cd.create_association(assoc_name, src_name, tgt_name, src_lower, src_upper, tgt_lower, tgt_upper) + if constraint != None: + _add_constraint_to_obj(class_name, constraint) + + tree = parser.parse(m_text) + t = T(visit_tokens=True).transform(tree) + + return m diff --git a/concrete_syntax/textual_od/parser.py b/concrete_syntax/textual_od/parser.py index 2e4c317..3a0ef13 100644 --- a/concrete_syntax/textual_od/parser.py +++ b/concrete_syntax/textual_od/parser.py @@ -1,9 +1,10 @@ # Parser for Object Diagrams textual concrete syntax -from lark import Lark, logger, Transformer +from lark import Lark, logger from lark.indenter import Indenter from services.od import OD from services.scd import SCD +from concrete_syntax.common import _Code, TBase from uuid import UUID grammar = r""" @@ -39,12 +40,6 @@ slot: IDENTIFIER "=" literal ";" parser = Lark(grammar, parser='lalr') -# internal use only -# just a dumb wrapper to distinguish between code and string -class _Code: - def __init__(self, code): - self.code = code - # given a concrete syntax text string, and a meta-model, parses the CS def parse_od(state, m_text, mm): tree = parser.parse(m_text) @@ -54,41 +49,11 @@ def parse_od(state, m_text, mm): int_mm_id = UUID(state.read_value(state.read_dict(state.read_root(), "Integer"))) - class T(Transformer): + class T(TBase): def __init__(self, visit_tokens): super().__init__(visit_tokens) self.obj_counter = 0 - def IDENTIFIER(self, token): - return str(token) - - def INT(self, token): - return int(token) - - def BOOL(self, token): - return token == "True" - - def STR(self, token): - return str(token[1:-1]) # strip the "" or '' - - def CODE(self, token): - return _Code(str(token[1:-1])) # strip the `` - - def INDENTED_CODE(self, token): - skip = 4 # strip the ``` and the following newline character - space_count = 0 - while token[skip+space_count] == " ": - space_count += 1 - lines = token.split('\n')[1:-1] - for line in lines: - if len(line) >= space_count and line[0:space_count] != ' '*space_count: - raise Exception("wrong indentation of INDENTED_CODE") - unindented_lines = [l[space_count:] for l in lines] - return _Code('\n'.join(unindented_lines)) - - def literal(self, el): - return el[0] - def link_spec(self, el): [src, tgt] = el return (src, tgt) diff --git a/examples/conformance/woods.py b/examples/conformance/woods.py index 022a8a0..6f7f0d0 100644 --- a/examples/conformance/woods.py +++ b/examples/conformance/woods.py @@ -43,6 +43,7 @@ woods_mm_cs = """ Man:Class { # We can define lower and upper cardinalities on Classes # (if unspecified, the lower-card is 0, and upper-card is infinity) + lower_cardinality = 1; # there must be at least one Man in every model upper_cardinality = 2; # there must be at most two Men in every model diff --git a/examples/conformance/woods2.py b/examples/conformance/woods2.py new file mode 100644 index 0000000..92c3049 --- /dev/null +++ b/examples/conformance/woods2.py @@ -0,0 +1,130 @@ +from state.devstate import DevState +from bootstrap.scd import bootstrap_scd +from framework.conformance import Conformance, render_conformance_check_result +from concrete_syntax.textual_cd import parser as parser_cd +from concrete_syntax.textual_od import parser as parser_od +from concrete_syntax.textual_od import renderer as renderer_od +from concrete_syntax.common import indent +from concrete_syntax.plantuml import renderer as plantuml +from util.prompt import yes_no, pause + +state = DevState() + +print("Loading meta-meta-model...") +scd_mmm = bootstrap_scd(state) +print("OK") + +print("Is our meta-meta-model a valid class diagram?") +conf = Conformance(state, scd_mmm, scd_mmm) +print(render_conformance_check_result(conf.check_nominal())) + +# If you are curious, you can serialize the meta-meta-model: +# print("--------------") +# print(indent( +# renderer.render_od(state, +# m_id=scd_mmm, +# mm_id=scd_mmm), +# 4)) +# print("--------------") + + +# Change this: +woods_mm_cs = """ + abstract class Animal + + class Bear (Animal) + + class Man [1..2] (Animal) { + Integer weight `get_value(get_target(this)) > 20`; # <- constraint in context of attribute-link + + `get_value(get_slot(this, "weight")) > 20` # <- constraint in context of Man-object + } + + association afraidOf [0..6] Man -> Animal [1..2] + + global total_weight_small_enough ``` + total_weight = 0 + for man_name, man_id in get_all_instances("Man"): + total_weight += get_value(get_slot(man_id, "weight")) + total_weight < 85 + ``` +""" + +print() +print("Parsing 'woods' meta-model...") +woods_mm = parser_cd.parse_cd( + state, + m_text=woods_mm_cs, # the string of text to parse +) +print("OK") + +# As a double-check, you can serialize the parsed model: +print("--------------") +print(indent( + renderer_od.render_od(state, + m_id=woods_mm, + mm_id=scd_mmm), + 4)) +print("--------------") + +print("Is our 'woods' meta-model a valid class diagram?") +conf = Conformance(state, woods_mm, scd_mmm) +print(render_conformance_check_result(conf.check_nominal())) + +# Change this: +woods_m_cs = """ + george:Man { + weight = 15; + } + billy:Man { + weight = 100; + } + bear1:Bear + bear2:Bear + :afraidOf (george -> bear1) + :afraidOf (george -> bear2) +""" + +print() +print("Parsing 'woods' model...") +woods_m = parser_od.parse_od( + state, + m_text=woods_m_cs, + mm=woods_mm, # this time, the meta-model is the previous model we parsed +) +print("OK") + +# As a double-check, you can serialize the parsed model: +# print("--------------") +# print(indent( +# renderer.render_od(state, +# m_id=woods_m, +# mm_id=woods_mm), +# 4)) +# print("--------------") + +print("Is our model a valid woods-diagram?") +conf = Conformance(state, woods_m, woods_mm) +print(render_conformance_check_result(conf.check_nominal())) + + +print() +print("==================================") +if yes_no("Print PlantUML?"): + print_mm = yes_no(" ▸ Print meta-model?") + print_m = yes_no(" ▸ Print model?") + print_conf = print_mm and print_m and yes_no(" ▸ Print conformance links?") + + uml = "" + if print_mm: + uml += plantuml.render_package("Meta-model", plantuml.render_class_diagram(state, woods_mm)) + if print_m: + uml += plantuml.render_package("Model", plantuml.render_object_diagram(state, woods_m, woods_mm)) + if print_conf: + uml += plantuml.render_trace_conformance(state, woods_m, woods_mm) + + print("==================================") + print(uml) + print("==================================") + print("Go to http://www.plantuml.com/plantuml/uml/") + print("and paste the above string.") diff --git a/services/scd.py b/services/scd.py index 8259bd4..ecdce8a 100644 --- a/services/scd.py +++ b/services/scd.py @@ -132,21 +132,21 @@ class SCD: set_cardinality("target_upper", tgt_max_c) return assoc_edge - def create_global_constraint(self, name: str): - """ - Defines a global constraint element. + # def create_global_constraint(self, name: str): + # """ + # Defines a global constraint element. - Args: - name: the name of the global constraint to be created + # Args: + # name: the name of the global constraint to be created - Returns: - Nothing. - """ - # create element + morphism links - element_node = self.bottom.create_node() # create element node - self.bottom.create_edge(self.model, element_node, name) # attach to model - scd_node, = self.bottom.read_outgoing_elements(self.scd_model, "GlobalConstraint") # retrieve type - self.bottom.create_edge(element_node, scd_node, "Morphism") # create morphism link + # Returns: + # Nothing. + # """ + # # create element + morphism links + # element_node = self.bottom.create_node() # create element node + # self.bottom.create_edge(self.model, element_node, name) # attach to model + # scd_node, = self.bottom.read_outgoing_elements(self.scd_model, "GlobalConstraint") # retrieve type + # self.bottom.create_edge(element_node, scd_node, "Morphism") # create morphism link def create_attribute(self, name: str): """