debug Swift compiler
简介
作为工程师,我们几乎花费了70%时间来调试,剩下的20%来思考架构和团队协作,只有10%的时间来用写码.
Debugging is like being the detective in a crime movie where you are also the murderer.
— Filipe Fortes via Twitter
下面我们会对如何调试表一起和编译器的输出指令进行说明。
在遇到一些方法分派的问题时,因为编译器实现方式不同,导致调用结果不符合预期的"bug",都可以通过对编译中间结果的查看来追踪问题.下面我们除了讲解几种调试方式,还会针对Swift常见的几个"bug",从编译器的角度进行说明.
基础工具
通常调试一个crash追踪或者构建log的编译器问题的第一步是通过命令行重新运行编译器。
utils/dev-scripts
内的split-cmdline
脚本将命令行拆分成了多个部分。这样用于理解和编辑比较长的指令。
打印中间语言
调试编译器最重要的环节就是检查IR
。 以下是如何在编译器的主流程中输出IR的指令:
-
语法分析:打印语法分析后的
AST
:lisp
swiftc -dump-ast -O file.swift
-
SILGen:在SILGen之后立刻打印
SIL
:
swiftc -emit-silgen -O file.swift
what is dmangle??
-
强制SIL检测:打印强制检测后的
SIL
:
swiftc -emit-sil -Onone file.swift
其他指令
-
SIL性能检查:打印
SIL
通道优化完成之后的SIL
swiftc -emit-sil -O file.swift
- IRGen:在IR生成之后打印LLVM IR
swiftc -emit-ir -Xfrontend -disable-llvm-optzns -O file.swift
- LLVM检测:打印LLVM检测之后的LLVM IR
swiftc -emit-ir -O file.swift
- 生成代码:打印最终生成的代码
swiftc -S -O file.swift
编译器会在打印完对应阶段后终止。所以如果想打印SIL
和LLVM IR,需要运行编译器两次。
调试类型检查
允许打印
可以通过以下参数来允许类型检查的打印-Xfrontend -debug-constraints
。使用该指令会打印类型检查器内部的状态,打印已经解决的约束,展示最终的检查结果:
---Constraint solving for the expression at [test.swift:3:10 - line:3:10]---
---Initial constraints for the given expression---
(integer_literal_expr type='$T0' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] value=0)
Score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Contextual Type: Int
Type Variables:
#0 = $T0 [inout allowed]
Active Constraints:
Inactive Constraints:
$T0 literal conforms to ExpressibleByIntegerLiteral [[locator@0x7ffa3a865a00 [IntegerLiteral@test.swift:3:10]]];
$T0 conv Int [[locator@0x7ffa3a865a00 [IntegerLiteral@test.swift:3:10]]];
($T0 literal=3 bindings=(subtypes of) (default from ExpressibleByIntegerLiteral) Int)
Active bindings: $T0 := Int
(trying $T0 := Int
(found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
)
---Solution---
Fixed score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Type variables:
$T0 as Int
Overload choices:
Constraint restrictions:
Disjunction choices:
Conformances:
At locator@0x7ffa3a865a00 [IntegerLiteral@test.swift:3:10]
(normal_conformance type=Int protocol=ExpressibleByIntegerLiteral lazy
(normal_conformance type=Int protocol=_ExpressibleByBuiltinIntegerLiteral lazy))
(found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
---Type-checked expression---
(call_expr implicit type='Int' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] arg_labels=_builtinIntegerLiteral:
(constructor_ref_call_expr implicit type='(_MaxBuiltinIntegerType) -> Int' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10]
(declref_expr implicit type='(Int.Type) -> (_MaxBuiltinIntegerType) -> Int' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] decl=Swift.(file).Int.init(_builtinIntegerLiteral:) function_ref=single)
(type_expr implicit type='Int.Type' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] typerepr='Int'))
(tuple_expr implicit type='(_builtinIntegerLiteral: Int2048)' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] names=_builtinIntegerLiteral
(integer_literal_expr type='Int2048' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] value=0)))
在使用整体的Swift的REPL(read-eval-print loop: 读取-求值-输出 循环),可以输出每个表达式的结果,这个结果和通过:constraints debug on
指令允许约束打印的结果相同:
$ swift -frontend -repl -enable-objc-interop -module-name REPL
*** You are running Swift's integrated REPL, ***
*** intended for compiler and stdlib ***
*** development and testing purposes only. ***
*** The full REPL is built as part of LLDB. ***
*** Type ':help' for assistance. ***
(swift) :constraints debug on
捕获第一个错误的
在修改类型检查器时,会引发一系列的连锁错误。因为Swift不会抛出错误,所以开发者需要对类型检查器足够理解,通过判断觉得如何停止调试器。比起这样,开发者还可以使用条件-Xllvm -swift-diagnostics-assert-on-error=1
来唤起DiagnosticsEngine
诊断引擎抛出第一个错误,用于向开发者提供捕获的信息。
调试SIL的级别
SIL输出指令选项
SILPassManager
提供了有效的选项用于输出各阶段的SIL
。
-Xllvm -sil-print-all
选项用于输出全部检测之后的整个SIL组件。尽管打印结果只是检测后被修改的函数,但是输出非常巨大.
可以通过函数名过滤输出信息:-Xllvm -sil-print-only-function/s
或指定区段-Xllvm -sil-print-before/after/around
.
更多信息可以参考PassManager.cpp.
在LLDB中输出SIL和其他数据
使用LLDB调试Swift编译器,可以发挥非常强大的检测编译数据(比如SIL)能力.遵循LLVM dump()协议,很多SIL类(包括AST类)提供了dump()方法.可以通过LLDB的expression --
,print
或者p
指令来调用dump()方法.
例如,检测SIL结构体
(lldb) p Inst->dump()
%12 = struct_extract %10 : $UnsafeMutablePointer<X>, #UnsafeMutablePointer._rawValue // user: %13
输出检测后的整个方法
(lldb) p getFunction()->dump()
SIL
组件和方法都可能非常大,所以将结果输出到一个文件内更为方便
(lldb) p getFunction()->dump("myfunction.sil")
也可以输出方法的CFG(control flow graph: 控制流程图)
(lldb) p Func->viewCFG()
这个指令会打开一个包含函数CFG的预览窗口.
在SIL层调试和推断
如果想要SIL调试需要同时增加 front-end option -gsil 和 -g :
swiftc -g -Xfrontend -gsil -O test.swift -o a.out
以上指令会在优化之后将SIL写入一个文件,并且生成相关的调试信息.在调试器内看到的是SIL代码而非Swift源码.详情可以查看SILDebugInfoGenerator.
如果想调试Swift标准库,可以使用 build-script-impl的选项--build-sil-debugging-stdlib
.
ViewCFG: 基于CFG打印的正则表达式
ViewCFG(./utils/viewcfg
)是一个脚本,作用是解析文本的CFG然后通过.dot格式展示.解析是通过正则表达式完成的. ViewCFG具有以下能力
- 解析SIL和LLVM IR
- 解析block和函数,不需要知晓上下文信息.(类型或者声明等信息)
使用断点
LLDB具有强大的断点能力.下面通过LLDB在命令行的使用示例来说明.
有时我们在查看SIL输出的函数时,想要知道函数是在编译器哪里创造的.这时就可以SILFunction结构体设置一个断点.
(lldb) br set -c 'hasName("_TFC3nix1Xd")' -f SILFunction.cpp -l 91
如果想知道插入了哪些优化,移除或者移动了哪些说明,可以使用在SILInstruction.cpp内的ilist_traits<SILInstruction>::addNodeToList
或ilist_traits<SILInstruction>::removeNodeFromList
来设置断点.以下示例是在strong_retain
说明被移除时设置断点的指令
(lldb) br set -c 'I->getKind() == ValueKind::StrongRetainInst' -f SILInstruction.cpp -l 63
还可以测试是在哪个方法内发生的
(lldb) br set -c 'I->getKind() == ValueKind::StrongRetainInst &&
I->getFunction()->hasName("_TFC3nix1Xd")'
-f SILInstruction.cpp -l 63