#include <stdint.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <inttypes.h>
// PRINCIPLE
/*
A simple 64-bit bytecode virtual machine in portable C, suitable
for being the core of a HYDROGEN implementation.
Instructions are packed several into each cell. A cell of instructions
is loaded into IR, the Instruction Register, and executed from the
least significant instruction to the most until we run out of instructions and move on to the next cell full of instructions.
However if any instructions consume immediate operands, they are taken
from the next cell and the instruction pointer IP is moved forwards.
This means that a single cell full of instructions might be followed
by several cells of immediate values consumed by those instructions.
We make instruction 0 be the one that loads a new cell full of instructions into
IR, so that we always end up with that instruction as we shift IR down an
instruction at a time, or we can explicitly use instruction 0 to terminate a
group of instructions in a cell early.
*/
// CONFIG
typedef int64_t cell;
typedef uint64_t u_cell;
#define CELL_BITS 64
#define BITS_PER_INSTRUCTION 8
#define DEBUG
// INSN_NEXT *must* be 0. All others can be arbitrary values as long as they're
// no larger than BITS_PER_INSTRUCTION bits.
#define INSN_NEXT 0 // Move to next instruction group cell
#define INSN_PUSH 1 // Push immediate
#define INSN_CALL 2 // Call to immediate address
#define INSN_ICALL 3 // Call to address from stack
#define INSN_RET 4 // Return from call
#define INSN_HALT 5 // Halt VM (TODO: Replace with more general vm-operations syscall instruction that takes an immediate syscall number)
#define INSN_IF 6 // Conditional call (pop cell from stack and read immediate word, call to immediate address if cell from stack is nonzero)
#define INSN_IFELSE 7 // Biconditional call (pop cell from stack and read two immediate words, call to first immediate address if cell from stack is nonzero or second otherwise
#define INSN_TAILCALL 8 // Tail-call immediate
#define INSN_TAILICALL 9 // Tail-call indirect
#define INSN_ADD 10
#define INSN_SUB 11
// TODO: Syscall codes for INSN_SYSCALL
#define SYSCALL_HALT 0 // Halt CPU
#define SYSCALL_CPUID 1 // Push CPUID
#define SYSCALL_MALLOC 2 // Alloc from C heap
#define SYSCALL_FREE 3 // Return to C heap
#define SYSCALL_READ_BOOT 4 // Read a character from the boot media stream, or EOF
// TODO: ...plus console syscalls, at least for logging for now
// Directives for the assembler (they look like instructions)
#define DIRECTIVE_CELL -1
#define DIRECTIVE_STRING -2
// TODO: A directive to define a constant, .define <immediate> "name"; goes into a linked list of constants in the assembler state; then a new kind of immediate syntax is ".name" that looks up the value of "name" in the constant list. Useful to make code a bit clearer. Re-defining the same name should update the existing linked list entry rather than adding a new one.
// Special value for invalid instruction
#define INVALID_INSTRUCTION -127
// TODO: Add pointer registers (IX,IY,IZ) to the VM with instructions to push/pop them to the stack, and to read/write cells/bytes from/to memory via them, with optional pre/post inc/decrement.
// TODO: Add REPEAT that acts like CALL but afterwards pops a number from the stack and repeats the call if it's nonzero.
// TODO: Add WHILE that has two immediate operands; CALLs the first and pops a cell and if it's nonzero calls the second and repeats the whole process.
// Both need to set something magic in the state on the stack to mark it as a
// REPEAT or WHILE, so that the RETs ending the called subroutines "know" to
// invoke the special behaviour rather than just continuing.
// TODO: Add instructions for managing task state. There's a task register
// pointing to the current task block, with instructions to push and pop
// it. There's also an instruction to switch tasks, which saves all the current
// registers to the current task block, pops a new task block pointer, and loads
// all the registers from it. And instructions to load/save user cells that are
// stored in the task block after the system-specific task data. This enables
// task switching, with user task-local data. The task-local data is needed at
// this stage because it's where all the "globals" in traditional FORTH will be
// kept - we need them task-local so we can be multithreaded at the core.
#define INSTRUCTION_COUNT 12
struct {
char *name;
int immediate_operands;
} insn_details[] = {
{"NEXT",0},
{"PUSH",1},
{"CALL",1},
{"ICALL",0},
{"RET",0},
{"HALT",0},
{"IF",1},
{"IFELSE",2},
{"TAILCALL",1},
{"TAILICALL",0},
{"ADD",0},
{"SUB",0}
};
int insn_lookup(char *name) {
if(!strcasecmp(".cell", name)) {
return DIRECTIVE_CELL;
}
if(!strcasecmp(".string", name)) {
return DIRECTIVE_STRING;
}
for(int i=0;i<INSTRUCTION_COUNT;i++) {
if(!strcasecmp(insn_details[i].name, name))
return i;
}
return INVALID_INSTRUCTION;
}
// ENGINE
void hydrogen(cell CPUID, u_cell *IP, cell *SP, cell *SP0, cell *RSP, cell *RSP0) {
u_cell IR;
// Preload instruction register with first group
IR = *(IP++);
// Instructions that don't load IR need to end with this macro to move to the
// next instruction in the IR. This zero-fills from the most significant bit
// so we will eventually hit "instruction" 0, INSN_NEXT.
#define NEXT IR >>= BITS_PER_INSTRUCTION; break;
// Macro to read the next word from the instruction stream
#define IMMED *(IP++)
// Macros to access the stacks
#define PUSH(x) *(--SP) = (x)
#define POP *(SP++)
#define RPUSH(x) *(--RSP) = (x)
#define RPOP *(RSP++)
// Execution loop
while(1) {
// Mask off least significant instruction in the IR and branch on it
cell insn = IR & ((1<<BITS_PER_INSTRUCTION)-1);
#ifdef DEBUG
printf("IP = %p, IR = %" PRIx64 ", insn = %s", IP, IR, insn_details[insn].name);
for(int i=0;i<insn_details[insn].immediate_operands;i++) {
printf (" %" PRIx64, IP[i]);
}
printf("\nSTACK : ");
for(cell *sp = SP; sp < SP0; sp++)
printf(" %" PRIx64, *sp);
printf("\nRSTACK: ");
for(cell *sp = RSP; sp < RSP0; sp++)
printf(" %" PRIx64, *sp);
printf("\n");
#endif
switch(insn) {
case INSN_NEXT: // End of instruction group in the IR, load next group
IR = *(IP++);
break;
case INSN_PUSH: // Push immediate
PUSH((cell)IMMED);
NEXT
case INSN_CALL: // Call immediate
//Save our IR and IP on the return stack
RPUSH(IR>>BITS_PER_INSTRUCTION); /* IR must be pre-advanced to the
next instruction or we'll return
straight back to the call */
RPUSH((cell)(IP+1)); // Skip the immediate address
IP = (u_cell*)IMMED; // Load address to call to from our instruction stream
IR = IMMED; // Load first group into IR from the callee's instruction stream
break;
case INSN_ICALL: // Indirect call to address in SP[0]
//Save our IR and IP on the return stack
RPUSH(IR>>BITS_PER_INSTRUCTION); /* IR must be pre-advanced to the
next instruction or we'll return
straight back to the call */
RPUSH((cell)IP);
IP = (cell*)POP; // Load address to call to from stack
IR = IMMED; // Load first group into IR from the callee's instruction stream
break;
case INSN_RET:
// Restore IR and IP from the return stack
IP = (cell*)RPOP;
IR = RPOP;
break;
case INSN_HALT:
// Terminate the VM. Probably replace this with a more generic "trap"
// instruction that takes a code in SP[0] and uses it to dispatch to other
// VM-specific operations such as I/O, to not waste opcodes.
return;
case INSN_IF:
if(POP != 0) {
//Save our IR and IP on the return stack
RPUSH(IR>>BITS_PER_INSTRUCTION); /* IR must be pre-advanced to the
next instruction or we'll return
straight back to the call*/
RPUSH((cell)(IP+1)); // Skip the immediate address
IP = (u_cell*)IMMED; // Load address to call to from our instruction stream
IR = IMMED; // Load first group into IR from the callee's instruction stream
break;
} else {
IP++; // Skip unused call target
NEXT
}
case INSN_IFELSE:
if(POP != 0) {
//Save our IR and IP on the return stack
RPUSH(IR>>BITS_PER_INSTRUCTION); /* IR must be pre-advanced to the
next instruction or we'll return
straight back to the call */
RPUSH((cell)(IP+2)); // Skip two immediate addresses
IP = (u_cell*)IMMED; // Load address to call to from our instruction stream
IR = IMMED; // Load first group into IR from the callee's instruction stream
} else {
//Save our IR and IP on the return stack
RPUSH((IR>>BITS_PER_INSTRUCTION)); /* IR must be pre-advanced to the
next instruction or we'll return
straight back to the call */
RPUSH((cell)(IP+2)); // Skip two immediate addresses
IP++; // Skip IF address, we're going to the ELSE address
IP = (u_cell*)IMMED; // Load address to call to from our instruction stream
IR = *(IP++); // Load first group into IR from the callee's instruction stream
}
break;
case INSN_TAILCALL:
// Tail call: effectively a jump, RET from this routine and then CALL the next in one swoop. Discards current IP/IR.
IP = (u_cell*)IMMED; // Load address to call to from our instruction stream
IR = IMMED; // Load first group into IR from the callee's instruction stream
break;
case INSN_TAILICALL:
// Tail indirect call: effectively a jump, RET from this routine and then ICALL the next in one swoop. Discards current IP/IR.
IP = (cell*)POP; // Load address to call to from stack
IR = IMMED; // Load first group into IR from the callee's inst
break;
case INSN_ADD:
SP[1] += SP[0];
SP++;
NEXT
case INSN_SUB:
SP[1] -= SP[0];
SP++;
NEXT
}
}
}
// Assembler
/*
Define a little struct full of state that assembles instructions into a memory buffer, handling the location of instruction group cells and literal operand cells for us automatically.
*/
#define MAX_INSTRUCTIONS_PER_CELL (CELL_BITS/BITS_PER_INSTRUCTION)
#define MAX_IMMEDIATES_PER_INSTRUCTION 2 // IFELSE has 2 immediates
#define MAX_IMMEDIATE_CELLS MAX_INSTRUCTIONS_PER_CELL*MAX_IMMEDIATES_PER_INSTRUCTION
// Must be longer than the longest instruction name from insn_details and the longest numerical literal and the longest label
#define MAX_TOKEN_LENGTH 128
struct label_def {
struct label_def *next; // Linked list
cell *location;
char *name; // malloced memory
};
typedef struct {
cell *ip; // Where to assemble to
cell *limit; // Do not write to this address or beyond
u_cell insn_buffer;
int insn_offset;
cell immed_buffer[MAX_IMMEDIATE_CELLS];
int immediates;
// Named labels
struct label_def *labels;
// Stuff for the parser
enum {
PS_INITIAL,
PS_LABEL_DEF,
PS_INSTRUCTION,
PS_AFTER_INSTRUCTION,
PS_LABEL_IMMEDIATE,
PS_HEX_IMMEDIATE,
PS_DECIMAL_IMMEDIATE,
PS_STRING_IMMEDIATE,
PS_STRING_IMMEDIATE_ESCAPE,
PS_LINE_COMMENT,
PS_BLOCK_COMMENT
} parse_state;
char parse_buffer[MAX_TOKEN_LENGTH+1];
int parse_buffer_pos;
int parse_instruction;
cell parse_immediates[MAX_IMMEDIATE_CELLS];
int parse_immediate_count;
int parse_block_comment_depth;
} assembler;
void asm_init(assembler *a, cell *ip, cell *limit) {
assert(ip < limit);
a->ip = ip;
a->limit = limit;
a->insn_buffer = 0;
a->insn_offset = 0;
a->immediates = 0;
a->labels = NULL;
a->parse_state = PS_INITIAL;
}
// Flush instruction buffer to ip
void asm_flush(assembler *a) {
if(a->insn_buffer == 0) {
assert(a->immediates == 0);
assert(a->insn_offset == 0);
// Nothing to flush
} else {
assert(a->ip < a->limit);
*(a->ip++) = (cell)a->insn_buffer;
for(int i=0;i<a->immediates;i++) {
assert(a->ip < a->limit);
*(a->ip++) = (u_cell)a->immed_buffer[i];
}
a->insn_buffer = 0;
a->insn_offset = 0;
a->immediates = 0;
}
}
void asm_literal_cell(assembler *a, cell data) {
asm_flush(a);
*(a->ip++) = data;
}
void asm_literal_string(assembler *a, char *data) {
asm_flush(a);
char *char_ip = (char*)a->ip;
int len = 0;
while(*data) {
*char_ip++ = *data++;
len++;
}
// Trailing \0
*char_ip++ = '\0';
len++;
// Fix alignment by adding more \0s
while(len % (CELL_BITS/8)) {
*char_ip++ = '\0';
len++;
}
// Update ip
a->ip = (cell*)char_ip;
}
void asm_done(assembler *a) {
asm_flush(a);
struct label_def *ld = a->labels;
while (ld != NULL) {
free(ld->name);
struct label_def *tmp = ld;
ld = ld->next;
free(tmp);
}
}
u_cell *asm_label(assembler *a, char *name) {
asm_flush(a);
struct label_def *ld = (struct label_def*)malloc(sizeof(struct label_def));
assert(ld != NULL);
ld->next = a->labels;
ld->location = a->ip;
ld->name = strdup(name);
a->labels = ld;
return a->ip;
}
u_cell *asm_label_lookup(assembler *a, char *name) {
struct label_def *ld = a->labels;
while(ld != NULL) {
if (!strcmp(ld->name, name))
return ld->location;
ld = ld->next;
}
return NULL;
}
void asm_assemble(assembler *a, u_cell instruction, int num_immediates, cell *immediates) {
// Put the instruction in the buffer
a->insn_buffer |= (instruction << a->insn_offset);
a->insn_offset += BITS_PER_INSTRUCTION;
// Add any immediates
for(int i=0;i<num_immediates;i++) {
a->immed_buffer[a->immediates++] = immediates[i];
}
// Flush if buffer is now full
if (a->insn_offset >= CELL_BITS) {
asm_flush(a);
}
}
void asm_assemble0(assembler *a, u_cell instruction) {
asm_assemble(a, instruction, 0, NULL);
}
void asm_assemble1(assembler *a, u_cell instruction, cell immediate) {
asm_assemble(a, instruction, 1, &immediate);
}
void asm_assemble2(assembler *a, u_cell instruction, cell immediate1, cell immediate2) {
cell immediates[2] = {immediate1,immediate2};
asm_assemble(a, instruction, 2, immediates);
}
// returns NULL on success, or a static error string. The syntax that this
// parses isn't really documented - see *.hydrogen for examples and go from
// there, unless you fancy reading the raw state machine herein:
char *asm_parse(assembler *a,char ch) {
// TODO: Add directives to assemble literal data directly to IP, as cells, bytes, or NULL-terminated ASCII strings
int instruction_complete = 0;
switch(a->parse_state) {
case PS_INITIAL:
if(!isspace(ch)) {
if(ch == ':') {
a->parse_state = PS_LABEL_DEF;
a->parse_buffer_pos = 0;
} else if(ch == '\'') {
a->parse_state = PS_LINE_COMMENT;
} else if(ch == '{') {
a->parse_state = PS_BLOCK_COMMENT;
a->parse_block_comment_depth = 1;
} else if(isalpha(ch) || ch == '.') { // Directives are like instructions but start with a .
a->parse_state = PS_INSTRUCTION;
a->parse_buffer[0] = ch;
a->parse_buffer_pos = 1;
} else {
return "Invalid character in PS_INTIIAL";
}
} // else continue in PS_INITIAL
break;
case PS_LABEL_DEF:
if(isspace(ch)) {
// End of label def
a->parse_buffer[a->parse_buffer_pos] = '\0';
asm_label(a, a->parse_buffer);
a->parse_state = PS_INITIAL;
} else {
if(a->parse_buffer_pos >= MAX_TOKEN_LENGTH) return "Label definition name too long";
a->parse_buffer[a->parse_buffer_pos++] = ch;
}
break;
case PS_INSTRUCTION:
if(ch == '\n') {
// End of instruction (no immediates)
a->parse_buffer[a->parse_buffer_pos] = '\0';
a->parse_instruction = insn_lookup(a->parse_buffer);
if (a->parse_instruction == INVALID_INSTRUCTION)
return "Invalid instruction name";
a->parse_immediate_count = 0;
a->parse_state = PS_INITIAL;
instruction_complete = 1;
} else if(isspace(ch)) {
// End of instruction name but there's immediates to be found
a->parse_buffer[a->parse_buffer_pos] = '\0';
a->parse_instruction = insn_lookup(a->parse_buffer);
if (a->parse_instruction == INVALID_INSTRUCTION)
return "Invalid instruction name";
a->parse_state = PS_AFTER_INSTRUCTION;
a->parse_immediate_count = 0;
} else {
if(a->parse_buffer_pos >= MAX_TOKEN_LENGTH) return "Instruction name too long";
a->parse_buffer[a->parse_buffer_pos++] = ch;
}
break;
case PS_AFTER_INSTRUCTION:
if(!isspace(ch)) {
switch(ch) {
case '\n': // End of instruction
a->parse_state = PS_INITIAL;
instruction_complete = 1;
break;
case '\'': // End of instruction, line comment
a->parse_state = PS_LINE_COMMENT;
instruction_complete = 1;
break;
case '@': // Label immediate
a->parse_state = PS_LABEL_IMMEDIATE;
a->parse_buffer_pos = 0;
break;
case '#': // Hex immediate
a->parse_state = PS_HEX_IMMEDIATE;
a->parse_buffer_pos = 0;
break;
case '=': // Decimal immediate
a->parse_state = PS_DECIMAL_IMMEDIATE;
a->parse_buffer_pos = 0;
break;
case '"': // String immediate (special case for .string directive)
a->parse_state = PS_STRING_IMMEDIATE;
a->parse_buffer_pos = 0;
break;
default:
return "Unknown character in PS_AFTER_INSTRUCTION";
}
} // Else continue in PS_AFTER_INSTRUCTION
break;
case PS_LABEL_IMMEDIATE:
if(isspace(ch)) {
a->parse_buffer[a->parse_buffer_pos] = '\0';
cell *location = asm_label_lookup(a, a->parse_buffer);
if(!location) return "Reference to unknown label";
a->parse_immediates[a->parse_immediate_count++] = (cell)location;
if(ch == '\n') {
a->parse_state = PS_INITIAL;
instruction_complete = 1;
} else {
a->parse_state = PS_AFTER_INSTRUCTION;
}
} else {
if(a->parse_buffer_pos >= MAX_TOKEN_LENGTH) return "Label reference too long";
a->parse_buffer[a->parse_buffer_pos++] = ch;
}
break;
case PS_HEX_IMMEDIATE:
if(isspace(ch)) {
a->parse_buffer[a->parse_buffer_pos] = '\0';
cell val = strtoll(a->parse_buffer, NULL, 16);
a->parse_immediates[a->parse_immediate_count++] = val;
if(ch == '\n') {
a->parse_state = PS_INITIAL;
instruction_complete = 1;
} else {
a->parse_state = PS_AFTER_INSTRUCTION;
}
} else {
if(a->parse_buffer_pos >= MAX_TOKEN_LENGTH) return "Hex immediate too long";
a->parse_buffer[a->parse_buffer_pos++] = ch;
}
break;
case PS_DECIMAL_IMMEDIATE:
if(isspace(ch)) {
a->parse_buffer[a->parse_buffer_pos] = '\0';
cell val = strtoll(a->parse_buffer, NULL, 10);
a->parse_immediates[a->parse_immediate_count++] = val;
if(ch == '\n') {
a->parse_state = PS_INITIAL;
instruction_complete = 1;
} else {
a->parse_state = PS_AFTER_INSTRUCTION;
}
} else {
if(a->parse_buffer_pos >= MAX_TOKEN_LENGTH) return "Decimal immediate too long";
a->parse_buffer[a->parse_buffer_pos++] = ch;
}
break;
case PS_STRING_IMMEDIATE:
switch(ch) {
case '\\':
a->parse_state = PS_STRING_IMMEDIATE_ESCAPE;
break;
case '\"': // End of string
a->parse_buffer[a->parse_buffer_pos] = '\0'; // We leave the string in the buffer
a->parse_immediates[a->parse_immediate_count++] = 0; // Leave a dummy immediate value
a->parse_state = PS_AFTER_INSTRUCTION;
break;
default:
if(a->parse_buffer_pos >= MAX_TOKEN_LENGTH) return "String immediate too long";
a->parse_buffer[a->parse_buffer_pos++] = ch;
}
break;
case PS_STRING_IMMEDIATE_ESCAPE:
if(a->parse_buffer_pos >= MAX_TOKEN_LENGTH) return "String immediate too long";
a->parse_buffer[a->parse_buffer_pos++] = ch;
a->parse_state = PS_STRING_IMMEDIATE;
case PS_LINE_COMMENT:
if(ch == '\n') {
a->parse_state = PS_INITIAL;
} // else: continue in line comment
break;
case PS_BLOCK_COMMENT:
if(ch == '{') {
a->parse_block_comment_depth++;
} else if(ch == '}') {
a->parse_block_comment_depth--;
if(a->parse_block_comment_depth == 0) {
a->parse_state = PS_INITIAL;
}
} // Else continue in block comment
break;
}
// Common code that ends many different cases so we put it behind a flag
if(instruction_complete) {
// Directive or real instruction?
switch(a->parse_instruction) {
case DIRECTIVE_CELL:
if(a->parse_immediate_count != 1)
return ".cell requires 1 operand";
asm_literal_cell(a, a->parse_immediates[0]);
break;
case DIRECTIVE_STRING:
// String literal is retained in buffer
if(a->parse_immediate_count != 1)
return ".string requires 1 operand";
asm_literal_string(a, a->parse_buffer);
break;
default: // Real instruction
// Check number of immediates matches the instruction opcode by looking in insn_details
if(insn_details[a->parse_instruction].immediate_operands != a->parse_immediate_count)
return "Incorrect number of immediate operands for instruction";
asm_assemble(a, a->parse_instruction, a->parse_immediate_count, a->parse_immediates);
}
instruction_complete = 0;
}
return NULL;
}
// Boot loader
/*
Use the assembler to load a minimal HYDROGEN boot loader from a file that then
expands into a full HYDROGEN interpiler, Jonesforth style, that loads up a
HYDROGEN standard library.
*/
/* TODO: Start writing HYDROGEN kernel in assembly
Change test.hydrogen to test.hasm
Make a series of .hasm files that the makefile cats together into
"hydrogen-core.hasm" that we can load and run. Maybe feed it through a
preprocessor so we can have macros?
First part: root dictionary
Start putting together hardcoded dictionary headers for core system
words. Use a redefined-each-time :label to track the current head of the
linked list.
Define basic words for all the core capabilities of the VM.
Second part: heap management
Have a syscall give us access to the C system heap via malloc/free
Create a library for managing an apr_pool style memory pool system. The root
pool is the C heap. Pools have parent pools they use to do their own
allocations from; they can, in turn, offer sub-allocations which are either
"freeable" (just passed on to the parent heap, but the pool keeps a reference
to it in a linked list of things to free when the pool is destroyed) which
can be explicited "freed" when no longer used (freeing it in theparent heap
and removing it from our list of things to free) or "fixed" (allocated by
advancing a pointer in a memory block obtained on demand from the parent
heap, which is also in the linked list of things to free); and the whole pool
can be destroyed, freeing everything in its tracking list.
Third part: allocation streams
Create the HYDROGEN allocation stream library, that lets you create a stream
(in a heap pool). You can write cells and strings and other HYDROGEN types to
the stream, then when you destroy the stream, it turns into a single block of
memory (allocated from the heap pool, fixed or freeable at your choice) with
all the stuff you write contiguously in it. Works by creating a sub-pool and
doing fixed allocations of a linked list of buffer pages, then concatenating
them into one at the end.
Fourth part: compiler
Create the HYDROGEN compiler library, that lets you create a compile context
with a dictionary pointer. Lets you compile words as a sequence of PUSH and
CALL operations, and then end them, getting an XT for a word that can be
CALLed. Also lets you create dictionary entries giving names to words or look
up dictionary entries. The compile context has an allocation stream from
which all allocated objects are created, and when "done" closes the stream
into a fixed memory block in the parent heap pool given to the compile
context. To resolve pointers, it needs to keep a linked list of offsets into
the allocation stream that should be fixed up with the resulting base address
of the allocated block, stored in a sub-pool that gets destroyed at the end.
TODO: Add inlining of small words when they are CALLed
TODO: Add peephole optimiser that fixes up common patterns, like
PUSH-then-ICALL, PUSH-then-TAILICALL, etc
Fifth part: interpreter
Create the HYDROGEN interpreter, that (given a compile context) reads words
from an input stream (passed to the interpreter as an XT for a word that
returns a character or EOF), looks them up in the compile context's
dictionary, and runs them in immediate mode.
Defines the ( word that creates a new compile context with the parent's
dicionary pointer and compiles code until a matching ), then pushes the XT of
the resulting word (or, when being compiled, compiles code to push the XT).
Sixth part: Bootstrap HYDROGEN
Binds CORE-DICTIONARY to the hardcoded root dictionary, then that's the last
word defined in it.
Uses a syscall to access "boot media" (which just reads
hydrogen-core.hydrogen) to apply the interpreter to the hydrogen source for
the hydrogen-in-c kernel, then again for whatever HYDROGEN
(implementation-unspecific!) application code we are running today, all in a
compile context in a new "system" heap pool with the hardcoded root
dictionary full of words we've defined thus far.
Binds APPLICATION-DICTIONARY to the resulting dictionary (in itself), by
mutating a reference set up in hydrogen-core.hydrogen.
End result should be a word called "start" being defined in the application
dictionary, which the top-level driver can invoke once all the vCPU threads
are running.
*/
cell *asm_load(cell *program_buffer, int program_buffer_cells, char *filename) {
assembler a;
asm_init(&a, program_buffer, program_buffer+program_buffer_cells);
FILE *fp = fopen(filename, "r");
assert(fp);
int line = 1;
int col = -1;
while(!feof(fp)) {
int ch = fgetc(fp);
// Track line/col and report them in error messages
if(ch == '\n') {
col = 0;
line++;
} else {
col++;
}
if(ch != EOF) {
char *error = asm_parse(&a, ch);
if(error) {
printf ("ASM error on line %d col %d: %s\n", line, col, error);
}
assert(error == NULL);
}
}
asm_flush(&a);
printf("Assembly complete, used %d cells\n", a.ip - program_buffer);
cell *main_start= asm_label_lookup(&a, "main");
asm_done(&a);
return main_start;
}
// TODO: Top level driver
/*
Create a POSIX thread per virtual CPU and use them to run the core image the
boot loader loaded, as per the HYDROGEN boot spec, and present the initial
device tree.
*/
// TEST HARNESS
int main(void) {
const int program_space_size = 64;
cell program[program_space_size];
cell *main_start = asm_load(program, program_space_size, "test.hydrogen");
// Check literal data at start of test.hydrogen was assembled correctly
assert(program[0] == 12345); // Literal integer
assert(program[1] == 0x6f77206f6c6c6548); // "Hello wo"
assert(program[2] == 0x0000000000646c72); // "rld_____" where _ = \0
cell stack[16] = {0};
cell rstack[16] = {0};
// Push 1 to initial stack
stack[15] = 1;
hydrogen(0, main_start, stack+15, stack+16, rstack+16, rstack+16);
printf("Answer should be 11: It's %" PRId64 "\n", stack[15]);
assert(stack[15] == 11); // Correct answer from computation
}