Flex/Bison: 无法使用 semantic_type

Flex/Bison: cannot use semantic_type

我尝试创建一个 C++ flex/bison 解析器。我使用 this tutorial 作为起点并且没有更改任何 bison/flex 配置。我现在坚持要尝试对词法分析器进行单元测试。

我的单元测试中有一个函数直接调用 yylex,并检查它的结果:

private: static void checkIntToken(MyScanner &scanner, Compiler *comp, unsigned long expected, unsigned char size, char isUnsigned, unsigned int line, const std::string &label) {
    yy::MyParser::location_type loc;
    yy::MyParser::semantic_type semantic; // <---- is seems like the destructor of this variable causes the crash

    int type = scanner.yylex(&semantic, &loc, comp);
    Assert::equals(yy::MyParser::token::INT, type, label + "__1");

    MyIntToken* token = semantic.as<MyIntToken*>();
    Assert::equals(expected, token->value, label + "__2");
    Assert::equals(size, token->size, label + "__3");
    Assert::equals(isUnsigned, token->isUnsigned, label + "__4");
    Assert::equals(line, loc.begin.line, label + "__5");

    //execution comes to this point, and then, program crashes
}

错误信息是:

program: ../src/__autoGenerated__/MyParser.tab.hh:190: yy::variant<32>::~variant() [S = 32]: Assertion `!yytypeid_' failed.

我试图遵循自动生成的 bison 文件中的逻辑,并从中理解一些道理。但我没有成功,最终放弃了。然后我在网上搜索了有关此错误消息的任何建议,但没有找到任何建议。

错误指示的位置有以下代码:

~variant (){
  YYASSERT (!yytypeid_);
}

编辑:只有当我删除

时问题才会消失
%define parse.assert

野牛文件中的选项。但我不确定这是否是个好主意...

为了单元测试目的,获取 flex 生成的令牌值的正确方法是什么?

注意:我已尽我所知尝试解释野牛变体类型。我希望它是准确的,但除了一些玩具实验外,我没有使用过它们。假设此解释以任何方式暗示对接口的认可是错误的。

bison的C++接口提供的所谓“变体”类型并不是通用的变体类型。这是一个深思熟虑的决定,基于这样一个事实,即解析器总是能够找出与解析器堆栈上的语义值关联的语义类型。 (这一事实还允许在解析器中安全地使用 C union。)因此,在“变体”中记录类型信息将是多余的。所以他们没有。从这个意义上说,它并不是真正的有区别的联合,尽管人们可能对名为“variant”的类型有什么期望。

(野牛变体类型是一个带有整数(非类型)模板参数的模板。该参数是变体中允许的最大类型的字节大小;它没有以任何其他方式指定可能的类型。semantic_type 别名用于确保相同的模板参数用于解析器代码中的每个野牛变体对象。)

因为不是可辨别联合,所以它的析构函数不能析构当前值;它无法知道该怎么做。

这个设计决定实际上在 Bison“变体”类型的(可悲的是不够)文档中提到。 (读这篇文章时,请记住它最初是在 std::variant 存在之前写的。现在,std::variant 被拒绝为“冗余”,尽管 [= 的存在也是可能的13=] 重新审视这个设计决定可能会得到令人满意的结果)。在 C++ 变体类型一章中,我们读到:

Warning: We do not use Boost.Variant, for two reasons. First, it appeared unacceptable to require Boost on the user’s machine (i.e., the machine on which the generated parser will be compiled, not the machine on which bison was run). Second, for each possible semantic value, Boost.Variant not only stores the value, but also a tag specifying its type. But the parser already “knows” the type of the semantic value, so that would be duplicating the information.

Therefore we developed light-weight variants whose type tag is external (so they are really like unions for C++ actually).

确实如此。所以任何对野牛“变体”的使用都必须有一个明确的类型:

  • 您可以 build 带有要构建的类型参数的变体。 (这是唯一不需要模板参数的情况,因为类型是从参数中推导出来的。只有当参数不是精确类型时,您才必须使用显式模板参数;例如,一个整数等级较低。)
  • 您可以使用 as<T> 获取对已知类型 T 的值的引用。 (如果值具有不同的类型,则这是未定义的行为。)
  • 您可以使用 destroy<T>.
  • 破坏已知类型 T 的值
  • 您可以使用 copy<T>move<T> 从已知类型 T 的另一个变体复制或移动值。 (move<T> 涉及构建然后销毁 T(),所以如果 T 有一个昂贵的默认构造函数,你可能不想这样做。总的来说,我不相信语义move 方法的名称。它的名称在语义上与 std::move 冲突,但它再次排在第一位。)
  • 您可以交换具有相同已知类型 Tswap<T> 的两个变体的值。

现在,生成的解析器了解所有这些限制,并且它始终知道它所拥有的“变体”的真实类型。但是您可能会出现并尝试以违反约束的方式对其中一个对象执行某些操作。由于该对象实际上没有任何方法来检查约束,您最终会得到未定义的行为,这可能会产生一些灾难性的最终后果。

所以他们还实现了一个允许“变体”检查约束的选项。毫不奇怪,这包括添加鉴别器。但是由于鉴别器仅用于验证而不用于修改行为,因此它不是一个在少数已知备选方案之间进行选择的小整数,而是指向 std::typeid (或 NULL if该变体还不包含值。)(公平地说,在大多数情况下,对齐约束意味着为此目的使用指针并不比使用小枚举更昂贵。都一样...)

这就是您 运行 的兴趣所在。您使用 %define parse.assert 启用了断言;该选项专门用于防止您做您想做的事情,即让变体对象的析构函数 运行 在显式销毁变体的值之前。

所以避免这个问题的“正确”方法是在作用域的末尾插入一个显式调用:

   // execution comes to this point, and then, without the following
   // call, the program will fail on an assertion
   semantic.destroy<MyIntType*>();
}

启用解析断言后,变体对象将能够验证指定为 semantic.as<T>semantic.destroy<T> 模板参数的类型是否与存储在对象中的值的类型相同。 (没有 parse.assert,这也是你的责任。)


警告:意见如下。

万一有人读到这篇文章,我更喜欢使用真正的 std::variant 类型是因为 AST 节点的语义值需要可区分的联合实际上是很常见的。通常的解决方案(在 C++ 中)是构造一个类型层次结构,在某些方面,它完全是人为的,并且 std::variant 很可能可以更好地表达语义。

在实践中,我使用 C 接口和我自己的可区分联合实现。