Files
akbasic/tests/runtime_verbs.c
Andrew Kesterson d2ae0d75de File the scanned line in RUNSTREAM, not the raw one
process_line_runstream() chose between the stripped line and the raw buffer
on obj->mode == AKBASIC_MODE_REPL, but nothing reaches that function except
the RUNSTREAM arm of step(). The stripped branch was dead, so every program
the driver ran from a file was stored with its own line number inside
source[]: LIST printed "10 10 PRINT", and DSAVE and HELP would have echoed
it the same way.

No .bas in either corpus calls LIST, and runtime_verbs.c's load() helper --
which claims to load "the way RUNSTREAM would" -- already stored the scanned
line, so it encoded the correct contract and could not catch the wrong one.
The new test drives the real sink path instead.

Keep the step-over-a-leading-number allowance in scan_line_labels() and
skip_lineno(): no path inside the library stores a raw line now, but
akbasic_runtime_store_line() is public and a host may hand it one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:25:05 -04:00

274 lines
11 KiB
C

/**
* @file runtime_verbs.c
* @brief Tests the verbs the golden corpus never reaches.
*
* LIST, DELETE, AUTO, INPUT, DLOAD, DSAVE, EXIT, STOP and the memory verbs are
* all real code with no `.bas`/`.txt` pair behind them, because the reference's
* own corpus never exercises them either. They are driven through a whole
* runtime here rather than unit-tested in pieces: most of them exist to mutate
* runtime state, and that is the thing worth asserting.
*/
#include <stdio.h>
#include <unistd.h>
#include "harness.h"
/* Run one line as though it had been typed at the REPL. */
static akerr_ErrorContext AKERR_NOIGNORE *run_line(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);
}
/* Load a program into the source table the way RUNSTREAM would. */
static void load(const char *lines[], int count)
{
char scanned[AKBASIC_MAX_LINE_LENGTH];
int i = 0;
for ( i = 0; i < count; i++ ) {
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, lines[i], scanned, sizeof(scanned)));
TEST_REQUIRE_OK(akbasic_runtime_store_line(&HARNESS_RUNTIME,
HARNESS_RUNTIME.environment->lineno, scanned));
}
}
static void clear_output(void)
{
memset(HARNESS_OUTPUT, 0, sizeof(HARNESS_OUTPUT));
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[] = {
"10 PRINT \"ONE\"",
"20 PRINT \"TWO\"",
"30 PRINT \"THREE\"",
};
char tmpname[] = "/tmp/akbasic_dsave_XXXXXX";
char buffer[256];
akbasic_Value *out = NULL;
FILE *saved = NULL;
int fd = -1;
TEST_REQUIRE_OK(harness_start("42\n"));
/* PRINT with no argument emits a bare newline. */
clear_output();
TEST_REQUIRE_OK(run_line("PRINT"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "\n");
/*
* LET does nothing; the assignment already happened during evaluation. A
* bare LET with no assignment after it is a parse error, in the reference
* too -- its parse path is just assignment(), which needs an expression.
*/
TEST_REQUIRE_OK(run_line("LET A# = 5"));
TEST_REQUIRE_ANY_ERROR(run_line("LET"));
/* LIST prints the program with its line numbers restored. */
load(PROGRAM, 3);
clear_output();
TEST_REQUIRE_OK(run_line("LIST"));
TEST_REQUIRE_STR(HARNESS_OUTPUT,
"10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");
/* LIST n lists from n to the end. */
clear_output();
TEST_REQUIRE_OK(run_line("LIST 20"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");
/* LIST -n lists from the start through n. */
clear_output();
TEST_REQUIRE_OK(run_line("LIST -20"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n");
/* LIST n-n lists an inclusive range. */
clear_output();
TEST_REQUIRE_OK(run_line("LIST 20-30"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");
/* DSAVE writes the program out; DLOAD reads it back. */
fd = mkstemp(tmpname);
TEST_REQUIRE(fd >= 0, "could not create a temporary file");
if ( fd >= 0 ) {
close(fd);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
snprintf(buffer, sizeof(buffer), "DSAVE \"%s\"", tmpname);
TEST_REQUIRE_OK(run_line(buffer));
saved = fopen(tmpname, "r");
TEST_REQUIRE(saved != NULL, "DSAVE produced no file");
if ( saved != NULL ) {
TEST_REQUIRE(fgets(buffer, sizeof(buffer), saved) != NULL, "DSAVE wrote nothing");
TEST_REQUIRE_STR(buffer, "10 PRINT \"ONE\"\n");
fclose(saved);
}
/* DELETE with no range clears the whole program. */
TEST_REQUIRE_OK(run_line("DELETE"));
clear_output();
TEST_REQUIRE_OK(run_line("LIST"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
/* DLOAD brings it back. */
snprintf(buffer, sizeof(buffer), "DLOAD \"%s\"", tmpname);
TEST_REQUIRE_OK(run_line(buffer));
clear_output();
TEST_REQUIRE_OK(run_line("LIST"));
TEST_REQUIRE_STR(HARNESS_OUTPUT,
"10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");
remove(tmpname);
}
/* DELETE n-n removes an inclusive range. */
TEST_REQUIRE_OK(run_line("DELETE 20-30"));
clear_output();
TEST_REQUIRE_OK(run_line("LIST"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n");
/* A missing file is an error, not a crash. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_ANY_ERROR(run_line("DLOAD \"/nonexistent/akbasic/nope.bas\""));
/* An empty filename is refused before it reaches aksl_fopen. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("DLOAD \"\""), AKBASIC_ERR_VALUE);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("DLOAD 5"), AKBASIC_ERR_TYPE);
/* AUTO sets and clears the line-numbering increment. */
TEST_REQUIRE_OK(run_line("AUTO 10"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.autoLineNumber, 10);
TEST_REQUIRE_OK(run_line("AUTO"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.autoLineNumber, 0);
/* INPUT prompts and stores. The harness input stream holds "42". */
clear_output();
TEST_REQUIRE_OK(run_line("INPUT \"AGE? \" N#"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "AGE? ");
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
{
akbasic_ASTLeaf *leaf = NULL;
TEST_REQUIRE_OK(harness_parse("A# = N#", &leaf));
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
TEST_REQUIRE_INT(out->intval, 42);
}
/* Mode transitions. */
TEST_REQUIRE_OK(run_line("STOP"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_REPL);
TEST_REQUIRE_OK(run_line("RUN"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_RUN);
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 0);
TEST_REQUIRE_OK(run_line("RUN 20"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 20);
/*
* QUIT sets a mode. It must not call exit(): a host game decides what
* quitting means, and this test would not survive it doing otherwise.
*/
TEST_REQUIRE_OK(run_line("QUIT"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT);
TEST_REQUIRE_OK(akbasic_runtime_set_mode(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
/* State errors: verbs used outside the structure they need. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("RETURN"), AKBASIC_ERR_STATE);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("EXIT"), AKBASIC_ERR_STATE);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("NEXT I#"), AKBASIC_ERR_STATE);
/* GOTO and GOSUB want an integer. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("GOTO \"nope\""), AKBASIC_ERR_TYPE);
/* GOTO sets the next line. */
TEST_REQUIRE_OK(run_line("GOTO 30"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 30);
/* GOSUB pushes an environment; RETURN pops it and restores the line. */
{
akbasic_Environment *before = HARNESS_RUNTIME.environment;
TEST_REQUIRE_OK(akbasic_environment_zero(before));
before->lineno = 100;
TEST_REQUIRE_OK(run_line("GOSUB 500"));
TEST_REQUIRE(HARNESS_RUNTIME.environment != before, "GOSUB must push an environment");
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 500);
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->gosubReturnLine, 101);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("RETURN"));
TEST_REQUIRE(HARNESS_RUNTIME.environment == before, "RETURN must pop back");
TEST_REQUIRE_INT(before->nextline, 101);
}
/* LABEL records the current line under a name. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
HARNESS_RUNTIME.environment->lineno = 70;
TEST_REQUIRE_OK(run_line("LABEL AGAIN"));
{
int64_t where = 0;
TEST_REQUIRE_OK(akbasic_environment_get_label(HARNESS_RUNTIME.environment, "AGAIN", &where));
TEST_REQUIRE_INT(where, 70);
}
/* POKE, PEEK and POINTER round-trip through real memory. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("V# = 255"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("P# = POINTER(V#)"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("Q# = PEEK(P#)"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("POKE P#, 127"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("R# = POINTERVAR(V#)"));
/* PEEK through a null address is refused rather than segfaulting. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("Z# = 0"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("Y# = PEEK(Z#)"), AKBASIC_ERR_VALUE);
harness_stop();
test_runstream_strips_line_numbers();
return akbasic_test_failures;
}