Files
akbasic/MAINTENANCE.md

835 lines
48 KiB
Markdown
Raw Normal View History

Execute every documented example as a test docs/ and README.md carry 85 fenced blocks. Every one was checked by hand exactly once, when it was written, which is not a standard that survives a changing interpreter -- and four were already wrong: two transcripts showing a leading space PRINT does not emit, akbasic_TextSink in README.md missing the two members it had grown hours earlier, and FILTER's refusal quoted with wording the code does not use. tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds. BASIC programs and transcripts run and are byte-compared against an `output` block; C snippets compile with -fsyntax-only against the real include path, which CMake writes out because it is transitive through akerror, akstdlib and akgl; shell blocks run in a sandbox. Anything that would reconfigure the build tree, hit the network or re-enter the suite is tagged norun with the reason in MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision. An untagged block is a failure rather than a default, and the pass line reports what it executed by kind. Both exist because the way a harness like this dies is by quietly matching nothing and passing -- which it duly did on the first CTest run, where a generator expression evaluating to nothing still contributed an empty argument that the script read as a filename. The count is what caught it. The excerpt check earns its own mention: a block tagged `c excerpt=include/akbasic/sink.h` must still appear in that header, comments and whitespace ignored. Compiling it would only redefine the type, so a compile check could not have found the stale struct, and did not. Registered as the CTest case docs_examples in both configurations. Fixing the four wrong examples turned up two interpreter defects, fixed in the previous commit and recorded in TODO.md section 8. MAINTENANCE.md is new: the fence-tag reference, what to do when the case fails, and the conventions that until now only existed inside source comments -- the three test lists and how two of them invert "passed", the sorted verb table, that a golden file is never edited to suit this interpreter, and that a fix gets mutation-checked with a file copy rather than git checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 22:41:36 -04:00
# Maintaining akbasic
Split the documentation by who reads it README.md was 577 lines and answered four different questions at once: what the project is, how to build it, every verb and function in the language, and how to maintain the test harness. The verb and function lists had already been written a second time in docs/11 and docs/12, which is how a list of that size goes stale -- there is no way to notice the two have drifted apart. README.md is now 150 lines and holds only what somebody evaluating the project needs: what it is, the quickstart, why it was rewritten in C, the five rules embedding imposes on the design, the two ways to use it, and where everything else lives. Technical detail goes to docs/, maintenance to MAINTENANCE.md. The akbasic_TextSink struct moved to docs/10-embedding.md rather than being deleted. It was the corpus's only `c excerpt=` block -- the check that caught the stale struct two commits ago -- so dropping it with the README would have quietly retired a test. docs/10 also stopped claiming README.md carries the full API surface and the pool limits, which the trim made false. CLAUDE.md went from 458 lines to 62, because almost none of it was agent-specific. The project goals, the Go reference and its architecture, the dependency version and ABI rules, the four ways an embedded build collides, the libakerror convention, the error-code range map and the style rules are all things a maintainer needs, and they are now in MAINTENANCE.md with one copy to keep true. CLAUDE.md points there and keeps only the rules no test enforces: tests in the same commit asserting the correct contract, file a missing dependency capability upstream, do not edit generated output or tests/reference/, co-author your commits. Four claims did not survive the move, having gone stale where nothing could notice: - "The repository is currently empty apart from its submodules -- no commits, no source tree, no build files." There are 43 commits. - libakgl's target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE) at deps/libakgl/CMakeLists.txt:44, described as inert but present. It is gone; only a historical mention in a comment remains. - "akbasic_init() claims 512-767." There is no akbasic_init. It is akbasic_error_register(), called from akbasic_runtime_init(). - Time-relative phrasing ("libakgl hit two of them in the last week"). Five places pointed at CLAUDE.md for the range map or the file-it-upstream rule and now point at MAINTENANCE.md: include/akbasic/error.h, src/runtime_disk.c and three entries in TODO.md. Both source changes are comments. deps/libakgl/TODO.md cites it too and is left alone; it is a submodule, and the rule it quotes is still reachable from CLAUDE.md. ctest is green at 95 of 95, docs_examples included: 36 programs, 9 transcripts, 44 output comparisons, 3 C snippets, 1 excerpt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 22:59:43 -04:00
`README.md` is for people evaluating the interpreter. `docs/` is for people writing BASIC in
it. This file is for whoever has to change the thing: what the project is trying to be, how
the build fits together, and the conventions — the ones enforced by a test, and the ones
enforced by nothing at all and therefore written down here.
`CLAUDE.md` is for agents, and it is deliberately thin: it points here. Everything an agent
is told about this project is in this file, so there is one copy to keep true.
Document the interpreter's architecture as chapter fourteen Chapters 1 through 13 describe the language. Nothing described the machine that runs it, and the answers were spread across header comments, TODO.md sections written for a different purpose, and the source itself. Somebody embedding the interpreter, debugging something it did, or adding a verb had to reconstruct the shape from all three. docs/14-architecture.md is that shape, and only that: the three targets and the driver, the single akbasic_Runtime and why nothing is file-scope, akbasic_runtime_step() unrolled with the reason each stage sits where it does, the four modes and what set_mode() does beyond assigning, a line's journey from text through tokens and leaves to a verb handler, the dispatch table, the pool map with what each exhaustion actually says, environments doubling as block state, the two kinds of error, devices, and interrupts. It defers rather than restates. The headers are the authority on every function's contract and the chapter says so up front; where a rule is subtle the header comment already states it at more length than a chapter should. MAINTENANCE.md keeps the conventions and now points here for the mechanism, so there is still one copy of each. Two sections are the reason it exists at all. Debugging: reading a TRON trace as evidence about the loop rather than the lines, reading an akerror stack trace and what it is not, four breakpoints and the expressions worth printing at them, narrowing with ctest -R and the mock devices, and a symptom-to-cause table. Changing it: the verb recipe end to end including the private src/verbs.h prototype that is easy to miss, the rule that a missing dependency capability gets filed upstream rather than worked around, and the five constraints goal 3 puts on any change. A `text` fence tag comes with it. Every fenced block in docs/ is executed and an untagged one is a hard error, so six block diagrams had nowhere to live. The tag means never executed, it is counted in the skip line like `cmake`, and MAINTENANCE.md documents it -- the alternative was an indented block the extractor never sees, and a picture nobody decided about is indistinguishable from a test nobody ran. tests/docs_examples.sh now makes --root and --basic absolute before it starts. Both are used from inside a sandbox directory it cd's into, so the invocation MAINTENANCE.md itself documents -- --root . --basic ./build/basic -- failed every example with "exited 127" and every setup= with "setup failed". CTest passes absolute paths and never saw it; running one document by hand hits it immediately. Writing the error section turned up a defect and TODO.md section 8 records it. The ATTEMPT blocks that turn a script's mistake into an error line wrap parsing and interpretation but not scanning, so a line with more than 32 tokens escapes as an interpreter error: stack trace, exit 1, and at a prompt the REPL is gone. That is the same shape as section 8 item 2, on a path that fix did not cover. Not fixed here -- it is a behaviour change and wants its own tests -- but written down with the three call sites and what would cover them. Both configurations stay green: 95/95 and 94/94. docs_examples now runs 37 programs, 9 transcripts, 45 output comparisons, 3 C snippets, 2 excerpts and 2 shell blocks, and skips 9 text blocks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 23:14:17 -04:00
One thing is deliberately *not* here. **How the interpreter actually works — the step loop,
the four modes, the pools, the scopes that double as block state, the two kinds of error,
and how to debug all of it — is [`docs/14-architecture.md`](docs/14-architecture.md).** This
file is the conventions for changing the thing; that chapter is the thing.
Split the documentation by who reads it README.md was 577 lines and answered four different questions at once: what the project is, how to build it, every verb and function in the language, and how to maintain the test harness. The verb and function lists had already been written a second time in docs/11 and docs/12, which is how a list of that size goes stale -- there is no way to notice the two have drifted apart. README.md is now 150 lines and holds only what somebody evaluating the project needs: what it is, the quickstart, why it was rewritten in C, the five rules embedding imposes on the design, the two ways to use it, and where everything else lives. Technical detail goes to docs/, maintenance to MAINTENANCE.md. The akbasic_TextSink struct moved to docs/10-embedding.md rather than being deleted. It was the corpus's only `c excerpt=` block -- the check that caught the stale struct two commits ago -- so dropping it with the README would have quietly retired a test. docs/10 also stopped claiming README.md carries the full API surface and the pool limits, which the trim made false. CLAUDE.md went from 458 lines to 62, because almost none of it was agent-specific. The project goals, the Go reference and its architecture, the dependency version and ABI rules, the four ways an embedded build collides, the libakerror convention, the error-code range map and the style rules are all things a maintainer needs, and they are now in MAINTENANCE.md with one copy to keep true. CLAUDE.md points there and keeps only the rules no test enforces: tests in the same commit asserting the correct contract, file a missing dependency capability upstream, do not edit generated output or tests/reference/, co-author your commits. Four claims did not survive the move, having gone stale where nothing could notice: - "The repository is currently empty apart from its submodules -- no commits, no source tree, no build files." There are 43 commits. - libakgl's target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE) at deps/libakgl/CMakeLists.txt:44, described as inert but present. It is gone; only a historical mention in a comment remains. - "akbasic_init() claims 512-767." There is no akbasic_init. It is akbasic_error_register(), called from akbasic_runtime_init(). - Time-relative phrasing ("libakgl hit two of them in the last week"). Five places pointed at CLAUDE.md for the range map or the file-it-upstream rule and now point at MAINTENANCE.md: include/akbasic/error.h, src/runtime_disk.c and three entries in TODO.md. Both source changes are comments. deps/libakgl/TODO.md cites it too and is left alone; it is a submodule, and the rule it quotes is still reachable from CLAUDE.md. ctest is green at 95 of 95, docs_examples included: 36 programs, 9 transcripts, 44 output comparisons, 3 C snippets, 1 excerpt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 22:59:43 -04:00
---
## What this project is
`akbasic` is a **C rewrite of the Go BASIC interpreter in `deps/basicinterpret`**, written in
the idiom of the `ak*` C libraries it builds on.
| Submodule | Language | Role |
|---|---|---|
| `deps/basicinterpret` | Go | The implementation that was rewritten; the behavioural spec for questions about semantics |
| `deps/libakerror` | C | TRY/CATCH-style error contexts — the 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 |
### The goals, in priority order
1. **Port Go → C.** Reproduce the reference interpreter in C: `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.** The full Dartmouth BASIC and Commodore 128 BASIC 7.0 verb and
function set. `deps/basicinterpret/README.md` ends with the original's list of what was
unimplemented, and that list was the work queue. Every group of it is done; what remains is
in the issue tracker and is summarised for a BASIC programmer in `docs/13-differences.md`. A few entries are
Split the documentation by who reads it README.md was 577 lines and answered four different questions at once: what the project is, how to build it, every verb and function in the language, and how to maintain the test harness. The verb and function lists had already been written a second time in docs/11 and docs/12, which is how a list of that size goes stale -- there is no way to notice the two have drifted apart. README.md is now 150 lines and holds only what somebody evaluating the project needs: what it is, the quickstart, why it was rewritten in C, the five rules embedding imposes on the design, the two ways to use it, and where everything else lives. Technical detail goes to docs/, maintenance to MAINTENANCE.md. The akbasic_TextSink struct moved to docs/10-embedding.md rather than being deleted. It was the corpus's only `c excerpt=` block -- the check that caught the stale struct two commits ago -- so dropping it with the README would have quietly retired a test. docs/10 also stopped claiming README.md carries the full API surface and the pool limits, which the trim made false. CLAUDE.md went from 458 lines to 62, because almost none of it was agent-specific. The project goals, the Go reference and its architecture, the dependency version and ABI rules, the four ways an embedded build collides, the libakerror convention, the error-code range map and the style rules are all things a maintainer needs, and they are now in MAINTENANCE.md with one copy to keep true. CLAUDE.md points there and keeps only the rules no test enforces: tests in the same commit asserting the correct contract, file a missing dependency capability upstream, do not edit generated output or tests/reference/, co-author your commits. Four claims did not survive the move, having gone stale where nothing could notice: - "The repository is currently empty apart from its submodules -- no commits, no source tree, no build files." There are 43 commits. - libakgl's target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE) at deps/libakgl/CMakeLists.txt:44, described as inert but present. It is gone; only a historical mention in a comment remains. - "akbasic_init() claims 512-767." There is no akbasic_init. It is akbasic_error_register(), called from akbasic_runtime_init(). - Time-relative phrasing ("libakgl hit two of them in the last week"). Five places pointed at CLAUDE.md for the range map or the file-it-upstream rule and now point at MAINTENANCE.md: include/akbasic/error.h, src/runtime_disk.c and three entries in TODO.md. Both source changes are comments. deps/libakgl/TODO.md cites it too and is left alone; it is a submodule, and the rule it quotes is still reachable from CLAUDE.md. ctest is green at 95 of 95, docs_examples included: 36 programs, 9 transcripts, 44 output comparisons, 3 C snippets, 1 excerpt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 22:59:43 -04:00
deliberately out of scope on a modern PC — `BANK`, `FAST`, `MONITOR`, and `SPRDEF`, which
is an interactive editor rather than a programmable verb. Keep that reasoning rather than
reviving them.
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:
- The interpreter is a library target with a thin `src/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 a
`main()`.
- The interpreter owns no window, renderer or game loop. It draws through whatever `akgl`
renderer the host already initialized, and a host must be able to bound execution rather
than surrender control to a `run()` that never returns.
### Missing capabilities get filed upstream, not worked around
**When `libakgl` — or `libakstdlib` — cannot supply something a verb needs, do not work
around it here.** Open an issue in that repository's tracker on
<https://source.starfort.tech> describing the missing API: what the BASIC verb requires, what
the `akgl_*` or `aksl_*` entry point should look like, and what tests would cover it.
**Recording it in this repository instead is not filing it** — two `akgl_ui` gaps sat in
`TODO.md` for a release on the reasoning that changing a submodule is that repository's
decision, which is true of *changing* it and not of *reporting* it. Follow the prose-paragraph style of the entries already
Split the documentation by who reads it README.md was 577 lines and answered four different questions at once: what the project is, how to build it, every verb and function in the language, and how to maintain the test harness. The verb and function lists had already been written a second time in docs/11 and docs/12, which is how a list of that size goes stale -- there is no way to notice the two have drifted apart. README.md is now 150 lines and holds only what somebody evaluating the project needs: what it is, the quickstart, why it was rewritten in C, the five rules embedding imposes on the design, the two ways to use it, and where everything else lives. Technical detail goes to docs/, maintenance to MAINTENANCE.md. The akbasic_TextSink struct moved to docs/10-embedding.md rather than being deleted. It was the corpus's only `c excerpt=` block -- the check that caught the stale struct two commits ago -- so dropping it with the README would have quietly retired a test. docs/10 also stopped claiming README.md carries the full API surface and the pool limits, which the trim made false. CLAUDE.md went from 458 lines to 62, because almost none of it was agent-specific. The project goals, the Go reference and its architecture, the dependency version and ABI rules, the four ways an embedded build collides, the libakerror convention, the error-code range map and the style rules are all things a maintainer needs, and they are now in MAINTENANCE.md with one copy to keep true. CLAUDE.md points there and keeps only the rules no test enforces: tests in the same commit asserting the correct contract, file a missing dependency capability upstream, do not edit generated output or tests/reference/, co-author your commits. Four claims did not survive the move, having gone stale where nothing could notice: - "The repository is currently empty apart from its submodules -- no commits, no source tree, no build files." There are 43 commits. - libakgl's target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE) at deps/libakgl/CMakeLists.txt:44, described as inert but present. It is gone; only a historical mention in a comment remains. - "akbasic_init() claims 512-767." There is no akbasic_init. It is akbasic_error_register(), called from akbasic_runtime_init(). - Time-relative phrasing ("libakgl hit two of them in the last week"). Five places pointed at CLAUDE.md for the range map or the file-it-upstream rule and now point at MAINTENANCE.md: include/akbasic/error.h, src/runtime_disk.c and three entries in TODO.md. Both source changes are comments. deps/libakgl/TODO.md cites it too and is left alone; it is a submodule, and the rule it quotes is still reachable from CLAUDE.md. ctest is green at 95 of 95, docs_examples included: 36 programs, 9 transcripts, 44 output comparisons, 3 C snippets, 1 excerpt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 22:59:43 -04:00
there. Growing the dependency to serve the interpreter is a wanted outcome, not a detour.
It works. Four gaps were filed this way — text measurement, immediate-mode drawing, audio,
and a non-blocking keystroke read — and all four landed upstream as `akgl_text_measure`, the
`akgl_draw_*` family, `akgl_audio_*` and `akgl_controller_poll_key`. `FILTER` is the one verb
still blocked on a gap, and `DIRECTORY` is refused pending an `opendir`/`readdir` wrapper in
`libakstdlib`. Both refuse at execution and say so, rather than being silently ignored: a
program that asks for a low-pass filter and gets an unfiltered square wave has been lied to.
### The Go reference
`deps/basicinterpret` is a Commodore BASIC 7.0 / Dartmouth BASIC dialect in Go with SDL2 +
SDL2_ttf. It is the behavioural spec: when a question about semantics comes up — "what does
`READ` do when it runs off the end of `DATA`?" — the answer lives in that code.
**That project is deprecated and this interpreter is no longer required to match it.**
`TODO.md` §0.1 retires the byte-for-byte fidelity constraint that several sections of that
file were originally written on. The corpus stays green as a *regression* suite; the Go
source stays readable as documentation of what the original did. Neither is binding.
**It is not a build or test dependency.** Both configurations have been configured, built and
run from scratch with it moved out of the tree. Its acceptance corpus is checked in at
`tests/language/` and its Commodore font at `assets/fonts/`.
Split the documentation by who reads it README.md was 577 lines and answered four different questions at once: what the project is, how to build it, every verb and function in the language, and how to maintain the test harness. The verb and function lists had already been written a second time in docs/11 and docs/12, which is how a list of that size goes stale -- there is no way to notice the two have drifted apart. README.md is now 150 lines and holds only what somebody evaluating the project needs: what it is, the quickstart, why it was rewritten in C, the five rules embedding imposes on the design, the two ways to use it, and where everything else lives. Technical detail goes to docs/, maintenance to MAINTENANCE.md. The akbasic_TextSink struct moved to docs/10-embedding.md rather than being deleted. It was the corpus's only `c excerpt=` block -- the check that caught the stale struct two commits ago -- so dropping it with the README would have quietly retired a test. docs/10 also stopped claiming README.md carries the full API surface and the pool limits, which the trim made false. CLAUDE.md went from 458 lines to 62, because almost none of it was agent-specific. The project goals, the Go reference and its architecture, the dependency version and ABI rules, the four ways an embedded build collides, the libakerror convention, the error-code range map and the style rules are all things a maintainer needs, and they are now in MAINTENANCE.md with one copy to keep true. CLAUDE.md points there and keeps only the rules no test enforces: tests in the same commit asserting the correct contract, file a missing dependency capability upstream, do not edit generated output or tests/reference/, co-author your commits. Four claims did not survive the move, having gone stale where nothing could notice: - "The repository is currently empty apart from its submodules -- no commits, no source tree, no build files." There are 43 commits. - libakgl's target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE) at deps/libakgl/CMakeLists.txt:44, described as inert but present. It is gone; only a historical mention in a comment remains. - "akbasic_init() claims 512-767." There is no akbasic_init. It is akbasic_error_register(), called from akbasic_runtime_init(). - Time-relative phrasing ("libakgl hit two of them in the last week"). Five places pointed at CLAUDE.md for the range map or the file-it-upstream rule and now point at MAINTENANCE.md: include/akbasic/error.h, src/runtime_disk.c and three entries in TODO.md. Both source changes are comments. deps/libakgl/TODO.md cites it too and is left alone; it is a submodule, and the rule it quotes is still reachable from CLAUDE.md. ctest is green at 95 of 95, docs_examples included: 36 programs, 9 transcripts, 44 output comparisons, 3 C snippets, 1 excerpt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 22:59:43 -04:00
```sh norun
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
```
#### Its architecture, and the two decisions that shaped ours
Scanner → parser → tree-walking runtime, one source line at a time: