Browse Source

Add concrete syntax for class diagrams + example (woods2.py)

Joeri Exelmans 1 year ago
parent
commit
52ded8af77

+ 41 - 0
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]

+ 123 - 7
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: /```[^`]*```/
+
+INT_OR_INF: INT | "*"
+
+multiplicity: "[" INT ".." INT_OR_INF "]"
+
+ABSTRACT: "abstract"
+
+superclasses: IDENTIFIER ("," IDENTIFIER)*
+
+attrs: attr*
+
+constraint: CODE | INDENTED_CODE
 
-object: [IDENTIFIER] ":" IDENTIFIER [link] _NL [_INDENT slot+ _DEDENT]
-link: "(" IDENTIFIER "->" IDENTIFIER ")"
-slot: IDENTIFIER "=" literal _NL
+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

+ 3 - 38
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)

+ 1 - 0
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
 

+ 130 - 0
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.")

+ 15 - 15
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.
-
-        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
+    # def create_global_constraint(self, name: str):
+    #     """
+    #     Defines a global constraint element.
+
+    #     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
 
     def create_attribute(self, name: str):
         """