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
_start
inbase/client.jl
Julia 的 REPL 启动流程分析
以下行号基于
v"1.3.1"
版本.
从上至下为调用关系
调用 _start() 的函数在 repl.c 里,涉及到 C 语言所以折叠了。
main()
@ ui/repl.c
main()
@repl.c#L151
做了一些系统相关的初始化工作。
并且如果带有--lisp
参数,会在这里启动femtolisp
REPL(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
开始解析命令行参数ARGS
run_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 components
setup_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,需要的时候也有必要覆盖掉这个函数。
理论分析就是如此。我们再来实际的试一试。