suspend 函数怎样变成可恢复的字节码 固定 Kotlin 编译器源码并对照了 `suspend` 的 JVM 生成物。它重点解释后端为什么需要额外的 continuation、标签与状态字段;想区分前端与后端,不必再次从协程下手。一个普通的可空字段判断,足以展示“类型被分析”“条件被表示”“分支成为 JVM 指令”是三种不同的工作。

选一条已经在工程里运行的表达式

累计工程的 CommandParser.kt 没有为编译器研究改写业务语义:

1
2
3
4
fun textLength(source: TaskTextSource): Int {
val captured = source.text
return if (captured != null) captured.length else 0
}

TaskTextSource.text 是自定义 getter。读入局部 val captured 再判断非空,避免在判断后重新调用一个可能返回别的值的 getter。新增 CompilerPhaseBoundaryTest 将 getter 的调用次数计入 AtomicInteger:返回 "task" 时长度是 4、读取 1 次;返回 null 时长度为 0、读取也是 1 次。XML 两项均通过。这是源码语义与运行结果的对应检查,不是读取编译器内部 FIR 对象的证明。

在冻结的源码中定位三处职责

所有源码路径和 Git blob 身份以 Kotlin v2.2.21 对应的 commit 2146684dcba708e5a304758b41a9e4ec9c7eff71 为准。若升级编译器,应重新定位入口,不把这次的类名当公共 API。

位置与关键符号 这条表达式提供的线索 不能据此宣称
Fir2IrConverter.generateIrModuleFragment 把 FIR 文件集合交给 fir2IrVisitor,这是从前端表示进入 IR 转换的入口 已经打印或逐步调试这个函数的 FIR 树
Fir2IrVisitor.visitWhenExpression / visitSmartCastExpression 对源自 if 的条件设置 IrStatementOrigin.IF、转换分支;smart cast 表达式单独处理 所有 if 都生成完全一样的临时变量或 CHECKCAST
ExpressionCodegen.visitWhen JVM IR 后端处理 IrWhen,逐分支把条件和结果交给生成器 每条最终 ifnull 都能反向归因给这一单个入口

这里的名字容易引起误解:源码写 if,IR 中的分支却走 visitWhenExpression / IrWhen 路径;“When”是编译器共享的条件表示,不意味着源程序使用了 when。visitSmartCastExpression 的实现先转换原始表达式,再交给 implicitCastInserter;它解释为什么前端确认局部变量可非空与后端所需指令之间仍存在转换层。没有抓到本函数的实际 FIR/IR dump,因此这里只追踪相关源码入口、语义测试与最终生成物,不画一个未经观察的精确中间树。

最终字节码能确认什么

在固定插件 Kotlin 2.2.21 与 JDK 21 下,javap -classpath build/classes/kotlin/main -p -c example.tasks.CommandParserKt 输出的 textLength 关键行:

1
2
3
4
5
6
7
8
7:  invokevirtual TaskTextSource.getText:()Ljava/lang/String;
10: astore_1
11: aload_1
13: ifnull 22
16: invokevirtual java/lang/String.length:()I
22: pop
23: iconst_0
24: ireturn

完整字节码在 verification/E08/bytecode.txt,其中还包含 Kotlin 的非空参数入口检查与 goto;上面只截取理解这个判断所需的行。assertions.txt 核对方法中 getter 调用仅一次、确有 ifnull 和 String.length。这能把单次 getter、条件跳转与两个运行结果连起来,却无法通过 ifnull 的存在证明优化之前的 IR 字面结构或任意编译器版本输出同一局部变量槽位。02 还验证过可变属性反例;这份 javap 不会让它变成安全的 smart cast。

编译器插件是另一条兼容边界

K2 迁移指南将自定义编译器插件 API 标为 Experimental,明确不保证向后兼容。只实现 IrGenerationExtension 的后端插件应检查升级后的 API;依赖旧前端扩展点的插件需要迁移到 K2 API。把插件源码跟某版 Kotlin 编译器一起编译成功,不等于它能跨后续版本二进制直接加载;E07 验证的是普通应用库消费者,不代表编译器插件接口也受到同样的兼容约束。本篇没有开发/加载自定义插件,前端/后端插件兼容性实测均 NOT_RUN。

在解包的 kotlin-language/ 中用 JDK 21 和已缓存依赖复跑:

1
2
./gradlew --no-daemon --offline clean test
javap -classpath build/classes/kotlin/main -p -c example.tasks.CommandParserKt

上述行为只说明一次 getter 与可空分支的实际运行。原始测试 XML、三个编译器源码 Git blob SHA、官方文档请求、字节码与断言存于 writing-plans/kotlin/verification/E08/。没有逐步执行 K2 编译器、没有打印真实 IR 逐阶段 diff,也没有对插件 API 作版本兼容实测;这些应保持 NOT_RUN,不能以读源码和反编译替代。

练习与答案

练习一:把 val captured = source.text 改成在 if 条件和分支里各读一次 getter,预期测试会怎样?答案:getter 计数应变成两次,并可能让“判断的值”和“取长度的值”来自不同读取;这是基于当前测试结构的预测,未改默认源码。

练习二:字节码里看到 ifnull,能否证明已打印真实 FIR/IR 树?答案:不能。ifnull 只证明最终 JVM 指令形状;FIR/IR 中间表示这里只靠固定源码入口定位,没有 dump 实证。

练习三:普通应用库 ABI 兼容实验能否覆盖编译器插件兼容?答案:不能。插件 API 标为 Experimental,加载和二进制兼容要用插件工程单独测试,本篇保持 NOT_RUN。

参考资料