Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
27 KiB
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:
-
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 intomalloc-based data structures. SDL2/SDL2_ttf calls in the Go version becomelibakglcalls (akgl_text_*, the renderer, the registry), not raw SDL3 calls. -
Finish the language. Implement the full Dartmouth BASIC and Commodore 128 BASIC v7 verb and function set.
deps/basicinterpret/README.mdends 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
libakglcannot supply a capability a verb needs, do not work around it here. Add a TODO item todeps/libakgl/TODO.mddescribing the missing API — what the BASIC verb requires, what theakgl_*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.candsrc/text.care both at 0% coverage and there is no mixer-backed audio API yet. Growinglibakglto serve the interpreter is a wanted outcome, not a detour. -
Be embeddable. The end state is the interpreter linking into
libakglas a scripting engine for game authors. That constrains the design from the start:- Build the interpreter as a library target with a thin
main.cdriver 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_NORETURNbelongs only in the driver'smain(). - The interpreter does not own the window, renderer, or game loop. It draws through whatever
akglrenderer the host has already initialized, and a host must be able to step or bound execution rather than surrendering control to arun()that never returns.
- Build the interpreter as a library target with a thin
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.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
<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.1.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 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—BasicScannertokenizes a line intoBasicTokens. Verbs and function names are case-insensitive; variable names are case-sensitive.basicparser.go/basicparser_commands.go— recursive-descent parser producingBasicASTLeafnodes (basicgrammar.godefinesBasicASTLeafTypeand leaf structure). Commands get their own parse paths inbasicparser_commands.go.basicruntime.go—BasicRuntimeowns 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 itsBasicRuntimemethod 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—BasicEnvironmentis a scope and the per-line working state: variables, functions, labels, plus the in-flight state ofIF/FOR/GOSUB/READ. Environments chain viaparentand are pushed/popped withnewEnvironment()/prevEnvironment().basicvalue.go/basicvariable.go—BasicValue(the strongly-typed value) andBasicVariable(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()andPrintln()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 throughakgl) or the wholetests/corpus becomes unusable.
Two design decisions matter most if you are porting this to C:
- Everything is fixed-size and statically allocated.
main.godefines the budget:MAX_LEAVES/MAX_TOKENS32 per environment,MAX_VALUES64,MAX_VARIABLES128,MAX_SOURCE_LINES9999,MAX_LINE_LENGTH256,MAX_ARRAY_DEPTH64. The 32-leaf ceiling is why the README says a line is limited to roughly 16 operations. This lines up with theak*libraries' no-dynamic-allocation rule — the port should keep the pool-allocation model, not replace it withmalloc. waitingForCommanddrives 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 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 withPREPARE_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; useFINISH_NORETURN(e)inmain(). CATCHand theFAIL_*_BREAKmacros expand to a Cbreak, so they must never appear inside a loop or nestedswitchwithin anATTEMPT— thebreakwould escape only the loop and the rest of theATTEMPTwould run with an error pending. Inside loops usePASS/FAIL_*_RETURN, or move the loop into its ownakerr_ErrorContext *helper andCATCHthat single call.- Custom error codes must be reserved with
akerr_reserve_status_range()and named withakerr_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:
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:
#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
libakgldefect 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.