Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
458 lines
30 KiB
Markdown
458 lines
30 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.
|
||
|
||
**That Go project is deprecated and will not be updated, and this interpreter is no longer required
|
||
to match it.** See `TODO.md` §0.1, which is the first thing to read: it retires the byte-for-byte
|
||
fidelity constraint that several sections of that file were written on. The reference's corpus is
|
||
checked in at `tests/reference/` and stays green as a *regression* suite; the Go source stays
|
||
readable as documentation of what the original did. Neither is binding any more.
|
||
|
||
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.
|
||
|
||
**Group H (sprites) is done.** `src/runtime_sprite.c` and `src/sprite_akgl.c`. libakgl's
|
||
sprite JSON turned out not to be in the way: `akgl_sprite_load_json()` is a thin wrapper over
|
||
four initializers plus writes to public struct fields, and every field it fills from a
|
||
document is something a Commodore sprite does not have. The one thing that *did* survive into
|
||
the verb syntax is the image path — `SPRSAV "ship.png", 1` goes through `akgl_path_relative()`
|
||
and `akgl_spritesheet_initialize()`, the same calls the JSON loader makes. `SPRDEF` is out of
|
||
scope with `BANK`/`FAST`/`MONITOR`: it is an interactive editor, not a programmable verb.
|
||
|
||
**When `libakgl` cannot supply a capability a verb needs, do not work around it here.** Add a TODO
|
||
item to `deps/libakgl/TODO.md` describing the missing API — what the BASIC verb requires, what the
|
||
`akgl_*` entry point should look like, and what tests would cover it — and follow the numbered,
|
||
prose-paragraph style of the existing "Carried over" entries. Audio (`PLAY`/`SOUND`/`ENVELOPE`/
|
||
`FILTER`/`VOL`/`TEMPO`) and immediate-mode drawing (`DRAW`/`BOX`/`PAINT`/`CIRCLE`) are the obvious
|
||
gaps: `src/draw.c` and `src/text.c` are both at 0% coverage and there is no mixer-backed audio API
|
||
yet. Growing `libakgl` to serve the interpreter is a wanted outcome, not a detour.
|
||
|
||
3. **Be embeddable.** The end state is the interpreter linking *into* `libakgl` as a scripting engine
|
||
for game authors. That constrains the design from the start:
|
||
- Build the interpreter as a library target with a thin `main.c` driver on top; the REPL, `QUIT`,
|
||
and argv handling belong to the driver, not the library.
|
||
- Interpreter state lives in explicit structs passed by pointer. No file-scope mutable globals.
|
||
- **Nothing in the library may terminate the process.** Errors propagate out as
|
||
`akerr_ErrorContext *` for the host game to handle; `FINISH_NORETURN` belongs only in the
|
||
driver's `main()`.
|
||
- The interpreter does not own the window, renderer, or game loop. It draws through whatever
|
||
`akgl` renderer the host has already initialized, and a host must be able to step or bound
|
||
execution rather than surrendering control to a `run()` that never returns.
|
||
|
||
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.4.0 | `libakgl.so.0.4` | **`MAJOR.MINOR` while major is 0** | `AKGL_VERSION*`, `akgl_version()`, `AKGL_VERSION_AT_LEAST()` |
|
||
|
||
For both 0.x libraries the soname carries `MAJOR.MINOR` deliberately: 0.1 and 0.2 are *different*
|
||
ABIs, and both become major-only at 1.0. Do not read `0.1 → 0.2` as a compatible bump — and both
|
||
libraries have now actually made that jump, so anything built against the 0.1 headers must be
|
||
rebuilt rather than relinked.
|
||
|
||
**The two moved together and had to.** `libakgl` 0.2.0 consumes the `libakstdlib` 0.2.0 API, so a
|
||
tree that pins `libakstdlib` at 0.1.0 and adds `libakgl` by `add_subdirectory()` compiles
|
||
0.2.0-era code against 0.1.0 headers and fails on `aksl_fwrite`, `aksl_fread` and
|
||
`aksl_realpath`. Bump them as a pair.
|
||
|
||
`libakstdlib` 0.1.0 added a version API worth using, and it still applies at 0.2.0. `akstdlib.h` now pulls in
|
||
`<akstdlib_version.h>`, which is **generated into the build tree** by CMake and installed beside
|
||
`akstdlib.h` — so link `akstdlib::akstdlib` and let the target carry its include directories; hand-adding
|
||
`deps/libakstdlib/include` to an include path gets you a missing-header error. It defines
|
||
`AKSL_VERSION_MAJOR`/`MINOR`/`PATCH`/`STRING`/`NUMBER` and `AKSL_VERSION_SONAME`, recording what the
|
||
caller was *compiled* against, and the library exposes `aksl_version()`, `aksl_version_string()` and
|
||
`aksl_version_soname()` for what actually *loaded*. `AKSL_VERSION_CHECK()` compares them and raises
|
||
`AKERR_VALUE` naming both. **Call it once during initialization** — the soname normally catches a
|
||
mismatch at load time, but a 0.2.0 dropped in under the 0.1 filename loads happily and only the check
|
||
notices. It is a macro on purpose: it must expand at your call site to capture your numbers.
|
||
|
||
`libakgl` 0.1.0 followed the same pattern one commit later, for the same reason: it had `AKGL_VERSION`
|
||
hand-maintained in `game.h` and no `VERSION` on `project()` at all, so `akgl.pc` shipped an empty
|
||
`Version:` and the `.so` had no soname. Now `project(akgl VERSION 0.2.0)` is the single source and
|
||
drives the generated `include/akgl/version.h` — which is `.gitignore`d, because `include/` precedes the
|
||
build tree on the include path and a stray copy there would shadow the generated one and pin every
|
||
consumer. It publishes `AKGL_VERSION_AT_LEAST(major, minor, patch)`, the compile-time test
|
||
`libakstdlib` could not write against `libakerror`, and `akgl_version()` for what actually loaded.
|
||
|
||
`libakstdlib` also states, and tests, that it defines no status codes and reserves no range. That is
|
||
why it does not appear as an owner in the range map below.
|
||
|
||
**Version-pinning in `find_package` is asymmetric, and that is deliberate.** `find_package(akstdlib 0.1)`
|
||
and `find_package(akgl 0.1)` both work — each ships a `ConfigVersion.cmake` at `SameMinorVersion`,
|
||
mirroring its soname. `find_package(akerror 1.0)` **fails against a correct install**, because
|
||
`libakerror` ships `akerrorConfig.cmake` and `akerrorTargets.cmake` but no
|
||
`akerrorConfigVersion.cmake`. Ask for `akerror` unversioned. Its floor is enforced instead by an
|
||
`#error` feature-testing `AKERR_FIRST_CONSUMER_STATUS`, which both `akstdlib.h` and `akgl/error.h`
|
||
now carry — include either and you inherit the guard. The missing version file is filed in both
|
||
`deps/libakstdlib/TODO.md` §2.3 and `deps/libakgl`'s history; fix it upstream, then add the `1.0` floor
|
||
to the `find_dependency` calls.
|
||
|
||
### Embedded builds collide on test names, target names and coverage data
|
||
|
||
All three libraries register their CTest tests unconditionally and add coverage/mutation targets. A
|
||
top-level `akbasic` build that `add_subdirectory`s all three walks into four separate collisions.
|
||
`libakgl` hit two of them in the last week and its fixes are the ones to copy.
|
||
|
||
**1. Duplicate CTest entries.** Every dependency's tests register into *our* suite. Pulled in
|
||
`EXCLUDE_FROM_ALL` their binaries are never built, so each shows up as "Not Run" and fails. CMake
|
||
offers no way to un-register a test and `set_tests_properties` cannot reach across directory scopes.
|
||
`libakstdlib` solved it by shadowing `add_test` and `set_tests_properties` for the duration of the
|
||
`add_subdirectory()` call — `deps/libakstdlib/CMakeLists.txt:104-150`. Note that its shadow only arms
|
||
when *it* is top-level, so it does nothing for us: **we need our own, around all three
|
||
`add_subdirectory()` calls**, not just `libakerror`'s.
|
||
|
||
**2. Duplicate test *target* names.** `add_subdirectory` creates a dependency's targets even under
|
||
`EXCLUDE_FROM_ALL`. When `libakstdlib` 0.1.0 added `tests/test_version.c`, `libakgl`'s own
|
||
`test_version` stopped the configure dead:
|
||
|
||
```
|
||
add_executable cannot create target "test_version" because another target with the same name
|
||
already exists.
|
||
```
|
||
|
||
`libakgl` fixed it by building its programs as `akgl_test_<name>` while keeping the bare CTest names
|
||
(`ctest -R version` still works). `libakstdlib` still uses bare `test_<name>` targets. **Name every
|
||
test target in this repo `akbasic_test_<name>`** — it costs nothing and it is the collision that
|
||
actually stopped a build.
|
||
|
||
**3. Duplicate custom targets.** `libakerror` namespaces its `mutation` target when embedded but
|
||
**not** its `coverage` target (`deps/libakerror/CMakeLists.txt:194` vs `:172`), so any
|
||
coverage-enabled top-level build fails with *"another target with the same name already exists"*.
|
||
Shadow `add_custom_target` and rename that one, as `deps/libakstdlib/CMakeLists.txt:134-141` does.
|
||
`libakstdlib` (both targets) and `libakgl` (its `mutation` target) namespace themselves correctly.
|
||
**The real fix is upstream in `libakerror`** — the same
|
||
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test it already applies to `mutation` — and it is
|
||
filed in `deps/libakstdlib/TODO.md` §2.3. Delete the workaround when it lands.
|
||
|
||
**4. Stale build trees poison the coverage report.** `libakgl` defect #13: `gcovr` searches for
|
||
`.gcda`/`.gcno` under `--root`, which its `CMakeLists.txt:208`/`:223` set to the *source* directory, so
|
||
every instrumented build tree left in the source dir is folded into the report. `--object-directory`
|
||
does not narrow that search. A leftover `build-coverage/` makes a freshly configured run fail in
|
||
`coverage_reset`, before any test executes, with `Got function ... on multiple lines`. Because
|
||
`build*/` is `.gitignore`d, the state is easy to reach and hard to see. **Keep build trees out of the
|
||
source directory** — the `cmake -S . -B build` convention above already does, and this is the reason
|
||
to hold to it.
|
||
|
||
## Reference implementation (`deps/basicinterpret`)
|
||
|
||
This is a Commodore BASIC 7.0 / Dartmouth BASIC dialect written in Go with SDL2 + SDL2_ttf. It is the
|
||
behavioral spec: when a question about semantics comes up ("what does `READ` do when it runs off the
|
||
end of `DATA`?"), the answer lives in this code and in `tests/`.
|
||
|
||
```sh
|
||
cd deps/basicinterpret
|
||
make # builds ./basic (CGO_ENABLED=1, needs SDL2 + SDL2_ttf dev packages)
|
||
./basic # interactive REPL
|
||
./basic tests/language/functions.bas
|
||
make tests # or: bash ./test.sh
|
||
```
|
||
|
||
`test.sh` is a golden-file harness: for every `tests/**/*.bas` it runs the interpreter and md5-compares
|
||
stdout against the sibling `.txt`. The exit code is the number of failures. To run a single case,
|
||
just run the interpreter on that `.bas` and diff against its `.txt`.
|
||
|
||
That corpus is the acceptance suite for the rewrite: the C interpreter reproduces the same stdout
|
||
for the same `.bas` files. **It has been copied into this repository at `tests/reference/`** and no
|
||
longer runs out of the submodule — see `tests/reference/README.md` for the provenance and for the
|
||
rule that those expectations are never edited to suit this interpreter. It sits alongside the
|
||
per-module CTest executables the `ak*` style calls for; the two answer different questions, and new
|
||
language features from goal 2 need a `.bas`/`.txt` pair in `tests/language/` as well as unit tests.
|
||
|
||
**`deps/basicinterpret` is no longer a build or test dependency.** Both configurations were
|
||
configured, built and run from scratch with it moved out of the tree. It is kept only as the
|
||
behavioural spec to read when a question about semantics comes up — which is a real use, and the
|
||
reason it has not been removed. The Commodore font it also supplied now lives in `assets/fonts/`.
|
||
|
||
`deps/basicinterpret/README.md` is the language reference — implemented verbs, functions, variable
|
||
type suffixes, and an explicit list of what is *not* implemented. Read it before adding or porting
|
||
language features.
|
||
|
||
### Architecture of the reference interpreter
|
||
|
||
Scanner → parser → tree-walking runtime, one source line at a time:
|
||
|
||
- `basicscanner.go` — `BasicScanner` tokenizes a line into `BasicToken`s. Verbs and function names are
|
||
case-insensitive; variable names are case-sensitive.
|
||
- `basicparser.go` / `basicparser_commands.go` — recursive-descent parser producing `BasicASTLeaf`
|
||
nodes (`basicgrammar.go` defines `BasicASTLeafType` and leaf structure). Commands get their own
|
||
parse paths in `basicparser_commands.go`.
|
||
- `basicruntime.go` — `BasicRuntime` owns the program source (`[MAX_SOURCE_LINES]BasicSourceLine`),
|
||
the variable pool, the scanner/parser, the environment stack, and the SDL window. `evaluate()` walks
|
||
the AST; `commandByReflection()` dispatches a verb to its `BasicRuntime` method by name, so adding a
|
||
verb is largely a matter of adding a correctly-named method.
|
||
- `basicruntime_commands.go` / `basicruntime_functions.go` — the verb and function implementations.
|
||
- `basicenvironment.go` — `BasicEnvironment` is a scope *and* the per-line working state: variables,
|
||
functions, labels, plus the in-flight state of `IF`/`FOR`/`GOSUB`/`READ`. Environments chain via
|
||
`parent` and are pushed/popped with `newEnvironment()` / `prevEnvironment()`.
|
||
- `basicvalue.go` / `basicvariable.go` — `BasicValue` (the strongly-typed value) and `BasicVariable`
|
||
(a named slot plus array metadata). Type is carried by the identifier suffix: `#` integer,
|
||
`%` float, `$` string.
|
||
- `basicruntime_graphics.go` — output goes through an SDL text surface with a cursor, wrapped text,
|
||
scrolling and a print buffer. `Write()` and `Println()` **mirror every line to stdout** as well as
|
||
to the surface; that mirror is the only reason the golden-file suite works, so the C port needs an
|
||
equivalent (a text sink the driver can point at stdout while the embedded case renders through
|
||
`akgl`) or the whole `tests/` corpus becomes unusable.
|
||
|
||
Two design decisions matter most if you are porting this to C:
|
||
|
||
1. **Everything is fixed-size and statically allocated.** `main.go` defines the budget:
|
||
`MAX_LEAVES`/`MAX_TOKENS` 32 per environment, `MAX_VALUES` 64, `MAX_VARIABLES` 128,
|
||
`MAX_SOURCE_LINES` 9999, `MAX_LINE_LENGTH` 256, `MAX_ARRAY_DEPTH` 64. The 32-leaf ceiling is why
|
||
the README says a line is limited to roughly 16 operations. This lines up with the `ak*` libraries'
|
||
no-dynamic-allocation rule — the port should keep the pool-allocation model, not replace it with `malloc`.
|
||
2. **`waitingForCommand` drives block structure.** Rather than building nested blocks in the AST, the
|
||
environment records a verb it is skipping forward to (`NEXT`, `RETURN`, …) and suppresses execution
|
||
of intervening lines until it sees it. Loops and branches evaluate their condition at the *bottom*
|
||
of the structure. Any reimplementation has to reproduce this or restructure control flow deliberately.
|
||
|
||
Runtime modes are `MODE_REPL`, `MODE_RUN`, `MODE_RUNSTREAM` (piped input), `MODE_QUIT`; `BASIC_TRUE`
|
||
is `-1` and `BASIC_FALSE` is `0`, per Commodore convention.
|
||
|
||
## Working in the `ak*` C libraries
|
||
|
||
Each of the three C libraries carries its own `AGENTS.md` (`CLAUDE.md` in them just points at it) with
|
||
authoritative per-repo rules. Read the relevant one before editing a submodule. The shared shape:
|
||
|
||
```sh
|
||
cmake -S . -B build # out-of-tree, always
|
||
cmake --build build --parallel
|
||
ctest --test-dir build --output-on-failure
|
||
ctest --test-dir build -R <name> --output-on-failure # single test
|
||
```
|
||
|
||
**Optional test tooling for this repository.** `xdotool` and `script`(1) drive the `akgl_typing`
|
||
test, which types at a real focused SDL window -- the only test that covers the path upstream of
|
||
SDL, and the one that would have caught the missing `SDL_StartTextInput()`. It needs a real X
|
||
server, steals keyboard focus for about fifteen seconds, and skips itself (CTest `Skipped`, not
|
||
a failure) when it cannot run. `AKBASIC_SKIP_INTERACTIVE=1` skips it deliberately.
|
||
|
||
Extra harnesses, by library (option prefixes differ per repo):
|
||
|
||
```sh
|
||
cmake -S . -B build-asan -DAKSL_SANITIZE=ON # libakstdlib: ASan + UBSan
|
||
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON # libakstdlib
|
||
cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug # libakgl (needs gcovr)
|
||
cmake --build build --target coverage
|
||
cmake --build build --target mutation # slow; scripts/mutation_test.py for a subset
|
||
```
|
||
|
||
`libakerror/test.sh` shows the full CI gate for that library (ctest + mutation ≥65 + coverage ≥90 line
|
||
/ ≥50 branch). `rebuild.sh` in `libakgl` and `libakstdlib` installs into the developer-specific prefix
|
||
`/home/andrew/local` and deletes build directories — use the portable commands above unless that exact
|
||
behavior is wanted.
|
||
|
||
Tests are one-file C programs registered in a list in `CMakeLists.txt`. `libakstdlib` splits them three
|
||
ways and **two of the lists invert the meaning of "Passed"**: `AKSL_TESTS` must exit 0;
|
||
`AKSL_WILL_FAIL_TESTS` abort by design; `AKSL_KNOWN_FAILING_TESTS` assert the *correct* contract for a
|
||
defect documented in `TODO.md` and are expected to fail. A green `ctest` run therefore does not mean
|
||
defect-free. When a known-failing test starts passing, CTest reports "unexpectedly passed" — that is the
|
||
cue to move it into `AKSL_TESTS` along with the fix. `libakerror` has the same split
|
||
(`AKERR_TESTS` / `AKERR_WILL_FAIL_TESTS`).
|
||
|
||
That split *used* to be load-bearing for `libakstdlib`, and no longer is: 0.2.0 fixed all six of the
|
||
confirmed defects its `TODO.md` §2.1 listed, folded the four known-failing tests back into the
|
||
suites for the things they test, and left `AKSL_KNOWN_FAILING_TESTS` empty. The `aksl_ato*` family
|
||
now raises `AKERR_VALUE` on no digits or trailing junk and `ERANGE` on overflow, the linked-list
|
||
functions no longer truncate, `AKERR_ITERATOR_BREAK` stops a tree traversal, and `aksl_realpath` no
|
||
longer reads uninitialised memory.
|
||
|
||
`TODO.md` §1.9 in this repo is still the place to check before reaching into `akstdlib.h`, but it
|
||
is now a record of what changed rather than a list of bans. One caveat survives the upgrade:
|
||
`aksl_strhash_djb2` still sign-extends `char`, so a high-bit byte hashes differently from the
|
||
`unsigned char` answer — see §1.3, which explains why the symbol tables cannot reach it.
|
||
The branch-coverage gate there was re-ratcheted 45 → 40 after the 1.0.0 bump, because the new
|
||
`PREPARE_ERROR`/`FAIL_*` macros expand to more branches per call site — that is a denominator change,
|
||
not a regression, and the same effect will show up in this repo's numbers.
|
||
|
||
## The libakerror error convention
|
||
|
||
New C code in this repo should follow it — it is what `libakstdlib` and `libakgl` both do, and mixing
|
||
conventions breaks error propagation.
|
||
|
||
- Any function that can fail returns `akerr_ErrorContext AKERR_NOIGNORE *` and opens with `PREPARE_ERROR(e);`.
|
||
- Return with `SUCCEED_RETURN(e)` / `FAIL_RETURN(...)`, never a bare status code.
|
||
- Propagate with `PASS(e, some_call());` when there is nothing local to do.
|
||
- Handle locally with `ATTEMPT { CATCH(e, call()); } CLEANUP { } PROCESS(e) { } HANDLE(e, AKERR_X) { } FINISH(e, true);`
|
||
— `FINISH(e, true)` re-raises unhandled errors to the caller; use `FINISH_NORETURN(e)` in `main()`.
|
||
- **`CATCH` and the `FAIL_*_BREAK` macros expand to a C `break`**, so they must never appear inside a
|
||
loop or nested `switch` within an `ATTEMPT` — the `break` would escape only the loop and the rest of
|
||
the `ATTEMPT` would run with an error pending. Inside loops use `PASS` / `FAIL_*_RETURN`, or move the
|
||
loop into its own `akerr_ErrorContext *` helper and `CATCH` that single call.
|
||
- Custom error codes must be **reserved** with `akerr_reserve_status_range()` and **named** with
|
||
`akerr_register_status_name()` during initialization — see the allocation section below before
|
||
defining any.
|
||
- Unhandled errors that reach the top level abort the process with a stack trace. That is the intended
|
||
behavior for the standalone driver — but see goal 3: the interpreter *library* must not rely on it.
|
||
|
||
### Error-code allocation across libraries
|
||
|
||
**`libakerror` 1.0.0 changed this completely.** The hazard that used to live in this section — a
|
||
consumer-sized `__AKERR_ERROR_NAMES` array, a mismatched `AKERR_MAX_ERR_VALUE` corrupting BSS, and no
|
||
authority to allocate from — is gone. Read `deps/libakerror/UPGRADING.md` before writing any error
|
||
code; the short version follows.
|
||
|
||
**What 1.0.0 removed.** These no longer exist. Any code, compile definition, or documentation
|
||
referring to them is stale:
|
||
|
||
| Removed | Replacement |
|
||
|---|---|
|
||
| `AKERR_MAX_ERR_VALUE` | nothing — the name registry is sparse and takes any `int` |
|
||
| `__AKERR_ERROR_NAMES` | `akerr_name_for_status()` (the table is private to the library now) |
|
||
| `AKERR_STATUS_RANGE_OK` / `AKERR_STATUS_NAME_OK` | success is a `NULL` `akerr_ErrorContext *`, like everything else |
|
||
|
||
It is also a **source and ABI break** carrying an soname (`libakerror.so.1`). Anything built against a
|
||
pre-1.0.0 header must be rebuilt — including `libakstdlib` and `libakgl`. A stale
|
||
`-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless; delete it when you see it.
|
||
|
||
**How allocation works now.** Values `0`–`255` belong to `libakerror` (the host errno space plus the
|
||
`AKERR_*` codes), reserved by `akerr_init()` under the owner string `"libakerror"`. Consumers allocate
|
||
from `AKERR_FIRST_CONSUMER_STATUS` (256) upward, as **absolute integer constants** — never as offsets
|
||
from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno cannot move them. Every library that can
|
||
coexist in one process reserves its range at init:
|
||
|
||
```c
|
||
akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
|
||
akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner, int status, const char *name);
|
||
```
|
||
|
||
Both return `akerr_ErrorContext *` and are `AKERR_NOIGNORE`, so a collision is an ordinary error you
|
||
`CATCH`, `HANDLE`, or `PASS` out of your init function. Ownership is **enforced**: naming a status in
|
||
someone else's range fails with `AKERR_STATUS_NAME_FOREIGN`, and naming one nobody reserved fails with
|
||
`AKERR_STATUS_NAME_UNRESERVED`. You do not need to call `akerr_init()` first — every registry entry
|
||
point calls it for you, and (unlike pre-1.0.0) it no longer clears reservations made before it ran.
|
||
Repeating an identical reservation for the same owner is a no-op; a subset or superset of your own
|
||
range is not, so reserve the whole thing in one call.
|
||
|
||
**The coordinated range map for this dependency stack.** The library cannot enumerate its consumers,
|
||
so this table is the coordination — keep it current, and reserve the whole 256 even where fewer codes
|
||
are used, so a later addition does not need a second reservation:
|
||
|
||
| Owner string | Range | Status |
|
||
|---|---|---|
|
||
| `"libakerror"` | 0 – 255 | reserved by `akerr_init()`; do not touch |
|
||
| *(none)* | — | `libakstdlib` deliberately reserves nothing and defines no codes of its own — it raises `AKERR_*` and propagates `errno`, both inside the reserved band. Its `tests/test_status_registry.c` pins that as a contract, so nobody has to coordinate with it. |
|
||
| `"libakgl"` | 256 – 260 | reserved by `akgl_error_init()`; `AKGL_ERR_BASE` … `AKGL_ERR_LIMIT - 1`, five codes |
|
||
| *(free)* | 261 – 511 | headroom for `libakgl` to grow into; do not claim it |
|
||
| `"akbasic"` | 512 – 767 | ours; `AKBASIC_ERR_BASE` is 512 |
|
||
|
||
**Every owner reserves at init, and each does it in its own initializer.** `akerr_init()` claims
|
||
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.
|