WARNING, THIS CODE IS EXPERIMENTAL AND UNSTABLE. USE WITH CAUTION
A lispy language for creating NES / Famicom software. Based upon original work by Dave Griffiths.
CO2 takes Lispy source files and compiles them into 6502 ROMs meant to run in an NES emulator. It includes high-level control structures like subroutines, loops, and conditionals, as well as low-level access to memory and individual 6502 CPU instructions.
Though it was used for (and developed along with) the game What Remains, CO2 should be considered highly experimental. It is lacking many features, has poor errors, may rarely generate invalid code, lacks many standard language abstractions, and in general is somewhat unstable. Nevertheless, it still has some real benefits if you are willing to deal with its rough spots.
Example:
(defun (read-joypad)
; Latch the controller
(set! REG-JOYPAD-0 1)
(set! REG-JOYPAD-0 0)
;; Read controller 8 times, put each bit into `joypad-data`.
(loop-down-from x 8
(lsr (lda REG-JOYPAD-0))
(rol joypad-data)))
Requires racket
and asm6
$ racket co2.scm -o rom.nes example/example.co2
CO2 is meant to enable high-level structured programming while still providing access to low level facilities. It maintains some useful knowledge of ROM banks in order to make it easier to develop large scale games. It emphasizes writing fairly performant code, at the expense of some safety. In some cases, it has slightly leaky abstractions (such as putting state into the X and Y registers) that the compiler does not insulate you from. Basically: "you have to know what you're doing".
The goal is to allow development of software that doens't absolutely require the performance of raw assembly, in a manner that's convenient for experimentation and understanding.
Variables declared as subroutine parameters or by using let
are statically allocated using a "compiled stack", calculated by analyzing the program's entire call graph. This means scopes will not use memory locations used by any inner scopes, but are free to use them from sibling scopes. This ensures efficient variables lookups, while also not wasting RAM. However, it does mean that recursion is not supported.
Functions "know" what bank they exist in. This enables conveniently calling functions in other banks, though of course this much less efficient than a normal function call.
Similarly, it is an error to call a function in a non-accessible bank, detected statically by the compiler. This saves development time by catching errors early.
Resources provide a simple mechanism to include binary data such that it's cheap and efficient to load a pointer and bank number to that data using a zero-cost (compile-time only) handle. No need to manually keep track of where data is stored in ROM.
When using fceux, CO2 produces *.nl files that easily enable source level debugging of the original high-level co2 files within the debugger window.
The cond
form can compile properly structured conditionals into lookup tables and jump tables, while still looking like high-level conditionals.
A very simple peephole optimizer and tail call optimizer help squeek out ROM bytes and CPU cycles without any extra work.
Macros are available, but lack many features, as they were implemented late in development. They take advantage of racket's namespaces
and eval
, yielding a very simple implementation.
Some 16-bit math exists, but support is lacking. Ideally, types would automatically promote and demote, which proper errors from the compiler when the user is doing something wrong.
Though cond
will sometimes compile to efficient tables, it could use more work. Ideally, it would always use binary search over sorted values, and tables with small gaps whereever possible.
Similar to the Dollhouse Demo, it would be nice to have a construct that declares blocks which are aware of their own timing.
Would be beneficial to have an easier way to include third-party code, like Famitracker's NSF Driver, or the TV Detection Code.
$000 - $007 : used internally
$010 - $0ff : defvar, function parameters, local vars
$100 - $1ff : stack
$200 - $2ff : sprite data
$300 - ... : available
TODO(dustmop): Fill the rest of this documentation out.
(defsub (my-func param-a param-b)
; body goes here
)
Defines a subroutine. Can be called with (my-func val-a val-b)
Same as defsub
, but for defining reset
and nmi
.
(deflabel my-table)
Define a label, such as a data table in ROM.
(bytes 1 2 3)
Define raw bytes in ROM.
(words 6502 1234)
Define word sized (16-bit) values in ROM.
(defconst starting-health 30)
Define a constant.
(defenum 'days-of-week monday tuesday wednesday)
Define constants that increase monotomically in representation.
TODO
(defvar my-var)
Define a byte-sized var. Defaults to zeropage.
(defvarmem another-var #x310)
Define a byte-sized var at an arbitrary memory location.
(defword my-word)
Define a word-sized (16-bit) var.
(defpointer my-pointer)
Define a pointer. Must be used in zeropage. See set-pointer!
and peek
.
(let ((n) (m))
; body ...
)
Define local variables that are only visible in this scope. Initial values are undefined.
(set! n 7)
Assign a value to a variable.
(set-multiple! n m p (func-with-multiple-returns))
Assigns values from a multiple-return-value function to multiple variables. See return
.
(return 3)
Return from the current subroutine. The argument will be used as the return value from this subroutine. Up to three values can be returned at a time. These should be handled by the caller by using set-multiple!
.
(defsub (func-with-multiple-returns)
(return 23 45 67))
(memset start value limit?)
Sets bytes from start
, counting through limit
, to value
. limit
is inclusive, so (memset #x20 0 1)
sets #x20 and #x21 to 0. if limit
is omitted, defaults to 255 (convenient for clearing a "page" of memory; for example, (memset sprite-data 0)
clears the 256 bytes of sprite memory to all zeroes).
(asm "lda #1")
Emit raw Assembly.
adc cmp cpx cpy eor sbc and ora xor
lda ldx ldy sta stx sty asl lsr rol ror
bit beq bcc bcs bne bmi bpl bvc bvs jmp
clc cld cli clv dex dey inx iny nop pha
pla rts sec tax tay tsx txa txs tya jsr
All standard 6502 instructions can be used directly. Expressions are not allowed for these usages.
(lsr a)
(lda 7)
(lda n)
(lda (addr #x421))
(lda (addr #x421) x)