request_handler.py 18 KB

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