418 lines
27 KiB
Markdown
418 lines
27 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.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 `.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 natural acceptance suite for the rewrite: the C interpreter should reproduce the
|
|||
|
|
same stdout for the same `.bas` files. Carry it over (or drive it in place) alongside the per-module
|
|||
|
|
CTest executables the `ak*` style calls for — the two answer different questions, and new language
|
|||
|
|
features from goal 2 need a `.bas`/`.txt` pair as well as unit tests.
|
|||
|
|
|
|||
|
|
`deps/basicinterpret/README.md` is the language reference — implemented verbs, functions, variable
|
|||
|
|
type suffixes, and an explicit list of what is *not* implemented. Read it before adding or porting
|
|||
|
|
language features.
|
|||
|
|
|
|||
|
|
### Architecture of the reference interpreter
|
|||
|
|
|
|||
|
|
Scanner → parser → tree-walking runtime, one source line at a time:
|
|||
|
|
|
|||
|
|
- `basicscanner.go` — `BasicScanner` tokenizes a line into `BasicToken`s. Verbs and function names are
|
|||
|
|
case-insensitive; variable names are case-sensitive.
|
|||
|
|
- `basicparser.go` / `basicparser_commands.go` — recursive-descent parser producing `BasicASTLeaf`
|
|||
|
|
nodes (`basicgrammar.go` defines `BasicASTLeafType` and leaf structure). Commands get their own
|
|||
|
|
parse paths in `basicparser_commands.go`.
|
|||
|
|
- `basicruntime.go` — `BasicRuntime` owns the program source (`[MAX_SOURCE_LINES]BasicSourceLine`),
|
|||
|
|
the variable pool, the scanner/parser, the environment stack, and the SDL window. `evaluate()` walks
|
|||
|
|
the AST; `commandByReflection()` dispatches a verb to its `BasicRuntime` method by name, so adding a
|
|||
|
|
verb is largely a matter of adding a correctly-named method.
|
|||
|
|
- `basicruntime_commands.go` / `basicruntime_functions.go` — the verb and function implementations.
|
|||
|
|
- `basicenvironment.go` — `BasicEnvironment` is a scope *and* the per-line working state: variables,
|
|||
|
|
functions, labels, plus the in-flight state of `IF`/`FOR`/`GOSUB`/`READ`. Environments chain via
|
|||
|
|
`parent` and are pushed/popped with `newEnvironment()` / `prevEnvironment()`.
|
|||
|
|
- `basicvalue.go` / `basicvariable.go` — `BasicValue` (the strongly-typed value) and `BasicVariable`
|
|||
|
|
(a named slot plus array metadata). Type is carried by the identifier suffix: `#` integer,
|
|||
|
|
`%` float, `$` string.
|
|||
|
|
- `basicruntime_graphics.go` — output goes through an SDL text surface with a cursor, wrapped text,
|
|||
|
|
scrolling and a print buffer. `Write()` and `Println()` **mirror every line to stdout** as well as
|
|||
|
|
to the surface; that mirror is the only reason the golden-file suite works, so the C port needs an
|
|||
|
|
equivalent (a text sink the driver can point at stdout while the embedded case renders through
|
|||
|
|
`akgl`) or the whole `tests/` corpus becomes unusable.
|
|||
|
|
|
|||
|
|
Two design decisions matter most if you are porting this to C:
|
|||
|
|
|
|||
|
|
1. **Everything is fixed-size and statically allocated.** `main.go` defines the budget:
|
|||
|
|
`MAX_LEAVES`/`MAX_TOKENS` 32 per environment, `MAX_VALUES` 64, `MAX_VARIABLES` 128,
|
|||
|
|
`MAX_SOURCE_LINES` 9999, `MAX_LINE_LENGTH` 256, `MAX_ARRAY_DEPTH` 64. The 32-leaf ceiling is why
|
|||
|
|
the README says a line is limited to roughly 16 operations. This lines up with the `ak*` libraries'
|
|||
|
|
no-dynamic-allocation rule — the port should keep the pool-allocation model, not replace it with `malloc`.
|
|||
|
|
2. **`waitingForCommand` drives block structure.** Rather than building nested blocks in the AST, the
|
|||
|
|
environment records a verb it is skipping forward to (`NEXT`, `RETURN`, …) and suppresses execution
|
|||
|
|
of intervening lines until it sees it. Loops and branches evaluate their condition at the *bottom*
|
|||
|
|
of the structure. Any reimplementation has to reproduce this or restructure control flow deliberately.
|
|||
|
|
|
|||
|
|
Runtime modes are `MODE_REPL`, `MODE_RUN`, `MODE_RUNSTREAM` (piped input), `MODE_QUIT`; `BASIC_TRUE`
|
|||
|
|
is `-1` and `BASIC_FALSE` is `0`, per Commodore convention.
|
|||
|
|
|
|||
|
|
## Working in the `ak*` C libraries
|
|||
|
|
|
|||
|
|
Each of the three C libraries carries its own `AGENTS.md` (`CLAUDE.md` in them just points at it) with
|
|||
|
|
authoritative per-repo rules. Read the relevant one before editing a submodule. The shared shape:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
cmake -S . -B build # out-of-tree, always
|
|||
|
|
cmake --build build --parallel
|
|||
|
|
ctest --test-dir build --output-on-failure
|
|||
|
|
ctest --test-dir build -R <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 is not academic for `libakstdlib`: `deps/libakstdlib/TODO.md` §2.1 lists six **confirmed**
|
|||
|
|
defects, four with a known-failing test apiece. Several are in functions this port would otherwise
|
|||
|
|
reach for by default — the `aksl_ato*` family cannot report a conversion failure at all, and the
|
|||
|
|
linked-list functions truncate. **Read §2.1 before calling anything in `akstdlib.h` for the first
|
|||
|
|
time**; `TODO.md` §1.9 in this repo records which calls are cleared for use here and which are not.
|
|||
|
|
The branch-coverage gate there was re-ratcheted 45 → 40 after the 1.0.0 bump, because the new
|
|||
|
|
`PREPARE_ERROR`/`FAIL_*` macros expand to more branches per call site — that is a denominator change,
|
|||
|
|
not a regression, and the same effect will show up in this repo's numbers.
|
|||
|
|
|
|||
|
|
## The libakerror error convention
|
|||
|
|
|
|||
|
|
New C code in this repo should follow it — it is what `libakstdlib` and `libakgl` both do, and mixing
|
|||
|
|
conventions breaks error propagation.
|
|||
|
|
|
|||
|
|
- Any function that can fail returns `akerr_ErrorContext AKERR_NOIGNORE *` and opens with `PREPARE_ERROR(e);`.
|
|||
|
|
- Return with `SUCCEED_RETURN(e)` / `FAIL_RETURN(...)`, never a bare status code.
|
|||
|
|
- Propagate with `PASS(e, some_call());` when there is nothing local to do.
|
|||
|
|
- Handle locally with `ATTEMPT { CATCH(e, call()); } CLEANUP { } PROCESS(e) { } HANDLE(e, AKERR_X) { } FINISH(e, true);`
|
|||
|
|
— `FINISH(e, true)` re-raises unhandled errors to the caller; use `FINISH_NORETURN(e)` in `main()`.
|
|||
|
|
- **`CATCH` and the `FAIL_*_BREAK` macros expand to a C `break`**, so they must never appear inside a
|
|||
|
|
loop or nested `switch` within an `ATTEMPT` — the `break` would escape only the loop and the rest of
|
|||
|
|
the `ATTEMPT` would run with an error pending. Inside loops use `PASS` / `FAIL_*_RETURN`, or move the
|
|||
|
|
loop into its own `akerr_ErrorContext *` helper and `CATCH` that single call.
|
|||
|
|
- Custom error codes must be **reserved** with `akerr_reserve_status_range()` and **named** with
|
|||
|
|
`akerr_register_status_name()` during initialization — see the allocation section below before
|
|||
|
|
defining any.
|
|||
|
|
- Unhandled errors that reach the top level abort the process with a stack trace. That is the intended
|
|||
|
|
behavior for the standalone driver — but see goal 3: the interpreter *library* must not rely on it.
|
|||
|
|
|
|||
|
|
### Error-code allocation across libraries
|
|||
|
|
|
|||
|
|
**`libakerror` 1.0.0 changed this completely.** The hazard that used to live in this section — a
|
|||
|
|
consumer-sized `__AKERR_ERROR_NAMES` array, a mismatched `AKERR_MAX_ERR_VALUE` corrupting BSS, and no
|
|||
|
|
authority to allocate from — is gone. Read `deps/libakerror/UPGRADING.md` before writing any error
|
|||
|
|
code; the short version follows.
|
|||
|
|
|
|||
|
|
**What 1.0.0 removed.** These no longer exist. Any code, compile definition, or documentation
|
|||
|
|
referring to them is stale:
|
|||
|
|
|
|||
|
|
| Removed | Replacement |
|
|||
|
|
|---|---|
|
|||
|
|
| `AKERR_MAX_ERR_VALUE` | nothing — the name registry is sparse and takes any `int` |
|
|||
|
|
| `__AKERR_ERROR_NAMES` | `akerr_name_for_status()` (the table is private to the library now) |
|
|||
|
|
| `AKERR_STATUS_RANGE_OK` / `AKERR_STATUS_NAME_OK` | success is a `NULL` `akerr_ErrorContext *`, like everything else |
|
|||
|
|
|
|||
|
|
It is also a **source and ABI break** carrying an soname (`libakerror.so.1`). Anything built against a
|
|||
|
|
pre-1.0.0 header must be rebuilt — including `libakstdlib` and `libakgl`. A stale
|
|||
|
|
`-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless; delete it when you see it.
|
|||
|
|
|
|||
|
|
**How allocation works now.** Values `0`–`255` belong to `libakerror` (the host errno space plus the
|
|||
|
|
`AKERR_*` codes), reserved by `akerr_init()` under the owner string `"libakerror"`. Consumers allocate
|
|||
|
|
from `AKERR_FIRST_CONSUMER_STATUS` (256) upward, as **absolute integer constants** — never as offsets
|
|||
|
|
from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno cannot move them. Every library that can
|
|||
|
|
coexist in one process reserves its range at init:
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
|
|||
|
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner, int status, const char *name);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Both return `akerr_ErrorContext *` and are `AKERR_NOIGNORE`, so a collision is an ordinary error you
|
|||
|
|
`CATCH`, `HANDLE`, or `PASS` out of your init function. Ownership is **enforced**: naming a status in
|
|||
|
|
someone else's range fails with `AKERR_STATUS_NAME_FOREIGN`, and naming one nobody reserved fails with
|
|||
|
|
`AKERR_STATUS_NAME_UNRESERVED`. You do not need to call `akerr_init()` first — every registry entry
|
|||
|
|
point calls it for you, and (unlike pre-1.0.0) it no longer clears reservations made before it ran.
|
|||
|
|
Repeating an identical reservation for the same owner is a no-op; a subset or superset of your own
|
|||
|
|
range is not, so reserve the whole thing in one call.
|
|||
|
|
|
|||
|
|
**The coordinated range map for this dependency stack.** The library cannot enumerate its consumers,
|
|||
|
|
so this table is the coordination — keep it current, and reserve the whole 256 even where fewer codes
|
|||
|
|
are used, so a later addition does not need a second reservation:
|
|||
|
|
|
|||
|
|
| Owner string | Range | Status |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `"libakerror"` | 0 – 255 | reserved by `akerr_init()`; do not touch |
|
|||
|
|
| *(none)* | — | `libakstdlib` deliberately reserves nothing and defines no codes of its own — it raises `AKERR_*` and propagates `errno`, both inside the reserved band. Its `tests/test_status_registry.c` pins that as a contract, so nobody has to coordinate with it. |
|
|||
|
|
| `"libakgl"` | 256 – 260 | reserved by `akgl_error_init()`; `AKGL_ERR_BASE` … `AKGL_ERR_LIMIT - 1`, five codes |
|
|||
|
|
| *(free)* | 261 – 511 | headroom for `libakgl` to grow into; do not claim it |
|
|||
|
|
| `"akbasic"` | 512 – 767 | ours; `AKBASIC_ERR_BASE` is 512 |
|
|||
|
|
|
|||
|
|
**Every owner reserves at init, and each does it in its own initializer.** `akerr_init()` claims
|
|||
|
|
0–255 for itself. `akgl_error_init()` (`deps/libakgl/src/error.c`, new in libakgl 0.1.0) claims
|
|||
|
|
`AKGL_ERR_BASE` = `AKERR_FIRST_CONSUMER_STATUS` = 256 through `AKGL_ERR_LIMIT - 1` = 260 and names all
|
|||
|
|
five codes. `libakstdlib` claims nothing. `akbasic_init()` claims 512–767.
|
|||
|
|
|
|||
|
|
`akgl_error_init()` has an ordering requirement worth knowing before you wire up `main()`: it must run
|
|||
|
|
before anything else in `libakgl`, because a code raised before it registers carries no name into its
|
|||
|
|
stack trace. `akgl_game_init()` calls it as its first statement, but a program driving subsystems
|
|||
|
|
directly — which is what the embedded interpreter does — has to call it itself. It is idempotent, so
|
|||
|
|
calling it more than once is fine; what is *not* fine is a subset or superset reservation, which
|
|||
|
|
`libakerror` refuses.
|
|||
|
|
|
|||
|
|
**What `akbasic` does.** Reserve and name in `akbasic_init()`, and let failures propagate:
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
#define AKBASIC_OWNER "akbasic"
|
|||
|
|
|
|||
|
|
enum {
|
|||
|
|
AKBASIC_ERR_BASE = 512, /* AKERR_FIRST_CONSUMER_STATUS + 256; see the map above */
|
|||
|
|
AKBASIC_ERR_SYNTAX = AKBASIC_ERR_BASE,
|
|||
|
|
AKBASIC_ERR_TYPE,
|
|||
|
|
/* ... */
|
|||
|
|
AKBASIC_ERR_LIMIT = AKBASIC_ERR_BASE + 256
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Use an `enum`, not a chain of `#define`s: the values stay compile-time integer constants, so `HANDLE`
|
|||
|
|
still works (its `case` labels require that), and adding a code does not mean editing an arithmetic
|
|||
|
|
offset. Reserve `AKBASIC_ERR_LIMIT - AKBASIC_ERR_BASE` in one call.
|
|||
|
|
|
|||
|
|
**What is still not solved, and what it means for us.** `libakerror`'s own `TODO.md` §2 says it plainly:
|
|||
|
|
ownership enforcement covers *naming*, which is the part the library mediates. It cannot detect two
|
|||
|
|
components compiling the same integer into a `HANDLE` `case` label without ever registering a name —
|
|||
|
|
that never reaches the registry. §3 adds that there is no way to ask who owns a status or to enumerate
|
|||
|
|
reservations, so the coordination advice above has no tooling behind it yet; the table in this file is
|
|||
|
|
the tooling. Two practical rules follow:
|
|||
|
|
|
|||
|
|
- **Always reserve, even for codes you never name.** Reservation is the only thing that makes a
|
|||
|
|
collision visible at all.
|
|||
|
|
- **Never define an error code as an offset from another library's symbol.** The `libakgl` defect above
|
|||
|
|
is exactly what that produces.
|
|||
|
|
|
|||
|
|
Capacity is not a concern here: the name registry holds 3072 entries and `akerr_init()` consumes about
|
|||
|
|
150, and reservations cap at 64 ranges. Both are `PRIVATE` to the `libakerror` target and invisible to
|
|||
|
|
consumers — raising them is a `libakerror` configure-time decision, not something `akbasic` sets.
|
|||
|
|
Registration is **not thread safe**; do it during single-threaded init, before the host game spawns
|
|||
|
|
anything.
|
|||
|
|
|
|||
|
|
`libakgl` adds a hard rule of its own: **avoid dynamic memory allocation.** Obtain objects from
|
|||
|
|
`akgl_heap_next_*` and release them back; if no heap layer exists for a needed type, add one rather
|
|||
|
|
than calling `malloc`.
|
|||
|
|
|
|||
|
|
## Style and commits
|
|||
|
|
|
|||
|
|
C is C99-compatible, 4-space indent, braces on the next line for function bodies, spaces inside
|
|||
|
|
control-flow parens. Match the surrounding file — several files mix tabs and spaces, and there is no
|
|||
|
|
repo-wide formatter, so avoid unrelated formatting churn. Public symbols take a library prefix
|
|||
|
|
(`akerr_`, `aksl_`, `akgl_`) with `akerr_TypeName`-style types and `AKGL_UPPER_SNAKE_CASE` macros.
|
|||
|
|
Since this interpreter is meant to link into `libakgl` (goal 3), it needs its own non-colliding
|
|||
|
|
prefix in the same shape — `akbasic_` for functions, `akbasic_TypeName` for types, `AKBASIC_` for
|
|||
|
|
macros and constants — and matching header/source pairs named by feature, as in `src/scanner.c` /
|
|||
|
|
`include/akbasic/scanner.h`.
|
|||
|
|
|
|||
|
|
Commit subjects are short, imperative, sentence case ("Fix refcount leak and stack-trace buffer
|
|||
|
|
overflow"). `libakgl`'s `AGENTS.md` requires agents to add themselves (program, model, version) as a
|
|||
|
|
commit co-author. Never edit generated output — `build/` trees, generated `akerror.h`, `akgl.pc`,
|
|||
|
|
`include/akgl/SDL_GameControllerDB.h`; change the template or generator script instead.
|