rewriter.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. # Things you can do:
  2. # - Create/delete objects, associations, attributes
  3. # - Change attribute values
  4. # - ? that's it?
  5. import re
  6. from uuid import UUID
  7. from api.od import ODAPI, bind_api
  8. from services.bottom.V0 import Bottom
  9. from transformation import ramify
  10. from services import od
  11. from services.primitives.string_type import String
  12. from services.primitives.actioncode_type import ActionCode
  13. from services.primitives.integer_type import Integer
  14. from util.eval import exec_then_eval, simply_exec
  15. identifier_regex_pattern = '[_A-Za-z][._A-Za-z0-9]*'
  16. identifier_regex = re.compile(identifier_regex_pattern)
  17. class TryAgainNextRound(Exception):
  18. pass
  19. # Rewrite is performed in-place (modifying `host_m`)
  20. def rewrite(state,
  21. lhs_m: UUID, # LHS-pattern
  22. rhs_m: UUID, # RHS-pattern
  23. pattern_mm: UUID, # meta-model of both patterns (typically the RAMified host_mm)
  24. lhs_match: dict, # a match, morphism, from lhs_m to host_m (mapping pattern name -> host name), typically found by the 'match_od'-function.
  25. host_m: UUID, # host model
  26. host_mm: UUID, # host meta-model
  27. eval_context={}, # optional: additional variables/functions to be available while executing condition-code. These will be seen as global variables.
  28. ):
  29. bottom = Bottom(state)
  30. # Need to come up with a new, unique name when creating new element in host-model:
  31. def first_available_name(suggested_name: str):
  32. if len(bottom.read_outgoing_elements(host_m, suggested_name)) == 0:
  33. return suggested_name # already unique :)
  34. i = 0
  35. while True:
  36. name = suggested_name + str(i)
  37. if len(bottom.read_outgoing_elements(host_m, name)) == 0:
  38. return name # found unique name
  39. i += 1
  40. # function that can be called from within RHS action code
  41. def matched_callback(pattern_name: str):
  42. host_name = lhs_match[pattern_name]
  43. return bottom.read_outgoing_elements(host_m, host_name)[0]
  44. scd_metamodel_id = state.read_dict(state.read_root(), "SCD")
  45. scd_metamodel = UUID(state.read_value(scd_metamodel_id))
  46. class_type = od.get_scd_mm_class_node(bottom)
  47. attr_link_type = od.get_scd_mm_attributelink_node(bottom)
  48. assoc_type = od.get_scd_mm_assoc_node(bottom)
  49. actioncode_type = od.get_scd_mm_actioncode_node(bottom)
  50. modelref_type = od.get_scd_mm_modelref_node(bottom)
  51. # To be replaced by ODAPI (below)
  52. host_od = od.OD(host_mm, host_m, bottom.state)
  53. rhs_od = od.OD(pattern_mm, rhs_m, bottom.state)
  54. host_odapi = ODAPI(state, host_m, host_mm)
  55. host_mm_odapi = ODAPI(state, host_mm, scd_metamodel)
  56. rhs_odapi = ODAPI(state, rhs_m, pattern_mm)
  57. rhs_mm_odapi = ODAPI(state, pattern_mm, scd_metamodel)
  58. lhs_keys = lhs_match.keys()
  59. rhs_keys = set(k for k in bottom.read_keys(rhs_m)
  60. # extremely dirty - should think of a better way
  61. if "GlobalCondition" not in k and not k.endswith("_condition") and not k.endswith(".condition")
  62. and (not k.endswith("_name") or k.endswith("RAM_name")) and (not k.endswith(".name")))
  63. # See which keys were ignored by the rewriter:
  64. # print('filtered out:', set(k for k in bottom.read_keys(rhs_m) if k.endswith(".name") or k.endswith("_name")))
  65. common = lhs_keys & rhs_keys
  66. to_delete = lhs_keys - common
  67. to_create = rhs_keys - common
  68. # print("to delete:", to_delete)
  69. # print("to create:", to_create)
  70. # to be grown
  71. rhs_match = { name : lhs_match[name] for name in common }
  72. bound_api = bind_api(host_odapi)
  73. original_delete = bound_api["delete"]
  74. def wrapped_delete(obj):
  75. not_allowed_to_delete = { host_odapi.get(host_name): pattern_name for pattern_name, host_name in rhs_match.items() }
  76. if obj in not_allowed_to_delete:
  77. pattern_name = not_allowed_to_delete[obj]
  78. raise Exception(f"\n\nYou're trying to delete the element that was matched with the RHS-element '{pattern_name}'. This is not allowed! You're allowed to delete anything BUT NOT elements matched with your RHS-pattern. Instead, simply remove the element '{pattern_name}' from your RHS, if you want to delete it.")
  79. return original_delete(obj)
  80. bound_api["delete"] = wrapped_delete
  81. builtin = {
  82. **bound_api,
  83. 'matched': matched_callback,
  84. 'odapi': host_odapi,
  85. }
  86. for key in eval_context:
  87. if key in builtin:
  88. print(f"WARNING: custom global '{key}' overrides pre-defined API function. Consider renaming it.")
  89. eval_globals = {
  90. **builtin,
  91. **eval_context,
  92. }
  93. # 1. Perform creations - in the right order!
  94. remaining_to_create = list(to_create)
  95. while len(remaining_to_create) > 0:
  96. next_round = []
  97. for rhs_name in remaining_to_create:
  98. # Determine the type of the thing to create
  99. rhs_obj = rhs_odapi.get(rhs_name)
  100. # what to name our new object?
  101. try:
  102. name_expr = rhs_odapi.get_slot_value(rhs_obj, "name")
  103. except:
  104. name_expr = f'"{rhs_name}"' # <- if the 'name' slot doesnt exist, use the pattern element name
  105. suggested_name = exec_then_eval(name_expr, _globals=eval_globals)
  106. if not identifier_regex.match(suggested_name):
  107. raise Exception(f"In the RHS pattern element '{rhs_name}', the following name-expression:\n {name_expr}\nproduced the name:\n '{suggested_name}'\nwhich contains illegal characters.\nNames should match the following regex: {identifier_regex_pattern}")
  108. rhs_type = rhs_odapi.get_type(rhs_obj)
  109. host_type = ramify.get_original_type(bottom, rhs_type)
  110. # for debugging:
  111. if host_type != None:
  112. host_type_name = host_odapi.get_name(host_type)
  113. else:
  114. host_type_name = ""
  115. def get_src_tgt():
  116. src = rhs_odapi.get_source(rhs_obj)
  117. tgt = rhs_odapi.get_target(rhs_obj)
  118. src_name = rhs_odapi.get_name(src)
  119. tgt_name = rhs_odapi.get_name(tgt)
  120. try:
  121. host_src_name = rhs_match[src_name]
  122. host_tgt_name = rhs_match[tgt_name]
  123. except KeyError:
  124. # some creations (e.g., edges) depend on other creations
  125. raise TryAgainNextRound()
  126. host_src = host_odapi.get(host_src_name)
  127. host_tgt = host_odapi.get(host_tgt_name)
  128. return (host_src_name, host_tgt_name, host_src, host_tgt)
  129. try:
  130. if od.is_typed_by(bottom, rhs_type, class_type):
  131. obj_name = first_available_name(suggested_name)
  132. host_od._create_object(obj_name, host_type)
  133. host_odapi._ODAPI__recompute_mappings()
  134. rhs_match[rhs_name] = obj_name
  135. elif od.is_typed_by(bottom, rhs_type, assoc_type):
  136. _, _, host_src, host_tgt = get_src_tgt()
  137. link_name = first_available_name(suggested_name)
  138. host_od._create_link(link_name, host_type, host_src, host_tgt)
  139. host_odapi._ODAPI__recompute_mappings()
  140. rhs_match[rhs_name] = link_name
  141. elif od.is_typed_by(bottom, rhs_type, attr_link_type):
  142. host_src_name, _, host_src, host_tgt = get_src_tgt()
  143. host_attr_link = ramify.get_original_type(bottom, rhs_type)
  144. host_attr_name = host_mm_odapi.get_slot_value(host_attr_link, "name")
  145. link_name = f"{host_src_name}_{host_attr_name}" # must follow naming convention here
  146. host_od._create_link(link_name, host_type, host_src, host_tgt)
  147. host_odapi._ODAPI__recompute_mappings()
  148. rhs_match[rhs_name] = link_name
  149. elif rhs_type == rhs_mm_odapi.get("ActionCode"):
  150. # If we encounter ActionCode in our RHS, we assume that the code computes the value of an attribute...
  151. # This will be the *value* of an attribute. The attribute-link (connecting an object to the attribute) will be created as an edge later.
  152. # Problem: attributes must follow the naming pattern '<obj_name>.<attr_name>'
  153. # So we must know the host-object-name, and the host-attribute-name.
  154. # However, all we have access to here is the name of the attribute in the RHS.
  155. # We cannot even see the link to the RHS-object.
  156. # But, assuming the RHS-attribute is also named '<RAMified_obj_name>.<RAMified_attr_name>', we can:
  157. rhs_src_name, rhs_attr_name = rhs_name.split('.')
  158. try:
  159. host_src_name = rhs_match[rhs_src_name]
  160. except KeyError:
  161. # unmet dependency - object to which attribute belongs not created yet
  162. raise TryAgainNextRound()
  163. rhs_src_type = rhs_odapi.get_type(rhs_odapi.get(rhs_src_name))
  164. rhs_src_type_name = rhs_mm_odapi.get_name(rhs_src_type)
  165. rhs_attr_link_name = f"{rhs_src_type_name}_{rhs_attr_name}"
  166. rhs_attr_link = rhs_mm_odapi.get(rhs_attr_link_name)
  167. host_attr_link = ramify.get_original_type(bottom, rhs_attr_link)
  168. host_attr_name = host_mm_odapi.get_slot_value(host_attr_link, "name")
  169. val_name = f"{host_src_name}.{host_attr_name}"
  170. python_expr = ActionCode(UUID(bottom.read_value(rhs_obj)), bottom.state).read()
  171. result = exec_then_eval(python_expr, _globals=eval_globals)
  172. host_odapi.create_primitive_value(val_name, result, is_code=False)
  173. rhs_match[rhs_name] = val_name
  174. else:
  175. rhs_type_name = rhs_odapi.get_name(rhs_type)
  176. raise Exception(f"Host type {host_type_name} of pattern element '{rhs_name}:{rhs_type_name}' is not a class, association or attribute link. Don't know what to do with it :(")
  177. except TryAgainNextRound:
  178. next_round.append(rhs_name)
  179. if len(next_round) == len(remaining_to_create):
  180. raise Exception("Creation of objects did not make any progress - there must be some kind of cyclic dependency?!")
  181. remaining_to_create = next_round
  182. # 2. Perform updates (only on values)
  183. for common_name in common:
  184. host_obj_name = rhs_match[common_name]
  185. host_obj = host_odapi.get(host_obj_name)
  186. host_type = host_odapi.get_type(host_obj)
  187. if od.is_typed_by(bottom, host_type, class_type):
  188. # nothing to do
  189. pass
  190. elif od.is_typed_by(bottom, host_type, assoc_type):
  191. # nothing to do
  192. pass
  193. elif od.is_typed_by(bottom, host_type, attr_link_type):
  194. # nothing to do
  195. pass
  196. elif od.is_typed_by(bottom, host_type, modelref_type):
  197. rhs_obj = rhs_odapi.get(common_name)
  198. python_expr = ActionCode(UUID(bottom.read_value(rhs_obj)), bottom.state).read()
  199. result = exec_then_eval(python_expr,
  200. _globals=eval_globals,
  201. _locals={'this': host_obj}) # 'this' can be used to read the previous value of the slot
  202. host_odapi.overwrite_primitive_value(host_obj_name, result, is_code=False)
  203. else:
  204. msg = f"Don't know what to do with element '{common_name}' -> '{host_obj_name}:{host_type}')"
  205. # print(msg)
  206. raise Exception(msg)
  207. # 3. Perform deletions
  208. # This way, action code can read from elements that are deleted...
  209. # Even better would be to not modify the model in-place, but use copy-on-write...
  210. for pattern_name_to_delete in to_delete:
  211. # For every name in `to_delete`, look up the name of the matched element in the host graph
  212. model_el_name_to_delete = lhs_match[pattern_name_to_delete]
  213. # print('deleting', model_el_name_to_delete)
  214. # Look up the matched element in the host graph
  215. els_to_delete = bottom.read_outgoing_elements(host_m, model_el_name_to_delete)
  216. if len(els_to_delete) == 0:
  217. # This can happen: if the SRC/TGT of a link was deleted, the link itself is also immediately deleted.
  218. # If we then try to delete the link, it is not found (already gone).
  219. # The most accurate way of handling this, would be to perform deletions in opposite order of creations (see the whole TryNextRound-mechanism above)
  220. # However I *think* it is also OK to simply ignore this case.
  221. pass
  222. elif len(els_to_delete) == 1:
  223. bottom.delete_element(els_to_delete[0])
  224. else:
  225. raise Exception("This should never happen!")
  226. # 4. Object-level actions
  227. # Iterate over the (now complete) mapping RHS -> Host
  228. for rhs_name, host_name in rhs_match.items():
  229. host_obj = host_odapi.get(host_name)
  230. rhs_obj = rhs_odapi.get(rhs_name)
  231. rhs_type = rhs_odapi.get_type(rhs_obj)
  232. rhs_type_of_type = rhs_mm_odapi.get_type(rhs_type)
  233. rhs_type_of_type_name = rhs_mm_odapi.get_name(rhs_type_of_type)
  234. if rhs_mm_odapi.cdapi.is_subtype(super_type_name="Class", sub_type_name=rhs_type_of_type_name):
  235. # rhs_obj is an object or link (because association is subtype of class)
  236. python_code = rhs_odapi.get_slot_value_default(rhs_obj, "condition", default="")
  237. simply_exec(python_code,
  238. _globals=eval_globals,
  239. _locals={'this': host_obj})
  240. # 5. Execute global actions
  241. for cond_name, cond in rhs_odapi.get_all_instances("GlobalCondition"):
  242. python_code = rhs_odapi.get_slot_value(cond, "condition")
  243. simply_exec(python_code, _globals=eval_globals)
  244. return rhs_match