符号系统 Symbol 是Kotlin K2编译器的核心抽象,为编译器各个阶段提供统一的代码表示方式,是学习编译器的基础。K2编译器采用两套符号系统:FIR(Frontend Intermediate Representation)符号系统负责语义分析,理解代码含义;IR(Intermediate Representation)符号系统负责代码生成,将语义转换为可执行代码。
本文通过一个简单的Person.greet()
方法示例,展示两套符号系统如何协同工作,帮助读者建立对K2编译器符号系统的整体认识。
版本说明:本文基于Kotlin 2.0+版本的K2编译器进行分析。K2编译器从Kotlin 1.9开始作为Alpha版本引入,在Kotlin 2.0中成为稳定版本。文中涉及的FIR和IR符号系统是K2编译器的核心组件,与旧版K1编译器的实现有显著差异。
1. 符号系统概述
符号系统是编译器中的核心抽象,提供统一的方式来引用和管理程序中的各种声明(类、函数、属性等)。在Kotlin编译器中存在两套符号系统:
- FIR符号系统:用于前端语义分析和类型检查
- IR符号系统:用于后端代码生成和优化
1.1 符号示例
为了更好地理解符号系统,我们以一个简单的函数为例,展示它在编译过程中的不同形态:
说明:为了更直观地理解符号,下面的示例仅保留了核心内容。
示例:Kotlin源码
以Person
的greet
方法为例:
kotlin
package com.example
class Person(val name: String) {
fun greet(): String {
return "Hello, $name!"
}
}
FIR符号表示
kotlin
class FirFunctionSymbol {
val callableId: CallableId // "com.example.Person.greet"
val fir: FirFunction // FIR方法声明
}
// FIR方法声明详细结构
class FirFunction {
val name: Name // "greet"
val returnTypeRef: FirTypeRef // String类型引用
val valueParameters: List<FirValueParameter> // 参数列表(本例为空)
val body: FirBlock? // 方法体
}
IR符号表示
kotlin
class IrFunctionSymbol {
val signature: IdSignature // "com.example.Person.greet"的二进制签名
val owner: IrFunction // IR方法声明
}
// IR方法声明详细结构
class IrFunction {
val name: Name // "greet"
val returnType: IrType // String类型
val valueParameters: List<IrValueParameter> // 参数列表(本例为空)
val body: IrBody? // 方法体
}
2. 符号:描述代码的代码
2.1 符号的本质
符号系统本质上是"描述代码的代码"------它不是程序的执行逻辑,而是程序结构的抽象描述。就像我们用文字描述一幅画一样,符号用数据结构描述程序的结构。
2.2 FIR和IR:编译器的架构分层
编译器采用分层架构,每层有不同的职责定位:
说明:由于源码类型关系复杂,同一个接口/抽象类子类繁多不利于理解,所以示例忽略了具体的实现类,没有严格遵循源码的类型。
FIR层:前端语义分析
kotlin
// 完整的greet方法在FIR中的表示
FirSimpleFunction(
name = "greet",
returnTypeRef = FirResolvedTypeRef(StringType),
body = FirBlock(
statements = [
FirReturnExpression(
result = FirStringConcatenationCall( // 字符串拼接函数
arguments = [
FirConstExpression("Hello, "), // 参数1:字符串常量
FirPropertyAccessExpression( // 参数2:属性访问
calleeReference = FirResolvedNamedReference(
name = "name",
resolvedSymbol = FirPropertySymbol( // name属性的符号
callableId = CallableId("com.example.Person.name"),
fir = FirProperty( // name属性的FIR声明
name = "name",
returnTypeRef = FirResolvedTypeRef(StringType),
isVal = true, // "这是只读属性"
initializer = FirParameterReference(...) // "来自构造函数参数"
)
)
),
typeRef = FirResolvedTypeRef(StringType) // 确定类型为String
),
FirConstExpression("!") // 参数3:字符串常量
]
)
)
]
)
)
IR层:后端代码生成
kotlin
// 完整的greet方法在IR中的表示
IrSimpleFunction(
name = "greet",
returnType = IrType.String,
body = IrBlockBody(
statements = [
IrReturn(
value = IrStringConcatenation(
arguments = [
IrConst(value = "Hello, "), // 直接的常量
IrGetValue( // 简化的字段访问
symbol = IrFieldSymbol( // name字段的符号
signature = IdSignature.CommonSignature("com.example.Person.name"),
owner = IrField( // name字段的IR声明
name = "name",
type = IrType.String,
visibility = DescriptorVisibilities.PUBLIC, // "运行时访问权限"
isFinal = true, // "运行时是否可变"
isStatic = false // 实例字段
)
),
type = IrType.String // 运行时类型
),
IrConst(value = "!")
]
)
)
]
)
)
关键差异分析:变量引用的不同处理方式
通过对比完整的方法表示,我们发现:
- 基础结构相似:方法名、返回类型、常量等在两套系统中表示方式基本一致
- 核心差异在变量引用 :对于
$name
这个变量引用,两套系统的处理方式截然不同
FIR:深度语义分析
kotlin
// FIR需要完整理解:这个name究竟是什么?
FirPropertyAccessExpression( // 识别为"属性访问表达式"
calleeReference = FirResolvedNamedReference( // 解析引用目标
name = "name",
resolvedSymbol = FirPropertySymbol( // 确定是属性符号
callableId = CallableId("com.example.Person.name"), // 全限定名
fir = FirProperty( // 属性的完整定义
isVal = true, // 语义:只读属性
returnTypeRef = FirResolvedTypeRef(StringType), // 类型信息
initializer = FirParameterReference(...) // 来源:构造函数参数
)
)
),
typeRef = FirResolvedTypeRef(StringType) // 表达式结果类型
)
// FIR保存了完整的"身份证":这是什么、从哪来、为什么可以访问
IR:直接执行指令
kotlin
// IR只关心:如何快速获取这个值?
IrGetValue( // 简化为"取值指令"
symbol = IrFieldSymbol( // 直接指向字段
signature = IdSignature.CommonSignature("com.example.Person.name"),
owner = IrField( // 字段的运行时信息
name = "name",
type = IrType.String, // 运行时类型
visibility = PUBLIC, // 访问权限检查
isFinal = true, // 是否可变
isStatic = false // 实例字段
)
),
type = IrType.String // 指令结果类型
)
// IR只保留了"操作手册":怎么访问、需要什么权限、返回什么类型
name
属性引用符号数量对比:
- FIR :一个简单的属性访问需要多个符号协作
FirPropertyAccessExpression
- 属性访问表达式FirResolvedNamedReference
- 解析后的名称引用FirPropertySymbol
- 属性符号FirProperty
- 属性声明FirParameterReference
- 参数引用
- IR :同样的操作只需要少数几个符号
IrGetValue
- 取值指令IrFieldSymbol
- 字段符号IrField
- 字段声明
本质区别总结
方面 | FIR(语义分析) | IR(代码生成) |
---|---|---|
目的 | 理解代码含义,验证正确性 | 生成可执行代码 |
信息类型 | 语义信息(为什么这样写) | 执行信息(怎么运行) |
复杂度 | 复杂,保留所有语义细节 | 简化,只保留执行必需 |
举例 | "这是只读属性,来自构造参数" | "从字段读取值" |
从上面的分析还可以推断出一个结论:相比IR,FIR的符号建模更为精细,符号数量也更多。
2.3 类比:建筑师的两套图纸
想象建造一栋房子需要两套不同的图纸:
设计图(类似FIR):
- 详细标注房间用途、采光要求、美学设计
- 包含大量设计理念和约束条件
- 帮助理解"为什么这样设计"
- 用于审批、沟通、验证合规性
施工图(类似IR):
- 精确的尺寸、材料规格、施工步骤
- 去除设计理念,专注实施细节
- 帮助回答"如何具体建造"
- 用于指导工人、采购材料、质量控制
3. 两套符号系统的关系与转换
FIR和IR符号系统不是独立工作的,而是在编译流程中协同配合。FIR符号承担前端的语义分析任务,IR符号负责后端的代码生成工作。两者通过FIR2IR转换器建立映射关系,实现从语义世界到执行世界的桥梁。
markdown
Kotlin源码 → LightTree → FIR构建 → FIR2IR转换 → IR优化 → 目标代码生成
↓ ↓ ↓ ↓ ↓ ↓
源码解析 轻量语法树 FIR符号 符号映射 IR符号 平台代码
这种设计实现了编译器的关注点分离:让FIR专注于"理解代码",让IR专注于"生成代码"。
4. 符号系统在编译流程中的作用
符号系统不仅仅是编译器的数据结构,更是编译器各个阶段协同工作的核心机制。通过我们的Person.greet()
示例,可以清楚地看到符号系统如何在编译的每个关键环节发挥作用。
4.1 FIR阶段:语义理解的基石
在FIR阶段,符号系统的主要任务是理解代码的语义。让我们看看编译器如何处理我们的示例:
符号构建与类型推断
当编译器遇到"Hello, $name!"
时,FIR符号系统的工作过程:
kotlin
// 详细的类型处理过程:
// 步骤1:符号查找与类型解析
class Person(val name: String) // ← name的类型在这里明确声明
// ^^^^^^^^ ^^^^^^^
// FIR符号系统记录:name: String
// 步骤2:表达式类型推断
"Hello, $name!" // ← 这里需要推断整个表达式的类型
FirStringConcatenationCall(
arguments = [
"Hello, ", // 已知:String
name, // 已知:String(从符号获取)
"!" // 已知:String
]
)
// 推断规则:String + String + String = String
// 步骤3:类型兼容性检查
fun greet(): String = "Hello, $name!"
// ^^^^^^ ^^^^^^^^^^^^^^^
// 期望类型 实际类型(推断得出)
// 类型检查:String = String ✓
这里涉及两个不同的过程:
- 符号类型解析 :
name
的类型来自其声明val name: String
,不需要推断 - 表达式类型推断 :字符串拼接的结果类型通过分析所有参数类型推断得出
- 扩展思考:如果
name
不是String类型会有什么不同?(本文暂不作分析)
- 扩展思考:如果
4.2 IR阶段:执行优化的引擎
在IR阶段,符号系统的焦点转向如何高效执行代码,IR符号将属性访问优化为直接的字段读取,符号声明数量减少,构建开销和理解成本也大大降低。
4.3 插件支持的差异
由于FIR和IR符号系统承担着不同的编译职责,因此它们对编译器插件的支持方式也存在显著差异:
插件支持差异对比
方面 | FIR插件 | IR插件 |
---|---|---|
作用阶段 | 语义分析阶段 | 代码生成阶段 |
操作对象 | 声明、类型、符号引用 | 函数体、表达式、指令 |
主要能力 | 生成新声明、修改类型关系 | 修改代码实现、优化执行逻辑 |
查找机制 | 基于谓词的声明查找 | 基于访问者模式的遍历 |
API稳定性 | 相对稳定(K2专用) | 不稳定(跨版本兼容性差) |
典型用例 | 序列化、依赖注入、元编程 | 性能优化、代码插桩、平台适配 |
5. 主要源码文件
5.1 FIR符号系统相关源码
compiler/fir/tree/src/org/jetbrains/kotlin/fir/symbols/FirBasedSymbol.kt
- FIR符号基础接口compiler/fir/tree/src/org/jetbrains/kotlin/fir/symbols/impl/FirCallableSymbol.kt
- 可调用符号实现compiler/fir/tree/src/org/jetbrains/kotlin/fir/symbols/impl/FirClassLikeSymbol.kt
- 类符号实现compiler/fir/tree/gen/org/jetbrains/kotlin/fir/expressions/FirExpression.kt
- FIR表达式定义compiler/fir/light-tree2fir/src/
- LightTree到FIR的转换
5.2 IR符号系统相关源码
compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/symbols/IrSymbol.kt
- IR符号基础接口compiler/ir/ir.tree/gen/org/jetbrains/kotlin/ir/symbols/IrSymbol.kt
- IR符号生成接口compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/symbols/impl/IrSymbolImpl.kt
- IR符号实现
5.3 FIR2IR转换相关源码
compiler/fir/fir2ir/src/org/jetbrains/kotlin/fir/backend/Fir2IrConverter.kt
- FIR到IR转换器compiler/fir/fir2ir/src/org/jetbrains/kotlin/fir/backend/Fir2IrDeclarationStorage.kt
- 声明存储compiler/fir/fir2ir/src/org/jetbrains/kotlin/fir/backend/Fir2IrClassifierStorage.kt
- 分类器存储
本文档是符号系统系列的第一篇,后续将深入讲解FIR符号系统和IR符号系统的具体实现。