docs(frontend): explain source readers and PEG grammar rules

This commit is contained in:
2026-11-15 18:42:54 +08:00
parent 6da2e54bcf
commit 3a75476bc5
3 changed files with 121 additions and 0 deletions
+23
View File
@@ -1,20 +1,28 @@
// 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 {
@@ -25,32 +33,42 @@ peg::parser! {
}
}
// 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 }
@@ -71,6 +89,7 @@ peg::parser! {
/ "*" { TokenKind::Star }
/ "/" { TokenKind::Slash }
// token 按顺序尝试名称、数字、字符串、标点;未知字符作为错误值返回,避免吞掉后重试别种 Token。
rule kind() -> Result<TokenKind, Diagnostic>
= name()
/ number()
@@ -80,6 +99,7 @@ peg::parser! {
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) })
@@ -87,9 +107,12 @@ peg::parser! {
// 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)
}
}