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>
438 lines
29 KiB
Markdown
438 lines
29 KiB
Markdown
# 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.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 `.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
|
||
```
|
||
|
||
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
|
||
0–255 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 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.
|