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.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: raise UnhandledRequestHandlerException(ex, stack_trace) 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() 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)