1

在 C++ 中,一个著名的解析歧义发生在如下代码中

x<T> a;

如果T是一个类型,它就是它的样子(a类型变量的声明x<T>,否则它是(x < T) > a<>比较运算符,不是尖括号)。

事实上,我们可以进行更改以使其变得明确:我们可以使<>非关联。所以x < T > a,如果没有括号,即使x,T并且a都是变量名,也不会是一个有效的句子。

如何解决孟希尔的这场冲突?乍一看,我们似乎做不到。即使进行了上述修改,我们也需要在看到另一个 close 之前先查看不确定数量的标记>,并得出结论它是模板实例化,否则,得出结论它是一个表达式。Menhir 有什么方法可以实现这种任意的前瞻吗?

4

2 回答 2

3

不同的语言(包括你的标题中列出的那些)实际上对模板/泛型有非常不同的规则(比如可以有什么类型的参数,模板/泛型可以出现在哪里,什么时候允许它们有一个明确的参数列表以及什么泛型方法上的模板/类型参数的语法是),这会强烈影响您用于解析的选项。在我所知道的任何语言中, 的含义都x<T> a;取决于是否T是类型。

那么让我们来看看 C++、Java、Rust 和 C# 语言:

在所有这四种语言中,类型和函数/方法都可以是模板/通用的。因此,我们不仅要担心变量声明的歧义,还要担心函数/方法调用:是f<T>(x)带有显式模板/类型参数的函数/方法调用,还是带有括号的最后一个操作数的两个关系运算符?在所有四种语言中,当可以推断出模板/泛型函数/方法时,可以在没有模板/类型的情况下调用,但这种推断并不总是可能的,因此不允许函数/方法调用的显式模板/类型参数不是一种选择。

即使一种语言不允许将关系运算符链接起来,我们也会在这样的表达式中产生歧义:f(a<b, c, d>(e)). 这是f使用三个参数调用a<bc还是d>e使用单个参数调用以类型/模板参数a<b, c, d>(e)命名的函数/方法?ab,c,d

现在除了这个共同的基础之外,这些语言之间的其他大部分内容都不同:

在 Rust 中,变量声明的语法是let variableName: type = expr;, 所以x<T> a;不可能是变量声明,因为它根本不匹配语法。此外,它也不是一个有效的表达式语句(不再),因为比较运算符不能被链接(不再)。

所以这里没有歧义,甚至没有解析困难。但是函数调用呢?对于函数调用,Rust 通过简单地选择不同的语法来提供类型参数来避免歧义:而不是f<T>(x)语法 is f::<T>(x)。由于函数调用的类型参数在可以推断时是可选的,因此幸运的是,这种丑陋并不经常需要。

所以总结一下:let a: x<T> = ...;是一个变量声明,用三个参数f(a<b, c, d>(e));调用和用三个类型参数调用。解析很容易,因为所有这些都足够不同,只需一个前瞻标记即可区分。ff(a::<b, c, d>(e));a

爪哇

在Javax<T> a;中实际上是一个有效的变量声明,但它不是一个有效的表达式语句。原因是 Java 的语法有一个专用的非终结符,用于可以作为表达式语句出现的表达式,并且关系运算符(或任何其他非赋值运算符)的应用程序与该非终结符不匹配。赋值是,但赋值表达式的左侧同样受到限制。=事实上,如果下一个标记是 a 、或.,则标识符只能是表达式语句的开始。所以一个标识符后跟一个只能是变量声明的开始,这意味着我们只需要一个前瞻标记来解析它。[(<

请注意,在访问泛型类的静态成员时,您可以而且必须引用不带类型参数的类(即,FooClass.bar();而不是FooClass<T>.bar()),因此即使在这种情况下,类名后面也会跟着 a .,而不是 a <

但是泛型方法调用呢?像这样y = f<T>(x);的东西仍然可能会遇到歧义,因为关系运算符当然可以在=. 在这里,Java 通过简单地更改泛型方法调用的语法来选择与 Rust 类似的解决方案。代替object.f<T>(x)语法的是object.<T>f(x)部分object.是非可选的,即使对象是this. 因此,要在当前对象上调用带有显式类型参数的泛型方法,您必须this.<T>f(x);编写f(x);.

所以总的来说x<T> a;是一个变量声明,不能有以关系操作开头的表达式语句;一般来说,表达式this.<T>f(x)是一个泛型方法调用并且f<T>(x);是一个比较(嗯,实际上是一个类型错误)。同样,解析很容易。

C#

C# 对表达式语句的限制与 Java 相同,因此变量声明不是问题,但与前两种语言不同,它确实允许f<T>(x)作为函数调用的语法。为了避免歧义,关系运算符在以一种也可以是通用函数的有效调用的方式使用时需要加上括号。所以表达式f<T>(x)是一个方法调用,您需要添加括号f<(T>(x))(f<T)>(x)进行比较(尽管实际上这些将是类型错误,因为您无法将布尔值与<or进行比较>,但解析器并不关心)和类似地f(a<b, c, d>(e))调用以a类型参数命名的泛型方法,b,c,df((a<b), c, (d<e))将涉及两次比较(实际上您可以省略两对括号中的一对)。

这使得带有显式类型参数的方法调用的语法比前两种语言更好,但是解析变得有点棘手。考虑到在上面的例子中,f(a<b, c, d>(e))我们实际上可以在前面放置任意数量的参数,d>(e)并且如果后面没有 d>(e)a<b是一个完全有效的比较,我们实际上需要任意数量的前瞻、回溯或非确定性来解析它。

所以总而言之x<T> a;是一个变量声明,没有以比较开头的表达式语句,f<T>(x)是一个方法调用表达式,(f<T)>(x)或者f<(T>(x))是(错误类型的)比较。用 menhir 解析 C# 是不可能的。

C++

在 C++a < b;中是一个有效的(尽管没用的)表达式语句,带有显式模板参数的模板函数调用的语法是f<T>(x)并且a<b>c可以是一个完全有效的(甚至是类型良好的)比较。因此,如果没有附加信息,语句 likea<b>c;和表达式 likea<b>(c)实际上是模棱两可的。此外,C++ 中的模板参数不必是类型。也就是说,Foo<42> x;甚至Foo<c> x;wherec定义为const int x = 42;,例如,Foo如果Foo定义为将整数作为模板参数,则可能是模板的完全有效的实例化。所以这是一个无赖。

为了解决这种歧义,C++ 语法引用规则template-name而不是identifier在需要模板名称的地方。因此,如果我们将它们视为不同的实体,这里就不会有歧义了。但是当然template-name就像template-name: identifier在语法中一样简单地定义,所以这似乎毫无用处,......除了标准还说template-name只有当给定的标识符命名当前范围内的模板时才应该匹配。同样,它说标识符只应在引用模板(或类型名称)时被解释为变量名称。

请注意,与前三种语言不同,C++ 要求在使用之前声明所有类型和模板。因此,当我们看到 statement 时a<b>c;,我们知道它只能是模板实例化,前提是我们之前已经解析了名为模板的声明a并且它当前在范围内。

因此,如果我们在解析时跟踪范围,我们可以简单地使用 if 语句来检查名称是否a引用了以前解析过的模板,或者不是在手写解析器中。在允许语义谓词的解析器生成器中,我们可以做同样的事情。这样做甚至不需要任何前瞻或回溯。

但是像 yacc 或 menhir 这样不支持语义谓词的解析器生成器呢?对于这些,我们可以使用被称为 lexer hack 的东西,这意味着我们让词法分析器为类型名称、模板名称和普通标识符生成不同的标记。然后我们有一个非常明确的语法,我们可以提供给我们的解析器生成器。当然,诀窍是让词法分析器真正做到这一点。为了实现这一点,我们需要使用符号表跟踪当前在范围内的模板和类型,然后从词法分析器访问该符号表。当我们读取定义的名称时,我们还需要告诉词法分析器,比如xin int x;,因为这样我们想要生成一个常规标识符,即使一个名为的模板x当前在范围内(定义int x;将隐藏模板,直到变量超出范围)。

相同的方法用于解决C 和 C++ 中的转换歧义(是类型转换或名为 ?(T)(x)x函数T的函数调用)。T

总而言之,当foo<T> a;foo<T>(x)仅当foo是模板时,模板实例化。解析是个婊子,但在应用 lexer hack 时无需任意前瞻或回溯甚至使用 menhir 即可。

于 2020-04-13T14:19:43.713 回答
3

AFAIK C++ 的模板语法是现实世界非 LR语法的著名示例。严格来说,它不是任何有限 k 的 LR(k) ......所以 C++ 解析器通常是用 hacks (如 clang)手工编写或由 GLR 语法(带分支的 LR)生成的。所以理论上不可能在Menhir中实现一个完整的C++解析器,也就是LR。

然而,即使是相同的泛型语法也可能不同。如果涉及比较运算符的泛型类型和表达式从未出现在相同的上下文中,则语法可能仍然是 LR 兼容的。例如,考虑变量声明的 rust 语法(仅适用于本部分):

let x : Vec<T> = ...

标记表示后面:是一个类型,而不是一个表达式,所以在这种情况下,语法可以是 LR,甚至是 LL(未验证)。

所以最终的答案是,这取决于。但是对于 C++ 的情况,在 Menhir 中实现语法应该是不可能的。

于 2020-04-13T07:56:54.873 回答