/** * @file scanner_tokens.c * @brief Tests the line tokenizer. */ #include "harness.h" static akbasic_Environment *env(void) { return HARNESS_RUNTIME.environment; } /* Scan a line and assert the token type at a given index. */ static void expect_token(int index, akbasic_TokenType want, const char *lexeme) { TEST_REQUIRE(index < env()->nexttoken, "expected a token at index %d, only %d were produced", index, env()->nexttoken); if ( index >= env()->nexttoken ) { return; } TEST_REQUIRE_INT(env()->tokens[index].tokentype, want); if ( lexeme != NULL ) { TEST_REQUIRE_STR(env()->tokens[index].lexeme, lexeme); } } int main(void) { char rewritten[AKBASIC_MAX_LINE_LENGTH]; TEST_REQUIRE_OK(harness_start(NULL)); /* Single-character operators. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "+-*/^(),", NULL, 0)); TEST_REQUIRE_INT(env()->nexttoken, 8); expect_token(0, AKBASIC_TOK_PLUS, "+"); expect_token(1, AKBASIC_TOK_MINUS, "-"); expect_token(2, AKBASIC_TOK_STAR, "*"); expect_token(3, AKBASIC_TOK_LEFT_SLASH, "/"); expect_token(4, AKBASIC_TOK_CARAT, "^"); expect_token(5, AKBASIC_TOK_LEFT_PAREN, "("); expect_token(6, AKBASIC_TOK_RIGHT_PAREN, ")"); expect_token(7, AKBASIC_TOK_COMMA, ","); /* * Two-character comparisons, and the one-character fallbacks. The trailing * space is not decoration: matchNextChar cannot peek past the end of the * line, and when it fails it returns without setting a token type, so a * comparison operator in the final column is silently dropped. TODO.md * section 12 item 14. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "<= <> >= < > == = ", NULL, 0)); TEST_REQUIRE_INT(env()->nexttoken, 7); expect_token(0, AKBASIC_TOK_LESS_THAN_EQUAL, NULL); expect_token(1, AKBASIC_TOK_NOT_EQUAL, NULL); expect_token(2, AKBASIC_TOK_GREATER_THAN_EQUAL, NULL); expect_token(3, AKBASIC_TOK_LESS_THAN, NULL); expect_token(4, AKBASIC_TOK_GREATER_THAN, NULL); expect_token(5, AKBASIC_TOK_EQUAL, NULL); expect_token(6, AKBASIC_TOK_ASSIGNMENT, NULL); /* Without the trailing space the final operator vanishes. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# =", NULL, 0)); TEST_REQUIRE_INT(env()->nexttoken, 1); /* Literals. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 1.5", NULL, 0)); expect_token(0, AKBASIC_TOK_COMMAND, "PRINT"); expect_token(1, AKBASIC_TOK_LITERAL_FLOAT, "1.5"); /* * A hex literal does not survive the scanner: matchNumber allows 'x' through * but not the hex digits after it, so "0xff" lexes as "0x" and the "ff" is * scanned as an identifier. The parser's base-16 branch is therefore * unreachable. TODO.md section 12 item 15. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0xff", NULL, 0)); expect_token(1, AKBASIC_TOK_LITERAL_INT, "0x"); TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT \"HELLO\"", NULL, 0)); expect_token(1, AKBASIC_TOK_LITERAL_STRING, "HELLO"); /* An empty string literal really is empty, not a stray quote. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT \"\"", NULL, 0)); expect_token(1, AKBASIC_TOK_LITERAL_STRING, ""); /* Identifier suffixes pick the token type. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# B% C$ D", NULL, 0)); expect_token(0, AKBASIC_TOK_IDENTIFIER_INT, "A#"); expect_token(1, AKBASIC_TOK_IDENTIFIER_FLOAT, "B%"); expect_token(2, AKBASIC_TOK_IDENTIFIER_STRING, "C$"); expect_token(3, AKBASIC_TOK_IDENTIFIER, "D"); /* Verbs are case-insensitive; a variable is not a verb. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "print goto", NULL, 0)); expect_token(0, AKBASIC_TOK_COMMAND, "print"); expect_token(1, AKBASIC_TOK_COMMAND, "goto"); /* Reserved words get their own token types, not COMMAND. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# AND B# OR NOT C#", NULL, 0)); expect_token(1, AKBASIC_TOK_AND, NULL); expect_token(3, AKBASIC_TOK_OR, NULL); expect_token(4, AKBASIC_TOK_NOT, NULL); /* * The line-number rule: an integer in token position 0 is consumed, not * emitted, and the line is rewritten to what follows it with leading spaces * stripped. The REPL stores the rewritten line as the program text, so this * is load-bearing rather than cosmetic. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "10 PRINT 1", rewritten, sizeof(rewritten))); TEST_REQUIRE_STR(rewritten, "PRINT 1"); TEST_REQUIRE_INT(env()->lineno, 10); expect_token(0, AKBASIC_TOK_COMMAND, "PRINT"); expect_token(1, AKBASIC_TOK_LITERAL_INT, "1"); /* An integer that is *not* in position 0 stays a literal. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 10", NULL, 0)); expect_token(1, AKBASIC_TOK_LITERAL_INT, "10"); /* REM ends the line: nothing after it becomes a token. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "REM PRINT \"NOT SEEN\"", NULL, 0)); TEST_REQUIRE_INT(env()->nexttoken, 0); TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "10 REM A COMMENT", NULL, 0)); TEST_REQUIRE_INT(env()->nexttoken, 0); TEST_REQUIRE_INT(env()->lineno, 10); /* * The "Reserved word in variable name" check never fires: the lexeme still * carries its suffix when the verb table is searched, so "PRINT$" misses and * becomes an ordinary string variable. Pinned as it stands -- TODO.md * section 12 item 16. */ TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT$ = 1", NULL, 0)); TEST_REQUIRE(!HARNESS_RUNTIME.hasError, "PRINT$ is currently accepted as a variable name; see TODO.md 12.16"); expect_token(0, AKBASIC_TOK_IDENTIFIER_STRING, "PRINT$"); /* An unknown character is diagnosed. */ HARNESS_RUNTIME.hasError = false; TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT ~", NULL, 0)); TEST_REQUIRE(HARNESS_RUNTIME.hasError, "an unknown token must be diagnosed"); /* A malformed number is diagnosed rather than silently becoming zero. */ HARNESS_RUNTIME.hasError = false; TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "1x2 PRINT 1", NULL, 0)); TEST_REQUIRE(HARNESS_RUNTIME.hasError, "a malformed line number must be diagnosed, not converted to 0"); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "INTEGER CONVERSION ON '1x2'") != NULL, "expected the reference's conversion message, got: %s", HARNESS_OUTPUT); TEST_REQUIRE_STATUS(akbasic_scanner_scan(&HARNESS_RUNTIME, NULL, NULL, 0), AKERR_NULLPOINTER); harness_stop(); return akbasic_test_failures; }