Compare commits
3 Commits
40
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
eb93bb7da0
|
|||
|
1e514f679b
|
|||
|
17af2d406c
|
@@ -17,7 +17,7 @@ jobs:
|
|||||||
# Not recursive, deliberately. The top-level build needs
|
# Not recursive, deliberately. The top-level build needs
|
||||||
# deps/libakerror and deps/libakstdlib, via add_subdirectory, and
|
# deps/libakerror and deps/libakstdlib, via add_subdirectory, and
|
||||||
# nothing else: the golden corpus and the Commodore font now live in
|
# 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.
|
# deps/basicinterpret is no longer a build dependency at all.
|
||||||
# It does *not* need deps/libakgl, which is guarded behind
|
# It does *not* need deps/libakgl, which is guarded behind
|
||||||
# AKBASIC_WITH_AKGL and defaults OFF -- and recursing into it would
|
# AKBASIC_WITH_AKGL and defaults OFF -- and recursing into it would
|
||||||
@@ -62,9 +62,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cmake -S . -B build
|
cmake -S . -B build
|
||||||
cmake --build build --parallel 2
|
cmake --build build --parallel 2
|
||||||
# The suite is 112 cases: 65 language files with sibling expectations,
|
# The suite is 78 cases: 41 golden files byte-compared against the Go
|
||||||
# 43 unit tests, 3 embedding examples, and docs_examples.
|
# reference's own corpus (checked in at tests/reference/, see its README),
|
||||||
# Some unit tests assert the *correct* contract for known defects (TODO.md
|
# 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
|
# section 6). A green run therefore does not mean defect-free -- see
|
||||||
# AKBASIC_KNOWN_FAILING_TESTS.
|
# 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.
|
`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
|
- **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.
|
and there is no repo-wide formatter; style conversions get their own commit.
|
||||||
- **Keep `tests/language/` editable.** Its `.bas` programs and sibling `.txt` expectations
|
- **Do not edit `tests/reference/`.** Those expectations came from the Go implementation and
|
||||||
are changed together when behavior changes. Record deliberate language decisions in
|
are never edited to suit this interpreter. A deliberate divergence goes in
|
||||||
`TODO.md` or `docs/13-differences.md`.
|
`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`.**
|
- **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
|
<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
|
andrew/akbasic`. Name the file and line, the functional consequence, and what closing it would
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ if(AKBASIC_WITH_AKGL)
|
|||||||
# against.
|
# against.
|
||||||
#
|
#
|
||||||
# **A byte comparison of a rendered PNG is a deliberate bet**, the same bet
|
# **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
|
# 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
|
# 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
|
# a diagonal, and the answer to that is to regenerate the figures in the same
|
||||||
@@ -531,20 +531,63 @@ if(AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The editable language corpus. One CTest case per .bas so a failure names the
|
# The reference's own corpus, byte-compared against the sibling .txt. One CTest
|
||||||
# file. Each program is paired with a sibling .txt expectation; see
|
# case per .bas so a failure names the file.
|
||||||
# tests/language/README.md for the editing rule.
|
|
||||||
#
|
#
|
||||||
# The corpus includes programs carried over from the deprecated Go implementation
|
# It used to be driven in place out of deps/basicinterpret, on the reasoning that
|
||||||
# and cases written for this interpreter. Their provenance is useful when
|
# copying a submodule's corpus guarantees drift. That reasoning was sound and it
|
||||||
# investigating a regression, but it does not make any case immutable.
|
# 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
|
# Still separate now that the reference's corpus lives in this repository too,
|
||||||
# for this interpreter use the same editable `.bas`/`.txt` contract. Registered
|
# and the reason changed rather than went away: tests/reference/ is a *record* of
|
||||||
# under local_ so every failure identifies the program that produced it.
|
# 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
|
# 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
|
# whatever a program can PRINT about the state they changed. The behaviour that
|
||||||
# reaches a device is asserted against tests/mockdevice.h instead.
|
# reaches a device is asserted against tests/mockdevice.h instead.
|
||||||
file(GLOB_RECURSE AKBASIC_LOCAL_CASES
|
file(GLOB_RECURSE AKBASIC_LOCAL_CASES
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ source stays readable as documentation of what the original did. Neither is bind
|
|||||||
|
|
||||||
**It is not a build or test dependency.** Both configurations have been configured, built and
|
**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
|
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
|
```sh norun
|
||||||
cd deps/basicinterpret
|
cd deps/basicinterpret
|
||||||
@@ -351,10 +351,15 @@ name. That is not cosmetic: `add_executable` creates a dependency's targets even
|
|||||||
|
|
||||||
### The golden corpora
|
### The golden corpora
|
||||||
|
|
||||||
`tests/language/` is the editable language corpus. It includes cases carried over from the
|
`tests/reference/` is the Go implementation's own acceptance suite, byte-compared.
|
||||||
deprecated Go implementation as well as cases written for this interpreter. Every `.bas` file
|
**Nothing in it is ever edited to suit this interpreter.** If a case fails, either this
|
||||||
has a sibling `.txt` expectation, and a new language feature needs that pair as well as unit
|
interpreter is wrong or the divergence is deliberate — and a deliberate one goes in
|
||||||
tests. Change both deliberately in the same commit; provenance does not make a case immutable.
|
`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
|
### Mutation-check a fix before you believe it
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ implementation that started from the Java Lox instructions in
|
|||||||
[craftinginterpreters.com](https://craftinginterpreters.com) and then struck off on its own. That
|
[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
|
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
|
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.
|
building or testing this project needs it.
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
@@ -24,7 +24,7 @@ ctest --test-dir build --output-on-failure
|
|||||||
|
|
||||||
```sh norun
|
```sh norun
|
||||||
./build/basic # the REPL
|
./build/basic # the REPL
|
||||||
./build/basic tests/language/functions.bas # run a program
|
./build/basic tests/reference/language/functions.bas # run a program
|
||||||
```
|
```
|
||||||
|
|
||||||
```basic
|
```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 |
|
| [`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 |
|
| [`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 |
|
| [`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`.
|
API documentation builds with `doxygen Doxyfile`, into `build/docs/html`.
|
||||||
|
|
||||||
|
|||||||
37
TODO.md
37
TODO.md
@@ -38,7 +38,7 @@ What it changes:
|
|||||||
- **§1.8's message-text contract is now a convention.** Improving a message is allowed; it costs
|
- **§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.
|
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.
|
- **§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.
|
it is allowed and must be deliberate and recorded — see its README.
|
||||||
|
|
||||||
What it does **not** change:
|
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
|
**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
|
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
|
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
|
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,
|
awkwardly *may* now be improved; do it deliberately, move the expectations in the same commit,
|
||||||
and add a line to §5.
|
and add a line to §5.
|
||||||
|
|
||||||
Numeric formatting still matches the reference: integers via `%" PRId64 "`, floats via `%f`
|
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.
|
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**
|
### 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:
|
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
|
§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.
|
longer a gate.
|
||||||
|
|
||||||
```sh
|
```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
|
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.
|
signature change breaks the build rather than rotting the document.
|
||||||
|
|
||||||
**The acceptance suite is the editable language corpus, checked in at `tests/language/`.** All
|
**The acceptance suite is the reference's own corpus, checked in at `tests/reference/`.** All
|
||||||
65 `.bas` files are registered as individual CTest cases and compared against their `.txt`
|
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).
|
— 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
|
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
|
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
|
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
|
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
|
byte-identical to `basicinterpreter@d76162c`, and `tests/reference/README.md` records the
|
||||||
provenance and the rule that programs and expectations are edited together deliberately.
|
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
|
**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
|
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
|
**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
|
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
|
### 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
|
**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
|
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
|
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
|
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
|
`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
|
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
|
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.**
|
**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
|
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
|
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 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
|
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
|
## 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
|
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
|
requirement; two cases have diverged on purpose since, and
|
||||||
`tests/language/README.md` lists them.
|
`tests/reference/README.md` lists them.
|
||||||
|
|
||||||
| Gate | Result |
|
| 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 |
|
| `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_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 |
|
| `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 |
|
| ASan + UBSan | 112/112 |
|
||||||
| Line coverage | 94.1% (7227/7681) — above the 90% gate |
|
| Line coverage | 94.1% (7227/7681) — above the 90% gate |
|
||||||
| Function coverage | 97.9% (474/484) |
|
| Function coverage | 97.9% (474/484) |
|
||||||
@@ -2552,7 +2553,7 @@ What remains, in priority order:
|
|||||||
this whole arrangement exists against.
|
this whole arrangement exists against.
|
||||||
|
|
||||||
The byte comparison is a deliberate bet that the dummy driver and the software renderer
|
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
|
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
|
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
|
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
|
**It is narrower than it looks, and the golden corpus is why.** The first attempt
|
||||||
released the scope on *any* skip, which broke
|
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
|
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
|
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
|
`FOR`. Releasing it turns that case into "NEXT outside the context of FOR". So the
|
||||||
|
|||||||
@@ -743,8 +743,8 @@ way to see that something pushed a scope and never popped it.
|
|||||||
|
|
||||||
```sh norun
|
```sh norun
|
||||||
ctest --test-dir build --output-on-failure -R for_next # one unit test
|
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
|
ctest --test-dir build --output-on-failure -R golden_ # the reference corpus
|
||||||
./build/basic tests/language/functions.bas | diff - tests/language/functions.txt
|
./build/basic tests/reference/language/functions.bas | diff - tests/reference/language/functions.txt
|
||||||
./tests/docs_examples.sh --root . --basic ./build/basic \
|
./tests/docs_examples.sh --root . --basic ./build/basic \
|
||||||
--cflags-file build/docs_cflags.txt docs/14-architecture.md
|
--cflags-file build/docs_cflags.txt docs/14-architecture.md
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -685,9 +685,9 @@ seconds asks for fifty frames a second.
|
|||||||
### Why `GOTO` rather than `DO ... LOOP`
|
### Why `GOTO` rather than `DO ... LOOP`
|
||||||
|
|
||||||
A `DO ... LOOP` around the frame would read better, and it is not usable here: **a `GOTO`
|
A `DO ... LOOP` around the frame would read better, and it is not usable here: **a `GOTO`
|
||||||
that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 32
|
that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 12
|
||||||
scopes, so a game that leaves its main loop once per lost life stops on the
|
scopes, so a game that leaves its main loop once per lost life stops on the
|
||||||
thirty-second one:
|
twelfth one:
|
||||||
|
|
||||||
```basic
|
```basic
|
||||||
N# = 0
|
N# = 0
|
||||||
@@ -700,7 +700,7 @@ PRINT "SURVIVED " + N#
|
|||||||
```
|
```
|
||||||
|
|
||||||
```output
|
```output
|
||||||
? 3 : PARSE ERROR Environment pool exhausted at line 3 (32 in use)
|
? 3 : PARSE ERROR Environment pool exhausted at line 3 (12 in use)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1423,7 +1423,8 @@ IF STATE# = 2 THEN GOSUB UNSTICK
|
|||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
LABEL PRESSPAUSE
|
LABEL PRESSPAUSE
|
||||||
IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN
|
IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED"
|
||||||
|
IF STATE# = 6 THEN GOSUB SETBANNER : RETURN
|
||||||
IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER
|
IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER
|
||||||
RETURN
|
RETURN
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ This is a tour of the interpreter's edges, on purpose:
|
|||||||
counterpoint: a genuine 24x21 `SPRSAV` type-in sprite, 63 bytes, which is
|
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
|
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
|
stride-63 column dissolve — 63 is coprime to 160, so the walk hits every
|
||||||
column once and looks random while carrying no state.
|
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.
|
||||||
- **And then the picture is video.** Six delta frames loop the floor grid toward
|
- **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
|
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`
|
220 bytes a frame. A delta re-encodes only the rows that changed: an `R`
|
||||||
@@ -82,6 +85,10 @@ 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
|
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
|
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.
|
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
|
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
|
device: no graphics refuses by name and exits, no audio mutes the soundtrack, no
|
||||||
|
|||||||
@@ -461,38 +461,78 @@ BS% = W# / 160
|
|||||||
IF BS% < 1 THEN BS% = 1
|
IF BS% < 1 THEN BS% = 1
|
||||||
REM ---- PICTURE-BEGIN (generated by vaporwave.py; do not
|
REM ---- PICTURE-BEGIN (generated by vaporwave.py; do not
|
||||||
REM ---- hand-edit -- rerun the script to change the picture)
|
REM ---- hand-edit -- rerun the script to change the picture)
|
||||||
DIM IM$(16)
|
DIM IM$(56)
|
||||||
DIM VA#(6)
|
DIM VA#(6)
|
||||||
DIM VB#(6)
|
DIM VB#(6)
|
||||||
NS# = 9
|
NS# = 32
|
||||||
VA#(0) = 9
|
VA#(0) = 32
|
||||||
VB#(0) = 9
|
VB#(0) = 35
|
||||||
VA#(1) = 10
|
VA#(1) = 36
|
||||||
VB#(1) = 10
|
VB#(1) = 39
|
||||||
VA#(2) = 11
|
VA#(2) = 40
|
||||||
VB#(2) = 12
|
VB#(2) = 43
|
||||||
VA#(3) = 13
|
VA#(3) = 44
|
||||||
VB#(3) = 13
|
VB#(3) = 47
|
||||||
VA#(4) = 14
|
VA#(4) = 48
|
||||||
VB#(4) = 14
|
VB#(4) = 51
|
||||||
VA#(5) = 15
|
VA#(5) = 52
|
||||||
VB#(5) = 15
|
VB#(5) = 55
|
||||||
IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEHG5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGIEHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEH"
|
IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEH"
|
||||||
IM$(1) = "GHEPGHEHGPEHGHEPGHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPECPAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEPGHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEP"
|
IM$(1) = "G5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGI"
|
||||||
IM$(2) = "GHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHAE9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAETEHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKHEHKAH9HKKHEHKHEP"
|
IM$(2) = "EHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEH"
|
||||||
IM$(3) = "KHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKHEHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKHEHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSEDKPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH"
|
IM$(3) = "GPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPEHGHEP"
|
||||||
IM$(4) = "KPEDH9HUKCEHK5EHK5EHKLH9HUKKEHK5EHK5EHKCH9HWEBKPEHKXKHEHK8I9IWKZEHKPKPEHK0I9IWK7EHKHKXEHKSI9IWK9KFEHK9KOI9IWK9KNK9KOI9IWK9KNK9KOI9IWK9KNKPIHKZI9IYK6IHKHKXIHKSI9IWK9KFIHK5IHKKI9IWK9KNIHK5IHKCI9IYKPIHKXKHIHK5IHKPIHK5IHKPIHKPKPIHK0I9IWKJIHKPIH"
|
IM$(4) = "GHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPEC"
|
||||||
IM$(5) = "KHIHKPIHKPI9IZKBIHKHIHKPIHKHIHKPIHKKI9IWKJIHKHIHKPIHKHIHKPIHKDI9IXKPIHKHIHKHKHIHKHIPKHI9IYKCIHKPIHKHIHKPIHKHIPKHIHKPIHKHIPKHIHKPIHKHIHKPIHKHI9I9IHKHIHKPIHKHIPKHIHKHIEC9CSKDIPKHIPKHIHKHIPKHIHKEC9CSIDKHIPKHIPIPKHIPKHIFC9CQKEIHKHIPKHIHIXKHIPKG"
|
IM$(5) = "PAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEP"
|
||||||
IM$(6) = "C9COIFKHI5KHI5KHIPKHI5KHIPKHI5KHI5KHIPKHI5KHIPKHIXIHKHI9IDC9CMI4KHIPIPKHI6C9CKI9IDKHIHIXKHIZC9CII9IMKHI9IWC9CGI9IVI9I9I9I9IPI9I9I9I9IPQ9Q9Q9Q9QPE9E9E9E9EPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9"
|
IM$(6) = "GHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEP"
|
||||||
IM$(7) = "Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9"
|
IM$(7) = "GHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEPGHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHA"
|
||||||
IM$(8) = "Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP"
|
IM$(8) = "E9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAET"
|
||||||
IM$(9) = "RBRQ9QOKMQHK5RBSQ9QTI9IRRBXQ9QTKPQHKHQPKERBYQ9QPI9IHRB3Q9QSKAI5KHIJRB5Q9QTC9CMRB9Q9QWI9IGRCBQ9QYC9CCRCIA9A9A9A9APRCJE9E9E9E9EPRCNA9A9A9A9APRCOE9E9E9E9EPRCUA9A9A9A9APRCVE9E9E9E9EPRC5A9A9A9A9APRC7E9E9E9E9EP"
|
IM$(9) = "EHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9"
|
||||||
IM$(10) = "RBQQ9QOK9KIQHKFRBRQ9QOI9IQRBWQ9QPKLQHKHQHKPRBXQ9QTI9QLIERB2Q9QRIBKHIPKHIPKCRB4Q9QSC9CORB8Q9QVI3KHIGRCAQ9QXC9CERCJA9A9A9A9APRCKE9E9E9E9EPRCOA9A9A9A9APRCPE9E9E9E9EPRCVA9A9A9A9APRCXE9E9E9E9EPRC7A9A9A9A9APRDAE9E9E9E9EP"
|
IM$(10) = "HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKH"
|
||||||
IM$(11) = "RBPQ9QOK9KAQHKNRBQQ9QOI9IWRBVQ9QOKEQHKHQHKPQHKFRBWQ9QPI9IPRB1Q9QQKCIPKHIPKHIDRB3Q9QSC9CORB7Q9QUIWKHIPRB9Q9QWC9CGRCEA9A9A9A9APRCFE9E9E9E9EPRCGA9A9A9A9APRCHE9E9E9E9EPRCPA9A9A9A9APRCQE9E9E9E9EPRCXA9A9A9A9APRCZE9E9E9E9EPRDAA9A9A9A9APRDCE9E9E9E9"
|
IM$(11) = "EHKAH9HKKHEHKHEPKHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKH"
|
||||||
IM$(12) = "EP"
|
IM$(12) = "EHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKH"
|
||||||
IM$(13) = "RBOQ9QNK3QHKWRBPQ9QOI9IWRBUQ9QTKHQHKPQHKNRBVQ9QOI9IWRB0Q9QQIKKHIPKHIHKDRB1Q9QQC9CSRB2Q9QRC9CQRB6Q9QTIPKHIYRB8Q9QVC9CIRCKA9A9A9A9APRCLE9E9E9E9EPRCQA9A9A9A9APRCRE9E9E9E9EPRCZA9A9A9A9APRC1E9E9E9E9EPRDCA9A9A9A9APRDFE9E9E9E9EP"
|
IM$(13) = "EHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSED"
|
||||||
IM$(14) = "RBOQ9QNI9IYRBTQ9QOKEQHKPQHKVRBUQ9QTI9IRRBZQ9QTKHQHKPQHKHRB0Q9QQC9CSRB5Q9QTIHKHI5KARB7Q9QUC9CKRCBQ9QYI9ICRCHA9A9A9A9APRCIE9E9E9E9EPRCLA9A9A9A9APRCME9E9E9E9EPRCRA9A9A9A9APRCTE9E9E9E9EPRC1A9A9A9A9APRC3E9E9E9E9EPRDFA9A9A9A9APRDIE9E9E9E9EP"
|
IM$(14) = "KPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH"
|
||||||
IM$(15) = "RBSQ9QTKPQHK3RBTQ9QOI9IWRBYQ9QPKDQHKPQHKHRBZQ9QTI9ILRB4Q9QSIAKHI5KHIBRB6Q9QTC9CMRCAQ9QXI9IERCEE9E9E9E9EPRCFA9A9A9A9APRCGE9E9E9E9EPRCMA9A9A9A9APRCNE9E9E9E9EPRCTA9A9A9A9APRCUE9E9E9E9EPRC3A9A9A9A9APRC5E9E9E9E9EPRDIA9A9A9A9AP"
|
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"
|
||||||
REM ---- PICTURE-END
|
REM ---- PICTURE-END
|
||||||
SS# = 0
|
SS# = 0
|
||||||
SE# = NS# - 1
|
SE# = NS# - 1
|
||||||
@@ -1317,13 +1357,25 @@ TB# = TB# + 1
|
|||||||
IF TB# > 2 THEN TB# = 0
|
IF TB# > 2 THEN TB# = 0
|
||||||
RETURN
|
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
|
LABEL TUNEA
|
||||||
PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B"
|
PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE"
|
||||||
|
PLAY "V1T3U9S O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A"
|
||||||
|
PLAY "V1T3U9S O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C"
|
||||||
|
PLAY "V1T3U9S O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B"
|
||||||
MT# = TI# + 270
|
MT# = TI# + 270
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
LABEL TUNEB
|
LABEL TUNEB
|
||||||
PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E"
|
PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A"
|
||||||
|
PLAY "V1T3U9S O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A"
|
||||||
|
PLAY "V1T3U9S O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D"
|
||||||
|
PLAY "V1T3U9S O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E"
|
||||||
MT# = TI# + 270
|
MT# = TI# + 270
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,14 @@ SKIP = "Q"
|
|||||||
ROWREC = "R"
|
ROWREC = "R"
|
||||||
LENCH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
LENCH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
MAXRUN = len(LENCH)
|
MAXRUN = len(LENCH)
|
||||||
PAYLOAD = 240
|
|
||||||
|
# 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
|
||||||
|
|
||||||
SUN_CX, SUN_CY, SUN_R = 80, 50, 30
|
SUN_CX, SUN_CY, SUN_R = 80, 50, 30
|
||||||
HORIZON = 74
|
HORIZON = 74
|
||||||
@@ -206,13 +213,32 @@ def encode_delta(prev, cur):
|
|||||||
|
|
||||||
|
|
||||||
def chop(blob):
|
def chop(blob):
|
||||||
return [blob[i:i + PAYLOAD] for i in range(0, len(blob), PAYLOAD)]
|
"""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
|
||||||
|
|
||||||
|
|
||||||
def simulate(raster, blob):
|
def simulate(raster, blob, x=0, y=0):
|
||||||
"""Apply one encoded stream to a raster exactly the way the BASIC
|
"""Apply one encoded stream to a raster exactly the way the BASIC
|
||||||
decoder does, skips-draw-nothing and all."""
|
decoder does, skips-draw-nothing and all. The cursor comes in and
|
||||||
x = y = p = 0
|
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
|
||||||
while p < len(blob):
|
while p < len(blob):
|
||||||
c = blob[p]
|
c = blob[p]
|
||||||
if c == ROWREC:
|
if c == ROWREC:
|
||||||
@@ -230,20 +256,31 @@ def simulate(raster, blob):
|
|||||||
x = 0
|
x = 0
|
||||||
y += 1
|
y += 1
|
||||||
p += 2
|
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
|
return raster
|
||||||
|
|
||||||
|
|
||||||
def verify(frames, base_blob, delta_blobs):
|
def verify(frames, base_lines, delta_line_groups):
|
||||||
"""The base must reproduce frame 0 exactly, and each delta must
|
"""The base must reproduce frame 0 exactly, and each delta must
|
||||||
carry the raster exactly to the next frame. A skip leaves the cell
|
carry the raster exactly to the next frame. A skip leaves the cell
|
||||||
the encoder promised was already right, so equality is total and
|
the encoder promised was already right, so equality is total and
|
||||||
any difference at all is an encoder bug."""
|
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."""
|
||||||
raster = [[1] * W for _ in range(H)]
|
raster = [[1] * W for _ in range(H)]
|
||||||
raster = simulate(raster, base_blob)
|
raster = simulate_lines(raster, base_lines)
|
||||||
assert raster == frames[0], "base stream does not reproduce frame 0"
|
assert raster == frames[0], "base stream does not reproduce frame 0"
|
||||||
for i, blob in enumerate(delta_blobs):
|
for i, lines in enumerate(delta_line_groups):
|
||||||
want = frames[(i + 1) % PHASES]
|
want = frames[(i + 1) % PHASES]
|
||||||
raster = simulate(raster, blob)
|
raster = simulate_lines(raster, lines)
|
||||||
assert raster == want, "delta %d does not reproduce its frame" % i
|
assert raster == want, "delta %d does not reproduce its frame" % i
|
||||||
|
|
||||||
|
|
||||||
@@ -261,6 +298,9 @@ def emit_block(base_lines, delta_ranges, all_lines):
|
|||||||
for i, s in enumerate(all_lines):
|
for i, s in enumerate(all_lines):
|
||||||
out.append('IM$(%d) = "%s"' % (i, s))
|
out.append('IM$(%d) = "%s"' % (i, s))
|
||||||
out.append("REM ---- PICTURE-END")
|
out.append("REM ---- PICTURE-END")
|
||||||
|
for line in out:
|
||||||
|
assert len(line) <= MAXLINE, "emitted line over %d chars: %r" % (
|
||||||
|
MAXLINE, line)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -303,15 +343,15 @@ def main():
|
|||||||
base_lines = chop(base_blob)
|
base_lines = chop(base_blob)
|
||||||
all_lines = list(base_lines)
|
all_lines = list(base_lines)
|
||||||
delta_ranges = []
|
delta_ranges = []
|
||||||
delta_blobs = []
|
delta_line_groups = []
|
||||||
for i in range(PHASES):
|
for i in range(PHASES):
|
||||||
blob = encode_delta(frames[i], frames[(i + 1) % PHASES])
|
blob = encode_delta(frames[i], frames[(i + 1) % PHASES])
|
||||||
delta_blobs.append(blob)
|
|
||||||
lines = chop(blob)
|
lines = chop(blob)
|
||||||
|
delta_line_groups.append(lines)
|
||||||
delta_ranges.append((len(all_lines), len(all_lines) + len(lines) - 1))
|
delta_ranges.append((len(all_lines), len(all_lines) + len(lines) - 1))
|
||||||
all_lines.extend(lines)
|
all_lines.extend(lines)
|
||||||
verify(frames, base_blob, delta_blobs)
|
verify(frames, base_lines, delta_line_groups)
|
||||||
dbytes = sum(len(b) for b in delta_blobs)
|
dbytes = sum(len(l) for g in delta_line_groups for l in g)
|
||||||
print("base %d bytes in %d strings; video %d bytes in %d strings; "
|
print("base %d bytes in %d strings; video %d bytes in %d strings; "
|
||||||
"%d strings total" %
|
"%d strings total" %
|
||||||
(sum(len(s) for s in base_lines), len(base_lines), dbytes,
|
(sum(len(s) for s in base_lines), len(base_lines), dbytes,
|
||||||
|
|||||||
@@ -211,8 +211,12 @@ typedef struct akbasic_Runtime
|
|||||||
* not exist relative to the working directory, which is what makes a `.bas`
|
* 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
|
* 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.
|
* 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_LINE_LENGTH];
|
char sourcepath[AKBASIC_MAX_SOURCE_PATH_LENGTH];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The armed interrupts, and the environment the one currently running was
|
* The armed interrupts, and the environment the one currently running was
|
||||||
@@ -365,7 +369,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_ui(akbasic_Runtime *obj,
|
|||||||
* @param path Path to the program file, or NULL for none.
|
* @param path Path to the program file, or NULL for none.
|
||||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||||
* @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_LINE_LENGTH.
|
* @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_SOURCE_PATH_LENGTH.
|
||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path);
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,17 @@
|
|||||||
* serves variables, functions and labels. Capacity is a member; the table is
|
* serves variables, functions and labels. Capacity is a member; the table is
|
||||||
* kept below a 75% load factor by construction because `capacity` counts slots
|
* kept below a 75% load factor by construction because `capacity` counts slots
|
||||||
* and the caller's logical maximum is smaller.
|
* 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 256
|
#define AKBASIC_SYMTAB_MAX_SLOTS 172
|
||||||
#define AKBASIC_SYMTAB_MAX_KEY 64
|
#define AKBASIC_SYMTAB_MAX_KEY 24
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#ifndef _AKBASIC_TYPES_H_
|
#ifndef _AKBASIC_TYPES_H_
|
||||||
#define _AKBASIC_TYPES_H_
|
#define _AKBASIC_TYPES_H_
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
@@ -27,15 +28,39 @@
|
|||||||
#define AKBASIC_MAX_VALUES 64
|
#define AKBASIC_MAX_VALUES 64
|
||||||
#define AKBASIC_MAX_VARIABLES 128
|
#define AKBASIC_MAX_VARIABLES 128
|
||||||
|
|
||||||
/* Whole-runtime pools */
|
/*
|
||||||
#define AKBASIC_MAX_SOURCE_LINES 9999
|
* Whole-runtime pools.
|
||||||
#define AKBASIC_MAX_LINE_LENGTH 256
|
*
|
||||||
|
* 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
|
||||||
#define AKBASIC_MAX_ARRAY_DEPTH 64 /* dimensions per array */
|
#define AKBASIC_MAX_ARRAY_DEPTH 64 /* dimensions per array */
|
||||||
#define AKBASIC_MAX_ARRAY_ELEMENTS 1024 /* elements in one array */
|
#define AKBASIC_MAX_ARRAY_ELEMENTS 1024 /* elements in one array */
|
||||||
#define AKBASIC_MAX_ARRAY_VALUES 4096 /* array elements across all variables */
|
#define AKBASIC_MAX_ARRAY_VALUES 2048 /* array elements across all variables */
|
||||||
#define AKBASIC_MAX_STRING_LENGTH 256 /* see TODO.md 1.2 */
|
#define AKBASIC_MAX_STRING_LENGTH 256 /* see TODO.md 1.2 */
|
||||||
#define AKBASIC_MAX_ENVIRONMENTS 32 /* new: Go allocated these unbounded */
|
#define AKBASIC_MAX_ENVIRONMENTS 12 /* new: Go allocated these unbounded */
|
||||||
#define AKBASIC_MAX_FUNCTIONS 64 /* new: Go used an unbounded map */
|
#define AKBASIC_MAX_FUNCTIONS 8 /* new: Go used an unbounded map */
|
||||||
#define AKBASIC_MAX_LABELS 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
|
* Leaves a DO/LOOP condition may use. Its own small pool rather than a second
|
||||||
|
|||||||
@@ -13,12 +13,22 @@
|
|||||||
|
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akbasic/symtab.h>
|
||||||
#include <akbasic/types.h>
|
#include <akbasic/types.h>
|
||||||
#include <akbasic/value.h>
|
#include <akbasic/value.h>
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
char name[AKBASIC_MAX_STRING_LENGTH];
|
/*
|
||||||
|
* 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];
|
||||||
akbasic_Type valuetype;
|
akbasic_Type valuetype;
|
||||||
akbasic_Value *values; /** The pool, or `inlinevalue` for a scalar */
|
akbasic_Value *values; /** The pool, or `inlinevalue` for a scalar */
|
||||||
int valuecount;
|
int valuecount;
|
||||||
|
|||||||
@@ -282,8 +282,9 @@ def copy_tree(src, dst):
|
|||||||
# and a coverage tree drags along every .gcno/.gcda as well.
|
# and a coverage tree drags along every .gcno/.gcda as well.
|
||||||
#
|
#
|
||||||
# deps/ is copied wholesale and has to be: the build pulls libakerror and
|
# deps/ is copied wholesale and has to be: the build pulls libakerror and
|
||||||
# libakstdlib in with add_subdirectory. The language corpus is the other
|
# libakstdlib in with add_subdirectory. The golden corpus used to be the
|
||||||
# reason it comes along with the rest of the tree.
|
# 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".
|
# Only vendored Windows DLLs are dead weight, hence "*.dll".
|
||||||
#
|
#
|
||||||
# **PNGs are ours or theirs, and the difference matters.** This used to drop
|
# **PNGs are ours or theirs, and the difference matters.** This used to drop
|
||||||
|
|||||||
@@ -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,
|
FAIL_ZERO_RETURN(errctx, (length < sizeof(obj->sourcepath)), AKBASIC_ERR_BOUNDS,
|
||||||
"Program path of %zu characters exceeds the %d character limit",
|
"Program path of %zu characters exceeds the %d character limit",
|
||||||
length, AKBASIC_MAX_LINE_LENGTH - 1);
|
length, AKBASIC_MAX_SOURCE_PATH_LENGTH - 1);
|
||||||
PASS(errctx, aksl_memcpy(obj->sourcepath, path, length));
|
PASS(errctx, aksl_memcpy(obj->sourcepath, path, length));
|
||||||
obj->sourcepath[length] = '\0';
|
obj->sourcepath[length] = '\0';
|
||||||
SUCCEED_RETURN(errctx);
|
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
|
* **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
|
* 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
|
* 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`
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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.
|
* has an expectation with three values for four PRINT statements to prove it.
|
||||||
*/
|
*/
|
||||||
if ( buffer[0] == '\0' ) {
|
if ( buffer[0] == '\0' ) {
|
||||||
|
|||||||
@@ -74,6 +74,19 @@ static akerr_ErrorContext *stdio_readline(akbasic_TextSink *self, char *dest, si
|
|||||||
if ( *eof ) {
|
if ( *eof ) {
|
||||||
SUCCEED_RETURN(errctx);
|
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
|
* 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
|
* anyway, but leaving them on would make a stored source line differ from
|
||||||
|
|||||||
@@ -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
|
* `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
|
* been. Here the entry test treats "the counter has reached the limit" as
|
||||||
* "do not enter", so the body is skipped entirely -- and
|
* "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.
|
* which is why this cannot simply be corrected.
|
||||||
*/
|
*/
|
||||||
static void test_single_iteration_loop(void)
|
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.
|
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
|
20 REM An identifier's subscript list used to hang off .right, which is also
|
||||||
30 REM where an argument list chains its arguments -- so the arity counter walked
|
30 REM where an argument list chains its arguments; the arity counter walked
|
||||||
40 REM straight into the subscripts and refused the call. TODO.md section 4.
|
40 REM straight into the subscripts and refused the call. TODO.md section 4.
|
||||||
50 DIM C#(4)
|
50 DIM C#(4)
|
||||||
60 C#(1) = -9
|
60 C#(1) = -9
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
10 REM FILTER has no device capability behind it -- akgl_audio_* synthesises and
|
10 REM FILTER has no device capability; akgl_audio_* synthesises and mixes but
|
||||||
20 REM mixes but has no filter stage, and SDL3 supplies no primitive to build one
|
20 REM has no filter stage, and SDL3 supplies no primitive to build one from.
|
||||||
30 REM from. It is refused rather than silently ignored, so a program that asked
|
30 REM 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.
|
40 REM for a low-pass finds out it did not get one.
|
||||||
50 PRINT "BEFORE"
|
50 PRINT "BEFORE"
|
||||||
60 FILTER 1000, 1, 0, 0, 5
|
60 FILTER 1000, 1, 0, 0, 5
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
10 REM The standalone driver lends the script no audio device. ENVELOPE, VOL and
|
10 REM The standalone driver lends the script no audio device. ENVELOPE, VOL,
|
||||||
20 REM TEMPO only change interpreter state, so they work regardless; SOUND 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.
|
30 REM PLAY need the device and must name themselves when there is none.
|
||||||
40 ENVELOPE 1, 5, 9, 12, 2
|
40 ENVELOPE 1, 5, 9, 12, 2
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
10 REM A DEF call takes its environment from the pool, one per call, exactly as
|
10 REM A DEF call takes one environment from the pool, exactly as
|
||||||
20 REM GOSUB does. It used to be owned by the funcdef and reset on every call,
|
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 in one expression shared a slot,
|
30 REM which cost two silent defects: calls in one expression shared a slot,
|
||||||
40 REM and recursion never came back at all.
|
40 REM and recursion never came back at all.
|
||||||
50 DEF FACT(N#)
|
50 DEF FACT(N#)
|
||||||
60 IF N# <= 1 THEN RETURN 1
|
60 IF N# <= 1 THEN RETURN 1
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
150 DEF DBL(N#) = N# * 2
|
150 DEF DBL(N#) = N# * 2
|
||||||
160 PRINT DBL(10) + DBL(1)
|
160 PRINT DBL(10) + DBL(1)
|
||||||
170 PRINT DBL(1) + DBL(10) + DBL(100)
|
170 PRINT DBL(1) + DBL(10) + DBL(100)
|
||||||
180 REM Depth answers to the environment pool now, so runaway recursion reports
|
180 REM Depth answers to the environment pool, so runaway recursion reports
|
||||||
190 REM "Environment pool exhausted" rather than hanging. It is not exercised here
|
190 REM "Environment pool exhausted" rather than hanging. It is not exercised
|
||||||
200 REM because the statement containing a failed call still prints a junk value
|
200 REM here because the failed statement still prints junk afterwards;
|
||||||
210 REM afterwards -- a separate, pre-existing defect recorded in TODO.md, and one
|
210 REM this is a separate defect in TODO.md, and one this golden file
|
||||||
220 REM this golden file would pin if it went in. tests/user_functions.c asserts
|
220 REM would pin if it went in. tests/user_functions.c asserts
|
||||||
230 REM the message instead.
|
230 REM the message instead.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
10 REM A range check reported through the driver, end to end. The program stops
|
10 REM A range check reported through the driver, end to end. The program
|
||||||
20 REM at the first error, so this file covers one; the rest of the checks are
|
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.
|
30 REM asserted in tests/graphics_verbs.c against the recording backend.
|
||||||
40 PRINT "BEFORE"
|
40 PRINT "BEFORE"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
10 REM The standalone driver lends the script no graphics device.
|
10 REM The standalone driver lends the script no graphics device.
|
||||||
20 REM COLOR, LOCATE and SCALE only touch interpreter state and must still work.
|
20 REM COLOR, LOCATE and SCALE touch interpreter state and must still work.
|
||||||
30 COLOR 1, 3
|
30 COLOR 1, 3
|
||||||
40 LOCATE 40, 50
|
40 LOCATE 40, 50
|
||||||
50 SCALE 1, 640, 400
|
50 SCALE 1, 640, 400
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
10 REM Group B: the housekeeping verbs. None of these is in the Go reference --
|
10 REM Group B: housekeeping verbs. None is in the Go reference --
|
||||||
20 REM they are on its own unimplemented list -- so what each one means here is
|
20 REM they are on its unimplemented list -- so each meaning here is
|
||||||
30 REM a decision, recorded in src/runtime_housekeeping.c beside the verb.
|
30 REM a decision, recorded in src/runtime_housekeeping.c beside the verb.
|
||||||
40 A# = 1 : B# = 2
|
40 A# = 1 : B# = 2
|
||||||
50 PRINT A# : PRINT B#
|
50 PRINT A# : PRINT B#
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
110 P#(2) = 7 : Q#(2) = 9
|
110 P#(2) = 7 : Q#(2) = 9
|
||||||
120 SWAP P#, Q#
|
120 SWAP P#, Q#
|
||||||
130 PRINT P#(2) : PRINT Q#(2)
|
130 PRINT P#(2) : PRINT Q#(2)
|
||||||
140 REM TRON prints each line number inline before the line runs, as a C128 does.
|
140 REM TRON prints each line number inline, as a C128 does.
|
||||||
150 TRON
|
150 TRON
|
||||||
160 PRINT "TRACED"
|
160 PRINT "TRACED"
|
||||||
170 TROFF
|
170 TROFF
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
10 REM A leading zero is padding, not a radix. The reference selects base 8 for
|
10 REM A leading zero is padding, not a radix. The reference selected base 8
|
||||||
20 REM any lexeme starting with 0, so 010 printed 8 and 08 was a parse error --
|
20 REM for lexemes starting with 0; 010 printed 8 and 08 was a parse error --
|
||||||
30 REM TODO.md section 6 item 10, fixed. Commodore BASIC has no octal literals.
|
30 REM TODO.md section 6 item 10 is fixed; Commodore BASIC has no octal.
|
||||||
40 REM 0x is the one prefix that changes the base, and it now reaches the scanner
|
40 REM 0x is the one prefix that changes the base, and now reaches the scanner
|
||||||
50 REM whole: that was section 6 item 15.
|
50 REM whole: that was section 6 item 15.
|
||||||
60 PRINT 010
|
60 PRINT 010
|
||||||
70 PRINT 08
|
70 PRINT 08
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
10 REM A truth value carries its payload in boolvalue, not floatval. The three
|
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
|
20 REM numeric operators were `if ( INTEGER ) ... else <treat as float>`, and
|
||||||
30 REM that else was a catch-all rather than a float branch -- so a truth value
|
30 REM that else was a catch-all, not a float branch -- so a truth value
|
||||||
40 REM on the LEFT computed 0 - 1 into a field nothing reads, kept its BOOLEAN
|
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.
|
50 REM type, and printed `true` instead of -2. Silent, and wrong twice over.
|
||||||
60 A# = 1
|
60 A# = 1
|
||||||
70 PRINT (A# == 1)
|
70 PRINT (A# == 1)
|
||||||
80 REM On the RIGHT it stays legal and stays -1, which is the same property that
|
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 here would have
|
90 REM lets AND and OR act as logical operators. Refusing it would have
|
||||||
100 REM broken every condition in the language.
|
100 REM broken every condition in the language.
|
||||||
110 PRINT 5 - (A# == 1)
|
110 PRINT 5 - (A# == 1)
|
||||||
120 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 of
|
10 REM Statements separated by colons. The COLON token existed from the start;
|
||||||
20 REM the port and nothing consumed it, so a line could hold only one statement.
|
20 REM nothing consumed it, so a line could hold only one statement.
|
||||||
30 PRINT "A" : PRINT "B"
|
30 PRINT "A" : PRINT "B"
|
||||||
40 A# = 1 : B# = 2 : PRINT A# + B#
|
40 A# = 1 : B# = 2 : PRINT A# + B#
|
||||||
50 REM An empty statement is not an error: a trailing separator, or a run of them.
|
50 REM Empty statements are valid: a trailing separator or runs of them.
|
||||||
60 PRINT "C" :
|
60 PRINT "C" :
|
||||||
70 PRINT "D" :: PRINT "E"
|
70 PRINT "D" :: PRINT "E"
|
||||||
80 REM Everything after THEN belongs to the condition, which is BASIC 7.0 and is
|
80 REM Everything after THEN belongs to the condition, as in BASIC 7.0, and is
|
||||||
90 REM not something the reference had an opinion about -- it never got here.
|
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"
|
100 IF 1 == 1 THEN PRINT "TRUE-1" : PRINT "TRUE-2"
|
||||||
110 IF 1 == 0 THEN PRINT "NEVER-1" : PRINT "NEVER-2"
|
110 IF 1 == 0 THEN PRINT "NEVER-1" : PRINT "NEVER-2"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
10 REM A field name is checked against a closed set the program declared, which
|
10 REM A field name is checked against the closed set the program declared;
|
||||||
20 REM is the one thing in this language whose valid spellings are written down.
|
20 REM this is the one thing whose valid spellings are written down.
|
||||||
30 REM A misspelled *variable* is still silent -- see the last two lines.
|
30 REM A misspelled *variable* is still silent -- see the last two lines.
|
||||||
40 TYPE RECT
|
40 TYPE RECT
|
||||||
50 W#
|
50 W#
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
170 RETURN B@.W#
|
170 RETURN B@.W#
|
||||||
180 PRINT WIDEN(A@)
|
180 PRINT WIDEN(A@)
|
||||||
190 PRINT A@.W#
|
190 PRINT A@.W#
|
||||||
200 REM To change one on purpose, pass a pointer. Assignment copies a pointer's
|
200 REM To change one on purpose, pass a pointer. Assignment copies its
|
||||||
210 REM reference, so the callee is looking at the caller's own record.
|
210 REM reference, so the callee is looking at the caller's own record.
|
||||||
220 DIM Q@ AS PTR TO CRATE
|
220 DIM Q@ AS PTR TO CRATE
|
||||||
230 POINT Q@ AT A@
|
230 POINT Q@ AT A@
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
370 N2@.COUNT# = 20
|
370 N2@.COUNT# = 20
|
||||||
380 POINT N1@.TAIL@ AT N2@
|
380 POINT N1@.TAIL@ AT N2@
|
||||||
390 DEF TOTAL(P@ AS PTR TO NODE)
|
390 DEF TOTAL(P@ AS PTR TO NODE)
|
||||||
395 REM A pointer is true when it points at something, which is how a walk knows
|
395 REM A pointer is true when it points at something, so a walk knows
|
||||||
396 REM where the list ends. NOT is the bitwise operator here, so the test is
|
396 REM where the list ends. NOT is the bitwise operator here, so the test is
|
||||||
397 REM written the positive way round.
|
397 REM written the positive way round.
|
||||||
400 IF P@->TAIL@ THEN RETURN P@->COUNT# + TOTAL(P@->TAIL@)
|
400 IF P@->TAIL@ THEN RETURN P@->COUNT# + TOTAL(P@->TAIL@)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
10 REM A TYPE declares its fields; each takes its own type from its own suffix,
|
10 REM A TYPE declares its fields; each takes its type from its own suffix,
|
||||||
20 REM which is the same rule every other name in this language follows.
|
20 REM which is the same rule every other name in this language follows.
|
||||||
30 TYPE COORD
|
30 TYPE COORD
|
||||||
40 X#
|
40 X#
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
10 REM A type name and a field name are bare words, and so is every verb, so
|
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
|
20 REM they share a namespace whether we like it or not. Both are refused with
|
||||||
30 REM the same rule the scanner already applies to variable names -- and a type
|
30 REM the scanner applies the same rule to variable names -- and a type
|
||||||
40 REM name is refused by the prescan, which can say so plainly rather than
|
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".
|
50 REM leaving the parser to report "Expected expression or literal".
|
||||||
60 TYPE POINT
|
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
|
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
|
11 REM when we have a nested for loop. The inner loop SHOULD execute, but
|
||||||
12 REM the outer loop should NOT execute. Therefore, neither loop should execute.
|
12 REM the outer loop should NOT execute; neither loop should execute.
|
||||||
20 FOR I# = 1 TO 0
|
20 FOR I# = 1 TO 0
|
||||||
25 FOR J# = 2 TO 4
|
25 FOR J# = 2 TO 4
|
||||||
30 PRINT "waitingForCommand FAILS if this is seen"
|
30 PRINT "waitingForCommand FAILS if this is seen"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
10 PRINT MOD(10, 3)
|
10 PRINT MOD(10, 3)
|
||||||
20 PRINT MOD(12, 5)
|
20 PRINT MOD(12, 5)
|
||||||
30 PRINT MOD(4, 2)
|
30 PRINT MOD(4, 2)
|
||||||
40 REM MOD() ONLY WORKS WITH INTEGERS - RESULTS WITH FLOATING POINT ARE UNRELIABLE
|
40 REM MOD() ONLY WORKS WITH INTEGERS; FLOATING POINT RESULTS ARE UNRELIABLE
|
||||||
50 REM PRINT MOD(1.2, 0.4)
|
50 REM PRINT MOD(1.2, 0.4)
|
||||||
60 REM THERE IS NO ERROR THROWN HERE. JUST DONT DO IT.
|
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