Execute every documented example as a test
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 3m2s
akbasic CI Build / sanitizers (push) Successful in 3m52s
akbasic CI Build / coverage (push) Failing after 3m24s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Has been cancelled

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>
This commit is contained in:
2026-07-31 22:41:36 -04:00
parent 6f49f6a7f2
commit 342e4c07da
29 changed files with 1303 additions and 103 deletions

View File

@@ -1,6 +1,6 @@
This BASIC is styled after [Commodore BASIC 7.0](http://www.jbrain.com/pub/cbm/manuals/128/C128PRG.pdf) and the [Dartmouth BASIC from 1964](https://www.dartmouth.edu/basicfifty/basic.html). It is a C rewrite of [basicinterpreter](https://source.starfort.tech/andrew/basicinterpreter), which was itself built from the instructions for the Java implementation of Lox in [craftinginterpreters.com](https://craftinginterpreters.com) before striking off on its own. The Go version is vendored at `deps/basicinterpret` and is the behavioural specification: when a question about semantics comes up, the answer lives in that code. Its acceptance corpus is **checked in here** at [`tests/reference/`](tests/reference/README.md) and runs on every build, so nothing about building or testing this project needs that submodule any more.
```sh
```sh norun
git submodule update --init --recursive
cmake -S . -B build
cmake --build build --parallel
@@ -67,7 +67,7 @@ control flow at all. `libakerror`'s `ATTEMPT`/`CATCH`/`PASS` macros expand at th
so gcov attributes them to the caller and line coverage cannot see them. `scripts/mutation_test.py`
breaks the library many small ways and checks that the suite notices:
```sh
```sh norun
cmake --build build --target mutation # the whole src/ tree; slow, hours
python3 scripts/mutation_test.py --target src/value.c --list
python3 scripts/mutation_test.py --target src/value.c --threshold 70
@@ -78,6 +78,14 @@ or symbol-table key, so every `MAX - 1` off-by-one in a `strncpy` would have gon
and that `errno` was never asserted to be cleared before a `strtoll`, which is what stops a
stale `ERANGE` from failing a perfectly valid conversion.
Every example in `README.md` and in `docs/` is executed by the suite, and its output
compared byte for byte, as the CTest case `docs_examples`. Documentation goes stale because
the *code* moved, not because somebody edited a chapter, so it runs on every build rather
than on a docs path filter. It found four wrong examples the day it was added — two
transcripts carrying a leading space `PRINT` does not emit, a `struct` in this file that had
grown two members hours earlier, and a refusal message quoted with the wrong wording. The
fence-tag convention it reads is documented in [`MAINTENANCE.md`](MAINTENANCE.md).
The `Doxyfile` is configured the way `libakgl`'s is, including
`WARN_AS_ERROR = FAIL_ON_WARNINGS` — a doc block that documents some of a function's
parameters but not all of them fails the run, so `doxygen Doxyfile` is a gate rather than a
@@ -165,14 +173,24 @@ The following commands/verbs are implemented:
* `EXIT`: Exit a loop before it would normally finish
* `FOR` : Iterate over a range of values and perform (statement) or block each time.
```
```basic
10 FOR I# = 1 TO 5
20 REM Do some stuff in here
20 PRINT I#
30 NEXT I#
40 FOR J# = 1 TO 5 STEP 2
50 PRINT J#
60 NEXT J#
```
10 FOR I# = 1 TO 5 STEP 2
20 REM Do some stuff here
30 NEXT I#
```output
1
2
3
4
5
1
3
5
```
* `CLR`: Drop every variable and function definition, keeping the program
@@ -261,7 +279,7 @@ Unlike the Go version, none of these are bootstrapped by running a BASIC program
In addition to `DEF`, `GOTO` and `GOSUB`, this BASIC also implements subroutines that accept arguments, return a value, and can be called as functions. Example
```
```basic
10 DEF ADDTWO(A#, B#)
20 C# = A# + B#
30 RETURN C#
@@ -269,6 +287,10 @@ In addition to `DEF`, `GOTO` and `GOSUB`, this BASIC also implements subroutines
50 PRINT D#
```
```output
8
```
Subroutines must be defined before they are called. Subroutines share the global variable scope with the rest of the program.
# Embedding the interpreter
@@ -284,7 +306,7 @@ Four rules the library holds to, because a game engine cannot tolerate a scripti
A complete, compiled, runnable example is in [`examples/embed.c`](examples/embed.c) — it is built by every build and registered as a test, so it cannot rot. The shape is:
```c
```c wrap=hostvars
#include <akerror.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
@@ -335,7 +357,7 @@ type comes from the name's suffix, exactly as it does for BASIC code, so `HP#` i
and `NAME$` is a string. Every scalar is really a one-element array, which is why the subscript
list is `{0}` with a count of 1.
```c
```c wrap=hostvars
/* host -> script, before the script starts */
akerr_ErrorContext AKERR_NOIGNORE *host_set_int(akbasic_Runtime *obj, const char *name, int64_t value)
{
@@ -367,7 +389,7 @@ akerr_ErrorContext AKERR_NOIGNORE *host_get_int(akbasic_Runtime *obj, const char
Used like this, with `akbasic_variable_set_string` and `set_float` as the other two:
```c
```c wrap=hostcalls
CATCH(errctx, akbasic_runtime_load(&SCRIPT, PROGRAM));
CATCH(errctx, host_set_int(&SCRIPT, "HP#", 100)); /* seed */
@@ -411,7 +433,7 @@ a `GOSUB`, and still readable by the script afterwards.
`PRINT` writes through an `akbasic_TextSink`, which is a record of function pointers plus whatever state you hang off `self`:
```c
```c excerpt=include/akbasic/sink.h
typedef struct akbasic_TextSink
{
void *self;
@@ -419,6 +441,8 @@ typedef struct akbasic_TextSink
akerr_ErrorContext AKERR_NOIGNORE *(*writeln)(struct akbasic_TextSink *self, const char *text);
akerr_ErrorContext AKERR_NOIGNORE *(*readline)(struct akbasic_TextSink *self, char *dest, size_t len, bool *eof);
akerr_ErrorContext AKERR_NOIGNORE *(*clear)(struct akbasic_TextSink *self);
akerr_ErrorContext AKERR_NOIGNORE *(*moveto)(struct akbasic_TextSink *self, int col, int row);
akerr_ErrorContext AKERR_NOIGNORE *(*window)(struct akbasic_TextSink *self, int left, int top, int right, int bottom);
} akbasic_TextSink;
```