diff --git a/api/od.py b/api/od.py index 5e9d195..d4d4716 100644 --- a/api/od.py +++ b/api/od.py @@ -211,3 +211,42 @@ class ODAPI: def create_object(self, object_name: Optional[str], class_name: str): return self.od.create_object(object_name, class_name) + + +# internal use +# Get API methods as bound functions, to pass as globals to 'eval' +# Readonly version is used for: +# - Conformance checking +# - Pattern matching (LHS/NAC of rule) +def bind_api_readonly(odapi): + funcs = { + 'read_value': odapi.state.read_value, + 'get': odapi.get, + 'get_value': odapi.get_value, + 'get_target': odapi.get_target, + 'get_source': odapi.get_source, + 'get_slot': odapi.get_slot, + 'get_slot_value': odapi.get_slot_value, + 'get_slot_value_default': odapi.get_slot_value_default, + 'get_all_instances': odapi.get_all_instances, + 'get_name': odapi.get_name, + 'get_type_name': odapi.get_type_name, + 'get_outgoing': odapi.get_outgoing, + 'get_incoming': odapi.get_incoming, + 'has_slot': odapi.has_slot, + } + return funcs + +# internal use +# Get API methods as bound functions, to pass as globals to 'eval' +# Read/write version is used for: +# - Graph rewriting (RHS of rule) +def bind_api(odapi): + funcs = { + **bind_api_readonly(odapi), + 'create_object': odapi.create_object, + 'create_link': odapi.create_link, + 'delete': odapi.delete, + 'set_slot_value': odapi.set_slot_value, + } + return funcs diff --git a/concrete_syntax/common.py b/concrete_syntax/common.py index b6b598e..3427b03 100644 --- a/concrete_syntax/common.py +++ b/concrete_syntax/common.py @@ -4,14 +4,16 @@ def indent(multiline_string, how_much): lines = multiline_string.split('\n') return '\n'.join([' '*how_much+l for l in lines]) -def display_value(val: any, type_name: str, indentation=0): +def display_value(val: any, type_name: str, indentation=0, newline_character='\n'): if type_name == "ActionCode": if '\n' in val: - return '```\n'+indent(val, indentation+4)+'\n'+' '*indentation+'```' + orig = '```\n'+indent(val, indentation+4)+'\n'+' '*indentation+'```' + escaped = orig.replace('\n', newline_character) + return escaped else: return '`'+val+'`' elif type_name == "String": - return '"'+val+'"' + return '"'+val+'"'.replace('\n', newline_character) elif type_name == "Integer" or type_name == "Boolean": return str(val) else: diff --git a/concrete_syntax/plantuml/renderer.py b/concrete_syntax/plantuml/renderer.py index c68f356..bf984a5 100644 --- a/concrete_syntax/plantuml/renderer.py +++ b/concrete_syntax/plantuml/renderer.py @@ -110,7 +110,8 @@ def render_object_diagram(state, m, mm, render_attributes=True, prefix_ids=""): 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)}" + escaped_newline = ";" + output += f"\n{attr_name} => {display_value(val, type_name, newline_character=escaped_newline)}" output += '\n}' output += '\n' diff --git a/examples/model_transformation/woods.py b/examples/model_transformation/woods.py index 322ebb4..688ac33 100644 --- a/examples/model_transformation/woods.py +++ b/examples/model_transformation/woods.py @@ -13,6 +13,7 @@ from transformation import rewriter from services.bottom.V0 import Bottom from services.primitives.integer_type import Integer from concrete_syntax.plantuml import renderer as plantuml +from concrete_syntax.plantuml.make_url import make_url as make_plantuml_url from concrete_syntax.textual_od import parser, renderer def main(): @@ -112,7 +113,9 @@ def main(): # object to match man:{prefix}Man {{ # match only men heavy enough - {prefix}weight = `v > 60`; + {prefix}weight = ``` + get_value(this) > 60 + ```; }} # object to delete @@ -134,7 +137,7 @@ def main(): # matched object man:{prefix}Man {{ # man gains weight - {prefix}weight = `v + 5`; + {prefix}weight = `get_value(this) + 5`; }} # object to create @@ -216,7 +219,7 @@ def main(): # Render conformance uml += plantuml.render_trace_conformance(state, snapshot_dsl_m_id, dsl_mm_id) - return uml + return make_plantuml_url(uml) # plantuml_str = render_all_matches() plantuml_str = render_rewrite() diff --git a/framework/conformance.py b/framework/conformance.py index 67a975b..df5a4bf 100644 --- a/framework/conformance.py +++ b/framework/conformance.py @@ -10,7 +10,7 @@ from concrete_syntax.common import indent from util.eval import exec_then_eval from api.cd import CDAPI -from api.od import ODAPI +from api.od import ODAPI, bind_api_readonly import functools @@ -138,7 +138,7 @@ class Conformance: for ref_inst_name, ref_inst in self.odapi.get_all_instances(ref_name): sub_m = UUID(self.bottom.read_value(ref_inst)) nested_errors = Conformance(self.state, sub_m, sub_mm).check_nominal() - errors += [f"In ModelRef ({m_name}):" + err for err in nested_errors] + errors += [f"In ModelRef ({ref_name}):" + err for err in nested_errors] return errors @@ -219,38 +219,6 @@ class Conformance: errors.append(f"Target cardinality of type '{assoc_name}' ({count}) out of bounds ({lc}..{uc}) in '{obj_name}'.") return errors - def evaluate_constraint(self, code, **kwargs): - """ - Evaluate constraint code (Python code) - """ - - funcs = { - 'read_value': self.state.read_value, - 'get': self.odapi.get, - 'get_value': self.odapi.get_value, - 'get_target': self.odapi.get_target, - 'get_source': self.odapi.get_source, - 'get_slot': self.odapi.get_slot, - 'get_slot_value': self.odapi.get_slot_value, - 'get_all_instances': self.odapi.get_all_instances, - 'get_name': self.odapi.get_name, - 'get_type_name': self.odapi.get_type_name, - 'get_outgoing': self.odapi.get_outgoing, - 'get_incoming': self.odapi.get_incoming, - 'has_slot': self.odapi.has_slot, - } - # print("evaluating constraint ...", code) - loc = {**kwargs, } - result = exec_then_eval( - code, - {'__builtins__': {'isinstance': isinstance, 'print': print, - 'int': int, 'float': float, 'bool': bool, 'str': str, 'tuple': tuple, 'len': len, 'set': set, 'dict': dict}, - **funcs - }, # globals - loc # locals - ) - return result - def check_constraints(self): """ Check whether all constraints defined for a model are respected @@ -288,7 +256,7 @@ class Conformance: description = f"Local constraint of \"{type_name}\" in \"{obj_name}\"" # print(description) try: - result = self.evaluate_constraint(code, this=obj_id) # may raise + result = exec_then_eval(code, _globals=bind_api_readonly(self.odapi), _locals={'this': obj_id}) # may raise check_result(result, description) except: errors.append(f"Runtime error during evaluation of {description}:\n{indent(traceback.format_exc(), 6)}") @@ -310,10 +278,10 @@ class Conformance: if code != None: description = f"Global constraint \"{tm_name}\"" try: - result = self.evaluate_constraint(code, model=self.model) + result = exec_then_eval(code, _globals=bind_api_readonly(self.odapi)) # may raise + check_result(result, description) except: errors.append(f"Runtime error during evaluation of {description}:\n{indent(traceback.format_exc(), 6)}") - check_result(result, description) return errors def precompute_structures(self): diff --git a/transformation/matcher/mvs_adapter.py b/transformation/matcher/mvs_adapter.py index a224c8b..0931c9a 100644 --- a/transformation/matcher/mvs_adapter.py +++ b/transformation/matcher/mvs_adapter.py @@ -1,9 +1,11 @@ from api.cd import CDAPI +from api.od import ODAPI, bind_api_readonly +from util.eval import exec_then_eval from state.base import State from uuid import UUID from services.bottom.V0 import Bottom from services.scd import SCD -from services.od import OD +from services import od as services_od from transformation.matcher.matcher import Graph, Edge, Vertex, MatcherVF2 from transformation import ramify import itertools @@ -76,7 +78,7 @@ 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- # ModelRefs are flattened def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""): # with Timer("model_to_graph"): - od = OD(model, metamodel, state) + od = services_od.OD(model, metamodel, state) scd = SCD(model, state) scd_mm = SCD(metamodel, state) @@ -208,6 +210,7 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}): # compute subtype relations and such: cdapi = CDAPI(state, host_mm) + odapi = ODAPI(state, host_m, 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: @@ -251,16 +254,26 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}): if hasattr(g_vtx, 'modelref'): if not hasattr(h_vtx, 'modelref'): return False - g_ref_m, g_ref_mm = g_vtx.modelref - h_ref_m, h_ref_mm = h_vtx.modelref - nested_matches = [m for m in match_od(state, h_ref_m, h_ref_mm, g_ref_m, g_ref_mm)] + + 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)] + + + # 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.") + # 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 @@ -280,23 +293,15 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}): if h_vtx.value == IS_MODELREF: return False - # # print(g_vtx.value, h_vtx.value) - # def get_slot(h_vtx, slot_name: str): - # slot_node = self.host_od.get_slot(h_vtx.node_id, slot_name) - # return slot_node - - # def read_int(slot: UUID): - # i = Integer(slot, self.bottom.state) - # return i.read() - - try: - return eval(g_vtx.value, {}, { - 'v': h_vtx.value, - # 'get_slot': functools.partial(get_slot, h_vtx), - # 'read_int': read_int, - }) - except Exception as e: - 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 # Convert to format understood by matching algorithm h_names, host = model_to_graph(state, host_m, host_mm) @@ -309,7 +314,7 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}): if guest_name in g_names } - matcher = MatcherVF2(host, guest, RAMCompare(Bottom(state), OD(host_mm, host_m, state))) + matcher = MatcherVF2(host, guest, RAMCompare(Bottom(state), services_od.OD(host_mm, host_m, state))) for m in matcher.match(graph_pivot): # print("\nMATCH:\n", m) # Convert mapping diff --git a/transformation/rewriter.py b/transformation/rewriter.py index 9587d43..43c0be2 100644 --- a/transformation/rewriter.py +++ b/transformation/rewriter.py @@ -4,12 +4,14 @@ # - ? that's it? from uuid import UUID +from api.od import ODAPI, bind_api from services.bottom.V0 import Bottom from transformation import ramify from services import od from services.primitives.string_type import String from services.primitives.actioncode_type import ActionCode from services.primitives.integer_type import Integer +from util.eval import exec_then_eval def process_rule(state, lhs: UUID, rhs: UUID): bottom = Bottom(state) @@ -41,6 +43,8 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic to_delete, to_create, common = process_rule(state, lhs_m, rhs_m) + odapi = ODAPI(state, host_m, 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 @@ -95,7 +99,8 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic 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() - result = eval(python_expr, {}, {}) + + result = exec_then_eval(python_expr, _globals=bind_api(odapi)) # 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): @@ -152,8 +157,10 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic 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}) + python_expr, _ = od.read_primitive_value(bottom, rhs_el, pattern_mm) + result = exec_then_eval(python_expr, + _globals=bind_api(odapi), + _locals={'this': host_el}) # print('eval result=', result) if isinstance(result, int): # overwrite the old value, in-place diff --git a/util/eval.py b/util/eval.py index d368b90..e022929 100644 --- a/util/eval.py +++ b/util/eval.py @@ -1,9 +1,13 @@ # 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): +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, '', mode='exec'), _globals, _locals) - return eval(compile(last, '', mode='eval'), _globals, _locals) + extended_globals = { + '__builtins__': {'isinstance': isinstance, 'print': print, 'int': int, 'float': float, 'bool': bool, 'str': str, 'tuple': tuple, 'len': len, 'set': set, 'dict': dict }, + **_globals, + } + exec(compile(block, '', mode='exec'), extended_globals, _locals) + return eval(compile(last, '', mode='eval'), extended_globals, _locals)