Você está na página 1de 75

Introduo aos Compiladores

Introduo aos Compiladores

Introduo aos Compiladores

Sumrio

Introduo

1.1

Linguagens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.2

O que um Compilador? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.3

Processadores de Programas: Compiladores, Interpretadores e Mquinas Virtuais . .

1.4

Organizao de um Compilador . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.4.1

Anlise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.4.2

Sntese . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.5

Por que estudar os compiladores? . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.6

Aplicaes da Tecnologia de Compiladores . . . . . . . . . . . . . . . . . . . . . .

1.7

Exemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.8

Concluso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Anlise Lxica

10

2.1

O Funcionamento da Anlise Lxica . . . . . . . . . . . . . . . . . . . . . . . . . .

10

2.1.1

Implementao Manual de um Analisador Lxico . . . . . . . . . . . . . . .

12

Linguagens Regulares e Expresses Regulares . . . . . . . . . . . . . . . . . . . . .

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

Classes de caracteres, intervalos e negao . . . . . . . . . . . . .

19

2.2.1.4

Metacaracteres e sequncias de escape . . . . . . . . . . . . . . .

20

2.2.1.5

Outras caractersticas . . . . . . . . . . . . . . . . . . . . . . . .

20

2.2.1.6

Alguns exemplos . . . . . . . . . . . . . . . . . . . . . . . . . .

20

2.3

Geradores de Analisadores Lxicos . . . . . . . . . . . . . . . . . . . . . . . . . . .

21

2.4

Uso do flex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

22

2.4.1

Formato da entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

22

2.4.2

Uma especificao simples do flex . . . . . . . . . . . . . . . . . . . . . . .

22

2.4.3

Analisador lxico para expresses usando flex . . . . . . . . . . . . . . . . .

25

2.2

ii

Introduo aos Compiladores


2.4.4
2.5

2.6
3

Lendo um arquivo de entrada . . . . . . . . . . . . . . . . . . . . . . . . . .

30

Anlise Lxica de uma Linguagem de Programao . . . . . . . . . . . . . . . . . .

30

2.5.1

A Linguagem Mini C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

31

2.5.2

O analisador lxico para a linguagem Mini C . . . . . . . . . . . . . . . . .

31

Concluso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

36

Anlise Sinttica

38

3.1

Estrutura sinttica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

38

3.1.1

rvores de expresso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

40

3.2

Relao com o Analisador Lxico . . . . . . . . . . . . . . . . . . . . . . . . . . .

41

3.3

Gramticas Livres de Contexto . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

42

3.3.1

Exemplo: Palndromos . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

43

3.3.2

Derivao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

44

3.3.3

Exemplo: Expresses Aritmticas . . . . . . . . . . . . . . . . . . . . . . .

45

3.3.4

rvores de Derivao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

46

3.3.5

Ambiguidade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

47

3.3.6

Exemplo: Linguagem de programao simples . . . . . . . . . . . . . . . .

49

Geradores de Analisadores Sintticos . . . . . . . . . . . . . . . . . . . . . . . . .

49

3.4

A Instalao de Softwares

51

A.1 Instalao do flex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


B Cdigos completos

52

B.1 Captulo 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

51

52

B.1.1

exp_lexer.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

52

B.1.2

simples.ll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

56

B.1.3

exp_flex/Makefile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

57

B.1.4

exp_flex/exp.ll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

57

B.1.5

exp_flex/exp_tokens.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

58

B.1.6

exp_flex/exp_flex.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

59

B.1.7

minic/minic_tokens.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

61

B.1.8

minic/lex.ll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

63

B.1.9

minic/lex_teste.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

64

ndice Remissivo

67

iii

Introduo aos Compiladores

Prefcio
BAIXANDO A VERSO MAIS NOVA DESTE LIVRO
Acesse https://github.com/edusantana/compiladores-livro/releases para verificar se h
uma verso mais o Histrico de revises, na incio do livro, para verificar o que mudou
entre uma verso e outra.

Como voc deve estudar cada captulo


Leia a viso geral do captulo
Estude os contedos das sees
Realize as atividades no final do captulo
Verifique se voc atingiu os objetivos do captulo
NA SALA DE AULA DO CURSO
Tire dvidas e discuta sobre as atividades do livro com outros integrantes do curso
Leia materiais complementares eventualmente disponibilizados
Realize as atividades propostas pelo professor da disciplina

Caixas de dilogo
Nesta seo apresentamos as caixas de dilogo que podero ser utilizadas durante o texto. Confira os
significados delas.

Nota
Esta caixa utilizada para realizar alguma reflexo.

Dica
Esta caixa utilizada quando desejamos remeter a materiais complementares.

iv

Introduo aos Compiladores

Importante
Esta caixa utilizada para chamar ateno sobre algo importante.

Cuidado
Esta caixa utilizada para alertar sobre algo que exige cautela.

Ateno
Esta caixa utilizada para alertar sobre algo potencialmente perigoso.

Os significados das caixas so apenas uma referncia, podendo ser adaptados conforme as intenes
dos autores.

Vdeos
Os vdeos so apresentados da seguinte forma:

Figura 1: Como baixar os cdigos fontes: http://youtu.be/Od90rVXJV78


Nota
Na verso impressa ir aparecer uma imagem quadriculada.
Isto o qrcode
(http://pt.wikipedia.org/wiki/C%C3%B3digo_QR) contendo o link do vdeo. Caso voc tenha
um celular com acesso a internet poder acionar um programa de leitura de qrcode para
acessar o vdeo.
Na verso digital voc poder assistir o vdeo clicando diretamente sobre o link.

Introduo aos Compiladores

Compreendendo as referncias
As referncias so apresentadas conforme o elemento que est sendo referenciado:
Referncias a captulos
Prefcio [iv]
Referncias a sees
Como voc deve estudar cada captulo [iv], Caixas de dilogo [iv].
Referncias a imagens
Figura 2 [vii]
Nota
Na verso impressa, o nmero que aparece entre chaves [ ] corresponde ao nmero da
pgina onde est o contedo referenciado. Na verso digital do livro voc poder clicar no
link da referncia.

Feedback
Voc pode contribuir com a atualizao e correo deste livro. Ao final de cada captulo voc ser
convidado a faz-lo, enviando um feedback como a seguir:
Feedback sobre o captulo
Voc pode contribuir para melhoria dos nossos livros. Encontrou algum erro? Gostaria de
submeter uma sugesto ou crtica?
Para compreender melhor como feedbacks funcionam consulte o guia do curso.

Nota
A seo sobre o feedback, no guia do curso, pode ser acessado em: https://github.com/edusantana/guia-geral-ead-computacao-ufpb/blob/master/livro/capitulos/livroscontribuicao.adoc.

vi

Introduo aos Compiladores

Figura 2: Exemplo de contribuio

vii

Introduo aos Compiladores

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

linguagem
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 / 68

Introduo aos Compiladores


se tornam simplesmente notaes para descrever algoritmos o que pode ser suficiente para algumas pessoas mas em geral se quer executar esses algoritmos e no apenas descreve-los. Para isso
necessrio ter ao menos um conhecimento mnimo sobre compiladores. Entender mais do que esse
mnimo d ao programador um maior poder e controle sobre questes de eficincia dos seus programas. Tambm importante para aprender a usar melhor as linguagens de programao.

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

Figura 1.1: Estrutura bsica de um compilador.

1.3

Processadores de Programas: Compiladores, Interpretadores e Mquinas Virtuais

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 / 68

Introduo aos Compiladores

programa
programa
compilador
entrada

interpretador

sada
entrada

executvel

sada

Figura 1.2: Fluxo de execuo de um interpretador ( esquerda) e de um compilador ( direita).


Nos sistemas reais, modelos hbridos de interpretao e compilao so comuns. Por exemplo, o compilador Java javac no traduz os programas em linguagem Java para alguma linguagem de mquina
de um processador, mas sim para a linguagem da mquina virtual Java (JVM), constituda de bytecodes. Uma implementao simples da JVM roda o programa compilado em bytecodes interpretando-o.
Atualmente, a maioria das mquinas virtuais Java compilam o programa em bytecode para cdigo
nativo da mquina onde reside antes de executa-lo, para melhorar o desempenho. Isso chamado de
compilao Just In Time, ou JIT. Da mesma forma, os interpretadores reais no analisam e traduzem o
programa inteiro em cada execuo; os programas so normalmente transformados para alguma forma
intermediria e parcialmente analisados para facilitar sua execuo. Tambm comum que mesmo
linguagens compiladas para cdigo nativo tenham um sistema de tempo de execuo (runtime) que
acoplado aos programas traduzidos para cdigo de mquina e que, como o nome esclarece, serve para
dar suporte ao programa durante sua execuo; desta forma, pode-se ter um pouco de interpretao
envolvida. Com vista nestes fatos, difcil dividir exatamente os compiladores dos interpretadores.
Nesta disciplina consideramos principalmente os compiladores, mas muito do que estudado serve
tambm para interpretadores.
Aqui vale a pena considerar a relao entre o modelo semntico de uma linguagem de programao
e uma mquina virtual. De fato, cada linguagem de programao pode ser vista como definindo uma
mquina virtual que a executa. O modelo semntico da linguagem o funcionamento desta mquina.
Um interpretador puro para uma linguagem uma mquina virtual para ela. Como no estudo da
Organizao de Computadores, necessrio organizar as mquinas em camadas. Por isso existe,
em um nvel mais baixo, a linguagem de mquina, que define o modelo de execuo do hardware
em si; logo acima temos o Sistema Operacional, que define uma linguagem com novas primitivas,
conhecidas como chamadas de sistema. Acima do SO podemos ter um compilador de linguagem de
alto nvel que traduz diretamente para cdigo nativo, como o compilador C gcc; ou podemos ter uma
mquina virtual que executa diretamente uma linguagem em bytecode, como o caso da mquina
virtual Java. Acima da JVM temos o compilador javac, que traduz um programa em Java para sua
verso em bytecode.
A definio do que feito em software e o que feito em hardware no absoluta, sendo estabelecida
por motivos de praticidade, desempenho e economia. Poderia se criar um processador que executasse
diretamente a linguagem C, mas seu projeto seria complicadssimo e seu custo muito alto.

3 / 68

Introduo aos Compiladores

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

Figura 1.3: O modelo de anlise e sntese.


Na figura, v-se uma ligao entre as duas partes. O que transmitido entre a anlise e a sntese
uma forma chamada de representao intermediria do programa. como se fosse uma linguagem a
meio caminho entre as linguagens fonte e destino. A representao intermediria a sada da fase
de anlise, e entrada da fase de sntese.
H uma srie de razes para dividir os compiladores desta forma. Uma delas modularizar a construo dos compiladores: a interface entre anlise e sntese fica bem determinada a representao
intermediria. As duas partes ficam ento menos acopladas e mais independentes, podendo ser trocadas sem afetar a outra parte, desde que a interface seja mantida. A construo modular reduz o custo
de suportar vrias linguagens fonte e vrias linguagens destino: digamos que seja necessrio compilar
M linguagens diferentes para N arquiteturas; se for construdo um compilador para cada combinao,
sero necessrios M N compiladores no total. Caso a representao intermediria seja compartilhada, pode-se escrever apenas M mdulos de anlise e N mdulos de sntese, para um esforo total
de M + N (ao invs de M N). Um exemplo dessa tcnica o GCC (GNU Compiler Collection), que
usa as linguagens intermedirias RTL, GENERIC e GIMPLE, o que possibilita que os mdulos de
anlise sejam escritos independente dos mdulos de sntese; de fato, o GCC suporta vrias linguagens
(C, C++, Fortran, etc) e gera cdigo para vrias arquiteturas (Intel x86, Sparc, MIPS, etc).
O ideal deste modelo que a representao intermediria fosse completamente independente tanto
da linguagem fonte como da linguagem de destino. Neste caso seria possvel ter uma representao
intermediria universal, e todos os compiladores poderiam utiliza-la: para criar um compilador para
uma nova linguagem seria necessrio apenas escrever o mdulo de anlise; para suportar uma nova
arquitetura bastaria escrever um mdulo de sntese. Na prtica, entretanto, isto no possvel. Para
gerar cdigo de destino com eficincia aceitvel, a representao intermediria de um compilador vai
depender tanto de caractersticas da linguagem fonte como da linguagem destino.
Agora consideramos em mais detalhe o que os mdulos de anlise e sntese devem fazer.

4 / 68

Introduo aos Compiladores

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 / 68

Introduo aos Compiladores

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.

1.5

Por que estudar os compiladores?

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.

6 / 68

Introduo aos Compiladores


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.

1.6

Aplicaes da Tecnologia de Compiladores

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.
7 / 68

Introduo aos Compiladores


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.

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

8 / 68

Introduo aos Compiladores


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 / 68

Introduo aos Compiladores

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
Organizao prvia
Para acompanhar as explicaes do captulo recomendado que voc instale o software
flex no seu computador, as intrues de instalao se encontram no Apndice A [51], na
Seo A.1 [51].

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

O Funcionamento da Anlise Lxica

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:
10 / 68

Introduo aos Compiladores


1. um tipo;
2. um valor opcional.
O tipo indica que espcie de palavra o token representa: um nmero, um sinal de pontuao, um
identificador (nome de varivel ou funo), etc. O valor usado em alguns tipos de tokens para
armazenar alguma informao adicional necessria. Outras informaes podem ser associadas a cada
token, dependendo das necessidades do compilador. Um exemplo comum a posio no arquivo de
entrada (linha e coluna) onde o token comea, o que ajuda no tratamento de erros.
Um exemplo vai deixar essas ideias mais claras. Vamos usar uma linguagem simples para expresses
aritmticas com operandos constantes, uma linguagem "de calculadora". Na linguagem so permitidos nmeros inteiros (positivos ou negativos), parnteses, e as quatro operaes aritmticas bsicas,
representadas pelos caracteres usuais:
soma +
subtrao multiplicao *
diviso /
Os tipos de tokens so: nmero, operador e pontuao (para representar os parnteses). Todos os trs
tipos precisam armazenar informao no campo de valor do token. Por exemplo, um token do tipo
nmero diz apenas que um nmero foi encontrado, e o valor do nmero guardado no campo de valor
do token.
Um exemplo de programa nessa linguagem o seguinte:
Exemplo de expresso aritmtica
42 + (675 * 31) - 20925

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

11 / 68

Valor
42
SOMA
PARESQ
675
MULT
31
PARDIR
SUB
20925

Introduo aos Compiladores


Um lexema a sequncia de caracteres que d origem a um token. No exemplo atual, o lexema
20925 gera um token de tipo nmero e valor 20925. Note que o lexema um conjunto de caracteres,
a string "20925", enquanto que o valor do token o valor numrico 20925.
Os valores dos tokens de tipo operador representam que operador gerou o token, e os valores do tipo
pontuao funcionam da mesma forma. Os valores so escritos em letra o do analisador lxico
esses valores so representados por constantes numricas.
Para tornar esse exemplo mais concreto, vamos examinar a estrutura da implementao do analisador
lxico para essa linguagem simples.

2.1.1

Implementao Manual de um Analisador Lxico

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 /exp_lexer.c[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
12 / 68

Introduo aos Compiladores


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
recebe o programa de entrada em um arquivo). As informaes guardadas em variveis globais so a
string contendo o cdigo do programa de entrada, o tamanho dessa string e a posio atual da anlise
dentro da string:
Variveis globais para guardar o estado da anlise
// string que contem o codigo que esta em analise
char *codigo;
// tamanho da string com o codigo
int tamanho;
// guarda posicao atual no codigo
int pos;

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.

13 / 68

Introduo aos Compiladores


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
para chegar at o primeiro caractere do prximo token. Isso feito pela seguinte parte da funo
proximo_token:
Cdigo para pular o espao em branco antes do prximo token
c = le_caractere();
while (isspace(c)) {
c = le_caractere();
}

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);
}

Se o primeiro caractere no for um dgito, a anlise testa se um caractere de operador e, se for,


apenas determina qual constante deve ser usada como valor do token:
Leitura de um token operador
else if (strchr(ops, c) != NULL) {
tok->tipo = TOK_OP;
14 / 68

Introduo aos Compiladores


tok->valor = operador(c);
}

A condio no if acima uma forma mais curta de verificar se o caractere um dos operadores, ao
invs de usar quatro comparaes. A constante global ops definida da seguinte forma:
Conjunto de operadores da linguagem
const char *ops = "+-*/";

E a funo strchr da biblioteca padro da linguagem C retorna um valor diferente de NULL se o


caractere c faz parte da string ops. A funo auxiliar operador apenas associa o caractere do
operador com a constante numrica correspondente; por exemplo se c for o caractere +, a funo
operador retorna a constante SOMA. A definio da funo operador pode ser vista no cdigofonte completo do analisador.
A ltima possibilidade de token vlido para essa linguagem ocorre se o primeiro caractere for um
parntese. Nesse caso o tipo do token determinado como pontuao e o valor a constante PARESQ
se o caractere lido foi ( e PARDIR se o caractere lido foi ). Se o caractere no foi nenhuma das possibilidades anteriores (dgito, operador ou parntese) a anlise retorna o valor NULL para indicar uma
falha na anlise. Isso pode ocorrer porque foi encontrado na entrada um caractere que no pertence
linguagem, ou porque a entrada chegou ao fim. Se no ocorreu uma falha de anlise, a funo deve
retornar o token que foi obtido da anlise. Isso nos leva ao final da funo proximo_token:
Final da funo de anlise lxica
else if (c == ( || c == )) {
tok->tipo = TOK_PONT;
tok->valor = (c == ( ? PARESQ : PARDIR);
}
else
return NULL;
return tok;
}

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;
15 / 68

Introduo aos Compiladores


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);
}
else if (strchr(ops, c) != NULL) {
tok->tipo = TOK_OP;
tok->valor = operador(c);
}
else if (c == ( || c == )) {
tok->tipo = TOK_PONT;
tok->valor = (c == ( ? PARESQ : PARDIR);
}
else
return NULL;
return tok;
}

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");

16 / 68

Introduo aos Compiladores


return 0;
}

Executando esse programa para a expresso de exemplo que vimos anteriormente, obtemos a seguinte
saida:
Sada para a expresso 42 + (675 * 31) - 20925
Analise Lexica para Expressoes
Expressao: 42 + (675 * 31) - 20925
=====
Tipo:
Tipo:
Tipo:
Tipo:
Tipo:
Tipo:
Tipo:
Tipo:
Tipo:

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

Linguagens Regulares e Expresses Regulares

As linguagens regulares so um tipo de linguagem formal que so frequentemente utilizadas para


representar padres simples de texto. Uma tcnica de representao muito utilizada para as linguagens
regulares so as chamadas expresses regulares. Bibliotecas que do suporte ao uso de expresses
regulares esto disponveis na maioria das linguagens de programao e so muito usadas para busca
em textos e para validao de entrada textual (para formulrios de entrada de dados, por exemplo).
Uma outra tcnica de representao usada para linguagens regulares so os autmatos finitos. Autmatos finitos e expresses regulares so equivalentes, ou seja, todo padro que pode ser representado
por uma tcnica tambm pode ser representada pela outra. Os autmatos finitos podem ser utilizados para organizar os padres lxicos de uma linguagem, facilitando a implementao direta de um
analisador lxico para ela. Ou seja, com os autmatos finitos podemos criar analisadores lxicos para
linguagens mais complexas, e de maneira mais sistemtica e confivel do que vimos no exemplo da
linguagem de expresses.
Para criar um analisador lxico dessa forma devemos definir os autmatos finitos que representam os
padres associados a cada tipo de token, depois combinar esses autmatos em um nico autmato,
17 / 68

Introduo aos Compiladores


e ento implementar o autmato finito resultante como um programa. Mais detalhes sobre como
fazer isso podem ser encontrados em outros livros sobre compiladores, por exemplo o famoso livro
do drago (Compiladores: Princpios, Tcnicas e Ferramentas, 2a edio, de Aho et al., editora
Pearson/Addison-Wesley).
Aqui vamos usar uma abordagem mais automatizada, criando analisadores lxicos a partir de ferramentas chamadas de geradores de analisadores lxicos. Esses geradores recebem como entrada uma
especificao dos padres que definem cada tipo de token, e criam na sada o cdigo-fonte do analisador lxico. Criar analisadores usando um gerador prtico e temos um certo nvel de garantia que
o cdigo gerado estar correto. Para usar um gerador, no entanto, preciso saber como representar os
padres que definem tipos de tokens da linguagem como expresses regulares.

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.
18 / 68

Introduo aos Compiladores


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:
"ab" (zero repeties do padro interno (a|b)*)
"aab"
"abb"
"aabbbb"
"abbaabab"
2.2.1.2

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

Classes de caracteres, intervalos e negao

As classes de caracteres so uma notao adicional para representar opes de um caractere em um


padro. A classe [abc] representa apenas um caractere, que pode ser a, b ou c. Isso o mesmo que
a expresso (a|b|c), e a notao de classes apenas um atalho, principalmente quando existem
vrias opes.
A expresso [0123456789] representa um caractere que um dgito numrico. Adicionando um
caractere de repetio temos [0123456789]+, que representa strings contendo um ou mais dgitos.
Essas so exatamente as strings, como "145" ou "017", que representam constantes inteiras.
Quando uma classe inclui vrios caracteres em uma sequncia, como o exemplo anterior, podemos
usar intervalos para tornar as expresses mais compactas. Por exemplo, a expresso [0123456789]
pode ser igualmente representada pelo intervalo [0-9]. A expresso [a-z] representa uma letra
minscula. Podemos usar vrios intervalos em uma classe. Por exemplo, [A-Za-z] representa uma
letra maiscula ou minscula, e [0-9A-Za-z] representa um dgito ou letra. Note que cada classe
ainda representa apenas um caractere; os intervalos apenas criam novas opes para esse caractere.
Algumas classes especiais podem ser usadas como abreviaes. Por exemplo [:alpha:] representa um caractere alfabtico (ou seja, o mesmo que [A-Za-z]), e [:alnum:] representa um
caractere alfabtico ou um dgito. Outras classes especiais teis so [:space:] para caracteres de
espao em branco, [:upper:] para caracteres maisculos e [:lower:] para caracteres minsculos. Existe a classe especial [:digit:] para dgitos, mas em geral mais compacto escrever
[0-9]. importante lembrar que essas classes especiais, assim como os intervalos, s podem ser
usados dentro de classes de caracteres, ou seja, no possvel ter uma expresso que seja apenas
[:alpha:]; preciso colocar a classe especial [:alpha:] dentro de uma classe, resultando na
expresso [[:alpha:]], que representa um caractere que pode ser qualquer letra (maiscula ou
minscula).
19 / 68

Introduo aos Compiladores

Importante
Ateno ao uso dos colchetes ([]) nas classes especiais, eles fazem parte da definio
da classe. Quando utilizamos as classes especiais dentro de um intervalo ns teremos
dois colchetes. Por exemplo, a expresso para nmeros de 0 a 7 ou letras maisculas :
[0-7[:upper]].

exemplo, para identificar numeros de 1 a 7, ou letras maisculas temos: [0-7[:upper:]].


Uma outra notao til com classes a negao. Usar um caractere no comeo de uma classe
representa caracteres que no esto na classe. Por exemplo, [0-9] representa um caractere que
no um dgito de 0 a 9. A negao tambm pode ser usada com classes especiais: [:alnum:]
representa um caractere que no uma letra ou dgito.
2.2.1.4

Metacaracteres e sequncias de escape

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
p um padro, p{2} representa exatamente duas repeties do padro, p{2,} representa duas ou
mais repeties, e p{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".
Dica
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 ou atravs do comando info flex nos sistemas Unix.

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.
20 / 68

Introduo aos Compiladores


[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
nome@dominio.com, mas no um endereo que tenha mais de um ponto aps a arroba (por
exemplo um email do Centro de Informtica da UFPB, da forma nome@ci.ufpb.br).
Agora que sabemos como especificar padres de texto usando expresses regulares no flex, podemos
us-lo para gerar analisadores lxicos.

2.3

Geradores de Analisadores Lxicos

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

Figura 2.1: Uso de um gerador de analisadores lxicos (flex).


21 / 68

Introduo aos Compiladores

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.

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

Uma especificao simples do flex

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.
Cdigo fonte /simples.ll[code/cap2/simples.ll]
Especificao simples para o flex
22 / 68

Introduo aos Compiladores

%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();
}

Opo para no precisar criar uma funo yywrap

Definies

Regras usando as definies

Cdigo: funo main para o programa do analisador

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:
[0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}
23 / 68

{ printf("CPF\n"); }

Introduo aos Compiladores


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 :
.

{ printf("Token nao reconhecido\n"); }

Como j vimos, o ponto um metacaractere no flex que representa qualquer caracter. O padro .
(carcter .) 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.

O cdigo nesse caso define uma funo main, para que o cdigo C gerado pelo flex possa ser executado diretamente. A funo main apenas chama a funo yylex() que a funo principal do
24 / 68

Introduo aos Compiladores


analisador lxico criado pelo flex. Todo arquivo C gerado pelo flex contm uma funo yylex. O
comportamento dessa funo pode ser alterado de vrias formas que veremos adiante, mas se chamada
diretamente, da forma que fazemos nesse exemplo, ela funciona da seguinte forma: recebe tokens na
entrada padro (lendo do teclado) e executa as aes associadas para cada regra que satisfeita; esse
processo continua at que o fim de arquivo seja encontrado. Isso funciona bem como um forma de
testar os padres usados na especificao.
Com o arquivo de especificao acima, podemos gerar o cdigo C correspondente chamando a ferramenta flex na linha de comando. Por conveno, os arquivos de especificao do flex tm extenso
.ll. Depois de gerar um arquivo com cdigo C, este pode ser compilado e executado diretamente
(j que ele possui uma funo main). Em um sistema Unix, a sequncia de comandos a seguinte:
Gerao e compilao do arquivo C
Sandman:cap2 andrei$ flex -o simples.c simples.ll
Sandman:cap2 andrei$ gcc -o simples simples.c
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

Analisador lxico para expresses usando flex

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.
O analisador composto pelo arquivo de especificao, exp.ll, um arquivo C que chama o analisador lxico (exp_flex.c), e um arquivo de cabealho com definies (exp_tokens.h).
25 / 68

Introduo aos Compiladores

Nota
Voc pode consultar os cdigos completos destes arquivos no Apndice B [52].

O arquivo de cabealho contm as definies de tipos e constantes, iguais ao analisador que foi mostrado anteriormente:
Cdigo fonte /exp_flex/exp_tokens.h[code/cap2/exp_flex/exp_tokens.h]
O contedo principal desse arquivo :
Arquivo de cabealho para analisador lxico de expresses
// constantes booleanas
#define TRUE
1
#define FALSE
0
// constantes para tipo de token
#define TOK_NUM
0
#define TOK_OP
1
#define TOK_PONT
2
#define TOK_ERRO
3
// constantes para valores de operadores
#define SOMA
0
#define SUB
1
#define MULT
2
#define DIV
3
// constantes para valores de pontuacao (parenteses)
#define PARESQ
0
#define PARDIR
1
// estrutura que representa um token
typedef struct
{
int tipo;
int valor;
} Token;
// funcao para criar um token
extern Token *token();
// funcao principal do analisador lexico
extern Token *yylex();

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).
26 / 68

Introduo aos Compiladores


Em seguida vamos ver o arquivo de especificao para essa linguagem, que contm algumas novidades em relao ao que vimos antes:
Cdigo fonte /exp_flex/exp.ll[code/cap2/exp_flex/exp.ll]
Arquivo de especificao do flex para a linguagem de expresses
x

%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 token(TOK_ERRO, 0);

return
return
return
return
return
return
return

atoi(yytext)); } 3x
SOMA);
}
SUB);
}
MULT);
}
DIV);
}
PARESQ); }
PARDIR); }
}

// erro para
// token desconhecido

%%
x

// variavel global para um token


Token tok;

Token * token(int tipo, int valor)


{
tok.tipo = tipo;
tok.valor = valor;
return &tok;
}

Opes: sem regra default, nomes dos arquivos gerados

Trecho de cdigo para incluir no topo do arquivo gerado

Uso do lexema casado pelo padro: a varivel yytext

Cdigo para incluir no final do arquivo gerado

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
27 / 68

Introduo aos Compiladores


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
gerado, mas tambm garantir que ser gerado um arquivo de cabealho com definies do analisador
lxico (nesse caso, o arquivo lexer.h).
A opo nodefault deve ser usada para evitar que o flex inclua uma regra padro (default) se
nenhuma outra regra for satisfeita. A regra padro do flex apenas mostra na sada os caracteres que
no forem reconhecidos em alguma regra. Isso significa que erros lxicos no programa de entrada
no sero reconhecidos, e que o analisador vai gerar sada desnecessria.
Apenas uma definio criada, a definio NUM para constantes numricas. A primeira regra ignora
quaisquer caracteres de espao (espaos, tabulaes, caracteres de nova linha). Essa regra tem uma
ao vazia, o que indica que os espaos devem ser apenas ignorados. As outras regras seguem a
estrutura lxica da linguagem de expresses, que j vimos antes. O nico cuidado adicional que
muitos dos caractere usados na linguagem so caracteres especiais no flex (+, *, barra e os parnteses)
e portanto precisam ser includos no padro com uma contrabarra antes. Cada ao apenas cria um
token com o tipo e o valor adequados, usando a funo token que veremos a seguir.
A regra que cria tokens de tipo nmero precisa obter o valor do nmero, e isso depende da sequncia
de caracteres que foi casada com o padro. Ou seja, o analisador precisa ter acesso ao lexema que
foi identificado para gerar o token. Analisadores gerados pelo flex incluem uma varivel chamada
yytext que guarda os caracteres casados pelo padro da regra que foi disparada. Por isso, a regra para tokens de tipo nmero obtem o valor chamando a funo atoi da linguagem C na cadeia
yytext, como visto na regra:
{NUM}

{ 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 as 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 /exp_flex/exp_flex.c[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");

28 / 68

Introduo aos Compiladores


tok = proximo_token();
while (tok != NULL) {
imprime_token(tok);
tok = proximo_token();
}
printf("\n");
return 0;
}

A funo principal quase igual ao analisador criado diretamente, mas as funes inicializa_analise() e proximo_token() so diferentes.
A funo
proximo_token() apenas chama a funo principal do analisador gerado pelo flex, yylex():
Funo para obter o prximo token
Token *proximo_token()
{
return yylex();
}

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:


1. Gerar o arquivo C do analisador usando o flex;
2. 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:
Comando para gerar os arquivos lexer.c e lexer.h
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:
Comandos para compilar os arquivos lexer.c e exp_flex.c gerando o executvel exp
gcc -o exp lexer.c exp_flex.c
29 / 68

Introduo aos Compiladores


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.

2.4.4

Lendo um arquivo de entrada

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

Anlise Lxica de uma Linguagem de Programao

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.

30 / 68

Introduo aos Compiladores

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.
Discutiremos mais sobre a sintaxe da linguagem Mini C no prximo captulo. Aqui o que interessa
a estrutura lxica da linguagem, ou seja, que tipos de token ocorrem na linguagem e quais devem
ser os valores associados a eles. J que o analisador lxico encarregado de retirar os comentrios do
cdigo-fonte, tambm nesta fase precisamos definir a sintaxe para comentrios. A linguagem C tem
atualmente dois tipos de comentrios: os comentrios que podem se extender por mltiplas linhas,
delimitados por /* e */, e os comentrios de linha nica, que comeam com // e vo at o final da
linha. Vamos adotar esse ltimo tipo de comentrio na linguagem Mini C, pois ele um pouco mais
simples de tratar no analisador lxico.
Os tipos de tokens da linguagem Mini C so:
identificadores (nomes de variveis e funes)
palavras-chave (if, else, while, return e printf)
constantes numricas
strings (delimitadas por aspas duplas ")
pontuao (parnteses, chaves, etc.)
Os identificadores devem ser iniciados por uma letra, seguida de zero ou mais letras ou dgitos. As
constantes numricas so formadas por um ou mais dgitos, e como pontuao inclumos as chaves
{ e }, os parnteses ( e ), a vrgula e o ponto-e-vrgula. Nos operadores esto includos operadores
aritmticos (as quatro operaes bsicas), comparaes (a operao de menor e de igualdade), e dois
operadores lgicos (E-lgico e negao).
Como a ideia que os programas Mini C sejam compatveis com compiladores C, todo programa
Mini C deve poder ser compilado como se fosse um programa C. Os programas Mini C podem usar
printf para imprimir na tela (em Mini C, printf uma palavra-chave, no uma funo como em
C), e portanto para que isso no crie problemas com compiladores C, preciso que os programas Mini
C tenham a linha #include <stdio.h> no comeo. Por isso, um token especial na linguagem
Mini C o chamado de prlogo, que a string #include <stdio.h>.
Nos captulos seguintes veremos mais detalhes sobre a linguagem Mini C, mas neste captulo vamos
apenas trabalhar a estrutura lxica da linguagem.

2.5.2

O analisador lxico para a linguagem Mini C

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.
31 / 68

Introduo aos Compiladores


Constantes para os tipos e valores de tokens so definidos no arquivo de cabealho
minic_tokens.h.
Cdigo fonte /minic/minic_tokens.h[code/cap2/minic/minic_tokens.h]
A parte mais importante do contedo deste arquivo mostrada abaixo:
Definio de constantes para o analisador lxico
// 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

// valores para palavra-chave


#define PC_IF
#define PC_ELSE
#define PC_WHILE
#define PC_RETURN
#define PC_PRINTF

0
1
2
3
4

// valores para pontuacao


#define PARESQ
#define PARDIR
#define CHVESQ
#define CHVDIR
#define VIRG
#define PNTVIRG

1
2
3
4
5
6

// valores para operadores


#define SOMA
#define SUB
#define MULT
#define DIV
#define MENOR
#define IGUAL
#define AND
#define NOT
#define ATRIB

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 /minic/lex.ll[code/cap2/minic/lex.ll]
32 / 68

Introduo aos Compiladores


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
%option nodefault
%option outfile="lexer.c" header-file="lexer.h"
%top {
#include "minic_tokens.h"
#include "tabelas.h"
// prototipo da funcao token
Token *token(int, int);
}
NUM [0-9]+
ID [[:alpha:]]([[:alnum:]])*
STRING \"[^\"\n]*\"

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]*

} /* ignora espacos em branco */


{ } /* elimina comentarios */

"#include <stdio.h>"

{ return token(TOK_PROLOGO, 0); }

{STRING} { return token(TOK_STRING, adiciona_string(yytext)); }


if
else
while
return
printf

{
{
{
{
{

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); }

33 / 68

Introduo aos Compiladores

{NUM}
{ID}

{ return token(TOK_NUM, atoi(yytext)); }


{ return token(TOK_ID, adiciona_simbolo(yytext)); }

\+
\*
\/
\<
==
&&
!
=

{
{
{
{
{
{
{
{
{

return
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,
token(TOK_OP,

\(
\)
\{
\}
,
;

{
{
{
{
{
{

return
return
return
return
return
return

token(TOK_PONT,
token(TOK_PONT,
token(TOK_PONT,
token(TOK_PONT,
token(TOK_PONT,
token(TOK_PONT,

{ return token(TOK_ERRO, 0);

SOMA);
SUB);
MULT);
DIV);
MENOR);
IGUAL);
AND);
NOT);
ATRIB);

}
}
}
}
}
}
}
}
}

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 /minic/lex_teste.c[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) {
34 / 68

Introduo aos Compiladores


imprime_token(tok);
tok = yylex();
}
finaliza_analise();
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;
}

E a funo de finalizao destri as tabelas de smbolos e de strings, e fecha o arquivo de entrada:


Funo de finalizao da anlise lxica
void finaliza_analise()
{
// destroi tabelas
destroi_tab_strings();
destroi_tab_simbolos();
// fecha arquivo de entrada
fclose(yyin);
}

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 /minic/teste.c[code/cap2/minic/teste.c]
Arquivo de teste na linguagem Mini C
// teste para o compilador Mini C
#include <stdio.h>
35 / 68

Introduo aos Compiladores

int main()
{
printf("Ola, mundo!\n");
return 0;
}

Compilando o programa principal lex_teste.c para gerar um executvel mclex, obtemos a seguinte sada para o arquivo teste.c:
Execuo do analisador mclex sobre o arquivo teste.c
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

Nota
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
36 / 68

Introduo aos Compiladores


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.

37 / 68

Introduo aos Compiladores

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
38 / 68

Introduo aos Compiladores


possa acessar esses componentes para poder gerar cdigo executvel para o if. Por isso, no basta
apenas agrupar os caracteres em tokens, preciso tambm agrupar os tokens em estruturas sintticas.
Uma forma comum de representar a estrutura sinttica usando rvores. As rvores na cincia da
computao so normalmente desenhadas de cabea para baixo, com a raiz no topo (ver Figura 3.1
[39]). A raiz um n da rvore da qual todo o resto se origina. Cada n pode ter ns filhos que
aparecem abaixo dele, ligados por um arco ou aresta. As folhas so os ns que no possuem filhos e
portanto aparecem nas pontas da rvore.

Topo da rvore

Arco ou Aresta
B

C
Folhas
B pai de C
C lho de B

(ns que no
possuem lhos)

Figura 3.1: Representao usual de uma rvore na computao: com a raiz no topo e folhas em baixo.
Uma rvore que representa a estrutura sinttica de um programa normalmente chamada de rvore
sinttica. Para o exemplo do comando condicional mencionado antes, a rvore sinttica seria similar
mostrada na Figura 3.2 [39]. Como filhos do n if (a raiz) existem trs ns, o primeiro representando a condio, o segundo o comando para o caso da condio ser verdadeira (C1) e o terceiro
representando o comando para o caso da condio ser falsa (C2).

if

cond

C1

C2

Figura 3.2: rvore sinttica para o comando condicional completo.


O papel da anlise sinttica construir uma rvore, como a da Figura 3.2 [39], para todas as partes de
um programa. Essa rvore incompleta pois a condio e os comandos C1 e C2 possuem estrutura
tambm, e essa estrutura precisa estar representada na rvore completa. Para discutir a estrutura
dessas partes, vamos ver um pouco mais sobre rvores sintticas, especificamente sobre rvores de
expresso.
39 / 68

Introduo aos Compiladores

3.1.1

rvores de expresso

rvores de expresso so rvores sintticas construdas para expresses aritmticas, relacionais ou


lgicas. Uma expresso formada por operadores, que designam as operaes que fazem parte da
expresso, e operandos, que designam os valores ou sub-expresses sobre os quais os operadores
agem. Em uma rvore de expresso, os operadores so ns internos (ns que possuem filhos) e os
valores bsicos so folhas da rvore.
Um exemplo a expresso 2 + 3, cuja rvore sinttica mostrada na Figura Figura 3.3 [40]a. O
operador + e os operandos so 2 e 3. Uma expresso mais complexa (2 + 3) * 7, cuja
rvore mostrada na Figura Figura 3.3 [40]b. O operando direito da multiplicao o nmero 7,
mas o operando esquerdo a sub-expresso 2 + 3. Note que como a estrutura da expresso fica
evidenciada pela estrutura da rvore, no necessrio usar parnteses na rvore para (2 + 3) *
7, apesar dessa expresso precisar de parnteses. Os parnteses so necessrios para representar a
estrutura da expresso como uma string, uma sequncia linear de caracteres, mas no na representao
em rvore.

*
+
+
2

3
2
(a)

3
(b)

Figura 3.3: rvore sinttica para duas expresses, (a) 2 + 3 (b) (2 + 3) * 7.


As operaes em expresses so geralmente binrias, porque precisam de dois operandos. Algumas
operaes so unrias, como a negao de um nmero ou o NO lgico. Por isso, as rvores de
expresso so compostas por ns que podem ter 0, 1 ou 2 filhos. Esse tipo de rvore normalmente
chamado de rvore binria.
Em um comando como o condicional if, podemos pensar que o if um operador com trs operandos: condio, comando 1 e comando 2. Voltando rvore para um comando condicional, digamos
que um trecho do cdigo do programa que est sendo processado pelo compilador o seguinte:
Exemplo de comando condicional
if (x > 2)
y = (x - 2) * 7;
else
y = x + 2 * 5;

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.4 [41].
40 / 68

Introduo aos Compiladores

if
=

>
x

*
x

7
2

+
x

*
2

Figura 3.4: rvore sinttica completa para um comando condicional.


O analisador sinttico de um compilador vai gerar, para o cdigo mostrado antes, a rvore na Figura 3.4 [41]. As tcnicas para fazer isso sero estudadas no resto do captulo.

3.2

Relao com o Analisador Lxico

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.5
[41].

caracteres

lxico

tokens

sinttico

rvore

Figura 3.5: Relao simplificada entre analisadores lxico e sinttico.


Na prtica, as duas etapas so organizadas de forma diferente nos compiladores reais. No necessrio, para o analisador sinttico, ter acesso a toda a sequncia de tokens para fazer a anlise sinttica.
Na maioria dos casos, possvel construir a rvore sinttica examinando apenas um, ou um nmero
pequeno de tokens de cada vez. Por isso, o mais comum fazer com que o analisador sinttico e
o analisador lxico funcionem em conjunto, ao invs do lxico terminar todo seu processamento e
passar o resultado para o sinttico. Nesse arranjo, o analisador sinttico est no comando, por assim
dizer: o analisador sinttico que aciona o analisador lxico, quando necessrio para obter o prximo token da entrada. O analisador lxico deve manter controle sobre que partes da entrada j foram
41 / 68

Introduo aos Compiladores


lidas e a partir de onde comea o prximo token. Essa relao ilustrada na Figura 3.6 [42], onde
proxtoken() a funo do analisador lxico que deve ser chamada para obter o prximo token;
o analisador sinttico chama essa funo sempre que necessrio, obtem o prximo token, e continua
com a anlise.

proxtoken()
caracteres

lxico

sinttico

rvore

token
Figura 3.6: 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.6 [42] 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

Gramticas Livres de Contexto

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.
As gramticas livres de contexto esto associadas s linguagens livres de contexto. Assim como a
classe das linguagens regulares usada na anlise lxica, a classe das linguagens livres de contexto
essencial para a anlise sinttica. Aqui no vamos nos preocupar com linguagens livres do contexto
em geral, apenas usando as gramticas como ferramentas para fazer a anlise sinttica.
42 / 68

Introduo aos Compiladores


Uma gramtica livre do contexto G especificada por quatro componentes: o conjunto de smbolos
terminais T , o conjunto de smbolos variveis (ou no-terminais) V , o conjunto de produes P e o
smbolo inicial S, sendo que o smbolo inicial deve ser um dos smbolos variveis (S V ).
As gramticas funcionam como um formalismo gerador, similar s expresses regulares: comeando
pelo smbolo inicial, possvel usar as produes para gerar cadeias ou sentenas da linguagem que
desejamos. Os smbolos terminais representam smbolos que aparecem na linguagem, enquanto que
os smbolos variveis so usados como smbolos auxiliares durante as substituies. Veremos alguns
exemplos para tornar essas ideias mais claras.

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.

43 / 68

Introduo aos Compiladores

3.3.2

Derivao

Vamos comear estabelecendo algumas definies necessrias:


Uma sentena em uma gramtica uma sequncia de smbolos terminais. Para a gramtica de palndromos com a e b, abba uma sentena.
Uma forma sentencial de uma gramtica uma sequncia de smbolos terminais e variveis. Uma
forma sentencial pode ser formada apenas por smbolos variveis, apenas por smbolos terminais, ou
uma mistura dos dois tipos. Dessa forma, toda sentena uma forma sentencial, mas uma forma
sentencial que inclua algum smbolo varivel no uma sentena. Para a gramtica de palndromos
em a e b, aSa uma forma sentencial (mas no sentena), enquanto aaa uma forma sentencial
que tambm uma sentena.
Uma derivao na gramtica G uma sequncia de formas sentenciais tal que:
1. A primeira forma sentencial da sequncia apenas o smbolo inicial da gramtica G
2. A ltima forma sentencial uma sentena (ou seja, s tem smbolos terminais)
3. Cada forma sentencial na sequncia (exceto a primeira) pode ser obtida da forma sentencial
anterior pela substituio de um smbolo varivel pelo lado direito de uma de suas produes
Um exemplo simples de derivao na gramtica de palndromos :
Sa
Essa derivao tem apenas duas formas sentenciais: S, que o smbolo inicial, e a, que uma sentena. Para separar as formas sentenciais em uma derivao usamos o smbolo . A derivao
demonstra que a cadeia a uma sentena da linguagem gerada pela gramtica, e ela obtida a partir
do smbolo S pelo uso da terceira produo da gramtica, S a. Como especificado pela produo,
substitumos o smbolo S pelo smbolo a, gerando assim a segunda forma sentencial; nesse caso, a
segunda forma sentencial j uma sentena, e a derivao termina por a (at porque no existem
mais smbolos variveis na forma sentencial).
Uma derivao com um passo a mais seria:
S aSa aa
A sentena gerada nessa derivao aa. No primeiro passo da derivao, substitumos o smbolo S
por aSa, usando a sua primeira produo. No segundo passo o smbolo S entre os dois a substitudo
pela cadeia vazia (a ltima produo na gramtica), desaparecendo e deixando apenas os dois as.
Agora vejamos a derivao para gerar a cadeia abba:
S aSa abSba abba
Os dois primeiros passos mostram S sendo substitudo por aSa e bSb, nesta ordem. O ltimo passo
mais uma vez substitui o S pela cadeia vazia, fazendo com que ele desaparea da forma sentencial.
Para gerar ababa a derivao similar, mudando apenas no ltimo passo:
S aSa abSba ababa
Desta vez, ao invs de substituir S pela cadeia vazia no ltimo passo, substitumos por a, obtendo o
resultado esperado. Podemos ver que a derivao para um palndromo par termina com a substituio
44 / 68

Introduo aos Compiladores


de S pela cadeia vazia no ltimo passo, enquanto que a derivao para um palndromo mpar termina
com S substitudo por a ou b.
Qualquer derivao usando a gramtica para palndromos vai gerar, ao final, uma sentena que um
palndromo usando os dois smbolos a e b. No h como, seguindo as produes da gramtica, gerar
uma sentena que no um palndromo usando esses dois smbolos. O conjunto de todas as sentenas
geradas por uma gramtica livre de contexto a linguagem gerada pela gramtica.
A ideia usar as gramticas para descrever as estruturas sintticas que podem ser formadas na linguagem que queremos analisar. Isso parecido com o que vimos na anlise lxica, de usar expresses
regulares para descrever os padres de tokens que podem ser usados na linguagem.
Agora que j entendemos como especificar uma gramtica livre de contexto e o processo de derivao
a partir dela, vamos ver mais alguns exemplos de linguagens e suas estruturas sintticas descritas por
gramticas.

3.3.3

Exemplo: Expresses Aritmticas

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).
45 / 68

Introduo aos Compiladores


Vejamos algumas derivaes nessa gramtica. Comeando por uma expresso simples, 142 + 17.
A sequncia de tokens associada a essa expresso <num, 142> <op, SOMA> <num, 17>.
Na derivao a seguir vamos representar os tokens da mesma forma que na gramtica (ou seja, <op,
SOMA> vira apenas +, e qualquer token de tipo nmero representado apenas como num):
E E + E num + E num + num
Em cada passo de derivao substitumos um smbolo varivel pelo lado direito de uma de suas
produes. Na derivao anterior, quando chegamos na forma sentencial E + E, temos a opo de
substituir o E da esquerda ou o da direita; no caso, escolhemos o da esquerda. Mas o resultado seria
o mesmo se tivssemos comeado pelo E da direita. Apenas a sequncia de passos da derivao
apareceria em outra ordem, mas o resultado final seria o mesmo, e a estrutura sinttica da expresso
seria a mesma.
Podemos estabelecer algumas ordens padronizadas, por exemplo em uma derivao mais esquerda,
quando h uma escolha de qual smbolo varivel substituir, sempre escolhemos o smbolo mais
esquerda (como no exemplo anterior). Da mesma forma podemos falar de uma derivao mais
direita.
Mas existe uma forma melhor de visualizar uma derivao, uma forma que deixa mais clara a estrutura
sinttica de cada sentena derivada, e que no depende da ordem dos smbolos variveis substitudos.
Essa forma so as rvores de derivao.

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:

E
E
E

num
num

num

Figura 3.7: rvores de derivao para duas expresses.


46 / 68

Introduo aos Compiladores


Vemos que quando o smbolo E substitudo apenas por num, o n correspondente na rvore s tem
um filho (ver rvore esquerda na Figura 3.7 [46]). Quando o smbolo E substitudo por E + E, isso
significa que o n correspondente na rvore ter trs filhos (ver rvore direita na Figura 3.7 [46]).
Para uma rvore como a que est mostrada no lado direito da Figura 3.7 [46], no importa a ordem de
substituio dos dois smbolos E na forma sentencial E + E; qualquer que seja a ordem, a rvore de
derivao ser a mesma.
Entretanto, existem sentenas geradas por essa gramtica de expresses para as quais ns podemos
encontrar mais de uma rvore de derivao. Quando temos mais de uma rvore de derivao para
uma mesma sentena, dizemos que a gramtica ambgua, e a ambiguidade de uma gramtica um
problema, como veremos a seguir.

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
2. ou uma multiplicao com operando esquerdo 6 e operando direito 5 + 12
Ns somos acostumados com a conveno de sempre fazer multiplicaes e divises antes de somas
e subtraes, ento para ns o mais natural seguir a primeira interpretao. Mas a gramtica que
vimos no estabelece nenhuma interpretao, possibilitando as duas. Para essa mesma sentena, nesta
gramtica, duas rvores de derivao podem ser construdas:

47 / 68

Introduo aos Compiladores


E

num

num

num

num

num

num

Figura 3.8: Duas rvores de derivao para a sentena 6 * 5 + 12


Cada uma das rvores representa uma das duas interpretaes para a expresso. A rvore da esquerda
representa a primeira interpretao: para realizar a soma necessrio obter o valor dos seus dois operandos, sendo que o operando esquerdo da soma a multiplicao 6 * 5; portanto, a multiplicao
seria realizada primeiro. A rvore direita da Figura 3.8 [48] representa a segunda interpretao, que
seria calcular primeiro a soma 5 + 12 e depois multiplicar por 6.
O que queremos que a prpria gramtica evite a ambiguidade, determinando apenas uma das duas
rvores de derivao para uma sentena como 6 * 5 + 12, e que essa rvore corresponda interpretao esperada: que a multiplicao deve ser efetuada antes da soma. Para isso precisamos
construir uma nova gramtica, que codifica nos smbolos variveis os diferentes nveis de precedncia dos operadores:
E E +T | E T | T
T T F | T /F | F
F (E) | num
Essa gramtica tem trs smbolos variveis E, T e F (que podemos pensar como expresso, termo e
fator). Cada um representa um nvel de precedncia:
o smbolo E representa a precedncia mais baixa, onde esto os operadores de soma e subtrao.
T representa o prximo nvel de precedncia, com os operadores de multiplicao e diviso.
F representa o nvel mais alto de precedncia, onde ficam os nmeros isolados e as expresses entre
parnteses; isso significa que o uso de parnteses se sobrepe precedncia de qualquer operador,
como esperado.
Esta gramtica gera as mesmas sentenas que a primeira gramtica de expresses que vimos, mas
sem ambiguidade. Nesta gramtica, existe apenas uma rvore de derivao para a sentena 6 * 5
+ 12, mostrada na Figura 3.9 [49].

48 / 68

Introduo aos Compiladores


E

num

num

num

Figura 3.9: rvore de derivao para a sentena 6 * 5 + 12 na nova gramtica


A rvore mostrada na Figura 3.9 [49] mais complexa do que as rvores da Figura 3.8 [48], mas essa
complexidade adicional necessria para evitar a ambiguidade.
Toda linguagem de programao tem uma parte para expresses aritmticas, relacionais e lgicas.
Isso significa que a gramtica para uma linguagem de programao vai incluir uma parte para expresses. Essa parte da gramtica de qualquer linguagem de programao segue a mesma ideia vista no
ltimo exemplo: usado um smbolo varivel para cada nvel de precedncia. Como as expresses
em uma linguagem de programao completa pode ter vrios nveis de precedncia (bem mais do
que trs), essa acaba se tornando uma parte grande da gramtica da linguagem. A seguir veremos um
exemplo de gramtica para uma linguagem de programao simples.

3.3.6

Exemplo: Linguagem de programao simples

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
as quais teremos que lidar para criar o analisador sinttico de um compilador.
C print string
C if R then C else C
C num := E
RR=E |R<E |E
E E +T | E T | T
T T F | T /F | F
F (E) | num | id

3.4

Geradores de Analisadores Sintticos

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;

49 / 68

Introduo aos Compiladores


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.10 [50] 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

Figura 3.10: Uso de um gerador de analisadores sintticos

50 / 68

Introduo aos Compiladores

Apndice A
Instalao de Softwares
A.1

Instalao do 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:
Mac OS X
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.
Linux
Em sistemas Linux o flex deve estar disponvel como um pacote no sistema gerenciador de pacotes da distribuio. Por exemplo, no Ubuntu a instalao pode ser feita digitando no terminal:
sudo apt-get install flex
Windows
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
Voc pode testar a instalao do flex passando o parmetro --version, que ir retornar a verso do
flex instalado:
Testando a instalao do flex no terminal
$ flex --version
flex 2.5.35

51 / 68

Introduo aos Compiladores

Apndice B
Cdigos completos
Neste captulo apresentamos os cdigos completos dos arquivos, pois durante os captulos optamos
por apresentar, didaticamente, apenas os trechos mais relevantes ao que estava sendo explicado.

B.1

Captulo 2

B.1.1

exp_lexer.c

Cdigo fonte /exp_lexer.c[code/cap2/exp_lexer.c]


exp_lexer.c
//
// exp_lexer.c
// Analisador lexico para linguagem de expressoes aritmeticas
//
// Andrei de Araujo Formiga, 2014-07-25
//
#include
#include
#include
#include

<stdio.h>
<stdlib.h>
<ctype.h>
<string.h>

// --- definicao de constantes e tipos ----------------------------// constantes booleanas


#define TRUE
1
#define FALSE
0
// constantes para tipo de token
#define TOK_NUM
0
#define TOK_OP
1
#define TOK_PONT
2
// constantes para valores de operadores
52 / 68

Introduo aos Compiladores


#define
#define
#define
#define

SOMA
SUB
MULT
DIV

0
1
2
3

// constantes para valores de pontuacao (parenteses)


#define PARESQ
0
#define PARDIR
1
// estrutura que representa um token
typedef struct
{
int tipo;
int valor;
} Token;

// --- variaveis globais ------------------------------------------// string contendo os caracteres de operadores


const char *ops = "+-*/";
// string que contem o codigo que esta em analise
char *codigo;
// tamanho da string com o codigo
int tamanho;
// guarda posicao atual no codigo
int pos;

// --- funcoes ----------------------------------------------------// funcao utilitaria para obter proximo caractere do codigo
// retorna -1 quando chega ao final da string
char le_caractere(void)
{
char c;
if (pos < tamanho) {
c = codigo[pos];
pos++;
}
else
c = -1;
return c;
}
// determina se um caractere eh um operador, e retorna o tipo se for
int operador(char c)
{
53 / 68

Introduo aos Compiladores


int res;
switch (c) {
case +:
res = SOMA;
break;
case -:
res = SUB;
break;
case *:
res = MULT;
break;
case /:
res = DIV;
break;
default:
res = -1;
}
return res;
}
// inicializa a analise lexica do codigo em uma string
void inicializa_analise(char *prog)
{
codigo = prog;
tamanho = strlen(codigo);
pos = 0;
}
// funcao que faz a analise lexica, retornando o proximo token
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;
54 / 68

Introduo aos Compiladores


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);
}
else if (strchr(ops, c) != NULL) {
tok->tipo = TOK_OP;
tok->valor = operador(c);
}
else if (c == ( || c == )) {
tok->tipo = TOK_PONT;
tok->valor = (c == ( ? PARESQ : PARDIR);
}
else
return NULL;
return tok;
}
// imprime um token
char *operador_str(int op)
{
char *res;
switch (op) {
case SOMA:
res = "SOMA";
break;
case SUB:
res = "SUB";
break;
case MULT:
res = "MULT";
break;
case DIV:
res = "DIV";
break;
default:
res = "NENHUM";
}
return res;
}

55 / 68

Introduo aos Compiladores


void imprime_token(Token *tok)
{
printf("Tipo: ");
switch (tok->tipo) {
case TOK_NUM:
printf("Numero \t -- Valor: %d\n", tok->valor);
break;
case TOK_OP:
printf("Operador \t -- Valor: %s\n", operador_str(tok->valor));
break;
case TOK_PONT:
printf("Pontuacao -- Valor: %s\n", (tok->valor == PARESQ ? " PARESQ" : "PARDIR"));
break;
default:
printf("TIPO DE TOKEN DESCONHECIOO\n");
}
}

// --- funcao principal -------------------------------------------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;
}

B.1.2

simples.ll

Cdigo fonte /simples.ll[code/cap2/simples.ll]


simples.ll
56 / 68

Introduo aos Compiladores

%option noyywrap
CPF [0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}
EMAIL [[:alnum:]\._]+@[[:alnum:]]+\.[[:alnum:]]+
%%
{CPF}
{ printf("CPF\n"); }
{EMAIL} { printf("EMAIL\n"); }
.
{ printf("Caractere nao reconhecido\n"); }
%%
// funcao principal que chama o analisador
int main()
{
yylex();
}

B.1.3

exp_flex/Makefile

Cdigo fonte /exp_flex/Makefile[code/cap2/exp_flex/Makefile]


exp_flex/Makefile
exp: lexer.c lexer.h exp_flex.c
clang -o exp lexer.c exp_flex.c
lexer.c: exp.ll
flex -DYY_DECL="Token * yylex()" exp.ll
lexer.h: exp.ll
flex -DYY_DECL="Token * yylex()" exp.ll
clean:
rm -f exp
rm -f lexer.c lexer.h
rm -f *.o

B.1.4

exp_flex/exp.ll

Cdigo fonte /exp_flex/exp.ll[code/cap2/exp_flex/exp.ll]


exp_flex/exp.ll
%option noyywrap
%option nodefault
%option outfile="lexer.c" header-file="lexer.h"
%top {
#include "exp_tokens.h"
57 / 68

Introduo aos Compiladores


}
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 token(TOK_ERRO, 0);

return
return
return
return
return
return
return

atoi(yytext)); }
SOMA);
}
SUB);
}
MULT);
}
DIV);
}
PARESQ); }
PARDIR); }
}

// erro para
// token desconhecido

%%
// variavel global para um token
Token tok;
Token * token(int tipo, int valor)
{
tok.tipo = tipo;
tok.valor = valor;
return &tok;
}

B.1.5

exp_flex/exp_tokens.h

Cdigo fonte /exp_flex/exp_tokens.h[code/cap2/exp_flex/exp_tokens.h]


exp_flex/exp_tokens.h
//
// exp_tokens.h
//
// Tipos e constantes para tokens da linguagem Exp
//
// Andrei de A. Formiga - 2014-09-01
//
#ifndef __EXP_TOKENS_H
#define __EXP_TOKENS_H
// constantes booleanas
#define TRUE
1
#define FALSE
0

58 / 68

Introduo aos Compiladores


// constantes para tipo de token
#define TOK_NUM
0
#define TOK_OP
1
#define TOK_PONT
2
#define TOK_ERRO
3
// constantes para valores de operadores
#define SOMA
0
#define SUB
1
#define MULT
2
#define DIV
3
// constantes para valores de pontuacao (parenteses)
#define PARESQ
0
#define PARDIR
1
// estrutura que representa um token
typedef struct
{
int tipo;
int valor;
} Token;
// funcao para criar um token
extern Token *token();
// funcao principal do analisador lexico
extern Token *yylex();

#endif

B.1.6

// __EXP_TOKENS_H

exp_flex/exp_flex.c

Cdigo fonte /exp_flex/exp_flex.c[code/cap2/exp_flex/exp_flex.c]


exp_flex/exp_flex.c
//
// exp_flex.c
//
// Analisador lexico para expressoes usando flex
//
// Andrei de A. Formiga, 2014-09-01
//
#include <stdio.h>
#include <stdlib.h>
#include "lexer.h"
#include "exp_tokens.h"

59 / 68

Introduo aos Compiladores


// variavel global para o buffer
YY_BUFFER_STATE buffer;
void inicializa_analise(char *str)
{
buffer = yy_scan_string(str);
}
// obtem o proximo token ou NULL para fim de arquivo
Token *proximo_token()
{
return yylex();
}
void finaliza_analise()
{
yy_delete_buffer(buffer);
}
char *operador_str(int op)
{
char *res;
switch (op) {
case SOMA:
res = "SOMA";
break;
case SUB:
res = "SUB";
break;
case MULT:
res = "MULT";
break;
case DIV:
res = "DIV";
break;
default:
res = "NENHUM";
}
return res;
}
void imprime_token(Token *tok)
{
printf("Tipo: ");
switch (tok->tipo) {
case TOK_NUM:
printf("Numero \t -- Valor: %d\n", tok->valor);
60 / 68

Introduo aos Compiladores


break;
case TOK_OP:
printf("Operador \t -- Valor: %s\n", operador_str(tok->valor));
break;
case TOK_PONT:
printf("Pontuacao -- Valor: %s\n", (tok->valor == PARESQ ? " PARESQ" : "PARDIR"));
break;
case TOK_ERRO:
printf("Erro: token nao reconhecido!\n");
break;
default:
printf("TIPO DE TOKEN DESCONHECIOO\n");
}
}
// funcao principal
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) {
imprime_token(tok);
tok = proximo_token();
}
printf("\n");
finaliza_analise();
return 0;
}

B.1.7

minic/minic_tokens.h

Cdigo fonte /minic/minic_tokens.h[code/cap2/minic/minic_tokens.h]


61 / 68

Introduo aos Compiladores


minic/minic_tokens.h
//
// minic_tokens.h
//
// Tipos e constantes para tokens da linguagem Mini C
//
// Andrei de A. Formiga, 2014-09-15
//
#ifndef __MINIC_TOKENS_H
#define __MINIC_TOKENS_H
// 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

// valores para palavra-chave


#define PC_IF
#define PC_ELSE
#define PC_WHILE
#define PC_RETURN
#define PC_PRINTF

0
1
2
3
4

// valores para pontuacao


#define PARESQ
#define PARDIR
#define CHVESQ
#define CHVDIR
#define VIRG
#define PNTVIRG

1
2
3
4
5
6

// valores para operadores


#define SOMA
#define SUB
#define MULT
#define DIV
#define MENOR
#define IGUAL
#define AND
#define NOT
#define ATRIB

1
2
3
4
5
6
7
8
9

// tipos
typedef struct
{

62 / 68

Introduo aos Compiladores


int tipo;
int valor;
} Token;
// declaracao do analisador lexico
#define YY_DECL Token * yylex()
extern Token * yylex();

#endif

B.1.8

// __MINIC_TOKENS_H

minic/lex.ll

Cdigo fonte /minic/lex.ll[code/cap2/minic/lex.ll]


minic/lex.ll
%option noyywrap
%option nodefault
%option outfile="lexer.c"
%top {
#include "minic_tokens.h"
#include "tabelas.h"
// prototipo da funcao token
Token *token(int, int);
}
NUM [0-9]+
ID [[:alpha:]]([[:alnum:]])*
STRING \"[^\"\n]*\"
%%
[[:space:]]
\/\/[^\n]*

} /* ignora espacos em branco */


{ } /* elimina comentarios de linha */

"#include <stdio.h>"

{ return token(TOK_PROLOGO, 0); }

{STRING} { return token(TOK_STRING, adiciona_string(yytext)); }


if
else
while
return
printf

{
{
{
{
{

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 token(TOK_NUM, atoi(yytext)); }


{ return token(TOK_ID, adiciona_simbolo(yytext)); }

63 / 68

Introduo aos Compiladores


\+
\*
\/
\<
==
&&
!
=

{
{
{
{
{
{
{
{
{

return
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,
token(TOK_OP,

SOMA);
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,

{ return token(TOK_ERRO, 0);

}
}
}
}
}
}
}
}
}

PARESQ); }
PARDIR); }
CHVESQ); }
CHVDIR); }
VIRG);
}
PNTVIRG); }
}

%%
Token tok;
Token *token(int tipo, int valor)
{
tok.tipo = tipo;
tok.valor = valor;
return &tok;
}

B.1.9

minic/lex_teste.c

Cdigo fonte /minic/lex_teste.c[code/cap2/minic/lex_teste.c]


minic/lex_teste.c
//
// lex_teste.c
//
// Teste do analisador lexico para linguagem Mini C
//
// Andrei de A. Formiga, 2014-09-21
//
#include <stdio.h>
#include <stdlib.h>
#include "minic_tokens.h"
#include "tabelas.h"
// declaracao do arquivo yyin
64 / 68

Introduo aos Compiladores


extern FILE *yyin;
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;
}
void finaliza_analise()
{
// destroi tabelas
destroi_tab_strings();
destroi_tab_simbolos();
// fecha arquivo de entrada
fclose(yyin);
}
void imprime_token(Token *tok)
{
char *tipo;
switch (tok->tipo) {
case TOK_PCHAVE:
tipo = "palavra chave";
break;
case TOK_ID:
tipo = "identificador";
break;
case TOK_NUM:
tipo = "numero";
break;
case TOK_PONT:
tipo = "pontuacao";
break;
case TOK_OP:
tipo = "operador";
break;
case TOK_STRING:
tipo = "string";
break;
65 / 68

Introduo aos Compiladores

case TOK_PROLOGO:
tipo = "prologo";
break;
case TOK_ERRO:
tipo = "erro";
break;
default:
tipo = "desconhecido";
}
printf("Tipo: %s - Valor: %d\n", tipo, tok->valor);
}
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();
return 0;
}

66 / 68

Introduo aos Compiladores

Captulo 4
ndice Remissivo
_
*, 18
+, 19
., 20, 24
:alpha:, 19
?, 19
[:digit:], 19
[:lower:], 19
[:space:], 19
[:upper:], 19
[], 19
rvores de expresso, 40
rvore sinttica, 39
, 20
|, 18
\, 20
\n, 20
\t, 20
{}, 20

definies, 22
E
email, 21
ER, 18
especificao simples, 22
expresso, 40
Expresses Regulares, 18
F
fim de arquivo, 25
flex, 20, 22
cdigo, 22
definies, 22
especificao simples, 22
regras, 22
terminar, 25
I
interpretadores, 2
intervalos, 19

A
Anlise
Lxica, 10
anlise sinttica, 38
Analisador Lxico, 12
autmatos finitos, 17

L
Lxica, 10
lex, 22
lexema, 12
linguagem, 1
linguagem de programao, 1
Linguagens Regulares, 17

B
bytecodes, 3

M
metacaracteres, 20

C
cdigo, 22
chamadas de sistema, 3
Classes de caracteres, 19
compilador, 2
CPF, 20, 23

N
nmero de repeties, 20
negao, 20
nodefault, 28
noyywrap, 23

D
datas, 21

P
67 / 68

Introduo aos Compiladores


placas, 21
programa, 1
R
regras, 22
T
terminar, 25
tokens, 5, 10, 11
Y
yyin, 30
yylex(), 24
yytext, 28
yywrap, 23, 27

68 / 68

Você também pode gostar