const readline = require('readline'); const readlineSync = require('readline-sync'); const { INITIAL_IP_ADDRESS, CYCLE_LIMIT, KEYPAD_ADDR, KEY_MAP, } = require('./machine.config'); const { num2hex, bool2bit, } = require('./logging.js'); const display = require('./display.js'); // STATE const CPU = { // Core state running: false, IP: INITIAL_IP_ADDRESS, FLAGS: {'C': false, 'Z': false, 'N': false, 'O': false}, FLAGNUMS2NAMES: {0: 'C', 1: 'Z', 2: 'N', 3: 'O'}, Acc: 0, memory: null, // Functions that update core state /** @param {Uint8Array} data */ loadMemory: (data) => { CPU.memory = new Uint8Array(256); CPU.memory.set(data, 0); }, incrementIP: (offset) => { CPU.previousIP = CPU.IP; CPU.IP = CPU.IP + offset; }, setIP: (address) => { CPU.previousIP = CPU.IP; CPU.IP = address; }, updateFlagZero: () => { CPU.FLAGS.Z = CPU.Acc === 0; }, updateFlagNegative: () => { CPU.Acc & 128 ? CPU.FLAGS.N = true : CPU.FLAGS.N = false }, // Debug info previousIP: 0, currentInstruction: { opcode: null, operand: null, mnemonic: null, }, cycleCounter: 0, } // FUNCTIONS THAT MODIFY STATE const Instructions = { end: () => { CPU.currentInstruction.mnemonic = 'END'; CPU.running = false; CPU.incrementIP(2); }, store_lit: (lit) => { CPU.currentInstruction.mnemonic = 'STO lit'; CPU.memory[lit] = CPU.Acc; CPU.incrementIP(2); }, store_addr: (addr) => { CPU.currentInstruction.mnemonic = `STO addr; @addr: ${num2hex(CPU.memory[addr])}`; CPU.memory[CPU.memory[addr]] = CPU.Acc; CPU.incrementIP(2); }, load_lit: (lit) => { CPU.currentInstruction.mnemonic = 'LDA lit'; CPU.Acc = lit; CPU.updateFlagNegative(); CPU.updateFlagZero(); CPU.incrementIP(2); }, load_addr: (addr) => { CPU.currentInstruction.mnemonic = `LDA addr; @ addr: ${num2hex(CPU.memory[addr])}`; CPU.Acc = CPU.memory[addr]; CPU.updateFlagNegative(); CPU.updateFlagZero(); CPU.incrementIP(2); }, add_lit: (lit) => { CPU.currentInstruction.mnemonic = 'ADD lit'; // Calculate sum let sum = CPU.Acc + lit; if (sum > 255) { CPU.FLAGS.C = true; sum = (sum % 255) - 1; } else { CPU.FLAGS.C = false; } // Calculate overflow flag status let bitSixCarry = 0; if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; } // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); // FIXME FIXME FIXME // I'm on a plane and can't remember how this works let overflow = 0; if (overflow) { CPU.FLAGS.O = true; } else { CPU.FLAGS.O = false; } CPU.Acc = sum; CPU.updateFlagNegative(); CPU.updateFlagZero(); CPU.incrementIP(2); }, add_addr: (addr) => { CPU.currentInstruction.mnemonic = 'ADD addr'; // Calculate sum let sum = CPU.Acc + CPU.memory[addr]; if (sum > 255) { CPU.FLAGS.C = true; sum = (sum % 255) - 1; } else { CPU.FLAGS.C = false; } // Calculate overflow flag status let bitSixCarry = 0; if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; } // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); // FIXME FIXME FIXME // I'm on a plane and can't remember how this works let overflow = 0; if (overflow) { CPU.FLAGS.O = true; } else { CPU.FLAGS.O = false; } CPU.Acc = sum; CPU.updateFlagNegative(); CPU.updateFlagZero(); CPU.incrementIP(2); }, sub_lit: (lit) => { CPU.currentInstruction.mnemonic = 'SUB lit'; // Calculate sum let sum = CPU.Acc - lit; if (sum < 0) { CPU.FLAGS.C = true; sum = sum + 256; } else { CPU.FLAGS.C = false; } // Calculate overflow flag status let bitSixCarry = 0; if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; } // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); // FIXME FIXME FIXME // I'm on a plane and can't remember how this works let overflow = 0; if (overflow) { CPU.FLAGS.O = true; } else { CPU.FLAGS.O = false; } CPU.Acc = sum; CPU.updateFlagNegative(); CPU.updateFlagZero(); CPU.incrementIP(2); }, sub_addr: (addr) => { CPU.currentInstruction.mnemonic = 'SUB addr'; // Calculate sum let sum = CPU.Acc - CPU.memory[addr]; if (sum < 0) { CPU.FLAGS.C = true; sum = sum + 256; } else { CPU.FLAGS.C = false; } // Calculate overflow flag status let bitSixCarry = 0; if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; } // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); // FIXME FIXME FIXME // I'm on a plane and can't remember how this works let overflow = 0; if (overflow) { CPU.FLAGS.O = true; } else { CPU.FLAGS.O = false; } CPU.Acc = sum; CPU.updateFlagNegative(); CPU.updateFlagZero(); CPU.incrementIP(2); }, hop_lit: (lit) => { CPU.currentInstruction.mnemonic = `HOP lit; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`; if (CPU.Acc === lit) { CPU.incrementIP(4); } else { CPU.incrementIP(2); } }, hop_addr: (addr) => { CPU.currentInstruction.mnemonic = 'HOP addr'; if (CPU.Acc === CPU.memory[addr]) { CPU.incrementIP(4); } else { CPU.incrementIP(2); } }, jump_lit: (lit) => { CPU.currentInstruction.mnemonic = 'JMP lit'; CPU.setIP(lit); }, jump_addr: (addr) => { CPU.currentInstruction.mnemonic = 'JMP addr'; CPU.setIP(CPU.memory[addr]); }, flag_toggle: (flagNum) => { if (flagNum === null) { console.error('Invalid flag number'); process.exit(); } const flagName = CPU.FLAGNUMS2NAMES[flagNum]; CPU.currentInstruction.mnemonic = `FTG ${flagName}`; CPU.FLAGS[flagName] = !CPU.FLAGS[flagName]; CPU.incrementIP(2); }, flag_hop: (flagNum) => { if (flagNum === null) { console.error('Invalid flag number'); process.exit(); } const flagName = CPU.FLAGNUMS2NAMES[flagNum]; CPU.currentInstruction.mnemonic = `FHP ${flagName}; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`; if (CPU.FLAGS[CPU.FLAGNUMS2NAMES[flagNum]]) { CPU.incrementIP(4); } else { CPU.incrementIP(2); } }, no_op: () => { CPU.currentInstruction.mnemonic = `NOP`; CPU.incrementIP(2); }, } const opcodes2mnemonics = { 0: (operand) => Instructions.end(), 1: (operand) => Instructions.store_lit(operand), 2: (operand) => Instructions.store_addr(operand), 3: (operand) => Instructions.load_lit(operand), 4: (operand) => Instructions.load_addr(operand), 5: (operand) => Instructions.add_lit(operand), 6: (operand) => Instructions.add_addr(operand), 7: (operand) => Instructions.sub_lit(operand), 8: (operand) => Instructions.sub_addr(operand), 9: (operand) => Instructions.hop_lit(operand), 10: (operand) => Instructions.hop_addr(operand), 11: (operand) => Instructions.jump_lit(operand), 12: (operand) => Instructions.jump_addr(operand), 13: (operand) => Instructions.flag_toggle(operand), 14: (operand) => Instructions.flag_hop(operand), 15: (operand) => Instructions.no_op(), }; /** * Load code into memory and set CPU state to "running" * @param {Uint8Array} code - Machine code to load **/ function startCPU(code) { CPU.loadMemory(code); CPU.cycleCounter = 0; CPU.running = true; // FIXME: This conflicts with single-stepping // (you can single-step, but keys aren't passed // through to the Cardiograph) // // -> The fix is maybe to remove readlineSync, // and instead stash the keypress into a buffer variable.* // Then have the stepping function check that buffer, // and then clear the buffer, each time it runs. // // * If it's in the set of keys that are relevant // to single-stepping. // Start listening for keypresses... readline.emitKeypressEvents(process.stdin); if (process.stdin.setRawMode != null) { process.stdin.setRawMode(true); } process.stdin.on('keypress', (str, key) => { // TODO: is it possible to turn this off again? if (key.sequence === '\x03') process.exit(); let name = key.name.toUpperCase(); if (name in KEY_MAP) { CPU.memory[KEYPAD_ADDR] = KEY_MAP[name]; } }); } /** * Execute just the next instruction in memory * @param {Object} debugInfo * @param {Boolean} [debug] - Print machine status and the line of code being executed **/ async function stepCPU(debugInfo, debug = false, prettyPrintDisplay = false) { if (CPU.IP >= CPU.memory.length) { console.error('HALTING - IP greater than memory size'); CPU.running = false; process.exit(); } else { CPU.currentInstruction.opcode = CPU.memory[CPU.IP]; CPU.currentInstruction.operand = CPU.memory[CPU.IP+1]; let executeInstruction = opcodes2mnemonics[CPU.currentInstruction.opcode]; if (typeof executeInstruction === 'undefined') { let info = debugInfo[CPU.previousIP]; console.error(); console.error(`Error: Invalid opcode`); console.error(` Executing $${num2hex(info.machine[0])} $${num2hex(info.machine[1])}`); console.error(` from line ${info.lineNumber}: ${info.source}`); process.exit(); } executeInstruction(CPU.currentInstruction.operand); CPU.cycleCounter += 1; } logCPUState(debugInfo, debug, prettyPrintDisplay); if (CYCLE_LIMIT) { // Temporary limit as a lazy way to halt infinite loops if (CPU.cycleCounter >= CYCLE_LIMIT) { console.warn(' HALTING - reached cycle limit'); CPU.running = false; } } if (!CPU.running) process.exit(); } /** * @param {Uint8Array} code - Machine code to run * @param {Object} debugInfo TODO type * @param {Boolean} [debug] - Enable/disable debugging printouts * @param {Boolean} [singleStep] * @param {Boolean} [prettyPrint] - Print display with black and white emoji, instead of in hex * @param {Number} [clockSpeed] - CPU clock speed in milliseconds **/ exports.runProgram = (code, debugInfo, debug=false, singleStep=false, prettyPrint=false, clockSpeed=100) => { if (singleStep) { this.singleStepProgram(code, debugInfo, debug, prettyPrint); } else { startCPU(code); // Animate the output by pausing between steps const loop = setInterval(async () => { stepCPU(debugInfo, debug, prettyPrint); if (!CPU.running) { logCPUState(debugInfo, debug, prettyPrint); console.log('Halted'); process.exit(); } }, clockSpeed); } }; /** * @param {Uint8Array} code - Machine code to run * @param {any} debugInfo - TODO * @param {Boolean} [debug] - Enable/disable debugging printouts * @param {Boolean} [prettyPrintDisplay] - Print display using black and white emoji **/ exports.singleStepProgram = (code, debugInfo, debug = false, prettyPrintDisplay = false) => { startCPU(code); while (CPU.running) { stepCPU(debugInfo, debug, prettyPrintDisplay); // FIXME: this prevents exiting with Ctrl+C: let key = readlineSync.keyIn('S to step, Q to quit > ', { limit: ['s', 'S', 'q', 'Q'], }); if (key.toLowerCase() === 'q') { process.exit(); } console.log(); } } // FUNCTIONS THAT PULL INFO FROM STATE TO DISPLAY /** * @param {Boolean} [debug] - Enable/disable debugging printouts **/ function logCPUState(debugInfo, debug = false, prettyPrintDisplay = false) { debugInfo = debugInfo[CPU.previousIP] !== 'undefined' ? debugInfo[CPU.previousIP] : false; console.group(`Step ${CPU.cycleCounter}`); console.log(); if (!debug) console.clear(); display.show(CPU.memory, prettyPrintDisplay); console.log(); if (debugInfo) { console.log(`Line ${debugInfo.lineNumber}: ${debugInfo.source}`); console.log(); } console.log('Mnemonic:', CPU.currentInstruction.mnemonic); console.log(`Machine: $${num2hex(CPU.currentInstruction.opcode)} $${num2hex(CPU.currentInstruction.operand)}`); console.log(); console.log(`IP: $${num2hex(CPU.IP)} Acc: $${num2hex(CPU.Acc)} ONZC ${bool2bit(CPU.FLAGS.O)}${bool2bit(CPU.FLAGS.N)}${bool2bit(CPU.FLAGS.Z)}${bool2bit(CPU.FLAGS.C)}`); console.log(`KEY: $${num2hex(CPU.memory[KEYPAD_ADDR])}  ${CPU.running ? "running" : "halted" }`); console.log(); console.log(); console.groupEnd(); };