It captured the HUD strip into sprite 1 and the whole play field into sprite 2, and its own header called them "the screen". That was never a choice: a drawing lasted one frame, so a sprite was the only thing the interpreter would redraw for nothing. Both slots are free now, and five things went with them. **`WINDOW 0, 35, 49, 36` is the first executable line.** The text layer repaints every row it owns and by default owns the whole window; moved to two rows at the bottom, it leaves the other thirty-five to the drawing verbs, and what the program draws stays. **`PACE` is gone.** It spun on `TI#` until a batch boundary so an `SSHAPE` capture would not be cut in half by a present. Nothing is captured, so a drawing spanning two batches simply arrives over two frames and the layer keeps both halves. The 256-line drawing deadline that shaped every draw routine in this listing does not apply to it any more. **The flattened live list is gone** -- `BUILDLIVE`, `BUILDROW`, `STAMPROW` and the four arrays behind them. They existed so a full-field redraw could be one stamp and an advance per brick, and a full-field redraw existed because removing one brick meant rebuilding all sixty. A broken brick is now erased in place with a blank stamp, so the field is drawn once per level and not again. **`BALLBRICKS` and `TESTCELL` are gone** -- about sixty lines that turned the ball's box into a range of candidate cells, walked them with a nested `DO`, and computed an overlap rectangle to pick an axis. `SOLID` registers each brick as the level is laid out, `COLLISION 2` fires, and `RCOLLISION` says which brick, which way out, how far, and which axis to reverse. **The `SSHAPE` pool accounting is gone.** `SHN#` counted slots because every frame's capture spent another and `GRAPHIC 5` had to throw them all away periodically; eight are now spent once and never again. Three mistakes worth recording, because each was silent: - **The bricks were registered and then immediately thrown away.** `GOSUB BUILDLIVE` sat *after* the fill loop in `SETUPLEVEL` and meant "rebuild the list of what is standing"; replaced in place by a bare `SOLID`, it meant "retire everything" and ran after all sixty had been registered. Nothing failed -- the ball simply passed through the wall. - **The handler tested the wrong bits.** `BUMP` is a mask by *sprite*, and the balls are sprites 5, 6 and 7, so their bits are 16, 32 and 64. Testing 1, 2 and 4 asked about the HUD and field sprites that no longer exist. - **The eraser stamps were undeclared**, so `SSHAPE` filled them inside `DRAWPROTOS` and they were empty everywhere else -- exactly the scope trap chapter 17 Step 3 is about, found by printing the handle inside the routine and again outside it. Verified by driving it: a copy with a tracking paddle and auto-relaunch breaks thirteen bricks in sixty seconds with no error line, and the shipped listing runs clean. Both suites green. Chapter 18 still describes the architecture this commit removed; that is the rest of section 6 item 39. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
akbasic
A BASIC interpreter written in C, styled after Commodore BASIC 7.0
and the Dartmouth BASIC of 1964. It runs a
.bas file, it gives you a prompt, and — the point of the exercise — it links into a C program
as a scripting engine.
It is a rewrite of basicinterpreter, a Go
implementation that started from the Java Lox instructions in
craftinginterpreters.com and then struck off on its own. That
project is deprecated. It is vendored here as the behavioural spec to read when a question about
semantics comes up, and its acceptance corpus is checked in at
tests/reference/ and runs on every build — so nothing about
building or testing this project needs it.
Quickstart
git submodule update --init --recursive
cmake -S . -B build
cmake --build build --parallel
ctest --test-dir build --output-on-failure
./build/basic # the REPL
./build/basic tests/reference/language/functions.bas # run a program
10 FOR I# = 1 TO 3
20 PRINT "HELLO " + I#
30 NEXT I#
HELLO 1
HELLO 2
HELLO 3
Two things in that program are not Commodore BASIC and will catch you out immediately: variables
carry a type suffix (I# is an integer), and + concatenates a string with a number.
Chapter 3 explains both; Chapter 13 is the
whole list of what differs from a C128.
Graphics, sound and sprites need the SDL build, which is off by default because the interpreter and its entire test suite build on a machine with no SDL installed at all:
cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON
cmake --build build-akgl --parallel
That basic is a different program: it opens a window, draws BASIC output into it in the
Commodore font, and still puts every byte on stdout.
Why rewrite it in C?
Three reasons, in the order they matter.
The interpreter is meant to end up inside libakgl as a scripting engine for game authors, and libakgl is C. Embedding a Go runtime in a C game is not a thing anybody should do to themselves.
The Go version was already written against static pools and explicit state structs — a fixed source table, a fixed variable pool, a 32-leaf ceiling per line — so it ports across almost directly. It reads like C that happens to be spelled in Go.
And the port is a good excuse to find out what the original actually does, as opposed to what it looks like it does. It found five defects nobody knew about.
Design philosophy
A game engine cannot tolerate a scripting language that surprises it. Five rules follow from that, and between them they explain most of what looks unusual in this interpreter:
- Nothing in the library terminates the process. No
exit(), noabort(), nopanic. Errors come back asakerr_ErrorContext *for the host to handle.FINISH_NORETURNappears only in amain(). - Nothing calls
malloc. Every object comes from a fixed pool insideakbasic_Runtime. Exhausting one is a diagnosable error, not a crash and not a slow leak. - No file-scope mutable state. Interpreter state lives in an
akbasic_Runtimeyou own. Two of them in one process do not interfere. - The host owns the loop.
akbasic_runtime_run(rt, n)executes at mostnsteps and returns. A script containing10 GOTO 10costs younsteps per frame and nothing else. - Hardware is a record of function pointers. Graphics, audio, input and sprites attach as
backends, and any of them may be
NULL— that is how a host withholds a capability, and how a host that renders some other way never links libakgl at all.
Nothing is silently ignored, either. A verb that needs a device it was not given, or a capability nothing underneath can supply, refuses by name and says why.
Two ways to use it
As a program. basic is a REPL and a script runner, and the standalone driver owns the
things a library has no business owning: argv, QUIT, and the window in the SDL build.
The guide is written for this reader.
As a library. A host links akbasic::akbasic, hands the interpreter a script, and steps it a
frame at a time:
add_subdirectory(deps/akbasic EXCLUDE_FROM_ALL)
target_link_libraries(YOUR_GAME PRIVATE akbasic::akbasic)
Host and script exchange variables through the same pool the script itself uses — no marshalling
layer and no copy. PRINT goes through an akbasic_TextSink the host supplies, so a game draws
BASIC output into its own text layer. examples/embed.c and
examples/hostvars.c are complete and runnable, and both are built and
run by every build, so they cannot rot. Chapter 10 walks through the API.
What state it is in
Everything the Go version does, and by now a good deal more. All 41 .bas files of the
reference's corpus produce the expected output, including error messages, each as a separate
CTest case so a failure names the file.
What is still missing, what is refused deliberately, and the eleven defects inherited from the Go
version are catalogued in TODO.md and summarised for a BASIC programmer in
Chapter 13.
Where everything else lives
docs/ |
The guide: eighteen chapters, the language then each hardware area then a reference section for every verb and function, Chapter 14 on the interpreter's own architecture, Chapter 15 listing every error code, and Chapters 17 and 18 building a whole game twice |
MAINTENANCE.md |
For contributors and maintainers: the documentation-example harness, the three test lists, mutation testing, error-code allocation, style |
TODO.md |
Outstanding defects, with file, line and consequence |
tests/reference/README.md |
Where the golden corpus came from, and the rule for changing it |
API documentation builds with doxygen Doxyfile, into build/docs/html.
Dependencies
Everything is a submodule; git submodule update --init --recursive gets all of it. There is
nothing to install first.
- libakerror 2.0.1 — TRY/CATCH-style error contexts. Every function that can fail returns one. 2.0.0 made it thread safe and broke the ABI; anything built against a 1.x header must be rebuilt rather than relinked.
- libakstdlib 0.2.0 — libc wrappers that
report through
libakerror. String-to-number conversion goes straight to it, which is whyVAL("garbage")is an error rather than a silent0. - libakgl 0.8.0 — optional, only for
-DAKBASIC_WITH_AKGL=ON. Pulls in SDL3. Its soname carriesMAJOR.MINORwhile the major is 0, so rebuild rather than relink. - basicinterpret — the Go original. Not linked, not built, and safe to omit.