ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ruff ty 类型推断器中的回归测试解析:非可推断类型变量的约束投影与 `always` 终端

Ruff ty 类型推断器中的回归测试解析:非可推断类型变量的约束投影与 `always` 终端 Ruff ty 类型推断器中的回归测试解析非可推断类型变量的约束投影与always终端【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff本篇文章以仓库内回归测试文档 noninferable_projection_to_terminal.md 为线索深入讲解 Ruff 新一代类型检查器ty在求解泛型调用时如何把不可推断non-inferable类型变量的约束从约束集中投影projection出来并正确处理其产生的always终端节点。读完本文你将掌握这条回归用例背后的 BDD 约束求解流程、终端节点在路径枚举中的关键作用以及如何用 mdtest 格式为类型推断器编写、运行此类回归测试。背景ty类型推断器与 BDD 约束集Ruff 的ty类型检查器将一次泛型调用的类型推断建模为约束求解问题调用点在推断过程中会产生一组类型变量TypeVar约束求解器需要找出所有可能的类型变量特化specialization进而推导返回类型。这些约束被组织成有序二叉决策图ordered BDD/TDD其核心实现在 constraints.rs。在 BDD 中每个变量是一条被 intern 的约束节点分为三类见 constraints.rs 中的Node枚举与注释AlwaysTruealways终端恒真 / 恒可见对应NodeId(0xffff_ffff)硬编码 IDAlwaysFalsenever终端恒假 / 恒不可见对应NodeId(0xffff_fffe)Interior内部节点存储在ConstraintSetBuilder的存储数组中索引从小到大出边只能指向更小的索引。从源码注释可以确认两个关键设计事实终端节点拥有硬编码 ID这是求解器能够认出终端的必要条件BDD 是局部归约locally reduced且有序的重复节点通过 Salsa intern 合并冗余边形状被折叠任意根到叶子的路径都按ConstraintId::ordering的固定变量顺序访问变量见 constraints.rs。回归场景逐行解析回归文档给出了完整的复现用例其核心代码如下from collections.abc import Awaitable from typing import Callable, Generic, TypeVar T_co TypeVar(T_co, covariantTrue) T TypeVar(T) class Call(Generic[T_co]): def __call__(self) - T_co | Awaitable[T_co]: raise NotImplementedError def result(self) - T_co: raise NotImplementedError def cast_to_call(value: Callable[[], T | Awaitable[T]] | Call[T]) - Call[T]: raise NotImplementedError def wait(value: Callable[[], T] | Call[T]) - T: call cast_to_call(value) reveal_type(call) # revealed: Call[Awaitable[Twait] | Twait] return call.result() # error: [invalid-return-type]整个用例围绕两个泛型函数展开cast_to_call接受Callable[[], T | Awaitable[T]]与Call[T]的联合返回Call[T]wait接受Callable[[], T]与Call[T]的联合返回T。当wait内部调用cast_to_call(value)时类型推断器需要为cast_to_call的返回类型中的T求解特化。这里的难点在于wait自身的T是一个**不可推断non-inferable**的类型变量——它是调用方还没有给出的待定参数因此约束求解器不能为它枚举具体类型只能把它当作一个悬而未决的变量参与投影。reveal_type的期望结果回归用例断言reveal_type(call) # revealed: Call[Awaitable[Twait] | Twait]即cast_to_call(value)的返回类型被推断为Call[Awaitable[Twait] | Twait]其中Twait表示wait函数作用域内那个不可推断的T。Awaitable[Twait] | Twait这个联合恰好对应cast_to_call参数中Callable[[], T | Awaitable[T]]分支对返回Call[T]中T的贡献——说明求解器正确地把不可推断变量的约束保留了下来而不是丢弃或退化为未知类型。invalid-return-type诊断回归用例同时断言return call.result() # error: [invalid-return-type]call.result()的返回类型是T_co即T_co Twait而wait声明的返回类型是T同样是Twait因为类型变量按函数作用域区分。这里call.result()被推断为Awaitable[Twait] | Twait而不是T因此返回值与wait的返回标注不匹配触发了invalid-return-type诊断。该诊断的实现在 diagnostic.rs 的report_invalid_return_type它会输出expected{expected_ty}, found{actual_ty} 这类消息并附上返回类型标注位置与返回值位置的二级注解。核心机制非可推断约束的投影产生always终端回归文档在开头用一段话精确概括了 bug 的机理When inferring the innerTfor the call tocast_to_call, the outerTfromwaitis non-inferable. Projecting its constraint out of the constraint set produces thealwaysterminal. That terminal must be recognized before enumerating the remaining BDD paths; otherwise, the empty path list is interpreted as unsatisfiable and the inferred specialization degrades toUnknown.翻译过来即为cast_to_call调用求解内部T时外部wait的T不可推断从约束集中投影掉这个不可推断变量的约束后约束集退化为always终端恒真求解器必须在枚举剩余 BDD 路径之前识别出这个终端否则空的路径列表会被误判为不可满足unsatisfiable导致推断出的特化退化为Unknown。这正是本回归测试要守护的行为边界没有约束需要满足恒真Unconstrained与约束永远无法满足恒假Unsatisfiable是两种截然不同的结果必须被区分开来。终端识别在投影流水线中的位置投影projection是指给定一个可推断类型变量集合inferable从约束集 BDD 中移除只涉及不可推断变量的约束得到只关于可推断变量的简化约束。这一过程体现在 constraints.rs 的remove_noninferable遇到Node::AlwaysTrue直接返回ALWAYS_TRUE终端遇到Node::AlwaysFalse直接返回ALWAYS_FALSE终端只有Interior节点才需要递归进入interior.remove_noninferable做真正的变量消除。也就是说终端节点是递归的基例base case在遍历内部节点之前就被短路返回。若这一识别被跳过BDD 上投影后无任何可推断变量约束的情况就会表现为空路径集而下游求解逻辑可能把空路径解读为矛盾从而错误地输出Unknown。枚举阶段对终端的显式分支投影得到的 BDD 随后进入路径枚举与折叠阶段实现在 projection.rs 的PathBounds::try_fold_with。其开头对三种形态做了显式区分let paths match self { Self::Unsatisfiable return Ok(SolutionProjection::Unsatisfiable), Self::Unconstrained return Ok(SolutionProjection::Unconstrained), Self::Constrained(paths) paths, };Unsatisfiablenever终端直接返回Unsatisfiable表示约束不可满足Unconstrainedalways终端直接返回Unconstrained表示约束恒真、无需枚举任何路径Constrained才进入for path in paths的逐路径求解与fold累加。注意前两种情况下根本不进入路径循环这正是终端必须在枚举剩余 BDD 路径之前被识别的源码级体现。若always终端被错误地当作空路径集合处理fold将得不到任何解最终投影结果就会与Unknown退化路径混淆。终端投影不需要路径也不需要分配类型上述设计在单元测试中被显式验证。见 projection/tests.rs 的terminal_projections_need_no_paths_or_types// Terminal answers do not allocate any path or construct any type. let terminal_budget SolutionBudget { paths: 0, visits: 1, type_terms: 0, }; for (set, expected) in [ ( ConstraintSet::always(builder), SolutionProjection::Unconstrained, ), ( ConstraintSet::never(builder), SolutionProjection::Unsatisfiable, ), ] { assert_eq!(collect_paths(db, builder, set, [t], terminal_budget), Ok(expected)); }该测试用paths: 0、type_terms: 0的极端预算验证always终端投影为Unconstrained、never终端投影为Unsatisfiable且两者都不需要分配任何路径或构造任何类型。这与回归文档描述的空路径列表被误判为不可满足形成了对照——如果终端没有被提前识别paths: 0的预算会直接触发PathBudgetExceeded或者产生一个无法区分Unconstrained与Unsatisfiable的空路径。同文件的 valid_unsolved_path_is_not_unconstrained 也从另一个角度验证了语义区别一条有效但未求解的路径PathBoundSolution::Unsolved被折叠为Constrained1 条空解路径而Unsatisfiable才对应Unsatisfiable。可见求解器对有解但未绑定任何变量与无解有着严格的区分。如何用 mdtest 格式编写并运行此类回归测试mdtest 文档结构该回归用例采用 mdtestMarkdown test格式编写完整格式说明见 crates/ty_test/README.md。一个 Markdown 文件就是一个测试套件其中每个代码块是一个嵌入的文件语言标签支持py、pyi、ipynb、toml与ignore见 parser.rs 与 ruff_mdtest/src/lib.rstoml代码块用于书写测试配置本文档的[environment]段即属于此类[environment] python-version 3.11该配置对整个小节及其嵌套小节生效子节可覆盖父节配置见 ty_test/README.md。python-version 3.11用于固定目标 Python 版本因为collections.abc.Awaitable等标准库签名的可用性随版本变化。断言语法测试通过行内注释断言诊断# revealed: Call[Awaitable[Twait] | Twait]必须与reveal_type(call)推断出的类型显示形式逐字符精确匹配# error: [invalid-return-type]要求本行产生规则码为invalid-return-type的诊断。# error:断言还支持列号与文本片段两种附加限定例如# error: 8 [invalid-return-type]或# error: expectedT, foundAwaitable[T] | T详见 ty_test/README.md。运行该测试回归用例位于crates/ty_python_semantic/resources/mdtest/regression/目录测试入口为 tests/mdtest.rs它通过datatest_stable把该目录下所有 Markdown 文件作为测试套件驱动ty_test::run资源根为./resources/mdtest。运行单个回归文件的方式cargo test -p ty_python_semantic --test mdtest -- regression/noninferable_projection_to_terminal.md也可以按测试名过滤测试名由文件名与各级标题拼接而成MDTEST_TEST_FILTERNon-inferable constraint projection cargo test -p ty_python_semantic --test mdtest每个测试都在独立的内存文件系统与独立的 Salsa 数据库中从头运行/src为工作区根也是导入根测试之间互不共享状态见 ty_test/README.md 与 ruff_mdtest/src/lib.rs。测试运行时会解析 Markdown 中的代码块与 TOML 配置把文件写入内存系统执行类型检查再将诊断与断言逐一匹配所有匹配成功才通过。小结noninferable_projection_to_terminal.md是一条精准的回归测试它用wait/cast_to_call这对泛型函数构造出不可推断类型变量投影后约束集变为always终端的边界情形并断言cast_to_call的返回类型特化必须保留不可推断变量Call[Awaitable[Twait] | Twait]随后对call.result()的返回类型检查必须正确报告invalid-return-type。其背后的实现要点可归纳为三点均可在源码中得到印证终端节点硬编码ALWAYS_TRUE/ALWAYS_FALSE拥有固定NodeId在 constraints.rs 中定义投影时短路返回终端remove_noninferable以AlwaysTrue/AlwaysFalse为基例不进入内部节点递归constraints.rs枚举前显式分支try_fold_with在进入路径循环前先区分Unsatisfiable、Unconstrained与Constrainedprojection.rs。如果忽略这条规则投影出always终端 → 空路径列表 → 误判不可满足 → 特化退化为Unknown的链条就会复现从而破坏reveal_type的精确类型与返回类型检查。理解这条回归用例也就掌握了 Ruff 类型推断器约束求解的核心边界语义。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表