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:
19
TODO.md
19
TODO.md
@@ -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
|
||||
|
||||
@@ -987,15 +987,19 @@ akerr_ErrorContext *akbasic_runtime_process_line_runstream(akbasic_Runtime *obj)
|
||||
|
||||
/*
|
||||
* All this mode does is pick the line number off the front and file the
|
||||
* source line under it. DLOAD reaches this from REPL mode, where the line
|
||||
* numbers must be stripped the same way the REPL strips them.
|
||||
* source line under it. What is filed is what the scanner handed back, with
|
||||
* the number stripped -- the same text `akbasic_runtime_load()` and `DLOAD`
|
||||
* file, so a program is stored identically however it arrived.
|
||||
*
|
||||
* It used to file the raw buffer here, number and all, on the theory that
|
||||
* only a REPL-driven load needed stripping. Nothing reaches this function
|
||||
* except the RUNSTREAM arm of step(), so that was every file the driver
|
||||
* runs: `source[]` held `10 PRINT "A"` under slot 10, and LIST, DSAVE and
|
||||
* HELP would each have printed the number twice. No .bas in either corpus
|
||||
* calls LIST, which is why it went unseen.
|
||||
*/
|
||||
PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned)));
|
||||
if ( obj->mode == AKBASIC_MODE_REPL ) {
|
||||
PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned));
|
||||
} else {
|
||||
PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, buffer));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -1229,12 +1233,11 @@ static akerr_ErrorContext *scan_line_labels(akbasic_Environment *root, const cha
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* A stored line may still carry its own line number. RUNSTREAM files the
|
||||
* raw text and lets the scanner strip the number again on the way to
|
||||
* execution, where akbasic_runtime_load() files what the scanner already
|
||||
* stripped -- so "30 LABEL X" and "LABEL X" are both real spellings of
|
||||
* source[30], depending on how the program arrived. Step over the number
|
||||
* without ending the statement.
|
||||
* A stored line may still carry its own line number. Every path inside the
|
||||
* library files what the scanner already stripped, but
|
||||
* akbasic_runtime_store_line() is public and a host may hand it a raw line
|
||||
* -- so "30 LABEL X" and "LABEL X" are both real spellings of source[30].
|
||||
* Step over the number without ending the statement.
|
||||
*/
|
||||
if ( statementstart && isdigit((unsigned char)*cursor) ) {
|
||||
while ( isdigit((unsigned char)*cursor) ) {
|
||||
|
||||
@@ -42,9 +42,9 @@ static const char *skip_space(const char *cursor)
|
||||
/**
|
||||
* @brief Step over a stored line's own line number, if it still carries one.
|
||||
*
|
||||
* A line arrives either from akbasic_runtime_load(), which files what the
|
||||
* scanner already stripped, or from RUNSTREAM, which files the raw text. Both
|
||||
* spellings are real, so both are handled here -- the same allowance
|
||||
* Every path inside the library files what the scanner already stripped, but
|
||||
* akbasic_runtime_store_line() is public and a host may hand it a raw line, so
|
||||
* both spellings are real and both are handled here -- the same allowance
|
||||
* scan_line_labels() makes.
|
||||
*/
|
||||
static const char *skip_lineno(const char *cursor)
|
||||
|
||||
@@ -47,6 +47,32 @@ static void clear_output(void)
|
||||
rewind(HARNESS_OUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A program read through RUNSTREAM is stored without its line numbers.
|
||||
*
|
||||
* The load() helper above files what the scanner stripped, which is what every
|
||||
* caller inside the library does -- but RUNSTREAM used to file the raw buffer,
|
||||
* so a program run from a file held "10 PRINT" under slot 10 and LIST, DSAVE
|
||||
* and HELP would each have printed the number twice. This drives the real sink
|
||||
* path rather than the helper, because the helper is exactly what was not
|
||||
* happening.
|
||||
*/
|
||||
static void test_runstream_strips_line_numbers(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(harness_start("10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n"));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUNSTREAM));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||||
|
||||
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"ONE\"");
|
||||
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[20].code, "PRINT \"TWO\"");
|
||||
|
||||
/* And LIST therefore prints each number once. */
|
||||
clear_output();
|
||||
TEST_REQUIRE_OK(run_line("LIST"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const char *PROGRAM[] = {
|
||||
@@ -241,5 +267,7 @@ int main(void)
|
||||
TEST_REQUIRE_STATUS(run_line("Y# = PEEK(Z#)"), AKBASIC_ERR_VALUE);
|
||||
|
||||
harness_stop();
|
||||
|
||||
test_runstream_strips_line_numbers();
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user