news 2026/11/2 11:20:29

02 词法与语法分析(前端)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
02 词法与语法分析(前端)

02 词法与语法分析(前端)

本册目标:搞清楚源代码文本如何变成语法树——Lexer 如何扫描 Token、LanguageParser 如何用递归下降 + 优先级爬升建树、红绿树为何能做到编辑时 O(1) 复用、Syntax API 长什么样、语法错误如何恢复。


一、要点速览

  1. 词法:Lexer(4785 行)逐字符扫描,产出SyntaxToken序列;空白与注释作为Trivia挂在 Token 上,不进语法树主体。
  2. 语法:LanguageParser(11539 行)是手写递归下降分析器;二元表达式用优先级爬升(precedence climbing)而非传统算符优先文法表。
  3. 红绿树:语法树分两层——内部"绿节点"(GreenNode,不可变、无父指针、全局缓存)与对外"红节点"(SyntaxNodefaçade,按需创建)。这是 IDE 高性能编辑的关键。
  4. 错误恢复:SyntaxParser.EatToken系列在缺 Token 时插入"缺失节点"并继续解析,保证一棵完整(但不完美)的树总能产出。
  5. 所有节点的种类用SyntaxKind枚举(ushort)标识,是贯穿前端的总开关。

二、词法分析(Lexer)

2.1 入口与文件

文件(src/Compilers/CSharp/Portable/Parser/)行数职责
AbstractLexer.cs—词法器骨架(缓冲区、位置管理)
Lexer.cs4785C# 词法器主体:普通 Token、指令(#if…)、XML 文档注释
QuickScanner.cs433快速路径:常见 Token(标识符、运算符)的免分配扫描

2.2 主循环:Lex()

Lexer.cs:235,词法器按"模式"分派(普通代码 / 预处理指令 / XML 注释的多种子模式):

publicSyntaxTokenLex(LexerModemode){_mode=mode;switch(_mode){caseLexerMode.Syntax:caseLexerMode.DebuggerSyntax:returnthis.QuickScanSyntaxToken()??this.LexSyntaxToken();// L245caseLexerMode.Directive:returnthis.LexDirectiveToken();}// ...XML 文档注释的各种模式(L250-276)}

性能设计值得注意:普通代码 Token 先走QuickScanner.cs:193的QuickScanSyntaxToken()——一个手写的、只为"最常见 Token 形态"服务的快速路径,失败才回退到完整的LexSyntaxToken()(Lexer.cs:297)。这是编译器里典型的"快慢双路径"优化。

2.3 Token 的解剖:TokenInfo

Lexer.cs:84定义了扫描一个 Token 时的临时信息结构:

internalstructTokenInfo{internalSyntaxKindKind;// SyntaxKind.IdentifierToken / NumericLiteralToken / ...internalSyntaxKindContextualKind;// 上下文关键字(如 async)在语法阶段的"第二身份"internalstringText;internalstringStringValue;internalstringIdentifierName;...}

ContextualKind值得记住:async、partial、var这类上下文关键字在词法阶段仍是普通标识符,只是额外登记了"可能是关键字"的候选身份,真正决策推迟到语法/绑定阶段。

2.4 Trivia:空白与注释的去处

语法树的主体只包含有语义的节点;空格、换行、注释、预处理指令统称Trivia,按规则挂到 Token 上:

  • 一个 Token 最多带1 个 trailing trivia(本行内其后的空白/注释);
  • 其后直到下一个 Token 之前的所有 trivia 都算它的leading trivia;
  • 预处理指令(#if、#pragma)也是结构化 trivia(StructuredTriviaSyntax),因此 IDE 折叠/着色能精确定位。

为什么这样设计?——①语法结构不受排版影响,树可以任意格式化后再打印;②重构重写节点时 trivia 随 Token 走,最大程度保留用户格式。


三、语法分析(LanguageParser)

3.1 总入口

方法(Parser/LanguageParser.cs)行号作用
ParseCompilationUnit()298整个文件入口 →CompilationUnitSyntax
ParseStatement()6464语句分派(if/for/while/表达式语句…)
ParseSubExpression(Precedence)8889一切表达式的总入口

LanguageParser继承自SyntaxParser.cs(1111 行),后者封装了"从词法器流式取 Token"的基础设施:EatToken()(SyntaxParser.cs:435)、PeekToken、前看缓冲等。

3.2 递归下降 + 优先级爬升

C# 的语句/声明部分是典型的手写递归下降(一个ParseStatement大 switch)。表达式则采用优先级爬升,核心在LanguageParser.cs:8724的Precedence枚举:

enumPrecedence:uint{Expression=0,// 最宽松,接受一切表达式Assignment,Lambda=Assignment,// "=> 与赋值同级、右结合"(语言规范的直接体现)Ternary,Coalescing,ConditionalOr,ConditionalAnd,LogicalOr,LogicalXor,LogicalAnd,Equality,Relational,Shift,Additive,Mutiplicative,Unary,Cast,PointerIndirection,AddressOf,Primary_UNUSED,// 基础表达式按 ad-hoc 方式解析}

运算符 → 优先级的映射在GetPrecedence(SyntaxKind)(L8748)。解析循环(ParseSubExpressionCore,L8901)的工作方式:

  1. 先解析一个操作数(基础表达式:字面量、标识符、括号…);
  2. 看下一个 Token 是什么运算符,查GetPrecedence得newPrecedence;
  3. 若newPrecedence高于(或同级且右结合)当前precedence,就把刚解析的操作数当左操作数,递归ParseSubExpression(newPrecedence)解析右侧(L9052–9057 的BinaryExpression分支即此);
  4. 循环直至遇到优先级不够的运算符,返回。

读代码建议:在ParseSubExpressionCore(L8901)打断点,编译a = b + c * d,单步看优先级如何驱动结合方向——这是理解本节最快的方式。

3.3 为什么不用语法生成器(ANTLR/Yacc)?

  1. C# 语法里有大量上下文依赖(泛型 vs 比较运算符的歧义、查询表达式、模式匹配),手写代码能自然表达"试试看"式的回溯(isDeclaration试探等);
  2. 错误恢复策略需要精细控制(见 3.4),生成器难以定制;
  3. 性能:手写解析器可以针对热路径做QuickScanner这类特化。

3.4 错误恢复:残缺输入也要长出完整的树

语法错误的处理原则是绝不抛异常中断,而是:

  • EatToken(SyntaxKind kind)(SyntaxParser.cs:463/518):期望的 Token 不在时,产出一个IsMissing的占位 Token 并报告诊断(如 CS1002 应输入;),解析继续;
  • 坏 Token 被"吞掉"并挂为 skipped trivia,使后续正确代码仍能进入树;
  • 因此任何乱码输入都能得到一棵结构完整(局部带 Omitted/Skipped 节点)的语法树——这是 IDE 在残缺代码上仍能提供补全/绑定的前提。

四、红绿树:Roslyn 最著名的工程决策

4.1 两层节点

层基类特征
绿节点(内部)GreenNode(src/Compilers/Core/Portable/Syntax/GreenNode.cs:17,internal abstract)不可变;没有父指针;只存宽度不存绝对位置;引用相等即结构相等(配合节点缓存)
红节点(对外 API)SyntaxNode(src/Compilers/Core/Portable/Syntax/SyntaxNode.cs:22,public abstract partial)持父指针与绝对位置(Span);按需从绿节点包装生成,本身轻量

C# 特化的红节点基类是CSharpSyntaxNode(Syntax/CSharpSyntaxNode.cs:25):

publicabstractpartialclassCSharpSyntaxNode:SyntaxNode,IFormattable

绿节点版本在Syntax/InternalSyntax/(如InternalSyntax/SyntaxToken.cs475 行),解析器(Parser)全程只操作绿节点;你用 API 拿到的SyntaxNode都是事后包装的红 façade。

4.2 为什么这样能 O(1) 复用

树不可变 + 绿节点无父指针 → 任何未修改的子树在"编辑后新树"里被原样共享:

编辑前: CompilationUnit ─ classDecl ─ methodDecl ─ body ─ stmt1, stmt2 编辑 stmt2: CompilationUnit'─ classDecl' ─ methodDecl'─ body' ─ stmt1(共享), stmt2'(新)

ReplaceNode只需沿着路径新建 O(树高) 个绿节点,其余全部复用;配合Microsoft.CodeAnalysis.CSharp的节点缓存(InternalSyntax/CSharpSyntaxNodeCache.cs),常见小子树(短标识符、小字面量)甚至是跨编辑共享的。IDE 每个按键触发一次全量"重建语法树"因此变得廉价——这是 Roslyn 能支撑现代 IDE 体验的基石。

红节点则是另一重缓存:parent、Span等派生信息只在被查询时才计算。记住口诀:绿管结构(不可变、可共享),红管导航(父指针、位置)。

4.3 SyntaxKind:前端的总开关

Syntax/SyntaxKind.cs:6:

publicenumSyntaxKind:ushort

用ushort而非int:每个绿节点都要存一个 Kind,内存减半在百万节点的树上很可观。树里所有"种类"——SyntaxKind.ClassDeclaration、SyntaxKind.IdentifierToken、SyntaxKind.WhitespaceTrivia——共用这一个枚举。


五、Syntax API 使用者视角速写

读懂前两节后,这些 API 的行为就有了着落:

vartree=CSharpSyntaxTree.ParseText(@"class C { void M() { } }");varroot=(CompilationUnitSyntax)tree.GetRoot();// 红节点入口varcls=root.Members.OfType<ClassDeclarationSyntax>().Single();varm=cls.Members.OfType<MethodDeclarationSyntax>().Single();m.Span// 绝对位置(红节点派生)m.Identifier// SyntaxToken:绿色数据的红包装m.Body.OpenBraceToken.LeadingTrivia// 挂在 { 上的前导 triviavarm2=m.WithIdentifier(SyntaxFactory.ParseToken("N"));// 产生新树,旧树不变

遍历/改写基础设施(全部由红绿树结构免费获得):

  • CSharpSyntaxWalker(Syntax/CSharpSyntaxWalker.cs)—— visitor 遍历;
  • CSharpSyntaxRewriter(Syntax/CSharpSyntaxRewriter.cs)—— visitor 改写(返回新树)。

六、实验:语法树转储

用本地构建产物(roslyn-src/Binaries/Debug/Dlls/,或main/packages中被覆盖过的 2.10 包)写一个 30 行的控制台程序:

// 引用 Microsoft.CodeAnalysis.dll 与 Microsoft.CodeAnalysis.CSharp.dllvartree=CSharpSyntaxTree.ParseText("class C { int x = a + b * c; }",CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest));tree.GetRoot().WriteTo(Console.Out);// 打印规范化源码(Trivia 被剥离的体现)foreach(vartokintree.GetRoot().DescendantTokens())Console.WriteLine($"{tok.Kind()}'{tok.Text}' leading={tok.LeadingTrivia}");

观察三件事:①WriteTo输出与原文的空白差异(Trivia 不属于结构);②a + b * c的树形(BinaryExpression(Add, a, BinaryExpression(Multiply, b, c)));③每个 Token 的 trivia 分布。把LanguageVersion换成CSharp5再跑,体会语言版本在语法层就生效(某些语法直接报错)。


七、代码位置索引

内容位置(相对roslyn-src/src/Compilers/CSharp/Portable/)
Token 信息结构TokenInfoParser/Lexer.cs:84
词法主入口Lex()Parser/Lexer.cs:235
完整扫描LexSyntaxToken()Parser/Lexer.cs:297
快速路径QuickScanSyntaxToken()Parser/QuickScanner.cs:193
语法总入口ParseCompilationUnit()Parser/LanguageParser.cs:298
语句分派ParseStatement()Parser/LanguageParser.cs:6464
表达式入口ParseSubExpression()Parser/LanguageParser.cs:8889
优先级枚举 / 映射Parser/LanguageParser.cs:8724/:8748
取 Token 基础设施EatToken系Parser/SyntaxParser.cs:435-533
绿节点基类GreenNode../Core/Portable/Syntax/GreenNode.cs:17
红节点基类SyntaxNode../Core/Portable/Syntax/SyntaxNode.cs:22
C# 红节点基类Syntax/CSharpSyntaxNode.cs:25
C# 绿 TokenSyntax/InternalSyntax/SyntaxToken.cs
节点缓存Syntax/InternalSyntax/CSharpSyntaxNodeCache.cs
SyntaxKind枚举Syntax/SyntaxKind.cs:6
遍历/改写器Syntax/CSharpSyntaxWalker.cs、Syntax/CSharpSyntaxRewriter.cs
语法树实现(红 façade 总目录)Syntax/CSharpSyntaxTree.cs及同目录各*Syntax.cs

下一册:03-符号与语义绑定.md——语法树只回答"长什么样",绑定阶段回答"是什么意思"。

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

从 Virtual Workspace 到 MCP,看懂 GitHub Copilot 如何真正进入 ABAP 开发流程

这段话真正有价值的地方,并不只是 SAP 在说 GitHub Copilot 可以配合 ABAP Development Tools for Visual Studio Code 使用。更关键的是,它透露了 SAP 为 ABAP 进入现代 Agentic Development 世界所选择的一条技术路线。 读懂这句话,需要把几个容易混在一起的概念拆开来看…

作者头像 李华
网站建设 2026/11/2 11:18:08

SAP TechEd 2026 之后,ABAP 开发正在从 AI 辅助走向 Agentic AI

2026 年 10 月的 SAP TechEd Berlin,有一条关于 ABAP 的变化很容易被低估。 过去两三年,我们谈 ABAP 和生成式 AI,关注点大多还是 Copilot 式体验。开发人员写一句需求,AI 帮忙生成一段代码,解释一个 Class,补一段 ABAP Unit,或者告诉我们某个 CDS View 为什么激活失败…

作者头像 李华
网站建设 2026/11/2 11:14:41

AI创业者通识日报 | 2026年10月29日

AI创业者通识日报 | 2026年10月29日 &#x1f4d6; 首屏导读 本教程配套付费专栏&#xff1a;《大模型工程师修炼手记》 19.9 元&#xff08;AI 编程 Agent 实战 本文同主题系统课程&#xff09; 《AI时代程序员的自我提升》 49.9 元&#xff08;AI 时代成长方法论&#xff…

作者头像 李华
网站建设 2026/11/2 11:13:59

从小白到程序员:大模型Agent开发入门指南,抓住2026年蓝海机遇!

本文对比了大模型算法岗与Agent开发岗的异同&#xff0c;指出大模型算法岗薪资高但门槛极高&#xff0c;适合少数顶尖人才&#xff1b;而Agent开发岗则更注重AI工程化落地&#xff0c;适合大多数程序员和初学者&#xff0c;且市场前景广阔&#xff0c;薪资待遇优厚。文章建议关…

作者头像 李华
网站建设 2026/11/2 11:11:03

五条同时绷紧的线:孩子厌学究竟卡在哪

孩子不是某一天突然说不去学校了&#xff0c;而是在很长一段时间里&#xff0c;功课一点点落下&#xff0c;情绪一点点往里收&#xff0c;直到有一天撑不住。家长这才慌了神&#xff0c;转而在孩子身上找原因——是不是屏幕看太久&#xff0c;是不是太松懈。我们的经验是&#…

作者头像 李华