首页 > 解决方案 > 在 Flex/Bison 中实现字符串插值

问题描述

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

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

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

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

例如

print "this is the [result]. yay!"

print "this is the " + result + ". yay!"

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

有任何想法吗?

标签: stringdbisonyaccstring-interpolation

解决方案


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

但是你真的不需要重新解析任何东西;您可以一次完成整个解析。您只需要在您的扫描仪中有足够的智能,以便它知道嵌套插值。

基本思想是让扫描器通过插值将字符串文字拆分为一系列标记,这些标记可以很容易地被解析器组装成适当的 AST。由于扫描器可能会从单个字符串文字中返回多个标记,因此我们需要引入一个开始条件来跟踪扫描当前是否在字符串文字内。并且由于插值可能是嵌套的,我们将使用 flex 的可选开始条件堆栈(启用%option stack)来跟踪嵌套的上下文。

所以这是一个粗略的草图。

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

%option stack
%s SC_PROGRAM
%x SC_STRING
%%

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

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

["]                 { yy_push_state(SC_STRING);    return '"'; }
<SC_STRING>["]      { yy_pop_state();              return '"'; }

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

<*>"["              { yy_push_state(SC_PROGRAM);   return '['; }
<INITIAL>"]"        {                              return ']'; }
<*>"]"              { yy_pop_state();              return ']'; } 

上面的第二条规则避免在启动条件堆栈为空时弹出它(因为它将处于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; }
}

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

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

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); }

推荐阅读