use crate::keywords::KEYWORDS;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenKind {
Ws,
Equal,
LParens,
RParens,
Comma,
Semicolon,
BitFormat,
HexaFormat,
DQuoteString,
SQuoteString,
Number,
IdentifierQuoted,
IdentifierUnquoted,
Keyword(String),
Unknown,
}
#[derive(Debug, Clone)]
pub struct Token {
pub kind: TokenKind,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct LexError {
pub line: usize,
}
pub fn tokenize(input: &str) -> Result<Vec<Token>, LexError> {
let chars: Vec<char> = input.chars().collect();
let mut pos = 0usize;
let mut tokens = Vec::new();
while pos < chars.len() {
match next_token(&chars, pos) {
Some((token, len)) => {
pos += len;
tokens.push(token);
}
None => {
let line = 1 + chars[..pos].iter().filter(|&&c| c == '\n').count();
return Err(LexError { line });
}
}
}
Ok(tokens)
}
fn next_token(chars: &[char], start: usize) -> Option<(Token, usize)> {
for (name, letters) in KEYWORDS.iter() {
if let Some(len) = match_keyword(chars, start, letters) {
let value: String = chars[start..start + len].iter().collect();
return Some((
Token {
kind: TokenKind::Keyword((*name).to_string()),
value,
},
len,
));
}
}
if let Some(len) = match_ws(chars, start) {
return Some((tok(TokenKind::Ws, chars, start, len), len));
}
if chars[start] == '=' {
return Some((tok(TokenKind::Equal, chars, start, 1), 1));
}
if chars[start] == '(' {
return Some((tok(TokenKind::LParens, chars, start, 1), 1));
}
if chars[start] == ')' {
return Some((tok(TokenKind::RParens, chars, start, 1), 1));
}
if chars[start] == ',' {
return Some((tok(TokenKind::Comma, chars, start, 1), 1));
}
if chars[start] == ';' {
return Some((tok(TokenKind::Semicolon, chars, start, 1), 1));
}
if let Some(len) = match_bit(chars, start) {
return Some((tok(TokenKind::BitFormat, chars, start, len), len));
}
if let Some(len) = match_hexa(chars, start) {
return Some((tok(TokenKind::HexaFormat, chars, start, len), len));
}
if let Some((len, value)) = match_dquote(chars, start) {
return Some((
Token {
kind: TokenKind::DQuoteString,
value,
},
len,
));
}
if let Some((len, value)) = match_squote(chars, start) {
return Some((
Token {
kind: TokenKind::SQuoteString,
value,
},
len,
));
}
if let Some(len) = match_number(chars, start) {
return Some((tok(TokenKind::Number, chars, start, len), len));
}
if let Some((len, value)) = match_ident_quoted(chars, start) {
return Some((
Token {
kind: TokenKind::IdentifierQuoted,
value,
},
len,
));
}
if let Some(len) = match_ident_unquoted(chars, start) {
return Some((tok(TokenKind::IdentifierUnquoted, chars, start, len), len));
}
let mut end = start;
while end < chars.len() && chars[end] != '\n' && chars[end] != '\r' {
end += 1;
}
let unknown_len = (end - start).max(1);
Some((
tok(TokenKind::Unknown, chars, start, unknown_len),
unknown_len,
))
}
fn tok(kind: TokenKind, chars: &[char], start: usize, len: usize) -> Token {
Token {
kind,
value: chars[start..start + len].iter().collect(),
}
}
fn match_keyword(chars: &[char], start: usize, letters: &[char]) -> Option<usize> {
let n = letters.len();
if start + n > chars.len() {
return None;
}
for (i, want) in letters.iter().enumerate() {
let got = chars[start + i];
if !got.eq_ignore_ascii_case(want) {
return None;
}
}
if start > 0 && is_word(chars[start - 1]) && is_word(letters[0]) {
return None;
}
let after = start + n;
if after < chars.len() && is_word(chars[after]) && is_word(letters[n - 1]) {
return None;
}
Some(n)
}
fn is_word(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
fn match_ws(chars: &[char], start: usize) -> Option<usize> {
let mut pos = start;
loop {
let before = pos;
while pos < chars.len() && chars[pos].is_whitespace() {
pos += 1;
}
if pos < chars.len() && chars[pos] == '#' {
while pos < chars.len() && chars[pos] != '\n' && chars[pos] != '\r' {
pos += 1;
}
}
if pos + 2 < chars.len()
&& chars[pos] == '-'
&& chars[pos + 1] == '-'
&& chars[pos + 2] == ' '
{
pos += 2;
while pos < chars.len() && chars[pos] == ' ' {
pos += 1;
}
while pos < chars.len() && chars[pos] != '\n' && chars[pos] != '\r' {
pos += 1;
}
}
if pos + 1 < chars.len() && chars[pos] == '/' && chars[pos + 1] == '*' {
let mut end = pos + 2;
while end + 1 < chars.len() && !(chars[end] == '*' && chars[end + 1] == '/') {
end += 1;
}
if end + 1 < chars.len() {
pos = end + 2;
}
}
if pos == before {
break;
}
}
if pos > start {
Some(pos - start)
} else {
None
}
}
fn match_bit(chars: &[char], start: usize) -> Option<usize> {
if chars[start] == 'b' && start + 1 < chars.len() && chars[start + 1] == '\'' {
let mut pos = start + 2;
let digits_start = pos;
while pos < chars.len() && (chars[pos] == '0' || chars[pos] == '1') {
pos += 1;
}
if pos > digits_start && pos < chars.len() && chars[pos] == '\'' {
return Some(pos + 1 - start);
}
}
if chars[start] == '0' && start + 1 < chars.len() && chars[start + 1] == 'b' {
let mut pos = start + 2;
let digits_start = pos;
while pos < chars.len() && (chars[pos] == '0' || chars[pos] == '1') {
pos += 1;
}
if pos > digits_start {
return Some(pos - start);
}
}
None
}
fn match_hexa(chars: &[char], start: usize) -> Option<usize> {
if (chars[start] == 'X' || chars[start] == 'x')
&& start + 1 < chars.len()
&& chars[start + 1] == '\''
{
let mut pos = start + 2;
let digits_start = pos;
while pos < chars.len() && chars[pos].is_ascii_hexdigit() {
pos += 1;
}
if pos > digits_start && pos < chars.len() && chars[pos] == '\'' {
return Some(pos + 1 - start);
}
}
if chars[start] == '0' && start + 1 < chars.len() && chars[start + 1] == 'x' {
let mut pos = start + 2;
let digits_start = pos;
while pos < chars.len() && chars[pos].is_ascii_hexdigit() {
pos += 1;
}
if pos > digits_start {
return Some(pos - start);
}
}
None
}
fn match_dquote(chars: &[char], start: usize) -> Option<(usize, String)> {
match_quoted(chars, start, '"')
}
fn match_squote(chars: &[char], start: usize) -> Option<(usize, String)> {
match_quoted(chars, start, '\'')
}
fn match_quoted(chars: &[char], start: usize, q: char) -> Option<(usize, String)> {
if chars[start] != q {
return None;
}
if start + 1 < chars.len() && chars[start + 1] == q {
if let Some(res) = scan_quoted_body(chars, start, q) {
return Some(res);
}
return Some((2, String::new()));
}
scan_quoted_body(chars, start, q)
}
fn scan_quoted_body(chars: &[char], start: usize, q: char) -> Option<(usize, String)> {
let mut pos = start + 1;
let mut value = String::new();
while pos < chars.len() {
let c = chars[pos];
if c == q {
if pos + 1 < chars.len() && chars[pos + 1] == q {
value.push(q);
pos += 2;
continue;
}
return Some((pos + 1 - start, value));
}
if c == '\\' {
if pos + 1 < chars.len() {
let next = chars[pos + 1];
if next == q {
value.push(q);
} else {
value.push('\\');
value.push(next);
}
pos += 2;
continue;
}
return None;
}
value.push(c);
pos += 1;
}
None
}
fn match_number(chars: &[char], start: usize) -> Option<usize> {
let mut pos = start;
if pos < chars.len() && (chars[pos] == '+' || chars[pos] == '-') {
pos += 1;
}
let int_start = pos;
while pos < chars.len() && chars[pos].is_ascii_digit() {
pos += 1;
}
if pos == int_start {
return None;
}
if pos < chars.len() && chars[pos] == '.' {
let mut p = pos + 1;
let frac_start = p;
while p < chars.len() && chars[p].is_ascii_digit() {
p += 1;
}
if p > frac_start {
pos = p;
}
}
if pos < chars.len() && (chars[pos] == 'e' || chars[pos] == 'E') {
let mut p = pos + 1;
if p < chars.len() && (chars[p] == '+' || chars[p] == '-') {
p += 1;
}
let exp_start = p;
while p < chars.len() && chars[p].is_ascii_digit() {
p += 1;
}
if p > exp_start {
pos = p;
}
}
Some(pos - start)
}
fn match_ident_quoted(chars: &[char], start: usize) -> Option<(usize, String)> {
if chars[start] != '`' {
return None;
}
let mut pos = start + 1;
let mut value = String::new();
while pos < chars.len() {
let c = chars[pos];
if c == '`' {
if pos + 1 < chars.len() && chars[pos + 1] == '`' {
value.push('`');
pos += 2;
continue;
}
return Some((pos + 1 - start, value));
}
if c == '\\' {
return None;
}
value.push(c);
pos += 1;
}
None
}
fn match_ident_unquoted(chars: &[char], start: usize) -> Option<usize> {
let mut pos = start;
while pos < chars.len() {
let c = chars[pos];
if c.is_ascii_alphanumeric() || c == '$' || c == '_' {
pos += 1;
} else {
break;
}
}
if pos > start {
Some(pos - start)
} else {
None
}
}