import sys import modelverse_kernel.primitives as primitive_functions import modelverse_kernel.jit as jit from collections import defaultdict class KnownRequestHandled(Exception): """An exception that signifies that a known request was handled.""" pass 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): import traceback Exception.__init__( self, """The request handler encountered an unknown exception.\n Inner exception: %s\n """ % (traceback.format_exc())) 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.produce_stack_trace = True self.handlers = { 'CALL' : self.execute_call, 'CALL_ARGS' : self.execute_call_args, 'CALL_KWARGS' : self.execute_call_kwargs, 'SLEEP' : self.execute_sleep, } 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.generator_stack: raise ValueError('handle_request cannot be called with an empty generator stack.') # Append the server's replies to the list of replies. if reply is not None: gen = self.generator_stack[-1] if gen["replies"]: gen["replies"].extend(reply) else: gen["replies"] = reply while 1: # Silence pylint's warning about catching Exception. # pylint: disable=I0011,W0703 try: gen = self.generator_stack[-1] if gen["finished_requests"]: gen["pending_requests"] = gen["generator"].send(gen["replies"]) gen["finished_requests"] = False gen["replies"] = None return self.pop_requests() except KnownRequestHandled: pass except StopIteration: # Done, so remove the generator del self.generator_stack[-1] if self.generator_stack: # 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 del self.generator_stack[-1] if self.generator_stack: # 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 def push_generator(self, gen): """Pushes a new generator onto the stack.""" dd = defaultdict(lambda : None) dd["generator"] = gen dd["finished_requests"] = True self.generator_stack.append(dd) # print('Pushed generator %s. Generator count: %d' % (gen, len(self.generator_stack))) def append_reply(self, new_reply): """Appends a reply to the top-of-stack generator's list of pending replies.""" if self.generator_stack[-1]["replies"] is None: self.generator_stack[-1]["replies"] = [new_reply] else: self.generator_stack[-1]["replies"].append(new_reply) 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: if requests[0][0] in self.handlers: # First element is a known request elem = requests.pop(0) # The list of requests might be empty now. If so, then flag this # batch of requests as finished. if not requests: self.generator_stack[-1]["finished_requests"] = True # Handle the request. self.handlers[elem[0]](elem[1]) raise KnownRequestHandled() else: for i, elem in enumerate(requests): if elem[0] in self.handlers: # Handle any requests that precede the known request first. pre_requests = requests[:i] del requests[:i] return pre_requests # We couldn't find a known request in the batch of requests, so we might as well # handle them all at once then. self.generator_stack[-1]["finished_requests"] = True 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 except: print("EXCEPTION for " + str(locals())) 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_sleep(self, request_args): """Executes a SLEEP-request with the given argument list.""" self.append_reply(None) raise primitive_functions.SleepKernel(request_args[0], request_args[1])