/**************************************************************************/ /* EightBall Virtual Machine */ /* */ /* The Eight Bit Algorithmic Language */ /* For Apple IIe/c/gs (64K), Commodore 64, VIC-20 +32K RAM expansion */ /* (also builds for Linux as 32 bit executable (gcc -m32) only) */ /* */ /* Compiles with cc65 v2.15 for VIC-20, C64, Apple II */ /* and gcc 7.3 for Linux */ /* */ /* Note that this code assumes that sizeof(int) = sizeof(int*), which is */ /* true for 6502 (16 bits each) and i686 (32 bits each) - but not amd64 */ /* */ /* cc65: Define symbol VIC20 to build for Commodore VIC-20 + 32K. */ /* Define symbol C64 to build for Commodore 64. */ /* Define symbol A2E to build for Apple //e. */ /* */ /* Copyright Bobbi Webber-Manners 2018 */ /* Reference implementation of EightBall Virtual Machine. */ /* */ /* This is not intended to be optimized for speed. I plan to implement */ /* an optimized version in 6502 assembler later. */ /* */ /* Formatted with indent -kr -nut */ /**************************************************************************/ /**************************************************************************/ /* GNU PUBLIC LICENCE v3 OR LATER */ /* */ /* This program 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 of the License, or */ /* (at your option) any later version. */ /* */ /* This program 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 this program. If not, see . */ /* */ /**************************************************************************/ /* #define DEBUG #define EXTRADEBUG #define DEBUGREGS #define DEBUGADDRESSING #define DEBUGSTACK */ /* Define STACKCHECKS to enable paranoid stack checking */ #ifdef __GNUC__ #define STACKCHECKS #else #undef STACKCHECKS #endif #include "eightballvm.h" #include "eightballutils.h" #include #include #include #ifdef A2E #include #endif #define EVALSTACKSZ 16 /* * Call stack grows down from top of memory. * If it hits CALLSTACKLIM then VM will quit with error */ #ifdef __GNUC__ #define MEMORYSZ (64 * 1024) #else /* Has to be 64K minus a byte otherwise winds up being zero! */ #define MEMORYSZ (64 * 1024) - 1 #endif #define CALLSTACKLIM (32 * 1024) #ifdef __GNUC__ #define UINT16 unsigned short #else #define UINT16 unsigned short #endif UINT16 pc = RTPCSTART; /* Program counter */ UINT16 sp = RTCALLSTACKTOP; /* Stack pointer */ UINT16 fp = RTCALLSTACKTOP; /* Frame pointer */ /* Evaluation stack - 16 bit ints. Addressed by evalptr */ UINT16 evalstack[EVALSTACKSZ]; /* evalptr points to the empty slot above the top of the evaluation stack */ unsigned char evalptr = 0; /* * System memory - addressed in bytes. * Used for program storage. Addressed by pc. * - Programs are stored from address 0 upwards. * Used for callstack. Addressed by sp. * - Callstack grows down from top of memory. */ #ifdef __GNUC__ unsigned char memory[MEMORYSZ]; #else unsigned char *memory = 0; #endif #define XREG evalstack[evalptr - 1] /* Only valid if evalptr >= 1 */ #define YREG evalstack[evalptr - 2] /* Only valid if evalptr >= 2 */ #define ZREG evalstack[evalptr - 3] /* Only valid if evalptr >= 3 */ #define TREG evalstack[evalptr - 4] /* Only valid if evalptr >= 4 */ /* * Error checks are called through macros to make it easy to * disable them in production. We should not need these checks * in production (assuming no bugs in the compiler!) ... but they * are helpful for debugging! */ #ifdef STACKCHECKS /* Check evaluation stack is not going to underflow */ #define CHECKUNDERFLOW(level) checkunderflow(level) /* Check evaluation stack is not going to overflow */ #define CHECKOVERFLOW() checkoverflow() /* Check call stack is not going to underflow */ #define CHECKSTACKUNDERFLOW(bytes) checkstackunderflow(bytes) /* Check call stack is not going to overflow */ #define CHECKSTACKOVERFLOW(bytes) checkstackoverflow(bytes) #else /* For production use, do not do these checks */ #define CHECKUNDERFLOW(level) #define CHECKOVERFLOW() #define CHECKSTACKUNDERFLOW(bytes) #define CHECKSTACKOVERFLOW(bytes) #endif /* Handler for unsupported bytecode */ #define UNSUPPORTED() unsupported() #ifdef STACKCHECKS /* * Check for evaluation stack underflow. * level - Number of 16 bit operands required on eval stack. */ void checkunderflow(unsigned char level) { if (evalptr < level) { print("Eval stack underflow\nPC="); printhex(pc); printchar('\n'); while (1); } } /* * Check evaluation stack is not going to overflow. * Assumes evalptr has already been advanced. */ void checkoverflow() { if (evalptr > EVALSTACKSZ - 1) { print("Eval stack overflow\nPC="); printhex(pc); printchar('\n'); while (1); } } /* * Check call stack is not going to underflow. * bytes - Number of bytes required on call stack. */ void checkstackunderflow(unsigned char bytes) { if ((MEMORYSZ - sp) < bytes) { print("Call stack underflow\nPC="); printhex(pc); printchar('\n'); while (1); } } /* * Check call stack is not going to overflow. * Assumes sp has already been advanced. */ void checkstackoverflow() { if (sp < CALLSTACKLIM + 1) { print("Call stack overflow\nPC="); printhex(pc); printchar('\n'); while (1); } } #endif /* * Handler for unsupported bytecodes */ void unsupported() { print("Unsupported instruction "); printhexbyte(memory[pc]); print("\nPC="); printhex(pc); printchar('\n'); while (1); } /* * Fetch, decode and execute a VM instruction, then advance the program counter. */ void execute_instruction() { unsigned short tempword; unsigned short *wordptr; unsigned char *byteptr; #ifndef __GNUC__ unsigned short delay; #endif //print("--->PC "); printhex(pc); print(" eval stk: "); printhex(evalptr); print("\n"); #ifdef DEBUGREGS unsigned short i; print("\n"); print("--->PC "); printhex(pc); print("\n"); print("--->SP "); printhex(sp); print("\n"); print("--->FP "); printhex(fp); print("\n"); print("Call Stk: "); for (i = sp + 1; i <= RTCALLSTACKTOP; ++i) { printhexbyte(memory[i]); printchar(' '); } print("\nEval Stk: "); printhex(XREG); printchar(' '); printhex(YREG); printchar(' '); printhex(ZREG); printchar(' '); printhex(TREG); printchar('\n'); #endif #ifdef DEBUG #ifdef A2E printchar('\r'); #else printchar('\n'); #endif printhex(pc); print(": "); print(bytecodenames[memory[pc]]); /* TODO: Should encode immediate mode as one bit of opcode to make this more efficient */ if ((memory[pc] == VM_LDIMM) || (memory[pc] == VM_LDAWORDIMM) || (memory[pc] == VM_LDABYTEIMM) || (memory[pc] == VM_LDRWORDIMM) || (memory[pc] == VM_LDRBYTEIMM) || (memory[pc] == VM_STAWORDIMM) || (memory[pc] == VM_STABYTEIMM) || (memory[pc] == VM_STRWORDIMM) || (memory[pc] == VM_STRBYTEIMM) || (memory[pc] == VM_JMPIMM) || (memory[pc] == VM_BRNCHIMM) || (memory[pc] == VM_JSRIMM)) { printchar(' '); wordptr = (unsigned short *)&memory[pc + 1]; printhex(*wordptr); printchar(' '); } else { print(" "); } #ifdef EXTRADEBUG print("stk: "); if (evalptr >= 1) { printhex(XREG); } if (evalptr >= 2) { print(", "); printhex(YREG); } if (evalptr >= 3) { print(", "); printhex(ZREG); } if (evalptr >= 4) { print(", "); printhex(TREG); } if (evalptr >= 5) { print(", "); printhex(evalstack[evalptr - 5]); } if (evalptr >= 6) { print(" ..."); } #endif #endif switch (memory[pc]) { /* * Miscellaneous */ case VM_END: /* Terminate execution */ if (evalptr > 0) { print("WARNING: evalptr "); printdec(evalptr); printchar('\n'); } #ifdef __GNUC__ exit(0); #else for (delay = 0; delay < 25000; ++delay); exit(0); #endif break; /* * Load Immediate */ case VM_LDIMM: /* Pushes the following 16 bit word to the evaluation stack */ ++evalptr; CHECKOVERFLOW(); wordptr = (unsigned short *)&memory[++pc]; ++pc; XREG = *wordptr; break; /* * Absolute addressing: * XREG points to absolute address within system memory. */ case VM_LDAWORD: /* Replaces X with 16 bit value pointed to by X. */ CHECKUNDERFLOW(1); #ifdef DEBUGADDRESSING print("\n XREG: "); printhex(XREG); printchar('\n'); #endif wordptr = (unsigned short *)&memory[XREG]; XREG = *wordptr; break; case VM_LDAWORDIMM: /* Imm mode - push 16 bit value pointed to by addr after opcode */ ++evalptr; CHECKOVERFLOW(); wordptr = (unsigned short *)&memory[++pc]; /* Pointer to operand */ wordptr = (unsigned short *)&memory[*wordptr]; /* Pointer to variable */ ++pc; XREG = *wordptr; break; case VM_LDABYTE: /* Replaces X with 8 bit value pointed to by X. */ CHECKUNDERFLOW(1); XREG = memory[XREG]; break; case VM_LDABYTEIMM: /* Imm mode - push byte pointed to by addr after opcode */ ++evalptr; CHECKOVERFLOW(); wordptr = (unsigned short *)&memory[++pc]; /* Pointer to operand */ byteptr = (unsigned char *)&memory[*wordptr]; /* Pointer to variable */ ++pc; XREG = *byteptr; break; case VM_STAWORD: /* Stores 16 bit value Y in addr pointed to by X. Drops X and Y.*/ CHECKUNDERFLOW(2); wordptr = (unsigned short *)&memory[XREG]; *wordptr = YREG; evalptr -= 2; break; case VM_STAWORDIMM: /* Imm mode - store 16 bit value X in addr after opcode. Drop X.*/ CHECKUNDERFLOW(1); wordptr = (unsigned short *)&memory[++pc]; /* Pointer to operand */ wordptr = (unsigned short *)&memory[*wordptr]; /* Pointer to variable */ ++pc; *wordptr = XREG; --evalptr; break; case VM_STABYTE: /* Stores 8 bit value Y in addr pointed to by X. Drops X and Y. */ CHECKUNDERFLOW(2); memory[XREG] = YREG; evalptr -= 2; break; case VM_STABYTEIMM: /* Imm mode - store 8 bit value X in addr after opcode. Drop X.*/ CHECKUNDERFLOW(1); wordptr = (unsigned short *)&memory[++pc]; /* Pointer to operand */ byteptr = (unsigned char *)&memory[*wordptr]; /* Pointer to variable */ ++pc; *byteptr = XREG; --evalptr; break; /* * Relative to Frame Pointer addressing: * XREG points to address in system memory relative to the frame pointer. */ case VM_LDRWORD: /* Replaces X with 16 bit value pointed to by X. */ CHECKUNDERFLOW(1); #ifdef DEBUGADDRESSING print("\n XREG: "); printhex(XREG); print(", FP: "); printhex(fp); print(" -> "); printhex((XREG + fp + 1) & 0xffff); printchar('\n'); #endif wordptr = (unsigned short *)&memory[(XREG + fp + 1) & 0xffff]; XREG = *wordptr; break; case VM_LDRWORDIMM: /* Imm mode - push 16 bit value pointed to by addr after opcode */ ++evalptr; CHECKOVERFLOW(); wordptr = (unsigned short *)&memory[++pc]; /* Pointer to operand */ wordptr = (unsigned short *)&memory[(*wordptr + fp + 1) & 0xffff]; /* Pointer to variable */ ++pc; XREG = *wordptr; break; case VM_LDRBYTE: /* Replaces X with 8 bit value pointed to by X. */ CHECKUNDERFLOW(1); #ifdef DEBUGADDRESSING print("\n XREG: "); printhex(XREG); print(", FP: "); printhex(fp); print(" -> "); printhex((XREG + fp + 1) & 0xffff); printchar('\n'); #endif XREG = memory[(XREG + fp + 1) & 0xffff]; break; case VM_LDRBYTEIMM: /* Imm mode - push byte pointed to by addr after opcode */ ++evalptr; CHECKOVERFLOW(); wordptr = (unsigned short *)&memory[++pc]; /* Pointer to operand */ byteptr = (unsigned char *)&memory[(*wordptr + fp + 1) & 0xffff]; /* Pointer to variable */ ++pc; XREG = *byteptr; break; case VM_STRWORD: /* Stores 16 bit value Y in addr pointed to by X. Drops X and Y. */ CHECKUNDERFLOW(2); wordptr = (unsigned short *)&memory[(XREG + fp + 1) & 0xffff]; *wordptr = YREG; evalptr -= 2; break; case VM_STRWORDIMM: /* Imm mode - store 16 bit value X in addr after opcode. Drop X.*/ CHECKUNDERFLOW(1); wordptr = (unsigned short *)&memory[++pc]; /* Pointer to operand */ wordptr = (unsigned short *)&memory[(*wordptr + fp + 1) & 0xffff]; /* Pointer to variable */ ++pc; *wordptr = XREG; --evalptr; break; case VM_STRBYTE: /* Stores 8 bit value Y in addr pointed to by X. Drops X and Y. */ CHECKUNDERFLOW(2); memory[(XREG + fp + 1) & 0xffff] = YREG; evalptr -= 2; break; case VM_STRBYTEIMM: /* Imm mode - store 8 bit value X in addr after opcode. Drop X.*/ CHECKUNDERFLOW(1); wordptr = (unsigned short *)&memory[++pc]; /* Pointer to operand */ byteptr = (unsigned char *)&memory[(*wordptr + fp + 1) & 0xffff]; /* Pointer to variable */ ++pc; *byteptr = XREG; --evalptr; break; /* * Manipulate evaluation stack */ case VM_SWAP: /* Swaps X and Y */ CHECKUNDERFLOW(2); tempword = XREG; XREG = YREG; YREG = tempword; break; case VM_DUP: /* Duplicates X -> X, Y */ CHECKUNDERFLOW(1); ++evalptr; CHECKOVERFLOW(); XREG = YREG; break; case VM_DUP2: /* Duplicates X -> X,Z; Y -> Y,T */ CHECKUNDERFLOW(2); evalptr += 2; CHECKOVERFLOW(); XREG = ZREG; YREG = TREG; break; case VM_DROP: /* Drops X */ CHECKUNDERFLOW(1); --evalptr; break; case VM_OVER: /* Duplicates Y -> X,Z */ CHECKUNDERFLOW(2); ++evalptr; CHECKOVERFLOW(); XREG = ZREG; break; case VM_PICK: /* Duplicates stack level specified in X+1 -> X */ CHECKUNDERFLOW(XREG + 1); XREG = evalstack[evalptr - (XREG + 1)]; break; /* * Manipulate call stack */ case VM_POPWORD: /* Pop 16 bit value from call stack, push onto eval stack [X] */ CHECKSTACKUNDERFLOW(2); sp += 2; ++evalptr; CHECKOVERFLOW(); wordptr = (unsigned short *)&memory[sp - 1]; XREG = *wordptr; break; case VM_POPBYTE: /* Pop 8 bit value from call stack, push onto eval stack [X] */ CHECKSTACKUNDERFLOW(1); ++sp; ++evalptr; CHECKOVERFLOW(); XREG = memory[sp]; break; case VM_PSHWORD: /* Push 16 bit value in X onto call stack. Drop X. */ CHECKUNDERFLOW(1); #ifdef DEBUGSTACK print("\n Push word to "); printhex(sp - 1); printchar('\n'); #endif byteptr = (unsigned char *)&XREG; memory[sp] = *(byteptr + 1); --sp; CHECKSTACKOVERFLOW(); memory[sp] = *byteptr; --sp; CHECKSTACKOVERFLOW(); --evalptr; break; case VM_PSHBYTE: /* Push 8 bit value in X onto call stack. Drop X. */ CHECKUNDERFLOW(1); #ifdef DEBUGSTACK print("\n Push byte to "); printhex(sp); printchar('\n'); #endif memory[sp] = XREG & 0x00ff; --sp; CHECKSTACKOVERFLOW(); --evalptr; break; case VM_DISCARD: /* Discard X bytes from call stack. Drop X. */ CHECKUNDERFLOW(1); sp += XREG; --evalptr; break; case VM_SPTOFP: /* Copy stack pointer to frame pointer. (Enter function scope) */ #ifdef DEBUGSTACK print("\n SPTOFP FP before "); printhex(fp); print(" SP "); printhex(sp); printchar('\n'); #endif /* Push old FP to stack */ byteptr = (unsigned char *)&fp; memory[sp] = *(byteptr + 1); --sp; CHECKSTACKOVERFLOW(); memory[sp] = *byteptr; --sp; CHECKSTACKOVERFLOW(); fp = sp; break; case VM_FPTOSP: /* Copy frame pointer to stack pointer. (Release local vars) */ #ifdef DEBUGSTACK print("\n FPTOSP SP before "); printhex(sp); print(" FP "); printhex(fp); printchar('\n'); #endif sp = fp; /* Pop old FP from stack -> FP */ CHECKSTACKUNDERFLOW(2); sp += 2; CHECKOVERFLOW(); wordptr = (unsigned short *)&memory[sp - 1]; fp = *wordptr; #ifdef DEBUGSTACK print(" Recovered FP "); printhex(fp); print(" from stack\n"); #endif break; case VM_ATOR: /* Convert absolute address in X to relative address */ XREG = (XREG - fp - 1) & 0xffff; break; case VM_RTOA: /* Convert relative address in X to absolute address */ XREG = (XREG + fp + 1) & 0xffff; break; /* * Integer math */ case VM_INC: /* X = X+1. */ CHECKUNDERFLOW(1); ++XREG; break; case VM_DEC: /* X = X-1. */ CHECKUNDERFLOW(1); --XREG; break; case VM_ADD: /* X = Y+X. Y is dropped. */ CHECKUNDERFLOW(2); YREG = YREG + XREG; --evalptr; break; case VM_SUB: /* X = Y-X. Y is dropped. */ CHECKUNDERFLOW(2); YREG = YREG - XREG; --evalptr; break; case VM_MUL: /* X = Y*X. Y is dropped. */ CHECKUNDERFLOW(2); YREG = YREG * XREG; --evalptr; break; case VM_DIV: /* X = Y/X. Y is dropped. */ CHECKUNDERFLOW(2); YREG = YREG / XREG; --evalptr; break; case VM_MOD: /* X = Y%X. Y is dropped . */ CHECKUNDERFLOW(2); YREG = YREG % XREG; --evalptr; break; case VM_NEG: /* X = -X */ CHECKUNDERFLOW(1); XREG = -XREG; break; /* * Comparisons */ case VM_GT: /* X = Y>X. Y is dropped. */ CHECKUNDERFLOW(2); YREG = YREG > XREG; --evalptr; break; case VM_GTE: /* X = Y>=X. Y is dropped. */ CHECKUNDERFLOW(2); YREG = YREG >= XREG; --evalptr; break; case VM_LT: /* X = Y>X. Y is dropped. */ CHECKUNDERFLOW(2); YREG = YREG >> XREG; --evalptr; break; /* * Flow control */ case VM_JMP: /* Jump to address X. Drop X. */ CHECKUNDERFLOW(1); pc = XREG; --evalptr; return; /* Do not advance program counter */ case VM_JMPIMM: /* Imm mode - jump to 16 bit word following opcode */ wordptr = (unsigned short *)&memory[++pc]; ++pc; pc = *wordptr; return; case VM_BRNCH: /* If Y!= 0, jump to address X. Drop X, Y. */ CHECKUNDERFLOW(2); if (YREG) { pc = XREG; } else { ++pc; } evalptr -= 2; return; /* Do not advance program counter */ case VM_BRNCHIMM: /* Imm mode - if X!=0 branch to 16 bit word following opcode */ wordptr = (unsigned short *)&memory[++pc]; ++pc; CHECKUNDERFLOW(1); if (XREG) { pc = *wordptr; } else { ++pc; } --evalptr; return; /* Do not advance program counter */ case VM_JSR: /* Push PC to call stack. Jump to address X. Drop X. */ CHECKUNDERFLOW(1); byteptr = (unsigned char *) &pc; memory[sp] = *(byteptr + 1); --sp; CHECKSTACKOVERFLOW(); memory[sp] = *byteptr; --sp; CHECKSTACKOVERFLOW(); pc = XREG; --evalptr; return; /* Do not advance program counter */ case VM_JSRIMM: /* Imm mode - push PC to calls stack, jump to 16 bit word */ wordptr = (unsigned short *)&memory[++pc]; ++pc; byteptr = (unsigned char *) &pc; memory[sp] = *(byteptr + 1); --sp; CHECKSTACKOVERFLOW(); memory[sp] = *byteptr; --sp; CHECKSTACKOVERFLOW(); pc = *wordptr; return; /* Do not advance program counter */ case VM_RTS: /* Pop call stack, jump to the address popped. */ CHECKSTACKUNDERFLOW(2); ++sp; wordptr = (unsigned short *)&memory[sp]; pc = *wordptr; ++sp; break; /* * Input / Output */ case VM_PRDEC: /* Print 16 bit decimal in X. Drop X */ CHECKUNDERFLOW(1); printdec(XREG); --evalptr; break; case VM_PRHEX: /* Print 16 bit hex in X. Drop X */ CHECKUNDERFLOW(1); printhex(XREG); --evalptr; break; case VM_PRCH: /* Print character in X. Drop X */ CHECKUNDERFLOW(1); printchar((unsigned char) XREG); --evalptr; break; case VM_PRSTR: /* Print null terminated string pointed to by X. Drop X */ CHECKUNDERFLOW(1); while (memory[XREG]) { printchar(memory[XREG++]); } --evalptr; break; case VM_PRMSG: /* Print literal string at PC (null terminated) */ ++pc; while (memory[pc]) { printchar(memory[pc++]); } break; case VM_KBDCH: /* Push character from keyboard onto eval stack */ CHECKUNDERFLOW(1); ++evalptr; /* Loop until we get a keypress */ #ifdef A2E while (!(XREG = getkey())); #elif defined(CBM) while (!(*(char *) XREG = cbm_k_getin())); #else /* TODO: Unimplemented in Linux */ XREG = 0; #endif break; case VM_KBDLN: /* Obtain line from keyboard and write to memory pointed to by */ /* Y. X contains the max number of bytes in buf. Drop X, Y. */ CHECKUNDERFLOW(2); getln((char *) &memory[YREG], XREG); evalptr -= 2; break; /* * Unsupported instruction */ default: UNSUPPORTED(); break; } ++pc; }; /* * Run the program! */ void execute() { while (1) { execute_instruction(); } } /* * Load bytecode into memory[]. */ void load() { FILE *fp; char ch; char *p = (char*)&memory[RTPCSTART]; pc = RTPCSTART; do { print("\nBytecode file (CR for default)>"); getln(p, 15); if (strlen(p) == 0) { strcpy(p, "bytecode"); } print("Loading '"); print(p); print("'\n"); fp = fopen(p, "r"); } while (!fp); while (!feof(fp)) { ch = fgetc(fp); memory[pc++] = ch; /* Print dot for each page */ if (pc%0xff == 0) { printchar('.'); } } fclose(fp); pc = RTPCSTART; #ifdef A2E printchar(7); #endif } int main() { print("EightBallVM v" VERSIONSTR "\n"); #ifdef STACKCHECKS print("[Stack Checks ON]\n"); #endif print("(c)Bobbi, 2018\n"); print("Free Software.\n"); print("Licenced under GPL.\n\n"); load(); #ifdef __GNUC__ print(" Done.\n\n"); #endif #ifdef A2E videomode(VIDEOMODE_80COL); clrscr(); #elif defined(CBM) printchar(147); /* Clear */ #endif execute(); return 0; }