use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::machine::diag::Diagnostic;
use crate::machine::span::{SourceFile, Span};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Radix {
Bin,
Oct,
Dec,
Hex,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SizeUnit {
Byte,
Kilo,
Mega,
Giga,
Tera,
}
impl SizeUnit {
pub const fn scale(self) -> u64 {
match self {
SizeUnit::Byte => 1,
SizeUnit::Kilo => 1 << 10,
SizeUnit::Mega => 1 << 20,
SizeUnit::Giga => 1 << 30,
SizeUnit::Tera => 1 << 40,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DurationUnit {
Nanos,
Micros,
Millis,
Secs,
}
impl DurationUnit {
pub const fn scale(self) -> u64 {
match self {
DurationUnit::Nanos => 1,
DurationUnit::Micros => 1_000,
DurationUnit::Millis => 1_000_000,
DurationUnit::Secs => 1_000_000_000,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumUnit {
None,
Size(SizeUnit),
Duration(DurationUnit),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NumLit {
pub value: u64,
pub digits: u64,
pub radix: Radix,
pub unit: NumUnit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenKind {
Ident(String),
Str(String),
Num(NumLit),
LBrace,
RBrace,
LParen,
RParen,
LBracket,
RBracket,
Comma,
Eq,
Arrow,
Dot,
DotDot,
DotDotEq,
Plus,
Minus,
Star,
Slash,
Percent,
Dollar,
Eof,
}
impl TokenKind {
pub const fn symbol(&self) -> Option<&'static str> {
Some(match self {
TokenKind::LBrace => "{",
TokenKind::RBrace => "}",
TokenKind::LParen => "(",
TokenKind::RParen => ")",
TokenKind::LBracket => "[",
TokenKind::RBracket => "]",
TokenKind::Comma => ",",
TokenKind::Eq => "=",
TokenKind::Arrow => "->",
TokenKind::Dot => ".",
TokenKind::DotDot => "..",
TokenKind::DotDotEq => "..=",
TokenKind::Plus => "+",
TokenKind::Minus => "-",
TokenKind::Star => "*",
TokenKind::Slash => "/",
TokenKind::Percent => "%",
TokenKind::Dollar => "$",
_ => return None,
})
}
pub fn describe(&self) -> String {
match self {
TokenKind::Ident(name) => format!("`{name}`"),
TokenKind::Str(_) => "a string literal".to_string(),
TokenKind::Num(_) => "a number".to_string(),
TokenKind::Eof => "end of file".to_string(),
other => match other.symbol() {
Some(sym) => format!("`{sym}`"),
None => "a token".to_string(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token {
pub kind: TokenKind,
pub span: Span,
}
pub fn tokenize(src: &SourceFile<'_>) -> Result<Vec<Token>, Diagnostic> {
Lexer::new(src)?.run()
}
struct Lexer<'a> {
text: &'a str,
pos: usize,
}
impl<'a> Lexer<'a> {
fn new(src: &SourceFile<'a>) -> Result<Self, Diagnostic> {
if u32::try_from(src.text().len()).is_err() {
return Err(Diagnostic::new(
Span::at(0),
"machine description is larger than 4 GiB",
));
}
Ok(Lexer {
text: src.text(),
pos: 0,
})
}
fn peek(&self) -> Option<char> {
self.text[self.pos..].chars().next()
}
fn peek_nth(&self, n: usize) -> Option<char> {
self.text[self.pos..].chars().nth(n)
}
fn bump(&mut self) -> Option<char> {
let c = self.peek()?;
self.pos += c.len_utf8();
Some(c)
}
fn eat(&mut self, want: char) -> bool {
if self.peek() == Some(want) {
self.pos += want.len_utf8();
true
} else {
false
}
}
fn span_from(&self, start: usize) -> Span {
Span::new(start as u32, self.pos as u32)
}
fn run(mut self) -> Result<Vec<Token>, Diagnostic> {
let mut out = Vec::new();
loop {
self.skip_trivia();
let start = self.pos;
let Some(c) = self.peek() else {
out.push(Token {
kind: TokenKind::Eof,
span: self.span_from(start),
});
return Ok(out);
};
let kind = match c {
'{' => self.punct(TokenKind::LBrace),
'}' => self.punct(TokenKind::RBrace),
'(' => self.punct(TokenKind::LParen),
')' => self.punct(TokenKind::RParen),
'[' => self.punct(TokenKind::LBracket),
']' => self.punct(TokenKind::RBracket),
',' => self.punct(TokenKind::Comma),
'=' => self.punct(TokenKind::Eq),
'+' => self.punct(TokenKind::Plus),
'*' => self.punct(TokenKind::Star),
'/' => self.punct(TokenKind::Slash),
'%' => self.punct(TokenKind::Percent),
'$' => self.punct(TokenKind::Dollar),
'-' => {
self.pos += 1;
if self.eat('>') {
TokenKind::Arrow
} else {
TokenKind::Minus
}
}
'.' => {
self.pos += 1;
if self.eat('.') {
if self.eat('=') {
TokenKind::DotDotEq
} else {
TokenKind::DotDot
}
} else {
TokenKind::Dot
}
}
'"' => self.lex_string()?,
c if c.is_ascii_digit() => self.lex_number()?,
c if is_ident_start(c) => self.lex_ident(),
other => {
self.pos += other.len_utf8();
return Err(Diagnostic::new(
self.span_from(start),
format!("unexpected character `{other}`"),
));
}
};
out.push(Token {
kind,
span: self.span_from(start),
});
}
}
fn punct(&mut self, kind: TokenKind) -> TokenKind {
self.pos += 1;
kind
}
fn skip_trivia(&mut self) {
loop {
match self.peek() {
Some(c) if c.is_whitespace() => {
self.pos += c.len_utf8();
}
Some('#') => {
while let Some(c) = self.peek() {
if c == '\n' {
break;
}
self.pos += c.len_utf8();
}
}
_ => return,
}
}
}
fn lex_ident(&mut self) -> TokenKind {
let start = self.pos;
self.pos += 1; loop {
match self.peek() {
Some(c) if is_ident_continue(c) => self.pos += 1,
Some('-') if self.peek_nth(1).is_some_and(|c| c.is_ascii_alphabetic()) => {
self.pos += 1;
}
_ => break,
}
}
TokenKind::Ident(self.text[start..self.pos].to_string())
}
fn lex_string(&mut self) -> Result<TokenKind, Diagnostic> {
let open = self.pos;
self.pos += 1; let mut value = String::new();
loop {
let Some(c) = self.bump() else {
return Err(Diagnostic::new(
Span::new(open as u32, open as u32 + 1),
"unterminated string literal",
));
};
match c {
'"' => return Ok(TokenKind::Str(value)),
'\n' => {
return Err(Diagnostic::new(
Span::new(open as u32, open as u32 + 1),
"unterminated string literal",
));
}
'\\' => value.push(self.lex_escape()?),
other => value.push(other),
}
}
}
fn lex_escape(&mut self) -> Result<char, Diagnostic> {
let start = self.pos - 1;
let Some(c) = self.bump() else {
return Err(Diagnostic::new(
self.span_from(start),
"unterminated string literal",
));
};
Ok(match c {
'n' => '\n',
'r' => '\r',
't' => '\t',
'0' => '\0',
'\\' => '\\',
'"' => '"',
'x' => {
let mut value: u32 = 0;
for _ in 0..2 {
let Some(d) = self.peek().and_then(|c| c.to_digit(16)) else {
return Err(Diagnostic::new(
self.span_from(start),
"`\\x` needs exactly two hexadecimal digits",
));
};
self.pos += 1;
value = value * 16 + d;
}
if value > 0x7f {
return Err(Diagnostic::new(
self.span_from(start),
"`\\x` escapes are limited to `\\x00`-`\\x7f`; write the character itself",
));
}
char::from_u32(value).unwrap_or('\0')
}
other => {
return Err(Diagnostic::new(
self.span_from(start),
format!("unknown escape `\\{other}`"),
));
}
})
}
fn lex_number(&mut self) -> Result<TokenKind, Diagnostic> {
let start = self.pos;
let mut radix = Radix::Dec;
if self.peek() == Some('0') {
match self.peek_nth(1) {
Some('x' | 'X') => radix = Radix::Hex,
Some('b' | 'B') => radix = Radix::Bin,
Some('o' | 'O') => radix = Radix::Oct,
_ => {}
}
if radix != Radix::Dec {
self.pos += 2;
}
}
let base = match radix {
Radix::Bin => 2,
Radix::Oct => 8,
Radix::Dec => 10,
Radix::Hex => 16,
};
let digits_start = self.pos;
let mut digits: u64 = 0;
let mut any = false;
loop {
match self.peek() {
Some('_') if any => self.pos += 1,
Some(c) => {
let Some(d) = c.to_digit(16).filter(|_| c.is_ascii_alphanumeric()) else {
break;
};
if d >= base {
if c.is_ascii_digit() {
let span = Span::new(self.pos as u32, self.pos as u32 + 1);
return Err(Diagnostic::new(
span,
format!("invalid digit `{c}` in {} literal", radix_name(radix)),
));
}
break; }
self.pos += 1;
any = true;
digits = match digits
.checked_mul(u64::from(base))
.and_then(|n| n.checked_add(u64::from(d)))
{
Some(n) => n,
None => {
self.skip_number_tail();
return Err(Diagnostic::new(
self.span_from(start),
"integer literal does not fit in 64 bits",
));
}
};
}
None => break,
}
}
if !any {
self.skip_number_tail();
return Err(Diagnostic::new(
self.span_from(start),
format!(
"expected digits after `{}`",
&self.text[start..digits_start]
),
));
}
let suffix_start = self.pos;
while let Some(c) = self.peek() {
if c.is_ascii_alphanumeric() || c == '_' {
self.pos += 1;
} else {
break;
}
}
let suffix = &self.text[suffix_start..self.pos];
let suffix_span = Span::new(suffix_start as u32, self.pos as u32);
let unit = if suffix.is_empty() {
NumUnit::None
} else if radix != Radix::Dec {
return Err(Diagnostic::new(
suffix_span,
format!(
"suffix `{suffix}` is only allowed on decimal literals; write `{}` in decimal",
&self.text[start..suffix_start]
),
));
} else {
match unit_for(suffix) {
Some(u) => u,
None => {
return Err(Diagnostic::new(
suffix_span,
format!(
"unknown suffix `{suffix}`; expected a size (`K`, `M`, `G`, `T`) or a duration (`ns`, `us`, `ms`, `s`)"
),
));
}
}
};
let scale = match unit {
NumUnit::None => 1,
NumUnit::Size(u) => u.scale(),
NumUnit::Duration(u) => u.scale(),
};
let Some(value) = digits.checked_mul(scale) else {
return Err(Diagnostic::new(
self.span_from(start),
"integer literal does not fit in 64 bits once its suffix is applied",
));
};
Ok(TokenKind::Num(NumLit {
value,
digits,
radix,
unit,
}))
}
fn skip_number_tail(&mut self) {
while let Some(c) = self.peek() {
if c.is_ascii_alphanumeric() || c == '_' {
self.pos += 1;
} else {
break;
}
}
}
}
fn unit_for(suffix: &str) -> Option<NumUnit> {
let duration = match suffix {
"ns" => Some(DurationUnit::Nanos),
"us" => Some(DurationUnit::Micros),
"ms" => Some(DurationUnit::Millis),
"s" => Some(DurationUnit::Secs),
_ => None,
};
if let Some(d) = duration {
return Some(NumUnit::Duration(d));
}
let mut folded = String::with_capacity(suffix.len());
for c in suffix.chars() {
folded.push(c.to_ascii_lowercase());
}
let size = match folded.as_str() {
"b" => SizeUnit::Byte,
"k" | "kb" | "ki" | "kib" => SizeUnit::Kilo,
"m" | "mb" | "mi" | "mib" => SizeUnit::Mega,
"g" | "gb" | "gi" | "gib" => SizeUnit::Giga,
"t" | "tb" | "ti" | "tib" => SizeUnit::Tera,
_ => return None,
};
Some(NumUnit::Size(size))
}
fn radix_name(radix: Radix) -> &'static str {
match radix {
Radix::Bin => "a binary",
Radix::Oct => "an octal",
Radix::Dec => "a decimal",
Radix::Hex => "a hexadecimal",
}
}
fn is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
fn is_ident_continue(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds(text: &str) -> Vec<TokenKind> {
let src = SourceFile::new("t", text);
tokenize(&src)
.expect("should lex")
.into_iter()
.map(|t| t.kind)
.collect()
}
fn error(text: &str) -> String {
let src = SourceFile::new("t", text);
tokenize(&src).expect_err("should fail").message
}
fn num(text: &str) -> NumLit {
match &kinds(text)[0] {
TokenKind::Num(n) => *n,
other => panic!("not a number: {other:?}"),
}
}
#[test]
fn punctuation_and_the_arrow() {
assert_eq!(
kinds("{ } ( ) [ ] , = -> . .. ..= + - * / % $"),
alloc::vec![
TokenKind::LBrace,
TokenKind::RBrace,
TokenKind::LParen,
TokenKind::RParen,
TokenKind::LBracket,
TokenKind::RBracket,
TokenKind::Comma,
TokenKind::Eq,
TokenKind::Arrow,
TokenKind::Dot,
TokenKind::DotDot,
TokenKind::DotDotEq,
TokenKind::Plus,
TokenKind::Minus,
TokenKind::Star,
TokenKind::Slash,
TokenKind::Percent,
TokenKind::Dollar,
TokenKind::Eof,
]
);
}
#[test]
fn comments_run_to_end_of_line() {
assert_eq!(
kinds("a # comment ) ] } \n b"),
alloc::vec![
TokenKind::Ident("a".to_string()),
TokenKind::Ident("b".to_string()),
TokenKind::Eof
]
);
assert_eq!(kinds("# only a comment"), alloc::vec![TokenKind::Eof]);
}
#[test]
fn identifiers_may_contain_hyphens_between_letters() {
assert_eq!(
kinds("open-bus"),
alloc::vec![TokenKind::Ident("open-bus".to_string()), TokenKind::Eof]
);
assert_eq!(
kinds("n-1"),
alloc::vec![
TokenKind::Ident("n".to_string()),
TokenKind::Minus,
TokenKind::Num(NumLit {
value: 1,
digits: 1,
radix: Radix::Dec,
unit: NumUnit::None,
}),
TokenKind::Eof
]
);
assert_eq!(
kinds("a -> b"),
alloc::vec![
TokenKind::Ident("a".to_string()),
TokenKind::Arrow,
TokenKind::Ident("b".to_string()),
TokenKind::Eof
]
);
}
#[test]
fn numbers_in_every_base_with_separators() {
assert_eq!(num("1234").value, 1234);
assert_eq!(num("0x2000").value, 0x2000);
assert_eq!(num("0X2000").radix, Radix::Hex);
assert_eq!(num("0b1010").value, 0b1010);
assert_eq!(num("0o755").value, 0o755);
assert_eq!(num("236_250_000").value, 236_250_000);
assert_eq!(num("0xdead_beef").value, 0xdead_beef);
}
#[test]
fn size_and_duration_suffixes_scale_the_value() {
assert_eq!(num("2K").value, 2048);
assert_eq!(num("2K").digits, 2);
assert_eq!(num("2K").unit, NumUnit::Size(SizeUnit::Kilo));
assert_eq!(num("4M").value, 4 << 20);
assert_eq!(num("8G").value, 8 << 30);
assert_eq!(num("1T").value, 1 << 40);
assert_eq!(num("512KiB").value, 512 * 1024);
assert_eq!(num("4m").unit, NumUnit::Size(SizeUnit::Mega));
assert_eq!(num("1ms").unit, NumUnit::Duration(DurationUnit::Millis));
assert_eq!(num("1ms").value, 1_000_000);
assert_eq!(num("2s").value, 2_000_000_000);
assert_eq!(num("100ns").value, 100);
}
#[test]
fn strings_resolve_escapes() {
assert_eq!(
kinds(r#""a\tb\n\"c\\\x41""#),
alloc::vec![TokenKind::Str("a\tb\n\"c\\A".to_string()), TokenKind::Eof]
);
}
#[test]
fn lexical_errors_say_what_is_wrong() {
assert_eq!(error("\"abc"), "unterminated string literal");
assert_eq!(error("\"abc\n\""), "unterminated string literal");
assert_eq!(error(r#""a\q""#), "unknown escape `\\q`");
assert_eq!(
error(r#""a\xZZ""#),
"`\\x` needs exactly two hexadecimal digits"
);
assert_eq!(error("0b12"), "invalid digit `2` in a binary literal");
assert_eq!(error("0x"), "expected digits after `0x`");
assert_eq!(
error("18446744073709551616"),
"integer literal does not fit in 64 bits"
);
assert_eq!(
error("18446744073709551615K"),
"integer literal does not fit in 64 bits once its suffix is applied"
);
assert!(error("12qux").starts_with("unknown suffix `qux`"));
assert!(error("0x10K").starts_with("suffix `K` is only allowed on decimal literals"));
assert_eq!(error("@"), "unexpected character `@`");
}
#[test]
fn spans_cover_exactly_the_token() {
let text = " cpubus ";
let src = SourceFile::new("t", text);
let toks = tokenize(&src).expect("should lex");
assert_eq!(toks[0].span, Span::new(2, 8));
assert_eq!(&text[2..8], "cpubus");
assert_eq!(toks[1].span, Span::at(10));
}
#[test]
fn non_ascii_never_splits_a_character() {
let err = error("# héllo\nλ");
assert_eq!(err, "unexpected character `λ`");
}
#[test]
fn describe_is_what_the_parser_prints() {
assert_eq!(TokenKind::RBrace.describe(), "`}`");
assert_eq!(TokenKind::Eof.describe(), "end of file");
assert_eq!(TokenKind::Ident("cpu".to_string()).describe(), "`cpu`");
assert_eq!(TokenKind::Str(String::new()).describe(), "a string literal");
}
}