在 Flex/Bison 中实现字符串插值

Dr.*_*eon 4 string yacc d bison string-interpolation

我目前正在为我设计的语言编写一个解释器。

词法分析器/解析器 (GLR) 是用 Flex/Bison 编写的,主解释器是用 D 编写的 - 到目前为止一切工作都完美无缺。

问题是我还想添加字符串插值,即识别包含特定模式(例如"[some expression]")的字符串文字并转换包含的表达式。我认为这应该在解析器级别从相应的语法操作中完成。

我的想法是将插值字符串转换/处理为简单连接的样子(因为它现在可以工作)。

例如

print "this is the [result]. yay!"
Run Code Online (Sandbox Code Playgroud)

print "this is the " + result + ". yay!"
Run Code Online (Sandbox Code Playgroud)

然而,我对如何在 Bison 中做到这一点有点困惑:基本上,我如何告诉它重新解析特定的字符串(在构建主 AST 时)?

有任何想法吗?

ric*_*ici 5

如果您确实需要,您可以通过生成可重入解析器来重新解析字符串。你可能也想要一个可重入的扫描仪,尽管我想你可以使用flex的缓冲堆栈将某些东西与默认扫描仪混在一起。事实上,值得学习如何在避免不必要的全局变量的一般原则上构建可重入解析器和扫描器,无论您是否需要它们用于此特定目的。

但你实际上并不需要重新解析任何东西;您可以一次性完成整个解析。您只需要扫描仪有足够的智能,以便它了解嵌套插值。

基本思想是让扫描器将带有插值的字符串文字分割成一系列标记,解析器可以轻松地将这些标记组装成适当的 AST。由于扫描器可能会从单个字符串文字中返回多个标记,因此我们需要引入一个启动条件来跟踪扫描当前是否在字符串文字内。由于插值可能是嵌套的,我们将使用 flex 的可选启动条件 stack%option stack来跟踪嵌套的上下文。

这是一个粗略的草图。

如前所述,扫描器有额外的启动条件:SC_PROGRAM,默认值,在扫描器扫描常规程序文本时有效,以及SC_STRING,在扫描器扫描字符串时有效。SC_PROGRAM只是因为flex官方没有提供检查启动条件栈是否为空的接口才需要;INITIAL除了嵌套之外,它与顶级启动条件相同。起始条件堆栈用于跟踪插值标记([]本例中),这是必需的,因为插值表达式可能使用括号(例如,作为数组下标),甚至可能包含嵌套的插值字符串。由于SC_PROGRAMis(除一个例外)与 相同INITIAL,因此我们将使其成为一条包容性规则。

%option stack
%s SC_PROGRAM
%x SC_STRING
%%
Run Code Online (Sandbox Code Playgroud)

由于我们使用单独的开始条件来分析字符串文字,因此我们还可以在解析时标准化转义序列。并非所有应用程序都希望这样做,但这很常见。但由于这并不是这个答案的重点,所以我省略了大部分细节。更有趣的是处理嵌入式插值表达式的方式,特别是深度嵌套的插值表达式。

最终结果是将字符串文字转换为一系列标记,可能表示嵌套结构。为了避免在扫描器中实际解析,我们不会尝试创建 AST 节点或以其他方式重写字符串文字;相反,我们只是将引号字符本身传递给解析器,从而分隔字符串文字片段的序列:

["]                 { yy_push_state(SC_STRING);    return '"'; }
<SC_STRING>["]      { yy_pop_state();              return '"'; }
Run Code Online (Sandbox Code Playgroud)

插值标记使用一组非常相似的规则:

<*>"["              { yy_push_state(SC_PROGRAM);   return '['; }
<INITIAL>"]"        {                              return ']'; }
<*>"]"              { yy_pop_state();              return ']'; } 
Run Code Online (Sandbox Code Playgroud)

上面的第二条规则避免在起始条件堆栈为空时弹出该堆栈(因为它将处于该INITIAL状态)。没有必要在扫描仪中发出错误消息;我们可以将不匹配的右括号传递给解析器,然后解析器将执行任何看似必要的错误恢复。

为了结束SC_STRING状态,我们需要返回字符串片段的标记,可能包括转义序列:

<SC_STRING>{
  [^[\\"]+          { yylval.str = strdup(yytext); return T_STRING; }

  \\n               { yylval.chr = '\n';           return T_CHAR; }
  \\t               { yylval.chr = '\t';           return T_CHAR; }
          /* ... Etc. */
  \\x[[:xdigit]]{2} { yylval.chr = strtoul(yytext, NULL, 16);
                                               return T_CHAR; }
  \\.               { yylval.chr = yytext[1];      return T_CHAR; }
}
Run Code Online (Sandbox Code Playgroud)

将转义字符返回给解析器可能不是最好的策略;通常我会使用内部扫描器缓冲区来累积整个字符串。但出于说明目的,它很简单。(这里省略了一些错误处理;存在各种极端情况,包括换行符处理以及程序中的最后一个字符是未终止字符串文字内的反斜杠的恼人情况。)

在解析器中,我们只需要为内插字符串插入一个串联节点。唯一的复杂性是,我们不想在没有任何插值的情况下为字符串文字的常见情况插入这样的节点,因此我们使用两种语法产生式,一种用于仅包含一个包含片段的字符串,另一种用于带有两件或更多件:

string : '"' piece '"'                 { $$ = $2; }
       | '"' piece piece_list '"'      { $$ = make_concat_node(
                                                prepend_to_list($2, $3));
                                       }
piece  : T_STRING                      { $$ = make_literal_node($1); }  
       | '[' expr ']'                  { $$ = $2; }
piece_list
       : piece                         { $$ = new_list($1); }
       | piece_list piece              { $$ = append_to_list($1, $2); }
Run Code Online (Sandbox Code Playgroud)