【问题标题】:Parser Combinators, separating grammar and AST construction解析器组合器,分离语法和 AST 构造
【发布时间】:2015-10-31 10:14:32
【问题描述】:

我正在使用解析器组合器库在 Scala 中编写一种简单的函数式编程语言。

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

有一件事我无法通过实现解决:如何将语法定义与转换为 AST 节点分开?

如果将一个接近人类可读的语法直接作为解析器源,那真是太酷了,特别是考虑到我是项目 ATM 上唯一的程序员,它可以用作文档。

如何分离语法和 AST 特定的代码?

【问题讨论】:

    标签: scala grammar abstract-syntax-tree parser-combinators


    【解决方案1】:

    这是一个很好的问题,在想出一个我认为对我来说非常有效的解决方案之前,我一直在努力解决这个问题。

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

    • 具体语法树或 CST:这是文本的树形形式,与文本具有 1:1 对应关系。文本中出现的所有内容也将出现在 CST 中。

    • 抽象语法树或 AST:这不一定与文本有 1:1 的对应关系,因为不必要的文本细节(如大括号、标点符号等)已被删除并且不会出现在 AST 中。

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

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

    2. CST -> AST 这是我按摩解析树的地方,提取重要的东西,忽略其余的。这也是我经常执行上下文相关检查的地方(例如:检查函数定义没有重复的参数名称),将这些细节排除在实际解析阶段之外。


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

    【讨论】:

    • 这听起来和我想要的完全一样,但我不确定你是如何完成的 1. 你有代码示例,或者你能澄清如何从问题中的链接更改我的代码吗?跨度>
    • @Felix -- 我添加了一个示例(不幸的是,它使用的是 Javascript 而不是 Scala,但原理是一样的)。请让我知道是否需要进一步解释——我很乐意这样做!
    • @Felix -- 为了在您的代码中执行此操作,我建议 1. 从解析器中删除像 ^^ { case id ~ _ ~ term => Abstr(id, term) } 这样的操作,2. 创建一个用于将 CST 转换为 AST 的新模块。
    • 我不需要明确类型来实际获取 PackratParsers 吗?
    • @Felix 这是一个很好的观点,而且我还没有考虑过。我需要再考虑一下——感谢您指出!
    【解决方案2】:

    嗯,原则上,你所有的 AST 转换都有一个特定的类型。你可以在别处定义它们,并从你的语法定义中使用它们。它会在某种程度上澄清一些事情。或者,您可以将语法定义定义为在调用时评估的“按名称传递”函数,然后从您的转换中使用它们。

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

    【讨论】:

    • 然而,此解决方案强制语法定义明确支持不同的 AST 转换。您是否认为可以做类似的事情,同时提供没有任何转换的语法,即所有解析器都应该只是 Parser[String]?
    • 取决于什么引用什么,正如我所说。在另一个方向,没有这种依赖。即使在第一个方向上,如果您能够保留泛型类型,您可以使用定义转换而不指定它们的特征(还)。虽然不是该领域的专家,但上次我做这种事情时,我使用LEX & YACC 将这些部分分开。另一方面,您将不得不使用标准的 Scala 语言功能......
    【解决方案3】:

    拥有一个接近人类可读的语法直接“构建”解析器源真的很酷......

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

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

    您拥有的是一个手写的 Packrat 解析器。

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

    所以,语法可以是 EBNF 或 PEG 或 CFG 或“你自己的”语法,对吧?

    无论如何...

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

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

    • 用该解析器结果解析该语法是该语法的内部结构:语法树。

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

      foreach grammar key add matching grammar rule

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

    • 最后:您有一个由各个“语法规则”组装而成的“构造解析器”,能够解析由给定语法定义的 Source。

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

    通过 1

    Grammar -> GrammarParser -> GrammarTree -> GrammarRules -> ConstructedParserForGrammar

    通过 2

    Source -> ConstructedParserForGrammar -> Syntax Tree -> Transformations...

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

    【讨论】:

      【解决方案4】:

      自从这次提交

      https://github.com/scala/scala-parser-combinators/commit/33792d3380791ddafb41760604857d2fc43e54e1

      解析器组合器链接到可以准确解决我的问题的帖子。恕我直言,这是对我问题的最准确答案。

      https://enear.github.io/2016/03/31/parser-combinators/ 此处的帖子首先将词法分析为具体的语法树(词法分析),然后生成 AST。

      我把它留在这里是因为它为接受的答案添加了一个示例。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-05-03
        • 1970-01-01
        • 2021-03-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多