request_handler.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import modelverse_kernel.primitives as primitive_functions
  2. class KnownRequestHandled(Exception):
  3. """An exception that signifies that a known request was handled."""
  4. pass
  5. class RequestHandler(object):
  6. """A type of object that intercepts logic-related Modelverse requests, and
  7. forwards Modelverse state requests."""
  8. def __init__(self):
  9. # generator_stack is a stack of (generator, pending requests, request replies, has-reply)
  10. # tuples.
  11. self.generator_stack = []
  12. # exception_handlers is a stack of
  13. # (generator_stack index, [(exception type, handler function)])
  14. # tuples.
  15. self.exception_handlers = []
  16. self.handlers = {
  17. 'CALL' : self.execute_call,
  18. 'CALL_ARGS' : self.execute_call_args,
  19. 'CALL_KWARGS' : self.execute_call_kwargs,
  20. 'TAIL_CALL' : self.execute_tail_call,
  21. 'TAIL_CALL_ARGS' : self.execute_tail_call_args,
  22. 'TAIL_CALL_KWARGS' : self.execute_tail_call_kwargs,
  23. 'TRY' : self.execute_try,
  24. 'CATCH' : self.execute_catch,
  25. 'END_TRY' : self.execute_end_try
  26. }
  27. def is_active(self):
  28. """Tests if this request handler has a top-of-stack generator."""
  29. return len(self.generator_stack) > 0
  30. def handle_request(self, reply):
  31. """Replies to a request from the top-of-stack generator, and returns a new request."""
  32. if not self.is_active():
  33. raise ValueError('handle_request cannot be called with an empty generator stack.')
  34. # Append the server's replies to the list of replies.
  35. self.extend_replies(reply)
  36. while 1:
  37. # Silence pylint's warning about catching Exception.
  38. # pylint: disable=I0011,W0703
  39. try:
  40. if self.has_pending_requests():
  41. try:
  42. # Try to pop a request for the modelverse state.
  43. result = self.pop_requests()
  44. return result
  45. except KnownRequestHandled:
  46. # Carry on.
  47. pass
  48. # Perform a single generator step.
  49. self.step()
  50. except StopIteration:
  51. # Done, so remove the generator
  52. self.pop_generator()
  53. if self.is_active():
  54. # This generator was called from another generator.
  55. # Append 'None' to the caller's list of replies.
  56. self.append_reply(None)
  57. else:
  58. # Looks like we're done here.
  59. return None
  60. except primitive_functions.PrimitiveFinished as ex:
  61. # Done, so remove the generator
  62. self.pop_generator()
  63. if self.is_active():
  64. # This generator was called from another generator.
  65. # Append the callee's result to the caller's list of replies.
  66. self.append_reply(ex.result)
  67. else:
  68. # Looks like we're done here.
  69. return None
  70. except Exception as ex:
  71. # Maybe get an exception handler to do this.
  72. if not self.handle_exception(ex):
  73. raise
  74. def set_finished_requests_flag(self):
  75. """Sets the finished_requests flag in the top-of-stack tuple."""
  76. current_generator, requests, _, replies, has_reply = self.generator_stack[-1]
  77. self.generator_stack[-1] = (current_generator, requests, True, replies, has_reply)
  78. def has_pending_requests(self):
  79. """Tests if the top-of-stack generator has pending requests."""
  80. _, _, finished_requests, _, _ = self.generator_stack[-1]
  81. return not finished_requests
  82. def push_generator(self, gen):
  83. """Pushes a new generator onto the stack."""
  84. self.generator_stack.append((gen, None, True, [], False))
  85. # print('Pushed generator %s. Generator count: %d' % (gen, len(self.generator_stack)))
  86. def pop_generator(self):
  87. """Removes the top-of-stack generator from the generator stack."""
  88. # Pop the generator itself.
  89. self.generator_stack.pop()
  90. # print('Popped generator %s. Generator count: %d' % (gen, len(self.generator_stack)))
  91. # Pop any exception handlers defined by the generator.
  92. top_of_stack_index = len(self.generator_stack)
  93. while len(self.exception_handlers) > 0:
  94. stack_index, _ = self.exception_handlers[-1]
  95. if stack_index == top_of_stack_index:
  96. # Pop exception handlers until exception_handlers is empty or until
  97. # we find an exception handler that is not associated with the popped
  98. # generator.
  99. self.exception_handlers.pop()
  100. else:
  101. # We're done here.
  102. break
  103. def append_reply(self, new_reply):
  104. """Appends a reply to the top-of-stack generator's list of pending replies."""
  105. current_generator, requests, requests_done, replies, has_reply = self.generator_stack[-1]
  106. replies.append(new_reply)
  107. has_reply = True
  108. self.generator_stack[-1] = (current_generator, requests, requests_done, replies, has_reply)
  109. def extend_replies(self, new_replies):
  110. """Appends a list of replies to the top-of-stack generator's list of pending replies."""
  111. current_generator, requests, requests_done, replies, has_reply = self.generator_stack[-1]
  112. if new_replies is not None:
  113. replies.extend(new_replies)
  114. has_reply = True
  115. self.generator_stack[-1] = (
  116. current_generator, requests, requests_done, replies, has_reply)
  117. def step(self):
  118. """Performs a single step: accumulated replies are fed to the generator,
  119. which then produces requests."""
  120. current_generator, _, _, replies, has_reply = self.generator_stack[-1]
  121. # Send the replies to the generator, and ask for new requests.
  122. requests = current_generator.send(replies if has_reply else None)
  123. # Update the entry on the stack.
  124. self.generator_stack[-1] = (current_generator, requests, False, [], False)
  125. def handle_exception(self, exception):
  126. """Handles the given exception. A Boolean is returned that tells if
  127. the exception was handled."""
  128. # print('Exception thrown from %s: %s' % (str(self.generator_stack[-1]), str(exception)))
  129. while len(self.exception_handlers) > 0:
  130. # Pop the top-of-stack exception handler.
  131. stack_index, handlers = self.exception_handlers.pop()
  132. # Try to find an applicable handler.
  133. applicable_handler = None
  134. for handled_type, handler in handlers:
  135. if isinstance(exception, handled_type):
  136. applicable_handler = handler
  137. if applicable_handler is not None:
  138. # We handle exceptions by first clearing the current stack frame and
  139. # all of its children. Then, we place a dummy frame on the stack with
  140. # a single 'TAIL_CALL_ARGS' request. The next iteration will replace
  141. # the dummy frame by an actual frame.
  142. del self.generator_stack[stack_index:]
  143. self.generator_stack.append(
  144. (None,
  145. [('TAIL_CALL_ARGS', [applicable_handler, (exception,)])],
  146. False,
  147. [],
  148. False))
  149. return True
  150. # We couldn't find an applicable exception handler, even after exhausting the
  151. # entire exception handler stack. All is lost.
  152. # Also, clean up after ourselves.
  153. self.generator_stack = []
  154. self.exception_handlers = []
  155. return False
  156. def pop_requests(self):
  157. """Tries to pop a batch of Modelverse _state_ requests from the
  158. current list of requests. Known requests are executed immediately.
  159. A list of requests and a Boolean are returned. The latter is True
  160. if there are no more requests to process, and false otherwise."""
  161. _, requests, _, _, _ = self.generator_stack[-1]
  162. if requests is None or len(requests) == 0:
  163. # Couldn't find a request for the state to handle.
  164. self.set_finished_requests_flag()
  165. return requests
  166. for i, elem in enumerate(requests):
  167. if elem[0] in self.handlers:
  168. # The kernel should handle known requests.
  169. if i > 0:
  170. # Handle any requests that precede the known request first.
  171. pre_requests = requests[:i]
  172. del requests[:i]
  173. return pre_requests
  174. # The known request must be the first element in the list. Pop it.
  175. requests.pop(0)
  176. # The list of requests might be empty now. If so, then flag this
  177. # batch of requests as finished.
  178. if len(requests) == 0:
  179. self.set_finished_requests_flag()
  180. # Handle the request.
  181. _, request_args = elem
  182. self.handlers[elem[0]](request_args)
  183. raise KnownRequestHandled()
  184. # We couldn't find a known request in the batch of requests, so we might as well
  185. # handle them all at once then.
  186. self.set_finished_requests_flag()
  187. return requests
  188. def execute_call(self, request_args):
  189. """Executes a CALL-request with the given argument list."""
  190. # Format: ("CALL", [gen])
  191. gen, = request_args
  192. self.push_generator(gen)
  193. def execute_call_kwargs(self, request_args):
  194. """Executes a CALL_KWARGS-request with the given argument list."""
  195. # Format: ("CALL_KWARGS", [func, kwargs])
  196. # This format is useful because it also works for functions that
  197. # throw an exception but never yield.
  198. func, kwargs = request_args
  199. # We need to be extra careful here, because func(**kwargs) might
  200. # not be a generator at all: it might simply be a method that
  201. # raises an exception. To cope with this we need to push a dummy
  202. # entry onto the stack if a StopIteration or PrimtiveFinished
  203. # exception is thrown. The logic in execute_yields will then pop
  204. # that dummy entry.
  205. try:
  206. self.push_generator(func(**kwargs))
  207. except StopIteration:
  208. self.push_generator(None)
  209. raise
  210. except primitive_functions.PrimitiveFinished:
  211. self.push_generator(None)
  212. raise
  213. def execute_call_args(self, request_args):
  214. """Executes a CALL_ARGS-request with the given argument list."""
  215. # Format: ("CALL_ARGS", [gen, args])
  216. func, args = request_args
  217. # We need to be extra careful here, because func(*args) might
  218. # not be a generator at all: it might simply be a method that
  219. # raises an exception. To cope with this we need to push a dummy
  220. # entry onto the stack if a StopIteration or PrimtiveFinished
  221. # exception is thrown. The logic in execute_yields will then pop
  222. # that dummy entry.
  223. try:
  224. self.push_generator(func(*args))
  225. except StopIteration:
  226. self.push_generator(None)
  227. raise
  228. except primitive_functions.PrimitiveFinished:
  229. self.push_generator(None)
  230. raise
  231. def execute_tail_call(self, request_args):
  232. """Executes a TAIL_CALL-request with the given argument list."""
  233. # Format: ("TAIL_CALL", [gen])
  234. self.pop_generator()
  235. self.execute_call(request_args)
  236. def execute_tail_call_args(self, request_args):
  237. """Executes a TAIL_CALL_ARGS-request with the given argument list."""
  238. # Format: ("TAIL_CALL_ARGS", [gen, args])
  239. self.pop_generator()
  240. self.execute_call_args(request_args)
  241. def execute_tail_call_kwargs(self, request_args):
  242. """Executes a TAIL_CALL_KWARGS-request with the given argument list."""
  243. # Format: ("TAIL_CALL_KWARGS", [gen, kwargs])
  244. self.pop_generator()
  245. self.execute_call_kwargs(request_args)
  246. def execute_try(self, request_args):
  247. """Executes a TRY-request with the given argument list."""
  248. # TRY pushes an exception handler onto the exception handler stack.
  249. # Format: ("TRY", [])
  250. if len(request_args) != 0:
  251. raise ValueError(
  252. ("TRY was given argument list '%s', " +
  253. "expected exactly zero arguments.") % repr(request_args))
  254. self.exception_handlers.append((len(self.generator_stack) - 1, []))
  255. def execute_catch(self, request_args):
  256. """Executes a CATCH-request with the given argument list."""
  257. if len(request_args) != 2:
  258. raise ValueError(
  259. ("CATCH was given argument list '%s', "
  260. "expected exactly two arguments: an exception "
  261. "type and an exception handler.") % repr(request_args))
  262. exception_type, handler = request_args
  263. stack_index, handlers = self.exception_handlers[-1]
  264. if stack_index != len(self.generator_stack) - 1:
  265. raise ValueError(
  266. 'Cannot comply with CATCH because there is no exception handler for the '
  267. 'current generator.')
  268. handlers.append((exception_type, handler))
  269. def execute_end_try(self, request_args):
  270. """Executes an END_TRY-request with the given argument list."""
  271. # END_TRY pops a value from the exception handler stack. The
  272. # popped value must reference the top-of-stack element in the
  273. # generator stack. END_TRY takes no arguments.
  274. # Format: ("END_TRY", [])
  275. if len(request_args) != 0:
  276. raise ValueError(
  277. "END_TRY was given argument list '%s', expected '%s'." % (
  278. repr(request_args), repr([])))
  279. if len(self.exception_handlers) == 0:
  280. raise ValueError(
  281. 'Cannot comply with END_TRY because the exception handler stack is empty.')
  282. stack_index, _ = self.exception_handlers[-1]
  283. if stack_index != len(self.generator_stack) - 1:
  284. raise ValueError(
  285. 'Cannot comply with END_TRY because there is no exception handler for the '
  286. 'current generator.')
  287. # Everything seems to be in order. Pop the exception handler.
  288. self.exception_handlers.pop()