Split the documentation by who reads it
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 3m2s
akbasic CI Build / sanitizers (push) Successful in 3m51s
akbasic CI Build / coverage (push) Failing after 3m24s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Has been cancelled

README.md was 577 lines and answered four different questions at once: what
the project is, how to build it, every verb and function in the language, and
how to maintain the test harness. The verb and function lists had already been
written a second time in docs/11 and docs/12, which is how a list of that size
goes stale -- there is no way to notice the two have drifted apart.

README.md is now 150 lines and holds only what somebody evaluating the project
needs: what it is, the quickstart, why it was rewritten in C, the five rules
embedding imposes on the design, the two ways to use it, and where everything
else lives. Technical detail goes to docs/, maintenance to MAINTENANCE.md.

The akbasic_TextSink struct moved to docs/10-embedding.md rather than being
deleted. It was the corpus's only `c excerpt=` block -- the check that caught
the stale struct two commits ago -- so dropping it with the README would have
quietly retired a test. docs/10 also stopped claiming README.md carries the
full API surface and the pool limits, which the trim made false.

CLAUDE.md went from 458 lines to 62, because almost none of it was
agent-specific. The project goals, the Go reference and its architecture, the
dependency version and ABI rules, the four ways an embedded build collides,
the libakerror convention, the error-code range map and the style rules are
all things a maintainer needs, and they are now in MAINTENANCE.md with one
copy to keep true. CLAUDE.md points there and keeps only the rules no test
enforces: tests in the same commit asserting the correct contract, file a
missing dependency capability upstream, do not edit generated output or
tests/reference/, co-author your commits.

Four claims did not survive the move, having gone stale where nothing could
notice:

  - "The repository is currently empty apart from its submodules -- no
    commits, no source tree, no build files." There are 43 commits.
  - libakgl's target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE)
    at deps/libakgl/CMakeLists.txt:44, described as inert but present. It is
    gone; only a historical mention in a comment remains.
  - "akbasic_init() claims 512-767." There is no akbasic_init. It is
    akbasic_error_register(), called from akbasic_runtime_init().
  - Time-relative phrasing ("libakgl hit two of them in the last week").

Five places pointed at CLAUDE.md for the range map or the file-it-upstream
rule and now point at MAINTENANCE.md: include/akbasic/error.h,
src/runtime_disk.c and three entries in TODO.md. Both source changes are
comments. deps/libakgl/TODO.md cites it too and is left alone; it is a
submodule, and the rule it quotes is still reachable from CLAUDE.md.

ctest is green at 95 of 95, docs_examples included: 36 programs, 9
transcripts, 44 output comparisons, 3 C snippets, 1 excerpt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 22:59:43 -04:00
parent 342e4c07da
commit 80b76ad467
7 changed files with 747 additions and 1107 deletions

491
CLAUDE.md
View File

@@ -1,74 +1,34 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this
repository.
## What this project is
## Read MAINTENANCE.md first
`akbasic` is a **C rewrite of the Go BASIC interpreter in `deps/basicinterpret`**, written in the same
style as the `ak*` C libraries it builds on.
**[`MAINTENANCE.md`](MAINTENANCE.md) is the authoritative document for changing this project**,
and it is written for whoever has to do the changing — human or agent. It carries what used to
live in this file: the project goals, the Go reference implementation and its architecture, the
dependency versions and their ABI rules, the four ways an embedded build collides, the
`libakerror` error convention, the coordinated error-code range map, the test lists, the
documentation-example harness, and the style and commit rules.
**That Go project is deprecated and will not be updated, and this interpreter is no longer required
to match it.** See `TODO.md` §0.1, which is the first thing to read: it retires the byte-for-byte
fidelity constraint that several sections of that file were written on. The reference's corpus is
checked in at `tests/reference/` and stays green as a *regression* suite; the Go source stays
readable as documentation of what the original did. Neither is binding any more.
There is one copy of all of that, on purpose. Anything you would have added here, add there
instead — the maintainer needs it as much as you do, and a second copy is a second thing to
keep true.
The repository is currently **empty apart from its submodules** — no commits, no source tree, no build
files. The staged content is `.gitmodules` plus four submodules under `deps/`. Everything below the
"Project goals" section describes the material to build from, not code that exists here yet.
`akbasic` is a C rewrite of the Go BASIC interpreter in `deps/basicinterpret`, written in the
idiom of the `ak*` C libraries it builds on, and meant to end up embedded in `libakgl` as a
scripting engine for game authors.
| Submodule | Language | Role |
|---|---|---|
| `deps/basicinterpret` | Go | The implementation being rewritten; behavioral spec for the port (its `go.mod` is already `module akbasic`) |
| `deps/libakerror` | C | TRY/CATCH-style error contexts — the error-handling substrate every other `ak*` library is built on |
| `deps/libakstdlib` | C | libc and data-structure wrappers that report through `libakerror` |
| `deps/libakgl` | C | SDL3-based game/graphics library (sprites, text, tilemaps, actors, heap) — supplies all multimedia |
### Project goals
In priority order:
1. **Port Go → C.** Reproduce the reference interpreter in C, in the idiom of `libakerror` /
`libakstdlib` / `libakgl`: `akerr_ErrorContext *` returns everywhere, fixed-size pools instead of
dynamic allocation, library-prefixed symbols, one-file CTest executables. The Go code is already
written against static pools and explicit state structs, so it ports fairly directly — resist
"improving" it into `malloc`-based data structures. SDL2/SDL2_ttf calls in the Go version become
`libakgl` calls (`akgl_text_*`, the renderer, the registry), not raw SDL3 calls.
2. **Finish the language.** Implement the full Dartmouth BASIC and Commodore 128 BASIC v7 verb and
function set. `deps/basicinterpret/README.md` ends with an explicit list of everything unimplemented
(`SOUND`, `PLAY`, `ENVELOPE`, `VOL`, `SPRITE`, `MOVSPR`, `GRAPHIC`, `DRAW`, `BOX`, `PAINT`,
`WINDOW`, `GETKEY`, `DO`/`LOOP`/`WHILE`/`UNTIL`, `ON`, `TRAP`, the disk verbs, …) — that list is
the work queue. A few entries are deliberately out of scope on a modern PC and marked so there
(`BANK`, `FAST`, `MONITOR`); keep that reasoning rather than reviving them.
**Group H (sprites) is done.** `src/runtime_sprite.c` and `src/sprite_akgl.c`. libakgl's
sprite JSON turned out not to be in the way: `akgl_sprite_load_json()` is a thin wrapper over
four initializers plus writes to public struct fields, and every field it fills from a
document is something a Commodore sprite does not have. The one thing that *did* survive into
the verb syntax is the image path — `SPRSAV "ship.png", 1` goes through `akgl_path_relative()`
and `akgl_spritesheet_initialize()`, the same calls the JSON loader makes. `SPRDEF` is out of
scope with `BANK`/`FAST`/`MONITOR`: it is an interactive editor, not a programmable verb.
**When `libakgl` cannot supply a capability a verb needs, do not work around it here.** Add a TODO
item to `deps/libakgl/TODO.md` describing the missing API — what the BASIC verb requires, what the
`akgl_*` entry point should look like, and what tests would cover it — and follow the numbered,
prose-paragraph style of the existing "Carried over" entries. Audio (`PLAY`/`SOUND`/`ENVELOPE`/
`FILTER`/`VOL`/`TEMPO`) and immediate-mode drawing (`DRAW`/`BOX`/`PAINT`/`CIRCLE`) are the obvious
gaps: `src/draw.c` and `src/text.c` are both at 0% coverage and there is no mixer-backed audio API
yet. Growing `libakgl` to serve the interpreter is a wanted outcome, not a detour.
3. **Be embeddable.** The end state is the interpreter linking *into* `libakgl` as a scripting engine
for game authors. That constrains the design from the start:
- Build the interpreter as a library target with a thin `main.c` driver on top; the REPL, `QUIT`,
and argv handling belong to the driver, not the library.
- Interpreter state lives in explicit structs passed by pointer. No file-scope mutable globals.
- **Nothing in the library may terminate the process.** Errors propagate out as
`akerr_ErrorContext *` for the host game to handle; `FINISH_NORETURN` belongs only in the
driver's `main()`.
- The interpreter does not own the window, renderer, or game loop. It draws through whatever
`akgl` renderer the host has already initialized, and a host must be able to step or bound
execution rather than surrendering control to a `run()` that never returns.
| Read | For |
|---|---|
| [`MAINTENANCE.md`](MAINTENANCE.md) | Everything above. Start here |
| [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence. §0.1 first — it retires the byte-for-byte fidelity constraint several later sections were written on |
| [`README.md`](README.md) | What the project is and why, for somebody who has not seen it |
| [`docs/`](docs/README.md) | The language itself: thirteen chapters, verb and function reference |
| `deps/libakerror/AGENTS.md` | The `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/`FINISH` protocol, authoritatively |
| `deps/libakerror/UPGRADING.md` | 1.0.0's status registry. Required before writing an error code |
| `deps/<library>/AGENTS.md` | Per-repo rules. Read the relevant one **before editing a submodule** |
Before doing anything else in a fresh clone:
@@ -76,382 +36,27 @@ Before doing anything else in a fresh clone:
git submodule update --init --recursive
```
Note that `libakgl` vendors its *own* copies of `libakerror` and `libakstdlib` (plus SDL3, SDL3_image,
SDL3_mixer, SDL3_ttf, jansson, semver) under `deps/libakgl/deps/`. Its `CMakeLists.txt` guards every
dependency with `if(NOT TARGET ...)`, so a top-level build must define `akerror::akerror` and
`akstdlib::akstdlib` from `deps/libakerror` / `deps/libakstdlib` *before* `add_subdirectory(deps/libakgl)`
or the targets will be declared twice.
Doing it in that order is now load-bearing for a second reason: `deps/libakerror` is at **1.0.0**,
which is a source and ABI break with an soname (`libakerror.so.1`). `libakstdlib` and `libakgl` must be
compiled against that header, not a pre-1.0.0 one, and an installed `libakerror.so.0` on the system
must not be picked up. Build everything from the submodules in one tree. `libakgl`'s
`target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE=256)` at
`deps/libakgl/CMakeLists.txt:44` refers to a macro 1.0.0 deleted — it is inert, and it should go when
that repo is next touched. See the error-code section below for a live collision this upgrade exposed.
### Dependency versions and what they promise
| Submodule | Version | soname | ABI rule | Version API |
|---|---|---|---|---|
| `deps/libakerror` | 1.0.0 | `libakerror.so.1` | major only | **none** — no version macro, no `ConfigVersion` file |
| `deps/libakstdlib` | 0.2.0 | `libakstdlib.so.0.2` | **`MAJOR.MINOR` while major is 0** | `AKSL_VERSION_*`, `aksl_version()`, `AKSL_VERSION_CHECK()` |
| `deps/libakgl` | 0.4.0 | `libakgl.so.0.4` | **`MAJOR.MINOR` while major is 0** | `AKGL_VERSION*`, `akgl_version()`, `AKGL_VERSION_AT_LEAST()` |
For both 0.x libraries the soname carries `MAJOR.MINOR` deliberately: 0.1 and 0.2 are *different*
ABIs, and both become major-only at 1.0. Do not read `0.1 → 0.2` as a compatible bump — and both
libraries have now actually made that jump, so anything built against the 0.1 headers must be
rebuilt rather than relinked.
**The two moved together and had to.** `libakgl` 0.2.0 consumes the `libakstdlib` 0.2.0 API, so a
tree that pins `libakstdlib` at 0.1.0 and adds `libakgl` by `add_subdirectory()` compiles
0.2.0-era code against 0.1.0 headers and fails on `aksl_fwrite`, `aksl_fread` and
`aksl_realpath`. Bump them as a pair.
`libakstdlib` 0.1.0 added a version API worth using, and it still applies at 0.2.0. `akstdlib.h` now pulls in
`<akstdlib_version.h>`, which is **generated into the build tree** by CMake and installed beside
`akstdlib.h` — so link `akstdlib::akstdlib` and let the target carry its include directories; hand-adding
`deps/libakstdlib/include` to an include path gets you a missing-header error. It defines
`AKSL_VERSION_MAJOR`/`MINOR`/`PATCH`/`STRING`/`NUMBER` and `AKSL_VERSION_SONAME`, recording what the
caller was *compiled* against, and the library exposes `aksl_version()`, `aksl_version_string()` and
`aksl_version_soname()` for what actually *loaded*. `AKSL_VERSION_CHECK()` compares them and raises
`AKERR_VALUE` naming both. **Call it once during initialization** — the soname normally catches a
mismatch at load time, but a 0.2.0 dropped in under the 0.1 filename loads happily and only the check
notices. It is a macro on purpose: it must expand at your call site to capture your numbers.
`libakgl` 0.1.0 followed the same pattern one commit later, for the same reason: it had `AKGL_VERSION`
hand-maintained in `game.h` and no `VERSION` on `project()` at all, so `akgl.pc` shipped an empty
`Version:` and the `.so` had no soname. Now `project(akgl VERSION 0.2.0)` is the single source and
drives the generated `include/akgl/version.h` — which is `.gitignore`d, because `include/` precedes the
build tree on the include path and a stray copy there would shadow the generated one and pin every
consumer. It publishes `AKGL_VERSION_AT_LEAST(major, minor, patch)`, the compile-time test
`libakstdlib` could not write against `libakerror`, and `akgl_version()` for what actually loaded.
`libakstdlib` also states, and tests, that it defines no status codes and reserves no range. That is
why it does not appear as an owner in the range map below.
**Version-pinning in `find_package` is asymmetric, and that is deliberate.** `find_package(akstdlib 0.1)`
and `find_package(akgl 0.1)` both work — each ships a `ConfigVersion.cmake` at `SameMinorVersion`,
mirroring its soname. `find_package(akerror 1.0)` **fails against a correct install**, because
`libakerror` ships `akerrorConfig.cmake` and `akerrorTargets.cmake` but no
`akerrorConfigVersion.cmake`. Ask for `akerror` unversioned. Its floor is enforced instead by an
`#error` feature-testing `AKERR_FIRST_CONSUMER_STATUS`, which both `akstdlib.h` and `akgl/error.h`
now carry — include either and you inherit the guard. The missing version file is filed in both
`deps/libakstdlib/TODO.md` §2.3 and `deps/libakgl`'s history; fix it upstream, then add the `1.0` floor
to the `find_dependency` calls.
### Embedded builds collide on test names, target names and coverage data
All three libraries register their CTest tests unconditionally and add coverage/mutation targets. A
top-level `akbasic` build that `add_subdirectory`s all three walks into four separate collisions.
`libakgl` hit two of them in the last week and its fixes are the ones to copy.
**1. Duplicate CTest entries.** Every dependency's tests register into *our* suite. Pulled in
`EXCLUDE_FROM_ALL` their binaries are never built, so each shows up as "Not Run" and fails. CMake
offers no way to un-register a test and `set_tests_properties` cannot reach across directory scopes.
`libakstdlib` solved it by shadowing `add_test` and `set_tests_properties` for the duration of the
`add_subdirectory()` call — `deps/libakstdlib/CMakeLists.txt:104-150`. Note that its shadow only arms
when *it* is top-level, so it does nothing for us: **we need our own, around all three
`add_subdirectory()` calls**, not just `libakerror`'s.
**2. Duplicate test *target* names.** `add_subdirectory` creates a dependency's targets even under
`EXCLUDE_FROM_ALL`. When `libakstdlib` 0.1.0 added `tests/test_version.c`, `libakgl`'s own
`test_version` stopped the configure dead:
```
add_executable cannot create target "test_version" because another target with the same name
already exists.
```
`libakgl` fixed it by building its programs as `akgl_test_<name>` while keeping the bare CTest names
(`ctest -R version` still works). `libakstdlib` still uses bare `test_<name>` targets. **Name every
test target in this repo `akbasic_test_<name>`** — it costs nothing and it is the collision that
actually stopped a build.
**3. Duplicate custom targets.** `libakerror` namespaces its `mutation` target when embedded but
**not** its `coverage` target (`deps/libakerror/CMakeLists.txt:194` vs `:172`), so any
coverage-enabled top-level build fails with *"another target with the same name already exists"*.
Shadow `add_custom_target` and rename that one, as `deps/libakstdlib/CMakeLists.txt:134-141` does.
`libakstdlib` (both targets) and `libakgl` (its `mutation` target) namespace themselves correctly.
**The real fix is upstream in `libakerror`** — the same
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test it already applies to `mutation` — and it is
filed in `deps/libakstdlib/TODO.md` §2.3. Delete the workaround when it lands.
**4. Stale build trees poison the coverage report.** `libakgl` defect #13: `gcovr` searches for
`.gcda`/`.gcno` under `--root`, which its `CMakeLists.txt:208`/`:223` set to the *source* directory, so
every instrumented build tree left in the source dir is folded into the report. `--object-directory`
does not narrow that search. A leftover `build-coverage/` makes a freshly configured run fail in
`coverage_reset`, before any test executes, with `Got function ... on multiple lines`. Because
`build*/` is `.gitignore`d, the state is easy to reach and hard to see. **Keep build trees out of the
source directory** — the `cmake -S . -B build` convention above already does, and this is the reason
to hold to it.
## Reference implementation (`deps/basicinterpret`)
This is a Commodore BASIC 7.0 / Dartmouth BASIC dialect written in Go with SDL2 + SDL2_ttf. It is the
behavioral spec: when a question about semantics comes up ("what does `READ` do when it runs off the
end of `DATA`?"), the answer lives in this code and in `tests/`.
```sh
cd deps/basicinterpret
make # builds ./basic (CGO_ENABLED=1, needs SDL2 + SDL2_ttf dev packages)
./basic # interactive REPL
./basic tests/language/functions.bas
make tests # or: bash ./test.sh
```
`test.sh` is a golden-file harness: for every `tests/**/*.bas` it runs the interpreter and md5-compares
stdout against the sibling `.txt`. The exit code is the number of failures. To run a single case,
just run the interpreter on that `.bas` and diff against its `.txt`.
That corpus is the acceptance suite for the rewrite: the C interpreter reproduces the same stdout
for the same `.bas` files. **It has been copied into this repository at `tests/reference/`** and no
longer runs out of the submodule — see `tests/reference/README.md` for the provenance and for the
rule that those expectations are never edited to suit this interpreter. It sits alongside the
per-module CTest executables the `ak*` style calls for; the two answer different questions, and new
language features from goal 2 need a `.bas`/`.txt` pair in `tests/language/` as well as unit tests.
**`deps/basicinterpret` is no longer a build or test dependency.** Both configurations were
configured, built and run from scratch with it moved out of the tree. It is kept only as the
behavioural spec to read when a question about semantics comes up — which is a real use, and the
reason it has not been removed. The Commodore font it also supplied now lives in `assets/fonts/`.
`deps/basicinterpret/README.md` is the language reference — implemented verbs, functions, variable
type suffixes, and an explicit list of what is *not* implemented. Read it before adding or porting
language features.
### Architecture of the reference interpreter
Scanner → parser → tree-walking runtime, one source line at a time:
- `basicscanner.go``BasicScanner` tokenizes a line into `BasicToken`s. Verbs and function names are
case-insensitive; variable names are case-sensitive.
- `basicparser.go` / `basicparser_commands.go` — recursive-descent parser producing `BasicASTLeaf`
nodes (`basicgrammar.go` defines `BasicASTLeafType` and leaf structure). Commands get their own
parse paths in `basicparser_commands.go`.
- `basicruntime.go``BasicRuntime` owns the program source (`[MAX_SOURCE_LINES]BasicSourceLine`),
the variable pool, the scanner/parser, the environment stack, and the SDL window. `evaluate()` walks
the AST; `commandByReflection()` dispatches a verb to its `BasicRuntime` method by name, so adding a
verb is largely a matter of adding a correctly-named method.
- `basicruntime_commands.go` / `basicruntime_functions.go` — the verb and function implementations.
- `basicenvironment.go``BasicEnvironment` is a scope *and* the per-line working state: variables,
functions, labels, plus the in-flight state of `IF`/`FOR`/`GOSUB`/`READ`. Environments chain via
`parent` and are pushed/popped with `newEnvironment()` / `prevEnvironment()`.
- `basicvalue.go` / `basicvariable.go``BasicValue` (the strongly-typed value) and `BasicVariable`
(a named slot plus array metadata). Type is carried by the identifier suffix: `#` integer,
`%` float, `$` string.
- `basicruntime_graphics.go` — output goes through an SDL text surface with a cursor, wrapped text,
scrolling and a print buffer. `Write()` and `Println()` **mirror every line to stdout** as well as
to the surface; that mirror is the only reason the golden-file suite works, so the C port needs an
equivalent (a text sink the driver can point at stdout while the embedded case renders through
`akgl`) or the whole `tests/` corpus becomes unusable.
Two design decisions matter most if you are porting this to C:
1. **Everything is fixed-size and statically allocated.** `main.go` defines the budget:
`MAX_LEAVES`/`MAX_TOKENS` 32 per environment, `MAX_VALUES` 64, `MAX_VARIABLES` 128,
`MAX_SOURCE_LINES` 9999, `MAX_LINE_LENGTH` 256, `MAX_ARRAY_DEPTH` 64. The 32-leaf ceiling is why
the README says a line is limited to roughly 16 operations. This lines up with the `ak*` libraries'
no-dynamic-allocation rule — the port should keep the pool-allocation model, not replace it with `malloc`.
2. **`waitingForCommand` drives block structure.** Rather than building nested blocks in the AST, the
environment records a verb it is skipping forward to (`NEXT`, `RETURN`, …) and suppresses execution
of intervening lines until it sees it. Loops and branches evaluate their condition at the *bottom*
of the structure. Any reimplementation has to reproduce this or restructure control flow deliberately.
Runtime modes are `MODE_REPL`, `MODE_RUN`, `MODE_RUNSTREAM` (piped input), `MODE_QUIT`; `BASIC_TRUE`
is `-1` and `BASIC_FALSE` is `0`, per Commodore convention.
## Working in the `ak*` C libraries
Each of the three C libraries carries its own `AGENTS.md` (`CLAUDE.md` in them just points at it) with
authoritative per-repo rules. Read the relevant one before editing a submodule. The shared shape:
```sh
cmake -S . -B build # out-of-tree, always
cmake --build build --parallel
ctest --test-dir build --output-on-failure
ctest --test-dir build -R <name> --output-on-failure # single test
```
**Optional test tooling for this repository.** `xdotool` and `script`(1) drive the `akgl_typing`
test, which types at a real focused SDL window -- the only test that covers the path upstream of
SDL, and the one that would have caught the missing `SDL_StartTextInput()`. It needs a real X
server, steals keyboard focus for about fifteen seconds, and skips itself (CTest `Skipped`, not
a failure) when it cannot run. `AKBASIC_SKIP_INTERACTIVE=1` skips it deliberately.
Extra harnesses, by library (option prefixes differ per repo):
```sh
cmake -S . -B build-asan -DAKSL_SANITIZE=ON # libakstdlib: ASan + UBSan
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON # libakstdlib
cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug # libakgl (needs gcovr)
cmake --build build --target coverage
cmake --build build --target mutation # slow; scripts/mutation_test.py for a subset
```
`libakerror/test.sh` shows the full CI gate for that library (ctest + mutation ≥65 + coverage ≥90 line
/ ≥50 branch). `rebuild.sh` in `libakgl` and `libakstdlib` installs into the developer-specific prefix
`/home/andrew/local` and deletes build directories — use the portable commands above unless that exact
behavior is wanted.
Tests are one-file C programs registered in a list in `CMakeLists.txt`. `libakstdlib` splits them three
ways and **two of the lists invert the meaning of "Passed"**: `AKSL_TESTS` must exit 0;
`AKSL_WILL_FAIL_TESTS` abort by design; `AKSL_KNOWN_FAILING_TESTS` assert the *correct* contract for a
defect documented in `TODO.md` and are expected to fail. A green `ctest` run therefore does not mean
defect-free. When a known-failing test starts passing, CTest reports "unexpectedly passed" — that is the
cue to move it into `AKSL_TESTS` along with the fix. `libakerror` has the same split
(`AKERR_TESTS` / `AKERR_WILL_FAIL_TESTS`).
That split *used* to be load-bearing for `libakstdlib`, and no longer is: 0.2.0 fixed all six of the
confirmed defects its `TODO.md` §2.1 listed, folded the four known-failing tests back into the
suites for the things they test, and left `AKSL_KNOWN_FAILING_TESTS` empty. The `aksl_ato*` family
now raises `AKERR_VALUE` on no digits or trailing junk and `ERANGE` on overflow, the linked-list
functions no longer truncate, `AKERR_ITERATOR_BREAK` stops a tree traversal, and `aksl_realpath` no
longer reads uninitialised memory.
`TODO.md` §1.9 in this repo is still the place to check before reaching into `akstdlib.h`, but it
is now a record of what changed rather than a list of bans. One caveat survives the upgrade:
`aksl_strhash_djb2` still sign-extends `char`, so a high-bit byte hashes differently from the
`unsigned char` answer — see §1.3, which explains why the symbol tables cannot reach it.
The branch-coverage gate there was re-ratcheted 45 → 40 after the 1.0.0 bump, because the new
`PREPARE_ERROR`/`FAIL_*` macros expand to more branches per call site — that is a denominator change,
not a regression, and the same effect will show up in this repo's numbers.
## The libakerror error convention
New C code in this repo should follow it — it is what `libakstdlib` and `libakgl` both do, and mixing
conventions breaks error propagation.
- Any function that can fail returns `akerr_ErrorContext AKERR_NOIGNORE *` and opens with `PREPARE_ERROR(e);`.
- Return with `SUCCEED_RETURN(e)` / `FAIL_RETURN(...)`, never a bare status code.
- Propagate with `PASS(e, some_call());` when there is nothing local to do.
- Handle locally with `ATTEMPT { CATCH(e, call()); } CLEANUP { } PROCESS(e) { } HANDLE(e, AKERR_X) { } FINISH(e, true);`
`FINISH(e, true)` re-raises unhandled errors to the caller; use `FINISH_NORETURN(e)` in `main()`.
- **`CATCH` and the `FAIL_*_BREAK` macros expand to a C `break`**, so they must never appear inside a
loop or nested `switch` within an `ATTEMPT` — the `break` would escape only the loop and the rest of
the `ATTEMPT` would run with an error pending. Inside loops use `PASS` / `FAIL_*_RETURN`, or move the
loop into its own `akerr_ErrorContext *` helper and `CATCH` that single call.
- Custom error codes must be **reserved** with `akerr_reserve_status_range()` and **named** with
`akerr_register_status_name()` during initialization — see the allocation section below before
defining any.
- Unhandled errors that reach the top level abort the process with a stack trace. That is the intended
behavior for the standalone driver — but see goal 3: the interpreter *library* must not rely on it.
### Error-code allocation across libraries
**`libakerror` 1.0.0 changed this completely.** The hazard that used to live in this section — a
consumer-sized `__AKERR_ERROR_NAMES` array, a mismatched `AKERR_MAX_ERR_VALUE` corrupting BSS, and no
authority to allocate from — is gone. Read `deps/libakerror/UPGRADING.md` before writing any error
code; the short version follows.
**What 1.0.0 removed.** These no longer exist. Any code, compile definition, or documentation
referring to them is stale:
| Removed | Replacement |
|---|---|
| `AKERR_MAX_ERR_VALUE` | nothing — the name registry is sparse and takes any `int` |
| `__AKERR_ERROR_NAMES` | `akerr_name_for_status()` (the table is private to the library now) |
| `AKERR_STATUS_RANGE_OK` / `AKERR_STATUS_NAME_OK` | success is a `NULL` `akerr_ErrorContext *`, like everything else |
It is also a **source and ABI break** carrying an soname (`libakerror.so.1`). Anything built against a
pre-1.0.0 header must be rebuilt — including `libakstdlib` and `libakgl`. A stale
`-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless; delete it when you see it.
**How allocation works now.** Values `0``255` belong to `libakerror` (the host errno space plus the
`AKERR_*` codes), reserved by `akerr_init()` under the owner string `"libakerror"`. Consumers allocate
from `AKERR_FIRST_CONSUMER_STATUS` (256) upward, as **absolute integer constants** — never as offsets
from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno cannot move them. Every library that can
coexist in one process reserves its range at init:
```c
akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner, int status, const char *name);
```
Both return `akerr_ErrorContext *` and are `AKERR_NOIGNORE`, so a collision is an ordinary error you
`CATCH`, `HANDLE`, or `PASS` out of your init function. Ownership is **enforced**: naming a status in
someone else's range fails with `AKERR_STATUS_NAME_FOREIGN`, and naming one nobody reserved fails with
`AKERR_STATUS_NAME_UNRESERVED`. You do not need to call `akerr_init()` first — every registry entry
point calls it for you, and (unlike pre-1.0.0) it no longer clears reservations made before it ran.
Repeating an identical reservation for the same owner is a no-op; a subset or superset of your own
range is not, so reserve the whole thing in one call.
**The coordinated range map for this dependency stack.** The library cannot enumerate its consumers,
so this table is the coordination — keep it current, and reserve the whole 256 even where fewer codes
are used, so a later addition does not need a second reservation:
| Owner string | Range | Status |
|---|---|---|
| `"libakerror"` | 0 255 | reserved by `akerr_init()`; do not touch |
| *(none)* | — | `libakstdlib` deliberately reserves nothing and defines no codes of its own — it raises `AKERR_*` and propagates `errno`, both inside the reserved band. Its `tests/test_status_registry.c` pins that as a contract, so nobody has to coordinate with it. |
| `"libakgl"` | 256 260 | reserved by `akgl_error_init()`; `AKGL_ERR_BASE``AKGL_ERR_LIMIT - 1`, five codes |
| *(free)* | 261 511 | headroom for `libakgl` to grow into; do not claim it |
| `"akbasic"` | 512 767 | ours; `AKBASIC_ERR_BASE` is 512 |
**Every owner reserves at init, and each does it in its own initializer.** `akerr_init()` claims
0255 for itself. `akgl_error_init()` (`deps/libakgl/src/error.c`) claims
`AKGL_ERR_BASE` = `AKERR_FIRST_CONSUMER_STATUS` = 256 through `AKGL_ERR_LIMIT - 1` = 260 and names all
five codes. `libakstdlib` claims nothing. `akbasic_init()` claims 512767.
`akgl_error_init()` has an ordering requirement worth knowing before you wire up `main()`: it must run
before anything else in `libakgl`, because a code raised before it registers carries no name into its
stack trace. `akgl_game_init()` calls it as its first statement, but a program driving subsystems
directly — which is what the embedded interpreter does — has to call it itself. It is idempotent, so
calling it more than once is fine; what is *not* fine is a subset or superset reservation, which
`libakerror` refuses.
**What `akbasic` does.** Reserve and name in `akbasic_init()`, and let failures propagate:
```c
#define AKBASIC_OWNER "akbasic"
enum {
AKBASIC_ERR_BASE = 512, /* AKERR_FIRST_CONSUMER_STATUS + 256; see the map above */
AKBASIC_ERR_SYNTAX = AKBASIC_ERR_BASE,
AKBASIC_ERR_TYPE,
/* ... */
AKBASIC_ERR_LIMIT = AKBASIC_ERR_BASE + 256
};
```
Use an `enum`, not a chain of `#define`s: the values stay compile-time integer constants, so `HANDLE`
still works (its `case` labels require that), and adding a code does not mean editing an arithmetic
offset. Reserve `AKBASIC_ERR_LIMIT - AKBASIC_ERR_BASE` in one call.
**What is still not solved, and what it means for us.** `libakerror`'s own `TODO.md` §2 says it plainly:
ownership enforcement covers *naming*, which is the part the library mediates. It cannot detect two
components compiling the same integer into a `HANDLE` `case` label without ever registering a name —
that never reaches the registry. §3 adds that there is no way to ask who owns a status or to enumerate
reservations, so the coordination advice above has no tooling behind it yet; the table in this file is
the tooling. Two practical rules follow:
- **Always reserve, even for codes you never name.** Reservation is the only thing that makes a
collision visible at all.
- **Never define an error code as an offset from another library's symbol.** The `libakgl` defect above
is exactly what that produces.
Capacity is not a concern here: the name registry holds 3072 entries and `akerr_init()` consumes about
150, and reservations cap at 64 ranges. Both are `PRIVATE` to the `libakerror` target and invisible to
consumers — raising them is a `libakerror` configure-time decision, not something `akbasic` sets.
Registration is **not thread safe**; do it during single-threaded init, before the host game spawns
anything.
`libakgl` adds a hard rule of its own: **avoid dynamic memory allocation.** Obtain objects from
`akgl_heap_next_*` and release them back; if no heap layer exists for a needed type, add one rather
than calling `malloc`.
## Style and commits
C is C99-compatible, 4-space indent, braces on the next line for function bodies, spaces inside
control-flow parens. Match the surrounding file — several files mix tabs and spaces, and there is no
repo-wide formatter, so avoid unrelated formatting churn. Public symbols take a library prefix
(`akerr_`, `aksl_`, `akgl_`) with `akerr_TypeName`-style types and `AKGL_UPPER_SNAKE_CASE` macros.
Since this interpreter is meant to link into `libakgl` (goal 3), it needs its own non-colliding
prefix in the same shape — `akbasic_` for functions, `akbasic_TypeName` for types, `AKBASIC_` for
macros and constants — and matching header/source pairs named by feature, as in `src/scanner.c` /
`include/akbasic/scanner.h`.
Commit subjects are short, imperative, sentence case ("Fix refcount leak and stack-trace buffer
overflow"). `libakgl`'s `AGENTS.md` requires agents to add themselves (program, model, version) as a
commit co-author. Never edit generated output — `build/` trees, generated `akerror.h`, `akgl.pc`,
`include/akgl/SDL_GameControllerDB.h`; change the template or generator script instead.
## Rules that are on you rather than on a test
Most of the conventions in `MAINTENANCE.md` are enforced by something — a sorted-table test, a
`WILL_FAIL` list, a Doxygen gate, a byte-compared corpus. These are not, so they are worth
repeating where you will see them:
- **Add tests in the same commit as the behaviour change**, and assert the *correct* contract
even where the code is currently wrong. A known-failing test goes in
`AKBASIC_KNOWN_FAILING_TESTS` with a `TODO.md` entry; it does not get pinned to the buggy
behaviour, because that turns the eventual fix into a test failure.
- **Never work around a missing dependency capability here.** File it in that repository's
`TODO.md` — what the BASIC verb requires, what the entry point should look like, what tests
would cover it. `MAINTENANCE.md` explains why, and names the four gaps this closed upstream.
- **Never edit generated output** — `build/` trees, the generated `akerror.h`, `akgl.pc`,
`include/akgl/SDL_GameControllerDB.h`. Change the template or the generator script.
- **Do not reformat code you are not otherwise changing.** Several files mix tabs and spaces
and there is no repo-wide formatter; style conversions get their own commit.
- **Do not edit `tests/reference/`.** Those expectations came from the Go implementation and
are never edited to suit this interpreter. A deliberate divergence goes in `TODO.md` and
`docs/13-differences.md`.
- **Update `TODO.md` when you learn something about a defect**, including that it is worse or
better than recorded. Publishing a problem you cannot fix yet is a contribution.
- **Add yourself — program, model and version — as a commit co-author.** `libakgl`'s
`AGENTS.md` requires it and this repository follows the same rule.