#include #include #include #include #include #include #include // 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 "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>= 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); /* 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;iimmediates;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;iimmed_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 }