mod ident;
pub mod keywords;
mod number;
mod strings;
#[cfg(test)]
mod test;
use crate::gql::token::{Token, TokenKind, t};
use crate::syn::error::{SyntaxError, bail, syntax_error};
use crate::syn::lexer::BytesReader;
use crate::syn::token::Span;
pub struct Lexer<'a> {
reader: BytesReader<'a>,
last_offset: u32,
pub(in crate::gql) error: Option<SyntaxError>,
}
impl<'a> Lexer<'a> {
pub fn new(source: &'a str) -> Lexer<'a> {
assert!(source.len() <= u32::MAX as usize, "source code exceeded maximum size");
Lexer {
reader: BytesReader::new(source.as_bytes()),
last_offset: 0,
error: None,
}
}
pub fn next_token(&mut self) -> Token {
loop {
self.last_offset = self.reader.offset();
let Some(byte) = self.reader.next() else {
return self.eof_token();
};
let token = if byte.is_ascii() {
self.lex_ascii(byte)
} else {
self.lex_char(byte)
};
if let Some(token) = token {
return token;
}
}
}
fn lex_ascii(&mut self, byte: u8) -> Option<Token> {
let kind = match byte {
b' ' | b'\t' | b'\n' | 0x0B | 0x0C | b'\r' | 0x1C..=0x1F => {
self.eat_whitespace();
return None;
}
b'(' => t!("("),
b')' => t!(")"),
b'[' => t!("["),
b']' => match self.reader.peek() {
Some(b'-') => {
self.reader.next();
if self.eat(b'>') {
t!("]->")
} else {
t!("]-")
}
}
Some(b'~') => {
self.reader.next();
if self.eat(b'>') {
t!("]~>")
} else {
t!("]~")
}
}
_ => t!("]"),
},
b'{' => t!("{"),
b'}' => t!("}"),
b',' => t!(","),
b'&' => t!("&"),
b'%' => t!("%"),
b'!' => t!("!"),
b'?' => t!("?"),
b'*' => t!("*"),
b'+' => t!("+"),
b':' => {
if self.eat(b':') {
t!("::")
} else {
t!(":")
}
}
b'=' => {
if self.eat(b'>') {
t!("=>")
} else {
t!("=")
}
}
b'>' => {
if self.eat(b'=') {
t!(">=")
} else {
t!(">")
}
}
b'.' => {
if self.reader.peek().is_some_and(|x| x.is_ascii_digit()) {
return Some(self.lex_number_starting_period());
}
if self.eat(b'.') {
t!("..")
} else {
t!(".")
}
}
b'|' => {
if self.reader.peek() == Some(b'+') && self.reader.peek1() == Some(b'|') {
self.reader.next();
self.reader.next();
t!("|+|")
} else if self.eat(b'|') {
t!("||")
} else {
t!("|")
}
}
b'-' => match self.reader.peek() {
Some(b'-') => {
self.reader.next();
self.eat_line_comment();
return None;
}
Some(b'[') => {
self.reader.next();
t!("-[")
}
Some(b'>') => {
self.reader.next();
t!("->")
}
Some(b'/') => {
self.reader.next();
t!("-/")
}
_ => t!("-"),
},
b'<' => match self.reader.peek() {
Some(b'-') => {
self.reader.next();
match self.reader.peek() {
Some(b'[') => {
self.reader.next();
t!("<-[")
}
Some(b'>') => {
self.reader.next();
t!("<->")
}
Some(b'/') => {
self.reader.next();
t!("<-/")
}
_ => t!("<-"),
}
}
Some(b'~') => {
self.reader.next();
match self.reader.peek() {
Some(b'[') => {
self.reader.next();
t!("<~[")
}
Some(b'/') => {
self.reader.next();
t!("<~/")
}
_ => t!("<~"),
}
}
Some(b'=') => {
self.reader.next();
t!("<=")
}
Some(b'>') => {
self.reader.next();
t!("<>")
}
_ => t!("<"),
},
b'~' => match self.reader.peek() {
Some(b'[') => {
self.reader.next();
t!("~[")
}
Some(b'>') => {
self.reader.next();
t!("~>")
}
Some(b'/') => {
self.reader.next();
t!("~/")
}
_ => t!("~"),
},
b'/' => match self.reader.peek() {
Some(b'/') => {
self.reader.next();
self.eat_line_comment();
return None;
}
Some(b'*') => {
self.reader.next();
return match self.eat_bracketed_comment() {
Ok(()) => None,
Err(e) => Some(self.invalid_token(e)),
};
}
Some(b'-') => {
self.reader.next();
if self.eat(b'>') {
t!("/->")
} else {
t!("/-")
}
}
Some(b'~') => {
self.reader.next();
if self.eat(b'>') {
t!("/~>")
} else {
t!("/~")
}
}
_ => t!("/"),
},
b'@' => match self.reader.peek() {
Some(quote @ (b'\'' | b'"' | b'`')) => {
self.reader.next();
return Some(self.lex_quoted_token(quote, true));
}
_ => t!("@"),
},
quote @ (b'\'' | b'"' | b'`') => return Some(self.lex_quoted_token(quote, false)),
b'$' => return Some(self.lex_param()),
b'0'..=b'9' => return Some(self.lex_number(byte)),
b'a'..=b'z' | b'A'..=b'Z' | b'_' => return Some(self.lex_ident()),
x => {
let error = syntax_error!(
"Unexpected character `{}`",
(x as char).escape_debug(),
@self.current_span()
);
return Some(self.invalid_token(error));
}
};
Some(self.finish_token(kind))
}
fn lex_char(&mut self, byte: u8) -> Option<Token> {
let char = match self.reader.complete_char(byte) {
Ok(x) => x,
Err(e) => return Some(self.invalid_token(e.into())),
};
if is_whitespace_char(char) {
self.eat_whitespace();
return None;
}
if ident::is_ident_start(char) {
return Some(self.lex_ident());
}
let error = syntax_error!(
"Unexpected character `{}`",
char.escape_debug(),
@self.current_span()
);
Some(self.invalid_token(error))
}
fn eat_whitespace(&mut self) {
loop {
let Some(byte) = self.reader.peek() else {
return;
};
match byte {
b' ' | b'\t' | b'\n' | 0x0B | 0x0C | b'\r' | 0x1C..=0x1F => {
self.reader.next();
}
x if !x.is_ascii() => {
let backup = self.reader.offset();
self.reader.next();
match self.reader.complete_char(x) {
Ok(c) if is_whitespace_char(c) => {}
_ => {
self.reader.backup(backup);
return;
}
}
}
_ => return,
}
}
}
fn eat_line_comment(&mut self) {
while let Some(byte) = self.reader.peek() {
if matches!(byte, b'\r' | b'\n') {
return;
}
self.reader.next();
}
}
fn eat_bracketed_comment(&mut self) -> Result<(), SyntaxError> {
let start_span = self.current_span();
loop {
let Some(byte) = self.reader.next() else {
bail!(
"Unexpected end of file, expected the comment to be closed with `*/`",
@start_span => "Comment starting here"
);
};
if byte == b'*' && self.eat(b'/') {
return Ok(());
}
}
}
fn eof_token(&mut self) -> Token {
Token {
kind: TokenKind::Eof,
span: Span {
offset: self.last_offset,
len: 0,
},
}
}
fn invalid_token(&mut self, error: SyntaxError) -> Token {
self.error = Some(error);
self.finish_token(TokenKind::Invalid)
}
pub(crate) fn current_span(&self) -> Span {
Span {
offset: self.last_offset,
len: self.reader.offset() - self.last_offset,
}
}
fn advance_span(&mut self) -> Span {
let span = self.current_span();
self.last_offset = self.reader.offset();
span
}
fn finish_token(&mut self, kind: TokenKind) -> Token {
Token {
kind,
span: self.advance_span(),
}
}
fn eat(&mut self, byte: u8) -> bool {
if self.reader.peek() == Some(byte) {
self.reader.next();
true
} else {
false
}
}
pub fn span_str(&self, span: Span) -> &'a str {
std::str::from_utf8(self.reader.span(span)).expect("invalid span segment for source")
}
}
fn is_whitespace_char(char: char) -> bool {
matches!(
char,
' ' | '\t'
| '\n' | '\u{000B}'
| '\u{000C}'
| '\r' | '\u{001C}'..='\u{001F}'
| '\u{00A0}'
| '\u{1680}'
| '\u{180E}'
| '\u{2000}'..='\u{200A}'
| '\u{2028}'
| '\u{2029}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
)
}