Files
endorphin-aster-vm/crates/frontend/src/lexer.peg
T

97 lines
3.7 KiB
Plaintext

peg::parser! {
grammar lexical() for str {
rule trivia()
= ([' ' | '\t' | '\r' | '\n'] / "//" [^'\r' | '\n']*)*
rule name_start()
= [character if character == '_' || character.is_alphabetic()]
rule name_continue()
= [character if character == '_' || character.is_alphanumeric()]
rule name() -> Result<TokenKind, Diagnostic>
= name_start() name_continue()* { Ok(TokenKind::Name) }
rule number() -> Result<TokenKind, Diagnostic>
= start:position!() ['0'..='9']+ ("." ['0'..='9']+)?
suffix:$(name_continue()*) end:position!() {
if suffix.is_empty() {
Ok(TokenKind::Number)
} else {
Err(Diagnostic::lexical(
Span::new(start, end),
"unsupported numeric suffix or missing token separator",
))
}
}
rule escape()
= "\\" ['n' | 't' | '"' | '\\']
rule string_character()
= [^'"' | '\\' | '\r' | '\n' | '\0']
rule string_end(start: usize) -> Result<(), Diagnostic>
= "\"" { Ok(()) }
/ position:position!() "\\" [_] end:position!() {
Err(Diagnostic::lexical(Span::new(position, end), "unsupported string escape"))
}
/ "\\" end:position!() ![_] {
Err(Diagnostic::lexical(Span::new(start, end), "unterminated string"))
}
/ position:position!() ['\r' | '\n' | '\0'] end:position!() {
Err(Diagnostic::lexical(Span::new(position, end), "unescaped newline or NUL in string"))
}
/ end:position!() ![_] {
Err(Diagnostic::lexical(Span::new(start, end), "unterminated string"))
}
rule string() -> Result<TokenKind, Diagnostic>
= start:position!() "\"" (escape() / string_character())* end:string_end(start) {
end.map(|()| TokenKind::String)
}
rule punctuation() -> TokenKind
= "(" { TokenKind::LeftParen }
/ ")" { TokenKind::RightParen }
/ "{" { TokenKind::LeftBrace }
/ "}" { TokenKind::RightBrace }
/ ":" { TokenKind::Colon }
/ "," { TokenKind::Comma }
/ "." { TokenKind::Dot }
/ "==" { TokenKind::EqualEqual }
/ "!=" { TokenKind::NotEqual }
/ "<=" { TokenKind::LessEqual }
/ ">=" { TokenKind::GreaterEqual }
/ "<" { TokenKind::Less }
/ ">" { TokenKind::Greater }
/ "=" { TokenKind::Equal }
/ "+" { TokenKind::Plus }
/ "-" { TokenKind::Minus }
/ "*" { TokenKind::Star }
/ "/" { TokenKind::Slash }
rule kind() -> Result<TokenKind, Diagnostic>
= name()
/ number()
/ string()
/ kind:punctuation() { Ok(kind) }
/ start:position!() [_] end:position!() {
Err(Diagnostic::lexical(Span::new(start, end), "unexpected source character"))
}
rule token() -> Result<Token, Diagnostic>
= start:position!() kind:kind() end:position!() {
kind.map(|kind| Token { kind, span: Span::new(start, end) })
}
// Scan only one prefix. Errors are returned as values, so a matched
// malformed number/string cannot backtrack into another token kind.
#[no_eof]
pub rule next() -> (usize, Result<Option<Token>, Diagnostic>)
= trivia() result:(token:token() { token.map(Some) } / ![_] { Ok(None) }) end:position!() {
(end, result)
}
}
}