use crate::{SyntaxKind, SyntaxKind::*};
use logos::Logos;
use rowan::{TextRange, TextSize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Token {
pub kind: SyntaxKind,
pub len: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LexErrorKind {
InvalidDigit { digit: char, radix: u32, token: String },
CouldNotLex { content: String },
BidiOverride,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LexError {
pub range: TextRange,
pub kind: LexErrorKind,
}
fn comment_block(lex: &mut logos::Lexer<LogosToken>) -> bool {
let mut last_asterisk = false;
for (index, c) in lex.remainder().char_indices() {
if c == '*' {
last_asterisk = true;
} else if c == '/' && last_asterisk {
lex.bump(index + 1);
return true;
} else if matches!(c, '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}') {
lex.bump(index);
return true;
} else {
last_asterisk = false;
}
}
let remaining = lex.remainder().len();
lex.bump(remaining);
true
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Logos)]
enum LogosToken {
#[regex(r"[ \t\f]+")]
Whitespace,
#[regex(r"\r?\n")]
Linebreak,
#[regex(r"//[^\r\n\u{202A}-\u{202E}\u{2066}-\u{2069}]*")]
CommentLine,
#[token(r"/*", comment_block)]
CommentBlock,
#[regex(r"aleo1[a-z0-9]*")]
AddressLiteral,
#[regex(r"0x[0-9A-Za-z_]+([ui](8|16|32|64|128)|field|group|scalar)?", priority = 3)]
#[regex(r"0o[0-9A-Za-z_]+([ui](8|16|32|64|128)|field|group|scalar)?", priority = 3)]
#[regex(r"0b[0-9A-Za-z_]+([ui](8|16|32|64|128)|field|group|scalar)?", priority = 3)]
#[regex(r"[0-9][0-9A-Za-z_]*([ui](8|16|32|64|128)|field|group|scalar)?")]
Integer,
#[regex(r#""[^"]*""#)]
StaticString,
#[regex(r"'[a-zA-Z][a-zA-Z0-9_]*'")]
IdentifierLiteral,
#[regex(r"group::[a-zA-Z][a-zA-Z0-9_]*")]
#[regex(r"signature::[a-zA-Z][a-zA-Z0-9_]*")]
#[regex(r"Future::[a-zA-Z][a-zA-Z0-9_]*")]
PathSpecial,
#[regex(r"_[a-zA-Z][a-zA-Z0-9_]*")]
IdentIntrinsic,
#[regex(r"[a-zA-Z][a-zA-Z0-9_]*")]
Ident,
#[token("**=")]
PowAssign,
#[token("&&=")]
AndAssign,
#[token("||=")]
OrAssign,
#[token("<<=")]
ShlAssign,
#[token(">>=")]
ShrAssign,
#[token("**")]
Pow,
#[token("&&")]
And,
#[token("||")]
Or,
#[token("<<")]
Shl,
#[token(">>")]
Shr,
#[token("==")]
EqEq,
#[token("!=")]
NotEq,
#[token("<=")]
LtEq,
#[token(">=")]
GtEq,
#[token("+=")]
AddAssign,
#[token("-=")]
SubAssign,
#[token("*=")]
MulAssign,
#[token("/=")]
DivAssign,
#[token("%=")]
RemAssign,
#[token("&=")]
BitAndAssign,
#[token("|=")]
BitOrAssign,
#[token("^=")]
BitXorAssign,
#[token("->")]
Arrow,
#[token("=>")]
FatArrow,
#[token("..=")]
DotDotEq,
#[token("..")]
DotDot,
#[token("::")]
ColonColon,
#[token("=")]
Eq,
#[token("!")]
Bang,
#[token("<")]
Lt,
#[token(">")]
Gt,
#[token("+")]
Plus,
#[token("-")]
Minus,
#[token("*")]
Star,
#[token("/")]
Slash,
#[token("%")]
Percent,
#[token("&")]
Amp,
#[token("|")]
Pipe,
#[token("^")]
Caret,
#[token("(")]
LParen,
#[token(")")]
RParen,
#[token("[")]
LBracket,
#[token("]")]
RBracket,
#[token("{")]
LBrace,
#[token("}")]
RBrace,
#[token(",")]
Comma,
#[token(".")]
Dot,
#[token(";")]
Semicolon,
#[token(":")]
Colon,
#[token("?")]
Question,
#[token("_")]
Underscore,
#[token("@")]
At,
#[regex(r"[\u{202A}-\u{202E}\u{2066}-\u{2069}]")]
Bidi,
}
fn ident_to_kind(s: &str) -> SyntaxKind {
match s {
"true" => KW_TRUE,
"false" => KW_FALSE,
"none" => KW_NONE,
"address" => KW_ADDRESS,
"bool" => KW_BOOL,
"field" => KW_FIELD,
"group" => KW_GROUP,
"scalar" => KW_SCALAR,
"signature" => KW_SIGNATURE,
"string" => KW_STRING,
"record" => KW_RECORD,
"dyn" => KW_DYN,
"identifier" => KW_IDENTIFIER,
"i8" => KW_I8,
"i16" => KW_I16,
"i32" => KW_I32,
"i64" => KW_I64,
"i128" => KW_I128,
"u8" => KW_U8,
"u16" => KW_U16,
"u32" => KW_U32,
"u64" => KW_U64,
"u128" => KW_U128,
"if" => KW_IF,
"else" => KW_ELSE,
"for" => KW_FOR,
"in" => KW_IN,
"return" => KW_RETURN,
"let" => KW_LET,
"const" => KW_CONST,
"constant" => KW_CONSTANT,
"final" => KW_FINAL,
"Final" => KW_FINAL_UPPER,
"fn" => KW_FN,
"Fn" => KW_FN_UPPER,
"struct" => KW_STRUCT,
"constructor" => KW_CONSTRUCTOR,
"interface" => KW_INTERFACE,
"program" => KW_PROGRAM,
"import" => KW_IMPORT,
"mapping" => KW_MAPPING,
"storage" => KW_STORAGE,
"network" => KW_NETWORK,
"aleo" => KW_ALEO,
"script" => KW_SCRIPT,
"block" => KW_BLOCK,
"public" => KW_PUBLIC,
"private" => KW_PRIVATE,
"as" => KW_AS,
"self" => KW_SELF,
"assert" => KW_ASSERT,
"assert_eq" => KW_ASSERT_EQ,
"assert_neq" => KW_ASSERT_NEQ,
_ => IDENT,
}
}
fn strip_int_suffix(s: &str) -> Option<&str> {
let suffixes = ["u128", "i128", "u64", "i64", "u32", "i32", "u16", "i16", "u8", "i8"];
for suffix in suffixes {
if let Some(prefix) = s.strip_suffix(suffix) {
return Some(prefix);
}
}
None
}
fn validate_integer_digits(text: &str, offset: usize, errors: &mut Vec<LexError>) {
let num_part = text
.strip_suffix("field")
.or_else(|| text.strip_suffix("group"))
.or_else(|| text.strip_suffix("scalar"))
.or_else(|| strip_int_suffix(text))
.unwrap_or(text);
let (digits, radix, _prefix_len): (&str, u32, usize) = if let Some(s) = num_part.strip_prefix("0x") {
(s, 16, 2)
} else if let Some(s) = num_part.strip_prefix("0X") {
(s, 16, 2)
} else if let Some(s) = num_part.strip_prefix("0o") {
(s, 8, 2)
} else if let Some(s) = num_part.strip_prefix("0O") {
(s, 8, 2)
} else if let Some(s) = num_part.strip_prefix("0b") {
(s, 2, 2)
} else if let Some(s) = num_part.strip_prefix("0B") {
(s, 2, 2)
} else {
(num_part, 10, 0)
};
for (_, c) in digits.char_indices() {
if c == '_' {
continue; }
if !c.is_digit(radix) {
let error_end = offset + num_part.len();
errors.push(LexError {
range: TextRange::new(TextSize::new(offset as u32), TextSize::new(error_end as u32)),
kind: LexErrorKind::InvalidDigit { digit: c, radix, token: num_part.to_string() },
});
return; }
}
}
pub fn lex(source: &str) -> (Vec<Token>, Vec<LexError>) {
let mut tokens = Vec::new();
let mut errors = Vec::new();
let mut lexer = LogosToken::lexer(source);
while let Some(result) = lexer.next() {
let span = lexer.span();
let len = (span.end - span.start) as u32;
let slice = lexer.slice();
let kind = match result {
Ok(token) => match token {
LogosToken::Whitespace => WHITESPACE,
LogosToken::Linebreak => LINEBREAK,
LogosToken::CommentLine => COMMENT_LINE,
LogosToken::CommentBlock => {
if !slice.ends_with("*/") {
let preview_len = slice.len().min(10);
let preview = &slice[..preview_len];
errors.push(LexError {
range: TextRange::new(
TextSize::new(span.start as u32),
TextSize::new((span.start + 2) as u32), ),
kind: LexErrorKind::CouldNotLex { content: preview.to_string() },
});
}
COMMENT_BLOCK
}
LogosToken::AddressLiteral => ADDRESS_LIT,
LogosToken::Integer => INTEGER,
LogosToken::StaticString => STRING,
LogosToken::IdentifierLiteral => IDENT_LIT,
LogosToken::Ident => ident_to_kind(slice),
LogosToken::IdentIntrinsic => IDENT,
LogosToken::PathSpecial => IDENT,
LogosToken::PowAssign => STAR2_EQ,
LogosToken::AndAssign => AMP2_EQ,
LogosToken::OrAssign => PIPE2_EQ,
LogosToken::ShlAssign => SHL_EQ,
LogosToken::ShrAssign => SHR_EQ,
LogosToken::Pow => STAR2,
LogosToken::And => AMP2,
LogosToken::Or => PIPE2,
LogosToken::Shl => SHL,
LogosToken::Shr => SHR,
LogosToken::EqEq => EQ2,
LogosToken::NotEq => BANG_EQ,
LogosToken::LtEq => LT_EQ,
LogosToken::GtEq => GT_EQ,
LogosToken::AddAssign => PLUS_EQ,
LogosToken::SubAssign => MINUS_EQ,
LogosToken::MulAssign => STAR_EQ,
LogosToken::DivAssign => SLASH_EQ,
LogosToken::RemAssign => PERCENT_EQ,
LogosToken::BitAndAssign => AMP_EQ,
LogosToken::BitOrAssign => PIPE_EQ,
LogosToken::BitXorAssign => CARET_EQ,
LogosToken::Arrow => ARROW,
LogosToken::FatArrow => FAT_ARROW,
LogosToken::DotDotEq => DOT_DOT_EQ,
LogosToken::DotDot => DOT_DOT,
LogosToken::ColonColon => COLON_COLON,
LogosToken::Eq => EQ,
LogosToken::Bang => BANG,
LogosToken::Lt => LT,
LogosToken::Gt => GT,
LogosToken::Plus => PLUS,
LogosToken::Minus => MINUS,
LogosToken::Star => STAR,
LogosToken::Slash => SLASH,
LogosToken::Percent => PERCENT,
LogosToken::Amp => AMP,
LogosToken::Pipe => PIPE,
LogosToken::Caret => CARET,
LogosToken::LParen => L_PAREN,
LogosToken::RParen => R_PAREN,
LogosToken::LBracket => L_BRACKET,
LogosToken::RBracket => R_BRACKET,
LogosToken::LBrace => L_BRACE,
LogosToken::RBrace => R_BRACE,
LogosToken::Comma => COMMA,
LogosToken::Dot => DOT,
LogosToken::Semicolon => SEMICOLON,
LogosToken::Colon => COLON,
LogosToken::Question => QUESTION,
LogosToken::Underscore => UNDERSCORE,
LogosToken::At => AT,
LogosToken::Bidi => {
errors.push(LexError {
range: TextRange::new(TextSize::new(span.start as u32), TextSize::new(span.end as u32)),
kind: LexErrorKind::BidiOverride,
});
ERROR
}
},
Err(()) => {
errors.push(LexError {
range: TextRange::new(TextSize::new(span.start as u32), TextSize::new(span.end as u32)),
kind: LexErrorKind::CouldNotLex { content: slice.to_string() },
});
ERROR
}
};
if kind == INTEGER {
validate_integer_digits(slice, span.start, &mut errors);
}
tokens.push(Token { kind, len });
}
tokens.push(Token { kind: EOF, len: 0 });
(tokens, errors)
}
#[cfg(test)]
mod tests {
use super::*;
use expect_test::{Expect, expect};
fn check_lex(input: &str, expect: Expect) {
let (tokens, _errors) = lex(input);
let mut output = String::new();
let mut offset = 0usize;
for token in &tokens {
let text = &input[offset..offset + token.len as usize];
output.push_str(&format!("{:?} {:?}\n", token.kind, text));
offset += token.len as usize;
}
expect.assert_eq(&output);
}
fn check_lex_errors(input: &str, expect: Expect) {
let (_tokens, errors) = lex(input);
let output = errors
.iter()
.map(|e| format!("{}..{}:{:?}", u32::from(e.range.start()), u32::from(e.range.end()), e.kind))
.collect::<Vec<_>>()
.join("\n");
expect.assert_eq(&output);
}
#[test]
fn lex_empty() {
check_lex("", expect![[r#"
EOF ""
"#]]);
}
#[test]
fn lex_whitespace() {
check_lex(" \t ", expect![[r#"
WHITESPACE " \t "
EOF ""
"#]]);
}
#[test]
fn lex_linebreaks() {
check_lex("\n\r\n\n", expect![[r#"
LINEBREAK "\n"
LINEBREAK "\r\n"
LINEBREAK "\n"
EOF ""
"#]]);
}
#[test]
fn lex_mixed_whitespace() {
check_lex(" \n \t\n", expect![[r#"
WHITESPACE " "
LINEBREAK "\n"
WHITESPACE " \t"
LINEBREAK "\n"
EOF ""
"#]]);
}
#[test]
fn lex_line_comments() {
check_lex("// hello\n// world", expect![[r#"
COMMENT_LINE "// hello"
LINEBREAK "\n"
COMMENT_LINE "// world"
EOF ""
"#]]);
}
#[test]
fn lex_block_comments() {
check_lex("/* hello */ /* multi\nline */", expect![[r#"
COMMENT_BLOCK "/* hello */"
WHITESPACE " "
COMMENT_BLOCK "/* multi\nline */"
EOF ""
"#]]);
}
#[test]
fn lex_identifiers() {
check_lex("foo Bar _baz x123", expect![[r#"
IDENT "foo"
WHITESPACE " "
IDENT "Bar"
WHITESPACE " "
IDENT "_baz"
WHITESPACE " "
IDENT "x123"
EOF ""
"#]]);
}
#[test]
fn lex_keywords() {
check_lex("let fn if return true false", expect![[r#"
KW_LET "let"
WHITESPACE " "
KW_FN "fn"
WHITESPACE " "
KW_IF "if"
WHITESPACE " "
KW_RETURN "return"
WHITESPACE " "
KW_TRUE "true"
WHITESPACE " "
KW_FALSE "false"
EOF ""
"#]]);
}
#[test]
fn lex_type_keywords() {
check_lex("u8 u16 u32 u64 u128 i8 i16 i32 i64 i128", expect![[r#"
KW_U8 "u8"
WHITESPACE " "
KW_U16 "u16"
WHITESPACE " "
KW_U32 "u32"
WHITESPACE " "
KW_U64 "u64"
WHITESPACE " "
KW_U128 "u128"
WHITESPACE " "
KW_I8 "i8"
WHITESPACE " "
KW_I16 "i16"
WHITESPACE " "
KW_I32 "i32"
WHITESPACE " "
KW_I64 "i64"
WHITESPACE " "
KW_I128 "i128"
EOF ""
"#]]);
}
#[test]
fn lex_more_type_keywords() {
check_lex("bool field group scalar address signature string record", expect![[r#"
KW_BOOL "bool"
WHITESPACE " "
KW_FIELD "field"
WHITESPACE " "
KW_GROUP "group"
WHITESPACE " "
KW_SCALAR "scalar"
WHITESPACE " "
KW_ADDRESS "address"
WHITESPACE " "
KW_SIGNATURE "signature"
WHITESPACE " "
KW_STRING "string"
WHITESPACE " "
KW_RECORD "record"
EOF ""
"#]]);
}
#[test]
fn lex_identifier_literal() {
check_lex("'foo' 'bar_baz' 'x'", expect![[r#"
IDENT_LIT "'foo'"
WHITESPACE " "
IDENT_LIT "'bar_baz'"
WHITESPACE " "
IDENT_LIT "'x'"
EOF ""
"#]]);
}
#[test]
fn lex_identifier_keyword() {
check_lex("identifier", expect![[r#"
KW_IDENTIFIER "identifier"
EOF ""
"#]]);
}
#[test]
fn lex_integers() {
check_lex("123 0xFF 0b101 0o77", expect![[r#"
INTEGER "123"
WHITESPACE " "
INTEGER "0xFF"
WHITESPACE " "
INTEGER "0b101"
WHITESPACE " "
INTEGER "0o77"
EOF ""
"#]]);
}
#[test]
fn lex_integers_with_underscores() {
check_lex("1_000_000 0xFF_FF", expect![[r#"
INTEGER "1_000_000"
WHITESPACE " "
INTEGER "0xFF_FF"
EOF ""
"#]]);
}
#[test]
fn lex_address_literal() {
check_lex("aleo1abc123", expect![[r#"
ADDRESS_LIT "aleo1abc123"
EOF ""
"#]]);
}
#[test]
fn lex_strings() {
check_lex(r#""hello" "world""#, expect![[r#"
STRING "\"hello\""
WHITESPACE " "
STRING "\"world\""
EOF ""
"#]]);
}
#[test]
fn lex_punctuation() {
check_lex("( ) [ ] { } , . ; : :: ? -> => _ @", expect![[r#"
L_PAREN "("
WHITESPACE " "
R_PAREN ")"
WHITESPACE " "
L_BRACKET "["
WHITESPACE " "
R_BRACKET "]"
WHITESPACE " "
L_BRACE "{"
WHITESPACE " "
R_BRACE "}"
WHITESPACE " "
COMMA ","
WHITESPACE " "
DOT "."
WHITESPACE " "
SEMICOLON ";"
WHITESPACE " "
COLON ":"
WHITESPACE " "
COLON_COLON "::"
WHITESPACE " "
QUESTION "?"
WHITESPACE " "
ARROW "->"
WHITESPACE " "
FAT_ARROW "=>"
WHITESPACE " "
UNDERSCORE "_"
WHITESPACE " "
AT "@"
EOF ""
"#]]);
}
#[test]
fn lex_arithmetic_operators() {
check_lex("+ - * / % **", expect![[r#"
PLUS "+"
WHITESPACE " "
MINUS "-"
WHITESPACE " "
STAR "*"
WHITESPACE " "
SLASH "/"
WHITESPACE " "
PERCENT "%"
WHITESPACE " "
STAR2 "**"
EOF ""
"#]]);
}
#[test]
fn lex_comparison_operators() {
check_lex("== != < <= > >=", expect![[r#"
EQ2 "=="
WHITESPACE " "
BANG_EQ "!="
WHITESPACE " "
LT "<"
WHITESPACE " "
LT_EQ "<="
WHITESPACE " "
GT ">"
WHITESPACE " "
GT_EQ ">="
EOF ""
"#]]);
}
#[test]
fn lex_logical_operators() {
check_lex("&& || !", expect![[r#"
AMP2 "&&"
WHITESPACE " "
PIPE2 "||"
WHITESPACE " "
BANG "!"
EOF ""
"#]]);
}
#[test]
fn lex_bitwise_operators() {
check_lex("& | ^ << >>", expect![[r#"
AMP "&"
WHITESPACE " "
PIPE "|"
WHITESPACE " "
CARET "^"
WHITESPACE " "
SHL "<<"
WHITESPACE " "
SHR ">>"
EOF ""
"#]]);
}
#[test]
fn lex_assignment_operators() {
check_lex("= += -= *= /= %= **= &&= ||=", expect![[r#"
EQ "="
WHITESPACE " "
PLUS_EQ "+="
WHITESPACE " "
MINUS_EQ "-="
WHITESPACE " "
STAR_EQ "*="
WHITESPACE " "
SLASH_EQ "/="
WHITESPACE " "
PERCENT_EQ "%="
WHITESPACE " "
STAR2_EQ "**="
WHITESPACE " "
AMP2_EQ "&&="
WHITESPACE " "
PIPE2_EQ "||="
EOF ""
"#]]);
}
#[test]
fn lex_more_assignment_operators() {
check_lex("&= |= ^= <<= >>=", expect![[r#"
AMP_EQ "&="
WHITESPACE " "
PIPE_EQ "|="
WHITESPACE " "
CARET_EQ "^="
WHITESPACE " "
SHL_EQ "<<="
WHITESPACE " "
SHR_EQ ">>="
EOF ""
"#]]);
}
#[test]
fn lex_dot_dot() {
check_lex("0..10", expect![[r#"
INTEGER "0"
DOT_DOT ".."
INTEGER "10"
EOF ""
"#]]);
}
#[test]
fn lex_simple_expression() {
check_lex("x + y * 2", expect![[r#"
IDENT "x"
WHITESPACE " "
PLUS "+"
WHITESPACE " "
IDENT "y"
WHITESPACE " "
STAR "*"
WHITESPACE " "
INTEGER "2"
EOF ""
"#]]);
}
#[test]
fn lex_function_call() {
check_lex("foo(a, b)", expect![[r#"
IDENT "foo"
L_PAREN "("
IDENT "a"
COMMA ","
WHITESPACE " "
IDENT "b"
R_PAREN ")"
EOF ""
"#]]);
}
#[test]
fn lex_function_definition() {
check_lex("fn add(x: u32) -> u32 {", expect![[r#"
KW_FN "fn"
WHITESPACE " "
IDENT "add"
L_PAREN "("
IDENT "x"
COLON ":"
WHITESPACE " "
KW_U32 "u32"
R_PAREN ")"
WHITESPACE " "
ARROW "->"
WHITESPACE " "
KW_U32 "u32"
WHITESPACE " "
L_BRACE "{"
EOF ""
"#]]);
}
#[test]
fn lex_let_statement() {
check_lex("let x: u32 = 42;", expect![[r#"
KW_LET "let"
WHITESPACE " "
IDENT "x"
COLON ":"
WHITESPACE " "
KW_U32 "u32"
WHITESPACE " "
EQ "="
WHITESPACE " "
INTEGER "42"
SEMICOLON ";"
EOF ""
"#]]);
}
#[test]
fn lex_typed_integers() {
check_lex("1000u32 42i64 0u8 255u128", expect![[r#"
INTEGER "1000u32"
WHITESPACE " "
INTEGER "42i64"
WHITESPACE " "
INTEGER "0u8"
WHITESPACE " "
INTEGER "255u128"
EOF ""
"#]]);
}
#[test]
fn lex_typed_integers_field() {
check_lex("123field 456group 789scalar", expect![[r#"
INTEGER "123field"
WHITESPACE " "
INTEGER "456group"
WHITESPACE " "
INTEGER "789scalar"
EOF ""
"#]]);
}
#[test]
fn lex_special_paths() {
check_lex("group::GEN signature::verify Future::await", expect![[r#"
IDENT "group::GEN"
WHITESPACE " "
IDENT "signature::verify"
WHITESPACE " "
IDENT "Future::await"
EOF ""
"#]]);
}
#[test]
fn lex_typed_integer_range() {
check_lex("0u8..STOP", expect![[r#"
INTEGER "0u8"
DOT_DOT ".."
IDENT "STOP"
EOF ""
"#]]);
}
#[test]
fn lex_error_unknown_char() {
check_lex_errors("hello $ world", expect![[r#"6..7:CouldNotLex { content: "$" }"#]]);
}
#[test]
fn lex_invalid_hex_digit() {
let (tokens, errors) = lex("0xGAu32");
assert_eq!(tokens.len(), 2); assert!(!errors.is_empty());
assert!(matches!(errors[0].kind, LexErrorKind::InvalidDigit { digit: 'G', radix: 16, .. }));
}
#[test]
fn lex_invalid_octal_digit() {
let (_, errors) = lex("0o9u32");
assert!(!errors.is_empty());
assert!(matches!(errors[0].kind, LexErrorKind::InvalidDigit { digit: '9', radix: 8, .. }));
}
#[test]
fn lex_invalid_binary_digit() {
let (_, errors) = lex("0b2u32");
assert!(!errors.is_empty());
assert!(matches!(errors[0].kind, LexErrorKind::InvalidDigit { digit: '2', radix: 2, .. }));
}
#[test]
fn lex_valid_hex_is_ok() {
let (_, errors) = lex("0xDEADBEEFu64");
assert!(errors.is_empty());
}
#[test]
fn lex_invalid_hex_lowercase() {
let (_, errors) = lex("0xghu32");
assert!(!errors.is_empty());
assert!(matches!(errors[0].kind, LexErrorKind::InvalidDigit { digit: 'g', radix: 16, .. }));
}
#[test]
fn lex_bidi_override_error() {
let (_, errors) = lex("let x\u{202E} = 1;");
assert!(!errors.is_empty());
assert!(matches!(errors[0].kind, LexErrorKind::BidiOverride));
}
#[test]
fn lex_unclosed_block_comment() {
let (tokens, errors) = lex("/* unclosed");
assert!(!errors.is_empty());
assert!(matches!(errors[0].kind, LexErrorKind::CouldNotLex { .. }));
assert!(tokens.iter().any(|t| t.kind == COMMENT_BLOCK));
}
#[test]
fn lex_nested_comment_not_supported() {
let (tokens, errors) = lex("/* outer /* inner */");
assert!(errors.is_empty());
assert!(tokens.iter().any(|t| t.kind == COMMENT_BLOCK));
}
#[test]
fn lex_closed_comment_ok() {
let (_, errors) = lex("/* closed */");
assert!(errors.is_empty());
}
}