【问题标题】:Parsing complex mathematical expressions解析复杂的数学表达式
【发布时间】:2020-07-31 17:09:24
【问题描述】:

我目前正在编写(为了好玩)一门深受数学启发的编程语言。

运算符(及其优先级/关联性)在语言中定义,例如:

let factorial be an operator[int <- int!]
    with precedence of 2
    and left-to-right associativity;
let factorial(n) = ...;

let abs be an operator[number <- |number|]
    with precedence of 2
    and no associativity;
let abs(x) = ...;

let not be an operator[bool <- !bool]
   with precedence of 2
   and right-to-left associativity;

这适用于更常见的运算符(+-*/,...)。

注意:语法不是确定的

为了生成有用的 AST,我决定进行多个解析步骤。 第一步,表达式只是术语和符号的列表(支持括号分组),例如:

m! / (n! * (m - n)!)

将被解析为(这里是简化的 AST):

["m", "!", "/", ["n", "!", "*", ["m", "-", "n"], "!"]]

在第一步之后,我知道定义了哪些运算符,我知道它们的优先顺序以及它们的关联性。

我在执行第二步时遇到了困难,该步骤应该为表达式生成树(而不是列表)。

第一步是使用library 以EBNF 语法作为输入来实现的。 第二步,我试图在表达式中“找到”运算符的模式(使用自制的Parser Combinator):

let factorial be an operator[int <- int!] ...;
# here the pattern is ["int", "!"]

但是这种方法不尊重优先级和关联性。

如果有人有关于如何从这里开始的建议,或者有关该主题的论文的链接,我将非常欢迎,因为我已经没有毛可拉了 :)

【问题讨论】:

    标签: parsing math


    【解决方案1】:

    您可以使用运算符优先级解析器,例如臭名昭​​著的Shunting Yard Algorithm。 (This answer 也可能有帮助。也可能没有。)

    调车码算法经常被介绍(包括在维基百科的那篇文章中)作为“将中缀转换为反向抛光”的一种方式。它不是。它是一种解析算法,它可以轻松生成 AST,或三地址代码,或您想用它生成的任何其他代码。但我同意 AST 是最好的选择。

    要将 Wikipedia 上的算法修改为生成语法树的算法,请执行以下操作:

    • 用一堆树节点替换他们所谓的“输出队列”。
    • 当他们“将值推入输出队列”时,创建该值的 AST 节点(最有可能是文字或标识符),并将其推入堆栈。}
    • 当他们“将运算符推入输出队列”时:堆栈顶部的节点是运算符语法节点的子节点。所以弹出它们(记住堆栈顶部的那个是最右边的参数,对于接受多个操作数的运算符)并将它们组合成一个新的运算符节点。然后将此节点推回堆栈。
    • 在解析结束时,堆栈将只包含一个元素(如果所有运算符都与正确数量的参数一起使用),它是整个表达式的 AST 节点。

    许多允许在源代码中定义运算符语法的语言都使用这种算法。

    我自己已经完成了其中的一些,让我提供一些您可以忽略的建议:

    1. 虽然如今 Unicode 提供了使用各种单一“字符”作为数学运算符的可能性,但现实情况是 ⊛ 并不是那么容易键入,而且坚持使用它来执行某些功能的库可能不是所有这些都受用户欢迎。当然,您可以尝试编写自己的跨平台支持 Unicode 的 IDE,但在我看来,这就像一个非常深且令人费解的兔子洞,一个曲折的迷宫一样的迷宫。因此,您很可能希望将其输入为(*) 之类的内容。但是,您会遇到一个问题,即在您阅读运算符声明之前,您不知道令牌是什么。没有真正令人满意的解决方案(直到有人编写上述 IDE),而且我见过的大多数允许自定义运算符的语言在指定了一些可用作运算符字符的字符集后坚持用空格分隔它们.

    2. 任何用 C 或 C++ 编写过按位表达式的人都知道,有很多不同的运算符优先级并不是那么用户友好。我们大多数人都没有直觉来指导我们理解x + 2 &lt;&lt; n ^ 1 | 7 是如何被解析的,而且大多数风格指南都要求你(合理地,恕我直言)添加多余的括号,即使你确切地知道该表达式的含义,因为人们阅读您的代码可能不会。 ((((x + 2) &lt;&lt; n) ^ 1) | 7))。当运算符刚刚ad hoc 添加到语法中时,直觉可能就更没用了。但是,虽然只有少数人可能会猜测 xor 介于 andor 之间,或者除了 ~ 之外的所有位运算符都比布尔值绑定得更紧密,但通常可以理解为乘法运算符的优先级高于加法运算符在同一域中

      无论如何,我想说的一点是,像“⋇”这样的声明具有优先级 5,虽然在定义 ⋇ 的库中完全合理,但不能很好地组合,因为“5”是一个完全任意的数字。也许有一个用于不同域的不同库,它声明“⋈ 具有优先级 3”。这两个库的作者可能从来没有一起决定 ⋇ 和 ⋈ 的相对优先级是什么,一个使用 5 而另一个使用 3 的事实并不表明这些运算符中的一个“旨在”比另一个。就个人而言,我更喜欢在同一模块中相对于其他运算符定义运算符优先级的样式(“∷ 与 ⋕ 具有相同的优先级,比 ⊹ 具有更高的优先级”。)从这些声明中,您可以构建一个偏序,这就足够了用于运算符优先级解析;如果解析器发现两个没有声明的优先顺序的无括号运算符,它应该抱怨而不是仅仅使用任何上下文之外的任意数字之间的比较。但这只是我的意见。)

    【讨论】:

    • 感谢您的回答!对于您的第一点,我不打算使用 unicode,我喜欢坚持您可以在键盘上轻松输入的内容(我不想制作 APL 2.0 并提供键盘)。至于您的第二点,这是 Perl 6 方式(perlgeek.de/blog-en/perl-5-to-6/13-custom-operators.html),实际上它更具可读性!使用括号来提高可读性应该是开发人员关心的问题,恕我直言,该语言仍然需要可预测的规则(即使是复杂的规则)。
    • @linkdd:是的,像multi sub infix:&lt;foo&gt; is tighter(&amp;infix:&lt;+&gt;) 这样的相对优先级声明本质上是我所提倡的。这肯定比使用任意数字作为优先级要好。但是,至少来自 Raku 文档的 afaics,运算符优先级几乎是完全排序(非关联优先级除外)。我的方式要严格得多(但在实践中,效果很好)。我在文档中没有找到任何解释当你的定义不明确时会发生什么的东西,这似乎是可能的。 (例如,后缀运算符也是中缀)。
    • 可以检测到歧义,所以至少可以想象会产生错误信息。我没有尝试安装 Raku 来找出答案。无论如何,祝你的项目好运。
    猜你喜欢
    • 2011-02-25
    • 1970-01-01
    • 1970-01-01
    • 2016-04-07
    • 1970-01-01
    • 2015-01-10
    • 1970-01-01
    相关资源
    最近更新 更多