nessemble
nessemble is a 6502 assembler targeting the Nintendo Entertainment System
(NES), written in Rust.
Upgrading from a 1.x release? See Upgrading for what changed in 2.0.
Getting Started
To initialize a new project:
nessemble init
Build the project:
nessemble project.asm --output project.nes --format nes
Run project.nes in any NES emulator to see the result.
Documentation
Start here: Installation.
- Usage — the command-line interface.
- Syntax — the assembly language reference.
- Extending — custom pseudo-instructions with Rhai.
- Building — building from source.
- Translating — adding a locale.
Installation
Download and install the latest release for your system:
https://github.com/kevinselwyn/nessemble-rs/releases
Release artifacts are provided for all five supported platforms:
| Platform | Artifact(s) |
|---|---|
| macOS | nessemble_<v>.pkg, nessemble_<v>_macos.tar.gz |
| Linux amd64 | nessemble_<v>_amd64.deb |
| Linux i386 | nessemble_<v>_i386.deb |
| Windows 32-bit | nessemble_<v>_win32.exe, …_win32.msi |
| Windows 64-bit | nessemble_<v>_win64.exe, …_win64.msi |
macOS: "Apple could not verify…"
The macOS .pkg is not signed with an Apple Developer ID or notarized, so after
you download it, Gatekeeper blocks it with:
"nessemble_<v>.pkg" Not Opened — Apple could not verify "nessemble_<v>.pkg" is free of malware…
This is expected for an unsigned package; it does not mean the file is harmful. You have two options.
Install the .pkg anyway — clear the download-quarantine flag, then install:
xattr -d com.apple.quarantine nessemble_<v>.pkg
sudo installer -pkg nessemble_<v>.pkg -target /
Or use the plain binary tarball (nessemble_<v>_macos.tar.gz) and skip the
installer:
tar -xzf nessemble_<v>_macos.tar.gz
xattr -d com.apple.quarantine nessemble # clear quarantine
sudo mv nessemble /usr/local/bin/ # put it on your PATH
Both install the same binary to /usr/local/bin/nessemble. (The tarball binary
is a 64-bit Intel build, matching the .pkg; it runs on Apple Silicon under
Rosetta.)
From source
nessemble is a Cargo workspace and builds with a stock Rust toolchain.
git clone https://github.com/kevinselwyn/nessemble-rs
cd nessemble-rs
cargo build --release
The binary is written to target/release/nessemble. See
Building for cross-compilation and packaging details.
Upgrading from 1.x to 2.x
nessemble 2.0 is a ground-up rewrite in Rust. Assembly source and ROM output
are compatible — the same .asm files assemble to the same bytes — but the tool
around them changed. This page covers what a 1.x user needs to know.
Assembly & ROM output
- No changes needed to your source. The assembly language (instructions, addressing modes, expressions, labels, macros, conditionals, includes, data and iNES directives, media importers) is unchanged, and assembled ROMs are byte-for-byte identical to 1.x output. The one behavioral change is how relative filenames are resolved — see Include & asset paths.
Include & asset paths
- In 1.x, every filename-based directive resolved its path against a single global working directory (the top-level file's directory). A relative path in an included file was therefore resolved from the project root, not from the included file.
- In 2.x, relative filenames in
.include,.inestrn, and the.inc*media importers (.incbin,.incpng,.incpal,.incrle,.incwav) resolve relative to the directory of the file that contains the directive. This makes subdirectory modules self-contained: a file insub/that does.include "helper.asm"or.incbin "data.bin"now finds them insub/. - What to check: if a file you
.includefrom a subdirectory referenced a sibling file or asset by a path written relative to the project root, update that path to be relative to the including file instead. Projects that keep each file's includes and assets alongside it need no changes.
Custom pseudo-instructions
- The three embedded scripting engines (JavaScript, Lua, and Scheme) and native
shared-object (
.so/.dll) plugins are replaced by a single embedded language, Rhai. - Rewrite custom scripts as
.rhaifiles and update your--pseudomapping to point at them. A script now definesfn custom(ints, texts)and returns the emitted bytes. See Extending. - The bundled
easescript is provided as.rhai; runnessemble scriptsto install it. - Rhai scripts can still read and write files (as the old Lua/Scheme hosts
could), via the
rhai-fsopen_fileAPI; relative paths resolve against the source file's directory. See Filesystem access. - Script paths in a
--pseudomapping now resolve relative to the mapping file's own directory, not the source file's directory. In 1.x the paths were resolved against the input file's location (the tool'scwd_path). If yourpseudo.txtsits next to the source you assemble, nothing changes; otherwise, keep each script beside the mapping file that names it (or update the paths).
Removed commands and options
The following 1.x features are not part of 2.x — they are not parsed and do
not appear in --help:
- The disassembler / reassembler (
-d/--disassemble,-R/--reassemble). - The simulator / debugger (
-s/--simulate,-r/--recipe). - The package registry:
registry,install,uninstall,publish,info,ls,search, and the user/auth commands (adduser,login,logout,forgotpassword,resetpassword).
config remains, but is now a general key/value store (the registry key it used
to manage is gone).
Internationalization
- Translations moved from gettext (
.po/.mo) to Project Fluent. Drop a~/.nessemble/locales/<lang>.ftlfile and select it withNESSEMBLE_LANG. See Translating.
Building & installing
- Building no longer needs a C toolchain, flex/bison, or gettext — just a Rust toolchain. See Building.
- Release artifacts (
.deb,.msi,.pkg, and standalone.exe) are provided for the same platforms as before. See Installation.
Usage
Usage: nessemble [options] <infile.asm>
<command> [args]
Options:
-o, --output <outfile.rom> output file
-f, --format {NES,RAW} output format
-e, --empty <hex> empty byte value
-u, --undocumented use undocumented opcodes
-l, --list <listfile.txt> generate list of labels and constants
-p, --pseudo <pseudo.txt> use custom pseudo-instruction functions
-c, --check check syntax only
-C, --coverage log data coverage
-v, --version display program version
-L, --license display program license
-h, --help print this message
Commands:
init [<arg> ...] initialize new project
scripts install scripts
reference [<category>] [<term>] get reference info about assembly terms
config [<key>] [<val>] list/get/set config info
lsp run the language server (stdio)
The lsp command starts the built-in Language Server for use with
LSP-capable editors.
Options
-o, --output <outfile.rom>
Sets the filename where output is written. An outfile of - (or omitting the
flag) writes to stdout.
nessemble infile.asm --output outfile.rom
-f, --format
Specifies the output format:
NES— an iNES ROM, complete with a 16-byte header.RAW— raw assembled 6502 code.
The format is RAW by default, but if iNES header directives (.inesprg,
.ineschr, …) are present, it becomes NES unless overridden.
-e, --empty <hex>
Sets the fill value for empty/unwritten ROM bytes. Defaults to FF.
nessemble infile.asm --empty 00
-u, --undocumented
Allows the use of undocumented ("illegal") opcodes.
-l, --list <listfile.txt>
Writes a list of labels and constants to the given file.
-p, --pseudo <pseudo.txt>
Points to a mapping file that enables custom pseudo-instructions. See Extending.
-c, --check
Checks the input for syntax errors only; produces no output.
-C, --coverage
Reports per-bank ROM coverage. Only meaningful when the format is NES.
-v, --version / -L, --license / -h, --help
Print the version, license, or usage message respectively.
Commands
init [<arg> ...]
Scaffolds a new project, prompting for any values not supplied as arguments:
nessemble init [filename] [prg] [chr] [mapper] [mirroring]
filename— file to create.prg/chr— number of PRG / CHR banks.mapper/mirroring— iNES mapper and mirroring.
scripts
Installs the bundled custom-pseudo-instruction scripts into
~/.nessemble/scripts. See Extending.
reference [<category>] [<term>]
Prints reference information from locally bundled data. With no arguments it
lists the categories (instructions, directives); with a category it lists
its entries; with a term it prints the details (e.g. reference instructions LDA).
config [<key>] [<val>]
Gets or sets configuration stored in ~/.nessemble/config. With no arguments it
lists all keys; with a <key> it prints that value; with a <key> and <val>
it sets the key.
Syntax
Numbers
Binary, decimal, octal, hexadecimal, and ASCII character are all valid numbers.
| Base | Example A | Example B |
|---|---|---|
| Binary | %01000001 | 01000001b |
| Decimal | 65 | 65d |
| Octal | 0101 | 101o |
| Hexadecimal | $41 | 41h |
| ASCII char | 'A' |
Symbols
Mathematical Operators
| Symbol | Description |
|---|---|
| + | Add |
| - | Subtract |
| * | Multiply |
| / | Divide |
| ** | Exponent |
| & | Bitwise AND |
| | | Bitwise OR |
| ^ | Bitwise XOR |
| >> | Shift right |
| << | Shift left |
| % | Modulo |
Comparison Operators
| Symbol | Description |
|---|---|
| == | Equals |
| != | Not equals |
| < | Less than |
| > | Greater than |
| <= | Less than or equals |
| >= | Greater than or equals |
Special
| Symbol | Description |
|---|---|
| -> | Accessor (functions like +) |
Labels
Named
Named label declarations must be in the follow format:
NAME:
NAME- Label name, required.
Example:
LDX #$08
loop:
DEX
BNE loop
BRK
Output:
00000000 a2 08 ca d0 fd 00 |......|
00000006
LDX #$08
loop:
DEX
BNE loop
BRK
Temporary
Temporary/un-named labels may also be declared by placing only a colon.
:
To jump to a temporary label, the direction and count of the jumps must be given.
JMP :[+-]
[+-] - Direction, required.
N-number of +s means to jump to the temporary label that is N temporary labels
further down in the code.
N-number of -s means to jump to the temporary label that is N temporary labels
further up in the code.
Example:
LDX #$08
:
DEX
BNE :-
BRK
Output:
00000000 a2 08 ca d0 fd 00 |......|
00000006
LDX #$08
:
DEX
BNE :-
BRK
Mnemonics
All 56 mnemonics are supported:
| Mnemonic | Description |
|---|---|
| ADC | Add with Carry |
| AND | Bitwise AND with Accumulator |
| ASL | Arithmetic shift left |
| BIT | Test bits |
| BCC | Branch on Carry clear |
| BCS | Branch on Carry set |
| BEQ | Branch on equal |
| BMI | Branch on minus |
| BNE | Branch on not equal |
| BPL | Branch on plus |
| BRK | Break |
| BVC | Branch on Overflow clear |
| BVS | Branch on Overflow set |
| CLC | Clear Carry |
| CLD | Clear Decimal |
| CLI | Clear Interrupt |
| CLV | Clear Overflow |
| CMP | Compare Accumulator |
| CPX | Compare X register |
| CPY | Compare Y register |
| DEC | Decrement memory |
| DEX | Decrement X register |
| DEY | Decrement Y register |
| EOR | Bitwise exclusive OR |
| INC | Increment memory |
| INX | Increment X register |
| INY | Increment Y register |
| JMP | Jump |
| JSR | Jump to subroutine |
| LDA | Load Accumulator |
| LDX | Load X register |
| LDY | Load Y register |
| LSR | Logical shift right |
| NOP | No operation |
| ORA | Bitwise OR with Accumulator |
| PHA | Push Accumulator |
| PHP | Push processor status |
| PLA | Pull Accumulator |
| PLP | Pull processor status |
| ROL | Rotate left |
| ROR | Rotate right |
| RTI | Return from Interrupt |
| RTS | Return from subroutine |
| SBC | Subtract with Carry |
| SEC | Set Carry |
| SED | Set Decimal |
| SEI | Set Interrupt |
| STA | Store Accumulator |
| STX | Store X register |
| STY | Store Y register |
| TAX | Transfer Accumulator to X register |
| TAY | Transfer Accumulator to Y register |
| TSX | Transfer Stack Pointer to X register |
| TXA | Transfer X register to Accumulator |
| TXS | Transfer X register to Stack Pointer |
| TYA | Transfer Y register to Accumulator |
Read more about 6502 opcodes here.
In addition, 24 illegal/undocumented mnemonics may be used when assembled with
the -u, --undocumented flag.
| Mnemonic | Description |
|---|---|
| AAC | AND with Accumulator |
| AAX | AND X register with Accumulator |
| ARR | AND with Accumulator |
| ASR | AND with Accumulator |
| ATX | AND with Accumulator |
| AXA | AND X register with Accumulator |
| AXS | AND X register with Accumulator |
| DCP | Subtract 1 from memory |
| DOP | No operation (x2) |
| ICS | Increase memory by 1 |
| KIL | Stop program counter |
| LAR | AND memory with stack pointer |
| LAX | Load Accumulator and X register |
| NOP | No operation |
| RLA | Rotate one bit left in memory |
| RRA | Rotate one bit right in memory |
| SBC | Subtract with Carry |
| SLO | Shift left one bit in memory |
| SRE | Shift right one bit in memory |
| SXA | AND Y register with the high byte of address |
| SYA | AND Y register with the high byte of address |
| TOP | No operation (x3) |
| XAA | Unknown |
| XAS | AND X register with Accumulator |
Read more about undocumented 6502 opcodes here.
Addressing Modes
| Mode | Example |
|---|---|
| Implied | RTS |
| Accumulator | ROL A |
| Immediate | LDA #$42 |
| Zeropage | STA <$42 |
| Zeropage, X | EOR <$42, X |
| Zeropage, Y | LDX <$42, Y |
| Absolute | STA $4200 |
| Absolute, X | EOR $4200, X |
| Absolute, Y | LDX $4200, Y |
| Indirect | JMP [$4200] |
| Indirect, X | LDA [$42, X] |
| Indirect, Y | STA [$42], Y |
| Relative | BEQ label |
nessembleuses square brackets[]instead of parentheses()in its addressing modes because the latter are used to indicate precedence in mathematical operations.
Read more about 6502 addressing modes here.
Functions
| Function | Description |
|---|---|
| HIGH() | Get high byte of address |
| LOW() | Get low byte of address |
| BANK() | Get bank of address |
Pseudo-Instructions
| Pseudo-Instruction | Description |
|---|---|
| .ascii | Convert ASCII string to bytes |
| .byte | Alias for .db |
| .checksum | Calculate crc32 checksum |
| .chr | Set CHR bank index |
| .color | Convert hex color to NES color |
| .db | Define 8-bit byte(s) |
| .defchr | Define CHR tile |
| .dw | Define 16-bit word(s) |
| .else | Else condition of an .if/.ifdef/.ifndef statement |
| .endenum | End .enum |
| .endif | End .if/.ifdef/.ifndef statement |
| .endm | End .macrodef |
| .enum | Start enumerated variable declarations |
| .fill | Fill with bytes |
| .font | Generate font character tile |
| .hibytes | Output only the high byte of 16-bit word(s) |
| .if | Test if condition |
| .ifdef | Test if variable is defined |
| .ifndef | Test if variable has not been defined |
| .incbin | Include binary file |
| .include | Include assembly file |
| .incpal | Include palette from PNG |
| .incpng | Include PNG |
| .incrle | Include binary data to be RLE-encoded |
| .incwav | Include WAV |
| .ineschr | iNES CHR count |
| .inesmap | iNES mapper number |
| .inesmir | iNES mirroring |
| .inesprg | iNES PRG count |
| .inestrn | iNES trainer include |
| .lobytes | Output only the low byte of 16-bit word(s) |
| .macro | Call macro |
| .macrodef | Start macro definition |
| .org | Organize code |
| .out | Output debugging message |
| .prg | Set PRG bank index |
| .random | Output random byte(s) |
| .rsset | Set initial value for .rs declarations |
| .rs | Reserve space for variable declaration |
| .segment | Set code segment |
| .word | Alias for .dw |
.ascii
Convert ASCII string to bytes.
Usage:
.ascii "STRING"[(+/-)NUMBER]
"STRING"- String, required. ASCII string to turn into bytes. Must be within quotes.(+/-)NUMBER- Number, optional. Amount to increase/decrease ASCII values.
Example:
.ascii "When, in disgrace with fortune and men's eyes"
Output:
00000000 57 68 65 6e 2c 20 69 6e 20 64 69 73 67 72 61 63 |When, in disgrac|
00000010 65 20 77 69 74 68 20 66 6f 72 74 75 6e 65 20 61 |e with fortune a|
00000020 6e 64 20 6d 65 6e 27 73 20 65 79 65 73 |nd men's eyes|
0000002d
The +/- operators may also be used to increase/decrease the output.
Example:
.ascii "I all alone beweep my outcast state"-32
Output:
00000000 29 00 41 4c 4c 00 41 4c 4f 4e 45 00 42 45 57 45 |).ALL.ALONE.BEWE|
00000010 45 50 00 4d 59 00 4f 55 54 43 41 53 54 00 53 54 |EP.MY.OUTCAST.ST|
00000020 41 54 45 |ATE|
00000023
.checksum
Calculate crc32 checksum.
Usage:
.checksum LABEL
LABEL- Label, required. Label at which to start generating the checksum.
Example:
start:
LDA #$01
STA <$02
.checksum start
Output:
00000000 a9 01 85 02 b8 1f ee 86 |........|
00000008
The checksum is b8 1f ee 86.
Checksums may only be performed on preceding data.
start:
LDA #$01
STA <$02
.checksum start
.chr
Set CHR bank index.
Usage:
.chr NUMBER
NUMBER- Number, required. CHR bank index.
Example:
.chr 0
CHR banks are 2K bytes (0x2000) in size.
.color
Convert hex color to NES color.
Finds the closest valid NES color to the given hex color.
Usage:
.color NUMBER[, NUMBER, ...]
NUMBER- Number, required. At least one number is required., NUMBER, ...- Number(s), optional. Additional comma-separated numbers may be used.
Example:
.color $FF0000
Output:
00000000 16 |.|
00000001
Read more about the NES color palette here.
.db
Define 8-bit byte(s).
Usage:
.db NUMBER[, NUMBER, ...]
NUMBER- Number, required. At least one number is required., NUMBER, ...- Number(s), optional. Additional comma-separated numbers may be used.
Example:
.db $12, $34
Output:
00000000 12 34 |.4|
00000002
.defchr
Define CHR tile.
Only numbers from 0-3 may be used: 0 representing black, 1 dark grey, 2
light grey, and 3 representing white.
Usage:
.defchr XXXXXXXX,
XXXXXXXX,
XXXXXXXX,
XXXXXXXX,
XXXXXXXX,
XXXXXXXX,
XXXXXXXX,
XXXXXXXX
XXXXXXXX,- Number, required. Must be exactly 8 numbers of 8-characters each.
Example:
.defchr 333333333,
300000003,
300000003,
300000003,
300000003,
300000003,
300000003,
333333333
Output:
00000000 ff 01 01 01 01 01 01 ff ff 01 01 01 01 01 01 ff |................|
00000010
Read more about PPU pattern tables here.
.defchr 333333333,
300000003,
300000003,
300000003,
300000003,
300000003,
300000003,
333333333
.dw
Define 16-bit word(s).
Usage:
.dw NUMBER[, NUMBER, ...]
NUMBER- Number, required. At least one number is required., NUMBER, ...- Number(s), optional. Additional comma-separated numbers may be used.
Example:
.dw $1234, $45678
Output:
00000000 34 12 78 56 |4.xV|
00000004
.else
Else condition of an .if/.ifdef/.ifndef statement.
Usage:
.else
Example:
.ifdef SOMETHING
STA $00
.else
STA $01
.endif
.endenum
End .enum.
Usage:
.endenum
Example:
.enum $0080
TEST_0 .rs 1
TEST_1 .rs 2
TEST_2 .rs 1
.endenum
.endif
End .if/.ifdef/.ifndef statement.
Usage:
.endif
Example:
.ifdef SOMETHING
STA $00
.else
STA $01
.endif
.endm
End .macrodef.
Usage:
.endm
Example:
.macrodef TEST_MACRO
LDA #\1
STA <\2
.endm
See the section on Macros for more information.
.enum
Start enumerated variable declarations.
Usage:
.enum START[, INC]
START- Number, required. Value at which to start enumerating., INC- Number, optional. Amount to increment after each enumeration.
Example:
.enum $0080
TEST_0 .rs 1
TEST_1 .rs 2
TEST_2 .rs 1
.endenum
.fill
Fill with bytes.
Usage:
.fill COUNT[, VALUE]
COUNT- Number, required. Number of bytes to fill., VALUE- Number, optional. Value of each byte. Defaults to $FF.
Example:
.fill 16
Output:
00000000 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00000010
.font
Generate font character tile.
Usage:
.font START[, END]
START- Character/number, required. Starting ASCII character or code.[, END]- Character/number, optional. Ending ASCII character or code. If included, all font tiles fromSTARTto[, END](inclusive) will be generated.
Example:
.font 'A', 'G'
Output:
00000000 38 44 7c 44 44 44 44 00 38 44 7c 44 44 44 44 00 |8D|DDDD.8D|DDDD.|
00000010 78 44 78 44 44 44 78 00 78 44 78 44 44 44 78 00 |xDxDDDx.xDxDDDx.|
00000020 38 44 40 40 40 44 38 00 38 44 40 40 40 44 38 00 |8D@@@D8.8D@@@D8.|
00000030 78 44 44 44 44 44 78 00 78 44 44 44 44 44 78 00 |xDDDDDx.xDDDDDx.|
00000040 7c 40 70 40 40 40 7c 00 7c 40 70 40 40 40 7c 00 ||@p@@@|.|@p@@@|.|
00000050 7c 40 70 40 40 40 40 00 7c 40 70 40 40 40 40 00 ||@p@@@@.|@p@@@@.|
00000060 3c 40 4c 44 44 44 38 00 3c 40 4c 44 44 44 38 00 |<@LDDD8.<@LDDD8.|
00000070
Read more about PPU pattern tables here.
.hibytes
Output only the high byte of 16-bit word(s).
Usage:
.hibytes NUMBER[, NUMBER]
NUMBER- Number, required. At least one number is required., NUMBER, ...- Number(s), optional. Additional comma-separated numbers may be used.
Example:
.hibytes $1234, $5678
Output:
00000000 12 56 |.V|
00000002
.if
Test if condition.
Can be accompanied by an .else and must be accompanied by an
.endif.
Usage:
.if CONDITION
CONDITION- Condition, required. The code that follows will be processed if the condition is true. See Comparison Operators.
Example:
.if SOMETHING == $01
LDA #$01
.endif
.ifdef
Test if variable is defined.
Can be accompanied by an .else and must be accompanied by an
.endif.
Usage:
.ifdef VARIABLE
VARIABLE- Variable/constant/etc., required. The code that follows will be processed if the variable has been defined.
Example:
.ifdef SOMETHING
STA $00
.else
STA $01
.endif
.ifndef
Test if variable has not been defined.
Can be accompanied by an .else and must be accompanied by an
.endif.
Usage:
.ifndef VARIABLE
VARIABLE- Variable/constant/etc., required. The code that follows will be processed if the variable has not been defined.
Example:
.ifndef SOMETHING
STA $01
.else
STA $00
.endif
.incbin
Include binary file.
Usage:
.incbin "FILENAME"[, OFFSET[, LIMIT]]
"FILENAME"- Path to file, required. Must be within quotes.[, OFFSET- File offset index, optional. Index at which to start including binary file.[, LIMIT]]- Limit in bytes, optional. Number of total bytes to include.
Example:
.incbin "file.bin"
.include
Include assembly file.
Usage:
.incbin "FILENAME"
"FILENAME"- Path to file, required. Must be within quotes.
Example:
.include "file.asm"
Included files share a global state with other included files and the main entry point file. That means if a variable is defined in one file, it is available to all other files, provided that they are included after the definition.
Relative filenames in
.include— and in every filename-based directive (.incbin,.incpng,.incpal,.incrle,.incwav,.inestrn) — are resolved relative to the directory of the file that contains the directive. A file included from a subdirectory therefore resolves its own includes and assets from that subdirectory, not from the top-level project directory.
.incpal
Include palette from PNG.
Usage:
.incpal "FILENAME"
"FILENAME"- Path to file, required. Must be within quotes.
Example:
.incpal "palette.png"
The PNG will be scanned, row-by-row/pixel-by-pixel, from the top-left to the bottom-right until it encounters 4 different, but not necessarily unique, colors.
.incpng
Include PNG.
Converts the PNG to CHR tiles. The image must include only 4 colors:
| Color | Name | RGB | Hex |
|---|---|---|---|
| Black | 0, 0, 0 | #000000 | |
| Dark Grey | 85, 85, 85 | #555555 | |
| Light Grey | 170, 170, 170 | #AAAAAA | |
| White | 255, 255, 255 | #FFFFFF |
Other colors may be used, but accuracy is not guaranteed.
Usage:
.incpng "FILENAME"
"FILENAME"- Path to file, required. Must be within quotes.
Example:
.incpng "image.png"
Read more about PPU pattern tables here.
.incrle
Include binary data to be RLE-encoded
The RLE-encoding scheme used is one featured in a few Konami NES titles, known
as Konami RLE. The breakdown of bytes:
| Value | Description |
|---|---|
| 00-80 | Read another byte and write it to the output N times |
| 81-FE | Copy N-128 bytes from input to output |
| FF | End of compressed data |
Usage:
.incrle "FILENAME"
"FILENAME"- Path to file, required. Must be within quotes.
Read more about NES RLE compression here.
.incwav
Include WAV.
Converts WAV to a 1-bit PCM.
Usage:
.incwav "FILENAME"[, AMPLITUDE]
"FILENAME"- Path to file, required. Must be within quotes.[, AMPLITUDE]- Amplitude, optional. Amplitude of WAV.
Example:
.incwav "audio.wav", 24
.ineschr
iNES CHR count.
Usage:
.ineschr COUNT
COUNT- Number, required. Number of CHR banks.
Example:
.ineschr 1
.inesmap
iNES mapper number.
Usage:
.inesmap NUMBER
NUMBER- Number, required. Mapper number.
Read more about NES mappers here.
.inesmir
iNES mirroring.
xxxx3210
||||
|||+- Mirroring: 0: horizontal (vertical arrangement)
||| 1: vertical (horizontal arrangement)
||+-- 1: Cartridge contains battery-backed PRG-RAM
|+--- 1: 512-byte trainer at $7000-$71FF
+---- 1: Ignore mirroring control, provide 4-screen VRAM
| Value | Binary | Mirroring | PRG-RAM | Trainer | 4-Screen |
|---|---|---|---|---|---|
| 0 | 00000000 | Horizontal | |||
| 1 | 00000001 | Vertical | |||
| 2 | 00000010 | Horizontal | |||
| 3 | 00000011 | Vertical | |||
| 4 | 00000100 | Horizontal | |||
| 5 | 00000101 | Vertical | |||
| 6 | 00000110 | Horizontal | |||
| 7 | 00000111 | Vertical | |||
| 8 | 00001000 | Horizontal | |||
| 9 | 00001001 | Vertical | |||
| 10 | 00001010 | Horizontal | |||
| 11 | 00001011 | Vertical | |||
| 12 | 00001100 | Horizontal | |||
| 13 | 00001101 | Vertical | |||
| 14 | 00001110 | Horizontal | |||
| 15 | 00001111 | Vertical |
Usage:
.inesmir NUMBER
NUMBER- Number, required. Mirroring type.
.inesprg
iNES PRG count.
Usage:
.inesprg COUNT
COUNT- Number, required. Number of PRG banks.
Example:
.inesprg 1
.inestrn
iNES trainer include.
The assembled trainer must be no larger than 512 (0x200) bytes. The appropriate flag is automatically set in the iNES header to indicate a trainer is present.
Usage:
.inestrn "FILENAME"
"FILENAME"- Path to file, required. Must be within quotes.
Example:
.inestrn "trainer.asm"
.lobytes
Output only the low byte of 16-bit word(s).
Usage:
.lobytes NUMBER[, NUMBER]
NUMBER- Number, required. At least one number is required., NUMBER, ...- Number(s), optional. Additional comma-separated numbers may be used.
Example:
.lobytes $1234, $5678
Output:
00000000 34 78 |4x|
00000002
.macro
Call macro.
Usage:
.macro MACRO[, NUMBER, ...]
MACRO- Name, required. Name of previously-defined macro., NUMBER, ...- Number(s), optional. Additional comma-separated numbers may be used.
Example:
.macro TEST_MACRO
See the section on Macros for more information.
.macrodef
Start macro definition.
Usage:
.macrodef MACRO
CODE...
.endm
MACRO- Name, required. Name of macro.CODE...- Code, required. Assembly code.
Example:
.macrodef TEST_MACRO
LDA #\1
STA <\2
.endm
See the section on Macros for more information.
.org
Organize code.
Set the address of the current bank in which to start organizing code.
Usage:
.org ADDRESS
Example:
.org $C000
.prg
Set PRG bank index.
Usage:
.prg NUMBER
NUMBER- Number, required. PRG bank index.
Example:
.prg 0
PRG banks are 4K bytes (0x4000) in size.
.random
Output random byte(s).
The algorithm for the PRNG is the suggested POSIX implementation of rand().
Usage:
.random [SEED[, COUNT]]
[SEED- Number or string, optional. Seeds the random number generator. Defaults to the current system time.[, COUNT]]- Number of bytes to output, optional. Defaults to 1.
Example:
.random "Secret Key", 16
.rsset
Set initial value for .rs declarations.
Usage:
.rsset ADDRESS
ADDRESS- Number, required. Address to start.rsdeclarations.
Example:
.rsset $0000
.rs
Reserve space for variable declaration.
Usage:
VARIABLE .rs NUMBER
VARIABLE- Variable name, required. Name of variable to declare.NUMBER- Number (in bytes) to reserve, required.
Example:
.rsset $0000
label_01 .rs 1
label_02 .rs 2
label_03 .rs 1
.db label_01, label_02, label_03
Output:
00000000 00 01 03 |...|
00000003
.rsset $0000
label_01 .rs 1
label_02 .rs 2
label_03 .rs 1
.db label_01, label_02, label_03
.segment
Set code segment.
Usage:
.segment "SEGMENT[0-9]+"
SEGMENT- Type of segment, required.PRGorCHR.[0-9]+- Number, required. Segment index.
The whole segment must be within quotes.
Example:
.segment "PRG1"
This is an alias for
.prg x.
Optional Scripts
Some scripts are included with nessemble, but totally optional. They must be
installed with the scripts command which provides additional
pseudo-instructions to use.
| Pseudo-Instruction | Description |
|---|---|
| .ease | Generates bytes to simulate easing |
.ease
Generates bytes to simulate easing
Usage:
.ease FUNCTION[, START[, END[, STEPS]]]
FUNCTION- String, required. Easing function to perform. Must be within quotes.[, START- Number, optional. Starting value. Defaults to 0.[, END- Number, optional. Ending value. Defaults to 16.[, STEPS]]]- Number, optional. Steps to perform. Defaults to 16.
Valid FUNCTIONs include:
- "easeInQuad"
- "easeOutQuad"
- "easeInOutQuad"
- "easeInCubic"
- "easeOutCubic"
- "easeInOutCubic"
- "easeInQuint"
- "easeOutQuint"
- "easeInOutQuint"
- "easeInBounce"
- "easeOutBounce"
- "easeInOutBounce"
Example:
.ease "easeOutBounce", 0, $20, $40
Output:
00000000 00 00 00 00 00 01 02 02 03 04 06 07 08 0a 0b 0d |................|
00000010 0f 11 13 16 18 1a 1d 1f 1e 1d 1c 1b 1a 19 19 18 |................|
00000020 18 18 18 18 18 18 18 19 19 1a 1b 1c 1d 1e 1f 1f |................|
00000030 1e 1e 1e 1e 1e 1e 1e 1e 1f 1f 1f 1f 1f 1f 1f 20 |............... |
00000040
Macros
Macros may be utilized to maximize code-reuse and may also be treated as custom functions.
Example:
.macrodef TEST_MACRO
LDA #$00
STA $2005
STA $2005
.endm
.macro TEST_MACRO
Output:
00000000 a9 00 8d 05 20 8d 05 20 |.... .. |
00000008
.macrodef TEST_MACRO
LDA #$00
STA $2005
STA $2005
.endm
.macro TEST_MACRO
Parameters
Macros may also have parameters.
Example:
.macrodef TEST_MACRO
LDA #\1
STA \2
STA \2
.endm
.macro TEST_MACRO, $00, $2005
Output:
.macrodef TEST_MACRO
LDA #\1
STA \2
STA \2
.endm
.macro TEST_MACRO, $00, $2005
One macro may have up to 256 parameters which are denoted with a \ prefix. The
first parameter being \1, the next \2, and so on up to \256. All
parameters must be numbers (or label variables).
There is also a pseudo-parameter, \#, that returns the number of input
parameters.
Example:
.macrodef COUNT_PARAMS
.db \#
.endm
.macro COUNT_PARAMS, $01, $01, $01
Output:
00000000 03 |.|
00000001
There is another pseudo-parameter, \@, that returns a unique number every time
the macro is called.
Example:
.macrodef TEST_MACRO
LDX #$08
label_\@:
DEX
BNE label_\@:
.endm
.macro TEST_MACRO
.macro TEST_MACRO
.macro TEST_MACRO
Output:
00000000 a2 08 ca d0 fd a2 08 ca d0 fd a2 08 ca d0 fd |...............|
0000000f
Editor support
nessemble ships a built-in Language Server for its flavor of 6502
assembly. It runs from the CLI and speaks the Language Server Protocol over
stdio, so any LSP-capable editor — VS Code, Cursor, Neovim, Helix, Emacs
(eglot/lsp-mode), Sublime Text (LSP), and others — can drive it.
Starting the server
nessemble lsp
The server reads LSP messages on stdin and writes them to stdout, the
transport every LSP client expects. You normally don't run this by hand; you
point your editor's LSP client at it and the editor manages the process.
Features
Once connected, the server provides:
- Diagnostics — errors and warnings as you type, each underlined at the offending token. Several problems are reported at once (the analyzer recovers past the first error), and includes are followed.
- Project-aware analysis — when a workspace folder is open, a file that is
.included into a larger program is analyzed in the context of that program, so symbols defined in sibling or parent files are not reported as undefined. The server discovers entry points from the workspace's.includegraph (no configuration needed) and reflects unsaved edits across files. - Completion — instruction mnemonics, assembler directives, and the
labels, constants, and macros defined in the current buffer. Typing
.triggers directive completion. - Formatting — “format document” tidies indentation and comma spacing while preserving comments, blank lines, and letter case. Formatting is lossless and idempotent.
- Semantic highlighting — tokens are classified (mnemonic, directive, number, string, comment, identifier, operator) for richer coloring than a regex grammar can offer.
- Outline & navigation — a document outline of labels, constants, and
macros; go-to-definition (cmd/ctrl-click) and find-all-references for symbols.
With a workspace folder open, go-to-definition follows
.includes across the project, so it reaches a symbol defined in a sibling or parent file. - Hover — opcode and addressing-mode details for an instruction, the description of a directive, and the resolved value of a constant or label.
- Folding — macro (
.macrodef….endm) and conditional (.if*….endif) blocks, and runs of consecutive comments, can be collapsed. - Rename — renaming a symbol updates its definition and every use across the open buffers.
- Code actions — convert a numeric literal between hexadecimal, decimal, and binary.
- Custom pseudo-instructions — directives declared in a
--pseudo-style mapping file in the workspace are recognized, so they aren't flagged as unknown, and cmd/ctrl-click on one opens the script that implements it.
Editor setup
The server needs no configuration beyond the command nessemble lsp and a file
type. Associate the .asm extension (or a dedicated language id such as
nessemble) with the server in your editor's LSP settings.
Neovim (nvim-lspconfig)
vim.api.nvim_create_autocmd('FileType', {
pattern = 'asm',
callback = function(args)
vim.lsp.start({
name = 'nessemble',
cmd = { 'nessemble', 'lsp' },
root_dir = vim.fs.dirname(args.file),
})
end,
})
Helix (languages.toml)
[language-server.nessemble]
command = "nessemble"
args = ["lsp"]
[[language]]
name = "assembly"
language-servers = ["nessemble"]
VS Code / Cursor
Cursor is a VS Code fork and uses the same extension model. There is no published Marketplace extension yet, but Cursor can't spawn a stdio language server on its own — it needs a small client extension. A minimal one is a few files; you can develop it locally and run it from Cursor without publishing.
-
Make sure
nessembleis on yourPATH(nessemble --versionshould print2.5.0or newer). -
Create a folder, e.g.
nessemble-vscode/, with these two files:package.json:{ "name": "nessemble", "displayName": "nessemble", "version": "0.0.1", "engines": { "vscode": "^1.75.0" }, "categories": ["Programming Languages"], "activationEvents": ["onLanguage:nessemble"], "main": "./extension.js", "contributes": { "languages": [ { "id": "nessemble", "aliases": ["nessemble", "NES assembly"], "extensions": [".asm", ".s"] } ] }, "dependencies": { "vscode-languageclient": "^9.0.0" } }extension.js:const { LanguageClient } = require("vscode-languageclient/node"); let client; function activate() { const serverOptions = { command: "nessemble", args: ["lsp"], }; const clientOptions = { documentSelector: [{ scheme: "file", language: "nessemble" }], }; client = new LanguageClient( "nessemble", "nessemble", serverOptions, clientOptions ); client.start(); } function deactivate() { return client ? client.stop() : undefined; } module.exports = { activate, deactivate }; -
From that folder, run
npm installto fetchvscode-languageclient. -
Open the folder in Cursor and press F5 ("Run Extension") to launch an Extension Development Host with the extension loaded. Open a
.asmfile in that window — diagnostics, completion, hover, formatting, outline, and go-to-definition should all work.To install it permanently instead of running the dev host, package it with
vsce(vsce package) and install the resulting.vsixvia the Extensions view's Install from VSIX… command.
Any other client that can spawn a stdio language server for .asm/.s files
works the same way.
Emacs (eglot)
(add-to-list 'eglot-server-programs
'(asm-mode . ("nessemble" "lsp")))
Notes
- The server was compiled in by default. A build made with
--no-default-features(without thelspfeature) still acceptsnessemble lsp, but the command reports that language-server support was not included. - The server analyzes the in-editor buffer, so diagnostics reflect unsaved changes.
- Project-aware analysis needs a workspace folder to be open (most editors send one automatically). Opening a lone file with no folder still works, but each file is then analyzed on its own, so cross-file symbols may be reported as undefined.
- Custom pseudo-instructions are discovered from any
*.txtmapping file in the workspace (or next to the open file) whose.name = scriptentries point at existing scripts — the same mapping you pass to the CLI's--pseudo. Their scripts are not executed during analysis, so the bytes they emit aren't modeled; addresses after a custom pseudo-op may be approximate.
Extending
nessemble can be extended with custom pseudo-instructions written in
Rhai, a small, pure-Rust scripting language. Scripts can also
read and write files (see Filesystem access), so run only
scripts you trust.
Usage
Pass the --pseudo flag to point at a mapping file that associates each custom
directive with a script.
Example pseudo.txt:
.foo = foo.rhai
Example example.asm:
.foo 1, 2, 3
To assemble:
nessemble example.asm --pseudo pseudo.txt
A script path in the mapping file is resolved relative to the mapping file's
own directory, so a pseudo.txt and the scripts it names can live together and
be pointed at from anywhere. Bundled scripts installed with nessemble scripts
(into ~/.nessemble/scripts) are resolved via ~/.nessemble/scripts/scripts.txt
and need no --pseudo flag.
Writing a script
A script defines a function named custom that receives the directive's
arguments and returns the bytes to emit:
fn custom(ints, texts) {
// ...
}
intsis an array of the integer arguments.textsis an array of the string arguments (quotes already removed).- Return the emitted bytes as an array of integers (each taken
& 0xFF), a blob, or a string (its bytes are emitted). Returning()emits nothing.
Example
A .product directive that multiplies its integer arguments:
fn custom(ints, texts) {
let product = 0;
let first = true;
for i in ints {
if first { product = i; first = false; } else { product *= i; }
}
[product % 256]
}
.product 1, 2, 3 ; emits a single byte: 6
String arguments
String arguments arrive (with quotes removed) in texts:
.foo "easeInQuad", 0, 16
fn custom(ints, texts) {
let name = texts[0]; // "easeInQuad"
// ...
}
Errors
Signal an error with throw. The thrown message becomes the assembler
diagnostic:
fn custom(ints, texts) {
if texts.is_empty() {
throw "No arguments provided";
}
[]
}
Filesystem access
Scripts can read and write files through the
rhai-fs package, so a directive can pull bytes from
disk instead of only computing them. The main entry point is open_file:
open_file(path, "r")opens a file for reading;open_file(path)opens it for reading and writing, creating or truncating it.- On the returned file handle:
read_blob()/read_string()return the whole file,read_blob(n)/read_string(n)readnbytes,write(blob_or_string)writes bytes and returns the count, andseek(pos)moves the cursor.
Relative paths resolve against the source file's directory — the same base
as .include and the .inc* importers — while absolute paths are used as-is.
A .embed "file" directive that emits a file's bytes verbatim:
fn custom(ints, texts) {
open_file(texts[0], "r").read_blob()
}
.embed "logo.chr" ; emits the raw bytes of logo.chr
Filesystem access is not sandboxed. A script can read or write any path the
nessembleprocess can. Only run pseudo-op scripts you trust, as with any build tooling.
Decoding PNGs
decode_png(blob) decodes PNG bytes (typically from open_file(...).read_blob())
into a map of the image's dimensions and its pixels:
let img = decode_png(open_file("sprite.png", "r").read_blob());
The returned map has:
width— the image width in pixels (integer).height— the image height in pixels (integer).pixels— a flat array ofwidth * height * 4integers, four per pixel inR, G, B, Aorder, row-major. Pixel(x, y)starts at index(y * width + x) * 4.
decode_png throws if the blob is not a valid PNG. For example, a directive that
emits a single tile's worth of a PNG's red channel:
fn custom(ints, texts) {
let img = decode_png(open_file(texts[0], "r").read_blob());
let out = [];
for y in 0..8 {
for x in 0..8 {
out.push(img.pixels[(y * img.width + x) * 4]); // red channel
}
}
out
}
Bundled scripts
Running nessemble scripts installs the bundled scripts. The ease script
emits an easing curve as bytes:
.ease "easeInQuad"
Supported easing types include easeInQuad, easeOutQuad, easeInOutQuad,
and the cubic, quint, and bounce variants.
Building
nessemble is a Cargo workspace of pure-Rust crates. Building requires only a
stock Rust toolchain (1.83+).
Build
cargo build --release
The CLI binary is written to target/release/nessemble.
Test
cargo test
The parity harness compares nessemble output against the committed golden
ROMs:
cargo run -p xtask -- parity
Cross-compilation
The dependencies are pure Rust, so the five release targets cross-compile cleanly. Add a target and build:
rustup target add i686-unknown-linux-gnu
cargo build --release --target i686-unknown-linux-gnu
| Platform | Target triple |
|---|---|
| macOS | x86_64-apple-darwin |
| Linux amd64 | x86_64-unknown-linux-gnu |
| Linux i386 | i686-unknown-linux-gnu |
| Windows 32-bit | i686-pc-windows-msvc |
| Windows 64-bit | x86_64-pc-windows-msvc |
Packaging
Release artifacts are produced by the CI release workflow
(.github/workflows/release.yml):
.deb(Linux) viacargo-deb..msi(Windows) viacargo-wix..pkg(macOS) viapkgbuild..tar.gz(macOS) — the raw binary, as a signing-free alternative to the unsigned.pkg(which Gatekeeper blocks after download).
Scripting
Custom pseudo-instruction scripting (Rhai) is enabled by default. To build the CLI without it:
cargo build --release -p nessemble-cli --no-default-features
Translating
nessemble routes every user-facing string through a
Project Fluent catalog, so it can be fully
translated. en-US ships built in and is always the fallback: any message a
locale does not translate falls back to its English value.
Add a locale at runtime
Drop a Fluent file at ~/.nessemble/locales/<lang>.ftl and select it with the
NESSEMBLE_LANG environment variable (or the standard LANG / LC_ALL):
# ~/.nessemble/locales/de.ftl
no-errors = Alles gut
NESSEMBLE_LANG=de nessemble -c game.asm
# -> Alles gut
<lang> should be a valid locale identifier such as de, de-DE, or fr.
Notes for translators
- Copy the built-in
en-US.ftlcatalog and translate the values; message ids are stable and must not be renamed. - Interpolate variables as
{ $name }— the variable names are part of each message's contract. - To keep a trailing space (Fluent trims trailing whitespace), write it as an
explicit literal, e.g.
init-prompt-filename = Filename:{ " " }. - Numbers are interpolated verbatim (no locale grouping).
Only the messages you translate are overridden; everything else falls back to
en-US.
Contributing
Feel free to fork the project and submit pull requests on GitHub:
https://github.com/kevinselwyn/nessemble-rs
Before submitting, please make sure the workspace is clean:
cargo fmt --all
cargo clippy --all-targets
cargo test
cargo run -p xtask -- parity
Licensing
nessemble-rs is licensed under the GNU General Public License, version 3 or
later.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.