Escolar Documentos
Profissional Documentos
Cultura Documentos
Livro
Livro
Ed. v1.0
i
Sumrio
Introduo
1.1
Linguagens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2
O que um Compilador? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3
1.4
Organizao de um Compilador . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.4.1
Anlise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.4.2
Sntese . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.5
1.6
1.7
Exemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.8
Concluso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Anlise Lxica
10
2.1
10
2.1.1
12
17
2.2.1
Expresses Regulares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
2.2.1.1
Expresses bsicas . . . . . . . . . . . . . . . . . . . . . . . . . .
18
2.2.1.2
Caracteres especiais + e ? . . . . . . . . . . . . . . . . . . . . . .
19
2.2.1.3
19
2.2.1.4
20
2.2.1.5
Outras caractersticas . . . . . . . . . . . . . . . . . . . . . . . .
20
2.2.1.6
Alguns exemplos . . . . . . . . . . . . . . . . . . . . . . . . . .
20
2.3
21
2.4
Uso do flex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
2.4.1
Formato da entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22
2.4.2
22
2.4.3
25
2.2
ii
2.6
3
30
30
2.5.1
A Linguagem Mini C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
2.5.2
31
Concluso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
Anlise Sinttica
37
3.1
Estrutura sinttica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
37
3.1.1
rvores de expresso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
38
3.2
40
3.3
41
3.3.1
Exemplo: Palndromos . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
42
3.3.2
Derivao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
3.3.3
44
3.3.4
rvores de Derivao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
45
3.3.5
Ambiguidade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
3.3.6
48
49
3.4
4
Anlise Semntica
50
iii
Prefcio
texto
Pblico lvo
estudantes
Mtodo de Elaborao
Financiamento da capes.
Contribuio
Erros e etc.
iv
Captulo 1
Introduo
O BJETIVOS DO CAPTULO
Ao final deste captulo voc dever ser capaz de:
Entender a funo e a estrutura geral de um compilador
Diferenciar interpretadores de compiladores
Compreender os motivos por que se estuda os compiladores
Este captulo uma introduo aos compiladores: o que so, para que servem, e como so organizados. Tambm discutimos para qu se aprende sobre compiladores e onde esse conhecimento pode ser
til. Compiladores so, essencialmente, tradutores de linguagens de programao. Por isso, vamos
comear a discusso falando sobre linguagens de programao em geral.
1.1
Linguagens
O que uma linguagem? Deixamos para os linguistas e filsofos a definio geral do que vem a
ser uma linguagem, nos seus vrios sentidos. Aqui nos preocupamos apenas com as linguagens de
programao, e daqui em diante quando se falar em linguagem ser entendido que uma linguagem
de programao; quando for preciso tratar de outro tipo de linguagem, isso estar explcito no texto.
Um programa uma seqncia de instrues que devem ser executadas por um computador. Em outras palavras, um programa especifica um algoritmo de maneira executvel. Uma linguagem de programao uma notao para escrever programas. Enquanto as linguagens naturais, como portugus,
so notaes para comunicao entre pessoas, as linguagens de programao existem, a princpio,
para que o programador comunique ao computador as tarefas que devem ser realizadas. A linguagem
deve ser, portanto, precisa; o computador no pode fazer julgamentos e resolver ambiguidades.
importante notar tambm que um programa freqentemente um instrumento de comunicao entre
programadores: comum que um deles tenha que ler e entender programas escritos por outro. Alguns
importantes cientistas da computao, alis, defendem que a comunicao entre programadores o
objetivo primrio de um programa, a sua execuo sendo praticamente um efeito colateral. Donald
Knuth sugeriu que programao a arte de dizer a outra pessoa o que se quer que o computador faa.
impossvel estudar compiladores sem estudar as linguagens de programao. J o contrrio possvel: podemos estudar linguagens sem conhecer nada sobre compiladores. Desta forma, as linguagens
1 / 50
1.2
O que um Compilador?
Como vimos, uma linguagem de programao uma notao para escrever programas. Em geral,
programas so escritos por pessoas para serem executados por computadores. Mas pessoas e computadores funcionam de forma diferente, o que leva existncia de linguagens de programao com
diferentes nveis. Os processadores que executam os programas de computador normalmente executam instrues simples e elementares. As linguagens de baixo nvel so aquelas mais prximas das
linguagens dos processadores. Essas linguagens, entretanto, so consideradas difceis de programar,
devido grande quantidade de detalhes que precisam ser especificados. Assim, algumas linguagens
foram criadas para tornar mais fcil a tarefa de programao de computadores. Essas linguagens so
chamadas de linguagens de alto nvel.
Para executar programas escritos em uma linguagem de alto nvel, entretanto, preciso traduzir esses programas para uma linguagem de baixo nvel que possa ser executada diretamente por alguma
mquina. O programa que faz essa traduo chamado de compilador.
Portanto, um compilador um programa que traduz programas escritos em uma linguagem, chamada
de linguagem-fonte, para outra linguagem, a linguagem-destino. Normalmente, a linguagem-fonte
uma de alto nvel, e a linguagem de destino uma linguagem de mquina de algum processador, ou
algum outro tipo de linguagem de baixo nvel que seja executada diretamente por uma plataforma
existente. O diagrama na Figura 1.1 [2] resume essa estrutura bsica.
programa
fonte
compilador
programa
destino
1.3
Mais uma vez, um compilador um tradutor cujo objetivo principal transformar um programa para
uma forma diretamente executvel. Esta no a nica maneira de executar programas em linguagens
de alto-nvel: uma alternativa traduzir e executar ao mesmo tempo. o que fazem os interpretadores. Um interpretador puro tem que analisar e traduzir o programa-fonte toda vez que ele precisa ser
executado.
2 / 50
3 / 50
1.4
Organizao de um Compilador
Na Figura 1.1 [2] a estrutura bsica de um compilador apresentada de uma forma muito simplificada. Agora consideramos essa estrutura em maiores detalhes. Em dcadas de desenvolvimento dos
compiladores, estabeleceram-se algumas tradies na forma de estrutur-los. Uma dessas tradies
separar o compilador em duas partes principais: a primeira analisa o programa-fonte para verificar
sua corretude e extrair as informaes necessrias para a traduo; a segunda utiliza as informaes
coletadas para gerar, ou sintetizar, o programa na linguagem de destino. o modelo de anlise e
sntese; a fase de anlise tambm chamada de vanguarda do compilador (front-end) e a de sntese
conhecida como retaguarda (back-end). Isso mostrado na Figura 1.3 [4].
fonte
sntese
anlise
destino
4 / 50
1.4.1
Anlise
caracteres
anlise lxica
tokens
anlise sinttica
rvore sinttica
anlise semntica
rvore com anotaes
gerao de cd. int.
cdigo intermedirio
Figura 1.4: Estrutura do mdulo de anlise.
A Figura 1.4 [5] mostra a estrutura do mdulo de anlise. O programa-fonte , inicialmente, um
conjunto de caracteres; a tarefa da fase de anlise lxica agrupar esses caracteres em palavras significativas para a linguagem, ou tokens. Em seguida, a anlise sinttica deve, atravs do conjunto e
ordem dos tokens, extrair a estrutura gramatical do programa, que expressa em uma rvore sinttica. A anlise semntica, ou anlise contextual, examina a rvore sinttica para obter informaes
de contexto, adicionando anotaes rvore com estas informaes. A fase final da anlise a transformao da rvore com anotaes, resultado de todas as fases anteriores, no cdigo intermedirio
necessrio para a sntese.
5 / 50
1.4.2
Sntese
cdigo intermedirio
otimizao de cdigo
cdigo otimizado
gerao de cdigo
cdigo de destino
otimizao
cdigo final
Figura 1.5: Estrutura do mdulo de sntese.
O mdulo de sntese detalhado na Figura 1.5 [6]. Primeiro, o cdigo intermedirio recebido do
mdulo de anlise otimizado; o objetivo tornar o cdigo gerado mais eficiente no uso do tempo
e/ou do espao. Depois, o cdigo intermedirio otimizado utilizado para gerar cdigo na linguagem
de destino, geralmente a linguagem de mquina de alguma arquitetura. O cdigo de destino gerado
ainda pode ser passado por mais uma fase de otimizao, chegando enfim ao cdigo final gerado
pelo compilador. Dependendo da arquitetura, tambm pode ser preciso colocar o cdigo final em um
formato adequado para ser executado (no mostrado na figura).
Existem mais dois componentes dos compiladores que no so fases do mdulo de anlise nem do de
sntese. Estes so a tabela de smbolos e o sistema de tempo de execuo. A tabela de smbolos usada
por praticamente todas as fases do compilador; durante o processamento do programa fonte, muitos
smbolos nomes de variveis, funes, classes, mdulos e outras construes da linguagem so
definidos e referenciados. A tabela de smbolos guarda as informaes sobre cada um deles (por
exemplo, o tipo de um smbolo que o nome de uma varivel). O sistema de tempo de execuo, como
j mencionado, composto por vrios servios que existem para suportar a execuo dos programas
gerados. Um exemplo de tarefa realizada pelo sistema de tempo de execuo o gerenciamento de
memria, tanto da memria alocada na pilha quanto da memria alocada dinamicamente. Sempre que
existe, em um programa em C, uma chamada a malloc ou free, o sistema de tempo de execuo
invocado para administrar o heap. Na mquina virtual Java o sistema de tempo de execuo inclui o
coletor de lixo, que faz o gerenciamento automtico da memria alocada dinamicamente.
6 / 50
1.5
O estudo das linguagens de programao uma das reas principais da cincia da computao. Como
os compiladores so, a rigor, implementaes de linguagens de programao, sua importncia fica automaticamente estabelecida. As situaes encontradas por cientistas da computao requerem algum
entendimento sobre a implementao das linguagens que ele usa, mesmo que ele nunca implemente
um compilador em sua carreira.
Mas h outros motivos que tornam este estudo importante e interessante. Aprender sobre compiladores til, pois os algoritmos e estruturas de dados utilizados so aplicveis em vrios outros
contextos. Compreender como as linguagens so implementadas tambm confere ao programador
um maior conhecimento sobre elas, e quais os custos envolvidos no uso de suas caractersticas. Isso
permite tomar melhor decises sobre que linguagem usar para um determinado problema; um profissional competente deve sempre, dentro das restries apresentadas, escolher a melhor linguagem para
cada problema.
O estudo tambm interessante do ponto de vista terico, pois os compiladores interagem com vrias
outras reas centrais da computao, demonstrando um timo exemplo de sintonia entre teoria e
prtica:
Teoria da Computao Como o compilador um programa, a teoria da computao nos permite
prever que tipo de anlises podem ser feitas, e quais so possveis mas a um custo muito alto
(problema NP)
Linguagens Formais e Autmatos Os formalismos empregados na anlise sinttica vm do
estudo dessa rea
Arquitetura de Computadores importante para entender as interaes entre o cdigo gerado
e a mquina que executa o programa, e qual o impacto dessas interaes na eficincia do programa
gerado
Paradigmas de programao Permite entender os diferentes modelos semnticos utilizados nas
linguagens que devem ser traduzidas
natural esperar que poucos profissionais da rea da computao precisem, algum dia, escrever um
compilador para uma linguagem de propsito geral. Entretanto, uma tendncia atual no desenvolvimento de software usar Linguagens de Domnio Especfico para dividir a soluo de um problema
em partes gerais e partes especficas. Como mencionado antes, o uso de LDEs traz vantagens expressivas na criao de solues, diminuindo a distncia entre a linguagem de programao utilizada e os
conceitos do domnio do problema. Alguns profissionais e pesquisadores da rea j propem, hoje,
um paradigma chamado de Programao Orientada s Linguagens (ou Language-Oriented Programming, em ingls), que consiste em sempre criar linguagens especficas para cada sistema desenvolvido.
Isso enfatiza a necessidade de se educar sobre linguagens de programao e sua implementao.
Por fim, como ferramentas que traduzem de uma linguagem para outra, os compiladores tambm
so mais abrangentes do que parecem a princpio. Um exemplo na rea de banco de dados so os
programas que compilam buscas a partir de uma especificao SQL: eles traduzem da linguagem de
consulta para um conjunto de operaes em arquivos que so as primitivas do banco de dados. Tcnicas usadas pelos compiladores tambm so empregadas para resolver dependncias entre equaes
em planilhas como Excel. Como estes, existem vrios outros exemplos, em praticamente todas as
reas da computao, onde as tcnicas estudadas nesta disciplina so utilizadas. Alguns exemplos so
mostrados a seguir.
7 / 50
1.6
As tcnicas usadas na implementao dos compiladores encontram aplicao em muitos outros problemas que envolvem a anlise de uma linguagem de entrada ou a traduo de informaes de um
formato para outro.
Algumas aplicaes esto relacionadas a outras tarefas envolvendo linguagens de programao. Por
exemplo, editores de cdigo e IDEs para programao precisam analisar o cdigo para sinalizar erros,
sugerir melhorias e fornecer outras ferramentas para auxiliar na programao. Outro exemplo so as
ferramentas de anlise esttica, que podem analisar o cdigo-fonte de um programa para descobrir
erros ou condies de falha, sugerir melhorias no cdigo ou gerar testes automaticamente.
Outras aplicaes que usam das mesmas tcnicas dos compiladores so relacionadas anlise de alguma linguagem ou formato de dados de entrada. Um exemplo so aplicaes que precisam obter
informaes que esto em alguma pgina na Web, no formato HTML. Essas aplicaes podem usar
um analisador sinttico de HTML para mais facilmente obter as informaes procuradas. De forma
similar, algumas aplicaes armazenam dados em algum formato derivado de XML, e usar um analisador sinttico de XML pode ajudar bastante a acessar as informaes dessas aplicaes. Alm de
formatos padronizados como HTML e XML, muitas aplicaes usam vrios outros formatos proprietrios. Saber usar as tcnicas de anlise sinttica usadas em compiladores torna tarefas como essas
muito mais simples.
Existem tambm classes de aplicaes que precisam analisar textos escritos em alguma linguagem
natural, como a lngua portuguesa. Embora um texto em portugus seja bem mais difcil de analisar do
que um cdigo-fonte escrito em alguma linguagem de programao, as tcnicas bsicas e os conceitos
envolvidos so similares. Muitas aplicaes de anlise de linguagen natural so usadas hoje em dia
nas redes sociais. Um exemplo: comits de campanha eleitoral de um candidato podem coletar o que
as pessoas esto falando sobre o candidato nas redes sociais, e determinar automaticamente (sem que
algum precise ler todas as mensagens) se a maioria est falando bem ou mal dele. Com anlises mais
detalhadas, possvel tentar determinar que pontos positivos e negativos esto sendo comentados; essa
uma tarefa normalmente chamada de anlise de sentimento. Aplicaes de traduo automtica
de textos em uma lngua para outra lngua (como o servio de traduo do Google) tambm usam
algumas tcnicas que so similares s utilizadas em compiladores.
1.7
Exemplos
Existem vrios compiladores que so utilizados no dia-a-dia pelos programadores. Para as linguagens
C e C++ a coleo de compiladores GCC (GNU Compiler Collection) muito utilizada, sendo o
compilador padro em muitas IDEs de programao como o Dev-C++.
Na plataforma Windows, a ferramenta de programao nativa mais utilizada a IDE Visual Studio,
que inclui compiladores para vrias linguagens: para C e C++ o compilador usado o cl.exe,
enquanto que para C# o compilador o csc.exe.
A linguagem Java possui um sistema de compilao mais complexo, mas o compilador principal
do JDK (Java Development Kit) o javac, que compila cdigo Java para bytecodes da Mquina
Virtual Java. A Mquina Virtual Java traduz o cdigo em bytecodes para cdigo nativo no momento
da interpretao, usando um compilador JIT (Just In Time). Outro compilador comumente usado por
programadores Java o compilador incremental includo como parte da IDE Eclipse.
8 / 50
1.8
Concluso
Este captulo serviu como um primeiro contato com as ideias e tcnicas envolvidas na implementao
de compiladores. Vimos o que so linguagens de programao e o que um compilador, alm da
estrutura geral de um compilador e como ela dividida primariamente nas etapas de anlise e sntese. Essas duas etapas so, por sua vez, divididas em sequncias de fases que efetuam tarefas bem
definidas. Os captulos seguintes iro detalhar as tcnicas necessrias em cada uma dessas fases.
9 / 50
Captulo 2
Anlise Lxica
O BJETIVOS DO CAPTULO
Ao final deste captulo voc dever ser capaz de:
Entender a funo do analisador lxico dentro de um compilador
Descrever a estrutura lxica de uma linguagem usando expresses regulares
Criar o analisador lxico para uma linguagem usando um gerador de analisadores
A anlise lxica a primeira etapa do compilador, e recebe o arquivo de entrada criado pelo usurio.
O arquivo de entrada geralmente armazenado como uma sequncia de caracteres individuais que
podem ser lidos. A anlise lxica tem como funo agrupar os caracteres individuais em tokens, que
so as menores unidades com significado no programa-fonte. Um token pode ser pensado como sendo
similar a uma palavra.
Podemos fazer uma analogia do processo de anlise do compilador com o ato de ler um texto. Na
leitura, ns no lemos e decodificamos individualmente cada letra; nosso crebro l e processa um
texto uma palavra por vez. Isso comprovado pelo fato que conseguimos entender um texto mesmo
que as palavras tenham erros de ortografia ou mesmo sejam escritas de maneira diferente.
A anlise lxica faz com que as etapas seguintes do compilador possam trabalhar no nvel das palavras, ao invs do nvel dos caracteres individuais. Isso facilita bastante o trabalho das etapas posteriores. Na anlise lxica tambm so realizadas algumas tarefas como remover comentrios dos arquivos
do programa-fonte e registrar em uma tabela os nomes de identificadores usados no programa. Os
detalhes de como isso feito so o tpico deste captulo.
2.1
Como vimos, a anlise lxica agrupa os caracteres do arquivo de entrada (que contm o programafonte) em tokens. Um token similar a uma palavra do texto de entrada e composto por duas partes
principais:
1. um tipo;
2. um valor opcional.
10 / 50
Neste exemplo, todos os tokens de tipo nmero so formados por mais de um caractere, o maior tendo
cinco caracteres (20925). O analisador lxico gera para esse exemplo a seguinte sequncia de tokens:
Tabela 2.1: Sequncia de tokens para o exemplo
Lexema
42
+
(
675
*
31
)
20925
Tipo
Nmero
Operador
Pontuao
Nmero
Operador
Nmero
Pontuao
Operador
Nmero
Valor
42
SOMA
PARESQ
675
MULT
31
PARDIR
SUB
20925
11 / 50
2.1.1
Para uma linguagem simples como a linguagem de expresses aritmtica do exemplo, escrever um
programa que faz a anlise lxica no apresenta grande dificuldade. Nesta seo vamos examinar
as partes mais importantes do analisador lxico para essa linguagem, pois vrios elementos sero
similares para linguagens mais complexas.
O cdigo fonte completo do analisador lxico para a linguagem de expresses pode ser encontrado
no seguinte arquivo:
Cdigo fonte code/cap2/exp_lexer.c
Aqui vamos analisar as principais partes deste programa. Comeamos com a definio da estrutura
que vai guardar os tokens:
Definio de estrutura para tokens
typedef struct
{
int tipo;
int valor;
} Token;
Como vimos, um token tem dois campos: o tipo do token e um valor associado. Ambos os campos
so inteiros, ento definimos algumas constantes para representar os valores possveis desses campos.
As primeiras constantes especificam o tipo de token:
Constantes que representam o tipo do token
#define TOK_NUM
#define TOK_OP
#define TOK_PONT
0
1
2
Com relao ao valor, para nmeros o valor do token apenas o valor do nmero encontrado. Para
operadores e pontuao, por outro lado, precisamos apenas de alguns valores para representar os
quatro operadores e dois caracteres de pontuao:
Constantes para operadores e pontuao
#define
#define
#define
#define
SOMA
SUB
MULT
DIV
#define PARESQ
#define PARDIR
0
1
2
3
0
1
O cdigo do analisador lxico usa algumas variveis globais, para facilitar o entendimento. O programa funciona recebendo o programa de entrada como uma string (normalmente um compilador
12 / 50
A anlise iniciada ao chamar a funo inicia_analise, que estabelece o valor inicial das
variveis globais:
Funo para inicializar a anlise lxica
void inicializa_analise(char *prog)
{
codigo = prog;
tamanho = strlen(codigo);
pos = 0;
}
A funo inicia_analise recebe uma string contendo o cdigo do programa de entrada como
parmetro (prog), e armazena um ponteiro para essa string na varivel global codigo; a funo
tambm estabelece o valor da varivel global tamanho e inicializa a posio atual na anlise com
valor zero.
A anlise lxica em si funciona de maneira incremental: ao invs de analisar todo o cdigo de entrada
de uma vez e retornar todo o fluxo de tokens, a funo de anlise retorna um token de cada vez. Por
isso, o nome da funo que realiza a anlise proximo_token, e ela retorna o prximo token na
sequncia a cada vez que chamada.
Vamos analisar a funo proximo_token por partes. Comeando pelas variveis locais usadas
pela funo:
Funo que realiza a anlise lxica
Token *proximo_token(Token *tok)
{
char c;
char valor[200];
// string para obter valor de um numero
int vpos = 0;
// posicao na string de valor
Como indicado nos comentrios, a string valor usada para determinar o valor de um token de
tipo nmero. Isso necessrio porque a funo de anlise l um caractere do nmero de cada vez; a
varivel vpos usada para guardar a posio atual na string valor. A varivel c, de tipo caractere,
guarda o caractere atualmente sendo lido do cdigo de entrada.
Na maioria das linguagens de programao, os espaos em branco no so significativos para o programa, e portanto o programador pode usar quantidades variveis de espao entre os tokens do programa. Por isso, a primeira tarefa do analisador pular todo o espao em branco que for necessrio
13 / 50
A funo le_caractere uma funo auxiliar que obtm o prximo caractere do programa de
entrada, atualizando a posio atual dentro da string que contm o programa (para detalhes, veja
o cdigo-fonte completo do analisador). O cdigo acima l caracteres da entrada enquanto eles os
caracteres lidos forem de espao (espao em branco, tabulao e caracteres de nova linha), usando a
funo isspace da biblioteca padro da linguagem C. Ao final desse loop, a varivel c vai conter o
primeiro caractere do prximo token.
A funo de anlise deve ento usar esse caractere para determinar que tipo de token est sendo lido,
e continuar de acordo com o tipo. Nessa linguagem possvel determinar o tipo do token olhando
apenas para o seu primeiro caractere, mas em linguagens mais complexas isso geralmente no
possvel.
Se o primeiro caractere do token for um dgito, a anlise determina que o prximo token um nmero.
O processo a seguir ler os prximos caracteres enquanto forem dgitos, armazenando cada dgito
lido na string auxiliar valor. Ao final, o valor do token obtido atravs da converso da string
valor para um nmero inteiro, usando a funo atoi():
Leitura de um token de tipo nmero
if (isdigit(c)) {
tok->tipo = TOK_NUM;
valor[vpos++] = c;
c = le_caractere();
while (isdigit(c)) {
valor[vpos++] = c;
c = le_caractere();
}
// retorna o primeiro caractere que nao eh um digito
// para ser lido como parte do proximo token
pos--;
// termina a string de valor com um caractere 0
valor[vpos] = \0;
// converte string de valor para numero
tok->valor = atoi(valor);
}
14 / 50
A funo proximo_token completa, reunindo os trechos vistos de forma separada, pode ser vista
a seguir:
Funo completa que faz a anlise lxica
Token *proximo_token(Token *tok)
{
char c;
char valor[200];
// string para obter valor de um numero
int vpos = 0;
// posicao na string de valor
c = le_caractere();
// pula todos os espacos em branco
while (isspace(c)) {
c = le_caractere();
}
if (isdigit(c)) {
tok->tipo = TOK_NUM;
valor[vpos++] = c;
c = le_caractere();
while (isdigit(c)) {
valor[vpos++] = c;
15 / 50
O cdigo completo do analisador inclui algumas funes de impresso e uma funo principal que
l o programa de entrada a partir do teclado e mostra a sequncia de tokens obtida desta entrada. A
funo principal :
Funo principal do programa de anlise lxica
int main(void)
{
char entrada[200];
Token tok;
printf("Analise Lexica para Expressoes\n");
printf("Expressao: ");
fgets(entrada, 200, stdin);
inicializa_analise(entrada);
printf("\n===== Analise =====\n");
while (proximo_token(&tok) != NULL) {
imprime_token(&tok);
}
printf("\n");
return 0;
}
16 / 50
Analise =====
Numero
-Operador
-Pontuacao -Numero
-Operador
-Numero
-Pontuacao -Operador
-Numero
--
Valor:
Valor:
Valor:
Valor:
Valor:
Valor:
Valor:
Valor:
Valor:
42
SOMA
PARESQ
675
MULT
31
PARDIR
SUB
20925
A sada est de acordo com o que esperamos da anlise lxica dessa linguagem, como pode ser visto
na Tabela Tabela 2.1 [11].
Vimos que para uma linguagem simples como a de expresses, fcil criar diretamente o analisador
lxico necessrio. Entretanto, medida que a estrutura da linguagem se torna mais complexa (como
ocorre nas linguagens de programao real), a complexidade do analisador lxico vai crescendo e se
torna difcil criar o analisador lxico sem ter alguma tcnica sistemtica para lidar com a complexidade.
As tcnicas que usaremos para isso so relacionadas a uma classe de linguagens formais conhecida
como linguagens regulares. Essas tcnicas so fundamentadas em uma teoria bem desenvolvida, e
contam com ferramentas que automatizam a maior parte do processo de anlise lxica.
2.2
2.2.1
Expresses Regulares
As expresses regulares descrevem padres simples de texto de forma compacta e sem ambiguidade.
Por exemplo, o padro que descreve todas as strings formadas com caracteres a e b que comeam
com a e terminam com b pode ser escrito como a expresso regular a(a|b)*b (a construo dessa
expresso ser explicada em breve).
Existem vrias sintaxes e representaes diferentes para expresses regulares, dependendo da linguagem ou biblioteca utilizada. Como vamos utilizar o gerador de analisadores flex, usaremos aqui a
sintaxe usada nessa ferramenta.
2.2.1.1
Expresses bsicas
Cada expresso regular (ER) uma string que representa um conjunto de strings; tambm podemos
dizer que uma ER representa um padro que satisfeito por um conjunto de strings.
A maioria dos caracteres representam eles mesmos em uma expresso regular. Por exemplo, o caractere a em uma ER representa o prprio caractere a. A ER a representa um padro que poderia
ser descrito em portugus como o conjunto de strings que possuem um caractere a. Obviamente
s existe uma string dessa forma: a string "a". Colocando um padro aps o outro realiza a concatenao dos padres. Comeando com caracteres simples, se juntarmos um a e um b formamos a
expresso ab, que representa a string que contm um a seguido por um b, ou seja, a string "ab".
Mas o poder das Expresses Regulares vem de alguns caracteres que no representam eles mesmos;
esses so caracteres especiais. Um caractere especial bastante usado o *, que representa zero ou
mais repeties de um padro. Por exemplo, a expresso a* representa strings com zero ou mais
caracteres a. A string vazia satisfaz esse padro e corresponde a zero repeties; outras strings
satisfeitas pelo padro so "a", "aa", "aaa", etc. O asterisco representa zero ou mais repeties
do padro que vem antes, no s de um caractere: a expresso (ab)* representa , "ab", "abab",
"ababab", etc. Mas pelas regras de precedncia das expresses, ab* o mesmo que a(b*), que
representa um a seguido por zero ou mais caracteres b, e no igual a (ab)*.
Outro caractere especial importante a barra vertical |, que representa opes nas partes de um
padro. Por exemplo a|b representa a ou b, ou seja, as strings "a" e "b".
Isso nos leva ao exemplo apresentado antes: a(a|b)*b uma expresso regular formada por trs
partes concatenadas em sequncia: a, depois (a|b)* e por fim b. Isso significa que uma string que
satisfaz essa expresso deve comear com um caractere a, seguido por caracteres que satisfazem o
padro (a|b)* e terminando com um caractere b. O padro (a|b)* satisfeito por zero ou mais
repeties do padro (a|b), que por sua vez um padro que satisfeito por caracteres a ou b.
Ou seja, (a|b)* um padro que representa zero ou mais repeties de caracteres a ou b. Alguns
exemplos de cadeias que so representadas pela expresso a(a|b)*b:
18 / 50
Caracteres especiais + e ?
Ja vimos que o caractere especial * representa zero ou mais repeties de um padro. O caractere
especial + similar, mas representa uma ou mais repeties; a nica diferena que o caractere +
causa a obrigatoriedade de pelo menos uma repetio do padro. A expresso a+ representa as strings
"a", "aa", "aaa", etc., sem incluir a string vazia.
O caractere especial ? representa partes opcionais em um padro, ou seja, zero ou uma repetio de
um determinado padro. A expresso b?a+ representa strings com uma ou mais repeties de a,
podendo comear opcionalmente com um b.
2.2.1.3
Um outro tipo de caracteres especiais so os metacaracteres. Um metacaractere um caractere especial que pode representar outros caracteres. O exemplo mais simples o metacaractere ., que pode
representar qualquer caractere. A expresso a.*b representa strings que comeam com a, terminam
com b e podem ter qualquer nmero de outros caracteres no meio, por exemplo "a0x13b".
As sequncias de escape so iguais as que existem na linguagem C: \n representa um caractere de
nova linha, \t um caractere de tabulao, etc. A barra invertida tambm pode ser usada para desativar
a interpretao especial de um caractere especial. Por exemplo, se quisermos um caractere + em uma
expresso regular que representa o smbolo de soma, e no a repetio de uma ou mais vezes, devemos
usar \+.
2.2.1.5
Outras caractersticas
Alm das possibilidades de repetio que vimos at agora (zero ou mais vezes, uma ou mais vezes,
zero ou uma vez), possvel na notao do flex ser mais especfico no nmero de repeties. Se
r um padro, r{2} representa exatamente duas repeties do padro, r{2,} representa duas ou
mais repeties, e r{2, 5} representa um nmero de repeties entre duas e cinco, inclusive. A
expresso (la){3} representa trs repeties de la, ou seja, a string "lalala"; e a expresso
(la){1,3} representa as strings "la", "lala" e "lalala".
No vamos tratar aqui de todos os detalhes das expresses regulares no flex, mas eles podem ser
consultados no manual da ferramenta em http://flex.sourceforge.net/manual/Patterns.html
2.2.1.6
Alguns exemplos
Agora que introduzimos a maior parte das caractersticas das expresses regulares no flex, veremos
alguns exemplos de padres descritos usando essa notao. Depois veremos outros exemplos diretamente ligados anlise lxica de linguagens de programao.
[0-9]{3}\.[0-9]{3}\.[0-9]{3}\-[0-9]{2} um padro que descreve os nmeros de
CPF: trs dgitos ([0-9]{3}) seguidos por um ponto (\.), depois mais trs dgitos e um ponto,
depois mais trs dgitos, um hfen (\-) e finalmente dois dgitos.
[0-9]{2}\/[0-9]{2}\/[0-9]{4} um padro que descreve datas no formato DD/MM/AAAA com dois dgitos para dia e ms, e quatro dgitos para o ano. preciso usar uma barra invertida
\ para incluir a barra / no padro, caso contrrio a barra seria interpretada como um caractere especial; no padro, isso ocorre como \/. Esse padro no verifica se a data vlida (uma string
como "33/55/2033" satisfaz o padro).
[A-Z]{3}-[0-9]{4} descreve placas de carro no Brasil, comeando com trs letras maisculas
([A-Z]{3}) seguidas por um hfen e quatro dgitos.
Os endereos de email seguem um conjunto de vrias regras que estabelecem que caracteres podem ser usados (a maioria das regras pode ser encontrada nos RFCs 2821
e 2822).
Um padro simplificado para endereos de email pode ser o seguinte:
[[:alnum:]\._]+@[[:alnum:]]+\.[[:alnum:]]+ comeando com um ou mais caracteres que podem ser letras, nmeros, pontos e underscore (a parte [[:alnum:]\._]+), seguidos pela arroba, depois uma ou mais letras ou dgitos, seguindo por um ponto e mais um grupo
de uma ou mais letras ou dgitos. Esse padro descreve um endereo de email simples como
20 / 50
2.3
Um gerador de analisadores lxicos um programa que recebe como entrada a especificao lxica
para uma linguagem, e que produz como sada um programa que faz a anlise lxica para essa linguagem. Como vimos anteriormente, possvel escrever o cdigo de um analisador lxico sem o uso
de uma ferramenta, mas usar um gerador de analisadores menos trabalhoso e tem maior garantia de
gerar um analisador lxico correto. A Figura 2.1 [21] mostra um diagrama de blocos que descreve
o uso de um gerador de analisadores (no caso o flex). O gerador recebe uma especificao da estrutura lxica na entrada, e gera um programa analisador (no caso do flex, um programa na linguagem
C). Este programa, quando executado, recebe como entrada os caracteres do programa de entrada, e
produz como sada a sequncia de tokens correspondentes.
especificao
flex
caracteres
analisador
tokens
2.4
Uso do flex
No sistema Unix original foi criado um gerador de analisadores lxicos chamado lex, um dos primeiros geradores desse tipo. O projeto GNU criou o flex como uma verso do lex com licena de software
livre. O flex, assim como o lex original, um gerador de analisadores lxicos que gera analisadores
na linguagem C, e possui verses compatveis nos principais sistemas operacionais atuais.
Como mostrado na Figura 2.1 [21], o flex recebe como entrada um arquivo de especificao e produz,
na sada, um programa na linguagem C que implementa o analisador lxico que segue a especificao dada. Para usar o flex primeiramente precisamos saber como escrever a especificao lxica da
linguagem no formato esperado pela ferramenta.
21 / 50
Instalando o flex
Como comum para a maioria das ferramentas, para usar o flex preciso instalar o programa
antes. Essa instalao geralmente simples, ou mesmo desnecessria por j vir instalado,
dependendo do sistema operacional utilizado:
Em sistemas Mac OS X j vem uma verso do flex instalada. Embora no seja uma das
verses mais recentes da ferramenta, isso no um problema para o nosso uso.
Em sistemas Linux o flex deve estar disponvel como um pacote no sistema gerenciador de
pacotes da distribuio. Por exemplo, no Ubuntu basta fazer apt-get install flex
(possivelmente com o uso do sudo) para instalar.
No Windows o mais adequado instalar usando um instalador criado especificamente para esse sistema operacional, que pode ser encontrado no endereo
http://gnuwin32.sourceforge.net/packages/flex.htm
2.4.1
Formato da entrada
A parte principal da especificao lxica no flex um conjunto de regras. Cada regra composta por
duas partes: um padro e uma ao; o padro uma expresso regular que descreve um determinado
tipo de tokens da linguagem, e a ao determina o que fazer quando encontrar o padro correspondente. Para um compilador, a maioria das aes vai simplesmente criar um token para o lexema
encontrado, como veremos adiante.
O formato do arquivo de especificao do flex dividido em trs partes separadas por uma linha
contendo os caracteres %%, da seguinte forma:
Formato de um arquivo de especificao do flex
definies
%%
regras
%%
cdigo
A nica parte obrigatria do arquivo so as regras. As definies permitem dar nomes a expresses
regulares, o que til quando uma determinada expresso regular aparece como parte de vrios padres, ou como forma de documentao, para deixar mais claro o que significam as partes de um
padro complexo. Veremos exemplos de uso das definies mais adiante. No comeo do arquivo
tamm podem ser especificadas algumas opes que alteram o comportamento do analisador gerado.
A terceira parte do arquivo pode conter cdigo em linguagem C que ser adicionado, sem alteraes,
ao programa C gerado pelo flex. Como a sada do flex um programa em linguagem C, isso permite que o criador do arquivo de especificao adicione funes ou variveis ao analisador gerado.
Geralmente a parte de cdigo til para definir funes auxiliares que podem ser usadas pelas aes.
J entendemos a maior parte do que necessrio para usar o flex, mas alguns detalhes s ficam claros
com alguns exemplos. Vamos comear com um exemplo de especificao bastante simples.
2.4.2
Para exemplificar o uso do flex, vamos ver um exemplo de especificao simples e auto-contida que
tambm serve como uma forma de testar os padres do flex.
22 / 50
%option noyywrap
2x
CPF [0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}
EMAIL [[:alnum:]\._]+@[[:alnum:]]+\.[[:alnum:]]+
%%
3x
{CPF}
{ printf("CPF\n"); }
{EMAIL} { printf("EMAIL\n"); }
.
{ printf("Caractere nao reconhecido\n"); }
%%
// funcao principal que chama o analisador
int main()
{
yylex();
}
x
Definies
A especificao contm as trs partes: definies, regras e cdigo. Antes das definies est especificada a opo noyywrap, que simplifica a especificao (sem essa opo seria necessrio escrever
uma funo chamada yywrap). So definidas duas expresses, CPF e EMAIL.
Na parte de regras so descritas trs, as duas primeiras determinam o que o programa deve fazer
quando encontra um token que satisfaz os padres CPF e EMAIL, e a terceira regra determina o que
o programa deve fazer com tokens que no satisfazem nenhum dos dois.
A primeira regra :
{CPF}
{ printf("CPF\n"); }
Uma ao sempre tem duas partes, o padro e a ao, separados por espaos em branco:
padro ao
CPF entre chaves especifica que deve ser usada a definio com esse nome, ao invs de tratar os
caracteres como representando eles mesmos (o padro CPF, sem chaves, seria satisfeito apenas pela
string "CPF"). A regra um trecho de cdigo C que ser executado casa o padro seja satisfeito pelo
token. Nesse caso apenas impressa a string "CPF", para que o usurio veja o tipo de token que foi
determinado.
O uso de definies opcional. A seguinte regra tem exatamente o mesmo efeito que a regra anterior
para nmeros de CPF:
23 / 50
[0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}
{ printf("CPF\n"); }
Veja que nesse caso no preciso usar chaves ao redor do padro. O uso de uma definio torna a
especificao muito mais legvel, deixando claro para o leitor o que a expresso regular representa.
A regra para endereos de email segue os mesmos princpios. A terceira regra :
.
Como j vimos, o ponto um metacaractere no flex que representa qualquer caracter. O padro .
significa qualquer caractere, ou seja, esse padro reconhece qualquer caractere no reconhecido
pelos padres anteriores. importante entender como as regras do flex so processadas: o analisador
testa a string atual com todos os padres da especificao, procurando ver que padres so satisfeitos
pela string. Se mais de um padro satisfeito pel string ou parte dela, o analisador vai escolher o
padro que satisfeito pelo maior nmero de caracteres.
Usando as regras do exemplo atual, digamos que a string atual seja um CPF. Essa string satisfaz o
padro para nmeros de CPF na primeira regra do arquivo de especificao, mas o primeiro caractere
da string, que um nmero, tambm satisfaz a ltima regra (o padro .), pois esse padro satisfaz
qualquer caractere. Entre as duas regras ativadas, a regra do CPF casada com todos os caracteres
da string atual, enquanto que a regra do ponto s casada com o primeiro caractere da string atual.
Portanto, a regra do CPF casada com o maior nmero de caracteres, e essa regra escolhida. Mas se
a string atual for uma sequncia de trs dgitos como 123, o nico padro que satisfeito o ltimo,
do ponto, que aceita qualquer caractere, e nesse caso o analisador imprime mensagens de erro (o
padro do ponto satisfeito trs vezes por essa string, j que o ponto representa apenas um caractere).
Como o flex casa a entrada com os padres
O funcionamento geral do flex determinado pelos padres que esto presentes nas regras
especificadas para o analisador. Para a sequncia de caracteres da entrada, o analisador
gerado pelo flex tenta casar os caracteres de entrada, ou uma parte inicial deles, com algum
padro nas regras.
Se apenas um padro casado com os caracteres iniciais da sequncia, a regra de onde
vem o padro disparada, ou seja, a ao da regra executada pelo analisador. Se nenhum padro for casado com os caracteres atuais, o analisador gerado executa uma regra
padro inserida pelo flex. A regra padro simplesmente imprime na sada os caracteres no
reconhecidos por nenhum padro.
Se mais de um padro for satisfeito por uma sequncia inicial dos caracteres atuais, o analisador escolhe o padro que casado com o maior nmero de caracteres e dispara a regra
desse padro. Se vrios padres casam com o mesmo nmero de caracteres da sequncia
atual, o analisador escolhe aquele que aparece primeiro no arquivo de especificao. Isso
significa que a ordem das regras no arquivo de especificao importante.
Se uma regra disparada ao casar os caracteres atuais com algum padro, os caracteres
restantes que no foram casados com o padro permanecem guardados para uma prxima
vez que o analisador for chamado. Por exemplo, se a entrada a string 123456 e o nico
padro do analisador representa cadeias de trs dgitos, a primeira chamada ao analisador
vai casar os caracteres 123 e disparar a regra associada, deixando os caracteres 456 no
analisador, para uma prxima chamada. Se o analisador for chamado novamente, a regra vai
casar com os caracteres 456 e disparar novamente. Dessa forma o analisador pode atuar
em um token de cada vez.
24 / 50
Depois disso, o executvel simples vai funcionar como descrito: esperando entrada pelo teclado e
imprimindo os tipos de tokens reconhecidos:
Exemplo de uso do analisador
Sandman:cap2 andrei$ ./simples
111.222.333-99
CPF
nome@mail.com
EMAIL
123
Caractere nao reconhecido
Caractere nao reconhecido
Caractere nao reconhecido
Para terminar o teste, deve-se digitar o caractere de fim de arquivo (em sistemas Unix o fim de arquivo
entrado com Ctrl+D, enquanto em sistemas Windows deve-se usar Ctrl+Z).
Essa especificao pode ser usada para testar outros padres. Ao mudar a especificao, deve-se gerar
novamente o arquivo C usando o flex e compilar o arquivo C gerado.
Em um compilador geralmente queremos que a entrada seja lida de um arquivo, e precisamos gerar
os tokens para cada lexema, no simplesmente imprimir o tipo de cada token encontrado. Veremos
nos prximos exemplos como fazer isso.
2.4.3
Vimos anteriormente um analisador lxico para uma linguagem de expresses criado diretamente na
linguagem C, sem uso de gerador. Nosso prximo exemplo um analisador para a mesma linguagem,
mas agora usando flex. Isso serve a dois propsitos: o primeiro mostrar mais algumas caractersticas
do uso do flex; o segundo comparar o esforo necessrio para criar um analisador com e sem usar
um gerador como o flex.
25 / 50
Alm das constantes j vistas para tipo e valor do token, temos um novo tipo de token declarada e
prottipos para duas funes. O novo tipo o TOK_ERRO, que sinaliza um erro na anlise lxica.
Esse tipo usado quando o analisador recebe uma sequncia de caracteres que no reconhece como
token da linguagem. As duas funes declaradas so a funo principal do analisador, e uma funo
para criar tokens que usada pelo analisador. A funo principal desse analisador retorna um ponteiro
para uma estrutura Token, e por isso ela deve ser declarada de outra forma (a funo yylex padro
retorna um inteiro, como vimos antes).
Em seguida vamos ver o arquivo de especificao para essa linguagem, que contm algumas novidades em relao ao que vimos antes:
26 / 50
%option noyywrap
%option nodefault
%option outfile="lexer.c" header-file="lexer.h"
%top {
#include "exp_tokens.h"
}
NUM [0-9]+
%%
[[:space:]] { }
/* ignora espacos */
{NUM}
\+
\*
\/
\(
\)
token(TOK_NUM,
token(TOK_OP,
token(TOK_OP,
token(TOK_OP,
token(TOK_OP,
token(TOK_PONT,
token(TOK_PONT,
{
{
{
{
{
{
{
return
return
return
return
return
return
return
atoi(yytext)); } 3x
SOMA);
}
SUB);
}
MULT);
}
DIV);
}
PARESQ); }
PARDIR); }
/* erro p/ token
%%
x
O arquivo de especificao usa todas as trs partes: definies (e opes), regras e cdigo. Na parte
de definio so includas as opes para no precisar da funo yywrap e opes para selecionar
o nome dos arquivos de sada. No exemplo anterior o nome do arquivo de sada foi selecionado na
linha de comando; nesta especificao usamos opes para no s selecionar o nome do arquivo C
27 / 50
{ return token(TOK_NUM,
atoi(yytext)); }
De resto, a ltima regra usa o padro com um ponto para capturar erros lxicos na entrada.
A seo de cdigo inclui uma varivel e uma funo que sero includas no analisador gerado. A
varivel tok serve para guardar o token atual, e a funo token serve para guardar os dados de um
token e retorn-lo para a parte do programa que chama o analisador.
O arquivo C exp_flex.c contm o programa principal que recebe entrada do teclado e chama
o analisador lxico. Esse arquivo contm funes operador_str e imprime_token que so
idnticas s funes no arquivo exp_lexer.c do analisador lxico anterior. A funo principal do
programa em exp_flex.c mostrada abaixo:
Cdigo fonte code/cap2/exp_flex/exp_flex.c
Funo principal do progra em exp_flex.c
int main(int argc, char **argv)
{
char entrada[200];
Token *tok;
printf("Analise Lexica para Expressoes\n");
printf("Expressao: ");
fgets(entrada, 200, stdin);
inicializa_analise(entrada);
printf("\n===== Analise =====\n");
tok = proximo_token();
while (tok != NULL) {
28 / 50
E a funo inicializa_analise chama uma funo do analisador gerado que configura a anlise lxica para ler de uma string ao invs da entrada padro. Para isso preciso usar uma varivel do
tipo YY_BUFFER_STATE, que um tipo declarado no analisador gerado.
Funo que inicializa a anlise lxica
YY_BUFFER_STATE buffer;
void inicializa_analise(char *str)
{
buffer = yy_scan_string(str);
}
Para compilar o analisador so necessrios dois passos, como antes: gerar o arquivo C do analisador
usando o flex, e ento compilar os arquivos C usando o compilador C. Para gerar o analisador usando
o flex, preciso usar uma opo para determinar uma declarao diferente para a funo principal do
analisador, yylex:
flex -DYY_DECL="Token * yylex()" exp.ll
Isso vai gerar os arquivos lexer.c e lexer.h. O passo seguinte compilar o arquivo lexer.c
juntamente com o arquivo principal exp_flex.c. Usando o gcc como compilador, a linha de
comando seria:
gcc -o exp lexer.c exp_flex.c
Isso gera um executvel de nome exp. Quando executado, o programa funciona praticamente da
mesma forma que o analisador criado diretamente para a mesma linguagem de expresses; a nica
diferena o tratamento de erros. O analisador que usa o flex sinaliza os erros obtidos e continua com
a anlise.
Esse exemplo demonstra quase tudo que precisamos para fazer a anlise lxica de uma linguagem de
programao. O nico detalhe que falta saber como ler a entrada de um arquivo ao invs de uma
string ou da entrada padro.
29 / 50
2.4.4
A funo yylex gerada pelo flex normalmente l sua entrada de um arquivo, a no ser que seja usada
a funo yy_scan_string como no exemplo anterior. O analisador gerado pelo flex contm
uma varivel chamada yyin que um ponteiro para o arquivo de entrada usado pelo analisador.
Normalmente essa varivel igual varivel global stdin da linguagem C, ou seja, a entrada padro.
Para fazer que o analisador leia de um arquivo ao invs da entrada padro, necessrio mudar o valor
dessa varivel para o arquivo desejado.
O cdigo necessrio basicamente o seguinte:
Exemplo de como fazer o analisador ler de um arquivo
int inicializa_analise(char *nome)
{
FILE *f = fopen(nome, "r");
if (f == NULL)
return FALSE;
// arquivo aberto com sucesso, direcionar analisador
yyin = f;
}
Ou seja, deve-se abrir o arquivo desejado usando fopen, e depois atribuir a varivel yyin do analisador para apontar para o mesmo arquivo aberto. A partir da, chamadas funo yylex vo ler os
caracteres usados no analisador do arquivo, ao invs da entrada padro. uma boa prtica fechar o
arquivo aberto durante a inicializao ao final da anlise lxica. Vamos ver um exemplo de analisador
que l a entrada a partir de um arquivo a seguir.
2.5
Os exemplos vistos at agora foram preparao para sabermos como criar um analisador lxico para
uma linguagem de programao. Vamos usar o flex como maneira mais fcil de criar um analisador do
que escrever o cdigo diretamente. Para especificar os padres que definem os vrios tipos de tokens
no flex, precisamos usar expresses regulares. J vimos como escrever um arquivo de especificao
do flex para criar um analisador lxico, e como trabalhar com o cdigo C gerado pelo flex. Nesta
seo vamos juntar todas as peas e criar um analisador lxico para uma pequena linguagem de
programao, uma verso simplificada da linguagem C.
2.5.1
A Linguagem Mini C
A linguagem que vamos usar como exemplo uma simplificao da linguagem de programao C,
chamada aqui de Mini C. A ideia que todo programa Mini C seja um programa C vlido, mas muitas
caractersticas da linguagem C no esto disponveis em Mini C.
Um programa Mini C um conjunto de declaraes de funes. Cada funo uma sequncia de
comandos. Alguns comandos podem conter expresses. Variveis podem ser declaradas, apenas do
tipo int. As estruturas de controle so apenas o condicional if e o lao while. As expresses que
podem ser formadas na linguagem tambm so simplificadas.
30 / 50
2.5.2
Aqui veremos um analisador lxico para a linguagem Mini C criado com o flex. Quase todas as
caractersticas do flex usadas aqui j foram apresentadas antes, mas veremos algumas novidades. A
maior novidade no analisador para a linguagem Mini C a necessidade de usar tabelas para armazenar
as strings e os nomes de identificadores que ocorrem no programa.
Constantes para os tipos e valores de tokens so definidos no arquivo de cabealho
minic_tokens.h.
Cdigo fonte code/cap2/minic/minic_tokens.h
A parte mais importante do contedo deste arquivo mostrada abaixo:
Definio de constantes para o analisador lxico
31 / 50
// Tipos de token
#define TOK_PCHAVE
#define TOK_ID
#define TOK_NUM
#define TOK_PONT
#define TOK_OP
#define TOK_STRING
#define TOK_PROLOGO
#define TOK_ERRO
1
4
5
6
7
8
9
100
0
1
2
3
4
1
2
3
4
5
6
1
2
3
4
5
6
7
8
9
// tipos
typedef struct
{
int tipo;
int valor;
} Token;
Agora vamos analisar o arquivo de especificao do flex para a linguagem Mini C, uma seo de cada
vez. O arquivo completo pode ser encontrado no endereo abaixo.
Cdigo fonte code/cap2/minic/lex.ll
A seo inicial contm opes e definies, alm de trechos de cdigo que so adicionados no comeo
do arquivo do analisador gerado:
Opes e definies na especificao para o analisador Mini C
%option noyywrap
32 / 50
As opes so as mesmas que j vimos no exemplo anterior. Temos um trecho de cdigo includo
no comeo do analisador (a parte comeando com %top) que realiza a incluso de dois arquivos
de cabealho e define o prottipo da funo token (definida na seo de cdigo). Os cabealhos
includos so minic_tokens.h, visto acima, e tabelas.h, que declara as funes para tabelas
de strings e de smbolos: as funes so chamadas de adiciona_string para adicionar uma
nova string na tabela, e adiciona_simbolo para um novo identificador na tabela de smbolos.
Essas funes so definidas no arquivo tabelas.c e retornam o ndice da string ou smbolo na
tabela respectiva; esse ndice pode ser usado como valor do token. O uso de tabelas de strings e
de smbolos importante por vrios motivos, entre eles a eficincia do cdigo do compilador. Em
captulos seguintes veremos como a tabela de smbolos uma estrutura de importncia central em um
compilador.
A especificao tem trs definies: NUM o padro para constantes numricas inteiras, idntica que
vimos no exemplo anterior; ID o padro que especifica os identificadores da linguagem e STRING
o padro que determina o que uma literal string em um programa Mini C: deve comear e terminar
com aspas duplas, contendo no meio qualquer sequncia de zero ou mais caracteres que no sejam
aspas duplas nem caracteres de nova linha (\n).
As regras da especificao so mostradas abaixo:
Regras na especificao para o analisador da linguagem Mini C
[[:space:]]
\/\/[^\n]*
"#include <stdio.h>"
{
{
{
{
{
return
return
return
return
return
token(TOK_PCHAVE,
token(TOK_PCHAVE,
token(TOK_PCHAVE,
token(TOK_PCHAVE,
token(TOK_PCHAVE,
PC_IF); }
PC_ELSE); }
PC_WHILE); }
PC_RETURN); }
PC_PRINTF); }
{NUM}
{ID}
\+
{
{
{
{
{
{
{
{
return
return
return
return
return
return
return
return
token(TOK_OP,
token(TOK_OP,
token(TOK_OP,
token(TOK_OP,
token(TOK_OP,
token(TOK_OP,
token(TOK_OP,
token(TOK_OP,
SUB);
MULT);
DIV);
MENOR);
IGUAL);
AND);
NOT);
ATRIB);
\(
\)
\{
\}
,
;
{
{
{
{
{
{
return
return
return
return
return
return
token(TOK_PONT,
token(TOK_PONT,
token(TOK_PONT,
token(TOK_PONT,
token(TOK_PONT,
token(TOK_PONT,
}
}
}
}
}
}
}
}
PARESQ); }
PARDIR); }
CHVESQ); }
CHVDIR); }
VIRG);
}
PNTVIRG); }
}
A primeira regra ignora os espaos, como vimos no exemplo anterior. A segunda para ignorar os comentrios. Um comentrio qualquer sequncia que comea com duas barras e vai at o fim da linha.
Como a barra um caractere especial no flex, ele precisa ser especificado com a contrabarra antes, e
portanto // vira \/\/ no padro. As outras regras no tm novidade, exceto que a regra para strings
e a regra para identificadores obtm o valor do token chamando as funes adiciona_string e
adiciona_simbolo, como discutimos antes.
O analisador lxico normalmente no vai ser usado de forma isolada: ele chamado pelo analisador sinttico para produzir tokens quando necessrio, como veremos no prximo captulo. Mas
aqui vamos testar o funcionamento do analisador lxico isoladamente, criando um programa principal que abre um arquivo de entrada e chama o analisador lxico. Esse programa est no arquivo
lex_teste.c:
Cdigo fonte code/cap2/minic/lex_teste.c
O programa principal seguinte:
Funo do principal do programa de teste do analisador lxico
int main(int argc, char **argv)
{
Token *tok;
if (argc < 2) {
printf("Uso: mclex <arquivo>\n");
return 0;
}
inicializa_analise(argv[1]);
tok = yylex();
while (tok != NULL) {
imprime_token(tok);
tok = yylex();
}
finaliza_analise();
34 / 50
return 0;
}
A funo principal obtm o nome do arquivo de entrada dos argumentos de linha de comando, inicializa a anlise passando esse nome de arquivo, e para cada token encontrado na entrada imprime uma
representao desse token na tela. A funo inicializa_analise apenas tenta abrir o arquivo
com o nome passado e configura o analisador lxico para usar esse arquivo, como discutimos antes:
Funo que inicializa a anlise lxica
void inicializa_analise(char *nome_arq)
{
FILE *f = fopen(nome_arq, "r");
if (f == NULL) {
fprintf(stderr, "Nao foi possivel abrir o arquivo de entrada: %s\n ", nome_arq);
exit(1);
}
yyin = f;
}
A funo de impresso de tokens apenas imprime o tipo e o valor do token, mas usando uma string
que representa o nome do tipo ao invs de simplesmente imprimir a constante numrica. A funo
completa pode ser vista no cdigo fonte.
Para testar o analisador, vamos criar um pequeno programa na linguagem Mini C. Esse programa
pode ser encontrado no arquivo teste.c.
Cdigo fonte code/cap2/minic/teste.c
Arquivo de teste na linguagem Mini C
// teste para o compilador Mini C
#include <stdio.h>
int main()
{
printf("Ola, mundo!\n");
35 / 50
Compilando o programa principal lex_teste.c para gerar um executvel mclex, obtemos a seguinte sada para o arquivo teste.c:
Sada do teste do analisador lxico para o arquivo de teste
andrei$ ./mclex teste.c
Tipo: prologo - Valor: 0
Tipo: identificador - Valor:
Tipo: identificador - Valor:
Tipo: pontuacao - Valor: 1
Tipo: pontuacao - Valor: 2
Tipo: pontuacao - Valor: 3
Tipo: palavra chave - Valor:
Tipo: pontuacao - Valor: 1
Tipo: string - Valor: 0
Tipo: pontuacao - Valor: 2
Tipo: pontuacao - Valor: 6
Tipo: palavra chave - Valor:
Tipo: numero - Valor: 0
Tipo: pontuacao - Valor: 6
Tipo: pontuacao - Valor: 4
0
1
interessante ver que o analisador ignorou a primeira linha com um comentrio, e o primeiro token
mostrado o prlogo. A sequncia de tokens segue corretamente do contedo do arquivo fonte.
Tambm deve ser observado que o valor dos tokens de tipo string e identificador so os ndices deles
nas tabelas correspondentes. Para obter a string ou identificador encontradas pelo analisador, preciso
acessar as tabelas. Veremos exemplos disso no captulo seguinte.
2.6
Concluso
Neste captulo vimos o que a etapa de anlise lxica do compilador, e como criar um analisador
lxico para qualquer linguagem de entrada. Vimos o que so os tokens na anlise lxica e como especificar os padres que determinam os tipos de tokens usando expresses regulares. possvel criar
um analisador lxico escrevendo o cdigo diretamente a partir dos padres dos tokens, mas mais
prtico usar um gerador de analisadores lxicos como o flex. Vimos o uso do flex para linguagens
bastante simples e tambm para linguagens de programao mais prxima da realidade. Neste captulo tambm vimos uma introduo linguagem Mini C, que ser usada como exemplo no resto
do livro. Um analisador lxico completo para a linguagem Mini C, usando o flex, foi mostrado e
detalhado neste captulo. Esse analisador pode servir como base para criar analisadores para outros
tipos de linguagens reais.
No captulo seguinte veremos como utilizar a sada do analisador lxico para fazer a anlise sinttica dos programas, e estabelecer a estrutura sinttica. Essa estrutura de importncia central na
compilao dos programas.
36 / 50
Captulo 3
Anlise Sinttica
O BJETIVOS DO CAPTULO
Ao final deste captulo voc dever ser capaz de:
Entender a funo do analisador sinttico e como ele se integra ao resto do compilador
Compreender o conceito de estrutura sinttica
Criar Gramticas Livres de Contexto que capturam a estrutura sinttica de linguagens
Criar o analisador sinttico para uma linguagem usando uma ferramenta geradora
A anlise sinttica a etapa do compilador que ocorre aps a anlise lxica. O objetivo da anlise
sinttica determinar a estrutura sinttica do cdigo-fonte que est sendo compilado. Para isso, a
anlise sinttica utiliza o fluxo de tokens produzido pela anlise lxica.
No captulo anterior mencionamos uma analogia entre compilao de um programa e leitura de um
texto. Vimos que a anlise lxica pode ser entendida como o agrupamento de letras individuais em
palavras. J a anlise sinttica similar ao agrupamento de palavras para formar frases. Quando
lemos, nosso crebro determina automaticamente a estrutura sinttica das frases, pois entender essa
estrutura (mesmo que apenas intuitivamente) necessrio para compreender o significado da frase:
quem o sujeito (quem realiza a ao), quem o objeto (quem sofre a ao), etc. A anlise sinttica
em um compilador faz o mesmo tipo de tarefa, mas determinando a estrutura do cdigo-fonte que est
sendo compilado.
Neste captulo vamos entender em mais detalhes a funo do analisador sinttico e como ele funciona. Assim como usamos o formalismo das expresses regulares para guiar a criao do analisador
lxico, veremos o uso das Gramticas Livres de Contexto para guiar a criao de analisadores sintticos. Tambm veremos como usar o gerador bison como uma ferramenta para ajudar na criao do
analisador sinttico (assim como usamos o flex para a anlise lxica).
3.1
Estrutura sinttica
A estrutura sinttica de um programa relaciona cada parte do programa com suas sub-partes componentes. Por exemplo, um comando if completo tem trs partes que o compem: uma condio de
teste, um comando para executar caso a condio seja verdadeira, e um comando para excutar caso a
condio seja falsa. Quando o compilador identifica um if em um programa, importante que ele
37 / 50
if
cond
C1
C2
3.1.1
rvores de expresso
*
+
+
2
(a)
3
(b)
Nesse comando, a condio uma expresso relacional (de comparao), x > 2, o comando 1 a
atribuio do valor da expresso (x - 2) * 7 varivel y, e o comando 2 a atribuio do valor
da expresso x + 2 * 5 varivel y. A rvore para esse comando comea da forma que j vimos,
mas inclui a estrutura das partes do comando. Essa rvore pode ser vista na Figura 3.3 [40].
39 / 50
if
=
>
x
*
x
+
x
*
2
3.2
O analisador sinttico a etapa que vem logo aps o analisador lxico no compilador, e isso acontece
porque as etapas esto fortemente relacionadas. A tarefa do analisador sinttico muito mais simples
de realizar partindo dos tokens da entrada, ao invs dos caracteres isolados.
Em teoria, como vimos no Captulo 1, a comunicao entre o analisador lxico e o analisador sinttico
sequencial: o analisador lxico produz toda a sequncia de tokens (criada a partir do arquivo de
entrada) e passa essa sequncia inteira para o analisador sinttico. Essa ideia mostrada na Figura 3.4
[40].
caracteres
lxico
tokens
sinttico
rvore
proxtoken()
caracteres
lxico
sinttico
rvore
token
Figura 3.5: Relao entre analisadores lxico e sinttico, na prtica.
Um dos motivos que levaram a essa organizao das duas primeiras etapas de anlise foi que em
computadores antigos era pouco provvel ter memria suficiente para armazenar toda a sequncia
de tokens da entrada (a no ser que o programa de entrada fosse pequeno), ento fazia mais sentido
processar um token de cada vez. Da mesma forma, no havia memria suficiente para guardar toda a
rvore sinttica do programa. Por isso, os compiladores eram organizados de maneira que o analisador
sinttico comandava todo o processo de traduo: obtia o prximo token do analisador lxico e, se
fosse possvel, passava uma sub-estrutura completa do programa (uma sub-rvore) para ser processada
pelas etapas seguintes, j gerando o cdigo-destino para essa parte. Em seguida, essa parte da rvore
era descartada e o analisador sinttico passava para a prxima parte da rvore.
Esse tipo de organizao de um compilador era conhecida como traduo dirigida pela sintaxe. Hoje
em dia, com os computadores tendo quantidades de memria disponvel muito maiores, menos comum ver compiladores reais seguindo esse esquema, e muitos constroem a rvore sinttica inteira do
programa, que passada para as etapas seguintes. Isso porque vrios processos das etapas seguintes podem funcionar melhor se puderem ter acesso rvore sinttica inteira, ao invs de apenas um
pedao de cada vez.
Mas a relao entre o analisador lxico e o analisador sinttico continua a mesma mostrada na Figura 3.5 [41] at hoje, mesmo tendo mais memria, pois para a maioria das linguagens de programao no mesmo necessrio acessar toda a sequncia de tokens da entrada. Alguns tipos de analisador
sinttico armazenam e analisam os ltimos n tokens, para n pequeno (ao invs de analisar apenas um
token por vez). Mesmo assim, isso no muda a relao entre os analisadores lxico e sinttico, o
analisador sinttico apenas chama a funo de obter o prximo token quantas vezes precisar.
3.3
As gramticas formais so ferramentas para descrio de linguagens. Usamos aqui o adjetivo gramticas formais para distinguir de outros sentidos da palavra gramtica, por exemplo na frase a
gramtica da lngua portuguesa, mas daqui para frente, sempre que usarmos a palavra gramtica,
estaremos nos referindo s gramticas formais, a no ser que haja indicao do contrrio.
41 / 50
3.3.1
Exemplo: Palndromos
O primeiro exemplo uma linguagem bastante simples que gera cadeias que so palndromos. Um
palndromo uma palavra ou frase que lida da mesma forma de frente para trs e de trs para
frente, como roma e amor ou socorram-me, subi no nibus em marrocos. Vamos trabalhar com
palndromos construdos com um alfabeto bastante limitado, de apenas dois smbolos: a e b. Alguns
palndromos nesse alfabeto so abba, aaa e ababa.
Existem dois tipos de palndromos, que podemos chamar de palndromos pares e palndromos mpares. Os palndromos pares, como abba, contm um nmero par de smbolos, com a segunda metade
igual ao reverso da primeira metade. No caso de abba, as metades so ab e ba, sendo que a segunda metade, ba, o reverso da primeira, ab. Cada smbolo em uma metade deve ocorrer na outra
tambm.
Os palndromos mpares, como ababa, possuem um nmero mpar de smbolos, com uma primeira
parte, um smbolo do meio, e uma ltima parte; a ltima parte o reverso da primeira, mas o smbolo
do meio pode ser qualquer um. No caso do alfabeto com smbolos a e b, tanto ababa quanto abbba
so palndromos mpares com primeira e ltima partes idnticas, mas smbolos do meio diferentes.
A gramtica para essa linguagem de palndromos tem dois smbolos terminais (a e b), um smbolo
varivel (S) que tambm o smbolo inicial, e quatro produes:
S aSa
S bSb
S
a
S
b
S
Cada uma dessas produes representam uma forma em que o smbolo S pode ser transformado para
gerar cadeias da linguagem. O smbolo representa uma cadeia vazia, ou seja, uma cadeia sem
nenhum smbolo. Quando temos vrias produes para o mesmo smbolo varivel, como no caso da
gramtica para palndromos, podemos economizar espao usando a seguinte notao:
S aSa | bSb | a | b |
Todas as produes para o smbolo S aparecem na mesma linha, separadas por barras. Podemos ler
essa gramtica como S pode produzir aSa ou bSb ou . . . .
O processo de gerao de uma cadeia seguindo as regras de produo de uma gramtica chamado
de derivao, e ser explicado a seguir.
42 / 50
3.3.2
Derivao
3.3.3
Um exemplo mais similar s linguagens de programao uma linguagem simples para expresses
aritmticas, como vimos no Captulo 2. Aqui veremos uma gramtica para uma linguagem de expresses aritmticas formadas por nmeros inteiros e as quatro operaes bsicas.
Diferente do exemplo anterior dos palndromos, para a linguagem de expresses no interessante
trabalhar com caracteres isolados. Afinal, vimos como criar um analisador lxico justamente para
agrupar os caracteres em tokens, o que facilita muito a anlise sinttica. Por isso, nesse exemplo
e em praticamente todos daqui para a frente, os smbolos terminais no sero caracteres, mas sim
tokens. Alguns tokens so formados por apenas um caractere, mas para a gramtica no faz diferena;
a anlise sinttica vai ser realizada com base nos tokens.
Para a linguagem de expresses, temos tokens de trs tipos: nmeros, operadores e pontuao. Os
operadores so os smbolos para as quatro operaes, e o tipo pontuao para os parnteses. Lembrando do captulo anterior, cada token tem um tipo e um valor; um token do tipo operador vai ter
um valor associado que determina qual dos quatro operadores o token representa. O mesmo acontece
com o valor dos tokens de tipo pontuao: o valor especifica se um parntese abrindo ou fechando.
Para os tokens de tipo nmero, o valor o valor numrico do token.
Uma gramtica para a linguagem de expresses a seguinte:
E E + E | E E | E E | E/E | (E) | num
Essas produes representam o fato que uma expresso pode ser:
Uma soma (ou multiplicao, subtrao, diviso) de duas expresses
Uma expresso entre parnteses
Uma constante numrica (representada aqui por um token de tipo num)
Todos os smbolos nas produes dessa gramtica so variveis ou so tokens; para deixar a notao
mais leve, usamos o caractere + para representar um token de tipo operador e valor que representa um
operador de soma. Isso no deve causar problema; deve-se apenas lembrar que todos os terminais so
tokens. No caso do token de tipo num, o valor dele no aparece na gramtica porque no relevante
para a estrutura sinttica da linguagem. Qualquer token de tipo nmero, independente do valor, faz
parte dessa mesma produo (diferente dos tokens de operadores).
44 / 50
3.3.4
rvores de Derivao
Uma alternativa para representar derivaes em uma gramtica usar as rvores de derivao ao invs
de sequncias lineares de formas sentenciais que vimos at agora. Uma rvore de derivao semelhante s rvores sintticas que vimos antes, mas incluem mais detalhes relacionados s produes da
gramtica utilizada. Uma rvore sinttica no inclui nenhuma informao sobre smbolos variveis
da gramtica, por exemplo. Mais frente, um dos nossos objetivos ser obter a rvore sinttica de um
programa, mas para fazer a anlise sinttica importante entender as rvores de derivao.
Em uma rvore de derivao, cada n um smbolo terminal ou varivel. As folhas da rvore so
smbolos terminais, e os ns internos so smbolos variveis. Um smbolo varivel V vai ter como
filhos na rvore os smbolos para os quais V substitudo na derivao. Por exemplo, sejam as
seguintes derivaes na gramtica de expresses:
E num
E E + E num + E num + num
As rvores de derivao correspondentes so:
45 / 50
E
E
E
num
num
num
3.3.5
Ambiguidade
O exemplo anterior demonstra um problema importante que pode ocorrer com gramticas livres de
contexto: ambiguidade. Uma gramtica ambgua quando existe pelo menos uma sentena gerada
pela gramtica que pode ser gerada de duas ou mais formas diferentes; ou seja, essa sentena ter
duas ou mais rvores de derivao diferentes.
A ambiguidade um problema pois significa que uma mesma sentena pode ter duas estruturas sintticas diferentes, na mesma gramtica. A estrutura sinttica de uma sentena vai influenciar no seu
significado e como ela interpretada pelo compilador, por exemplo. Desta forma, uma gramtica
ambgua para uma linguagem de programao significa que certos programas poderiam funcionar de
duas (ou mais) maneiras diferentes, dependendo de como o compilador interprete as partes ambgua. Obviamente importante que uma linguagem tenha programas que funcionem sempre de uma
mesma maneira, caso contrrio o programador teria dificuldade para aprender como trabalhar com a
linguagem.
No exemplo da gramtica de expresses, uma ambiguidade ocorre quando misturamos operadores
como soma e multiplicao. Na expresso 6 * 5 + 12, deve ser efetuada primeiro a soma ou a
multiplicao? Em termos de estrutura sinttica, a pergunta se a expresso
1. uma soma, com operando esquerdo 6 * 5 e operando direito 12
46 / 50
num
num
num
num
num
num
num
num
num
3.3.6
Agora que j vimos as caractersticas das gramticas livres de contexto e alguns exemplos, vamos ver
uma gramtica para uma linguagem de programao simples, que demonstra o tipo de situaes com
48 / 50
3.4
Os geradores de analisadores sintticos funcionam de maneira bastante similar aos geradores de analisadores lxicos vistos no Captulo 2. Para gerar um analisador sinttico, usamos a ferramenta geradora
passando como entrada uma especificao da estrutura sinttica da linguagem que queremos analisar;
a sada do gerador um analisador sinttico na forma de cdigo em alguma linguagem de programao (no nosso caso, um arquivo na linguagem C). Esse analisador recebe um fluxo de tokens na
entrada e gera uma rvore sinttica na sada. A Figura 3.9 [49] mostra um diagrama de blocos que representa o uso de um gerador de analisadores sintticos, como descrito. No nosso caso, a ferramenta
de gerao o bison, verso do projeto GNU para o utilitrio yacc do Unix.
especificao
bison
tokens
analisador
rvore
49 / 50
Captulo 4
Anlise Semntica
O BJETIVOS DO CAPTULO
Ao final deste captulo voc dever ser capaz de:
objetivo 1
objetivo 2
objetivo N
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa
qui officia deserunt mollit anim id est laborum. == Representao Intermediria
O BJETIVOS DO CAPTULO
Ao final deste captulo voc dever ser capaz de:
objetivo 1
objetivo 2
objetivo N
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa
qui officia deserunt mollit anim id est laborum.
50 / 50