1.4.2 Compiling to machine code

We want to generate machine code that can be cast to this type and then directly executed in-process:

typedef int (*toyvm_compiled_code) (int);

The lifetime of the code is tied to that of a gcc_jit_result *. We’ll handle this by bundling them up in a structure, so that we can clean them up together by calling gcc_jit_result_release():


struct toyvm_compiled_function
{
  gcc_jit_result *cf_jit_result;
  toyvm_compiled_code cf_code;
};

Our compiler isn’t very sophisticated; it takes the implementation of each opcode above, and maps it directly to the operations supported by the libgccjit API.

How should we handle the stack? In theory we could calculate what the stack depth will be at each opcode, and optimize away the stack manipulation “by hand”. We’ll see below that libgccjit is able to do this for us, so we’ll implement stack manipulation in a direct way, by creating a stack array and stack_depth variables, local within the generated function, equivalent to this C code:

int stack_depth;
int stack[MAX_STACK_DEPTH];

We’ll also have local variables x and y for use when implementing the opcodes, equivalent to this:

int x;
int y;

This means our compiler has the following state:


struct compilation_state
{
  gcc_jit_context *ctxt;

  gcc_jit_type *int_type;
  gcc_jit_type *bool_type;
  gcc_jit_type *stack_type; /* int[MAX_STACK_DEPTH] */

  gcc_jit_rvalue *const_one;

  gcc_jit_function *fn;
  gcc_jit_param *param_arg;
  gcc_jit_lvalue *stack;
  gcc_jit_lvalue *stack_depth;
  gcc_jit_lvalue *x;
  gcc_jit_lvalue *y;

  gcc_jit_location *op_locs[MAX_OPS];
  gcc_jit_block *initial_block;
  gcc_jit_block *op_blocks[MAX_OPS];

};