☰
03 符号与语义绑定(中端)
2026/11/2 6:40:14 网站建设 项目流程

03 符号与语义绑定(中端)

本册目标:理解 Roslyn 如何回答"这段代码是什么意思"——符号体系如何统一源码与元数据、Binder 责任链如何做名称解析与类型检查、BoundTree 这个中间表示长什么样、流分析如何判定明确赋值、SemanticModel 如何把这一切暴露给 IDE。


一、要点速览

  1. 符号(Symbol)是语言层实体(类型/方法/字段)的统一表示:源码符号(Symbols/Source/)与元数据符号(Symbols/Metadata/)同宗,Retargeting/Substituted/Error符号处理特殊场景。
  2. 绑定(Binding)由Binder 责任链完成:每个语法位置按作用域嵌套组装一串 Binder(最内层 lambda → 最外层 compilation),名称查找沿链向上。
  3. BoundTree是绑定的产物:语法节点 → Bound 节点(BoundCall、BoundBinaryOperator…),补全了语义(目标方法、隐式转换、常量值)。
  4. 流分析(FlowAnalysis/)在 BoundTree 上做数据流/控制流分析,产出 CS0164(未使用变量)、CS0162(不可达代码)、明确赋值错误等诊断。
  5. 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 为什么要有中间层

  1. 消除表面差异:foreach/using/查询表达式/模式匹配绑定后都变成基础 Bound 节点组合,04 册的代码生成器因此只需处理"小而稳"的节点集;
  2. 挂接语义决策:重载决议结果、转换种类、常量折叠值(ConstantValue)都在 Bound 节点上,CodeGen 不再查表;
  3. 解耦:语法 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)做两件事:

  1. 门面 + 缓存:GetSymbolInfo(node)、GetTypeInfo(node)、GetDeclaredSymbol(declSyntax)都按"语法节点"粒度缓存结果——同一节点反复查询只绑定一次;
  2. 微语法 → 声明:把"表达式里的某个子表达式"定位到合适的 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。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询