本文由知乎網友「漫慢忙」翻譯自官方博客 《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
存在問題以及該問題所在的位置,可是類型推斷引擎只能依靠約束簡化算法來肯定問題所在。
正如咱們以前討論的,約束求解器首先爲 str
,1
和 +
生成約束。輸入表達式的每一個不一樣子元素(如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 代碼:
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