Bladeren bron

Merge branch 'jit' of msdl.uantwerpen.be:jonathanvdc/modelverse into yentl

Yentl Van Tendeloo 8 jaren geleden
bovenliggende
commit
0a411eb9c7

+ 40 - 15
hybrid_server/classes/mvkcontroller.xml

@@ -12,24 +12,49 @@
             self.root = self.mvs.read_root()[0]
 
             # Instantiate the kernel.
-            default_kernel_type = 'baseline-jit'
-            kernel_type = default_kernel_type
+            self.mvk = ModelverseKernel(self.root)
+
+            # Parse kernel-related options
+            default_kernel_type = 'adaptive-jit'
+            kernel_opts = [default_kernel_type]
             for parameter in params:
                 if parameter.startswith('--kernel='):
-                    kernel_type = parameter[len('--kernel='):]
+                    kernel_opts = parameter[len('--kernel='):].split(',')
+
+            for opt in kernel_opts:
+                if opt == 'legacy-interpreter':
+                    self.mvk = LegacyModelverseKernel(self.root)
+                elif opt == 'interpreter':
+                    self.mvk.jit.set_jit_enabled(False)
+                elif opt == 'no-thunks':
+                    self.mvk.jit.enable_thunks(False)
+                elif opt == 'thunks':
+                    self.mvk.jit.enable_thunks()
+                elif opt == 'no-source-maps':
+                    self.mvk.jit.enable_source_maps(False)
+                elif opt == 'source-maps':
+                    self.mvk.jit.enable_source_maps()
+                elif opt == 'trace':
+                    self.mvk.jit.enable_tracing()
+                elif opt == 'fast-jit':
+                    self.mvk.jit.set_function_body_compiler(jit.compile_function_body_fast)
+                elif opt == 'baseline-jit':
+                    self.mvk.jit.set_function_body_compiler(jit.compile_function_body_baseline)
+                elif opt == 'adaptive-jit-favor-large-functions':
+                    self.mvk.jit.set_function_body_compiler(
+                            lambda *args: jit.compile_function_body_adaptive(
+                                *args, temperature_heuristic=jit.favor_large_functions))
+                elif opt == 'adaptive-jit-favor-small-functions':
+                    self.mvk.jit.set_function_body_compiler(
+                        lambda *args: jit.compile_function_body_adaptive(
+                            *args, temperature_heuristic=jit.favor_small_functions))
+                elif opt == 'adaptive-jit' or opt == 'adaptive-jit-favor-loops':
+                    self.mvk.jit.set_function_body_compiler(
+                        lambda *args: jit.compile_function_body_adaptive(
+                            *args, temperature_heuristic=jit.favor_loops))
+                else:
+                    print("warning: unknown kernel option '%s'." % opt)
 
-            if kernel_type == 'legacy-interpreter':
-                self.mvk = LegacyModelverseKernel(self.root)
-            elif kernel_type == 'interpreter':
-                self.mvk = ModelverseKernel(self.root)
-                self.mvk.jit.set_jit_enabled(False)
-            else:
-                if kernel_type != default_kernel_type:
-                    print(
-                        "warning: unknown kernel type '%s'. Defaulting to '%s'."
-                        % (kernel_type, default_kernel_type))
-                self.mvk = ModelverseKernel(self.root)
-                
             self.all_failed = False
             self.timeout = False
             self.init_time = time.time()

+ 1 - 0
hybrid_server/server.xml

@@ -19,6 +19,7 @@
         from modelverse_kernel.legacy import ModelverseKernel as LegacyModelverseKernel
         from modelverse_state.main import ModelverseState
         #from modelverse_state.rdf import ModelverseState
+        import modelverse_jit.jit as jit
     </top>
 
     <inport name="socket_in"/>

+ 307 - 0
kernel/modelverse_jit/bytecode_ir.py

@@ -0,0 +1,307 @@
+"""Provides data structures that represent parsed Modelverse bytecode graphs."""
+
+class Instruction(object):
+    """Represents a Modelverse bytecode instruction."""
+    def __init__(self):
+        self.next_instruction = None
+        self.debug_information = None
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction, excluding the
+           next instruction."""
+        raise NotImplementedError()
+
+    def get_reachable(self):
+        """Gets the set of all instructions that are reachable from the given instruction, including
+           this instruction."""
+        results = set()
+        stack = [self]
+        while len(stack) > 0:
+            instr = stack.pop()
+            results.add(instr)
+            next_instr = instr.next_instruction
+            if next_instr is not None and next_instr not in results:
+                stack.append(next_instr)
+            for other in instr.get_directly_reachable():
+                if other not in results:
+                    assert other is not None
+                    stack.append(other)
+
+        return results
+
+class VariableNode(object):
+    """Represents a variable node, which has an identifier and an optional name."""
+    def __init__(self, node_id, name):
+        self.node_id = node_id
+        self.name = name
+
+    def __str__(self):
+        return 'var(%d, %s)' % (self.node_id, self.name)
+
+    def __repr__(self):
+        return 'VariableNode(%r, %r)' % (self.node_id, self.name)
+
+class SelectInstruction(Instruction):
+    """Represents an 'if/else' instruction."""
+    def __init__(self, condition, if_clause, else_clause):
+        Instruction.__init__(self)
+        self.condition = condition
+        self.if_clause = if_clause
+        self.else_clause = else_clause
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        if self.else_clause is None:
+            return (self.condition, self.if_clause)
+        else:
+            return (self.condition, self.if_clause, self.else_clause)
+
+    constructor_parameters = (
+        ('cond', Instruction),
+        ('then', Instruction),
+        ('else', Instruction))
+
+    def __repr__(self):
+        return '@%r: SelectInstruction(@%r, @%r, @%r)' % (
+            id(self), id(self.condition), id(self.if_clause), id(self.else_clause))
+
+class WhileInstruction(Instruction):
+    """Represents a 'while' instruction."""
+    def __init__(self, condition, body):
+        Instruction.__init__(self)
+        self.condition = condition
+        self.body = body
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return (self.condition, self.body)
+
+    constructor_parameters = (
+        ('cond', Instruction),
+        ('body', Instruction))
+
+    def __repr__(self):
+        return '@%r: WhileInstruction(@%r, @%r)' % (
+            id(self), id(self.condition), id(self.body))
+
+class BreakInstruction(Instruction):
+    """Represents a 'break' instruction."""
+    def __init__(self, loop):
+        Instruction.__init__(self)
+        self.loop = loop
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return (self.loop,)
+
+    constructor_parameters = (('while', WhileInstruction),)
+
+    def __repr__(self):
+        return '@%r: BreakInstruction(@%r)' % (
+            id(self), id(self.loop))
+
+class ContinueInstruction(Instruction):
+    """Represents a 'continue' instruction."""
+    def __init__(self, loop):
+        Instruction.__init__(self)
+        self.loop = loop
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return (self.loop,)
+
+    constructor_parameters = (('while', WhileInstruction),)
+
+    def __repr__(self):
+        return '@%r: ContinueInstruction(@%r)' % (
+            id(self), id(self.loop))
+
+class ReturnInstruction(Instruction):
+    """Represents a 'return' instruction, which terminates the current function
+       and optionally returns a value."""
+    def __init__(self, value):
+        Instruction.__init__(self)
+        self.value = value
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        if self.value is None:
+            return ()
+        else:
+            return (self.value,)
+
+    constructor_parameters = (('value', Instruction),)
+
+    def __repr__(self):
+        return '@%r: ReturnInstruction(@%r)' % (
+            id(self), id(self.value))
+
+class CallInstruction(Instruction):
+    """Represents a 'call' instruction, which calls a function with an argument
+       list, encoded as a list of name-instruction tuples."""
+    def __init__(self, target, argument_list):
+        Instruction.__init__(self)
+        self.target = target
+        self.argument_list = argument_list
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return (self.target,) + tuple((value for _, value in self.argument_list))
+
+    def __repr__(self):
+        return '@%r: CallInstruction(@%r, [%s])' % (
+            id(self), id(self.target),
+            ', '.join(['%s=@%r' % (name, id(value)) for name, value in self.argument_list]))
+
+class ConstantInstruction(Instruction):
+    """Represents a 'constant' instruction, which produces a reference
+       to a constant node."""
+    def __init__(self, constant_id):
+        Instruction.__init__(self)
+        self.constant_id = constant_id
+        assert self.constant_id is not None
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return ()
+
+    constructor_parameters = (('node', int),)
+
+    def __repr__(self):
+        return '@%r: ConstantInstruction(%r)' % (id(self), self.constant_id)
+
+class InputInstruction(Instruction):
+    """Represents an 'input' instruction, which pops a node from the input
+       queue."""
+    def __init__(self):
+        Instruction.__init__(self)
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return ()
+
+    constructor_parameters = ()
+
+    def __repr__(self):
+        return '@%r: InputInstruction()' % id(self)
+
+class OutputInstruction(Instruction):
+    """Represents an 'output' instruction, which pushes a node onto the output
+       queue."""
+    def __init__(self, value):
+        Instruction.__init__(self)
+        self.value = value
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return (self.value,)
+
+    constructor_parameters = (('value', Instruction),)
+
+    def __repr__(self):
+        return '@%r: OutputInstruction(@%r)' % (
+            id(self), id(self.value))
+
+class DeclareInstruction(Instruction):
+    """Represents a 'declare' instruction, which declares a local variable."""
+    def __init__(self, variable):
+        Instruction.__init__(self)
+        self.variable = variable
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return ()
+
+    constructor_parameters = (('var', VariableNode),)
+
+    def __repr__(self):
+        return '@%r: DeclareInstruction(%r)' % (
+            id(self), self.variable)
+
+class GlobalInstruction(Instruction):
+    """Represents a 'global' instruction, which declares a global variable."""
+    def __init__(self, variable):
+        Instruction.__init__(self)
+        self.variable = variable
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return ()
+
+    constructor_parameters = (('var', VariableNode),)
+
+    def __repr__(self):
+        return '@%r: GlobalInstruction(%r)' % (
+            id(self), self.variable)
+
+class ResolveInstruction(Instruction):
+    """Represents a 'resolve' instruction, which resolves a variable node/name as
+       either a local or global variable."""
+    def __init__(self, variable):
+        Instruction.__init__(self)
+        self.variable = variable
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return ()
+
+    constructor_parameters = (('var', VariableNode),)
+
+    def __repr__(self):
+        return '@%r: ResolveInstruction(%r)' % (
+            id(self), self.variable)
+
+class AccessInstruction(Instruction):
+    """Represents an 'access' instruction, which loads the node pointed to by a
+       pointer node."""
+    def __init__(self, pointer):
+        Instruction.__init__(self)
+        self.pointer = pointer
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return (self.pointer,)
+
+    constructor_parameters = (('var', Instruction),)
+
+    def __repr__(self):
+        return '@%r: AccessInstruction(@%r)' % (
+            id(self), id(self.pointer))
+
+class AssignInstruction(Instruction):
+    """Represents an 'assign' instruction, which sets the node pointed to by a
+       pointer node to the given node."""
+    def __init__(self, pointer, value):
+        Instruction.__init__(self)
+        self.pointer = pointer
+        self.value = value
+
+    def get_directly_reachable(self):
+        """Gets all instructions that are directly reachable from this instruction."""
+        return (self.pointer, self.value)
+
+    constructor_parameters = (
+        ('var', Instruction),
+        ('value', Instruction))
+
+    def __repr__(self):
+        return '@%r: AssignInstruction(@%r, @%r)' % (
+            id(self), id(self.pointer), id(self.value))
+
+INSTRUCTION_TYPE_MAPPING = {
+    'if' : SelectInstruction,
+    'while' : WhileInstruction,
+    'return' : ReturnInstruction,
+    'constant' : ConstantInstruction,
+    'resolve' : ResolveInstruction,
+    'declare' : DeclareInstruction,
+    'global' : GlobalInstruction,
+    'assign' : AssignInstruction,
+    'access' : AccessInstruction,
+    'output' : OutputInstruction,
+    'input' : InputInstruction,
+    'call' : CallInstruction,
+    'break' : BreakInstruction,
+    'continue' : ContinueInstruction
+}
+"""Maps instruction names to types."""

+ 117 - 0
kernel/modelverse_jit/bytecode_parser.py

@@ -0,0 +1,117 @@
+"""Parses Modelverse bytecode graphs into bytecode ir."""
+
+import modelverse_jit.bytecode_ir as bytecode_ir
+import modelverse_jit.runtime as jit_runtime
+import modelverse_kernel.primitives as primitive_functions
+
+class BytecodeParser(object):
+    """Parses bytecode graphs."""
+    def __init__(self):
+        self.parsed_nodes = {}
+
+    def parse_instruction(self, node_id):
+        """Parses the instruction node with the given identifier."""
+        if node_id is None:
+            raise primitive_functions.PrimitiveFinished(None)
+        elif node_id in self.parsed_nodes:
+            # We've already parsed this node, so return it right away.
+            raise primitive_functions.PrimitiveFinished(self.parsed_nodes[node_id])
+
+        instruction_val, = yield [("RV", [node_id])]
+        instruction_type = instruction_val["value"]
+        # Create an instruction and store it in the instruction dictionary.
+        instruction = self.create_instruction(instruction_type)
+        self.parsed_nodes[node_id] = instruction
+        # Initialize the instruction from the node's data.
+        yield [("CALL_ARGS", [self.initialize_instruction, (instruction, node_id)])]
+        # Retrieve the debug information.
+        debug_info, = yield [("RD", [node_id, "__debug"])]
+        if debug_info is not None:
+            debug_info, = yield [("RV", [debug_info])]
+            instruction.debug_information = debug_info
+
+        # Check if the instruction has a 'next' instruction.
+        next_instr_id, = yield [("RD", [node_id, "next"])]
+        if next_instr_id is not None:
+            instruction.next_instruction, = yield [
+                ("CALL_ARGS", [self.parse_instruction, (next_instr_id,)])]
+
+        raise primitive_functions.PrimitiveFinished(instruction)
+
+    def parse_variable(self, node_id):
+        """Parses the given variable node."""
+        var_name, = yield [("RV", [node_id])]
+        raise primitive_functions.PrimitiveFinished(
+            bytecode_ir.VariableNode(node_id, var_name))
+
+    def __parse_node_unchecked(self, node_id, result_type):
+        """Parses the given node as the specified type of object, without
+           checking that the result actually conforms to said type."""
+        if result_type is bytecode_ir.VariableNode:
+            yield [("TAIL_CALL_ARGS", [self.parse_variable, (node_id,)])]
+        elif result_type is int:
+            raise primitive_functions.PrimitiveFinished(node_id)
+        else:
+            yield [("TAIL_CALL_ARGS", [self.parse_instruction, (node_id,)])]
+
+    def parse_node(self, node_id, result_type):
+        """Parses the given node as the specified type of object."""
+        result, = yield [("CALL_ARGS", [self.__parse_node_unchecked, (node_id, result_type)])]
+        if result is not None and not isinstance(result, result_type):
+            raise jit_runtime.JitCompilationFailedException(
+                "Parsed a node as an instance of '%s', expected an instance of '%s'." % (
+                    type(result).__name__, result_type.__name__))
+
+        raise primitive_functions.PrimitiveFinished(result)
+
+    def parse_arguments(self, first_argument_id):
+        """Parses the parameter-to-argument mapping started by the specified first argument
+           node."""
+        next_param = first_argument_id
+        named_args = []
+        while next_param is not None:
+            param_name_id, = yield [("RD", [next_param, "name"])]
+            param_name, = yield [("RV", [param_name_id])]
+            param_val_id, = yield [("RD", [next_param, "value"])]
+            param_val, = yield [("CALL_ARGS", [self.parse_instruction, (param_val_id,)])]
+            named_args.append((param_name, param_val))
+
+            next_param, = yield [("RD", [next_param, "next_param"])]
+
+        raise primitive_functions.PrimitiveFinished(named_args)
+
+    def create_instruction(self, instruction_type):
+        """Creates an instruction of the given type."""
+        if instruction_type in bytecode_ir.INSTRUCTION_TYPE_MAPPING:
+            return object.__new__(bytecode_ir.INSTRUCTION_TYPE_MAPPING[instruction_type])
+        else:
+            raise jit_runtime.JitCompilationFailedException(
+                "Unknown instruction type: '%s'" % instruction_type)
+
+    def initialize_call(self, call_instruction, node_id):
+        """Initializes the given call instruction."""
+        func_id, first_arg_id, = yield [
+            ("RD", [node_id, "func"]),
+            ("RD", [node_id, "params"])]
+        func_val, = yield [("CALL_ARGS", [self.parse_instruction, (func_id,)])]
+        named_args, = yield [("CALL_ARGS", [self.parse_arguments, (first_arg_id,)])]
+        call_instruction.__init__(func_val, named_args)
+        raise primitive_functions.PrimitiveFinished(None)
+
+    def initialize_instruction(self, instruction, node_id):
+        """Initializes the given instruction with data from the given node."""
+        instr_type = type(instruction)
+        # print("Initializing '%s' node" % instr_type)
+        if instr_type is bytecode_ir.CallInstruction:
+            # Call instructions are complicated, so they get special treatment.
+            yield [("TAIL_CALL_ARGS", [self.initialize_call, (instruction, node_id)])]
+        else:
+            # Construct an argument list based on the `constructor_parameters` attribute
+            # of the instruction type.
+            arg_list = []
+            for dict_key, ctor_param_ty in instr_type.constructor_parameters:
+                arg_id, = yield [("RD", [node_id, dict_key])]
+                arg, = yield [("CALL_ARGS", [self.parse_node, (arg_id, ctor_param_ty)])]
+                arg_list.append(arg)
+            instruction.__init__(*arg_list)
+            raise primitive_functions.PrimitiveFinished(None)

+ 368 - 0
kernel/modelverse_jit/bytecode_to_cfg.py

@@ -0,0 +1,368 @@
+"""Converts bytecode IR to CFG IR."""
+
+import modelverse_jit.bytecode_ir as bytecode_ir
+import modelverse_jit.cfg_ir as cfg_ir
+import modelverse_jit.runtime as jit_runtime
+
+def emit_debug_info_trace(block, debug_info, function_name):
+    """Appends a tracing instruction to the given block that prints
+       the given debug information and function name."""
+    if debug_info is not None or function_name is not None:
+        block.append_definition(
+            cfg_ir.create_print(
+                block.append_definition(
+                    cfg_ir.Literal(jit_runtime.format_trace_message(
+                        debug_info, function_name, jit_runtime.FAST_JIT_ORIGIN_NAME)))))
+
+class AnalysisState(object):
+    """State that is common to the bytecode->CFG transformation of a function."""
+    def __init__(self, jit, function_name, param_dict):
+        self.jit = jit
+        self.function_name = function_name
+        self.counter = cfg_ir.SharedCounter()
+        self.analyzed_instructions = set()
+        self.loop_instructions = {}
+        self.entry_point = cfg_ir.BasicBlock(self.counter)
+        self.current_block = self.entry_point
+        self.root_node = None
+        self.__write_prolog(param_dict)
+
+    def __write_prolog(self, param_dict):
+        # Write a prolog in CFG IR.
+        # We want to create the following definition:
+        #
+        #     !entry_point():
+        #         static if self.jit.source_maps_enabled:
+        #             $function_name = literal <function map>
+        #             $source_map_name = literal <source map name>
+        #             $origin_name = literal jit_runtime.FAST_JIT_ORIGIN_NAME
+        #             $_ = direct-call ('macro-positional', void) register_debug_info(
+        #                 function_name=$functionsource_map=$source_map_name)
+        #
+        #         $jit_locals = alloc-root-node
+        #         $_ = declare-local var(...)
+        #         $param_1 = resolve-local var(...)
+        #         $arg_1 = function-parameter ...
+        #         $_ = store $param_1, $arg_1
+        #         ...
+        #
+        #         static if self.jit.nop_insertion_enabled:
+        #             $_ = direct-call ('macro-io', void) nop()
+        #
+        # We also want to store '$jit_locals' in an attribute, so we can
+        # use it to shield locals from the GC.
+        if self.jit.source_maps_enabled:
+            function_name = self.current_block.append_definition(
+                cfg_ir.Literal(self.function_name))
+            source_map_name = self.current_block.append_definition(
+                cfg_ir.Literal(self.jit.get_source_map_name(self.function_name)))
+            origin_name = self.current_block.append_definition(
+                cfg_ir.Literal(jit_runtime.FAST_JIT_ORIGIN_NAME))
+            self.current_block.append_definition(
+                cfg_ir.DirectFunctionCall(
+                    cfg_ir.REGISTER_DEBUG_INFO_MACRO_NAME,
+                    [('function_name', function_name),
+                     ('source_map', source_map_name),
+                     ('origin_name', origin_name)],
+                    calling_convention=cfg_ir.MACRO_POSITIONAL_CALLING_CONVENTION,
+                    has_value=False))
+
+        self.root_node = self.current_block.append_definition(cfg_ir.AllocateRootNode())
+        for node_id, name in param_dict.items():
+            variable = bytecode_ir.VariableNode(node_id, name)
+            self.current_block.append_definition(cfg_ir.DeclareLocal(variable, self.root_node))
+            param_i = self.current_block.append_definition(cfg_ir.ResolveLocal(variable))
+            arg_i = self.current_block.append_definition(cfg_ir.FunctionParameter(name))
+            self.current_block.append_definition(cfg_ir.StoreAtPointer(param_i, arg_i))
+
+        if self.jit.nop_insertion_enabled:
+            self.current_block.append_definition(cfg_ir.create_nop())
+
+    def analyze(self, instruction):
+        """Analyzes the given instruction as a basic block."""
+        if instruction in self.analyzed_instructions:
+            raise jit_runtime.JitCompilationFailedException(
+                'Cannot jit non-tree instruction graph.')
+
+        self.analyzed_instructions.add(instruction)
+
+        # Find an analyzer.
+        instruction_type = type(instruction)
+        if instruction_type in self.instruction_analyzers:
+            if self.jit.tracing_enabled:
+                emit_debug_info_trace(
+                    self.current_block, instruction.debug_information, self.function_name)
+
+            # Analyze the instruction.
+            result = self.instruction_analyzers[instruction_type](self, instruction)
+
+            if self.jit.source_maps_enabled:
+                result.debug_information = instruction.debug_information
+
+            # Check if the instruction has a 'next' instruction. If so, analyze it!
+            if instruction.next_instruction is not None:
+                next_result = self.analyze(instruction.next_instruction)
+                if next_result.value.has_value() or (not result.value.has_value()):
+                    result = next_result
+
+            return result
+        else:
+            raise jit_runtime.JitCompilationFailedException(
+                "Unknown instruction type: '%s'" % type(instruction))
+
+    def emit_select(self, create_condition, create_if_body, create_else_body):
+        """Emits a 'select' instruction."""
+        # Create blocks that look like this:
+        #
+        # !current_block(...):
+        #     ...
+        #     $cond = <condition>
+        #     select $cond, !if_block(), !else_block()
+        #
+        # !if_block():
+        #     $if_result = <if-body>
+        #     jump !phi_block($if_result)
+        #
+        # !else_block():
+        #     $else_result = <else-body>
+        #     jump !phi_block($else_result)
+        #
+        # !phi_block($result = block-parameter):
+        #     ...
+        #
+        if_block = cfg_ir.BasicBlock(self.counter)
+        else_block = cfg_ir.BasicBlock(self.counter)
+        phi_block = cfg_ir.BasicBlock(self.counter)
+        param_def = phi_block.append_parameter(cfg_ir.BlockParameter())
+
+        condition = create_condition()
+        self.current_block.flow = cfg_ir.SelectFlow(
+            condition, cfg_ir.Branch(if_block), cfg_ir.Branch(else_block))
+
+        self.current_block = if_block
+        if_result = create_if_body()
+        self.current_block.flow = cfg_ir.create_jump(phi_block, [if_result])
+
+        self.current_block = else_block
+        else_result = create_else_body()
+        self.current_block.flow = cfg_ir.create_jump(phi_block, [else_result])
+
+        self.current_block = phi_block
+        return param_def
+
+    def analyze_if(self, instruction):
+        """Analyzes an 'if' instruction."""
+        def __analyze_condition():
+            condition_node = self.analyze(instruction.condition)
+            return self.current_block.append_definition(cfg_ir.Read(condition_node))
+        return self.emit_select(
+            __analyze_condition,
+            lambda: self.analyze(instruction.if_clause),
+            lambda:
+            self.current_block.append_definition(cfg_ir.Literal(None))
+            if instruction.else_clause is None
+            else self.analyze(instruction.else_clause))
+
+    def analyze_while(self, instruction):
+        """Analyzes a 'while' instruction."""
+        # Create blocks that look like this:
+        #
+        # !current_block(...):
+        #     ...
+        #     jump !loop_condition()
+        #
+        # !loop_condition():
+        #     $condition_node = <condition>
+        #     $condition = read condition_node
+        #     select $condition, !loop_body(), !loop_exit()
+        #
+        # !loop_body():
+        #     $result = <body>
+        #     static if jit.nop_insertion_enabled:
+        #         $_ = direct-call ('macro-io', void) nop()
+        #     jump !loop_condition()
+        #
+        # !loop_exit():
+        #     $nothing = literal None
+        #     ...
+        loop_condition_block = cfg_ir.BasicBlock(self.counter)
+        loop_body_block = cfg_ir.BasicBlock(self.counter)
+        loop_exit_block = cfg_ir.BasicBlock(self.counter)
+        self.loop_instructions[instruction] = (loop_condition_block, loop_exit_block)
+
+        self.current_block.flow = cfg_ir.create_jump(loop_condition_block)
+
+        self.current_block = loop_condition_block
+        condition_node = self.analyze(instruction.condition)
+        condition = self.current_block.append_definition(cfg_ir.Read(condition_node))
+        self.current_block.flow = cfg_ir.SelectFlow(
+            condition, cfg_ir.Branch(loop_body_block), cfg_ir.Branch(loop_exit_block))
+
+        self.current_block = loop_body_block
+        self.analyze(instruction.body)
+        if self.jit.nop_insertion_enabled:
+            self.current_block.append_definition(cfg_ir.create_nop())
+        self.current_block.flow = cfg_ir.create_jump(loop_condition_block)
+
+        self.current_block = loop_exit_block
+        return loop_exit_block.append_definition(cfg_ir.Literal(None))
+
+    def analyze_return(self, instruction):
+        """Analyzes a 'return' instruction."""
+        if instruction.value is None:
+            return_value = self.current_block.append_definition(cfg_ir.Literal(None))
+        else:
+            return_value = self.analyze(instruction.value)
+        # Don't forget to deallocate the root node.
+        self.current_block.append_definition(cfg_ir.DeallocateRootNode(self.root_node))
+        self.current_block.flow = cfg_ir.ReturnFlow(return_value)
+        self.current_block = cfg_ir.BasicBlock(self.counter)
+        return self.current_block.append_definition(cfg_ir.Literal(None))
+
+    def analyze_constant(self, instruction):
+        """Analyzes a 'constant' instruction."""
+        return self.current_block.append_definition(cfg_ir.Literal(instruction.constant_id))
+
+    def analyze_resolve(self, instruction):
+        """Analyzes a 'resolve' instruction."""
+        def __resolve_global_carefully():
+            # We might be resolving a global that does not exist. In that case, we
+            # need to throw. We want to create the following blocks:
+            #
+            # !current_block(...):
+            #     ...
+            #     $resolved_global = resolve-global global
+            #     $nothing = literal None
+            #     $condition = binary $resolved_global, 'is', $nothing
+            #     select $condition, !no_global_block(), !global_exists_block()
+            #
+            # !no_global_block():
+            #     $message = literal <GLOBAL_NOT_FOUND_MESSAGE_FORMAT % instruction.variable.name>
+            #     $exception = direct-call "simple-positional" Exception(message=$message)
+            #     throw $exception
+            #
+            # !global_exists_block():
+            #     ...
+            #
+            no_global_block = cfg_ir.BasicBlock(self.counter)
+            global_exists_block = cfg_ir.BasicBlock(self.counter)
+
+            resolved_global = self.current_block.append_definition(
+                cfg_ir.ResolveGlobal(instruction.variable))
+            nothing = self.current_block.append_definition(cfg_ir.Literal(None))
+            condition = self.current_block.append_definition(
+                cfg_ir.Binary(resolved_global, 'is', nothing))
+            self.current_block.flow = cfg_ir.SelectFlow(
+                condition, cfg_ir.Branch(no_global_block), cfg_ir.Branch(global_exists_block))
+
+            message = no_global_block.append_definition(
+                cfg_ir.Literal(
+                    jit_runtime.GLOBAL_NOT_FOUND_MESSAGE_FORMAT % instruction.variable.name))
+            exception = no_global_block.append_definition(
+                cfg_ir.DirectFunctionCall(
+                    'Exception',
+                    [('message', message)],
+                    cfg_ir.SIMPLE_POSITIONAL_CALLING_CONVENTION))
+            no_global_block.flow = cfg_ir.ThrowFlow(exception)
+
+            self.current_block = global_exists_block
+            return resolved_global
+        return self.emit_select(
+            lambda:
+            self.current_block.append_definition(
+                cfg_ir.CheckLocalExists(instruction.variable)),
+            lambda:
+            self.current_block.append_definition(
+                cfg_ir.ResolveLocal(instruction.variable)),
+            __resolve_global_carefully)
+
+    def analyze_declare(self, instruction):
+        """Analyzes a 'declare' instruction."""
+        return self.current_block.append_definition(
+            cfg_ir.DeclareLocal(instruction.variable, self.root_node))
+
+    def analyze_global(self, instruction):
+        """Analyzes a 'global' instruction."""
+        resolved_global = self.current_block.append_definition(
+            cfg_ir.ResolveGlobal(instruction.variable))
+        nothing = self.current_block.append_definition(cfg_ir.Literal(None))
+        return self.emit_select(
+            lambda:
+            self.current_block.append_definition(
+                cfg_ir.Binary(resolved_global, 'is', nothing)),
+            lambda:
+            self.current_block.append_definition(
+                cfg_ir.DeclareGlobal(instruction.variable)),
+            lambda: resolved_global)
+
+    def analyze_assign(self, instruction):
+        """Analyzes an 'assign' instruction."""
+        pointer_result = self.analyze(instruction.pointer)
+        value_result = self.analyze(instruction.value)
+        return self.current_block.append_definition(
+            cfg_ir.StoreAtPointer(pointer_result, value_result))
+
+    def analyze_access(self, instruction):
+        """Analyzes an 'access' instruction."""
+        pointer_result = self.analyze(instruction.pointer)
+        return self.current_block.append_definition(cfg_ir.LoadPointer(pointer_result))
+
+    def analyze_output(self, instruction):
+        """Analyzes an 'output' instruction."""
+        value_result = self.analyze(instruction.value)
+        return self.current_block.append_definition(cfg_ir.create_output(value_result))
+
+    def analyze_input(self, _):
+        """Analyzes an 'input' instruction."""
+        return self.current_block.append_definition(cfg_ir.create_input())
+
+    def analyze_break(self, instruction):
+        """Analyzes a 'break' instruction."""
+        if instruction.loop not in self.loop_instructions:
+            raise jit_runtime.JitCompilationFailedException(
+                "'break' instruction targets a 'while' loop that has not been defined yet.")
+
+        _, exit_block = self.loop_instructions[instruction.loop]
+        self.current_block.flow = cfg_ir.create_jump(exit_block)
+        self.current_block = cfg_ir.BasicBlock(self.counter)
+        return self.current_block.append_definition(cfg_ir.Literal(None))
+
+    def analyze_continue(self, instruction):
+        """Analyzes a 'continue' instruction."""
+        if instruction.loop not in self.loop_instructions:
+            raise jit_runtime.JitCompilationFailedException(
+                "'continue' instruction targets a 'while' loop that has not been defined yet.")
+
+        if self.jit.nop_insertion_enabled:
+            self.current_block.append_definition(cfg_ir.create_nop())
+        cond_block, _ = self.loop_instructions[instruction.loop]
+        self.current_block.flow = cfg_ir.create_jump(cond_block)
+        self.current_block = cfg_ir.BasicBlock(self.counter)
+        return self.current_block.append_definition(cfg_ir.Literal(None))
+
+    def analyze_call(self, instruction):
+        """Analyzes the given 'call' instruction."""
+        target = self.analyze(instruction.target)
+        arg_list = []
+        for key, arg_instruction in instruction.argument_list:
+            arg_list.append((key, self.analyze(arg_instruction)))
+
+        return self.current_block.append_definition(cfg_ir.IndirectFunctionCall(target, arg_list))
+
+    instruction_analyzers = {
+        bytecode_ir.SelectInstruction : analyze_if,
+        bytecode_ir.WhileInstruction : analyze_while,
+        bytecode_ir.ReturnInstruction : analyze_return,
+        bytecode_ir.ConstantInstruction : analyze_constant,
+        bytecode_ir.ResolveInstruction : analyze_resolve,
+        bytecode_ir.DeclareInstruction : analyze_declare,
+        bytecode_ir.GlobalInstruction : analyze_global,
+        bytecode_ir.AssignInstruction : analyze_assign,
+        bytecode_ir.AccessInstruction : analyze_access,
+        bytecode_ir.OutputInstruction : analyze_output,
+        bytecode_ir.InputInstruction : analyze_input,
+        bytecode_ir.CallInstruction : analyze_call,
+        bytecode_ir.BreakInstruction : analyze_break,
+        bytecode_ir.ContinueInstruction : analyze_continue
+    }
+

+ 720 - 0
kernel/modelverse_jit/bytecode_to_tree.py

@@ -0,0 +1,720 @@
+"""Naively converts bytecode IR to tree IR."""
+
+import modelverse_jit.bytecode_ir as bytecode_ir
+import modelverse_jit.tree_ir as tree_ir
+import modelverse_jit.runtime as jit_runtime
+import modelverse_kernel.primitives as primitive_functions
+
+def get_parameter_names(compiled_function):
+    """Gets the given compiled function's parameter names."""
+    if hasattr(compiled_function, '__code__'):
+        return compiled_function.__code__.co_varnames[
+            :compiled_function.__code__.co_argcount]
+    elif hasattr(compiled_function, '__init__'):
+        return get_parameter_names(compiled_function.__init__)[1:]
+    else:
+        raise ValueError("'compiled_function' must be a function or a type.")
+
+def apply_intrinsic(intrinsic_function, named_args):
+    """Applies the given intrinsic to the given sequence of named arguments."""
+    param_names = get_parameter_names(intrinsic_function)
+    if tuple(param_names) == tuple([n for n, _ in named_args]):
+        # Perfect match. Yay!
+        return intrinsic_function(**dict(named_args))
+    else:
+        # We'll have to store the arguments into locals to preserve
+        # the order of evaluation.
+        stored_args = [(name, tree_ir.StoreLocalInstruction(None, arg)) for name, arg in named_args]
+        arg_value_dict = dict([(name, arg.create_load()) for name, arg in stored_args])
+        store_instructions = [instruction for _, instruction in stored_args]
+        return tree_ir.CompoundInstruction(
+            tree_ir.create_block(*store_instructions),
+            intrinsic_function(**arg_value_dict))
+
+def retrieve_task_root():
+    """Creates an instruction that stores the task_root variable in a local."""
+    return tree_ir.StoreLocalInstruction(
+        'task_root', load_task_root())
+
+def load_task_root():
+    """Creates an instruction that loads the task_root variable."""
+    return tree_ir.LoadIndexInstruction(
+        tree_ir.LoadLocalInstruction(jit_runtime.KWARGS_PARAMETER_NAME),
+        tree_ir.LiteralInstruction('task_root'))
+
+def load_kernel():
+    """Creates an instruction that loads the Modelverse kernel."""
+    return tree_ir.LoadIndexInstruction(
+        tree_ir.LoadLocalInstruction(jit_runtime.KWARGS_PARAMETER_NAME),
+        tree_ir.LiteralInstruction('mvk'))
+
+def create_access(pointer):
+    """Creates a tree that loads the given pointer's value."""
+    # Accessing a variable is pretty easy. It really just boils
+    # down to reading the value corresponding to the 'value' key
+    # of the variable.
+    #
+    #     value, = yield [("RD", [returnvalue, "value"])]
+    #
+    return tree_ir.ReadDictionaryValueInstruction(
+        pointer,
+        tree_ir.LiteralInstruction('value'))
+
+def create_assign(pointer, value):
+    """Creates a tree that assigns the given value to the given pointer."""
+    # Assignments work like this:
+    #
+    #     value_link = yield [("RDE", [variable, "value"])]
+    #     _, _ =       yield [("CD", [variable, "value", value]),
+    #                         ("DE", [value_link])]
+    #
+    variable = tree_ir.StoreLocalInstruction(None, pointer)
+    value = tree_ir.StoreLocalInstruction(None, value)
+    value_link = tree_ir.StoreLocalInstruction(
+        'value_link',
+        tree_ir.ReadDictionaryEdgeInstruction(
+            variable.create_load(),
+            tree_ir.LiteralInstruction('value')))
+
+    return tree_ir.create_block(
+        variable,
+        value,
+        value_link,
+        tree_ir.CreateDictionaryEdgeInstruction(
+            variable.create_load(),
+            tree_ir.LiteralInstruction('value'),
+            value.create_load()),
+        tree_ir.DeleteEdgeInstruction(
+            value_link.create_load()))
+
+def create_input(use_input_function=False):
+    """Creates an instruction that pops a value from the input queue."""
+    # Possible alternative to the explicit syntax tree: just call the jit_runtime.__get_input
+    # function.
+    if use_input_function:
+        return tree_ir.create_jit_call(
+            tree_ir.LoadGlobalInstruction(jit_runtime.GET_INPUT_FUNCTION_NAME),
+            [],
+            tree_ir.LoadLocalInstruction(jit_runtime.KWARGS_PARAMETER_NAME))
+
+    # The plan is to generate this tree:
+    #
+    #     value = None
+    #     while True:
+    #         _input = yield [("RD", [task_root, "input"])]
+    #         value = yield [("RD", [_input, "value"])]
+    #
+    #         if value is None:
+    #             kwargs['mvk'].success = False # to avoid blocking
+    #             yield None # nop/interrupt
+    #         else:
+    #             break
+    #
+    #     _next = yield [("RD", [_input, "next"])]
+    #     yield [("CD", [task_root, "input", _next])]
+    #     yield [("CE", [jit_locals, value])]
+    #     yield [("DN", [_input])]
+
+    task_root = retrieve_task_root()
+    _input = tree_ir.StoreLocalInstruction(
+        None,
+        tree_ir.ReadDictionaryValueInstruction(
+            task_root.create_load(),
+            tree_ir.LiteralInstruction('input')))
+
+    value = tree_ir.StoreLocalInstruction(
+        None,
+        tree_ir.ReadDictionaryValueInstruction(
+            _input.create_load(),
+            tree_ir.LiteralInstruction('value')))
+
+    raise primitive_functions.PrimitiveFinished(
+        tree_ir.CompoundInstruction(
+            tree_ir.create_block(
+                task_root,
+                value.create_store(tree_ir.LiteralInstruction(None)),
+                tree_ir.LoopInstruction(
+                    tree_ir.create_block(
+                        _input,
+                        value,
+                        tree_ir.SelectInstruction(
+                            tree_ir.BinaryInstruction(
+                                value.create_load(),
+                                'is',
+                                tree_ir.LiteralInstruction(None)),
+                            tree_ir.create_block(
+                                tree_ir.StoreMemberInstruction(
+                                    load_kernel(),
+                                    'success',
+                                    tree_ir.LiteralInstruction(False)),
+                                tree_ir.NopInstruction()),
+                            tree_ir.BreakInstruction()))),
+                tree_ir.CreateDictionaryEdgeInstruction(
+                    task_root.create_load(),
+                    tree_ir.LiteralInstruction('input'),
+                    tree_ir.ReadDictionaryValueInstruction(
+                        _input.create_load(),
+                        tree_ir.LiteralInstruction('next'))),
+                tree_ir.CreateEdgeInstruction(
+                    tree_ir.LoadLocalInstruction(jit_runtime.LOCALS_NODE_NAME),
+                    value.create_load()),
+                tree_ir.DeleteNodeInstruction(_input.create_load())),
+            value.create_load()))
+
+def create_output(output_value):
+    """Creates an output instruction that outputs the given value."""
+    # The plan is to basically generate this tree:
+    #
+    # value = <some tree>
+    # last_output, last_output_link, new_last_output = \
+    #                 yield [("RD", [task_root, "last_output"]),
+    #                        ("RDE", [task_root, "last_output"]),
+    #                        ("CN", []),
+    #                       ]
+    # _, _, _, _ = \
+    #                 yield [("CD", [last_output, "value", value]),
+    #                        ("CD", [last_output, "next", new_last_output]),
+    #                        ("CD", [task_root, "last_output", new_last_output]),
+    #                        ("DE", [last_output_link])
+    #                       ]
+    # yield None
+
+    value_local = tree_ir.StoreLocalInstruction('value', output_value)
+
+    store_task_root = retrieve_task_root()
+    last_output = tree_ir.StoreLocalInstruction(
+        'last_output',
+        tree_ir.ReadDictionaryValueInstruction(
+            store_task_root.create_load(),
+            tree_ir.LiteralInstruction('last_output')))
+
+    last_output_link = tree_ir.StoreLocalInstruction(
+        'last_output_link',
+        tree_ir.ReadDictionaryEdgeInstruction(
+            store_task_root.create_load(),
+            tree_ir.LiteralInstruction('last_output')))
+
+    new_last_output = tree_ir.StoreLocalInstruction(
+        'new_last_output',
+        tree_ir.CreateNodeInstruction())
+
+    return tree_ir.create_block(
+        value_local,
+        store_task_root,
+        last_output,
+        last_output_link,
+        new_last_output,
+        tree_ir.CreateDictionaryEdgeInstruction(
+            last_output.create_load(),
+            tree_ir.LiteralInstruction('value'),
+            value_local.create_load()),
+        tree_ir.CreateDictionaryEdgeInstruction(
+            last_output.create_load(),
+            tree_ir.LiteralInstruction('next'),
+            new_last_output.create_load()),
+        tree_ir.CreateDictionaryEdgeInstruction(
+            store_task_root.create_load(),
+            tree_ir.LiteralInstruction('last_output'),
+            new_last_output.create_load()),
+        tree_ir.DeleteEdgeInstruction(last_output_link.create_load()),
+        tree_ir.NopInstruction())
+
+def create_indirect_call(target, argument_list):
+    """Creates an indirect call to the function defined by the node with the id computed
+       by the first argument."""
+    # Call the __call_function function to run the interpreter, like so:
+    #
+    # __call_function(function_id, { first_param_name : first_param_val, ... }, **kwargs)
+    #
+    dict_literal = tree_ir.DictionaryLiteralInstruction(
+        [(tree_ir.LiteralInstruction(key), val) for key, val in argument_list])
+    return tree_ir.create_jit_call(
+        tree_ir.LoadGlobalInstruction(jit_runtime.CALL_FUNCTION_NAME),
+        [('function_id', target), ('named_arguments', dict_literal)],
+        tree_ir.LoadLocalInstruction(jit_runtime.KWARGS_PARAMETER_NAME))
+
+def with_debug_info_trace(instruction, debug_info, function_name):
+    """Prepends the given instruction with a tracing instruction that prints
+       the given debug information and function name."""
+    if debug_info is None and function_name is None:
+        return instruction
+    else:
+        return tree_ir.create_block(
+            tree_ir.PrintInstruction(
+                tree_ir.LiteralInstruction(
+                    jit_runtime.format_trace_message(
+                        debug_info, function_name,
+                        jit_runtime.BASELINE_JIT_ORIGIN_NAME))),
+            instruction)
+
+class LocalNameMap(object):
+    """A map that converts local variable nodes to identifiers."""
+    def __init__(self, local_mapping=None):
+        if local_mapping is None:
+            local_mapping = {}
+        self.local_mapping = local_mapping
+
+    def get_local_name(self, local_variable_id):
+        """Gets the name for the local variable node with the given id."""
+        if local_variable_id not in self.local_mapping:
+            self.local_mapping[local_variable_id] = 'local%d' % local_variable_id
+        return self.local_mapping[local_variable_id]
+
+class AnalysisState(object):
+    """The state of a bytecode analysis call graph."""
+    def __init__(self, jit, body_id, task_root, local_mapping, max_instructions=None):
+        self.analyzed_instructions = set()
+        self.function_vars = set()
+        self.local_vars = set()
+        self.body_id = body_id
+        self.max_instructions = max_instructions
+        self.task_root = task_root
+        self.jit = jit
+        self.local_name_map = LocalNameMap(local_mapping)
+        self.function_name = jit.jitted_entry_points[body_id]
+        self.enclosing_loop_instruction = None
+
+    def register_local_var(self, local_id):
+        """Registers the given variable node id as a local."""
+        if local_id in self.function_vars:
+            raise jit_runtime.JitCompilationFailedException(
+                "Local is used as target of function call.")
+        self.local_vars.add(local_id)
+
+    def register_function_var(self, local_id):
+        """Registers the given variable node id as a function."""
+        if local_id in self.local_vars:
+            raise jit_runtime.JitCompilationFailedException(
+                "Local is used as target of function call.")
+        self.function_vars.add(local_id)
+
+    def analyze(self, instruction):
+        """Tries to build an intermediate representation from the instruction with the
+        given id."""
+        # Check the analyzed_instructions set for instruction_id to avoid
+        # infinite loops.
+        if instruction in self.analyzed_instructions:
+            raise jit_runtime.JitCompilationFailedException(
+                'Cannot jit non-tree instruction graph.')
+        elif (self.max_instructions is not None and
+              len(self.analyzed_instructions) > self.max_instructions):
+            raise jit_runtime.JitCompilationFailedException(
+                'Maximum number of instructions exceeded.')
+
+        self.analyzed_instructions.add(instruction)
+        instruction_type = type(instruction)
+        if instruction_type in self.instruction_analyzers:
+            # Analyze the instruction itself.
+            outer_result, = yield [
+                ("CALL_ARGS", [self.instruction_analyzers[instruction_type], (self, instruction)])]
+            if instruction.debug_information is not None:
+                if self.jit.tracing_enabled:
+                    outer_result = with_debug_info_trace(
+                        outer_result, instruction.debug_information, self.function_name)
+                if self.jit.source_maps_enabled:
+                    outer_result = tree_ir.DebugInfoInstruction(
+                        outer_result, instruction.debug_information)
+
+            # Check if the instruction has a 'next' instruction.
+            if instruction.next_instruction is None:
+                raise primitive_functions.PrimitiveFinished(outer_result)
+            else:
+                next_result, = yield [
+                    ("CALL_ARGS", [self.analyze, (instruction.next_instruction,)])]
+                raise primitive_functions.PrimitiveFinished(
+                    tree_ir.CompoundInstruction(
+                        outer_result,
+                        next_result))
+        else:
+            raise jit_runtime.JitCompilationFailedException(
+                "Unknown instruction type: '%s'" % type(instruction))
+
+    def analyze_all(self, instruction_ids):
+        """Tries to compile a list of IR trees from the given list of instruction ids."""
+        results = []
+        for inst in instruction_ids:
+            analyzed_inst, = yield [("CALL_ARGS", [self.analyze, (inst,)])]
+            results.append(analyzed_inst)
+
+        raise primitive_functions.PrimitiveFinished(results)
+
+    def analyze_return(self, instruction):
+        """Tries to analyze the given 'return' instruction."""
+        def create_return(return_value):
+            return tree_ir.ReturnInstruction(
+                tree_ir.CompoundInstruction(
+                    return_value,
+                    tree_ir.DeleteEdgeInstruction(
+                        tree_ir.LoadLocalInstruction(jit_runtime.LOCALS_EDGE_NAME))))
+
+        if instruction.value is None:
+            raise primitive_functions.PrimitiveFinished(
+                create_return(
+                    tree_ir.EmptyInstruction()))
+        else:
+            retval, = yield [("CALL_ARGS", [self.analyze, (instruction.value,)])]
+            raise primitive_functions.PrimitiveFinished(
+                create_return(retval))
+
+    def analyze_if(self, instruction):
+        """Tries to analyze the given 'if' instruction."""
+        if instruction.else_clause is None:
+            (cond_r, true_r), = yield [
+                ("CALL_ARGS",
+                 [self.analyze_all,
+                  ([instruction.condition, instruction.if_clause],)])]
+            false_r = tree_ir.EmptyInstruction()
+        else:
+            (cond_r, true_r, false_r), = yield [
+                ("CALL_ARGS",
+                 [self.analyze_all,
+                  ([instruction.condition, instruction.if_clause, instruction.else_clause],)])]
+
+        raise primitive_functions.PrimitiveFinished(
+            tree_ir.SelectInstruction(
+                tree_ir.ReadValueInstruction(cond_r),
+                true_r,
+                false_r))
+
+    def analyze_while(self, instruction):
+        """Tries to analyze the given 'while' instruction."""
+        # Analyze the condition.
+        cond_r, = yield [("CALL_ARGS", [self.analyze, (instruction.condition,)])]
+        # Store the old enclosing loop on the stack, and make this loop the
+        # new enclosing loop.
+        old_loop_instruction = self.enclosing_loop_instruction
+        self.enclosing_loop_instruction = instruction
+        body_r, = yield [("CALL_ARGS", [self.analyze, (instruction.body,)])]
+        # Restore hte old enclosing loop.
+        self.enclosing_loop_instruction = old_loop_instruction
+        if self.jit.nop_insertion_enabled:
+            create_loop_body = lambda check, body: tree_ir.create_block(
+                check,
+                body_r,
+                tree_ir.NopInstruction())
+        else:
+            create_loop_body = tree_ir.CompoundInstruction
+
+        raise primitive_functions.PrimitiveFinished(
+            tree_ir.LoopInstruction(
+                create_loop_body(
+                    tree_ir.SelectInstruction(
+                        tree_ir.ReadValueInstruction(cond_r),
+                        tree_ir.EmptyInstruction(),
+                        tree_ir.BreakInstruction()),
+                    body_r)))
+
+    def analyze_constant(self, instruction):
+        """Tries to analyze the given 'constant' (literal) instruction."""
+        raise primitive_functions.PrimitiveFinished(
+            tree_ir.LiteralInstruction(instruction.constant_id))
+
+    def analyze_output(self, instruction):
+        """Tries to analyze the given 'output' instruction."""
+        value_val, = yield [("CALL_ARGS", [self.analyze, (instruction.value,)])]
+        raise primitive_functions.PrimitiveFinished(create_output(value_val))
+
+    def analyze_input(self, _):
+        """Tries to analyze the given 'input' instruction."""
+        raise primitive_functions.PrimitiveFinished(create_input(self.jit.input_function_enabled))
+
+    def analyze_resolve(self, instruction):
+        """Tries to analyze the given 'resolve' instruction."""
+        # To resolve a variable, we'll do something along the
+        # lines of:
+        #
+        #     if 'local_var' in locals():
+        #         tmp = local_var
+        #     else:
+        #         _globals, = yield [("RD", [task_root, "globals"])]
+        #         global_var, = yield [("RD", [_globals, var_name])]
+        #
+        #         if global_var is None:
+        #             raise Exception("Not found as global: %s" % (var_name))
+        #
+        #         tmp = global_var
+
+        name = self.local_name_map.get_local_name(instruction.variable.node_id)
+
+        if instruction.variable.name is None:
+            raise primitive_functions.PrimitiveFinished(
+                tree_ir.LoadLocalInstruction(name))
+
+        task_root = retrieve_task_root()
+        global_var = tree_ir.StoreLocalInstruction(
+            'global_var',
+            tree_ir.ReadDictionaryValueInstruction(
+                tree_ir.ReadDictionaryValueInstruction(
+                    task_root.create_load(),
+                    tree_ir.LiteralInstruction('globals')),
+                tree_ir.LiteralInstruction(instruction.variable.name)))
+
+        err_block = tree_ir.SelectInstruction(
+            tree_ir.BinaryInstruction(
+                global_var.create_load(),
+                'is',
+                tree_ir.LiteralInstruction(None)),
+            tree_ir.RaiseInstruction(
+                tree_ir.CallInstruction(
+                    tree_ir.LoadGlobalInstruction('Exception'),
+                    [tree_ir.LiteralInstruction(
+                        jit_runtime.GLOBAL_NOT_FOUND_MESSAGE_FORMAT % instruction.variable.name)
+                    ])),
+            tree_ir.EmptyInstruction())
+
+        raise primitive_functions.PrimitiveFinished(
+            tree_ir.SelectInstruction(
+                tree_ir.LocalExistsInstruction(name),
+                tree_ir.LoadLocalInstruction(name),
+                tree_ir.CompoundInstruction(
+                    tree_ir.create_block(
+                        task_root,
+                        global_var,
+                        err_block),
+                    global_var.create_load())))
+
+    def analyze_declare(self, instruction):
+        """Tries to analyze the given 'declare' function."""
+        self.register_local_var(instruction.variable.node_id)
+
+        name = self.local_name_map.get_local_name(instruction.variable.node_id)
+
+        # The following logic declares a local:
+        #
+        #     if 'local_name' not in locals():
+        #         local_name, = yield [("CN", [])]
+        #         yield [("CE", [LOCALS_NODE_NAME, local_name])]
+
+        raise primitive_functions.PrimitiveFinished(
+            tree_ir.SelectInstruction(
+                tree_ir.LocalExistsInstruction(name),
+                tree_ir.EmptyInstruction(),
+                tree_ir.create_new_local_node(
+                    name,
+                    tree_ir.LoadLocalInstruction(jit_runtime.LOCALS_NODE_NAME))))
+
+    def analyze_global(self, instruction):
+        """Tries to analyze the given 'global' (declaration) instruction."""
+        # To declare a variable, we'll do something along the
+        # lines of:
+        #
+        #     _globals, = yield [("RD", [task_root, "globals"])]
+        #     global_var = yield [("RD", [_globals, var_name])]
+        #
+        #     if global_var is None:
+        #         global_var, = yield [("CN", [])]
+        #         yield [("CD", [_globals, var_name, global_var])]
+        #
+        #     tmp = global_var
+
+        task_root = retrieve_task_root()
+        _globals = tree_ir.StoreLocalInstruction(
+            '_globals',
+            tree_ir.ReadDictionaryValueInstruction(
+                task_root.create_load(),
+                tree_ir.LiteralInstruction('globals')))
+
+        global_var = tree_ir.StoreLocalInstruction(
+            'global_var',
+            tree_ir.ReadDictionaryValueInstruction(
+                _globals.create_load(),
+                tree_ir.LiteralInstruction(instruction.variable.name)))
+
+        raise primitive_functions.PrimitiveFinished(
+            tree_ir.CompoundInstruction(
+                tree_ir.create_block(
+                    task_root,
+                    _globals,
+                    global_var,
+                    tree_ir.SelectInstruction(
+                        tree_ir.BinaryInstruction(
+                            global_var.create_load(),
+                            'is',
+                            tree_ir.LiteralInstruction(None)),
+                        tree_ir.create_block(
+                            global_var.create_store(
+                                tree_ir.CreateNodeInstruction()),
+                            tree_ir.CreateDictionaryEdgeInstruction(
+                                _globals.create_load(),
+                                tree_ir.LiteralInstruction(
+                                    instruction.variable.name),
+                                global_var.create_load())),
+                        tree_ir.EmptyInstruction())),
+                global_var.create_load()))
+
+    def analyze_assign(self, instruction):
+        """Tries to analyze the given 'assign' instruction."""
+        (var_r, value_r), = yield [
+            ("CALL_ARGS", [self.analyze_all, ([instruction.pointer, instruction.value],)])]
+
+        raise primitive_functions.PrimitiveFinished(create_assign(var_r, value_r))
+
+    def analyze_access(self, instruction):
+        """Tries to analyze the given 'access' instruction."""
+        var_r, = yield [("CALL_ARGS", [self.analyze, (instruction.pointer,)])]
+        raise primitive_functions.PrimitiveFinished(create_access(var_r))
+
+    def analyze_direct_call(self, callee_id, callee_name, argument_list):
+        """Tries to analyze a direct 'call' instruction."""
+        body_id, = yield [("RD", [callee_id, jit_runtime.FUNCTION_BODY_KEY])]
+
+        # Make this function dependent on the callee.
+        if body_id in self.jit.compilation_dependencies:
+            self.jit.compilation_dependencies[body_id].add(self.body_id)
+
+        # Figure out if the function might be an intrinsic.
+        intrinsic = self.jit.get_intrinsic(callee_name)
+
+        if intrinsic is None:
+            if callee_name is not None:
+                self.jit.register_global(body_id, callee_name)
+                compiled_func = self.jit.lookup_compiled_function(callee_name)
+            else:
+                compiled_func = None
+
+            if compiled_func is None:
+                # Compile the callee.
+                yield [
+                    ("CALL_ARGS", [self.jit.jit_compile, (self.task_root, body_id, callee_name)])]
+
+            # Get the callee's name.
+            compiled_func_name = self.jit.get_compiled_name(body_id)
+
+            # This handles the corner case where a constant node is called, like
+            # 'call(constant(9), ...)'. In this case, `callee_name` is `None`
+            # because 'constant(9)' doesn't give us a name. However, we can look up
+            # the name of the function at a specific node. If that turns out to be
+            # an intrinsic, then we still want to pick the intrinsic over a call.
+            intrinsic = self.jit.get_intrinsic(compiled_func_name)
+
+        # Analyze the argument dictionary.
+        named_args, = yield [("CALL_ARGS", [self.analyze_arguments, (argument_list,)])]
+
+        if intrinsic is not None:
+            raise primitive_functions.PrimitiveFinished(
+                apply_intrinsic(intrinsic, named_args))
+        else:
+            raise primitive_functions.PrimitiveFinished(
+                tree_ir.create_jit_call(
+                    tree_ir.LoadGlobalInstruction(compiled_func_name),
+                    named_args,
+                    tree_ir.LoadLocalInstruction(jit_runtime.KWARGS_PARAMETER_NAME)))
+
+    def analyze_arguments(self, argument_list):
+        """Analyzes the given parameter-to-value mapping."""
+        named_args = []
+        for param_name, arg in argument_list:
+            param_val, = yield [("CALL_ARGS", [self.analyze, (arg,)])]
+            named_args.append((param_name, param_val))
+
+        raise primitive_functions.PrimitiveFinished(named_args)
+
+    def analyze_indirect_call(self, target, argument_list):
+        """Analyzes a call to an unknown function."""
+        # First off, let's analyze the callee and the argument list.
+        func_val, = yield [("CALL_ARGS", [self.analyze, (target,)])]
+        named_args, = yield [("CALL_ARGS", [self.analyze_arguments, (argument_list,)])]
+        func_val = tree_ir.StoreLocalInstruction(None, func_val)
+        raise primitive_functions.PrimitiveFinished(
+            tree_ir.create_block(
+                func_val,
+                create_indirect_call(func_val.create_load(), named_args)))
+
+    def try_analyze_direct_call(self, target, argument_list):
+        """Tries to analyze the given 'call' instruction as a direct call."""
+        if not self.jit.direct_calls_allowed:
+            raise jit_runtime.JitCompilationFailedException(
+                'Direct calls are not allowed by the JIT.')
+
+        # Figure out what the 'func' instruction's type is.
+        if isinstance(target, bytecode_ir.AccessInstruction):
+            # 'access(resolve(var))' instructions are translated to direct calls.
+            if isinstance(target.pointer, bytecode_ir.ResolveInstruction):
+                self.register_function_var(target.pointer.variable.node_id)
+                resolved_var_name = target.pointer.variable.name
+
+                if self.jit.thunks_enabled:
+                    # Analyze the argument dictionary.
+                    named_args, = yield [("CALL_ARGS", [self.analyze_arguments, (argument_list,)])]
+
+                    # Try to resolve the callee as an intrinsic.
+                    intrinsic = self.jit.get_intrinsic(resolved_var_name)
+                    if intrinsic is not None:
+                        raise primitive_functions.PrimitiveFinished(
+                            apply_intrinsic(intrinsic, named_args))
+
+                    # Otherwise, build a thunk.
+                    thunk_name = self.jit.jit_thunk_global(target.pointer.variable.name)
+                    raise primitive_functions.PrimitiveFinished(
+                        tree_ir.create_jit_call(
+                            tree_ir.LoadGlobalInstruction(thunk_name),
+                            named_args,
+                            tree_ir.LoadLocalInstruction(jit_runtime.KWARGS_PARAMETER_NAME)))
+                else:
+                    # Try to look up the name as a global.
+                    _globals, = yield [("RD", [self.task_root, "globals"])]
+                    global_var, = yield [("RD", [_globals, resolved_var_name])]
+                    global_val, = yield [("RD", [global_var, "value"])]
+
+                    if global_val is not None:
+                        result, = yield [("CALL_ARGS", [self.analyze_direct_call, (
+                            global_val, resolved_var_name, argument_list)])]
+                        raise primitive_functions.PrimitiveFinished(result)
+        elif isinstance(target, bytecode_ir.ConstantInstruction):
+            # 'const(func_id)' instructions are also translated to direct calls.
+            result, = yield [("CALL_ARGS", [self.analyze_direct_call, (
+                target.constant_id, None, argument_list)])]
+            raise primitive_functions.PrimitiveFinished(result)
+
+        raise jit_runtime.JitCompilationFailedException(
+            "Cannot JIT function calls that target an unknown value as direct calls.")
+
+    def analyze_call(self, instruction):
+        """Tries to analyze the given 'call' instruction."""
+        def handle_exception(_):
+            # Looks like we'll have to compile it as an indirect call.
+            gen = self.analyze_indirect_call(instruction.target, instruction.argument_list)
+            result, = yield [("CALL", [gen])]
+            raise primitive_functions.PrimitiveFinished(result)
+
+        # Try to analyze the call as a direct call.
+        yield [("TRY", [])]
+        yield [("CATCH", [jit_runtime.JitCompilationFailedException, handle_exception])]
+        result, = yield [
+            ("CALL_ARGS",
+             [self.try_analyze_direct_call, (instruction.target, instruction.argument_list)])]
+        yield [("END_TRY", [])]
+        raise primitive_functions.PrimitiveFinished(result)
+
+    def analyze_break(self, instruction):
+        """Tries to analyze the given 'break' instruction."""
+        if instruction.loop == self.enclosing_loop_instruction:
+            raise primitive_functions.PrimitiveFinished(tree_ir.BreakInstruction())
+        else:
+            raise jit_runtime.JitCompilationFailedException(
+                "Multilevel 'break' is not supported by the baseline JIT.")
+
+    def analyze_continue(self, instruction):
+        """Tries to analyze the given 'continue' instruction."""
+        if instruction.loop == self.enclosing_loop_instruction:
+            raise primitive_functions.PrimitiveFinished(tree_ir.ContinueInstruction())
+        else:
+            raise jit_runtime.JitCompilationFailedException(
+                "Multilevel 'continue' is not supported by the baseline JIT.")
+
+    instruction_analyzers = {
+        bytecode_ir.SelectInstruction : analyze_if,
+        bytecode_ir.WhileInstruction : analyze_while,
+        bytecode_ir.ReturnInstruction : analyze_return,
+        bytecode_ir.ConstantInstruction : analyze_constant,
+        bytecode_ir.ResolveInstruction : analyze_resolve,
+        bytecode_ir.DeclareInstruction : analyze_declare,
+        bytecode_ir.GlobalInstruction : analyze_global,
+        bytecode_ir.AssignInstruction : analyze_assign,
+        bytecode_ir.AccessInstruction : analyze_access,
+        bytecode_ir.OutputInstruction : analyze_output,
+        bytecode_ir.InputInstruction : analyze_input,
+        bytecode_ir.CallInstruction : analyze_call,
+        bytecode_ir.BreakInstruction : analyze_break,
+        bytecode_ir.ContinueInstruction : analyze_continue
+    }

+ 96 - 0
kernel/modelverse_jit/cfg_data_structures.py

@@ -0,0 +1,96 @@
+"""Defines optimizations that replace Modelverse data structures by Python data structures."""
+
+import modelverse_jit.cfg_ir as cfg_ir
+
+SET_DEF_REWRITE_RULES = {
+    'dict_keys':
+    lambda def_def:
+    def_def.redefine(
+        cfg_ir.DirectFunctionCall(
+            cfg_ir.REVERSE_LIST_MACRO_NAME,
+            [('seq',
+              def_def.insert_before(
+                  cfg_ir.DirectFunctionCall(
+                      cfg_ir.READ_DICT_KEYS_MACRO_NAME,
+                      cfg_ir.get_def_value(def_def).argument_list,
+                      cfg_ir.MACRO_POSITIONAL_CALLING_CONVENTION,
+                      has_side_effects=False)))],
+            cfg_ir.MACRO_POSITIONAL_CALLING_CONVENTION,
+            has_side_effects=False))
+}
+
+def __redefine_as_list_len(use_def, def_def):
+    use_def.redefine(
+        cfg_ir.CreateNode(
+            use_def.insert_before(
+                cfg_ir.DirectFunctionCall(
+                    'len', [('seq', def_def)],
+                    cfg_ir.SIMPLE_POSITIONAL_CALLING_CONVENTION,
+                    has_side_effects=False))))
+
+SET_USE_REWRITE_RULES = {
+    ('set_pop', 'a'):
+    lambda use_def, def_def:
+    use_def.redefine(
+        cfg_ir.DirectFunctionCall(
+            'pop', [('self', def_def)],
+            cfg_ir.SELF_POSITIONAL_CALLING_CONVENTION)),
+    ('list_len', 'a'): __redefine_as_list_len,
+    ('read_nr_out', 'a'): __redefine_as_list_len
+}
+
+def get_call_def_rewriter(definition, def_rewrite_rules):
+    """Gets an appropriate rewrite rule from the given dictionary of call rewrite rules."""
+    if cfg_ir.is_call(definition, calling_convention=cfg_ir.JIT_CALLING_CONVENTION):
+        call = cfg_ir.get_def_value(definition)
+        if call.target_name in def_rewrite_rules:
+            return def_rewrite_rules[call.target_name]
+
+    return None
+
+def get_call_use_rewriter(use_definition, def_definition, use_rewrite_rules):
+    """Gets an appropriate rewrite rule from the given dictionary of call rewrite rules."""
+    if not cfg_ir.is_call(use_definition):
+        return None
+
+    call = cfg_ir.get_def_value(use_definition)
+    if call.calling_convention not in (
+            cfg_ir.JIT_CALLING_CONVENTION,
+            cfg_ir.JIT_NO_GC_CALLING_CONVENTION,
+            cfg_ir.JIT_CFG_INTRINSIC_CALLING_CONVENTION):
+        return None
+
+    for arg_name, arg_def in call.argument_list:
+        if arg_def == def_definition:
+            key = (call.target_name, arg_name)
+            if key in use_rewrite_rules:
+                return use_rewrite_rules[key]
+
+    return None
+
+def apply_rewrite_rules(
+        entry_point,
+        get_def_rewriter,
+        get_use_rewriter):
+    """Applies the given definition and use rewrite rules to all definitions and uses where a
+       rewrite rule can be found for both the definitions and the uses."""
+    # pylint: disable=I0011,W0108
+    cfg_ir.match_and_rewrite(
+        entry_point,
+        lambda def_def:
+        get_def_rewriter(def_def) is not None,
+        lambda use_def, def_def:
+        get_use_rewriter(use_def, def_def) is not None,
+        lambda def_def:
+        get_def_rewriter(def_def)(def_def),
+        lambda use_def, def_def:
+        get_use_rewriter(use_def, def_def)(use_def, def_def))
+
+def optimize_data_structures(entry_point):
+    """Optimizes data structures in the graph defined by the given entry point."""
+    apply_rewrite_rules(
+        entry_point,
+        lambda def_def:
+        get_call_def_rewriter(def_def, SET_DEF_REWRITE_RULES),
+        lambda use_def, def_def:
+        get_call_use_rewriter(use_def, def_def, SET_USE_REWRITE_RULES))

+ 113 - 0
kernel/modelverse_jit/cfg_dominators.py

@@ -0,0 +1,113 @@
+"""Computes dominator trees for control-flow graphs."""
+
+from collections import defaultdict
+import modelverse_jit.cfg_ir as cfg_ir
+
+def sort_postorder(entry_point):
+    """Produces a postorder traversal of the graph with the given block as entry point."""
+    processed = set()
+    results = []
+    def __sort_postorder_step(block):
+        if block in processed:
+            return
+
+        processed.add(block)
+        for branch in block.flow.branches():
+            __sort_postorder_step(branch.block)
+
+        results.append(block)
+
+    __sort_postorder_step(entry_point)
+    return results
+
+class DominatorTree(object):
+    """A data structure that represents a dominator tree."""
+    def __init__(self, immediate_dominators):
+        self.immediate_dominators = immediate_dominators
+        self.dominator_sets = {}
+
+    def get_immediate_dominator(self, block):
+        """Gets the given block's immediate dominator."""
+        return self.immediate_dominators[block]
+
+    def get_dominators(self, block):
+        """Gets the given block's set of all dominators."""
+        if block in self.dominator_sets:
+            return self.dominator_sets
+        else:
+            idom = self.get_immediate_dominator(block)
+            if idom == block:
+                results = set([block])
+            else:
+                results = set(self.get_dominators(idom))
+                results.add(block)
+
+            self.dominator_sets[block] = results
+            return results
+
+    def dominates_block(self, dominator, dominated):
+        """Tests if the first block dominates the second."""
+        return dominator in self.get_dominators(dominated)
+
+    def dominates_instruction(self, dominator, dominated):
+        """Tests if the first instruction dominates the second."""
+        dominator_block = dominator.block
+        dominated_block = dominated.block
+        if dominator_block == dominated_block:
+            return dominator.definition_index < dominated.definition_index
+        else:
+            return self.dominates_block(dominator_block, dominated_block)
+
+# The algorithms below are based on "A Simple, Fast Dominance Algorithm" by
+# Keith D. Cooper, Timothy J. Harvey, and Ken Kennedy
+# (http://www.cs.rice.edu/~keith/Embed/dom.pdf)
+
+def intersect_immediate_dominators(block1, block2, idoms, postorder_nums):
+    """Computes the intersection of the given blocks' immediate dominators."""
+    finger1 = block1
+    finger2 = block2
+    while finger1 != finger2:
+        while postorder_nums[finger1] < postorder_nums[finger2]:
+            finger1 = idoms[finger1]
+
+        while postorder_nums[finger2] < postorder_nums[finger1]:
+            finger2 = idoms[finger2]
+    return finger1
+
+def get_immediate_dominators(entry_point):
+    """Computes the immediate dominators of the control-flow graph defined by the given entry
+       point."""
+    predecessor_map = cfg_ir.get_all_predecessor_blocks(entry_point)
+    idoms = {}
+    postorder_sort = sort_postorder(entry_point)
+    postorder_nums = {}
+    for i, elem in enumerate(postorder_sort):
+        postorder_nums[elem] = i
+        idoms[elem] = None
+
+    idoms[entry_point] = entry_point
+
+    changed = True
+    while changed:
+        changed = False
+        for block in reversed(postorder_sort):
+            if block == entry_point:
+                continue
+
+            new_idom = None
+            for pred in predecessor_map[block]:
+                assert pred in postorder_nums
+                if new_idom is None:
+                    new_idom = pred
+                elif idoms[pred] is not None:
+                    new_idom = intersect_immediate_dominators(pred, new_idom, idoms, postorder_nums)
+
+            if idoms[block] is not new_idom:
+                idoms[block] = new_idom
+                changed = True
+
+    return idoms
+
+def get_dominator_tree(entry_point):
+    """Constructs the dominator tree for the control-flow graph defined by the given entry point."""
+    return DominatorTree(get_immediate_dominators(entry_point))

File diff suppressed because it is too large
+ 1209 - 0
kernel/modelverse_jit/cfg_ir.py


+ 542 - 0
kernel/modelverse_jit/cfg_optimization.py

@@ -0,0 +1,542 @@
+"""Optimizes and analyzes CFG-IR."""
+
+from collections import defaultdict
+import modelverse_jit.cfg_ir as cfg_ir
+import modelverse_jit.cfg_dominators as cfg_dominators
+import modelverse_jit.cfg_ssa_construction as cfg_ssa_construction
+import modelverse_jit.cfg_data_structures as cfg_data_structures
+import modelverse_kernel.primitives as primitive_functions
+
+def is_empty_block(block):
+    """Tests if the given block contains no parameters or definitions."""
+    return len(block.parameters) == 0 and len(block.definitions) == 0
+
+def optimize_flow(block):
+    """Optimizes the given block's flow instruction."""
+    changed = True
+    while changed:
+        changed = False
+
+        # Select flow with a literal condition can be optimized to a direct jump.
+        if (isinstance(block.flow, cfg_ir.SelectFlow)
+                and cfg_ir.is_literal_def(block.flow.condition)):
+            literal = cfg_ir.get_literal_def_value(block.flow.condition)
+            block.flow = cfg_ir.JumpFlow(
+                block.flow.if_branch if literal else block.flow.else_branch)
+            changed = True
+
+        # Jumps to blocks which contain no parameters or definitions can be replaced
+        # by the target block's flow.
+        if (isinstance(block.flow, cfg_ir.JumpFlow)
+                and is_empty_block(block.flow.branch.block)
+                and block.flow.branch.block is not block):
+            block.flow = block.flow.branch.block.flow
+            changed = True
+
+        # Branches to blocks which contain nothing but a jump can be replaced by branches
+        # to the jump's target.
+        for branch in block.flow.branches():
+            if (is_empty_block(branch.block)
+                    and branch.block is not block
+                    and isinstance(branch.block.flow, cfg_ir.JumpFlow)):
+                new_branch = branch.block.flow.branch
+                branch.block = new_branch.block
+                branch.arguments = new_branch.arguments
+                changed = True
+
+def optimize_graph_flow(entry_point):
+    """Optimizes all flow instructions in the graph defined by the given entry point."""
+    for block in cfg_ir.get_all_blocks(entry_point):
+        optimize_flow(block)
+
+def merge_blocks(entry_point):
+    """Merges blocks which have exactly one predecessor with said predecessor, if the
+       predecessor has a jump flow instruction."""
+    predecessor_map = cfg_ir.get_all_predecessor_blocks(entry_point)
+    queue = set(predecessor_map.keys())
+    queue.add(entry_point)
+    def __do_merge(source, target):
+        target_params = list(target.parameters)
+        branch_args = list(source.flow.branch.arguments)
+        for target_param, branch_arg in zip(target_params, branch_args):
+            target.remove_parameter(target_param)
+            target_param.redefine(branch_arg)
+            source.append_definition(target_param)
+
+        target_defs = list(target.definitions)
+        for target_def in target_defs:
+            target.remove_definition(target_def)
+            source.append_definition(target_def)
+
+        source.flow = target.flow
+        for preds in predecessor_map.values():
+            if target in preds:
+                preds[preds.index(target)] = source
+                # preds.remove(target)
+                # preds.add(source)
+
+    while len(queue) > 0:
+        block = queue.pop()
+        if isinstance(block.flow, cfg_ir.JumpFlow):
+            next_block = block.flow.branch.block
+            preds = predecessor_map[next_block]
+            if (len(preds) == 1
+                    and next(iter(preds)) == block
+                    and block != next_block
+                    and next_block != entry_point):
+                __do_merge(block, next_block)
+                del predecessor_map[next_block]
+                queue.add(block)
+                if next_block in queue:
+                    queue.remove(next_block)
+
+def elide_local_checks(entry_point):
+    """Tries to elide redundant checks on local variables."""
+    # The plan here is to replace all check-local-exists defs by literals if
+    # they are either dominated by an appropriate declare-local or not reachable
+    # from a declare-local.
+    local_checks = []
+    local_defs = defaultdict(list)
+    for block in cfg_ir.get_all_blocks(entry_point):
+        for definition in block.definitions:
+            def_value = definition.value
+            if isinstance(def_value, cfg_ir.CheckLocalExists):
+                local_checks.append((def_value.variable.node_id, definition))
+            elif isinstance(def_value, cfg_ir.DeclareLocal):
+                local_defs[def_value.variable.node_id].append(definition)
+
+    dominator_tree = cfg_dominators.get_dominator_tree(entry_point)
+    reachable_blocks = cfg_ir.get_all_reachable_blocks(entry_point)
+    for (variable, check) in local_checks:
+        is_reachable = False
+        for local_def in local_defs[variable]:
+            if dominator_tree.dominates_instruction(local_def, check):
+                # Check is dominated by a definition. Replace it by a 'True' literal.
+                check.redefine(cfg_ir.Literal(True))
+                is_reachable = True
+                break
+            elif check.block in reachable_blocks[local_def.block]:
+                is_reachable = True
+
+        if not is_reachable:
+            # Check cannot be reached from any definition. Replace it by a 'False' literal.
+            check.redefine(cfg_ir.Literal(False))
+
+def eliminate_unused_definitions(entry_point):
+    """Tries to eliminate unused definitions in the control-flow graphb defined by the
+       given entry point."""
+    def_dependencies = defaultdict(set)
+    root_defs = set()
+    # Gather dependencies.
+    for block in cfg_ir.get_all_blocks(entry_point):
+        for definition in block.parameters + block.definitions:
+            all_dependencies = list(definition.get_all_dependencies())
+            def_dependencies[definition].update(
+                [dep for dep in all_dependencies
+                 if isinstance(dep, cfg_ir.Definition)])
+
+            if len(all_dependencies) > 0 and definition.has_bidirectional_dependencies():
+                for dep in all_dependencies:
+                    def_dependencies[dep].add(definition)
+
+            if definition.has_side_effects():
+                root_defs.add(definition)
+
+        for dep in block.flow.get_dependencies():
+            if isinstance(dep, cfg_ir.Definition):
+                root_defs.add(dep)
+            else:
+                assert isinstance(dep, cfg_ir.Branch)
+                for param, arg in zip(dep.block.parameters, dep.arguments):
+                    def_dependencies[param].add(arg)
+
+    # Figure out which definitions are live.
+    live_defs = set()
+    def __mark_live(definition):
+        if definition in live_defs:
+            return
+
+        live_defs.add(definition)
+        if definition in def_dependencies:
+            for dep in def_dependencies[definition]:
+                __mark_live(dep)
+
+    for root in root_defs:
+        __mark_live(root)
+
+    # Remove all dead definitions.
+    dead_defs = set.difference(set(def_dependencies.keys()), live_defs)
+    dead_phis = set()
+    for dead_def in dead_defs:
+        if isinstance(dead_def.value, cfg_ir.BlockParameter):
+            dead_phis.add(dead_def)
+        else:
+            dead_def.block.remove_definition(dead_def)
+
+    erase_parameters(entry_point, dead_phis)
+
+def eliminate_trivial_phis(entry_point):
+    """Eliminates trivial block parameters, i.e., block parameters which are really
+       aliases."""
+    phi_values = defaultdict(set)
+    all_blocks = list(cfg_ir.get_all_blocks(entry_point))
+    for block in all_blocks:
+        for branch in block.flow.branches():
+            for phi, arg in zip(branch.block.parameters, branch.arguments):
+                phi_values[phi].add(arg)
+
+    replacements = []
+    for block in all_blocks:
+        block_parameters = list(block.parameters)
+        for parameter_def in block_parameters:
+            trivial_phi_val = cfg_ir.get_trivial_phi_value(
+                parameter_def, phi_values[parameter_def])
+            if trivial_phi_val is not None:
+                replacements.append((parameter_def, trivial_phi_val))
+
+    erase_parameters(entry_point, set([parameter_def for parameter_def, _ in replacements]))
+
+    for parameter_def, trivial_phi_val in replacements:
+        block = parameter_def.block
+        parameter_def.redefine(trivial_phi_val)
+        block.prepend_definition(parameter_def)
+
+def erase_parameters(entry_point, parameters_to_erase):
+    """Erases all arguments for the given set of parameters, and then takes out the
+       parameters themselves."""
+    for block in cfg_ir.get_all_blocks(entry_point):
+        for branch in block.flow.branches():
+            new_arg_list = []
+            for parameter, arg in zip(branch.block.parameters, branch.arguments):
+                if parameter not in parameters_to_erase:
+                    new_arg_list.append(arg)
+            branch.arguments = new_arg_list
+
+    for parameter_def in parameters_to_erase:
+        parameter_def.block.remove_parameter(parameter_def)
+
+def apply_cfg_intrinsic(intrinsic_function, original_definition, named_args):
+    """Applies the given intrinsic to the given sequence of named arguments."""
+    kwargs = dict(named_args)
+    kwargs['original_def'] = original_definition
+    return intrinsic_function(**kwargs)
+
+def try_redefine_as_direct_call(definition, jit, called_globals):
+    """Tries to redefine the given indirect call definition as a direct call."""
+    call = cfg_ir.get_def_value(definition)
+    if not isinstance(call, cfg_ir.IndirectFunctionCall):
+        return
+
+    target = cfg_ir.get_def_value(call.target)
+    if isinstance(target, cfg_ir.LoadPointer):
+        loaded_ptr = cfg_ir.get_def_value(target.pointer)
+        if isinstance(loaded_ptr, cfg_ir.ResolveGlobal):
+            resolved_var_name = loaded_ptr.variable.name
+            called_globals.add(loaded_ptr)
+
+            # Try to resolve the callee as an intrinsic.
+            intrinsic = jit.get_cfg_intrinsic(resolved_var_name)
+            if intrinsic is not None:
+                definition.redefine(
+                    cfg_ir.DirectFunctionCall(
+                        resolved_var_name, call.argument_list,
+                        cfg_ir.JIT_CFG_INTRINSIC_CALLING_CONVENTION))
+            else:
+                # Otherwise, build a thunk.
+                thunk_name = jit.jit_thunk_global(resolved_var_name)
+                calling_convention = (
+                    cfg_ir.JIT_NO_GC_CALLING_CONVENTION
+                    if jit.get_intrinsic(thunk_name) is not None
+                    else cfg_ir.JIT_CALLING_CONVENTION)
+                definition.redefine(
+                    cfg_ir.DirectFunctionCall(
+                        thunk_name, call.argument_list, calling_convention))
+                called_globals.add(loaded_ptr)
+    elif isinstance(target, cfg_ir.Literal):
+        node_id = target.literal
+        thunk_name = jit.jit_thunk_constant_function(node_id)
+        definition.redefine(
+            cfg_ir.DirectFunctionCall(
+                thunk_name, call.argument_list, cfg_ir.JIT_CALLING_CONVENTION))
+
+def get_checked_global(definition):
+    """If the definition is a check that tests if a global does not exist, then
+       the instruction that resolves the global is returned; otherwise None."""
+    def_value = cfg_ir.get_def_value(definition)
+    if not isinstance(def_value, cfg_ir.Binary):
+        return None
+
+    if def_value.operator != 'is':
+        return None
+
+    def __get_checked_global_single_dir(lhs, rhs):
+        if (isinstance(lhs, cfg_ir.ResolveGlobal)
+                and isinstance(rhs, cfg_ir.Literal)
+                and rhs.literal is None):
+            return lhs
+        else:
+            return None
+
+    bin_lhs = cfg_ir.get_def_value(def_value.lhs)
+    bin_rhs = cfg_ir.get_def_value(def_value.rhs)
+    result = __get_checked_global_single_dir(bin_lhs, bin_rhs)
+    if result is None:
+        result = __get_checked_global_single_dir(bin_rhs, bin_lhs)
+
+    return result
+
+def optimize_calls(entry_point, jit):
+    """Converts indirect calls to direct calls in the control-flow graph defined by the
+       given entry point."""
+    called_globals = set()
+    global_exists_defs = defaultdict(list)
+    for block in cfg_ir.get_all_blocks(entry_point):
+        for definition in block.definitions:
+            checked_global = get_checked_global(definition)
+            if checked_global is not None:
+                global_exists_defs[checked_global].append(definition)
+            else:
+                try_redefine_as_direct_call(definition, jit, called_globals)
+
+    for resolve_global in called_globals:
+        for exists_def in global_exists_defs[resolve_global]:
+            exists_def.redefine(cfg_ir.Literal(False))
+
+def expand_cfg_intrinsics(entry_point, jit):
+    """Expands CFG JIT intrinsics in the control-flow graph defined by the given entry point."""
+    for block in cfg_ir.get_all_blocks(entry_point):
+        for definition in block.definitions:
+            def_value = definition.value
+            if (isinstance(def_value, cfg_ir.DirectFunctionCall)
+                    and def_value.calling_convention ==
+                    cfg_ir.JIT_CFG_INTRINSIC_CALLING_CONVENTION):
+                intrinsic = jit.get_cfg_intrinsic(def_value.target_name)
+                apply_cfg_intrinsic(intrinsic, definition, def_value.argument_list)
+
+def simplify_values(entry_point):
+    """Simplifies values in the control-flow graph defined by the given entry point."""
+    for block in cfg_ir.get_all_blocks(entry_point):
+        for definition in block.definitions:
+            def_val = cfg_ir.get_def_value(definition)
+            if isinstance(def_val, cfg_ir.Read):
+                read_node = cfg_ir.get_def_value(def_val.node)
+                if isinstance(read_node, cfg_ir.CreateNode):
+                    definition.redefine(read_node.value)
+            elif isinstance(def_val, cfg_ir.Binary):
+                lhs = cfg_ir.get_def_value(def_val.lhs)
+                rhs = cfg_ir.get_def_value(def_val.rhs)
+                if isinstance(lhs, cfg_ir.Literal) and isinstance(rhs, cfg_ir.Literal):
+                    definition.redefine(
+                        cfg_ir.Literal(
+                            eval('%r %s %r' % (lhs.literal, def_val.operator, rhs.literal))))
+            elif isinstance(def_val, cfg_ir.Unary):
+                operand = cfg_ir.get_def_value(def_val.operand)
+                if isinstance(operand, cfg_ir.Literal):
+                    definition.redefine(
+                        cfg_ir.Literal(
+                            eval('%s %r' % (def_val.operator, operand.literal))))
+
+def inline_constants(entry_point):
+    """Replaces reads of constant nodes by the literals they contain."""
+    for block in cfg_ir.get_all_blocks(entry_point):
+        for definition in block.definitions:
+            def_val = cfg_ir.get_def_value(definition)
+            if isinstance(def_val, cfg_ir.Read):
+                read_node = cfg_ir.get_def_value(def_val.node)
+                if isinstance(read_node, cfg_ir.Literal):
+                    val, = yield [("RV", [read_node.literal])]
+                    definition.redefine(cfg_ir.Literal(val))
+
+def expand_indirect_definitions(entry_point):
+    """Replaces indirect definitions by the values referred to by those definitions."""
+    def __expand_indirect_defs(value):
+        dependencies = value.get_dependencies()
+        if len(dependencies) == 0:
+            return value
+        else:
+            new_dependencies = []
+            for dep in dependencies:
+                new_dep = dep
+                if isinstance(new_dep, cfg_ir.Definition):
+                    while isinstance(new_dep.value, cfg_ir.Definition):
+                        new_dep = new_dep.value
+                else:
+                    new_dep = __expand_indirect_defs(new_dep)
+
+                new_dependencies.append(new_dep)
+            return value.create(new_dependencies)
+
+    for block in cfg_ir.get_all_blocks(entry_point):
+        block_definitions = list(block.definitions)
+        for definition in block_definitions:
+            if isinstance(definition.value, cfg_ir.Definition):
+                block.remove_definition(definition)
+            else:
+                definition.redefine(
+                    __expand_indirect_defs(definition.value))
+
+        block.flow = __expand_indirect_defs(block.flow)
+
+def optimize_reads(entry_point):
+    """Tries to replace repeated reads by a single read."""
+    cfg_ir.match_and_rewrite(
+        entry_point,
+        lambda _: True,
+        lambda use_def, _: cfg_ir.is_value_def(use_def, cfg_ir.Read),
+        lambda def_def:
+        def_def.redefine(
+            cfg_ir.Read(def_def.insert_before(def_def.value))),
+        lambda use_def, def_def: use_def.redefine(def_def))
+
+def protect_from_gc(entry_point):
+    """Protects locals in the control-flow graph defined by the given
+       entry point from the GC."""
+    root_node = entry_point.prepend_definition(cfg_ir.AllocateRootNode())
+    def protect_def_from_gc(definition):
+        """Protects the given definition from the GC."""
+        definition.insert_after(cfg_ir.create_gc_protect(definition, root_node))
+
+    def maybe_protect_def_from_gc(definition):
+        """Protects the given definition from the GC, if its result is not None."""
+        definition.insert_after(cfg_ir.create_conditional_gc_protect(definition, root_node))
+
+    for block in cfg_ir.get_all_blocks(entry_point):
+        for definition in block.definitions:
+            def_value = cfg_ir.get_def_value(definition)
+            if isinstance(def_value, cfg_ir.CreateNode):
+                protect_def_from_gc(definition)
+            elif (isinstance(def_value, cfg_ir.IndirectFunctionCall)
+                  or (isinstance(def_value, cfg_ir.DirectFunctionCall)
+                      and (def_value.calling_convention == cfg_ir.JIT_CALLING_CONVENTION
+                           or def_value.calling_convention == cfg_ir.JIT_NO_GC_CALLING_CONVENTION
+                           or def_value.calling_convention == cfg_ir.MACRO_IO_CALLING_CONVENTION)
+                      and def_value.has_value())):
+                maybe_protect_def_from_gc(definition)
+
+        if isinstance(block.flow, (cfg_ir.ReturnFlow, cfg_ir.ThrowFlow)):
+            block.append_definition(cfg_ir.DeallocateRootNode(root_node))
+
+def elide_gc_protects(entry_point):
+    """Tries to elide GC protection values."""
+    # We don't need to protect a value from the GC if it is used for the
+    # last time _before_ the GC has an opportunity to kick in. To simplify
+    # things, we'll do a quick block-based analysis.
+    def __may_cause_gc(definition):
+        def_value = cfg_ir.get_def_value(definition)
+        if isinstance(def_value, cfg_ir.IndirectFunctionCall):
+            return True
+        elif (isinstance(def_value, cfg_ir.DirectFunctionCall)
+              and (def_value.calling_convention == cfg_ir.JIT_CALLING_CONVENTION
+                   or def_value.calling_convention == cfg_ir.MACRO_IO_CALLING_CONVENTION)):
+            return True
+        else:
+            return False
+
+    def __get_protected_def(def_or_value):
+        value = cfg_ir.get_def_value(def_or_value)
+        if cfg_ir.is_call(
+                value, target_name=cfg_ir.GC_PROTECT_MACRO_NAME,
+                calling_convention=cfg_ir.MACRO_POSITIONAL_CALLING_CONVENTION):
+            _, protected_def = value.argument_list[0]
+            return protected_def
+        elif cfg_ir.is_call(
+                value, target_name=cfg_ir.MAYBE_GC_PROTECT_MACRO_NAME,
+                calling_convention=cfg_ir.MACRO_POSITIONAL_CALLING_CONVENTION):
+            _, protected_def = value.argument_list[1]
+            return protected_def
+        else:
+            return None
+
+    def_blocks = {}
+    def __register_def_or_use(definition, block):
+        if definition in def_blocks and def_blocks[definition] != block:
+            # Definition seems to be used across basic blocks.
+            ineligible_defs.add(definition)
+
+        def_blocks[definition] = block
+
+    ineligible_defs = set()
+    def_protections = defaultdict(list)
+    for block in cfg_ir.get_all_blocks(entry_point):
+        no_gc_defs = set()
+        block_defs = set()
+        first_gc = {}
+        last_def_uses = {}
+        for i, definition in enumerate(block.definitions):
+            if isinstance(definition.value, cfg_ir.Definition):
+                # Handling definitions of definitions is complicated and they should already have
+                # been expanded at this point. Just mark them as ineligible.
+                ineligible_defs.add(definition)
+                ineligible_defs.add(definition.value)
+                continue
+
+            protected_def = __get_protected_def(definition)
+            if protected_def is not None:
+                # We just ran into a gc_protect/maybe_gc_protect.
+                def_protections[protected_def].append(definition)
+                continue
+
+            block_defs.add(definition)
+            __register_def_or_use(definition, block)
+
+            for dependency in definition.get_all_dependencies():
+                __register_def_or_use(dependency, block)
+                last_def_uses[dependency] = i
+
+            if __may_cause_gc(definition):
+                for gc_def in no_gc_defs:
+                    first_gc[gc_def] = i
+                no_gc_defs = set()
+
+            no_gc_defs.add(definition)
+
+        # Mark all branch arguments as ineligible.
+        for branch in block.flow.branches():
+            ineligible_defs.update(branch.arguments)
+
+        for dependency in block.flow.get_dependencies():
+            last_def_uses[dependency] = None
+
+        for definition in block_defs:
+            if definition in ineligible_defs:
+                # Definition was already ineligible.
+                continue
+
+            # Mark `definition` as ineligible if there is a GC definition in the range of
+            # definitions (definition, last_def_uses[definition]].
+            if definition in first_gc:
+                if definition in last_def_uses:
+                    last_use = last_def_uses[definition]
+                    if last_use is None or first_gc[definition] <= last_use:
+                        ineligible_defs.add(definition)
+
+    # Elide all GC protections for definitions which are not in the `ineligible_defs` set.
+    for protected, protections in def_protections.items():
+        if protected not in ineligible_defs:
+            for protect_def in protections:
+                protect_def.redefine(cfg_ir.Literal(None))
+
+def optimize(entry_point, jit):
+    """Optimizes the control-flow graph defined by the given entry point.
+       A potentially altered entry point is returned."""
+    optimize_graph_flow(entry_point)
+    elide_local_checks(entry_point)
+    optimize_graph_flow(entry_point)
+    eliminate_trivial_phis(entry_point)
+    entry_point = cfg_ssa_construction.construct_ssa_form(entry_point)
+    if jit.direct_calls_allowed:
+        optimize_calls(entry_point, jit)
+    cfg_data_structures.optimize_data_structures(entry_point)
+    expand_cfg_intrinsics(entry_point, jit)
+    yield [("CALL_ARGS", [inline_constants, (entry_point,)])]
+    optimize_reads(entry_point)
+    simplify_values(entry_point)
+    eliminate_unused_definitions(entry_point)
+    optimize_graph_flow(entry_point)
+    expand_indirect_definitions(entry_point)
+    eliminate_unused_definitions(entry_point)
+    merge_blocks(entry_point)
+    protect_from_gc(entry_point)
+    elide_gc_protects(entry_point)
+    eliminate_unused_definitions(entry_point)
+    raise primitive_functions.PrimitiveFinished(entry_point)

+ 268 - 0
kernel/modelverse_jit/cfg_ssa_construction.py

@@ -0,0 +1,268 @@
+"""Converts 'declare-local', 'load' and 'store' instructions into SSA form."""
+
+from collections import defaultdict
+import modelverse_jit.cfg_ir as cfg_ir
+
+def get_local_id(def_or_value):
+    """Gets the node of the local resolved or declared by the given definition or value.
+       If the given definition or value does not refer to a 'resolve-local' or
+       'declare-local' node, then None is returned."""
+    value = cfg_ir.get_def_value(def_or_value)
+    if isinstance(value, (cfg_ir.ResolveLocal, cfg_ir.DeclareLocal)):
+        return value.variable.node_id
+    else:
+        return None
+
+def get_ineligible_local_ids(entry_point):
+    """Finds the ids of all local variables which are not eligible for conversion to SSA form."""
+    # Local variables are eligible for conversion to SSA form if their pointer node is never
+    # leaked to the outside world. So we know that we can safely convert a local to SSA form
+    # if 'resolve-local' values are only used by 'load' and 'store' values.
+    ineligible_local_ids = set()
+    def __maybe_mark_ineligible(def_or_value):
+        local_id = get_local_id(def_or_value)
+        if local_id is not None:
+            ineligible_local_ids.add(local_id)
+
+    for block in cfg_ir.get_all_blocks(entry_point):
+        for definition in block.definitions + [block.flow]:
+            value = cfg_ir.get_def_value(definition)
+            if isinstance(value, cfg_ir.LoadPointer):
+                # Loading a pointer to a local is fine.
+                pass
+            elif isinstance(value, cfg_ir.StoreAtPointer):
+                # Storing a value in a local is fine, too.
+                # But be careful not to ignore a store where the stored value is a local pointer.
+                __maybe_mark_ineligible(value.value)
+            else:
+                # Walk over the dependencies, and mark them all as ineligible for
+                # local-to-SSA conversion.
+                for dependency in value.get_all_dependencies():
+                    __maybe_mark_ineligible(dependency)
+
+    return ineligible_local_ids
+
+def construct_ssa_form(entry_point):
+    """Converts local variables into SSA form in the graph defined by the given entry point.
+       A potentially new entry block is returned."""
+    # Build some helper data structures.
+    all_blocks = list(cfg_ir.get_all_blocks(entry_point))
+    ineligible_locals = get_ineligible_local_ids(entry_point)
+    predecessor_map = cfg_ir.get_all_predecessor_blocks(entry_point)
+
+    # Create the SSA construction state.
+    state = SSAConstructionState(all_blocks, ineligible_locals, predecessor_map)
+
+    # Fill all blocks in the graph.
+    for block in all_blocks:
+        state.fill_block(block)
+
+    # Update branches.
+    for block in all_blocks:
+        state.update_block_branches(block)
+
+    # Nullify entry point parameters.
+    return nullify_entry_block_parameters(entry_point)
+
+def nullify_entry_block_parameters(entry_point):
+    """Creates or returns an entry block that takes no parameters.
+       The (new) entry block is returned."""
+    # The SSA construction algorithm has the nasty habit of replacing potentially
+    # undefined variables by entry block parameters. Codegen doesn't like that one
+    # bit: it assumes that all block parameters are defined by the branches to those
+    # parameters -- but there usually is no branch to the entry point!
+    #
+    # We can fix this by either replacing the block-parameters in the entry block by
+    # definitions, or by creating a new entry point that jumps to the old entry
+    # point with an argument list. The former construction is more efficient, but it is only
+    # correct if there are no pre-existing branches to the entry point.
+    #
+    # We could build the predecessor map block and use the first option if possible,
+    # but we won't; trivial phi elimination followed by block merging will reduce
+    # the second construction to the first one anyway.
+    if len(entry_point.parameters) == 0:
+        return entry_point
+
+    pre_entry_point = cfg_ir.BasicBlock(entry_point.counter)
+    arg_list = []
+    for _ in entry_point.parameters:
+        literal_def = pre_entry_point.append_definition(cfg_ir.Literal(None))
+        arg_list.append(literal_def)
+
+    pre_entry_point.flow = cfg_ir.create_jump(entry_point, arg_list)
+    return pre_entry_point
+
+# The algorithms below are based on
+# Simple and Efficient Construction of Static Single Assignment Form by M. Braun et al
+# (https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf).
+
+class SSAConstructionState(object):
+    """Encapsulates state related to SSA construction."""
+    def __init__(self, all_blocks, ineligible_locals, predecessor_map):
+        self.all_blocks = all_blocks
+        self.ineligible_locals = ineligible_locals
+        self.predecessor_map = predecessor_map
+        # `current_defs` is a local node id -> basic block -> definition map.
+        self.current_defs = defaultdict(dict)
+        # `incomplete_phis` is a basic block -> local node id -> block parameter def map.
+        self.incomplete_phis = defaultdict(dict)
+        # `extra_phi_operands` is a basic block -> block parameter def -> def map.
+        self.extra_phi_operands = defaultdict(dict)
+        self.processed_blocks = set()
+        self.filled_blocks = set()
+        self.sealed_blocks = set()
+
+    def read_variable(self, block, node_id):
+        """Reads the latest definition of the local variable with the
+           given node id for the specified block."""
+        if block in self.current_defs[node_id]:
+            return self.current_defs[node_id][block]
+        else:
+            return self.read_variable_recursive(block, node_id)
+
+    def write_variable(self, block, node_id, value):
+        """Writes the given value to the local with the specified id in the
+           specified block."""
+        self.current_defs[node_id][block] = value
+
+    def read_variable_recursive(self, block, node_id):
+        """Reads the latest definition of the local variable with the
+           given node id from one of the given block's predecessor blocks."""
+        if block not in self.sealed_blocks:
+            # Create an incomplete phi.
+            val = block.append_parameter(cfg_ir.BlockParameter())
+            self.incomplete_phis[block][node_id] = val
+        elif len(self.predecessor_map[block]) == 1:
+            # Optimize the common case of one predecessor: no phi needed.
+            pred = next(iter(self.predecessor_map[block]))
+            val = self.read_variable(pred, node_id)
+        else:
+            # Break potential cycles with an operandless phi.
+            val = block.append_parameter(cfg_ir.BlockParameter())
+            self.write_variable(block, node_id, val)
+            val = self.add_phi_operands(node_id, val)
+
+        self.write_variable(block, node_id, val)
+        return val
+
+    def add_phi_operands(self, node_id, phi_def):
+        """Finds out which arguments branches should provide for the given block
+           parameter definition."""
+        # Determine operands from predecessors
+        all_values = []
+        for pred in self.predecessor_map[phi_def.block]:
+            arg = self.read_variable(pred, node_id)
+            self.extra_phi_operands[pred][phi_def] = arg
+            all_values.append(arg)
+        return self.try_remove_trivial_phi(phi_def, all_values)
+
+    def try_remove_trivial_phi(self, phi_def, values):
+        """Tries to remove a trivial block parameter definition."""
+        # This is a somewhat simplified (and less powerful) version of the
+        # algorithm in the SSA construction paper. That's kind of okay, though;
+        # trivial phi elimination is also implemented as a separate pass in the
+        # optimization pipeline.
+        trivial_phi_val = cfg_ir.get_trivial_phi_value(phi_def, values)
+        if trivial_phi_val is None:
+            return phi_def
+        else:
+            for pred in self.predecessor_map[phi_def.block]:
+                del self.extra_phi_operands[pred][phi_def]
+
+            phi_def.block.remove_parameter(phi_def)
+            phi_def.redefine(trivial_phi_val)
+            phi_def.block.prepend_definition(phi_def)
+            return trivial_phi_val
+
+    def has_sealed(self, block):
+        """Tells if the given block has been sealed yet."""
+        return block in self.sealed_blocks
+
+    def can_seal(self, block):
+        """Tells if the given block can be sealed right away."""
+        # A block can be sealed if all if its predecessors have been filled.
+        return all(
+            [predecessor in self.filled_blocks for predecessor in self.predecessor_map[block]])
+
+    def seal_all_sealable_blocks(self):
+        """Seals all sealable blocks."""
+        for block in self.all_blocks:
+            if self.can_seal(block):
+                self.seal_block(block)
+
+    def seal_block(self, block):
+        """Seals the given block."""
+        if self.has_sealed(block):
+            return
+
+        for node_id, phi_def in self.incomplete_phis[block].items():
+            self.add_phi_operands(node_id, phi_def)
+
+        self.sealed_blocks.add(block)
+
+    def has_filled(self, block):
+        """Tells if the given block has been filled yet."""
+        return block in self.filled_blocks
+
+    def fill_block(self, block):
+        """Visits all definitions in the given block. Locals are converted into SSA form."""
+        if block in self.processed_blocks:
+            return
+
+        self.processed_blocks.add(block)
+
+        # Try to seal the block right away if at all possible.
+        if self.can_seal(block):
+            self.seal_block(block)
+
+        block_definitions = list(block.definitions)
+        for definition in block_definitions:
+            value = definition.value
+            if cfg_ir.is_value_def(value, cfg_ir.LoadPointer):
+                # Read the variable from the definitions dictionary.
+                node_id = get_local_id(value.pointer)
+                if node_id is not None and node_id not in self.ineligible_locals:
+                    definition.redefine(self.read_variable(block, node_id))
+            elif isinstance(value, cfg_ir.StoreAtPointer):
+                node_id = get_local_id(value.pointer)
+                if node_id is not None and node_id not in self.ineligible_locals:
+                    # Write to the variable, and replace the definition by a 'None' literal.
+                    self.write_variable(block, node_id, value.value)
+                    definition.redefine(cfg_ir.Literal(None))
+            elif isinstance(value, cfg_ir.DeclareLocal):
+                node_id = value.variable.node_id
+                if node_id not in self.ineligible_locals:
+                    definition.redefine(cfg_ir.Literal(None))
+
+
+        # Mark the block as filled.
+        self.filled_blocks.add(block)
+
+        # Seal all sealable blocks.
+        self.seal_all_sealable_blocks()
+
+        # Fill successor blocks.
+        for branch in block.flow.branches():
+            self.fill_block(branch.block)
+
+    def update_block_branches(self, block):
+        """Appends arguments to the given block's flow instruction's branches, if necessary."""
+        for branch in block.flow.branches():
+            # Find all pairs phis which are defined in the branch target block.
+            applicable_pairs = [
+                (phi_def, operand_def)
+                for phi_def, operand_def in self.extra_phi_operands[block].items()
+                if phi_def.block == branch.block]
+
+            if len(applicable_pairs) == 0:
+                # We might as well early-out here.
+                continue
+
+            # Sort the pairs by block parameter index.
+            sorted_pairs = sorted(
+                applicable_pairs,
+                key=lambda (phi_def, _): phi_def.block.parameters.index(phi_def))
+
+            # Append arguments to the branch.
+            for _, arg in sorted_pairs:
+                branch.arguments.append(arg)

File diff suppressed because it is too large
+ 1022 - 0
kernel/modelverse_jit/cfg_to_tree.py


+ 198 - 34
kernel/modelverse_jit/intrinsics.py

@@ -1,6 +1,8 @@
-import jit
-import tree_ir
 import time
+import modelverse_jit.jit as jit
+import modelverse_jit.tree_ir as tree_ir
+import modelverse_jit.cfg_ir as cfg_ir
+import modelverse_jit.runtime as jit_runtime
 
 BINARY_INTRINSICS = {
     'value_eq' : '==',
@@ -60,22 +62,22 @@ def create_get_length(expression):
 # get them right.
 # pylint: disable=I0011,C0103
 def __set_add(a, b):
-    tmp = tree_ir.StoreLocalInstruction(None, a)
+    store_a, load_a = tree_ir.evaluate_and_load(a)
     return tree_ir.create_block(
-        tmp,
-        tree_ir.CreateEdgeInstruction(tmp.create_load(), b),
-        tmp.create_load())
+        store_a,
+        tree_ir.CreateEdgeInstruction(load_a, b),
+        load_a)
 
 def __dict_add(a, b, c):
-    a_tmp = tree_ir.StoreLocalInstruction(None, a)
-    b_tmp = tree_ir.StoreLocalInstruction(None, b)
+    store_a, load_a = tree_ir.evaluate_and_load(a)
+    store_b, load_b = tree_ir.evaluate_and_load(b)
     return tree_ir.create_block(
-        a_tmp,
-        b_tmp,
+        store_a,
+        store_b,
         tree_ir.CreateEdgeInstruction(
-            tree_ir.CreateEdgeInstruction(a_tmp.create_load(), c),
-            b_tmp.create_load()),
-        a_tmp.create_load())
+            tree_ir.CreateEdgeInstruction(load_a, c),
+            load_b),
+        load_a)
 
 def __list_read(a, b):
     # The statements in this function generate the following code:
@@ -87,17 +89,17 @@ def __list_read(a, b):
     #     raise Exception("List read out of bounds: %s" % b_value)
     # result
 
-    a_tmp = tree_ir.StoreLocalInstruction(None, a)
+    store_a, load_a = tree_ir.evaluate_and_load(a)
     b_val = tree_ir.StoreLocalInstruction(
         None,
         tree_ir.ReadValueInstruction(b))
     result = tree_ir.StoreLocalInstruction(
         None,
         tree_ir.ReadDictionaryValueInstruction(
-            a_tmp.create_load(), b_val.create_load()))
+            load_a.create_load(), b_val.create_load()))
 
     return tree_ir.create_block(
-        a_tmp,
+        store_a,
         b_val,
         result,
         tree_ir.SelectInstruction(
@@ -112,7 +114,7 @@ def __list_read(a, b):
                         tree_ir.LiteralInstruction('List read out of bounds: %s'),
                         '%',
                         b_val.create_load())])),
-            tree_ir.NopInstruction()),
+            tree_ir.EmptyInstruction()),
         result.create_load())
 
 def __list_append(a, b):
@@ -124,17 +126,18 @@ def __list_append(a, b):
     # _ = yield [("CD", [a_tmp, len(a_outgoing), b_tmp])]
     # a
 
-    a_tmp = tree_ir.StoreLocalInstruction(None, a)
-    b_tmp = tree_ir.StoreLocalInstruction(None, b)
+    store_a, load_a = tree_ir.evaluate_and_load(a)
+    store_b, load_b = tree_ir.evaluate_and_load(b)
     return tree_ir.create_block(
-        a_tmp,
+        store_a,
+        store_b,
         tree_ir.CreateDictionaryEdgeInstruction(
-            a_tmp.create_load(),
+            load_a,
             create_get_length(
                 tree_ir.ReadOutgoingEdgesInstruction(
-                    a_tmp.create_load())),
-            b_tmp),
-        a_tmp.create_load())
+                    load_a)),
+            load_b),
+        load_a)
 
 def __log(a):
     # Original definition:
@@ -144,18 +147,29 @@ def __log(a):
     #     print("== LOG == " + str(a_value))
     #     raise PrimitiveFinished(a)
 
-    a_tmp = tree_ir.StoreLocalInstruction(None, a)
+    store_a, load_a = tree_ir.evaluate_and_load(a)
     return tree_ir.CompoundInstruction(
         tree_ir.create_block(
-            a_tmp,
+            store_a,
             tree_ir.PrintInstruction(
                 tree_ir.BinaryInstruction(
                     tree_ir.LiteralInstruction("== LOG == "),
                     '+',
                     tree_ir.CallInstruction(
                         tree_ir.LoadGlobalInstruction('str'),
-                        [tree_ir.ReadValueInstruction(a_tmp.create_load())])))),
-        a_tmp.create_load())
+                        [tree_ir.ReadValueInstruction(load_a)])))),
+        load_a)
+
+def __read_nr_out(a):
+    # Original definition:
+    #
+    # def read_nr_out(a, **remainder):
+    #     outgoing, = yield [("RO", [a])]
+    #     result, = yield [("CNV", [len(outgoing)])]
+    #     raise PrimitiveFinished(result)
+
+    return tree_ir.CreateNodeWithValueInstruction(
+        create_get_length(tree_ir.ReadOutgoingEdgesInstruction(a)))
 
 MISC_INTRINSICS = {
     # Reference equality
@@ -235,18 +249,20 @@ MISC_INTRINSICS = {
                 'is not',
                 tree_ir.LiteralInstruction(None))),
 
+    'read_nr_out' : __read_nr_out,
+
     # read_root
     'read_root' :
         lambda:
         tree_ir.LoadIndexInstruction(
-            tree_ir.LoadLocalInstruction(jit.KWARGS_PARAMETER_NAME),
+            tree_ir.LoadLocalInstruction(jit_runtime.KWARGS_PARAMETER_NAME),
             tree_ir.LiteralInstruction('root')),
 
     # read_taskroot
     'read_taskroot' :
         lambda:
         tree_ir.LoadIndexInstruction(
-            tree_ir.LoadLocalInstruction(jit.KWARGS_PARAMETER_NAME),
+            tree_ir.LoadLocalInstruction(jit_runtime.KWARGS_PARAMETER_NAME),
             tree_ir.LiteralInstruction('task_root')),
 
     # Dictionary operations
@@ -261,15 +277,13 @@ MISC_INTRINSICS = {
             a, tree_ir.ReadValueInstruction(b)),
 
     'dict_add' : __dict_add,
+    'dict_len' : __read_nr_out,
 
     # Set operations
     'set_add' : __set_add,
 
     # List operations
-    'list_len' :
-        lambda a:
-        tree_ir.CreateNodeWithValueInstruction(
-            create_get_length(tree_ir.ReadOutgoingEdgesInstruction(a))),
+    'list_len' : __read_nr_out,
 
     'list_read' : __list_read,
     'list_append' : __list_append,
@@ -278,6 +292,154 @@ MISC_INTRINSICS = {
     'log' : __log
 }
 
+def __read_nr_out_cfg(original_def, a):
+    # Original definition:
+    #
+    # def read_nr_out(a, **remainder):
+    #     outgoing, = yield [("RO", [a])]
+    #     result, = yield [("CNV", [len(outgoing)])]
+    #     raise PrimitiveFinished(result)
+
+    original_def.redefine(
+        cfg_ir.CreateNode(
+            original_def.insert_before(
+                cfg_ir.create_pure_simple_call(
+                    'len',
+                    original_def.insert_before(
+                        cfg_ir.create_read_outgoing_edges(a))))))
+
+def __dict_in_cfg(original_def, a, b):
+    # Original definition:
+    #
+    # def dict_in(a, b, **remainder):
+    #     b_value, = yield [("RV", [b])]
+    #     value, = yield [("RD", [a, b_value])]
+    #     is_in = value is not None
+    #     result, = yield [("CNV", [is_in])]
+    #     raise PrimitiveFinished(result)
+
+    original_def.redefine(
+        cfg_ir.CreateNode(
+            original_def.insert_before(
+                cfg_ir.Binary(
+                    original_def.insert_before(
+                        cfg_ir.create_read_dict_value(
+                            a, original_def.insert_before(cfg_ir.Read(b)))),
+                    'is not',
+                    original_def.insert_before(cfg_ir.Literal(None))))))
+
+def __dict_in_node_cfg(original_def, a, b):
+    # Original definition:
+    #
+    # def dict_in_node(a, b, **remainder):
+    #     value, = yield [("RDN", [a, b])]
+    #     result, = yield [("CNV", [value is not None])]
+    #     raise PrimitiveFinished(result)
+
+    original_def.redefine(
+        cfg_ir.CreateNode(
+            original_def.insert_before(
+                cfg_ir.Binary(
+                    original_def.insert_before(cfg_ir.create_read_dict_node(a, b)),
+                    'is not',
+                    original_def.insert_before(cfg_ir.Literal(None))))))
+
+def __dict_read_cfg(original_def, a, b):
+    # Original definition:
+    #
+    # def dict_read(a, b, **remainder):
+    #     b_value, = yield [("RV", [b])]
+    #     result, = yield [("RD", [a, b_value])]
+    #     raise PrimitiveFinished(result)
+
+    original_def.redefine(
+        cfg_ir.create_read_dict_value(
+            a,
+            original_def.insert_before(cfg_ir.Read(b))))
+
+MISC_CFG_INTRINSICS = {
+    # Reference equality
+    'element_eq' :
+        lambda original_def, a, b:
+        original_def.redefine(
+            cfg_ir.CreateNode(
+                original_def.insert_before(
+                    cfg_ir.Binary(a, '==', b)))),
+    'element_neq' :
+        lambda original_def, a, b:
+        original_def.redefine(
+            cfg_ir.CreateNode(
+                original_def.insert_before(
+                    cfg_ir.Binary(a, '!=', b)))),
+
+
+    # String operations
+    'string_get' :
+        lambda original_def, a, b:
+        original_def.redefine(
+            cfg_ir.CreateNode(
+                original_def.insert_before(
+                    cfg_ir.create_index(
+                        original_def.insert_before(cfg_ir.Read(a)),
+                        original_def.insert_before(cfg_ir.Read(b)))))),
+    'string_len' :
+        lambda original_def, a:
+        original_def.redefine(
+            cfg_ir.CreateNode(
+                original_def.insert_before(
+                    cfg_ir.create_pure_simple_call(
+                        'len',
+                        original_def.insert_before(cfg_ir.Read(a)))))),
+    'string_join' :
+        lambda original_def, a, b:
+        original_def.redefine(
+            cfg_ir.CreateNode(
+                original_def.insert_before(
+                    cfg_ir.Binary(
+                        original_def.insert_before(
+                            cfg_ir.create_pure_simple_call(
+                                'str',
+                                original_def.insert_before(cfg_ir.Read(a)))),
+                        '+',
+                        original_def.insert_before(
+                            cfg_ir.create_pure_simple_call(
+                                'str',
+                                original_def.insert_before(cfg_ir.Read(b)))))))),
+    'string_startswith' :
+        lambda original_def, a, b:
+        original_def.redefine(
+            cfg_ir.CreateNode(
+                original_def.insert_before(
+                    cfg_ir.DirectFunctionCall(
+                        'startswith',
+                        [('self', original_def.insert_before(cfg_ir.Read(a))),
+                         ('substring', original_def.insert_before(cfg_ir.Read(b)))],
+                        calling_convention=cfg_ir.SELF_POSITIONAL_CALLING_CONVENTION,
+                        has_value=True, has_side_effects=False)))),
+
+    # State creation
+    'create_node' :
+        lambda original_def:
+        original_def.redefine(
+            cfg_ir.CreateNode(original_def.insert_before(cfg_ir.Literal(None)))),
+    'create_value' :
+        lambda original_def, a:
+        original_def.redefine(
+            cfg_ir.CreateNode(original_def.insert_before(cfg_ir.Read(a)))),
+
+    # State reads
+    'read_nr_out' : __read_nr_out_cfg,
+
+    # Dictionary operations
+    'dict_len' : __read_nr_out_cfg,
+    'dict_read' : __dict_read_cfg,
+    'dict_in' : __dict_in_cfg,
+    'dict_in_node' : __dict_in_node_cfg,
+
+    # List operations
+    'list_len' : __read_nr_out_cfg
+}
+
 def register_time_intrinsic(target_jit):
     """Registers the time() intrinsic with the given JIT."""
     import_name = target_jit.import_value(time.time, 'time')
@@ -298,5 +460,7 @@ def register_intrinsics(target_jit):
         target_jit.register_cast_intrinsic(key, value)
     for (key, value) in MISC_INTRINSICS.items():
         target_jit.register_intrinsic(key, value)
+    for (key, value) in MISC_CFG_INTRINSICS.items():
+        target_jit.register_cfg_intrinsic(key, value)
 
     register_time_intrinsic(target_jit)

File diff suppressed because it is too large
+ 632 - 777
kernel/modelverse_jit/jit.py


+ 74 - 5
kernel/modelverse_jit/runtime.py

@@ -10,6 +10,59 @@ MUTABLE_FUNCTION_KEY = "mutable"
 FUNCTION_BODY_KEY = "body"
 """A dictionary key for function bodies."""
 
+KWARGS_PARAMETER_NAME = "kwargs"
+"""The name of the kwargs parameter in jitted functions."""
+
+CALL_FUNCTION_NAME = "__call_function"
+"""The name of the '__call_function' function, in the jitted function scope."""
+
+GET_INPUT_FUNCTION_NAME = "__get_input"
+"""The name of the '__get_input' function, in the jitted function scope."""
+
+JIT_THUNK_CONSTANT_FUNCTION_NAME = "__jit_thunk_constant_function"
+"""The name of the jit_thunk_constant_function function in the JIT's global context."""
+
+JIT_THUNK_GLOBAL_FUNCTION_NAME = "__jit_thunk_global"
+"""The name of the jit_thunk_global function in the JIT's global context."""
+
+JIT_REJIT_FUNCTION_NAME = "__jit_rejit"
+"""The name of the rejit function in the JIT's global context."""
+
+JIT_COMPILE_FUNCTION_BODY_FAST_FUNCTION_NAME = "__jit_compile_function_body_fast"
+"""The name of the compile_function_body_fast function in the JIT's global context."""
+
+UNREACHABLE_FUNCTION_NAME = "__unreachable"
+"""The name of the unreachable function in the JIT's global context."""
+
+LOCALS_NODE_NAME = "jit_locals"
+"""The name of the node that is connected to all JIT locals in a given function call."""
+
+LOCALS_EDGE_NAME = "jit_locals_edge"
+"""The name of the edge that connects the LOCALS_NODE_NAME node to a user root."""
+
+GLOBAL_NOT_FOUND_MESSAGE_FORMAT = "Not found as global: %s"
+"""The format of the 'not found as global' message. Takes a single argument."""
+
+BASELINE_JIT_ORIGIN_NAME = "baseline-jit"
+"""The origin name for functions that were produced by the baseline JIT."""
+
+FAST_JIT_ORIGIN_NAME = "fast-jit"
+"""The origin name for functions that were produced by the fast JIT."""
+
+def format_stack_frame(function_name, debug_info, origin='unknown'):
+    """Formats a stack frame, which consists of a function name, debug
+       information and an origin."""
+    if function_name is None:
+        function_name = 'unknown function'
+    if debug_info is None:
+        debug_info = '[unknown location] '
+
+    return '%sin %s (%s)' % (debug_info, function_name, origin)
+
+def format_trace_message(debug_info, function_name, origin='unknown'):
+    """Creates a formatted trace message."""
+    return 'TRACE: %s' % format_stack_frame(function_name, debug_info, origin)
+
 def call_function(function_id, named_arguments, **kwargs):
     """Runs the function with the given id, passing it the specified argument dictionary."""
     task_root = kwargs['task_root']
@@ -22,9 +75,9 @@ def call_function(function_id, named_arguments, **kwargs):
     # frame.
     def handle_jit_failed(_):
         """Interprets the function."""
-        interpreter_args = {'function_id' : function_id, 'named_arguments' : named_arguments}
+        interpreter_args = {'body_id' : body_id, 'named_arguments' : named_arguments}
         interpreter_args.update(kwargs)
-        yield [("TAIL_CALL_KWARGS", [interpret_function, interpreter_args])]
+        yield [("TAIL_CALL_KWARGS", [interpret_function_body, interpreter_args])]
 
     if is_mutable is not None:
         kernel.jit.mark_no_jit(body_id)
@@ -45,10 +98,18 @@ def call_function(function_id, named_arguments, **kwargs):
 def interpret_function(function_id, named_arguments, **kwargs):
     """Makes the interpreter run the function with the given id for the specified
        argument dictionary."""
+    body_id, = yield [("RD", [function_id, FUNCTION_BODY_KEY])]
+    args = {'body_id' : body_id, named_arguments : named_arguments}
+    args.update(kwargs)
+    yield [("TAIL_CALL_KWARGS", [interpret_function_body, args])]
+
+def interpret_function_body(body_id, named_arguments, **kwargs):
+    """Makes the interpreter run the function body with the given id for the specified
+       argument dictionary."""
     task_root = kwargs['task_root']
     kernel = kwargs['mvk']
-    task_frame, = yield [("RD", [task_root, "frame"])]
-    inst, body_id = yield [("RD", [task_frame, "IP"]), ("RD", [function_id, FUNCTION_BODY_KEY])]
+    user_frame, = yield [("RD", [task_root, "frame"])]
+    inst, = yield [("RD", [user_frame, "IP"])]
     kernel.jit.mark_entry_point(body_id)
 
     # Create a new stack frame.
@@ -71,7 +132,7 @@ def interpret_function(function_id, named_arguments, **kwargs):
                            ("CD", [new_frame, "caller", inst]),
                            ("CD", [new_frame, "phase", new_phase]),
                            ("CD", [new_frame, "IP", body_id]),
-                           ("CD", [new_frame, "prev", task_frame]),
+                           ("CD", [new_frame, "prev", user_frame]),
                            ("CD", [
                                new_frame,
                                primitive_functions.EXCEPTION_RETURN_KEY,
@@ -104,6 +165,14 @@ def interpret_function(function_id, named_arguments, **kwargs):
         # An instruction has completed. Forward it.
         yield result
 
+class UnreachableCodeException(Exception):
+    """The type of exception that is thrown when supposedly unreachable code is executed."""
+    pass
+
+def unreachable():
+    """Marks unreachable code."""
+    raise UnreachableCodeException('An unreachable statement was reached.')
+
 def get_input(**parameters):
     """Retrieves input."""
     mvk = parameters["mvk"]

+ 52 - 0
kernel/modelverse_jit/source_map.py

@@ -0,0 +1,52 @@
+"""Defines source maps: dictionaries that map lines in generated source to debug information."""
+
+class SourceMap(object):
+    """A source map, which converts generated source lines to debug information."""
+    def __init__(self):
+        self.lines = {}
+
+    def map_line(self, line_number, debug_info):
+        """Assigns the given debug information to the given line."""
+        self.lines[line_number] = debug_info
+
+    def get_debug_info(self, line_number):
+        """Gets the debug information for the given line number, or None if no debug info was
+           found."""
+        if line_number in self.lines:
+            return self.lines[line_number]
+        else:
+            return None
+
+    def __str__(self):
+        return '\n'.join(
+            ['%d: %s' % pair
+             for pair in sorted(self.lines.items(), key=lambda (key, _): key)])
+
+class SourceMapBuilder(object):
+    """A type of object that makes it easy to build source maps for hierarchical instructions."""
+    def __init__(self, initial_line_number=0):
+        self.source_map = SourceMap()
+        self.debug_info_stack = []
+        self.line_number = initial_line_number - 1
+
+    def push_debug_info(self, debug_info):
+        """Informs the source map that subsequent lines of code will have the given debug
+           information associated with them."""
+        self.debug_info_stack.append(debug_info)
+
+    def pop_debug_info(self):
+        """Informs the source map that the debug information that was pushed last should
+           be discarded in favor of the debug information that was pushed onto the stack
+           just prior to it."""
+        return self.debug_info_stack.pop()
+
+    def append_line(self):
+        """Has the source map builder increment its line number counter, and assigns the debug
+           information that is at the top of the debug information stack to that line."""
+        self.line_number += 1
+        if len(self.debug_info_stack) > 0:
+            self.source_map.map_line(self.line_number, self.debug_info_stack[-1])
+
+    def __str__(self):
+        return str(self.source_map)
+

+ 467 - 79
kernel/modelverse_jit/tree_ir.py

@@ -28,6 +28,8 @@
 # Let's just agree to disagree on map vs list comprehensions, pylint.
 # pylint: disable=I0011,W0141
 
+import modelverse_jit.source_map as source_map
+
 NOP_LITERAL = None
 """A literal that results in a nop during which execution may be interrupted
    when yielded."""
@@ -108,6 +110,10 @@ class Instruction(object):
            instruction if it is not None."""
         return None
 
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return True
+
     def generate_python_def(self, code_generator):
         """Generates a Python statement that executes this instruction.
            The statement is appended immediately to the code generator."""
@@ -150,7 +156,6 @@ class Instruction(object):
 
 class PythonGenerator(object):
     """Generates Python code from instructions."""
-
     def __init__(self, combine_state_definitions=True):
         self.code = []
         self.state_definitions = []
@@ -159,11 +164,14 @@ class PythonGenerator(object):
         self.indentation = 0
         self.result_name_dict = {}
         self.combine_state_definitions = combine_state_definitions
+        self.source_map_builder = source_map.SourceMapBuilder()
 
     def append(self, text):
         """Appends the given string to this code generator."""
         self.flush_state_definitions()
         self.code.append(text)
+        for _ in xrange(text.count('\n')):
+            self.source_map_builder.append_line()
 
     def append_indentation(self):
         """Appends indentation to the code generator."""
@@ -225,7 +233,7 @@ class PythonGenerator(object):
             # Generate the rhs' definition.
             rhs.generate_python_def(self)
             # Only perform an assignment if it's truly necessary.
-            if lhs_result_name != rhs_result_name:
+            if lhs_result_name != rhs_result_name or not rhs.has_result_temporary():
                 self.append_definition(lhs, rhs)
         else:
             self.append_definition(lhs, rhs)
@@ -284,6 +292,9 @@ class EmptyInstruction(VoidInstruction):
         """Tells if this instruction requires a definition."""
         return False
 
+    def __repr__(self):
+        return "EmptyInstruction()"
+
 class SelectInstruction(Instruction):
     """Represents a select-instruction: an instruction that defines one of two
        child instructions, and sets its result to the defined child's result."""
@@ -315,14 +326,15 @@ class SelectInstruction(Instruction):
         condition, if_clause, else_clause = new_children
         return SelectInstruction(condition, if_clause, else_clause)
 
-    def generate_python_def(self, code_generator):
+    def generate_python_if(self, code_generator, is_elif=False):
         """Generates Python code for this instruction."""
         if_has_result = self.has_result()
         if self.condition.has_definition():
             self.condition.generate_python_def(code_generator)
 
         code_generator.append_line(
-            'if ' + self.condition.generate_python_use(code_generator) + ':')
+            ('elif ' if is_elif else 'if ') +
+            self.condition.generate_python_use(code_generator) + ':')
         code_generator.increase_indentation()
         if if_has_result:
             code_generator.append_move_definition(self, self.if_clause)
@@ -331,13 +343,28 @@ class SelectInstruction(Instruction):
         code_generator.decrease_indentation()
         else_has_def = self.else_clause.has_definition()
         if else_has_def or if_has_result:
-            code_generator.append_line('else:')
-            code_generator.increase_indentation()
-            if if_has_result:
-                code_generator.append_move_definition(self, self.else_clause)
+            if (isinstance(self.else_clause, SelectInstruction) and
+                    not self.else_clause.condition.has_definition()):
+                self.else_clause.generate_python_if(code_generator, True)
+                if if_has_result:
+                    code_generator.increase_indentation()
+                    code_generator.append_definition(self, self.else_clause)
+                    code_generator.decrease_indentation()
             else:
-                self.else_clause.generate_python_def(code_generator)
-            code_generator.decrease_indentation()
+                code_generator.append_line('else:')
+                code_generator.increase_indentation()
+                if if_has_result:
+                    code_generator.append_move_definition(self, self.else_clause)
+                else:
+                    self.else_clause.generate_python_def(code_generator)
+                code_generator.decrease_indentation()
+
+    def generate_python_def(self, code_generator):
+        """Generates Python code for this instruction."""
+        return self.generate_python_if(code_generator)
+
+    def __repr__(self):
+        return "SelectInstruction(%r, %r, %r)" % (self.condition, self.if_clause, self.else_clause)
 
 class ReturnInstruction(VoidInstruction):
     """Represents a return-instruction."""
@@ -387,6 +414,9 @@ class ReturnInstruction(VoidInstruction):
             self.value.generate_python_use(code_generator) +
             ')')
 
+    def __repr__(self):
+        return "ReturnInstruction(%r)" % self.value
+
 class RaiseInstruction(VoidInstruction):
     """An instruction that raises an error."""
     def __init__(self, value):
@@ -408,6 +438,9 @@ class RaiseInstruction(VoidInstruction):
         code_generator.append_line(
             'raise ' + self.value.generate_python_use(code_generator))
 
+    def __repr__(self):
+        return "RaiseInstruction(%r)" % self.value
+
 class CallInstruction(Instruction):
     """An instruction that performs a simple call."""
     def __init__(self, target, argument_list):
@@ -438,11 +471,15 @@ class CallInstruction(Instruction):
                 self.target.generate_python_use(code_generator),
                 ', '.join([arg.generate_python_use(code_generator) for arg in self.argument_list])))
 
+    def __repr__(self):
+        return "CallInstruction(%r, %r)" % (self.target, self.argument_list)
+
 class PrintInstruction(VoidInstruction):
     """An instruction that prints a value."""
     def __init__(self, argument):
         VoidInstruction.__init__(self)
         self.argument = argument
+        assert isinstance(argument, Instruction)
 
     def get_children(self):
         """Gets this instruction's sequence of child instructions."""
@@ -462,6 +499,57 @@ class PrintInstruction(VoidInstruction):
             'print(%s)' % (
                 self.argument.generate_python_use(code_generator)))
 
+    def __repr__(self):
+        return "PrintInstruction(%r)" % self.argument
+
+class DebugInfoInstruction(Instruction):
+    """An instruction that defines debug information for its child instruction."""
+    def __init__(self, value, debug_info):
+        Instruction.__init__(self)
+        self.value = value
+        self.debug_info = debug_info
+
+    def has_definition_impl(self):
+        """Tells if this instruction requires a definition."""
+        return self.value.has_definition()
+
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return self.value.has_result_temporary()
+
+    def get_result_type_impl(self):
+        """Gets this instruction's result type."""
+        return self.value.get_result_type()
+
+    def get_result_name_override(self, code_generator):
+        """Gets a value that overrides the code generator's result name for this
+           instruction if it is not None."""
+        return self.value.get_result_name_override(code_generator)
+
+    def get_children(self):
+        """Gets this instruction's sequence of child instructions."""
+        return [self.value]
+
+    def create(self, new_children):
+        """Creates a new instruction of this type from the given sequence of child instructions."""
+        arg, = new_children
+        return DebugInfoInstruction(arg, self.debug_info)
+
+    def generate_python_def(self, code_generator):
+        """Generates Python code for this instruction."""
+        if self.has_definition():
+            code_generator.source_map_builder.push_debug_info(self.debug_info)
+            code_generator.append_move_definition(self, self.value)
+            code_generator.source_map_builder.pop_debug_info()
+
+    def generate_python_use(self, code_generator):
+        """Generates a Python expression that retrieves this instruction's
+           result. The expression is returned as a string."""
+        return self.value.generate_python_use(code_generator)
+
+    def __repr__(self):
+        return "DebugInfoInstruction(%r, %r)" % (self.value, self.debug_info)
+
 class BinaryInstruction(Instruction):
     """An instruction that performs a binary operation."""
     def __init__(self, lhs, operator, rhs):
@@ -492,6 +580,10 @@ class BinaryInstruction(Instruction):
         else:
             return self
 
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return False
+
     def generate_python_use(self, code_generator):
         """Generates a Python expression that retrieves this instruction's
            result. The expression is returned as a string."""
@@ -503,15 +595,19 @@ class BinaryInstruction(Instruction):
     def generate_python_def(self, code_generator):
         """Generates a Python statement that executes this instruction.
            The statement is appended immediately to the code generator."""
-        if self.lhs.has_definition():
+        lhs_has_def, rhs_has_def = self.lhs.has_definition(), self.rhs.has_definition()
+        if lhs_has_def:
             self.lhs.generate_python_def(code_generator)
-            if self.rhs.has_definition():
+            if rhs_has_def:
                 self.rhs.generate_python_def(code_generator)
-        elif self.rhs.has_definition():
+        elif rhs_has_def:
             self.rhs.generate_python_def(code_generator)
         else:
             code_generator.append_line('pass')
 
+    def __repr__(self):
+        return "BinaryInstruction(%r, %r, %r)" % (self.lhs, self.operator, self.rhs)
+
 class UnaryInstruction(Instruction):
     """An instruction that performs a unary operation."""
     def __init__(self, operator, operand):
@@ -527,6 +623,10 @@ class UnaryInstruction(Instruction):
         """Gets this instruction's sequence of child instructions."""
         return [self.operand]
 
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return False
+
     def create(self, new_children):
         """Creates a new instruction of this type from the given sequence of child instructions."""
         operand, = new_children
@@ -556,9 +656,11 @@ class UnaryInstruction(Instruction):
         else:
             code_generator.append_line('pass')
 
+    def __repr__(self):
+        return "UnaryInstruction(%r, %r)" % (self.operator, self.operand)
+
 class LoopInstruction(VoidInstruction):
     """Represents a loop-instruction, which loops until broken."""
-
     def __init__(self, body):
         VoidInstruction.__init__(self)
         self.body = body
@@ -579,18 +681,61 @@ class LoopInstruction(VoidInstruction):
         self.body.generate_python_def(code_generator)
         code_generator.decrease_indentation()
 
+    def __repr__(self):
+        return "LoopInstruction(%r)" % self.body
+
 class BreakInstruction(VoidInstruction):
     """Represents a break-instruction."""
     def generate_python_def(self, code_generator):
         """Generates Python code for this instruction."""
         code_generator.append_line('break')
 
+    def __repr__(self):
+        return "BreakInstruction()"
+
 class ContinueInstruction(VoidInstruction):
     """Represents a continue-instruction."""
     def generate_python_def(self, code_generator):
         """Generates Python code for this instruction."""
         code_generator.append_line('continue')
 
+    def __repr__(self):
+        return "ContinueInstruction()"
+
+class IgnoreInstruction(VoidInstruction):
+    """Represents an instruction that evaluates its operand, and then discards
+       the result."""
+    def __init__(self, value):
+        VoidInstruction.__init__(self)
+        self.value = value
+
+    def has_definition_impl(self):
+        """Tells if this instruction requires a definition."""
+        return self.value.has_definition()
+
+    def get_children(self):
+        """Gets this instruction's sequence of child instructions."""
+        return [self.value]
+
+    def create(self, new_children):
+        """Creates a new instruction of this type from the given sequence of child instructions."""
+        value, = new_children
+        return IgnoreInstruction(value)
+
+    def simplify_node(self):
+        """Applies basic simplification to this instruction and its children."""
+        if not self.value.has_result():
+            return self.value
+        else:
+            return self
+
+    def generate_python_def(self, code_generator):
+        """Generates Python code for this instruction."""
+        self.value.generate_python_def(code_generator)
+
+    def __repr__(self):
+        return "IgnoreInstruction(%r)" % self.value
+
 class CompoundInstruction(Instruction):
     """Represents an instruction that evaluates two other instructions
        in order, and returns the second instruction's result."""
@@ -637,6 +782,9 @@ class CompoundInstruction(Instruction):
             self.first.generate_python_def(code_generator)
             self.second.generate_python_def(code_generator)
 
+    def __repr__(self):
+        return "CompoundInstruction(%r, %r)" % (self.first, self.second)
+
 class LiteralInstruction(Instruction):
     """Represents an integer, floating-point, string or Boolean literal."""
     def __init__(self, literal):
@@ -647,6 +795,10 @@ class LiteralInstruction(Instruction):
         """Tells if this instruction requires a definition."""
         return False
 
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return False
+
     def get_children(self):
         """Gets this instruction's sequence of child instructions."""
         return []
@@ -660,6 +812,9 @@ class LiteralInstruction(Instruction):
            result. The expression is returned as a string."""
         return repr(self.literal)
 
+    def __repr__(self):
+        return "LiteralInstruction(%r)" % self.literal
+
 class DictionaryLiteralInstruction(Instruction):
     """Constructs a dictionary literal."""
     def __init__(self, key_value_pairs):
@@ -668,12 +823,18 @@ class DictionaryLiteralInstruction(Instruction):
 
     def get_children(self):
         """Gets this instruction's sequence of child instructions."""
-        return [val for _, val in self.key_value_pairs]
+        results = []
+        for key, val in self.key_value_pairs:
+            results.append(key)
+            results.append(val)
+        return results
 
     def create(self, new_children):
         """Creates a new instruction of this type from the given sequence of child instructions."""
-        keys = [k for k, _ in self.key_value_pairs]
-        return DictionaryLiteralInstruction(zip(keys, new_children))
+        new_kv_pairs = []
+        for i in xrange(len(self.key_value_pairs)):
+            new_kv_pairs.append((new_children[2 * i], new_children[2 * i + 1]))
+        return DictionaryLiteralInstruction(new_kv_pairs)
 
     def has_definition_impl(self):
         """Tells if this instruction requires a definition."""
@@ -681,42 +842,92 @@ class DictionaryLiteralInstruction(Instruction):
             [key.has_definition() or val.has_definition()
              for key, val in self.key_value_pairs])
 
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return False
+
     def simplify(self):
         """Applies basic simplification to this instruction and its children."""
         return DictionaryLiteralInstruction(
             [(key.simplify(), val.simplify()) for key, val in self.key_value_pairs])
 
-    def generate_dictionary_expr(self, code_generator):
-        """Generates an expression that creates this dictionary."""
+    def generate_python_def(self, code_generator):
+        """Generates a Python statement that executes this instruction.
+            The statement is appended immediately to the code generator."""
+        for key, val in self.key_value_pairs:
+            if key.has_definition():
+                key.generate_python_def(code_generator)
+            if val.has_definition():
+                val.generate_python_def(code_generator)
+
+    def generate_python_use(self, code_generator):
+        """Generates a Python expression that retrieves this instruction's
+           result. The expression is returned as a string."""
         return '{%s}' % ', '.join(
             ['%s : %s' % (
                 key.generate_python_use(code_generator),
                 val.generate_python_use(code_generator))
              for key, val in self.key_value_pairs])
 
+    def __repr__(self):
+        return "DictionaryLiteralInstruction(%r)" % self.key_value_pairs
+
+class ListSliceInstruction(Instruction):
+    """Slices a list."""
+    def __init__(self, seq, start, end, step):
+        Instruction.__init__(self)
+        self.seq = seq
+        self.start = start
+        self.end = end
+        self.step = step
+
+    def get_children(self):
+        """Gets this instruction's sequence of child instructions."""
+        all_items = (self.seq, self.start, self.end, self.step)
+        return [item for item in all_items if item is not None]
+
+    def create(self, new_children):
+        """Creates a new instruction of this type from the given sequence of child instructions."""
+        # pylint: disable=I0011,E1120
+        args = []
+        i = 0
+        for old_item in (self.seq, self.start, self.end, self.step):
+            if old_item is None:
+                args.append(None)
+            else:
+                args.append(new_children[i])
+                i += 1
+
+        assert len(new_children) == i
+        assert len(args) == 4
+        return ListSliceInstruction(*args)
+
+    def has_definition_impl(self):
+        """Tells if this instruction requires a definition."""
+        return any([item.has_definition() for item in self.get_children()])
+
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return False
+
     def generate_python_def(self, code_generator):
         """Generates a Python statement that executes this instruction.
             The statement is appended immediately to the code generator."""
-        if self.has_definition():
-            for key, val in self.key_value_pairs:
-                if key.has_definition():
-                    key.generate_python_def(code_generator)
-                if val.has_definition():
-                    val.generate_python_def(code_generator)
-
-            code_generator.append_line('%s = %s' % (
-                code_generator.get_result_name(self),
-                self.generate_dictionary_expr(code_generator)))
-        else:
-            code_generator.append_line('pass')
+        for item in self.get_children():
+            if item.has_definition():
+                item.generate_python_def(code_generator)
 
     def generate_python_use(self, code_generator):
         """Generates a Python expression that retrieves this instruction's
            result. The expression is returned as a string."""
-        if self.has_definition():
-            return code_generator.get_result_name(self)
-        else:
-            return self.generate_dictionary_expr(code_generator)
+        return '%s[%s:%s:%s]' % (
+            self.seq.generate_python_use(code_generator),
+            '' if self.start is None else self.start.generate_python_use(code_generator),
+            '' if self.end is None else self.end.generate_python_use(code_generator),
+            '' if self.step is None else self.step.generate_python_use(code_generator))
+
+    def __repr__(self):
+        return "ListSliceInstruction(%r, %r, %r, %r)" % (self.seq, self.start, self.end, self.step)
 
 class StateInstruction(Instruction):
     """An instruction that accesses the modelverse state."""
@@ -743,7 +954,6 @@ class StateInstruction(Instruction):
     def generate_python_def(self, code_generator):
         """Generates a Python statement that executes this instruction.
            The statement is appended immediately to the code generator."""
-
         args = self.get_arguments()
         for arg_i in args:
             if arg_i.has_definition():
@@ -751,6 +961,9 @@ class StateInstruction(Instruction):
 
         code_generator.append_state_definition(self, self.get_opcode(), args)
 
+    def __repr__(self):
+        return "StateInstruction(%s)" % ', '.join(map(repr, self.get_arguments()))
+
 class RunGeneratorFunctionInstruction(StateInstruction):
     """An instruction that runs a generator function."""
     def __init__(self, function, argument_dict, result_type=PRIMITIVE_RESULT_TYPE):
@@ -774,10 +987,27 @@ class RunGeneratorFunctionInstruction(StateInstruction):
 
 class RunTailGeneratorFunctionInstruction(StateInstruction):
     """An instruction that runs a generator function."""
-    def __init__(self, function, argument_dict):
+    def __init__(self, function, argument_dict, result_type=NO_RESULT_TYPE):
         StateInstruction.__init__(self)
         self.function = function
         self.argument_dict = argument_dict
+        self.result_type_cache = result_type
+
+    def get_opcode(self):
+        """Gets the opcode for this state instruction."""
+        return "TAIL_CALL_KWARGS"
+
+    def get_arguments(self):
+        """Gets this state instruction's argument list."""
+        return [self.function, self.argument_dict]
+
+class RegisterDebugInfoInstruction(StateInstruction):
+    """An instruction that sends a DEBUG_INFO request to the request handler."""
+    def __init__(self, function_name, function_source_map, function_origin):
+        StateInstruction.__init__(self)
+        self.function_name = function_name
+        self.function_source_map = function_source_map
+        self.function_origin = function_origin
 
     def get_result_type_impl(self):
         """Gets the type of value produced by this instruction."""
@@ -785,11 +1015,11 @@ class RunTailGeneratorFunctionInstruction(StateInstruction):
 
     def get_opcode(self):
         """Gets the opcode for this state instruction."""
-        return "TAIL_CALL_KWARGS"
+        return "DEBUG_INFO"
 
     def get_arguments(self):
         """Gets this state instruction's argument list."""
-        return [self.function, self.argument_dict]
+        return [self.function_name, self.function_source_map, self.function_origin]
 
 class VariableName(object):
     """A data structure that unifies names across instructions that access the
@@ -802,6 +1032,12 @@ class VariableName(object):
            instruction if it is not None."""
         return self.name
 
+    def __str__(self):
+        return self.name
+
+    def __repr__(self):
+        return 'VariableName(%r)' % self.name
+
 class VariableInstruction(Instruction):
     """A base class for instructions that access variables."""
     def __init__(self, name):
@@ -863,6 +1099,60 @@ class StoreLocalInstruction(LocalInstruction):
            The statement is appended immediately to the code generator."""
         code_generator.append_move_definition(self, self.value)
 
+    def __repr__(self):
+        return 'StoreLocalInstruction(%r, %r)' % (self.name, self.value)
+
+class TupleStoreLocalInstruction(VoidInstruction):
+    """Assigns a number of values to a number of variables."""
+    def __init__(self, name_value_pairs):
+        VoidInstruction.__init__(self)
+        self.name_value_pairs = name_value_pairs
+
+    def get_children(self):
+        """Gets this instruction's sequence of child instructions."""
+        return [val for _, val in self.name_value_pairs]
+
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return False
+
+    def create(self, new_children):
+        """Creates a new instruction of this type from the given sequence of child instructions."""
+        new_name_value_pairs = [
+            (name, new_value) for (name, _), new_value in zip(self.name_value_pairs, new_children)]
+        return TupleStoreLocalInstruction(new_name_value_pairs)
+
+    def generate_python_def(self, code_generator):
+        """Generates a Python statement that executes this instruction.
+           The statement is appended immediately to the code generator."""
+        tuple_lhs = []
+        tuple_rhs = []
+        for name, value in self.name_value_pairs:
+            if isinstance(name, str) or isinstance(name, unicode) or name is None:
+                variable = VariableName(name)
+            else:
+                variable = name
+
+            # Retrieve the result name for the variable.
+            var_result_name = code_generator.get_result_name(variable)
+            # Encourage the value to take on the same result name as the variable.
+            value_result_name = code_generator.get_result_name(value, var_result_name)
+            if value.has_definition():
+                # Generate the value' definition.
+                value.generate_python_def(code_generator)
+
+            # Only perform an assignment if it's truly necessary.
+            if var_result_name != value_result_name or not value.has_result_temporary():
+                tuple_lhs.append(var_result_name)
+                tuple_rhs.append(value.generate_python_use(code_generator))
+
+        if len(tuple_lhs) > 0:
+            code_generator.append_line(
+                '%s = %s' % (', '.join(tuple_lhs), ', '.join(tuple_rhs)))
+
+    def __repr__(self):
+        return 'TupleStoreLocalInstruction(%r)' % (self.name_value_pairs)
+
 class LoadLocalInstruction(LocalInstruction):
     """An instruction that loads a value from a local variable."""
     def has_definition_impl(self):
@@ -877,6 +1167,9 @@ class LoadLocalInstruction(LocalInstruction):
         """Creates a new instruction of this type from the given sequence of child instructions."""
         return self
 
+    def __repr__(self):
+        return 'LoadLocalInstruction(%r)' % self.name
+
 class DefineFunctionInstruction(VariableInstruction):
     """An instruction that defines a function."""
     def __init__(self, name, parameter_list, body):
@@ -902,6 +1195,9 @@ class DefineFunctionInstruction(VariableInstruction):
         self.body.generate_python_def(code_generator)
         code_generator.decrease_indentation()
 
+    def __repr__(self):
+        return 'DefineFunctionInstruction(%r, %r, %r)' % (self.name, self.parameter_list, self.body)
+
 class LocalExistsInstruction(LocalInstruction):
     """An instruction that checks if a local variable exists."""
     def has_definition_impl(self):
@@ -921,6 +1217,9 @@ class LocalExistsInstruction(LocalInstruction):
            result. The expression is returned as a string."""
         return "'%s' in locals()" % self.get_result_name_override(code_generator)
 
+    def __repr__(self):
+        return 'LocalExistsInstruction(%r)' % self.name
+
 class LoadGlobalInstruction(VariableInstruction):
     """An instruction that loads a value from a global variable."""
     def has_definition_impl(self):
@@ -935,6 +1234,58 @@ class LoadGlobalInstruction(VariableInstruction):
         """Creates a new instruction of this type from the given sequence of child instructions."""
         return self
 
+    def __repr__(self):
+        return 'LoadGlobalInstruction(%r)' % self.name
+
+class StoreGlobalInstruction(VariableInstruction):
+    """An instruction that assigns a value to a global variable."""
+    def __init__(self, name, value):
+        VariableInstruction.__init__(self, name)
+        self.value = value
+
+    def get_children(self):
+        """Gets this instruction's sequence of child instructions."""
+        return [self.value]
+
+    def create(self, new_children):
+        """Creates a new instruction of this type from the given sequence of child instructions."""
+        val, = new_children
+        return StoreGlobalInstruction(self.name, val)
+
+    def generate_python_def(self, code_generator):
+        """Generates a Python statement that executes this instruction.
+           The statement is appended immediately to the code generator."""
+        code_generator.append_move_definition(self, self.value)
+
+    def __repr__(self):
+        return 'StoreGlobalInstruction(%r, %r)' % (self.name, self.value)
+
+class DeclareGlobalInstruction(VariableInstruction):
+    """An instruction that declares a name as a global variable."""
+    def get_children(self):
+        """Gets this instruction's sequence of child instructions."""
+        return []
+
+    def get_result_type_impl(self):
+        """Gets the type of value produced by this instruction."""
+        return NO_RESULT_TYPE
+
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return False
+
+    def create(self, new_children):
+        """Creates a new instruction of this type from the given sequence of child instructions."""
+        return self
+
+    def generate_python_def(self, code_generator):
+        """Generates a Python statement that executes this instruction.
+           The statement is appended immediately to the code generator."""
+        code_generator.append_line('global %s' % self.name)
+
+    def __repr__(self):
+        return 'DeclareGlobalInstruction(%r)' % self.name
+
 class LoadIndexInstruction(Instruction):
     """An instruction that produces a value by indexing a specified expression with
        a given key."""
@@ -945,12 +1296,16 @@ class LoadIndexInstruction(Instruction):
 
     def has_definition_impl(self):
         """Tells if this instruction requires a definition."""
-        return False
+        return self.indexed.has_definition() or self.key.has_definition()
 
     def get_children(self):
         """Gets this instruction's sequence of child instructions."""
         return [self.indexed, self.key]
 
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return False
+
     def create(self, new_children):
         """Creates a new instruction of this type from the given sequence of child instructions."""
         indexed, key = new_children
@@ -959,15 +1314,25 @@ class LoadIndexInstruction(Instruction):
     def generate_python_use(self, code_generator):
         """Generates a Python expression that retrieves this instruction's
            result. The expression is returned as a string."""
-        if self.indexed.has_definition():
+        return "%s[%s]" % (
+            self.indexed.generate_python_use(code_generator),
+            self.key.generate_python_use(code_generator))
+
+    def generate_python_def(self, code_generator):
+        """Generates a Python statement that executes this instruction.
+           The statement is appended immediately to the code generator."""
+        indexed_has_def, key_has_def = self.indexed.has_definition(), self.key.has_definition()
+        if indexed_has_def:
             self.indexed.generate_python_def(code_generator)
 
-        if self.key.has_definition():
+        if key_has_def:
             self.key.generate_python_def(code_generator)
 
-        return "%s[%s]" % (
-            self.indexed.generate_python_use(code_generator),
-            self.key.generate_python_use(code_generator))
+        if not indexed_has_def and not key_has_def:
+            code_generator.append_line('pass')
+
+    def __repr__(self):
+        return 'LoadIndexInstruction(%r, %r)' % (self.indexed, self.key)
 
 class LoadMemberInstruction(Instruction):
     """An instruction that produces a value by loading a member from a container."""
@@ -984,6 +1349,10 @@ class LoadMemberInstruction(Instruction):
         """Gets this instruction's sequence of child instructions."""
         return [self.container]
 
+    def has_result_temporary(self):
+        """Tells if this instruction stores its result in a temporary."""
+        return False
+
     def create(self, new_children):
         """Creates a new instruction of this type from the given sequence of child instructions."""
         container, = new_children
@@ -1001,6 +1370,9 @@ class LoadMemberInstruction(Instruction):
             self.container.generate_python_use(code_generator),
             self.member_name)
 
+    def __repr__(self):
+        return 'LoadMemberInstruction(%r, %r)' % (self.container, self.member_name)
+
 class StoreMemberInstruction(VoidInstruction):
     """An instruction that stores a value in a container member."""
     def __init__(self, container, member_name, value):
@@ -1032,6 +1404,9 @@ class StoreMemberInstruction(VoidInstruction):
             self.member_name,
             self.value.generate_python_use(code_generator)))
 
+    def __repr__(self):
+        return 'StoreMemberInstruction(%r, %r, %r)' % (self.container, self.member_name, self.value)
+
 class NopInstruction(VoidInstruction):
     """A nop instruction, which allows for the kernel's thread of execution to be interrupted."""
     def generate_python_def(self, code_generator):
@@ -1039,6 +1414,9 @@ class NopInstruction(VoidInstruction):
            The statement is appended immediately to the code generator."""
         code_generator.append_line('yield %s' % repr(NOP_LITERAL))
 
+    def __repr__(self):
+        return 'NopInstruction()'
+
 class ReadValueInstruction(StateInstruction):
     """An instruction that reads a value from a node."""
     def __init__(self, node_id):
@@ -1079,6 +1457,21 @@ class ReadDictionaryValueInstruction(StateInstruction):
         """Gets this state instruction's argument list."""
         return [self.node_id, self.key]
 
+class ReadDictionaryNodeInstruction(StateInstruction):
+    """An instruction that reads a dictionary node."""
+    def __init__(self, node_id, key):
+        StateInstruction.__init__(self)
+        self.node_id = node_id
+        self.key = key
+
+    def get_opcode(self):
+        """Gets the opcode for this state instruction."""
+        return "RDN"
+
+    def get_arguments(self):
+        """Gets this state instruction's argument list."""
+        return [self.node_id, self.key]
+
 class ReadDictionaryEdgeInstruction(StateInstruction):
     """An instruction that reads a dictionary edge."""
     def __init__(self, node_id, key):
@@ -1094,6 +1487,20 @@ class ReadDictionaryEdgeInstruction(StateInstruction):
         """Gets this state instruction's argument list."""
         return [self.node_id, self.key]
 
+class ReadDictionaryKeysInstruction(StateInstruction):
+    """An instruction that reads all keys from a dictionary."""
+    def __init__(self, node_id):
+        StateInstruction.__init__(self)
+        self.node_id = node_id
+
+    def get_opcode(self):
+        """Gets the opcode for this state instruction."""
+        return "RDK"
+
+    def get_arguments(self):
+        """Gets this state instruction's argument list."""
+        return [self.node_id]
+
 class ReadEdgeInstruction(StateInstruction):
     """An instruction that reads an edge."""
     def __init__(self, node_id):
@@ -1247,7 +1654,9 @@ def create_block(*statements):
             statements[0],
             create_block(*statements[1:]))
 
-def create_jit_call(target, named_arguments, kwargs):
+def create_jit_call(
+        target, named_arguments, kwargs,
+        create_run_generator=RunGeneratorFunctionInstruction):
     """Creates a call that abides by the JIT's calling convention."""
     # A JIT call looks like this:
     #
@@ -1273,9 +1682,19 @@ def create_jit_call(target, named_arguments, kwargs):
             [kwargs]))
     return CompoundInstruction(
         create_block(*results),
-        RunGeneratorFunctionInstruction(
+        create_run_generator(
             target, arg_dict.create_load(), NODE_RESULT_TYPE))
 
+def evaluate_and_load(value):
+    """Creates a statement that evaluates the given tree, and creates
+       an expression that loads the result. These instructions are returned
+       as a pair of trees."""
+    if isinstance(value, (LoadLocalInstruction, LiteralInstruction)):
+        return EmptyInstruction(), value
+    else:
+        store = StoreLocalInstruction(None, value)
+        return IgnoreInstruction(store), store.create_load()
+
 def create_new_local_node(local_variable, connected_node, edge_variable=None):
     """Creates a local node that is the backing storage for a local variable.
        This node is connected to a given node to make sure it's not perceived
@@ -1286,22 +1705,7 @@ def create_new_local_node(local_variable, connected_node, edge_variable=None):
     if edge_variable is not None:
         create_edge = StoreLocalInstruction(edge_variable, create_edge)
 
-    return create_block(local_store, create_edge)
-
-def with_debug_info_trace(instruction, debug_info, function_name):
-    """Prepends the given instruction with a tracing instruction that prints
-       the given debug information and function name."""
-    if debug_info is None and function_name is None:
-        return instruction
-    else:
-        if debug_info is None:
-            debug_info = 'unknown location '
-        if function_name is None:
-            function_name = 'unknown function'
-        return create_block(
-            PrintInstruction(
-                LiteralInstruction('TRACE: %s(%s, JIT)' % (debug_info, function_name))),
-            instruction)
+    return create_block(IgnoreInstruction(local_store), IgnoreInstruction(create_edge))
 
 def map_instruction_tree_top_down(function, instruction):
     """Applies the given mapping function to every instruction in the tree
@@ -1464,19 +1868,3 @@ def protect_temporaries_from_gc(instruction, connected_node):
             return instruction
 
     return map_instruction_tree_top_down(protect_result, instruction)
-
-if __name__ == "__main__":
-    example_tree = SelectInstruction(
-        LiteralInstruction(True),
-        LoopInstruction(
-            CompoundInstruction(
-                BreakInstruction(),
-                CompoundInstruction(
-                    EmptyInstruction(),
-                    ContinueInstruction()
-                )
-            )
-        ),
-        ReturnInstruction(
-            EmptyInstruction()))
-    print(example_tree.simplify())

+ 18 - 14
kernel/modelverse_kernel/main.py

@@ -4,6 +4,7 @@ from modelverse_kernel.request_handler import RequestHandler
 import modelverse_jit.jit as jit
 import modelverse_jit.intrinsics as jit_intrinsics
 import modelverse_jit.jit_primitives as jit_primitives
+import modelverse_jit.runtime as jit_runtime
 from collections import defaultdict
 import sys
 import time
@@ -31,10 +32,9 @@ class ModelverseKernel(object):
         self.allow_compiled = True
         #self.allow_compiled = False
 
-        # suggested_function_names maps body ids to suggested function names.
-        # You can tell the kernel to stop caring about getting the function names
-        # right by setting this to `None`.
-        self.suggested_function_names = {}
+        # Set `self.suggest_function_names` to True to associate global function names
+        # with their function bodies.
+        self.suggest_function_names = True
 
         # `self.jit` handles most JIT-related functionality.
         self.jit = jit.ModelverseJit()
@@ -52,11 +52,19 @@ class ModelverseKernel(object):
         #
         #     self.jit.allow_direct_calls(False)
         #
+        # To disable thunks in the JIT, uncomment the line below:
+        #
+        #     self.jit.enable_thunks(False)
+        #
         # To make the JIT compile 'input' instructions as calls to
         # modelverse_jit.runtime.get_input, uncomment the line below:
         #
         #     self.jit.use_input_function()
         #
+        # To disable source maps in the JIT, uncomment the line below:
+        #
+        #     self.jit.enable_source_maps(False)
+        #
         # To enable tracing in the JIT (for debugging purposes), uncomment
         # the line below:
         #
@@ -182,11 +190,7 @@ class ModelverseKernel(object):
 
     def jit_compile(self, task_root, inst):
         # Try to retrieve the suggested name.
-        if self.suggested_function_names is not None and inst in self.suggested_function_names:
-            suggested_name = self.suggested_function_names[inst]
-        else:
-            suggested_name = None
-
+        suggested_name = self.jit.get_global_name(inst)
         # Have the JIT compile the function.
         return self.jit.jit_compile(task_root, inst, suggested_name)
 
@@ -576,7 +580,7 @@ class ModelverseKernel(object):
                                    ("CNV", ["finish"]),
                                   ]
             if variable is None:
-                raise Exception("Not found as global: %s" % var_name)
+                raise Exception(jit_runtime.GLOBAL_NOT_FOUND_MESSAGE_FORMAT % var_name)
 
             # Resolved a global, so this is a string
             # Potentially, this might even be a function that we have precompiled already!
@@ -587,18 +591,18 @@ class ModelverseKernel(object):
                     # We have a compiled function ready!
                     # Now we have to bind the ID to the compiled functions
                     # For this, we read out the body of the resolved data
-                    compiler_val, =  yield [("RD", [variable, "value"])]
+                    compiler_val, = yield [("RD", [variable, "value"])]
                     compiler_body, = yield [("RD", [compiler_val, "body"])]
                     self.jit.register_compiled(compiler_body, compiled_function, var_name)
 
             # If we're dealing with a function, then we might want to figure out what its body id
             # is now so we can suggest a name to the JIT later.
-            if self.suggested_function_names is not None:
-                compiler_val, =  yield [("RD", [variable, "value"])]
+            if self.suggest_function_names and self.jit.get_global_body_id(var_name) is None:
+                compiler_val, = yield [("RD", [variable, "value"])]
                 if compiler_val is not None:
                     compiler_body, = yield [("RD", [compiler_val, "body"])]
                     if compiler_body is not None:
-                        self.suggested_function_names[compiler_body] = var_name
+                        self.jit.register_global(compiler_body, var_name)
 
         else:
             phase_link, returnvalue_link, new_phase = \

+ 131 - 42
kernel/modelverse_kernel/request_handler.py

@@ -1,20 +1,72 @@
+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 (generator, pending requests, request replies, has-reply)
-        # tuples.
+        # 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,
@@ -24,7 +76,8 @@ class RequestHandler(object):
             'TAIL_CALL_KWARGS' : self.execute_tail_call_kwargs,
             'TRY' : self.execute_try,
             'CATCH' : self.execute_catch,
-            'END_TRY' : self.execute_end_try
+            'END_TRY' : self.execute_end_try,
+            'DEBUG_INFO' : self.execute_debug_info
         }
 
     def is_active(self):
@@ -45,14 +98,14 @@ class RequestHandler(object):
                 if self.has_pending_requests():
                     try:
                         # Try to pop a request for the modelverse state.
-                        result = self.pop_requests()
-                        return result
+                        return self.pop_requests()
                     except KnownRequestHandled:
                         # Carry on.
                         pass
 
-                # Perform a single generator step.
-                self.step()
+                if not self.has_pending_requests():
+                    # Perform a single generator step.
+                    self.step()
             except StopIteration:
                 # Done, so remove the generator
                 self.pop_generator()
@@ -75,22 +128,24 @@ class RequestHandler(object):
                     return None
             except Exception as ex:
                 # Maybe get an exception handler to do this.
-                if not self.handle_exception(ex):
-                    raise
+                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."""
-        current_generator, requests, _, replies, has_reply = self.generator_stack[-1]
-        self.generator_stack[-1] = (current_generator, requests, True, replies, has_reply)
+        self.generator_stack[-1].finished_requests = True
 
     def has_pending_requests(self):
         """Tests if the top-of-stack generator has pending requests."""
-        _, _, finished_requests, _, _ = self.generator_stack[-1]
-        return not finished_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((gen, None, True, [], False))
+        self.generator_stack.append(GeneratorStackEntry(gen))
         # print('Pushed generator %s. Generator count: %d' % (gen, len(self.generator_stack)))
 
     def pop_generator(self):
@@ -113,30 +168,16 @@ class RequestHandler(object):
 
     def append_reply(self, new_reply):
         """Appends a reply to the top-of-stack generator's list of pending replies."""
-        current_generator, requests, requests_done, replies, has_reply = self.generator_stack[-1]
-        replies.append(new_reply)
-        has_reply = True
-        self.generator_stack[-1] = (current_generator, requests, requests_done, replies, has_reply)
+        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."""
-        current_generator, requests, requests_done, replies, has_reply = self.generator_stack[-1]
-        if new_replies is not None:
-            replies.extend(new_replies)
-            has_reply = True
-            self.generator_stack[-1] = (
-                current_generator, requests, requests_done, replies, has_reply)
+        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."""
-        current_generator, _, _, replies, has_reply = self.generator_stack[-1]
-
-        # Send the replies to the generator, and ask for new requests.
-        requests = current_generator.send(replies if has_reply else None)
-
-        # Update the entry on the stack.
-        self.generator_stack[-1] = (current_generator, requests, False, [], False)
+        self.generator_stack[-1].step()
 
     def handle_exception(self, exception):
         """Handles the given exception. A Boolean is returned that tells if
@@ -158,20 +199,55 @@ class RequestHandler(object):
                 # 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
+                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.
-        self.generator_stack = []
+        # 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 = []
-        return False
+
+        # 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
@@ -179,7 +255,7 @@ class RequestHandler(object):
 
            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]
+        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()
@@ -285,6 +361,7 @@ class RequestHandler(object):
                 ("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."""
@@ -301,6 +378,7 @@ class RequestHandler(object):
                 '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."""
@@ -325,4 +403,15 @@ class RequestHandler(object):
 
         # 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)
 

+ 47 - 0
performance/code/dict_iterate.alc

@@ -0,0 +1,47 @@
+include "primitives.alh"
+
+Void function log_dict_keys(dict : Element, n : Integer):
+	Element keys
+	Element k
+	Integer i
+
+	i = 0
+	keys = dict_keys(dict)
+	while (read_nr_out(keys) > 0):
+		k = set_pop(keys)
+		if (i == n):
+			log(k)
+			i = 0
+
+		i = i + 1
+
+	return!
+
+Element function create_dict(n : Integer):
+	Integer i
+	Element dict
+
+	i = 0
+	dict = create_node()
+
+	while (i < n):
+		dict_add(dict, i, 0)
+		i = i + 1
+
+	return dict!
+
+Void function test_main():
+	Integer size
+	Integer log_skip
+	Integer trip_count
+	Integer i
+
+	size = input()
+	log_skip = input()
+	trip_count = input()
+	i = 0
+	while (i < trip_count):
+		log_dict_keys(create_dict(size), log_skip)
+		i = i + 1
+
+	return!

+ 3 - 2
performance/perf2tex.py

@@ -11,7 +11,8 @@ LATEX_COLORS = [
     ('chartBlue', 0x4F81BD),
     ('chartRed', 0xC0504D),
     ('chartGreen', 0x9BBB59),
-    ('chartPurple', 0x9F4C7C)
+    ('chartPurple', 0x9F4C7C),
+    ('chartDarkYellow', 0xCCCC00)
 ]
 
 LATEX_HEADER = r"""\documentclass[12pt,a4paper,onecolumn,openright]{report}
@@ -33,7 +34,7 @@ def encode_latex_string(value):
     """Encodes the given string as a LaTeX string."""
     # I guess this is good enough for now. This may need to be
     # revisited if we encounter more complicated names.
-    return value.replace('_', '\\_')
+    return '{%s}' % value.replace('_', '\\_')
 
 def assemble_latex_chart(optimization_levels, color_defs, test_names, data):
     """Assembles a LaTeX chart from the given components."""

+ 13 - 0
performance/test_dict_iterate.py

@@ -0,0 +1,13 @@
+import unittest
+import utils
+
+class TestDictIterate(unittest.TestCase):
+    def dict_iterate(self, optimization_level):
+        utils.write_perf_to_file(
+            'dict_iterate', optimization_level,
+            utils.run_perf_test(
+                ["test_harness.alc", "dict_iterate.alc", "primitives.alc", "jit.alc"],
+                [50, 10, 100],
+                optimization_level))
+
+utils.define_perf_tests(TestDictIterate, TestDictIterate.dict_iterate)

+ 11 - 1
performance/utils.py

@@ -28,10 +28,20 @@ PORTS = set()
 OPTIMIZATION_LEVEL_LEGACY_INTERPRETER = "legacy-interpreter"
 OPTIMIZATION_LEVEL_INTERPRETER = "interpreter"
 OPTIMIZATION_LEVEL_BASELINE_JIT = "baseline-jit"
+OPTIMIZATION_LEVEL_BASELINE_JIT_NO_THUNKS = "baseline-jit,no-thunks"
+OPTIMIZATION_LEVEL_FAST_JIT = "fast-jit"
+OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_LARGE_FUNCTIONS = "adaptive-jit-favor-large-functions"
+OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_SMALL_FUNCTIONS = "adaptive-jit-favor-small-functions"
+OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_LOOPS = "adaptive-jit-favor-loops"
 ALL_OPTIMIZATION_LEVELS = [
     OPTIMIZATION_LEVEL_LEGACY_INTERPRETER,
     OPTIMIZATION_LEVEL_INTERPRETER,
-    OPTIMIZATION_LEVEL_BASELINE_JIT
+    OPTIMIZATION_LEVEL_BASELINE_JIT,
+    OPTIMIZATION_LEVEL_BASELINE_JIT_NO_THUNKS,
+    OPTIMIZATION_LEVEL_FAST_JIT,
+    OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_LARGE_FUNCTIONS,
+    OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_SMALL_FUNCTIONS,
+    OPTIMIZATION_LEVEL_ADAPTIVE_JIT_FAVOR_LOOPS
 ]
 
 class ModelverseTerminated(Exception):

+ 4 - 2
scripts/run_local_modelverse.py

@@ -7,5 +7,7 @@ if len(sys.argv) < 2:
     sys.stderr.write("    %s port\n" % sys.argv[0])
 else:
     subprocess.check_call([sys.executable, "-m", "sccd.compiler.sccdc", "-p", "threads", "server.xml"], cwd="hybrid_server")
-    subprocess.call([sys.executable, "run_mvk_server.py"] + sys.argv[1:] + ["--kernel=baseline-jit"], cwd="hybrid_server")
-    #subprocess.call([sys.executable, "run_mvk_server.py"] + sys.argv[1:] + ["--kernel=legacy-interpreter"], cwd="hybrid_server")
+    # There's no need to specify `--kernel=baseline-jit` here, because that's the default kernel.
+    # Also, specifying a kernel here breaks the performance tests.
+    subprocess.call([sys.executable, "run_mvk_server.py"] + sys.argv[1:], cwd="hybrid_server")
+    # subprocess.call([sys.executable, "run_mvk_server.py"] + sys.argv[1:] + ["--kernel=legacy-interpreter"], cwd="hybrid_server")