C64jasm

Table of contents:

Overview

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.

Installation

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.

VICE and C64Debugger debug symbols

You can output debug symbols for VICE and C64Debugger with the following options:

Debug symbol files can be used for setting breakpoints and enables source level debugging in C64Debugger.

See my blog Debugging C64jasm projects with VICE and C64Debugger for a short tutorial.

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 scopes

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

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
loop:
    sta $1234, x
ptr:
    inx
    bne loop
    rts
}

; 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"

    +util::inc_border()

Label, variable and macro names all follow the same scoping rules. 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
}

It is not legal to declare symbols with the same name in the same scope. This will fail:

!let var1 = 0
!let var1 = 1 ; error: Variable 'var1' already defined

; but so will for example declaring a variable and a macro with the same name:
!let xyz = 0
!macro xyz() { ; error: Symbol 'xyz' already defined
    nop
}
+xyz()

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:

; As of c64jasm you will use keyword args with !binary.
!binary (file="file1.bin")                     ; all of file1.bin
!binary (file="file2.bin", size=256)           ; first 256 bytes of file
!binary (file="file2.bin", size=256, offset=8) ; 256 bytes from offset 8

; Earlier syntax is still supported and will be kept around.
!binary "file2.bin", 256, 8                    ; 256 bytes from offset 8

Text strings: C64jasm does not have built-in PETSCII or screencode emitting directives like !text or !scr. You can, however, implement this yourself using an assembler plugin. See this gist for an example. Armed with such a plugin, you can write code like this:

!use "./text" as text  ; see the above gist link for full code!
!byte text("testing 123")

Variables and expressions

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

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
    nop
    ; bcc jumps here if carry clear

You can access the lo/hi parts of a 16-bit value or a memory address with < and >. To keep the parser simple, this syntax only works in immediate fields like lda #<foo. To access lo/hi parts of any value within any expression context, use bitwise operations & and >> like in the below example:

    lda #<buf           ; take lo byte of 'buf' address
    ldx #>buf           ; take hi byte of 'buf' address
buf: !byte 0, 1, 2, 3

!let lbl_lo = buf & 255 ; lo byte of 'buf' address
!let lbl_hi = buf >> 8  ; hi byte of 'buf' address

If...else

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
}

For-loops

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
    tax
    lda shift_lut, x  ; A = 1<<3

; Create a left shift lookup table
shift_lut:
    !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

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

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
loop:
    sta addr, x
    dex
    bpl loop
}

+memset16(buffer0)
+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
    rts
}

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.

Segments

Segments offer extra flexibility over code and data mamory layout. They allow decoupling memory order from source code order.

By default, everything is compiled into a segment called default.

Declare a new segment with !segment <name>(start=<addr>, end=<addr>) (start and end both inclusive).

Activate a segment with !segment <name>. Activating a segment means any code or data emitted by the compiler will now go into the active segment.

An example to illustrate. The disassembly in comments shows where code and data gets placed in the output.

!segment code(start=$2000, end=$2100)
!segment gfx(start=$3000, end=$3100)

* = $801
    lda #0          ; 0801: A9 00      LDA #$00
    jsr start_part  ; 0803: 20 00 20   JSR $2000

; switch to code segment, any emitted code or data will be stored at
; address $2000 or above
!segment code
start_part:         ; 2000: 60         RTS
    rts

; switch to data.  any emitted code or data will be stored at
; address $3000 or above
!segment gfx
!byte 0,1,2,3       ; 3000: 00 01 02 03

!segment default
; switch back to default segment, resume at address $806
    lda #1          ; 0806: A9 01      LDA #$01
    jsr start_part  ; 0808: 20 00 20   JSR $2000

As of c64jasm v0.9.0, all segments are output into the prg output defined by the --out <out.prg> argument. Future versions of c64jasm will support specifying a PRG or a binary output file for each segment.

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:

math.js:

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

test.asm:

!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.

sintab.js:

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`
}

foo.asm:

!use "sintab" as sintab
!let SIN_LEN = 128
!let sinvals = sintab(SIN_LEN, 30)
sintab:
!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:

src/
  main.asm
  assets/
    petscii.json

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.

Error handling in extensions

Use throw new Error in your JavaScript extension to report errors back to the assembler. The assembler will turn these into assembler errors.

plugin.js:

module.exports = ({}, arg0) => {
    if (arg0 < 5) {
        return arg0;
    }
    throw new Error('arg0 must be less than 5');
}

test.asm:

!use "./plugin" as p
!let b = p(10) ; reports an error like below

; test/errors/js_errors3.js:5:11: error: Error: arg0 must be less than 5

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) => {
        stack.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.9.2 (2021-04-10):

c64jasm 0.9.1 (2021-02-22):

c64jasm 0.9.0 (2021-02-12):

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: