尝试使用 GNU 的 GMP 库中的类型作为 Bison 的 yylval 类型时出错

Error when attempting to use a type from GNU's GMP library as the yylval type for Bison

我试图通过在 Bison 文件中包含以下内容来使用 GMP 库中的类型 mpz_t 作为 yylval 的类型:

%define api.value.type {mpz_t}

我检查了生成的解析器,它正确地生成了 typedef mpz_t YYSTYPE 行,YYSTYPE 后来被用来创建 yylval.

mpz_t 在 GMP 头文件 gmp.h 中的类型定义为 typedef __mpz_struct mpz_t[1];。反过来,__mpz_struct 被定义为

typedef struct
{
    // struct members here - don't believe they're important
} __mpz_struct;

Bison 运行没有错误,但每当我尝试创建可执行文件时,我都会收到以下错误:

calc.tab.c: In function ‘yyparse’:

calc.tab.c:1148:12: error: incompatible types when assigning to type ‘YYSTYPE’ from type ‘struct __mpz_struct *’

*++yyvsp = yylval;

yyvsp 被定义为指向 YYSTYPE.

的指针

知道如何解决这个问题吗?

如您所说,mpz_t 被定义为数组类型的别名:

typedef __mpz_struct mpz_t[1];

因此,对 mpz_t 类型变量的赋值是非法的:

mpz_t a, b;
mpz_init(b);
a = b;  /* Error: incompatible types when assigning to type ‘mpz_t’ */
        /* from type ‘struct __mpz_struct *’                        */

相反,有必要使用内置赋值函数之一:

mpz_t a, b;
mpz_inits(a, b, 0);
mpz_set(a, b);   /* a is now a copy of b */

由于 gmp 管理内存的方式,禁止直接分配给 mpz_t 是必要的。请参阅下面的注释 1。

Bison假设语义类型YYSTYPE可以赋值(见注2),也就是说不能是数组类型。这通常不是问题,因为通常 YYSTYPE 是联合类型,并且可以分配给具有数组成员的联合。因此,只要将数组类型包含在 %union 声明中,就可以将数组类型与 bison 一起使用。

但是你不能用 gmp 这样做,因为虽然它可以编译,但它不会工作。您最终会遇到大量泄漏的内存,并且很可能会遇到隐蔽的错误,其中 gmp 计算出错误的值(或者以更明显的方式失败,例如 freempz_t).

直接使用mpz_t对象作为语义值是可能的,但这并不容易。您最终会花费大量时间思考哪些堆栈槽具有已初始化的语义值;哪些具有需要 mpz_cleared 的值,以及许多其他令人不安的细节。

一个更简单(但不简单)的解决方案是使语义值成为指向mpz_t指针。如果您只是制作一个 bignum 计算器,您可以完全绕过语义值并维护您自己的值堆栈。只要每个缩减操作都从值堆栈中弹出所有参数并推送其结果,这就会成功。

这个值堆栈也是 mpz_t 个值的向量,但它在几个重要方面与解析器堆栈不同,因为它完全在您的控制之下:

  1. 您没有义务创造 bison 需要创造的临时价值(见注释 2)。例如,如果你想做一个加法,这会将两个操作数从堆栈中弹出并将结果推回,你可以这样做:

    mpz_add(val_stack[top - 2], val_stack[top - 2], val_stack[top - 1]);
    --top;
    
  2. 可以在解析前初始化值栈,解析完成后清空所有元素。这使内存管理变得更加简单,并允许您重用分配的肢体向量。

  3. 像运算符和括号这样没有关联语义值的标记不会在值堆栈上占据space。这并没有节省多少space,但它避免了初始化和清除其中从来没有有用数据的堆栈槽的需要。

备注

1。为什么 GMP 不鼓励直接赋值

根据 gmp 手册,制作大小为 1 的 mpz_t(和其他类似类型)数组只是为了弥补 C 缺乏传递引用。由于数组在用作函数参数时会衰减为指针,因此您无需显式标记参数即可获得按引用传递。但是一定有人想到使用数组类型也可以防止直接赋值给 mpz_t。由于 gmp 管理内存的方式,直接分配无法工作。

Gmp 值必须包含对已分配存储空间的引用。 (必然的,因为bignum的大小没有限制,所以不同的bignum是不同的大小。)一般来说,这样管理对象有两种方式:

  1. 使对象不可变。然后就可以随便分享了,因为不能修改。

  2. 始终在分配时复制对象,从而无法共享。然后可以在不影响任何其他对象的情况下修改对象。

例如,Java 和 C++ 字符串方法就是这两种策略的例证。不幸的是,这两种策略都依赖于语言中的一些基础设施:

  • 不可变字符串需要进行垃圾回收。如果没有垃圾收集器,就无法判断何时可以释放字符串的存储空间。可以对内存分配进行引用计数,但引用计数需要递增和递减,除非您准备让您的代码成为引用计数维护的沼泽,否则您需要一些语言支持。

  • 复制字符串需要覆盖赋值运算符。这在 C++ 中是可能的,但很少有其他语言能如此灵活。

上述两种策略都存在性能问题。

  • 不可变对象修改时需要复制,可以将简单的线性复杂度变成二次复杂度。这是一个众所周知的问题,重复附加到 Java 或 Python 字符串; Java 的 StringBuilder 旨在弥补这个问题。不可变整数会很烦人;累加和很常见,例如 (sum += value;),并且每次都必须通过这样的循环复制 sum 可能会大大减慢循环速度。

  • 另一方面,在赋值时强制复制使得无法共享常量,甚至无法重新排列向量。这会导致大量额外的复制,再次导致线性算法变成二次算法。

Gmp 选择了可变对象策略。 Bignums 必须 在赋值时复制,并且由于 C 不允许覆盖赋值运算符,最简单的解决方案是禁止使用赋值运算符,强制使用库函数。

由于在某些情况下移动大数而不进行复制很有用——例如,混洗大数数组——gmp 还提供了交换功能。而且,如果你非常非常小心并且比我更了解 gmp 的内部结构,则可能只使用上面提到的 union hack,或者使用 memcpy(),以便做更多gmp 对象的复杂重新排列,前提是您保持重要的不变性:

肢体的每个矢量必须被一个且仅一个 mpz_t 对象引用。

重要的原因是 gmp 将在必要时使用 realloc 调整 bignum 的大小。假设 abmpz_t,我们使用一些 hack 使它们成为相同的 bignum,共享内存:

memcpy(a, b, sizeof(a));

现在,我们让 b 变得更大:

mpz_mul(b, b, b);  /* Set b to b squared */

这会工作得很好,但在内部它会做类似

的事情
tmp = realloc(b->_mp_d, 2 * b->_mp_size);
if (tmp) b->_mp_d = tmp;

为了使 b 足够大以容纳结果。这对 b 来说很好用,但它可能会导致 a 指向的肢体进入边缘状态,因为分配新存储的成功 realloc 将自动释放旧存储。

任何增加 b 大小的操作都会发生同样的事情;就地平方只是一个例子。 a 在几乎任何增加 b 大小的修改之后都可能以悬空指针结束: mpz_add(b, tmp1, tmp2); (假设 tmp1 and/or tmp2 是大于 b.)

2。为什么 Bison 要求语义值是可分配的

Bison 为每次归约创建一个临时 YYSTYPE 对象;这个临时变量是 bison 操作中表示为 $$ 的实际变量。在执行缩减操作之前,解析器执行 $$ = ; 的等价物。一旦动作完成,</code> 到 <code>$n 被弹出堆栈,$$ 被压入堆栈。实际上,这会用 $$ 覆盖旧的 </code>,这就是必须使用临时变量的原因。 (否则,在操作中设置 <code>$$ 会意外地使 无效。)