matcher.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. from api.cd import CDAPI
  2. from api.od import ODAPI, bind_api_readonly
  3. from util.eval import exec_then_eval
  4. from state.base import State
  5. from uuid import UUID
  6. from services.bottom.V0 import Bottom
  7. from services.scd import SCD
  8. from services import od as services_od
  9. from transformation.vf2 import Graph, Edge, Vertex, MatcherVF2
  10. from transformation import ramify
  11. import itertools
  12. import re
  13. import functools
  14. from util.timer import Timer
  15. class _is_edge:
  16. def __repr__(self):
  17. return "EDGE"
  18. def to_json(self):
  19. return "EDGE"
  20. # just a unique symbol that is only equal to itself
  21. IS_EDGE = _is_edge()
  22. class _is_modelref:
  23. def __repr__(self):
  24. return "REF"
  25. def to_json(self):
  26. return "REF"
  27. IS_MODELREF = _is_modelref()
  28. # class IS_TYPE:
  29. # def __init__(self, type):
  30. # # mvs-node of the type
  31. # self.type = type
  32. # def __repr__(self):
  33. # return f"TYPE({str(self.type)[-4:]})"
  34. class NamedNode(Vertex):
  35. def __init__(self, value, name):
  36. super().__init__(value)
  37. # the name of the node in the context of the model
  38. # the matcher by default ignores this value
  39. self.name = name
  40. # MVS-nodes become vertices
  41. class MVSNode(NamedNode):
  42. def __init__(self, value, node_id, name):
  43. super().__init__(value, name)
  44. # useful for debugging
  45. self.node_id = node_id
  46. def __repr__(self):
  47. if self.value == None:
  48. return f"N({self.name})"
  49. if isinstance(self.value, str):
  50. return f"N({self.name}=\"{self.value}\")"
  51. return f"N({self.name}={self.value})"
  52. # if isinstance(self.value, str):
  53. # return f"N({self.name}=\"{self.value}\",{str(self.node_id)[-4:]})"
  54. # return f"N({self.name}={self.value},{str(self.node_id)[-4:]})"
  55. # MVS-edges become vertices.
  56. class MVSEdge(NamedNode):
  57. def __init__(self, node_id, name):
  58. super().__init__(IS_EDGE, name)
  59. # useful for debugging
  60. self.node_id = node_id
  61. def __repr__(self):
  62. return f"E({self.name})"
  63. # return f"E({self.name}{str(self.node_id)[-4:]})"
  64. # dirty way of detecting whether a node is a ModelRef
  65. 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-z][0-9a-z]-[0-9a-z][0-9a-z][0-9a-z][0-9a-z]-[0-9a-z][0-9a-z][0-9a-z][0-9a-z]-[0-9a-z][0-9a-z][0-9a-z][0-9a-z]-[0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z]")
  66. # Converts an object diagram in MVS state to the pattern matcher graph type
  67. # ModelRefs are flattened
  68. def model_to_graph(state: State, model: UUID, metamodel: UUID,
  69. _filter=lambda node: True, prefix=""):
  70. # with Timer("model_to_graph"):
  71. od = services_od.OD(model, metamodel, state)
  72. scd = SCD(model, state)
  73. scd_mm = SCD(metamodel, state)
  74. bottom = Bottom(state)
  75. graph = Graph()
  76. mvs_edges = []
  77. modelrefs = {}
  78. # constraints = {}
  79. names = {}
  80. def to_vtx(el, name):
  81. # print("name:", name)
  82. if bottom.is_edge(el):
  83. # if filter_constraint:
  84. # try:
  85. # supposed_obj = bottom.read_edge_source(el)
  86. # slot_node = od.get_slot(supposed_obj, "constraint")
  87. # if el == slot_node:
  88. # # `el` is the constraint-slot
  89. # constraints[supposed_obj] = el
  90. # return
  91. # except:
  92. # pass
  93. mvs_edges.append(el)
  94. edge = MVSEdge(el, name)
  95. names[name] = edge
  96. return edge
  97. # If the value of the el is a ModelRef (only way to detect this is to match a regex - not very clean), then extract it. We'll create a link to the referred model later.
  98. value = bottom.read_value(el)
  99. if isinstance(value, str):
  100. if UUID_REGEX.match(value) != None:
  101. # side-effect
  102. modelrefs[el] = (UUID(value), name)
  103. return MVSNode(IS_MODELREF, el, name)
  104. node = MVSNode(value, el, name)
  105. names[name] = node
  106. return node
  107. # Objects and Links become vertices
  108. 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) }
  109. graph.vtxs = [ vtx for vtx in uuid_to_vtx.values() ]
  110. # For every Link, two edges are created (for src and tgt)
  111. for mvs_edge in mvs_edges:
  112. mvs_src = bottom.read_edge_source(mvs_edge)
  113. if mvs_src in uuid_to_vtx:
  114. graph.edges.append(Edge(
  115. src=uuid_to_vtx[mvs_src],
  116. tgt=uuid_to_vtx[mvs_edge],
  117. label="outgoing"))
  118. mvs_tgt = bottom.read_edge_target(mvs_edge)
  119. if mvs_tgt in uuid_to_vtx:
  120. graph.edges.append(Edge(
  121. src=uuid_to_vtx[mvs_edge],
  122. tgt=uuid_to_vtx[mvs_tgt],
  123. label="tgt"))
  124. for node, (ref_m, name) in modelrefs.items():
  125. vtx = uuid_to_vtx[node]
  126. # Get MM of ref'ed model
  127. ref_mm, = bottom.read_outgoing_elements(node, "Morphism")
  128. # print("modelref type node:", type_node)
  129. # Recursively convert ref'ed model to graph
  130. # ref_graph = model_to_graph(state, ref_m, ref_mm, prefix=name+'/')
  131. vtx.modelref = (ref_m, ref_mm)
  132. # We no longer flatten:
  133. # # Flatten and create link to ref'ed model
  134. # graph.vtxs += ref_model.vtxs
  135. # graph.edges += ref_model.edges
  136. # graph.edges.append(Edge(
  137. # src=uuid_to_vtx[node],
  138. # tgt=ref_model.vtxs[0], # which node to link to?? dirty
  139. # label="modelref"))
  140. def add_types(node):
  141. vtx = uuid_to_vtx[node]
  142. type_node, = bottom.read_outgoing_elements(node, "Morphism")
  143. # Put the type straight into the Vertex-object
  144. # The benefit is that our Vertex-matching callback can then be coded cleverly, look at the types first, resulting in better performance
  145. vtx.typ = type_node
  146. # The old approach (creating special vertices containing the types), commented out:
  147. # print('node', node, 'has type', type_node)
  148. # We create a Vertex storing the type
  149. # type_vertex = Vertex(value=IS_TYPE(type_node))
  150. # graph.vtxs.append(type_vertex)
  151. # type_edge = Edge(
  152. # src=uuid_to_vtx[node],
  153. # tgt=type_vertex,
  154. # label="type")
  155. # # print(type_edge)
  156. # graph.edges.append(type_edge)
  157. # Add typing information for:
  158. # - classes
  159. # - attributes
  160. # - associations
  161. for class_name, class_node in scd_mm.get_classes().items():
  162. objects = scd.get_typed_by(class_node)
  163. # print("typed by:", class_name, objects)
  164. for obj_name, obj_node in objects.items():
  165. if _filter(obj_node):
  166. add_types(obj_node)
  167. for attr_name, attr_node in scd_mm.get_attributes(class_name).items():
  168. attrs = scd.get_typed_by(attr_node)
  169. for slot_name, slot_node in attrs.items():
  170. if _filter(slot_node):
  171. add_types(slot_node)
  172. for assoc_name, assoc_node in scd_mm.get_associations().items():
  173. objects = scd.get_typed_by(assoc_node)
  174. # print("typed by:", assoc_name, objects)
  175. for link_name, link_node in objects.items():
  176. if _filter(link_node):
  177. add_types(link_node)
  178. return names, graph
  179. class _No_Matched(Exception):
  180. pass
  181. def _cannot_call_matched(_):
  182. raise _No_Matched()
  183. # This function returns a Generator of matches.
  184. # The idea is that the user can iterate over the match set, lazily generating it: if only interested in the first match, the entire match set doesn't have to be generated.
  185. def match_od(state, host_m, host_mm, pattern_m, pattern_mm, pivot={}):
  186. bottom = Bottom(state)
  187. # compute subtype relations and such:
  188. cdapi = CDAPI(state, host_mm)
  189. odapi = ODAPI(state, host_m, host_mm)
  190. pattern_odapi = ODAPI(state, pattern_m, pattern_mm)
  191. pattern_mm_odapi = ODAPI(state, pattern_mm, cdapi.mm)
  192. # 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)
  193. class RAMCompare:
  194. def __init__(self, bottom, host_od):
  195. self.bottom = bottom
  196. self.host_od = host_od
  197. type_model_id = bottom.state.read_dict(bottom.state.read_root(), "SCD")
  198. self.scd_model = UUID(bottom.state.read_value(type_model_id))
  199. # 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
  200. self.conditions_to_check = {}
  201. def match_types(self, g_vtx_type, h_vtx_type):
  202. # types only match with their supertypes
  203. # we assume that 'RAMifies'-traceability links have been created between guest and host types
  204. try:
  205. g_vtx_unramified_type = ramify.get_original_type(self.bottom, g_vtx_type)
  206. except:
  207. return False
  208. try:
  209. host_type_name = cdapi.type_model_names[h_vtx_type]
  210. guest_type_name_unramified = cdapi.type_model_names[g_vtx_unramified_type]
  211. except KeyError:
  212. return False
  213. return cdapi.is_subtype(
  214. super_type_name=guest_type_name_unramified,
  215. sub_type_name=host_type_name)
  216. # Memoizing the result of comparison gives a huge performance boost!
  217. # Especially `is_subtype_of` is very slow, and will be performed many times over on the same pair of nodes during the matching process.
  218. # Assuming the model is not altered *during* matching, this is safe.
  219. @functools.cache
  220. def __call__(self, g_vtx, h_vtx):
  221. # First check if the types match (if we have type-information)
  222. if hasattr(g_vtx, 'typ'):
  223. if not hasattr(h_vtx, 'typ'):
  224. # if guest has a type, host must have a type
  225. return False
  226. return self.match_types(g_vtx.typ, h_vtx.typ)
  227. if hasattr(g_vtx, 'modelref'):
  228. if not hasattr(h_vtx, 'modelref'):
  229. return False
  230. python_code = services_od.read_primitive_value(self.bottom, g_vtx.node_id, pattern_mm)[0]
  231. try:
  232. # Try to execute code, but if the `matched` API-function is called, we fail.
  233. with Timer(f'EVAL condition {g_vtx.name}'):
  234. ok = exec_then_eval(python_code,
  235. _globals={
  236. **bind_api_readonly(odapi),
  237. 'matched': _cannot_call_matched,
  238. },
  239. _locals={'this': h_vtx.node_id})
  240. self.conditions_to_check.pop(g_vtx.name, None)
  241. return ok
  242. except _No_Matched:
  243. # The code made a call to the `matched`-function.
  244. self.conditions_to_check[g_vtx.name] = python_code
  245. return True # to be determined later, if it's actually a match
  246. if g_vtx.value == None:
  247. return h_vtx.value == None
  248. # mvs-edges (which are converted to vertices) only match with mvs-edges
  249. if g_vtx.value == IS_EDGE:
  250. return h_vtx.value == IS_EDGE
  251. if h_vtx.value == IS_EDGE:
  252. return False
  253. if g_vtx.value == IS_MODELREF:
  254. return h_vtx.value == IS_MODELREF
  255. if h_vtx.value == IS_MODELREF:
  256. return False
  257. return True
  258. # Convert to format understood by matching algorithm
  259. h_names, host = model_to_graph(state, host_m, host_mm)
  260. # Only match matchable pattern elements
  261. # E.g., the 'condition'-attribute that is added to every class, cannot be matched with anything
  262. def is_matchable(pattern_el):
  263. pattern_el_name = pattern_odapi.get_name(pattern_el)
  264. if pattern_odapi.get_type_name(pattern_el) == "GlobalCondition":
  265. return False
  266. # Super-cheap and unreliable way of filtering out the 'condition'-attribute, added to every class:
  267. return not (pattern_el_name.endswith("condition")
  268. # as an extra safety measure, if the user defined her own 'condition' attribute, RAMification turned this into 'RAM_condition', and we can detect this
  269. # of course this breaks if the class name already ended with 'RAM', but let's hope that never happens
  270. # also, we are assuming the default "RAM_" prefix is used, but the user can change this...
  271. and not pattern_el_name.endswith("RAM_condition"))
  272. g_names, guest = model_to_graph(state, pattern_m, pattern_mm,
  273. _filter=is_matchable)
  274. graph_pivot = {
  275. g_names[guest_name] : h_names[host_name]
  276. for guest_name, host_name in pivot.items()
  277. if guest_name in g_names
  278. }
  279. obj_conditions = []
  280. for class_name, class_node in pattern_mm_odapi.get_all_instances("Class"):
  281. for obj_name, obj_node in pattern_odapi.get_all_instances(class_name):
  282. python_code = pattern_odapi.get_slot_value_default(obj_node, "condition", 'True')
  283. if class_name == "GlobalCondition":
  284. obj_conditions.append((python_code, None))
  285. else:
  286. obj_conditions.append((python_code, obj_name))
  287. def check_conditions(name_mapping):
  288. def check(python_code: str, loc):
  289. return exec_then_eval(python_code,
  290. _globals={
  291. **bind_api_readonly(odapi),
  292. 'matched': lambda name: bottom.read_outgoing_elements(host_m, name_mapping[name])[0],
  293. },
  294. _locals=loc)
  295. # Attribute conditions
  296. for pattern_name, host_name in name_mapping.items():
  297. try:
  298. python_code = compare.conditions_to_check[pattern_name]
  299. except KeyError:
  300. continue
  301. host_node = odapi.get(host_name)
  302. with Timer(f'EVAL condition {pattern_name}'):
  303. if not check(python_code, {'this': host_node}):
  304. return False
  305. for python_code, pattern_el_name in obj_conditions:
  306. if pattern_el_name == None:
  307. # GlobalCondition
  308. with Timer(f'EVAL all global conditions'):
  309. if not check(python_code, {}):
  310. return False
  311. else:
  312. # object-lvl condition
  313. host_el_name = name_mapping[pattern_el_name]
  314. host_node = odapi.get(host_el_name)
  315. with Timer(f'EVAL local condition {pattern_el_name}'):
  316. if not check(python_code, {'this': host_node}):
  317. return False
  318. return True
  319. compare = RAMCompare(bottom, services_od.OD(host_mm, host_m, state))
  320. matcher = MatcherVF2(host, guest, compare)
  321. for m in matcher.match(graph_pivot):
  322. # Convert mapping
  323. name_mapping = {}
  324. for guest_vtx, host_vtx in m.mapping_vtxs.items():
  325. if isinstance(guest_vtx, NamedNode) and isinstance(host_vtx, NamedNode):
  326. name_mapping[guest_vtx.name] = host_vtx.name
  327. if not check_conditions(name_mapping):
  328. continue # not a match after all...
  329. yield name_mapping