///|
/// Token category emitted by the GraphQL tokenizer.
pub(all) enum TokenKind {
TkPunctuator
TkName
TkIntValue
TkFloatValue
TkStringValue
TkBlockString
TkEof
} derive(Eq, Debug)
///|
/// GraphQL token with source span and the offset after ignored trivia.
///
/// `start_offset` and `end_offset` cover the token itself. `next_offset` points
/// to the next non-ignored source position after whitespace and comments.
pub(all) struct Token {
kind : TokenKind
value : String
position : Pos
start_offset : Int32
end_offset : Int32
next_offset : Int32
} derive(Eq, Debug)
///|
/// Tokenizer state.
///
/// `recursion_limit` bounds nested punctuation pairs so malformed deeply nested
/// input fails quickly before parser recursion.
priv struct Lexer {
source : String
mut input : StringView
mut line : Int32
mut column : Int32
mut recursion_limit : Int32
}
///|
/// Tokenize a GraphQL source document.
///
/// The returned array always ends with a `TkEof` token. Whitespace, commas, and
/// comments are skipped but reflected in token offsets.
pub fn tokenize(source : String) -> Array[Token] raise ParseError {
let lexer = Lexer::{
source,
input: source[:],
line: 1,
column: 1,
recursion_limit: 50,
}
let tokens : Array[Token] = []
while true {
lexer.skip_ignored()
let start = lexer.input.start_offset()
let position = { line: lexer.line, column: lexer.column, }
match lexer.input {
[] => {
tokens.push({
kind: TkEof,
value: "",
position,
start_offset: start,
end_offset: start,
next_offset: start,
})
break
}
_ => {
let (kind, end_offset) = lexer.take_token(position)
let value = lexer.source.unsafe_substring(start~, end=end_offset)
lexer.skip_ignored()
tokens.push({
kind,
value,
position,
start_offset: start,
end_offset,
next_offset: lexer.input.start_offset(),
})
continue
}
}
}
tokens
}
///|
fn tokenize_first_query_definition(
source : String,
) -> (Array[Token], Int32) raise ParseError {
let lexer = Lexer::{
source,
input: source[:],
line: 1,
column: 1,
recursion_limit: 50,
}
let tokens : Array[Token] = []
let mut nesting = 0
let mut first = true
while true {
lexer.skip_ignored()
let start = lexer.input.start_offset()
let position = { line: lexer.line, column: lexer.column, }
match lexer.input {
[] => {
push_eof(tokens, position, start)
return (tokens, start)
}
_ => {
let (kind, end_offset) = lexer.take_token(position)
let value = lexer.source.unsafe_substring(start~, end=end_offset)
lexer.skip_ignored()
let token = {
kind,
value,
position,
start_offset: start,
end_offset,
next_offset: lexer.input.start_offset(),
}
tokens.push(token)
if first {
first = false
if !is_query_definition_start(token) {
let eof_offset = token.next_offset
push_eof(
tokens,
{ line: lexer.line, column: lexer.column, },
eof_offset,
)
return (tokens, eof_offset)
}
}
if token.kind == TkPunctuator {
match token.value {
"(" | "[" | "{" => nesting += 1
")" | "]" | "}" => nesting -= 1
_ => ()
}
if token.value == "}" && nesting == 0 {
let eof_offset = token.next_offset
push_eof(
tokens,
{ line: lexer.line, column: lexer.column, },
eof_offset,
)
return (tokens, eof_offset)
}
}
continue
}
}
}
(tokens, lexer.input.start_offset())
}
///|
fn push_eof(tokens : Array[Token], position : Pos, offset : Int32) -> Unit {
tokens.push({
kind: TkEof,
value: "",
position,
start_offset: offset,
end_offset: offset,
next_offset: offset,
})
}
///|
fn is_query_definition_start(token : Token) -> Bool {
(token.kind == TkPunctuator && token.value == "{") ||
(
token.kind == TkName &&
(
token.value == "query" ||
token.value == "mutation" ||
token.value == "subscription" ||
token.value == "fragment"
)
)
}
///|
fn Lexer::take_token(
self : Lexer,
position : Pos,
) -> (TokenKind, Int32) raise ParseError {
match self.input {
['(', .. rest] | ['[', .. rest] | ['{', .. rest] => {
if self.recursion_limit == 0 {
parse_error("Recursion limit exceeded", position)
}
self.recursion_limit -= 1
self.advance_to(rest, 1)
(TkPunctuator, self.input.start_offset())
}
[')', .. rest] | [']', .. rest] | ['}', .. rest] => {
self.recursion_limit += 1
self.advance_to(rest, 1)
(TkPunctuator, self.input.start_offset())
}
['!', .. rest]
| ['$', .. rest]
| [':', .. rest]
| ['=', .. rest]
| ['@', .. rest]
| ['|', .. rest]
| ['&', .. rest] => {
self.advance_to(rest, 1)
(TkPunctuator, self.input.start_offset())
}
[.. "...", .. rest] => {
self.advance_to(rest, 3)
(TkPunctuator, self.input.start_offset())
}
['.', ..] =>
parse_error("bare dot '.' is not supported, only \"...\"", position)
['_', ..] | ['a'..<='z', ..] | ['A'..<='Z', ..] => {
let rest = scan_while(self.input, is_name_continue)
let count = self.input.start_offset().until(rest.start_offset()).count()
self.advance_to(rest, count)
(TkName, self.input.start_offset())
}
['-', ..] | ['0'..<='9', ..] => self.take_number(position)
['"', '"', '"', .. rest] => self.take_block_string(rest, position)
['"', .. rest] => self.take_string(rest, position)
[c, ..] => parse_error("unexpected character \{c}", position)
[] => parse_error("unexpected end of input", position)
}
}
///|
fn Lexer::take_number(
self : Lexer,
position : Pos,
) -> (TokenKind, Int32) raise ParseError {
let start = self.input.start_offset()
let rest = scan_number_tail(self.input)
let end_offset = rest.start_offset()
let value = self.source.unsafe_substring(start~, end=end_offset)
if value.contains_char('.') ||
value.contains_char('e') ||
value.contains_char('E') {
if !check_float(value) {
parse_error("unsupported float \{value}", position)
}
self.advance_to(rest, value.char_length())
(TkFloatValue, end_offset)
} else {
if !check_int(value) {
parse_error("unsupported integer \{value}", position)
}
self.advance_to(rest, value.char_length())
(TkIntValue, end_offset)
}
}
///|
fn Lexer::take_string(
self : Lexer,
rest_after_quote : StringView,
position : Pos,
) -> (TokenKind, Int32) raise ParseError {
let start = self.input.start_offset()
let mut escaped = false
let rest = rest_after_quote
for rest = rest {
match rest {
['"', .. tail] if !escaped => {
let end_offset = tail.start_offset()
let value = self.source.unsafe_substring(start~, end=end_offset)
self.advance_to(tail, value.char_length())
break (TkStringValue, end_offset)
}
['\n', ..] => parse_error("unterminated string value", position)
['\\', .. tail] if !escaped => {
escaped = true
continue tail
}
[_, .. tail] => {
escaped = false
continue tail
}
[] => parse_error("unterminated string value", position)
}
}
}
///|
fn Lexer::take_block_string(
self : Lexer,
rest_after_open : StringView,
position : Pos,
) -> (TokenKind, Int32) raise ParseError {
let start = self.input.start_offset()
let rest = rest_after_open
let mut previous = '\u{0}'
for rest = rest {
match rest {
[.. "\"\"\"", .. tail] if previous != '\\' => {
let end_offset = tail.start_offset()
let value = self.source.unsafe_substring(start~, end=end_offset)
self.update_position(value)
self.input = tail
break (TkBlockString, end_offset)
}
[c, .. tail] => {
previous = c
continue tail
}
[] => parse_error("unterminated block string value", position)
}
}
}
///|
fn Lexer::advance_to(self : Lexer, rest : StringView, columns : Int32) -> Unit {
self.input = rest
self.column += columns
}
///|
fn Lexer::update_position(self : Lexer, value : String) -> Unit {
let mut lines = 0
let mut after_last_newline = 0
for c in value.iter() {
if c == '\n' {
lines += 1
after_last_newline = 0
} else {
after_last_newline += 1
}
}
self.line += lines
if lines > 0 {
self.column = after_last_newline + 1
} else {
self.column += after_last_newline
}
}
///|
fn Lexer::skip_ignored(self : Lexer) -> Unit {
while true {
match self.input {
['\u{feff}', .. rest] | ['\r', .. rest] => {
self.input = rest
continue
}
['\t', .. rest] => {
self.column += 8
self.input = rest
continue
}
['\n', .. rest] => {
self.column = 1
self.line += 1
self.input = rest
continue
}
[' ', .. rest] | [',', .. rest] => {
self.column += 1
self.input = rest
continue
}
['#', .. rest] => {
let rest = self.skip_comment(rest)
self.input = rest
continue
}
_ => break
}
}
}
///|
fn Lexer::skip_comment(self : Lexer, input : StringView) -> StringView {
for input = input {
match input {
['\n', .. rest] | ['\r', .. rest] => {
self.column = 1
self.line += 1
break rest
}
[_, .. rest] => continue rest
[] as rest => break rest
}
}
}
///|
fn scan_while(input : StringView, pred : (Char) -> Bool) -> StringView {
for input = input {
match input {
[c, .. rest] if pred(c) => continue rest
rest => break rest
}
}
}
///|
fn scan_number_tail(input : StringView) -> StringView {
for input = input {
match input {
[c, ..] as rest if is_number_stop(c) => break rest
[_, .. rest] => continue rest
[] as rest => break rest
}
}
}
///|
fn is_number_stop(c : Char) -> Bool {
match c {
' '
| '\n'
| '\r'
| '\t'
| ','
| '#'
| '!'
| '$'
| ':'
| '='
| '@'
| '|'
| '&'
| '('
| ')'
| '['
| ']'
| '{'
| '}' => true
_ => false
}
}
///|
fn is_name_continue(c : Char) -> Bool {
c == '_' || c.is_ascii_alphabetic() || c.is_ascii_digit()
}
///|
fn all_digits(input : StringView) -> Bool {
for input = input {
match input {
[c, .. rest] if c.is_ascii_digit() => continue rest
[] => break true
_ => break false
}
}
}
///|
fn check_int(value : String) -> Bool {
if value == "0" || value == "-0" {
true
} else if value.has_prefix("0") {
false
} else if value == "-" {
false
} else if value.has_prefix("-0") {
false
} else {
all_digits_after_first(value)
}
}
///|
fn all_digits_after_first(value : String) -> Bool {
let mut first = true
for c in value.iter() {
if first {
first = false
continue
}
if !c.is_ascii_digit() {
return false
}
}
true
}
///|
pub impl Show for TokenKind with fn output(self : TokenKind, logger : &Logger) -> Unit {
logger.write_string(
match self {
TkPunctuator => "TkPunctuator"
TkName => "TkName"
TkIntValue => "TkIntValue"
TkFloatValue => "TkFloatValue"
TkStringValue => "TkStringValue"
TkBlockString => "TkBlockString"
TkEof => "TkEof"
},
)
}
///|
fn check_dec(value : StringView) -> Bool {
!value.is_empty() && all_digits(value)
}
///|
fn check_exp(value : StringView) -> Bool {
match value {
[] => false
[first, .. rest] =>
if first != '-' && first != '+' && (first <= '0' || first >= '9') {
false
} else {
all_digits(rest)
}
}
}
///|
fn check_float(value : String) -> Bool {
let exponent = first_exponent(value)
let real = value.find(".")
match (exponent, real) {
(Some(e), Some(r)) if e < r => false
(Some(e), Some(r)) =>
check_int(value.unsafe_substring(start=0, end=r)) &&
check_dec(value.view(start_offset=r + 1, end_offset=e)) &&
check_exp(value.view(start_offset=e + 1))
(Some(e), None) =>
check_int(value.unsafe_substring(start=0, end=e)) &&
check_exp(value.view(start_offset=e + 1))
(None, Some(r)) =>
check_int(value.unsafe_substring(start=0, end=r)) &&
check_dec(value.view(start_offset=r + 1))
(None, None) => false
}
}
///|
fn first_exponent(value : String) -> Int32? {
let lower = value.find("e")
let upper = value.find("E")
match (lower, upper) {
(Some(a), Some(b)) => Some(Int32::min(a, b))
(Some(a), None) => Some(a)
(None, Some(b)) => Some(b)
(None, None) => None
}
}