# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this project is `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. 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. | 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. **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. Before doing anything else in a fresh clone: ```sh 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.1.0 | `libakstdlib.so.0.1` | **`MAJOR.MINOR` while major is 0** | `AKSL_VERSION_*`, `aksl_version()`, `AKSL_VERSION_CHECK()` | | `deps/libakgl` | 0.1.0 | `libakgl.so.0.1` | **`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. `libakstdlib` 0.1.0 added a version API worth using. `akstdlib.h` now pulls in ``, 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.1.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_` while keeping the bare CTest names (`ctest -R version` still works). `libakstdlib` still uses bare `test_` targets. **Name every test target in this repo `akbasic_test_`** — 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 natural acceptance suite for the rewrite: the C interpreter should reproduce the same stdout for the same `.bas` files. Carry it over (or drive it in place) 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 as well as unit tests. `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 --output-on-failure # single test ``` 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 is not academic for `libakstdlib`: `deps/libakstdlib/TODO.md` §2.1 lists six **confirmed** defects, four with a known-failing test apiece. Several are in functions this port would otherwise reach for by default — the `aksl_ato*` family cannot report a conversion failure at all, and the linked-list functions truncate. **Read §2.1 before calling anything in `akstdlib.h` for the first time**; `TODO.md` §1.9 in this repo records which calls are cleared for use here and which are not. 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 0–255 for itself. `akgl_error_init()` (`deps/libakgl/src/error.c`, new in libakgl 0.1.0) 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 512–767. `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.