Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

3227 lines
119 KiB
C
Generated

#include "tree_sitter/alloc.h"
#include "tree_sitter/array.h"
#include "tree_sitter/parser.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wctype.h>
// 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 '\<whitespace>' 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:
// <range_start> .. /
// ^
// 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);
}