120 lines
6.3 KiB
Plaintext
120 lines
6.3 KiB
Plaintext
// lexer.rs 编译时引入,生成 lexical::next;输入为本轮尚未消费的 UTF-8 后缀,所有 position! 都相对这个后缀。
|
|
peg::parser! {
|
|
// Lexer::next 调用的字符串扫描规则;每轮消费 trivia 后至多一个 Token。
|
|
grammar lexical() for str {
|
|
// next 首先跳过 ASCII 空白和双斜杠行注释,不产生 Token;注释在 CR/LF 前结束。
|
|
rule trivia()
|
|
= ([' ' | '\t' | '\r' | '\n'] / "//" [^'\r' | '\n']*)*
|
|
|
|
// name 首字符允许下划线或 Unicode 字母,不允许数字开头。
|
|
rule name_start()
|
|
= [character if character == '_' || character.is_alphabetic()]
|
|
|
|
// name 后续字符允许下划线或 Unicode 字母/数字;number 也用它识别非法黏连后缀。
|
|
rule name_continue()
|
|
= [character if character == '_' || character.is_alphanumeric()]
|
|
|
|
// kind 的名称分支;关键字也返回 Name,具体语法上下文稍后才识别。
|
|
rule name() -> Result<TokenKind, Diagnostic>
|
|
= name_start() name_continue()* { Ok(TokenKind::Name) }
|
|
|
|
// kind 的十进制数分支;无符号整数后可接点和至少一位小数,负号留给单独操作符。
|
|
rule number() -> Result<TokenKind, Diagnostic>
|
|
= start:position!() ['0'..='9']+ ("." ['0'..='9']+)?
|
|
suffix:$(name_continue()*) end:position!() {
|
|
// 数字后没有名称字符才是完整数 Token;否则将整段作为词法错误,避免误拆成两个合法 Token。
|
|
if suffix.is_empty() {
|
|
Ok(TokenKind::Number)
|
|
} else {
|
|
Err(Diagnostic::lexical(
|
|
Span::new(start, end),
|
|
"unsupported numeric suffix or missing token separator",
|
|
))
|
|
}
|
|
}
|
|
|
|
// string 只接受换行、制表、双引号、反斜杠四种反斜杠转义,解码在 parser 的 decode_string 中进行。
|
|
rule escape()
|
|
= "\\" ['n' | 't' | '"' | '\\']
|
|
|
|
// string 的普通字符不能是引号、反斜杠、原始换行或 NUL;其他 Unicode 字符原样消费。
|
|
rule string_character()
|
|
= [^'"' | '\\' | '\r' | '\n' | '\0']
|
|
|
|
// string 主体停止后检查闭引号或报告具体错误;start 是开引号位置,供未闭合字符串定位。
|
|
rule string_end(start: usize) -> Result<(), Diagnostic>
|
|
= "\"" { Ok(()) }
|
|
// 主体已消费所有合法转义,剩余反斜杠加字符必是未支持的转义。
|
|
/ position:position!() "\\" [_] end:position!() {
|
|
Err(Diagnostic::lexical(Span::new(position, end), "unsupported string escape"))
|
|
}
|
|
// 反斜杠后直接 EOF,按从开引号到 EOF 的未闭合范围报告。
|
|
/ "\\" end:position!() ![_] {
|
|
Err(Diagnostic::lexical(Span::new(start, end), "unterminated string"))
|
|
}
|
|
// 原始换行/NUL 不可出现在字符串体内,定位具体违规字符。
|
|
/ position:position!() ['\r' | '\n' | '\0'] end:position!() {
|
|
Err(Diagnostic::lexical(Span::new(position, end), "unescaped newline or NUL in string"))
|
|
}
|
|
// 普通 EOF 也表示缺少闭引号,范围从开引号覆盖到输入结尾。
|
|
/ end:position!() ![_] {
|
|
Err(Diagnostic::lexical(Span::new(start, end), "unterminated string"))
|
|
}
|
|
|
|
// kind 的字符串分支;扫描已验证主体及终结情况,Token 跨度仍包含引号并保持未解码原文。
|
|
rule string() -> Result<TokenKind, Diagnostic>
|
|
= start:position!() "\"" (escape() / string_character())* end:string_end(start) {
|
|
// 闭引号验证通过才确认 String 种类,错误按值继续向上传递。
|
|
end.map(|()| TokenKind::String)
|
|
}
|
|
|
|
// kind 的操作符/分隔符分支;共享前缀的双字符比较必须放在单字符比较/赋值之前。
|
|
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 }
|
|
|
|
// token 按顺序尝试名称、数字、字符串、标点;未知字符作为错误值返回,避免吞掉后重试别种 Token。
|
|
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"))
|
|
}
|
|
|
|
// next 用此包装单词种类及相对字节范围;只在 kind 成功时生成 Token。
|
|
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.
|
|
// 禁用默认整段消费检查,才能逐个前缀扫描;最终 EOF 由规则内的 ![_] 判断。
|
|
#[no_eof]
|
|
// Lexer::next 的唯一公开扫描入口;允许剩余后缀尚有内容,返回本轮消费字节数和 Token/EOF/诊断。
|
|
pub rule next() -> (usize, Result<Option<Token>, Diagnostic>)
|
|
= trivia() result:(token:token() { token.map(Some) } / ![_] { Ok(None) }) end:position!() {
|
|
// end 包括前导 trivia 与当前 Token;Lexer 用它推进游标,再把结果跨度加回本轮原文起点。
|
|
(end, result)
|
|
}
|
|
}
|
|
}
|