Check the reference corpus in, so the build stops needing the Go submodule

All 41 .bas/.txt pairs copied byte for byte from basicinterpreter@d76162c into
tests/reference/, plus the Commodore font into assets/fonts/ -- the two things
that tied the build to deps/basicinterpret. Both were verified cmp-identical
to the submodule copies.

This reverses a decision that was deliberate and correct at the time: the
corpus was driven in place because copying a submodule's corpus guarantees
drift. Overruled on purpose -- the Go dependency is being deprecated, and a
build that cannot run its acceptance suite without cloning the implementation
it replaced is not finished. tests/reference/README.md records what the drift
now costs and that those expectations are never edited.

Checked rather than assumed: both configurations configure, build and pass
from scratch with deps/basicinterpret moved out of the tree entirely.

The submodule is kept as the behavioural spec, which is a real use. The font
came with an open licence question; assets/fonts/PROVENANCE.md states it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 12:35:40 -04:00
parent bf9cf85130
commit eb5374d212
92 changed files with 503 additions and 50 deletions

View File

@@ -15,8 +15,10 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
# 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
# deps/basicinterpret (the golden corpus is driven in place from it). # nothing else: the golden corpus and the Commodore font now live in
# this repository (tests/reference/ and assets/fonts/), so
# deps/basicinterpret is no longer a build dependency at all.
# It does *not* need deps/libakgl, which is guarded behind # 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
# clone SDL, SDL_image, SDL_mixer, SDL_ttf and jansson for a target # clone SDL, SDL_image, SDL_mixer, SDL_ttf and jansson for a target
@@ -32,12 +34,13 @@ jobs:
run: | run: |
cmake -S . -B build cmake -S . -B build
cmake --build build --parallel cmake --build build --parallel
# The suite is 70 cases: 41 golden files byte-compared against the Go # The suite is 78 cases: 41 golden files byte-compared against the Go
# reference's own corpus, 5 local golden cases for verbs the reference # reference's own corpus (checked in at tests/reference/, see its README),
# never implemented, 21 unit tests, 2 embedding examples, and 1 # 9 local golden cases for verbs the reference never implemented, 25 unit
# known-failing test that asserts the *correct* contract for defects # tests, 2 embedding examples, and 1 known-failing test that asserts the
# carried over from the reference (TODO.md section 6). A green run # *correct* contract for defects carried over from the reference (TODO.md
# therefore does not mean defect-free -- see AKBASIC_KNOWN_FAILING_TESTS. # section 6). A green run therefore does not mean defect-free -- see
# AKBASIC_KNOWN_FAILING_TESTS.
# #
# AKBASIC_WITH_AKGL is off here, which is the point rather than an # AKBASIC_WITH_AKGL is off here, which is the point rather than an
# omission: this job is what proves the interpreter builds and passes on a # omission: this job is what proves the interpreter builds and passes on a
@@ -96,13 +99,14 @@ jobs:
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y cmake gcc gcovr sudo apt-get install -y cmake gcc gcovr
# The gate is a ratchet, not a target: src/ sits at 93.5% of lines and # The gate is a ratchet, not a target: src/ sits at 94.6% of lines and
# 97.8% of functions, so 90 fails on a real regression (a test deleted, or # 98.6% of functions, so 90 fails on a real regression (a test deleted, or
# new untested code added) without tripping over rounding. # new untested code added) without tripping over rounding.
# #
# The akbasic_akgl target is not in this figure. It is not built in a # The akbasic_akgl and akbasic_frontend targets are not in this figure.
# default configuration, and instrumenting it would drag SDL into the # Neither is built in a default configuration, and instrumenting them would
# coverage job for four thin adaptors. # drag SDL into the coverage job for four thin adaptors and a host. What
# covers them instead is the akgl_build job below.
# #
# There is deliberately no branch gate. Branch coverage reads about 18% # There is deliberately no branch gate. Branch coverage reads about 18%
# and is not a meaningful number here, for the reason libakgl's and # and is not a meaningful number here, for the reason libakgl's and
@@ -235,9 +239,9 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
# The harness copies the repo and configures a build inside the copy, # The harness copies the repo and configures a build inside the copy,
# so it needs the same submodules the main build does -- including # so it needs the same submodules the main build does. The golden
# deps/basicinterpret, because the golden corpus is part of what kills # corpus is part of what kills mutants and it is checked in now, so
# mutants. # that is libakerror and libakstdlib and nothing else.
submodules: true submodules: true
- name: dependencies - name: dependencies
run: | run: |

View File

@@ -66,9 +66,9 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
# The harness copies the repo and configures a build inside the copy, # The harness copies the repo and configures a build inside the copy,
# so it needs the same submodules the main build does -- including # so it needs the same submodules the main build does. The golden
# deps/basicinterpret, because the golden corpus is part of what kills # corpus is part of what kills mutants and it is checked in now, so
# mutants. # that is libakerror and libakstdlib and nothing else.
submodules: true submodules: true
- name: dependencies - name: dependencies
run: | run: |

View File

@@ -190,10 +190,17 @@ make tests # or: bash ./test.sh
stdout against the sibling `.txt`. The exit code is the number of failures. To run a single case, stdout against the sibling `.txt`. The exit code is the number of failures. To run a single case,
just run the interpreter on that `.bas` and diff against its `.txt`. just run the interpreter on that `.bas` and diff against its `.txt`.
That corpus is the natural acceptance suite for the rewrite: the C interpreter should reproduce the That corpus is the acceptance suite for the rewrite: the C interpreter reproduces the same stdout
same stdout for the same `.bas` files. Carry it over (or drive it in place) alongside the per-module for the same `.bas` files. **It has been copied into this repository at `tests/reference/`** and no
CTest executables the `ak*` style calls for the two answer different questions, and new language longer runs out of the submodule — see `tests/reference/README.md` for the provenance and for the
features from goal 2 need a `.bas`/`.txt` pair as well as unit tests. rule that those expectations are never edited to suit this interpreter. It sits alongside the
per-module CTest executables the `ak*` style calls for; the two answer different questions, and new
language features from goal 2 need a `.bas`/`.txt` pair in `tests/language/` as well as unit tests.
**`deps/basicinterpret` is no longer a build or test dependency.** Both configurations were
configured, built and run from scratch with it moved out of the tree. It is kept only as the
behavioural spec to read when a question about semantics comes up — which is a real use, and the
reason it has not been removed. The Commodore font it also supplied now lives in `assets/fonts/`.
`deps/basicinterpret/README.md` is the language reference — implemented verbs, functions, variable `deps/basicinterpret/README.md` is the language reference — implemented verbs, functions, variable
type suffixes, and an explicit list of what is *not* implemented. Read it before adding or porting type suffixes, and an explicit list of what is *not* implemented. Read it before adding or porting

View File

@@ -195,10 +195,12 @@ if(AKBASIC_WITH_AKGL)
# The reference's own font, at the reference's own size. Its main.go opens # The reference's own font, at the reference's own size. Its main.go opens
# "./fonts/C64_Pro_Mono-STYLE.ttf", which only ever resolved from its own # "./fonts/C64_Pro_Mono-STYLE.ttf", which only ever resolved from its own
# source directory; compiled in, it resolves from anywhere, and AKBASIC_FONT # source directory; compiled in, it resolves from anywhere, and AKBASIC_FONT
# overrides it at runtime for an installed copy. # overrides it at runtime for an installed copy. Vendored into assets/fonts/
# rather than reached for in the submodule -- see assets/fonts/PROVENANCE.md,
# which also carries the licence question that came with it.
target_compile_definitions(basic PRIVATE target_compile_definitions(basic PRIVATE
AKBASIC_HAVE_AKGL=1 AKBASIC_HAVE_AKGL=1
AKBASIC_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/deps/basicinterpret/fonts/C64_Pro_Mono-STYLE.ttf") AKBASIC_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
endif() endif()
akbasic_instrument(basic) akbasic_instrument(basic)
@@ -308,7 +310,7 @@ if(AKBASIC_WITH_AKGL)
target_link_libraries(akbasic_test_akgl_frontend PRIVATE akbasic_frontend) target_link_libraries(akbasic_test_akgl_frontend PRIVATE akbasic_frontend)
target_include_directories(akbasic_test_akgl_frontend PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests") target_include_directories(akbasic_test_akgl_frontend PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
target_compile_definitions(akbasic_test_akgl_frontend PRIVATE target_compile_definitions(akbasic_test_akgl_frontend PRIVATE
AKBASIC_TEST_C64_FONT="${CMAKE_CURRENT_SOURCE_DIR}/deps/basicinterpret/fonts/C64_Pro_Mono-STYLE.ttf") AKBASIC_TEST_C64_FONT="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
akbasic_instrument(akbasic_test_akgl_frontend) akbasic_instrument(akbasic_test_akgl_frontend)
_add_test(NAME akgl_frontend COMMAND akbasic_test_akgl_frontend) _add_test(NAME akgl_frontend COMMAND akbasic_test_akgl_frontend)
_set_tests_properties(akgl_frontend PROPERTIES _set_tests_properties(akgl_frontend PROPERTIES
@@ -331,13 +333,19 @@ if(AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
) )
endif() endif()
# The golden-file suite. It drives deps/basicinterpret/tests/**/*.bas in place -- # The reference's own corpus, byte-compared against the sibling .txt. One CTest
# the corpus is a submodule and copying it guarantees drift -- and byte-compares # case per .bas so a failure names the file.
# stdout against the sibling .txt. One CTest case per .bas so a failure names the #
# file. # 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 file(GLOB_RECURSE AKBASIC_GOLDEN_CASES
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/deps/basicinterpret" RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/tests/reference"
"${CMAKE_CURRENT_SOURCE_DIR}/deps/basicinterpret/tests/*.bas" "${CMAKE_CURRENT_SOURCE_DIR}/tests/reference/*.bas"
) )
foreach(_case IN LISTS AKBASIC_GOLDEN_CASES) foreach(_case IN LISTS AKBASIC_GOLDEN_CASES)
@@ -348,7 +356,7 @@ foreach(_case IN LISTS AKBASIC_GOLDEN_CASES)
NAME golden_${_name} NAME golden_${_name}
COMMAND ${CMAKE_COMMAND} COMMAND ${CMAKE_COMMAND}
-DBASIC=$<TARGET_FILE:basic> -DBASIC=$<TARGET_FILE:basic>
-DCASE=${CMAKE_CURRENT_SOURCE_DIR}/deps/basicinterpret/${_case} -DCASE=${CMAKE_CURRENT_SOURCE_DIR}/tests/reference/${_case}
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/golden.cmake -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/golden.cmake
) )
endforeach() endforeach()
@@ -373,10 +381,12 @@ endif()
# The local golden corpus, for verbs the reference never implemented. # The local golden corpus, for verbs the reference never implemented.
# #
# It has to be separate: the corpus above is a submodule and nothing in this # Still separate now that the reference's corpus lives in this repository too,
# repository may add files to it, but goal 2's new verbs still need the .bas/.txt # and the reason changed rather than went away: tests/reference/ is a *record* of
# half of their coverage. Registered under local_ so a failure says at a glance # what the Go implementation did and nothing in it should ever be edited to suit
# which corpus it came from. # 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

View File

@@ -1,4 +1,4 @@
This BASIC is styled after [Commodore BASIC 7.0](http://www.jbrain.com/pub/cbm/manuals/128/C128PRG.pdf) and the [Dartmouth BASIC from 1964](https://www.dartmouth.edu/basicfifty/basic.html). It is a C rewrite of [basicinterpreter](https://source.starfort.tech/andrew/basicinterpreter), which was itself built from the instructions for the Java implementation of Lox in [craftinginterpreters.com](https://craftinginterpreters.com) before striking off on its own. The Go version is vendored at `deps/basicinterpret` and is the behavioural specification: when a question about semantics comes up, the answer lives in that code and in its `tests/`. This BASIC is styled after [Commodore BASIC 7.0](http://www.jbrain.com/pub/cbm/manuals/128/C128PRG.pdf) and the [Dartmouth BASIC from 1964](https://www.dartmouth.edu/basicfifty/basic.html). It is a C rewrite of [basicinterpreter](https://source.starfort.tech/andrew/basicinterpreter), which was itself built from the instructions for the Java implementation of Lox in [craftinginterpreters.com](https://craftinginterpreters.com) before striking off on its own. The Go version is vendored at `deps/basicinterpret` and is the behavioural specification: when a question about semantics comes up, the answer lives in that code. Its acceptance corpus is **checked in here** at [`tests/reference/`](tests/reference/README.md) and runs on every build, so nothing about building or testing this project needs that submodule any more.
```sh ```sh
git submodule update --init --recursive git submodule update --init --recursive
@@ -9,7 +9,7 @@ cmake --build build --parallel
./build/basic ./build/basic
# To run a basic file from the command line # To run a basic file from the command line
./build/basic deps/basicinterpret/tests/language/functions.bas ./build/basic tests/reference/language/functions.bas
# The test suite: unit tests plus the Go version's own corpus, byte-compared # The test suite: unit tests plus the Go version's own corpus, byte-compared
ctest --test-dir build --output-on-failure ctest --test-dir build --output-on-failure
@@ -25,7 +25,7 @@ ctest --test-dir build-akgl --output-on-failure
# That build of `basic` is a different program: it opens an 800x600 window, draws # That build of `basic` is a different program: it opens an 800x600 window, draws
# BASIC output into it in the Commodore font, and still puts every byte on stdout. # BASIC output into it in the Commodore font, and still puts every byte on stdout.
./build-akgl/basic deps/basicinterpret/tests/language/functions.bas ./build-akgl/basic tests/reference/language/functions.bas
``` ```
There are two workflows. **`.gitea/workflows/ci.yaml`** runs on every push: the suite, There are two workflows. **`.gitea/workflows/ci.yaml`** runs on every push: the suite,
@@ -72,7 +72,9 @@ And the port is a good excuse to find out what the original actually does, as op
# What Works? # What Works?
Everything the Go version does. All 41 `.bas` files in `deps/basicinterpret/tests/` produce **byte-identical** output from both implementations, including error messages and the trailing blank line after one. Those files are driven in place as individual CTest cases rather than copied, so the corpus cannot drift from upstream. Everything the Go version does. All 41 `.bas` files of the reference's own corpus produce **byte-identical** output from both implementations, including error messages and the trailing blank line after one. Each is a separate CTest case, so a failure names the file.
That corpus lives at [`tests/reference/`](tests/reference/README.md), copied byte for byte out of the Go project. It was driven in place out of the submodule until the Go dependency started being deprecated; its README records where it came from, and that nobody is watching for drift any more because the thing it tracked has stopped moving. **Do not edit those expectations** — a failure there means this interpreter's behaviour changed.
## Case Sensitivity ## Case Sensitivity
@@ -461,7 +463,7 @@ The exception is `FILTER`, which sets the SID's filter cutoff, band switches and
* [libakerror](https://source.starfort.tech/andrew/libakerror) 1.0.0 — TRY/CATCH-style error contexts. Every function that can fail returns one. * [libakerror](https://source.starfort.tech/andrew/libakerror) 1.0.0 — TRY/CATCH-style error contexts. Every function that can fail returns one.
* [libakstdlib](https://source.starfort.tech/andrew/libakstdlib) 0.2.0 — libc wrappers that report through `libakerror`. * [libakstdlib](https://source.starfort.tech/andrew/libakstdlib) 0.2.0 — libc wrappers that report through `libakerror`.
* [libakgl](https://source.starfort.tech/andrew/libakgl) 0.2.0 — **optional**, only for `-DAKBASIC_WITH_AKGL=ON`. Pulls in SDL3, and requires `libakstdlib` 0.2, which is why the two move together. * [libakgl](https://source.starfort.tech/andrew/libakgl) 0.2.0 — **optional**, only for `-DAKBASIC_WITH_AKGL=ON`. Pulls in SDL3, and requires `libakstdlib` 0.2, which is why the two move together.
* [basicinterpret](https://source.starfort.tech/andrew/basicinterpret) — the Go original, vendored as the behavioural spec and the acceptance corpus. Not linked, not built. * [basicinterpret](https://source.starfort.tech/andrew/basicinterpret) — the Go original. Vendored as the behavioural spec for questions about semantics, and for nothing else: its acceptance corpus and its Commodore font are checked in here now, so **the build and the test suite do not need it**. Not linked, not built, and safe to omit.
Everything is a submodule; `git submodule update --init --recursive` gets all of it. There is nothing to install first. Everything is a submodule; `git submodule update --init --recursive` gets all of it. There is nothing to install first.

23
TODO.md
View File

@@ -354,10 +354,23 @@ 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 reference's own corpus, driven in place.** All 41 `.bas` files **The acceptance suite is the reference's own corpus, checked in at `tests/reference/`.** All
under `deps/basicinterpret/tests/` are registered as individual CTest cases and byte-compared 41 `.bas` files are registered as individual CTest cases and byte-compared against their `.txt`
against their `.txt` — including the trailing double newline on an error line (§1.8). Nothing — including the trailing double newline on an error line (§1.8).
is copied, so the corpus cannot drift from its upstream.
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.
**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
submodule moved out of the tree. The Commodore font moved too, to `assets/fonts/`, which was the
other thing tying the build to it; `assets/fonts/PROVENANCE.md` carries the licence question that
came with it.
Eighteen unit tests cover the modules the corpus cannot reach on its own. Eighteen unit tests cover the modules the corpus cannot reach on its own.
`tests/known_reference_defects.c` is registered in `AKBASIC_KNOWN_FAILING_TESTS` and asserts `tests/known_reference_defects.c` is registered in `AKBASIC_KNOWN_FAILING_TESTS` and asserts
@@ -1037,7 +1050,7 @@ entire test corpus and passes clean under ASan and UBSan.
|---|---| |---|---|
| `ctest` | 76/76 — 41 upstream golden cases, 9 local ones, 23 unit tests, 2 embedding examples, 1 known-failing | | `ctest` | 76/76 — 41 upstream golden cases, 9 local ones, 23 unit tests, 2 embedding examples, 1 known-failing |
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 75/75 — the same, minus the three `no_device` cases the SDL driver contradicts, plus `akgl_backends` and `akgl_frontend`; the `akgl_build` CI job | | `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 75/75 — the same, minus the three `no_device` cases the SDL driver contradicts, plus `akgl_backends` and `akgl_frontend`; the `akgl_build` CI job |
| Golden corpus | 41/41 byte-exact, driven in place from the submodule**and 41/41 again through the SDL binary**, which is most of what proves the frontend changes no output | | Golden corpus | 41/41 byte-exact from `tests/reference/`**and 41/41 again through the SDL binary**, which is most of what proves the frontend changes no output |
| ASan + UBSan | 76/76 | | ASan + UBSan | 76/76 |
| Line coverage | 94.6% (3857/4076) — above the 90% gate | | Line coverage | 94.6% (3857/4076) — above the 90% gate |
| Function coverage | 98.6% (287/291) | | Function coverage | 98.6% (287/291) |

Binary file not shown.

View File

@@ -0,0 +1,41 @@
# C64_Pro_Mono-STYLE.ttf
| | |
|---|---|
| Font | C64 Pro Mono, by STYLE — <https://style64.org/c64-truetype> |
| Copied from | `basicinterpreter@d76162c`, `fonts/C64_Pro_Mono-STYLE.ttf` |
| Copied on | 2026-07-31 |
| Verified | `cmp`-identical to the submodule copy |
## Why it is here
It is the font `deps/basicinterpret/main.go` opens, at the size it opens it, and reproducing
that is part of what the SDL frontend is for. It was reached for inside the submodule until the
Go dependency started being deprecated; the build now has no reason to clone that repository,
and this was the last thing tying it to one.
Two things use it, both through compile definitions in `CMakeLists.txt`:
- `AKBASIC_FONT_PATH` — what an `-DAKBASIC_WITH_AKGL=ON` build of `basic` opens. Overridable at
runtime with the `AKBASIC_FONT` environment variable, which is what an installed copy or a
different font wants.
- `AKBASIC_TEST_C64_FONT` — what `tests/akgl_frontend.c` opens, because `TODO.md` §3's
acceptance asks the drawn text to be proved in *this* font rather than any monospaced one.
## The licence question, which is open
**STYLE's C64 TrueType fonts are not public domain and this repository does not carry their
licence.** Neither did `basicinterpreter`, which is where the file came from. The terms at
<https://style64.org/c64-truetype/license> permit free personal and non-commercial use and
require the licence to accompany redistribution; commercial redistribution needs their
permission.
Copying it here changes nothing legally — the same file was already being redistributed from
the same owner's other public repository — but it does move the question into a project that
is meant to be embeddable in other people's games, which is where it starts to matter.
**Before this is shipped in anything commercial, or vendored into a game that is:** either
obtain STYLE's permission and include their licence file beside this one, or replace the font.
Replacing it is cheap — the frontend measures whatever font it is handed
(`akbasic_sink_init_akgl` derives the character grid from `akgl_text_measure`), so any
monospaced TTF works and only the two compile definitions above would change.

View File

@@ -282,9 +282,10 @@ 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, and the golden suite drives the .bas # libakstdlib in with add_subdirectory. The golden corpus used to be the
# corpus in deps/basicinterpret in place. Only the reference's vendored # other reason -- it was driven in place out of deps/basicinterpret -- and
# Windows DLLs are dead weight, hence "*.dll". # now lives in tests/reference/, which comes along with the rest of the tree.
# Only vendored Windows DLLs are dead weight, hence "*.dll".
ignore = shutil.ignore_patterns("build*", ".git", "*.o", "*.so", "*~", ignore = shutil.ignore_patterns("build*", ".git", "*.o", "*.so", "*~",
"#*#", "*.iso", "*.png", "*.gcno", "*.gcda", "#*#", "*.iso", "*.png", "*.gcno", "*.gcda",
"*.dll") "*.dll")

49
tests/reference/README.md Normal file
View File

@@ -0,0 +1,49 @@
# 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 now, 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. It was overruled on purpose, because 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.
**What the change costs, stated plainly so nobody has to rediscover it:** if the Go
implementation ever gains a test, this directory will not. There is no longer anything
watching for that. The trade is deliberate — the Go project is the thing being retired, so a
corpus that stops tracking it is a corpus that stopped tracking something that stopped moving.
## The rule for editing these
**Don't.**
This directory is a *record of what the Go implementation did*, and it is what makes the claim
"reproduces the reference byte for byte" checkable at all. A failure here means this
interpreter's behaviour changed, and the answer is to change this interpreter or to record a
deliberate deviation in `TODO.md` §5 — never to edit the expectation to match.
New cases for this project's own behaviour go in `tests/language/`, which is registered under
`local_*` for exactly this reason: the prefix says which corpus a failure came from, and a diff
that touches `tests/reference/` stands out as the thing it is.
There is one case worth knowing about before anybody "fixes" it.
`examples/strreverse.bas` names a variable `INPUT$`, which a real Commodore 128 would refuse
and which this interpreter accepts only because the reference's reserved-word check never
fires. That is `TODO.md` §6 item 16, and it is the reason that item is filed as a decision
rather than a defect: making the check work costs this case.

View File

@@ -0,0 +1,29 @@
10 DIM A#(5)
15 ITERATIONS# = 0
20 A#(0) = 5
21 A#(1) = 2
22 A#(2) = 4
23 A#(3) = 1
24 A#(4) = 3
25 PRINT "BEFORE SORTING: "
26 FOR I# = 0 TO 4
27 PRINT A#(I#)
28 NEXT I#
30 CHANGED# = 0
32 ITERATIONS# = ITERATIONS# + 1
35 FOR I# = 0 TO 3
45 J# = I#+1
46 REM PRINT "COMPARING INDEXES " + I# + " (" + A#(I#) + ") AND " + J# + "
50 IF A#(I#) <= A#(J#) THEN GOTO 100
55 REM PRINT "SWAPPING INDEXES " + I# + " AND " + J#
60 TMP# = A#(I#)
70 A#(I#) = A#(J#)
80 A#(J#) = TMP#
85 CHANGED# = CHANGED# + 1
100 NEXT I#
110 IF CHANGED# <> 0 THEN GOTO 30
115 PRINT "AFTER SORTING:"
120 FOR I# = 0 TO 4
130 PRINT A#(I#)
140 NEXT I#
145 PRINT "SORTED IN " + ITERATIONS# + " ITERATIONS"

View File

@@ -0,0 +1,13 @@
BEFORE SORTING:
5
2
4
1
3
AFTER SORTING:
1
2
3
4
5
SORTED IN 4 ITERATIONS

View File

@@ -0,0 +1,10 @@
10 N# = 5
20 RESULT# = 1
30 GOSUB 100
40 PRINT "FACTORIAL(5) IS " + RESULT#
50 QUIT
100 IF N# <= 1 THEN RETURN
110 RESULT# = RESULT# * N#
120 N# = N# - 1
130 GOSUB 100
140 RETURN

View File

@@ -0,0 +1 @@
FACTORIAL(5) IS 120

View File

@@ -0,0 +1,7 @@
10 INPUT$ = "HELLO"
20 STRLEN# = LEN(INPUT$) - 1
30 REV$ = ""
40 FOR I# = STRLEN# TO 0 STEP -1
50 REV$ = REV$ + MID(INPUT$, I#, 1)
60 NEXT I#
70 PRINT "REVERSED: " + REV$

View File

@@ -0,0 +1 @@
REVERSED: OLLEH

View File

@@ -0,0 +1,7 @@
10 PRINT 2.0 + 2.0
20 PRINT 2.0 * 2.0
30 PRINT 4.0 / 2.0
40 PRINT 4.0 - 2.0
50 PRINT 1.0 / 0.5
60 PRINT 12.0 / 4.0
70 PRINT 1.20 / 0.4

View File

@@ -0,0 +1,7 @@
4.000000
4.000000
2.000000
2.000000
2.000000
3.000000
3.000000

View File

@@ -0,0 +1,5 @@
10 PRINT 2 + 2
20 PRINT 2 * 2
30 PRINT 4 / 2
40 PRINT 4 - 2

View File

@@ -0,0 +1,3 @@
4
4
2

View File

@@ -0,0 +1,6 @@
10 DIM A#(8, 8)
20 PRINT LEN(A#)
30 A#(0,7) = 31337
40 A#(1,7) = 65535
50 PRINT A#(0,7)
60 PRINT A#(1,7)

View File

@@ -0,0 +1,3 @@
64
31337
65535

View File

@@ -0,0 +1,3 @@
10 DIM A#(3)
20 PRINT A#(4)
30 PRINT "FAILURE"

View File

@@ -0,0 +1,2 @@
? 20 : RUNTIME ERROR Variable index access out of bounds at dimension 0: 4 (max 2)

View File

@@ -0,0 +1,9 @@
10 DIM A#(3)
20 A#(0) = 100
30 A#(1) = 101
40 A#(2) = 102
50 IF LEN(A#) <> 3 THEN PRINT "LEN(A#) != 3 : " + LEN(A#) + " FAILURE"
60 IF A#(0) <> 100 THEN PRINT "A#(0) != 100 : " + A#(0) + " FAILURE"
70 IF A#(1) <> 101 THEN PRINT "A#(1) != 101 : " + A#(1) + " FAILURE"
80 IF A#(2) <> 102 THEN PRINT "A#(2) != 102 : " + A#(2) + " FAILURE"
90 PRINT "SUCCESS"

View File

@@ -0,0 +1 @@
SUCCESS

View File

@@ -0,0 +1,2 @@
10 print "HELLO"
20 print mod(12, 5)

View File

@@ -0,0 +1,2 @@
HELLO
2

View File

@@ -0,0 +1,4 @@
10 FOR I# = 1 TO 4
20 PRINT I#
30 NEXT I#
40 QUIT

View File

@@ -0,0 +1,4 @@
1
2
3
4

View File

@@ -0,0 +1,13 @@
10 REM This shows the waitingForCommand utility in the BasicEnvironment
11 REM We have a FOR loop here with a condition where the loop should
12 REM not execute at all. But because the checking of the conditional is
13 REM delayed until the bottom of the loop, we run the risk of the
14 REM runtime executing every line between FOR ... NEXT even though it
15 REM shouldn't. waitingForCommand prevents this from occurring
20 FOR I# = 1 TO 1
30 PRINT "waitingForCommand FAILS if this is seen"
40 NEXT I#
50 FOR I# = 1 TO 2
60 PRINT "waitingForCommand PASS if this is seen"
70 NEXT I#
80 QUIT

View File

@@ -0,0 +1,2 @@
waitingForCommand PASS if this is seen
waitingForCommand PASS if this is seen

View File

@@ -0,0 +1,3 @@
10 GOTO 30
20 PRINT "FAILURE"
30 PRINT "SUCCESS"

View File

@@ -0,0 +1 @@
SUCCESS

View File

@@ -0,0 +1,11 @@
10 A# = 1
20 IF A# == 1 THEN GOTO 30 ELSE GOTO 40
30 PRINT "A# IS 1"
35 GOTO 50
45 PRINT "A# IS NOT 1"
50 IF A# == 2 THEN GOTO 60 ELSE GOTO 80
60 PRINT "A# IS 2"
65 PRINT A#
70 GOTO 90
80 PRINT "A# IS NOT 2"
90 PRINT "DONE"

View File

@@ -0,0 +1,3 @@
A# IS 1
A# IS NOT 2
DONE

View File

@@ -0,0 +1,10 @@
10 FOR I# = 1 TO 4
15 PRINT "OUTER : I# IS " + I#
20 FOR J# = 2 TO 3
23 PRINT "INNER : I# IS " + I#
25 PRINT "INNER : J# IS " + J#
30 PRINT "INNER : I# * J# IS " + (I# * J#)
40 NEXT J#
50 NEXT I#
60 PRINT "DONE"
70 QUIT

View File

@@ -0,0 +1,29 @@
OUTER : I# IS 1
INNER : I# IS 1
INNER : J# IS 2
INNER : I# * J# IS 2
INNER : I# IS 1
INNER : J# IS 3
INNER : I# * J# IS 3
OUTER : I# IS 2
INNER : I# IS 2
INNER : J# IS 2
INNER : I# * J# IS 4
INNER : I# IS 2
INNER : J# IS 3
INNER : I# * J# IS 6
OUTER : I# IS 3
INNER : I# IS 3
INNER : J# IS 2
INNER : I# * J# IS 6
INNER : I# IS 3
INNER : J# IS 3
INNER : I# * J# IS 9
OUTER : I# IS 4
INNER : I# IS 4
INNER : J# IS 2
INNER : I# * J# IS 8
INNER : I# IS 4
INNER : J# IS 3
INNER : I# * J# IS 12
DONE

View File

@@ -0,0 +1,11 @@
10 REM This shows the waitingForCommand utility in the BasicEnvironment
11 REM when we have a nested for loop. The inner loop SHOULD execute, but
12 REM the outer loop should NOT execute. Therefore, neither loop should execute.
20 FOR I# = 1 TO 0
25 FOR J# = 2 TO 4
30 PRINT "waitingForCommand FAILS if this is seen"
32 QUIT
35 NEXT J#
40 NEXT I#
50 PRINT "SUCCESS"
80 QUIT

View File

@@ -0,0 +1 @@
SUCCESS

View File

@@ -0,0 +1,6 @@
10 DEF SQR(X#) = X# * X#
20 DEF MUL(X#, Y#) = X# * Y#
30 A# = SQR(3)
40 B# = MUL(A#, 4)
50 IF A# <> 9 THEN PRINT "SQR FAIL" ELSE PRINT "SQR PASS"
60 IF B# <> 36 THEN PRINT "MUL FAIL" ELSE PRINT "MUL PASS"

View File

@@ -0,0 +1,2 @@
SQR PASS
MUL PASS

View File

@@ -0,0 +1 @@
10 PRINT ATN(3)

View File

@@ -0,0 +1 @@
1.249046

View File

@@ -0,0 +1,3 @@
10 PRINT "97 : " + CHR(97)
20 PRINT "65 : " + CHR(65)
30 PRINT "64 : " + CHR(64)

View File

@@ -0,0 +1,3 @@
97 : a
65 : A
64 : @

View File

@@ -0,0 +1 @@
10 PRINT COS(90)

View File

@@ -0,0 +1 @@
-0.448074

View File

@@ -0,0 +1 @@
10 PRINT HEX(255)

View File

@@ -0,0 +1 @@
ff

View File

@@ -0,0 +1,3 @@
10 X$ = "HELLO"
20 Y$ = "LLO"
30 PRINT INSTR(X$, Y$)

View File

@@ -0,0 +1 @@
2

View File

@@ -0,0 +1 @@
10 PRINT LEFT("HELLO", 2)

View File

@@ -0,0 +1 @@
HE

View File

@@ -0,0 +1,5 @@
10 A$ = "HELLO"
20 STRLEN# = LEN(A$)
30 IF STRLEN# == 5 THEN GOTO 50
40 PRINT "FAILURE"
50 PRINT "SUCCESS"

View File

@@ -0,0 +1 @@
SUCCESS

View File

@@ -0,0 +1 @@
10 PRINT LOG(3.14)

View File

@@ -0,0 +1 @@
1.144223

View File

@@ -0,0 +1,3 @@
10 A$ = "HELLO"
20 B$ = MID(A$, 2, 3)
30 PRINT B$

View File

@@ -0,0 +1 @@
LLO

View File

@@ -0,0 +1,6 @@
10 PRINT MOD(10, 3)
20 PRINT MOD(12, 5)
30 PRINT MOD(4, 2)
40 REM MOD() ONLY WORKS WITH INTEGERS - RESULTS WITH FLOATING POINT ARE UNRELIABLE
50 REM PRINT MOD(1.2, 0.4)
60 REM THERE IS NO ERROR THROWN HERE. JUST DONT DO IT.

View File

@@ -0,0 +1,3 @@
1
2
0

View File

@@ -0,0 +1,8 @@
10 A# = 255
20 B# = POINTER(A#)
30 IF PEEK(B#) == 255 THEN GOTO 50
40 PRINT "FAILURE: PEEK != 255"
50 POKE B#, 127
60 IF PEEK(B#) == 127 THEN GOTO 80
70 PRINT "FAILURE : POKE DID NOT SET 127"
80 PRINT "SUCCESS"

View File

@@ -0,0 +1 @@
SUCCESS

View File

@@ -0,0 +1 @@
10 PRINT RAD(90)

View File

@@ -0,0 +1 @@
1.570796

View File

@@ -0,0 +1,2 @@
10 PRINT RIGHT("HELLO", 3)
20 PRINT RIGHT("HELLO", 7)

View File

@@ -0,0 +1,2 @@
LLO
HELLO

View File

@@ -0,0 +1,4 @@
10 X# = -1
20 PRINT SGN(X#)
30 PRINT SGN(0)
40 PRINT SGN(1)

View File

@@ -0,0 +1,3 @@
-1
0
1

View File

@@ -0,0 +1,4 @@
10 A# = 8
20 B# = SHL(A#, 2)
30 PRINT B#
40 PRINT SHL(8, 2)

View File

@@ -0,0 +1,2 @@
32
32

View File

@@ -0,0 +1,4 @@
10 A# = 32
20 B# = SHR(A#, 2)
30 PRINT B#
40 PRINT SHR(32, 2)

View File

@@ -0,0 +1,2 @@
8
8

View File

@@ -0,0 +1 @@
10 PRINT SIN(90)

View File

@@ -0,0 +1 @@
0.893997

View File

@@ -0,0 +1 @@
10 PRINT "BASIC IS" + SPC(16) + "FUN"

View File

@@ -0,0 +1 @@
BASIC IS FUN

View File

@@ -0,0 +1,2 @@
10 X% = -123.456
20 PRINT STR(X%)

View File

@@ -0,0 +1 @@
-123.456000

View File

@@ -0,0 +1 @@
10 PRINT TAN(90)

View File

@@ -0,0 +1 @@
-1.995200

View File

@@ -0,0 +1,3 @@
10 PRINT VAL("32")
20 PRINT VAL("123.456")
30 PRINT VAL("-256")

View File

@@ -0,0 +1,3 @@
32.000000
123.456000
-256.000000

View File

@@ -0,0 +1,2 @@
10 PRINT XOR(50, 10)
20 PRINT XOR(25, 5)

View File

@@ -0,0 +1,2 @@
56
28

View File

@@ -0,0 +1,9 @@
5 DEF ADDTWO(A#, B#) = A# + B#
10 DEF ADDTWOSR(A#, B#)
20 C# = A# + B#
30 RETURN C#
40 REM PRINT ADDTWO(3, 5)
45 D# = ADDTWO(3, 5)
50 PRINT D#
55 E# = ADDTWOSR(3, 5)
60 PRINT E#

View File

@@ -0,0 +1,2 @@
8
8

View File

@@ -0,0 +1,5 @@
10 I# = 0
20 LABEL DOITAGAIN
30 I# = I# + 1
40 IF I# <> 3 THEN GOTO DOITAGAIN
50 PRINT "DONE"

View File

@@ -0,0 +1 @@
DONE

View File

@@ -0,0 +1,4 @@
10 READ A$, B#
20 DATA "HELLO", 12345
30 PRINT A$
40 PRINT B#

View File

@@ -0,0 +1,2 @@
HELLO
12345