阅读 1924

Swift 5.2 新诊断框架

本文由知乎网友“漫慢忙”翻译自官方博客 《New Diagnostic Architecture Overview》

诊断程序(Diagnostics)在编程语言体验中扮演着非常重要的角色。开发人员在编写代码时非常关心的一点是:编译器可以在任何情况下(尤其是代码不完整或无效时)提供适当的指导并指出问题。

在此博客文章中,我们想分享一些即将推出的 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
复制代码

尽管这个诊断结果指出了真正的错误,但由于它不明确,因此并没有太大的帮助。这是因为旧的类型检查器主要用来猜测错误的确切位置。这在许多情况下都有效,但是用户仍然会出现很多无法准确识别的编程错误。为了解决这个问题,我们正在开发一种新的诊断架构。类型检查器不再是在猜测错误发生的位置,而是尝试在遇到问题时“修复”问题,并记住所应用的修复措施。这不仅使类型检查器可以查明更多种类的程序中的错误,也使它能够提前暴露更多的故障。

类型推断概述

由于新的诊断框架与类型检查器紧密结合,因此我们需要先讨论一下类型推断。请注意,这里只是简单地介绍一下。有关类型检查更多详细信息,请参阅 compiler’s documentation on the type checker[1]

Swift 使用基于约束的类型检查器实现双向类型推断,这使人联想到经典的 Hindley-Milner[2] 类型推断算法[3]

• 类型检查器将源代码转换为约束系统,该约束系统表示代码中类型之间的关系。

• 类型关系是通过类型约束表达的,类型约束要么对单个类型提出要求(例如,它是整数字面量类型),要么将两种类型相关联(例如,一种类型可以转换为另一种类型)。

• 约束中描述的类型可以是 Swift 类型系统中的任何类型,包括元组类型、函数类型、枚举/结构/类类型、协议类型和泛型类型。此外,类型可以是表示为 $<name> 的类型变量。

• 类型变量可以在任何其他类型中使用,例如,类型变量 $Foo 在元组类型 ($Foo,Int) 中使用。

约束系统执行三步操作:

• 产生约束

• 求解约束

• 应用解决方案

诊断过程关注的阶段是约束生成和求解。

给定输入表达式(有时还包括其他上下文信息),约束求解器将生成以下信息:

• 一组类型变量,代表每个子表达式的抽象类型

• 一组描述这些类型变量之间关系的类型约束

最常见的约束类型是二进制约束(binary constraint),它涉及两种类型,可以表示为:

type1 <constraint kind> type2
复制代码

常用的二进制约束有:

$X <bind to> Y - 将类型变量 $X 绑定到固定类型 Y

X <convertible to> Y - 转换约束要求第一个类型 X 可转换为第二个 Y,其中包括子类型和等价形式

X <conforms to> Y - 指定第一种类型 X 必须符合协议 Y

(Arg1,Arg2,...) → Result <applicable to> $Function - “适用函数(applicable function)”约束要求两种类型都是具有相同输入和输出类型的函数类型

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

让我们来看看下面的例子:

func foo(_ str: String) {
  str + 1
}
复制代码

对于我们来说,很快就能发现表达式 str + 1 存在问题以及该问题所在的位置,但是类型推断引擎只能依靠约束简化算法来确定问题所在。

正如我们之前讨论的,约束求解器首先为 str1+ 生成约束。输入表达式的每个不同子元素(如str)均由以下方式表示:

• 具体类型(提前知道)

• 用 $<name> 表示的类型变量,可以假定满足与之关联的约束的任何类型。

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

类型变量

$Str 表示变量 str 的类型,它是 + 调用中的第一个参数

$One 代表文字 1 的类型,它是 + 调用中的第二个参数

$Result 表示对运算符 + 调用的结果类型

$Plus 代表运算符 + 本身的类型,它是一组重载方法的集合。

约束

$Str <bind to> String

参数 str 具有固定的 String 类型。
复制代码

$One <conforms to> ExpressibleByIntegerLiteral

由于 Swift 中的整数字面量(例如1)可以采用任何符合 ExpressibleByIntegerLiteral 协议的类型(例如 Int 或 Double),因此求解器只能在开始时依赖该信息。
复制代码

$Plus <bind to> disjunction((String, String) -> String, (Int, Int) -> Int, ...)

运算符 `+` 形成一组不相交的选择,其中每个元素代表独立的重载类型。
复制代码

($Str, $One) -> $Result <applicable to> $Plus

`$Result` 的类型尚不清楚;它可以通过使用参数元组 ($Str,$One) 测试 `$Plus` 的每个重载来确定。
复制代码

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

推断算法尝试为约束系统中的所有类型变量找到合适的类型,并针对关联的约束对其进行测试。在我们的示例中,$One 可以是 Int 或 Double 类型,因为这两种类型都满足 ExpressibleByIntegerLiteral 协议一致性要求。但是,简单地枚举约束系统中每个“空”类型变量的所有可能类型是非常低效,因为当特定类型变量约束不足时可以尝试许多类型。例如,$Result 没有任何限制,因此它可以采用任何类型。要变通地解决此问题,约束求解器首先尝试分离选项,这使求解器可以缩小涉及的每个类型变量的可能类型的范围。对于 $Result,这会将可能类型的数量减少到仅与 $Plus 的重载选项相关联的结果类型,而不是所有可能的类型。

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

单轮推断算法执行步骤

• 首先将 $Plus 绑定到它的第一个析取选项 (String,String) -> String

• 现在可以测试 applicable to 约束,因为 $Plus 已绑定到具体类型。($Str, $One) -> $Result <applicable to> $Plus 约束最终简化为两个匹配的函数类型 ($Str, $One) -> $Result(String, String) -> String, 处理流程如下:

添加新的转换约束以将 argument 0 与 parameter 0 匹配 - `$Str <convertible to> String`

添加新的转换约束以将 argument 1 与 parameter 1 匹配 - $One <convertible to> String

将 $Result 等同于 String,因为结果类型必须相等
复制代码

• 一些新产生的约束可以立即进行测试/简化,例如:

$Str <convertible to> String 为 true,因为$Str 已经具有固定类型 String 并且 String可转换为自身

可以根据相等约束为 $Result 分配某种 String 类型
复制代码

• 此时,剩下的唯一约束是:

$One <convertible to> String
$One <conforms to> ExpressibleByIntegerLiteral
复制代码

$One 的可能类型是 Int,Double 和 String。这很有趣,因为这些可能的类型都不满足所有剩余的约束:Int 和 Double 都不能转换为 String,而 String 不符合 ExpressibleByIntegerLiteral 协议

• 在尝试了 $One 的所有可能类型之后,求解器将停止并认为当前类型集和重载选择均失败。然后,求解器回溯并尝试 $Plus 的下一个析取选择。

我们可以看到,错误位置将由求解程序执行推断算法时确定。由于没有任何可能的类型与 $One 匹配,因此应将其视为错误位置(因为它不能绑定到任何类型)。复杂表达式可能具有多个这样的位置,因为随着推断算法的执行,现有的错误会导致新的错误。为了缩小这种情况下的错误位置范围,求解器只会选择数量尽可能少的解决方案。

至此,我们或多或少地清楚了如何识别错误位置,但是如何帮助求解器在这种情况下取​​得进展尚不清楚,因此无法得出一个完整的解决方案。

解决方案

新的诊断架构采用了 “约束修复(constraint fix)” 技术,来尝试解决不一致的情况(在这些情况下,求解器会陷入无法尝试其他类型的情况)。我们示例的解决方法是忽略 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 有关,如果没有显式使用 ,则参数 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,因此约束求解器将失败记录为 missing & [4]并忽略 <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 的第一个参数记录 missing & 的失败。此外,它将为缺少的参数 bar 记录失败。一旦记录了两个失败,也就求解了约束系统的其余部分。然后,类型检查器针对需要修复此代码的两个问题产生错误(使用Fix-Its):

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 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)
                     ^
复制代码

结论

新的诊断架构旨在克服旧方法的所有缺点。它的架构方式旨在简化/改进现有的诊断程序,并让新功能实现者用来提供出色的诊断程序。到目前为止,我们已移植的所有诊断程序都显示出非常可喜的结果,并且我们每天都在努力地进行更多移植。

参考

[1]https://github.com/apple/swift/blob/master/docs/TypeChecker.rst
[2]https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system
[3]https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system#An_inference_algorithm
[4]https://github.com/apple/swift/blob/master/lib/Sema/CSFix.h#L542L554
[5]https://github.com/apple/swift/blob/master/lib/Sema/CSDiagnostics.cpp#L1030L1047

关注下面的标签,发现更多相似文章
评论