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]; };