Files
akbasic/tests/parser_commands.c
Andrew Kesterson a31058cf37 Consume the COLON token: a line can hold several statements
The token has existed since the scanner was written and nothing read it, so
10 PRINT A$ : REM ... was a parse error. Leading separators are consumed
before each statement and an empty statement yields a NULL leaf rather than
an error, so a trailing colon and a run of them are both legal.

BASIC 7.0 scopes everything after THEN to the condition, which the reference
had no opinion about because it never got here. The rule is not "skip when
false": the rest of the line belongs to whichever arm was written last.

A whole FOR/NEXT on one line still does not loop -- block skipping works by
source line. Recorded in TODO.md as what group A has to fix first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:46:10 -04:00

180 lines
6.3 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");
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;
}