Swift5.1编译器诊断系统


苹果最近更新的swift5.1的时候,在这篇博客介绍了新的诊断系统。其实博客也对苹果的诊断系统原理做了一些简单的分析,理解这个特点,对以后用swift语言做开发,解决一些编译错误,感觉帮助挺大的。

苹果也描述了新的诊断系统的好处,就是不仅仅是报告错误,而是会提供解决问题的方法。在swift5.1之前的诊断系统,出一个错误后就停止诊断,而新的诊断系统会继续诊断后面的错误并提供一些fix建议,可以说是非常智能了。

这么智能的诊断系统,苹果也讲解了他们基本的设计思路,swift语言的诊断系统本质上是基于Hindley-Milner类型推断算法开发的。如果熟悉函数式编程语言的话,就会知道Hindley-Milner类型推断系统是函数式程序设计语言的基石。因为函数式语言大多是表达式结构,在程序编译的时候,计算机来确定表达式中变量类型的合法性,都是靠这个算法保证的。对于这个算法相对来讲彻底理解还是比较复杂的,苹果举了几个简单的实际应用的例子给开发者,也方便开发者之后理解swift这套智能诊断系统。

类型推导算法

通过下面这段代码,我们来分析下swift编译器是如何来做语法诊断的

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

要分析上面的代码,对于诊断系统的话,大致分为三步

变量类型的分析

  • $Str代表的str变量的类型,这其实是Hindley-Milner算法的定义方式,str是作为+调用第一个参数
  • $One代表的是1这个类型,代表的是+调用的第二个参数
  • $Result代表着调用+后的结果类型
  • $Plus代表是+操作符本身的类型,并且这个符号可以被重载。本质上就是函数的调用。

变量的约束

  • $Str <bind to> String 这个表达式是类型推断系统的语法,意思是参数str有一个固定的String类型
  • $One <conforms to> ExpressibleByIntegerLiteral 这代表这个类型要遵循ExpressibleByIntegerLiteral协议,比如Int,Double都是遵循这个协议的
  • $Plus <bind to> disjunction ((String, String) -> String, (Int, Int) -> Int, …) 这个代表的是一个表达式,满足里面这些参数条件,并且可以被重载
  • ($Str, $One) -> $Result <applicable to> $Plus 这时候$Result这个类型就是未知的。他将需要上面的表达式,带上二个参数的类型来决定的。

在swift语法解析后,会生成这样的一个树形结构,我们把类型推导的变量约束放到语法树中,就得到如下的图:

图一

推断算法执行的过程

有了上面对变量和表达式的类型定义和约束之后,下面我们来看下swift语言类型推导的过程

1.首先绑定$Plus这个表达式的第一个表述(String, String) -> String

2.第二步我们就拿($Str, $One) -> $Result上面解析出来的表达式,应用到$Plus这个表达式的约束,这个过程如下:

  • 首先拿表达式第0个参数String匹配$Str的类型。
  • 再拿第一个参数String匹配$One的类型
  • 最后就是String类型匹配$Result的类型

3.然后就针对第二步,对类型匹配做判断:

  • $Str匹配了String类型
  • $Result类型如果匹配上面的表达式,应该也要分配为String类型

4.现在就剩下$One类型的判断

  • 从表达式匹配来看$One需要是String类型
  • 而前面一步已经确定$One需要 <conforms to> 符合ExpressibleByIntegerLiteral这个协议

5.符合ExpressibleByIntegerLiteral有Int和Double类型,他们都不符合String类型。所以$One的类型约束就出现了歧义

6.尝试$Plus其他表达式(例如 (Int, Int) -> Int)循环上面的1-5步骤

在遍历完所有的情况后,发现都不能满足约束,这时候编译器的推断系统就会报错,通过上面的分析,我们清楚了如何识别错误位置,但是如何帮助诊断系统,判断出来这种情况下如何修改为正确的内容,下面就是苹果给出的一个完整的解决方案。

错误提示解决方案

swift新的诊断系统会根据上面判断出来的错误位置,给出解决的方法。这个是先前的诊断系统和现在的主要区别,以前的只会告诉你那里错误了,而现在会告诉你怎么更改。例如上面的这个例子,推断系统判断,如果使用$Plus表达式,错误的地方是$One代表的参数类型不能适配ExpressibleByIntegerLiteral协议,我们基于这个错误,可以给出下面两个提示。

error: binary operator '+' cannot be applied to arguments 'String' and 'Int'

还有一个错误提示如下:

error: argument type 'String' does not conform to 'ExpressibleByIntegerLiteral'

我们会根据一些规则来选择这两个提示方案中的一个,新的推断系统往往会选择第二个错误提示方案,下面就通过另一个例子再来深入剖析下,具体为啥选择第二个提示。

深入剖析

当一个错误约束被侦测出来的时候,我们通过捕获下面的信息来fix这个错误:

  • 哪种类型的错误
  • 错误在源码中的位置
  • 错误的类型和声明

诊断系统会综合判断上面所有的错误,直到能够给出所有的解决方法,然后就会停止诊断,下面看个例子

func foo(_: inout Int) {}

var x: Int = 0
foo(x)

通过上面的分析我们知道错误如何定位了,下面我就简单的列出来步骤

变量类型分析

$X := Int
$Foo := (inout Int) -> Void
$Result

变量的约束

先看下3个变量类型约束的表达式

($X) -> $Result <applicable to> $Foo

($X) -> $Result to (inout Int) -> Void这个表达式的约束,可以让我们推断出下面的约束

Int <convertible to> inout Int
$Result <equal to> Void

这时候就很容易发现Int不能够适配inout Int这个类型,因此约束推断系统就会记录这个错误位置,变量缺少一个&地址符号。然后根据这个错误,类型诊断系统就会提示需要插入一个&解决这个错误:

error: passing value of type 'Int' to an inout parameter requires explicit '&'
foo(x)
    ^
    &

下面这个例子是说明swift的诊断系统不会判断出来一个错误后就停止诊断了,而是会继续寻找直到把所有的错误都解决掉,例如:

func foo(_: inout Int, bar: String) {}

var x: Int = 0
foo(x, "bar")

上面的错误提示就会有下面这些

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:

如果你想看更多的例子的话可以去swift的blog

总结

总之苹果新的诊断系统,能够提供出解决方案,可以更方便开发者快速解决错误。这看起来是相当诱人的,不过从之前苹果对编译器智能提示的优化的体验来看,但愿新的诊断系统,别让我的mac电脑的风扇转动的更厉害,然后所有的提示都罢工的情况再出现了😹。

如果你喜欢这篇文章,谢谢你的赞赏

图3

如有疑问请联系我