新的诊断架构概述
诊断在编程语言体验中起着非常重要的作用。对于开发者生产力而言,编译器能够在任何情况下(尤其是不完整或无效的代码)生成适当的指导至关重要。
在这篇博文中,我们想分享一些关于即将发布的 Swift 5.2 版本中诊断改进的重要更新。这包括一种新的编译器故障诊断策略,最初作为 Swift 5.1 版本的一部分引入,它产生了一些令人兴奋的新结果和改进的错误消息。
挑战
Swift 是一种非常富有表现力的语言,具有丰富的类型系统,其中包含类继承、协议一致性、泛型和重载等诸多特性。尽管我们程序员尽力编写结构良好的程序,但有时我们需要一些帮助。幸运的是,编译器确切地知道哪些 Swift 代码是有效和无效的。问题是如何最好地告诉您哪里出错了,错误发生在何处,以及如何修复它。
编译器的许多部分确保程序的正确性,但这项工作的重点是改进类型检查器。Swift 类型检查器强制执行关于如何在源代码中使用类型的规则,并且负责在这些规则被违反时通知您。
例如,以下代码
struct S<T> {
init(_: [T]) {}
}
var i = 42
_ = S<Int>([i!])
产生以下诊断信息
error: type of expression is ambiguous without more context
虽然此诊断指出了一个真正的错误,但它没有帮助,因为它不具体或不可操作。这是因为旧的类型检查器过去常常猜测错误的确切位置。这在许多情况下都有效,但用户编写的许多类型的编程错误仍然无法准确识别。为了解决这个问题,一种新的诊断基础设施正在开发中。类型检查器不是猜测错误发生的位置,而是尝试在遇到问题时立即“修复”问题,同时记住它已应用的修复。这不仅使类型检查器能够更准确地指出更多类型程序中的错误,而且还允许它显示更多以前只会报告第一个错误后就停止的故障。
类型推断概述
由于新的诊断基础设施与类型检查器紧密耦合,因此我们必须稍作绕道,谈谈类型推断。请注意,这是一个简短的介绍;有关更多详细信息,请参阅关于类型检查器的编译器文档。
Swift 使用基于约束的类型检查器实现双向类型推断,这让人想起经典的 Hindley-Milner 类型推断 算法
- 类型检查器将源代码转换为约束系统,该系统表示代码中类型之间的关系。
- 类型关系通过类型约束来表达,类型约束要么对单个类型施加要求(例如,它是一个整数文字类型),要么关联两个类型(例如,一个是可转换为另一个的类型)。
- 约束中描述的类型可以是 Swift 类型系统中的任何类型,包括元组类型、函数类型、枚举/结构体/类类型、协议类型和泛型类型。此外,类型可以是表示为
$<name>
的类型变量。 - 类型变量可以用来代替任何其他类型,例如,涉及类型变量
$Foo
的元组类型($Foo, Int)
。
约束系统执行三个步骤
对于诊断,唯一有趣的阶段是约束生成和求解。
给定一个输入表达式(有时还有额外的上下文信息),约束求解器生成
- 一组类型变量,表示每个子表达式的抽象类型
- 一组类型约束,描述这些类型变量之间的关系
最常见的约束类型是二元约束,它关联两个类型,表示为
type1 <constraint kind> type2
常用的二元约束有
$X <绑定到> Y
- 将类型变量$X
绑定到固定类型Y
X <可转换为> Y
- 转换约束要求第一个类型X
可转换为第二个类型Y
,其中包括子类型化和相等性X <符合> Y
- 指定第一个类型X
必须符合协议Y
(Arg1, Arg2, ...) → Result <适用于> $Function
- “适用函数”约束要求两个类型都是函数类型,具有相同的输入和输出类型
约束生成完成后,求解器尝试为约束系统中每个类型变量分配具体类型,并形成满足所有约束的解决方案。
让我们考虑以下示例函数
func foo(_ str: String) {
str + 1
}
对于人类来说,很快就会清楚表达式 str + 1
存在问题,以及问题所在的位置,但推断引擎只能依靠约束简化算法来确定哪里出错了。
正如我们之前建立的那样,约束求解器首先为 str
、1
和 +
生成约束(请参阅 约束生成 阶段)。输入表达式的每个不同子元素,例如 str
,都由以下之一表示:
- 具体类型(预先知道)
- 类型变量,用
$<name>
表示,它可以假定任何满足与其关联的约束的类型。
在 约束生成 阶段完成后,表达式 str + 1
的约束系统将具有类型变量和约束的组合。现在让我们看看这些。
类型变量
-
$Str
表示变量str
的类型,它是调用+
中的第一个参数 -
$One
表示字面量1
的类型,它是调用+
中的第二个参数 -
$Result
表示调用运算符+
的结果类型 -
$Plus
表示运算符+
本身的类型,这是一组可能的重载选项以供尝试。
约束
$Str <绑定到> String
- 参数
str
具有固定的 String 类型。
- 参数
$One <符合> ExpressibleByIntegerLiteral
- 由于 Swift 中的整数文字(如
1
)可以假定为任何符合 ExpressibleByIntegerLiteral 协议的类型(例如Int
或Double
),因此求解器在一开始只能依赖该信息。
- 由于 Swift 中的整数文字(如
$Plus <绑定到> 析取((String, String) -> String, (Int, Int) -> Int, ...)
- 运算符
+
形成一个不相交集 的选择,其中每个元素表示单个重载的类型。
- 运算符
($Str, $One) -> $Result <适用于> $Plus
$Result
的类型尚不清楚;它将通过使用参数元组($Str, $One)
测试$Plus
的每个重载选项来确定。
请注意,所有约束和类型变量都与输入表达式中的特定位置相关联
推断算法尝试为约束系统中的所有类型变量找到合适的类型,并针对相关约束测试它们。在我们的示例中,$One
可以获得 Int
或 Double
类型,因为这两种类型都满足 ExpressibleByIntegerLiteral
协议一致性要求。但是,简单地枚举约束系统中每个“空”类型变量的所有可能类型是非常低效的,因为当特定类型变量受到约束不足时,可能有很多类型需要尝试。例如,$Result
没有限制,因此它可能假定任何类型。为了解决这个问题,约束求解器首先尝试析取选项,这允许求解器缩小每个相关类型变量的可能类型集。在 $Result
的情况下,这会将可能类型的数量减少到仅与 $Plus
的重载选项关联的结果类型,而不是所有可能的类型。
现在,是时候运行推断算法来确定 $One
和 $Result
的类型了。
推断算法执行的单个回合:
-
让我们首先将
$Plus
绑定到其第一个析取选项(String, String) -> String
-
现在可以测试
适用于
约束,因为$Plus
已绑定到具体类型。($Str, $One) -> $Result <适用于> $Plus
约束的简化最终匹配两个函数类型($Str, $One) -> $Result
和(String, String) -> String
,其过程如下:- 添加新的转换约束以将参数 0 匹配到参数 0 -
$Str <可转换为> String
- 添加新的转换约束以将参数 1 匹配到参数 1 -
$One <可转换为> String
- 将
$Result
等同于String
,因为结果类型必须相等
- 添加新的转换约束以将参数 0 匹配到参数 0 -
-
一些新生成的约束可以立即测试/简化,例如
$Str <可转换为> String
为true
,因为$Str
已经具有String
的固定类型,并且String
可以转换为自身$Result
可以根据相等约束分配String
类型
-
此时,唯一剩下的约束是
$One <可转换为> String
$One <符合> ExpressibleByIntegerLiteral
-
$One
的可能类型为Int
、Double
和String
。这很有趣,因为这些可能的类型都不能满足所有剩余的约束;Int
和Double
都不能转换为String
,而String
不符合ExpressibleByIntegerLiteral
协议 -
在尝试了
$One
的所有可能类型后,求解器停止并将当前的类型集和重载选项视为失败。然后,求解器回溯并尝试$Plus
的下一个析取选项。
我们可以看到,错误位置将由求解器在执行推断算法时确定。由于 $One
的所有可能类型都不匹配,因此应将其视为错误位置(因为它无法绑定到任何类型)。复杂的表达式可能具有多个这样的位置,因为现有错误会在推断算法进行时产生新的错误。为了缩小这种情况下的错误位置,求解器只会选择错误位置数量最少的解决方案。
至此,错误位置是如何识别的或多或少已经清楚了,但如何帮助求解器在这种情况下取得进展,以便它可以得出完整的解决方案,这一点尚不明显。
方法
新的诊断基础设施采用我们称之为约束修复的方法,以尝试解决不一致的情况,即求解器陷入僵局,没有其他类型可以尝试。对于我们的示例,修复方法是忽略 String
不符合 ExpressibleByIntegerLiteral
协议。修复的目的是能够从求解器中捕获有关错误位置的所有有用信息,并在以后将其用于诊断。这是当前方法和新方法之间的主要区别。前者会尝试猜测错误的位置,而新方法与求解器具有共生关系,求解器向其提供所有错误位置。
正如我们之前指出的,所有类型变量和约束都带有关于它们与它们源自的子表达式的关系的信息。这种关系与类型信息相结合,使得为通过新的诊断框架诊断的所有问题提供量身定制的诊断和修复变得简单明了。
在我们的示例中,已确定类型变量 $One
是一个错误位置,因此诊断可以检查 $One
在输入表达式中的使用方式:$One
表示调用运算符 +
中位置 #2 的参数,并且已知问题与 String
不符合 ExpressibleByIntegerLiteral
协议有关。基于所有这些信息,可以形成以下两种诊断中的任何一种
error: binary operator '+' cannot be applied to arguments 'String' and 'Int'
并附带关于第二个参数不符合 ExpressibleByIntegerLiteral
协议的注释,或者更简单的
error: argument type 'String' does not conform to 'ExpressibleByIntegerLiteral'
诊断指的是第二个参数。
我们选择了第一种替代方案,并生成了关于运算符的诊断,以及针对每个部分匹配的重载选项的注释。让我们仔细看看所描述方法的内部工作原理。
诊断的剖析
当检测到约束失败时,会创建一个约束修复,以捕获有关失败的信息
- 发生的失败类型
- 源自失败的源代码位置
- 失败中涉及的类型和声明
约束求解器累积这些修复。一旦它找到一个解决方案,它就会查看作为解决方案一部分的修复,并生成可操作的错误或警告。让我们看一下这一切是如何协同工作的。考虑以下示例
func foo(_: inout Int) {}
var x: Int = 0
foo(x)
这里的问题与参数 x
有关,在没有显式 &
的情况下,它不能作为参数传递给 inout
参数。
现在让我们看一下此约束系统的类型变量和约束。
类型变量
有三个类型变量
$X := Int
$Foo := (inout Int) -> Void
$Result
约束
这三个类型变量具有以下约束
($X) -> $Result <applicable to> $Foo
推断算法将尝试匹配 ($X) -> $Result
到 (inout Int) -> Void
,这将导致以下新的约束
Int <convertible to> inout Int
$Result <equal to> Void
Int
无法转换为 inout Int
,因此约束求解器将此失败记录为 缺失 &
并忽略 <convertible to>
约束。
在忽略该约束后,约束系统的其余部分可以求解。然后,类型检查器查看记录的修复并 发出错误,描述问题(缺失 &
)以及插入 &
的 Fix-It。
error: passing value of type 'Int' to an inout parameter requires explicit '&'
foo(x)
^
&
此示例中只有一个类型错误,但这种诊断架构也可以解释代码中的多个不同类型错误。考虑一个稍微更复杂的示例
func foo(_: inout Int, bar: String) {}
var x: Int = 0
foo(x, "bar")
在求解此约束系统时,类型检查器将再次记录 foo
的第一个参数上缺失 &
的失败。此外,它将记录缺失参数标签 bar
的失败。一旦记录了这两个失败,约束系统的其余部分将被求解。然后,类型检查器会为需要解决的两个问题生成错误(带有 Fix-It)以修复此代码。
error: passing value of type 'Int' to an inout parameter requires explicit '&'
foo(x)
^
&
error: missing argument label 'bar:' in call
foo(x, "bar")
^
bar:
记录每个特定的失败,然后继续求解剩余的约束系统,这意味着解决这些失败将产生一个类型良好的解决方案。这允许类型检查器生成可操作的诊断信息,通常带有修复建议,引导开发人员编写正确的代码。
改进的诊断示例
缺失标签
考虑以下无效代码
func foo(answer: Int) -> String { return "a" }
func foo(answer: String) -> String { return "b" }
let _: [String] = [42].map { foo($0) }
以前,这导致了以下诊断信息
error: argument labels '(_:)' do not match any available overloads`
现在诊断为
error: missing argument label 'answer:' in call
let _: [String] = [42].map { foo($0) }
^
answer:
实参到形参转换不匹配
考虑以下无效代码
let x: [Int] = [1, 2, 3, 4]
let y: UInt = 4
_ = x.filter { ($0 + y) > 42 }
以前,这导致了以下诊断信息
error: binary operator '+' cannot be applied to operands of type 'Int' and 'UInt'`
现在诊断为
error: cannot convert value of type 'UInt' to expected argument type 'Int'
_ = x.filter { ($0 + y) > 42 }
^
Int( )
无效的可选展开
考虑以下无效代码
struct S<T> {
init(_: [T]) {}
}
var i = 42
_ = S<Int>([i!])
以前,这导致了以下诊断信息
error: type of expression is ambiguous without more context
现在诊断为
error: cannot force unwrap value of non-optional type 'Int'
_ = S<Int>([i!])
~^
缺失成员
考虑以下无效代码
class A {}
class B : A {
override init() {}
func foo() -> A {
return A()
}
}
struct S<T> {
init(_ a: T...) {}
}
func bar<T>(_ t: T) {
_ = S(B(), .foo(), A())
}
以前,这导致了以下诊断信息
error: generic parameter ’T’ could not be inferred
现在诊断为
error: type 'A' has no member 'foo'
_ = S(B(), .foo(), A())
~^~~~~
缺失协议一致性
考虑以下无效代码
protocol P {}
func foo<T: P>(_ x: T) -> T {
return x
}
func bar<T>(x: T) -> T {
return foo(x)
}
以前,这导致了以下诊断信息
error: generic parameter 'T' could not be inferred
现在诊断为
error: argument type 'T' does not conform to expected type 'P'
return foo(x)
^
条件一致性
考虑以下无效代码
extension BinaryInteger {
var foo: Self {
return self <= 1
? 1
: (2...self).reduce(1, *)
}
}
以前,这导致了以下诊断信息
error: ambiguous reference to member '...'
现在诊断为
error: referencing instance method 'reduce' on 'ClosedRange' requires that 'Self.Stride' conform to 'SignedInteger'
: (2...self).reduce(1, *)
^
Swift.ClosedRange:1:11: note: requirement from conditional conformance of 'ClosedRange<Self>' to 'Sequence'
extension ClosedRange : Sequence where Bound : Strideable, Bound.Stride : SignedInteger {
^
SwiftUI 示例
实参到形参转换不匹配
考虑以下无效的 SwiftUI 代码
import SwiftUI
struct Foo: View {
var body: some View {
ForEach(1...5) {
Circle().rotation(.degrees($0))
}
}
}
以前,这导致了以下诊断信息
error: Cannot convert value of type '(Double) -> RotatedShape<Circle>' to expected argument type '() -> _'
现在诊断为
error: cannot convert value of type 'Int' to expected argument type 'Double'
Circle().rotation(.degrees($0))
^
Double( )
缺失成员
考虑以下无效的 SwiftUI 代码
import SwiftUI
struct S: View {
var body: some View {
ZStack {
Rectangle().frame(width: 220.0, height: 32.0)
.foregroundColor(.systemRed)
HStack {
Text("A")
Spacer()
Text("B")
}.padding()
}.scaledToFit()
}
}
以前,这通常被诊断为完全无关的问题
error: 'Double' is not convertible to 'CGFloat?'
Rectangle().frame(width: 220.0, height: 32.0)
^~~~~
新的诊断现在正确地指出没有名为 systemRed
的颜色
error: type 'Color?' has no member 'systemRed'
.foregroundColor(.systemRed)
~^~~~~~~~~
缺失实参
考虑以下无效的 SwiftUI 代码
import SwiftUI
struct S: View {
@State private var showDetail = false
var body: some View {
Button(action: {
self.showDetail.toggle()
}) {
Image(systemName: "chevron.right.circle")
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
.animation(.spring)
}
}
}
以前,这导致了以下诊断信息
error: type of expression is ambiguous without more context
现在诊断为
error: member 'spring' expects argument of type '(response: Double, dampingFraction: Double, blendDuration: Double)'
.animation(.spring)
^
结论
新的诊断基础设施旨在克服旧方法的所有缺点。它的结构方式旨在使改进/移植现有诊断信息变得容易,并供新功能实现者使用,以便立即提供出色的诊断信息。它在我们迄今为止移植的所有诊断信息中都显示出非常有希望的结果,我们正在努力每天移植更多。