diff --git a/src/runtime.c b/src/runtime.c index cf095cb..1be1d01 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -856,7 +856,20 @@ akerr_ErrorContext *akbasic_runtime_process_line_repl(akbasic_Runtime *obj) * was unreachable from a prompt. */ if ( !obj->hadlinenumber ) { - PASS(errctx, akbasic_runtime_interpret(obj, leaf, &value)); + /* + * Swallow the context exactly as process_line_run() does, and for the + * same reason: interpret() has already put the BASIC-visible line on + * the sink, and letting the error out of here hands a *script's* + * mistake to the host. A bare PASS here meant `VERIFY` against a file + * that did not match -- an ordinary user answer -- terminated the + * driver with a stack trace instead of printing an error line. + */ + ATTEMPT { + CATCH(errctx, akbasic_runtime_interpret(obj, leaf, &value)); + } CLEANUP { + } PROCESS(errctx) { + } HANDLE_DEFAULT(errctx) { + } FINISH(errctx, false); if ( obj->errclass != AKBASIC_ERRCLASS_NONE ) { SUCCEED_RETURN(errctx); } diff --git a/src/runtime_commands.c b/src/runtime_commands.c index ef6e2ef..f071c6b 100644 --- a/src/runtime_commands.c +++ b/src/runtime_commands.c @@ -556,6 +556,16 @@ akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *exp } PROCESS(errctx) { } FINISH(errctx, true); + /* + * Abandon the rest of the line that ran this. Scanning the file above went + * through the runtime's own scanner, so the tokens of the calling line are + * gone and `hadlinenumber` and `environment->lineno` now describe the last + * line of the *file*. Carrying on would parse whatever the scanner happens + * to be holding and, in REPL mode, file the DLOAD command itself as program + * text under that line number -- which silently overwrote the last line of + * every program loaded this way. + */ + obj->skiprestofline = true; SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } diff --git a/tests/disk_verbs.c b/tests/disk_verbs.c index 525dc71..f8e3ac7 100644 --- a/tests/disk_verbs.c +++ b/tests/disk_verbs.c @@ -264,6 +264,52 @@ static void test_dclear(void) remove("t_clear.txt"); } +/** + * @brief DLOAD from a prompt loads the whole program and nothing else. + * + * It used to lose the last line. DLOAD scans the file through the runtime's own + * scanner, so by the time it returns, the tokens of the line that ran it are + * gone and `hadlinenumber` and `environment->lineno` describe the last line of + * the *file*. The REPL then carried on parsing the buffer it thought was still + * its own, decided the leftovers were program text because hadlinenumber was + * now true, and filed the `DLOAD` command itself under that line number -- + * silently replacing the last line of every program loaded from a prompt. + * + * Found while making the examples in docs/ execute: a save/load round trip is + * the second thing chapter 2 shows a reader. + */ +static akerr_ErrorContext AKERR_NOIGNORE *test_dload_keeps_the_last_line(void) +{ + PREPARE_ERROR(errctx); + + remove("t_round.bas"); + TEST_REQUIRE_OK(harness_start("10 PRINT \"FIRST\"\n" + "20 PRINT \"SECOND\"\n" + "30 PRINT \"THIRD\"\n" + "DSAVE \"t_round.bas\"\n" + "NEW\n" + "DLOAD \"t_round.bas\"\n" + "LIST\n" + "RUN\n")); + PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL)); + PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 200)); + + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "FIRST\nSECOND\nTHIRD\n") != NULL, + "a save/load round trip should run all three lines, got \"%s\"", + HARNESS_OUTPUT); + /* + * And the command is not in the program. LIST rather than the run output, + * because a clobbered last line reloads the file and re-runs it, which + * prints the right text for the wrong reason. + */ + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "DLOAD") == NULL, + "the DLOAD command should not have been filed as program text, got \"%s\"", + HARNESS_OUTPUT); + harness_stop(); + remove("t_round.bas"); + SUCCEED_RETURN(errctx); +} + int main(void) { test_channel_round_trip(); @@ -276,5 +322,6 @@ int main(void) test_bsave_bload(); test_no_drive_verbs(); test_dclear(); + IGNORE(test_dload_keeps_the_last_line()); return akbasic_test_failures; } diff --git a/tests/housekeeping_verbs.c b/tests/housekeeping_verbs.c index 0e83f9e..966e382 100644 --- a/tests/housekeeping_verbs.c +++ b/tests/housekeeping_verbs.c @@ -281,6 +281,50 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_direct_mode(void) SUCCEED_RETURN(errctx); } +/** + * @brief A BASIC error typed at the prompt is reported, not raised at the host. + * + * Goal 3: a script's mistakes are the script's problem. process_line_run() has + * always swallowed the context after interpret() put the error line on the sink, + * but the direct-mode branch of process_line_repl() used a bare PASS and let it + * out -- so `VERIFY` against a file that did not match, which is an ordinary + * answer rather than a fault, terminated the driver with a stack trace and took + * an embedding host with it. + * + * VERIFY is used because it is the shortest verb that raises through + * report_and_reraise() with a message a reader would recognise as their own + * mistake. Found while making the examples in docs/ execute. + */ +static akerr_ErrorContext AKERR_NOIGNORE *test_direct_mode_error_stays_inside(void) +{ + PREPARE_ERROR(errctx); + + remove("t_direct.bas"); + TEST_REQUIRE_OK(harness_start("10 PRINT \"HI\"\n" + "DSAVE \"t_direct.bas\"\n" + "20 PRINT \"EXTRA\"\n" + "VERIFY \"t_direct.bas\"\n" + "PRINT \"STILL HERE\"\n")); + PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL)); + + /* + * PASS, not CATCH: the failure being tested is an error escaping this call, + * so letting it reach this test's own handler is the assertion. + */ + PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 200)); + + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "does not match") != NULL, + "the mismatch should have been reported to the sink, got \"%s\"", + HARNESS_OUTPUT); + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "STILL HERE") != NULL, + "the prompt should have carried on after the error, got \"%s\"", + HARNESS_OUTPUT); + + harness_stop(); + remove("t_direct.bas"); + SUCCEED_RETURN(errctx); +} + int main(void) { PREPARE_ERROR(errctx); @@ -295,6 +339,7 @@ int main(void) CATCH(errctx, test_trace_flag()); CATCH(errctx, test_repl_stops_after_an_error()); CATCH(errctx, test_direct_mode()); + CATCH(errctx, test_direct_mode_error_stays_inside()); } CLEANUP { } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) {