【问题标题】:Correct way of parsing S-expressions in OOP在 OOP 中解析 S 表达式的正确方法
【发布时间】:2012-02-24 17:38:57
【问题描述】:

我正在寻找一种方法来实现 S 表达式阅读器(稍后将与 Scheme 解释器和编译器一起使用),但我一直在问自己应该如何(如果有的话)编写 AST为它。

我一直在阅读 SICP,这在 Scheme 中非常简单,但我希望以 OO 方式在 C++ 中实现解释器和编译器。

请记住,我这样做只是为了学习目的,所以我并不是在寻找最简单或最快的方法,而是在寻找正确且可重复使用的方法。

我在一些 Scheme 实现中看到人们解析 s 表达式并轻松输出 cons 单元格,如下所示:

  struct Sexpr
  {
  };

  struct Cons : public Sexpr
  {
    Sexpr* left;
    Sexpr* right;
  };

  struct IntAtom : Sexpr
  {
    int value;
  };

对于每一种 Scheme Atom 或类似的东西都有一个 Sexpr 子类。

我不确定,但这对我来说似乎是一个 hack……这项工作不应该由口译员而不是读者来完成吗?

我想知道的是,这是否被认为是阅读 S 表达式的最佳(或正确)方式,或者这更像是解释器而不是解析器的工作?解析器是否应该有自己的 AST 而不是依赖 cons 单元?

【问题讨论】:

  • 如果我没看错,这个问题与解析没有太大关系。相反,我认为您在问:对于 s-expression 数据类型,最合适的表示是什么。你同意吗?
  • @dyoo 是也不是。是的,你是对的,我正在寻找最适合 s 表达式的表示。不,你错了,这个问题显然与解析有关。如果我只是为 sexpr 寻找最合适的表示,那么毫无疑问它将是 cons 细胞。但是,我正在寻找最合适的 sexpr 表示,特别是用于解析
  • 酷。很好的澄清。然后:可能区分解析任务的一件事是需要源位置信息。普通的 cons 细胞不记得它们在原始来源中的来源。在解析期间,您可能希望支持可以指向源的错误消息。我们还需要什么其他东西来解析?
  • @dyoo 不是这样。我在这里要区分的是cons cells 是一个运行时结构。它是语言的语义部分,而不是句法部分。由于我希望完全分离编译/解释阶段,我不希望我的解析器处理语义内容。
  • @dyoo 虽然我应该补充一点,对于 Lisps 来说,将它们分开的线似乎很模糊,所以我可能推得太多了。

标签: c++ oop scheme abstract-syntax-tree s-expression


【解决方案1】:

从围栏的计划/球拍一侧跟进:

Racket(和其他一些 Scheme 实现)对语法对象使用更丰富的表示,因此它们可以附加属性来指示(至少在 Racket 中)它们绑定在什么上下文中,它们来自什么源位置,编译器的哪些通道插入了它们,以及您可能想要存储的任何其他信息(参见 Racket 中的“语法属性”)。

这些附加信息支持诸如带有源指针的错误消息和卫生宏之类的功能。

请注意,我这里的“更丰富”只是“包含更多价值”的意思,而不是任何非价值中立的方式。

我还应该补充一点——在陷入图灵焦油坑之前——你也可以使用旁边的表格来表示这个完全相同的信息;假设您有指针比较,将值放入结构中和使用表将结构与值相关联之间没有表现力差异。

【讨论】:

    【解决方案2】:

    如果你想在语法上有点完整,你需要支持

    sexpr ::= atom | sexpr sexpr
    atom ::= nil | intatom | etc.
    

    但这比你会遇到的大多数性别更普遍。在 LISP/Scheme 中,最简单和最常见的 S-expr 形式类似于 (a b c d),其中 a、b、c、d 中的每一个都是原子或列表。在对形式中,这是 [a [b [c [d nil] ] ] ],这意味着您的 conses 的所有右侧都是列表。

    所以如果你想要干净,你可能会这样做

    class sexpr {};
    class atom : sexpr {};
    class s_list : forward_list<smart_ptr<sexpr>> {};
    

    【讨论】:

    • 没问题!这与我的想法一致,但我有两个问题:1)这是否意味着我现在不应该担心cons cells(解析器)并将其作为解释器的问题,并且如您所展示的那样使用这种AST? 2) 我可能弄错了,但 s_list 不应该从 Sexpr 继承吗?
    【解决方案3】:

    在我看来,虽然人们可能会就“正确”方法是什么来回争论,但您建议的方法是使用相同的数据结构进行读取、编译、评估、处理——它最能教你什么是 Lisp 和“代码就是数据”的口头禅,尤其是 quote 运算符的实际含义(这是一件非常深刻的事情)。

    顺便说一句,这也是大多数 Lisps(有趣的是,不包括 Scheme)传统上的工作方式。

    所以是的,让阅读器生成 Lisp 数据:conses、符号、Lisp 数字、字符串等等,与用户级 Lisp 代码将处理的内容完全相同。它将使实现的其余部分变得更简单和更有指导意义。

    【讨论】:

    • 感谢您的回答!我的主要目标是全面了解编译器和解释器,不一定是 Lisps,这就是为什么我想知道最正确的方法。我选择了 Scheme,因为我认为它是一种相当简单的语言(至少是它的一部分),我已经使用过它,最后但并非最不重要的一点是,我也喜欢它。 :) 你的最后一行是什么意思?
    • @ivanmp 我的意思是,如果你想了解 Lisp,那么这样做会更有指导性。是的,instructive 是我要找的词。 :) 实现也会更简单,因为您不必为不同阶段处理不同的数据结构,并且一些数据可以简单地“通过”阶段而无需将它们转换为其他任何东西(如quote)。另一方面,如果您想学习一般的编译知识,而不是专门学习 Lisp,那么任何一种方式都可能很好。
    • 我的意思是这句话:顺便说一句,这也是大多数 Lisps(有趣的是,不包括 Scheme)传统上的工作方式。
    • @ivanmp 啊,是的,这确实是编辑前的最后一行。 :) 是的,我认为有趣的是,虽然大多数 Lisps 的语义是在 data(即 conses 和 atom)上定义的,并且读取步骤与编译完全分开,但 Scheme 的语义是在代码的文本表示。 (参见 R5RS,尤其是 “Syntax” section,它清楚地区分了代码语法和数据语法。)
    • @ivanmp 为了说明区别,将Scheme理解为“代码”的内容与meaning of the term in Common Lisp进行对比。
    【解决方案4】:

    您可能想查看this c/c++ s-expr parser library 的示例,了解它是如何完成的。

    看起来基本表示是:

    struct elt {
      int type;
      char *val;
      struct elt *list; 
      struct elt *next;
    };
    

    我引用他们的文档:

    由于元素可以是列表或原子,因此元素结构有一个类型指示符,可以是 LIST 或 VALUE。如果类型指示符是 LIST,则结构成员“list”将是指向此元素表示的列表头部的指针。如果类型指示符为 VALUE,则结构成员“val”将包含由元素表示的原子作为字符串。在这两种情况下,“next”指针都将指向当前 s 表达式的下一个元素。

    此外,here 是 s-expr 阅读器的其他实现的完整列表,其中包含可能感兴趣的多种语言。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-02-02
      • 1970-01-01
      • 1970-01-01
      • 2017-11-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多