(WIP) implementing CBD language... Meta-meta-model: Association inherits from Class. Matcher accepts pivot. Add generic graphviz renderer.
This commit is contained in:
parent
a26ceef10f
commit
1eb8a84553
25 changed files with 542 additions and 170 deletions
|
|
@ -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)
|
||||
|
|
@ -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())
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}):
|
||||
|
||||
# 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 = {}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue