/* A simple stack-based virtual machine to demonstrate JIT-compilation. Copyright (C) 2014-2018 Free Software Foundation, Inc. This file is part of GCC. GCC is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GCC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GCC; see the file COPYING3. If not see . */ #include #include #include #include #include #include #include /* Wrapper around a gcc_jit_result *. */ class compilation_result { public: compilation_result (gcc_jit_result *result) : m_result (result) { } ~compilation_result () { gcc_jit_result_release (m_result); } void *get_code (const char *funcname) { return gcc_jit_result_get_code (m_result, funcname); } private: gcc_jit_result *m_result; }; /* Functions are compiled to this function ptr type. */ typedef int (*toyvm_compiled_func) (int); enum opcode { /* Ops taking no operand. */ DUP, ROT, BINARY_ADD, BINARY_SUBTRACT, BINARY_MULT, BINARY_COMPARE_LT, RECURSE, RETURN, /* Ops taking an operand. */ PUSH_CONST, JUMP_ABS_IF_TRUE }; #define FIRST_UNARY_OPCODE (PUSH_CONST) const char * const opcode_names[] = { "DUP", "ROT", "BINARY_ADD", "BINARY_SUBTRACT", "BINARY_MULT", "BINARY_COMPARE_LT", "RECURSE", "RETURN", "PUSH_CONST", "JUMP_ABS_IF_TRUE", }; struct toyvm_op { /* Which operation. */ enum opcode op_opcode; /* Some opcodes take an argument. */ int op_operand; /* The line number of the operation within the source file. */ int op_linenum; }; #define MAX_OPS (64) class toyvm_function { public: void add_op (enum opcode opcode, int operand, int linenum); void add_unary_op (enum opcode opcode, const char *rest_of_line, int linenum); static toyvm_function * parse (const char *filename, const char *name); void disassemble_op (toyvm_op *op, int index, FILE *out); void disassemble (FILE *out); int interpret (int arg, FILE *trace); compilation_result compile (); const char * get_function_name () const { return m_funcname; } private: void make_function_name (const char *filename); private: const char *fn_filename; char *m_funcname; int fn_num_ops; toyvm_op fn_ops[MAX_OPS]; friend struct compilation_state; }; #define MAX_STACK_DEPTH (8) class toyvm_frame { public: void push (int arg); int pop (); void dump_stack (FILE *out); private: toyvm_function *frm_function; int frm_pc; int frm_stack[MAX_STACK_DEPTH]; int frm_cur_depth; friend int toyvm_function::interpret (int arg, FILE *trace); }; void toyvm_function::add_op (enum opcode opcode, int operand, int linenum) { toyvm_op *op; assert (fn_num_ops < MAX_OPS); op = &fn_ops[fn_num_ops++]; op->op_opcode = opcode; op->op_operand = operand; op->op_linenum = linenum; } void toyvm_function::add_unary_op (enum opcode opcode, const char *rest_of_line, int linenum) { int operand = atoi (rest_of_line); add_op (opcode, operand, linenum); } void toyvm_function::make_function_name (const char *filename) { /* Skip any path separators. */ const char *pathsep = strrchr (filename, '/'); if (pathsep) filename = pathsep + 1; /* Copy filename to funcname. */ m_funcname = (char *)malloc (strlen (filename) + 1); strcpy (m_funcname, filename); /* Convert "." to NIL terminator. */ *(strchr (m_funcname, '.')) = '\0'; } toyvm_function * toyvm_function::parse (const char *filename, const char *name) { FILE *f = NULL; toyvm_function *fn = NULL; char *line = NULL; ssize_t linelen; size_t bufsize; int linenum = 0; assert (filename); assert (name); f = fopen (filename, "r"); if (!f) { fprintf (stderr, "cannot open file %s: %s\n", filename, strerror (errno)); goto error; } fn = (toyvm_function *)calloc (1, sizeof (toyvm_function)); if (!fn) { fprintf (stderr, "out of memory allocating toyvm_function\n"); goto error; } fn->fn_filename = filename; fn->make_function_name (filename); /* Read the lines of the file. */ while ((linelen = getline (&line, &bufsize, f)) != -1) { /* Note that this is a terrible parser, but it avoids the need to bring in lex/yacc as a dependency. */ linenum++; if (0) fprintf (stdout, "%3d: %s", linenum, line); /* Lines beginning with # are comments. */ if (line[0] == '#') continue; /* Skip blank lines. */ if (line[0] == '\n') continue; #define LINE_MATCHES(OPCODE) (0 == strncmp ((OPCODE), line, strlen (OPCODE))) if (LINE_MATCHES ("DUP\n")) fn->add_op (DUP, 0, linenum); else if (LINE_MATCHES ("ROT\n")) fn->add_op (ROT, 0, linenum); else if (LINE_MATCHES ("BINARY_ADD\n")) fn->add_op (BINARY_ADD, 0, linenum); else if (LINE_MATCHES ("BINARY_SUBTRACT\n")) fn->add_op (BINARY_SUBTRACT, 0, linenum); else if (LINE_MATCHES ("BINARY_MULT\n")) fn->add_op (BINARY_MULT, 0, linenum); else if (LINE_MATCHES ("BINARY_COMPARE_LT\n")) fn->add_op (BINARY_COMPARE_LT, 0, linenum); else if (LINE_MATCHES ("RECURSE\n")) fn->add_op (RECURSE, 0, linenum); else if (LINE_MATCHES ("RETURN\n")) fn->add_op (RETURN, 0, linenum); else if (LINE_MATCHES ("PUSH_CONST ")) fn->add_unary_op (PUSH_CONST, line + strlen ("PUSH_CONST "), linenum); else if (LINE_MATCHES ("JUMP_ABS_IF_TRUE ")) fn->add_unary_op (JUMP_ABS_IF_TRUE, line + strlen("JUMP_ABS_IF_TRUE "), linenum); else { fprintf (stderr, "%s:%d: parse error\n", filename, linenum); free (fn); fn = NULL; goto error; } #undef LINE_MATCHES } free (line); fclose (f); return fn; error: free (line); if (f) fclose (f); free (fn); return NULL; } void toyvm_function::disassemble_op (toyvm_op *op, int index, FILE *out) { fprintf (out, "%s:%d: index %d: %s", fn_filename, op->op_linenum, index, opcode_names[op->op_opcode]); if (op->op_opcode >= FIRST_UNARY_OPCODE) fprintf (out, " %d", op->op_operand); fprintf (out, "\n"); } void toyvm_function::disassemble (FILE *out) { int i; for (i = 0; i < fn_num_ops; i++) { toyvm_op *op = &fn_ops[i]; disassemble_op (op, i, out); } } void toyvm_frame::push (int arg) { assert (frm_cur_depth < MAX_STACK_DEPTH); frm_stack[frm_cur_depth++] = arg; } int toyvm_frame::pop () { assert (frm_cur_depth > 0); return frm_stack[--frm_cur_depth]; } void toyvm_frame::dump_stack (FILE *out) { int i; fprintf (out, "stack:"); for (i = 0; i < frm_cur_depth; i++) { fprintf (out, " %d", frm_stack[i]); } fprintf (out, "\n"); } /* Execute the given function. */ int toyvm_function::interpret (int arg, FILE *trace) { toyvm_frame frame; #define PUSH(ARG) (frame.push (ARG)) #define POP(ARG) (frame.pop ()) frame.frm_function = this; frame.frm_pc = 0; frame.frm_cur_depth = 0; PUSH (arg); while (1) { toyvm_op *op; int x, y; assert (frame.frm_pc < fn_num_ops); op = &fn_ops[frame.frm_pc++]; if (trace) { frame.dump_stack (trace); disassemble_op (op, frame.frm_pc, trace); } switch (op->op_opcode) { /* Ops taking no operand. */ case DUP: x = POP (); PUSH (x); PUSH (x); break; case ROT: y = POP (); x = POP (); PUSH (y); PUSH (x); break; case BINARY_ADD: y = POP (); x = POP (); PUSH (x + y); break; case BINARY_SUBTRACT: y = POP (); x = POP (); PUSH (x - y); break; case BINARY_MULT: y = POP (); x = POP (); PUSH (x * y); break; case BINARY_COMPARE_LT: y = POP (); x = POP (); PUSH (x < y); break; case RECURSE: x = POP (); x = interpret (x, trace); PUSH (x); break; case RETURN: return POP (); /* Ops taking an operand. */ case PUSH_CONST: PUSH (op->op_operand); break; case JUMP_ABS_IF_TRUE: x = POP (); if (x) frame.frm_pc = op->op_operand; break; default: assert (0); /* unknown opcode */ } /* end of switch on opcode */ } /* end of while loop */ #undef PUSH #undef POP } /* JIT compilation. */ class compilation_state { public: compilation_state (toyvm_function &toyvmfn) : toyvmfn (toyvmfn) {} void create_context (); void create_types (); void create_locations (); void create_function (const char *funcname); compilation_result compile (); private: void add_push (gccjit::block block, gccjit::rvalue rvalue, gccjit::location loc); void add_pop (gccjit::block block, gccjit::lvalue lvalue, gccjit::location loc); private: /* State. */ toyvm_function &toyvmfn; gccjit::context ctxt; gccjit::type int_type; gccjit::type bool_type; gccjit::type stack_type; /* int[MAX_STACK_DEPTH] */ gccjit::rvalue const_one; gccjit::function fn; gccjit::param param_arg; gccjit::lvalue stack; gccjit::lvalue stack_depth; gccjit::lvalue x; gccjit::lvalue y; gccjit::location op_locs[MAX_OPS]; gccjit::block initial_block; gccjit::block op_blocks[MAX_OPS]; }; /* The main compilation hook. */ compilation_result toyvm_function::compile () { compilation_state state (*this); state.create_context (); state.create_types (); state.create_locations (); state.create_function (get_function_name ()); /* We've now finished populating the context. Compile it. */ return state.compile (); } /* Stack manipulation. */ void compilation_state::add_push (gccjit::block block, gccjit::rvalue rvalue, gccjit::location loc) { /* stack[stack_depth] = RVALUE */ block.add_assignment ( /* stack[stack_depth] */ ctxt.new_array_access ( stack, stack_depth, loc), rvalue, loc); /* "stack_depth++;". */ block.add_assignment_op ( stack_depth, GCC_JIT_BINARY_OP_PLUS, const_one, loc); } void compilation_state::add_pop (gccjit::block block, gccjit::lvalue lvalue, gccjit::location loc) { /* "--stack_depth;". */ block.add_assignment_op ( stack_depth, GCC_JIT_BINARY_OP_MINUS, const_one, loc); /* "LVALUE = stack[stack_depth];". */ block.add_assignment ( lvalue, /* stack[stack_depth] */ ctxt.new_array_access (stack, stack_depth, loc), loc); } /* Create the context. */ void compilation_state::create_context () { ctxt = gccjit::context::acquire (); ctxt.set_bool_option (GCC_JIT_BOOL_OPTION_DUMP_INITIAL_GIMPLE, 0); ctxt.set_bool_option (GCC_JIT_BOOL_OPTION_DUMP_GENERATED_CODE, 0); ctxt.set_int_option (GCC_JIT_INT_OPTION_OPTIMIZATION_LEVEL, 3); ctxt.set_bool_option (GCC_JIT_BOOL_OPTION_KEEP_INTERMEDIATES, 0); ctxt.set_bool_option (GCC_JIT_BOOL_OPTION_DUMP_EVERYTHING, 0); ctxt.set_bool_option (GCC_JIT_BOOL_OPTION_DEBUGINFO, 1); } /* Create types. */ void compilation_state::create_types () { /* Create types. */ int_type = ctxt.get_type (GCC_JIT_TYPE_INT); bool_type = ctxt.get_type (GCC_JIT_TYPE_BOOL); stack_type = ctxt.new_array_type (int_type, MAX_STACK_DEPTH); /* The constant value 1. */ const_one = ctxt.one (int_type); } /* Create locations. */ void compilation_state::create_locations () { for (int pc = 0; pc < toyvmfn.fn_num_ops; pc++) { toyvm_op *op = &toyvmfn.fn_ops[pc]; op_locs[pc] = ctxt.new_location (toyvmfn.fn_filename, op->op_linenum, 0); /* column */ } } /* Creating the function. */ void compilation_state::create_function (const char *funcname) { std::vector params; param_arg = ctxt.new_param (int_type, "arg", op_locs[0]); params.push_back (param_arg); fn = ctxt.new_function (GCC_JIT_FUNCTION_EXPORTED, int_type, funcname, params, 0, op_locs[0]); /* Create stack lvalues. */ stack = fn.new_local (stack_type, "stack"); stack_depth = fn.new_local (int_type, "stack_depth"); x = fn.new_local (int_type, "x"); y = fn.new_local (int_type, "y"); /* 1st pass: create blocks, one per opcode. */ /* We need an entry block to do one-time initialization, so create that first. */ initial_block = fn.new_block ("initial"); /* Create a block per operation. */ for (int pc = 0; pc < toyvmfn.fn_num_ops; pc++) { char buf[16]; sprintf (buf, "instr%i", pc); op_blocks[pc] = fn.new_block (buf); } /* Populate the initial block. */ /* "stack_depth = 0;". */ initial_block.add_assignment (stack_depth, ctxt.zero (int_type), op_locs[0]); /* "PUSH (arg);". */ add_push (initial_block, param_arg, op_locs[0]); /* ...and jump to insn 0. */ initial_block.end_with_jump (op_blocks[0], op_locs[0]); /* 2nd pass: fill in instructions. */ for (int pc = 0; pc < toyvmfn.fn_num_ops; pc++) { gccjit::location loc = op_locs[pc]; gccjit::block block = op_blocks[pc]; gccjit::block next_block = (pc < toyvmfn.fn_num_ops ? op_blocks[pc + 1] : NULL); toyvm_op *op; op = &toyvmfn.fn_ops[pc]; /* Helper macros. */ #define X_EQUALS_POP()\ add_pop (block, x, loc) #define Y_EQUALS_POP()\ add_pop (block, y, loc) #define PUSH_RVALUE(RVALUE)\ add_push (block, (RVALUE), loc) #define PUSH_X()\ PUSH_RVALUE (x) #define PUSH_Y() \ PUSH_RVALUE (y) block.add_comment (opcode_names[op->op_opcode], loc); /* Handle the individual opcodes. */ 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 ( ctxt.new_binary_op ( GCC_JIT_BINARY_OP_PLUS, int_type, x, y, loc)); break; case BINARY_SUBTRACT: Y_EQUALS_POP (); X_EQUALS_POP (); PUSH_RVALUE ( ctxt.new_binary_op ( GCC_JIT_BINARY_OP_MINUS, int_type, x, y, loc)); break; case BINARY_MULT: Y_EQUALS_POP (); X_EQUALS_POP (); PUSH_RVALUE ( ctxt.new_binary_op ( GCC_JIT_BINARY_OP_MULT, int_type, x, y, loc)); break; case BINARY_COMPARE_LT: Y_EQUALS_POP (); X_EQUALS_POP (); PUSH_RVALUE ( /* cast of bool to int */ ctxt.new_cast ( /* (x < y) as a bool */ ctxt.new_comparison ( GCC_JIT_COMPARISON_LT, x, y, loc), int_type, loc)); break; case RECURSE: { X_EQUALS_POP (); PUSH_RVALUE ( ctxt.new_call ( fn, x, loc)); break; } case RETURN: X_EQUALS_POP (); block.end_with_return (x, loc); break; /* Ops taking an operand. */ case PUSH_CONST: PUSH_RVALUE ( ctxt.new_rvalue (int_type, op->op_operand)); break; case JUMP_ABS_IF_TRUE: X_EQUALS_POP (); block.end_with_conditional ( /* "(bool)x". */ ctxt.new_cast (x, bool_type, loc), op_blocks[op->op_operand], /* on_true */ next_block, /* on_false */ loc); break; default: assert(0); } /* end of switch on opcode */ /* Go to the next block. */ if (op->op_opcode != JUMP_ABS_IF_TRUE && op->op_opcode != RETURN) block.end_with_jump (next_block, loc); } /* end of loop on PC locations. */ } compilation_result compilation_state::compile () { return ctxt.compile (); } char test[1024]; #define CHECK_NON_NULL(PTR) \ do { \ if ((PTR) != NULL) \ { \ pass ("%s: %s is non-null", test, #PTR); \ } \ else \ { \ fail ("%s: %s is NULL", test, #PTR); \ abort (); \ } \ } while (0) #define CHECK_VALUE(ACTUAL, EXPECTED) \ do { \ if ((ACTUAL) == (EXPECTED)) \ { \ pass ("%s: actual: %s == expected: %s", test, #ACTUAL, #EXPECTED); \ } \ else \ { \ fail ("%s: actual: %s != expected: %s", test, #ACTUAL, #EXPECTED); \ fprintf (stderr, "incorrect value\n"); \ abort (); \ } \ } while (0) static void test_script (const char *scripts_dir, const char *script_name, int input, int expected_result) { char *script_path; toyvm_function *fn; int interpreted_result; toyvm_compiled_func code; int compiled_result; snprintf (test, sizeof (test), "toyvm.cc: %s", script_name); script_path = (char *)malloc (strlen (scripts_dir) + strlen (script_name) + 1); CHECK_NON_NULL (script_path); sprintf (script_path, "%s%s", scripts_dir, script_name); fn = toyvm_function::parse (script_path, script_name); CHECK_NON_NULL (fn); interpreted_result = fn->interpret (input, NULL); CHECK_VALUE (interpreted_result, expected_result); compilation_result compiler_result = fn->compile (); const char *funcname = fn->get_function_name (); code = (toyvm_compiled_func)compiler_result.get_code (funcname); CHECK_NON_NULL (code); compiled_result = code (input); CHECK_VALUE (compiled_result, expected_result); free (script_path); } #define PATH_TO_SCRIPTS ("/jit/docs/examples/tut04-toyvm/") static void test_suite (void) { const char *srcdir; char *scripts_dir; snprintf (test, sizeof (test), "toyvm.cc"); /* We need to locate the test scripts. Rely on "srcdir" being set in the environment. */ srcdir = getenv ("srcdir"); CHECK_NON_NULL (srcdir); scripts_dir = (char *)malloc (strlen (srcdir) + strlen(PATH_TO_SCRIPTS) + 1); CHECK_NON_NULL (scripts_dir); sprintf (scripts_dir, "%s%s", srcdir, PATH_TO_SCRIPTS); test_script (scripts_dir, "factorial.toy", 10, 3628800); test_script (scripts_dir, "fibonacci.toy", 10, 55); free (scripts_dir); } int main (int argc, char **argv) { const char *filename = NULL; toyvm_function *fn = NULL; /* If called with no args, assume we're being run by the test suite. */ if (argc < 3) { test_suite (); return 0; } if (argc != 3) { fprintf (stdout, "%s FILENAME INPUT: Parse and run a .toy file\n", argv[0]); exit (1); } filename = argv[1]; fn = toyvm_function::parse (filename, filename); if (!fn) exit (1); if (0) fn->disassemble (stdout); printf ("interpreter result: %d\n", fn->interpret (atoi (argv[2]), NULL)); /* JIT-compilation. */ compilation_result compiler_result = fn->compile (); const char *funcname = fn->get_function_name (); toyvm_compiled_func code = (toyvm_compiled_func)compiler_result.get_code (funcname); printf ("compiler result: %d\n", code (atoi (argv[2]))); return 0; }