Logo Passei Direto
Buscar
Material
páginas com resultados encontrados.
páginas com resultados encontrados.

Prévia do material em texto

LINGUAGENS 
FORMAIS E 
AUTÔMATOS
OBJETIVOS DE APRENDIZAGEM
 > Conceituar as fases de análise léxica e sintática em processadores de lin-
guagens.
 > Apresentar as ferramentas Lex e Yacc.
 > Demonstrar a construção de um processador de linguagem utilizando Lex e Yacc.
Introdução
Uma aplicação da teoria de linguagens formais é o reconhecimento de linguagens, 
o que envolve os processos de análise léxica e análise sintática. Nesse âmbito, 
duas ferramentas são bastante usadas, Flex e Bison, que são versões atualizadas 
de duas ferramentas clássicas, Lex e Yacc, aplicadas respectivamente para gerar 
analisadores léxicos e sintáticos. 
Neste capítulo, você vai compreender os conceitos de análise léxica e análise 
sintática e seu importante papel no processo de reconhecimento e processa-
mento de linguagens. Você estudará ferramentas dedicadas a essas análises e 
acompanhará um exemplo de sua aplicação para construir um interpretador para 
uma linguagem de programação simplificada.
Processos de análise léxica e sintática 
No contexto de linguagens de programação, um tradutor é um sistema, se-
gundo Price e Toscani (2001), que aceita como entrada um programa escrito 
em certa linguagem de programação (linguagem-fonte) e que produz como 
resultado um programa equivalente em outra linguagem (linguagem-objeto). 
A esse programa damos o nome de compilador.
Estudo de caso
Rafael Leal Martins
O processo de tradução feito por um compilador é composto de várias 
fases, conforme ilustrado na Figura 1. 
Figura 1. As fases de um compilador.
Fonte: Aho et al. (2007, p. 4). 
Segundo (AHO et al., 2007, p. 3): 
A parte de análise subdivide o programa-fonte em partes constituintes e impõe 
uma estrutura gramatical sobre elas. Depois, usa essa estrutura para criar uma 
representação intermediária do programa-fonte. Se a parte de análise detectar que 
o programa-fonte está sintaticamente mal formado ou semanticamente incorreto, 
então ele precisa oferecer mensagens esclarecedoras, de modo que o usuário 
possa tomar a ação corretiva. A parte de análise também coleta informações sobre 
o programa-fonte e as armazena em uma estrutura de dados chamada tabela de 
símbolos, que é passada adiante junto com a representação intermediária para a 
parte de síntese. A parte de síntese constrói o programa-objeto desejado a partir 
da representação intermediária e das informações na tabela de símbolos.
Estudo de caso2
O processo de reconhecimento de uma linguagem é chamado de aná-
lise. A análise divide a entrada em símbolos básicos (tokens) e impõe uma 
estrutura gramatical sobre eles (AHO et al., 2007). Como a primeira fase de 
um reconhecedor de linguagem, a principal tarefa da análise léxica é ler os 
caracteres de entrada do programa de origem, agrupá-los em lexemas e 
produzir como saída uma sequência de tokens para cada lexema na entrada. 
O fluxo de tokens é enviado para análise sintática. 
As análises léxica e sintática são complementares, o que muitas vezes 
nos permite simplificar pelo menos uma dessas tarefas. Numa linguagem 
de programação, por exemplo, comentários são irrelevantes na análise do 
código-fonte. O analisador léxico pode lidar com comentários e outras en-
tradas irrelevantes removendo-as, simplificando o trabalho realizado pelo 
analisador sintático. Quando estamos projetando uma nova linguagem, se-
parar preocupações léxicas e sintáticas pode levar a um design de linguagem 
geral mais limpo.
Análise léxica
Essa primeira fase de um compilador, a análise léxica, também é chamada de 
scanning. O analisador léxico (ou scanner) faz a leitura do fluxo de caracteres 
que compõem o programa-fonte e os agrupa de acordo com seu significado 
dentro do programa. As sequências de caracteres reconhecidos pelo ana-
lisador léxico são chamadas de lexemas. Para cada lexema reconhecido, o 
analisador léxico produz como saída um token no formato , que é passado para a próxima fase, a análise sintática. O 
nome-token é um símbolo abstrato que identifica o tipo de símbolo dentro 
do programa, e será usado posteriormente durante a análise sintática. O 
segundo componente, valor-atributo, aponta para uma entrada em uma 
tabela de símbolos referente a esse token. A informação da entrada da tabela 
de símbolos é necessária para a fase de análise semântica e posteriormente 
para a geração de código. 
Exemplo
Considere uma típica operação de atribuição em uma linguagem de 
programação:
X = X + valor * 50
Um analisador léxico tipicamente poderia fazer os seguintes 
reconhecimentos:
Estudo de caso 3
1. X seria reconhecido como um identificador (nome de variável ou 
função) e seria mapeado para o token , em que ID repre-
sentaria o tipo do símbolo encontrado (ID: identificador) e 1 
seria a posição desse token na tabela de símbolos. O identificador é 
armazenado na tabela de símbolos, pois representa uma variável, uma 
posição de memória específica que provavelmente será referenciada 
novamente no código.
2. O operador = seria reconhecido como um token , para indicar 
que esse caractere representa uma operação de atribuição a ser feita. 
Esse token não precisaria de um atributo, pois não são necessárias 
mais informações além da operação a ser realizada em si.
3. Novamente, o lexema X seria reconhecido como um identificador, 
mas como ele já estaria armazenado na tabela de símbolos, o token 
retornado seria novamente , pois está referenciando a mesma 
variável.
4. O caractere + seria reconhecido como um token .
5. A palavra valor seria mapeada para , pois trata-se de uma 
nova variável reconhecida. Logo, ela deve ser armazenada como uma 
nova entrada na tabela de símbolos. 
6. O caractere * seria reconhecido como .
7. A sequência 50 seria mapeada para um token , ou mesmo para um 
token ,a fim de representar uma constante numérica 
nessa expressão.
Os espaços entre os elementos da sentença seriam descartados pelo 
analisador léxico.
Análise sintática
A segunda fase do compilador é a análise sintática. O analisador sintático, ou 
parser, utiliza os tokens reconhecidos pelo analisador léxico para criar uma 
representação tipo árvore, que mostra a estrutura gramatical da sequência 
de tokens. Nessa árvore, cada nó interior representa uma operação, e os filhos 
do nó representam os argumentos da operação. A essa árvore damos o nome 
de árvore sintática. A árvore sintática é construída a partir de uma gramática 
que define a ordem dos tokens na formação das sentenças da linguagem. Se 
for possível criar a árvore sintática com a sequência de tokens reconhecidos 
pelo analisador léxico, a entrada é aceita, ou seja, o programa-fonte está de 
acordo com a estrutura da linguagem.
Estudo de caso4
Exemplo
Considere a seguinte gramática:
CMD → EXPRESSAO
EXPRESSAO → EXPRESSAO TERMO
EXPRESSAO → TERMO
TERMO → TERMO FATOR
TERMO → FATOR
FATOR → | 
Para a sentença X = X + valor * 50, o analisador léxico reconheceria a 
sequência de tokens: , , , , , 
e . O trabalho do analisador sintático é tentar criar uma árvore sintática 
para essa sequência de tokens. 
De acordo com a gramática recém-apresentada, o analisador sintático poderia 
montar a árvore sintática mostrada na Figura 2. Isso significa que a sequência 
de tokens é aceita pela análise sintática, ou seja, X = X + valor * 50 é uma 
instrução válida.
Figura 2. Árvore sintática para a instrução X = X + valor * 50.
Vejamos agora a sentença X + 10 = Y. Embora o analisador léxico conse-
guisse reconhecer os tokens , , , e , 
essa sequência não é capaz de produzir uma árvore sintática a partir da 
gramática proposta, o que torna essa instrução inválida de acordo com a 
sintaxe da linguagem. 
Estudo de caso 5
E as outras fases do compilador? O analisador semântico usa a árvore 
sintática e as informações na tabela de símbolos para verificar a 
consistênciado programa-fonte com a definição da linguagem. Uma parte 
importante da análise semântica é a verificação de tipos de dados, em que o 
compilador verifica se cada operador possui operandos compatíveis. Como 
muitas linguagens de programação exigem que um índice de um array seja um 
número inteiro, o compilador precisa informar um erro de tipo se um número 
de ponto flutuante for usado para indexar um array. Outra verificação seria, 
numa divisão, verificar se o denominador tem valor igual a zero e alertar o 
programador, a fim de evitar um erro de execução. 
Depois da análise do programa-fonte, muitos compiladores geram uma 
representação intermediária que pode ser facilmente produzida e traduzida 
para a máquina-alvo. A fase de otimização aplica certas transformações no 
código intermediário, visando a produção de um melhor código-objeto. Nesse 
caso, segundo Aho et al. (2007), melhor normalmente significa mais rápido, 
menor ou que consume menos energia. Na fase de geração de código, os códigos 
intermediários são traduzidos em sequências de instruções de máquina que 
realizam a mesma tarefa proposta nas instruções da linguagem-fonte.
Flex e Yacc: ferramentas para gerar 
analisadores léxicos e sintáticos
Na década de 1950, os primeiros compiladores usavam técnicas totalmente 
ad hoc para analisar a sintaxe do código-fonte dos programas que estavam 
compilando. Durante a década de 1960, o campo recebeu muita atenção 
acadêmica, e no início da década de 1970 a análise sintática era um campo 
bem estudado (LEVINE, 2009).
Um dos principais conceitos foi dividir o trabalho em duas partes: análise 
léxica (lexing ou scanning) e análise sintática (parsing). Relembrando: a aná-
lise léxica divide a entrada em pedaços significativos, chamados tokens, e a 
análise sintática verifica como os tokens se relacionam entre si.
Os analisadores léxicos geralmente funcionam procurando padrões de 
caracteres na entrada. Em linguagens de programação típicas, por exemplo, 
uma constante de inteiro é uma sequência de um ou mais dígitos, um iden-
tificador é uma letra seguida por zero ou mais letras ou dígitos e os vários 
operadores são caracteres únicos ou pares de caracteres. Uma maneira 
simples de descrever esses padrões são expressões regulares.
Estudo de caso6
O trabalho do analisador sintático é descobrir a relação entre os tokens 
de entrada. Como já vimos, uma maneira comum de exibir tais relações é 
uma árvore sintática. Para escrever um analisador sintático, precisamos de 
uma maneira de descrever as regras que o analisador usa para transformar 
uma sequência de tokens em uma árvore sintática. Para isso, recorremos a 
uma gramática livre de contexto. Uma forma de descrever uma gramática é 
a forma de Backus-Naur.
Flex e Bison são ferramentas para construir programas que lidam com 
entradas estruturadas. O nome Flex vem da expressão Flexible Lexical Gene-
rator (Gerador Léxico Flexível). Já o nome Bison vem de uma brincadeira com 
o nome da ferramenta Yacc (Yet Another Compiler Compiler, ou Mais Outro 
Compilador de Compilador). Ocorre que o nome Yacc lembra o termo em 
inglês yak (iaque), um parente do bisão (bison, em inglês). Flex e Bison eram 
originalmente ferramentas para construir compiladores, mas se revelaram 
úteis em muitas outras áreas. 
Flex é um gerador de analisadores léxicos e Bison é um gerador de ana-
lisadores sintáticos. Ambos geram código na linguagem de programação C a 
partir de arquivos (ou programas) escritos em certos formatos.
Flex e Bison são substitutos modernos para as ferramentas clássicas 
Lex e Yacc, que foram desenvolvidas na Bell Laboratories na década 
de 1970. O Yacc foi o primeiro dos dois, desenvolvido por Stephen C. Johnson. Já 
o Lex foi projetado por Mike Lesk e Eric Schmidt para trabalhar com o Yacc. Tanto 
Lex quanto Yacc têm sido utilitários Unix padrão desde a sétima edição Unix, 
na década de 1970. O Flex pode ser obtido em https://github.com/westes/flex/
releases, enquanto o Bison está disponível em http://www.gnu.org/software/
bison/. Ambos são ferramentas UNIX/Linux clássicas e também podem ser 
instaladas por meio de repositórios de pacotes de várias distribuições Linux.
Usuários Windows podem usar máquinas virtuais Linux. Também pode-se 
optar por instalar o ambiente Cygwin e compilar os códigos-fonte do Flex e do 
Bison diretamente no Windows. Especificamente para usuários do Windows 10, 
ainda há uma alternativa mais fácil: instalar uma distribuição Linux no Subsistema 
Windows para Linux, que é um módulo do sistema operacional Windows 10 que 
visa disponibilizar um ambiente Linux compatível no sistema da Microsoft sem a 
necessidade de emuladores ou do uso de máquinas virtuais. Na distribuição Linux 
Ubuntu, por exemplo, para instalar o Flex e o Bison basta digitar os seguintes 
comandos no terminal (é necessário confirmar a instalação dos pacotes):
sudo apt update
sudo apt install flex bison
Estudo de caso 7
Um arquivo Bison consiste em um arquivo de texto contendo uma definição 
de uma gramática livre de contexto na forma Backus-Naur. Os tokens são os 
símbolos terminais dessa gramática e são definidos também no arquivo Bison. 
Além de reconhecer os símbolos pelas produções gramaticais, é possível 
definir ações a serem realizadas quando uma produção gramatical é reconhe-
cida com sucesso. Uma ação pode ser usada para gerar código equivalente à 
instrução na linguagem-alvo ou pode ser usada para executar instruções na 
linguagem C relativas à sentença reconhecida pela gramática. Esse processo 
se chama tradução dirigida pela sintaxe, e é muito usado na construção de 
compiladores e interpretadores. No nosso exemplo, a ação será usada para 
realizar a operação aritmética relativa à expressão reconhecida. A ferramenta 
Bison lê o arquivo contendo o arquivo Bison (recomenda-se utilizar a extensão 
.y para arquivos Bison) e gera o código em C para um analisador (e tradutor) 
sintático para a gramática definida. Ele também pode gerar um arquivo-fonte 
com extensão .tab.c, além do arquivo de cabeçalho com extensão .tab.h para 
integração com o analisador léxico gerado pelo Flex posteriormente.
Um arquivo Bison consiste em três seções, separadas por linhas contendo 
apenas %%:
 � a primeira seção contém declarações;
 � a segunda seção é uma lista de padrões e ações;
 � a terceira seção é o código C que é copiado para o analisador léxico 
gerado (geralmente funções relacionadas ao código nas ações). 
Na primeira seção, a seção de declaração, o código pode ser inserido dentro 
de um bloco iniciando com %{ e finalizando com %}. Esse código é inserido 
início do arquivo fonte C gerado. Nessa seção, também definimos quais são 
os tokens usados pela linguagem, bem como sua ordem de precedência e 
associatividade (muito importante no caso de operadores aritméticos).
Na segunda seção, inserimos as produções gramaticais que representam 
a sintaxe da linguagem a ser analisada. Essas produções são escritas em uma 
variação da forma Backus-Naur e têm o seguinte formato:
 : { ação (opcional)} 
onde é uma sequência de um ou mais caracteres que representa um 
símbolo não terminal da gramática da linguagem. é uma sequ-
ência de símbolos, tokens ou não terminais, que são derivados a partir no 
símbolo proposto. A definição da ação é opcional, mas permite definir 
um bloco de instruções em C para realizar a tradução desejada para aquela 
produção gramatical. 
Estudo de caso8
Cada símbolo numa produção gramatical de Bison tem um valor. Assim, 
o valor do símbolo de destino (à esquerda dos dois pontos) é chamado de 
$$ no código de ação, e os valores à direita são numerados $1, $2, e assim 
por diante, até o número de símbolos contidos na produção. Os valores dos 
tokens são o que estava em yylval quando o scanner retorna o token; os 
valores de outros símbolos são definidos em regras no analisador sintático. 
Para uma produção no formato X : Y, o Bison irá automaticamente realizar 
a ação $$ = $1, ou seja, ovalor do símbolo Y ($1) será copiado para o 
símbolo não terminal X ($$). 
A terceira seção do arquivo Bison permite definir uma função principal 
(main) para chamar o analisador sintático por meio de uma chamada à função 
yyparse()gerada pelo Bison. Também podemos definir ações para outras 
funções, como a função yyerror(char *s), que é chamada quando um erro 
sintático é detectado. 
Um programa Flex consiste basicamente em uma lista de expressões 
regulares com instruções sobre o que fazer quando a entrada corresponde a 
qualquer uma delas (LEVINE, 2009). O Flex gera como saída um arquivo-fonte 
escrito na linguagem C contendo o código para um analisador léxico que 
reconhece os tokens para os padrões de lexemas que você definir por meio de 
expressões regulares. Um analisador léxico gerado pelo Flex lê os caracteres de 
entrada (do console ou de um arquivo), tentando estabelecer correspondência 
entre as sequências de caracteres lidos com essas expressões regulares e 
fazendo a ação apropriada quando um padrão for correspondido. 
Grande parte dos programas com analisadores léxicos gerados pelo Flex 
recorre a esse analisador para retornar um fluxo de tokens que são manu-
seados por um analisador sintático (que em grande parte das vezes é gerado 
pelo Bison, mas não necessariamente). Cada vez que o programa precisa de 
um token, ele chama a função yylex(), que lê uma sequência de caracteres 
da entrada e retorna o token. Quando o analisador sintático precisa de outro 
token, ele chama yylex() novamente. O analisador léxico age como uma 
função (ou corrotina), ou seja, cada vez que retorna um token, ele recorda de 
onde estava na leitura da entrada, e na próxima chamada retoma do ponto 
onde parou.
Dentro do analisador léxico, quando o código de ação tem um token pronto, 
ele apenas o devolve como o valor de yylex(). Da próxima vez que o programa 
chamar yylex(), ele retoma a varredura com os próximos caracteres de 
entrada. Por isso, neste nosso exemplo, analisaremos primeiro o programa 
em Bison para depois analisarmos o programa em Flex, mas isso é apenas 
para facilitar a compreensão. Você pode escrevê-los em qualquer ordem.
Estudo de caso 9
Um programa Flex consiste em três seções, separadas por linhas %%:
 � a primeira seção contém declarações e configurações de opções;
 � a segunda seção é uma lista de padrões e ações;
 � a terceira seção é o código C que é copiado para o analisador léxico 
gerado (geralmente funções relacionadas ao código nas ações). 
Na primeira seção, a seção de declaração, o código pode ser inserido 
dentro de um bloco iniciando com %{ e finalizando com %}. Esse código é 
inserido no início do arquivo-fonte C gerado.
Na segunda seção, cada padrão definido deve ser escrito no início de uma 
linha, seguido do código em C para executar uma ação quando o padrão for 
correspondido. O código C pode ser apenas uma instrução ou um bloco de 
instruções entre chaves, { }. Cada padrão deve começar no início de uma 
linha no arquivo, uma vez que o Flex considera qualquer linha que comece 
com espaço branco a ser codificada no código C gerado pelo Flex.
Construção de uma calculadora simples 
usando Bison e Flex
O primeiro programa que escreveremos usando Flex e Bison é uma calculadora 
simples. Primeiro escreveremos um analisador sintático com Bison, depois 
escreveremos um analisador léxico usando Flex e por fim combinaremos os 
dois.
Para simplificar, começaremos reconhecendo apenas inteiros e os quatro 
operadores aritméticos básicos. Para a calculadora proposta, podemos usar 
o seguinte arquivo Bison (chamado calculadora.y):
/* calculadora de expressoes */
%{
#include 
%}
/* declarar tokens */
%token NUMERO
%left SOMA SUB 
Estudo de caso10
%left MUL DIV 
%token EOL
%%
calclist: calclist expressao EOL { printf("= %d\n", $2); } 
| /*Quando nada e definido, assume-se epsilon*/ 
;
expressao: NUMERO /* default $$ = $1 */
| expressao SOMA expressao { $$ = $1 + $3; }
| expressao SUB expressao { $$ = $1 - $3; }
| expressao MUL expressao { $$ = $1 * $3; }
| expressao DIV expressao { $$ = $1 / $3; }
;
%%
main(int argc, char **argv)
{
yyparse();
}
yyerror(char *s)
{
fprintf(stderr, "Erro: %s\n", s);
}
A linha 01 contém um comentário. Tudo que for escrito entre /* e */ 
influenciará a execução do analisador sintático gerado. 
Na linha 02, inicia-se o bloco %{. Dentro dele, quaisquer declarações 
necessárias para o programa final devem ser inseridas, como inclusão de 
bibliotecas e declarações de variáveis globais. O código C digitado aqui será 
transcrito para o código gerado pelo Bison.
Estudo de caso 11
Na linha 03, incluímos a biblioteca C stdio.h para podermos usar a função 
de impressão em tela printf mais à frente. Note que essa linha está contida 
dentro do bloco %{ e }%, que é fechado na linha 04. 
Na linha 06, a declaração %token NUMERO define um token NUMERO para 
representar os valores numéricos inteiros avaliados nas expressões. Repare 
que ainda não estamos definindo quais caracteres formam um token NUMERO. 
Isso é função do analisador léxico, que será construindo posteriormente.
As linhas 07 e 08 definem os tokens dos operadores aritméticos, mas 
observando uma ordem de associatividade e procedência. Na linha 07, a 
declaração %left SOMA SUB define esses dois tokens para representar as 
instruções de soma e subtração, respectivamente, e os define como associa-
tividade à esquerda (left). Logo, uma expressão 5 + 6 – 7 será interpretada 
como ((5+6)+7). Algo similar é feito na linha 08, só que para operadores de 
multiplicação e divisão (tokens MUL e DIV, respectivamente). A precedência 
dos operadores é definida por ordem de declaração: tokens definidos primeiro 
têm menos precedência que tokens definidos em linhas seguintes. Nesse 
caso específico, MUL e DIV têm mais precedência que SOMA e SUB, pois foram 
declarados depois destes. 
Na linha 09, um token EOL é definido para representar o final de uma 
linha digitada. A calculadora proposta permitirá avaliar várias expressões 
aritméticas, uma por linha.
A linha 10 (%%) marca a separação da primeira e da segunda seção do 
arquivo Bison.
Na linha 11, temos a primeira produção gramatical: 
calclist: calclist expressao EOL { printf("= %d\n", $2); } 
Os símbolos não terminais não precisam ser declarados, bastando que 
haja uma produção em que esses símbolos apareçam antes dos dois pontos. 
Em seguida, temos a sequência de símbolos que pode ser derivada a partir 
do não terminal. Nesse caso, calclist pode derivar calclist expressao 
EOL. Numa gramática livre de contexto, um não terminal pode produzir uma 
sequência contendo quaisquer outros símbolos não terminais ou terminais 
(tokens). Também é possível uma derivação vazia (ε — épsilon). Nessa produção 
específica, calclist gera outro calclist, o que permite que a sentença 
derive outra sequência em um comportamento de repetição (loop). Isso foi 
feito aqui para permitir que várias expressões sejam avaliadas, uma a cada 
linha (o fim da linha é marcado pelo token EOL). 
Depois dos símbolos da produção, temos o bloco de código. Essa é a ação 
a ser executada após a avaliação com sucesso dessa produção gramatical. 
Estudo de caso12
Dentro das chaves, você pode inserir código C para realizar alguma operação 
específica. Aqui, após o final de uma linha (token EOL), deseja-se imprimir o 
valor da expressão por meio do comando printf. Note que o valor impresso 
foi a variável $2, que contém o valor armazenado no segundo símbolo dessa 
produção gramatical, no caso o símbolo seria expressao. 
Na linha 12, temos uma segunda produção para calclist. O símbolo | 
(pipe) no início da linha é usado como um “ou” lógico para outras produções. 
Observe que a produção aqui consiste apenas nesse símbolo, isto é, significa 
uma produção ε, usada para finalizar o loop criado pela primeira produção 
de calclist.
Na linha 13, há apenas um ponto e vírgula (;), que marca o fim das produ-
ções para um símbolo não terminal.Nas linhas de 14 a 19, temos as produções gramaticais para o não terminal 
expressão, que pode ser gerado pelo não terminal calclist definido 
anteriormente. 
Não é permitido no Bison mencionar um símbolo não terminal em 
uma produção gramatical sem definir produções para esse símbolo. 
Tampouco é permitido mencionar um token (terminal) sem declará-lo na seção 
anterior. 
A primeira produção expressao: NUMERO define que uma expressão 
pode ser formada por apenas um número. A ação associada é copiar o valor 
do número ($1) para o não terminal expressao que o gerou ($$). Como 
essa é a ação-padrão numa produção assim, nenhum bloco de comandos é 
necessário (automaticamente $$ = $1). 
As demais produções (das linhas de 15 a 18) permitem a construção de 
expressões contendo um número indeterminado de termos com os operado-
res SOMA, SUB, MUL e DIV. Para cada operação aritmética avaliada, tem-se a 
ação de realizar a operação associada em C com os valores armazenados no 
primeiro e segundo operando ($1 e $3, respectivamente, pois $2 refere-se ao 
token do operador, cujo valor específico não é relevante aqui), armazenando 
o resultado no símbolo gerador da produção ($$). A linha 19 marca o fim das 
produções para expressao.
A linha 20 (%%) marca o início da terceira seção, onde define-se a função main 
em C (linha 21) para que ela chame o analisador sintático por meio da chamada 
yyparse() (linha 23). Isso permite que o compilador gere um executável e 
possamos executar a calculadora a partir do próprio console, por exemplo. 
Estudo de caso 13
Uma última definição é feita para a função yyerror(), nas linhas de 
25 a 28. Aqui definimos que, caso haja algum erro sintático, ele deve ser 
impresso na saída-padrão.
Agora que já temos o analisador sintático definido, criaremos um arquivo 
Flex, que chamaremos de calculadora.l (.l é a extensão recomendada para 
arquivos Flex) para gerar o analisador léxico para a calculadora proposta:
/* calculadora.l : reconhece os tokens para a calculadora*/
%{
#include "calculadora.tab.h" /*O nome do arquivo .tab.h deve 
ser o mesmo gerado pelo Bison (calculadora.y)*/
%}
%%
"+" { return SOMA; }
"-" { return SUB; }
"*" { return MUL; }
"/" { return DIV; }
[0-9]+ { yylval = atoi(yytext); return NUMERO; }
\n { return EOL; }
[ \t] { /* ignore whitespace */ }
. { printf("Caractere desconhecido %c\n", *yytext); }
%%
A estrutura de um arquivo Flex é muito similar à de um arquivo Bison. 
Na linha 01, também temos um comentário escrito que funciona da mesma 
forma que em um arquivo Bison. 
Na linha 02, inicia-se o bloco %{. Dentro dele, quaisquer declarações ne-
cessárias para o programa final devem ser inseridas, como a inclusão de 
bibliotecas e declarações de variáveis globais. 
Na linha 3, incluímos um arquivo de cabeçalho .h que o Bison criará para 
nós, que inclui ambas as definições dos tokens e para implementação de uma 
tabela para armazenamento dos tokens reconhecidos pelo analisador léxico da 
calculadora. O bloco de declarações é fechado na linha 4 com os símbolos }%.
Estudo de caso14
A linha 5 marca o início da segunda seção, onde serão feitas as definições 
dos lexemas a serem reconhecidos pela calculadora. A linha 6 reconhece o 
lexema + e reconhece (retorna) um token do tipo SOMA. Para reconhecer 
apenas um caractere ou mesmo uma sequência específica de caracteres, 
como palavras-chave da linguagem (palavras como imprima, leia, início, fim, 
etc.), deve-se colocar o lexema desejado entre aspas duplas. 
Nas linhas de 07, 08 e 09, são definidos os lexemas para os demais ope-
radores, retornando os respectivos tokens.
Os tokens retornados pelo analisador léxico devem ser idênticos 
aos declarados na primeira seção do arquivo Bison. Se o token não 
for definido, um erro ocorrerá e o Flex não conseguirá gerar o código para o 
analisador léxico.
Na linha 10, temos a definição dos lexemas para números inteiros. A ex-
pressão [0-9]+ significa ocorrência de um ou mais dígitos de 0 até 9. A ação 
executada converterá a sequência de caracteres armazenada em yytext 
em um valor inteiro (usando a função em C atoi) e armazenará esse valor na 
variável yylval. Isso permite armazenar o valor do número lido na entrada 
para sua avaliação nas regras do analisador sintático gerado pelo Bison.
A linha 11 reconhece uma quebra de linha (“\n”) retornando um token EOL.
A linha 12 faz o analisador léxico ignorar (descartar) espaços em branco 
e tabulações (“\t”). Note o espaço em branco após [ nessa linha e o bloco 
vazio (apenas com comentário) ao final.
Finalmente, na linha 13 temos a expressão . (ponto), que representa 
qualquer outro caractere não definido nas regras anteriores. Por não fazer 
parte das expressões propostas, a ação será apenas relatar o aparecimento 
de algum caractere desconhecido.
Testando a calculadora
Com os arquivos calculadora.y e calculadora.l prontos, podemos finalmente 
gerar os códigos dos analisadores sintático e léxico, compilá-los usando um 
compilador C e executar a calculadora.
Primeiro precisamos gerar o código do analisador sintático usando o 
comando:
bison -d calculadora.y
Isso gerará dois arquivos: calculadora.tab.h e calculadora.tab.c.
Estudo de caso 15
Em seguida, geraremos o código para o analisador léxico por meio do 
comando:
flex calculadora.l
Isso gerará o arquivo lex.yy.c
Finalmente, compilaremos o código usando o compilador (neste exemplo 
usamos o gcc):
gcc calculadora.tab.c lex.yy.c -lfl
É normal que algumas warnings possam aparecer, sem problemas. 
Por padrão, o gcc gerará o arquivo a.out contendo o executável do código, 
que pode ser executado gerando a saída mostrada na Figura 3. Podemos 
avaliar quantas expressões quisermos, uma em cada linha. Quando quisermos 
finalizar a entrada, digitamos CTRL-D.
Figura 3. Executando a calculadora feita com Flex e Bison.
Neste capítulo, você aprendeu sobre as análises léxica e sintática e sua 
importância para o reconhecimento e tradução de linguagens. É recomendável 
que você estude a documentação completa do Flex e do Bison para poder 
usar recursos ainda mais poderosos dessas ferramentas. 
Referências
AHO, A. V. et al. Compiladores: princípios, técnicas e ferramentas. 2. ed. São Paulo: 
Pearson, 2007. 
LEVINE, J. Flex & Bison. Sebastopol: O’Reilly Media, 2009.
PRICE, A. M. de A.; TOSCANI, S. S. Implementação de linguagens de programação: 
compiladores. 2. ed. Porto Alegre: Sagra Luzzatto, 2001.
Estudo de caso16
Leituras recomendadas
COOPER, K.; TORCZON, L. Construindo compiladores. 2. ed. Rio de Janeiro: Elsevier, 2014.
GRUNE, D. et al. Projeto moderno de compiladores. Rio de Janeiro: Campus, 2001.
LEVINE, J. R.; MASON, T.; BROWN, D. Lex & Yacc: Unix programming tools. Sebastopol: 
O´Reilly, 1995.
MENEZES, P. B. Linguagens formais e autômatos. 3. ed. Porto Alegre: Sagra-Luzzato; 
Instituto de Informática da UFRGS, 2000. (Série Livros Didáticos).
Os links para sites da web fornecidos neste capítulo foram todos 
testados, e seu funcionamento foi comprovado no momento da 
publicação do material. No entanto, a rede é extremamente dinâmica; suas 
páginas estão constantemente mudando de local e conteúdo. Assim, os editores 
declaram não ter qualquer responsabilidade sobre qualidade, precisão ou 
integralidade das informações referidas em tais links.
Estudo de caso 17

Mais conteúdos dessa disciplina