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 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/readme.md b/readme.md index 8fa9e80..6acb72f 100644 --- a/readme.md +++ b/readme.md @@ -8,52 +8,77 @@ Cardiograph is an imaginary computer. It has three main components: ## Simulator +### Dependencies +Cardiograph is an imaginary computer. It has three main components: + +1. the CPU, *Card* (short for 'Completely Analogue Risc Machine') +2. an input-output processor, *IO* +3. a display, *Graph* + +## Simulator + ### Dependencies - Node.js - - readline-sync -### Use -#### Assemble +### Quick examples -Hex output: -```./run-assembler run source_code.asm``` +Assemble and run: +```./assembler.js -i | ./cardiograph.js``` -Binary output: -```./run-assembler runbin source_code.asm``` +Assemble to a file: +```./assembler.js -i -o ``` -Verbose debugging output (hex): -```./run-assembler debug source_code.asm``` +Run from a file: +```./cardiograph.js -i ``` -#### Assemble and run -With animated display of screen memory: -```./run-cpu run source_code.asm``` +### Assembler: assembler.js -With verbose debugging output: -```./run-cpu debug source_code.asm``` +``` +Usage: ./assembler.js [-a] -i [-o ] -With single stepping + pretty-printed display: -```./run-cpu step source_code.asm``` +-a, --annotate Output code with debugging annotations +-i, --in Assembly-language input +-o, --out Machine-code output +``` -With single stepping + verbose debugging output: -```./run-cpu stepdebug source_code.asm``` +- 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 -- `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 @@ -82,7 +107,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 @@ -118,6 +143,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 `;` @@ -144,18 +172,24 @@ 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 -- `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 diff --git a/src/argparser b/src/argparser deleted file mode 160000 index 584d9dd..0000000 --- a/src/argparser +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 584d9dd95f4b1b3c69065826cf96b3cda0cf9e16 diff --git a/src/assembler.js b/src/assembler.js old mode 100644 new mode 100755 index d30c41c..0b92bba --- a/src/assembler.js +++ b/src/assembler.js @@ -1,31 +1,21 @@ -const { logMemory, num2hex } = require('./logging.js'); -const { - INITIAL_IP_ADDRESS, - DISPLAY_ADDR, - POINTER_TO_DISPLAY, -} = require('./machine.config.js'); +#!/usr/bin/env node -// 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() +const fs = require('fs'); -/** - * @param {string} assemblyCode - * @param {Boolean} [debug = false] - **/ -exports.assemble = (assemblyCode, debug = false) => { - DEBUG = debug; - return decodeInstructions(assemblyCode); -} +const Opter = require('./opter/opter.js'); +const { logMemory } = require('./logging.js'); +const { num2hex, hex2num, bin2num } = require('./conversions.js'); +const DBG = require('./dbg.js'); -// Configure pseudo-ops: +const CFG = require('./machine.config.js'); + + +/** 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 }, @@ -40,7 +30,6 @@ const mnemonics2opcodes = { nop: { direct: 15, indirect: 15 }, }; - /** * @typedef {('code'|'comment'|'blank')} SourceLineType **/ @@ -77,15 +66,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] @@ -125,13 +114,15 @@ 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); } /** * @param {string} op - * @param {object} labels // TODO document better + * @param {object} labels // TODO - document labels object * @param {number} IP * @returns {Array} - array of labels **/ @@ -146,11 +137,11 @@ 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`); - dbgGroupEnd(1, 'Input line'); + 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`); + dbg.nitGroupEnd('Input line'); return labels; } @@ -167,10 +158,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; } @@ -182,15 +173,16 @@ 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 {{ sourceAnnotations: Object, machineCode: Array }}; **/ +// TODO rename? 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... @@ -203,7 +195,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 @@ -211,18 +203,18 @@ function decodeInstructions(source) { /** @type {Array} - Assembled source code, as an array of bytes **/ let machineCode = new Array(IP).fill(0); - let debugInfo = {}; + let sourceAnnotations = {}; // 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 let labels = {}; let constants = {}; - // Decode line by line... + for (let i = 0; i < lines.length; i++) { let line = lines[i]; // dbg(2, `line info:`); @@ -248,7 +240,6 @@ function decodeInstructions(source) { } } - // *** Decode special operations *** // Opcodes - Handle label definitions @@ -287,23 +278,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 { @@ -313,7 +304,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}'`); @@ -326,7 +317,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]); @@ -352,46 +343,45 @@ function decodeInstructions(source) { machineCode[IP] = decodedOp; machineCode[IP + 1] = decodedArg; - debugInfo[IP] = { + sourceAnnotations[IP] = { lineNumber: line.number, source: line.source, address: IP, machine: [decodedOp, decodedArg] }; - - 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}`); + dbg.nitGroup(`${ASM_LABEL_PREFIX}${k}`); let label = labels[k]; - dbg(1, `Points to byte: ${label.pointsToByte}`); - dbg(1, `Bytes to replace: ${label.bytesToReplace}`); - dbgGroupEnd(1); + dbg.nit(`Points to byte: ${label.pointsToByte}`); + dbg.nit(`Bytes to replace: ${label.bytesToReplace}`); + dbg.nitGroupEnd(); for (let j = 0; j < label.bytesToReplace.length; j++) { machineCode[label.bytesToReplace[j]] = label.pointsToByte; } } - return { 'debugInfo': debugInfo, 'machineCode': new Uint8Array(machineCode) }; + return { 'machineCode': machineCode, 'sourceAnnotations': sourceAnnotations }; } @@ -413,10 +403,57 @@ function stripWhitespaceFromEnds(line) { return line; } -function hex2num(hex) { return parseInt(hex, 16) }; +/** + * 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} 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(inputFilename, outputToFile, includeMetadata, outputFilename=null) { + const sourceCode = fs.readFileSync(inputFilename, 'utf8'); + const out = decodeInstructions(sourceCode); -// 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 + if (includeMetadata) { + const debugJSON = JSON.stringify(out); + if (outputToFile) { + fs.writeFileSync(outputFilename, debugJSON); + } else { + console.log(debugJSON); + } + } else { + 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 opter = new Opter(); +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 outputWithAnnotations = 'annotate' in opts; + +// Assemble...! +if ('out' in opts) { + const outputFilename = opts.out[0]; + assemble(inputFilename, true, outputWithAnnotations, outputFilename); +} else { + dbg.setLevel('none'); + assemble(inputFilename, false, outputWithAnnotations); +} diff --git a/src/cardiograph.js b/src/cardiograph.js new file mode 100755 index 0000000..31bbaf8 --- /dev/null +++ b/src/cardiograph.js @@ -0,0 +1,99 @@ +#!/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'); +const CPU = require('./cpu.js'); +const io = require('./io.js'); + + +/** SETUP **/ +const dbg = new DBG('nitpick'); +let cpu = new CPU(CFG.initialIP, CFG.defaultCycleLimit); + +main(); + +async function main() { + const opter = new Opter(); + opter.addOption('-i', '--in', false, true, 1); + const opts = opter.parse(process.argv); + + 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 sourceAnnotations = null; + + try { + const parsedInput = JSON.parse(input); + sourceAnnotations = parsedInput.sourceAnnotations; + code = new Uint8Array(parsedInput.machineCode); + } catch (error) { + if (error.name === 'SyntaxError') { + code = new Uint8Array(input.split(' ')); + } + } + + cpu.loadMemory(code); + if (sourceAnnotations !== null) { cpu.loadSourceAnnotations(sourceAnnotations); } + + cpu.onCycleEnd(tick); + cpu.onCycleEnd(logCPUState); + + cpu.start(); + io.getKeypadInput(cpu); + 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 logCPUState() { + let lineInfo = null; + if (cpu.dbg.sourceAnnotations) { + lineInfo = cpu.dbg.sourceAnnotations[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}`); + 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)}`); + console.log(`KEY: ${io.readKeyMem(cpu.memory)} ${cpu.running ? "running" : "halted" }`); + console.log(); + console.groupEnd(); +}; + +async function readPipedStdin() { + // 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 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 diff --git a/src/cpu.js b/src/cpu.js index fa1d567..e81a7d0 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -1,422 +1,320 @@ -const readline = require('readline'); -const readlineSync = require('readline-sync'); +const { num2hex } = require('./conversions.js'); -const { - INITIAL_IP_ADDRESS, - DEFAULT_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 = { - // 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, + this._cycleLimit = cycleLimit; - // Functions that update core state - /** @param {Uint8Array} data */ - loadMemory: (data) => { - CPU.memory = new Uint8Array(256); - CPU.memory.set(data, 0); - }, - incrementIP: (offset) => { - CPU.previousIP = CPU.IP; - CPU.IP = CPU.IP + offset; - }, - setIP: (address) => { - CPU.previousIP = CPU.IP; - CPU.IP = address; - }, - updateFlagZero: () => { CPU.FLAGS.Z = CPU.Acc === 0; }, - updateFlagNegative: () => { CPU.Acc & 128 ? CPU.FLAGS.N = true : CPU.FLAGS.N = false }, - - // Debug info - previousIP: 0, - currentInstruction: { - opcode: null, - operand: null, - mnemonic: null, - }, - cycleCounter: 0, -} - - -// FUNCTIONS THAT MODIFY STATE - -const Instructions = { - end: () => { - CPU.currentInstruction.mnemonic = 'END'; - CPU.running = false; - CPU.incrementIP(2); - }, - - store_lit: (lit) => { - CPU.currentInstruction.mnemonic = 'STO lit'; - CPU.memory[lit] = CPU.Acc; - CPU.incrementIP(2); - }, - - store_addr: (addr) => { - CPU.currentInstruction.mnemonic = `STO addr; @addr: ${num2hex(CPU.memory[addr])}`; - CPU.memory[CPU.memory[addr]] = CPU.Acc; - CPU.incrementIP(2); - }, - - load_lit: (lit) => { - CPU.currentInstruction.mnemonic = 'LDA lit'; - CPU.Acc = lit; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - load_addr: (addr) => { - CPU.currentInstruction.mnemonic = `LDA addr; @ addr: ${num2hex(CPU.memory[addr])}`; - CPU.Acc = CPU.memory[addr]; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - add_lit: (lit) => { - CPU.currentInstruction.mnemonic = 'ADD lit'; - // Calculate sum - let sum = CPU.Acc + lit; - if (sum > 255) { - CPU.FLAGS.C = true; - sum = (sum % 255) - 1; - } else { - CPU.FLAGS.C = false; + this.dbg = { + sourceInfo: null, + currentMnemonic: null, + previousIP: initialIP, + cycleCounter: 0, } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); - // FIXME FIXME FIXME - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - CPU.FLAGS.O = true; - } else { - CPU.FLAGS.O = false; - } - CPU.Acc = sum; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - add_addr: (addr) => { - CPU.currentInstruction.mnemonic = 'ADD addr'; - // Calculate sum - let sum = CPU.Acc + CPU.memory[addr]; - if (sum > 255) { - CPU.FLAGS.C = true; - sum = (sum % 255) - 1; - } else { - CPU.FLAGS.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); - // FIXME FIXME FIXME - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - CPU.FLAGS.O = true; - } else { - CPU.FLAGS.O = false; - } - CPU.Acc = sum; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - sub_lit: (lit) => { - CPU.currentInstruction.mnemonic = 'SUB lit'; - // Calculate sum - let sum = CPU.Acc - lit; - if (sum < 0) { - CPU.FLAGS.C = true; - sum = sum + 256; - } else { - CPU.FLAGS.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); - // FIXME FIXME FIXME - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - CPU.FLAGS.O = true; - } else { - CPU.FLAGS.O = false; - } - CPU.Acc = sum; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - sub_addr: (addr) => { - CPU.currentInstruction.mnemonic = 'SUB addr'; - // Calculate sum - let sum = CPU.Acc - CPU.memory[addr]; - if (sum < 0) { - CPU.FLAGS.C = true; - sum = sum + 256; - } else { - CPU.FLAGS.C = false; - } - // Calculate overflow flag status - let bitSixCarry = 0; - if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; } - // let overflow = bitSixCarry ^ (CPU.FLAGS & 8); - // FIXME FIXME FIXME - // I'm on a plane and can't remember how this works - let overflow = 0; - if (overflow) { - CPU.FLAGS.O = true; - } else { - CPU.FLAGS.O = false; - } - CPU.Acc = sum; - CPU.updateFlagNegative(); - CPU.updateFlagZero(); - CPU.incrementIP(2); - }, - - hop_lit: (lit) => { - CPU.currentInstruction.mnemonic = `HOP lit; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`; - if (CPU.Acc === lit) { - CPU.incrementIP(4); - } else { - CPU.incrementIP(2); - } - }, - - hop_addr: (addr) => { - CPU.currentInstruction.mnemonic = 'HOP addr'; - if (CPU.Acc === CPU.memory[addr]) { - CPU.incrementIP(4); - } else { - CPU.incrementIP(2); - } - }, - - jump_lit: (lit) => { - CPU.currentInstruction.mnemonic = 'JMP lit'; - CPU.setIP(lit); - }, - - jump_addr: (addr) => { - CPU.currentInstruction.mnemonic = 'JMP addr'; - CPU.setIP(CPU.memory[addr]); - }, - - flag_toggle: (flagNum) => { - if (flagNum === null) { - console.error('Invalid flag number'); - process.exit(); - } - const flagName = CPU.FLAGNUMS2NAMES[flagNum]; - CPU.currentInstruction.mnemonic = `FTG ${flagName}`; - CPU.FLAGS[flagName] = !CPU.FLAGS[flagName]; - CPU.incrementIP(2); - }, - - flag_hop: (flagNum) => { - if (flagNum === null) { - console.error('Invalid flag number'); - process.exit(); - } - const flagName = CPU.FLAGNUMS2NAMES[flagNum]; - CPU.currentInstruction.mnemonic = `FHP ${flagName}; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`; - if (CPU.FLAGS[CPU.FLAGNUMS2NAMES[flagNum]]) { - CPU.incrementIP(4); - } else { - CPU.incrementIP(2); - } - }, - - no_op: () => { - CPU.currentInstruction.mnemonic = `NOP`; - CPU.incrementIP(2); - }, -} - -const opcodes2mnemonics = { - 0: (operand) => Instructions.end(), - 1: (operand) => Instructions.store_lit(operand), - 2: (operand) => Instructions.store_addr(operand), - 3: (operand) => Instructions.load_lit(operand), - 4: (operand) => Instructions.load_addr(operand), - 5: (operand) => Instructions.add_lit(operand), - 6: (operand) => Instructions.add_addr(operand), - 7: (operand) => Instructions.sub_lit(operand), - 8: (operand) => Instructions.sub_addr(operand), - 9: (operand) => Instructions.hop_lit(operand), - 10: (operand) => Instructions.hop_addr(operand), - 11: (operand) => Instructions.jump_lit(operand), - 12: (operand) => Instructions.jump_addr(operand), - 13: (operand) => Instructions.flag_toggle(operand), - 14: (operand) => Instructions.flag_hop(operand), - 15: (operand) => Instructions.no_op(), -}; - -/** - * Load code into memory and set CPU state to "running" - * @param {Uint8Array} code - Machine code to load - **/ -function startCPU(code) { - CPU.loadMemory(code); - CPU.cycleCounter = 0; - CPU.running = true; - - // FIXME: This conflicts with single-stepping - // (you can single-step, but keys aren't passed - // through to the Cardiograph) - // - // -> The fix is maybe to remove readlineSync, - // and instead stash the keypress into a buffer variable.* - // Then have the stepping function check that buffer, - // and then clear the buffer, each time it runs. - // - // * If it's in the set of keys that are relevant - // to single-stepping. - - // Start listening for keypresses... - readline.emitKeypressEvents(process.stdin); - if (process.stdin.setRawMode != null) { - process.stdin.setRawMode(true); } - process.stdin.on('keypress', (str, key) => { // TODO: is it possible to turn this off again? - if (key.sequence === '\x03') process.exit(); - let name = key.name.toUpperCase(); - if (name in KEY_MAP) { - CPU.memory[KEYPAD_ADDR] = KEY_MAP[name]; - } - }); -} -/** - * Execute just the next instruction in memory - * @param {Object} debugInfo - * @param {Boolean} [debug] - Print machine status and the line of code being executed - **/ -async function stepCPU(debugInfo, debug = false, prettyPrintDisplay = false) { - if (CPU.IP >= CPU.memory.length) { - console.error('HALTING - IP greater than memory size'); - CPU.running = false; + + /** Public interface **/ + + /** + * @param {Uint8Array} machineCode + **/ + loadMemory(machineCode) { + this.memory = new Uint8Array(256); + this.memory.set(machineCode, 0); + } + + peek() { return; } // TODO - implement Peek + + poke() { return; } // TODO - implement Poke + + /** @param {Array} info **/ // TODO - document type for 'sourceInfo' + loadSourceAnnotations(info) { + this.dbg.sourceInfo = info; + } + + /** Set CPU state to "state.running" **/ + start() { + this.running = true; + } + + /** Execute the next instruction in memory **/ + step() { + this._cycleStartCallbacks.forEach((fn) => fn()); + + if (this.IP >= this.memory.length) { + this.running = false; + throw new Error('HALTING - IP greater than memory size'); + } else { + this.instruction.opcode = this.memory[this.IP]; + this.instruction.operand = this.memory[this.IP+1]; + let mnem = this._nums2mnems[this.instruction.opcode]; + let op = this._ops[mnem]; + if (typeof op === 'undefined') { this._failInvalidOpcode(); } + op(this.instruction.operand); + this.dbg.cycleCounter += 1; + } + + // Temporary limit as a lazy way to halt infinite loops + if ((this._cycleLimit > 0) && this.dbg.cycleCounter >= this._cycleLimit) { + this.running = false; + throw new Error(' HALTING - reached cycle limit'); + } + + this._cycleEndCallbacks.forEach((fn) => fn()); + if (!this.running) process.exit(); + } + + + /** Private methods **/ + + _incrementIP(offset) { + this.dbg.previousIP = this.IP; + this.IP = this.IP + offset; + } + + _setIP(address) { + this.dbg.previousIP = this.IP; + this.IP = address; + } + + _updateFlagZero() { + this.flags.Z = this.acc === 0; + } + + _updateFlagNegative() { + if (this.acc & 128) + { this.flags.N = true; } + else + { this.flags.N = false; } + } + + + /** Hooks **/ + + /** @type Array **/ + _cycleStartCallbacks = []; + + /** @type Array **/ + _cycleEndCallbacks = []; + + /** @param {function} fn **/ + onCycleStart(fn) { this._cycleStartCallbacks.push(fn) }; + + /** @param {function} fn **/ + onCycleEnd(fn) { this._cycleEndCallbacks.push(fn) }; + + _ops = { + end: () => { + this.dbg.currentMnemonic = 'END'; + this.running = false; + this._incrementIP(2); + }, + + store_lit: (lit) => { + this.dbg.currentMnemonic = 'STO lit'; + this.memory[lit] = this.acc; + this._incrementIP(2); + }, + + store_addr: (addr) => { + this.dbg.currentMnemonic = `STO addr; @addr: $${num2hex(this.memory[addr])}`; + this.memory[this.memory[addr]] = this.acc; + this._incrementIP(2); + }, + + load_lit: (lit) => { + this.dbg.currentMnemonic = 'LDA lit'; + this.acc = lit; + this._updateFlagNegative(); + this._updateFlagZero(); + this._incrementIP(2); + }, + + load_addr: (addr) => { + this.dbg.currentMnemonic = `LDA addr; @ addr: $${num2hex(this.memory[addr])}`; + this.acc = this.memory[addr]; + this._updateFlagNegative(); + this._updateFlagZero(); + this._incrementIP(2); + }, + + add_lit: (lit) => { + this.dbg.currentMnemonic = 'ADD lit'; + const [sum, carry, overflow] = sumCarryOverflow(this.acc, lit); + this.acc = sum; + this.flags.C = carry; + this.flags.O = overflow; + this._updateFlagNegative(); + this._updateFlagZero(); + this._incrementIP(2); + }, + + add_addr: (addr) => { + this.dbg.currentMnemonic = `ADD addr; @ addr: $${num2hex(this.memory[addr])}`; + const [sum, carry, overflow] = sumCarryOverflow(this.acc, this.memory[addr]); + this.acc = sum; + this.flags.C = carry; + this.flags.O = overflow; + this._updateFlagNegative(); + this._updateFlagZero(); + this._incrementIP(2); + }, + + sub_lit: (lit) => { + this.dbg.currentMnemonic = 'SUB lit'; + const [difference, carry, overflow] = differenceCarryOverflow(this.acc, lit); + this.acc = difference; + this.flags.C = carry; + this.flags.O = overflow; + this._updateFlagNegative(); + this._updateFlagZero(); + this._incrementIP(2); + }, + + sub_addr: (addr) => { + this.dbg.currentMnemonic = `SUB addr; @ addr: $${num2hex(this.memory[addr])}`; + const [difference, carry, overflow] = differenceCarryOverflow(this.acc, this.memory[addr]); + this.acc = difference; + this.flags.C = carry; + this.flags.O = overflow; + this._updateFlagNegative(); + this._updateFlagZero(); + this._incrementIP(2); + }, + + hop_lit: (lit) => { + this.dbg.currentMnemonic = `HOP lit; IP+2: $${this.memory[this.IP+2]}, IP+3: $${this.memory[this.IP+3]}`; + if (this.acc === lit) { + this._incrementIP(4); + } else { + this._incrementIP(2); + } + }, + + hop_addr: (addr) => { + this.dbg.currentMnemonic = 'HOP addr'; + if (this.acc === this.memory[addr]) { + this._incrementIP(4); + } else { + this._incrementIP(2); + } + }, + + jump_lit: (lit) => { + this.dbg.currentMnemonic = 'JMP lit'; + this._setIP(lit); + }, + + jump_addr: (addr) => { + this.dbg.currentMnemonic = 'JMP addr'; + this._setIP(this.memory[addr]); + }, + + flag_toggle: (flagNum) => { + if (flagNum === null) { + let info = this.dbg.sourceInfo[this.IP]; + throw new Error(`Invalid flag number: '${flagNum}' on line ${info.lineNumber}: ${info.source}`); + } + const flagName = this.flagNums[flagNum]; + this.dbg.currentMnemonic = `FTG ${flagName}`; + this.flags[flagName] = !this.flags[flagName]; + this._incrementIP(2); + }, + + flag_hop: (flagNum) => { + if (flagNum === null) { + console.error('Invalid flag number'); + process.exit(); + } + const flagName = this.flagNums[flagNum]; + this.dbg.currentMnemonic = + `FHP ${flagName}; IP+2: ${this.memory[this.IP+2]}, IP+3: ${this.memory[this.IP+3]}`; + if (this.flags[this.flagNums[flagNum]]) { + this._incrementIP(4); + } else { + this._incrementIP(2); + } + }, + + no_op: () => { + this.dbg.currentMnemonic = `NOP`; + this._incrementIP(2); + }, + } + + _nums2mnems = { + 0: "end", + 1: "store_lit", + 2: "store_addr", + 3: "load_lit", + 4: "load_addr", + 5: "add_lit", + 6: "add_addr", + 7: "sub_lit", + 8: "sub_addr", + 9: "hop_lit", + 10: "hop_addr", + 11: "jump_lit", + 12: "jump_addr", + 13: "flag_toggle", + 14: "flag_hop", + 15: "no_op", + } + + _failInvalidOpcode() { + let info = this.dbg.sourceInfo[this.dbg.previousIP]; + console.error(); + console.error(`Error: Invalid opcode`); + console.error(` Executing $${num2hex(info.machine[0])} $${num2hex(info.machine[1])}`); + console.error(` from line ${info.lineNumber}: ${info.source}`); process.exit(); - } else { - CPU.currentInstruction.opcode = CPU.memory[CPU.IP]; - CPU.currentInstruction.operand = CPU.memory[CPU.IP+1]; - let executeInstruction = opcodes2mnemonics[CPU.currentInstruction.opcode]; - if (typeof executeInstruction === 'undefined') { - let info = debugInfo[CPU.previousIP]; - console.error(); - console.error(`Error: Invalid opcode`); - console.error(` Executing $${num2hex(info.machine[0])} $${num2hex(info.machine[1])}`); - console.error(` from line ${info.lineNumber}: ${info.source}`); - process.exit(); - } - executeInstruction(CPU.currentInstruction.operand); - CPU.cycleCounter += 1; - } - logCPUState(debugInfo, debug, prettyPrintDisplay); - if (DEFAULT_CYCLE_LIMIT) { // Temporary limit as a lazy way to halt infinite loops - if (CPU.cycleCounter >= DEFAULT_CYCLE_LIMIT) { - console.warn(' HALTING - reached cycle limit'); - CPU.running = false; - } - } - if (!CPU.running) process.exit(); -} - -/** - * @param {Uint8Array} code - Machine code to run - * @param {Object} debugInfo TODO type - * @param {Boolean} [debug] - Enable/disable debugging printouts - * @param {Boolean} [singleStep] - * @param {Boolean} [prettyPrint] - Print display with black and white emoji, instead of in hex - * @param {Number} [clockSpeed] - CPU clock speed in milliseconds - **/ -exports.runProgram = - (code, debugInfo, debug=false, singleStep=false, prettyPrint=false, clockSpeed=100) => { - if (singleStep) { - this.singleStepProgram(code, debugInfo, debug, prettyPrint); - } else { - startCPU(code); - // Animate the output by pausing between steps - const loop = setInterval(async () => { - stepCPU(debugInfo, debug, prettyPrint); - if (!CPU.running) { - logCPUState(debugInfo, debug, prettyPrint); - console.log('Halted'); - process.exit(); - } - }, clockSpeed); - } - }; - -/** - * @param {Uint8Array} code - Machine code to run - * @param {any} debugInfo - TODO - * @param {Boolean} [debug] - Enable/disable debugging printouts - * @param {Boolean} [prettyPrintDisplay] - Print display using black and white emoji - **/ -exports.singleStepProgram = (code, debugInfo, debug = false, prettyPrintDisplay = false) => { - startCPU(code); - while (CPU.running) { - stepCPU(debugInfo, debug, prettyPrintDisplay); - // FIXME: this prevents exiting with Ctrl+C: - let key = readlineSync.keyIn('S to step, Q to quit > ', { - limit: ['s', 'S', 'q', 'Q'], - }); - if (key.toLowerCase() === 'q') { process.exit(); } - console.log(); } } +/** + * @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; + } -// FUNCTIONS THAT PULL INFO FROM STATE TO DISPLAY + 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]; +} /** - * @param {Boolean} [debug] - Enable/disable debugging printouts + * @arg {number} n1 + * @arg {number} n2 + * @returns {[number, boolean, boolean]} [sum, carry, overflow] **/ -function logCPUState(debugInfo, debug = false, prettyPrintDisplay = false) { - debugInfo = debugInfo[CPU.previousIP] !== 'undefined' ? debugInfo[CPU.previousIP] : false; - console.group(`Step ${CPU.cycleCounter}`); - console.log(); - if (!debug) console.clear(); - display.show(CPU.memory, prettyPrintDisplay); - console.log(); - if (debugInfo) { - console.log(`Line ${debugInfo.lineNumber}: ${debugInfo.source}`); - console.log(); - } - console.log('Mnemonic:', CPU.currentInstruction.mnemonic); - console.log(`Machine: $${num2hex(CPU.currentInstruction.opcode)} $${num2hex(CPU.currentInstruction.operand)}`); - console.log(); - console.log(`IP: $${num2hex(CPU.IP)} Acc: $${num2hex(CPU.Acc)} ONZC ${bool2bit(CPU.FLAGS.O)}${bool2bit(CPU.FLAGS.N)}${bool2bit(CPU.FLAGS.Z)}${bool2bit(CPU.FLAGS.C)}`); - console.log(`KEY: $${num2hex(CPU.memory[KEYPAD_ADDR])}  ${CPU.running ? "running" : "halted" }`); - console.log(); - console.log(); - console.groupEnd(); -}; \ No newline at end of file +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/src/dbg.js b/src/dbg.js new file mode 100644 index 0000000..cb585dd --- /dev/null +++ b/src/dbg.js @@ -0,0 +1,113 @@ +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 { + throw new Error(`'${level}' is not a valid debug level`); + } + } + + /** @param {any} s **/ + warn = (s='', ...z) => { + if (this._lvl2num('warn') < this._lvl2num(this._level)) return + console.log(s, ...z); + } + /** @param {any} s **/ + i = (s='', ...z) => { + if (this._lvl2num('info') < this._lvl2num(this._level)) return + console.log(s, ...z); + } + /** @param {any} s **/ + d = (s='', ...z) => { + if (this._lvl2num('debug') < this._lvl2num(this._level)) return + console.log(s, ...z); + } + /** @param {any} s **/ + nit = (s='', ...z) => { + if (this._lvl2num('nitpick') < this._lvl2num(this._level)) return + console.log(s, ...z); + } + + 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); + } + + 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(); + } + + 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) { + 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 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..ec7f413 --- /dev/null +++ b/src/io.js @@ -0,0 +1,44 @@ +const readline = require('readline'); + +const CFG = require('./machine.config.js'); +const { num2hex } = require('./logging.js'); + +function readKeyMem(mem) { + return mem[CFG.keypadAddr]; +} + +function getKeypadInput(cpu) { + readline.emitKeypressEvents(process.stdin); + if (process.stdin.setRawMode != null) { + process.stdin.setRawMode(true); + } + process.stdin.on('keypress', (str, key) => { + if (key.sequence === '\x03') process.exit(); + let name = key.name.toUpperCase(); + if (name in CFG.keyMap) { + cpu.memory[CFG.keypadAddr] = CFG.keyMap[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.pointerToDisplay]; + 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 diff --git a/src/logging.js b/src/logging.js index 39ee03e..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. @@ -15,7 +17,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)} │`); } @@ -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 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 diff --git a/src/opter b/src/opter new file mode 160000 index 0000000..1d98a07 --- /dev/null +++ b/src/opter @@ -0,0 +1 @@ +Subproject commit 1d98a0707c3e61e362d2d3d5413b475437b5de0e 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" }, 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 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; -} diff --git a/test-programs/flag-overflow-2.asm b/test-programs/flag-overflow-2.asm new file mode 100644 index 0000000..741a647 --- /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