There’s some one-time initialization, and the API treats the first block you create as the entrypoint of the function, so we need to create that block first:
state.initial_block = gcc_jit_function_new_block (state.fn, "initial");
We can now create blocks for each of the operations. Most of these will be consolidated into larger blocks when the optimizer runs.
for (pc = 0; pc < fn->fn_num_ops; pc++) { char buf[100]; sprintf (buf, "instr%i", pc); state.op_blocks[pc] = gcc_jit_function_new_block (state.fn, buf); }
Now that we have a block it can jump to when it’s done, we can populate the initial block:
/* "stack_depth = 0;". */ gcc_jit_block_add_assignment ( state.initial_block, state.op_locs[0], state.stack_depth, gcc_jit_context_zero (state.ctxt, state.int_type)); /* "PUSH (arg);". */ add_push (&state, state.initial_block, gcc_jit_param_as_rvalue (state.param_arg), state.op_locs[0]); /* ...and jump to insn 0. */ gcc_jit_block_end_with_jump (state.initial_block, state.op_locs[0], state.op_blocks[0]);
We can now populate the blocks for the individual operations. We loop through them, adding instructions to their blocks:
for (pc = 0; pc < fn->fn_num_ops; pc++) { gcc_jit_location *loc = state.op_locs[pc]; gcc_jit_block *block = state.op_blocks[pc]; gcc_jit_block *next_block = (pc < fn->fn_num_ops ? state.op_blocks[pc + 1] : NULL); toyvm_op *op; op = &fn->fn_ops[pc];
We’re going to have another big switch
statement for implementing
the opcodes, this time for compiling them, rather than interpreting
them. It’s helpful to have macros for implementing push and pop, so that
we can make the switch
statement that’s coming up look as much as
possible like the one above within the interpreter:
#define X_EQUALS_POP()\ add_pop (&state, block, state.x, loc) #define Y_EQUALS_POP()\ add_pop (&state, block, state.y, loc) #define PUSH_RVALUE(RVALUE)\ add_push (&state, block, (RVALUE), loc) #define PUSH_X()\ PUSH_RVALUE (gcc_jit_lvalue_as_rvalue (state.x)) #define PUSH_Y() \ PUSH_RVALUE (gcc_jit_lvalue_as_rvalue (state.y))
|
When I first implemented this compiler, I accidentally missed an edit
when copying and pasting the Y_EQUALS_POP
macro, so that popping the
stack into y
instead erroneously assigned it to x
, leaving y
uninitialized.
To track this kind of thing down, we can use
gcc_jit_block_add_comment() to add descriptive comments
to the internal representation. This is invaluable when looking through
the generated IR for, say factorial
:
gcc_jit_block_add_comment (block, loc, opcode_names[op->op_opcode]);
We can now write the big switch
statement that implements the
individual opcodes, populating the relevant block with statements:
switch (op->op_opcode) { case DUP: X_EQUALS_POP (); PUSH_X (); PUSH_X (); break; case ROT: Y_EQUALS_POP (); X_EQUALS_POP (); PUSH_Y (); PUSH_X (); break; case BINARY_ADD: Y_EQUALS_POP (); X_EQUALS_POP (); PUSH_RVALUE ( gcc_jit_context_new_binary_op ( state.ctxt, loc, GCC_JIT_BINARY_OP_PLUS, state.int_type, gcc_jit_lvalue_as_rvalue (state.x), gcc_jit_lvalue_as_rvalue (state.y))); break; case BINARY_SUBTRACT: Y_EQUALS_POP (); X_EQUALS_POP (); PUSH_RVALUE ( gcc_jit_context_new_binary_op ( state.ctxt, loc, GCC_JIT_BINARY_OP_MINUS, state.int_type, gcc_jit_lvalue_as_rvalue (state.x), gcc_jit_lvalue_as_rvalue (state.y))); break; case BINARY_MULT: Y_EQUALS_POP (); X_EQUALS_POP (); PUSH_RVALUE ( gcc_jit_context_new_binary_op ( state.ctxt, loc, GCC_JIT_BINARY_OP_MULT, state.int_type, gcc_jit_lvalue_as_rvalue (state.x), gcc_jit_lvalue_as_rvalue (state.y))); break; case BINARY_COMPARE_LT: Y_EQUALS_POP (); X_EQUALS_POP (); PUSH_RVALUE ( /* cast of bool to int */ gcc_jit_context_new_cast ( state.ctxt, loc, /* (x < y) as a bool */ gcc_jit_context_new_comparison ( state.ctxt, loc, GCC_JIT_COMPARISON_LT, gcc_jit_lvalue_as_rvalue (state.x), gcc_jit_lvalue_as_rvalue (state.y)), state.int_type)); break; case RECURSE: { X_EQUALS_POP (); gcc_jit_rvalue *arg = gcc_jit_lvalue_as_rvalue (state.x); PUSH_RVALUE ( gcc_jit_context_new_call ( state.ctxt, loc, state.fn, 1, &arg)); break; } case RETURN: X_EQUALS_POP (); gcc_jit_block_end_with_return ( block, loc, gcc_jit_lvalue_as_rvalue (state.x)); break; /* Ops taking an operand. */ case PUSH_CONST: PUSH_RVALUE ( gcc_jit_context_new_rvalue_from_int ( state.ctxt, state.int_type, op->op_operand)); break; case JUMP_ABS_IF_TRUE: X_EQUALS_POP (); gcc_jit_block_end_with_conditional ( block, loc, /* "(bool)x". */ gcc_jit_context_new_cast ( state.ctxt, loc, gcc_jit_lvalue_as_rvalue (state.x), state.bool_type), state.op_blocks[op->op_operand], /* on_true */ next_block); /* on_false */ break; default: assert(0); } /* end of switch on opcode */
Every block must be terminated, via a call to one of the
gcc_jit_block_end_with_
entrypoints. This has been done for two
of the opcodes, but we need to do it for the other ones, by jumping
to the next block.
if (op->op_opcode != JUMP_ABS_IF_TRUE && op->op_opcode != RETURN) gcc_jit_block_end_with_jump ( block, loc, next_block);
This is analogous to simply incrementing the program counter.