Files
akbasic/CLAUDE.md
Andrew Kesterson eb5374d212 Check the reference corpus in, so the build stops needing the Go submodule
All 41 .bas/.txt pairs copied byte for byte from basicinterpreter@d76162c into
tests/reference/, plus the Commodore font into assets/fonts/ -- the two things
that tied the build to deps/basicinterpret. Both were verified cmp-identical
to the submodule copies.

This reverses a decision that was deliberate and correct at the time: the
corpus was driven in place because copying a submodule's corpus guarantees
drift. Overruled on purpose -- the Go dependency is being deprecated, and a
build that cannot run its acceptance suite without cloning the implementation
it replaced is not finished. tests/reference/README.md records what the drift
now costs and that those expectations are never edited.

Checked rather than assumed: both configurations configure, build and pass
from scratch with deps/basicinterpret moved out of the tree entirely.

The submodule is kept as the behavioural spec, which is a real use. The font
came with an open licence question; assets/fonts/PROVENANCE.md states it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:35:40 -04:00

29 KiB
Raw Blame History

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:

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.2.0 libakgl.so.0.2 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 .gitignored, 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_subdirectorys 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 .gitignored, 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/.

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.goBasicScanner tokenizes a line into BasicTokens. 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.goBasicRuntime 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.goBasicEnvironment 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.goBasicValue (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:

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

Extra harnesses, by library (option prefixes differ per repo):

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

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

#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 #defines: 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.