Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
# 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.
2026-07-31 12:52:00 -04:00
**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.
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
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 |
2026-07-31 09:55:47 -04:00
| `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()` |
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
For both 0.x libraries the soname carries `MAJOR.MINOR` deliberately: 0.1 and 0.2 are * different *
2026-07-31 09:55:47 -04:00
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.
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
2026-07-31 09:55:47 -04:00
**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
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
`<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
2026-07-31 09:55:47 -04:00
`Version:` and the `.so` had no soname. Now `project(akgl VERSION 0.2.0)` is the single source and
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
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` .
2026-07-31 12:35:40 -04:00
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/` .
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
`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` ).
2026-07-31 09:55:47 -04:00
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.
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
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
2026-07-31 09:55:47 -04:00
0– 255 for itself. `akgl_error_init()` (`deps/libakgl/src/error.c` ) claims
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
`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.