# 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.5.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.