Compare commits
9 Commits
deps-updat
...
5c815345f4
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c815345f4 | |||
|
dd10dc143a
|
|||
|
54ab85a276
|
|||
|
d5a0edd692
|
|||
|
47c6be58c5
|
|||
|
27837aeabc
|
|||
|
743e610f8f
|
|||
|
dde1d91c6e
|
|||
|
17af2d406c
|
@@ -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.
|
||||
#
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,14 +51,12 @@ option(AKBASIC_SANITIZE "Build with ASan + UBSan" OFF
|
||||
# through: the dependencies set target and directory properties that their own
|
||||
# builds depend on.
|
||||
#
|
||||
# All three dependencies now namespace both their `coverage` and their
|
||||
# `mutation` targets when embedded, so there is no custom-target collision left
|
||||
# to work around. libakerror was the last holdout -- it namespaced `mutation`
|
||||
# but not `coverage`, and a coverage build collided on the bare name and failed
|
||||
# to configure at all. 2.0.2 applies the same CMAKE_SOURCE_DIR test to both
|
||||
# (deps/libakerror/CMakeLists.txt:429-434), closing libakerror issue #15, and
|
||||
# the add_custom_target() shadow that renamed it on the way past is gone with
|
||||
# this comment.
|
||||
# libakerror additionally namespaces its `mutation` target when embedded but not
|
||||
# its `coverage` target (deps/libakerror/CMakeLists.txt:194 vs :172), so a
|
||||
# coverage build collides on the `coverage` target and fails to configure at all.
|
||||
# Rename the dependency's on the way past. Remove this once libakerror applies
|
||||
# the same CMAKE_SOURCE_DIR test to `coverage` that it already applies to
|
||||
# `mutation` -- filed as libakerror issue #15.
|
||||
#
|
||||
# **Only one project in a tree may shadow add_test(), and this is that project.**
|
||||
# CMake exposes an overridden command as `_name` and chains exactly one level: a
|
||||
@@ -88,6 +86,14 @@ function(set_property _scope)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(add_custom_target _name)
|
||||
if(AKBASIC_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage")
|
||||
_add_custom_target(akerror_coverage ${ARGN})
|
||||
else()
|
||||
_add_custom_target(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
|
||||
if(AKBASIC_WITH_AKGL)
|
||||
@@ -505,7 +511,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
|
||||
@@ -588,20 +594,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
|
||||
|
||||
132
MAINTENANCE.md
132
MAINTENANCE.md
@@ -68,19 +68,12 @@ the `akgl_*` or `aksl_*` entry point should look like, and what tests would cove
|
||||
decision, which is true of *changing* it and not of *reporting* it. Follow the prose-paragraph style of the entries already
|
||||
there. Growing the dependency to serve the interpreter is a wanted outcome, not a detour.
|
||||
|
||||
It works. **Five** gaps were filed this way and all five landed upstream: text measurement,
|
||||
immediate-mode drawing, audio and a non-blocking keystroke read became `akgl_text_measure`,
|
||||
the `akgl_draw_*` family, `akgl_audio_*` and `akgl_controller_poll_key`; and the
|
||||
directory-reading wrapper `DIRECTORY` was waiting on became `aksl_opendir`, `aksl_readdir`,
|
||||
`aksl_closedir` and `aksl_rewinddir` — `libakstdlib` issue #10, in the revision this tree
|
||||
pins.
|
||||
|
||||
That leaves the two refusals in different positions, and the difference is worth keeping
|
||||
straight. **`FILTER` is the one verb still blocked on a gap** — there is no filter stage in
|
||||
`akgl_audio_*` to configure. **`DIRECTORY` is no longer blocked on anything**; it is simply
|
||||
unwritten, and its refusal says so rather than naming a wrapper that now exists. Both refuse
|
||||
at execution and say so, rather than being silently ignored: a program that asks for a
|
||||
low-pass filter and gets an unfiltered square wave has been lied to.
|
||||
It works. Four gaps were filed this way — text measurement, immediate-mode drawing, audio,
|
||||
and a non-blocking keystroke read — and all four landed upstream as `akgl_text_measure`, the
|
||||
`akgl_draw_*` family, `akgl_audio_*` and `akgl_controller_poll_key`. `FILTER` is the one verb
|
||||
still blocked on a gap, and `DIRECTORY` is refused pending an `opendir`/`readdir` wrapper in
|
||||
`libakstdlib`. Both refuse at execution and say so, rather than being silently ignored: a
|
||||
program that asks for a low-pass filter and gets an unfiltered square wave has been lied to.
|
||||
|
||||
### The Go reference
|
||||
|
||||
@@ -95,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
|
||||
@@ -163,10 +156,10 @@ guards every dependency with `if(NOT TARGET ...)`, so a top-level build must def
|
||||
`akerror::akerror` and `akstdlib::akstdlib` from `deps/libakerror` and `deps/libakstdlib`
|
||||
**before** `add_subdirectory(deps/libakgl)`, or the targets are declared twice.
|
||||
|
||||
That order is load-bearing for a second reason: `deps/libakerror` is at **2.0.2**, whose 2.0.0
|
||||
That order is load-bearing for a second reason: `deps/libakerror` is at **2.0.1**, whose 2.0.0
|
||||
was a source and ABI break carrying an soname (`libakerror.so.2`). `libakstdlib` and `libakgl`
|
||||
must be compiled against that header, not a 1.x one, and an installed `libakerror.so.1` must
|
||||
not be picked up. The break is quiet if you get it wrong: the context behind `IGNORE` became
|
||||
not be picked up. The break is quiet if you get it wrong: `__akerr_last_ignored` became
|
||||
thread-local and `akerr_next_error()` now returns a context that already holds a reference, so
|
||||
a mixed build leaks pool slots or frees one twice rather than failing to link.
|
||||
|
||||
@@ -174,47 +167,10 @@ a mixed build leaks pool slots or frees one twice rather than failing to link.
|
||||
|
||||
| Submodule | Version | soname | ABI rule | Version API |
|
||||
|---|---|---|---|---|
|
||||
| `deps/libakerror` | 2.0.2 | `libakerror.so.2` | major only | **none** — no version macro; `include/akbasic/error.h` feature-tests `AKERR_THREAD_SAFE` and `AKERR_EXIT_STATUS_UNREPRESENTABLE` instead |
|
||||
| `deps/libakerror` | 2.0.1 | `libakerror.so.2` | major only | **none** — no version macro; `include/akbasic/error.h` feature-tests `AKERR_THREAD_SAFE` and `AKERR_EXIT_STATUS_UNREPRESENTABLE` instead |
|
||||
| `deps/libakstdlib` | 0.2.0 | `libakstdlib.so.0.2` | **`MAJOR.MINOR` while major is 0** | `AKSL_VERSION_*`, `aksl_version()`, `AKSL_VERSION_CHECK()` |
|
||||
| `deps/libakgl` | 0.9.0 | `libakgl.so.0.9` | **`MAJOR.MINOR` while major is 0** | `AKGL_VERSION*`, `akgl_version()`, `AKGL_VERSION_AT_LEAST()` |
|
||||
|
||||
**The `libakerror` row is what `project(akerror VERSION ...)` declares, and it disagrees with
|
||||
that library's own release notes**, which carry a "Release 2.0.3" section describing CI-only
|
||||
changes. The version the build stamps into the soname and into
|
||||
`akerrorConfigVersion.cmake` is 2.0.2; nothing consumers can observe says 2.0.3. Read the
|
||||
table as the ABI and the notes as the changelog, and do not "correct" this row to 2.0.3
|
||||
without the upstream `project()` bump to go with it. Reported as `libakerror` issue #38.
|
||||
|
||||
#### What 2.0.2's `IGNORE` change costs, measured
|
||||
|
||||
`IGNORE` now takes a *copy* of the context so the pool slot can be released, which fixes a
|
||||
real leak. The copy lives in the public header as a file-scope `static`:
|
||||
|
||||
```c norun
|
||||
static AKERR_THREAD_LOCAL akerr_ErrorContext akerr_last_ignored;
|
||||
```
|
||||
|
||||
`static` means one per translation unit, and `AKERR_THREAD_LOCAL` means each one is
|
||||
thread-local. `sizeof(akerr_ErrorContext)` is **37,296 bytes**, so:
|
||||
|
||||
| Build | TLS segment of `build/basic` |
|
||||
|---|---|
|
||||
| libakerror 2.0.1 | none at all |
|
||||
| libakerror 2.0.2 | 1,417,248 bytes — **1.35 MiB**, per thread |
|
||||
|
||||
38 copies × 37,296 accounts for the segment exactly; `readelf -sW build/basic` shows all 38.
|
||||
It scales with translation-unit count, not with anything about the program, and every thread
|
||||
a host game spawns pays it again — which is worth knowing in a tree that spent a release
|
||||
taking `akbasic_Runtime` from 10.75 MiB to 2.40 MiB.
|
||||
|
||||
It also emits **84 `-Wunused-variable` warnings**, one per TU that includes `akerror.h`
|
||||
without using `IGNORE`. Nothing here builds with `-Werror`, so it is noise rather than a
|
||||
failure — but a consumer that does cannot include the header.
|
||||
|
||||
**Both are `libakerror` issue #37 and neither is worked around here**, per the rule above: a
|
||||
one-line `extern` in the header and a definition in `src/error.c` fixes it upstream, and
|
||||
patching our copy would fork the submodule. Delete this subsection when it lands.
|
||||
|
||||
For both 0.x libraries the soname carries `MAJOR.MINOR` deliberately: 0.1 and 0.2 are
|
||||
*different* ABIs, and both become major-only at 1.0. Do not read `0.1 → 0.2` as a compatible
|
||||
bump — both libraries have actually made that jump, so anything built against the 0.1 headers
|
||||
@@ -240,32 +196,16 @@ precedes the build tree on the include path and a stray copy there would shadow
|
||||
one and pin every consumer. It publishes `AKGL_VERSION_AT_LEAST(major, minor, patch)` — the
|
||||
compile-time test `libakstdlib` could not write against `libakerror` — and `akgl_version()`.
|
||||
|
||||
**Version-pinning in `find_package` used to be asymmetric. It no longer is.**
|
||||
**Version-pinning in `find_package` is asymmetric, and that is deliberate.**
|
||||
`find_package(akstdlib 0.1)` and `find_package(akgl 0.1)` both work; each ships a
|
||||
`ConfigVersion.cmake` at `SameMinorVersion`, mirroring its soname. `libakerror` shipped
|
||||
`akerrorConfig.cmake` and `akerrorTargets.cmake` but no `akerrorConfigVersion.cmake`, so any
|
||||
versioned request failed against a correct install and the advice here was to ask for
|
||||
`akerror` unversioned. That was `libakerror` issue #16 — closed — and `libakstdlib` issue #5,
|
||||
which tracks the same fix from the other side and is still open only because nobody has shut
|
||||
it. It has landed: `libakerror` now writes `akerrorConfigVersion.cmake` at
|
||||
**`SameMajorVersion`**, matching the soname's major-only rule, rather than the
|
||||
`SameMinorVersion` the other two use to match theirs.
|
||||
|
||||
Two things follow, and the second is the one that bites:
|
||||
|
||||
- **A versioned request now works** — but the floor to ask for is **`2.0`**, not the `1.0`
|
||||
this file used to say. `find_package(akerror 1.0)` fails *harder* than before: it is a
|
||||
request for major 1 against a major-2 install, which `SameMajorVersion` correctly rejects.
|
||||
- **There is nothing in this repository to change.** `akbasic` reaches all three dependencies
|
||||
by `add_subdirectory`, not `find_package`, and ships no CMake package config of its own —
|
||||
so it has no `find_dependency` calls to add a floor to. The instruction that used to live
|
||||
here was written for a consumer this project never became. It matters to anyone *installing*
|
||||
these libraries and linking `akbasic` against the installed copies, which is why it is
|
||||
recorded rather than deleted.
|
||||
|
||||
The compile-time floor is unchanged and still the real guard: an `#error` feature-testing
|
||||
`AKERR_FIRST_CONSUMER_STATUS`, which `akstdlib.h`, `akgl/error.h` and our own
|
||||
`include/akbasic/error.h` all carry — include any of them and you inherit it.
|
||||
`ConfigVersion.cmake` at `SameMinorVersion`, mirroring its soname. `find_package(akerror 1.0)`
|
||||
**fails against a correct install**, because `libakerror` ships `akerrorConfig.cmake` and
|
||||
`akerrorTargets.cmake` but no `akerrorConfigVersion.cmake`. Ask for `akerror` unversioned. Its
|
||||
floor is enforced instead by an `#error` feature-testing `AKERR_FIRST_CONSUMER_STATUS`, which
|
||||
`akstdlib.h`, `akgl/error.h` and our own `include/akbasic/error.h` all carry — include any of
|
||||
them and you inherit the guard. The missing version file is filed in
|
||||
`libakstdlib` issue #5 and `libakerror` issue #16; when it lands, add the `1.0` floor to the `find_dependency`
|
||||
calls.
|
||||
|
||||
### Embedding all three dependencies collides four ways
|
||||
|
||||
@@ -310,19 +250,14 @@ CTest names. `libakstdlib` still uses bare `test_<name>` targets. **Name every t
|
||||
this repo `akbasic_test_<name>`** — it costs nothing and it is the collision that actually
|
||||
stopped a build.
|
||||
|
||||
**3. Duplicate custom targets — fixed upstream, and the workaround is gone.** `libakerror`
|
||||
used to namespace its `mutation` target when embedded but **not** its `coverage` target, so
|
||||
any coverage-enabled top-level build failed with *"another target with the same name already
|
||||
exists"*. This project shadowed `add_custom_target` and renamed that one to
|
||||
`akerror_coverage` on the way past, and recorded the real fix as `libakerror` issue #15: the
|
||||
same `CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test it already applied to
|
||||
`mutation`.
|
||||
|
||||
That landed in 2.0.2. `libakerror` now picks `akerror_coverage` itself when embedded, so the
|
||||
shadow was dead code that could only ever fire on a name the dependency had stopped using —
|
||||
and it is deleted. All three dependencies namespace both targets correctly now, so **there is
|
||||
no custom-target collision left**; only collisions 1, 2 and 4 below are live. The heading says
|
||||
four because four is what a reader coming from the issue tracker will be looking for.
|
||||
**3. Duplicate custom targets.** `libakerror` namespaces its `mutation` target when embedded
|
||||
but **not** its `coverage` target, so any coverage-enabled top-level build fails with *"another
|
||||
target with the same name already exists"*. We shadow `add_custom_target` and rename that one
|
||||
to `akerror_coverage` on the way past. `libakstdlib` (both targets) and `libakgl` (its
|
||||
`mutation` target) namespace themselves correctly. **The real fix is upstream in
|
||||
`libakerror`** — the same `CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test it already
|
||||
applies to `mutation` — and it is filed as `libakerror` issue #15. Delete the
|
||||
workaround when it lands.
|
||||
|
||||
**4. Stale build trees poison the coverage report.** See below; it is the reason for
|
||||
`cmake -S . -B build`.
|
||||
@@ -416,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
|
||||
|
||||
|
||||
11
README.md
11
README.md
@@ -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
|
||||
@@ -129,7 +129,7 @@ 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 |
|
||||
| [`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`.
|
||||
|
||||
@@ -138,13 +138,12 @@ API documentation builds with `doxygen Doxyfile`, into `build/docs/html`.
|
||||
Everything is a submodule; `git submodule update --init --recursive` gets all of it. There is
|
||||
nothing to install first.
|
||||
|
||||
* [libakerror](https://source.starfort.tech/andrew/libakerror) 2.0.2 — TRY/CATCH-style error
|
||||
* [libakerror](https://source.starfort.tech/andrew/libakerror) 2.0.1 — TRY/CATCH-style error
|
||||
contexts. Every function that can fail returns one. 2.0.0 made it thread safe and broke the
|
||||
ABI; anything built against a 1.x header must be rebuilt rather than relinked.
|
||||
* [libakstdlib](https://source.starfort.tech/andrew/libakstdlib) 0.2.0 — libc wrappers that
|
||||
report through `libakerror`. String-to-number conversion goes straight to it, which is why
|
||||
`VAL("garbage")` is an error rather than a silent `0`. Its public header now also pulls in
|
||||
`<dirent.h>` and `<sys/stat.h>` for the directory and file-metadata wrappers.
|
||||
`VAL("garbage")` is an error rather than a silent `0`.
|
||||
* [libakgl](https://source.starfort.tech/andrew/libakgl) 0.9.0 — **optional**, only for
|
||||
`-DAKBASIC_WITH_AKGL=ON`. Pulls in SDL3. Its soname carries `MAJOR.MINOR` while the major is 0,
|
||||
so rebuild rather than relink.
|
||||
|
||||
43
TODO.md
43
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) |
|
||||
@@ -2339,9 +2340,9 @@ Dependency baseline:
|
||||
|
||||
| Submodule | Version | Notes |
|
||||
|---|---|---|
|
||||
| `deps/libakerror` | 2.0.2 | Private ownership-enforced status registry. akbasic reserves 512–767 in `akbasic_error_register()`. **2.0.0 is thread safe and an ABI break** (`libakerror.so.2`): the `IGNORE` context is thread-local and `akerr_next_error()` returns an owned reference, neither of which fails to link when mismatched. **2.0.1 fixes an exit status that mattered more to this band than to any other** — see below. **2.0.2** namespaces its embedded `coverage` target (issue #15 — our `add_custom_target` shadow is deleted), installs `akerrorConfigVersion.cmake` at `SameMajorVersion` (issue #16), fixes the `AKERR_USE_STDLIB=OFF` build, retires `PATH_MAX` for `AKERR_MAX_ERROR_FNAME_LENGTH`, and turns the `IGNORE` slot into a released copy — `__akerr_last_ignored` was an `extern` pointer and is now a per-TU `static akerr_last_ignored`. **That last one costs us 1.35 MiB of thread-local storage**: 38 translation units x 37,296 bytes, measured as the whole TLS segment of `build/basic`, where 2.0.1 produced no TLS segment at all. Filed upstream as libakerror issue #37, along with the 84 `-Wunused-variable` warnings it emits. Its `UPGRADING.md` describes a 2.0.3 that `project()` never stamped — libakerror issue #38, which is why the version above reads 2.0.2. |
|
||||
| `deps/libakstdlib` | 0.2.0 | soname `libakstdlib.so.0.2`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. This release fixed all six confirmed defects the port was working around — see §1.9, where the bans are now lifted. Since then, and with no version bump: **directory wrappers landed** (`aksl_opendir`/`readdir`/`closedir`/`rewinddir`, issue #10) — the gap `DIRECTORY` was refused for, so that refusal now says only that the verb is unwritten; file-metadata wrappers landed; and `aksl_snprintf` keeps its `int *count` but now reports the *required* length on truncation rather than 0. Only read on success here, so nothing moved. |
|
||||
| `deps/libakgl` | 0.9.0 | soname `libakgl.so.0.9`. Owns status codes 256–262. Linked and tested under `-DAKBASIC_WITH_AKGL=ON`, which still defaults OFF so the core library and its whole suite build on a machine with no SDL. **0.3.0 closed every API gap this port had filed** — see §7 — so the four workarounds §3 used to list are gone. 0.4.0 was a leak-and-overread release that changed no public struct. **0.5.0 is the first that broke our source as well as our ABI**: it namespaced every exported symbol, so `akgl_render_bind2d` is `akgl_render_2d_bind`, `akgl_sprite_sheet_coords_for_frame` is `akgl_spritesheet_coords_for_frame`, the `renderer`/`camera`/`window` globals carry the prefix, and `_akgl_renderer`/`_akgl_camera` are `akgl_default_renderer`/`akgl_default_camera`. `include/akbasic/akgl.h` asserts the floor. **0.6.0 and 0.7.0 broke nothing here** — 0.6.0 is three arcade-physics fixes and a `physics.max_timestep` property this port does not use, and 0.7.0 reports failures `libakstdlib`'s wrappers were already catching and takes `libakerror` 2.0.1. **0.8.0 and 0.9.0 broke nothing here either** — 0.8.0 is the collision subsystem (`AKGL_ERR_COLLISION`, code 261, and the vendored `libccd` and `tg` submodules that come with it), and 0.9.0 is the `akgl_ui` subsystem (`AKGL_ERR_UI`, code 262, and vendored `clay`), which is what `src/ui_akgl.c` draws through. Both widen libakgl's reserved band from five codes to seven; the range map in `MAINTENANCE.md` carries it. The floor moved anyway, because deciding for ourselves which of libakgl's minor releases were really compatible is the judgement the soname exists to take away. **Note `deps/libakgl/deps/` pins its own `libakerror` and `libakstdlib` older than ours**; the top-level build declares both targets first and libakgl's `if(NOT TARGET ...)` guards mean its copies are never configured, so what libakgl actually compiles against is what this tree pins. |
|
||||
| `deps/libakerror` | 2.0.1 | Private ownership-enforced status registry. akbasic reserves 512–767 in `akbasic_error_register()`. Does not namespace its `coverage` target when embedded — worked around in our `CMakeLists.txt`. **2.0.0 is thread safe and an ABI break** (`libakerror.so.2`): `__akerr_last_ignored` is thread-local and `akerr_next_error()` returns an owned reference, neither of which fails to link when mismatched. **2.0.1 fixes an exit status that mattered more to this band than to any other** — see below. |
|
||||
| `deps/libakstdlib` | 0.2.0 | soname `libakstdlib.so.0.2`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. This release fixed all six confirmed defects the port was working around — see §1.9, where the bans are now lifted. |
|
||||
| `deps/libakgl` | 0.7.0 | soname `libakgl.so.0.7`. Owns status codes 256–260. Linked and tested under `-DAKBASIC_WITH_AKGL=ON`, which still defaults OFF so the core library and its whole suite build on a machine with no SDL. **0.3.0 closed every API gap this port had filed** — see §7 — so the four workarounds §3 used to list are gone. 0.4.0 was a leak-and-overread release that changed no public struct. **0.5.0 is the first that broke our source as well as our ABI**: it namespaced every exported symbol, so `akgl_render_bind2d` is `akgl_render_2d_bind`, `akgl_sprite_sheet_coords_for_frame` is `akgl_spritesheet_coords_for_frame`, the `renderer`/`camera`/`window` globals carry the prefix, and `_akgl_renderer`/`_akgl_camera` are `akgl_default_renderer`/`akgl_default_camera`. `include/akbasic/akgl.h` asserts the floor. **0.6.0 and 0.7.0 broke nothing here** — 0.6.0 is three arcade-physics fixes and a `physics.max_timestep` property this port does not use, and 0.7.0 reports failures `libakstdlib`'s wrappers were already catching and takes `libakerror` 2.0.1. The floor moved anyway, because deciding for ourselves which of libakgl's minor releases were really compatible is the judgement the soname exists to take away. |
|
||||
|
||||
**An unhandled error in this band used to exit zero, and 512 is the worst possible base for
|
||||
that.** `libakerror`'s default unhandled-error handler ended in `exit(errctx->status)`, and a
|
||||
@@ -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
|
||||
|
||||
2
deps/libakerror
vendored
2
deps/libakerror
vendored
Submodule deps/libakerror updated: ee38eed31c...5eaa956f50
2
deps/libakgl
vendored
2
deps/libakgl
vendored
Submodule deps/libakgl updated: 77091ed177...bbb7b8f494
2
deps/libakstdlib
vendored
2
deps/libakstdlib
vendored
Submodule deps/libakstdlib updated: 821200e618...2b79aca103
@@ -126,7 +126,7 @@ exists, and reading into one you never sized writes over something else.
|
||||
| `COLLECT` | validates a disk's block allocation map. There is no map |
|
||||
| `BACKUP` | duplicates one disk onto another. There are no disks |
|
||||
| `BOOT` | loads and runs a boot sector. There is no boot sector |
|
||||
| `DIRECTORY` / `CATALOG` | not written yet. It was blocked on a directory-reading wrapper in the standard library; that landed, so only the verb is outstanding |
|
||||
| `DIRECTORY` / `CATALOG` | needs a directory-reading wrapper the standard library does not have yet. Filed upstream |
|
||||
|
||||
`DCLEAR` is the exception among the drive verbs: resetting a drive also closes its
|
||||
channels, and closing the channels is real, so that is what it does.
|
||||
|
||||
@@ -35,7 +35,7 @@ for the reasoning in each case.
|
||||
| `DIALOG` | `DIALOG ["text"]` | Show a text panel across the bottom of the screen. No argument takes it down. See Chapter 19. |
|
||||
| `DIM` | `DIM A#(n [,...])` | Make an array. Subscripts start at zero; `n` is the count. |
|
||||
| `DIM` … `AS` | `DIM S@ AS T`, `DIM P@ AS PTR TO T` | Make a structure, or a strict pointer to one. See Chapter 16. |
|
||||
| `DIRECTORY` | `DIRECTORY` | **Refused.** Not written yet; the standard-library wrapper it waited on has landed. |
|
||||
| `DIRECTORY` | `DIRECTORY` | **Refused.** Needs a directory-reading wrapper that does not exist yet. |
|
||||
| `DLOAD` | `DLOAD "name"` | Load a program from a file. |
|
||||
| `DO` | `DO [WHILE c | UNTIL c]` | Start a loop. The condition may be here, on the `LOOP`, or neither. |
|
||||
| `DOPEN` | `DOPEN n, "name" [,W]` | Open a file on channel `n`. `W` opens it for writing. |
|
||||
|
||||
@@ -199,8 +199,7 @@ interpreter's error code, which bears no relation to a Commodore error number. P
|
||||
- **`BLOAD` requires a length.**
|
||||
- **`HEADER`, `COLLECT`, `BACKUP` and `BOOT` are refused.** They operate on a physical
|
||||
disk.
|
||||
- **`DIRECTORY` is refused** because it is not written yet. The standard-library
|
||||
wrapper it was waiting on has landed, so the remaining work is the verb.
|
||||
- **`DIRECTORY` is refused** pending a wrapper in the standard library.
|
||||
|
||||
## Machine
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -1049,17 +1049,17 @@ every program. It remains a useful from-scratch PRNG example:
|
||||
```basic norun
|
||||
SEED# = 12345
|
||||
RMAX# = 6
|
||||
ROLL# = 0
|
||||
RND# = 0
|
||||
I# = 0
|
||||
FOR I# = 1 TO 5
|
||||
GOSUB RANDOM
|
||||
PRINT "ROLL " + (ROLL# + 1)
|
||||
PRINT "ROLL " + (RND# + 1)
|
||||
NEXT I#
|
||||
END
|
||||
|
||||
LABEL RANDOM
|
||||
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
||||
ROLL# = MOD((SEED# / 65536), RMAX#)
|
||||
RND# = MOD((SEED# / 65536), RMAX#)
|
||||
RETURN
|
||||
```
|
||||
|
||||
|
||||
@@ -1423,8 +1423,7 @@ IF STATE# = 2 THEN GOSUB UNSTICK
|
||||
RETURN
|
||||
|
||||
LABEL PRESSPAUSE
|
||||
IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED"
|
||||
IF STATE# = 2 THEN BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN
|
||||
IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN
|
||||
IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER
|
||||
RETURN
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ whole development loop; the engine never rebuilds.
|
||||
- **[Step 2](#step-2-bind-the-engines-own-actor)** — bind the engine's own
|
||||
actor as the second type, which is the point of the whole exercise
|
||||
- **[Step 3](#step-3-share-the-frame-and-the-dice)** — share the frame state,
|
||||
and hand the script dice the engine controls
|
||||
and give the script randomness it cannot make itself
|
||||
- **[Step 4](#step-4-why-bindings-and-not-arguments)** — see why the structures
|
||||
are bindings rather than function arguments
|
||||
- **[Step 5](#step-5-the-shape-of-the-script)** — learn the three language
|
||||
@@ -75,7 +75,7 @@ typedef struct galaga_Enemy
|
||||
float t; /* parametric clock for the current maneuver */
|
||||
int32_t hp;
|
||||
int32_t fire; /* outbox: script sets 1, engine consumes */
|
||||
float rnd; /* inbox: fresh 0..1 each call; ROLL% in BASIC */
|
||||
float rnd; /* inbox: engine writes fresh 0..1 each call */
|
||||
} galaga_Enemy;
|
||||
```
|
||||
|
||||
@@ -107,7 +107,7 @@ static const akbasic_HostField ENEMY_FIELDS[] = {
|
||||
AKBASIC_HOST_FIELD( galaga_Enemy, t, "T%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||
AKBASIC_HOST_FIELD( galaga_Enemy, hp, "HP#", AKBASIC_HOSTFIELD_INT32 ),
|
||||
AKBASIC_HOST_FIELD( galaga_Enemy, fire, "FIRE#", AKBASIC_HOSTFIELD_INT32 ),
|
||||
AKBASIC_HOST_FIELD( galaga_Enemy, rnd, "ROLL%", AKBASIC_HOSTFIELD_FLOAT )
|
||||
AKBASIC_HOST_FIELD( galaga_Enemy, rnd, "RND%", AKBASIC_HOSTFIELD_FLOAT )
|
||||
};
|
||||
static const akbasic_HostType ENEMY_TYPE = {
|
||||
"ENEMY", sizeof(galaga_Enemy), ENEMY_FIELDS, 8
|
||||
@@ -194,7 +194,7 @@ typedef struct galaga_Shared
|
||||
float playerx; /* the player actor's position, this frame */
|
||||
float playery;
|
||||
int32_t wave;
|
||||
float rnd; /* fresh 0..1 each frame; ROLL% to the script */
|
||||
float rnd; /* fresh 0..1 each frame; the issue #16 route */
|
||||
} galaga_Shared;
|
||||
```
|
||||
|
||||
@@ -203,24 +203,13 @@ the engine refreshes it at the top of every frame. The boss reads
|
||||
`GAME@.PLAYERX%` to lead its dive; the fire decision reads it to know whether
|
||||
anything is worth shooting at.
|
||||
|
||||
The `rnd` fields — one here per frame, one on each enemy per call — carry the
|
||||
engine's PRNG into the script: write `SELF@.ROLL% < DT% * 1.5` and an enemy's
|
||||
trigger finger is a dice roll.
|
||||
|
||||
The dialect does now have a native `RND` function — issue #16 closed, and
|
||||
[Chapter 12](12-function-reference.md) documents it — so this is no longer the
|
||||
*only* route; Chapter 17's breakout hand-rolls a linear congruential generator
|
||||
in BASIC as a third. The engine keeps filling the field here on purpose,
|
||||
because it buys something `RND` cannot: the numbers come from the example's own
|
||||
PRNG rather than libc's, so a headless run is the same game on every machine,
|
||||
which is what makes `example_galaga` a test and not just a demo.
|
||||
|
||||
**The BASIC name is `ROLL%`, not `RND%`.** A host field is a bare word and
|
||||
shares a namespace with every verb and function, so once `RND` became a
|
||||
function name a field could no longer be called that — the scanner refuses it
|
||||
with *"Reserved word in variable name"*. The C member stays `rnd`; only the
|
||||
name the script sees had to move. [Chapter 16](16-structures.md) has the same
|
||||
rule for `TYPE` declarations.
|
||||
The `rnd` fields — one here per frame, one on each enemy per call — exist
|
||||
because the engine's PRNG is the script's **only** source of randomness: write
|
||||
`SELF@.RND% < DT% * 1.5` and an enemy's trigger finger is a dice roll. There
|
||||
is no `RND` verb in this dialect; issue #16 tracks adding one, and Chapter
|
||||
17's breakout hand-rolls a linear congruential generator in BASIC as the other
|
||||
route. Here the engine fills the field, which also keeps a headless run the
|
||||
same game on every machine — the PRNG is the example's own, not libc's.
|
||||
|
||||
## Step 4: Why bindings, and not arguments
|
||||
|
||||
@@ -362,7 +351,7 @@ DEF DECIDEFIRE(DT%)
|
||||
DX% = GAME@.PLAYERX% - ACTOR@.X%
|
||||
IF ABS(DX%) > 140 THEN RETURN 0
|
||||
IF ACTOR@.Y% > GAME@.PLAYERY% THEN RETURN 0
|
||||
IF SELF@.ROLL% < DT% * 1.5 THEN SELF@.FIRE# = 1
|
||||
IF SELF@.RND% < DT% * 1.5 THEN SELF@.FIRE# = 1
|
||||
RETURN 0
|
||||
END
|
||||
```
|
||||
@@ -397,7 +386,7 @@ DEF UPDATEBEE(DT%)
|
||||
IF (S# AND 2) > 0 THEN BEGIN
|
||||
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 1.7) * 16
|
||||
ACTOR@.Y% = SELF@.HOMEY%
|
||||
IF SELF@.ROLL% < DT% * 0.04 THEN SELF@.STATE# = 4 : SELF@.T% = 0
|
||||
IF SELF@.RND% < DT% * 0.04 THEN SELF@.STATE# = 4 : SELF@.T% = 0
|
||||
BEND
|
||||
IF (S# AND 4) > 0 THEN BEGIN
|
||||
R# = DIVESTEP(DT%, 130, 0.2)
|
||||
|
||||
@@ -145,7 +145,7 @@ RX# = 0
|
||||
N# = 0
|
||||
MROW# = 0
|
||||
RMAX# = 2
|
||||
ROLL# = 0
|
||||
RND# = 0
|
||||
SND# = 0
|
||||
P$ = ""
|
||||
H$ = ""
|
||||
@@ -172,8 +172,7 @@ BSG$(4) = "[--]"
|
||||
BSG$(5) = "[--]"
|
||||
|
||||
REM --- the seed -------------------------------------------------------
|
||||
REM RND exists now, but this program keeps its own LCG so a headless run
|
||||
REM is the same game every time. TI# is jiffies off the host's clock
|
||||
REM There is no RND in this dialect. TI# is jiffies off the host's clock
|
||||
REM and is host uptime rather than zero-based, which makes it a fine seed.
|
||||
SEED# = TI#
|
||||
|
||||
@@ -390,18 +389,17 @@ BY# = PY# - 10
|
||||
RMAX# = 2
|
||||
GOSUB RANDOM
|
||||
BVX# = BSPD#
|
||||
IF ROLL# = 0 THEN BVX# = 0 - BSPD#
|
||||
IF RND# = 0 THEN BVX# = 0 - BSPD#
|
||||
BVY# = 0 - BSPD#
|
||||
PDEC# = 0
|
||||
GOSUB SHOWSPR
|
||||
RETURN
|
||||
|
||||
REM A linear congruential generator. RND(n) would do this in one token
|
||||
REM now; the LCG stays because its sequence is reproducible.
|
||||
REM A linear congruential generator, because this dialect has no RND.
|
||||
REM The multiply stays inside int64 for any seed under 2^31.
|
||||
LABEL RANDOM
|
||||
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
||||
ROLL# = MOD((SEED# / 65536), RMAX#)
|
||||
RND# = MOD((SEED# / 65536), RMAX#)
|
||||
RETURN
|
||||
|
||||
REM ####################################################################
|
||||
@@ -634,7 +632,7 @@ NUDGE# = 0
|
||||
STALL# = 0
|
||||
RMAX# = 4
|
||||
GOSUB RANDOM
|
||||
BVX# = (ROLL# * 3) - 6
|
||||
BVX# = (RND# * 3) - 6
|
||||
IF BVX# = 0 THEN BVX# = 3
|
||||
RETURN
|
||||
|
||||
@@ -643,7 +641,7 @@ REM like something with a hand on the paddle rather than a mirror.
|
||||
LABEL DEMOAIM
|
||||
RMAX# = 81
|
||||
GOSUB RANDOM
|
||||
DOFF# = ROLL# - 40
|
||||
DOFF# = RND# - 40
|
||||
RETURN
|
||||
|
||||
REM ####################################################################
|
||||
|
||||
@@ -241,8 +241,7 @@ SLX# = 0
|
||||
SLY# = 0
|
||||
SLI# = 0
|
||||
REM The two eraser stamps. Declared here for exactly the same reason -- built
|
||||
REM inside DRAWPROTOS and left undeclared, they were SHAPE:6 and SHAPE:7
|
||||
REM inside
|
||||
REM inside DRAWPROTOS and left undeclared, they were SHAPE:6 and SHAPE:7 inside
|
||||
REM it and empty everywhere else.
|
||||
BL$ = ""
|
||||
HBL$ = ""
|
||||
@@ -255,13 +254,10 @@ REM cursor through every DATA item in the program in the order they appear
|
||||
REM in the file, so whichever loader runs first gets the DATA that is
|
||||
REM written first. The tables are written first.
|
||||
REM The text layer repaints every row it owns, opaque, so it has to be moved
|
||||
REM out of the way before anything drawn can be seen. Two rows at the bottom
|
||||
REM is
|
||||
REM out of the way before anything drawn can be seen. Two rows at the bottom is
|
||||
REM enough for the final score, and hands the other thirty-five to the drawing
|
||||
REM verbs. Everything this game draws then simply stays there -- a drawing
|
||||
REM goes
|
||||
REM into a layer the frame composites, so nothing here is captured into a
|
||||
REM sprite
|
||||
REM verbs. Everything this game draws then simply stays there -- a drawing goes
|
||||
REM into a layer the frame composites, so nothing here is captured into a sprite
|
||||
REM and nothing is redrawn every frame.
|
||||
WINDOW 0, 35, 49, 36
|
||||
|
||||
@@ -297,8 +293,7 @@ ENVELOPE 0, 0, 6, 0, 4
|
||||
TEMPO 12
|
||||
|
||||
COLLISION 2, BRICKHIT
|
||||
REM The stamps, once. They used to be rebuilt whenever the SSHAPE pool ran
|
||||
REM dry,
|
||||
REM The stamps, once. They used to be rebuilt whenever the SSHAPE pool ran dry,
|
||||
REM because every frame's capture spent another slot; nothing captures now, so
|
||||
REM eight slots are spent here and never again.
|
||||
GOSUB DRAWPROTOS
|
||||
@@ -379,8 +374,7 @@ COLOR 1, 1
|
||||
FOR K# = 0 TO 15
|
||||
DRAW 1, 0, 130 + K# TO 67, 130 + K#
|
||||
NEXT K#
|
||||
REM And an eighth the width of the HUD strip, for the same reason: the strip
|
||||
REM is
|
||||
REM And an eighth the width of the HUD strip, for the same reason: the strip is
|
||||
REM rewritten whenever a number in it changes, and the old digits have to go
|
||||
REM somewhere first.
|
||||
FOR K# = 0 TO 59
|
||||
@@ -399,8 +393,7 @@ DPLAY# = 1
|
||||
DHUD# = 1
|
||||
RETURN
|
||||
|
||||
REM Take one brick off the screen: stamp the blank over it. Called when a
|
||||
REM brick
|
||||
REM Take one brick off the screen: stamp the blank over it. Called when a brick
|
||||
REM breaks, so the field is never redrawn as a whole during play -- which is
|
||||
REM what lets the whole live-list machinery go.
|
||||
LABEL ERASEBRICK
|
||||
@@ -451,8 +444,7 @@ WIDTH 1
|
||||
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
|
||||
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
|
||||
REM The old strip goes first. Nothing here clears the screen -- a drawing
|
||||
REM stays, which is the whole point -- so the digits that were there have to
|
||||
REM be
|
||||
REM stays, which is the whole point -- so the digits that were there have to be
|
||||
REM stamped over before the new ones are drawn.
|
||||
Z$ = HBL$
|
||||
GSHAPE Z$, 0, 0
|
||||
@@ -503,10 +495,7 @@ IF SNDON# = 0 THEN VOL 0
|
||||
RETURN
|
||||
|
||||
LABEL PRESSPAUSE
|
||||
IF STATE# <> 2 THEN GOTO PRESSPAUSE2
|
||||
STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER
|
||||
RETURN
|
||||
LABEL PRESSPAUSE2
|
||||
IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN
|
||||
IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER
|
||||
RETURN
|
||||
|
||||
|
||||
@@ -71,15 +71,10 @@ static char *ENEMY_CHARACTER[GALAGA_ENEMY_KINDS] = {
|
||||
/* --------------------------------------------------------------- random --- */
|
||||
|
||||
/*
|
||||
* The engine is this script's source of randomness: it refreshes GAME@.ROLL%
|
||||
* each frame and SELF@.ROLL% each call from this PRNG. A hand-rolled LCG
|
||||
* rather than rand() so a headless run is the same game on every libc, which
|
||||
* is what lets interop_test.c assert exact counts. The dialect gained a native
|
||||
* RND (issue #16) after this example was written; the field stays because RND
|
||||
* would reintroduce exactly the per-machine variation this avoids.
|
||||
*
|
||||
* The BASIC-visible name is ROLL%, not RND%: host field names share a
|
||||
* namespace with verbs and functions, so RND stopped being available as one.
|
||||
* No RND verb exists (issue #16), so the engine is the script's only source
|
||||
* of randomness: it refreshes GAME@.RND% each frame and SELF@.RND% each call
|
||||
* from this PRNG. A hand-rolled LCG rather than rand() so a headless run is
|
||||
* the same game on every libc.
|
||||
*/
|
||||
static uint32_t PRNG_STATE = 0x12345678u;
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ DEF DECIDEFIRE(DT%)
|
||||
DX% = GAME@.PLAYERX% - ACTOR@.X%
|
||||
IF ABS(DX%) > 140 THEN RETURN 0
|
||||
IF ACTOR@.Y% > GAME@.PLAYERY% THEN RETURN 0
|
||||
IF SELF@.ROLL% < DT% * 1.5 THEN SELF@.FIRE# = 1
|
||||
IF SELF@.RND% < DT% * 1.5 THEN SELF@.FIRE# = 1
|
||||
RETURN 0
|
||||
|
||||
REM Bee: enter, breathe in formation, occasionally dive nearly straight.
|
||||
@@ -65,7 +65,7 @@ DEF UPDATEBEE(DT%)
|
||||
IF (S# AND 2) > 0 THEN BEGIN
|
||||
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 1.7) * 16
|
||||
ACTOR@.Y% = SELF@.HOMEY%
|
||||
IF SELF@.ROLL% < DT% * 0.04 THEN SELF@.STATE# = 4 : SELF@.T% = 0
|
||||
IF SELF@.RND% < DT% * 0.04 THEN SELF@.STATE# = 4 : SELF@.T% = 0
|
||||
BEND
|
||||
IF (S# AND 4) > 0 THEN BEGIN
|
||||
R# = DIVESTEP(DT%, 130, 0.2)
|
||||
@@ -85,7 +85,7 @@ DEF UPDATEBFLY(DT%)
|
||||
IF (S# AND 2) > 0 THEN BEGIN
|
||||
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 2.1) * 24
|
||||
ACTOR@.Y% = SELF@.HOMEY%
|
||||
IF SELF@.ROLL% < DT% * 0.05 THEN SELF@.STATE# = 4 : SELF@.T% = 0
|
||||
IF SELF@.RND% < DT% * 0.05 THEN SELF@.STATE# = 4 : SELF@.T% = 0
|
||||
BEND
|
||||
IF (S# AND 4) > 0 THEN BEGIN
|
||||
R# = DIVESTEP(DT%, 260, 0.1)
|
||||
@@ -108,7 +108,7 @@ DEF UPDATEBOSS(DT%)
|
||||
IF (S# AND 2) > 0 THEN BEGIN
|
||||
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 1.1) * 10
|
||||
ACTOR@.Y% = SELF@.HOMEY%
|
||||
IF SELF@.ROLL% < DT% * 0.03 THEN SELF@.STATE# = 4 : SELF@.T% = 0
|
||||
IF SELF@.RND% < DT% * 0.03 THEN SELF@.STATE# = 4 : SELF@.T% = 0
|
||||
BEND
|
||||
IF (S# AND 4) > 0 THEN BEGIN
|
||||
R# = DIVESTEP(DT%, 60, 0.9)
|
||||
|
||||
@@ -77,7 +77,7 @@ typedef struct galaga_Enemy
|
||||
float t; /* parametric clock for the current maneuver */
|
||||
int32_t hp;
|
||||
int32_t fire; /* outbox: script sets 1, engine consumes */
|
||||
float rnd; /* inbox: fresh 0..1 each call; ROLL% in BASIC */
|
||||
float rnd; /* inbox: engine writes fresh 0..1 each call */
|
||||
} galaga_Enemy;
|
||||
|
||||
/** @brief Frame state every enemy may read. Bound once as GAME@. */
|
||||
@@ -86,7 +86,7 @@ typedef struct galaga_Shared
|
||||
float playerx; /* the player actor's position, this frame */
|
||||
float playery;
|
||||
int32_t wave;
|
||||
float rnd; /* fresh 0..1 each frame; ROLL% to the script */
|
||||
float rnd; /* fresh 0..1 each frame; the issue #16 route */
|
||||
} galaga_Shared;
|
||||
|
||||
/* --------------------------------------------------------------- screens --- */
|
||||
|
||||
@@ -491,8 +491,7 @@ static akerr_ErrorContext *frame(bool *running)
|
||||
}
|
||||
|
||||
/* The shared frame state, refreshed before any enemy thinks. The engine
|
||||
* fills GAME@.ROLL% from its own PRNG rather than letting the script call
|
||||
* the native RND, so a headless run is the same game on every machine. */
|
||||
* filling GAME@.RND% is the issue #16 route: no RND verb exists. */
|
||||
galaga_shared.playerx = galaga_game.player->x + 50.0f;
|
||||
galaga_shared.playery = galaga_game.player->y;
|
||||
galaga_shared.rnd = galaga_random();
|
||||
|
||||
@@ -62,7 +62,7 @@ static const akbasic_HostField ENEMY_FIELDS[] = {
|
||||
AKBASIC_HOST_FIELD( galaga_Enemy, t, "T%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||
AKBASIC_HOST_FIELD( galaga_Enemy, hp, "HP#", AKBASIC_HOSTFIELD_INT32 ),
|
||||
AKBASIC_HOST_FIELD( galaga_Enemy, fire, "FIRE#", AKBASIC_HOSTFIELD_INT32 ),
|
||||
AKBASIC_HOST_FIELD( galaga_Enemy, rnd, "ROLL%", AKBASIC_HOSTFIELD_FLOAT )
|
||||
AKBASIC_HOST_FIELD( galaga_Enemy, rnd, "RND%", AKBASIC_HOSTFIELD_FLOAT )
|
||||
};
|
||||
static const akbasic_HostType ENEMY_TYPE = {
|
||||
"ENEMY", sizeof(galaga_Enemy), ENEMY_FIELDS, 8
|
||||
@@ -87,7 +87,7 @@ static const akbasic_HostField GAME_FIELDS[] = {
|
||||
AKBASIC_HOST_FIELD( galaga_Shared, playerx, "PLAYERX%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||
AKBASIC_HOST_FIELD( galaga_Shared, playery, "PLAYERY%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||
AKBASIC_HOST_FIELD( galaga_Shared, wave, "WAVE#", AKBASIC_HOSTFIELD_INT32 ),
|
||||
AKBASIC_HOST_FIELD( galaga_Shared, rnd, "ROLL%", AKBASIC_HOSTFIELD_FLOAT )
|
||||
AKBASIC_HOST_FIELD( galaga_Shared, rnd, "RND%", AKBASIC_HOSTFIELD_FLOAT )
|
||||
};
|
||||
static const akbasic_HostType GAME_TYPE = {
|
||||
"GAME", sizeof(galaga_Shared), GAME_FIELDS, 4
|
||||
|
||||
@@ -56,10 +56,7 @@ This is a tour of the interpreter's edges, on purpose:
|
||||
counterpoint: a genuine 24x21 `SPRSAV` type-in sprite, 63 bytes, which is
|
||||
everything `DATA` has room to say. The paint is the reveal, and the exit is a
|
||||
stride-63 column dissolve — 63 is coprime to 160, so the walk hits every
|
||||
column once and looks random while carrying no state. The strings come in
|
||||
64-character chunks because a source line is 80 columns, like the machines
|
||||
this pretends to be — the decoder carries its cursor from one `IM$` entry to
|
||||
the next, and the generator refuses to cut inside a run.
|
||||
column once and looks random while carrying no state.
|
||||
- **And then the picture is video.** Six delta frames loop the floor grid toward
|
||||
you and crawl the sun's slice pattern — full motion video, 1.3 KB total, about
|
||||
220 bytes a frame. A delta re-encodes only the rows that changed: an `R`
|
||||
@@ -85,10 +82,6 @@ This is a tour of the interpreter's edges, on purpose:
|
||||
batch a bar of noise drums. Noise is real: `ENVELOPE`'s sixth argument is a
|
||||
waveform, 0 to 3, which Chapter 7 forgot to mention. Voice 1 carries the tune so
|
||||
the collision blips (voice 2) and scene sweeps (voice 3) never steal its channel.
|
||||
A batch is four `PLAY` statements, one bar each — the 80-column line limit will
|
||||
not hold four bars in one string, and it does not need to: the parser's voice,
|
||||
envelope and duration state persists across statements and every `PLAY` appends
|
||||
to the same queue, so four bars queue exactly as one line would.
|
||||
|
||||
It also probes for its hardware like a proper boot loader, with one `TRAP` per
|
||||
device: no graphics refuses by name and exits, no audio mutes the soundtrack, no
|
||||
|
||||
@@ -10,7 +10,7 @@ REM A demoscene production for the akbasic interpreter, written as if
|
||||
REM it were 1985 and this dialect were the machine under the tree.
|
||||
REM It leans on every corner of the interpreter on purpose:
|
||||
REM
|
||||
REM - it carries chapter 17's LCG rather than the native RND, and seeds it
|
||||
REM - there is no RND, so it carries chapter 17's LCG and seeds it
|
||||
REM from the jiffy clock
|
||||
REM - the palette cannot be rewritten, so every "colour cycle" is an
|
||||
REM honest redraw of the same strokes in the next colour
|
||||
@@ -81,7 +81,7 @@ REM machine and a predeclared scratch variable occupies one forever,
|
||||
REM where a scoped one gives its slot back. The first cut of this
|
||||
REM program predeclared everything and ran the pool dry.
|
||||
RMAX# = 0
|
||||
ROLL# = 0
|
||||
RND# = 0
|
||||
TX% = 0
|
||||
TB# = 0
|
||||
|
||||
@@ -461,78 +461,38 @@ BS% = W# / 160
|
||||
IF BS% < 1 THEN BS% = 1
|
||||
REM ---- PICTURE-BEGIN (generated by vaporwave.py; do not
|
||||
REM ---- hand-edit -- rerun the script to change the picture)
|
||||
DIM IM$(56)
|
||||
DIM IM$(16)
|
||||
DIM VA#(6)
|
||||
DIM VB#(6)
|
||||
NS# = 32
|
||||
VA#(0) = 32
|
||||
VB#(0) = 35
|
||||
VA#(1) = 36
|
||||
VB#(1) = 39
|
||||
VA#(2) = 40
|
||||
VB#(2) = 43
|
||||
VA#(3) = 44
|
||||
VB#(3) = 47
|
||||
VA#(4) = 48
|
||||
VB#(4) = 51
|
||||
VA#(5) = 52
|
||||
VB#(5) = 55
|
||||
IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEH"
|
||||
IM$(1) = "G5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGI"
|
||||
IM$(2) = "EHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEH"
|
||||
IM$(3) = "GPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPEHGHEP"
|
||||
IM$(4) = "GHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPEC"
|
||||
IM$(5) = "PAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEP"
|
||||
IM$(6) = "GHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEP"
|
||||
IM$(7) = "GHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEPGHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHA"
|
||||
IM$(8) = "E9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAET"
|
||||
IM$(9) = "EHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9"
|
||||
IM$(10) = "HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKH"
|
||||
IM$(11) = "EHKAH9HKKHEHKHEPKHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKH"
|
||||
IM$(12) = "EHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKH"
|
||||
IM$(13) = "EHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSED"
|
||||
IM$(14) = "KPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH"
|
||||
IM$(15) = "KPEDH9HUKCEHK5EHK5EHKLH9HUKKEHK5EHK5EHKCH9HWEBKPEHKXKHEHK8I9IWKZ"
|
||||
IM$(16) = "EHKPKPEHK0I9IWK7EHKHKXEHKSI9IWK9KFEHK9KOI9IWK9KNK9KOI9IWK9KNK9KO"
|
||||
IM$(17) = "I9IWK9KNKPIHKZI9IYK6IHKHKXIHKSI9IWK9KFIHK5IHKKI9IWK9KNIHK5IHKCI9"
|
||||
IM$(18) = "IYKPIHKXKHIHK5IHKPIHK5IHKPIHKPKPIHK0I9IWKJIHKPIHKHIHKPIHKPI9IZKB"
|
||||
IM$(19) = "IHKHIHKPIHKHIHKPIHKKI9IWKJIHKHIHKPIHKHIHKPIHKDI9IXKPIHKHIHKHKHIH"
|
||||
IM$(20) = "KHIPKHI9IYKCIHKPIHKHIHKPIHKHIPKHIHKPIHKHIPKHIHKPIHKHIHKPIHKHI9I9"
|
||||
IM$(21) = "IHKHIHKPIHKHIPKHIHKHIEC9CSKDIPKHIPKHIHKHIPKHIHKEC9CSIDKHIPKHIPIP"
|
||||
IM$(22) = "KHIPKHIFC9CQKEIHKHIPKHIHIXKHIPKGC9COIFKHI5KHI5KHIPKHI5KHIPKHI5KH"
|
||||
IM$(23) = "I5KHIPKHI5KHIPKHIXIHKHI9IDC9CMI4KHIPIPKHI6C9CKI9IDKHIHIXKHIZC9CI"
|
||||
IM$(24) = "I9IMKHI9IWC9CGI9IVI9I9I9I9IPI9I9I9I9IPQ9Q9Q9Q9QPE9E9E9E9EPE9E9E9"
|
||||
IM$(25) = "E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QP"
|
||||
IM$(26) = "Q9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9"
|
||||
IM$(27) = "Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9"
|
||||
IM$(28) = "QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9"
|
||||
IM$(29) = "Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9"
|
||||
IM$(30) = "Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP"
|
||||
IM$(31) = "Q9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP"
|
||||
IM$(32) = "RBRQ9QOKMQHK5RBSQ9QTI9IRRBXQ9QTKPQHKHQPKERBYQ9QPI9IHRB3Q9QSKAI5"
|
||||
IM$(33) = "KHIJRB5Q9QTC9CMRB9Q9QWI9IGRCBQ9QYC9CCRCIA9A9A9A9APRCJE9E9E9E9EP"
|
||||
IM$(34) = "RCNA9A9A9A9APRCOE9E9E9E9EPRCUA9A9A9A9APRCVE9E9E9E9EPRC5A9A9A9A9"
|
||||
IM$(35) = "APRC7E9E9E9E9EP"
|
||||
IM$(36) = "RBQQ9QOK9KIQHKFRBRQ9QOI9IQRBWQ9QPKLQHKHQHKPRBXQ9QTI9QLIERB2Q9QR"
|
||||
IM$(37) = "IBKHIPKHIPKCRB4Q9QSC9CORB8Q9QVI3KHIGRCAQ9QXC9CERCJA9A9A9A9APRCK"
|
||||
IM$(38) = "E9E9E9E9EPRCOA9A9A9A9APRCPE9E9E9E9EPRCVA9A9A9A9APRCXE9E9E9E9EP"
|
||||
IM$(39) = "RC7A9A9A9A9APRDAE9E9E9E9EP"
|
||||
IM$(40) = "RBPQ9QOK9KAQHKNRBQQ9QOI9IWRBVQ9QOKEQHKHQHKPQHKFRBWQ9QPI9IPRB1Q9"
|
||||
IM$(41) = "QQKCIPKHIPKHIDRB3Q9QSC9CORB7Q9QUIWKHIPRB9Q9QWC9CGRCEA9A9A9A9AP"
|
||||
IM$(42) = "RCFE9E9E9E9EPRCGA9A9A9A9APRCHE9E9E9E9EPRCPA9A9A9A9APRCQE9E9E9E9"
|
||||
IM$(43) = "EPRCXA9A9A9A9APRCZE9E9E9E9EPRDAA9A9A9A9APRDCE9E9E9E9EP"
|
||||
IM$(44) = "RBOQ9QNK3QHKWRBPQ9QOI9IWRBUQ9QTKHQHKPQHKNRBVQ9QOI9IWRB0Q9QQIKKH"
|
||||
IM$(45) = "IPKHIHKDRB1Q9QQC9CSRB2Q9QRC9CQRB6Q9QTIPKHIYRB8Q9QVC9CIRCKA9A9A9"
|
||||
IM$(46) = "A9APRCLE9E9E9E9EPRCQA9A9A9A9APRCRE9E9E9E9EPRCZA9A9A9A9APRC1E9E9"
|
||||
IM$(47) = "E9E9EPRDCA9A9A9A9APRDFE9E9E9E9EP"
|
||||
IM$(48) = "RBOQ9QNI9IYRBTQ9QOKEQHKPQHKVRBUQ9QTI9IRRBZQ9QTKHQHKPQHKHRB0Q9QQ"
|
||||
IM$(49) = "C9CSRB5Q9QTIHKHI5KARB7Q9QUC9CKRCBQ9QYI9ICRCHA9A9A9A9APRCIE9E9E9"
|
||||
IM$(50) = "E9EPRCLA9A9A9A9APRCME9E9E9E9EPRCRA9A9A9A9APRCTE9E9E9E9EPRC1A9A9"
|
||||
IM$(51) = "A9A9APRC3E9E9E9E9EPRDFA9A9A9A9APRDIE9E9E9E9EP"
|
||||
IM$(52) = "RBSQ9QTKPQHK3RBTQ9QOI9IWRBYQ9QPKDQHKPQHKHRBZQ9QTI9ILRB4Q9QSIAKH"
|
||||
IM$(53) = "I5KHIBRB6Q9QTC9CMRCAQ9QXI9IERCEE9E9E9E9EPRCFA9A9A9A9APRCGE9E9E9"
|
||||
IM$(54) = "E9EPRCMA9A9A9A9APRCNE9E9E9E9EPRCTA9A9A9A9APRCUE9E9E9E9EPRC3A9A9"
|
||||
IM$(55) = "A9A9APRC5E9E9E9E9EPRDIA9A9A9A9AP"
|
||||
NS# = 9
|
||||
VA#(0) = 9
|
||||
VB#(0) = 9
|
||||
VA#(1) = 10
|
||||
VB#(1) = 10
|
||||
VA#(2) = 11
|
||||
VB#(2) = 12
|
||||
VA#(3) = 13
|
||||
VB#(3) = 13
|
||||
VA#(4) = 14
|
||||
VB#(4) = 14
|
||||
VA#(5) = 15
|
||||
VB#(5) = 15
|
||||
IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEHG5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGIEHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEH"
|
||||
IM$(1) = "GHEPGHEHGPEHGHEPGHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPECPAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEPGHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEP"
|
||||
IM$(2) = "GHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHAE9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAETEHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKHEHKAH9HKKHEHKHEP"
|
||||
IM$(3) = "KHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKHEHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKHEHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSEDKPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH"
|
||||
IM$(4) = "KPEDH9HUKCEHK5EHK5EHKLH9HUKKEHK5EHK5EHKCH9HWEBKPEHKXKHEHK8I9IWKZEHKPKPEHK0I9IWK7EHKHKXEHKSI9IWK9KFEHK9KOI9IWK9KNK9KOI9IWK9KNK9KOI9IWK9KNKPIHKZI9IYK6IHKHKXIHKSI9IWK9KFIHK5IHKKI9IWK9KNIHK5IHKCI9IYKPIHKXKHIHK5IHKPIHK5IHKPIHKPKPIHK0I9IWKJIHKPIH"
|
||||
IM$(5) = "KHIHKPIHKPI9IZKBIHKHIHKPIHKHIHKPIHKKI9IWKJIHKHIHKPIHKHIHKPIHKDI9IXKPIHKHIHKHKHIHKHIPKHI9IYKCIHKPIHKHIHKPIHKHIPKHIHKPIHKHIPKHIHKPIHKHIHKPIHKHI9I9IHKHIHKPIHKHIPKHIHKHIEC9CSKDIPKHIPKHIHKHIPKHIHKEC9CSIDKHIPKHIPIPKHIPKHIFC9CQKEIHKHIPKHIHIXKHIPKG"
|
||||
IM$(6) = "C9COIFKHI5KHI5KHIPKHI5KHIPKHI5KHI5KHIPKHI5KHIPKHIXIHKHI9IDC9CMI4KHIPIPKHI6C9CKI9IDKHIHIXKHIZC9CII9IMKHI9IWC9CGI9IVI9I9I9I9IPI9I9I9I9IPQ9Q9Q9Q9QPE9E9E9E9EPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9"
|
||||
IM$(7) = "Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9"
|
||||
IM$(8) = "Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP"
|
||||
IM$(9) = "RBRQ9QOKMQHK5RBSQ9QTI9IRRBXQ9QTKPQHKHQPKERBYQ9QPI9IHRB3Q9QSKAI5KHIJRB5Q9QTC9CMRB9Q9QWI9IGRCBQ9QYC9CCRCIA9A9A9A9APRCJE9E9E9E9EPRCNA9A9A9A9APRCOE9E9E9E9EPRCUA9A9A9A9APRCVE9E9E9E9EPRC5A9A9A9A9APRC7E9E9E9E9EP"
|
||||
IM$(10) = "RBQQ9QOK9KIQHKFRBRQ9QOI9IQRBWQ9QPKLQHKHQHKPRBXQ9QTI9QLIERB2Q9QRIBKHIPKHIPKCRB4Q9QSC9CORB8Q9QVI3KHIGRCAQ9QXC9CERCJA9A9A9A9APRCKE9E9E9E9EPRCOA9A9A9A9APRCPE9E9E9E9EPRCVA9A9A9A9APRCXE9E9E9E9EPRC7A9A9A9A9APRDAE9E9E9E9EP"
|
||||
IM$(11) = "RBPQ9QOK9KAQHKNRBQQ9QOI9IWRBVQ9QOKEQHKHQHKPQHKFRBWQ9QPI9IPRB1Q9QQKCIPKHIPKHIDRB3Q9QSC9CORB7Q9QUIWKHIPRB9Q9QWC9CGRCEA9A9A9A9APRCFE9E9E9E9EPRCGA9A9A9A9APRCHE9E9E9E9EPRCPA9A9A9A9APRCQE9E9E9E9EPRCXA9A9A9A9APRCZE9E9E9E9EPRDAA9A9A9A9APRDCE9E9E9E9"
|
||||
IM$(12) = "EP"
|
||||
IM$(13) = "RBOQ9QNK3QHKWRBPQ9QOI9IWRBUQ9QTKHQHKPQHKNRBVQ9QOI9IWRB0Q9QQIKKHIPKHIHKDRB1Q9QQC9CSRB2Q9QRC9CQRB6Q9QTIPKHIYRB8Q9QVC9CIRCKA9A9A9A9APRCLE9E9E9E9EPRCQA9A9A9A9APRCRE9E9E9E9EPRCZA9A9A9A9APRC1E9E9E9E9EPRDCA9A9A9A9APRDFE9E9E9E9EP"
|
||||
IM$(14) = "RBOQ9QNI9IYRBTQ9QOKEQHKPQHKVRBUQ9QTI9IRRBZQ9QTKHQHKPQHKHRB0Q9QQC9CSRB5Q9QTIHKHI5KARB7Q9QUC9CKRCBQ9QYI9ICRCHA9A9A9A9APRCIE9E9E9E9EPRCLA9A9A9A9APRCME9E9E9E9EPRCRA9A9A9A9APRCTE9E9E9E9EPRC1A9A9A9A9APRC3E9E9E9E9EPRDFA9A9A9A9APRDIE9E9E9E9EP"
|
||||
IM$(15) = "RBSQ9QTKPQHK3RBTQ9QOI9IWRBYQ9QPKDQHKPQHKHRBZQ9QTI9ILRB4Q9QSIAKHI5KHIBRB6Q9QTC9CMRCAQ9QXI9IERCEE9E9E9E9EPRCFA9A9A9A9APRCGE9E9E9E9EPRCMA9A9A9A9APRCNE9E9E9E9EPRCTA9A9A9A9APRCUE9E9E9E9EPRC3A9A9A9A9APRC5E9E9E9E9EPRDIA9A9A9A9AP"
|
||||
REM ---- PICTURE-END
|
||||
SS# = 0
|
||||
SE# = NS# - 1
|
||||
@@ -816,12 +776,11 @@ REM =====================================================================
|
||||
REM Subroutines.
|
||||
REM =====================================================================
|
||||
|
||||
REM Chapter 17's historical generator, verbatim. RND(n) is built in now;
|
||||
REM this stays so the demo runs identically on every machine.
|
||||
REM Answers 0 to RMAX#-1 in ROLL#, from the middle bits of the seed.
|
||||
REM Chapter 17's generator, verbatim: there is no RND in this dialect.
|
||||
REM Answers 0 to RMAX#-1 in RND#, from the middle bits of the seed.
|
||||
LABEL RANDOM
|
||||
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
||||
ROLL# = MOD((SEED# / 65536), RMAX#)
|
||||
RND# = MOD((SEED# / 65536), RMAX#)
|
||||
RETURN
|
||||
|
||||
REM NS# stars in four brightnesses. Two LCG pulls a star, and the
|
||||
@@ -831,10 +790,10 @@ SI# = 0
|
||||
DO WHILE SI# < NS#
|
||||
RMAX# = W#
|
||||
GOSUB RANDOM
|
||||
X# = ROLL#
|
||||
X# = RND#
|
||||
RMAX# = H#
|
||||
GOSUB RANDOM
|
||||
Y# = ROLL#
|
||||
Y# = RND#
|
||||
B# = MOD(SEED#, 4)
|
||||
COLOR 1, STC#(B#)
|
||||
DRAW 1, X#, Y#
|
||||
@@ -1292,7 +1251,7 @@ LABEL REHOME
|
||||
MOVSPR L2#, 0.5 * W#, 0.5 * H#
|
||||
RMAX# = 360
|
||||
GOSUB RANDOM
|
||||
SBG#(L2#) = ROLL#
|
||||
SBG#(L2#) = RND#
|
||||
MOVSPR L2#, SBG#(L2#) # 5
|
||||
RETURN
|
||||
|
||||
@@ -1358,25 +1317,13 @@ TB# = TB# + 1
|
||||
IF TB# > 2 THEN TB# = 0
|
||||
RETURN
|
||||
|
||||
REM A batch is four PLAY statements, one bar each: the parser's V, T,
|
||||
REM U and duration state persists across statements and every PLAY
|
||||
REM appends to the same queue, so four bars queue exactly as one long
|
||||
REM string would -- which the 80-column line limit no longer allows.
|
||||
REM Each bar restates the prefix anyway, so a bar dropped by QFULL
|
||||
REM never leaves the next one playing with drum-kit state.
|
||||
LABEL TUNEA
|
||||
PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE"
|
||||
PLAY "V1T3U9S O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A"
|
||||
PLAY "V1T3U9S O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C"
|
||||
PLAY "V1T3U9S O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B"
|
||||
PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B"
|
||||
MT# = TI# + 270
|
||||
RETURN
|
||||
|
||||
LABEL TUNEB
|
||||
PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A"
|
||||
PLAY "V1T3U9S O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A"
|
||||
PLAY "V1T3U9S O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D"
|
||||
PLAY "V1T3U9S O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E"
|
||||
PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E"
|
||||
MT# = TI# + 270
|
||||
RETURN
|
||||
|
||||
|
||||
@@ -58,14 +58,7 @@ SKIP = "Q"
|
||||
ROWREC = "R"
|
||||
LENCH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
MAXRUN = len(LENCH)
|
||||
|
||||
# The interpreter reads source through an 80-byte line buffer and refuses
|
||||
# any line that fills it (AKBASIC_MAX_LINE_LENGTH, sink_stdio.c), so a
|
||||
# stored line is at most 78 characters plus its newline. 'IM$(NN) = "' and
|
||||
# the closing quote spend 12 of those; 64 keeps the emitted lines under
|
||||
# the ceiling with margin to spare while the index stays two digits.
|
||||
PAYLOAD = 64
|
||||
MAXLINE = 78
|
||||
PAYLOAD = 240
|
||||
|
||||
SUN_CX, SUN_CY, SUN_R = 80, 50, 30
|
||||
HORIZON = 74
|
||||
@@ -213,32 +206,13 @@ def encode_delta(prev, cur):
|
||||
|
||||
|
||||
def chop(blob):
|
||||
"""Split a stream into strings of at most PAYLOAD characters, cutting
|
||||
only between records. The decoder reads a record's tail characters
|
||||
with MID on the string it is walking, so a run (two characters) or a
|
||||
row record (three) that straddled two IM$ entries would decode as
|
||||
garbage; DRAWSTREAM only carries the cursor, never a partial record."""
|
||||
out, cur = [], ""
|
||||
p = 0
|
||||
while p < len(blob):
|
||||
n = 3 if blob[p] == ROWREC else 2
|
||||
if len(cur) + n > PAYLOAD:
|
||||
out.append(cur)
|
||||
cur = ""
|
||||
cur += blob[p:p + n]
|
||||
p += n
|
||||
if cur:
|
||||
out.append(cur)
|
||||
return out
|
||||
return [blob[i:i + PAYLOAD] for i in range(0, len(blob), PAYLOAD)]
|
||||
|
||||
|
||||
def simulate(raster, blob, x=0, y=0):
|
||||
def simulate(raster, blob):
|
||||
"""Apply one encoded stream to a raster exactly the way the BASIC
|
||||
decoder does, skips-draw-nothing and all. The cursor comes in and
|
||||
goes back out because DRAWSTREAM carries it from one IM$ entry to
|
||||
the next -- decoding the chopped strings one at a time with the
|
||||
cursor threaded through is exactly what the demo will execute."""
|
||||
p = 0
|
||||
decoder does, skips-draw-nothing and all."""
|
||||
x = y = p = 0
|
||||
while p < len(blob):
|
||||
c = blob[p]
|
||||
if c == ROWREC:
|
||||
@@ -256,31 +230,20 @@ def simulate(raster, blob, x=0, y=0):
|
||||
x = 0
|
||||
y += 1
|
||||
p += 2
|
||||
return raster, x, y
|
||||
|
||||
|
||||
def simulate_lines(raster, lines):
|
||||
"""One stream as its chopped strings, cursor carried across the
|
||||
boundaries the way DRAWSTREAM carries X# and Y#."""
|
||||
x = y = 0
|
||||
for line in lines:
|
||||
raster, x, y = simulate(raster, line, x, y)
|
||||
return raster
|
||||
|
||||
|
||||
def verify(frames, base_lines, delta_line_groups):
|
||||
def verify(frames, base_blob, delta_blobs):
|
||||
"""The base must reproduce frame 0 exactly, and each delta must
|
||||
carry the raster exactly to the next frame. A skip leaves the cell
|
||||
the encoder promised was already right, so equality is total and
|
||||
any difference at all is an encoder bug. This decodes the CHOPPED
|
||||
strings, not the blobs, so a chop that split a record would fail
|
||||
here instead of corrupting the screen."""
|
||||
any difference at all is an encoder bug."""
|
||||
raster = [[1] * W for _ in range(H)]
|
||||
raster = simulate_lines(raster, base_lines)
|
||||
raster = simulate(raster, base_blob)
|
||||
assert raster == frames[0], "base stream does not reproduce frame 0"
|
||||
for i, lines in enumerate(delta_line_groups):
|
||||
for i, blob in enumerate(delta_blobs):
|
||||
want = frames[(i + 1) % PHASES]
|
||||
raster = simulate_lines(raster, lines)
|
||||
raster = simulate(raster, blob)
|
||||
assert raster == want, "delta %d does not reproduce its frame" % i
|
||||
|
||||
|
||||
@@ -298,9 +261,6 @@ def emit_block(base_lines, delta_ranges, all_lines):
|
||||
for i, s in enumerate(all_lines):
|
||||
out.append('IM$(%d) = "%s"' % (i, s))
|
||||
out.append("REM ---- PICTURE-END")
|
||||
for line in out:
|
||||
assert len(line) <= MAXLINE, "emitted line over %d chars: %r" % (
|
||||
MAXLINE, line)
|
||||
return out
|
||||
|
||||
|
||||
@@ -343,15 +303,15 @@ def main():
|
||||
base_lines = chop(base_blob)
|
||||
all_lines = list(base_lines)
|
||||
delta_ranges = []
|
||||
delta_line_groups = []
|
||||
delta_blobs = []
|
||||
for i in range(PHASES):
|
||||
blob = encode_delta(frames[i], frames[(i + 1) % PHASES])
|
||||
delta_blobs.append(blob)
|
||||
lines = chop(blob)
|
||||
delta_line_groups.append(lines)
|
||||
delta_ranges.append((len(all_lines), len(all_lines) + len(lines) - 1))
|
||||
all_lines.extend(lines)
|
||||
verify(frames, base_lines, delta_line_groups)
|
||||
dbytes = sum(len(l) for g in delta_line_groups for l in g)
|
||||
verify(frames, base_blob, delta_blobs)
|
||||
dbytes = sum(len(b) for b in delta_blobs)
|
||||
print("base %d bytes in %d strings; video %d bytes in %d strings; "
|
||||
"%d strings total" %
|
||||
(sum(len(s) for s in base_lines), len(base_lines), dbytes,
|
||||
|
||||
@@ -12,12 +12,9 @@
|
||||
* libakerror 2.0.0 is the floor, raised from 1.0.0 because 2.0.0 is an ABI break
|
||||
* that a compile against the wrong header cannot survive quietly:
|
||||
*
|
||||
* - The context behind `IGNORE` became thread-local. `IGNORE` expands at *our*
|
||||
* call site, so our objects reference that storage under whichever model the
|
||||
* header on the include path declared. 2.0.2 went further and made it a
|
||||
* per-translation-unit `static` snapshot named `akerr_last_ignored`, copied
|
||||
* from the pool slot so the slot can be released; the old spelling
|
||||
* `__akerr_last_ignored` was an `extern` pointer and no longer exists.
|
||||
* - `__akerr_last_ignored` became thread-local. `IGNORE` expands at *our* call
|
||||
* site, so our objects reference that symbol under whichever storage model
|
||||
* the header on the include path declared.
|
||||
* - `akerr_next_error()` now returns a context that already holds a reference,
|
||||
* and `ENSURE_ERROR_READY` no longer increments. Objects compiled against a
|
||||
* 1.x header count every reference twice and never give a slot back.
|
||||
|
||||
@@ -282,8 +282,9 @@ def copy_tree(src, dst):
|
||||
# and a coverage tree drags along every .gcno/.gcda as well.
|
||||
#
|
||||
# deps/ is copied wholesale and has to be: the build pulls libakerror and
|
||||
# libakstdlib in with add_subdirectory. The language corpus is the other
|
||||
# reason it comes along with the rest of the tree.
|
||||
# libakstdlib in with add_subdirectory. The golden corpus used to be the
|
||||
# other reason -- it was driven in place out of deps/basicinterpret -- and
|
||||
# now lives in tests/reference/, which comes along with the rest of the tree.
|
||||
# Only vendored Windows DLLs are dead weight, hence "*.dll".
|
||||
#
|
||||
# **PNGs are ours or theirs, and the difference matters.** This used to drop
|
||||
|
||||
@@ -893,7 +893,7 @@ akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTL
|
||||
*
|
||||
* **Only a block skip**, which is what the `BEND` test is for. A
|
||||
* zero-iteration `FOR` skips its body the same way, and there the
|
||||
* orphan is load-bearing: `tests/language/.../nestedforloopwaiting
|
||||
* orphan is load-bearing: `tests/reference/.../nestedforloopwaiting
|
||||
* forcommand.bas` nests a loop inside one that runs zero times, and
|
||||
* the inner scope is what absorbs the inner `NEXT` so the outer `NEXT`
|
||||
* still finds its `FOR`. Popping it there turns that case into "NEXT
|
||||
@@ -1286,7 +1286,7 @@ akerr_ErrorContext *akbasic_runtime_process_line_runstream(akbasic_Runtime *obj)
|
||||
* This mode used to file it like any other, under the cursor -- which for a
|
||||
* blank line is the number of the line *before* it. A file ending in a blank
|
||||
* line therefore had its last line erased before it ever ran, silently. The
|
||||
* reference did the same, and `tests/language/arithmetic/integer.bas`
|
||||
* reference did the same, and `tests/reference/language/arithmetic/integer.bas`
|
||||
* has an expectation with three values for four PRINT statements to prove it.
|
||||
*/
|
||||
if ( buffer[0] == '\0' ) {
|
||||
|
||||
@@ -407,22 +407,17 @@ akerr_ErrorContext *akbasic_cmd_directory(akbasic_Runtime *obj, akbasic_ASTLeaf
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in DIRECTORY");
|
||||
/*
|
||||
* Refused rather than half-built. This was blocked upstream: listing a
|
||||
* directory needs opendir/readdir, `libakstdlib` did not wrap them, and
|
||||
* this project's rule is that a missing capability gets filed upstream
|
||||
* rather than worked around here (MAINTENANCE.md). That was libakstdlib
|
||||
* issue #10, and it landed -- aksl_opendir, aksl_readdir, aksl_closedir
|
||||
* and aksl_rewinddir all exist as of the revision this tree pins.
|
||||
* Refused rather than half-built. Listing a directory needs opendir/readdir,
|
||||
* which `libakstdlib` does not wrap -- and this project's rule is that a
|
||||
* missing capability gets filed upstream rather than worked around here
|
||||
* (MAINTENANCE.md). Filed as libakstdlib issue #10.
|
||||
*
|
||||
* So the blocker is gone and only the work is left. Writing the verb needs
|
||||
* decisions this commit is not the place for: what a listing looks like on
|
||||
* a filesystem with no disk-image block counts, which of the Commodore
|
||||
* wildcard forms to honour, and where the entries go. Tracked as akbasic
|
||||
* issue #55; the refusal stays honest until then rather than growing a
|
||||
* half-listing nobody specified.
|
||||
* The alternative was shelling out to `ls`, which a library has no business
|
||||
* doing, or calling readdir directly and stepping outside the error
|
||||
* convention every other call in this file follows.
|
||||
*/
|
||||
FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
|
||||
"DIRECTORY is not implemented yet");
|
||||
"DIRECTORY is not implemented: libakstdlib has no directory-reading wrapper yet");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ BSAVE/BLOAD -- */
|
||||
|
||||
@@ -244,16 +244,10 @@ static void test_no_drive_verbs(void)
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/*
|
||||
* DIRECTORY is refused for a different reason than the five above: not for
|
||||
* want of a drive, but because it is unwritten. It used to name the missing
|
||||
* libakstdlib wrapper; that wrapper landed, so naming it would be a lie.
|
||||
*/
|
||||
/* DIRECTORY is refused for a different reason, and says which. */
|
||||
TEST_REQUIRE_OK(run_program("10 DIRECTORY\n"));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "not implemented") != NULL,
|
||||
"DIRECTORY should say it is unwritten, got \"%s\"", HARNESS_OUTPUT);
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "libakstdlib") == NULL,
|
||||
"DIRECTORY must not still blame libakstdlib, got \"%s\"", HARNESS_OUTPUT);
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "libakstdlib") != NULL,
|
||||
"DIRECTORY should name the missing wrapper, got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ static void test_counter_survives_the_loop(void)
|
||||
* `FOR I = 1 TO 1` executes the body one time on every BASIC there has ever
|
||||
* been. Here the entry test treats "the counter has reached the limit" as
|
||||
* "do not enter", so the body is skipped entirely -- and
|
||||
* tests/language/flowcontrol/forloopwaitingforcommand.bas pins that,
|
||||
* tests/reference/language/flowcontrol/forloopwaitingforcommand.bas pins that,
|
||||
* which is why this cannot simply be corrected.
|
||||
*/
|
||||
static void test_single_iteration_loop(void)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# The language test corpus
|
||||
|
||||
The `.bas` files in this directory and their sibling `.txt` files are the editable language
|
||||
corpus. CMake registers every program as an individual `local_*` CTest case and compares its
|
||||
output with the sibling expectation.
|
||||
|
||||
The corpus includes cases carried over from the deprecated Go implementation as well as cases
|
||||
written for this interpreter. Their provenance is useful when investigating a regression, but it
|
||||
does not make any case immutable or turn the old implementation into a specification.
|
||||
|
||||
**Change a program and its expectation deliberately and in the same commit.** If the changed
|
||||
output represents an intentional language decision, record the reason in `TODO.md` or the
|
||||
relevant documentation. If it is an accidental change, fix the interpreter instead.
|
||||
|
||||
The `examples/` subdirectory contains complete BASIC programs used as worked examples. It is
|
||||
part of the same corpus and follows the same `.bas`/`.txt` pairing rule.
|
||||
@@ -1,6 +1,6 @@
|
||||
10 REM An array reference used as a function argument, and as one of several.
|
||||
20 REM An identifier's subscript list used to hang off .right, which is also
|
||||
30 REM where arguments chain their arguments -- so the arity counter walked
|
||||
30 REM where an argument list chains its arguments -- so the arity counter walked
|
||||
40 REM straight into the subscripts and refused the call. TODO.md section 4.
|
||||
50 DIM C#(4)
|
||||
60 C#(1) = -9
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
10 REM FILTER has no device capability -- audio synthesises and mixes but
|
||||
20 REM has no filter stage; SDL3 supplies no primitive to build one from. It
|
||||
30 REM is refused rather than silently ignored, so a program that asked
|
||||
10 REM FILTER has no device capability behind it -- akgl_audio_* synthesises and
|
||||
20 REM mixes but has no filter stage, and SDL3 supplies no primitive to build one
|
||||
30 REM from. It is refused rather than silently ignored, so a program that asked
|
||||
40 REM for a low-pass finds out it did not get one.
|
||||
50 PRINT "BEFORE"
|
||||
60 FILTER 1000, 1, 0, 0, 5
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
10 REM The standalone driver has no audio device. ENVELOPE, VOL and
|
||||
10 REM The standalone driver lends the script no audio device. ENVELOPE, VOL and
|
||||
20 REM TEMPO only change interpreter state, so they work regardless; SOUND and
|
||||
30 REM PLAY need the device and must name themselves when there is none.
|
||||
40 ENVELOPE 1, 5, 9, 12, 2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
10 REM A DEF call takes an environment from the pool, one per call, exactly as
|
||||
10 REM A DEF call takes its environment from the pool, one per call, exactly as
|
||||
20 REM GOSUB does. It used to be owned by the funcdef and reset on every call,
|
||||
30 REM which cost two silent defects: two calls shared a slot,
|
||||
30 REM which cost two silent defects: two calls in one expression shared a slot,
|
||||
40 REM and recursion never came back at all.
|
||||
50 DEF FACT(N#)
|
||||
60 IF N# <= 1 THEN RETURN 1
|
||||
@@ -15,9 +15,9 @@
|
||||
150 DEF DBL(N#) = N# * 2
|
||||
160 PRINT DBL(10) + DBL(1)
|
||||
170 PRINT DBL(1) + DBL(10) + DBL(100)
|
||||
180 REM Depth answers to the pool, so runaway recursion reports
|
||||
190 REM "Environment pool exhausted" rather than hanging. It is not exercised
|
||||
200 REM here because a failed call's statement still prints a junk value
|
||||
210 REM afterwards -- a TODO.md defect this file would pin
|
||||
220 REM if it went in. tests/user_functions.c asserts
|
||||
180 REM Depth answers to the environment pool now, so runaway recursion reports
|
||||
190 REM "Environment pool exhausted" rather than hanging. It is not exercised here
|
||||
200 REM because the statement containing a failed call still prints a junk value
|
||||
210 REM afterwards -- a separate, pre-existing defect recorded in TODO.md, and one
|
||||
220 REM this golden file would pin if it went in. tests/user_functions.c asserts
|
||||
230 REM the message instead.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
10 REM A range check reported through the driver. The program stops
|
||||
10 REM A range check reported through the driver, end to end. The program stops
|
||||
20 REM at the first error, so this file covers one; the rest of the checks are
|
||||
30 REM asserted in tests/graphics_verbs.c against the recording backend.
|
||||
40 PRINT "BEFORE"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
10 REM The standalone driver lends the script no graphics device.
|
||||
20 REM COLOR, LOCATE and SCALE touch interpreter state and must still work.
|
||||
20 REM COLOR, LOCATE and SCALE only touch interpreter state and must still work.
|
||||
30 COLOR 1, 3
|
||||
40 LOCATE 40, 50
|
||||
50 SCALE 1, 640, 400
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
10 REM Group B: housekeeping verbs. None is in the Go reference --
|
||||
20 REM they are on its unimplemented list -- so what each one means here is
|
||||
10 REM Group B: the housekeeping verbs. None of these is in the Go reference --
|
||||
20 REM they are on its own unimplemented list -- so what each one means here is
|
||||
30 REM a decision, recorded in src/runtime_housekeeping.c beside the verb.
|
||||
40 A# = 1 : B# = 2
|
||||
50 PRINT A# : PRINT B#
|
||||
@@ -11,7 +11,7 @@
|
||||
110 P#(2) = 7 : Q#(2) = 9
|
||||
120 SWAP P#, Q#
|
||||
130 PRINT P#(2) : PRINT Q#(2)
|
||||
140 REM TRON prints each line number inline, as a C128 does.
|
||||
140 REM TRON prints each line number inline before the line runs, as a C128 does.
|
||||
150 TRON
|
||||
160 PRINT "TRACED"
|
||||
170 TROFF
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
10 REM A leading zero is padding, not a radix. The reference selects base 8;
|
||||
20 REM 010 printed 8 and 08 was a parse error -- TODO.md section 6 item 10.
|
||||
30 REM Commodore BASIC has no octal literals.
|
||||
40 REM 0x is the one prefix that changes the base and reaches the scanner
|
||||
10 REM A leading zero is padding, not a radix. The reference selects base 8 for
|
||||
20 REM any lexeme starting with 0, so 010 printed 8 and 08 was a parse error --
|
||||
30 REM TODO.md section 6 item 10, fixed. Commodore BASIC has no octal literals.
|
||||
40 REM 0x is the one prefix that changes the base, and it now reaches the scanner
|
||||
50 REM whole: that was section 6 item 15.
|
||||
60 PRINT 010
|
||||
70 PRINT 08
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
10 REM A truth value carries its payload in boolvalue, not floatval. The three
|
||||
20 REM numeric operators were `if ( INTEGER ) ... else <treat as float>`, and
|
||||
30 REM that else was a catch-all, not a float branch -- so a truth value
|
||||
30 REM that else was a catch-all rather than a float branch -- so a truth value
|
||||
40 REM on the LEFT computed 0 - 1 into a field nothing reads, kept its BOOLEAN
|
||||
50 REM type, and printed `true` instead of -2. Silent, and wrong twice over.
|
||||
60 A# = 1
|
||||
70 PRINT (A# == 1)
|
||||
80 REM On the RIGHT it stays legal and stays -1, the same property that
|
||||
90 REM lets AND and OR double as logical operators. Refusing it would have
|
||||
80 REM On the RIGHT it stays legal and stays -1, which is the same property that
|
||||
90 REM lets AND and OR double as logical operators. Refusing it here would have
|
||||
100 REM broken every condition in the language.
|
||||
110 PRINT 5 - (A# == 1)
|
||||
120 PRINT 5 * (A# == 1)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
10 REM Statements separated by colons. The COLON token existed from the start,
|
||||
20 REM but nothing consumed it, so a line could hold only one statement.
|
||||
10 REM Statements separated by colons. The COLON token existed from the start of
|
||||
20 REM the port and nothing consumed it, so a line could hold only one statement.
|
||||
30 PRINT "A" : PRINT "B"
|
||||
40 A# = 1 : B# = 2 : PRINT A# + B#
|
||||
50 REM An empty statement is no error: a trailing separator or run of them.
|
||||
50 REM An empty statement is not an error: a trailing separator, or a run of them.
|
||||
60 PRINT "C" :
|
||||
70 PRINT "D" :: PRINT "E"
|
||||
80 REM Everything after THEN belongs to the condition, as in BASIC 7.0, and is
|
||||
80 REM Everything after THEN belongs to the condition, which is BASIC 7.0 and is
|
||||
90 REM not something the reference had an opinion about -- it never got here.
|
||||
100 IF 1 == 1 THEN PRINT "TRUE-1" : PRINT "TRUE-2"
|
||||
110 IF 1 == 0 THEN PRINT "NEVER-1" : PRINT "NEVER-2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
10 REM A field name is checked against the type's closed set, which
|
||||
20 REM is the one thing here whose valid spellings are written down.
|
||||
10 REM A field name is checked against a closed set the program declared, which
|
||||
20 REM is the one thing in this language whose valid spellings are written down.
|
||||
30 REM A misspelled *variable* is still silent -- see the last two lines.
|
||||
40 TYPE RECT
|
||||
50 W#
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
170 RETURN B@.W#
|
||||
180 PRINT WIDEN(A@)
|
||||
190 PRINT A@.W#
|
||||
200 REM To change one, pass a pointer. Assignment copies the pointer.
|
||||
200 REM To change one on purpose, pass a pointer. Assignment copies a pointer's
|
||||
210 REM reference, so the callee is looking at the caller's own record.
|
||||
220 DIM Q@ AS PTR TO CRATE
|
||||
230 POINT Q@ AT A@
|
||||
@@ -37,7 +37,7 @@
|
||||
370 N2@.COUNT# = 20
|
||||
380 POINT N1@.TAIL@ AT N2@
|
||||
390 DEF TOTAL(P@ AS PTR TO NODE)
|
||||
395 REM A pointer is true when it points at something, so a walk knows
|
||||
395 REM A pointer is true when it points at something, which is how a walk knows
|
||||
396 REM where the list ends. NOT is the bitwise operator here, so the test is
|
||||
397 REM written the positive way round.
|
||||
400 IF P@->TAIL@ THEN RETURN P@->COUNT# + TOTAL(P@->TAIL@)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
10 REM A TYPE declares fields; each takes its type from its own suffix,
|
||||
10 REM A TYPE declares its fields; each takes its own type from its own suffix,
|
||||
20 REM which is the same rule every other name in this language follows.
|
||||
30 TYPE COORD
|
||||
40 X#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
10 REM A type name and a field name are bare words, and so is every verb, so
|
||||
20 REM they share a namespace whether we like it or not. Both are refused with
|
||||
30 REM the same rule the scanner applies to variable names -- and a type
|
||||
30 REM the same rule the scanner already applies to variable names -- and a type
|
||||
40 REM name is refused by the prescan, which can say so plainly rather than
|
||||
50 REM leaving the parser to report "Expected expression or literal".
|
||||
60 TYPE POINT
|
||||
|
||||
63
tests/reference/README.md
Normal file
63
tests/reference/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# The reference interpreter's own test corpus
|
||||
|
||||
These 41 `.bas` files and their 41 `.txt` expectations are **not this project's tests.** They
|
||||
are a byte-for-byte copy of the acceptance corpus belonging to the Go implementation this
|
||||
interpreter was ported from:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | `https://source.starfort.tech/andrew/basicinterpreter`, `tests/` |
|
||||
| Copied at | commit `d76162cb37eeaccddae2cf6cab7af7bc8175d6d2` ("Update README.md") |
|
||||
| Copied on | 2026-07-31 |
|
||||
| Verified | every file `cmp`-identical to the submodule at the time of the copy |
|
||||
|
||||
They are registered as the `golden_*` CTest cases and run against `basic` on every build, in
|
||||
both the default and the `-DAKBASIC_WITH_AKGL=ON` configurations.
|
||||
|
||||
## Why they are a copy, when they deliberately were not
|
||||
|
||||
Until 2026-07-31 these were driven *in place* out of `deps/basicinterpret`, and the reasoning
|
||||
was written down in `CMakeLists.txt`: the corpus is a submodule, and copying a submodule's
|
||||
corpus guarantees drift.
|
||||
|
||||
That reasoning was sound and it stopped applying. The Go project is deprecated and will not be
|
||||
updated, so there is nothing left to drift *from* — and a build that cannot run its own
|
||||
acceptance suite without cloning the implementation it replaced is not finished.
|
||||
|
||||
## What these are for now
|
||||
|
||||
**A regression suite, not a specification.** That distinction is the whole of how to treat
|
||||
them, and it changed on 2026-07-31 (`TODO.md` §0.1): matching the Go implementation is no
|
||||
longer a goal, so a difference between it and this interpreter is not automatically a bug in
|
||||
this one.
|
||||
|
||||
What they are still good for is unchanged and is worth a lot: forty-one real BASIC programs
|
||||
with known-good output, covering arithmetic, arrays, flow control, every builtin function,
|
||||
`READ`/`DATA`, labels and three worked examples. That catches regressions whatever its
|
||||
provenance.
|
||||
|
||||
## The rule for editing these
|
||||
|
||||
**Diverge deliberately, never by accident.**
|
||||
|
||||
A failure here means this interpreter's behaviour changed. Nine times in ten that is a bug you
|
||||
just introduced and the answer is to fix the code. The tenth time it is an improvement on the
|
||||
reference, and then the answer is:
|
||||
|
||||
1. Change the expectation in the same commit as the code.
|
||||
2. Say why in the commit message.
|
||||
3. Add a line to `TODO.md` §5, which is the list of deliberate deviations.
|
||||
|
||||
What is not acceptable is editing a `.txt` to make a red suite green without deciding which of
|
||||
the two you are doing.
|
||||
|
||||
New cases for this project's own behaviour go in `tests/language/`, registered under `local_*`.
|
||||
The prefix says which corpus a failure came from, and a diff touching `tests/reference/` stands
|
||||
out as the thing it is.
|
||||
|
||||
## Divergences so far
|
||||
|
||||
| Case | Change | Why |
|
||||
|---|---|---|
|
||||
| `language/arithmetic/integer.txt` | Expectation grew from `4 4 2` to `4 4 2 2` | The program has **four** `PRINT` statements and the expectation had three values. The file ends in a blank line, and both interpreters filed a blank line under the loader's cursor — which for a line with no number of its own is the number of the line before it. So `40 PRINT 4 - 2` was erased before the program ran. The reference lost it the same way, which is why the expectation was written short. Fixed by skipping blank lines in RUNSTREAM, as `akbasic_runtime_load()` and `DLOAD` already did. `TODO.md` §5. |
|
||||
| `examples/strreverse.bas` | Variable `INPUT$` renamed to `SOURCE$` | `INPUT$` is a reserved word with a type suffix, which a real C128 refuses and which this interpreter now refuses too (`TODO.md` §6 item 16). The reference accepted it only because its own reserved-word check never fired. The expectation is byte-for-byte unchanged — the program still prints `REVERSED: OLLEH` — so the case keeps all of its value. |
|
||||
@@ -1,6 +1,6 @@
|
||||
10 REM This shows the waitingForCommand utility in the BasicEnvironment
|
||||
11 REM when we have a nested for loop. The inner loop SHOULD execute, but
|
||||
12 REM the outer loop should NOT execute. Neither loop should execute.
|
||||
12 REM the outer loop should NOT execute. Therefore, neither loop should execute.
|
||||
20 FOR I# = 1 TO 0
|
||||
25 FOR J# = 2 TO 4
|
||||
30 PRINT "waitingForCommand FAILS if this is seen"
|
||||
@@ -1,6 +1,6 @@
|
||||
10 PRINT MOD(10, 3)
|
||||
20 PRINT MOD(12, 5)
|
||||
30 PRINT MOD(4, 2)
|
||||
40 REM MOD() ONLY WORKS WITH INTEGERS - FLOATING-POINT RESULTS ARE UNRELIABLE
|
||||
40 REM MOD() ONLY WORKS WITH INTEGERS - RESULTS WITH FLOATING POINT ARE UNRELIABLE
|
||||
50 REM PRINT MOD(1.2, 0.4)
|
||||
60 REM THERE IS NO ERROR THROWN HERE. JUST DONT DO IT.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user