十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Rust 编译器类型表示(ty 模块)完全指南:从 HIR 语法到 ty::Ty 语义

Rust 编译器类型表示(ty 模块)完全指南:从 HIR 语法到 ty::Ty 语义 Rust 编译器类型表示ty 模块完全指南从 HIR 语法到 ty::Ty 语义【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust导读ty模块是 rustc 内部类型的核心表示层它定义了编译器如何把用户书写的类型语法HIR翻译成语义化的、可比较的类型实体ty::Ty并在此基础上构建了整个类型检查typeck体系以及贯穿全编译器的中央数据结构——类型上下文TyCtxt即tcx。读完本文你将理解rustc_hir::Ty与ty::Ty的本质区别、Ty的驻留interning实现与分配方式、类型比较的正确姿势、TyKind各变体的含义以及TyKind::Error背后的错误抑制不变量。本文内容以 rustc 开发指南src/doc/rustc-dev-guide/src/ty.md为骨架并结合当前仓库源码进行佐证。1. ty 模块的职责ty模块位于 compiler/rustc_middle/src/ty定义了两件核心事情rustc 内部如何表示类型——即Tytcx类型及其背后的TyKind枚举类型上下文tcxTyCtxt——编译器中最核心的中央数据结构几乎所有查询query和类型操作都要经过它。当我们讨论rustc 如何表示一个类型时指的通常是Ty。需要注意仓库里与Ty相关的模块和类型非常多本文特指rustc_middle::ty::Ty不是rustc_hir::Ty。先厘清二者的区别是理解整个类型系统的基础。2.rustc_hir::Ty与ty::Ty的区别2.1 一个描述语法一个描述语义rustc 的 HIRHigh-Level Intermediate Representation可以理解为更接近 AST 的中间表示关于 HIR 的详细介绍见 hir.md。它是用户所写源代码经过解析parsing和少量脱糖desugaring后的产物因此rustc_hir::Ty反映的是用户写了什么——即用户为了表示某个类型而写下的语法形式。而ty::Ty反映的是用户写的到底是什么——即类型的语义。举例说明用户在程序中写了两次u32rustc_hir::Ty会记录这里出现了一个u32的名称产生两个独立的 HIR 类型实例而ty::Ty会记录这两处都指向同一个类型最终只存在一个ty::Ty。示例fn foo(x: u32) - u32 { x }函数签名里u32出现了两次。从语义上讲参数和返回值是同一个类型。但从 HIR 的视角看这是两个出现在程序不同位置拥有两个不同Span的独立类型实例。2.2 HIR 会丢失信息ty::Ty 是完整的HIR 层面还可能遗漏信息。例如u32这个类型在完整的 Rust 类型中其实包含一个生命周期参数只是用户不需要写出来编译器随后会依据省略规则elision rules插入缺失的生命周期结果可能变成fn fooa(x: a u32) - a u32。在 HIR 层这些细节没有被明示出来可以说图景是不完整的而在ty::Ty层这些细节被补全类型是完整的。并且对于给定类型比如u32整个程序中只会有恰好一个ty::Ty它被所有u32使用共享而不是像rustc_hir::Ty那样针对每一次出现单独建一个实例。2.3 对比总结表rustc_hir::Tyty::Ty描述类型的语法用户写了什么含少量脱糖描述类型的语义用户写的内容的含义每个rustc_hir::Ty都带有对应程序位置的Span不对应程序中某个单一位置带有泛型和生命周期但部分生命周期是特殊标记如LifetimeKind::Implicit拥有完整类型包括用户省略的泛型与生命周期fn foo(x: u32) - u32两个rustc_hir::Ty分别表示两处u32各有自己的Span且无法直接告诉我们二者是同一类型fn foo(x: u32) - u32全程序中只有一个ty::Ty表示u32它直接告诉我们两处u32含义相同fn foo(x: u32) - u32仍是两个rustc_hir::Ty引用的生命周期以LifetimeKind::Implicit特殊标记出现fn foo(x: u32) - u32只有一个ty::Ty且其中包含被省略的隐藏生命周期参数2.4 处理顺序HIR 在前ty::Ty 在后HIR 直接从 AST 构建因此在任何ty::Ty产生之前就已经存在。HIR 构建完成后编译器进行基础的类型推断type inference与类型检查type checking。在类型推断阶段我们确定每一处的ty::Ty是什么同时检查某个类型是否含糊不清ambiguous随后用ty::Ty进行类型检查确保一切都有预期类型。负责把rustc_hir::Ty**降级lower**为ty::Ty的代码位于hir_ty_lowering模块其主入口是lower_ty。这一过程主要发生在类型检查阶段但编译器其他部分——比如想询问这个函数期望的参数类型是什么——也会用到它。在源码中可以看到lower_ty的完整实现compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs它逐类匹配 HIR 的类型语法节点例如Slice(ty)降级为Ty::new_slicePtr(mt)降级为Ty::new_ptrRef(region, mt)先通过lower_lifetime处理生命周期再调用Ty::new_refNever直接返回tcx.types.never元组则用Ty::new_tup_from_iter逐元素降级后组装。2.5 语义如何驱动两类 Ty 的差异可以把 HIR 看作是假设最少的类型信息视角在证明两处类型相同之前我们默认它们是不同的。因为我们对它们所知更少就应该假设得更少。语法上它们是两个字符串u32在第 N 行第 20 列另一个u32在第 N 行第 35 列。在 HIR 阶段我们还不知道它们相同因此当作不同来处理。之后我们确定它们语义上是同一个类型于是得到共享的ty::Ty。再考虑泛型示例fn fooT(x: T) - u32。假设有人调用foo::u32(0)那么在这次调用中T和u32实际上变成了同一类型最终我们会得到同一个ty::Ty但两者各自仍有不同的rustc_hir::Ty。这里略有简化类型检查时会泛化地检查函数本身此时T与u32仍是不同的只有在后续做代码生成codegen时我们处理的总是每个函数单态化monomorphized即完全代入后的版本届时才知道T具体代表什么——即u32。还有一个体现类型别名type alias语义的例子mod a { type X u32; pub fn foo(x: X) - u32 { 22 } } mod b { type X i32; pub fn foo(x: X) - i32 { x } }这里的X显然随上下文而变化。若查看rustc_hir::Ty两种情况都会得到X是一个别名经名称解析后指向不同的别名定义但查看ty::Ty的函数签名则分别是fn(u32) - u32或fn(i32) - i32类型别名已被完全展开。3.ty::Ty的实现Interned WithCachedTypeInfo TyKind在源码中Ty的定义非常简洁compiler/rustc_middle/src/ty/mod.rspub struct Tytcx(Internedtcx, WithCachedTypeInfoTyKindtcx);它实际上是对InternedWithCachedTypeInfoTyKind的一层包装。拆解如下TyKind一个大型枚举包含许多变体用来表示各类 Rust 类型如基本类型、引用、代数数据类型、泛型参数、生命周期等。TyKind是泛型于Interner的pub enum TyKindI: Interner这是新求解器体系下类型系统泛型化rustc_type_ir的体现。WithCachedTypeInfo缓存少量便捷信息例如flags和outer_exclusive_binder。它们是用于提升效率的便捷技巧汇总了关于该类型我们可能想知道的摘要信息在本文语境下不需要过多关注。Interned让Ty成为瘦指针式thin pointer-like类型从而可以廉价地进行相等比较同时获得驻留interning的其他好处。关于驻留机制的详细介绍见 memory.md。Interned一般不需要直接操作——它始终被藏在Ty内部通过Deref实现或方法自动跳过。4. 分配与操作类型4.1 使用new_*构造方法要分配一个新类型可以使用Ty上定义的各种new_*方法方法名大体与各类类型对应。例如let array_ty Ty::new_array_with_const_len(tcx, ty, count);这些方法在源码中均有对应实现例如Ty::new_ref、Ty::new_array_with_const_len、Ty::new_slice等都定义在 compiler/rustc_middle/src/ty/sty.rs 中如new_ref接收tcx、生命周期/区域Region、被引用类型和可变性new_slice接收tcx与元素类型。这些方法都返回Tytcx——注意返回的生命周期tcx正是该tcx所访问的 arena区域的生命周期。类型总是被规范化canonicalized并驻留interned的所以我们绝不会重复分配两个完全相同的类型。4.2 通过tcx.types访问常见类型许多常见类型可以直接从tcx的字段中取得tcx.types.bool、tcx.types.char等。这些字段由CommonTypes结构体定义仓库中可以看到它包含unit、bool、char、各种有/无符号整数isize/i8/…/u128、浮点类型f16/f32/f64/f128、str_、never、self_param以及用于 trait 对象场景的 dummy 类型等字段。5. 类型比较几乎从不是正确选择5.1 为什么不够用由于类型被驻留用可以高效判断相等——但这几乎从不是你想要的除非你恰好是在哈希并查找重复项。原因在于Rust 中同一个类型往往有多种表示方式尤其当涉及推断inference时。例如类型{integer}即ty::Infer(ty::IntVar(..))一个整数推断变量是整数字面量如0的类型和u8ty::UInt(..)在测试它们能否相互赋值时这是诊断diagnostics代码中很常见的操作应当视为相等但直接用比较会返回false因为它们确实是不同的类型。5.2 有推断上下文时infcx.can_eq正确比较两个类型的最简单方式需要一个推断上下文infcxInferCtxt。若有infcx用infcx.can_eq(param_env, ty1, ty2)来检查两个类型能否被置为相等。这正是诊断代码通常想要的关心的问题是两个类型能否相互赋值而不是它们在编译器类型检查层是否以完全相同的形式表示。在仓库中可以看到can_eq的实际使用遍布类型检查与借用检查代码例如HIR 类型检查的demand/expr逻辑中用它判断期望类型与实际类型是否可相等见 compiler/rustc_hir_typeck/src/demand.rs借用检查的诊断中对相关类型做相等性判定见 compiler/rustc_borrowck/src/diagnostics/mod.rscompare_impl_item中比较self类型见 compiler/rustc_hir_analysis/src/check/compare_impl_item.rs。使用推断上下文时务必小心类型内部潜在的推断变量必须确实属于该推断上下文。如果你所在的函数已经能拿到推断上下文例如 HIR 类型检查或 MIR 借用检查期间通常就满足此条件。5.3 规范化normalization是另一个考量两个类型可能实际相同但其中一个位于关联类型associated type之后。要正确比较需要先规范化类型。这主要发生在 HIR 类型检查期间以及处理来自TyCtxt查询的类型时比如tcx.type_of()的结果。类型检查期间若手头有FnCtxt或ObligationCtxt应对类型调用.normalize(ty)类型检查之后诊断代码可以使用tcx.normalize_erasing_regions(ty)。5.4 什么时候是安全的在某些场景下直接是没问题的例如late lints 中单态化monomorphization之后——此时类型检查已完成所有推断变量都已求解、所有区域region都已擦除。在这些情况下如果你确定推断变量或规范化不会成为问题推荐对该 lint 使用#[allow]或#[expect]显式放行。5.5 没有推断上下文怎么办当诊断代码没有推断上下文时如果某处如类型检查期间有可用的推断上下文应通过函数参数把它传递进来。如果完全不可得则可以按 type-inference.md 中描述的方法创建一个推断上下文。但这只有一种情况下有用涉及的类型例如来自tcx.type_of()等查询确实通过fresh_args_for_item之类的方法被替换成了全新的推断变量。这可以用来回答类似VecT对任意T能否与Vecu32统一这样的问题。6.TyKind变体速览注意TyKind不是函数式编程概念中的Kind种类。在编译器中处理Ty时常见的模式是按类型的种类做模式匹配fn foo(x: Tytcx) { match x.kind { ... } }kind字段的类型是TyKindtcx一个定义了编译器中所有类型种类的枚举。N.B. 在类型推断期间检视kind字段是有风险的可能遇到推断变量等需要额外考虑的情况或者某些类型尚未确定、稍后才会揭晓。TyKind枚举定义于 compiler/rustc_type_ir/src/ty_kind.rs包含大量变体以下是文档选取的代表性样例ADT代数数据类型struct、enum或union。底层实现上三者是完全一致的都是ty::TyKind::Adt——本质上是用户自定义类型。在源码中Adt(I::AdtDef, I::GenericArgs)携带 ADT 定义与泛型实参例如Listi32由struct ListT的AdtDef与实参[i32]表示。后续章节会详细介绍。Foreign对应extern type T是 Rust 中不透明的非安全 FFI 类型。Str类型str。当用户写str时Str表示该类型中的str部分。Slice对应[T]。Array对应[T; n]携带元素类型与长度常量Array(I::Ty, I::Const)。RawPtr对应*mut T或*const T。Ref表示安全引用a mut T或a T。Ref带有几个关联部分被引用类型Tytcx、引用的生命周期/区域Regiontcx以及表示是否可变的Mutability源码中为Ref(RegionI, I::Ty, Mutability)。Param表示类型参数如VecT中的T。Error表示程序某处发生了类型错误以便打印更好的诊断信息。后续会详细讨论。其他更多变体如Bool、Char、Int(IntTy)、Uint(UintTy)、Float(FloatTy)、FnDef函数定义的类型如fn() - i32 {foo}、函数指针、闭包类型等完整列表见 compiler/rustc_type_ir/src/ty_kind.rs。7. 导入约定Import conventions虽然没有硬性规定ty模块的惯用导入方式如下use ty::{self, Ty, TyCtxt};特别是由于Ty和TyCtxt太常见通常直接导入其他类型则习惯用显式的ty::前缀引用如ty::TraitReftcx。当然不同模块也会按需导入更多或更少的名字。8. 类型错误TyKind::Error与错误抑制不变量当用户犯下类型错误时编译器会生成一个TyKind::Error类型。其设计意图是传播这个错误类型并抑制由它引发的其他错误以免用户被级联的错误消息淹没。8.1 关键不变量TyKind::Error有一个重要不变量编译器绝不应该在未确认已经向用户报告了一个错误的情况下产生Error类型。通常是因为(a) 你刚刚就在这里报告了错误或 (b) 你在传播一个已有的Error类型此时该错误应当在Error类型产生时就已报告。维护这一不变量至关重要因为Error类型的全部意义就在于抑制其他错误——即不再报告它们。如果我们产生了一个Error类型却没有真正向用户输出任何错误就可能导致后续错误被误抑制编译可能在报错缺失的情况下意外成功8.2 第三种情况delayed bug偶尔存在第三种情形你相信某个错误已被报告但认为它发生在编译的更早阶段而非本地。此时可以用delayed_bug或span_delayed_bug创建一个延迟缺陷delayed bug它记录你预期这次编译会产生错误——如果编译最终竟然成功就会触发一个编译器 bug 报告panic 并提示这是个内部错误。8.3 构造TyKind::Error的正确途径为了加强安全性在rustc_middle::ty之外实际上无法直接构造TyKind::Error值——TyKind::Error有一个私有成员阻止了在别处构造它。取而代之应使用Ty::new_error或Ty::new_error_with_message方法。从源码可以看到new_error_with_message的实现compiler/rustc_middle/src/ty/sty.rspub fn new_error_with_messageS: IntoMultiSpan( tcx: TyCtxttcx, span: S, msg: impl IntoCowstatic, str, ) - Tytcx { let reported tcx.dcx().span_delayed_bug(span, msg); Ty::new(tcx, Error(reported)) }这些方法要么接受一个ErrorGuaranteed要么在返回一个Error种类的驻留Ty之前调用span_delayed_bug。如果你本来就打算使用span_delayed_bug那么直接把 span 和消息传给new_error_with_message即可避免重复记录一次 delayed bug。另外还有一个辅助方法new_misc_error同文件第 530-532 行它用TyKind::Error constructed but no error reported作为消息构造错误适用于兜底场景。9.TyKind变体的简写语法Debug 输出速查表查看Ty的调试输出或在编译器中口头讨论不同类型时会碰到一些不是合法 Rust 语法、但被用来简洁表示类型内部信息的记号。以下是快速参考表泛型参数{name}/#{index}如T/#0其中index是该参数在泛型参数列表中的位置。推断变量?{id}如?x/?0其中id标识该推断变量。来自 binder 的变量^{binder}_{index}如^0_x/^0_2其中binder和index标识引用的是哪个 binder 中的哪个变量。占位符placeholder!{id}或!{id}_{universe}如!x/!0/!x_2/!0_2表示指定 universe 中某个唯一类型universe 为0时通常省略。这些概念会在后续章节如类型推断、区域、替代等中更深入地展开。结语ty模块是理解 rustc 类型系统的一把钥匙rustc_hir::Ty记录用户写了什么ty::Ty记录用户写的是什么意思lower_ty完成两者之间的语义桥接驻留机制让类型比较变得廉价而规范TyKind枚举穷尽了编译器认识的所有类型形态TyKind::Error则通过严格的构造与传播约束确保错误抑制机制不会反过来掩盖真正的编译失败。如果你想继续深入可以从 memory.md驻留与 arena、type-inference.md推断上下文、hir.mdHIR 表示以及 ty-fold.md类型折叠等章节入手。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表