Ruff ty 类型检查器整数比较类型推断:从 Literal 常量折叠到 unsupported-operator 诊断
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
本篇技术指南基于 Ruff 仓库中crates/ty_python_semantic下的 mdtest 测试套件,深入剖析实验性静态类型检查器 ty 对Python 整数比较表达式(==、!=、<、<=、>、>=、is、is not,含链式比较)的推断规则。读者将掌握:整数字面量比较如何被折叠为Literal[True]/Literal[False]、整数实例比较为何退化为bool、跨类型比较(如1 == ""、1 <= "")如何触发unsupported-operator诊断,以及这些行为背后的 Rust 源码实现位置。
测试文件与运行背景
本文的绝对主体是 integers.md,它隶属于 Ruff 类型检查器 ty 的 mdtest 测试套件(位于 resources/mdtest/comparison/)。该目录下还有 identity.md、unsupported.md、instances/rich_comparison.md 等姊妹测试,共同刻画比较表达式的推断语义。
mdtest 测试的语法特征非常直观:普通代码块中通过reveal_type(...)声明期望的类型,注释# revealed: <Type>即断言结果;# error: [unsupported-operator] "..."则断言某表达式必须产生指定错误码与消息。这些断言由 mdtest.py 驱动的测试框架执行。整个 comparison 目录可看作一份"可执行的类型语义规范"——本文讨论的每一条规则,都有对应可验证的测试用例背书。
整数字面量比较:常量折叠到Literal[bool]
integers.md 的第一组用例聚焦整数字面量之间的比较,此时类型检查器可以完全在编译期得出结论:
reveal_type(1 == 1 == True) # revealed: Literal[True] reveal_type(1 == 1 == 2 == 4) # revealed: Literal[False] reveal_type(False < True <= 2 < 3 != 6) # revealed: Literal[True] reveal_type(1 < 1) # revealed: Literal[False] reveal_type(1 > 1) # revealed: Literal[False]观察这些结果可以归纳出三条核心规则:
- 同类型字面量比较被完全折叠:两个同值字面量比较出
Literal[True],异值则出Literal[False]。1 < 1、1 > 1这类"明显为假"的比较同样被折叠为Literal[False],说明检查器并没有偷懒退化为bool,而是做了精确求值。 - 链式比较按元素逐步求值:
1 == 1 == True拆分为1 == 1(真)与1 == True(真)两部分,因而整体为Literal[True];False < True <= 2 < 3 != 6的每一环都成立,整体仍为Literal[True]。这说明链式比较并非整体运算,而是a op1 b and b op2 c ...的逐段合取(Python 语言语义正是如此,但类型层面需要逐环求值才能给出常量结果)。 bool与int在比较层面互通:True/False作为int的子类参与数值比较,False < True、1 == True都能正常求值。
链式比较的布尔转换陷阱
ty 对链式比较有一个隐蔽但重要的行为:对前导元素(非最后一个元素)的比较结果隐式调用bool()。如果某个比较方法返回了不可布尔化的类型,就会触发unsupported-bool-conversion诊断。这在 instances/rich_comparison.md 中有专门用例:
class NotBoolable: __bool__: int = 3 class Comparable: def __lt__(self, item) -> NotBoolable: return NotBoolable() def __gt__(self, item) -> NotBoolable: return NotBoolable() 10 < Comparable() < 20对应快照诊断为error[unsupported-bool-conversion]: Boolean conversion is not supported for typeNotBoolable``,并附带info:boolonNotBoolablemust be callable。值得注意的是Comparable() < Comparable()单独出现时是合法的(它是链尾,无需布尔转换),只有作为链中前导元素时才会报错——这与 Python 运行时"链式比较等价于a op1 b and b op2 c,中间结果必须可被and判定真假"的语义严格一致。
身份比较:is与is not的独特推断
integers.md 中整数身份比较的结果非常反直觉,值得单独拆解:
reveal_type(1 is 1) # revealed: bool reveal_type(1 is not 1) # revealed: bool reveal_type(1 is 2) # revealed: Literal[False] reveal_type(1 is not 7) # revealed: Literal[True]1 is 1竟然无法确定:因为小整数虽常驻内存(CPython 对-5..256做了缓存),但类型系统不能假设两次出现的字面量1一定共享内存地址,它们"可能共享也可能不共享",因此结果是bool。同目录的 identity.md 给出了这条规则的完整表述:"two occurrences of the same literal1do not necessarily share the same memory address, as1is not a singleton (but they alsomight!)"。1 is 2可以确定为Literal[False]:两个不同的整数字面量必然不共享内存地址,所以身份比较恒为假。1 is not 7确定为Literal[True]:是上一条的对偶。
对照 identity.md 可以看到,False is False、... is ...、NotImplemented is NotImplemented都能折叠为Literal[True],因为这些是真正的单例对象;而普通整数没有单例保证,只能在"两个不同字面量"时断言恒假。
身份比较的源码实现
上述规则在 Rust 端的实现位于 comparisons.rs。其中Type::identity_comparison_truthiness方法(约第 126–177 行)是核心判定逻辑,返回Truthiness::{AlwaysTrue, AlwaysFalse, Ambiguous}三态:
- 先用
identity_comparison_type对两侧类型做"身份比较上转型":把NewType还原为具体基类(因为NewType构造器原样返回参数,两个标签不同的视图可能指向同一对象)、把带替换签名的函数字面量还原为原始函数对象、把 TypeVar 展开到上界/约束,从而聚焦"运行时可能是什么对象"; - 然后若两侧类型互不相交(
is_disjoint_from),直接判定AlwaysFalse; - 否则若两侧都是单例/含单例的交集,且一方是另一方的子类型,才判定
AlwaysTrue; - 其余情况为
Ambiguous,即bool。
这正是1 is 1 → bool(非单例、结果不确定)、1 is 2 → Literal[False](不同字面量在类型上不相交)两个测试断言的直接来源。
跨类型比较与unsupported-operator诊断
整数与字符串、布尔与字符串等异构类型比较是整数文档中最具诊断价值的部分:
reveal_type(1 == "") # revealed: Literal[False] reveal_type(1 != "") # revealed: Literal[True] # error: [unsupported-operator] "Operator `<=` is not supported between objects of type `Literal[1]` and `Literal[""]`" reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True] reveal_type(-1 < 0) # revealed: Literal[True]这里展示了两类不同的行为:
==/!=对任意不匹配类型都"合法":int与str的相等比较在运行时总是返回False/True(因为 Python 对不兼容类型直接判定不等),因此类型层面可以精确折叠为Literal[False]/Literal[True],不产生任何诊断。这与object基类__eq__的默认行为一致。- 有序比较(
</<=/>/>=)要求类型兼容:int与str之间不存在排序关系,因此1 <= ""触发error: [unsupported-operator]诊断,报错消息中会以Literal[1]、Literal[""]的形式给出两侧精确类型。
值得注意1 <= "" and 0 < 1的推断结果:由于and左侧表达式已经出错(结果类型为Unknown,并带上& ~AlwaysTruthy的约束表示"它不能为真,因此短路不会走向右侧分支"之外的情形),整体被推断为(Unknown & ~AlwaysTruthy) | Literal[True]——即"要么左侧短路失败(Unknown 且不总是为真),要么右侧0 < 1恒真"。这展示了 ty 在错误表达式下仍然保持的精确约束传播。
unsupported-operator诊断在 builder.rs 与 diagnostic.rs 中定义与签发;unsupported.md 则系统性地覆盖了各类不支持操作符的报错快照,例如object() < 5(<在object与Literal[5]间不支持)、(1, 2) < (1, "hello")(元组逐元素比较时定位到 index 2 处的Literal[2]与Literal["hello"]不支持),可见诊断信息会尽力指出具体哪一环节、哪一对类型出了问题。
整数实例比较:退化为bool
当比较对象不再是字面量,而是整数类型的实例(变量、形参)时,ty 无法在编译期确定具体值,结果退化为bool:
# TODO: implement lookup of `__eq__` on typeshed `int` stub. def _(a: int, b: int): reveal_type(1 == a) # revealed: bool reveal_type(9 < a) # revealed: bool reveal_type(a < b) # revealed: bool文档中的TODO注释透露了一个重要实现现状:ty 目前尚未在 typeshed 的intstub 中查表__eq__等富比较双下划线方法,因此int实例(以及字面量与实例混合)的比较直接给出宽松的bool,而不是尝试调用 stub 里的签名。
这条行为与 instances/rich_comparison.md 所定义的一般规则形成对照:对于自定义类,==/!=/</<=/>/>=会分别查__eq__、__ne__、__lt__、__le__、__gt__、__ge__并返回其声明返回类型;当左侧方法缺失或参数类型不匹配时,会回退到右侧的反射方法(如100 > t回退到t.__lt__(100));两侧都匹配不上时==/!=回退为身份比较(得到bool)。int实例之所以一律得到bool,正是因为 stub 查找尚未落地,走的是最保守的默认路径。
归纳:整数比较推断决策表
将 integers.md 及其姊妹测试的规则汇总如下,便于作为速查表引用:
| 表达式形态 | 推断结果 | 依据与备注 |
|---|---|---|
同值整数字面量==/!=/</<=/>/>= | Literal[True] | 常量折叠,见本文第一节用例 |
| 异值整数字面量比较 | Literal[False] | 同上 |
| 整数字面量链式比较(逐环均成立) | Literal[True] | 每环单独求值后再合取 |
| 整数字面量链式比较(某环失败) | Literal[False] | 同上 |
1 is 1/1 is not 1 | bool | int非单例,见 identity.md |
两个不同整数字面量is | Literal[False] | 运行时必不共享地址 |
1 is not 7 | Literal[True] | 上一条的对偶 |
int与str做==/!= | Literal[False]/Literal[True] | 不兼容类型相等比较恒为假/真 |
int与str做有序比较 | error: [unsupported-operator],结果为Unknown | 见 unsupported.md |
int实例与字面量、int实例之间比较 | bool | stub__eq__查找尚未实现,见文档 TODO 注释 |
负数与正数比较(-1 < 0) | Literal[True] | 支持符号处理的常量折叠 |
延伸阅读
- 整数身份比较的完整语义与单例规则:identity.md
- 富比较双下划线方法(含反射比较、子类优先级、字面量参数)的全量用例:instances/rich_comparison.md
unsupported-operator诊断的各种快照(含元组逐元素定位、in操作符):unsupported.md- 身份比较三态判定核心实现:comparisons.rs
unsupported-operator诊断定义:diagnostic.rs 与签发位置 builder.rs
综上,integers.md 虽然篇幅不大,却是 ty 比较推断语义中"常量折叠精确求值、身份比较保守三态、异构比较分级诊断"三套机制的浓缩样例,任何希望在 Ruff 类型检查器上做比较语义二次开发或测试扩展的读者,都应以这份文档为起点逐条对照源码验证。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考