优化flex字符串文字解析

时间:2017-02-20 21:41:19

标签: c++ parsing flex-lexer stdstring string-literals

我开始为我的编程语言编写一个词法分析器。

此语言中的字符串文字以"开头,并在遇到未转义的"时结束。保留内部的所有内容(包括换行符),除了转义序列(通常的\n s,\t\"等等,还有一种通过使用ASCII代码转义字符的方法,例如\097\97)。

这是我到目前为止编写的代码:

%{
#include <iostream>
#define YY_DECL extern "C" int yylex()

std::string buffstr;
%}
%x SSTATE
%%

\"                   {
                         buffstr.clear();
                         BEGIN(SSTATE);
                     }
<SSTATE>\\[0-9]{1,3} {
                         unsigned code = atoi(yytext + 1);
                         if (code > 255) {
                             std::cerr << "SyntaxError: decimal escape sequence larger than 255 (" << code << ')' << std::endl;
                             exit(1);
                         }
                         buffstr += code;
                     }

<SSTATE>\\a          buffstr += '\a';
<SSTATE>\\b          buffstr += '\b';
<SSTATE>\\f          buffstr += '\f';
<SSTATE>\n           buffstr += '\n';
<SSTATE>\r           buffstr += '\r';
<SSTATE>\t           buffstr += '\t';
<SSTATE>\v           buffstr += '\v';
<SSTATE>\\\\         buffstr += '\\';
<SSTATE>\\\"         buffstr += '\"';
<SSTATE>\\.          {
                         std::cerr << "SyntaxError: invalid escape sequence (" << yytext << ')' << std::endl;
                         exit(1);
                     }
<SSTATE>\"           {
                         std::cout << "Found a string: " << buffstr << std::endl;
                         BEGIN(INITIAL);
                     }
<SSTATE>.            buffstr += yytext[0];

.                    ;

%%

int main(int argc, char** argv) {
    yylex();
}

它完美无缺,但你可以看到它没有特别优化。

对于正在解析的字符串文字中的每个字符,它将一个字符附加到std :: string一次,这是不理想的。

我想知道是否有更好的方法,例如存储指针并增加长度,然后使用std::string(const char* ptr, size_t lenght)构建字符串。

有吗?会是什么?

1 个答案:

答案 0 :(得分:2)

可能的情况是,所提供的代码对于所有实际目的而言都足够快,并且您不必担心在实际观察到它成为瓶颈之前对其进行优化。词汇扫描,即使效率低下,也很少是编译时间的重要贡献。

然而,一些优化是直截了当的。

最简单的方法是观察大多数字符串不包含转义序列。因此,应用通常的优化技术来寻找低位果实,我们首先在一个模式中处理没有转义序列的字符串,甚至没有通过单独的词汇状态。 [注1]

\"[^"\\]*\"   { yylval.str = new std::string(yytext + 1, yyleng - 2); 
                return T_STRING;
              }

(F)lex提供yyleng这是它找到的令牌的长度,所以从来没有任何理由用strlen重新计算长度。在这种情况下,我们不希望字符串中包含周围的双引号,因此我们从第二个字符开始选择yyleng - 2个字符。

当然,我们需要处理转义码;我们可以使用类似于你的开始条件来做到这一点。我们只在字符串文字中找到转义字符时才输入此启动条件。 [注2]为了捕捉这种情况,我们依赖于(f)lex实现的 maximal munch 规则,即具有最长匹配的模式击败了碰巧匹配的任何其他模式。相同的输入点。 [注3]由于我们已经匹配任何以&#34; 开头并且在结束&#34; 之前不包含反斜杠的令牌,我们可以添加一个非常相似的模式而没有收尾报价,只有在第一条规则没有的情况下才能匹配,因为与收盘价的匹配是一个字符更长。

\"[^"\\]*     { yylval.str = new std::string(yytext + 1, yyleng - 1);
                BEGIN(S_STRING);
                /* No return, so the scanner will continue in the new state */
              }

S_STRING状态下,我们仍然可以匹配不包含反斜杠的序列(不仅仅是单个字符),从而大大减少了动作执行次数和字符串追加次数:

(开始条件中的支撑模式列表为flex extension。)

<S_STRING>{
  [^"\\]+       { yylval.str->append(yytext, yyleng); }
  \\n           { (*yylval.str) += '\n'; }
   /* Etc. Handle other escape sequences similarly */
  \\.           { (*yylval.str) += yytext[1]; }
  \\\n          { /* A backslash at the end of the line. Do nothing */ }
  \"            { BEGIN(INITIAL); return T_STRING; }
     /* See below */
}

当我们最终找到一个与最后一个模式匹配的未转义双引号时,我们首先重置词法状态,然后返回已完全构造的字符串。

模式\\\n实际匹配行末尾的反斜杠。完全忽略此反斜杠和换行符是很常见的,以便允许在多个源代码行上继续使用长字符串。如果您不想提供此功能,只需将\.模式更改为\(.|\n)

如果我们找不到未转义的双引号怎么办?也就是说,如果意外省略了收盘双引号怎么办?在这种情况下,我们将以S_STRING开始条件结束,因为字符串未被引号终止,因此回退模式将匹配。在S_STRING模式中,我们需要添加两种可能性:

<S_STRING>{
    // ... As above
  <<EOF>>      |
  \\           { /* Signal a lexical error */ }
}

这些规则中的第一个捕获简单的未终止字符串错误。第二个是捕获反斜杠后面没有合法字符的情况,只有当反斜杠是带有未终止字符串的程序中的最后一个字符时才会发生其他规则。虽然不太可能,但它可能会发生,所以我们应该抓住它。

进一步优化相对简单,虽然我不推荐它,因为它主要使代码复杂化,并且好处是无穷小的。 (由于这个原因,我还没有包含任何示例代码。)

在开始条件中,反斜杠(几乎)总是会在我们正在累积的字符串中附加一个字符,这意味着我们可能会调整字符串大小以便执行此追加,即使我们只是调整了大小它附加非转义字符。相反,我们可以在与非转义字符匹配的操作中为字符串添加一个附加字符。 (因为(f)lex将输入缓冲区修改为NUL终止令牌,令牌后面的字符将始终为NUL,因此将附加的长度增加1将插入此NUL而不是反斜杠到字符串中。但是这并不重要。)

然后处理转义字符的代码需要替换字符串中的最后一个字符,而不是将单个字符附加到字符串,从而避免一个追加调用。当然,在我们不想插入任何内容的情况下,我们需要将字符串的大小减少一个字符,如果有一个转义序列(例如unicode转义),则添加字符串不止一个字节,我们需要做一些其他的杂技。

简而言之,我认为这不仅仅是一种优化,而是一种黑客攻击。但是对于它的价值,过去我做过这样的事情,所以我也要承担过早优化的责任。

注释

  1. 您的代码仅打印出令牌,这使得很难知道您的设计是什么将字符串传递给解析器。我在这里假设一个或多或少的标准策略,其中语义值yylval是一个联盟,其成员是std::string* a {{1 }})。我没有解决由此导致的内存管理问题,但std::string声明会有很多帮助。

  2. 在这个答案的原始版本中,我建议使用一个匹配反斜杠作为尾随上下文的模式来捕获这种情况:

    %destruct

    但是使用最大蒙克规则更简单,更通用。

  3. 如果多个模式具有相同的最长匹配,则扫描程序描述中的第一个模式将获胜。