request_handler.py 15 KB

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