request_handler.py 17 KB

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