#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub line: usize,
pub col: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Token {
pub kind: TokenKind,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TokenKind {
Ident(String),
Const,
Input,
Extern,
Shared,
Volatile,
Cursor,
Over,
Pragma,
For(String),
Tile,
TileBody(String, crate::ast::TileBodyKind),
Dot,
IntLit(u64),
FloatLit(f64),
StringLit(String),
ColonEq,
Eq,
LParen,
RParen,
LBracket,
RBracket,
LBrace,
RBrace,
Comma,
Colon,
Arrow,
Plus,
Minus,
Star,
Slash,
Percent,
Caret,
StarStar,
ShiftLeft,
ShiftRight,
Ampersand,
AmpAmp,
Pipe,
PipePipe,
Bang,
Lt,
Gt,
EqEq,
BangEq,
LtEq,
GtEq,
Eof,
}
pub fn lex(source: &str) -> Result<Vec<Token>, String> {
let mut tokens = Vec::new();
let chars: Vec<char> = source.chars().collect();
let mut pos = 0;
let mut line = 1;
let mut col = 1;
let mut tile_pending = false;
while pos < chars.len() {
let c = chars[pos];
if c == '\n' {
line += 1;
col = 1;
pos += 1;
tile_pending = false;
continue;
}
if c.is_ascii_whitespace() {
col += 1;
pos += 1;
continue;
}
if c == '#' {
while pos < chars.len() && chars[pos] != '\n' {
pos += 1;
}
continue;
}
if c == '/' && pos + 1 < chars.len() {
if chars[pos + 1] == '/' {
while pos < chars.len() && chars[pos] != '\n' {
pos += 1;
}
continue;
}
if chars[pos + 1] == '*' {
pos += 2;
col += 2;
while pos + 1 < chars.len() {
if chars[pos] == '\n' {
line += 1;
col = 1;
}
if chars[pos] == '*' && chars[pos + 1] == '/' {
pos += 2;
col += 2;
break;
}
pos += 1;
col += 1;
}
continue;
}
}
if c == '/' {
let span = Span { line, col };
tokens.push(Token {
kind: TokenKind::Slash,
span,
});
pos += 1;
col += 1;
continue;
}
let span = Span { line, col };
if !tile_pending
&& chars[pos..].starts_with(&['<', '<', '<'])
&& let Some((body, _, consumed, newlines, end_col)) =
capture_tile_body(&chars, pos, col)?
{
tokens.push(Token {
kind: TokenKind::StringLit(body),
span,
});
pos += consumed;
if newlines > 0 {
line += newlines;
col = end_col;
} else {
col += consumed;
}
continue;
}
if c == ':' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
tokens.push(Token {
kind: TokenKind::ColonEq,
span,
});
pos += 2;
col += 2;
if tile_pending {
tile_pending = false;
if let Some((body, kind, consumed, newlines, end_col)) =
capture_tile_body(&chars, pos, col)?
{
tokens.push(Token {
kind: TokenKind::TileBody(body, kind),
span: Span { line, col },
});
pos += consumed;
if newlines > 0 {
line += newlines;
col = end_col;
} else {
col += consumed;
}
}
}
continue;
}
if c == '-' && pos + 1 < chars.len() && chars[pos + 1] == '>' {
tokens.push(Token {
kind: TokenKind::Arrow,
span,
});
pos += 2;
col += 2;
continue;
}
if c.is_ascii_digit() {
let start = pos;
if pos + 1 < chars.len()
&& chars[pos] == '0'
&& (chars[pos + 1] == 'x' || chars[pos + 1] == 'X')
{
pos += 2;
col += 2;
let hex_start = pos;
while pos < chars.len() && chars[pos].is_ascii_hexdigit() {
pos += 1;
col += 1;
}
let hex: String = chars[hex_start..pos].iter().collect();
let val = u64::from_str_radix(&hex, 16).map_err(|e| {
format!(
"invalid hex literal at line {}, col {}: {e}",
span.line, span.col
)
})?;
tokens.push(Token {
kind: TokenKind::IntLit(val),
span,
});
continue;
}
while pos < chars.len() && chars[pos].is_ascii_digit() {
pos += 1;
col += 1;
}
let mut is_float = false;
if pos < chars.len()
&& chars[pos] == '.'
&& pos + 1 < chars.len()
&& chars[pos + 1].is_ascii_digit()
{
is_float = true;
pos += 1;
col += 1;
while pos < chars.len() && chars[pos].is_ascii_digit() {
pos += 1;
col += 1;
}
if pos < chars.len() && (chars[pos] == 'e' || chars[pos] == 'E') {
pos += 1;
col += 1;
if pos < chars.len() && (chars[pos] == '+' || chars[pos] == '-') {
pos += 1;
col += 1;
}
while pos < chars.len() && chars[pos].is_ascii_digit() {
pos += 1;
col += 1;
}
}
} else if pos < chars.len() && (chars[pos] == 'e' || chars[pos] == 'E') {
is_float = true;
pos += 1;
col += 1;
if pos < chars.len() && (chars[pos] == '+' || chars[pos] == '-') {
pos += 1;
col += 1;
}
while pos < chars.len() && chars[pos].is_ascii_digit() {
pos += 1;
col += 1;
}
}
let suffix_consumed = match peek_si_suffix(&chars, pos) {
Some((multiplier, len, is_subunit)) => {
pos += len;
col += len;
Some((multiplier, is_subunit))
}
None => None,
};
if is_float {
let num: String = chars[start..pos - suffix_len_consumed(suffix_consumed)]
.iter()
.collect();
let mut val: f64 = num.parse().map_err(|e| {
format!("invalid float at line {}, col {}: {e}", span.line, span.col)
})?;
if let Some((mult, is_sub)) = suffix_consumed {
if is_sub {
val /= mult as f64;
} else {
val *= mult as f64;
}
}
if suffix_consumed.is_some()
&& val.fract() == 0.0
&& val >= 0.0
&& val <= u64::MAX as f64
{
tokens.push(Token {
kind: TokenKind::IntLit(val as u64),
span,
});
} else {
tokens.push(Token {
kind: TokenKind::FloatLit(val),
span,
});
}
} else {
let num_end = pos - suffix_len_consumed(suffix_consumed);
let num: String = chars[start..num_end]
.iter()
.filter(|c| **c != '_')
.collect();
let val: u64 = num.parse().map_err(|e| {
format!(
"invalid integer at line {}, col {}: {e}",
span.line, span.col
)
})?;
match suffix_consumed {
Some((mult, true)) => {
let f = val as f64 / mult as f64;
tokens.push(Token {
kind: TokenKind::FloatLit(f),
span,
});
}
Some((mult, false)) => {
let v = val.checked_mul(mult).ok_or_else(|| {
format!(
"integer literal with SI suffix overflows u64 at line {}, col {}",
span.line, span.col
)
})?;
tokens.push(Token {
kind: TokenKind::IntLit(v),
span,
});
}
None => {
tokens.push(Token {
kind: TokenKind::IntLit(val),
span,
});
}
}
}
continue;
}
match c {
'(' => {
tokens.push(Token {
kind: TokenKind::LParen,
span,
});
pos += 1;
col += 1;
continue;
}
')' => {
tokens.push(Token {
kind: TokenKind::RParen,
span,
});
pos += 1;
col += 1;
continue;
}
'[' => {
tokens.push(Token {
kind: TokenKind::LBracket,
span,
});
pos += 1;
col += 1;
continue;
}
']' => {
tokens.push(Token {
kind: TokenKind::RBracket,
span,
});
pos += 1;
col += 1;
continue;
}
'{' => {
tokens.push(Token {
kind: TokenKind::LBrace,
span,
});
pos += 1;
col += 1;
continue;
}
'}' => {
tokens.push(Token {
kind: TokenKind::RBrace,
span,
});
pos += 1;
col += 1;
continue;
}
',' => {
tokens.push(Token {
kind: TokenKind::Comma,
span,
});
pos += 1;
col += 1;
continue;
}
'=' => {
if pos + 1 < chars.len() && chars[pos + 1] == '=' {
tokens.push(Token {
kind: TokenKind::EqEq,
span,
});
pos += 2;
col += 2;
} else {
tokens.push(Token {
kind: TokenKind::Eq,
span,
});
pos += 1;
col += 1;
}
continue;
}
':' => {
tokens.push(Token {
kind: TokenKind::Colon,
span,
});
pos += 1;
col += 1;
continue;
}
'+' => {
tokens.push(Token {
kind: TokenKind::Plus,
span,
});
pos += 1;
col += 1;
continue;
}
'-' => {
tokens.push(Token {
kind: TokenKind::Minus,
span,
});
pos += 1;
col += 1;
continue;
}
'*' => {
if pos + 1 < chars.len() && chars[pos + 1] == '*' {
tokens.push(Token {
kind: TokenKind::StarStar,
span,
});
pos += 2;
col += 2;
} else {
tokens.push(Token {
kind: TokenKind::Star,
span,
});
pos += 1;
col += 1;
}
continue;
}
'%' => {
tokens.push(Token {
kind: TokenKind::Percent,
span,
});
pos += 1;
col += 1;
continue;
}
'^' => {
tokens.push(Token {
kind: TokenKind::Caret,
span,
});
pos += 1;
col += 1;
continue;
}
'<' => {
if pos + 1 < chars.len() && chars[pos + 1] == '<' {
tokens.push(Token {
kind: TokenKind::ShiftLeft,
span,
});
pos += 2;
col += 2;
} else if pos + 1 < chars.len() && chars[pos + 1] == '=' {
tokens.push(Token {
kind: TokenKind::LtEq,
span,
});
pos += 2;
col += 2;
} else {
tokens.push(Token {
kind: TokenKind::Lt,
span,
});
pos += 1;
col += 1;
}
continue;
}
'>' => {
if pos + 1 < chars.len() && chars[pos + 1] == '>' {
tokens.push(Token {
kind: TokenKind::ShiftRight,
span,
});
pos += 2;
col += 2;
} else if pos + 1 < chars.len() && chars[pos + 1] == '=' {
tokens.push(Token {
kind: TokenKind::GtEq,
span,
});
pos += 2;
col += 2;
} else {
tokens.push(Token {
kind: TokenKind::Gt,
span,
});
pos += 1;
col += 1;
}
continue;
}
'.' => {
tokens.push(Token {
kind: TokenKind::Dot,
span,
});
pos += 1;
col += 1;
continue;
}
'&' => {
if pos + 1 < chars.len() && chars[pos + 1] == '&' {
tokens.push(Token {
kind: TokenKind::AmpAmp,
span,
});
pos += 2;
col += 2;
continue;
}
tokens.push(Token {
kind: TokenKind::Ampersand,
span,
});
pos += 1;
col += 1;
continue;
}
'|' => {
if pos + 1 < chars.len() && chars[pos + 1] == '|' {
tokens.push(Token {
kind: TokenKind::PipePipe,
span,
});
pos += 2;
col += 2;
continue;
}
tokens.push(Token {
kind: TokenKind::Pipe,
span,
});
pos += 1;
col += 1;
continue;
}
'!' => {
if pos + 1 < chars.len() && chars[pos + 1] == '=' {
tokens.push(Token {
kind: TokenKind::BangEq,
span,
});
pos += 2;
col += 2;
} else {
tokens.push(Token {
kind: TokenKind::Bang,
span,
});
pos += 1;
col += 1;
}
continue;
}
_ => {}
}
if c == '"' || c == '\'' {
let quote = c;
pos += 1;
col += 1;
let mut s = String::new();
while pos < chars.len() && chars[pos] != quote {
if chars[pos] == '\\' && pos + 1 < chars.len() {
pos += 1;
col += 1;
match chars[pos] {
'n' => s.push('\n'),
't' => s.push('\t'),
'\\' => s.push('\\'),
c if c == quote => s.push(c),
other => {
s.push('\\');
s.push(other);
}
}
} else {
s.push(chars[pos]);
}
pos += 1;
col += 1;
}
if pos < chars.len() {
pos += 1; col += 1;
} else {
return Err(format!(
"unterminated string at line {}, col {}",
span.line, span.col
));
}
tokens.push(Token {
kind: TokenKind::StringLit(s),
span,
});
continue;
}
if c.is_ascii_alphabetic() || c == '_' {
let start = pos;
while pos < chars.len() && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_') {
pos += 1;
col += 1;
}
let word: String = chars[start..pos].iter().collect();
let kind = match word.as_str() {
"const" => TokenKind::Const,
"input" => TokenKind::Input,
"extern" => TokenKind::Extern,
"shared" => TokenKind::Shared,
"volatile" => TokenKind::Volatile,
"cursor" => TokenKind::Cursor,
"over" => TokenKind::Over,
"pragma" => TokenKind::Pragma,
"tile" => {
tile_pending = true;
TokenKind::Tile
}
"for" => {
let (text, consumed) = capture_for_text(&chars, pos);
pos += consumed;
col += consumed;
tokens.push(Token {
kind: TokenKind::For(text),
span,
});
continue;
}
_ => TokenKind::Ident(word),
};
tokens.push(Token { kind, span });
continue;
}
return Err(format!(
"unexpected character '{}' at line {}, col {}",
c, line, col
));
}
tokens.push(Token {
kind: TokenKind::Eof,
span: Span { line, col },
});
Ok(tokens)
}
type TileBodyCapture = (String, crate::ast::TileBodyKind, usize, usize, usize);
fn capture_tile_body(
chars: &[char],
start: usize,
start_col: usize,
) -> Result<Option<TileBodyCapture>, String> {
use crate::ast::TileBodyKind;
let mut pos = start;
let mut col = start_col;
let mut newlines = 0;
while pos < chars.len() && chars[pos].is_whitespace() {
if chars[pos] == '\n' {
newlines += 1;
col = 1;
} else {
col += 1;
}
pos += 1;
}
if pos >= chars.len() {
return Ok(None);
}
let body_start = pos;
let kind;
match chars[pos] {
'{' | '[' => {
kind = TileBodyKind::Block;
let mut depth = 0i32;
let mut quote: Option<char> = None;
loop {
if pos >= chars.len() {
return Err("unterminated tile body: block never closed".to_string());
}
let c = chars[pos];
if c == '\n' {
newlines += 1;
col = 0;
}
if let Some(q) = quote {
if c == '\\' && pos + 1 < chars.len() {
pos += 2;
col += 2;
continue;
}
if c == q {
quote = None;
}
} else {
match c {
'"' => quote = Some(c),
'{' | '[' => depth += 1,
'}' | ']' => depth -= 1,
_ => {}
}
}
pos += 1;
col += 1;
if depth == 0 && quote.is_none() {
break;
}
}
}
'<' if chars[pos..].starts_with(&['<', '<', '<']) => {
kind = TileBodyKind::Heredoc;
pos += 3;
col += 3;
let text_start = pos;
loop {
if pos + 2 >= chars.len() {
return Err(
"unterminated tile body: heredoc never closed with `>>>`".to_string()
);
}
if chars[pos..].starts_with(&['>', '>', '>']) {
break;
}
if chars[pos] == '\n' {
newlines += 1;
col = 0;
}
pos += 1;
col += 1;
}
let mut text: String = chars[text_start..pos].iter().collect();
if let Some(t) = text.strip_prefix('\n') {
text = t.to_string();
}
if let Some(t) = text.strip_suffix('\n') {
text = t.to_string();
}
pos += 3;
col += 3;
return Ok(Some((text, kind, pos - start, newlines, col)));
}
_ => return Ok(None),
}
let text: String = chars[body_start..pos].iter().collect();
Ok(Some((
dedent_block(&text),
kind,
pos - start,
newlines,
col,
)))
}
fn dedent_block(text: &str) -> String {
let mut lines = text.split('\n');
let first = lines.next().unwrap_or("");
let rest: Vec<&str> = lines.collect();
let indent = rest
.iter()
.filter(|l| !l.trim().is_empty())
.map(|l| l.chars().take_while(|c| *c == ' ' || *c == '\t').count())
.min()
.unwrap_or(0);
if indent == 0 {
return text.to_string();
}
let mut out = String::with_capacity(text.len());
out.push_str(first);
for line in rest {
out.push('\n');
let skip = line
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.count()
.min(indent);
out.push_str(&line.chars().skip(skip).collect::<String>());
}
out
}
fn capture_for_text(chars: &[char], start: usize) -> (String, usize) {
let mut pos = start;
let mut depth = 0usize;
let mut text = String::new();
while pos < chars.len() {
let c = chars[pos];
match c {
'\n' => break,
'{' if depth == 0 => {
match placeholder_len(chars, pos) {
Some(n) => {
text.extend(chars[pos..pos + n].iter());
pos += n;
continue;
}
None => break,
}
}
'#' => break,
'/' if pos + 1 < chars.len() && chars[pos + 1] == '/' => break,
'"' | '\'' => {
let quote = c;
text.push(c);
pos += 1;
while pos < chars.len() && chars[pos] != quote && chars[pos] != '\n' {
if chars[pos] == '\\' && pos + 1 < chars.len() {
text.push(chars[pos]);
pos += 1;
}
text.push(chars[pos]);
pos += 1;
}
if pos < chars.len() && chars[pos] == quote {
text.push(quote);
pos += 1;
}
continue;
}
'(' | '[' => depth += 1,
')' | ']' => depth = depth.saturating_sub(1),
_ => {}
}
text.push(c);
pos += 1;
}
(text.trim().to_string(), pos - start)
}
fn placeholder_len(chars: &[char], pos: usize) -> Option<usize> {
let mut i = pos + 1;
let first = *chars.get(i)?;
if !(first.is_ascii_alphabetic() || first == '_') {
return None;
}
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
i += 1;
}
(chars.get(i) == Some(&'}')).then_some(i + 1 - pos)
}
fn peek_si_suffix(chars: &[char], pos: usize) -> Option<(u64, usize, bool)> {
if pos >= chars.len() {
return None;
}
if pos + 1 < chars.len() && chars[pos + 1] == 'i' {
let mult = match chars[pos] {
'K' => 1u64 << 10,
'M' => 1u64 << 20,
'G' => 1u64 << 30,
'T' => 1u64 << 40,
'P' => 1u64 << 50,
_ => 0,
};
if mult > 0 {
let next = chars.get(pos + 2);
if !next.is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_') {
return Some((mult, 2, false));
}
}
}
let (mult, is_subunit) = match chars[pos] {
'K' => (1_000u64, false),
'M' => (1_000_000u64, false),
'G' => (1_000_000_000u64, false),
'T' => (1_000_000_000_000u64, false),
'P' => (1_000_000_000_000_000u64, false),
'm' => (1_000u64, true), 'u' => (1_000_000u64, true), 'n' => (1_000_000_000u64, true), _ => return None,
};
let next = chars.get(pos + 1);
if !next.is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_') {
Some((mult, 1, is_subunit))
} else {
None
}
}
fn suffix_len_consumed(suffix: Option<(u64, bool)>) -> usize {
match suffix {
Some((m, _)) => {
if m.is_power_of_two() && m >= (1 << 10) {
2
} else {
1
}
}
None => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lex_cycle_binding() {
let tokens = lex("seed := hash(cycle)").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "seed"));
assert!(matches!(tokens[1].kind, TokenKind::ColonEq));
assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "hash"));
assert!(matches!(tokens[3].kind, TokenKind::LParen));
assert!(matches!(tokens[4].kind, TokenKind::Ident(ref s) if s == "cycle"));
assert!(matches!(tokens[5].kind, TokenKind::RParen));
}
#[test]
fn lex_const_binding() {
let tokens = lex("const lut := dist_normal(72.0, 5.0)").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Const));
assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "lut"));
assert!(matches!(tokens[2].kind, TokenKind::ColonEq));
assert!(matches!(tokens[3].kind, TokenKind::Ident(ref s) if s == "dist_normal"));
assert!(matches!(tokens[5].kind, TokenKind::FloatLit(v) if v == 72.0));
assert!(matches!(tokens[7].kind, TokenKind::FloatLit(v) if v == 5.0));
}
#[test]
fn lex_input_keyword_tuple() {
let tokens = lex("input (cycle: u64, thread: u64)").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Input));
assert!(matches!(tokens[1].kind, TokenKind::LParen));
assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "cycle"));
}
#[test]
fn lex_destructuring() {
let tokens = lex("(a, b, c) := mixed_radix(cycle, 100, 1000, 0)").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::LParen));
assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "a"));
}
#[test]
fn lex_string_with_interpolation() {
let tokens = lex(r#"id := "{code}-{seq}""#).unwrap();
assert!(matches!(tokens[2].kind, TokenKind::StringLit(ref s) if s == "{code}-{seq}"));
}
#[test]
fn lex_named_args() {
let tokens = lex("dist_normal(mean: 72.0, stddev: 5.0)").unwrap();
assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "mean"));
assert!(matches!(tokens[3].kind, TokenKind::Colon));
assert!(matches!(tokens[4].kind, TokenKind::FloatLit(v) if v == 72.0));
}
#[test]
fn lex_array_literal() {
let tokens = lex("[60.0, 20.0, 15.0, 5.0]").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::LBracket));
assert!(matches!(tokens[1].kind, TokenKind::FloatLit(v) if v == 60.0));
assert!(matches!(tokens[8].kind, TokenKind::RBracket));
}
#[test]
fn lex_comments_stripped() {
let tokens = lex("// this is a comment\nseed := hash(cycle)").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "seed"));
}
#[test]
fn lex_arrow() {
let tokens = lex("(x: u64) -> (y: u64)").unwrap();
assert!(matches!(tokens[5].kind, TokenKind::Arrow));
}
#[test]
fn lex_large_int() {
let tokens = lex("1710000000000").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::IntLit(1710000000000)));
}
#[test]
fn lex_hex_int() {
let tokens = lex("0xFF").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::IntLit(255)));
}
#[test]
fn lex_block_comment() {
let tokens = lex("a := /* skip this */ hash(b)").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
assert!(matches!(tokens[1].kind, TokenKind::ColonEq));
assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "hash"));
}
#[test]
fn lex_block_comment_multiline() {
let tokens = lex("a := 42\n/* this\nis\na\nblock */\nb := 7").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
assert!(matches!(tokens[2].kind, TokenKind::IntLit(42)));
assert!(matches!(tokens[3].kind, TokenKind::Ident(ref s) if s == "b"));
}
#[test]
fn lex_doc_comment() {
let tokens = lex("/// doc comment\nseed := hash(cycle)").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "seed"));
}
#[test]
fn lex_input_keyword_bare() {
let tokens = lex("input cycle: u64").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Input));
assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "cycle"));
assert!(matches!(tokens[2].kind, TokenKind::Colon));
assert!(matches!(tokens[3].kind, TokenKind::Ident(ref s) if s == "u64"));
}
#[test]
fn lex_arithmetic_operators() {
let tokens = lex("a + b * 2.0 - c / d % e ^ f").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
assert!(matches!(tokens[1].kind, TokenKind::Plus));
assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "b"));
assert!(matches!(tokens[3].kind, TokenKind::Star));
assert!(matches!(tokens[4].kind, TokenKind::FloatLit(v) if v == 2.0));
assert!(matches!(tokens[5].kind, TokenKind::Minus));
assert!(matches!(tokens[6].kind, TokenKind::Ident(ref s) if s == "c"));
assert!(matches!(tokens[7].kind, TokenKind::Slash));
assert!(matches!(tokens[8].kind, TokenKind::Ident(ref s) if s == "d"));
assert!(matches!(tokens[9].kind, TokenKind::Percent));
assert!(matches!(tokens[10].kind, TokenKind::Ident(ref s) if s == "e"));
assert!(matches!(tokens[11].kind, TokenKind::Caret));
assert!(matches!(tokens[12].kind, TokenKind::Ident(ref s) if s == "f"));
}
#[test]
fn lex_minus_binary_vs_negative_literal() {
let tokens = lex("x - 3").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "x"));
assert!(matches!(tokens[1].kind, TokenKind::Minus));
assert!(matches!(tokens[2].kind, TokenKind::IntLit(3)));
}
#[test]
fn lex_negative_via_minus_token() {
let tokens = lex("-3.0").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Minus));
assert!(matches!(tokens[1].kind, TokenKind::FloatLit(v) if v == 3.0));
let tokens = lex("-3").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Minus));
assert!(matches!(tokens[1].kind, TokenKind::IntLit(3)));
}
#[test]
fn lex_scientific_notation() {
let tokens = lex("1e10").unwrap();
assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if (*v - 1e10).abs() < 1e5));
}
#[test]
fn lex_scientific_notation_negative_exponent() {
let tokens = lex("1e-10").unwrap();
assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if *v > 0.0 && *v < 1e-5));
}
#[test]
fn lex_scientific_notation_with_decimal() {
let tokens = lex("2.5e3").unwrap();
assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if (*v - 2500.0).abs() < 0.1));
}
#[test]
fn lex_scientific_notation_positive_exponent() {
let tokens = lex("3E+5").unwrap();
assert!(matches!(&tokens[0].kind, TokenKind::FloatLit(v) if (*v - 3e5).abs() < 1.0));
}
#[test]
fn lex_sci_uppercase_e() {
let t = lex("2.5E3").unwrap();
assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 2500.0).abs() < 0.1));
}
#[test]
fn lex_sci_explicit_positive_exp() {
let t = lex("1e+10").unwrap();
assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 1e10).abs() < 1e5));
}
#[test]
fn lex_sci_decimal_negative_exp() {
let t = lex("3.14e-2").unwrap();
assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 0.0314).abs() < 0.001));
}
#[test]
fn lex_sci_zero_exponent() {
let t = lex("0.5e0").unwrap();
assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 0.5).abs() < 0.001));
}
#[test]
fn lex_sci_very_small() {
let t = lex("1e-300").unwrap();
assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if *v > 0.0 && *v < 1e-299));
}
#[test]
fn lex_sci_uppercase_no_decimal() {
let t = lex("1E10").unwrap();
assert!(matches!(&t[0].kind, TokenKind::FloatLit(v) if (*v - 1e10).abs() < 1e5));
}
#[test]
fn lex_slash_vs_comment() {
let tokens = lex("a / b // comment").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Ident(ref s) if s == "a"));
assert!(matches!(tokens[1].kind, TokenKind::Slash));
assert!(matches!(tokens[2].kind, TokenKind::Ident(ref s) if s == "b"));
assert!(matches!(tokens[3].kind, TokenKind::Eof));
}
#[test]
fn lex_si_decimal_k_m_g_t_p() {
let tokens = lex("1K 1M 1G 1T 1P").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::IntLit(1_000)));
assert!(matches!(tokens[1].kind, TokenKind::IntLit(1_000_000)));
assert!(matches!(tokens[2].kind, TokenKind::IntLit(1_000_000_000)));
assert!(matches!(
tokens[3].kind,
TokenKind::IntLit(1_000_000_000_000)
));
assert!(matches!(
tokens[4].kind,
TokenKind::IntLit(1_000_000_000_000_000)
));
}
#[test]
fn lex_si_binary_ki_mi_gi_ti_pi() {
let tokens = lex("1Ki 1Mi 1Gi 1Ti 1Pi").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::IntLit(1024)));
assert!(matches!(tokens[1].kind, TokenKind::IntLit(1_048_576)));
assert!(matches!(tokens[2].kind, TokenKind::IntLit(1_073_741_824)));
assert!(matches!(
tokens[3].kind,
TokenKind::IntLit(1_099_511_627_776)
));
assert!(matches!(
tokens[4].kind,
TokenKind::IntLit(1_125_899_906_842_624)
));
}
#[test]
fn lex_si_subunit_m_u_n() {
let tokens = lex("5m 5u 5n").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::FloatLit(v) if (v - 0.005).abs() < 1e-12));
assert!(matches!(tokens[1].kind, TokenKind::FloatLit(v) if (v - 0.000_005).abs() < 1e-15));
assert!(
matches!(tokens[2].kind, TokenKind::FloatLit(v) if (v - 0.000_000_005).abs() < 1e-18)
);
}
#[test]
fn lex_si_float_base_with_decimal_suffix() {
let tokens = lex("1.5K").unwrap();
assert!(
matches!(tokens[0].kind, TokenKind::IntLit(1_500)),
"1.5K should be IntLit(1500), got {:?}",
tokens[0].kind
);
}
#[test]
fn lex_si_float_base_non_integral_stays_float() {
let tokens = lex("1.5m").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::FloatLit(v) if (v - 0.0015).abs() < 1e-12));
}
#[test]
fn lex_si_kilometers_stays_identifier() {
let tokens = lex("1 Kilometers").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::IntLit(1)));
assert!(matches!(tokens[1].kind, TokenKind::Ident(ref s) if s == "Kilometers"));
}
#[test]
fn lex_si_overflow_errors_loud() {
let err = lex("100000P").unwrap_err();
assert!(err.contains("overflows u64"), "{err}");
}
#[test]
fn lex_si_in_range_expression() {
let tokens = lex("1K..1M..100K").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::IntLit(1_000)));
let int_lits: Vec<u64> = tokens
.iter()
.filter_map(|t| match &t.kind {
TokenKind::IntLit(v) => Some(*v),
_ => None,
})
.collect();
assert_eq!(int_lits, vec![1_000, 1_000_000, 100_000]);
}
#[test]
fn lex_si_disambiguation_two_char_first() {
let tokens = lex("1Ki").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::IntLit(1024)));
assert!(matches!(tokens[1].kind, TokenKind::Eof));
}
#[test]
fn lex_si_followed_by_operator_applies_suffix() {
let tokens = lex("1K+5").unwrap();
assert!(matches!(tokens[0].kind, TokenKind::IntLit(1000)));
assert!(matches!(tokens[1].kind, TokenKind::Plus));
assert!(matches!(tokens[2].kind, TokenKind::IntLit(5)));
}
}