Table of contents:


C64jasm is a symbolic assembler for the Commodore 64 that supports:

C64jasm also runs in the browser, you can try this interactive assembler demo to play with it.

C64jasm is free and open source -- its source code can be found here: c64jasm on GitHub.


In order to use the c64jasm assembler, you need to install the following:

Furthermore, if you wish to use c64jasm with VSCode, you should also install:

Assembler installation: npm install -g c64jasm

Upon successful installation, running c64jasm --help in your shell should work.

VSCode extension: Search for c64jasm in the VSCode marketplace and install.

VICE: See the VICE website for download and installation instructions. Once you have it installed, make sure the VICE emulator binary x64 is in your PATH.

Extras: Vim users: C64jasm vim plugin for syntax highlighting and better editing support: c64jasm plugin for vim

Getting started

Assuming you successfully installed the C64jasm command line compiler, you should be able to compile and run some code. Let's build the sprites sample from under examples/sprites/:

git clone https://github.com/nurpax/c64jasm
cd c64jasm/examples/sprites
c64jasm --out sprites.prg sprites.asm
x64 sprites.prg

You should see something like this in your VICE window:

If you installed the necessary VSCode parts of VSCode, you should be able to load this example project in VSCode and build it with Ctrl+Shift+P + Tasks: Run Build Task. Build errors will be reported under the Problems tab and you should be able to hit F5 to start your program in VICE.

Command line usage

Run c64jasm --help for all c64jasm command line options.

Basic usage:

c64jasm --out output.prg source.asm

where output.prg is the desired output .prg filename and source.asm is the assembly source you want to compile.

Automatic recompilation (watch mode)

Like many modern compiler tools, c64jasm supports "watch mode". Watch mode automatically recompiles your source code when any of the input source files change. To use watch mode, invoke c64jasm with the --watch <DIR> argument as follows:

c64jasm --out output.prg --watch src src/source.asm

C64jasm will watch the directory specified with --watch <DIR> (and its subdirectories) for any changes and recompile when anything changed. Changes to all types of input files (.asm files, plugin .js files, files loaded by .js extensions, !include/binary'd files, etc.) are considered as rebuild triggers.

A good project structure that makes it easy to work with watch mode is to place all your source files and assets under a single root directory, say src. This makes it easy to specify the watched directory with a single --watch src argument.

Watch mode works well with VSCode. The .vscode configs for examples/ are setup to use watched compiles.

Macro assembler

C64jasm has fairly extensive symbolic macro assembly support. This includes macros, compile-time variables, for-loops, if/else, and source and binary file inclusion.

Assembler pseudo directives start with a bang !. Examples: !let, !if, !include.

Labels and nested label scopes

; Clear the screen RAM (all 1024 bytes)
clear_screen: {
    lda #$20
    ldx #0
    sta $0400, x
    sta $0400 + $100, x
    sta $0400 + $200, x
    sta $0400 + $300, x
    bne loop

A label followed by braces {} starts a new scope. Labels declared inside the braces will be local to that scope. Labels declared within such a scope can still be referenced by using the namespacing operator ::, e.g.,

memset256: {
    ldx #0
    sta $1234, x
    bne loop

; Use self-modifying code to set target ptr
; for a memset

    lda #<buf           ; take lo byte of 'buf' address
    sta memset256::ptr-2
    lda #>buf           ; take hi byte of 'buf' address
    sta memset256::ptr-1
    jsr memset256

buf: !fill 256, 0

You can guard a whole file inside a scope if you start the source file with !filescope:

; Contents of util.asm
!filescope util

!macro inc_border() {
    inc $d020

Using util.asm from another file:

; Contents of main.asm
!include "util.asm"


Symbol references are relative to the current scope. If you need to reference a symbol in the root scope, use ::foo::bar:

bar: {
    !let foo = 20

foo: {
    bar: {
        !let foo = 0
    lda #bar::foo    ; evaluates to 0
    lda #::bar::foo  ; evaluates to 20

The implicit * label always points to the current line's address. You can use it to for example jump over the next instruction:

    bcc *+3
    ; bcc jumps here if carry clear

Data directives

Emitting bytes/words:

foo:  !byte 0     ; declare 8-bit
bar:  !word 0     ; declare 16-bit int (2 bytes)
baz:  !byte 0,1,2 ; declare bytes 0,1,2

baz_256: ; 256 bytes of zero
    !fill 256, 0

Including other source files:

!include "macros.asm"

Including binary data:

!binary "file1.bin"       ; all of file1.bin
!binary "file2.bin",256   ; first 256 bytes of file
!binary "file2.bin",256,8 ; 256 bytes from offset 8


You can declare a variable with !let. You can use standard C operators like +, -, *, /, <<, >>, &, |, ~ with them.

!let num_sprites = 4

    lda #(1 << num_sprites)-1
    sta $d015

Variable assignment:

!let a = 0   ; declare 'a'
a = 1        ; assign 1 to 'a'
!! a = 1     ; assign 1 to 'a' (same as above, see Statements)

Variables take on JavaScript values such as numbers, strings, arrays and objects. We will explore later in this document why array and object values are useful.

Array literals:

!let foo = [0,2,4]
    lda #foo[1]      ; emits LDA #02

Object literals:

; Declare zero-page offset helper

!let zp = {
    tmp0: $20,
    sprite_idx: $22

    lda #3
    sta zp.sprite_idx


Conditional assembly is supported by !if/elif/else.

!let debug = 1

!if (debug) { ; set border color to measure frame time
    inc $d020
    ; Play music or do some other expensive thing
    jsr play_music
!if (debug) {
    dec $d020


Use !for to repeat a particular set of instructions or data statements.

For-loops are typically written using the built-in range() function that returns an array of integers. This works similar to Python's range built-in.

!let xyptr = $40   ; alias zero-page $40 to xyptr

; shift left xyptr by 5 (e.g., xyptr<<5)
!for i in range(5) {
    asl xyptr
    rol xyptr+1

Lookup table generation:

    lda #3            ; index == 3
    lda shift_lut, x  ; A = 1<<3

; Create a left shift lookup table
    !for i in range(8) {
        !byte 1<<i

If you want to loop over some small set of fixed values (say 1, 10, 100), you can use array literals with !for:

!for i in [1, 10, 100] {


Statements such as variable assignment or calling a plugin function for just its side-effect is expressed by starting the line with !!:

; 'my_log' would be a JavaScript extension in your project
!use "my_log_plugin" as log

!let a = 0
!! a = 1   ; assign 1 to 'a'

!! log.print("hello")   ; console.log()


Macros are declared using the !macro keyword and expanded by +macroname().

; move an immediate value to a memory location
!macro mov8imm(dst, imm) {
    lda #imm
    sta dst

+mov8imm($40, 13)  ; writes 13 to zero page $40

Any labels or variables defined inside a macro definition will be local to that macro. For example, the below code is fine -- the loop label will not conflict:

; clear 16 bytes starting at 'addr'
!macro memset16(addr) {
    ldx #15
    sta addr, x
    bpl loop

+memset16(buffer0) ; this is ok, loop will not conflict

However, sometimes you do want the macro expanded labels to be visible to the outside scope. You can make them visible by giving the (normally anonymous) macro expansion scope a name by declaring a label on the same line as your macro expand:

; A = lo byte of memory address
; B = hi byte of memory address
clear_memory: {
    sta memset::loop+1
    stx memset::loop+2
memset: +memset16($1234)  ; macro labels are under memset

Built-in functions

C64jasm injects some commonly used functionality into the global scope.

All JavaScript Math constants and functions (except Math.random) are available in the Math object:

Constants: Math.E, Math.PI, Math.SQRT2, Math.SQRT1_2, Math.LN2, Math.LN10, Math.LOG2E, Math.LOG10E.

Functions: Math.abs(x), Math.acos(x), Math.asin(x), Math.atan(x), Math.atan2(y, x), Math.ceil(x), Math.cos(x), Math.exp(x), Math.floor(x), Math.log(x), Math.max(x, y, z, ..., n), Math.min(x, y, z, ..., n), Math.pow(x, y), Math.round(x), Math.sin(x), Math.sqrt(x), Math.tan(x).

Math.random() is not allowed as using a non-deterministic random would lead to non-reproducible builds. If you need a pseudo random number generator (PRNG), write a deterministic PRNG in JavaScript and use that instead.

C64jasm JavaScript

Extending the assembler with JavaScript was the primary reason why C64jasm was built. This is a powerful mechanism for implementing lookup table generators, graphics format converters, etc.

Learning resources on c64jasm extensions:

Making extensions

A c64jasm extension is simply a JavaScript file that exports a function ("default" export) or a JavaScript object containing functions (named exports). The functions can be called from assembly and their return values can be operated on using standard c64jasm pseudo ops.

Minimal example:


module.exports = {
    square: ({}, v) => {
        return v*v;


!use "math" as math
!byte math.sqr(3)  ; produces 9

Here's another example. Here we'll compute a sine table (see examples/sprites). This extension uses the JavaScript module "default export", ie. it exports just a single function, not an object of function properties.


module.exports = ({}, len, scale) => {
    const res = Array(len).fill(0).map((v,i) => Math.sin(i/len * Math.PI * 2.0) * scale);
    return res; // return an array of length `len`


!use "sintab" as sintab
!let SIN_LEN = 128
!let sinvals = sintab(SIN_LEN, 30)
!for v in sinvals {
    !byte v

JavaScript / assembly API

An extension function is declared as follows:

(context, ...args) => { return ... };

For example, if you're defining an extension function that takes one input argument, it must be declared as:

(context, arg0) => { return ... };

C64jasm calls an extension function with a context value that contains some extra functions for the extension to use. The rest of the arguments (...args) come from the assembly source invocation. For example:

!let v = math.sqr(3)

will be called as:

// const sqr = (context, arg0) => return arg0*arg0;
sqr(context, 3);

If your extensions doesn't need anything from the context parameter, you can declare your extension function like so: ({}, arg0) => return arg0*arg0;

What is the context parameter?

The context parameter contains functionality that an extension can use to load input files. It may also be extended to contain functions for error reporting.

Currently (c64jasm 0.3), the context object contains the following properties:

A well-behaving extension would use these to load input files as follows:

const loadJson = ({readFileSync, resolveRelative}, fname) => {
    const json = JSON.parse(readFileSync(resolveRelative(filename)));
    return json;
module.exports = loadJson;

A relative filename is relative to the location of the assembly source file that called the extension. E.g., assuming the following folder structure:


Consider calling an extension with a filename assets/petscii.json from main.asm:

!use "json" as json
!let j = json("assets/petscii.json")

Suppose you invoke c64jasm outside of the src directory like: c64jasm ./src/main.asm. As main.asm is being compiled, c64jasm knows it resides in ./src/main.asm and with resolveRelative, an extension knows how to resolve assets/petscii.json to ./src/assets/petscii.json.

Why do I need context.readFileSync?

You might be asking: why do I need context.readFileSync when I could just as well import Node's readFileSync and use that.

Using the c64jasm provided I/O functions is necessary as it allows for c64jasm to know about your input files. For example, if you're running c64jasm in watch mode, it can cache all your input files if they didn't change since the previous compile.

Rules of authoring extensions

A limited form of side-effects is permitted though. It is OK for an extension function to return a closure that holds its internal state. For example this code is fine:

module.exports = {
  create: ({}, initial) => {
    const stack = [initial];
    return {
      push: (elt) => {
      pop: () => stack.pop(),
      top: () => {
        return stack[stack.length-1];

Usage in assembler:

!use "stack" as stack
!let s = stack.create({ tmp0: $20 })
!let zp = s.top()
    lda #zp.tmp0

In this example, the stack array holds the state which can be manipulated by calls to push(elt), pop().

Release notes

c64jasm 0.8.1 (released on 2020-02-05):

c64jasm 0.8.0 (released on 2019-11-11):

c64jasm 0.7.0 (released on 2019-07-05):

c64jasm 0.6.0 (released on 2019-07-26):

c64jasm 0.5.1 (released on 2019-07-18):

c64jasm 0.5.0 (released on 2019-07-14):

c64jasm 0.4.0 (released on 2019-06-29):

c64jasm 0.3.0:

c64jasm 0.2.0: