const { num2hex } = require('./conversions.js'); module.exports = class CPU { /** * @arg {number} initialIP **/ constructor(initialIP, cycleLimit) { this.running = false; this.IP = initialIP; this.acc = 0; this.flags = {'C': false, 'Z': false, 'N': false, 'O': false}; this.flagNums = {0: 'C', 1: 'Z', 2: 'N', 3: 'O'}; this.instruction = { opcode: null, operand: null }; this.memory = null; this._cycleLimit = cycleLimit; this.dbg = { sourceInfo: null, currentMnemonic: null, previousIP: initialIP, cycleCounter: 0, } } /** Public interface **/ /** * @param {Uint8Array} machineCode **/ loadMemory(machineCode) { this.memory = new Uint8Array(256); this.memory.set(machineCode, 0); } peek() { return; } // TODO - implement Peek poke() { return; } // TODO - implement Poke /** @param {Array} info **/ // TODO - document type for 'sourceInfo' loadSourceAnnotations(info) { this.dbg.sourceInfo = info; } /** Set CPU state to "state.running" **/ start() { this.running = true; } /** Execute the next instruction in memory **/ step() { this._cycleStartCallbacks.forEach((fn) => fn()); if (this.IP >= this.memory.length) { this.running = false; throw new Error('HALTING - IP greater than memory size'); } else { this.instruction.opcode = this.memory[this.IP]; this.instruction.operand = this.memory[this.IP+1]; let mnem = this._nums2mnems[this.instruction.opcode]; let op = this._ops[mnem]; if (typeof op === 'undefined') { this._failInvalidOpcode(); } op(this.instruction.operand); this.dbg.cycleCounter += 1; } // Temporary limit as a lazy way to halt infinite loops if ((this._cycleLimit > 0) && this.dbg.cycleCounter >= this._cycleLimit) { this.running = false; throw new Error(' HALTING - reached cycle limit'); } this._cycleEndCallbacks.forEach((fn) => fn()); if (!this.running) process.exit(); } /** Private methods **/ _incrementIP(offset) { this.dbg.previousIP = this.IP; this.IP = this.IP + offset; } _setIP(address) { this.dbg.previousIP = this.IP; this.IP = address; } _updateFlagZero() { this.flags.Z = this.acc === 0; } _updateFlagNegative() { if (this.acc & 128) { this.flags.N = true; } else { this.flags.N = false; } } /** Hooks **/ /** @type Array **/ _cycleStartCallbacks = []; /** @type Array **/ _cycleEndCallbacks = []; /** @param {function} fn **/ onCycleStart(fn) { this._cycleStartCallbacks.push(fn) }; /** @param {function} fn **/ onCycleEnd(fn) { this._cycleEndCallbacks.push(fn) }; _ops = { end: () => { this.dbg.currentMnemonic = 'END'; this.running = false; this._incrementIP(2); }, store_lit: (lit) => { this.dbg.currentMnemonic = 'STO lit'; this.memory[lit] = this.acc; this._incrementIP(2); }, store_addr: (addr) => { this.dbg.currentMnemonic = `STO addr; @addr: $${num2hex(this.memory[addr])}`; this.memory[this.memory[addr]] = this.acc; this._incrementIP(2); }, load_lit: (lit) => { this.dbg.currentMnemonic = 'LDA lit'; this.acc = lit; this._updateFlagNegative(); this._updateFlagZero(); this._incrementIP(2); }, load_addr: (addr) => { this.dbg.currentMnemonic = `LDA addr; @ addr: $${num2hex(this.memory[addr])}`; this.acc = this.memory[addr]; this._updateFlagNegative(); this._updateFlagZero(); this._incrementIP(2); }, add_lit: (lit) => { this.dbg.currentMnemonic = 'ADD lit'; const [sum, carry, overflow] = sumCarryOverflow(this.acc, lit); this.acc = sum; this.flags.C = carry; this.flags.O = overflow; this._updateFlagNegative(); this._updateFlagZero(); this._incrementIP(2); }, add_addr: (addr) => { this.dbg.currentMnemonic = `ADD addr; @ addr: $${num2hex(this.memory[addr])}`; const [sum, carry, overflow] = sumCarryOverflow(this.acc, this.memory[addr]); this.acc = sum; this.flags.C = carry; this.flags.O = overflow; this._updateFlagNegative(); this._updateFlagZero(); this._incrementIP(2); }, sub_lit: (lit) => { this.dbg.currentMnemonic = 'SUB lit'; const [difference, carry, overflow] = differenceCarryOverflow(this.acc, lit); this.acc = difference; this.flags.C = carry; this.flags.O = overflow; this._updateFlagNegative(); this._updateFlagZero(); this._incrementIP(2); }, sub_addr: (addr) => { this.dbg.currentMnemonic = `SUB addr; @ addr: $${num2hex(this.memory[addr])}`; const [difference, carry, overflow] = differenceCarryOverflow(this.acc, this.memory[addr]); this.acc = difference; this.flags.C = carry; this.flags.O = overflow; this._updateFlagNegative(); this._updateFlagZero(); this._incrementIP(2); }, hop_lit: (lit) => { this.dbg.currentMnemonic = `HOP lit; IP+2: $${this.memory[this.IP+2]}, IP+3: $${this.memory[this.IP+3]}`; if (this.acc === lit) { this._incrementIP(4); } else { this._incrementIP(2); } }, hop_addr: (addr) => { this.dbg.currentMnemonic = 'HOP addr'; if (this.acc === this.memory[addr]) { this._incrementIP(4); } else { this._incrementIP(2); } }, jump_lit: (lit) => { this.dbg.currentMnemonic = 'JMP lit'; this._setIP(lit); }, jump_addr: (addr) => { this.dbg.currentMnemonic = 'JMP addr'; this._setIP(this.memory[addr]); }, flag_toggle: (flagNum) => { if (flagNum === null) { let info = this.dbg.sourceInfo[this.IP]; throw new Error(`Invalid flag number: '${flagNum}' on line ${info.lineNumber}: ${info.source}`); } const flagName = this.flagNums[flagNum]; this.dbg.currentMnemonic = `FTG ${flagName}`; this.flags[flagName] = !this.flags[flagName]; this._incrementIP(2); }, flag_hop: (flagNum) => { if (flagNum === null) { console.error('Invalid flag number'); process.exit(); } const flagName = this.flagNums[flagNum]; this.dbg.currentMnemonic = `FHP ${flagName}; IP+2: ${this.memory[this.IP+2]}, IP+3: ${this.memory[this.IP+3]}`; if (this.flags[this.flagNums[flagNum]]) { this._incrementIP(4); } else { this._incrementIP(2); } }, no_op: () => { this.dbg.currentMnemonic = `NOP`; this._incrementIP(2); }, } _nums2mnems = { 0: "end", 1: "store_lit", 2: "store_addr", 3: "load_lit", 4: "load_addr", 5: "add_lit", 6: "add_addr", 7: "sub_lit", 8: "sub_addr", 9: "hop_lit", 10: "hop_addr", 11: "jump_lit", 12: "jump_addr", 13: "flag_toggle", 14: "flag_hop", 15: "no_op", } _failInvalidOpcode() { let info = this.dbg.sourceInfo[this.dbg.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(); } } /** * @arg {number} n1 * @arg {number} n2 * @returns {[number, boolean, boolean]} [sum, carry, overflow] **/ function sumCarryOverflow(n1, n2) { let sum = n1 + n2; let carry = false; if (sum > 255) { carry = true; sum = (sum % 255) - 1; } let n1_bit6 = (n1 & 64) === 64; // Bit 6 is the 64s place let n2_bit6 = (n2 & 64) === 64; // 64 & n == 64 where n >= 64 let carryIntoLastBit = n1_bit6 && n2_bit6; console.log('c_in', carryIntoLastBit, 'c_out', carry); let overflow = carryIntoLastBit != carry; return [sum, carry, overflow]; } /** * @arg {number} n1 * @arg {number} n2 * @returns {[number, boolean, boolean]} [sum, carry, overflow] **/ function differenceCarryOverflow(n1, n2) { // https://www.righto.com/2012/12/the-6502-overflow-flag-explained.html // > SBC simply takes the ones complement of the second value and then performs an ADC. // // https://stackoverflow.com/a/8966863 // > The signed overflow flag value, however, must be the same for both A-B and A+(-B) because it depends on whether or not the result has the correct sign bit and in both cases the sign bit will be the same. return sumCarryOverflow(n1, -n2); }