// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // lexer.c -- DVX BASIC lexer implementation // // Single-pass tokenizer. Keywords are case-insensitive. Identifiers // preserve their original case for display but comparisons are // case-insensitive. Line continuations (underscore at end of line) // are handled transparently. #include "lexer.h" #include "opcodes.h" #include #include #include #include #include // ============================================================ // Keyword table // ============================================================ typedef struct { const char *text; uint8_t textLen; // precomputed so lookupKeyword() can length-reject cheaply BasTokenTypeE type; } KeywordEntryT; #define KW(s, t) { s, (uint8_t)(sizeof(s) - 1), t } static const KeywordEntryT sKeywords[] = { KW("AND", TOK_AND), KW("APP", TOK_APP), KW("APPEND", TOK_APPEND), KW("AS", TOK_AS), KW("BASE", TOK_BASE), KW("BINARY", TOK_BINARY), KW("BOOLEAN", TOK_BOOLEAN), KW("BYREF", TOK_BYREF), KW("BYVAL", TOK_BYVAL), KW("CALL", TOK_CALL), KW("CASE", TOK_CASE), KW("CHDIR", TOK_CHDIR), KW("CHDRIVE", TOK_CHDRIVE), KW("CLOSE", TOK_CLOSE), KW("CONST", TOK_CONST), KW("CREATECONTROL", TOK_CREATECONTROL), KW("CREATEFORM", TOK_CREATEFORM), KW("CURDIR", TOK_CURDIR), KW("CURDIR$", TOK_CURDIR), KW("DATA", TOK_DATA), KW("DECLARE", TOK_DECLARE), KW("DEF", TOK_DEF), KW("DEFDBL", TOK_DEFDBL), KW("DEFINT", TOK_DEFINT), KW("DEFLNG", TOK_DEFLNG), KW("DEFSNG", TOK_DEFSNG), KW("DEFSTR", TOK_DEFSTR), KW("DIM", TOK_DIM), KW("DIR", TOK_DIR), KW("DIR$", TOK_DIR), KW("DO", TOK_DO), KW("DOEVENTS", TOK_DOEVENTS), KW("DOUBLE", TOK_DOUBLE), KW("ELSE", TOK_ELSE), KW("ELSEIF", TOK_ELSEIF), KW("END", TOK_END), KW("EOF", TOK_EOF_KW), KW("EQV", TOK_EQV), KW("ERASE", TOK_ERASE), KW("ERL", TOK_ERL), KW("ERR", TOK_ERR), KW("ERROR", TOK_ERROR_KW), KW("EXIT", TOK_EXIT), KW("EXPLICIT", TOK_EXPLICIT), KW("FALSE", TOK_FALSE_KW), KW("FILECOPY", TOK_FILECOPY), KW("FILELEN", TOK_FILELEN), KW("FOR", TOK_FOR), KW("FUNCTION", TOK_FUNCTION), KW("GET", TOK_GET), KW("GETATTR", TOK_GETATTR), KW("GOSUB", TOK_GOSUB), KW("GOTO", TOK_GOTO), KW("HIDE", TOK_HIDE), KW("IF", TOK_IF), KW("IMP", TOK_IMP), KW("INIREAD", TOK_INIREAD), KW("INIREAD$", TOK_INIREAD), KW("INIWRITE", TOK_INIWRITE), KW("INPUT", TOK_INPUT), KW("INPUTBOX", TOK_INPUTBOX), KW("INPUTBOX$", TOK_INPUTBOX), KW("INTEGER", TOK_INTEGER), KW("IS", TOK_IS), KW("KILL", TOK_KILL), KW("LBOUND", TOK_LBOUND), KW("LET", TOK_LET), KW("LINE", TOK_LINE), KW("LOAD", TOK_LOAD), KW("LONG", TOK_LONG), KW("LOOP", TOK_LOOP), KW("ME", TOK_ME), KW("MKDIR", TOK_MKDIR), KW("MOD", TOK_MOD), KW("MSGBOX", TOK_MSGBOX), KW("NAME", TOK_NAME), KW("NEXT", TOK_NEXT), KW("NOT", TOK_NOT), KW("NOTHING", TOK_NOTHING), KW("ON", TOK_ON), KW("OPEN", TOK_OPEN), KW("OPTION", TOK_OPTION), KW("OPTIONAL", TOK_OPTIONAL), KW("OR", TOK_OR), KW("OUTPUT", TOK_OUTPUT), KW("PRESERVE", TOK_PRESERVE), KW("PRINT", TOK_PRINT), KW("PUT", TOK_PUT), KW("RANDOM", TOK_RANDOM), KW("RANDOMIZE", TOK_RANDOMIZE), KW("READ", TOK_READ), KW("REDIM", TOK_REDIM), KW("REM", TOK_REM), KW("REMOVECONTROL", TOK_REMOVECONTROL), KW("RESTORE", TOK_RESTORE), KW("RESUME", TOK_RESUME), KW("RETURN", TOK_RETURN), KW("RMDIR", TOK_RMDIR), KW("SEEK", TOK_SEEK), KW("SELECT", TOK_SELECT), KW("SET", TOK_SET), KW("SETATTR", TOK_SETATTR), KW("SETEVENT", TOK_SETEVENT), KW("SHARED", TOK_SHARED), KW("SHELL", TOK_SHELL), KW("SHOW", TOK_SHOW), KW("SINGLE", TOK_SINGLE), KW("SLEEP", TOK_SLEEP), KW("STATIC", TOK_STATIC), KW("STEP", TOK_STEP), KW("STRING", TOK_STRING_KW), KW("SUB", TOK_SUB), KW("SWAP", TOK_SWAP), KW("THEN", TOK_THEN), KW("TIMER", TOK_TIMER), KW("TO", TOK_TO), KW("TRUE", TOK_TRUE_KW), KW("TYPE", TOK_TYPE), KW("UBOUND", TOK_UBOUND), KW("UNLOAD", TOK_UNLOAD), KW("UNTIL", TOK_UNTIL), KW("WEND", TOK_WEND), KW("WHILE", TOK_WHILE), KW("WITH", TOK_WITH), KW("WRITE", TOK_WRITE), KW("XOR", TOK_XOR), { NULL, 0, TOK_ERROR } }; #undef KW #define KEYWORD_COUNT (sizeof(sKeywords) / sizeof(sKeywords[0]) - 1) // ============================================================ // Type-suffix table // ============================================================ // // Single source of truth for the BASIC type-suffix characters and the // type each one implies; basIsTypeSuffixChar and basTypeSuffixType (and // through them the parser) all consult this table. typedef struct { char suffix; uint8_t dataType; } TypeSuffixEntryT; static const TypeSuffixEntryT sTypeSuffixes[] = { { '%', BAS_TYPE_INTEGER }, { '&', BAS_TYPE_LONG }, { '!', BAS_TYPE_SINGLE }, { '#', BAS_TYPE_DOUBLE }, { '$', BAS_TYPE_STRING }, }; #define TYPE_SUFFIX_COUNT ((int32_t)(sizeof(sTypeSuffixes) / sizeof(sTypeSuffixes[0]))) // Function prototypes (alphabetical) static char advance(BasLexerT *lex); static void appendTokenChar(BasLexerT *lex, int32_t *idx, char c); static bool atEnd(const BasLexerT *lex); char basAsciiUpper(char c); bool basIsIdentChar(char c); bool basIsTypeSuffixChar(char c); bool basIsValidIdent(const char *name); void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen); const char *basLexerKeywordAt(int32_t i); BasKeywordClassE basLexerKeywordClass(int32_t i); int32_t basLexerKeywordCount(void); BasTokenTypeE basLexerNext(BasLexerT *lex); BasTokenTypeE basLexerPeek(const BasLexerT *lex); const char *basTokenName(BasTokenTypeE type); int32_t basTypeSuffixType(char c); static bool lexIntegerLiteral(BasLexerT *lex, int64_t minVal, int64_t maxVal, const char *what, int64_t *outVal); static BasTokenTypeE lookupKeyword(const char *text, int32_t len); static BasTokenTypeE makeNewlineToken(BasLexerT *lex); static char peek(const BasLexerT *lex); static char peekNext(const BasLexerT *lex); static void setError(BasLexerT *lex, const char *msg); static void skipLineComment(BasLexerT *lex); static void skipWhitespace(BasLexerT *lex); static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex); static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex); static BasTokenTypeE tokenizeNumber(BasLexerT *lex); static BasTokenTypeE tokenizeString(BasLexerT *lex); static char advance(BasLexerT *lex) { if (atEnd(lex)) { return '\0'; } char c = lex->source[lex->pos++]; // LF, or a CR that is not the first half of a CRLF pair, ends a line // (the LF of a CRLF pair is counted when it is consumed). if (c == '\n' || (c == '\r' && peek(lex) != '\n')) { lex->line++; lex->col = 1; } else { lex->col++; } return c; } static void appendTokenChar(BasLexerT *lex, int32_t *idx, char c) { if (*idx < BAS_MAX_TOKEN_LEN - 1) { lex->token.text[(*idx)++] = c; } } static bool atEnd(const BasLexerT *lex) { return lex->pos >= lex->sourceLen; } char basAsciiUpper(char c) { if (c >= 'a' && c <= 'z') { c -= ('a' - 'A'); } return c; } bool basIsIdentChar(char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; } bool basIsTypeSuffixChar(char c) { return basTypeSuffixType(c) >= 0; } bool basIsValidIdent(const char *name) { if (!name || !basIsIdentChar(name[0]) || (name[0] >= '0' && name[0] <= '9')) { return false; } for (const char *p = name + 1; *p; p++) { if (!basIsIdentChar(*p)) { return false; } } return true; } void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen) { memset(lex, 0, sizeof(*lex)); lex->source = source; lex->sourceLen = (sourceLen < 0) ? (int32_t)strlen(source) : sourceLen; lex->pos = 0; lex->line = 1; lex->col = 1; // Prime the first token basLexerNext(lex); } const char *basLexerKeywordAt(int32_t i) { if (i < 0 || i >= (int32_t)KEYWORD_COUNT) { return NULL; } return sKeywords[i].text; } BasKeywordClassE basLexerKeywordClass(int32_t i) { if (i < 0 || i >= (int32_t)KEYWORD_COUNT) { return BAS_KW_CLASS_OTHER; } switch (sKeywords[i].type) { case TOK_BOOLEAN: case TOK_DOUBLE: case TOK_INTEGER: case TOK_LONG: case TOK_SINGLE: case TOK_STRING_KW: return BAS_KW_CLASS_TYPE; case TOK_TRUE_KW: case TOK_FALSE_KW: case TOK_NOTHING: return BAS_KW_CLASS_LITERAL; default: return BAS_KW_CLASS_OTHER; } } int32_t basLexerKeywordCount(void) { return (int32_t)KEYWORD_COUNT; } BasTokenTypeE basLexerNext(BasLexerT *lex) { skipWhitespace(lex); lex->token.line = lex->line; lex->token.col = lex->col; lex->token.textLen = 0; lex->token.text[0] = '\0'; if (atEnd(lex)) { lex->token.type = TOK_EOF; return TOK_EOF; } char c = peek(lex); // Newline if (c == '\n') { advance(lex); return makeNewlineToken(lex); } // Carriage return (handle CR, CRLF) if (c == '\r') { advance(lex); if (!atEnd(lex) && peek(lex) == '\n') { advance(lex); } return makeNewlineToken(lex); } // Comment (apostrophe) if (c == '\'') { skipLineComment(lex); return makeNewlineToken(lex); } // String literal if (c == '"') { lex->token.type = tokenizeString(lex); return lex->token.type; } // Number if (isdigit((unsigned char)c) || (c == '.' && isdigit((unsigned char)peekNext(lex)))) { lex->token.type = tokenizeNumber(lex); return lex->token.type; } // Numeric-base literals: &H hex, &O octal, &B binary. &B is an // extension beyond classic QBASIC; it's convenient for bitmask // work in the widget/graphics code. if (c == '&') { char n = basAsciiUpper(peekNext(lex)); if (n == 'H' || n == 'O' || n == 'B') { lex->token.type = tokenizeHexLiteral(lex); return lex->token.type; } } // Identifier or keyword if (basIsIdentChar(c) && !isdigit((unsigned char)c)) { lex->token.type = tokenizeIdentOrKeyword(lex); return lex->token.type; } // Single and multi-character operators/punctuation advance(lex); switch (c) { case '+': lex->token.type = TOK_PLUS; break; case '-': lex->token.type = TOK_MINUS; break; case '*': lex->token.type = TOK_STAR; break; case '/': lex->token.type = TOK_SLASH; break; case '\\': lex->token.type = TOK_BACKSLASH; break; case '^': lex->token.type = TOK_CARET; break; case '&': lex->token.type = TOK_AMPERSAND; break; case '(': lex->token.type = TOK_LPAREN; break; case ')': lex->token.type = TOK_RPAREN; break; case ',': lex->token.type = TOK_COMMA; break; case ';': lex->token.type = TOK_SEMICOLON; break; case ':': lex->token.type = TOK_COLON; break; case '.': lex->token.type = TOK_DOT; break; case '#': lex->token.type = TOK_HASH; break; case '?': lex->token.type = TOK_PRINT; break; case '=': lex->token.type = TOK_EQ; break; case '<': if (!atEnd(lex) && peek(lex) == '>') { advance(lex); lex->token.type = TOK_NE; } else if (!atEnd(lex) && peek(lex) == '=') { advance(lex); lex->token.type = TOK_LE; } else { lex->token.type = TOK_LT; } break; case '>': if (!atEnd(lex) && peek(lex) == '=') { advance(lex); lex->token.type = TOK_GE; } else { lex->token.type = TOK_GT; } break; default: setError(lex, "Unexpected character"); lex->token.type = TOK_ERROR; break; } // Store the operator text if (lex->token.type != TOK_ERROR) { lex->token.text[0] = c; lex->token.textLen = 1; if (lex->token.type == TOK_NE || lex->token.type == TOK_LE || lex->token.type == TOK_GE) { lex->token.text[1] = lex->source[lex->pos - 1]; lex->token.textLen = 2; } lex->token.text[lex->token.textLen] = '\0'; } return lex->token.type; } BasTokenTypeE basLexerPeek(const BasLexerT *lex) { return lex->token.type; } const char *basTokenName(BasTokenTypeE type) { switch (type) { case TOK_INT_LIT: return "integer"; case TOK_LONG_LIT: return "long"; case TOK_FLOAT_LIT: return "float"; case TOK_STRING_LIT: return "string"; case TOK_IDENT: return "identifier"; case TOK_DOT: return "'.'"; case TOK_COMMA: return "','"; case TOK_SEMICOLON: return "';'"; case TOK_COLON: return "':'"; case TOK_LPAREN: return "'('"; case TOK_RPAREN: return "')'"; case TOK_HASH: return "'#'"; case TOK_PLUS: return "'+'"; case TOK_MINUS: return "'-'"; case TOK_STAR: return "'*'"; case TOK_SLASH: return "'/'"; case TOK_BACKSLASH: return "'\\'"; case TOK_CARET: return "'^'"; case TOK_AMPERSAND: return "'&'"; case TOK_EQ: return "'='"; case TOK_NE: return "'<>'"; case TOK_LT: return "'<'"; case TOK_GT: return "'>'"; case TOK_LE: return "'<='"; case TOK_GE: return "'>='"; case TOK_NEWLINE: return "newline"; case TOK_EOF: return "end of file"; case TOK_ERROR: return "error"; default: break; } // Keywords for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) { if (sKeywords[i].type == type) { return sKeywords[i].text; } } return "?"; } int32_t basTypeSuffixType(char c) { for (int32_t i = 0; i < TYPE_SUFFIX_COUNT; i++) { if (sTypeSuffixes[i].suffix == c) { return sTypeSuffixes[i].dataType; } } return -1; } // Convert the decimal digits in token.text to an integer and range-check // the result against [minVal, maxVal]. On overflow the token becomes // TOK_ERROR naming `what`, and false is returned. strtoll is used rather // than atol so the conversion is identical on the 32-bit DOS build and // the 64-bit host build of the compiler. static bool lexIntegerLiteral(BasLexerT *lex, int64_t minVal, int64_t maxVal, const char *what, int64_t *outVal) { errno = 0; long long val = strtoll(lex->token.text, NULL, 10); if (errno == ERANGE || val < minVal || val > maxVal) { char buf[BAS_LEX_ERROR_LEN]; snprintf(buf, sizeof(buf), "%s literal out of range", what); setError(lex, buf); lex->token.type = TOK_ERROR; return false; } *outVal = (int64_t)val; return true; } static BasTokenTypeE lookupKeyword(const char *text, int32_t len) { // Case-insensitive keyword lookup. Short-circuits on length mismatch // (via cached keyword length) and on the very first character, both of // which reject the vast majority of entries before doing a full scan. char firstUp = basAsciiUpper(text[0]); for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) { const KeywordEntryT *kw = &sKeywords[i]; if (kw->textLen != len || kw->text[0] != firstUp) { continue; } bool match = true; for (int32_t j = 1; j < len; j++) { if (basAsciiUpper(text[j]) != kw->text[j]) { match = false; break; } } if (match) { return kw->type; } } return TOK_IDENT; } // Fill the current token as a synthesized newline (used for real newlines, // CR/CRLF, and the end of apostrophe / REM comments). static BasTokenTypeE makeNewlineToken(BasLexerT *lex) { lex->token.type = TOK_NEWLINE; lex->token.text[0] = '\n'; lex->token.text[1] = '\0'; lex->token.textLen = 1; return TOK_NEWLINE; } static char peek(const BasLexerT *lex) { if (atEnd(lex)) { return '\0'; } return lex->source[lex->pos]; } static char peekNext(const BasLexerT *lex) { if (lex->pos + 1 >= lex->sourceLen) { return '\0'; } return lex->source[lex->pos + 1]; } static void setError(BasLexerT *lex, const char *msg) { snprintf(lex->error, sizeof(lex->error), "Line %d, Col %d: %s", (int)lex->line, (int)lex->col, msg); } static void skipLineComment(BasLexerT *lex) { while (!atEnd(lex) && peek(lex) != '\n' && peek(lex) != '\r') { advance(lex); } } // // Skips spaces and tabs. Does NOT skip newlines (they are tokens). // Handles line continuation: underscore followed by newline joins // the next line to the current logical line. static void skipWhitespace(BasLexerT *lex) { while (!atEnd(lex)) { char c = peek(lex); if (c == ' ' || c == '\t') { advance(lex); continue; } // Line continuation: _ at end of line if (c == '_') { int32_t savedPos = lex->pos; int32_t savedLine = lex->line; int32_t savedCol = lex->col; advance(lex); // Skip spaces/tabs after underscore while (!atEnd(lex) && (peek(lex) == ' ' || peek(lex) == '\t')) { advance(lex); } // Must be followed by newline if (!atEnd(lex) && (peek(lex) == '\n' || peek(lex) == '\r')) { advance(lex); if (!atEnd(lex) && peek(lex) == '\n' && lex->source[lex->pos - 1] == '\r') { advance(lex); } continue; // Continue skipping whitespace on next line } // Not a continuation -- put back lex->pos = savedPos; lex->line = savedLine; lex->col = savedCol; break; } break; } } static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) { advance(lex); // skip & char base = basAsciiUpper(peek(lex)); advance(lex); // skip H/O/B int32_t shift; int32_t maxDigit; if (base == 'O') { shift = 3; maxDigit = 7; } else if (base == 'B') { shift = 1; maxDigit = 1; } else { shift = 4; maxDigit = 15; } bool overflow = false; int32_t idx = 0; uint64_t value = 0; for (;;) { if (atEnd(lex)) { break; } char c = peek(lex); int32_t digit; if (c >= '0' && c <= '9') { digit = c - '0'; } else if (shift == 4 && c >= 'A' && c <= 'F') { digit = c - 'A' + 10; } else if (shift == 4 && c >= 'a' && c <= 'f') { digit = c - 'a' + 10; } else { break; } if (digit > maxDigit) { break; } advance(lex); appendTokenChar(lex, &idx, c); value = (value << shift) | (uint64_t)digit; // Latch overflow instead of testing value after the loop: with // enough digits the shifts wrap the uint64 back into range, which // would hide the overflow. if (value > UINT32_MAX) { overflow = true; } } // No digits after the &H/&O/&B marker is a typo, not the integer 0. if (idx == 0) { setError(lex, "Empty hexadecimal/octal/binary literal"); lex->token.type = TOK_ERROR; return TOK_ERROR; } // The widest runtime integer (LONG) is 32 bits; silently truncating a // wider literal would miscompile the constant, so reject it. Note the // literal is NOT typed by width: &HFFFF is 65535 (a LONG), not QBASIC's // 16-bit -1 -- the parser picks the push width from the value. if (overflow) { setError(lex, "Hexadecimal/octal/binary literal exceeds 32 bits"); lex->token.type = TOK_ERROR; return TOK_ERROR; } lex->token.text[idx] = '\0'; lex->token.textLen = idx; // Check for trailing & (long suffix) if (!atEnd(lex) && peek(lex) == '&') { advance(lex); lex->token.longVal = (int64_t)value; return TOK_LONG_LIT; } lex->token.intVal = (int32_t)value; return TOK_INT_LIT; } static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) { int32_t idx = 0; while (!atEnd(lex) && basIsIdentChar(peek(lex))) { appendTokenChar(lex, &idx, advance(lex)); } lex->token.text[idx] = '\0'; lex->token.textLen = idx; // Check for type suffix. A keyword only ever takes '$' (CURDIR$, // DIR$, INPUT$ ...); any other suffix after a keyword belongs to the // next token, so PRINT#1 / CLOSE#1 / INPUT#1 lex as keyword + '#'. if (!atEnd(lex)) { char c = peek(lex); if (basIsTypeSuffixChar(c) && (c == '$' || lookupKeyword(lex->token.text, idx) == TOK_IDENT)) { advance(lex); appendTokenChar(lex, &idx, c); lex->token.text[idx] = '\0'; lex->token.textLen = idx; } } // Check if this is a keyword // For suffix-bearing identifiers, only check the base (without suffix) int32_t baseLen = idx; if (baseLen > 0) { char last = lex->token.text[baseLen - 1]; if (basIsTypeSuffixChar(last)) { baseLen--; } } // Try the full text first (including any type suffix). Suffix-bearing // keywords like CURDIR$, DIR$, INIREAD$, INPUTBOX$ are listed in the // keyword table with their $ and will match here. If the full text // isn't a keyword, fall back to the base name (without suffix). BasTokenTypeE kwType = lookupKeyword(lex->token.text, idx); bool matchedWithSuffix = (kwType != TOK_IDENT && baseLen != idx); if (kwType == TOK_IDENT && baseLen != idx) { kwType = lookupKeyword(lex->token.text, baseLen); } // REM is a comment -- skip to end of line. Apply the same acceptance // gate as below so a suffixed identifier like REM$ stays an identifier // instead of swallowing the rest of the line as a comment. if (kwType == TOK_REM && (baseLen == idx || matchedWithSuffix)) { skipLineComment(lex); return makeNewlineToken(lex); } // Accept the keyword if it's a plain keyword (no suffix on source) or // if it explicitly matched a $-suffixed entry in the keyword table. if (kwType != TOK_IDENT && (baseLen == idx || matchedWithSuffix)) { return kwType; } return TOK_IDENT; } static BasTokenTypeE tokenizeNumber(BasLexerT *lex) { int32_t idx = 0; bool hasDecimal = false; bool hasExp = false; // Integer part while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) { appendTokenChar(lex, &idx, advance(lex)); } // Decimal part if (!atEnd(lex) && peek(lex) == '.' && isdigit((unsigned char)peekNext(lex))) { hasDecimal = true; appendTokenChar(lex, &idx, advance(lex)); // . while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) { appendTokenChar(lex, &idx, advance(lex)); } } // Exponent. Accept QBASIC/Fortran 'D' (double) as well as 'E', but // always store 'E' in the buffer so atof() can parse it. Require at // least one digit after the marker (and optional sign); otherwise the // marker is not part of the number (e.g. "1E" tokenizes as 1 then E). char marker = basAsciiUpper(peek(lex)); if (!atEnd(lex) && (marker == 'E' || marker == 'D')) { int32_t savedIdx = idx; int32_t savedPos = lex->pos; int32_t savedLine = lex->line; int32_t savedCol = lex->col; advance(lex); // consume marker appendTokenChar(lex, &idx, 'E'); // always store 'E', never 'D' if (!atEnd(lex) && (peek(lex) == '+' || peek(lex) == '-')) { appendTokenChar(lex, &idx, advance(lex)); } if (!atEnd(lex) && isdigit((unsigned char)peek(lex))) { hasExp = true; while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) { appendTokenChar(lex, &idx, advance(lex)); } } else { // Not a valid exponent -- roll back the consumed marker/sign. idx = savedIdx; lex->pos = savedPos; lex->line = savedLine; lex->col = savedCol; } } lex->token.text[idx] = '\0'; lex->token.textLen = idx; // Check for type suffix if (!atEnd(lex)) { char c = peek(lex); int64_t val; if (c == '%') { advance(lex); if (!lexIntegerLiteral(lex, INT16_MIN, INT16_MAX, "INTEGER", &val)) { return TOK_ERROR; } lex->token.intVal = (int32_t)val; return TOK_INT_LIT; } if (c == '&') { advance(lex); if (!lexIntegerLiteral(lex, INT32_MIN, INT32_MAX, "LONG", &val)) { return TOK_ERROR; } lex->token.longVal = val; return TOK_LONG_LIT; } if (c == '!' || c == '#') { advance(lex); lex->token.dblVal = atof(lex->token.text); return TOK_FLOAT_LIT; } } // No suffix: determine type from content if (hasDecimal || hasExp) { lex->token.dblVal = atof(lex->token.text); return TOK_FLOAT_LIT; } // Unsuffixed integers must fit LONG; anything wider would be silently // truncated by the 32-bit runtime. int64_t val; int64_t maxVal = lex->allowNegMagnitude ? BAS_LONG_NEG_MAGNITUDE : INT32_MAX; if (!lexIntegerLiteral(lex, INT32_MIN, maxVal, "Integer", &val)) { return TOK_ERROR; } if (val >= INT16_MIN && val <= INT16_MAX) { lex->token.intVal = (int32_t)val; return TOK_INT_LIT; } lex->token.longVal = val; return TOK_LONG_LIT; } static BasTokenTypeE tokenizeString(BasLexerT *lex) { advance(lex); // skip opening quote int32_t idx = 0; bool overflow = false; while (!atEnd(lex) && peek(lex) != '"' && peek(lex) != '\n' && peek(lex) != '\r') { // appendTokenChar silently drops chars once the buffer is full; // detect that here so over-long literals are diagnosed instead // of truncated. Keep consuming so the lexer skips the whole // literal and its closing quote. if (idx >= BAS_MAX_STRING_LEN) { overflow = true; } appendTokenChar(lex, &idx, advance(lex)); } if (atEnd(lex) || peek(lex) != '"') { setError(lex, "Unterminated string literal"); lex->token.text[idx] = '\0'; lex->token.textLen = idx; return TOK_ERROR; } advance(lex); // skip closing quote lex->token.text[idx] = '\0'; lex->token.textLen = idx; if (overflow) { char buf[BAS_LEX_ERROR_LEN]; snprintf(buf, sizeof(buf), "String literal too long (maximum is %d characters)", (int)BAS_MAX_STRING_LEN); setError(lex, buf); return TOK_ERROR; } return TOK_STRING_LIT; }