REPL 相关的需求
关于 REPL 可以能有以下几种需求:
-
自定义 Julia 的 REPL 让它更好看、更好用
类似增加更多的代码高亮、设置自定义的自动补全等等 -
为 Julia 自带的 REPL 添加新的类似
?help、]pkg、;shell的模式
一般包开发者可能有这个需求,加载包后能进入自定义的另一个 REPL -
从脚本启动自定义的 REPL。
这种主要是只想利用下 julia 的 libreadline 库。循环 +eachline当然能解决一点问题,不过不能用方向键还是令人不爽。
第一种有包可用
OhMyREPL.jl 做了 语法高亮、括号匹配高亮、彩虹括号等等
HeaderREPLs.jl: They allow you to define a custom “header” type that you can use to print extra information above the prompt.
欢迎补充
第二种也有包可以用
ReplMaker.jl 能在 julia 自带的 REPL 的基础上,通过自定义的按键进入特定的模式。
就是类似?help那种模式,去看一眼包给的例子就懂了。
第三种目前我还没发现可用的包。所以需要探究一下原生 Julia 的 REPL 的启动过程。
从 Base、stdlib 里抄点代码。
我从英文论坛的帖子里找到了起点
See
_startinbase/client.jl
Julia 的 REPL 启动流程分析
以下行号基于
v"1.3.1"版本.
从上至下为调用关系
调用 _start() 的函数在 repl.c 里,涉及到 C 语言所以折叠了。
main() @ ui/repl.c
main()@repl.c#L151
做了一些系统相关的初始化工作。
并且如果带有--lisp参数,会在这里启动femtolispREPL(jl_lisp_prompt())true_main()@repl.c#L85
julia 在 C-API 层面的一些初始化工作。
很有意思的是,这里也做了 fallback,当Base._start未定义时,REPL 依旧能正常工作。
L116-L146 演示了,怎样通过 julia 的 C-API 搭建一个 REPL。
默认情况下会通过 jl_get_global 取得符号 _start 对应的函数指针,然后用 jl_apply 执行它。
_start() @ base/client.jl
_start()@base/client.jl#L452
使用了JLOptions()的全部默认值exec_options()@base/client.jl#L213
开始解析命令行参数ARGSrun_main_repl()@base/client.jl#L351
全局变量active_repl在此定义,这个变量的定义与否(isdefined(Base, :active_repl))可以用来区分程序是否在 Julia 的 REPL 中运行。
函数还会根据参数、环境变量以及系统的差异决定使用:BasicREPL、LineEditREPL还是作为 fallback 的循环。( L405-L415 是循环实现简单 REPL 的例子。)
我更关心基于LineEditREPL的 REPL 怎么启动,那么抄代码就要从 L367-L382 开始。
下一步调用了 REPL.run_repl 这里要换文件了。
run_repl() @stdlib/REPL.jl
@L196 const JULIA_PROMPT = "julia> " REPL 的提示符 PROMPT 在这个文件头定义。
run_repl()@stdlib/REPL.jl#L196
这个函数很短一共就 8 行。它声明了两个Channel用于输入和输出,然后分别调用了 REPL 的前后端。
我们更关心后端,因为要替换成自己的处理函数。所以先看后端。start_repl_backend()@stdlib/REPL.jl#L105
@async开了个死循环持续调用eval_user_input。
这里输入repl_channel里放着的是元组ast, show_value当show_value==-1时后端退出。eval_user_input()@stdlib/REPL.jl#L76
这里又是一个死循环,默认是通过Core.eval(Main, ast)对输入求值,当然我们会想实现自己的求值函数,替代掉这个。
ans变量也是在这里设置的。
求值成功返回(value,false)报错则返回(lasterr,true),返回值塞到response_channel里。
后端(5、6)到这里就结束了。返回去看前端。
前端有3个版本,分别针对:BasicREPL、LineEditREPL 和 StreamREPL。
run_frontend(::BasicREPL)@stdlib/REPL.jl#L215
BasicREPL比较简单,Base.parse_input_line输入、REPL.eval_with_backend执行、REPL.print_response输出,需要修改就复制源代码,直接覆盖掉原函数就好了。run_frontend(::LineEditREPL)@stdlib/REPL.jl#L1034
分三步:配置setup_interface、初始化LineEdit.init_state、启动run_interface。setup_interface()@stdlib/REPL.jl#L772
这个函数一看就很重要,因为函数体开头整齐的写了不少注释。
函数功能如下:初始化需要的组件、并配置 TAB 补全。
函数接受的参数太少了# We setup the interface in two stages. # First, we set up all components (prompt,rsearch,shell,help) # Second, we create keymaps with appropriate transitions between them # and assign them to the componentssetup_interface(repl, hascolor, extra_repl_keymap)通过参数无法自定义太多东西,所以这个函数,需要魔改。
LineEdit.init_state 和 run_interface 跑到 stdlib/REPL/LineEdit.jl 里了
init_state() @stdlib/REPL/LineEdit.jl
init_state()@stdlib/REPL/LineEdit.jl#L2290
新建了一个MIState然后拿传入的参数初始化它。没什么可配置的。run_interface()@stdlib/REPL/LineEdit.jl#L2299
如果未设置中止 flag (!s::MIState.aborted)则循环执行prompt!和mode(state(s)).on_done,后者通过Base.invokelatest执行。on_done是个函数,默认为永远返回nothing的匿名函数。
ReplMaker.jl通过设置on_done来达到自行处理输入的目的。
prompt!()@stdlib/REPL/LineEdit.jl#L2381
初步判断是用来处理keymap的,暂时应该不用动它。
到这里前端也结束了。
暂时没有进一步深入的必要了。
从脚本启动自定义的 REPL
BasicREPL的主要工作都在run_frontend(::BasicREPL)里面,用自己改过的函数覆盖掉原函数即可。LineEditREPL涉及到的函数比较多,还好他的可配置性也更高,先手动初始化,然后修改对应的配置。
eval_user_input 中调用了 Core.eval(Main, ast) 用来执行 ast,需要的时候也有必要覆盖掉这个函数。
理论分析就是如此。我们再来实际的试一试。