lexer/lexer.spice
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Copyright (c) 2021-2026 ChilliBits. All rights reserved. | ||
| 2 | |||
| 3 | // Std imports | ||
| 4 | import "std/data/vector"; | ||
| 5 | import "std/text/format"; | ||
| 6 | import "std/text/analysis"; | ||
| 7 | import "std/io/filepath"; | ||
| 8 | |||
| 9 | // Own imports | ||
| 10 | import "@/exception/fatal-error"; | ||
| 11 | import "@/lexer/token"; | ||
| 12 | import "@/reader/reader"; | ||
| 13 | import "@/reader/code-loc"; | ||
| 14 | import "@/exception/lexer-error"; | ||
| 15 | |||
| 16 | 3951 | public type Lexer struct { | |
| 17 | Reader reader | ||
| 18 | Vector<Token> tokens // Fully materialized token stream, terminated by a single EOF token | ||
| 19 | unsigned long curIdx // Index of the current token within 'tokens' | ||
| 20 | // Comment-free lookahead index. 'relevantIndices' lists the positions in 'tokens' of all | ||
| 21 | // non-comment (relevant) tokens, and 'relevantOrdinal[i]' is the ordinal of the relevant token | ||
| 22 | // at position i within 'relevantIndices'. Together they give the parser O(1) relevant-token | ||
| 23 | // lookahead (see peekRelevantType) instead of re-scanning the stream on every peek. | ||
| 24 | Vector<unsigned long> relevantIndices | ||
| 25 | Vector<unsigned long> relevantOrdinal | ||
| 26 | } | ||
| 27 | |||
| 28 | 3963 | public p Lexer.ctor(const FilePath& filePath) { | |
| 29 | 3963 | this.reader = Reader(filePath); | |
| 30 | 3963 | this.curIdx = 0l; | |
| 31 | |||
| 32 | // Tokenize the whole input up front. Materializing the token stream once turns getToken(), | ||
| 33 | // advance() and peek() into O(1) index operations. The previous streaming design re-scanned | ||
| 34 | // the reader (and shifted a lookahead buffer) on every peek, which made the parser's repeated | ||
| 35 | // lookahead quadratic and dominated the overall parse time. | ||
| 36 | 2663887 | while true { | |
| 37 | 2663887 | Token token = this.readRawToken(); | |
| 38 | 2663887 | const bool isEof = token.tokenType == TokenType::EOF; | |
| 39 | 2663887 | this.tokens.pushBack(token); | |
| 40 | 2663887 | if isEof { | |
| 41 | 3963 | break; | |
| 42 | } | ||
| 43 | 2663887 | } | |
| 44 | |||
| 45 | // Build the comment-free lookahead index over the materialized token stream. | ||
| 46 | 3963 | const unsigned long tokenCount = this.tokens.getSize(); | |
| 47 | 3963 | unsigned long ordinal = 0l; | |
| 48 | 2667850 | for unsigned long i = 0l; i < tokenCount; i++ { | |
| 49 | 2663887 | this.relevantOrdinal.pushBack(ordinal); | |
| 50 | 5280958 | if !this.isIgnoredType(this.tokens.get(i).tokenType) { | |
| 51 | 2568154 | this.relevantIndices.pushBack(i); | |
| 52 | 2568154 | ordinal++; | |
| 53 | 2568154 | } | |
| 54 | 2663887 | } | |
| 55 | 3963 | } | |
| 56 | |||
| 57 | inline f<bool> Lexer.isIgnoredType(TokenType tokenType) { | ||
| 58 | return tokenType == TokenType::LINE_COMMENT || tokenType == TokenType::BLOCK_COMMENT || tokenType == TokenType::DOC_COMMENT; | ||
| 59 | } | ||
| 60 | |||
| 61 | /** | ||
| 62 | * Return the token type of the k-th relevant (non-comment) token at or after the current one, | ||
| 63 | * where k=0 is the current token. This is O(1) and is the fast path the parser uses for lookahead. | ||
| 64 | * | ||
| 65 | * @param k Zero-based relevant-token lookahead distance | ||
| 66 | * @return The token type at that position (EOF when looking past the end) | ||
| 67 | */ | ||
| 68 | 4479612 | public f<TokenType> Lexer.peekRelevantType(unsigned long k) { | |
| 69 | 4479612 | unsigned long ord = this.relevantOrdinal.get(this.curIdx) + k; | |
| 70 | 4479612 | const unsigned long lastOrd = this.relevantIndices.getSize() - 1l; | |
| 71 | 4479612 | if ord > lastOrd { | |
| 72 | ✗ | ord = lastOrd; | |
| 73 | ✗ | } | |
| 74 | 4479612 | return this.tokens.get(this.relevantIndices.get(ord)).tokenType; | |
| 75 | } | ||
| 76 | |||
| 77 | 28832386 | public f<const Token&> Lexer.getToken() { | |
| 78 | 28832386 | return this.tokens.get(this.curIdx); | |
| 79 | } | ||
| 80 | |||
| 81 | /** | ||
| 82 | * Get the last relevant (non-comment) token before the current one, e.g. the last token of a node, that was just parsed. | ||
| 83 | * | ||
| 84 | * @return Previous relevant token or the current token, if there is none | ||
| 85 | */ | ||
| 86 | 2230901 | public f<const Token&> Lexer.getPreviousRelevantToken() { | |
| 87 | 2230901 | const unsigned long ordinal = this.relevantOrdinal.get(this.curIdx); | |
| 88 | 2230901 | if ordinal == 0l { | |
| 89 | 1 | return this.tokens.get(this.curIdx); | |
| 90 | } | ||
| 91 | 2230900 | return this.tokens.get(this.relevantIndices.get(ordinal - 1l)); | |
| 92 | } | ||
| 93 | |||
| 94 | 2659840 | public p Lexer.advance() { | |
| 95 | // Stop at the terminating EOF token so that getToken() keeps returning EOF once the end is reached. | ||
| 96 | 2659840 | if this.curIdx + 1l < this.tokens.getSize() { | |
| 97 | 2659840 | this.curIdx++; | |
| 98 | 2659840 | } | |
| 99 | 2659840 | } | |
| 100 | |||
| 101 | /** | ||
| 102 | * Get the index of the current token within the token stream | ||
| 103 | * | ||
| 104 | * @return Current token index | ||
| 105 | */ | ||
| 106 | 8478 | public f<unsigned long> Lexer.getTokenIndex() { | |
| 107 | 8478 | return this.curIdx; | |
| 108 | } | ||
| 109 | |||
| 110 | /** | ||
| 111 | * Concatenate the texts of all relevant (non-comment) tokens in the index range [startIdx, endIdx), without whitespace. | ||
| 112 | * | ||
| 113 | * @param startIdx Index of the first token | ||
| 114 | * @param endIdx Index after the last token | ||
| 115 | * @return Concatenated token text | ||
| 116 | */ | ||
| 117 | 159 | public f<String> Lexer.getTokenText(unsigned long startIdx, unsigned long endIdx) { | |
| 118 | 159 | String text; | |
| 119 | 429 | for unsigned long i = startIdx; i < endIdx; i++ { | |
| 120 | 270 | const Token& token = this.tokens.get(i); | |
| 121 | 540 | if !this.isIgnoredType(token.tokenType) { | |
| 122 | 270 | text += token.text; | |
| 123 | 270 | } | |
| 124 | 270 | } | |
| 125 | 159 | return text; | |
| 126 | } | ||
| 127 | |||
| 128 | /** | ||
| 129 | * Get the source code, that is covered by the relevant (non-comment) tokens in the index range [startIdx, endIdx). | ||
| 130 | * | ||
| 131 | * @param startIdx Index of the first token | ||
| 132 | * @param endIdx Index after the last token | ||
| 133 | * @return Source code snippet | ||
| 134 | */ | ||
| 135 | 4080 | public f<String> Lexer.getSourceCode(unsigned long startIdx, unsigned long endIdx) { | |
| 136 | // Skip trailing comment tokens | ||
| 137 | 8160 | while endIdx > startIdx && this.isIgnoredType(this.tokens.get(endIdx - 1l).tokenType) { | |
| 138 | ✗ | endIdx--; | |
| 139 | ✗ | } | |
| 140 | 4080 | if endIdx <= startIdx { | |
| 141 | ✗ | return String(); | |
| 142 | } | ||
| 143 | 4080 | const Token& firstToken = this.tokens.get(startIdx); | |
| 144 | 4080 | const Token& lastToken = this.tokens.get(endIdx - 1l); | |
| 145 | 4080 | return this.reader.getSourceCode(firstToken.startOffset, lastToken.endOffset); | |
| 146 | } | ||
| 147 | |||
| 148 | /** | ||
| 149 | * Read the next raw token directly from the reader, skipping leading whitespace. | ||
| 150 | * This does not touch the lookahead buffer and is the single source of truth for | ||
| 151 | * turning characters into tokens. | ||
| 152 | */ | ||
| 153 | 2663888 | f<Token> Lexer.readRawToken() { | |
| 154 | // Skip any whitespaces | ||
| 155 | 5412968 | while isWhitespace(this.reader.getChar()) && !this.reader.isEOF() { | |
| 156 | 2749080 | this.reader.advance(); | |
| 157 | 2749080 | } | |
| 158 | |||
| 159 | // Consume the token and remember where it is located within the source code | ||
| 160 | 2663888 | const unsigned long startOffset = this.reader.getOffset(); | |
| 161 | 2663888 | Token token = this.consumeToken(); | |
| 162 | 2663888 | token.startOffset = startOffset; | |
| 163 | 2663888 | token.endOffset = this.reader.getOffset(); | |
| 164 | // A code location, created from a token, covers the token | ||
| 165 | 2663888 | token.codeLoc.sourceIntervalStart = cast<long>(token.startOffset); | |
| 166 | 2663888 | token.codeLoc.sourceIntervalStop = cast<long>(token.endOffset) - 1l; | |
| 167 | 2663888 | return token; | |
| 168 | } | ||
| 169 | |||
| 170 | 2323751 | public f<CodeLoc> Lexer.getCodeLoc() { | |
| 171 | 2323751 | return this.getToken().codeLoc; | |
| 172 | } | ||
| 173 | |||
| 174 | 2663888 | f<Token> Lexer.consumeToken() { | |
| 175 | // Get the current char from the reader instance | ||
| 176 | 2663888 | const char curChar = this.reader.getChar(); | |
| 177 | 2663888 | const CodeLoc codeLoc = this.reader.getCodeLoc(); | |
| 178 | |||
| 179 | // Check if EOF | ||
| 180 | 2663888 | if this.reader.isEOF() { | |
| 181 | 3963 | return Token(TokenType::EOF, "EOF", codeLoc); | |
| 182 | } | ||
| 183 | // Check if identifier | ||
| 184 | 2659925 | if isAlpha(curChar) || curChar == '_' { | |
| 185 | 1171437 | if isUpper(curChar) { // Type identifier | |
| 186 | 136986 | return this.consumeTypeIdentifier(codeLoc); | |
| 187 | } else { // Normal identifier or keyword | ||
| 188 | 1034451 | return this.consumeKeywordOrIdentifier(codeLoc); | |
| 189 | } | ||
| 190 | ✗ | } | |
| 191 | // Check if number | ||
| 192 | 1488488 | if isDigit(curChar) { | |
| 193 | // Consume number literal | ||
| 194 | 51680 | Token numericLiteral = this.consumeNumberLiteral(codeLoc); | |
| 195 | 51680 | if numericLiteral.tokenType == TokenType::DOUBLE_LIT { | |
| 196 | 1351 | return numericLiteral; | |
| 197 | } | ||
| 198 | |||
| 199 | // Consume the optional unsigned suffix. It stays part of the token text, so that the parser can detect it | ||
| 200 | 50329 | if this.reader.getChar() == 'u' { | |
| 201 | 3712 | numericLiteral.text += 'u'; | |
| 202 | 3712 | this.reader.advance(); // Consume 'u' | |
| 203 | 3712 | } | |
| 204 | |||
| 205 | // Patch token type if suffix is present and consume the suffix character | ||
| 206 | 50329 | const char literalSuffix = this.reader.getChar(); | |
| 207 | 50329 | if literalSuffix == 's' { | |
| 208 | 2234 | numericLiteral.tokenType = TokenType::SHORT_LIT; | |
| 209 | 2234 | this.reader.advance(); // Consume 's' | |
| 210 | 50329 | } else if literalSuffix == 'l' { | |
| 211 | 29377 | numericLiteral.tokenType = TokenType::LONG_LIT; | |
| 212 | 29377 | this.reader.advance(); // Consume 'l' | |
| 213 | 48095 | } | |
| 214 | |||
| 215 | 50329 | return numericLiteral; | |
| 216 | } | ||
| 217 | // Check if char literal | ||
| 218 | 1436808 | if curChar == '\'' { | |
| 219 | 9889 | return this.consumeCharLiteral(codeLoc); | |
| 220 | } | ||
| 221 | // Check if string literal | ||
| 222 | 1426919 | if curChar == '"' { | |
| 223 | 17183 | return this.consumeStringLiteral(codeLoc); | |
| 224 | } | ||
| 225 | |||
| 226 | // Double literal without integer part, e.g. '.5' | ||
| 227 | 1409736 | if curChar == '.' && isDigit(this.reader.peekChar()) { | |
| 228 | ✗ | return this.consumeNumberLiteral(codeLoc); | |
| 229 | } | ||
| 230 | |||
| 231 | // Consume the first char of the operator and check for longer operators, starting with it | ||
| 232 | 1409736 | this.reader.advance(); | |
| 233 | 1409736 | switch curChar { | |
| 234 | 98433 | case '{': { return Token(TokenType::LBRACE, "{", codeLoc); } | |
| 235 | 98433 | case '}': { return Token(TokenType::RBRACE, "}", codeLoc); } | |
| 236 | 149442 | case '(': { return Token(TokenType::LPAREN, "(", codeLoc); } | |
| 237 | 149442 | case ')': { return Token(TokenType::RPAREN, ")", codeLoc); } | |
| 238 | 18457 | case '[': { return Token(TokenType::LBRACKET, "[", codeLoc); } | |
| 239 | 18457 | case ']': { return Token(TokenType::RBRACKET, "]", codeLoc); } | |
| 240 | 34 | case '~': { return Token(TokenType::BITWISE_NOT, "~", codeLoc); } | |
| 241 | 2600 | case '?': { return Token(TokenType::QUESTION_MARK, "?", codeLoc); } | |
| 242 | 159657 | case ';': { return Token(TokenType::SEMICOLON, ";", codeLoc); } | |
| 243 | 75919 | case ',': { return Token(TokenType::COMMA, ",", codeLoc); } | |
| 244 | case '|': { | ||
| 245 | 15916 | if this.consumeIf('|') { return Token(TokenType::LOGICAL_OR, "||", codeLoc); } | |
| 246 | 5808 | if this.consumeIf('=') { return Token(TokenType::OR_EQUAL, "|=", codeLoc); } | |
| 247 | 2881 | return Token(TokenType::BITWISE_OR, "|", codeLoc); | |
| 248 | } | ||
| 249 | case '&': { | ||
| 250 | 55912 | if this.consumeIf('&') { return Token(TokenType::LOGICAL_AND, "&&", codeLoc); } | |
| 251 | 51160 | if this.consumeIf('=') { return Token(TokenType::AND_EQUAL, "&=", codeLoc); } | |
| 252 | 25559 | return Token(TokenType::BITWISE_AND, "&", codeLoc); | |
| 253 | } | ||
| 254 | case '^': { | ||
| 255 | 628 | if this.consumeIf('=') { return Token(TokenType::XOR_EQUAL, "^=", codeLoc); } | |
| 256 | 83 | return Token(TokenType::BITWISE_XOR, "^", codeLoc); | |
| 257 | } | ||
| 258 | case '!': { | ||
| 259 | 19732 | if this.consumeIf('=') { return Token(TokenType::NOT_EQUAL, "!=", codeLoc); } | |
| 260 | 4240 | return Token(TokenType::NOT, "!", codeLoc); | |
| 261 | } | ||
| 262 | case '+': { | ||
| 263 | 43118 | if this.consumeIf('+') { return Token(TokenType::PLUS_PLUS, "++", codeLoc); } | |
| 264 | 28724 | if this.consumeIf('=') { return Token(TokenType::PLUS_EQUAL, "+=", codeLoc); } | |
| 265 | 11118 | return Token(TokenType::PLUS, "+", codeLoc); | |
| 266 | } | ||
| 267 | case '-': { | ||
| 268 | 23464 | if this.consumeIf('-') { return Token(TokenType::MINUS_MINUS, "--", codeLoc); } | |
| 269 | 21010 | if this.consumeIf('=') { return Token(TokenType::MINUS_EQUAL, "-=", codeLoc); } | |
| 270 | 20404 | if this.consumeIf('>') { return Token(TokenType::ARROW, "->", codeLoc); } | |
| 271 | 10198 | return Token(TokenType::MINUS, "-", codeLoc); | |
| 272 | } | ||
| 273 | case '*': { | ||
| 274 | 71202 | if this.consumeIf('=') { return Token(TokenType::MUL_EQUAL, "*=", codeLoc); } | |
| 275 | 35191 | return Token(TokenType::MUL, "*", codeLoc); | |
| 276 | } | ||
| 277 | case '/': { | ||
| 278 | 192392 | if this.consumeIf('=') { return Token(TokenType::DIV_EQUAL, "/=", codeLoc); } | |
| 279 | 96058 | if this.reader.getChar() == '/' { return this.consumeLineComment(codeLoc); } | |
| 280 | 49242 | if this.reader.getChar() == '*' { return this.consumeBlockOrDocComment(codeLoc); } | |
| 281 | 325 | return Token(TokenType::DIV, "/", codeLoc); | |
| 282 | } | ||
| 283 | case '%': { | ||
| 284 | 628 | if this.consumeIf('=') { return Token(TokenType::REM_EQUAL, "%=", codeLoc); } | |
| 285 | 264 | return Token(TokenType::REM, "%", codeLoc); | |
| 286 | } | ||
| 287 | case '=': { | ||
| 288 | 183020 | if this.consumeIf('=') { return Token(TokenType::EQUAL, "==", codeLoc); } | |
| 289 | 67473 | return Token(TokenType::ASSIGN, "=", codeLoc); | |
| 290 | } | ||
| 291 | case '<': { | ||
| 292 | 178660 | if this.consumeIf('=') { return Token(TokenType::LESS_EQUAL, "<=", codeLoc); } | |
| 293 | 175448 | if this.consumeIf('<') { | |
| 294 | 994 | if this.consumeIf('=') { return Token(TokenType::SHL_EQUAL, "<<=", codeLoc); } | |
| 295 | 493 | return Token(TokenType::SHL, "<<", codeLoc); | |
| 296 | } | ||
| 297 | 87227 | return Token(TokenType::LESS, "<", codeLoc); | |
| 298 | } | ||
| 299 | case '>': { | ||
| 300 | 170130 | if this.consumeIf('=') { return Token(TokenType::GREATER_EQUAL, ">=", codeLoc); } | |
| 301 | 163158 | if this.consumeIf('>') { | |
| 302 | 5978 | if this.consumeIf('=') { return Token(TokenType::SHR_EQUAL, ">>=", codeLoc); } | |
| 303 | 2978 | return Token(TokenType::SHR, ">>", codeLoc); | |
| 304 | } | ||
| 305 | 78590 | return Token(TokenType::GREATER, ">", codeLoc); | |
| 306 | } | ||
| 307 | case ':': { | ||
| 308 | 12830 | if this.consumeIf(':') { return Token(TokenType::SCOPE_ACCESS, "::", codeLoc); } | |
| 309 | 3676 | return Token(TokenType::COLON, ":", codeLoc); | |
| 310 | } | ||
| 311 | case '.': { | ||
| 312 | 303864 | if this.consumeIf('.') { | |
| 313 | 717 | this.reader.expect('.'); // Consume third '.' | |
| 314 | 717 | return Token(TokenType::ELLIPSIS, "...", codeLoc); | |
| 315 | } | ||
| 316 | 151215 | return Token(TokenType::DOT, ".", codeLoc); | |
| 317 | } | ||
| 318 | case '#': { | ||
| 319 | 6228 | if this.consumeIf('!') { return Token(TokenType::MOD_ATTR_PREAMBLE, "#!", codeLoc); } | |
| 320 | 1574 | return Token(TokenType::FCT_ATTR_PREAMBLE, "#", codeLoc); | |
| 321 | } | ||
| 322 | } | ||
| 323 | |||
| 324 | − | const String message = String("token recognition error at: '") + curChar + "'"; // LCOV_EXCL_LINE | |
| 325 | − | this.throwLexerError(codeLoc, message.getRaw()); // LCOV_EXCL_LINE | |
| 326 | − | return Token(TokenType::INVALID); // LCOV_EXCL_LINE | |
| 327 | ✗ | } | |
| 328 | |||
| 329 | /** | ||
| 330 | * Consume the current char, if it is the expected one | ||
| 331 | * | ||
| 332 | * @param expected Expected char | ||
| 333 | * @return Whether the char was consumed | ||
| 334 | */ | ||
| 335 | inline f<bool> Lexer.consumeIf(char expected) { | ||
| 336 | if this.reader.getChar() != expected { | ||
| 337 | return false; | ||
| 338 | } | ||
| 339 | this.reader.advance(); | ||
| 340 | return true; | ||
| 341 | } | ||
| 342 | |||
| 343 | /** | ||
| 344 | * Abort lexing with a lexer error | ||
| 345 | * | ||
| 346 | * @param codeLoc Code location of the error | ||
| 347 | * @param message Error message suffix | ||
| 348 | */ | ||
| 349 | 1 | p Lexer.throwLexerError(const CodeLoc& codeLoc, string message) { | |
| 350 | 1 | const LexerError lexerError = LexerError(codeLoc, LexerErrorType::TOKENIZING_FAILED, message); | |
| 351 | 1 | abortWithError(lexerError.getMessage()); | |
| 352 | 1 | } | |
| 353 | |||
| 354 | 51680 | f<Token> Lexer.consumeNumberLiteral(const CodeLoc& codeLoc) { | |
| 355 | 51680 | String numberStr; | |
| 356 | |||
| 357 | // Check for optional minus sign | ||
| 358 | 51680 | if this.reader.getChar() == '-' { | |
| 359 | ✗ | numberStr += '-'; | |
| 360 | ✗ | this.reader.advance(); // Consume '-' | |
| 361 | ✗ | } | |
| 362 | |||
| 363 | // Check for different numeric literal formats | ||
| 364 | 51680 | if this.reader.getChar() == '0' { // Potentially a base-prefixed literal | |
| 365 | 24559 | this.reader.advance(); // Consume '0' | |
| 366 | 24559 | numberStr += '0'; | |
| 367 | // Peek the character after '0', which is the base of the number ('x', 'b', 'o' or 'd') if present | ||
| 368 | 24559 | const char base = toLower(this.reader.getChar()); | |
| 369 | 24559 | if base == 'x' || base == 'h' || base == 'b' || base == 'o' || base == 'd' { | |
| 370 | 1308 | numberStr += this.reader.getChar(); | |
| 371 | 1308 | this.reader.advance(); // Consume the base character | |
| 372 | // Decide what to do, depending on the base | ||
| 373 | 1308 | if base == 'x' || base == 'h' { // Hexadecimal number | |
| 374 | 6030 | while isHexDigit(this.reader.getChar()) { | |
| 375 | 5456 | numberStr += this.reader.getChar(); | |
| 376 | 5456 | this.reader.advance(); | |
| 377 | 5456 | } | |
| 378 | 1308 | } else if base == 'b' { // Binary number | |
| 379 | ✗ | while isBinDigit(this.reader.getChar()) { | |
| 380 | ✗ | numberStr += this.reader.getChar(); | |
| 381 | ✗ | this.reader.advance(); | |
| 382 | ✗ | } | |
| 383 | 734 | } else if base == 'o' { // Octal number | |
| 384 | 5326 | while isOctDigit(this.reader.getChar()) { | |
| 385 | 4592 | numberStr += this.reader.getChar(); | |
| 386 | 4592 | this.reader.advance(); | |
| 387 | 4592 | } | |
| 388 | 734 | } else { // Decimal number with explicit '0d' prefix | |
| 389 | ✗ | while isDigit(this.reader.getChar()) { | |
| 390 | ✗ | numberStr += this.reader.getChar(); | |
| 391 | ✗ | this.reader.advance(); | |
| 392 | ✗ | } | |
| 393 | 734 | } | |
| 394 | // The correct token type is set a layer above in the parsing functions for int, short, long, etc. | ||
| 395 | 2616 | return Token(TokenType::INT_LIT, numberStr, codeLoc); | |
| 396 | } | ||
| 397 | 23251 | } | |
| 398 | |||
| 399 | // Decimal number (or the remainder of a decimal number with leading zero) | ||
| 400 | 90544 | while isDigit(this.reader.getChar()) { | |
| 401 | 40172 | numberStr += this.reader.getChar(); | |
| 402 | 40172 | this.reader.advance(); | |
| 403 | 40172 | } | |
| 404 | |||
| 405 | // Check for a double literal: [0-9]*[.][0-9]+([eE][+-]?[0-9]+)? | ||
| 406 | 50372 | if this.reader.getChar() == '.' && isDigit(this.reader.peekChar()) { | |
| 407 | 1351 | numberStr += '.'; | |
| 408 | 1351 | this.reader.advance(); // Consume '.' | |
| 409 | 4203 | while isDigit(this.reader.getChar()) { | |
| 410 | 2852 | numberStr += this.reader.getChar(); | |
| 411 | 2852 | this.reader.advance(); | |
| 412 | 2852 | } | |
| 413 | // Check for optional exponent | ||
| 414 | 1351 | const char exponentChar = this.reader.getChar(); | |
| 415 | 1351 | const char afterExponentChar = this.reader.peekChar(); | |
| 416 | 1351 | const bool hasExponentSign = afterExponentChar == '+' || afterExponentChar == '-'; | |
| 417 | 1351 | if (exponentChar == 'e' || exponentChar == 'E') && (isDigit(afterExponentChar) || hasExponentSign) { | |
| 418 | 10 | numberStr += exponentChar; | |
| 419 | 10 | this.reader.advance(); // Consume 'e' or 'E' | |
| 420 | 10 | if hasExponentSign { | |
| 421 | 6 | numberStr += afterExponentChar; | |
| 422 | 6 | this.reader.advance(); // Consume sign | |
| 423 | 6 | } | |
| 424 | 10 | if !isDigit(this.reader.getChar()) { | |
| 425 | ✗ | const String message = "token recognition error at: '" + numberStr + "'"; | |
| 426 | ✗ | this.throwLexerError(codeLoc, message.getRaw()); | |
| 427 | ✗ | } | |
| 428 | 34 | while isDigit(this.reader.getChar()) { | |
| 429 | 24 | numberStr += this.reader.getChar(); | |
| 430 | 24 | this.reader.advance(); | |
| 431 | 24 | } | |
| 432 | 10 | } | |
| 433 | 2702 | return Token(TokenType::DOUBLE_LIT, numberStr, codeLoc); | |
| 434 | } | ||
| 435 | |||
| 436 | // The correct token type is set a layer above in the parsing functions for int, short, long, etc. | ||
| 437 | 98042 | return Token(TokenType::INT_LIT, numberStr, codeLoc); | |
| 438 | 51680 | } | |
| 439 | |||
| 440 | 9889 | f<Token> Lexer.consumeCharLiteral(const CodeLoc& codeLoc) { | |
| 441 | // Parse the following regex: '\'' (~['\\\r\n] | '\\' (. | EOF)) '\'' | ||
| 442 | 9889 | String charStr; | |
| 443 | 9889 | this.reader.expect('\''); // Consume '\'' | |
| 444 | 9889 | if this.reader.getChar() == '\\' { // Escape sequence | |
| 445 | // Keep the escape sequence as is. It gets resolved by the parser | ||
| 446 | 3638 | charStr += '\\'; | |
| 447 | 3638 | this.reader.advance(); // Consume '\\' | |
| 448 | 3638 | charStr += this.reader.getChar(); | |
| 449 | 3638 | this.reader.advance(); // Consume escaped character | |
| 450 | 3638 | } else { // Normal character | |
| 451 | 6251 | charStr += this.reader.getChar(); | |
| 452 | 6251 | this.reader.advance(); // Consume character | |
| 453 | 6251 | } | |
| 454 | 9889 | this.reader.expect('\''); // Consume '\'' | |
| 455 | 19778 | return Token(TokenType::CHAR_LIT, charStr, codeLoc); | |
| 456 | 9889 | } | |
| 457 | |||
| 458 | 17183 | f<Token> Lexer.consumeStringLiteral(const CodeLoc& codeLoc) { | |
| 459 | // Parse the following regex: '"' (~["\\\r\n] | '\\' (. | EOF))* '"' | ||
| 460 | 17183 | String stringStr; | |
| 461 | 17183 | this.reader.expect('"'); // Consume '"' | |
| 462 | 296042 | while this.reader.getChar() != '"' { | |
| 463 | // A string literal must be terminated in the same line | ||
| 464 | 278859 | if this.reader.isEOF() || this.reader.getChar() == '\n' { | |
| 465 | 1 | const String message = "token recognition error at: '\"" + stringStr + "'"; | |
| 466 | 1 | this.throwLexerError(codeLoc, message.getRaw()); | |
| 467 | 1 | } | |
| 468 | 278859 | if this.reader.getChar() == '\\' { // Escape sequence | |
| 469 | // Keep the escape sequence as is. It gets resolved by the parser | ||
| 470 | 4233 | stringStr += '\\'; | |
| 471 | 4233 | this.reader.advance(); // Consume '\\' | |
| 472 | 4233 | stringStr += this.reader.getChar(); | |
| 473 | 4233 | this.reader.advance(); // Consume escaped character | |
| 474 | 4233 | } else { // Normal character | |
| 475 | 274626 | stringStr += this.reader.getChar(); | |
| 476 | 274626 | this.reader.advance(); // Consume character | |
| 477 | 274626 | } | |
| 478 | 278859 | } | |
| 479 | 17183 | this.reader.expect('"'); // Consume '"' | |
| 480 | 34366 | return Token(TokenType::STRING_LIT, stringStr, codeLoc); | |
| 481 | 17183 | } | |
| 482 | |||
| 483 | 1034451 | f<Token> Lexer.consumeKeywordOrIdentifier(const CodeLoc& codeLoc) { | |
| 484 | 1034451 | String identifier; | |
| 485 | 1034451 | do { | |
| 486 | 5645394 | identifier += this.reader.getChar(); | |
| 487 | 5645394 | this.reader.advance(); | |
| 488 | 5645394 | } while isAlphaNum(this.reader.getChar()) || this.reader.getChar() == '_'; | |
| 489 | |||
| 490 | // Keywords keep their text as token text, so only the token type needs to be looked up | ||
| 491 | 2068902 | return Token(this.getKeywordTokenType(identifier), identifier, codeLoc); | |
| 492 | 1034451 | } | |
| 493 | |||
| 494 | /** | ||
| 495 | * Map an identifier to its keyword token type. | ||
| 496 | * | ||
| 497 | * @param identifier Identifier text | ||
| 498 | * @return Token type of the keyword or IDENTIFIER, if the text is not a keyword | ||
| 499 | */ | ||
| 500 | 1034451 | f<TokenType> Lexer.getKeywordTokenType(const String& identifier) { | |
| 501 | // Dispatch on the length first, so that an identifier is only compared against the keywords of the same length | ||
| 502 | 1034451 | switch identifier.getLength() { | |
| 503 | case 1l: { | ||
| 504 | 97654 | if identifier == "f" { return TokenType::F; } | |
| 505 | 62890 | if identifier == "p" { return TokenType::P; } | |
| 506 | 44029 | } | |
| 507 | case 2l: { | ||
| 508 | 33550 | if identifier == "if" { return TokenType::IF; } | |
| 509 | 8375 | if identifier == "do" { return TokenType::DO; } | |
| 510 | 8325 | if identifier == "as" { return TokenType::AS; } | |
| 511 | 8220 | } | |
| 512 | case 3l: { | ||
| 513 | 77197 | if identifier == "int" { return TokenType::TYPE_INT; } | |
| 514 | 58780 | if identifier == "dyn" { return TokenType::TYPE_DYN; } | |
| 515 | 57147 | if identifier == "for" { return TokenType::FOR; } | |
| 516 | 52742 | if identifier == "nil" { return TokenType::NIL; } | |
| 517 | 44460 | if identifier == "ext" { return TokenType::EXT; } | |
| 518 | 39231 | } | |
| 519 | case 4l: { | ||
| 520 | 260082 | if identifier == "long" { return TokenType::TYPE_LONG; } | |
| 521 | 225431 | if identifier == "byte" { return TokenType::TYPE_BYTE; } | |
| 522 | 212462 | if identifier == "char" { return TokenType::TYPE_CHAR; } | |
| 523 | 190511 | if identifier == "bool" { return TokenType::TYPE_BOOL; } | |
| 524 | 176848 | if identifier == "heap" { return TokenType::HEAP; } | |
| 525 | 158271 | if identifier == "else" { return TokenType::ELSE; } | |
| 526 | 156874 | if identifier == "case" { return TokenType::CASE; } | |
| 527 | 156678 | if identifier == "type" { return TokenType::TYPE; } | |
| 528 | 149396 | if identifier == "enum" { return TokenType::ENUM; } | |
| 529 | 148930 | if identifier == "main" { return TokenType::MAIN; } | |
| 530 | 147893 | if identifier == "cast" { return TokenType::CAST; } | |
| 531 | 135444 | if identifier == "true" { return TokenType::TRUE; } | |
| 532 | 131661 | } | |
| 533 | case 5l: { | ||
| 534 | 84704 | if identifier == "short" { return TokenType::TYPE_SHORT; } | |
| 535 | 81494 | if identifier == "const" { return TokenType::CONST; } | |
| 536 | 48971 | if identifier == "while" { return TokenType::WHILE; } | |
| 537 | 47056 | if identifier == "break" { return TokenType::BREAK; } | |
| 538 | 46315 | if identifier == "union" { return TokenType::UNION; } | |
| 539 | 46289 | if identifier == "alias" { return TokenType::ALIAS; } | |
| 540 | 46130 | if identifier == "false" { return TokenType::FALSE; } | |
| 541 | 43169 | } | |
| 542 | case 6l: { | ||
| 543 | 227811 | if identifier == "double" { return TokenType::TYPE_DOUBLE; } | |
| 544 | 226388 | if identifier == "string" { return TokenType::TYPE_STRING; } | |
| 545 | 208420 | if identifier == "signed" { return TokenType::SIGNED; } | |
| 546 | 208412 | if identifier == "inline" { return TokenType::INLINE; } | |
| 547 | 195386 | if identifier == "public" { return TokenType::PUBLIC; } | |
| 548 | 148666 | if identifier == "switch" { return TokenType::SWITCH; } | |
| 549 | 148627 | if identifier == "assert" { return TokenType::ASSERT; } | |
| 550 | 144547 | if identifier == "import" { return TokenType::IMPORT; } | |
| 551 | 142029 | if identifier == "return" { return TokenType::RETURN; } | |
| 552 | 101794 | if identifier == "struct" { return TokenType::STRUCT; } | |
| 553 | 98561 | if identifier == "unsafe" { return TokenType::UNSAFE; } | |
| 554 | 85803 | } | |
| 555 | case 7l: { | ||
| 556 | 29730 | if identifier == "compose" { return TokenType::COMPOSE; } | |
| 557 | 29711 | if identifier == "default" { return TokenType::DEFAULT; } | |
| 558 | 29691 | if identifier == "foreach" { return TokenType::FOREACH; } | |
| 559 | 29256 | } | |
| 560 | case 8l: { | ||
| 561 | 115544 | if identifier == "unsigned" { return TokenType::UNSIGNED; } | |
| 562 | 75823 | if identifier == "continue" { return TokenType::CONTINUE; } | |
| 563 | 75061 | if identifier == "operator" { return TokenType::OPERATOR; } | |
| 564 | 68787 | } | |
| 565 | case 9l: { | ||
| 566 | 27734 | if identifier == "interface" { return TokenType::INTERFACE; } | |
| 567 | 27407 | } | |
| 568 | case 11l: { | ||
| 569 | 14549 | if identifier == "fallthrough" { return TokenType::FALLTHROUGH; } | |
| 570 | 14533 | } | |
| 571 | } | ||
| 572 | 557992 | return TokenType::IDENTIFIER; | |
| 573 | } | ||
| 574 | |||
| 575 | 136986 | f<Token> Lexer.consumeTypeIdentifier(const CodeLoc& codeLoc) { | |
| 576 | 136986 | String identifier; | |
| 577 | 136986 | do { | |
| 578 | 954673 | identifier += this.reader.getChar(); | |
| 579 | 954673 | this.reader.advance(); | |
| 580 | 954673 | } while isAlphaNum(this.reader.getChar()) || this.reader.getChar() == '_'; | |
| 581 | 273972 | return Token(TokenType::TYPE_IDENTIFIER, identifier, codeLoc); | |
| 582 | 136986 | } | |
| 583 | |||
| 584 | 46816 | f<Token> Lexer.consumeLineComment(const CodeLoc& codeLoc) { | |
| 585 | 46816 | this.reader.expect('/'); // Consume second '/' (first '/' is consumed by the caller) | |
| 586 | |||
| 587 | 46816 | String comment = String("//"); | |
| 588 | 2446996 | while !this.reader.isEOF() { | |
| 589 | // Check for comment end | ||
| 590 | 2446993 | if this.reader.getChar() == '\n' { | |
| 591 | 46813 | this.reader.advance(); // Consume '\n' | |
| 592 | 46813 | break; | |
| 593 | } | ||
| 594 | |||
| 595 | 2400180 | comment += this.reader.getChar(); | |
| 596 | 2400180 | this.reader.advance(); | |
| 597 | 2400180 | } | |
| 598 | |||
| 599 | 93632 | return Token(TokenType::LINE_COMMENT, comment, codeLoc); | |
| 600 | 46816 | } | |
| 601 | |||
| 602 | 48917 | f<Token> Lexer.consumeBlockOrDocComment(const CodeLoc& codeLoc) { | |
| 603 | 48917 | this.reader.expect('*'); // Consume '*' ('/' is consumed by the caller) | |
| 604 | 48917 | String comment = String("/*"); | |
| 605 | |||
| 606 | // Check if doc comment | ||
| 607 | 48917 | bool isDocComment = false; | |
| 608 | 48917 | if this.reader.getChar() == '*' { | |
| 609 | 44587 | isDocComment = true; | |
| 610 | 44587 | this.reader.advance(); // Consume '*' | |
| 611 | 44587 | comment += '*'; | |
| 612 | 44587 | } | |
| 613 | |||
| 614 | 5895993 | while !this.reader.isEOF() { | |
| 615 | // Check for comment end | ||
| 616 | 5895993 | if this.reader.getChar() == '*' { | |
| 617 | 197952 | this.reader.advance(); // Consume '*' | |
| 618 | 197952 | comment += '*'; | |
| 619 | 197952 | if this.reader.getChar() == '/' { | |
| 620 | 48917 | this.reader.advance(); // Consume '/' | |
| 621 | 48917 | comment += '/'; | |
| 622 | 48917 | break; | |
| 623 | } | ||
| 624 | 149035 | } | |
| 625 | |||
| 626 | 5847076 | comment += this.reader.getChar(); | |
| 627 | 5847076 | this.reader.advance(); | |
| 628 | 5847076 | } | |
| 629 | |||
| 630 | 48917 | return Token(isDocComment ? TokenType::DOC_COMMENT : TokenType::BLOCK_COMMENT, comment, codeLoc); | |
| 631 | 48917 | } | |
| 632 |