03 符号与语义绑定(中端)
本册目标:理解 Roslyn 如何回答"这段代码是什么意思"——符号体系如何统一源码与元数据、Binder 责任链如何做名称解析与类型检查、BoundTree 这个中间表示长什么样、流分析如何判定明确赋值、SemanticModel 如何把这一切暴露给 IDE。
一、要点速览
- 符号(Symbol)是语言层实体(类型/方法/字段)的统一表示:源码符号(
Symbols/Source/)与元数据符号(Symbols/Metadata/)同宗,Retargeting/Substituted/Error符号处理特殊场景。 - 绑定(Binding)由Binder 责任链完成:每个语法位置按作用域嵌套组装一串 Binder(最内层 lambda → 最外层 compilation),名称查找沿链向上。
- BoundTree是绑定的产物:语法节点 → Bound 节点(
BoundCall、BoundBinaryOperator…),补全了语义(目标方法、隐式转换、常量值)。 - 流分析(
FlowAnalysis/)在 BoundTree 上做数据流/控制流分析,产出 CS0164(未使用变量)、CS0162(不可达代码)、明确赋值错误等诊断。 - SemanticModel是把上述能力按"每节点惰性查询"方式暴露给 IDE 的门面(
CSharpCompilation.GetSemanticModel,CSharpCompilation.cs:1833)。
二、符号体系(Symbols)
2.1 目录速览(src/Compilers/CSharp/Portable/Symbols/)
| 子目录/文件 | 表示什么 |
|---|---|
Source/(SourceAssemblySymbol.cs、SourceNamedTypeSymbol.cs、SourceMethodSymbol.cs…) | 本编译内源代码声明的符号——字段符号可回指到声明语法节点 |
Metadata/(PEAssemblySymbol.cs、PENamedTypeSymbol.cs…) | 引用程序集里的符号——从元数据解码,无源码语法 |
Retargeting/ | 跨引用版本对齐时的重定向符号 |
Substituted*.cs(SubstitutedMethodSymbol等) | 泛型替换后的"实例化视图"符号 |
ErrorTypeSymbol.cs、ErrorMethodSymbol.cs等 | 错误符号——名字解析失败也返回符号,让绑定继续(与语法层的"缺失 Token"同理) |
| 基础符号 | NamedTypeSymbol.cs、MethodSymbol.cs、FieldSymbol.cs、PropertySymbol.cs、LocalSymbol.cs、ParameterSymbol.cs、NamespaceSymbol.cs |
对外 API 是ISymbol系列接口(Microsoft.CodeAnalysis层),编译器内部直接使用这些具体类。
2.2 三个关键设计
① 源码/元数据同构。SourceNamedTypeSymbol与PENamedTypeSymbol都派生自NamedTypeSymbol——绑定代码不关心"这个类型来自本项目还是引用的 dll"。这使"源码 + 引用"天然融合为一张符号表(由ReferenceManager.cs负责合并,处理 NoPia/类型等价等边角)。
② 错误符号永不中断。Console.WritLine(...)拼错方法时,绑定产出ErrorMethodSymbol并记录诊断,调用点得到一个类型为 error 的 Bound 节点——后续绑定照常进行,因此 IDE 能在错误代码上继续工作。"可恢复的错误"是贯穿 Roslyn 前中端的原则(02 册缺失 Token、本册错误符号、04 册 BadExpression)。
③ 惰性声明周期。符号用CompletionPart(CompletionPart.cs)描述自己的构造阶段(先建骨架 → 再绑定成员签名 → 再绑定主体),按依赖图逐步完成。这避免了"解析成员 A 需要成员 B 的签名、B 又引用 A"的先有鸡先有蛋问题。
三、Binder:责任链式绑定
3.1 文件结构(src/Compilers/CSharp/Portable/Binder/)
Binder是一个分部类大家族(每个语言功能一个文件):
Binder.cs 链骨架:Next 指针、通用入口 Binder_Expressions.cs 表达式绑定(BindExpression 总入口) Binder_Statements.cs 语句绑定 Binder_Invocation.cs 调用:重载决议 Binder_Operators.cs 运算符:一元/二元/用户定义运算符查找 Binder_Conversions.cs 类型转换(隐式/显式/用户定义) Binder_Lambda.cs / Binder_Await.cs / Binder_Query.cs / Binder_Patterns.cs … Binder_Lookup.cs 名称查找核心 BlockBinder.cs / BuckStopsHereBinder.cs / ExecutableCodeBinder.cs … 各层作用域 BinderFactory.cs ★ 按语法位置组装 Binder 链3.2 链的组装:BinderFactory
BinderFactory.cs:101:
internalBinderGetBinder(SyntaxNodenode,CSharpSyntaxNodememberDeclarationOpt=null,SymbolmemberOpt=null)给定任意语法位置(IDE 光标处的一个表达式、或发射时要绑定的方法体),BinderFactory(配合BinderFactory.BinderFactoryVisitor.cs按语法节点类型分派)组装出一条从内到外的 Binder 链,例如:
lambda 参数 binder → 局部变量 binder(BlockBinder)→ 成员 binder(方法内) → 类型成员 binder → 命名空间/using binder(AliasAndUsingDirective) → BuckStopsHereBinder(compilation 根,报告"到此还没找到")名称查找(Binder_Lookup.cs)就是沿Next逐层询问,内层命中即止——这正是 C# 的名称遮蔽(shadowing)语义。
3.3 表达式绑定入口
Binder_Expressions.cs:322:
publicBoundExpressionBindExpression(ExpressionSyntaxnode,DiagnosticBagdiagnostics){returnBindExpression(node,diagnostics,invoked:false,indexed:false);}protectedBoundExpressionBindExpression(ExpressionSyntaxnode,DiagnosticBagdiagnostics,boolinvoked,boolindexed){BoundExpressionexpr=BindExpressionInternal(node,diagnostics,invoked,indexed);// L329:真正分派VerifyUnchecked(node,diagnostics,expr);...}BindExpressionInternal(L365)按node.Kind()大 switch 分派到Binder_Invocation/Binder_Operators/...各专文件。重载决议(OverloadResolution,在Binder_Invocation.cs及../Core公共算法协助下)在这里决定M(1)到底调用哪个M,结果直接写进BoundCall.Function。
读代码路线:BindExpression→BindExpressionInternal→ 挑一个简单 case(Binder_Expressions里字面量/标识符分支)→ 再看Binder_Invocation的调用链。带M(x)样例断点单步最佳。
四、BoundTree:绑定的中间表示
4.1 与语法树的差别
| SyntaxTree(02 册) | BoundTree(本册) | |
|---|---|---|
| 表示 | 表面形式——代码怎么写的 | 语义——代码什么意思 |
| 节点 | BinaryExpressionSyntax(a, +, b) | BoundBinaryOperator(Add, BoundField a, BoundField b, 方法, 转换) |
| 存储 | 绿树持久化,随源码永久存在 | 每方法体按需生成,供降级/CodeGen 消费 |
| 例子 | x++有专门节点 | 可能被改写为x = x + 1(BoundAssignmentOperator) |
| 例子 | var s = "a" + 1; | 节点上已标注 string 拼接的隐藏调用与转换 |
目录:src/Compilers/CSharp/Portable/BoundTree/——BoundNode.cs、BoundExpression.cs、Expression.cs(上百个 Bound 节点类)、BoundTreeWalker.cs、BoundTreeRewriter.cs(后两者是 04 册降级器遍历/改写的基础设施)。
4.2 为什么要有中间层
- 消除表面差异:
foreach/using/查询表达式/模式匹配绑定后都变成基础 Bound 节点组合,04 册的代码生成器因此只需处理"小而稳"的节点集; - 挂接语义决策:重载决议结果、转换种类、常量折叠值(
ConstantValue)都在 Bound 节点上,CodeGen 不再查表; - 解耦:语法 API(对用户稳定)与编译器内部表示(可随版本重构)互不拖累。
五、流分析(FlowAnalysis)
目录:src/Compilers/CSharp/Portable/FlowAnalysis/。在 BoundTree(语句层面)上做经典数据流/控制流遍历:
| 文件 | 干什么 |
|---|---|
AbstractFlowPass.cs | 所有流遍历的骨架(控制流图式的语句遍历 + 状态合并) |
DataFlowPass.cs | 明确赋值分析:CS0165 使用了未赋值的局部变量 |
ControlFlowPass.cs | 可达性:CS0162 检测到无法访问的代码;CS8020 无返回值等 |
UnassignedFieldsReadWalker.cs、AlwaysAssignedWalker.cs、DataFlowsInWalker.cs… | 支撑Compilation公开的数据流 API 与各类诊断 |
分析方法论的样本:DataFlowPass对每个变量维护"已赋值/未赋值"状态,分支处合并状态(并集),使用点检查——教科书式的数据流分析在真实编译器里的样子。
六、SemanticModel:把绑定暴露给 IDE
CSharpCompilation.cs:1833:
publicnewSemanticModelGetSemanticModel(SyntaxTreesyntaxTree,boolignoreAccessibility)=>newSyntaxTreeSemanticModel(this,(SyntaxTree)syntaxTree,ignoreAccessibility);SyntaxTreeSemanticModel(Compilation/SyntaxTreeSemanticModel.cs)做两件事:
- 门面 + 缓存:
GetSymbolInfo(node)、GetTypeInfo(node)、GetDeclaredSymbol(declSyntax)都按"语法节点"粒度缓存结果——同一节点反复查询只绑定一次; - 微语法 → 声明:把"表达式里的某个子表达式"定位到合适的 Binder(通过
BinderFactory),执行与编译时完全一致的绑定路径。
这就是 IDE"转到定义/查找所有引用/波浪线"背后的机制:编辑器与编译器共用同一套绑定代码。也解释了为什么编辑器诊断与命令行编译诊断通常一致——它们是同一份代码的两次运行。
七、实验:语义查询窥探
引用本地Microsoft.CodeAnalysis.dll+Microsoft.CodeAnalysis.CSharp.dll(roslyn-src/Binaries/Debug/Dlls/):
vartree=CSharpSyntaxTree.ParseText(@" using System; class C { int total; void M(int n) { var sum = 0; for (var i = 0; i < n; i++) { sum += i; } // 试着改成使用未赋值变量看 CS0165 total = sum; Console.WriteLine(total); } }");varcomp=CSharpCompilation.Create("probe",new[]{tree},new[]{MetadataReference.CreateFromFile(typeof(object).Assembly.Location)});varmodel=comp.GetSemanticModel(tree);varident=tree.GetRoot().DescendantNodes().OfType<IdentifierNameSyntax>().First(id=>id.Identifier.Text=="n");Console.WriteLine(model.GetSymbolInfo(ident).Symbol);// ParameterSymbol n (parameter)Console.WriteLine(model.GetTypeInfo(ident).Type);// System.Int32varsumDecl=tree.GetRoot().DescendantNodes().OfType<VariableDeclaratorSyntax>().First();Console.WriteLine(model.GetDeclaredSymbol(sumDecl));// LocalSymbol sumforeach(vardincomp.GetDiagnostics())// 试试引入错误,观察诊断码Console.WriteLine(d);改代码观察:把var sum = 0;改成未初始化使用 →GetDiagnostics()出现 CS0165(流分析介入);把n拼错 →GetSymbolInfo返回错误符号(SymbolKind.ErrorType相关行为)。
八、代码位置索引
| 内容 | 位置(相对roslyn-src/src/Compilers/CSharp/Portable/) |
|---|---|
Binder 链骨架(Next) | Binder/Binder.cs:36(构造) |
| Binder 链组装 | Binder/BinderFactory.cs:101、Binder/BinderFactory.BinderFactoryVisitor.cs |
| 表达式绑定入口 | Binder/Binder_Expressions.cs:322(分派:365 BindExpressionInternal) |
| 重载决议 | Binder/Binder_Invocation.cs(配合../Core公共部分) |
| 名称查找 | Binder/Binder_Lookup.cs |
| 源码符号 | Symbols/Source/SourceAssemblySymbol.cs等 |
| 元数据符号 | Symbols/Metadata/ |
| 错误符号 | Symbols/ErrorTypeSymbol.cs、Symbols/ErrorMethodSymbol.cs |
| 引用合并 | Symbols/ReferenceManager.cs |
| Bound 节点与遍历器 | BoundTree/BoundNode.cs、BoundTree/BoundTreeWalker.cs、BoundTree/BoundTreeRewriter.cs |
| 确定赋值分析 | FlowAnalysis/DataFlowPass.cs、FlowAnalysis/AbstractFlowPass.cs |
| 可达性分析 | FlowAnalysis/ControlFlowPass.cs |
| SemanticModel 入口 | Compilation/CSharpCompilation.cs:1833、Compilation/SyntaxTreeSemanticModel.cs |
下一册:04-降级与IL代码生成.md——把 BoundTree 变成真正的 IL。