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

This commit is contained in:
Joeri Exelmans 2024-11-07 09:46:29 +01:00
parent a26ceef10f
commit 1eb8a84553
25 changed files with 542 additions and 170 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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}"

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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;
# }

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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_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)

View file

@ -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 = ""

View file

@ -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

Binary file not shown.

View file

@ -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)

View file

@ -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())

View file

@ -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 = {}

View file

@ -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:

View file

@ -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
util/eval.py Normal file
View file

@ -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)