# Record

**Outstanding work is in the issue tracker, not in this file:**
<https://source.starfort.tech/andrew/akbasic/issues>

This file was the implementation plan for the Go → C port of `deps/basicinterpret`, written to be
executed by AI agents rather than read for inspiration. **The port is done**, so what it is now is
the record: the design decisions that are settled, the deviations from the reference and why each
was taken, the defects that were found and fixed, and the reasoning behind the measurements.

Each open item that moved leaves a line saying what it was and which issue carries it, because an
entry explaining *why* a defect matters is worth keeping beside the work it constrains — but the
tracking happens there, not here.

Issues are labelled by kind and blast radius, and milestoned by what they can land in: `0.1.x` for
anything that changes no public contract, `0.2.0` for new verbs and observable behaviour changes,
`1.0.0` for the design decisions. Everything filed carries `status::grooming`.

Two gaps this file recorded and deliberately did not file — the missing `HUD` anchors and a
dismissable dialog — are now `libakgl` #79 and #80. §7's rule is right that *changing* a dependency
is that repository's decision; it does not follow that reporting the gap is.

---

## 0. Agent protocol

### 0.1 The Go reference is deprecated. Stop matching it.

**`deps/basicinterpret` is a dead project. It will not be updated, and this interpreter is no
longer required to reproduce its behaviour.** Recorded here first because it silently reverses
the premise several sections of this file were written on, and because an agent that reads them
without this will park work that is no longer blocked.

What it changes:

- **§6 is now an ordinary defect list.** "Port the behaviour first so the port is provably
  faithful" is retired. Fix them because they are wrong, not when fidelity permits.
- **§1.8's message-text contract is now a convention.** Improving a message is allowed; it costs
  a golden file, which is a cost rather than a veto.
- **§5's bar drops** from "defensible against the golden suite" to defensible on its own merits.
- **`tests/language/` is the editable language corpus rather than a protected specification.** Diverging from
  it is allowed and must be deliberate and recorded — see its README.

What it does **not** change:

- The corpus stays and stays green. Forty-one real BASIC programs with known-good output are
  worth having whatever their provenance, and an unexplained change there is still a red flag.
- The Go source stays readable as documentation. It remains the best answer to "what did the
  original actually do here", which is a question worth being able to answer even once the
  answer stops being binding.
- Nothing about the `ak*` house rules, which never came from the reference.

**Read these before touching anything**, in this order:

1. `MAINTENANCE.md` in this repository — project goals, the `libakerror` convention, the
   error-code range map, and the dependency versions.
2. `deps/libakerror/AGENTS.md` — the `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/`FINISH` protocol.
3. `deps/libakerror/UPGRADING.md` — 1.0.0's status registry. Required before writing an error
   code; the mechanism it replaced is gone.
4. `deps/libakstdlib`'s issue tracker — the defects and gaps in the library this port calls
   into. §1.9 below says which calls are cleared for use; that section is not optional
   reading, it bans a family of functions the port would otherwise reach for by reflex.
5. `deps/libakgl/AGENTS.md` — the no-`malloc` rule and the commit co-author requirement.
6. `deps/basicinterpret/README.md` — the language reference and the unimplemented list. Still
   worth reading for the verb set and the semantics; no longer binding, per §0.1.

**Rules for working this file:**

- **Do not** mark an item done until its acceptance command passes on a clean out-of-tree
  build. "It compiles" is not acceptance.
- **Do** update the tracker in the same commit as the work: close the issue, or replace it with
  the defect it uncovered. Outstanding items live there; this file holds the record.
- **Do** add the agent program name, model name and version as a commit co-author. That rule
  comes from `libakgl` and applies here.
- **Never** hand-edit generated output. `build/` trees and the generated `akerror.h` are
  off-limits; change the generator.
- **Never** reformat a file you are not otherwise changing.
- When a step is blocked because `libakgl` cannot supply a capability, **do not work around
  it here.** File it against `libakgl` in its issue tracker -- what the BASIC verb requires,
  what the `akgl_*` entry point should look like, and what tests would cover it -- and note the
  block in §7 below.

**Style**, restated so nobody has to go look: C99, 4-space indent, tabs at width 8
(`stroustrup`), function-body braces in column 0 on their own line, control braces on the same
line, **always brace**, spaces inside control-flow parens — `if ( x == y ) {`. Pointer star
binds to the identifier: `char *name`. Prefix is `akbasic_` for functions,
`akbasic_TypeName` for types, `AKBASIC_UPPER_SNAKE` for macros. `static` helpers drop the
prefix. Parameter names must match between header and source.

---

## 1. Design decisions already made

These are settled. Do not relitigate them mid-port; if evidence says one is wrong, say so in
a commit that changes it deliberately, and update this section.

### 1.1 Reflection becomes one aligned dispatch table

The Go runtime resolves verbs with `reflect.MethodByName("Command" + NAME)`
(`basicruntime.go:401`), functions with `"Function" + NAME`, and special parse paths with
`"ParseCommand" + NAME` (`basicparser.go:103`). C has no reflection and we are not adding
any. Replace all three with **one** static table in `src/verbs.c`, sorted by name, searched
with `bsearch(3)`:

```c
/* name        token type              parse handler          exec handler        */
{ "AUTO",      AKBASIC_TOK_CMDIMM,     NULL,                  cmd_auto      },
{ "DATA",      AKBASIC_TOK_COMMAND,    parse_data,            cmd_data      },
{ "DEF",       AKBASIC_TOK_COMMAND,    parse_def,             cmd_def       },
```

A `NULL` parse handler means "parse the rval as a plain expression", which is exactly what
`commandByReflection` returning `(nil, nil)` means today. Adding a verb is adding one row plus
two functions. **Keep the table column-aligned and one row per verb** — it is a table, so it
gets laid out as one.

This also kills the Go scanner's three separate maps (`reservedwords`, `commands`,
`functions`, `basicscanner.go:64-67`): the token type lives in the same row.

### 1.2 Strings are fixed-size and live inline

`libakstdlib` has no string type. `libakgl` has `akgl_String` but it is `PATH_MAX` bytes,
refcounted, and pool-allocated — wrong shape for a value that gets copied on every
assignment, and it would drag a `libakgl` dependency into the core interpreter.

Define in `include/akbasic/types.h`:

```c
#define AKBASIC_MAX_STRING_LENGTH    256    /* matches AKBASIC_MAX_LINE_LENGTH */
```

and give `akbasic_Value` a `char stringval[AKBASIC_MAX_STRING_LENGTH]` **inline**. `clone()`
becomes a struct assignment. No allocator, no refcount, no lifetime question.

**Tradeoff, stated:** every `akbasic_Value` is ~300 bytes, so one environment's
`values[AKBASIC_MAX_VALUES]` pool is ~19KB, and 32 environments is ~610KB of BSS. That is
fine on a PC and is the price of never calling `malloc`. If it ever isn't fine, the knob is
`AKBASIC_MAX_STRING_LENGTH`, not the allocator.

Truncation is an **error**, not a silent clamp: `FAIL_RETURN(e, AKBASIC_ERR_VALUE, ...)`.

### 1.3 Maps become fixed-capacity open-addressed tables

Five Go maps need replacing:

| Go site | Purpose | C replacement |
|---|---|---|
| `BasicScanner.reservedwords/commands/functions` | keyword → token type | the §1.1 static table + `bsearch` |
| `BasicEnvironment.variables` | name → `*BasicVariable` | `akbasic_SymbolTable`, capacity `AKBASIC_MAX_VARIABLES` |
| `BasicEnvironment.functions` | name → `*BasicFunctionDef` | `akbasic_SymbolTable`, capacity `AKBASIC_MAX_FUNCTIONS` |
| `BasicEnvironment.labels` | name → line number | `akbasic_SymbolTable`, capacity `AKBASIC_MAX_LABELS` |

One implementation, `src/symtab.c`, keyed by `aksl_strhash_djb2()` (already in
`libakstdlib`) with linear probing and a fixed slot array. **Use the existing hash; do not
write another one.** Table full is an error, not a resize.

Caveat, recorded upstream: the wrapper sign-extends `char`, so a
high-bit byte hashes wrong — `"\xff\xfe"` returns 5859874 where the `unsigned char` answer is
5868578. BASIC identifiers are 7-bit ASCII (the scanner only accepts `IsLetter`/`IsDigit` plus
a type suffix), so this cannot bite the symbol tables. It **would** bite if anyone later keys
a table on a string literal or a filename. Do not work around it here; it is already filed
upstream.

### 1.4 Environments come from a pool and are released

Go calls `new(BasicEnvironment)` at `basicruntime.go:121` and `basicparser_commands.go:124`
and never frees one. A long-running `GOSUB` or `FOR` in Go leaks; the GC eventually catches
some of it, and nothing in the tests notices.

C gets `HEAP_ENVIRONMENT[AKBASIC_MAX_ENVIRONMENTS]` with
`akbasic_env_acquire()` / `akbasic_env_release()`, in the shape of `akgl_heap_next_*`.
`akbasic_runtime_prev_environment()` **must** release the environment it pops. Pool
exhaustion is `AKBASIC_ERR_ENVIRONMENT`, reported with the current line number.

Watch the one place this is not a clean stack: `userFunction` (`basicruntime.go:348`) stores
a `BasicEnvironment` **by value** inside `BasicFunctionDef` and re-`init()`s it on every call.
In C the funcdef holds an `akbasic_Environment *` acquired at `DEF` time and reset per call —
it is owned by the funcdef, not the pool's free list, until the funcdef dies.

### 1.5 Output goes through a text sink backend

`Write()` and `Println()` (`basicruntime_graphics.go:140,148`) mirror every line to stdout
*and* to an SDL surface. That mirror is the only reason the golden-file suite works. Do not
reproduce it as a hardcoded pair of calls.

Define a record of function pointers, populated by an initializer — the house pattern:

```c
typedef struct akbasic_TextSink
{
    void *self;
    akerr_ErrorContext AKERR_NOIGNORE *(*write)(struct akbasic_TextSink *self, char *text);
    akerr_ErrorContext AKERR_NOIGNORE *(*writeln)(struct akbasic_TextSink *self, char *text);
    akerr_ErrorContext AKERR_NOIGNORE *(*readline)(struct akbasic_TextSink *self, char *dest, size_t len);
    akerr_ErrorContext AKERR_NOIGNORE *(*clear)(struct akbasic_TextSink *self);
} akbasic_TextSink;
```

`akbasic_sink_init_stdio()` is in the library and `akbasic_sink_init_akgl()` is in the
akgl-backed module. The driver is supposed to pick: a default build selects stdio, while an
`AKBASIC_WITH_AKGL` build selects the SDL text path and mirrors its output to stdout. It does
not do that yet; §3 records the missing standalone frontend. Cursor arithmetic, wrapping and
scrolling belong to the akgl sink, not to the interpreter.

### 1.6 The interpreter steps; it does not run

Go's `run()` (`basicruntime.go:682`) is a `for {}` that owns the process until `MODE_QUIT`.
Goal 3 forbids that: a host game must be able to bound execution.

The library exposes:

```c
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_step(akbasic_Runtime *obj);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_run(akbasic_Runtime *obj, int maxsteps);
```

`_step()` does exactly what one iteration of Go's `for {}` body does and returns.
`_run(obj, maxsteps)` loops `_step()` until the mode is `AKBASIC_MODE_QUIT` or `maxsteps`
steps have elapsed; `maxsteps <= 0` means unbounded, which is what the standalone driver
passes. **This is a deliberate restructure, and it must not change a single byte of golden
output.**

`QUIT` sets `AKBASIC_MODE_QUIT` and returns. **Nothing in the library calls `exit()`,
`abort()`, or `FINISH_NORETURN`.** `FINISH_NORETURN` appears exactly once in the tree, in
`src/main.c`.

### 1.7 Error codes are absolute, reserved from the 1.0.0 registry

`deps/libakerror` is at **1.0.0**. Read `deps/libakerror/UPGRADING.md` before writing an error
code; `MAINTENANCE.md`'s error-code section is the condensed version. Three things this changes
from what you may have seen in an older draft or in `libakgl`:

- `AKERR_MAX_ERR_VALUE` **no longer exists**. The name registry is sparse and takes any `int`.
  There is nothing for a consumer to size, and no compile definition to set. Anywhere you find
  one, delete it.
- `__AKERR_ERROR_NAMES` **no longer exists**. The table is private.
- Codes are **absolute integer constants at 256 or above**, never `AKERR_LAST_ERRNO_VALUE + N`.
  The offset scheme is what produced the live `libakgl` collision documented in
  `MAINTENANCE.md`.

`akbasic` owns **512–767** per the range map in `MAINTENANCE.md`. Declare it as an `enum`, so the
values stay compile-time integer constants (`HANDLE` expands to `case` labels, which require
that) and adding a code does not mean renumbering an offset:

```c
#define AKBASIC_OWNER "akbasic"

enum {
    AKBASIC_ERR_BASE        = 512,   /** Start of akbasic's reserved status range */
    AKBASIC_ERR_SYNTAX      = AKBASIC_ERR_BASE,
                                     /** Parse-time grammar violation */
    AKBASIC_ERR_TYPE,                /** Incompatible types in an operation */
    AKBASIC_ERR_UNDEFINED,           /** Reference to an undefined verb, function or label */
    AKBASIC_ERR_BOUNDS,              /** Array subscript or pool index out of range */
    AKBASIC_ERR_ENVIRONMENT,         /** Environment pool exhausted or orphaned environment */
    AKBASIC_ERR_VALUE,               /** A value was malformed, truncated or unconvertible */
    AKBASIC_ERR_STATE,               /** A verb ran outside the block structure it requires */
    AKBASIC_ERR_LIMIT       = AKBASIC_ERR_BASE + 256
};
```

`akbasic_init()` reserves the **whole** 256 in one call and then names each code. Both
registry calls return `akerr_ErrorContext *` and are `AKERR_NOIGNORE`, so a collision is an
ordinary error — `PASS` it and let it propagate out of init:

```c
akerr_ErrorContext AKERR_NOIGNORE *akbasic_init(void)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, akerr_reserve_status_range(AKBASIC_ERR_BASE,
					    AKBASIC_ERR_LIMIT - AKBASIC_ERR_BASE,
					    AKBASIC_OWNER));
    PASS(errctx, akerr_register_status_name(AKBASIC_OWNER, AKBASIC_ERR_SYNTAX,
					    "Syntax Error"));
    /* ... one per code ... */
    SUCCEED_RETURN(errctx);
}
```

Reserve the whole range in **one** call — a subset or superset of your own range raises
`AKERR_STATUS_RANGE_OVERLAP`, not a no-op. Use `akerr_register_status_name()`, never the
two-argument `akerr_name_for_status(status, name)` set path: the owned form is the one that
catches a component writing into a range that is not its own, and the two-argument form is
what let `libakgl` silently clobber `libakerror`'s names.

There is no startup ceiling to assert any more — the BSS-overflow hazard the old draft
defended against was deleted along with `AKERR_MAX_ERR_VALUE`. What replaces it is the
reservation itself: if `akbasic_init()` returns an error, something else owns part of 512–767
and the process must not continue as though it does not.

Do not call `akerr_init()` first. Every registry entry point calls it, and since 1.0.0 it no
longer clears reservations made before it ran.

### 1.8 Error message text is a convention, no longer a contract

`tests/language/array_outofbounds.txt` is, verbatim:

```
? 20 : RUNTIME ERROR Variable index access out of bounds at dimension 0: 4 (max 2)\n\n
```

The trailing double newline is real: `basicError` builds a string ending in `\n` and hands it
to `Println`, which adds another.

**This used to be a hard contract and is now a default.** The Go implementation is deprecated
and will not be updated, so the two projects are no longer required to match — see §0.1. What
survives is the practical half: these strings and this newline behaviour are what every
expectation in `tests/language/` was written against, so changing one means changing the paired
files, and that is worth doing on purpose rather than by accident. A message that reads
awkwardly *may* now be improved; do it deliberately, move the expectations in the same commit,
and add a line to §5.

Numeric formatting still matches the reference: integers via `%" PRId64 "`, floats via `%f`
(Go's `%f` and C's `%f` both give six decimals — `tests/language/arithmetic/float.txt`
confirms). No reason to change it, which is different from not being allowed to.

### 1.9 Which `libakstdlib` calls are cleared for use — **the bans are lifted**

`deps/libakstdlib` is at **0.2.0**, and that release *fixed all six* of the confirmed defects
this section was built around, plus seventeen contract gaps. The table of bans that used to
live here is gone because it is no longer true, and leaving it would send an agent around a
workaround for a function that now works.

What changed, in the four that mattered to this port:

| Was banned | Now |
|---|---|
| `aksl_atoi` / `_atol` / `_atoll` / `_atof` | Every one takes a `dest` pointer and raises `AKERR_VALUE` on no digits or trailing junk, `ERANGE` on overflow — exactly the contract this section demanded |
| `aksl_list_append` / `_iterate` | Append no longer truncates to two nodes; iterate no longer skips the first half |
| `aksl_tree_iterate` | `AKERR_ITERATOR_BREAK` stops the traversal |
| `aksl_realpath` | Rewritten; no longer reads uninitialised memory on its error path |

Two signature changes reach this repository. `aksl_fread` and `aksl_fwrite` now take a
required `size_t *nmemb_out` and report a short transfer as `AKERR_IO` instead of a silent
success — so a `DSAVE` onto a full disk is now an error a program sees, where before it
reported nothing. `src/runtime_commands.c` passes the count and discards it, which is correct:
the library does the noticing now.

**`src/convert.c` is gone.** It existed only because `aksl_ato*` could not report a conversion
failure, and its own note said what to do when that changed: *"When it grows it, delete
`src/convert.c` and switch the call sites over."* That is done. Six call sites now go straight
to the library — `newLiteralInt` and `newLiteralFloat` in `src/grammar.c`, the line number in
`src/scanner.c`, `FunctionVAL` in `src/runtime_functions.c`, `INPUT` in
`src/runtime_commands.c`, and `GSHAPE`'s handle in `src/runtime_graphics.c`.

Three things that fell out of it, recorded because each was checked rather than assumed:

- **`aksl_strtoll(str, NULL, base, &dest)` is the exact replacement**, not `aksl_atoll`, wherever
  a base is involved. The `ato*` forms are base 10; `src/grammar.c` picks base 8 or 16 from the
  lexeme's prefix. A `NULL` endptr is what makes trailing junk an error rather than a stopping
  point, and that is the whole contract this port needs.
- **The raised status changed** from `AKBASIC_ERR_VALUE` to `AKERR_VALUE`, and the message text
  with it — `VAL("garbage")` now says `no digits in "garbage"`. §1.8 makes message text part of
  the acceptance contract, so this was checked against the corpus first: **no golden file
  contained a conversion message**, which is also why §1.9 used to ask for one. Two now exist,
  `tests/language/numeric/val_refuses_garbage` and `octal_literal`, so the next change to that
  text has to move a golden file.
- **`tests/convert.c` became `tests/numeric_contract.c`.** The assertions did not become
  worthless when the wrapper went away — they became assertions about a contract this port
  depends on and no longer owns, which is worth pinning exactly because a regression in it would
  make `VAL("garbage")` return 0 silently. Same reasoning `libakstdlib`'s own
  `tests/test_status_registry.c` gives for pinning that it reserves no status range.

One caveat survives the upgrade unchanged: `aksl_strhash_djb2` still sign-extends `char`, so a
high-bit byte hashes differently from the `unsigned char` answer. BASIC identifiers are 7-bit
ASCII so the symbol tables cannot reach it — see §1.3, which is still accurate.

### 1.10 Generators share the `DEF` namespace, and the rest of their v1 semantics

Decided with issue #57 and its review. `GEN` and `DEF` live in the same functions table —
one lookup, one "unknown function" error path, a name cannot be both. What follows from
that and from the review of PR #61, all settled:

- A `GEN` called like a function (`X# = COUNTUP(3)`) is refused at the call, before
  anything is pushed — `akbasic_FunctionDef.isGenerator` exists for exactly this check.
- Bare `RETURN` standing in a `GEN`'s own frame ends the generator exactly as `END GEN`
  does; `RETURN expr` there is an error, because values leave a `GEN` only through `EMIT`.
  Both inherit the interpreter-wide restriction that `RETURN` does not unwind nested
  `FOR`/`DO` scopes — lifting that everywhere (the C64 reference *does* unwind) is filed
  separately.
- `DO EACH ... LOOP WHILE c | UNTIL c` composes: the condition is checked after each trip,
  with the loop variable still holding that trip's value, and stopping abandons the
  generator exactly as `EXIT` does. A condition on the `DO EACH` line itself is a parse
  error.
- Self-recursion — a `GEN` reached again down its own parent chain — is refused; sibling
  and nested invocations of the same `GEN` are each a fresh pool environment and are fine.
- Every path that discards a `FOR EACH`/`DO EACH` scope must release the generator
  suspended off it; see MAINTENANCE.md's "Abandoned generators" note for the invariant
  and `akbasic_runtime_unwind_to_environment()` for the one primitive that enforces it.

One known limitation, pre-existing and shared with `DEF`: `parse_def_parameters()` does
not accept an empty parameter list, so `GEN NAME()` cannot be written — every generator
takes at least one parameter whether it wants one or not. Location:
`src/parser_commands.c`, `parse_def_parameters()`. Consequence: pointless parameters in
programs. Blast radius: cosmetic, both `DEF` and `GEN` headers. Closure: teach the shared
helper to accept `()`, one test each for `DEF` and `GEN`; filed as its own issue.

---

## 2. What exists — **the core port is complete and green**

Phases 0 through 6 of the original plan are done. The interpreter builds clean under
`-Wall -Wextra`, passes the reference's entire corpus, and passes under ASan and UBSan.

It *did* reproduce the reference byte for byte, and that claim is retired rather than broken:
§0.1 released it, and one case has since diverged deliberately (§6 item 16, listed in
`tests/language/README.md`). Everything else still matches, which is worth knowing but is no
longer a gate.

```sh
cmake -S . -B build && cmake --build build --parallel
ctest --test-dir build --output-on-failure          # 59/59
cmake -S . -B build-asan -DAKBASIC_SANITIZE=ON      # 59/59
cmake -S . -B build-cov  -DAKBASIC_COVERAGE=ON      # 92.3% line, 96.9% function
```

| Module | Source | Reference |
|---|---|---|
| Symbol table | `src/symtab.c` | the five Go maps |
| Value | `src/value.c` | `basicvalue.go` |
| Variable | `src/variable.c` | `basicvariable.go` |
| Tokens and AST leaves | `src/grammar.c` | `basicgrammar.go` + `basicparser.go` |
| Dispatch table | `src/verbs.c` | the three reflection lookups |
| Scanner | `src/scanner.c` | `basicscanner.go` |
| Parser | `src/parser.c`, `src/parser_commands.c` | `basicparser*.go` |
| Environment | `src/environment.c` | `basicenvironment.go` |
| Runtime | `src/runtime.c` | `basicruntime.go` minus SDL |
| Verbs and functions | `src/runtime_commands.c`, `src/runtime_functions.c` | `basicruntime_{commands,functions}.go` |
| Text sink | `src/sink_stdio.c` | `basicruntime_graphics.go`'s stdout mirror |
| Stdio driver | `src/main.c` | `main.go` minus its SDL frontend |
| Embedding examples | `examples/embed.c`, `examples/hostvars.c` | *(new)* |

`akbasic_runtime_load(rt, source)` was added while writing the README's embedding
section: a host usually holds its script as a string and wants the sink reserved for output,
and the only path that existed — `AKBASIC_MODE_RUNSTREAM` reading through the sink's
`readline` — forces a game to point its output device at its source text. `examples/embed.c`
is the code the README quotes, built by every build and registered as a CTest case so a
signature change breaks the build rather than rotting the document.

**The acceptance suite is the editable language corpus, checked in at `tests/language/`.** All
65 `.bas` files are registered as individual CTest cases and compared against their `.txt`
— including the trailing double newline on an error line (§1.8).

It was driven *in place* out of `deps/basicinterpret` until 2026-07-31, on the reasoning that
copying a submodule's corpus guarantees drift. That reasoning was sound and was overruled
deliberately: the Go dependency is being deprecated, and a build that cannot run its own
acceptance suite without cloning the implementation it replaced is not finished. The copy is
originally byte-identical to `basicinterpreter@d76162c`, and `tests/language/README.md` records the
provenance and the rule that programs and expectations are edited together deliberately.

**Nothing in the build or the suite needs `deps/basicinterpret` any more**, and that is checked
rather than assumed — both configurations were configured, built and run from scratch with the
submodule moved out of the tree. The Commodore font moved too, to `assets/fonts/`, which was the
other thing tying the build to it; `assets/fonts/PROVENANCE.md` carries the licence question that
came with it.

Eighteen unit tests cover the modules the corpus cannot reach on its own.
`tests/known_reference_defects.c` is registered in `AKBASIC_KNOWN_FAILING_TESTS` and asserts
the **correct** contract for six of the defects in §6; when one is fixed CTest reports
"unexpectedly passed", which is the cue to split that assertion out and strike the item.

Two budget numbers moved during implementation and are worth knowing:
`AKBASIC_MAX_ARRAY_ELEMENTS` (1024) caps one array and `AKBASIC_MAX_ARRAY_VALUES` (4096) caps
all of them together, drawn from an `akbasic_ValuePool` the runtime owns. The reference had no
such ceiling because it called `make()`.

---


## 3. The akgl-backed sink, devices and standalone frontend — **done**

`-DAKBASIC_WITH_AKGL=ON` builds and its suite passes, and the build option now changes what
the executable *does*: an AKGL build of `basic` opens a window, draws BASIC output into it,
pumps SDL events, lets you type at it, and still puts every byte on stdout. Four adaptors in
the `akbasic_akgl` target, which is the only thing here that links SDL, are complete and
tested:

| File | Backs |
|---|---|
| `src/sink_akgl.c` | the §1.5 text sink, over `akgl_text_measure` and `akgl_text_rendertextat` |
| `src/graphics_akgl.c` | `akbasic_GraphicsBackend`, over `akgl_draw_*`, and the SSHAPE surface pool |
| `src/audio_akgl.c` | `akbasic_AudioBackend`, over `akgl_audio_*` |
| `src/input_akgl.c` | `akbasic_InputBackend`, over `akgl_controller_poll_key` |

The character grid comes from `akgl_text_measure(font, "A", &w, &h)` — the direct equivalent of
the `font.SizeUTF8("A")` at `basicruntime.go:96`, and the call this was blocked on until
42b60f7. Wrapping is done on the character grid rather than by handing SDL_ttf a `wraplength`,
because the cursor has to land somewhere definite: a program that `PRINT`s a long string and
then `PRINT`s again expects the second to start on the row after the first ended, and only the
code that placed the characters knows which row that is.

**The interpreter owns no window, renderer or event loop.** Every initializer takes something
the host already made. Each calls `akgl_error_init()` first: `akgl_game_init()` would have,
but we drive subsystems directly and never call it, and a code raised before that registration
carries no name into its stack trace. It is idempotent.

*Acceptance:* `tests/akgl_backends.c`, a 128x128 software renderer under the dummy video
driver read back with `SDL_RenderReadPixels` — the pattern `deps/libakgl/tests/draw.c`
established, which needs no display and no offscreen harness. Registered as the `akgl_backends`
CTest case, and only when `AKBASIC_WITH_AKGL` is on.

### The standalone frontend

The standalone executable is itself a host, and that half of the Go frontend is now ported —
`src/frontend_akgl.c`, in its own `akbasic_frontend` target. **The separation is in the build
graph and not only in a comment**: `akbasic` is the interpreter, `akbasic_akgl` is the four
adaptors that draw through somebody else's renderer, and `akbasic_frontend` is the one thing in
the repository that creates a window. A game embedding the interpreter links the first two and
not the third.

| Piece | Where |
|---|---|
| Window, renderer, font, the four adaptors | `akbasic_frontend_akgl_init()` |
| Sink and devices onto a runtime | `akbasic_frontend_akgl_attach()` |
| One frame: pump events, draw the text layer, present | `akbasic_frontend_akgl_pump()` |
| The bounded frame loop | `akbasic_frontend_akgl_drive()` |
| stdout mirror | `src/sink_tee.c`, in the *core* library |

Four things are worth knowing about how it came out.

1. **The stdout mirror is a sink, not a second write.** §1.5 made the sink boundary precisely
   so the interpreter would never carry the reference's hardcoded pair of calls, and §3 item 5
   said it again. `akbasic_sink_init_tee()` composes two sinks into one and takes `readline`
   from whichever of the two was named as the reader — which is the whole difference between
   the two modes: a program read from a file comes in through the stdio half, and typed lines
   come in through the akgl half's line editor. It lives in the core library and is tested
   there (`tests/sink_tee.c`), because composing two function-pointer records needs no SDL.

2. **The line editor borrows the host's loop a frame at a time.** `readline` has to wait for a
   typed line, and §1.6 forbids the library blocking — and a sink that sat on the keyboard with
   no way to pump events would deadlock the process on its first `INPUT`. So the host installs
   an `akbasic_AkglPump` with `akbasic_sink_akgl_set_pump()`, and the editor calls it between
   keystrokes. The loop is still the host's. `akbasic_frontend_akgl_pump()` has that exact
   signature and is installed as-is. Without a pump the sink reports EOF, which is what it did
   before and what a host that wants no editor still gets.

3. **The frame loop is bounded, and that is what makes the close button work.** 256 steps per
   frame (`AKBASIC_FRONTEND_STEPS_PER_FRAME`), then pump and present. `10 GOTO 10` under an
   unbounded `run()` would own the process forever; `tests/akgl_frontend.c` runs exactly that
   and asserts the window close ends it.

4. **The frame is not cleared.** The graphics verbs draw straight to the renderer rather than
   into a display list, so clearing would wipe every `DRAW` at the end of the frame it was
   issued in. The text layer is drawn over whatever is there, which is also how a C128 stacks
   its text plane on its bitmap plane.

*Acceptance:* two things, and the second one is the stronger.

- `tests/akgl_frontend.c`, registered as the `akgl_frontend` CTest case: a program run through
  the frontend under the dummy driver, asserting the mirrored stdout **byte for byte** and
  reading the renderer back to prove the same characters were drawn in the Commodore font;
  synthetic key events pushed into SDL's own queue and read back through the frontend's input
  backend, so every link the host owns is in the path; the line editor's fold to upper case,
  its backspace and its escape; a whole REPL session typed at the window and ended with a typed
  `QUIT`; and the window-close path.
- **The entire golden corpus is driven through the AKGL binary.** A `-DAKBASIC_WITH_AKGL=ON`
  build registers the same 41 upstream cases against the same executable, which now opens a
  window to run them, and all 41 still match byte for byte. That is a much broader statement
  than any hand-written frontend test: the SDL path changes no observable output anywhere in
  the corpus.

Three `no_device.bas` local cases are **deliberately not registered** in an AKGL build. Their
premise is a driver that was given no devices, which is true of the stdio driver and
deliberately false of this one. The refusal path they cover is asserted directly against the
backend records in `tests/devices.c`, which both configurations build.

**Manual check against a real display, 2026-07-31.** The dummy driver cannot prove presentation,
so `build-akgl/basic` was run against X display `:0` on a program that prints a line, draws a
`BOX` and then loops. `xwininfo` found a real 800x600 window titled `BASIC`; the window was
captured with `import` and measured: the first text row carries glyph pixels (mean 65/255), the
box's left edge at x=100 is a solid white line (mean 255), the box interior is black — it
outlines rather than fills — and an empty corner is black. stdout carried `HELLO WORLD` at the
same time. `QUIT` exits cleanly with status 0.

**The typing half is no longer manual, and it is no longer a gap.** It used to read "somebody
should type at it once", because no key-synthesis tool was installed. `xdotool` now is, and
`tests/akgl_typing.sh` is registered as the `akgl_typing` CTest case: it starts the driver under
a pty, waits for the window with `xdotool search --sync`, gives it keyboard focus, types a
program containing a **string literal** and a **lower-case** one, and polls the mirrored stdout
for what they print.

That case earns its keep twice over. It is the only test that covers the path *upstream* of SDL
— X11 → SDL composition → libakgl's ring → the editor — and everything else synthesises SDL
events, which is precisely how the missing `SDL_StartTextInput()` shipped with a green suite.
Confirmed by reverting both halves of that fix and watching it fail with the same symptom that
was reported: `READY`, and nothing after it.

It needs a real X server, a window manager and `xdotool`, and it steals keyboard focus for about
fifteen seconds. Absent any of those it reports CTest `Skipped` rather than failing, which is
what it does in CI. `AKBASIC_SKIP_INTERACTIVE=1` skips it deliberately.

### The five things that had to be worked around — **all gone**

Every one was filed against `libakgl` and every one is resolved in its 0.3.0. What is left here
is the note that they existed, because the pattern is the point: file it upstream, comment the
workaround at its site with the words "filed upstream", delete it when it lands.

| Was | Resolved by |
|---|---|
| An embedded `libakgl` required its vendored SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to be *installed*, so our `CMakeLists.txt` declared all five by hand first | It adds them whether or not it is top-level |
| `akgl/controller.h` did not compile on its own — it used `akgl_Actor *` and included nothing that declared it | It includes `<akgl/actor.h>` |
| There was no way to bind a 2D backend to a renderer you already had, so the six vtable pointers were assigned by hand in two files | `akgl_render_bind2d()` |
| `akgl_text_rendertextat()` dereferenced an unset `draw_texture`, so the first `PRINT` through an unbound backend segfaulted | It checks, and reports `AKERR_NULLPOINTER` like the draw entry points |
| `SOUND`'s frequency sweep had no equivalent and was refused | `akgl_audio_sweep()` |

**0.3.0 also arrived with a regression that this repository is the one that found**, because it
is the only consumer that embeds `libakgl` alongside other projects: the commit that made the
vendored dependencies unconditional also made its `add_test()` shadow unconditional, and CMake
chains command overrides exactly one level deep. Two projects in one tree cannot both shadow
`add_test` — the second rebinds `_add_test` to the first and the builtin becomes unreachable to
everybody, so our own registrations recursed to CMake's depth limit. Fixed upstream by putting
the top-level guard back; filed as `libakgl` defect 27, with the two-line CMake program that
demonstrates the one-level limit.

### Still missing from the sink

Nothing that blocks a verb, and as of `libakgl` 0.3.0 nothing that blocks a *character* either.
The ring now carries the composed UTF-8 text SDL worked out from each keystroke, so the editor
takes that in preference to the keycode and shifted characters, keyboard layouts, compose keys
and dead keys all work. **A double quote can be typed, so a BASIC string literal can be typed**,
which was the sharp end of the old limitation — it used to mean a program with a string in it
had to be passed on the command line. Letters are no longer folded to upper case, because there
is no longer any reason to.

**The host has to turn text input on**, and forgetting to is how this broke once. SDL3 has it
off by default and per-window, so it is `akbasic_frontend_akgl_init()` that calls
`SDL_StartTextInput()` — `akgl/controller.h` says as much. Without it SDL emits no
`SDL_EVENT_TEXT_INPUT` at all, every keystroke reaches the ring with an empty `text`, and an
editor that reads that as "not a character" is silently dead: nothing echoes in the window, and
nothing reaches stdout either, because `RUN` can never be typed.

The editor therefore **falls back to the keycode** when a keystroke carries no composed text,
folded to upper case. A worse keyboard is a great deal better than no keyboard, and it means a
host that embeds these adaptors and forgets `SDL_StartTextInput()` gets a usable editor rather
than a dead one. Both halves are asserted in `tests/akgl_frontend.c`, and both were checked by
reverting each in turn and watching the test fail.

Worth knowing about the test suite that missed this: every other keyboard test pushes
`SDL_EVENT_TEXT_INPUT` into SDL's queue by hand, which is what a real keyboard produces — *once
text input has been started*. Synthesising the end of a chain cannot test the beginning of it.
The new test asserts `SDL_TextInputActive()` directly for that reason, and the whole frontend
suite has been run against a real X11 window as well as the dummy driver.

One limit remains, and it is a choice rather than a gap:

- **No cursor movement within a line.** Backspace and escape only. The arrow keys stay in the
  ring for a script's own `GET` loop, which is the more valuable use of them.

A non-ASCII character is accepted by the keyboard and then dropped rather than stored, which is
§1.2 rather than the editor: the grid is a byte per cell and a value's string is a fixed 256
bytes, so a multi-byte character has nowhere to go. Dropping it is honest where storing half of
it is not.

`WINDOW` and `KEY` in group E want a text-window rectangle and a function-key table on top of
this, which is ordinary work here rather than anything blocked.

---

## 4. Remaining work: language completion (goal 2)

The work queue is the "What Isn't Implemented" list in `deps/basicinterpret/README.md`.
Each verb is: table row (§1.1) → parse handler if it needs one → exec handler → unit test →
a new `.bas`/`.txt` pair. **Both** kinds of test; they answer different questions.

Order by what unblocks the most, and by what does not need `libakgl` to grow first:

| Group | Verbs | Blocked on |
|---|---|---|
| ~~A. Structure~~ | ~~`DO`, `LOOP`, `WHILE`, `UNTIL`, `ON`, `BEGIN`, `BEND`, `END`~~ | **done** — `src/runtime_structure.c`, `tests/structure_verbs.c` |
| ~~B. Housekeeping~~ | ~~`NEW`, `CLR`, `CONT`, `SWAP`, `TRON`, `TROFF`, `HELP`~~ | **done** — `src/runtime_housekeeping.c`, `tests/housekeeping_verbs.c` |
| ~~B′. Housekeeping, deferred~~ | ~~`RESTORE`, `RENUMBER`~~ | **done** — `src/data.c` and `src/renumber.c`, `tests/read_data.c` and `tests/renumber.c` |
| ~~C. Errors~~ | ~~`TRAP`, `RESUME`, `ER`, `ERR`~~ | **done** — `src/runtime_trap.c`, `tests/trap_verbs.c`. `ER`/`EL` are `ER#`/`EL#`; see §5 |
| ~~D. Strings/format~~ | ~~`USING`, `PUDEF`, `WIDTH`, `CHAR`~~ | **done** — `src/format.c`, `src/runtime_format.c`, `tests/format_verbs.c` |
| ~~E. Console~~ | ~~`WINDOW`, `KEY`, `SLEEP`, `WAIT`, `TI`~~ | **done** — `src/runtime_console.c`, `tests/console_verbs.c` |
| ~~F. Disk~~ | ~~all 21~~ | **done** — `src/runtime_disk.c`, `tests/disk_verbs.c`. Five refuse for want of a drive and `DIRECTORY` for want of an upstream wrapper; see §5 |
| ~~G. Graphics~~ | ~~`GRAPHIC`, `DRAW`, `BOX`, `CIRCLE`, `PAINT`, `COLOR`, `SCALE`, `SSHAPE`, `GSHAPE`, `LOCATE`~~ | **done** — `src/runtime_graphics.c`, `tests/graphics_verbs.c` |
| ~~H. Sprites~~ | ~~`SPRITE`, `MOVSPR`, `SPRCOLOR`, `SPRSAV`, `COLLISION`~~ | **done** — `src/runtime_sprite.c`, `src/sprite_akgl.c`, `tests/sprite_verbs.c`. `SPRDEF` is out of scope; see below |
| ~~I. Audio~~ | ~~`PLAY`, `SOUND`, `ENVELOPE`, `VOL`, `TEMPO`~~ | **done** — `src/runtime_audio.c`, `src/play.c`, `tests/audio_verbs.c` |
| I′. Audio, still gapped | `FILTER` | `libakgl` has no filter stage and SDL3 supplies no primitive; refused with `AKBASIC_ERR_DEVICE`. `SOUND`'s sweep arguments used to be here and landed with `akgl_audio_sweep` in libakgl 0.3.0 |
| ~~E′. Console input~~ | ~~`GET`, `GETKEY`, `SCNCLR`~~ | **done** — `src/runtime_input.c`, `tests/input_verbs.c` |
| ~~J. Machine~~ | ~~`SYS`, `FETCH`, `STASH`~~ | **done** — `src/runtime_machine.c`, `tests/machine_verbs.c`. `SYS` is refused by name; see §5 |

**`RESTORE` and `RENUMBER` are deferred, and neither is a small job.**

- ~~**`RESTORE` needs a DATA pointer, and there isn't one.**~~ **Done.** Every `DATA` statement
  is pre-scanned into one flat list before the program runs -- `src/data.c`, called from
  `akbasic_runtime_set_mode()` beside the label prescan -- and `READ` walks a cursor along it.
  `RESTORE` resets the cursor; `RESTORE (line)` moves it to the first item on or after that
  line.

  It fixed two defects on the way, both of which were consequences of `READ` not reading.
  **A `DATA` line above its `READ` is now found**, where skipping forward from the `READ` used
  to run off the end of the program in silence. And **the lines between a `READ` and its `DATA`
  now execute**, where the skip used to swallow them -- a C128 runs them, because `READ` takes
  the next item and execution carries on. The one corpus case has its `DATA` immediately after
  its `READ`, so neither was observable there. `tests/read_data.c`.

  `DATA` at run time is now a no-op: it is a declaration, and reaching the statement means
  walking past it.

- ~~**`RENUMBER` has to rewrite references or it is worse than useless.**~~ **Done.**
  `src/renumber.c` builds the whole old-to-new map first, then rewrites every line -- including
  lines *before* the renumbered region, which can branch into it -- and only then moves them.

  The rewrite is textual and understands exactly one thing about the rest of the language:
  where a string literal starts and stops, so `PRINT "GOTO 10"` survives. `GOTO`, `GOSUB`,
  `RUN`, `RESTORE` and `TRAP` targets are rewritten, and so is the comma-separated list after
  `ON X GOTO` -- for free, since the targets follow the `GOTO`. `COLLISION` is special-cased
  because its handler is its *second* argument.

  Two deliberate non-rewrites. **A target naming a line that does not exist is left alone**:
  `GOTO 9999` in a program with no line 9999 is already broken, and inventing a destination
  would hide that. And **`THEN`/`ELSE` do not take bare line numbers in this dialect** -- the
  corpus writes `THEN GOTO 100` -- so a number after `THEN` is an expression, not a target.

  A renumbering that would land a moved line on a kept one is refused before anything is
  written, because losing a line to a renumbering is not recoverable.

  **A program written with labels needs none of this.** `GOTO DONE` is unaffected by any
  renumbering, which is the strongest argument for `LABEL` there is.

`LOCATE` used to appear in both E and G. It belongs to G alone: in BASIC 7.0 `LOCATE` moves the
*graphics* pixel cursor that `DRAW` starts from, and the text cursor verb is `CHAR`, which is
already in group D.

**Out of scope, and staying that way** — the reference marks these as incompatible with a
modern PC and that reasoning stands: `BANK` (no bank switching), `FAST` (irrelevant CPU speed
control), `MONITOR` (no machine-language monitor). **`SPRDEF` joins them** on the same
reasoning: it is not a programmable verb but an interactive full-screen sprite editor, driven
by single keystrokes and its own cursor. A program cannot call it usefully and this interpreter
does not own the screen it would take over. The three `SPRSAV` source forms are what replace it.

**The `libakgl` JSON layer turned out not to be in the way at all.** Group H was parked on
"check the sprite/actor API before filing", and the concern was that libakgl's sprites are
described by JSON documents that would be cumbersome to reach through BASIC.
`akgl_sprite_load_json()` is a thin wrapper over `akgl_spritesheet_initialize` +
`akgl_sprite_initialize` + writes to public struct fields, and every field it fills from a
document — frame list, animation speed, loop flags, state-to-sprite map — is something a
Commodore sprite does not have. So `src/sprite_akgl.c` builds the same four objects directly.
The one field that *did* survive into the verb syntax is the spritesheet's filename:
`SPRSAV "ship.png", 1` goes through `akgl_path_relative()` and `akgl_spritesheet_initialize()`,
which is the same pair of calls the JSON loader makes.

Also on the queue, from the reference's own defect list:

- ~~**Multiple statements per line**~~ **Done.** `akbasic_parser_parse()` consumes a run of
  `COLON` tokens before each statement and hands the caller a NULL leaf when nothing followed
  them, which is how an empty statement — a trailing separator, or `::` — stays legal rather
  than becoming an error. `tests/parser_commands.c` counts statements per line;
  `tests/language/statements/multiple_per_line.bas` asserts what actually runs.

  **One thing about it needed a decision rather than a transcription**, and it is recorded as
  deviation 31 in §5: BASIC 7.0 scopes everything after `THEN` to the condition, so
  `IF C THEN A : B` must run neither `A` nor `B` when `C` is false. The parser takes exactly one
  statement per arm, so the rest of the line reaches the statement loop as ordinary statements
  and the branch has to tell the loop whether to run them.

  **A known limitation, not a defect:** a whole `FOR`/`NEXT` on one line
  (`FOR I# = 1 TO 3 : PRINT I# : NEXT I#`) does not loop. The block structure is the
  reference's `waitingForCommand` model (§1.6), which skips forward *by source line* to the verb
  it is waiting for, so a `NEXT` on the same line as its `FOR` is never reached. Fixing it means
  restructuring control flow to work on statements rather than lines, which is a deliberate
  piece of work and wants its own commit. Group A's `DO`/`LOOP` will meet the same wall.
- ~~**Array references in parameter lists**~~ **Done**, and it turned out to be §6 item 13 a
  third time rather than a separate defect. An identifier's subscript list moved to `.expr`, and
  `akbasic_ASTLeaf` grew a `next` field so an argument list chains through a link of its own.
  That last part is the real fix: the reference chains arguments through each argument's own
  `.right`, and *every* leaf type that can be an argument already uses `.right` for something,
  so moving one operand out of the way only shifted the collision along. Covered by
  `tests/language/arrays_in_parameter_lists.bas` and `tests/runtime_evaluate.c`.

### What a demoscene program wanted and could not have

`examples/megademo/megademo.bas` was written to push the interpreter to its edges, and these are
the edges it hit. **None of them blocked the demo** — every one has a workaround and the program
carries all six — but each workaround is a routine every future game will carry too, and that is
the argument for the verb.

Ordered by how much BASIC each one would delete: `RND` and `ASC` (#16), the unwritable palette
(#17), `GSHAPE`'s missing blend modes (#18), no way to ask whether `PLAY` has drained (#19),
`PLAY` being monophonic (#20), `DATA` being too small a pipe for assets (#21), and the `TI#` spin
that starves the audio queue (#22).

**The last of those is the one worth reading before writing a game loop.** Polling `TI#` does not
merely waste CPU: the host calls `settime()` once per `runtime_run()` batch, so a batch of spin
statements drops the note release rate below what a sixteenth-note tune needs, the queue never
reclaims slots until it drains completely, and the backlog compounds until `PLAY queue is full`.
Reproduced both ways in isolation. Sleep a frame instead.

---

## 5. Deliberate deviations from the reference

Keep this list current. Most of these change structure without changing observable output;
the ones that *do* change output say so and carry the golden file they moved.

The bar has dropped since this list was started. It used to be "defensible against the golden
suite", because matching the Go implementation byte for byte was goal 1's headline claim. That
implementation is now deprecated (§0.1), so the bar is the ordinary one: defensible on its own
merits, recorded here, and tested.

1. Reflection → static dispatch table (§1.1).
2. Go maps → fixed open-addressed tables (§1.3).
3. Unbounded `new(BasicEnvironment)` → pool with release (§1.4).
4. Hardcoded stdout+SDL mirror → text sink backend (§1.5).
5. `run()` owns the process → `step()` / bounded `run()` (§1.6).
6. `panic()` → `FAIL_RETURN`; no library call terminates the process (§1.6).
7. `debug.PrintStack()` on parse error → the `akerr` stack trace only (§3.1 of the original plan).
8. `BasicEnvironment.eval_clone_identifiers` deleted as dead state (§4.2 of the original plan).
9. `BasicValue.name` and `BasicEnvironment.update()` deleted as dead: nothing ever writes the
   field a non-empty string, and `update()` — its only reader — has no callers.
10. The `DEF`-statement bootstrap is gone. The reference declares every builtin by *running a
    BASIC program of `DEF` lines through the interpreter* at startup and then nulling out the
    expressions (`basicruntime_functions.go:14`); here a builtin's name, arity and handler are
    a row in the dispatch table, and `MOD`, `SPC` and `STR` are ordinary native handlers. This
    removes the need to run the interpreter before the interpreter is ready.
11. Undefined behaviour the reference reaches by a defined route is refused rather than
    inherited: a shift count outside 0..63, a negative string multiplier, and integer division
    by zero all raise. Go defines all three (or panics); C does not. No golden case exercises
    any of them, so observable behaviour is unchanged.
12. `CommandEXIT` clears the pending `NEXT` wait before popping. The reference does not, which
    leaves the parent waiting for a `NEXT` that never arrives (§6 item 8) — in C that is a hang
    rather than a misbehaviour, and no golden case depends on it.

### Deviations in the verbs the reference never implemented

Items 1–12 above are deviations *from ported code*. These are deviations from **Commodore
BASIC 7.0**, in verbs the reference lists as unimplemented and which therefore had no Go
behaviour to port. They matter to somebody typing in a listing out of a C128 manual.

13. **Hardware verbs reach a device through a backend record, and refuse when there is none.**
    `akbasic_GraphicsBackend`, `_AudioBackend` and `_InputBackend` are records of function
    pointers on the runtime; all three may be `NULL`, which is what the current stdio-only
    standalone driver gives them. That is correct for a default build and unfinished for an
    `AKBASIC_WITH_AKGL` build; see §3. A verb that needs a device it was not given raises
    `AKBASIC_ERR_DEVICE` naming itself. `COLOR`, `LOCATE`, `SCALE`, `ENVELOPE`, `TEMPO` and
    `VOL` deliberately do *not* require one — they change interpreter state, so a program can
    configure itself before the host lends it a renderer.

14. **`CIRCLE` is a polygon, always.** BASIC 7.0's `CIRCLE` takes two radii, a start and end
    angle, a rotation and a degree increment, which makes it an inc-degree polygon by
    definition. `akgl_draw_circle` exists and is deliberately **not** used: it draws one
    radius, full sweep, unrotated, so it could serve only the case where every optional
    argument is defaulted — and a shape that changed character depending on whether the two
    radii happened to be equal is worse than one that is uniformly a polygon.

15. **`SSHAPE` stores a handle in the string, not the pixels.** A real C128 packs the
    region's bitmap into the string variable. Here a value's string is a fixed 256 bytes
    (§1.2) and a saved region is a device surface, so the variable gets `SHAPE:<n>` and the
    surface stays in a fixed pool on the backend. `GSHAPE` accepts only a string carrying that
    prefix, so a hand-written one is refused rather than pasting an unrelated slot. **What
    this costs:** a shape cannot be written to disk, cannot be concatenated, and `LEN` of it
    is not its size. Nothing in BASIC does any of those to a shape string except a program
    deliberately poking at it.

16. **`BOX` cannot fill at all, and the plan for it is to spell fill as a negative angle.**
    7.0's last argument selects outline or fill and sits *after* the rotation, which would
    make a filled box a seven-argument call — one more than `akbasic_cmd_box()` collects.
    Spelling it as a negative rotation instead is the intended answer and **is not
    implemented**: today a rotation of zero outlines through the renderer's rectangle and any
    other rotation, negative included, draws four lines.

    **This entry used to claim in bold that it fills**, with the body then saying it was
    filed rather than implemented — a headline that contradicted its own paragraph, which is
    exactly how a reader ends up believing a feature exists. Found by writing a
    documentation figure for `BOX` and getting an outline back. `PAINT` is the fill a
    program has today.

    **`akbasic_GraphicsBackend::filled_rect` is dead from the language's side** as a direct
    consequence: `src/graphics_akgl.c` implements it and `tests/mockdevice.h` records it, but
    no BASIC verb reaches it. It is the entry point this fix would call, so it is waiting
    rather than unused — worth knowing before somebody tidies it away.

17. **`GRAPHIC` records its mode but honours only one consequence of it.** 7.0's five modes
    differ in bitmap resolution and in whether the bottom of the screen stays text. Neither
    means anything against a host's renderer, whose size the interpreter does not own. The
    mode is stored, an out-of-range one is refused because that is a typo worth catching, and
    the one observable rule is that mode 0 is text. The mode is readable as `RGR(0)`, which
    is 7.0's whole `RGR` and the one field of ours that is.

18. **A coordinate reaching a backend is a device pixel, and the whole of the host's window
    is reachable from BASIC.** This used to read "coordinates are BASIC's 320x200 space and
    scaling to the window is the host's job", on the reasoning that the interpreter cannot
    ask the renderer how big it is without owning it. **That reasoning was wrong twice over**,
    which is what §8 item 9 came to say.

    It was wrong about the mechanism: not owning a thing is no bar to *asking* it, and the
    backend record is how everything else here asks. `akbasic_GraphicsBackend` grew a `size`
    entry point, `require_graphics()` calls it before every verb that draws -- so a resized
    window is honoured between two statements rather than at attach -- and 320x200 became
    the fallback for a backend that leaves `size` NULL. It is the record's one optional
    entry point, so a host written against the old header keeps working and gets exactly the
    behaviour it used to get.

    And it was wrong about the description: nothing ever stretched anything. With `SCALE`
    off a coordinate was passed straight to `akgl_draw_*` as a pixel address, so an 800x600
    window drew a C128 listing into its top-left 320x200 corner and left the rest unused.
    The documentation described a coordinate transform that did not exist.

    **What changed observably** is `SCALE`, which now maps user coordinates onto the device
    rather than onto the constants, and `RGR(1)` / `RGR(2)`, which report the drawing
    surface's width and height so a program can use a window whose size it did not choose.
    A C128 listing draws in the corner as it always did; `SCALE 1, 319, 199` gives it the
    whole window.

    **One fix rode along, in the same line of code and too small to defer.** `SCALE` mapped
    `xmax` onto the *width*, which put the user space's far corner one pixel past the
    surface: `SCALE 1, 319, 199` then `DRAW 1, 319, 199` drew nothing at all. It maps onto
    the last pixel now, which is what makes that sentence above true rather than nearly
    true. `tests/graphics_verbs.c` and `tests/akgl_backends.c`, the latter against a
    128x128 target chosen because it is *smaller* than the old constants -- a `SCALE` still
    dividing by them misses it entirely rather than landing somewhere plausible.

19. **`PLAY` does not block.** On a C128 `PLAY` holds the program until the last note ends.
    §1.6 forbids that outright, so `PLAY` parses the string into a fixed queue and returns;
    `akbasic_runtime_step()` releases one note at a time against whatever time the host last
    passed to `akbasic_runtime_settime()`. **What this changes for a program:** the statement
    after a `PLAY` runs immediately rather than a bar later, so a listing that relied on `PLAY`
    for timing will race ahead. A program that wants to wait should test the queue rather than
    assume the verb waits for it.

20. **The host owns the clock, and an unset clock rushes the music.** The library reads no
    clock — it owns no loop and must not block. A host calls `akbasic_runtime_settime()` once a
    frame; the standalone driver calls it every step off `CLOCK_MONOTONIC`. Left unset it reads
    zero forever, so every duration has already expired and the queue drains as fast as
    `step()` is called. Audible, deterministic, and never a hang, which is the right way for
    that to fail.

21. **`PLAY`'s `M` (measure) is a no-op, and `SOUND`'s sweep runs once rather than
    oscillating.** `M` synchronises the three voices on a C128; with one sequential queue there
    is nothing to synchronise, and a listing full of them should still play, so it is accepted
    and ignored.

    The sweep *used* to be refused outright — there was no `akgl_audio_*` equivalent and faking
    one would have tied audible pitch to how often the host calls us. `libakgl` 0.3.0 added
    `akgl_audio_sweep()` and `SOUND voice, freq, dur, dir, min, step` now reaches it. What does
    not survive is `dir` 3, "oscillate": `akgl_audio_sweep` runs one pass between two endpoints,
    and a real oscillation needs the mixer to turn around at them. **What this changes for a
    program:** a `dir` 3 siren rises or falls once instead of warbling. `dir` 1 and 2 are exact,
    and so is the direction — both the SID and `akgl_audio_sweep` take it from which endpoint is
    higher, so a `min` above the starting frequency rises whatever `dir` says.

    `step` is converted as a *delta* rather than a position: it is a register increment, and the
    register-to-hertz table maps positions, so it goes across as the distance between register 0
    and register `step`. A step that converts to zero would never arrive and is raised to one
    hertz, which is the smallest move that still gets there.

    A backend whose record has no `sweep` — a host written against `libakgl` 0.2.0 — still
    refuses a swept note with `AKBASIC_ERR_DEVICE` and plays a held one normally.
    `tests/audio_verbs.c` asserts both halves.

22. **`TEMPO`'s constant is a calibration choice, not a transcription.** BASIC 7.0 documents
    `TEMPO` as a relative duration from 1 to 255 defaulting to 8, and does not publish what a
    whole note actually lasts. `src/audio_tables.c` uses 16000 ms at `TEMPO` 1, which puts a
    default-tempo quarter note at 500 ms — 120 beats per minute. If the real constant turns
    up, that is the one line to change.

23. **`GETKEY` holds the step loop rather than blocking.** A C128 sits on the keyboard inside
    `GETKEY` until somebody types. §1.6 forbids the library blocking, so the verb sets a flag
    and `akbasic_runtime_step()` declines to advance the program until a key arrives. Every
    step still returns and a bounded `akbasic_runtime_run()` still comes back, so a host keeps
    its frame rate; the program simply does not move past the `GETKEY`, which is the part a
    program author cares about. The `PLAY` queue is serviced *before* this check on purpose —
    music should keep playing while a program waits for a keypress. Withdrawing the input
    device while a `GETKEY` is holding releases it rather than wedging the script forever.

24. **`GET` and `GETKEY` into a numeric variable yield the key code.** BASIC 7.0's `GET` takes
    a string variable. Accepting an integer one as well, and giving it the raw code, is what a
    program testing for cursor or function keys needs and costs nothing. No key is code zero,
    matching what a C128 reports. A float variable is refused: it is neither a character nor a
    code.

### Deviations in the standalone frontend

Items 1–12 are deviations from ported interpreter code and 13–24 from BASIC 7.0. These four are
deviations from the reference's *program*: `main.go` and the SDL half of
`basicruntime_graphics.go`.

25. **The frame loop is bounded and the reference's is not.** Go's `run()` is a `for {}` that
    owns the process until `MODE_QUIT` (`basicruntime.go:682`), which is why the reference's
    window never answers its close button. Here the driver runs
    `AKBASIC_FRONTEND_STEPS_PER_FRAME` steps, pumps, presents, and goes round again. **What
    this changes for a program:** nothing observable — a bounded run reproduces an unbounded one
    exactly, and the whole golden corpus is driven through this loop to prove it. What it
    changes for a *user* is that the window closes when asked.

26. **The text layer repaints every row it owns, every frame.** Not just the rows that changed.

    That was tried — a `drawn[]` array marking which rows had carried glyphs, so an untouched
    row could be skipped — and it is **wrong on real hardware**. `SDL_RenderPresent` swaps
    buffers, so a frame does not inherit the frame before it; it inherits the one *two* back,
    and on some backends something undefined. A row erased once is clean in one buffer and still
    dirty in the other, and presenting alternates between them.

    That is what a backspace across a line wrap looked like: the first character of the wrapped
    tail flickering in and out forever after the rest had gone. It reproduces on X11 and never
    under the dummy driver or a software renderer, which is exactly why the suite was green
    through two rounds of fixing it. `tests/akgl_frontend.c` now paints stale pixels by hand —
    which is what a swapped-in buffer hands back — so the case is covered without needing real
    hardware.

    There is no cheaper correct answer available: a frame either owns every pixel it presents or
    it inherits pixels it cannot reason about.

    **What this costs, and it is more than it used to.** The host still does not call
    `SDL_RenderClear`, but the text area is now repainted in full every frame, so anything a
    graphics verb drew underneath it is erased every frame rather than only where text lands. In
    the standalone driver the text area is the whole window, so `DRAW` and `PRINT` no longer
    coexist there.

    **And the same buffer swap means the graphics verbs were never reliable across frames
    anyway**, which is worth stating plainly rather than leaving as a surprise: a one-shot `DRAW`
    lands in one buffer and the next present shows the other. It looked fine in a single
    screenshot and would have flickered exactly as the text did. Making graphics survive needs a
    persistent surface the frame is composited from, which is real work and its own commit —
    filed here rather than half-done.

27. **The cursor is a blinking block.** The reference draws an underscore glyph (`drawCursor`,
    `basicruntime_graphics.go:33`), and so did this until the underscore turned out to sit
    *under* the text being typed and make it hard to read. A block occupies the cell after the
    text instead of the space beneath it.

    Half a second per blink (`AKBASIC_SINK_CURSOR_BLINK_MS`), which is about a Commodore's and
    slow enough to read under. `akbasic_AkglSink.cursorperiodms` set to zero holds it solid,
    which is what makes a frame deterministic for a test.

    Two details that are decisions rather than accidents. The clock is **SDL's**, not the
    host's: everywhere else the host owns the clock because the library owns no loop and must
    not block, but this is an adaptor that already links SDL and threading a timestamp through
    the sink interface to animate a cursor would be a poor trade. And a line that exactly fills
    a row leaves the cursor one column past the end, because `putchar_at` wraps only when the
    next character arrives — the cursor is drawn at the start of the next row instead, which is
    where the next character will actually land and where a C128 puts it.

28. **The window is closed by the host, and that is not `QUIT`.** Closing the window stops the
    frontend and leaves `obj->mode` alone; it does not set `AKBASIC_MODE_QUIT`. The distinction
    is invisible to the standalone program, which exits either way, and load-bearing for an
    embedding host: a game that closes its own window has not decided that the script is
    finished, and a script that ran `QUIT` has. `tests/akgl_frontend.c` asserts both halves.

29. **Piped input still works in an AKGL build.** With a terminal on stdin the window is the
    console and lines come from its editor; piped or redirected, they come from the pipe. This
    is not in the reference, which always reads `os.Stdin` in REPL mode. It exists so
    `basic < program.bas` behaves the same in both builds — and so the golden corpus can be
    driven through the SDL binary, which is where most of the frontend's real coverage comes
    from.

### Deviations that change what a program is allowed to do

30. **A verb name with a type suffix is not a variable name.** `PRINT$ = 1` is refused where
    the reference accepts it, because its reserved-word check searched the keyword tables with
    the suffix still attached and therefore never matched (§6 item 16). A real C128 refuses it
    too, so this is the reference being wrong rather than this interpreter being strict.

    **What it costs a program:** a listing that used `INPUT$`, `LEN#` or `GOTO%` as a variable
    stops parsing, and the fix is to rename the variable. One case in the reference's own corpus
    did exactly that; see `tests/language/examples/strreverse.bas`.

### Deviations in statement separation

31. **A branch decides who owns the rest of its line.** BASIC 7.0 scopes every statement after
    `THEN` to the condition, and the reference has no opinion on the matter because it never
    consumed the `COLON` token at all. The parser here takes exactly one statement for each arm,
    so the rest of the line arrives at the statement loop as ordinary top-level statements and
    something has to say whether to run them. The `BRANCH` case in
    `akbasic_runtime_evaluate()` sets `runtime->skiprestofline`, and the two statement loops
    stop on it.

    **The rule is not "skip when false"**, which is the reason this is written down:

    | Line | Condition | What runs |
    |---|---|---|
    | `IF C THEN A : B` | true | `A`, `B` |
    | `IF C THEN A : B` | false | nothing |
    | `IF C THEN A ELSE B : D` | true | `A` |
    | `IF C THEN A ELSE B : D` | false | `B`, `D` |

    The remainder always belongs to whichever arm was written *last*, so it is skipped exactly
    when that arm is the one not taken — with an `ELSE` the last arm is `ELSE`, without one it
    is `THEN`. That is one line of code and it reads as an oddity without the table above it.

### Deviations in the structure and error verbs (groups A and C)

61. **Structures are an addition, not a port.** BASIC 7.0 has no records at all, so
    `TYPE`/`END TYPE`, `DIM X@ AS T`, `.` and `->`, `PTR TO` and `POINT ... AT` are
    invented here. The `@` suffix was not: the Go reference reserved
    `IDENTIFIER_STRUCT` and never used it, and `src/grammar.c` rendered such a leaf as
    `"NOT IMPLEMENTED"` until this landed. `docs/16-structures.md` is the whole feature.

    **Assignment copies and `POINT` shares**, which is the decision everything else rests
    on. A structure is a value like every other value here, so a program that never writes
    `POINT` can never be surprised by aliasing — and the two field operators are kept
    apart so a reader always knows from the spelling which they are looking at. `.` on a
    pointer and `->` on a value are both errors, each naming the other.

    **A declared type is what buys the storage model.** An instance has a known slot count
    and is laid out exactly as an array is, out of the same value pool, so nothing new
    holds data — only a table of descriptors. It also bounds copy depth statically, since a
    `TYPE` cannot contain itself by value.

    **Three limits are ours and are stated rather than derived:** 16 types, 16 fields per
    type, and four levels of nesting in `PRINT`. The last is not a safety bound on copying —
    copy stops at pointers by construction — but on *rendering*, which must follow a
    pointer and would not come back on a cycle. Four rather than eight because the bound
    has to bite before the 256-byte render buffer does, or a cycle stops because it ran out
    of room rather than because it was told to.

    **A type name shares a namespace with verbs and labels**, since all three are bare
    words. Refused at declaration with a message that says so, because the parser's own
    answer was "Expected expression or literal" pointing at the line rather than the
    problem. Field names follow the same reserved-word rule variable names already do,
    enforced by the loader's scan — `TO@` is a bad field name for exactly the reason `TO#`
    is a bad variable name.

62. **A host's C struct is the same thing with its bytes somewhere else.**
    `akbasic_host_register_type()` puts it in the *same* table a `TYPE` fills, so copy,
    `PTR TO`, `.` and `->` all work across the boundary with no second set of rules — and
    the language's own copy-versus-`POINT` distinction turns out to be exactly the
    snapshot-versus-share distinction a host needs, so there is one API rather than two.

    A binding takes a **shadow run** of slots; a read refreshes from host memory and a
    write converts back. Conversion **refuses rather than truncates** — 70000 into an
    `int16_t` names the field — and a field name's suffix must agree with the C type it
    describes, refused at registration.

    **It is the only pointer this interpreter holds that it did not allocate**, and that
    is the one thing a host has to think about. `akbasic_host_unbind()` exists for it.
    `examples/hoststruct.c` is a working host, built and run by every build.

44. **A whole loop on one line does not loop.** `DO : PRINT 1 : LOOP` runs once, exactly as
    `FOR I=1 TO 3 : PRINT I : NEXT I` does. Block skipping walks *source lines*, which is the
    reference's `waitingForCommand` model (§1.6), so a `LOOP` on the same line as its `DO` is
    never reached as a separate step. Fixing it means making the skip operate on statements
    rather than lines — a deliberate piece of work, already noted in §4 against `FOR`.

45. **`ER` and `EL` are `ER#` and `EL#`.** A C128 exposes them as bare reserved names. This
    dialect cannot: an identifier carries its type in a suffix, and a name without one is a
    *label*. They are written into the global scope when a trap fires, so `PRINT ER#` reads the
    way the rest of the language does and needs no new syntax.

    `ER#` carries the `libakerror` status code, not a Commodore error number — 515 for an
    out-of-bounds subscript rather than C128 error 9. There is no correspondence to reproduce:
    the errors this interpreter raises are not the errors a 1985 ROM raised. `ERR(ER#)` gives
    the name, and that is the part a program should be printing anyway.

46. **`END` does not arm `CONT`.** A C128 lets `CONT` resume after `END` as well as after
    `STOP`. Here `END` means the program finished, and continuing from a finished program
    resumes at whatever line happens to follow — so `CONT` refuses instead. `STOP` still arms it.

### Deviations in the format verbs (group D)

47. **`PRINT USING` renders one field per statement.** BASIC 7.0 lets a format string hold
    several fields and `PRINT USING` take a list of values against them. Here the *first* field
    is used and any literal text around it is copied through, so `PRINT USING "A: ###"; X` works
    and `PRINT USING "### ###"; X, Y` does not. Multiple fields need `PRINT` to take a value
    list, which it does not yet — a separate piece of work from the field rendering itself,
    which `src/format.c` does properly.

    Exponential fields (`^^^^`) are not implemented either. Nothing in the language produces a
    value that needs one, since `PRINT` renders a float with `%f`.

48. **`WIDTH` is emulated with parallel passes.** It sets the thickness of drawn lines, and
    neither the graphics backend record nor `akgl_draw_line` takes a thickness — so a `WIDTH 2`
    line is drawn twice, offset one pixel along whichever axis the line does *not* mostly run
    along. Visually right at the only other value the verb accepts; a general thickness would
    want a real perpendicular offset and an API that takes one.

49. **`CHAR` ignores its colour argument, and needs a sink that has a cursor.** The text sink
    draws in one colour, chosen by the host when it built the sink, so a per-call colour would
    have to become part of the sink interface. The argument is accepted and ignored rather than
    refused, because refusing it would make every published `CHAR` line a syntax error.

    Positioning is a new optional `moveto` on the sink record. A stdio sink leaves it NULL and
    `CHAR` refuses by name — a terminal's cursor is not this library's to move and a pipe has no
    cursor at all — so `CHAR` works in an AKGL build and says why it cannot in a stdio one.

### Deviations in the console verbs (group E)

50. **`SLEEP` and `WAIT` hold the step loop; they do not block.** §1.6 forbids blocking, so a
    waiting program is one that does not *advance* — `akbasic_runtime_step()` still returns, a
    bounded `akbasic_runtime_run()` still comes back on time, and an embedded game keeps its
    frame rate while a script sleeps.

    `SLEEP` with no host clock does nothing at all rather than waiting forever. A deadline
    computed from a clock that never advances is never reached, and the first version of this
    hung the test suite proving it.

51. **`TI` and `TI$` are `TI#` and `TI$`.** Same reason as `ER#`/`EL#`: no bare variable names.
    They are ordinary globals refreshed once per step from the host's clock, rather than
    pseudo-variables, because this dialect has no mechanism for a name that computes itself.
    A host that never sets a clock gets a stopped clock rather than a wrong one.

52. **`WAIT` polls ordinary process memory.** On a C128 the byte is a hardware register an
    interrupt is changing. Here nothing changes it but the host, another thread, or a `POKE`
    from a `TRAP` handler — so a program that waits on a byte nobody writes waits forever, which
    is exactly what the same program does on a C128 with the wrong address. Read through a
    `volatile` pointer, because the whole point is that something outside this program writes it.

53. **`KEY` stores its macros and nothing expands them.** The definitions are kept and bare
    `KEY` lists them, which is the half that is this library's business. Expanding a macro when
    a function key is pressed belongs to whatever owns the keyboard — the frontend's line editor
    — and the input backend delivers keycodes rather than editor commands.

### Deviations in the machine verbs (group J)

54. **`FETCH` and `STASH` are the same byte copy.** On a C128 they differ by which side is the
    RAM Expansion Unit — `STASH` writes out to it, `FETCH` reads back. There is no expansion
    unit and there are no banks, so both are `memmove` between two addresses and saying so is
    better than inventing a distinction. `memmove` rather than `memcpy` because a program
    shifting a buffer along by a few bytes overlaps its own source, which is a normal ask.

    Addresses are real process addresses, which is the decision `POKE`, `PEEK` and `POINTER`
    already made. There is no bounds check and there cannot be one: a wrong address is a
    segmentation fault, not an error message.

55. **`SYS` is refused by name.** It calls machine code at an address. There is no 6502 and no
    ROM to call, so there is nothing to jump to and nothing sensible to emulate — and jumping to
    a real address in this process would be a way to crash it on purpose. It parses first and
    refuses second, so a listing containing `SYS` still loads and still lists; only running it
    fails, and it says why.

    Same reasoning as `BANK`, `FAST` and `MONITOR`, with one difference: those have no table row
    at all, and `SYS` has a handler because it is common enough in published listings to deserve
    a specific message rather than "Unknown command".

### Deviations in the disk verbs (group F)

56. **There is no 1541, so the verbs split three ways.** The ones that mean something on a
    filesystem are implemented against `aksl_f*` — `DOPEN`, `DCLOSE`, `APPEND`, `RECORD`,
    `SCRATCH`, `RENAME`, `COPY`, `CONCAT`, `BSAVE`, `BLOAD`, `VERIFY`. The ones that are
    spellings of verbs already here are aliases: `SAVE` is `DSAVE`, `LOAD` is `DLOAD`, `CATALOG`
    is `DIRECTORY`, `DVERIFY` is `VERIFY`.

    **`HEADER`, `COLLECT`, `BACKUP` and `BOOT` are refused by name**, with the reason. Each
    operates on a physical disk — formatting one, validating its block allocation map,
    duplicating it, booting from it — and a filesystem has no equivalent that is not a lie.
    `HEADER` would have to mean "delete everything in this directory", which is a
    spectacularly bad thing to do to somebody who typed a C128 verb. `DCLEAR` is the exception:
    resetting a drive also closes its channels, and closing the channels is real, so that is
    what it does.

57. **`DIRECTORY` is refused for want of an upstream wrapper.** `libakstdlib` has no
    `aksl_opendir`, and this project's rule is that a missing capability is filed upstream
    rather than worked around — so it is filed against `libakstdlib` and the verb says
    so. Calling `opendir(3)` here would mean reporting through `errno` in a file where
    everything else reports through an `akerr_ErrorContext *`.

58. **`PRINT #` and `INPUT #` need the space before the `#`.** A C128 writes `PRINT#1,A$`. The
    scanner reads `#` as a type suffix, so `PRINT#` comes out as an identifier named `PRINT`
    with an integer suffix — and a verb name carrying a suffix is refused (deviation 30). With
    a space the `#` is its own token and the parse handler reads it.

59. **`RECORD` counts lines, not fixed-length records.** A C128's relative file has a record
    length fixed when the file was created; a filesystem file has none. So a record is a line
    and `RECORD` rewinds and reads forward to it, which is slower than a seek and is the only
    definition that does not invent a record length the file does not have.

60. **`BLOAD` requires a length.** A C128 reads until the file ends. Here the address is a real
    process address, so a file longer than the caller expected would write past whatever it was
    pointed at with no way to notice. Refusing to guess is the only safe answer.

### Deviations in conditions

41. **A lone `=` is equality inside a condition.** `IF A# = 2 THEN` works, which it did not
    before: the scanner reads `==` as EQUAL and a single `=` as ASSIGNMENT, because at that
    point it cannot know whether it is looking at a statement or a condition, and the reference
    never resolved the ambiguity anywhere. A condition is the one context where it has an
    answer — BASIC has no assignment expression — so `akbasic_Parser::comparing` is set around
    the condition and the relation parser offers ASSIGNMENT as a comparison operator only while
    it is. `==` keeps working, and the whole checked-in corpus is written in it.

    Outside a condition `=` has to stay an assignment, which is what the flag is for: the first
    attempt put ASSIGNMENT in the operator list unconditionally, and `FOR I# = 1 TO 5` promptly
    stopped initializing its counter.

42. **A condition is a whole expression, so `AND` and `OR` work in one.**
    `IF A# = 5 AND B# = 3 THEN` used to report "Incomplete IF statement". The reference parses a
    single *relation* after `IF`, and a relation sits below `AND` and `OR` in the grammar chain,
    so the `AND` was never consumed and the `THEN` check found the wrong token.
    `akbasic_parse_if()` calls `akbasic_parser_expression()` now.

43. **Truth is nonzero, not "a boolean holding true".** `akbasic_value_is_truthy()` is what a
    branch tests. Commodore BASIC has no boolean type: a comparison yields -1 or 0, `AND` and
    `OR` *are* the bitwise operators, and `IF A THEN` is legal for any numeric A. The reference
    tests `boolvalue == -1` regardless of the value's type, so `IF A# = 1 OR B# = 2 THEN` — whose
    `OR` yields an integer — was silently always false, and so was `IF A# THEN`.

    The bitwise operators accept a truth value as an operand for the same reason: -1 is every
    bit set precisely so that `AND` and `OR` double as the logical pair. A string is still never
    true; a C128 raises a type mismatch there, which is a stricter answer this could adopt later.

### Deviations in the sprite verbs (group H)

32. **Labels are filed before the program runs, not as each `LABEL` executes.**
    `akbasic_runtime_scan_labels()` walks the stored source on every entry into
    `AKBASIC_MODE_RUN` and files every `LABEL <name>` it finds, textually, without parsing the
    lines around it. The reference resolves a label only once its `LABEL` statement has run, so
    `GOTO` and `GOSUB` reached backwards and never forwards.

    **This was not a nice-to-have.** An interrupt handler by definition sits on a line normal
    flow does not fall into, so `COLLISION 1, BUMPED` could not be made to work at all under the
    old rule — neither resolving at arm time nor at fire time helps when the `LABEL` line is
    never executed. Forward `GOTO` is the improvement that came with it, covered by
    `tests/language/statements/label_forward.bas`.

    `LABEL` still executes and still files itself, so a name that appears twice resolves to the
    last one in the source until one of them runs. The scan is textual on purpose: parsing every
    line up front would raise on lines the program would never have reached.

33. **Sprite coordinates are device pixels, not the VIC-II's 0–511 by 0–255.** `MOVSPR` takes
    the same coordinate space `DRAW` does. A C128's sprite coordinates are the raster's, offset
    so that (24, 50) is the top-left of the visible screen; reproducing that would give the
    language two coordinate systems and make `MOVSPR 1, 0, 0` put a sprite off-screen. Consistent
    with deviation 18, which is where "the same space `DRAW` uses" is defined — and it moved,
    so this one moved with it.

    **`SCALE` deliberately does not apply to sprites.** It is a graphics-verb transform and
    the sprite verbs do not call it, which was true before deviation 18 was rewritten and is
    worth writing down now that the two spaces can differ: with `SCALE 1, 319, 199` on, a
    `DRAW` at 160,100 and a `MOVSPR` to 160,100 land in different places. Arguably they
    should agree; not changed here because it is a decision about what `SCALE` means rather
    than a slip, and it wants its own commit.

34. **`MOVSPR`'s speed unit is ours.** BASIC 7.0 documents speed 0–15 with 15 fastest and says
    nothing about what a unit is worth. Here one unit is
    `AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND` (5) pixels per second, so speed 15 crosses the
    320-pixel screen in about four seconds. Paced off `akbasic_runtime_settime()` from
    `akbasic_runtime_step()`, exactly as the `PLAY` queue is, so a host that never sets a clock
    gets a still picture rather than a hang — same trade as deviation 20.

35. **`SPRSAV` takes an integer array where a C128 takes a string, and takes a file path as
    well.** Three source forms, one of which is the C128's:

    | Form | Source |
    |---|---|
    | `SPRSAV A$, n` | a region `SSHAPE` saved. The C128 documents `SPRSAV`'s string as *the `SSHAPE` data format at a fixed 24×21*, so sharing the shape pool is faithful rather than a shortcut |
    | `SPRSAV A#, n` | 63 elements of a DIMmed integer array — the DATA-driven path a type-in listing uses |
    | `SPRSAV "ship.png", n` | an image file |

    **The array form exists because a string here cannot hold a sprite.** A value carries a
    NUL-terminated `char[256]` (§1.2) and a pattern is 63 raw bytes including zeros, so the
    C128's own spelling is unrepresentable. `DIM P#(63)` is exactly the right size.

    **The file form is a deliberate addition, not a fallback.** A string source is a path unless
    it starts with `SHAPE:`, which is the prefix `SSHAPE` mints, so the two never collide. It
    resolves through `akgl_path_relative()` — the working directory first, then the directory the
    running program was loaded from — and loads through `akgl_spritesheet_initialize()`, which
    are the same two calls `akgl_sprite_load_json()` makes for a spritesheet. A sprite loaded
    this way takes **the image's own size** rather than being forced to 24×21; a modern PC has
    no reason to throw away art that is not sprite-shaped.

    `akbasic_runtime_set_source_path()` is what tells the interpreter where the program came
    from. Group F's disk verbs will want the same thing.

    **The reverse direction is refused.** `SPRSAV n, A$` on a C128 copies a sprite *out*; here
    that would mean writing an image file, which is a disk operation, and group F is
    unimplemented as a whole. Refused by name rather than half-built.

36. **`COLLISION` implements type 1 only, and types 2 and 3 are refused by name.**
    Sprite-to-background needs the whole render target read back and compared against each
    sprite every frame; a light pen has no meaning on a machine with no light pen. Both raise
    `AKBASIC_ERR_DEVICE` with the reason rather than being accepted and never firing.

    Collision is **bounding-box, not pixel**. A C128's VIC-II collides on set pixels, so two
    sprites whose boxes overlap but whose art does not are reported as colliding here and would
    not be there. Pixel-exact collision would mean keeping every sprite's unpacked bitmap and
    testing the overlap rectangle a pixel at a time; the box test is four comparisons.
    `akgl_collide_rectangles()` is deliberately not used — it has a documented
    corner-containment defect that misses a plus-shaped overlap, and nothing in libakgl calls it.

37. **A collision handler is entered between source lines, and must end in `RETURN`.**
    `akbasic_runtime_service_interrupts()` runs at the top of `akbasic_runtime_step()` and
    injects what amounts to a `GOSUB` the program did not write. Between lines is the only safe
    place: a handler entered mid-statement would have to return into the middle of a line and
    the parser keeps no state that could resume there. That is the same granularity block
    skipping already works at (§1.6).

    An interrupt does not interrupt an interrupt. A collision that is still true while its own
    handler runs would otherwise re-enter on the next line until the environment pool was gone.
    The event is not lost — it is taken as soon as the handler's `RETURN` lands.

38. **Sprite priority is recorded and not honoured.** `SPRITE n,,,1` sets the bit, `RSPRITE`
    reads it back, and nothing draws differently. A C128 draws a low-priority sprite behind the
    bitmap plane; here the text layer, the drawing surface and the sprites share one render
    target and sprites are composited on top of it every frame, so there is nothing to go
    behind. Honouring it would mean a separate composited surface per plane — which is the same
    piece of work deviation 19 already needs.

39. **Multicolour mode is recorded and not honoured.** Same shape as priority: `SPRITE`'s
    seventh argument and `SPRCOLOR`'s two registers are kept, `RSPRITE` and `RSPCOLOR` read them
    back, and no sprite is drawn in multicolour. Multicolour packs *two* bitmap bits per pixel
    and selects between the sprite's own colour and the two shared registers; every `SPRSAV`
    source form here carries one bit per pixel or a full-colour image, so there is no second bit
    to select with. The argument positions are kept honest for the day a multicolour pattern
    format exists.

40. **A sprite is a real `libakgl` actor, and it is drawn by a renderfunc of ours.** Each of the
    eight becomes a `SpriteSheet` + `Sprite` + `Character` + `Actor` registered in
    `AKGL_REGISTRY_ACTOR`, so an embedding game sees BASIC's sprites alongside its own (goal 3).
    But `akgl_actor_render()` is not what draws them. There were two reasons and **libakgl
    0.5.0 fixed one**: the default used to compute its destination *height* from the sprite's
    **width**, drawing a 24×21 Commodore sprite as a 24×24 square, and it now uses the height
    — libakgl defect 26, filed from here and closed there.

    **The reason that remains is the one that keeps this file.** An actor carries a single
    scalar `scale` applied to both axes, which cannot express `SPRITE`'s separate x- and
    y-expand bits, so `MOVSPR`'s twice-as-wide-same-height is unrepresentable. libakgl records
    it as open and names this interpreter as the caller that needs it; the fix is a design
    choice over there — `scale_x`/`scale_y` beside `scale`, or replacing `scale` and taking
    the ABI break while the major is still 0 — rather than a patch. Until then
    `src/sprite_akgl.c` installs its own `renderfunc`, which is libakgl's own extension point
    for exactly this.

    Worth keeping as a pattern: both defects were found by *using* the library for something
    it had not been used for, and both were filed rather than worked around silently. One came
    back fixed.

63. **Line numbers are optional in a loaded program, and a blank line is not a program line.**
    **This one moved a golden file.** A line with no number used to be filed under the loader's
    cursor unchanged — that is, on top of the line before it — so two unnumbered lines in a row
    silently lost the first, and a *blank* line erased whatever preceded it. The reference does
    the same, and `tests/language/arithmetic/integer.bas` is the proof: four `PRINT`
    statements, an expectation with three values, and a trailing blank line that erased
    `40 PRINT 4 - 2` before the program ran. The expectation is now `4 4 2 2` and
    `tests/language/README.md` records it.

    In its place: `akbasic_runtime_file_line()` (`src/runtime.c`) is the one implementation of
    the rule, shared by `akbasic_runtime_load()`, RUNSTREAM and `DLOAD`. A numbered line is
    filed under its number and moves the cursor; an unnumbered one takes the slot after it;
    blank lines are skipped by all three. A collision involving an assigned number is refused
    with `AKBASIC_ERR_BOUNDS` rather than overwriting. `akbasic_SourceLine` grew a `numbered`
    flag so deviation 64 can tell the two apart.

    **The prompt is deliberately untouched.** A line typed without a number is direct mode and
    runs now; that is the only thing separating program text from a statement at a REPL, and it
    is why this is a *loading* feature rather than a language-wide one. `AUTO` remains the way
    to enter program text without typing numbers.

    Consequence worth stating: an unnumbered program is capped at 9998 lines, the cap that
    already existed, and its assigned numbers are increment-1 — so `LIST` and `DSAVE` show a
    listing with no gaps to insert into. `RENUMBER` before `DSAVE` gives the gaps back, and
    marks every line numbered.

    **Not changed:** two lines that both carry the *same* written number still silently keep
    the last, as they always have. That is a separate decision with its own corpus risk and it
    is not this one.

64. **Branching by number to a line the program did not number is refused before it runs.**
    The other half of deviation 63, and the reason `akbasic_SourceLine` carries a flag rather
    than the loader just picking slots quietly. In a script written without line numbers
    `GOTO 100` finds the hundredth line and branches there: plausible, silent and wrong, which
    is the worst thing to hand somebody at run time.

    `akbasic_runtime_check_targets()` is a fourth prescan, run beside the label, `DATA` and
    `TYPE` ones on every entry into `AKBASIC_MODE_RUN` — the earliest the check can be made and
    the only place all four ways a program arrives pass through. It refuses with
    `AKBASIC_ERR_SYNTAX`, naming the line and saying what to do instead:

    ```text
    ? 1 : PARSE ERROR Line 1: branch to line 4, which the program did not number.
          Branch by LABEL, or RENUMBER first
    ```

    Two things are deliberately **not** refused. A target naming an *empty* line, for the same
    reason `RENUMBER` leaves one alone — `GOTO 9999` in a program with no line 9999 is already
    broken and inventing a rule about it would hide that. And a target in a fully numbered
    program, which is every program that existed before this, so nothing checked in changed.

    **It shares `RENUMBER`'s walk rather than repeating it.** `src/renumber.c` grew an
    `akbasic_TargetWalk` — a `self` pointer and a `visit` function — and `rewrite_line()` now
    takes one. `RENUMBER`'s visitor substitutes the number a line moved to; the check's
    substitutes the number unchanged and raises if it names an assigned line. One walk, so the
    two cannot disagree about what a branch target is, which they would have within a release.

    Worth knowing: the check points `environment->lineno` at the line it is walking, so the
    `? N :` prefix names the offending line. **The other three prescans do not**, so a
    malformed `TYPE` or a bad `DATA` item still reports whichever line the loader stopped on —
    usually the last line of the program. They work around it by putting `Line %d:` in the
    message text, which is why the message above says the number twice. Worth fixing in
    `akbasic_runtime_set_mode()`'s wrapper rather than in four places; not fixed here.

---

---

## 6. Reference defects — **fix them; fidelity is no longer a reason not to**

These are real bugs in `deps/basicinterpret`, reproduced faithfully during the port.

**The rule that governed this section is gone.** It used to read "port the behaviour first so
the golden suite passes and the port is provably faithful, then fix each one deliberately" —
and faithfulness was the reason several of these are still here. The Go implementation is
deprecated and will not be updated (§0.1), so there is no longer anything to be faithful *to*.
Each of these is now an ordinary defect, to be judged on whether the fix is right rather than on
whether it matches.

What has not changed is the working method: one fix per commit, with a test that asserts the
correct contract, and a golden file moved in the same commit if the fix changes output.
`AKBASIC_KNOWN_FAILING_TESTS` is empty now and is kept declared for the next defect that has to
be reproduced before it can be fixed.

1. ~~**`basicvariable.go:176`** — `toString()` tests `len(self.values) == 0` and then indexes
   `self.values[0]`.~~ **Never ported.** There is no `akbasic_variable_to_string`: rendering a
   value is `akbasic_value_to_string()`, which switches on the value type and has no count test
   to invert. The buggy function had no counterpart to reproduce.
2. ~~**`basicvariable.go:108`** — `setBoolean` builds a value with `valuetype: TYPE_STRING`.~~
   **Fixed in the port.** `akbasic_value_set_bool()` sets `AKBASIC_TYPE_BOOLEAN`, and
   `tests/value_compare.c` reads a boolean back through every comparison operator.
3. ~~**`basicenvironment.go:157`** — `stopWaiting(command)` ignores `command`.~~ **Fixed in
   the port.** `akbasic_environment_stop_waiting()` walks the scope chain and clears the first
   environment actually waiting for *that* verb, so an inner block can no longer clear an outer
   block's wait. A miss is tolerated rather than raised, which is the one part of the suggested
   fix not taken: `EXIT` and the end of a `DEF` body both call it speculatively.
4. ~~**`basicvalue.go:181`** — `mathPlus` mutates `self` in place when `self.mutable` is
   true.~~ **Fixed.** It clones like every other operator now, so `A# + 1` cannot modify `A#`.

   The gate on this item was `FOR`/`NEXT` coverage, because `NEXT`'s increment *relied* on the
   mutation to advance the counter. `tests/for_next.c` is that coverage -- `STEP`, a negative
   step, a float counter, a body that assigns to the counter, `EXIT`, and nesting, none of which
   the golden corpus reaches -- and `akbasic_cmd_next()` now writes the incremented value back
   with `akbasic_variable_set_subscript()`. Removing that write-back does not fail the suite, it
   *hangs* it: the counter stops advancing and the loop never ends.
5. ~~**`basicvalue.go:191` and siblings** — every binary operator adds both of the right-hand
   operand's numeric fields.~~ **Fixed.** `rval_as_int()` and `rval_as_float()` select on the
   operand's type instead of summing `intval + (int64_t)floatval`.

   It was filed as a landmine with no consequence today, and that is why it needed a test
   written against the value API rather than against the language: no BASIC program can build a
   value carrying both fields, and the old code passes every other test in the tree.
   `tests/value_arithmetic.c` constructs one directly.
6. ~~**`basicvalue.go`, all comparisons** — `dest` is cloned from `self`, so the resulting
   boolean inherits `self.name`.~~ **Moot.** `BasicValue.name` was dropped as dead during the
   port along with `BasicEnvironment.update()`, its only reader — deviation 9 above. There is no
   name for a comparison result to inherit.
7. ~~**`basicruntime_commands.go:484`** — `CommandIF` dereferences `expr.right` after the loop
   guaranteed it is `nil`, so the "Malformed IF statement" check is unreachable.~~ **Fixed in
   the port.** `akbasic_parse_if()` matches the `THEN` token and compares its lexeme directly,
   so the check runs on every `IF`.
8. ~~**`basicruntime_commands.go:669`** — `CommandEXIT` pops the environment without
   `stopWaiting`.~~ **Fixed**, and then fixed again — see item 18, which is what clearing the
   wait left behind.
9. ~~**`basicruntime.go:149`** — `newVariable()` and `BasicRuntime.variables[MAX_VARIABLES]`
   are dead.~~ **Not ported as dead code.** The pair is live here and is the storage model:
   `akbasic_environment_create()` takes a slot from `akbasic_runtime_new_variable()`, because a
   pool is what replaced Go's per-environment map (§1.3). The reference's versions were dead;
   these are not.
10. ~~**`basicgrammar.go:224`** — `newLiteralInt` selects base 8 whenever the lexeme starts
    with `0`.~~ **Fixed.** Base 10 unless prefixed `0x`; a leading zero in a listing is padding,
    not a radix. `PRINT 010` prints 10 and `PRINT 08` parses. Nothing in the upstream corpus
    had a leading-zero literal, which is why nothing there noticed. Regression tests in
    `tests/grammar_leaves.c` and `tests/language/numeric/octal_literal.bas`, which was rewritten
    from pinning the defect to pinning the fix.
11. ~~**`basicruntime.go:121` / `basicparser_commands.go:124`** — environments are never
    freed.~~ **Fixed in the port.** They come from a fixed pool and
    `akbasic_runtime_prev_environment()` releases each one — deviation 3 above. An unreleased
    scope shows up as pool exhaustion rather than as unbounded growth, which is how item 18
    announced itself.
12. ~~**`basicparser.go:329` — `subtraction()` returns after one operator where `addition()`
    loops.**~~ **Fixed.** `1 - 2 - 3` computed `1 - 2` and abandoned the rest of the line; the
    remaining `- 3` was left in the token stream, picked up by the statement loop as a second
    statement, evaluated and discarded. A wrong answer, silently, which made this the
    highest-impact item on the list. `exponent()` has the identical shape — a `for` loop with a
    `return` inside it, in both the reference and the port — so `2 ^ 3 ^ 2` had it too. Both
    now fold left, which is what a C128 does with a run of same-precedence operators.
    Regression tests in `tests/parser_expressions.c`.

    Worth recording for whoever reads the reference next: `exponent()` there is
    `for self.match(CARAT) { ... return expr, nil }` (`basicparser.go:519-539`), so the loop is
    a loop in shape only. `subtraction()` is the same at `:357-377`. Neither is a
    transcription slip in the port.

13. ~~**`basicparser.go:565` — a unary-minus argument inflates a function's arity count.**~~
    **Fixed**, and the fix is structural rather than a smarter counter. The counter walked
    `.right`, which is where a unary leaf keeps its operand *and* where an argument list chains
    its arguments; the two meanings were indistinguishable, so `ABS(-9)` counted as two
    arguments and was refused with "function ABS takes 1 arguments, received 2". No builtin
    could be called with a negative literal at all. The corpus hid it — `sgn.bas` assigns `-1`
    to a variable first.

    **A unary leaf's operand now hangs off `.left`.** A unary leaf had no other use for that
    field, so the two meanings separate for the cost of one nobody was using — and counting
    then needs no special case at all. That also fixes the half of the defect the original
    entry did not mention: chaining the *next* argument through `.right` overwrote the operand,
    so `MOD(-7, 3)` destroyed its own first argument. Three readers moved (`src/runtime.c`,
    `src/grammar.c`'s renderer, `src/runtime_commands.c`'s `LIST` range parser) and that is the
    whole change. Regression tests in `tests/runtime_evaluate.c`, both halves.

    **The same collision remains for array subscripts**, which is why §4 still lists
    "array references in parameter lists" as outstanding: an identifier keeps its subscript
    list on `.right` too. The same move — onto `.left` — is very likely the fix, and it is not
    made here because it wants its own commit and its own tests.

14. ~~**`basicscanner.go:272` — `matchNextChar` returns without setting a token type when it
    cannot peek past the end of the line.**~~ **Fixed.** A comparison operator in the final
    column was silently dropped: `A# =` produced one token, not two, and the parse error that
    followed pointed somewhere else entirely. `falsetype` is now set before returning on the
    peek failure. Regression tests in `tests/scanner_tokens.c`, for `=`, `<` and `>`.

15. ~~**`basicscanner.go:318` — hex literals do not survive the scanner.**~~ **Fixed.**
    `matchNumber` let `x` through but not the hex digits after it, so `0xff` lexed as `0x`
    followed by an identifier `ff`, the base-16 branch in `newLiteralInt` was unreachable, and
    the README's hex support did not exist. Once the `x` is seen the run continues over hex
    digits. Accepted anywhere in a number run rather than only after a leading `0`, which is
    the reference's rule and is worth keeping: it is what makes `1x2` one malformed token that
    the line-number conversion diagnoses by name. Regression tests in `tests/scanner_tokens.c`
    and `tests/language/numeric/octal_literal.bas`.

16. ~~**`basicscanner.go:349` — the "Reserved word in variable name" check never fires.**~~
    **Fixed**, and it is the item §0.1 unblocked. The keyword tables are now searched on the
    *base* name with any type suffix stripped, so `PRINT$`, `LEN#` and `GOTO%` are refused as
    variable names and the reference's own diagnostic stops being dead code. A name that merely
    contains a verb — `PRINTER$` — is still fine, which is what says the check compares the
    whole base name rather than a prefix. Regression tests in `tests/scanner_tokens.c`.

    **It cost one golden case, exactly as predicted, and the cost turned out to be nothing.**
    The reference's `examples/strreverse.bas` names a variable `INPUT$`; the variable is renamed
    to `SOURCE$` in `tests/language/examples/strreverse.bas`, the expectation is byte-for-byte unchanged — the program
    still prints `REVERSED: OLLEH` — and the case keeps every bit of its coverage. Recorded in
    `tests/language/README.md` records the corpus editing rule.

    This item sat parked because the corpus was the acceptance contract and lived in a submodule
    this repository could not edit. Both premises are gone: the corpus is checked in, and
    matching the Go implementation is no longer a goal. Worth keeping the history, because it is
    a clean example of a fix that was correct all along and blocked entirely by a constraint
    that has since expired.

### Ours, not the reference's

17. ~~**A host cannot reliably create a script variable while a script is suspended.**~~
    **Fixed.** `akbasic_runtime_global(obj, name, dest)` walks `obj->environment` to the root
    and finds or creates there unconditionally. `README.md` and `examples/hostvars.c` both point
    at it now, and `tests/hostvars.c` asserts it: a global created from inside a `FOR` body and
    from inside a `GOSUB`, still readable by the script after the body pops, plus the
    seed-before / read-after path that always worked.

    This one was not inherited — it fell out of §1.4's environment pool meeting §1.6's bounded
    `run()`, a combination the reference never had because its `run()` never returned. Both
    failure modes were silent: a variable created through `akbasic_environment_get()` landed in
    the loop's scope and died with it, so the script read it correctly inside the loop and got
    `0` immediately after; and reaching for the root explicitly returned `NULL` through `dest`
    **without raising**, because that function only auto-creates when
    `obj->runtime->environment == obj`.

    Three things settled while doing it, each worth keeping:

    - **The design question §6 named has an answer.** Suspended inside a *user function's*
      scope, the root is still right and the walk still gets there: a funcdef's environment is
      initialized with the caller's as its parent (`akbasic_runtime_user_function`), so the
      chain terminates at the same root as any other.
    - **`akbasic_environment_create()` is the new half**, and it searches *only* the scope it
      is given — no walk in either direction. Walking up would find an outer variable and put
      the value somewhere other than where the caller asked for it.
    - **`akbasic_environment_get()` is unchanged.** Declining to create in a non-active scope is
      the right behaviour for what the interpreter uses it for; it is only wrong as a *host*
      API, which is why the answer was a new entry point rather than a change to that one.
      `tests/hostvars.c` asserts the old behaviour too, so a later "tidy-up" has to argue with a
      test.

---


### Found while auditing this section against the C port

Writing the `FOR`/`NEXT` coverage that item 4 was gated on turned these up. The first two are
the reference's semantics reproduced faithfully; the third is the port's own.

18. ~~**`EXIT` on the first pass through a loop restarts the program.**~~ **Fixed.** `EXIT`
    jumped to `loopExitLine`, which is only ever written by `NEXT` — so an `EXIT` before any
    `NEXT` had run jumped to line 0. Each restart entered the `FOR` again and took another
    environment and another variable, so what a reader saw was "Maximum runtime variables
    reached" reported against the `FOR`, on a four-line program.

    It now skips forward to the `NEXT` using the same `waitingForCommand` machinery a
    zero-iteration body uses, and the `NEXT` pops on an `exiting` flag. Neither verb has to know
    where the loop ends. `tests/for_next.c`.

19. **A `FOR` body runs once with the overshot counter**, `FOR I = 1 TO 1` does not run its
    body at all, and the counter does not survive the loop. The first two compensate for a step
    of 1, which is why neither was noticed. Pinned by `tests/for_semantics.c` in
    `AKBASIC_KNOWN_FAILING_TESTS`; fixing it means deciding what to do with
    `forloopwaitingforcommand.bas`, which depends on the current behaviour. **Issue #5.**

### Found while fixing the DEF recursion hang

27. ~~**A call leaked one variable slot per parameter.**~~ **Fixed.**
    `akbasic_runtime_prev_environment()` released the scope but not the variables the scope
    created, so a subroutine with a local of its own exhausted the 128-slot pool after
    about 128 calls -- `Maximum runtime variables reached`, on a four-line program that
    does nothing unusual.

    **It was there for `GOSUB` all along**, measured on a build stashed back rather than
    assumed: `FOR I# = 1 TO 300 : GOSUB 100 : NEXT` where the subroutine assigns a local
    failed before this work and passes after it. Giving each `DEF` call its own scope
    (item 26) simply made it reachable a second way, which is how it was found.

    The release is safe because a scope's own table holds *only* what it created --
    `akbasic_environment_get()` walks up to find an outer variable and returns it without
    caching a reference. That is worth knowing before anyone adds caching there, and the
    loop says so. It is the variable *slot* that comes back, not its storage: the value
    pool never frees, so a pointer into a record `DIM`med in a scope stays sound after the
    scope is gone, which is a documented property of structures rather than an accident
    this could have taken away.

28. ~~**A function could not take a structure.**~~ **Done.**
    `DEF AREA(S@ AS RECT)` and `DEF POKEIT(P@ AS PTR TO RECT)` now work, and a bare
    `DEF F(S@)` is refused: `@` says "a structure" without saying which, so it does not
    state a contract the way `S$` does. Accepting it would mean checking fields at the
    call rather than at the declaration -- duck typing, and the hole naming the type
    closes. **The cost is that there are no generic functions**, one `AREA` cannot serve
    two types, and that is a real loss recorded rather than glossed.

    Passing is by value because a parameter is bound by assignment and assignment copies;
    a pointer parameter copies its reference. Neither is a special rule. A structure
    parameter needs its storage prepared before the copy, which is the one thing a
    primitive parameter does not -- a structure variable is a run of slots and there is
    nothing to copy into until the run exists.

    **`akbasic_value_is_truthy()` learned that a pointer is true when it points at
    something**, which had to come with it: without it there is no way to test for the end
    of a list at all, and comparing a pointer to 0 reads a numeric field it does not carry.
    A *structure* is deliberately given no truth value -- it always exists, so the question
    has no answer worth guessing at.

25. **A statement containing a failed multi-line `DEF` call still completes, and prints a junk
    value.** `akbasic_runtime_process_line_run()` swallows a script's error by design, so the
    loop inside `akbasic_runtime_user_function()` sees the run end normally. It is a decision
    about what a failed call *evaluates to*. **Issue #7.**

### Found while building structures

23. ~~**A host could not run one script twice.**~~ **Fixed.**
    `akbasic_runtime_start()` never rewound: `RUN` sets `nextline = 0` and clears
    `stopped`, and `start()` set neither. That cost nothing while a host started a script
    once, because `nextline` is already 0 the first time — and cost everything to a host
    running one script per enemy, where the second `start()` began past the end and
    silently did nothing at all. `start` means start now. Found by writing
    `examples/hoststruct.c`, which is exactly that shape.

24. ~~**The prescan wiped host-registered types.**~~ **Fixed.** It runs again on every
    `RUN` and used to reinitialise the whole table, so the first `RUN` unregistered
    everything the host had registered before loading — with no way for the host to know
    it had to register them again. It now drops what the *script* declared and keeps what
    the *host* registered, which works because host types are always a prefix and so every
    index a field already recorded keeps pointing at the same type.
    `tests/hoststruct.c` pins it.

26. ~~**A `DEF` call was not re-entrant, which cost a wrong answer and a hang.**~~
    **Fixed.** The function's environment was owned by the funcdef and re-initialised on
    every call, so a second call trampled the first:

    - **Two calls in one expression aliased one slot.** `DBL(10) + DBL(1)` was 4 rather
      than 22 -- both operands became the last call's answer, because the result was a
      pointer into the funcdef's own environment. Two *different* functions were fine,
      which is most of why it was invisible. Same severity class as §6 item 12, and the
      same shape: a silent wrong answer from a two-line program.
    - **Recursion never came back.** The recursive call re-initialised the environment the
      outer call was still using, so the loop waiting for control could not see it return.
      No error, no bound, no diagnostic -- the one place in this interpreter that looped
      forever instead of raising.

    A call takes an environment from the pool now, exactly as `GOSUB` does, and the result
    is copied into a caller-scope scratch before that environment goes back. `RETURN`
    parks its result on the *parent* rather than on the environment about to be released,
    so nothing reads a freed slot. Recursion depth answers to `AKBASIC_MAX_ENVIRONMENTS`
    like every other nesting, so too deep is `Environment pool exhausted` -- a diagnosis
    where there was none.

    `akbasic_FunctionDef.environment` is gone with it, as dead state.
    `tests/user_functions.c` and `tests/language/functions/recursion.bas`.

### Found while auditing `akbasic_value_clone()` for the structures work

22. ~~**The numeric operators' `else` was a catch-all, not a float branch.**~~ **Fixed.**
    `math_minus`, `math_multiply` and `math_divide` were `if ( INTEGER ) … else <treat as
    float>`, and that `else` read `floatval` from whatever it was handed. A truth value
    keeps its payload in `boolvalue` and leaves `floatval` zero, so a truth value on the
    *left* computed into a field nothing reads and kept its `BOOLEAN` type:

    ```
    (A# == 1) - 1      printed  true      should be -2
    (A# == 1) * 3      printed  true      should be -3
    (A# == 1) + 1      correctly refused
    ```

    Wrong in value and in type, and silent. `math_plus` escaped only because it enumerates
    its cases and ends in an error; the other three now share one `require_numeric()` guard
    so a type added later is refused by all of them at once instead of quietly taking the
    float branch in each.

    **The two operands have different rules, deliberately.** The left one picks the branch
    and must be a number. The right one is read through `rval_as_int()`, which handles a
    truth value on purpose — `5 - (A# == 1)` is 6, and that is the same property that lets
    `AND` and `OR` double as logical operators. Refusing it on the right would have broken
    every condition in the language, and `tests/language/numeric/truth_value_arithmetic.bas`
    pins both halves.

    **Found by asking what a structure operand would do**, since `AKBASIC_TYPE_STRUCT` would
    have taken exactly this path and read a `floatval` it does not carry. The structures
    work did not create this defect; it made an already-reachable one worth chasing. Same
    shape as item 5 and fixed the same way, with the assertions against the value API where
    the bad value can actually be constructed.

    Two stale comments went with it: `src/value.c`'s file header and a duplicate block above
    `rval_as_int()` both cited "TODO.md section 12", which does not exist — the defect list
    is §6 — and the duplicate described the summing behaviour item 5 had already removed.

### Found while writing the error-code appendix

21. **A dependency's status code reaches `ER#` where the interpreter has one of its own.**
    `VAL("XYZ")` reports `AKERR_VALUE` rather than `AKBASIC_ERR_VALUE`, and the errno-derived
    number is not portable across machines where 517 would have been. A decision about a
    boundary rather than a patch -- the disk verbs are the counter-case, since `ENOENT` out of
    `DOPEN` is more useful than a flat 519. **Issue #13.**

### Found while making line numbers optional

26. **`akbasic_environment_set_label()` files into the active scope, not the root**, so a
    prescanned label and a re-filed one behave differently. One condition, plus a test. Wants
    checking against deviation 32 first. **Issue #9.**

27. **Three of the four prescans report against the wrong line**, and work around it by
    putting `Line %d:` in the message text -- so the message names the number twice and the
    prefix is wrong. `akbasic_runtime_check_targets()` shows the fix. **Issue #10.**

28. **Two lines carrying the same written line number still silently keep the last.** The one
    collision that can appear in an existing program, so it carries corpus risk the other three
    did not. **Issue #11.**

29. **`akbasic_renumber()` keeps two 9999-entry `static` locals**, so `RENUMBER` and the target
    prescan are not reentrant across two runtimes in one process. Hanging them off the runtime
    costs ~2.5MB per interpreter for something one verb uses; both answers are defensible.
    **Issue #12.**

### Mutation-testing gaps in the files the game work touched

Recorded the way §8's CI note records `src/audio_tables.c`: a real test gap rather than a
reason to avoid the file.

34. **`src/runtime.c` has never had a complete mutation run** -- the one attempted was cut off
    at 551 of 997 mutants. What it covered has been read rather than filed unopened, and three
    of the four skipped-block survivors are equivalent mutants. **Issue #24.**

### Found while writing a game against the interpreter

A complete Breakout, sprites and text grid and keyboard, running for minutes at a time. Nothing
in either corpus runs for minutes, which is why the first of these had never been seen.

30. ~~**A variable created inside a scope costs pool slots that never come back, so a
    long-running program has a hard budget of 4096 name creations.**~~ **Done** — a scalar now
    lives in the variable rather than in the pool (`akbasic_Variable::inlinevalue`,
    `src/variable.c`), so a `GOSUB` local, a `FOR` counter and a `DEF` parameter all cost
    nothing at all. Twenty thousand scoped creations where four thousand used to be the whole
    run. `tests/value_pool.c` is the coverage, and it asserts the mechanism as well as the
    consequence — a later change that moved arrays inline too would pass every behavioural
    case and break the pointer guarantee. **An array created in a scope still leaks**, which is
    deliberate and is the same statement `akbasic_ValuePool` has always made: a pointer into a
    record outlives the scope that DIMmed it. The record below is what was found and why.

    **Also fixed with it:** `SWAP` exchanges whole variable records, so the `values` pointer
    that came over named the other variable's inline slot and SWAP silently did nothing. Caught
    by `tests/language/housekeeping/verbs.bas`, which is the golden corpus earning its keep.

    The original report: `akbasic_ValuePool` is a bump allocator
    on purpose (`include/akbasic/value.h:30`), and the reason given is that "nothing in BASIC
    destroys a variable". **Scope exit destroys variables.**
    `akbasic_runtime_prev_environment()` (`src/runtime.c:121`) marks a popped environment's
    variables `used = false`; `akbasic_runtime_new_variable()` (`src/runtime.c:31`) `memset`s
    the slot it hands back, clearing `values` and `valuecount`; and `akbasic_variable_init()`
    (`src/variable.c:98`) therefore takes *fresh* slots for a variable whose old ones are still
    counted against `obj->next`. Nothing decrements it, so every `GOSUB` that creates a local
    leaks the local's slots for the life of the run.

    Reduced, and it fails on both builds:

    ```basic
    X# = 0
    FOR T# = 1 TO 6000
      GOSUB SUBA
    NEXT T#
    PRINT "OK " + X#
    END

    LABEL SUBA
    LOC# = 1
    X# = X# + LOC#
    RETURN
    ```

    `? 110 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots`,
    at `LOC# = 1`, on the 4091st call -- 4096 less the handful of names the program itself
    declared. Both builds, same line, same count. Declare `LOC#` alongside `X#` and the same
    6000 calls cost nothing at all. A `FOR` counter behaves the same way: name one that does
    not already exist and every entry to the loop spends a slot.

    **What it costs a program:** a game loop that creates one name per tick is dead in half a
    minute, and the error names the line that happened to be unlucky rather than the line that
    caused it. Breakout ran twenty-five seconds before this was understood. The workaround is
    real and not onerous -- declare every name, loop counters included, at the top -- but it
    has to be *known*, and right now the only way to learn it is to hit it.

    Three ways out, in increasing order of work. Reclaim on scope exit: give the pool a mark
    and release, take the mark when an environment is pushed and rewind to it when it pops,
    which is exactly the discipline the environments themselves already have and is why they
    are a pool in the first place. Or keep the slice on the recycled variable: stop the
    `memset` in `new_variable()` from clearing `values`/`valuecount` and let
    `variable_init()`'s existing "reuse the slice when it is already big enough" branch do the
    work -- much smaller, and it fixes the common case of the same-sized scalar being recreated
    over and over. Or, at minimum, say so in the manual and in `value.h`'s comment, which
    currently states the opposite.

    Whichever is chosen, the test is a CTest program that enters and leaves a scope creating a
    variable tens of thousands of times and returns zero -- and it wants to exist even for the
    documentation-only outcome, pinned at whatever the real budget turns out to be. Item 33
    has to be settled with it: while the pool is dry the failure cannot be trapped, so a
    program cannot even report this one against itself.

31. ~~**`WINDOW` cannot be reached in the standalone AKGL build.**~~ **Done** — `tee_window()`
    (`src/sink_tee.c`) forwards to whichever half offers one, offered only when one does,
    with the case in `tests/sink_tee.c` beside the `moveto` one. `WINDOW 0, 0, 20, 4` now
    works in the windowed build and still refuses by name without a grid.

    **`akbasic_sink_init_tee()` and `akbasic_sink_init_stdio()` also now clear `moveto` and
    `window` rather than leaving them alone.** Writing the test found it: both are
    conditional in the tee, so a caller with a sink on the stack got whatever was in that
    memory, and `CHAR` decides whether it can move a cursor by testing that pointer for
    NULL. An initializer that leaves a field alone is not an initializer.

    The grid-size half of this item is closed separately; see item 31a below.

    The original report: `akbasic_sink_init_tee()`
    (`src/sink_tee.c:143`) wires `write`, `writeln`, `readline`, `clear` and -- conditionally
    -- `moveto`, and never sets `window`. The standalone frontend runs the interpreter against
    the tee (`src/frontend_akgl.c:215`), so the fully implemented `sink_window()` underneath it
    (`src/sink_akgl.c:413`) is unreachable: `WINDOW 0, 0, 10, 10` in the windowed build answers
    `? RUNTIME ERROR WINDOW needs a text device with a character grid, and this one has none`.
    The verb is only usable by a host that hands the runtime the akgl sink directly.

    It looks like an oversight rather than a decision, because `moveto` right above it *is*
    forwarded, and with the same reasoning that would apply here. The fix is a `tee_window()`
    that forwards to whichever half offers one, offered only when one does, and a case in
    `tests/sink_tee.c` beside the `moveto` one. §3's "Still missing from the sink" closes by
    calling `WINDOW` "ordinary work here rather than anything blocked", which is now half true:
    the work was done and then not plumbed.

    ~~Worth deciding at the same time: **a program still has no way to ask how big the text
    grid is.**~~ **Done, and with BASIC 7.0's own function rather than more `RGR` fields.**
    `RWINDOW(0)` is the current text window's rows and `RWINDOW(1)` its columns -- the C128
    has exactly this function and this interpreter had never implemented it. `RWINDOW(2)` is
    refused by name: it reports a 40 or 80 column screen mode and there is no such mode here,
    so answering 0 would be a plausible lie.

    The cell size in pixels is `RGR(3)` and `RGR(4)`, beside the surface's own dimensions,
    because that is a fact about the surface -- and because `RWINDOW` reports the *window*,
    which makes dividing `RGR(1)` by a column count wrong the moment a program calls
    `WINDOW`. Both read a new optional `grid` entry point on `akbasic_TextSink`, implemented
    by the akgl sink and forwarded by the tee, NULL everywhere else so both refuse by name.

    `RGR(3)` on the standalone build now answers 16 -- the number the Breakout listing had
    measured by hand and written out as a constant.

    The original note: `RGR(1)` and `RGR(2)` give the window in pixels, and nothing gives columns, rows or
    the cell size, so anything that wants to place a character *and* a sprite at the same spot
    has to hardcode a cell size measured against the bundled font -- which is what the Breakout
    in `examples/breakout/characters` does, and it is the one thing in that listing that will break
    on a different font or window. Two more `RGR()` indices would answer it.

32. ~~**A character written past a short row's terminator is stored where nothing will
    render it.**~~ **Done** -- `putchar_at()` (`src/sink_akgl.c`) fills the gap with spaces
    before storing, so a positioned write always lands.

    **Padded in `putchar_at()` rather than in `sink_moveto()`**, which is the friendlier of
    the two options this entry offered and avoids its own objection: `moveto` stays
    read-only, and a row is only ever padded when a character actually arrives. The pad
    fills from the terminator rather than replacing it, because the buffer is not cleared
    between rows -- replacing only the terminator would expose the tail of whatever longer
    row used to be there, and `tests/akgl_backends.c` asserts exactly that case.

    The documented truncation on the way back is unaffected and is asserted beside it.

    The original report: A grid row is a NUL-terminated string and `putchar_at()` (`src/sink_akgl.c:89`)
    writes the character, advances, and terminates -- so `CHAR 1, 40, 1, "#"` on an empty row
    stores `#` at column 40 with `text[1][0]` still `'\0'`, and `sink_akgl_render()` draws
    nothing at all. The same call on a row already 50 characters long draws the `#` and erases
    columns 41 onward, which is the documented truncation and is fine.

    It is the *silent nothing* that is the trap: the write succeeds, the cursor moves, stdout's
    mirror shows the character, and the window stays blank. Either pad the gap with spaces on
    a `moveto` past the terminator, so a positioned write always lands, or say plainly beside
    `CHAR` in `docs/11-verb-reference.md` and deviation 49 that a row is written from column 0
    or not at all. Padding is the friendlier of the two and costs one loop in `sink_moveto()`;
    the argument against it is that it makes `moveto` write to the grid, which it currently
    does not.

33. ~~**A `TRAP` handler cannot be entered once the value pool is dry, and the error is
    dropped in silence.**~~ **Done**, in two halves. `akbasic_runtime_reserve_globals()`
    (`src/runtime.c`) creates `ER#` and `EL#` at runtime init and again after `CLR`, so the
    dispatch never allocates -- there is nothing left for it to fail at. And
    `report_and_reraise()` no longer lets a failure *inside* reporting replace the error the
    program actually made: the secondary one is logged where a developer will see it and the
    original is re-raised.

    **The reduction below no longer reproduces, and not because of this fix.** §6 item 30
    made a scalar free, so the value pool can no longer be emptied by creating names and
    `ER#`/`EL#` could be created after all. The defect was still there, reachable through the
    *variable* table instead: 124 names and a `TRAP` armed, and the handler was silently
    skipped while the program carried on. That is what `tests/trap_verbs.c` now pins, along
    with the invariant itself -- both globals present before a program runs.

    The original report: `akbasic_trap_set_error_variables()` (`src/runtime_trap.c:36`) sets `ER#`
    and `EL#` through `akbasic_runtime_global()` (`:51` and `:53`), which *creates* them if
    the program never named them. Creating them needs pool slots. So the one error most
    likely to leave the pool empty -- item 30's -- is the one error whose handler cannot be
    reached, and because the dispatch fails inside the error path rather than raising, **the
    program keeps running with the failed statement's effect quietly missing**.

    It is easy to see, and it is much worse than an abort:

    ```basic
    X# = 0
    TRAP RANOUT
    FOR T# = 1 TO 6000
      GOSUB SUBA
    NEXT T#
    PRINT "OK " + X#
    END

    LABEL SUBA
    LOC# = 1
    X# = X# + LOC#
    RETURN

    LABEL RANOUT
    PRINT "DIED AT X " + X#
    QUIT
    ```

    prints `OK 4092`. The handler never ran, nothing was reported, and `X#` is 1908 short of
    the 6000 the loop counted -- a wrong answer delivered as a right one. Add `ER# = 0` and
    `EL# = 0` at the top, so the dispatch has nothing left to create, and the same program
    prints `DIED AT X 4090` from its handler as it should.

    Two things want fixing and they are separable. The dispatch should not allocate: create
    `ER#` and `EL#` at runtime init, where there is always room, rather than at the moment the
    program is in trouble -- they are reserved names and §1's other reserved globals are
    already there. And a failure *inside* the trap dispatch should not be swallowed; if the
    handler cannot be entered, the original error is the thing to report, and reporting it is
    what the untrapped path already does correctly.

    `AKBASIC_ERR_BOUNDS` from the pool is documented as "bounded and diagnosable, which is the
    point" (`include/akbasic/value.h:34`). With a `TRAP` armed it is currently neither.

### Found while rewriting the game tutorials

Chapters 17 and 18 were rewritten to teach a beginner rather than to explain a finished
listing, which meant writing every rule down as a rule instead of as a comment beside the
code that dodges it. Three of them turned out to have no entry anywhere. The first two are
the same shape as item 12 above — a parser function that consumes one operator where it
should loop — and the third is why both games are built out of `LABEL` and `GOTO`.

35. **An expression mixing `-` and `+` at the same level is evaluated right to left.**
    `10 - 3 + 2` is 5 where a C128 gives 9, silently. The cause is the grammar's shape: two
    additive precedence levels where there should be one. **Issue #1.**

36. **Only one unparenthesised `AND` or `OR` is matched per expression**, so a three-term
    condition is refused. `logicalandor()` is an `if` where `subtraction()` is a `while` -- the
    same defect as item 12, on the operator pair item 12 did not reach. **Issue #2.** Worth
    doing in the same sitting as item 35.

37. **A `GOTO` that leaves a `FOR` or a `DO` leaks the loop's scope**, so a program using the
    pattern as its main loop dies on the thirty-third pass -- and the error names the loop being
    entered rather than the `GOTO` that did the damage. Both Breakout listings are built out of
    `LABEL`/`GOTO` because of it. **Issue #3.**

### Found while integrating libakgl's collision subsystem

`spr_collisions()` now answers through `akgl_collision_test()` and every sprite carries an
`akgl_CollisionShape` and a pooled proxy. Three things were deliberately left for later, and
each is here so the reasoning does not have to be reconstructed.

38. **Finish moving `COLLISION` and `BUMP` onto the subsystem.** **Half done** -- the contact
    is no longer discarded and `RCOLLISION(n, f)` reports all of it, with
    `tests/akgl_backends.c::test_contact_geometry` pinning the normal's *sign* for both parties.
    What stands is the pairing seam, which is all-against-all over eight slots and is the right
    shape at eight: 54.9 ns a scan against a 1.20 ms frame. **The threshold is the thing to
    record, not the work** -- anybody raising `AKBASIC_MAX_SPRITES` should re-run
    `tests/collision_perf.c` first. **Issue #14.**

39. ~~**Convert the breakout listings and chapters 17 and 18 to the collision verbs.**~~
    **Done.** Both games and both tutorials now register their bricks with `SOLID`, arm
    `COLLISION 2` and read the contact back with `RCOLLISION`.

    What went: `BALLBRICKS` (29 lines) and `TESTCELL` (28) in the artwork game, and `HITTEST`
    (14) plus its pixel-to-cell arithmetic in the characters game — replaced by a handler that
    asks which box was hit and which axis to reverse. The hand-written minimum-translation-axis
    is `RCOLLISION(n, 7)`. `BUILDLIVE`/`BUILDROW`/`STAMPROW` went with the full-field redraw
    they existed to make cheap, and the artwork game gave six orphaned declarations back with
    them: the label table went from 61 of 64 to 57.

    Two things are worth keeping from doing it, because neither is obvious and both cost an
    evening:

    - **`BUMP`'s mask is by sprite number, not by anything the handler is about.** The artwork
      game's balls are sprites 5, 6 and 7, so their bits are 16, 32 and 64. A handler that
      tests 1, 2 and 4 runs, matches nothing and returns, with no error and nothing to see.
    - **A bare `SOLID` must run *before* the level is laid out, not after.** Put after the fill
      loop it retires all sixty rectangles that were just registered, and the only symptom is a
      ball that passes through the wall.

    Chapter 18 also lost a rule it depended on and no longer stated: a `FOR` with equal bounds
    does not run its body (§6 item 19, now issue #5), which is why every list in the chapter has its one-item
    case written out beside the loop. It is now stated in Step 7 where it is first used, rather
    than cross-referenced to a step that the rewrite deleted.

40. **`akbasic_runtime_call_function()`: call a BASIC function from C with bound values.**

    A verb cannot currently take a BASIC function as an argument, and it came up while designing
    the collision query — a callback per overlapping shape is one obvious shape for that verb.
    It was rejected for the query on the use case rather than on feasibility: the motivating
    program wants one answer rather than many, a loop is the idiomatic form in this dialect, and
    the language already has a callback shaped the way a BASIC programmer expects — an interrupt
    handler taking a label, which `COLLISION` uses.

    **The finding worth keeping is that the hard half already exists.** `src/runtime.c:1031` is
    the multi-line `DEF` path:

    ```c
    callenv->gosubReturnLine = callenv->lineno + 1;
    callenv->nextline = fndef->lineno;
    while ( obj->environment != targetenv && obj->mode == AKBASIC_MODE_RUN ) {
        PASS(errctx, akbasic_runtime_process_line_run(obj));
    }
    ```

    The interpreter already re-enters its own line loop synchronously from inside expression
    evaluation, with a scope from the pool, re-entrant since §6 item 26, and with recursion
    depth answering to `AKBASIC_MAX_ENVIRONMENTS` as a diagnosis rather than a hang.

    ~~What is missing is narrow.~~ **Done**, as `akbasic_runtime_call_function()`: the
    argument binding is split out of the call-site handling, so the AST path evaluates leaves
    and then calls the same code a verb can call with values it already has. Bounded by item
    41 -- a multi-line body only runs while a program is running, which is the case a verb is
    in and not the case a host with a stopped runtime is in.

    What is still not done is the *language* half: no verb takes a function yet, and nothing
    resolves a bare word to a function rather than a label. That wants
    `akbasic_environment_get_function()` and the precedent of `COLLISION 1, BUMPED` taking a
    bare word by name (`src/runtime_sprite.c`) -- and a verb that actually needs it, rather
    than being built on speculation.

    **One hazard, if it is ever wired to a collision query.** Do not pass the BASIC function
    through as an `akgl_CollisionVisitFunc`: it would run inside the partitioner's cell-chain
    walk, and the callback a brick loop wants removes the brick it was just told about, which is
    `partitioner.remove()` rewriting the chain the walk holds a cursor into. Collect the
    candidates during the walk, close the walk, then dispatch.

41. **A multi-line `DEF` called outside a running program does not run its body**, and says
    nothing about it -- the loop is guarded on `mode == AKBASIC_MODE_RUN`, and the whole suite
    drives every case through `run_program`. **The obvious fix is wrong and was tried**: widening
    the guard makes the interpreter hang rather than answer wrongly. **Issue #8.**

42. ~~**A name that merely contains a branch verb was treated as a branch.**~~ **Fixed.** The
    target prescan walks a line character by character looking for `GOTO`, `GOSUB`,
    `COLLISION` and the rest, and checked only the character *after* a match:

    ```basic
    T# = RCOLLISION(1, 1)
    ```

    ```output
    ? 2 : PARSE ERROR Line 2: branch to line 1, which the program did not number. Branch by LABEL, or RENUMBER first

    ```

    `COLLISION` was found at `RCOLLISION`'s second character, the `(1,` after it read as a
    handler line number, and the program was refused -- naming a line that contains no branch.
    `src/renumber.c` now requires a non-alphanumeric character on *both* sides. Its own
    comment was already right that the trailing check protects `GOTOX#`; nothing protected
    `XGOTO#`, and `RCOLLISION` is the first name in the language to reach it.
    `tests/unnumbered.c` covers all three shapes.

## 7. Filing gaps against `libakgl`

When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in
`libakgl`'s issue tracker: what the BASIC verb requires, what
the `akgl_*` entry point should look like, and what tests would cover it. Growing `libakgl` to
serve the interpreter is a wanted outcome, not a detour.

**All ten gaps filed so far are closed upstream**, as of `libakgl` 0.3.0. Nothing here is
blocked on `libakgl`, and this repository carries no `libakgl` workarounds at all — the four
§3 used to list are deleted.

| Was blocking | Closed by |
|---|---|
| Text measurement (§3, the akgl sink) | `akgl_text_measure`, `akgl_text_measure_wrapped` |
| Immediate-mode drawing (§4 group G) | `akgl_draw_point`/`_line`/`_rect`/`_filled_rect`/`_circle`/`_flood_fill`/`_copy_region`/`_paste_region` |
| Audio (§4 group I) | `akgl_audio_init`/`_tone`/`_envelope`/`_waveform`/`_volume`/`_stop`/`_voice_active`/`_mix` |
| Console input (§4 group E) | `akgl_controller_poll_key`, `akgl_controller_flush_keys` |
| Vendored dependencies when embedded | added unconditionally, 0.3.0 |
| `controller.h` not self-contained | includes `<akgl/actor.h>`, 0.3.0 |
| Binding a 2D backend to an existing renderer | `akgl_render_bind2d()`, 0.3.0 |
| `akgl_text_rendertextat()` on an unbound backend | checked, 0.3.0 |
| `SOUND`'s frequency sweep (§5 item 21) | `akgl_audio_sweep()`, 0.3.0 |
| The line editor could not see Shift (§3) | `akgl_Keystroke` + `akgl_controller_poll_keystroke()`, 0.3.0 |

Three notes for whoever files the next one. The draw calls take an `akgl_RenderBackend *` as
their first argument rather than reaching for a global renderer, which fits the rule that the
interpreter draws through whatever renderer the host already initialized. The audio API is a
synthesised-voice one — `akgl_audio_tone(voice, hz, ms)` with a separate ADSR envelope — which
is the shape `PLAY` and `ENVELOPE` need, rather than the sample playback SDL3_mixer would have
given. And `akgl_controller_poll_key()` survives alongside the keystroke form on purpose: `GET`
and `GETKEY` still use it, because a script asking "was the up arrow pressed" wants a keycode
and would get the empty string from the other one.

**The UI group left four things on the floor, and none of them is filed.** Group K —
`MENU`, `GETMENU`, `RMENU`, `DIALOG`, `HUD` and `UISTYLE`, over `akgl_ui` — deliberately
covers libakgl's three widget helpers and nothing else. What a program cannot have, and
what closing each would cost:

1. **A top-centre or bottom-centre `HUD` anchor.** `akgl_UiAnchor` has the four corners
   and dead centre, so `HUD` has exactly those five. Two appended enum values and two arms in
   `akgl_ui_label()`'s switch, using constants the dialog helper in the same file already uses.
   **Filed upstream as `libakgl` #79** — this entry used to say "not filed, because changing a
   dependency submodule is that repository's decision", which was the right instinct about
   *changing* it and the wrong outcome for *reporting* it.
2. **Per-widget styles.** `UISTYLE` sets one look shared by everything, because
   `akgl_UiStyle` is per-call and threading an optional style number through three verbs'
   argument shapes buys less than it costs. Nothing upstream is missing.
3. **Anything of clay beyond the three helpers** — containers, scrolling, images in
   panels, custom elements, hit-testing. Reachable from C against `akgl/ui.h` in the same
   window; deliberately not reachable from BASIC.
4. **A dialog the player can dismiss.** libakgl has no dismiss contract — a declarative
   frame is the visibility flag — so `DIALOG` with no argument is the only way down. **Filed
   upstream as `libakgl` #80.**

One cost carried rather than fixed: libakgl re-rasterizes every line of widget text every
frame, which is its own open `TODO.md` item. A `HUD` rebuilt every frame is the expensive
case. Recorded in `docs/19-user-interface.md` so a program author is not surprised by it.

**`FILTER` is the one verb still refused**, and it is not filed as a gap because upstream said
plainly what it would take: `akgl_audio_*` synthesises raw waveforms and mixes them, there is no
filter stage to configure, and SDL3 supplies no primitive to build one from. Until an
`akgl_audio_filter()` exists, `FILTER` is accepted by the parser and refused at execution with
`AKBASIC_ERR_DEVICE` rather than silently ignored — a program that asks for a low-pass and gets
an unfiltered square wave has been lied to.

**One thing went the other way**, and it is worth recording because it is the only defect this
project has ever sent upstream in a release it also consumed. 0.3.0's vendored-dependency fix
also made `libakgl`'s `add_test()` shadow unconditional. CMake chains command overrides exactly
one level deep, so two projects in one tree cannot both shadow it: the second rebinds
`_add_test` to the first and the builtin becomes unreachable to everyone, and this repository's
own test registrations recursed until CMake stopped at depth 1000. `akbasic` is the only
consumer that embeds `libakgl` next to other projects, so it is the only place this could show
up. Fixed upstream by restoring the top-level guard and filed as `libakgl` defect 27.

Building against any of it needs `-DAKBASIC_WITH_AKGL=ON`, which pulls in `libakgl`'s own
submodules (SDL and friends). Those are not initialized in a default clone; `git submodule
update --init --recursive` gets them.

---

## 8. Status

**The port is done.** The C interpreter passes the language corpus and passes clean
under ASan and UBSan. It reproduced that corpus byte for byte until §0.1 retired the
requirement; two cases have diverged on purpose since, and
`tests/language/README.md` lists them.

| Gate | Result |
|---|---|
| `ctest` | 112/112 — 41 reference golden cases, 24 local ones, 41 unit tests, 1 known-failing, 3 embedding examples, and `docs_examples` |
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 112/112 headless, with `akgl_typing` skipping itself. The same set minus the four `no_device` cases the SDL driver contradicts, plus `akgl_backends`, `akgl_frontend`, `docs_screenshots` and `akgl_typing` — the last of which is the skip, and the `akgl_build` CI job is where it skips |
| `docs_examples` | Every fenced block in `README.md`, `MAINTENANCE.md` and `docs/` executed and byte-compared: 71 programs, 9 transcripts, 79 output comparisons, 4 C snippets, 2 excerpts, 2 shell blocks and 19 figures in the default build. The C-snippet count reads 0 when the harness is run by hand without `--cflags-file`; CTest passes it. `MAINTENANCE.md` documents the fence-tag convention |
| `docs_screenshots` | 19/19 figures re-rendered and byte-identical to the checked-in PNGs. AKGL build only — rendering a picture needs the SDL half |
| Language corpus | 65/65 paired expectations — **and 65/65 again through the SDL binary**, which is most of what proves the frontend changes no output |
| ASan + UBSan | 112/112 |
| Line coverage | 94.1% (7227/7681) — above the 90% gate |
| Function coverage | 97.9% (474/484) |
| Warnings | none under `-Wall -Wextra` |
| `doxygen Doxyfile` | clean |
| Mutation (`src/symtab.c`) | 74.1%, against a gate of 65 — see `.gitea/workflows/ci.yaml` |

**Two defects surfaced from writing that harness**, both fixed with a test that fails when the
fix is reverted:

1. **`DLOAD` overwrote the last line of every program it loaded from a prompt.** It scans the
   file through the runtime's own scanner, so on return the tokens of the calling line are gone
   and `hadlinenumber` and `environment->lineno` describe the last line of the *file*. The REPL
   carried on parsing what it believed was still its own buffer, decided the leftovers were
   program text, and filed the `DLOAD` command under that line number.
   `src/runtime_commands.c` now sets `skiprestofline`. `tests/disk_verbs.c`.
2. **A BASIC error typed at the prompt terminated the driver.** `process_line_run()` had always
   swallowed the context after `interpret()` put the error line on the sink — goal 3, in the
   comment, in so many words — but the direct-mode branch of `process_line_repl()` used a bare
   `PASS`. A `VERIFY` against a file that did not match, which is an ordinary user answer rather
   than a fault, came out as a stack trace and would have taken an embedding host with it.
   `tests/housekeeping_verbs.c`.
3. **An armed `TRAP` made a parse error vanish outright.** Found the same way, writing §8
   item 11's error-code appendix: a program with `TRAP` set and a line that would not parse
   printed *nothing at all* and exited zero. No error line, because
   `akbasic_runtime_error()` intercepts for the trap and deliberately prints nothing; and no
   handler, because the parse branch of `process_line_run()` then set `run_finished_mode`
   regardless — QUIT for a file run — so the next line boundary the handler would have been
   entered at was never reached. **Arming an error handler made errors disappear**, which is
   the exact opposite of the verb's purpose, and it was silent in both directions.

   The guard is one `if`: set the finished mode only when the error was actually reported.
   The runtime path was always right, because `report_and_reraise()` never touched the mode.

   **And the same branch never recorded the status**, so the handler that now runs was
   handed `ER# 0`. It sets `obj->lasterrorstatus` from the context the way
   `report_and_reraise()` does, and a trapped `SCALE` with no arguments now reports 512.
   Both halves in `tests/trap_verbs.c`.

None of the three was on any list. All were found by writing down what a chapter claimed and then
running it — which is the argument for the harness rather than a footnote to it.

**A fourth is open, and it is the same defect as item 2 on a path that fix did not cover.**
The boundary between a script's error and the host's error sits around parsing and around
`interpret()`, but **not around scanning** — so the token ceiling takes the prompt down with a
stack trace for a line a person typed. **Issue #4**, which also names the third call site whose
answer may want to differ.

**A separate one was found while designing optional line numbers, and is fixed.**
`akbasic_runtime_process_line_runstream()` filed the *raw* buffer, line number and all, while
`akbasic_runtime_load()` and `DLOAD` filed what the scanner had stripped. The `if` that chose
between them tested `obj->mode == AKBASIC_MODE_REPL`, and nothing reaches that function except
the `RUNSTREAM` arm of `step()` — so the stripped branch was dead and every program the driver
ran from a file was stored with its own number inside `source[]`. `LIST` printed
`10 10 PRINT "ONE"`; `DSAVE` would have written it that way and `HELP` echoed it that way.

It went unseen because no `.bas` in either corpus calls `LIST`, and because the one test helper
that claimed to load "the way RUNSTREAM would" (`tests/runtime_verbs.c:30`) stored the scanned
line — that is, it encoded the *correct* contract and so could never catch the wrong one.
`src/runtime.c` now files `scanned`. `tests/runtime_verbs.c` drives the real sink path and
fails with `10 10 PRINT "ONE"` when the fix is reverted.

The step-over-a-leading-number allowance in `scan_line_labels()` (`src/runtime.c:1235`) and
`skip_lineno()` (`src/structtype.c:42`) is kept, and both comments now say why: no path inside
the library stores a raw line any more, but `akbasic_runtime_store_line()` is public and a host
may hand it one.

Coverage of the verb groups added for goal 2, since they are the new surface: `src/audio_tables.c`
and `src/graphics_tables.c` 100%, `src/runtime_input.c` 100%, `src/runtime_graphics.c` and
`src/runtime_audio.c` 98%, `src/play.c` 87%. The `akbasic_akgl` target is not in the coverage
figure — it is not built in a default configuration.

**The denominator moved a long way and this is not an explanation of why.** The line total went
from 4076 to 6222 between two entries, which is far more than the work added between them, so one
of the two measurements is counting something the other is not. Recorded rather than quietly
overwritten, because a coverage percentage whose denominator nobody can account for is worth
exactly as much as the accounting. The ratio is above the gate on either reading. **Issue #26.**

**A note on reading that number, because it was misread once while producing it.** A stale
`build-cov/` left in the source directory from an earlier session was silently folded into the
report by `gcovr --root .`, which produced the *previous* run's 92.3% for a tree that had grown
by 800 lines. The number above was produced from a coverage tree outside the source directory
entirely, after a `find . -name '*.gcda'` came back empty. That is `libakgl` defect #13 happening here rather than there, and `build*/` being
gitignored is exactly what makes it invisible. **Keep build trees out of the source directory**,
and if a coverage number looks suspiciously unchanged, `find . -name '*.gcda'` before believing
it.

Branch coverage reads 18.0% and is not a target, for the reason `libakgl` and
`libakstdlib` both give: the akerror control-flow macros expand into large branch trees
at every call site, most of them unreachable in normal operation. Track line and function
coverage.

Dependency baseline:

| Submodule | Version | Notes |
|---|---|---|
| `deps/libakerror` | 2.0.2 | Private ownership-enforced status registry. akbasic reserves 512–767 in `akbasic_error_register()`. **2.0.0 is thread safe and an ABI break** (`libakerror.so.2`): the `IGNORE` context is thread-local and `akerr_next_error()` returns an owned reference, neither of which fails to link when mismatched. **2.0.1 fixes an exit status that mattered more to this band than to any other** — see below. **2.0.2** namespaces its embedded `coverage` target (issue #15 — our `add_custom_target` shadow is deleted), installs `akerrorConfigVersion.cmake` at `SameMajorVersion` (issue #16), fixes the `AKERR_USE_STDLIB=OFF` build, retires `PATH_MAX` for `AKERR_MAX_ERROR_FNAME_LENGTH`, and turns the `IGNORE` slot into a released copy — `__akerr_last_ignored` was an `extern` pointer and is now a per-TU `static akerr_last_ignored`. **That last one costs us 1.35 MiB of thread-local storage**: 38 translation units x 37,296 bytes, measured as the whole TLS segment of `build/basic`, where 2.0.1 produced no TLS segment at all. Filed upstream as libakerror issue #37, along with the 84 `-Wunused-variable` warnings it emits. Its `UPGRADING.md` describes a 2.0.3 that `project()` never stamped — libakerror issue #38, which is why the version above reads 2.0.2. |
| `deps/libakstdlib` | 0.2.0 | soname `libakstdlib.so.0.2`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. This release fixed all six confirmed defects the port was working around — see §1.9, where the bans are now lifted. Since then, and with no version bump: **directory wrappers landed** (`aksl_opendir`/`readdir`/`closedir`/`rewinddir`, issue #10) — the gap `DIRECTORY` was refused for, so that refusal now says only that the verb is unwritten; file-metadata wrappers landed; and `aksl_snprintf` keeps its `int *count` but now reports the *required* length on truncation rather than 0. Only read on success here, so nothing moved. |
| `deps/libakgl` | 0.9.0 | soname `libakgl.so.0.9`. Owns status codes 256–262. Linked and tested under `-DAKBASIC_WITH_AKGL=ON`, which still defaults OFF so the core library and its whole suite build on a machine with no SDL. **0.3.0 closed every API gap this port had filed** — see §7 — so the four workarounds §3 used to list are gone. 0.4.0 was a leak-and-overread release that changed no public struct. **0.5.0 is the first that broke our source as well as our ABI**: it namespaced every exported symbol, so `akgl_render_bind2d` is `akgl_render_2d_bind`, `akgl_sprite_sheet_coords_for_frame` is `akgl_spritesheet_coords_for_frame`, the `renderer`/`camera`/`window` globals carry the prefix, and `_akgl_renderer`/`_akgl_camera` are `akgl_default_renderer`/`akgl_default_camera`. `include/akbasic/akgl.h` asserts the floor. **0.6.0 and 0.7.0 broke nothing here** — 0.6.0 is three arcade-physics fixes and a `physics.max_timestep` property this port does not use, and 0.7.0 reports failures `libakstdlib`'s wrappers were already catching and takes `libakerror` 2.0.1. **0.8.0 and 0.9.0 broke nothing here either** — 0.8.0 is the collision subsystem (`AKGL_ERR_COLLISION`, code 261, and the vendored `libccd` and `tg` submodules that come with it), and 0.9.0 is the `akgl_ui` subsystem (`AKGL_ERR_UI`, code 262, and vendored `clay`), which is what `src/ui_akgl.c` draws through. Both widen libakgl's reserved band from five codes to seven; the range map in `MAINTENANCE.md` carries it. The floor moved anyway, because deciding for ourselves which of libakgl's minor releases were really compatible is the judgement the soname exists to take away. **Note `deps/libakgl/deps/` pins its own `libakerror` and `libakstdlib` older than ours**; the top-level build declares both targets first and libakgl's `if(NOT TARGET ...)` guards mean its copies are never configured, so what libakgl actually compiles against is what this tree pins. |

**An unhandled error in this band used to exit zero, and 512 is the worst possible base for
that.** `libakerror`'s default unhandled-error handler ended in `exit(errctx->status)`, and a
process exit status is one byte — the kernel keeps the low 8 bits and discards the rest.
`AKBASIC_ERR_BASE` is 512, and **512 truncates to 0**, so an unhandled `AKBASIC_ERR_SYNTAX`
reported *success* to a shell, a CI job or a supervisor watching `$?`. Every other code in the
band came out as some unrelated error's number: 515 as 3, 519 as 7.

libakerror 2.0.1 routes the handler through `akerr_exit()`, which substitutes
`AKERR_EXIT_STATUS_UNREPRESENTABLE` (125) for anything a byte cannot carry. Verified here
rather than taken on trust: a probe raising `AKBASIC_ERR_DEVICE` through `FINISH_NORETURN`
exits 125.

**It was latent here, not live**, and that is worth stating precisely rather than claiming a
narrow escape. `src/main.c` handles the context itself and returns `EXIT_FAILURE`, and every
test with a top-level `ATTEMPT` carries a `HANDLE_DEFAULT` — so nothing in this repository ever
reached the defaulted handler. `libakgl` was not so lucky and found it the hard way: its
`tests/character.c` had been passing while running one of its four tests, because `AKGL_ERR_SDL`
is exactly 256 and exited 0.

The guard is now two `#error`s in `include/akbasic/error.h` and two assertions in
`tests/version_check.c`, including one that fails if `AKBASIC_ERR_BASE` stops truncating to
zero — because the day the band moves is the day this note stops being about *our* base, and a
silent change of subject is how a comment becomes a lie.

**The `libakgl` requirement used to be pinned by commit, and no longer is.** 42b60f7 added 22
public symbols across four headers and left `project(akgl VERSION 0.1.0)` and the
`libakgl.so.0.1` soname unchanged, so nothing here could feature-test for the new API and the
submodule pointer was the only thing expressing the requirement. That was filed upstream as
`libakgl` defect #14 and fixed by its commit 1066ac7 — the two crossed rather than one
following the other. `libakgl` is 0.2.0 with an `libakgl.so.0.2` soname, so
`AKGL_VERSION_AT_LEAST(0, 2, 0)` now means something and an ABI mismatch is refused at load
time. Kept here because the *reason* is the reusable part: an additive release under an
unchanged soname costs its consumers the ability to detect it at all.

CI is split in two. `.gitea/workflows/ci.yaml` runs on push -- the suite, ASan+UBSan, coverage
and a two-file mutation run -- and `.gitea/workflows/release.yaml` is manual
(`workflow_dispatch`), carrying the doxygen gate and the whole-tree mutation run. The split is
about cost: the full mutation set is 3675 mutants and hours of runner time, which is a release
gate rather than a per-commit one, and the docs are wanted at release time. Three notes worth
keeping:

- The checkout is `submodules: true`, **not** `recursive`. The build needs `libakerror`,
  `libakstdlib` and `basicinterpret`; it does not need `libakgl`, which is guarded behind
  `AKBASIC_WITH_AKGL` and defaults OFF — and recursing into it would clone SDL, SDL_image,
  SDL_mixer, SDL_ttf and jansson for a target that is never configured. The `docs` job needs
  no submodules at all.
- The push-path `mutation_test` job is bounded to `src/symtab.c`, scoring 74.1% against a gate
  of 65. It was two files until `src/convert.c` was deleted. `src/audio_tables.c` was measured
  as a replacement and scored 64.7%, below the gate — a real gap rather than a reason to skip
  the file: almost every survivor is in `akbasic_audio_state_init`, where nothing asserts that
  a freshly initialised audio state is actually zeroed and defaulted. That is the same gap this
  job's history records closing for `src/symtab.c`, and closing it is what earns the file its
  place back in CI.

  Worth knowing before reading a score: the harness's `ICR` operator only rewrites the
  constants `0` and `1`, so a lookup table of other values produces no mutants at all and a
  high score over one says nothing about whether its entries are right. `tests/audio_verbs.c`
  asserts all 32 ADSR entries anyway — that catches a hand-edit typo, which is the real failure
  mode, but it did not and could not move the mutation number. `src/value.c` is the file most
  worth mutating and is deliberately not on that path: 368 mutants at roughly 11 seconds each
  is about 70 minutes, because almost everything links against it. It is covered by
  `release.yaml`, or locally with `cmake --build build --target mutation`.
- `release.yaml`'s threshold defaults to 65, the same as the push path, rather than something
  stricter. Only two files have ever been measured; a tighter whole-tree number would be a
  guess until a full run establishes a baseline. Raise it through the workflow input once one
  has.

What remains, in priority order:

1. ~~**The standalone AKGL frontend in §3.**~~ **Done** — `src/frontend_akgl.c` in its own
   `akbasic_frontend` target, `src/sink_tee.c` for the stdout mirror, and
   `tests/akgl_frontend.c`. The build option now changes what the executable does. The one
   thing not machine-verified is typing at a real focused window; §3 records why and what was
   verified instead.
2. ~~**A line editor for the akgl sink.**~~ **Done** — `readline` in `src/sink_akgl.c`, over
   the keystroke ring, borrowing frames from the host through an `akbasic_AkglPump`. It cannot
   type a shifted character, which is `libakgl` item 10 rather than work outstanding here.
3. **The language completion queue is done, and this entry used to say otherwise.** Every group
   in §4's table — A through J, `RESTORE` and `RENUMBER` included — is implemented and tested.
   The list that used to sit here naming A, D, F, J and H as outstanding was stale.

   **What survives it is one piece of structural work, and it is not a verb.** Block skipping
   works by source *line*, so a whole `FOR`/`NEXT` written on one line never reaches its `NEXT`
   — inherited structure that did not matter until a line could hold more than one statement,
   and `DO`/`LOOP`/`WHILE`/`UNTIL` are exactly the verbs people write on one line. **Issue #6.**

   `FILTER` is the one verb still refused, and deliberately: `akgl_audio_*` synthesises raw
   waveforms and mixes them, there is no filter stage to configure, and SDL3 supplies no
   primitive to build one from. It is refused with `AKBASIC_ERR_DEVICE` rather than silently
   ignored — a program that asks for a low-pass and gets an unfiltered square wave has been
   lied to. The libakgl half is that repository's issue #58.

4. ~~**CI does not cover `-DAKBASIC_WITH_AKGL=ON`.**~~ **Done** — the `akgl_build` job in
   `.gitea/workflows/ci.yaml`. It turned out much cheaper than the deferral assumed, and the
   two reasons are worth keeping because both were guesses that measurement corrected:

   - **`submodules: recursive` is not needed and costs 461 MB.** It descends into
     `SDL_image/external`, `SDL_mixer/external` and `SDL_ttf/external` — aom, dav1d, libjxl,
     libtiff, mpg123, opus, flac and a dozen more — and *not one of them is used*. Every one
     configures as "Could NOT find". The job initialises exactly six submodules
     non-recursively instead, which takes about 25 seconds.
   - **The build is about a minute of CPU, not "a long build".** 330 object files, because
     SDL3 compiles out almost every backend that is not wanted here.

   The one real system dependency is `libfreetype-dev` and `libharfbuzz-dev`: SDL_ttf prefers
   the system copies and would otherwise reach for `external/freetype`, which the checkout
   deliberately does not clone. Two apt packages against two more submodules is an easy trade.

   Every step was verified from a clean clone before being written down, not inferred.
5. ~~**§6 items 12–17.**~~ **All done**, each with a regression test in the suite that pinned
   it. Item 16 was the last, and it was never a coding problem — it was blocked on the
   fidelity constraint §0.1 retired, and took about ten minutes once that went away.
   `AKBASIC_KNOWN_FAILING_TESTS` is empty as a result and `tests/known_reference_defects.c` is
   gone.

   **§6 items 1–9 and 11 are now open in a way they were not.** They were reproduced
   deliberately and left alone because faithfulness was the goal; it is not any more. Read them
   as an ordinary defect list. Item 4 (`mathPlus` mutates `self` in place, which `CommandNEXT`
   relies on) is the one with real blast radius and still wants `FOR`/`NEXT` coverage before
   anybody touches it. Item 5 (binary operators add *both* of the right operand's numeric
   fields) is the one worth doing early: it is currently harmless, it makes two mutants in
   `src/value.c` unkillable, and it is a landmine for any future value carrying both.
6. **Mutation survivors in `src/value.c`** — ~~closed~~, with one correction to what was
   written here before. `tests/value_arithmetic.c` grew two functions,
   `test_maximum_length_string()` and `test_pool_starts_empty()`, and each was verified by
   hand-applying the mutant and watching the test fail rather than by assuming it would:

   | Mutant | Result |
   |---|---|
   | `strncpy(dest->stringval, src, AKBASIC_MAX_STRING_LENGTH - 1)` → `- 2` | killed |
   | `dest->stringval[AKBASIC_MAX_STRING_LENGTH - 1] = '\0'` → `- 2` | killed |
   | `strncpy(..., AKBASIC_MAX_STRING_LENGTH - 1)` → `- 0` | **equivalent — cannot be killed** |
   | `memset(obj, 0, sizeof(*obj))` deleted from `akbasic_valuepool_init` | killed |
   | `obj->next = 0` → `= 1` | killed |

   **The correction.** This entry used to say all five were real bugs and that "one test that
   round-trips a 255-character string kills all five". Two things about that were wrong.

   The `- 0` mutant is *equivalent*. `strncpy(dest, src, 256)` into a 256-byte buffer writes at
   most 256 bytes, and `set_string()` has already refused anything longer than 255 two lines
   above, so the extra byte is always the terminator `strncpy` would have written anyway. It
   cannot be killed and should not be counted against the score.

   And the obvious test does *not* kill the truncating mutant. Joining a full-length string to
   an empty one passes with the mutant in place, because `akbasic_value_math_plus` clones
   `self` into the scratch before writing and the byte a short copy fails to write is already
   the right one. The operands have to sum to the limit **without either of them being the
   answer** — the test uses 200 `X` plus 55 `Z`, and says so at the site, because this is
   exactly the sort of thing that gets "simplified" back into a passing no-op.

   Two survivors there are genuinely equivalent and cannot be killed: `rval_as_int` and
   `rval_as_float` (`src/value.c:30,35`) tolerate `+` → `-` because the reference's habit of
   adding *both* of the right operand's numeric fields only works when the unused one is zero
   — §6 item 5. Fixing that defect would make these two mutants killable, which is a small
   argument for doing it.

   `src/value.c` takes about 70 minutes to mutate in full (368 mutants, ~11s each, because
   almost everything links against it), which is why CI runs `src/symtab.c` instead and the
   whole-tree run is a local `cmake --build build --target mutation`.

9. ~~**Use the actual full SDL window for graphics operations.**~~ **Done**, and the premise
   turned out to be half wrong in a way worth keeping. The documentation did say coordinates
   are 320x200 whatever the window is — but nothing ever stretched them: with `SCALE` off a
   coordinate went straight to `akgl_draw_*` as a pixel address, so an 800x600 window drew a
   listing into its top-left corner and left the rest unused. The described transform did not
   exist.

   `akbasic_GraphicsBackend` grew an optional `size` entry point, `require_graphics()` asks it
   before every verb that draws so a resized window is honoured between statements, `SCALE`
   maps onto the answer instead of onto the constants, and `RGR(1)` / `RGR(2)` let a program
   read the size. 320x200 is now the fallback for a backend that will not answer. Deviation 18
   in §5 has the whole of it, including the off-by-one it fixed on the way.

11. ~~**Error codes are undefined for the user.**~~ **Done** — `docs/15-error-codes.md`, an
    appendix covering the four error classes the interpreter prints, the eight codes it owns
    with what raises each, and the codes that reach `ER#` from `errno`, `libakerror` and
    `libakgl` underneath it. The table is not asserted: a program in the chapter trips seven
    of the eight and prints what it got, and `docs_examples` byte-compares the result.

    **It found three things rather than documenting eight.** A parse error under an armed
    `TRAP` vanished entirely and a trapped parse error reported `ER# 0` — both fixed, both
    recorded above. And `VAL` reports a dependency's status code where the interpreter has
    one of its own, which is §6 item 21 and is filed rather than fixed.

12. ~~**Screenshots for graphics operations.**~~ **Done** — eight figures, six in
    `docs/06-graphics.md` and two in `docs/08-sprites.md`. The item said chapter 8; chapter 8
    is sprites and chapter 6 is the drawing verbs, so both got them.

    **A figure here is output, not an asset**, which is the part worth keeping. Each one is
    generated by running the listing printed immediately above it:

    - `tools/screenshot.c` is a second SDL host, and a much smaller one than
      `src/frontend_akgl.c` — dummy video driver, software renderer, run to completion,
      `SDL_RenderReadPixels`, `IMG_SavePNG`. The pattern is `deps/libakgl/tests/draw.c`'s and
      this is its third user. It draws no text layer on purpose: a `READY` in the corner is
      noise in a figure about `BOX`, and skipping it means no font has to be resolved.
    - `tools/docs_screenshots.sh` reads the new `screenshot=NAME` fence tag straight out of
      the markdown, so the picture cannot drift from the code beside it without somebody
      editing one of the two. `size=WxH` is the second tag, and `SCALE`'s figure uses it —
      640x400, because the point being illustrated is a 320x200 listing filling a larger
      window, and that point cannot be made on a 320x200 surface.
    - **Two gates, and they answer different questions.** `docs_examples` checks that a
      tagged block *has* an image, in both configurations, so a figure cannot be added and
      forgotten. `docs_screenshots` — a CTest, AKGL build only — re-renders every figure and
      compares byte for byte, so a listing edited without regenerating fails. The first
      catches a missing picture; only the second catches a *stale* one, which is the failure
      this whole arrangement exists against.

    The byte comparison is a deliberate bet that the dummy driver and the software renderer
    are reproducible, which is the same bet `tests/language/` makes about golden
    output. Verified run-to-run and build-to-build here; what is untested is an SDL upgrade
    that moves one pixel of a diagonal. **If that happens, regenerate the figures in the same
    commit as the bump** — do not weaken the test to a size check, which would pass for every
    wrong picture that happened to be 320x200.

    The PNGs are checked in, because a reader on the forge has no build tree, and
    `docs/images/README.md` says loudly that they are generated so nobody hand-edits one.
    Regenerating is never part of a build: `cmake --build build-akgl --target
    docs_screenshots` is a deliberate act, so a `make` cannot quietly put eight binary diffs
    in front of whoever ran it.

    **Writing them turned up a documentation defect of this file's own**, recorded at §5
    deviation 16: that entry claimed in bold that `BOX` fills on a negative angle, while its
    own body said the fill was filed rather than implemented. Drawing the figure and getting
    an outline back is what caught it. `akbasic_GraphicsBackend::filled_rect` is implemented,
    recorded by the mock, and reached by no verb at all as a result.

---

## 9. Defects found by writing a game against it

`examples/breakout/sprites/breakout.bas` is a complete Breakout — sprite artwork,
powerups, a stroke-font HUD — written in this dialect, for this interpreter, by somebody who
had not written a line of it before. Eight things came out of that, and they are here for the
same reason §8's three are: **none of them was on any list, and all of them were found by
using the thing rather than by testing it.** The corpus reaches none of them.

Ordered by blast radius. Each has a minimal reproduction that fits on a screen; every one was
reduced against `build/basic`, the stdio build, unless it says otherwise.

1. ~~**Every call to a user-defined function leaks value-pool slots, so a program may call one
   about four thousand times and then die.**~~ **Done** with §6 item 30, and by the same
   change: the leaking slot was the call scope's parameter, which is a scalar and no longer
   comes from the pool. Both `DEF` forms run eight thousand calls in
   `tests/user_functions.c`. The original report:

   ```basic
   DEF ADDIT(N#)
     RETURN N# + 1
   R# = 0
   FOR I# = 1 TO 8000
     R# = ADDIT(I#)
   NEXT I#
   ```

   ```output
   ? 5 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots
   ```

   It fails between 4000 and 5000 calls, which is `AKBASIC_MAX_ARRAY_VALUES` (4096,
   `include/akbasic/types.h:29`) at one slot per call — the call scope's parameter. **Both
   `DEF` forms leak**; the single-expression `DEF SQR1(X#) = X# * X#` fails at the same point.
   A `GOSUB` doing the same work runs eight thousand times without complaint, which is the
   comparison that isolates it.

   `akbasic_valuepool_take()` (`src/value.c:142`) is a bump allocator — `obj->next += count` at
   `:160` — and **nothing anywhere returns a slot**. The only reset is
   `akbasic_valuepool_init()`, from `src/runtime.c:209` at startup and
   `src/runtime_housekeeping.c:53` for `CLR`/`NEW`. Environments *are* released back to their
   own pool (§1.4, and §6 item 11 says so), but the value slots the variables in them held are
   not: `src/environment.c:321` takes them through `akbasic_variable_init()` and the release
   path has no counterpart.

   The consequence is a hard ceiling on a whole language feature. A game calling one function
   per frame at 30fps gets a little over two minutes. It is why the game in question spells its
   pseudo-random generator as a `GOSUB` over globals rather than the `DEF` it wants to be.

   Fixing it means giving the pool a free list, or giving each environment a value arena
   released with it. The second is closer to the house style and to what §1.4 already does for
   environments; it touches `src/value.c`, `src/variable.c`, `src/environment.c` and
   `include/akbasic/value.h`. A test belongs in `tests/user_functions.c`: call a `DEF` ten
   thousand times and then `DIM` an array.

2. ~~**A `BEGIN` block that is skipped leaks a scope for every loop inside it**, because
   `FOR` and `DO` create their environment at *parse* time and the skip is decided at
   *evaluate* time.~~ **Done** — the skip now releases what parsing pushed
   (`akbasic_runtime_interpret()`, `src/runtime.c`), taking the second of the two routes
   below. `tests/structure_verbs.c` covers both loop forms inside a routine and the
   forty-skip top-level case.

   **It is narrower than it looks, and the golden corpus is why.** The first attempt
   released the scope on *any* skip, which broke
   `tests/language/flowcontrol/nestedforloopwaitingforcommand.bas`: a zero-
   iteration `FOR` skips its body by the same mechanism, and there the orphan is
   load-bearing -- it is what absorbs the inner `NEXT` so the outer one still finds its
   `FOR`. Releasing it turns that case into "NEXT outside the context of FOR". So the
   release is conditional on the skip being a *block* skip, which is testable because a
   skipped block can never contain a live `NEXT` wait. Both halves are now asserted side by
   side in `tests/structure_verbs.c`.

   The original report:

   ```basic
   N# = 0
   LABEL AGAIN
   N# = N# + 1
   IF 1 = 0 THEN BEGIN
     FOR I# = 0 TO 2
       N# = N# + 1000
     NEXT I#
   BEND
   IF N# < 40 THEN GOTO AGAIN
   ```

   ```output
   ? 5 : PARSE ERROR Environment pool exhausted at line 5 (32 in use)
   ```

   Thirty-two skips is `AKBASIC_MAX_ENVIRONMENTS` (`include/akbasic/types.h:31`) at one per
   skip. Inside a subroutine it announces itself on the very first skip and much more
   confusingly — the orphaned scope sits between the routine and its caller, so the `RETURN`
   after the block reports from a routine that plainly *was* entered by a `GOSUB`:

   ```basic
   T# = 0
   GOSUB DOIT
   PRINT "came back"
   END
   LABEL DOIT
   IF 1 = 0 THEN BEGIN
     FOR I# = 0 TO 2
       T# = T# + 1
     NEXT I#
   BEND
   RETURN
   ```

   ```output
   ? 11 : RUNTIME ERROR RETURN outside the context of GOSUB
   ```

   That is the form it was found in, and it costs an evening because the error names the one
   construct that is not at fault.

   **The cause is an ordering mismatch and it is exact.** `akbasic_parse_do()`
   (`src/parser_commands.c:266`) and `akbasic_parse_for()` (`:849`) both call
   `akbasic_runtime_new_environment()` while parsing the line. The block skip is tested in
   `akbasic_runtime_interpret_line()` (`src/runtime.c:767`), which runs *after* the line has
   been parsed — so a skipped `FOR` pushes a scope, its execution is skipped, and the `NEXT`
   that would pop it is skipped too. `BEGIN` itself pushes nothing, which is why a nested
   `BEGIN`/`BEND` inside a skipped block is harmless, and a `GOSUB` inside one is harmless for
   the same reason.

   Confirmed for both loop forms: `DO`/`LOOP` in a skipped block fails identically. Confirmed
   for both kinds of enclosing routine: a multi-line `DEF` reports the same thing.

   A fix has to either push the loop's scope at execution rather than at parse, or have the
   skip release what parsing pushed. Touches `src/parser_commands.c`, `src/runtime.c` and
   `tests/structure_verbs.c`, which today only exercises blocks whose bodies are plain
   statements.

3. **In the standalone SDL build, the text layer owns every row of the window by default.**
   **Half done.** The tee sink now forwards `WINDOW`, so a program *can* take rows back, and the
   drawing verbs render into a layer the frame composites under the text and the sprites, so a
   drawing survives the frame it was made in.

   What stands is the default, and it is a real question rather than a defect: a program that
   does not call `WINDOW` still cannot see a drawing. **The consequence worth keeping is that
   chapter 6 cannot be run in the interpreter it documents** — its figures are right because
   `tools/screenshot.c` deliberately omits the text layer, so the harness that proves the chapter
   is exactly the harness that hides this, and `docs_screenshots` passes and always would.
   **Issue #23.**

4. ~~**Mixed-type arithmetic is decided by the left operand alone, and nothing says so.**~~
   **Done, as documentation, by decision.** The behaviour is unchanged: a dialect is
   allowed to say the left operand wins and this one is consistent about it, and promotion
   would change the result of every mixed expression in every existing program. What was
   missing was anybody saying so. Now: a section in `docs/03-the-language.md` with the
   two-line demonstration and the two rules that keep a program out of it, a row in
   `docs/13-differences.md` naming it as the difference from 7.0 most likely to turn a
   working listing into a quietly wrong one, and the reasoning on `include/akbasic/value.h`
   where the operators are declared.

   `tests/value_arithmetic.c` pins it in both directions, so **promotion is now a decision
   somebody has to take deliberately** rather than a change that could slip in. The
   original report:

   ```basic
   A# = 3
   PRINT A# * 0.45
   PRINT 0.45 * A#
   ```

   ```output
   0
   1.350000
   ```

   `akbasic_value_math_plus()` and its siblings branch on `self->valuetype`
   (`src/value.c:510`, `:512`, `:539`) and convert the right operand to match, so an integer on
   the left silently truncates a float on the right. **This is inherited, not introduced** —
   `deps/basicinterpret/basicvalue.go:190-193` has the same shape — and §6 item 5 already fixed
   the other half of that expression without changing the branch.

   It may well be intended; a dialect is allowed to say the left operand wins, and this one is
   consistent about it. The defect is that **it is documented nowhere**. Chapter 3's "Numbers"
   does not mention it, chapter 13 does not list it among the differences, and a C128 promotes
   to float instead. Nothing fails when you get it wrong — the program computes something else
   and carries on.

   It cost the game two live bugs before either was noticed. `SPD% = 5.6 + LEVEL# * 0.45`
   evaluated to a flat 5.6, so no level ever ran faster than level one; and `0 - BLVX%(B#)`,
   the obvious way to reverse a ball, quantised its velocity to whole pixels on every bounce
   and bled speed out of it. Both read correctly. Neither produced a diagnostic.

   The cheap fix is documentation: a paragraph in chapter 3 and a row in chapter 13. The other
   fix is promotion, which would be a real change of semantics and wants its own decision.

5. ~~**A drawing that spans a 256-line batch boundary is torn, not merely transient.**~~
   **Done, as documentation.** No fix is possible without changing what a host owns -- the
   budget is the host's and the present is the host's -- so what was needed was for the
   consequence to be written down beside the advice it qualifies. `docs/13-differences.md`
   now says that "redraw it every frame" is not sufficient on its own and carries the
   measured numbers, `docs/06-graphics.md` says it where `SSHAPE` is introduced, and
   `docs/14-architecture.md` explains the mechanism from the step loop's side and points at
   the `TI#` synchronisation a program has to build.

   **Untested, and deliberately:** it needs the real frontend, a clock and a timing
   window, and a test that reproduced it would be reproducing a race. Said here rather than
   left looking like an oversight. The original report: §5 and
   chapter 13 record that drawing does not persist across frames. What neither says is that a
   single uninterrupted sequence of drawing verbs longer than one `akbasic_runtime_run()` budget
   comes back **half drawn**, because the present in the middle of it discards the buffer — so
   an `SSHAPE` at the end captures only the statements issued since that present, over whatever
   the frame before left behind.

   Measured against `AKBASIC_FRONTEND_STEPS_PER_FRAME` of 256, in the SDL build: after
   synchronising to a jiffy edge, 110 `DRAW` statements in a `FOR` loop (220 executed lines)
   are captured whole; 125 (250 lines) capture nothing at all; 140 (280 lines) capture the last
   fifteen. A program cannot see the boundary except by watching `TI#` change, which is what
   the game's `PACE` routine does and what makes the whole thing workable.

   This is worth a paragraph in chapter 13 beside the existing note, because "redraw it every
   frame" is the advice there and it is not sufficient advice — the redraw also has to fit.

6. ~~**`SSHAPE` and `GSHAPE` ignore the subscript on a string array element.**~~ **Done** --
   `shape_variable()` (`src/runtime_graphics.c`) now resolves the subscript the way
   `SPRSAV` does, through a shared `akbasic_environment_collect_subscripts()` lifted out of
   `src/environment.c` so that a verb taking a variable by name resolves one exactly as
   assignment does. `tests/graphics_verbs.c` covers the reduction and the case a program
   actually wants: two shapes captured into two elements and each stamped back by its own.

   The original report, which needed a graphics device and was reduced against
   `build-akgl/basic`:

   ```basic
   DIM SH$(4)
   SSHAPE SH$(2), 0, 0, 61, 4
   PRINT "[" + SH$(0) + "] [" + SH$(2) + "]"
   ```

   ```output
   [SHAPE:0] []
   ```

   The handle lands in `SH$(0)`; `SH$(2)` stays empty. `GSHAPE SH$(2)` then stamps whatever is
   in `SH$(0)`. Ordinary assignment and `PRINT` honour the subscript, so a program that keeps
   several saved shapes in an array gets every one of them resolving to element zero, silently.

   `shape_variable()` (`src/runtime_graphics.c:625`) takes `arg->identifier` and calls
   `akbasic_environment_get()` — it never evaluates the subscript — and both verbs then use a
   literal zero: `src/runtime_graphics.c:688` writes with `zerosubscript`, `:724` reads with it.
   **`SPRSAV` is the counter-example and the model for the fix**: it calls
   `akbasic_runtime_evaluate()` on its argument (`src/runtime_sprite.c:480`) and handles an
   array element correctly.

   The game keeps six brick stamps in six separate scalars because of this, and the workaround
   is not obvious from the failure — every brick simply comes out the colour of the last one
   captured. `tests/graphics_verbs.c` is where the regression goes.

7. ~~**`GRAPHIC CLR` is documented and refused.**~~ **Done** -- `akbasic_parse_graphic()`
   (`src/parser_commands.c`) takes `CLR` as this verb's keyword argument and emits mode 5,
   which `akbasic_cmd_graphic()` already treats as exactly this, so both spellings are one
   statement and the exec handler is unchanged. The documentation was right and the parser
   was not; `tests/parser_commands.c` now pins the form the two chapters give.

   The original report: `docs/06-graphics.md:65` and
   `docs/11-verb-reference.md:54` both give the form as `GRAPHIC mode | CLR`.

   ```basic
   GRAPHIC CLR
   ```

   ```output
   ? 1 : PARSE ERROR 1 at 'CLR', Expected expression or literal
   ```

   `GRAPHIC` parses with `akbasic_parse_arglist` (`src/verbs.c:86`), so `CLR` scans as the `CLR`
   verb rather than as a keyword argument. `GRAPHIC 5` does the job and is what the game calls.
   Either the parse handler learns the word or both documents lose it; the second is smaller and
   the first is what a C128 programmer will type.

   Worth noting because `docs_examples` cannot catch it: every fenced `GRAPHIC CLR` in the
   documentation is prose, not a tagged runnable block.

8. ~~**A `DATA` line holding a negative number cannot be executed.**~~ **Done** --
   `akbasic_parse_data()` accepts a unary minus over a numeric literal, which is the only
   shape that was refused. Deliberately narrow: `DATA -A#` is still a mistake worth naming,
   and `akbasic_leaf_is_literal()` keeps meaning what it says because it is used elsewhere.
   `tests/read_data.c` covers reading one *and* walking past the line, which were two
   different mechanisms and only one of them was ever wrong.

   The original report: §4 settled that "`DATA` at
   run time is now a no-op: it is a declaration, and reaching the statement means walking past
   it." Walking past a negative one raises instead:

   ```basic
   READ A#
   PRINT "READ GAVE " + A#
   DATA -5
   PRINT "REACHED THE LINE AFTER THE DATA"
   ```

   ```output
   READ GAVE -5
   ? 3 : PARSE ERROR Expected literal
   ```

   `READ` handles the value correctly — the negative comes back intact — so this is only the
   statement's own argument parse, in `akbasic_parse_data()`
   (`src/parser_commands.c:934`), refusing a leading `-` where the prescan accepted it. The
   positive form falls through cleanly.

   Reachable in ordinary code: a table of `DATA` at the foot of a program is walked into by any
   routine above it that ends in a fall-through rather than an `END`, and a table of angles or
   offsets is exactly where negative numbers live. `tests/read_data.c`.

9. ~~**A program that wants a picture on the screen has to spend a sprite slot on it, and
   `examples/breakout/sprites/breakout.bas` spends two of its eight on the screen itself.**~~
   **Done.** The listing has been converted with it: no `SSHAPE` of the screen, no `PACE`
   routine hunting for a batch boundary, and both slots free.

   The drawing verbs now render into a layer the frame composites under the text and the
   sprites (`akbasic_graphics_akgl_begin`/`_end`/`_render`, `src/graphics_akgl.c`), so a
   drawing survives the frame it was made in. With `WINDOW` already reachable to shrink the
   text area, the two halves together are what a picture needed: draw it once, uncover it,
   and it stays. `tests/akgl_backends.c` asserts both -- a pixel still present a frame later
   with nothing redrawn, and a pixel below a shrunk text area surviving the text repaint.

   **The layer is transparent where nothing was drawn**, which is not a detail: it covers the
   whole window and composites *underneath*, so an opaque one would black out the frame the
   moment a program issued a single `DRAW`. A fresh SDL target texture's contents are
   undefined, so it is cleared on creation; skipping that put uninitialised memory under the
   first frame's text and looked like a driver bug.

   The remaining half of item 3 -- whether the text layer *should* own the whole window by
   default -- is untouched and is still a real question rather than a defect.

   The original report:

   That listing draws its play field -- walls, sixty stamped bricks, the lettering -- captures
   the whole 800 by 540 region with `SSHAPE`, and installs the capture as sprite 2. It does the
   same for the HUD strip as sprite 1. Its own header calls them "the screen". It reads as a
   silly thing to do and it is not: it is the only thing that works, and it is working around
   the combination of item 3 and item 5 rather than choosing anything.

   Neither defect alone would force it:

   - **Item 3** says the text layer owns every row of the window by default, so a drawing is
     painted over before it is presented. That half is now answerable -- `WINDOW` is reachable
     since §6 item 31, so a program *can* take rows back.
   - **Item 5** is what `WINDOW` does not fix. The frontend never clears and SDL double-buffers,
     so a drawing has to be **re-issued every frame**, and a re-issue has to fit inside one
     256-line batch to survive a present. Breakout's field is sixty `GSHAPE` stamps plus two
     `BOX`es plus a stroke-drawn banner. It does not fit in one batch and never will.

   So the program cannot draw its field every frame, and a drawing it makes once does not
   survive. A sprite is drawn from state the interpreter keeps, every frame, for nothing --
   which is exactly the property the field needs and the only place in the interpreter that
   has it.

   **What it costs, now measured rather than asserted:**

   - **Two of eight sprite slots.** That is the whole reason the bricks are drawn rather than
     made of artwork, why only one gem can fall at a time, and why chapter 18 opens with a
     sprite budget table. Every design compromise in that game traces back here.
   - **4.5% of a frame in collision alone.** Sprite 2's bounding box covers the entire play
     field, so every moving sprite overlaps it permanently and the broad-phase reject can never
     throw those pairs out. `tests/collision_perf.c` has the row: 211.8 ns a scan for that
     layout against 54.9 ns for eight sprites that do not overlap. The game pays it on every
     one of 256 scans a frame to collide with *the backdrop*.
   - **A whole chapter of documentation.** Chapter 18 Step 3 is called "Turn a drawing into a
     sprite" and exists solely to teach the workaround.

   **What would fix it: somewhere to draw that persists and is not a sprite.** A drawing layer
   the sink composites beneath the text layer and the sprites, that a program writes once and
   the frontend does not discard. `GRAPHIC` already selects a mode; the natural shape is for a
   drawing mode to mean "this layer is yours and it stays" rather than "this is thrown away at
   the next present". That is `src/sink_akgl.c` and `src/frontend_akgl.c`, and it is a real
   design decision about what a frame owns rather than a patch -- which is why this is a filed
   item and not a fix.

   Nothing in libakgl 0.9.0 supplies it either -- 0.9.0 is the UI subsystem, and its arena draws
   inside the frame like everything else. There is no render-to-texture layer and no persistent
   surface in `include/akgl/renderer.h`; `frame_start` clears the target, which is the opposite.
   If the answer turns out to want one, it is a §7 filing against `libakgl` rather than
   something to build here.

   **Do not "fix" this by rewriting the game.** The listing is correct for the interpreter it
   was written against, and item 3's own entry says the trick "is still the right one for a
   game; it should not be the *only* way". The game changes when the interpreter gives it
   something better, and chapters 17 and 18 change with it -- see §6 item 39.

**A note on what this list is.** Items 1, 2, 6, 7 and 8 are defects by any reading. Item 3 is a
design consequence nobody chose, and item 9 is what that consequence costs a real program.
Item 4 may be intended and is a documentation gap either way. Item 5 sharpens something already
recorded. None of them is a complaint about the interpreter,
which was a pleasure to write a real program against — they are the kind of thing one real
program finds and a test suite written alongside the implementation structurally cannot,
because the suite tests the constructs the implementer had in mind and a game reaches for
whatever it needs.
