没有语义值但使用动作的副作用是一种很好的野牛做法吗?

Is it a good bison practice to not have semantic values, but use side effects of actions?

我有一种语言,其中所有事物的语义都是字符数组或数组数组。所以我有以下 YYSTYPE:

typedef struct _array {
    union {
        char *chars; // start of string
        void *base;  // start of array
    };
    unsigned n;      // number of valid elements in above
    unsigned allocated;  // number of allocated elements for above
} array;

#define YYSTYPE array

并且我可以使用

将一个字符数组附加到一个数组数组中
void append(YYSTYPE *parray, YYSTYPE *string);

假设语法 (SSCCE) 是:

%token  WORD
%%
array       :       WORD    
            | array WORD    
            ;

所以我接受了一个单词序列。对于每个单词,语义值变成了字符数组,然后我想将每个字符附加到数组数组中,用于整个序列。

有几种可能的方法来设计动作:

  1. array个符号有array类型的语义值。如果我这样做,那么 array WORD 的操作将不得不将数组 </code> 复制到 <code>$$,这很慢,所以我不喜欢那样。

  2. array个符号具有array *类型的语义值。现在 array WORD 的动作,我可以只添加到数组 *,然后设置 $$ 等于 </code>。但我不喜欢这个有两个原因。首先,语义不是指向<code>array的指针,而是array。其次,对于规则 array : WORD 的操作,我将不得不 malloc 结构,这很慢。是的,'append' 有时会做 malloc,但如果我分配足够多,就不会频繁。出于性能原因,我想避免任何不必要的 malloc

  3. 完全忘记尝试为符号 array 赋予语义值,并使用全局变量:

    static YYSTYPE g_array;

    YYSTYPE *g_parray = &g_array;

然后,操作将只使用

append(g_parray, word_array)

整个语法的工作方式,我不需要超过一个 g_array。以上是我能想到的最快的。但这确实是一个糟糕的设计——很多全局变量,没有语义值,相反,一切都是全局变量的副作用。

所以,就我个人而言,我不喜欢他们中的任何一个。哪个是普遍接受的野牛最佳实践?

在大多数情况下,使用全局变量没有意义。或多或少的现代版本的野牛有 %parse-param 指令,它允许你有一种 'parsing context'。上下文可能会处理所有内存分配等。

它可能反映了当前的解析状态-i。 e.有 'current array' 等概念。在这种情况下,您的语义动作可以依赖于知道您所在位置的上下文。

%{
    typedef struct tagContext Context;
    typedef struct tagCharString CharString;

    void start_words(Context* ctx);
    void add_word(Context* ctx, CharString* word);
%}

%union {
    CharString* word;
}
%parse-param {Context* ctx}

%token<word> WORD
%start words

%%

words
    : { start_words(ctx); } word
    | words                 word
    ;

word
    : WORD { add_word(ctx, ); }
    ;

如果您解析单词列表而不解析其他任何内容,您可以将作为您的上下文。

但是,在简单的语法中,如果通过YYSTYPE:

来传递信息就清楚多了
%{
    typedef struct tagContext Context;
    typedef struct tagCharString CharString;
    typedef struct tagWordList WordList;

    // word_list = NULL to start a new list
    WordList* add_word(Context* ctx, WordList* prefix, CharString* word);
%}

%union {
    CharString* word;
    WordList* word_list;
}
%parse-param {Context* ctx}

%token<word> WORD
%type<word_list> words words_opt
%start words

%%

words
    : words_opt WORD { $words = add_word(ctx, $words_opt, $WORD); }
    ;

words_opt
    : %empty { $words_opt = NULL; }
    | words
    ;

这两种方法之间的性能差异似乎可以忽略不计。

内存清理

如果您的输入文本被正确解析,则您始终有责任清理所有动态内存。但是,如果您的输入文本导致解析错误,解析器将不得不丢弃一些标记。在这种情况下可能有两种清理方法。

首先,您可以跟踪上下文中的所有内存分配,并在销毁上下文时将它们全部释放。

其次,你可以依赖bison的析构函数:

%{
    void free_word_list(WordList* word_list);
%}

%destructor { free_word_list($$); } <word_list>