Two defects that the same mechanism produced, both found by making the examples in docs/ actually run. DLOAD scans the file it reads through the runtime's own scanner, so by the time it returns, the tokens of the line that invoked it are gone and hadlinenumber and environment->lineno describe the last line of the *file*. The REPL carried on parsing what it believed was still its own buffer, concluded 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. It now abandons the rest of the line, which is the only thing that can sensibly follow replacing the whole program. The second is the same class of oversight one layer up. process_line_run() has always swallowed the error context after interpret() puts the BASIC-level line on the sink, with a comment saying that is what goal 3 requires. The direct-mode branch of process_line_repl() used a bare PASS instead, so `VERIFY` against a file that did not match -- an ordinary user answer, not a fault -- came back as a stack trace and terminated the driver. An embedding host would have gone with it. Both tests fail when their fix is reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
352 lines
13 KiB
C
352 lines
13 KiB
C
/**
|
|
* @file housekeeping_verbs.c
|
|
* @brief Tests the group B verbs: NEW, CLR, CONT, TRON, TROFF, SWAP and HELP.
|
|
*
|
|
* The golden case at tests/language/housekeeping/verbs.bas covers what a running
|
|
* program can see of these. What is here is the rest: the refusals, the two
|
|
* verbs that only mean anything at a REPL prompt (CONT and HELP), and the state
|
|
* a program cannot PRINT.
|
|
*/
|
|
|
|
#include "harness.h"
|
|
|
|
/** @brief Run a program to completion in RUN mode, from a string. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Evaluate one immediate line against the current runtime state. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *immediate(const char *line)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *leaf = NULL;
|
|
akbasic_Value *out = NULL;
|
|
|
|
PASS(errctx, akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
|
PASS(errctx, harness_parse(line, &leaf));
|
|
PASS(errctx, akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief NEW empties the program *and* the variables; CLR keeps the program. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_new_and_clr(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT \"KEPT\"\n"));
|
|
PASS(errctx, immediate("A# = 5"));
|
|
|
|
PASS(errctx, immediate("CLR"));
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"KEPT\"");
|
|
PASS(errctx, immediate("PRINT A#"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n");
|
|
|
|
PASS(errctx, immediate("NEW"));
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->lineno, 0);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 0);
|
|
|
|
harness_stop();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief CLR releases the value pool, so an array can be re-DIMmed indefinitely.
|
|
*
|
|
* The pool is a bump allocator and re-DIMming larger abandons the old slice
|
|
* (see akbasic_ValuePool), so a program that grows an array in a loop exhausts
|
|
* it. CLR is the only thing that hands the storage back, which makes this the
|
|
* assertion that says CLR did something rather than merely appearing to.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_clr_reclaims_storage(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
int i = 0;
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
|
|
/* Without CLR this runs out; the loop count is well past the pool's size. */
|
|
for ( i = 0; i < 40; i++ ) {
|
|
PASS(errctx, immediate("DIM Z#(200)"));
|
|
PASS(errctx, immediate("CLR"));
|
|
}
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.valuepool.next, 0);
|
|
|
|
harness_stop();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief CONT refuses when nothing stopped, and resumes when something did. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_cont(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *leaf = NULL;
|
|
akbasic_Value *out = NULL;
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
|
|
/* Nothing has run, so there is nothing to continue. */
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
|
TEST_REQUIRE_OK(harness_parse("CONT", &leaf));
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
|
|
AKBASIC_ERR_STATE);
|
|
|
|
/* A STOP part-way through, and then a CONT that finishes the program. */
|
|
PASS(errctx, run_program("10 PRINT \"BEFORE\"\n"
|
|
"20 STOP\n"
|
|
"30 PRINT \"AFTER\"\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\nREADY\n");
|
|
TEST_REQUIRE(HARNESS_RUNTIME.stopped, "STOP should have armed CONT");
|
|
|
|
PASS(errctx, immediate("CONT"));
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
/*
|
|
* The READY is STOP's, not the end of the program's: akbasic_runtime_start()
|
|
* in RUN mode sets run_finished_mode to QUIT, so a program that runs off its
|
|
* own end stops rather than dropping to a prompt.
|
|
*/
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\nREADY\nAFTER\n");
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.stopped, "CONT should have disarmed itself");
|
|
|
|
/* And a second CONT has nothing left to resume. */
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
|
TEST_REQUIRE_OK(harness_parse("CONT", &leaf));
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
|
|
AKBASIC_ERR_STATE);
|
|
|
|
harness_stop();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief A RUN takes the CONT away: the program is starting over, not resuming. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_run_disarms_cont(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
PASS(errctx, run_program("10 STOP\n"));
|
|
TEST_REQUIRE(HARNESS_RUNTIME.stopped, "STOP should have armed CONT");
|
|
|
|
PASS(errctx, immediate("RUN"));
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.stopped, "RUN should have disarmed CONT");
|
|
|
|
harness_stop();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief SWAP refuses what it cannot exchange, and tolerates the identity case. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_swap_refusals(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *leaf = NULL;
|
|
akbasic_Value *out = NULL;
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
PASS(errctx, immediate("A# = 1"));
|
|
PASS(errctx, immediate("S$ = \"X\""));
|
|
|
|
/* Different types: the suffix is the only type declaration there is. */
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
|
TEST_REQUIRE_OK(harness_parse("SWAP A#, S$", &leaf));
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
|
|
AKBASIC_ERR_TYPE);
|
|
|
|
/* Not a variable. */
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
|
TEST_REQUIRE_OK(harness_parse("SWAP A#, 5", &leaf));
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
|
|
AKBASIC_ERR_SYNTAX);
|
|
|
|
/* Only one of them. */
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
|
TEST_REQUIRE_OK(harness_parse("SWAP A#", &leaf));
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
|
|
AKBASIC_ERR_SYNTAX);
|
|
|
|
/* A variable with itself is legal and leaves it alone, rather than corrupting it. */
|
|
PASS(errctx, immediate("SWAP A#, A#"));
|
|
PASS(errctx, immediate("PRINT A#"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n");
|
|
|
|
harness_stop();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief HELP re-lists the line the last error happened on, and says so when there is none. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_help(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
PASS(errctx, immediate("HELP"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "NO ERROR TO HELP WITH\n");
|
|
|
|
/* An out-of-bounds read on line 20, then HELP. */
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
PASS(errctx, run_program("10 DIM Q#(2)\n"
|
|
"20 PRINT Q#(9)\n"
|
|
"30 PRINT \"UNREACHED\"\n"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.errorline, 20);
|
|
PASS(errctx, immediate("HELP"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "20 PRINT Q#(9)") != NULL,
|
|
"HELP should re-list the line the error happened on, got: %s", HARNESS_OUTPUT);
|
|
|
|
harness_stop();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief TRON and TROFF move one flag, and the flag is what the run loop reads. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_trace_flag(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.trace, "tracing starts off");
|
|
PASS(errctx, immediate("TRON"));
|
|
TEST_REQUIRE(HARNESS_RUNTIME.trace, "TRON turns tracing on");
|
|
PASS(errctx, immediate("TROFF"));
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.trace, "TROFF turns tracing off");
|
|
|
|
harness_stop();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief An interactive session that errors and then ends does not spin.
|
|
*
|
|
* Not a group B verb, but found while testing them and fixed alongside: the
|
|
* runtime's error class is deliberately sticky, and with run_finished_mode REPL
|
|
* that meant every later step re-entered REPL mode and printed READY again --
|
|
* overwriting the QUIT that end of input had just set. A session that hit one
|
|
* runtime error printed READY forever instead of exiting.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_repl_stops_after_an_error(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
int steps = 0;
|
|
|
|
TEST_REQUIRE_OK(harness_start("10 PRINT NOSUCHTHING\nRUN\n"));
|
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL));
|
|
|
|
/* Bounded, so a regression fails this test rather than hanging the suite. */
|
|
while ( steps < 64 && HARNESS_RUNTIME.mode != AKBASIC_MODE_QUIT ) {
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
|
|
steps += 1;
|
|
}
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT);
|
|
TEST_REQUIRE(steps < 64, "the REPL should quit at end of input, not spin");
|
|
|
|
harness_stop();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief A statement typed with no line number runs; one with a number is stored.
|
|
*
|
|
* Direct mode. The reference only ran the verbs it marked immediate -- RUN, LIST,
|
|
* NEW and the rest -- and filed everything else as program text, so `PRINT 2 + 2`
|
|
* at a prompt silently became line 0 of a program instead of answering. See
|
|
* TODO.md section 5.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_direct_mode(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
TEST_REQUIRE_OK(harness_start("PRINT 2 + 2\n"
|
|
"A# = 5\n"
|
|
"PRINT A# * 3\n"
|
|
"10 PRINT \"STORED\"\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, "4\n") != NULL,
|
|
"PRINT 2 + 2 should have answered 4, got \"%s\"", HARNESS_OUTPUT);
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "15\n") != NULL,
|
|
"an assignment then a PRINT should both have run, got \"%s\"",
|
|
HARNESS_OUTPUT);
|
|
/* And the numbered line was stored rather than run when it was typed. */
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"STORED\"");
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "STORED") != NULL,
|
|
"RUN should have run the stored line, got \"%s\"", HARNESS_OUTPUT);
|
|
|
|
harness_stop();
|
|
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);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, test_new_and_clr());
|
|
CATCH(errctx, test_clr_reclaims_storage());
|
|
CATCH(errctx, test_cont());
|
|
CATCH(errctx, test_run_disarms_cont());
|
|
CATCH(errctx, test_swap_refusals());
|
|
CATCH(errctx, test_help());
|
|
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) {
|
|
LOG_ERROR_WITH_MESSAGE(errctx, "housekeeping verb test failed");
|
|
akbasic_test_failures += 1;
|
|
} FINISH_NORETURN(errctx);
|
|
|
|
return akbasic_test_failures;
|
|
}
|