Compare commits
1 Commits
galaga-tut
...
16
| Author | SHA1 | Date | |
|---|---|---|---|
| 699ac9ab93 |
@@ -17,7 +17,7 @@ jobs:
|
||||
# Not recursive, deliberately. The top-level build needs
|
||||
# deps/libakerror and deps/libakstdlib, via add_subdirectory, and
|
||||
# nothing else: the golden corpus and the Commodore font now live in
|
||||
# this repository (tests/language/ and assets/fonts/), so
|
||||
# this repository (tests/reference/ and assets/fonts/), so
|
||||
# deps/basicinterpret is no longer a build dependency at all.
|
||||
# It does *not* need deps/libakgl, which is guarded behind
|
||||
# AKBASIC_WITH_AKGL and defaults OFF -- and recursing into it would
|
||||
@@ -62,9 +62,11 @@ jobs:
|
||||
run: |
|
||||
cmake -S . -B build
|
||||
cmake --build build --parallel 2
|
||||
# The suite is 112 cases: 65 language files with sibling expectations,
|
||||
# 43 unit tests, 3 embedding examples, and docs_examples.
|
||||
# Some unit tests assert the *correct* contract for known defects (TODO.md
|
||||
# The suite is 78 cases: 41 golden files byte-compared against the Go
|
||||
# reference's own corpus (checked in at tests/reference/, see its README),
|
||||
# 9 local golden cases for verbs the reference never implemented, 25 unit
|
||||
# tests, 2 embedding examples, and 1 known-failing test that asserts the
|
||||
# *correct* contract for defects carried over from the reference (TODO.md
|
||||
# section 6). A green run therefore does not mean defect-free -- see
|
||||
# AKBASIC_KNOWN_FAILING_TESTS.
|
||||
#
|
||||
|
||||
@@ -26,7 +26,7 @@ scripting engine for game authors.
|
||||
| [the issue tracker](https://source.starfort.tech/andrew/akbasic/issues) | **Outstanding defects and gaps.** Labelled by kind and blast radius; `status::grooming` means the scope is not settled yet |
|
||||
| [`TODO.md`](TODO.md) | The record: settled design decisions, the deviation register, defects already fixed, and the reasoning behind the measurements. §0.1 first — it retires the byte-for-byte fidelity constraint several later sections were written on |
|
||||
| [`README.md`](README.md) | What the project is and why, for somebody who has not seen it |
|
||||
| [`docs/`](docs/README.md) | The language itself: twenty-one chapters, verb and function reference. [Chapter 14](docs/14-architecture.md) is the interpreter's architecture — the step loop, the pools, the two kinds of error, and how to debug it. [Chapter 15](docs/15-error-codes.md) is the error-code appendix. [Chapters 17](docs/17-tutorial-breakout.md) and [18](docs/18-tutorial-breakout-artwork.md) are tutorials that build the games in `examples/breakout/`; [Chapters 20](docs/20-tutorial-galaga.md) and [21](docs/21-tutorial-galaga-enemies.md) build the embedding host in `examples/galaga/` |
|
||||
| [`docs/`](docs/README.md) | The language itself: eighteen chapters, verb and function reference. [Chapter 14](docs/14-architecture.md) is the interpreter's architecture — the step loop, the pools, the two kinds of error, and how to debug it. [Chapter 15](docs/15-error-codes.md) is the error-code appendix. [Chapters 17](docs/17-tutorial-breakout.md) and [18](docs/18-tutorial-breakout-artwork.md) are tutorials that build the games in `examples/breakout/` |
|
||||
| `deps/libakerror/AGENTS.md` | The `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/`FINISH` protocol, authoritatively |
|
||||
| `deps/libakerror/UPGRADING.md` | 1.0.0's status registry. Required before writing an error code |
|
||||
| `deps/<library>/AGENTS.md` | Per-repo rules. Read the relevant one **before editing a submodule** |
|
||||
@@ -57,9 +57,9 @@ repeating where you will see them:
|
||||
`include/akgl/SDL_GameControllerDB.h`. Change the template or the generator script.
|
||||
- **Do not reformat code you are not otherwise changing.** Several files mix tabs and spaces
|
||||
and there is no repo-wide formatter; style conversions get their own commit.
|
||||
- **Keep `tests/language/` editable.** Its `.bas` programs and sibling `.txt` expectations
|
||||
are changed together when behavior changes. Record deliberate language decisions in
|
||||
`TODO.md` or `docs/13-differences.md`.
|
||||
- **Do not edit `tests/reference/`.** Those expectations came from the Go implementation and
|
||||
are never edited to suit this interpreter. A deliberate divergence goes in
|
||||
`tests/reference/README.md`'s divergence table and `docs/13-differences.md`.
|
||||
- **Open an issue for outstanding work; do not add it to `TODO.md`.**
|
||||
<https://source.starfort.tech/andrew/akbasic/issues>, or `tea issues create --repo
|
||||
andrew/akbasic`. Name the file and line, the functional consequence, and what closing it would
|
||||
|
||||
128
CMakeLists.txt
128
CMakeLists.txt
@@ -268,69 +268,6 @@ if(AKBASIC_BUILD_EXAMPLES)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# The galaga example: a C game on libakgl with the interpreter embedded as its
|
||||
# enemy-behavior engine. Chapters 20 and 21 build it from an empty file, so it
|
||||
# is compiled and run by every AKGL build rather than rotting in a document.
|
||||
# The asset, script and font paths are baked in so the smoke test can launch
|
||||
# from any working directory; --assets and --script override them at runtime.
|
||||
if(AKBASIC_BUILD_EXAMPLES AND AKBASIC_WITH_AKGL)
|
||||
add_executable(akbasic_example_galaga
|
||||
examples/galaga/main.c
|
||||
examples/galaga/script.c
|
||||
examples/galaga/enemies.c
|
||||
examples/galaga/player.c)
|
||||
target_compile_options(akbasic_example_galaga PRIVATE -Wall -Wextra)
|
||||
target_compile_definitions(akbasic_example_galaga PRIVATE
|
||||
GALAGA_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/assets"
|
||||
GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/galaga.bas"
|
||||
GALAGA_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
|
||||
target_link_libraries(akbasic_example_galaga PRIVATE akbasic akgl
|
||||
SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image)
|
||||
akbasic_instrument(akbasic_example_galaga)
|
||||
# Ten seconds of scripted play under the headless drivers: the script boots,
|
||||
# a wave enters and forms, the autoplay pilot shoots at it, and the program
|
||||
# tears down and exits 0. A tutorial that stops working fails here rather
|
||||
# than in front of a reader.
|
||||
_add_test(NAME example_galaga COMMAND akbasic_example_galaga --frames 600 --autoplay)
|
||||
_set_tests_properties(example_galaga PROPERTIES TIMEOUT 120
|
||||
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
|
||||
|
||||
# The boundary's round-trip test: links the real script.c and the real
|
||||
# galaga.bas, and fails the moment the two sides of the interop disagree.
|
||||
add_executable(akbasic_example_galaga_interop
|
||||
examples/galaga/interop_test.c
|
||||
examples/galaga/script.c)
|
||||
target_compile_options(akbasic_example_galaga_interop PRIVATE -Wall -Wextra)
|
||||
target_compile_definitions(akbasic_example_galaga_interop PRIVATE
|
||||
GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/galaga.bas")
|
||||
target_link_libraries(akbasic_example_galaga_interop PRIVATE akbasic akgl
|
||||
SDL3::SDL3 m)
|
||||
akbasic_instrument(akbasic_example_galaga_interop)
|
||||
_add_test(NAME example_galaga_interop COMMAND akbasic_example_galaga_interop)
|
||||
_set_tests_properties(example_galaga_interop PROPERTIES TIMEOUT 120)
|
||||
|
||||
# Regenerating the game figures in docs/ is a deliberate act, never part of
|
||||
# a build, for the same reason docs_screenshots is: the PNGs are checked in.
|
||||
# Wall-clock dt makes each regeneration differ by a few pixels of starfield,
|
||||
# so expect a binary diff every time this runs; commit one only when the
|
||||
# content changed on purpose. (docs_galaga_figures, not docs_game_figures:
|
||||
# the libakgl submodule already owns that target name.)
|
||||
add_custom_target(docs_galaga_figures
|
||||
COMMAND ${CMAKE_COMMAND} -E env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy
|
||||
SDL_RENDER_DRIVER=software
|
||||
$<TARGET_FILE:akbasic_example_galaga> --frames 40
|
||||
--screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/galaga-title.png"
|
||||
--screenshot-frame 30
|
||||
COMMAND ${CMAKE_COMMAND} -E env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy
|
||||
SDL_RENDER_DRIVER=software
|
||||
$<TARGET_FILE:akbasic_example_galaga> --autoplay --frames 370
|
||||
--screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/galaga-wave.png"
|
||||
--screenshot-frame 360
|
||||
DEPENDS akbasic_example_galaga
|
||||
COMMENT "Regenerating the galaga figures in docs/images"
|
||||
VERBATIM)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests.
|
||||
#
|
||||
@@ -511,7 +448,7 @@ if(AKBASIC_WITH_AKGL)
|
||||
# against.
|
||||
#
|
||||
# **A byte comparison of a rendered PNG is a deliberate bet**, the same bet
|
||||
# the language corpus makes about golden output: that the dummy video
|
||||
# tests/reference/ already makes about golden output: that the dummy video
|
||||
# driver and the software renderer are reproducible. They are, run to run and
|
||||
# build to build. What is untested is an SDL upgrade that shifts one pixel of
|
||||
# a diagonal, and the answer to that is to regenerate the figures in the same
|
||||
@@ -594,20 +531,63 @@ if(AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
|
||||
)
|
||||
endif()
|
||||
|
||||
# The editable language corpus. One CTest case per .bas so a failure names the
|
||||
# file. Each program is paired with a sibling .txt expectation; see
|
||||
# tests/language/README.md for the editing rule.
|
||||
# The reference's own corpus, byte-compared against the sibling .txt. One CTest
|
||||
# case per .bas so a failure names the file.
|
||||
#
|
||||
# The corpus includes programs carried over from the deprecated Go implementation
|
||||
# and cases written for this interpreter. Their provenance is useful when
|
||||
# investigating a regression, but it does not make any case immutable.
|
||||
# It used to be driven in place out of deps/basicinterpret, on the reasoning that
|
||||
# copying a submodule's corpus guarantees drift. That reasoning was sound and it
|
||||
# has been overruled deliberately: the Go dependency is being deprecated, and a
|
||||
# build that cannot run its acceptance suite without cloning the implementation
|
||||
# it replaced is not finished. The copy is byte-identical to
|
||||
# basicinterpreter@d76162c and tests/reference/README.md records
|
||||
# where it came from and what the drift now costs.
|
||||
file(GLOB_RECURSE AKBASIC_GOLDEN_CASES
|
||||
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/tests/reference"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/reference/*.bas"
|
||||
)
|
||||
|
||||
foreach(_case IN LISTS AKBASIC_GOLDEN_CASES)
|
||||
string(REGEX REPLACE "^tests/" "" _name "${_case}")
|
||||
string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
|
||||
string(REPLACE "/" "_" _name "${_name}")
|
||||
_add_test(
|
||||
NAME golden_${_name}
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DBASIC=$<TARGET_FILE:basic>
|
||||
-DCASE=${CMAKE_CURRENT_SOURCE_DIR}/tests/reference/${_case}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/golden.cmake
|
||||
)
|
||||
endforeach()
|
||||
|
||||
if(AKBASIC_GOLDEN_CASES)
|
||||
set(AKBASIC_GOLDEN_NAMES)
|
||||
foreach(_case IN LISTS AKBASIC_GOLDEN_CASES)
|
||||
string(REGEX REPLACE "^tests/" "" _name "${_case}")
|
||||
string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
|
||||
string(REPLACE "/" "_" _name "${_name}")
|
||||
list(APPEND AKBASIC_GOLDEN_NAMES golden_${_name})
|
||||
endforeach()
|
||||
_set_tests_properties(${AKBASIC_GOLDEN_NAMES} PROPERTIES TIMEOUT 30)
|
||||
# An AKGL build of `basic` opens a window, and forty-one of them is not what
|
||||
# anybody running the suite wanted. The dummy driver produces the same stdout,
|
||||
# which is the only thing a golden case compares.
|
||||
if(AKBASIC_WITH_AKGL)
|
||||
_set_tests_properties(${AKBASIC_GOLDEN_NAMES} PROPERTIES
|
||||
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The local golden corpus, for verbs the reference never implemented.
|
||||
#
|
||||
# Programs carried over from the deprecated Go implementation and cases written
|
||||
# for this interpreter use the same editable `.bas`/`.txt` contract. Registered
|
||||
# under local_ so every failure identifies the program that produced it.
|
||||
# Still separate now that the reference's corpus lives in this repository too,
|
||||
# and the reason changed rather than went away: tests/reference/ is a *record* of
|
||||
# what the Go implementation did and nothing in it should ever be edited to suit
|
||||
# this one, while tests/language/ is ours to change. Registered under local_ so a
|
||||
# failure says at a glance which of the two it came from -- and so a diff that
|
||||
# touches tests/reference/ stands out as the thing it is.
|
||||
#
|
||||
# Note what this can and cannot cover. The graphics and sound verbs draw and play
|
||||
# rather than print, so what a golden file sees of them is their refusals and
|
||||
# rather than print, so what a golden file sees of them is their *refusals* and
|
||||
# whatever a program can PRINT about the state they changed. The behaviour that
|
||||
# reaches a device is asserted against tests/mockdevice.h instead.
|
||||
file(GLOB_RECURSE AKBASIC_LOCAL_CASES
|
||||
|
||||
@@ -88,7 +88,7 @@ source stays readable as documentation of what the original did. Neither is bind
|
||||
|
||||
**It is not a build or test dependency.** Both configurations have been configured, built and
|
||||
run from scratch with it moved out of the tree. Its acceptance corpus is checked in at
|
||||
`tests/language/` and its Commodore font at `assets/fonts/`.
|
||||
`tests/reference/` and its Commodore font at `assets/fonts/`.
|
||||
|
||||
```sh norun
|
||||
cd deps/basicinterpret
|
||||
@@ -351,10 +351,15 @@ name. That is not cosmetic: `add_executable` creates a dependency's targets even
|
||||
|
||||
### The golden corpora
|
||||
|
||||
`tests/language/` is the editable language corpus. It includes cases carried over from the
|
||||
deprecated Go implementation as well as cases written for this interpreter. Every `.bas` file
|
||||
has a sibling `.txt` expectation, and a new language feature needs that pair as well as unit
|
||||
tests. Change both deliberately in the same commit; provenance does not make a case immutable.
|
||||
`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
|
||||
`tests/reference/README.md`'s divergence table 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. A new language feature needs a
|
||||
`.bas`/`.txt` pair there as well as unit tests.
|
||||
|
||||
### Mutation-check a fix before you believe it
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ implementation that started from the Java Lox instructions in
|
||||
[craftinginterpreters.com](https://craftinginterpreters.com) and then struck off on its own. That
|
||||
project is deprecated. It is vendored here as the behavioural spec to read when a question about
|
||||
semantics comes up, and its acceptance corpus is checked in at
|
||||
[`tests/language/`](tests/language/README.md) and runs on every build — so nothing about
|
||||
[`tests/reference/`](tests/reference/README.md) and runs on every build — so nothing about
|
||||
building or testing this project needs it.
|
||||
|
||||
## Quickstart
|
||||
@@ -24,7 +24,7 @@ ctest --test-dir build --output-on-failure
|
||||
|
||||
```sh norun
|
||||
./build/basic # the REPL
|
||||
./build/basic tests/language/functions.bas # run a program
|
||||
./build/basic tests/reference/language/functions.bas # run a program
|
||||
```
|
||||
|
||||
```basic
|
||||
@@ -126,10 +126,10 @@ version are catalogued in [`TODO.md`](TODO.md) and summarised for a BASIC progra
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| [`docs/`](docs/README.md) | The guide: twenty-one chapters, the language then each hardware area then a reference section for every verb and function, [Chapter 14](docs/14-architecture.md) on the interpreter's own architecture, [Chapter 15](docs/15-error-codes.md) listing every error code, [Chapters 17](docs/17-tutorial-breakout.md) and [18](docs/18-tutorial-breakout-artwork.md) building a whole game twice, and [Chapters 20](docs/20-tutorial-galaga.md) and [21](docs/21-tutorial-galaga-enemies.md) building a C game that embeds the interpreter |
|
||||
| [`docs/`](docs/README.md) | The guide: eighteen chapters, the language then each hardware area then a reference section for every verb and function, [Chapter 14](docs/14-architecture.md) on the interpreter's own architecture, [Chapter 15](docs/15-error-codes.md) listing every error code, and [Chapters 17](docs/17-tutorial-breakout.md) and [18](docs/18-tutorial-breakout-artwork.md) building a whole game twice |
|
||||
| [`MAINTENANCE.md`](MAINTENANCE.md) | For contributors and maintainers: the documentation-example harness, the three test lists, mutation testing, error-code allocation, style |
|
||||
| [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence |
|
||||
| [`tests/language/README.md`](tests/language/README.md) | The editable language corpus and the rule for changing it |
|
||||
| [`tests/reference/README.md`](tests/reference/README.md) | Where the golden corpus came from, and the rule for changing it |
|
||||
|
||||
API documentation builds with `doxygen Doxyfile`, into `build/docs/html`.
|
||||
|
||||
|
||||
37
TODO.md
37
TODO.md
@@ -38,7 +38,7 @@ What it changes:
|
||||
- **§1.8's message-text contract is now a convention.** Improving a message is allowed; it costs
|
||||
a golden file, which is a cost rather than a veto.
|
||||
- **§5's bar drops** from "defensible against the golden suite" to defensible on its own merits.
|
||||
- **`tests/language/` is the editable language corpus rather than a protected specification.** Diverging from
|
||||
- **`tests/reference/` becomes a regression suite rather than a specification.** Diverging from
|
||||
it is allowed and must be deliberate and recorded — see its README.
|
||||
|
||||
What it does **not** change:
|
||||
@@ -306,13 +306,13 @@ to `Println`, which adds another.
|
||||
**This used to be a hard contract and is now a default.** The Go implementation is deprecated
|
||||
and will not be updated, so the two projects are no longer required to match — see §0.1. What
|
||||
survives is the practical half: these strings and this newline behaviour are what every
|
||||
expectation in `tests/language/` was written against, so changing one means changing the paired
|
||||
expectation in `tests/reference/` was written against, so changing one means changing golden
|
||||
files, and that is worth doing on purpose rather than by accident. A message that reads
|
||||
awkwardly *may* now be improved; do it deliberately, move the expectations in the same commit,
|
||||
and add a line to §5.
|
||||
|
||||
Numeric formatting still matches the reference: integers via `%" PRId64 "`, floats via `%f`
|
||||
(Go's `%f` and C's `%f` both give six decimals — `tests/language/arithmetic/float.txt`
|
||||
(Go's `%f` and C's `%f` both give six decimals — `tests/reference/language/arithmetic/float.txt`
|
||||
confirms). No reason to change it, which is different from not being allowed to.
|
||||
|
||||
### 1.9 Which `libakstdlib` calls are cleared for use — **the bans are lifted**
|
||||
@@ -375,7 +375,7 @@ Phases 0 through 6 of the original plan are done. The interpreter builds clean u
|
||||
|
||||
It *did* reproduce the reference byte for byte, and that claim is retired rather than broken:
|
||||
§0.1 released it, and one case has since diverged deliberately (§6 item 16, listed in
|
||||
`tests/language/README.md`). Everything else still matches, which is worth knowing but is no
|
||||
`tests/reference/README.md`). Everything else still matches, which is worth knowing but is no
|
||||
longer a gate.
|
||||
|
||||
```sh
|
||||
@@ -408,16 +408,17 @@ and the only path that existed — `AKBASIC_MODE_RUNSTREAM` reading through the
|
||||
is the code the README quotes, built by every build and registered as a CTest case so a
|
||||
signature change breaks the build rather than rotting the document.
|
||||
|
||||
**The acceptance suite is the editable language corpus, checked in at `tests/language/`.** All
|
||||
65 `.bas` files are registered as individual CTest cases and compared against their `.txt`
|
||||
**The acceptance suite is the reference's own corpus, checked in at `tests/reference/`.** All
|
||||
41 `.bas` files are registered as individual CTest cases and byte-compared against their `.txt`
|
||||
— including the trailing double newline on an error line (§1.8).
|
||||
|
||||
It was driven *in place* out of `deps/basicinterpret` until 2026-07-31, on the reasoning that
|
||||
copying a submodule's corpus guarantees drift. That reasoning was sound and was overruled
|
||||
deliberately: the Go dependency is being deprecated, and a build that cannot run its own
|
||||
acceptance suite without cloning the implementation it replaced is not finished. The copy is
|
||||
originally byte-identical to `basicinterpreter@d76162c`, and `tests/language/README.md` records the
|
||||
provenance and the rule that programs and expectations are edited together deliberately.
|
||||
byte-identical to `basicinterpreter@d76162c`, and `tests/reference/README.md` records the
|
||||
provenance, the cost of the drift nobody is watching for now, and the rule that those
|
||||
expectations are never edited to suit this interpreter.
|
||||
|
||||
**Nothing in the build or the suite needs `deps/basicinterpret` any more**, and that is checked
|
||||
rather than assumed — both configurations were configured, built and run from scratch with the
|
||||
@@ -1025,7 +1026,7 @@ deviations from the reference's *program*: `main.go` and the SDL half of
|
||||
|
||||
**What it costs a program:** a listing that used `INPUT$`, `LEN#` or `GOTO%` as a variable
|
||||
stops parsing, and the fix is to rename the variable. One case in the reference's own corpus
|
||||
did exactly that; see `tests/language/examples/strreverse.bas`.
|
||||
did exactly that; see `tests/reference/README.md`.
|
||||
|
||||
### Deviations in statement separation
|
||||
|
||||
@@ -1390,10 +1391,10 @@ deviations from the reference's *program*: `main.go` and the SDL half of
|
||||
**This one moved a golden file.** A line with no number used to be filed under the loader's
|
||||
cursor unchanged — that is, on top of the line before it — so two unnumbered lines in a row
|
||||
silently lost the first, and a *blank* line erased whatever preceded it. The reference does
|
||||
the same, and `tests/language/arithmetic/integer.bas` is the proof: four `PRINT`
|
||||
the same, and `tests/reference/language/arithmetic/integer.bas` is the proof: four `PRINT`
|
||||
statements, an expectation with three values, and a trailing blank line that erased
|
||||
`40 PRINT 4 - 2` before the program ran. The expectation is now `4 4 2 2` and
|
||||
`tests/language/README.md` records it.
|
||||
`tests/reference/README.md` records it.
|
||||
|
||||
In its place: `akbasic_runtime_file_line()` (`src/runtime.c`) is the one implementation of
|
||||
the rule, shared by `akbasic_runtime_load()`, RUNSTREAM and `DLOAD`. A numbered line is
|
||||
@@ -1585,9 +1586,9 @@ be reproduced before it can be fixed.
|
||||
|
||||
**It cost one golden case, exactly as predicted, and the cost turned out to be nothing.**
|
||||
The reference's `examples/strreverse.bas` names a variable `INPUT$`; the variable is renamed
|
||||
to `SOURCE$` in `tests/language/examples/strreverse.bas`, the expectation is byte-for-byte unchanged — the program
|
||||
to `SOURCE$` in `tests/reference/`, the expectation is byte-for-byte unchanged — the program
|
||||
still prints `REVERSED: OLLEH` — and the case keeps every bit of its coverage. Recorded in
|
||||
`tests/language/README.md` records the corpus editing rule.
|
||||
`tests/reference/README.md`'s divergence table, which this is the first entry in.
|
||||
|
||||
This item sat parked because the corpus was the acceptance contract and lived in a submodule
|
||||
this repository could not edit. Both premises are gone: the corpus is checked in, and
|
||||
@@ -2231,10 +2232,10 @@ update --init --recursive` gets them.
|
||||
|
||||
## 8. Status
|
||||
|
||||
**The port is done.** The C interpreter passes the language corpus and passes clean
|
||||
**The port is done.** The C interpreter passes the Go reference's entire corpus and passes clean
|
||||
under ASan and UBSan. It reproduced that corpus byte for byte until §0.1 retired the
|
||||
requirement; two cases have diverged on purpose since, and
|
||||
`tests/language/README.md` lists them.
|
||||
`tests/reference/README.md` lists them.
|
||||
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
@@ -2242,7 +2243,7 @@ requirement; two cases have diverged on purpose since, and
|
||||
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 112/112 headless, with `akgl_typing` skipping itself. The same set minus the four `no_device` cases the SDL driver contradicts, plus `akgl_backends`, `akgl_frontend`, `docs_screenshots` 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: 71 programs, 9 transcripts, 79 output comparisons, 4 C snippets, 2 excerpts, 2 shell blocks and 19 figures in the default build. The C-snippet count reads 0 when the harness is run by hand without `--cflags-file`; CTest passes it. `MAINTENANCE.md` documents the fence-tag convention |
|
||||
| `docs_screenshots` | 19/19 figures re-rendered and byte-identical to the checked-in PNGs. AKGL build only — rendering a picture needs the SDL half |
|
||||
| Language corpus | 65/65 paired expectations — **and 65/65 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 | 112/112 |
|
||||
| Line coverage | 94.1% (7227/7681) — above the 90% gate |
|
||||
| Function coverage | 97.9% (474/484) |
|
||||
@@ -2552,7 +2553,7 @@ What remains, in priority order:
|
||||
this whole arrangement exists against.
|
||||
|
||||
The byte comparison is a deliberate bet that the dummy driver and the software renderer
|
||||
are reproducible, which is the same bet `tests/language/` makes about golden
|
||||
are reproducible, which is the same bet `tests/reference/` already makes about golden
|
||||
output. Verified run-to-run and build-to-build here; what is untested is an SDL upgrade
|
||||
that moves one pixel of a diagonal. **If that happens, regenerate the figures in the same
|
||||
commit as the bump** — do not weaken the test to a size check, which would pass for every
|
||||
@@ -2635,7 +2636,7 @@ reduced against `build/basic`, the stdio build, unless it says otherwise.
|
||||
|
||||
**It is narrower than it looks, and the golden corpus is why.** The first attempt
|
||||
released the scope on *any* skip, which broke
|
||||
`tests/language/flowcontrol/nestedforloopwaitingforcommand.bas`: a zero-
|
||||
`tests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas`: a zero-
|
||||
iteration `FOR` skips its body by the same mechanism, and there the orphan is
|
||||
load-bearing -- it is what absorbs the inner `NEXT` so the outer one still finds its
|
||||
`FOR`. Releasing it turns that case into "NEXT outside the context of FOR". So the
|
||||
|
||||
@@ -100,50 +100,6 @@ bounded run is usually inside a `FOR` or `GOSUB` body, and a variable created th
|
||||
dies when the body pops — silently, with the script reading it correctly right up until
|
||||
it stops.
|
||||
|
||||
## Calling a function every frame
|
||||
|
||||
`akbasic_runtime_call_function()` calls a `DEF` by name with values you already
|
||||
hold — the entry point a game loop wants. A host that calls it repeatedly signs
|
||||
up for three rules the one-shot examples never meet:
|
||||
|
||||
```c wrap=hostcalls
|
||||
CATCH(errctx, akbasic_runtime_call_function(&SCRIPT, "THINK", argp, 1, &result));
|
||||
/* ...consume the result... */
|
||||
CATCH(errctx, akbasic_environment_zero(SCRIPT.environment));
|
||||
```
|
||||
|
||||
1. **Reset the value scratch after every call, once the result is consumed.**
|
||||
Each call parks its result in the caller environment's per-line scratch
|
||||
(`AKBASIC_MAX_VALUES` slots), and a host calling in a loop never crosses the
|
||||
line boundary that would reset it. Skip the `akbasic_environment_zero()` and
|
||||
the pool drains — measured at under two frames of forty calls — after which
|
||||
every call fails with `Maximum values per line reached`. The reset also
|
||||
invalidates `result`, which is why it comes after the consumption.
|
||||
2. **Force RUN mode once after the boot run.** A multi-line `DEF` body only
|
||||
runs while the runtime is in RUN mode, and by the time a host can call, the
|
||||
program that filed the definitions has ended. One
|
||||
`akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN)` after
|
||||
`akbasic_runtime_run()` makes the bodies run, and the mode stays put because
|
||||
nothing steps the runtime between calls. Issue #8 tracks making this
|
||||
unnecessary.
|
||||
3. **Revive after a script error, deliberately.** A BASIC-level error inside a
|
||||
called body reports through the sink, answers a stale value, and latches:
|
||||
the runtime leaves RUN mode and every later call does nothing. When your
|
||||
policy is to absorb the error and keep calling — a game marking one actor
|
||||
dumb rather than killing the frame — the revival is two calls:
|
||||
`akbasic_runtime_clear_error()`, then `akbasic_runtime_set_mode(RUN)` again.
|
||||
The latch is deliberate for *programs* — the first error ends a run, once,
|
||||
with one line — so nothing clears it for you.
|
||||
|
||||
Do not pass structures as per-frame arguments. A structure or pointer parameter
|
||||
spends a value-pool slot on every call and the pool never reclaims, so the
|
||||
interface dies after about a thousand calls — issue #36 has the measurements.
|
||||
Bind the instance once with `akbasic_host_bind()` and point it at each object
|
||||
with `akbasic_host_rebind()` ([Chapter 16](16-structures.md)), which spends
|
||||
nothing per call. The GALAGA tutorial ([Chapters 20](20-tutorial-galaga.md)
|
||||
and [21](21-tutorial-galaga-enemies.md)) is this whole recipe as a working
|
||||
game, forty calls a frame.
|
||||
|
||||
## Where the output goes
|
||||
|
||||
`PRINT` writes through an `akbasic_TextSink`, which is a record of function pointers plus
|
||||
|
||||
@@ -9,6 +9,7 @@ so a call with the wrong number is a syntax error rather than a surprise.
|
||||
| Function | Args | Form | What it gives |
|
||||
|---|---|---|---|
|
||||
| `ABS` | 1 | `ABS(n)` | The absolute value of an integer or float. |
|
||||
| `ASC` | 1 | `ASC(A$)` | The Unicode code point of a string's first character. |
|
||||
| `ATN` | 1 | `ATN(n)` | Arctangent, in radians. |
|
||||
| `BUMP` | 1 | `BUMP(1)` | Which sprites have collided, as a bitmask. **Reading clears it.** |
|
||||
| `CHR` | 1 | `CHR(n)` | The character for a Unicode code point, as a string. |
|
||||
@@ -29,6 +30,7 @@ so a call with the wrong number is a syntax error rather than a surprise.
|
||||
| `RGR` | 1 | `RGR(f)` | The `GRAPHIC` mode (0), the drawing surface's width (1) or height (2) in pixels, or a character cell's width (3) or height (4). |
|
||||
| `RIGHT` | 2 | `RIGHT(A$, n)` | The rightmost `n` characters. Clamped. |
|
||||
| `RMENU` | 2 | `RMENU(n, f)` | A menu's state: field 0 the highlighted entry, field 1 whether it has been confirmed. **Reading field 1 clears it.** |
|
||||
| `RND` | 1 | `RND(n)` | A random integer from 0 up to but not including `n`. |
|
||||
| `RWINDOW` | 1 | `RWINDOW(f)` | The current text window's rows (0) or columns (1). Field 2 is a C128 screen mode and is refused. |
|
||||
| `RSPCOLOR` | 1 | `RSPCOLOR(n)` | One of `SPRCOLOR`'s two shared registers, 1 or 2. |
|
||||
| `RSPHIT` | 2 | `RSPHIT(n, f)` | One of `SPRHIT`'s settings for sprite `n`, in `SPRHIT`'s own argument order: 0 the kind, 1 to 4 the two corners. |
|
||||
|
||||
@@ -743,8 +743,8 @@ way to see that something pushed a scope and never popped it.
|
||||
|
||||
```sh norun
|
||||
ctest --test-dir build --output-on-failure -R for_next # one unit test
|
||||
ctest --test-dir build --output-on-failure -R local_ # the language corpus
|
||||
./build/basic tests/language/functions.bas | diff - tests/language/functions.txt
|
||||
ctest --test-dir build --output-on-failure -R golden_ # the reference corpus
|
||||
./build/basic tests/reference/language/functions.bas | diff - tests/reference/language/functions.txt
|
||||
./tests/docs_examples.sh --root . --basic ./build/basic \
|
||||
--cflags-file build/docs_cflags.txt docs/14-architecture.md
|
||||
```
|
||||
|
||||
@@ -685,9 +685,9 @@ seconds asks for fifty frames a second.
|
||||
### Why `GOTO` rather than `DO ... LOOP`
|
||||
|
||||
A `DO ... LOOP` around the frame would read better, and it is not usable here: **a `GOTO`
|
||||
that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 12
|
||||
that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 32
|
||||
scopes, so a game that leaves its main loop once per lost life stops on the
|
||||
twelfth one:
|
||||
thirty-second one:
|
||||
|
||||
```basic
|
||||
N# = 0
|
||||
@@ -700,7 +700,7 @@ PRINT "SURVIVED " + N#
|
||||
```
|
||||
|
||||
```output
|
||||
? 3 : PARSE ERROR Environment pool exhausted at line 3 (12 in use)
|
||||
? 3 : PARSE ERROR Environment pool exhausted at line 3 (32 in use)
|
||||
|
||||
```
|
||||
|
||||
@@ -1005,20 +1005,48 @@ IF NUDGE# = 1 THEN GOSUB UNSTICK
|
||||
LABEL UNSTICK
|
||||
NUDGE# = 0
|
||||
STALL# = 0
|
||||
RMAX# = 4
|
||||
GOSUB RANDOM
|
||||
BVX# = (RND# * 3) - 6
|
||||
BVX# = (RND(4) * 3) - 6
|
||||
IF BVX# = 0 THEN BVX# = 3
|
||||
RETURN
|
||||
```
|
||||
|
||||
### You have to write your own random numbers
|
||||
### Random numbers are built in
|
||||
|
||||
**There is no `RND` in this dialect**, and no `INT`, `SQR`, `ASC` or `TIMER` either. A
|
||||
linear congruential generator is nine tokens and does the job. Put the number of possible
|
||||
answers in `RMAX#` and read the result from `RND#`:
|
||||
There is no `INT`, `SQR` or `TIMER` in this dialect, but
|
||||
`RND(n)` returns an integer from zero through `n - 1`. It seeds itself
|
||||
from the host clock the first time it is called, so a program only needs the bound:
|
||||
|
||||
```basic
|
||||
I# = 0
|
||||
FOR I# = 1 TO 5
|
||||
PRINT "ROLL " + (RND(6) + 1)
|
||||
NEXT I#
|
||||
END
|
||||
```
|
||||
|
||||
Use `RND` for the serve, too, so the ball does not always leave in the same direction:
|
||||
|
||||
```basic norun
|
||||
LABEL SERVE
|
||||
PX# = (SCW# - PW#) / 2
|
||||
HELD# = 1
|
||||
BX# = PX# + ((PW# / 2) - 4)
|
||||
BY# = PY# - 10
|
||||
BVX# = BSPD#
|
||||
IF RND(2) = 0 THEN BVX# = 0 - BSPD#
|
||||
BVY# = 0 - BSPD#
|
||||
PDEC# = 0
|
||||
GOSUB SHOWSPR
|
||||
RETURN
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Historical aside: the LCG this chapter used to teach</summary>
|
||||
|
||||
Before `RND` existed, this nine-token linear congruential generator was copied into
|
||||
every program. It remains a useful from-scratch PRNG example:
|
||||
|
||||
```basic norun
|
||||
SEED# = 12345
|
||||
RMAX# = 6
|
||||
RND# = 0
|
||||
@@ -1035,43 +1063,11 @@ RND# = MOD((SEED# / 65536), RMAX#)
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
ROLL 1
|
||||
ROLL 5
|
||||
ROLL 2
|
||||
ROLL 1
|
||||
ROLL 2
|
||||
```
|
||||
The multiplication stays inside a 64-bit integer for any seed below 2147483648. The
|
||||
answer is taken from the middle bits because the low bits of a power-of-two modulus
|
||||
barely change from one call to the next. This used to be required; it is now built in.
|
||||
|
||||
The multiplication stays inside a 64-bit integer for any seed below 2147483648, which is
|
||||
why the modulus is that number. The answer is taken from the middle bits — `SEED# / 65536`
|
||||
— because the low bits of a power-of-two modulus barely change from one call to the next.
|
||||
Integer division truncating for free is the `INT` you do not have.
|
||||
|
||||
Seed it from the clock at startup. `TI#` is the host's uptime in sixtieths of a second,
|
||||
which is different every time the game is run:
|
||||
|
||||
```basic norun
|
||||
SEED# = TI#
|
||||
```
|
||||
|
||||
Use `RANDOM` for the serve, too, so the ball does not always leave in the same direction:
|
||||
|
||||
```basic norun
|
||||
LABEL SERVE
|
||||
PX# = (SCW# - PW#) / 2
|
||||