diff --git a/experiments/exp_scd.py b/experiments/exp_scd.py index 24f2a2a..59b4f37 100644 --- a/experiments/exp_scd.py +++ b/experiments/exp_scd.py @@ -79,6 +79,8 @@ def main(): tgt_max_c=None, ) + print(dsl_mm_scd.list_elements()) + conf = Conformance(state, dsl_mm_id, scd_mm_id) print("conforms?", conf.check_nominal(log=True)) @@ -86,17 +88,22 @@ def main(): dsl_m_id = state.create_node() dsl_m_od = OD(dsl_mm_id, dsl_m_id, state) + # dsl_m_od.create_object("animal", "Animal") dsl_m_od.create_object("george", "Man") + dsl_m_od.create_slot("weight", "george", + dsl_m_od.create_integer_value("george.weight", 80)) + + # "george_weight" + dsl_m_od.create_object("bear1", "Bear") dsl_m_od.create_object("bear2", "Bear") dsl_m_od.create_link("georgeAfraidOfBear1", "afraidOf", "george", "bear1") dsl_m_od.create_link("georgeAfraidOfBear2", "afraidOf", "george", "bear2") - dsl_m_od.create_slot("weight", "george", - dsl_m_od.create_integer_value("george.weight", 80)) conf2 = Conformance(state, dsl_m_id, dsl_mm_id) - print("Model conforms?", conf2.check_nominal(log=True)) + print("DSL instance conforms?", conf2.check_nominal(log=True)) + print(conf2.type_mapping) # RAMify MM ramified_mm_id = ramify(state, dsl_mm_id) @@ -105,10 +112,10 @@ def main(): lhs_id = state.create_node() lhs_od = OD(ramified_mm_id, lhs_id, state) - lhs_od.create_object("man", "RAM_Man") - lhs_od.create_slot("RAM_weight", "man", lhs_od.create_string_value("man.RAM_weight", 'v < 99')) - lhs_od.create_object("scaryAnimal", "RAM_Animal") - lhs_od.create_link("manAfraidOfAnimal", "RAM_afraidOf", "man", "scaryAnimal") + lhs_od.create_object("man", "Man") + lhs_od.create_slot("weight", "man", lhs_od.create_string_value("man.weight", 'v < 99')) + lhs_od.create_object("scaryAnimal", "Animal") + lhs_od.create_link("manAfraidOfAnimal", "afraidOf", "man", "scaryAnimal") conf3 = Conformance(state, lhs_id, ramified_mm_id) print("LHS conforms?", conf3.check_nominal(log=True)) @@ -117,8 +124,13 @@ def main(): rhs_id = state.create_node() rhs_od = OD(ramified_mm_id, rhs_id, state) - rhs_od.create_object("man", "RAM_Man") - rhs_od.create_slot("RAM_weight", "man", rhs_od.create_string_value("man.RAM_weight", 'v + 5')) + rhs_od.create_object("man", "Man") + rhs_od.create_slot("weight", "man", rhs_od.create_string_value("man.weight", 'v + 5')) + + rhs_od.create_object("bill", "Man") + rhs_od.create_slot("weight", "bill", rhs_od.create_string_value("bill.weight", '100')) + + rhs_od.create_link("billAfraidOfMan", "afraidOf", "bill", "man") conf4 = Conformance(state, rhs_id, ramified_mm_id) print("RHS conforms?", conf4.check_nominal(log=True)) @@ -137,7 +149,6 @@ def main(): print("matching...") matcher = MatcherVF2(host, guest, mvs_adapter.RAMCompare(Bottom(state), dsl_m_od)) - prev = None for m in matcher.match(): print("\nMATCH:\n", m) name_to_matched = {} @@ -145,7 +156,7 @@ def main(): if isinstance(guest_vtx, mvs_adapter.NamedNode) and isinstance(host_vtx, mvs_adapter.NamedNode): name_to_matched[guest_vtx.name] = host_vtx.name print(name_to_matched) - rewriter.rewrite(state, lhs_id, rhs_id, name_to_matched, dsl_m_id) + rewriter.rewrite(state, lhs_id, rhs_id, ramified_mm_id, name_to_matched, dsl_m_id, dsl_mm_id) break print("DONE") diff --git a/pattern_matching/mvs_adapter.py b/pattern_matching/mvs_adapter.py index 50f42a7..29d54dc 100644 --- a/pattern_matching/mvs_adapter.py +++ b/pattern_matching/mvs_adapter.py @@ -227,6 +227,7 @@ class RAMCompare: # 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. + # Assuming the model is not altered *during* matching, this is safe. @functools.cache def __call__(self, g_vtx, h_vtx): # First check if the types match (if we have type-information) diff --git a/services/od.py b/services/od.py index 5790c92..f19b490 100644 --- a/services/od.py +++ b/services/od.py @@ -3,6 +3,7 @@ from state.base import State from services.bottom.V0 import Bottom from services.primitives.integer_type import Integer from services.primitives.string_type import String +from services.primitives.boolean_type import Boolean from typing import Optional def get_attr_link_name(class_name: str, attr_name: str): @@ -29,13 +30,21 @@ class OD: def create_object(self, name: str, class_name: str): class_node, = self.bottom.read_outgoing_elements(self.type_model, class_name) - - abstract_nodes = self.bottom.read_outgoing_elements(self.type_model, f"{class_name}.abstract") - if len(abstract_nodes) == 1: - is_abstract = self.bottom.read_value(abstract_node) + return self._create_object(name, class_node) + + def _create_object(self, name: str, class_node: UUID): + # Look at our `type_model` as if it's an object diagram: + mm_od = OD( + get_scd_mm(self.bottom), # the type model of our type model + self.type_model, + self.bottom.state) + # # Read the 'abstract' slot of the class + abstract_slot = mm_od.get_slot(class_node, "abstract") + print('abstract_slot:', abstract_slot) + if abstract_slot != None: + is_abstract = Boolean(abstract_slot, self.bottom.state).read() else: - # 'abstract' is optional attribute, default is False is_abstract = False if is_abstract: @@ -51,6 +60,7 @@ class OD: object_node, = self.bottom.read_outgoing_elements(self.model, object_name) # get the object return self._get_class_of_object(object_node) + def _get_class_of_object(self, object_node: UUID): type_el, = self.bottom.read_outgoing_elements(object_node, "Morphism") for key in self.bottom.read_keys(self.type_model): @@ -61,8 +71,11 @@ class OD: def create_slot(self, attr_name: str, object_name: str, target_name: str): class_name = self.get_class_of_object(object_name) attr_link_name = get_attr_link_name(class_name, attr_name) + print('attr_link_name:', attr_link_name) # An attribute-link is indistinguishable from an ordinary link: - return self.create_link(None, attr_link_name, object_name, target_name) + return self.create_link( + get_attr_link_name(object_name, attr_name), + attr_link_name, object_name, target_name) def get_slot(self, object_node: UUID, attr_name: str): # I really don't like how complex and inefficient it is to read an attribute of an object... @@ -109,8 +122,6 @@ class OD: src_obj_node, = self.bottom.read_outgoing_elements(self.model, src_obj_name) tgt_obj_node, = self.bottom.read_outgoing_elements(self.model, tgt_obj_name) - link_edge = self.bottom.create_edge(src_obj_node, tgt_obj_node) - # generate a unique name for the link if link_name == None: i = 0; @@ -119,9 +130,60 @@ class OD: if len(self.bottom.read_outgoing_elements(self.model, link_name)) == 0: break i += 1 - - self.bottom.create_edge(self.model, link_edge, link_name) + print('link_name:', link_name) type_edge, = self.bottom.read_outgoing_elements(self.type_model, assoc_name) - self.bottom.create_edge(link_edge, type_edge, "Morphism") + return self._create_link(link_name, type_edge, src_obj_node, tgt_obj_node) + + def _create_link(self, link_name: str, type_edge: str, src_obj_node: UUID, tgt_obj_node: UUID): + # the link itself is unlabeled: + link_edge = self.bottom.create_edge(src_obj_node, tgt_obj_node) + # it is only in the context of the model, that the link has a name: + self.bottom.create_edge(self.model, link_edge, link_name) # add to model + self.bottom.create_edge(link_edge, type_edge, "Morphism") + return link_edge + +def get_types(bottom: Bottom, obj: UUID): + return bottom.read_outgoing_elements(obj, "Morphism") + +def get_type(bottom: Bottom, obj: UUID): + types = get_types(bottom, obj) + if len(types) == 1: + return types[0] + elif len(types) > 1: + raise Exception(f"Expected at most one type. Instead got {len(types)}.") + +def is_typed_by(bottom, el: UUID, typ: UUID): + for typed_by in get_types(bottom, el): + if typed_by == typ: + return True + return False + +def get_scd_mm(bottom): + scd_metamodel_id = bottom.state.read_dict(bottom.state.read_root(), "SCD") + scd_metamodel = UUID(bottom.state.read_value(scd_metamodel_id)) + return scd_metamodel + +def get_scd_mm_class_node(bottom: Bottom): + return get_scd_mm_node(bottom, "Class") + +def get_scd_mm_attributelink_node(bottom: Bottom): + return get_scd_mm_node(bottom, "AttributeLink") + +def get_scd_mm_assoc_node(bottom: Bottom): + return get_scd_mm_node(bottom, "Association") + +def get_scd_mm_modelref_node(bottom: Bottom): + return get_scd_mm_node(bottom, "ModelRef") + +def get_scd_mm_node(bottom: Bottom, node_name: str): + scd_metamodel = get_scd_mm(bottom) + node, = bottom.read_outgoing_elements(scd_metamodel, node_name) + return node + +def get_object_name(bottom: Bottom, model: UUID, object_node: UUID): + for key in bottom.read_keys(model): + for el in bottom.read_outgoing_elements(model, key): + if el == object_node: + return key diff --git a/services/primitives/boolean_type.py b/services/primitives/boolean_type.py index 9f157f2..0fc4113 100644 --- a/services/primitives/boolean_type.py +++ b/services/primitives/boolean_type.py @@ -18,3 +18,7 @@ class Boolean: self.bottom.create_edge(self.model, _instance, "boolean") _type, = self.bottom.read_outgoing_elements(self.type_model, "Boolean") self.bottom.create_edge(_instance, _type, "Morphism") + + def read(self): + instance, = self.bottom.read_outgoing_elements(self.model, "boolean") + return self.bottom.read_value(instance) \ No newline at end of file diff --git a/services/primitives/string_type.py b/services/primitives/string_type.py index 926c1cd..420b592 100644 --- a/services/primitives/string_type.py +++ b/services/primitives/string_type.py @@ -18,3 +18,7 @@ class String: self.bottom.create_edge(self.model, _instance, "string") _type, = self.bottom.read_outgoing_elements(self.type_model, "String") self.bottom.create_edge(_instance, _type, "Morphism") + + def read(self): + instance, = self.bottom.read_outgoing_elements(self.model, "string") + return self.bottom.read_value(instance) \ No newline at end of file diff --git a/services/scd.py b/services/scd.py index b8688f2..b4a95ae 100644 --- a/services/scd.py +++ b/services/scd.py @@ -327,8 +327,11 @@ class SCD: def get_attributes(self, class_name: str): - attr_link_node, = self.bottom.read_outgoing_elements(self.scd_model, "AttributeLink") class_node, = self.bottom.read_outgoing_elements(self.model, class_name) + return self._get_attributes(class_node) + + def _get_attributes(self, class_node: UUID): + attr_link_node, = self.bottom.read_outgoing_elements(self.scd_model, "AttributeLink") name_to_attr = {} for name in self.bottom.read_keys(class_node): edges = self.bottom.read_outgoing_edges(class_node, name) diff --git a/transformation/ramify.py b/transformation/ramify.py index c4ae5e0..8d519c3 100644 --- a/transformation/ramify.py +++ b/transformation/ramify.py @@ -6,7 +6,7 @@ from framework.conformance import Conformance RAMIFIES_LABEL = "RAMifies" -def ramify(state: State, model: UUID, prefix = "RAM_") -> UUID: +def ramify(state: State, model: UUID, prefix = "") -> UUID: # def print_tree(root, max_depth, depth=0): # print(" "*depth, "root=", root, "value=", state.read_value(root)) @@ -174,7 +174,7 @@ def ramify(state: State, model: UUID, prefix = "RAM_") -> UUID: # Every RAMified type has a link to its original type def get_original_type(bottom, typ: UUID): original_types = bottom.read_outgoing_elements(typ, RAMIFIES_LABEL) - if len(original_types) != 1: - raise Exception("Expected 1 original type, got " + str(len(original_types))) - else: + if len(original_types) > 1: + raise Exception("Expected at most 1 original type, got " + str(len(original_types))) + elif len(original_types) == 1: return original_types[0] diff --git a/transformation/rewriter.py b/transformation/rewriter.py index 10677c5..56499fe 100644 --- a/transformation/rewriter.py +++ b/transformation/rewriter.py @@ -5,6 +5,10 @@ from uuid import UUID from services.bottom.V0 import Bottom +from transformation import ramify +from services import od +from services.primitives.string_type import String +from services.primitives.integer_type import Integer def process_rule(state, lhs: UUID, rhs: UUID): bottom = Bottom(state) @@ -12,25 +16,165 @@ def process_rule(state, lhs: UUID, rhs: UUID): # : bottom.read_outgoing_elements(rhs, name)[0] to_delete = { name for name in bottom.read_keys(lhs) if name not in bottom.read_keys(rhs) } to_create = { name for name in bottom.read_keys(rhs) if name not in bottom.read_keys(lhs) } + common = { name for name in bottom.read_keys(lhs) if name in bottom.read_keys(rhs) } print("to_delete:", to_delete) print("to_create:", to_create) - return to_delete, to_create + return to_delete, to_create, common -def rewrite(state, lhs: UUID, rhs: UUID, match_mapping: dict, model_to_transform: UUID) -> UUID: +def rewrite(state, lhs: UUID, rhs: UUID, rhs_mm: UUID, match_mapping: dict, m_to_transform: UUID, mm: UUID) -> UUID: bottom = Bottom(state) - to_delete, to_create = process_rule(state, lhs, rhs) + scd_metamodel_id = state.read_dict(state.read_root(), "SCD") + scd_metamodel = UUID(state.read_value(scd_metamodel_id)) + class_type = od.get_scd_mm_class_node(bottom) + attr_link_type = od.get_scd_mm_attributelink_node(bottom) + assoc_type = od.get_scd_mm_assoc_node(bottom) + modelref_type = od.get_scd_mm_modelref_node(bottom) + + m_od = od.OD(mm, m_to_transform, bottom.state) + rhs_od = od.OD(rhs_mm, rhs, bottom.state) + + print('rhs type:', od.get_type(bottom, rhs)) + + to_delete, to_create, common = process_rule(state, lhs, rhs) + + # Perform deletions for pattern_name_to_delete in to_delete: # For every name in `to_delete`, look up the name of the matched element in the host graph model_element_name_to_delete = match_mapping[pattern_name_to_delete] print('deleting', model_element_name_to_delete) # Look up the matched element in the host graph - element_to_delete, = bottom.read_outgoing_elements(model_to_transform, model_element_name_to_delete) + element_to_delete, = bottom.read_outgoing_elements(m_to_transform, model_element_name_to_delete) # Delete bottom.delete_element(element_to_delete) + extended_mapping = dict(match_mapping) # will be extended with created elements + edges_to_create = [] # postpone creation of edges after creation of nodes + + # Perform creations for pattern_name_to_create in to_create: - pass \ No newline at end of file + print('creating', pattern_name_to_create) + # We have to come up with a name for the element-to-create in the host graph + i = 0 + while True: + model_element_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(m_to_transform, model_element_name_to_create)) == 0: + break # found an available name + + # Determine the type of the thing to create + rhs_element_to_create, = bottom.read_outgoing_elements(rhs, pattern_name_to_create) + rhs_type = od.get_type(bottom, rhs_element_to_create) + original_type = ramify.get_original_type(bottom, rhs_type) + if original_type != None: + # Now get the type of the type + if od.is_typed_by(bottom, original_type, class_type): + # It's type is typed by Class -> it's an object + print(' -> creating object') + o = m_od._create_object(model_element_name_to_create, original_type) + extended_mapping[pattern_name_to_create] = model_element_name_to_create + elif od.is_typed_by(bottom, original_type, attr_link_type): + print(' -> postpone (is attribute link)') + edges_to_create.append((pattern_name_to_create, rhs_element_to_create, original_type, 'attribute link', rhs_type, model_element_name_to_create)) + elif od.is_typed_by(bottom, original_type, assoc_type): + print(' -> postpone (is link)') + edges_to_create.append((pattern_name_to_create, rhs_element_to_create, original_type, 'link', rhs_type, model_element_name_to_create)) + else: + original_type_name = od.get_object_name(bottom, mm, original_type) + print(" -> warning: don't know about", original_type_name) + else: + print(" -> no original (un-RAMified) type") + # assume the type of the object is already the original type + # this is because primitive types (e.g., Integer) are not RAMified + type_name = od.get_object_name(bottom, rhs_mm, rhs_type) + if type_name == "String": + s_model = UUID(bottom.read_value(rhs_element_to_create)) + python_expr = String(s_model, bottom.state).read() + result = eval(python_expr, {}, {}) + print('result:', result) + if isinstance(result, int): + m_od.create_integer_value(model_element_name_to_create, result) + elif isinstance(result, str): + m_od.create_string_value(model_element_name_to_create, result) + extended_mapping[pattern_name_to_create] = model_element_name_to_create + + + print('extended_mapping:', extended_mapping) + + print("create edges....") + for pattern_name_to_create, rhs_element_to_create, original_type, original_type_name, rhs_type, model_element_name_to_create in edges_to_create: + print('creating', pattern_name_to_create) + if original_type_name == 'attribute link': + print(' -> creating attribute link') + src = bottom.read_edge_source(rhs_element_to_create) + src_name = od.get_object_name(bottom, rhs, src) + tgt = bottom.read_edge_target(rhs_element_to_create) + tgt_name = od.get_object_name(bottom, rhs, tgt) + obj_name = extended_mapping[src_name] # name of object in host graph to create slot for + attr_name = od.get_object_name(bottom, mm, original_type) + class_name = m_od.get_class_of_object(obj_name) + # Just when you thought the code couldn't get any dirtier: + attribute_name = attr_name[len(class_name)+1:] + # print(attribute_name, obj_name, extended_mapping[tgt_name]) + m_od.create_slot(attribute_name, obj_name, extended_mapping[tgt_name]) + elif original_type_name == 'link': + print(' -> creating link') + src = bottom.read_edge_source(rhs_element_to_create) + src_name = od.get_object_name(bottom, rhs, src) + tgt = bottom.read_edge_target(rhs_element_to_create) + tgt_name = od.get_object_name(bottom, rhs, tgt) + obj_name = extended_mapping[src_name] # name of object in host graph to create slot for + attr_name = od.get_object_name(bottom, mm, original_type) + class_name = m_od.get_class_of_object(obj_name) + # print(attr_name, obj_name, extended_mapping[tgt_name]) + m_od.create_link(model_element_name_to_create, attr_name, obj_name, extended_mapping[tgt_name]) + + # Perform updates + for pattern_element_name in common: + model_element_name = match_mapping[pattern_element_name] + print('updating', model_element_name) + model_element, = bottom.read_outgoing_elements(m_to_transform, model_element_name) + old_value = bottom.read_value(model_element) + print('old value:', old_value) + host_type = od.get_type(bottom, model_element) + if od.is_typed_by(bottom, host_type, class_type): + print(' -> is classs') + elif od.is_typed_by(bottom, host_type, attr_link_type): + print(' -> is attr link') + elif od.is_typed_by(bottom, host_type, modelref_type): + print(' -> is modelref') + referred_model_id = UUID(bottom.read_value(model_element)) + # referred_model_type = od.get_type(bottom, referred_model_id) # None + # print('referred_model_type:', referred_model_type) + + host_type_name = od.get_object_name(bottom, mm, host_type) + print('host_type_name:', host_type_name) + if host_type_name == "Integer": + v = Integer(UUID(old_value), state).read() + elif host_type_name == "String": + v = String(UUID(old_value), state).read() + else: + raise Exception("Unimplemented type:", host_type_name) + + # the referred model itself doesn't have a type, so we have to look at the type of the ModelRef element in the RHS-MM: + rhs_element, = bottom.read_outgoing_elements(rhs, pattern_element_name) + rhs_type = od.get_type(bottom, rhs_element) + rhs_type_name = od.get_object_name(bottom, rhs_mm, rhs_type) + print("rhs_type_name:", rhs_type_name) + + print(od.get_object_name(bottom, mm, model_element)) + + if rhs_type_name == "String": + python_expr = String(UUID(bottom.read_value(rhs_element)), state).read() + result = eval(python_expr, {}, {'v': v}) + print('eval result=', result) + if isinstance(result, int): + # overwrite the old value + Integer(UUID(old_value), state).create(result) + else: + raise Exception("Unimplemented type. Value:", result) + + + # type_name = od.get_object_name()