File the scanned line in RUNSTREAM, not the raw one

process_line_runstream() chose between the stripped line and the raw buffer
on obj->mode == AKBASIC_MODE_REPL, but nothing reaches that function except
the RUNSTREAM arm of step(). The stripped branch was dead, so every program
the driver ran from a file was stored with its own line number inside
source[]: LIST printed "10 10 PRINT", and DSAVE and HELP would have echoed
it the same way.

No .bas in either corpus calls LIST, and runtime_verbs.c's load() helper --
which claims to load "the way RUNSTREAM would" -- already stored the scanned
line, so it encoded the correct contract and could not catch the wrong one.
The new test drives the real sink path instead.

Keep the step-over-a-leading-number allowance in scan_line_labels() and
skip_lineno(): no path inside the library stores a raw line now, but
akbasic_runtime_store_line() is public and a host may hand it one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 16:25:05 -04:00
parent 812d846e47
commit d2ae0d75de
4 changed files with 66 additions and 16 deletions

19
TODO.md
View File

@@ -1878,6 +1878,25 @@ situation as one typed at a prompt. Wants a test in `tests/scanner_tokens.c` ass
error line rather than the raised status, and one in `tests/housekeeping_verbs.c` asserting
the REPL survives it — the shape item 2's test already has.
**A separate one was found while designing optional line numbers, and is fixed.**
`akbasic_runtime_process_line_runstream()` filed the *raw* buffer, line number and all, while
`akbasic_runtime_load()` and `DLOAD` filed what the scanner had stripped. The `if` that chose
between them tested `obj->mode == AKBASIC_MODE_REPL`, and nothing reaches that function except
the `RUNSTREAM` arm of `step()` — so the stripped branch was dead and every program the driver
ran from a file was stored with its own number inside `source[]`. `LIST` printed
`10 10 PRINT "ONE"`; `DSAVE` would have written it that way and `HELP` echoed it that way.
It went unseen because no `.bas` in either corpus calls `LIST`, and because the one test helper
that claimed to load "the way RUNSTREAM would" (`tests/runtime_verbs.c:30`) stored the scanned
line — that is, it encoded the *correct* contract and so could never catch the wrong one.
`src/runtime.c` now files `scanned`. `tests/runtime_verbs.c` drives the real sink path and
fails with `10 10 PRINT "ONE"` when the fix is reverted.
The step-over-a-leading-number allowance in `scan_line_labels()` (`src/runtime.c:1235`) and
`skip_lineno()` (`src/structtype.c:42`) is kept, and both comments now say why: no path inside
the library stores a raw line any more, but `akbasic_runtime_store_line()` is public and a host
may hand it one.
Coverage of the verb groups added for goal 2, since they are the new surface: `src/audio_tables.c`
and `src/graphics_tables.c` 100%, `src/runtime_input.c` 100%, `src/runtime_graphics.c` and
`src/runtime_audio.c` 98%, `src/play.c` 87%. The `akbasic_akgl` target is not in the coverage