在Swift中从不内部工作

在Swift 3中添加的Never类型可以让您定义一个确保您的应用程序崩溃的方法。 尽管我们很少编写直接使用此类型的方法,但由于它是强制崩溃方法的返回类型(如fatalError() ,因此我们会不断与之交互。 这种类型带来的好处是,调用另一个Never -returning方法的方法不需要提供返回值,毕竟肯定会发生崩溃:

  func getSomeNumber()->整数 
致命错误()
//我没有返回Int,但是仍然可以编译
//因为fatalError()返回“从不”。
}

内心深处, Never只是@noreturn的经过改进的“ swifty”版本:该属性提供了完全相同的功能,但由于其性质太复杂而被删除。 将此行为连接到返回类型可以使编译器更好地工作,并且当然也可以最终为开发人员带来更好的外观。

不过,关于Never一些事情吸引了我。 我们知道语言本身没有什么可以让您跳过返回值,那么在编译器中做了什么神奇的事情呢?

就像我以前关于CaseIterable的内在特性的文章一样,我将深入研究编译器,以提取和分析可拦截并更改返回Never的方法功能的代码段,以详细了解如何Swift编译器有效。

免责声明:与往常一样,这是我自己的研究和反向工程的结果。 由于我显然与 Never 的原始开发无关 ,因此某些假设可能并不完全正确。 如果您知道编译器的工作原理,请随时纠正我!

(如果您发现在Medium上难以阅读的代码,请单击此处在我的博客上阅读本文!)

期望找到复杂的代码,标准库中Never的实现向我们展示了其他东西-类型不过是一个空的枚举:

 公共枚举从来没有{} 

尽管这看上去确实很奇怪,但从理论上讲是正确的。 当Never实现时,Swift还在其术语中增加了无人居住类型的概念-一种没有价值的类型,通常用于表示永远不会发生的事情的不存在的结果。 由于无法以任何方式实例化没有大小写的枚举,因此可以完美地表示该概念。 但是,如果类型本身不执行任何操作,那么魔术在哪里完成呢?

在Swift回购中快速搜索"never typecheck"发现一个名为isNoReturnFunction()的方法,该方法搜索是否存在无人类型的返回:

  bool SILFunctionType :: isNoReturnFunction()const { 
for(无符号i = 0,e = getNumResults(); i <e; ++ i){
如果(getResults()[i] .getType()-> isUninhabited())
返回true;
}
返回false;
}

提到的isUninhabited()本身只是检查我们是否正在处理一个空的枚举:

  bool TypeBase :: isUninhabited(){ 
//空的枚举声明无人居住
如果(自动名义上的Decl = getAnyNominal())
如果(自动enumDecl = dyn_cast (nominalDecl))
如果(enumDecl-> getAllElements()。empty())
返回true;
返回false;
}

因为Never从没有实际代码,所以我希望编译器以某种方式直接识别和更改它,但是这表明Never 真正是一个空的枚举-我们看到的行为与类型本身无关,而与概念无关一种无人居住的类型。 这意味着您根本不需要Never忽略return语句:

 枚举崩溃{} 

func logAndCrash()->崩溃{
打印(“糟糕!”)
致命错误()
}

func doSomething()-> Int {
logAndCrash()//编译!
}

实际上,因为编译器不知道它们之间的区别,所以我的Crash类型甚至会抛出提及Never错误:

  func logAndCrash()->崩溃{ 
//无人返回类型为“ Crash”的函数
//缺少对另一个永不返回的函数的调用
//在所有路径上
}

为了完整起见, Never第一个提交确实确实使用了类型本身来生成这种行为,但是由于Never解决方案引起的多个错误,后来将其更改为与所有无人居住的类型一起使用。

这是一个很好的开始,但是我仍然有两个主要问题:

首先,这些方法在哪里使用?

其次,如果所有永不返回的方法都需要返回另一个永不返回的方法,那么我们是否有无限递归? 由于应用程序一定会崩溃,因此线下的某人将不必返回任何东西。 谁做出这个决定?

第一个问题可以通过检查Swift在编译过程中对代码执行的操作来回答。 为了简单明了, Never将秘密隐藏在源文件的SIL表示中。

简而言之,SIL是您的.swift文件和LLVM IR之间的中间地带,基本上是将您的Swift文件转换为“语言”,其中包含有关幕后发生情况的高级语义信息。 这使编译器可以诊断编译错误并执行早期优化,同时仍可以无缝生成最终的LLVM IR,以使LLVM处理其余的编译。

检查返回Never的方法的SIL版本时,应该向我们展示该方法的优化版本,并希望可以指示该类型在后台的工作方式。 我将编译以下代码段–使用显式return语句,以查看SIL版本对此的反应:

  @inline(从不)func crash()->从不{ 
致命错误()
}

func doSomething()-> Int {
崩溃()
让数字= 1 + 1
让otherNumber =数字* 2
返回otherNumber
}

  swiftc -emit-sil never.swift 

运行上述命令后,输出将包含对doSomething()的以下引用:

  // 做点什么() 
sil隐藏@ $ S5never11doSomethingSiyF:$ @ convention(thin)()-> Int {
bb0:
// function_ref crash()
%0 = function_ref @ $ S5never5crashs5NeverOyF:$ @ convention(thin)()->从不//用户:%1
%1 =申请%0():$ @ convention(thin)()->从不
无法访问// ID:%2
} //结束sil函数'$ S5never11doSomethingSiyF'

SIL并不是很容易阅读,但是值得庆幸的是它附带了一些注释,可以帮助我们了解发生了什么。 我们在这里注意到的第一件事是优化的美丽:我在那里添加的所有无法访问的数字代码都完全消失了!

除此之外,我们可以看到在调用crash()之后添加了一个unreachable语句。 普通方法将显示一个return语句,因此无论用什么逻辑处理Never ,显然都会添加该语句。

快速搜索Swift回购后发现, unreachableUnreachableInst ,该类型是注入的,以后当编译器需要对永远不会成功的代码进行决策(在这种情况下甚至无法执行)时,可以使用该类型。

 终止符(UnreachableInst,unreachable,   TermInst,无,不发布) 

但是,此类型不是Never专用的,因此需要更多研究:在将lldb附加到上面使用的命令并为UnreachableInst的init创建断点之后,将在simpleBlocksWithCallsToNoReturn simplifyBlocksWithCallsToNoReturn()内部进行调用:这是在DiagnoseUnreachable.cpp内部定义的方法DiagnoseUnreachable.cpp的回溯显示它是尝试生成最终LLVM IR之前强制执行的优化步骤之一。 (有关如何手动使用lldb的详细信息,请参阅我的CaseIterable文章!)

实际的方法很大,所以我已经对其进行了伪编码:

 静态布尔simpleBlocksWithCallsToNoReturn(SILBasicBlock&BB, 
UnreachableUserCodeReportingState *州){
如果method_returns_never
delete_everything_after_call
inject_fake_unreachable_instruction
}

这完全符合我们在代码段中看到的内容,但是仍然无法回答第一个问题! 为什么这足以停止要求返回值?

答案是另一个名为DataflowDiagnostics.cpp强制性优化传递文件:负责引发与不可达相关的编译错误,例如上一个中的“丢失返回”,“后卫缺少返回”和“从不方法必须调用另一个从不方法”例子。

该文件中的一种方法称为isNoReturnFunction() ,它会引发“缺少返回”错误:(请注意对isNoReturnFunction()的调用会引发不同的Never错误!)

 静态void diagnosticMissingReturn(const UnreachableInst * UI, 
ASTContext&Context){
//删除:获取类型数据
自动diagID = F-> isNoReturnFunction()吗? diag :: missing_never_call
:diag :: missing_return;

//“诊断”抛出编译错误
诊断(上下文,
L.getEndSourceLoc(),
diagID,ResTy,
FLoc.isASTNode&ClosureExpr>()吗? 1:0);
}

调用diagnoseMissingReturn()的决定由diagnoseUnreachable()处理,该函数检查相关的UnreachableInst指向代码中的实际位置(当您确实错过了返回值时,因此抛出错误)或由编译器注入(就像返回Never ,什么也不做),这正是我们的情况。 请记住,之前的优化删除了Never调用之后的所有内容,因此我们现在只注入了一个:

  static void diagnosticUnreachable(const SILInstruction * I, 
ASTContext&Context){
如果(自动* UI = dyn_cast (I)){
SILLocation L = UI-> getLoc();

//无效的位置意味着指令已由SIL生成
//通过,例如DCE。 FIXME:我们可能只想介绍一个单独的
//指令类型,而不是保持不变。
//
//我们也不想为以前的代码发出诊断信息
//透明内联。 我们应该已经发出了这些
//在处理被调用者函数之前进行诊断
//内联。
如果(!L || L.is ())
返回;

//收到不可达指令的最常见情况是
//缺少return语句。 在这种情况下,我们知道指令
// location将是封闭函数。
如果(L.isASTNode ()|| L.isASTNode ()){
diagnosticMissingReturn(UI,Context);
返回;
}

如果(自动* Guard = L.getAsASTNode ()){
诊断(上下文,警卫队-> getBody()-> getEndLoc(),
diag :: guard_body_must_not_fallthrough);
返回;
}
}
}

总之,在优化过程中定义了Never的行为–在DiagnoseUnreachable.cppNever调用之后检测到指令并将其标记为Unreachable之后, DataflowDiagnostics.cpp看到该特定的unreachable语句由编译器自身注入,避免抛出“缺少返回”编译错误,并使编译继续进行。

尽管现在已经发现了主要功能,但是仍然有一些问题困扰着我:我们已经看到,创建一个Never返回的方法,如果不调用另一个Never方法,则会导致编译错误。 这不是一个无限循环吗? 该循环在哪里中断?

为了得到答案,我们可以检查fatalError()的内容并开始向上追溯。 以下是fatalError()的定义方式:

 公共功能fatalError( 
_消息:@autoclosure()-> String = String(),
文件:StaticString = #file,行:UInt = #line
)->从不{
_assertionFailure(“致命错误”,message(),文件:文件,行:行,
标志:_fatalErrorFlags())
}

这将正确编译,因为_assertionFailure也返回Never 。 回溯到回溯,我们将看到它具有以下实现:

 内部函数_assertionFailure( 
_前缀:StaticString,_消息:StaticString,
文件:StaticString,行:UInt,
标志:UInt32
)->从不{
//已删除:写入文件
Builtin.int_trap()
}

现在, Builtin.int_trap()还返回Never ,因此也可以正确编译。 回溯到回溯,我们将看到int_trap()被定义为……呃……。

实际上,此方法没有定义! Builtin不是一个普通的框架-它似乎是在编译器内部生成的,它是允许Swift代码直接访问LLVM函数的一种方式。 通过从swift-llvm存储库一直解析表并从中生成“预编译” Swift方法,似乎可以在Builtins.cpp中完成所有操作。 在这种情况下,将int_trap()解析为Never返回的方法,该方法调用llvm.trap(),该函数添加了一条指令来炸毁您的应用程序。

那么无限循环如何停止? 答案似乎是根本没有。 因为从某种意义上说,链的最终调用是在“预编译框架”内,所以诊断似乎并没有影响它,从而允许该应用正确编译。

那是相当不错的旅程,但是我希望您觉得这很有用。 正如我在前面的文章中提到的那样,编译器是可怕的怪物,但是了解语言的内部知识确实可以帮助您编写高效的代码。 在这种情况下,Severunking Never很有趣,因为每个优化过程在发现问题或更改一段代码的行为时如何相互补充。

在我的Twitter上关注我-@rockthebruno,让我知道您想分享的任何建议和更正。

Swift源代码
SE-0102:从不介绍
快速中级语言
swift-llvm
LLVM参考


最初发布于 swiftrocks.com