Files
akbasic/tests/scanner_tokens.c
Andrew Kesterson 1eb8f9ffaa Fix five defects the port uncovered in the reference parser and scanner
Chained subtraction and exponentiation fold left instead of abandoning the
rest of the line -- `1 - 2 - 3` gave -1 and dropped the `- 3`. A unary leaf's
operand moves to .left, so a negative literal is one argument again and a
second argument no longer overwrites it. An operator in a line's final column
survives. Hex literals reach the parser whole. A leading zero is padding, not
a radix.

Item 16 stays pinned: the fix works and costs an upstream golden case, which
is a decision rather than a defect.

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

181 lines
7.8 KiB
C

/**
* @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. */
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);
/*
* An operator in the final column survives, with no trailing space to help
* it. matchNextChar cannot peek past the end of the line, and the reference
* returns from that without setting a token type (basicscanner.go:272), so
* `A# =` produced one token and the parse error that followed pointed
* somewhere else. TODO.md section 6 item 14; this is its regression test.
*/
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# =", NULL, 0));
TEST_REQUIRE_INT(env()->nexttoken, 2);
expect_token(0, AKBASIC_TOK_IDENTIFIER_INT, "A#");
expect_token(1, AKBASIC_TOK_ASSIGNMENT, "=");
/* The same for the other two, each alone in the final column. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# <", NULL, 0));
TEST_REQUIRE_INT(env()->nexttoken, 2);
expect_token(1, AKBASIC_TOK_LESS_THAN, "<");
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# >", NULL, 0));
TEST_REQUIRE_INT(env()->nexttoken, 2);
expect_token(1, AKBASIC_TOK_GREATER_THAN, ">");
/* 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 survives the scanner whole. The reference lets 'x' through
* but stops at the hex digits after it (basicscanner.go:318), so "0xff"
* lexed as "0x" plus an identifier "ff", the parser's base-16 branch was
* unreachable and the README's hex support did not exist. TODO.md section 6
* item 15; this is its regression test.
*/
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0xff", NULL, 0));
TEST_REQUIRE_INT(env()->nexttoken, 2);
expect_token(1, AKBASIC_TOK_LITERAL_INT, "0xff");
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0xDEAD", NULL, 0));
expect_token(1, AKBASIC_TOK_LITERAL_INT, "0xDEAD");
/* And the hex run ends where it should, rather than eating what follows. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0x10 + 1", NULL, 0));
TEST_REQUIRE_INT(env()->nexttoken, 4);
expect_token(1, AKBASIC_TOK_LITERAL_INT, "0x10");
expect_token(2, AKBASIC_TOK_PLUS, "+");
expect_token(3, AKBASIC_TOK_LITERAL_INT, "1");
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;
}