CBD model is computing Fibonacci numbers! :)

This commit is contained in:
Joeri Exelmans 2024-11-07 15:38:13 +01:00
parent 9c68b288c1
commit 80cba4b9f8
27 changed files with 429 additions and 269 deletions

View file

@ -150,14 +150,18 @@ def render_trace_ramifies(state, mm, ramified_mm, render_attributes=True, prefix
# Render RAMifies-edges between classes # Render RAMifies-edges between classes
for ram_name, ram_class_node in ramified_mm_scd.get_classes().items(): for ram_name, ram_class_node in ramified_mm_scd.get_classes().items():
original_class, = bottom.read_outgoing_elements(ram_class_node, ramify.RAMIFIES_LABEL) original_class = ramify.get_original_type(bottom, ram_class_node)
if original_class == None:
continue # not all classes come from original (e.g., 'GlobalCondition')
original_name = mm_scd.get_class_name(original_class) original_name = mm_scd.get_class_name(original_class)
output += f"\n{make_ram_id(ram_class_node)} ..> {make_orig_id(original_class)} #line:green;text:green : RAMifies" output += f"\n{make_ram_id(ram_class_node)} ..> {make_orig_id(original_class)} #line:green;text:green : RAMifies"
if render_attributes: if render_attributes:
# and between attributes # and between attributes
for (ram_attr_name, ram_attr_edge) in od.get_attributes(bottom, ram_class_node): for (ram_attr_name, ram_attr_edge) in od.get_attributes(bottom, ram_class_node):
orig_attr_edge, = bottom.read_outgoing_elements(ram_attr_edge, ramify.RAMIFIES_LABEL) orig_attr_edge = ramify.get_original_type(bottom, ram_attr_edge)
if orig_attr_edge == None:
continue # not all attributes come from original (e.g., 'condition')
orig_class_node = bottom.read_edge_source(orig_attr_edge) orig_class_node = bottom.read_edge_source(orig_attr_edge)
# dirty AF: # dirty AF:
orig_attr_name = mm_scd.get_class_name(orig_attr_edge)[len(original_name)+1:] orig_attr_name = mm_scd.get_class_name(orig_attr_edge)[len(original_name)+1:]

View file

@ -51,7 +51,7 @@ def get_fibonacci(state, scd_mmm):
return (mm, mm_rt, m, m_rt_initial) return (mm, mm_rt, m, m_rt_initial)
RULE_NAMES = ["delay"] RULE_NAMES = ["delay_out", "function_out", "delay_in", "advance_time"]
KINDS = ["nac", "lhs", "rhs"] KINDS = ["nac", "lhs", "rhs"]
def get_rules(state, rt_mm): def get_rules(state, rt_mm):

View file

@ -1,7 +1,7 @@
# Adder, two inputs, one output # Adder, two inputs, one output
adder:Function { adder:Function {
func = ``` func = ```
n2_out = n0_in + n1_in read('n0_in') + read('n1_in')
```; ```;
} }
n0_in:InPort n0_in:InPort

View file

@ -1,9 +1,13 @@
# Initial state for both delay blocks: # Initial state for both delay blocks:
d0s:State { d0s:State {
state = 0; x = 0;
} }
d1s:State { d1s:State {
state = 1; x = 1;
} }
:delay2State (d0 -> d0s) :delay2State (d0 -> d0s)
:delay2State (d1 -> d1s) :delay2State (d1 -> d1s)
clock:Clock {
time = 0;
}

View file

@ -2,12 +2,8 @@ Block:Class {
abstract = True; abstract = True;
} }
InPort:Class { InPort:Class
# abstract = True; OutPort:Class
}
OutPort:Class {
# abstract = True;
}
hasInPort:Association (Block -> InPort) { hasInPort:Association (Block -> InPort) {
# Every Port contained by exactly one Block: # Every Port contained by exactly one Block:
@ -21,27 +17,12 @@ hasOutPort:Association (Block -> OutPort) {
} }
link:Association (OutPort -> InPort) { link:Association (OutPort -> InPort) {
#abstract = True;
# Every InPort connected to exactly one OutPort # Every InPort connected to exactly one OutPort
source_lower_cardinality = 1; source_lower_cardinality = 1;
source_upper_cardinality = 1; source_upper_cardinality = 1;
} }
# In- and Out-Ports are labeled:
# hasInPort_label:AttributeLink (hasInPort -> String) {
# name = "label";
# optional = False;
# }
# hasOutPort_label:AttributeLink (hasOutPort -> String) {
# name = "label";
# optional = False;
# }
# Function Block: pure function that computes outputs based on inputs # Function Block: pure function that computes outputs based on inputs
Function:Class Function:Class
@ -69,61 +50,9 @@ Delay:Class {
num_outports = len(get_outgoing(this, "hasOutPort")) num_outports = len(get_outgoing(this, "hasOutPort"))
if num_inports != 1: if num_inports != 1:
errors.append(f"Delay block must have one inport, instead got {num_inports}") errors.append(f"Delay block must have one inport, instead got {num_inports}")
in_type = None
else:
in_type = get_type_name(get_target(get_outgoing(this, "hasInPort")[0]))
if num_outports != 1: if num_outports != 1:
errors.append(f"Delay block must have one inport, instead got {num_outports}") errors.append(f"Delay block must have one inport, instead got {num_outports}")
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})")
errors errors
```; ```;
} }
:Inheritance (Delay -> Block) :Inheritance (Delay -> Block)
# Object Diagrams are statically typed, so we must create in/out-ports, and MemorySlots for all primitive types:
# # Port types
# BoolInPort:Class
# IntInPort:Class
# StrInPort:Class
# BoolOutPort:Class
# IntOutPort:Class
# StrOutPort:Class
# :Inheritance (BoolInPort -> InPort)
# :Inheritance (IntInPort -> InPort)
# :Inheritance (StrInPort -> InPort)
# :Inheritance (BoolOutPort -> OutPort)
# :Inheritance (IntOutPort -> OutPort)
# :Inheritance (StrOutPort -> OutPort)
# # Link types
# boolLink:Association (BoolOutPort -> BoolInPort)
# intLink:Association (IntOutPort -> IntInPort)
# strLink:Association (StrOutPort -> StrInPort)
# :Inheritance (boolLink -> link)
# :Inheritance (intLink -> link)
# :Inheritance (strLink -> link)
# # Delay block types
# BoolDelay:Class
# IntDelay:Class
# StrDelay:Class
# :Inheritance (BoolDelay -> Delay)
# :Inheritance (IntDelay -> Delay)
# :Inheritance (StrDelay -> Delay)

View file

@ -1,85 +1,47 @@
# Link state ("signal") # Link state ("signal")
Signal:Class { Signal:Class
# abstract = True;
}
Signal_signal:AttributeLink (Signal -> Integer) { Signal_x:AttributeLink (Signal -> Integer) {
name = "signal"; name = "x";
optional = False; optional = False;
} }
hasSignal:Association (link -> Signal) { hasSignal:Association (OutPort -> Signal) {
# every Signal has 1 link # every Signal has 1 link
source_lower_cardinality = 1; source_lower_cardinality = 1;
source_upper_cardinality = 1; source_upper_cardinality = 1;
# every link has 0..1 Signals: # every link has 0..1 Signals:
target_upper_cardinality = 1; 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 # Delay block state
# mandatory - otherwise we cannot determine the output signal of a delay block # mandatory - otherwise we cannot determine the output signal of a delay block
State:Class { State:Class
# abstract = True;
}
State_state:AttributeLink (State -> Integer) { State_x:AttributeLink (State -> Integer) {
name = "state"; name = "x";
optional = False; optional = False;
} }
delay2State:Association (Delay -> State) { delay2State:Association (Delay -> State) {
# one-to-one
source_lower_cardinality = 1; source_lower_cardinality = 1;
source_upper_cardinality = 1; source_upper_cardinality = 1;
target_lower_cardinality = 1; target_lower_cardinality = 1;
target_upper_cardinality = 1; target_upper_cardinality = 1;
} }
# BoolState:Class Clock:Class {
# IntState:Class lower_cardinality = 1;
# StrState:Class upper_cardinality = 1;
}
# :Inheritance (BoolState -> State) Clock_time:AttributeLink (Clock -> Integer) {
# :Inheritance (IntState -> State) name = "time";
# :Inheritance (StrState -> State) optional = False;
}
# BoolState_state:AttributeLink (BoolState -> Boolean) {
# name = "state";
# optional = False;
# }
# IntState_state:AttributeLink (IntState -> Integer) {
# name = "state";
# optional = False;
# }
# StrState_state:AttributeLink (StrState -> String) {
# name = "state";
# optional = False;
# }

View file

@ -0,0 +1,3 @@
clock:RAM_Clock {
RAM_time = `True`;
}

View file

@ -0,0 +1,36 @@
# If there is a Delay-block whose input signal differs from its state, we cannot yet advance time:
delay:RAM_Delay
delay_in:RAM_InPort
delay_has_input:RAM_hasInPort (delay -> delay_in)
some_outport:RAM_OutPort
delay_in_conn:RAM_link (some_outport -> delay_in)
in_signal:RAM_Signal
port_has_signal:RAM_hasSignal (some_outport -> in_signal)
state:RAM_State {
RAM_x = `get_slot_value(matched('in_signal'), 'x') != get_value(this)`;
}
delay_to_state:RAM_delay2State (delay -> state)
# Also, we cannot advance time until all outports have signals:
:GlobalCondition {
condition = ```
missing_signals = False
for _, outport in get_all_instances("OutPort"):
if len(get_outgoing(outport, 'hasSignal')) == 0:
missing_signals = True
break
missing_signals
```;
}

View file

@ -0,0 +1,10 @@
clock:RAM_Clock {
RAM_time = `get_value(this) + 1`;
}
:GlobalCondition {
condition = ```
for _, signal in get_all_instances("Signal"):
delete(signal)
```;
}

View file

@ -0,0 +1,27 @@
# We look for a Delay-block, its inport connected to an outport that has a signal
delay:RAM_Delay
delay_in:RAM_InPort
delay_has_input:RAM_hasInPort (delay -> delay_in)
some_outport:RAM_OutPort
delay_in_conn:RAM_link (some_outport -> delay_in)
in_signal:RAM_Signal
port_has_signal:RAM_hasSignal (some_outport -> in_signal)
# State of Delay block... (will be updated in RHS)
state:RAM_State {
# Attention: you MUST match the existing attribute, in order to force an UDPATE of the attribute, rather than CREATION
RAM_x = `True`;
}
delay_to_state:RAM_delay2State (delay -> state)

View file

@ -0,0 +1,6 @@
state:RAM_State # <- must repeat elements from LHS that we refer to
in_signal:RAM_Signal {
# If the signal is already equal to the state, the NAC holds:
RAM_x = `get_value(this) == get_slot_value(matched('state'), 'x')`;
}

View file

@ -0,0 +1,26 @@
# Everything from our LHS (don't delete anything)
delay:RAM_Delay
delay_in:RAM_InPort
delay_has_input:RAM_hasOutPort (delay -> delay_in)
some_outport:RAM_OutPort
delay_in_conn:RAM_link (some_outport -> delay_in)
in_signal:RAM_Signal
port_has_signal:RAM_hasSignal (some_outport -> in_signal)
state:RAM_State {
# Update:
RAM_x = ```
new_state = get_slot_value(matched('in_signal'), 'x')
print(f"Updating delay {get_name(matched('delay'))} state: {new_state}")
new_state
```;
}
delay_to_state:RAM_delay2State (delay -> state)

View file

@ -1,16 +0,0 @@
# 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

@ -1,14 +0,0 @@
# 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,14 @@
# We look for a Delay-block, its outport, and its State
delay:RAM_Delay
delay_out:RAM_OutPort
delay_has_output:RAM_hasOutPort (delay -> delay_out)
state:RAM_State
delay_to_state:RAM_delay2State (delay -> state)
clock:RAM_Clock

View file

@ -0,0 +1,10 @@
# From our LHS:
delay_out:RAM_OutPort
# We don't want to see the delay block's outport already having a signal:
some_signal:RAM_Signal
:RAM_hasSignal (delay_out -> some_signal)

View file

@ -0,0 +1,20 @@
# Our entire LHS (don't delete anything):
delay:RAM_Delay
delay_out:RAM_OutPort
delay_has_output:RAM_hasOutPort (delay -> delay_out)
state:RAM_State
delay_to_state:RAM_delay2State (delay -> state)
clock:RAM_Clock
# To create:
new_signal:RAM_Signal {
RAM_x = `get_slot_value(matched('state'), 'x')`;
}
:RAM_hasSignal (delay_out -> new_signal)

View file

@ -1,23 +0,0 @@
# 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

@ -0,0 +1,22 @@
# Match function and its out-connection
f:RAM_Function {
# Only match if the signal of all inputs has been computed:
condition = ```
ok = True
for o in get_outgoing(this, "hasInPort"):
inport = get_target(o)
in_conn = get_incoming(inport, "link")[0]
some_outport = get_source(in_conn)
if len(get_outgoing(some_outport, "hasSignal")) == 0:
ok = False
break
ok
```;
}
f_outport:RAM_OutPort
f_has_outport:RAM_hasOutPort (f -> f_outport)
clock:RAM_Clock

View file

@ -0,0 +1,14 @@
# From our LHS:
f:RAM_Function
f_outport:RAM_OutPort
f_has_outport:RAM_hasOutPort (f -> f_outport)
# We don't want to see the function's out-connection already having a signal:
some_signal:RAM_Signal
:RAM_hasSignal (f_outport -> some_signal)

View file

@ -0,0 +1,26 @@
# Our entire LHS (don't delete anything):
f:RAM_Function
f_outport:RAM_OutPort
f_has_outport:RAM_hasOutPort (f -> f_outport)
clock:RAM_Clock
# To create:
f_out_signal:RAM_Signal {
RAM_x = ```
def read(inport_name):
inport = get(inport_name)
outport = get_source(get_incoming(inport, "link")[0])
signal = get_target(get_outgoing(outport, "hasSignal")[0])
return get_slot_value(signal, "x")
code = get_slot_value(matched('f'), 'func')
eval(code, {}, { 'read': read })
```;
}
:RAM_hasSignal (f_outport -> f_out_signal)

View file

@ -1,6 +1,8 @@
from state.devstate import DevState from state.devstate import DevState
from bootstrap.scd import bootstrap_scd from bootstrap.scd import bootstrap_scd
from framework.conformance import Conformance, render_conformance_check_result
from concrete_syntax.common import indent from concrete_syntax.common import indent
from concrete_syntax.textual_od import renderer as od_renderer from concrete_syntax.textual_od import renderer as od_renderer
from concrete_syntax.plantuml import renderer as plantuml from concrete_syntax.plantuml import renderer as plantuml
@ -17,12 +19,10 @@ import models
state = DevState() state = DevState()
scd_mmm = bootstrap_scd(state) scd_mmm = bootstrap_scd(state)
print("Parsing models...")
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) mm_rt_ram, rules = models.get_rules(state, mm_rt)
# print("RT-MM") # print("RT-MM")
# print(make_plantuml_url(plantuml.render_class_diagram(state, mm_rt))) # print(make_plantuml_url(plantuml.render_class_diagram(state, mm_rt)))
@ -42,8 +42,8 @@ def get_matches():
pattern_m=lhs, pattern_m=lhs,
pattern_mm=mm_rt_ram) pattern_mm=mm_rt_ram)
try:
for i, lhs_match in enumerate(lhs_matcher): for i, lhs_match in enumerate(lhs_matcher):
nac_matcher = match_od(state, nac_matcher = match_od(state,
host_m=m_rt, host_m=m_rt,
host_mm=mm_rt, host_mm=mm_rt,
@ -51,19 +51,31 @@ def get_matches():
pattern_mm=mm_rt_ram, pattern_mm=mm_rt_ram,
pivot=lhs_match) pivot=lhs_match)
try:
for j, nac_match in enumerate(nac_matcher): for j, nac_match in enumerate(nac_matcher):
break # there may be more NAC-matches, but we already now enough break # there may be more NAC-matches, but we already now enough
else: else:
# We got a match! # We got a match!
yield (rule_name, lhs, rule["rhs"], lhs_match) yield (rule_name, lhs, rule["rhs"], lhs_match)
except Exception as e:
# Make exceptions raised in eval'ed code easier to trace:
e.add_note(f"while matching NAC of '{rule_name}'")
raise
except Exception as e:
# Make exceptions raised in eval'ed code easier to trace:
e.add_note(f"while matching LHS of '{rule_name}'")
raise
while True: while True:
# print(make_graphviz_url(graphviz.render_object_diagram(state, m_rt, mm_rt))) # 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) cs = od_renderer.render_od(state, m_rt, mm_rt, hide_names=False)
print(indent(cs, 6)) print(indent(cs, 6))
conf = Conformance(state, m_rt, mm_rt)
print(render_conformance_check_result(conf.check_nominal()))
matches = list(get_matches()) matches = list(get_matches())
print(f"There are {len(matches)} matches.") print(f"There are {len(matches)} matches.")
if len(matches) == 0: if len(matches) == 0:
break break
@ -75,21 +87,23 @@ while True:
# txt += graphviz.render_trace_match(state, lhs_match, lhs, m_rt, color="orange") # txt += graphviz.render_trace_match(state, lhs_match, lhs, m_rt, color="orange")
# match_urls.append(make_graphviz_url(txt)) # match_urls.append(make_graphviz_url(txt))
print('picking', lhs_match) print(f"executing rule '{rule_name}' ", lhs_match)
print('rewriting')
# copy or will be overwritten in-place # copy or will be overwritten in-place
m_rt = clone_od(state, m_rt, mm_rt) m_rt = clone_od(state, m_rt, mm_rt)
rhs_match = dict(lhs_match) rhs_match = dict(lhs_match)
try:
rewrite(state, rewrite(state,
lhs_m=lhs, lhs_m=lhs,
rhs_m=rhs, rhs_m=rhs,
pattern_mm=mm_rt_ram, pattern_mm=mm_rt_ram,
name_mapping=rhs_match, name_mapping=rhs_match,
host_m=m_rt, host_m=m_rt,
mm=mm_rt) host_mm=mm_rt)
except Exception as e:
# Make exceptions raised in eval'ed code easier to trace:
e.add_note(f"while executing RHS of '{rule_name}'")
raise
# import subprocess # import subprocess
# subprocess.run(["firefox", "--new-window", *match_urls]) # subprocess.run(["firefox", "--new-window", *match_urls])

View file

@ -254,7 +254,10 @@ def is_typed_by(bottom, el: UUID, typ: UUID):
def get_typed_by(bottom, model, type_node: UUID): def get_typed_by(bottom, model, type_node: UUID):
name_to_instance = {} name_to_instance = {}
for key in bottom.read_keys(model): for key in bottom.read_keys(model):
element, = bottom.read_outgoing_elements(model, key) els = bottom.read_outgoing_elements(model, key)
if len(els) > 1:
raise Exception(f"Assertion failed: Model contains more than one object named '{key}'!")
element = els[0]
element_types = bottom.read_outgoing_elements(element, "Morphism") element_types = bottom.read_outgoing_elements(element, "Morphism")
if type_node in element_types: if type_node in element_types:
name_to_instance[key] = element name_to_instance[key] = element

View file

@ -76,7 +76,8 @@ UUID_REGEX = re.compile(r"[0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-
# Converts an object diagram in MVS state to the pattern matcher graph type # Converts an object diagram in MVS state to the pattern matcher graph type
# ModelRefs are flattened # ModelRefs are flattened
def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""): def model_to_graph(state: State, model: UUID, metamodel: UUID,
_filter=lambda node: True, prefix=""):
# with Timer("model_to_graph"): # with Timer("model_to_graph"):
od = services_od.OD(model, metamodel, state) od = services_od.OD(model, metamodel, state)
scd = SCD(model, state) scd = SCD(model, state)
@ -121,7 +122,7 @@ def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""):
return node return node
# Objects and Links 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) } 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) if _filter(node) }
graph.vtxs = [ vtx for vtx in uuid_to_vtx.values() ] graph.vtxs = [ vtx for vtx in uuid_to_vtx.values() ]
# For every Link, two edges are created (for src and tgt) # For every Link, two edges are created (for src and tgt)
@ -183,7 +184,6 @@ def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""):
# # print(type_edge) # # print(type_edge)
# graph.edges.append(type_edge) # graph.edges.append(type_edge)
# Add typing information for: # Add typing information for:
# - classes # - classes
# - attributes # - attributes
@ -192,25 +192,31 @@ def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""):
objects = scd.get_typed_by(class_node) objects = scd.get_typed_by(class_node)
# print("typed by:", class_name, objects) # print("typed by:", class_name, objects)
for obj_name, obj_node in objects.items(): for obj_name, obj_node in objects.items():
if _filter(obj_node):
add_types(obj_node) add_types(obj_node)
for attr_name, attr_node in scd_mm.get_attributes(class_name).items(): for attr_name, attr_node in scd_mm.get_attributes(class_name).items():
attrs = scd.get_typed_by(attr_node) attrs = scd.get_typed_by(attr_node)
for slot_name, slot_node in attrs.items(): for slot_name, slot_node in attrs.items():
if _filter(slot_node):
add_types(slot_node) add_types(slot_node)
for assoc_name, assoc_node in scd_mm.get_associations().items(): for assoc_name, assoc_node in scd_mm.get_associations().items():
objects = scd.get_typed_by(assoc_node) objects = scd.get_typed_by(assoc_node)
# print("typed by:", assoc_name, objects) # print("typed by:", assoc_name, objects)
for link_name, link_node in objects.items(): for link_name, link_node in objects.items():
if _filter(link_node):
add_types(link_node) add_types(link_node)
return names, 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, pivot={}):
bottom = Bottom(state)
# compute subtype relations and such: # compute subtype relations and such:
cdapi = CDAPI(state, host_mm) cdapi = CDAPI(state, host_mm)
odapi = ODAPI(state, host_m, host_mm) odapi = ODAPI(state, host_m, host_mm)
pattern_odapi = ODAPI(state, pattern_m, pattern_mm)
pattern_mm_odapi = ODAPI(state, pattern_mm, cdapi.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) # 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: class RAMCompare:
@ -221,6 +227,9 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}):
type_model_id = bottom.state.read_dict(bottom.state.read_root(), "SCD") type_model_id = bottom.state.read_dict(bottom.state.read_root(), "SCD")
self.scd_model = UUID(bottom.state.read_value(type_model_id)) self.scd_model = UUID(bottom.state.read_value(type_model_id))
# constraints need to be checked at the very end, after a complete match is established, because constraint code may refer to matched elements by their name
self.conditions_to_check = []
def match_types(self, g_vtx_type, h_vtx_type): def match_types(self, g_vtx_type, h_vtx_type):
# types only match with their supertypes # types only match with their supertypes
# we assume that 'RAMifies'-traceability links have been created between guest and host types # we assume that 'RAMifies'-traceability links have been created between guest and host types
@ -256,26 +265,10 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}):
return False return False
python_code = services_od.read_primitive_value(self.bottom, g_vtx.node_id, pattern_mm)[0] python_code = services_od.read_primitive_value(self.bottom, g_vtx.node_id, pattern_mm)[0]
return exec_then_eval(python_code,
_globals=bind_api_readonly(odapi),
_locals={'this': h_vtx.node_id})
# nested_matches = [m for m in match_od(state, h_ref_m, h_ref_mm, g_ref_m, g_ref_mm)] self.conditions_to_check.append((python_code, h_vtx.node_id))
return True # do be determined later, if it's actually a match
# print('begin recurse')
# g_ref_m, g_ref_mm = g_vtx.modelref
# h_ref_m, h_ref_mm = h_vtx.modelref
# print('nested_matches:', nested_matches)
# if len(nested_matches) == 0:
# return False
# elif len(nested_matches) == 1:
# return True
# else:
# raise Exception("We have a problem: there is more than 1 match in the nested models.")
# print('end recurse')
# Then, match by value
if g_vtx.value == None: if g_vtx.value == None:
return h_vtx.value == None return h_vtx.value == None
@ -293,20 +286,26 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}):
if h_vtx.value == IS_MODELREF: if h_vtx.value == IS_MODELREF:
return False return False
# python_code = g_vtx.value
# try:
# return exec_then_eval(python_code,
# _globals=bind_api_readonly(odapi),
# _locals={'this': h_vtx.node_id})
# except Exception as e:
# print(e)
# return False
return True return True
# Convert to format understood by matching algorithm # Convert to format understood by matching algorithm
h_names, host = model_to_graph(state, host_m, host_mm) h_names, host = model_to_graph(state, host_m, host_mm)
g_names, guest = model_to_graph(state, pattern_m, pattern_mm)
# Only match matchable pattern elements
# E.g., the 'condition'-attribute that is added to every class, cannot be matched with anything
def is_matchable(pattern_el):
pattern_el_name = pattern_odapi.get_name(pattern_el)
if pattern_odapi.get_type_name(pattern_el) == "GlobalCondition":
return False
# Super-cheap and unreliable way of filtering out the 'condition'-attribute, added to every class:
return not (pattern_el_name.endswith("condition")
# as an extra safety measure, if the user defined her own 'condition' attribute, RAMification turned this into 'RAM_condition', and we can detect this
# of course this breaks if the class name already ended with 'RAM', but let's hope that never happens
# also, we are assuming the default "RAM_" prefix is used, but the user can change this...
and not pattern_el_name.endswith("RAM_condition"))
g_names, guest = model_to_graph(state, pattern_m, pattern_mm,
_filter=is_matchable)
graph_pivot = { graph_pivot = {
g_names[guest_name] : h_names[host_name] g_names[guest_name] : h_names[host_name]
@ -314,7 +313,46 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}):
if guest_name in g_names if guest_name in g_names
} }
matcher = MatcherVF2(host, guest, RAMCompare(Bottom(state), services_od.OD(host_mm, host_m, state))) obj_conditions = []
for class_name, class_node in pattern_mm_odapi.get_all_instances("Class"):
for obj_name, obj_node in pattern_odapi.get_all_instances(class_name):
python_code = pattern_odapi.get_slot_value_default(obj_node, "condition", 'True')
if class_name == "GlobalCondition":
obj_conditions.append((python_code, None))
else:
obj_conditions.append((python_code, obj_name))
def check_conditions(name_mapping):
def check(python_code: str, loc):
return exec_then_eval(python_code,
_globals={
**bind_api_readonly(odapi),
'matched': lambda name: bottom.read_outgoing_elements(host_m, name_mapping[name])[0],
},
_locals=loc)
# Attribute conditions
for python_code, host_node in compare.conditions_to_check:
if not check(python_code, {'this': host_node}):
return False
for python_code, pattern_el_name in obj_conditions:
if pattern_el_name == None:
# GlobalCondition
if not check(python_code, {}):
return False
else:
# object-lvl condition
host_el_name = name_mapping[pattern_el_name]
host_node = odapi.get(host_el_name)
if not check(python_code, {'this': host_node}):
return False
return True
compare = RAMCompare(bottom, services_od.OD(host_mm, host_m, state))
matcher = MatcherVF2(host, guest, compare)
for m in matcher.match(graph_pivot): for m in matcher.match(graph_pivot):
# print("\nMATCH:\n", m) # print("\nMATCH:\n", m)
# Convert mapping # Convert mapping
@ -322,4 +360,8 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}):
for guest_vtx, host_vtx in m.mapping_vtxs.items(): for guest_vtx, host_vtx in m.mapping_vtxs.items():
if isinstance(guest_vtx, NamedNode) and isinstance(host_vtx, NamedNode): if isinstance(guest_vtx, NamedNode) and isinstance(host_vtx, NamedNode):
name_mapping[guest_vtx.name] = host_vtx.name name_mapping[guest_vtx.name] = host_vtx.name
if not check_conditions(name_mapping):
continue # not a match after all...
yield name_mapping yield name_mapping

View file

@ -54,8 +54,13 @@ def ramify(state: State, model: UUID, prefix = "RAM_") -> UUID:
# create traceability link # create traceability link
bottom.create_edge(ramified_attr_link, attr_edge, RAMIFIES_LABEL) bottom.create_edge(ramified_attr_link, attr_edge, RAMIFIES_LABEL)
# Additional constraint that can be specified
ramified_scd._create_attribute_link(prefix+class_name, actioncode_modelref, "condition", optional=True)
already_ramified.add(class_name) already_ramified.add(class_name)
glob_cond = ramified_scd.create_class("GlobalCondition", abstract=None)
ramified_scd._create_attribute_link("GlobalCondition", actioncode_modelref, "condition", optional=False)
assocs_to_ramify = m_scd.get_associations() assocs_to_ramify = m_scd.get_associations()
@ -90,6 +95,9 @@ def ramify(state: State, model: UUID, prefix = "RAM_") -> UUID:
# create traceability link # create traceability link
bottom.create_edge(ramified_assoc, assoc_node, RAMIFIES_LABEL) bottom.create_edge(ramified_assoc, assoc_node, RAMIFIES_LABEL)
# Additional constraint that can be specified
ramified_scd._create_attribute_link(prefix+assoc_name, actioncode_modelref, "condition", optional=True)
already_ramified.add(assoc_name) already_ramified.add(assoc_name)
assocs_to_ramify = ramify_later assocs_to_ramify = ramify_later

View file

@ -11,25 +11,34 @@ from services import od
from services.primitives.string_type import String from services.primitives.string_type import String
from services.primitives.actioncode_type import ActionCode from services.primitives.actioncode_type import ActionCode
from services.primitives.integer_type import Integer from services.primitives.integer_type import Integer
from util.eval import exec_then_eval from util.eval import exec_then_eval, simply_exec
def process_rule(state, lhs: UUID, rhs: UUID): def preprocess_rule(state, lhs: UUID, rhs: UUID, name_mapping):
bottom = Bottom(state) bottom = Bottom(state)
to_delete = { name for name in bottom.read_keys(lhs) if name not in bottom.read_keys(rhs) } to_delete = { name for name in bottom.read_keys(lhs) if name not in bottom.read_keys(rhs) and name in name_mapping }
to_create = { name for name in bottom.read_keys(rhs) if name not in bottom.read_keys(lhs) } 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) } # extremely dirty:
and "GlobalCondition" not in name }
common = { name for name in bottom.read_keys(lhs) if name in bottom.read_keys(rhs) and name in name_mapping }
# print("to_delete:", to_delete) print("to_delete:", to_delete)
# print("to_create:", to_create) print("to_create:", to_create)
return to_delete, to_create, common return to_delete, to_create, common
# Rewrite is performed in-place (modifying `host_m`) # Rewrite is performed in-place (modifying `host_m`)
# Also updates the `mapping` in-place, to become RHS -> host # Also updates the `mapping` in-place, to become RHS -> host
def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dict, host_m: UUID, mm: UUID): def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dict, host_m: UUID, host_mm: UUID):
bottom = Bottom(state) bottom = Bottom(state)
orig_name_mapping = dict(name_mapping)
# function that can be called from within RHS action code
def matched_callback(pattern_name: str):
host_name = orig_name_mapping[pattern_name]
return bottom.read_outgoing_elements(host_m, host_name)[0]
scd_metamodel_id = state.read_dict(state.read_root(), "SCD") scd_metamodel_id = state.read_dict(state.read_root(), "SCD")
scd_metamodel = UUID(state.read_value(scd_metamodel_id)) scd_metamodel = UUID(state.read_value(scd_metamodel_id))
@ -38,12 +47,12 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
assoc_type = od.get_scd_mm_assoc_node(bottom) assoc_type = od.get_scd_mm_assoc_node(bottom)
modelref_type = od.get_scd_mm_modelref_node(bottom) modelref_type = od.get_scd_mm_modelref_node(bottom)
m_od = od.OD(mm, host_m, bottom.state) m_od = od.OD(host_mm, host_m, bottom.state)
rhs_od = od.OD(pattern_mm, rhs_m, bottom.state) rhs_od = od.OD(pattern_mm, rhs_m, bottom.state)
to_delete, to_create, common = process_rule(state, lhs_m, rhs_m) to_delete, to_create, common = preprocess_rule(state, lhs_m, rhs_m, orig_name_mapping)
odapi = ODAPI(state, host_m, mm) odapi = ODAPI(state, host_m, host_mm)
# Perform deletions # Perform deletions
for pattern_name_to_delete in to_delete: for pattern_name_to_delete in to_delete:
@ -89,7 +98,7 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
# print(' -> postpone (is link)') # print(' -> postpone (is link)')
edges_to_create.append((pattern_name_to_create, rhs_el_to_create, original_type, 'link', model_el_name_to_create)) edges_to_create.append((pattern_name_to_create, rhs_el_to_create, original_type, 'link', model_el_name_to_create))
else: else:
original_type_name = od.get_object_name(bottom, mm, original_type) original_type_name = od.get_object_name(bottom, host_mm, original_type)
print(" -> warning: don't know about", original_type_name) print(" -> warning: don't know about", original_type_name)
else: else:
# print(" -> no original (un-RAMified) type") # print(" -> no original (un-RAMified) type")
@ -100,7 +109,11 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
# Assume the string is a Python expression to evaluate # Assume the string is a Python expression to evaluate
python_expr = ActionCode(UUID(bottom.read_value(rhs_el_to_create)), bottom.state).read() python_expr = ActionCode(UUID(bottom.read_value(rhs_el_to_create)), bottom.state).read()
result = exec_then_eval(python_expr, _globals=bind_api(odapi)) result = exec_then_eval(python_expr, _globals={
**bind_api(odapi),
'matched': matched_callback,
})
# Write the result into the host model. # Write the result into the host model.
# This will be the *value* of an attribute. The attribute-link (connecting an object to the attribute) will be created as an edge later. # This will be the *value* of an attribute. The attribute-link (connecting an object to the attribute) will be created as an edge later.
if isinstance(result, int): if isinstance(result, int):
@ -109,7 +122,8 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
m_od.create_string_value(model_el_name_to_create, result) m_od.create_string_value(model_el_name_to_create, result)
name_mapping[pattern_name_to_create] = model_el_name_to_create name_mapping[pattern_name_to_create] = model_el_name_to_create
else: else:
raise Exception(f"RHS element '{pattern_name_to_create}' needs to be created in host, but has no un-RAMified type, and I don't know what to do with it. It's type is '{type_name}'") msg = f"RHS element '{pattern_name_to_create}' needs to be created in host, but has no un-RAMified type, and I don't know what to do with it. It's type is '{type_name}'"
raise Exception(msg)
# print("create edges....") # print("create edges....")
for pattern_name_to_create, rhs_el_to_create, original_type, original_type_name, model_el_name_to_create in edges_to_create: for pattern_name_to_create, rhs_el_to_create, original_type, original_type_name, model_el_name_to_create in edges_to_create:
@ -130,7 +144,7 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
tgt = bottom.read_edge_target(rhs_el_to_create) tgt = bottom.read_edge_target(rhs_el_to_create)
tgt_name = od.get_object_name(bottom, rhs_m, tgt) tgt_name = od.get_object_name(bottom, rhs_m, tgt)
obj_name = name_mapping[src_name] # name of object in host graph to create slot for obj_name = name_mapping[src_name] # name of object in host graph to create slot for
attr_name = od.get_object_name(bottom, mm, original_type) attr_name = od.get_object_name(bottom, host_mm, original_type)
m_od.create_link(model_el_name_to_create, attr_name, obj_name, name_mapping[tgt_name]) m_od.create_link(model_el_name_to_create, attr_name, obj_name, name_mapping[tgt_name])
@ -146,7 +160,7 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
# nothing to do # nothing to do
pass pass
elif od.is_typed_by(bottom, host_type, assoc_type): elif od.is_typed_by(bottom, host_type, assoc_type):
print(' -> is association') # print(' -> is association')
# nothing to do # nothing to do
pass pass
elif od.is_typed_by(bottom, host_type, attr_link_type): elif od.is_typed_by(bottom, host_type, attr_link_type):
@ -154,12 +168,15 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
# nothing to do # nothing to do
pass pass
elif od.is_typed_by(bottom, host_type, modelref_type): elif od.is_typed_by(bottom, host_type, modelref_type):
print(' -> is modelref') # print(' -> is modelref')
old_value, _ = od.read_primitive_value(bottom, host_el, mm) old_value, _ = od.read_primitive_value(bottom, host_el, host_mm)
rhs_el, = bottom.read_outgoing_elements(rhs_m, pattern_el_name) rhs_el, = bottom.read_outgoing_elements(rhs_m, pattern_el_name)
python_expr, _ = od.read_primitive_value(bottom, rhs_el, pattern_mm) python_expr, _ = od.read_primitive_value(bottom, rhs_el, pattern_mm)
result = exec_then_eval(python_expr, result = exec_then_eval(python_expr,
_globals=bind_api(odapi), _globals={
**bind_api(odapi),
'matched': matched_callback,
},
_locals={'this': host_el}) _locals={'this': host_el})
# print('eval result=', result) # print('eval result=', result)
if isinstance(result, int): if isinstance(result, int):
@ -172,3 +189,11 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
msg = f"Don't know what to do with element '{pattern_el_name}'->'{host_el_name}' 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) # print(msg)
raise Exception(msg) raise Exception(msg)
rhs_odapi = ODAPI(state, rhs_m, pattern_mm)
for cond_name, cond in rhs_odapi.get_all_instances("GlobalCondition"):
python_code = rhs_odapi.get_slot_value(cond, "condition")
simply_exec(python_code, _globals={
**bind_api(odapi),
'matched': matched_callback,
})

View file

@ -6,8 +6,16 @@ def exec_then_eval(code, _globals={}, _locals={}):
# assumes last node is an expression # assumes last node is an expression
last = ast.Expression(block.body.pop().value) last = ast.Expression(block.body.pop().value)
extended_globals = { extended_globals = {
'__builtins__': {'isinstance': isinstance, 'print': print, 'int': int, 'float': float, 'bool': bool, 'str': str, 'tuple': tuple, 'len': len, 'set': set, 'dict': dict }, '__builtins__': {'isinstance': isinstance, 'print': print, 'int': int, 'float': float, 'bool': bool, 'str': str, 'tuple': tuple, 'len': len, 'set': set, 'dict': dict, 'eval': eval },
**_globals, **_globals,
} }
exec(compile(block, '<string>', mode='exec'), extended_globals, _locals) exec(compile(block, '<string>', mode='exec'), extended_globals, _locals)
return eval(compile(last, '<string>', mode='eval'), extended_globals, _locals) return eval(compile(last, '<string>', mode='eval'), extended_globals, _locals)
def simply_exec(code, _globals={}, _locals={}):
block = ast.parse(code, mode='exec')
extended_globals = {
'__builtins__': {'isinstance': isinstance, 'print': print, 'int': int, 'float': float, 'bool': bool, 'str': str, 'tuple': tuple, 'len': len, 'set': set, 'dict': dict, 'eval': eval },
**_globals,
}
exec(compile(block, '<string>', mode='exec'), extended_globals, _locals)