news 2026/9/13 23:05:55

rustc 词法分析与解析(Lexing Parsing):编译器前端的入口与源码级实现剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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_lexerrustc_parserustc_ast等真实源码,系统讲解 rustc 如何处理一段 UTF-8 源码:从把字符串切分为 token 流,到把 token 流组织成抽象语法树(AST),再到解析会话(ParseSess)、手写词法分析器、解析器子模块划分与限制性位掩码(Restrictions)等实现细节。读完本文,你将理解 rustc 前端第一阶段的完整数据流、各 crate 的职责边界,以及定位解析相关 bug 时应该去哪里读代码。

编译器要做的第一件事:Lexing 与 Parsing

编译器拿到的第一个东西是一段程序源码(UTF-8 编码的 Unicode 文本)。在把文本变成编译器内部能高效处理的数据格式之前,必须经过两个阶段:

  1. 词法分析(Lexing):把字符串切分成 token 的流。例如foo.bar + buz会被切分为 5 个 token:foo.bar+buz。这一层由独立的 rustc_lexer crate 实现。
  2. 解析(Parsing):把 token 流组织成结构化形式,即抽象语法树(Abstract Syntax Tree,AST),这是后续所有编译阶段工作的基础。AST 的定义位于 rustc_ast,其中同时包含 token 与 token 流的定义、修改 AST 的数据结构/特征(trait),以及词法分析、宏展开等其他 AST 相关模块共享的类型。

从源码结构看,这个“两阶段”的分工在 crate 级别被刻意切干净了:

阶段所在 crate核心职责
底层切词rustc_lexer纯词法,直接操作&str,不感知 span 与报错
集成词法rustc_parse::lexer给底层 token 补上Span、对标识符做 interning
解析rustc_parse::parser从 token 流构建 AST
AST 数据结构rustc_ast定义ast::CrateTokenTokenStreamNodeId、visit 等

AST:内存中镜像整个 Rust 程序

AST 与 Span

AST 在内存中镜像一个 Rust 程序的结构,并用Span把每个 AST 节点关联回它的原始源码文本。Span是 rustc 中一切诊断(错误、警告、提示)能精确指向源码行列的根基。

NodeId:crate 内唯一、但增量不友好

AST 中的每个节点都有自己的NodeId——不仅包括 struct 这类顶层 item,也包括单独的语句和表达式。NodeId是 crate 内部唯一标识一个 AST 节点的编号。

NodeId有一个关键的工程性质:它是crate 内绝对编号。这意味着在 AST 中插入或删除任何一个节点,都会导致其后所有NodeId全部变动。而增量编译(incremental compilation)恰恰希望尽可能少的东西发生变化,所以从源码结构看,NodeId对增量编译几乎无用——这也是为什么 rustc 后续的 HIR、MIR 等表示会引入更稳定的 ID 体系。

NodeId主要服务于直接操作 AST 的那些前端阶段:宏展开名称解析(resolve)。相关定义见 node_id.rs,visit.rsmut_visit.rs则提供了遍历(visit)和变换(mut visit)AST 的通用接口,expand/目录存放宏展开过程中使用的 AST 扩展结构(如 typetree)。

解析器入口:rustc_parse 的调用链

解析器定义在 rustc_parse 中,该 crate 除了解析器本体,还包含面向 lexer 的高层接口,以及一些在宏展开之后运行的校验例程。真正的解析实现位于rustc_parse::parser模块。

从源文件到 Crate 的完整入口

rustc_parse顶层(lib.rs)暴露的主入口是各种parse_*函数与工厂函数,典型调用链如下:

  1. 从文件名创建解析器new_parser_from_file——通过ParseSess持有的SourceMap::load_file读入源码。注意其中的错误处理细节:文件不存在时会在父目录里用编辑距离(find_best_match_for_name)找一个最相似的文件名并提示 “you might have meant to open …”;文件不是合法 UTF-8 时,utf8_error会定位到具体非法字节并生成带 span 的诊断。
  2. 从源码字符串创建解析器new_parser_from_source_str——先new_source_file注册一个SourceFile,再走同一条路径。
  3. token 流生成new_parser_from_source_file内部调用source_file_to_stream,后者最终调用lexer::lex_token_trees把整个文件词法化为一个TokenStream,然后Parser::new(psess, stream, None)构建解析器;若第一个 token 就是Eof(空文件),会为其补一个指向文件末尾的 span。
  4. 执行解析得到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<_, Vec<Diag>>的“缓冲诊断”风格:失败时错误不会立即打印,而必须通过unwrap_or_emit_fatal(lib.rs 中定义)统一发射,否则诊断被 drop 时会 panic。这让调用方有机会在收集到多个错误后统一决策。

生命周期绑定 ParseSess:以最小拷贝为目标

文档特别指出一个设计决策:为了最小化拷贝LexerParser都带一个绑定到父ParseSess的生命周期。也就是说你无法把Parser单独存起来跨会话使用——它只在一次解析期间有效。

从 rustc_session/src/parse.rs 的源码可以看到ParseSess的实际构成:诊断上下文DiagCtxt、edition、Arc<SourceMap>、缓冲的 early lint(buffered_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 doesn't 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和长度len(lib.rs),真正的文本始终由调用方按(起点, len)从原字符串中截取——这是又一个“零拷贝”设计。TokenKind枚举覆盖了 Rust 词法的方方面面:行/块注释(块注释可嵌套,如/* /* */不会终结)、空白、Ident、含 emoji 的InvalidIdent、raw identifier(r#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_lexerrustc_spanunicode-normalizationunicode-width使用同一 Unicode 版本——这保证标识符合法性判断与字符宽度/规范化计算口径一致。

Lexer:集成 span、interning 与错误报告

rustc_parse::lexer::Lexerrustc_lexer与 rustc 特有数据结构粘合起来,具体来说:给底层 token 补上Span信息,并对标识符做 interning(转为Symbol)。

词法化的总入口是lex_token_trees,其参数StripTokens控制词法前预处理行为,共有三种取值(mod.rs):

  • ShebangAndFrontmatter:剥离 shebang 与 frontmatter;
  • Shebang:只剥离 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_itemparse_mod
stmt.rs语句
ty.rs/pat.rs/generics.rs/path.rs类型、模式、泛型参数、路径
function.rs函数签名/头
nonterminal.rs宏非终结符参数($e:expr$p:pat等),入口parse_nonterminal
asm.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_LET(let 链中的let表达式)、IN_IF_GUARD(match guard 缺=>的检测)等。这种“用状态位驱动解析行为”的方式,让同一个parse_expr能在不同语法位置复用。

解析中遇到宏:先存起来

解析过程中会不断遇到宏定义或宏调用,解析器不会当场展开它们,而是把这些 token 存起来,交给宏展开阶段处理(见指南中 Macro Expansion 一章)。而宏展开本身可能产生新的 token 流,展开输出又需要再次解析,解析出的结果里可能又暴露出新的宏——如此循环,直到没有宏可展开。这正是解析(rustc_parse)与宏展开(rustc_expandrustc_ast::expand)之间互相递归调用的原因:rustc_parse顶层的fake_token_stream_for_itemfake_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 23:04:51

STM32 ADC-DMA协同原理与uCOS3实时采样实战

1. 为什么“ADC-DMA协同”不是配置开关&#xff0c;而是电压采样系统的生死线在STM32F411CEU6这类中高端MCU上做电压采样&#xff0c;很多人以为只要打开ADC时钟、配置好采样时间、选个通道、启动转换——完事。我去年调试一个电池管理系统&#xff08;BMS&#xff09;的电压采…

作者头像 李华
网站建设 2026/9/13 23:01:52

NocoBase 如何安装、启用和升级插件

NocoBase 如何安装、启用和升级插件 【免费下载链接】nocobase NocoBase is an open-source AI no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG …

作者头像 李华
网站建设 2026/9/13 22:59:49

油烟机霍尔传感器易损坏?无感FOC方案彻底解决

前些天一位做厨电控制板的客户打电话过来&#xff0c;说他们那批油烟机返修率突然拉高了&#xff0c;拆开一看&#xff0c;霍尔传感器烧了一片&#xff0c;维修师傅一上午换了五个霍尔也没用&#xff0c;最后发现是电机端的霍尔小板被油泥整个包住&#xff0c;高温下一氧化&…

作者头像 李华
网站建设 2026/9/13 22:58:16

智能体记忆能否在模型升级后存活?记忆可移植性对照研究

智能体记忆能否在模型升级后存活?记忆可移植性对照研究 论文来源:arXiv:2609.05339v1 摘要 大语言模型驱动的智能体(Agent)普遍依赖外部记忆模块(RAG向量索引、记忆数据库)保存历史上下文、工具调用记录、任务知识。工程实践中经常发生基座模型版本升级:将同一个Agent后…

作者头像 李华
网站建设 2026/9/13 22:56:43

Wasp 生产部署补充指南:自定义域名、CDN 防护与生产就绪性

Wasp 生产部署补充指南&#xff1a;自定义域名、CDN 防护与生产就绪性 【免费下载链接】wasp The batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-s…

作者头像 李华