Files
akbasic/tests/parser_commands.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

201 lines
7.4 KiB
C

/**
* @file parser_commands.c
* @brief Tests the verbs that carry their own parse path.
*/
#include "harness.h"
static void expect_command(const char *line, akbasic_LeafType wanttype, const char *wantname)
{
akbasic_ASTLeaf *leaf = NULL;
akerr_ErrorContext *e = NULL;
e = harness_parse(line, &leaf);
if ( e != NULL ) {
fprintf(stderr, "FAIL: \"%s\" did not parse: %s\n", line, e->message);
akbasic_test_failures += 1;
test_discard_error(e);
return;
}
TEST_REQUIRE(leaf != NULL, "\"%s\" produced no leaf", line);
if ( leaf == NULL ) {
return;
}
TEST_REQUIRE_INT(leaf->leaftype, wanttype);
if ( wantname != NULL ) {
TEST_REQUIRE_STR(leaf->identifier, wantname);
}
}
static void expect_parse_error(const char *line)
{
akbasic_ASTLeaf *leaf = NULL;
akerr_ErrorContext *e = NULL;
e = harness_parse(line, &leaf);
TEST_REQUIRE(e != NULL, "\"%s\" should not have parsed", line);
if ( e != NULL ) {
test_discard_error(e);
}
}
/**
* @brief Parse a whole line and report how many statements came out of it.
*
* The statement loop the runtime uses, in miniature: parse until the token
* stream is spent, skipping the NULL leaf that an empty statement produces.
*/
static int count_statements(const char *line)
{
akbasic_Parser parser;
akbasic_ASTLeaf *leaf = NULL;
akerr_ErrorContext *e = NULL;
int count = 0;
e = akbasic_scanner_zero(&HARNESS_RUNTIME);
if ( e == NULL ) {
e = akbasic_scanner_scan(&HARNESS_RUNTIME, line, NULL, 0);
}
if ( e == NULL ) {
e = akbasic_parser_init(&parser, &HARNESS_RUNTIME);
}
while ( e == NULL && !akbasic_parser_is_at_end(&parser) ) {
leaf = NULL;
e = akbasic_parser_parse(&parser, &leaf);
if ( e == NULL && leaf != NULL ) {
count += 1;
}
}
if ( e != NULL ) {
fprintf(stderr, "FAIL: \"%s\" did not parse: %s\n", line, e->message);
akbasic_test_failures += 1;
test_discard_error(e);
return -1;
}
return count;
}
int main(void)
{
akbasic_ASTLeaf *leaf = NULL;
TEST_REQUIRE_OK(harness_start(NULL));
/* Plain verbs with an expression rval. */
expect_command("PRINT 1", AKBASIC_LEAF_COMMAND, "PRINT");
expect_command("GOTO 100", AKBASIC_LEAF_COMMAND, "GOTO");
expect_command("GOSUB 100", AKBASIC_LEAF_COMMAND, "GOSUB");
/* A verb with no rval at all must not fail. */
expect_command("RETURN", AKBASIC_LEAF_COMMAND, "RETURN");
expect_command("STOP", AKBASIC_LEAF_COMMAND, "STOP");
/* Immediate commands get their own leaf type. */
expect_command("QUIT", AKBASIC_LEAF_COMMAND_IMMEDIATE, "QUIT");
expect_command("LIST", AKBASIC_LEAF_COMMAND_IMMEDIATE, "LIST");
expect_command("RUN", AKBASIC_LEAF_COMMAND_IMMEDIATE, "RUN");
/* Verbs with their own parse path. */
expect_command("LABEL LOOPTOP", AKBASIC_LEAF_COMMAND, "LABEL");
expect_command("DIM A#(5)", AKBASIC_LEAF_COMMAND, "DIM");
expect_command("DATA 1, 2, 3", AKBASIC_LEAF_COMMAND, "DATA");
/*
* A negated literal is one. `DATA -5` used to be refused here and only here:
* READ scans the source text directly and returned the -5 intact, so the
* value was right and reaching the *statement* raised "Expected literal" --
* which is a table of coordinates or velocities failing on the line after
* it. TODO.md section 9 item 8.
*/
expect_command("DATA -5", AKBASIC_LEAF_COMMAND, "DATA");
expect_command("DATA -1, 2, -3.5", AKBASIC_LEAF_COMMAND, "DATA");
/* Only over a literal, though: a negated variable is still a mistake. */
expect_parse_error("DATA -A#");
/*
* `GRAPHIC CLR` is documented in two chapters and used to be refused: CLR is
* a verb of its own, so the generic arglist path scanned it as a command
* token rather than as this verb's keyword argument. TODO.md section 9 item
* 7. It becomes mode 5, which the exec handler already treats as exactly
* this, so both spellings are one statement.
*/
expect_command("GRAPHIC CLR", AKBASIC_LEAF_COMMAND, "GRAPHIC");
expect_command("GRAPHIC 1, 1", AKBASIC_LEAF_COMMAND, "GRAPHIC");
expect_command("READ A#, B#", AKBASIC_LEAF_COMMAND, "READ");
expect_command("POKE 100, 1", AKBASIC_LEAF_COMMAND, "POKE");
expect_command("INPUT \"NAME? \" A$", AKBASIC_LEAF_COMMAND, "INPUT");
/* LET parses as a bare assignment. */
expect_command("LET A# = 1", AKBASIC_LEAF_BINARY, NULL);
/* IF produces a BRANCH, not a COMMAND. */
expect_command("IF 1 == 1 THEN PRINT 1", AKBASIC_LEAF_BRANCH, NULL);
TEST_REQUIRE_OK(harness_parse("IF 1 == 1 THEN PRINT 1 ELSE PRINT 2", &leaf));
TEST_REQUIRE(leaf != NULL && leaf->left != NULL && leaf->right != NULL,
"IF ... THEN ... ELSE must fill both branches");
/* FOR installs a loop environment and returns the assignment. */
{
akbasic_Environment *before = HARNESS_RUNTIME.environment;
TEST_REQUIRE_OK(harness_parse("FOR I# = 1 TO 5", &leaf));
TEST_REQUIRE(HARNESS_RUNTIME.environment != before,
"FOR must install a new environment");
TEST_REQUIRE(HARNESS_RUNTIME.environment->forToLeaf != NULL,
"FOR must record its TO expression");
TEST_REQUIRE(HARNESS_RUNTIME.environment->forStepLeaf != NULL,
"FOR must default its STEP to 1");
TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME));
}
{
akbasic_Environment *before = HARNESS_RUNTIME.environment;
TEST_REQUIRE_OK(harness_parse("FOR I# = 1 TO 10 STEP 2", &leaf));
TEST_REQUIRE(HARNESS_RUNTIME.environment != before, "FOR STEP must install an environment");
TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME));
}
/* Malformed FOR is rejected at parse time. */
expect_parse_error("FOR I# = 1");
expect_parse_error("FOR I# = 1 TO 5 STEP");
expect_parse_error("FOR 1 TO 5");
/* Other malformed forms. */
expect_parse_error("IF 1 == 1 PRINT 1");
expect_parse_error("LABEL 5");
expect_parse_error("READ 1");
expect_parse_error("DATA A#");
/*
* Colons separate statements. The token has existed since the scanner was
* written and nothing consumed it, so every one of these was a parse error
* until the statement loop learned to skip past it -- TODO.md section 4.
*/
TEST_REQUIRE_INT(count_statements("PRINT 1"), 1);
TEST_REQUIRE_INT(count_statements("PRINT 1 : PRINT 2"), 2);
TEST_REQUIRE_INT(count_statements("A# = 1 : B# = 2 : PRINT A# + B#"), 3);
/* An empty statement is not an error, and does not count as one. */
TEST_REQUIRE_INT(count_statements("PRINT 1 :"), 1);
TEST_REQUIRE_INT(count_statements(": PRINT 1"), 1);
TEST_REQUIRE_INT(count_statements("PRINT 1 :: PRINT 2"), 2);
TEST_REQUIRE_INT(count_statements(":"), 0);
TEST_REQUIRE_INT(count_statements(":::"), 0);
/*
* A verb that takes no rval stops at the separator rather than trying to
* parse one out of it.
*/
TEST_REQUIRE_INT(count_statements("PRINT : PRINT 2"), 2);
/*
* IF takes exactly one statement for each arm; the rest of the line arrives
* at the statement loop as ordinary statements, and it is the *runtime* that
* decides whether to run them. That split is asserted end to end in
* tests/language/statements/multiple_per_line.bas, because it is a question
* about execution rather than about the shape of the tree.
*/
TEST_REQUIRE_INT(count_statements("IF 1 == 1 THEN PRINT 1 : PRINT 2"), 2);
TEST_REQUIRE_INT(count_statements("IF 1 == 1 THEN PRINT 1 ELSE PRINT 2 : PRINT 3"), 2);
harness_stop();
return akbasic_test_failures;
}