代码审查 - BJonas的SchemeInterpreter(J语言)
摘要
本文审查了一个用J实现的Scheme解释器,解释了分词器和递归下降解析器的代码。
<p>相关地,关于如何处理树以生成s表达式解析器的说明:<a href="https://tangentstorm.github.io/apljk/treebuild.ijs.html" rel="ugc">https://tangentstorm.github.io/apljk/treebuild.ijs.html</a></p>
<p><a href="https://lobste.rs/s/742jls/code_review_bjonas_schemeinterpreter_j">评论</a></p>
查看缓存全文
缓存时间: 2026/06/29 06:24
# 代码审查:BJonas在J中的Scheme解释器实验
Source: http://tangentstorm.github.io/apljk/bjonas-scheme.ijs.html
> `tok`动词将Scheme代码字符串分割为标记,但实际上并不解码这些标记。
``
spcp =: e.&(9 10 11 12 13 32{a.)
tok1 =: (((~:1&|.)@:spcp+.(+.1&|.)@:(e.&'()'))<;.2])
tok =: ((#~-.@:spcp@:{.@:>"0)@:tok1@:(,&' ')) ::[:
``
好。`spcp`是一个单子,它仅仅是检查其`y`参数是否对应任何编号的ASCII字符(它们都代表空白)。我猜这个名字是“空白谓词”的助记符,遵循Lisp传统。
至于另外两个,我还没习惯看这么密集的代码,所以试着展开一点。首先,`tok1`:
``
tok1 =: (((~: 1&|.)@:spcp +. (+. 1&|.)@:(e.&'()')) <;.2 ])
``
整体结构是:
``
((a b)@:c d (d b)@:e) f ]
``
它被用作单子,具体作用如下:
``
tok1 '(one (( two) three) (four))'
┌─┬───┬──┬─┬─┬──┬───┬─┬─────┬─────┬─┬─┬─┬────┬─┬─┐
│(│one│ │(│(│ │two│)│ │three│)│ │(│four│)│)│
└─┴───┴──┴─┴─┴──┴───┴─┴─────┴─────┴─┴─┴─┴────┴─┴─┘
``
这里的主要动词是`<;\.2`,其中`;\.`是([二元切割连词](http://www.jsoftware.com/help/dictionary/d331.htm))。左边是动词(单子`<`,即[装箱](http://www.jsoftware.com/help/dictionary/d010.htm)),右边是数字2,`;\.`产生一个新动词,它右边接受一个值数组,左边接受一个布尔数组。布尔值指示在应用动词之前在哪里进行切割。
因此左边的分叉(a...e位置)只是分别(在*其*左右两侧)检查空格和括号。二元`+.`是布尔“或”,`~:`是异或/不等,`1&|.`是将输入左移一个字符。所以右侧表示在每个括号字符前后进行切割,左侧表示在空格处切割,但仅当我们*尚未处于*空格中时。
现在它被用在`tok`中,我为了可读性展开如下:
``
tok =: (( #~ -.@: spcp@: {.@: > "0 ) @: tok1 @: (,&' ')) :: [:
``
使用`::`连词(*adverse*)让我感到意外。它的目的是指定当左边动词抛出错误时该做什么。由于`[:`本身会抛出错误,我还不理解他为什么要这样做。
其余部分是一个长管道。从右向左读:
- 在输入后追加一个空格,然后运行`tok1`
- 让`tok1`将字符串切割成箱子
- 对于每个箱子:
- 打开箱子
- 忽略第一个字符
- 检查剩余的字符是否为空格
- 反转结果(`-.`表示布尔“非”)
- 去除空格(非空格保留1个副本,空格保留0个副本)
说得有道理!
> `rdr`动词将Scheme代码字符串读入树,但树的叶子仍然是未解码的标记。
``
rdrc =: ('()'i.{.@:>)
rdrs =: rdrb`rdre`rdra@.(rdrc@:{.)
rdrb =: (<@:}: , rdrs@:>@:{:) @: rdrs@:}.
rdre =: <@:}.
rdra =: {. , rdrs@:}.
rdr1 =: {.@:rdrs@:(,&(<')'))
rdr =: (rdr1 @: tok) ::[:
``
这里我们又看到了神秘的`::[:`。更重要的是,它只是将`rdr1`与`tok`组合起来。
这实际上是一个递归下降解析器。让我们跟踪调用链,从`rdr`开始:
- `rdr`对输入应用`tok`,然后对结果应用`rdr1`。
- `rdr1`在token链末尾追加一个装箱的右括号,然后调用`rdrs`并取结果的头部。
- `rdrs`选择第一个token,然后根据`rdrc`的结果,调用`rdrb`、`rdre`或`rdra`。
- `rdrc`只是解包一个token,并返回其在字符串'()'中第一个字符的索引:
- '(' 得到0,所以`rdrb`中的'b'代表*开始*。
- ')' 得到1,所以`rdre`中的'e'代表*结束*。
- 其他值得到2,所以`rdra`中的'a'代表*任意*。
假设输入是一个格式良好的s-表达式,那么第一个token将是左括号。让我们看看`rdrb`:
``
rdrb =: (<@:}: , rdrs@:>@:{:) @: rdrs@:}.
``
这是一个管道。从右向左,去掉输入的第一个元素(即移除左括号token),然后对剩下的token调用`rdrs`。
让我们用一个具体例子来追踪它:
``
]ts =: '(';'a';'(';'b';'c';')';')'
┌─┬─┬─┬─┬─┬─┬─┐
│(│a│(│b│c│)│)│
└─┴─┴─┴─┴─┴─┴─┘
``
到目前为止,我们已经切掉了第一个 '(',现在看到的是 'a'。所以我们需要将`rdrb`压入一个思维栈,并查看`rdra`,因为当`rdrs`看到一个 'a' 时会调用它。
``
rdra =: {. , rdrs@:}.
``
这是一个叉子。它会将列表的头部(装箱的 'a')追加到对尾部运行`rdrs`的结果上。所以我们再次递归。由于尾部以 '(' 开头,我们又执行了一次`rdrb`,然后为 'b' 和 'c' 执行两次`rdra`调用。所有这些情况都是递归的,因此此时我们已经深入到调用栈的多层中。只有当我们遇到第一个 ')' 时才开始展开栈。
用 ')' 作为第一个token调用`rdrs`会调用`rdre`:
``
rdre =: <@:}.
``
这会去掉token流的头部(移除开头的 ')' token),然后将整个剩余的token流放入一个新的箱子中。
在我们的例子中,我们看到的是:
``
')';')';')'
┌─┬─┬─┐
│)│)│)│
└─┴─┴─┘
``
(额外的 ')' 是由`rdr1`追加的)
结果将是:
``
rdre ')';')';')'
┌─────┐
│┌─┬─┐│
││)│)││
│└─┴─┘│
└─────┘
``
这个结果沿着链向上返回到为 'c' token调用的最内层`rdra`,在那里`rdra`只是将它追加到 'c' 后面。
``
(<'c') , rdre ')';')';')'
┌─┬─────┐
│c│┌─┬─┐│
│ ││)│)││
│ │└─┴─┘│
└─┴─────┘
``
对于 'b' 也是同样:
``
] sofar =. (<'b'), (<'c'), rdre ')';')';')'
┌─┬─┬─────┐
│b│c│┌─┬─┐│
│ │ ││)│)││
│ │ │└─┴─┘│
└─┴─┴─────┘
``
现在我们回到了最内层的`rdrb`调用。
``
rdrb =: (<@:}: , rdrs@:>@:{:) @: rdrs@:}.
^^^^^^^^^^^ 这部分已完成。
``
这给我们留下了叉子,我们将把它应用于上面的结构。
``
(<@:}: , rdrs@:>@:{:)
``
左侧截尾并装箱:
``
<@:}: sofar
┌─────┐
│┌─┬─┐│
││b│c││
│└─┴─┘│
└─────┘
``
右侧取数组的尾部,解包,然后递归地对其调用`rdrs`。
注意,如果你习惯于考虑*列表*的头部和尾部,请记住在J中,尾部是数组的*最后一个元素*,而不是嵌套的cons单元格链。
因此,我们可以从右向左自己构建结果:
``
{: sofar
┌─────┐
│┌─┬─┐│
││)│)││
│└─┴─┘│
└─────┘
``
``
>@:{: sofar
┌─┬─┐
│)│)│
└─┴─┘
``
``
rdrs@:>@:{: sofar
┌───┐
│┌─┐│
││)││
│└─┘│
└───┘
``
现在我们可以通过将这个值追加到左侧的结果上来完成叉子:
``
] sofar2 =. (<@:}: , rdrs@:>@:{:) sofar
┌─────┬───┐
│┌─┬─┐│┌─┐│
││b│c│││)││
│└─┴─┘│└─┘│
└─────┴───┘
``
所以这是对`rdrb`的内层调用的结果,现在我们向上爬回调用栈到`rdra`,它只是将这个值追加到 'a' token:
``
] sofar3 =. (<'a'), sofar2
┌─┬─────┬───┐
│a│┌─┬─┐│┌─┐│
│ ││b│c│││)││
│ │└─┴─┘│└─┘│
└─┴─────┴───┘
``
现在我们回到了最初调用`rdrb`的地方。
``
rdrb =: (<@:}: , rdrs@:>@:{:) @: rdrs@:}.
^^^^^^^^^^^ 再次回到这里,但现在是顶层
``
``
(<@:}: , rdrs@:>@:{:) sofar3
┌─────────┬┐
│┌─┬─────┐││
││a│┌─┬─┐│││
││ ││b│c││││
││ │└─┴─┘│││
│└─┴─────┘││
└─────────┴┘
``
最后,我们回到`rdr1`,它返回头部。
``
{. (<@:}: , rdrs@:>@:{:) sofar3
┌─────────┐
│┌─┬─────┐│
││a│┌─┬─┐││
││ ││b│c│││
││ │└─┴─┘││
│└─┴─────┘│
└─────────┘
``
以下测试字符串在代码中被注释掉了:
``
echo rdr '(lambda (x) (+ 1 (* x x) x))'
┌────────────────────────────┐
│┌──────┬───┬───────────────┐│
││lambda│┌─┐│┌─┬─┬───────┬─┐││
││ ││x│││+│1│┌─┬─┬─┐│x│││
││ │└─┘││ │ ││*│x│x││ │││
││ │ ││ │ │└─┴─┴─┘│ │││
││ │ │└─┴─┴───────┴─┘││
│└──────┴───┴───────────────┘│
└────────────────────────────┘
``
所以现在我们有了递归下降解析器。
相似文章
(如何用Python编写一个(Lisp)解释器)(2010)
Peter Norvig 的经典教程,讲解如何在Python中实现Scheme解释器,阐述了语言解释和求值的核心概念。
7行代码,3分钟:实现一种编程语言(2010)
本文介绍了一种基于 Lambda 演算的图灵完备函数式语言的极简 7 行解释器,展示了 eval/apply 设计模式。
@hwchase17:代码解释器是一个轻量级的代码执行环境,让你可以:- RLMs - 程序化工具调用 - 更多!w…
Harrison Chase 发布了一个名为 code interpreter 的轻量级代码执行环境,它支持 RLMs 和程序化工具调用,无需启动完整的沙箱,更多用例将陆续公布。
jank 现已拥有自己的自定义 IR
jank 是一种 Clojure 方言,现已引入一种在 Clojure 语义层面设计的自定义中间表示,以实现更好的优化并与 JVM 竞争。
@boshen_c: 是时候揭晓这项工作的幕后人物了!@Shanshrew 创建了一种新型解析器架构,速度提升 2-3 倍。Pl…
由 Shanshrew 开发的新型解析器架构比当前最快的 JS/TS 解析器快 2-3 倍,并正在集成到 Oxc 中。