rustc 词法分析与解析(Lexing Parsing):编译器前端的入口与源码级实现剖析
rustc 词法分析与解析Lexing Parsing编译器前端的入口与源码级实现剖析【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust本文基于 Rust 官方开发者指南中“词法分析与解析”Lexing and parsing章节结合当前仓库rust 编译器源码树中的rustc_lexer、rustc_parse、rustc_ast等真实源码系统讲解 rustc 如何处理一段 UTF-8 源码从把字符串切分为 token 流到把 token 流组织成抽象语法树AST再到解析会话ParseSess、手写词法分析器、解析器子模块划分与限制性位掩码Restrictions等实现细节。读完本文你将理解 rustc 前端第一阶段的完整数据流、各 crate 的职责边界以及定位解析相关 bug 时应该去哪里读代码。编译器要做的第一件事Lexing 与 Parsing编译器拿到的第一个东西是一段程序源码UTF-8 编码的 Unicode 文本。在把文本变成编译器内部能高效处理的数据格式之前必须经过两个阶段词法分析Lexing把字符串切分成 token 的流。例如foo.bar buz会被切分为 5 个 tokenfoo、.、bar、、buz。这一层由独立的 rustc_lexer crate 实现。解析Parsing把 token 流组织成结构化形式即抽象语法树Abstract Syntax TreeAST这是后续所有编译阶段工作的基础。AST 的定义位于 rustc_ast其中同时包含 token 与 token 流的定义、修改 AST 的数据结构/特征trait以及词法分析、宏展开等其他 AST 相关模块共享的类型。从源码结构看这个“两阶段”的分工在 crate 级别被刻意切干净了阶段所在 crate核心职责底层切词rustc_lexer纯词法直接操作str不感知 span 与报错集成词法rustc_parse::lexer给底层 token 补上Span、对标识符做 interning解析rustc_parse::parser从 token 流构建 ASTAST 数据结构rustc_ast定义ast::Crate、Token、TokenStream、NodeId、visit 等AST内存中镜像整个 Rust 程序AST 与 SpanAST 在内存中镜像一个 Rust 程序的结构并用Span把每个 AST 节点关联回它的原始源码文本。Span是 rustc 中一切诊断错误、警告、提示能精确指向源码行列的根基。NodeIdcrate 内唯一、但增量不友好AST 中的每个节点都有自己的NodeId——不仅包括 struct 这类顶层 item也包括单独的语句和表达式。NodeId是 crate 内部唯一标识一个 AST 节点的编号。NodeId有一个关键的工程性质它是crate 内绝对编号。这意味着在 AST 中插入或删除任何一个节点都会导致其后所有NodeId全部变动。而增量编译incremental compilation恰恰希望尽可能少的东西发生变化所以从源码结构看NodeId对增量编译几乎无用——这也是为什么 rustc 后续的 HIR、MIR 等表示会引入更稳定的 ID 体系。NodeId主要服务于直接操作 AST 的那些前端阶段宏展开和名称解析resolve。相关定义见 node_id.rsvisit.rs与mut_visit.rs则提供了遍历visit和变换mut visitAST 的通用接口expand/目录存放宏展开过程中使用的 AST 扩展结构如 typetree。解析器入口rustc_parse 的调用链解析器定义在 rustc_parse 中该 crate 除了解析器本体还包含面向 lexer 的高层接口以及一些在宏展开之后运行的校验例程。真正的解析实现位于rustc_parse::parser模块。从源文件到 Crate 的完整入口rustc_parse顶层lib.rs暴露的主入口是各种parse_*函数与工厂函数典型调用链如下从文件名创建解析器new_parser_from_file——通过ParseSess持有的SourceMap::load_file读入源码。注意其中的错误处理细节文件不存在时会在父目录里用编辑距离find_best_match_for_name找一个最相似的文件名并提示 “you might have meant to open …”文件不是合法 UTF-8 时utf8_error会定位到具体非法字节并生成带 span 的诊断。从源码字符串创建解析器new_parser_from_source_str——先new_source_file注册一个SourceFile再走同一条路径。token 流生成new_parser_from_source_file内部调用source_file_to_stream后者最终调用lexer::lex_token_trees把整个文件词法化为一个TokenStream然后Parser::new(psess, stream, None)构建解析器若第一个 token 就是Eof空文件会为其补一个指向文件末尾的 span。执行解析得到Crate拿到Parser后调用各parse_*方法逐项解析 item最终得到根 AST 节点rustc_ast::ast::Crate。此外还有两个值得注意的辅助入口source_str_to_stream直接对一段源码字符串做词法化得到 token 树序列常用于宏参数、测试等场景。它目前只剥离 shebang不剥离 frontmatter代码中的 FIXME 注释说明了对 edition 兼容性的考虑。parse_in把任意TokenStream交给一个子解析器闭包f处理并要求处理完后 token 必须恰好耗尽否则报unexpected。这是“对一段独立 token 流做子解析”的标准姿势。错误处理上所有入口都返回Result_, VecDiag的“缓冲诊断”风格失败时错误不会立即打印而必须通过unwrap_or_emit_fatallib.rs 中定义统一发射否则诊断被 drop 时会 panic。这让调用方有机会在收集到多个错误后统一决策。生命周期绑定 ParseSess以最小拷贝为目标文档特别指出一个设计决策为了最小化拷贝Lexer和Parser都带一个绑定到父ParseSess的生命周期。也就是说你无法把Parser单独存起来跨会话使用——它只在一次解析期间有效。从 rustc_session/src/parse.rs 的源码可以看到ParseSess的实际构成诊断上下文DiagCtxt、edition、ArcSourceMap、缓冲的 early lintbuffered_lints、特性门控收集器GatedSpans记录哪些 feature 在哪些 span 被使用供后续check_crate检查、SymbolGallery记录符号首次出现位置、AttrIdGenerator等。解析期间需要的一切信息都集中在这里Parser借住borrow而非拥有这些状态这正是生命周期绑定的收益。词法分析手写词法器 rustc_lexer 与集成层 Lexer词法分析的代码分布在两个 crate 中这一分层是理解 rustc 前端复用性的关键。rustc_lexer纯函数式的手写词法器rustc_lexer负责把一段str切分为构成 token 的片段。虽然业界很流行用生成的有限状态机实现 lexer但 rustc 的词法器是完全手写的。其 crate 文档明确了设计目标可复用把“纯词法”与 rustc 特有关注点span、报错、interning分离。因此rustc_lexer直接操作str产出的是简单 token——“类型标签 一小段原始文本”它不报告错误而是把错误作为标志位flag存到 token 上产出的 token 尚不足以直接用于解析 Rust 语法真正供解析器使用的是rustc_parse::lexer转换出的“wide token”。核心数据结构/// Parsed token. /// It doesnt contain information about data that has been parsed, /// only the type of the token and its size. #[derive(Debug)] pub struct Token { pub kind: TokenKind, pub len: u32, }Token只携带TokenKind和长度lenlib.rs真正的文本始终由调用方按(起点, len)从原字符串中截取——这是又一个“零拷贝”设计。TokenKind枚举覆盖了 Rust 词法的方方面面行/块注释块注释可嵌套如/* /* */不会终结、空白、Ident、含 emoji 的InvalidIdent、raw identifierr#ident、未知字面量前缀UnknownPrefix、Literal { kind, suffix_start }、lifetime、各种运算符等。从源码结构看还有一个值得注意的“性能护栏”集成层 rustc_parse/src/lexer/mod.rs 中用static_assert_size!(rustc_lexer::Token, 12)断言底层Token在 64 位平台上不超过 12 字节——因为它在词法化循环中被高频创建任何意外的膨胀都会直接影响编译速度注释解释了为何断言放在本 crate 而非rustc_lexer后者不能依赖rustc_data_structures。此外 rustc_parse/src/lib.rs 中有一组编译期断言强制rustc_lexer、rustc_span、unicode-normalization、unicode-width使用同一 Unicode 版本——这保证标识符合法性判断与字符宽度/规范化计算口径一致。Lexer集成 span、interning 与错误报告rustc_parse::lexer::Lexer把rustc_lexer与 rustc 特有数据结构粘合起来具体来说给底层 token 补上Span信息并对标识符做 interning转为Symbol。词法化的总入口是lex_token_trees其参数StripTokens控制词法前预处理行为共有三种取值mod.rsShebangAndFrontmatter剥离 shebang 与 frontmatterShebang只剥离 shebang长得像 frontmatter 的序列按普通 Rust 词素处理Nothing什么都不剥离。内部流程是先按策略用rustc_lexer::strip_shebang截掉首行 shebang 并修正start_pos再创建Cursor::new(src, frontmatter_allowed)和Lexer { psess, start_pos, src, cursor, ... }逐 token 推进。词法错误如未闭合的字符串、失配的分隔符在diagnostics.rs中统一组织其中UnmatchedDelim结构会记录找到的分隔符、未闭合位置及候选位置用于生成精确的“未闭合括号”类诊断。解析器实现结构rustc_parse::parser 内部parser/mod.rs是整个解析器的“枢纽”按语法范畴拆分为多个子模块从源码结构看其组织方式一目了然子模块解析对象expr.rs表达式item.rsitem函数、struct、impl 等如parse_item、parse_modstmt.rs语句ty.rs/pat.rs/generics.rs/path.rs类型、模式、泛型参数、路径function.rs函数签名/头nonterminal.rs宏非终结符参数$e:expr、$p:pat等入口parse_nonterminalasm.rs/cfg_select.rs内联汇编、cfg_select!Parser维护当前 token、lookahead 能力TokenCursor/TokenStream并维护一个名为Restrictions的位掩码mod.rs用来表达“当前正在解析什么”这一上下文状态从而改变解析行为。例如STMT_EXPR限制语句位置的表达式解析。源码注释给出了经典例子开启该限制时if true {} 1被解析为两个语句if语句与对1取引用的语句关闭时则解析为按位与表达式if在左、1在右NO_STRUCT_LITERAL在需要前瞻或存在歧义的语法位置禁止结构体字面量如if Foo {} {}无法确定Foo{}是结构体字面量还是条件表达式后跟两个块。其他位还包括CONST_EXPR无花括号 const 泛型参数的更好报错、ALLOW_LETlet 链中的let表达式、IN_IF_GUARDmatch guard 缺的检测等。这种“用状态位驱动解析行为”的方式让同一个parse_expr能在不同语法位置复用。解析中遇到宏先存起来解析过程中会不断遇到宏定义或宏调用解析器不会当场展开它们而是把这些 token 存起来交给宏展开阶段处理见指南中 Macro Expansion 一章。而宏展开本身可能产生新的 token 流展开输出又需要再次解析解析出的结果里可能又暴露出新的宏——如此循环直到没有宏可展开。这正是解析rustc_parse与宏展开rustc_expand及rustc_ast::expand之间互相递归调用的原因rustc_parse顶层的fake_token_stream_for_item、fake_token_stream_for_crate等函数lib.rs就是为“把 AST 节点重新打印成文本再词法化”这一展开回路服务的。验证与测试如何在仓库中验证这些行为解析器单元测试位于 parser/tests.rs同目录还有针对 token 流行为的测试模块rustc_lexer的单元测试在 rustc_lexer/src/tests.rs覆盖各TokenKind的切分边界UI 测试层面词法/解析错误的大规模回归测试集中在 tests/ui大量*.rs与期望输出*.stderr成对出现可用于验证诊断信息AST 生成后的结构性校验见指南中的 AST 校验章节。小结一张图看清前端第一阶段的职责边界UTF-8 源码 (str) │ ▼ rustc_lexer::Cursor / Token{kind,len} ← 手写、纯词法、零拷贝、错误以标志位存于 token │ ▼ rustc_parse::lexer::Lexer ← 补 Span、标识符 interning、生成诊断 │ (StripTokens: 控制 shebang/frontmatter 剥离) ▼ TokenStream (rustc_ast::tokenstream) │ ▼ rustc_parse::parser::Parser (生命周期绑定 ParseSess) │ Restrictions 位掩码驱动上下文相关解析宏暂存待展开 ▼ ast::Crate (AST, 每节点带 NodeId 与 Span) │ ▼ 宏展开 / 名称解析等后续前端阶段对 rustc 前端的修改者而言这条链路上“改哪里”有清晰对应想调整某个词素的切分规则改rustc_lexer想给 token 加 span 语义或词法错误信息改rustc_parse::lexer想改语法结构、解析歧义或解析错误恢复改rustc_parse::parser对应子模块想改 AST 形状或 visit 接口改rustc_ast。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考