use super::{MathParseErrorKind, RawParseError};
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum TokenKind {
Number(f64),
Ident,
Str(String),
Op(char),
Open(char),
Close(char),
Comma,
Dot,
End,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Token<'a> {
pub(crate) kind: TokenKind,
pub(crate) start: usize,
pub(crate) text: &'a str,
pub(crate) space_before: bool,
}
const OPERATOR_CHARS: &[char] = &['+', '-', '*', '/', '%', '^'];
pub(crate) fn tokenize(src: &str) -> Result<Vec<Token<'_>>, RawParseError> {
let bytes = src.as_bytes();
let mut out = Vec::new();
let mut pos = 0;
loop {
let ws_start = pos;
while let Some(c) = src[pos..].chars().next().filter(|c| c.is_whitespace()) {
pos += c.len_utf8();
}
let space_before = pos > ws_start;
let start = pos;
let Some(c) = src[pos..].chars().next() else {
out.push(Token {
kind: TokenKind::End,
start,
text: "",
space_before,
});
return Ok(out);
};
let next_is_digit = bytes.get(pos + 1).is_some_and(u8::is_ascii_digit);
let kind = if c.is_ascii_digit() || (c == '.' && next_is_digit) {
pos = scan_number(bytes, pos);
let text = &src[start..pos];
match text.parse::<f64>() {
Ok(v) => TokenKind::Number(v),
Err(_) => {
return Err(RawParseError::new(
start,
MathParseErrorKind::InvalidNumber(text.into()),
));
}
}
} else if c.is_ascii_alphabetic() || c == '_' {
pos += 1;
while bytes
.get(pos)
.is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_')
{
pos += 1;
}
TokenKind::Ident
} else if c == 'π' || c == 'φ' {
pos += c.len_utf8();
TokenKind::Ident
} else if c == '"' || c == '\'' {
let (content, end) = scan_string(src, pos, c)?;
pos = end;
TokenKind::Str(content)
} else {
pos += c.len_utf8();
match c {
'(' | '[' | '{' => TokenKind::Open(c),
')' | ']' | '}' => TokenKind::Close(c),
',' => TokenKind::Comma,
'.' => TokenKind::Dot,
c if OPERATOR_CHARS.contains(&c) => TokenKind::Op(c),
c => {
return Err(RawParseError::new(
start,
MathParseErrorKind::UnexpectedChar(c),
));
}
}
};
out.push(Token {
kind,
start,
text: &src[start..pos],
space_before,
});
}
}
fn scan_number(bytes: &[u8], start: usize) -> usize {
let mut pos = start + 1;
while let Some(&b) = bytes.get(pos) {
let after_e = matches!(bytes.get(pos - 1), Some(b'e' | b'E'));
if b.is_ascii_digit()
|| matches!(b, b'.' | b'e' | b'E')
|| (after_e && matches!(b, b'+' | b'-'))
{
pos += 1;
} else {
break;
}
}
if matches!(bytes.get(pos - 1), Some(b'e' | b'E')) {
pos -= 1;
}
pos
}
fn scan_string(src: &str, start: usize, quote: char) -> Result<(String, usize), RawParseError> {
let mut content = String::new();
let mut chars = src[start + 1..].char_indices();
while let Some((i, c)) = chars.next() {
let at = start + 1 + i;
if c == quote {
return Ok((content, at + 1));
}
if c == '\\' {
match chars.next() {
Some((_, e @ ('\\' | '"' | '\''))) => content.push(e),
Some((_, e)) => {
return Err(RawParseError::new(at, MathParseErrorKind::InvalidEscape(e)));
}
None => break,
}
} else {
content.push(c);
}
}
Err(RawParseError::new(
start,
MathParseErrorKind::UnterminatedString,
))
}