├── src ├── compiler │ └── index.js ├── common │ ├── bitness.constant.js │ ├── log-level.constant.js │ ├── output-providers │ │ └── console-log.provider.js │ ├── memory.serializer.js │ ├── format.js │ ├── logger.js │ ├── memory.serializer.test.js │ ├── format.test.js │ └── logger.test.js ├── vm │ ├── virtual-machine.test.js │ ├── text-screen.js │ ├── virtual-machine.js │ ├── registers.js │ ├── cpu.js │ ├── text-screen.test.js │ ├── memory.test.js │ ├── memory.js │ ├── registers.test.js │ ├── cpu.test.js │ ├── memory-mapper.js │ ├── memory-mapper.test.js │ ├── alu.js │ └── alu.test.js ├── core │ ├── instruction.test.js │ ├── register.js │ └── instruction.js └── architecture │ └── sample │ ├── register.constant.js │ └── instruction.constant.js ├── .gitignore ├── assets ├── bios │ ├── seabios.bin │ ├── vgabios.bin │ └── bochs-bios.bin ├── datasheets │ ├── 8086.pdf │ ├── 8080 Programmers Manual.pdf │ ├── Atmel-0856-AVR-Instruction-Set-Manual.pdf │ └── Atmel-2549-8-bit-AVR-Microcontroller-ATmega640-1280-1281-2560-2561_datasheet.pdf └── images │ ├── freedos722.img │ └── windows101.img ├── .github └── workflows │ ├── build.yml │ ├── tests.yml │ └── linter.yml ├── package.json ├── LICENSE ├── .eslintrc.json └── README.md /src/compiler/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | coverage 4 | dist 5 | -------------------------------------------------------------------------------- /assets/bios/seabios.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitalyTartynov/jsvm/HEAD/assets/bios/seabios.bin -------------------------------------------------------------------------------- /assets/bios/vgabios.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitalyTartynov/jsvm/HEAD/assets/bios/vgabios.bin -------------------------------------------------------------------------------- /assets/bios/bochs-bios.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitalyTartynov/jsvm/HEAD/assets/bios/bochs-bios.bin -------------------------------------------------------------------------------- /assets/datasheets/8086.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitalyTartynov/jsvm/HEAD/assets/datasheets/8086.pdf -------------------------------------------------------------------------------- /assets/images/freedos722.img: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitalyTartynov/jsvm/HEAD/assets/images/freedos722.img -------------------------------------------------------------------------------- /assets/images/windows101.img: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitalyTartynov/jsvm/HEAD/assets/images/windows101.img -------------------------------------------------------------------------------- /assets/datasheets/8080 Programmers Manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitalyTartynov/jsvm/HEAD/assets/datasheets/8080 Programmers Manual.pdf -------------------------------------------------------------------------------- /assets/datasheets/Atmel-0856-AVR-Instruction-Set-Manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitalyTartynov/jsvm/HEAD/assets/datasheets/Atmel-0856-AVR-Instruction-Set-Manual.pdf -------------------------------------------------------------------------------- /assets/datasheets/Atmel-2549-8-bit-AVR-Microcontroller-ATmega640-1280-1281-2560-2561_datasheet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitalyTartynov/jsvm/HEAD/assets/datasheets/Atmel-2549-8-bit-AVR-Microcontroller-ATmega640-1280-1281-2560-2561_datasheet.pdf -------------------------------------------------------------------------------- /src/common/bitness.constant.js: -------------------------------------------------------------------------------- 1 | const BYTE_SIZE = 1; 2 | const WORD_SIZE = 2; 3 | const DWORD_SIZE = 4; 4 | const QWORD_SIZE = 8; 5 | 6 | module.exports = { 7 | BYTE_SIZE, 8 | WORD_SIZE, 9 | DWORD_SIZE, 10 | QWORD_SIZE 11 | }; 12 | -------------------------------------------------------------------------------- /src/common/log-level.constant.js: -------------------------------------------------------------------------------- 1 | const LOGLEVEL = Object.freeze({ 2 | 'ALL' :0, 3 | 'DEBUG' :1, 4 | 'INFO' :2, 5 | 'WARN' :3, 6 | 'ERROR' :4, 7 | 'FATAL' :5, 8 | 'OFF' :6 9 | }); 10 | 11 | module.exports = LOGLEVEL; 12 | -------------------------------------------------------------------------------- /src/vm/virtual-machine.test.js: -------------------------------------------------------------------------------- 1 | const VirtualMachine = require('./virtual-machine'); 2 | 3 | describe('VirtualMachine', () => { 4 | let virtualMachine; 5 | 6 | beforeEach(() => { 7 | virtualMachine = new VirtualMachine(); 8 | }); 9 | 10 | test('should be created', () => { 11 | expect(virtualMachine).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/vm/text-screen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Text screen device. 3 | */ 4 | class TextScreen { 5 | constructor() { 6 | } 7 | 8 | getUint8() { } 9 | setUint8() { } 10 | getUint16() { } 11 | setUint16(address, value) { 12 | const character = String.fromCharCode(value); 13 | console.log(character); 14 | } 15 | } 16 | 17 | module.exports = TextScreen; 18 | -------------------------------------------------------------------------------- /src/core/instruction.test.js: -------------------------------------------------------------------------------- 1 | const INSTRUCTION = require('../architecture/sample/instruction.constant'); 2 | 3 | describe('Instruction', () => { 4 | test('should be created', () => { 5 | expect(INSTRUCTION.NOP).toBeTruthy(); 6 | }); 7 | 8 | test('should have debug info', () => { 9 | const info = INSTRUCTION.NOP.toString(); 10 | 11 | expect(info).toBeTruthy(); 12 | expect(info).toEqual('NOP \t: 0x00 \t: NO OPERATION'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm build 22 | env: 23 | CI: true 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | env: 23 | CI: true 24 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm run lint 22 | env: 23 | CI: true 24 | -------------------------------------------------------------------------------- /src/core/register.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CPU register information. 3 | */ 4 | class Register { 5 | /** 6 | * Register instance. 7 | * @param {string} name Human-friendly name of register. 8 | * @param {number} address Address of register in internal register memory. 9 | * @param {string} description Some description about register. 10 | */ 11 | constructor(name, address, description) { 12 | this.name = name; 13 | this.address = address; 14 | this.description = description; 15 | } 16 | } 17 | 18 | module.exports = Register; 19 | -------------------------------------------------------------------------------- /src/vm/virtual-machine.js: -------------------------------------------------------------------------------- 1 | const Cpu = require('./cpu'); 2 | const Alu = require('./alu'); 3 | const Memory = require('./memory'); 4 | const Registers = require('./registers'); 5 | 6 | const REGISTER = require('../architecture/sample/register.constant'); 7 | 8 | /** 9 | * Virtual machine. 10 | */ 11 | class VirtualMachine { 12 | constructor(ramSize = 64) { 13 | this.ram = new Memory(ramSize); 14 | this.registers = new Registers(REGISTER.ALL); 15 | this.alu = new Alu(this.ram, this.registers); 16 | this.flash = new Memory(0); 17 | this.cpu = new Cpu(this.ram, this.registers, this.alu, this.flash); 18 | } 19 | } 20 | 21 | module.exports = VirtualMachine; 22 | -------------------------------------------------------------------------------- /src/architecture/sample/register.constant.js: -------------------------------------------------------------------------------- 1 | const Register = require('../../core/register'); 2 | 3 | const IP = new Register('IP', 0x00, 'Instruction Pointer'); 4 | const AC = new Register('AC', 0x02, 'Accumulator'); 5 | const SP = new Register('SP', 0x04, 'Stack Pointer'); 6 | const R1 = new Register('R1', 0x06, 'General Purpose register 1'); 7 | const R2 = new Register('R2', 0x08, 'General Purpose register 2'); 8 | const R3 = new Register('R3', 0x0A, 'General Purpose register 3'); 9 | const R4 = new Register('R4', 0x0C, 'General Purpose register 4'); 10 | 11 | const ALL = [IP, AC, SP, R1, R2, R3, R4]; 12 | 13 | module.exports = { 14 | IP, 15 | AC, 16 | SP, 17 | R1, 18 | R2, 19 | R3, 20 | R4, 21 | ALL 22 | }; 23 | -------------------------------------------------------------------------------- /src/core/instruction.js: -------------------------------------------------------------------------------- 1 | const format = require('../common/format'); 2 | 3 | /** 4 | * CPU instruction information. 5 | */ 6 | class Instruction { 7 | /** 8 | * CPU instruction instance. 9 | * @param {number} opcode Operation code. 10 | * @param {string} mnemonic Assemble command. 11 | * @param {string} description Some description about instruction and operands. 12 | */ 13 | constructor(opcode, mnemonic, description) { 14 | this.mnemonic = mnemonic; 15 | this.opcode = opcode; 16 | this.description = description; 17 | } 18 | 19 | toString() { 20 | return `${this.mnemonic} \t: ${format.asByte(this.opcode)} \t: ${this.description}`; 21 | } 22 | } 23 | 24 | module.exports = Instruction; 25 | -------------------------------------------------------------------------------- /src/vm/registers.js: -------------------------------------------------------------------------------- 1 | const Memory = require('./memory'); 2 | 3 | const format = require('../common/format'); 4 | 5 | /** 6 | * CPU register set. 7 | */ 8 | class Registers { 9 | constructor(registerNames) { 10 | this._registers = registerNames; 11 | this._memory = new Memory(this._registers.length * 2); 12 | } 13 | 14 | get(address) { 15 | return this._memory.getUint16(address); 16 | } 17 | 18 | set(address, newValue) { 19 | this._memory.setUint16(address, newValue); 20 | } 21 | 22 | debug() { 23 | let result = ''; 24 | this._registers.forEach(register => { 25 | result += `${register.name}: 0x${format.asWord(this.get(register.address))}\n`; 26 | }); 27 | 28 | return result; 29 | } 30 | } 31 | 32 | module.exports = Registers; 33 | -------------------------------------------------------------------------------- /src/vm/cpu.js: -------------------------------------------------------------------------------- 1 | const REGISTER = require('../architecture/sample/register.constant'); 2 | 3 | /** 4 | * Central processor unit. 5 | */ 6 | class Cpu { 7 | constructor(ram, registers, alu, flash) { 8 | this.ram = ram; 9 | this.registers = registers; 10 | this.alu = alu; 11 | this.flash = flash; 12 | 13 | // VM 16 bit, we should have ability to PUSH 2 bytes to stack. 14 | this.stackPointerInitial = this.ram.length - 2; 15 | this.registers.set(REGISTER.SP.address, this.stackPointerInitial); 16 | } 17 | 18 | tick() { 19 | const opcode = this.alu.fetch8(); 20 | 21 | return this.alu.execute(opcode); 22 | } 23 | 24 | run() { 25 | let isHalt = false; 26 | do { 27 | isHalt = this.tick(); 28 | } while (!isHalt); 29 | } 30 | } 31 | 32 | module.exports = Cpu; 33 | -------------------------------------------------------------------------------- /src/vm/text-screen.test.js: -------------------------------------------------------------------------------- 1 | const MemoryMapper = require('./memory-mapper'); 2 | const Memory = require('./memory'); 3 | const TextScreen = require('./text-screen'); 4 | 5 | describe('TextScreen', () => { 6 | const ramSize = 64; 7 | let memoryMapper; 8 | let ram; 9 | let textScreen; 10 | 11 | beforeEach(() => { 12 | ram = new Memory(ramSize); 13 | memoryMapper = new MemoryMapper(); 14 | textScreen = new TextScreen(); 15 | 16 | memoryMapper.map(ram, 0, ramSize); 17 | memoryMapper.map(textScreen, 0x0010, 0x0020, true); 18 | }); 19 | 20 | test('should be created', () => { 21 | expect(textScreen).toBeTruthy(); 22 | }); 23 | 24 | test('should log characters via text screen', () => { 25 | memoryMapper.setUint16(0x0010, 'G'.charCodeAt(0)); 26 | memoryMapper.setUint16(0x0010, 'O'.charCodeAt(0)); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsvm", 3 | "version": "1.0.0", 4 | "description": "16 bit js virtual machine implementation", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "browserify ./src/vm/virtual-machine.js -o ./dist/vm.bundle.js -d", 8 | "test": "jest", 9 | "lint": "eslint --ext .js src/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/VitalyTartynov/jsvm.git" 14 | }, 15 | "keywords": [ 16 | "16bit", 17 | "assembler", 18 | "vm" 19 | ], 20 | "author": "Vitaly.Tartynov", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/VitalyTartynov/jsvm/issues" 24 | }, 25 | "homepage": "https://github.com/VitalyTartynov/jsvm#readme", 26 | "devDependencies": { 27 | "browserify": "^17.0.0", 28 | "eslint": "^7.17.0", 29 | "jest": "^27.0.6", 30 | "minimist": "^1.2.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vitaly Tartynov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/vm/memory.test.js: -------------------------------------------------------------------------------- 1 | const Memory = require('./memory'); 2 | 3 | describe('Memory', () => { 4 | test('should be created', () => { 5 | const expectedSize = 16; 6 | const memory = new Memory(expectedSize); 7 | 8 | expect(memory).toBeTruthy(); 9 | expect(memory.length).toBe(expectedSize); 10 | }); 11 | 12 | test('should have debug memory view', () => { 13 | const expectedSize = 16; 14 | const memory = new Memory(expectedSize); 15 | memory.byteAt[5] = 0x64; 16 | 17 | expect(memory.debugAt).toBeDefined(); 18 | 19 | const result = memory.debugAt(0x0000); 20 | expect(result).toBe('0x0000: 0x00 0x00 0x00 0x00 0x00 0x64 0x00 0x00'); 21 | }); 22 | 23 | test('should have debug memory view with multiple lines', () => { 24 | const expectedSize = 16; 25 | const memory = new Memory(expectedSize); 26 | memory.byteAt[5] = 0x64; 27 | memory.byteAt[14] = 0x32; 28 | 29 | expect(memory.debugAt).toBeDefined(); 30 | 31 | const result = memory.debugAt(0x0000, 2); 32 | expect(result).toBe('0x0000: 0x00 0x00 0x00 0x00 0x00 0x64 0x00 0x00\n0x0008: 0x00 0x00 0x00 0x00 0x00 0x00 0x32 0x00'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "indent": [ 18 | "error", 19 | 2, 20 | { "SwitchCase" : 1 } 21 | ], 22 | "quotes": [ 23 | "error", 24 | "single" 25 | ], 26 | "semi": [ 27 | "error", 28 | "always" 29 | ], 30 | "eol-last": [ 31 | "error", 32 | "always" 33 | ], 34 | "no-var": "error", 35 | "valid-jsdoc": "error", 36 | "require-jsdoc": ["error", { 37 | "require": { 38 | "FunctionDeclaration": true, 39 | "MethodDefinition": false, 40 | "ClassDeclaration": true, 41 | "ArrowFunctionExpression": false, 42 | "FunctionExpression": false 43 | } 44 | }] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/common/output-providers/console-log.provider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Console provider for logging information. 3 | */ 4 | class ConsoleLogProvider { 5 | 6 | /** 7 | * Log debug message to console. 8 | * @param {string} message Debug message string. 9 | * @returns {undefined} 10 | */ 11 | debug(message) { 12 | console.debug(message); 13 | } 14 | 15 | /** 16 | * Log info message to console. 17 | * @param {string} message Info message string. 18 | * @returns {undefined} 19 | */ 20 | info(message) { 21 | console.info(message); 22 | } 23 | 24 | /** 25 | * Log warning message to console. 26 | * @param {string} message Warning message string. 27 | * @returns {undefined} 28 | */ 29 | warn(message) { 30 | console.warn(message); 31 | } 32 | 33 | /** 34 | * Log error message to console. 35 | * @param {string} message Error message string. 36 | * @returns {undefined} 37 | */ 38 | error(message) { 39 | console.error(message); 40 | } 41 | 42 | /** 43 | * Log fatal message to console. 44 | * @param {string} message Fatal message string. 45 | * @returns {undefined} 46 | */ 47 | fatal(message) { 48 | console.error(message); 49 | } 50 | } 51 | 52 | module.exports = ConsoleLogProvider; 53 | -------------------------------------------------------------------------------- /src/vm/memory.js: -------------------------------------------------------------------------------- 1 | const format = require('../common/format'); 2 | 3 | /** 4 | * Computer memory. 5 | */ 6 | class Memory { 7 | constructor(sizeInBytes) { 8 | const array = new ArrayBuffer(sizeInBytes); 9 | this._memory = new DataView(array); 10 | 11 | this.length = this._memory.buffer.byteLength; 12 | this.byteAt = new Uint8Array(this._memory.buffer); 13 | } 14 | 15 | getUint8(address) { 16 | return this._memory.getUint8(address); 17 | } 18 | 19 | getUint16(address) { 20 | return this._memory.getUint16(address); 21 | } 22 | 23 | setUint8(address, newValue) { 24 | this._memory.setUint8(address, newValue); 25 | } 26 | 27 | setUint16(address, newValue) { 28 | this._memory.setUint16(address, newValue); 29 | } 30 | 31 | debugAt(address, lines = 1) { 32 | let result = ''; 33 | const bytesInOneLine = 8; 34 | for (let i = 0; i < lines; i++) { 35 | const nextBytes = Array.from({length: bytesInOneLine}, (_, i) => this._memory.getUint8(address + i)).map(value => format.asByte(value)); 36 | result += `${format.asWord(address)}: ${nextBytes.join(' ')}`; 37 | if (i !== lines - 1) { 38 | result += '\n'; 39 | address += bytesInOneLine; 40 | } 41 | } 42 | 43 | return result; 44 | } 45 | } 46 | 47 | module.exports = Memory; 48 | -------------------------------------------------------------------------------- /src/common/memory.serializer.js: -------------------------------------------------------------------------------- 1 | const format = require('./format'); 2 | 3 | /** 4 | * Serialize/deserialize memory data to HEX string. 5 | * It's 'human-friendly' string with HEX information like '0x01 0x02 0xFF'. 6 | */ 7 | class HexMemorySerializer { 8 | constructor() { 9 | this.delimeter = ' '; 10 | } 11 | 12 | /** 13 | * Serialize data from memory to HEX string. 14 | * @param {Memory} memory Memory object. 15 | * @return {string} Serialized data. 16 | */ 17 | serialize(memory) { 18 | let result = ''; 19 | for (let i = 0; i < memory.length; i++) { 20 | result += format.asByte(memory.byteAt[i]); 21 | if (i !== memory.length - 1) { 22 | result += this.delimeter; 23 | } 24 | } 25 | 26 | return result; 27 | } 28 | 29 | /** 30 | * Deserialize HEX string to memory. 31 | * @param {string} data Serialized data. 32 | * @param {Memory} memory Memory object. 33 | * @returns {undefined} 34 | */ 35 | deserialize(data, memory) { 36 | const values = data.split(this.delimeter); 37 | 38 | if (values.length > memory.length) { 39 | throw new Error('Program is bigger than memory'); 40 | } 41 | 42 | for (let i = 0; i < memory.length; i++) { 43 | memory.byteAt[i] = values[i]; 44 | } 45 | } 46 | } 47 | 48 | module.exports = HexMemorySerializer; 49 | -------------------------------------------------------------------------------- /src/vm/registers.test.js: -------------------------------------------------------------------------------- 1 | const Registers = require('./registers'); 2 | 3 | const REGISTER = require('../architecture/sample/register.constant'); 4 | 5 | describe('Registers', () => { 6 | let registers; 7 | 8 | beforeEach(() => { 9 | registers = new Registers(REGISTER.ALL); 10 | }); 11 | 12 | test('should be created', () => { 13 | expect(registers).toBeTruthy(); 14 | }); 15 | 16 | test('should contain register names', () => { 17 | const internalRegisters = registers._registers; 18 | 19 | expect(internalRegisters).toBeTruthy(); 20 | expect(internalRegisters.length).toBeGreaterThan(0); 21 | }); 22 | 23 | test('should contain register values', () => { 24 | const registersMemory = registers._memory; 25 | const registersCount = registers._registers.length; 26 | 27 | expect(registersMemory).toBeTruthy(); 28 | expect(registersMemory.length).toEqual(registersCount * 2); 29 | }); 30 | 31 | test('should have debug registers view', () => { 32 | expect(registers.debug).toBeDefined(); 33 | 34 | const result = registers.debug(); 35 | 36 | expect(result).toBe('IP: 0x0x0000\n' + 37 | 'AC: 0x0x0000\n' + 38 | 'SP: 0x0x0000\n' + 39 | 'R1: 0x0x0000\n' + 40 | 'R2: 0x0x0000\n' + 41 | 'R3: 0x0x0000\n' + 42 | 'R4: 0x0x0000\n'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/common/format.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Format byte value in HEX. 3 | * @param {number} value Value in DEC. 4 | * @returns {string} Result HEX value as string. 5 | */ 6 | function asByte(value) { 7 | return _format(value, 2); 8 | } 9 | 10 | /** 11 | * Format word value in HEX. 12 | * @param {number} value Value in DEC. 13 | * @returns {string} Result HEX value as string. 14 | */ 15 | function asWord(value) { 16 | return _format(value, 4); 17 | } 18 | 19 | /** 20 | * Format dword value in HEX. 21 | * @param {number} value Value in DEC. 22 | * @returns {string} Result HEX value as string. 23 | */ 24 | function asDword(value) { 25 | return _format(value, 8); 26 | } 27 | 28 | /** 29 | * Format qword value in HEX. 30 | * @param {number} value Value in DEC. 31 | * @returns {string} Result HEX value as string. 32 | */ 33 | function asQword(value) { 34 | return _format(value, 16); 35 | } 36 | 37 | /** 38 | * Format value to HEX with base. 39 | * @param {number} value Value in DEC. 40 | * @param {number} base 2 for byte, 4 for word, 8 for dword. 41 | * @returns {string} Result HEX value as string. 42 | * @private 43 | */ 44 | function _format(value, base) { 45 | if (value < 0) { 46 | throw new Error('Negative values not allowed'); 47 | } 48 | 49 | let hexValue = value.toString(16) 50 | .padStart(base, '0') 51 | .toUpperCase(); 52 | 53 | if (hexValue.length > base) { 54 | hexValue = hexValue.substring(hexValue.length - base); 55 | } 56 | 57 | return `0x${hexValue}`; 58 | } 59 | 60 | module.exports = { 61 | asByte, 62 | asWord, 63 | asDword, 64 | asQword 65 | }; 66 | -------------------------------------------------------------------------------- /src/architecture/sample/instruction.constant.js: -------------------------------------------------------------------------------- 1 | const Instruction = require('../../core/instruction'); 2 | 3 | const NOP = new Instruction(0x00, 'NOP', 'NO OPERATION'); 4 | const MOV_LIT_REG = new Instruction(0x10, 'MOV', 'MOVE WORD Rx -> Rx'); 5 | const MOV_REG_REG = new Instruction(0x11, 'MOV', 'MOVE Rx Ry -> Ry'); 6 | const MOV_REG_MEM = new Instruction(0x12, 'MOV', 'MOVE Rx 0x**** -> 0x****'); 7 | const MOV_MEM_REG = new Instruction(0x13, 'MOV', 'MOVE 0x**** Rx -> Rx'); 8 | const ADD_REG_REG = new Instruction(0x14, 'ADD', 'ADD Rx Ry -> ACC'); 9 | const SUB_REG_REG = new Instruction(0x15, 'SUB', 'SUB Rx Ry -> ACC'); 10 | const JMP_EQ = new Instruction(0x16, 'JMP', 'JMP WORD 0x****'); 11 | const JMP_NOT_EQ = new Instruction(0x17, 'JNE', 'JNE WORD 0x****'); 12 | const PSH_LIT = new Instruction(0x18, 'PUSH', 'PUSH WORD -> STACK'); 13 | const PSH_REG = new Instruction(0x19, 'PUSH', 'PUSH Rx -> STACK'); 14 | const POP = new Instruction(0x1A, 'POP', 'POP Rx <- STACK'); 15 | const JMP = new Instruction(0x1B, 'JMP', 'JUMP WORD'); 16 | const CALL = new Instruction(0x1C, 'CALL', 'CALL 0x****'); 17 | const RET = new Instruction(0x1D, 'RET', 'RETURN'); 18 | 19 | const UNKNOWN = new Instruction(0xBC, 'UNKNOWN', 'UNKNOWN INSTRUCTION'); 20 | const HLT = new Instruction(0xFF, 'HALT', 'HALT'); 21 | 22 | module.exports = { 23 | NOP, 24 | MOV_LIT_REG, 25 | MOV_REG_REG, 26 | MOV_REG_MEM, 27 | MOV_MEM_REG, 28 | ADD_REG_REG, 29 | SUB_REG_REG, 30 | JMP_EQ, 31 | JMP_NOT_EQ, 32 | PSH_LIT, 33 | PSH_REG, 34 | POP, 35 | JMP, 36 | CALL, 37 | RET, 38 | UNKNOWN, 39 | HLT 40 | }; 41 | -------------------------------------------------------------------------------- /src/common/logger.js: -------------------------------------------------------------------------------- 1 | const LOGLEVEL = require('./log-level.constant'); 2 | 3 | /** 4 | * Log information using selected provider. 5 | */ 6 | class Logger { 7 | constructor(outputProvider, logLevel = LOGLEVEL.ERROR) { 8 | this.outputProvider = outputProvider; 9 | this.logLevel = logLevel; 10 | } 11 | 12 | /** 13 | * Log debug message. 14 | * @param {string} message Debug message string. 15 | * @returns {undefined} 16 | */ 17 | debug(message) { 18 | if (this.logLevel <= LOGLEVEL.DEBUG) { 19 | this.outputProvider.debug(message); 20 | } 21 | } 22 | 23 | /** 24 | * Log info message. 25 | * @param {string} message Info message string. 26 | * @returns {undefined} 27 | */ 28 | info(message) { 29 | if (this.logLevel <= LOGLEVEL.INFO) { 30 | this.outputProvider.info(message); 31 | } 32 | } 33 | 34 | /** 35 | * Log warning message. 36 | * @param {string} message Warning message string. 37 | * @returns {undefined} 38 | */ 39 | warn(message) { 40 | if (this.logLevel <= LOGLEVEL.WARN) { 41 | this.outputProvider.warn(message); 42 | } 43 | } 44 | 45 | /** 46 | * Log error message. 47 | * @param {string} message Error message string. 48 | * @returns {undefined} 49 | */ 50 | error(message) { 51 | if (this.logLevel <= LOGLEVEL.ERROR) { 52 | this.outputProvider.error(message); 53 | } 54 | } 55 | 56 | /** 57 | * Log fatal message. 58 | * @param {string} message Fatal message string. 59 | * @returns {undefined} 60 | */ 61 | fatal(message) { 62 | if (this.logLevel <= LOGLEVEL.FATAL) { 63 | this.outputProvider.fatal(message); 64 | } 65 | } 66 | } 67 | 68 | module.exports = Logger; 69 | -------------------------------------------------------------------------------- /src/common/memory.serializer.test.js: -------------------------------------------------------------------------------- 1 | const Memory = require('../vm/memory'); 2 | const HexMemorySerializer = require('./memory.serializer'); 3 | 4 | describe('Hex memory loader', () => { 5 | let memory; 6 | let serializer; 7 | 8 | beforeEach(() => { 9 | memory = new Memory(8); 10 | serializer = new HexMemorySerializer(); 11 | }); 12 | 13 | test('should save HEX bytecode to string', () => { 14 | memory.byteAt[0] = 0x01; 15 | memory.byteAt[1] = 0x23; 16 | memory.byteAt[2] = 0x45; 17 | memory.byteAt[3] = 0x67; 18 | memory.byteAt[4] = 0x89; 19 | memory.byteAt[5] = 0xAB; 20 | memory.byteAt[6] = 0xCD; 21 | memory.byteAt[7] = 0xEF; 22 | 23 | const result = serializer.serialize(memory); 24 | 25 | expect(result).toBeTruthy(); 26 | expect(result).toEqual('0x01 0x23 0x45 0x67 0x89 0xAB 0xCD 0xEF'); 27 | }); 28 | 29 | test('should load HEX bytecode from string', () => { 30 | const data = '0x01 0x23 0x45 0x67 0x89 0xAB 0xCD 0xEF'; 31 | 32 | serializer.deserialize(data, memory); 33 | 34 | expect(memory.byteAt[0]).toBe(0x01); 35 | expect(memory.byteAt[1]).toBe(0x23); 36 | expect(memory.byteAt[2]).toBe(0x45); 37 | expect(memory.byteAt[3]).toBe(0x67); 38 | expect(memory.byteAt[4]).toBe(0x89); 39 | expect(memory.byteAt[5]).toBe(0xAB); 40 | expect(memory.byteAt[6]).toBe(0xCD); 41 | expect(memory.byteAt[7]).toBe(0xEF); 42 | }); 43 | 44 | test('should throw error when HEX bytecode bigger than memory', () => { 45 | // eslint-disable-next-line require-jsdoc 46 | function loadBiggerBytecode() { 47 | const data = '0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00'; 48 | serializer.load(data, memory); 49 | } 50 | 51 | expect(loadBiggerBytecode).toThrowError(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSVM 2 | ![Node.js CI](https://github.com/VitalyTartynov/jsvm/workflows/Build/badge.svg) 3 | ![Node.js CI](https://github.com/VitalyTartynov/jsvm/workflows/Linter/badge.svg) 4 | ![Node.js CI](https://github.com/VitalyTartynov/jsvm/workflows/Tests/badge.svg) 5 | 6 | 16 bit js virtual machine implementation 7 | 8 | ## Memory model 9 | Virtual CPU use: 10 | * RAM memory (memory size can be changed). Flat model, 0x0000 - 0xFFFF (Up to 64Kb). 11 | * Internal registers memory (access only via registers). 12 | 13 | Flash memory now not used. 14 | 15 | ## Register set 16 | Actual information can be found [here](./src/core/register.constant.js). 17 | * IP - Instruction pointer. Register value contains memory address of next command for executing. 18 | * AC - Accumulator. 19 | * SP - Stack pointer. Register value contains memory address of stack head. 20 | * Rx - some of general purpose registers. 21 | 22 | ## Program execution 23 | After CPU start IP register contain `0x0000` address, fetch instruction from RAM and start executing. 24 | 25 | ## Stack 26 | After CPU start SP register contain `RAM memory size - 2` address. 27 | After push some data SP value decreased by 2 bytes and after pop - increased by 2 bytes. 28 | 29 | ## Instruction set 30 | Actual information can be found [here](./src/core/instruction.constant.js). 31 | 32 | | Opcode | Command | Arguments | Sample | Description | 33 | | ------ | ------- | ------------ | ------------- | ----------------------------------- | 34 | | 0x00 | `NOP` | | | No operation | 35 | | 0x?? | `PUSH` | `0x????` | `PUSH 0x1234` | Push 16 bit constant to stack | 36 | | 0x?? | `PUSH` | `Rx` | `PUSH R4` | Push register value to stack | 37 | | 0x?? | `POP` | `Rx` | `POP R1` | Pop value from stack to register | 38 | | 0xFF | `HLT` | | | Halt | 39 | -------------------------------------------------------------------------------- /src/vm/cpu.test.js: -------------------------------------------------------------------------------- 1 | const Memory = require('./memory'); 2 | const Registers = require('./registers'); 3 | const Alu = require('./alu'); 4 | const Cpu = require('./cpu'); 5 | 6 | const INSTRUCTION = require('../architecture/sample/instruction.constant'); 7 | const REGISTER = require('../architecture/sample/register.constant'); 8 | 9 | describe('CPU', () => { 10 | const ramSize = 64; 11 | const flashSize = 32; 12 | 13 | let ram; 14 | let registers; 15 | let alu; 16 | let flash; 17 | let cpu; 18 | 19 | beforeEach(() => { 20 | ram = new Memory(ramSize); 21 | registers = new Registers(REGISTER.ALL); 22 | alu = new Alu(ram, registers); 23 | flash = new Memory(flashSize); 24 | cpu = new Cpu(ram, registers, alu, flash); 25 | }); 26 | 27 | test('should be created', () => { 28 | expect(cpu).toBeTruthy(); 29 | }); 30 | 31 | test('should contain ram, registers, alu and flash', () => { 32 | expect(cpu.ram).toBeTruthy(); 33 | expect(cpu.registers).toBeTruthy(); 34 | expect(cpu.alu).toBeTruthy(); 35 | expect(cpu.flash).toBeTruthy(); 36 | }); 37 | 38 | test('stack pointer should point to end of memory after start', () => { 39 | expect(cpu.registers.get(REGISTER.SP.address)).toBe(cpu.stackPointerInitial); 40 | }); 41 | 42 | test('should run program until HALT instruction', () => { 43 | ram.byteAt[0] = INSTRUCTION.MOV_LIT_REG.opcode; 44 | ram.byteAt[1] = 0x12; 45 | ram.byteAt[2] = 0x34; 46 | ram.byteAt[3] = REGISTER.R1.address; 47 | 48 | ram.byteAt[4] = INSTRUCTION.MOV_LIT_REG.opcode; 49 | ram.byteAt[5] = 0x56; 50 | ram.byteAt[6] = 0x78; 51 | ram.byteAt[7] = REGISTER.R2.address; 52 | 53 | ram.byteAt[8] = INSTRUCTION.HLT.opcode; 54 | 55 | cpu.run(); 56 | 57 | expect(cpu.registers.get(REGISTER.R1.address)).toBe(0x1234); 58 | expect(cpu.registers.get(REGISTER.R2.address)).toBe(0x5678); 59 | expect(cpu.registers.get(REGISTER.IP.address)).toBe(0x0009); 60 | }); 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /src/vm/memory-mapper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Map memory to other devices like video memory or smth else. 3 | */ 4 | class MemoryMapper { 5 | constructor() { 6 | this.regions = []; 7 | } 8 | 9 | /** 10 | * Map part of memory to specific device. 11 | * @param {object} device Device for map. 12 | * @param {number} start Start address. 13 | * @param {number} end End address. 14 | * @param {boolean} remap Remap memory to new device (change address). 15 | * @returns {undefined} 16 | */ 17 | map(device, start, end, remap = true) { 18 | const region = { 19 | device, 20 | start, 21 | end, 22 | remap 23 | }; 24 | this.regions.unshift(region); 25 | 26 | return () => { 27 | this.regions = this.regions.filter(x => x !== region); 28 | }; 29 | } 30 | 31 | getUint8(address) { 32 | const region = this._findRegion(address); 33 | const finalAddress = region.remap 34 | ? address - region.start 35 | : address; 36 | return region.device.getUint8(finalAddress); 37 | } 38 | 39 | setUint8(address, value) { 40 | const region = this._findRegion(address); 41 | const finalAddress = region.remap 42 | ? address - region.start 43 | : address; 44 | return region.device.setUint8(finalAddress, value); 45 | } 46 | 47 | getUint16(address) { 48 | const region = this._findRegion(address); 49 | const finalAddress = region.remap 50 | ? address - region.start 51 | : address; 52 | return region.device.getUint16(finalAddress); 53 | } 54 | 55 | setUint16(address, value) { 56 | const region = this._findRegion(address); 57 | const finalAddress = region.remap 58 | ? address - region.start 59 | : address; 60 | return region.device.setUint16(finalAddress, value); 61 | } 62 | 63 | /** 64 | * Find mapped region by address. 65 | * @param {number} address Address. 66 | * @return {object} Region. 67 | * @private 68 | */ 69 | _findRegion(address) { 70 | let region = this.regions.find(r => address >= r.start && address <= r.end); 71 | if (!region) { 72 | throw new Error(`No memory region found for address ${address}`); 73 | } 74 | 75 | return region; 76 | } 77 | } 78 | 79 | module.exports = MemoryMapper; 80 | -------------------------------------------------------------------------------- /src/vm/memory-mapper.test.js: -------------------------------------------------------------------------------- 1 | const MemoryMapper = require('./memory-mapper'); 2 | const Memory = require('./memory'); 3 | 4 | describe('MemoryMapper', () => { 5 | const ramSize = 64; 6 | let memoryMapper; 7 | 8 | beforeEach(() => { 9 | memoryMapper = new MemoryMapper(); 10 | }); 11 | 12 | test('should be created', () => { 13 | expect(memoryMapper).toBeTruthy(); 14 | }); 15 | 16 | test('should throw error when unmapped', () => { 17 | // eslint-disable-next-line no-unused-vars,require-jsdoc 18 | function getByteFromMemory() { 19 | memoryMapper.getUint8(); 20 | } 21 | 22 | // eslint-disable-next-line no-unused-vars,require-jsdoc 23 | function setByteToMemory() { 24 | memoryMapper.setUint8(); 25 | } 26 | 27 | // eslint-disable-next-line no-unused-vars,require-jsdoc 28 | function getWordFromMemory() { 29 | memoryMapper.getUint16(); 30 | } 31 | 32 | // eslint-disable-next-line no-unused-vars,require-jsdoc 33 | function setWordToMemory() { 34 | memoryMapper.setUint16(); 35 | } 36 | 37 | 38 | expect(getByteFromMemory).toThrowError(); 39 | expect(setByteToMemory).toThrowError(); 40 | expect(getWordFromMemory).toThrowError(); 41 | expect(setWordToMemory).toThrowError(); 42 | }); 43 | 44 | test('should work with mapped device memory', () => { 45 | const ram = new Memory(ramSize); 46 | ram.byteAt[0] = 0x01; 47 | ram.byteAt[1] = 0x23; 48 | ram.byteAt[2] = 0x45; 49 | memoryMapper.map(ram, 0, ramSize); 50 | 51 | expect(memoryMapper.getUint8(0x0000)).toBe(0x01); 52 | expect(memoryMapper.getUint16(0x0001)).toBe(0x2345); 53 | 54 | memoryMapper.setUint8(0x000A, 0xFE); 55 | expect(ram.byteAt[0x000A]).toBe(0xFE); 56 | 57 | memoryMapper.setUint16(0x00E, 0xABCD); 58 | expect(ram.byteAt[0x000E]).toBe(0xAB); 59 | expect(ram.byteAt[0x000F]).toBe(0xCD); 60 | }); 61 | 62 | test('should override mapped device memory', () => { 63 | const ram = new Memory(ramSize); 64 | ram.byteAt[0] = 0x01; 65 | ram.byteAt[1] = 0x23; 66 | memoryMapper.map(ram, 0, ramSize); 67 | 68 | // create video memory and map area from 0x0002 to 0x000A to it 69 | const videoMemory = new Memory(8); 70 | videoMemory.byteAt[0] = 0xFE; 71 | videoMemory.byteAt[1] = 0xDC; 72 | memoryMapper.map(videoMemory, 0x0002, 0x000A, true); 73 | 74 | // this area from RAM memory. 75 | expect(memoryMapper.getUint8(0x0000)).toBe(0x01); 76 | expect(memoryMapper.getUint16(0x0000)).toBe(0x0123); 77 | memoryMapper.setUint8(0x0000, 0x98); 78 | expect(ram.byteAt[0x0000]).toBe(0x98); 79 | memoryMapper.setUint16(0x0000, 0x7654); 80 | expect(ram.byteAt[0x0000]).toBe(0x76); 81 | expect(ram.byteAt[0x0001]).toBe(0x54); 82 | 83 | // this area from video memory. 84 | expect(memoryMapper.getUint8(0x0002)).toBe(0xFE); 85 | expect(memoryMapper.getUint16(0x0002)).toBe(0xFEDC); 86 | memoryMapper.setUint8(0x0002, 0x12); 87 | expect(videoMemory.byteAt[0x0000]).toBe(0x12); // mapped address not equal real address from device! 88 | memoryMapper.setUint16(0x0002, 0x3456); 89 | expect(videoMemory.byteAt[0x0000]).toBe(0x34); 90 | expect(videoMemory.byteAt[0x0001]).toBe(0x56); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/common/format.test.js: -------------------------------------------------------------------------------- 1 | const format = require('./format'); 2 | 3 | describe('Format function', () => { 4 | test('should format byte', () => { 5 | const values = [ 6 | { value: 0, result: '0x00'}, 7 | { value: 1, result: '0x01'}, 8 | { value: 12, result: '0x0C'}, 9 | { value: 15, result: '0x0F'}, 10 | { value: 128, result: '0x80'}, 11 | { value: 170, result: '0xAA'}, 12 | { value: 255, result: '0xFF'}, 13 | { value: 256, result: '0x00'}, 14 | { value: 257, result: '0x01'}, 15 | ]; 16 | 17 | values.forEach(item => { 18 | expect(format.asByte(item.value)).toEqual(item.result); 19 | }); 20 | }); 21 | 22 | test('should throw error for format negative value as byte', () => { 23 | // eslint-disable-next-line require-jsdoc 24 | function formatNegativeByte() { 25 | format.asByte(-1); 26 | } 27 | 28 | expect(formatNegativeByte).toThrowError(); 29 | }); 30 | 31 | test('should format word', () => { 32 | const values = [ 33 | { value: 0, result: '0x0000'}, 34 | { value: 1, result: '0x0001'}, 35 | { value: 12, result: '0x000C'}, 36 | { value: 15, result: '0x000F'}, 37 | { value: 128, result: '0x0080'}, 38 | { value: 170, result: '0x00AA'}, 39 | { value: 255, result: '0x00FF'}, 40 | { value: 65535, result: '0xFFFF'}, 41 | { value: 65536, result: '0x0000'}, 42 | { value: 65537, result: '0x0001'}, 43 | ]; 44 | 45 | values.forEach(item => { 46 | expect(format.asWord(item.value)).toEqual(item.result); 47 | }); 48 | }); 49 | 50 | test('should throw error for format negative value as word', () => { 51 | // eslint-disable-next-line require-jsdoc 52 | function formatNegativeWord() { 53 | format.asWord(-1); 54 | } 55 | 56 | expect(formatNegativeWord).toThrowError(); 57 | }); 58 | 59 | test('should format dword', () => { 60 | const values = [ 61 | { value: 0, result: '0x00000000'}, 62 | { value: 1, result: '0x00000001'}, 63 | { value: 12, result: '0x0000000C'}, 64 | { value: 15, result: '0x0000000F'}, 65 | { value: 128, result: '0x00000080'}, 66 | { value: 170, result: '0x000000AA'}, 67 | { value: 255, result: '0x000000FF'}, 68 | { value: 65535, result: '0x0000FFFF'}, 69 | { value: 65536, result: '0x00010000'}, 70 | { value: 65537, result: '0x00010001'}, 71 | { value: 4294967295, result: '0xFFFFFFFF'}, 72 | { value: 4294967296, result: '0x00000000'}, 73 | { value: 4294967297, result: '0x00000001'}, 74 | ]; 75 | 76 | values.forEach(item => { 77 | expect(format.asDword(item.value)).toEqual(item.result); 78 | }); 79 | }); 80 | 81 | test('should throw error for format negative value as dword', () => { 82 | // eslint-disable-next-line require-jsdoc 83 | function formatNegativeWord() { 84 | format.asDword(-1); 85 | } 86 | 87 | expect(formatNegativeWord).toThrowError(); 88 | }); 89 | 90 | test('should format qword', () => { 91 | const values = [ 92 | { value: 0, result: '0x0000000000000000'}, 93 | { value: 1, result: '0x0000000000000001'}, 94 | { value: 12, result: '0x000000000000000C'}, 95 | { value: 15, result: '0x000000000000000F'}, 96 | { value: 128, result: '0x0000000000000080'}, 97 | { value: 170, result: '0x00000000000000AA'}, 98 | { value: 255, result: '0x00000000000000FF'}, 99 | { value: 65535, result: '0x000000000000FFFF'}, 100 | { value: 65536, result: '0x0000000000010000'}, 101 | { value: 65537, result: '0x0000000000010001'}, 102 | { value: 4294967295, result: '0x00000000FFFFFFFF'}, 103 | { value: 4294967296, result: '0x0000000100000000'}, 104 | { value: 4294967297, result: '0x0000000100000001'}, 105 | { value: Number.MAX_SAFE_INTEGER, result: '0x001FFFFFFFFFFFFF'}, 106 | ]; 107 | 108 | values.forEach(item => { 109 | expect(format.asQword(item.value)).toEqual(item.result); 110 | }); 111 | }); 112 | 113 | test('should throw error for format negative value as qword', () => { 114 | // eslint-disable-next-line require-jsdoc 115 | function formatNegativeWord() { 116 | format.asQword(-1); 117 | } 118 | 119 | expect(formatNegativeWord).toThrowError(); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /src/common/logger.test.js: -------------------------------------------------------------------------------- 1 | const Logger = require('./logger'); 2 | 3 | const LOGLEVEL = require('./log-level.constant'); 4 | 5 | describe('Logger', () => { 6 | let logger; 7 | let testProvider; 8 | 9 | beforeEach(() => { 10 | testProvider = {}; 11 | testProvider.debug = jest.fn(); 12 | testProvider.info = jest.fn(); 13 | testProvider.warn = jest.fn(); 14 | testProvider.error = jest.fn(); 15 | testProvider.fatal = jest.fn(); 16 | 17 | logger = new Logger(testProvider); 18 | }); 19 | 20 | test('should be created', () => { 21 | expect(logger).toBeTruthy(); 22 | expect(logger.logLevel).toBe(LOGLEVEL.ERROR); // it's default log level 23 | }); 24 | 25 | test('should log all messages', () => { 26 | logger.logLevel = LOGLEVEL.ALL; 27 | 28 | logger.debug('debug log message'); 29 | logger.info('info log message'); 30 | logger.warn('warn log message'); 31 | logger.error('error log message'); 32 | logger.fatal('fatal log message'); 33 | 34 | expect(testProvider.debug).toBeCalledTimes(1); 35 | expect(testProvider.info).toBeCalledTimes(1); 36 | expect(testProvider.warn).toBeCalledTimes(1); 37 | expect(testProvider.error).toBeCalledTimes(1); 38 | expect(testProvider.fatal).toBeCalledTimes(1); 39 | }); 40 | 41 | test('should log messages on DEBUG log level', () => { 42 | logger.logLevel = LOGLEVEL.DEBUG; 43 | 44 | logger.debug('debug log message'); 45 | logger.info('info log message'); 46 | logger.warn('warn log message'); 47 | logger.error('error log message'); 48 | logger.fatal('fatal log message'); 49 | 50 | expect(testProvider.debug).toBeCalledTimes(1); 51 | expect(testProvider.info).toBeCalledTimes(1); 52 | expect(testProvider.warn).toBeCalledTimes(1); 53 | expect(testProvider.error).toBeCalledTimes(1); 54 | expect(testProvider.fatal).toBeCalledTimes(1); 55 | }); 56 | 57 | test('should log messages on INFO log level', () => { 58 | logger.logLevel = LOGLEVEL.INFO; 59 | 60 | logger.debug('debug log message'); 61 | logger.info('info log message'); 62 | logger.warn('warn log message'); 63 | logger.error('error log message'); 64 | logger.fatal('fatal log message'); 65 | 66 | expect(testProvider.debug).toBeCalledTimes(0); 67 | expect(testProvider.info).toBeCalledTimes(1); 68 | expect(testProvider.warn).toBeCalledTimes(1); 69 | expect(testProvider.error).toBeCalledTimes(1); 70 | expect(testProvider.fatal).toBeCalledTimes(1); 71 | }); 72 | 73 | test('should log messages on WARN log level', () => { 74 | logger.logLevel = LOGLEVEL.WARN; 75 | 76 | logger.debug('debug log message'); 77 | logger.info('info log message'); 78 | logger.warn('warn log message'); 79 | logger.error('error log message'); 80 | logger.fatal('fatal log message'); 81 | 82 | expect(testProvider.debug).toBeCalledTimes(0); 83 | expect(testProvider.info).toBeCalledTimes(0); 84 | expect(testProvider.warn).toBeCalledTimes(1); 85 | expect(testProvider.error).toBeCalledTimes(1); 86 | expect(testProvider.fatal).toBeCalledTimes(1); 87 | }); 88 | 89 | test('should log messages on ERROR log level', () => { 90 | logger.logLevel = LOGLEVEL.ERROR; 91 | 92 | logger.debug('debug log message'); 93 | logger.info('info log message'); 94 | logger.warn('warn log message'); 95 | logger.error('error log message'); 96 | logger.fatal('fatal log message'); 97 | 98 | expect(testProvider.debug).toBeCalledTimes(0); 99 | expect(testProvider.info).toBeCalledTimes(0); 100 | expect(testProvider.warn).toBeCalledTimes(0); 101 | expect(testProvider.error).toBeCalledTimes(1); 102 | expect(testProvider.fatal).toBeCalledTimes(1); 103 | }); 104 | 105 | test('should log messages on FATAL log level', () => { 106 | logger.logLevel = LOGLEVEL.FATAL; 107 | 108 | logger.debug('debug log message'); 109 | logger.info('info log message'); 110 | logger.warn('warn log message'); 111 | logger.error('error log message'); 112 | logger.fatal('fatal log message'); 113 | 114 | expect(testProvider.debug).toBeCalledTimes(0); 115 | expect(testProvider.info).toBeCalledTimes(0); 116 | expect(testProvider.warn).toBeCalledTimes(0); 117 | expect(testProvider.error).toBeCalledTimes(0); 118 | expect(testProvider.fatal).toBeCalledTimes(1); 119 | }); 120 | 121 | test('should not log messages on OFF log level', () => { 122 | logger.logLevel = LOGLEVEL.OFF; 123 | 124 | logger.debug('debug log message'); 125 | logger.info('info log message'); 126 | logger.warn('warn log message'); 127 | logger.error('error log message'); 128 | logger.fatal('fatal log message'); 129 | 130 | expect(testProvider.debug).toBeCalledTimes(0); 131 | expect(testProvider.info).toBeCalledTimes(0); 132 | expect(testProvider.warn).toBeCalledTimes(0); 133 | expect(testProvider.error).toBeCalledTimes(0); 134 | expect(testProvider.fatal).toBeCalledTimes(0); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /src/vm/alu.js: -------------------------------------------------------------------------------- 1 | const INSTRUCTION = require('../architecture/sample/instruction.constant'); 2 | const REGISTER = require('../architecture/sample/register.constant'); 3 | 4 | /** 5 | * CPU arithmetic logic unit. 6 | */ 7 | class Alu { 8 | constructor(memory, registers) { 9 | this.memory = memory; 10 | this.registers = registers; 11 | } 12 | 13 | fetch8() { 14 | const address = this.registers.get(REGISTER.IP.address); 15 | const instruction = this.memory.getUint8(address); 16 | this.registers.set(REGISTER.IP.address, address + 1); 17 | 18 | return instruction; 19 | } 20 | 21 | fetch16() { 22 | const address = this.registers.get(REGISTER.IP.address); 23 | const instruction = this.memory.getUint16(address); 24 | this.registers.set(REGISTER.IP.address, address + 2); 25 | 26 | return instruction; 27 | } 28 | 29 | push(value) { 30 | const stackAddress = this.registers.get(REGISTER.SP.address); 31 | this.memory.setUint16(stackAddress, value); 32 | this.registers.set(REGISTER.SP.address, stackAddress - 2); 33 | } 34 | 35 | pop() { 36 | const nextStackAddress = this.registers.get(REGISTER.SP.address) + 2; 37 | this.registers.set(REGISTER.SP.address, nextStackAddress); 38 | 39 | return this.memory.getUint16(nextStackAddress); 40 | } 41 | 42 | execute(opcode) { 43 | switch (opcode) { 44 | case INSTRUCTION.NOP.opcode: { 45 | return; 46 | } 47 | 48 | case INSTRUCTION.MOV_LIT_REG.opcode: { 49 | const literal = this.fetch16(); 50 | const registerAddress = this.fetch8(); 51 | this.registers.set(registerAddress, literal); 52 | 53 | return; 54 | } 55 | 56 | case INSTRUCTION.MOV_REG_REG.opcode: { 57 | const registerAddressFrom = this.fetch8(); 58 | const registerAddressTo = this.fetch8(); 59 | const value = this.registers.get(registerAddressFrom); 60 | 61 | this.registers.set(registerAddressTo, value); 62 | 63 | return; 64 | } 65 | 66 | case INSTRUCTION.MOV_REG_MEM.opcode: { 67 | const registerAddressFrom = this.fetch8(); 68 | const memoryAddressTo = this.fetch16(); 69 | const value = this.registers.get(registerAddressFrom); 70 | this.memory.setUint16(memoryAddressTo, value); 71 | 72 | return; 73 | } 74 | 75 | case INSTRUCTION.MOV_MEM_REG.opcode: { 76 | const memoryAddressFrom = this.fetch16(); 77 | const registerAddressTo = this.fetch8(); 78 | const value = this.memory.getUint16(memoryAddressFrom); 79 | this.registers.set(registerAddressTo, value); 80 | 81 | return; 82 | } 83 | 84 | case INSTRUCTION.ADD_REG_REG.opcode: { 85 | const firstRegisterAddress = this.fetch8(); 86 | const firstValue = this.registers.get(firstRegisterAddress); 87 | 88 | const secondRegisterAddress = this.fetch8(); 89 | const secondValue = this.registers.get(secondRegisterAddress); 90 | 91 | this.registers.set(REGISTER.AC.address, firstValue + secondValue); 92 | 93 | return; 94 | } 95 | 96 | case INSTRUCTION.SUB_REG_REG.opcode: { 97 | const firstRegisterAddress = this.fetch8(); 98 | const firstValue = this.registers.get(firstRegisterAddress); 99 | 100 | const secondRegisterAddress = this.fetch8(); 101 | const secondValue = this.registers.get(secondRegisterAddress); 102 | 103 | this.registers.set(REGISTER.AC.address, firstValue - secondValue); 104 | 105 | return; 106 | } 107 | 108 | case INSTRUCTION.JMP.opcode: { 109 | const address = this.fetch16(); 110 | this.registers.set(REGISTER.IP.address, address); 111 | 112 | return; 113 | } 114 | 115 | case INSTRUCTION.JMP_EQ.opcode: { 116 | const value = this.fetch16(); 117 | const address = this.fetch16(); 118 | if (value === this.registers.get(REGISTER.AC.address)) { 119 | this.registers.set(REGISTER.IP.address, address); 120 | } 121 | 122 | return; 123 | } 124 | 125 | case INSTRUCTION.JMP_NOT_EQ.opcode: { 126 | const value = this.fetch16(); 127 | const address = this.fetch16(); 128 | if (value !== this.registers.get(REGISTER.AC.address)) { 129 | this.registers.set(REGISTER.IP.address, address); 130 | } 131 | 132 | return; 133 | } 134 | 135 | case INSTRUCTION.PSH_LIT.opcode: { 136 | const value = this.fetch16(); 137 | this.push(value); 138 | 139 | return; 140 | } 141 | 142 | case INSTRUCTION.PSH_REG.opcode: { 143 | const registerAddress = this.fetch8(); 144 | this.push(this.registers.get(registerAddress)); 145 | 146 | return; 147 | } 148 | 149 | case INSTRUCTION.POP.opcode: { 150 | const registerAddress = this.fetch8(); 151 | const value = this.pop(); 152 | this.registers.set(registerAddress, value); 153 | 154 | return; 155 | } 156 | 157 | case INSTRUCTION.CALL.opcode: { 158 | const address = this.fetch16(); 159 | this.push(this.registers.get(REGISTER.IP.address)); 160 | this.registers.set(REGISTER.IP.address, address); 161 | 162 | return; 163 | } 164 | 165 | case INSTRUCTION.RET.opcode: { 166 | const address = this.pop(); 167 | this.registers.set(REGISTER.IP.address, address); 168 | 169 | return; 170 | } 171 | 172 | case INSTRUCTION.HLT.opcode: { 173 | return true; 174 | } 175 | 176 | default: { 177 | throw new Error(`Tried to execute unknown opcode ${opcode}. ALU stopped.`); 178 | } 179 | } 180 | } 181 | } 182 | 183 | module.exports = Alu; 184 | -------------------------------------------------------------------------------- /src/vm/alu.test.js: -------------------------------------------------------------------------------- 1 | const Memory = require('./memory'); 2 | const Registers = require('./registers'); 3 | const Alu = require('./alu'); 4 | const Cpu = require('./cpu'); 5 | 6 | const INSTRUCTION = require('../architecture/sample/instruction.constant'); 7 | const REGISTER = require('../architecture/sample/register.constant'); 8 | 9 | describe('ALU', () => { 10 | const ramSize = 64; 11 | const flashSize = 32; 12 | 13 | let ram; 14 | let registers; 15 | let alu; 16 | let flash; 17 | let cpu; 18 | 19 | beforeEach(() => { 20 | ram = new Memory(ramSize); 21 | registers = new Registers(REGISTER.ALL); 22 | alu = new Alu(ram, registers); 23 | flash = new Memory(flashSize); 24 | cpu = new Cpu(ram, registers, alu, flash); 25 | }); 26 | 27 | describe('common', () => { 28 | test('should fetch 8 bit instruction from RAM', () => { 29 | const expectedValue = 234; 30 | ram.setUint8(0, expectedValue); 31 | 32 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 33 | expect(cpu.alu.fetch8()).toEqual(expectedValue); 34 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0001); 35 | }); 36 | 37 | test('should fetch 16 bit instruction from ram', () => { 38 | const expectedValue = 65432; 39 | ram.setUint16(0, expectedValue); 40 | 41 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 42 | expect(cpu.alu.fetch16()).toEqual(expectedValue); 43 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0002); 44 | }); 45 | }); 46 | 47 | describe('instruction processor', () => { 48 | test('should execute instruction NO OPERATION', () => { 49 | ram.byteAt[0] = INSTRUCTION.NOP.opcode; 50 | 51 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 52 | 53 | cpu.tick(); 54 | 55 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0001); 56 | }); 57 | 58 | test('should execute instruction MOVE LITERAL TO REGISTER', () => { 59 | ram.byteAt[0] = INSTRUCTION.MOV_LIT_REG.opcode; 60 | ram.byteAt[1] = 0xAB; 61 | ram.byteAt[2] = 0xCD; 62 | ram.byteAt[3] = REGISTER.R1.address; 63 | 64 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 65 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0000); 66 | 67 | cpu.tick(); 68 | 69 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 70 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0xABCD); 71 | }); 72 | 73 | test('should execute instruction MOVE REGISTER TO REGISTER', () => { 74 | ram.byteAt[0] = INSTRUCTION.MOV_LIT_REG.opcode; 75 | ram.byteAt[1] = 0xAB; 76 | ram.byteAt[2] = 0xCD; 77 | ram.byteAt[3] = REGISTER.R1.address; 78 | 79 | ram.byteAt[4] = INSTRUCTION.MOV_REG_REG.opcode; 80 | ram.byteAt[5] = REGISTER.R1.address; 81 | ram.byteAt[6] = REGISTER.R2.address; 82 | 83 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 84 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0000); 85 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0x0000); 86 | 87 | cpu.tick(); 88 | 89 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 90 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0xABCD); 91 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0x0000); 92 | 93 | cpu.tick(); 94 | 95 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0007); 96 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0xABCD); 97 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0xABCD); 98 | }); 99 | 100 | test('should execute instruction MOVE REGISTER TO MEMORY', () => { 101 | ram.byteAt[0] = INSTRUCTION.MOV_LIT_REG.opcode; 102 | ram.byteAt[1] = 0xAB; 103 | ram.byteAt[2] = 0xCD; 104 | ram.byteAt[3] = REGISTER.R1.address; 105 | 106 | ram.byteAt[4] = INSTRUCTION.MOV_REG_MEM.opcode; 107 | ram.byteAt[5] = REGISTER.R1.address; 108 | ram.byteAt[6] = 0x00; 109 | ram.byteAt[7] = 0x10; // ram address 110 | 111 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 112 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0000); 113 | expect(ram.getUint16(0x0010)).toEqual(0x0000); 114 | 115 | cpu.tick(); 116 | 117 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 118 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0xABCD); 119 | expect(ram.getUint16(0x0010)).toEqual(0x0000); 120 | 121 | cpu.tick(); 122 | 123 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0008); 124 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0xABCD); 125 | expect(ram.getUint16(0x0010)).toEqual(0xABCD); 126 | }); 127 | 128 | test('should execute instruction MOVE MEMORY TO REGISTER', () => { 129 | ram.byteAt[0] = INSTRUCTION.MOV_MEM_REG.opcode; 130 | ram.byteAt[1] = 0x00; 131 | ram.byteAt[2] = 0x04; 132 | ram.byteAt[3] = REGISTER.R1.address; 133 | ram.byteAt[4] = 0x23; 134 | ram.byteAt[5] = 0x45; // value to move 135 | 136 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 137 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0000); 138 | expect(ram.getUint16(0x0004)).toEqual(0x2345); 139 | 140 | cpu.tick(); 141 | 142 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 143 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x2345); 144 | expect(ram.getUint16(0x0004)).toEqual(0x2345); 145 | 146 | }); 147 | 148 | test('should execute instruction ADD REGISTER TO REGISTER', () => { 149 | ram.byteAt[0] = INSTRUCTION.MOV_LIT_REG.opcode; 150 | ram.byteAt[1] = 0x02; 151 | ram.byteAt[2] = 0x04; 152 | ram.byteAt[3] = REGISTER.R1.address; 153 | 154 | ram.byteAt[4] = INSTRUCTION.MOV_LIT_REG.opcode; 155 | ram.byteAt[5] = 0x03; 156 | ram.byteAt[6] = 0x06; 157 | ram.byteAt[7] = REGISTER.R2.address; 158 | 159 | ram.byteAt[8] = INSTRUCTION.ADD_REG_REG.opcode; 160 | ram.byteAt[9] = REGISTER.R1.address; 161 | ram.byteAt[10] = REGISTER.R2.address; 162 | 163 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 164 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0000); 165 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0x0000); 166 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x0000); 167 | 168 | cpu.tick(); 169 | 170 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 171 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0204); 172 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0x0000); 173 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x0000); 174 | 175 | cpu.tick(); 176 | 177 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0008); 178 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0204); 179 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0x0306); 180 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x0000); 181 | 182 | cpu.tick(); 183 | 184 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x000B); 185 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0204); 186 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0x0306); 187 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x050A); 188 | }); 189 | 190 | test('should execute instruction SUBTRACT REGISTER TO REGISTER', () => { 191 | ram.byteAt[0] = INSTRUCTION.MOV_LIT_REG.opcode; 192 | ram.byteAt[1] = 0x03; 193 | ram.byteAt[2] = 0x06; 194 | ram.byteAt[3] = REGISTER.R1.address; 195 | 196 | ram.byteAt[4] = INSTRUCTION.MOV_LIT_REG.opcode; 197 | ram.byteAt[5] = 0x01; 198 | ram.byteAt[6] = 0x02; 199 | ram.byteAt[7] = REGISTER.R2.address; 200 | 201 | ram.byteAt[8] = INSTRUCTION.SUB_REG_REG.opcode; 202 | ram.byteAt[9] = REGISTER.R1.address; 203 | ram.byteAt[10] = REGISTER.R2.address; 204 | 205 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 206 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0000); 207 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0x0000); 208 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x0000); 209 | 210 | cpu.tick(); 211 | 212 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 213 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0306); 214 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0x0000); 215 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x0000); 216 | 217 | cpu.tick(); 218 | 219 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0008); 220 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0306); 221 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0x0102); 222 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x0000); 223 | 224 | cpu.tick(); 225 | 226 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x000B); 227 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0306); 228 | expect(cpu.registers.get(REGISTER.R2.address)).toEqual(0x0102); 229 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x0204); 230 | }); 231 | 232 | test('should execute instruction JUMP EQUAL', () => { 233 | ram.byteAt[0] = INSTRUCTION.MOV_LIT_REG.opcode; 234 | ram.byteAt[1] = 0x12; 235 | ram.byteAt[2] = 0x34; 236 | ram.byteAt[3] = REGISTER.AC.address; 237 | 238 | ram.byteAt[4] = INSTRUCTION.JMP_EQ.opcode; 239 | ram.byteAt[5] = 0x12; 240 | ram.byteAt[6] = 0x34; // value for check 241 | ram.byteAt[7] = 0x00; 242 | ram.byteAt[8] = 0x1F; // address for jump 243 | 244 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 245 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x0000); 246 | 247 | cpu.tick(); 248 | 249 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 250 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x1234); 251 | 252 | cpu.tick(); 253 | 254 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x001F); 255 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x1234); 256 | }); 257 | 258 | test('should execute instruction JUMP NOT EQUAL', () => { 259 | ram.byteAt[0] = INSTRUCTION.MOV_LIT_REG.opcode; 260 | ram.byteAt[1] = 0x12; 261 | ram.byteAt[2] = 0x34; 262 | ram.byteAt[3] = REGISTER.AC.address; 263 | 264 | ram.byteAt[4] = INSTRUCTION.JMP_NOT_EQ.opcode; 265 | ram.byteAt[5] = 0x43; 266 | ram.byteAt[6] = 0x21; // value for check 267 | ram.byteAt[7] = 0x00; 268 | ram.byteAt[8] = 0x1F; // address for jump 269 | 270 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 271 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x0000); 272 | 273 | cpu.tick(); 274 | 275 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 276 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x1234); 277 | 278 | cpu.tick(); 279 | 280 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x001F); 281 | expect(cpu.registers.get(REGISTER.AC.address)).toEqual(0x1234); 282 | }); 283 | 284 | test('should execute instruction PUSH LITERAL TO STACK', () => { 285 | ram.byteAt[0] = INSTRUCTION.PSH_LIT.opcode; 286 | ram.byteAt[1] = 0x12; 287 | ram.byteAt[2] = 0x34; 288 | 289 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 290 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial); 291 | expect(cpu.ram.getUint16(cpu.registers.get(REGISTER.SP.address))).toEqual(0x0000); 292 | 293 | cpu.tick(); 294 | 295 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0003); 296 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial - 2); 297 | expect(cpu.ram.getUint16(cpu.registers.get(REGISTER.SP.address) + 2)).toEqual(0x1234); 298 | }); 299 | 300 | test('should execute instruction PUSH REGISTER TO STACK', () => { 301 | ram.byteAt[0] = INSTRUCTION.MOV_LIT_REG.opcode; 302 | ram.byteAt[1] = 0x12; 303 | ram.byteAt[2] = 0x34; 304 | ram.byteAt[3] = REGISTER.R1.address; 305 | 306 | ram.byteAt[4] = INSTRUCTION.PSH_REG.opcode; 307 | ram.byteAt[5] = REGISTER.R1.address; 308 | 309 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 310 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0000); 311 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial); 312 | expect(cpu.ram.getUint16(cpu.registers.get(REGISTER.SP.address))).toEqual(0x0000); 313 | 314 | cpu.tick(); 315 | 316 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 317 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x1234); 318 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial); 319 | expect(cpu.ram.getUint16(cpu.registers.get(REGISTER.SP.address))).toEqual(0x0000); 320 | 321 | cpu.tick(); 322 | 323 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0006); 324 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x1234); 325 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial - 2); 326 | expect(cpu.ram.getUint16(cpu.registers.get(REGISTER.SP.address) + 2)).toEqual(0x1234); 327 | }); 328 | 329 | test('should execute instruction POP FROM STACK', () => { 330 | ram.byteAt[0] = INSTRUCTION.PSH_LIT.opcode; 331 | ram.byteAt[1] = 0x12; 332 | ram.byteAt[2] = 0x34; 333 | 334 | ram.byteAt[3] = INSTRUCTION.POP.opcode; 335 | ram.byteAt[4] = REGISTER.R1.address; 336 | 337 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 338 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0000); 339 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial); 340 | expect(cpu.ram.getUint16(cpu.registers.get(REGISTER.SP.address))).toEqual(0x0000); 341 | 342 | cpu.tick(); 343 | 344 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0003); 345 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x0000); 346 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial - 2); 347 | expect(cpu.ram.getUint16(cpu.registers.get(REGISTER.SP.address) + 2)).toEqual(0x1234); 348 | 349 | cpu.tick(); 350 | 351 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0005); 352 | expect(cpu.registers.get(REGISTER.R1.address)).toEqual(0x1234); 353 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial); 354 | }); 355 | 356 | test('should execute instruction JUMP', () => { 357 | ram.byteAt[0] = INSTRUCTION.JMP.opcode; 358 | ram.byteAt[1] = 0x00; 359 | ram.byteAt[2] = 0x04; // jump to 0x0004 360 | 361 | ram.byteAt[3] = INSTRUCTION.NOP.opcode; 362 | 363 | ram.byteAt[4] = INSTRUCTION.JMP.opcode; 364 | ram.byteAt[5] = 0x00; 365 | ram.byteAt[6] = 0x00; // jump back to 0x0000 366 | 367 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 368 | 369 | cpu.tick(); 370 | 371 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 372 | 373 | cpu.tick(); 374 | 375 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 376 | }); 377 | 378 | test('should throw exception on unknown opcode', () => { 379 | ram.byteAt[0] = INSTRUCTION.UNKNOWN.opcode; 380 | 381 | // eslint-disable-next-line require-jsdoc 382 | function tryToTick() { 383 | cpu.tick(); 384 | } 385 | 386 | expect(tryToTick).toThrowError(); 387 | }); 388 | 389 | test('should execute instruction CALL', () => { 390 | ram.byteAt[0] = INSTRUCTION.CALL.opcode; 391 | ram.byteAt[1] = 0x00; 392 | ram.byteAt[2] = 0x04; // address for call 393 | 394 | ram.byteAt[3] = 0x00; 395 | 396 | ram.byteAt[4] = INSTRUCTION.CALL.opcode; 397 | ram.byteAt[5] = 0x00; 398 | ram.byteAt[6] = 0x00; // address for call 399 | 400 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 401 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial); 402 | 403 | cpu.tick(); 404 | 405 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 406 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial - 2); 407 | expect(ram.getUint16(cpu.stackPointerInitial)).toEqual(0x0003); 408 | 409 | cpu.tick(); 410 | 411 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 412 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial - 4); 413 | expect(ram.getUint16(cpu.stackPointerInitial - 2)).toEqual(0x0007); 414 | }); 415 | 416 | test('should execute instruction RET', () => { 417 | ram.byteAt[0] = INSTRUCTION.CALL.opcode; 418 | ram.byteAt[1] = 0x00; 419 | ram.byteAt[2] = 0x04; // address for call 420 | 421 | ram.byteAt[3] = 0x00; 422 | 423 | ram.byteAt[4] = INSTRUCTION.RET.opcode; 424 | 425 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0000); 426 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial); 427 | 428 | cpu.tick(); 429 | 430 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0004); 431 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial - 2); 432 | expect(ram.getUint16(cpu.stackPointerInitial)).toEqual(0x0003); 433 | 434 | cpu.tick(); 435 | 436 | expect(cpu.registers.get(REGISTER.IP.address)).toEqual(0x0003); 437 | expect(cpu.registers.get(REGISTER.SP.address)).toEqual(cpu.stackPointerInitial); 438 | }); 439 | }); 440 | }); 441 | --------------------------------------------------------------------------------