From b173d46cb6855ce074c911ad70725e6a68cf274b Mon Sep 17 00:00:00 2001 From: n loewen Date: Mon, 28 Aug 2023 16:25:43 -0400 Subject: [PATCH 01/62] cpu - Change structure of CPU object to group State and Debug info --- src/cpu.js | 233 ++++++++++++++++++++++++++++------------------------- 1 file changed, 125 insertions(+), 108 deletions(-) diff --git a/src/cpu.js b/src/cpu.js index fa1d567..d6861f2 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -16,39 +16,56 @@ 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 + /** Core state **/ + 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, + }, + + /** Debug info **/ + debug: { + previousIP: 0, + currentInstruction: { + opcode: null, + operand: null, + mnemonic: null, + }, + cycleCounter: 0, + }, + + /** Functions that update state **/ + /** @param {Uint8Array} data */ - loadMemory: (data) => { - CPU.memory = new Uint8Array(256); - CPU.memory.set(data, 0); + loadMemory(data) { + this.state.memory = new Uint8Array(256); + this.state.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, + incrementIP(offset) { + this.state.previousIP = this.state.IP; + this.state.IP = this.state.IP + offset; }, - cycleCounter: 0, + + setIP(address) { + this.state.previousIP = this.state.IP; + this.state.IP = address; + }, + + updateFlagZero() { this.state.flags.Z = this.state.acc === 0; }, + updateFlagNegative() { this.state.acc & 128 ? this.state.flags.N = true : this.state.flags.N = false }, + + + /** Hooks **/ + + onTickHooks: [], + + /** @param {function} fn **/ + onTick(fn) { this.onTickHooks.push(fn) }, } @@ -56,154 +73,154 @@ const CPU = { const Instructions = { end: () => { - CPU.currentInstruction.mnemonic = 'END'; - CPU.running = false; + CPU.debug.currentInstruction.mnemonic = 'END'; + CPU.state.running = false; CPU.incrementIP(2); }, store_lit: (lit) => { - CPU.currentInstruction.mnemonic = 'STO lit'; - CPU.memory[lit] = CPU.Acc; + CPU.debug.currentInstruction.mnemonic = 'STO lit'; + CPU.state.memory[lit] = CPU.state.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.debug.currentInstruction.mnemonic = `STO addr; @addr: ${num2hex(CPU.state.memory[addr])}`; + CPU.state.memory[CPU.state.memory[addr]] = CPU.state.acc; CPU.incrementIP(2); }, load_lit: (lit) => { - CPU.currentInstruction.mnemonic = 'LDA lit'; - CPU.Acc = lit; + CPU.debug.currentInstruction.mnemonic = 'LDA lit'; + CPU.state.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.debug.currentInstruction.mnemonic = `LDA addr; @ addr: ${num2hex(CPU.state.memory[addr])}`; + CPU.state.acc = CPU.state.memory[addr]; CPU.updateFlagNegative(); CPU.updateFlagZero(); CPU.incrementIP(2); }, add_lit: (lit) => { - CPU.currentInstruction.mnemonic = 'ADD lit'; + CPU.debug.currentInstruction.mnemonic = 'ADD lit'; // Calculate sum - let sum = CPU.Acc + lit; + let sum = CPU.state.acc + lit; if (sum > 255) { - CPU.FLAGS.C = true; + CPU.state.flags.C = true; sum = (sum % 255) - 1; } else { - CPU.FLAGS.C = false; + CPU.state.flags.C = false; } // Calculate overflow flag status let bitSixCarry = 0; - if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); + if ((CPU.state.acc & 64) && (lit & 64)) { bitSixCarry = 1; } + // let overflow = bitSixCarry ^ (CPU.state.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; + CPU.state.flags.O = true; } else { - CPU.FLAGS.O = false; + CPU.state.flags.O = false; } - CPU.Acc = sum; + CPU.state.acc = sum; CPU.updateFlagNegative(); CPU.updateFlagZero(); CPU.incrementIP(2); }, add_addr: (addr) => { - CPU.currentInstruction.mnemonic = 'ADD addr'; + CPU.debug.currentInstruction.mnemonic = 'ADD addr'; // Calculate sum - let sum = CPU.Acc + CPU.memory[addr]; + let sum = CPU.state.acc + CPU.state.memory[addr]; if (sum > 255) { - CPU.FLAGS.C = true; + CPU.state.flags.C = true; sum = (sum % 255) - 1; } else { - CPU.FLAGS.C = false; + CPU.state.flags.C = false; } // Calculate overflow flag status let bitSixCarry = 0; - if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); + if ((CPU.state.acc & 64) && (addr & 64)) { bitSixCarry = 1; } + // let overflow = bitSixCarry ^ (CPU.state.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; + CPU.state.flags.O = true; } else { - CPU.FLAGS.O = false; + CPU.state.flags.O = false; } - CPU.Acc = sum; + CPU.state.acc = sum; CPU.updateFlagNegative(); CPU.updateFlagZero(); CPU.incrementIP(2); }, sub_lit: (lit) => { - CPU.currentInstruction.mnemonic = 'SUB lit'; + CPU.debug.currentInstruction.mnemonic = 'SUB lit'; // Calculate sum - let sum = CPU.Acc - lit; + let sum = CPU.state.acc - lit; if (sum < 0) { - CPU.FLAGS.C = true; + CPU.state.flags.C = true; sum = sum + 256; } else { - CPU.FLAGS.C = false; + CPU.state.flags.C = false; } // Calculate overflow flag status let bitSixCarry = 0; - if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); + if ((CPU.state.acc & 64) && (lit & 64)) { bitSixCarry = 1; } + // let overflow = bitSixCarry ^ (CPU.state.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; + CPU.state.flags.O = true; } else { - CPU.FLAGS.O = false; + CPU.state.flags.O = false; } - CPU.Acc = sum; + CPU.state.acc = sum; CPU.updateFlagNegative(); CPU.updateFlagZero(); CPU.incrementIP(2); }, sub_addr: (addr) => { - CPU.currentInstruction.mnemonic = 'SUB addr'; + CPU.debug.currentInstruction.mnemonic = 'SUB addr'; // Calculate sum - let sum = CPU.Acc - CPU.memory[addr]; + let sum = CPU.state.acc - CPU.state.memory[addr]; if (sum < 0) { - CPU.FLAGS.C = true; + CPU.state.flags.C = true; sum = sum + 256; } else { - CPU.FLAGS.C = false; + CPU.state.flags.C = false; } // Calculate overflow flag status let bitSixCarry = 0; - if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); + if ((CPU.state.acc & 64) && (addr & 64)) { bitSixCarry = 1; } + // let overflow = bitSixCarry ^ (CPU.state.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; + CPU.state.flags.O = true; } else { - CPU.FLAGS.O = false; + CPU.state.flags.O = false; } - CPU.Acc = sum; + CPU.state.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.debug.currentInstruction.mnemonic = `HOP lit; IP+2: ${CPU.state.memory[CPU.state.IP+2]}, IP+3: ${CPU.state.memory[CPU.state.IP+3]}`; + if (CPU.state.acc === lit) { CPU.incrementIP(4); } else { CPU.incrementIP(2); @@ -211,8 +228,8 @@ const Instructions = { }, hop_addr: (addr) => { - CPU.currentInstruction.mnemonic = 'HOP addr'; - if (CPU.Acc === CPU.memory[addr]) { + CPU.debug.currentInstruction.mnemonic = 'HOP addr'; + if (CPU.state.acc === CPU.state.memory[addr]) { CPU.incrementIP(4); } else { CPU.incrementIP(2); @@ -220,13 +237,13 @@ const Instructions = { }, jump_lit: (lit) => { - CPU.currentInstruction.mnemonic = 'JMP lit'; + CPU.debug.currentInstruction.mnemonic = 'JMP lit'; CPU.setIP(lit); }, jump_addr: (addr) => { - CPU.currentInstruction.mnemonic = 'JMP addr'; - CPU.setIP(CPU.memory[addr]); + CPU.debug.currentInstruction.mnemonic = 'JMP addr'; + CPU.setIP(CPU.state.memory[addr]); }, flag_toggle: (flagNum) => { @@ -235,8 +252,8 @@ const Instructions = { process.exit(); } const flagName = CPU.FLAGNUMS2NAMES[flagNum]; - CPU.currentInstruction.mnemonic = `FTG ${flagName}`; - CPU.FLAGS[flagName] = !CPU.FLAGS[flagName]; + CPU.debug.currentInstruction.mnemonic = `FTG ${flagName}`; + CPU.state.flags[flagName] = !CPU.state.flags[flagName]; CPU.incrementIP(2); }, @@ -246,8 +263,8 @@ const Instructions = { 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.debug.currentInstruction.mnemonic = `FHP ${flagName}; IP+2: ${CPU.state.memory[CPU.state.IP+2]}, IP+3: ${CPU.state.memory[CPU.state.IP+3]}`; + if (CPU.state.flags[CPU.FLAGNUMS2NAMES[flagNum]]) { CPU.incrementIP(4); } else { CPU.incrementIP(2); @@ -255,7 +272,7 @@ const Instructions = { }, no_op: () => { - CPU.currentInstruction.mnemonic = `NOP`; + CPU.debug.currentInstruction.mnemonic = `NOP`; CPU.incrementIP(2); }, } @@ -280,13 +297,13 @@ const opcodes2mnemonics = { }; /** - * Load code into memory and set CPU state to "running" + * Load code into memory and set CPU state to "state.running" * @param {Uint8Array} code - Machine code to load **/ function startCPU(code) { CPU.loadMemory(code); - CPU.cycleCounter = 0; - CPU.running = true; + CPU.debug.cycleCounter = 0; + CPU.state.running = true; // FIXME: This conflicts with single-stepping // (you can single-step, but keys aren't passed @@ -309,7 +326,7 @@ function startCPU(code) { if (key.sequence === '\x03') process.exit(); let name = key.name.toUpperCase(); if (name in KEY_MAP) { - CPU.memory[KEYPAD_ADDR] = KEY_MAP[name]; + CPU.state.memory[KEYPAD_ADDR] = KEY_MAP[name]; } }); } @@ -320,14 +337,14 @@ function startCPU(code) { * @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) { + if (CPU.state.IP >= CPU.state.memory.length) { console.error('HALTING - IP greater than memory size'); - CPU.running = false; + CPU.state.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]; + CPU.debug.currentInstruction.opcode = CPU.state.memory[CPU.state.IP]; + CPU.debug.currentInstruction.operand = CPU.state.memory[CPU.state.IP+1]; + let executeInstruction = opcodes2mnemonics[CPU.debug.currentInstruction.opcode]; if (typeof executeInstruction === 'undefined') { let info = debugInfo[CPU.previousIP]; console.error(); @@ -336,17 +353,17 @@ async function stepCPU(debugInfo, debug = false, prettyPrintDisplay = false) { console.error(` from line ${info.lineNumber}: ${info.source}`); process.exit(); } - executeInstruction(CPU.currentInstruction.operand); - CPU.cycleCounter += 1; + executeInstruction(CPU.debug.currentInstruction.operand); + CPU.debug.cycleCounter += 1; } logCPUState(debugInfo, debug, prettyPrintDisplay); if (DEFAULT_CYCLE_LIMIT) { // Temporary limit as a lazy way to halt infinite loops - if (CPU.cycleCounter >= DEFAULT_CYCLE_LIMIT) { + if (CPU.debug.cycleCounter >= DEFAULT_CYCLE_LIMIT) { console.warn(' HALTING - reached cycle limit'); - CPU.running = false; + CPU.state.running = false; } } - if (!CPU.running) process.exit(); + if (!CPU.state.running) process.exit(); } /** @@ -366,7 +383,7 @@ exports.runProgram = // Animate the output by pausing between steps const loop = setInterval(async () => { stepCPU(debugInfo, debug, prettyPrint); - if (!CPU.running) { + if (!CPU.state.running) { logCPUState(debugInfo, debug, prettyPrint); console.log('Halted'); process.exit(); @@ -383,7 +400,7 @@ exports.runProgram = **/ exports.singleStepProgram = (code, debugInfo, debug = false, prettyPrintDisplay = false) => { startCPU(code); - while (CPU.running) { + while (CPU.state.running) { stepCPU(debugInfo, debug, prettyPrintDisplay); // FIXME: this prevents exiting with Ctrl+C: let key = readlineSync.keyIn('S to step, Q to quit > ', { @@ -402,20 +419,20 @@ exports.singleStepProgram = (code, debugInfo, debug = false, prettyPrintDisplay **/ function logCPUState(debugInfo, debug = false, prettyPrintDisplay = false) { debugInfo = debugInfo[CPU.previousIP] !== 'undefined' ? debugInfo[CPU.previousIP] : false; - console.group(`Step ${CPU.cycleCounter}`); + console.group(`Step ${CPU.debug.cycleCounter}`); console.log(); if (!debug) console.clear(); - display.show(CPU.memory, prettyPrintDisplay); + display.show(CPU.state.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('Mnemonic:', CPU.debug.currentInstruction.mnemonic); + console.log(`Machine: $${num2hex(CPU.debug.currentInstruction.opcode)} $${num2hex(CPU.debug.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(`IP: $${num2hex(CPU.state.IP)} Acc: $${num2hex(CPU.state.acc)} ONZC ${bool2bit(CPU.state.flags.O)}${bool2bit(CPU.state.flags.N)}${bool2bit(CPU.state.flags.Z)}${bool2bit(CPU.state.flags.C)}`); + console.log(`KEY: $${num2hex(CPU.state.memory[KEYPAD_ADDR])}  ${CPU.state.running ? "state.running" : "halted" }`); console.log(); console.log(); console.groupEnd(); From b7d13087e67a220014a770510a7fd04f7d49010c Mon Sep 17 00:00:00 2001 From: n loewen Date: Mon, 28 Aug 2023 16:55:24 -0400 Subject: [PATCH 02/62] cpu - WIP - Continue removing material that has to move elsewhere so that this can be treated as a library --- src/cpu.js | 201 +++++++++++++++-------------------------------------- 1 file changed, 57 insertions(+), 144 deletions(-) diff --git a/src/cpu.js b/src/cpu.js index d6861f2..5ccbc22 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -3,7 +3,7 @@ const readlineSync = require('readline-sync'); const { INITIAL_IP_ADDRESS, - DEFAULT_CYCLE_LIMIT, + DEFAULT_CYCLE_LIMIT: CYCLE_LIMIT, KEYPAD_ADDR, KEY_MAP, } = require('./machine.config'); @@ -29,6 +29,7 @@ const CPU = { /** Debug info **/ debug: { + sourceInfo: null, previousIP: 0, currentInstruction: { opcode: null, @@ -40,12 +41,17 @@ const CPU = { /** Functions that update state **/ - /** @param {Uint8Array} data */ + /** @param {Uint8Array} data **/ loadMemory(data) { this.state.memory = new Uint8Array(256); this.state.memory.set(data, 0); }, + /** @param {Array} info **/ // TODO type info + loadSourceInfo(info) { + this.debug.sourceInfo = info; + }, + incrementIP(offset) { this.state.previousIP = this.state.IP; this.state.IP = this.state.IP + offset; @@ -59,13 +65,52 @@ const CPU = { updateFlagZero() { this.state.flags.Z = this.state.acc === 0; }, updateFlagNegative() { this.state.acc & 128 ? this.state.flags.N = true : this.state.flags.N = false }, + /** + * Load code into memory and set CPU state to "state.running" + * @param {Uint8Array} code - Machine code to load + **/ + startCPU(code) { + CPU.loadMemory(code); + CPU.state.running = true; + }, + + /** Execute the next instruction in memory **/ + stepCPU() { + CPU.cycleStartCallbacks.forEach((fn) => fn()); + + if (CPU.state.IP >= CPU.state.memory.length) { + console.error('HALTING - IP greater than memory size'); // TODO -- should this throw an error instead? + CPU.state.running = false; + } else { + CPU.debug.currentInstruction.opcode = CPU.state.memory[CPU.state.IP]; + CPU.debug.currentInstruction.operand = CPU.state.memory[CPU.state.IP+1]; + let executeInstruction = opcodes2mnemonics[CPU.debug.currentInstruction.opcode]; + + if (typeof executeInstruction === 'undefined') { failInvalidOpcode(); } + + executeInstruction(CPU.debug.currentInstruction.operand); + CPU.debug.cycleCounter += 1; + } + + // Temporary limit as a lazy way to halt infinite loops + if ((CYCLE_LIMIT > 0) && CPU.debug.cycleCounter >= CYCLE_LIMIT) { + console.warn(' HALTING - reached cycle limit'); // TODO -- throw error? + CPU.state.running = false; + } + + if (!CPU.state.running) process.exit(); + CPU.cycleEndCallbacks.forEach((fn) => fn()); + }, + /** Hooks **/ - onTickHooks: [], + cycleStartCallbacks: [], + cycleEndCallbacks: [], /** @param {function} fn **/ - onTick(fn) { this.onTickHooks.push(fn) }, + onCycleStart(fn) { this.cycleStartCallbacks.push(fn) }, + onCycleEnd(fn) { this.cycleEndCallbacks.push(fn) }, } @@ -296,144 +341,12 @@ const opcodes2mnemonics = { 15: (operand) => Instructions.no_op(), }; -/** - * Load code into memory and set CPU state to "state.running" - * @param {Uint8Array} code - Machine code to load - **/ -function startCPU(code) { - CPU.loadMemory(code); - CPU.debug.cycleCounter = 0; - CPU.state.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.state.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.state.IP >= CPU.state.memory.length) { - console.error('HALTING - IP greater than memory size'); - CPU.state.running = false; - process.exit(); - } else { - CPU.debug.currentInstruction.opcode = CPU.state.memory[CPU.state.IP]; - CPU.debug.currentInstruction.operand = CPU.state.memory[CPU.state.IP+1]; - let executeInstruction = opcodes2mnemonics[CPU.debug.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.debug.currentInstruction.operand); - CPU.debug.cycleCounter += 1; - } - logCPUState(debugInfo, debug, prettyPrintDisplay); - if (DEFAULT_CYCLE_LIMIT) { // Temporary limit as a lazy way to halt infinite loops - if (CPU.debug.cycleCounter >= DEFAULT_CYCLE_LIMIT) { - console.warn(' HALTING - reached cycle limit'); - CPU.state.running = false; - } - } - if (!CPU.state.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.state.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.state.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.debug.cycleCounter}`); - console.log(); - if (!debug) console.clear(); - display.show(CPU.state.memory, prettyPrintDisplay); - console.log(); - if (debugInfo) { - console.log(`Line ${debugInfo.lineNumber}: ${debugInfo.source}`); - console.log(); - } - console.log('Mnemonic:', CPU.debug.currentInstruction.mnemonic); - console.log(`Machine: $${num2hex(CPU.debug.currentInstruction.opcode)} $${num2hex(CPU.debug.currentInstruction.operand)}`); - console.log(); - console.log(`IP: $${num2hex(CPU.state.IP)} Acc: $${num2hex(CPU.state.acc)} ONZC ${bool2bit(CPU.state.flags.O)}${bool2bit(CPU.state.flags.N)}${bool2bit(CPU.state.flags.Z)}${bool2bit(CPU.state.flags.C)}`); - console.log(`KEY: $${num2hex(CPU.state.memory[KEYPAD_ADDR])}  ${CPU.state.running ? "state.running" : "halted" }`); - console.log(); - console.log(); - console.groupEnd(); -}; \ No newline at end of file +function failInvalidOpcode() { + let info = CPU.debug.sourceInfo[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(); +} \ No newline at end of file From 03715a6a8e7127759df49b0a5ca37fb1b959c76f Mon Sep 17 00:00:00 2001 From: n loewen Date: Mon, 28 Aug 2023 16:56:13 -0400 Subject: [PATCH 03/62] cpu - Rename to cpu.lib.js --- src/{cpu.js => cpu.lib.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{cpu.js => cpu.lib.js} (100%) diff --git a/src/cpu.js b/src/cpu.lib.js similarity index 100% rename from src/cpu.js rename to src/cpu.lib.js From 18d77086dff5b44aaacb9f2f5b54137d59d1bde0 Mon Sep 17 00:00:00 2001 From: n loewen Date: Mon, 28 Aug 2023 23:09:12 -0400 Subject: [PATCH 04/62] cpu - WIP - Finish refactoring into a library --- src/cpu.lib.js | 646 ++++++++++++++++++++++++------------------------- 1 file changed, 323 insertions(+), 323 deletions(-) diff --git a/src/cpu.lib.js b/src/cpu.lib.js index 5ccbc22..22649de 100644 --- a/src/cpu.lib.js +++ b/src/cpu.lib.js @@ -1,352 +1,352 @@ -const readline = require('readline'); -const readlineSync = require('readline-sync'); +const { num2hex } = require('./logging.js'); -const { - INITIAL_IP_ADDRESS, - DEFAULT_CYCLE_LIMIT: CYCLE_LIMIT, - KEYPAD_ADDR, - KEY_MAP, -} = require('./machine.config'); +module.exports = class CPU { -const { - num2hex, - bool2bit, -} = require('./logging.js'); -const display = require('./display.js'); + /** + * @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; -// STATE -const CPU = { + this._cycleLimit = cycleLimit; - /** Core state **/ - 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, - }, + this.dbg = { + sourceInfo: null, + currentMnemonic: null, + previousIP: initialIP, + cycleCounter: 0, + } + } - /** Debug info **/ - debug: { - sourceInfo: null, - previousIP: 0, - currentInstruction: { - opcode: null, - operand: null, - mnemonic: null, - }, - cycleCounter: 0, - }, - /** Functions that update state **/ + /** Public interface **/ - /** @param {Uint8Array} data **/ - loadMemory(data) { - this.state.memory = new Uint8Array(256); - this.state.memory.set(data, 0); - }, + /** + * @param {Uint8Array} machineCode + **/ + loadMemory(machineCode) { + this.memory = new Uint8Array(256); + this.memory.set(machineCode, 0); + } + + peek() { return; } // TODO + + poke() { return; } // TODO /** @param {Array} info **/ // TODO type info loadSourceInfo(info) { - this.debug.sourceInfo = info; - }, + this.dbg.sourceInfo = info; + } - incrementIP(offset) { - this.state.previousIP = this.state.IP; - this.state.IP = this.state.IP + offset; - }, - - setIP(address) { - this.state.previousIP = this.state.IP; - this.state.IP = address; - }, - - updateFlagZero() { this.state.flags.Z = this.state.acc === 0; }, - updateFlagNegative() { this.state.acc & 128 ? this.state.flags.N = true : this.state.flags.N = false }, - - /** - * Load code into memory and set CPU state to "state.running" - * @param {Uint8Array} code - Machine code to load - **/ - startCPU(code) { - CPU.loadMemory(code); - CPU.state.running = true; - }, + /** Set CPU state to "state.running" **/ + start() { + this.running = true; + } /** Execute the next instruction in memory **/ - stepCPU() { - CPU.cycleStartCallbacks.forEach((fn) => fn()); + step() { + this._cycleStartCallbacks.forEach((fn) => fn()); - if (CPU.state.IP >= CPU.state.memory.length) { + if (this.IP >= this.memory.length) { console.error('HALTING - IP greater than memory size'); // TODO -- should this throw an error instead? - CPU.state.running = false; + this.running = false; } else { - CPU.debug.currentInstruction.opcode = CPU.state.memory[CPU.state.IP]; - CPU.debug.currentInstruction.operand = CPU.state.memory[CPU.state.IP+1]; - let executeInstruction = opcodes2mnemonics[CPU.debug.currentInstruction.opcode]; - - if (typeof executeInstruction === 'undefined') { failInvalidOpcode(); } - - executeInstruction(CPU.debug.currentInstruction.operand); - CPU.debug.cycleCounter += 1; + 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 ((CYCLE_LIMIT > 0) && CPU.debug.cycleCounter >= CYCLE_LIMIT) { + if ((this._cycleLimit > 0) && this.dbg.cycleCounter >= this._cycleLimit) { console.warn(' HALTING - reached cycle limit'); // TODO -- throw error? - CPU.state.running = false; + this.running = false; } - if (!CPU.state.running) process.exit(); - CPU.cycleEndCallbacks.forEach((fn) => fn()); - }, + 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 **/ - cycleStartCallbacks: [], - cycleEndCallbacks: [], + /** @type Array **/ + _cycleStartCallbacks = []; + + /** @type Array **/ + _cycleEndCallbacks = []; /** @param {function} fn **/ - onCycleStart(fn) { this.cycleStartCallbacks.push(fn) }, - onCycleEnd(fn) { this.cycleEndCallbacks.push(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'; + // Calculate sum + let sum = this.acc + lit; + if (sum > 255) { + this.flags.C = true; + sum = (sum % 255) - 1; + } else { + this.flags.C = false; + } + // Calculate overflow flag status + let bitSixCarry = 0; + if ((this.acc & 64) && (lit & 64)) { bitSixCarry = 1; } + // let overflow = bitSixCarry ^ (this.flags & 8); + // FIXME FIXME FIXME + // I'm on a plane and can't remember how this works + let overflow = 0; + if (overflow) { + this.flags.O = true; + } else { + this.flags.O = false; + } + this.acc = sum; + this._updateFlagNegative(); + this._updateFlagZero(); + this._incrementIP(2); + }, + + add_addr: (addr) => { + this.dbg.currentMnemonic = 'ADD addr'; + // Calculate sum + let sum = this.acc + this.memory[addr]; + if (sum > 255) { + this.flags.C = true; + sum = (sum % 255) - 1; + } else { + this.flags.C = false; + } + // Calculate overflow flag status + let bitSixCarry = 0; + if ((this.acc & 64) && (addr & 64)) { bitSixCarry = 1; } + // let overflow = bitSixCarry ^ (this.flags & 8); + // FIXME FIXME FIXME + // I'm on a plane and can't remember how this works + let overflow = 0; + if (overflow) { + this.flags.O = true; + } else { + this.flags.O = false; + } + this.acc = sum; + this._updateFlagNegative(); + this._updateFlagZero(); + this._incrementIP(2); + }, + + sub_lit: (lit) => { + this.dbg.currentMnemonic = 'SUB lit'; + // Calculate sum + let sum = this.acc - lit; + if (sum < 0) { + this.flags.C = true; + sum = sum + 256; + } else { + this.flags.C = false; + } + // Calculate overflow flag status + let bitSixCarry = 0; + if ((this.acc & 64) && (lit & 64)) { bitSixCarry = 1; } + // let overflow = bitSixCarry ^ (this.flags & 8); + // FIXME FIXME FIXME + // I'm on a plane and can't remember how this works + let overflow = 0; + if (overflow) { + this.flags.O = true; + } else { + this.flags.O = false; + } + this.acc = sum; + this._updateFlagNegative(); + this._updateFlagZero(); + this._incrementIP(2); + }, + + sub_addr: (addr) => { + this.dbg.currentMnemonic = 'SUB addr'; + // Calculate sum + let sum = this.acc - this.memory[addr]; + if (sum < 0) { + this.flags.C = true; + sum = sum + 256; + } else { + this.flags.C = false; + } + // Calculate overflow flag status + let bitSixCarry = 0; + if ((this.acc & 64) && (addr & 64)) { bitSixCarry = 1; } + // let overflow = bitSixCarry ^ (this.flags & 8); + // FIXME FIXME FIXME + // I'm on a plane and can't remember how this works + let overflow = 0; + if (overflow) { + this.flags.O = true; + } else { + this.flags.O = false; + } + this.acc = sum; + 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) { + console.error('Invalid flag number'); + process.exit(); // TODO review + } + 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(); + } } - - -// FUNCTIONS THAT MODIFY STATE - -const Instructions = { - end: () => { - CPU.debug.currentInstruction.mnemonic = 'END'; - CPU.state.running = false; - CPU.incrementIP(2); - }, - - store_lit: (lit) => { - CPU.debug.currentInstruction.mnemonic = 'STO lit'; - CPU.state.memory[lit] = CPU.state.acc; - CPU.incrementIP(2); - }, - - store_addr: (addr) => { - CPU.debug.currentInstruction.mnemonic = `STO addr; @addr: ${num2hex(CPU.state.memory[addr])}`; - CPU.state.memory[CPU.state.memory[addr]] = CPU.state.acc; - CPU.incrementIP(2); - }, - - load_lit: (lit) => { - CPU.debug.currentInstruction.mnemonic = 'LDA lit'; - CPU.state.acc = lit; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - load_addr: (addr) => { - CPU.debug.currentInstruction.mnemonic = `LDA addr; @ addr: ${num2hex(CPU.state.memory[addr])}`; - CPU.state.acc = CPU.state.memory[addr]; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - add_lit: (lit) => { - CPU.debug.currentInstruction.mnemonic = 'ADD lit'; - // Calculate sum - let sum = CPU.state.acc + lit; - if (sum > 255) { - CPU.state.flags.C = true; - sum = (sum % 255) - 1; - } else { - CPU.state.flags.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((CPU.state.acc & 64) && (lit & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.state.flags & 8); - // FIXME FIXME FIXME - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - CPU.state.flags.O = true; - } else { - CPU.state.flags.O = false; - } - CPU.state.acc = sum; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - add_addr: (addr) => { - CPU.debug.currentInstruction.mnemonic = 'ADD addr'; - // Calculate sum - let sum = CPU.state.acc + CPU.state.memory[addr]; - if (sum > 255) { - CPU.state.flags.C = true; - sum = (sum % 255) - 1; - } else { - CPU.state.flags.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((CPU.state.acc & 64) && (addr & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.state.flags & 8); - // FIXME FIXME FIXME - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - CPU.state.flags.O = true; - } else { - CPU.state.flags.O = false; - } - CPU.state.acc = sum; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - sub_lit: (lit) => { - CPU.debug.currentInstruction.mnemonic = 'SUB lit'; - // Calculate sum - let sum = CPU.state.acc - lit; - if (sum < 0) { - CPU.state.flags.C = true; - sum = sum + 256; - } else { - CPU.state.flags.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((CPU.state.acc & 64) && (lit & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.state.flags & 8); - // FIXME FIXME FIXME - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - CPU.state.flags.O = true; - } else { - CPU.state.flags.O = false; - } - CPU.state.acc = sum; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - sub_addr: (addr) => { - CPU.debug.currentInstruction.mnemonic = 'SUB addr'; - // Calculate sum - let sum = CPU.state.acc - CPU.state.memory[addr]; - if (sum < 0) { - CPU.state.flags.C = true; - sum = sum + 256; - } else { - CPU.state.flags.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((CPU.state.acc & 64) && (addr & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.state.flags & 8); - // FIXME FIXME FIXME - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - CPU.state.flags.O = true; - } else { - CPU.state.flags.O = false; - } - CPU.state.acc = sum; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - hop_lit: (lit) => { - CPU.debug.currentInstruction.mnemonic = `HOP lit; IP+2: ${CPU.state.memory[CPU.state.IP+2]}, IP+3: ${CPU.state.memory[CPU.state.IP+3]}`; - if (CPU.state.acc === lit) { - CPU.incrementIP(4); - } else { - CPU.incrementIP(2); - } - }, - - hop_addr: (addr) => { - CPU.debug.currentInstruction.mnemonic = 'HOP addr'; - if (CPU.state.acc === CPU.state.memory[addr]) { - CPU.incrementIP(4); - } else { - CPU.incrementIP(2); - } - }, - - jump_lit: (lit) => { - CPU.debug.currentInstruction.mnemonic = 'JMP lit'; - CPU.setIP(lit); - }, - - jump_addr: (addr) => { - CPU.debug.currentInstruction.mnemonic = 'JMP addr'; - CPU.setIP(CPU.state.memory[addr]); - }, - - flag_toggle: (flagNum) => { - if (flagNum === null) { - console.error('Invalid flag number'); - process.exit(); - } - const flagName = CPU.FLAGNUMS2NAMES[flagNum]; - CPU.debug.currentInstruction.mnemonic = `FTG ${flagName}`; - CPU.state.flags[flagName] = !CPU.state.flags[flagName]; - CPU.incrementIP(2); - }, - - flag_hop: (flagNum) => { - if (flagNum === null) { - console.error('Invalid flag number'); - process.exit(); - } - const flagName = CPU.FLAGNUMS2NAMES[flagNum]; - CPU.debug.currentInstruction.mnemonic = `FHP ${flagName}; IP+2: ${CPU.state.memory[CPU.state.IP+2]}, IP+3: ${CPU.state.memory[CPU.state.IP+3]}`; - if (CPU.state.flags[CPU.FLAGNUMS2NAMES[flagNum]]) { - CPU.incrementIP(4); - } else { - CPU.incrementIP(2); - } - }, - - no_op: () => { - CPU.debug.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(), -}; - - -function failInvalidOpcode() { - let info = CPU.debug.sourceInfo[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(); -} \ No newline at end of file From fbda5ce927e5f93a11b10ed7778b7ac71f01ae4d Mon Sep 17 00:00:00 2001 From: n loewen Date: Mon, 28 Aug 2023 23:09:51 -0400 Subject: [PATCH 05/62] cardiograph - WIP - Create minimal version of `cardiograph` file to act as CLI entrypoint to simulator --- src/cardiograph.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/cardiograph.js diff --git a/src/cardiograph.js b/src/cardiograph.js new file mode 100644 index 0000000..c227129 --- /dev/null +++ b/src/cardiograph.js @@ -0,0 +1,80 @@ +const CFG = require('./machine.config'); +const CPU = require('./cpu.lib.js'); + +const { num2hex, bool2bit } = require('./logging.js'); +const display = require('./display.js'); + + // TODO TEMPORARY - replace with reading STDIN: + const assembler = require('./assembler.js'); + const fs = require('fs'); +const { log } = require('console'); + + + const filename = process.argv[2]; + const inputFile_str = fs.readFileSync(filename, 'utf8'); + let assemblerOutput = assembler.assemble(inputFile_str); + +let cpu = new CPU(CFG.INITIAL_IP_ADDRESS, CFG.DEFAULT_CYCLE_LIMIT); + +cpu.loadMemory(assemblerOutput.machineCode); // TODO +cpu.loadSourceInfo(assemblerOutput.debugInfo); // TODO + +cpu.onCycleEnd(tick); +cpu.onCycleEnd(logDisplay); +cpu.onCycleEnd(logCPUState); + +cpu.start(); +cpu.step(); + +async function tick() { + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + await sleep(100); + cpu.step(); + if (!cpu.running) { + console.log('Halted'); + process.exit(); + } +} + +function logDisplay() { + display.show(cpu.memory, true); +} + +function logCPUState() { + let lineInfo = cpu.dbg.sourceInfo[cpu.dbg.previousIP]; + console.group(`Step ${cpu.dbg.cycleCounter}`); + console.log(); + if (lineInfo) { + console.log(`Line ${lineInfo.lineNumber}: ${lineInfo.source}`); + console.log(); + } + console.log('Mnemonic:', cpu.dbg.currentMnemonic); + console.log(`Machine: $${num2hex(cpu.instruction.opcode)} $${num2hex(cpu.instruction.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)}`); + // TODO: + // console.log(`KEY: $${num2hex(cpu.state.memory[KEYPAD_ADDR])}  ${cpu.state.running ? "state.running" : "halted" }`); + console.log(); + console.log(); + console.groupEnd(); +}; + + +/* +(async function() { + let input = await getInput(); + console.log(input); +})() +*/ + +async function getInput() { + // https://wellingguzman.com/notes/node-pipe-input + return new Promise(function (resolve, reject) { + const stdin = process.stdin; + stdin.setEncoding('utf8'); + let data = ''; + stdin.on('data', function (chunk) { data += chunk; }); + stdin.on('end', function () { resolve(data); }); + stdin.on('error', reject); + }); +} \ No newline at end of file From 5b52143ad0bb5199d5f82ca1ab314021259dcba0 Mon Sep 17 00:00:00 2001 From: n loewen Date: Mon, 28 Aug 2023 23:11:15 -0400 Subject: [PATCH 06/62] Remove `run-cpu.js` (it is superseded by `cardiograph`) --- src/run-cpu.js | 71 -------------------------------------------------- 1 file changed, 71 deletions(-) delete mode 100755 src/run-cpu.js diff --git a/src/run-cpu.js b/src/run-cpu.js deleted file mode 100755 index face7d4..0000000 --- a/src/run-cpu.js +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node - -// Usage: ./run-cpu.js -f code.asm [--debug] [--step] [--pretty] - -const fs = require('fs'); -const computer = require('./cpu.js'); -const assembler = require('./assembler.js'); -const { logRunningHeader } = require('./logging.js'); - -// Load file... - -let filename; -try { - filename = getArgumentValue('-f', `Missing filename`); -} catch (error) { - console.error(error.message); - process.exit() -} - -const inputFile_str = fs.readFileSync(filename, 'utf8'); - - -// Check optional arguments... - -let debug = false; -let singleStep = false; -let prettyPrint = false; -process.argv.forEach((arg) => { if (arg === '--debug') { debug = true } }); -process.argv.forEach((arg) => { if (arg === '--step') { singleStep = true } }); -process.argv.forEach((arg) => { if (arg === '--pretty') { prettyPrint = true } }); - -let speed = null; -process.argv.forEach((arg, index) => { - if (arg === '--speed' && process.argv.length > (index -1)) { - speed = parseInt(process.argv[index + 1]); - } -}); - - -let assemblerOutput = assembler.assemble(inputFile_str); -logRunningHeader(); -computer.runProgram( - assemblerOutput.machineCode, - assemblerOutput.debugInfo, - debug, - singleStep, - prettyPrint, - speed -); - - -// CLI args TODO -// - check if value is the name of another arg -// - usage info -// - catch nonexistant flags - -/** - * @param {string} flag - The command line flag, eg. '-f' - * @param {string} errorMessage - The error to throw if a value isn't found - * @returns {string} - **/ -function getArgumentValue(flag, errorMessage) { - let value = null; - process.argv.forEach((arg, index) => { - if (arg === flag && process.argv.length > (index -1)) { - value = process.argv[index + 1]; - } - }); - if (!value) throw new Error(errorMessage); - return value; -} From efe20eabdf81b97b218284e7573eae37ec2e0188 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 09:30:33 -0400 Subject: [PATCH 07/62] cpu - Rename back to 'cpu.js' --- src/{cpu.lib.js => cpu.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{cpu.lib.js => cpu.js} (100%) diff --git a/src/cpu.lib.js b/src/cpu.js similarity index 100% rename from src/cpu.lib.js rename to src/cpu.js From ccc032b379844f69aaf036e1787da0e8a0e35c20 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 09:49:21 -0400 Subject: [PATCH 08/62] Move display functions to `io.js`, and re-implement keypad in `io.js` --- src/cardiograph.js | 28 +++++++++++++--------------- src/display.js | 22 ---------------------- src/io.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 37 deletions(-) delete mode 100644 src/display.js create mode 100644 src/io.js diff --git a/src/cardiograph.js b/src/cardiograph.js index c227129..68ddf59 100644 --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -1,14 +1,13 @@ -const CFG = require('./machine.config'); -const CPU = require('./cpu.lib.js'); - const { num2hex, bool2bit } = require('./logging.js'); -const display = require('./display.js'); - // TODO TEMPORARY - replace with reading STDIN: +const CFG = require('./machine.config.js'); +const CPU = require('./cpu.js'); +const io = require('./io.js'); + + +// TODO TEMPORARY - replace with reading STDIN: const assembler = require('./assembler.js'); const fs = require('fs'); -const { log } = require('console'); - const filename = process.argv[2]; const inputFile_str = fs.readFileSync(filename, 'utf8'); @@ -16,14 +15,15 @@ const { log } = require('console'); let cpu = new CPU(CFG.INITIAL_IP_ADDRESS, CFG.DEFAULT_CYCLE_LIMIT); -cpu.loadMemory(assemblerOutput.machineCode); // TODO -cpu.loadSourceInfo(assemblerOutput.debugInfo); // TODO +cpu.loadMemory(assemblerOutput.machineCode); +cpu.loadSourceInfo(assemblerOutput.debugInfo); cpu.onCycleEnd(tick); cpu.onCycleEnd(logDisplay); cpu.onCycleEnd(logCPUState); cpu.start(); +io.getKeypadInput(cpu); cpu.step(); async function tick() { @@ -37,7 +37,7 @@ async function tick() { } function logDisplay() { - display.show(cpu.memory, true); + io.showDisplay(cpu.memory, true); // TODO more compled printing } function logCPUState() { @@ -52,9 +52,7 @@ function logCPUState() { console.log(`Machine: $${num2hex(cpu.instruction.opcode)} $${num2hex(cpu.instruction.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)}`); - // TODO: - // console.log(`KEY: $${num2hex(cpu.state.memory[KEYPAD_ADDR])}  ${cpu.state.running ? "state.running" : "halted" }`); - console.log(); + console.log(`KEY: ${io.readKeyMem(cpu.memory)} ${cpu.running ? "running" : "halted" }`); console.log(); console.groupEnd(); }; @@ -62,12 +60,12 @@ function logCPUState() { /* (async function() { - let input = await getInput(); + let input = await readPipedStdin(); console.log(input); })() */ -async function getInput() { +async function readPipedStdin() { // https://wellingguzman.com/notes/node-pipe-input return new Promise(function (resolve, reject) { const stdin = process.stdin; diff --git a/src/display.js b/src/display.js deleted file mode 100644 index efa4c34..0000000 --- a/src/display.js +++ /dev/null @@ -1,22 +0,0 @@ -const { POINTER_TO_DISPLAY } = require('./machine.config'); -const { num2hex } = require('./logging.js'); - -/** - * Print the contents of display memory - * by default, each pixel is shown as a hex number - * @param {Uint8Array} mem - CPU memory - * @param {Boolean} pretty - Display pixels using black and white emoji circles - **/ -const printDisplay = (mem, pretty=false) => { - const disp = mem[POINTER_TO_DISPLAY]; - const num2pic = (n) => n > 0 ? '⚫' : '⚪'; - let fmt = (n) => num2hex(n); - if (pretty) fmt = (n) => num2pic(n); - for (let i = disp; i < disp + 25; i += 5) { - console.log(`${fmt(mem[i])} ${fmt(mem[i+1])} ${fmt(mem[i+2])} ${fmt(mem[i+3])} ${fmt(mem[i+4])}`); - } -} - -module.exports = { - "show": printDisplay, -} \ No newline at end of file diff --git a/src/io.js b/src/io.js new file mode 100644 index 0000000..364bd12 --- /dev/null +++ b/src/io.js @@ -0,0 +1,45 @@ +const readline = require('readline'); + +const CFG = require('./machine.config.js'); +const { num2hex } = require('./logging.js'); + +function readKeyMem(mem) { + return mem[CFG.KEYPAD_ADDR]; +} + +function getKeypadInput(cpu) { + 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 CFG.KEY_MAP) { + cpu.memory[CFG.KEYPAD_ADDR] = CFG.KEY_MAP[name]; + } + }); +} + +/** + * Print the contents of display memory + * by default, each pixel is shown as a hex number + * @param {Uint8Array} mem - CPU memory + * @param {Boolean} pretty - Display pixels using black and white emoji circles + **/ +function showDisplay(mem, pretty=false) { + const disp = mem[CFG.POINTER_TO_DISPLAY]; + const num2pic = (n) => n > 0 ? '⚫' : '⚪'; + let fmt = (n) => num2hex(n); + if (pretty) fmt = (n) => num2pic(n); + for (let i = disp; i < disp + 25; i += 5) { + console.log(`${fmt(mem[i])} ${fmt(mem[i+1])} ${fmt(mem[i+2])} ${fmt(mem[i+3])} ${fmt(mem[i+4])}`); + } +} + +module.exports = { + "showDisplay": showDisplay, + "getKeypadInput": getKeypadInput, + "readKeyMem": readKeyMem, +} \ No newline at end of file From 74c6f83fccd9153d3a316ea61c1c96aede575b63 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 09:56:21 -0400 Subject: [PATCH 09/62] machine config - Change CFG variable names to lowercase; Change to always access them as 'CFG.property' --- src/assembler.js | 10 +++------- src/cardiograph.js | 2 +- src/io.js | 8 ++++---- src/machine.config.js | 12 ++++++------ 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index d30c41c..d054906 100644 --- a/src/assembler.js +++ b/src/assembler.js @@ -1,9 +1,5 @@ const { logMemory, num2hex } = require('./logging.js'); -const { - INITIAL_IP_ADDRESS, - DISPLAY_ADDR, - POINTER_TO_DISPLAY, -} = require('./machine.config.js'); +const CFG = require('./machine.config.js'); // 1 = verbose // 2 = what i'm currently focusing on @@ -203,7 +199,7 @@ function decodeInstructions(source) { if (lines[idOfFirstLineWithCode].operation.startsWith(ASM_IP_LABEL)) { IP = parseInt(lines[idOfFirstLineWithCode].argument); } else { - IP = INITIAL_IP_ADDRESS; + IP = CFG.initialIP; } // Initialize arrays to collect assembled code @@ -214,7 +210,7 @@ function decodeInstructions(source) { let debugInfo = {}; // Initialize memory-mapped IO -- TODO this should probably be in the CPU, not here - machineCode[POINTER_TO_DISPLAY] = DISPLAY_ADDR; + machineCode[CFG.pointerToDisplay] = CFG.displayAddr; // Initialize arrays that collect code references that // have to be revisited after our first pass through the source diff --git a/src/cardiograph.js b/src/cardiograph.js index 68ddf59..35dc9e9 100644 --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -13,7 +13,7 @@ const io = require('./io.js'); const inputFile_str = fs.readFileSync(filename, 'utf8'); let assemblerOutput = assembler.assemble(inputFile_str); -let cpu = new CPU(CFG.INITIAL_IP_ADDRESS, CFG.DEFAULT_CYCLE_LIMIT); +let cpu = new CPU(CFG.initialIP, CFG.defaultCycleLimit); cpu.loadMemory(assemblerOutput.machineCode); cpu.loadSourceInfo(assemblerOutput.debugInfo); diff --git a/src/io.js b/src/io.js index 364bd12..0bd3b24 100644 --- a/src/io.js +++ b/src/io.js @@ -4,7 +4,7 @@ const CFG = require('./machine.config.js'); const { num2hex } = require('./logging.js'); function readKeyMem(mem) { - return mem[CFG.KEYPAD_ADDR]; + return mem[CFG.keypadAddr]; } function getKeypadInput(cpu) { @@ -16,8 +16,8 @@ function getKeypadInput(cpu) { // TODO: is it possible to turn this off again? if (key.sequence === '\x03') process.exit(); let name = key.name.toUpperCase(); - if (name in CFG.KEY_MAP) { - cpu.memory[CFG.KEYPAD_ADDR] = CFG.KEY_MAP[name]; + if (name in CFG.keyMap) { + cpu.memory[CFG.keypadAddr] = CFG.keyMap[name]; } }); } @@ -29,7 +29,7 @@ function getKeypadInput(cpu) { * @param {Boolean} pretty - Display pixels using black and white emoji circles **/ function showDisplay(mem, pretty=false) { - const disp = mem[CFG.POINTER_TO_DISPLAY]; + const disp = mem[CFG.pointerToDisplay]; const num2pic = (n) => n > 0 ? '⚫' : '⚪'; let fmt = (n) => num2hex(n); if (pretty) fmt = (n) => num2pic(n); diff --git a/src/machine.config.js b/src/machine.config.js index 190ead1..7ced02f 100644 --- a/src/machine.config.js +++ b/src/machine.config.js @@ -1,13 +1,13 @@ module.exports = { - "INITIAL_IP_ADDRESS": 29, + "initialIP": 29, // Use these in CPU: - "DISPLAY_ADDR": 0, - "KEYPAD_ADDR": 27, + "displayAddr": 0, + "keypadAddr": 27, // Store the `DISPLAY_ADDR` at this address when assembling: - "POINTER_TO_DISPLAY": 26, + "pointerToDisplay": 26, - "KEY_MAP": { + "keyMap": { // Same layout as COSMAC VIP / CHIP-8 // (This object maps qwerty keys to hex keys // so that they are arranged in the same layout @@ -27,5 +27,5 @@ module.exports = { // max number of times to step the CPU, // to stop endless loops // 0 = infinite - "DEFAULT_CYCLE_LIMIT": 2048, + "defaultCycleLimit": 2048, } \ No newline at end of file From b08d9854c4ff83a97d97a39873d81ff5b091371c Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 10:04:18 -0400 Subject: [PATCH 10/62] Update TODO/FIXME comments for increased clarity and accuracy --- src/assembler.js | 2 +- src/cardiograph.js | 4 ++-- src/cpu.js | 20 ++++++++++---------- src/io.js | 1 - 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index d054906..c55a0fd 100644 --- a/src/assembler.js +++ b/src/assembler.js @@ -127,7 +127,7 @@ function decodeNumericOp(arg) { /** * @param {string} op - * @param {object} labels // TODO document better + * @param {object} labels // TODO - document labels object * @param {number} IP * @returns {Array} - array of labels **/ diff --git a/src/cardiograph.js b/src/cardiograph.js index 35dc9e9..194ff2b 100644 --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -5,7 +5,7 @@ const CPU = require('./cpu.js'); const io = require('./io.js'); -// TODO TEMPORARY - replace with reading STDIN: +// TODO - replace with reading STDIN: const assembler = require('./assembler.js'); const fs = require('fs'); @@ -37,7 +37,7 @@ async function tick() { } function logDisplay() { - io.showDisplay(cpu.memory, true); // TODO more compled printing + io.showDisplay(cpu.memory, true); // FIXME - display - allow printing hex as well as pretty-printing } function logCPUState() { diff --git a/src/cpu.js b/src/cpu.js index 22649de..54df653 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -35,11 +35,11 @@ module.exports = class CPU { this.memory.set(machineCode, 0); } - peek() { return; } // TODO + peek() { return; } // TODO - implement Peek - poke() { return; } // TODO + poke() { return; } // TODO - implement Poke - /** @param {Array} info **/ // TODO type info + /** @param {Array} info **/ // TODO - document type for 'sourceInfo' loadSourceInfo(info) { this.dbg.sourceInfo = info; } @@ -54,7 +54,7 @@ module.exports = class CPU { this._cycleStartCallbacks.forEach((fn) => fn()); if (this.IP >= this.memory.length) { - console.error('HALTING - IP greater than memory size'); // TODO -- should this throw an error instead? + console.error('HALTING - IP greater than memory size'); // FIXME - halting the CPU should throw an error instead this.running = false; } else { this.instruction.opcode = this.memory[this.IP]; @@ -68,7 +68,7 @@ module.exports = class CPU { // Temporary limit as a lazy way to halt infinite loops if ((this._cycleLimit > 0) && this.dbg.cycleCounter >= this._cycleLimit) { - console.warn(' HALTING - reached cycle limit'); // TODO -- throw error? + console.warn(' HALTING - reached cycle limit'); // FIXME - throw error instead this.running = false; } @@ -164,7 +164,7 @@ module.exports = class CPU { let bitSixCarry = 0; if ((this.acc & 64) && (lit & 64)) { bitSixCarry = 1; } // let overflow = bitSixCarry ^ (this.flags & 8); - // FIXME FIXME FIXME + // FIXME - re-implement overflow // I'm on a plane and can't remember how this works let overflow = 0; if (overflow) { @@ -192,7 +192,7 @@ module.exports = class CPU { let bitSixCarry = 0; if ((this.acc & 64) && (addr & 64)) { bitSixCarry = 1; } // let overflow = bitSixCarry ^ (this.flags & 8); - // FIXME FIXME FIXME + // FIXME - re-implement overflow // I'm on a plane and can't remember how this works let overflow = 0; if (overflow) { @@ -220,7 +220,7 @@ module.exports = class CPU { let bitSixCarry = 0; if ((this.acc & 64) && (lit & 64)) { bitSixCarry = 1; } // let overflow = bitSixCarry ^ (this.flags & 8); - // FIXME FIXME FIXME + // FIXME - re-implement overflow // I'm on a plane and can't remember how this works let overflow = 0; if (overflow) { @@ -248,7 +248,7 @@ module.exports = class CPU { let bitSixCarry = 0; if ((this.acc & 64) && (addr & 64)) { bitSixCarry = 1; } // let overflow = bitSixCarry ^ (this.flags & 8); - // FIXME FIXME FIXME + // FIXME - re-implement overflow // I'm on a plane and can't remember how this works let overflow = 0; if (overflow) { @@ -293,7 +293,7 @@ module.exports = class CPU { flag_toggle: (flagNum) => { if (flagNum === null) { console.error('Invalid flag number'); - process.exit(); // TODO review + process.exit(); // FIXME -- throw error instead } const flagName = this.flagNums[flagNum]; this.dbg.currentMnemonic = `FTG ${flagName}`; diff --git a/src/io.js b/src/io.js index 0bd3b24..ec7f413 100644 --- a/src/io.js +++ b/src/io.js @@ -13,7 +13,6 @@ function getKeypadInput(cpu) { 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 CFG.keyMap) { From 37bb92f2960ed464650cc9b92ee17daaa9ec7bd5 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 10:42:36 -0400 Subject: [PATCH 11/62] cpu - Change to throw errors, instead of logging with console.error() --- src/cpu.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cpu.js b/src/cpu.js index 54df653..c66fa69 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -54,8 +54,8 @@ module.exports = class CPU { this._cycleStartCallbacks.forEach((fn) => fn()); if (this.IP >= this.memory.length) { - console.error('HALTING - IP greater than memory size'); // FIXME - halting the CPU should throw an error instead 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]; @@ -68,8 +68,8 @@ module.exports = class CPU { // Temporary limit as a lazy way to halt infinite loops if ((this._cycleLimit > 0) && this.dbg.cycleCounter >= this._cycleLimit) { - console.warn(' HALTING - reached cycle limit'); // FIXME - throw error instead this.running = false; + throw new Error(' HALTING - reached cycle limit'); } this._cycleEndCallbacks.forEach((fn) => fn()); @@ -292,8 +292,8 @@ module.exports = class CPU { flag_toggle: (flagNum) => { if (flagNum === null) { - console.error('Invalid flag number'); - process.exit(); // FIXME -- throw error instead + 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}`; From d9212ab6207a21da925a810589d111090ccba356 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 10:44:06 -0400 Subject: [PATCH 12/62] cardiograph - Move display-drawing so that it nests nicely inside a console.group() --- src/cardiograph.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/cardiograph.js b/src/cardiograph.js index 194ff2b..4694460 100644 --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -19,7 +19,6 @@ cpu.loadMemory(assemblerOutput.machineCode); cpu.loadSourceInfo(assemblerOutput.debugInfo); cpu.onCycleEnd(tick); -cpu.onCycleEnd(logDisplay); cpu.onCycleEnd(logCPUState); cpu.start(); @@ -36,13 +35,11 @@ async function tick() { } } -function logDisplay() { - io.showDisplay(cpu.memory, true); // FIXME - display - allow printing hex as well as pretty-printing -} - function logCPUState() { let lineInfo = cpu.dbg.sourceInfo[cpu.dbg.previousIP]; console.group(`Step ${cpu.dbg.cycleCounter}`); + console.log(); + io.showDisplay(cpu.memory, true); // FIXME - display - allow printing hex as well as pretty-printing console.log(); if (lineInfo) { console.log(`Line ${lineInfo.lineNumber}: ${lineInfo.source}`); From b776951e8151f69b5df45039a46bb9a39d2a9e12 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 11:51:00 -0400 Subject: [PATCH 13/62] logging - Fix bad alignment on rows with just one entry --- src/logging.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logging.js b/src/logging.js index 39ee03e..a8ca701 100644 --- a/src/logging.js +++ b/src/logging.js @@ -15,7 +15,7 @@ const logMemory = (mem, start=0, end=mem.length) => { for (let i = start; i < mem.length; i +=2) { let operand = mem[i+1]; if (typeof operand === 'undefined') { - console.log(` ${num2hex(i)} ${num2hex(i+1)} │ ${num2hex(mem[i])} │ │`); + console.log(`│ ${num2hex(i)} ${num2hex(i+1)} │ ${num2hex(mem[i])} │ │`); } else { console.log(`│ ${num2hex(i)} ${num2hex(i+1)} │ ${num2hex(mem[i])} │ ${num2hex(operand)} │`); } From a2a79cea4620a4dc58a691a5c613847534242377 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 11:51:17 -0400 Subject: [PATCH 14/62] dbg - Create debugging library --- src/dbg.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/dbg.js diff --git a/src/dbg.js b/src/dbg.js new file mode 100644 index 0000000..a0806a9 --- /dev/null +++ b/src/dbg.js @@ -0,0 +1,80 @@ +module.exports = class DBG { + /** + * @param ${'warn'|'info'|'debug'|'nitpick'} [level='info'] + **/ + constructor(level = 'info') { + if (this._levels.includes(level)) { + this._level = level; + } else { + throw new Error(`'${level}' is not a valid debug level`); + } + this._levels.forEach((l) => { + this._createLogger(l); + this._createGrouper(l) + this._createGroupEnder(l) + this._createExecer(l) + }); + } + + _levels = ['nitpick', 'debug', 'info', 'warn']; + _shortNames = {'nitpick':'nit', 'debug':'d', 'info':'i', 'warn':'warn'}; + + _createLogger(name) { + const short = this._shortNames[name] + this[short] = (s = '') => { + if (this._lvl2num(name) >= this._lvl2num(this._level)) { + console.log(s); + } + } + } + + _createGrouper(name) { + const short = this._shortNames[name] + this[short + 'Group'] = (s) => { + if (this._lvl2num(name) >= this._lvl2num(this._level)) { + console.group(s); + } + } + } + + _createGroupEnder(name) { + const short = this._shortNames[name] + this[short+'GroupEnd'] = () => { + if (this._lvl2num(name) >= this._lvl2num(this._level)) { + console.groupEnd(); + } + } + } + + _createExecer(name) { + const short = this._shortNames[name] + this[short+'Exec'] = (func) => { + if (this._lvl2num(name) >= this._lvl2num(this._level)) { + func(); + } + } + } + + _lvl2num(lvl) { + return 1 + this._levels.findIndex(l => l === lvl); + } +} + +/* TEST +const dbg = new DBG('nitpick'); +dbg.warnGroup('w'); +dbg.warn('warn'); +dbg.warnGroupEnd(); + +dbg.iGroup('i'); +dbg.i('info'); +dbg.iGroupEnd(); + +dbg.dGroup('d'); +dbg.d('debug'); +dbg.dGroupEnd(); + +dbg.nitGroup('n'); +dbg.nit('nitpick'); +dbg.nitGroupEnd(); +*/ \ No newline at end of file From 28b92b49807797797776bada9eb41cfb6cc25559 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 11:51:53 -0400 Subject: [PATCH 15/62] assembler - Replace debugging with new debugging library --- src/assembler.js | 116 +++++++++++++++++++++++------------------------ 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index c55a0fd..2e7cb1c 100644 --- a/src/assembler.js +++ b/src/assembler.js @@ -1,22 +1,10 @@ +const fs = require('fs'); + const { logMemory, num2hex } = require('./logging.js'); +const DBG = require('./dbg.js'); + const CFG = require('./machine.config.js'); -// 1 = verbose -// 2 = what i'm currently focusing on -// 3 = always print -// 4 = silent -const DEBUG_LEVEL = 2; -let DEBUG; // Turn debugging on/off -- set by assemble() - -/** - * @param {string} assemblyCode - * @param {Boolean} [debug = false] - **/ -exports.assemble = (assemblyCode, debug = false) => { - DEBUG = debug; - return decodeInstructions(assemblyCode); -} - // Configure pseudo-ops: const ASM_IP_LABEL = '*'; const ASM_CONSTANT_PREFIX = '#'; @@ -36,7 +24,6 @@ const mnemonics2opcodes = { nop: { direct: 15, indirect: 15 }, }; - /** * @typedef {('code'|'comment'|'blank')} SourceLineType **/ @@ -73,15 +60,15 @@ function preparseSourceCode(source) { } return lines.map((line, index) => { - dbg(1, ` in: ${line}`); + dbg.nit(` in: ${line}`); let info = { number: index + 1, source: line, sanitized: stripWhitespaceFromEnds(stripComments(line)), type: getLineType(line), }; - dbg(1, ` → ${info.number} - ${info.type}: ${info.sanitized}`); - dbg(1, ``); + dbg.nit(` → ${info.number} - ${info.type}: ${info.sanitized}`); + dbg.nit(``); if (info.type === 'code') { const op_arg_array = info.sanitized.split(/\s+/); // split line into an array of [op, arg, extra_arg] @@ -142,10 +129,10 @@ function handleLabelDefinition(op, IP, labels) { bytesToReplace: [], }; } - dbg(1, ` Label definition:`); - dbg(1, ` Points to byte: ${labels[label].pointsToByte}`); - dbg(1, ` Bytes to replace: ${labels[label].bytesToReplace}`); - dbg(1, ` IP: $${num2hex(IP)}, new code: none`); + dbg.nit(` Label definition:`); + dbg.nit(` Points to byte: ${labels[label].pointsToByte}`); + dbg.nit(` Bytes to replace: ${labels[label].bytesToReplace}`); + dbg.nit(` IP: $${num2hex(IP)}, new code: none`); dbgGroupEnd(1, 'Input line'); return labels; } @@ -163,10 +150,10 @@ function handleConstantDefinitions(op, arg, IP, constants) { constantValue = IP.toString(); } constants[constantName] = constantValue; - dbg(1, ''); - dbg(1, `Constants:`); - dbg(1, constants); - dbg(1, ''); + dbg.nit(''); + dbg.nit(`Constants:`); + dbg.nit(constants); + dbg.nit(''); return constants; } @@ -181,12 +168,12 @@ function handleConstantDefinitions(op, arg, IP, constants) { * @return {{ debugInfo: Object, machineCode: Uint8Array }}; **/ function decodeInstructions(source) { - dbg(1, 'Pre-parsing...'); + dbg.nit('Pre-parsing...'); let lines = preparseSourceCode(source); - dbg(1, ''); - dbg(1, 'Done pre-parsing.'); - dbg(1, ''); - dbg(1, 'Assembling...'); + dbg.nit(''); + dbg.nit('Done pre-parsing.'); + dbg.nit(''); + dbg.nit('Assembling...'); // Figure out where to start assembly... @@ -283,23 +270,23 @@ function decodeInstructions(source) { if (line.argument.startsWith(ASM_LABEL_PREFIX)) { let label = line.argument.substring(1); // strip label prefix if (label in labels) { - dbg(1, `'${label}' already in labels object`); + dbg.nit(`'${label}' already in labels object`); labels[label].bytesToReplace.push(IP + 1); } else { - dbg(1, `'${label}' NOT in labels object`); + dbg.nit(`'${label}' NOT in labels object`); labels[label] = { bytesToReplace: [IP + 1], }; } - dbg(1, `Label reference:`); - dbg(1, ` Points to byte: ${labels[label].pointsToByte}`); - dbg(1, ` Bytes to replace: ${labels[label].bytesToReplace}`); + dbg.nit(`Label reference:`); + dbg.nit(` Points to byte: ${labels[label].pointsToByte}`); + dbg.nit(` Bytes to replace: ${labels[label].bytesToReplace}`); decodedArg = 0; // Return 0 for operand for now -- we'll replace it later } // Operands - Handle references to the Instruction Pointer if (line.argument === ASM_IP_LABEL) { - dbg(1, ` References current IP - ${IP}`); + dbg.nit(` References current IP - ${IP}`); if (typeof line.extraArgument === 'undefined') { decodedArg = IP; } else { @@ -309,7 +296,7 @@ function decodeInstructions(source) { // Operands - Handle references to constants if (line.argument.startsWith(ASM_CONSTANT_PREFIX)) { - dbg(1, `References '${line.argument}'`); + dbg.nit(`References '${line.argument}'`); if (typeof constants[line.argument.substring(1)] === 'undefined') { console.error(); console.error(`Error: Undefined constant '${line.argument}'`); @@ -322,7 +309,7 @@ function decodeInstructions(source) { // Operands - Handle references to constants in indirect mode if (line.argument.startsWith(`(${ASM_CONSTANT_PREFIX}`)) { addressingMode = "indirect"; - dbg(1, `(Indirectly) References '${line.argument}'`); + dbg.nit(`(Indirectly) References '${line.argument}'`); let constName = line.argument.replace(`(${ASM_CONSTANT_PREFIX}`, ""); constName = constName.replace(")", ""); decodedArg = decodeNumericOp(constants[constName]); @@ -356,31 +343,31 @@ function decodeInstructions(source) { }; - dbg(3, ``); - dbg(3, `Line ${line.number}: ${line.source}`); + dbg.i(); + dbg.i(`Line ${line.number}: ${line.source}`); if (line.argument) { - dbg(3, ` Asm operation: ${line.operation.toUpperCase()} ${line.argument}`); + dbg.i(` Asm operation: ${line.operation.toUpperCase()} ${line.argument}`); } else if (line.operation) { - dbg(3, ` Asm operation: ${line.operation.toUpperCase()}`); + dbg.i(` Asm operation: ${line.operation.toUpperCase()}`); } - dbg(3, ` Machine code: $${num2hex(decodedOp)} $${num2hex(decodedArg)}`); - dbg(3, ` IP: $${num2hex(IP)}`); + dbg.i(` Machine code: $${num2hex(decodedOp)} $${num2hex(decodedArg)}`); + dbg.i(` IP: $${num2hex(IP)}`); IP += 2; }; } - dbg(1, ''); - dbgGroup(1, 'Memory before filling in label constants'); - dbgExec(1, () => logMemory(new Uint8Array(machineCode))); - dbgGroupEnd(1); + dbg.nit(''); + dbg.nitGroup('Memory before filling in label constants'); + dbg.nitExec(() => logMemory(new Uint8Array(machineCode))); + dbg.nitGroupEnd(); // Backfill label references for (let k of Object.keys(labels)) { dbgGroup(1, `${ASM_LABEL_PREFIX}${k}`); let label = labels[k]; - dbg(1, `Points to byte: ${label.pointsToByte}`); - dbg(1, `Bytes to replace: ${label.bytesToReplace}`); + dbg.nit(`Points to byte: ${label.pointsToByte}`); + dbg.nit(`Bytes to replace: ${label.bytesToReplace}`); dbgGroupEnd(1); for (let j = 0; j < label.bytesToReplace.length; j++) { machineCode[label.bytesToReplace[j]] = label.pointsToByte; @@ -411,8 +398,21 @@ function stripWhitespaceFromEnds(line) { function hex2num(hex) { return parseInt(hex, 16) }; -// Debug helpers -const dbg = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.log(s) }; -const dbgGroup = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.group(s) }; -const dbgGroupEnd = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.groupEnd() }; -const dbgExec = (lvl, func) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) func(); } \ No newline at end of file + +/** MAIN **/ + +// Initialize debugger +const dbg = new DBG('nitpick'); + +// Get input +const filename = process.argv[2]; // FIXME +const inputFile_str = fs.readFileSync(filename, 'utf8'); +assemble(inputFile_str); + + +/** + * @param {string} assemblyCode + **/ +function assemble(assemblyCode) { + return decodeInstructions(assemblyCode); +} \ No newline at end of file From 8a1923861236bca52b7a86ca6cb22f1fa48636c0 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 12:02:18 -0400 Subject: [PATCH 16/62] dbg - Change to hardcoded functions instead of factory-generated ones, because VS Code type checking can't cope with the generated ones --- src/assembler.js | 6 +-- src/dbg.js | 95 ++++++++++++++++++++++++++++++------------------ 2 files changed, 63 insertions(+), 38 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index 2e7cb1c..82d4b42 100644 --- a/src/assembler.js +++ b/src/assembler.js @@ -133,7 +133,7 @@ function handleLabelDefinition(op, IP, labels) { dbg.nit(` Points to byte: ${labels[label].pointsToByte}`); dbg.nit(` Bytes to replace: ${labels[label].bytesToReplace}`); dbg.nit(` IP: $${num2hex(IP)}, new code: none`); - dbgGroupEnd(1, 'Input line'); + dbg.nitGroupEnd('Input line'); return labels; } @@ -364,11 +364,11 @@ function decodeInstructions(source) { // Backfill label references for (let k of Object.keys(labels)) { - dbgGroup(1, `${ASM_LABEL_PREFIX}${k}`); + dbg.nitGroup(`${ASM_LABEL_PREFIX}${k}`); let label = labels[k]; dbg.nit(`Points to byte: ${label.pointsToByte}`); dbg.nit(`Bytes to replace: ${label.bytesToReplace}`); - dbgGroupEnd(1); + dbg.nitGroupEnd(); for (let j = 0; j < label.bytesToReplace.length; j++) { machineCode[label.bytesToReplace[j]] = label.pointsToByte; } diff --git a/src/dbg.js b/src/dbg.js index a0806a9..e9aa3a9 100644 --- a/src/dbg.js +++ b/src/dbg.js @@ -8,51 +8,76 @@ module.exports = class DBG { } else { throw new Error(`'${level}' is not a valid debug level`); } - this._levels.forEach((l) => { - this._createLogger(l); - this._createGrouper(l) - this._createGroupEnder(l) - this._createExecer(l) - }); } _levels = ['nitpick', 'debug', 'info', 'warn']; - _shortNames = {'nitpick':'nit', 'debug':'d', 'info':'i', 'warn':'warn'}; - _createLogger(name) { - const short = this._shortNames[name] - this[short] = (s = '') => { - if (this._lvl2num(name) >= this._lvl2num(this._level)) { - console.log(s); - } - } + warn = (s='') => { + if (this._lvl2num('warn') < this._lvl2num(this._level)) return + console.log(s); + } + i = (s='') => { + if (this._lvl2num('info') < this._lvl2num(this._level)) return + console.log(s); + } + d = (s='') => { + if (this._lvl2num('debug') < this._lvl2num(this._level)) return + console.log(s); + } + nit = (s='') => { + if (this._lvl2num('nitpick') < this._lvl2num(this._level)) return + console.log(s); } - _createGrouper(name) { - const short = this._shortNames[name] - this[short + 'Group'] = (s) => { - if (this._lvl2num(name) >= this._lvl2num(this._level)) { - console.group(s); - } - } + warnGroup = (s) => { + if (this._lvl2num('warn') < this._lvl2num(this._level)) return + console.group(s); + } + infoGroup = (s) => { + if (this._lvl2num('info') < this._lvl2num(this._level)) return + console.group(s); + } + debugGroup = (s) => { + if (this._lvl2num('debug') < this._lvl2num(this._level)) return + console.group(s); + } + nitGroup = (s) => { + if (this._lvl2num('nit') < this._lvl2num(this._level)) return + console.group(s); } - _createGroupEnder(name) { - const short = this._shortNames[name] - this[short+'GroupEnd'] = () => { - if (this._lvl2num(name) >= this._lvl2num(this._level)) { - console.groupEnd(); - } - } + warnGroupEnd = (s) => { + if (this._lvl2num('warn') < this._lvl2num(this._level)) return + console.groupEnd(); + } + infoGroupEnd = (s) => { + if (this._lvl2num('info') < this._lvl2num(this._level)) return + console.group(); + } + debugGroupEnd = (s) => { + if (this._lvl2num('debug') < this._lvl2num(this._level)) return + console.group(); + } + nitGroupEnd = (s) => { + if (this._lvl2num('nit') < this._lvl2num(this._level)) return + console.group(); } - _createExecer(name) { - const short = this._shortNames[name] - this[short+'Exec'] = (func) => { - if (this._lvl2num(name) >= this._lvl2num(this._level)) { - func(); - } - } + warnExec = (fn) => { + if (this._lvl2num('warn') < this._lvl2num(this._level)) return + fn(); + } + infoExec = (fn) => { + if (this._lvl2num('info') < this._lvl2num(this._level)) return + fn(); + } + debugExec = (fn) => { + if (this._lvl2num('debug') < this._lvl2num(this._level)) return + fn(); + } + nitExec = (fn) => { + if (this._lvl2num('nit') < this._lvl2num(this._level)) return + fn(); } _lvl2num(lvl) { From 14a32b9c273f57a8236f2680034d06f75f395ba9 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 12:51:28 -0400 Subject: [PATCH 17/62] dbg - Add feature: allow any number of arguments when logging --- src/dbg.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/dbg.js b/src/dbg.js index e9aa3a9..3ba23a1 100644 --- a/src/dbg.js +++ b/src/dbg.js @@ -12,21 +12,25 @@ module.exports = class DBG { _levels = ['nitpick', 'debug', 'info', 'warn']; - warn = (s='') => { + /** @param {any} s **/ + warn = (s='', ...z) => { if (this._lvl2num('warn') < this._lvl2num(this._level)) return - console.log(s); + console.log(s, ...z); } - i = (s='') => { + /** @param {any} s **/ + i = (s='', ...z) => { if (this._lvl2num('info') < this._lvl2num(this._level)) return - console.log(s); + console.log(s, ...z); } - d = (s='') => { + /** @param {any} s **/ + d = (s='', ...z) => { if (this._lvl2num('debug') < this._lvl2num(this._level)) return - console.log(s); + console.log(s, ...z); } - nit = (s='') => { + /** @param {any} s **/ + nit = (s='', ...z) => { if (this._lvl2num('nitpick') < this._lvl2num(this._level)) return - console.log(s); + console.log(s, ...z); } warnGroup = (s) => { From 41d7b7ba5443209777db44c943c8f878a710f8df Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 12:52:48 -0400 Subject: [PATCH 18/62] assembler - Rename 'debugInfo' to 'sourceInfo' and return it second in output - {machinecode, sourceinfo} --- src/assembler.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index 82d4b42..f86db2c 100644 --- a/src/assembler.js +++ b/src/assembler.js @@ -165,7 +165,7 @@ function handleConstantDefinitions(op, arg, IP, constants) { * it will be assembled to the default intial value of the IP, * as specified in `machine.config.js`. * @param {string} source - Assembly source to decode - * @return {{ debugInfo: Object, machineCode: Uint8Array }}; + * @return {{ sourceInfo: Object, machineCode: Array }}; **/ function decodeInstructions(source) { dbg.nit('Pre-parsing...'); @@ -194,7 +194,7 @@ function decodeInstructions(source) { /** @type {Array} - Assembled source code, as an array of bytes **/ let machineCode = new Array(IP).fill(0); - let debugInfo = {}; + let sourceInfo = {}; // Initialize memory-mapped IO -- TODO this should probably be in the CPU, not here machineCode[CFG.pointerToDisplay] = CFG.displayAddr; @@ -335,7 +335,7 @@ function decodeInstructions(source) { machineCode[IP] = decodedOp; machineCode[IP + 1] = decodedArg; - debugInfo[IP] = { + sourceInfo[IP] = { lineNumber: line.number, source: line.source, address: IP, @@ -374,7 +374,7 @@ function decodeInstructions(source) { } } - return { 'debugInfo': debugInfo, 'machineCode': new Uint8Array(machineCode) }; + return { 'machineCode': machineCode, 'sourceInfo': sourceInfo }; } From 7faf190fe22fbd8ae059a7c62e5053ce0136baff Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 12:54:06 -0400 Subject: [PATCH 19/62] assembler / run-assembler - Make assembler get input from stdin! / Remove `run-assembler` --- src/assembler.js | 19 +++++++++++++++---- src/run-assembler.js | 33 --------------------------------- 2 files changed, 15 insertions(+), 37 deletions(-) delete mode 100755 src/run-assembler.js diff --git a/src/assembler.js b/src/assembler.js index f86db2c..d051f7b 100644 --- a/src/assembler.js +++ b/src/assembler.js @@ -405,14 +405,25 @@ function hex2num(hex) { return parseInt(hex, 16) }; const dbg = new DBG('nitpick'); // Get input -const filename = process.argv[2]; // FIXME +const filename = process.argv[2]; // FIXME - Get filename in a more robust way +const outputFilename = process.argv[3]; // FIXME - Get filename in a more robust way const inputFile_str = fs.readFileSync(filename, 'utf8'); -assemble(inputFile_str); +assemble(inputFile_str, outputFilename); /** * @param {string} assemblyCode + * @param {string} outputFile **/ -function assemble(assemblyCode) { - return decodeInstructions(assemblyCode); +function assemble(assemblyCode, outputFile='out.txt', debuggable='false') { + const out = decodeInstructions(assemblyCode); + + if (!debuggable) { + const asciiMachineCode = out.machineCode.toString().replace(/,/g, ' '); + fs.writeFileSync(outputFile, asciiMachineCode); + } else { + const debugJSON = JSON.stringify(out); + dbg.nit(debugJSON); + fs.writeFileSync(outputFile, debugJSON); + } } \ No newline at end of file diff --git a/src/run-assembler.js b/src/run-assembler.js deleted file mode 100755 index 4a87a55..0000000 --- a/src/run-assembler.js +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node - -// Run with hex output: `./run-assembler.js run assembly.asm` -// Run with binary output: `./run-assembler.js runbin assembly.asm` -// Debug: `./run-assembler.js debug assembly.asm` - -const fs = require('fs'); -const assembler = require('./assembler.js'); -const { logMemory, num2hex, num2bin } = require('./logging.js'); -const machineConfig = require('./machine.config.js'); - -const mode = process.argv[2]; -const filename = process.argv[3]; -const inputFile_str = fs.readFileSync(filename, 'utf8'); - -let assembler_output; - -if (mode === "debug") { - assembler_output = assembler.assemble(inputFile_str, true); - console.log(''); - console.group("Machine code output"); - logMemory(assembler_output.machineCode, machineConfig.INITIAL_IP_ADDRESS); - console.groupEnd(); -} else { - assembler_output = assembler.assemble(inputFile_str); - let output = ''; - if (mode === 'runbin') { // print binary output - assembler_output.machineCode.forEach((n) => output = `${output} ${num2bin(n)}`); - } else { // print hex output - assembler_output.machineCode.forEach((n) => output = `${output} ${num2hex(n)}`); - } - console.log(output); -} \ No newline at end of file From 049d9367ac4b289ac47ececd180637a5a04c80b1 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 12:54:32 -0400 Subject: [PATCH 20/62] cardiograph - Change to take input from stdin! --- src/cardiograph.js | 64 +++++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/src/cardiograph.js b/src/cardiograph.js index 4694460..931c8d2 100644 --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -1,3 +1,4 @@ +const DBG = require('./dbg.js'); const { num2hex, bool2bit } = require('./logging.js'); const CFG = require('./machine.config.js'); @@ -5,25 +6,45 @@ const CPU = require('./cpu.js'); const io = require('./io.js'); -// TODO - replace with reading STDIN: - const assembler = require('./assembler.js'); - const fs = require('fs'); - - const filename = process.argv[2]; - const inputFile_str = fs.readFileSync(filename, 'utf8'); - let assemblerOutput = assembler.assemble(inputFile_str); - +/** SETUP **/ +const dbg = new DBG('nitpick'); let cpu = new CPU(CFG.initialIP, CFG.defaultCycleLimit); -cpu.loadMemory(assemblerOutput.machineCode); -cpu.loadSourceInfo(assemblerOutput.debugInfo); +main(); -cpu.onCycleEnd(tick); -cpu.onCycleEnd(logCPUState); +async function main() { + const input = await readPipedStdin(); + dbg.nit('input: \n', input); + + const debuggable = true; // FIXME - Get this as a command line flag instead + // if debuggable === true, the input is JSON {sourceInfo, machineCode} + // otherwise, the input is a string of space-separated numbers + + let code = null; + let sourceInfo = null; + + if (!debuggable) { + code = new Uint8Array(input.split(' ')); + dbg.nit(code); + } else { + const parsedInput = JSON.parse(input); + code = new Uint8Array(parsedInput.machineCode); + sourceInfo = parsedInput.sourceInfo; + dbg.nit(code); + dbg.nit(sourceInfo); + } + + cpu.loadMemory(code); + if (debuggable) { cpu.loadSourceInfo(sourceInfo); } + + cpu.onCycleEnd(tick); + cpu.onCycleEnd(logCPUState); + + cpu.start(); + io.getKeypadInput(cpu); + cpu.step(); +} -cpu.start(); -io.getKeypadInput(cpu); -cpu.step(); async function tick() { const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) @@ -36,7 +57,10 @@ async function tick() { } function logCPUState() { - let lineInfo = cpu.dbg.sourceInfo[cpu.dbg.previousIP]; + let lineInfo = null; + if (cpu.dbg.sourceInfo) { + lineInfo = cpu.dbg.sourceInfo[cpu.dbg.previousIP]; + } console.group(`Step ${cpu.dbg.cycleCounter}`); console.log(); io.showDisplay(cpu.memory, true); // FIXME - display - allow printing hex as well as pretty-printing @@ -54,14 +78,6 @@ function logCPUState() { console.groupEnd(); }; - -/* -(async function() { - let input = await readPipedStdin(); - console.log(input); -})() -*/ - async function readPipedStdin() { // https://wellingguzman.com/notes/node-pipe-input return new Promise(function (resolve, reject) { From 45c8fe3bd27b978fd12f794bac8410db9ee3088f Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 13:04:26 -0400 Subject: [PATCH 21/62] assembler - Remove dbg log --- src/assembler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/assembler.js b/src/assembler.js index d051f7b..d476ad1 100644 --- a/src/assembler.js +++ b/src/assembler.js @@ -423,7 +423,6 @@ function assemble(assemblyCode, outputFile='out.txt', debuggable='false') { fs.writeFileSync(outputFile, asciiMachineCode); } else { const debugJSON = JSON.stringify(out); - dbg.nit(debugJSON); fs.writeFileSync(outputFile, debugJSON); } } \ No newline at end of file From be13802e1e8698f138892437ad9e2eb4f7f2019c Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 13:04:38 -0400 Subject: [PATCH 22/62] assembler / cardiograph - Add shebangs --- src/assembler.js | 2 ++ src/cardiograph.js | 2 ++ 2 files changed, 4 insertions(+) mode change 100644 => 100755 src/assembler.js mode change 100644 => 100755 src/cardiograph.js diff --git a/src/assembler.js b/src/assembler.js old mode 100644 new mode 100755 index d476ad1..2b20ffc --- a/src/assembler.js +++ b/src/assembler.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + const fs = require('fs'); const { logMemory, num2hex } = require('./logging.js'); diff --git a/src/cardiograph.js b/src/cardiograph.js old mode 100644 new mode 100755 index 931c8d2..8c51bbe --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + const DBG = require('./dbg.js'); const { num2hex, bool2bit } = require('./logging.js'); From 8a38d6f8313671bfaa0515ebdf35b83b188bab68 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 13:05:04 -0400 Subject: [PATCH 23/62] (docs) readme - Update dependency list --- readme.md | 1 - 1 file changed, 1 deletion(-) diff --git a/readme.md b/readme.md index 0dba40d..8f449eb 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,6 @@ ## Dependencies - Node.js - - readline-sync ## Run From a6e99667973cee0c0136583e4b97587757afb26a Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 13:08:48 -0400 Subject: [PATCH 24/62] (docs) Readme - Update info on how to run the assembler and simulator --- readme.md | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/readme.md b/readme.md index 8f449eb..3c02095 100644 --- a/readme.md +++ b/readme.md @@ -6,30 +6,16 @@ ## Run -### Assemble +### Assembler -Hex output: -```./run-assembler run source_code.asm``` +```./assembler.js source_code.asm [output.txt]``` -Binary output: -```./run-assembler runbin source_code.asm``` +By default, the output is written to `out.txt`. It is saved as a string of space-separated decimal numbers. -Verbose debugging output (hex): -```./run-assembler debug source_code.asm``` -### Assemble and run +### Simulator -With animated display of screen memory: -```./run-cpu run source_code.asm``` - -With verbose debugging output: -```./run-cpu debug source_code.asm``` - -With single stepping + pretty-printed display: -```./run-cpu step source_code.asm``` - -With single stepping + verbose debugging output: -```./run-cpu stepdebug source_code.asm``` +```./cardiograph.js < machine_code.txt``` ## Registers and Flags From 5671314b106e4fec52293b48d106f7206bef5e4a Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 09:26:09 -0400 Subject: [PATCH 25/62] (docs) readme - Add intro + re-arrange, to clarify relationship between Cardiograph and its constituent parts --- readme.md | 105 +++++++++++++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/readme.md b/readme.md index 3c02095..f0555c9 100644 --- a/readme.md +++ b/readme.md @@ -1,24 +1,32 @@ # Cardiograph Mark I — simulator for an imaginary computer -## Dependencies +Cardiograph is an imaginary computer. It has three main components: -- Node.js +1. the CPU, *Card* (short for 'Completely Analogue Risc Machine') +2. an input-output processor, *IO* +3. a display, *Graph* -## Run +## Simulator -### Assembler +### Run assembler ```./assembler.js source_code.asm [output.txt]``` By default, the output is written to `out.txt`. It is saved as a string of space-separated decimal numbers. -### Simulator +### Run simulator ```./cardiograph.js < machine_code.txt``` +### Dependencies -## Registers and Flags +- Node.js + + +## CPU + +### Registers and Flags - `A` - accumulator - `IP` - instruction pointer (aka program counter) @@ -31,9 +39,9 @@ By default, the output is written to `out.txt`. It is saved as a string of space - (bitwise, `0000 = ONZC`) -## Instruction set +### Instruction set -### Operations +#### Operations ``` Hex Mnem. Operand Effect @@ -60,7 +68,7 @@ Hex Mnem. Operand Effect one byte for the opcode, one for the operand -### Effects on memory, flags, registers +#### Effects on memory, flags, registers ``` op mem flags IP @@ -87,50 +95,13 @@ FTG r ONZC +2 FHP r ONZC +2/+4 ``` - -## CPU start-up +### Start-up When starting up, the CPU executes a `JMP $FF`. Put differently: it starts executing instructions at the address contained in `$FF`. -## Cardiograph memory map - -- `00-19` - display (5x5) -- `1A ` - pointer to display memory -- `1B ` - keypad: value of latest key pressed -- `1C ` - reserved for future use (bank switching flag) -- `1D ` - initial IP -- `1D-FE` - free -- `FF ` - ROM (unwriteable) pointer to initial IP (not yet implemented) - -## Peripherals - -### Keypad - -The value of the latest keypress on a hex keypad is stored at `$1B`. - -The keypad uses the same layout as the COSMAC VIP (and CHIP-8). The CPU simulator maps those keys onto a Qwerty set: - -``` -1 2 3 C 1 2 3 4 -4 5 6 D Q W E R -7 8 9 E A S D F -A 0 B F Z X C V - -hex pad simulator -``` - -The arrow keys are also mapped onto the hex keypad: - -``` - 5 ↑ - 7 8 9 ← ↓ → - -hex pad simulator -``` - -## Assembly language +### Assembly language ADD $01 ; comments follow a `;` @@ -157,4 +128,40 @@ hex pad simulator ; where the current line will be stored after assembly - Hexadecimal numbers are preceded by a `$` -- Whitespace is ignored \ No newline at end of file +- Whitespace is ignored + +## Cardiograph memory map + +- `00-19` - display (5x5) +- `1A ` - pointer to display memory +- `1B ` - keypad: value of latest key pressed +- `1C ` - reserved for future use (bank switching flag) +- `1D ` - initial IP +- `1D-FE` - free +- `FF ` - ROM (unwriteable) pointer to initial IP (not yet implemented) + +## Cardiograph peripherals + +### Keypad + +The value of the latest keypress on a hex keypad is stored at `$1B`. + +The keypad uses the same layout as the COSMAC VIP (and CHIP-8). The CPU simulator maps those keys onto a Qwerty set: + +``` +1 2 3 C 1 2 3 4 +4 5 6 D Q W E R +7 8 9 E A S D F +A 0 B F Z X C V + +hex pad simulator +``` + +The arrow keys are also mapped onto the hex keypad: + +``` + 5 ↑ + 7 8 9 ← ↓ → + +hex pad simulator +``` \ No newline at end of file From 12d592c262a75f8a7b5de29fc88f66fbbe767b05 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 19:32:43 -0400 Subject: [PATCH 26/62] assembler - WIP - Add a 'return on stdout' mode that turns off all debug logging, and prints machine code output to stdout, so that output can be piped directly to the simulator --- src/assembler.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index 2b20ffc..ae2d503 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -404,7 +404,9 @@ function hex2num(hex) { return parseInt(hex, 16) }; /** MAIN **/ // Initialize debugger -const dbg = new DBG('nitpick'); +let dbg = new DBG('nitpick'); +let returnOnStdout = true; // FIXME - set this using a CLI arg +if (returnOnStdout) { dbg = new DBG('none'); } // Get input const filename = process.argv[2]; // FIXME - Get filename in a more robust way @@ -414,13 +416,24 @@ assemble(inputFile_str, outputFilename); /** - * @param {string} assemblyCode - * @param {string} outputFile + * Assemble source code into machine code. + * If 'includeMetadata' is true, a JSON object containing + * both machine code and metadata is written to the output file. + * Otherwise, a string of decimal numbers is written. + * @arg {string} sourceCode - Source code to assemble + * @arg {string} [outputFile='out.txt'] - Output file for machine code (and optional metadata) + * @arg {boolean} [includeMetadata=false] - Include metadata for use when debugging using the simulator? **/ -function assemble(assemblyCode, outputFile='out.txt', debuggable='false') { - const out = decodeInstructions(assemblyCode); +function assemble(sourceCode, outputFile='out.txt', includeMetadata=false) { + const out = decodeInstructions(sourceCode); - if (!debuggable) { + if (returnOnStdout) { + const debugJSON = JSON.stringify(out); + console.log(debugJSON); + return; + } + + if (!includeMetadata) { const asciiMachineCode = out.machineCode.toString().replace(/,/g, ' '); fs.writeFileSync(outputFile, asciiMachineCode); } else { From 19784b56b6ed8421f0795c07d40798bdff41baa8 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 19:34:22 -0400 Subject: [PATCH 27/62] dbg - Add 'none' debugging level, which turns off all output --- src/dbg.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dbg.js b/src/dbg.js index 3ba23a1..55f73a1 100644 --- a/src/dbg.js +++ b/src/dbg.js @@ -1,6 +1,6 @@ module.exports = class DBG { /** - * @param ${'warn'|'info'|'debug'|'nitpick'} [level='info'] + * @param ${'none'|'warn'|'info'|'debug'|'nitpick'} [level='info'] **/ constructor(level = 'info') { if (this._levels.includes(level)) { @@ -10,7 +10,7 @@ module.exports = class DBG { } } - _levels = ['nitpick', 'debug', 'info', 'warn']; + _levels = ['nitpick', 'debug', 'info', 'warn', 'none']; /** @param {any} s **/ warn = (s='', ...z) => { From e9d721042b3d1cdfbf8988d49dfa9da82183c963 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 20:29:15 -0400 Subject: [PATCH 28/62] cardiograph - Remove old debug logging --- src/cardiograph.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/cardiograph.js b/src/cardiograph.js index 8c51bbe..49cf372 100755 --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -27,13 +27,10 @@ async function main() { if (!debuggable) { code = new Uint8Array(input.split(' ')); - dbg.nit(code); } else { const parsedInput = JSON.parse(input); code = new Uint8Array(parsedInput.machineCode); sourceInfo = parsedInput.sourceInfo; - dbg.nit(code); - dbg.nit(sourceInfo); } cpu.loadMemory(code); From 8bb3b9b43fffa74f1c9c1f7635704a9b9c55070f Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 20:30:44 -0400 Subject: [PATCH 29/62] conversions - Create `conversions.js` and move `hex2num` et al. out of `logging.js` --- src/assembler.js | 3 ++- src/cardiograph.js | 3 +-- src/cpu.js | 2 +- src/logging.js | 49 ++-------------------------------------------- 4 files changed, 6 insertions(+), 51 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index ae2d503..1ec4981 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -2,7 +2,8 @@ const fs = require('fs'); -const { logMemory, num2hex } = require('./logging.js'); +const { logMemory } = require('./logging.js'); +const { num2hex } = require('./conversions.js'); const DBG = require('./dbg.js'); const CFG = require('./machine.config.js'); diff --git a/src/cardiograph.js b/src/cardiograph.js index 49cf372..e25c921 100755 --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -1,7 +1,7 @@ #!/usr/bin/env node const DBG = require('./dbg.js'); -const { num2hex, bool2bit } = require('./logging.js'); +const { num2hex, bool2bit } = require('./conversions.js'); const CFG = require('./machine.config.js'); const CPU = require('./cpu.js'); @@ -16,7 +16,6 @@ main(); async function main() { const input = await readPipedStdin(); - dbg.nit('input: \n', input); const debuggable = true; // FIXME - Get this as a command line flag instead // if debuggable === true, the input is JSON {sourceInfo, machineCode} diff --git a/src/cpu.js b/src/cpu.js index c66fa69..9e40913 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -1,4 +1,4 @@ -const { num2hex } = require('./logging.js'); +const { num2hex } = require('./conversions.js'); module.exports = class CPU { diff --git a/src/logging.js b/src/logging.js index a8ca701..7b5fbf5 100644 --- a/src/logging.js +++ b/src/logging.js @@ -1,3 +1,5 @@ +let { num2hex } = require('./conversions.js'); + /** * Display a table of memory locations. * Call with [start] and [end] indices to display a range. @@ -38,54 +40,7 @@ const logRunningHeader = () => { console.log( `└─────────────────────┘`); } -/** - * @param {number} num - * @returns {string} - */ -const num2hex = (num) => num.toString(16).toUpperCase().padStart(2, "0"); - -/** - * @param {string} hex - * @returns {number} - */ -const hex2num = (hex) => parseInt(hex, 16); - -/** - * Convert a number to binary, padded to 8 bits - * See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript - * @param {number} num - * @returns {string} binary representation of the input - **/ -const num2bin = (num) => (num >>> 0).toString(2).padStart(8, "0"); - -/** - * Convert a number to binary, padded to 4 bits - * See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript - * @param {number} num - * @returns {string} binary representation of the input - **/ -const num2bin_4bit = (num) => (num >>> 0).toString(2).padStart(4, "0"); - -/** - * @param {string} bin - * @returns {number} - */ -const bin2num = (bin) => parseInt(bin, 2) - -/** - * @param {Boolean} bool - * @returns {0|1} - **/ -const bool2bit = (bool) => bool ? 1 : 0; - - module.exports = { "logMemory": logMemory, "logRunningHeader": logRunningHeader, - "num2hex": num2hex, - "hex2num": hex2num, - "num2bin": num2bin, - "num2bin_4bit": num2bin_4bit, - "bin2num": bin2num, - "bool2bit": bool2bit, } \ No newline at end of file From 1fe582663b7d21c9a2714a5f8114dfdb00eddb51 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 20:30:53 -0400 Subject: [PATCH 30/62] conversions - Create `conversions.js` and move `hex2num` et al. out of `logging.js` --- src/conversions.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/conversions.js diff --git a/src/conversions.js b/src/conversions.js new file mode 100644 index 0000000..f6e9f88 --- /dev/null +++ b/src/conversions.js @@ -0,0 +1,48 @@ +/** + * @param {number} num + * @returns {string} + */ +const num2hex = (num) => num.toString(16).toUpperCase().padStart(2, "0"); + +/** + * @param {string} hex + * @returns {number} + */ +const hex2num = (hex) => parseInt(hex, 16); + +/** + * Convert a number to binary, padded to 8 bits + * See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript + * @param {number} num + * @returns {string} binary representation of the input + **/ +const num2bin = (num) => (num >>> 0).toString(2).padStart(8, "0"); + +/** + * Convert a number to binary, padded to 4 bits + * See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript + * @param {number} num + * @returns {string} binary representation of the input + **/ +const num2bin_4bit = (num) => (num >>> 0).toString(2).padStart(4, "0"); + +/** + * @param {string} bin + * @returns {number} + */ +const bin2num = (bin) => parseInt(bin, 2) + +/** + * @param {Boolean} bool + * @returns {0|1} + **/ +const bool2bit = (bool) => bool ? 1 : 0; + +module.exports = { + "num2hex": num2hex, + "hex2num": hex2num, + "num2bin": num2bin, + "num2bin_4bit": num2bin_4bit, + "bin2num": bin2num, + "bool2bit": bool2bit, +} \ No newline at end of file From eff904366530a4f4f8be5145cb10fcecd0e9cfbd Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 21:25:59 -0400 Subject: [PATCH 31/62] cpu - Fix: Include '$' before hex in debug-peeking for indirect ops --- src/cpu.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cpu.js b/src/cpu.js index 9e40913..b3a210d 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -129,7 +129,7 @@ module.exports = class CPU { }, store_addr: (addr) => { - this.dbg.currentMnemonic = `STO addr; @addr: ${num2hex(this.memory[addr])}`; + this.dbg.currentMnemonic = `STO addr; @addr: $${num2hex(this.memory[addr])}`; this.memory[this.memory[addr]] = this.acc; this._incrementIP(2); }, @@ -143,7 +143,7 @@ module.exports = class CPU { }, load_addr: (addr) => { - this.dbg.currentMnemonic = `LDA addr; @ addr: ${num2hex(this.memory[addr])}`; + this.dbg.currentMnemonic = `LDA addr; @ addr: $${num2hex(this.memory[addr])}`; this.acc = this.memory[addr]; this._updateFlagNegative(); this._updateFlagZero(); @@ -263,7 +263,7 @@ module.exports = class CPU { }, hop_lit: (lit) => { - this.dbg.currentMnemonic = `HOP lit; IP+2: ${this.memory[this.IP+2]}, IP+3: ${this.memory[this.IP+3]}`; + 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 { From 866f553346eb6d6ea0b52f1c7216702c4467f2c8 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 21:26:36 -0400 Subject: [PATCH 32/62] assembler - Add feature: Allow binary numbers with prefix '0b' and hex numbers with prefix '0x' --- src/assembler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index 1ec4981..484546d 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -3,7 +3,7 @@ const fs = require('fs'); const { logMemory } = require('./logging.js'); -const { num2hex } = require('./conversions.js'); +const { num2hex, hex2num, bin2num } = require('./conversions.js'); const DBG = require('./dbg.js'); const CFG = require('./machine.config.js'); @@ -111,6 +111,8 @@ function preparseSourceCode(source) { **/ function decodeNumericOp(arg) { if (arg.startsWith("$")) return hex2num(arg.replace("$", "")); + if (arg.startsWith("0x")) return hex2num(arg.replace("0x", "")); + if (arg.startsWith("0b")) return bin2num(arg.replace("0b", "")); return parseInt(arg); } @@ -399,8 +401,6 @@ function stripWhitespaceFromEnds(line) { return line; } -function hex2num(hex) { return parseInt(hex, 16) }; - /** MAIN **/ From 7b143ab000fdf70e5b93cada880bd70ab552fd3c Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 21:28:01 -0400 Subject: [PATCH 33/62] (docs) readme - Update info on numeric types in assembly --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index f0555c9..902a21d 100644 --- a/readme.md +++ b/readme.md @@ -127,7 +127,8 @@ Put differently: it starts executing instructions at the address contained in `$ LDA * ; `*` is a special label referencing the memory address ; where the current line will be stored after assembly -- Hexadecimal numbers are preceded by a `$` +- Prefix hexadecimal numbers with `$` (or `0x`) +- Prefix binary numbers with `0b` - Whitespace is ignored ## Cardiograph memory map From c84c86c160a6173b5a745f9d8410c4922a66b311 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 21:28:39 -0400 Subject: [PATCH 34/62] cpu - Fix overflow flag! --- src/cpu.js | 136 ++++++++++++------------------ test-programs/flag-overflow-2.asm | 63 ++++++++++++++ 2 files changed, 115 insertions(+), 84 deletions(-) create mode 100644 test-programs/flag-overflow-2.asm diff --git a/src/cpu.js b/src/cpu.js index b3a210d..16588fb 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -152,55 +152,21 @@ module.exports = class CPU { add_lit: (lit) => { this.dbg.currentMnemonic = 'ADD lit'; - // Calculate sum - let sum = this.acc + lit; - if (sum > 255) { - this.flags.C = true; - sum = (sum % 255) - 1; - } else { - this.flags.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((this.acc & 64) && (lit & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (this.flags & 8); - // FIXME - re-implement overflow - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - this.flags.O = true; - } else { - this.flags.O = false; - } + 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'; - // Calculate sum - let sum = this.acc + this.memory[addr]; - if (sum > 255) { - this.flags.C = true; - sum = (sum % 255) - 1; - } else { - this.flags.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((this.acc & 64) && (addr & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (this.flags & 8); - // FIXME - re-implement overflow - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - this.flags.O = true; - } else { - this.flags.O = false; - } + 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); @@ -208,55 +174,21 @@ module.exports = class CPU { sub_lit: (lit) => { this.dbg.currentMnemonic = 'SUB lit'; - // Calculate sum - let sum = this.acc - lit; - if (sum < 0) { - this.flags.C = true; - sum = sum + 256; - } else { - this.flags.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((this.acc & 64) && (lit & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (this.flags & 8); - // FIXME - re-implement overflow - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - this.flags.O = true; - } else { - this.flags.O = false; - } - this.acc = sum; + 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'; - // Calculate sum - let sum = this.acc - this.memory[addr]; - if (sum < 0) { - this.flags.C = true; - sum = sum + 256; - } else { - this.flags.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((this.acc & 64) && (addr & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (this.flags & 8); - // FIXME - re-implement overflow - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - this.flags.O = true; - } else { - this.flags.O = false; - } - this.acc = sum; + 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); @@ -350,3 +282,39 @@ module.exports = class CPU { 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); +} \ No newline at end of file diff --git a/test-programs/flag-overflow-2.asm b/test-programs/flag-overflow-2.asm new file mode 100644 index 0000000..80a8605 --- /dev/null +++ b/test-programs/flag-overflow-2.asm @@ -0,0 +1,63 @@ +;; Test behaviour of flags during addition and subtraction +;; with a focus on the Overflow flag +2023-08-29 + +; http://teaching.idallen.com/dat2343/11w/notes/040_overflow.txt: +; +; > 1. If the sum of two numbers with the sign bits off yields a result number + ; with the sign bit on, the "overflow" flag is turned on. +; > +; > 0100 + 0100 = 1000 (overflow flag is turned on) +; > +; > 2. If the sum of two numbers with the sign bits on yields a result number +; > with the sign bit off, the "overflow" flag is turned on. +; > +; > 1000 + 1000 = 0000 (overflow flag is turned on) +; > +; > Otherwise, the overflow flag is turned off. +; > * 0100 + 0001 = 0101 (overflow flag is turned off) +; > * 0110 + 1001 = 1111 (overflow flag is turned off) +; > * 1000 + 0001 = 1001 (overflow flag is turned off) +; > * 1100 + 1100 = 1000 (overflow flag is turned off) + +;; Check simple addition and subtraction +; LDA 1 +; STO 0 +; LDA 1 +; ADD 1 +; LDA 1 +; ADD (0) +; LDA 3 +; SUB 1 +; LDA 3 +; SUB (0) + +;; Check zero flag, negative flag +; LDA 0 +; LDA 255 + +;; Check overflow flag + +LDA 0b01000000 +ADD 0b01000000 ; 10000000 ; Overflow flag is on + +LDA 0b10000000 +ADD 0b10000000 ; 00000000 ; Overflow flag is on + + +; > * 0100 + 0001 = 0101 (overflow flag is turned off) +; > * 0110 + 1001 = 1111 (overflow flag is turned off) +; > * 1000 + 0001 = 1001 (overflow flag is turned off) +; > * 1100 + 1100 = 1000 (overflow flag is turned off) + +LDA 0b01000000 +ADD 0b00010000 ; 01010000 ; overflow off + +LDA 0b01100000 +ADD 0b10010000 ; 11110000 ; overflow off + +LDA 0b10000000 +ADD 0b00010000 ; 10010000 ; overflow off + +LDA 0b11000000 +ADD 0b11000000 ; 10000000 ; overflow off \ No newline at end of file From f9901d304b45628dceea62adbae04e3ee0f4da85 Mon Sep 17 00:00:00 2001 From: n loewen Date: Tue, 29 Aug 2023 22:32:36 -0400 Subject: [PATCH 35/62] ?? submodule diff - idk what this is about --- src/argparser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/argparser b/src/argparser index 584d9dd..216cdca 160000 --- a/src/argparser +++ b/src/argparser @@ -1 +1 @@ -Subproject commit 584d9dd95f4b1b3c69065826cf96b3cda0cf9e16 +Subproject commit 216cdcae53e86d6908aa1b4d86549b1ca5e7bb53 From 16f85d3b9fd4d5dfd6b166cdd63aeeec205d21df Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 2 Sep 2023 13:39:47 -0400 Subject: [PATCH 36/62] (tests) flag-overflow-2 - Fix: comment out date --- test-programs/flag-overflow-2.asm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-programs/flag-overflow-2.asm b/test-programs/flag-overflow-2.asm index 80a8605..741a647 100644 --- a/test-programs/flag-overflow-2.asm +++ b/test-programs/flag-overflow-2.asm @@ -1,6 +1,6 @@ ;; Test behaviour of flags during addition and subtraction ;; with a focus on the Overflow flag -2023-08-29 +;; 2023-08-29 ; http://teaching.idallen.com/dat2343/11w/notes/040_overflow.txt: ; From b3d10a4197178f6041678a0227c0bc0182810295 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 2 Sep 2023 13:40:56 -0400 Subject: [PATCH 37/62] assembler - Change to use new argument parsing library --- src/argparser | 2 +- src/assembler.js | 49 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/argparser b/src/argparser index 216cdca..86e16a9 160000 --- a/src/argparser +++ b/src/argparser @@ -1 +1 @@ -Subproject commit 216cdcae53e86d6908aa1b4d86549b1ca5e7bb53 +Subproject commit 86e16a9855901b96d09d613f431fd7ee4690c6af diff --git a/src/assembler.js b/src/assembler.js index 484546d..709740e 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -2,6 +2,7 @@ const fs = require('fs'); +const argparser = require('./argparser/argparser.js'); const { logMemory } = require('./logging.js'); const { num2hex, hex2num, bin2num } = require('./conversions.js'); const DBG = require('./dbg.js'); @@ -406,14 +407,39 @@ function stripWhitespaceFromEnds(line) { // Initialize debugger let dbg = new DBG('nitpick'); -let returnOnStdout = true; // FIXME - set this using a CLI arg -if (returnOnStdout) { dbg = new DBG('none'); } -// Get input -const filename = process.argv[2]; // FIXME - Get filename in a more robust way -const outputFilename = process.argv[3]; // FIXME - Get filename in a more robust way -const inputFile_str = fs.readFileSync(filename, 'utf8'); -assemble(inputFile_str, outputFilename); +// Handle command-line options... +const args = argparser(process.argv); +dbg.d('args', args); + +if (typeof args.i === 'undefined' || args.i.length === 0) { + console.error('Input file required (-i prog.asm)'); + process.exit(); +} +const inputFilename = args.i[0]; +const inputFile_str = fs.readFileSync(inputFilename, 'utf8'); + +if ('o' in args && args.o.length === 0) { + console.error('Missing output file name (-o prog.asm)'); + process.exit(); +} + +let outputFilename = null; +let returnOnStdout = 'o' in args ? false : true; +if (!returnOnStdout) { + outputFilename = args.o[0]; +} else { + dbg = new DBG('none'); +} + +let outputWithMetadata = false; +if ('d' in args || 'debug' in args) { + outputWithMetadata = true; +} + +// TODO: maybe check for too many args? + +assemble(inputFile_str, outputFilename, outputWithMetadata); /** @@ -422,8 +448,8 @@ assemble(inputFile_str, outputFilename); * both machine code and metadata is written to the output file. * Otherwise, a string of decimal numbers is written. * @arg {string} sourceCode - Source code to assemble - * @arg {string} [outputFile='out.txt'] - Output file for machine code (and optional metadata) - * @arg {boolean} [includeMetadata=false] - Include metadata for use when debugging using the simulator? + * @arg {string} [outputFile] - Output file for machine code (and optional metadata) + * @arg {boolean} [includeMetadata=false] - Include metadata when writing output to a file? (for use when debugging using the simulator) **/ function assemble(sourceCode, outputFile='out.txt', includeMetadata=false) { const out = decodeInstructions(sourceCode); @@ -431,10 +457,7 @@ function assemble(sourceCode, outputFile='out.txt', includeMetadata=false) { if (returnOnStdout) { const debugJSON = JSON.stringify(out); console.log(debugJSON); - return; - } - - if (!includeMetadata) { + } else if (!includeMetadata) { const asciiMachineCode = out.machineCode.toString().replace(/,/g, ' '); fs.writeFileSync(outputFile, asciiMachineCode); } else { From 2848588fc20330e7790638adf781d50838510b66 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 2 Sep 2023 13:43:42 -0400 Subject: [PATCH 38/62] (infra) - gitignore - Ignore files with '.tmp.' in their name --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dde1915..b1203f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store .vscode +*.tmp.* node_modules cardiograph.code-workspace \ No newline at end of file From 9c1cf4abba3ae3320f9cd36802e7816134a92324 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 2 Sep 2023 15:40:16 -0400 Subject: [PATCH 39/62] opter - Rename 'argparser' to 'opter' --- .gitmodules | 2 +- src/argparser | 1 - src/opter | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 160000 src/argparser create mode 160000 src/opter diff --git a/.gitmodules b/.gitmodules index 554e1cb..9395a8d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "src/argparser"] - path = src/argparser + path = src/opter url = https://git.nloewen.com/n/argv-parser.git diff --git a/src/argparser b/src/argparser deleted file mode 160000 index 86e16a9..0000000 --- a/src/argparser +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 86e16a9855901b96d09d613f431fd7ee4690c6af diff --git a/src/opter b/src/opter new file mode 160000 index 0000000..c2ea099 --- /dev/null +++ b/src/opter @@ -0,0 +1 @@ +Subproject commit c2ea099bbfddada734b5fc6e4f1125658c8ba578 From e25723fcc960d99ed31108a93d8f380f0aaa608f Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 2 Sep 2023 16:42:11 -0400 Subject: [PATCH 40/62] assembler - Change to make use of improvements in Opter --- src/assembler.js | 42 ++++++++++++++---------------------------- src/opter | 2 +- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index 709740e..6262a59 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -2,7 +2,7 @@ const fs = require('fs'); -const argparser = require('./argparser/argparser.js'); +const Opter = require('./opter/opter.js'); const { logMemory } = require('./logging.js'); const { num2hex, hex2num, bin2num } = require('./conversions.js'); const DBG = require('./dbg.js'); @@ -405,40 +405,26 @@ function stripWhitespaceFromEnds(line) { /** MAIN **/ -// Initialize debugger -let dbg = new DBG('nitpick'); - // Handle command-line options... -const args = argparser(process.argv); -dbg.d('args', args); +const opts = new Opter(process.argv); +opts.synonymize('-d', '--debug'); +opts.requireOption('-i', 'Input file required (-i prog.asm)'); +opts.requireOptionArgument('-i', 1, 1, 'Input file required (-i prog.asm)'); +opts.requireOptionArgument('-o', 1, 1, 'Missing output file name (-o prog.asm)'); -if (typeof args.i === 'undefined' || args.i.length === 0) { - console.error('Input file required (-i prog.asm)'); - process.exit(); -} -const inputFilename = args.i[0]; +const inputFilename = opts.opts.i[0]; const inputFile_str = fs.readFileSync(inputFilename, 'utf8'); -if ('o' in args && args.o.length === 0) { - console.error('Missing output file name (-o prog.asm)'); - process.exit(); -} +let outputFileProvided = opts.contains('-o'); +let returnOnStdout = !outputFileProvided; +let outputFilename = outputFileProvided ? opts.opts.o[0] : null; -let outputFilename = null; -let returnOnStdout = 'o' in args ? false : true; -if (!returnOnStdout) { - outputFilename = args.o[0]; -} else { - dbg = new DBG('none'); -} +let outputWithMetadata = opts.contains('--debug'); -let outputWithMetadata = false; -if ('d' in args || 'debug' in args) { - outputWithMetadata = true; -} - -// TODO: maybe check for too many args? +// Initialize debugger... +const dbg = returnOnStdout ? new DBG('none') : new DBG('nitpick'); +// Assemble...! assemble(inputFile_str, outputFilename, outputWithMetadata); diff --git a/src/opter b/src/opter index c2ea099..4dec08a 160000 --- a/src/opter +++ b/src/opter @@ -1 +1 @@ -Subproject commit c2ea099bbfddada734b5fc6e4f1125658c8ba578 +Subproject commit 4dec08afbc0c96e454dd532ed9cfa16a4a1ddd51 From c8c188e55aed5165864e0e02306a87aea37c99bf Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 2 Sep 2023 16:45:38 -0400 Subject: [PATCH 41/62] assembler - Move 'main' up top --- src/assembler.js | 57 ++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index 6262a59..5df973b 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -9,11 +9,37 @@ const DBG = require('./dbg.js'); const CFG = require('./machine.config.js'); -// Configure pseudo-ops: +/** MAIN **/ + +// Handle command-line options... +const opts = new Opter(process.argv); +opts.synonymize('-d', '--debug'); +opts.requireOption('-i', 'Input file required (-i prog.asm)'); +opts.requireOptionArgument('-i', 1, 1, 'Input file required (-i prog.asm)'); +opts.requireOptionArgument('-o', 1, 1, 'Missing output file name (-o prog.asm)'); + +const inputFilename = opts.opts.i[0]; +const inputFile_str = fs.readFileSync(inputFilename, 'utf8'); + +let outputFileProvided = opts.contains('-o'); +let returnOnStdout = !outputFileProvided; +let outputFilename = outputFileProvided ? opts.opts.o[0] : null; + +let outputWithMetadata = opts.contains('--debug'); + +// Initialize debugger... +const dbg = returnOnStdout ? new DBG('none') : new DBG('nitpick'); + +// Assemble...! +assemble(inputFile_str, outputFilename, outputWithMetadata); + + +/** Configure pseudo-ops **/ const ASM_IP_LABEL = '*'; const ASM_CONSTANT_PREFIX = '#'; const ASM_LABEL_PREFIX = '@'; +/** Configure mnemonics **/ const mnemonicsWithOptionalArgs = ['end', 'nop']; const mnemonics2opcodes = { end: { direct: 0, indirect: 0 }, @@ -402,32 +428,6 @@ function stripWhitespaceFromEnds(line) { return line; } - -/** MAIN **/ - -// Handle command-line options... -const opts = new Opter(process.argv); -opts.synonymize('-d', '--debug'); -opts.requireOption('-i', 'Input file required (-i prog.asm)'); -opts.requireOptionArgument('-i', 1, 1, 'Input file required (-i prog.asm)'); -opts.requireOptionArgument('-o', 1, 1, 'Missing output file name (-o prog.asm)'); - -const inputFilename = opts.opts.i[0]; -const inputFile_str = fs.readFileSync(inputFilename, 'utf8'); - -let outputFileProvided = opts.contains('-o'); -let returnOnStdout = !outputFileProvided; -let outputFilename = outputFileProvided ? opts.opts.o[0] : null; - -let outputWithMetadata = opts.contains('--debug'); - -// Initialize debugger... -const dbg = returnOnStdout ? new DBG('none') : new DBG('nitpick'); - -// Assemble...! -assemble(inputFile_str, outputFilename, outputWithMetadata); - - /** * Assemble source code into machine code. * If 'includeMetadata' is true, a JSON object containing @@ -450,4 +450,5 @@ function assemble(sourceCode, outputFile='out.txt', includeMetadata=false) { const debugJSON = JSON.stringify(out); fs.writeFileSync(outputFile, debugJSON); } -} \ No newline at end of file +} + From d26bf39f054434fbcc0e3a894d9e828b069e8831 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 2 Sep 2023 20:34:54 -0700 Subject: [PATCH 42/62] assembler - Fix: move 'main' back to end --- src/assembler.js | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index 5df973b..68b54aa 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -9,30 +9,6 @@ const DBG = require('./dbg.js'); const CFG = require('./machine.config.js'); -/** MAIN **/ - -// Handle command-line options... -const opts = new Opter(process.argv); -opts.synonymize('-d', '--debug'); -opts.requireOption('-i', 'Input file required (-i prog.asm)'); -opts.requireOptionArgument('-i', 1, 1, 'Input file required (-i prog.asm)'); -opts.requireOptionArgument('-o', 1, 1, 'Missing output file name (-o prog.asm)'); - -const inputFilename = opts.opts.i[0]; -const inputFile_str = fs.readFileSync(inputFilename, 'utf8'); - -let outputFileProvided = opts.contains('-o'); -let returnOnStdout = !outputFileProvided; -let outputFilename = outputFileProvided ? opts.opts.o[0] : null; - -let outputWithMetadata = opts.contains('--debug'); - -// Initialize debugger... -const dbg = returnOnStdout ? new DBG('none') : new DBG('nitpick'); - -// Assemble...! -assemble(inputFile_str, outputFilename, outputWithMetadata); - /** Configure pseudo-ops **/ const ASM_IP_LABEL = '*'; @@ -452,3 +428,26 @@ function assemble(sourceCode, outputFile='out.txt', includeMetadata=false) { } } +/** MAIN **/ + +// Handle command-line options... +const opts = new Opter(process.argv); +opts.synonymize('-d', '--debug'); +opts.requireOption('-i', 'Input file required (-i prog.asm)'); +opts.requireOptionArgument('-i', 1, 1, 'Input file required (-i prog.asm)'); +opts.requireOptionArgument('-o', 1, 1, 'Missing output file name (-o prog.asm)'); + +const inputFilename = opts.opts.i[0]; +const inputFile_str = fs.readFileSync(inputFilename, 'utf8'); + +let outputFileProvided = opts.contains('-o'); +let returnOnStdout = !outputFileProvided; +let outputFilename = outputFileProvided ? opts.opts.o[0] : null; + +let outputWithMetadata = opts.contains('--debug'); + +// Initialize debugger... +const dbg = returnOnStdout ? new DBG('none') : new DBG('nitpick'); + +// Assemble...! +assemble(inputFile_str, outputFilename, outputWithMetadata); From 101421c4c32653acea5517e7e45d7d594aea8c86 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 2 Sep 2023 20:36:59 -0700 Subject: [PATCH 43/62] dbg - Add 'set level' function --- src/dbg.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/dbg.js b/src/dbg.js index 55f73a1..cb585dd 100644 --- a/src/dbg.js +++ b/src/dbg.js @@ -3,6 +3,12 @@ module.exports = class DBG { * @param ${'none'|'warn'|'info'|'debug'|'nitpick'} [level='info'] **/ constructor(level = 'info') { + this.setLevel(level); + } + + _levels = ['nitpick', 'debug', 'info', 'warn', 'none']; + + setLevel(level) { if (this._levels.includes(level)) { this._level = level; } else { @@ -10,8 +16,6 @@ module.exports = class DBG { } } - _levels = ['nitpick', 'debug', 'info', 'warn', 'none']; - /** @param {any} s **/ warn = (s='', ...z) => { if (this._lvl2num('warn') < this._lvl2num(this._level)) return From 77a41d47c3265c96042e19b65611a8340c130374 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 2 Sep 2023 20:55:45 -0700 Subject: [PATCH 44/62] assembler - Change to a tidier approach, made possible by new Opter library --- src/assembler.js | 55 +++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index 68b54aa..ba9bf01 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -175,6 +175,7 @@ function handleConstantDefinitions(op, arg, IP, constants) { * @param {string} source - Assembly source to decode * @return {{ sourceInfo: Object, machineCode: Array }}; **/ +// TODO rename? function decodeInstructions(source) { dbg.nit('Pre-parsing...'); let lines = preparseSourceCode(source); @@ -212,8 +213,8 @@ function decodeInstructions(source) { let labels = {}; let constants = {}; - // Decode line by line... + for (let i = 0; i < lines.length; i++) { let line = lines[i]; // dbg(2, `line info:`); @@ -239,7 +240,6 @@ function decodeInstructions(source) { } } - // *** Decode special operations *** // Opcodes - Handle label definitions @@ -350,7 +350,6 @@ function decodeInstructions(source) { machine: [decodedOp, decodedArg] }; - dbg.i(); dbg.i(`Line ${line.number}: ${line.source}`); if (line.argument) { @@ -409,27 +408,37 @@ function stripWhitespaceFromEnds(line) { * If 'includeMetadata' is true, a JSON object containing * both machine code and metadata is written to the output file. * Otherwise, a string of decimal numbers is written. - * @arg {string} sourceCode - Source code to assemble - * @arg {string} [outputFile] - Output file for machine code (and optional metadata) - * @arg {boolean} [includeMetadata=false] - Include metadata when writing output to a file? (for use when debugging using the simulator) + * @arg {string} inputFilename File containing code to assemble + * @arg {boolean} outputToFile If false, output is on stdout + * @arg {boolean} includeMetadata Include metadata when writing output to a file? (for use when debugging using the simulator) + * @arg {string} [outputFilename] Output file for machine code (and optional metadata) **/ -function assemble(sourceCode, outputFile='out.txt', includeMetadata=false) { +function assemble(inputFilename, outputToFile, includeMetadata, outputFilename=null) { + const sourceCode = fs.readFileSync(inputFilename, 'utf8'); const out = decodeInstructions(sourceCode); - if (returnOnStdout) { + if (includeMetadata) { const debugJSON = JSON.stringify(out); - console.log(debugJSON); - } else if (!includeMetadata) { - const asciiMachineCode = out.machineCode.toString().replace(/,/g, ' '); - fs.writeFileSync(outputFile, asciiMachineCode); + if (outputToFile) { + fs.writeFileSync(outputFilename, debugJSON); + } else { + console.log(debugJSON); + } } else { - const debugJSON = JSON.stringify(out); - fs.writeFileSync(outputFile, debugJSON); + const asciiMachineCode = out.machineCode.toString().replace(/,/g, ' '); + if (outputToFile) { + fs.writeFileSync(outputFilename, asciiMachineCode); + } else { + console.log(asciiMachineCode); + } } } /** MAIN **/ +// Initialize debugger... +const dbg = new DBG('nitpick'); + // Handle command-line options... const opts = new Opter(process.argv); opts.synonymize('-d', '--debug'); @@ -438,16 +447,14 @@ opts.requireOptionArgument('-i', 1, 1, 'Input file required (-i prog.asm)'); opts.requireOptionArgument('-o', 1, 1, 'Missing output file name (-o prog.asm)'); const inputFilename = opts.opts.i[0]; -const inputFile_str = fs.readFileSync(inputFilename, 'utf8'); - -let outputFileProvided = opts.contains('-o'); -let returnOnStdout = !outputFileProvided; -let outputFilename = outputFileProvided ? opts.opts.o[0] : null; - +const outputToFile = opts.contains('-o'); let outputWithMetadata = opts.contains('--debug'); -// Initialize debugger... -const dbg = returnOnStdout ? new DBG('none') : new DBG('nitpick'); - // Assemble...! -assemble(inputFile_str, outputFilename, outputWithMetadata); +if (outputToFile) { + const outputFilename = opts.opts.o[0]; + assemble(inputFilename, outputToFile, outputWithMetadata, outputFilename); +} else { + dbg.setLevel('none'); + assemble(inputFilename, outputToFile, outputWithMetadata); +} From b0996d30c33c333409a0a22391a4681ad38f5794 Mon Sep 17 00:00:00 2001 From: n loewen Date: Mon, 4 Sep 2023 18:04:35 -0700 Subject: [PATCH 45/62] opter - Update to latest version --- src/opter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opter b/src/opter index 4dec08a..b6005e1 160000 --- a/src/opter +++ b/src/opter @@ -1 +1 @@ -Subproject commit 4dec08afbc0c96e454dd532ed9cfa16a4a1ddd51 +Subproject commit b6005e11bb8867f77ac7ba4d26ab585127a5048a From 93f88560a256dd6e5093244c12f47e72fa57b08e Mon Sep 17 00:00:00 2001 From: n loewen Date: Sun, 10 Sep 2023 21:28:01 -0700 Subject: [PATCH 46/62] (notes) - Remove notes from this branch, since they moved to their own --- notes/2023-08-05--dev-notes.md | 31 ------ notes/2023-08-07--dev-notes.md | 82 ---------------- notes/2023-08-09--dev-notes.md | 81 --------------- notes/2023-08-10--dev-notes.md | 32 ------ notes/2023-08-12--dev-notes.md | 60 ------------ notes/2023-08-15--dev-notes.md | 52 ---------- notes/2023-08-16--dev-notes.md | 24 ----- notes/2023-08-17--dev-notes.md | 16 --- notes/2023-08-21--dev-notes.md | 77 --------------- notes/2023-08-23--dev-notes.md | 174 --------------------------------- notes/2023-08-24--dev-notes.md | 81 --------------- notes/2023-08-28--dev-notes.md | 26 ----- notes/bibliography.md | 107 -------------------- 13 files changed, 843 deletions(-) delete mode 100644 notes/2023-08-05--dev-notes.md delete mode 100644 notes/2023-08-07--dev-notes.md delete mode 100644 notes/2023-08-09--dev-notes.md delete mode 100644 notes/2023-08-10--dev-notes.md delete mode 100644 notes/2023-08-12--dev-notes.md delete mode 100644 notes/2023-08-15--dev-notes.md delete mode 100644 notes/2023-08-16--dev-notes.md delete mode 100644 notes/2023-08-17--dev-notes.md delete mode 100644 notes/2023-08-21--dev-notes.md delete mode 100644 notes/2023-08-23--dev-notes.md delete mode 100644 notes/2023-08-24--dev-notes.md delete mode 100644 notes/2023-08-28--dev-notes.md delete mode 100644 notes/bibliography.md diff --git a/notes/2023-08-05--dev-notes.md b/notes/2023-08-05--dev-notes.md deleted file mode 100644 index 2448b28..0000000 --- a/notes/2023-08-05--dev-notes.md +++ /dev/null @@ -1,31 +0,0 @@ -# Dev notes — 2023-08-05 - -- [ ] consider refactoring assembler to do something more like 'tokenize, then call helper functons' - -- [ ] consider an extended system: - - add a byte in the ~zero-page to act as a flag for display mode - - 0 = 1 byte per pixel (default) - - 1 = 1 bit per pixel - - add another 16 ops (or fewer) - - rotate left / right (or shift?) - - AND - - OR - - more flags? - - another register? - - would require several new ops - - add binary input/output to assembler - -- consider gamepad vs. hex keypad - - stick with hex - - but permit gamepad since that's a subset (NES layout = 8 bits/bytes, depending on mode) - - look at how uxn does it? - -- [ ] rewrite to call things 'opcodes' and 'operands' - -- add bank switching for higher 128 bytes - - add a flag in the ~zero-page - -- try writing: - - a 'greater than' routine - -- [x] make coding forms! \ No newline at end of file diff --git a/notes/2023-08-07--dev-notes.md b/notes/2023-08-07--dev-notes.md deleted file mode 100644 index 595448b..0000000 --- a/notes/2023-08-07--dev-notes.md +++ /dev/null @@ -1,82 +0,0 @@ -# Dev notes — 2023-08-07 - -## Carry vs overflow - -[Understanding the difference between overflow and carry flags](https://stackoverflow.com/questions/69124873/understanding-the-difference-between-overflow-and-carry-flags) - -> Carry indicates the result isn't mathematically correct when interpreted as unsigned, overflow indicates the result isn't mathematically correct when interpreted as signed. -> - 1111 + 0001 = 0000 should set carry (15 + 1 = 0 is false) and clear overflow (-1 + 1 = 0 is true). -> - 0111 + 0010 = 1001 should clear carry (7 + 2 = 9 is true) and set overflow (7 + 2 = -7 is false). -> - 1001 + 1001 = 0010 should set both (9 + 9 = 2 and -7 + -7 = 2 are both false). - -so carry is unsigned -and overflow is signed - -(which is what i've got, good) - -## add more flags + change flag ops - -### flags register - -- [ ] Replace the current 'Carry Flag' with a Flags Register. - -Here's a sketch for the bit pattern: - -``` -hi bit - -0 -0 -0 -0 - -0 ? negative -0 ? zero -0 overflow -0 carry - -lo bit -``` - -cf. 6502: - -- NV-BDIZC -- 7: Negative, 6: Overflow, 5: none, 4: Break, 3: Decimal, 2: Interrupt disable, 1: Zero, 0: Carry - -### flag opcodes - -- [ ] replace `CHP` and `CFC` with `FHP` and `FTG` - -- `FHP n`: hop if flag _n_ is set - - eg: `FHP 0` = hop if carry flag set - - eg: `FHP 1` = hop if overflow flag set - - to keep it simple, we're just giving each flag a number, not fussing with bitmasking or anything -- `FTG n`: toggle flag _n_ on/off - - eg: if Carry is on, `FTG 0` turns it off - - eg: if Overflow is off, `FTG 1` turns it on - -`FHP` and `FTG` can be combined to create `set flag` and `unset flag` routines: - -``` -@set_carry: - FHP 0 - FTG 0 -``` -If Carry is on when this is called, then `FTG` is skipped and Carry remains set. Otherwise, `FTG` sets carry. - -``` -; call with a return address stored at $01 - -@unset_carry: - FHP 0 ; 1 - FHP 0 ; 2 - FTG 0 ; 3 - JMP ($01) ; jump back to caller -``` -If Carry is on when this is called, then the execution is: 1, 3, and Carry is turned off. - -If Carry is off, then the execution is: 1, 2, (hop over 3; Carry is still off), jump back to caller. - -## Think about a subroutine stack? - -Maybe? \ No newline at end of file diff --git a/notes/2023-08-09--dev-notes.md b/notes/2023-08-09--dev-notes.md deleted file mode 100644 index 489c833..0000000 --- a/notes/2023-08-09--dev-notes.md +++ /dev/null @@ -1,81 +0,0 @@ -# Dev notes — 2023-08-07 - -## Keypad - -An actual hexadecimal layout: - -``` -0 1 2 3 -4 5 6 7 -8 9 A B -C D E F -``` - -Another: - -``` -1 2 3 A -4 5 6 B -7 8 9 C -0 F E D -``` - -The conventional layout for cheap hardware: - -``` -1 2 3 A -4 5 6 B -7 8 9 C -* 0 # D -``` - -Kim-1: - -``` - GO ST RS -AD DA PC + -C D E F -8 9 A B -4 5 6 7 -0 1 2 3 -``` - -COSMAC VIP/CHIP-8: - -``` -1 2 3 C -4 5 6 D -7 8 9 E -A 0 B F -``` - -CHIP-8 to QWERTY mapping: - -``` -1 2 3 4 -Q W E R -A S D F -Z X C V -``` - -## Turtle graphics - -- yesterday Elizabeth had the great idea to create a turtle robot to go with the paper computer -- [ ] a minimal LOGO would be a fantastic (if challenging) program to write for the computer - - it could use the hex keypad: - - using CHIP-8 layout... - - A key: mode toggle - - mode 1: numeric - - mode 2: commands (tokens?)... something like this: - - `F` - mode toggle - - `2` - forward - - `4` - left - - `6` - right - - `0` - reverse - - `5` - turn - - `1` - pen up - - `C` - pen down - -## Dot-matrix display - -- [ ] Maybe try 5x5, like the micro:bit? \ No newline at end of file diff --git a/notes/2023-08-10--dev-notes.md b/notes/2023-08-10--dev-notes.md deleted file mode 100644 index 4236aac..0000000 --- a/notes/2023-08-10--dev-notes.md +++ /dev/null @@ -1,32 +0,0 @@ -# Dev notes — 2023-08-10 - -- [x] Name it "Cardiograph" - - homage to CARDIAC - - (and cardboard) - - and the special feature is that unlike other paper computers, this one has a graphical display - -- art note: "performing computation" - -## Keypad (and memory map) - -- [ ] Copy the CHIP-8 approach: one memory location that stores the current key pressed - - (Only one key can be pressed at a time) - - And then do a bitmask-y thing for the secret advanced mode, to add the ability to detect multiple simultaneous keypresses - -## Display (and memory map) - -- Move display to $10? -- Then $00 could contain a jump to the start of code, and we wouldn't have this strange "IP doesn't start at 0" situation -- But, this might feel more complicated, and it would make working with the display a little less elegant... -- C64 has random stuff at $0000 - - How does the C64's PC get initialized ?? - -## (Moved from readme:) Nice features that didn't fit - -- Hop `IF<` and hop `IF>` -- `MUL` and `DIV` -- Rotates and shifts - -## (Moved from readme:) Possible features, maybe someday - -- Timer (for a version in software/electronic-hardware) \ No newline at end of file diff --git a/notes/2023-08-12--dev-notes.md b/notes/2023-08-12--dev-notes.md deleted file mode 100644 index b7b29af..0000000 --- a/notes/2023-08-12--dev-notes.md +++ /dev/null @@ -1,60 +0,0 @@ -# Dev notes — 2023-08-12 - -Brainstorming/sketching around subroutines with a return stack... - - ; need an instruction for IP → A i guess? - ; ideally… - ; but a jump table would work - ; put that at beginning of the code - ; then store numbers for subroutine labels in a designated memory slot - - lda $1 - sto $19 ; contains ID # for the next fn to jump to - - @jump_table - hop $1 - jmp @jt2 - jmp @example_computation - @jt2 - hop $2 - ; jmp @jt3 - nop - ; etc … - jmp @end - - @example_computation - lda 5 - sto $20 - lda 3 - sto $21 - ; $19 still has the # for this routine - ; but let’s pretend it doesn’t and demonstrate updating it - lda $1 - sto $19 - jmp @greater? - - ; call with numbers to test in $20 and $21 - ; result is stored in acc - @greater? - ; lda ($20) - ; sub ($21) - ; todo… - ; wouldn’t it be great to have a “hop if neg” op… - ; do we have to just subtract numbers until we get 0? - - ; no! - ; here’s an approach that’s at least better than that - lda ($21) - sto $22 ; stash - @loop - lda ($21) - sub $1 - sto $22 ; stash - sub ($20) - hop $0 - jmp @loop - sto $1 - jmp $jmp_table - ; ok this isn’t quite it… we also need to chexk if we hit 0 by just deceementinf and if so retuen 0 - - jmp @jump_table \ No newline at end of file diff --git a/notes/2023-08-15--dev-notes.md b/notes/2023-08-15--dev-notes.md deleted file mode 100644 index c66a77d..0000000 --- a/notes/2023-08-15--dev-notes.md +++ /dev/null @@ -1,52 +0,0 @@ -# Dev notes — 2023-08-15 - -## Goals for today - -- [x] Review planned changes to simulator - - [x] 'opcodes' and 'operands' - - [x] fix $00 contains $20 bug - -- [x] Review planned changes to the system - - [x] CHP, CFC -> FHP, FTG -- dev note 2023-08-07 - - [/] bank-switching flag in 0 page - - added notes below, but decided to leave implementation for another day - - [x] ? 5x5 display - -- [/] Implement any changes necessary for writing a program? -- [-] Write a program - - [-] LOGO turtle on 5x5? - -## Overflow flag - -Ken Shirriff, [The 6502 overflow flag explained mathematically](https://www.righto.com/2012/12/the-6502-overflow-flag-explained.html): - -> A common definition of overflow is `V = C6 xor C7`. That is, overflow happens if the carry into bit 7 is different from the carry out. - -## Bank switching - -### Planned memory map - -- `00-0F` - display (4x4) -- `10-1F` - keypad? (details TBD) -- `20 ` - pointer to display memory -- `21 ` - pointer to keypad memory -- `22 ` - pointer to memory bank -- `23-2F` - reserved for future use / variable storage -- `30 ` - initial value for IP -- `30-80` - free -- `80-FF` - free, can be bank-switched - -## Looping using an interval timer - - const loop = setInterval(async () => { - step = step + 1; - // Temporary limit as a lazy way to halt infinite loops: - if (CYCLE_LIMIT && (step > CYCLE_LIMIT)) { - console.log('SIMULATION HALTING - reached cycle limit'); - clearInterval(loop); - } - if (!CPU.running) clearInterval(loop); - if (CPU.IP >= CPU.memory.length) clearInterval(loop); - stepCPU(); - await logCPUState(debug); - }, frameRate); \ No newline at end of file diff --git a/notes/2023-08-16--dev-notes.md b/notes/2023-08-16--dev-notes.md deleted file mode 100644 index fc45933..0000000 --- a/notes/2023-08-16--dev-notes.md +++ /dev/null @@ -1,24 +0,0 @@ -# Dev notes — 2023-08-16 - -## Goals for today - -- [x] Finish implementing *ADDR -- [x] Rename =constants to #constants -- [ ] ? Bank switching -- [>] Notes re: ROM and/or tape loader -- [x] CPU updates - - [x] Rename to CPU - - [x] Implement single-stepping - - [x] Implement keypad input - - [-] Look at KIM-1 and VIP buttons for memory editing -- [x] Rename to 'Cardiograph Mark I' (a 'Harvard Mark I' reference, plus a dumb drawing joke) - -- Programming ideas: - - Draw dot at (x, y) - - Move dot around display using keypad - - simple LOGO - -## Misc. earlier notes - -- make sth that can run on phone! -- ? rename repository \ No newline at end of file diff --git a/notes/2023-08-17--dev-notes.md b/notes/2023-08-17--dev-notes.md deleted file mode 100644 index 2d6dc9a..0000000 --- a/notes/2023-08-17--dev-notes.md +++ /dev/null @@ -1,16 +0,0 @@ -# Dev notes — 2023-08-17 - -## Goals for today - -- [ ] *ADDR - Add relative offsets - `*ADDR +1`, `$ADDR -2` -- [ ] ? Add keypad visualization to simulator - 1. Display qwerty-to-VIP mapping for reference - 2. Highlight the most recent keypress on there -- [ ] Notes re: ROM and/or tape loader - -- Programming ideas: - - Keypad display - 1. Light pixel corresponding to most recent keypress - 2. Display character corresponging to most recent keypress - - Draw dot at (x, y) - - Move dot around display using keypad \ No newline at end of file diff --git a/notes/2023-08-21--dev-notes.md b/notes/2023-08-21--dev-notes.md deleted file mode 100644 index 59fd849..0000000 --- a/notes/2023-08-21--dev-notes.md +++ /dev/null @@ -1,77 +0,0 @@ -# Dev notes - 2023-08-21 - -## Goals for today - -- graphic design - - [x] graphics: 80 col card template - - see below -- printing - - [x] print: paper tape template - - [x] print: colour coding forms - - [x] print: 80 col card template - -- [ ] see code/design commentary below... - -- (consider python (microbit) version of simulator) - - maybe ask for e's input - -## Memory map re-think - -### CPU start-up - -When starting up, the CPU executes a `JMP $FF`. - -Put differently: it starts executing instructions at the address contained in `$FF`. - -### Cardiograph memory map - -``` -00-19 - display (5x5) -1A - pointer to display memory -1B - keypad: value of latest key pressed -1C - reserved for future use (bank switching flag) -1D-FE - free -``` - -## References re: where do CPU instruction pointers start, and how are they set? - -- ["Memory Map Requirements", in *6502 PRIMER: Building your own 6502 computer*](http://wilsonminesco.com/6502primer/MemMapReqs.html) - - "Reset (RST): When the 6502's RST input gets pulled low and then brought back high, the 6502 starts its reset process, and gets the address to start executing program instructions from $FFFC-FFFD. Notice it does not start executing at address $FFFC, but reads it to get the beginning address of the routine where it should start executing. That routine will normally have to be in ROM." -- [What address does the x86 begin executing at?](https://stackoverflow.com/questions/4004493/what-address-does-the-x86-begin-executing-at) - - "The `cs` (code selector) register is set to `0xffff` and `ip` (instruction pointer) is set to `0x0000`." -- [Why is the first BIOS instruction located at 0xFFFFFFF0 ("top" of RAM)?](https://superuser.com/questions/988473/why-is-the-first-bios-instruction-located-at-0xfffffff0-top-of-ram) (x86) - -## Imported notes from earlier - -### 2023-08-18 cardiograph loose thoughts - -- use binary encoded punch cards for cardiog progs in machine code -- try making a microbit based emulator - - (microbit + hex keypad) - - (machine code monitor like VIP…) - - (+ tape input??) -- a4 template with full size 80 col card -- snake -- [/] add simulator todo: pass asm line thru to cpu to print when debugging - - asm: create a second array that stores every line with code (nor blank or comment only lines) + its line number - - cpu: accept an optional debugging array, print line # and statement -- readme: - - [x] readme: rename (or split up?) mem map / peripherals section - - [x] ? readme: put 2 keypad charts side by side (they would fit on my phone) -- [/] see paper notes on mem map - - -## 2023-08-19 - - [/] reconsider ISA order in light of supercat comment here - - [Why didn't the 6503 have increment/decrement opcodes for A?](https://retrocomputing.stackexchange.com/questions/13023/why-didnt-the-6502-have-increment-decrement-opcodes-for-a) - -- [/] look at use of `*` or `.` in assembly - - [What does "jmp *" mean in 6502 assembly?](https://retrocomputing.stackexchange.com/questions/7998/what-does-jmp-mean-in-6502-assembly) - - -## 2023-07-17 - cardiograph - worksheet for hand-assembling code… + other docs - -- [ ] docs/graphics: machine code quick ref (ops + short explanations + mnems) -- [ ] docs/graphics: assembly quick ref (as above, plus assembler-specific syntax like constants) -- [ ] docs/graphics: worksheet for hand-assembling \ No newline at end of file diff --git a/notes/2023-08-23--dev-notes.md b/notes/2023-08-23--dev-notes.md deleted file mode 100644 index 9ad3e74..0000000 --- a/notes/2023-08-23--dev-notes.md +++ /dev/null @@ -1,174 +0,0 @@ -# Dev notes — 2023-08-23 - -## Problems to solve - -The outline below / in the new README has some appeal, but it makes each instruction 8 bits long, which would require altering the simulator to support 8 bit addressing/storage... - -Do I want to do this?? - -Or maybe go with the simpler "just swap NOP and FHP" plan... - -**→ Ok i'm going to bail on this for now; the current set is easier to work with and nicer to teach. It was good to learn about and think about this, and maybe it will come back later, but for now it feels like adding this complexity would be contrary to my goals of maximum simplicity and rapid learnability.** - -## Instruction set layout notes - -### Reference: 6502 - -[The 6502 Instruction Set Decoded](https://llx.com/Neil/a2/opcodes.html) - -> Most instructions that explicitly reference memory locations have bit patterns of the form aaabbbcc. The aaa and cc bits determine the opcode, and the bbb bits determine the addressing mode. - -## CHUMP reference - -from David Feinberg, "A Simple and Affordable TTL Processor for the Classroom": - -> The CHUMP instruction set features seven key operations, each of which comes in two flavors: constant and memory. For example, there is an ADD command for adding a constant to the accumulator, and another ADD for adding a value from memory to the accumulator. The 4-bit constant portion of the instruction is ignored by the seven memory commands. Table 1 describes the seven constant commands. The corresponding memory commands operate similarly on a memory value, and have a 1 in the op-code's low-order bit. -> -> For example, the following program increments the value in RAM location 2 repeatedly. Used properly, every READ command should be followed by a memory command, and every memory command should be preceded by a READ command. - -> ```0: 10000010 READ 2 -> 1: 00010000 LOAD IT -> 2: 00100001 ADD 1 -> 3: 01100010 STORETO 2 -> 4: 10100000 GOTO 0 ->``` - -Constant instructions: - - dec bin - 00 0000 Load - 02 0010 Add - 04 0100 Subtract - 06 0110 Store To - 08 1000 Read - 10 1010 GOTO - 12 1100 If Zero - -Memory instructions (I think): - - dec bin - 01 0001 Load - 03 0011 Add - 05 0101 Subtract - 07 0111 Store To - 09 1001 Read - 11 1011 GOTO - 13 1101 If Zero - -## Current Cardiograph - -``` -hex bin -00 0000 END * -01 0001 STO lit# -02 0010 STO addr -03 0011 LDA lit# -04 0100 LDA addr -05 0101 ADD lit# -06 0110 ADD addr -07 0111 SUB lit# -08 1000 SUB addr -09 1001 HOP lit# -0A 1010 HOP addr -0B 1011 JMP lit# -0C 1100 JMP addr -0D 1101 FTG lit# -0E 1110 FHP lit# * -0F 1111 NOP ———— * -``` -so the least significant bit indicates the addressing mode (0 = direct, 1 = indirect) - -except for three exceptions: END, FHP, and NOP - -## Possible Cardiograph revisions - -If NOP swaps with FHP, then a 1 in the least significant bit always indicates literal addressing: - -``` -0000 END -0001 STO lit# -0010 STO addr -0011 LDA lit# -0100 LDA addr -0101 ADD lit# -0110 ADD addr -0111 SUB lit# -1000 SUB addr -1001 HOP lit# -1010 HOP addr -1011 JMP lit# -1100 JMP addr -1101 FTG lit# -1110 NOP -1111 FHP lit# -``` - -Or we could use 8 bits, and use one of the upper 4 to group instructions: - -``` -00 0000 0000 END -01 0000 0001 NOP -... ... NOP -0F 0000 1111 NOP - -10 0001 0000 STO lit# -11 0001 0001 STO addr -12 0001 0010 LDA lit# -13 0001 0011 LDA addr -14 0001 0100 ADD lit# -15 0001 0101 ADD addr -16 0001 0110 SUB lit# -17 0001 0111 SUB addr -18 0001 1000 HOP lit# -19 0001 1001 HOP addr -1A 0001 1010 JMP lit# -1B 0001 1011 JMP addr -1C 0001 1100 FTG lit# -1D 0001 1101 FTG addr -1E 0001 1110 FHP lit# -1F 0001 1111 FHP addr -``` - -``` -gggg iii a - -g: group -i: instruction -a: addressing mode (for group 0) -``` - -- makes the use of the LSB for direct/indirect addressing perfectly consistent for group `0001` -- makes room for indirect `FTG` and `FHP` (but those still don't seem very useful) - -... - -But if I want to be able to add more groups later, something like `gg aa iiii` might be better... - -``` -hex bin group mode op - -00 0000 0000 0 -- END -01 0000 0001 0 -- NOP - -50 0101 0000 1 direct STO -51 0101 0001 1 direct LDA -52 0101 0010 1 direct ADD -53 0101 0011 1 direct SUB -54 0101 0100 1 direct HOP -55 0101 0101 1 direct JMP -56 0101 0110 1 direct FTG -57 0101 0111 1 direct FHP - -60 0110 0000 1 indirect STO -61 0110 0001 1 indirect LDA -62 0110 0010 1 indirect ADD -63 0110 0011 1 indirect SUB -64 0110 0100 1 indirect HOP -65 0110 0101 1 indirect JMP -66 0110 0110 1 indirect FTG -67 0110 0111 1 indirect FHP -``` - -~~**let's do that!**~~ - -(but for now i'm going to skip indirect FTG and FHP out of laziness) \ No newline at end of file diff --git a/notes/2023-08-24--dev-notes.md b/notes/2023-08-24--dev-notes.md deleted file mode 100644 index 9bb2981..0000000 --- a/notes/2023-08-24--dev-notes.md +++ /dev/null @@ -1,81 +0,0 @@ -# Dev notes — 2023-08-24 - -## CPU start-up - -Thinking about booting up... -how would the "pointer to display memory" etc. get initialized? - -They could be in ROM, but then they're not re-locatable. - -Maybe there's a start-up routine in ROM that sets them up? - -[cf. C64 bank switching](https://www.c64-wiki.com/wiki/Bank_Switching) - -The C64 uses I/O lines on the CPU (+ others) to signal to the PLA that it's time to switch ROM banks in or out. - -Maybe I need something like that? - -~~A simplified approach might be to put the pointer-to-display and ...~~ - -Current memory map: - -- `00-19` - display (5x5) -- `1A ` - pointer to display memory -- `1B ` - keypad: value of latest key pressed -- `1C ` - reserved for future use (bank switching flag) -- `1D ` - initial IP -- `1D-FF` - free - -Thinking about changing it for better bank-switching: - -- `00-19` - display (5x5) -- `20 ` - initial IP -- ` ` - free -- `FC ` - I/O controller - pointer to display memory -- `FD ` - I/O controller - reserved for future use (bank switching flag) -- `FE ` - I/O controller - keypad: value of latest key pressed -- `FF ` - ROM - pointer to initial IP - -Ah so actually the issue is that the CPU needs to be paired with an I/O controller, -and that needs to know where to look to find these pointers/where to put the keypad info. - -And I think that can just be hand-waved away for now? - -### Here's the plan - -**But I got started on this because I was trying to work out how FF gets loaded with the initial IP, and I think that's still a question.** - -***(And maybe some way to switch ROM in and out would be good for the secret-advanced-mode, since it would be great to have a ROM pre-loaded with a set of convenient utility routines.)*** - -I think maybe we just leave the IP hardcoded for now; say that there's a 1-byte ROM at $FF. - -- `00-19` - display (5x5) -- `1A ` - pointer to display memory -- `1B ` - keypad: value of latest key pressed -- `1C ` - reserved for future use (bank switching flag) -- `1D ` - initial IP -- `1D-FE` - free -- `FF ` - ROM (unwriteable) - pointer to initial IP - -- [ ] store `$1D` at `$FF` -- [ ] make CPU execute `JMP $FF` on startup -- [ ] make ROM unwriteable - -More step-by-step: - - - Change memory from a Uint8Array to a regular array, - and make every entry { number | { type: 'ROM', value: number }} - - Store ROM as an object in machine.config.js - - Load ROM data into memory at CPU startup (`startCPU(RAM, ROM)`) - -(And continue to handwave away how that RAM already contains data, for now...) - -## TODO - -- [ ] Check that we're starting execution at $1D now -- [ ] Programming - - [ ] Subroutine stack - - [ ] Conway's Life -- [ ] ? bank switching -- [ ] Keypad handling -- [ ] Start-up process described above \ No newline at end of file diff --git a/notes/2023-08-28--dev-notes.md b/notes/2023-08-28--dev-notes.md deleted file mode 100644 index 88b39d5..0000000 --- a/notes/2023-08-28--dev-notes.md +++ /dev/null @@ -1,26 +0,0 @@ -# Dev notes — 2023-08-28 - -Two current goals: - -## 1. Implement a new CLI interface - -- New arg-parser -- Allow setting clock speed with a command line flag - - -## 2. Re-architect - -- Move the bulk of cpu.js to cpu.lib.js -- Add a way to register functions to be called each cycle -- Create a new 'cardiograph.js' - - imports cpu.lib.js - - registers functions for logging, updating display - - (split those apart) - - provides terminal interface - - get machine code input from stdin (or like, file stream?) - - `./assembler.js | ./cardiograph.js - - see below re: changes to assembler -- Assembler - - Move CLI interface into assembler.js and delete run-assembler.js - - Add a --json flag, which causes the assembler to print the json of {machineCode, debugInfo} on stdin - - Otherwise, print hex/bin to stdin \ No newline at end of file diff --git a/notes/bibliography.md b/notes/bibliography.md deleted file mode 100644 index 2724a10..0000000 --- a/notes/bibliography.md +++ /dev/null @@ -1,107 +0,0 @@ -# Bibliography - -Entries in bold are especially influential to my work on this project so far. - -## To-read - -- https://www.drdobbs.com/embedded-systems/paper-to-fpga/240155922 -- LMC: - - http://elearning.algonquincollege.com/coursemat/dat2343/lectures.f03/12-LMC.htm - - http://www.povinelli.org/teaching/eece2710/lmc.html -- https://web.archive.org/web/20220628132003/https://thgie.ch/notes/Paper%20Computer.html -- https://wiki.osdev.org/Expanded_Main_Page - - (I haven't looked at this in this context at all yet) -- https://www.computerenhance.com/p/table-of-contents -- https://turingcomplete.game - -### Games - -- https://xscx.itch.io/110 -- **https://jimhall.itch.io/toy-cpu** -- https://annwan.itch.io/a-cpu -- **https://nandgame.com** -- https://tobiasvl.itch.io/flip-8 -- https://internet-janitor.itch.io/octo - -### Machines - -- https://en.wikipedia.org/wiki/CHIP-8 -- https://en.wikipedia.org/wiki/KIM-1 -- BBC Micro:bit - -### 4-bits - -- https://github.com/Subsystems-us/4-bit-Microprocessor-Trainer/blob/main/SubsySTEM2_Manual_rev01.pdf -- https://blog.lapinozz.com/learning/2016/11/19/calculator-with-caordboard-and-marbles.html -- https://jacobsweeten.github.io/4-Bit-Computer/ -- **CHUMP** - - http://darcy.rsgc.on.ca/ACES/TEI4M/4BitComputer/index.html - - https://www.youtube.com/watch?app=desktop&v=b5qDwCN9Q2c - - -### Misc./To-sort - -- https://retrocomputingforum.com/t/some-mechanical-and-optical-curiosities/1598/5 -- "Coloring computers" -- **"NAND to Tetris" / Elements of Computing Systems** -- **Charles Petzold, _Code_.** -- **Mark Jones Lorenzo, _The Paper Computer Unfolded: A Twenty-First Century Guide to the Bell Labs CARDIAC (CARDboard Illustrative Aid to Computation), the LMC (Little Man Computer), and the IPC (Instructo Paper Computer)_ (self published, 2017).** - -## Implementation reference - -### Assembler design - -- https://stackoverflow.com/questions/10244422/how-is-a-2-pass-assembler-different-from-a-one-pass-assembler-in-resolving-the-f - - https://gear.kku.ac.th/~watis/courses/188231/sp2-4.pdf - -### Javascript - -- https://devhints.io/jsdoc -- https://stackoverflow.com/questions/25354313/saving-a-uint8array-to-a-binary-file - - -## Temporary(?) references - -- [Putting the “You” in CPU](https://cpu.land) - - on HN: https://news.ycombinator.com/item?id=36823605 - - "These projects are really fun. On the other hand, you might want to learn in a way that lets you build hardware (esp for FPGA's). For that, I suggest a few types of books with examples: - ..." https://news.ycombinator.com/item?id=36825693 - -- https://stackoverflow.com/questions/29193303/6502-emulation-proper-way-to-implement-adc-and-sbc -- http://6502.org/tutorials/6502opcodes.html - - -## Learning about CPUs - -- http://skilldrick.github.io/easy6502/ -- [Beagle Bros "6502 instruction reference"](https://raw.githubusercontent.com/camsaul/nesasm/master/beagle_bros_6502_reference.png) - -## Instructional/toy computers - -### Paper based - -- **https://en.wikipedia.org/wiki/CARDboard_Illustrative_Aid_to_Computation** - - <3 -- **https://en.wikipedia.org/wiki/Little_man_computer** -- **https://en.wikipedia.org/wiki/WDR_paper_computer** - -### Mechanical - -- Paperclip Computer - - https://hackaday.com/2020/02/08/a-modern-take-on-the-paperclip-computer/ -- https://en.wikipedia.org/wiki/Digi-Comp_I -- https://en.wikipedia.org/wiki/Digi-Comp_II -- https://en.wikipedia.org/wiki/Turing_Tumble -- https://en.wikipedia.org/wiki/Dr._Nim -- https://en.wikipedia.org/wiki/Geniac - -### Virtual machine - -- https://wiki.xxiivv.com/site/uxn.html - - <3 - -### Computer games - -- https://en.wikipedia.org/wiki/TIS-100 -- https://en.wikipedia.org/wiki/Human_Resource_Machine - - I haven't played this one \ No newline at end of file From 98fa9a4ab788a33921e7bb6a9f6737356a28f15a Mon Sep 17 00:00:00 2001 From: n loewen Date: Sun, 10 Sep 2023 21:31:21 -0700 Subject: [PATCH 47/62] Remove 'issues' dir, since they moved to the 'notes' branch --- issues/issues.md | 46 ---------------------------------------- issues/todo.md | 55 ------------------------------------------------ 2 files changed, 101 deletions(-) delete mode 100644 issues/issues.md delete mode 100644 issues/todo.md diff --git a/issues/issues.md b/issues/issues.md deleted file mode 100644 index 4a9e2dc..0000000 --- a/issues/issues.md +++ /dev/null @@ -1,46 +0,0 @@ -# Cardiograph issues - -## Open - -### #1 - Improve CLI interface - -I'm thinking of an interface like this... - - $ ./cpu.js -mc code.bin - $ ./cpu.js code.asm - $ ./cpu.js --debug code.asm - -Full list of flags I want: - - -d --debug - -s --singlestep - -p --prettydisplay - -mc --machinecode - -### #2 - Startup: Execute `JMP $FF` - -See [2023-08-24](../notes/2023-08-24--dev-notes.md#cpu-start-up) - -... say that there's a 1-byte ROM at $FF. - -- `00-19` - display (5x5) -- `1A ` - pointer to display memory -- `1B ` - keypad: value of latest key pressed -- `1C ` - reserved for future use (bank switching flag) -- `1D ` - initial IP -- `1D-FE` - free -- `FF ` - ROM (unwriteable) - pointer to initial IP - -- store `$1D` at `$FF` -- make CPU execute `JMP $FF` on startup -- make ROM unwriteable - -More step-by-step: - - - Change memory from a Uint8Array to a regular array, - and make every entry { number | { type: 'ROM', value: number }} - - Store ROM as an object in machine.config.js - - Load ROM data into memory at CPU startup (`startCPU(RAM, ROM)`) - - -## Closed \ No newline at end of file diff --git a/issues/todo.md b/issues/todo.md deleted file mode 100644 index e29a352..0000000 --- a/issues/todo.md +++ /dev/null @@ -1,55 +0,0 @@ -# To do — Summary - -This is a quick todo list. - -For extended commentary, see [issues](issues.md). - -## Open - -### Todo - -- Finish WIP on run-cli arg parsing -- Pass CYCLE_COUNT as a cli arg - -- (cpu) !! Fix overflow flag -- Add a flag for bank-switching to the ~zero-page -- Remove run-scripts and add the ability to run `./cpu.js` and `./assembler.js` directly -- cf. [#1](issues.md#1---improve-cli-interface) -- [fix] (cpu) Make single-stepping work with simulated keypad - -### Features - -- (cpu) allow arrow keys, too -- [fix] (cpu) Execute `JMP $FF` on startup / Implement ROM — see [#2](issues.md#2---startup-execute-jmp-ff) -- (assembler) Validate labels -- (assembler) Extract debugging to its own module -- (cpu) Consider adding a VIP-style keypad-based machine code monitor -- (cpu) Add a mode that prints the display as text, but still animates -- (cpu) Allow running pre-compiled machine code -- (cpu) DRY out addition and subtraction -- [Extended system (secret bonus operations)](../notes/2023-08-07--dev-notes.md) -- (research) Review CHIP-8 - - read about the spec / ISA - - read these, and add them to the bibliography: - - Steve Losh: https://stevelosh.com/blog/2016/12/chip8-input/ - - https://tonisagrista.com/blog/2021/chip8-spec/ - -### Documentation - -- Improve docs for flags register - -### Testing - -- Display (hex) numbers -- Greater than -- Minimal LOGO-ish interpreter for turtle graphics - - -## Closed - -- 2023-08-26 - [fix] (logging) - 'undefined operand' situation is caused by assembling to an initial IP of $1C, which is an odd number -- (assembler) Pass asm line thru to cpu to print when debugging - - -## Abandoned - -- (assembler) Return pure machine code when printing to stdout (and not in debug mode) \ No newline at end of file From 51c64cc615ee5cac0a22de40c31fcd1ef87f9c91 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 18:30:58 -0700 Subject: [PATCH 48/62] opter - Update to newest version --- src/opter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opter b/src/opter index b6005e1..f43915c 160000 --- a/src/opter +++ b/src/opter @@ -1 +1 @@ -Subproject commit b6005e11bb8867f77ac7ba4d26ab585127a5048a +Subproject commit f43915c735509c55b858dc3a6ec5aeebf4fe4fc7 From 63eb4a9500ca15920d86928ae79242c02047c538 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 19:09:28 -0700 Subject: [PATCH 49/62] opter - Update to latest --- src/opter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opter b/src/opter index f43915c..c3166ef 160000 --- a/src/opter +++ b/src/opter @@ -1 +1 @@ -Subproject commit f43915c735509c55b858dc3a6ec5aeebf4fe4fc7 +Subproject commit c3166ef17b9882d017665476147ccaf0999e7717 From 9c82265a8885f62017feefc998b06b6e85bd6c69 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 19:09:56 -0700 Subject: [PATCH 50/62] assembler - Change to use new Opter interface --- src/assembler.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index ba9bf01..b46080e 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -440,21 +440,20 @@ function assemble(inputFilename, outputToFile, includeMetadata, outputFilename=n const dbg = new DBG('nitpick'); // Handle command-line options... -const opts = new Opter(process.argv); -opts.synonymize('-d', '--debug'); -opts.requireOption('-i', 'Input file required (-i prog.asm)'); -opts.requireOptionArgument('-i', 1, 1, 'Input file required (-i prog.asm)'); -opts.requireOptionArgument('-o', 1, 1, 'Missing output file name (-o prog.asm)'); +const opter = new Opter(); +opter.addOption('-d', '--debug'); +opter.addOption('-i', '--in', true, true, 1); +opter.addOption('-o', '--out', false, true, 1); +let opts = opter.parse(process.argv); -const inputFilename = opts.opts.i[0]; -const outputToFile = opts.contains('-o'); -let outputWithMetadata = opts.contains('--debug'); +const inputFilename = opts.in[0]; +let outputWithMetadata = 'debug' in opts; // Assemble...! -if (outputToFile) { - const outputFilename = opts.opts.o[0]; - assemble(inputFilename, outputToFile, outputWithMetadata, outputFilename); +if ('out' in opts) { + const outputFilename = opts.out[0]; + assemble(inputFilename, true, outputWithMetadata, outputFilename); } else { dbg.setLevel('none'); - assemble(inputFilename, outputToFile, outputWithMetadata); + assemble(inputFilename, false, outputWithMetadata); } From 98bfa502692881ff324a6984dfa0676e92f0d856 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 19:16:34 -0700 Subject: [PATCH 51/62] opter - Update --- src/opter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opter b/src/opter index c3166ef..1d98a07 160000 --- a/src/opter +++ b/src/opter @@ -1 +1 @@ -Subproject commit c3166ef17b9882d017665476147ccaf0999e7717 +Subproject commit 1d98a0707c3e61e362d2d3d5413b475437b5de0e From 91cba57aa1c4f34123c8bec126a0fa58ac0497d4 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 19:33:58 -0700 Subject: [PATCH 52/62] cardiograph - Change to read command line options using Opter --- src/cardiograph.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/cardiograph.js b/src/cardiograph.js index e25c921..47d89b0 100755 --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -1,6 +1,9 @@ #!/usr/bin/env node +const fs = require('fs'); + const DBG = require('./dbg.js'); +const Opter = require('./opter/opter.js'); const { num2hex, bool2bit } = require('./conversions.js'); const CFG = require('./machine.config.js'); @@ -15,16 +18,25 @@ let cpu = new CPU(CFG.initialIP, CFG.defaultCycleLimit); main(); async function main() { - const input = await readPipedStdin(); + const opter = new Opter(); + opter.addOption('-d', '--debug'); + opter.addOption('-i', '--in', false, true, 1); + const opts = opter.parse(process.argv); - const debuggable = true; // FIXME - Get this as a command line flag instead - // if debuggable === true, the input is JSON {sourceInfo, machineCode} + const sourcecodeHasAnnotations = 'debug' in opts; + // if true, then the input is JSON {sourceInfo, machineCode} // otherwise, the input is a string of space-separated numbers + let input = null; + if ('in' in opts) { // Read from file + input = fs.readFileSync(opts.in[0], 'utf8'); + } else { // Read from stdin + input = await readPipedStdin(); + } + let code = null; let sourceInfo = null; - - if (!debuggable) { + if (!sourcecodeHasAnnotations) { code = new Uint8Array(input.split(' ')); } else { const parsedInput = JSON.parse(input); @@ -33,7 +45,7 @@ async function main() { } cpu.loadMemory(code); - if (debuggable) { cpu.loadSourceInfo(sourceInfo); } + if (sourcecodeHasAnnotations) { cpu.loadSourceInfo(sourceInfo); } cpu.onCycleEnd(tick); cpu.onCycleEnd(logCPUState); From 3e32cb97e114046485dc48e95cf0ccfdc41e0a24 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 20:20:42 -0700 Subject: [PATCH 53/62] cardiograph - Change to programmatically infer whether the input includes annotations or not --- src/cardiograph.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/cardiograph.js b/src/cardiograph.js index 47d89b0..e2fbff0 100755 --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -19,14 +19,9 @@ main(); async function main() { const opter = new Opter(); - opter.addOption('-d', '--debug'); opter.addOption('-i', '--in', false, true, 1); const opts = opter.parse(process.argv); - const sourcecodeHasAnnotations = 'debug' in opts; - // if true, then the input is JSON {sourceInfo, machineCode} - // otherwise, the input is a string of space-separated numbers - let input = null; if ('in' in opts) { // Read from file input = fs.readFileSync(opts.in[0], 'utf8'); @@ -36,16 +31,19 @@ async function main() { let code = null; let sourceInfo = null; - if (!sourcecodeHasAnnotations) { - code = new Uint8Array(input.split(' ')); - } else { + + try { const parsedInput = JSON.parse(input); - code = new Uint8Array(parsedInput.machineCode); sourceInfo = parsedInput.sourceInfo; + code = new Uint8Array(parsedInput.machineCode); + } catch (error) { + if (error.name === 'SyntaxError') { + code = new Uint8Array(input.split(' ')); + } } cpu.loadMemory(code); - if (sourcecodeHasAnnotations) { cpu.loadSourceInfo(sourceInfo); } + if (sourceInfo !== null) { cpu.loadSourceInfo(sourceInfo); } cpu.onCycleEnd(tick); cpu.onCycleEnd(logCPUState); From f802420799ec5415be9fe55e85f21cdb6f1d5ca9 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 20:23:44 -0700 Subject: [PATCH 54/62] Rename 'sourceInfo' variables (etc) to 'sourceAnnotations' --- src/assembler.js | 8 ++++---- src/cardiograph.js | 10 +++++----- src/cpu.js | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index b46080e..32ecef2 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -173,7 +173,7 @@ function handleConstantDefinitions(op, arg, IP, constants) { * it will be assembled to the default intial value of the IP, * as specified in `machine.config.js`. * @param {string} source - Assembly source to decode - * @return {{ sourceInfo: Object, machineCode: Array }}; + * @return {{ sourceAnnotations: Object, machineCode: Array }}; **/ // TODO rename? function decodeInstructions(source) { @@ -203,7 +203,7 @@ function decodeInstructions(source) { /** @type {Array} - Assembled source code, as an array of bytes **/ let machineCode = new Array(IP).fill(0); - let sourceInfo = {}; + let sourceAnnotations = {}; // Initialize memory-mapped IO -- TODO this should probably be in the CPU, not here machineCode[CFG.pointerToDisplay] = CFG.displayAddr; @@ -343,7 +343,7 @@ function decodeInstructions(source) { machineCode[IP] = decodedOp; machineCode[IP + 1] = decodedArg; - sourceInfo[IP] = { + sourceAnnotations[IP] = { lineNumber: line.number, source: line.source, address: IP, @@ -381,7 +381,7 @@ function decodeInstructions(source) { } } - return { 'machineCode': machineCode, 'sourceInfo': sourceInfo }; + return { 'machineCode': machineCode, 'sourceAnnotations': sourceAnnotations }; } diff --git a/src/cardiograph.js b/src/cardiograph.js index e2fbff0..31bbaf8 100755 --- a/src/cardiograph.js +++ b/src/cardiograph.js @@ -30,11 +30,11 @@ async function main() { } let code = null; - let sourceInfo = null; + let sourceAnnotations = null; try { const parsedInput = JSON.parse(input); - sourceInfo = parsedInput.sourceInfo; + sourceAnnotations = parsedInput.sourceAnnotations; code = new Uint8Array(parsedInput.machineCode); } catch (error) { if (error.name === 'SyntaxError') { @@ -43,7 +43,7 @@ async function main() { } cpu.loadMemory(code); - if (sourceInfo !== null) { cpu.loadSourceInfo(sourceInfo); } + if (sourceAnnotations !== null) { cpu.loadSourceAnnotations(sourceAnnotations); } cpu.onCycleEnd(tick); cpu.onCycleEnd(logCPUState); @@ -66,8 +66,8 @@ async function tick() { function logCPUState() { let lineInfo = null; - if (cpu.dbg.sourceInfo) { - lineInfo = cpu.dbg.sourceInfo[cpu.dbg.previousIP]; + if (cpu.dbg.sourceAnnotations) { + lineInfo = cpu.dbg.sourceAnnotations[cpu.dbg.previousIP]; } console.group(`Step ${cpu.dbg.cycleCounter}`); console.log(); diff --git a/src/cpu.js b/src/cpu.js index 16588fb..e81a7d0 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -40,7 +40,7 @@ module.exports = class CPU { poke() { return; } // TODO - implement Poke /** @param {Array} info **/ // TODO - document type for 'sourceInfo' - loadSourceInfo(info) { + loadSourceAnnotations(info) { this.dbg.sourceInfo = info; } From ae587a0712c291b560d339bc12f6ebcdabc9d7a9 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 20:29:05 -0700 Subject: [PATCH 55/62] assembler - Change variables name to 'outputWithAnnotations' to match the change made in the previous commit --- src/assembler.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/assembler.js b/src/assembler.js index 32ecef2..0b92bba 100755 --- a/src/assembler.js +++ b/src/assembler.js @@ -441,19 +441,19 @@ const dbg = new DBG('nitpick'); // Handle command-line options... const opter = new Opter(); -opter.addOption('-d', '--debug'); +opter.addOption('-a', '--annotate'); opter.addOption('-i', '--in', true, true, 1); opter.addOption('-o', '--out', false, true, 1); let opts = opter.parse(process.argv); const inputFilename = opts.in[0]; -let outputWithMetadata = 'debug' in opts; +let outputWithAnnotations = 'annotate' in opts; // Assemble...! if ('out' in opts) { const outputFilename = opts.out[0]; - assemble(inputFilename, true, outputWithMetadata, outputFilename); + assemble(inputFilename, true, outputWithAnnotations, outputFilename); } else { dbg.setLevel('none'); - assemble(inputFilename, false, outputWithMetadata); + assemble(inputFilename, false, outputWithAnnotations); } From 92a619fded93e3808ad8bfbb676008ff718a548a Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 20:29:44 -0700 Subject: [PATCH 56/62] (docs) readme - Update simulator usage info --- readme.md | 49 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/readme.md b/readme.md index 902a21d..eedc4af 100644 --- a/readme.md +++ b/readme.md @@ -8,22 +8,49 @@ Cardiograph is an imaginary computer. It has three main components: ## Simulator -### Run assembler - -```./assembler.js source_code.asm [output.txt]``` - -By default, the output is written to `out.txt`. It is saved as a string of space-separated decimal numbers. - - -### Run simulator - -```./cardiograph.js < machine_code.txt``` - ### Dependencies - Node.js +### Quick examples + +Assemble and run: +```./assembler.js -i | cardiograph.js``` + +Assemble to a file: +```./assembler.js -i -o ``` + +Run from a file: +```./cardiograph.js -i ``` + + +### Assembler: assembler.js + +``` +Usage: ./assembler.js [-a] -i [-o ] + +-a, --annotate Output code with debugging annotations +-i, --in Assembly-language input +-o, --out Machine-code output +``` + +- If an output file is not provided, the output is printed to stdout + +- If the `annotate` flag is not set, the machine code is returned as a string of space-separated decimal numbers + + +### Simulator: cardiograph.js + +``` +Usage: ./cardiograph.js [-i ] + +-i, --in Machine-code input +``` + +- If an input file is not provided, the input is read from stdin + + ## CPU ### Registers and Flags From 4854ce34faec42246e1ee33bcbe23fa9b0ebac65 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 20:31:34 -0700 Subject: [PATCH 57/62] Change name to 'cardiograph' in package.json --- src/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package.json b/src/package.json index fa555ee..9cdeacd 100644 --- a/src/package.json +++ b/src/package.json @@ -1,5 +1,5 @@ { - "name": "paper-computer", + "name": "cardiograph", "scripts": { "jsdoc": "./node_modules/.bin/jsdoc" }, From 83e980b685b28c211a6b92a88548b43be2efb547 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 20:33:06 -0700 Subject: [PATCH 58/62] (docs) readme - Fix typo --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index eedc4af..dda9759 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,7 @@ Cardiograph is an imaginary computer. It has three main components: ### Quick examples Assemble and run: -```./assembler.js -i | cardiograph.js``` +```./assembler.js -i | ./cardiograph.js``` Assemble to a file: ```./assembler.js -i -o ``` From b2933a50a096196c3839d29c4dc784746c612281 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 20:47:16 -0700 Subject: [PATCH 59/62] (docs) readme - Change 'register and flags' section to improve style and clarity --- readme.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/readme.md b/readme.md index dda9759..4928863 100644 --- a/readme.md +++ b/readme.md @@ -55,15 +55,22 @@ Usage: ./cardiograph.js [-i ] ### Registers and Flags -- `A` - accumulator -- `IP` - instruction pointer (aka program counter) -- `FLAGS` - flags: **O**verflow, **N**egative, **Z**ero, **C**arry - - in machine language, each flag is given a number: - - O = 3 - N = 2 - Z = 1 - C = 0 - - (bitwise, `0000 = ONZC`) +There are three registers: + +1. **A**, an 8-bit accumulator +2. **IP**, an 8-bit instruction pointer (aka program counter) +3. **flags**, a 4-bit flag register + +The four flags are **O**verflow, **N**egative, **Z**ero, and **C**arry. + +(Overflow is the high bit and carry is the low bit.) + +In decimal: + +| O | N | Z | C | +|---|---|---|---| +| 3 | 2 | 1 | 0 | + ### Instruction set From 9bd88aa8bc57db46a2f974df329c8fe111d51019 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 20:52:31 -0700 Subject: [PATCH 60/62] (docs) readme - Add todo in 'start-up' section, pointing out that the simulator currently doesn't follow the spec here --- readme.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 4928863..c28ff00 100644 --- a/readme.md +++ b/readme.md @@ -72,7 +72,6 @@ In decimal: | 3 | 2 | 1 | 0 | - ### Instruction set #### Operations @@ -99,7 +98,7 @@ Hex Mnem. Operand Effect ``` - Instructions are two bytes long: - one byte for the opcode, one for the operand + one byte for the opcode, one for the operand #### Effects on memory, flags, registers @@ -135,6 +134,9 @@ When starting up, the CPU executes a `JMP $FF`. Put differently: it starts executing instructions at the address contained in `$FF`. +TODO: currently the simulator doesn't actually do this + + ### Assembly language ADD $01 ; comments follow a `;` From 62b7396ab620e838da11dd5dfa30029d5190a8eb Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 21:15:12 -0700 Subject: [PATCH 61/62] (docs) readme - Change memory map to use a nice table --- readme.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index c28ff00..09a4995 100644 --- a/readme.md +++ b/readme.md @@ -169,13 +169,18 @@ Put differently: it starts executing instructions at the address contained in `$ ## Cardiograph memory map -- `00-19` - display (5x5) -- `1A ` - pointer to display memory -- `1B ` - keypad: value of latest key pressed -- `1C ` - reserved for future use (bank switching flag) -- `1D ` - initial IP -- `1D-FE` - free -- `FF ` - ROM (unwriteable) pointer to initial IP (not yet implemented) +| Address | Used for... | +|----------|-----------------------------------------------| +| 00 to 19 | display (5x5) | +| 1A | pointer to display memory | +| 1B | keypad: value of latest key pressed | +| 1C | reserved for future use (bank switching flag) | +| 1D | initial IP | +| 1D to FE | free | +| FF | * ROM (unwriteable) pointer to initial IP | + +\* Not implemented yet + ## Cardiograph peripherals From 35d164b0a7838ab92c067e5bc26e2ede51178740 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 21:18:59 -0700 Subject: [PATCH 62/62] (docs) readme - Change 'keypad' section to use the nicer formatting from the main branch --- readme.md | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index 09a4995..29e994e 100644 --- a/readme.md +++ b/readme.md @@ -190,20 +190,12 @@ The value of the latest keypress on a hex keypad is stored at `$1B`. The keypad uses the same layout as the COSMAC VIP (and CHIP-8). The CPU simulator maps those keys onto a Qwerty set: -``` -1 2 3 C 1 2 3 4 -4 5 6 D Q W E R -7 8 9 E A S D F -A 0 B F Z X C V - -hex pad simulator -``` +`1` `2` `3` `C`   =   `1` `2` `3` `4` +`4` `5` `6` `D`   =   `Q` `W` `E` `R` +`7` `8` `9` `E`   =   `A` `S` `D` `F` +`A` `0` `B` `F`   =   `Z` `X` `C` `V` The arrow keys are also mapped onto the hex keypad: -``` - 5 ↑ - 7 8 9 ← ↓ → - -hex pad simulator -``` \ No newline at end of file +` ` `5` ` `   =   ` ` `↑` ` ` +`7` `8` `9`   =   `←` `↓` `→` \ No newline at end of file