#include "tree_sitter/alloc.h" #include "tree_sitter/array.h" #include "tree_sitter/parser.h" #include #include #include #include #include // Functions for converting between UTF-8 and UTF-32 #include "unicode.c" /* * Token types */ enum Token { LINE_BREAK, LINE_CONTINUATION, START_OF_BRACE_BLOCK, START_OF_HASH_OR_TUPLE, START_OF_NAMED_TUPLE, START_OF_TUPLE_TYPE, START_OF_NAMED_TUPLE_TYPE, START_OF_INDEX_OPERATOR, END_OF_WITH_EXPRESSSION, UNARY_PLUS, UNARY_MINUS, BINARY_PLUS, BINARY_MINUS, UNARY_WRAPPING_PLUS, UNARY_WRAPPING_MINUS, BINARY_WRAPPING_PLUS, BINARY_WRAPPING_MINUS, POINTER_STAR, UNARY_STAR, BINARY_STAR, UNARY_DOUBLE_STAR, BINARY_DOUBLE_STAR, BLOCK_AMPERSAND, BINARY_AMPERSAND, BEGINLESS_RANGE_OPERATOR, REGEX_START, BINARY_SLASH, BINARY_DOUBLE_SLASH, REGULAR_IF_KEYWORD, MODIFIER_IF_KEYWORD, REGULAR_UNLESS_KEYWORD, MODIFIER_UNLESS_KEYWORD, REGULAR_RESCUE_KEYWORD, MODIFIER_RESCUE_KEYWORD, REGULAR_ENSURE_KEYWORD, MODIFIER_ENSURE_KEYWORD, MODULO_OPERATOR, START_OF_SYMBOL, UNQUOTED_SYMBOL_CONTENT, TYPE_FIELD_COLON, STRING_LITERAL_START, DELIMITED_STRING_CONTENTS, STRING_LITERAL_END, COMMAND_LITERAL_START, COMMAND_LITERAL_END, STRING_PERCENT_LITERAL_START, COMMAND_PERCENT_LITERAL_START, STRING_ARRAY_PERCENT_LITERAL_START, SYMBOL_ARRAY_PERCENT_LITERAL_START, REGEX_PERCENT_LITERAL_START, PERCENT_LITERAL_END, DELIMITED_ARRAY_ELEMENT_START, DELIMITED_ARRAY_ELEMENT_END, HEREDOC_START, HEREDOC_BODY_START, HEREDOC_CONTENT, HEREDOC_END, REGEX_MODIFIER, MACRO_START, MACRO_DELIMITER_END, MACRO_DELIMITER_ELSE, MACRO_DELIMITER_ELSIF, MACRO_CONTENT, MACRO_CONTENT_NESTING, // Never returned START_OF_PARENLESS_ARGS, END_OF_RANGE, START_OF_MACRO_VAR_EXPS, // Only used when error recovery mode is active ERROR_RECOVERY, NONE, }; typedef enum Token Token; /* * Helpful macros */ // NOTE(margret): This is temporarily disabled as calling lexer->log // causes a seg fault with WASM builds. Enable locally when needed. #define DEBUG(...) // lexer->log(lexer, "[LOG] " __VA_ARGS__); /* * State types */ enum LiteralTypeEnum { STRING, STRING_NO_ESCAPE, COMMAND, STRING_ARRAY, SYMBOL_ARRAY, REGEX, }; typedef uint8_t LiteralType; struct PercentLiteral { // We compare these chars with int32_t codepoints, but all valid delimiters // in Crystal are in the ASCII range, so we won't overflow. uint8_t opening_char; uint8_t closing_char; uint8_t nesting_level; LiteralType type; }; typedef struct PercentLiteral PercentLiteral; struct Heredoc { bool allow_escapes; bool started; // Heredoc identifier encoded with UTF-8 Array(uint8_t) identifier; }; typedef struct Heredoc Heredoc; struct MacroState { // Set to true if the macro scan is currently in a comment (the rest of the line after #). // Defaults to false. bool in_comment; // Tracks if the regular (not modifier) versions of `if` and `unless` may // be scanned in the macro text. (Also affects `while` and `until`, because // they used to be modifier keywords too.) bool non_modifier_keyword_can_begin; // TODO: heredocs? }; typedef struct MacroState MacroState; #define MAX_LITERAL_COUNT 16 #define MAX_HEREDOC_COUNT 16 // The maximum number of bytes that can be stored in the state, across all heredocs #define HEREDOC_BUFFER_SIZE 512 // We only use a single byte to store the size #define MAX_HEREDOC_WORD_SIZE 255 struct State { bool has_leading_whitespace; bool previous_line_continued; MacroState macro_state; // It's possible to have nested delimited literals, like // %(#{%(foo)}) // We can handle up to MAX_LITERAL_COUNT levels of nesting. Array(PercentLiteral) literals; Array(Heredoc) heredocs; }; typedef struct State State; /* * State-related macros and functions */ #define HAS_ACTIVE_LITERAL(state) \ (state->literals.size > 0) #define ACTIVE_LITERAL(state) \ array_back(&state->literals) #define PUSH_LITERAL(state, literal) \ array_push(&state->literals, literal) #define POP_LITERAL(state) \ array_pop(&state->literals) // Return true if any heredoc is started static bool has_active_heredoc(State *state) { for (uint8_t i = 0; i < state->heredocs.size; i++) { if (array_get(&state->heredocs, i)->started) { return true; } } return false; } // Return true if the first heredoc on the queue is unstarted static bool has_unstarted_heredoc(State *state) { if (state->heredocs.size == 0) { return false; } return !array_front(&state->heredocs)->started; } // Return the number of bytes currently stored across all heredocs static size_t heredoc_current_buffer_size(State *state) { size_t bytes = 0; for (uint8_t i = 0; i < state->heredocs.size; i++) { bytes += array_get(&state->heredocs, i)->identifier.size; } return bytes; } // Pop the active heredoc off the queue static void pop_heredoc(State *state) { assert(state->heredocs.size > 0); Heredoc *popped = array_front(&state->heredocs); assert(popped->started); array_delete(&popped->identifier); array_erase(&state->heredocs, 0); } // Return true if state has room to track another heredoc static bool has_room_for_heredoc(State *state, Heredoc heredoc) { if (state->heredocs.size >= MAX_HEREDOC_COUNT) { return false; } size_t current_bytes = heredoc_current_buffer_size(state); return (current_bytes + heredoc.identifier.size) <= HEREDOC_BUFFER_SIZE; } // Push a heredoc onto the end of the state queue. If there's already an active // heredoc, the new heredoc must be nested, so it is added to the queue before // the active heredoc. static void push_heredoc(State *state, Heredoc heredoc) { assert(state->heredocs.size < MAX_HEREDOC_COUNT); if (has_active_heredoc(state)) { // This must be a nested heredoc, so insert it before the currently-active heredoc size_t index; for (index = 0; index < state->heredocs.size; index++) { if (array_get(&state->heredocs, index)->started) { break; } } assert(index < state->heredocs.size); array_insert(&state->heredocs, index, heredoc); } else { array_push(&state->heredocs, heredoc); } } enum LookaheadResult { LOOKAHEAD_UNKNOWN = 0, LOOKAHEAD_TYPE, LOOKAHEAD_NAMED_TUPLE, }; typedef enum LookaheadResult LookaheadResult; enum ScanResult { // Keep scanning to match a different external token SR_CONTINUE, // Stop scanning and return content SR_STOP, // Stop scanning and don't return content (we expect non-external tokens to match) SR_STOP_NO_CONTENT, }; typedef enum ScanResult ScanResult; // Reset the macro state to its defaults static void reset_macro_state(State *state) { state->macro_state.in_comment = false; state->macro_state.non_modifier_keyword_can_begin = true; } // Skip one character, which will not be included in the token emitted by the scanner. // WARNING: this will set the _start_ of the token range. Don't use this after mark_end! static void lex_skip(State *state, TSLexer *lexer) { state->has_leading_whitespace = true; lexer->advance(lexer, true); } // NOTE: apparently this can't be called `advance` because it conflicts with a // symbol in glibc static void lex_advance(TSLexer *lexer) { lexer->advance(lexer, false); } static bool next_char_is_identifier(TSLexer *lexer) { int32_t lookahead = lexer->lookahead; return iswalnum(lookahead) || lookahead == '_' || lookahead == '?' || lookahead == '!' || lookahead >= 0xa0; } static bool is_ident_part(int32_t codepoint) { // identifier token characters are in the range [0-9A-Za-z_\u{00a0}-\u{10ffff}] // (except for the first and last character) return ('0' <= codepoint && codepoint <= '9') || ('A' <= codepoint && codepoint <= 'Z') || ('a' <= codepoint && codepoint <= 'z') || (codepoint == '_') || (0x00a0 <= codepoint && codepoint <= 0x10ffffff); } // Usually scan_whitespace will handle starting heredocs, but it won't be called if a heredoc is // already active. This function is called before heredoc contents or whitespace is scanned, so it // can handle the start of a nested heredoc. static bool check_for_heredoc_start(State *state, TSLexer *lexer, const bool *valid_symbols) { // Note: calling get_column(lexer) at EOF seems to cause loops, so make sure EOF is checked first if (valid_symbols[HEREDOC_BODY_START] && has_unstarted_heredoc(state) && !state->previous_line_continued && !lexer->eof(lexer) && lexer->get_column(lexer) == 0) { assert(state->heredocs.size > 0); assert(!array_front(&state->heredocs)->started); array_front(&state->heredocs)->started = true; lexer->result_symbol = HEREDOC_BODY_START; return true; } return false; } static bool scan_whitespace(State *state, TSLexer *lexer, const bool *valid_symbols) { bool crossed_newline = false; for (;;) { switch (lexer->lookahead) { case ' ': case '\t': case '\r': lex_skip(state, lexer); break; case '\n': if (valid_symbols[HEREDOC_BODY_START] && has_unstarted_heredoc(state)) { assert(state->heredocs.size > 0); Heredoc *heredoc = array_front(&state->heredocs); assert(!heredoc->started); heredoc->started = true; // HEREDOC_BODY_START is a zero-width token. Use skip instead // of advance because we don't want to include the newline. lex_skip(state, lexer); lexer->result_symbol = HEREDOC_BODY_START; return true; } else if (valid_symbols[LINE_BREAK] && !crossed_newline) { lex_advance(lexer); lexer->mark_end(lexer); crossed_newline = true; state->has_leading_whitespace = true; } else { lex_skip(state, lexer); } break; case '\v': case '\f': // In regular code, these characters are not allowed. But they // may be used in between strings in a %w array. if (HAS_ACTIVE_LITERAL(state)) { lex_skip(state, lexer); break; } return false; default: if (crossed_newline) { if (lexer->lookahead == '.') { // Check if this is the continuation of a method call, // or the start of a beginless range literal. lex_advance(lexer); if (lexer->lookahead == '.') { lexer->result_symbol = LINE_BREAK; } } else if (lexer->lookahead == '#') { // Comments don't interrupt line continuations } else if (lexer->lookahead == ':' && valid_symbols[TYPE_FIELD_COLON]) { // Check for a type field separator that comes after a newline, e.g. // ( params ) // : ReturnType lex_advance(lexer); if (iswspace(lexer->lookahead)) { lex_advance(lexer); lexer->mark_end(lexer); lexer->result_symbol = TYPE_FIELD_COLON; } else { lexer->result_symbol = LINE_BREAK; } } else { lexer->result_symbol = LINE_BREAK; } } return true; } } } // Returns true if a string content token is found static ScanResult scan_string_contents(State *state, TSLexer *lexer, const bool *valid_symbols) { bool found_content = false; LiteralType active_type; // This may be overridden later. lexer->result_symbol = DELIMITED_STRING_CONTENTS; for (;;) { if (lexer->eof(lexer)) { DEBUG("reached EOF"); if (found_content) { return SR_STOP; } else { return SR_STOP_NO_CONTENT; } } active_type = ACTIVE_LITERAL(state)->type; switch (lexer->lookahead) { case '\\': switch (active_type) { case STRING: case COMMAND: if (found_content) { return SR_STOP; } else { // do the regular check for LINE_CONTINUATION return SR_CONTINUE; } case REGEX: // No special regex escapes lex_advance(lexer); break; case STRING_NO_ESCAPE: break; case STRING_ARRAY: case SYMBOL_ARRAY: // %w and %i allow only '\' or the closing // delimiter as an escape sequence, so we have to look // ahead one character. lexer->mark_end(lexer); lex_advance(lexer); if (iswspace(lexer->lookahead) || ACTIVE_LITERAL(state)->closing_char == lexer->lookahead) { if (found_content) { return SR_STOP; } else { return SR_STOP_NO_CONTENT; } } // The backslash must be part of the word contents. found_content = true; lexer->mark_end(lexer); continue; } break; case '#': if (active_type == STRING_NO_ESCAPE || active_type == STRING_ARRAY || active_type == SYMBOL_ARRAY) { // These types don't allow interpolation break; } lexer->mark_end(lexer); lex_advance(lexer); if (lexer->lookahead == '{') { if (found_content) { return SR_STOP; } else { return SR_STOP_NO_CONTENT; } } found_content = true; lexer->mark_end(lexer); continue; case ' ': case '\t': case '\n': case '\r': case '\v': case '\f': if (active_type == STRING_ARRAY || active_type == SYMBOL_ARRAY) { assert(found_content || valid_symbols[DELIMITED_ARRAY_ELEMENT_END]); if (found_content) { // We've already found string contents, return that. return SR_STOP; } else if (valid_symbols[DELIMITED_ARRAY_ELEMENT_END]) { // We've reached the end of an array word. lexer->result_symbol = DELIMITED_ARRAY_ELEMENT_END; return SR_STOP; } } break; case '"': case '|': case '`': // These delimiters can't nest if (ACTIVE_LITERAL(state)->closing_char == lexer->lookahead) { if (found_content) { // We've already found string contents, return that. return SR_STOP; } else if (valid_symbols[DELIMITED_ARRAY_ELEMENT_END]) { // We've reached the end of an array word. lexer->result_symbol = DELIMITED_ARRAY_ELEMENT_END; return SR_STOP; } else { // The check for PERCENT_LITERAL_END comes later. return SR_CONTINUE; } } break; case '(': case '[': case '{': case '<': if (ACTIVE_LITERAL(state)->opening_char == lexer->lookahead) { ACTIVE_LITERAL(state)->nesting_level++; } break; case ')': case ']': case '}': case '>': if (ACTIVE_LITERAL(state)->closing_char == lexer->lookahead) { if (ACTIVE_LITERAL(state)->nesting_level == 0) { if (found_content) { // We've already found string contents, return that. return SR_STOP; } else if (valid_symbols[DELIMITED_ARRAY_ELEMENT_END]) { // We've reached the end of an array word. lexer->result_symbol = DELIMITED_ARRAY_ELEMENT_END; return SR_STOP; } else { // The check for PERCENT_LITERAL_END comes later. return SR_CONTINUE; } } ACTIVE_LITERAL(state)->nesting_level--; } break; } lex_advance(lexer); lexer->mark_end(lexer); found_content = true; } } // Scan for heredoc contents, and return true if the body or end tag of the // current heredoc is matched. This function will scan across multiple lines, // so one heredoc_content node will contain as many characters as possible. static bool scan_heredoc_contents(State *state, TSLexer *lexer, const bool *valid_symbols) { if (valid_symbols[ERROR_RECOVERY] && !has_active_heredoc(state)) { return false; } assert(state->heredocs.size > 0); assert(has_active_heredoc(state)); bool found_content = false; Heredoc *active_heredoc = array_front(&state->heredocs); bool heredoc_pending_start; if (active_heredoc->started) { heredoc_pending_start = false; } else { // The first heredoc in the queue isn't started, which means it's a // pending nested heredoc that will begin on the next line. heredoc_pending_start = true; for (uint8_t i = 1; i < state->heredocs.size; i++) { if (array_get(&state->heredocs, i)->started) { active_heredoc = array_get(&state->heredocs, i); break; } } } for (;;) { start_of_line: if (found_content && heredoc_pending_start) { // We matched the remaining heredoc_content on a previous line after // the start of a nested heredoc. Now we return that content, and // the next call to the scanner will trigger check_for_heredoc_start. return true; } if (valid_symbols[HEREDOC_END] && !lexer->eof(lexer) && lexer->get_column(lexer) == 0) { if (found_content) { lexer->mark_end(lexer); while (lexer->lookahead == '\t' || lexer->lookahead == ' ') { lex_advance(lexer); } } else { while (lexer->lookahead == '\t' || lexer->lookahead == ' ') { lex_skip(state, lexer); } lexer->mark_end(lexer); } size_t byte_size = active_heredoc->identifier.size; size_t matched_codepoint_count; // Load all the expected codepoints at once. Theoretically this is less efficient than // checking one codepoint at a time, but the actual performance impact is minimal. int32_t codepoints[MAX_HEREDOC_WORD_SIZE]; size_t codepoint_count = utf8_to_codepoints(codepoints, active_heredoc->identifier.contents, byte_size); for (matched_codepoint_count = 0; matched_codepoint_count < codepoint_count; matched_codepoint_count++) { int32_t expected_codepoint = codepoints[matched_codepoint_count]; if (lexer->lookahead == expected_codepoint) { lex_advance(lexer); } else { break; } } bool end_of_line = (lexer->lookahead == '\n' || lexer->lookahead == '\r' || lexer->eof(lexer)); if ((matched_codepoint_count == codepoint_count) && end_of_line) { if (found_content) { // We already scanned content on a previous line, which must be // returned. The next call to the scanner will match this heredoc word // again, and return it. return true; } else { pop_heredoc(state); lexer->mark_end(lexer); lexer->result_symbol = HEREDOC_END; return true; } } if (matched_codepoint_count > 0) { // lex_advance was called at least once while scanning for the heredoc // word, make sure those characters are counted as content. found_content = true; lexer->mark_end(lexer); } } // We found either a partial or no match for the heredoc identifier, so scan for string contents lexer->result_symbol = HEREDOC_CONTENT; for (;;) { if (lexer->eof(lexer)) { DEBUG("reached EOF"); return found_content; } switch (lexer->lookahead) { case '\\': if (active_heredoc->allow_escapes) { return found_content; } break; case '#': if (!active_heredoc->allow_escapes) { break; } lexer->mark_end(lexer); lex_advance(lexer); if (lexer->lookahead == '{') { return found_content; } found_content = true; lexer->mark_end(lexer); continue; case '\r': lex_advance(lexer); lexer->mark_end(lexer); found_content = true; if (lexer->lookahead != '\n') { continue; } // fall through case '\n': lex_advance(lexer); lexer->mark_end(lexer); found_content = true; goto start_of_line; } lex_advance(lexer); lexer->mark_end(lexer); found_content = true; } } } // Check if a given keyword matches at the current location. // // Returns true if the keyword matches exactly. // Returns false if: // - a different identifier is matched (shorter or longer) // - the keyword ends in /[:?!]/ // // Will consume the entire identifier even if there's only a partial match. static bool match_macro_keyword(TSLexer *lexer, const char keyword[]) { size_t keyword_size = strlen(keyword); bool found_match = true; for (size_t i = 0; i < keyword_size; i++) { if (lexer->lookahead != (int32_t)keyword[i]) { found_match = false; break; } lex_advance(lexer); } if (lexer->lookahead == ':') { // Looks like a tuple keyword return false; } if (!found_match || next_char_is_identifier(lexer)) { // consume the rest of the identifier, so e.g. `beginbegin` doesn't get split in the middle // and then match on the next loop while (is_ident_part(lexer->lookahead)) { lex_advance(lexer); } if (next_char_is_identifier(lexer)) { lex_advance(lexer); } return false; } return true; } // Scan for macro literal content, which is treated as text instead of being parsed normally. // This function is modeled after Crystal::Lexer#next_macro_token. To simplify the implementation // here, we use grammar rules to model the nesting of different keywords. // // When scanning directly inside a `macro` definition, the MACRO_CONTENT_NESTING symbol is valid, // and nesting keywords are treated as a boundary. When an `end` is reached, the scanner stops // scanning. We rely on the grammar rules to decide if `end` terminates the macro, or just reduces // the nesting level. // // When scanning inside other macro expressions like `{%begin%}`/`{%end%}`, the MACRO_CONTENT // symbol is valid. We don't care about matching `end` or other keywords. // // This function may end up scanning some characters multiple times. First, it will scan until it // finds a nesting keyword like `begin`. It will return SR_STOP if any macro literal content has // been consumed so far, so the overall scan has a result of MACRO_CONTENT_NESTING. Then the // external scanner is triggered again, starting exactly at the beginning of the keyword. The // keyword is matched again, and this function returns SR_STOP_NO_CONTENT. The overall scan will // not return a token on the second scan, so the grammar rule for the `begin` // keyword matches instead. static ScanResult scan_macro_contents(State *state, TSLexer *lexer, const bool *valid_symbols) { // Set to true if any content has been scanned with advance. This signals // the overall scan will return MACRO_CONTENT or MACRO_CONTENT_NESTING. bool found_content = false; // Set to true if the scan is looking for nesting keywords. bool nesting = false; // Set to true if a nesting keyword may begin at this point in the scan. bool keyword_can_begin = true; // NOTE: See also the comments for MacroState.in_comment and // MacroState.non_modifier_keyword_can_begin lexer->result_symbol = MACRO_CONTENT; if (valid_symbols[MACRO_CONTENT_NESTING]) { assert(!valid_symbols[MACRO_CONTENT] || valid_symbols[ERROR_RECOVERY]); nesting = true; lexer->result_symbol = MACRO_CONTENT_NESTING; } #define RETURN_NESTING_CONTENT \ if (nesting && !state->macro_state.in_comment && keyword_can_begin) { \ if (found_content) { \ return SR_STOP; \ } else { \ return SR_STOP_NO_CONTENT; \ } \ } #define RETURN_CONTENT \ if (found_content) { \ return SR_STOP; \ } else { \ return SR_STOP_NO_CONTENT; \ } for (;;) { if (lexer->eof(lexer)) { DEBUG("reached EOF"); RETURN_CONTENT; } // keywords that decrease nesting: // end // keywords that don't change nesting: // abstract def // keywords that increase nesting: // abstract class // abstract struct // annotation // begin // case // class // do // def // enum // fun // lib // macro // module // select // struct // union // keywords that increase nesting only at the beginning of line // if // unless // until // while switch (lexer->lookahead) { case '{': lexer->mark_end(lexer); // In a state like // %macro_var{foo} // ^ // let the grammar handle the rest of the macro var expressions if (valid_symbols[START_OF_MACRO_VAR_EXPS] && !found_content) { return SR_STOP_NO_CONTENT; } lex_advance(lexer); if (lexer->lookahead == '{' || lexer->lookahead == '%') { // This is the start of a macro expression. After the macro expression ends, // if/unless is a modifier. state->macro_state.non_modifier_keyword_can_begin = false; if (found_content) { return SR_STOP; } lex_advance(lexer); lexer->mark_end(lexer); // Check if next token is a special macro keyword. If we detect end/else/elsif, // return the corresponding MACRO_DELIMITER_* token for the initial `{%`. while (iswspace(lexer->lookahead)) { lex_advance(lexer); } if (lexer->lookahead == 'e') { lex_advance(lexer); if (lexer->lookahead == 'n') { if (match_macro_keyword(lexer, "nd")) { lexer->result_symbol = MACRO_DELIMITER_END; return SR_STOP; } } else if (lexer->lookahead == 'l') { lex_advance(lexer); if (lexer->lookahead == 's') { lex_advance(lexer); if (lexer->lookahead == 'e') { if (match_macro_keyword(lexer, "e")) { lexer->result_symbol = MACRO_DELIMITER_ELSE; return SR_STOP; } } else if (lexer->lookahead == 'i') { if (match_macro_keyword(lexer, "if")) { lexer->result_symbol = MACRO_DELIMITER_ELSIF; return SR_STOP; } } } } } return SR_STOP_NO_CONTENT; } // This is the start of a tuple, brace block, etc. found_content = true; keyword_can_begin = true; state->macro_state.non_modifier_keyword_can_begin = true; lexer->mark_end(lexer); continue; case '}': lexer->mark_end(lexer); lex_advance(lexer); // This might be the end of a block, hash/tuple, or macro expression. In any case, // if/unless is a modifier after this point. state->macro_state.non_modifier_keyword_can_begin = false; if (lexer->lookahead == '}') { RETURN_CONTENT; } // This is the end for a tuple, brace block, etc. Non-modifier keywords are valid. found_content = true; keyword_can_begin = true; lexer->mark_end(lexer); continue; case '%': lexer->mark_end(lexer); lex_advance(lexer); // This might be the end of a macro expression, or a macro variable, or a modulo // operator. In any case, if/unless is a modifier after this point. state->macro_state.non_modifier_keyword_can_begin = false; if (lexer->lookahead == '}') { RETURN_CONTENT; } else if (lexer->lookahead == 'i' || lexer->lookahead == 'q' || lexer->lookahead == 'Q' || lexer->lookahead == 'r' || lexer->lookahead == 'w' || lexer->lookahead == 'x') { lex_advance(lexer); if (lexer->lookahead == '(' || lexer->lookahead == '<' || lexer->lookahead == '[' || lexer->lookahead == '{' || lexer->lookahead == '|') { // TODO eventually we'll return here to mark this as a delimiter // For now, just continue lex_advance(lexer); } } else if (is_ident_part(lexer->lookahead)) { // This is a macro var RETURN_CONTENT; } // Keywords are not valid here: `%begin` is a macro variable found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case '"': // Whether we are delegating to string rules or not, if/unless is a // modifier after this point. state->macro_state.non_modifier_keyword_can_begin = false; if (valid_symbols[STRING_LITERAL_START]) { // Delegate to string rules if (found_content) { return SR_STOP; } else { return SR_CONTINUE; } } lex_advance(lexer); lexer->mark_end(lexer); // Probably inside a string in a non-nesting context, so keywords aren't valid found_content = true; keyword_can_begin = false; continue; case '#': lex_advance(lexer); lexer->mark_end(lexer); // Mark the rest of the line as a comment, where nesting keywords don't apply state->macro_state.in_comment = true; found_content = true; keyword_can_begin = false; state->macro_state.non_modifier_keyword_can_begin = false; continue; case 'a': lexer->mark_end(lexer); lex_advance(lexer); // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; if (lexer->lookahead == 'b') { if (match_macro_keyword(lexer, "bstract")) { if (iswspace(lexer->lookahead)) { lex_advance(lexer); switch (lexer->lookahead) { case 'c': if (match_macro_keyword(lexer, "class")) { RETURN_NESTING_CONTENT; } break; case 's': if (match_macro_keyword(lexer, "struct")) { RETURN_NESTING_CONTENT; } break; case 'd': // fully consume "abstract def", which doesn't increase the nesting level match_macro_keyword(lexer, "def"); } } } } else if (lexer->lookahead == 'n') { if (match_macro_keyword(lexer, "nnotation")) { RETURN_NESTING_CONTENT; } } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 'b': lexer->mark_end(lexer); // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; if (match_macro_keyword(lexer, "begin")) { RETURN_NESTING_CONTENT; } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 'c': lexer->mark_end(lexer); lex_advance(lexer); // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; if (lexer->lookahead == 'l') { if (match_macro_keyword(lexer, "lass")) { RETURN_NESTING_CONTENT; } } else if (lexer->lookahead == 'a') { if (match_macro_keyword(lexer, "ase")) { RETURN_NESTING_CONTENT; } } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 'd': lexer->mark_end(lexer); lex_advance(lexer); // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; if (lexer->lookahead == 'e') { if (match_macro_keyword(lexer, "ef")) { RETURN_NESTING_CONTENT; } } else if (lexer->lookahead == 'o') { if (match_macro_keyword(lexer, "o")) { RETURN_NESTING_CONTENT; } } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 'e': lexer->mark_end(lexer); lex_advance(lexer); // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; if (lexer->lookahead == 'n') { lex_advance(lexer); if (lexer->lookahead == 'd') { if (match_macro_keyword(lexer, "d")) { RETURN_NESTING_CONTENT; } } else if (lexer->lookahead == 'u') { if (match_macro_keyword(lexer, "um")) { RETURN_NESTING_CONTENT; } } } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 'f': lexer->mark_end(lexer); // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; if (match_macro_keyword(lexer, "fun")) { RETURN_NESTING_CONTENT; } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 'i': lexer->mark_end(lexer); if (state->macro_state.non_modifier_keyword_can_begin) { if (match_macro_keyword(lexer, "if")) { if (nesting && !state->macro_state.in_comment && keyword_can_begin) { if (found_content) { // Don't set non_modifier_keyword_can_begin yet, the scan is going // to re-enter at this point. return SR_STOP; } else { state->macro_state.non_modifier_keyword_can_begin = false; return SR_STOP_NO_CONTENT; } } } else { // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; } } else { lex_advance(lexer); } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 'l': lexer->mark_end(lexer); // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; if (match_macro_keyword(lexer, "lib")) { RETURN_NESTING_CONTENT; } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 'm': lexer->mark_end(lexer); lex_advance(lexer); // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; if (lexer->lookahead == 'a') { if (match_macro_keyword(lexer, "acro")) { RETURN_NESTING_CONTENT; } } else if (lexer->lookahead == 'o') { if (match_macro_keyword(lexer, "odule")) { RETURN_NESTING_CONTENT; } } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 's': lexer->mark_end(lexer); lex_advance(lexer); // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; if (lexer->lookahead == 'e') { if (match_macro_keyword(lexer, "elect")) { RETURN_NESTING_CONTENT; } } else if (lexer->lookahead == 't') { if (match_macro_keyword(lexer, "truct")) { RETURN_NESTING_CONTENT; } } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 'u': lexer->mark_end(lexer); lex_advance(lexer); if (lexer->lookahead == 'n') { lex_advance(lexer); if (lexer->lookahead == 'i') { // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; if (match_macro_keyword(lexer, "ion")) { RETURN_NESTING_CONTENT; } } else if (lexer->lookahead == 'l') { if (state->macro_state.non_modifier_keyword_can_begin && match_macro_keyword(lexer, "less")) { if (nesting && !state->macro_state.in_comment && keyword_can_begin) { if (found_content) { // Don't set non_modifier_keyword_can_begin yet, the scan is going // to re-enter at this point. return SR_STOP; } else { state->macro_state.non_modifier_keyword_can_begin = false; return SR_STOP_NO_CONTENT; } } } } else if (lexer->lookahead == 't') { if (state->macro_state.non_modifier_keyword_can_begin && match_macro_keyword(lexer, "til")) { if (nesting && !state->macro_state.in_comment && keyword_can_begin) { if (found_content) { // Don't set non_modifier_keyword_can_begin yet, the scan is going // to re-enter at this point. return SR_STOP; } else { state->macro_state.non_modifier_keyword_can_begin = false; return SR_STOP_NO_CONTENT; } } } } else { // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; } } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case 'w': lexer->mark_end(lexer); if (state->macro_state.non_modifier_keyword_can_begin) { if (match_macro_keyword(lexer, "while")) { if (nesting && !state->macro_state.in_comment && keyword_can_begin) { if (found_content) { // Don't set non_modifier_keyword_can_begin yet, the scan is going // to re-enter at this point. return SR_STOP; } else { state->macro_state.non_modifier_keyword_can_begin = false; return SR_STOP_NO_CONTENT; } } } else { // Whatever identifier this is, if/unless is a modifier after this point state->macro_state.non_modifier_keyword_can_begin = false; } } else { lex_advance(lexer); } // Keywords are not valid immediately after an identifier found_content = true; keyword_can_begin = false; lexer->mark_end(lexer); continue; case '\n': lex_advance(lexer); lexer->mark_end(lexer); found_content = true; // We've reached the end of the line, no more comment state->macro_state.in_comment = false; // Keywords are always valid on a new line keyword_can_begin = true; state->macro_state.non_modifier_keyword_can_begin = true; continue; case ' ': case '\t': case '\f': case '\v': case '\r': // These whitespace characters may be followed by a keyword lex_advance(lexer); lexer->mark_end(lexer); found_content = true; keyword_can_begin = true; continue; case '(': case '[': // These nesting characters may be followed by a keyword lex_advance(lexer); lexer->mark_end(lexer); found_content = true; keyword_can_begin = true; continue; case '=': lex_advance(lexer); lexer->mark_end(lexer); found_content = true; keyword_can_begin = false; if (iswspace(lexer->lookahead)) { // After `= `, if/unless is treated as a regular keyword, not modifier state->macro_state.non_modifier_keyword_can_begin = true; } continue; } lex_advance(lexer); lexer->mark_end(lexer); found_content = true; // We've scanned something that's not whitespace or an opening character. Most keywords // are not allowed until we reach whitespace, and if/unless is treated as a modifier // for the rest of the line. keyword_can_begin = false; state->macro_state.non_modifier_keyword_can_begin = false; } } static ScanResult scan_symbol_content(TSLexer *lexer) { int32_t lookahead = lexer->lookahead; if (('A' <= lookahead && lookahead <= 'Z') || ('a' <= lookahead && lookahead <= 'z') || (lookahead == '_') || (0x00a0 <= lookahead && lookahead <= 0x10ffffff)) { lexer->result_symbol = UNQUOTED_SYMBOL_CONTENT; lex_advance(lexer); while (is_ident_part(lexer->lookahead)) { lex_advance(lexer); } switch (lexer->lookahead) { case '?': // Symbols like `:foo?` always include the trailing character lex_advance(lexer); return SR_STOP; case '!': case '=': // Symbols like `:foo!` or `:bar=` include the trailing character, // only if the next char isn't also `=` lexer->mark_end(lexer); lex_advance(lexer); if (lexer->lookahead != '=') { lexer->mark_end(lexer); } return SR_STOP; default: return SR_STOP; } } return SR_CONTINUE; } static bool scan_regex_modifier(State *state, TSLexer *lexer) { if (!state->has_leading_whitespace) { bool found_modifier = false; for (;;) { switch (lexer->lookahead) { case 'i': case 'm': case 'x': found_modifier = true; lex_advance(lexer); continue; } if (found_modifier) { lexer->result_symbol = REGEX_MODIFIER; return true; } else { break; } } } return false; } // Advance while the next character is a space or tab static void advance_space(TSLexer *lexer) { while (lexer->lookahead == ' ' || lexer->lookahead == '\t') { lex_advance(lexer); } } // Advance while the next character is a space, tab, or newline static void advance_space_and_newline(TSLexer *lexer) { while (lexer->lookahead == ' ' || lexer->lookahead == '\t' || lexer->lookahead == '\r' || lexer->lookahead == '\n') { lex_advance(lexer); } } static void consume_const(TSLexer *lexer) { if ('A' <= lexer->lookahead && lexer->lookahead <= 'Z') { lex_advance(lexer); while (is_ident_part(lexer->lookahead)) { lex_advance(lexer); } } } static void consume_string_literal(TSLexer *lexer) { bool can_escape = true, can_nest; int32_t opening_char = 0, closing_char, nesting_level = 0; if (lexer->lookahead == '"') { opening_char = '"'; closing_char = '"'; can_nest = false; } else if (lexer->lookahead == '%') { lex_advance(lexer); if (lexer->lookahead == 'q') { can_escape = false; lex_advance(lexer); } else if (lexer->lookahead == 'Q') { lex_advance(lexer); } switch (lexer->lookahead) { case '{': opening_char = '{'; closing_char = '}'; can_nest = true; break; case '(': opening_char = '('; closing_char = ')'; can_nest = true; break; case '[': opening_char = '['; closing_char = ']'; can_nest = true; break; case '<': opening_char = '<'; closing_char = '>'; can_nest = true; break; case '|': opening_char = '|'; closing_char = '|'; can_nest = false; break; } } if (!opening_char) { // this isn't a string return; } // advance past opening char lex_advance(lexer); for (;;) { if (lexer->eof(lexer)) { return; } if (lexer->lookahead == '\\' && can_escape) { // consume the backslash and next character lex_advance(lexer); lex_advance(lexer); continue; } if (lexer->lookahead == closing_char) { lex_advance(lexer); if (nesting_level == 0) { // reached the end of the literal return; } assert(nesting_level > 0); nesting_level--; continue; } if (lexer->lookahead == opening_char && can_nest) { lex_advance(lexer); nesting_level++; continue; } lex_advance(lexer); } } // Check if there is a type suffix (e.g. `?` or `.class`) or a delimiter like `|` static LookaheadResult lookahead_delimiter_or_type_suffix(State *state, TSLexer *lexer) { if (lexer->eof(lexer)) { return true; } switch (lexer->lookahead) { case '.': lex_advance(lexer); advance_space_and_newline(lexer); if (lexer->lookahead != 'c') { return false; } lex_advance(lexer); if (lexer->lookahead != 'l') { return false; } lex_advance(lexer); if (lexer->lookahead != 'a') { return false; } lex_advance(lexer); if (lexer->lookahead != 's') { return false; } lex_advance(lexer); if (lexer->lookahead != 's') { return false; } lex_advance(lexer); if (is_ident_part(lexer->lookahead)) { // the keyword doesn't end after `class` return LOOKAHEAD_UNKNOWN; } else { // .class type return LOOKAHEAD_TYPE; } case '?': case '*': lex_advance(lexer); return lookahead_delimiter_or_type_suffix(state, lexer); case '-': lex_advance(lexer); if (lexer->lookahead == '>') { // Const -> is considered a type suffix return LOOKAHEAD_TYPE; } return LOOKAHEAD_UNKNOWN; case '=': lex_advance(lexer); switch (lexer->lookahead) { case '>': // Const => is considered a type suffix return LOOKAHEAD_TYPE; case '=': case '~': // other operators return LOOKAHEAD_UNKNOWN; default: // Const = is considered a type suffix return LOOKAHEAD_TYPE; } case '|': case ',': case ';': case '\n': case '(': case ')': case '[': case ']': // other type delimiters return LOOKAHEAD_TYPE; default: return LOOKAHEAD_UNKNOWN; } } // Check if there is an identifier followed by `:` indicating the start of a named tuple item static LookaheadResult lookahead_start_of_named_tuple_entry(TSLexer *lexer, bool started) { if (started || ('a' <= lexer->lookahead && lexer->lookahead <= 'z') || ('A' <= lexer->lookahead && lexer->lookahead <= 'Z') || (lexer->lookahead == '_') || (0x00a0 <= lexer->lookahead && lexer->lookahead <= 0x10ffffff)) { while (('0' <= lexer->lookahead && lexer->lookahead <= '9') || ('A' <= lexer->lookahead && lexer->lookahead <= 'Z') || ('a' <= lexer->lookahead && lexer->lookahead <= 'z') || (lexer->lookahead == '_') || (0x00a0 <= lexer->lookahead && lexer->lookahead <= 0x10ffffff)) { lex_advance(lexer); } if ((lexer->lookahead == '!') || (lexer->lookahead == '?')) { lex_advance(lexer); } if (lexer->lookahead == ':') { lex_advance(lexer); if (lexer->lookahead == ':') { return LOOKAHEAD_UNKNOWN; } return LOOKAHEAD_NAMED_TUPLE; } } if (lexer->lookahead == '"' || lexer->lookahead == '%') { consume_string_literal(lexer); if (lexer->lookahead == ':') { lex_advance(lexer); if (lexer->lookahead == ':') { return LOOKAHEAD_UNKNOWN; } return LOOKAHEAD_NAMED_TUPLE; } } return LOOKAHEAD_UNKNOWN; } // Check to see if the next token is part of the type grammar or not. Based on // https://github.com/crystal-lang/crystal/blob/cd2b7d6490301e092cecc22dfbc91d0f9553ba20/src/compiler/crystal/syntax/parser.cr#L5195 // As the compiler code notes, these conditions are not completely accurate in determining what // could or could not be a type. static LookaheadResult lookahead_start_of_type(State *state, TSLexer *lexer) { advance_space(lexer); if (lexer->eof(lexer)) { DEBUG("reached EOF"); return LOOKAHEAD_UNKNOWN; } while (lexer->lookahead == '{' || lexer->lookahead == '(') { lex_advance(lexer); advance_space_and_newline(lexer); } // Check for identifier if (lexer->lookahead == 't') { lex_advance(lexer); if (lexer->lookahead == 'y') { lex_advance(lexer); if (lexer->lookahead == 'p') { lex_advance(lexer); if (lexer->lookahead == 'e') { lex_advance(lexer); if (lexer->lookahead == 'o') { lex_advance(lexer); if (lexer->lookahead == 'f') { lex_advance(lexer); if (lexer->lookahead == ':') { // named tuple start return LOOKAHEAD_NAMED_TUPLE; } if (is_ident_part(lexer->lookahead) || (lexer->lookahead == '!') || (lexer->lookahead == '?')) { // identifier continues beyond `typeof` return lookahead_start_of_named_tuple_entry(lexer, true); } return LOOKAHEAD_TYPE; } } } } } } else if (lexer->lookahead == 's') { lex_advance(lexer); if (lexer->lookahead == 'e') { lex_advance(lexer); if (lexer->lookahead == 'l') { lex_advance(lexer); if (lexer->lookahead == 'f') { lex_advance(lexer); if (lexer->lookahead == ':') { // named tuple start return LOOKAHEAD_NAMED_TUPLE; } if (is_ident_part(lexer->lookahead) || (lexer->lookahead == '!')) { // identifier continues beyond `self` return lookahead_start_of_named_tuple_entry(lexer, true); } advance_space(lexer); return lookahead_delimiter_or_type_suffix(state, lexer); } } } } else if (('a' <= lexer->lookahead && lexer->lookahead <= 'z') || (0x00a0 <= lexer->lookahead && lexer->lookahead <= 0x10ffffff)) { // other identifiers are not part of the type grammar return lookahead_start_of_named_tuple_entry(lexer, false); } // Check for constant while ('A' <= lexer->lookahead && lexer->lookahead <= 'Z') { consume_const(lexer); if (lexer->lookahead == ':') { lex_advance(lexer); if (lexer->lookahead == ':') { lex_advance(lexer); advance_space_and_newline(lexer); // continue consuming const segments } else { // named tuple start return LOOKAHEAD_NAMED_TUPLE; } } else { advance_space(lexer); return lookahead_delimiter_or_type_suffix(state, lexer); } } switch (lexer->lookahead) { case '_': lex_advance(lexer); if (!iswalnum(lexer->lookahead)) { // This is just a plain underscore return LOOKAHEAD_TYPE; } break; case '-': lex_advance(lexer); if (lexer->lookahead == '>') { // proc type return LOOKAHEAD_TYPE; } break; case '*': lex_advance(lexer); advance_space_and_newline(lexer); if (lexer->lookahead == '*') { // double splat is not a valid type operator return LOOKAHEAD_UNKNOWN; } else { if (lookahead_start_of_type(state, lexer) == LOOKAHEAD_TYPE) { return LOOKAHEAD_TYPE; } else { // If lookahead_start_of_type returns LOOKAHEAD_NAMED_TUPLE // at this point, it's not valid, because named tuple tags // can't start with '*' return LOOKAHEAD_UNKNOWN; } } break; case '"': case '%': return lookahead_start_of_named_tuple_entry(lexer, false); } DEBUG("Not the start of a type"); return LOOKAHEAD_UNKNOWN; } static bool inner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { State *state = (State *)payload; state->has_leading_whitespace = false; if (check_for_heredoc_start(state, lexer, valid_symbols)) { return true; } // The previous_line_continued flag only matters for check_for_heredoc_start, // so it can now be cleared. if (state->previous_line_continued) { state->previous_line_continued = false; } if (valid_symbols[DELIMITED_STRING_CONTENTS] && HAS_ACTIVE_LITERAL(state)) { switch (scan_string_contents(state, lexer, valid_symbols)) { case SR_STOP: return true; case SR_STOP_NO_CONTENT: return false; case SR_CONTINUE: break; } } if (valid_symbols[HEREDOC_CONTENT] && state->heredocs.size > 0 && scan_heredoc_contents(state, lexer, valid_symbols)) { return true; } if (valid_symbols[MACRO_START] && !valid_symbols[ERROR_RECOVERY]) { reset_macro_state(state); lexer->result_symbol = MACRO_START; return true; } if ((valid_symbols[MACRO_CONTENT] || valid_symbols[MACRO_CONTENT_NESTING]) && !valid_symbols[ERROR_RECOVERY]) { switch (scan_macro_contents(state, lexer, valid_symbols)) { case SR_STOP: return true; case SR_STOP_NO_CONTENT: return false; case SR_CONTINUE: break; } } if (valid_symbols[UNQUOTED_SYMBOL_CONTENT] && !valid_symbols[ERROR_RECOVERY]) { switch (scan_symbol_content(lexer)) { case SR_STOP: return true; case SR_STOP_NO_CONTENT: return false; case SR_CONTINUE: break; } } lexer->result_symbol = NONE; if (!scan_whitespace(state, lexer, valid_symbols)) { return false; } if (lexer->result_symbol != NONE) { return true; } if (valid_symbols[PERCENT_LITERAL_END] && HAS_ACTIVE_LITERAL(state)) { if (lexer->lookahead == ACTIVE_LITERAL(state)->closing_char) { lex_advance(lexer); (void)POP_LITERAL(state); lexer->result_symbol = PERCENT_LITERAL_END; return true; } } if (valid_symbols[STRING_LITERAL_END] && HAS_ACTIVE_LITERAL(state)) { if (lexer->lookahead == ACTIVE_LITERAL(state)->closing_char) { lex_advance(lexer); (void)POP_LITERAL(state); lexer->result_symbol = STRING_LITERAL_END; return true; } } if (valid_symbols[COMMAND_LITERAL_END] && HAS_ACTIVE_LITERAL(state)) { if (lexer->lookahead == ACTIVE_LITERAL(state)->closing_char) { lex_advance(lexer); (void)POP_LITERAL(state); lexer->result_symbol = COMMAND_LITERAL_END; return true; } } if (valid_symbols[DELIMITED_ARRAY_ELEMENT_START] && HAS_ACTIVE_LITERAL(state)) { lexer->result_symbol = DELIMITED_ARRAY_ELEMENT_START; return true; } if (valid_symbols[REGEX_MODIFIER] && scan_regex_modifier(state, lexer)) { return true; } switch (lexer->lookahead) { case '{': lex_advance(lexer); // Start of a macro expression if (lexer->lookahead == '{' || lexer->lookahead == '%') { return false; } // We expect these symbols to always be valid or not valid together assert(valid_symbols[START_OF_TUPLE_TYPE] == valid_symbols[START_OF_NAMED_TUPLE_TYPE]); #define BRACE_BLOCK (valid_symbols[START_OF_BRACE_BLOCK]) #define BRACE_EXPR (valid_symbols[START_OF_HASH_OR_TUPLE] || valid_symbols[START_OF_NAMED_TUPLE]) #define BRACE_TYPE (valid_symbols[START_OF_TUPLE_TYPE] || valid_symbols[START_OF_NAMED_TUPLE_TYPE]) if (BRACE_BLOCK || BRACE_EXPR || BRACE_TYPE) { if (BRACE_BLOCK && BRACE_EXPR && BRACE_TYPE) { if (valid_symbols[ERROR_RECOVERY]) { return false; } else { // Shouldn't reach here assert(!( valid_symbols[START_OF_BRACE_BLOCK] && (valid_symbols[START_OF_HASH_OR_TUPLE] || valid_symbols[START_OF_NAMED_TUPLE]) && (valid_symbols[START_OF_TUPLE_TYPE] || valid_symbols[START_OF_NAMED_TUPLE_TYPE]))); return false; } } else if (BRACE_BLOCK && BRACE_EXPR) { // In Crystal, the '{' token may be used as the start of a block, // or the start of another literal like a tuple. The language // resolves this potential ambiguity by requiring that the first // non-block argument to a method invoked without parentheses may // not start with a '{'. In other words, if you want to pass a // tuple as the first argument, the method call _must_ use // parentheses. // // This means, if we see a '{' and we're in a context where a block // could be valid, it must be the start of a block. if (valid_symbols[START_OF_PARENLESS_ARGS]) { lexer->result_symbol = START_OF_BRACE_BLOCK; return true; } // Another edge case here is after a range operator: // foo 1, 2, 3 .. { 4 } // Crystal always considers this as a hash or tuple, not a block if (valid_symbols[END_OF_RANGE]) { // We don't want to consume while looking ahead lexer->mark_end(lexer); advance_space_and_newline(lexer); // After a range operator, these symbols should only be valid together assert(valid_symbols[START_OF_NAMED_TUPLE] && valid_symbols[START_OF_HASH_OR_TUPLE]); switch (lookahead_start_of_named_tuple_entry(lexer, false)) { case LOOKAHEAD_NAMED_TUPLE: lexer->result_symbol = START_OF_NAMED_TUPLE; return true; default: lexer->result_symbol = START_OF_HASH_OR_TUPLE; return true; } } // This is the start of an array-like or hash-like constructor: // MyArray { 1, 2, 3 } // Named tuples are not accepted here. if (valid_symbols[START_OF_HASH_OR_TUPLE] && !valid_symbols[START_OF_NAMED_TUPLE]) { lexer->result_symbol = START_OF_HASH_OR_TUPLE; return true; } // Is there anywhere else '{' could represent either a block or a hash/tuple? assert(valid_symbols[END_OF_RANGE] || valid_symbols[START_OF_PARENLESS_ARGS]); return false; } else if (BRACE_BLOCK && BRACE_TYPE) { // We don't want to consume while looking ahead lexer->mark_end(lexer); // Use type lookahead to resolve conflict between start of block and start of tuple type // For example, as a tuple type: // -> : -> {->} { // # ... // } // Or as a block: // -> : -> { ; Proc(Nil).new{} } switch (lookahead_start_of_type(state, lexer)) { case LOOKAHEAD_TYPE: assert(valid_symbols[START_OF_TUPLE_TYPE]); lexer->result_symbol = START_OF_TUPLE_TYPE; return true; case LOOKAHEAD_NAMED_TUPLE: // When the Crystal parser is trying to resolve whether a token is part // of a type or not, anything that looks like the start of a named // tuple is assumed _not_ to be a type. lexer->result_symbol = START_OF_BRACE_BLOCK; return true; default: lexer->result_symbol = START_OF_BRACE_BLOCK; return true; } } else if (BRACE_EXPR && BRACE_TYPE) { // We don't want to consume while looking ahead lexer->mark_end(lexer); // Use type lookahead to resolve conflict between start of hash/tuple and start of tuple type // For example, as a tuple type: // def foo : ->{Char,Char}; ->{ {'a','b'} } end // Or as a hash: // def foo : ->{'a'=>'b'}; ->{ nil } end // When distinguishing between type and expression, these symbols should only be valid together assert(valid_symbols[START_OF_NAMED_TUPLE] && valid_symbols[START_OF_HASH_OR_TUPLE]); switch (lookahead_start_of_type(state, lexer)) { case LOOKAHEAD_TYPE: assert(valid_symbols[START_OF_TUPLE_TYPE]); lexer->result_symbol = START_OF_TUPLE_TYPE; return true; case LOOKAHEAD_NAMED_TUPLE: // When the Crystal parser is trying to resolve whether a token is part // of a type or not, anything that looks like the start of a named // tuple is assumed _not_ to be a type. assert(valid_symbols[START_OF_NAMED_TUPLE]); lexer->result_symbol = START_OF_NAMED_TUPLE; return true; default: assert(valid_symbols[START_OF_HASH_OR_TUPLE]); lexer->result_symbol = START_OF_HASH_OR_TUPLE; return true; } } else if (BRACE_EXPR) { // We don't want to consume while looking ahead lexer->mark_end(lexer); advance_space_and_newline(lexer); if (valid_symbols[START_OF_HASH_OR_TUPLE] && !valid_symbols[START_OF_NAMED_TUPLE]) { // This is possible with "array-like" or "hash-like" syntax: // Const { a => 1 } lexer->result_symbol = START_OF_HASH_OR_TUPLE; return true; } else if (valid_symbols[START_OF_NAMED_TUPLE] && !valid_symbols[START_OF_HASH_OR_TUPLE]) { // NOTE(keidax): I'm not sure if this can ever occur? lexer->result_symbol = START_OF_NAMED_TUPLE; return true; } switch (lookahead_start_of_named_tuple_entry(lexer, false)) { case LOOKAHEAD_NAMED_TUPLE: lexer->result_symbol = START_OF_NAMED_TUPLE; return true; default: lexer->result_symbol = START_OF_HASH_OR_TUPLE; return true; } } else if (BRACE_TYPE) { // We don't want to consume while looking ahead lexer->mark_end(lexer); advance_space_and_newline(lexer); switch (lookahead_start_of_named_tuple_entry(lexer, false)) { case LOOKAHEAD_NAMED_TUPLE: assert(valid_symbols[START_OF_NAMED_TUPLE_TYPE]); lexer->result_symbol = START_OF_NAMED_TUPLE_TYPE; return true; default: assert(valid_symbols[START_OF_TUPLE_TYPE]); lexer->result_symbol = START_OF_TUPLE_TYPE; return true; } } else if (BRACE_BLOCK) { lexer->result_symbol = START_OF_BRACE_BLOCK; return true; } else { assert(!"This should never be reached"); } } break; case '[': if (valid_symbols[START_OF_INDEX_OPERATOR]) { // If there's ambiguity whether '[' is the start of an index // access or an array literal, we assume it's an array if // there's leading whitespace and we're at the start of a // potential method call: // foo [1] // If there's no leading whitespace, or we know this isn't the // first parameter of a method call, then it must be an index // operator: // puts({42} [0]) if (state->has_leading_whitespace && valid_symbols[START_OF_PARENLESS_ARGS]) { return false; } else { lex_advance(lexer); lexer->result_symbol = START_OF_INDEX_OPERATOR; return true; } } break; case '<': if (valid_symbols[HEREDOC_START]) { lex_advance(lexer); if (lexer->lookahead == '<') { lex_advance(lexer); if (lexer->lookahead == '-') { lex_advance(lexer); bool quoted = false; bool got_end_quote = false; if (lexer->lookahead == '\'') { quoted = true; lex_advance(lexer); } // How much space is left for this heredoc identifier. size_t max_word_size = HEREDOC_BUFFER_SIZE - heredoc_current_buffer_size(state); if (max_word_size < 1) { return false; } if (max_word_size > MAX_HEREDOC_WORD_SIZE) { max_word_size = MAX_HEREDOC_WORD_SIZE; } uint8_t word[HEREDOC_BUFFER_SIZE + 4]; size_t word_length = 0; // First character must be valid in an identifier, even for a quoted heredoc if (is_ident_part(lexer->lookahead)) { size_t byte_size = codepoint_to_utf8(lexer->lookahead, word); if (byte_size == 0) { // We couldn't convert the codepoint return false; } word_length = byte_size; lex_advance(lexer); } else { return false; } while (word_length <= max_word_size) { if (lexer->lookahead == '\r' || lexer->lookahead == '\n' || lexer->eof(lexer)) { // Reached the end of the line break; } if (lexer->lookahead == '\'' && quoted) { // This must be the end of the identifier got_end_quote = true; lex_advance(lexer); break; } if (quoted || is_ident_part(lexer->lookahead)) { // Add to the identifier buffer size_t byte_size = codepoint_to_utf8(lexer->lookahead, word + word_length); if (byte_size == 0) { // We couldn't convert the codepoint return false; } word_length += byte_size; lex_advance(lexer); } else { // Not a valid identifier character break; } } if (word_length == 0) { // There wasn't a valid heredoc identifier return false; } else if ((word_length > max_word_size) || (word_length == max_word_size && is_ident_part(lexer->lookahead))) { // The heredoc identifier is too big to store in state. return false; } else if (quoted && !got_end_quote) { // Unterminated quoted heredoc return false; } else { // word contains a heredoc identifier we can store. Heredoc heredoc = { .allow_escapes = !quoted, .started = false, .identifier = array_new(), }; array_extend(&heredoc.identifier, word_length, &word); // double check we can safely store the new heredoc if (!has_room_for_heredoc(state, heredoc)) { array_delete(&heredoc.identifier); return false; } push_heredoc(state, heredoc); lexer->result_symbol = HEREDOC_START; DEBUG("heredoc size = %d", word_length); return true; } } } } break; case '+': if (valid_symbols[UNARY_PLUS] || valid_symbols[BINARY_PLUS]) { lex_advance(lexer); if (lexer->lookahead == '=') { return false; } // Give precedence to the unary operator if: // - there is space before but not after, e.g. // puts +foo // - we are just after a range operator, e.g. // -5 .. + foo bool unary_priority = (state->has_leading_whitespace && !iswspace(lexer->lookahead)) || valid_symbols[END_OF_RANGE]; if (valid_symbols[UNARY_PLUS] && unary_priority) { lexer->result_symbol = UNARY_PLUS; } else if (valid_symbols[BINARY_PLUS]) { lexer->result_symbol = BINARY_PLUS; } else { lexer->result_symbol = UNARY_PLUS; } return true; } break; case '-': if (valid_symbols[UNARY_MINUS] || valid_symbols[BINARY_MINUS]) { lex_advance(lexer); if (lexer->lookahead == '=' || lexer->lookahead == '>') { return false; } bool unary_priority = (state->has_leading_whitespace && !iswspace(lexer->lookahead)) || valid_symbols[END_OF_RANGE]; if (valid_symbols[UNARY_MINUS] && unary_priority) { lexer->result_symbol = UNARY_MINUS; } else if (valid_symbols[BINARY_MINUS]) { lexer->result_symbol = BINARY_MINUS; } else { lexer->result_symbol = UNARY_MINUS; } return true; } break; case '*': if (valid_symbols[POINTER_STAR] || valid_symbols[UNARY_STAR] || valid_symbols[BINARY_STAR] || valid_symbols[UNARY_DOUBLE_STAR] || valid_symbols[BINARY_DOUBLE_STAR]) { lex_advance(lexer); if (valid_symbols[POINTER_STAR] && !valid_symbols[ERROR_RECOVERY]) { lexer->result_symbol = POINTER_STAR; return true; } if (lexer->lookahead == '=') { return false; } if (lexer->lookahead == '*') { lex_advance(lexer); if (lexer->lookahead == '=') { return false; } bool unary_priority = state->has_leading_whitespace && !iswspace(lexer->lookahead); if (valid_symbols[UNARY_DOUBLE_STAR] && unary_priority) { lexer->result_symbol = UNARY_DOUBLE_STAR; return true; } else if (valid_symbols[BINARY_DOUBLE_STAR]) { lexer->result_symbol = BINARY_DOUBLE_STAR; return true; } else if (valid_symbols[UNARY_DOUBLE_STAR] && !iswspace(lexer->lookahead)) { // A splat _cannot_ have whitespace after the * lexer->result_symbol = UNARY_DOUBLE_STAR; return true; } return false; } bool unary_priority = state->has_leading_whitespace && !iswspace(lexer->lookahead); if (valid_symbols[UNARY_STAR] && unary_priority) { lexer->result_symbol = UNARY_STAR; return true; } else if (valid_symbols[BINARY_STAR]) { lexer->result_symbol = BINARY_STAR; return true; } else if (valid_symbols[UNARY_STAR] && !iswspace(lexer->lookahead)) { // A splat _cannot_ have whitespace after the * lexer->result_symbol = UNARY_STAR; return true; } } break; case '&': if ( valid_symbols[UNARY_WRAPPING_PLUS] || valid_symbols[UNARY_WRAPPING_MINUS] || valid_symbols[BINARY_WRAPPING_PLUS] || valid_symbols[BINARY_WRAPPING_MINUS] || valid_symbols[BLOCK_AMPERSAND] || valid_symbols[BINARY_AMPERSAND]) { lex_advance(lexer); lexer->mark_end(lexer); if (lexer->lookahead == '+') { lex_advance(lexer); if (lexer->lookahead == '=') { return false; } // The binary form of &+ is always preferred. E.g. // foo! &+bar // is still binary. if (valid_symbols[BINARY_WRAPPING_PLUS]) { lexer->mark_end(lexer); lexer->result_symbol = BINARY_WRAPPING_PLUS; return true; } else if (valid_symbols[UNARY_WRAPPING_PLUS]) { lexer->mark_end(lexer); lexer->result_symbol = UNARY_WRAPPING_PLUS; return true; } return false; } if (lexer->lookahead == '-') { lex_advance(lexer); if (lexer->lookahead == '=') { return false; } if (lexer->lookahead == '>') { // In the case of '&->', we always return just the '&', // so mark_end is _not_ called here. bool unary_priority = state->has_leading_whitespace; if (unary_priority && valid_symbols[BLOCK_AMPERSAND]) { lexer->result_symbol = BLOCK_AMPERSAND; return true; } else if (valid_symbols[BINARY_AMPERSAND]) { lexer->result_symbol = BINARY_AMPERSAND; return true; } else if (valid_symbols[BLOCK_AMPERSAND]) { lexer->result_symbol = BLOCK_AMPERSAND; return true; } else { return false; } } if (valid_symbols[BINARY_WRAPPING_MINUS]) { lexer->mark_end(lexer); lexer->result_symbol = BINARY_WRAPPING_MINUS; return true; } else if (valid_symbols[UNARY_WRAPPING_MINUS]) { lexer->mark_end(lexer); lexer->result_symbol = UNARY_WRAPPING_MINUS; return true; } return false; } if (lexer->lookahead == '*' || lexer->lookahead == '&' || lexer->lookahead == '=') { // Symbols not managed by this external scanner: // '&*', '&**', '&&', '&=' return false; } if (lexer->lookahead == '.') { // '&.' is always treated as a block ampersand. if (valid_symbols[BLOCK_AMPERSAND]) { lexer->result_symbol = BLOCK_AMPERSAND; return true; } return false; } // Use whitespace to distinguish between '&' as a block // argument, and as a binary operator. // For example, these are parsed as binary operators: // foo & bar // foo&bar // while this is parsed as a block argument: // foo &bar bool unary_priority = (state->has_leading_whitespace && !iswspace(lexer->lookahead)); if (unary_priority && valid_symbols[BLOCK_AMPERSAND]) { lexer->result_symbol = BLOCK_AMPERSAND; return true; } else if (valid_symbols[BINARY_AMPERSAND]) { lexer->result_symbol = BINARY_AMPERSAND; return true; } else if (valid_symbols[BLOCK_AMPERSAND]) { lexer->result_symbol = BLOCK_AMPERSAND; return true; } } break; case '/': if (valid_symbols[REGEX_START] || valid_symbols[BINARY_SLASH] || valid_symbols[BINARY_DOUBLE_SLASH]) { lex_advance(lexer); if (lexer->lookahead == '=') { if (valid_symbols[REGEX_START] || valid_symbols[BINARY_SLASH]) { if (valid_symbols[REGEX_START] && !valid_symbols[BINARY_SLASH]) { // This is a case like `foo = /=` or `foo(/=`. // We can unambiguously prefer a regex here. lexer->result_symbol = REGEX_START; return true; } else if (valid_symbols[BINARY_SLASH] && !valid_symbols[REGEX_START]) { // This might be a case like `@foo /=`. We assume that `@a /= b` is valid // everywhere `@a / b` is valid, but `/=` is not an external symbol so return false. return false; } else { if (valid_symbols[START_OF_PARENLESS_ARGS]) { // This is a case like `a.b /=`. The Crystal parser uses // `slash_is_not_regex!` in this scenario, so return false. return false; } else { // This could be a case like `a .. /=`. The Crystal parser doesn't // recognize the `/=` token here, so just return false. return false; } } } return false; } if (lexer->lookahead == '/' && valid_symbols[BINARY_DOUBLE_SLASH]) { lex_advance(lexer); if (lexer->lookahead == '=') { return false; } lexer->result_symbol = BINARY_DOUBLE_SLASH; return true; } if (valid_symbols[BINARY_SLASH] && !valid_symbols[REGEX_START]) { lexer->result_symbol = BINARY_SLASH; return true; } else if (valid_symbols[REGEX_START] && !valid_symbols[BINARY_SLASH]) { lexer->result_symbol = REGEX_START; return true; } else { // Both are valid assert(valid_symbols[REGEX_START] && valid_symbols[BINARY_SLASH]); // If we've reached this point, we need to distinguish between two cases: // foo / // ^ // and // .. / // ^ // START_OF_PARENLESS_ARGS signals the first case, and END_OF_RANGE signals the // second. If error recovery is active, we won't know for sure which we're in, // so just prefer the first branch. if (valid_symbols[START_OF_PARENLESS_ARGS]) { if (state->has_leading_whitespace && !(lexer->lookahead == ' ' || lexer->lookahead == '\t' || lexer->lookahead == '\n' || lexer->lookahead == '\r')) { // If we're in the state // foo /a // ^ // then we assume this is the start of a regex. lexer->result_symbol = REGEX_START; return true; } else { // We must be in one of these states: // foo/a // ^ // or // foo/ a // ^ // or // foo / a // ^ // In each of these cases, we give the slash operator // higher precedence over a regex. lexer->result_symbol = BINARY_SLASH; return true; } } else if (valid_symbols[END_OF_RANGE]) { // We must be in this state: // .. / // ^ // Assume this is the start of a regex. lexer->result_symbol = REGEX_START; return true; } else { // This sort of ambiguity should only happen after an identifier without // parentheses, or after a range operator. assert(valid_symbols[START_OF_PARENLESS_ARGS] || valid_symbols[END_OF_RANGE]); } } } break; case '%': lex_advance(lexer); // End of a macro expression if (lexer->lookahead == '}') { return false; } // `%=` is not an external token if (lexer->lookahead == '=') { return false; } if (valid_symbols[STRING_PERCENT_LITERAL_START] || valid_symbols[COMMAND_PERCENT_LITERAL_START] || valid_symbols[STRING_ARRAY_PERCENT_LITERAL_START] || valid_symbols[SYMBOL_ARRAY_PERCENT_LITERAL_START] || valid_symbols[REGEX_PERCENT_LITERAL_START]) { LiteralType type = STRING; Token return_symbol = STRING_PERCENT_LITERAL_START; switch (lexer->lookahead) { case 'Q': lex_advance(lexer); // type is already STRING break; case 'q': lex_advance(lexer); type = STRING_NO_ESCAPE; break; case 'x': lex_advance(lexer); type = COMMAND; return_symbol = COMMAND_PERCENT_LITERAL_START; break; case 'w': lex_advance(lexer); type = STRING_ARRAY; return_symbol = STRING_ARRAY_PERCENT_LITERAL_START; break; case 'i': lex_advance(lexer); type = SYMBOL_ARRAY; return_symbol = SYMBOL_ARRAY_PERCENT_LITERAL_START; break; case 'r': lex_advance(lexer); type = REGEX; return_symbol = REGEX_PERCENT_LITERAL_START; break; } int32_t opening_char = 0, closing_char; switch (lexer->lookahead) { case '{': opening_char = '{'; closing_char = '}'; break; case '(': opening_char = '('; closing_char = ')'; break; case '[': opening_char = '['; closing_char = ']'; break; case '<': opening_char = '<'; closing_char = '>'; break; case '|': opening_char = '|'; closing_char = '|'; break; default: if (valid_symbols[MODULO_OPERATOR]) { lexer->result_symbol = MODULO_OPERATOR; return true; } } if (opening_char) { lex_advance(lexer); if (!valid_symbols[return_symbol]) { return false; } lexer->result_symbol = return_symbol; if (state->literals.size >= MAX_LITERAL_COUNT) { // Instead of overflowing the state (and accessing out-of-bounds memory) // we'll just return false, resulting in an error in the syntax tree. The // literals already on the stack can still be parsed successfully. return false; } PUSH_LITERAL(state, ((PercentLiteral){ .opening_char = opening_char, .closing_char = closing_char, .type = type, .nesting_level = 0, })); return true; } } else if (valid_symbols[MODULO_OPERATOR]) { lexer->result_symbol = MODULO_OPERATOR; return true; } break; case '"': if (valid_symbols[STRING_LITERAL_START]) { lex_advance(lexer); PUSH_LITERAL(state, ((PercentLiteral){ .opening_char = '"', .closing_char = '"', .type = STRING, .nesting_level = 0, })); lexer->result_symbol = STRING_LITERAL_START; return true; } else if (valid_symbols[STRING_LITERAL_END]) { lex_advance(lexer); lexer->result_symbol = STRING_LITERAL_END; return true; } break; case '`': if (valid_symbols[COMMAND_LITERAL_START]) { lex_advance(lexer); PUSH_LITERAL(state, ((PercentLiteral){ .opening_char = '`', .closing_char = '`', .type = COMMAND, .nesting_level = 0, })); lexer->result_symbol = COMMAND_LITERAL_START; return true; } else if (valid_symbols[COMMAND_LITERAL_END]) { lex_advance(lexer); lexer->result_symbol = COMMAND_LITERAL_END; return true; } break; case '\\': if (valid_symbols[LINE_CONTINUATION]) { // Don't allow line continuation in a quoted heredoc if (has_active_heredoc(state) && !array_get(&state->heredocs, 0)->allow_escapes) { return false; } // Line continuations may be allowed in some literals if (HAS_ACTIVE_LITERAL(state)) { switch (ACTIVE_LITERAL(state)->type) { case STRING_NO_ESCAPE: case REGEX: case STRING_ARRAY: case SYMBOL_ARRAY: // Line continuations aren't allowed here return false; case STRING: case COMMAND: // Continue checking for line continuation break; } } lex_advance(lexer); if (lexer->lookahead == '\r') { lex_advance(lexer); } if (lexer->lookahead == '\n') { lex_advance(lexer); lexer->result_symbol = LINE_CONTINUATION; state->previous_line_continued = true; return true; } } break; case ':': if (valid_symbols[START_OF_SYMBOL] || valid_symbols[TYPE_FIELD_COLON]) { lex_advance(lexer); int32_t lookahead = lexer->lookahead; if (state->has_leading_whitespace && iswspace(lookahead) && valid_symbols[TYPE_FIELD_COLON]) { lex_advance(lexer); lexer->result_symbol = TYPE_FIELD_COLON; return true; } if (!valid_symbols[START_OF_SYMBOL]) { return false; } switch (lookahead) { case '!': case '%': case '&': case '*': case '+': case '-': case '/': case '<': case '=': case '>': case '[': case '^': case '|': case '~': // start of an operator symbol lexer->result_symbol = START_OF_SYMBOL; return true; } if (('A' <= lookahead && lookahead <= 'Z') || ('a' <= lookahead && lookahead <= 'z') || (lookahead == '_') || (0x00a0 <= lookahead && lookahead <= 0x10ffffff)) { // This is the start of an unquoted symbol lexer->result_symbol = START_OF_SYMBOL; return true; } } break; case '.': if (valid_symbols[BEGINLESS_RANGE_OPERATOR] && !valid_symbols[START_OF_PARENLESS_ARGS]) { lex_advance(lexer); if (lexer->lookahead != '.') { return false; } lex_advance(lexer); if (lexer->lookahead == '.') { lex_advance(lexer); } lexer->result_symbol = BEGINLESS_RANGE_OPERATOR; return true; } break; case 'e': if (valid_symbols[REGULAR_ENSURE_KEYWORD] || valid_symbols[MODIFIER_ENSURE_KEYWORD]) { lex_advance(lexer); if (lexer->lookahead != 'n') { return false; } lex_advance(lexer); if (lexer->lookahead != 's') { return false; } lex_advance(lexer); if (lexer->lookahead != 'u') { return false; } lex_advance(lexer); if (lexer->lookahead != 'r') { return false; } lex_advance(lexer); if (lexer->lookahead != 'e') { return false; } lex_advance(lexer); if (next_char_is_identifier(lexer)) { // This is some other identifier, not 'ensure' return false; } if (valid_symbols[MODIFIER_ENSURE_KEYWORD] && !valid_symbols[REGULAR_ENSURE_KEYWORD]) { lexer->result_symbol = MODIFIER_ENSURE_KEYWORD; return true; } else if (valid_symbols[REGULAR_ENSURE_KEYWORD] && !valid_symbols[MODIFIER_ENSURE_KEYWORD]) { lexer->result_symbol = REGULAR_ENSURE_KEYWORD; return true; } else { // Both are valid assert(valid_symbols[MODIFIER_ENSURE_KEYWORD] && valid_symbols[REGULAR_ENSURE_KEYWORD]); // TODO: currently assuming that the modifier always takes // precedence here. Is that correct? lexer->result_symbol = MODIFIER_ENSURE_KEYWORD; return true; } } break; case 'i': if (valid_symbols[REGULAR_IF_KEYWORD] || valid_symbols[MODIFIER_IF_KEYWORD]) { lex_advance(lexer); if (lexer->lookahead != 'f') { return false; } lex_advance(lexer); if (next_char_is_identifier(lexer)) { // This is some other identifier, not 'if' return false; } if (valid_symbols[MODIFIER_IF_KEYWORD] && !valid_symbols[REGULAR_IF_KEYWORD]) { lexer->result_symbol = MODIFIER_IF_KEYWORD; return true; } else if (valid_symbols[REGULAR_IF_KEYWORD] && !valid_symbols[MODIFIER_IF_KEYWORD]) { lexer->result_symbol = REGULAR_IF_KEYWORD; return true; } else { // Both are valid assert(valid_symbols[MODIFIER_IF_KEYWORD] && valid_symbols[REGULAR_IF_KEYWORD]); // This sort of ambiguity may happen after an identifier // without parentheses, or after a keyword like `return` // that takes an optional expression. lexer->result_symbol = MODIFIER_IF_KEYWORD; return true; } } break; case 'r': if (valid_symbols[REGULAR_RESCUE_KEYWORD] || valid_symbols[MODIFIER_RESCUE_KEYWORD]) { lex_advance(lexer); if (lexer->lookahead != 'e') { return false; } lex_advance(lexer); if (lexer->lookahead != 's') { return false; } lex_advance(lexer); if (lexer->lookahead != 'c') { return false; } lex_advance(lexer); if (lexer->lookahead != 'u') { return false; } lex_advance(lexer); if (lexer->lookahead != 'e') { return false; } lex_advance(lexer); if (next_char_is_identifier(lexer)) { // This is some other identifier, not 'rescue' return false; } if (valid_symbols[MODIFIER_RESCUE_KEYWORD] && !valid_symbols[REGULAR_RESCUE_KEYWORD]) { lexer->result_symbol = MODIFIER_RESCUE_KEYWORD; return true; } else if (valid_symbols[REGULAR_RESCUE_KEYWORD] && !valid_symbols[MODIFIER_RESCUE_KEYWORD]) { lexer->result_symbol = REGULAR_RESCUE_KEYWORD; return true; } else { // Both are valid assert(valid_symbols[MODIFIER_RESCUE_KEYWORD] && valid_symbols[REGULAR_RESCUE_KEYWORD]); // TODO: currently assuming that the modifier always takes // precedence here. Is that correct? lexer->result_symbol = MODIFIER_RESCUE_KEYWORD; return true; } } break; case 'u': if (valid_symbols[REGULAR_UNLESS_KEYWORD] || valid_symbols[MODIFIER_UNLESS_KEYWORD]) { lex_advance(lexer); if (lexer->lookahead != 'n') { return false; } lex_advance(lexer); if (lexer->lookahead != 'l') { return false; } lex_advance(lexer); if (lexer->lookahead != 'e') { return false; } lex_advance(lexer); if (lexer->lookahead != 's') { return false; } lex_advance(lexer); if (lexer->lookahead != 's') { return false; } lex_advance(lexer); if (next_char_is_identifier(lexer)) { // This is some other identifier, not 'unless' return false; } if (valid_symbols[MODIFIER_UNLESS_KEYWORD] && !valid_symbols[REGULAR_UNLESS_KEYWORD]) { lexer->result_symbol = MODIFIER_UNLESS_KEYWORD; return true; } else if (valid_symbols[REGULAR_UNLESS_KEYWORD] && !valid_symbols[MODIFIER_UNLESS_KEYWORD]) { lexer->result_symbol = REGULAR_UNLESS_KEYWORD; return true; } else { // Both are valid assert(valid_symbols[MODIFIER_UNLESS_KEYWORD] && valid_symbols[REGULAR_UNLESS_KEYWORD]); // This sort of ambiguity may happen after an identifier // without parentheses, or after a keyword like `return` // that takes an optional expression. lexer->result_symbol = MODIFIER_UNLESS_KEYWORD; return true; } } break; case 'y': if (valid_symbols[END_OF_WITH_EXPRESSSION]) { // We don't want to consume the yield keyword lexer->mark_end(lexer); lex_advance(lexer); if (lexer->lookahead != 'i') { return false; } lex_advance(lexer); if (lexer->lookahead != 'e') { return false; } lex_advance(lexer); if (lexer->lookahead != 'l') { return false; } lex_advance(lexer); if (lexer->lookahead != 'd') { return false; } lex_advance(lexer); if (next_char_is_identifier(lexer)) { // This is some other identifier, not 'yield' return false; } lexer->result_symbol = END_OF_WITH_EXPRESSSION; return true; } break; case '}': lex_advance(lexer); // End of a macro expression if (lexer->lookahead == '}') { return false; } break; } return false; } bool tree_sitter_crystal_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { DEBUG("starting external scan"); if (lexer->lookahead == '\n') { DEBUG("char is '\\n'"); } else if (lexer->lookahead == '\0') { DEBUG("char is '\\0'"); } else if (lexer->lookahead == '\r') { DEBUG("char is '\\r'"); } else if (lexer->lookahead == '\t') { DEBUG("char is '\\t'"); } else { DEBUG("char is '%c'", lexer->lookahead); } DEBUG("valid symbols are:"); #define LOG_SYMBOL(sym) \ if (valid_symbols[sym]) { DEBUG(" " #sym); } LOG_SYMBOL(LINE_BREAK); LOG_SYMBOL(LINE_CONTINUATION); LOG_SYMBOL(START_OF_BRACE_BLOCK); LOG_SYMBOL(START_OF_HASH_OR_TUPLE); LOG_SYMBOL(START_OF_NAMED_TUPLE); LOG_SYMBOL(START_OF_TUPLE_TYPE); LOG_SYMBOL(START_OF_NAMED_TUPLE_TYPE); LOG_SYMBOL(START_OF_INDEX_OPERATOR); LOG_SYMBOL(END_OF_WITH_EXPRESSSION); LOG_SYMBOL(UNARY_PLUS); LOG_SYMBOL(UNARY_MINUS); LOG_SYMBOL(BINARY_PLUS); LOG_SYMBOL(BINARY_MINUS); LOG_SYMBOL(UNARY_WRAPPING_PLUS); LOG_SYMBOL(UNARY_WRAPPING_MINUS); LOG_SYMBOL(BINARY_WRAPPING_PLUS); LOG_SYMBOL(BINARY_WRAPPING_MINUS); LOG_SYMBOL(UNARY_STAR); LOG_SYMBOL(BINARY_STAR); LOG_SYMBOL(UNARY_DOUBLE_STAR); LOG_SYMBOL(BINARY_DOUBLE_STAR); LOG_SYMBOL(BLOCK_AMPERSAND); LOG_SYMBOL(BINARY_AMPERSAND); LOG_SYMBOL(BEGINLESS_RANGE_OPERATOR); LOG_SYMBOL(REGEX_START); LOG_SYMBOL(BINARY_SLASH); LOG_SYMBOL(BINARY_DOUBLE_SLASH); LOG_SYMBOL(REGULAR_IF_KEYWORD); LOG_SYMBOL(MODIFIER_IF_KEYWORD); LOG_SYMBOL(REGULAR_UNLESS_KEYWORD); LOG_SYMBOL(MODIFIER_UNLESS_KEYWORD); LOG_SYMBOL(REGULAR_RESCUE_KEYWORD); LOG_SYMBOL(MODIFIER_RESCUE_KEYWORD); LOG_SYMBOL(REGULAR_ENSURE_KEYWORD); LOG_SYMBOL(MODIFIER_ENSURE_KEYWORD); LOG_SYMBOL(MODULO_OPERATOR); LOG_SYMBOL(START_OF_SYMBOL); LOG_SYMBOL(UNQUOTED_SYMBOL_CONTENT); LOG_SYMBOL(TYPE_FIELD_COLON); LOG_SYMBOL(STRING_LITERAL_START); LOG_SYMBOL(DELIMITED_STRING_CONTENTS); LOG_SYMBOL(STRING_LITERAL_END); LOG_SYMBOL(COMMAND_LITERAL_START); LOG_SYMBOL(COMMAND_LITERAL_END); LOG_SYMBOL(STRING_PERCENT_LITERAL_START); LOG_SYMBOL(COMMAND_PERCENT_LITERAL_START); LOG_SYMBOL(STRING_ARRAY_PERCENT_LITERAL_START); LOG_SYMBOL(SYMBOL_ARRAY_PERCENT_LITERAL_START); LOG_SYMBOL(REGEX_PERCENT_LITERAL_START); LOG_SYMBOL(PERCENT_LITERAL_END); LOG_SYMBOL(DELIMITED_ARRAY_ELEMENT_START); LOG_SYMBOL(DELIMITED_ARRAY_ELEMENT_END); LOG_SYMBOL(HEREDOC_START); LOG_SYMBOL(HEREDOC_BODY_START); LOG_SYMBOL(HEREDOC_CONTENT); LOG_SYMBOL(HEREDOC_END); LOG_SYMBOL(REGEX_MODIFIER); LOG_SYMBOL(MACRO_START); LOG_SYMBOL(MACRO_DELIMITER_END); LOG_SYMBOL(MACRO_DELIMITER_ELSE); LOG_SYMBOL(MACRO_DELIMITER_ELSIF); LOG_SYMBOL(MACRO_CONTENT); LOG_SYMBOL(MACRO_CONTENT_NESTING); LOG_SYMBOL(START_OF_PARENLESS_ARGS); LOG_SYMBOL(END_OF_RANGE); LOG_SYMBOL(START_OF_MACRO_VAR_EXPS); LOG_SYMBOL(ERROR_RECOVERY); bool result = inner_scan(payload, lexer, valid_symbols); if (result) { DEBUG("external scan got a result"); } else { DEBUG("external scan returned nothing"); } return result; } void *tree_sitter_crystal_external_scanner_create(void) { State *state; state = ts_calloc(1, sizeof(State)); state->has_leading_whitespace = false; state->previous_line_continued = false; reset_macro_state(state); array_init(&state->literals); array_init(&state->heredocs); return state; } // Release any memory allocated for heredoc identifiers static void free_old_heredoc_identifiers(State *state) { for (size_t i = 0; i < state->heredocs.size; i++) { Heredoc *heredoc = array_get(&state->heredocs, i); array_delete(&heredoc->identifier); } } void tree_sitter_crystal_external_scanner_destroy(void *payload) { State *state = (State *)payload; free_old_heredoc_identifiers(state); array_delete(&state->literals); array_delete(&state->heredocs); ts_free(state); } unsigned tree_sitter_crystal_external_scanner_serialize(void *payload, char *buffer) { State *state = (State *)payload; size_t offset = 0; buffer[offset++] = (char)state->has_leading_whitespace; buffer[offset++] = (char)state->previous_line_continued; buffer[offset++] = (char)state->macro_state.in_comment; buffer[offset++] = (char)state->macro_state.non_modifier_keyword_can_begin; // It's safe to cast the literal count into a char since it will always be // less than MAX_LITERAL_COUNT. buffer[offset++] = (char)state->literals.size; // The literals array can be serialized in one chunk. size_t literal_content_size = state->literals.size * array_elem_size(&state->literals); if (literal_content_size > 0) memcpy(&buffer[offset], state->literals.contents, literal_content_size); offset += literal_content_size; // It's safe to cast the heredoc count into a char since it will always be // less than MAX_HEREDOC_COUNT. buffer[offset++] = (char)state->heredocs.size; // Heredoc are serialized one at a time, with their identifier buffers inlined. for (uint8_t i = 0; i < state->heredocs.size; i++) { Heredoc *heredoc = array_get(&state->heredocs, i); buffer[offset++] = (char)heredoc->allow_escapes; buffer[offset++] = (char)heredoc->started; // It's safe to cast the identifier size into a char since it will // always be less than or equal to MAX_HEREDOC_WORD_SIZE. buffer[offset++] = (char)heredoc->identifier.size; memcpy(&buffer[offset], heredoc->identifier.contents, heredoc->identifier.size); offset += heredoc->identifier.size; } assert(offset <= TREE_SITTER_SERIALIZATION_BUFFER_SIZE); return offset; } #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) static_assert( 2 // boolean variables + sizeof(MacroState) // macro state + 1 // literals count + sizeof(PercentLiteral) * MAX_LITERAL_COUNT // each literal + 1 // heredocs count + 3 * MAX_HEREDOC_COUNT // each heredoc object + HEREDOC_BUFFER_SIZE // heredoc buffer total <= TREE_SITTER_SERIALIZATION_BUFFER_SIZE, "Maximum serialized size is too large"); #endif void tree_sitter_crystal_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) { State *state = (State *)payload; // Any deserialized heredocs will overwrite what's here already, so free the // existing heredoc references. free_old_heredoc_identifiers(state); // Reset the size of the arrays, but don't touch their reserved memory or capacity. // We can reuse the same content buffers without freeing and reallocating memory. array_clear(&state->literals); array_clear(&state->heredocs); if (length == 0) { // Sometimes this function is called with a length of zero. In that // case we just finish resetting the state. state->has_leading_whitespace = false; state->previous_line_continued = false; reset_macro_state(state); return; } size_t offset = 0; state->has_leading_whitespace = (bool)buffer[offset++]; state->previous_line_continued = (bool)buffer[offset++]; state->macro_state.in_comment = (bool)buffer[offset++]; state->macro_state.non_modifier_keyword_can_begin = (bool)buffer[offset++]; // The literals array can be deserialized in one chunk. uint8_t literals_size = (uint8_t)buffer[offset++]; array_extend(&state->literals, literals_size, &buffer[offset]); offset += literals_size * array_elem_size(&state->literals); // Each heredoc must be deserialized individually to handle the identifier buffer. uint8_t heredocs_size = (uint8_t)buffer[offset++]; for (uint8_t i = 0; i < heredocs_size; i++) { Heredoc heredoc = { .allow_escapes = false, .started = false, .identifier = array_new(), }; heredoc.allow_escapes = (bool)buffer[offset++]; heredoc.started = (bool)buffer[offset++]; uint8_t identifier_size = (uint8_t)buffer[offset++]; array_extend(&heredoc.identifier, identifier_size, &buffer[offset]); offset += identifier_size; array_push(&state->heredocs, heredoc); } assert(offset == length); }