Retire the byte-for-byte fidelity constraint, and fix what it was blocking

The Go reference is deprecated and will not be updated, so the two projects
are no longer required to match. Recorded as TODO.md section 0.1, first thing
in the file, because it silently reverses the premise several later sections
were written on -- an agent reading section 6 without it will park work that
is no longer blocked.

Section 6 item 16 was the item waiting on exactly this and is now fixed: the
keyword tables are searched on the base name with any type suffix stripped, so
PRINT$, LEN# and GOTO% are refused as variable names and the reference's own
diagnostic stops being dead code. It cost the one golden case predicted, and
the cost was nothing -- renaming a variable in strreverse.bas left its expected
output byte-for-byte unchanged.

AKBASIC_KNOWN_FAILING_TESTS is empty as a result and known_reference_defects.c
is gone. Section 6 items 1-9 and 11 are reopened as an ordinary defect list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 12:52:00 -04:00
parent eb5374d212
commit 583c0abbd2
9 changed files with 205 additions and 127 deletions

View File

@@ -7,6 +7,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
`akbasic` is a **C rewrite of the Go BASIC interpreter in `deps/basicinterpret`**, written in the same `akbasic` is a **C rewrite of the Go BASIC interpreter in `deps/basicinterpret`**, written in the same
style as the `ak*` C libraries it builds on. style as the `ak*` C libraries it builds on.
**That Go project is deprecated and will not be updated, and this interpreter is no longer required
to match it.** See `TODO.md` §0.1, which is the first thing to read: it retires the byte-for-byte
fidelity constraint that several sections of that file were written on. The reference's corpus is
checked in at `tests/reference/` and stays green as a *regression* suite; the Go source stays
readable as documentation of what the original did. Neither is binding any more.
The repository is currently **empty apart from its submodules** — no commits, no source tree, no build The repository is currently **empty apart from its submodules** — no commits, no source tree, no build
files. The staged content is `.gitmodules` plus four submodules under `deps/`. Everything below the files. The staged content is `.gitmodules` plus four submodules under `deps/`. Everything below the
"Project goals" section describes the material to build from, not code that exists here yet. "Project goals" section describes the material to build from, not code that exists here yet.

View File

@@ -264,8 +264,11 @@ set(AKBASIC_TESTS
set(AKBASIC_WILL_FAIL_TESTS set(AKBASIC_WILL_FAIL_TESTS
) )
# Empty, and worth keeping declared rather than deleted -- libakstdlib left its
# AKSL_KNOWN_FAILING_TESTS in place for the same reason when 0.2.0 cleared it.
# The next defect that has to be reproduced before it can be fixed goes here, and
# the machinery to do that should not have to be reinvented.
set(AKBASIC_KNOWN_FAILING_TESTS set(AKBASIC_KNOWN_FAILING_TESTS
known_reference_defects
) )
foreach(_test IN LISTS AKBASIC_TESTS AKBASIC_WILL_FAIL_TESTS AKBASIC_KNOWN_FAILING_TESTS) foreach(_test IN LISTS AKBASIC_TESTS AKBASIC_WILL_FAIL_TESTS AKBASIC_KNOWN_FAILING_TESTS)

View File

@@ -72,9 +72,11 @@ 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 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. Everything the Go version does, and by now a little more. All 41 `.bas` files of the reference's own corpus produce the expected output, 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. The Go project is deprecated and will not be updated, so the two are **no longer required to match**. That corpus is kept as a regression suite rather than a specification — forty-one real BASIC programs with known-good output are worth having whatever their provenance. Where this interpreter has deliberately improved on the reference it is recorded in `TODO.md` §5, and any case whose expectation moved is listed in the corpus's own README.
That corpus lives at [`tests/reference/`](tests/reference/README.md), copied byte for byte out of the Go project. Its README records where it came from and the rule for changing it: **diverge deliberately, never by accident.** A failure there means this interpreter's behaviour changed — nine times in ten that is a bug you just introduced, and the tenth time it is an improvement that should move the expectation, say why, and add a line to `TODO.md` §5.
## Case Sensitivity ## Case Sensitivity

157
TODO.md
View File

@@ -11,6 +11,32 @@ parallel.
## 0. Agent protocol ## 0. Agent protocol
### 0.1 The Go reference is deprecated. Stop matching it.
**`deps/basicinterpret` is a dead project. It will not be updated, and this interpreter is no
longer required to reproduce its behaviour.** Recorded here first because it silently reverses
the premise several sections of this file were written on, and because an agent that reads them
without this will park work that is no longer blocked.
What it changes:
- **§6 is now an ordinary defect list.** "Port the behaviour first so the port is provably
faithful" is retired. Fix them because they are wrong, not when fidelity permits.
- **§1.8's message-text contract is now a convention.** Improving a message is allowed; it costs
a golden file, which is a cost rather than a veto.
- **§5's bar drops** from "defensible against the golden suite" to defensible on its own merits.
- **`tests/reference/` becomes a regression suite rather than a specification.** Diverging from
it is allowed and must be deliberate and recorded — see its README.
What it does **not** change:
- The corpus stays and stays green. Forty-one real BASIC programs with known-good output are
worth having whatever their provenance, and an unexplained change there is still a red flag.
- The Go source stays readable as documentation. It remains the best answer to "what did the
original actually do here", which is a question worth being able to answer even once the
answer stops being binding.
- Nothing about the `ak*` house rules, which never came from the reference.
**Read these before touching anything**, in this order: **Read these before touching anything**, in this order:
1. `CLAUDE.md` in this repository — project goals, the `libakerror` convention, the error-code 1. `CLAUDE.md` in this repository — project goals, the `libakerror` convention, the error-code
@@ -22,7 +48,8 @@ parallel.
into. §1.9 below says which calls are cleared for use; that section is not optional into. §1.9 below says which calls are cleared for use; that section is not optional
reading, it bans a family of functions the port would otherwise reach for by reflex. reading, it bans a family of functions the port would otherwise reach for by reflex.
5. `deps/libakgl/AGENTS.md` — the no-`malloc` rule and the commit co-author requirement. 5. `deps/libakgl/AGENTS.md` — the no-`malloc` rule and the commit co-author requirement.
6. `deps/basicinterpret/README.md` — the language reference and the unimplemented list. 6. `deps/basicinterpret/README.md` — the language reference and the unimplemented list. Still
worth reading for the verb set and the semantics; no longer binding, per §0.1.
**Rules for working this file:** **Rules for working this file:**
@@ -250,7 +277,7 @@ and the process must not continue as though it does not.
Do not call `akerr_init()` first. Every registry entry point calls it, and since 1.0.0 it no Do not call `akerr_init()` first. Every registry entry point calls it, and since 1.0.0 it no
longer clears reservations made before it ran. longer clears reservations made before it ran.
### 1.8 Error message text is part of the acceptance contract ### 1.8 Error message text is a convention, no longer a contract
`tests/language/array_outofbounds.txt` is, verbatim: `tests/language/array_outofbounds.txt` is, verbatim:
@@ -259,12 +286,19 @@ longer clears reservations made before it ran.
``` ```
The trailing double newline is real: `basicError` builds a string ending in `\n` and hands it The trailing double newline is real: `basicError` builds a string ending in `\n` and hands it
to `Println`, which adds another. **Reproduce the message strings and the newline behaviour to `Println`, which adds another.
byte for byte** or the golden suite fails for reasons that have nothing to do with the
interpreter. Where a Go message reads awkwardly, keep it awkward and note it here.
Numeric formatting must match too: integers via `%" PRId64 "`, floats via `%f` (Go's `%f` and **This used to be a hard contract and is now a default.** The Go implementation is deprecated
C's `%f` both give six decimals — `tests/language/arithmetic/float.txt` confirms). and will not be updated, so the two projects are no longer required to match — see §0.1. What
survives is the practical half: these strings and this newline behaviour are what every
expectation in `tests/reference/` was written against, so changing one means changing golden
files, and that is worth doing on purpose rather than by accident. A message that reads
awkwardly *may* now be improved; do it deliberately, move the expectations in the same commit,
and add a line to §5.
Numeric formatting still matches the reference: integers via `%" PRId64 "`, floats via `%f`
(Go's `%f` and C's `%f` both give six decimals — `tests/reference/language/arithmetic/float.txt`
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**
@@ -322,7 +356,12 @@ ASCII so the symbol tables cannot reach it — see §1.3, which is still accurat
## 2. What exists — **the core port is complete and green** ## 2. What exists — **the core port is complete and green**
Phases 0 through 6 of the original plan are done. The interpreter builds clean under Phases 0 through 6 of the original plan are done. The interpreter builds clean under
`-Wall -Wextra`, reproduces the reference byte for byte, and passes under ASan and UBSan. `-Wall -Wextra`, passes the reference's entire corpus, and passes under ASan and UBSan.
It *did* reproduce the reference byte for byte, and that claim is retired rather than broken:
§0.1 released it, and one case has since diverged deliberately (§6 item 16, listed in
`tests/reference/README.md`). Everything else still matches, which is worth knowing but is no
longer a gate.
```sh ```sh
cmake -S . -B build && cmake --build build --parallel cmake -S . -B build && cmake --build build --parallel
@@ -626,8 +665,13 @@ Also on the queue, from the reference's own defect list:
## 5. Deliberate deviations from the reference ## 5. Deliberate deviations from the reference
Keep this list current. Each of these changes structure without changing observable output, Keep this list current. Most of these change structure without changing observable output;
and each one has to be defensible against the golden suite. the ones that *do* change output say so and carry the golden file they moved.
The bar has dropped since this list was started. It used to be "defensible against the golden
suite", because matching the Go implementation byte for byte was goal 1's headline claim. That
implementation is now deprecated (§0.1), so the bar is the ordinary one: defensible on its own
merits, recorded here, and tested.
1. Reflection → static dispatch table (§1.1). 1. Reflection → static dispatch table (§1.1).
2. Go maps → fixed open-addressed tables (§1.3). 2. Go maps → fixed open-addressed tables (§1.3).
@@ -778,6 +822,17 @@ deviations from the reference's *program*: `main.go` and the SDL half of
driven through the SDL binary, which is where most of the frontend's real coverage comes driven through the SDL binary, which is where most of the frontend's real coverage comes
from. from.
### Deviations that change what a program is allowed to do
30. **A verb name with a type suffix is not a variable name.** `PRINT$ = 1` is refused where
the reference accepts it, because its reserved-word check searched the keyword tables with
the suffix still attached and therefore never matched (§6 item 16). A real C128 refuses it
too, so this is the reference being wrong rather than this interpreter being strict.
**What it costs a program:** a listing that used `INPUT$`, `LEN#` or `GOTO%` as a variable
stops parsing, and the fix is to rename the variable. One case in the reference's own corpus
did exactly that; see `tests/reference/README.md`.
### Deviations in statement separation ### Deviations in statement separation
29. **A branch decides who owns the rest of its line.** BASIC 7.0 scopes every statement after 29. **A branch decides who owns the rest of its line.** BASIC 7.0 scopes every statement after
@@ -803,12 +858,21 @@ deviations from the reference's *program*: `main.go` and the SDL half of
--- ---
## 6. Reference defects — ported faithfully, fix them deliberately ## 6. Reference defects — **fix them; fidelity is no longer a reason not to**
These are real bugs in `deps/basicinterpret`. **Port the behaviour first** so the golden suite These are real bugs in `deps/basicinterpret`, reproduced faithfully during the port.
passes and the port is provably faithful, then fix each one in its own commit with a test that
asserts the correct contract. Until fixed, the correct-contract test lives in **The rule that governed this section is gone.** It used to read "port the behaviour first so
`AKBASIC_KNOWN_FAILING_TESTS`. the golden suite passes and the port is provably faithful, then fix each one deliberately" —
and faithfulness was the reason several of these are still here. The Go implementation is
deprecated and will not be updated (§0.1), so there is no longer anything to be faithful *to*.
Each of these is now an ordinary defect, to be judged on whether the fix is right rather than on
whether it matches.
What has not changed is the working method: one fix per commit, with a test that asserts the
correct contract, and a golden file moved in the same commit if the fix changes output.
`AKBASIC_KNOWN_FAILING_TESTS` is empty now and is kept declared for the next defect that has to
be reproduced before it can be fixed.
1. **`basicvariable.go:176`** — `toString()` tests `len(self.values) == 0` and then indexes 1. **`basicvariable.go:176`** — `toString()` tests `len(self.values) == 0` and then indexes
`self.values[0]`. Inverted condition; on an empty variable it panics, and on a non-empty one `self.values[0]`. Inverted condition; on an empty variable it panics, and on a non-empty one
@@ -907,23 +971,24 @@ question.
the line-number conversion diagnoses by name. Regression tests in `tests/scanner_tokens.c` the line-number conversion diagnoses by name. Regression tests in `tests/scanner_tokens.c`
and `tests/language/numeric/octal_literal.bas`. and `tests/language/numeric/octal_literal.bas`.
16. **`basicscanner.go:349` — the "Reserved word in variable name" check never fires.** The 16. ~~**`basicscanner.go:349` — the "Reserved word in variable name" check never fires.**~~
lexeme still carries its type suffix when the keyword tables are searched, so `PRINT$` **Fixed**, and it is the item §0.1 unblocked. The keyword tables are now searched on the
misses every table and becomes an ordinary string variable. *Consequence:* the diagnostic *base* name with any type suffix stripped, so `PRINT$`, `LEN#` and `GOTO%` are refused as
is dead code and `PRINT$ = 1` is accepted. variable names and the reference's own diagnostic stops being dead code. A name that merely
contains a verb — `PRINTER$` — is still fine, which is what says the check compares the
whole base name rather than a prefix. Regression tests in `tests/scanner_tokens.c`.
**Deliberately not fixed, and this is the interesting one.** The fix is four lines — strip **It cost one golden case, exactly as predicted, and the cost turned out to be nothing.**
the suffix before the lookup — and it works. It also breaks a golden case: the reference's The reference's `examples/strreverse.bas` names a variable `INPUT$`; the variable is renamed
own `deps/basicinterpret/tests/examples/strreverse.bas` names a variable `INPUT$`, and with to `SOURCE$` in `tests/reference/`, the expectation is byte-for-byte unchanged — the program
the check working that line is refused. still prints `REVERSED: OLLEH` — and the case keeps every bit of its coverage. Recorded in
`tests/reference/README.md`'s divergence table, which this is the first entry in.
A real C128 refuses it too, so the reference *program* is the wrong one here, not the This item sat parked because the corpus was the acceptance contract and lived in a submodule
diagnostic. But the corpus is the acceptance contract (§1.8), it lives in a submodule this this repository could not edit. Both premises are gone: the corpus is checked in, and
repository may not edit, and reproducing it byte for byte is goal 1's headline claim. The matching the Go implementation is no longer a goal. Worth keeping the history, because it is
two cannot both hold. Fixing this means deciding that the diagnostic is worth one a clean example of a fix that was correct all along and blocked entirely by a constraint
permanently-excluded upstream case, which is a call for the author rather than for whoever that has since expired.
is next through this file. Until then it stays pinned in
`tests/known_reference_defects.c`, which is now the only thing left in there.
### Ours, not the reference's ### Ours, not the reference's
@@ -1043,15 +1108,17 @@ When the next gap turns up, file it there rather than working around it here.
## 8. Status ## 8. Status
**The port is done.** The C interpreter reproduces the Go reference byte for byte across its **The port is done.** The C interpreter passes the Go reference's entire corpus and passes clean
entire test corpus and passes clean under ASan and UBSan. under ASan and UBSan. It reproduced that corpus byte for byte until §0.1 retired the
requirement; exactly one case has diverged on purpose since, and
`tests/reference/README.md` lists it.
| Gate | Result | | Gate | Result |
|---|---| |---|---|
| `ctest` | 76/76 — 41 upstream golden cases, 9 local ones, 23 unit tests, 2 embedding examples, 1 known-failing | | `ctest` | 77/77 — 41 reference golden cases, 9 local ones, 25 unit tests, 2 embedding examples. No known-failing tests: the list is empty |
| `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` | 76/76 — 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 from `tests/reference/`**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 | 77/77 |
| 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) |
| Warnings | none under `-Wall -Wextra` | | Warnings | none under `-Wall -Wextra` |
@@ -1178,13 +1245,19 @@ What remains, in priority order:
deliberately does not clone. Two apt packages against two more submodules is an easy trade. deliberately does not clone. Two apt packages against two more submodules is an easy trade.
Every step was verified from a clean clone before being written down, not inferred. Every step was verified from a clean clone before being written down, not inferred.
5. **§6 item 16.** Items 10, 1215 and 17 are fixed, each with a regression test in the 5. ~~**§6 items 1217.**~~ **All done**, each with a regression test in the suite that pinned
suite that pinned it. What is left is not more of the same: it. Item 16 was the last, and it was never a coding problem — it was blocked on the
- **Item 16** is a *decision*, not a fix. Making the "Reserved word in variable name" check fidelity constraint §0.1 retired, and took about ten minutes once that went away.
fire costs an upstream golden case, because the reference's own `strreverse.bas` names a `AKBASIC_KNOWN_FAILING_TESTS` is empty as a result and `tests/known_reference_defects.c` is
variable `INPUT$`. §6 states the trade; somebody has to make it. gone.
- ~~**Item 17**~~ **Done**`akbasic_runtime_global()`, `tests/hostvars.c`, and the README
and example both pointed at it. **§6 items 19 and 11 are now open in a way they were not.** They were reproduced
deliberately and left alone because faithfulness was the goal; it is not any more. Read them
as an ordinary defect list. Item 4 (`mathPlus` mutates `self` in place, which `CommandNEXT`
relies on) is the one with real blast radius and still wants `FOR`/`NEXT` coverage before
anybody touches it. Item 5 (binary operators add *both* of the right operand's numeric
fields) is the one worth doing early: it is currently harmless, it makes two mutants in
`src/value.c` unkillable, and it is a landmine for any future value carrying both.
6. **Mutation survivors in `src/value.c`**~~closed~~, with one correction to what was 6. **Mutation survivors in `src/value.c`**~~closed~~, with one correction to what was
written here before. `tests/value_arithmetic.c` grew two functions, written here before. `tests/value_arithmetic.c` grew two functions,
`test_maximum_length_string()` and `test_pool_starts_empty()`, and each was verified by `test_maximum_length_string()` and `test_pool_starts_empty()`, and each was verified by

View File

@@ -224,10 +224,12 @@ static akerr_ErrorContext *match_identifier(akbasic_Runtime *obj)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
char lexeme[AKBASIC_MAX_LINE_LENGTH]; char lexeme[AKBASIC_MAX_LINE_LENGTH];
char basename[AKBASIC_MAX_LINE_LENGTH];
const akbasic_Verb *verb = NULL; const akbasic_Verb *verb = NULL;
void *fndef = NULL; void *fndef = NULL;
bool userfunction = false; bool userfunction = false;
char c = '\0'; char c = '\0';
size_t used = 0;
obj->tokentype = AKBASIC_TOK_IDENTIFIER; obj->tokentype = AKBASIC_TOK_IDENTIFIER;
while ( !is_at_end(obj) ) { while ( !is_at_end(obj) ) {
@@ -260,13 +262,21 @@ static akerr_ErrorContext *match_identifier(akbasic_Runtime *obj)
} }
PASS(errctx, get_lexeme(obj, lexeme, sizeof(lexeme))); PASS(errctx, get_lexeme(obj, lexeme, sizeof(lexeme)));
/* /*
* Searched with the type suffix still attached, which is the reference's * The verb table is searched on the *base* name, with any type suffix
* rule (basicscanner.go:349) and is why the "Reserved word in variable name" * stripped. The reference searches with the suffix still attached
* branch below is unreachable -- TODO.md section 6 item 16, which explains * (basicscanner.go:349), so `PRINT$` misses every table, the "Reserved word
* why it is still here rather than fixed. * in variable name" branch below is dead code, and `PRINT$ = 1` is quietly
* accepted as an ordinary string variable. TODO.md section 6 item 16.
*/ */
PASS(errctx, akbasic_verb_lookup(lexeme, &verb)); strncpy(basename, lexeme, sizeof(basename) - 1);
basename[sizeof(basename) - 1] = '\0';
used = strlen(basename);
if ( obj->tokentype != AKBASIC_TOK_IDENTIFIER && used > 0 ) {
basename[used - 1] = '\0';
}
PASS(errctx, akbasic_verb_lookup(basename, &verb));
ATTEMPT { ATTEMPT {
CATCH(errctx, akbasic_environment_get_function(obj->environment, lexeme, &fndef)); CATCH(errctx, akbasic_environment_get_function(obj->environment, lexeme, &fndef));

View File

@@ -1,46 +0,0 @@
/**
* @file known_reference_defects.c
* @brief Asserts the *correct* contract for defects carried over from the reference.
*
* Registered in AKBASIC_KNOWN_FAILING_TESTS, so CTest expects it to fail. Every
* assertion here states what the interpreter *should* do, not what it currently
* does -- pinning current-but-wrong behaviour would turn the eventual fix into a
* test failure.
*
* When one of these is fixed, this test starts passing and CTest reports
* "unexpectedly passed". That is the cue to split the fixed assertion out into
* the corresponding suite in AKBASIC_TESTS and strike the item from TODO.md
* section 6.
*
* Five of the six this file was written with are gone that way. What is left is
* the one whose fix is not a coding question but a decision, and it is described
* where it is asserted.
*/
#include "harness.h"
int main(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
/*
* 6.16 -- "Reserved word in variable name" never fires, because the lexeme
* still carries its type suffix when the verb table is searched, so PRINT$
* misses every table and becomes an ordinary string variable.
*
* Still here rather than fixed, and the reason is worth knowing before
* anybody "just fixes it": stripping the suffix before the lookup is four
* lines and it works, and it costs a golden case. The reference's own
* examples/strreverse.bas names a variable INPUT$ -- which a real C128 would
* refuse too, so the reference program is the wrong one. But the corpus is
* the acceptance contract and it lives in a submodule this repository may
* not edit, so the diagnostic and the corpus cannot both be right. TODO.md
* section 6 records the trade rather than making it silently.
*/
HARNESS_RUNTIME.hasError = false;
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT$ = 1", NULL, 0));
TEST_REQUIRE(HARNESS_RUNTIME.hasError, "PRINT$ should be refused as a variable name");
harness_stop();
return akbasic_test_failures;
}

View File

@@ -14,36 +14,49 @@ interpreter was ported from:
They are registered as the `golden_*` CTest cases and run against `basic` on every build, in 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. both the default and the `-DAKBASIC_WITH_AKGL=ON` configurations.
## Why they are a copy now, when they deliberately were not ## 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 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 was written down in `CMakeLists.txt`: the corpus is a submodule, and copying a submodule's
corpus guarantees drift. corpus guarantees drift.
That reasoning was sound. It was overruled on purpose, because the Go dependency is being That reasoning was sound and it stopped applying. The Go project is deprecated and will not be
deprecated and a build that cannot run its own acceptance suite without cloning the updated, so there is nothing left to drift *from* — and a build that cannot run its own
implementation it replaced is not finished. 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 ## What these are for now
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 **A regression suite, not a specification.** That distinction is the whole of how to treat
corpus that stops tracking it is a corpus that stopped tracking something that stopped moving. 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 ## The rule for editing these
**Don't.** **Diverge deliberately, never by accident.**
This directory is a *record of what the Go implementation did*, and it is what makes the claim A failure here means this interpreter's behaviour changed. Nine times in ten that is a bug you
"reproduces the reference byte for byte" checkable at all. A failure here means this just introduced and the answer is to fix the code. The tenth time it is an improvement on the
interpreter's behaviour changed, and the answer is to change this interpreter or to record a reference, and then the answer is:
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 1. Change the expectation in the same commit as the code.
`local_*` for exactly this reason: the prefix says which corpus a failure came from, and a diff 2. Say why in the commit message.
that touches `tests/reference/` stands out as the thing it is. 3. Add a line to `TODO.md` §5, which is the list of deliberate deviations.
There is one case worth knowing about before anybody "fixes" it. What is not acceptable is editing a `.txt` to make a red suite green without deciding which of
`examples/strreverse.bas` names a variable `INPUT$`, which a real Commodore 128 would refuse the two you are doing.
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 New cases for this project's own behaviour go in `tests/language/`, registered under `local_*`.
rather than a defect: making the check work costs this case. 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 |
|---|---|---|
| `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. |

View File

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

View File

@@ -150,15 +150,32 @@ int main(void)
TEST_REQUIRE_INT(env()->lineno, 10); TEST_REQUIRE_INT(env()->lineno, 10);
/* /*
* The "Reserved word in variable name" check never fires: the lexeme still * A verb name with a type suffix stuck on it is refused as a variable name.
* carries its suffix when the verb table is searched, so "PRINT$" misses and * The reference searches the keyword tables with the suffix still attached
* becomes an ordinary string variable. Pinned as it stands -- TODO.md * (basicscanner.go:349), so "PRINT$" misses every table, its own "Reserved
* section 12 item 16. * word in variable name" diagnostic is dead code, and `PRINT$ = 1` is
* quietly accepted. TODO.md section 6 item 16; this is its regression test.
*/ */
HARNESS_RUNTIME.hasError = false;
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT$ = 1", NULL, 0)); TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT$ = 1", NULL, 0));
TEST_REQUIRE(!HARNESS_RUNTIME.hasError, TEST_REQUIRE(HARNESS_RUNTIME.hasError, "PRINT$ must be refused as a variable name");
"PRINT$ is currently accepted as a variable name; see TODO.md 12.16");
expect_token(0, AKBASIC_TOK_IDENTIFIER_STRING, "PRINT$"); /* The same for a function name, and for the other two suffixes. */
HARNESS_RUNTIME.hasError = false;
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "LEN# = 1", NULL, 0));
TEST_REQUIRE(HARNESS_RUNTIME.hasError, "LEN# must be refused as a variable name");
HARNESS_RUNTIME.hasError = false;
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "GOTO% = 1", NULL, 0));
TEST_REQUIRE(HARNESS_RUNTIME.hasError, "GOTO%% must be refused as a variable name");
/*
* And an ordinary name that merely *contains* a verb is still fine, which is
* what says the check compares the whole base name rather than a prefix.
*/
HARNESS_RUNTIME.hasError = false;
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINTER$ = 1", NULL, 0));
TEST_REQUIRE(!HARNESS_RUNTIME.hasError, "PRINTER$ is not a reserved word");
expect_token(0, AKBASIC_TOK_IDENTIFIER_STRING, "PRINTER$");
/* An unknown character is diagnosed. */ /* An unknown character is diagnosed. */
HARNESS_RUNTIME.hasError = false; HARNESS_RUNTIME.hasError = false;