Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

PlatformArtifact(s)
macOSnessemble_<v>.pkg, nessemble_<v>_macos.tar.gz
Linux amd64nessemble_<v>_amd64.deb
Linux i386nessemble_<v>_i386.deb
Windows 32-bitnessemble_<v>_win32.exe, …_win32.msi
Windows 64-bitnessemble_<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 in sub/ that does .include "helper.asm" or .incbin "data.bin" now finds them in sub/.
  • What to check: if a file you .include from 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 .rhai files and update your --pseudo mapping to point at them. A script now defines fn custom(ints, texts) and returns the emitted bytes. See Extending.
  • The bundled ease script is provided as .rhai; run nessemble scripts to install it.
  • Rhai scripts can still read and write files (as the old Lua/Scheme hosts could), via the rhai-fs open_file API; relative paths resolve against the source file's directory. See Filesystem access.
  • Script paths in a --pseudo mapping 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's cwd_path). If your pseudo.txt sits 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>.ftl file and select it with NESSEMBLE_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.

BaseExample AExample B
Binary%0100000101000001b
Decimal6565d
Octal0101101o
Hexadecimal$4141h
ASCII char'A'

Symbols

Mathematical Operators

SymbolDescription
+Add
-Subtract
*Multiply
/Divide
**Exponent
&Bitwise AND
|Bitwise OR
^Bitwise XOR
>>Shift right
<<Shift left
%Modulo

Comparison Operators

SymbolDescription
==Equals
!=Not equals
<Less than
>Greater than
<=Less than or equals
>=Greater than or equals

Special

SymbolDescription
->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:

MnemonicDescription
ADCAdd with Carry
ANDBitwise AND with Accumulator
ASLArithmetic shift left
BITTest bits
BCCBranch on Carry clear
BCSBranch on Carry set
BEQBranch on equal
BMIBranch on minus
BNEBranch on not equal
BPLBranch on plus
BRKBreak
BVCBranch on Overflow clear
BVSBranch on Overflow set
CLCClear Carry
CLDClear Decimal
CLIClear Interrupt
CLVClear Overflow
CMPCompare Accumulator
CPXCompare X register
CPYCompare Y register
DECDecrement memory
DEXDecrement X register
DEYDecrement Y register
EORBitwise exclusive OR
INCIncrement memory
INXIncrement X register
INYIncrement Y register
JMPJump
JSRJump to subroutine
LDALoad Accumulator
LDXLoad X register
LDYLoad Y register
LSRLogical shift right
NOPNo operation
ORABitwise OR with Accumulator
PHAPush Accumulator
PHPPush processor status
PLAPull Accumulator
PLPPull processor status
ROLRotate left
RORRotate right
RTIReturn from Interrupt
RTSReturn from subroutine
SBCSubtract with Carry
SECSet Carry
SEDSet Decimal
SEISet Interrupt
STAStore Accumulator
STXStore X register
STYStore Y register
TAXTransfer Accumulator to X register
TAYTransfer Accumulator to Y register
TSXTransfer Stack Pointer to X register
TXATransfer X register to Accumulator
TXSTransfer X register to Stack Pointer
TYATransfer 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.

MnemonicDescription
AACAND with Accumulator
AAXAND X register with Accumulator
ARRAND with Accumulator
ASRAND with Accumulator
ATXAND with Accumulator
AXAAND X register with Accumulator
AXSAND X register with Accumulator
DCPSubtract 1 from memory
DOPNo operation (x2)
ICSIncrease memory by 1
KILStop program counter
LARAND memory with stack pointer
LAXLoad Accumulator and X register
NOPNo operation
RLARotate one bit left in memory
RRARotate one bit right in memory
SBCSubtract with Carry
SLOShift left one bit in memory
SREShift right one bit in memory
SXAAND Y register with the high byte of address
SYAAND Y register with the high byte of address
TOPNo operation (x3)
XAAUnknown
XASAND X register with Accumulator

Read more about undocumented 6502 opcodes here.

Addressing Modes

ModeExample
ImpliedRTS
AccumulatorROL A
ImmediateLDA #$42
ZeropageSTA <$42
Zeropage, XEOR <$42, X
Zeropage, YLDX <$42, Y
AbsoluteSTA $4200
Absolute, XEOR $4200, X
Absolute, YLDX $4200, Y
IndirectJMP [$4200]
Indirect, XLDA [$42, X]
Indirect, YSTA [$42], Y
RelativeBEQ label

nessemble uses 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

FunctionDescription
HIGH()Get high byte of address
LOW()Get low byte of address
BANK()Get bank of address

Pseudo-Instructions

Pseudo-InstructionDescription
.asciiConvert ASCII string to bytes
.byteAlias for .db
.checksumCalculate crc32 checksum
.chrSet CHR bank index
.colorConvert hex color to NES color
.dbDefine 8-bit byte(s)
.defchrDefine CHR tile
.dwDefine 16-bit word(s)
.elseElse condition of an .if/.ifdef/.ifndef statement
.endenumEnd .enum
.endifEnd .if/.ifdef/.ifndef statement
.endmEnd .macrodef
.enumStart enumerated variable declarations
.fillFill with bytes
.fontGenerate font character tile
.hibytesOutput only the high byte of 16-bit word(s)
.ifTest if condition
.ifdefTest if variable is defined
.ifndefTest if variable has not been defined
.incbinInclude binary file
.includeInclude assembly file
.incpalInclude palette from PNG
.incpngInclude PNG
.incrleInclude binary data to be RLE-encoded
.incwavInclude WAV
.ineschriNES CHR count
.inesmapiNES mapper number
.inesmiriNES mirroring
.inesprgiNES PRG count
.inestrniNES trainer include
.lobytesOutput only the low byte of 16-bit word(s)
.macroCall macro
.macrodefStart macro definition
.orgOrganize code
.outOutput debugging message
.prgSet PRG bank index
.randomOutput random byte(s)
.rssetSet initial value for .rs declarations
.rsReserve space for variable declaration
.segmentSet code segment
.wordAlias 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 from START to [, 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:

ColorNameRGBHex
Black0, 0, 0#000000
Dark Grey85, 85, 85#555555
Light Grey170, 170, 170#AAAAAA
White255, 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:

ValueDescription
00-80Read another byte and write it to the output N times
81-FECopy N-128 bytes from input to output
FFEnd 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

ValueBinaryMirroringPRG-RAMTrainer4-Screen
000000000Horizontal
100000001Vertical
200000010Horizontal
300000011Vertical
400000100Horizontal
500000101Vertical
600000110Horizontal
700000111Vertical
800001000Horizontal
900001001Vertical
1000001010Horizontal
1100001011Vertical
1200001100Horizontal
1300001101Vertical
1400001110Horizontal
1500001111Vertical

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 .rs declarations.

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. PRG or CHR.
  • [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-InstructionDescription
.easeGenerates 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 .include graph (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.

  1. Make sure nessemble is on your PATH (nessemble --version should print 2.5.0 or newer).

  2. 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 };
    
  3. From that folder, run npm install to fetch vscode-languageclient.

  4. Open the folder in Cursor and press F5 ("Run Extension") to launch an Extension Development Host with the extension loaded. Open a .asm file 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 .vsix via 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 the lsp feature) still accepts nessemble 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 *.txt mapping file in the workspace (or next to the open file) whose .name = script entries 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) {
    // ...
}
  • ints is an array of the integer arguments.
  • texts is 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) read n bytes, write(blob_or_string) writes bytes and returns the count, and seek(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 nessemble process 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 of width * height * 4 integers, four per pixel in R, G, B, A order, 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
PlatformTarget triple
macOSx86_64-apple-darwin
Linux amd64x86_64-unknown-linux-gnu
Linux i386i686-unknown-linux-gnu
Windows 32-biti686-pc-windows-msvc
Windows 64-bitx86_64-pc-windows-msvc

Packaging

Release artifacts are produced by the CI release workflow (.github/workflows/release.yml):

  • .deb (Linux) via cargo-deb.
  • .msi (Windows) via cargo-wix.
  • .pkg (macOS) via pkgbuild.
  • .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.ftl catalog 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/>.