【问题标题】:Parsing with user-defined operator precedence使用用户定义的运算符优先级进行解析
【发布时间】:2015-05-24 23:31:30
【问题描述】:

好的,所以这里有一个问题:鉴于 Haskell 允许您定义具有任意运算符优先级的新运算符...如何实际解析 Haskell 源代码?

在解析源代码之前,您无法知道设置了哪些运算符优先级。但是在您知道正确的运算符优先级之前,您无法解析源代码。所以……嗯,怎么样?

例如,考虑表达式

x *** y +++ z

在我们完成对模块的解析之前,我们不知道导入了哪些其他模块,因此不知道哪些运算符(和其他标识符)可能在范围内。我们当然还不知道它们的优先级。但是解析器必须返回 something... 但它应该返回

(x *** y) +++ z

还是应该返回

x *** (y +++ z)

可怜的解析器无法知道。这只能在您找到将(+++)(***) 带入范围的导入、从磁盘加载该文件并发现运算符优先级后才能确定。很明显,解析器本身不会做所有的 I/O。解析器只是将字符流转换为 AST。

显然,某个地方的某个人已经想出了如何做到这一点。但我无法解决...有什么提示吗?

【问题讨论】:

  • 你可以构建一个包含两个以上孩子的 AST。假设这个特定节点作为子节点获得列表[x, ***, y, +++, z],然后检查优先级并构建一个二进制节点以在之后替换自己。 (可能有更好的方法)。
  • 请注意,您也可以很容易地做到这一点,无需任何技巧,只需进行两次解析,一次获取运算符的固定性和优先级,另一次实际解析源代码。

标签: parsing haskell syntax


【解决方案1】:

为解析器引用page on GHC trac

中缀运算符被解析为好像它们都是左结合的。这 renamer 使用固定性声明来重新关联语法树。

【讨论】:

  • 天哪,太可怕了! o_O
  • 那么“重新关联语法树”基本上相当于一系列的树轮转?
  • 嗯,是的。您可以查看重命名器的来源(例如this part)并亲自查看。
  • @MathematicalOrchid 确实,这有点吓人,但至少很简单。我不敢窥视 Agda 如何处理 mixfix 表示法 (use_and_now : A->B->C),或者 Coq 如何处理任意用户定义的 Notation(甚至可以包括活页夹!粗略地说,Notation "match e with Left x->ex ; Right y->ey" := either (\x->ex) (\y->ey) e)或者 Maude 如何允许定义@987654327 @ 是 并列运算符(以便 x y zx,y,z 是您的自定义类型时使用这样的op)。
  • 有一个 paper 关于 Agda 的 mixfix 表示法。
【解决方案2】:

András Kovács 的回答说明了 GHC 中真正做了什么,但这有一些历史。

实际上从 Haskell 98 到 Haskell 2010 标准有一些假设性的变化。在前者的 BNF 语法中,运算符固定性和解析以这样一种方式交织在一起,理论上您可以在固定性规则与表达式和缩进块结束时的规则之间产生一些非常奇怪的相互作用。 (对于后两者,规则本质上是“继续前进,直到你不得不停下来”。)

特别是,您可以重新定义本地运算符及其固定性,以便它的使用完全属于重新定义内部 where 块......而它没有。所以你遇到了解析器悖论。我找不到任何旧的例子,但这可能是一个:

let (+) = (Prelude.+)
    infix 9 + -- make the inner + high precedence and non-associative
in 2 + 3 + 4
--       ^ this + cannot parse here as the inner operator, which means
--         the let ... in ... expression should end automatically first,
--         but then it's the standard +, and its fixity says it should parse
--         as part of the inner expression...

在 Haskell 2010 中,他们正式更改了这一点,以便在正确解析之后在单独的阶段确定运算符的固定性。

那么为什么这是一个假设的变化?因为所有编译器编写者都已经按照 Haskell 2010 的方式进行了操作,并且出于自身的理智一直这样做。

【讨论】:

  • Haskell 98 Report §9.3 说:“……表达式do a == b == c 有一个明确的……解析,即(do { a == b }) == c,因为(==) 是非关联的”——因此在@ 之后会导致错误987654327@,由于布局规则而插入右大括号。这意味着像您的示例 这样的本地固定性声明实际上就像您描述的那样模棱两可/自相矛盾。他们很有帮助地提供“因此建议程序员避免编写需要解析器在这种情况下插入右大括号的代码”。 ;)
【解决方案3】:

总结到目前为止的 cmets,似乎可能性如下:

  • 返回一个解析树,其中任何中缀运算符都保留为某种“列表”结构,然后在知道优先级后重新排列。
  • 假设您知道运算符的优先级,然后在事后重新排列解析树。
  • 执行第一次解析,仅读取导入和固定性声明,加载导入,然后执行具有已知优先级的完整解析。

【讨论】:

  • 当您像我的示例中那样对运算符进行本地重新定义时,最后一个选项有点棘手。
猜你喜欢
  • 2015-07-09
  • 2018-04-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-09
  • 1970-01-01
  • 2011-06-13
相关资源
最近更新 更多