Swift 5.2 新诊断框架

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

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

在此博客文章中,咱们想分享一些即将推出的 Swift 5.2 的重要更新,以改进新版本的的诊断功能。这包括编译器诊断故障的新策略,该策略最初是 Swift 5.1 发行版的一部分,其引入了一些使人兴奋的新结果并改进了错误消息。github

挑战

Swift 是一种具备丰富表现力的语言,它有丰富的类型系统,这个系统有许多特性,例如类继承,协议一致性,泛型和重载。尽管做为程序员,咱们会不遗余力编写格式良好的代码,但有时咱们须要一点帮助。幸运的是,编译器知道什么样的 Swift 代码是有效的或者无效的。问题是如何更好地告诉您出了什么问题,问题在哪以及如何解决。算法

编译器作了许多事情来确保程序的正确性,可是这项工做的重点一直是改进类型检查器。Swift 类型检查器强制执行有关如何在源代码中使用类型的规则,并在你违反了这些规则时告诉你。spring

例如如下代码:express

struct S<T> {
  init(_: [T]) {}
}

var i = 42
_ = S<Int>([i!])
复制代码

会产生如下诊断结果:编程

error: type of expression is ambiguous without more context
复制代码

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

类型推断概述

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

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

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

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

• 约束中描述的类型能够是 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

相关文章
相关标签/搜索