From 342e4c07da858e7dd12c1e2d560973e62f7077bf Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Fri, 31 Jul 2026 22:41:36 -0400 Subject: [PATCH] Execute every documented example as a test 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) --- CMakeLists.txt | 53 +++ MAINTENANCE.md | 225 +++++++++++++ README.md | 48 ++- TODO.md | 36 +- docs/01-introduction.md | 11 +- docs/02-getting-started.md | 72 +++- docs/03-the-language.md | 53 ++- docs/04-control-flow.md | 107 +++++- docs/05-strings-and-formatting.md | 36 +- docs/06-graphics.md | 19 +- docs/07-sound.md | 15 +- docs/08-sprites.md | 15 +- docs/09-files-and-disk.md | 43 ++- docs/10-embedding.md | 8 +- docs/13-differences.md | 9 +- docs/README.md | 18 +- tests/docs_examples.sh | 512 +++++++++++++++++++++++++++++ tests/docs_preludes/akglbody.post | 2 + tests/docs_preludes/akglbody.pre | 21 ++ tests/docs_preludes/hostbody.post | 2 + tests/docs_preludes/hostbody.pre | 22 ++ tests/docs_preludes/hostcalls.post | 6 + tests/docs_preludes/hostcalls.pre | 27 ++ tests/docs_preludes/hostloop.post | 2 + tests/docs_preludes/hostloop.pre | 13 + tests/docs_preludes/hostvars.pre | 10 + tests/docs_setups/program.sh | 8 + tests/docs_setups/ship.sh | 6 + tests/docs_setups/textfiles.sh | 7 + 29 files changed, 1303 insertions(+), 103 deletions(-) create mode 100644 MAINTENANCE.md create mode 100755 tests/docs_examples.sh create mode 100644 tests/docs_preludes/akglbody.post create mode 100644 tests/docs_preludes/akglbody.pre create mode 100644 tests/docs_preludes/hostbody.post create mode 100644 tests/docs_preludes/hostbody.pre create mode 100644 tests/docs_preludes/hostcalls.post create mode 100644 tests/docs_preludes/hostcalls.pre create mode 100644 tests/docs_preludes/hostloop.post create mode 100644 tests/docs_preludes/hostloop.pre create mode 100644 tests/docs_preludes/hostvars.pre create mode 100644 tests/docs_setups/program.sh create mode 100644 tests/docs_setups/ship.sh create mode 100644 tests/docs_setups/textfiles.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e927b5..a57c0a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -372,6 +372,59 @@ if(AKBASIC_WITH_AKGL) SKIP_RETURN_CODE 77) endif() +# --------------------------------------------------------------------------- +# The documentation's own examples. +# +# docs/ and README.md are full of programs, transcripts, C snippets and shell +# commands, and every one of them was checked by hand exactly once -- when it +# was written. Three were already wrong the day this was added, one of them a +# struct that had grown two members hours earlier. Documentation goes stale +# because the *code* moved, so this is an ordinary test case rather than +# something a docs-only CI job runs: it fails when the interpreter changes under +# the chapter, which is the case a path filter would miss. +# +# tests/docs_examples.sh reads the fence info strings; MAINTENANCE.md documents +# them. +# +# The C snippets compile against the real include path, which is transitive +# through akerror, akstdlib and -- in this configuration -- akgl. Writing it out +# for the script to read keeps that one source of truth: a hardcoded -I list +# here would rot exactly the way the documentation does. +set(AKBASIC_DOCS_CFLAGS_FILE "${CMAKE_CURRENT_BINARY_DIR}/docs_cflags.txt") +if(AKBASIC_WITH_AKGL) + set(AKBASIC_DOCS_CFLAGS_TARGET akbasic_akgl) +else() + set(AKBASIC_DOCS_CFLAGS_TARGET akbasic) +endif() +file(GENERATE + OUTPUT "${AKBASIC_DOCS_CFLAGS_FILE}" + CONTENT "-I$,\n-I>\n" +) + +# Spelled out with if() rather than $<$:--akgl>, because a +# generator expression that evaluates to nothing still contributes an *empty +# argument*. The script read that empty string as the first filename, checked +# no documents at all, and passed -- caught only because it reports what it ran. +set(AKBASIC_DOCS_ARGS + --root "${CMAKE_CURRENT_SOURCE_DIR}" + --basic $ + --cflags-file "${AKBASIC_DOCS_CFLAGS_FILE}") +if(AKBASIC_WITH_AKGL) + list(APPEND AKBASIC_DOCS_ARGS --akgl) +endif() + +_add_test(NAME docs_examples + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/docs_examples.sh ${AKBASIC_DOCS_ARGS}) +_set_tests_properties(docs_examples PROPERTIES + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + TIMEOUT 300) +if(AKBASIC_WITH_AKGL) + # Same reason as the golden cases: an AKGL `basic` opens a window, and the + # chapters on graphics, sound and sprites are most of what runs here. + _set_tests_properties(docs_examples PROPERTIES + ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software") +endif() + if(AKBASIC_TESTS OR AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS) _set_tests_properties( ${AKBASIC_TESTS} ${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS} diff --git a/MAINTENANCE.md b/MAINTENANCE.md new file mode 100644 index 0000000..b48da5d --- /dev/null +++ b/MAINTENANCE.md @@ -0,0 +1,225 @@ +# 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. diff --git a/README.md b/README.md index 990ceac..92db5ac 100644 --- a/README.md +++ b/README.md @@ -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 #include #include @@ -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; ``` diff --git a/TODO.md b/TODO.md index 750f2cf..6320b95 100644 --- a/TODO.md +++ b/TODO.md @@ -1527,8 +1527,9 @@ requirement; exactly one case has diverged on purpose since, and | Gate | Result | |---|---| -| `ctest` | 77/77 — 41 reference golden cases, 9 local ones, 25 unit tests, 2 embedding examples. No known-failing tests: the list is empty | -| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 77/77 on a machine with a display; 76 passed + 1 skipped headless. The same set minus the three `no_device` cases the SDL driver contradicts, plus `akgl_backends`, `akgl_frontend` and `akgl_typing` — the last of which is the skip, and the `akgl_build` CI job is where it skips | +| `ctest` | 95/95 — 41 reference golden cases, 15 local ones, 36 unit tests, 2 embedding examples, and `docs_examples` | +| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 94/94 on a machine with a display; 93 passed + 1 skipped headless. The same set minus the three `no_device` cases the SDL driver contradicts, plus `akgl_backends`, `akgl_frontend` and `akgl_typing` — the last of which is the skip, and the `akgl_build` CI job is where it skips | +| `docs_examples` | Every fenced block in `README.md`, `MAINTENANCE.md` and `docs/` executed and byte-compared: 37 programs, 9 transcripts, 45 output comparisons, 6 C snippets, 1 excerpt and 2 shell blocks in the default build; 50 programs and 7 C snippets in the AKGL one. `MAINTENANCE.md` documents the fence-tag convention | | Golden corpus | 41/41 byte-exact from `tests/reference/` — **and 41/41 again through the SDL binary**, which is most of what proves the frontend changes no output | | ASan + UBSan | 77/77 | | Line coverage | 94.6% (3857/4076) — above the 90% gate | @@ -1537,6 +1538,25 @@ requirement; exactly one case has diverged on purpose since, and | `doxygen Doxyfile` | clean | | Mutation (`src/symtab.c`) | 74.1%, against a gate of 65 — see `.gitea/workflows/ci.yaml` | +**Two defects surfaced from writing that harness**, both fixed with a test that fails when the +fix is reverted: + +1. **`DLOAD` overwrote the last line of every program it loaded from a prompt.** It scans the + file through the runtime's own scanner, so on return the tokens of the calling line are gone + and `hadlinenumber` and `environment->lineno` describe the last line of the *file*. The REPL + carried on parsing what it believed was still its own buffer, decided the leftovers were + program text, and filed the `DLOAD` command under that line number. + `src/runtime_commands.c` now sets `skiprestofline`. `tests/disk_verbs.c`. +2. **A BASIC error typed at the prompt terminated the driver.** `process_line_run()` had always + swallowed the context after `interpret()` put the error line on the sink — goal 3, in the + comment, in so many words — but the direct-mode branch of `process_line_repl()` used a bare + `PASS`. A `VERIFY` against a file that did not match, which is an ordinary user answer rather + than a fault, came out as a stack trace and would have taken an embedding host with it. + `tests/housekeeping_verbs.c`. + +Neither was on any list. Both were found by writing down what a chapter claimed and then +running it — which is the argument for the harness rather than a footnote to it. + Coverage of the verb groups added for goal 2, since they are the new surface: `src/audio_tables.c` and `src/graphics_tables.c` 100%, `src/runtime_input.c` 100%, `src/runtime_graphics.c` and `src/runtime_audio.c` 98%, `src/play.c` 87%. The `akbasic_akgl` target is not in the coverage @@ -1707,3 +1727,15 @@ What remains, in priority order: `src/value.c` takes about 70 minutes to mutate in full (368 mutants, ~11s each, because almost everything links against it), which is why CI runs `src/symtab.c` instead and the whole-tree run is a local `cmake --build build --target mutation`. + +9. Use the actual full SDL window for graphics operation width + +The current documentation states that graphics operations always use a 320x200 space regardless of the window's size. These verbs should be able to access the entirety of the SDL window to which they are bound. + +11. Error codes are undefined for the user + +In the documentation, include a complete table of all possible values of ER# and EL# in the documentation in an appendix. + +12. Screenshots for graphics operations + +Chapter 8 of the documentation should include screenshots of various graphics operations being performed. diff --git a/docs/01-introduction.md b/docs/01-introduction.md index 5511d87..efd79d2 100644 --- a/docs/01-introduction.md +++ b/docs/01-introduction.md @@ -33,7 +33,7 @@ The default build has no dependency on SDL and no graphics, sound, sprites or wi text. Everything else works, and the whole test suite runs on a machine with no SDL installed at all. -```sh +```sh norun cmake -S . -B build cmake --build build ``` @@ -42,7 +42,7 @@ The SDL build adds a window, the Commodore font, and the graphics, sound and spr devices. It needs [libakgl](https://source.starfort.tech/andrew/libakgl), which is vendored as a submodule. -```sh +```sh norun git submodule update --init --recursive cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON cmake --build build-akgl @@ -51,8 +51,13 @@ cmake --build build-akgl A verb that needs a device the build does not have does not crash and does not lie. It reports itself by name: +```basic requires=noakgl +10 DRAW 1, 0, 0 TO 100, 100 ``` + +```output ? 10 : RUNTIME ERROR DRAW needs a graphics device and this runtime has none + ``` That is the same message an embedded host sees when it deliberately withholds a @@ -60,7 +65,7 @@ device — a game may want a script that can print but not draw. ## Running the tests -```sh +```sh norun ctest --test-dir build --output-on-failure ``` diff --git a/docs/02-getting-started.md b/docs/02-getting-started.md index 454bc13..db915b0 100644 --- a/docs/02-getting-started.md +++ b/docs/02-getting-started.md @@ -4,7 +4,7 @@ Run `basic` with no arguments and you get a prompt: -``` +```sh norun $ ./build/basic READY ``` @@ -15,21 +15,33 @@ and after a program stops. Anything you type **with a line number** is stored as part of a program. Anything you type **without** one runs immediately: -``` +```basic repl PRINT 2 + 2 - 4 -READY +``` + +```output +4 ``` ## Your first program -``` +```basic repl 10 PRINT "WHAT IS YOUR NAME" 20 INPUT "> " N$ 30 PRINT "HELLO, " + N$ RUN +ADA ``` +```output +WHAT IS YOUR NAME +> HELLO, ADA +READY +``` + +The last line of the first block is not part of the program — it is what you type when +`INPUT` asks. + Type `LIST` to see it back, `RUN` to run it again, and `NEW` to throw it away. ## Line numbers @@ -37,11 +49,14 @@ Type `LIST` to see it back, `RUN` to run it again, and `NEW` to throw it away. Lines are stored under their numbers and run in numeric order, so the gaps are what let you insert later: -``` +```basic repl 10 PRINT "FIRST" 30 PRINT "THIRD" 20 PRINT "SECOND" LIST +``` + +```output 10 PRINT "FIRST" 20 PRINT "SECOND" 30 PRINT "THIRD" @@ -58,34 +73,53 @@ it off again. Statements are separated by colons: -``` +```basic 10 A# = 1 : B# = 2 : PRINT A# + B# ``` +```output +3 +``` + There is one important limit: **block structures do not work inside a single line.** -``` +```basic 10 FOR I# = 1 TO 3 : PRINT I# : NEXT I# +20 PRINT "DONE" +``` + +```output +DONE ``` prints nothing at all. The loop body is skipped entirely, because the interpreter skips forward a *line* at a time looking for the `NEXT` and never finds one on the line it is already past. Write loops across several lines: -``` +```basic 10 FOR I# = 1 TO 3 20 PRINT I# 30 NEXT I# ``` +```output +1 +2 +3 +``` + The same applies to `DO`/`LOOP`. ## Running a file -```sh +```sh setup=program $ ./build/basic program.bas ``` +```output +HELLO FROM A FILE +``` + The file is read, stored, and run. It is exactly the same as typing the program in and saving yourself the trouble. @@ -96,13 +130,29 @@ $ echo '10 PRINT "HI" RUN' | ./build/basic ``` +```output +READY +HI +READY +``` + ## Saving and loading -``` +```basic repl +10 PRINT "HI" DSAVE "myprogram.bas" +NEW DLOAD "myprogram.bas" +LIST ``` +```output +10 PRINT "HI" +``` + +That is a whole round trip: save it, throw it away with `NEW`, load it back, and `LIST` +shows it again. + `SAVE` and `LOAD` are the same verbs under their other names. `VERIFY "myprogram.bas"` compares what is in memory against the file and prints `OK` if they match. diff --git a/docs/03-the-language.md b/docs/03-the-language.md index f4fd709..2d24a62 100644 --- a/docs/03-the-language.md +++ b/docs/03-the-language.md @@ -11,7 +11,7 @@ a character that says what it holds: | `%` | floating point | `RATE%`, `X%` | | `$` | string | `NAME$` | -``` +```basic 10 COUNT# = 42 20 RATE% = 1.5 30 NAME$ = "ADA" @@ -31,9 +31,12 @@ Variable names are **case sensitive**. Verb and function names are not: `print`, Integers are 64-bit. Floats are IEEE doubles, so they print with six decimal places: -``` +```basic repl PRINT 1.5 - 1.500000 +``` + +```output +1.500000 ``` Literals may be written in hexadecimal with a `0x` prefix. A leading zero is *not* @@ -47,16 +50,22 @@ escaping: a string cannot contain a double quote. `+` concatenates, and it will concatenate a string with a number: -``` +```basic repl PRINT "COUNT: " + 42 - COUNT: 42 +``` + +```output +COUNT: 42 ``` `*` repeats: -``` +```basic repl PRINT "-" * 20 - -------------------- +``` + +```output +-------------------- ``` ## Arrays @@ -64,12 +73,16 @@ PRINT "-" * 20 `DIM` makes one. Subscripts start at zero and the number you give is the *count*, so `DIM A#(3)` gives you `A#(0)` through `A#(2)`: -``` +```basic 10 DIM A#(3) 20 A#(0) = 10 : A#(1) = 20 : A#(2) = 30 30 PRINT A#(0) + A#(1) + A#(2) ``` +```output +60 +``` + Arrays can have several dimensions: `DIM GRID#(10, 10)`. `LEN(A#)` gives the total number of elements. @@ -93,7 +106,7 @@ In order of precedence, tightest first: Both mean equality **inside a condition**: -``` +```basic norun 10 IF A# = 5 THEN PRINT "FIVE" 20 IF A# == 5 THEN PRINT "ALSO FIVE" ``` @@ -108,20 +121,26 @@ and the reason `AND` and `OR` double as the logical operators: -1 is every bit s Anything non-zero is true, so `IF A# THEN ...` works: -``` +```basic 10 A# = 5 20 IF A# THEN PRINT "NON-ZERO IS TRUE" 30 IF A# = 5 AND A# > 1 THEN PRINT "AND WORKS" 40 IF NOT (A# = 9) THEN PRINT "SO DOES NOT" ``` +```output +NON-ZERO IS TRUE +AND WORKS +SO DOES NOT +``` + `AND` and `OR` are still bitwise on ordinary numbers: `PRINT 12 AND 10` gives `8`. ## Comments `REM` comments to the end of the line. -``` +```basic 10 REM This does nothing at all ``` @@ -129,20 +148,28 @@ Anything non-zero is true, so `IF A# THEN ...` works: `DEF` makes a single-expression function: -``` +```basic 10 DEF SQUARE(X#) = X# * X# 20 PRINT SQUARE(7) ``` +```output +49 +``` + A multi-line definition runs until `RETURN`, which is how you write a subroutine that takes arguments: -``` +```basic 10 DEF GREET(N$) 20 PRINT "HELLO, " + N$ 30 RETURN 0 40 X# = GREET("WORLD") ``` +```output +HELLO, WORLD +``` + `RETURN` carries the value back, so a multi-line `DEF` is a function even when you only wanted the effect — assign the result somewhere to throw it away. diff --git a/docs/04-control-flow.md b/docs/04-control-flow.md index 947d35e..a21c6c3 100644 --- a/docs/04-control-flow.md +++ b/docs/04-control-flow.md @@ -2,15 +2,24 @@ ## IF ... THEN ... ELSE -``` +```basic 10 A# = 5 20 IF A# = 5 THEN PRINT "FIVE" ELSE PRINT "NOT FIVE" ``` +```output +FIVE +``` + The condition is a whole expression, so `AND`, `OR` and `NOT` all work in one: +```basic +10 A# = 5 +20 IF A# > 0 AND A# < 10 THEN PRINT "IN RANGE" ``` -10 IF A# > 0 AND A# < 10 THEN PRINT "IN RANGE" + +```output +IN RANGE ``` **Everything after `THEN` on the line belongs to the condition**, which matters as soon @@ -29,32 +38,56 @@ The remainder always belongs to whichever arm was written *last*. `BEGIN` and `BEND` make an `IF` span lines: +```basic +10 A# = 5 +20 IF A# = 5 THEN BEGIN +30 PRINT "IN THE BLOCK" +40 PRINT "STILL IN IT" +50 BEND +60 PRINT "AFTER" ``` -10 IF A# = 5 THEN BEGIN -20 PRINT "IN THE BLOCK" -30 PRINT "STILL IN IT" -40 BEND -50 PRINT "AFTER" + +```output +IN THE BLOCK +STILL IN IT +AFTER ``` When the condition is false every line up to the `BEND` is skipped. ## FOR ... NEXT -``` +```basic 10 FOR I# = 1 TO 5 20 PRINT I# 30 NEXT I# ``` +```output +1 +2 +3 +4 +5 +``` + `STEP` sets the stride, and a negative one counts down: -``` +```basic 10 FOR I# = 10 TO 0 STEP -2 20 PRINT I# 30 NEXT I# ``` +```output +10 +8 +6 +4 +2 +0 +``` + The counter is an ordinary variable and the body may assign to it. Two things to know: - **The counter does not survive the loop.** It lives in the loop's own scope, so @@ -64,17 +97,22 @@ The counter is an ordinary variable and the body may assign to it. Two things to `EXIT` leaves the loop early: -``` +```basic 10 FOR I# = 1 TO 100 20 IF I# = 5 THEN EXIT 30 NEXT I# +40 PRINT "OUT" +``` + +```output +OUT ``` ## DO ... LOOP The condition can go on either end, or neither: -``` +```basic 10 I# = 0 20 DO WHILE I# < 3 30 PRINT I# @@ -82,7 +120,13 @@ The condition can go on either end, or neither: 50 LOOP ``` +```output +0 +1 +2 ``` + +```basic 10 I# = 0 20 DO 30 PRINT I# @@ -90,6 +134,12 @@ The condition can go on either end, or neither: 50 LOOP UNTIL I# = 3 ``` +```output +0 +1 +2 +``` + A condition on the `DO` is tested before the body, so the body may run zero times. A condition on the `LOOP` is tested after, so it runs at least once. `DO` with no condition at all loops forever until an `EXIT` or a `GOTO` leaves it. @@ -98,7 +148,7 @@ condition at all loops forever until an `EXIT` or a `GOTO` leaves it. ## GOTO and GOSUB -``` +```basic 10 GOSUB 100 20 PRINT "BACK" 30 END @@ -106,6 +156,11 @@ condition at all loops forever until an `EXIT` or a `GOTO` leaves it. 110 RETURN ``` +```output +IN THE SUBROUTINE +BACK +``` + `RETURN` goes back to the line after the `GOSUB`. ## Labels @@ -113,13 +168,17 @@ condition at all loops forever until an `EXIT` or a `GOTO` leaves it. A label is a name with no type suffix. It marks a line, and anything that takes a line number takes a label instead: -``` +```basic 10 GOTO SETUP 20 PRINT "SKIPPED" 100 LABEL SETUP 110 PRINT "ARRIVED" ``` +```output +ARRIVED +``` + **Labels are filed before the program runs**, so a forward `GOTO` works. This is worth using for its own sake: a program written with labels is immune to `RENUMBER`, because there is no number to rewrite. @@ -128,9 +187,20 @@ there is no number to rewrite. `ON` picks the *n*th target from a list, counting from one: +```basic +10 CHOICE# = 2 +20 ON CHOICE# GOTO 100, 200, 300 +30 PRINT "CHOICE WAS OUT OF RANGE" +40 END +100 PRINT "FIRST" +110 END +200 PRINT "SECOND" +210 END +300 PRINT "THIRD" ``` -10 ON CHOICE# GOTO 100, 200, 300 -20 PRINT "CHOICE WAS OUT OF RANGE" + +```output +SECOND ``` Out of range is not an error — it falls through to the next statement, which is what @@ -141,7 +211,7 @@ returns. `TRAP` sends an error to a handler instead of stopping the program: -``` +```basic 10 TRAP HANDLER 20 DIM Q#(2) 30 PRINT Q#(9) @@ -152,6 +222,11 @@ returns. 120 RESUME NEXT ``` +```output +CAUGHT Out Of Bounds ON LINE 30 +CARRIED ON +``` + Two variables are set when the trap fires: **`ER#`** is the error code and **`EL#`** is the line it happened on. `ERR(ER#)` gives the message text. On a C128 these are called `ER` and `EL` with no suffix; this dialect has no bare variable names. diff --git a/docs/05-strings-and-formatting.md b/docs/05-strings-and-formatting.md index b4358ab..24d31b9 100644 --- a/docs/05-strings-and-formatting.md +++ b/docs/05-strings-and-formatting.md @@ -22,7 +22,7 @@ string has gives you the whole string. `MID("HELLO WORLD", 6, 5)` is `WORLD`, and `INSTR("HELLO WORLD", "WORLD")` is `6`. A failed `INSTR` gives -1, not 0, so there is no ambiguity with a match at the start. -``` +```basic 10 A$ = "HELLO WORLD" 20 PRINT LEN(A$) 30 PRINT LEFT(A$, 5) @@ -31,6 +31,14 @@ failed `INSTR` gives -1, not 0, so there is no ambiguity with a match at the sta 60 PRINT INSTR(A$, "WORLD") ``` +```output +11 +HELLO +WORLD +WORLD +6 +``` + `VAL` refuses text that is not a number rather than quietly returning zero, so a program can tell "the user typed 0" from "the user typed nonsense". @@ -38,13 +46,13 @@ program can tell "the user typed 0" from "the user typed nonsense". `PRINT USING` lays a value out in a fixed field, which is what makes columns line up. -``` +```basic 10 PRINT USING "###.##"; 3.14159 20 PRINT USING "TOTAL: $#,###.##"; 1234.5 30 PRINT USING "####-"; -42 ``` -``` +```output 3.14 TOTAL: $1,234.50 42- @@ -68,11 +76,11 @@ stands: is loud: printing more digits than the field asked for would push every later column out of line. -``` +```basic 10 PRINT USING "###"; 99999 ``` -``` +```output *** ``` @@ -81,12 +89,12 @@ printing `-5` as `5` would be worse. String fields centre and right-justify: -``` +```basic 10 PRINT USING "=========="; "MID" 20 PRINT USING ">>>>>>>>>>"; "RIGHT" ``` -``` +```output MID RIGHT ``` @@ -100,12 +108,12 @@ Chapter 13. to four, in this order: the leading blank, the thousands separator, the decimal point, and the currency sign. -``` +```basic 10 PUDEF "*" 20 PRINT USING "#####"; 42 ``` -``` +```output ***42 ``` @@ -116,11 +124,17 @@ only the padding. `CHAR` puts text at a character position rather than at the cursor: -``` +```basic requires=akgl 10 CHAR 1, 10, 5, "HERE" +20 PRINT ``` -The arguments are colour, column, row and the text. It needs a text device with a +```output +HERE +``` + +The arguments are colour, column, row and the text. `CHAR` writes no newline of its +own and leaves the cursor after the text, which is why line 20 is there. It needs a text device with a cursor, which means the SDL build — a terminal's cursor is not this library's to move, and it refuses by name in the default build. diff --git a/docs/06-graphics.md b/docs/06-graphics.md index af1c5fb..974c438 100644 --- a/docs/06-graphics.md +++ b/docs/06-graphics.md @@ -3,8 +3,13 @@ Everything in this chapter needs the SDL build and a graphics device. Without one each verb refuses by name: +```basic requires=noakgl +10 DRAW 1, 0, 0 TO 100, 100 ``` + +```output ? 10 : RUNTIME ERROR DRAW needs a graphics device and this runtime has none + ``` ## The coordinate space @@ -19,7 +24,7 @@ screen. `COLOR` binds a *source* to a palette index, and the drawing verbs name the source rather than the colour: -``` +```basic requires=akgl 10 COLOR 1, 3 20 DRAW 1, 10, 20 ``` @@ -37,7 +42,7 @@ refuses to draw. ### DRAW -``` +```basic requires=akgl 10 DRAW 1, 10, 20 20 DRAW 1, 0, 0 TO 100, 100 TO 200, 0 ``` @@ -47,7 +52,7 @@ bare `DRAW 1` plots wherever `LOCATE` left the pixel cursor. ### BOX -``` +```basic requires=akgl 10 BOX 1, 10, 10, 40, 40 ``` @@ -55,7 +60,7 @@ Corners, and an optional rotation angle. An unrotated `BOX` outlines rather than ### CIRCLE -``` +```basic requires=akgl 10 CIRCLE 1, 160, 100, 50, 30 ``` @@ -65,7 +70,7 @@ an arc or a polygon. ### PAINT -``` +```basic requires=akgl 10 PAINT 1, 160, 100 ``` @@ -80,7 +85,7 @@ coordinates finishes. ### SCALE -``` +```basic requires=akgl 10 SCALE 1, 1023, 1023 ``` @@ -96,7 +101,7 @@ parallel passes; see Chapter 13. `SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back: -``` +```basic requires=akgl 10 BOX 1, 0, 0, 20, 20 20 SSHAPE A$, 0, 0, 20, 20 30 GSHAPE A$, 100, 100 diff --git a/docs/07-sound.md b/docs/07-sound.md index 759bbaf..6bd944c 100644 --- a/docs/07-sound.md +++ b/docs/07-sound.md @@ -6,7 +6,7 @@ name, and a machine with no sound card still runs the interpreter — only `SOUN ## SOUND -``` +```basic requires=akgl 10 SOUND 1, 4000, 60 ``` @@ -24,7 +24,7 @@ frame loop for a second would freeze the game. `PLAY` takes a string of notes: -``` +```basic requires=akgl 10 PLAY "C D E F G" ``` @@ -50,7 +50,7 @@ immediately `QUIT`s may not hear all of it. ## TEMPO -``` +```basic requires=akgl 10 TEMPO 8 ``` @@ -60,7 +60,7 @@ transcription, so it may not match a real machine exactly. ## ENVELOPE and VOL -``` +```basic requires=akgl 10 ENVELOPE 0, 5, 9, 4, 6 20 VOL 8 ``` @@ -72,8 +72,13 @@ and release. `VOL` sets the overall volume, 0 to 15. `FILTER` parses and then **refuses at execution**: +```basic +10 FILTER 1000, 1, 0, 0, 8 ``` -? 10 : RUNTIME ERROR FILTER needs an audio device that can filter, and this one cannot + +```output +? 10 : RUNTIME ERROR FILTER needs a filter stage this device does not have + ``` It sets the SID's filter cutoff, band switches and resonance. The audio backend diff --git a/docs/08-sprites.md b/docs/08-sprites.md index 7a83799..b5672ad 100644 --- a/docs/08-sprites.md +++ b/docs/08-sprites.md @@ -12,7 +12,7 @@ see and manipulate a script's sprites. ### From an image file -``` +```basic requires=akgl setup=ship 10 SPRSAV "ship.png", 1 ``` @@ -25,7 +25,7 @@ A sprite loaded this way takes **the image's own size**. It is not forced to 24 ### From a saved region -``` +```basic requires=akgl 10 BOX 1, 0, 0, 24, 21 20 SSHAPE A$, 0, 0, 24, 21 30 SPRSAV A$, 1 @@ -37,7 +37,7 @@ faithful rather than a shortcut. ### From data -``` +```basic norun 10 DIM P#(63) 20 FOR I# = 0 TO 62 30 READ P#(I#) @@ -58,9 +58,10 @@ operation. ## Showing and moving -``` -10 SPRITE 1, 1, 3 -20 MOVSPR 1, 100, 50 +```basic requires=akgl setup=ship +10 SPRSAV "ship.png", 1 +20 SPRITE 1, 1, 3 +30 MOVSPR 1, 100, 50 ``` `SPRITE n [,on] [,colour] [,priority] [,xexpand] [,yexpand] [,multicolour]`. Only the @@ -88,7 +89,7 @@ raster coordinates. ## Collision -``` +```basic norun 10 COLLISION 1, BUMPED 20 REM ... main loop ... 90 GOTO 20 diff --git a/docs/09-files-and-disk.md b/docs/09-files-and-disk.md index 91dc59b..051bce2 100644 --- a/docs/09-files-and-disk.md +++ b/docs/09-files-and-disk.md @@ -5,12 +5,17 @@ something, and refuse by name where it does not. ## Program storage -``` +```basic repl +10 PRINT "HI" DSAVE "myprogram.bas" DLOAD "myprogram.bas" VERIFY "myprogram.bas" ``` +```output +OK +``` + `SAVE` and `LOAD` are the same verbs under their other names, and `DVERIFY` is `VERIFY`. A program is saved as plain text with its line numbers, so you can edit it in anything. @@ -22,7 +27,7 @@ many lines differ. Ten channels, numbered 0 to 9. -``` +```basic 10 DOPEN 1, "scores.txt", W 20 PRINT #1, "ADA 4000" 30 PRINT #1, "GRACE 3800" @@ -33,6 +38,10 @@ Ten channels, numbered 0 to 9. 80 DCLOSE 2 ``` +```output +ADA 4000 +``` + | Verb | What it does | |---|---| | `DOPEN n, "name"` | open for reading | @@ -50,7 +59,7 @@ its own word, because `PRINT#` would otherwise scan as a variable name. See Chap Reading past the end of a file leaves the variable empty rather than raising, so a read loop tests what it got: -``` +```basic setup=textfiles 10 DOPEN 1, "data.txt" 20 DO 30 INPUT #1, L$ @@ -60,6 +69,11 @@ loop tests what it got: 70 DCLOSE 1 ``` +```output +ONE +TWO +``` + Reading a channel opened for writing — or writing one opened for reading — is refused, as is using a channel that is not open. @@ -68,7 +82,7 @@ length to seek by, so it rewinds and reads forward. ## Managing files -``` +```basic setup=textfiles 10 COPY "a.txt", "b.txt" 20 CONCAT "a.txt", "b.txt" 30 RENAME "b.txt", "c.txt" @@ -80,15 +94,23 @@ deletes. Deleting a file that is not there is reported rather than ignored. ## Binary blocks -``` +```basic 10 A$ = "BINARY DATA" -20 BSAVE "block.dat", POINTER(A$), POINTER(A$) + 12 -30 BLOAD "block.dat", POINTER(B$), 12 +20 B$ = "............" +30 BSAVE "block.dat", POINTER(A$), POINTER(A$) + 12 +40 BLOAD "block.dat", POINTER(B$), 12 +50 PRINT B$ +``` + +```output +BINARY DATA ``` `BSAVE` writes a range of memory and `BLOAD` reads one back. **`BLOAD` requires a length**, unlike a C128's, because the address is a real address in this process and a -file longer than you expected would write past whatever you pointed at. +file longer than you expected would write past whatever you pointed at. For the same +reason line 20 is not optional: `POINTER` gives you the address of a string that already +exists, and reading into one you never sized writes over something else. ## What is refused, and why @@ -105,6 +127,11 @@ channels, and closing the channels is real, so that is what it does. Each refusal names itself and says why: +```basic +10 HEADER "DISK" ``` + +```output ? 10 : RUNTIME ERROR HEADER formats a disk, and there is no disk drive here -- only a filesystem + ``` diff --git a/docs/10-embedding.md b/docs/10-embedding.md index 919e051..f4d4436 100644 --- a/docs/10-embedding.md +++ b/docs/10-embedding.md @@ -27,7 +27,7 @@ target_link_libraries(YOUR_GAME PRIVATE akbasic::akbasic) ## The shortest useful host -```c +```c wrap=hostloop #include #include @@ -65,7 +65,7 @@ immediately — audible, but never a hang. Use `akbasic_runtime_global()`. It finds or creates the variable in the script's outermost scope, which is the only place both of you can reliably see: -```c +```c wrap=hostbody akbasic_Variable *health = NULL; int64_t subscript[1] = { 0 }; @@ -80,7 +80,7 @@ it stops. ## Lending devices -```c +```c wrap=hostbody PASS(e, akbasic_runtime_set_devices(&RUNTIME, &graphics, &audio, &input, &sprites)); ``` @@ -91,7 +91,7 @@ all. `akbasic_akgl` provides implementations that draw through a renderer *you* created: -```c +```c wrap=akglbody requires=akgl PASS(e, akbasic_graphics_init_akgl(&graphics, &gstate, my_renderer)); PASS(e, akbasic_sprite_init_akgl(&sprites, &sstate, my_renderer, &gstate)); ``` diff --git a/docs/13-differences.md b/docs/13-differences.md index cb9e4ba..7f517c0 100644 --- a/docs/13-differences.md +++ b/docs/13-differences.md @@ -46,11 +46,16 @@ leading zero is not octal; `0x` is hexadecimal. **A whole loop on one line does not loop.** -``` +```basic 10 FOR I# = 1 TO 3 : PRINT I# : NEXT I# +20 PRINT "DONE" ``` -prints nothing. Block skipping walks source *lines*, so a `NEXT` on the same line as +```output +DONE +``` + +The loop prints nothing. Block skipping walks source *lines*, so a `NEXT` on the same line as its `FOR` is never reached. The same applies to `DO`/`LOOP`. Write loops across lines. ## Two known defects in `FOR` diff --git a/docs/README.md b/docs/README.md index f722919..245950c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -31,16 +31,30 @@ everything that behaves differently here and why. ## The shortest possible start -``` +```sh norun $ cmake -S . -B build && cmake --build build $ ./build/basic -READY +``` + +```basic repl 10 FOR I# = 1 TO 5 20 PRINT "HELLO " + I# 30 NEXT I# RUN ``` +```output +HELLO 1 +HELLO 2 +HELLO 3 +HELLO 4 +HELLO 5 +READY +``` + 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 explains both. + +Every example in these chapters is executed by the test suite and its output +compared byte for byte — see `MAINTENANCE.md` if you are editing them. diff --git a/tests/docs_examples.sh b/tests/docs_examples.sh new file mode 100755 index 0000000..3c9818f --- /dev/null +++ b/tests/docs_examples.sh @@ -0,0 +1,512 @@ +#!/bin/bash +# +# Run every example in the documentation and check what it produces. +# +# The guide in docs/ and the walkthrough in README.md are full of programs, +# transcripts, C snippets and shell commands. Every one of them was checked by +# hand once, when it was written, and that is not a standard that survives +# contact with a changing interpreter. Three of them were already wrong when this +# harness was written -- two transcripts carrying a leading space PRINT does not +# emit, and a struct in README.md that had grown two members hours earlier. +# +# Documentation goes stale because the *code* moved, not because somebody edited +# a chapter, so this runs on every ctest rather than on a docs path filter. +# +# **Exit status is the number of failed examples**, the house convention. +# +# The contract with the documentation is a set of fence info strings. They are +# ordinary markdown, they render as syntax highlighting on the forge, and they +# sit next to the thing they describe. MAINTENANCE.md is the reference; the short +# version: +# +# ```basic a whole program: run it, and it must not error +# ```basic repl input lines fed to a fresh interpreter on stdin +# ```basic norun a fragment, shown but not run +# ```basic requires=akgl only run in the -DAKBASIC_WITH_AKGL=ON build +# ```basic requires=noakgl only run in the build with no devices attached +# ```output the exact stdout of the block above, byte for byte +# ```c a translation unit: compile it with -fsyntax-only +# ```c wrap=NAME the same, wrapped in tests/docs_preludes/NAME.pre/.post +# ```c excerpt=PATH must appear verbatim in PATH; not compiled +# ```c norun shown but not compiled +# ```sh run in a sandbox; must exit 0 +# ```sh setup=NAME the same, after tests/docs_setups/NAME.sh +# ```sh norun destructive, networked, or re-enters this suite +# ```cmake never executed; hand-maintained, by decision +# +# A block with **no** info string is a hard error. That is deliberate: the +# failure mode this whole harness exists to avoid is passing because it quietly +# ran nothing, so an unannotated block is a missing decision rather than a +# default. + +set -u + +ROOT="" +BASIC="" +CFLAGS_FILE="" +WITH_AKGL=0 + +usage() +{ + cat >&2 <<'EOF' +usage: docs_examples.sh --root DIR --basic PATH [--cflags-file FILE] [--akgl] [FILE...] + + --root DIR repository root; documentation paths are relative to it + --basic PATH the built interpreter + --cflags-file FILE one compiler flag per line, for the ```c blocks + --akgl this is an AKBASIC_WITH_AKGL build; run requires=akgl blocks + FILE... which documents to check (default: README.md docs/*.md) +EOF + exit 2 +} + +while [ $# -gt 0 ]; do + case "$1" in + --root) ROOT="$2"; shift 2 ;; + --basic) BASIC="$2"; shift 2 ;; + --cflags-file) CFLAGS_FILE="$2"; shift 2 ;; + --akgl) WITH_AKGL=1; shift ;; + --help|-h) usage ;; + --*) echo "unknown option $1" >&2; usage ;; + *) break ;; + esac +done + +[ -n "${ROOT}" ] || usage +[ -n "${BASIC}" ] || usage +[ -x "${BASIC}" ] || { echo "FAIL: no interpreter at ${BASIC}" >&2; exit 2; } + +cd "${ROOT}" || exit 2 + +DOCS=("$@") +if [ ${#DOCS[@]} -eq 0 ]; then + # MAINTENANCE.md is here for the tagging rule rather than for its examples: + # every one of its blocks is `norun`, and the point is that the file which + # documents the convention is held to it. + DOCS=(README.md MAINTENANCE.md) + for _doc in docs/*.md; do + DOCS+=("${_doc}") + done +fi + +# Refuse a document that is not there rather than checking nothing and passing. +# An empty argument is the specific case that got here: a CMake generator +# expression evaluating to nothing still contributes an empty argument, which +# looked like a filename and silently replaced the whole default list. +for _doc in "${DOCS[@]}"; do + if [ ! -r "${_doc}" ]; then + echo "FAIL: no document at \"${_doc}\"" >&2 + exit 2 + fi +done + +WORK="$(mktemp -d)" +trap 'rm -rf "${WORK}"' EXIT + +# The compiler flags the ```c blocks need. CMake writes them, because the +# include path is transitive through akerror, akstdlib and (in the AKGL build) +# akgl, and hardcoding it here would go stale exactly the way the docs do. +CFLAGS=() +if [ -n "${CFLAGS_FILE}" ] && [ -r "${CFLAGS_FILE}" ]; then + while IFS= read -r _flag; do + [ -n "${_flag}" ] && CFLAGS+=("${_flag}") + done < "${CFLAGS_FILE}" +fi +CC="${CC:-cc}" + +FAILURES=0 +declare -A RAN=([basic]=0 [repl]=0 [output]=0 [c]=0 [excerpt]=0 [sh]=0) +declare -A SKIPPED=([norun]=0 [akgl]=0 [cmake]=0 [nocc]=0) + +# Every failure names the file and line of the block, so the message points at +# the thing to edit rather than at this script. +fail() +{ + local where="$1"; shift + echo "FAIL ${where}: $*" >&2 + FAILURES=$((FAILURES + 1)) +} + +# Show a byte-exact comparison. `cat -A` because the errors this catches are +# leading spaces and missing newlines, which a plain diff renders invisibly. +show_diff() +{ + local want="$1" got="$2" + echo "--- expected ---" >&2 + cat -A "${want}" >&2 + echo "--- actual ---" >&2 + cat -A "${got}" >&2 +} + +# ---------------------------------------------------------------- extraction +# +# One pass per document, writing each block's body to ${WORK}/blocks/NNNN and a +# line to ${WORK}/index. Keeping the bodies in files rather than shell variables +# means a block containing a NUL, a backslash or an unbalanced quote is carried +# through untouched -- and BASIC string literals contain plenty of all three. +extract() +{ + local doc="$1" out="$2" + + awk -v OUT="${out}" -v INDEX="${out}/index" ' + function pad(n) { return sprintf("%04d", n) } + /^```/ { + if ( !inblock ) { + inblock = 1 + tag = substr($0, 4) + sub(/[ \t]+$/, "", tag) + start = NR + count++ + body = OUT "/" pad(count) + printf "" > body + next + } + if ( $0 == "```" ) { + close(body) + printf "%s\t%d\t%s\n", pad(count), start, tag >> INDEX + inblock = 0 + next + } + } + inblock { print >> body } + ' "${doc}" +} + +# ------------------------------------------------------------------- helpers + +# The value of attribute $1 in an info string $2, or empty. `wrap=embed` and +# `requires=akgl` are both read this way. +attr() +{ + local name="$1" info="$2" word + for word in ${info}; do + case "${word}" in + "${name}"=*) echo "${word#*=}"; return ;; + esac + done +} + +# True when a bare word appears in an info string. Used for `norun` and `repl`. +has_word() +{ + local want="$1" info="$2" word + for word in ${info}; do + [ "${word}" = "${want}" ] && return 0 + done + return 1 +} + +# Strip C comments and collapse whitespace, so an excerpt can be compared to the +# header it came from without either side having to carry the other's doc +# comments or indentation. +normalise_c() +{ + awk ' + { + line = $0 + out = "" + while ( length(line) > 0 ) { + if ( incomment ) { + i = index(line, "*/") + if ( i == 0 ) { line = ""; break } + incomment = 0 + line = substr(line, i + 2) + continue + } + i = index(line, "/*") + j = index(line, "//") + if ( j > 0 && (i == 0 || j < i) ) { + out = out substr(line, 1, j - 1) + line = "" + break + } + if ( i == 0 ) { out = out line; line = ""; break } + out = out substr(line, 1, i - 1) + incomment = 1 + line = substr(line, i + 2) + } + printf "%s ", out + } + END { printf "\n" } + ' "$1" | tr -s ' \t' ' ' | sed -e 's/^ //' -e 's/ $//' +} + +# --------------------------------------------------------------- block kinds + +# A BASIC program in a file, or a transcript on stdin. Both go through the same +# comparison, because the only difference is how the interpreter is invoked. +run_basic() +{ + local where="$1" body="$2" info="$3" expected="$4" + local sandbox="${WORK}/run" got="${WORK}/got" status=0 setup + + rm -rf "${sandbox}" + mkdir -p "${sandbox}" + + # An example that reads a file needs that file. Putting the fixture in a + # setup script rather than in the chapter keeps the example the shape a + # reader wants to see -- `SPRSAV "ship.png", 1` and nothing else. + setup="$(attr setup "${info}")" + if [ -n "${setup}" ]; then + if [ ! -r "tests/docs_setups/${setup}.sh" ]; then + fail "${where}" "setup=${setup} names no tests/docs_setups/${setup}.sh" + return + fi + if ! ( cd "${sandbox}" && bash "${ROOT}/tests/docs_setups/${setup}.sh" ) >/dev/null 2>&1; then + fail "${where}" "setup=${setup} failed" + return + fi + fi + + if has_word repl "${info}"; then + ( cd "${sandbox}" && "${BASIC}" < "${body}" ) > "${got}.raw" 2> "${WORK}/stderr" + status=$? + # Drop the startup banner. A piped interpreter prints READY once, before + # it has read anything, so carrying it in every transcript would be one + # line of noise per example and would put it in the wrong place besides: + # at a real prompt READY comes back *after* each line. Chapter 2 shows + # that interactively, which is where it belongs. + sed -e '1{/^READY$/d}' "${got}.raw" > "${got}" + RAN[repl]=$((RAN[repl] + 1)) + else + cp "${body}" "${sandbox}/example.bas" + ( cd "${sandbox}" && "${BASIC}" example.bas ) > "${got}" 2> "${WORK}/stderr" + status=$? + RAN[basic]=$((RAN[basic] + 1)) + fi + + if [ "${status}" -ne 0 ]; then + fail "${where}" "the interpreter exited ${status}" + sed -n '1,20p' "${got}" "${WORK}/stderr" >&2 + return + fi + + # stdout only, the way tests/golden.cmake compares its cases. libakgl logs + # registry activity to stderr -- three lines every time a sprite is created + # -- and folding that into the comparison would put library chatter into the + # documentation of a BASIC verb. + if [ -n "${expected}" ]; then + RAN[output]=$((RAN[output] + 1)) + if ! cmp -s "${expected}" "${got}"; then + fail "${where}" "output does not match the block below it" + show_diff "${expected}" "${got}" + fi + return + fi + + # No expectation given, so the only thing to assert is that it ran cleanly. + # A BASIC-level error prints "? LINE : CLASS" and still exits 0 -- that is + # correct for the interpreter and useless as a test result, so look for it. + if grep -qE '^\? [0-9]+ :' "${got}"; then + fail "${where}" "the program raised a BASIC error" + grep -E '^\? [0-9]+ :' "${got}" >&2 + fi +} + +# A C snippet. Compiled, never linked and never run: what these examples are for +# is showing the API, and the API is exactly what -fsyntax-only checks. +run_c() +{ + local where="$1" body="$2" info="$3" doc="$4" line="$5" + local wrap unit="${WORK}/unit.c" log="${WORK}/cc.log" + + if [ ${#CFLAGS[@]} -eq 0 ]; then + SKIPPED[nocc]=$((SKIPPED[nocc] + 1)) + return + fi + + wrap="$(attr wrap "${info}")" + : > "${unit}" + if [ -n "${wrap}" ]; then + if [ ! -r "tests/docs_preludes/${wrap}.pre" ]; then + fail "${where}" "wrap=${wrap} names no tests/docs_preludes/${wrap}.pre" + return + fi + cat "tests/docs_preludes/${wrap}.pre" >> "${unit}" + fi + # Hand the compiler the document's own coordinates, so a diagnostic points + # at the line of markdown to fix rather than at a scratch file. + printf '#line %d "%s"\n' "$((line + 1))" "${doc}" >> "${unit}" + cat "${body}" >> "${unit}" + if [ -n "${wrap}" ] && [ -r "tests/docs_preludes/${wrap}.post" ]; then + printf '#line 1 "tests/docs_preludes/%s.post"\n' "${wrap}" >> "${unit}" + cat "tests/docs_preludes/${wrap}.post" >> "${unit}" + fi + + # gnu99 rather than c99: akerror.h uses PATH_MAX, which hides + # under __STRICT_ANSI__, and the library itself is built with the compiler's + # default dialect. A stricter flag here would fail on the dependency rather + # than on the example. + if "${CC}" -fsyntax-only -std=gnu99 -Wall -Wextra -Werror \ + "${CFLAGS[@]}" "${unit}" > "${log}" 2>&1; then + RAN[c]=$((RAN[c] + 1)) + else + fail "${where}" "does not compile" + sed -n '1,25p' "${log}" >&2 + fi +} + +# A declaration echoed from a header. Compiling it would redefine the type, so +# the check is that it still says what the header says -- which is the drift +# that actually happened, and the one a compile could not have caught. +run_excerpt() +{ + local where="$1" body="$2" path="$3" + + if [ ! -r "${path}" ]; then + fail "${where}" "excerpt=${path} names no such file" + return + fi + RAN[excerpt]=$((RAN[excerpt] + 1)) + if ! normalise_c "${path}" | grep -qF -- "$(normalise_c "${body}")"; then + fail "${where}" "no longer appears in ${path}" + echo "--- the documentation says ---" >&2 + cat "${body}" >&2 + echo "--- ${path} has drifted from it; comments and whitespace are ignored ---" >&2 + fi +} + +# A shell command, in a sandbox with the built interpreter reachable at the path +# the documentation tells a reader to use. +run_sh() +{ + local where="$1" body="$2" info="$3" expected="$4" + local setup sandbox="${WORK}/sh" script="${WORK}/block.sh" got="${WORK}/got" + + rm -rf "${sandbox}" + mkdir -p "${sandbox}/build" + ln -s "${BASIC}" "${sandbox}/build/basic" + + setup="$(attr setup "${info}")" + if [ -n "${setup}" ]; then + if [ ! -r "tests/docs_setups/${setup}.sh" ]; then + fail "${where}" "setup=${setup} names no tests/docs_setups/${setup}.sh" + return + fi + if ! ( cd "${sandbox}" && bash "${ROOT}/tests/docs_setups/${setup}.sh" ) >/dev/null 2>&1; then + fail "${where}" "setup=${setup} failed" + return + fi + fi + + # A leading "$ " is a prompt in a transcript, not part of the command. + sed -e 's/^\$ //' "${body}" > "${script}" + if ! ( cd "${sandbox}" && bash -e "${script}" ) > "${got}" 2>&1; then + fail "${where}" "the command failed" + sed -n '1,20p' "${got}" >&2 + return + fi + RAN[sh]=$((RAN[sh] + 1)) + + if [ -n "${expected}" ]; then + RAN[output]=$((RAN[output] + 1)) + if ! cmp -s "${expected}" "${got}"; then + fail "${where}" "output does not match the block below it" + show_diff "${expected}" "${got}" + fi + fi +} + +# ------------------------------------------------------------------ the pass +for DOC in "${DOCS[@]}"; do + BLOCKS="${WORK}/blocks" + rm -rf "${BLOCKS}" + mkdir -p "${BLOCKS}" + : > "${BLOCKS}/index" + extract "${DOC}" "${BLOCKS}" + + # Read the index into arrays first. An `output` block belongs to the block + # before it, so each iteration has to be able to look one ahead. + CLAIMED=0 + IDS=(); LINES=(); INFOS=() + while IFS=$'\t' read -r _id _line _info; do + IDS+=("${_id}"); LINES+=("${_line}"); INFOS+=("${_info}") + done < "${BLOCKS}/index" + + for ((n = 0; n < ${#IDS[@]}; n++)); do + INFO="${INFOS[n]}" + BODY="${BLOCKS}/${IDS[n]}" + WHERE="${DOC}:${LINES[n]}" + KIND="${INFO%% *}" + + # An `output` block belongs to the block above it, which claimed it on the + # way past. One that nothing claimed is compared against nothing at all -- + # indistinguishable from a passing test, and the exact failure this + # harness exists to rule out. Note a *skipped* block still claims its + # output: `norun` and `requires=` are decisions, not accidents. + if [ "${KIND}" = "output" ]; then + if [ "${CLAIMED}" -eq 0 ]; then + fail "${WHERE}" "an output block with nothing runnable above it" + fi + CLAIMED=0 + continue + fi + CLAIMED=0 + + EXPECTED="" + case "${KIND}" in + basic|sh) + if [ $((n + 1)) -lt ${#IDS[@]} ] && [ "${INFOS[n + 1]%% *}" = "output" ]; then + EXPECTED="${BLOCKS}/${IDS[n + 1]}" + CLAIMED=1 + fi + ;; + esac + + if has_word norun "${INFO}"; then + SKIPPED[norun]=$((SKIPPED[norun] + 1)) + continue + fi + + # A handful of examples are about what a *build* does rather than what the + # language does -- a verb refusing because no device was attached is the + # clearest one -- so they run in one configuration and are skipped in the + # other. Both directions are needed, and both are visible in the count. + REQUIRES="$(attr requires "${INFO}")" + if [ "${REQUIRES}" = "akgl" ] && [ "${WITH_AKGL}" -eq 0 ]; then + SKIPPED[akgl]=$((SKIPPED[akgl] + 1)) + continue + fi + if [ "${REQUIRES}" = "noakgl" ] && [ "${WITH_AKGL}" -eq 1 ]; then + SKIPPED[akgl]=$((SKIPPED[akgl] + 1)) + continue + fi + + case "${KIND}" in + basic) + run_basic "${WHERE}" "${BODY}" "${INFO}" "${EXPECTED}" + ;; + c) + EXCERPT="$(attr excerpt "${INFO}")" + if [ -n "${EXCERPT}" ]; then + run_excerpt "${WHERE}" "${BODY}" "${EXCERPT}" + else + run_c "${WHERE}" "${BODY}" "${INFO}" "${DOC}" "${LINES[n]}" + fi + ;; + sh) + run_sh "${WHERE}" "${BODY}" "${INFO}" "${EXPECTED}" + ;; + cmake) + SKIPPED[cmake]=$((SKIPPED[cmake] + 1)) + ;; + "") + fail "${WHERE}" "fenced block with no info string; tag it (see MAINTENANCE.md)" + ;; + *) + fail "${WHERE}" "unknown info string \"${INFO}\" (see MAINTENANCE.md)" + ;; + esac + done +done + +# The count is part of the result, not decoration. A harness that passes because +# a chapter stopped matching the extractor looks exactly like a harness that +# passes because the documentation is correct, and this is what tells them apart. +echo "ran: ${RAN[basic]} programs, ${RAN[repl]} transcripts, ${RAN[output]} output comparisons, ${RAN[c]} C snippets, ${RAN[excerpt]} excerpts, ${RAN[sh]} shell blocks" +echo "skipped: ${SKIPPED[norun]} norun, ${SKIPPED[akgl]} needing akgl, ${SKIPPED[cmake]} cmake, ${SKIPPED[nocc]} C (no compiler flags given)" + +if [ "${FAILURES}" -eq 0 ]; then + echo "PASS: every documented example does what the documentation says" +fi +exit "${FAILURES}" diff --git a/tests/docs_preludes/akglbody.post b/tests/docs_preludes/akglbody.post new file mode 100644 index 0000000..4e6965d --- /dev/null +++ b/tests/docs_preludes/akglbody.post @@ -0,0 +1,2 @@ + SUCCEED_RETURN(e); +} diff --git a/tests/docs_preludes/akglbody.pre b/tests/docs_preludes/akglbody.pre new file mode 100644 index 0000000..aab9728 --- /dev/null +++ b/tests/docs_preludes/akglbody.pre @@ -0,0 +1,21 @@ +/* + * Prelude for the akgl-backend fragment in docs/10-embedding.md. Same shape as + * hostbody.pre, plus the renderer and the two backend states those calls take. + * Only compiled in the AKBASIC_WITH_AKGL build, which is the only one where + * exists to include. + */ +#include +#include +#include + +static akbasic_Runtime RUNTIME; +static akbasic_GraphicsBackend graphics; +static akbasic_SpriteBackend sprites; +static akbasic_AkglGraphics gstate; +static akbasic_AkglSprites sstate; +static akgl_RenderBackend *my_renderer; + +akerr_ErrorContext AKERR_NOIGNORE *akbasic_docs_fragment(void); +akerr_ErrorContext AKERR_NOIGNORE *akbasic_docs_fragment(void) +{ + PREPARE_ERROR(e); diff --git a/tests/docs_preludes/hostbody.post b/tests/docs_preludes/hostbody.post new file mode 100644 index 0000000..4e6965d --- /dev/null +++ b/tests/docs_preludes/hostbody.post @@ -0,0 +1,2 @@ + SUCCEED_RETURN(e); +} diff --git a/tests/docs_preludes/hostbody.pre b/tests/docs_preludes/hostbody.pre new file mode 100644 index 0000000..3682a6a --- /dev/null +++ b/tests/docs_preludes/hostbody.pre @@ -0,0 +1,22 @@ +/* + * Prelude for the statement fragments in docs/10-embedding.md: the variable + * exchange, the device lending, and the akgl backends. + * + * Each is shown as the two or three lines that matter, so they need a function + * to sit in and the surrounding declarations to refer to. `e` is the error + * context the chapter's examples name. + */ +#include +#include +#include + +static akbasic_Runtime RUNTIME; +static akbasic_GraphicsBackend graphics; +static akbasic_AudioBackend audio; +static akbasic_InputBackend input; +static akbasic_SpriteBackend sprites; + +akerr_ErrorContext AKERR_NOIGNORE *akbasic_docs_fragment(void); +akerr_ErrorContext AKERR_NOIGNORE *akbasic_docs_fragment(void) +{ + PREPARE_ERROR(e); diff --git a/tests/docs_preludes/hostcalls.post b/tests/docs_preludes/hostcalls.post new file mode 100644 index 0000000..14fb300 --- /dev/null +++ b/tests/docs_preludes/hostcalls.post @@ -0,0 +1,6 @@ + } CLEANUP { + } PROCESS(errctx) { + } FINISH(errctx, true); + (void)score; + SUCCEED_RETURN(errctx); +} diff --git a/tests/docs_preludes/hostcalls.pre b/tests/docs_preludes/hostcalls.pre new file mode 100644 index 0000000..a79548d --- /dev/null +++ b/tests/docs_preludes/hostcalls.pre @@ -0,0 +1,27 @@ +/* + * Prelude for the "used like this" fragment in README.md. + * + * The fragment is a run of CATCH calls, which only compile inside an ATTEMPT -- + * that is the whole point of the macro protocol and the reason the fragment is + * shown without its scaffolding. `host_set_string` is the third accessor the + * surrounding prose says exists but does not print. + */ +#include +#include +#include +#include + +static akbasic_Runtime SCRIPT; +static const char *PROGRAM = "10 PRINT \"HI\"\n"; + +akerr_ErrorContext AKERR_NOIGNORE *host_set_int(akbasic_Runtime *obj, const char *name, int64_t value); +akerr_ErrorContext AKERR_NOIGNORE *host_get_int(akbasic_Runtime *obj, const char *name, int64_t *dest); +akerr_ErrorContext AKERR_NOIGNORE *host_set_string(akbasic_Runtime *obj, const char *name, const char *value); + +akerr_ErrorContext AKERR_NOIGNORE *akbasic_docs_fragment(void); +akerr_ErrorContext AKERR_NOIGNORE *akbasic_docs_fragment(void) +{ + PREPARE_ERROR(errctx); + int64_t score = 0; + + ATTEMPT { diff --git a/tests/docs_preludes/hostloop.post b/tests/docs_preludes/hostloop.post new file mode 100644 index 0000000..12e9d3a --- /dev/null +++ b/tests/docs_preludes/hostloop.post @@ -0,0 +1,2 @@ +static int64_t your_clock_ms(void) { return 0; } +static void your_draw_a_frame(void) { } diff --git a/tests/docs_preludes/hostloop.pre b/tests/docs_preludes/hostloop.pre new file mode 100644 index 0000000..72d10ef --- /dev/null +++ b/tests/docs_preludes/hostloop.pre @@ -0,0 +1,13 @@ +/* + * Prelude for the "shortest useful host" example in docs/10-embedding.md. + * + * The example calls two functions it does not define, on purpose: a host's clock + * and a host's renderer are the host's business, and spelling them out would + * bury the four calls the section is actually about. Declare them here so the + * block compiles as written. + */ +#include +#include + +static int64_t your_clock_ms(void); +static void your_draw_a_frame(void); diff --git a/tests/docs_preludes/hostvars.pre b/tests/docs_preludes/hostvars.pre new file mode 100644 index 0000000..cdefd2e --- /dev/null +++ b/tests/docs_preludes/hostvars.pre @@ -0,0 +1,10 @@ +/* + * Prelude for the two host<->script accessors in README.md. + * + * The block is shown as two functions and nothing else, because the includes + * are already listed two sections above it. + */ +#include +#include +#include +#include diff --git a/tests/docs_setups/program.sh b/tests/docs_setups/program.sh new file mode 100644 index 0000000..ffefc89 --- /dev/null +++ b/tests/docs_setups/program.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# +# Chapter 2 shows `./build/basic program.bas` without showing the file, because +# a reader following along already has one. Give the sandbox the shortest thing +# that proves the invocation works. +cat > program.bas <<'BAS' +10 PRINT "HELLO FROM A FILE" +BAS diff --git a/tests/docs_setups/ship.sh b/tests/docs_setups/ship.sh new file mode 100644 index 0000000..6bd1010 --- /dev/null +++ b/tests/docs_setups/ship.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# +# Chapter 8 loads a sprite from `ship.png`. The picture is not the point of the +# example, so any decodable image will do -- reuse the one tests/sprite_verbs.c +# already keeps. +cp "$(dirname "${BASH_SOURCE[0]}")/../assets/sprite8x8.png" ship.png diff --git a/tests/docs_setups/textfiles.sh b/tests/docs_setups/textfiles.sh new file mode 100644 index 0000000..3844d60 --- /dev/null +++ b/tests/docs_setups/textfiles.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# +# Chapter 9's read loop and its COPY/CONCAT/RENAME/SCRATCH example both work on +# files a reader is assumed to already have. Two lines of text is all either +# needs. +printf 'ONE\nTWO\n' > data.txt +printf 'AAA\n' > a.txt