解析器组合器,将语法和AST构建分离

22

我正在使用解析器组合库(parser-combinators library)在Scala中编写一个简单的函数式程序语言。

语法规范在这里: https://github.com/hejfelix/Frase/blob/master/src/main/scala/it/vigtig/lambda/ParserLike.scala

有一件事情我一直无法用实现解决:如何将语法定义与AST节点转换分离?

如果能够在解析器源码中直接使用接近人类可读的语法规范,那将非常酷,特别是考虑到目前我是该项目上唯一的程序员,因此它可以作为文档使用。

我应该如何分离语法和AST特定代码?

4个回答

11

这是一个很棒的问题,我在想出一种适合自己的解决方案之前曾经苦苦思索过很长时间。

在构建解析器时,我使用两个不同的语法树:

  • 一个具体语法树(Concrete Syntax Tree,CST):这是文本的树形表示形式,并且与文本具有1:1对应关系。所有出现在文本中的内容也会出现在CST中。

  • 一个抽象语法树(Abstract Syntax Tree,AST):这个树形结构不一定与文本存在1:1的对应关系,因为无关的文本细节(如括号、标点符号等)已被删除并且不会出现在AST中。

因此,从输入文本到AST需要两个步骤:第一步是将输入字符串解析成CST;第二步是将CST转换为AST,丢弃不必要的细节。

  1. String -> CST 这是我使用解析组合器的地方。在这个阶段,我不进行任何树形结构上的操作。 CST的结构完全由使用的组合器决定。每个组合器生成一个特定形状的子树,在这个阶段我不会改变这些子树的形状。没有任何附加在组合器上的操作,因此语法定义干净且没有任何AST信息。

  2. CST -> AST 这是我对解析树进行加工的地方,提取重要信息并忽略其余细节。这也是我通常执行上下文敏感检查的地方(例如:检查函数定义是否具有重复的参数名称),将这些细节保持在实际解析阶段之外。


示例:以下是我使用这种方法构建的JSON解析器:

  • 用于构建CST节点的组合器

  • 实际的JSON解析器,用于构建CST


  • 这听起来正是我想要的,但我不确定你是如何完成1的。你有代码示例吗?或者你能澄清一下你会如何修改我在问题链接中提供的代码吗? - Felix
    @Felix -- 我添加了一个示例(不幸的是它是用Javascript而不是Scala编写的,但原理是相同的)。如果需要我解释更多,请告诉我! - Matt Fenwick
    @Felix -- 为了在您的代码中实现此功能,我建议 1. 从解析器中删除 ^^ { case id ~ _ ~ term => Abstr(id, term) } 这样的操作, 2. 创建一个将 CST 转换为 AST 的新模块。 - Matt Fenwick
    我需要明确指定类型才能获得PackratParsers吗? - Felix
    @Felix,你说得很好,这是我还没有考虑过的。我需要再想一想——谢谢你指出来! - Matt Fenwick

    2

    原则上,所有AST转换都有特定的类型。您可以在其他地方定义它们并从语法定义中使用它们。这样会使事情更加清晰。或者,您可以将语法定义定义为“按名称传递”的函数,在调用时进行评估,然后从转换中使用它们。

    基本上,任何语言都允许您通过在某个地方定义事物并在任何其他地方引用它们来打破复杂性。由于Scala允许您将函数作为值使用,因此这更容易实现。


    然而,这种解决方案强制语法定义明确支持不同的AST转换。您认为是否可能在提供没有任何转换的语法的同时执行类似的操作,即所有解析器都应该只是Parser [String]? - Felix
    取决于引用关系,正如我所说的。在另一个方向上,没有这种依赖关系。即使在第一个方向上,如果您能够保持通用类型,可以使用定义变换但尚未指定它们的特征。虽然不是该领域的专家,但上一次我做了类似的事情时,我使用了[LEX&YACC](http://dinosaur.compilertools.net/)来将这些部分分开。另一方面,您将不得不使用标准Scala语言功能... - Daniel Langdon

    1

    有一个近乎人类可读的语法直接"构建"解析器源代码将会非常酷...

    我想知道那个“近乎人类可读的语法”是什么?

    我如何将语法定义与转换为AST节点分开?

    你手写了一个Packrat Parser。

    我可能错了,但我理解这个问题是要求使用独立的语法定义来构建解析器。然后使用该解析器获取已解析源的语法树。

    因此,语法可以是EBNF或PEG或CFG或“你自己”的语法,对吗?

    无论如何...

    • 首先,让我们从“独立的语法定义”开始,例如EBNF。

    • 然后你需要一个解析器来解析语法,例如一个EBNF解析器。

    • 使用该解析器解析语法将生成该语法的内部结构:语法树。

    • 给定有效语法的语法树,您可以返回一个关联列表,其中包含键(作为元标识符)并将语法规则附加到它们上面。

      对于每个语法键,添加匹配的语法规则。

    • 这意味着您需要选择由RuleName标识的语法规则,并将其规则添加到“构造解析器”中。

    • 最终结果是:您拥有一个“构造解析器”,由单个“语法规则”组成,能够解析由给定语法定义的源。

    • 解析源代码,将为您提供源的语法树。

    第一步

    语法 -> 语法解析器 -> 语法树 -> 语法规则 -> 用于语法的构造解析器

    第二步

    源代码 -> 语法分析器构建 -> 语法树 -> 转换...

    换句话说:从BNF到自动构建的Packrat解析器是一个相当复杂的难题。


    0

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接