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
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:
@@ -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$<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)
|
||||
_set_tests_properties(
|
||||
${AKBASIC_TESTS} ${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS}
|
||||
|
||||
225
MAINTENANCE.md
Normal file
225
MAINTENANCE.md
Normal 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 **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.
|
||||
48
README.md
48
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 <akerror.h>
|
||||
#include <akbasic/runtime.h>
|
||||
#include <akbasic/sink.h>
|
||||
@@ -335,7 +357,7 @@ type comes from the name's suffix, exactly as it does for BASIC code, so `HP#` i
|
||||
and `NAME$` is a string. Every scalar is really a one-element array, which is why the subscript
|
||||
list is `{0}` with a count of 1.
|
||||
|
||||
```c
|
||||
```c wrap=hostvars
|
||||
/* host -> script, before the script starts */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *host_set_int(akbasic_Runtime *obj, const char *name, int64_t value)
|
||||
{
|
||||
@@ -367,7 +389,7 @@ akerr_ErrorContext AKERR_NOIGNORE *host_get_int(akbasic_Runtime *obj, const char
|
||||
|
||||
Used like this, with `akbasic_variable_set_string` and `set_float` as the other two:
|
||||
|
||||
```c
|
||||
```c wrap=hostcalls
|
||||
CATCH(errctx, akbasic_runtime_load(&SCRIPT, PROGRAM));
|
||||
|
||||
CATCH(errctx, host_set_int(&SCRIPT, "HP#", 100)); /* seed */
|
||||
@@ -411,7 +433,7 @@ a `GOSUB`, and still readable by the script afterwards.
|
||||
|
||||
`PRINT` writes through an `akbasic_TextSink`, which is a record of function pointers plus whatever state you hang off `self`:
|
||||
|
||||
```c
|
||||
```c excerpt=include/akbasic/sink.h
|
||||
typedef struct akbasic_TextSink
|
||||
{
|
||||
void *self;
|
||||
@@ -419,6 +441,8 @@ typedef struct akbasic_TextSink
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*writeln)(struct akbasic_TextSink *self, const char *text);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*readline)(struct akbasic_TextSink *self, char *dest, size_t len, bool *eof);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*clear)(struct akbasic_TextSink *self);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*moveto)(struct akbasic_TextSink *self, int col, int row);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*window)(struct akbasic_TextSink *self, int left, int top, int right, int bottom);
|
||||
} akbasic_TextSink;
|
||||
```
|
||||
|
||||
|
||||
36
TODO.md
36
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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
```output
|
||||
4
|
||||
READY
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -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,8 +31,11 @@ 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
|
||||
```
|
||||
|
||||
```output
|
||||
1.500000
|
||||
```
|
||||
|
||||
@@ -47,15 +50,21 @@ escaping: a string cannot contain a double quote.
|
||||
|
||||
`+` concatenates, and it will concatenate a string with a number:
|
||||
|
||||
```
|
||||
```basic repl
|
||||
PRINT "COUNT: " + 42
|
||||
```
|
||||
|
||||
```output
|
||||
COUNT: 42
|
||||
```
|
||||
|
||||
`*` repeats:
|
||||
|
||||
```
|
||||
```basic repl
|
||||
PRINT "-" * 20
|
||||
```
|
||||
|
||||
```output
|
||||
--------------------
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -27,7 +27,7 @@ target_link_libraries(YOUR_GAME PRIVATE akbasic::akbasic)
|
||||
|
||||
## The shortest useful host
|
||||
|
||||
```c
|
||||
```c wrap=hostloop
|
||||
#include <akbasic/runtime.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
|
||||
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));
|
||||
```
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
512
tests/docs_examples.sh
Executable file
512
tests/docs_examples.sh
Executable 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}"
|
||||
2
tests/docs_preludes/akglbody.post
Normal file
2
tests/docs_preludes/akglbody.post
Normal file
@@ -0,0 +1,2 @@
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
21
tests/docs_preludes/akglbody.pre
Normal file
21
tests/docs_preludes/akglbody.pre
Normal 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);
|
||||
2
tests/docs_preludes/hostbody.post
Normal file
2
tests/docs_preludes/hostbody.post
Normal file
@@ -0,0 +1,2 @@
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
22
tests/docs_preludes/hostbody.pre
Normal file
22
tests/docs_preludes/hostbody.pre
Normal 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);
|
||||
6
tests/docs_preludes/hostcalls.post
Normal file
6
tests/docs_preludes/hostcalls.post
Normal file
@@ -0,0 +1,6 @@
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
(void)score;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
27
tests/docs_preludes/hostcalls.pre
Normal file
27
tests/docs_preludes/hostcalls.pre
Normal 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 {
|
||||
2
tests/docs_preludes/hostloop.post
Normal file
2
tests/docs_preludes/hostloop.post
Normal file
@@ -0,0 +1,2 @@
|
||||
static int64_t your_clock_ms(void) { return 0; }
|
||||
static void your_draw_a_frame(void) { }
|
||||
13
tests/docs_preludes/hostloop.pre
Normal file
13
tests/docs_preludes/hostloop.pre
Normal 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);
|
||||
10
tests/docs_preludes/hostvars.pre
Normal file
10
tests/docs_preludes/hostvars.pre
Normal 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>
|
||||
8
tests/docs_setups/program.sh
Normal file
8
tests/docs_setups/program.sh
Normal 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
|
||||
6
tests/docs_setups/ship.sh
Normal file
6
tests/docs_setups/ship.sh
Normal 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
|
||||
7
tests/docs_setups/textfiles.sh
Normal file
7
tests/docs_setups/textfiles.sh
Normal 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
|
||||
Reference in New Issue
Block a user