/** * @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; }