use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Piece {
pub text: String,
pub space_before: bool,
}
const PUNCTUATORS: [&str; 23] = [
"...", "<<=", ">>=", "##", "::", "<<", ">>", "<=", ">=", "==", "!=", "&&", "||", "+=", "-=",
"*=", "/=", "%=", "&=", "|=", "^=", "->", "++",
];
pub(crate) fn tokenize(text: &str) -> Result<Vec<Piece>, String> {
let bytes = text.as_bytes();
let mut out = Vec::new();
let mut at = 0;
let mut space = false;
while at < bytes.len() {
let byte = bytes[at];
if byte.is_ascii_whitespace() {
space = true;
at += 1;
continue;
}
if byte == b'/' && bytes.get(at + 1) == Some(&b'/') {
while at < bytes.len() && bytes[at] != b'\n' {
at += 1;
}
space = true;
continue;
}
if byte == b'/' && bytes.get(at + 1) == Some(&b'*') {
let Some(end) = text[at + 2..].find("*/") else {
return Err("a comment that does not end".to_string());
};
at += 2 + end + 2;
space = true;
continue;
}
let start = at;
if byte == b'L' && matches!(bytes.get(at + 1), Some(b'\'' | b'"')) {
at = quoted(bytes, at + 1)?;
} else if is_identifier_start(byte) {
while at < bytes.len() && is_identifier_continue(bytes[at]) {
at += 1;
}
} else if byte.is_ascii_digit()
|| (byte == b'.' && bytes.get(at + 1).is_some_and(u8::is_ascii_digit))
{
at += 1;
while at < bytes.len() {
let next = bytes[at];
let signed_exponent =
matches!(next, b'+' | b'-') && matches!(bytes[at - 1], b'e' | b'E');
if signed_exponent || next.is_ascii_alphanumeric() || matches!(next, b'_' | b'.') {
at += 1;
} else {
break;
}
}
} else if matches!(byte, b'\'' | b'"') {
at = quoted(bytes, at)?;
} else if let Some(punctuator) = PUNCTUATORS
.iter()
.find(|candidate| text[at..].starts_with(*candidate))
{
at += punctuator.len();
} else {
at += text[at..].chars().next().map_or(1, char::len_utf8);
}
out.push(Piece {
text: text[start..at].to_string(),
space_before: space,
});
space = false;
}
Ok(out)
}
fn quoted(bytes: &[u8], at: usize) -> Result<usize, String> {
let quote = bytes[at];
let mut index = at + 1;
while index < bytes.len() {
match bytes[index] {
b'\\' => index += 2,
b'\n' => break,
found if found == quote => return Ok(index + 1),
_ => index += 1,
}
}
Err(if quote == b'"' {
"a string literal that does not end".to_string()
} else {
"a character literal that does not end".to_string()
})
}
const fn is_identifier_start(byte: u8) -> bool {
byte.is_ascii_alphabetic() || byte == b'_' || byte >= 0x80
}
const fn is_identifier_continue(byte: u8) -> bool {
is_identifier_start(byte) || byte.is_ascii_digit()
}
pub(crate) fn is_identifier(text: &str) -> bool {
let mut bytes = text.bytes();
bytes.next().is_some_and(is_identifier_start) && bytes.all(is_identifier_continue)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn texts(text: &str) -> Vec<String> {
tokenize(text)
.unwrap()
.into_iter()
.map(|p| p.text)
.collect()
}
#[test]
fn operators_are_taken_longest_first() {
assert_eq!(texts("a##b"), ["a", "##", "b"]);
assert_eq!(texts("#x"), ["#", "x"]);
assert_eq!(
texts("A&&!B||C==1"),
["A", "&&", "!", "B", "||", "C", "==", "1"]
);
assert_eq!(texts("x<<=2"), ["x", "<<=", "2"]);
}
#[test]
fn a_pp_number_is_wider_than_a_literal() {
assert_eq!(texts("1e+5 0x1F 1.5d .5"), ["1e+5", "0x1F", "1.5d", ".5"]);
assert_eq!(texts("1-2"), ["1", "-", "2"]);
}
#[test]
fn literals_and_comments() {
assert_eq!(
texts(r#""a\"b" L"w" 'c' L'd'"#),
[r#""a\"b""#, "L\"w\"", "'c'", "L'd'"]
);
let pieces = tokenize("a /* c */ b // tail").unwrap();
assert_eq!(pieces.len(), 2);
assert!(pieces[1].space_before);
assert!(tokenize("\"open").is_err());
assert!(tokenize("/* open").is_err());
}
#[test]
fn identifiers() {
assert!(is_identifier("_A1"));
assert!(!is_identifier("1A"));
assert!(!is_identifier(""));
}
}