Files
akbasic/tests/read_data.c
Andrew Kesterson d5f3c3ef5b Accept GRAPHIC CLR and a negative DATA item
Two parse handlers refusing something the documentation promises. Landing
together because they are the same defect twice -- a verb's own argument shape
falling through to a general path that cannot see it -- in one file, found by
one program, and verified in one pass.

**`GRAPHIC CLR`** is given as `GRAPHIC mode | CLR` in both docs/06-graphics.md
and docs/11-verb-reference.md, and was refused: `CLR` is a verb of its own, so
the generic arglist path scanned it as a command token and the expression parser
answered "Expected expression or literal". `akbasic_parse_graphic()` takes it as
this verb's keyword argument and emits mode 5 -- which `akbasic_cmd_graphic()`
already treats as "drop the saved shapes and go back to text", so both spellings
are one statement and the exec handler is untouched. The documentation was right
all along; nothing in it changes.

**`DATA -5`** was refused by `akbasic_parse_data()`, and only there: `READ` scans
the source text directly (src/data.c) and always returned the -5 intact. So the
value was right and *reaching* the statement raised -- which, since section 4
settled that `DATA` at run time is a no-op, is what a program does with every
`DATA` line it walks past. A table of coordinates or velocities is full of
negative numbers, which is how a game found it.

The fix accepts a unary minus over a numeric literal and nothing else: `DATA -A#`
is still a mistake worth naming, and `akbasic_leaf_is_literal()` keeps meaning
what it says because other callers rely on it.

tests/read_data.c covers both mechanisms -- reading a negative item and reaching
the line after it -- with a mixed-sign table and a negative float, since the two
were never the same code path.

TODO.md section 9 items 7 and 8, struck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:18:53 -04:00

234 lines
7.0 KiB
C

/**
* @file read_data.c
* @brief Tests READ, DATA and RESTORE against the pre-scanned item list.
*
* The reference's READ does not read: it records its identifiers, sets the scope
* waiting for a DATA verb, and lets execution skip forward until one turns up.
* Two things follow, and both are asserted here as the defects they were --
* TODO.md section 6.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include "harness.h"
#include "testutil.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, harness_start(NULL));
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 The ordinary case: DATA below its READ, filled in order. */
static void test_read_in_order(void)
{
TEST_REQUIRE_OK(run_program("10 READ A$, B#\n"
"20 DATA \"HELLO\", 12345\n"
"30 PRINT A$\n"
"40 PRINT B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "HELLO\n12345\n");
harness_stop();
}
/**
* @brief A DATA line *above* its READ is found.
*
* It was not: skipping forward from the READ ran off the end of the program in
* silence, and a second READ re-read the same line.
*/
static void test_data_before_read(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1, 2\n"
"20 READ A#, B#\n"
"30 PRINT A#\n"
"40 PRINT B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\n");
harness_stop();
}
/**
* @brief Lines between a READ and its DATA are not skipped.
*
* They were, because the skip-forward *was* the implementation. A C128 runs
* them: READ takes the next item and execution carries on normally.
*/
static void test_no_skipping(void)
{
TEST_REQUIRE_OK(run_program("10 READ A#\n"
"20 PRINT \"BETWEEN\"\n"
"30 DATA 5\n"
"40 PRINT A#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BETWEEN\n5\n");
harness_stop();
}
/** @brief Successive READs walk the cursor along rather than starting over. */
static void test_cursor_advances(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1, 2, 3\n"
"20 READ A#\n"
"30 READ B#\n"
"40 READ C#\n"
"50 PRINT A# + B# + C#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "6\n");
harness_stop();
}
/** @brief Bare RESTORE goes back to the first item. */
static void test_restore(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1, 2, 3\n"
"20 READ A#\n"
"30 RESTORE\n"
"40 READ B#\n"
"50 PRINT A#\n"
"60 PRINT B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n1\n");
harness_stop();
}
/** @brief RESTORE (line) starts again at the first item on or after that line. */
static void test_restore_line(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1, 2\n"
"20 DATA 30, 40\n"
"30 RESTORE 20\n"
"40 READ A#, B#\n"
"50 PRINT A#\n"
"60 PRINT B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n40\n");
harness_stop();
/*
* A line with no DATA on it finds the next one that has some -- a program
* names the line it wants to read *from*, not one that must hold items.
*/
TEST_REQUIRE_OK(run_program("10 DATA 1, 2\n"
"20 REM NOTHING HERE\n"
"30 DATA 99\n"
"40 RESTORE 20\n"
"50 READ A#\n"
"60 PRINT A#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "99\n");
harness_stop();
}
/** @brief Reading past the last item is reported rather than yielding zeroes. */
static void test_out_of_data(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1\n"
"20 READ A#, B#\n"
"30 PRINT \"UNREACHED\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "OUT OF DATA") != NULL,
"reading past the end should say so, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHED") == NULL,
"the program should have stopped, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief A quoted item read into a number is a mistake worth reporting. */
static void test_type_mismatch(void)
{
TEST_REQUIRE_OK(run_program("10 DATA \"TEXT\"\n"
"20 READ A#\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is a string") != NULL,
"a quoted item read into a number should be refused, got \"%s\"",
HARNESS_OUTPUT);
harness_stop();
/* An unquoted number read into a string is fine: it becomes its text. */
TEST_REQUIRE_OK(run_program("10 DATA 42\n"
"20 READ A$\n"
"30 PRINT A$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "42\n");
harness_stop();
}
/** @brief A quoted item may hold commas and colons; that is what quotes are for. */
static void test_quoted_items(void)
{
TEST_REQUIRE_OK(run_program("10 DATA \"ONE, TWO\", \"A:B\"\n"
"20 READ A$, B$\n"
"30 PRINT A$\n"
"40 PRINT B$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE, TWO\nA:B\n");
harness_stop();
}
/** @brief A colon ends a DATA statement, as it does on a Commodore. */
static void test_colon_ends_data(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1, 2 : PRINT \"AFTER DATA\"\n"
"20 READ A#, B#\n"
"30 PRINT A# + B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER DATA\n3\n");
harness_stop();
}
/** @brief A float item fills a float variable with its fractional part intact. */
static void test_float_items(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1.5\n"
"20 READ A%\n"
"30 PRINT A%\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1.500000\n");
harness_stop();
}
/**
* @brief A `DATA` line holding a negative number can be read *and* walked past.
*
* Two different mechanisms, and only one of them was ever wrong. `READ` scans
* the source text directly, so it always returned the -5 intact; the statement's
* own argument parse refused a leading minus, so reaching the line raised
* "Expected literal". Section 4 settled that `DATA` at run time is a no-op --
* reaching it means walking past it -- which is what made this reachable at all.
*
* TODO.md section 9 item 8. A table of coordinates or velocities is full of
* negative numbers, which is how a game found it.
*/
static void test_negative_items(void)
{
TEST_REQUIRE_OK(run_program("10 READ A#\n"
"20 PRINT \"READ GAVE \" + A#\n"
"30 DATA -5\n"
"40 PRINT \"PAST IT\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "READ GAVE -5\nPAST IT\n");
harness_stop();
/* Mixed signs across a real table, and a negative float. */
TEST_REQUIRE_OK(run_program("10 DATA -1, 2, -3\n"
"20 DATA -0.5\n"
"30 READ A#, B#, C#\n"
"40 READ D%\n"
"50 PRINT \"\" + A# + \" \" + B# + \" \" + C# + \" \" + D%\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "-1 2 -3 -0.500000\n");
harness_stop();
}
int main(void)
{
test_read_in_order();
test_data_before_read();
test_no_skipping();
test_cursor_advances();
test_restore();
test_restore_line();
test_out_of_data();
test_type_mismatch();
test_quoted_items();
test_colon_ends_data();
test_float_items();
test_negative_items();
return akbasic_test_failures;
}