123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- import sys
- import modelverse_kernel.primitives as primitive_functions
- import modelverse_jit.runtime as jit_runtime
- class KnownRequestHandled(Exception):
- """An exception that signifies that a known request was handled."""
- pass
- class GeneratorStackEntry(object):
- """An entry in the generator stack of a request handles."""
- def __init__(self, generator):
- self.generator = generator
- self.function_name = None
- self.source_map = None
- self.function_origin = None
- self.pending_requests = None
- self.finished_requests = True
- self.replies = []
- self.has_reply = False
- def append_reply(self, new_reply):
- """Appends a reply to the this entry's list of pending replies."""
- self.replies.append(new_reply)
- self.has_reply = True
- def extend_replies(self, new_replies):
- """Appends a list of replies to this entry's list of pending replies."""
- if new_replies is not None:
- self.replies.extend(new_replies)
- self.has_reply = True
- def step(self):
- """Performs a single step: accumulated replies are fed to the generator,
- which then produces requests."""
- # Send the replies to the generator, and ask for new requests.
- self.pending_requests = self.generator.send(self.replies if self.has_reply else None)
- # Reset some data structures.
- self.finished_requests = False
- self.replies = []
- self.has_reply = False
- def format_stack_trace(stack_trace):
- """Formats a list of (function name, debug info, origin) triples."""
- return '\n'.join([jit_runtime.format_stack_frame(*triple) for triple in stack_trace])
- class UnhandledRequestHandlerException(Exception):
- """The type of exception that is thrown when the request handler encounters an
- unhandled exception."""
- def __init__(self, inner_exception, stack_trace):
- Exception.__init__(
- self,
- """The request handler encountered an unknown exception.\n
- Inner exception: %s\n
- Stack trace:\n%s\n""" % (inner_exception, format_stack_trace(stack_trace)))
- self.inner_exception = inner_exception
- self.stack_trace = stack_trace
- class RequestHandler(object):
- """A type of object that intercepts logic-related Modelverse requests, and
- forwards Modelverse state requests."""
- def __init__(self):
- # generator_stack is a stack of GeneratorStackEntry values.
- self.generator_stack = []
- # exception_handlers is a stack of
- # (generator_stack index, [(exception type, handler function)])
- # tuples.
- self.exception_handlers = []
- self.produce_stack_trace = True
- self.handlers = {
- 'CALL' : self.execute_call,
- 'CALL_ARGS' : self.execute_call_args,
- 'CALL_KWARGS' : self.execute_call_kwargs,
- 'TAIL_CALL' : self.execute_tail_call,
- 'TAIL_CALL_ARGS' : self.execute_tail_call_args,
- 'TAIL_CALL_KWARGS' : self.execute_tail_call_kwargs,
- 'TRY' : self.execute_try,
- 'CATCH' : self.execute_catch,
- 'END_TRY' : self.execute_end_try,
- 'DEBUG_INFO' : self.execute_debug_info
- }
- def is_active(self):
- """Tests if this request handler has a top-of-stack generator."""
- return len(self.generator_stack) > 0
- def handle_request(self, reply):
- """Replies to a request from the top-of-stack generator, and returns a new request."""
- if not self.is_active():
- raise ValueError('handle_request cannot be called with an empty generator stack.')
- # Append the server's replies to the list of replies.
- self.extend_replies(reply)
- while 1:
- # Silence pylint's warning about catching Exception.
- # pylint: disable=I0011,W0703
- try:
- if self.has_pending_requests():
- try:
- # Try to pop a request for the modelverse state.
- return self.pop_requests()
- except KnownRequestHandled:
- # Carry on.
- pass
- if not self.has_pending_requests():
- # Perform a single generator step.
- self.step()
- except StopIteration:
- # Done, so remove the generator
- self.pop_generator()
- if self.is_active():
- # This generator was called from another generator.
- # Append 'None' to the caller's list of replies.
- self.append_reply(None)
- else:
- # Looks like we're done here.
- return None
- except primitive_functions.PrimitiveFinished as ex:
- # Done, so remove the generator
- self.pop_generator()
- if self.is_active():
- # This generator was called from another generator.
- # Append the callee's result to the caller's list of replies.
- self.append_reply(ex.result)
- else:
- # Looks like we're done here.
- return None
- except Exception as ex:
- # Maybe get an exception handler to do this.
- stack_trace = self.handle_exception(ex)
- if stack_trace is not None:
- if self.produce_stack_trace:
- raise UnhandledRequestHandlerException(ex, stack_trace)
- else:
- raise
- def set_finished_requests_flag(self):
- """Sets the finished_requests flag in the top-of-stack tuple."""
- self.generator_stack[-1].finished_requests = True
- def has_pending_requests(self):
- """Tests if the top-of-stack generator has pending requests."""
- return not self.generator_stack[-1].finished_requests
- def push_generator(self, gen):
- """Pushes a new generator onto the stack."""
- self.generator_stack.append(GeneratorStackEntry(gen))
- # print('Pushed generator %s. Generator count: %d' % (gen, len(self.generator_stack)))
- def pop_generator(self):
- """Removes the top-of-stack generator from the generator stack."""
- # Pop the generator itself.
- self.generator_stack.pop()
- # print('Popped generator %s. Generator count: %d' % (gen, len(self.generator_stack)))
- # Pop any exception handlers defined by the generator.
- top_of_stack_index = len(self.generator_stack)
- while len(self.exception_handlers) > 0:
- stack_index, _ = self.exception_handlers[-1]
- if stack_index == top_of_stack_index:
- # Pop exception handlers until exception_handlers is empty or until
- # we find an exception handler that is not associated with the popped
- # generator.
- self.exception_handlers.pop()
- else:
- # We're done here.
- break
- def append_reply(self, new_reply):
- """Appends a reply to the top-of-stack generator's list of pending replies."""
- self.generator_stack[-1].append_reply(new_reply)
- def extend_replies(self, new_replies):
- """Appends a list of replies to the top-of-stack generator's list of pending replies."""
- self.generator_stack[-1].extend_replies(new_replies)
- def step(self):
- """Performs a single step: accumulated replies are fed to the generator,
- which then produces requests."""
- self.generator_stack[-1].step()
- def handle_exception(self, exception):
- """Handles the given exception. A Boolean is returned that tells if
- the exception was handled."""
- # print('Exception thrown from %s: %s' % (str(self.generator_stack[-1]), str(exception)))
- while len(self.exception_handlers) > 0:
- # Pop the top-of-stack exception handler.
- stack_index, handlers = self.exception_handlers.pop()
- # Try to find an applicable handler.
- applicable_handler = None
- for handled_type, handler in handlers:
- if isinstance(exception, handled_type):
- applicable_handler = handler
- if applicable_handler is not None:
- # We handle exceptions by first clearing the current stack frame and
- # all of its children. Then, we place a dummy frame on the stack with
- # a single 'TAIL_CALL_ARGS' request. The next iteration will replace
- # the dummy frame by an actual frame.
- del self.generator_stack[stack_index:]
- stack_entry = GeneratorStackEntry(None)
- stack_entry.pending_requests = [
- ('TAIL_CALL_ARGS', [applicable_handler, (exception,)])]
- stack_entry.finished_requests = False
- self.generator_stack.append(stack_entry)
- return None
- # We couldn't find an applicable exception handler, even after exhausting the
- # entire exception handler stack. All is lost.
- # Also, clean up after ourselves by unwinding the stack.
- return self.unwind_stack()
- def unwind_stack(self):
- """Unwinds the entirety of the stack. All generators and exception handlers are
- discarded. A list of (function name, debug information, source) statements is
- returned."""
- class UnwindStackException(Exception):
- """A hard-to-catch exception that is used to make generators crash.
- The exceptions they produce can then be analyzed for line numbers."""
- pass
- # First throw away all exception handlers. We won't be needing them any more.
- self.exception_handlers = []
- # Then pop every generator from the stack and make it crash.
- stack_trace = []
- while len(self.generator_stack) > 0:
- top_entry = self.generator_stack.pop()
- if top_entry.function_origin is None:
- # Skip this function.
- continue
- try:
- # Crash the generator.
- top_entry.generator.throw(UnwindStackException())
- except UnwindStackException:
- # Find out where the exception was thrown.
- _, _, exc_traceback = sys.exc_info()
- while exc_traceback.tb_next is not None:
- exc_traceback = exc_traceback.tb_next
- line_number = exc_traceback.tb_lineno
- source_map = top_entry.source_map
- if source_map is not None:
- debug_info = source_map.get_debug_info(line_number)
- else:
- debug_info = None
- function_name = top_entry.function_name
- stack_trace.append((function_name, debug_info, top_entry.function_origin))
- return stack_trace[::-1]
- def pop_requests(self):
- """Tries to pop a batch of Modelverse _state_ requests from the
- current list of requests. Known requests are executed immediately.
- A list of requests and a Boolean are returned. The latter is True
- if there are no more requests to process, and false otherwise."""
- requests = self.generator_stack[-1].pending_requests
- if requests is None or len(requests) == 0:
- # Couldn't find a request for the state to handle.
- self.set_finished_requests_flag()
- return requests
- for i, elem in enumerate(requests):
- if elem[0] in self.handlers:
- # The kernel should handle known requests.
- if i > 0:
- # Handle any requests that precede the known request first.
- pre_requests = requests[:i]
- del requests[:i]
- return pre_requests
- # The known request must be the first element in the list. Pop it.
- requests.pop(0)
- # The list of requests might be empty now. If so, then flag this
- # batch of requests as finished.
- if len(requests) == 0:
- self.set_finished_requests_flag()
- # Handle the request.
- _, request_args = elem
- self.handlers[elem[0]](request_args)
- raise KnownRequestHandled()
- # We couldn't find a known request in the batch of requests, so we might as well
- # handle them all at once then.
- self.set_finished_requests_flag()
- return requests
- def execute_call(self, request_args):
- """Executes a CALL-request with the given argument list."""
- # Format: ("CALL", [gen])
- gen, = request_args
- self.push_generator(gen)
- def execute_call_kwargs(self, request_args):
- """Executes a CALL_KWARGS-request with the given argument list."""
- # Format: ("CALL_KWARGS", [func, kwargs])
- # This format is useful because it also works for functions that
- # throw an exception but never yield.
- func, kwargs = request_args
- # We need to be extra careful here, because func(**kwargs) might
- # not be a generator at all: it might simply be a method that
- # raises an exception. To cope with this we need to push a dummy
- # entry onto the stack if a StopIteration or PrimtiveFinished
- # exception is thrown. The logic in execute_yields will then pop
- # that dummy entry.
- try:
- self.push_generator(func(**kwargs))
- except StopIteration:
- self.push_generator(None)
- raise
- except primitive_functions.PrimitiveFinished:
- self.push_generator(None)
- raise
- def execute_call_args(self, request_args):
- """Executes a CALL_ARGS-request with the given argument list."""
- # Format: ("CALL_ARGS", [gen, args])
- func, args = request_args
- # We need to be extra careful here, because func(*args) might
- # not be a generator at all: it might simply be a method that
- # raises an exception. To cope with this we need to push a dummy
- # entry onto the stack if a StopIteration or PrimtiveFinished
- # exception is thrown. The logic in execute_yields will then pop
- # that dummy entry.
- try:
- self.push_generator(func(*args))
- except StopIteration:
- self.push_generator(None)
- raise
- except primitive_functions.PrimitiveFinished:
- self.push_generator(None)
- raise
- def execute_tail_call(self, request_args):
- """Executes a TAIL_CALL-request with the given argument list."""
- # Format: ("TAIL_CALL", [gen])
- self.pop_generator()
- self.execute_call(request_args)
- def execute_tail_call_args(self, request_args):
- """Executes a TAIL_CALL_ARGS-request with the given argument list."""
- # Format: ("TAIL_CALL_ARGS", [gen, args])
- self.pop_generator()
- self.execute_call_args(request_args)
- def execute_tail_call_kwargs(self, request_args):
- """Executes a TAIL_CALL_KWARGS-request with the given argument list."""
- # Format: ("TAIL_CALL_KWARGS", [gen, kwargs])
- self.pop_generator()
- self.execute_call_kwargs(request_args)
- def execute_try(self, request_args):
- """Executes a TRY-request with the given argument list."""
- # TRY pushes an exception handler onto the exception handler stack.
- # Format: ("TRY", [])
- if len(request_args) != 0:
- raise ValueError(
- ("TRY was given argument list '%s', " +
- "expected exactly zero arguments.") % repr(request_args))
- self.exception_handlers.append((len(self.generator_stack) - 1, []))
- self.append_reply(None)
- def execute_catch(self, request_args):
- """Executes a CATCH-request with the given argument list."""
- if len(request_args) != 2:
- raise ValueError(
- ("CATCH was given argument list '%s', "
- "expected exactly two arguments: an exception "
- "type and an exception handler.") % repr(request_args))
- exception_type, handler = request_args
- stack_index, handlers = self.exception_handlers[-1]
- if stack_index != len(self.generator_stack) - 1:
- raise ValueError(
- 'Cannot comply with CATCH because there is no exception handler for the '
- 'current generator.')
- handlers.append((exception_type, handler))
- self.append_reply(None)
- def execute_end_try(self, request_args):
- """Executes an END_TRY-request with the given argument list."""
- # END_TRY pops a value from the exception handler stack. The
- # popped value must reference the top-of-stack element in the
- # generator stack. END_TRY takes no arguments.
- # Format: ("END_TRY", [])
- if len(request_args) != 0:
- raise ValueError(
- "END_TRY was given argument list '%s', expected '%s'." % (
- repr(request_args), repr([])))
- if len(self.exception_handlers) == 0:
- raise ValueError(
- 'Cannot comply with END_TRY because the exception handler stack is empty.')
- stack_index, _ = self.exception_handlers[-1]
- if stack_index != len(self.generator_stack) - 1:
- raise ValueError(
- 'Cannot comply with END_TRY because there is no exception handler for the '
- 'current generator.')
- # Everything seems to be in order. Pop the exception handler.
- self.exception_handlers.pop()
- self.append_reply(None)
- def execute_debug_info(self, request_args):
- """Executes a DEBUG_INFO-request with the given argument list."""
- # DEBUG_INFO updates the function name and source map for the top-of-stack generator.
- # These two things allow us to unwind the stack neatly if an unhandled exception is
- # encountered.
- # Format: ("DEBUG_INFO", [function_name, source_map])
- top_entry = self.generator_stack[-1]
- top_entry.function_name, top_entry.source_map, top_entry.function_origin = request_args
- top_entry.append_reply(None)
|