Implement the housekeeping verbs, and stop the REPL spinning after an error

NEW, CLR, CONT, SWAP, TRON, TROFF and HELP. None is in the Go reference, so
what each means here is recorded beside it.

Testing CONT turned up a hang that predates this: the runtime's error class is
deliberately sticky, and with run_finished_mode REPL every later step
re-entered REPL and printed READY -- overwriting the QUIT that end of input
had just set. An interactive session that hit one runtime error printed READY
forever instead of exiting.

RESTORE and RENUMBER are deferred with reasons; RESTORE turns out to need a
DATA pointer this port does not have, which is a defect in its own right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 12:07:50 -04:00
parent f4007d80d4
commit bfac13ff87
11 changed files with 658 additions and 1 deletions

271
tests/housekeeping_verbs.c Normal file
View File

@@ -0,0 +1,271 @@
/**
* @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);
}
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());
} 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;
}