作者很高兴地宣布发布一个功能齐全的 Julia 调试器。你现在可以用多种方式轻松调试和检查 Julia 代码
进入函数并手动遍历代码,同时检查其状态
设置断点并捕获错误,允许你发现问题点出了什么问题
交互式更新和替换现有代码,以便快速修复错误而无需重新启动
使用 Juno 中的功能齐全的 IDE 将所有这些功能捆绑在一个易于使用的图形界面中
调试器本身是一组工具,可以实现这些功能。核心由 一个解释器 提供支持,该解释器可以忠实地运行 Julia 代码,同时允许各种前端控制其执行。每个前端都是它自己的包:Juno 将调试器集成到它的 IDE 中,Rebugger 提供了一个 REPL 文本 UI,而传统的 step/next/continue 命令行界面由 Debugger 提供。所有这些新的调试功能都与 Revise 无缝集成,因此你可以在单个会话中持续分析和修改代码。
为了让潜在用户了解调试器前端,我们在此包含几张屏幕截图,突出显示了新功能。
Juno 在解释器周围提供了一个丰富的用户界面,并允许你在源代码中直接设置断点并逐步执行。调试器 REPL 可以在一个局部上下文中执行任意代码,而工作区允许你检查局部变量。下面的截图显示了 gcd
的一个小调试会话
Juno.@run
宏解释你的代码,如果它遇到断点,就会把你放到一个调试会话中,而 Juno.@enter
允许你从第一行开始逐步执行。
如果你有一个与 Atom 不同的编辑器,或者有时通过控制台界面在远程会话中工作,你可以选择通过 REPL 进行调试。有两个 REPL 界面:Debugger 提供了一个类似于 gdb
等调试器的“step, next, continue”界面,而 Rebugger 旨在提供一个类似于 IDE 的控制台界面。Debugger 有一些其他界面都没有的功能(例如,对步进的非常细粒度的控制,执行生成函数的生成器等等),因此对于特别困难的情况,它应该是你的首选。下面是一个使用 Debugger.jl 的短会话的截图,展示了它的一些功能
在截图中,函数 closestpair
通过在调用之前加上 @enter
宏来调试。然后在函数的第一行(第 4 行)暂停执行,并且可以看出第 8 行有一个断点。在运行命令 c
(代表“continue”)后,执行将继续,直到遇到断点。此时,命令 fr
(代表“frame”)显示了代码执行因断点而暂停时所有局部变量及其值。最后,使用 `
键进入“Julia REPL 模式”。这将提供一个正常的 Julia REPL 模式,此外局部变量也可用。
Rebugger 通过一个键绑定进入调用。要尝试它,输入 gcd(10, 20)
并且不要按回车键,然后输入 Meta-i(Esc-i、Alt-i 或 option-i)。短暂暂停后,显示应更新;输入 ?
查看可能的动作
Rebugger 还具有“edit”界面。有关更多信息,请参阅 Rebugger 的文档。
随着几个包首次亮相,以及一些旧包获得了新功能,我们觉得应该提供一个对新生态系统基础的概述。
JuliaInterpreter 是整个堆栈的关键;它包含评估和检查正在运行的 Julia 代码所需的逻辑。一个 解释器 自然地适合逐步代码评估和断点的实现。
JuliaInterpreter 来源于 Keno Fischer 编写的原始包 ASTInterpreter2。在它的原始形式中(在 2019 年 1 月之前),ASTInterpreter2 是一个相当小但很复杂的包,能够处理 Julia 代码内部表示的许多高级方面。它需要更新到 Julia 1.0 的许多更改中,虽然大部分工作已经由 Neethu Joy 在 2018 年底完成。当我们开始自己的努力时,我们完成了更新,并决定在许多方面扩展它
JuliaInterpreter 变得默认递归,它解释所有调用,一直到定义 Julia 最低级别的 ccall
、内在函数和内置函数。通过运行几乎所有代码,它变得更容易实现断点并捕获错误。
JuliaInterpreter 收到了大量的性能增强,现在可以以大约 50 倍于原始速度逐步执行代码。这些优化减少了运行所有代码中最严重的缺点——缓慢的性能,但远没有消除。希望在未来几个月内,编译代码和解释代码之间的性能差距(可能相差几个数量级)会缩小。但是,解释器始终会比编译代码慢。
值得注意的是,在某些情况下,解释器感觉更快,至少在最初执行时是这样。Julia 的 JIT 编译器产生了很好的结果,但所有这些代码分析都需要时间;人们感兴趣的是探索是否在解释器中运行更多代码可以减少延迟,也就是“第一次绘图时间”问题。JuliaInterpreter 是探索这种权衡的潜在工具,而且似乎 不需要太多额外的工作。
JuliaInterpreter 获得了解释“顶层代码”的能力,例如用于定义包和创建测试套件的代码。这是一个重大变化,部分原因是顶层代码使用扩展的词汇,但主要是由于顶层代码可以定义新的模块、结构和方法,这反过来又引入了管理“世界年龄”的需要,世界年龄是确定方法对调用者可见性的计数器。(如果失败,你会看到类似于“方法太新,无法调用...”的错误。)
支持顶层代码使 JuliaInterpreter 实现了两个目标:能够作为 Revise 的新代码解析能力的基础,以及能够运行最初为编译后的 Julia 代码设计的测试套件。一旦我们部分完成了顶层执行,我们 决定 使用最广泛的单一测试套件(即 Julia 本身)来评估 JuliaInterpreter。这揭示了数十个错误,这些错误出现在调用 C 库(ccall
、@cfunction
和 cglobal
)、llvmcall
、关键字参数函数、生成函数、匿名函数、struct
定义、全局变量、try/catch 的处理、锁和线程以及 @eval
ed 代码的处理等方面。我们通过另外两位贡献者 Gunnar Farnebäck 和 Don MacMillen 从测试套件失败中隔离了其中的一些问题。
截至撰写本文时,大多数干净隔离的问题都已修复。虽然我们距离完美还很远,但追求如此苛刻的目标极大地促进了这些年轻包的稳健性。
JuliaInterpreter 获得了对断点的支持。虽然它不是解释器本身的功能,但它们是构建功能强大的调试器所必需的,并且可以看作是解释器本身内的控制流的一种附加形式。这些断点可以使用函数 breakpoint
和宏 @breakpoint
手动设置,可以在 Juno、Rebugger 或 Debugger 中操作,或者使用 @bp
宏直接添加到代码中。现有的断点可以 disable
、enable
或 remove
。我们支持在特定源代码行或在进入特定方法时设置断点,支持有条件和无条件断点,并且可以自动捕获错误,就好像它们是手动设置的断点一样。
要探索解释器本身,你可以像这样开始
using JuliaInterpreter
A = rand(1:10, 5)
@interpret sum(A)
如果一切正常,你应该看到与在没有 @interpret
的情况下运行 sum(A)
所得的答案相同。
LoweredCodeUtils 是新包中最专业和最不透明的包。它的目的是在多个协作方法之间建立链接。例如,看似简单的定义
mymethod(x, y=0; z="Hello", msg="world") = 1
实际上创建了 5 个方法:一个“主体方法”(这里,简单地返回 1
),两个“位置参数”方法(不接受任何关键字参数的方法),以及两个“关键字函数”方法(当提供至少一个关键字参数时调用,然后填充默认值并标准化顺序)。由于所有这 5 个方法都来自相同的用户提供的表达式,因此需要隐式地将它们链接起来,以便提供令人满意的用户体验。特别是,对源文件的更改会导致已编译方法的行号过时;如果我们不纠正这一点,Juno 可能会在步进代码时打开一个文件到过时的行号。LoweredCodeUtils 执行源级别分析以发现这些关联,并处理多次解析同一个文件时出现的差异。
如果你曾经想要能够解析 Julia 代码并提取它定义的方法的签名(无需重新定义方法),LoweredCodeUtils 就是你的包。
CodeTracking 被设计为从 Revise 检索数据的简单、轻量级的“查询 API”。本质上,LoweredCodeUtils 执行分析,Revise 管理随时间发生的更改,而 CodeTracking 通知世界其他地方。为了让 CodeTracking 做一些有趣的事情,你需要运行 Revise;为了让 CodeTracking 成为一个轻量级的依赖项,它依赖于 Revise 来填充它自己的内部变量。
有关更多信息,请参阅 CodeTracking 的自述文件。
由于基于 JuliaInterpreter 的重写,Revise 和 Rebugger 在核心任务上变得更好(在某些情况下,好得多)。特别是,如果你使用过早期的 Rebugger 版本,你可能已经注意到它被许多语言结构所击败(例如,包含关键字参数的函数、@eval
生成的函数等等)。大多数潜在原因都由 LoweredCodeUtils 解决,而 LoweredCodeUtils 又被 Revise 使用,然后 Revise 将必要的数据提供给 CodeTracking 供 Rebugger 使用。作为衡量差异的一个指标,在 Base 中的 10,000 多个方法中,Revise 1.1.0 无法捕获 1,425 个方法签名(失败率为 13%)。相比之下,Revise 2.0.0 只漏掉了 10 个(<0.1%)。
因此,除了新的“interpret”界面之外,新的 Rebugger 在它的原始“edit”界面上也更好。
Revise(以及随之而来的 Rebugger)也获得了其他一些新功能,例如处理在 REPL 中定义的方法。从长远来看,JuliaInterpreter 和 LoweredCodeUtils 允许的代码深度分析可能会支持以前无法实现的功能。
这只是一个高层概述。一些独立的包有广泛的文档,鼓励感兴趣的读者仔细阅读。对于任何希望深入了解 Julia 代码内部结构的人来说,这些新包提供了一套强大的工具来进行内省和分析。当然,我们希望新的调试功能进一步加速 Julia 的快速发展,并使它成为一种更有趣的编程语言。