【问题标题】:Implementing String Interpolation in Flex/Bison在 Flex/Bison 中实现字符串插值
【发布时间】:2019-09-17 09:21:39
【问题描述】:

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

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

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

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

例如

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

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

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

有什么想法吗?

【问题讨论】:

  • 我想我会建议您不要使用此语言功能,而不是提供解决方案。字面量是字面量:解析在它们的边界处停止。

标签: string d bison yacc string-interpolation


【解决方案1】:

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-15
    相关资源
    最近更新 更多