新的诊断架构概述

诊断在编程语言体验中起着非常重要的作用。对于开发者生产力而言,编译器能够在任何情况下(尤其是不完整或无效的代码)生成适当的指导至关重要。

在这篇博文中,我们想分享一些关于即将发布的 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 类型推断 算法

约束系统执行三个步骤

  1. 约束生成
  2. 约束求解
  3. 解决方案应用

对于诊断,唯一有趣的阶段是约束生成和求解。

给定一个输入表达式(有时还有额外的上下文信息),约束求解器生成

  1. 一组类型变量,表示每个子表达式的抽象类型
  2. 一组类型约束,描述这些类型变量之间的关系

最常见的约束类型是二元约束,它关联两个类型,表示为

type1 <constraint kind> type2

常用的二元约束有

  1. $X <绑定到> Y - 将类型变量 $X 绑定到固定类型 Y
  2. X <可转换为> Y - 转换约束要求第一个类型 X 可转换为第二个类型 Y,其中包括子类型化和相等性
  3. X <符合> Y - 指定第一个类型 X 必须符合协议 Y
  4. (Arg1, Arg2, ...) → Result <适用于> $Function - “适用函数”约束要求两个类型都是函数类型,具有相同的输入和输出类型

约束生成完成后,求解器尝试为约束系统中每个类型变量分配具体类型,并形成满足所有约束的解决方案。

让我们考虑以下示例函数

func foo(_ str: String) {
  str + 1
}

对于人类来说,很快就会清楚表达式 str + 1 存在问题,以及问题所在的位置,但推断引擎只能依靠约束简化算法来确定哪里出错了。

正如我们之前建立的那样,约束求解器首先为 str1+ 生成约束(请参阅 约束生成 阶段)。输入表达式的每个不同子元素,例如 str,都由以下之一表示:

  1. 具体类型(预先知道)
  2. 类型变量,用 $<name> 表示,它可以假定任何满足与其关联的约束的类型。

约束生成 阶段完成后,表达式 str + 1 的约束系统将具有类型变量和约束的组合。现在让我们看看这些。

类型变量

约束

请注意,所有约束和类型变量都与输入表达式中的特定位置相关联

Constraints Linked To Expressions

推断算法尝试为约束系统中的所有类型变量找到合适的类型,并针对相关约束测试它们。在我们的示例中,$One 可以获得 IntDouble 类型,因为这两种类型都满足 ExpressibleByIntegerLiteral 协议一致性要求。但是,简单地枚举约束系统中每个“空”类型变量的所有可能类型是非常低效的,因为当特定类型变量受到约束不足时,可能有很多类型需要尝试。例如,$Result 没有限制,因此它可能假定任何类型。为了解决这个问题,约束求解器首先尝试析取选项,这允许求解器缩小每个相关类型变量的可能类型集。在 $Result 的情况下,这会将可能类型的数量减少到仅与 $Plus 的重载选项关联的结果类型,而不是所有可能的类型。

现在,是时候运行推断算法来确定 $One$Result 的类型了。

推断算法执行的单个回合:

  1. 让我们首先将 $Plus 绑定到其第一个析取选项 (String, String) -> String

  2. 现在可以测试 适用于 约束,因为 $Plus 已绑定到具体类型。($Str, $One) -> $Result <适用于> $Plus 约束的简化最终匹配两个函数类型 ($Str, $One) -> $Result(String, String) -> String,其过程如下:

    • 添加新的转换约束以将参数 0 匹配到参数 0 - $Str <可转换为> String
    • 添加新的转换约束以将参数 1 匹配到参数 1 - $One <可转换为> String
    • $Result 等同于 String,因为结果类型必须相等
  3. 一些新生成的约束可以立即测试/简化,例如

    • $Str <可转换为> Stringtrue,因为 $Str 已经具有 String 的固定类型,并且 String 可以转换为自身
    • $Result 可以根据相等约束分配 String 类型
  4. 此时,唯一剩下的约束是

    • $One <可转换为> String
    • $One <符合> ExpressibleByIntegerLiteral
  5. $One 的可能类型为 IntDoubleString。这很有趣,因为这些可能的类型都不能满足所有剩余的约束;IntDouble 都不能转换为 String,而 String 不符合 ExpressibleByIntegerLiteral 协议

  6. 在尝试了 $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)
                     ^

结论

新的诊断基础设施旨在克服旧方法的所有缺点。它的结构方式旨在使改进/移植现有诊断信息变得容易,并供新功能实现者使用,以便立即提供出色的诊断信息。它在我们迄今为止移植的所有诊断信息中都显示出非常有希望的结果,我们正在努力每天移植更多。

有问题吗?

请随时在 相关主题 上发布关于此帖子的疑问,该主题位于 Swift 论坛 上。