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)
|
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
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.
|
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
36
TODO.md
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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));
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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
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