ER# held numbers nothing explained. The appendix lists the four error classes the interpreter prints, the eight codes it owns and what raises each, and the codes that reach ER# from errno, libakerror and libakgl underneath it. The table is not asserted. A program in the chapter trips seven of the eight and prints what it got, and docs_examples byte-compares the result -- so the numbers are checked rather than claimed. The eighth, 516, is not usefully trappable and the chapter says why: entering a handler takes a scope, and the pool being empty is what raised it. Two things worth a reader's attention came out of writing it. Two codes register the same ERR() text, so a program must compare the number and print the text. And VAL reports libakerror's Value Error rather than the interpreter's 517, which makes that number the platform's rather than ours -- filed as section 6 item 21, not fixed here, because deciding which libakstdlib failures to translate is a boundary question and ENOENT out of DOPEN is the counter-case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
151 lines
7.1 KiB
Markdown
151 lines
7.1 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) 1.0.0 — TRY/CATCH-style error
|
|
contexts. Every function that can fail returns one.
|
|
* [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.
|