首页 > 解决方案 > Flex查找子字符串直到字符

问题描述

这是我的lexer.l文件:

%{
#include "../h/Tokens.h"
%}

%option yylineno

%%

[+-]?([1-9]*\.[0-9]+)([eE][+-]?[0-9])?  return FLOAT;
[+-]?[1-9]([eE][+-]?[0-9])?             return INTEGER;
\"(\\\\|\\\"|[^\"])*\"                  return STRING;
(true|false)                            return BOOLEAN;

(func|val|if|else|while|for)*           return KEYWORD;
[A-Za-z_][A-Za-z_0-9]*                  return IDENTIFIER;

"+"                                     return PLUS;
"-"                                     return MINUS;
"*"                                     return MULTI;
"."                                     return DOT;
","                                     return COMMA;
":"                                     return COLON;
";"                                     return SEMICOLON;

.                                       printf("Unexpected or invalid token: '%s'\n", yytext);

%%

int yywrap(void)
{
    return 1;
}

现在,如果我的词法分析器发现了一个意外的标记,它会为每个字符发送一个错误。我希望它为每个子字符串发送一条错误消息,直到出现空格或运算符。

例子:

Input:

foo bar baz
~±`≥ hello


Output:

Identifier.
Identifier.
Identifier.
Unexpected or invalid token: '~±`≥'
Identifier.

有没有办法用正则表达式模式做到这一点?

谢谢。

标签: flex-lexerlexer

解决方案


当然可以使用正则表达式。但是您不能使用独立于其他令牌规则的正则表达式来做到这一点。找到正确的正则表达式可能并非易事。

然而,在这个相当简单的例子中,它相当简单,尽管有一个极端情况。由于没有多字符运算符,因此字符不能开始标记,除非它是字母、数字、运算符之一 ( -+*.,:;) 或双引号。因此,任何此类字符的序列都是无效序列。另外,我认为您确实希望忽略空格字符(基于示例输出),即使您的问题没有显示任何与空格匹配的规则。所以假设你只是省略了空格规则,这就像

[[:space:]]+                    { /* Ignore whitespace */ }

您匹配一系列非法字符的正则表达式将是

[^-+*.,:;[:alnum:][:space:]]+   { fprintf(stderr, "Invalid sequence %s\m", yytext); }

极端情况是未终止的字符串文字;也就是说,以 a 开头"但不包含匹配的结束引号的标记。这样的标记必须延伸到输入的末尾,并且可以通过使用您的字符串模式轻松匹配,省去 final "。(这是有效的,因为 (f)lex 总是使用最长的匹配模式,所以如果有一个终止",正确的字符串文字将被匹配。)

您的模式中有许多错误:

  1. +-在数字文字的开头匹配几乎总是一个坏主意。如果你这样做,那么x+2将不会被正确分析;您的词法分析器将返回两个标记 anIDENTIFIER和 an INTEGER,而不是正确的三个标记 ( IDENTIFIER, PLUS, INTEGER)。

  2. 您的FLOAT模式不接受以小数点前包含 0 开头的数字,因此0.5两者10.3都会失败。此外,您强制指数为单个数字,因此1.3E11也不会匹配。你强迫用户在小数点后面加一个数字;大多数语言接受3.等同于3.0. (最后一个不一定是错误,但它是非常规的。)

  3. 您的INTEGER模式不接受包含 0 的数字,例如10. 但它会接受科学计数法,这有点奇怪;在大多数语言3E10中是浮点常量,而不是整数。

  4. 您的KEYWORD模式接受由一系列单词组成的关键字,例如forwhilefuncif. 您可能不打算将 a*放在模式的末尾。

  5. 您的字符串文字模式允许除 之外的任何字符序列",这意味着反斜杠\将被允许作为单个字符匹配,即使它后跟引号或反斜杠也是如此。这将导致某些字符串文字未正确终止。例如,给定字符串文字

     "\\"
    

    (这是一个包含单个反斜杠的字符串文字),正则表达式将匹配初始的",然后\作为单个字符,然后是\"序列,然后是字符串文字后面的任何内容,直到遇到另一个引号。

    该错误是 flex 需要\在括号表达式内转义的结果,这与 Posix 正则表达式在\括号内失去特殊意义不同。

所以这会给你留下这样的东西:

%{
#include "../h/Tokens.h"
%}

%option yylineno noyywrap

%%
[[:space:]]+                    /* Ignore whitespace */

(\.[0-9]+|[0-9]+\.[0-9]*)([eE][+-]?[0-9]+)?  {
                                  return FLOAT;
                                }
0|[1-9][0-9]*                   return INTEGER;
true|false                      return BOOLEAN;
func|val|if|else|while|for      return KEYWORD;
[A-Za-z_][A-Za-z_0-9]*          return IDENTIFIER;

"+"                             return PLUS;
"-"                             return MINUS;
"*"                             return MULTI;
"."                             return DOT;
","                             return COMMA;
":"                             return COLON;
";"                             return SEMICOLON;

\"(\\\\|\\\"|[^\\"])*\"         return STRING;
\"(\\\\|\\\"|[^\\"])*           { fprintf(stderr,
                                          "Unterminated string literal\n"); }
[^-+*.,:;[:alnum:][:space:]]+   { fprintf(stderr,
                                          "Invalid sequence %s\m", yytext); }

(如果这些模式中的任何一个看起来很神秘,您可能需要查看flex 手册中对 flex 模式的描述。)


但我有一种感觉,您正在寻找不同的东西:一种无需过度分析即可神奇地适应令牌模式的任何变化的方法。

这也是可能的,但我不知道如何在没有代码重复的情况下做到这一点。基本思想很简单:当我们遇到不匹配的字符时,我们只需将其附加到错误标记的末尾,当我们找到有效标记时,我们发出错误消息并清除错误标记。

问题在于“当我们找到有效令牌时”部分,因为这意味着我们需要在除错误规则之外的每个规则的开头插入一个操作。最简单的方法是使用宏,它至少可以避免为每个操作写出代码。

(F)lex 确实为我们提供了一些有用的工具,我们可以在此基础上进行构建。我们将使用 (f)lex 的特殊操作之一yymore(),它会导致将当前匹配附加到正在构建的标记中,这对于构建错误标记很有用。

为了知道错误标记的长度(因此要知道是否有),我们需要一个额外的变量。幸运的是,(f)lex 允许我们在扫描仪内部定义自己的局部变量。然后我们定义宏E_(其名称被选为简短,以避免混乱规则操作),它打印错误消息,移动yytext错误标记,并重置错误计数。

把它放在一起:

%{
#include "../h/Tokens.h"
%}

%option yylineno noyywrap

%%
    int nerrors = 0; /* To keep track of the length of the error token */
    /* This macro must be inserted at the beginning of every rule,
     * except the fallback error rule.
     */
    #define E_                                                       \
      if (nerrors > 0) {                                             \
        fprintf(stderr, "Invalid sequence %.*s\n", nerrors, yytext); \
        yytext += nerrors; yyleng -= nerrors; nerrors = 0;           \
      } else /* Absorb the following semicolon */

[[:space:]]+                    { E_; /* Ignore whitespace */ }

(\.[0-9]+|[0-9]+\.[0-9]*)([eE][+-]?[0-9]+)?  { E_; return FLOAT; }
0|[1-9][0-9]*                   { E_; return INTEGER; }
true|false                      { E_; return BOOLEAN; }
func|val|if|else|while|for      { E_; return KEYWORD; }
[A-Za-z_][A-Za-z_0-9]*          { E_; return IDENTIFIER; }

"+"                             { E_; return PLUS; }
"-"                             { E_; return MINUS; }
"*"                             { E_; return MULTI; }
"."                             { E_; return DOT; }
","                             { E_; return COMMA; }
":"                             { E_; return COLON; }
";"                             { E_; return SEMICOLON; }

\"(\\\\|\\\"|[^\\"])*\"         { E_; return STRING; }
\"(\\\\|\\\"|[^\\"])*           { E_; 
                                  fprintf(stderr,
                                          "Unterminated string literal\n"); }

.                               { yymore(); ++nerror; }

这一切都假设我们很乐意在扫描仪内生成错误消息,否则忽略错误字符。但实际返回错误指示并让调用者决定如何处理错误可能会更好。这会带来额外的麻烦,因为它要求我们在一个操作中返回两个令牌。

对于一个简单的解决方案,我们使用另一个 (f)lex 功能,yyless()它允许我们重新扫描部分或全部当前令牌。我们可以使用它从当前标记中删除错误标记,而不是调整 yytext 和 yyleng。(yyless将为我们进行调整。)这意味着发生错误后,下一个正确的令牌会被扫描两次。这可能看起来效率低下,但它可能是可以接受的,因为:

  1. 大多数令牌都很短,
  2. 优化错误并没有多大意义。优化正确输入的处理更为有用。

为了做到这一点,我们只需要对E_宏做一点小改动:

    #define E_                                                       \
      if (nerrors > 0) {                                             \
        yyless(nerrors);                                             \
        fprintf(stderr, "Invalid sequence %s\n", yytext);            \
        nerrors = 0;                                                 \
        return BAD_INPUT;                                            \
      } else /* Absorb the following semicolon */

推荐阅读