微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

启动方案语言的解析器 芦苇芦苇令牌宏字符其他注意事项

如何解决启动方案语言的解析器 芦苇芦苇令牌宏字符其他注意事项

我正在为 Scheme 解释器编写一个基本的解析器,这里是我为定义各种类型的标记而设置的定义:

# 1. Parens
Type:
    PAREN
Subtype:
    LEFT_PAREN
Value:
    '('

# 2. Operators (<=,=,+,...)
Type:
    OPERATOR
Subtype:
    EQUALS
Value:
    '='
Arity:
    2

# 3. Types (2.5,"Hello",#f,etc.)
Type:
    DATA
Subtype:
    NUMBER
Value:
    2.4

# 4. Procedures,builtins,and such
Type:
    KEYWORD
Subtype:
    BUILTIN
Value:
    "set"
Arity:
    2
PROCEDURE:
    ... // probably need a new class for this

以上看起来是一个好的起点吗?是否有一些明显的东西我在这里遗漏了,或者这是否给了我一个“足够好”的基础?

解决方法

您的方法做出了语言语法中不存在的区别,并且还为时过早地做出了决定。例如考虑这个程序:

(let ((x 1))
  (with-assignment-notes
    (set! x 2)
    (set! x 3)
    x))

当我运行这个时:

> (let ((x 1))
    (with-assignment-notes
      (set! x 2)
      (set! x 3)
      x))
setting x to 2
setting x to 3
3

为了使其起作用,with-assignment-notes 必须以某种方式重新定义 (set! ...) 在其主体中的含义。这是一个hacky且可能不正确的(Racket)实现:

(define-syntax with-assignment-notes
  (syntax-rules (set!)
    [(_ form ...)
     (let-syntax ([rewrite/maybe
                   (syntax-rules (set!)
                     [(_ (set! var val))
                      (let ([r val])
                        (printf "setting ~A to ~A~%" 'var r)
                        (set! var r))]
                     [(_ thing)
                      thing])])
       (rewrite/maybe form) ...)]))

因此,Lisp 家族语言的任何解析器的关键特性是:

  • 它不应该对它可以避免的语言语义做出任何决定;
  • 它构建的结​​构必须作为第一类对象可供语言本身使用;
  • (和可选)解析器应该可以从语言本身进行修改。

例如:

  • 解析器可能不可避免地需要决定什么是数字,什么不是数字以及它是什么类型的数字;
  • 如果它有对字符串的默认处理会很好,但理想情况下这应该由用户控制;
  • 它根本不应该决定什么,比如 (< x y) 意味着什么,而是应该返回一个表示它的结构以供语言解释。

最后一个可选要求的原因是 Lisp 家族语言被有兴趣使用它们来实现语言的人使用。允许从语言内部改变读者可以让这变得更加容易,因为每次你想要创建一种有点像你开始使用的语言但不完全的语言时,你不必从头开始。

解析 Lisp

解析 Lisp 家族语言的常用方法是使用机器将字符序列转换为由语言本身定义的对象组成的 s 表达式序列,特别是符号和 conses(还有数字,字符串 &c)。一旦你有了这个结构,你就可以遍历它,将它解释为一个程序:要么动态评估它,要么编译它。至关重要的是,您还可以编写程序来操作此结构本身:宏。

在像 CL 这样的“传统”Lisps 中,这个过程是明确的:有一个“阅读器”将字符序列转换为 s 表达式序列,宏显式地操作这些 s 表达式的列表结构,之后评估器/编译器处理它们。所以在传统的 Lisp 中,(< x y) 会被解析为 (a cons of a symbol < and (a cons of a symbol x and (a cons of a symbol y and空列表对象)) 或 (< . (x . (y . ()))),并且这个结构被传递给宏扩展器,从而传递给评估器或编译器。

在 Scheme 中,它有点微妙:宏是根据规则指定的(无论如何都是可移植的),这些规则将一些语法转换为另一种语法,并且(我认为)并不明确这些对象是否由缺点和符号与否。但是可用于语法规则的结构需要像由 conses 和符号组成的东西一样丰富,因为语法规则可以在其中闲逛。如果你想写类似下面的宏:

(define-syntax with-silly-escape
  (syntax-rules ()
    [(_ (escape) form ...)
     (call/cc (λ (c)
                (define (escape) (c 'escaped))
                form ...))]
    [(_ (escape val ...) form ...)
     (call/cc (λ (c)
                (define (escape) (c val ...))
                form ...))]))

然后,您需要能够查看来自读者的内容的结构,并且该结构需要像由列表和 conses 组成的内容一样丰富。

玩具阅读器:reeder

Reeder 是一个用 Common Lisp 编写的小型 Lisp 阅读器,我不久前写了它,原因我忘记了(但也许是为了帮助我学习它使用的 CL-PPCRE)。它无疑是一个玩具,但它也足够小且足够简单易懂:当然它比标准 CL 阅读器更小、更简单,并且它展示了一种解决此问题的方法。它由一个称为 reedtable 的表驱动,该表定义了解析的进行方式。

例如:

> (with-input-from-string (in "(defun foo (x) x)")
    (reed :from in))
(defun foo (x) x)

芦苇

使用芦苇台阅读(芦苇)某物:

  1. 寻找下一个有趣的字符,它是表中未定义为空白的下一个字符(reedtables 有一个可配置的空白字符列表);
  2. 如果该字符在表中被定义为宏字符,则调用其函数读取内容;
  3. 否则调用表的令牌读取器来读取和解释令牌。

芦苇令牌

token reader 位于 reedtable 中,负责积累和解释 token:

  1. 它以自己已知的方式累积一个令牌(但默认的方法是沿着字符串处理在 reedtable 中定义的单个(\)和多个(|)转义,直到它获取表格中的空白内容);
  2. 此时它有一个字符串,它要求 reedtable 将这个字符串转换成某种东西,它通过标记解析器来完成。

第二步有一个小问题:当令牌阅读器累积一个令牌时,它会跟踪它是否“变性”,这意味着其中有转义字符。它将此信息传递给令牌解析器,例如,这允许它们将已变性的 |1| 解释为与未变性的 1 不同。

标记解析器也在 reedtable 中定义:有一个 define-token-parser 形式来定义它们。他们有优先级,所以优先级最高的人会先被尝试,然后他们可以决定是否应该对变性代币进行尝试。一些令牌解析器应该总是适用:如果没有,这是一个错误。

默认的 reedtable 具有可以解析整数和有理数的标记解析器,以及解析符号的回退解析器。下面是一个示例,说明如何替换此回退解析器,以便它不返回符号,而是返回称为“镲片”的对象,这可能是某种嵌入式语言中符号的表示:

首先,我们需要一份 reedtable 的副本,我们需要从该副本中删除符号解析器(之前已使用 reedtable-token-parser-names 检查过它的名称)。

(defvar *cymbal-reedtable* (copy-reedtable nil))
(remove-token-parser 'symbol *cymbal-reedtable*)

现在是钹的实现:

(defvar *namespace* (make-hash-table :test #'equal))

(defstruct cymbal
  name)

(defgeneric ensure-cymbal (thing))

(defmethod ensure-cymbal ((thing string))
  (or (gethash thing *namespace*)
      (setf (gethash thing *namespace*)
            (make-cymbal :name thing))))

(defmethod ensure-cymbal ((thing cymbal))
  thing)

最后是钹令牌解析器:

(define-token-parser (cymbal 0 :denatured t :reedtable *cymbal-reedtable*)
    ((:sequence
      :start-anchor
      (:register (:greedy-repetition 0 nil :everything))
      :end-anchor)
     name)
  (ensure-cymbal name))

一个例子。修改reedtable之前:

> (with-input-from-string (in "(x y . z)")
    (reed :from in :reedtable *cymbal-reedtable*))
(x y . z)

之后:

> (with-input-from-string (in "(x y . z)")
    (reed :from in :reedtable *cymbal-reedtable*))
(#S(cymbal :name "x") #S(cymbal :name "y") . #S(cymbal :name "z"))

宏字符

如果某些东西不是标记的开始,那么它就是一个宏字符。宏字符有关联的函数,这些函数被调用来读取一个对象,但是它们选择这样做。默认的 reedtable 有两个半宏字符:

  • " 读取一个字符串,使用 reedtable 的单个和多个转义字符;
  • ( 读取列表或缺点。
  • ) 被定义为引发异常,因为它只有在存在不平衡的括号时才会发生。

字符串读取器非常简单(虽然代码不同,但它与令牌读取器有很多共同点)。

list/cons 阅读器有点繁琐:大部分繁琐都是通过一个有点恶心的技巧来处理 consing 点:它安装了一个秘密令牌解析器,如果动态变量,它会将 consing 点解析为特殊对象是真的,否则会引发异常。然后 cons 读取器适当地绑定此变量以确保仅在允许的地方解析 consing 点。显然,list/cons 阅读器在很多地方递归地调用了整个阅读器。

这就是所有的宏字符。因此,例如在默认设置中,' 会读作符号(或钹)。但是你可以只安装一个宏字符:

(defvar *qr-reedtable* (copy-reedtable nil))

(setf (reedtable-macro-character #\' *qr-reedtable*)
      (lambda (from quote table)
        (declare (ignore quote))
        (values `(quote,(reed :from from :reedtable table))
                (inch from nil))))

现在 'x 将在 (quote x) 中读作 *qr-reedtable*

同样,您可以在 # 上添加一个更复杂的宏字符,以按照 CL 的方式根据对象的下一个字符读取对象。

报价阅读器的示例。之前:

> (with-input-from-string (in "'(x y . z)")
    (reed :from in :reedtable *qr-reedtable*))
\'

它返回的对象是一个名称为 "'" 的符号,它当然没有读取超出该范围的内容。之后:

> (with-input-from-string (in "'(x y . z)")
    (reed :from in :reedtable *qr-reedtable*))
`(x y . z)

其他注意事项

一切都提前一个字符运行,所以所有不同的函数都会获取正在读取的流、他们应该感兴趣的第一个字符和 reedtable,并返回它们的值和下一个字符。这避免了无休止的未读字符(并且可能会告诉您它可以本地处理什么语法类(显然宏字符解析器可以做任何他们喜欢的事情,只要它们返回时一切正常)。

它可能不使用在非 Lisp 语言中无法适度实现的任何东西。一些

  • 宏会以通常的方式引起疼痛,但唯一的一种是 define-token-parser。我认为解决方案是通常的手动扩展宏并编写代码,但是您可能可以通过使用 install-or-replace-token-parser 函数来处理保留的簿记列表排序等
  • 您需要一种具有动态变量的语言来实现类似 cons reeder 的功能。
  • 它使用 CL-PPCRE 的 s-expression 正则表达式表示。我敢肯定其他语言也有类似的东西(Perl 有),因为没有人想编写字符串正则表达式:它们一定在几十年前就消失了。

这是一个玩具:读起来可能很有趣,但不适合任何严肃的用途。我在写这篇文章时发现了至少一个错误:还会有更多。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。