代码审查 - BJonas的SchemeInterpreter(J语言)

Lobsters Hottest 工具

摘要

本文审查了一个用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││ │││ ││ │ ││ │ │└─┴─┴─┘│ │││ ││ │ │└─┴─┴───────┴─┘││ │└──────┴───┴───────────────┘│ └────────────────────────────┘ `` 所以现在我们有了递归下降解析器。

相似文章

jank 现已拥有自己的自定义 IR

Lobsters Hottest

jank 是一种 Clojure 方言,现已引入一种在 Clojure 语义层面设计的自定义中间表示,以实现更好的优化并与 JVM 竞争。