ソースを参照

(WIP) implementing CBD language... Meta-meta-model: Association inherits from Class. Matcher accepts pivot. Add generic graphviz renderer.

Joeri Exelmans 1 年間 前
コミット
1eb8a84553

+ 16 - 2
api/od.py

@@ -118,7 +118,13 @@ class ODAPI:
         )[0]
 
     def get(self, name: str):
-        return self.bottom.read_outgoing_elements(self.m, name)[0]
+        results = self.bottom.read_outgoing_elements(self.m, name)
+        if len(results) == 1:
+            return results[0]
+        elif len(results) >= 2:
+            raise Exception("this should never happen")
+        else:
+            raise Exception(f"No such element in model: '{name}'")
 
     def get_type_name(self, obj: UUID):
         return self.get_name(self.get_type(obj))
@@ -144,6 +150,9 @@ class ODAPI:
         slot = self.get_slot(obj, attr_name)
         return self.get_value(slot)
 
+    # Returns the given default value if the slot does not exist on the object.
+    # The attribute must exist in the object's class, or an exception will be thrown.
+    # The slot may not exist however, if the attribute is defined as 'optional' in the class.
     def get_slot_value_default(self, obj: UUID, attr_name: str, default: any):
         try:
             return self.get_slot_value(obj, attr_name)
@@ -187,7 +196,12 @@ class ODAPI:
 
     def create_link(self, link_name: Optional[str], assoc_name: str, src: UUID, tgt: UUID):
         global NEXT_ID
-        typ, = self.bottom.read_outgoing_elements(self.mm, assoc_name)
+        types = self.bottom.read_outgoing_elements(self.mm, assoc_name) 
+        if len(types) == 0:
+            raise Exception(f"No such association: '{assoc_name}'")
+        elif len(types) >= 2:
+            raise Exception(f"More than one association exists with name '{assoc_name}' - this means the MM is invalid.")
+        typ = types[0]
         if link_name == None:
             link_name = f"__{assoc_name}{NEXT_ID}"
             NEXT_ID += 1

+ 2 - 1
bootstrap/scd.py

@@ -86,7 +86,8 @@ def bootstrap_scd(state: State) -> UUID:
     # # Attribute inherits from Element
     add_edge_element("attr_inh_element", attr_node, element_node)
     # # Association inherits from Element
-    add_edge_element("assoc_inh_element", assoc_edge, element_node)
+    # add_edge_element("assoc_inh_element", assoc_edge, element_node)
+    add_edge_element("assoc_inh_element", assoc_edge, class_node)
     # # AttributeLink inherits from Element
     add_edge_element("attr_link_inh_element", attr_link_edge, element_node)
     # # ModelRef inherits from Attribute

+ 5 - 0
concrete_syntax/common.py

@@ -17,6 +17,11 @@ def display_value(val: any, type_name: str, indentation=0):
     else:
         raise Exception("don't know how to display value" + type_name)
 
+def display_name(raw_name: str) -> str:
+    if raw_name[0:2] == "__":
+        return "" # hide names that start with '__', they are anonymous (by convention)
+    else:
+        return raw_name
 
 # internal use only
 # just a dumb wrapper to distinguish between code and string

+ 12 - 0
concrete_syntax/graphviz/make_url.py

@@ -0,0 +1,12 @@
+from concrete_syntax.common import indent
+import urllib.parse
+
+def make_url(graphviz_txt: str) -> str:
+
+    as_digraph = f"digraph {{\n{indent(graphviz_txt, 2)}\n}}"
+
+    # This one seems much faster:
+    return "https://edotor.net/?engine=dot#"+urllib.parse.quote(as_digraph)
+
+    # Keeping this one here just in case:
+    # return "https://dreampuf.github.io/GraphvizOnline/#"+urllib.parse.quote(graphviz)

+ 88 - 0
concrete_syntax/graphviz/renderer.py

@@ -0,0 +1,88 @@
+from uuid import UUID
+from services import scd, od
+from services.bottom.V0 import Bottom
+from concrete_syntax.common import display_value, display_name, indent
+
+
+def render_object_diagram(state, m, mm, render_attributes=True, prefix_ids=""):
+    bottom = Bottom(state)
+    mm_scd = scd.SCD(mm, state)
+    m_od = od.OD(mm, m, state)
+
+    def make_id(uuid) -> str:
+        return 'n'+(prefix_ids+str(uuid).replace('-',''))[24:]
+
+    output = ""
+
+    # Render objects
+    for class_name, class_node in mm_scd.get_classes().items():
+        if render_attributes:
+            attributes = od.get_attributes(bottom, class_node)
+
+        for obj_name, obj_node in m_od.get_objects(class_node).items():
+            output += f"\n{make_id(obj_node)} [label=\"{display_name(obj_name)} : {class_name}\", shape=rect] ;"
+            #" {{"
+
+            # if render_attributes:
+            #     for attr_name, attr_edge in attributes:
+            #         slot = m_od.get_slot(obj_node, attr_name)
+            #         if slot != None:
+            #             val, type_name = od.read_primitive_value(bottom, slot, mm)
+            #             output += f"\n{attr_name} => {display_value(val, type_name)}"
+            # output += '\n}'
+
+    output += '\n'
+
+    # Render links
+    for assoc_name, assoc_edge in mm_scd.get_associations().items():
+        for link_name, link_edge in m_od.get_objects(assoc_edge).items():
+            src_obj = bottom.read_edge_source(link_edge)
+            tgt_obj = bottom.read_edge_target(link_edge)
+            src_name = m_od.get_object_name(src_obj)
+            tgt_name = m_od.get_object_name(tgt_obj)
+
+            output += f"\n{make_id(src_obj)} -> {make_id(tgt_obj)} [label=\"{display_name(link_name)}:{assoc_name}\"] ;"
+
+    return output
+
+def render_trace_match(state, name_mapping: dict, pattern_m: UUID, host_m: UUID, color="grey", prefix_pattern_ids="", prefix_host_ids=""):
+    bottom = Bottom(state)
+    class_type = od.get_scd_mm_class_node(bottom)
+    attr_link_type = od.get_scd_mm_attributelink_node(bottom)
+
+    def make_pattern_id(uuid) -> str:
+        return 'n'+(prefix_pattern_ids+str(uuid).replace('-',''))[24:]
+    def make_host_id(uuid) -> str:
+        return 'n'+(prefix_host_ids+str(uuid).replace('-',''))[24:]
+
+    output = ""
+
+    # render_suffix = f"#line:{color};line.dotted;text:{color} : matchedWith"
+    render_suffix = f"[label=\"\",style=dashed,color={color}] ;"
+
+    for pattern_el_name, host_el_name in name_mapping.items():
+        # print(pattern_el_name, host_el_name)
+        try:
+            pattern_el, = bottom.read_outgoing_elements(pattern_m, pattern_el_name)
+            host_el, = bottom.read_outgoing_elements(host_m, host_el_name)
+        except:
+            continue
+        # only render 'match'-edges between objects (= those elements where the type of the type is 'Class'):
+        pattern_el_type = od.get_type(bottom, pattern_el)
+        pattern_el_type_type = od.get_type(bottom, pattern_el_type)
+        if pattern_el_type_type == class_type:
+            output += f"\n{make_pattern_id(pattern_el)} -> {make_host_id(host_el)} {render_suffix}"
+        # elif pattern_el_type_type == attr_link_type:
+        #     pattern_obj = bottom.read_edge_source(pattern_el)
+        #     pattern_attr_name = od.get_attr_name(bottom, pattern_el_type)
+        #     host_obj = bottom.read_edge_source(host_el)
+        #     host_el_type = od.get_type(bottom, host_el)
+        #     host_attr_name = od.get_attr_name(bottom, host_el_type)
+        #     output += f"\n{make_pattern_id(pattern_obj)}::{pattern_attr_name} -> {make_host_id(host_obj)}::{host_attr_name} {render_suffix}"
+    return output
+
+def render_package(name, contents):
+    output = f"subgraph cluster_{name} {{\n  label=\"{name}\";"
+    output += indent(contents, 2)
+    output += "\n}\n"
+    return output

+ 21 - 0
concrete_syntax/plantuml/make_url.py

@@ -0,0 +1,21 @@
+from zlib import compress
+import base64
+import string
+
+maketrans = bytes.maketrans
+
+# Includes code fragments from: https://github.com/dougn/python-plantuml/blob/bb5407e87aabbac9e8baef5a6726b03f72afca16/plantuml.py
+# Copyright (c) 2013, Doug Napoleone and then Copyright (c) 2015, Samuel Marks
+
+plantuml_alphabet = string.digits + string.ascii_uppercase + string.ascii_lowercase + '-_'
+base64_alphabet   = string.ascii_uppercase + string.ascii_lowercase + string.digits + '+/'
+b64_to_plantuml = maketrans(base64_alphabet.encode('utf-8'), plantuml_alphabet.encode('utf-8'))
+
+def encode(plantuml_text: str) -> str:
+    zlibbed_str = compress(plantuml_text.encode('utf-8'))
+    compressed_string = zlibbed_str[2:-4]
+    return base64.b64encode(compressed_string).translate(b64_to_plantuml).decode('utf-8')
+
+def make_url(plantuml_text: str) -> str:
+    encoded = encode(plantuml_text)
+    return f"https://deemz.org/plantuml/pdf/{encoded}"

+ 4 - 4
concrete_syntax/plantuml/renderer.py

@@ -3,9 +3,10 @@
 from services import scd, od
 from services.bottom.V0 import Bottom
 from transformation import ramify
-from concrete_syntax.common import display_value
+from concrete_syntax.common import display_value, display_name
 from uuid import UUID
 
+
 def render_class_diagram(state, model, prefix_ids=""):
     bottom = Bottom(state)
     model_scd = scd.SCD(model, state)
@@ -102,7 +103,7 @@ def render_object_diagram(state, m, mm, render_attributes=True, prefix_ids=""):
             attributes = od.get_attributes(bottom, class_node)
 
         for obj_name, obj_node in m_od.get_objects(class_node).items():
-            output += f"\nmap \"{obj_name} : {class_name}\" as {make_id(obj_node)} {{"
+            output += f"\nmap \"{display_name(obj_name)} : {class_name}\" as {make_id(obj_node)} {{"
 
             if render_attributes:
                 for attr_name, attr_edge in attributes:
@@ -122,7 +123,7 @@ def render_object_diagram(state, m, mm, render_attributes=True, prefix_ids=""):
             src_name = m_od.get_object_name(src_obj)
             tgt_name = m_od.get_object_name(tgt_obj)
 
-            output += f"\n{make_id(src_obj)} -> {make_id(tgt_obj)} : :{assoc_name}"
+            output += f"\n{make_id(src_obj)} -> {make_id(tgt_obj)} : {display_name(link_name)}:{assoc_name}"
 
     return output
 
@@ -229,4 +230,3 @@ def render_trace_match(state, name_mapping: dict, pattern_m: UUID, host_m: UUID,
             host_attr_name = od.get_attr_name(bottom, host_el_type)
             output += f"\n{make_pattern_id(pattern_obj)}::{pattern_attr_name} ..> {make_host_id(host_obj)}::{host_attr_name} {render_suffix}"
     return output
-

+ 28 - 12
examples/cbd/models.py

@@ -1,7 +1,9 @@
+# This module loads all the models (including the transformation rules) and performs a conformance-check on them.
+
 import os
 from framework.conformance import Conformance, render_conformance_check_result
 from concrete_syntax.textual_od import parser
-
+from transformation.ramify import ramify
 
 # get file contents as string
 def read_file(filename):
@@ -9,15 +11,6 @@ def read_file(filename):
     with open(dir+'/'+filename) as file:
         return file.read()
 
-# def parse_and_check(state, cs_file, mm):
-#     m_cs = read_file(cs_file)
-#     try:
-#         _parse_and_check(state, m_cs, mm)
-#     except Exception as e:
-#         e.add_note(f"While parsing '{cs_file}'")
-#         raise
-#     return m
-
 def parse_and_check(state, m_cs, mm, descr: str):
     try:
         m = parser.parse_od(
@@ -25,12 +18,16 @@ def parse_and_check(state, m_cs, mm, descr: str):
             m_text=m_cs,
             mm=mm,
         )
+    except Exception as e:
+        e.add_note("While parsing model " + descr)
+        raise
+    try:
         conf = Conformance(state, m, mm)
         errors = conf.check_nominal()
         if len(errors) > 0:
-            raise Exception(render_conformance_check_result(errors))
+            print(render_conformance_check_result(errors))
     except Exception as e:
-        e.add_note("While parsing model " + descr)
+        e.add_note("In model " + descr)
         raise
     return m
 
@@ -53,3 +50,22 @@ def get_fibonacci(state, scd_mmm):
     m_rt_initial = parse_and_check(state, m_rt_initial_cs, mm_rt, "Fibonacci initial state")
 
     return (mm, mm_rt, m, m_rt_initial)
+
+RULE_NAMES = ["delay"]
+KINDS = ["nac", "lhs", "rhs"]
+
+def get_rules(state, rt_mm):
+    rt_mm_ramified = ramify(state, rt_mm)
+
+    rules = {} # e.g., { "delay": {"nac": <UUID>, "lhs": <UUID>, ...}, ...}
+
+    for rule_name in RULE_NAMES:
+        rule = {}
+        for kind in KINDS:
+            filename = f"models/r_{rule_name}_{kind}.od";
+            cs = read_file(filename)
+            rule_m = parse_and_check(state, cs, rt_mm_ramified, descr=f"'{filename}'")
+            rule[kind] = rule_m
+        rules[rule_name] = rule
+
+    return (rt_mm_ramified, rules)

+ 16 - 16
examples/cbd/models/m_fibonacci.od

@@ -1,34 +1,34 @@
+# Adder, two inputs, one output
 adder:Function {
   func = ```
-    n2_out = in0 + in1
+    n2_out = n0_in + n1_in
   ```;
 }
-n0_in:IntInPort
-n1_in:IntInPort
-n2_out:IntOutPort
+n0_in:InPort
+n1_in:InPort
+n2_out:OutPort
 :hasInPort (adder -> n0_in)
 :hasInPort (adder -> n1_in)
 :hasOutPort (adder -> n2_out)
 
-
-
+# Delay block 0
 d0:Delay
-d0_in:IntInPort
-d0_out:IntOutPort
+d0_in:InPort
+d0_out:OutPort
 :hasInPort (d0 -> d0_in)
 :hasOutPort (d0 -> d0_out)
 
 
-
+# Delay block 1
 d1:Delay
-d1_in:IntInPort
-d1_out:IntOutPort
+d1_in:InPort
+d1_out:OutPort
 :hasInPort (d1 -> d1_in)
 :hasOutPort (d1 -> d1_out)
 
 
-
-:intLink (n2_out -> d1_in)
-:intLink (d1_out -> n1_in)
-:intLink (d1_out -> d0_in)
-:intLink (d1_out -> n0_in)
+# Connections
+conn0:link (n2_out -> d1_in) # n2 becomes n1 in next step
+conn1:link (d1_out -> d0_in) # n1 becomes n0 in next step
+conn2:link (d1_out -> n1_in) # n1 input to adder
+conn3:link (d0_out -> n0_in) # n0 input to adder

+ 3 - 2
examples/cbd/models/m_fibonacci_initial.od

@@ -1,7 +1,8 @@
-d0s:IntState {
+# Initial state for both delay blocks:
+d0s:State {
   state = 0;
 }
-d1s:IntState {
+d1s:State {
   state = 1;
 }
 :delay2State (d0 -> d0s)

+ 31 - 31
examples/cbd/models/mm_design.od

@@ -3,10 +3,10 @@ Block:Class {
 }
 
 InPort:Class {
-  abstract = True;
+  # abstract = True;
 }
 OutPort:Class {
-  abstract = True;
+  # abstract = True;
 }
 
 hasInPort:Association (Block -> InPort) {
@@ -77,8 +77,8 @@ Delay:Class {
       out_type = None
     else:
       out_type = get_type_name(get_target(get_outgoing(this, "hasOutPort")[0]))
-    if in_type != None and out_type != None and in_type[0:3] != out_type[0:3]:
-      errors.append(f"Inport type ({in_type}) differs from outport type ({out_type})")
+    # if in_type != None and out_type != None and in_type[0:3] != out_type[0:3]:
+    #   errors.append(f"Inport type ({in_type}) differs from outport type ({out_type})")
     errors
   ```;
 }
@@ -90,40 +90,40 @@ Delay:Class {
 # Object Diagrams are statically typed, so we must create in/out-ports, and MemorySlots for all primitive types:
 
 
-# Port types
+# # Port types
 
-BoolInPort:Class
-IntInPort:Class
-StrInPort:Class
+# BoolInPort:Class
+# IntInPort:Class
+# StrInPort:Class
 
-BoolOutPort:Class
-IntOutPort:Class
-StrOutPort:Class
+# BoolOutPort:Class
+# IntOutPort:Class
+# StrOutPort:Class
 
-:Inheritance (BoolInPort -> InPort)
-:Inheritance (IntInPort -> InPort)
-:Inheritance (StrInPort -> InPort)
+# :Inheritance (BoolInPort -> InPort)
+# :Inheritance (IntInPort -> InPort)
+# :Inheritance (StrInPort -> InPort)
 
-:Inheritance (BoolOutPort -> OutPort)
-:Inheritance (IntOutPort -> OutPort)
-:Inheritance (StrOutPort -> OutPort)
+# :Inheritance (BoolOutPort -> OutPort)
+# :Inheritance (IntOutPort -> OutPort)
+# :Inheritance (StrOutPort -> OutPort)
 
-# Link types
+# # Link types
 
-boolLink:Association (BoolOutPort -> BoolInPort)
-intLink:Association (IntOutPort -> IntInPort)
-strLink:Association (StrOutPort -> StrInPort)
+# boolLink:Association (BoolOutPort -> BoolInPort)
+# intLink:Association (IntOutPort -> IntInPort)
+# strLink:Association (StrOutPort -> StrInPort)
 
-:Inheritance (boolLink -> link)
-:Inheritance (intLink -> link)
-:Inheritance (strLink -> link)
+# :Inheritance (boolLink -> link)
+# :Inheritance (intLink -> link)
+# :Inheritance (strLink -> link)
 
-# Delay block types
+# # Delay block types
 
-BoolDelay:Class
-IntDelay:Class
-StrDelay:Class
+# BoolDelay:Class
+# IntDelay:Class
+# StrDelay:Class
 
-:Inheritance (BoolDelay -> Delay)
-:Inheritance (IntDelay -> Delay)
-:Inheritance (StrDelay -> Delay)
+# :Inheritance (BoolDelay -> Delay)
+# :Inheritance (IntDelay -> Delay)
+# :Inheritance (StrDelay -> Delay)

+ 58 - 28
examples/cbd/models/mm_runtime.od

@@ -1,28 +1,58 @@
 # Link state ("signal")
-# is optional: absent for yet-to-compute signals
 
-intLink_signal:AttributeLink (intLink -> Integer) {
-  name = "signal";
-  optional = True;
+Signal:Class {
+  # abstract = True;
 }
-boolLink_signal:AttributeLink (boolLink -> Boolean) {
+
+Signal_signal:AttributeLink (Signal -> Integer) {
   name = "signal";
-  optional = True;
+  optional = False;
 }
-strLink_signal:AttributeLink (strLink -> String) {
-  name = "signal";
-  optional = True;
+
+hasSignal:Association (link -> Signal) {
+  # every Signal has 1 link
+  source_lower_cardinality = 1;
+  source_upper_cardinality = 1;
+   # every link has 0..1 Signals: 
+  target_upper_cardinality = 1;
 }
 
+# BoolSignal:Class
+# IntSignal:Class
+# StrSignal:Class
+
+# :Inheritance (BoolSignal -> Signal)
+# :Inheritance (IntSignal -> Signal)
+# :Inheritance (StrSignal -> Signal)
+
+# BoolSignal_signal:AttributeLink (BoolSignal -> Boolean) {
+#   name = "signal";
+#   optional = False;
+# }
+# IntSignal_signal:AttributeLink (IntSignal -> Integer) {
+#   name = "signal";
+#   optional = False;
+# }
+# StrSignal_signal:AttributeLink (StrSignal -> String) {
+#   name = "signal";
+#   optional = False;
+# }
+
 
 
 # Delay block state
 # mandatory - otherwise we cannot determine the output signal of a delay block
 
 State:Class {
-  abstract = True;
+  # abstract = True;
 }
 
+State_state:AttributeLink (State -> Integer) {
+  name = "state";
+  optional = False;
+}
+
+
 delay2State:Association (Delay -> State) {
   source_lower_cardinality = 1;
   source_upper_cardinality = 1;
@@ -30,26 +60,26 @@ delay2State:Association (Delay -> State) {
   target_upper_cardinality = 1;
 }
 
-BoolState:Class
-IntState:Class
-StrState:Class
+# BoolState:Class
+# IntState:Class
+# StrState:Class
 
-:Inheritance (BoolState -> State)
-:Inheritance (IntState -> State)
-:Inheritance (StrState -> State)
+# :Inheritance (BoolState -> State)
+# :Inheritance (IntState -> State)
+# :Inheritance (StrState -> State)
 
 
-BoolState_state:AttributeLink (BoolState -> Boolean) {
-  name = "state";
-  optional = False;
-}
+# BoolState_state:AttributeLink (BoolState -> Boolean) {
+#   name = "state";
+#   optional = False;
+# }
 
-IntState_state:AttributeLink (IntState -> Integer) {
-  name = "state";
-  optional = False;
-}
+# IntState_state:AttributeLink (IntState -> Integer) {
+#   name = "state";
+#   optional = False;
+# }
 
-StrState_state:AttributeLink (StrState -> String) {
-  name = "state";
-  optional = False;
-}
+# StrState_state:AttributeLink (StrState -> String) {
+#   name = "state";
+#   optional = False;
+# }

+ 16 - 0
examples/cbd/models/r_delay_lhs.od

@@ -0,0 +1,16 @@
+# We look for a Delay-block, its outgoing connection, and its State
+
+delay:RAM_Delay
+
+delay_out:RAM_OutPort # abstract
+
+delay_has_output:RAM_hasOutPort (delay -> delay_out)
+
+some_inport:RAM_InPort # abstract
+
+delay_out_conn:RAM_link (delay_out -> some_inport)
+
+
+state:RAM_State
+
+delay_to_state:RAM_delay2State (delay -> state)

+ 14 - 0
examples/cbd/models/r_delay_nac.od

@@ -0,0 +1,14 @@
+# From our LHS:
+
+delay_out:RAM_OutPort # abstract
+
+some_inport:RAM_InPort # abstract
+
+delay_out_conn:RAM_link (delay_out -> some_inport) # abstract
+
+
+# The delay block's outgoing connection already has a signal:
+
+some_signal:RAM_Signal
+
+:RAM_hasSignal (delay_out_conn -> some_signal)

+ 23 - 0
examples/cbd/models/r_delay_rhs.od

@@ -0,0 +1,23 @@
+# Our entire LHS (don't delete anything):
+
+delay:RAM_Delay
+
+delay_out:RAM_OutPort # abstract
+
+delay_has_output:RAM_hasOutPort (delay -> delay_out)
+
+some_inport:RAM_InPort # abstract
+
+delay_out_conn:RAM_link (delay_out -> some_inport) # abstract
+
+state:RAM_State
+
+delay_to_state:RAM_delay2State (delay -> state)
+
+
+# To create:
+
+new_signal:RAM_Signal {
+  RAM_signal = `get_slot_value(match('state'), 'state')`;
+}
+:RAM_hasSignal (delay_out_conn -> new_signal)

+ 90 - 1
examples/cbd/runner.py

@@ -1,8 +1,97 @@
 from state.devstate import DevState
 from bootstrap.scd import bootstrap_scd
+
+from concrete_syntax.common import indent
+from concrete_syntax.textual_od import renderer as od_renderer
+from concrete_syntax.plantuml import renderer as plantuml
+from concrete_syntax.plantuml.make_url import make_url as make_plantuml_url
+from concrete_syntax.graphviz.make_url import make_url as make_graphviz_url
+from concrete_syntax.graphviz import renderer as graphviz
+
+from transformation.matcher.mvs_adapter import match_od
+from transformation.rewriter import rewrite
+from transformation.cloner import clone_od
+
 import models
 
 state = DevState()
 scd_mmm = bootstrap_scd(state)
 
-mm, mm_rt, m, m_rt_initial = models.get_fibonacci(state, scd_mmm)
+mm, mm_rt, m, m_rt_initial = models.get_fibonacci(state, scd_mmm)
+
+mm_rt_ram, rules = models.get_rules(state, mm_rt)
+
+
+
+# print("RT-MM")
+# print(make_plantuml_url(plantuml.render_class_diagram(state, mm_rt)))
+
+
+# print("RAMIFIED RT-MM")
+# print(make_plantuml_url(plantuml.render_class_diagram(state, mm_rt_ram)))
+
+m_rt = m_rt_initial
+
+def get_matches():
+    for rule_name, rule in rules.items():
+        lhs = rule["lhs"]
+
+        lhs_matcher = match_od(state,
+            host_m=m_rt,
+            host_mm=mm_rt,
+            pattern_m=lhs,
+            pattern_mm=mm_rt_ram)
+
+        for i, lhs_match in enumerate(lhs_matcher):
+
+            nac_matcher = match_od(state,
+                host_m=m_rt,
+                host_mm=mm_rt,
+                pattern_m=rule["nac"],
+                pattern_mm=mm_rt_ram,
+                pivot=lhs_match)
+
+            for j, nac_match in enumerate(nac_matcher):
+                break # there may be more NAC-matches, but we already now enough
+            else:
+                # We got a match!
+                yield (rule_name, lhs, rule["rhs"], lhs_match)
+
+while True:
+    # print(make_graphviz_url(graphviz.render_object_diagram(state, m_rt, mm_rt)))
+    cs = od_renderer.render_od(state, m_rt, mm_rt, hide_names=False)
+    print(indent(cs, 6))
+
+
+    matches = list(get_matches())
+    print(f"There are {len(matches)} matches.")
+    if len(matches) == 0:
+        break
+    rule_name, lhs, rhs, lhs_match = matches[0]
+
+
+                # txt = graphviz.render_package("Host", graphviz.render_object_diagram(state, m_rt, mm_rt))
+                # txt += graphviz.render_package("LHS", graphviz.render_object_diagram(state, lhs, mm_rt_ram))
+                # txt += graphviz.render_trace_match(state, lhs_match, lhs, m_rt, color="orange")
+                # match_urls.append(make_graphviz_url(txt))
+
+    print('picking', lhs_match)
+
+    print('rewriting')
+
+    # copy or will be overwritten in-place
+    m_rt = clone_od(state, m_rt, mm_rt)
+    rhs_match = dict(lhs_match)
+
+    rewrite(state,
+        lhs_m=lhs,
+        rhs_m=rhs,
+        pattern_mm=mm_rt_ram,
+        name_mapping=rhs_match,
+        host_m=m_rt,
+        mm=mm_rt)
+
+    # import subprocess
+    # subprocess.run(["firefox", "--new-window", *match_urls])
+
+# get_actions(state, rules, m_rt_initial, mm_rt)

+ 2 - 4
examples/semantics/operational/port/renderer.py

@@ -1,5 +1,5 @@
-import urllib
 from concrete_syntax.common import indent
+from concrete_syntax.graphviz.make_url import make_url
 from examples.semantics.operational.port.helpers import design_to_state, state_to_design, get_time, get_num_ships
 
 def render_port_graphviz(od):
@@ -53,9 +53,7 @@ def render_port_graphviz(od):
             if berth not in already_have:
                 txt += f"{name} -> {od.get_name(berth)} [style=dotted, arrowhead=none, color=chocolate];\n"
 
-    graphviz = f"digraph {{\n{indent(txt, 2)}}}"
-
-    return "https://dreampuf.github.io/GraphvizOnline/#"+urllib.parse.quote(graphviz)
+    return make_url(txt)
 
 def render_port_textual(od):
     txt = ""

+ 2 - 10
framework/conformance.py

@@ -7,22 +7,14 @@ from pprint import pprint
 import traceback
 from concrete_syntax.common import indent
 
+from util.eval import exec_then_eval
+
 from api.cd import CDAPI
 from api.od import ODAPI
 
 import functools
 
 
-# based on https://stackoverflow.com/a/39381428
-# Parses and executes a block of Python code, and returns the eval result of the last statement
-import ast
-def exec_then_eval(code, _globals, _locals):
-    block = ast.parse(code, mode='exec')
-    # assumes last node is an expression
-    last = ast.Expression(block.body.pop().value)
-    exec(compile(block, '<string>', mode='exec'), _globals, _locals)
-    return eval(compile(last, '<string>', mode='eval'), _globals, _locals)
-
 def render_conformance_check_result(error_list):
     if len(error_list) == 0:
         return "CONFORM"

BIN
state.p


+ 2 - 0
transformation/cloner.py

@@ -1,8 +1,10 @@
 from uuid import UUID
 from concrete_syntax.textual_od import parser, renderer
+from concrete_syntax.common import indent
 
 # Clones an object diagram
 def clone_od(state, m: UUID, mm: UUID):
     # cheap-ass implementation: render and parse
     cs = renderer.render_od(state, m, mm, hide_names=False)
+    # print(indent(cs, 6))
     return parser.parse_od(state, cs, mm)

+ 7 - 5
transformation/matcher/matcher.py

@@ -76,10 +76,12 @@ class MatcherState:
         self.boundary = None
 
     @staticmethod
-    def make_initial(host, guest):
+    def make_initial(host, guest, pivot):
         state = MatcherState()
-        state.h_unmatched_vtxs = host.vtxs
-        state.g_unmatched_vtxs = guest.vtxs
+        state.h_unmatched_vtxs = [vtx for vtx in host.vtxs if vtx not in pivot.values()]
+        state.g_unmatched_vtxs = [vtx for vtx in guest.vtxs if vtx not in pivot.keys()]
+        state.mapping_vtxs = pivot
+        state.r_mapping_vtxs = { v: k for k,v in state.mapping_vtxs.items() }
         return state
 
     # Grow the match set (creating a new copy)
@@ -138,9 +140,9 @@ class MatcherVF2:
 
         # print("number of guest connected components:", len(self.guest_component_to_vtxs))
 
-    def match(self):
+    def match(self, pivot={}):
         yield from self._match(
-            state=MatcherState.make_initial(self.host, self.guest),
+            state=MatcherState.make_initial(self.host, self.guest, pivot),
             already_visited=set())
 
 

+ 35 - 28
transformation/matcher/mvs_adapter.py

@@ -1,3 +1,4 @@
+from api.cd import CDAPI
 from state.base import State
 from uuid import UUID
 from services.bottom.V0 import Bottom
@@ -87,6 +88,8 @@ def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""):
         modelrefs = {}
         # constraints = {}
 
+        names = {}
+
         def to_vtx(el, name):
             # print("name:", name)
             if bottom.is_edge(el):
@@ -101,7 +104,9 @@ def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""):
                 #     except:
                 #         pass
                 mvs_edges.append(el)
-                return MVSEdge(el, name)
+                edge = MVSEdge(el, name)
+                names[name] = edge
+                return edge
             # If the value of the el is a ModelRef (only way to detect this is to match a regex - not very clean), then extract it. We'll create a link to the referred model later.
             value = bottom.read_value(el)
             if isinstance(value, str):
@@ -109,13 +114,15 @@ def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""):
                     # side-effect
                     modelrefs[el] = (UUID(value), name)
                     return MVSNode(IS_MODELREF, el, name)
-            return MVSNode(value, el, name)
+            node = MVSNode(value, el, name)
+            names[name] = node
+            return node
 
-        # MVS-Nodes become vertices
+        # Objects and Links become vertices
         uuid_to_vtx = { node: to_vtx(node, prefix+key) for key in bottom.read_keys(model) for node in bottom.read_outgoing_elements(model, key) }
         graph.vtxs = [ vtx for vtx in uuid_to_vtx.values() ]
 
-        # For every MSV-Edge, two edges are created (for src and tgt)
+        # For every Link, two edges are created (for src and tgt)
         for mvs_edge in mvs_edges:
             mvs_src = bottom.read_edge_source(mvs_edge)
             if mvs_src in uuid_to_vtx:
@@ -194,10 +201,13 @@ def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""):
             for link_name, link_node in objects.items():
                 add_types(link_node)
 
-        return graph
+        return names, graph
+
 
+def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}):
 
-def match_od(state, host_m, host_mm, pattern_m, pattern_mm):
+    # compute subtype relations and such:
+    cdapi = CDAPI(state, host_mm)
 
     # Function object for pattern matching. Decides whether to match host and guest vertices, where guest is a RAMified instance (e.g., the attributes are all strings with Python expressions), and the host is an instance (=object diagram) of the original model (=class diagram)
     class RAMCompare:
@@ -208,33 +218,23 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm):
             type_model_id = bottom.state.read_dict(bottom.state.read_root(), "SCD")
             self.scd_model = UUID(bottom.state.read_value(type_model_id))
 
-        def is_subtype_of(self, supposed_subtype: UUID, supposed_supertype: UUID):
-            if supposed_subtype == supposed_supertype:
-                # reflexive:
-                return True
-
-            inheritance_node, = self.bottom.read_outgoing_elements(self.scd_model, "Inheritance")
-
-            for outgoing in self.bottom.read_outgoing_edges(supposed_subtype):
-                if inheritance_node in self.bottom.read_outgoing_elements(outgoing, "Morphism"):
-                    # 'outgoing' is an inheritance link
-                    supertype = self.bottom.read_edge_target(outgoing)
-                    if supertype != supposed_subtype:
-                        if self.is_subtype_of(supertype, supposed_supertype):
-                            return True
-
-            return False
-
         def match_types(self, g_vtx_type, h_vtx_type):
             # types only match with their supertypes
             # we assume that 'RAMifies'-traceability links have been created between guest and host types
             try:
-                g_vtx_original_type = ramify.get_original_type(self.bottom, g_vtx_type)
+                g_vtx_unramified_type = ramify.get_original_type(self.bottom, g_vtx_type)
             except:
                 return False
 
-            return self.is_subtype_of(h_vtx_type, g_vtx_original_type)
+            try:
+                host_type_name = cdapi.type_model_names[h_vtx_type]
+                guest_type_name_unramified = cdapi.type_model_names[g_vtx_unramified_type]
+            except KeyError:
+                return False
 
+            return cdapi.is_subtype(
+                super_type_name=guest_type_name_unramified,
+                sub_type_name=host_type_name)
 
         # Memoizing the result of comparison gives a huge performance boost!
         # Especially `is_subtype_of` is very slow, and will be performed many times over on the same pair of nodes during the matching process.
@@ -299,11 +299,18 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm):
                 return False
 
     # Convert to format understood by matching algorithm
-    host = model_to_graph(state, host_m, host_mm)
-    guest = model_to_graph(state, pattern_m, pattern_mm)
+    h_names, host = model_to_graph(state, host_m, host_mm)
+    g_names, guest = model_to_graph(state, pattern_m, pattern_mm)
+
+
+    graph_pivot = {
+        g_names[guest_name] : h_names[host_name]
+            for guest_name, host_name in pivot.items()
+                if guest_name in g_names
+    }
 
     matcher = MatcherVF2(host, guest, RAMCompare(Bottom(state), OD(host_mm, host_m, state)))
-    for m in matcher.match():
+    for m in matcher.match(graph_pivot):
         # print("\nMATCH:\n", m)
         # Convert mapping
         name_mapping = {}

+ 42 - 18
transformation/ramify.py

@@ -26,6 +26,8 @@ def ramify(state: State, model: UUID, prefix = "RAM_") -> UUID:
     string_modelref = ramified_scd.create_model_ref("String", string_type)
     actioncode_modelref = ramified_scd.create_model_ref("ActionCode", actioncode_type)
 
+    already_ramified = set() # for correct order of ramification
+
     classes = m_scd.get_classes()
     for class_name, class_node in classes.items():
         # For every class in our original model, create a class:
@@ -49,26 +51,48 @@ def ramify(state: State, model: UUID, prefix = "RAM_") -> UUID:
             # Every attribute becomes 'string' type
             # The string will be a Python expression
             ramified_attr_link = ramified_scd._create_attribute_link(prefix+class_name, actioncode_modelref, prefix+attr_name, optional=True)
-            # traceability link
+            # create traceability link
             bottom.create_edge(ramified_attr_link, attr_edge, RAMIFIES_LABEL)
 
-    associations = m_scd.get_associations()
-    for assoc_name, assoc_node in associations.items():
-        # For every association in our original model, create an association:
-        #   - src-min-card: 0
-        #   - src-max-card: same as original
-        #   - tgt-min-card: 0
-        #   - tgt-max-card: same as original
-        _, src_upper_card, _, tgt_upper_card = m_scd.get_assoc_cardinalities(assoc_node)
-        src = m_scd.get_class_name(bottom.read_edge_source(assoc_node))
-        tgt = m_scd.get_class_name(bottom.read_edge_target(assoc_node))
-        # print('creating assoc', src, "->", tgt, ", name =", assoc_name, ", src card = 0 ..", src_upper_card, "and tgt card = 0 ..", tgt_upper_card)
-        ramified_assoc = ramified_scd.create_association(
-            prefix+assoc_name, prefix+src, prefix+tgt,
-            src_max_c=src_upper_card,
-            tgt_max_c=tgt_upper_card)
-        # traceability link
-        bottom.create_edge(ramified_assoc, assoc_node, RAMIFIES_LABEL)
+        already_ramified.add(class_name)
+
+
+    assocs_to_ramify = m_scd.get_associations()
+
+    while len(assocs_to_ramify) > 0:
+        ramify_later = {}
+        for assoc_name, assoc_node in assocs_to_ramify.items():
+            # For every association in our original model, create an association:
+            #   - src-min-card: 0
+            #   - src-max-card: same as original
+            #   - tgt-min-card: 0
+            #   - tgt-max-card: same as original
+
+            if assoc_name in already_ramified:
+                raise Exception("Assertion failed: did not expect this to ever happen!")
+                continue
+
+            _, src_upper_card, _, tgt_upper_card = m_scd.get_assoc_cardinalities(assoc_node)
+            src = m_scd.get_class_name(bottom.read_edge_source(assoc_node))
+            tgt = m_scd.get_class_name(bottom.read_edge_target(assoc_node))
+
+            if src not in already_ramified or tgt not in already_ramified:
+                ramify_later[assoc_name] = assoc_node
+                continue
+
+            # print('creating assoc', src, "->", tgt, ", name =", assoc_name, ", src card = 0 ..", src_upper_card, "and tgt card = 0 ..", tgt_upper_card)
+
+            ramified_assoc = ramified_scd.create_association(name=prefix+assoc_name,
+                source=prefix+src, target=prefix+tgt,
+                src_max_c=src_upper_card,
+                tgt_max_c=tgt_upper_card)
+
+            # create traceability link
+            bottom.create_edge(ramified_assoc, assoc_node, RAMIFIES_LABEL)
+
+            already_ramified.add(assoc_name)
+
+        assocs_to_ramify = ramify_later
 
     for inh_name, inh_node in m_scd.get_inheritances().items():
         # Re-create inheritance links like in our original model:

+ 16 - 8
transformation/rewriter.py

@@ -65,6 +65,7 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
             model_el_name_to_create = pattern_name_to_create + str(i) # use the label of the element in the RHS as a basis
             if len(bottom.read_outgoing_elements(host_m, model_el_name_to_create)) == 0:
                 break # found an available name
+            i += 1
         
         # Determine the type of the thing to create
         rhs_el_to_create, = bottom.read_outgoing_elements(rhs_m, pattern_name_to_create)
@@ -130,30 +131,37 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
 
     # Perform updates (only on values)
     for pattern_el_name in common:
-        model_el_name = name_mapping[pattern_el_name]
-        # print('updating', model_el_name)
-        model_el, = bottom.read_outgoing_elements(host_m, model_el_name)
-        host_type = od.get_type(bottom, model_el)
+        host_el_name = name_mapping[pattern_el_name]
+        host_el, = bottom.read_outgoing_elements(host_m, host_el_name)
+        # print('updating', host_el_name, host_el)
+        host_type = od.get_type(bottom, host_el)
+        # print('we have', pattern_el_name, '->', host_el_name, 'of type', type_name)
         if od.is_typed_by(bottom, host_type, class_type):
             # print(' -> is classs')
             # nothing to do
             pass
+        elif od.is_typed_by(bottom, host_type, assoc_type):
+            print(' -> is association')
+            # nothing to do
+            pass
         elif od.is_typed_by(bottom, host_type, attr_link_type):
             # print(' -> is attr link')
             # nothing to do
             pass
         elif od.is_typed_by(bottom, host_type, modelref_type):
-            # print(' -> is modelref')
-            old_value, _ = od.read_primitive_value(bottom, model_el, mm)
+            print(' -> is modelref')
+            old_value, _ = od.read_primitive_value(bottom, host_el, mm)
             rhs_el, = bottom.read_outgoing_elements(rhs_m, pattern_el_name)
             expr, _ = od.read_primitive_value(bottom, rhs_el, pattern_mm)
             result = eval(expr, {}, {'v': old_value})
             # print('eval result=', result)
             if isinstance(result, int):
                 # overwrite the old value, in-place
-                referred_model_id = UUID(bottom.read_value(model_el))
+                referred_model_id = UUID(bottom.read_value(host_el))
                 Integer(referred_model_id, state).create(result)
             else:
                 raise Exception("Unimplemented type. Value:", result)
         else:
-            raise Exception("Don't know what to do with element of type", host_type)
+            msg = f"Don't know what to do with element '{pattern_el_name}'->'{host_el_name}' of type ({host_type})"
+            # print(msg)
+            raise Exception(msg)

+ 9 - 0
util/eval.py

@@ -0,0 +1,9 @@
+# based on https://stackoverflow.com/a/39381428
+# Parses and executes a block of Python code, and returns the eval result of the last statement
+import ast
+def exec_then_eval(code, _globals, _locals):
+    block = ast.parse(code, mode='exec')
+    # assumes last node is an expression
+    last = ast.Expression(block.body.pop().value)
+    exec(compile(block, '<string>', mode='exec'), _globals, _locals)
+    return eval(compile(last, '<string>', mode='eval'), _globals, _locals)