Cheat Sheet
Need a quick reminder of how to write something? These two pages tour the entire Adder language through small, complete programs you can run right away. Each section links to the handbook chapter where you can explore the full details.
Declarations
const declares a fixed value, var declares mutable state, and <- creates a reactive binding or hardware output that stays in sync automatically. Inside a handler or its loops, name = expression updates existing var state. See Types and Declarations.
type Lane = Number<0...39> // a friendly nickname for a type
enum Mode
case title, playing, over
end
struct Speed(x: Number<-3...3>, y: Number<-3...3> = 0)
const limit = 40 // a constant: fixed before the program runs
var lane: Lane = 0 // state: remembered from frame to frame
var mode: Mode = .title
var speed: Speed = (x: 1)
fn mirror(x: Lane): Lane // a pure function: same inputs, same answer
Lane.last - x
end
op change(value: Lane) // an op: a calculation that remembers past frames
value - value.previous(from: value)
end
far <- mirror(lane) // a binding: updates whenever lane changes
moved <- change(lane) != 0
System.screen.border <- if moved then .yellow else .black end
System.screen.text[0][0..<2] <- far.digits(2)
when System.keyboard.space.pressed // a handler: runs when an event happens
lane = lane +% 1
mode = mode.next()
endA struct block can bundle fields and helper functions together (mut fn can update the struct's own var fields), and at places a value at a specific memory address.
struct Score
step: Number<u8>
var points: Number<0...999> = 0
mut fn award()
points = points +| step
end
fn text(): [3 of Char]
points.digits(3)
end
end
var score = Score(step: 10)
seen: Number<u8> at 0xC000
when System.keyboard.space.pressed
score.award()
end
System.screen.text[0][0..<3] <- score.text()
System.screen.border <- Color(seen % 16)To split a larger project across files, include "lib/world" brings another file from the project into your program.
Numbers
Every number in Adder knows the range of values it can hold. Write a custom range like Number<0...9> or Number<0..<40>, or pick a hardware storage format directly (u8, i8, u16, i16 for whole numbers, and u8.8, i8.8, u16.8, i16.8 for fractions in steps of 1/256). Writing Number by itself gives you a two-byte whole number from -32,768 to 32,767. See Numbers and Expressions.
var a: Number<u8> = 0xFF // also 255 or 0b1111_1111
var b: Number<0...9> = 5
var c: Number<0..<40> = 0
var d: Number<-2.0...2.0> = 0.5
const mask = 0b0000_0101
when System.screen.frame
b = b +% 1 // wraps around: 9 steps back to 0
c = c +| 3 // clamps at the top: stops at 39
a = a.bitwise_and(mask).shift_left(1)
end
whole <- d.floor() // also round() and ceil()
byte <- Number<u8>(b) // an explicit conversion
System.screen.border <- Color(b)When a calculation reaches the edge of a variable's range, the operator you choose decides what happens next:
| Operator | What happens if the result leaves the range |
|---|---|
+ - * | stops the program and reports the line number on screen |
+% -% | wraps around to the other end of the range |
+| -| | clamps at the nearer end of the range |
/ % | dividing by zero stops the program and names the line |
Multiplication (*) is always checked. If you want a product to wrap around, take its remainder with %, such as (a * b) % 16.
Control
Use if ... then ... end for conditions and match for branching on cases or condition tables. Inside a when handler, you can filter events with a second when, bail out early with guard, or pause across frames with wait. See Events and Handlers and Statements.
var lives: Number<0...3> = 3
var score: Number<0...99> = 0
shade <- if lives == 1 then Color.red else Color.black end
System.screen.border <- match
when lives == 0 -> Color.gray
else -> shade
end
when System.keyboard.space.pressed when lives > 0
lives = lives - 1
end
when lives.changed
guard lives == 0 // stops the handler here unless true
score = 0
end
when start
forever // repeats; every turn must reach a wait
wait(ms: 500) // or wait(30) frames, wait(seconds: 2)
score = score +% 1
end
endAmong declarations, a match or an if holds declarations, and what a case declares exists only while the case matches. A match divides the program into cases—such as a title screen and a play screen—where only the active case's bindings and handlers run. Cases can add when condition; without a value after match, ordered when condition -> lines and a final else -> choose the case.
enum Scene
case title, game
end
var scene: Scene = .title
match scene
title ->
System.screen.text[11][15..<25] <- "PRESS FIRE"
when System.joystick[2].fire.pressed
scene = .game
end
game ->
System.screen.border <- .green
when start
wait(seconds: 2)
wait System.joystick[2].fire.pressed
scene = .title
end
endLists and Slices
[n of T] is a fixed-length list of n items. Put a range in brackets to take a window (slice) into a list, or two ranges to take a 2D rectangle from a grid of rows. See Lists and Expressions.
var xs: [8 of Number<u8>] = [3, 1, 4, 1, 5, 9, 2, 6]
const grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
var left: Number<0...4> = 0
window <- xs[left..<(left + 4)] // a 4-item window whose start can slide
corner <- grid[0..<2, 1..<3] // a 2-by-2 rectangle
big <- xs.filter(n -> n > 3) // keeps matching items as Number<u8>?
doubled <- xs.map(n -> n * 2)
all_of <- xs.sum()
fn add_up(values: [n of Number<u8>]): Number<u16>
var t: Number<u16> = 0
for v in values
t = t + v
end
t
end
part <- add_up(window)
when System.keyboard.space.pressed
xs[0] = 7
left = (left + 1) % 5
endOptionals
Add ? to any type (T?) when a place might either hold a value or be empty (nil). See Optionals.
var held: Number<u8>? = nil
when System.keyboard.space.pressed
held = 7
end
when System.keyboard.delete.pressed
if n = held then // unwraps and names the value when present
held = n -| 1
end
end
shown <- held ?? 0 // uses 0 when held is nil
System.screen.border <- if held != nil then Color(shown % 16) else .black endDevices
Every part of the Commodore 64 lives under System—no hex addresses required. See The System Device Tree.
| Device | Members |
|---|---|
System.screen | border, background, text, ink, font, sprite, frame, frames, standard, frame_cycles, canvas(...), region(...) |
| a region | text, print, println, newline, home, clear, shown, cursor |
| a canvas | drawings, ink, paper, shown |
System.screen.sprite[i] | x, y, shown, shape, color |
System.keyboard | each key by name, stroke, line, caret, dropped |
System.joystick[1], [2] | dx, dy, fire |
System.mouse | x, y, left, right |
System.paddle[1], [2] | a knob reading from 0 to 255 |
System.sound[1].voice[1] to [3] | <- a held Tone or .silence, play(...), release |
System.clock | interval(...), timer(...), hours, minutes, seconds, tenths |
bat: Number<0...296> <- (System.joystick[2].dx * 4).sum(from: 136)
held <- System.joystick[2].fire
beat <- System.clock.interval(seconds: 0.25)
var hue: Number<0...15> = 0
when beat
hue = hue +% 1
end
when System.keyboard.space.pressed
System.sound[1].voice[2].play(Tone(.c6, decay: .short))
end
System.screen.border <- Color(hue)
System.screen.background <- .black
System.screen.text[0][0..<5] <- "SCORE"
System.screen.ink[0][0..<5] <- .yellow
System.screen.sprite[0] <- (x: bat, y: 170, color: .white)
System.sound[1].voice[1] <- if held then Tone(.c4) else .silence endconst picture = System.screen.canvas()
const badge = Picture(["XXXXXXXX", "X......X", "X......X", "XXXXXXXX"])
x: Number<0...312> <- System.joystick[2].dx.sum(from: 80)
picture.ink <- .yellow
picture.paper <- .blue
picture.drawings <- [badge.place(x: x, y: 40), Text(content: "SCORE").place(x: 16, y: 8)]const log = System.screen.region(row: 1, height: 3, width: 12)
var items: Number<0...999> = 0
when System.screen.frame.every(30)
items = items +% 1
log.println(items.digits(3))
endInterrupt Routines
When you want a tiny block of 6510 assembly to run at a specific raster line on screen or on a millisecond timer, wrap it in an interrupt handler. The compiler wires up the interrupt and budgets its cycles in every frame. See Interrupt Routines.
when System.screen.interrupt(line: 100) // once a frame, when the beam hits line 100
asm
inc System.screen.border
end
end
when System.clock.interrupt(ms: 20) // every 20 milliseconds
asm
inc ticks
ticks:
.byte 0
end
endCosts
The compiler proves before your program runs that every frame's work finishes before the next frame begins. When a heavy loop needs a speed boost, put fast before a handler, function, op, binding, or case to spend a few extra bytes of memory in exchange for faster code. See Costs and the Frame.
var weights: [64 of Number<u8>] = 3
var levels: [64 of Number<u8>] = 5
var mix: Number<u16> = 0
fast when System.screen.frame
var acc: Number<u16> = 0
for i in 0..<64
acc = acc +% weights[i] * levels[i]
end
mix = acc
end
System.screen.border <- Color(mix % 16)You can also inspect every line's exact byte count and worst-case cycle estimate from the terminal, or compare two versions of a program without running an emulator:
bun tools/costs.ts ../game.adder --listing
bun tools/cost-diff.ts ../game.adder --at HEADfast forces the listed larger, faster forms on a code head; small keeps its compact forms. Both can mark functions, ops, handlers, bindings, case patterns and regions. The innermost mark decides, including an explicitly marked body copied at a call.
About the Language Reference
Think of this reference as your desk manual for Adder and the Commodore 64. Each chapter takes one part of the language and shows you how to write it, what it means at runtime, which types and number ranges it works with, what it costs in bytes and processor cycles, and how the compiler helps you catch mistakes early.
If you would rather learn by building a small arcade game from scratch, begin with the Adder Quickstart and come back here whenever you are curious about the full details of a feature.
Examples
Every framed code block in this handbook is a complete, runnable Adder program. You can paste it into the Lab or save it to a .adder file and run it immediately:
System.screen.border <- Color.light_blueWhenever a chapter explains a rule that the compiler checks before your program runs, it names the message key in backticks, such as wrongType. You can look up any key in Diagnostics to see the exact message the compiler gives and how to fix it.
Grammar Notation
Each construct includes a small grammar box that shows all the ways you can write it. Every line of the form name ::= form defines what name stands for. When a construct has several forms, | separates them, and a line starting with | continues the choices from the line above.
example ::= 'when' event ( 'when' condition )? newline statement* 'end'Here is how to read the symbols inside a grammar box:
| Notation | Meaning |
|---|---|
'when' | the word or symbol itself, written just as shown between the quotes |
event | a part that another grammar rule defines |
a b | a followed by b |
a | b | either a or b |
( a b ) | a b grouped together so a mark below applies to both |
a? | a is optional (write it once or leave it out) |
a* | a repeated zero or more times |
a+ | a repeated one or more times |
'a' to 'z' | any single character from a through z |
newline | the end of a line |
The grammar tells you which programs have a valid shape. Once a program's shape is valid, the compiler checks its types, number ranges, and frame timing—and each chapter explains how those checks work. The Grammar Summary gathers every grammar box from the handbook into one place for quick lookup.
Lexical Structure
Before your program can light up the screen or play a note, the compiler reads your source text character by character and groups those characters into tokens: names, reserved words, literals, operators and punctuation. It scans each line from left to right and always takes the longest run of characters that forms a valid token at each spot—so format is read as one whole name rather than the reserved word for followed by mat, and <= is one operator rather than < followed by =.
Source Text
An Adder source file is plain ASCII text up to 65,535 characters long—plenty of room for a brisk arcade game or a musical toy, while still fitting comfortably in the machine's memory. Outside comments, every character you type belongs either to a token, to a string literal, or to the whitespace between tokens. If a stray character slips in, the compiler points it out by name so you can sweep it away.
Spaces, tabs and carriage returns simply separate tokens and carry no hidden meaning. Indentation is purely for your own eyes: every block closes explicitly with the word end, never with a shift in indentation, so you can lay out your code however it reads best to you.
Comments
A comment starts with // and runs to the end of the line. The compiler skips right past it, so you can jot down any characters or notes you like inside. Adder has no multi-line comment marker; for a longer note, just start each line with //.
// The border steps through its sixteen colors.
var hue: Number<0...15> = 0 // the color shown now
when System.screen.frame
hue = hue +% 1
end
System.screen.border <- Color(hue)Lines
A line break finishes a declaration or a statement, giving each line a single job. Adder uses no semicolons and keeps one declaration to a line; if you pack two declarations onto one line or end a line with a semicolon, the compiler catches it right away.
When a call, a struct value or a list grows long, you can split it across lines inside an open ( or [ at three clean spots:
- right after the opening bracket;
- right after a comma;
- right after a finished item, just before the closing bracket.
You are welcome to leave a trailing comma on the last item of a call, a struct value or a list—handy when you rearrange lines later. A value if or match inside brackets keeps its own multi-line layout.
System.screen.text <- Text(
row: 2,
col: 4,
content: "Hello",
)If a line breaks anywhere else inside brackets—such as right after a colon or an operator—the compiler flags the break and names the bracket that is still waiting to close.
Brackets can nest up to 16 levels deep within a single expression. If an expression burrows deeper than that, the compiler stops with nestingExpression and invites you to pull part of the calculation out onto an earlier line with its own name.
line-break-in-brackets ::= '(' newline | '[' newline
| ',' newline
| item newline ( ')' | ']' )Names
A name starts with an ASCII letter (a to z or A to Z) or an underscore, followed by any mix of letters, digits and underscores. Case matters: score and Score are two distinct names.
name ::= name-head name-character*
name-head ::= 'a' to 'z' | 'A' to 'Z' | '_'
name-character ::= name-head | '0' to '9'You can tell a type from a value at a glance by its very first character:
- a value name—for a constant, variable, binding, function, op or parameter—starts with a lowercase letter or an underscore;
- a type name—for a
type,enumorstruct—starts with a capital letter.
If those get swapped, the compiler gently steers you back: a capitalized value declaration triggers capitalizedDeclaration, and a lowercase type declaration triggers aliasName, enumName or structName.
Reserved words (listed under Reserved Words) cannot be used as names, and any name starting with two underscores (__) is reserved for the compiler's own machinery (internalName). A handful of built-in names mean the exact same thing in every program, so the compiler keeps them safe from redeclaration with builtinName: the built-in types, the root word System that starts every device path, and the scratch workspace used in assembly. Built-in member names like count, sum, min, max, digits and random, on the other hand, are yours to use freely: you can name a constant, variable, function or parameter count, and scores.count() will still give you the built-in count of scores.
Reserved Words
These keywords form the backbone of Adder's grammar and are never used as ordinary names. Number opens every numeric type, and the eight storage format names appear only inside Number<...>, though none of them may name a value.
keyword ::= 'and' | 'asm' | 'at' | 'break' | 'case' | 'const' | 'else' | 'end' | 'enum'
| 'false' | 'fast' | 'small' | 'partial' | 'fn' | 'for' | 'forever' | 'guard' | 'if' | 'in'
| 'include' | 'match' | 'mut' | 'nil' | 'not' | 'Number' | 'of' | 'on'
| 'op' | 'or' | 'output' | 'start' | 'struct' | 'then' | 'true' | 'type'
| 'var' | 'wait' | 'when' | 'work'
format-name ::= 'u8' | 'i8' | 'u16' | 'i16' | 'u8.8' | 'i8.8' | 'u16.8' | 'i16.8'Words Inside Assembly
Step inside an asm block and the vocabulary shifts to match the processor. Inside asm, the 6510 instruction mnemonics below become reserved words, while guard, start, then, for, break, wait and forever relax into ordinary names. Outside asm, those instruction mnemonics are free for your own variables—so calling a counter inc or dex in regular Adder code is completely fine. The header words writes, runs and cycles are reserved only on the opening line of an asm block, where they introduce its optional clauses, and the addressing marks below belong exclusively to assembly lines. Inside assembly, hexadecimal numbers wear a leading $ and binary numbers wear a leading %. Turn to the Assembly chapter for the full tour.
assembly-word ::= 'writes' | 'runs' | 'cycles' | '#' | ',x' | ',y' | '.lo' | '.hi' | '.byte' | '.text'
instruction-name ::= 'adc' | 'alr' | 'anc' | 'and' | 'arr' | 'asl' | 'asr' | 'axs'
| 'bcc' | 'bcs' | 'beq' | 'bit' | 'bmi' | 'bne' | 'bpl' | 'brk'
| 'bvc' | 'bvs' | 'clc' | 'cld' | 'cli' | 'clv' | 'cmp' | 'cpx'
| 'cpy' | 'dcp' | 'dec' | 'dex' | 'dey' | 'eor' | 'inc' | 'inx'
| 'iny' | 'isb' | 'isc' | 'jmp' | 'jsr' | 'lax' | 'lda' | 'ldx'
| 'ldy' | 'lsr' | 'nop' | 'ora' | 'pha' | 'php' | 'pla' | 'plp'
| 'rla' | 'rol' | 'ror' | 'rra' | 'rti' | 'rts' | 'sax' | 'sbc'
| 'sbx' | 'sec' | 'sed' | 'sei' | 'slo' | 'sre' | 'sta' | 'stx'
| 'sty' | 'tax' | 'tay' | 'tsx' | 'txa' | 'txs' | 'tya'Literals
A literal spells out a fixed value right in your code: a number, a quoted string of text, a yes-or-no flag or nil.
literal ::= whole-literal | decimal-literal | string-literal | 'true' | 'false' | 'nil'Whole-Number Literals
You can write a whole number in everyday decimal digits, in hexadecimal after 0x, or in binary after 0b—perfect when you are masking bits or laying out hardware flags. Both the prefix letters (0x, 0X, 0b, 0B) and the hexadecimal digits (a–f or A–F) accept uppercase or lowercase. In hexadecimal and binary literals, you can tuck a single underscore between digits to group nybbles or bytes cleanly (0b0000_0101). Decimal literals are written as plain digits without underscores.
whole-literal ::= decimal-digits
| '0x' hex-digit ( '_'? hex-digit )*
| '0b' binary-digit ( '_'? binary-digit )*
decimal-digits ::= decimal-digit+
decimal-digit ::= '0' to '9'
hex-digit ::= decimal-digit | 'a' to 'f' | 'A' to 'F'
binary-digit ::= '0' | '1'How you write the literal never locks in its storage width or type. 5, 0x05 and 0b0000_0101 are the exact same number five; the spot that receives the number decides how it is stored, as Types explains.
const north_and_south: Number<u8> = 0b0000_0101
var open_paths: Number<u8> = 0x0F
either_open <- open_paths.bitwise_and(north_and_south) != 0
System.screen.border <- if either_open then .green else .red endA number literal itself is always positive: -5 is the unary - operator applied to 5. If a number is mistyped—say, trailing letters after digits, a 0x or 0b prefix with no digits, a leading or trailing underscore, or two underscores side by side—the compiler flags it with badNumber.
Decimal Literals
A decimal literal has digits on both sides of a decimal point, like 0.5 or 3.25. It holds its exact mathematical value until it lands in a destination type, which then snaps it to the nearest fixed-point step of 1/256. Writing a decimal point always marks the literal as fractional, even when the fraction part is zero—so 2.0 asks for a fractional type while 2 stays a whole number.
decimal-literal ::= decimal-digits '.' decimal-digitsHanding a decimal literal to a whole-number type won't slip past the compiler; use a whole number or an explicit rounding call instead.
String Literals
A string literal is a row of printable ASCII characters—from space ( ) through tilde (~)—wrapped in double quotes on a single line. Inside the quotes, \" produces a literal double quote and \\ produces a literal backslash; those are the only two escape pairs you ever need to remember.
string-literal ::= '"' string-character* '"'
string-character ::= any printable ASCII character except '"' and '\'
| '\"' | '\\'Under the hood, a string literal is simply a fixed-length list of Char, one per character: "HELLO" has the type [5 of Char]. Wrapping it in ScreenCode("HELLO") translates those characters straight into screen codes ready for the display, as Types shows.
const greeting = "SAY \"HI\""
System.screen.text[0][0..<8] <- greetingThe compiler keeps strings crisp and predictable: it catches characters outside printable ASCII (such as accented letters) with stringNotPrintable, a string that runs off the end of its line without a closing quote with unterminatedString, and any unknown backslash escape with badEscape.
Yes-or-No and Missing Values
true and false are the two values of the boolean type bool. nil stands for the missing value of any optional type.
Operators and Punctuation
These symbol tokens glue expressions and declarations together. The Expressions chapter shows what each operator computes and the precedence order in which they bind.
operator ::= '+' | '-' | '*' | '/' | '%' | '+%' | '-%' | '+|' | '-|'
| '==' | '!=' | '<' | '<=' | '>' | '>=' | '??' | '++' | '...' | '..<'
punctuation ::= '=' | '<-' | ':' | '->' | '?' | '(' | ')' | '[' | ']' | ',' | '.'| Token | Use |
|---|---|
= | names a constant, gives a variable its first value, or sets a variable in a handler |
<- | keeps a binding or an output equal to an expression |
: | follows a name with its type, and a label with its argument |
-> | follows a pattern in a match, and a parameter in an arrow function |
? | after a type, makes it optional |
... and ..< | a range with its last value, or without it |
. | picks a member, or a case of the expected type when it begins a value |
Types
Every value in an Adder program has a type, and the compiler knows every type before a single instruction runs. A type tells the machine which values a name can hold and how many precious bytes of memory to set aside for it. Values never silently morph from one type into another behind your back—Adder converts a value only where your program writes the conversion out in the open. If a value of the wrong type is handed to a spot that expects something else, the compiler catches the mismatch right away with wrongType.
Here is the full cast of types you can work with:
- numbers, defined by the exact range of values each one holds;
- yes-or-no values, of type
bool; - named sets built into the machine, such as
Color,NoteandKey; - characters and screen codes for text and screen cells;
- pictures and fonts, which the compiler packs straight into your program;
- enums and structs, either declared by your program or built in;
- lists of a fixed length;
- optionals, which hold either a value or
nil; - events, which happen at specific moments rather than sitting in memory.
type ::= number-type | name type-range? | list-type | type '?'
| 'Event' ( 'of' type )? | 'Result' 'of' type
number-type ::= 'Number' ( '<' format-name '>' | '<' range '>' )?
range ::= bound '...' bound | bound '..<' bound
type-range ::= '<' bound '...' bound '>'
list-type ::= '[' size 'of' type ']'Numbers
In Adder, a number type is simply the range of values you need it to hold. Number<low...high> holds every value from low through high, including both ends. Number<low..<high> holds every value from low up to, but stopping just short of, high—handy when counting across 40 columns numbered 0 through 39.
var lives: Number<0...3> = 3
var column: Number<0..<40> = 0
when System.keyboard.space.pressed
lives = lives -| 1
column = column +% 1
end
System.screen.text[0][0..<1] <- lives.digits(1)
System.screen.border <- Color(column % 16)Range Bounds
Both ends of a range must be fixed values that the compiler can work out before the program runs. An end can be a literal number, a numeric constant declared earlier, a bound member of a numeric type like Lane.last, a unary minus, or a compile-time calculation built from those using +, -, *, / and %. Whenever an end is a calculation, wrap it in parentheses so the angle brackets stay easy to read. Each bound expression can have at most 128 parts and nest up to 32 levels deep.
const columns = 40
type Column = Number<0..<(columns * 2)>
var column: Column = 0
System.screen.border <- Color(column % 16)Because a variable, a binding, a live device reading or a function call only gets its value while the program is running, none of them can serve as a range bound (rangeEndNotFixed).
Likewise, a range whose starting value is greater than its ending value has no room for any number at all, so the compiler flags it with rangeEmpty.
Fractions
Write a decimal point in either end of a range and you get a fixed-point fractional type, measured in crisp steps of 1/256. Number<0.0...1.0> holds 0, 1/256, 2/256 and so on all the way up to 1—smooth enough for sub-pixel sprite motion and gentle gravity curves without floating-point overhead. In a fractional range, ..< leaves out just the final 1/256 step: Number<0.0..<1.0> tops out at 255/256 (about 0.996).
When a fractional type receives a decimal literal, it snaps the number to the nearest 1/256 step; if that rounding nudges a literal past either end of the range, the compiler lets you know.
A whole number fits naturally into a fractional spot without a conversion whenever its range fits inside. Going the other way—turning a fraction back into a whole number—always asks you to pick how to round it with floor(), round() or ceil(), as Expressions describes.
Storage Formats
The compiler packs each number into the smallest of eight formats that can hold its entire range. If you want the full span of a machine format, you can write the format name itself inside Number<...> instead of a range.
| Format | Holds | Step | Bytes |
|---|---|---|---|
u8 | 0 to 255 | 1 | 1 |
i8 | -128 to 127 | 1 | 1 |
u16 | 0 to 65,535 | 1 | 2 |
i16 | -32,768 to 32,767 | 1 | 2 |
u8.8 | 0 to 255.996 | 1/256 | 2 |
i8.8 | -128 to 127.996 | 1/256 | 2 |
u16.8 | 0 to 65,535.996 | 1/256 | 3 |
i16.8 | -32,768 to 32,767.996 | 1/256 | 3 |
Even when a narrow range rides inside a 1-byte format, it remembers its own exact bounds: Number<0...9> takes one byte in memory and still accepts only 0 through 9. If a range stretches wider than every available format—such as -1 to 65,535, which needs both negative numbers and the top half of a 16-bit unsigned word—the compiler stops with rangeNoRepresentation, and any unknown format name inside Number<...> triggers numericFormat.
Individual bit positions belong to the two unsigned formats: Number<u8> gives you 8 bits and Number<u16> gives you 16, ready for the bit operations in Expressions to inspect and flip. If you have a ranged number or a default Number and want to twiddle its bits, convert it to Number<u8> or Number<u16> first.
The Default Number
Written by itself, Number holds whole numbers from -32,768 to 32,767—the full span of i16. When you declare a var without spelling out its type, it takes that i16 range if its starting value is a whole number, or the i16.8 range if its starting value has a decimal point. Starting a variable at a small number never boxes it in: var score = 0 has plenty of headroom to climb to 30,000 later on.
var score = 0
var speed = 0.5
fn apart(a: Number, b: Number): Number
if a > b then a - b else b - a end
end
System.screen.text[0][0..<3] <- apart(score, 100).digits(3)A <- binding automatically takes the tightest range the compiler can prove for its expression, while a constant keeps its exact single value.
Values Outside a Range
Whenever the compiler can see right away that a value falls outside its target range, it catches the mistake at compile time.
Of course, variables change as the game plays, and the compiler cannot always prove ahead of time that a calculation stays in bounds. When a runtime check is needed, the compiled program guards the store automatically: if an out-of-range number tries to slip in, the program halts cleanly and prints the offending line number right on the screen. When you want a number to loop around or stop at the ends—like a color cycling from 15 back to 0 or a life counter stopping at 0—reach for the wrapping (+%, -%) or clamping (+|, -|) operators described in Expressions.
Type Aliases
type Name = T gives an existing type a clear, domain-specific name of your choosing. An alias does not wall off a separate type: values of the alias and values of T work together seamlessly, while compiler messages kindly use your alias name. For any numeric alias, .first and .last give you its lower and upper bounds by name.
type Lane = Number<0...39>
fn mirror(x: Lane): Lane
Lane.last - x
end
column: Lane <- System.screen.frame.cycle()
System.screen.text[0][0..<2] <- mirror(column).digits(2)Yes-or-No Values
The boolean type bool has just two values—true and false—and takes a single byte. You can combine boolean values with and, or and not. Adder never guesses whether a number or an optional means true or false, so the compiler won't let either one stand in for a bool condition.
var paused: bool = false
when System.keyboard.p.pressed
paused = not paused
end
System.screen.border <- if paused then .red else .black endNamed Sets
The machine's own palettes, musical notes, keys and settings are represented as named sets: each item has both a readable name and an underlying number, and each takes one byte. You can write a value in full, like Color.red, or use the short dotted form .red wherever the expected type is already clear from context. Inside a match on a named set, each pattern is written as the bare name without a leading dot.
| Type | Numbers | Names |
|---|---|---|
Color | 0 to 15 | black, white, red, cyan, purple, green, blue, yellow, orange, brown, light_red, dark_gray, gray, light_green, light_blue, light_gray |
Pitch | 0 to 11 | c, cs, d, ds, e, f, fs, g, gs, a, as, b |
Note | 0 to 94 | a pitch and an octave, from c0 through c4 (middle C) to as7 |
Key | 0 to 63 | the keys of the keyboard, listed below |
Waveform | 0 to 3 | triangle, sawtooth, square, noise |
Attack | 1 to 15 | no names |
Decay | 0 to 15 | short (5), medium (8), long (10) |
Sustain | 0 to 15 | no names |
Release | 0 to 15 | no names |
Flip | 0 to 1 | vertical, horizontal |
TextOverflow | 0 to 1 | clip, wrap |
TextWrap | names only | character, word, hyphenated |
TextAlignment | names only | leading, center, trailing |
The 64 Key names are delete, return, cursor_right, f7, f1, f3, f5, cursor_down, three, w, a, four, z, s, e, left_shift, five, r, d, six, c, f, t, x, seven, y, g, eight, b, h, u, v, nine, i, j, zero, m, k, o, n, plus, p, l, minus, period, colon, at_sign, comma, pound, asterisk, semicolon, home, right_shift, equals, up_arrow, slash, one, left_arrow, control, two, space, commodore, q and run_stop.
Calling the type's name like a function converts a number into a member of that set: Color(n) picks color number n, and Attack(2) sets a voice attack rate of 2. If a fixed number falls outside the set's valid range, the compiler catches it with conversionOutOfRange; if the number can change while the program runs, the program checks it on the spot.
var hue: Number<0...15> = 0
when System.keyboard.space.pressed
hue = hue +% 1
end
System.screen.border <- Color(hue)
System.screen.background <- Color.random()Narrowed Named Sets
Just like numbers, a named set can be narrowed to a contiguous slice of its values by writing a range in angle brackets after the type name, with both ends included. Note<Note.c3...Note.c5> holds only the two octaves from c3 through c5, and a type alias makes a narrowed set effortless to reuse.
type Melody = Note<Note.c3...Note.c5>
var note: Melody = .c4
when System.keyboard.space.pressed
note = if note == Note.c5 then Note.c3 else Note.c4 end
endCharacters and Screen Codes
A Char represents a single character of text, numbered 0 to 127 in standard ASCII order: Char(65) is A, and Char.space is a blank space. Every string literal you write is a fixed-length list of Char.
A ScreenCode, numbered 0 to 255, is the byte that actually sits in a cell of the text screen to pick one glyph from the active 256-character font. ScreenCode.space is the blank cell and ScreenCode.block is the solid filled block. Calling ScreenCode(text) converts a whole string into the screen codes that display it, while ScreenCode(c) converts a single Char.
const sign = ScreenCode("ADDER")
var shown: Char = .space
when s = System.keyboard.stroke
if c = s.character() then
shown = c
end
end
System.screen.text[0][0..<5] <- sign
System.screen.text[12][20] <- shownPictures and Fonts
Adder lets you draw sprites, custom characters and bitmap art right in your source file as visual grids of text, which the compiler bakes directly into your compiled program. These art values are constants, usually built from lists of strings where . leaves a pixel clear and X lights it up. A program can pack up to 256 distinct values of each art type.
| Type | Made with | What it holds |
|---|---|---|
Glyph | Glyph([...]) | one character: 8 rows of 8 pixels |
Charset | Charset.uppercase, Charset.lowercase, Charset(glyphs, map:), Charset(file:, map:) | a font of 256 glyphs |
Shape | Shape([...]), Shape.square | a sprite's picture: 21 rows of 24 pixels, or of 12 multicolor pixels |
Picture | Picture([...]) | art for a canvas: rows of one width |
Sheet | Sheet(file:, ink:, transparent:) | a PNG image that pictures are cut from |
Drawing | picture.place(...), picture.fill(...) | a picture at a place on a canvas |
When you draw a multicolor Shape or Picture, each pixel is twice as wide and chooses from four palette slots written ., A, X and B. Charset(glyphs, map:) builds a complete font from a list of 256 glyphs, with an optional map: of 128 screen codes that tells the machine which glyph each Char displays. Charset(file:) loads a raw 2,048-byte character set file straight from your project folder. See The System Device Tree for how to put your shapes, fonts and pictures up on the screen.
const ship = Shape([
"....AAAA....", "...AXXXXA...", "..AXXXXXXA..", ".AXXBBBBXXA.", "AXXBBBBBBXXA", "AXXBBBBBBXXA", ".AXXBBBBXXA.",
"..AXXXXXXA..", "...AXXXXA...", "....AAAA....", "............", "............", "............", "............",
"............", "............", "............", "............", "............", "............", "............"
])
System.screen.sprite[0] <- (x: 100, y: 100, shape: ship)If a row of pixel characters is the wrong width or contains an unrecognized character, the compiler points out the exact row with pixelAsset.
Enums
An enum defines a brand-new type whose values are a hand-picked list of named cases—ideal for game states, directions or monster behaviors. Every enum name starts with a capital letter.
enum Mode
case title, playing, over
end
var mode: Mode = .title
when System.joystick[2].fire.pressed
mode = mode.next()
end
System.screen.border <- match mode
title -> .blue
playing -> .black
over -> .red
endWhen an enum's cases carry no extra fields, they stay in the order you listed them: < and > compare them by position, Mode.first and Mode.last give the two ends, and .next() steps forward from case to case, wrapping neatly from the last back to the first. A simple enum like this takes just one byte.
Cases can also carry named fields of their own. You build such a case by labeling its fields, and unpack those fields inside a match arm:
struct Spot(x: Number<0...319>, y: Number<0...199>)
enum Drag
case still
case moving(grab: Spot, column: Number<0...39>)
end
var drag: Drag = .still
var column: Number<0...39> = 0
when System.joystick[2].fire.pressed
drag = .moving(grab: (x: 10, y: 20), column: 4)
end
when System.screen.frame
match drag
moving(grab, column: chosen) ->
column = chosen
still ->
end
endAn enum with fields takes one byte for its case tag plus enough bytes to hold the fields of its largest case.
enum-declaration ::= 'enum' name newline case-line+ 'end'
case-line ::= 'case' enum-case ( ',' enum-case )* newline
enum-case ::= name ( '(' field ( ',' field )* ')' )?
field ::= name ':' typeStructs
A struct bundles related named fields into a single tidy value—like a sprite's velocity, an enemy's coordinates or a high-score row. In its compact one-line form, you list the fields inside parentheses right after the struct's name. Any field can specify a default value with =, which steps in whenever a newly created struct value leaves that field out.
struct Speed(x: Number<-3...3>, y: Number<-3...3> = 0)
const speeds: [3 of Speed] = [(x: 1, y: 2), Speed(x: -1, y: 3), (x: 2)]
n: Number<0..<3> <- System.screen.frame.every(30).cycle()
s <- speeds[n]
System.screen.sprite[0] <- (x: 100 + s.x * 10, y: 100 + s.y * 10)When you create a struct value, you label each field you supply. You can write the type name out in front, like Speed(x: 1, y: 2), or write just (x: 1, y: 2) whenever the receiving spot already expects a Speed. A spot knows what struct type to expect when it is a typed var, a function parameter, a function's return value, a struct or enum field, an element of a typed list, a branch of an if or match, or a hardware sprite output. That expected type also flows right through an optional (Speed?), into a single value replicated across a whole list ([4 of Speed] = (x: 0, y: 0)), and into the body of a .map(...) arrow whose result is expected to be a list of structs. If you write an unnamed (x: 1, y: 2) where no struct type is expected, the compiler flags it with structNeedsContext, quotes the value back to you, and—when only one struct in your program has those field names—shows the exact named form to use. Reading a field is as simple as value.field, and a struct takes the sum of its fields' bytes in memory.
For richer types, the multi-line block form of struct can also hold mutable var fields and member functions (fn and op), as Declarations explains. To keep structs self-contained, a struct's function reads only its parameters, the struct's own fields and the program's top-level constants; if it needs a program variable, binding or hardware device, pass that value in as an argument rather than reading it directly (operationScope).
struct-declaration ::= 'struct' name '(' struct-field ( ',' struct-field )* ')'
| 'struct' name newline struct-member* 'end'
struct-field ::= name ':' type ( '=' expression )?Built-In Structs
| Type | Fields |
|---|---|
Point | x 0 to 319, y 0 to 199 |
Sprite | x -24 to 487, y -50 to 205, shown and behind as bool, shape a Shape, color a Color |
Tone | a note with its sound, made with Tone(note, decay:, waveform:, envelope:) |
Envelope | attack, decay, sustain and release |
Keystroke | key a Key, and shift, control, commodore and repeat, each a bool |
A sprite's x and y coordinates measure pixels from the top-left corner of the visible picture, and their negative lower bounds let a sprite glide smoothly onto and off every edge of the screen.
Lists
[n of T] is a fixed-length list holding n items of type T. Because small machines thrive on predictable memory layout, the length n is a whole number from 1 to 65,535 known at compile time (and may be a parenthesized calculation of constants). Elements can be of any type, including inner lists: [3 of [4 of Number<u8>]] gives you a neat 3-by-4 grid.
const rows = 3
var grid: [rows of [4 of Number<u8>]] = 0
const tune: [4 of Note?] = [.c4, nil, .e4, .g4]
when System.keyboard.space.pressed
grid[1][2] = 7
end
System.screen.text[0][0..<1] <- grid[1][2].digits(1)In memory, a list takes n times the byte size of a single element. Handing a single element value to a whole list fills every slot with that value—just as = 0 zeroes out every cell of grid above. The ++ operator splices two lists together as long as both lengths are known at compile time, even if their elements change while the program runs; the combined list has the sum of their lengths and a single shared element type:
const base = [1, 2, 3]
var extra: [2 of Number<u8>] = [7, 8]
joined <- base ++ extra
System.screen.border <- Color(joined[4] % 16)If a list's length falls outside 1 to 65,535, the compiler catches it with arraySize.
You can also write functions that accept a list of any length by using single-letter names for the length and element type in the parameter list, as Declarations shows:
fn keep(values: [n of T]): [n of T]
values
end
const numbers = keep([1, 2, 3])
const flags = keep([true, false])Turn to Expressions to see how to index a single element, slice a window of consecutive elements, or carve out a rectangular patch from a 2D grid.
Optionals
Adding a question mark to any type—T?—makes it optional: it holds either a present value of type T or the missing value nil. In memory, T? takes just one extra byte beyond T to track whether a value is there.
var held: Number<u8>? = nil
when System.keyboard.space.pressed
held = 7
end
when System.keyboard.delete.pressed
if n = held then
held = n -| 1
end
end
shown <- held ?? 0
System.screen.border <- if held != nil then Color(shown % 16) else .black endYou have three clean ways to work with an optional value:
value ?? fallbackunwraps the value when present, or hands backfallbackwhen it isnil;value != nilandvalue == niltest whether a value is currently there;if name = value then,guard name = valueandwhen name = eventunwrap the value intonamefor the body block whenever it is present.
An optional is never a boolean condition on its own, so writing if held then triggers bareOptional. Checking flares[i] != nil also proves to the compiler that flares[i] holds a real value inside the then branch (and on the right-hand side of and), allowing you to call mutating operations directly on flares[i] there. Likewise, flares[i] == nil proves the value is present inside the else branch, and guard flares[i] != nil proves it for the remainder of the handler.
Events
An event—like System.keyboard.space.pressed or System.screen.frame—represents a moment in time rather than a stored value in memory. Some events fire as a pure pulse (Event), while others carry a fresh payload each time they happen (Event of T), such as System.keyboard.stroke carrying a Keystroke.
press: Event <- System.joystick[2].fire.pressed
stroke: Event of Keystroke <- System.keyboard.stroke
var presses: Number<0...9> = 0
when press
presses = presses +% 1
end
System.screen.text[0][0..<1] <- presses.digits(1)Because an event happens at moments rather than sitting in RAM, you can give it a name with a <- binding, whereas trying to store an event in a constant or a var triggers eventConstant. The Events and Handlers chapter shows all the ways your program can listen, filter and respond to events.
Results
When background work talks to an external device like a disk drive, the operation finishes either with the data you asked for or with a report of what went wrong. Result of T is a built-in enum with two cases: success(value: T) and failure(reason: Failure). Failure is itself an enum with the cases not_present and device_error(code:), where code is a Number<u8>.
const reading: Result of Number<u8> = .success(value: 3)
System.screen.border <- match reading
success(value) -> Color(value)
failure -> .red
endExpressions
Whenever your program adds two scores, checks whether a sprite has reached the edge of the screen, or picks a fresh border color, it uses an expression. An expression is any formula or recipe that works out a value.
Before your program ever starts up, the compiler looks over every expression you write. It knows the exact type of every part, and for numbers it also tracks the lowest and highest values the calculation could possibly reach. If two parts do not belong together—say, trying to add a color to a number—the compiler catches the mix-up right away so you can fix it before running.
expression ::= unary ( binary-operator unary )*
unary ::= ( '-' | 'not' ) unary | postfix
postfix ::= primary selection*
selection ::= '.' name ( '(' arguments? ')' )?
| '[' expression ( ',' expression )? ']'
primary ::= literal | name | '.' name | name '(' arguments? ')'
| number-type '(' arguments ')' | 'start'
| '(' expression ')' | list-value | struct-value
| if-value | match-value
binary-operator ::= 'when' | 'or' | 'and'
| '==' | '!=' | '<' | '<=' | '>' | '>='
| '??' | '...' | '..<'
| '+' | '-' | '+%' | '-%' | '+|' | '-|' | '++'
| '*' | '/' | '%'Operators and Their Order
When you string several operators together in one expression, Adder needs to know which ones bind first—just like multiplication comes before addition in everyday math. The table below lists all the operators from the tightest grip at the top to the loosest at the bottom. Operators sitting on the same row have equal strength and work from left to right.
| Operators | Meaning |
|---|---|
. [...] (...) after a value | members, indexes and calls |
- before a value | negation |
* / % | multiplication, division and remainder |
+ - +% -% +| -| ++ | addition and subtraction, and joining lists |
... ..< | ranges |
?? | a fallback for nil |
== != < <= > >= | comparisons |
not | the opposite of a yes-or-no value |
and | both |
or | either |
when | an event filter, described in Events and Handlers |
Because of this order, -a * 2 means (-a) * 2, a + b * 2 means a + (b * 2), and not a == b means not (a == b). Whenever you want a different order—or just want to make your intent unmistakable at a glance—wrap that part in parentheses (...) so it runs first.
var a: Number<0...9> = 2
var b: Number<0...9> = 3
var paused: bool = false
result <- a + b * 2 // 8
grouped <- (a + b) * 2 // 10
moving <- not paused and a != b // (not paused) and (a != b)
System.screen.border <- if moving then Color(result) else Color(grouped % 16) endTwo small combinations ask you to spell out what you mean:
- Comparisons do not chain together in a row. Writing
a < b < craiseschainedComparison; spell out both halves asa < b and b < c. - A range (
...or..<) and the??fallback operator do not mix naked in the same expression. The compiler flags that withparenthesizeand asks you to add parentheses around whichever part should happen first.
Arithmetic
The five classic math operators—+, -, *, / and %—work on any two numbers. You can freely mix numbers of different storage formats, and if you combine a whole number with a fraction, the result becomes a fraction so nothing after the point is lost. If you accidentally hand an arithmetic operator something that is not a number, such as a Color or a bool, the compiler stops and lets you know.
Results and Their Ranges
One of Adder's friendliest tricks is that intermediate math inside an expression does not silently wrap around behind your back. Instead, the compiler works out the true range of each step from the ranges of the numbers going in. If a and b can each hold 0 through 9, then a + b spans 0 to 18, a - b spans -9 to 9, and a * b spans 0 to 81. It even notices when a number is multiplied by itself: a * a is a square, so its range never dips below zero! Adder then picks the smallest storage format that fits that full range, keeping the calculation itself safe from overflow.
var side: Number<1...20> = 1
area <- side * side // 1 to 400, in two bytes
when System.keyboard.space.pressed
side = side +% 1
end
System.screen.text[0][0..<3] <- area.digits(3)If an expression grows so wide that no storage format in the machine can hold its range—for instance, adding two full-range Number<u16> values in a binding—the compiler flags rangeNoRepresentation, points out the widest operand, and suggests giving it a tighter range.
The default number plays by a slightly different rule. Whenever either side of an operation is a plain, unannotated Number, the result is also a default Number spanning -32,768 to 32,767, and the program checks every step as it runs. Keep this in mind for formulas like (x + y) / 2: if x and y are default numbers holding 30,000, the addition itself steps past 32,767 and halts the program before / 2 gets a chance to bring the answer back down.
When an arithmetic expression is made entirely of constants, the compiler does the math at compile time and bakes in the exact answer.
Checked Arithmetic
What happens when a calculation finishes and you store the answer into a place with a specific range—a variable, a struct field, a function parameter, a list element, or a typed binding? There are three cases:
- Guaranteed to fit: When the compiler can prove from the ranges that the result always fits, it emits no runtime check at all—your code runs at full speed.
- Guaranteed to miss: When the compiler can see that the answer can never fit, it catches the mistake right away with
literalDoesNotFitfor a constant orrangeExceededfor a calculation. - Might or might not fit: When the expression's possible range is wider than the destination, Adder inserts a safety check each time the value is stored. If a number steps out of bounds while you play, the program halts cleanly and prints the offending line on the screen instead of corrupting your game state.
var lives: Number<0...3> = 3
when System.keyboard.space.pressed
lives = lives - 1 // the fourth press stops the program here
end
System.screen.text[0][0..<1] <- lives.digits(1)Even better, testing a number in an if condition narrows its known range inside the branch! Inside if lives > 0 then, the compiler knows lives is at least 1, so lives - 1 is guaranteed to stay within 0...2 and needs no runtime check at all. Any check that does stay in the program takes a handful of bytes and cycles, which you can inspect anytime in the cost report.
Wrapping and Clamping
In arcade games and demos, you often want a counter to loop smoothly back to the start or stop cleanly at a wall without writing an if statement. Adder gives you dedicated operators for both:
+%and-%wrap: step past one end of the range and you come right back in from the other end, like a color wheel or an asteroid drifting across the screen edge.+|and-|clamp: step past one end of the range and you stop right at the boundary, like a paddle hitting the wall or a volume knob turned all the way up.
Neither wrapping nor clamping ever halts your program.
| Operator | Result of 14 op 3 in Number<0...15> | Result of 1 op 3 in Number<0...15> |
|---|---|---|
+% | 1 | 4 |
-% | 11 | 14 |
+| | 15 | 4 |
-| | 11 | 0 |
Both pairs use the declared range of the destination receiving the value—whether that is a variable, a list element, a field, a parameter, or a typed binding. They follow your logical range rather than the underlying byte size, so a Number<0...9> wraps cleanly from 9 back to 0, not at 255. Because they need a target range to know where the ends are, using them in a place that declares no range—such as an untyped binding or inside a conversion like Color(...)—triggers operationNeedsRange.
var hue: Number<0...15> = 0
var level: Number<0.0...1.0> = 0
when System.screen.frame
hue = hue +% 1 // 15 wraps to 0
level = level +| 0.125 // stays at 1.0
end
System.screen.border <- Color(hue)Musical notes have a fixed keyboard span and do not wrap around from top to bottom, so using +% or -% on a Note raises noteWrap. You can still add or subtract a whole number from a Note (or clamp with +| and -|) to shift it by that many semitones, and subtracting one Note from another gives you the distance between them in semitones.
Division and Remainder
/ divides two numbers and drops any remainder toward zero: 7 / 2 gives 3, and -7 / 2 gives -3. % gives the remainder left over after division, keeping the sign of the left-hand number: 7 % 3 is 1, and -7 % 3 is -1.
Dividing by a constant zero is caught immediately with divisionByZero. If the divisor is a variable or expression that could become zero while the program runs, Adder checks it before dividing and halts on that line if zero ever shows up.
Fractions
A fractional number measures values in fine steps of 1/256. Adding and subtracting fractions is completely exact. Multiplying or dividing fractions works out the answer in steps of 1/256 and drops any leftover sub-step toward zero.
Conversions
Adder never turns one type into another behind your back. When you want to convert a number into a narrower range, turn a color into its palette number, or round a smooth fraction down to a pixel column, you write that step right in your code.
Number Conversions
Use any number type as if it were a function to convert a value into that type. Writing Number<u8>(v), Number<0...15>(v), or Number(v) gives you v expressed in that target type.
var wide: Number<0...300> = 0
var shade: Color = .red
byte <- Number<u8>(wide) // checked: stops above 255
nibble <- Number<0...15>(byte)
code <- Number<u8>(shade) // a color's number, 2 for red
System.screen.border <- Color(nibble)
System.screen.text[0][0..<3] <- code.digits(3)If the compiler cannot prove at compile time that v always fits inside the target range, the conversion checks it at runtime. If the compiler can see that v definitely lies outside the target range, it points that out immediately with conversionOutOfRange. And if you try to pass a fractional number directly into a whole-number type, it stops with conversionDropsFraction so you can choose how you want to round it.
Going the other way—placing a whole number into a fractional type—needs no conversion at all as long as its range fits. To turn a fraction into a whole number, pick one of the three rounding functions below.
The machine's built-in named sets convert from numbers using their own type names in the very same style, such as Color(n) or Char(65).
Dropping a Fraction
When a sprite's smooth fractional position needs to snap to a character cell or pixel coordinate, call floor(), round(), or ceil() to turn the fraction into a whole number:
| Function | Rounds | 2.5 gives | -2.5 gives |
|---|---|---|---|
floor() | down | 2 | -3 |
round() | to the nearest, halves up | 3 | -2 |
ceil() | up | 3 | -2 |
The compiler rounds the ends of the number's range the exact same way, so round() on a Number<-2.5...2.5> produces a whole number with range -2...3. Calling any of the three on a number that is already whole simply returns it unchanged.
var x: Number<0.0...39.75> = 0
column <- x.floor()
nearest <- x.round()
when System.screen.frame
x = x +% 0.25
end
System.screen.text[0][column] <- ScreenCode.block
System.screen.text[1][0..<2] <- nearest.digits(2)Other Number Functions
Every number also responds to a handy set of built-in helpers:
| Function | Result |
|---|---|
n.abs() | the size of n without its sign |
n.sqrt() | the square root of a whole number from 0 to 65,535, rounded down |
n.clamp(lo...hi) | n held within lo...hi (or lo..<hi), with range Number<lo...hi> |
n.wrap(lo...hi) | n wrapped around lo...hi (or lo..<hi), with range Number<lo...hi> |
n.digits(k) | k characters of n in decimal, with leading zeros, a [k of Char] |
Number.min(a, b) and Number.max(a, b) | the smaller and the larger of two numbers |
n.clamp(lo...hi) and n.wrap(lo...hi) bound a whole number right inside an expression and narrow its type to that exact range, so (x + dx).clamp(24...320) or (frame + offset).wrap(0..<8) flows straight into a sprite coordinate or list index without a separate variable. n.digits(k) is the classic way to print a live score or timer onto the screen: it turns n into a [k of Char] padded with leading zeros (like "0042" for 4 digits), checking at runtime that n is between 0 and the largest k-digit number. If you want to call a dot-function directly on a number literal, wrap the number in parentheses first—write (15).sqrt() rather than 15.sqrt(), which the compiler flags with numberMember because the dot looks like a decimal point.
Bit Operations
Under the hood, a Number<u8> is a neat row of 8 switches (bits 0 through 7, counting from the right) and a Number<u16> is a row of 16 (0 through 15). Packing flags, dungeon doors, or bitmasks into a single byte is a beloved 8-bit craft, and Adder gives you clear, readable functions to inspect and flip those bits. Every bit function returns a fresh value and leaves the original number untouched.
| Function | Result |
|---|---|
v.bit_is_set(p) | true when bit p is on |
v.set_bit(p) | v with bit p on |
v.clear_bit(p) | v with bit p off |
v.toggle_bit(p) | v with bit p flipped |
v.bitwise_and(mask) | the bits on in both |
v.bitwise_or(mask) | the bits on in either |
v.bitwise_xor(mask) | the bits on in exactly one |
v.bitwise_not() | every bit of the width flipped |
v.shift_left(n) | the bits moved n places left, filling with zeros |
v.shift_right(n) | the bits moved n places right, filling with zeros |
A bit position or shift count runs from 0 to 7 on a Number<u8>, and from 0 to 15 on a Number<u16>. Better yet, you can pass a case from a fieldless enum as a bit position—Adder uses its position in the enum's declaration (0, 1, 2, ...), giving your bit flags self-documenting names! Bits shifted past either end of the number simply drop off, so Number<u8>(0x81).shift_left(1) becomes 2. Any mask you pass to bitwise_and, bitwise_or, or bitwise_xor must fit within the receiver's 8-bit or 16-bit width.
enum Path
case north, east, south, west
end
var open_paths: Number<u8> = 0b0000_0101
north_open <- open_paths.bit_is_set(.north)
either <- open_paths.bitwise_and(0x05) != 0
when System.keyboard.e.pressed
open_paths = open_paths.toggle_bit(.east)
end
System.screen.border <- if north_open and either then .green else .red endBit operations only work on exact 8-bit or 16-bit unsigned values (Number<u8> or Number<u16>). Calling them on any other number triggers bitwiseReceiver—just convert the number to Number<u8> or Number<u16> first. If a constant bit position, shift count, or mask steps outside the width of the number, the compiler catches it with bitwiseArgument; if the position or count changes while the program runs, Adder checks it on the spot.
Comparisons and Yes-or-No Values
Comparisons
The six comparison operators—==, !=, <, <=, > and >=—compare two values and hand back a bool (true or false). You can compare any two values that share the same kind:
- any two numbers, regardless of their ranges or storage formats;
- two values from the same named set, such as two
Colorvalues or twoCharvalues; - two cases from the same
enum.
The ordering operators (<, <=, > and >=) compare numbers by their value, named sets by their underlying machine numbers, and fieldless enums by the order their cases appear in the enum declaration. Two bool values can be tested for equality with == and !=. Structs cannot be compared directly; trying to compare structs or mismatched types triggers cannotCombine.
What if you compare two lists, or a list against a single value? Adder compares them element by element and hands you back a list of bool values, as Operations Over Lists explains.
and, or and not
and, or and not combine and flip bool values. Both and and or are short-circuiting, meaning they stop as soon as the outcome is settled: in a and b, if a is already false, Adder skips working out b. These operators want real yes-or-no values, not numbers: handing a number to not triggers notNeedsBool, and handing a number to and or or triggers needsBooleans.
You can also use and and or to combine events, as shown in Events and Handlers.
Optionals and ??
Because an optional (T?) might hold nil, you can compare it with == and != against nil or against another optional of the same type. Comparing an optional directly to a plain, non-optional value raises optionalCompare.
When you want to unwrap an optional and supply a default whenever it is empty, reach for the ?? fallback operator: value ?? fallback gives you the value inside the optional when one is present, or fallback when the optional is nil. Because fallback is a plain value of the inner type, the result of value ?? fallback is guaranteed never to be nil. Using ?? on a value that is not optional in the first place triggers coalesceNeedsOptional.
var held: Number<u8>? = nil
var other: Number<u8>? = 5
same <- held == other
present <- held != nil
shown <- held ?? 0
when System.keyboard.space.pressed
held = 7
end
System.screen.border <- if same or present then Color(shown % 16) else .black endAn optional is not itself a bool, so writing if held then triggers bareOptional—write if held != nil then (or unwrap it with if v = held then in a statement) instead.
Lists
List Values
Write a list by placing its elements inside square brackets, separated by commas: [2, 3, 5, 7]. For a list of numbers, the element type spans from the smallest number in the list to the largest; for other values, the list takes the type of its first element. When you store the list into a place that already declares a list type, every element takes that declared element type instead. Every list in Adder has a fixed, non-zero size: an empty [] is flagged with emptyArray, and a list whose length does not match its destination is flagged with wrongLength.
list-value ::= '[' ( expression ( ',' expression )* ','? )? ']'
struct-value ::= '(' name ':' expression ( ',' name ':' expression )* ','? ')'Two handy shorthands also build lists for you:
- A range used as a value, such as
0..<4, expands into the list of numbers it spans:[0, 1, 2, 3]. - A string literal like
"HI"is a list ofChar.
A struct value, written in parentheses with labeled fields like (x: 10, y: 20), is described in Structs.
const primes = [2, 3, 5, 7] // four numbers, each from 2 to 7
const tune: [3 of Note] = [.c4, .e4, .g4]
steps <- 0..<4
System.screen.border <- Color(primes[1] + steps[2])Indexes
Write xs[i] to pick out element i of xs, counting from 0 at the front. The index must be a whole number: passing a fraction triggers indexNotInteger, and passing a constant index outside the list's bounds triggers indexOutOfBounds. When i changes at runtime and its range could step outside the list, Adder checks i before touching memory and stops cleanly on that line if it goes out of bounds.
For quick access to the ends of a list, xs.first and xs.last give you the first and last elements without counting. A two-dimensional grid is simply a list of row lists, so grid[row][col] picks out one cell from one row. And if you have a list of structs, asking for a field gives you a list of that field across every element: corners.x gives you the list of every corner's x coordinate.
const grid = [[1, 2, 3], [4, 5, 6]]
var row: Number<0...1> = 0
var col: Number<0...5> = 0
corner <- grid[row][2]
when System.keyboard.space.pressed
row = row +% 1
col = col +% 1
end
System.screen.border <- Color(corner + grid.last.first)
System.screen.background <- Color([1, 2, 3, 4][col % 4])Windows
Instead of a single index, you can put a range inside the brackets to look through a window of consecutive places in a list—perfect for scrolling a banner across the screen or updating a group of columns at once! For example, xs[2..<6] is a window of four elements starting at index 2.
Every window has a fixed length known at compile time, even when its starting index moves from frame to frame. When the start is a constant, the end is a constant too (xs[2..<6]); when the start is a changing expression a, write the end as a plus a constant offset, such as xs[a..<(a + 4)] or xs[a...(a + 3)]. If the compiler cannot tell the window's fixed length, it flags sliceLengthUnknown; if a constant window reaches past the end of the list, it flags sliceOutOfBounds. When a sliding window's start could step past the list at runtime, Adder checks the start index once when the window is formed.
const sign = ScreenCode(" ADDER SCROLLS A LONG SIGN ACROSS THE TOP ROW ")
left: Number<0...20> <- System.screen.frame.every(8).cycle()
System.screen.text[0] <- sign[left..<(left + 40)]A window is a live view, not a copy: reading from a window reads the underlying list in place, and passing a window to a function lets the function read those elements right where they sit. Even better, if you bind a name to a window of a variable list (window <- items[i..<(i + 4)]), that binding takes zero bytes of storage and points straight at those slots—so a handler that writes to window writes directly into items! (Trying to write through a window of a constant list is caught with sliceBindingWrite.) If you initialize a var from a window, that variable makes its own separate copy of the elements. See Statements for more on writing into windows.
Rectangles
On a two-dimensional list of rows, you can slice both directions at once by placing two ranges inside the brackets, separated by a comma—rows first, then columns. grid[r..<(r + 2), 1..<3] carves out a 2-by-2 rectangle: a list of two rows, where each row is a two-element window. Both ranges follow the same fixed-length window rules above. Because a rectangle index always slices both dimensions, both parts inside [..., ...] must be ranges; if either one is a single index, the compiler flags rectangleRanges—to take a window of a single row, index the row first and then slice it, as in grid[r][1..<3].
const grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
var top: Number<0...1> = 0
corner <- grid[top..<(top + 2), 1..<3] // two rows of two numbers
when System.keyboard.space.pressed
top = top +% 1
end
System.screen.border <- Color(corner[1][1])Joining Lists
The ++ operator joins two lists end to end: a ++ b contains all the elements of a followed immediately by all the elements of b. Both lists must share the same element type (or both be lists of numbers), and their combined length can be up to 65,535 elements. To keep memory layout simple and predictable, each side of ++ must be a list literal, a named list, a struct member, or an indexed element; if you try to join the result of a function call directly, the compiler flags joinLists—just bind that call to a name on the line above first.
const low = [1, 2, 3]
var high: [2 of Number<u8>] = [7, 8]
all_of <- low ++ high // [5 of Number<u8>]
System.screen.border <- Color(all_of[4] % 16)Value if and match
In Adder, if and match are not just for running statements in handlers—they can also be used right inside an expression to pick a value! Only the branch or arm that wins is worked out; the others are skipped.
if-value ::= 'if' expression 'then' value-body
( 'else' 'if' expression 'then' value-body )* 'else' value-body 'end'
value-body ::= expression | newline ( name '=' expression newline )* expression newline
match-value ::= 'match' expression newline value-arm+ 'end'
| 'match' newline condition-arm+ 'else' '->' expression newline 'end'
value-arm ::= pattern ( 'when' expression )? '->' expression newline
| 'else' '->' expression newline
condition-arm ::= 'when' expression '->' expression newline
pattern ::= name | '-'? whole-literal | 'true' | 'false' | 'nil'
| name '(' field-pattern ( ',' field-pattern )* ')'
field-pattern ::= name ( ':' name )?if
Because an expression must always produce a value, a value if always needs an else branch; leaving else off triggers valueIfElse. The condition after if can be any bool expression, or an event (which counts as true during any frame in which that event fires). You can chain additional tests with else if on the same line, closing the whole chain with a single end.
Each branch can be a compact one-liner or span several lines: when a branch spans multiple lines, you can define local helper constants with const on the opening lines, and the final line of the branch becomes its resulting value. Inside the then branch, Adder knows the condition is true and narrows your number ranges accordingly—so in if a > 3 then a - 4 else 0 end, the compiler knows a is at least 4 in the first branch and proves that a - 4 never drops below 0.
All branches of a value if must agree on the type of value they return (mixing types raises wrongType). When the branches return numbers, the overall range of the if expression spans from the lowest minimum of any branch to the highest maximum of any branch.
var a: Number<0...9> = 0
lift <- if a > 3 then a - 4 else 0 end // 0 to 5
shade <- if a == 0 then Color.red else if a < 5 then .yellow else .green end
scaled <- if a > 5 then
const over = a - 5
over * 3
else
0
end
when System.keyboard.space.pressed
a = a +% 1
end
System.screen.border <- shade
System.screen.text[0][0..<2] <- (lift + scaled).digits(2)match on a Value
When you are choosing between several cases—like game states, directions, or specific numbers—match value inspects the value from top to bottom, picks the first arm whose pattern matches, and returns the expression after ->.
| Value | Patterns |
|---|---|
| an enum | a case name without a dot, such as title; a case with fields names them, as in moving(grab, column: chosen) |
| a named set | a value's name without a dot, such as red |
| a number | a whole literal, such as 0 or -3 |
a bool | true and false |
| an optional | nil, and the patterns of its inner type |
| any value | a name, which matches any value and names it inside the arm |
You can refine any pattern by adding when condition before the ->; that arm then matches only when both the pattern and the condition hold, and the compiler narrows the range of any matched name by that condition! An else -> arm at the bottom catches everything that earlier arms did not take.
Adder makes sure your match is watertight:
- Every possible value of the input must be handled by some arm; if any case or number is left out, the compiler lets you know with
missingCoverage. - If an earlier arm without a
whenguard already catches a value, a later arm for the same value could never be reached, so the compiler flags it withduplicateArm. - Case patterns in a
matcharm are written cleanly without a leading dot (title ->, not.title ->); writing a dot triggersdottedArm.
enum Mode
case title, playing, over
end
var mode: Mode = .title
var lives: Number<0...3> = 3
System.screen.border <- match mode
title -> .blue
playing -> .black
over -> .red
end
System.screen.background <- match lives
0 -> .red
n when n < 3 -> Color(n + 7)
else -> .black
end
when System.joystick[2].fire.pressed
mode = mode.next()
lives = lives -| 1
endmatch Without a Value
You can also write match with nothing after the keyword to build a clean table of conditions—often much nicer to read than a long ladder of if ... else if ...! Each arm starts with when followed by a bool expression or an event. Adder checks the arms from top to bottom, picks the first one that holds, and skips working out the rest. A condition-table match must have at least one when arm and finish with an else -> fallback; leaving either out triggers conditionalMatch.
var lives: Number<0...3> = 3
System.screen.border <- match
when lives == 0 -> Color.gray
when System.joystick[2].fire -> Color.red
else -> Color.black
endCalls and Members
arguments ::= argument ( ',' argument )* ','?
argument ::= ( name ':' )? expression | name '->' expressionCalls
To call a function or an op, write its name followed by its arguments in parentheses: apart(ball, bat). Adder always works out all of the arguments from left to right before running the function. You can pass arguments by position, or attach the parameter's name as a label—like apart(ball, b: bat)—and labeled arguments can appear in whatever order reads best. Each argument is checked against the parameter's declared type and range just as if you were storing it into a variable.
If a call does not line up with the declaration, the compiler tells you right away:
badArgumentwhen a label does not match any parameter name, when the same parameter is supplied twice, or when too many arguments are passed;missingArgumentwhen a required parameter is left out;unknownFunctionwhen the function has not been declared above the call.
fn apart(a: Number<0...296>, b: Number<0...296>): Number<0...296>
if a > b then a - b else b - a end
end
var ball: Number<0...296> = 40
var bat: Number<0...296> = 100
gap <- apart(ball, b: bat)
System.screen.border <- if gap < 16 then .green else .black endDeclarations shows how to write your own functions and ops.
Members
A dot . after a value reaches for one of its members. Plain struct fields and properties need no parentheses (position.x, xs.first), while member functions that compute something take parentheses (score.digits(3)). When you declare a member function like fn Type.name(value: Type, ...), calling item.name(...) passes item (the value before the dot) into that first parameter automatically.
When the compiler already knows what type a place expects—for instance, assigning to System.screen.border or comparing against a Color—you can start with a bare dot like .red instead of spelling out Color.red. If you write .name in a spot where no specific type is expected yet, the compiler asks you to name the type with memberNeedsType. When a name before the dot begins with a capital letter, it refers to a type itself, giving you access to its cases (Color.red), its range bounds (Destination.first and Destination.last), and its type-level makers like Color.random() and Number.random(0...15).
struct Position(x: Number<u8>, y: Number<u8>)
fn Position.is_left(value: Position): bool
value.x < 20
end
var position: Position = (x: 10, y: 12)
left <- position.is_left()
System.screen.border <- if left then .white else Color.black endAsking a struct for a field it does not have raises unknownField, and asking a type or value for a member that does not exist raises noMember or unknownName.
Operations Over Lists
Lifted Operations
In Adder, you rarely need to write a loop just to do math on a list of numbers. Hand a list to an arithmetic or comparison operator and Adder automatically applies the operation to every element in turn, giving you back a fresh list of results! You can combine a list with a single value (which pairs with every element in the list) or combine two lists of the same length (which pair up slot by slot). Trying to combine two lists of different lengths raises arrayShapes.
Your own functions and ops get this superpower automatically too: if a function or op expects a single value for its parameter and you hand it a list instead, it runs across every element of the list—and for an op, each slot in the list even keeps its own independent memory from frame to frame! (If a parameter is explicitly declared to take a list, the function receives the whole list at once instead.)
const xs = [1, 2, 3]
const ys = [4, 5, 6]
fn half(x: Number<0...9>): Number<0...4>
x / 2
end
sums <- xs + ys // [5, 7, 9]
scaled <- xs * 2 // [2, 4, 6]
big <- ys > 4 // [false, true, true]
halves <- half(ys) // [2, 2, 3]
System.screen.border <- Color(sums[0] + scaled[1] + halves[2])
System.screen.background <- if big[2] then .green else .black endReductions
When you want to boil an entire list down to a single summary value—like adding up column heights or checking whether any alien has reached the bottom row—use a reduction:
| Reduction | On | Result |
|---|---|---|
xs.sum() | numbers | the total of every element |
xs.min() and xs.max() | numbers | the smallest and the largest element |
xs.count() | bool values | how many are true |
xs.any() and xs.all() | bool values | whether any, or every one, is true |
xs.first_index() and xs.last_index() | bool values | the place of the first or last true, or nil when none is |
List reductions work purely on the current contents of the list—they keep no memory between frames—and their cycle cost scales with the number of elements in the list.
var drops: [6 of Number<0...999>] = [60, 200, 120, 300, 90, 250]
deep <- (drops > 160).count()
all_drops <- drops.sum()
first_deep <- (drops > 160).first_index()
when System.keyboard.space.pressed
drops[0] = 400
end
System.screen.border <- if (drops > 350).any() then .red else Color(deep) end
System.screen.text[0][0..<4] <- all_drops.digits(4)
System.screen.text[1][0..<1] <- (first_deep ?? 0).digits(1)map and filter
For custom element-by-element transformations, map and filter let you write a short arrow expression right inside the call:
xs.map(n -> expression)works outexpressionfor each elementnofxsand gives back the list of results.xs.filter(n -> condition)tests each elementn; whereconditionistrueit keeps the element, and whereconditionisfalseit leavesnilin that slot, returning a list of optionals ([k of T?]).
Both keep the list's length intact and automatically skip over any slot that is already nil. Mapping over an optional list gives back an optional list, and if your map arrow itself produces an optional, Adder keeps it flat (T?) rather than wrapping an optional inside an optional. When you chain calls like xs.filter(...).map(...), each step finishes working out all of its elements in order before the next step in the chain begins.
const values: [3 of Number<u8>?] = [2, nil, 4]
kept <- values.filter(n -> n >= 3).map(n -> n - 1) // [nil, nil, 3]
System.screen.border <- Color(kept[2] ?? 0)A few guardrails keep arrows fast and predictable:
- The element name before
->is read-only and must be a fresh name not already in scope (reusing an outer name triggersshadowing). - The expression after
->can read surrounding values, but it cannot hold frame-to-frame memory or side effects of its own—such as drawing arandomnumber, testing an event, reading.previous(), or calling anop. The compiler flags any of those—or afilterarrow whose expression is not abool—withcollectionArrow. If you want to combine list elements with a random roll or an op value, bind that roll or op on its own line first and read its name inside the arrow.
map, filter, and sum(from:) also work on value-carrying events, as Events and Handlers describes.
Random Numbers
Games thrive on a little surprise! Number.random(range) draws a whole number from a range whose start and end are known at compile time (passed directly without an argument label), and Color.random() picks one of the sixteen colors.
roll <- Number.random(1...6)
const low = 2
sample <- Number.random(low..<(low + 8))
var shade: Color = .black
when System.keyboard.space.pressed
shade = Color.random()
end
System.screen.background <- shade
System.screen.border <- Color(roll + sample)Every random call site in your program has its own independent number generator under the hood, so adding a random starfield in one part of your code will never shift the dice rolls in another part. A call draws a fresh number only in the frames where its line actually runs—so a random call inside a when handler advances only when that handler fires. If you want one random roll to be shared by several places in the same frame, put the random call in a binding (roll <- Number.random(1...6)) and read roll wherever you need it. Restarting the program replays the exact same sequences from the beginning, making tricky bugs easy to reproduce.
Even better, Adder draws from ranges of any size—not just powers of two—without ever needing a slow division instruction! Because the generator is tailored to the range at compile time, a range with changing endpoints is flagged with rangeEndNotFixed, and passing anything other than a range to Number.random triggers randomNeedsRange. Finally, because a plain fn promises to return the exact same answer every time you give it the same arguments, calling random inside a fn is caught with randomImpure.
Declarations
Every game or demo you build is made of things you name: a ship's horizontal position, the number of lives left, a melody table, a helper formula for checking collisions, or the live rule that paints the screen border. In Adder, a declaration introduces a name and makes a clear promise about when—and how—its value can change:
- A constant declaration, written as
const name = expression, fixes a value at compile time. Inside a handler or its loops,=instead assigns to existingvarstate; it does not introduce a constant. - A variable, written with
var, holds state from frame to frame and changes only when an event handler assigns it a new value. - A binding, written with
<-, is a live wire: it always equals its expression, updating automatically as the values it reads change. - Functions (
fn), ops (op), and work (work) give names to calculations, while types (type,enum,struct) give names to kinds of values. atplaces a value at a specific spot in memory, andincludebrings another source file into your project.
declaration ::= constant | variable | binding | placed-value | output-declaration
| function | op-declaration | work-declaration
| type-alias | enum-declaration | struct-declaration | include | declaration-match | declaration-ifDeclarations stand at the top level of your program, one per line, or inside a case as Events and Handlers explains. You can arrange your declarations and handlers in whatever order tells the story of your program best, with one simple rule: declare a name above the first line that reads it.
Names and Scope
Every declared name follows the spelling rules in Names. Type names always start with a capital letter (Ship, Score), while constants, variables, bindings, functions, and ops always start with a small letter or an underscore (lives, ship_x). Starting a non-type declaration with a capital letter triggers capitalizedDeclaration.
Once you declare a name, it is in scope from that line all the way to the bottom of the program. Any line that reads a name or calls a function must appear after its declaration; reading a name too early raises unknownName (or unknownFunction for a call). There is just one special exception: a binding can look back at the previous frame's value of a binding written further down the page, as A Binding That Reads Itself shows.
Each name in your program—across all included files—is declared only once. Declaring the same top-level name a second time raises duplicateDeclaration, and reusing a top-level name for a parameter or local variable inside a block raises shadowing so you never lose track of which value you are looking at.
Built-in type names, System, and scratch are reserved for the machine and cannot be redeclared (builtinName). Built-in member names like digits and sum, however, are completely free for you to use as variable or function names of your own.
The compiler checks every single declaration in your file, even if nothing calls or reads it yet. An unused function or struct member is checked for undeclared names, missing types, or misplaced statements (like a wait outside a handler) just as thoroughly as one you call every frame. Generic functions with type parameters are checked up front for their names, types, and statement placement, and then checked for exact number ranges at each call site. And when it is time to emit your program, any declaration that nothing uses is stripped away—it costs zero bytes in your final binary!
Constants
Write const name = expression to declare a constant whose value is locked in when the program compiles. For number constants, the compiler bakes the number straight into the machine instructions that use it, so the constant does not even take up a byte of storage in memory.
constant ::= 'const' name ( ':' type )? '=' expressionA constant's expression can combine literals, earlier constants, pure functions, and type makers like Color.red, ScreenCode("ADDER"), or Shape([...]). You can also put a constant list, struct, or picture in a constant, or give a short nickname to a branch of the hardware tree—like screen = System.screen—and use screen as a stand-in for that path.
const columns = 40
const half = columns / 2
const table = [3, 1, 4]
const screen = System.screen
var lane: Number<0...39> = half
screen.border <- Color(table[1] + lane % 8)When you leave off the : type annotation, the constant takes the exact value and tightest range of its expression. When you write a : type annotation, the compiler verifies at compile time that the constant fits inside that type, flagging literalDoesNotFit if it steps outside.
Because a constant is worked out before the program ever starts, it cannot read anything that changes while the program runs—no variables, bindings, live device inputs, or ops (constantChanges). And once declared, a constant stays constant forever: trying to assign a new value to one in a handler raises notAssignable. Constants cannot hold events either, as Events explains.
Variables
Write var name = expression to declare a variable—a slot in memory that remembers its value from one frame to the next, like the player's score, remaining lives, or current game mode. The expression after = sets the variable's starting value when the program boots up, and from then on only an event handler can change it (either directly with = or by calling a mut fn on a struct stored in the variable).
variable ::= 'var' name ( ':' type )? '=' expressionEvery var declaration must include a starting value (= expression). Because the program works out that starting value as it starts up, the starting expression can read constants, earlier variables, bindings, functions, and even live hardware inputs like the joystick!
enum Mode
case title, playing
end
var lives: Number<0...3> = 3
var mode = Mode.title
var start_x: Number<-1...1> = System.joystick[2].dx
when System.joystick[2].fire.pressed
lives = lives -| 1
end
System.screen.border <- Color(lives + start_x + 1)If you omit the : type annotation on a var, it takes the type of its starting expression—and for a number, it widens to The Default Number (-32,768...32,767) unless you give it a range like Number<0...3>. If the starting value is a bare dot-name like .red, add a type annotation (var shade: Color = .red) or write Color.red so the compiler knows which set the case belongs to (memberNeedsType).
Each top-level variable occupies just the bytes needed by its type. Handlers and functions can also declare temporary local var values that live only while that block runs, as Statements describes.
Bindings
Write name <- expression to declare a binding. Think of <- as a live connection: instead of holding an old value until something overwrites it, a binding always equals its expression. Whenever any variable or input that the binding reads changes, the binding reflects the new answer automatically. Because a binding is defined entirely by its formula, a handler can never assign to it (notAssignable).
binding ::= ( 'fast' | 'small' )? name ( ':' type )? '<-' expression
| ( 'fast' | 'small' )? place '<-' expressionvar lives: Number<0...3> = 3
warning <- lives == 1
spare: Number<0...2> <- lives -| 1
System.screen.border <- if warning then .red else .black end
System.screen.text[0][0..<1] <- spare.digits(1)
when System.keyboard.space.pressed
lives = lives -| 1
endWithout a : type annotation, a binding takes the exact type and range that the compiler proves for its expression. With a : type annotation, the binding takes that declared type (and checks the value at runtime if the compiler cannot prove the expression already fits). A binding can also give a name to an event—for instance, jump <- System.joystick[2].fire.pressed—so that when jump handlers can listen to it; see Events.
Putting the keyword fast in front of a binding asks the compiler to spend extra bytes of memory or lookup tables to save CPU cycles on that line, as Costs and the Frame explains.
Outputs
When the left side of <- is a hardware or memory place rather than a fresh variable name, the binding is an output: System.screen.border <- expression keeps the screen's border color wired to that expression every frame. An output target can be any writable spot in the hardware tree—the border, the background, a row or cell of screen text, a sprite, a voice—or a memory location declared with at. The System Device Tree tours all the built-in output places and the types they accept.
To keep your hardware state crystal clear, each output place is driven by a single binding in your program (or within a case). Wiring two bindings to the same output place triggers overlap.
Handlers never write directly to outputs (notAssignable). Whenever you want handlers to control what appears on an output, have the handlers update a var and wire the output to that var with <-.
A Binding That Reads Itself
Within a single frame, a binding has no value until its expression finishes running—so a binding cannot read its own current-frame value, nor can two bindings read each other in a circle (bindingLoop).
What a binding can do is read what its value was in the previous frame using .previous(from: initial). The from: argument supplies the starting value for the very first frame before there is a previous frame to look back at:
tick: Number<0...99> <- tick.previous(from: 0) +% 1
System.screen.border <- Color(tick % 16).previous(from: ...) can even read a binding that is declared further down the page, letting two bindings feed into each other across frames! When a binding reads a later binding through previous, that later binding must write out its : type annotation so the compiler knows its size up front (forwardNeedsType).
Order in a Frame
Every television frame follows a clean, predictable rhythm:
- First, the program samples the hardware inputs (keyboard, joysticks, paddles) and works out which events fired this frame.
- Next, the
whenhandlers whose events fired run from top to bottom in the order they are written. - Finally, once all handlers have finished, every output binding updates the screen and sound hardware with the final value of its expression.
Inside a handler, any assignment to a var takes effect right away on the very next line—and if a handler reads a binding, that binding gives the up-to-the-instant value based on the current variables!
var n: Number<0...4> = 1
doubled <- n * 2
var seen: Number<0...8> = 0
when System.screen.frame
n = 3
seen = doubled
end
System.screen.border <- Color(seen)In this example, seen becomes 6 on the very first frame: the handler sets n to 3, and reading doubled on the next line uses that new 3 right away.
What about a binding that remembers history across frames—one that uses .previous(), an op, or a stateful member like .sum() or .count()? Those history-keeping bindings advance once per frame, so reading the same stateful binding in several places during a single frame always gives you the exact same snapshot. Events and Handlers walks through the frame cycle in complete detail.
Output Declarations
If you find yourself wiring the same group of hardware outputs together—say, setting both the border and background colors from one mood color, or configuring a sprite pair—you can bundle them into a reusable output declaration:
output-declaration ::= 'output' name '(' name ':' type ')' newline output-line* 'end'output frame_color(c: Color)
System.screen.border <- c
System.screen.background <- c
end
var hue: Number<0...15> = 0
frame_color <- Color(hue)
when System.keyboard.space.pressed
hue = hue +% 1
endWhen you wire frame_color <- Color(hue), the parameter c receives Color(hue) and drives every output binding inside the block. An output block may contain output bindings (including bindings to other declared outputs) and asm blocks; putting any other kind of line inside triggers outputBody, and using <- on a name that is not an output triggers outputTarget. Just like a built-in hardware output, your custom output takes a single <- binding and cannot be assigned with = inside a handler.
Functions
A function (fn) is a named, self-contained calculation. Give it the same argument values and it will give you back the exact same answer every time, with no hidden state or surprises.
function ::= ( 'fast' | 'small' )? 'fn' function-name '(' parameters? ')' ( ':' type )? newline
statement* expression newline 'end'
function-name ::= name | owner '.' name
owner ::= name | 'Number'
parameters ::= parameter ( ',' parameter )*
parameter ::= name ( ':' type )?fn apart(a: Number<0...296>, b: Number<0...296>): Number<u16>
if a > b then a - b else b - a end
end
bat: Number<0...296> <- (System.joystick[2].dx * 4).sum(from: 136)
ball: Number<0...296> <- System.screen.frame.cycle()
close <- apart(ball, b: bat) < 16
System.screen.border <- if close then .green else .black endParameters and Results
The head of a fn (or an op) declares its parameters inside the parentheses, and each call to it supplies the argument values for that run. Ordinary parameters are read-only input names written without var: each call hands them their value, they are visible only inside the body, and they never hold writable storage of their own.
In a fn or op head, each parameter can declare an explicit : type to check arguments against, or omit : type and infer its type and numeric range from the argument passed at each call site. Likewise, a fn head can declare : type after the closing ) to check the returned value against, or omit : type to infer the result type from the body's final expression (while mut fn methods, which perform actions without returning a value, still omit : type and work declarations still require : type on every parameter and result with parameterNeedsType / resultTypeNeeded). Each parameter in a signature needs a distinct name (duplicateParameter).
Inside the body, the last line is the expression that produces the function's return value. Before that final line, you can write statements to help work out the answer: local constants with const, local variables with var, if and match statements that update those local variables, and bounded for loops, as Statements describes. Because parameters and local const values are read-only inputs for the duration of the call, trying to assign a new value to either one triggers localImmutable—if you want a working value you can step or update inside the function, copy it into a local var first.
fn middle(low: Number<u8>, high: Number<u8>): Number<u8>
const sum_of = low + high
sum_of / 2
end
fn add_up(values: [4 of Number<u8>]): Number<u16>
var t: Number<u16> = 0
for v in values
t = t + v
end
t
end
mid <- middle(3, 9)
all_of <- add_up([1, 2, 3, 4])
System.screen.border <- Color(mid % 16)
System.screen.background <- Color(all_of % 16)Keep the shape of a function body in mind:
- Every line before the last line must be a statement that does work (like declaring or updating a local variable). Leaving a bare expression on an earlier line where its result is thrown away triggers
resultUnused. - The last line must be an expression that produces the return value; ending a function with a statement triggers
valueBlockEmpty. - When the last line of the body is an
ifor amatch, each branch or arm follows the same pattern: any lines before the branch's final line run as statements first, and the final line of each branch supplies the return value!
enum Size
case little, medium, large
end
fn bonus_for(size: Size): Number<u8>
var points: Number<u8> = 0
match size
little -> points = 1
medium -> points = 3
large -> points = 6
end
if size == .large then points = points + 2 end
match size
little -> points
medium ->
points = points + 1
points
large -> points
end
end
var size: Size = .large
System.screen.border <- Color(bonus_for(size))At the call site, you can pass arguments in parameter order (apart(ball, bat)) or attach a parameter's name as an optional label (apart(ball, b: bat)). Supplying an unknown label, repeating a parameter, or passing too many arguments raises badArgument, and leaving out a required parameter raises missingArgument. An argument whose constant value or range cannot fit the parameter's declared type is caught at compile time with literalDoesNotFit; otherwise, Adder checks changing arguments (and the function's return value) at runtime whenever the compiler cannot prove they already fit.
What a Function May Read
To guarantee that a function always returns the same output for the same inputs, a fn can only read:
- its own parameters and local names,
- top-level constants,
- other functions, and
- (when written inside a
structblock) the fields of its own struct.
This rule applies across all functions: free fn declarations as well as a struct's fn, mut fn, and maker functions. A function cannot reach out to read a top-level var, a binding, or a hardware device directly; trying to do so triggers operationScope.
Whenever a function needs a changing value from the outside—like a sprite's coordinate or a joystick direction—simply add a parameter for it and pass the value in at the call site! That small habit makes your functions much more flexible too. Consider a Ship struct with a mut fn steer:
struct Ship(x: Number<0...39>)
mut fn steer(dx: Number<-1...1>)
x = x +| dx
end
end
var ship = Ship(20)
when System.screen.frame
ship.steer(System.joystick[2].dx)
endIf steer were wired directly to System.joystick[2].dx inside the struct, every Ship in your game would be stuck on port 2. By taking dx as a parameter, player one's ship can steer with port 2 while player two's ship steers with port 1—or even with an AI routine!
Along the same lines, a function cannot modify a top-level variable (functionSets), roll a random number (randomImpure), use .previous() or any other frame-memory operation (memoryOutsideBinding), pause with wait (waitOutsideHandler), or call itself (recursion).
You can call a function from any binding, handler, op, or later function declared below it.
Member Functions
When you write a function's name as Type.name, it becomes a member function of Type. Its first parameter receives the value written before the dot when you call value.name(...):
struct Position(x: Number<u8>, y: Number<u8>)
fn Position.is_left(value: Position): bool
value.x < 20
end
var position: Position = (x: 10, y: 12)
left <- position.is_left()
System.screen.border <- if left then .white else .black endYou can attach member functions to any type you declare (struct, enum, or type alias), as well as to Number, bool, Event, or even a generic type parameter like T. The first parameter's type must match the type before the dot in the function's name (wrongType), and every field, function, and op on a given type must have a distinct name (memberOverlap).
For structs, you can also write member functions directly inside the struct ... end block, as Struct Blocks shows.
Type Parameters
Want to write a helper function that works on a list of any element type or any length? Use a type parameter or a length parameter right in the function's signature:
- A single capital letter, like
T, stands for any type supplied by the call. - A single small letter inside a list type's length slot, like
nin[n of T], stands for the length of the list passed at the call site—and inside the function body, you can readnas a number!
If the same letter appears more than once in one signature (like fn keep(values: [n of T]): [n of T]), every occurrence stands for the exact same type or length.
fn keep(values: [n of T]): [n of T]
values
end
fn length_of(values: [n of T]): Number<u16>
n
end
const numbers = keep([1, 2, 3])
const flags = keep([true, false])
size <- length_of(numbers)
System.screen.border <- Color(size)If a call passes two arguments that disagree on what T or n should be, the compiler catches the mismatch with parameterAgreement.
You can also combine type parameters with optionals and events: T? matches an optional of any type, and Event of T matches a value-carrying event of any payload type. Because single capital letters are reserved for type parameters, any type you declare yourself (type, enum, or struct) must have a name at least two letters long (parameterTypeName).
Ops
While a fn is a pure calculation with no memory of the past, an operation (op) is a reusable calculation that remembers earlier frames—perfect for velocities, smooth filters, edge detectors, timers, and bouncing movement! An op looks a lot like a function and ends with its result expression, but its body is made of <- bindings that can look back at previous frames.
op-declaration ::= ( 'fast' | 'small' )? 'op' function-name '(' ( op-parameter ( ',' op-parameter )* )? ')'
( ':' type )? newline op-line* expression newline 'end'
op-parameter ::= name ( ':' type )?
op-line ::= name ( ':' type )? '<-' expression newlineop speed(x)
x - x.previous()
end
bat: Number<0...296> <- (System.joystick[2].dx * 4).sum(from: 136)
moving <- speed(bat) != 0
System.screen.border <- if moving then .yellow else .black end
System.screen.sprite[0] <- (x: bat, y: 170)Just like a function head, an op head declares its parameters inside parentheses and each call supplies the argument values (optionally labeled with the parameter's name). Those parameters are read-only input names written without var, visible only inside the op body and given their fresh value by each call. Unlike a fn parameter, an op parameter can leave out its : type annotation and automatically adopt the exact type and range of the argument passed at each call! When an op parameter does include a : type, it checks the argument against that type just like a function parameter. The : type after the closing ) is optional too; when written, it sets the result type of the op.
Notice what happens in op speed(x) above: x is a read-only input parameter, not a variable you assign to, yet x.previous() remembers what x was on the previous frame! Remembering prior samples of an input parameter is part of the op call's own frame-to-frame history—it never turns the parameter into writable storage.
Inside an op, you can read its parameters, top-level constants, functions, and other ops (trying to read a top-level var or binding directly raises operationScope). Every line before the final result expression in an op body must be a <- binding (opHolds).
Because an op carries frame-to-frame memory, you call it inside a binding (<-), never inside a when handler, a plain fn, or the starting value of a var (memoryOutsideBinding). And each written op call site in your program gets its own separate memory across frames: if two bindings both call speed(...), each call tracks its own history independently. Even better, if you pass a list to an op parameter that expects a single item, each slot of the list gets its own separate history too!
Remembering With previous
Inside a binding or an op, x.previous() gives the value x had on the previous frame. On the very first frame—before there is a previous frame—x.previous() simply equals the current value of x. If you want a specific starting value on the first frame instead, write x.previous(from: value).
When a binding inside an op (or at the top level) reads its own previous value—or reads the previous value of a binding written further down in the block—you must include from: so the first frame has a known starting point; leaving from: off raises recurrenceNeedsFrom or forwardNeedsFrom.
All of Adder's other history-keeping helpers—such as .sum(), .count(), .cycle(), and .sampled()—are built on this same foundation, and Events and Handlers introduces each of them.
Destination and start
Sometimes an op wants to adapt to the range of whatever binding is receiving its output—for example, a bounce operation that bounces between 0 and 159 for one sprite and between 24 and 255 for another! Inside an op body, the special type name Destination stands for the declared type of the binding receiving the call's result, and Destination.first and Destination.last give the lower and upper ends of that range. Because Destination comes from the receiving binding's annotation, calling such an op in a binding without a : type annotation triggers operationNeedsRange.
Inside an op, the keyword start is a bool that is true while the op works out its very first value (or when it is reset) and false on subsequent frames.
op bounce(begin, speed: Number<-15...15>)
moved <- pos.previous(from: begin) + dir.previous(from: speed)
dir: Number<-15...15> <- if moved < Destination.first or moved > Destination.last then
-dir.previous(from: speed)
else
dir.previous(from: speed)
end
pos: Destination <- if moved < Destination.first then
Destination.first + (Destination.first - moved)
else
if moved > Destination.last then
Destination.last - (moved - Destination.last)
else
moved
end
end
pos
end
x: Number<0...159> <- bounce(150, 4)
System.screen.sprite[0] <- (x: x, y: 50)on: and reset:
Every op—whether you wrote it yourself or it is built into Adder—automatically accepts two optional event arguments at any call site: on: and reset:.
on:sets the pace at which theopsteps forward. Theopworks out its initial value on the first frame, and after that it updates only in frames where theon:event fires. On frames where theon:event does not fire, theopfreezes its result, its internal bindings, and itsprevioushistory in place—costing only the quick check of the event!reset:restarts theopfresh in any frame where thereset:event fires, even ifon:would otherwise pause it that frame. Theop(along with any nested ops or history-keeping members inside it) recomputes its first-frame value from its current arguments, andstartbecomestrueagain for that frame. If bothreset:andon:fire in the same frame,reset:wins.
op travel(initial)
position: Destination <- if start then initial else position.previous(from: initial) +% 1 end
position
end
moving <- System.screen.frame when System.joystick[2].fire
x: Number<0...159> <- travel(20, on: moving, reset: System.keyboard.space.pressed)
System.screen.sprite[0] <- (x: x, y: 50)Both on: and reset: require an event (onNeedsEvent). Plain fn functions do not keep history across frames, so passing on: or reset: to a fn call is flagged with badArgument.
Work
What if you have a big calculation—like generating a dungeon map, tallying a large table, or scoring a board—that takes too many CPU cycles to finish inside a single television frame without delaying the screen update? Declare it with work Type.name(...)! Calling a work declaration starts a background job that captures its arguments and chips away at the calculation across frames, using the cycles left over in each frame after your regular handlers and outputs finish.
work-declaration ::= 'work' name '.' name '(' parameters? ')' ':' type newline
statement* expression newline 'end'A work declaration is always written as a member of a type declared by your program (often a type alias for a list or a struct), and its body follows the same rules as a fn body. You do not need to sprinkle manual pause points through your loops: the compiler automatically splits the body across loop iterations! A short loop that fits comfortably inside one frame runs in a single part; a longer loop runs a few iterations per frame across several parts; and a loop-free body runs as one part. If a single turn of a loop has an unbounded cost or is too heavy to fit inside a frame at all, the compiler tells you right away and names the loop.
type Samples = [4 of Number<u8>]
work Samples.tally(input: Samples): Number<u16>
var amount: Number<u16> = 0
for sample in input
amount = amount + sample
end
amount
end
const samples: Samples = [1, 2, 3, 4]
answer: Number<u16>? <- samples.tally()
System.screen.text[0][0..<5] <- if amount = answer then amount.digits(5) else "wait " endBecause a job may take several frames to finish, binding its result with <- produces an optional type T?: the binding holds nil while the job is still working, and switches to the finished value T as soon as the job completes. (Giving the binding a non-optional type T raises wrongType.) If any argument to the work call changes while a job is in flight, Adder automatically starts a fresh job with the new arguments to replace the unfinished one.
Want a compile-time guarantee that your background job always finishes within a set number of frames? Tack .within(frames: n) onto the call in a binding! The compiler proves from the frame cycle budget that the job finishes in n frames or fewer—flagging jobDeadline if it cannot prove the deadline—and the check adds zero runtime cost to your program.
You can also step a job manually inside a var: write var job: Job of T = value.name(...), and in a handler call job.step() to run one part and get back T?, or call job.run() to finish the remaining work immediately in that handler turn and get back T. You can even line up multiple jobs in a queue to run one after another, as Events and Handlers explains.
Type Declarations
Adder gives you three ways to introduce your own types: type aliases, enum declarations, and struct declarations. Just like other declarations, a type must be declared above any line that names it (unknownType). Every type name starts with a capital letter and is at least two letters long; starting a type name with a small letter triggers aliasName, enumName, or structName.
type-alias ::= 'type' name '=' typeType Aliases, Enums, and Structs describe the values these types create. While an enum block contains only case lines, a struct can open a block to hold both its fields and its member functions!
Struct Blocks
A struct block lists its fields and functions one per line between struct Name and end. You can either list all the fields on lines inside the block, or put the plain fields in parentheses on the struct Name(...) header line and use the block body for var fields and member functions.
struct-member ::= name ':' type newline
| 'var' name ( ':' type )? '=' expression newline
| ( 'fast' | 'small' )? 'mut'? 'fn' function-name '(' parameters? ')' ( ':' type )? newline
statement* expression? newline 'end'There are two kinds of fields in a struct block:
- Plain fields (
step: Number<u8>) are supplied when you construct the struct, such asScore(step: 10). - Variable fields (
var points: Number<0...999> = 0) start at the initial value written in the declaration. Because they already have their starting value, the struct's maker does not take an argument for them (passing one raisesbadArgument).
Putting any line other than a field or a fn inside a struct block raises structBody.
struct Score
step: Number<u8>
var points: Number<0...999> = 0
mut fn award()
points = points +| step
end
fn text(): [3 of Char]
points.digits(3)
end
fn Score.started(step_size: Number<u8>): Score
Score(step: step_size)
end
end
var score = Score.started(10)
when System.keyboard.space.pressed
score.award()
end
System.screen.text[0][0..<3] <- score.text()Functions inside a struct block come in three flavors:
- Read-only member functions (
fn name(...)): Written without a type prefix before the name. You do not write a parameter for the struct itself—all of the struct's fields and other member functions are automatically in scope by name! A plainfnpromises not to change any of the struct's fields (even indirectly through another call); trying to modify a field inside a plainfnraisesstructMutationUndeclared. - Mutating member functions (
mut fn name(...)): Marked withmutso they can update the struct's fields. Amut fndoes not have to return a value (so its: typeis optional), and handlers can call it on avarstruct, a field of avar, or an element of avarlist. Because it modifies the struct in place, calling amut fninside a binding or on a constant struct is caught withstructChangingPlace. (mut fnis only for struct blocks; writingmut fnat the top level triggersstructMutationMethodOnly.) - Maker functions (
fn StructName.name(...)): Written with the struct's own type name before the dot, likefn Score.started(...). A maker belongs to the type as a whole rather than to an existing instance, so the struct's fields are not in scope inside it, and you call it on the type name (Score.started(10)). Prefixing a function inside a struct block with some other type's name triggersstructMakerOwner.
Placing a Value in Memory
Sometimes you want a variable, a table, or an output to live at an exact address in the machine's memory—for instance, to share a buffer with an asm block, place a custom screen matrix on a hardware boundary, or peek and poke a cartridge register. Write name: T at address to pin a value to a fixed memory address:
placed-value ::= name ':' type 'at' whole-literal ( '<-' expression )?seen: Number<u8> at 0xC000
shade: Number<u8> at 0xC001 <- seen +% 1
System.screen.border <- Color(seen % 16)Every time your program reads seen, it reads straight from that address in memory, so it immediately sees any byte written there by hardware or assembly code. The address after at must be a whole-number literal (addressNeeded) where the type actually fits within the machine's memory (hardwareInvalid). Unlike a var, a placed value takes no starting = expression. When you attach <- expression, the placed value becomes an output that Adder updates at that address every frame; without <-, a single placed value is read-only from Adder handlers (registerAssigned).
You can also place an entire list at a fixed address, and handlers can freely read and write its elements in place:
tiles: [16 of Number<u8>] at 0xC000
when System.screen.frame
tiles[0] = tiles[1] +% 1
end
System.screen.border <- Color(tiles[0] % 16)If you bind a placed list as the whole text screen, it must start on a 1,024-byte boundary where the video chip can reach it (listOffBoundary).
To keep your program from stepping on its own toes, the compiler checks every at address against your memory layout:
- Placing a list over addresses
0and1(the processor's built-in I/O port) is caught withplacedOnPort. - Two placed values that overlap each other's bytes are caught with
placedOverList. - Any placed value that collides with your compiled code, variables, or runtime routines is caught with
placedOverProgram. - Placed lists get three extra safety checks so a table write never clobbers the machine: they cannot overlap the zero page handed out by the compiler, the processor stack, or the runtime's interrupt vectors (
placedOverRuntime); they cannot overlap the active text screen unless the list is that screen (placedOverScreen); and in a bitmap program, they cannot overlap the picture's memory (placedOverPicture). - A single placed number only checks against your compiled program and other placed values, leaving you free to point it at special addresses as long as you keep it clear of the rest.
Assembly maps out how a compiled Adder program arranges memory.
Include and Project Files
As your game grows, you can split your types, art tables, sound helpers, and cases across multiple files and bring them together with include "path":
include ::= 'include' string-literalinclude "lib/world"
var lane: Number<0...39> = 0
when System.screen.frame
lane = wrap(lane)
endAny folder containing main.adder acts as a project, with main.adder as the starting file; a standalone .adder file treats its own folder as its project root. Every include path is resolved from that project root folder—even when written inside a file in a subfolder—and you can leave off the .adder extension, so "lib/world" and "lib/world.adder" refer to the exact same file.
If several files include the same helper file (or even include each other in a circle), Adder includes each file just once, at the spot where it is first mentioned. All files in a project share one flat namespace and follow the same top-to-bottom rule as a single file: a declaration from an included file is ready to use on any line after the include statement. The compiler typechecks every declaration in every included file, and then strips away any unused declarations so your final binary only pays for what your program actually uses.
Include paths use forward slashes / between folder and file names made of ASCII letters, digits, -, _, and ., with no name starting with a dot. Absolute paths, backslashes, empty path parts, and . or .. names are not allowed, and referencing a file that does not exist in the project raises includeMissing.
Write your include lines at the top level of a file, never tucked inside a handler or block.
Declarations in a Match
What a case declares exists only while the case matches. Among declarations, a match or an if holds declarations: bindings, variables, naming lines and handlers. Only the chosen case runs; the other cases cost the frame nothing.
declaration-match ::= 'match' expression? newline declaration-case+ 'end'
declaration-case ::= ( 'fast' | 'small' )? ( pattern ( 'when' expression )? | 'when' expression | 'else' ) '->' declaration-body
declaration-body ::= declaration-line | newline declaration-line*
declaration-if ::= ( 'fast' | 'small' )? 'if' condition 'then' declaration-body
( 'else' 'if' condition 'then' declaration-body )*
( 'else' declaration-body )? 'end'
declaration-line ::= declaration | handler | declaration-match | declaration-ifenum Scene
case title, game
end
var scene: Scene = .title
var rounds: Number<0...99> = 0
match scene
title ->
System.screen.border <- .blue
System.screen.text[11][15..<25] <- "PRESS FIRE"
when System.joystick[2].fire.pressed
scene = .game
end
game ->
var time_left: Number<0...9> = 9
System.screen.border <- .green
System.screen.text[0][0..<1] <- time_left.digits(1)
when System.screen.frame.every(50)
time_left = time_left -| 1
end
when start
wait(seconds: 10)
rounds = rounds +| 1
scene = .title
end
endThe value after match may be any value a statement match takes: a number, a named set such as Color, an enum, a bool, an optional, a list or a struct. Every possible value needs a case, or an else -> for the rest. Each case's lines begin on the line below its ->.
A number may have whole-number cases such as 0 ->; a named set uses its bare names, such as red ->. An optional needs nil -> for an absent value. A name pattern such as n -> holds the whole value, or an optional's present value, read-only throughout the case. That case starts again when its named value changes; an else -> case keeps its variables while the value still selects it.
var level: Number<0...9> = 1
match level
0 ->
System.screen.text[0][0..<1] <- "-"
n ->
System.screen.text[0][0..<1] <- n.digits(1)
end
when System.keyboard.space.pressed
level = level +% 1
endA pattern for an enum case that carries fields names them, as a statement match does: over(score) -> or over(score: final) ->. The names hold the case's values, have the fields' types, and can be read anywhere in the case: its bindings, handlers, constants and nested match. A pattern written without fields binds none. The compiler refuses a field the case does not have with unknownField, and an assignment to a field's name with notAssignable.
enum Flow
case title
case over(score: Number<0...99>)
end
var flow: Flow = .title
var points: Number<0...99> = 0
match flow
title ->
System.screen.border <- .blue
when System.joystick[2].fire.pressed
points = points +| 7
flow = .over(score: points)
end
over(score) ->
System.screen.border <- .red
System.screen.text[0][0..<2] <- score.digits(2)
when System.joystick[2].fire.pressed
flow = .title
end
endConditions on Cases
A case may add when condition after its pattern. The first pattern and condition that fit choose the case, just as in a statement match. Conditions are read with the value after match each frame; a condition changing can end one case and start another while the value after match stays equal.
enum Mode
case game
end
var mode: Mode = .game
var lives: Number<0...3> = 3
match mode
game when lives == 0 ->
System.screen.border <- .red
game ->
System.screen.border <- .green
end
when System.keyboard.space.pressed when lives > 0
lives = lives - 1
endWithout a value after match, write ordered when condition -> cases and a final else ->. A when line without the case arrow is a handler inside the case. Either case head may begin with fast or small.
held <- System.joystick[2].fire
match
when held ->
System.screen.border <- .green
else ->
System.screen.border <- .blue
endDeclarations in an If
A declaration if follows the same lifetime rule. A boolean head chooses its true or false case; else if checks heads in order, stopping at the first one that succeeds. An absent else declares nothing while the condition is false.
var paused = false
System.screen.text[10][17..<23] <- "GAME "
if paused then
System.screen.text[10][17..<23] <- "PAUSED"
end
when System.keyboard.space.pressed
paused = not paused
endAn optional head such as if held = item and held > 0 then captures the present value once. Its name is read-only throughout the selected case, and changing that named value starts the case again. Later else if heads are not read after an earlier one succeeds. fast or small before if applies to all its cases; an inner mark decides its own code.
A case may put exactly one declaration after -> on the same line. A handler there can hold its own multi-line body, ending at its own end; another declaration below the completed inline case is statementAfterArm. A one-line if keeps its branches and closing end on that line.
When a Case Starts and Stops
When a handler changes the value after match, the new case begins in the next frame. The case that ends stops its handlers, including the ones that are waiting. The case that begins gives its variables their first values, shows what it binds, and starts its handlers from the top; its start happens in its first frame. A case starts again when the value after match moves to another case, or when a field the case names changes. Setting flow = .over(score: 12) while over(score: 5) runs ends the case and starts it again in the next frame, because over(score) names score. A case that names no fields, such as level -> or else ->, keeps running when only the fields change. A case that names some of its case's fields starts again for those and ignores the others. Each case decides from its own line, so a sibling written with fields does not change it. Setting a value equal to the one it has changes nothing. A variable declared above the match keeps its value as cases change, and a handler outside every case keeps running and waiting.
A case may bind a part of the screen, the border or a sprite that the lines outside the match also bind. While the case shows, its line decides that part; when it ends, the outer line decides it again. Two lines at one level cannot both bind the same part, and the compiler refuses the second with overlap.
What a Case Holds
A case holds bindings, var lines, constants, handlers, and nested match or if blocks that hold declarations. Types, functions, ops, placed values and queues belong to the whole program, so the compiler refuses them in a case with caseDeclaration, or with caseNotBuilt for what it cannot yet build in a case.
| Refusal | Key |
|---|---|
a match that holds declarations inside the body of an output | outputMatch |
more declarations below a case with a declaration on its -> line | statementAfterArm |
| a case that cannot begin within a frame's time | caseStartLong, caseStartLate |
A case that has too much to show in one frame begins over several frames. A canvas's new picture is drawn with the screen blank until it is ready. Costs and the Frame describes the limits.
Events and Handlers
Bindings keep your screen and sound wired up from frame to frame, but a game also needs moments when things happen: a player taps the space bar, a fresh frame begins, a score ticks up, or a timer rings. An event happens in some frames and not in others. A when handler listens for an event and runs its statements in each frame where that event fires. Only a handler can change a variable, act on a device, pause with wait, or loop forever.
var hue: Number<0...15> = 0
when System.keyboard.space.pressed
hue = hue +% 1
end
System.screen.border <- Color(hue)Handlers
A handler starts with when and an event, lists the statements to run, and closes with end. It stands at the top of a program or inside a case, never tucked inside another handler or a function. Putting fast before when spends extra bytes of code to save cycles in that handler alone, as Costs and the Frame describes.
handler ::= ( 'fast' | 'small' )? 'when' ( name '=' )? expression newline statement* 'end'The expression after when must be an event—a moment in time, not an ongoing state. A plain yes-or-no value can stay true across many frames, so if you write one after when, the compiler catches it with onNeedsEvent and suggests the event you probably meant. Write when System.joystick[2].fire, for instance, and the message points you to System.joystick[2].fire.pressed for the single frame the button goes down. When you really do want something to run in every frame while the button is held, filter the frame beat with System.screen.frame when System.joystick[2].fire, as Filters describes.
Inside a handler you write statements: assignments, local var lines, if, match, for, and device actions (see Statements), along with guard, wait, and forever from this chapter. Bindings (<-) live outside handlers, where they stay wired up continuously; if you slip a <- inside a handler, the compiler flags it with actionNeeded and reminds you to assign a variable with = instead.
Inside a handler, if chooses statements to run. Among declarations, if chooses declarations that exist only while their case matches; see Declarations in an If.
Handlers in a Frame
Every television frame follows the same tidy three-step rhythm:
- The machine reads its inputs and decides which events happen in this frame.
- The handlers whose events happened run one after another, from top to bottom in the order you wrote them.
- The outputs—such as the screen, the border, and the sprites—show the fresh values that the handlers left behind.
Because handlers run in written order, each handler sees every change that an earlier handler made in the same frame. Suppose a key-press handler sets age = 0 and a frame handler below it adds one: age ends that frame at 1. Put them in the opposite order and age ends the frame at 0. Whenever two cases belong to the same decision, folding them into a single handler makes your intent crystal clear:
var age: Number<0...99> = 5
when System.screen.frame
if System.keyboard.space.pressed then
age = 0
else
age = age +| 1
end
end
System.screen.text[0][0..<2] <- age.digits(2)Notice how System.keyboard.space.pressed works right inside if ... then: an event used as a condition is true in the frames where it happens and false in all the others.
All events for a frame are decided in step 1, before any handler runs. That means when a handler changes a variable, that variable's .changed event fires at the start of the next frame, not midway through the current one.
If a handler stops the program—say, when a calculation steps outside its declared range—the variables it already changed keep their new values, and step 3 never updates the outputs.
Events
Every event has the type Event, or Event of T when it carries a value of type T each time it happens, as Types describes. You can give an event a name with a <- binding, so it stays live across frames, whereas storing an event in a constant or a var is caught with eventConstant.
| Event | Happens | Carries |
|---|---|---|
start | in the first frame of the program, and of each case as it begins | nothing |
System.screen.frame | in every frame, the first one included | nothing |
System.screen.frame.every(n) | in every nth frame | nothing |
key.pressed, key.released | in the frame a key or button goes down, or comes back up | nothing |
System.keyboard.stroke | in the frame a keystroke arrives | a Keystroke |
value.changed | in a frame where value differs from the frame before | nothing |
System.clock.interval(...) | again and again, at a fixed period | nothing |
System.clock.timer(...) | once, after a delay | nothing |
value when event | when event happens | value |
Other events belong to individual devices and queues: shown on a screen region or a canvas, System.keyboard.dropped, and a queue's dropped and completed. The System Device Tree describes each device's events in full.
Start
start fires once, in the very first frame of the program. Inside a case, start fires in the first frame of that case each time the case begins. A when start handler is the natural home for a scripted sequence of steps paced with wait:
var ready: bool = false
when start
wait(seconds: 2)
ready = true
end
System.screen.border <- if ready then Color.green else Color.black endUsed inside an expression, start is true during the first frame of the program (or of its enclosing case) and false thereafter. Inside an op, start is also true in a frame where a reset: event starts the op over from the beginning, as Declarations describes.
Frame Events
System.screen.frame is the steady heartbeat of the machine: it happens in every single frame, including the first. System.screen.frame.every(n) ticks on every nth frame after the first—so every(3) happens in the fourth frame, the seventh, the tenth, and so on.
var x: Number<0...296> = 136
when System.screen.frame
x = x +| System.joystick[2].dx
end
System.screen.sprite[0] <- (x: x, y: 170)Presses and Releases
pressed fires in the exact frame a key or button goes down, and released fires in the frame it springs back up. Holding the key or button down across later frames does not fire pressed again. Every key on System.keyboard, a key chosen while the program runs with System.keyboard[key], the fire button of each joystick, and the left and right buttons of the mouse all provide both events.
var key: Key = .a
var presses: Number<0...99> = 0
when System.keyboard[key].pressed or System.mouse.left.pressed
presses = presses +| 1
end
when System.joystick[2].fire.released
key = .b
endA value that is not a key or a button—such as a binding or a joystick's dx—has no pressed event; watch it with changed instead.
Changes
value.changed happens in any frame where a named value differs from what it was in the frame before. You can attach .changed to a variable, a binding, a struct field, or a device reading of any type.
var lives: Number<0...3> = 3
var over: bool = false
when System.keyboard.space.pressed
lives = lives -| 1
end
when lives.changed when lives == 0
over = true
end
System.screen.border <- if over then Color.red else Color.black endA device reading changes in the frame the machine reads its new state. A variable that a handler modifies shows its .changed event in the next frame, because the frame's events are all decided before any handler runs. changed itself carries no value; simply read the value by name inside your handler.
Events That Carry a Value
Some events arrive bearing a gift: System.keyboard.stroke carries a Keystroke, and a queue's batch carries a list. Write when name = event to catch the carried value and give it a name inside the handler:
var shown: Char = .space
when s = System.keyboard.stroke
if c = s.character() then
shown = c
end
end
System.screen.text[12][20] <- shownYou can also pack your own value onto any event with value when event, which creates a new event that carries value in each frame where event happens. The right side of when may even be a yes-or-no value, in which case the event happens in every frame where that condition is true.
points <- 2 when System.keyboard.space.pressed
var score: Number<0...99> = 0
when p = points
score = score +| p
end
System.screen.text[0][0..<2] <- score.digits(2)If an event carries no value, there is nothing to catch, so the compiler flags when name = on a valueless event with eventNoValue.
A stored optional (T?) is a value in memory rather than an event, even though it may hold something: writing when h = held triggers onNeedsEvent, and writing held when e triggers whenSubject. To react when an optional changes, watch held.changed, and inside the handler unwrap it with if h = held then.
Filters
Writing event when condition narrows an event so it happens only in the frames where event happens and condition holds true. The condition can be a yes-or-no expression, or another event (which counts as true in the frames where it fires). You filter a when handler the exact same way, by placing a second when after its event:
var shots: Number<0...99> = 0
var lives: Number<0...3> = 3
when System.keyboard.space.pressed when lives > 0
shots = shots +| 1
end
moving <- System.screen.frame when System.joystick[2].fire
when moving
lives = lives -| 1
endWhen a handler names a carried value, its when filter can inspect that name right on the header line:
var plain: Key = .space
when s = System.keyboard.stroke when not s.shift
plain = s.key
endKeep using when to attach a filter to a handler line; if you write and instead, as in when s = System.keyboard.stroke and s.shift, the compiler nudges you with andOnLine.
filtered-event ::= expression 'when' expression
joined-event ::= expression 'or' expressionJoined Events
a or b combines two events into one that happens in every frame where either a or b happens. If both events carry values of the exact same type, the joined event carries that type too, and when both happen in the same frame, the left-hand value wins. If only one carries a value, or their types differ, the joined event carries nothing.
left <- 1 when System.keyboard.a.pressed
right <- 2 when System.keyboard.d.pressed
var moved: Number<0...2> = 0
when step = (left or right)
moved = step
endIn when name = ..., the name captures only the expression after = up to the first or, and, or when. Writing when step = left or right without parentheses would leave step empty whenever right happened alone, so the compiler catches it with orAfterAssignment; wrapping (left or right) in parentheses joins the two value-carrying events first.
Events and Conditions
Events and yes-or-no (bool) values are distinct kinds of things, and the compiler keeps them from getting tangled in and, or, and not:
orjoins either two events or two yes-or-no values; mixing one of each raisesorNeedsEvents.andworks on yes-or-no values, never on events (andEvents); to require two events in the same frame, filter one by the other withwhen.notflips a yes-or-no value and cannot be applied directly to an event (notEvent).
You can always turn an event into a yes-or-no test by comparing it with nil: event == nil is true in frames where the event stays quiet, and event != nil is true in frames where it happens.
Event Members
Event members have built-in memory across frames—counting presses, cycling through colors, or keeping a running tally—so each one lives in a <- binding where it can keep its own state from frame to frame. Most belong to an event, while sum, sampled, and history can also watch a changing value in every frame. If you place one inside a handler, where it would be forgotten at the end of the block, the compiler catches it with memoryOutsideBinding. By contrast, .changed, .pressed, and .released are maintained once by their source, and every reader shares them freely.
| Member | Gives | In the first frame |
|---|---|---|
e.every(n) | an event that happens on every nth occurrence of e | does not happen |
e.after(n) | an event that happens n frames after e | does not happen |
e.hold(n) | a yes-or-no value that stays true for n frames each time e happens | true if e happens |
e.cooldown(n) | an event that passes e through and then ignores it for the next n - 1 frames | happens if e happens |
b.rising | an event that happens when b steps from false to true | does not happen |
b.falling | an event that happens when b steps from true to false | does not happen |
e.cycle() | the next value of the range or enum at each occurrence, wrapping at the end | the first value |
e.count() | how many times e has happened | 0 |
x.sum(from: v) | a running total of x, clamped to the range | v, or 0 without from: |
e.toggle() | a yes-or-no value that flips at each occurrence | false |
e.countdown(from: v) | a count that goes down by one at each occurrence and stops at the bottom of the range | v |
e.latest() | the value that e carried most recently, as an optional | nil |
x.sampled(on: e) | the value x had at the latest occurrence of e, as an optional | nil |
x.history(n) | a list of the values of x in the last n frames, newest first, each an optional | all nil |
press <- System.joystick[2].fire.pressed
third <- press.every(3)
presses <- press.count(0...255)
shade <- third.cycle(Color)
bat <- (System.joystick[2].dx * 4).sum(from: 136, in: 0...296)
System.screen.border <- shade
System.screen.sprite[0] <- (x: bat, y: 170)Ranges and Periods
cycle, count, countdown, and sum can take their range either from an inline range argument—such as e.cycle(0..<4), e.count(in: 0...99), or x.sum(from: 136, in: 0...296)—or from the type annotation on the binding that holds them. Passing the range inline lets you chain .cycle(0..<4) directly inside a larger expression like palette[System.screen.frame.every(6).cycle(0..<4)]. e.countdown(from: k) defaults its range to 0...K when k has a known non-negative upper bound K, so hit.countdown(from: 6) > 0 needs no extra range annotation. If neither an inline range nor a binding type supplies the bounds, the compiler reminds you with timeNeedsRange.
cycle steps through whole numbers or an enum, so giving it a fractional range raises wrongType. When a count climbs past the top of its range, the program stops and names the line; sum clamps gently to the nearer end of its range instead. sum adds numbers in every frame—or adds the numeric values carried by an event in the frames where it happens—and flags non-numeric inputs with sumNeedsNumber. latest remembers the last value an event carried, so calling it on an event that carries nothing raises timePayload.
The period n in every(n) is a whole number from 1 to 65,535 and may vary while the program runs. The frame count n in after(n) (0 to 65,535) and in hold(n) or cooldown(n) (1 to 65,535) is a constant whole number; all four also accept frames:, seconds:, or ms: labels. Any period or delay outside those bounds is caught with timePeriod. If e happens again before e.after(n) has delivered its delayed tick, the new occurrence replaces the waiting one and starts the n-frame countdown fresh. On any yes-or-no expression b, b.rising (or b.rising()) fires in the frames where b becomes true after being false on the prior frame, and b.falling (or b.falling()) fires when b becomes false after being true.
You can also pace sum and history with an event: x.sum(on: e) adds x only in the frames where e happens, and x.history(n, on: e) records a fresh sample only in those frames.
Resetting a Member
Every stateful event member accepts reset:, an event that snaps the member back to its starting value. When both the reset event and an update happen in the same frame, the reset wins.
step: Number<0...7> <- System.screen.frame.every(4).cycle(reset: System.keyboard.space.pressed)
System.screen.border <- Color(step)Guard
guard condition checks that a requirement holds before letting the handler continue. When the condition is false, the handler stops for this frame: none of the statements below the guard run, any enclosing forever loop stops as well, and the handler simply waits to start from the top on its next event.
guard-statement ::= 'guard' expression
| 'guard' name '=' expression ( 'and' expression )?guard name = optional unwraps an optional value for the rest of the handler when it is present, or ends the handler on the spot when it is nil. You can chain and right after the unwrap to test a condition that reads the newly named value:
var held: Number<u8>? = nil
var shown: Number<u8> = 0
when System.keyboard.space.pressed
guard h = held and h > 3
shown = h
end
when System.keyboard.h.pressed
held = 7
endA single guard line can name one optional value; adding a second = on the same line is caught with secondAssignment. Because functions, ops, and bindings must always produce a value rather than stopping early, placing a guard inside them raises guardOutsideHandler.
Wait
wait pauses a handler right where it stands. The handler rests for the remainder of the current frame and picks up on a later frame right after the wait, with all of its local variables still holding the values you left in them.
wait-statement ::= 'wait' '(' expression ')'
| 'wait' '(' time-unit ':' expression ')'
| 'wait' expression
time-unit ::= 'seconds' | 'ms'enum Scene
case title, game
end
var scene: Scene = .title
var credit: bool = false
match scene
title ->
System.screen.text[10][17..<22] <- "ADDER"
System.screen.text[12][13..<26] <- if credit then "A GAME BY YOU" else " " end
when start
wait(100)
credit = true
wait System.joystick[2].fire.pressed
scene = .game
end
game ->
System.screen.text[11][16..<23] <- "GAME ON"
endWhile a handler is paused at a wait, its own trigger event goes unheard: if a when ...pressed handler is mid-wait when the player presses that button again, the extra press passes by and is not saved up for later. In any single frame, a waiting handler runs only the stretch of statements between one wait and the next; Costs and the Frame explains how the compiler budgets each stretch.
wait belongs strictly inside a handler. Functions, work members, and bindings compute their answers immediately, so placing a wait or forever inside them raises waitOutsideHandler.
Waiting for Frames
wait(n) pauses for n frames, where n is a whole number from 1 to 65,535. The count n can come from a variable or calculation, as long as its type range starts at 1 or higher. Since a wait must always yield for at least one frame, the compiler flags wait(0) with waitNoFrames, a count whose range includes 0 with waitMayNotPause, and anything other than a whole number or event with waitNeedsEventOrFrames.
Because PAL and NTSC machines tick at different frame rates, a raw frame count runs at the machine's native speed: wait(100) lasts 2 seconds on PAL (50 Hz) and about 1.7 seconds on NTSC (60 Hz).
Waiting for a Time
When you want a pause to feel the same on every television standard, write wait(seconds: t) or wait(ms: t). The compiler converts your time into the nearest whole number of frames for each standard—always at least 1 frame—so the pause is accurate to within half a frame everywhere.
| Wait | PAL | NTSC | Early NTSC |
|---|---|---|---|
wait(seconds: 2) | 100 frames | 120 frames | 122 frames |
wait(ms: 500) | 25 frames | 30 frames | 30 frames |
wait(seconds: 0.25) | 13 frames | 15 frames | 15 frames |
The duration t must be a constant greater than zero, written with either seconds: or ms:. The compiler catches a variable or non-positive time with waitTimeAmount, unknown or doubled unit labels with waitTimeUnit, and a duration that exceeds 65,535 frames on any machine with waitTimeLong.
var lit: bool = false
when start
forever
wait(ms: 500)
lit = not lit
end
end
System.screen.border <- if lit then Color.white else Color.black endWaiting for an Event
wait event pauses the handler until event happens in a later frame. You can wait on any event, including one built with when filters or joined with or. Passing a plain yes-or-no value instead of an event raises waitNeedsEventOrFrames.
var go: bool = false
when start
wait System.keyboard.a.pressed or System.joystick[2].fire.pressed
go = true
endRemember that the machine decides all of a frame's events at the very top of the frame, before any handler runs. Because of that, the event in a wait cannot depend on a name created inside the handler—such as a local var, a for loop counter, or a carried event value—and the compiler flags such references with waitEventReadsLocal.
Waiting for a Timer
A System.clock.timer(...) fires once after its countdown from the start of the program or case, and never fires a second time. To make sure your handler is guaranteed to be listening before the timer goes off, the compiler accepts wait timer only inside a when start handler of the same program or case that declared the timer, outside any forever or for loop, and before any open-ended event wait or any timed wait as long as the timer itself:
ready <- System.clock.timer(seconds: 2)
var go: bool = false
when start
wait ready
go = true
end| Wait | Refused with |
|---|---|
inside forever or for | waitOnTimerLoop |
| in a handler on another event | waitOnTimerElsewhere |
| after a wait that can end after the timer fires | waitOnTimerLate |
| a second time in the same handler | waitOnTimerTwice |
Whenever one of those rules triggers, the compiler message suggests a direct timed wait such as wait(seconds: 2). Waiting on timer or another event is allowed anywhere, because the other event can always wake the handler even if the timer has already passed. If you want a recurring beat you can wait on again and again, use System.clock.interval(...) instead.
Forever
forever repeats its body over and over for as long as its handler stays active. Inside a case, it loops until the case ends; at the top of the program, it loops until the program stops.
forever-statement ::= 'forever' newline statement* 'end'Because a handler must yield back to the frame after each stretch of work, every trip around a forever loop must hit a wait on every possible path through its branches. If there is any path through the loop that could spin back to the top without waiting, the compiler catches it with foreverNeverWaits and points out the path that stayed awake.
break is for bounded for loops and cannot be used to jump out of a forever (breakInForever). To stop a forever loop, either end the whole handler with guard or switch to another case. A for loop has a fixed number of turns, so it does not require a wait, though you are welcome to put a wait inside one when you want to pace its steps across frames.
Handlers in a Case
A case's handlers start from the top when it starts matching and stop, waiting ones included, when it stops matching. Declarations in a Match describes the rule and its scope.
Queues
A queue is a first-in, first-out line of items: handlers drop items in the back, and your program takes them from the front in the order they arrived. Declare one at the top level with Queue(of: T, capacity: n), giving it a constant name, an item type T, and a maximum capacity from 1 to 65,535.
queue-declaration ::= name '=' 'Queue' '(' 'of' ':' type ',' 'capacity' ':' expression ')'A queue provides four members:
add(item), used as a statement inside a handler, places a new item at the back of the queue;take(on: System.screen.frame, limit: n), used in a single binding, is an event that fires when the queue is non-empty and carries a list ofnoptional items, oldest first, padded withnilin any slots it could not fill;count()gives the number of items currently waiting in line;droppedis an event that happens in the frame after a full queue had to turn away anadd.
const deliveries = Queue(of: Number<u8>, capacity: 4)
var received: Number<0...999> = 0
var full_turns: Number<0...999> = 0
when System.keyboard.space.pressed
deliveries.add(10)
deliveries.add(20)
deliveries.add(30)
end
batch <- deliveries.take(on: System.screen.frame, limit: 1)
when items = batch
if amount = items[0] then
received = received +| amount
end
end
when deliveries.dropped
full_turns = full_turns +| 1
end
System.screen.text[0][0..<3] <- received.digits(3)Items added by a handler become available to take starting in the next frame. When the queue is empty, take stays quiet and its event does not happen. If a handler calls add when the queue is already at capacity, the newest item is turned away and dropped fires once in the following frame, no matter how many extra items were rejected during that frame.
Only one binding may call take on a given queue—any other readers simply read that binding's name, and a second take on the same queue is caught with queueConsumer. The compiler checks the remaining queue rules under queueForm: a queue must be a constant rather than a var, its capacity must stay within 1 to 65,535, take must live in a binding with on: System.screen.frame and limit:, add must be called inside a handler, and the item type T cannot itself be an optional (since take uses nil to mark empty slots in a batch—wrap an optional inside a struct if an item needs a missing-value state).
Queues of Jobs
A job queue, declared as Queue(of: Job of T, capacity: n), runs background work jobs one after another using the cycles left over in each frame after handlers and outputs finish. Call add(job) in a handler to enqueue a job, such as a call to a work member. As each job finishes, the queue's completed event fires (at most once per frame) carrying that job's result of type T, while dropped reports an overflow just like any other queue. Because the machine drains and runs a job queue automatically, you never call take on one (jobForm). Declarations shows how to write work members and jobs.
type Samples = [4 of Number<u8>]
work Samples.tally(input: Samples): Number<u16>
var amount: Number<u16> = 0
for sample in input
amount = amount + sample
end
amount
end
const samples: Samples = [1, 2, 3, 4]
const jobs = Queue(of: Job of Number<u16>, capacity: 2)
var last: Number<u16> = 0
when System.keyboard.space.pressed
jobs.add(samples.tally())
end
when result = jobs.completed
last = result
endStatements
Bindings describe how values flow continuously, while statements are the step-by-step instructions that change state or choose what happens next. Statements live inside the bodies of when handlers, inside mut fn member functions, and inside the for loops of ordinary functions. They run in a straight line from top to bottom, and each statement immediately sees the changes made by the statements above it.
statement ::= assignment | local-variable | if-statement | match-statement
| for-loop | break-statement | call-statementA handler body can also use guard, wait, and forever (see Events and Handlers), as well as asm blocks (see Assembly). The top level of a program holds declarations and handlers. An if there holds declarations with case lifetimes; inside a handler it holds statements. A loose for still needs a handler (loopAtTopLevel).
Assignment
An assignment stores a fresh value into a place in memory—whether that place is a whole variable, a single element of a list, a field of a struct, or a multi-element window of a list. Write the destination place on the left of = and the new value on the right.
assignment ::= place '=' expression newline
place ::= name selector*
selector ::= '.' name | '[' expression ( ',' expression )? ']'The machine evaluates the entire right-hand expression first and then stores the result into the place on the left. The new value must match the place's type (wrongType), and a constant literal that falls outside the place's declared range is caught right away with literalDoesNotFit. When the right-hand side is a calculation whose exact range the compiler cannot prove ahead of time, the program checks the number as it stores it. If the value steps out of range at runtime, the program halts and prints the offending line number on screen, as Values Outside a Range describes.
var lives: Number<0...3> = 3
var score: Number<0...999> = 0
when System.keyboard.space.pressed
score = score +| 10
lives = lives -| 1
end
System.screen.text[0][0..<3] <- score.digits(3)Every assignment takes effect the moment it runs. In the handler below, the second line reads the updated value that the first line just stored:
var taps: Number<0...9> = 0
when System.keyboard.space.pressed
taps = taps +% 1
taps = taps +% 1
end
System.screen.text[0][0..<1] <- taps.digits(1)Adder has a single assignment symbol, =, so every change is spelled out explicitly. Shorthand operators like x += 1 are flagged with actionNeeded—write x = x + 1 (or +% / +|) instead. And if you accidentally assign a variable to itself (x = x), the compiler points out the no-op with selfAssignment.
Elements, Fields and Windows
A place on the left of = can drill into a list element with [index], a struct field with .field, or a chain of both in any order. Only that selected part of the variable changes; the rest of the list or struct stays untouched.
struct Spot(x: Number<0...319>, y: Number<0...199>)
var spot: Spot = (x: 0, y: 0)
var spots: [4 of Spot] = Spot(x: 0, y: 0)
var grid: [3 of [4 of Number<u8>]] = 0
when System.keyboard.space.pressed
spot.x = 10
spots[2].y = 5
grid[1][2] = 7
grid[0] = [1, 2, 3, 4]
endIf an index is known at compile time to fall outside the list, the compiler catches it early with indexOutOfBounds. When an index varies at runtime, the program checks it before writing and halts with the line number if it ever steps out of bounds.
You can also index a list with a range to select a contiguous window of elements, or with two ranges to select a 2D rectangle inside a list of rows, just as in Expressions. Assigning to a window replaces all of its elements at once with a list of the exact same length. Because the right-hand side is read in full before any element is written, you can safely shift a slice of a list onto an overlapping window of the very same list:
var row: [8 of Number<u8>] = 0
var grid: [3 of [4 of Number<u8>]] = 0
var left: Number<0...4> = 0
when System.keyboard.space.pressed
row[left..<(left + 4)] = [9, 9, 9, 9]
row[0...3] = row[1...4]
grid[0..<2, 1..<3] = [[1, 2], [3, 4]]
endIf the list on the right does not match the window's size, the compiler catches the mismatch with wrongLength.
Places That Cannot Be Assigned
Only a var variable—or a field, element, or window inside one—can receive a new value with =. Constants and <- bindings are fixed rules rather than storage boxes, so assigning to either is flagged with notAssignable.
Hardware outputs such as System.screen.border always take their values from a top-level or case-level <- binding, never from a statement inside a handler. Writing System.screen.border = .red inside a handler raises notAssignable, and writing System.screen.border <- .red inside a handler raises actionNeeded. To change what an output shows when an event happens, have the handler update a var and wire the output's binding to read that variable:
var alarm: bool = false
when System.keyboard.space.pressed
alarm = not alarm
end
System.screen.border <- if alarm then .red else .black endEvery variable must be declared with var before you assign to it. Assigning to an undeclared name raises assignUndeclared, which keeps a typo on the left of = from silently inventing a brand-new variable.
Likewise, a hardware register declared with at is driven by a <- binding rather than assigned in a handler: assigning to one raises registerAssigned (or hardwareBound when it already has a binding). Declarations shows how placed hardware registers work.
Local Variables
Inside a handler, function, or loop, you can declare a temporary working variable with var. A local var always starts with an initial value after =, and may optionally include a : Type annotation.
local-variable ::= 'var' name ( ':' type )? '=' expression newlineA local variable is visible from the line that declares it down to the end of its enclosing block—whether that block is the whole handler, one branch of an if, one case of a match, or the body of a for loop. Each time that block runs, the local variable starts fresh with its initial value.
var best: Number<0...9> = 0
when System.keyboard.space.pressed
var steps: Number<0...9> = 0
for i in 0..<3
steps = steps +| 4
end
best = steps
end
System.screen.text[0][0..<1] <- best.digits(1)To keep every name unambiguous on the page, a local variable cannot reuse a name that is already in view in an outer scope (shadowing). And once a block closes with end, its local variables go out of view; referring to one after its block ends raises unknownName.
If Statements
An if statement tests one or more conditions in order and runs the first branch whose condition holds true. Each condition is followed by then, and the entire if closes with a single end.
if-statement ::= 'if' condition 'then' newline statement* else-branch? 'end' newline
| 'if' condition 'then' statement
( 'else' 'if' condition 'then' statement )*
( 'else' statement )? 'end' newline
else-branch ::= 'else' 'if' condition 'then' newline statement* else-branch?
| 'else' newline statement*Writing else if on one line continues the same if statement with another test, so a whole ladder of else if branches still needs only one end at the bottom. An else branch at the end catches the case where none of the conditions held true. When you leave else off and no condition is true, the if statement simply does nothing and execution moves on.
var level: Number<0...99> = 0
var shade: Color = .black
when System.keyboard.space.pressed
level = level +% 7
if level > 60 then
shade = .red
else if level > 30 then
shade = .yellow
else
shade = .green
end
end
System.screen.border <- shadeNote that if you put else on its own line and start a new if on the line below it, you have opened a nested if inside the else branch, and each if will want its own end.
Every if condition must evaluate to a bool (or an event, which acts as a bool for the current frame). The compiler catches a numeric condition with conditionNeedsBool, an unwrapped optional without = with bareOptional, and a missing then keyword with ifNeedsThen.
One-Line Form
For short, punchy decisions, you can place a statement right after then on the very same line. In this one-line form, the whole if—including any else if and else branches, each holding a single statement—stays on that one line and finishes with end.
var taps: Number<0...9> = 0
when System.keyboard.space.pressed
if taps == 9 then taps = 0 else taps = taps + 1 end
end
System.screen.text[0][0..<1] <- taps.digits(1)If a one-line if spills across a newline or tries to pack a for loop onto the line, the compiler flags it with inlineIfLine.
Conditional Names
An if condition can check whether an optional holds a value and unwrap it into a fresh name in a single step. Write if name = optional then: the branch runs only when optional is not nil, and inside that branch name holds the unwrapped value with its plain non-optional type.
condition ::= expression | name '=' expression ( 'and' expression )?Right after the unwrap, you can chain and with an extra test that uses the newly bound name. The machine evaluates the and test only when the optional was present, and runs the branch only when both succeed:
var held: Number<u8>? = nil
var shown: Number<u8> = 0
when System.keyboard.space.pressed
held = 7
end
when System.keyboard.return.pressed
if n = held and n > 3 then
shown = n
else
shown = 0
end
end
System.screen.text[0][0..<3] <- shown.digits(3)The unwrapped name lives only inside the branch opened by that then—it is not in scope inside an else branch or after the if ends. (When you want an unwrapped name to stay in scope for the rest of a handler, reach for guard name = optional, as Events and Handlers describes.)
A conditional unwrap binds a single optional name and must stand at the very start of the condition. The compiler catches malformed unwraps with these messages:
| Form | Key |
|---|---|
if n = held or t > 3 then | orAfterAssignment |
if t > 3 and n = held then | secondAssignment |
if n = held = t then | secondAssignment |
if n = held when t > 1 then | whenInCondition |
if n = taps then, where taps is not optional | alwaysSucceeds |
Match Statements
A match statement compares a value against a list of patterns from top to bottom and runs the statements of the first case that fits. Each case writes its pattern, an optional when condition, an arrow ->, and the statements to run.
match-statement ::= 'match' expression newline match-case+ 'end' newline
| 'match' newline condition-case+ 'else' '->' case-body 'end' newline
match-case ::= pattern ( 'when' expression )? '->' case-body
| 'else' '->' case-body
condition-case ::= 'when' expression '->' case-body
case-body ::= statement | newline statement*
pattern ::= name | name '(' field-pattern ( ',' field-pattern )* ')'
| number | 'true' | 'false' | 'nil'
field-pattern ::= name ( ':' name )?You can put a single statement on the same line right after ->, or place a whole block of statements on the lines below ->. Leaving a case empty after -> is fine too—it simply does nothing when matched. Mixing both styles on the same case (putting a statement right after -> and more statements on the lines below it) is caught with statementAfterArm.
enum Mode
case title, playing, over
end
var mode: Mode = .title
var lives: Number<0...3> = 3
when System.keyboard.space.pressed
match mode
title -> mode = .playing
playing when lives == 0 -> mode = .over
playing ->
lives = lives - 1
over ->
lives = 3
mode = .title
end
endA match used as an expression to compute a value uses the exact same pattern rules (see Expressions), while a match at the top of a program switches between cases (see Events and Handlers).
Patterns
Each pattern describes which values its case accepts:
- a bare case name from an enum or named set, such as
title, matches that case; - a case name with fields in parentheses, such as
moving(grab, column: chosen), matches that case and unpacks its fields—either under each field's own name or under a local alias after:; - a literal number,
true,false, ornilmatches that exact value; - any other identifier matches every remaining value and gives it a name inside the case body (and when placed after a
nil ->case on an optional, that name holds the unwrapped non-optional value); elsematches every remaining value without naming it.
struct Spot(x: Number<0...319>, y: Number<0...199>)
enum Drag
case still
case moving(grab: Spot, column: Number<0...39>)
end
var drag: Drag = .still
var column: Number<0...39> = 0
var held: Number<u8>? = nil
var last: Number<u8> = 0
when System.screen.frame
match drag
moving(grab, column: chosen) ->
column = chosen
drag = .still
still ->
end
match held
nil -> last = 0
value -> last = value
end
endEvery Value Needs a Case
Every match must account for all possible values of the expression it tests. If any value is left unhandled, the compiler catches it with missingCoverage and lists the missing cases for you.
Because a when guard on a case might turn out to be false at runtime, a guarded case does not count as handling its pattern for all inputs—you still need an unguarded case (or else) below it. Conversely, writing a second unguarded case for a value that an earlier unguarded case already handled creates dead code, which the compiler flags with duplicateArm. And writing a nil case when the value being matched is not an optional raises nilArm.
Conditions on Cases
Adding when condition after a pattern narrows that case so it matches only when the pattern fits and the condition is true. The condition is free to read any names that the pattern unpacked. If the pattern fits but the when condition is false, the machine keeps scanning down to the remaining cases below.
var hits: Number<0...9> = 0
var shade: Color = .black
when System.keyboard.space.pressed
hits = hits +% 1
match hits
0 -> shade = .black
n when n > 5 -> shade = .red
else -> shade = .green
end
end
System.screen.border <- shadeMatch Without a Value
Write match with nothing after it on the header line when you want a clean multi-way table of boolean conditions. Each arm starts with when condition ->, and the final arm is else ->, which runs if none of the conditions above it held true.
var level: Number<0...99> = 0
var shade: Color = .black
when System.keyboard.space.pressed
level = level +% 9
match
when level > 60 -> shade = .red
when level > 30 -> shade = .yellow
else -> shade = .green
end
end
System.screen.border <- shadeA valueless match always requires at least one when case and a closing else case; leaving either out raises conditionalMatch.
For Loops
A for loop steps through each number in a range or each element in a list from start to finish, running its body once per step. The loop variable named after for holds the current item on each turn.
for-loop ::= 'for' name 'in' ( range | expression ) newline statement* 'end' newline
break-statement ::= 'break' newlineOver a Range
for i in 0..<8 runs eight turns with i counting from 0 through 7, while the inclusive range for i in 1...3 runs three turns with i counting from 1 through 3. Inside the loop body, i has the exact bounded type Number<low...high> of the range, which means you can use i to index into a list of matching size with zero runtime bounds checks. Both ends of a for range must be whole numbers known at compile time—literals, constants declared above, or parenthesized arithmetic on them, as Range Bounds describes.
const size = 8
var row: [size of Number<u8>] = 0
when System.keyboard.space.pressed
for i in 0..<size
row[i] = row[i] +% i
end
end
System.screen.text[0][0..<3] <- row[7].digits(3)The compiler flags a runtime-varying range bound with rangeEndNotFixed, a fractional bound with forRangeNeedsWhole, and an empty range (such as 5..<5 or 5...3) with rangeEmpty.
Over a List
for x in xs visits each element of the list xs in order from first to last, giving x the element type of the list. You can loop over a list variable, a list constant, a bracketed list literal, or any expression that produces a list.
var scores: [4 of Number<0...99>] = [3, 1, 4, 1]
var tally: Number<0...999> = 0
when System.keyboard.space.pressed
tally = 0
for s in scores
tally = tally + s
end
end
System.screen.text[0][0..<3] <- tally.digits(3)Because for x in xs reads elements directly from xs as it walks through memory, the loop body cannot modify xs while iterating over it (loopListChanged). When you want to update a list's elements in place, loop over its indices instead—for i in 0..<n—and assign to xs[i].
Trying to run a for loop over something that is neither a range nor a list raises forNeedsList.
The Loop's Name
The loop itself advances its counter or element name at the start of every turn, so the body is not allowed to assign a new value to the loop variable (loopVariableSet). Like any other local variable, the loop's name cannot shadow an existing name in scope, and it disappears once the loop reaches end.
Break
break jumps out of the innermost for loop immediately, continuing execution at the first statement after that loop's end. You can place break inside an if or match within the loop body whenever you want to stop searching early.
var xs: [8 of Number<u8>] = [3, 1, 4, 0, 5, 9, 2, 6]
var before: Number<0...8> = 0
when System.keyboard.space.pressed
before = 0
for x in xs
if x == 0 then break end
before = before + 1
end
end
System.screen.text[0][0..<1] <- before.digits(1)Using break outside of any for loop raises breakOutsideLoop, and using break inside a forever loop raises breakInForever (since a forever loop is ended with guard or a case change instead).
Loops and the Frame
Unless a handler's for loop contains a wait, all of its turns run within a single television frame. The compiler adds up the worst-case cycle cost of the loop across all its turns and flags any loop that could overrun a PAL or NTSC frame budget with loopOverrun. Costs and the Frame explains how the compiler budgets loop cycles and how a work job can spread a heavy calculation across several frames.
An op computes a streaming signal expression every frame and cannot hold a for loop (loopInOp).
Calls and Device Actions
When a call stands on a line of its own as a statement, it performs an action: striking a note on a sound voice, moving a text cursor, adding an item to a list or queue, or running a mut fn method on a struct.
call-statement ::= place '(' ( argument ( ',' argument )* )? ')' newline
argument ::= ( name ':' )? expressionHere are the built-in statement actions and member calls:
| Call | What it does |
|---|---|
voice.play(tone) | strikes a tone on a sound voice |
voice.release() | lets a voice's note fade |
region.print(text), region.println(text) | prints characters at a region's cursor |
region.newline(), region.home(), region.clear() | moves the cursor, or clears the region |
region.cursor.move(row:, col:) | places a region's cursor |
list.add(value), list.remove(at:) | fills or empties a place of a list of optionals |
queue.add(value) | adds a value to a queue |
value.name(...) | calls a mut fn member, which may change value |
const voice = System.sound[1].voice[1]
struct Score
var points: Number<0...999> = 0
mut fn award()
points = points +| 10
end
end
var score = Score()
when System.keyboard.space.pressed
score.award()
voice.play(Tone(.c4, decay: .short))
System.screen.home()
System.screen.print(score.points.digits(3))
end
when System.keyboard.space.released
voice.release()
endThe System Device Tree describes each device's actions and arguments in detail. Device actions like striking a sound note happen only in response to an event, so calling play inside a pure function is caught with playOutsideOn.
Statements in Functions
An ordinary fn function computes and returns a value purely from the parameters you pass in. Inside its body you are free to declare local var variables and step through for loops to build up your answer, while your program's top-level variables and hardware devices stay safely off-limits—trying to assign to a program variable inside a function raises functionSets. Declarations describes function declarations in full.
At the outer level of a fn body, an if or match is an expression that produces the function's return value (see Expressions), which is why a top-level if in a function always needs an else branch. Inside a for loop within that function, however, if and match act as ordinary statements that update your local variables. Inside a fn body, const name = value introduces a fixed local calculation; its expression is evaluated when that line runs. Writing name = value updates an existing local var. A fixed local calculation cannot be reassigned and does not update automatically when its inputs change. Trying to assign a second time to a local constant is caught with localImmutable.
fn capped(xs: [4 of Number<u8>]): Number<u16>
var kept: Number<u16> = 0
for x in xs
if x > 100 then
kept = kept + 100
else
kept = kept + x
end
end
kept
end
result <- capped([1, 2, 200, 4])
System.screen.text[0][0..<3] <- result.digits(3)A mut fn member function on a struct is built for state changes from top to bottom: its entire body consists of statements, and it can freely update the fields of its struct.
The System Device Tree
Open the lid of the machine and you meet its working parts: the video chip painting the television screen, the three-voice sound synthesizer, the keyboard matrix, the control ports waiting for joysticks, paddles or a mouse, and the timers ticking away inside the interface chips. In Adder, every one of those parts lives at a clear, readable path under System—such as System.screen.border for the frame around the picture or System.joystick[2].fire for the red button on port 2. You write the full System path wherever you touch a part of the hardware (or give it a short nickname with a constant), and no variable you declare can accidentally shadow a device. This chapter tours the whole machine: every device, its members and their types, what happens when you read or bind each one, and what it costs in bytes and cycles when the cost report measures it.
device-path ::= 'System' ( '.' name | '[' expression ']' )*Almost every member you meet on the device tree works in one of four ways:
| Kind | What a program does with it | Examples |
|---|---|---|
| reading | reads a fresh value that the machine samples at the start of each frame | System.joystick[2].dx, System.paddle[1] |
| event | responds with when, or filters and counts it as a stream | System.screen.frame, System.keyboard.stroke |
| output | binds a live value to it with <- | System.screen.border, System.screen.sprite[0] |
| action | calls it as a step inside a handler | voice.play(tone), region.print(text) |
The remaining members are makers: canvas(...) and region(...) carve out a pixel canvas or a text window on the screen, while System.clock.interval(...) and System.clock.timer(...) set up timed clock events. System.screen.interrupt(line:) and System.clock.interrupt(ms:) are events of a special breed: a when handler for either one is a hardware interrupt routine. To keep your logic steady from top to bottom, the program takes all of its hardware readings once at the very start of each frame, before any handler runs. See Outputs for how output bindings work, and Events and Handlers for everything you can do with events.
Paths
Devices With Numbers
Whenever the machine has more than one of the same unit—two joystick ports, two paddles on a plug, three voices on the sound chip—you pick the one you want with its hardware number in brackets, just like indexing a list. The two control ports on the side of the case are numbered 1 and 2, so the joysticks are System.joystick[1] and System.joystick[2]. If you leave the bracketed number off, the compiler reminds you with instanceNeeded; if you ask for a number the machine does not have, it catches that with noInstance and lists the valid numbers right in the message.
| Device | Numbers |
|---|---|
System.joystick[n] | 1 and 2, the control ports |
System.paddle[n] | 1 and 2, the pair of paddles on control port 1 |
System.sound[n] | 1, the sound chip |
System.sound[1].voice[n] | 1, 2 and 3 |
You do not have to hard-wire the number if you want the player to pick a port or cycle through voices on the fly: any variable whose range matches those exact numbers can sit inside the brackets. When a program picks a joystick through a variable this way, it samples both control ports every frame so either reading is ready on demand.
var port: Number<1...2> = 2
when System.keyboard.one.pressed
port = 1
end
System.screen.border <- if System.joystick[port].fire then .red else .black endNaming a Device
Typing System.joystick[2] over and over can wear out your fingers, so a top-level constant can give any device—or any branch of one—a short, friendly alias. Because a device is a physical place in the machine rather than data you can copy around, the compiler keeps you from storing a device in a variable, binding it as a value, putting it in a list, or passing it as a function argument (deviceNotValue).
const screen = System.screen
const stick = System.joystick[2]
screen.border <- if stick.fire then .red else .black endDevices Not Built Yet
A few paths on the tree reserve spots for expansion hardware that Adder does not drive yet: System.sound[2] for a second SID chip, System.reu and System.georam for RAM expansion cartridges, and System.lightpen for a light pen. If you touch one of them today, the compiler lets you know with notBuilt and links no code for it.
The Screen
System.screen is your television display, driven by the VIC-II video chip. Here is everything hanging off System.screen:
| Member | Kind | Type |
|---|---|---|
border | output | Color |
background | output | Color |
text | output | [25 of [40 of ScreenCode]] |
ink | output | [25 of [40 of Color]] |
font | output | Charset |
scroll | output | (x: 0...7, y: 0...7), described under Scroll and Window |
columns | output | 40 or 38 |
rows | output | 25 or 24 |
sprite | output | [8 of Sprite], described under Sprites |
frame | event | Event |
frames | reading | Number<u16> |
standard | reading | Number<0...2> |
frame_cycles | reading | Number<16768...19656> |
canvas(...) | maker | a canvas, described under Canvases |
region(...) | maker | a region, described under Regions and Printing |
The whole screen also acts as a text region in its own right, so it comes equipped with all the printing tools of a region: print, println, shown, cursor controls, and the rest.
Border and Background
border sets the color of the wide frame around the active picture, and background sets the paper color behind the characters. Without a binding, the machine greets you with its classic Commodore livery: a light blue border around a deep blue background. Want horizontal stripes of color down the screen? Color Bands shows how to tint individual raster ranges of either one.
var hue: Number<0...15> = 0
when System.keyboard.space.pressed
hue = hue +% 1
end
System.screen.border <- Color(hue)
System.screen.background <- .blackIn the cost report for this little program, the live border binding takes 20 bytes and at most 27 cycles per frame. The constant background <- .black line is set once at start-up: it takes 3 bytes of code and costs 0 cycles during the frame loop.
Text and Ink
text is the classic 25-row by 40-column character grid, where each cell holds one screen code. You can bind the whole 25-by-40 grid at once, a single row, a horizontal slice of a row, or a single cell, using the index and range forms from Expressions. Drop a string literal into a row slice of matching length and the compiler automatically translates its characters into screen codes for the active font. Both the row index and the starting column of a slice can move while the program runs; only the slice's width has to be fixed so the compiler knows how many cells to write.
var row: Number<0...24> = 12
when System.joystick[2].fire.pressed
row = row +% 1
end
System.screen.text[row][15..<20] <- "ADDER"
System.screen.text[0][0] <- .block
System.screen.ink[row] <- .yellowink is the matching 25-by-40 grid of foreground colors—one color nibble for each character cell's lit pixels. Because a single value bound to a list fills every slot of that list, ink[row] <- .yellow paints an entire row yellow and ink <- .yellow paints every character on the screen. If you bind a list of values instead, its length has to match the target slice; if they disagree, the compiler points out the mismatch with wrongLength.
font selects the character set that shapes every cell on the text screen. By default, Adder starts in Charset.lowercase, which gives you both lowercase and capital letters; switching to Charset.uppercase gives you uppercase letters alongside the full suite of PETSCII box-drawing and card-suit graphics.
System.screen.font <- Charset.uppercase
System.screen.text[12][17..<22] <- "HELLO"If you would rather place strings by row and column without counting out slice lengths by hand, see Text Placement.
Scroll and Window
scroll nudges the entire text grid and its colors smoothly by x pixels right and y pixels down, each from 0 to 7. Without a binding, the video chip rests at its normal home offset of (x: 0, y: 3). By feeding scroll from a variable and shifting characters by one cell whenever the pixel offset wraps past 7, you can build silky-smooth pixel scrolling.
columns and rows control the video chip's framing aperture. Switching to columns <- 38 and rows <- 24 pulls the border inward by a character width on the sides and a character height at the top and bottom (leaving pixels x 7 to 310 and y 4 to 195 visible), which neatly masks the seam where new characters pop in during a scroll. Each aperture setting takes one of its two fixed sizes, written as a constant in your program.
var shown_x: Number<0...255> = 0
System.screen.columns <- 38
System.screen.rows <- 24
System.screen.scroll <- (x: 7 - shown_x % 8, y: 3)
when System.screen.frame
shown_x = shown_x +% 1
endThe runtime latches the new scroll values at the very top of the next frame while the beam is still in the border, so the picture never tears mid-screen. A constant scroll and a narrowed 38-column or 24-row window are configured once at start-up and cost 0 cycles per frame; a live scroll that changes at run time costs just two loads and two stores per frame. If you bind a changing scroll while leaving the screen at the full 40 columns or 25 rows, the cost report adds a friendly reminder on that line noting that the character seam will show at the edge and showing the exact columns <- 38 or rows <- 24 line that hides it. Plain border and background bands preserve a changing vertical scroll. Bands whose interrupt needs raster line 256 or later, authored assembly, custom interrupts and cases still cannot share that changing vertical scroll. Programs that display a bitmap canvas or cases with different text modes do not use scroll.
Frames and the Video Standard
System.screen.frame is the heartbeat of an Adder program: it fires once at the beginning of every television frame—50 times a second on a European (PAL) machine and 60 times a second on a North American (NTSC) machine. System.screen.frames is a 16-bit counter that ticks up from 0 on each frame and wraps cleanly from 65,535 back to 0.
When your program boots, it measures the video chip against the timer to see what kind of machine it is running on. standard reports what it found: 0 for PAL, 1 for standard NTSC, and 2 for the early 64-cycle-per-line NTSC video chip found in the earliest silver-label machines. frame_cycles gives you the exact processor cycle budget of one frame on that hardware: 19,656, 17,095, or 16,768. Both values are measured once at start-up and stay constant for the life of the run.
var ticks: Number<0...9> = 0
when System.screen.frame
ticks = ticks +% 1
end
System.screen.border <- if System.screen.standard == 0 then .blue else .red end
System.screen.background <- Color(System.screen.frames % 16)
System.screen.text[0][0..<1] <- ticks.digits(1)Color Bands
Want a sunset sky behind your title screen, a black status bar at the top, or a copper-style striped border? Putting a pixel-row range in brackets after border or background creates a color band that paints just those scanlines. Row 0 is the very first pixel row of the main 200-line picture window, so the picture itself runs from row 0 down to row 199. Because the border extends above and below the main window, a border band can also reach into rows -51 to -1 in the upper border and rows 200 to 210 in the lower border—the visible border rows shared by every PAL and NTSC machine. A background band stays within the main window, rows 0 to 199.
var hue: Number<0...15> = 0
when System.keyboard.space.pressed
hue = hue +% 1
end
System.screen.border <- .blue
System.screen.border[-51..<0] <- .black
System.screen.border[49...98] <- Color(hue)
System.screen.background[96...199] <- .greenYou write the band's vertical span as a single row or a range (a...b or a..<b) with constant endpoints known at compile time. The color expression on the right of <- can be as lively as you like; when it changes, the new color appears on those scanlines starting on the next frame. An unindexed binding on the same output (like System.screen.border <- .blue above) sets the base color for every row not claimed by a band; if you omit the base binding, unclaimed rows keep the machine's default startup color. Two bands on the same output cannot overlap on the same scanline.
To make sure every raster split fires cleanly on every television standard, the compiler checks your band ranges up front:
- if a band endpoint is a changing variable rather than a compile-time constant, it flags
rangeEndNotFixed; - if a row falls outside the valid range (
-51...210forborder,0...199forbackground), it flagsrasterSelection; - if a border band reaches past row
210into scanlines that only PAL televisions have, it flagsrasterLine; - if two bands on the same output share a scanline, it flags
overlap.
Under the hood, each top and bottom edge of a color band is driven by a hardware raster interrupt: the video chip taps the processor on the shoulder at that exact scanline to swap the color register. In a program with a single border band, the cost report charges 67 bytes and 114 cycles per frame for the pair of interrupts. You can freely mix color bands with fixed-line interrupt routines—the compiler weaves them into a single sorted raster chain. However, a self-scheduling interrupt routine that writes its own raster lines needs full control of the raster interrupt vector (rasterOwner), and because bands use the hardware timer setup to stabilize their timing, they cannot share a program with a clock interval or timer (periodicOwnership).
Regions and Printing
A region is a rectangular window on the text screen with its own independent cursor, ink color, wrapping rules, and scrolling. System.screen itself is the root region spanning all 25 rows and 40 columns, and calling parent.region(row:, col:, width:, height:) carves out a sub-window inside any parent region.
All four geometry arguments are compile-time constants measured in character cells from the parent region's top-left corner. If you leave col out, it defaults to 0; if you leave width or height out, the region stretches to the right or bottom edge of its parent. The compiler checks your window layout at build time: if a region sticks out past its parent's edges, you get regionOutside, and if two sibling regions of the same parent claim the same character cell, you get regionOverlap.
const status = System.screen.region(row: 0, height: 1)
const log = System.screen.region(row: 1, height: 3, width: 12)
var entries: Number<0...999> = 0
status.text <- Text(row: 0, col: 0, content: "COUNT:")
when start
log.cursor.ink = .light_green
end
when System.screen.frame.every(30)
entries = entries +% 1
log.println(entries.digits(3))
endWhen you declare a region inside a case, it belongs to that case: it starts fresh and blank each time the case begins, and when the case finishes, whatever was on the screen underneath shows through again. See Events and Handlers for how cases work.
Region Members
| Member | Kind | What it does |
|---|---|---|
print(text) | action | prints the characters at the cursor and leaves the cursor right after them |
println(text) | action | prints the characters, then advances the cursor to the start of the next line |
newline() | action | moves the cursor to the start of the next line |
home() | action | moves the cursor back to the region's top-left cell (0, 0) |
clear() | action | fills the region with spaces and moves the cursor home |
wrap | variable | a TextWrap: .character (the starting setting), .word or .hyphenated |
cursor.row, cursor.col | reading | the cell coordinates where the next printed character will land |
cursor.move(row:, col:) | action | teleports the cursor to a specific cell inside the region |
cursor.ink | variable | the Color used for characters printed from now on |
cursor.reverse | variable | a bool: whether newly printed characters appear in reverse video |
text | output | takes a Text placement positioned in the region's own coordinates |
shown | event | fires when everything printed to the region has been copied onto the visible screen |
region(...) | maker | carves out a smaller sub-region inside this one |
Inside any when handler, you can update wrap, cursor.ink, and cursor.reverse with =, and your settings stay in effect for future prints until you change them again.
Printing
print and println accept any list of Char or ScreenCode values, including string literals. Adder never guesses how many digits or leading spaces you want for a number, so passing a bare number to print is caught with printList—just call .digits(n) on the number to format it into characters first.
diagonal <- ScreenCode(Number.random(77...78))
System.screen.font <- Charset.uppercase
when System.screen.frame
System.screen.print([diagonal])
endAs characters stream into a region, a line that reaches the right edge wraps onto the row below. When the cursor is at the bottom row and needs a fresh line, the region scrolls its contents up by one row and blanks the new bottom line. Even better, a trailing newline at the very bottom of a region waits politely until the next character is actually printed before scrolling, so the last line you printed never scrolls off the top before the player can read it. In .word wrap mode, words that would cross the right margin hop down to the next row intact, while a single word wider than the entire region splits across rows. Strings hold visible characters rather than hidden control codes: use the explicit newline(), home(), and clear() actions whenever you want to move the cursor or wipe the window.
Every print formats its characters into the region's buffer right inside the handler that called it, during that same frame. The compiler calculates the worst-case cycle cost of the longest string that print can receive—including word wrapping and scrolling—and verifies that the frame budget can hold it, as explained in Costs and the Frame. If a large region takes more than one frame to copy its buffer out to the screen, you can trigger your next batch of output on shown to print as fast as the display can keep up:
when System.screen.shown
System.screen.println("HELLO, WORLD!")
endPartial region updates
partial before a region declaration permits its updates to appear one complete row at a time. Each row shown keeps characters and ink from the same captured state; a later print cannot change a waiting row. Other areas keep their whole-image promise.
partial const log = System.screen.region(row: 12, width: 8, height: 3)
when log.shown
log.println("OK")
endarea-declaration ::= area-modifiers name '=' expression
area-modifiers ::= 'partial' ( 'fast' | 'small' )?
| ( 'fast' | 'small' ) 'partial'It combines with one fast or small preference, in either order. Plain printing regions with a fixed font use a private row, prepared in one frame and shown behind the beam in another. Shared colors and unmarked colored neighbors keep their existing updates; cases, overlapping layers and changing fonts retain their whole-image route. The cost report gives a shared showing bound after the final write; continuing writes can replace work. A hires partial canvas declared outside cases can show complete pixel rows and their cell colors while placed pictures change. Its row is captured before the following frame shows it; the report bounds when all its rows settle after the final change. An outline-only hires canvas keeps its ordinary resumable preparation in a private image, then captures the complete rows; its report includes the preparation and row-pass debt. A partial printing region using the default machine font beside a hires canvas with constant ink/paper and no drawing list also captures its logical characters and inks into a private glyph row, then shows that complete row at the following border. Its cursor, reverse, wrap and entered-line printing retain their ordinary meanings. An unmarked printing region in that fixed backdrop uses a real second bitmap and color matrix, keeps the old complete picture while rows are prepared, then switches the complete image at a border. It is refused with actual storage quantities when that second image cannot fit. Hidden catch-up after a flip does not keep the already complete visible region waiting. An unmarked default-font log can also share a whole hires picture with one root placed-picture engine and fixed ink/paper: each accepted drawing and captured log image finishes together while newer requests wait for the next picture. Source handlers keep running during hidden catch-up. Mixed picture/outline lists, multicolor canvases, canvases in cases and the remaining bitmap printing combinations still need their joined update paths and remain refused.
Text Placement
While print acts like a teletype cursor, Text(...) is for declarative labels and text boxes: it places characters at a given row and col, and binding it to System.screen.text (or a region's .text) keeps that text live on screen. You can also bind a list of Text(...) placements at once; if two placements overlap, a later item in the list paints over an earlier one.
var score: Number<0...999> = 0
when System.keyboard.space.pressed
score = score +| 10
end
System.screen.text <- [
Text(row: 2, col: 4, content: "SCORE"),
Text(row: 2, col: 10, content: score.digits(3)),
]| Argument | Type | Meaning |
|---|---|---|
row, col | whole numbers | the top-left cell, which may move while the program runs |
width | a positive constant | the column width of a fixed-size view |
height | a positive constant | the row height of a view; requires width |
overflow | TextOverflow | .clip (the default) or .wrap; wrapping requires both width and height |
wrap | TextWrap | .character (the default), .word or .hyphenated |
ink | Color | optional foreground color for the placement's cells |
content | text | a string, a list of Char or ScreenCode, an if/match choosing between them, or a list of horizontal parts |
Just as with print, a bare number is not content—use .digits(n) to turn it into characters first. To place several strings and values side by side on the same row without counting column offsets by hand, pass them as trailing positional arguments after row: and col: (or as a list in content: [...]). Each part is laid out immediately after the previous one's width, and any part can be wrapped in (part, ink: color) to give just those cells their own color while the rest use the placement's ink: (if any):
var score: Number<0...9999> = 42
System.screen.text <- Text(
row: 0, col: 2, ink: .light_blue,
"SCORE ", (score.digits(4), ink: .yellow),
)The compiler checks the box geometry up front and flags textLayout if a placement's rectangle would spill off the screen, if you give a height without a width, or if you ask for overflow: .wrap without specifying both width and height.
A placement without a width is a label: it is simply as wide as its current content (clipped at the right edge of the screen). If a label's content shrinks—say, when a message changes from a long word to a short one—Adder automatically restores whatever characters lay underneath the vacated cells (or fills them with spaces). A placement with a width is a view: a rectangular box that holds its size steady as its content changes, padding any unused cells with spaces. By default a view clips at its right edge; with overflow: .wrap, long text flows onto the rows below inside the box. .word breaks lines cleanly at spaces between words, and .hyphenated also hyphenates any single word that is wider than the view itself.
When you omit ink:, Text touches only character cells and foreground colors come from your ink bindings. If a large multi-row layout takes more than a single frame to typeset and copy to the screen, Adder spreads the redraw across frames automatically, and if content changes again in the meantime, the newest layout cleanly supersedes any unfinished redraw.
Alignment and Justification
Two chainable methods let you align or justify text inside a view, returning a fresh placement without mutating the original: aligned(a) takes a TextAlignment—.leading, .center, or .trailing—and positions each line within the view's width. justified() spreads extra spaces evenly between words on every wrapped line that continues onto the row below (leaving the final line of a paragraph ragged, just like book typography); it requires .word or .hyphenated wrapping. If you chain more than one alignment or justification call on the same placement, the last one in the chain wins.
System.screen.text <- [
Text(row: 0, col: 0, width: 16, content: "RADIO").aligned(.center),
Text(row: 2, col: 0, width: 10, height: 2, overflow: .wrap,
wrap: .word, content: "A B C LONGWORD").justified(),
]Programs that use Text, TextWrap, TextAlignment, or region printing automatically link in Adder's text layout engine; programs that don't use them pay zero bytes for it.
Multicolor Text
Bind System.screen.multicolor to choose the two shared colors for the whole text screen. This switches the video chip into multicolor character mode. A cell with ordinary ink still uses hires pixels; .multicolor(color) selects pairs of horizontal pixels for that cell. Both choices take a color from the first eight palette colors (Color.black through Color.yellow), because the cell's color bit also records which choice it uses.
System.screen.multicolor <- (first: .green, second: .brown)
System.screen.text[0][0..<2] <- "AB"
System.screen.ink[0][0] <- .yellow
System.screen.ink[0][1] <- .multicolor(.yellow)The shared colors may follow changing values and appear together at the frame border. A cell can choose hires or multicolor ink with if or a named binding. The compiler refuses a color outside the first eight for either choice. The whole-screen binding cannot be chosen separately inside a case until its text, ink, colors and mode can change together.
Calling System.screen.region(multicolor: true) also selects multicolor text for a region. Its ink uses the first eight colors; the compiler adds the cell's selection bit. Hires text beside that region keeps to the first eight colors too. A color from Color.orange through Color.light_grey is reported with multicolorInk or multicolorBeside, as appropriate.
const colors = System.screen.region(multicolor: true)
System.screen.text[10][14..<20] <- "COLORS"
System.screen.ink[10][14..<20] <- .yellowEach case can display either multicolor text or a bitmap canvas, and a multicolor region declared inside a case applies only while that case is active.
Canvases
When character cells are not enough and you want to plot individual pixels, stamps, outlines, and tiled fills, a canvas switches the video chip into bitmapped graphics mode—320 pixels wide by 200 pixels tall. You declare one with System.screen.canvas(...) and bind its members through the constant name you give it.
| Member | Kind | Type |
|---|---|---|
drawings | output | a list of Drawing? |
ink | output | Color, .white by default |
paper | output | Color, .blue by default; on a hires canvas only |
shown | event | Event |
| Argument | Meaning |
|---|---|
multicolor: true | double-wide pixels (160 across by 200 down) with more colors per cell |
row, col | top-left character cell of the canvas (0 when left out) |
width, height | size of the canvas in character cells (fills the rest of the screen when left out) |
const picture = System.screen.canvas()
const badge = Picture(["XXXXXXXX", "X......X", "X......X", "XXXXXXXX"])
x: Number<0...312> <- System.joystick[2].dx.sum(from: 80)
picture.ink <- .yellow
picture.paper <- .blue
picture.drawings <- [badge.place(x: x, y: 40)]
System.screen.sprite[0] <- (x: 160, y: 130, color: .white)On a standard high-resolution (hires) canvas, each 8-by-8 pixel cell displays two colors: ink for lit (1) pixels and paper for clear (0) pixels. On a multicolor: true canvas, clear pixels show System.screen.background instead, so a multicolor canvas has no paper output—and conversely, a full hires canvas does not use System.screen.background. If you bind paper on a multicolor canvas or background on a hires canvas, the compiler points you to the right output with bitmapPaper or bitmapBackground.
Because the video chip uses the screen's memory for bitmap pixels, a canvas replaces the character grid where it sits. In a case with a full-screen canvas, touching System.screen.text or System.screen.font is flagged with bitmapText; if the canvas is a smaller window (row:, col:, width:, height:), touching System.screen.text alongside it is flagged with canvasText. A case can hold at most one canvas (canvasMore). A canvas declared at the top level of the program stays on screen for the whole run, while a canvas declared inside a case appears only while that case is active. Hardware sprites float right over a canvas just as they do over text.
const map = System.screen.canvas(row: 2, col: 10, width: 20, height: 12)
map.ink <- .green
map.paper <- .blackPixel coordinates inside a canvas are always measured from the canvas's own top-left corner (0, 0), so the 20-cell-wide map canvas above is 160 pixels wide (x from 0 to 159). If a canvas's row, col, width, and height would push it past the 25-by-40 screen grid, the compiler catches it with canvasLeaves.
Drawings
drawings takes a list of drawing commands, rendered in list order so later items paint on top of earlier ones. Any slot in the list can evaluate to nil (giving the slot type Drawing?), and whenever a slot turns nil, the canvas cleanly erases that drawing and restores whatever was behind it. Helper functions can build and return drawings, and ++ concatenates lists of drawings together.
| Drawing | What it draws |
|---|---|
picture.place(x:, y:) | stamps a picture with its top-left corner at pixel (x, y) |
picture.fill(x:, y:, width:, height:) | tiles a picture across a pixel rectangle, cropping the last repeat cleanly |
points.outline() | draws straight lines connecting a list of Point values and closes the polygon back to the first point |
points.outline().inverted() | draws a closed polygon outline that XOR-inverts the pixels underneath it, rendered on top of normal drawings |
Outline Points count screen pixels relative to the canvas corner. Each endpoint is clipped to its canvas width and height before its screen position is added. On a multicolor canvas every x must be proved even, including later replacements of remembered Points; the last horizontal position is the left pixel of the final pair. Source Point lists and histories keep their local screen-pixel coordinates. | Text(content:, font:).place(x:, y:) | stamps a string using 8-by-8 character glyphs from a font at pixel (x, y) |
Both place and fill also accept flip: .horizontal or flip: .vertical to mirror the picture left-to-right or upside-down without storing a second copy of the art.
const picture = System.screen.canvas()
picture.ink <- .black
picture.paper <- .white
const corner = Picture(["XXXXXXXX", "X.......", "X.......", "X......."])
show <- System.joystick[2].fire
const box: [3 of Point] = [(x: 200, y: 20), (x: 300, y: 30), (x: 250, y: 90)]
picture.drawings <- [
corner.place(x: 32, y: 32),
corner.place(x: 64, y: 32, flip: .horizontal),
Text(content: "SCORE").place(x: 16, y: 8),
if show then box.outline().inverted() else nil end,
]For Text(content:, font:).place(x:, y:) on a canvas, content is a string literal of 1 to 40 characters written right in the program. If you omit font:, it uses Charset.uppercase; you can also pass Charset.lowercase or a custom Charset(file:) loaded from your project folder. On a multicolor canvas, the font must also be created with multicolor: true.
Where a Drawing May Go
Inside a canvas, x counts pixels from the left edge and y counts pixel rows from the top. Every drawing must stay completely inside its canvas: for example, a 24-pixel-wide picture on a 320-pixel-wide canvas needs x between 0 and 296. If the compiler can see at build time that a coordinate is always out of bounds, it catches the mistake right away with placeRange. When the declared ranges of your x and y variables already prove that the drawing can never step off the canvas, the compiler emits zero bounds-checking code. If a variable's type is wider than the safe span (say, Number<u16>), the compiler inserts a tiny run-time bounds check that halts on that line if the drawing would ever leave the canvas—and the cost report notes both the check and how to narrow the variable's range (like x: Number<0...296>) to make the check disappear.
In video memory, each byte of a canvas holds 8 horizontal pixels. Whenever the compiler can prove from your expression that x lands on an 8-pixel byte boundary—such as a constant multiple of 8 or column * 8—it takes a fast byte-aligned path that copies bytes straight into the bitmap without shifting. For arbitrary pixel positions, it automatically pre-shifts or masks the art, and the cost report tells you which path each drawing line uses. On a multicolor canvas, pixels come in pairs, so x must be provably even (such as an even constant or column * 2). If a .fill(...) starts or ends partway through a byte, its width must be a compile-time constant so the edge masks are fixed (placeShifted).
When a Picture carries its own cell colors—such as Picture(art, ink: .red) or a multicolor picture with palette colors—stamping it also colors the 8-by-8 character cells underneath, so its vertical position y must be a multiple of 8 (placeCells).
Drawing Over Frames
You never have to manually erase old positions or flip double buffers on a canvas: Adder tracks what moved or changed since the last frame and redraws only the dirty rectangles. When a drawing moves or turns nil, the runtime automatically repairs the uncovered area from any drawings sitting underneath it, and clears any pixels no longer touched by any drawing. If a big case change or a large .fill(...) needs more cycles than a single frame can spare, the canvas renderer works incrementally across a few frames, pausing cleanly each frame so your handlers and sound never miss a beat.
The picture.shown event fires at the start of every frame in which the canvas is completely caught up—with no incremental drawing left in flight and no new changes pending. Updating your drawing positions inside when picture.shown lets an animation step forward as fast as the bitmap renderer can complete each frame:
const picture = System.screen.canvas()
const badge = Picture(["XXXXXXXX", "X......X", "X......X", "XXXXXXXX"])
var x: Number<0...304> = 0
when picture.shown
x = if x == 296 then 0 else x + 8 end
end
picture.drawings <- [badge.place(x: x, y: 40)]Sprites
The VIC-II video chip gives you eight independent hardware sprites—movable 24-by-21 pixel objects that float over text and bitmap canvases alike without disturbing a single byte of screen memory. System.screen.sprite is the list of all eight, numbered 0 through 7 (with sprite 0 drawn in front of sprite 1, and so on), and each slot takes a Sprite value.
| Field | Type | When left out |
|---|---|---|
x | Number<-24...487> | required |
y | Number<-50...205> | required |
shown | bool | true |
behind | bool | false |
shape | Shape | Shape.square, a solid 24-by-21 block |
color | Color | .white |
Adder translates the video chip's raw hardware offsets for you so that (x: 0, y: 0) places the top-left corner of the sprite right at the top-left pixel of the 320-by-200 picture window. Because a standard sprite is 24 pixels wide and 21 pixels tall, keeping x in 0...296 and y in 0...179 keeps the whole sprite inside the main window, while negative coordinates let you slide a sprite smoothly in from the left or top border. Any sprite you do not bind stays switched off. Set behind: true to put a sprite behind text foreground pixels while leaving it in front of background pixels. In hires text, a glyph's set pixels are foreground. In multicolor text, the second shared color and the cell's own color are foreground; the background and first shared color stay behind the sprite. The priority changes at the frame border with the other sprite fields.
bat: Number<0...296> <- (System.joystick[2].dx * 4).sum(from: 136)
blink <- System.screen.frame.every(25).toggle()
System.screen.sprite[0] <- (x: bat, y: 170)
System.screen.sprite[1] <- (x: 40, y: 40, shown: blink, color: .yellow)When you give a sprite a multicolor shape, its X pixels use that sprite's own color, while its A and B pixels use the two shared sprite multicolor registers that all multicolor sprites on screen share.
Sprites as a List
Instead of writing eight separate bindings, you can bind a constant range of sprites—such as System.screen.sprite[0..<4]—to a list of matching length, or bind System.screen.sprite itself to a list of all eight. Each hardware sprite takes the corresponding element from the list, and if the list holds optional Sprite? values, any slot that evaluates to nil switches that sprite off for the frame.
var xs: [4 of Number<0...296>] = [10, 70, 130, 190]
when System.joystick[2].fire.pressed
xs[0] = xs[0] +% 8
end
System.screen.sprite[0..<4] <- xs.map(x -> Sprite(x: x, y: 50, color: .yellow))Because each hardware sprite maps to specific VIC-II registers, the index or range in System.screen.sprite[...] must be a compile-time constant within 0...7 (spriteIndex). Each sprite can be bound only once at a given scope—whether individually or as part of a slice—so the compiler catches duplicate bindings with spriteBoundTwice and mismatched list lengths with spriteListLength. When you build a sprite list with .map(...), the compiler normally emits one compact loop that updates the sprites in turn; putting fast before the binding unrolls it into direct stores to each sprite's registers, spending a few extra bytes to shave off cycles.
The Keyboard
System.keyboard lets you test individual keys as live switches, process a buffered queue of typed keystrokes, or let the player type and edit a full input line:
| Member | Kind | Type |
|---|---|---|
a key name, such as space or a | reading | bool, true while the key is held down |
[k], where k is a Key | reading | bool, for a key chosen dynamically while the program runs |
stroke | event | Event of Keystroke |
dropped | event | Event |
line | reading | [n of Char], where n is the configured capacity |
line.capacity | output | a constant from 0 to 255 |
line.entered | event | Event of [n of Char] |
caret | reading | Number<0...n> |
All 64 key names on the Commodore keyboard matrix are listed in Named Sets.
Keys
Every key on the keyboard is a boolean reading that stays true for as long as the player holds that key down. Tack on .pressed to get an event that fires in the single frame the key goes down, or .released for the frame it springs back up.
var lives: Number<0...3> = 3
when System.keyboard.space.pressed
lives = lives -| 1
end
System.screen.border <- if System.keyboard.left_shift then .red else .black end
System.screen.text[0][0..<1] <- lives.digits(1)Want customizable game controls where the player can rebind which key jumps or fires? System.keyboard[k] tests a key chosen at run time from a variable, parameter, or list element of type Key. If you already know the exact key when writing the line, write it as a member (System.keyboard.a) rather than in brackets (keyLiteral).
var k: Key = .a
when System.keyboard.space.pressed
k = .z
end
System.screen.border <- if System.keyboard[k] then .red else .black endBehind the cases, the compiler is thrifty with the keyboard matrix: if your program checks only a few fixed keys, it probes just those matrix columns, and if you read enough keys (or index dynamically with [k]) that a full matrix scan is cheaper, it automatically switches to scanning the whole keyboard once per frame.
Keystrokes
When you are building a text adventure, a high-score initials screen, or a menu shortcut handler, System.keyboard.stroke gives you a stream of typed Keystroke values—one event for each key press, including key repeat when a key is held down. Each Keystroke carries key (the Key that was pressed), boolean flags for shift, control, and commodore, and a repeat flag telling you whether the stroke came from auto-repeat. Calling s.character() on a stroke returns its printable Char (with shift already applied), or nil for non-printing keys like the cursor arrows, function keys, RETURN, or DEL.
var shown: Char = .space
when s = System.keyboard.stroke
if c = s.character() then
shown = c
end
end
System.screen.text[12][20] <- shownThe runtime buffers up to ten keystrokes in order so fast typists do not lose letters across busy frames. If ten strokes are already waiting in the queue and an eleventh arrives before your handlers consume them, System.keyboard.dropped fires in that frame so your program knows the buffer overflowed.
The Typed Line
For prompts where the player types a name or a command and hits RETURN, System.keyboard.line handles the editing for you. First, bind System.keyboard.line.capacity <- n to a constant length from 1 to 255 (if you read line without setting a capacity, the compiler reminds you with keyboardLineCapacity). As the player types, printable characters go into the buffer, DEL backs up and erases the previous character, and RETURN submits the completed line and resets the buffer for the next line. line is always a fixed-length list of [n of Char] padded on the right with spaces, and caret gives you the current cursor index from 0 to n. When the player presses RETURN, line.entered fires carrying a snapshot of the [n of Char] that was just submitted.
const keyboard = System.keyboard
keyboard.line.capacity <- 24
var sent: Number<0...99> = 0
when message = keyboard.line.entered
sent = sent +| 1
end
System.screen.text <- [
Text(row: 2, col: 2, width: 8, height: 3, overflow: .wrap, wrap: .word, content: keyboard.line),
Text(row: 10, col: 2, content: sent.digits(2)),
]
System.screen.text[0][0..<2] <- keyboard.caret.digits(2)Setting line.capacity <- 0 disables line editing. Notice that keyboard.line does not draw anything to the screen on its own—you display it wherever and however you like by binding it into System.screen.text or a Text(...) placement.
Joysticks
System.joystick[1] and System.joystick[2] read standard digital joysticks plugged into control ports 1 and 2.
| Member | Kind | Type |
|---|---|---|
dx | reading | Number<-1...1>: -1 left, 0 centered, 1 right |
dy | reading | Number<-1...1>: -1 up, 0 centered, 1 down |
fire | reading | bool, true while the fire button is held down |
Just like a keyboard key, fire.pressed fires in the frame the button goes down, and fire.released fires in the frame it is let go.
x: Number<0...296> <- (System.joystick[2].dx * 2).sum(from: 136)
y: Number<0...179> <- (System.joystick[2].dy * 2).sum(from: 80)
var shots: Number<0...99> = 0
when System.joystick[2].fire.pressed
shots = shots +| 1
end
System.screen.sprite[0] <- (x: x, y: y)
System.screen.text[0][0..<2] <- shots.digits(2)Why do almost all Commodore 64 games ask you to plug your joystick into port 2? Inside the machine, control port 1 shares its input lines with rows of the keyboard matrix. When a player pushes or fires a joystick in port 1, certain keys can read as released while the stick holds that line low; Adder lets you read both System.joystick[1] and the keyboard in the same program, and the cost report helpfully lists which keys port 1 can mask so there are no surprises.
The Mouse and Paddles
System.mouse reads a Commodore 1351 proportional mouse plugged into control port 1. As soon as your program reads any member of System.mouse, the compiler links in the mouse driver automatically.
| Member | Kind | Type |
|---|---|---|
x | reading | Number<0...319> |
y | reading | Number<0...199> |
left, right | reading | bool, true while that mouse button is held down |
x and y track the pointer's pixel position across the 320-by-200 picture window using the exact same coordinate system as sprites and canvases, and the driver clamps the pointer so it stays on screen. You draw the cursor yourself—most often by binding a sprite's x and y straight to mouse.x and mouse.y.
const mouse = System.mouse
var clicks: Number<0...99> = 0
when mouse.left.pressed
clicks = clicks +| 1
end
System.screen.sprite[0] <- (x: mouse.x, y: mouse.y)
System.screen.text[0][0..<2] <- clicks.digits(2)System.paddle[1] and System.paddle[2] read a pair of analog game paddles plugged into control port 1. Turning a paddle dial gives you a smooth Number<u8> reading from 0 at one end of the dial to 255 at the other.
x: Number<0...296> <- System.paddle[1] + 20
System.screen.sprite[0] <- (x: x, y: 170)Control Port 1
Because joystick 1, the 1351 mouse, and the paddle pair all plug into the same nine-pin socket on control port 1, only one of those three device kinds can be wired up at a time. The two paddles share a single Y-cable plug and count as one device, so a two-player paddle game can read both System.paddle[1] and System.paddle[2] together. If a program tries to mix two different port-1 devices (say, reading both System.mouse and System.joystick[1]), the compiler catches the conflict with controlPortOne.
Sound
System.sound[1] is the machine's famed three-voice synthesizer chip (the SID), and its three voices—voice[1], voice[2], and voice[3]—can each play an independent musical note or sound effect at the same time. You can drive a voice in either of two styles: bind it continuously to a Tone with <-, or trigger notes imperatively with play and release inside when handlers.
Tone
A Tone bundles a musical pitch with its timbre (waveform) and volume shape (decay or ADSR envelope):
| Argument | Type | Meaning |
|---|---|---|
| first | Note | the musical pitch, such as .c4 or .f_sharp3 |
waveform | Waveform | .triangle (the default), .sawtooth, .square or .noise |
decay | Decay | a plucked or struck note that rings out and fades over this decay |
envelope | Envelope | full control over the note's attack, decay, sustain and release |
Envelope(attack:, decay:, sustain:, release:) takes an Attack from 1 to 15 and a Decay, Sustain, and Release from 0 to 15, each wrapped in its constructor name—such as Attack(2) or Sustain(8). Attack ranges from 1 (a crisp 8-millisecond strike) up to 15 (a slow 8-second swell). Why does Attack start at 1 instead of 0? Because of a classic hardware quirk in the SID chip's envelope generator, re-triggering a voice with an attack rate of 0 while a previous note is still fading can stall the new note by up to 33 milliseconds; the compiler protects your rhythm by catching Attack(0) with attackZero. A Tone uses either a quick decay: preset or a full envelope:, never both at once (toneEnvelopeConflict).
.silence is the silent Tone value that releases or mutes a bound voice.
Binding a Voice
Binding a voice with <- turns it into a live instrument that tracks your expression: while the binding evaluates to a Tone, the voice sounds that note, and when it evaluates to .silence, the voice releases its gate and fades out. If you do not specify a decay: or envelope: on a bound tone, Adder gives it a clean organ-like sustain envelope (Attack(2), Sustain(15), Release(2)).
held <- System.joystick[2].fire
const tune: [8 of Note?] = [.c4, nil, .e4, .g4, .c5, nil, .g4, .e4]
step: Number<0..<8> <- System.screen.frame.every(12).cycle()
System.sound[1].voice[1] <- if held then Tone(.c4) else .silence end
System.sound[1].voice[2] <- if n = tune[step] then Tone(n, waveform: .square) else .silence endThe runtime touches the SID registers only in frames where the bound Tone actually changes. Sliding from one Note directly to another retunes the pitch smoothly (legato) without re-triggering the envelope, whereas coming back from .silence gates a fresh note. In the cost report, a voice binding takes at most 9 cycles in a quiet frame where the tone stays the same, and 17 cycles in a frame where the tone changes.
If you craft a custom Envelope for a bound voice where the Attack number is smaller than the Release number, your program still compiles, and the cost report attaches a helpful note on that line. It explains that re-gating that voice while its release tail is still audible can trigger the SID chip's 33-millisecond envelope delay, and tells you the exact Attack(...) value that avoids it.
Play and Release
For one-shot sound effects—laser zaps, coin chimes, jump boings—or piano-style key down / key up control, use voice.play(tone) to strike a note and voice.release() to let it fade out over its release rate. Because both are imperative actions, they belong inside when handlers; writing either one outside a handler is flagged with playOutsideOn. When you pass a Tone without an explicit decay: or envelope: to play, it defaults to a percussive struck envelope that rings out and decays on its own without needing a separate release(). To guarantee that every note strikes cleanly even when a voice is already sounding, play resets the SID envelope and gates the new note two frames later; if another play arrives on the same voice during that brief window, the newer note cleanly replaces the pending one.
const ping = Tone(.c6, decay: .short)
const soft = Envelope(attack: Attack(2), decay: Decay(4), sustain: Sustain(8), release: Release(6))
var v: Number<1...3> = 1
when System.keyboard.space.pressed
System.sound[1].voice[v].play(ping)
v = v +% 1
end
when System.keyboard.return.pressed
System.sound[1].voice[1].play(Tone(.c4, envelope: soft))
end
when System.keyboard.return.released
System.sound[1].voice[1].release()
endAs shown above, the voice index in System.sound[1].voice[v].play(...) and .release() can be a runtime variable of type Number<1...3>—handy for round-robin sound effect allocation across all three voices.
Within any given scope, each voice is controlled in one way: either bound with <- or driven by play/release. Mixing a binding and play/release on the same voice in the same scope is flagged with voiceBoundAndCalled, and binding the same voice twice is flagged with voiceBoundTwice. A case, however, can bind a voice that the outer program also binds: while the case is active, the case's binding takes over that voice.
The Clock
When you want wall-clock timing that runs at the exact same speed on 50 Hz European PAL machines and 60 Hz North American NTSC machines, System.clock keeps time for you:
| Member | Kind | Type |
|---|---|---|
interval(seconds:) or interval(ms:) | event maker | Event, firing repeatedly once every period |
timer(seconds:) or timer(ms:) | event maker | Event, firing once after the period elapses from program or case start |
hours | reading | Number<0...23> |
minutes, seconds | reading | Number<0...59> |
tenths | reading | Number<0...9> |
Intervals and Timers
You bind an interval or timer to a name with <- and respond to it in a when handler. Each maker takes either seconds: or ms: (never both, and never zero or negative), written as a positive compile-time number; any other period specification is caught with periodicPeriod.
beat <- System.clock.interval(seconds: 0.25)
ready <- System.clock.timer(ms: 1500)
var hue: Number<0...15> = 0
var lit: bool = false
when beat
hue = hue +% 1
end
when ready
lit = true
end
System.screen.border <- Color(hue)
System.screen.background <- if lit then .white else .black endWhether your player is on PAL or NTSC, a quarter-second interval lasts a true quarter of a second: the runtime calibrates the hardware timer in the detected machine's exact clock cycles (rounded up). Inspecting or filtering a clock event never advances or resets its hardware countdown. Because normal handlers run once per frame, an interval needs a period of at least 0.05 seconds (50 ms) so no tick can occur twice in a single frame (periodicOwnership). Intervals and timers use the CIA 2 hardware timer, so they cannot share a program with color bands, an asm block, or a hardware interrupt routine—the compiler flags that combination with periodicOwnership.
Interrupt Routines
What if you need a timer that fires faster than once per frame, or right in the middle of the screen? System.clock.interrupt(ms:) and System.clock.interrupt(seconds:) fire repeatedly on a hardware timer just like interval, except their when handler holds an asm block that runs immediately—between two instructions of your main program—instead of waiting for the next frame boundary. Likewise, System.screen.interrupt(line:) runs an asm block the instant the video beam reaches a given raster line from 0 to 261. Interrupt Routines describes both in detail.
Time of Day
hours, minutes, seconds, and tenths read the CIA chip's built-in time-of-day clock, which ticks steadily forward in tenths of a second from the start of the run. Whenever your program reads any of these four members, the runtime latches all four together once at the start of the frame—so minutes and seconds never roll over out of sync mid-frame.
System.screen.text[0][0..<2] <- System.clock.minutes.digits(2)
System.screen.text[0][3..<5] <- System.clock.seconds.digits(2)
System.screen.text[0][6..<7] <- System.clock.tenths.digits(1)In the cost report, a line that formats and displays System.clock.seconds.digits(2) takes 65 bytes and at most 204 cycles per frame.
Hardware Registers
For readers who already know their way around the Commodore 64 memory map—or who want to peek under the hood from an asm block—here are the hardware chips and register addresses behind each System device. Normal Adder code never writes raw hex addresses; the System tree handles them all for you.
| Device | Chip | Registers |
|---|---|---|
border, background | VIC-II | $D020, $D021 |
| color bands | VIC-II | raster interrupts through $D012 and $D019–$D01A |
sprite | VIC-II | positions $D000–$D010, shown $D015, multicolor $D01C, colors $D027–$D02E |
text, ink, font, canvases | VIC-II | $D011, $D016, $D018, with screen memory and color memory at $D800 |
keyboard, joystick[1], joystick[2] | CIA 1 | $DC00, $DC01 |
paddle[1], paddle[2], mouse | SID | $D419, $D41A, with $DC00 choosing port 1 |
sound[1].voice[n] | SID | voice 1 $D400–$D406, voice 2 $D407–$D40D, voice 3 $D40E–$D414 |
clock.hours to clock.tenths | CIA 1 | time of day $DC08–$DC0B |
clock.interval, clock.timer | CIA 2 | timer A, $DD04–$DD05 |
screen.interrupt(line:) | VIC-II | raster compare $D012 and $D011 bit 7, flag $D019, enable $D01A, through the IRQ vector at $FFFE |
clock.interrupt(ms:) | CIA 2 | timer A, $DD04–$DD05, control $DD0E, interrupt mask and flags $DD0D, through the NMI vector at $FFFA |
Costs and the Frame
On a classic home computer, smooth motion and responsive controls come down to one golden rule: finish each frame's work before the television beam comes back around for the next one. Adder does not leave you to cross your fingers or eyeball a flickering border to find out if your code made it in time. Instead, the compiler counts the exact byte size of every line you write and calculates a mathematical upper bound on the processor cycles each line can take. From those figures, it proves at build time that every frame's work fits inside a single television frame—and if a frame's worst case could ever run long, the compiler catches it early and points right to the heaviest line. When you need more speed, a fast mark tells the compiler where to spend extra bytes for faster cycles, and the cost report lays out every single figure on the table.
The Frame
A television electron beam paints the screen from top to bottom about fifty times a second on European (PAL) machines and sixty times a second on North American (NTSC) machines. Each complete sweep of the screen is one frame, and an Adder program runs its main loop in lockstep with that sweep—once per frame.
Order in a Frame
Each frame begins the instant the video beam reaches raster line 256, just below the bottom edge of the 200-line picture window. From that starting gun, your program runs through four tidy steps in order:
- It samples the keyboard, joysticks, paddles, mouse, and other hardware readings so they stay steady for the whole frame.
- It checks which events occurred in this frame.
- It runs the
whenhandlers for those events, in the top-to-bottom order they appear in your source. - It recomputes any bindings whose inputs changed and writes the updated outputs to the machine.
Once step 4 finishes, the program waits calmly for raster line 256 to come around again. Because a light frame simply rests until the next frame boundary, your game runs at the exact same rock-steady pace whether a given frame takes 2,000 cycles or 15,000 cycles—so long as the work fits inside the frame. See Order in a Frame for how bindings and handlers interleave across these steps, and Events and Handlers for how events trigger.
Frame Lengths
A television frame lasts a fixed number of processor clock cycles, determined by the video chip inside the machine. To make sure your program runs smoothly everywhere without slowdown, the compiler checks every frame against all three video chips found in Commodore 64 hardware:
| Machine | Lines | Cycles a line | Cycles a frame |
|---|---|---|---|
| PAL | 312 | 63 | 19,656 |
| NTSC | 263 | 65 | 17,095 |
| early NTSC | 262 | 64 | 16,768 |
The early 64-cycle-per-line NTSC video chip lived in the first wave of North American machines. While the processor clock ticks at a slightly different megahertz frequency on PAL and NTSC, the frame budget is counted directly in processor cycles, so the cycle math is crystal clear on every chip.
The Video Chip's Share
The processor does not have the memory bus all to itself: while the video chip draws the visible window, it periodically pauses the processor for a moment to fetch graphics data. On one scanline out of every eight (the famous "badlines" at the top of each character row), the video chip borrows 40 to 43 cycles to fetch character codes or color pointers; each visible sprite also borrows 2 cycles on every scanline it spans, plus a few bus-handover cycles before the processor steps back in. Because an Adder frame starts at line 256 in the lower border, short frame work finishes while the beam is still in the vertical border, before the video chip borrows a single character-row cycle. Longer frame work stretches down into the visible window, where the video chip takes its share of cycles along the way.
The compiler automatically accounts for the video chip's share when computing your frame's worst case, right alongside any raster-line interrupts your program uses and the runtime's own small bookkeeping steps at the start and end of the frame.
Exact Figures and Upper Bounds
When you open the cost report, the byte column is exact: each source line compiles into that exact number of bytes in your .prg file. The cycle column is a guaranteed upper bound. The compiler sums the documented 6510 cycle count of every emitted instruction, follows the slower path through every conditional branch, and multiplies the longest turn of a bounded loop by its maximum trip count. At run time, a line may finish in fewer cycles than its figure, but never more.
A single-character mark right after a cycle figure tells you what kind of bound you are looking at:
| Mark | Meaning |
|---|---|
+ | The line can finish faster than this figure (for instance, when a branch skips work or an indexed load avoids crossing a 256-byte page boundary). The number shown is the worst case. |
k | The figure is rounded in thousands, such as 11k+. |
? | The compiler cannot find a finite cycle bound for an asm loop or call on this line. |
* | The figure comes from the upper end of a cycles(...) range or runs(...) count declared on an asm block. |
! | The line contains hand-written assembly whose memory stores the compiler takes on trust. |
See Assembly for how asm blocks declare their cycle counts.
For a plain text case, the match line also gives a conservative shared-screen showing bound in frames. It starts after the last text write or case change, with geometry fixed and no failure or direct display store. It includes progressive word-layout preparation, pending row copies, a stale hidden pass and a fresh pass, and the following border flip. A change can show sooner than this bound. Further writes or case changes can keep replacing the waiting image, so this is not a promise that a continuously changing picture will settle after that many frames. Bitmap cases, changes between screen kinds and some word-layout combinations with untracked copies do not yet have this whole-image figure; those word layouts say that no showing bound is reported. When a word layout is prepared in full before any rows are shown, the report gives a one-frame bound if all that work fits.
To check a frame, the compiler totals the worst-case cycles of every line that can run in that same frame, adds the runtime's frame-loop overhead, and folds in the video chip's share. That total is a deductive proof, not a stopwatch sample from one test run: no combination of player inputs or branch choices on any of the three video standards can ever push the frame past that number. If any line that runs during the frame has an unknown (?) cycle count, the whole frame's bound is unknown too, and the cost report highlights it.
Frames That Do Not Fit
Whenever a frame's proven worst case exceeds the cycle budget of PAL, NTSC, or early NTSC, the compiler stops and shows you a breakdown of what happened. It points to the single costliest line in that frame and prints the frame's worst-case cycle total right next to the hardware's cycle budget. Why catch this at compile time? Because if a frame's work spilled past line 256, its screen updates would land a frame late (or mid-screen), and the game would stutter to half frame rate whenever that path ran.
A Frame Too Long
When straight-line work (rather than a single heavy loop) pushes the total past the budget, the compiler reports frameOverrun. For example, formatting three 16-bit numbers with .digits(...) on the same frame does repeated divisions that together add up past the PAL and NTSC budgets, producing this message:
Drop one of those three lines and the worst case comes down to 15,235 cycles on PAL and 15,493 on early NTSC—comfortably inside every chip's frame budget—and the program compiles cleanly.
A Loop Too Long
A for finishes inside the frame it starts in. When the costliest line is a loop, the compiler refuses with loopOverrun and gives its turns and each turn's cost. For a handler that multiplies 128 pairs of numbers, even the table routine does not fit:
The compiler has already selected the multiplication tables. The refusal names the selected line, its added bytes and saved cycles, and the remaining frame bound. With 64 pairs, the unmarked program fits by selecting those tables automatically, as fast shows. Loops and the Frame gives the rule for loops inside handlers.
A Stretch Between Waits
Inside a handler that uses wait(n), your code does not have to finish all at once in a single frame: instead, each frame runs one stretch of code—from the start of the handler (or from one wait) up to the next wait. The compiler checks each individual stretch against the frame budget, and if any stretch between two waits can take longer than a frame, it reports segmentOverrun and points to the wait where that stretch began. For instance, a for loop of 200 turns followed by wait(1) after the loop still runs all 200 turns in a single stretch before reaching the wait. Move wait(1) inside the for loop body, and each frame runs just one turn before pausing for the next frame. Because the compiler checks the longest possible path from one wait to the next, hiding a wait inside an if branch does not break up the path when the if condition is false. If a 200-turn for loop only calls wait(1) on every tenth turn via an if, the compiler sees that the if could be false on consecutive turns and reports the exact inner/outer loop pattern that guarantees a pause in time:
Splitting the 200 turns into an outer loop of 8 batches and an inner loop of 25 turns per frame makes the wait(1) unconditional on every batch:
var a: Number<u8> = 3
var b: Number<u8> = 5
var r: Number<u16> = 0
when start
for part in 0..<8
for i in 0..<25
r = r +% a * b
a = a +% 1
end
wait(1)
end
end
System.screen.border <- Color(r % 16)Work Without a Limit
Every Adder for loop has a known range, so pure Adder code always has a provable cycle bound. An asm block, however, can branch backward on a hardware register or a memory byte whose trip count the compiler cannot deduce, jump or jsr to an external address, or call a subroutine recursively. When an asm block runs during a frame and the compiler cannot prove where its loop or call ends, it flags frameUnbounded and points to the instruction or label responsible. For example, if an asm block inside a frame handler spins in a loop waiting for a raster line, the compiler tells you how to supply the cycle bound:
Adding the cycles(...) clause on the asm line gives the compiler your worst-case figure on trust so it can check the rest of the frame around it, as explained in The cycles Clause. There is one natural exception: work that runs once at boot before the very first frame has no 50/60 Hz deadline to race against. That includes the runtime's own start-up setup and the initial stretch of a top-level when start handler (before its first wait). If you put an uncounted assembly loop in that initial top-level start-up stretch, the program compiles without complaint, the cost report notes the ? on the start-up row, and normal frames are still verified as usual. By contrast, a when start handler inside a case—or any code in a top-level when start after its first wait—runs while live frames are ticking, so an uncounted loop there needs cycles(...) just like any other handler. And if an asm block tries to wire up hardware interrupt vectors by hand instead of using an Adder interrupt handler, the compiler flags interruptInstall regardless of where the block sits, as described in Interrupt Routines.
When the Compiler Spends Bytes Itself
The default uses compact code. When a frame does not fit, the compiler selects faster forms from the list below, taking the most cycles saved for each added byte and using source order to break ties. It stops when PAL, NTSC and early NTSC all fit, while the program still fits in memory. The cost report names each selected change on its line. A frame that still does not fit names the changes taken and the work left.
For a top-level match between fixed hires drawings and colors, the compiler prepares the next bitmap in separate storage while the previous whole image stays visible. It needs another 8,000 bytes for pixels, 1,024 for cell colors and sprite pointers, and 64 for each sprite shape. These parts must fit at the display's allowed positions beside the program and its other data. When they do not fit, bitmapSecondImage reports the bytes needed and the available space; a large but fragmented free area may still be unsuitable.
fast
Putting the keyword fast in front of a declaration tells the compiler: "In this declaration, feel free to spend extra program bytes to generate faster machine code." You can place fast before any of these six declaration heads:
| Head | Example |
|---|---|
a when handler | fast when System.screen.frame |
| a function | fast fn square(x: Number<u8>): Number<u16> |
| a function inside a struct | fast mut fn award() |
| an op | fast op speed(x) |
| a binding or an output | fast moved <- position * 2 |
| a case | fast game -> |
You can find the full syntax of each declaration head in Declarations and Events and Handlers. A fast mark applies to all the code inside that declaration, including helper functions copied into it. Other code stays compact when its frame fits.
The 64-pair program fits on every chip. Without a mark, the frame selects its tables; with fast, the handler asks for them whether or not its frame needs them:
var weights: [64 of Number<u8>] = 3
var levels: [64 of Number<u8>] = 5
var mix: Number<u16> = 0
fast when System.screen.frame
var acc: Number<u16> = 0
for i in 0..<64
acc = acc +% weights[i] * levels[i]
end
mix = acc
end
System.screen.border <- Color(mix % 16)The multiplication row says whether the frame or a written mark selected the faster routine. Its two tables and shared routine are counted once on their own report row.
What fast Spends
fast only alters operations that have a faster, larger machine-code strategy available:
| In fast code | Without fast | With fast |
|---|---|---|
* of two changing values | a compact 24-byte shift-and-add routine taking up to 181 cycles | a quarter-square lookup routine backed by 512-byte tables, taking 74 to 81 cycles |
| a case's list that starts as one repeated value | filled by a compact loop when the case starts | filled by an unrolled run of stores (3 bytes and 4 cycles per byte of the list) |
| a loop that copies a run of bytes | a compact indexed loop | unrolled to eight bytes per turn, or one straight copy per turn |
| a call of a function body in assembly | jsr to one shared subroutine body | inlines a copy of the assembly body right at the call site |
a list of sprites made with map | updated through one shared loop body | unrolled into direct stores to each sprite's hardware registers |
| a region's rows | built by one shared routine | each row built by dedicated code of its own |
No matter how many fast declarations use fast multiplication, your program carries at most one shared copy of the quarter-square lookup tables (1,083 bytes including the routine), listed on its own row in the cost report. The tables leave the program when no line needs or asks for them. Removing a fast mark still lets the frame select faster code when it needs it. Even better, once the lookup tables are already in your program because of one fast block, the cost report annotates any remaining slow multiplications elsewhere in the program to show how many cycles fast would save there for zero extra table bytes! Marking an assembly function fast fn inlines its asm body at every call site instead of calling it with jsr.
If you put fast on a declaration where none of the strategies in the table apply, your program still compiles cleanly, and the cost report simply notes fast changed nothing here on that line.
Keeping Code Compact with small
small belongs on the same code heads as fast: a function, op, handler, binding, case pattern or region declaration. It keeps the compact forms in the table above even when a frame needs faster code. An unmarked copied body inherits the caller's choice; a marked body makes its own choice. A small handler inside a fast case stays compact, and a fast handler inside a small case uses the faster forms.
The cost row says small: kept compact, with the complete image bytes and frame-bound cycles its faster alternative would add or save. If there is no alternative on the list, it says small changed nothing here. Regions that share one composed row have one shape decision, so their alternatives are labelled with all source lines and a joint byte difference. Explicitly conflicting fast and small marks on that shared code are refused.
The compiler first takes any allowed faster forms elsewhere. If removing small marks would then let every frame fit, the refusal names the necessary lines, the extra bytes and the remaining work. The reported alternative leaves the program compact, and the same byte and frame limits apply.
Where fast Cannot Go
Because fast speeds up executable code, it only belongs on declarations that run code; putting fast before a constant, a var, or a type declaration is caught with fastHead.
The same rules apply to small. Repeating one mark on a head gives fastRepeated; writing fast small or small fast gives codeMarkConflict. Neither mark rewrites an asm block (fastAsm). A mark on a case may contain marked handlers or nested cases: the innermost mark decides their code. An explicitly marked function or op keeps its choice when its body is copied into differently marked code.
Work Divided Between Frames
The strict single-frame check applies to code that runs from start to finish inside one frame. For heavier tasks that naturally span several frames, Adder provides five built-in ways to spread work across time—and the compiler checks each per-frame step on its own:
- A handler that calls
wait(n)runs one stretch per frame between waits, as shown in A Stretch Between Waits. - A
workmember runs as a background job in whatever cycles remain at the end of each frame after the frame's main handlers and bindings finish. - A complex canvas drawing change is rendered incrementally across frames so the frame loop stays on time.
- Text printed to a region is copied out to screen memory a few rows per frame.
- A case whose initial setup is too large for a single frame initializes across several frames. A canvas's new picture is drawn with the screen blank until it is ready.
Let's look at how background work jobs are divided into parts and verified. See Work for how to declare a work member, and The System Device Tree for canvas drawings and text regions.
Parts of a Job
When you write a work job, the compiler automatically breaks its body into bite-sized parts along the turns of its loops. A short loop that easily fits in a frame becomes a single part; a longer loop is grouped into a handful of turns per part; and straight-line code without loops is one part. Before starting each part at run time, the job scheduler checks how many cycles are left before the next television frame begins, and runs the next part only if it is guaranteed to finish before line 256. If there is not enough room left in the current frame, the job yields right there and picks up at that part on the next frame.
Because a background job yields whenever a frame is nearly full, a bound job can run alongside any frame that leaves enough breathing room for the job's longest single part:
type Samples = [4 of Number<u8>]
work Samples.tally(input: Samples): Number<u16>
var amount: Number<u16> = 0
for sample in input
amount = amount + sample
end
amount
end
const samples: Samples = [1, 2, 3, 4]
answer: Number<u16>? <- samples.tally()
System.screen.text[0][0..<5] <- if amount = answer then amount.digits(5) else "wait " endThe compiler verifies every job at build time so a background task can never stall or overrun a frame:
- if a part of a job contains an uncounted loop with no cycle limit, it flags
jobUnbounded; - if a single part—one turn of a loop, or the straight-line code before or after a loop—is too large to ever fit alongside the frame's regular work, it flags
jobPartTooLongand names the loop; - if a handler explicitly calls
.step()on a job in a frame that has no cycles left to spare for a part, it flagsjobFrameFull; - if a binding asks for a guaranteed completion window with
.within(frames: n)and the compiler cannot prove the job finishes withinnframes under worst-case frame load, it flagsjobDeadlineand tells you the shortest deadline it can prove.
Because the static deadline proof reserves cycles in every frame for the scheduler's remaining-time check before each part, the proven worst-case deadline is safely conservative—at run time the job will often finish even sooner.
The Cost Report
Want to see the exact byte and cycle ledger for your whole program? Run tools/costs.ts from the lab/ directory:
bun tools/costs.ts ../game.adder
bun tools/costs.ts ../game.adder --listingPass --listing to unfold the exact 6510 assembly instructions emitted under each source line along with every instruction's individual cycle count. If your program has a compile error or a frame overrun, costs.ts prints the diagnostic message instead and leaves with status 1.
Reading the Report
Here is the cost report for the fast 64-multiply program from fast:
source bytes cycles
mix.adder:4 4 5+ fast when System.screen.frame
fast: tables in 1 multiplication
mix.adder:6 10 11k+ for i in 0..<64
a loop of 64 turns of at most 184 cycles: 64 × 184 = 11,776; the figure also holds the cycles that set the counter and leave
mix.adder:7 61 171+ acc = acc +% weights[i] * levels[i]
fast: tables, 171+ cycles instead of 269+, no extra bytes
one turn of the loop at mix.adder:6, whose figure holds every turn
fast multiply tables, 1,083 bytes, used by line mix.adder:7
frame work, worst case: PAL 12,394 of 19,656 cycles, NTSC 12,652 of 17,095, early NTSC 12,652 of 16,768
runtime and shared tables (line 0): 1404 bytes; startup worst case 13,866 cycles, not counting the raster wait of standard detection; program file: 1552 bytes including the load addressEach numbered row shows the file and line number, the exact bytes emitted for that line, the worst-case cycle bound (with its mark), and the source text itself. Indented notes right underneath a row explain how the compiler arrived at the number: how many turns a loop runs, what fast changed, and any shared tables or storage claimed by that line. Notice that the for loop header row (mix.adder:6) holds the total cycle count of all 64 turns combined (11,776 plus loop setup), while the body line inside it (mix.adder:7) shows the cost of a single turn (171+) so you can see both without double-counting.
Near the bottom, the frame work, worst case summary compares your program's proven worst-case frame against the cycle budget of PAL, NTSC, and early NTSC side by side. Finally, the runtime and shared tables (line 0) row accounts for the shared machinery bundled into the .prg—start-up initialization, hardware input sampling, the frame loop, the range-overflow stop handler, and any shared lookup tables like the fast multiply tables—along with the final .prg file size on disk.
The cost report makes it fun and easy to compare language choices at a glance. For example, a checked subtraction - tests whether the result dipped below the variable's minimum range and calls the range-stop handler if so, whereas a saturating subtraction -| clamps at the floor in just a few instructions:
lives.adder:3 42 37+ lives = lives - 1lives2.adder:3 14 17+ lives = lives -| 1See Checked Arithmetic and Wrapping and Clamping for the full set of arithmetic operators.
Comparing Two Versions
Tuning a tight loop or experimenting with fast? bun tools/cost-diff.ts compares the cost reports of two versions of a program side by side in a fraction of a second, without needing to launch an emulator. You can point it at two separate .adder files, or compare your working file against any Git commit:
bun tools/cost-diff.ts ../before.adder ../after.adder
bun tools/cost-diff.ts ../game.adder --at HEADIt prints the before-and-after summary for the frame's worst case and the runtime size, followed by each source line that changed—sorted with the biggest cycle difference at the top:
frame work, worst case: PAL 161 -> 141 (-20), NTSC 161 -> 141 (-20), early NTSC 161 -> 141 (-20)
runtime and shared tables (line 0): 434 -> 311 (-123) bytes
line 3: lives = lives -| 1
bytes 42 -> 14 (-28); listing cycles 55 -> 18 (-37) (fewest 52 -> 17 (-35)); worst case 37 -> 17 (-20)Notice that switching that one line from lives - 1 to lives -| 1 not only saved 28 bytes and 20 cycles on line 3 itself—it also trimmed 123 bytes from the runtime because no line in the program could trigger a range-overflow stop anymore, allowing the compiler to leave the stop handler out completely!
Assembly
When you want to drop right down to the bare metal—counting individual processor cycles, banging hardware registers on a raster line, or hand-crafting a tight bit-twiddling routine—an asm block lets you write 6510 machine instructions right inside an Adder program. The compiler assembles your instructions byte-for-byte as you wrote them and never rewrites or optimizes them behind your back. At the same time, it stays on duty as your co-pilot: it resolves Adder variable and device names, checks that your block does not step on the runtime's memory, counts your instructions toward the frame budget, and tacks on a tiny epilogue after the block so the surrounding Adder code keeps humming along. And if you ever want to write a pure machine-language program from top to bottom with zero Adder code, the final section of this chapter shows how the same assembler builds standalone .s files.
Assembly Blocks
You can place an asm block anywhere a statement goes inside a when handler—at the top level of the handler or nested inside if, match, and for statements—or as a step inside an output declaration. The block executes right where it sits each time control reaches that line. Handlers inside a case can hold asm blocks too, and all the same rules apply.
asm-block ::= 'asm' instruction newline
| 'asm' writes-clause? runs-clause? cycles-clause? newline asm-line* 'end' newlineFor a quick one-liner, write asm followed by a single instruction on the same line. For a multi-line routine, open with asm, write as many assembly lines as you like, and close with end. On the opening line of a multi-line block, you can optionally include a writes clause, a runs clause, and a cycles clause, in that order (where runs is specifically for self-scheduling interrupt routines).
var ticks: Number<u8> = 0
when System.screen.frame
asm inc ticks
if ticks > 200 then
asm
lda #0
sta ticks
end
end
end
System.screen.border <- Color(ticks % 16)Because an asm statement needs a handler or output routine to run in, placing a loose asm block at the top level of a file outside any handler is caught with topLevelAssembly.
And since the assembler always emits the exact instructions you write without rewriting them, putting fast in front of an asm block is flagged with fastAsm. Beyond statement blocks inside handlers, an asm block can also form the body of a function (Assembly Functions) or compute the value of a binding or assignment (Hand-Written Bindings).
Registers and Flags
Inside an asm block, the 6510's registers (A, X, and Y) and status flags (N, V, Z, and C) are yours to use freely. The compiler never assumes a value stays in A, X, or Y across an asm block—any Adder code after the block reloads whatever registers it needs. You are responsible for leaving the hardware stack pointer where you found it (every pha matched by a pla).
Right after every asm block, the compiler emits a cld (Clear Decimal Mode) instruction so that if your block switched on BCD arithmetic with sed, normal binary arithmetic is restored at end. That cld adds 1 byte and 2 cycles, which you will see noted on the end line of the cost report.
Once the runtime finishes booting and sets up the frame loop's interrupts, it clears the processor's interrupt-disable (I) flag and keeps it clear. An asm block may temporarily mask interrupts with sei, but only a top-level when start block is allowed to execute cli; writing cli anywhere else is flagged with assemblyCli so a block cannot accidentally enable interrupts at the wrong moment.
The writes Clause
Whenever your assembly block stores directly to an Adder variable or binding by name—like sta ticks or inc ticks—the compiler spots the store automatically. Right after the block's end, it sets that variable's change flag so any downstream bindings that depend on ticks know to recompute on this frame. Those automatic change marks cost a 2-byte, 2-cycle lda #1 followed by a 3-byte, 4-cycle store per modified variable, shown on the end line in the cost report.
What if your block modifies an Adder variable indirectly through a zero-page pointer in scratch, where the instruction says sta (scratch),y instead of naming ticks? Add writes(name, ...) on the opening asm line to tell the compiler which variables or bindings your pointer store touches:
var ticks: Number<u8> = 0
seen <- ticks
when System.screen.frame
asm writes(ticks)
lda #<ticks
sta scratch
lda #>ticks
sta scratch+1
ldy #0
lda (scratch),y
clc
adc #1
sta (scratch),y
end
end
System.screen.border <- Color(seen % 16)writes-clause ::= 'writes' '(' name ( ',' name )* ')'Every name listed in writes(...) must be a variable or binding declared above the block (assemblyWritesName). If you leave a pointer-modified variable out of writes(...), downstream bindings will not know it changed and will keep displaying their previous value.
The cycles Clause
Normally, the compiler traces every path through your asm block and sums the exact 6510 cycle counts itself. However, if your block contains a loop whose trip count depends on a runtime register or hardware state, a jsr or jmp to an external address, or a recursive call, the compiler cannot deduce the maximum cycle count on its own (frameUnbounded, as described in Work Without a Limit). In those cases—or whenever you want to state an explicit budget—add cycles(...) on the opening asm line. The compiler takes your stated figure on trust, uses the upper bound for its frame-budget check, and skips counting the individual instructions inside the block.
cycles-clause ::= 'cycles' '(' cycle-figure ')'
| 'cycles' '(' standard-figure ( ',' standard-figure )* ')'
cycle-figure ::= decimal-digits ( '...' decimal-digits )?
standard-figure ::= name ':' cycle-figureWrite cycles(n) for a single cycle figure or cycles(low...high) for a minimum-to-maximum range that applies across all video standards. If your block's timing differs between PAL and NTSC (for instance, when waiting for a specific raster scanline), you can give separate figures per video standard using pal:, ntsc:, and optionally early_ntsc:. When you write ntsc:, that figure applies to both standard NTSC and early NTSC unless you also write an explicit early_ntsc: figure.
var delay: Number<1...200> = 1
when System.screen.frame
asm cycles(8...1203)
ldx delay
wait:
dex
bne wait
end
endFor example, a raster wait loop whose duration depends on the television standard can declare cycles(pal: 4800...16700, ntsc: 4800...14100).
To keep per-standard declarations complete, specifying pal: without ntsc: (or ntsc: without pal:) is caught with cyclesOneStandard. The compiler also checks for unknown standard names (cyclesStandard), duplicate standard names (cyclesRepeated), non-integer cycle counts (cyclesNeeded), and zero or inverted ranges where the minimum exceeds the maximum (cyclesRange).
In the cost report, a block with a cycles(...) clause displays its upper bound marked with *. An asm block without cycles(...) is counted automatically by the compiler and marked with ! as a reminder that its memory stores are taken on trust. See Costs and the Frame for how both marks appear in the report.
Lines of a Block
Each line inside an asm block can hold a label, an instruction (with or without a leading label), or a data directive (.byte or .text). Comments start with // just like in the rest of Adder, and indentation is purely visual.
asm-line ::= ( label | label? instruction | label? data ) newline
label ::= name ':'
instruction ::= instruction-name operand?
operand ::= '#' asm-value
| asm-value ( ',x' | ',y' )?
| '(' asm-value ',x' ')'
| '(' asm-value ')' ',y'
| '(' asm-value ')'
data ::= '.byte' asm-value ( ',' asm-value )*
| '.text' string-literal
asm-value ::= ( '<' | '>' )? '-'? asm-number
| ( '<' | '>' )? asm-name ( '.lo' | '.hi' )? ( '+' asm-number )?
asm-name ::= name ( '[' decimal-digits ']' )? ( '.' name ( '[' decimal-digits ']' )? )*
asm-number ::= decimal-digits | '$' hex-digit+ | '%' binary-digit+Instructions
An asm block supports all 56 official mnemonics of the MOS 6510 processor in either lowercase (lda) or uppercase (LDA). You can find the complete alphabetical list in Words Inside Assembly.
Seasoned 64 coders will be glad to know that Adder also supports the stable undocumented ("illegal") NMOS 6510 opcodes—the ones that behave identically across every production 6510 chip:
| Instruction | What it does |
|---|---|
lax | loads both A and X with the same memory byte |
sax | stores A bitwise-ANDed with X into memory (without changing flags) |
dcp, isc, slo, rla, sre, rra | read-modify-write a memory byte, then combine it with A |
anc, alr, arr, sbx | immediate operations on A (or A and X) with a constant byte |
nop with an operand | fetches its operand (consuming the addressing mode's bytes and cycles) and does nothing else |
If you are used to alternative assembler spellings, isb, asr, and axs are accepted as synonyms for isc, alr, and sbx.
What about the unstable undocumented opcodes whose behavior varies with chip batch, temperature, or bus capacitance (ane, lxa, sha, shx, shy, tas, las, and immediate lax #n which is lxa)? The compiler keeps them out of your program with assemblyUnstable so your code runs identically on every machine, and it blocks jam (kil/hlt) with assemblyJam since a JAM opcode freezes the 6510 until you power-cycle the computer.
Every instruction accepts only the addressing modes that actually exist for it on the 6510; writing a nonexistent mode (like sta #0 or inx #1) is caught with badInstructionForm or assemblyForm. In a single-line asm statement, an unrecognized mnemonic is flagged with unknownInstruction.
Operands
Operands use classic MOS 6502/6510 addressing syntax:
| Form | Meaning |
|---|---|
#value | immediate: the 8-bit constant value itself |
address | zero-page or absolute: the byte stored at address (or label target for branches/jumps) |
address,x or address,y | indexed: the byte at address + X or address + Y |
(address,x) | indexed indirect: the byte at the 16-bit pointer stored in zero page at address + X |
(address),y | indirect indexed: the byte at the 16-bit zero-page pointer at address, offset by + Y |
(address) | indirect: jumps through the 16-bit pointer stored at address (jmp only) |
Instructions with implied or accumulator addressing—such as inx, clc, rts, or asl shifting A—take no operand at all.
Inside an asm block, numbers use traditional 6502 notation: decimal (14), $ for hexadecimal ($0e), or % for binary (%00001110). Notice that Adder's 0x and 0b prefixes belong to high-level Adder expressions; inside an asm block, use $ and % instead (badNumber). A leading minus sign - gives the 8-bit two's-complement byte, so lda #-1 loads $ff. Every immediate #value must fit in a single byte (-128 to 255), or the compiler flags operandSize.
Prefixing any value or symbol with < extracts its low byte (bits 0–7), and > extracts its high byte (bits 8–15). When placed in front of a variable, label, or device name that lives in memory, <name and >name give you the low and high bytes of its 16-bit address—so lda #<ticks loads the low byte of the address of ticks.
Whenever a numeric address falls in $00–$ff and the instruction supports zero-page addressing, the assembler automatically picks the compact 2-byte zero-page opcode (whether you write $fb or $00fb). Named Adder variables and bindings always assemble with 16-bit absolute addressing.
Labels
Writing name: defines a label at the address of the next instruction or data byte. A label can sit on its own line or right in front of an instruction or .byte/.text directive on the same line, giving branch and jump instructions a clear target:
var pos: Number<u16> = 0
when System.screen.frame
asm
inc pos.lo
bne done
inc pos.hi
done:
end
end
System.screen.border <- Color(pos % 16)Labels are local to their own asm block, so two separate blocks can both have a label named done without clashing. There is one handy exception: labels defined inside a when start handler remain visible to every asm block written below that when start in the same scope, making it easy to share lookup tables or helper routines (see Start-Up Code). A case's when start shares its labels only with blocks inside that same case (and any sub-cases nested inside it), so two sibling cases can reuse the same label names cleanly.
To prevent ambiguity, a label cannot shadow an Adder name or another label already in scope (assemblyLabelCollides), nor can it reuse a built-in name like Color or scratch (builtinName). Because a label is an address rather than a multi-byte variable, use <label and >label to take the low and high bytes of its address rather than .lo or .hi (assemblyLabelSelector). Finally, 6510 conditional branches (beq, bne, bcc, and friends) use a signed 8-bit relative offset that reaches at most 128 bytes backward or 127 bytes forward from the following instruction; if a branch target is farther away than that, the assembler lets you know with branchTooFar.
Data
You can embed data tables right inside an asm block using .byte for comma-separated bytes and .text for a string literal converted into screen codes (matching ScreenCode("...")). Each item in a .byte list can be a number (0 to 255 or -128 to -1), a named Adder constant, or the < or > byte of an address.
when System.screen.frame
asm
ldx #4
show:
lda word,x
sta System.screen.text,x
dex
bpl show
word: .text "hello"
end
endHere is a neat convenience: when you put .byte or .text data under a label inside an asm block, the compiler automatically parks those data bytes out of the instruction stream at the end of the program's code. You never have to write a jmp just to hop over your own data table! Even better, if any asm block writes to a labeled data cell at run time, Adder treats it like an initialized variable and resets it to its original .byte/.text values whenever that scope starts (at boot for top-level blocks, or each time a case begins for case blocks). If no block ever writes to the labeled data, it stays read-only in place and costs zero start-up cycles. Writing .text without a string literal after it is caught with textNeeded.
Names in Assembly
An asm block can refer directly to the variables, bindings, constants, placed values, function parameters, and System devices declared on lines above it. Just like the rest of Adder, visibility flows from top to bottom: referencing a name declared below the block (or not declared at all) is flagged with assemblyUnknownName.
Values With an Address
Variables, bindings, placed values, and hardware device registers all live at memory addresses, so writing their name in an operand refers to the byte stored at that address. For a 1-byte (u8, i8, bool, Color, etc.) value, write its name by itself: lda ticks. For a 2-byte (u16, i16, u8.8, i8.8) value, tack on .lo for the low byte or .hi for the high byte (lda score.lo, lda score.hi); writing a 2-byte name without .lo or .hi is caught with assemblyWide, and putting .hi on a 1-byte value is caught with assemblyNarrow.
Three-byte fixed-point numbers (u16.8 and i16.8) are wider than asm operands address directly (assemblyTooWide)—if you need part of one in assembly, bind its integer part (via .floor()) or scaled value into a u8 or u16 variable first. Similarly, composite values like structs hold multiple fields rather than a single number (assemblyNeedsScalar).
Lists are laid out in assembly-friendly form:
- A list of 1-byte elements sits in contiguous bytes:
nameis element0,name+nis elementn(only 1-byte lists take+n, checked byassemblyByteOffset), andlda name,xindexes elementXat run time. - A list of 2-byte elements is stored in striped (structure-of-arrays) layout—all the low bytes together in one contiguous table and all the high bytes together in another—so
lda pos.lo,xandlda pos.hi,xcan index up to 256 16-bit elements with an 8-bitXorYregister without doubling the index!
var pos: [3 of Number<u16>] = [0x0101, 0x0202, 0x0303]
when System.screen.frame
asm
ldx #2
more:
inc pos.lo,x
bne step
inc pos.hi,x
step:
dex
bpl more
end
endConstants
A numeric constant stands for its number rather than a memory cell, so load it with immediate # syntax: lda #speed (and use #speed.lo / #speed.hi for a 2-byte constant). A list constant, on the other hand, lives as a lookup table in memory, so lda row,x reads element X from the table; because it is a constant, attempting to write to it (sta row,x) is caught with assemblyConstantWrite.
const speed = 3
const row = [6, 14, 3, 1]
var got: [4 of Number<u8>] = [0, 0, 0, 0]
when System.screen.frame
asm
ldx #speed
copy:
lda row,x
sta got,x
dex
bpl copy
end
endA struct constant groups multiple fields without a single scalar address (assemblyNoAddress).
Devices
You can read or write hardware device registers using their readable System paths right in assembly—such as sta System.screen.border or lda System.joystick[2].dx. System.screen.text always points to the first cell of screen memory (wherever the compiler placed the screen), and System.screen.text+2 points to the third cell. If your program gave a device a short alias with a constant (like screen = System.screen), use that alias (screen.border) inside your asm blocks too—the compiler flags the long path with deviceNamed so every reference stays consistent.
Sampled input devices (such as System.joystick[2].dx) hold the clean reading latched at the start of the frame; your block can read them with lda, and the compiler blocks stores to them with assemblyInputWrite.
Scratch
Every asm block has free use of four zero-page scratch bytes at $FB–$FE, named scratch, scratch+1, scratch+2, and scratch+3. Because they sit in zero page, scratch and scratch+1 (or scratch+2 and scratch+3) make a natural 16-bit pointer pair for indirect indexed addressing like lda (scratch),y or sta (scratch),y. Blocks inside when handlers can both read and write through a scratch pointer; pure assembly functions, hand-written bindings, and interrupt routines can read through a pointer (and must keep their stores to known places).
Keep in mind that scratch is a shared workbench: the runtime and other asm blocks use $FB–$FE too, so values left in scratch do not survive from one asm block to another or across frames. When you need a byte to persist, store it in an Adder var or a labeled .byte cell instead. Using an offset past scratch+3 or writing .lo/.hi on scratch is caught with assemblyScratch.
Assembly Functions
Want to wrap a fast 6510 routine in a clean, reusable Adder function call? When a function's entire body is a single asm block, it becomes an assembly function: it reads its parameters by name and returns its answer by storing into the built-in variable result.
assembly-function ::= 'fast'? 'fn' function-name '(' parameters? ')' ':' type newline
asm-block 'end'fn double(n: Number<u8>): Number<u16>
asm
lda n
asl
sta result.lo
lda #0
rol
sta result.hi
end
end
var level: Number<u8> = 200
when System.screen.frame
level = level +% 1
end
System.screen.text[0][0..<3] <- double(level).digits(3)result automatically matches the return type declared on your fn header—so a 1-byte return uses sta result, while a 2-byte return uses sta result.lo and sta result.hi. The compiler traces every branch through your function to make sure every possible path writes every byte of result before returning (assemblyResultUnwritten); direct stores inside a local subroutine called with jsr count, while indirect pointer stores do not. If your function declares a constrained return range like Number<0...127>, callers rely on that promise, so make sure your assembly produces a value within that range.
Function parameters are read-only inputs (since the compiler may pass a caller's variable storage directly without copying). Attempting to modify a parameter (sta n, inc n, asl n) is caught with assemblyParameterWrite—just load it into A or copy it into scratch if you need a working variable. Also avoid naming a parameter a, x, or y, since those letters belong to the 6510 registers (assemblyRegisterParameter).
Like every function in Adder, an assembly function is pure: given the same arguments, it computes a result without hidden side effects on the rest of the machine. Inside an assembly function, your block may write only to result, scratch, and its own labeled .byte cells (indexed or direct, such as sta table,y). Touching a System device, jumping or calling outside the function block, executing cli, rti, or brk, adding a writes(...) clause, or storing indirectly through a pointer (sta (scratch),y or sta (scratch,x)) is flagged with assemblyImpure (while reading through a pointer, such as lda (scratch),y to walk a list parameter, is completely fine). If you want a routine that mutates global program state through a pointer, place the asm block inside a when handler and list the targets in writes. Inside an assembly function, you can use rts to return early from a branch if you like (once result has been written).
Normally, all calls to an assembly function share one subroutine body in memory; putting fast fn on the declaration inlines a copy of the asm block at every call site to save the jsr/rts cycles.
Hand-Written Bindings
Sometimes you want the declarative convenience of an Adder <- binding, a variable assignment x = ..., or a placed value, while hand-crafting the exact 6510 instructions that compute the expression. Writing an asm block right after the expression gives you the best of both: the high-level expression tells the compiler what inputs the line depends on, when it needs to run, and what range the result has, while your asm block replaces the generated code and stores the answer into result.
hand-written ::= ( binding | place '=' expression | placed-value )
'asm' cycles-clause? newline asm-line* 'end' newlinevar ticks: Number<u8> = 0
when System.screen.frame
ticks = ticks +% 3 asm
lda ticks
clc
adc #3
sta result
end
end
System.screen.border <- Color(ticks % 16)Just like an assembly function, a hand-written binding must write every byte of result on every path through the block. To make sure the dependency graph stays accurate, the asm block may only read variables and bindings that actually appear in the high-level expression in front of asm—reading an unmentioned variable is caught with handWrittenRead.
Likewise, the block is pure calculation: it may write only to result, scratch, and its own labeled .byte cells, and any store to a device, an outer variable, or an indirect pointer is flagged with handImpure. Stream-state expressions that maintain their own internal frame-to-frame memory (such as .sum(...) or .toggle()) cannot be replaced by hand-written assembly (handWrittenMemory), nor can raster-timed color-band bindings that have no single static result cell in memory (handWrittenTarget).
Start-Up Code
An asm block inside a top-level when start handler runs once when the program boots, before the very first television frame begins. Unlike ordinary blocks whose labels are private, labels defined in a when start handler stay visible to every asm block written on later lines—making when start the natural home for shared data tables, persistent .byte variables, or helper subroutines that several handlers call with jsr. (Visibility still flows downward, so a block written above when start cannot see its labels.)
var seen: Number<u8> = 0
when start
asm
jmp done
ticks:
.byte 40
done:
end
end
when System.screen.frame
asm
inc ticks
lda ticks
sta seen
end
end
System.screen.border <- Color(seen % 16)Start-Up Code in a Case
When you write a when start handler inside a case (match arm), it runs on that case's opening frame each time the program enters the case—so its cycle cost is counted as part of that opening frame's budget. While the top-level when start runs before frame timing begins (and can therefore hold an uncounted hardware-wait loop prior to its first wait), a case's when start runs inside a live frame and requires bounded loops or a cycles(...) clause (frameUnbounded). Labels declared in a case's when start are visible to asm blocks below it within that same case (and any sub-cases nested inside it), and invisible outside the case (assemblyUnknownName). That means sibling cases can freely reuse the same label names without colliding; if two cases want to share a labeled cell, declare it in a top-level when start above the match.
Any labeled .byte cell inside a case that gets modified by an asm block is automatically reset to its initial .byte value each time the case starts—just like a case-local var. Adder resets those cells before enabling the case's interrupt routines and before running the case's when start, so an interrupt never sees leftover data from a previous visit to the case. By contrast, a labeled cell declared at the top level outside every case is initialized once at boot and keeps its value across case changes.
var f = false
var seen: Number<u8> = 0
when System.joystick[2].fire.pressed
f = not f
end
System.screen.text[0][0..<3] <- seen.digits(3)
match f
true ->
when start
asm
jmp done
laps:
.byte 40
done:
end
end
when System.screen.frame
asm writes(seen)
inc laps
lda laps
sta seen
end
end
false ->
const m = 1
endInterrupt Routines
Want to split scroll registers mid-screen, multiplex sprites, or run a high-frequency sound routine on a hardware timer? In Adder, a hardware interrupt routine is simply a when handler for System.screen.interrupt(line: n) or System.clock.interrupt(...) whose body is a single asm block. System.screen.interrupt(line: n) triggers the instant the VIC-II raster beam reaches scanline n (a compile-time constant from 0 to 261, using the video chip's hardware line numbers where the 200-line picture window begins at line 51). You write just the heart of the routine: the compiler wires up the interrupt vectors for your program's memory map, saves and restores whichever registers (A, X, Y) your block touches, acknowledges the hardware interrupt flag, and emits the return from interrupt when execution reaches end.
Here is a complete raster interrupt that flips the border color on scanline 100 of every frame:
var level: Number<u8> = 3
when System.screen.interrupt(line: 100)
asm
inc ticks
lda level
sta System.screen.border
ticks:
.byte 0
end
endBecause execution falls through end into the compiler's register-restore and rti sequence, your block must not terminate the interrupt on its own: writing rti, brk, or jumping out of the block is caught with interruptRoutineForm. Likewise, an interrupt handler must consist solely of that one asm block—no event filter condition, no fast mark, no writes(...) clause, and no extra statements around it (interruptRoutineForm).
Every interrupt routine is included in the compiler's frame-budget check: the compiler counts the cycles of each run (plus the interrupt's save-and-return overhead), as Work Without a Limit describes for any block, and displays the cost right next to the when line in the cost report. If your interrupt block contains a loop the compiler cannot bound automatically, add a cycles(...) clause on the asm line just like in any other block.
Installing by Hand
Why does Adder manage the interrupt vectors and control registers itself? Depending on whether your program uses text mode, a bitmap canvas, or cases that switch banks and ROMs, the active interrupt vectors live at different addresses ($0314–$0319 vs. $FFFA–$FFFF) and coordinate with the frame loop and color bands. To keep a manual poke from pulling the rug out from under the runtime, the compiler blocks direct stores to the interrupt vectors ($0314–$0319, $FFFA–$FFFF), the interrupt flag and enable registers ($D019, $D01A, $DC0D, $DD0D), and—except inside a self-scheduling runs(n) routine—the raster compare register $D012 (interruptInstall). When checking indexed stores like sta base,x, the compiler tracks the range of X if you load it with a constant and only decrement it, and otherwise conservatively checks all 256 possible offset addresses.
So if you are porting a classic routine that pokes $0314/$0315 and ends with jmp $ea81, simply drop the setup boilerplate and put the body instructions inside when System.screen.interrupt(line: n)!
What a Routine May Touch
Because a hardware interrupt can fire between any two instructions of your main program, an interrupt routine follows strict rules to avoid corrupting work in progress:
- It may write only to hardware chip registers and to its own labeled
.bytecells (by name). Writing to an Addervaror binding—or storing indirectly through a pointer—is flagged withassemblyInterruptWrite. - It may read any Adder variable, binding, constant, or memory address (including through a pointer).
- Because the main program may be right in the middle of using
scratch($FB–$FE) when the interrupt hits, an interrupt routine may not touchscratchby name or address (assemblyInterruptScratch). - The stack must stay balanced: every
pha/phpinside the block must have a matchingpla/plpon every path, andtxsis forbidden (assemblyInterruptScratch).
If your interrupt routine uses adc or sbc and any block in the program ever sets decimal mode with sed, the compiler automatically inserts a cld at the top of the interrupt routine so an interrupt arriving mid-sed never adds in BCD by accident.
The compiler also optimizes main-program variables around your interrupts: if an interrupt routine reads an Adder variable, the compiler makes sure every update to that variable is written straight through to its memory cell; if your interrupt reads no Adder variables, the optimizer has full freedom to keep values in registers.
Routines That Pick Their Own Lines
Advanced raster routines—like a sprite multiplexer or a wobbling raster bar—often compute and store their next trigger scanline into $D012 on the fly. To write a self-scheduling raster interrupt, add runs(n) on the opening asm line to declare the maximum number of times the routine will fire per frame. The line: number in when System.screen.interrupt(line: ...) sets the initial trigger line at start-up, and from then on your block writes $D012 at the end of each pass to schedule the next run (including wrapping back to the top line for the next frame).
runs-clause ::= 'runs' '(' decimal-digits ')'Here is a self-scheduling routine that fires 3 times per frame to paint red, white, and blue border splits at moving scanlines top, top + 40, and top + 80:
const red = Number<u8>(Color.red)
const white = Number<u8>(Color.white)
const blue = Number<u8>(Color.blue)
top: Number<40..<100> <- System.screen.frame.every(20).cycle()
when System.screen.interrupt(line: 1)
asm runs(3)
ldx step
lda colors,x
sta System.screen.border
inx
cpx #3
bne arm
ldx #0
arm:
stx step
lda top
clc
adc offsets,x
sta $d012
step:
.byte 2
offsets:
.byte 0, 40, 80
colors:
.byte red, white, blue
end
endBecause the compiler cannot predict which scanlines your code will write to $D012 at run time, it multiplies the routine's worst-case single-run cost by n and charges that total to the frame budget, marking the line with * in the cost report. Storing to $D012 without declaring runs(n) is flagged with interruptInstall, while declaring runs(n) on a block that never writes $D012 (or on a non-interrupt block) is flagged with interruptRuns.
A self-scheduling runs(n) routine claims the VIC-II raster interrupt for itself while active, so it cannot share the same scope with color bands or with other raster interrupt routines (rasterOwner).
Fixed-line raster routines (without runs), by contrast, can share the screen with color bands and with other fixed-line routines: the compiler sorts all of them by scanline and chains each interrupt into the next. If a fixed-line routine takes so many cycles that the electron beam would already pass the next scheduled raster line before the routine finishes and arms it, the compiler catches the collision at build time with interruptOverrun.
Routines on a Timer
System.clock.interrupt(ms: n) (or seconds:) runs your asm block repeatedly on a constant hardware timer period of up to 64 milliseconds (interruptPeriod if out of range), calibrated so the time between interrupts is identical on PAL and NTSC hardware. The routine runs on CIA 2 Timer A (wired to the 6510's NMI line) and ignores the RESTORE key. To protect the timer, no other asm block in the program may write to CIA 2 Timer A's registers or read the interrupt flag register at $DD0D (interruptInstall), since reading $DD0D clears the hardware flag. On a real Commodore 64, holding down the RESTORE key at the exact cycle the timer underflows can mask the NMI edge; Adder's frame loop automatically checks for a stuck timer flag once per frame and services one missed run so the timer keeps ticking (and includes that recovery run in the frame budget).
when System.clock.interrupt(ms: 10)
asm
inc ticks
ticks:
.byte 0
end
endIn the frame budget, the compiler charges each frame with the maximum number of timer ticks that can fall inside one frame (floor(frame_cycles / period_cycles) + 1). If the routine's own worst-case execution time is not strictly shorter than its timer period, the next interrupt would arrive before the previous one returned, so the compiler catches that with interruptSlow. Because the machine has one CIA 2 Timer A for timer interrupts, two timer interrupt routines can live in different cases, and the compiler flags two that would be active at the same time with interruptTimer.
What a Routine's Stores Add to the Frame
Because an interrupt routine can write directly to VIC-II registers, the compiler also accounts for any extra video-chip bus cycles those register writes can cause:
- Storing to a color register, horizontal scroll (
$D016), memory pointers ($D018), or a sprite's X coordinate, shape pointer, or color adds 0 extra bus cycles. - Storing to sprite enable (
$D015), sprite Y expansion ($D017), or a sprite's Y coordinate can make that sprite visible on additional scanlines, so the compiler conservatively budgets that sprite's 2-cycle DMA fetch across every line of the frame. - Storing to
$D011(which holds the 3-bit vertical scrollYSCROLL) can trigger an extra character-row badline fetch if it matches the low bits of the current scanline, so the compiler adds 43 cycles (one badline) for each$D011store a run can execute. - Writing to a register whose timing impact cannot be bounded statically—such as the processor banking port
$01—is flagged withframeUnbounded.
Routines in Cases
When you declare an interrupt routine inside a case, Adder automatically enables it when the case starts and switches it off when the case ends—so inactive cases add zero cycles to other cases' frames. Any labeled .byte counters inside the routine reset cleanly each time the case starts, as described in Start-Up Code in a Case. Pressing RUN/STOP or hitting a range-overflow stop cleanly shuts off all active interrupt routines before returning or halting.
Memory
Every Adder program loads at the standard Commodore 64 BASIC start address $0801, with machine code, tables, and variables laid out upward from there. The bottom line of the cost report shows your program's total byte size, and if code and data ever grow past $CFFF into the $D000 I/O area, the compiler lets you know with outOfMemory.
By default, the 1,024-byte text screen sits at $0400–$07FF (unless a bitmap canvas or case layout moves it to another VIC bank). Inside assembly, writing System.screen.text always resolves to the active screen's base address automatically.
Placed Values
Need a buffer or lookup table at a specific, fixed hardware address—say, in the free 4 KB RAM block at $C000–$CFFF? A placed value declared with at 0xC000 sits at that exact address, and both your high-level Adder code and your asm blocks can read and write it by name:
tiles: [16 of Number<u8>] at 0xC000
when System.screen.frame
asm
ldx #15
fill:
txa
sta tiles,x
dex
bpl fill
end
end
System.screen.border <- Color(tiles[3] % 16)To save you from mysterious memory collisions, the compiler checks the address span of every placed list against the rest of the memory map:
- overlapping the 6510 processor port at
$00–$01is flagged withplacedOnPort; - overlapping your compiled program code and variables is flagged with
placedOverProgram; - overlapping another placed list is flagged with
placedOverList; - overlapping compiler zero-page variables, the hardware stack (
$0100–$01FF), or the interrupt vectors is flagged withplacedOverRuntime; - overlapping a bitmap or sprite picture asset is flagged with
placedOverPicture; - overlapping the text screen is flagged with
placedOverScreen(unless that placed list is intentionally bound as the screen itself).
A single scalar placed value (one byte or word, often used for a custom hardware register) is checked only against program memory and other placed values, leaving you free to point it at memory-mapped hardware.
Memory a Block May Not Use
Adder keeps your program's variables and runtime state in zero page from $02 to $8F, assigning addresses automatically on each build. An asm block may read $02, and it has full use of scratch ($FB–$FE), while writing to $02 or touching any zero-page address from $03 through $8F directly is caught with assemblyZeroPage (including indexed instructions like lda $80,x whose reach overlaps $03–$8F). When your program starts from BASIC, Adder saves the zero-page range it uses and restores it if the program stops.
In a program that uses a bitmap canvas, the runtime configures the 6510 banking port ($01), the VIC-II memory bank ($DD00), the hardware interrupt vectors ($FFFA–$FFFF), and CIA 1 Timer B ($DC06, $DC07, $DC0F) at start-up to manage frame timing and drawing. In such a program, writing to any of those addresses from an asm block is blocked with bitmapAssembly.
Similarly, when a program switches between text and a bitmap canvas across cases (or between two different canvas modes), it runs with the BASIC and KERNAL ROMs banked out ($01 set to $35) so RAM underneath the ROMs is accessible. In those programs, calling or jumping into the ROM address ranges ($A000–$BFFF or $E000–$FFFF) or writing to the KERNAL RAM vectors at $0314–$0319 is caught with romAssembly.
Standalone Assembly
What if you want to write a pure 6510 assembly program from scratch—a tiny demo intro, a custom loader, or a standalone machine-code experiment—with no Adder runtime at all? The compiler has a dedicated --asm mode built right in. From the lab/ directory, run:
bun run compile --asm border.s -o border.prgThis assembles border.s into border.prg and writes a companion VICE symbol file border.lbl right next to it (if you leave off -o, both files are written alongside the .s source file, and if assembly fails, any existing output files are left untouched).
In --asm mode, the compiler uses the exact same 6510 instruction encoder as asm blocks, with zero inserted runtime and zero optimizer rewrites—every single byte in the .prg comes straight from your .s file. By default the origin starts at $0801 (and must finish below $D000), so if you want to launch your program from BASIC with RUN, start your file with a 12-byte BASIC SYS stub:
; Set the border to light blue and return to BASIC.
BORDER = $d020
.byte $0b, $08, $0a, $00, $9e, '2', '0', '6', '1', 0, 0, 0
start: lda #14
sta BORDER
rtsThose twelve .byte values form the tokenized BASIC line 10 SYS 2061, which jumps straight to $080D (2061 in decimal), where start: begins.
Standalone .s files also unlock a full macro-assembler toolkit beyond what inline asm blocks need:
- comments can start with either
;or//; - numbers accept decimal,
$hex,0xhex, or0bbinary, and%acts as the modulo (remainder) operator in expressions; NAME = expressionand.setdefine symbolic constants using full arithmetic, bit shifts, bitwise operators, comparisons, parentheses, and</>byte extractors;z:ora:before an operand forces 8-bit zero-page or 16-bit absolute addressing;.byte,.word,.ascii,.space count, fill,.balign, and.sectionlay out data and code sections;.macro/.endm,.rept/.endr, and conditional assembly (.if,.else,.endif,.ifndef,.error) expand at assembly time, while numbered local labels (1:referenced with1fforward and1bbackward) keep short loops tidy.
In a standalone .s file, the whole 64 KB address space is yours to manage; the assembler simply verifies branch distances and guards against the unstable and JAM opcodes.
Diagnostics
Whenever Adder spots something that doesn't quite fit—whether it is a typo, a number that could overflow its range, or a frame with too much work—it stops before building the program and explains what happened right at the line in question. Every compiler message has a short key that names the rule, such as wrongType. This chapter gathers every diagnostic key in one place, grouped by the part of the language it helps you with.
Each message starts with a clear one-line summary, followed by the rule behind it and, whenever possible, a ready-to-use Fix: written in Adder using your own variable names. Because the compiler speaks in terms of the names and numbers in your code, two programs that hit the same rule will see their own details in the message.
Below each key, you will find a sample one-line summary (Message: ...) and a link to the handbook section where you can read more and see working examples. The final two sections list the handful of keys that only show up in huge programs, and the keys kept as internal safety nets.
Reading a Program
Before checking types or ranges, the compiler reads the raw text of your program—its characters, literals, brackets, and line breaks. These messages help you fix syntax hiccups right away.
badCharacter
Message: @ is an invalid character: Adder doesn't use it.
Source Text explains this message.
badNumber
Message: 1_000 is not a number Adder understands.
Whole-Number Literals explains this message.
badEscape
Message: A backslash in text can only be followed by a quote or another backslash, not t.
String Literals explains this message.
unterminatedString
Message: This text starts with a quote mark but never ends.
String Literals explains this message.
stringNotPrintable
Message: This text holds a character the Commodore 64 screen can't show.
String Literals explains this message.
lineEndsInBracket
Message: This line ends inside the ( at column 27 of line 1.
Lines explains this message.
unexpected
Message: This line stops too soon.
Lines explains this message.
missingEnd
Message: This when block needs an end.
Lines explains this message.
strayEnd
Message: end has nothing to close.
Lines explains this message.
nestingExpression
Message: Brackets are nested more than 16 deep here.
Lines explains this message.
nestingType
Message: This type nests lists more than 16 deep.
Lists explains this message.
rangeNeeded
Message: A range was expected after the first number, not 1.
Range Bounds explains this message.
numberNeeded
Message: A number was expected here, not +.
Range Bounds explains this message.
arraySizeNeeded
Message: The size of a list must be a number, a constant's name or constant arithmetic in parentheses, not "a".
Lists explains this message.
typeNeeded
Message: A type was expected here, not =.
Variables explains this message.
resultTypeNeeded
Message: double needs a result type.
Parameters and Results explains this message.
parameterNeedsType
Message: The input a doesn't say what kind of value it takes.
Parameters and Results explains this message.
addressNeeded
Message: A memory address, as a number, was expected after at, not x.
Placing a Value in Memory explains this message.
assignmentNeeded
Message: x needs a declaration or binding.
Assignment explains this message.
actionNeeded
Message: <- can't follow here: a when handler holds updates and actions.
Handlers explains this message.
typeNeedsBrackets
Message: Number<u8> is a type, so it needs a value in brackets to convert.
Number Conversions explains this message.
chainedComparison
Message: A comparison can't be chained with another one.
Operators and Their Order explains this message.
parenthesize
Message: Ranges and ?? can't be mixed without brackets.
Operators and Their Order explains this message.
numberMember
Message: A number needs parentheses before a .: (3.5).
Members explains this message.
structColon
Message: This struct field needs a colon.
Struct Blocks explains this message.
structLabel
Message: 2 can't start a struct field.
Struct Blocks explains this message.
includeNeedsName
Message: include must be followed by a file name in quotes.
Include and Project Files explains this message.
textNeeded
Message: .text must be followed by some text in quotes.
Data explains this message.
ifNeedsThen
Message: An if needs then after its condition.
If Statements explains this message.
inlineIfLine
Message: An if ... then that starts its answer on the same line ends on that line.
One-Line Form explains this message.
statementAfterArm
Message: This line already has its whole body after the ->.
Match Statements explains this message.
dottedArm
Message: A line of a match names its case without a dot, so .b is written b.
Patterns explains this message.
Names and Declarations
These messages help you keep names clear and distinct across constants, variables, reactive bindings, functions, op declarations, and hardware outputs.
builtinName
Message: Color is a built-in type, so a declaration can't use that name.
Names explains this message.
capitalizedDeclaration
Message: Capitalized names belong to types; Speed cannot be declared here.
Names explains this message.
internalName
Message: __count begins with two underscores, which the compiler keeps for names of its own.
Names explains this message.
unknownName
Message: hue isn't declared here yet.
Names and Scope explains this message.
duplicateDeclaration
Message: x is already declared.
Names and Scope explains this message.
duplicateParameter
Message: This function has two inputs called a.
Parameters and Results explains this message.
shadowing
Message: limit is already a constant.
Names and Scope explains this message.
unknownFunction
Message: f isn't a function Adder knows.
Functions explains this message.
unknownType
Message: foo isn't a type.
Type Declarations explains this message.
constantChanges
Message: best can't be declared with const, because score changes while the program runs.
Constants explains this message.
eventConstant
Message: An event happens over time and cannot be a constant or a variable.
Events explains this message.
selfAssignment
Message: n = n gives the value back to itself.
Assignment explains this message.
unreadDeviceName
Message: border is declared here, but nothing reads it.
Naming a Device explains this message.
resultUnused
Message: The result of twice(3) is never used.
Calls and Device Actions explains this message.
recursion
Message: f can't call itself.
Functions explains this message.
functionSets
Message: A function cannot change g, which belongs to the program.
What a Function May Read explains this message.
operationScope
Message: A function cannot read the program's bonus directly.
What a Function May Read explains this message.
randomImpure
Message: random gives a new answer every time, so a function can't use it.
What a Function May Read explains this message.
functionBodyValues
Message: A function's body is made of values, and this line is something else.
Statements in Functions explains this message.
localImmutable
Message: value already has a value in this body and can't be assigned again.
Statements in Functions explains this message.
valueBlockEmpty
Message: This block holds no value.
Parameters and Results explains this message.
memberNeedsOwner
Message: Spot.show needs the value it belongs to as its first input.
Member Functions explains this message.
memberOverlap
Message: These members share a name on the same type.
Member Functions explains this message.
parameterAgreement
Message: The parameter T needs one type or length.
Type Parameters explains this message.
parameterTypeName
Message: X is a type parameter.
Type Parameters explains this message.
opHolds
Message: total_of cannot hold a variable declared with var.
Ops explains this message.
loopInOp
Message: f is an op, and an op cannot hold a loop.
Ops explains this message.
opControlParameter
Message: reset names an op instance control, so it cannot be an op parameter.
on: and reset: explains this message.
operationBody
Message: End empty with the value its op returns.
Ops explains this message.
memoryOutsideBinding
Message: cycle remembers earlier values, so it only makes sense in a <- line.
Remembering With previous explains this message.
loopMemory
Message: i.sum() keeps a memory for the line that calls it, and a loop would run that line once for each turn.
Loops and the Frame explains this message.
recurrenceNeedsFrom
Message: ticks reads its own previous value, so previous needs from:.
Destination and start explains this message.
forwardNeedsFrom
Message: b is declared further down, so previous needs from:.
A Binding That Reads Itself explains this message.
forwardNeedsType
Message: behind is read before its line, so it needs a declared range.
A Binding That Reads Itself explains this message.
recurrenceNeedsType
Message: x reads its own previous value, so it needs a declared range.
A Binding That Reads Itself explains this message.
bindingLoop
Message: tick reads itself in the same frame.
A Binding That Reads Itself explains this message.
overlap
Message: An earlier declaration already sets this destination.
Outputs explains this message.
notAnOutput
Message: a isn't something the program can draw to.
Outputs explains this message.
bindingNeedsOutput
Message: A <- line like this must say which part of the screen it sets.
Outputs explains this message.
functionBinding
Message: f is a function, so a binding cannot target it.
Output Declarations explains this message.
outputBody
Message: An output body holds bindings or assembly.
Output Declarations explains this message.
outputTarget
Message: v is not an output this body can bind.
Output Declarations explains this message.
outputState
Message: An output cannot write n.
Output Declarations explains this message.
includeMissing
Message: The project has no file lib/world.adder.
Include and Project Files explains this message.
Types and Ranges
These messages appear when a type or number range doesn't match the values you give it, helping you catch out-of-bounds numbers before the program ever starts.
numericFormat
Message: i32 is not a numeric storage format.
Storage Formats explains this message.
rangeEmpty
Message: The range 5...1 holds no numbers.
Range Bounds explains this message.
rangeEndNotFixed
Message: widest is not known before the program runs, so a range cannot end at it.
Range Bounds explains this message.
rangeFixedBounds
Message: width - 1 is a calculation, and a range ends at a number, a member, a constant name or constant arithmetic in parentheses.
Range Bounds explains this message.
rangeBoundLimit
Message: A range endpoint can contain at most 128 expression nodes and have depth 32.
Range Bounds explains this message.
rangeNeedsFixedIntegers
Message: A range of values needs whole numbers at both ends, written out.
Range Bounds explains this message.
rangeLength
Message: This range holds too many or too few numbers.
Range Bounds explains this message.
rangeDoesNotFitType
Message: The range 0...60 doesn't fit in a Color.
Narrowed Named Sets explains this message.
rangeOnStruct
Message: A range cannot follow a Point.
Narrowed Named Sets explains this message.
rangeNoRepresentation
Message: both can reach 0 to 131,070, wider than any number holds.
Results and Their Ranges explains this message.
literalDoesNotFit
Message: level allows 0 to 0.996, but this value is 0.999.
Fractions explains this message.
constantOutOfRange
Message: 300 doesn't fit in the allowed range 0 to 255.
Values Outside a Range explains this message.
fractionIntoWhole
Message: 0.3 has a fraction, and speed holds whole numbers from 0 to 255.
Decimal Literals explains this message.
wrongType
Message: paused: This is a number from 0 to 255, but a yes-or-no value is needed here.
Types explains this message.
arraySize
Message: A list needs between 1 and 65,535 places.
Lists explains this message.
emptyArray
Message: An empty list doesn't say what it would hold.
List Values explains this message.
textForOnePlace
Message: A single place on the screen holds one character, not a text of 1 character.
Characters and Screen Codes explains this message.
hardwareArithmetic
Message: Color has no addition.
Named Sets explains this message.
hardwareConversion
Message: A Note cannot convert directly to Color.
Named Sets explains this message.
memberNeedsType
Message: Nothing here gives .blue a type.
Named Sets explains this message.
nextNeedsGroup
Message: next moves on to the next name in a group of names.
Named Sets explains this message.
Structs and Enums
These messages guide you when declaring custom struct and enum types, building their values, and matching on their cases and fields.
structName
Message: A struct's name starts with a capital letter, and speed does not.
Structs explains this message.
aliasName
Message: A type's name starts with a capital letter, and screen does not.
Type Aliases explains this message.
structFieldNeedsType
Message: The field x of Speed doesn't say what kind of value it holds.
Structs explains this message.
structFieldRepeated
Message: Speed has two fields called x.
Structs explains this message.
structSizeConstant
Message: size must be a constant because Buffer's memory is fixed when the program is built.
Type Parameters explains this message.
structBody
Message: An Item body holds fields and functions.
Struct Blocks explains this message.
structDefaultConstant
Message: The default for value must be a constant.
Struct Blocks explains this message.
structFieldScope
Message: x is already a field of Item.
Struct Blocks explains this message.
structMakerOwner
Message: A maker inside Item belongs to Item, not Other.
Struct Blocks explains this message.
structMutationUndeclared
Message: add_one changes its Score but is declared with fn.
Struct Blocks explains this message.
structMutationMethodOnly
Message: read cannot be declared with mut fn.
Struct Blocks explains this message.
structChangingPlace
Message: bump is declared mut fn and needs an Item place that can change.
Struct Blocks explains this message.
structPresence
Message: xs[0] may hold no value.
Struct Blocks explains this message.
structAction
Message: ping gives no value.
Struct Blocks explains this message.
structNeedsContext
Message: (a: 1) doesn't say which struct it is.
Structs explains this message.
structPositional
Message: (1, -2) gives values without saying which field each one fills.
Structs explains this message.
duplicateField
Message: The field x appears twice in this struct.
Structs explains this message.
unknownField
Message: z isn't a field of a sprite.
Structs explains this message.
enumName
Message: An enum's name starts with a capital letter, and mode does not.
Enums explains this message.
enumNoCases
Message: The enum Mode has no cases.
Enums explains this message.
enumCaseRepeated
Message: Mode has two cases called a.
Enums explains this message.
enumCaseName
Message: A case of Mode starts with a lowercase letter, and A does not.
Enums explains this message.
noCase
Message: Mode has no case called c.
Enums explains this message.
notAMember
Message: playng is not one of the members of this enum.
Enums explains this message.
caseNeedsFields
Message: The case a of Choice carries p, so a value of it gives them.
Enums explains this message.
caseHasNoFields
Message: The case b of Choice carries no fields.
Enums explains this message.
caseFieldRepeated
Message: The field p of a is given twice.
Enums explains this message.
casePositional
Message: 1 gives a value without saying which field it fills.
Enums explains this message.
enumCompare
Message: e == .a(p: 1) compares a value of Choice, and == compares only with a case that carries no fields.
Enums explains this message.
enumFieldRead
Message: A value of Choice carries p in some of its cases only, so a dot cannot read it.
Enums explains this message.
casePatternFields
Message: The case b carries no fields, so its line names none.
Patterns explains this message.
armFieldRepeated
Message: This line takes the field p twice.
Patterns explains this message.
positionCaseAmbiguous
Message: More than one enum has a case called east: Path and Compass.
Bit Operations explains this message.
positionCaseUnknown
Message: No enum has a case called eats.
Bit Operations explains this message.
bitwisePositionCases
Message: Wide has 9 cases, but this number has only 8 bits.
Bit Operations explains this message.
Expressions and Conversions
These messages help you with arithmetic operators, explicit conversions, list indexing and slicing, value if and match expressions, function calls, and built-in members.
cannotCombine
Message: shade and 2 can't be used with ==.
Comparisons explains this message.
needsNumbers
Message: + works on numbers, but this is a Mode.
Arithmetic explains this message.
negationNeedsNumber
Message: A minus sign in front works on numbers, but this is a ScreenCode.
Arithmetic explains this message.
operationNeedsRange
Message: +% needs a declared range to wrap in.
Wrapping and Clamping explains this message.
rangeExceeded
Message: speed takes values from 0 to 9, but this one can reach 15.
Checked Arithmetic explains this message.
divisionByZero
Message: This divides by zero.
Division and Remainder explains this message.
needsBooleans
Message: and joins two yes-or-no values.
and, or and not explains this message.
notNeedsBool
Message: not turns yes into no, but this is a number from 0 to 255.
and, or and not explains this message.
optionalCompare
Message: a and b can't be compared.
Comparisons explains this message.
coalesceNeedsOptional
Message: ?? supplies a stand-in for a value that might be missing, but this is a number from 0 to 255, which is never missing.
Optionals and ?? explains this message.
conversionNeedsNumber
Message: Only numbers can be converted to Number<u8>.
Number Conversions explains this message.
conversionOutOfRange
Message: 300 is outside a Number<u8>, which holds 0 to 255.
Number Conversions explains this message.
conversionDropsFraction
Message: speed has a fraction, and a Number<u8> holds only whole numbers.
Number Conversions explains this message.
digitsNeedsWhole
Message: 0.5 holds a fraction, and digits writes only whole numbers.
Other Number Functions explains this message.
sqrtInput
Message: sqrt needs a whole number from 0 to 65,535.
Other Number Functions explains this message.
bitwiseReceiver
Message: bitwise_and needs a number with a chosen unsigned width.
Bit Operations explains this message.
bitwiseArgument
Message: This position is 8, but bit_is_set allows 0 through 7.
Bit Operations explains this message.
indexNeedsArray
Message: Square brackets pick one place out of a list, but this is a number from 0 to 255.
Indexes explains this message.
indexNotInteger
Message: A place in a list is chosen with a whole number, but this is a number from 0.0 to 255.996.
Indexes explains this message.
indexOutOfBounds
Message: Place 4 doesn't exist in a list of 4.
Indexes explains this message.
arrayShapes
Message: These two lists have different sizes: [2 of Number<u8>] and [3 of Number<u8>].
Lifted Operations explains this message.
argumentShapes
Message: These lists have different sizes.
Lifted Operations explains this message.
sliceLengthUnknown
Message: The length of left..<width isn't known when the program is built.
Windows explains this message.
sliceOutOfBounds
Message: This range goes past the end of a list of 40.
Windows explains this message.
sliceStartOutside
Message: A slice of 4 places can't start at a, which is 6 to 7.
Windows explains this message.
sliceOfColumn
Message: A slice can't start at a changing place in shots.x, which is the x of every place in shots.
Windows explains this message.
sliceBindingWrite
Message: strip names part of xs, which can't change, so its places can't be written.
Windows explains this message.
rectangleRanges
Message: A rectangle takes two ranges, rows and then columns, but 1 is not a range.
Rectangles explains this message.
rectangleRows
Message: A rectangle picks rows and columns, but this is [8 of Number<u8>], and its places are not rows.
Rectangles explains this message.
rectangleBase
Message: A rectangle is cut from a list that has a name, and pick(xs) is a calculation.
Rectangles explains this message.
rectangleShape
Message: This rectangle is 2 rows of 3, but the place for it is 3 rows of 3.
Rectangles explains this message.
rectangleWrite
Message: xs[0..<2, 0..<3] is a rectangle, and a binding with <- has no handler to set it row by row.
Rectangles explains this message.
joinLists
Message: ++ joins two lists of the same kind of thing, and this joins [2 of Number<u8>] to Number<u8>.
Joining Lists explains this message.
collectionArrow
Message: This collection arrow cannot be used here.
map and filter explains this message.
reductionInput
Message: any needs a list of yes-or-no values.
Reductions explains this message.
sumNeedsNumber
Message: sum adds up numbers, so its result must be a number type.
Reductions explains this message.
listChangeContext
Message: add changes a list, so call it in a when handler or a function that keeps the list.
Calls and Device Actions explains this message.
listRemoveAction
Message: remove empties a place; it gives no value back.
Calls and Device Actions explains this message.
valueIfElse
Message: An if that gives a value needs an else.
if explains this message.
conditionalMatch
Message: This conditional match needs when lines and a final else -> line.
match Without a Value explains this message.
missingCoverage
Message: This match doesn't say what to do for every possible value.
match on a Value explains this message.
duplicateArm
Message: 1 is already handled by an earlier line of this match.
match on a Value explains this message.
invalidPattern
Message: A line of a match starts with nil, a number, or a name.
match on a Value explains this message.
nilArm
Message: nil is an answer only for a value that might be missing.
match on a Value explains this message.
badArgument
Message: apart doesn't take an input called c, or it was given twice.
Calls explains this message.
missingArgument
Message: digits needs count, and it was left out.
Calls explains this message.
argumentRange
Message: a holds 24 to 320, but short_value is 3.
Calls explains this message.
argumentListSize
Message: xs takes a list of 4, but this list holds 3.
Calls explains this message.
noMember
Message: A number from 0 to 255 has nothing called foo.
Members explains this message.
memberValue
Message: This member is stored; read it without parentheses.
Members explains this message.
randomNeedsRange
Message: random picks from a range of numbers written out.
Random Numbers explains this message.
mustBeFixedWhole
Message: The history length must be a whole number from 1 to 65,535, written out.
Reductions explains this message.
Events, Handlers and Waiting
These messages guide you when working with events, when handlers, wait timelines, forever loops, top-level cases, and queues.
whenSubject
Message: shot is a stored optional, so when has no frame to filter.
Handlers explains this message.
onNeedsEvent
Message: This doesn't name an event; it is a yes-or-no value.
on: and reset: explains this message.
notEvent
Message: not turns yes into no, but an event is not a yes-or-no value.
Events and Conditions explains this message.
andEvents
Message: An event cannot be joined to another event with and.
Events and Conditions explains this message.
orNeedsEvents
Message: or joins two events, or two yes-or-no values, but not one of each.
Joined Events explains this message.
andOnLine
Message: A when line filters its event with when, not and.
Filters explains this message.
eventNoValue
Message: lives.changed carries no value, so there is nothing to give v.
Events That Carry a Value explains this message.
timeNeedsRange
Message: The count member needs a binding with its own range.
Ranges and Periods explains this message.
timePeriod
Message: The every member needs a whole period in range.
Ranges and Periods explains this message.
timePayload
Message: The latest member needs an event that carries a value.
Events That Carry a Value explains this message.
guardOutsideHandler
Message: guard belongs in a when handler.
Guard explains this message.
waitOutsideHandler
Message: wait belongs in a when handler.
Wait explains this message.
waitNeedsEventOrFrames
Message: wait takes an event or a number of frames, and true is a yes-or-no value.
Wait explains this message.
waitNoFrames
Message: wait(0) would not pause.
Waiting for Frames explains this message.
waitMayNotPause
Message: wait(delay) would not pause when delay is 0.
Waiting for Frames explains this message.
waitTimeUnit
Message: wait(minutes: 2) has to count time in seconds: or ms:, one of them.
Waiting for a Time explains this message.
waitTimeAmount
Message: wait(seconds: 0) needs a fixed number above zero.
Waiting for a Time explains this message.
waitTimeLong
Message: wait(seconds: 3000) is 182978 frames on an early NTSC machine, more than a wait can count.
Waiting for a Time explains this message.
waitEventReadsLocal
Message: wait System.keyboard[s.key].released reads s, which this handler gave its value.
Waiting for an Event explains this message.
waitOnTimerLoop
Message: tick fires once, 500 milliseconds after the program starts, so this wait would never end on the loop's second turn.
Waiting for a Timer explains this message.
waitOnTimerElsewhere
Message: tick fires once, 2 seconds after the program starts, so a wait for it can only work in a when start of the program or case that declares it.
Waiting for a Timer explains this message.
waitOnTimerLate
Message: tick fires once, 500 milliseconds after the program starts, and this wait can begin after that, so it would never end.
Waiting for a Timer explains this message.
waitOnTimerTwice
Message: tick fires once, and the wait on line 3 already uses it.
Waiting for a Timer explains this message.
foreverNeverWaits
Message: This forever can go round again without a wait.
Forever explains this message.
breakInForever
Message: break cannot leave a forever.
Forever explains this message.
caseDeclaration
Message: A type can't be declared inside a match.
What a Case Holds explains this message.
outputMatch
Message: An output's lines can't hold a match of declarations.
Cases explains this message.
caseNotBuilt
Message: A queue isn't built inside a match yet.
What a Case Holds explains this message.
caseStartLong
Message: Starting the case true needs 36,219 cycles in one frame, and a PAL frame holds 19,656.
Changing Cases explains this message.
queueForm
Message: This queue cannot be used this way.
Queues explains this message.
queueConsumer
Message: The queue q already has a take.
Queues explains this message.
Statements and Loops
These messages help you with assignments, if statements, unwrapping optionals in conditions, and for loops.
notAssignable
Message: limit is a constant, so it can't be given a new value.
Places That Cannot Be Assigned explains this message.
assignUndeclared
Message: scor isn't declared, and a statement in a when handler can't introduce a name.
Places That Cannot Be Assigned explains this message.
wrongLength
Message: This list holds 3 things, but 2 places are being filled.
Elements, Fields and Windows explains this message.
conditionNeedsBool
Message: A condition must be a yes-or-no value, but this is a number from 0 to 255.
If Statements explains this message.
bareOptional
Message: shot may be missing, and a condition needs a yes or no.
Conditional Names explains this message.
assignExisting
Message: done is already a name, so = here would not compare. Did you mean ==?
Conditional Names explains this message.
alwaysSucceeds
Message: if gap = n + 1 always succeeds, so the body would always run.
Conditional Names explains this message.
assignedAgain
Message: s was assigned in a condition, so it can't be given a new value.
Conditional Names explains this message.
secondAssignment
Message: An if condition assigns one name, and writes it first.
Conditional Names explains this message.
orAfterAssignment
Message: n would not be assigned when only the other side of or happens.
Conditional Names explains this message.
whenInCondition
Message: if tests with and, not when.
If Statements explains this message.
loopAtTopLevel
Message: A for goes inside a when handler or a function, not at the top of the program.
For Loops explains this message.
forNeedsList
Message: A for goes through a list or a range, and x is a number from 0 to 9.
For Loops explains this message.
forRangeNeedsWhole
Message: A for counts whole numbers, and 0.5...3 is not one.
Over a Range explains this message.
loopVariableSet
Message: i is the loop's value for this turn, so it can't be given a new value.
The Loop's Name explains this message.
loopListChanged
Message: This loop goes through xs, so a line inside it cannot change xs.
Over a List explains this message.
breakOutsideLoop
Message: break has no loop to leave here.
Break explains this message.
loopOverrun
Message: This loop can't finish within one PAL frame.
Loops and the Frame explains this message.
Devices
These messages help you navigate the Commodore 64 hardware under System: device paths, the screen, text regions, pixel canvases, sprites, the keyboard, sound voices, and the clock.
systemAlone
Message: System is the machine itself, not a value.
Paths explains this message.
noDevice
Message: System has no device called banana.
Paths explains this message.
noPart
Message: System.joystick[2] has no dz.
Paths explains this message.
noInstance
Message: System.joystick[3] is not a joystick the machine has.
Devices With Numbers explains this message.
instanceNeeded
Message: The machine has more than one joystick, so System.joystick needs the number of the one you mean.
Devices With Numbers explains this message.
instanceRange
Message: port picks the joystick, but it is 3.
Devices With Numbers explains this message.
chosenDevice
Message: System.joystick[p] is the joystick that a number picks while the program runs, so it is not a device a name can stand for.
Naming a Device explains this message.
deviceNotValue
Message: System.keyboard is a device, and only a constant holds one.
Naming a Device explains this message.
deviceNamed
Message: System.screen is named screen; write screen.border.
Naming a Device explains this message.
deviceHasNoPlaces
Message: System.screen is a device, and a device has parts, not places.
Paths explains this message.
notBuilt
Message: System.reu is not built yet.
Devices Not Built Yet explains this message.
registerAssigned
Message: reg is a register, so a statement can't give it a value.
Hardware Registers explains this message.
hardwareBound
Message: reg has a binding, so it can't also be set by hand.
Hardware Registers explains this message.
hardwareInvalid
Message: This memory address and its type don't fit.
Hardware Registers explains this message.
rasterSelection
Message: The rows picked here must be written out and be between -51 and 210.
Color Bands explains this message.
rasterLine
Message: Row 219 exists only on PAL; the last row every standard has is 210.
Color Bands explains this message.
textScreenOutput
Message: This is a screen output. Print into a region of the screen, or bind a value to it with <-. For ink: This is a screen output. A handler cannot assign it; bind a color to it with <-, as in System.screen.ink[row] <- .yellow.
Text and Ink explains this message.
textLayout
Message: This Text placement can't be used: a height needs an explicit width.
Text Placement explains this message.
multicolorInk
Message: Multicolor text (line 1) keeps its ink to the first eight colors, Color.black through Color.yellow.
Multicolor Text explains this message.
multicolorScene
Message: System.screen.multicolor needs one binding for the whole text screen.
Multicolor Text explains this message.
multicolorNeedsMode
Message: .multicolor(color) needs multicolor text.
Multicolor Text explains this message.
multicolorBeside
Message: Text next to multicolor text (line 1) cannot use colors from Color.orange on yet.
Multicolor Text explains this message.
rectangleBinding
Message: A part of the screen is a region.
Regions and Printing explains this message.
regionArgument
Message: A region has no rows.
Regions and Printing explains this message.
regionConstant
Message: A region's row is a whole number written out.
Regions and Printing explains this message.
regionOutside
Message: This region does not fit inside the screen.
Regions and Printing explains this message.
regionOverlap
The message names both regions: the second one uses cells that the first one already holds.
Regions and Printing explains this message.
regionPlace
Message: A region is declared on a line of its own.
Regions and Printing explains this message.
regionCall
Message: Region is not a name a program can call.
Regions and Printing explains this message.
regionValue
Message: log is a part of the screen, not a value.
Regions and Printing explains this message.
regionInFunction
Message: A function or work cannot use log.print.
Regions and Printing explains this message.
regionMove
Message: move takes a row and a column.
Region Members explains this message.
regionText
Message: This text does not fit inside log.
Region Members explains this message.
pageShape
Message: This grid has 4 rows of 2 cells, but this region has 3 rows of 2 cells.
Region Members explains this message.
printList
Message: print needs a list of characters.
Printing explains this message.
printElements
Message: print accepts lists of Char or ScreenCode; this list holds Color.
Printing explains this message.
printControl
Message: This text contains a control character at position 1.
Printing explains this message.
printExpression
Message: print changes a region and returns no value.
Printing explains this message.
pageOutruns
Message: log can scroll faster than the screen can show it.
Printing explains this message.
canvasArgument
Message: canvas cannot take colour: this way.
Canvases explains this message.
canvasLeaves
Message: A canvas of 40 by 8 cells at row 20, column 0 does not fit the screen.
Canvases explains this message.
canvasMore
Message: other is a second canvas, and the screen shows one canvas at a time.
Canvases explains this message.
canvasAlone
Message: picture is a canvas, and a canvas is a part of the screen, not a value.
Canvases explains this message.
canvasText
Message: This text update next to a canvas (line 1) is not built yet.
Canvases explains this message.
modePart
Message: System.screen.text belongs to text, and this case shows a canvas (line 4).
Canvases explains this message.
bitmapText
Message: System.screen.text belongs to text, and this program shows a canvas (line 1).
Canvases explains this message.
bitmapBackground
Message: A hires canvas has no System.screen.background; use its paper.
Canvases explains this message.
bitmapPaper
Message: A multicolor canvas has no paper; use System.screen.background.
Canvases explains this message.
multicolorCanvas
Message: picture is a canvas, and the multicolor text of line 1 is already on the screen.
Canvases explains this message.
bitmapList
Message: A canvas shows a list of drawings.
Drawings explains this message.
drawingElement
Message: A drawing is picture.place(...), picture.fill(...), corners.outline(), nil, or an if that chooses between them.
Drawings explains this message.
placeOutside
Message: place makes a drawing, and drawings go in the list bound to a canvas's drawings.
Drawings explains this message.
outlineOutside
Message: outline makes a drawing, and drawings go in the list bound to a canvas's drawings.
Drawings explains this message.
outlineInput
Message: This outline has an input it cannot use.
Drawings explains this message.
regionFontLayout
Message: This combination of region fonts and screen updates is not built yet.
Regions and Printing explains region fonts. The compiler must keep all their visible glyphs and screen updates together; some changing fonts or screen combinations still need another complete-picture path.
placeInput
Message: This place has an input it cannot use.
Drawings explains this message.
fillRange
Message: width must be at least 1, and here it is 0.
Drawings explains this message.
lettersInput
Message: This lettering has an input it cannot use.
Drawings explains this message.
lettersMode
Message: A hires font cannot letter a multicolor canvas.
Drawings explains this message.
bitmapMixed
Message: One place of a canvas's drawings list holds a picture or an outline, not both.
Drawings explains this message.
drawingFnOutside
Message: two returns drawings, and drawings go in the list bound to a canvas's drawings.
Drawings explains this message.
drawingFnBody
Message: two returns drawings, and its body is not a list of them.
Drawings explains this message.
drawingFnCount
Message: two says it returns 2 drawings, and its list has 1.
Drawings explains this message.
placeRange
Message: x must stay within 0...312 for this picture, and here it is 320.
Where a Drawing May Go explains this message.
placeShifted
Message: A fill that starts or ends between whole bytes has its width written out.
Where a Drawing May Go explains this message.
placeColoredShifted
Message: A picture with its own colors sits on whole cells, so x is a multiple of 8.
Where a Drawing May Go explains this message.
placeOdd
Message: In a multicolor canvas a pixel is two wide, so x has to be even.
Where a Drawing May Go explains this message.
placeCells
Message: A picture with its own colors sits on whole cells, so y is a multiple of 8.
Where a Drawing May Go explains this message.
placeColors
Message: A picture with its own colors needs a canvas ink and paper that stay the same.
Where a Drawing May Go explains this message.
spriteIndex
Message: The sprite number must be written out, from 0 to 7.
Sprites as a List explains this message.
spriteNeedsStruct
Message: A sprite is set with a struct that gives its x and y.
Sprites explains this message.
spriteFieldRequired
Message: This sprite has no x.
Sprites explains this message.
spriteFieldRange
Message: The sprite's x can reach 600, but it must stay within -24...487.
Sprites explains this message.
spriteBoundTwice
Message: Sprite 0 is set on line 1 and again on line 2.
Sprites explains this message.
spriteNotReadable
Message: System.screen.sprite can't be read: the sprites are only set, never read back.
Sprites explains this message.
spriteListLength
Message: System.screen.sprite[0..<4] has 4 places, but the list has 3 sprites.
Sprites as a List explains this message.
spriteListNeedsSprites
Message: A list of sprites is set with a list of Sprite values, or of values that may be missing.
Sprites as a List explains this message.
keyLiteral
Message: System.keyboard[.space] names a key that is known when the program is written, so the key is a part of the device.
Keys explains this message.
keyChoice
Message: A key is chosen with one Key value, but this is [3 of Key].
Keys explains this message.
keyboardLineCapacity
Message: The keyboard line needs a capacity.
The Typed Line explains this message.
controlPortOne
Message: System.joystick[1].fire and System.paddle[1] (line 1) are both on control port 1.
Control Port 1 explains this message.
toneEnvelopeConflict
Message: Give tone a decay or an envelope, not both.
Tone explains this message.
attackZero
Message: Attack runs from 1 (8 ms) to 15 (8 seconds).
Tone explains this message.
noteWrap
Message: Notes do not wrap; a tune cycles its index.
Tone explains this message.
playVoice
Message: voice is 1, 2 or 3, written out, and v is not.
Play and Release explains this message.
playOutsideOn
Message: play can only be used inside a when handler.
Play and Release explains this message.
playIsAction
Message: play makes a sound; it does not give back a value.
Play and Release explains this message.
voiceBindingIndex
Message: A voice that holds a value is 1, 2 or 3, written out, and v is not.
Binding a Voice explains this message.
voiceBoundTwice
Message: Voice 1 is set by two lines at the same level, on lines 1 and 2.
Binding a Voice explains this message.
voiceBoundAndCalled
Message: Voice 1 is bound on line 2 and also takes play on line 4.
Play and Release explains this message.
voiceCalledAround
Message: Voice 1 takes play on line 3, outside the case that binds it on line 7.
Binding a Voice explains this message.
periodicPeriod
Message: A clock period needs a positive constant duration.
Intervals and Timers explains this message.
periodicOwnership
Message: This program cannot keep the clock source's timing guarantee.
Intervals and Timers explains this message.
Costs and the Frame
These messages help you keep your program inside the Commodore 64's frame budget and memory map—checking frame timing, fast choices, background work jobs, and available RAM.
frameOverrun
Message: The work in one PAL frame, with the screen's share of the processor and the runtime's own steps, can take up to 22,734 cycles, and the frame holds 19,656.
A Frame Too Long explains this message.
frameUnbounded
Message: The compiler can't tell how long the asm block on line 3 takes, so it can't check that the frame fits.
Work Without a Limit explains this message.
segmentOverrun
Message: when start has a stretch of code, the code after the wait on line 9, that can't finish within one PAL frame.
A Stretch Between Waits explains this message.
printBudget
Message: This print cannot finish in one frame on early NTSC.
Work Divided Between Frames explains this message.
fastHead
Message: fast can't mark a var declaration.
Where fast Cannot Go explains this message.
codeMarkConflict
Message: fast and small both mark this line.
Keep one mark on a head. A composed row also cannot serve regions marked both fast and small; the message names both source lines.
Where fast Cannot Go explains this message.
fastRepeated
Message: fast is written twice on this head.
Where fast Cannot Go explains this message.
fastAsm
Message: fast can't mark an asm block.
Where fast Cannot Go explains this message.
jobForm
Message: This work can't run in that form: give within one count, as in within(frames: 40).
Work explains this message.
jobUnbounded
Message: The work has no proven bound.
Parts of a Job explains this message.
jobPartTooLong
Message: Running the loop on line 4 to its end needs 19,697 cycles on PAL, more than a frame.
Parts of a Job explains this message.
jobFrameFull
Message: The frame needs 134,403 cycles on early NTSC with step() in it, and a frame has 16,768.
Parts of a Job explains this message.
jobStepDeadline
Message: A job that a handler steps cannot take a deadline.
Parts of a Job explains this message.
jobDeadline
Message: This job deadline is not proven.
Parts of a Job explains this message.
outOfMemory
Message: This program needs more memory than the Commodore 64 has free.
When the Compiler Spends Bytes Itself explains this message.
bitmapSecondImage
Message: Keeping the old bitmap needs another 9,024 bytes for a whole picture, colors and sprite shapes; the largest usable screen storage area has 6,153 bytes left.
When the Compiler Spends Bytes Itself explains this message.
bitmapImageEnd
Message: This bitmap program is 236 bytes too large: its code and data must fit in 50,175 bytes.
When the Compiler Spends Bytes Itself explains this message.
Assembly and Placement
These messages help you write safe 6510 asm blocks, assembly functions, hand-written bindings, interrupt routines, and memory-mapped at declarations.
topLevelAssembly
Message: Assembly can't sit at the top of a program, outside a when handler.
Assembly Blocks explains this message.
assemblyCli
Message: cli can only appear in a when start block.
Registers and Flags explains this message.
unknownInstruction
Message: foo isn't a 6502 instruction Adder can assemble.
Instructions explains this message.
badInstructionForm
Message: lda can't be written that way.
Instructions explains this message.
assemblyForm
Message: stx can't be written that way on the original 6502.
Instructions explains this message.
assemblyUnstable
Message: lax # can't be used: LXA ($AB) is an unstable opcode.
Instructions explains this message.
assemblyJam
Message: jam can't be used: the JAM opcodes ($02, $12, $22, $32, $42, $52, $62, $72, $92, $B2, $D2 and $F2) stop the processor.
Instructions explains this message.
operandSize
Message: The value 300 doesn't fit in 1 byte.
Operands explains this message.
branchTooFar
Message: A branch has to go 130 bytes, which is further than the 6502 can reach in one step (127 forward or 128 back).
Labels explains this message.
assemblyLabelCollides
Message: The assembly label short_value is already the name of something in the program.
Labels explains this message.
assemblyLabelSelector
Message: An assembly label doesn't have a low or high byte of its own.
Labels explains this message.
assemblyUnknownName
Message: later isn't a name the assembly can see.
Names in Assembly explains this message.
A label of a case's when start is seen only in that case, so naming it from another case gets the message with two more lines. They say that it is a label in the when start on a given line, and that only the blocks in that case can name it, and they advise labeling a cell in a when start outside the match to share it between cases.
assemblyNeedsScalar
Message: a holds more than one number, so assembly can't use it as one value.
Values With an Address explains this message.
assemblyTooWide
Message: t takes 3 bytes, more than assembly can read as one value.
Values With an Address explains this message.
assemblyWide
Message: pos is two bytes wide, so assembly has to say which byte it wants.
Values With an Address explains this message.
assemblyNarrow
Message: short_value is one byte wide, so it has no high byte.
Values With an Address explains this message.
assemblyByteOffset
Message: A byte offset on seed needs a list of bytes and an offset within it.
Values With an Address explains this message.
assemblyNoAddress
Message: spot doesn't live in memory, so assembly has no address for it.
Constants explains this message.
assemblyConstantWrite
Message: table is a constant, so assembly cannot write it.
Constants explains this message.
assemblyInputWrite
Message: System.screen.frames is a sampled input and cannot be written.
Devices explains this message.
assemblyScratch
Message: scratch is four bytes, so it takes an offset from 0 to 3 and no .lo or .hi.
Scratch explains this message.
assemblyWritesName
Message: writes(missing) must name a variable or a binding.
The writes Clause explains this message.
cyclesNeeded
Message: cycles needs a whole number, and found x.
The cycles Clause explains this message.
cyclesRange
Message: cycles(30...20) is not a range of cycles.
The cycles Clause explains this message.
cyclesStandard
Message: cycles has figures for pal, ntsc and early_ntsc, and found secam.
The cycles Clause explains this message.
cyclesOneStandard
Message: cycles gives a figure for pal and none for ntsc.
The cycles Clause explains this message.
cyclesRepeated
Message: cycles gives pal two figures.
The cycles Clause explains this message.
assemblyParameterWrite
Message: n is a parameter, so assembly can't change it.
Assembly Functions explains this message.
assemblyRegisterParameter
Message: a can't name a parameter of a function written in assembly.
Assembly Functions explains this message.
assemblyResultUnwritten
Message: result is not written on every path through this assembly.
Assembly Functions explains this message.
assemblyImpure
Message: A function's assembly cannot use System.screen.border this way.
Assembly Functions explains this message. A store through a pointer gets the message: A function's assembly cannot store through a pointer: sta (scratch),y.
handImpure
Message: A hand-written block cannot use System.screen.border this way.
Hand-Written Bindings explains this message. A store through a pointer gets the message: A hand-written block cannot store through a pointer: sta (scratch),y.
handWrittenMemory
Message: System.joystick[2].dx.sum(from: 0) keeps memory between frames, so a block cannot stand in for it.
Hand-Written Bindings explains this message.
handWrittenRead
Message: The block reads b, which a never mentions.
Hand-Written Bindings explains this message.
handWrittenTarget
Message: A hand-written block can only write a destination that sits in memory at a fixed address.
Hand-Written Bindings explains this message.
assemblyInterruptWrite
Message: Interrupt code writes hits, which belongs to the program.
What a Routine May Touch explains this message. A store through a pointer gets the message: An interrupt routine cannot store through a pointer: sta ($90),y.
assemblyInterruptScratch
Message: Interrupt code cannot use scratch.
What a Routine May Touch explains this message.
rasterOwner
Message: The routine on line 2 chooses its own raster lines, so it cannot share the raster interrupt with a binding that shows System.screen.border or System.screen.background by raster line.
Routines That Pick Their Own Lines explains this message.
interruptInstall
Message: The asm block on line 4 sets up an interrupt by storing to $0314, so the compiler can't tell when it runs or check that the frame fits.
Installing by Hand explains this message.
interruptRoutineForm
Message: The handler on line 2 holds a line that is not its asm block.
Interrupt Routines explains this message.
interruptLine
Message: System.screen.interrupt takes line: and a whole number from 0 to 261, and found 300.
Interrupt Routines explains this message.
interruptPeriod
Message: The period of System.clock.interrupt can be at most 64 milliseconds, and this one is longer.
Routines on a Timer explains this message.
interruptRuns
Message: runs(...) belongs to the asm block of an interrupt routine.
Routines That Pick Their Own Lines explains this message.
interruptOverrun
Message: The routine on line 109 takes so long on PAL that the interrupt for line 121 comes too late to be set up.
Routines That Pick Their Own Lines explains this message.
interruptTimer
Message: The timer routine on line 6 and the one on line 1 would both need the program's timer.
Routines on a Timer explains this message.
interruptSlow
Message: The routine on line 1 can take 1,519 cycles on PAL, and its timer fires every 986.
Routines on a Timer explains this message.
assemblyZeroPage
Message: An asm block cannot use $10, which is in the zero page the compiler hands out.
Memory a Block May Not Use explains this message.
bitmapAssembly
Message: An asm block in a bitmap program cannot write $DD00.
Memory a Block May Not Use explains this message.
romAssembly
Message: An asm block in a program that changes screen mode can't use $FFD2.
Memory a Block May Not Use explains this message.
listOffBoundary
Message: tiles is placed at 49,408, but a screen the chip shows must start on a 1,024-byte boundary.
Placing a Value in Memory explains this message.
listInCharacterImage
Message: screen is placed at 4,096, where the chip sees the character set and not memory.
Placing a Value in Memory explains this message.
placedOnPort
Message: t is placed on the processor's own port, the first two bytes of memory.
Placing a Value in Memory explains this message.
placedOverProgram
Message: t is placed from 2,049 to 2,112, over the program itself.
Placing a Value in Memory explains this message.
placedOverRuntime
Message: t is placed from 16 to 79, over the zero page the program keeps its values in.
Placing a Value in Memory explains this message.
placedOverPicture
Message: t is placed from 57,600 to 57,663, over the picture the program draws.
Placing a Value in Memory explains this message.
placedOverScreen
Message: tiles is placed from 1,280 to 1,283, over the screen the program shows.
Placed Values explains this message.
placedOverList
Message: b is placed from 49,153 to 49,216, where a already lies.
Placing a Value in Memory explains this message.
ownedScreenShared
Message: Line 3 shows screens as the whole screen, and line 4 also writes the screen's text.
Text and Ink explains this message.
ownedBandRow
Message: Screen row 11 is set by both lines 8 and 11.
Text and Ink explains how each row gets one source.
ownedBandChoice
Message: The screen bands on lines 9 and 10 choose different screens.
Text and Ink explains how the whole picture chooses one screen.
ownedBandScroll
Message: The changing scroll on line 12 crosses the band on line 8 and the region on line 11.
Text and Ink explains why a changing scroll needs a qualified row-boundary change.
ownedScreensBank
Message: The 16 screens in screens (16,384 bytes) and the program's character sets and sprite shapes need 16,384 bytes together, and they don't fit in one stretch of 16,384 bytes beside this program's code and data.
Text and Ink explains this message.
beamLate
Message: Line 6 writes colors the screen is showing, and a row of them can change only after the screen has drawn it. On PAL the wait for row 24 ends about 18,910 cycles into the frame and the work from there takes 4,853 more, so the frame needs 23,463 cycles and holds 19,656.
Text and Ink explains this message.
beamAfterEnd
Message: Line 15 writes colors the screen is showing, but on PAL the frame can come to the wait for row 0 about 19,744 cycles in, and the screen starts its next picture at 19,656.
After the picture ends the wait can no longer tell that the screen has drawn the row, so it would hold the frame for a whole picture more. A frame that must begin after line 256 so its last row can finish (see beamLate) may not come to a write of an upper row after the screen has begun the next picture. Write that row before the rows that come after it on the screen.
Text and Ink explains this message.
paddleDeselected
Message: The interrupt routine on line 6 stores to the port that picks which pair of paddles the chip reads, and this program reads a paddle.
The chip gives a paddle reading only after it has read the same pair for two of its 512-cycle updates, about 1,000 cycles. A store from a routine that can run at any line can change the pair just before the program reads. Keep the top two bits of the stored value at 01 in the routine, or read the keys and sticks somewhere else than a routine.
Assembly explains this message.
deferredLate
Message: The cells kept for the end of the frame are stored about 3,600 cycles into it on early NTSC, and the screen starts drawing at 3,458.
The cells a program writes at places known when it is built, and never reads back, are stored together when the next frame begins, while the border is showing. When there are so many that the screen would start drawing before they are stored, the compiler refuses the program and names the first.
Text and Ink explains this message.
windowChoice
Message: System.screen.columns shows 40 or 38, not 39.
Scroll and Window explains this message.
windowInScene
Message: System.screen.scroll can't be set inside a case of a match yet.
Scroll and Window explains this message.
windowRaster
Message: A changing vertical scroll can't share this raster or case setup yet.
Scroll and Window explains this message.
windowTextModes
Message: System.screen.scroll can't be set while cases show different kinds of text.
Scroll and Window explains this message.
bitmapWindow
Message: System.screen.scroll moves text, and this program shows a canvas (line 1).
Scroll and Window explains this message.
Pictures and Files
These messages appear when pixel art rows or external asset files don't match the shape the compiler expects.
pixelAsset
Message: These Shape pixels can't be used: give it a constant list of pixel rows or bytes.
Pictures and Fonts explains this message.
assetFile
Message: The file ship.bin can't be used: the project holds no asset files; add it under the project root.
Include and Project Files explains this message.
Keys That Need a Long Program
You will only bump into one of these messages if your program grows past a hardware or compiler table limit.
sourceTooLong: a source file longer than 65,535 bytes. Source Text explains the rule.nestingBlock: blocks nested more than 16 deep, such as seventeenifstatements inside one another. Lines explains the rule.drawingCount: a canvas'sdrawingslist with more than 255 places. Drawings explains the rule.outlineSize: outlines in onedrawingslist that hold more than 85 points together. Drawings explains the rule.textOverlapBudget: so many text bindings on one row, such as 120, that the screen cannot redraw the row within a single frame. Text Placement explains the rule.pageTables: regions that hold more than 255 rows of text together, or rows that the program's regions and cases break into more than 255 runs. Regions and Printing explains the rule.ownedBandTables: bands beside so many regions or selectable screens that their row destinations exceed 255. Text and Ink explains the rule.pageMatrix: a printing program that is too large to keep the hidden screen copy that printing draws into. Printing explains the rule.bitmapShapes: a canvas program that uses more than 64 distinct sprite shapes. Sprites explains the rule.drawPiece: a drawing whose largest step takes more cycles than the frame leaves for it. Drawing Over Frames explains the rule.deferredLate: so many cells written at fixed places, and never read back, that they cannot all be stored before the screen starts drawing. Text and Ink explains the rule.assetPlacement: a program whose screen, character sets, and sprite shapes do not fit beside its code and data. Pictures and Fonts explains the rule.
Keys No Program Reaches
These keys exist as internal guardrails in the compiler, though no ordinary program reaches them today.
extraOnLineandexpressionNeeded: the line reader catches an extra word or a missing value withunexpectedfirst.caseStartLate: a case that takes more than a frame to start, whose screen would come back too late in the frame. Because the work before a case'smatchis short today, the screen always comes back in time.internal: an unexpected fault inside the compiler itself rather than in your program. The message asks you to share the program that triggered it.
partialHead requires an area declaration such as partial log = System.screen.region(row: 12). partialCanvas identifies a canvas shape whose complete-row path is still unfinished, such as mixed picture/outline lists, multicolor or case changes. Hires canvases with placed pictures and whole-screen outline-only canvases outside cases and partial text regions are available now.
Grammar Summary
This chapter collects the grammar of every chapter, in order. The compiler checks a program's structure with its grammar table, format 7, revision 59, and the reserved words and symbols below are exactly the ones that table reads. About the Language Reference explains the notation.
Lexical Structure
From Lexical Structure.
line-break-in-brackets ::= '(' newline | '[' newline
| ',' newline
| item newline ( ')' | ']' )name ::= name-head name-character*
name-head ::= 'a' to 'z' | 'A' to 'Z' | '_'
name-character ::= name-head | '0' to '9'keyword ::= 'and' | 'asm' | 'at' | 'break' | 'case' | 'const' | 'else' | 'end' | 'enum'
| 'false' | 'fast' | 'small' | 'partial' | 'fn' | 'for' | 'forever' | 'guard' | 'if' | 'in'
| 'include' | 'match' | 'mut' | 'nil' | 'not' | 'Number' | 'of' | 'on'
| 'op' | 'or' | 'output' | 'start' | 'struct' | 'then' | 'true' | 'type'
| 'var' | 'wait' | 'when' | 'work'
format-name ::= 'u8' | 'i8' | 'u16' | 'i16' | 'u8.8' | 'i8.8' | 'u16.8' | 'i16.8'assembly-word ::= 'writes' | 'runs' | 'cycles' | '#' | ',x' | ',y' | '.lo' | '.hi' | '.byte' | '.text'
instruction-name ::= 'adc' | 'alr' | 'anc' | 'and' | 'arr' | 'asl' | 'asr' | 'axs'
| 'bcc' | 'bcs' | 'beq' | 'bit' | 'bmi' | 'bne' | 'bpl' | 'brk'
| 'bvc' | 'bvs' | 'clc' | 'cld' | 'cli' | 'clv' | 'cmp' | 'cpx'
| 'cpy' | 'dcp' | 'dec' | 'dex' | 'dey' | 'eor' | 'inc' | 'inx'
| 'iny' | 'isb' | 'isc' | 'jmp' | 'jsr' | 'lax' | 'lda' | 'ldx'
| 'ldy' | 'lsr' | 'nop' | 'ora' | 'pha' | 'php' | 'pla' | 'plp'
| 'rla' | 'rol' | 'ror' | 'rra' | 'rti' | 'rts' | 'sax' | 'sbc'
| 'sbx' | 'sec' | 'sed' | 'sei' | 'slo' | 'sre' | 'sta' | 'stx'
| 'sty' | 'tax' | 'tay' | 'tsx' | 'txa' | 'txs' | 'tya'literal ::= whole-literal | decimal-literal | string-literal | 'true' | 'false' | 'nil'whole-literal ::= decimal-digits
| '0x' hex-digit ( '_'? hex-digit )*
| '0b' binary-digit ( '_'? binary-digit )*
decimal-digits ::= decimal-digit+
decimal-digit ::= '0' to '9'
hex-digit ::= decimal-digit | 'a' to 'f' | 'A' to 'F'
binary-digit ::= '0' | '1'decimal-literal ::= decimal-digits '.' decimal-digitsstring-literal ::= '"' string-character* '"'
string-character ::= any printable ASCII character except '"' and '\'
| '\"' | '\\'operator ::= '+' | '-' | '*' | '/' | '%' | '+%' | '-%' | '+|' | '-|'
| '==' | '!=' | '<' | '<=' | '>' | '>=' | '??' | '++' | '...' | '..<'
punctuation ::= '=' | '<-' | ':' | '->' | '?' | '(' | ')' | '[' | ']' | ',' | '.'Types
From Types.
type ::= number-type | name type-range? | list-type | type '?'
| 'Event' ( 'of' type )? | 'Result' 'of' type
number-type ::= 'Number' ( '<' format-name '>' | '<' range '>' )?
range ::= bound '...' bound | bound '..<' bound
type-range ::= '<' bound '...' bound '>'
list-type ::= '[' size 'of' type ']'enum-declaration ::= 'enum' name newline case-line+ 'end'
case-line ::= 'case' enum-case ( ',' enum-case )* newline
enum-case ::= name ( '(' field ( ',' field )* ')' )?
field ::= name ':' typestruct-declaration ::= 'struct' name '(' struct-field ( ',' struct-field )* ')'
| 'struct' name newline struct-member* 'end'
struct-field ::= name ':' type ( '=' expression )?Expressions
From Expressions.
expression ::= unary ( binary-operator unary )*
unary ::= ( '-' | 'not' ) unary | postfix
postfix ::= primary selection*
selection ::= '.' name ( '(' arguments? ')' )?
| '[' expression ( ',' expression )? ']'
primary ::= literal | name | '.' name | name '(' arguments? ')'
| number-type '(' arguments ')' | 'start'
| '(' expression ')' | list-value | struct-value
| if-value | match-value
binary-operator ::= 'when' | 'or' | 'and'
| '==' | '!=' | '<' | '<=' | '>' | '>='
| '??' | '...' | '..<'
| '+' | '-' | '+%' | '-%' | '+|' | '-|' | '++'
| '*' | '/' | '%'list-value ::= '[' ( expression ( ',' expression )* ','? )? ']'
struct-value ::= '(' name ':' expression ( ',' name ':' expression )* ','? ')'if-value ::= 'if' expression 'then' value-body
( 'else' 'if' expression 'then' value-body )* 'else' value-body 'end'
value-body ::= expression | newline ( name '=' expression newline )* expression newline
match-value ::= 'match' expression newline value-arm+ 'end'
| 'match' newline condition-arm+ 'else' '->' expression newline 'end'
value-arm ::= pattern ( 'when' expression )? '->' expression newline
| 'else' '->' expression newline
condition-arm ::= 'when' expression '->' expression newline
pattern ::= name | '-'? whole-literal | 'true' | 'false' | 'nil'
| name '(' field-pattern ( ',' field-pattern )* ')'
field-pattern ::= name ( ':' name )?arguments ::= argument ( ',' argument )* ','?
argument ::= ( name ':' )? expression | name '->' expressionDeclarations
From Declarations.
declaration ::= constant | variable | binding | placed-value | output-declaration
| function | op-declaration | work-declaration
| type-alias | enum-declaration | struct-declaration | include | declaration-match | declaration-ifconstant ::= 'const' name ( ':' type )? '=' expressionvariable ::= 'var' name ( ':' type )? '=' expressionbinding ::= ( 'fast' | 'small' )? name ( ':' type )? '<-' expression
| ( 'fast' | 'small' )? place '<-' expressionoutput-declaration ::= 'output' name '(' name ':' type ')' newline output-line* 'end'function ::= ( 'fast' | 'small' )? 'fn' function-name '(' parameters? ')' ( ':' type )? newline
statement* expression newline 'end'
function-name ::= name | owner '.' name
owner ::= name | 'Number'
parameters ::= parameter ( ',' parameter )*
parameter ::= name ( ':' type )?op-declaration ::= ( 'fast' | 'small' )? 'op' function-name '(' ( op-parameter ( ',' op-parameter )* )? ')'
( ':' type )? newline op-line* expression newline 'end'
op-parameter ::= name ( ':' type )?
op-line ::= name ( ':' type )? '<-' expression newlinework-declaration ::= 'work' name '.' name '(' parameters? ')' ':' type newline
statement* expression newline 'end'type-alias ::= 'type' name '=' typestruct-member ::= name ':' type newline
| 'var' name ( ':' type )? '=' expression newline
| ( 'fast' | 'small' )? 'mut'? 'fn' function-name '(' parameters? ')' ( ':' type )? newline
statement* expression? newline 'end'placed-value ::= name ':' type 'at' whole-literal ( '<-' expression )?include ::= 'include' string-literaldeclaration-match ::= 'match' expression? newline declaration-case+ 'end'
declaration-case ::= ( 'fast' | 'small' )? ( pattern ( 'when' expression )? | 'when' expression | 'else' ) '->' declaration-body
declaration-body ::= declaration-line | newline declaration-line*
declaration-if ::= ( 'fast' | 'small' )? 'if' condition 'then' declaration-body
( 'else' 'if' condition 'then' declaration-body )*
( 'else' declaration-body )? 'end'
declaration-line ::= declaration | handler | declaration-match | declaration-ifEvents and Handlers
From Events and Handlers.
handler ::= ( 'fast' | 'small' )? 'when' ( name '=' )? expression newline statement* 'end'filtered-event ::= expression 'when' expression
joined-event ::= expression 'or' expressionguard-statement ::= 'guard' expression
| 'guard' name '=' expression ( 'and' expression )?wait-statement ::= 'wait' '(' expression ')'
| 'wait' '(' time-unit ':' expression ')'
| 'wait' expression
time-unit ::= 'seconds' | 'ms'forever-statement ::= 'forever' newline statement* 'end'queue-declaration ::= name '=' 'Queue' '(' 'of' ':' type ',' 'capacity' ':' expression ')'Statements
From Statements.
statement ::= assignment | local-variable | if-statement | match-statement
| for-loop | break-statement | call-statementassignment ::= place '=' expression newline
place ::= name selector*
selector ::= '.' name | '[' expression ( ',' expression )? ']'local-variable ::= 'var' name ( ':' type )? '=' expression newlineif-statement ::= 'if' condition 'then' newline statement* else-branch? 'end' newline
| 'if' condition 'then' statement
( 'else' 'if' condition 'then' statement )*
( 'else' statement )? 'end' newline
else-branch ::= 'else' 'if' condition 'then' newline statement* else-branch?
| 'else' newline statement*condition ::= expression | name '=' expression ( 'and' expression )?match-statement ::= 'match' expression newline match-case+ 'end' newline
| 'match' newline condition-case+ 'else' '->' case-body 'end' newline
match-case ::= pattern ( 'when' expression )? '->' case-body
| 'else' '->' case-body
condition-case ::= 'when' expression '->' case-body
case-body ::= statement | newline statement*
pattern ::= name | name '(' field-pattern ( ',' field-pattern )* ')'
| number | 'true' | 'false' | 'nil'
field-pattern ::= name ( ':' name )?for-loop ::= 'for' name 'in' ( range | expression ) newline statement* 'end' newline
break-statement ::= 'break' newlinecall-statement ::= place '(' ( argument ( ',' argument )* )? ')' newline
argument ::= ( name ':' )? expressionThe System Device Tree
From The System Device Tree.
device-path ::= 'System' ( '.' name | '[' expression ']' )*area-declaration ::= area-modifiers name '=' expression
area-modifiers ::= 'partial' ( 'fast' | 'small' )?
| ( 'fast' | 'small' ) 'partial'Assembly
From Assembly.
asm-block ::= 'asm' instruction newline
| 'asm' writes-clause? runs-clause? cycles-clause? newline asm-line* 'end' newlinewrites-clause ::= 'writes' '(' name ( ',' name )* ')'cycles-clause ::= 'cycles' '(' cycle-figure ')'
| 'cycles' '(' standard-figure ( ',' standard-figure )* ')'
cycle-figure ::= decimal-digits ( '...' decimal-digits )?
standard-figure ::= name ':' cycle-figureasm-line ::= ( label | label? instruction | label? data ) newline
label ::= name ':'
instruction ::= instruction-name operand?
operand ::= '#' asm-value
| asm-value ( ',x' | ',y' )?
| '(' asm-value ',x' ')'
| '(' asm-value ')' ',y'
| '(' asm-value ')'
data ::= '.byte' asm-value ( ',' asm-value )*
| '.text' string-literal
asm-value ::= ( '<' | '>' )? '-'? asm-number
| ( '<' | '>' )? asm-name ( '.lo' | '.hi' )? ( '+' asm-number )?
asm-name ::= name ( '[' decimal-digits ']' )? ( '.' name ( '[' decimal-digits ']' )? )*
asm-number ::= decimal-digits | '$' hex-digit+ | '%' binary-digit+assembly-function ::= 'fast'? 'fn' function-name '(' parameters? ')' ':' type newline
asm-block 'end'hand-written ::= ( binding | place '=' expression | placed-value )
'asm' cycles-clause? newline asm-line* 'end' newlineruns-clause ::= 'runs' '(' decimal-digits ')'