blob: c2333f0de891ce65ef844e24fb82571902812084 [file] [log] [blame]
/* A simple stack-based virtual machine to demonstrate
JIT-compilation.
Copyright (C) 2014-2022 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
<http://www.gnu.org/licenses/>. */
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "jit-dejagnu.h"
#include <libgccjit.h>
/* Typedefs. */
typedef struct toyvm_op toyvm_op;
typedef struct toyvm_function toyvm_function;
typedef struct toyvm_frame toyvm_frame;
typedef struct compilation_state compilation_state;
typedef struct toyvm_compiled_function toyvm_compiled_function;
/* Functions are compiled to this function ptr type. */
typedef int (*toyvm_compiled_code) (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)
struct toyvm_function
{
const char *fn_filename;
int fn_num_ops;
toyvm_op fn_ops[MAX_OPS];
};
#define MAX_STACK_DEPTH (8)
struct toyvm_frame
{
toyvm_function *frm_function;
int frm_pc;
int frm_stack[MAX_STACK_DEPTH];
int frm_cur_depth;
};
static void
add_op (toyvm_function *fn, enum opcode opcode,
int operand, int linenum)
{
toyvm_op *op;
assert (fn->fn_num_ops < MAX_OPS);
op = &fn->fn_ops[fn->fn_num_ops++];
op->op_opcode = opcode;
op->op_operand = operand;
op->op_linenum = linenum;
}
static void
add_unary_op (toyvm_function *fn, enum opcode opcode,
const char *rest_of_line, int linenum)
{
int operand = atoi (rest_of_line);
add_op (fn, opcode, operand, linenum);
}
static char *
get_function_name (const char *filename)
{
/* Skip any path separators. */
const char *pathsep = strrchr (filename, '/');
if (pathsep)
filename = pathsep + 1;
/* Copy filename to funcname. */
char *funcname = (char *)malloc (strlen (filename) + 1);
strcpy (funcname, filename);
/* Convert "." to NIL terminator. */
*(strchr (funcname, '.')) = '\0';
return funcname;
}
static 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;
/* 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"))
add_op (fn, DUP, 0, linenum);
else if (LINE_MATCHES ("ROT\n"))
add_op (fn, ROT, 0, linenum);
else if (LINE_MATCHES ("BINARY_ADD\n"))
add_op (fn, BINARY_ADD, 0, linenum);
else if (LINE_MATCHES ("BINARY_SUBTRACT\n"))
add_op (fn, BINARY_SUBTRACT, 0, linenum);
else if (LINE_MATCHES ("BINARY_MULT\n"))
add_op (fn, BINARY_MULT, 0, linenum);
else if (LINE_MATCHES ("BINARY_COMPARE_LT\n"))
add_op (fn, BINARY_COMPARE_LT, 0, linenum);
else if (LINE_MATCHES ("RECURSE\n"))
add_op (fn, RECURSE, 0, linenum);
else if (LINE_MATCHES ("RETURN\n"))
add_op (fn, RETURN, 0, linenum);
else if (LINE_MATCHES ("PUSH_CONST "))
add_unary_op (fn, PUSH_CONST,
line + strlen ("PUSH_CONST "), linenum);
else if (LINE_MATCHES ("JUMP_ABS_IF_TRUE "))
add_unary_op (fn, 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;
}
static void
toyvm_function_disassemble_op (toyvm_function *fn, toyvm_op *op, int index, FILE *out)
{
fprintf (out, "%s:%d: index %d: %s",
fn->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");
}
static void
toyvm_function_disassemble (toyvm_function *fn, FILE *out)
{
int i;
for (i = 0; i < fn->fn_num_ops; i++)
{
toyvm_op *op = &fn->fn_ops[i];
toyvm_function_disassemble_op (fn, op, i, out);
}
}
static void
toyvm_frame_push (toyvm_frame *frame, int arg)
{
assert (frame->frm_cur_depth < MAX_STACK_DEPTH);
frame->frm_stack[frame->frm_cur_depth++] = arg;
}
static int
toyvm_frame_pop (toyvm_frame *frame)
{
assert (frame->frm_cur_depth > 0);
return frame->frm_stack[--frame->frm_cur_depth];
}
static void
toyvm_frame_dump_stack (toyvm_frame *frame, FILE *out)
{
int i;
fprintf (out, "stack:");
for (i = 0; i < frame->frm_cur_depth; i++)
{
fprintf (out, " %d", frame->frm_stack[i]);
}
fprintf (out, "\n");
}
/* Execute the given function. */
static int
toyvm_function_interpret (toyvm_function *fn, int arg, FILE *trace)
{
toyvm_frame frame;
#define PUSH(ARG) (toyvm_frame_push (&frame, (ARG)))
#define POP(ARG) (toyvm_frame_pop (&frame))
frame.frm_function = fn;
frame.frm_pc = 0;
frame.frm_cur_depth = 0;
PUSH (arg);
while (1)
{
toyvm_op *op;
int x, y;
assert (frame.frm_pc < fn->fn_num_ops);
op = &fn->fn_ops[frame.frm_pc++];
if (trace)
{
toyvm_frame_dump_stack (&frame, trace);
toyvm_function_disassemble_op (fn, 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 = toyvm_function_interpret (fn, 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. */
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];
};
/* Stack manipulation. */
static void
add_push (compilation_state *state,
gcc_jit_block *block,
gcc_jit_rvalue *rvalue,
gcc_jit_location *loc)
{
/* stack[stack_depth] = RVALUE */
gcc_jit_block_add_assignment (
block,
loc,
/* stack[stack_depth] */
gcc_jit_context_new_array_access (
state->ctxt,
loc,
gcc_jit_lvalue_as_rvalue (state->stack),
gcc_jit_lvalue_as_rvalue (state->stack_depth)),
rvalue);
/* "stack_depth++;". */
gcc_jit_block_add_assignment_op (
block,
loc,
state->stack_depth,
GCC_JIT_BINARY_OP_PLUS,
state->const_one);
}
static void
add_pop (compilation_state *state,
gcc_jit_block *block,
gcc_jit_lvalue *lvalue,
gcc_jit_location *loc)
{
/* "--stack_depth;". */
gcc_jit_block_add_assignment_op (
block,
loc,
state->stack_depth,
GCC_JIT_BINARY_OP_MINUS,
state->const_one);
/* "LVALUE = stack[stack_depth];". */
gcc_jit_block_add_assignment (
block,
loc,
lvalue,
/* stack[stack_depth] */
gcc_jit_lvalue_as_rvalue (
gcc_jit_context_new_array_access (
state->ctxt,
loc,
gcc_jit_lvalue_as_rvalue (state->stack),
gcc_jit_lvalue_as_rvalue (state->stack_depth))));
}
/* A struct to hold the compilation results. */
struct toyvm_compiled_function
{
gcc_jit_result *cf_jit_result;
toyvm_compiled_code cf_code;
};
/* The main compilation hook. */
static toyvm_compiled_function *
toyvm_function_compile (toyvm_function *fn)
{
compilation_state state;
int pc;
char *funcname;
memset (&state, 0, sizeof (state));
funcname = get_function_name (fn->fn_filename);
state.ctxt = gcc_jit_context_acquire ();
gcc_jit_context_set_bool_option (state.ctxt,
GCC_JIT_BOOL_OPTION_DUMP_INITIAL_GIMPLE,
0);
gcc_jit_context_set_bool_option (state.ctxt,
GCC_JIT_BOOL_OPTION_DUMP_GENERATED_CODE,
0);
gcc_jit_context_set_int_option (state.ctxt,
GCC_JIT_INT_OPTION_OPTIMIZATION_LEVEL,
3);
gcc_jit_context_set_bool_option (state.ctxt,
GCC_JIT_BOOL_OPTION_KEEP_INTERMEDIATES,
0);
gcc_jit_context_set_bool_option (state.ctxt,
GCC_JIT_BOOL_OPTION_DUMP_EVERYTHING,
0);
gcc_jit_context_set_bool_option (state.ctxt,
GCC_JIT_BOOL_OPTION_DEBUGINFO,
1);
/* Create types. */
state.int_type =
gcc_jit_context_get_type (state.ctxt, GCC_JIT_TYPE_INT);
state.bool_type =
gcc_jit_context_get_type (state.ctxt, GCC_JIT_TYPE_BOOL);
state.stack_type =
gcc_jit_context_new_array_type (state.ctxt, NULL,
state.int_type, MAX_STACK_DEPTH);
/* The constant value 1. */
state.const_one = gcc_jit_context_one (state.ctxt, state.int_type);
/* Create locations. */
for (pc = 0; pc < fn->fn_num_ops; pc++)
{
toyvm_op *op = &fn->fn_ops[pc];
state.op_locs[pc] = gcc_jit_context_new_location (state.ctxt,
fn->fn_filename,
op->op_linenum,
0); /* column */
}
/* Creating the function. */
state.param_arg =
gcc_jit_context_new_param (state.ctxt, state.op_locs[0],
state.int_type, "arg");
state.fn =
gcc_jit_context_new_function (state.ctxt,
state.op_locs[0],
GCC_JIT_FUNCTION_EXPORTED,
state.int_type,
funcname,
1, &state.param_arg, 0);
/* Create stack lvalues. */
state.stack =
gcc_jit_function_new_local (state.fn, NULL,
state.stack_type, "stack");
state.stack_depth =
gcc_jit_function_new_local (state.fn, NULL,
state.int_type, "stack_depth");
state.x =
gcc_jit_function_new_local (state.fn, NULL,
state.int_type, "x");
state.y =
gcc_jit_function_new_local (state.fn, NULL,
state.int_type, "y");
/* 1st pass: create blocks, one per opcode. */
/* We need an entry block to do one-time initialization, so create that
first. */
state.initial_block = gcc_jit_function_new_block (state.fn, "initial");
/* Create a block per operation. */
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);
}
/* 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]);
/* 2nd pass: fill in instructions. */
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];
/* Helper macros. */
#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))
gcc_jit_block_add_comment (block, loc, opcode_names[op->op_opcode]);
/* 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 (
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 */
/* Go 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);
} /* end of loop on PC locations. */
/* We've now finished populating the context. Compile it. */
gcc_jit_result *jit_result = gcc_jit_context_compile (state.ctxt);
gcc_jit_context_release (state.ctxt);
toyvm_compiled_function *toyvm_result =
(toyvm_compiled_function *)calloc (1, sizeof (toyvm_compiled_function));
if (!toyvm_result)
{
fprintf (stderr, "out of memory allocating toyvm_compiled_function\n");
gcc_jit_result_release (jit_result);
return NULL;
}
toyvm_result->cf_jit_result = jit_result;
toyvm_result->cf_code =
(toyvm_compiled_code)gcc_jit_result_get_code (jit_result,
funcname);
/* (this leaks "jit_result" and "funcname") */
free (funcname);
return toyvm_result;
}
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_function *compiled_fn;
toyvm_compiled_code code;
int compiled_result;
snprintf (test, sizeof (test), "toyvm.c: %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 = toyvm_function_interpret (fn, input, NULL);
CHECK_VALUE (interpreted_result, expected_result);
compiled_fn = toyvm_function_compile (fn);
CHECK_NON_NULL (compiled_fn);
code = (toyvm_compiled_code)compiled_fn->cf_code;
CHECK_NON_NULL (code);
compiled_result = code (input);
CHECK_VALUE (compiled_result, expected_result);
gcc_jit_result_release (compiled_fn->cf_jit_result);
free (compiled_fn);
free (fn);
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.c");
/* 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)
toyvm_function_disassemble (fn, stdout);
printf ("interpreter result: %d\n",
toyvm_function_interpret (fn, atoi (argv[2]), NULL));
/* JIT-compilation. */
toyvm_compiled_function *compiled_fn
= toyvm_function_compile (fn);
toyvm_compiled_code code = compiled_fn->cf_code;
printf ("compiler result: %d\n",
code (atoi (argv[2])));
gcc_jit_result_release (compiled_fn->cf_jit_result);
free (compiled_fn);
return 0;
}