A2 is a programming language targeting the MOS 6502 computer architecture of the Apple II. It is more higher-level than 6502 assembly, but not as high-level as C.
Copyright © 2022 Taeber Rapczak <[email protected]>. License: MIT.
Here's a sample:
; hello.a2 (without comments)
asm {
ORG $800
JSR main
JMP $3D0
}
use [
COUT : sub <- [ch: char @ A] @ $FDED
CROUT: sub @ $FD8E
]
var PTR: word @ $06
let Println = sub <- [txt: text @ PTR] {
var i: int @ Y
i := 0
loop if txt_i <> 0 {
COUT(txt_i)
i += 1
}
CROUT()
}
let main = sub {
CROUT()
Println("Hello, world!")
}
Check out the samples as well as the test cases being used to verify the correctness of the compiler.
(The grammar is written in PEG, if you're into that kind of thing. And if you are into that kind of thing, check out my VIM plugin for PEG.)
$ git clone https://github.com/taeber/a2lang
$ cd a2lang
$ ./a2 build samples/hello.a2
$ ./a2 run
# Opens an Apple II emulator.
# Load the build/DISK.DSK disk image, then run:
]BRUN PROG
The language provides very few data types.
The three fundamental types are byte
, char
, and word
.
byte
is simply an 8-bit value.word
is a 16-bit, little-endian, unsigned value.char
is also 8-bit, likebyte
, but it logically represents a single ASCII character.
In addition, there are composite types that allow for collections of both
homogeneous (array
) and heterogeneous (group
) items.
; Comments start with a semi-colon.
var scores: byte^10 ; array of 10 bytes
; Define a new group called Point3D
let Point3D = [
X: byte ; Like Go, commas are required by the grammar
Y: byte ; but the compiler accepts a new line as well.
Z: byte
]
var points: Point3D
There are also pointers to memory locations, but unlike other languages and specifically because of the limitations of the 6502, their locations must be known at compile-time and they must reside in the Zero Page.
You can also define new types aliases. These are builtin aliases:
let [
int = :byte
addr = :word
text = :char^
]
A2 provides conditional checks (if
) and looping (loop
), but only one kind
for each.
There are no for
-loops, do
-loops, else
, or switch
statements.
Inspiration was taken from Go when considering what not to include in the
language.
Comparisons always require two arguments and, with the exception of <>
for
not equals, are operators are C-like.
The 6502 has builtin support for subroutines using the JSR
(Jump to
Subroutine) and RTS
(Return from Subroutine), however, it doesn't support
parameters in the programming language sense of the word. A2 adds support for
input and output parameters.
; Println writes the NUL-terminated msg pointed at by PTR along with a carriage
; return and outputs the number of characters written, n.
use Println: sub
<- [msg: char^ @ PTR]
-> [n: byte]
It is vital to note that local variables (including arguments) do not have automatic storage like in C. This means that recursion is not generally supported; rather, it can be done, but you have to manage your own stack. This was done to make it easier to access locals when writing inline assembly.
When implementing a subroutine in assembly, you can access the return value by
using its qualified name, such as in: LDA Println.n
.
Furthermore, the 6502 stack is hardwired to be the First Page ($100-1FF
) and is therefore only 256 bytes, so the recursion depth would be seriously limited.
Perhaps the most tedious aspect of A2, relative to higher-level languages, is basic arithmetic. A2 does not support complex statements; you must split these up
; Most languages: Z = 42 + B - C
; A2 be like:
Z := 42
Z += B
Z -= C
This is because the 6502 has a single register that supports arithmetic operations, the Accumulator. Therefore, the language makes the user be explicit about where they are loading and storing values from so as to pick the appropriate addressing mode.
Inspired by TypeScript, the language started out as a markup for 6502 assembly. This can be seen in subroutine declarations.
Take the Apple II ROM subroutine PRNTAX
found at $F941
.
It "Prints A-Reg as Two-Digit Hex Number" followed by X
.
This is declared in A2 as: use PRNTAX: sub <- [val: word @ AX] @ $F941
.
It can then be called like: PRNTAX($BEAD)
.
Normally, such assembly routines and their inputs and outputs are documented in English prose or perhaps some arbitrary convention or not at all! Arguably the most useful aspect of A2 is this formalization of declarations of existing assembly code.
The PRNTAX
example illustrates both register-binding and
location-binding.
In short, a variable can live at a fixed memory location or in a register.
For register-bound variables, you can use a single register—A
, X
, or Y
—or
two registers like AX
in which the most-significant byte is stored in A
and
the other byte in X
.
The compiler is aware of the binding and generates the appropriate operations.
One neat example is that an X
-bound local variable being passed to an
A
-bound parameter only needs one operation: TXA
.
Such direct control of the underlying hardware should be used cautiously though;
it's very easy to clobber a register.
Like C, the language provides no facilities for input and output, no storage allocation facilities, no heap, and no garbage collection. The expectation is that there already exists routines for the system that need only be declared and called by the A2 program.
Why would you use this language? I hope that it's quicker to write software for the Apple II (and maybe other 6502-based machines like the Commander X16) than writing in pure assembly, but that it also allows for super easy integration with assembly when its needed.
On most systems, byte
and char
are essentially equivalent, but the Apple II
line of computers uses what is often called "High-ASCII" since the 8th-bit
needs to be set for a character to display normally in Apple II Text Mode.
In ASCII, an uppercase A is $41
in hexadecimal, but in High-ASCII,
it is $C1
.
Currently, the compiler treats the two types as a byte
, but does output text
and character literals in High-ASCII.
Since the compiler (compile
) only translates A2 code into 6502 assembly, an
assembler is still needed convert that into machine binary. I've included the
a2
bash script to make it less painful. That script will download a2asm
from GitHub, build it, and run it.
Anyway, you can add an a2
alias with tab completion for Bash by running:
$ eval $(./a2 bash)
$ a2 help
Bugs? Yeah, the compiler is buggy and only partially complete.
I very quickly ran out of time for this passion project.
Still, since a2
can produce working 6502 binaries that run on my Apple //e, I
decided it was time to put it out there.
Feel free to file a GitHub issue and I'll try to fix it. Or, you know, sponser me to work on it full time and I'll find all the bugs for you! ;-]