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.