Browse Source

Re-write parts of the kernel to use CALL for calls, TRY for exceptions

jonathanvdc 8 years ago
parent
commit
fb0351d141

+ 92 - 202
kernel/modelverse_jit/jit.py

@@ -52,23 +52,13 @@ def map_and_simplify_generator(function, instruction):
     # First handle the children by mapping on them and then simplifying them.
     new_children = []
     for inst in instruction.get_children():
-        try:
-            gen = map_and_simplify_generator(function, inst)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            new_children.append(ex.result)
+        new_inst, = yield [("CALL_ARGS", [map_and_simplify_generator, (function, inst)])]
+        new_children.append(new_inst)
 
     # Then apply the function to the top-level node.
-    try:
-        gen = function(instruction.create(new_children))
-        inp = None
-        while True:
-            inp = yield gen.send(inp)
-    except primitive_functions.PrimitiveFinished as ex:
-        # Finally, simplify the transformed top-level node.
-        raise primitive_functions.PrimitiveFinished(ex.result.simplify_node())
+    transformed, = yield [("CALL_ARGS", [function, (instruction.create(new_children),)])]
+    # Finally, simplify the transformed top-level node.
+    raise primitive_functions.PrimitiveFinished(transformed.simplify_node())
 
 def expand_constant_read(instruction):
     """Tries to replace a read of a constant node by a literal."""
@@ -253,36 +243,33 @@ class ModelverseJit(object):
         self.jitted_entry_points[body_id] = function_name
         self.jit_globals[function_name] = None
 
-        try:
-            gen = self.jit_parameters(body_id)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            parameter_ids, parameter_list = ex.result
+        (parameter_ids, parameter_list), = yield [("CALL_ARGS", [self.jit_parameters, (body_id,)])]
 
         param_dict = dict(zip(parameter_ids, parameter_list))
         body_param_dict = dict(zip(parameter_ids, [p + "_ptr" for p in parameter_list]))
         dependencies = set([body_id])
         self.compilation_dependencies[body_id] = dependencies
-        try:
-            gen = AnalysisState(
-                self, body_id, user_root, body_param_dict,
-                self.max_instructions).analyze(body_id)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            del self.compilation_dependencies[body_id]
-            constructed_body = ex.result
-        except JitCompilationFailedException as ex:
+
+        def handle_jit_exception(exception):
+            # If analysis fails, then a JitCompilationFailedException will be thrown.
             del self.compilation_dependencies[body_id]
             for dep in dependencies:
                 self.mark_no_jit(dep)
                 if dep in self.jitted_entry_points:
                     del self.jitted_entry_points[dep]
+
             raise JitCompilationFailedException(
-                "%s (function '%s' at %d)" % (ex.message, function_name, body_id))
+                "%s (function '%s' at %d)" % (exception.message, function_name, body_id))
+
+        # Try to analyze the function's body.
+        yield [("TRY", [])]
+        yield [("CATCH", [JitCompilationFailedException, handle_jit_exception])]
+        state = AnalysisState(
+            self, body_id, user_root, body_param_dict,
+            self.max_instructions)
+        constructed_body, = yield [("CALL_ARGS", [state.analyze, (body_id,)])]
+        yield [("END_TRY", [])]
+        del self.compilation_dependencies[body_id]
 
         # Write a prologue and prepend it to the generated function body.
         prologue_statements = []
@@ -300,13 +287,8 @@ class ModelverseJit(object):
         constructed_body = tree_ir.create_block(
             *(prologue_statements + [constructed_body]))
 
-        try:
-            gen = optimize_tree_ir(constructed_body)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            constructed_body = ex.result
+        # Optimize the function's body.
+        constructed_body, = yield [("CALL_ARGS", [optimize_tree_ir, (constructed_body,)])]
 
         # Wrap the IR in a function definition, give it a unique name.
         constructed_function = tree_ir.DefineFunctionInstruction(
@@ -391,37 +373,19 @@ class AnalysisState(object):
                 if debug_info is not None:
                     debug_info, = yield [("RV", [debug_info])]
 
-            try:
-                gen = self.instruction_analyzers[instruction_val](self, instruction_id)
-                inp = None
-                while True:
-                    inp = yield gen.send(inp)
-            except StopIteration:
-                raise Exception(
-                    "Instruction analyzer (for '%s') finished without returning a value!" %
-                    (instruction_val))
-            except primitive_functions.PrimitiveFinished as outer_e:
-                # Check if the instruction has a 'next' instruction.
-                next_instr, = yield [("RD", [instruction_id, "next"])]
-                if self.jit.tracing_enabled:
-                    outer_result = tree_ir.with_debug_info_trace(
-                        outer_e.result, debug_info, self.function_name)
-                else:
-                    outer_result = outer_e.result
-
-                if next_instr is None:
-                    raise primitive_functions.PrimitiveFinished(outer_result)
-                else:
-                    gen = self.analyze(next_instr)
-                    try:
-                        inp = None
-                        while True:
-                            inp = yield gen.send(inp)
-                    except primitive_functions.PrimitiveFinished as inner_e:
-                        raise primitive_functions.PrimitiveFinished(
-                            tree_ir.CompoundInstruction(
-                                outer_result,
-                                inner_e.result))
+            # Analyze the instruction itself.
+            outer_result, = yield [
+                ("CALL_ARGS", [self.instruction_analyzers[instruction_val], (self, instruction_id)])]
+            # Check if the instruction has a 'next' instruction.
+            next_instr, = yield [("RD", [instruction_id, "next"])]
+            if next_instr is None:
+                raise primitive_functions.PrimitiveFinished(outer_result)
+            else:
+                next_result, = yield [("CALL_ARGS", [self.analyze, (next_instr,)])]
+                raise primitive_functions.PrimitiveFinished(
+                    tree_ir.CompoundInstruction(
+                        outer_result,
+                        next_result))
         else:
             raise JitCompilationFailedException(
                 "Unknown instruction type: '%s'" % (instruction_val))
@@ -430,13 +394,8 @@ class AnalysisState(object):
         """Tries to compile a list of IR trees from the given list of instruction ids."""
         results = []
         for inst in instruction_ids:
-            try:
-                gen = self.analyze(inst)
-                inp = None
-                while True:
-                    inp = yield gen.send(inp)
-            except primitive_functions.PrimitiveFinished as ex:
-                results.append(ex.result)
+            analyzed_inst, = yield [("CALL_ARGS", [self.analyze, (inst,)])]
+            results.append(analyzed_inst)
 
         raise primitive_functions.PrimitiveFinished(results)
 
@@ -448,14 +407,9 @@ class AnalysisState(object):
                 tree_ir.ReturnInstruction(
                     tree_ir.EmptyInstruction()))
         else:
-            try:
-                gen = self.analyze(retval_id)
-                inp = None
-                while True:
-                    inp = yield gen.send(inp)
-            except primitive_functions.PrimitiveFinished as ex:
-                raise primitive_functions.PrimitiveFinished(
-                    tree_ir.ReturnInstruction(ex.result))
+            retval, = yield [("CALL_ARGS", [self.analyze, (retval_id,)])]
+            raise primitive_functions.PrimitiveFinished(
+                tree_ir.ReturnInstruction(retval))
 
     def analyze_if(self, instruction_id):
         """Tries to analyze the given 'if' instruction."""
@@ -464,25 +418,22 @@ class AnalysisState(object):
             ("RD", [instruction_id, "then"]),
             ("RD", [instruction_id, "else"])]
 
-        try:
-            gen = self.analyze_all(
-                [cond, true]
-                if false is None
-                else [cond, true, false])
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            if false is None:
-                cond_r, true_r = ex.result
-                false_r = tree_ir.EmptyInstruction()
-            else:
-                cond_r, true_r, false_r = ex.result
-            raise primitive_functions.PrimitiveFinished(
-                tree_ir.SelectInstruction(
-                    tree_ir.ReadValueInstruction(cond_r),
-                    true_r,
-                    false_r))
+        analysis_results, = yield [("CALL_ARGS", [self.analyze_all, (
+            [cond, true]
+            if false is None
+            else [cond, true, false],)])]
+
+        if false is None:
+            cond_r, true_r = analysis_results
+            false_r = tree_ir.EmptyInstruction()
+        else:
+            cond_r, true_r, false_r = analysis_results
+
+        raise primitive_functions.PrimitiveFinished(
+            tree_ir.SelectInstruction(
+                tree_ir.ReadValueInstruction(cond_r),
+                true_r,
+                false_r))
 
     def analyze_while(self, instruction_id):
         """Tries to analyze the given 'while' instruction."""
@@ -490,21 +441,15 @@ class AnalysisState(object):
             ("RD", [instruction_id, "cond"]),
             ("RD", [instruction_id, "body"])]
 
-        try:
-            gen = self.analyze_all([cond, body])
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            cond_r, body_r = ex.result
-            raise primitive_functions.PrimitiveFinished(
-                tree_ir.LoopInstruction(
-                    tree_ir.CompoundInstruction(
-                        tree_ir.SelectInstruction(
-                            tree_ir.ReadValueInstruction(cond_r),
-                            tree_ir.EmptyInstruction(),
-                            tree_ir.BreakInstruction()),
-                        body_r)))
+        (cond_r, body_r), = yield [("CALL_ARGS", [self.analyze_all, ([cond, body],)])]
+        raise primitive_functions.PrimitiveFinished(
+            tree_ir.LoopInstruction(
+                tree_ir.CompoundInstruction(
+                    tree_ir.SelectInstruction(
+                        tree_ir.ReadValueInstruction(cond_r),
+                        tree_ir.EmptyInstruction(),
+                        tree_ir.BreakInstruction()),
+                    body_r)))
 
     def analyze_constant(self, instruction_id):
         """Tries to analyze the given 'constant' (literal) instruction."""
@@ -531,13 +476,8 @@ class AnalysisState(object):
         # yield None
 
         value_id, = yield [("RD", [instruction_id, "value"])]
-        try:
-            gen = self.analyze(value_id)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            value_local = tree_ir.StoreLocalInstruction('value', ex.result)
+        value_val, = yield [("CALL_ARGS", [self.analyze, (value_id,)])]
+        value_local = tree_ir.StoreLocalInstruction('value', value_val)
 
         store_user_root = self.retrieve_user_root()
         last_output = tree_ir.StoreLocalInstruction(
@@ -785,13 +725,7 @@ class AnalysisState(object):
         var_id, value_id = yield [("RD", [instruction_id, "var"]),
                                   ("RD", [instruction_id, "value"])]
 
-        try:
-            gen = self.analyze_all([var_id, value_id])
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            var_r, value_r = ex.result
+        (var_r, value_r), = yield [("CALL_ARGS", [self.analyze_all, ([var_id, value_id],)])]
 
         # Assignments work like this:
         #
@@ -822,14 +756,7 @@ class AnalysisState(object):
     def analyze_access(self, instruction_id):
         """Tries to analyze the given 'access' instruction."""
         var_id, = yield [("RD", [instruction_id, "var"])]
-
-        try:
-            gen = self.analyze(var_id)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            var_r = ex.result
+        var_r, = yield [("CALL_ARGS", [self.analyze, (var_id,)])]
 
         # Accessing a variable is pretty easy. It really just boils
         # down to reading the value corresponding to the 'value' key
@@ -859,13 +786,7 @@ class AnalysisState(object):
             compiled_func = self.jit.lookup_compiled_function(callee_name)
             if compiled_func is None:
                 # Compile the callee.
-                try:
-                    gen = self.jit.jit_compile(self.user_root, body_id, callee_name)
-                    inp = None
-                    while True:
-                        inp = yield gen.send(inp)
-                except primitive_functions.PrimitiveFinished as ex:
-                    pass
+                yield [("CALL_ARGS", [self.jit.jit_compile, (self.user_root, body_id, callee_name)])]
             else:
                 self.jit.register_compiled(body_id, compiled_func, callee_name)
 
@@ -880,13 +801,7 @@ class AnalysisState(object):
             intrinsic = self.jit.get_intrinsic(compiled_func_name)
 
         # Analyze the argument dictionary.
-        try:
-            gen = self.analyze_arguments(first_parameter_id)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            named_args = ex.result
+        named_args, = yield [("CALL_ARGS", [self.analyze_arguments, (first_parameter_id,)])]
 
         if intrinsic is not None:
             raise primitive_functions.PrimitiveFinished(
@@ -907,13 +822,8 @@ class AnalysisState(object):
             param_name_id, = yield [("RD", [next_param, "name"])]
             param_name, = yield [("RV", [param_name_id])]
             param_val_id, = yield [("RD", [next_param, "value"])]
-            try:
-                gen = self.analyze(param_val_id)
-                inp = None
-                while True:
-                    inp = yield gen.send(inp)
-            except primitive_functions.PrimitiveFinished as ex:
-                named_args.append((param_name, ex.result))
+            param_val, = yield [("CALL_ARGS", [self.analyze, (param_val_id,)])]
+            named_args.append((param_name, param_val))
 
             next_param, = yield [("RD", [next_param, "next_param"])]
 
@@ -923,21 +833,8 @@ class AnalysisState(object):
         """Analyzes a call to an unknown function."""
 
         # First off, let's analyze the callee and the argument list.
-        try:
-            gen = self.analyze(func_id)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            func_val = ex.result
-
-        try:
-            gen = self.analyze_arguments(first_arg_id)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        except primitive_functions.PrimitiveFinished as ex:
-            named_args = ex.result
+        func_val, = yield [("CALL_ARGS", [self.analyze, (func_id,)])]
+        named_args, = yield [("CALL_ARGS", [self.analyze_arguments, (first_arg_id,)])]
 
         # Call the __call_function function to run the interpreter, like so:
         #
@@ -972,20 +869,15 @@ class AnalysisState(object):
                 global_val, = yield [("RD", [global_var, "value"])]
 
                 if global_val is not None:
-                    gen = self.analyze_direct_call(
-                        global_val, resolved_var_name, first_param_id)
-                    inp = None
-                    while True:
-                        inp = yield gen.send(inp)
-                    # PrimitiveFinished exception will bubble up from here.
+                    result, = yield [("CALL_ARGS", [self.analyze_direct_call, (
+                        global_val, resolved_var_name, first_param_id)])]
+                    raise primitive_functions.PrimitiveFinished(result)
         elif func_instruction_op['value'] == 'constant':
             # 'const(func_id)' instructions are also translated to direct calls.
             function_val_id, = yield [("RD", [func_id, "node"])]
-            gen = self.analyze_direct_call(
-                function_val_id, None, first_param_id)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
+            result, = yield [("CALL_ARGS", [self.analyze_direct_call, (
+                function_val_id, None, first_param_id)])]
+            raise primitive_functions.PrimitiveFinished(result)
 
         raise JitCompilationFailedException(
             "Cannot JIT function calls that target an unknown value as direct calls.")
@@ -995,20 +887,18 @@ class AnalysisState(object):
         func_id, first_param_id, = yield [("RD", [instruction_id, "func"]),
                                           ("RD", [instruction_id, "params"])]
 
-        try:
-            # Try to analyze the call as a direct call.
-            gen = self.try_analyze_direct_call(func_id, first_param_id)
-            inp = None
-            while 1:
-                inp = yield gen.send(inp)
-            # PrimitiveFinished exception will bubble up from here.
-        except JitCompilationFailedException:
+        def handle_exception(exception):
             # Looks like we'll have to compile it as an indirect call.
             gen = self.analyze_indirect_call(func_id, first_param_id)
-            inp = None
-            while True:
-                inp = yield gen.send(inp)
-        # PrimitiveFinished exception will bubble up from here.
+            result, = yield [("CALL", [gen])]
+            raise primitive_functions.PrimitiveFinished(result)
+
+        # Try to analyze the call as a direct call.
+        yield [("TRY", [])]
+        yield [("CATCH", [JitCompilationFailedException, handle_exception])]
+        result, = yield [("CALL_ARGS", [self.try_analyze_direct_call, (func_id, first_param_id)])]
+        yield [("END_TRY", [])]
+        raise primitive_functions.PrimitiveFinished(result)
 
     instruction_analyzers = {
         'if' : analyze_if,

+ 12 - 7
kernel/modelverse_jit/runtime.py

@@ -20,11 +20,11 @@ def call_function(function_id, named_arguments, **kwargs):
     # frame.
     try:
         # Try to compile.
-        compiled_func, = yield [("RUN", [kernel.jit_compile(user_root, body_id)])]
+        compiled_func, = yield [("CALL_ARGS", [kernel.jit_compile, (user_root, body_id)])]
         # Add the keyword arguments to the argument dictionary.
         named_arguments.update(kwargs)
         # Run the function.
-        result, = yield [("RUN", [compiled_func, named_arguments])]
+        result, = yield [("CALL_KWARGS", [compiled_func, named_arguments])]
         # Return.
         raise primitive_functions.PrimitiveFinished(result)
     except JitCompilationFailedException:
@@ -78,9 +78,14 @@ def call_function(function_id, named_arguments, **kwargs):
         yield [("CE", [symbol_edge, param_var])]
 
     username = kwargs['username']
-    try:
-        while 1:
-            result, = yield [("RUN", [kernel.execute_rule(username)])]
-            yield result
-    except primitive_functions.InterpretedFunctionFinished as ex:
+    def exception_handler(ex):
+        print('Returning from interpreted function. Result: %s' % ex.result)
         raise primitive_functions.PrimitiveFinished(ex.result)
+
+    # Create an exception handler to catch and translate InterpretedFunctionFinished.
+    yield [("TRY", [])]
+    yield [("CATCH", [primitive_functions.InterpretedFunctionFinished, exception_handler])]
+    while 1:
+        result, = yield [("CALL_ARGS", [kernel.execute_rule, (username,)])]
+        # An instruction has completed. Forward it.
+        yield result

+ 3 - 3
kernel/modelverse_jit/tree_ir.py

@@ -192,7 +192,7 @@ class PythonGenerator(object):
             ', '.join([arg_i.generate_python_use(self) for arg_i in args]))
         self.state_definitions.append((result_name, request_tuple))
         self.state_definition_names.add(result_name)
-        if not self.combine_state_definitions or opcode == "RUN":
+        if not self.combine_state_definitions or opcode == "CALL_KWARGS":
             self.flush_state_definitions()
 
     def flush_state_definitions(self):
@@ -685,7 +685,7 @@ class RunGeneratorFunctionInstruction(StateInstruction):
 
     def get_opcode(self):
         """Gets the opcode for this state instruction."""
-        return "RUN"
+        return "CALL_KWARGS"
 
     def get_arguments(self):
         """Gets this state instruction's argument list."""
@@ -1159,7 +1159,7 @@ def create_jit_call(target, named_arguments, kwargs):
     # target = ...
     # arg_dict = { ... }
     # arg_dict.update(kwargs)
-    # result, = yield [("RUN", [target, arg_dict])]
+    # result, = yield [("CALL_KWARGS", [target, arg_dict])]
 
     results = []
     if target.has_definition():

+ 17 - 40
kernel/modelverse_kernel/compiled.py

@@ -326,16 +326,10 @@ def selectPossibleIncoming(a, b, c, **remainder):
     name_values =   yield [("RV", [i]) for i in limit_set_names]
     limit_set =     yield [("RD", [model_dict, i]) for i in name_values]
 
-    try:
-        gen = get_superclasses(a, b)
-        inp = None
-        while 1:
-            inp =   yield gen.send(inp)
-    except PrimitiveFinished as e:
-        superclasses = e.result
-        vals, = yield [("RO", [superclasses])]
-        superclasses = yield [("RE", [i]) for i in vals]
-        superclasses = [i[1] for i in superclasses]
+    superclasses, = yield [("CALL_ARGS", [get_superclasses, (a, b)])]
+    vals, = yield [("RO", [superclasses])]
+    superclasses = yield [("RE", [i]) for i in vals]
+    superclasses = [i[1] for i in superclasses]
 
     superclass_names = yield [("RV", [i]) for i in superclasses]
     elems =         yield [("RD", [model_dict, i]) for i in superclass_names]
@@ -359,16 +353,10 @@ def selectPossibleOutgoing(a, b, c, **remainder):
     name_values =   yield [("RV", [i]) for i in limit_set_names]
     limit_set =     yield [("RD", [model_dict, i]) for i in name_values]
 
-    try:
-        gen = get_superclasses(a, b)
-        inp = None
-        while 1:
-            inp =  yield gen.send(inp)
-    except PrimitiveFinished as e:
-        superclasses = e.result
-        vals, = yield [("RO", [superclasses])]
-        superclasses = yield [("RE", [i]) for i in vals]
-        superclasses = [i[1] for i in superclasses]
+    superclasses, = yield [("CALL_ARGS", [get_superclasses, (a, b)])]
+    vals, = yield [("RO", [superclasses])]
+    superclasses = yield [("RE", [i]) for i in vals]
+    superclasses = [i[1] for i in superclasses]
 
     superclass_names = yield [("RV", [i]) for i in superclasses]
     elems =         yield [("RD", [model_dict, i]) for i in superclass_names]
@@ -423,13 +411,7 @@ def construct_const(**remainder):
     v, = yield [("CNV", [{"value": "constant"}])]
 
     # Get input: keep trying until we get something
-    try:
-        gen = __get_input(remainder)
-        inp = None
-        while 1:
-            inp = yield gen.send(inp)
-    except PrimitiveFinished as e:
-        inp = e.result
+    inp, = yield [("CALL_ARGS", [__get_input, (remainder,)])]
 
     yield [("CD", [v, "node", inp])]
 
@@ -461,16 +443,11 @@ def __get_input(parameters):
     mvk = parameters["mvk"]
     user_root = parameters["user_root"]
     while 1:
-        try:
-            gen = mvk.input_init(user_root)
-            inp = None
-            while 1:
-                inp = yield gen.send(inp)
-        except StopIteration:
-            # Finished
-            if mvk.success:
-                # Got some input, so we can access it
-                raise PrimitiveFinished(mvk.input_value)
-            else:
-                # No input, so yield None but don't stop
-                yield None
+        yield [("CALL_ARGS", [mvk.input_init, (user_root,)])]
+        # Finished
+        if mvk.success:
+            # Got some input, so we can access it
+            raise PrimitiveFinished(mvk.input_value)
+        else:
+            # No input, so yield None but don't stop
+            yield None

+ 11 - 11
kernel/modelverse_kernel/main.py

@@ -110,17 +110,17 @@ class ModelverseKernel(object):
             else:
                 raise Exception("%s: error understanding command (%s, %s)" % (self.debug_info[username], inst_v, self.phase_v))
 
-        try:
-            inp = None
-            while 1:
-                inp = yield gen.send(inp)
-        except StopIteration:
-            pass
-        except jit.JitCompilationFailedException as e:
+        def handle_jit_failed(exception):
             # Try again, but this time without the JIT.
-            # print(e.message)
+            # print(exception.message)
             gen = self.get_inst_phase_generator(inst_v, self.phase_v, user_root)
-            yield [("RUN", [gen])]
+            yield [("CALL", [gen])]
+            raise primitive_functions.PrimitiveFinished(None)
+
+        yield [("TRY", [])]
+        yield [("CATCH", [jit.JitCompilationFailedException, handle_jit_failed])]
+        yield [("CALL", [gen])]
+        yield [("END_TRY", [])]
 
     def get_inst_phase_generator(self, inst_v, phase_v, user_root):
         """Gets a generator for the given instruction in the given phase,
@@ -173,9 +173,9 @@ class ModelverseKernel(object):
         parameters["mvk"] = self
 
         # Have the JIT compile the function.
-        compiled_func, = yield [("RUN", [self.jit_compile(user_root, inst)])]
+        compiled_func, = yield [("CALL_ARGS", [self.jit_compile, (user_root, inst)])]
         # Run the compiled function.
-        results = yield [("RUN", [compiled_func, parameters])]
+        results = yield [("CALL_KWARGS", [compiled_func, parameters])]
         if results is None:
             raise Exception(
                 "%s: primitive finished without returning a value!" % (self.debug_info[username]))

+ 192 - 36
kernel/modelverse_kernel/request_handler.py

@@ -11,11 +11,20 @@ class RequestHandler(object):
         # generator_stack is a stack of (generator, pending requests, request replies, has-reply)
         # tuples.
         self.generator_stack = []
-        # exception_handlers is a stack of (generator_stack index, exception handler function)
+        # exception_handlers is a stack of
+        # (generator_stack index, [(exception type, handler function)])
         # tuples.
         self.exception_handlers = []
         self.handlers = {
-            'RUN' : self.execute_run
+            '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
         }
 
     def is_active(self):
@@ -30,6 +39,8 @@ class RequestHandler(object):
         # 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:
@@ -44,7 +55,7 @@ class RequestHandler(object):
                 self.step()
             except StopIteration:
                 # Done, so remove the generator
-                self.generator_stack.pop()
+                self.pop_generator()
                 if self.is_active():
                     # This generator was called from another generator.
                     # Append 'None' to the caller's list of replies.
@@ -54,7 +65,7 @@ class RequestHandler(object):
                     return None
             except primitive_functions.PrimitiveFinished as ex:
                 # Done, so remove the generator
-                self.generator_stack.pop()
+                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.
@@ -62,8 +73,10 @@ class RequestHandler(object):
                 else:
                     # Looks like we're done here.
                     return None
-            except:
-                raise
+            except Exception as ex:
+                # Maybe get an exception handler to do this.
+                if not self.handle_exception(ex):
+                    raise
 
     def set_finished_requests_flag(self):
         """Sets the finished_requests flag in the top-of-stack tuple."""
@@ -78,6 +91,27 @@ class RequestHandler(object):
     def push_generator(self, gen):
         """Pushes a new generator onto the stack."""
         self.generator_stack.append((gen, None, True, [], False))
+        # 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.
+        gen = 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."""
@@ -106,6 +140,42 @@ class RequestHandler(object):
         # Update the entry on the stack.
         self.generator_stack[-1] = (current_generator, requests, False, [], False)
 
+    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)))
+        if len(self.exception_handlers) == 0:
+            # Yep, we're hosed. Make sure the caller knows this.
+            # Also, clean up.
+            self.generator_stack = []
+            self.exception_handlers = []
+            return False
+
+        # 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 None:
+            # We couldn't find an applicable handler. All is lost.
+            self.generator_stack = []
+            self.exception_handlers = []
+            return False
+
+        # 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:]
+        self.generator_stack.append(
+            (None, [('TAIL_CALL_ARGS', [applicable_handler, (exception,)])], False, [], False))
+        return True
+
     def pop_requests(self):
         """Tries to pop a batch of Modelverse _state_ requests from the
            current list of requests. Known requests are executed immediately.
@@ -138,38 +208,124 @@ class RequestHandler(object):
                 # Handle the request.
                 _, request_args = elem
                 self.handlers[elem[0]](request_args)
+                raise KnownRequestHandled()
 
-        # We couldn't find a RUN-request in the batch of requests, so we might as well
+        # 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_run(self, request_args):
-        """Executes a RUN-request with the given argument list."""
-        # The list of requests might be empty now. If so, then flag this
-        # batch of requests as finished.
-        if len(request_args) == 1:
-            # Format: ("RUN", [gen])
-            gen, = request_args
-            self.push_generator(gen)
-            raise KnownRequestHandled()
-        else:
-            # Format: ("RUN", [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))
-                raise KnownRequestHandled()
-            except StopIteration:
-                self.push_generator(None)
-                raise
-            except primitive_functions.PrimitiveFinished:
-                self.push_generator(None)
-                raise
+    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", [generator-returning-function])
+        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, []))
+
+    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))
+
+    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()
+