bql 0.1.0

BQL — Bridge Query Language parser and formatter (Rust, generated).
Documentation
//! Generated BQL tokenizer. Do not edit by hand.
//!
//! Driven by the token definitions in `spec/bql.rules`.

use crate::ast::BinOp;
use crate::error::{BqlError, BqlResult};

#[derive(Debug, Clone, PartialEq)]
pub struct Token<'a> {
    pub kind: TokenKind<'a>,
    pub offset: usize,
}

#[derive(Debug, Clone, PartialEq)]
pub enum TokenKind<'a> {
    LParen,
    RParen,
    Comma,
    Op(BinOp),
    Str(&'a str),
    Word(&'a str),
    Eof,
}

pub fn lex<'a>(input: &'a str) -> BqlResult<Vec<Token<'a>>> {
    let mut out = Vec::new();
    let mut i = 0usize;
    let n = input.len();
    while i < n {
        let c = input[i..].chars().next().expect("in-bounds");
        if is_whitespace_char(c) {
            i += c.len_utf8();
            continue;
        }
        let start = i;
        match c {
            ',' => {
                out.push(Token {
                    kind: TokenKind::Comma,
                    offset: start,
                });
                i += 1;
            }
            '(' => {
                out.push(Token {
                    kind: TokenKind::LParen,
                    offset: start,
                });
                i += 1;
            }
            '!' => {
                if input[i..].starts_with("!^=") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::NotStartsWith),
                        offset: start,
                    });
                    i += 3;
                    continue;
                }
                if input[i..].starts_with("!$=") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::NotEndsWith),
                        offset: start,
                    });
                    i += 3;
                    continue;
                }
                if input[i..].starts_with("!=") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::Neq),
                        offset: start,
                    });
                    i += 2;
                    continue;
                }
                if input[i..].starts_with("!~") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::NotContains),
                        offset: start,
                    });
                    i += 2;
                    continue;
                }
                return Err(BqlError::new(start, "stray '!' (expected an operator)"));
            }
            '$' => {
                if input[i..].starts_with("$=") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::EndsWith),
                        offset: start,
                    });
                    i += 2;
                    continue;
                }
                return Err(BqlError::new(start, "stray '$' (expected an operator)"));
            }
            '<' => {
                if input[i..].starts_with("<=") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::Lte),
                        offset: start,
                    });
                    i += 2;
                    continue;
                }
                if input[i..].starts_with("<") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::Lt),
                        offset: start,
                    });
                    i += 1;
                    continue;
                }
                return Err(BqlError::new(start, "stray '<' (expected an operator)"));
            }
            '=' => {
                if input[i..].starts_with("=") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::Eq),
                        offset: start,
                    });
                    i += 1;
                    continue;
                }
                return Err(BqlError::new(start, "stray '=' (expected an operator)"));
            }
            '>' => {
                if input[i..].starts_with(">=") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::Gte),
                        offset: start,
                    });
                    i += 2;
                    continue;
                }
                if input[i..].starts_with(">") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::Gt),
                        offset: start,
                    });
                    i += 1;
                    continue;
                }
                return Err(BqlError::new(start, "stray '>' (expected an operator)"));
            }
            '^' => {
                if input[i..].starts_with("^=") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::StartsWith),
                        offset: start,
                    });
                    i += 2;
                    continue;
                }
                return Err(BqlError::new(start, "stray '^' (expected an operator)"));
            }
            '~' => {
                if input[i..].starts_with("~") {
                    out.push(Token {
                        kind: TokenKind::Op(BinOp::Contains),
                        offset: start,
                    });
                    i += 1;
                    continue;
                }
                return Err(BqlError::new(start, "stray '~' (expected an operator)"));
            }
            ')' => {
                out.push(Token {
                    kind: TokenKind::RParen,
                    offset: start,
                });
                i += 1;
            }
            '"' => {
                let (s, end) = lex_string(input, start)?;
                out.push(Token {
                    kind: TokenKind::Str(s),
                    offset: start,
                });
                i = end;
            }
            _ => {
                if !is_word_char(c) {
                    return Err(BqlError::new(start, format!("unexpected character `{c}`")));
                }
                let (w, end) = lex_word(input, i);
                out.push(Token {
                    kind: TokenKind::Word(w),
                    offset: start,
                });
                i = end;
            }
        }
    }
    out.push(Token {
        kind: TokenKind::Eof,
        offset: n,
    });
    Ok(out)
}

fn is_word_char(c: char) -> bool {
    matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' | ':' | '.' | '+')
}

fn is_whitespace_char(c: char) -> bool {
    matches!(c, ' ' | '\t' | '\r' | '\n')
}

#[allow(clippy::needless_lifetimes)]
fn lex_word<'a>(input: &'a str, start: usize) -> (&'a str, usize) {
    let mut end = start;
    for (off, c) in input[start..].char_indices() {
        if is_word_char(c) {
            end = start + off + c.len_utf8();
        } else {
            break;
        }
    }
    (&input[start..end], end)
}

#[allow(clippy::needless_lifetimes)]
fn lex_string<'a>(input: &'a str, start: usize) -> BqlResult<(&'a str, usize)> {
    let mut i = start + 1;
    let n = input.len();
    while i < n {
        let c = input[i..].chars().next().expect("in-bounds");
        if c == '"' {
            return Ok((&input[start + 1..i], i + 1));
        }
        if c == '\\' {
            i += 1;
            if i >= n {
                return Err(BqlError::new(start, "unterminated string literal"));
            }
            let e = input[i..].chars().next().expect("in-bounds");
            match e {
                '"' | '\\' | '/' | 'n' | 't' | 'r' | 'b' | 'f' => {}
                'u' => {
                    let hex: Vec<char> = input[i + 1..].chars().take(4).collect();
                    if hex.len() < 4 {
                        return Err(BqlError::new(
                            start,
                            "incomplete `\\u` escape in string literal",
                        ));
                    }
                    if !hex.iter().all(char::is_ascii_hexdigit) {
                        return Err(BqlError::new(
                            start,
                            "invalid `\\u` escape in string literal",
                        ));
                    }
                    i += 5;
                    continue;
                }
                other => {
                    return Err(BqlError::new(
                        i,
                        format!("invalid escape sequence `\\{other}` in string literal"),
                    ));
                }
            }
            i += e.len_utf8();
        } else {
            i += c.len_utf8();
        }
    }
    Err(BqlError::new(start, "unterminated string literal"))
}