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>
234 lines
7.0 KiB
C
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;
|
|
}
|