Overhaul VM design

Overhaul the VM design to more closely resemble System40.exe. See
system4.md for further details.
This commit is contained in:
Nunuhara Cabbage
2019-10-13 17:51:22 -07:00
parent 827e0a85d7
commit 8a40058c81
4 changed files with 292 additions and 131 deletions
+15 -11
View File
@@ -36,19 +36,23 @@ void free_string(struct string *str)
free(str);
}
struct string *string_dup(struct string *in)
{
struct string *out = xmalloc(sizeof(struct string) + in->size + 1);
out->size = in->size;
out->literal = false;
memcpy(out->text, in->text, in->size + 1);
return out;
}
struct string *string_append(struct string *a, struct string *b)
{
if (!a->literal) {
a = xrealloc(a, sizeof(struct string) + a->size + b->size + 1);
} else {
struct string *tmp = xmalloc(sizeof(struct string) + a->size + b->size + 1);
memcpy(tmp, a, sizeof(struct string) + a->size);
tmp->literal = false;
a = tmp;
}
memcpy(a->text + a->size, b->text, b->size + 1);
a->size = a->size + b->size;
return a;
struct string *s = xmalloc(sizeof(struct string) + a->size + b->size + 1);
s->size = a->size + b->size;
s->literal = false;
memcpy(s->text, a->text, a->size);
memcpy(s->text + a->size, b->text, b->size + 1);
return s;
}
struct string *integer_to_string(int n)
+73
View File
@@ -0,0 +1,73 @@
System 4
========
Memory Management
-----------------
### The Stack
Arguments and return values are pushed and popped from a stack. This stack does
not actually grow that fast since function arguments are immediately removed
from the stack and stored in a "page" by the CALLFUNC instruction.
### Pages
Non-pointer variables are stored in "pages". There is a global page for global
variables, and a local page for each function call.
The SH_LOCAL* family of instructions implicitly operate on the current page.
### The Heap
The VM maintains an array of pointers to heap-backed objects (strings, structs,
pages). When a heap-backed object is stored in a variable or put on the stack,
an index into this array is used to represent the object.
The PUSHLOCALPAGE instruction pushes the heap-index of the current page object
to the stack.
### Automatic Memory Management
System 4 uses reference counting to track the lifetime of heap objects. Because
the System 4 language does not have weak references, it is possible to create
memory leaks by creating circular references. E.g. the following code leaks:
struct a {
ref b ref_b;
};
struct b {
ref a ref_a;
};
void leak(void)
{
a local_a;
b local_b;
local_a.ref_b <- local_b;
local_b.ref_a <- local_a;
// the reference count of local_a and local_b never reach zero because
// they each hold a reference to the other, even after they're both out
// of scope
}
Calling Convention
------------------
Arguments and return values are passed on the stack. First, the caller pushes
the arguments, in order, and then issues the CALLFUNC instruction with the ID
of the function to be called.
The CALLFUNC instruction pops the arguments off of the stack, stores them in a
fresh page object, then jumps to the beginning of the function. When the
function is finished executing, it pushes its return value onto the stack, then
issues the RETURN instruction. The RETURN instruction jumps back to the
instruction immediately following the CALLFUNC instruction in the caller.
### Passing Arguments By Reference
To pass a variable by reference, the caller pushes two values: first, the index
of the page object to which the variable belongs; second, the index of the
variable within the page. Variable references can be accessed with the REF (get)
and ASSIGN (set) instructions.
+203 -120
View File
@@ -23,33 +23,103 @@
#include "instructions.h"
#include "little_endian.h"
#define INITIAL_STACK_SIZE 1024
#define INITIAL_HEAP_SIZE 4096
#define INITIAL_PAGES_SIZE 4096
/*
* NOTE: The current implementation is a simple bytecode interpreter.
* System40.exe uses a JIT compiler, and we should too.
*/
// Non-heap values. Stored in pages and on the stack.
union vm_value {
int32_t i;
int64_t i64;
float f;
struct string *s;
};
static union vm_value *stack = NULL;
static size_t stack_size;
static int32_t stack_ptr = 0;
enum vm_pointer_type {
VM_PAGE,
VM_STRING
};
static union vm_value frame_stack[4096];
static int32_t frame_ptr = 0;
// Heap-backed objects. Reference counted.
struct vm_pointer {
int ref;
enum vm_pointer_type type;
union {
struct string *s;
union vm_value *page;
};
};
#define FRAME_FN_OFF 0 // offset to function number
#define FRAME_IP_OFF 1 // offset to return address
#define FRAME_FP_OFF 2 // offset to frame pointer
#define FRAME_VAR_OFF 3 // offset to variables
struct function_call {
int32_t fno;
int32_t return_address;
int32_t page_slot;
int32_t page_ptr;
};
// The stack
static union vm_value *stack = NULL; // the stack
static int32_t stack_ptr = 0; // pointer to the top of the stack
static size_t stack_size; // current size of the stack
// The heap
// An array of pointers to heap-allocated objects, plus reference counts.
static struct vm_pointer *heap;
static size_t heap_size;
// Heap free list
// This is a list of unused indices into the 'heap' array.
static int32_t *heap_free_stack;
static int32_t heap_free_ptr = 0;
// Memory for global page + local pages
static union vm_value *page_stack;
static int32_t page_ptr = 0; // points to start of current local page
static int32_t pages_size;
// Stack of function call frames
static struct function_call call_stack[4096];
static int32_t call_stack_ptr = 0;
static struct ain *ain;
static size_t instr_ptr = 0;
static int32_t heap_alloc_slot(enum vm_pointer_type type)
{
int32_t slot = heap_free_stack[heap_free_ptr++];
heap[slot].ref = 1;
heap[slot].type = type;
return slot;
}
static void heap_free_slot(int32_t slot)
{
heap_free_stack[--heap_free_ptr] = slot;
}
static void heap_ref(int32_t slot)
{
heap[slot].ref++;
}
static void heap_unref(int32_t slot)
{
if (--heap[slot].ref <= 0) {
switch (heap[slot].type) {
case VM_PAGE:
break;
case VM_STRING:
free_string(heap[slot].s);
break;
}
heap_free_slot(slot);
}
}
static union vm_value _vm_id(union vm_value v)
{
return v;
@@ -70,37 +140,26 @@ static union vm_value vm_float(float v)
return (union vm_value) { .f = v };
}
static union vm_value vm_string(struct string *v)
{
return (union vm_value) { .s = v };
}
#define vm_value_cast(v) _Generic((v), \
union vm_value: _vm_id, \
int32_t: vm_int, \
int64_t: vm_long, \
float: vm_float, \
struct string*: vm_string)(v)
float: vm_float)(v)
static int32_t local_get(int varno)
{
return frame_stack[frame_ptr + FRAME_VAR_OFF + varno].i;
}
static int32_t local_ref(int varno)
{
return frame_ptr + FRAME_VAR_OFF + varno;
return page_stack[page_ptr + varno].i;
}
static void local_set(int varno, int32_t value)
{
frame_stack[frame_ptr + FRAME_VAR_OFF + varno].i = value;
page_stack[page_ptr + varno].i = value;
}
static enum ain_data_type local_type(int varno)
{
int32_t fno = frame_stack[frame_ptr + FRAME_FN_OFF].i;
return ain->functions[fno].vars[varno].data_type;
struct ain_function *f = &ain->functions[call_stack[call_stack_ptr-1].fno];
return f->vars[varno].data_type;
}
// Read the opcode at ADDR.
@@ -132,71 +191,92 @@ static union vm_value stack_pop(void)
}
// Pop a reference off the stack, returning the address of the referenced object.
static int32_t stack_pop_ref(void)
static union vm_value *stack_pop_ref(void)
{
int32_t index = stack_pop().i;
int32_t frame = stack_pop().i;
return frame + FRAME_VAR_OFF + index;
int32_t page_index = stack_pop().i;
int32_t heap_index = stack_pop().i;
return &heap[heap_index].page[page_index];
}
static void stack_push_string_literal(int32_t no)
static void stack_push_string(struct string *s)
{
stack[stack_ptr++].s = ain->strings[no];
}
static void stack_push_string(struct string *str)
{
stack[stack_ptr++].s = str;
}
static struct string *stack_pop_string(void)
{
stack_ptr--;
return stack[stack_ptr].s;
}
static void stack_toss_string(void)
{
stack_ptr--;
free_string(stack[stack_ptr].s);
int32_t heap_slot = heap_alloc_slot(VM_STRING);
heap[heap_slot].s = s;
stack[stack_ptr++].i = heap_slot;
}
static struct string *stack_peek_string(void)
{
return stack[stack_ptr-1].s;
return heap[stack_peek(0).i].s;
}
/*
* System 4 calling convention:
* - caller pushes arguments, in order
* - CALLFUNC creates stack frame ("page", on separate stack), pops arguments
* - CALLFUNC creates stack frame, pops arguments into local page
* - callee pushes return value on the stack
* - RETURN jumps to return address (saved in stack frame)
*/
static void function_call(int32_t no)
{
struct ain_function *f = &ain->functions[no];
int32_t cur_fno = frame_stack[frame_ptr + FRAME_FN_OFF].i;
int32_t new_fp = frame_ptr + FRAME_VAR_OFF + ain->functions[cur_fno].nr_vars;
int32_t cur_fno = call_stack[call_stack_ptr-1].fno;
//int32_t new_fp = frame_ptr + 1 + ain->functions[cur_fno].nr_vars;
int32_t page_slot = heap_alloc_slot(VM_PAGE);
int32_t new_pp = page_ptr + ain->functions[cur_fno].nr_vars;
// create new stack frame
frame_stack[new_fp + FRAME_FN_OFF].i = no;
frame_stack[new_fp + FRAME_IP_OFF].i = instr_ptr + instruction_width(CALLFUNC);
frame_stack[new_fp + FRAME_FP_OFF].i = frame_ptr;
call_stack[call_stack_ptr++] = (struct function_call) {
.fno = no,
.return_address = instr_ptr + instruction_width(CALLFUNC),
.page_slot = page_slot,
.page_ptr = page_ptr
};
// create local page
heap[page_slot].page = page_stack + new_pp;
for (int i = f->nr_args - 1; i >= 0; i--) {
frame_stack[new_fp + FRAME_VAR_OFF + i] = stack_pop();
page_stack[new_pp + i] = stack_pop();
}
// heap-backed variables need a allocate a slot
for (int i = f->nr_args; i < f->nr_vars; i++) {
int32_t slot;
switch (f->vars[i].data_type) {
case AIN_STRING:
slot = heap_alloc_slot(VM_STRING);
heap[slot].s = NULL;
page_stack[new_pp + i].i = slot;
break;
default:
break;
}
}
// update frame & instruction pointers
frame_ptr = new_fp;
// update stack/instruction pointers
page_ptr = new_pp;
instr_ptr = ain->functions[no].address;
}
static void function_return(void)
{
instr_ptr = frame_stack[frame_ptr + FRAME_IP_OFF].i;
frame_ptr = frame_stack[frame_ptr + FRAME_FP_OFF].i;
call_stack_ptr--;
// unref slots for heap-backed variables
struct ain_function *f = &ain->functions[call_stack[call_stack_ptr].fno];
for (int i = f->nr_args; i < f->nr_vars; i++) {
switch (f->vars[i].data_type) {
case AIN_STRING:
heap_unref(local_get(i));
break;
default:
break;
}
}
heap_free_slot(call_stack[call_stack_ptr].page_slot);
instr_ptr = call_stack[call_stack_ptr].return_address;
page_ptr = call_stack[call_stack_ptr].page_ptr;
}
static void system_call(int32_t code)
@@ -223,7 +303,7 @@ static void execute_instruction(int16_t opcode)
{
int32_t index, a, b, c, v;
union vm_value val;
struct string *sa, *sb;
union vm_value *ref;
const char *opcode_name = "UNKNOWN";
switch (opcode) {
//
@@ -233,27 +313,30 @@ static void execute_instruction(int16_t opcode)
stack_push(get_argument(0));
break;
case S_PUSH:
stack_push_string_literal(get_argument(0));
stack_push_string(ain->strings[get_argument(0)]);
break;
case POP:
stack_pop();
break;
case S_POP:
stack_toss_string();
index = stack_pop().i;
heap_unref(index);
break;
case REF:
// Dereference a reference to a value.
stack_push(frame_stack[stack_pop_ref()].i);
stack_push(stack_pop_ref()[0]);
break;
case S_REF:
// Dereference a reference to a string
stack_push_string(frame_stack[stack_pop_ref()].s);
index = stack_pop_ref()->i;
heap_ref(index);
stack_push(index);
break;
case REFREF:
// Dereference a reference to a reference.
index = stack_pop_ref();
stack_push(frame_stack[index].i);
stack_push(frame_stack[index + 1].i);
ref = stack_pop_ref();
stack_push(ref[0].i);
stack_push(ref[1].i);
break;
case DUP:
// A -> AA
@@ -288,25 +371,20 @@ static void execute_instruction(int16_t opcode)
stack_push(c);
break;
case PUSHLOCALPAGE:
stack_push(frame_ptr);
stack_push(call_stack[call_stack_ptr-1].page_slot);
break;
case ASSIGN:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i = v;
val = stack_pop();
stack_pop_ref()[0] = val;
break;
case SH_LOCALREF: // VARNO
// XXX: This instruction does different things depending on the
// type of the local variable.
a = get_argument(0);
switch (local_type(a)) {
index = get_argument(0);
stack_push(local_get(index));
switch (local_type(index)) {
case AIN_STRING:
// push a pointer to the stack (assignable)
stack_push(local_ref(a));
heap_ref(local_get(index));
break;
default:
// push the value to the stack (immediate)
stack_push(local_get(a));
break;
}
break;
@@ -438,76 +516,68 @@ static void execute_instruction(int16_t opcode)
// +=, -=, etc.
case PLUSA:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i += v;
stack_pop_ref()[0].i += v;
break;
case MINUSA:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i -= v;
stack_pop_ref()[0].i -= v;
break;
case MULA:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i *= v;
stack_pop_ref()[0].i *= v;
break;
case DIVA:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i /= v;
stack_pop_ref()[0].i /= v;
break;
case MODA:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i %= v;
stack_pop_ref()[0].i %= v;
break;
case ANDA:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i &= v;
stack_pop_ref()[0].i &= v;
break;
case ORA:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i |= v;
stack_pop_ref()[0].i |= v;
break;
case XORA:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i ^= v;
stack_pop_ref()[0].i ^= v;
break;
case LSHIFTA:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i <<= v;
stack_pop_ref()[0].i <<= v;
break;
case RSHIFTA:
v = stack_pop().i;
index = stack_pop_ref();
frame_stack[index].i >>= v;
stack_pop_ref()[0].i >>= v;
break;
case INC:
index = stack_pop_ref();
frame_stack[index].i++;
stack_pop_ref()[0].i++;
break;
case DEC:
index = stack_pop_ref();
frame_stack[index].i--;
stack_pop_ref()[0].i--;
break;
//
// --- Strings ---
//
case S_ASSIGN:
sa = stack_pop_string();
val = stack_pop();
frame_stack[val.i].s = sa;
stack_push_string(sa);
case S_ASSIGN: // A = B
b = stack_pop().i;
a = stack_peek(0).i;
if (heap[a].s) {
free_string(heap[a].s);
}
heap[a].s = string_dup(heap[b].s);
heap_unref(b);
break;
case S_ADD:
sb = stack_pop_string();
sa = stack_pop_string();
sa = string_append(sa, sb);
stack_push_string(sa);
b = stack_pop().i;
a = stack_pop().i;
stack_push_string(string_append(heap[a].s, heap[b].s));
heap_unref(a);
heap_unref(b);
break;
case I_STRING:
stack_push_string(integer_to_string(stack_pop().i));
@@ -525,16 +595,29 @@ static void execute_instruction(int16_t opcode)
void vm_execute(struct ain *program)
{
// initialize machine state
// initialize VM state
stack_size = INITIAL_STACK_SIZE;
stack = xmalloc(INITIAL_STACK_SIZE * sizeof(union vm_value));
stack_ptr = 0;
frame_ptr = 0;
stack_size = 1024;
stack = xmalloc(stack_size);
heap_size = INITIAL_HEAP_SIZE;
heap = xmalloc(INITIAL_HEAP_SIZE * sizeof(struct vm_pointer));
heap_free_stack = xmalloc(INITIAL_HEAP_SIZE * sizeof(int32_t));
for (size_t i = 0; i < INITIAL_HEAP_SIZE; i++) {
heap_free_stack[i] = i;
}
heap_free_ptr = 1; // global page at index 0
pages_size = INITIAL_PAGES_SIZE;
page_stack = xmalloc(INITIAL_PAGES_SIZE * sizeof(union vm_value));
page_ptr = 0;
ain = program;
// Jump to main. We set up a stack frame so that when main returns,
// the first instruction past the end of the code section is executed.
// ()When we read the AIN file, CALLSYS 0x0 was placed there.)
// (When we read the AIN file, CALLSYS 0x0 was placed there.)
instr_ptr = ain->code_size - instruction_width(CALLFUNC);
function_call(ain->main);
+1
View File
@@ -28,6 +28,7 @@ struct string {
struct string *make_string(const char *str, unsigned int len);
void free_string(struct string *str);
struct string *string_dup(struct string *in);
struct string *string_append(struct string *a, struct string *b);
struct string *integer_to_string(int n);