我们可以将(A 2 B 0 C 2 D 0 E 0) 语法视为令牌流。这很容易通过递归下降来解析。任何语言的递归下降解析器通常从流中读取标记,流是一个有状态的对象,对整个递归都是全局的,在递归函数之间传递,或者可能存储在全局区域中。流解决了将输入位置从一个标记移动到下一个标记的问题。
在 Lisp 中,列表本身很容易用作流。例如,如果我们有一个变量s 包含(1 2 3),我们可以使用(pop s) 和其他方式从中删除一个项目。在一些与 Lisp 相关的文献中,惰性列表甚至被称为流。
要在流上编写递归下降解析器,我们可以使递归部分成为局部函数,该函数在范围内具有流变量;然后它可以改变变量以消耗令牌。
这是一个名为 TXR Lisp 的 Lisp 方言中的函数,它解析 (A 2 B 0 C 2 D 0 E 0) 并将其转换为 (A (B) (C (D) (E)))
(defun parse (syntax)
(labels ((rec (parsym n)
(cons parsym
(collect-each ((i 0..n))
(match-case syntax
((@sym 0 . @rest)
(set syntax rest)
(list sym))
((@sym @(integerp @m) . @rest)
(set syntax rest)
(rec sym m))
(() (error "abrupt end in syntax"))
(@else (error "unhandled syntax: ~s" syntax)))))))
(cadr (rec :root 1))))
1> (parse '(A 0))
(A)
2> (parse '(A 1))
** abrupt end in syntax
** during evaluation of form (error "abrupt end in syntax")
** ... an expansion of (progn (error "abrupt end in syntax"))
** which is located at parse.tl:5
3> (parse '(A 1 B 0))
(A (B))
4> (parse '(A 2 B 0 C 2 D 0 E 0))
(A (B) (C (D) (E)))
递归解析由一个名为rec 的函数完成。 rec 通过赋值来改变 syntax 变量。 (set syntax rest) 这两种形式使用匹配的语法前缀,将列表指针移动到语法的未解析剩余部分。
该函数有两个参数:父符号和整数值:该符号有多少子。
这里有一个技巧来启动解析器:我们对rec 的顶级调用是(rec :root 1)。我们假设我们正在解析的整个语法,例如 (A 2 B 0 ...) 已经嵌入到更大的语法 (:root 1 A 2 B 0) 中,并且我们刚刚解析了 :root 1 部分。因此,我们告诉rec 函数:请解析出1 个参数节点,然后使用该参数创建一个:root 节点。然后从rec 函数(给定示例语法)中出现的实际上是(:root (A (B) (C (D) (E)))),因此我们将cadr 应用于它以删除包装以仅获得(A (B) ...)。
函数的工作原理很简单:它使用模式匹配识别两个可能的前缀:
-
一个项目,后跟0,以及其余的语法;否则
-
一个项目,后跟一个非零的整数,然后是其余的。
所有其他情况都是语法错误。我们不检查该项目是否是符号,所以它不一定是,或者整数是正数。
该函数在循环中执行此操作:它迭代其参数所指示的次数,以提取节点语法的许多实例。提取的内容被收集到一个列表中,父符号用cons 附加到前面以产生返回值。
在第一种情况下,当我们有一个后跟零的项目时,没有要为该节点提取的参数,因此没有递归调用。循环的该迭代产生包装在列表中的匹配符号/项目。例如,如果rec 被称为(rec 'Y 1),并且syntax 包含(X 0 whatever ...),则循环将识别X 0 的情况,并收集值(X),将syntax 设置为余数@ 987654351@。因为1,循环只会迭代一次,所以收集的列表是((X))。传入符号 Y 被附加到此以创建 (Y (X))。
在非零情况下,我们有递归。当看到像(Z 2 whatever ...) 这样的语法时,我们只需调用rec,并将提取的符号和数字作为参数:(rec 'Z 2)。这将执行它自己的循环,该循环提取以符号Z 为首的两个参数节点,将该节点返回给我们,并让syntax 变量指向剩余的语法。