diff --git a/api/od.py b/api/od.py index d4d4716..da5eef6 100644 --- a/api/od.py +++ b/api/od.py @@ -1,6 +1,10 @@ from services import od from api import cd from services.bottom.V0 import Bottom +from services.primitives.boolean_type import Boolean +from services.primitives.integer_type import Integer +from services.primitives.string_type import String +from services.primitives.actioncode_type import ActionCode from uuid import UUID from typing import Optional @@ -12,8 +16,10 @@ def build_name_mapping(state, m): mapping = {} bottom = Bottom(state) for name in bottom.read_keys(m): - element, = bottom.read_outgoing_elements(m, name) - mapping[element] = name + elements = bottom.read_outgoing_elements(m, name) + if len(elements) > 1: + print(f"Warning: more than one element with name '{name}'") + mapping[elements[0]] = name return mapping class NoSuchSlotException(Exception): @@ -194,6 +200,22 @@ class ODAPI: self.__recompute_mappings() return tgt + def overwrite_primitive_value(self, name: str, value: any, is_code=False): + referred_model = UUID(self.bottom.read_value(self.get(name))) + # watch out: in Python, 'bool' is subtype of 'int' + # so we must check for 'bool' first + if isinstance(value, bool): + Boolean(referred_model, self.state).create(value) + elif isinstance(value, int): + Integer(referred_model, self.state).create(value) + elif isinstance(value, str): + if is_code: + ActionCode(referred_model, self.state).create(value) + else: + String(referred_model, self.state).create(value) + else: + raise Exception("Unimplemented type "+value) + def create_link(self, link_name: Optional[str], assoc_name: str, src: UUID, tgt: UUID): global NEXT_ID types = self.bottom.read_outgoing_elements(self.mm, assoc_name) diff --git a/examples/cbd/models/r_advance_time_nac.od b/examples/cbd/models/r_advance_time_nac.od index a1546ab..2d48280 100644 --- a/examples/cbd/models/r_advance_time_nac.od +++ b/examples/cbd/models/r_advance_time_nac.od @@ -1,28 +1,6 @@ +# We cannot advance time until all outports have signals -# 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: +# BTW, this NAC is not really necessary, because our schedule already will only try to match 'advance_time' when no other actions are enabled :GlobalCondition { condition = ``` diff --git a/examples/cbd/models/r_advance_time_rhs.od b/examples/cbd/models/r_advance_time_rhs.od index 490cf0b..fcd0efc 100644 --- a/examples/cbd/models/r_advance_time_rhs.od +++ b/examples/cbd/models/r_advance_time_rhs.od @@ -2,6 +2,8 @@ clock:RAM_Clock { RAM_time = `get_value(this) + 1`; } +# Delete all Signals: + :GlobalCondition { condition = ``` for _, signal in get_all_instances("Signal"): diff --git a/examples/cbd/models/r_delay_in_lhs.od b/examples/cbd/models/r_delay_in_lhs.od index d7e1393..f5efebf 100644 --- a/examples/cbd/models/r_delay_in_lhs.od +++ b/examples/cbd/models/r_delay_in_lhs.od @@ -2,17 +2,22 @@ delay:RAM_Delay -delay_in:RAM_InPort +delay_inport:RAM_InPort -delay_has_input:RAM_hasInPort (delay -> delay_in) +delay_has_inport:RAM_hasInPort (delay -> delay_inport) some_outport:RAM_OutPort -delay_in_conn:RAM_link (some_outport -> delay_in) +delay_in_conn:RAM_link (some_outport -> delay_inport) -in_signal:RAM_Signal +in_signal:RAM_Signal { + # If the signal is already equal to the state, don't match: + # (without this, the rule could keep firing) -port_has_signal:RAM_hasSignal (some_outport -> in_signal) + RAM_x = `get_value(this) != get_slot_value(matched('state'), 'x')`; +} + +port_in_signal:RAM_hasSignal (some_outport -> in_signal) @@ -20,8 +25,16 @@ port_has_signal:RAM_hasSignal (some_outport -> in_signal) 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) + + + +# Only update Delay block state IF after its output signal has been computed: + +delay_outport:RAM_OutPort +delay_has_outport:RAM_hasOutPort (delay -> delay_outport) +out_signal:RAM_Signal +delay_out_signal:RAM_hasSignal (delay_outport -> out_signal) diff --git a/examples/cbd/models/r_delay_in_nac.od b/examples/cbd/models/r_delay_in_nac.od index aced4c5..b3c704e 100644 --- a/examples/cbd/models/r_delay_in_nac.od +++ b/examples/cbd/models/r_delay_in_nac.od @@ -1,6 +1,5 @@ -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')`; -} +:GlobalCondition { + # No NAC + condition = `False`; +} \ No newline at end of file diff --git a/examples/cbd/models/r_delay_in_rhs.od b/examples/cbd/models/r_delay_in_rhs.od index 9353dc5..0173481 100644 --- a/examples/cbd/models/r_delay_in_rhs.od +++ b/examples/cbd/models/r_delay_in_rhs.od @@ -2,17 +2,20 @@ delay:RAM_Delay -delay_in:RAM_InPort +delay_inport:RAM_InPort -delay_has_input:RAM_hasOutPort (delay -> delay_in) +delay_has_inport:RAM_hasOutPort (delay -> delay_inport) some_outport:RAM_OutPort -delay_in_conn:RAM_link (some_outport -> delay_in) +delay_in_conn:RAM_link (some_outport -> delay_inport) -in_signal:RAM_Signal +in_signal:RAM_Signal { + # Need to repeat this slot, otherwise it will be deleted: + RAM_x = `get_value(this)`; +} -port_has_signal:RAM_hasSignal (some_outport -> in_signal) +port_in_signal:RAM_hasSignal (some_outport -> in_signal) state:RAM_State { # Update: @@ -24,3 +27,9 @@ state:RAM_State { } delay_to_state:RAM_delay2State (delay -> state) + + +delay_outport:RAM_OutPort +delay_has_outport:RAM_hasOutPort (delay -> delay_outport) +out_signal:RAM_Signal +delay_out_signal:RAM_hasSignal (delay_outport -> out_signal) diff --git a/examples/cbd/models/r_delay_out_lhs.od b/examples/cbd/models/r_delay_out_lhs.od index f063681..e9dd3c2 100644 --- a/examples/cbd/models/r_delay_out_lhs.od +++ b/examples/cbd/models/r_delay_out_lhs.od @@ -10,5 +10,3 @@ delay_has_output:RAM_hasOutPort (delay -> delay_out) state:RAM_State delay_to_state:RAM_delay2State (delay -> state) - -clock:RAM_Clock \ No newline at end of file diff --git a/examples/cbd/models/r_delay_out_rhs.od b/examples/cbd/models/r_delay_out_rhs.od index 94abd70..6677521 100644 --- a/examples/cbd/models/r_delay_out_rhs.od +++ b/examples/cbd/models/r_delay_out_rhs.od @@ -10,8 +10,6 @@ state:RAM_State delay_to_state:RAM_delay2State (delay -> state) -clock:RAM_Clock - # To create: new_signal:RAM_Signal { diff --git a/examples/cbd/models/r_function_out_lhs.od b/examples/cbd/models/r_function_out_lhs.od index dd1a4d8..69b9bc9 100644 --- a/examples/cbd/models/r_function_out_lhs.od +++ b/examples/cbd/models/r_function_out_lhs.od @@ -18,5 +18,3 @@ f:RAM_Function { f_outport:RAM_OutPort f_has_outport:RAM_hasOutPort (f -> f_outport) - -clock:RAM_Clock diff --git a/examples/cbd/models/r_function_out_rhs.od b/examples/cbd/models/r_function_out_rhs.od index 688cd8f..dfc4449 100644 --- a/examples/cbd/models/r_function_out_rhs.od +++ b/examples/cbd/models/r_function_out_rhs.od @@ -6,8 +6,6 @@ f_outport:RAM_OutPort f_has_outport:RAM_hasOutPort (f -> f_outport) -clock:RAM_Clock - # To create: f_out_signal:RAM_Signal { diff --git a/examples/cbd/runner.py b/examples/cbd/runner.py index ac92aca..a550bb7 100644 --- a/examples/cbd/runner.py +++ b/examples/cbd/runner.py @@ -1,7 +1,10 @@ +import functools +import pprint + from state.devstate import DevState from bootstrap.scd import bootstrap_scd -from framework.conformance import Conformance, render_conformance_check_result +from api.od import ODAPI from concrete_syntax.common import indent from concrete_syntax.textual_od import renderer as od_renderer @@ -14,8 +17,88 @@ from transformation.matcher.mvs_adapter import match_od from transformation.rewriter import rewrite from transformation.cloner import clone_od +from examples.semantics.operational import simulator + import models + +def match_rule(rule_name, od: ODAPI, lhs, nac): + lhs_matcher = match_od(state, + host_m=od.m, + host_mm=od.mm, + pattern_m=lhs, + pattern_mm=mm_rt_ram) + + try: + for i, lhs_match in enumerate(lhs_matcher): + nac_matcher = match_od(state, + host_m=od.m, + host_mm=od.mm, + pattern_m=nac, + pattern_mm=mm_rt_ram, + pivot=lhs_match) + + try: + for j, nac_match in enumerate(nac_matcher): + break # there may be more NAC-matches, but we already now enough -> proceed to next lhs_match + else: + yield lhs_match # got 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 + +def exec_action(rule_name, od: ODAPI, lhs, rhs, lhs_match): + # copy these, will be overwritten in-place + cloned_m = clone_od(state, od.m, od.mm) + rhs_match = dict(lhs_match) + + try: + rewrite(state, + lhs_m=lhs, + rhs_m=rhs, + pattern_mm=mm_rt_ram, + lhs_name_mapping=rhs_match, + host_m=cloned_m, + host_mm=od.mm) + 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 + + print("Updated match:\n" + indent(pp.pformat(rhs_match), 6)) + + return (ODAPI(state, cloned_m, od.mm), [f"executed rule '{rule_name}'"]) + +pp = pprint.PrettyPrinter(depth=4) + +def attempt_rules(od: ODAPI, rule_dict): + at_least_one_match = False + for rule_name, rule in rule_dict.items(): + for lhs_match in match_rule(rule_name, od, rule["lhs"], rule["nac"]): + # We got a match! + yield (rule_name + '\n' + indent(pp.pformat(lhs_match), 6), + functools.partial(exec_action, + rule_name, od, rule["lhs"], rule["rhs"], lhs_match)) + at_least_one_match = True + return at_least_one_match + +def get_actions(od: ODAPI): + # transformation schedule + rule_advance_time = rules["advance_time"] + rules_not_advancing_time = { rule_name: rule for rule_name, rule in rules.items() if rule_name != "advance_time" } + + at_least_one_match = yield from attempt_rules(od, rules_not_advancing_time) + if not at_least_one_match: + yield from attempt_rules(od, {"advance_time": rule_advance_time}) + + + state = DevState() scd_mmm = bootstrap_scd(state) @@ -26,86 +109,17 @@ 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 +sim = simulator.Simulator( + action_generator=get_actions, + # decision_maker=simulator.InteractiveDecisionMaker(auto_proceed=False), + decision_maker=simulator.RandomDecisionMaker(seed=0), + termination_condition=lambda od: "Time is up" if od.get_slot_value(od.get_all_instances("Clock")[0][1], "time") >= 10 else None, + check_conformance=True, + verbose=True, + renderer=lambda od: od_renderer.render_od(state, od.m, od.mm, hide_names=False), +) -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) - - try: - 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) - - try: - 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) - 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: - # 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)) - conf = Conformance(state, m_rt, mm_rt) - print(render_conformance_check_result(conf.check_nominal())) - - 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(f"executing rule '{rule_name}' ", lhs_match) - - # copy or will be overwritten in-place - m_rt = clone_od(state, m_rt, mm_rt) - rhs_match = dict(lhs_match) - try: - rewrite(state, - lhs_m=lhs, - rhs_m=rhs, - pattern_mm=mm_rt_ram, - name_mapping=rhs_match, - host_m=m_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 - # subprocess.run(["firefox", "--new-window", *match_urls]) - -# get_actions(state, rules, m_rt_initial, mm_rt) \ No newline at end of file +sim.run(ODAPI(state, m_rt_initial, mm_rt)) diff --git a/examples/semantics/operational/simulator.py b/examples/semantics/operational/simulator.py index 87bd9a3..a603f62 100644 --- a/examples/semantics/operational/simulator.py +++ b/examples/semantics/operational/simulator.py @@ -36,7 +36,7 @@ class Simulator: print(*args) # Run simulation until termination condition satisfied - def run(self, od): + def run(self, od: ODAPI): self.__print("Start simulation") self.__print(f"Decision maker: {self.decision_maker}") step_counter = 0 @@ -112,8 +112,10 @@ class RandomDecisionMaker(DecisionMaker): return arr[i] class InteractiveDecisionMaker(DecisionMaker): - def __init__(self, msg="Select action:"): + # auto_proceed: whether to prompt if there is only one enabled action + def __init__(self, msg="Select action:", auto_proceed=False): self.msg = msg + self.auto_proceed = auto_proceed def __str__(self): return f"InteractiveDecisionMaker()" @@ -125,6 +127,8 @@ class InteractiveDecisionMaker(DecisionMaker): arr.append(result) if len(arr) == 0: return + if len(arr) == 1 and self.auto_proceed: + return arr[0] def __choose(): sys.stdout.write(f"{self.msg} ") diff --git a/transformation/rewriter.py b/transformation/rewriter.py index b0b84a5..8f97ea1 100644 --- a/transformation/rewriter.py +++ b/transformation/rewriter.py @@ -13,6 +13,7 @@ from services.primitives.actioncode_type import ActionCode from services.primitives.integer_type import Integer from util.eval import exec_then_eval, simply_exec + def preprocess_rule(state, lhs: UUID, rhs: UUID, name_mapping): bottom = Bottom(state) @@ -22,21 +23,27 @@ def preprocess_rule(state, lhs: UUID, rhs: UUID, name_mapping): 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_create:", to_create) - return to_delete, to_create, common +class TryAgainNextRound(Exception): + pass + # Rewrite is performed in-place (modifying `host_m`) -# 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, host_mm: UUID): +def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, lhs_name_mapping: dict, host_m: UUID, host_mm: UUID): bottom = Bottom(state) - orig_name_mapping = dict(name_mapping) + # Need to come up with a new, unique name when creating new element in host-model: + def first_available_name(prefix: str): + i = 0 + while True: + name = prefix + str(i) + if len(bottom.read_outgoing_elements(host_m, name)) == 0: + return name # found unique name + i += 1 # function that can be called from within RHS action code def matched_callback(pattern_name: str): - host_name = orig_name_mapping[pattern_name] + host_name = lhs_name_mapping[pattern_name] return bottom.read_outgoing_elements(host_m, host_name)[0] scd_metamodel_id = state.read_dict(state.read_root(), "SCD") @@ -45,155 +52,163 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic 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) + actioncode_type = od.get_scd_mm_actioncode_node(bottom) modelref_type = od.get_scd_mm_modelref_node(bottom) - m_od = od.OD(host_mm, host_m, bottom.state) + # To be replaced by ODAPI (below) + host_od = od.OD(host_mm, host_m, bottom.state) rhs_od = od.OD(pattern_mm, rhs_m, bottom.state) - to_delete, to_create, common = preprocess_rule(state, lhs_m, rhs_m, orig_name_mapping) + host_odapi = ODAPI(state, host_m, host_mm) + host_mm_odapi = ODAPI(state, host_mm, scd_metamodel) + rhs_odapi = ODAPI(state, rhs_m, pattern_mm) + rhs_mm_odapi = ODAPI(state, pattern_mm, scd_metamodel) + + to_delete, to_create, common = preprocess_rule(state, lhs_m, rhs_m, lhs_name_mapping) + + print("to_delete:", to_delete) + print("to_create:", to_create) + + # to be grown + rhs_name_mapping = { name : lhs_name_mapping[name] for name in common } - odapi = ODAPI(state, host_m, host_mm) # 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_el_name_to_delete = name_mapping[pattern_name_to_delete] + model_el_name_to_delete = lhs_name_mapping[pattern_name_to_delete] # print('deleting', model_el_name_to_delete) # Look up the matched element in the host graph el_to_delete, = bottom.read_outgoing_elements(host_m, model_el_name_to_delete) # Delete bottom.delete_element(el_to_delete) - # Remove from mapping - del name_mapping[pattern_name_to_delete] - # extended_mapping = dict(name_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: - # 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_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) - rhs_type = od.get_type(bottom, rhs_el_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_el_name_to_create, original_type) - name_mapping[pattern_name_to_create] = model_el_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_el_to_create, original_type, 'attribute link', model_el_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_el_to_create, original_type, 'link', model_el_name_to_create)) + # Perform creations - in the right order! + remaining_to_create = list(to_create) + while len(remaining_to_create) > 0: + next_round = [] + for rhs_name in remaining_to_create: + # Determine the type of the thing to create + rhs_obj = rhs_odapi.get(rhs_name) + rhs_type = rhs_odapi.get_type(rhs_obj) + host_type = ramify.get_original_type(bottom, rhs_type) + # for debugging: + if host_type != None: + host_type_name = host_odapi.get_name(host_type) else: - original_type_name = od.get_object_name(bottom, host_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, pattern_mm, rhs_type) - if type_name == "ActionCode": - # Assume the string is a Python expression to evaluate - python_expr = ActionCode(UUID(bottom.read_value(rhs_el_to_create)), bottom.state).read() + host_type_name = "" - result = exec_then_eval(python_expr, _globals={ - **bind_api(odapi), - 'matched': matched_callback, - }) + def get_src_tgt(): + src = rhs_odapi.get_source(rhs_obj) + tgt = rhs_odapi.get_target(rhs_obj) + src_name = rhs_odapi.get_name(src) + tgt_name = rhs_odapi.get_name(tgt) + try: + host_src_name = rhs_name_mapping[src_name] + host_tgt_name = rhs_name_mapping[tgt_name] + except KeyError: + # some creations (e.g., edges) depend on other creations + raise TryAgainNextRound() + host_src = host_odapi.get(host_src_name) + host_tgt = host_odapi.get(host_tgt_name) + return (host_src_name, host_tgt_name, host_src, host_tgt) - # 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. - if isinstance(result, int): - m_od.create_integer_value(model_el_name_to_create, result) - elif isinstance(result, str): - m_od.create_string_value(model_el_name_to_create, result) - name_mapping[pattern_name_to_create] = model_el_name_to_create - else: - 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) + try: + if od.is_typed_by(bottom, rhs_type, class_type): + obj_name = first_available_name(rhs_name) + host_od._create_object(obj_name, host_type) + host_odapi._ODAPI__recompute_mappings() + rhs_name_mapping[rhs_name] = obj_name + elif od.is_typed_by(bottom, rhs_type, assoc_type): + _, _, host_src, host_tgt = get_src_tgt() + link_name = first_available_name(rhs_name) + host_od._create_link(link_name, host_type, host_src, host_tgt) + host_odapi._ODAPI__recompute_mappings() + rhs_name_mapping[rhs_name] = link_name + elif od.is_typed_by(bottom, rhs_type, attr_link_type): + host_src_name, _, host_src, host_tgt = get_src_tgt() + host_attr_link = ramify.get_original_type(bottom, rhs_type) + host_attr_name = host_mm_odapi.get_slot_value(host_attr_link, "name") + link_name = f"{host_src_name}_{host_attr_name}" # must follow naming convention here + host_od._create_link(link_name, host_type, host_src, host_tgt) + host_odapi._ODAPI__recompute_mappings() + rhs_name_mapping[rhs_name] = link_name + elif rhs_type == rhs_mm_odapi.get("ActionCode"): + # If we encounter ActionCode in our RHS, we assume that the code computes the value of an attribute... + # This will be the *value* of an attribute. The attribute-link (connecting an object to the attribute) will be created as an edge later. - # 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: - # print('creating', pattern_name_to_create) - if original_type_name == 'attribute link': - # print(' -> creating attribute link') - src = bottom.read_edge_source(rhs_el_to_create) - src_name = od.get_object_name(bottom, rhs_m, src) - tgt = bottom.read_edge_target(rhs_el_to_create) - 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 - orig_attr_name = od.get_attr_name(bottom, original_type) - m_od.create_slot(orig_attr_name, obj_name, name_mapping[tgt_name]) - elif original_type_name == 'link': - # print(' -> creating link') - src = bottom.read_edge_source(rhs_el_to_create) - src_name = od.get_object_name(bottom, rhs_m, src) - tgt = bottom.read_edge_target(rhs_el_to_create) - 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 - 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]) + # Problem: attributes must follow the naming pattern '.' + # So we must know the host-object-name, and the host-attribute-name. + # However, all we have access to here is the name of the attribute in the RHS. + # We cannot even see the link to the RHS-object. + # But, assuming the RHS-attribute is also named '.', we can: + rhs_src_name, rhs_attr_name = rhs_name.split('.') + try: + host_src_name = rhs_name_mapping[rhs_src_name] + except KeyError: + # unmet dependency - object to which attribute belongs not created yet + raise TryAgainNextRound() + rhs_src_type = rhs_odapi.get_type(rhs_odapi.get(rhs_src_name)) + rhs_src_type_name = rhs_mm_odapi.get_name(rhs_src_type) + rhs_attr_link_name = f"{rhs_src_type_name}_{rhs_attr_name}" + rhs_attr_link = rhs_mm_odapi.get(rhs_attr_link_name) + host_attr_link = ramify.get_original_type(bottom, rhs_attr_link) + host_attr_name = host_mm_odapi.get_slot_value(host_attr_link, "name") + val_name = f"{host_src_name}.{host_attr_name}" + python_expr = ActionCode(UUID(bottom.read_value(rhs_obj)), bottom.state).read() + result = exec_then_eval(python_expr, _globals={ + **bind_api(host_odapi), + 'matched': matched_callback, + }) + host_odapi.create_primitive_value(val_name, result, is_code=False) + host_odapi._ODAPI__recompute_mappings() + rhs_name_mapping[rhs_name] = val_name + else: + rhs_type_name = rhs_odapi.get_name(rhs_type) + raise Exception(f"Host type {host_type_name} of pattern element '{rhs_name}:{rhs_type_name}' is not a class, association or attribute link. Don't know what to do with it :(") + except TryAgainNextRound: + next_round.append(rhs_name) + if len(next_round) == len(remaining_to_create): + raise Exception("Creation of objects did not make any progress - there must be some kind of cyclic dependency?!") + + remaining_to_create = next_round # Perform updates (only on values) - for pattern_el_name in common: - 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) + for common_name in common: + host_obj_name = rhs_name_mapping[common_name] + host_obj = host_odapi.get(host_obj_name) + host_type = host_odapi.get_type(host_obj) 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, host_el, host_mm) - rhs_el, = bottom.read_outgoing_elements(rhs_m, pattern_el_name) - python_expr, _ = od.read_primitive_value(bottom, rhs_el, pattern_mm) + rhs_obj = rhs_odapi.get(common_name) + python_expr = ActionCode(UUID(bottom.read_value(rhs_obj)), bottom.state).read() result = exec_then_eval(python_expr, _globals={ - **bind_api(odapi), + **bind_api(host_odapi), 'matched': matched_callback, }, - _locals={'this': host_el}) - # print('eval result=', result) - if isinstance(result, int): - # overwrite the old value, in-place - referred_model_id = UUID(bottom.read_value(host_el)) - Integer(referred_model_id, state).create(result) - else: - raise Exception("Unimplemented type. Value:", result) + _locals={'this': host_obj}) # 'this' can be used to read the previous value of the slot + host_odapi.overwrite_primitive_value(host_obj_name, result, is_code=False) else: - 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 '{common_name}' -> '{host_obj_name}:{host_type}')" # print(msg) raise Exception(msg) - rhs_odapi = ODAPI(state, rhs_m, pattern_mm) + # Execute global conditions 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), + **bind_api(host_odapi), 'matched': matched_callback, })