Files
akbasic/tests/housekeeping_verbs.c

352 lines
13 KiB
C
Raw Permalink Normal View History

/**
* @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);
}
Finish the language: every remaining verb group, and the defects that blocked them Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
/**
* @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());
Finish the language: every remaining verb group, and the defects that blocked them Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
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;
}