Browse Source

Make OD-API for consistent for constraints, LHS patterns, RHS actions.

Joeri Exelmans 9 months ago
parent
commit
9c68b288c1

+ 39 - 0
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

+ 5 - 3
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:

+ 2 - 1
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'

+ 6 - 3
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()

+ 5 - 37
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):

+ 34 - 29
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

+ 10 - 3
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

+ 7 - 3
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, '<string>', mode='exec'), _globals, _locals)
-    return eval(compile(last, '<string>', 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, '<string>', mode='exec'), extended_globals, _locals)
+    return eval(compile(last, '<string>', mode='eval'), extended_globals, _locals)