matcher.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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, counted
  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. names = {}
  79. def to_vtx(el, name):
  80. # print("name:", name)
  81. if bottom.is_edge(el):
  82. mvs_edges.append(el)
  83. edge = MVSEdge(el, name)
  84. names[name] = edge
  85. return edge
  86. # 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.
  87. value = bottom.read_value(el)
  88. if isinstance(value, str):
  89. if UUID_REGEX.match(value) != None:
  90. # side-effect
  91. modelrefs[el] = (UUID(value), name)
  92. return MVSNode(IS_MODELREF, el, name)
  93. node = MVSNode(value, el, name)
  94. names[name] = node
  95. return node
  96. # Objects and Links become vertices
  97. 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) }
  98. graph.vtxs = [ vtx for vtx in uuid_to_vtx.values() ]
  99. # For every Link, two edges are created (for src and tgt)
  100. for mvs_edge in mvs_edges:
  101. mvs_src = bottom.read_edge_source(mvs_edge)
  102. if mvs_src in uuid_to_vtx:
  103. graph.edges.append(Edge(
  104. src=uuid_to_vtx[mvs_src],
  105. tgt=uuid_to_vtx[mvs_edge],
  106. label="outgoing"))
  107. mvs_tgt = bottom.read_edge_target(mvs_edge)
  108. if mvs_tgt in uuid_to_vtx:
  109. graph.edges.append(Edge(
  110. src=uuid_to_vtx[mvs_edge],
  111. tgt=uuid_to_vtx[mvs_tgt],
  112. label="tgt"))
  113. for node, (ref_m, name) in modelrefs.items():
  114. vtx = uuid_to_vtx[node]
  115. # Get MM of ref'ed model
  116. ref_mm, = bottom.read_outgoing_elements(node, "Morphism")
  117. vtx.modelref = (ref_m, ref_mm)
  118. def add_types(node):
  119. vtx = uuid_to_vtx[node]
  120. type_node, = bottom.read_outgoing_elements(node, "Morphism")
  121. # Put the type straight into the Vertex-object
  122. # The benefit is that our Vertex-matching callback can then be coded cleverly, look at the types first, resulting in better performance
  123. vtx.typ = type_node
  124. # Add typing information for:
  125. # - classes
  126. # - attributes
  127. # - associations
  128. for class_name, class_node in scd_mm.get_classes().items():
  129. objects = scd.get_typed_by(class_node)
  130. for obj_name, obj_node in objects.items():
  131. if _filter(obj_node):
  132. add_types(obj_node)
  133. for attr_name, attr_node in scd_mm.get_attributes(class_name).items():
  134. attrs = scd.get_typed_by(attr_node)
  135. for slot_name, slot_node in attrs.items():
  136. if _filter(slot_node):
  137. add_types(slot_node)
  138. for assoc_name, assoc_node in scd_mm.get_associations().items():
  139. objects = scd.get_typed_by(assoc_node)
  140. for link_name, link_node in objects.items():
  141. if _filter(link_node):
  142. add_types(link_node)
  143. return names, graph
  144. class _No_Matched(Exception):
  145. pass
  146. def _cannot_call_matched(_):
  147. raise _No_Matched()
  148. # This function returns a Generator of matches.
  149. # 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.
  150. def match_od(state,
  151. host_m, # the host graph, in which to search for matches
  152. host_mm, # meta-model of the host graph
  153. pattern_m, # the pattern to look for
  154. pattern_mm, # the meta-model of the pattern (typically the RAMified version of host_mm)
  155. pivot={}, # optional: a partial match (restricts possible matches, and speeds up the match process)
  156. eval_context={}, # optional: additional variables, functions, ... to be available while evaluating condition-code in the pattern. Will be available as global variables in the condition-code.
  157. ):
  158. bottom = Bottom(state)
  159. # compute subtype relations and such:
  160. cdapi = CDAPI(state, host_mm)
  161. odapi = ODAPI(state, host_m, host_mm)
  162. pattern_odapi = ODAPI(state, pattern_m, pattern_mm)
  163. pattern_mm_odapi = ODAPI(state, pattern_mm, cdapi.mm)
  164. # 'globals'-dict used when eval'ing conditions
  165. bound_api = bind_api_readonly(odapi)
  166. builtin = {
  167. **bound_api,
  168. 'matched': _cannot_call_matched,
  169. 'odapi': odapi,
  170. }
  171. for key in eval_context:
  172. if key in builtin:
  173. print(f"WARNING: custom global '{key}' overrides pre-defined API function. Consider renaming it.")
  174. eval_globals = {
  175. **builtin,
  176. **eval_context,
  177. }
  178. # 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)
  179. class RAMCompare:
  180. def __init__(self, bottom, host_od):
  181. self.bottom = bottom
  182. self.host_od = host_od
  183. type_model_id = bottom.state.read_dict(bottom.state.read_root(), "SCD")
  184. self.scd_model = UUID(bottom.state.read_value(type_model_id))
  185. # 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
  186. self.conditions_to_check = {}
  187. def match_types(self, g_vtx_type, h_vtx_type):
  188. # types only match with their supertypes
  189. # we assume that 'RAMifies'-traceability links have been created between guest and host types
  190. try:
  191. g_vtx_unramified_type = ramify.get_original_type(self.bottom, g_vtx_type)
  192. except:
  193. return False
  194. try:
  195. host_type_name = cdapi.type_model_names[h_vtx_type]
  196. guest_type_name_unramified = cdapi.type_model_names[g_vtx_unramified_type]
  197. except KeyError:
  198. return False
  199. types_ok = cdapi.is_subtype(
  200. super_type_name=guest_type_name_unramified,
  201. sub_type_name=host_type_name)
  202. return types_ok
  203. # Memoizing the result of comparison gives a huge performance boost!
  204. # Especially `is_subtype_of` is very slow, and will be performed many times over on the same pair of nodes during the matching process.
  205. # Assuming the model is not altered *during* matching, this is safe.
  206. @functools.cache
  207. def __call__(self, g_vtx, h_vtx):
  208. # First check if the types match (if we have type-information)
  209. if hasattr(g_vtx, 'typ'):
  210. if not hasattr(h_vtx, 'typ'):
  211. # if guest has a type, host must have a type
  212. return False
  213. if not self.match_types(g_vtx.typ, h_vtx.typ):
  214. return False
  215. if hasattr(g_vtx, 'modelref'):
  216. if not hasattr(h_vtx, 'modelref'):
  217. return False
  218. python_code = services_od.read_primitive_value(self.bottom, g_vtx.node_id, pattern_mm)[0]
  219. try:
  220. # Try to execute code, but the likelyhood of failing is high:
  221. # - the `matched` API function is not yet available
  222. # - incompatible slots may be matched (it is only when their AttributeLinks are matched, that we know the types will be compatible)
  223. with Timer(f'EVAL condition {g_vtx.name}'):
  224. ok = exec_then_eval(python_code,
  225. _globals=eval_globals,
  226. _locals={'this': h_vtx.node_id})
  227. self.conditions_to_check.pop(g_vtx.name, None)
  228. return ok
  229. except:
  230. self.conditions_to_check[g_vtx.name] = python_code
  231. return True # to be determined later, if it's actually a match
  232. if g_vtx.value == None:
  233. return h_vtx.value == None
  234. # mvs-edges (which are converted to vertices) only match with mvs-edges
  235. if g_vtx.value == IS_EDGE:
  236. return h_vtx.value == IS_EDGE
  237. if h_vtx.value == IS_EDGE:
  238. return False
  239. if g_vtx.value == IS_MODELREF:
  240. return h_vtx.value == IS_MODELREF
  241. if h_vtx.value == IS_MODELREF:
  242. return False
  243. return True
  244. # Convert to format understood by matching algorithm
  245. h_names, host = model_to_graph(state, host_m, host_mm)
  246. # Only match matchable pattern elements
  247. # E.g., the 'condition'-attribute that is added to every class, cannot be matched with anything
  248. def is_matchable(pattern_el):
  249. pattern_el_name = pattern_odapi.get_name(pattern_el)
  250. if pattern_odapi.get_type_name(pattern_el) == "GlobalCondition":
  251. return False
  252. # Super-cheap and unreliable way of filtering out the 'condition'-attribute, added to every class:
  253. return ((not pattern_el_name.endswith("condition")
  254. # as an extra safety measure, if the user defined her own 'condition' attribute, RAMification turned this into 'RAM_condition', and we can detect this
  255. # of course this breaks if the class name already ended with 'RAM', but let's hope that never happens
  256. # also, we are assuming the default "RAM_" prefix is used, but the user can change this...
  257. or pattern_el_name.endswith("RAM_condition"))
  258. and (
  259. not pattern_el_name.endswith("name")
  260. or pattern_el_name.endswith("RAM_name") # same thing here as with the condition, explained above.
  261. ))
  262. g_names, guest = model_to_graph(state, pattern_m, pattern_mm,
  263. _filter=is_matchable)
  264. # precompute the candidates for every guest vertex:
  265. guest_to_host_candidate_vtxs = {}
  266. vtxs_of_host_type = {}
  267. for g_vtx in guest.vtxs:
  268. object_node = g_vtx.node_id
  269. if hasattr(g_vtx, 'typ'):
  270. orig_class_node = ramify.get_original_type(bottom, g_vtx.typ)
  271. orig_class_name = odapi.get_name(orig_class_node)
  272. if orig_class_name in vtxs_of_host_type:
  273. cands = vtxs_of_host_type[orig_class_name]
  274. else:
  275. cands = vtxs_of_host_type[orig_class_name] = len(odapi.get_all_instances(orig_class_name, include_subtypes=True))
  276. else:
  277. cands = len(host.vtxs)
  278. guest_to_host_candidate_vtxs[g_vtx] = cands
  279. # print(guest_to_host_candidate_vtxs)
  280. # transform 'pivot' into something VF2 understands
  281. graph_pivot = {
  282. g_names[guest_name] : h_names[host_name]
  283. for guest_name, host_name in pivot.items()
  284. if guest_name in g_names
  285. }
  286. obj_conditions = []
  287. for class_name, class_node in pattern_mm_odapi.get_all_instances("Class"):
  288. for obj_name, obj_node in pattern_odapi.get_all_instances(class_name):
  289. python_code = pattern_odapi.get_slot_value_default(obj_node, "condition", 'True')
  290. if class_name == "GlobalCondition":
  291. obj_conditions.append((python_code, None))
  292. else:
  293. obj_conditions.append((python_code, obj_name))
  294. def check_conditions(name_mapping):
  295. eval_globals = {
  296. **bound_api,
  297. # this time, the real 'matched'-function can be used:
  298. 'matched': lambda name: bottom.read_outgoing_elements(host_m, name_mapping[name])[0],
  299. **eval_context,
  300. }
  301. def check(python_code: str, loc):
  302. return exec_then_eval(python_code, _globals=eval_globals, _locals=loc)
  303. # Attribute conditions
  304. for pattern_name, host_name in name_mapping.items():
  305. try:
  306. python_code = compare.conditions_to_check[pattern_name]
  307. except KeyError:
  308. continue
  309. host_node = odapi.get(host_name)
  310. with Timer(f'EVAL condition {pattern_name}'):
  311. if not check(python_code, {'this': host_node}):
  312. return False
  313. for python_code, pattern_el_name in obj_conditions:
  314. if pattern_el_name == None:
  315. # GlobalCondition
  316. with Timer(f'EVAL all global conditions'):
  317. if not check(python_code, {}):
  318. return False
  319. else:
  320. # object-lvl condition
  321. host_el_name = name_mapping[pattern_el_name]
  322. host_node = odapi.get(host_el_name)
  323. with Timer(f'EVAL local condition {pattern_el_name}'):
  324. if not check(python_code, {'this': host_node}):
  325. return False
  326. return True
  327. compare = RAMCompare(bottom, services_od.OD(host_mm, host_m, state))
  328. matcher = MatcherVF2(host, guest, compare, guest_to_host_candidate_vtxs)
  329. for m in matcher.match(graph_pivot):
  330. # Convert mapping
  331. name_mapping = {}
  332. for guest_vtx, host_vtx in m.mapping_vtxs.items():
  333. if isinstance(guest_vtx, NamedNode) and isinstance(host_vtx, NamedNode):
  334. name_mapping[guest_vtx.name] = host_vtx.name
  335. if not check_conditions(name_mapping):
  336. continue # not a match after all...
  337. yield name_mapping