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

This commit is contained in:
Joeri Exelmans 2024-11-07 11:05:06 +01:00
parent 1eb8a84553
commit 9c68b288c1
8 changed files with 108 additions and 79 deletions

View file

@ -211,3 +211,42 @@ class ODAPI:
def create_object(self, object_name: Optional[str], class_name: str): def create_object(self, object_name: Optional[str], class_name: str):
return self.od.create_object(object_name, class_name) 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

View file

@ -4,14 +4,16 @@ def indent(multiline_string, how_much):
lines = multiline_string.split('\n') lines = multiline_string.split('\n')
return '\n'.join([' '*how_much+l for l in lines]) 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 type_name == "ActionCode":
if '\n' in val: 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: else:
return '`'+val+'`' return '`'+val+'`'
elif type_name == "String": elif type_name == "String":
return '"'+val+'"' return '"'+val+'"'.replace('\n', newline_character)
elif type_name == "Integer" or type_name == "Boolean": elif type_name == "Integer" or type_name == "Boolean":
return str(val) return str(val)
else: else:

View file

@ -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) slot = m_od.get_slot(obj_node, attr_name)
if slot != None: if slot != None:
val, type_name = od.read_primitive_value(bottom, slot, mm) 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}'
output += '\n' output += '\n'

View file

@ -13,6 +13,7 @@ from transformation import rewriter
from services.bottom.V0 import Bottom from services.bottom.V0 import Bottom
from services.primitives.integer_type import Integer from services.primitives.integer_type import Integer
from concrete_syntax.plantuml import renderer as plantuml 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 from concrete_syntax.textual_od import parser, renderer
def main(): def main():
@ -112,7 +113,9 @@ def main():
# object to match # object to match
man:{prefix}Man {{ man:{prefix}Man {{
# match only men heavy enough # match only men heavy enough
{prefix}weight = `v > 60`; {prefix}weight = ```
get_value(this) > 60
```;
}} }}
# object to delete # object to delete
@ -134,7 +137,7 @@ def main():
# matched object # matched object
man:{prefix}Man {{ man:{prefix}Man {{
# man gains weight # man gains weight
{prefix}weight = `v + 5`; {prefix}weight = `get_value(this) + 5`;
}} }}
# object to create # object to create
@ -216,7 +219,7 @@ def main():
# Render conformance # Render conformance
uml += plantuml.render_trace_conformance(state, snapshot_dsl_m_id, dsl_mm_id) 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_all_matches()
plantuml_str = render_rewrite() plantuml_str = render_rewrite()

View file

@ -10,7 +10,7 @@ from concrete_syntax.common import indent
from util.eval import exec_then_eval from util.eval import exec_then_eval
from api.cd import CDAPI from api.cd import CDAPI
from api.od import ODAPI from api.od import ODAPI, bind_api_readonly
import functools import functools
@ -138,7 +138,7 @@ class Conformance:
for ref_inst_name, ref_inst in self.odapi.get_all_instances(ref_name): for ref_inst_name, ref_inst in self.odapi.get_all_instances(ref_name):
sub_m = UUID(self.bottom.read_value(ref_inst)) sub_m = UUID(self.bottom.read_value(ref_inst))
nested_errors = Conformance(self.state, sub_m, sub_mm).check_nominal() 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 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}'.") errors.append(f"Target cardinality of type '{assoc_name}' ({count}) out of bounds ({lc}..{uc}) in '{obj_name}'.")
return errors 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): def check_constraints(self):
""" """
Check whether all constraints defined for a model are respected 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}\"" description = f"Local constraint of \"{type_name}\" in \"{obj_name}\""
# print(description) # print(description)
try: 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) check_result(result, description)
except: except:
errors.append(f"Runtime error during evaluation of {description}:\n{indent(traceback.format_exc(), 6)}") errors.append(f"Runtime error during evaluation of {description}:\n{indent(traceback.format_exc(), 6)}")
@ -310,10 +278,10 @@ class Conformance:
if code != None: if code != None:
description = f"Global constraint \"{tm_name}\"" description = f"Global constraint \"{tm_name}\""
try: 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: except:
errors.append(f"Runtime error during evaluation of {description}:\n{indent(traceback.format_exc(), 6)}") errors.append(f"Runtime error during evaluation of {description}:\n{indent(traceback.format_exc(), 6)}")
check_result(result, description)
return errors return errors
def precompute_structures(self): def precompute_structures(self):

View file

@ -1,9 +1,11 @@
from api.cd import CDAPI 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 state.base import State
from uuid import UUID from uuid import UUID
from services.bottom.V0 import Bottom from services.bottom.V0 import Bottom
from services.scd import SCD 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.matcher.matcher import Graph, Edge, Vertex, MatcherVF2
from transformation import ramify from transformation import ramify
import itertools 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 # ModelRefs are flattened
def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""): def model_to_graph(state: State, model: UUID, metamodel: UUID, prefix=""):
# with Timer("model_to_graph"): # with Timer("model_to_graph"):
od = OD(model, metamodel, state) od = services_od.OD(model, metamodel, state)
scd = SCD(model, state) scd = SCD(model, state)
scd_mm = SCD(metamodel, 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: # compute subtype relations and such:
cdapi = CDAPI(state, host_mm) 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) # 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:
@ -251,16 +254,26 @@ def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}):
if hasattr(g_vtx, 'modelref'): if hasattr(g_vtx, 'modelref'):
if not hasattr(h_vtx, 'modelref'): if not hasattr(h_vtx, 'modelref'):
return False return False
g_ref_m, g_ref_mm = g_vtx.modelref
h_ref_m, h_ref_mm = h_vtx.modelref python_code = services_od.read_primitive_value(self.bottom, g_vtx.node_id, pattern_mm)[0]
nested_matches = [m for m in match_od(state, h_ref_m, h_ref_mm, g_ref_m, g_ref_mm)] 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) # print('nested_matches:', nested_matches)
if len(nested_matches) == 0: # if len(nested_matches) == 0:
return False # return False
elif len(nested_matches) == 1: # elif len(nested_matches) == 1:
return True # return True
else: # else:
raise Exception("We have a problem: there is more than 1 match in the nested models.") # raise Exception("We have a problem: there is more than 1 match in the nested models.")
# print('end recurse')
# Then, match by value # 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: if h_vtx.value == IS_MODELREF:
return False return False
# # print(g_vtx.value, h_vtx.value) # python_code = g_vtx.value
# def get_slot(h_vtx, slot_name: str): # try:
# slot_node = self.host_od.get_slot(h_vtx.node_id, slot_name) # return exec_then_eval(python_code,
# return slot_node # _globals=bind_api_readonly(odapi),
# _locals={'this': h_vtx.node_id})
# def read_int(slot: UUID): # except Exception as e:
# i = Integer(slot, self.bottom.state) # print(e)
# return i.read() # return False
return True
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
# 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)
@ -309,7 +314,7 @@ 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), 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): for m in matcher.match(graph_pivot):
# print("\nMATCH:\n", m) # print("\nMATCH:\n", m)
# Convert mapping # Convert mapping

View file

@ -4,12 +4,14 @@
# - ? that's it? # - ? that's it?
from uuid import UUID from uuid import UUID
from api.od import ODAPI, bind_api
from services.bottom.V0 import Bottom from services.bottom.V0 import Bottom
from transformation import ramify from transformation import ramify
from services import od 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
def process_rule(state, lhs: UUID, rhs: UUID): def process_rule(state, lhs: UUID, rhs: UUID):
bottom = Bottom(state) 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) to_delete, to_create, common = process_rule(state, lhs_m, rhs_m)
odapi = ODAPI(state, host_m, mm)
# Perform deletions # Perform deletions
for pattern_name_to_delete in to_delete: 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 # 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": if type_name == "ActionCode":
# 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 = eval(python_expr, {}, {})
result = exec_then_eval(python_expr, _globals=bind_api(odapi))
# 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):
@ -152,8 +157,10 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
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, mm)
rhs_el, = bottom.read_outgoing_elements(rhs_m, pattern_el_name) rhs_el, = bottom.read_outgoing_elements(rhs_m, pattern_el_name)
expr, _ = od.read_primitive_value(bottom, rhs_el, pattern_mm) python_expr, _ = od.read_primitive_value(bottom, rhs_el, pattern_mm)
result = eval(expr, {}, {'v': old_value}) result = exec_then_eval(python_expr,
_globals=bind_api(odapi),
_locals={'this': host_el})
# print('eval result=', result) # print('eval result=', result)
if isinstance(result, int): if isinstance(result, int):
# overwrite the old value, in-place # overwrite the old value, in-place

View file

@ -1,9 +1,13 @@
# based on https://stackoverflow.com/a/39381428 # based on https://stackoverflow.com/a/39381428
# Parses and executes a block of Python code, and returns the eval result of the last statement # Parses and executes a block of Python code, and returns the eval result of the last statement
import ast import ast
def exec_then_eval(code, _globals, _locals): def exec_then_eval(code, _globals={}, _locals={}):
block = ast.parse(code, mode='exec') block = ast.parse(code, mode='exec')
# 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)
exec(compile(block, '<string>', mode='exec'), _globals, _locals) extended_globals = {
return eval(compile(last, '<string>', mode='eval'), _globals, _locals) '__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)