mcpField
U
understanding-stability-inference

understanding-stability-inference

github.com/skydoves开发520125 次安装19 周前更新
代码仓库

关于 understanding-stability-inference

面向 Compose 开发者的技能,逐步解释编译器如何推断类型稳定性,帮助分析报告中意外的分类结果。重点涵盖泛型、跨模块类型、接口和递归类型。

主要功能

  1. 01按 12 个阶段讲解稳定性推断
  2. 02分析泛型参数与类型替换
  3. 03解释跨模块的 runtime 稳定性
  4. 04辨析接口、Java 与递归类型的分类
  5. 05说明稳定性注解和已知类型注册表

适用场景

  1. 01解释类型为何被判为 runtime stable 或 unknown
  2. 02排查泛型类型的稳定性结果
  3. 03分析依赖模块中的类型分类
  4. 04理解递归类型为何导致不稳定

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/skydoves/compose-performance-skills --skill understanding-stability-inference

安装后在 Claude Code 中输入/understanding-stability-inference确认已加载。

附带文件references/bitmask-encoding.mdreferences/twelve-phase-algorithm.md

详细介绍

理解 Stability 推断——读懂编译器的思路

Stability 由 Compose 编译器内置的 12 阶段算法决定。本技能将教会 Claude 如何沿着类型逐步执行算法,从而解释报告为何会给出这样的结论,并在报告生成前预测分类结果。请将本技能与 ../diagnosing-compose-stability/SKILL.md(生成报告)和 ../stabilizing-compose-types/SKILL.md(修复明显不稳定的类型)配合使用。当更简单的技能给出的结论让开发者感到意外时,请使用本技能。

何时使用本技能

  • 开发者询问“为什么 Foo 被分类为 runtime stable,而不是 stable?”
  • 报告对一个“看起来没问题”的类显示 runtime 或 unknown。
  • 涉及泛型:Box<T>、Wrapper<A, B>、Pair<String, Int>、ImmutableList<User>。
  • 类位于其他模块中,并以 .class/.kotlin_metadata 制品形式发布。
  • 开发者询问 $stable: Int 字段、@StabilityInferred 或跨模块分类。
  • 自引用类型(class Node(val children: List<Node>))因不明显的原因而不稳定。
  • 参数列表中出现 Java 类型、接口或抽象基类,令开发者感到意外。

何时不要使用本技能

  • 修复方式很直接(var → val、List → ImmutableList、移除 Flow 参数)。请使用 ../stabilizing-compose-types/SKILL.md。
  • 尚无报告。请先运行 ../diagnosing-compose-stability/SKILL.md。
  • 开发者希望在 CI 中强制执行 stability 检查。请使用 ../enforcing-stability-in-ci/SKILL.md。

前提条件

  • 已生成 Compose Compiler 报告,或至少在 build/compose_compiler/ 中有一个 <module>-classes.txt 和一个 <module>-composables.txt。
  • 开发者了解 stability 的基本术语:stable、unstable、skippable、restartable、@Stable、@Immutable。
  • 使用搭载 Compose 编译器插件的 Kotlin 2.0+(Strong Skipping 默认开启)。

工作流 — 诊断问答树

按照编译器执行的相同 12 个阶段检查类型。对于每个调用点,按顺序提出问题;第一个匹配的阶段优先。

规范阶段顺序(本技能中处处采用,并与 references/twelve-phase-algorithm.md 一致):

  1. 阶段 1 — 基本类型 / String / 函数 / Unit 快速路径 → Stable。 编译器会立即返回,不会执行字段分析。请说明这是快速路径,让开发者知道没有检查其他内容。
  2. 阶段 2 — 类型参数替换。 裸类型变量 T 会变为 Stability.Parameter(T);推断会延迟到替换它的调用点进行。
  3. 阶段 3 — 去除可空类型(Int? → 分析 Int)。 可空性不会改变稳定性;算法会去掉 ? 并递归处理。
  4. 阶段 4 — 内联类 — 检查底层类型。 value class Wrapper(val raw: T) 的稳定性与 T 完全相同。
  5. 阶段 5 — 循环检测(递归树 → 保守判定为 UNSTABLE)。 算法会在循环处停止,以保证终止。逃逸方式是在递归类上使用 @Stable/@Immutable;这会在到达阶段 5 前的阶段 6 生效。
  6. 阶段 6 — 注解检查(@Stable、@Immutable、@StableMarker)。 是 → Stability.Certain(稳定)。@Immutable 启用的额外优化多于 @Stable,因为编译器可以将属性读取提升为静态表达式,并省略相等性检查;而 @Stable 仅承诺变更通知。
  7. 阶段 7 — 命中已知稳定结构注册表(Pair / Triple / Result / ImmutableList / dagger.Lazy / ClosedRange / 等)。 返回带有注册表位掩码的 Stability.Parameter。完整注册表请参见 references/bitmask-encoding.md。
  8. 阶段 8 — 命中外部配置(stability_config.conf)。 返回带有配置文件中声明的位掩码的 Stability.Parameter。
  9. 阶段 9 — 外部模块(由单独编译生成的 @StabilityInferred 注解)。 返回 Stability.Runtime。编译器会在 JVM 上生成 $stable: Int 字段(在 Native/JS 上生成经过名称改编的顶层属性),供运行时通过 Composer.changed 查询。请告诉开发者,这不是 bug——runtime 表示编译器在说:“我无法在编译时证明这一点,因此会在运行时检查。”
  10. 阶段 10 — Java 类型(默认 UNSTABLE — 通过配置文件修复)。 对推断算法而言,Java final 字段看起来像 var,因为算法没有可读取的 Kotlin 元数据。请通过 stabilityConfigurationFiles 修复,而不是编辑 Java 源代码。
  11. 阶段 11 — 接口(UNKNOWN;运行时使用 ===)。 编译器无法从单个调用点枚举所有实现;运行时会在相等性探测时回退到身份比较(===)。
  12. 阶段 12 — 逐字段分析(慢速路径):
    • 遍历线性化的类层次结构,因此继承的字段也会参与分析。没有单独的“继承”阶段——继承在这里处理。
    • 任何 var 属性 → Unstable(变更未通过 Snapshot 集成进行观察)。
    • 任何类型为 Unstable 的属性 → Unstable(Combined 以 Unstable 为准)。
    • 否则,该类为 Stable(所有字段均稳定时,Combined 会折叠为 Stable)。

所有 12 个阶段的完整伪代码以及逐字段循环,请参见 references/twelve-phase-algorithm.md。

模式

模式:“为什么 Box<String> 显示为 runtime stable?”
// Source — the developer's class, in a library module
class Box<T>(val value: T)

// Call site, in app module
@Composable fun BoxRow(box: Box<String>) { Text(box.value) }

编译器会遍历 Box<T>:

  1. 阶段 12(逐字段分析)在定义模块内部找到一个字段 value: T;对 T 的递归分析进入阶段 2(类型参数)→ Stability.Parameter。合并后的结果归并为 Stability.Parameter,位掩码为 0b1(这一个类型参数会影响稳定性)。
  2. 由于 Box 在另一个模块中被使用,编译器会在 Box 上生成 @StabilityInferred(parameters = 0b1),并生成一个 $stable: Int 字段,在运行时根据 T 的稳定性初始化。下游调用点会通过阶段 9 获取此信息。
  3. 调用点替换 → T = String → String 是 Certain Stable → 满足第 0 位。
  4. 最终报告行:runtime stable class Box<T>;在调用点,BoxRow 是 skippable。
// WRONG mental model
// "runtime stable means there is a runtime cost on every recomposition" — partly true but misleading
// WRONG because: the cost is one Int field load and a bitwise AND, performed once when the runtime
// computes the call-site stability. It is far cheaper than the unskipped recomposition it prevents.
// RIGHT mental model
// runtime stable = "the compiler proved stability conditional on the type arguments, and emitted
// a $stable: Int field whose bits the runtime ANDs against the substituted argument stabilities".
// The skip decision is still made; it is just made at runtime instead of compile time.
模式:“为什么 @Immutable data class Person(val name: String) 比 @Stable 启用更多优化?”

@Stable 是一项契约:“我会将更改通知 Compose。”@Immutable 是更强的契约:“我永远不会更改。”使用 @Immutable 时,编译器可以将对 Person.name 的读取提升为静态表达式,并省去嵌套用法中的相等性探测;使用 @Stable 时,仍必须生成相等性检查。两者都归类为 Stability.Certain,但下游优化器对 @Immutable 的处理更加激进。

// WRONG
@Stable data class Coordinates(val lat: Double, val lng: Double)
// WRONG because: Coordinates never mutates after construction. @Stable understates the contract
// and forfeits static-expression promotion at every read site.
// RIGHT
@Immutable data class Coordinates(val lat: Double, val lng: Double)
模式:“为什么我的递归树不稳定,尽管看起来没问题?”
data class Node(val id: String, val children: List<Node>)

阶段 5(循环检测)会中止。编译器不会尝试定点分析,因为那样就必须先假定结论,才能证明结论。保守的判定结果是 Unstable。即使每个字段本身都稳定,通过 children 的递归也会向父调用返回 Unstable。

// WRONG — adds @Stable to "force" stability
@Stable data class Node(val id: String, val children: List<Node>)
// WRONG because: List<Node> is a mutable interface backed by ArrayList in practice. The @Stable
// annotation tells the compiler to trust the contract, but the actual List instance can mutate
// between recompositions without notifying Compose, producing silent missed recompositions.
// RIGHT
import kotlinx.collections.immutable.ImmutableList
@Immutable data class Node(val id: String, val children: ImmutableList<Node>)

ImmutableList 位于已知稳定构造注册表(阶段 7)中,位掩码为 0b1,因此允许通过 children 递归:阶段 5 仍会触发循环检测,但注册表命中会短路保守判定。

模式:“为什么 Set<String> 会阻止跳过,而 ImmutableSet<String> 不会?”

kotlin.collections.Set 是一个接口(阶段 11 → Unknown),实际上由会发生变更的 LinkedHashSet 支持。kotlinx.collections.immutable.ImmutableSet 位于已知稳定构造注册表中,位掩码为 0b1,因此它属于 Stability.Parameter,并会在元素类型稳定时解析为稳定。

// WRONG
@Composable fun TagRow(tags: Set<String>) { /* ... */ }
// WRONG because: Set is an interface — phase 11 returns Unknown, the call site is non-skippable.
// RIGHT
@Composable fun TagRow(tags: ImmutableSet<String>) { /* ... */ }
模式:“为什么来自其他模块的类会显示为 runtime stable,即使它只有 val?”

独立编译。在编译调用点时,编译器没有依赖项的完整源代码 AST,只有其 .class 文件和 @StabilityInferred(parameters = ...) 中的元数据。阶段 9 会读取该注解;运行时则通过生成的 $stable: Int 字段,根据实际类型实参解析位掩码。分类结果是正确的——无需执行额外工作——但由于没有源代码就无法进行跨模块编译期分析,因此必须延迟到运行时处理。

五种编译器级稳定性类型

回答“为什么”类问题时,请直接使用这些名称。编译器将稳定性存储为以下类型之一:

  • Stability.Certain — 原始类型、String、Unit、函数类型、枚举,以及带有 @Stable/@Immutable 注解的类。判定结果在编译期即可确定。
  • Stability.Runtime — 单独编译的类。编译期会生成 $stable: Int 字段和 @StabilityInferred;运行时会将位与实际类型实参进行 AND 运算。
  • Stability.Unknown — 接口、未经具体分析的抽象类,或任何编译器不愿作出确定判定的类型。运行时会在相等性探测时回退到 === 身份比较。
  • Stability.Parameter — 泛型。稳定性通过位掩码取决于类型实参。
  • Stability.Combined — 多个组成部分的聚合结果(类的字段,或多个类型实参)。Unstable 优先——任意一个 Unstable 组成部分都会使整体不稳定。

位掩码编码(预览)

Container<T1, T2, T3> 使用 Int 位掩码,其中位 i 置位表示 Ti 会参与稳定性判断:

类型位掩码解读
kotlin.Pair<A, B>0b11A 和 B 都会影响稳定性
kotlin.Triple<A, B, C>0b111三者都会影响稳定性
kotlinx.collections.immutable.ImmutableList<E>0b1只有 E 会影响稳定性
java.math.BigInteger0b0没有参数;无论类型参数被擦除为何,都会被归类为稳定

完整规则——包括如何为单独编译的类型生成 @StabilityInferred(parameters = ...),以及 $stable: Int 字段在 JVM 上的布局与 Kotlin/Native 和 Kotlin/JS 上使用的改名顶层属性之间的区别——请参阅 references/bitmask-encoding.md。

强制规则

  • 必须让开发者明白,runtime stable 不是 Bug,也不是不稳定的判定结果——这是编译器表示“稳定性取决于类型参数,并会在运行时通过 $stable: Int 字段进行检查”的方式。
  • 不得在解释当前结构为何不稳定之前,建议进行结构性更改(将 var 改为 val、替换集合类型)。先诊断,再处理。
  • 必须在回答中区分 Stability.Unknown 和 Stability.Unstable——Unknown 表示“无法判断”,会回退到标识相等性;Unstable 表示“已证明不稳定”,会彻底禁用跳过。
  • 不得建议开发者为无法保证其契约的类型添加 @Stable。稳定性注解是一种契约;违反契约会导致静默漏掉重组,这比无法跳过的可组合项更糟。
  • 建议: 在有助于深入理解时,引用 Kotlin 编译器源码。具体文件:Stability.kt(代数数据类型)、KnownStableConstructs.kt(注册表)、ClassStabilityTransformer.kt($stable 字段的生成),以及 StabilityConfigParser.kt(配置文件读取器)。
  • 建议: 解释泛型时,明确说明位掩码的每一步:“Pair 的位掩码中位 0 已置位,A=String 是 Certain Stable,满足要求;位 1 已置位,B=List 是 Unstable,不满足——Combined 最终归为 Unstable”。

验证

  • Claude 能在运行报告前预测候选类型会被归类为 Certain、Runtime、Unknown、Parameter 还是 Combined。
  • Claude 能指出是 12 个阶段中的哪一个阶段得出了该判定。
  • 对于 runtime stable 类,Claude 能解释编译器在类声明上生成了 @StabilityInferred(parameters = ...),并生成了 $stable: Int 字段;运行时会将该字段与替换后的类型参数稳定性进行按位与运算。
  • 对于无法保证其变更契约的类型,Claude 会拒绝建议添加 @Stable 或 @Immutable。
  • 在解释 Set<T>、List<T>、Map<K, V> 为何会阻止跳过时,Claude 能正确指出这些是 Unknown 接口(而不是 Unstable)。

参考资料

参考文件
  • references/twelve-phase-algorithm.md — 通过伪代码逐步讲解全部 12 个阶段,以及逐字段分析的伪代码。
  • references/bitmask-encoding.md — 通用稳定性位掩码规则、已知稳定结构注册表、编译器生成的 @StabilityInferred(parameters = 0b1) 注解、JVM 上的运行时 $stable: Int 字段,以及 Native 和 JS 上的名称改编顶层属性方案。

评论

支持 Markdown