├── test ├── __init__.py ├── test_program.py ├── test_exceptions.py ├── test_integration.py └── test_statement.py ├── chip8asm ├── __init__.py ├── exceptions.py ├── program.py └── statement.py ├── .gitignore ├── requirements.txt ├── codecov.yml ├── .github └── workflows │ └── python-app.yml ├── LICENSE ├── assembler.py └── README.md /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chip8asm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | coverage -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "70...100" 5 | 6 | status: 7 | project: 8 | default: on 9 | patch: 10 | default: on 11 | changes: 12 | default: off 13 | 14 | comment: 15 | layout: "header, reach, diff, flags, files, footer" 16 | behavior: default 17 | require_changes: no 18 | require_base: no 19 | require_head: yes 20 | 21 | ignore: 22 | - "test/.*" -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | name: Build Test Coverage 2 | on: [pull_request, workflow_dispatch] 3 | jobs: 4 | run: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v3 9 | - name: Setup Python 3.8.12 10 | uses: actions/setup-python@v4 11 | with: 12 | python-version: '3.8.12' 13 | - name: Update PIP 14 | run: python -m pip install --upgrade pip 15 | - name: Install Requirements 16 | run: pip install -r requirements.txt 17 | - name: Generate Report 18 | run: coverage run --source=chip8asm -m unittest 19 | - name: Codecov 20 | uses: codecov/codecov-action@v4.2.0 21 | env: 22 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Craig Thomas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /chip8asm/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2014-2019 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | This file contains Exceptions for the Chip 8 Assembler. 6 | """ 7 | # C L A S S E S ############################################################### 8 | 9 | 10 | class TranslationError(Exception): 11 | """ 12 | Translation errors occur when the translate function is called from 13 | within the Statement class. Translation errors usually refer to the fact 14 | that an invalid mnemonic or invalid register was specified. 15 | """ 16 | def __init__(self, value): 17 | super().__init__() 18 | self.value = value 19 | 20 | def __str__(self): 21 | return repr(self.value) 22 | 23 | 24 | class ParseError(Exception): 25 | """ 26 | Parse errors occur when the parse function is called from 27 | within the Statement class. Parse errors usually refer to the fact 28 | that an invalid line of assembly code was encountered. 29 | """ 30 | def __init__(self, value): 31 | super().__init__() 32 | self.value = value 33 | 34 | def __str__(self): 35 | return repr(self.value) 36 | 37 | # E N D O F F I L E ####################################################### 38 | -------------------------------------------------------------------------------- /test/test_program.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2012-2019 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | A Chip 8 assembler - see the README.md file for details. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import unittest 10 | 11 | from chip8asm.program import Program 12 | from chip8asm.statement import Statement 13 | 14 | # C L A S S E S ############################################################### 15 | 16 | 17 | class TestProgram(unittest.TestCase): 18 | """ 19 | A test class for the Chip 8 Program class. 20 | """ 21 | def setUp(self): 22 | """ 23 | Common setup routines needed for all unit tests. 24 | """ 25 | pass 26 | 27 | def test_get_symbol_table_correct(self): 28 | program = Program() 29 | self.assertEqual(dict(), program.get_symbol_table()) 30 | program.symbol_table = dict(test="blah") 31 | self.assertEqual(dict(test="blah"), program.get_symbol_table()) 32 | 33 | def test_get_statements_correct(self): 34 | program = Program() 35 | statement = Statement() 36 | 37 | self.assertEqual([], program.get_statements()) 38 | program.statements.append(statement) 39 | self.assertEqual([statement], program.get_statements()) 40 | 41 | 42 | # E N D O F F I L E ####################################################### 43 | -------------------------------------------------------------------------------- /test/test_exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2012-2019 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | A Chip 8 assembler - see the README.md file for details. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import unittest 10 | 11 | from chip8asm.exceptions import TranslationError, ParseError 12 | 13 | # C L A S S E S ############################################################### 14 | 15 | 16 | class TestTranslationError(unittest.TestCase): 17 | """ 18 | A test class for the Chip 8 TranslationError class. 19 | """ 20 | def setUp(self): 21 | """ 22 | Common setup routines needed for all unit tests. 23 | """ 24 | pass 25 | 26 | def test_string_representation(self): 27 | translation_error = TranslationError("translation error") 28 | self.assertEqual("'translation error'", str(translation_error)) 29 | 30 | 31 | class TestParseError(unittest.TestCase): 32 | """ 33 | A test class for the Chip 8 ParseError class. 34 | """ 35 | def setUp(self): 36 | """ 37 | Common setup routines needed for all unit tests. 38 | """ 39 | pass 40 | 41 | def test_string_representation(self): 42 | parse_error = ParseError("parse error") 43 | self.assertEqual("'parse error'", str(parse_error)) 44 | 45 | # E N D O F F I L E ####################################################### 46 | -------------------------------------------------------------------------------- /assembler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2014-2018 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | A Chip 8 assembler - see the README.md file for details. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import argparse 10 | 11 | from chip8asm.program import Program 12 | 13 | # F U N C T I O N S ########################################################### 14 | 15 | 16 | def parse_arguments(): 17 | """ 18 | Parses the command-line arguments passed to the assembler. 19 | """ 20 | parser = argparse.ArgumentParser( 21 | description="Assemble or disassemble " 22 | "machine language code for the Chip8. See README.md for more " 23 | "information, and LICENSE for terms of use." 24 | ) 25 | parser.add_argument("filename", help="the name of the file to examine") 26 | parser.add_argument( 27 | "--symbols", action="store_true", help="print out the symbol table" 28 | ) 29 | parser.add_argument( 30 | "--print", action="store_true", 31 | help="print out the assembled statements when finished" 32 | ) 33 | parser.add_argument( 34 | "--output", metavar="FILE", help="stores the assembled program in FILE") 35 | return parser.parse_args() 36 | 37 | 38 | def main(args): 39 | """ 40 | Runs the assembler with the specified arguments. 41 | 42 | @param args: the arguments to the main function 43 | @type: namedtuple 44 | """ 45 | program = Program() 46 | program.parse_file(args.filename) 47 | program.translate_statements() 48 | program.set_addresses() 49 | program.fix_opcodes() 50 | 51 | if args.symbols: 52 | print("-- Symbol Table --") 53 | for symbol, value in program.get_symbol_table().items(): 54 | print("0x{} {}".format(value[2:].rjust(4, '0').upper(), symbol)) 55 | 56 | if args.print: 57 | print("-- Assembled Statements --") 58 | for statement in program.get_statements(): 59 | print(statement) 60 | 61 | if args.output: 62 | program.save_binary_file(args.output) 63 | 64 | 65 | main(parse_arguments()) 66 | 67 | # E N D O F F I L E ####################################################### 68 | -------------------------------------------------------------------------------- /test/test_integration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 204 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | A Chip 8 assembler - see the README.md file for details. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import unittest 10 | 11 | from chip8asm.program import Program 12 | from chip8asm.statement import Statement 13 | 14 | # C L A S S E S ############################################################### 15 | 16 | 17 | class TestIntegration(unittest.TestCase): 18 | """ 19 | A test class for integration tests. 20 | """ 21 | def setUp(self): 22 | """ 23 | Common setup routines needed for all unit tests. 24 | """ 25 | pass 26 | 27 | @staticmethod 28 | def translate_statements(program): 29 | """ 30 | Given a program, translate the statements into machine code. 31 | """ 32 | program.translate_statements() 33 | program.set_addresses() 34 | program.fix_opcodes() 35 | return program 36 | 37 | def test_audio_mnemonic_translate_correct(self): 38 | program = Program() 39 | statement = Statement() 40 | statement.parse_line(" AUDIO ; audio statement") 41 | program.statements.append(statement) 42 | program = self.translate_statements(program) 43 | machine_code = program.generate_machine_code() 44 | self.assertEqual([0xF0, 0x02], machine_code) 45 | 46 | def test_pitch_mnemonic_translate_correct(self): 47 | program = Program() 48 | statement = Statement() 49 | statement.parse_line(" PITCH r1 ; pitch statement") 50 | program.statements.append(statement) 51 | program = self.translate_statements(program) 52 | machine_code = program.generate_machine_code() 53 | self.assertEqual([0xF1, 0x3A], machine_code) 54 | 55 | def test_plane_mnemonic_translate_correct(self): 56 | program = Program() 57 | statement = Statement() 58 | statement.parse_line(" PLANE $2 ; plane statement") 59 | program.statements.append(statement) 60 | program = self.translate_statements(program) 61 | machine_code = program.generate_machine_code() 62 | self.assertEqual([0xF2, 0x03], machine_code) 63 | 64 | def test_savesub_mnemonic_translate_correct(self): 65 | program = Program() 66 | statement = Statement() 67 | statement.parse_line(" SAVESUB r1,r6 ; save subset statement") 68 | program.statements.append(statement) 69 | program = self.translate_statements(program) 70 | machine_code = program.generate_machine_code() 71 | self.assertEqual([0x51, 0x62], machine_code) 72 | 73 | def test_loadsub_mnemonic_translate_correct(self): 74 | program = Program() 75 | statement = Statement() 76 | statement.parse_line(" LOADSUB r1,r6 ; load subset statement") 77 | program.statements.append(statement) 78 | program = self.translate_statements(program) 79 | machine_code = program.generate_machine_code() 80 | self.assertEqual([0x51, 0x63], machine_code) 81 | 82 | def test_scrollup_mnemonic_translate_correct(self): 83 | program = Program() 84 | statement = Statement() 85 | statement.parse_line(" SCRU 5 ; scroll up statement") 86 | program.statements.append(statement) 87 | program = self.translate_statements(program) 88 | machine_code = program.generate_machine_code() 89 | self.assertEqual([0x00, 0xD5], machine_code) 90 | 91 | # E N D O F F I L E ####################################################### 92 | -------------------------------------------------------------------------------- /chip8asm/program.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2024 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | This file contains the main Program class for the Chip 8 Assembler. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import sys 10 | 11 | from chip8asm.exceptions import TranslationError 12 | from chip8asm.statement import Statement, FCB 13 | 14 | # C L A S S E S ############################################################### 15 | 16 | 17 | class Program(object): 18 | """ 19 | The Program class represents an actual Chip 8 program. Each Program 20 | contains a list of statements. Additionally, a Program keeps track of all 21 | the user-defined symbols in the program. 22 | """ 23 | def __init__(self): 24 | self.symbol_table = dict() 25 | self.statements = [] 26 | self.address = 0x200 27 | 28 | def parse_file(self, filename): 29 | """ 30 | Parses all of the lines in a file, and transforms each one into 31 | a Statement. 32 | 33 | :param filename: the name of the file to parse 34 | """ 35 | with open(filename) as infile: 36 | for line in infile: 37 | statement = Statement() 38 | statement.parse_line(line) 39 | if not statement.is_empty(): 40 | self.statements.append(statement) 41 | 42 | def translate_statements(self): 43 | """ 44 | Translates all the parsed statements into their respective 45 | opcodes. 46 | """ 47 | for index, statement in enumerate(self.statements): 48 | try: 49 | statement.translate() 50 | except TranslationError as error: 51 | self.throw_error(error, statement) 52 | label = statement.get_label() 53 | if label: 54 | if label in self.symbol_table: 55 | error = TranslationError("label [" + label + "] redefined") 56 | self.throw_error(error, statement) 57 | self.symbol_table[label] = index 58 | 59 | def set_addresses(self): 60 | """ 61 | Determines the address that each label refers to. 62 | """ 63 | for statement in self.statements: 64 | label = statement.get_label() 65 | if label: 66 | self.symbol_table[label] = hex(self.address) 67 | statement.set_address(hex(self.address)) 68 | if not statement.comment_only and not statement.is_empty(): 69 | self.address += 1 if statement.operation == FCB else 2 70 | 71 | def fix_opcodes(self): 72 | """ 73 | Calculates the final opcode for each statement in the program. 74 | """ 75 | for statement in self.statements: 76 | for label, value in self.symbol_table.items(): 77 | statement.replace_label(label, value[2:]) 78 | try: 79 | statement.fix_values() 80 | except TranslationError as error: 81 | self.throw_error(error, statement) 82 | 83 | def get_symbol_table(self): 84 | """ 85 | Returns the symbol table dictionary for the parsed program. 86 | 87 | :return: the symbol table for the program 88 | """ 89 | return self.symbol_table 90 | 91 | def get_statements(self): 92 | """ 93 | Returns the statements that make up the program. 94 | 95 | :return: the statements for the program 96 | """ 97 | return self.statements 98 | 99 | def generate_machine_code(self): 100 | """ 101 | Generates the machine code from the actual statements. 102 | 103 | :return: a list of integers representing the resulting machine code output 104 | """ 105 | machine_codes = [] 106 | for statement in self.statements: 107 | if not statement.is_empty() and not statement.comment_only: 108 | for index in range(0, len(statement.op_code), 2): 109 | machine_codes.append(int(statement.op_code[index:index + 2], 16)) 110 | return machine_codes 111 | 112 | def save_binary_file(self, filename): 113 | """ 114 | Writes out the assembled statements to the specified file 115 | name. 116 | 117 | :param filename: the name of the file to save statements 118 | """ 119 | machine_codes = self.generate_machine_code() 120 | with open(filename, "wb") as outfile: 121 | outfile.write(bytearray(machine_codes)) 122 | 123 | @staticmethod 124 | def throw_error(error, statement): 125 | """ 126 | Prints out an error message. 127 | 128 | :param error: the error message to throw 129 | :param statement: the assembly statement that caused the error 130 | """ 131 | print(error.value) 132 | print("line: {}".format(str(statement))) 133 | sys.exit(1) 134 | 135 | # E N D O F F I L E ####################################################### 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (Super) Chip 8 Assembler 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/craigthomas/Chip8Assembler/python-app.yml?style=flat-square&branch=main)](https://github.com/craigthomas/Chip8Assembler/actions) 4 | [![Codecov](https://img.shields.io/codecov/c/gh/craigthomas/Chip8Assembler?style=flat-square)](https://codecov.io/gh/craigthomas/Chip8Assembler) 5 | [![Dependencies](https://img.shields.io/librariesio/github/craigthomas/Chip8Assembler?style=flat-square)](https://libraries.io/github/craigthomas/Chip8Assembler) 6 | [![Releases](https://img.shields.io/github/release/craigthomas/Chip8Assembler?style=flat-square)](https://github.com/craigthomas/Chip8Python/releases) 7 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT) 8 | 9 | ## Table of Contents 10 | 11 | 1. [What is it?](#what-is-it) 12 | 2. [Requirements](#requirements) 13 | 3. [Installation](#installation) 14 | 4. [Usage](#usage) 15 | 1. [Input Format](#input-format) 16 | 2. [Print Symbol Table](#print-symbol-table) 17 | 3. [Print Assembled Statements](#print-assembled-statements) 18 | 5. [Mnemonic Table](#mnemonic-table) 19 | 1. [Chip 8 Mnemonics](#chip-8-mnemonics) 20 | 2. [Super Chip 8 Mnemonics](#super-chip-8-mnemonics) 21 | 3. [Pseudo Operations](#pseudo-operations) 22 | 4. [Operands](#operands) 23 | 6. [License](#license) 24 | 7. [Further Documentation](#further-documentation) 25 | 26 | ## What is it? 27 | 28 | This project is an XO Chip, Super Chip, and Chip 8 assembler written in Python 3.6. The assembler will 29 | take valid XO Chip, Super Chip, and Chip 8 assembly statements, and generate a binary file containing 30 | the correct machine instructions. 31 | 32 | 33 | ## Requirements 34 | 35 | In order to run the assembler, you will need to use Python 3.6 or greater. If you wish 36 | to clone the repository for development, you will need Git. 37 | 38 | 39 | ## Installation 40 | 41 | To install the source files, download the latest release from the 42 | [releases](https://github.com/craigthomas/Chip8Assembler/releases) section of 43 | the repository and unzip the contents in a directory of your choice. Or, 44 | clone the repository in the directory of your choice with: 45 | 46 | git clone https://github.com/craigthomas/Chip8Assembler.git 47 | 48 | Next, you will need to install the required packages for the file: 49 | 50 | pip install -r requirements.txt 51 | 52 | 53 | ## Usage 54 | 55 | To run the assembler: 56 | 57 | python assembler.py input_file --output output_file 58 | 59 | This will assemble the instructions found in file `input_file` and will generate 60 | the associated Chip 8 machine instructions in binary format in `output_file`. 61 | 62 | ### Input Format 63 | 64 | The input file needs to follow the format below: 65 | 66 | LABEL MNEMONIC OPERANDS COMMENT 67 | 68 | Where: 69 | 70 | * `LABEL` is a 15 character label for the statement 71 | * `MNEMONIC` is a Chip 8 operation mnemonic from the [Mnemonic Table](#mnemonic-table) below 72 | * `OPERANDS` are registers, values or labels, as described in the [Operands](#operands) section 73 | * `COMMENT` is a 30 character comment describing the statement (may have a `#` preceding it) 74 | 75 | An example file: 76 | 77 | # A comment line that contains nothing 78 | clear CLR 79 | start LOAD r1,$0 Clear contents of register 1 80 | ADD r1,$1 Add 1 to the register 81 | SKE r1,$A Check to see if we are at 10 82 | JUMP start Jump back to the start 83 | end JUMP end Loop forever 84 | data FCB $1A One byte piece of data 85 | data1 FDB $FBEE Two byte piece of data 86 | 87 | 88 | ### Print Symbol Table 89 | 90 | To print the symbol table that is generated during assembly, use the `--symbols` 91 | switch: 92 | 93 | python assembler.py test.asm --symbols 94 | 95 | Which will have the following output: 96 | 97 | -- Symbol Table -- 98 | 0x0200 clear 99 | 0x0202 start 100 | 0x020A end 101 | 0x020C data 102 | 0x020E data1 103 | 104 | 105 | ### Print Assembled Statements 106 | 107 | To print out the assembled version of the program, use the `--print` switch: 108 | 109 | python assembler.py test.asm --print 110 | 111 | Which will have the following output: 112 | 113 | -- Assembled Statements -- 114 | 0x0200 0000 # A comment line that contains nothing 115 | 0x0200 00E0 clear CLR # 116 | 0x0202 6100 start LOAD r1,$0 # Clear contents of register 1 117 | 0x0204 7101 ADD r1,$1 # Add 1 to the register 118 | 0x0206 310A SKE r1,$A # Check to see if we are at 10 119 | 0x0208 1202 JUMP start # Jump back to the start 120 | 0x020A 120A end JUMP end # Loop forever 121 | 0x020C 001A data FCB $1A # One byte piece of data 122 | 0x020E FBEE data1 FDB $FBEE # Two byte piece of data 123 | 124 | With this output, the first column is the offset in hex where the statement starts, 125 | the second column contains the full machine-code operand, the third column is the 126 | user-supplied label for the statement, the forth column is the mnemonic, the fifth 127 | column is the register values of other numeric or label data the operation will 128 | work on, and the fifth column is the comment string. 129 | 130 | 131 | ## Mnemonic Table 132 | 133 | The assembler supports mnemonics for both the Chip 8 and Super Chip 8 language 134 | specifications, as well as pseudo operations. 135 | 136 | ### Chip 8 Mnemonics 137 | 138 | | Mnemonic | Opcode | Operands | Description | 139 | | -------- | ------ |:--------:|----------------------------------------------------------------------------| 140 | | `SYS` | `0nnn` | 1 | System call (ignored) | 141 | | `CLR` | `00E0` | 0 | Clear the screen | 142 | | `RTS` | `00EE` | 0 | Return from subroutine | 143 | | `JUMP` | `1nnn` | 1 | Jump to address `nnn` | 144 | | `CALL` | `2nnn` | 1 | Call routine at address `nnn` | 145 | | `SKE` | `3snn` | 2 | Skip next instruction if register `s` equals `nn` | 146 | | `SKNE` | `4snn` | 2 | Do not skip next instruction if register `s` equals `nn` | 147 | | `SKRE` | `5st0` | 2 | Skip if register `s` equals register `t` | 148 | | `LOAD` | `6snn` | 2 | Load register `s` with value `nn` | 149 | | `ADD` | `7snn` | 2 | Add value `nn` to register `s` | 150 | | `MOVE` | `8st0` | 2 | Move value from register `s` to register `t` | 151 | | `OR` | `8st1` | 2 | Perform logical OR on register `s` and `t` and store in `t` | 152 | | `AND` | `8st2` | 2 | Perform logical AND on register `s` and `t` and store in `t` | 153 | | `XOR` | `8st3` | 2 | Perform logical XOR on register `s` and `t` and store in `t` | 154 | | `ADDR` | `8st4` | 2 | Add `s` to `t` and store in `s` - register `F` set on carry | 155 | | `SUB` | `8st5` | 2 | Subtract `s` from `t` and store in `s` - register `F` set on !borrow | 156 | | `SHR` | `8st6` | 2 | Shift bits in `s` 1 bit right, store in `t` - bit 0 shifts to register `F` | 157 | | `SHL` | `8stE` | 2 | Shift bits in `s` 1 bit left, store in `t` - bit 7 shifts to register `F` | 158 | | `SKRNE` | `9st0` | 2 | Skip next instruction if register `s` not equal register `t` | 159 | | `LOADI` | `Annn` | 1 | Load index with value `nnn` | 160 | | `JUMPI` | `Bnnn` | 1 | Jump to address `nnn` + index | 161 | | `RAND` | `Ctnn` | 2 | Generate random number between 0 and `nn` and store in `t` | 162 | | `DRAW` | `Dstn` | 3 | Draw `n` byte sprite at x location reg `s`, y location reg `t` | 163 | | `SKPR` | `Es9E` | 1 | Skip next instruction if the key in reg `s` is pressed | 164 | | `SKUP` | `EsA1` | 1 | Skip next instruction if the key in reg `s` is not pressed | 165 | | `MOVED` | `Ft07` | 1 | Move delay timer value into register `t` | 166 | | `KEYD` | `Ft0A` | 1 | Wait for keypress and store in register `t` | 167 | | `LOADD` | `Fs15` | 1 | Load delay timer with value in register `s` | 168 | | `LOADS` | `Fs18` | 1 | Load sound timer with value in register `s` | 169 | | `ADDI` | `Fs1E` | 1 | Add value in register `s` to index | 170 | | `LDSPR` | `Fs29` | 1 | Load index with sprite from register `s` | 171 | | `BCD` | `Fs33` | 1 | Store the binary coded decimal value of register `s` at index | 172 | | `STOR` | `Fs55` | 1 | Store the values of register `s` registers at index | 173 | | `READ` | `Fs65` | 1 | Read back the stored values at index into registers | 174 | 175 | ### Super Chip 8 Mnemonics 176 | 177 | | Mnemonic | Opcode | Operands | Description | 178 | |----------| ------ |:--------:|----------------------------------------------| 179 | | `SCRD` | `00Cn` | 1 | Scroll down `n` lines | 180 | | `SCRR` | `00FB` | 0 | Scroll right 4 pixels | 181 | | `SCRL` | `00FC` | 0 | Scroll left 4 pixels | 182 | | `EXIT` | `00FD` | 0 | Exit interpreter | 183 | | `EXTD` | `00FE` | 0 | Disable extended mode | 184 | | `EXTE` | `00FF` | 0 | Enable extended mode | 185 | | `SRPL` | `Fs75` | 1 | Store subset of registers in RPL store | 186 | | `LRPL` | `Fs85` | 1 | Read back subset of registers from RPL store | 187 | 188 | ### XO Chip Mnemonics 189 | 190 | | Mnemonic | Opcode | Operands | Description | 191 | |------------|--------|:--------:|-------------------------------------------------------| 192 | | `SCRU` | `00Dn` | 1 | Scrolls the current bitplane up `n` pixels | 193 | | `SAVESUB` | `5st2` | 2 | Saves subset of registers from `s` to `t` in memory | 194 | | `LOADSUB` | `5st3` | 2 | Loads subset of registers from `s` to `t` from memory | 195 | | `AUDIO` | `F002` | 0 | Load 16-byte audio pattern buffer from `index` | 196 | | `PLANE` | `Fn03` | 1 | Sets the drawing bitplane to `n` | 197 | | `PITCH` | `Fs3A` | 1 | Sets the internal pitch to the value in register `s` | 198 | 199 | 200 | ### Pseudo Operations 201 | 202 | | Mnemonic | Description | 203 | | -------- | ----------- | 204 | | `FCB` | Defines a single byte constant value | 205 | | `FDB` | Defines a double byte constant value | 206 | 207 | Both `FCB` and `FDB` should be defined at the end of your program, otherwise they will be interpreted as program code. 208 | 209 | 210 | ### Operands 211 | 212 | Operands may be one of three different types: 213 | 214 | | Operand Type | Example | Description | 215 | | ------------ | :-----: | ----------- | 216 | | Register | `r4` | Valid registers are in the range 0-F and must start with an `r` or `R` | 217 | | Hex value | `$1234` | Specifies a hexadecimal value. Must begin with a `$`. | 218 | | Label | `start` | Labels may be any string as long as it is not `r1` - `rF` | 219 | 220 | 221 | ## License 222 | 223 | Please see the file called `LICENSE`. 224 | 225 | 226 | ## Further Documentation 227 | 228 | The best documentation is in the code itself. Please feel free to examine the 229 | code and experiment with it. 230 | -------------------------------------------------------------------------------- /test/test_statement.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2024 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | A Chip 8 assembler - see the README.md file for details. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import unittest 10 | 11 | from chip8asm.statement import Statement, OPERATIONS 12 | from chip8asm.exceptions import ParseError, TranslationError 13 | 14 | # C L A S S E S ############################################################### 15 | 16 | 17 | class TestStatement(unittest.TestCase): 18 | """ 19 | A test class for the Chip 8 Statement class. 20 | """ 21 | def setUp(self): 22 | """ 23 | Common setup routines needed for all unit tests. 24 | """ 25 | pass 26 | 27 | def test_is_register_recognizes_only_registers(self): 28 | self.assertTrue(Statement.is_register("r0")) 29 | self.assertTrue(Statement.is_register("r1")) 30 | self.assertTrue(Statement.is_register("r2")) 31 | self.assertTrue(Statement.is_register("r3")) 32 | self.assertTrue(Statement.is_register("r4")) 33 | self.assertTrue(Statement.is_register("r5")) 34 | self.assertTrue(Statement.is_register("r6")) 35 | self.assertTrue(Statement.is_register("r7")) 36 | self.assertTrue(Statement.is_register("r8")) 37 | self.assertTrue(Statement.is_register("r9")) 38 | self.assertTrue(Statement.is_register("ra")) 39 | self.assertTrue(Statement.is_register("rb")) 40 | self.assertTrue(Statement.is_register("rc")) 41 | self.assertTrue(Statement.is_register("rd")) 42 | self.assertTrue(Statement.is_register("re")) 43 | self.assertTrue(Statement.is_register("rf")) 44 | 45 | self.assertTrue(Statement.is_register("rA")) 46 | self.assertTrue(Statement.is_register("rB")) 47 | self.assertTrue(Statement.is_register("rC")) 48 | self.assertTrue(Statement.is_register("rD")) 49 | self.assertTrue(Statement.is_register("rE")) 50 | self.assertTrue(Statement.is_register("rF")) 51 | 52 | self.assertTrue(Statement.is_register("R0")) 53 | self.assertTrue(Statement.is_register("R1")) 54 | self.assertTrue(Statement.is_register("R2")) 55 | self.assertTrue(Statement.is_register("R3")) 56 | self.assertTrue(Statement.is_register("R4")) 57 | self.assertTrue(Statement.is_register("R5")) 58 | self.assertTrue(Statement.is_register("R6")) 59 | self.assertTrue(Statement.is_register("R7")) 60 | self.assertTrue(Statement.is_register("R8")) 61 | self.assertTrue(Statement.is_register("R9")) 62 | self.assertTrue(Statement.is_register("Ra")) 63 | self.assertTrue(Statement.is_register("Rb")) 64 | self.assertTrue(Statement.is_register("Rc")) 65 | self.assertTrue(Statement.is_register("Rd")) 66 | self.assertTrue(Statement.is_register("Re")) 67 | self.assertTrue(Statement.is_register("Rf")) 68 | 69 | self.assertTrue(Statement.is_register("RA")) 70 | self.assertTrue(Statement.is_register("RB")) 71 | self.assertTrue(Statement.is_register("RC")) 72 | self.assertTrue(Statement.is_register("RD")) 73 | self.assertTrue(Statement.is_register("RE")) 74 | self.assertTrue(Statement.is_register("RF")) 75 | 76 | def test_is_register_rejects_bad_register_names(self): 77 | self.assertFalse(Statement.is_register("R11")) 78 | self.assertFalse(Statement.is_register("r11")) 79 | self.assertFalse(Statement.is_register("R")) 80 | self.assertFalse(Statement.is_register("r")) 81 | self.assertFalse(Statement.is_register("register")) 82 | 83 | def test_get_value_returns_hex(self): 84 | self.assertEqual('10', Statement.get_value("$10")) 85 | 86 | def test_get_register_correct(self): 87 | self.assertEqual('0', Statement.get_register("r0")) 88 | self.assertEqual('1', Statement.get_register("r1")) 89 | self.assertEqual('2', Statement.get_register("r2")) 90 | self.assertEqual('3', Statement.get_register("r3")) 91 | self.assertEqual('4', Statement.get_register("r4")) 92 | self.assertEqual('5', Statement.get_register("r5")) 93 | self.assertEqual('6', Statement.get_register("r6")) 94 | self.assertEqual('7', Statement.get_register("r7")) 95 | self.assertEqual('8', Statement.get_register("r8")) 96 | self.assertEqual('9', Statement.get_register("r9")) 97 | self.assertEqual('A', Statement.get_register("rA")) 98 | self.assertEqual('B', Statement.get_register("rB")) 99 | self.assertEqual('C', Statement.get_register("rC")) 100 | self.assertEqual('D', Statement.get_register("rD")) 101 | self.assertEqual('E', Statement.get_register("rE")) 102 | self.assertEqual('F', Statement.get_register("rF")) 103 | self.assertEqual('0', Statement.get_register("R0")) 104 | self.assertEqual('1', Statement.get_register("R1")) 105 | self.assertEqual('2', Statement.get_register("R2")) 106 | self.assertEqual('3', Statement.get_register("R3")) 107 | self.assertEqual('4', Statement.get_register("R4")) 108 | self.assertEqual('5', Statement.get_register("R5")) 109 | self.assertEqual('6', Statement.get_register("R6")) 110 | self.assertEqual('7', Statement.get_register("R7")) 111 | self.assertEqual('8', Statement.get_register("R8")) 112 | self.assertEqual('9', Statement.get_register("R9")) 113 | self.assertEqual('A', Statement.get_register("RA")) 114 | self.assertEqual('B', Statement.get_register("RB")) 115 | self.assertEqual('C', Statement.get_register("RC")) 116 | self.assertEqual('D', Statement.get_register("RD")) 117 | self.assertEqual('E', Statement.get_register("RE")) 118 | self.assertEqual('F', Statement.get_register("RF")) 119 | 120 | def test_get_register_bad_register(self): 121 | with self.assertRaises(TranslationError): 122 | Statement.get_register("r11") 123 | 124 | def test_is_pseudo_op_correct(self): 125 | statement = Statement() 126 | statement.mnemonic = "FDB" 127 | self.assertTrue(statement.is_pseudo_op()) 128 | statement.mnemonic = "FCB" 129 | self.assertTrue(statement.is_pseudo_op()) 130 | statement.mnemonic = "BLAH" 131 | self.assertFalse(statement.is_pseudo_op()) 132 | 133 | def test_has_comment_correct(self): 134 | statement = Statement() 135 | self.assertFalse(statement.has_comment()) 136 | statement.comment = "blah" 137 | self.assertTrue(statement.has_comment()) 138 | 139 | def test_is_empty_correct(self): 140 | statement = Statement() 141 | self.assertTrue(statement.is_empty()) 142 | statement.empty = False 143 | self.assertFalse(statement.is_empty()) 144 | 145 | def test_get_label_correct(self): 146 | statement = Statement() 147 | self.assertEqual("", statement.get_label()) 148 | statement.label = "label" 149 | self.assertEqual("label", statement.get_label()) 150 | 151 | def test_get_comment_correct(self): 152 | statement = Statement() 153 | self.assertEqual("", statement.get_comment()) 154 | statement.comment = "comment" 155 | self.assertEqual("comment", statement.get_comment()) 156 | 157 | def test_get_mnemonic_correct(self): 158 | statement = Statement() 159 | self.assertEqual("", statement.get_mnemonic()) 160 | statement.mnemonic = "mnemonic" 161 | self.assertEqual("mnemonic", statement.get_mnemonic()) 162 | 163 | def test_get_op_code_correct(self): 164 | statement = Statement() 165 | self.assertEqual("", statement.get_op_code()) 166 | statement.op_code = "op_code" 167 | self.assertEqual("op_code", statement.get_op_code()) 168 | 169 | def test_get_operands_correct(self): 170 | statement = Statement() 171 | self.assertEqual("", statement.get_operands()) 172 | statement.operands = "operands" 173 | self.assertEqual("operands", statement.get_operands()) 174 | 175 | def test_get_address_correct(self): 176 | statement = Statement() 177 | self.assertEqual("", statement.get_address()) 178 | statement.address = "address" 179 | self.assertEqual("address", statement.get_address()) 180 | 181 | def test_set_address_correct(self): 182 | statement = Statement() 183 | self.assertEqual("", statement.get_address()) 184 | statement.set_address("address") 185 | self.assertEqual("address", statement.get_address()) 186 | 187 | def test_parsing_recognizes_blank_line(self): 188 | statement = Statement() 189 | statement.parse_line(" ") 190 | self.assertFalse(statement.has_comment()) 191 | self.assertIsNone(statement.comment) 192 | self.assertIsNone(statement.operation) 193 | self.assertIsNone(statement.label) 194 | self.assertIsNone(statement.operands) 195 | self.assertIsNone(statement.address) 196 | self.assertIsNone(statement.mnemonic) 197 | self.assertEqual(0, statement.size) 198 | 199 | def test_parsing_recognizes_comment_line(self): 200 | statement = Statement() 201 | statement.parse_line("# This is a comment") 202 | self.assertTrue(statement.has_comment()) 203 | self.assertEqual("This is a comment", statement.comment) 204 | self.assertIsNone(statement.operation) 205 | self.assertIsNone(statement.label) 206 | self.assertIsNone(statement.operands) 207 | self.assertIsNone(statement.address) 208 | self.assertIsNone(statement.mnemonic) 209 | self.assertEqual(0, statement.size) 210 | 211 | def test_parsing_correct_asm_line(self): 212 | statement = Statement() 213 | statement.parse_line("label mnemonic operands # comment") 214 | self.assertTrue(statement.has_comment()) 215 | self.assertEqual("comment", statement.comment) 216 | self.assertIsNone(statement.operation) 217 | self.assertEqual("label", statement.label) 218 | self.assertEqual("operands", statement.operands) 219 | self.assertIsNone(statement.address) 220 | self.assertEqual("mnemonic", statement.mnemonic) 221 | self.assertEqual(0, statement.size) 222 | 223 | def test_parse_bad_line_raises_error(self): 224 | statement = Statement() 225 | with self.assertRaises(ParseError): 226 | statement.parse_line("bad") 227 | 228 | def test_translate_pseudo_op_does_nothing(self): 229 | statement = Statement() 230 | statement.parse_line(" FDB $FFEE") 231 | statement.translate() 232 | self.assertFalse(statement.has_comment()) 233 | self.assertIsNone(statement.comment) 234 | self.assertIsNone(statement.operation) 235 | self.assertIsNone(statement.label) 236 | self.assertEqual("FDB", statement.mnemonic) 237 | self.assertEqual("$FFEE", statement.operands) 238 | self.assertIsNone(statement.address) 239 | self.assertEqual(0, statement.size) 240 | 241 | def test_translate_pseudo_bad_num_operands_raises_error(self): 242 | statement = Statement() 243 | statement.parse_line(" FDB") 244 | with self.assertRaises(TranslationError): 245 | statement.translate() 246 | 247 | def test_translate_valid_line_correct(self): 248 | statement = Statement() 249 | statement.parse_line("label SKRNE r1,r2 # comment") 250 | statement.translate() 251 | self.assertTrue(statement.has_comment()) 252 | self.assertEqual("comment", statement.comment) 253 | self.assertEqual(OPERATIONS[19], statement.operation) 254 | self.assertEqual("label", statement.label) 255 | self.assertEqual("SKRNE", statement.mnemonic) 256 | self.assertEqual("r1,r2", statement.operands) 257 | self.assertIsNone(statement.address) 258 | self.assertEqual(0, statement.size) 259 | 260 | def test_translate_bad_mnemonic_raises_error(self): 261 | statement = Statement() 262 | statement.parse_line("label BLAH r1,r2 # comment") 263 | with self.assertRaises(TranslationError): 264 | statement.translate() 265 | 266 | def test_translate_bad_num_operands_raises_error(self): 267 | statement = Statement() 268 | statement.parse_line("label JUMP # comment") 269 | with self.assertRaises(TranslationError): 270 | statement.translate() 271 | 272 | def test_replace_label_correct(self): 273 | statement = Statement() 274 | statement.source = "source" 275 | statement.target = "target" 276 | statement.numeric = "numeric" 277 | statement.replace_label("source", "replaced source") 278 | self.assertEqual("replaced source", statement.source) 279 | self.assertEqual("target", statement.target) 280 | self.assertEqual("numeric", statement.numeric) 281 | statement.replace_label("target", "replaced target") 282 | self.assertEqual("replaced source", statement.source) 283 | self.assertEqual("replaced target", statement.target) 284 | self.assertEqual("numeric", statement.numeric) 285 | statement.replace_label("numeric", "replaced numeric") 286 | self.assertEqual("replaced source", statement.source) 287 | self.assertEqual("replaced target", statement.target) 288 | self.assertEqual("replaced numeric", statement.numeric) 289 | 290 | # E N D O F F I L E ####################################################### 291 | -------------------------------------------------------------------------------- /chip8asm/statement.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2024 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | This file contains Exceptions for the Chip 8 Assembler. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import re 10 | 11 | from collections import namedtuple 12 | from copy import copy 13 | 14 | from chip8asm.exceptions import TranslationError, ParseError 15 | 16 | # C O N S T A N T S ########################################################### 17 | 18 | SOURCE = "source" 19 | TARGET = "target" 20 | NUMERIC = "numeric" 21 | SOURCE_REG = "s" 22 | TARGET_REG = "t" 23 | NUMERIC_REG = "n" 24 | 25 | Operation = namedtuple('Operation', ['mnemonic', 'op', 'operands', 'source', 'target', 'numeric']) 26 | 27 | OPERATIONS = [ 28 | Operation(op="0nnn", operands=1, source=0, target=0, numeric=3, mnemonic="SYS"), 29 | Operation(op="00E0", operands=0, source=0, target=0, numeric=0, mnemonic="CLR"), 30 | Operation(op="00EE", operands=0, source=0, target=0, numeric=0, mnemonic="RTS"), 31 | Operation(op="1nnn", operands=1, source=0, target=0, numeric=3, mnemonic="JUMP"), 32 | Operation(op="2nnn", operands=1, source=0, target=0, numeric=3, mnemonic="CALL"), 33 | Operation(op="3snn", operands=2, source=1, target=0, numeric=2, mnemonic="SKE"), 34 | Operation(op="4snn", operands=2, source=1, target=0, numeric=2, mnemonic="SKNE"), 35 | Operation(op="5st0", operands=2, source=1, target=1, numeric=0, mnemonic="SKRE"), 36 | Operation(op="6snn", operands=2, source=1, target=0, numeric=2, mnemonic="LOAD"), 37 | Operation(op="7snn", operands=2, source=1, target=0, numeric=2, mnemonic="ADD"), 38 | Operation(op="8st0", operands=2, source=1, target=1, numeric=0, mnemonic="MOVE"), 39 | Operation(op="8st1", operands=2, source=1, target=1, numeric=0, mnemonic="OR"), 40 | Operation(op="8st2", operands=2, source=1, target=1, numeric=0, mnemonic="AND"), 41 | Operation(op="8st3", operands=2, source=1, target=1, numeric=0, mnemonic="XOR"), 42 | Operation(op="8st4", operands=2, source=1, target=1, numeric=0, mnemonic="ADDR"), 43 | Operation(op="8st5", operands=2, source=1, target=1, numeric=0, mnemonic="SUB"), 44 | Operation(op="8st6", operands=1, source=1, target=1, numeric=0, mnemonic="SHR"), 45 | Operation(op="8st7", operands=2, source=1, target=1, numeric=0, mnemonic="SUBN"), 46 | Operation(op="8stE", operands=1, source=1, target=1, numeric=0, mnemonic="SHL"), 47 | Operation(op="9st0", operands=2, source=1, target=1, numeric=0, mnemonic="SKRNE"), 48 | Operation(op="Annn", operands=1, source=0, target=0, numeric=3, mnemonic="LOADI"), 49 | Operation(op="Bnnn", operands=1, source=0, target=0, numeric=3, mnemonic="JUMPI"), 50 | Operation(op="Ctnn", operands=2, source=0, target=1, numeric=2, mnemonic="RAND"), 51 | Operation(op="Dstn", operands=3, source=1, target=1, numeric=1, mnemonic="DRAW"), 52 | Operation(op="Es9E", operands=1, source=1, target=0, numeric=0, mnemonic="SKPR"), 53 | Operation(op="EsA1", operands=1, source=1, target=0, numeric=0, mnemonic="SKUP"), 54 | Operation(op="Ft07", operands=1, source=0, target=1, numeric=0, mnemonic="MOVED"), 55 | Operation(op="Ft0A", operands=1, source=0, target=1, numeric=0, mnemonic="KEYD"), 56 | Operation(op="Fs15", operands=1, source=1, target=0, numeric=0, mnemonic="LOADD"), 57 | Operation(op="Fs18", operands=1, source=1, target=0, numeric=0, mnemonic="LOADS"), 58 | Operation(op="Fs1E", operands=1, source=1, target=0, numeric=0, mnemonic="ADDI"), 59 | Operation(op="Fs29", operands=1, source=1, target=0, numeric=0, mnemonic="LDSPR"), 60 | Operation(op="Fs33", operands=1, source=1, target=0, numeric=0, mnemonic="BCD"), 61 | Operation(op="Fs55", operands=1, source=1, target=0, numeric=0, mnemonic="STOR"), 62 | Operation(op="Fs65", operands=1, source=1, target=0, numeric=0, mnemonic="READ"), 63 | # Super Chip 8 Instructions 64 | Operation(op="00Cn", operands=1, source=0, target=0, numeric=1, mnemonic="SCRD"), 65 | Operation(op="00FB", operands=0, source=0, target=0, numeric=0, mnemonic="SCRR"), 66 | Operation(op="00FC", operands=0, source=0, target=0, numeric=0, mnemonic="SCRL"), 67 | Operation(op="00FD", operands=0, source=0, target=0, numeric=0, mnemonic="EXIT"), 68 | Operation(op="00FE", operands=0, source=0, target=0, numeric=0, mnemonic="EXTD"), 69 | Operation(op="00FF", operands=0, source=0, target=0, numeric=0, mnemonic="EXTE"), 70 | Operation(op="Fs75", operands=1, source=1, target=0, numeric=0, mnemonic="SRPL"), 71 | Operation(op="Fs85", operands=1, source=1, target=0, numeric=0, mnemonic="LRPL"), 72 | # XO Chip Instructions 73 | Operation(op="00Dn", operands=1, source=0, target=0, numeric=1, mnemonic="SCRU"), 74 | Operation(op="5st2", operands=2, source=1, target=1, numeric=0, mnemonic="SAVESUB"), 75 | Operation(op="5st3", operands=2, source=1, target=1, numeric=0, mnemonic="LOADSUB"), 76 | Operation(op="F002", operands=0, source=0, target=0, numeric=0, mnemonic="AUDIO"), 77 | Operation(op="Fn03", operands=1, source=0, target=0, numeric=1, mnemonic="PLANE"), 78 | Operation(op="Fs3A", operands=1, source=1, target=0, numeric=0, mnemonic="PITCH"), 79 | ] 80 | 81 | # Pseudo operations 82 | FCB = "FCB" 83 | FDB = "FDB" 84 | PSEUDO_OPERATIONS = [FCB, FDB] 85 | 86 | # Pattern to recognize a blank line 87 | BLANK_LINE_REGEX = re.compile(r"^\s*$") 88 | 89 | # Pattern to parse a comment line 90 | COMMENT_LINE_REGEX = re.compile(r"^\s*#\s*(?P.*)$") 91 | 92 | # Pattern to parse a single line 93 | ASM_LINE_REGEX = re.compile( 94 | r"^(?P