# Maintaining akbasic `README.md` is for people using the interpreter. `docs/` is for people writing BASIC in it. `CLAUDE.md` is for agents. This file is for whoever has to change the thing: the conventions that are enforced by a test rather than by a comment, and the ones that are not enforced at all and therefore have to be written down. --- ## Editing the documentation **Every fenced block in `README.md` and `docs/*.md` is executed by `ctest`.** The test is `docs_examples`; it runs in both build configurations and it fails the build. It caught four wrong examples the day it was added — two transcripts showing a leading space `PRINT` does not emit, a `struct` in `README.md` that had grown two members hours earlier, and a `FILTER` refusal quoted with the wrong wording. An **untagged block is a failure**, not a default. That is deliberate: the way a harness like this dies is by quietly matching nothing and passing, so leaving a block untagged is a missing decision rather than a free pass. ### The tags | Info string | What happens | |---|---| | `basic` | Written to a file and run. Must exit 0 and print no `? line : CLASS` error | | `basic repl` | Fed to a fresh interpreter on **stdin**. The leading `READY` banner is dropped before comparison | | `basic norun` | Shown, not run. For fragments and for anything that loops forever | | `basic requires=akgl` | Run only in the `-DAKBASIC_WITH_AKGL=ON` build | | `basic requires=noakgl` | Run only in the default build. For verbs whose *refusal* is the example | | `basic setup=NAME` | Runs `tests/docs_setups/NAME.sh` in the sandbox first | | `output` | The **exact stdout** of the block above it, compared byte for byte | | `c` | Compiled with `-fsyntax-only -std=gnu99 -Wall -Wextra -Werror` against the real include path | | `c wrap=NAME` | The same, with `tests/docs_preludes/NAME.pre` before it and `NAME.post` after | | `c excerpt=PATH` | Must appear in `PATH`, ignoring comments and whitespace. Not compiled | | `c norun` | Shown, not compiled | | `sh` | Run in a sandbox with the built interpreter at `./build/basic`. Must exit 0. A leading `$ ` is stripped | | `sh setup=NAME` | The same, after `tests/docs_setups/NAME.sh` | | `sh norun` | Shown, not run | | `cmake` | **Never executed.** Hand-maintained, by decision | Attributes combine: `basic requires=akgl setup=ship` is a real tag in `docs/08-sprites.md`. ### Which one to reach for - **A program with visible output** — `basic` plus an `output` block. Preferred: it is the only shape that pins what the reader will actually see. - **A program with no output** — `basic` alone. The harness still asserts it parses, runs, and raises nothing. - **An interactive transcript** — `basic repl`, with the input in one block and the output in the next. A block that interleaves the two cannot be checked mechanically, and splitting it is not a loss: the piped interpreter prints `READY` once at startup rather than after each line, so an interleaved transcript is not literally true anyway. - **A verb that draws, plays or moves** — `basic requires=akgl`. These produce no stdout, so they get no `output` block; the assertion is that they do not refuse. - **A refusal message** — write the program that provokes it and put the message in an `output` block. Refusals carry a trailing blank line; the block has to have it too. - **`norun`** — for a fragment (`60 DATA 255, 129, ...`), an infinite loop, or a command that would reconfigure the tree the suite is running inside. Say why in the prose. ### `sh` blocks: what is not run, and why Five of the seven shell blocks are `norun`, and the reason is the same for all of them: they operate on the build tree the test is running inside, or they need the network. | Block | Why | |---|---| | `git submodule update --init --recursive` | Network, and the sandbox is not a repository | | `cmake -S . -B build` | Would reconfigure the live build tree mid-test | | `ctest --test-dir build` | Re-enters this suite | | `doxygen Doxyfile` | Slow, and writes into the build tree | | the `mutation` target | Hours | That leaves a real gap: a renamed CMake option in `README.md` would go unnoticed. It could be closed later with an `excerpt`-style check against `CMakeLists.txt`. It has not been. ### Preludes `tests/docs_preludes/NAME.pre` and `NAME.post` bracket a `c wrap=NAME` block. They exist so an example can be written the way a reader wants to read it — `PASS(e, akbasic_runtime_global(...))` and nothing else — while still compiling. What belongs in a prelude: - Symbols the example **invents** to stay readable: `your_clock_ms()`, `my_renderer`. - Scaffolding the macro protocol requires: a function body, a `PREPARE_ERROR`, an `ATTEMPT` for a block of `CATCH` calls. - Includes the surrounding prose already listed and the block does not repeat. What does **not** belong in a prelude is anything that would let a wrong example compile. A prelude declaring `akbasic_runtime_init` itself, for instance, would defeat the check. Compiler diagnostics point at the markdown: the harness emits a `#line` directive, so a broken example reports as `README.md:322: error: too few arguments`. ### Excerpts `c excerpt=include/akbasic/sink.h` says the block is a copy of something in that header and must still match it, comments and whitespace ignored. Use it where compiling the block would be wrong — echoing a `typedef` compiles only by redefining the type. This is the check that caught the stale `akbasic_TextSink`, and a compile check could not have. ### When `docs_examples` fails The message names the file and the line of the block. For an output mismatch it prints both sides through `cat -A`, because the errors it catches are trailing spaces and missing newlines, which a plain diff renders invisibly. **Fix the documentation, not the expectation** — unless the interpreter is what changed, in which case fix the interpreter first and the documentation second. Never edit an `output` block to match output you have not looked at. Run one document at a time while you work: ```sh norun ./tests/docs_examples.sh --root . --basic ./build/basic \ --cflags-file build/docs_cflags.txt docs/04-control-flow.md ``` The pass line reports **what it executed, by kind**. Read it. A harness that passes because it stopped matching anything looks exactly like a harness that passes because the documentation is correct, and the count is the only thing that tells them apart — it has already caught one such case, where a CMake generator expression evaluated to an empty argument that the script read as a filename. --- ## Tests ### Three lists, and two of them invert "passed" `CMakeLists.txt` declares `AKBASIC_TESTS`, `AKBASIC_WILL_FAIL_TESTS` and `AKBASIC_KNOWN_FAILING_TESTS`. The first must exit 0. The second aborts by design. The third **asserts the correct contract for a defect that is documented in `TODO.md`** and is expected to fail. A green `ctest` therefore does not mean defect-free. When a known-failing test starts passing, CTest reports "unexpectedly passed" — that is the cue to move it into `AKBASIC_TESTS` along with the fix, not to delete it. `AKBASIC_WILL_FAIL_TESTS` is currently empty and is kept declared anyway, so the shape is there when it is next needed. ### Test target names Every test program builds as `akbasic_test_` while registering under the bare CTest name. That is not cosmetic: `add_executable` creates a dependency's targets even under `EXCLUDE_FROM_ALL`, and when `libakstdlib` added a `test_version` it collided with `libakgl`'s and stopped the configure dead. ### The golden corpora `tests/reference/` is the Go implementation's own acceptance suite, byte-compared. **Nothing in it is ever edited to suit this interpreter.** If a case fails, either this interpreter is wrong or the divergence is deliberate — and a deliberate one goes in `TODO.md` and `docs/13-differences.md`, not into the expectation file. `tests/reference/README.md` says the same thing at more length. `tests/language/` is ours and may be changed freely. ### Mutation-check a fix before you believe it Coverage says a line ran; it does not say anything would have noticed if it were wrong. That matters more here than usual, because the `akerror` macros expand at their call sites and `gcov` attributes them to the caller. The discipline for any fix with a test: **revert the fix, confirm the test fails, restore it.** Use a file copy, not `git checkout` — `git checkout -- src` in a loop like this has already wiped a session's worth of unrelated edits. ```sh norun cmake --build build --target mutation # whole tree; hours python3 scripts/mutation_test.py --target src/value.c --threshold 70 ``` ### Build trees stay out of the source directory `gcovr` searches for `.gcda`/`.gcno` under its `--root`, and the dependencies set that to their *source* directory — so a leftover instrumented build tree in the source dir is folded into the report and a coverage run fails before any test executes. `build*/` is `.gitignore`d, which makes the state easy to reach and hard to see. Always `cmake -S . -B build`. --- ## Code ### The verb table is sorted, and a test says so `src/verbs.c` is searched with `bsearch`. A mis-sorted table does not fail to compile — it silently fails to find a verb, and the symptom is `Unknown command PRINT` a long way from the cause. `tests/verbs_table.c` asserts the ordering; adding a verb in the wrong place fails there. ### Error codes `akbasic` owns **512–767** under the owner string `"akbasic"`, reserved in `akbasic_runtime_init()`. Codes are an `enum` so they stay compile-time integer constants — `HANDLE`'s `case` labels require that. Never define one as an offset from another library's symbol; the coordinated map across the whole dependency stack is in `CLAUDE.md`. ### Nothing in the library terminates the process Goal 3. `FINISH_NORETURN` appears only in a `main()` — today the driver's and the examples'. A *script's* error is reported through the sink and swallowed; an *interpreter* error propagates out as `akerr_ErrorContext *` for the host. Both halves of that need saying, because the second one is easy to get right and the first one is easy to get wrong: `process_line_run()` swallowed its context correctly from the start while the direct-mode branch of `process_line_repl()` used a bare `PASS`, so a `VERIFY` against a file that did not match — an ordinary user answer — tore down the driver with a stack trace. ### Generated files Never hand-edit `build/` trees, the generated `akerror.h`, `akgl.pc`, or `include/akgl/SDL_GameControllerDB.h`. Change the template or the generator. ### Style C99, four-space indent, braces on their own line for function bodies, spaces inside control-flow parentheses. **Match the surrounding file** — several mix tabs and spaces and there is no repo-wide formatter. Do not reformat code you are not otherwise changing; style conversions get their own commit. Public symbols take the `akbasic_` prefix, `akbasic_TypeName` for types, `AKBASIC_` for macros. `static` helpers drop it.