FLEX: Fast Lexical Analyser

Tendo concluído a secção das definições vamos analisar as regras, ou a regra neste caso, visto só termos uma.

{INT}|{DOUBLE}  { yylval.d = strtod(yytext, NULL); return NUMBER; }

A expressão regular neste caso é simples graças ao uso de definições, aceita um INT ou um DOUBLE. A acção também é simples mas a compreensão total envolve pormenores que só vamos ver quando começarmos a escrever o parser. Por enquanto basta saber que o yylval é uma union definida pelo parser onde guardamos o valor de retorno e o NUMBER é um #define que indica o tipo do retorno. O yylval.d é do tipo double, mas o que o FLEX fez match foi a uma string, portanto usamos a função strtod() da stdlib para converter a string para double antes de a enviarmos para o parser.

Agora que já temos o scanner a aceitar números, falta-nos aceitar as operações da calculadora e lidar com o restante input. Esta nova versão do ficheiro trata disso.

%option outfile="CalculatorScanner.c"
 
%{
#include "CalculatorParser.tab.h"
 
 
void yyerror(char * err_msg)
{
    printf("%s\n", err_msg);
    exit(1);
}
 
%}
 
WS          [ \t]
NL          "\n"|"\r"|"\r\n"
DIG         [0-9]
 
 
MANTISSA    ({DIG}*\.{DIG}+)|({DIG}+\.)
EXPONENT    [eE][-+]?{DIG}+
 
INT         (0|[1-9]{DIG}*)
DOUBLE      {MANTISSA}{EXPONENT}?
 
%%
 
{INT}|{DOUBLE}   { yylval.d = strtod(yytext, NULL); return NUMBER; }
 
[-+*/^()]        return *yytext;
 
{NL}             return NL;
{WS}             ; /* ignore whitespace */
 
.                yyerror("Unknown character");
 
%%

Vejamos o que foi introduzido aqui. Na primeira linha temos a seguinte instrução,

%option outfile="CalculatorScanner.c"

cujo propósito é simplesmente indicar ao FLEX que o nome do ficheiro que queremos que ele gere é CalculatorScanner.c e não o default lex.yy.c. Em seguida abrimos um bloco de código C com %{, e fechamos-lo mais tarde com %}. Podemos utilizar estes blocos para inserirmos código C/C++ como usual. Inserir código C/C++, incluindo comentários, fora destes blocos não vai funcionar como esperado a menos que todo esse código esteja indentado.

#include "CalculatorParser.tab.h"

Este #include – que também era necessário na versão anterior mas foi omitido por motivos de simplificação — é um header criado pelo BYACC e que contém a union e os #defines mencionados anteriormente. A função yyerror() que se lhe segue, é uma função C normal, criada para lidar com erros no input. Veremos à frente como é usada.

WS          [ \t]
NL          "\n"|"\r"|"\r\n" 

Estas duas definições fazem respectivamente match a espaço em branco (quer sejam espaços ou tabs) e a mudanças de linha (quer sejam de Linux, Mac ou Windows). As aspas na definição NL são um metacaractere e indicam que queremos fazer match exactamente ao que está lá dentro. No exemplo da expressão regular para os reais, quando usámos a expressão \. para dizer que queríamos o caractere ponto, podíamos ter usado a expressão "." para o mesmo efeito, tal como aqui podíamos ter usado a expressão \\n|\\r|\\r\\n em vez da que usamos.

[-+*/^()]        return *yytext;

Esta classe de caracteres apanha todas as operações que a nossa calculadora vai realizar, somas, subtracções, multiplicações, divisões e potências, tal como parênteses para podermos criar agrupamentos. Neste caso retornamos o caractere directamente ao parser, algo que podemos fazer sempre que o input ao qual queremos fazer match é só um caractere.

{NL}             return NL;

No caso duma mudança de linha não existe nenhum valor que nos interesse retornar ao parser, portanto basta-nos informar o parser sobre o acontecimento. A necessidade de efectuar este retorno tornar-se-á mais óbvia quando discutirmos o parser.

{WS}             ; /* ignore whitespace */

O espaço em branco não interessa à nossa calculadora, mas queremos permitir que o utilizador o possa usar, portanto usamos uma instrução vazia como acção, causando com que o scanner o ignore.

.                yyerror("Unknown character");

Esta última regra destina-se a lidar com input inválido. A primeira consideração a tomar é que esta regra necessita de estar em último lugar porque o ponto faz match a tudo. A forma como o FLEX funciona é a seguinte, dado input que pode ser aceite por duas regras, o FLEX usa a que aceita mais input. Se as duas aceitarem a mesma quantidade o FLEX usa a que vem primeiro no ficheiro. Tomemos o seguinte caso:

%%
 
[0-9]{3}     printf("número com 3 algarismos");
[0-9]*       printf("número com n algarismos");
 
%%

Para o input 123456 o output do scanner vai ser número com n algarismos e não número com 3 algarismosnúmero com 3 algarismos como seria se ele usasse a primeira regra duas vezes. A razão disto é que a segunda regra aceita mais do input que a primeira, como tal neste caso, é essa que o FLEX usa. Para o input 123 no entanto, o output vai ser número com 3 algarismos, já que, aceitando as duas regras a mesma quantidade do input, o FLEX usa a que aparece primeiro, [0-9]{3}.

No caso do ponto, ele vai apanhar todo o input que as regras que o precedem não aceitem. Isto resulta porque o ponto só aceita um caractere individual. Se em vez de . tivéssemos escrito .* ele usaria esta regra em detrimento de todas as outras (excepto as que contivessem \n) independentemente de onde a colocássemos no ficheiro. Percebendo em que casos é que esta regra é utilizada, basta decidir como queremos lidar com o input inválido. No nosso ficheiro estamos a lançar um erro e a parar a execução mas podíamos só ignorar o input inválido da mesma forma que os espaços, ou emitir só um aviso. Com isto concluímos o scanner para a nossa calculadora e este artigo. No próximo artigo veremos BYACC, e como implementar o parser.