Julia 解释器和调试器

2019 年 3 月 19 日 | Tim HolyKristoffer CarlssonSebastian PfitznerKeno Fischer

作者很高兴地宣布发布一个功能齐全的 Julia 调试器。你现在可以用多种方式轻松调试和检查 Julia 代码

调试器本身是一组工具,可以实现这些功能。核心由 一个解释器 提供支持,该解释器可以忠实地运行 Julia 代码,同时允许各种前端控制其执行。每个前端都是它自己的包:Juno 将调试器集成到它的 IDE 中,Rebugger 提供了一个 REPL 文本 UI,而传统的 step/next/continue 命令行界面由 Debugger 提供。所有这些新的调试功能都与 Revise 无缝集成,因此你可以在单个会话中持续分析和修改代码。

  1. 对前端调试器的简要用户级介绍
    1. Juno
    2. 调试器和 Rebugger
  2. 对包的概述
    1. JuliaInterpreter
    2. LoweredCodeUtils
    3. CodeTracking
    4. Revise 和 Rebugger
  3. 总结

对前端调试器的简要用户级介绍

为了让潜在用户了解调试器前端,我们在此包含几张屏幕截图,突出显示了新功能。

Juno

Juno 在解释器周围提供了一个丰富的用户界面,并允许你在源代码中直接设置断点并逐步执行。调试器 REPL 可以在一个局部上下文中执行任意代码,而工作区允许你检查局部变量。下面的截图显示了 gcd 的一个小调试会话

Juno

Juno.@run 宏解释你的代码,如果它遇到断点,就会把你放到一个调试会话中,而 Juno.@enter 允许你从第一行开始逐步执行。

调试器和 Rebugger

如果你有一个与 Atom 不同的编辑器,或者有时通过控制台界面在远程会话中工作,你可以选择通过 REPL 进行调试。有两个 REPL 界面:Debugger 提供了一个类似于 gdb 等调试器的“step, next, continue”界面,而 Rebugger 旨在提供一个类似于 IDE 的控制台界面。Debugger 有一些其他界面都没有的功能(例如,对步进的非常细粒度的控制,执行生成函数的生成器等等),因此对于特别困难的情况,它应该是你的首选。下面是一个使用 Debugger.jl 的短会话的截图,展示了它的一些功能

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

Rebugger 还具有“edit”界面。有关更多信息,请参阅 Rebugger 的文档

对包的概述

随着几个包首次亮相,以及一些旧包获得了新功能,我们觉得应该提供一个对新生态系统基础的概述。

JuliaInterpreter

JuliaInterpreter 是整个堆栈的关键;它包含评估和检查正在运行的 Julia 代码所需的逻辑。一个 解释器 自然地适合逐步代码评估和断点的实现。

JuliaInterpreter 来源于 Keno Fischer 编写的原始包 ASTInterpreter2。在它的原始形式中(在 2019 年 1 月之前),ASTInterpreter2 是一个相当小但很复杂的包,能够处理 Julia 代码内部表示的许多高级方面。它需要更新到 Julia 1.0 的许多更改中,虽然大部分工作已经由 Neethu Joy 在 2018 年底完成。当我们开始自己的努力时,我们完成了更新,并决定在许多方面扩展它

要探索解释器本身,你可以像这样开始

using JuliaInterpreter
A = rand(1:10, 5)
@interpret sum(A)

如果一切正常,你应该看到与在没有 @interpret 的情况下运行 sum(A) 所得的答案相同。

LoweredCodeUtils

LoweredCodeUtils 是新包中最专业和最不透明的包。它的目的是在多个协作方法之间建立链接。例如,看似简单的定义

mymethod(x, y=0; z="Hello", msg="world") = 1

实际上创建了 5 个方法:一个“主体方法”(这里,简单地返回 1),两个“位置参数”方法(不接受任何关键字参数的方法),以及两个“关键字函数”方法(当提供至少一个关键字参数时调用,然后填充默认值并标准化顺序)。由于所有这 5 个方法都来自相同的用户提供的表达式,因此需要隐式地将它们链接起来,以便提供令人满意的用户体验。特别是,对源文件的更改会导致已编译方法的行号过时;如果我们不纠正这一点,Juno 可能会在步进代码时打开一个文件到过时的行号。LoweredCodeUtils 执行源级别分析以发现这些关联,并处理多次解析同一个文件时出现的差异。

如果你曾经想要能够解析 Julia 代码并提取它定义的方法的签名(无需重新定义方法),LoweredCodeUtils 就是你的包。

CodeTracking

CodeTracking 被设计为从 Revise 检索数据的简单、轻量级的“查询 API”。本质上,LoweredCodeUtils 执行分析,Revise 管理随时间发生的更改,而 CodeTracking 通知世界其他地方。为了让 CodeTracking 做一些有趣的事情,你需要运行 Revise;为了让 CodeTracking 成为一个轻量级的依赖项,它依赖于 Revise 来填充它自己的内部变量。

有关更多信息,请参阅 CodeTracking 的自述文件

Revise 和 Rebugger

由于基于 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 的快速发展,并使它成为一种更有趣的编程语言。