diff --git a/TODO.md b/TODO.md index d2d48a7..af030a3 100644 --- a/TODO.md +++ b/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 diff --git a/src/runtime.c b/src/runtime.c index 9de8d2b..738a533 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -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)); - } + PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned)); 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) ) { diff --git a/src/structtype.c b/src/structtype.c index e27974e..e333664 100644 --- a/src/structtype.c +++ b/src/structtype.c @@ -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) diff --git a/tests/runtime_verbs.c b/tests/runtime_verbs.c index 6022d7d..b328ab8 100644 --- a/tests/runtime_verbs.c +++ b/tests/runtime_verbs.c @@ -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; }