2.0.0 makes the error pool and the status registry thread safe, and it is an ABI break carrying the soname to libakerror.so.2. The break is a quiet one: __akerr_last_ignored became thread-local and akerr_next_error() now returns a context that already holds a reference, so objects compiled against a 1.x header and linked against 2.x count every reference twice and never give a slot back. Nothing about that fails to link, which is exactly what a guard is for -- include/akbasic/error.h feature-tests AKERR_THREAD_SAFE instead of AKERR_FIRST_CONSUMER_STATUS, which 2.0.0 also still defines and which therefore no longer distinguishes anything. 2.0.1 is the release this band needed most. The default unhandled-error handler ended in exit(errctx->status), and a process exit status is one byte: AKBASIC_ERR_BASE is 512, and 512 truncates to 0, so an unhandled AKBASIC_ERR_SYNTAX reported success to anything watching $?. Every other code in the band came out as some unrelated error's number. akerr_exit() substitutes 125 for anything a byte cannot carry, and a probe raising AKBASIC_ERR_DEVICE through FINISH_NORETURN now exits 125 rather than 7. It was latent here rather than live -- src/main.c handles the context and returns EXIT_FAILURE, and every test with a top-level ATTEMPT carries a HANDLE_DEFAULT -- but "no caller relies on it today" is not a property a header can keep true. tests/version_check.c asserts the mapping and fails if AKBASIC_ERR_BASE ever stops truncating to zero, because that is the day this stops being about our base. Chapter 10 gains a threading section: libakerror is safe from any thread now, and this interpreter is not and has no lock anywhere in it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
152 lines
7.2 KiB
Markdown
152 lines
7.2 KiB
Markdown
# akbasic
|
|
|
|
A BASIC interpreter written in C, styled after [Commodore BASIC 7.0](http://www.jbrain.com/pub/cbm/manuals/128/C128PRG.pdf)
|
|
and the [Dartmouth BASIC of 1964](https://www.dartmouth.edu/basicfifty/basic.html). 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](https://source.starfort.tech/andrew/basicinterpreter), a Go
|
|
implementation that started from the Java Lox instructions in
|
|
[craftinginterpreters.com](https://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/`](tests/reference/README.md) and runs on every build — so nothing about
|
|
building or testing this project needs it.
|
|
|
|
## Quickstart
|
|
|
|
```sh norun
|
|
git submodule update --init --recursive
|
|
cmake -S . -B build
|
|
cmake --build build --parallel
|
|
ctest --test-dir build --output-on-failure
|
|
```
|
|
|
|
```sh norun
|
|
./build/basic # the REPL
|
|
./build/basic tests/reference/language/functions.bas # run a program
|
|
```
|
|
|
|
```basic
|
|
10 FOR I# = 1 TO 3
|
|
20 PRINT "HELLO " + I#
|
|
30 NEXT I#
|
|
```
|
|
|
|
```output
|
|
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](docs/03-the-language.md) explains both; [Chapter 13](docs/13-differences.md) 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:
|
|
|
|
```sh norun
|
|
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](https://source.starfort.tech/andrew/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()`, no `abort()`, no `panic`.
|
|
Errors come back as `akerr_ErrorContext *` for the host to handle. `FINISH_NORETURN` appears
|
|
only in a `main()`.
|
|
* **Nothing calls `malloc`.** Every object comes from a fixed pool inside `akbasic_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_Runtime` you own. Two
|
|
of them in one process do not interfere.
|
|
* **The host owns the loop.** `akbasic_runtime_run(rt, n)` executes at most `n` steps and
|
|
returns. A script containing `10 GOTO 10` costs you `n` steps 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](docs/README.md) 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:
|
|
|
|
```cmake
|
|
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`](examples/embed.c) and
|
|
[`examples/hostvars.c`](examples/hostvars.c) are complete and runnable, and both are built and
|
|
run by every build, so they cannot rot. [Chapter 10](docs/10-embedding.md) 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`](TODO.md) and summarised for a BASIC programmer in
|
|
[Chapter 13](docs/13-differences.md).
|
|
|
|
## Where everything else lives
|
|
|
|
| | |
|
|
|---|---|
|
|
| [`docs/`](docs/README.md) | The guide: fourteen chapters, the language then each hardware area then a reference section for every verb and function, [Chapter 14](docs/14-architecture.md) on the interpreter's own architecture, and [Chapter 15](docs/15-error-codes.md) listing every error code |
|
|
| [`MAINTENANCE.md`](MAINTENANCE.md) | For contributors and maintainers: the documentation-example harness, the three test lists, mutation testing, error-code allocation, style |
|
|
| [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence |
|
|
| [`tests/reference/README.md`](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](https://source.starfort.tech/andrew/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](https://source.starfort.tech/andrew/libakstdlib) 0.2.0 — libc wrappers that
|
|
report through `libakerror`. String-to-number conversion goes straight to it, which is why
|
|
`VAL("garbage")` is an error rather than a silent `0`.
|
|
* [libakgl](https://source.starfort.tech/andrew/libakgl) 0.4.0 — **optional**, only for
|
|
`-DAKBASIC_WITH_AKGL=ON`. Pulls in SDL3. Its soname carries `MAJOR.MINOR` while the major is 0,
|
|
so rebuild rather than relink.
|
|
* [basicinterpret](https://source.starfort.tech/andrew/basicinterpret) — the Go original.
|
|
Not linked, not built, and safe to omit.
|