request_handler.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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. gen = 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. if len(self.exception_handlers) == 0:
  130. # Yep, we're hosed. Make sure the caller knows this.
  131. # Also, clean up.
  132. self.generator_stack = []
  133. self.exception_handlers = []
  134. return False
  135. # Pop the top-of-stack exception handler.
  136. stack_index, handlers = self.exception_handlers.pop()
  137. # Try to find an applicable handler.
  138. applicable_handler = None
  139. for handled_type, handler in handlers:
  140. if isinstance(exception, handled_type):
  141. applicable_handler = handler
  142. if applicable_handler is None:
  143. # We couldn't find an applicable handler. All is lost.
  144. self.generator_stack = []
  145. self.exception_handlers = []
  146. return False
  147. # We handle exceptions by first clearing the current stack frame and
  148. # all of its children. Then, we place a dummy frame on the stack with
  149. # a single 'TAIL_CALL_ARGS' request. The next iteration will replace
  150. # the dummy frame by an actual frame.
  151. del self.generator_stack[stack_index:]
  152. self.generator_stack.append(
  153. (None, [('TAIL_CALL_ARGS', [applicable_handler, (exception,)])], False, [], False))
  154. return True
  155. def pop_requests(self):
  156. """Tries to pop a batch of Modelverse _state_ requests from the
  157. current list of requests. Known requests are executed immediately.
  158. A list of requests and a Boolean are returned. The latter is True
  159. if there are no more requests to process, and false otherwise."""
  160. _, requests, _, _, _ = self.generator_stack[-1]
  161. if requests is None or len(requests) == 0:
  162. # Couldn't find a request for the state to handle.
  163. self.set_finished_requests_flag()
  164. return requests
  165. for i, elem in enumerate(requests):
  166. if elem[0] in self.handlers:
  167. # The kernel should handle known requests.
  168. if i > 0:
  169. # Handle any requests that precede the known request first.
  170. pre_requests = requests[:i]
  171. del requests[:i]
  172. return pre_requests
  173. # The known request must be the first element in the list. Pop it.
  174. requests.pop(0)
  175. # The list of requests might be empty now. If so, then flag this
  176. # batch of requests as finished.
  177. if len(requests) == 0:
  178. self.set_finished_requests_flag()
  179. # Handle the request.
  180. _, request_args = elem
  181. self.handlers[elem[0]](request_args)
  182. raise KnownRequestHandled()
  183. # We couldn't find a known request in the batch of requests, so we might as well
  184. # handle them all at once then.
  185. self.set_finished_requests_flag()
  186. return requests
  187. def execute_call(self, request_args):
  188. """Executes a CALL-request with the given argument list."""
  189. # Format: ("CALL", [gen])
  190. gen, = request_args
  191. self.push_generator(gen)
  192. def execute_call_kwargs(self, request_args):
  193. """Executes a CALL_KWARGS-request with the given argument list."""
  194. # Format: ("CALL_KWARGS", [func, kwargs])
  195. # This format is useful because it also works for functions that
  196. # throw an exception but never yield.
  197. func, kwargs = request_args
  198. # We need to be extra careful here, because func(**kwargs) might
  199. # not be a generator at all: it might simply be a method that
  200. # raises an exception. To cope with this we need to push a dummy
  201. # entry onto the stack if a StopIteration or PrimtiveFinished
  202. # exception is thrown. The logic in execute_yields will then pop
  203. # that dummy entry.
  204. try:
  205. self.push_generator(func(**kwargs))
  206. except StopIteration:
  207. self.push_generator(None)
  208. raise
  209. except primitive_functions.PrimitiveFinished:
  210. self.push_generator(None)
  211. raise
  212. def execute_call_args(self, request_args):
  213. """Executes a CALL_ARGS-request with the given argument list."""
  214. # Format: ("CALL_ARGS", [gen, args])
  215. func, args = request_args
  216. # We need to be extra careful here, because func(*args) might
  217. # not be a generator at all: it might simply be a method that
  218. # raises an exception. To cope with this we need to push a dummy
  219. # entry onto the stack if a StopIteration or PrimtiveFinished
  220. # exception is thrown. The logic in execute_yields will then pop
  221. # that dummy entry.
  222. try:
  223. self.push_generator(func(*args))
  224. except StopIteration:
  225. self.push_generator(None)
  226. raise
  227. except primitive_functions.PrimitiveFinished:
  228. self.push_generator(None)
  229. raise
  230. def execute_tail_call(self, request_args):
  231. """Executes a TAIL_CALL-request with the given argument list."""
  232. # Format: ("TAIL_CALL", [gen])
  233. self.pop_generator()
  234. self.execute_call(request_args)
  235. def execute_tail_call_args(self, request_args):
  236. """Executes a TAIL_CALL_ARGS-request with the given argument list."""
  237. # Format: ("TAIL_CALL_ARGS", [gen, args])
  238. self.pop_generator()
  239. self.execute_call_args(request_args)
  240. def execute_tail_call_kwargs(self, request_args):
  241. """Executes a TAIL_CALL_KWARGS-request with the given argument list."""
  242. # Format: ("TAIL_CALL_KWARGS", [gen, kwargs])
  243. self.pop_generator()
  244. self.execute_call_kwargs(request_args)
  245. def execute_try(self, request_args):
  246. """Executes a TRY-request with the given argument list."""
  247. # TRY pushes an exception handler onto the exception handler stack.
  248. # Format: ("TRY", [generator-returning-function])
  249. if len(request_args) != 0:
  250. raise ValueError(
  251. ("TRY was given argument list '%s', " +
  252. "expected exactly zero arguments.") % repr(request_args))
  253. self.exception_handlers.append((len(self.generator_stack) - 1, []))
  254. def execute_catch(self, request_args):
  255. """Executes a CATCH-request with the given argument list."""
  256. if len(request_args) != 2:
  257. raise ValueError(
  258. ("CATCH was given argument list '%s', "
  259. "expected exactly two arguments: an exception "
  260. "type and an exception handler.") % repr(request_args))
  261. exception_type, handler = request_args
  262. stack_index, handlers = self.exception_handlers[-1]
  263. if stack_index != len(self.generator_stack) - 1:
  264. raise ValueError(
  265. 'Cannot comply with CATCH because there is no exception handler for the '
  266. 'current generator.')
  267. handlers.append((exception_type, handler))
  268. def execute_end_try(self, request_args):
  269. """Executes an END_TRY-request with the given argument list."""
  270. # END_TRY pops a value from the exception handler stack. The
  271. # popped value must reference the top-of-stack element in the
  272. # generator stack. END_TRY takes no arguments.
  273. # Format: ("END_TRY", [])
  274. if len(request_args) != 0:
  275. raise ValueError(
  276. "END_TRY was given argument list '%s', expected '%s'." % (
  277. repr(request_args), repr([])))
  278. if len(self.exception_handlers) == 0:
  279. raise ValueError(
  280. 'Cannot comply with END_TRY because the exception handler stack is empty.')
  281. stack_index, _ = self.exception_handlers[-1]
  282. if stack_index != len(self.generator_stack) - 1:
  283. raise ValueError(
  284. 'Cannot comply with END_TRY because there is no exception handler for the '
  285. 'current generator.')
  286. # Everything seems to be in order. Pop the exception handler.
  287. self.exception_handlers.pop()