rewriter.py 13 KB

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