2 Commits

Author SHA1 Message Date
350deb3a45 Merge pull request 'Move BASIC fixtures into the editable language corpus' (#42) from 40 into main
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 4m2s
akbasic CI Build / sanitizers (push) Successful in 4m59s
akbasic CI Build / coverage (push) Successful in 4m35s
akbasic CI Build / akgl_build (push) Successful in 9m17s
akbasic CI Build / mutation_test (push) Successful in 29m15s
Reviewed-on: #42
2026-08-04 16:30:25 -04:00
8a02674af5 Move BASIC fixtures into the editable language corpus
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m34s
akbasic CI Build / coverage (push) Successful in 4m4s
akbasic CI Build / sanitizers (push) Successful in 6m59s
akbasic CI Build / akgl_build (push) Successful in 7m57s
akbasic CI Build / mutation_test (push) Successful in 23m28s
Move every program and expectation out of tests/reference and register the unified tests/language corpus as local cases. Remove the old immutable-corpus protections from build, maintenance, and documentation paths.

Co-authored-by: andrew <andrew@aklabs.net>
2026-08-04 16:22:16 -04:00
105 changed files with 134 additions and 390 deletions

View File

@@ -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/reference/ and assets/fonts/), so
# this repository (tests/language/ 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,11 +62,9 @@ jobs:
run: |
cmake -S . -B build
cmake --build build --parallel 2
# 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
# 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
# section 6). A green run therefore does not mean defect-free -- see
# AKBASIC_KNOWN_FAILING_TESTS.
#

View File

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

View File

@@ -448,7 +448,7 @@ if(AKBASIC_WITH_AKGL)
# against.
#
# **A byte comparison of a rendered PNG is a deliberate bet**, the same bet
# tests/reference/ already makes about golden output: that the dummy video
# the language corpus 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
@@ -531,63 +531,20 @@ if(AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
)
endif()
# The reference's own corpus, byte-compared against the sibling .txt. One CTest
# case per .bas so a failure names the file.
# 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.
#
# 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.
# 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.
#
# 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.
# 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.
#
# 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

View File

@@ -88,7 +88,7 @@ source stays readable as documentation of what the original did. Neither is bind
**It is not a build or test dependency.** Both configurations have been configured, built and
run from scratch with it moved out of the tree. Its acceptance corpus is checked in at
`tests/reference/` and its Commodore font at `assets/fonts/`.
`tests/language/` and its Commodore font at `assets/fonts/`.
```sh norun
cd deps/basicinterpret
@@ -351,15 +351,10 @@ name. That is not cosmetic: `add_executable` creates a dependency's targets even
### The golden corpora
`tests/reference/` is the Go implementation's own acceptance suite, byte-compared.
**Nothing in it is ever edited to suit this interpreter.** If a case fails, either this
interpreter is wrong or the divergence is deliberate — and a deliberate one goes in
`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.
`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.
### Mutation-check a fix before you believe it

View File

@@ -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/reference/`](tests/reference/README.md) and runs on every build — so nothing about
[`tests/language/`](tests/language/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/reference/language/functions.bas # run a program
./build/basic tests/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: eighteen chapters, the language then each hardware area then a reference section for every verb and function, [Chapter 14](docs/14-architecture.md) on the interpreter's own architecture, [Chapter 15](docs/15-error-codes.md) listing every error code, and [Chapters 17](docs/17-tutorial-breakout.md) and [18](docs/18-tutorial-breakout-artwork.md) building a whole game twice |
| [`MAINTENANCE.md`](MAINTENANCE.md) | For contributors and maintainers: the documentation-example harness, the three test lists, mutation testing, error-code allocation, style |
| [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence |
| [`tests/reference/README.md`](tests/reference/README.md) | Where the golden corpus came from, and the rule for changing it |
| [`tests/language/README.md`](tests/language/README.md) | The editable language corpus and the rule for changing it |
API documentation builds with `doxygen Doxyfile`, into `build/docs/html`.

37
TODO.md
View File

@@ -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/reference/` becomes a regression suite rather than a specification.** Diverging from
- **`tests/language/` is the editable language corpus rather than a protected 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/reference/` was written against, so changing one means changing golden
expectation in `tests/language/` was written against, so changing one means changing the paired
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/reference/language/arithmetic/float.txt`
(Go's `%f` and C's `%f` both give six decimals — `tests/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/reference/README.md`). Everything else still matches, which is worth knowing but is no
`tests/language/README.md`). Everything else still matches, which is worth knowing but is no
longer a gate.
```sh
@@ -408,17 +408,16 @@ 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 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`
**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`
— 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
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.
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.
**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
@@ -1026,7 +1025,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/reference/README.md`.
did exactly that; see `tests/language/examples/strreverse.bas`.
### Deviations in statement separation
@@ -1391,10 +1390,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/reference/language/arithmetic/integer.bas` is the proof: four `PRINT`
the same, and `tests/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/reference/README.md` records it.
`tests/language/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
@@ -1586,9 +1585,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/reference/`, the expectation is byte-for-byte unchanged — the program
to `SOURCE$` in `tests/language/examples/strreverse.bas`, 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/reference/README.md`'s divergence table, which this is the first entry in.
`tests/language/README.md` records the corpus editing rule.
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
@@ -2232,10 +2231,10 @@ update --init --recursive` gets them.
## 8. Status
**The port is done.** The C interpreter passes the Go reference's entire corpus and passes clean
**The port is done.** The C interpreter passes the language 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/reference/README.md` lists them.
`tests/language/README.md` lists them.
| Gate | Result |
|---|---|
@@ -2243,7 +2242,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 |
| 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 |
| 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 |
| ASan + UBSan | 112/112 |
| Line coverage | 94.1% (7227/7681) — above the 90% gate |
| Function coverage | 97.9% (474/484) |
@@ -2553,7 +2552,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/reference/` already makes about golden
are reproducible, which is the same bet `tests/language/` 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
@@ -2636,7 +2635,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/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas`: a zero-
`tests/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

View File

@@ -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 golden_ # the reference corpus
./build/basic tests/reference/language/functions.bas | diff - tests/reference/language/functions.txt
ctest --test-dir build --output-on-failure -R local_ # the language corpus
./build/basic tests/language/functions.bas | diff - tests/language/functions.txt
./tests/docs_examples.sh --root . --basic ./build/basic \
--cflags-file build/docs_cflags.txt docs/14-architecture.md
```

View File

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

View File

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

View File

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

View File

@@ -211,12 +211,8 @@ typedef struct akbasic_Runtime
* not exist relative to the working directory, which is what makes a `.bas`
* beside its art work from anywhere. Group F's disk verbs will want the
* same, which is why it is on the runtime rather than in the sprite state.
*
* Sized to AKBASIC_MAX_SOURCE_PATH_LENGTH, not AKBASIC_MAX_LINE_LENGTH: a
* directory is a filesystem path, not a line of BASIC, and the two do not
* belong to the same budget.
*/
char sourcepath[AKBASIC_MAX_SOURCE_PATH_LENGTH];
char sourcepath[AKBASIC_MAX_LINE_LENGTH];
/*
* The armed interrupts, and the environment the one currently running was
@@ -369,7 +365,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_ui(akbasic_Runtime *obj,
* @param path Path to the program file, or NULL for none.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_SOURCE_PATH_LENGTH.
* @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_LINE_LENGTH.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path);

View File

@@ -23,17 +23,9 @@
* serves variables, functions and labels. Capacity is a member; the table is
* kept below a 75% load factor by construction because `capacity` counts slots
* and the caller's logical maximum is smaller.
*
* 172 rather than the old 256: no caller passes akbasic_symtab_init() anything
* larger than AKBASIC_MAX_VARIABLES (128), and 172 keeps that under the 75%
* load factor the comment above promises (128 / 0.75 = 170.7).
*
* AKBASIC_SYMTAB_MAX_KEY 24 rather than 64: the longest identifier across
* examples/breakout and examples/megademo -- variables, DIMmed arrays, labels
* and DEF FN names alike -- is 11 characters (TITLESCREEN, CLEARPOWERS).
*/
#define AKBASIC_SYMTAB_MAX_SLOTS 172
#define AKBASIC_SYMTAB_MAX_KEY 24
#define AKBASIC_SYMTAB_MAX_SLOTS 256
#define AKBASIC_SYMTAB_MAX_KEY 64
typedef struct
{

View File

@@ -6,7 +6,6 @@
#ifndef _AKBASIC_TYPES_H_
#define _AKBASIC_TYPES_H_
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
@@ -28,39 +27,15 @@
#define AKBASIC_MAX_VALUES 64
#define AKBASIC_MAX_VARIABLES 128
/*
* Whole-runtime pools.
*
* AKBASIC_MAX_SOURCE_LINES, AKBASIC_MAX_LINE_LENGTH, AKBASIC_MAX_ARRAY_VALUES,
* AKBASIC_MAX_ENVIRONMENTS and AKBASIC_MAX_FUNCTIONS were cut from their
* original values against measurements taken off examples/breakout and
* examples/megademo, the two most demanding programs this interpreter runs --
* see the memory-footprint discussion this commit's PR body links. Each is
* sized at roughly 1.5-2x the peak the reference corpus actually reaches, not
* at the peak itself.
*
* AKBASIC_MAX_LINE_LENGTH in particular follows Commodore BASIC's own 80-column
* line limit rather than a measurement, which is why sink_stdio.c now refuses a
* line that fills the buffer with no terminator instead of silently truncating
* it: at 256 bytes that failure mode was theoretical, and at 80 it is not.
*
* AKBASIC_MAX_SOURCE_PATH_LENGTH is its own constant rather than a reuse of
* AKBASIC_MAX_LINE_LENGTH, which is where it used to come from. `sourcepath`
* (runtime.h) holds a directory, not a line of BASIC, and the two ideas do not
* scale together: shrinking the line limit to 80 broke every golden test in
* this checkout, because this repository's own working directory is deeper
* than that. PATH_MAX is the actual bound a filesystem path is subject to, so
* it is the one this borrows.
*/
#define AKBASIC_MAX_SOURCE_LINES 2048
#define AKBASIC_MAX_LINE_LENGTH 80 /* Commodore BASIC's own line limit */
#define AKBASIC_MAX_SOURCE_PATH_LENGTH PATH_MAX
/* Whole-runtime pools */
#define AKBASIC_MAX_SOURCE_LINES 9999
#define AKBASIC_MAX_LINE_LENGTH 256
#define AKBASIC_MAX_ARRAY_DEPTH 64 /* dimensions per array */
#define AKBASIC_MAX_ARRAY_ELEMENTS 1024 /* elements in one array */
#define AKBASIC_MAX_ARRAY_VALUES 2048 /* array elements across all variables */
#define AKBASIC_MAX_ARRAY_VALUES 4096 /* array elements across all variables */
#define AKBASIC_MAX_STRING_LENGTH 256 /* see TODO.md 1.2 */
#define AKBASIC_MAX_ENVIRONMENTS 12 /* new: Go allocated these unbounded */
#define AKBASIC_MAX_FUNCTIONS 8 /* new: Go used an unbounded map */
#define AKBASIC_MAX_ENVIRONMENTS 32 /* new: Go allocated these unbounded */
#define AKBASIC_MAX_FUNCTIONS 64 /* new: Go used an unbounded map */
#define AKBASIC_MAX_LABELS 64 /* new: Go used an unbounded map */
/*
* Leaves a DO/LOOP condition may use. Its own small pool rather than a second

View File

@@ -13,22 +13,12 @@
#include <akerror.h>
#include <akbasic/symtab.h>
#include <akbasic/types.h>
#include <akbasic/value.h>
typedef struct
{
/*
* Sized to AKBASIC_SYMTAB_MAX_KEY, not AKBASIC_MAX_STRING_LENGTH: this name
* only ever gets here by surviving akbasic_symtab_set() first
* (akbasic_environment_create() calls it right after this field is
* populated), and that call refuses anything AKBASIC_SYMTAB_MAX_KEY
* characters or longer with AKBASIC_ERR_BOUNDS. A variable whose name did
* not fit could never exist, so the wider buffer was 232 bytes of headroom
* nothing could ever put a byte into.
*/
char name[AKBASIC_SYMTAB_MAX_KEY];
char name[AKBASIC_MAX_STRING_LENGTH];
akbasic_Type valuetype;
akbasic_Value *values; /** The pool, or `inlinevalue` for a scalar */
int valuecount;

View File

@@ -282,9 +282,8 @@ 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 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.
# libakstdlib in with add_subdirectory. The language corpus is the other
# reason it 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

View File

@@ -301,7 +301,7 @@ akerr_ErrorContext *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const
}
FAIL_ZERO_RETURN(errctx, (length < sizeof(obj->sourcepath)), AKBASIC_ERR_BOUNDS,
"Program path of %zu characters exceeds the %d character limit",
length, AKBASIC_MAX_SOURCE_PATH_LENGTH - 1);
length, AKBASIC_MAX_LINE_LENGTH - 1);
PASS(errctx, aksl_memcpy(obj->sourcepath, path, length));
obj->sourcepath[length] = '\0';
SUCCEED_RETURN(errctx);
@@ -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/reference/.../nestedforloopwaiting
* orphan is load-bearing: `tests/language/.../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
@@ -1262,7 +1262,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/reference/language/arithmetic/integer.bas`
* reference did the same, and `tests/language/arithmetic/integer.bas`
* has an expectation with three values for four PRINT statements to prove it.
*/
if ( buffer[0] == '\0' ) {

View File

@@ -74,19 +74,6 @@ static akerr_ErrorContext *stdio_readline(akbasic_TextSink *self, char *dest, si
if ( *eof ) {
SUCCEED_RETURN(errctx);
}
/*
* aksl_fgets(3)'s own contract: a full buffer with no trailing newline is
* how a caller spots a line longer than the buffer, because the rest of it
* is still sitting unread in the stream. Refusing here is what makes that
* true -- without it, the unread remainder is picked up by the *next*
* readline() as if it were its own statement, which does not fail, it just
* runs the wrong program. AKBASIC_MAX_LINE_LENGTH is small enough now that
* this is not a hypothetical: examples/breakout's own longest line used to
* clear the old 256-byte ceiling by more than half.
*/
FAIL_NONZERO_RETURN(errctx, (used == len - 1 && dest[used - 1] != '\n' && dest[used - 1] != '\r'),
AKBASIC_ERR_BOUNDS,
"Source line exceeds the %zu character limit", len - 1);
/*
* Strip the line terminator. The scanner treats \r and \n as end-of-line
* anyway, but leaving them on would make a stored source line differ from

View File

@@ -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/reference/language/flowcontrol/forloopwaitingforcommand.bas pins that,
* tests/language/flowcontrol/forloopwaitingforcommand.bas pins that,
* which is why this cannot simply be corrected.
*/
static void test_single_iteration_loop(void)

16
tests/language/README.md Normal file
View File

@@ -0,0 +1,16 @@
# 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.

Some files were not shown because too many files have changed in this diff Show More