Browse Source

Implement the 'RUN' instruction in the interpreter

jonathanvdc 8 years ago
parent
commit
f91f438320
3 changed files with 178 additions and 82 deletions
  1. 11 22
      kernel/modelverse_jit/runtime.py
  2. 149 51
      kernel/modelverse_kernel/main.py
  3. 18 9
      kernel/modelverse_kernel/primitives.py

+ 11 - 22
kernel/modelverse_jit/runtime.py

@@ -20,20 +20,13 @@ def call_function(function_id, named_arguments, **kwargs):
     # frame.
     try:
         # Try to compile.
-        jit_gen = kernel.jit_compile(user_root, body_id)
-        inp = None
-        while 1:
-            inp = yield jit_gen.send(inp)
-    except primitive_functions.PrimitiveFinished as ex:
-        compiled_func = ex.result
+        compiled_func, = yield [("RUN", kernel.jit_compile(user_root, body_id))]
         # Add the keyword arguments to the argument dictionary.
         named_arguments.update(kwargs)
         # Run the function.
-        gen = compiled_func(**named_arguments)
-        inp = None
-        while 1:
-            inp = yield gen.send(inp)
-        # Let primitive_functions.PrimitiveFinished bubble up.
+        result, = yield [("RUN", compiled_func, named_arguments)]
+        # Return.
+        raise primitive_functions.PrimitiveFinished(result)
     except JitCompilationFailedException:
         # That's quite alright. Just build a stack frame and hand the function to
         # the interpreter.
@@ -62,7 +55,7 @@ def call_function(function_id, named_arguments, **kwargs):
                            ("CD", [new_frame, "prev", user_frame]),
                            ("CD", [
                                new_frame,
-                               primitive_functions.PRIMITIVE_RETURN_KEY,
+                               primitive_functions.EXCEPTION_RETURN_KEY,
                                intrinsic_return]),
                            ("DE", [frame_link])
                           ]
@@ -85,13 +78,9 @@ def call_function(function_id, named_arguments, **kwargs):
         yield [("CE", [symbol_edge, param_var])]
 
     username = kwargs['username']
-    while 1:
-        try:
-            gen = kernel.execute_rule(username)
-            inp = None
-            while 1:
-                inp = yield gen.send(inp)
-        except StopIteration:
-            # An instruction has been completed. Forward it.
-            yield None
-        # Let primitive_functions.PrimitiveFinished bubble up.
+    try:
+        while 1:
+            result, = yield [("RUN", kernel.execute_rule(username))]
+            yield result
+    except primitive_functions.InterpretedFunctionFinished as ex:
+        raise primitive_functions.PrimitiveFinished(ex.result)

+ 149 - 51
kernel/modelverse_kernel/main.py

@@ -11,11 +11,70 @@ if sys.version > '3': # pragma: no cover
 else:
     string_types = (str, unicode)
 
+class RunRequest(Exception):
+    pass
+
+def set_finished_requests_flag(opStack):
+    """Sets the finished_requests flag in the top-of-stack tuple."""
+    current_generator, requests, finished_requests, replies, has_reply = opStack[-1]
+    opStack[-1] = (current_generator, requests, True, replies, has_reply)
+
+def push_generator(gen, opStack):
+    """Pushes a new generator onto the given stack."""
+    opStack.append((gen, None, True, [], False))
+
+def pop_requests(requests, opStack):
+    """Tries to pop a batch of Modelverse _state_ requests from the
+       given list of requests. RUN-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."""
+    if requests is None or len(requests) == 0:
+        # Couldn't find a request for the state to handle.
+        set_finished_requests_flag(opStack)
+        return requests
+
+    first_request = requests.pop(0)
+    if first_request[0] == "RUN":
+        # The kernel handles RUN-requests.
+        if len(requests) == 0:
+            set_finished_requests_flag(opStack)
+
+        request_length = len(first_request)
+        if request_length == 2:
+            # Format: ("RUN", gen)
+            _, gen = first_request
+            push_generator(gen, opStack)
+        else:
+            # Format: ("RUN", func, kwargs)
+            # This format is useful because it also works for functions that
+            # throw an exception but never yield.
+            _, func, kwargs = first_request
+            push_generator(func(**kwargs), opStack)
+
+        raise RunRequest()
+    else:
+        # The state handles all other requests.
+        if len(requests) == 0:
+            set_finished_requests_flag(opStack)
+
+        return [first_request]
+
 class ModelverseKernel(object):
     def __init__(self, root):
         self.root = root
         self.returnvalue = None
         self.success = True
+        #
+        # generators is a dictionary of usernames to dictionaries of operations
+        # to stacks of (generator, pending requests, request replies, has-reply) tuples.
+        # In generics notation:
+        # Dictionary<
+        #     Username,
+        #     Dictionary<
+        #         Operation,
+        #         Stack<(Generator, Queue<List<Request>>, List<Replies>, Boolean)>>
+        #
         self.generators = {}
         self.allow_compiled = True
         #self.allow_compiled = False
@@ -46,26 +105,80 @@ class ModelverseKernel(object):
         self.debug_info = defaultdict(list)
 
     def execute_yields(self, username, operation, params, reply):
-        try:
-            self.success = True
-            self.username = username
-            if username not in self.generators:
-                self.generators[username] = {}
-            if operation not in self.generators[username]:
-                # Create the generator for the function to execute
-                self.generators[username][operation] = getattr(self, operation)(username, *params)
-
-            if reply is not None:
-                return self.generators[username][operation].send(reply)
-            else:
-                return self.generators[username][operation].next()
-        except StopIteration:
-            # Done, so remove the generator
-            del self.generators[username][operation]
-            return None
-        except:
-            print("Unknown error @ %s" % self.debug_info[username])
-            raise
+        self.success = True
+        self.username = username
+        if username not in self.generators:
+            self.generators[username] = {}
+        if operation not in self.generators[username]:
+            # Create the generator for the function to execute
+            self.generators[username][operation] = []
+        opStack = self.generators[username][operation]
+        if len(opStack) == 0:
+            push_generator(getattr(self, operation)(username, *params), opStack)
+
+        current_generator, requests, finished_requests, replies, has_reply = opStack[-1]
+        # Append the server's replies to the list of replies.
+        if reply is not None:
+            replies.extend(reply)
+            has_reply = True
+            opStack[-1] = (current_generator, requests, finished_requests, replies, has_reply)
+        while 1:
+            try:
+                if not finished_requests:
+                    try:
+                        # Try to pop a request for the modelverse state.
+                        result = pop_requests(requests, opStack)
+                        return result
+                    except RunRequest:
+                        # Carry on.
+                        pass
+
+                # The call to pop_requests may have appended a tuple to the stack,
+                # so we need to reload the top-of-stack tuple.
+                current_generator, requests, finished_requests, replies, has_reply = opStack[-1]
+
+                # Send the replies to the generator, and ask for new requests.
+                requests = current_generator.send(replies if has_reply else None)
+                finished_requests = False
+
+                # Update the entry in the stack.
+                opStack[-1] = (current_generator, requests, finished_requests, [], False)
+            except StopIteration:
+                # Done, so remove the generator
+                opStack.pop()
+                if len(opStack) == 0:
+                    # Looks like we're done here.
+                    # Maybe remove the stack of generators as well?
+                    # del self.generators[username][operation]
+                    return None
+
+                # Update the current generator, requests, and replies.
+                current_generator, requests, finished_requests, replies, has_reply = opStack[-1]
+                # Append 'None' to the list of replies.
+                replies.append(None)
+                if not has_reply:
+                    has_reply = True
+                    # Update the stack.
+                    opStack[-1] = current_generator, requests, finished_requests, replies, has_reply
+            except primitive_functions.PrimitiveFinished as ex:
+                # Done, so remove the generator.
+                opStack.pop()
+                if len(opStack) == 0:
+                    # Looks like we're done here.
+                    # Maybe remove the stack of generators as well?
+                    # del self.generators[username][operation]
+                    return None
+
+                # Update the current generator, requests, and replies.
+                current_generator, requests, finished_requests, replies, has_reply = opStack[-1]
+                # Append the generator's result to the list of replies.
+                replies.append(ex.result)
+                if not has_reply:
+                    has_reply = True
+                    # Update the stack.
+                    opStack[-1] = current_generator, requests, finished_requests, replies, has_reply
+            except:
+                raise
 
     def execute_rule(self, username):
         user_root, =    yield [("RD", [self.root, username])]
@@ -176,34 +289,19 @@ class ModelverseKernel(object):
         parameters["mvk"] = self
 
         # Have the JIT compile the function.
-        try:
-            jit_gen = self.jit_compile(user_root, inst)
-            inp = None
-            while 1:
-                inp = yield jit_gen.send(inp)
-        except primitive_functions.PrimitiveFinished as e:
-            compiled_func = e.result
-
+        compiled_func, = yield [("RUN", self.jit_compile(user_root, inst))]
         # Run the compiled function.
-        try:
-            prim = compiled_func(**parameters)
-            inp = None
-            while 1:
-                inp = yield prim.send(inp)
-        except StopIteration:
-            # Execution has ended without return value, so we have no idea what to do
-            raise Exception("%s: primitive finished without returning a value!" % (self.debug_info[username]))
-        except primitive_functions.PrimitiveFinished as e:
-            # Execution has ended with a returnvalue, so read it out from the exception being thrown
-            result = e.result
-
-            #if result is None:
-            #    raise Exception("Primitive raised exception: value of None for operation %s with parameters %s" % (compiled_func, str(parameters)))
+        results = yield [("RUN", compiled_func, parameters)]
+        if results is None:
+            raise Exception(
+                "%s: primitive finished without returning a value!" % (self.debug_info[username]))
+        else:
+            result, = results
 
         # Clean up the current stack, as if a return happened
         old_frame, exception_return = yield [
             ("RD", [user_frame, "prev"]),
-            ("RD", [user_frame, primitive_functions.PRIMITIVE_RETURN_KEY])]
+            ("RD", [user_frame, primitive_functions.EXCEPTION_RETURN_KEY])]
 
         if self.debug_info[self.username]:
             self.debug_info[self.username].pop()
@@ -211,10 +309,10 @@ class ModelverseKernel(object):
         if exception_return is not None:
             # The caller has requested that we throw an exception instead of injecting
             # the return value into the caller's frame. Read the comment at
-            # primitive_functions.PRIMITIVE_RETURN_KEY for the rationale behind this design.
+            # primitive_functions.EXCEPTION_RETURN_KEY for the rationale behind this design.
             yield [("CD", [user_root, "frame", old_frame]),
                    ("DN", [user_frame])]
-            raise primitive_functions.PrimitiveFinished(result)
+            raise primitive_functions.InterpretedFunctionFinished(result)
         else:
             lnk, =          yield [("RDE", [old_frame, "returnvalue"])]
             _, _, _, _ =    yield [("CD", [old_frame, "returnvalue", result]),
@@ -614,10 +712,10 @@ class ModelverseKernel(object):
 
         if value is None:
             prev_frame, =   yield [("RD", [user_frame, "prev"])]
-            # If the callee's frame is marked with the '__primitive_return' key, then
+            # If the callee's frame is marked with the '__exception_return' key, then
             # we need to throw an exception instead of just finishing here. This design
             # gives us O(1) state reads per jit-interpreter transition.
-            exception_return, = yield [("RD", [user_frame, primitive_functions.PRIMITIVE_RETURN_KEY])]
+            exception_return, = yield [("RD", [user_frame, primitive_functions.EXCEPTION_RETURN_KEY])]
             if prev_frame is None:
                 _, =            yield [("DN", [user_root])]
                 del self.debug_info[self.username]
@@ -629,7 +727,7 @@ class ModelverseKernel(object):
                                       ]
 
             if exception_return is not None:
-                raise primitive_functions.PrimitiveFinished(None)
+                raise primitive_functions.InterpretedFunctionFinished(None)
         else:
             evalstack, evalstack_link, ip_link, new_evalstack, evalstack_phase = \
                             yield [("RD", [user_frame, "evalstack"]),
@@ -655,17 +753,17 @@ class ModelverseKernel(object):
         user_frame, = yield [("RD", [user_root, "frame"])]
         prev_frame, exception_return, returnvalue = yield [
             ("RD", [user_frame, "prev"]),
-            ("RD", [user_frame, primitive_functions.PRIMITIVE_RETURN_KEY]),
+            ("RD", [user_frame, primitive_functions.EXCEPTION_RETURN_KEY]),
             ("RD", [user_frame, "returnvalue"])]
 
-        # If the callee's frame is marked with the '__primitive_return' key, then
+        # If the callee's frame is marked with the '__exception_return' key, then
         # we need to throw an exception instead of just finishing here. This design
         # gives us O(1) state reads per jit-interpreter transition.
         if exception_return is not None:
             yield [
                 ("CD", [user_root, "frame", prev_frame]),
                 ("DN", [user_frame])]
-            raise primitive_functions.PrimitiveFinished(returnvalue)
+            raise primitive_functions.InterpretedFunctionFinished(returnvalue)
         else:
             old_returnvalue_link, = yield [("RDE", [prev_frame, "returnvalue"])]
             yield [

+ 18 - 9
kernel/modelverse_kernel/primitives.py

@@ -1,25 +1,34 @@
-# Exception to indicate the result value of the primitive, as a return cannot be used
+
 class PrimitiveFinished(Exception):
+    """Exception to indicate the result value of a primitive, as a return cannot be used."""
+    def __init__(self, value):
+        Exception.__init__(self)
+        self.result = value
+
+class InterpretedFunctionFinished(Exception):
+    """Exception to indicate the result value of an interpreted function, as a return
+       cannot be used."""
     def __init__(self, value):
+        Exception.__init__(self)
         self.result = value
 
-# Functions annotated with __primitive_return use the JIT's calling convention instead of
+# Functions annotated with __exception_return use the JIT's calling convention instead of
 # the kernel's: returns are handled by throwing a PrimitiveFinished exception; the caller's
 # returnvalue is not modified.
 #
-# ### Rationale for __primitive_return
+# ### Rationale for __exception_return
 #
-# __primitive_return is a useful mechanism because it allows us to have an __interpret_function
+# __exception_return is a useful mechanism because it allows us to have an __call_function
 # implementation that has O(1) state read overhead. A previous implementation of
-# __interpret_function checked if the caller's frame had been popped whenever
+# __call_function checked if the caller's frame had been popped whenever
 # ModelverseKernel.execute_yield threw a StopIteration exception. However, that incurs O(n) overhead
 # _per call,_ where n is the number of StopIteration exceptions that are thrown during the call.
-# O(n) is pretty bad, but this actually becomes O(n * m) when m calls to __interpret_function are
+# O(n) is pretty bad, but this actually becomes O(n * m) when m calls to __call_function are
 # nested. And that's just not acceptable.
-# __primitive_return requires kernel support, but I think the complexity gains are well worth it;
+# __exception_return requires kernel support, but I think the complexity gains are well worth it;
 # I reckon JIT-to-interpreter switches aren't going to get a whole lot cheaper than this.
-PRIMITIVE_RETURN_KEY = "__primitive_return"
-"""A dictionary key for functions which request that the kernel throw a PrimitiveFinished
+EXCEPTION_RETURN_KEY = "__exception_return"
+"""A dictionary key for functions which request that the kernel throw a InterpretedFunctionFinished
    exception with the return value instead of injecting the return value in the caller's frame."""
 
 def integer_subtraction(a, b, **remainder):