如何在优先级爬升算法中使用三元运算符?

22

我按照“优先级上升”部分在此网页中给出的解释,使用具有各种一元前缀和二元中缀运算符的优先级上升算法实现了算术求值器。我还想包括三元运算符(即三元条件运算符?:)。

网页上给出的算法使用以下语法:

E --> Exp(0) 
Exp(p) --> P {B Exp(q)} 
P --> U Exp(q) | "(" E ")" | v
B --> "+" | "-"  | "*" |"/" | "^" | "||" | "&&" | "="
U --> "-"

我该如何将三目运算符合并到这个语法中?


1
特别是在C/C++的上下文中,这尤其有趣,其中定义为“logical-OR-expression ? expression : conditional-expression”(C)或“logical-OR-expression ? expression : assignment-expression”(C++),条件和赋值表达式可以出现在“中间”和“右侧”表达式中。更糟糕的是,逗号表达式也可以出现在“中间”。我想知道为什么C和C++的定义不同。我将此标记为C,希望能更好地展示。 - Alexey Frunze
FYI。我有几个关于这个问题的讨论链接:123 - Alexey Frunze
3个回答

27
具体来说,我将以C/C++/Java的?:为例。
这些语言中,?:运算符允许在?:之间使用任何有效的表达式,包括由?:本身形成的表达式和那些由运算符形成,其优先级低于?:,例如=,(例如:a ? b = c : da ? b , c : da ? b ? c : d : e)。
这意味着您应该像解析表达式时处理()一样处理?:。当您解析出? expr :时,它作为一个整体是一个二进制运算符。因此,您可以以相同方式解析( expr )? expr :,但前者是一个表达式,而后者则是一个二进制运算符(其中嵌套了一个表达式)。
现在,? expr :是一个二进制运算符(a ?-expr-: b在“二进制性”方面与a * b没有区别),因此您应该能够像支持其他已支持的二进制运算符一样支持它。
个人而言,我不会费心将?:拆分为单独的二进制运算符。最终,它仍然是一个三元运算符,在表达式求值期间必须与3个操作数链接并作为一个整体考虑。如果您正在遵循您在问题中提到的文章中的方法,并创建AST,则不需要进行额外的操作,?:具有左子节点、右子节点(像其他二进制运算符一样),以及一个中间子节点。 ?-expr-:作为整体的优先级应该较低。在C/C++(和Java?)中,它的优先级不是最低的,但由您决定要将其设置为什么。
到目前为止,我们还没有涉及?:的结合性。在C/C++和Java中,?-expr-:与赋值运算符=一样是右结合的。同样,由您决定将其设置为左结合还是保持右结合。
还有这个:
E --> P {B P}
P --> v | "(" E ")" | U P
B --> "+" | "-" | "*" | "/" | "^"
U --> "-"

应该变成这样:

E --> P {B P}
P --> v | "(" E ")" | U P
B --> "+" | "-" | "*" | "/" | "^" | "?" E ":"
U --> "-"

终于有一个回答解决了我的问题!我想那就是我当时问这个问题时不知不觉中所做的事情。 - Matt
这对我来说是一个有趣的问题,因为我的C编译器目前不支持?:,我正在考虑添加对它的支持。当我在寻找答案时,我发现了你的这个问题,现在我也有了答案。 :) - Alexey Frunze
你在写自己的C编译器? - Matt

6
我在搜索关于将三元操作符转换为逆波兰表达式(RPN)的信息时遇到了这个问题,因此虽然我没有一个实现使用Precedence Climbing来实现?:运算符的坚实方法,但我认为我可以通过使用两个堆栈来给出一些将?:运算符转换为RPN的线索……这与您提供的网页中的Shunting Yard算法类似。
(编辑)我必须指出我在这里做的事情不是很高效(三元运算符的两个分支都必须被评估),但是为了演示如何在现有框架(RPN转换代码)中合并新运算符(?:)(通过声明具有两个最低优先级的:),我认为这很容易。
我想从一些示例开始,展示如何基于我的解析器将带有?:的表达式转换为RPN……我添加了两个运算符而不是只有一个,即:,但我认为这并没有太大的区别,因为总是会放在一起(RPN和原始表达式具有相同数量的标记只是更方便)。在下面的示例中,您可以看到它是如何工作的。
示例1:1 + ((0+1) ? 2 : 3 + 8) + 4 RPN:1 0 1 + 2 3 8 + : + 4 + 现在让我们来评估RPN。
1-将第一个元素推入堆栈,并遇到第一个二进制运算符: 1 0 1和运算符+,我们添加顶部2个元素,将堆栈变成1 1 2-然后再推入三个元素,并遇到第二个二进制运算符: 1 1 2 3 8 + ------> 1 1 2 11

3 - 现在我们有:?。这实际上告诉我们根据谓词(栈中第三个元素1)选择结论分支(栈中第二个顶部元素2)或可选分支(栈中顶部元素11)。由于谓词为1(真),我们选择2并丢弃11。解析器弹出3个元素(谓词/结论分支/可选分支)并推回所选的元素(在本例中是结论分支),因此堆栈变为

1 2

4- 消耗剩余的元素:

1 2 + ------> 3

3 4 + ------> 7


示例2: 1 + ((0+0+0?0:0) ? 2 : (3 + 8)) + 4

RPN:1 0 0 + 0 + 0 0 : ? 2 3 8 + : ? + 4 +

评估:

开始:

1 0 0 + 0 + 0 0 : ? 2 3 8 + : ? + 4 +

添加0到0后:

1 0 0 + 0 0 : ? 2 3 8 + : ? + 4 +

在将0加入0之后:

1 0 0 0 : ? 2 3 8 + : ? + 4 +

在第一个:?选择了0之后:

1 0 2 3 8 + : ? + 4 +

在添加3和8之后:

1 0 2 11 : ? + 4 +

?:选择了11之后:

1 11 + 4 +

在添加1和11之后:

12 4 +

最终结果为:

16


这可能表明可以将带有?:的表达式转换为逆波兰表示法。正如网页所说,RPN和AST是等效的,因为它们可以相互转换,三元运算符应该也可以用类似的方式使用优先级爬升实现。

需要做的一件事似乎是?:运算符的“优先级”(或优先级)。当我尝试进行RPN转换时,我确实遇到了这个问题。我已经赋予问号和冒号最低的优先级:

从上面的例子中可以看出,每当我们要执行?:时,precedence branchalternative branchpredicate都应该已经被计算出一个数字。这是由优先级保证的。

以下是优先级表格。

! ~ > * / % > + - > & > ^ > | > && > || > ? > :

在表达式中,?: 的优先级最低,它们不会取周围的操作数。

? 在逆波兰表达式中排在 : 前面,这只是一种设计选择,因为如果 :? 不必分成两个运算符,则可以不这样做。

希望这有所帮助..


引用文献:http://en.cppreference.com/w/cpp/language/operator_precedence


如果您在解析后添加对“立即”结构的支持,您可以像Forth一样使用普通的if/then/else结构来实现?:。这将与其他任何东西一样有效率,因为只有代码的一个部分(consequent或alternate)会执行。大多数关于移植或实现Forth的文本都会说明如何在底层完成此操作(一旦您看到它,它就非常简单)。 - Samuel A. Falvo II
不幸的是,这并没有考虑到大多数'?:'运算符的实现将短路,即:div==0 ? -1 : val/div不会导致除以零错误,因为当div=0时,第三个操作数(val/div)不会被评估。话虽如此,使用后缀(例如||或&&)进行任何短路都非常困难,唯一的解决方案是切换到前缀。 - David Ljung Madison Stellar

3
将冒号的优先级设定为低于问号。换句话说,a ? b : c将被解析为(a ? b) : c。这样做可以让解析器构建(if/then/empty-else)的抽象语法节点,然后由“:操作符”对其进行操作以提供所需的else组件。
优先关系还保持了运算符的可组合性。例如,a ? b : c ? d : e被解析为(a ? b) : (c ? d) : e,如人们所期望的那样。
希望这能帮到你。

1
实际上,a ? b : c ? d : e 是有歧义的。根据你的观察方式,你可能期望得到 ((a ? b : c) ? d : e) 或者 (a ? b : (c ? d : e))。我可以看到两种方式的论点,并且我很好奇 C 和其他语言会如何解析它。事实上,如果你要将它们分成两个独立的二元运算符,a ? (b : c) 的分组在数据类型方面更有意义。 - Matt
@Matt 1 ? 2 : 3 ? 4 : 5 在 Borland/Turbo C/C++、Open Watcom C/C++、gcc/g++ 和 clang 中的值为 2 - Alexey Frunze
@AlexeyFrunze 是的,在C/C++中它是明确定义的。如果你注意一下,我的问题并没有提到任何编程语言。 - Matt
三元运算符,我认为,源于布尔代数和证明中的 -> 运算符是“蕴含”关系。这个操作的代数符号是 a -> b : c,但由于 C 用 -> 表示指针解引用,因此 ? 成为了所使用的符号。我之所以提到这一点,是因为历史背景很重要;在证明中,a -> b : c -> d : e -> f : ... 同样是无歧义的。任何其他解释,例如,都会阻止多路分支被代数地书写。 - Samuel A. Falvo II

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