问题描述
这是我的 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.
有没有办法用正则表达式来做到这一点?
谢谢。
解决方法
当然可以使用正则表达式。但是您不能使用独立于其他令牌规则的正则表达式来做到这一点。找到正确的正则表达式可能并非易事。
在这个相当简单的例子中,虽然有一个角落案例,但它相当简单。由于没有多字符运算符,除非字符是字母、数字、运算符之一 (-+*.,:;
) 或双引号,否则字符不能作为标记开始。因此,任何此类字符的序列都是无效序列。另外,我认为您确实希望忽略空格字符(基于示例输出),即使您的问题没有显示任何与空格匹配的规则。因此,假设您刚刚省略了空格规则,这将类似于
[[:space:]]+ { /* Ignore whitespace */ }
匹配非法字符序列的正则表达式是
[^-+*.,:;[:alnum:][:space:]]+ { fprintf(stderr,"Invalid sequence %s\m",yytext); }
极端情况是一个未终止的字符串文字;即,以 "
开头但不包括匹配的结束引号的标记。这样的标记必须延伸到输入的末尾,并且可以通过使用您的字符串模式轻松匹配,省略最后的 "
。 (这是有效的,因为 (f)lex 总是使用最长的匹配模式,所以如果有一个终止 "
正确的字符串文字将被匹配。)
您的模式中有许多错误:
-
在数字文字的开头匹配
+-
几乎总是一个坏主意。如果你这样做,那么x+2
将不会被正确分析;您的词法分析器将返回两个标记,一个IDENTIFIER
和一个INTEGER
,而不是正确的三个标记(IDENTIFIER
、PLUS
、INTEGER
)。 -
您的
FLOAT
模式不接受以小数点前包含 0 开头的数字,因此0.5
和10.3
都会失败。此外,您强制指数为一位数,因此1.3E11
也不会匹配。你强迫用户在小数点后放一个数字;大多数语言都接受3.
等同于3.0
。 (最后一个不一定是错误,但它是非常规的。) -
您的
INTEGER
模式不接受包含 0 的数字,例如10
。但它会接受科学记数法,这有点奇怪;在大多数语言中,3E10
是浮点常量,而不是整数。 -
您的
KEYWORD
模式接受由一系列单词串联而成的关键字,例如forwhilefuncif
。您可能不打算在模式末尾放置*
。 -
您的字符串文字模式允许除
"
之外的任何字符序列,这意味着反斜杠\
将被允许作为单个字符匹配,即使它后面跟着一个引号或反斜杠。这将导致某些字符串文字未正确终止。例如,给定字符串文字"\\"
(这是一个包含单个反斜杠的字符串文字),正则表达式将匹配初始
"
,然后是\
作为单个字符,然后是\"
序列,以及然后是字符串文字后面的任何内容,直到遇到另一个引号。该错误是由于 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"); }
[^-+*.,yytext); }
(如果这些模式中的任何一个看起来很神秘,您可能需要查看 the description of flex patterns in the flex manual。)
但我感觉您正在寻找不同的东西:一种无需过度分析即可神奇地适应令牌模式变化的方法。
这也是可能的,但我不知道如何在没有代码重复的情况下做到这一点。基本思想很简单:当我们遇到无法匹配的字符时,我们只需将其附加到错误标记的末尾,当我们找到有效标记时,我们发出错误消息并清除错误标记。
问题在于“当我们找到有效令牌时”部分,因为这意味着我们需要在除错误规则之外的每个规则的开头插入一个动作。最简单的方法是使用宏,这样至少可以避免为每个动作写出代码。
(F)lex 确实为我们提供了一些有用的工具,我们可以在此基础上进行构建。我们将使用 (f)lex 的特殊操作之一 yymore()
,它会将当前匹配附加到正在构建的标记中,这对于构建错误标记很有用。
为了知道错误标记的长度(从而知道是否存在),我们需要一个额外的变量。幸运的是,(f)lex 允许我们define our own local variables inside the scanner。然后我们定义宏 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
会为我们做那个调整。)这意味着在出现错误后,下一个正确的标记会被扫描两次。这可能看起来效率低下,但它可能是可以接受的,因为:
- 大多数令牌都很短,
- 针对错误进行优化并没有多大意义。优化正确输入的处理要有用得多。
为了实现这一点,我们只需要对 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 */