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

@@ -372,6 +372,59 @@ if(AKBASIC_WITH_AKGL)
SKIP_RETURN_CODE 77) SKIP_RETURN_CODE 77)
endif() 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$<JOIN:$<TARGET_PROPERTY:${AKBASIC_DOCS_CFLAGS_TARGET},INCLUDE_DIRECTORIES>,\n-I>\n"
)
# Spelled out with if() rather than $<$<BOOL:...>:--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 $<TARGET_FILE: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) if(AKBASIC_TESTS OR AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
_set_tests_properties( _set_tests_properties(
${AKBASIC_TESTS} ${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS} ${AKBASIC_TESTS} ${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS}

225
MAINTENANCE.md Normal file
View File

@@ -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_<name>` 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 **512767** 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.

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. 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 git submodule update --init --recursive
cmake -S . -B build cmake -S . -B build
cmake --build build --parallel 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` 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: 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 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 --list
python3 scripts/mutation_test.py --target src/value.c --threshold 70 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 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. 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 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 `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 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 * `EXIT`: Exit a loop before it would normally finish
* `FOR` : Iterate over a range of values and perform (statement) or block each time. * `FOR` : Iterate over a range of values and perform (statement) or block each time.
``` ```basic
10 FOR I# = 1 TO 5 10 FOR I# = 1 TO 5
20 REM Do some stuff in here 20 PRINT I#
30 NEXT 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 ```output
20 REM Do some stuff here 1
30 NEXT I# 2
3
4
5
1
3
5
``` ```
* `CLR`: Drop every variable and function definition, keeping the program * `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 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#) 10 DEF ADDTWO(A#, B#)
20 C# = A# + B# 20 C# = A# + B#
30 RETURN C# 30 RETURN C#
@@ -269,6 +287,10 @@ In addition to `DEF`, `GOTO` and `GOSUB`, this BASIC also implements subroutines
50 PRINT D# 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. Subroutines must be defined before they are called. Subroutines share the global variable scope with the rest of the program.
# Embedding the interpreter # 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: 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 <akerror.h>
#include <akbasic/runtime.h> #include <akbasic/runtime.h>
#include <akbasic/sink.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 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. list is `{0}` with a count of 1.
```c ```c wrap=hostvars
/* host -> script, before the script starts */ /* host -> script, before the script starts */
akerr_ErrorContext AKERR_NOIGNORE *host_set_int(akbasic_Runtime *obj, const char *name, int64_t value) 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: 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, akbasic_runtime_load(&SCRIPT, PROGRAM));
CATCH(errctx, host_set_int(&SCRIPT, "HP#", 100)); /* seed */ 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`: `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 typedef struct akbasic_TextSink
{ {
void *self; 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 *(*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 *(*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 *(*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; } akbasic_TextSink;
``` ```

36
TODO.md
View File

@@ -1527,8 +1527,9 @@ requirement; exactly one case has diverged on purpose since, and
| Gate | Result | | 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` | 95/95 — 41 reference golden cases, 15 local ones, 36 unit tests, 2 embedding examples, and `docs_examples` |
| `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` 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 | | 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 | | ASan + UBSan | 77/77 |
| Line coverage | 94.6% (3857/4076) — above the 90% gate | | 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 | | `doxygen Doxyfile` | clean |
| Mutation (`src/symtab.c`) | 74.1%, against a gate of 65 — see `.gitea/workflows/ci.yaml` | | 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` 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 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 `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 `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 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`. 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.

View File

@@ -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 text. Everything else works, and the whole test suite runs on a machine with no SDL
installed at all. installed at all.
```sh ```sh norun
cmake -S . -B build cmake -S . -B build
cmake --build 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 devices. It needs [libakgl](https://source.starfort.tech/andrew/libakgl), which is
vendored as a submodule. vendored as a submodule.
```sh ```sh norun
git submodule update --init --recursive git submodule update --init --recursive
cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON
cmake --build build-akgl 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 A verb that needs a device the build does not have does not crash and does not lie. It
reports itself by name: 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 ? 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 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 ## Running the tests
```sh ```sh norun
ctest --test-dir build --output-on-failure ctest --test-dir build --output-on-failure
``` ```

View File

@@ -4,7 +4,7 @@
Run `basic` with no arguments and you get a prompt: Run `basic` with no arguments and you get a prompt:
``` ```sh norun
$ ./build/basic $ ./build/basic
READY 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 Anything you type **with a line number** is stored as part of a program. Anything you
type **without** one runs immediately: type **without** one runs immediately:
``` ```basic repl
PRINT 2 + 2 PRINT 2 + 2
4 ```
READY
```output
4
``` ```
## Your first program ## Your first program
``` ```basic repl
10 PRINT "WHAT IS YOUR NAME" 10 PRINT "WHAT IS YOUR NAME"
20 INPUT "> " N$ 20 INPUT "> " N$
30 PRINT "HELLO, " + N$ 30 PRINT "HELLO, " + N$
RUN 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. Type `LIST` to see it back, `RUN` to run it again, and `NEW` to throw it away.
## Line numbers ## 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 Lines are stored under their numbers and run in numeric order, so the gaps are what
let you insert later: let you insert later:
``` ```basic repl
10 PRINT "FIRST" 10 PRINT "FIRST"
30 PRINT "THIRD" 30 PRINT "THIRD"
20 PRINT "SECOND" 20 PRINT "SECOND"
LIST LIST
```
```output
10 PRINT "FIRST" 10 PRINT "FIRST"
20 PRINT "SECOND" 20 PRINT "SECOND"
30 PRINT "THIRD" 30 PRINT "THIRD"
@@ -58,34 +73,53 @@ it off again.
Statements are separated by colons: Statements are separated by colons:
``` ```basic
10 A# = 1 : B# = 2 : PRINT A# + B# 10 A# = 1 : B# = 2 : PRINT A# + B#
``` ```
```output
3
```
There is one important limit: **block structures do not work inside a single line.** 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# 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 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 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: already past. Write loops across several lines:
``` ```basic
10 FOR I# = 1 TO 3 10 FOR I# = 1 TO 3
20 PRINT I# 20 PRINT I#
30 NEXT I# 30 NEXT I#
``` ```
```output
1
2
3
```
The same applies to `DO`/`LOOP`. The same applies to `DO`/`LOOP`.
## Running a file ## Running a file
```sh ```sh setup=program
$ ./build/basic program.bas $ ./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 The file is read, stored, and run. It is exactly the same as typing the program in and
saving yourself the trouble. saving yourself the trouble.
@@ -96,13 +130,29 @@ $ echo '10 PRINT "HI"
RUN' | ./build/basic RUN' | ./build/basic
``` ```
```output
READY
HI
READY
```
## Saving and loading ## Saving and loading
``` ```basic repl
10 PRINT "HI"
DSAVE "myprogram.bas" DSAVE "myprogram.bas"
NEW
DLOAD "myprogram.bas" 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"` `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. compares what is in memory against the file and prints `OK` if they match.

View File

@@ -11,7 +11,7 @@ a character that says what it holds:
| `%` | floating point | `RATE%`, `X%` | | `%` | floating point | `RATE%`, `X%` |
| `$` | string | `NAME$` | | `$` | string | `NAME$` |
``` ```basic
10 COUNT# = 42 10 COUNT# = 42
20 RATE% = 1.5 20 RATE% = 1.5
30 NAME$ = "ADA" 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: Integers are 64-bit. Floats are IEEE doubles, so they print with six decimal places:
``` ```basic repl
PRINT 1.5 PRINT 1.5
1.500000 ```
```output
1.500000
``` ```
Literals may be written in hexadecimal with a `0x` prefix. A leading zero is *not* 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: `+` concatenates, and it will concatenate a string with a number:
``` ```basic repl
PRINT "COUNT: " + 42 PRINT "COUNT: " + 42
COUNT: 42 ```
```output
COUNT: 42
``` ```
`*` repeats: `*` repeats:
``` ```basic repl
PRINT "-" * 20 PRINT "-" * 20
-------------------- ```
```output
--------------------
``` ```
## Arrays ## Arrays
@@ -64,12 +73,16 @@ PRINT "-" * 20
`DIM` makes one. Subscripts start at zero and the number you give is the *count*, so `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)`: `DIM A#(3)` gives you `A#(0)` through `A#(2)`:
``` ```basic
10 DIM A#(3) 10 DIM A#(3)
20 A#(0) = 10 : A#(1) = 20 : A#(2) = 30 20 A#(0) = 10 : A#(1) = 20 : A#(2) = 30
30 PRINT A#(0) + A#(1) + A#(2) 30 PRINT A#(0) + A#(1) + A#(2)
``` ```
```output
60
```
Arrays can have several dimensions: `DIM GRID#(10, 10)`. `LEN(A#)` gives the total Arrays can have several dimensions: `DIM GRID#(10, 10)`. `LEN(A#)` gives the total
number of elements. number of elements.
@@ -93,7 +106,7 @@ In order of precedence, tightest first:
Both mean equality **inside a condition**: Both mean equality **inside a condition**:
``` ```basic norun
10 IF A# = 5 THEN PRINT "FIVE" 10 IF A# = 5 THEN PRINT "FIVE"
20 IF A# == 5 THEN PRINT "ALSO 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: Anything non-zero is true, so `IF A# THEN ...` works:
``` ```basic
10 A# = 5 10 A# = 5
20 IF A# THEN PRINT "NON-ZERO IS TRUE" 20 IF A# THEN PRINT "NON-ZERO IS TRUE"
30 IF A# = 5 AND A# > 1 THEN PRINT "AND WORKS" 30 IF A# = 5 AND A# > 1 THEN PRINT "AND WORKS"
40 IF NOT (A# = 9) THEN PRINT "SO DOES NOT" 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`. `AND` and `OR` are still bitwise on ordinary numbers: `PRINT 12 AND 10` gives `8`.
## Comments ## Comments
`REM` comments to the end of the line. `REM` comments to the end of the line.
``` ```basic
10 REM This does nothing at all 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: `DEF` makes a single-expression function:
``` ```basic
10 DEF SQUARE(X#) = X# * X# 10 DEF SQUARE(X#) = X# * X#
20 PRINT SQUARE(7) 20 PRINT SQUARE(7)
``` ```
```output
49
```
A multi-line definition runs until `RETURN`, which is how you write a subroutine that A multi-line definition runs until `RETURN`, which is how you write a subroutine that
takes arguments: takes arguments:
``` ```basic
10 DEF GREET(N$) 10 DEF GREET(N$)
20 PRINT "HELLO, " + N$ 20 PRINT "HELLO, " + N$
30 RETURN 0 30 RETURN 0
40 X# = GREET("WORLD") 40 X# = GREET("WORLD")
``` ```
```output
HELLO, WORLD
```
`RETURN` carries the value back, so a multi-line `DEF` is a function even when you only `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. wanted the effect — assign the result somewhere to throw it away.

View File

@@ -2,15 +2,24 @@
## IF ... THEN ... ELSE ## IF ... THEN ... ELSE
``` ```basic
10 A# = 5 10 A# = 5
20 IF A# = 5 THEN PRINT "FIVE" ELSE PRINT "NOT FIVE" 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: 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 **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: `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" ```output
30 PRINT "STILL IN IT" IN THE BLOCK
40 BEND STILL IN IT
50 PRINT "AFTER" AFTER
``` ```
When the condition is false every line up to the `BEND` is skipped. When the condition is false every line up to the `BEND` is skipped.
## FOR ... NEXT ## FOR ... NEXT
``` ```basic
10 FOR I# = 1 TO 5 10 FOR I# = 1 TO 5
20 PRINT I# 20 PRINT I#
30 NEXT I# 30 NEXT I#
``` ```
```output
1
2
3
4
5
```
`STEP` sets the stride, and a negative one counts down: `STEP` sets the stride, and a negative one counts down:
``` ```basic
10 FOR I# = 10 TO 0 STEP -2 10 FOR I# = 10 TO 0 STEP -2
20 PRINT I# 20 PRINT I#
30 NEXT 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 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 - **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: `EXIT` leaves the loop early:
``` ```basic
10 FOR I# = 1 TO 100 10 FOR I# = 1 TO 100
20 IF I# = 5 THEN EXIT 20 IF I# = 5 THEN EXIT
30 NEXT I# 30 NEXT I#
40 PRINT "OUT"
```
```output
OUT
``` ```
## DO ... LOOP ## DO ... LOOP
The condition can go on either end, or neither: The condition can go on either end, or neither:
``` ```basic
10 I# = 0 10 I# = 0
20 DO WHILE I# < 3 20 DO WHILE I# < 3
30 PRINT I# 30 PRINT I#
@@ -82,7 +120,13 @@ The condition can go on either end, or neither:
50 LOOP 50 LOOP
``` ```
```output
0
1
2
``` ```
```basic
10 I# = 0 10 I# = 0
20 DO 20 DO
30 PRINT I# 30 PRINT I#
@@ -90,6 +134,12 @@ The condition can go on either end, or neither:
50 LOOP UNTIL I# = 3 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 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 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. 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 ## GOTO and GOSUB
``` ```basic
10 GOSUB 100 10 GOSUB 100
20 PRINT "BACK" 20 PRINT "BACK"
30 END 30 END
@@ -106,6 +156,11 @@ condition at all loops forever until an `EXIT` or a `GOTO` leaves it.
110 RETURN 110 RETURN
``` ```
```output
IN THE SUBROUTINE
BACK
```
`RETURN` goes back to the line after the `GOSUB`. `RETURN` goes back to the line after the `GOSUB`.
## Labels ## 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 A label is a name with no type suffix. It marks a line, and anything that takes a line
number takes a label instead: number takes a label instead:
``` ```basic
10 GOTO SETUP 10 GOTO SETUP
20 PRINT "SKIPPED" 20 PRINT "SKIPPED"
100 LABEL SETUP 100 LABEL SETUP
110 PRINT "ARRIVED" 110 PRINT "ARRIVED"
``` ```
```output
ARRIVED
```
**Labels are filed before the program runs**, so a forward `GOTO` works. This is worth **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 using for its own sake: a program written with labels is immune to `RENUMBER`, because
there is no number to rewrite. 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: `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 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: `TRAP` sends an error to a handler instead of stopping the program:
``` ```basic
10 TRAP HANDLER 10 TRAP HANDLER
20 DIM Q#(2) 20 DIM Q#(2)
30 PRINT Q#(9) 30 PRINT Q#(9)
@@ -152,6 +222,11 @@ returns.
120 RESUME NEXT 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 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 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. `ER` and `EL` with no suffix; this dialect has no bare variable names.

View File

@@ -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 `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. failed `INSTR` gives -1, not 0, so there is no ambiguity with a match at the start.
``` ```basic
10 A$ = "HELLO WORLD" 10 A$ = "HELLO WORLD"
20 PRINT LEN(A$) 20 PRINT LEN(A$)
30 PRINT LEFT(A$, 5) 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") 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 `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". 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. `PRINT USING` lays a value out in a fixed field, which is what makes columns line up.
``` ```basic
10 PRINT USING "###.##"; 3.14159 10 PRINT USING "###.##"; 3.14159
20 PRINT USING "TOTAL: $#,###.##"; 1234.5 20 PRINT USING "TOTAL: $#,###.##"; 1234.5
30 PRINT USING "####-"; -42 30 PRINT USING "####-"; -42
``` ```
``` ```output
3.14 3.14
TOTAL: $1,234.50 TOTAL: $1,234.50
42- 42-
@@ -68,11 +76,11 @@ stands:
is loud: printing more digits than the field asked for would push every later column is loud: printing more digits than the field asked for would push every later column
out of line. out of line.
``` ```basic
10 PRINT USING "###"; 99999 10 PRINT USING "###"; 99999
``` ```
``` ```output
*** ***
``` ```
@@ -81,12 +89,12 @@ printing `-5` as `5` would be worse.
String fields centre and right-justify: String fields centre and right-justify:
``` ```basic
10 PRINT USING "=========="; "MID" 10 PRINT USING "=========="; "MID"
20 PRINT USING ">>>>>>>>>>"; "RIGHT" 20 PRINT USING ">>>>>>>>>>"; "RIGHT"
``` ```
``` ```output
MID MID
RIGHT RIGHT
``` ```
@@ -100,12 +108,12 @@ Chapter 13.
to four, in this order: the leading blank, the thousands separator, the decimal point, to four, in this order: the leading blank, the thousands separator, the decimal point,
and the currency sign. and the currency sign.
``` ```basic
10 PUDEF "*" 10 PUDEF "*"
20 PRINT USING "#####"; 42 20 PRINT USING "#####"; 42
``` ```
``` ```output
***42 ***42
``` ```
@@ -116,11 +124,17 @@ only the padding.
`CHAR` puts text at a character position rather than at the cursor: `CHAR` puts text at a character position rather than at the cursor:
``` ```basic requires=akgl
10 CHAR 1, 10, 5, "HERE" 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, 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. and it refuses by name in the default build.

View File

@@ -3,8 +3,13 @@
Everything in this chapter needs the SDL build and a graphics device. Without one each Everything in this chapter needs the SDL build and a graphics device. Without one each
verb refuses by name: 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 ? 10 : RUNTIME ERROR DRAW needs a graphics device and this runtime has none
``` ```
## The coordinate space ## The coordinate space
@@ -19,7 +24,7 @@ screen.
`COLOR` binds a *source* to a palette index, and the drawing verbs name the source `COLOR` binds a *source* to a palette index, and the drawing verbs name the source
rather than the colour: rather than the colour:
``` ```basic requires=akgl
10 COLOR 1, 3 10 COLOR 1, 3
20 DRAW 1, 10, 20 20 DRAW 1, 10, 20
``` ```
@@ -37,7 +42,7 @@ refuses to draw.
### DRAW ### DRAW
``` ```basic requires=akgl
10 DRAW 1, 10, 20 10 DRAW 1, 10, 20
20 DRAW 1, 0, 0 TO 100, 100 TO 200, 0 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 ### BOX
``` ```basic requires=akgl
10 BOX 1, 10, 10, 40, 40 10 BOX 1, 10, 10, 40, 40
``` ```
@@ -55,7 +60,7 @@ Corners, and an optional rotation angle. An unrotated `BOX` outlines rather than
### CIRCLE ### CIRCLE
``` ```basic requires=akgl
10 CIRCLE 1, 160, 100, 50, 30 10 CIRCLE 1, 160, 100, 50, 30
``` ```
@@ -65,7 +70,7 @@ an arc or a polygon.
### PAINT ### PAINT
``` ```basic requires=akgl
10 PAINT 1, 160, 100 10 PAINT 1, 160, 100
``` ```
@@ -80,7 +85,7 @@ coordinates finishes.
### SCALE ### SCALE
``` ```basic requires=akgl
10 SCALE 1, 1023, 1023 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: `SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back:
``` ```basic requires=akgl
10 BOX 1, 0, 0, 20, 20 10 BOX 1, 0, 0, 20, 20
20 SSHAPE A$, 0, 0, 20, 20 20 SSHAPE A$, 0, 0, 20, 20
30 GSHAPE A$, 100, 100 30 GSHAPE A$, 100, 100

View File

@@ -6,7 +6,7 @@ name, and a machine with no sound card still runs the interpreter — only `SOUN
## SOUND ## SOUND
``` ```basic requires=akgl
10 SOUND 1, 4000, 60 10 SOUND 1, 4000, 60
``` ```
@@ -24,7 +24,7 @@ frame loop for a second would freeze the game.
`PLAY` takes a string of notes: `PLAY` takes a string of notes:
``` ```basic requires=akgl
10 PLAY "C D E F G" 10 PLAY "C D E F G"
``` ```
@@ -50,7 +50,7 @@ immediately `QUIT`s may not hear all of it.
## TEMPO ## TEMPO
``` ```basic requires=akgl
10 TEMPO 8 10 TEMPO 8
``` ```
@@ -60,7 +60,7 @@ transcription, so it may not match a real machine exactly.
## ENVELOPE and VOL ## ENVELOPE and VOL
``` ```basic requires=akgl
10 ENVELOPE 0, 5, 9, 4, 6 10 ENVELOPE 0, 5, 9, 4, 6
20 VOL 8 20 VOL 8
``` ```
@@ -72,8 +72,13 @@ and release. `VOL` sets the overall volume, 0 to 15.
`FILTER` parses and then **refuses at execution**: `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 It sets the SID's filter cutoff, band switches and resonance. The audio backend

View File

@@ -12,7 +12,7 @@ see and manipulate a script's sprites.
### From an image file ### From an image file
``` ```basic requires=akgl setup=ship
10 SPRSAV "ship.png", 1 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 ### From a saved region
``` ```basic requires=akgl
10 BOX 1, 0, 0, 24, 21 10 BOX 1, 0, 0, 24, 21
20 SSHAPE A$, 0, 0, 24, 21 20 SSHAPE A$, 0, 0, 24, 21
30 SPRSAV A$, 1 30 SPRSAV A$, 1
@@ -37,7 +37,7 @@ faithful rather than a shortcut.
### From data ### From data
``` ```basic norun
10 DIM P#(63) 10 DIM P#(63)
20 FOR I# = 0 TO 62 20 FOR I# = 0 TO 62
30 READ P#(I#) 30 READ P#(I#)
@@ -58,9 +58,10 @@ operation.
## Showing and moving ## Showing and moving
``` ```basic requires=akgl setup=ship
10 SPRITE 1, 1, 3 10 SPRSAV "ship.png", 1
20 MOVSPR 1, 100, 50 20 SPRITE 1, 1, 3
30 MOVSPR 1, 100, 50
``` ```
`SPRITE n [,on] [,colour] [,priority] [,xexpand] [,yexpand] [,multicolour]`. Only the `SPRITE n [,on] [,colour] [,priority] [,xexpand] [,yexpand] [,multicolour]`. Only the
@@ -88,7 +89,7 @@ raster coordinates.
## Collision ## Collision
``` ```basic norun
10 COLLISION 1, BUMPED 10 COLLISION 1, BUMPED
20 REM ... main loop ... 20 REM ... main loop ...
90 GOTO 20 90 GOTO 20

View File

@@ -5,12 +5,17 @@ something, and refuse by name where it does not.
## Program storage ## Program storage
``` ```basic repl
10 PRINT "HI"
DSAVE "myprogram.bas" DSAVE "myprogram.bas"
DLOAD "myprogram.bas" DLOAD "myprogram.bas"
VERIFY "myprogram.bas" VERIFY "myprogram.bas"
``` ```
```output
OK
```
`SAVE` and `LOAD` are the same verbs under their other names, and `DVERIFY` is `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 `VERIFY`. A program is saved as plain text with its line numbers, so you can edit it in
anything. anything.
@@ -22,7 +27,7 @@ many lines differ.
Ten channels, numbered 0 to 9. Ten channels, numbered 0 to 9.
``` ```basic
10 DOPEN 1, "scores.txt", W 10 DOPEN 1, "scores.txt", W
20 PRINT #1, "ADA 4000" 20 PRINT #1, "ADA 4000"
30 PRINT #1, "GRACE 3800" 30 PRINT #1, "GRACE 3800"
@@ -33,6 +38,10 @@ Ten channels, numbered 0 to 9.
80 DCLOSE 2 80 DCLOSE 2
``` ```
```output
ADA 4000
```
| Verb | What it does | | Verb | What it does |
|---|---| |---|---|
| `DOPEN n, "name"` | open for reading | | `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 Reading past the end of a file leaves the variable empty rather than raising, so a read
loop tests what it got: loop tests what it got:
``` ```basic setup=textfiles
10 DOPEN 1, "data.txt" 10 DOPEN 1, "data.txt"
20 DO 20 DO
30 INPUT #1, L$ 30 INPUT #1, L$
@@ -60,6 +69,11 @@ loop tests what it got:
70 DCLOSE 1 70 DCLOSE 1
``` ```
```output
ONE
TWO
```
Reading a channel opened for writing — or writing one opened for reading — is refused, Reading a channel opened for writing — or writing one opened for reading — is refused,
as is using a channel that is not open. 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 ## Managing files
``` ```basic setup=textfiles
10 COPY "a.txt", "b.txt" 10 COPY "a.txt", "b.txt"
20 CONCAT "a.txt", "b.txt" 20 CONCAT "a.txt", "b.txt"
30 RENAME "b.txt", "c.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 ## Binary blocks
``` ```basic
10 A$ = "BINARY DATA" 10 A$ = "BINARY DATA"
20 BSAVE "block.dat", POINTER(A$), POINTER(A$) + 12 20 B$ = "............"
30 BLOAD "block.dat", POINTER(B$), 12 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 `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 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 ## 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: 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 ? 10 : RUNTIME ERROR HEADER formats a disk, and there is no disk drive here -- only a filesystem
``` ```

View File

@@ -27,7 +27,7 @@ target_link_libraries(YOUR_GAME PRIVATE akbasic::akbasic)
## The shortest useful host ## The shortest useful host
```c ```c wrap=hostloop
#include <akbasic/runtime.h> #include <akbasic/runtime.h>
#include <akbasic/sink.h> #include <akbasic/sink.h>
@@ -65,7 +65,7 @@ immediately — audible, but never a hang.
Use `akbasic_runtime_global()`. It finds or creates the variable in the script's 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: outermost scope, which is the only place both of you can reliably see:
```c ```c wrap=hostbody
akbasic_Variable *health = NULL; akbasic_Variable *health = NULL;
int64_t subscript[1] = { 0 }; int64_t subscript[1] = { 0 };
@@ -80,7 +80,7 @@ it stops.
## Lending devices ## Lending devices
```c ```c wrap=hostbody
PASS(e, akbasic_runtime_set_devices(&RUNTIME, &graphics, &audio, &input, &sprites)); 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: `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_graphics_init_akgl(&graphics, &gstate, my_renderer));
PASS(e, akbasic_sprite_init_akgl(&sprites, &sstate, my_renderer, &gstate)); PASS(e, akbasic_sprite_init_akgl(&sprites, &sstate, my_renderer, &gstate));
``` ```

View File

@@ -46,11 +46,16 @@ leading zero is not octal; `0x` is hexadecimal.
**A whole loop on one line does not loop.** **A whole loop on one line does not loop.**
``` ```basic
10 FOR I# = 1 TO 3 : PRINT I# : NEXT I# 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. its `FOR` is never reached. The same applies to `DO`/`LOOP`. Write loops across lines.
## Two known defects in `FOR` ## Two known defects in `FOR`

View File

@@ -31,16 +31,30 @@ everything that behaves differently here and why.
## The shortest possible start ## The shortest possible start
``` ```sh norun
$ cmake -S . -B build && cmake --build build $ cmake -S . -B build && cmake --build build
$ ./build/basic $ ./build/basic
READY ```
```basic repl
10 FOR I# = 1 TO 5 10 FOR I# = 1 TO 5
20 PRINT "HELLO " + I# 20 PRINT "HELLO " + I#
30 NEXT I# 30 NEXT I#
RUN 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 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 **`+` immediately: **variables carry a type suffix** (`I#` is an integer) and **`+`
concatenates a string with a number**. Chapter 3 explains both. 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.

512
tests/docs_examples.sh Executable file
View File

@@ -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 <limits.h> 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}"

View File

@@ -0,0 +1,2 @@
SUCCEED_RETURN(e);
}

View File

@@ -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
* <akbasic/akgl.h> exists to include.
*/
#include <akerror.h>
#include <akbasic/runtime.h>
#include <akbasic/akgl.h>
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);

View File

@@ -0,0 +1,2 @@
SUCCEED_RETURN(e);
}

View File

@@ -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 <akerror.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
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);

View File

@@ -0,0 +1,6 @@
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
(void)score;
SUCCEED_RETURN(errctx);
}

View File

@@ -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 <akerror.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include <akbasic/variable.h>
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 {

View File

@@ -0,0 +1,2 @@
static int64_t your_clock_ms(void) { return 0; }
static void your_draw_a_frame(void) { }

View File

@@ -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 <stdio.h>
#include <akerror.h>
static int64_t your_clock_ms(void);
static void your_draw_a_frame(void);

View File

@@ -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 <akerror.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include <akbasic/variable.h>

View File

@@ -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

View File

@@ -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

View File

@@ -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