Adder Commodore 64

Adder · a reactive language for the Commodore 64 · Language v0.1.0 · Lab & Guide v0.1.3

The Adder Language Reference

Welcome to the Adder Programmer's Handbook. Whenever you want to look up how a language feature works, what types or ranges it accepts, what the Commodore 64 hardware is doing under the hood, or what a compiler message means, this is the place to flip open.

If you are brand new to Adder and want to build your first game from a single line of code, start with the Adder Quickstart and keep this handbook nearby as you go. Every framed program here compiles as it stands with the Adder lab compiler, so you can copy any example straight into the Lab and start poking at it.

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()
end

A 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:

OperatorWhat 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
end

Among 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
end

Lists 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
end

Optionals

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 end

Devices

Every part of the Commodore 64 lives under System—no hex addresses required. See The System Device Tree.

DeviceMembers
System.screenborder, background, text, ink, font, sprite, frame, frames, standard, frame_cycles, canvas(...), region(...)
a regiontext, print, println, newline, home, clear, shown, cursor
a canvasdrawings, ink, paper, shown
System.screen.sprite[i]x, y, shown, shape, color
System.keyboardeach key by name, stroke, line, caret, dropped
System.joystick[1], [2]dx, dy, fire
System.mousex, 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.clockinterval(...), 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 end
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), 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))
end

Interrupt 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
end

Costs

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:

Terminal
bun tools/costs.ts ../game.adder --listing
bun tools/cost-diff.ts ../game.adder --at HEAD

fast 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_blue

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

Grammar
example ::= 'when' event ( 'when' condition )? newline statement* 'end'

Here is how to read the symbols inside a grammar box:

NotationMeaning
'when'the word or symbol itself, written just as shown between the quotes
eventa part that another grammar rule defines
a ba followed by b
a | beither 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
newlinethe 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.

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

Grammar
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, enum or struct—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.

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

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

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

Grammar
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 end

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

Grammar
decimal-literal ::= decimal-digits '.' decimal-digits

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

Grammar
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] <- greeting

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

Grammar
operator    ::= '+' | '-' | '*' | '/' | '%' | '+%' | '-%' | '+|' | '-|'
              | '==' | '!=' | '<' | '<=' | '>' | '>=' | '??' | '++' | '...' | '..<'
punctuation ::= '=' | '<-' | ':' | '->' | '?' | '(' | ')' | '[' | ']' | ',' | '.'
TokenUse
=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:

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

FormatHoldsStepBytes
u80 to 25511
i8-128 to 12711
u160 to 65,53512
i16-32,768 to 32,76712
u8.80 to 255.9961/2562
i8.8-128 to 127.9961/2562
u16.80 to 65,535.9961/2563
i16.8-32,768 to 32,767.9961/2563

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 end

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

TypeNumbersNames
Color0 to 15black, white, red, cyan, purple, green, blue, yellow, orange, brown, light_red, dark_gray, gray, light_green, light_blue, light_gray
Pitch0 to 11c, cs, d, ds, e, f, fs, g, gs, a, as, b
Note0 to 94a pitch and an octave, from c0 through c4 (middle C) to as7
Key0 to 63the keys of the keyboard, listed below
Waveform0 to 3triangle, sawtooth, square, noise
Attack1 to 15no names
Decay0 to 15short (5), medium (8), long (10)
Sustain0 to 15no names
Release0 to 15no names
Flip0 to 1vertical, horizontal
TextOverflow0 to 1clip, wrap
TextWrapnames onlycharacter, word, hyphenated
TextAlignmentnames onlyleading, 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
end

Characters 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] <- shown

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

TypeMade withWhat it holds
GlyphGlyph([...])one character: 8 rows of 8 pixels
CharsetCharset.uppercase, Charset.lowercase, Charset(glyphs, map:), Charset(file:, map:)a font of 256 glyphs
ShapeShape([...]), Shape.squarea sprite's picture: 21 rows of 24 pixels, or of 12 multicolor pixels
PicturePicture([...])art for a canvas: rows of one width
SheetSheet(file:, ink:, transparent:)a PNG image that pictures are cut from
Drawingpicture.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
end

When 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
end

An enum with fields takes one byte for its case tag plus enough bytes to hold the fields of its largest case.

Grammar
enum-declaration ::= 'enum' name newline case-line+ 'end'
case-line        ::= 'case' enum-case ( ',' enum-case )* newline
enum-case        ::= name ( '(' field ( ',' field )* ')' )?
field            ::= name ':' type

Structs

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

Grammar
struct-declaration ::= 'struct' name '(' struct-field ( ',' struct-field )* ')'
                     | 'struct' name newline struct-member* 'end'
struct-field       ::= name ':' type ( '=' expression )?

Built-In Structs

TypeFields
Pointx 0 to 319, y 0 to 199
Spritex -24 to 487, y -50 to 205, shown and behind as bool, shape a Shape, color a Color
Tonea note with its sound, made with Tone(note, decay:, waveform:, envelope:)
Envelopeattack, decay, sustain and release
Keystrokekey 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 end

You have three clean ways to work with an optional value:

  • value ?? fallback unwraps the value when present, or hands back fallback when it is nil;
  • value != nil and value == nil test whether a value is currently there;
  • if name = value then, guard name = value and when name = event unwrap the value into name for 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
end

Expressions

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.

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

OperatorsMeaning
. [...] (...) after a valuemembers, indexes and calls
- before a valuenegation
* / %multiplication, division and remainder
+ - +% -% +| -| ++addition and subtraction, and joining lists
... ..<ranges
??a fallback for nil
== != < <= > >=comparisons
notthe opposite of a yes-or-no value
andboth
oreither
whenan 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) end

Two small combinations ask you to spell out what you mean:

  • Comparisons do not chain together in a row. Writing a < b < c raises chainedComparison; spell out both halves as a < b and b < c.
  • A range (... or ..<) and the ?? fallback operator do not mix naked in the same expression. The compiler flags that with parenthesize and 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:

  1. 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.
  2. Guaranteed to miss: When the compiler can see that the answer can never fit, it catches the mistake right away with literalDoesNotFit for a constant or rangeExceeded for a calculation.
  3. 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.

OperatorResult of 14 op 3 in Number<0...15>Result of 1 op 3 in Number<0...15>
+%14
-%1114
+|154
-|110

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:

FunctionRounds2.5 gives-2.5 gives
floor()down2-3
round()to the nearest, halves up3-2
ceil()up3-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:

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

FunctionResult
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 end

Bit 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 Color values or two Char values;
  • 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 end

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

Grammar
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 of Char.

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.

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

ValuePatterns
an enuma case name without a dot, such as title; a case with fields names them, as in moving(grab, column: chosen)
a named seta value's name without a dot, such as red
a numbera whole literal, such as 0 or -3
a booltrue and false
an optionalnil, and the patterns of its inner type
any valuea 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 when guard already catches a value, a later arm for the same value could never be reached, so the compiler flags it with duplicateArm.
  • Case patterns in a match arm are written cleanly without a leading dot (title ->, not .title ->); writing a dot triggers dottedArm.
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
end

match 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
end

Calls and Members

Grammar
arguments ::= argument ( ',' argument )* ','?
argument  ::= ( name ':' )? expression | name '->' expression

Calls

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:

  • badArgument when a label does not match any parameter name, when the same parameter is supplied twice, or when too many arguments are passed;
  • missingArgument when a required parameter is left out;
  • unknownFunction when 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 end

Declarations 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 end

Asking 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 end

Reductions

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:

ReductionOnResult
xs.sum()numbersthe total of every element
xs.min() and xs.max()numbersthe smallest and the largest element
xs.count()bool valueshow many are true
xs.any() and xs.all()bool valueswhether any, or every one, is true
xs.first_index() and xs.last_index()bool valuesthe 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 out expression for each element n of xs and gives back the list of results.
  • xs.filter(n -> condition) tests each element n; where condition is true it keeps the element, and where condition is false it leaves nil in 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 triggers shadowing).
  • The expression after -> can read surrounding values, but it cannot hold frame-to-frame memory or side effects of its own—such as drawing a random number, testing an event, reading .previous(), or calling an op. The compiler flags any of those—or a filter arrow whose expression is not a bool—with collectionArrow. 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 existing var state; 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.
  • at places a value at a specific spot in memory, and include brings another source file into your project.
Grammar
declaration ::= constant | variable | binding | placed-value | output-declaration
              | function | op-declaration | work-declaration
              | type-alias | enum-declaration | struct-declaration | include | declaration-match | declaration-if

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

Grammar
constant ::= 'const' name ( ':' type )? '=' expression

A 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).

Grammar
variable ::= 'var' name ( ':' type )? '=' expression

Every 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).

Grammar
binding ::= ( 'fast' | 'small' )? name ( ':' type )? '<-' expression
          | ( 'fast' | 'small' )? place '<-' expression
var 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
end

Without 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:

  1. First, the program samples the hardware inputs (keyboard, joysticks, paddles) and works out which events fired this frame.
  2. Next, the when handlers whose events fired run from top to bottom in the order they are written.
  3. 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:

Grammar
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
end

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

Grammar
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 end

Parameters 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 if or a match, 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 struct block) 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)
end

If 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 end

You 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 n in [n of T], stands for the length of the list passed at the call site—and inside the function body, you can read n as 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.

Grammar
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 newline
op 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 the op steps forward. The op works out its initial value on the first frame, and after that it updates only in frames where the on: event fires. On frames where the on: event does not fire, the op freezes its result, its internal bindings, and its previous history in place—costing only the quick check of the event!
  • reset: restarts the op fresh in any frame where the reset: event fires, even if on: would otherwise pause it that frame. The op (along with any nested ops or history-keeping members inside it) recomputes its first-frame value from its current arguments, and start becomes true again for that frame. If both reset: and on: 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.

Grammar
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 " end

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

Grammar
type-alias ::= 'type' name '=' type

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

Grammar
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 as Score(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 raises badArgument).

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:

  1. 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 plain fn promises not to change any of the struct's fields (even indirectly through another call); trying to modify a field inside a plain fn raises structMutationUndeclared.
  2. Mutating member functions (mut fn name(...)): Marked with mut so they can update the struct's fields. A mut fn does not have to return a value (so its : type is optional), and handlers can call it on a var struct, a field of a var, or an element of a var list. Because it modifies the struct in place, calling a mut fn inside a binding or on a constant struct is caught with structChangingPlace. (mut fn is only for struct blocks; writing mut fn at the top level triggers structMutationMethodOnly.)
  3. Maker functions (fn StructName.name(...)): Written with the struct's own type name before the dot, like fn 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 triggers structMakerOwner.

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:

Grammar
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 0 and 1 (the processor's built-in I/O port) is caught with placedOnPort.
  • 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":

Grammar
include ::= 'include' string-literal
include "lib/world"
var lane: Number<0...39> = 0
when System.screen.frame
  lane = wrap(lane)
end

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

Grammar
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-if
enum 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
end

The 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
end

A 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
end

Conditions 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
end

Without 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
end

Declarations 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
end

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

RefusalKey
a match that holds declarations inside the body of an outputoutputMatch
more declarations below a case with a declaration on its -> linestatementAfterArm
a case that cannot begin within a frame's timecaseStartLong, 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.

Grammar
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:

  1. The machine reads its inputs and decides which events happen in this frame.
  2. The handlers whose events happened run one after another, from top to bottom in the order you wrote them.
  3. 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.

EventHappensCarries
startin the first frame of the program, and of each case as it beginsnothing
System.screen.framein every frame, the first one includednothing
System.screen.frame.every(n)in every nth framenothing
key.pressed, key.releasedin the frame a key or button goes down, or comes back upnothing
System.keyboard.strokein the frame a keystroke arrivesa Keystroke
value.changedin a frame where value differs from the frame beforenothing
System.clock.interval(...)again and again, at a fixed periodnothing
System.clock.timer(...)once, after a delaynothing
value when eventwhen event happensvalue

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 end

Used 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
end

A 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 end

A 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] <- shown

You 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
end

When 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
end

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

Grammar
filtered-event ::= expression 'when' expression
joined-event   ::= expression 'or' expression

Joined 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
end

In 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:

  • or joins either two events or two yes-or-no values; mixing one of each raises orNeedsEvents.
  • and works on yes-or-no values, never on events (andEvents); to require two events in the same frame, filter one by the other with when.
  • not flips 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.

MemberGivesIn the first frame
e.every(n)an event that happens on every nth occurrence of edoes not happen
e.after(n)an event that happens n frames after edoes not happen
e.hold(n)a yes-or-no value that stays true for n frames each time e happenstrue if e happens
e.cooldown(n)an event that passes e through and then ignores it for the next n - 1 frameshappens if e happens
b.risingan event that happens when b steps from false to truedoes not happen
b.fallingan event that happens when b steps from true to falsedoes not happen
e.cycle()the next value of the range or enum at each occurrence, wrapping at the endthe first value
e.count()how many times e has happened0
x.sum(from: v)a running total of x, clamped to the rangev, or 0 without from:
e.toggle()a yes-or-no value that flips at each occurrencefalse
e.countdown(from: v)a count that goes down by one at each occurrence and stops at the bottom of the rangev
e.latest()the value that e carried most recently, as an optionalnil
x.sampled(on: e)the value x had at the latest occurrence of e, as an optionalnil
x.history(n)a list of the values of x in the last n frames, newest first, each an optionalall 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.

Grammar
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
end

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

Grammar
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"
end

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

WaitPALNTSCEarly NTSC
wait(seconds: 2)100 frames120 frames122 frames
wait(ms: 500)25 frames30 frames30 frames
wait(seconds: 0.25)13 frames15 frames15 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 end

Waiting 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
end

Remember 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
WaitRefused with
inside forever or forwaitOnTimerLoop
in a handler on another eventwaitOnTimerElsewhere
after a wait that can end after the timer fireswaitOnTimerLate
a second time in the same handlerwaitOnTimerTwice

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.

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

Grammar
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 of n optional items, oldest first, padded with nil in any slots it could not fill;
  • count() gives the number of items currently waiting in line;
  • dropped is an event that happens in the frame after a full queue had to turn away an add.
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
end

Statements

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.

Grammar
statement       ::= assignment | local-variable | if-statement | match-statement
                  | for-loop | break-statement | call-statement

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

Grammar
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]
end

If 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]]
end

If 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 end

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

Grammar
local-variable  ::= 'var' name ( ':' type )? '=' expression newline

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

Grammar
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 <- shade

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

Grammar
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:

FormKey
if n = held or t > 3 thenorAfterAssignment
if t > 3 and n = held thensecondAssignment
if n = held = t thensecondAssignment
if n = held when t > 1 thenwhenInCondition
if n = taps then, where taps is not optionalalwaysSucceeds

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.

Grammar
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
end

A 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, or nil matches 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);
  • else matches 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
end

Every 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 <- shade

Match 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 <- shade

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

Grammar
for-loop        ::= 'for' name 'in' ( range | expression ) newline statement* 'end' newline
break-statement ::= 'break' newline

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

Grammar
call-statement  ::= place '(' ( argument ( ',' argument )* )? ')' newline
argument        ::= ( name ':' )? expression

Here are the built-in statement actions and member calls:

CallWhat 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()
end

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

Grammar
device-path ::= 'System' ( '.' name | '[' expression ']' )*

Almost every member you meet on the device tree works in one of four ways:

KindWhat a program does with itExamples
readingreads a fresh value that the machine samples at the start of each frameSystem.joystick[2].dx, System.paddle[1]
eventresponds with when, or filters and counts it as a streamSystem.screen.frame, System.keyboard.stroke
outputbinds a live value to it with <-System.screen.border, System.screen.sprite[0]
actioncalls it as a step inside a handlervoice.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.

DeviceNumbers
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 end

Naming 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 end

Devices 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:

MemberKindType
borderoutputColor
backgroundoutputColor
textoutput[25 of [40 of ScreenCode]]
inkoutput[25 of [40 of Color]]
fontoutputCharset
scrolloutput(x: 0...7, y: 0...7), described under Scroll and Window
columnsoutput40 or 38
rowsoutput25 or 24
spriteoutput[8 of Sprite], described under Sprites
frameeventEvent
framesreadingNumber<u16>
standardreadingNumber<0...2>
frame_cyclesreadingNumber<16768...19656>
canvas(...)makera canvas, described under Canvases
region(...)makera 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 <- .black

In 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] <- .yellow

ink 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
end

The 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] <- .green

You 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...210 for border, 0...199 for background), it flags rasterSelection;
  • if a border band reaches past row 210 into scanlines that only PAL televisions have, it flags rasterLine;
  • 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))
end

When 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

MemberKindWhat it does
print(text)actionprints the characters at the cursor and leaves the cursor right after them
println(text)actionprints the characters, then advances the cursor to the start of the next line
newline()actionmoves the cursor to the start of the next line
home()actionmoves the cursor back to the region's top-left cell (0, 0)
clear()actionfills the region with spaces and moves the cursor home
wrapvariablea TextWrap: .character (the starting setting), .word or .hyphenated
cursor.row, cursor.colreadingthe cell coordinates where the next printed character will land
cursor.move(row:, col:)actionteleports the cursor to a specific cell inside the region
cursor.inkvariablethe Color used for characters printed from now on
cursor.reversevariablea bool: whether newly printed characters appear in reverse video
textoutputtakes a Text placement positioned in the region's own coordinates
showneventfires when everything printed to the region has been copied onto the visible screen
region(...)makercarves 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])
end

As 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!")
end

Partial 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")
end
Grammar
area-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)),
]
ArgumentTypeMeaning
row, colwhole numbersthe top-left cell, which may move while the program runs
widtha positive constantthe column width of a fixed-size view
heighta positive constantthe row height of a view; requires width
overflowTextOverflow.clip (the default) or .wrap; wrapping requires both width and height
wrapTextWrap.character (the default), .word or .hyphenated
inkColoroptional foreground color for the placement's cells
contenttexta 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] <- .yellow

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

MemberKindType
drawingsoutputa list of Drawing?
inkoutputColor, .white by default
paperoutputColor, .blue by default; on a hires canvas only
showneventEvent
ArgumentMeaning
multicolor: truedouble-wide pixels (160 across by 200 down) with more colors per cell
row, coltop-left character cell of the canvas (0 when left out)
width, heightsize 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 <- .black

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

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

FieldTypeWhen left out
xNumber<-24...487>required
yNumber<-50...205>required
shownbooltrue
behindboolfalse
shapeShapeShape.square, a solid 24-by-21 block
colorColor.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:

MemberKindType
a key name, such as space or areadingbool, true while the key is held down
[k], where k is a Keyreadingbool, for a key chosen dynamically while the program runs
strokeeventEvent of Keystroke
droppedeventEvent
linereading[n of Char], where n is the configured capacity
line.capacityoutputa constant from 0 to 255
line.enteredeventEvent of [n of Char]
caretreadingNumber<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 end

Behind 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] <- shown

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

MemberKindType
dxreadingNumber<-1...1>: -1 left, 0 centered, 1 right
dyreadingNumber<-1...1>: -1 up, 0 centered, 1 down
firereadingbool, 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.

MemberKindType
xreadingNumber<0...319>
yreadingNumber<0...199>
left, rightreadingbool, 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):

ArgumentTypeMeaning
firstNotethe musical pitch, such as .c4 or .f_sharp3
waveformWaveform.triangle (the default), .sawtooth, .square or .noise
decayDecaya plucked or struck note that rings out and fades over this decay
envelopeEnvelopefull 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 end

The 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()
end

As 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:

MemberKindType
interval(seconds:) or interval(ms:)event makerEvent, firing repeatedly once every period
timer(seconds:) or timer(ms:)event makerEvent, firing once after the period elapses from program or case start
hoursreadingNumber<0...23>
minutes, secondsreadingNumber<0...59>
tenthsreadingNumber<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 end

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

DeviceChipRegisters
border, backgroundVIC-II$D020, $D021
color bandsVIC-IIraster interrupts through $D012 and $D019–$D01A
spriteVIC-IIpositions $D000–$D010, shown $D015, multicolor $D01C, colors $D027–$D02E
text, ink, font, canvasesVIC-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], mouseSID$D419, $D41A, with $DC00 choosing port 1
sound[1].voice[n]SIDvoice 1 $D400–$D406, voice 2 $D407–$D40D, voice 3 $D40E–$D414
clock.hours to clock.tenthsCIA 1time of day $DC08–$DC0B
clock.interval, clock.timerCIA 2timer A, $DD04–$DD05
screen.interrupt(line:)VIC-IIraster compare $D012 and $D011 bit 7, flag $D019, enable $D01A, through the IRQ vector at $FFFE
clock.interrupt(ms:)CIA 2timer 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:

  1. It samples the keyboard, joysticks, paddles, mouse, and other hardware readings so they stay steady for the whole frame.
  2. It checks which events occurred in this frame.
  3. It runs the when handlers for those events, in the top-to-bottom order they appear in your source.
  4. 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:

MachineLinesCycles a lineCycles a frame
PAL3126319,656
NTSC2636517,095
early NTSC2626416,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:

MarkMeaning
+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.
kThe 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:

Compiler says
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.
This is the costliest line, with up to 7,186 cycles. The rest of the frame's work adds the remainder.

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:

Compiler says
This loop can't finish within one PAL frame.
The loop needs up to 23,555 cycles: 128 turns of up to 184.
Including other frame work, the bound is 24,729 cycles; the frame budget is 19,656 cycles.

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:

Compiler says
`when start` has a stretch of code, the code after the `wait` on line 9, that can't finish within one PAL frame.
The `if` on line 8 can skip the `wait` on every turn, so all 200 turns of the `for` on line 5 can run in one stretch: up to 64,037 cycles. Including the rest of the frame's work, the bound is 65,182 cycles; the frame budget is 19,656 cycles.
Wait on a turn the loop always reaches: do the work in an inner `for` of at most 48 turns, followed by `wait(1)`, inside an outer `for`.

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:

Compiler says
The compiler can't tell how long the `asm` block on line 3 takes, so it can't check that the frame fits.
The loop at `wait_line` has no count of turns the compiler can see.
State the most cycles the block can take with `cycles(...)` on its first line, such as `asm cycles(pal: 4800...16700, ntsc: 4800...14100)`, and the compiler checks the frame with that figure.

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:

HeadExample
a when handlerfast when System.screen.frame
a functionfast fn square(x: Number<u8>): Number<u16>
a function inside a structfast mut fn award()
an opfast op speed(x)
a binding or an outputfast moved <- position * 2
a casefast 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 codeWithout fastWith fast
* of two changing valuesa compact 24-byte shift-and-add routine taking up to 181 cyclesa quarter-square lookup routine backed by 512-byte tables, taking 74 to 81 cycles
a case's list that starts as one repeated valuefilled by a compact loop when the case startsfilled by an unrolled run of stores (3 bytes and 4 cycles per byte of the list)
a loop that copies a run of bytesa compact indexed loopunrolled to eight bytes per turn, or one straight copy per turn
a call of a function body in assemblyjsr to one shared subroutine bodyinlines a copy of the assembly body right at the call site
a list of sprites made with mapupdated through one shared loop bodyunrolled into direct stores to each sprite's hardware registers
a region's rowsbuilt by one shared routineeach 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 work member 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 " end

The 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 jobPartTooLong and 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 flags jobFrameFull;
  • if a binding asks for a guaranteed completion window with .within(frames: n) and the compiler cannot prove the job finishes within n frames under worst-case frame load, it flags jobDeadline and 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:

Terminal
bun tools/costs.ts ../game.adder
bun tools/costs.ts ../game.adder --listing

Pass --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:

Cost report
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 address

Each 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:

Cost report
lives.adder:3     42     37+  lives = lives - 1
Cost report
lives2.adder:3     14     17+  lives = lives -| 1

See 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:

Terminal
bun tools/cost-diff.ts ../before.adder ../after.adder
bun tools/cost-diff.ts ../game.adder --at HEAD

It 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:

Cost report
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.

Grammar
asm-block      ::= 'asm' instruction newline
                 | 'asm' writes-clause? runs-clause? cycles-clause? newline asm-line* 'end' newline

For 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)
Grammar
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.

Grammar
cycles-clause  ::= 'cycles' '(' cycle-figure ')'
                 | 'cycles' '(' standard-figure ( ',' standard-figure )* ')'
cycle-figure   ::= decimal-digits ( '...' decimal-digits )?
standard-figure ::= name ':' cycle-figure

Write 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
end

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

Grammar
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:

InstructionWhat it does
laxloads both A and X with the same memory byte
saxstores A bitwise-ANDed with X into memory (without changing flags)
dcp, isc, slo, rla, sre, rraread-modify-write a memory byte, then combine it with A
anc, alr, arr, sbximmediate operations on A (or A and X) with a constant byte
nop with an operandfetches 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:

FormMeaning
#valueimmediate: the 8-bit constant value itself
addresszero-page or absolute: the byte stored at address (or label target for branches/jumps)
address,x or address,yindexed: 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),yindirect 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
end

Here 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: name is element 0, name+n is element n (only 1-byte lists take +n, checked by assemblyByteOffset), and lda name,x indexes element X at 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,x and lda pos.hi,x can index up to 256 16-bit elements with an 8-bit X or Y register 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
end

Constants

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
end

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

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

Grammar
hand-written   ::= ( binding | place '=' expression | placed-value )
                   'asm' cycles-clause? newline asm-line* 'end' newline
var 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
end

Interrupt 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
end

Because 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 .byte cells (by name). Writing to an Adder var or binding—or storing indirectly through a pointer—is flagged with assemblyInterruptWrite.
  • 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 touch scratch by name or address (assemblyInterruptScratch).
  • The stack must stay balanced: every pha/php inside the block must have a matching pla/plp on every path, and txs is 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).

Grammar
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
end

Because 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
end

In 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 scroll YSCROLL) 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 $D011 store a run can execute.
  • Writing to a register whose timing impact cannot be bounded statically—such as the processor banking port $01—is flagged with frameUnbounded.

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–$01 is flagged with placedOnPort;
  • 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 with placedOverRuntime;
  • 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:

Cost report
bun run compile --asm border.s -o border.prg

This 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:

Cost report
; 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
        rts

Those 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, or 0bbinary, and % acts as the modulo (remainder) operator in expressions;
  • NAME = expression and .set define symbolic constants using full arithmetic, bit shifts, bitwise operators, comparisons, parentheses, and < / > byte extractors;
  • z: or a: before an operand forces 8-bit zero-page or 16-bit absolute addressing;
  • .byte, .word, .ascii, .space count, fill, .balign, and .section lay 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 with 1f forward and 1b backward) 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 seventeen if statements inside one another. Lines explains the rule.
  • drawingCount: a canvas's drawings list with more than 255 places. Drawings explains the rule.
  • outlineSize: outlines in one drawings list 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.

  • extraOnLine and expressionNeeded: the line reader catches an extra word or a missing value with unexpected first.
  • 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's match is 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.

Grammar
line-break-in-brackets ::= '(' newline | '[' newline
                         | ',' newline
                         | item newline ( ')' | ']' )
Grammar
name           ::= name-head name-character*
name-head      ::= 'a' to 'z' | 'A' to 'Z' | '_'
name-character ::= name-head | '0' to '9'
Grammar
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'
Grammar
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'
Grammar
literal ::= whole-literal | decimal-literal | string-literal | 'true' | 'false' | 'nil'
Grammar
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'
Grammar
decimal-literal ::= decimal-digits '.' decimal-digits
Grammar
string-literal   ::= '"' string-character* '"'
string-character ::= any printable ASCII character except '"' and '\'
                   | '\"' | '\\'
Grammar
operator    ::= '+' | '-' | '*' | '/' | '%' | '+%' | '-%' | '+|' | '-|'
              | '==' | '!=' | '<' | '<=' | '>' | '>=' | '??' | '++' | '...' | '..<'
punctuation ::= '=' | '<-' | ':' | '->' | '?' | '(' | ')' | '[' | ']' | ',' | '.'

Types

From Types.

Grammar
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 ']'
Grammar
enum-declaration ::= 'enum' name newline case-line+ 'end'
case-line        ::= 'case' enum-case ( ',' enum-case )* newline
enum-case        ::= name ( '(' field ( ',' field )* ')' )?
field            ::= name ':' type
Grammar
struct-declaration ::= 'struct' name '(' struct-field ( ',' struct-field )* ')'
                     | 'struct' name newline struct-member* 'end'
struct-field       ::= name ':' type ( '=' expression )?

Expressions

From Expressions.

Grammar
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'
                  | '==' | '!=' | '<' | '<=' | '>' | '>='
                  | '??' | '...' | '..<'
                  | '+' | '-' | '+%' | '-%' | '+|' | '-|' | '++'
                  | '*' | '/' | '%'
Grammar
list-value   ::= '[' ( expression ( ',' expression )* ','? )? ']'
struct-value ::= '(' name ':' expression ( ',' name ':' expression )* ','? ')'
Grammar
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 )?
Grammar
arguments ::= argument ( ',' argument )* ','?
argument  ::= ( name ':' )? expression | name '->' expression

Declarations

From Declarations.

Grammar
declaration ::= constant | variable | binding | placed-value | output-declaration
              | function | op-declaration | work-declaration
              | type-alias | enum-declaration | struct-declaration | include | declaration-match | declaration-if
Grammar
constant ::= 'const' name ( ':' type )? '=' expression
Grammar
variable ::= 'var' name ( ':' type )? '=' expression
Grammar
binding ::= ( 'fast' | 'small' )? name ( ':' type )? '<-' expression
          | ( 'fast' | 'small' )? place '<-' expression
Grammar
output-declaration ::= 'output' name '(' name ':' type ')' newline output-line* 'end'
Grammar
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 )?
Grammar
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 newline
Grammar
work-declaration ::= 'work' name '.' name '(' parameters? ')' ':' type newline
                     statement* expression newline 'end'
Grammar
type-alias ::= 'type' name '=' type
Grammar
struct-member ::= name ':' type newline
                | 'var' name ( ':' type )? '=' expression newline
                | ( 'fast' | 'small' )? 'mut'? 'fn' function-name '(' parameters? ')' ( ':' type )? newline
                  statement* expression? newline 'end'
Grammar
placed-value ::= name ':' type 'at' whole-literal ( '<-' expression )?
Grammar
include ::= 'include' string-literal
Grammar
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-if

Events and Handlers

From Events and Handlers.

Grammar
handler ::= ( 'fast' | 'small' )? 'when' ( name '=' )? expression newline statement* 'end'
Grammar
filtered-event ::= expression 'when' expression
joined-event   ::= expression 'or' expression
Grammar
guard-statement ::= 'guard' expression
                  | 'guard' name '=' expression ( 'and' expression )?
Grammar
wait-statement ::= 'wait' '(' expression ')'
                 | 'wait' '(' time-unit ':' expression ')'
                 | 'wait' expression
time-unit      ::= 'seconds' | 'ms'
Grammar
forever-statement ::= 'forever' newline statement* 'end'
Grammar
queue-declaration ::= name '=' 'Queue' '(' 'of' ':' type ',' 'capacity' ':' expression ')'

Statements

From Statements.

Grammar
statement       ::= assignment | local-variable | if-statement | match-statement
                  | for-loop | break-statement | call-statement
Grammar
assignment      ::= place '=' expression newline
place           ::= name selector*
selector        ::= '.' name | '[' expression ( ',' expression )? ']'
Grammar
local-variable  ::= 'var' name ( ':' type )? '=' expression newline
Grammar
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*
Grammar
condition       ::= expression | name '=' expression ( 'and' expression )?
Grammar
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 )?
Grammar
for-loop        ::= 'for' name 'in' ( range | expression ) newline statement* 'end' newline
break-statement ::= 'break' newline
Grammar
call-statement  ::= place '(' ( argument ( ',' argument )* )? ')' newline
argument        ::= ( name ':' )? expression

The System Device Tree

From The System Device Tree.

Grammar
device-path ::= 'System' ( '.' name | '[' expression ']' )*
Grammar
area-declaration ::= area-modifiers name '=' expression
area-modifiers   ::= 'partial' ( 'fast' | 'small' )?
                  | ( 'fast' | 'small' ) 'partial'

Assembly

From Assembly.

Grammar
asm-block      ::= 'asm' instruction newline
                 | 'asm' writes-clause? runs-clause? cycles-clause? newline asm-line* 'end' newline
Grammar
writes-clause  ::= 'writes' '(' name ( ',' name )* ')'
Grammar
cycles-clause  ::= 'cycles' '(' cycle-figure ')'
                 | 'cycles' '(' standard-figure ( ',' standard-figure )* ')'
cycle-figure   ::= decimal-digits ( '...' decimal-digits )?
standard-figure ::= name ':' cycle-figure
Grammar
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+
Grammar
assembly-function ::= 'fast'? 'fn' function-name '(' parameters? ')' ':' type newline
                      asm-block 'end'
Grammar
hand-written   ::= ( binding | place '=' expression | placed-value )
                   'asm' cycles-clause? newline asm-line* 'end' newline
Grammar
runs-clause     ::= 'runs' '(' decimal-digits ')'