Buscar

impressao (1)

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 3, do total de 24 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 6, do total de 24 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 9, do total de 24 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Prévia do material em texto

COMPILADORES 
AULA 2 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Prof. Frank Coelho de Alcantara 
 
 
 
2 
CONVERSA INICIAL 
O processo de compilação começa no analisador léxico, um módulo 
dedicado à análise de palavras de uma determinada linguagem. No caso da 
compilação, estaremos sempre falando de linguagens formais. Veremos nesta 
aula as funcionalidades do analisador léxico, começando pela definição de 
linguagem formal; estudaremos depois o conceito de lexema e passaremos 
pelas técnicas que permitem sua identificação e classificação. 
Segundo Mogensen (2010), a palavra léxico tem o sentido original de 
“referente às palavras”; do ponto de vista da ciência da computação, é 
necessário entender palavras como sendo conjuntos de símbolos usados em um 
código, visando à execução de um determinado algoritmo. A análise léxica 
consiste na identificação de palavras e símbolos contidos no código-fonte. 
Existem duas técnicas importantes de reconhecimento de lexemas: o uso 
de máquinas de estado finito e o uso de expressões regulares. Nos dois casos, 
partiremos dos alicerces da linguística computacional e chegaremos a 
aplicações práticas, com base em experiência e eficiência. Aqui, quando 
estudamos o analisador léxico, precisaremos considerar um lexema como sendo 
qualquer conjunto de caracteres que tenha algum sentido em uma determinada 
linguagem de programação. Assim, se recorremos à linguagem C, a palavra 
chave for e o símbolo == serão lexemas. Caberá ao analisador léxico reconhecer 
e classificar esses lexemas, atendendo às regras criadas para a linguagem de 
programação, em tokens. 
Uma vez que as palavras e símbolos tenham sido corretamente 
identificados, cada token será composto de, no mínimo, duas informações: uma 
classe e um valor. As classes dividem os lexemas segundo sua funcionalidade. 
No caso da linguagem C, para não fugir do exemplo, os lexemas for e == serão 
de classes diferentes, já que um é um operador lógico e o outro é uma palavra 
reservada. Os valores atribuídos a cada token são opcionais, e serão utilizados 
apenas para completar a informação necessária. 
TEMA 1 – LINGUAGENS E STRINGS 
Uma linguagem de programação é uma linguagem formal. Será formal a 
linguagem que possa ser rígida e precisamente especificada (Bergmann, 2010). 
Definimos linguagens formais por meio do Teorema de Kleene. Esse teorema 
 
 
3 
afirma que, para que uma linguagem seja considerada formal, ela deve ser 
definida por expressões regulares e máquinas de estado finitas. Um dos mais 
importantes corolários deste teorema indica que qualquer máquina de estados 
finita pode ser transformada em uma expressão regular e vice-versa. A 
especificação de uma linguagem formal requer o uso de alguns conhecimentos 
importados da álgebra. O conceito de conjunto é o primeiro deles. Definimos um 
conjunto como sendo uma coleção de objetos únicos chamados de elementos. 
Os conjuntos serão representados por uma letra maiúscula em negrito e itálico 
e seus elementos por letras minúsculas, números ou símbolos, entre chaves, 
como podemos ver na Equação a seguir: 
 𝑨 = {a, b, c, e, g, h} 
Um conjunto pode conter um número infinito de elementos, elemento 
nenhum ou qualquer quantidade entre esses dois extremos. O conjunto que não 
têm elementos é um conjunto vazio, representado pelo símbolo ∅. O conjunto 
𝑩 = {∅} não é um conjunto vazio, é um conjunto com um único elemento, que 
por sua vez é um conjunto vazio. Observe que, ao listarmos os elementos de um 
conjunto, cada elemento deverá aparecer uma vez; é também permitido listar os 
elementos do conjunto na ordem que desejarmos sem que isso altere a 
informação que ele contém. Existem, do ponto de vista da linguística 
computacional, dois conjuntos de símbolos muito importantes: o alfabeto, 
doravante representado por 𝜮, e a string. 
O alfabeto é o conjunto finito de todos os símbolos possíveis em um 
determinado domínio do conhecimento. O código ASCII, com seus 255 símbolos, 
e o Unicode, contendo mais de 100.000, são exemplos de alfabetos (Aho et al., 
2007). Usando um pouco de formalidade algébrica, podemos exemplificar 
conjuntos da seguinte forma: 
 𝜮𝟏 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, alfabeto utilizado para escrever todos os 
números positivos inteiros; 
 𝜮𝟐 = {a, b, c, d, e … x, y. z}, alfabeto utilizado para escrever palavras em 
idiomas latinos, se desconsiderarmos os acentos e sinais especiais. 
Note que o conjunto dos números inteiros (Ι = {0, 1, 2, 3, … }) não pode ser 
considerado um alfabeto, por ser infinito. Alfabetos são, obrigatoriamente, 
conjuntos finitos. O outro conjunto de símbolos igualmente importante para o 
 
 
4 
nosso estudo é o string. Diferindo dos conjuntos algébricos, em uma string a 
ordem em que os elementos são listados é importante. Observe que a string 𝑠 é 
um conjunto de símbolos de um determinado alfabeto 𝜮. Ressalte-se que uma 
string vazia ainda é uma string, sendo representada pelo símbolo 𝜺 (Bergmann, 
2010). 
Formalmente, podemos definir uma string como sendo uma sequência de 
símbolos 𝑠 de comprimento 𝑛 de um alfabeto 𝜮, representada pela função 
𝑠: [𝑛] → 𝜮 com domínio [𝑛] e contradomínio 𝜮. Dessa forma, podemos considerar 
a string 𝑠 = 𝑐𝑐𝑑𝑎 do alfabeto 𝜮 = {𝑎, 𝑏, 𝑐, 𝑑}, com 𝑛 = 4 e função 𝑠: [4] → 𝜮 
definida por: 𝑠(1) = 𝑐; 𝑠(2) = 𝑐; 𝑠(3) = 𝑑 𝑒 𝑠(4) = 𝑎. Seguindo essa definição, uma 
string vazia 𝑣 será definida pela função 𝑣: [0] → 𝜮. Podemos utilizar essa 
definição para exemplificar uma string 𝑠, do alfabeto 𝜮𝒕 = {a, b, c}. Nesse caso, 
podemos, por exemplo, definir as strings: 𝑎𝑏𝑐, 𝑎𝑎𝑏𝑐, 𝑐𝑎𝑏𝑏𝑎, … todas em relação 
ao alfabeto 𝜮𝒕 de comprimentos 𝑛 = 3, 𝑛 = 4, e 𝑛 = 5, respectivamente. 
É indispensável assim destacar que existe uma grande diferença entre 
uma string vazia, 𝜺 e uma string nula. Tomando, novamente a linguagem C como 
exemplo, poderíamos ter uma string contendo apenas o símbolo ‘\0’, uma string 
com um único caractere. Nesse caso, teríamos uma string vazia; como o ‘\0’ não 
existe no alfabeto da linguagem C, essa string não contém caractere algum. Já 
a string nula consiste de um ponteiro do tipo NULL. 
Definir uma linguagem a partir de suas strings não é uma tarefa trivial. 
Para tanto, podemos recorrer ao Teorema de Kleene, citado anteriormente, e 
nos ater a máquinas de estados finitos e a expressões regulares. 
TEMA 2 – MÁQUINA DE ESTADOS FINITOS 
O estudo das máquinas de estados é chamado de Teoria dos 
Autômatos. Consiste no estudo das regras de formação e comportamento de 
tais máquinas do ponto de vista da matemática e da lógica. Definimos uma 
máquina de estados finitos como sendo “um conjunto finito de estados 
submetidos a um número finito de transições entre estes estados” (Mogensen, 
2010). Cada estado é um momento específico no tempo; as condições internas 
da máquina estão estáveis, enquanto a transição é uma função que, recebendo 
dois argumentos, um estado e um símbolo de entrada, resulta em outro estado. 
Nesta disciplina, serão utilizadas, fundamentalmente, as Máquinas de Mealy, 
 
 
5 
cuja função de transição é relacionada com o estado da máquina e com o 
símbolo da entrada. 
No estado inicial, a máquina recebe um símbolo de uma string do alfabeto 
escolhido. Todas as vezes que um símbolo é lido, a máquina prossegue para um 
novo estado, indicado pela função de transição. Esse novo estado será 
determinado pela função de transição de acordo com o estado anterior e com o 
símbolo lido, atendendo às características da Máquina de Mealy. Ao terminar a 
leitura de todos os símbolos de entrada, a máquina ficará em um estado que 
indica o reconhecimento de uma sequência de símbolos (accepting state) ou não 
(non-accepting state). O conjunto de todas as strings aceitas pela máquina forma 
uma linguagem. 
Existem duas formas interessantes de se representar uma máquina de 
estados:a forma gráfica, ou diagrama de estados, e a forma tabular. Na forma 
gráfica, cada estado é representado por um círculo e as funções de transição 
são representadas por setas etiquetadas por um símbolo que representa a 
entrada da função. O estado inicial é representado por uma seta solta, sem 
nenhum estado em sua origem. Observe que, ao longo do funcionamento da 
máquina, os estados que estiverem em modo accepting state são representados 
por dois círculos concêntricos. Podemos sintetizar uma máquina de estados com 
cinco conceitos: 
1. Estados: pontos estáveis no processo de computação da máquina, 
representados por círculos; 
2. Estado inicial: estado onde a máquina inicia o processamento, indicado 
por um círculo com uma seta apontada para ele; 
3. Estados intermediários: todos os estados intermediários têm, no 
mínimo, duas setas: uma apontada para o círculo que o representa e outra 
deixando esse círculo; 
4. Estado final: se a string for corretamente processada, a máquina deverá 
assumir este estado, em geral indicando que está pronta para processar 
outra string (accepting state); o estado final é representado por dois 
círculos concêntricos. 
5. Transições: as transições são representadas por uma seta com origem 
em um estado e destino no estado seguinte; se a máquina permanecer no 
mesmo estado, a transição será representada por uma seta que deixa o 
estado e aponta para ele mesmo. 
 
 
6 
Uma máquina de estados comum em comunicação de dados é capaz de 
detectar a paridade de um conjunto de bits. Uma máquina de detecção de 
paridade começa pela definição do alfabeto 𝜮𝑨 = {0,1} e termina na 
especificação do diagrama de estados apresentado na figura. 
Figura 1 – Máquina de estados de um detector de paridade (números pares de 
uns) 
 
Todos os estados e transições desta máquina já estão definidos, e ela só 
pode receber informações pelo estado 𝑆0, e só estará em accepting state se o 
número de uns na string de entrada for par, incluindo a string nula (Holub, 1990). 
Um zero aplicado ao estado 𝑆0 manterá a máquina em accepting state, indicando 
zero uns. Ainda que zero não seja par, este conceito é usado em detectores de 
paridade. O primeiro número um da sequência de entrada leva a máquina para 
o estado 𝑆1. A partir desse momento, qualquer quantidade de zeros manterá a 
máquina em 𝑆1 e fora do accepting state. Ainda em 𝑆1, qualquer um levará a 
máquina para o estado 𝑆1 e para accepting state. Essa máquina reconhece 
qualquer número par de uns, inclusive um nenhum. 
A máquina apresentada na Figura 1 está perfeitamente determinada. 
Como o alfabeto possui apenas dois símbolos, e como cada um deles pode 
atingir um estado, cada estado tem exatamente duas setas de saída. As 
máquinas em que todas as transições são definidas e conhecidas são chamadas 
de determinísticas. Se existir uma única transição entre quaisquer dois estados 
que não possa ser diretamente relacionada a um dos símbolos do alfabeto, ela 
será denominada de 𝜖, e teremos como resultado uma máquina de estados 
finitos não determinística (Aho, 2007). 
 
 
7 
Existem duas diferenças formais muito importantes entre uma máquina de 
estados finitos não determinística e uma determinística. A inexistência de 
transições 𝝐 é uma delas, e a outra é a inexistência de transições múltiplas de 
saída para um mesmo símbolo. Ou seja, em uma máquina de estados finitos 
determinística cada estado terá uma, e somente uma transição para cada 
símbolo do alfabeto escolhido. 
Outra representação interessante das máquinas de estados finitos é a 
tabela. Nesse caso, cada linha é nomeada como um dos estados possíveis, cada 
coluna representa um dos símbolos do alfabeto e a interseção entre essas linhas 
e colunas representa o estado resultante da transição, como pode ser visto na 
tabela que segue. 
Tabela 1 – Tabela representação de uma máquina de paridade 
 
 
 
 
Como exercício, vamos desenvolver uma máquina de estados capaz de 
identificar uma string contendo um número ímpar de zeros, dado que 𝜮 = {0, 1}. 
Figura 2 – Solução 
 
 
TEMA 3 – EXPRESSÕES REGULARES 
Além das máquinas de estados finitos, podemos utilizar as expressões 
regulares, indicadas pelas letras 𝑟, 𝑠 𝑜𝑢 𝑡, para representar as strings de uma 
determinada linguagem 𝐿 derivada de um alfabeto finito 𝜮. O importante, nesse 
momento, é perceber que o alfabeto é o coração das expressões regulares. 
Dado um alfabeto 𝜮 qualquer, iremos descrever as strings, 𝑟, 𝑠 𝑜𝑢 𝑡, de uma 
linguagem 𝐿, por meio de um conjunto de expressões lógicas e matemáticas que, 
 0 1 
𝑺𝟎 𝑆0 𝑆1 
𝑺𝟏 𝑆1 𝑆0 
 0 1 
𝑆0 𝑆1 𝑆0 
∗ 𝑆1 𝑆0 𝑆1 
 
 
8 
deste momento em diante, chamaremos de expressões regulares. Contudo, é 
necessário destacar que, para que nossa álgebra atenda às necessidades 
propostas, é necessário considerar que os símbolos 𝜺, ∅, |, (, ) 𝒆 ∗ não fazem 
parte do alfabeto 𝜮 escolhido. 
Vejamos um exemplo simples de expressões regulares, considerando o 
alfabeto 𝜮 composto de todas as letras latinas em minúsculo. A expressão 
regular pode ser representada por 𝑎. Trata-se da representação de todos as 
strings que atendam ao conjunto {𝑎}. Considerando que essa expressão regular 
está relacionada a um conjunto de strings da linguagem 𝐿 de alfabeto 𝜮 =
{𝑎, 𝑏, 𝑐, … , 𝑥, 𝑦, 𝑧}, a expressão regular representa o conjunto de todas as strings 
que possuem apenas um caractere, o caractere 𝑎. Com o auxílio do Teorema de 
Kleene, é possível representar essa expressão regular na forma de uma máquina 
de estados finitos. 
Para entendermos o potencial das expressões regulares, precisamos 
definir três operações importantes: a união, a concatenação e uma operação 
especial chamada de Kleene star ou closure. Elas definem todas as operações 
necessárias para a criação de expressões regulares. 
3.1 União 
Em linguística computacional, a união é definida exatamente da mesma 
forma que na teoria dos conjuntos. Tomando dos conjuntos 𝑳𝟏 =
{a, b, c, eg, hf} e 𝑳𝟐 = {ea, af}, temos que 𝑳𝟏 ∪ 𝑳𝟐 = {a, b, c, eg, hf, ea, af}. Observe 
que, em alguns textos técnicos, principalmente em inglês, o símbolo algébrico 
da união (∪) é substituído pelo símbolo da soma (+) ou por uma barra vertical 
(|). Precisamos ressaltar ainda que a união de um conjunto com um conjunto 
vazio é o próprio conjunto. Por fim, podemos representar um conjunto vazio 𝑨 de 
duas formas: 𝑨 = ∅ ou 𝑨 = {}. A operação de união é tão poderosa que, em 
alguns textos de linguística computacional, dizemos que a linguagem 𝐿1 é 
autocontida na operação de união. 
3.2 Concatenação 
Do ponto de vista intuitivo, o resultado da concatenação de duas strings é 
uma string formada pelas duas strings originais, em sequência. Assim, a 
concatenação de “um belo dia” e “ começa cedo” será “um belo dia começa 
 
 
9 
cedo”. Observe que, para que a operação de concatenação seja perfeita, é 
preciso que as strings tenham os valores desejados. Se tirarmos o espaço que 
existe no começo de “começa cedo”, o resultado seria: “um belo diacomeça 
cedo”. Considerando um pouco de formalidade matemática, devemos expandir 
o conceito de que a concatenação é uma operação a ser realizada entre duas 
linguagens 𝐿1 𝑒 𝐿2. Nesse caso, definimos a concatenação 𝐿 entre 𝐿1 e 𝐿2 como 
sendo: 
 𝐿 = 𝐿1 ⋅ 𝐿2 = {𝑥𝑦 |𝑥 ∈ 𝐿1, 𝑦 ∈ 𝐿2} 
No caso mais comum de concatenação, visto diariamente na construção 
de programas, consideramos as linguagens 𝐿1 e 𝐿2 como sendo conjuntos de 
uma única string; sendo assim, podemos extrapolar a definição de concatenação 
para esse caso específico. Dadas duas strings representadas pelas funções 
𝑠1: [𝑚] → 𝜮 e 𝑠2: [𝑛] → 𝚺, é possível definir a concatenação como sendo a função 
𝑠1 ⋅ 𝑠2: [𝑚 + 𝑛] → 𝜮 de comprimento 𝑙 = 𝑚 + 𝑛, para todos os 𝑖, se e somente se: 
 𝑠1(𝑖) ⋅ 𝑠2(𝑖) = {
𝑥(𝑖), 𝑠𝑒 𝑖 ≤ 𝑚
𝑦(𝑖−𝑚), 𝑠𝑒 𝑖 > 𝑚 
 
Destaforma, se tivermos 𝑠1 = “cbba” e 𝑠2 = “ac”, teremos 𝑠1 ⋅ 𝑠2 =
“𝑐𝑏𝑏𝑎𝑎𝑐”; ou, em formato de tabelas e correndo todos os valores de 𝑖 em cada 
string (Rangel, 2013), como pode ser visto na tabela abaixo. 
Tabela 2 – Concatenação de strings pelo método formal de concatenação 
𝒊 𝒔𝟏(𝒊) 𝒊 𝒃𝟐(𝒊) 𝒊 (𝒔𝟏 ⋅ 𝒔𝟐)(𝒊) 
𝟏 c 𝟏 a 𝟏 c 
𝟐 b 𝟐 c 𝟐 b 
𝟑 b 𝟑 b 
𝟒 a 𝟒 a 
 𝟓 a 
 𝟔 c 
Podemos ainda usar uma notação algébrica um pouco mais refinada para 
indicar a concatenação de duas strings. Se assim for, podemos dizer que dadas 
as strings 𝑠1 e 𝑠2, a concatenação será um conjunto representado por 𝜮
∗ que 
indica um conjunto finito de strings relativas ao alfabeto 𝜮, de tal forma que: 𝑠1 ⋅
 
 
10 
𝑠2 = 𝜮
∗. Podemos exemplificar considerando que 𝑠1 = 𝑎𝑏, 𝑠2 = 𝑟𝑎, 𝑠3 = 𝑐𝑎𝑑. 
Teremos: 𝑠2 ⋅ 𝑠1 = 𝑟𝑎𝑎𝑏, 𝑠1 ⋅ 𝑠1 = 𝑎𝑏𝑎𝑏 e 𝑠3 ⋅ 𝑠2 = 𝑐𝑎𝑑𝑟𝑎. Importante lembrar que 
a concatenação pode ser realizada entre várias strings (Pitts, 2013), mas sempre 
dois a dois, dessa forma: 
 𝑠1 ⋅ 𝑠2 ⋅ 𝑠3 ⋅ 𝑠1 ⋅ 𝑠2 = (((𝑠1 ⋅ 𝑠2) ⋅ 𝑠3) ⋅ 𝑠1) ⋅ 𝑠2 = 𝑎𝑏𝑟𝑎𝑐𝑎𝑑𝑎𝑏𝑟𝑎 
Além disso, é preciso destacar que o comprimento (length) da string 
concatenada é a soma dos comprimentos das strings, ou seja: 
 𝑙𝑒𝑛𝑔𝑡ℎ(𝑠1 ⋅ 𝑠2) = 𝑙𝑒𝑛𝑔𝑡ℎ(𝑠1) + 𝑙𝑒𝑛𝑔𝑡ℎ(𝑠2) 
O comprimento da string concatenada é importante em linguagens de 
programação, porque define o espaço de memória necessário ao manuseio da 
string. Vejamos alguns exemplos do uso da concatenação (Pitts, 2013): 
a. Se 𝜮 = {𝑎} então Σ∗ = {𝜺, 𝑎, 𝑎𝑎, 𝑎𝑎𝑎, … }; 
b. Se 𝜮 = {𝑎, 𝑏} então Σ∗ = {𝜺, 𝑎, 𝑎𝑎, 𝑏, 𝑎𝑏, 𝑏𝑎, 𝑏𝑏, 𝑎𝑎𝑎, 𝑎𝑎𝑏, … }; 
c. Se 𝜮 = ∅ então Σ∗ = {𝜺}. 
3.3 Kleene star 
A operação Kleene star, frequentemente chamada de closure, é uma 
operação unária representada por um ∗ e similar ao processo de exponenciação, 
de tal forma que Kleene de 𝐿, representada por 𝐿∗, é a linguagem 𝐿 repetida zero 
ou mais vezes. A operação Kleene star é uma sequência de concatenações de 
uma linguagem com ela mesmo (Hel-or, 2010). Por exemplo, se 𝐿 contém 
apenas um símbolo, 𝐿∗ será este símbolo repetido zero ou mais vezes. Sendo 
assim, se tomarmos uma linguagem 𝐿 podemos definir: 
a. 𝐿0 = 𝜺; 
b. 𝐿1 = 𝐿; 
c. 𝐿2 = 𝐿 ⋅ 𝐿; 
d. 𝐿𝑛 = 𝐿 ⋅ 𝐿𝑛−1; 
e. 𝐿∗ = 𝐿0 + 𝐿1 + 𝐿2 + 𝐿3 + ⋯. 
Observe que, aplicada sobre uma linguagem que seja um conjunto vazio, 
ou seja, que não possui nenhuma string, o resultado da closure será uma string 
 
 
11 
vazia: 𝐿 = ∅ ∴ ∅∗ = 𝜺. Podemos definir a operação Kleene star de forma 
algébrica por: 
 𝐿∗ = ⋃ 𝐿𝑖
∞
𝑖=0
 
Vejamos um exemplo: se 𝐿1 = {𝑎} e 𝐿2 = {𝑏}, então: 
𝐿1
∗ ⋅ 𝐿2 = {𝑏, 𝑎𝑏, 𝑎𝑎𝑏, 𝑎𝑎𝑎𝑏 … }. É possível aqui perceber a propagação Kleene do 
símbolo 𝑎 de zero ao infinito, sendo concatenado, repetidamente, ao símbolo 𝑏, 
que não está sujeito à operação Kleene star. Podemos exemplificar a Kleene 
star considerando a linguagem L = {0,1}. Dessa forma: 
 𝐿0 = {𝜺} 
 𝐿1 = {0,1} 
 𝐿2 = 𝐿 ⋅ 𝐿1 = {00,01,10,11} 
 𝐿3 = {000, 001, 010, 011, 100, 101, 110, 111} 
 𝐿∗ = {𝜺, 000, 001, 010, 011, 100, 101, 110, 111, 1000 … } 
Estudamos as três operações que usaremos para definir as expressões 
regulares: uma unária, a Kleene star, e duas binárias, a concatenação e a união. 
Com essas operações, podemos definir qualquer expressão regular para 
identificar qualquer string de qualquer alfabeto dado. A procedência será definida 
pelo uso de parênteses. Se não forem utilizados parênteses para definir a 
precedência entre as operações, Kleene star irá tomar a precedência sobre a 
concatenação e esta terá precedência sobre a união. Vamos ver o seguinte 
exemplo: 
 𝑎|𝑎𝑏∗ = (𝑎|(𝑎 ⋅ 𝑏∗)) 
Como dito anteriormente, as suas expressões regulares serão escritas 
com uma combinação das três operações básicas. Talvez isso não fique claro, 
até que você dê uma olhada na tabela abaixo. Ela apresenta algumas 
expressões regulares básicas para fixação dos conceitos, e introduz algumas 
alterações na simbologia utilizada, o que facilitará o uso das expressões 
regulares em ambiente de programação. 
 
 
12 
Tabela 3 – Concatenação de strings pelo método formal de concatenação 
Regex Álgebra Descrição 
𝒙 {“𝑥”} Um conjunto contendo apenas a string 𝑥 
𝜺 {““} Um conjunto contendo nenhuma string, a string 
vazia 
𝒔|𝒓 𝐿(𝑠) ∪ 𝐿(𝑟) A união de strings de ambas as linguagens 
𝒔 + 𝒓 𝐿𝑠 ∪ 𝐿(𝑟) A união de strings de ambas as linguagens 
𝒔 ⋅ 𝒓 = 𝒔𝒓 {𝑥𝑦|𝑥 ∈ 𝑠, 𝑦 ∈ 𝑟} Strings construídas com a concatenação das 
strings em 𝑠1 com as strings em 𝑠2 
𝒔∗ 𝜺 ∪ {𝑥𝑦|𝑥 ∈ 𝐿𝑠, 𝑦 ∈ 𝐿𝑠∗} Cada string é a concatenação de um número 
qualquer de strings da linguagem de 𝑠 
 
No último caso, a operação 𝑠∗ é definida de forma recursiva; consiste da 
string vazia 𝜺 mais aquilo que poderá ser obtido por intermédio da concatenação 
das linguagens 𝐿𝑠 e 𝐿𝑠∗. Equivale a dizer que a linguagem 𝐿𝑠∗ consiste de strings 
que podem ser obtidas pela concatenação de zero ou mais strings de 𝐿𝑠. 
Suponha que 𝐿𝑠 = { 𝑎, 𝑏}. Então, 𝐿𝑠∗ = {𝜺, 𝑎, 𝑏, 𝑎𝑎, 𝑏𝑏, 𝑎𝑏, 𝑏𝑎, … }, ou seja, qualquer 
string composta unicamente das combinações entre 𝑎 e 𝑏, incluindo a string 
vazia. 
Analisando a Tabela 3, é possível concluir que existe um símbolo qualquer 
𝑥 | 𝑥 ∈ 𝜮; assim, 𝑥 é uma expressão regular. Da mesma forma, a string vazia 𝜺 
também é uma expressão regular. É possível inferir algumas regras básicas do 
uso de expressões regulares para o reconhecimento de strings de uma 
linguagem 𝐿 sobre um alfabeto 𝜮. Podemos dizer que, dada uma expressão 
regular 𝑟: 
Tabela 4 – Condições de reconhecimento entre strings e expressões regulares 
Condições de concordância (reconhecimento) 
𝒓 reconhece 𝒂 ∈ 𝜮 se e somente se 𝒓 = 𝒂 
𝒓 reconhece 𝜺 ∈ 𝜮 se e somente se 𝒓 = 𝜺 
Nenhuma string será reconhecida por ∅ 
𝒓 reconhece 𝒂|𝒃 se e somente se 𝒓 = 𝒂 ou 𝒓 = 𝒃 
 
Neste ponto, estamos prontos para definir expressões regulares 
algébrica, formalmente, por seus quatro axiomas: 
1. ∅ é uma expressão regular que indica um conjunto vazio. 
 
 
13 
2. 𝜺 é uma expressão regular que indica um conjunto contendo uma string 
vazia; 
3. Para cada símbolo 𝑎 ∈ 𝜮 existe uma expressão regular 𝑎 que indica o 
conjunto {𝑎}. 
4. Se 𝑟 e 𝑠 são expressões regulares referentes as linguagens 𝑅 e 𝑆, então 
(𝑠𝑟), (𝑠|𝑟) 𝑒 𝑠∗ são expressões regulares que indicam as linguagens 𝑆 ⋅ 𝑅,
𝑆𝑈𝑅 e 𝑆∗ respectivamente. 
Algumas propriedades, apresentadas na Tabela 5, são derivadas desses 
axiomas e do que vimos sobre as operações união, concatenação e Kleene star. 
Tabela 5 - Propriedades das operações União, Concatenação e Kleene Star 
Propriedades 
1 ∅𝑠 = 𝑠∅ = 𝑠 
2 𝜀𝑠 = 𝑠𝜀 = 𝑠 
3 𝑠|∅ = 𝑠 + ∅ = ∅ + 𝑠 = 𝑠 
4 𝑠|𝑠 = 𝑠 + 𝑠 = 𝑠 
5 𝑟 + 𝑠 = 𝑠 + 𝑟 
6 𝑟(𝑠 + 𝑡) = 𝑟𝑠 + 𝑟𝑡 
7 (𝑟 + 𝑠)𝑡 = 𝑟𝑡 + 𝑠𝑡 
8 𝑟(𝑠𝑡) = (𝑟𝑠)𝑡 
9 ∅∗ = 𝜀 
10 𝜀∗ = 𝜀 
11 (𝜀 + 𝑟)+ = 𝑟∗ 
12 (𝜀 + 𝑟)∗ = 𝑟∗ 
13 𝑟∗(𝜀 + 𝑟) = (𝜀 + 𝑟)𝑟∗ = 𝑟∗ 
14 𝑟∗𝑠 + 𝑠 = 𝑟∗𝑠 
15 𝑟(𝑠𝑟)∗ = (𝑟𝑠)∗𝑟 
16 (𝑟 + 𝑠)∗ = (𝑟∗𝑠)∗𝑟∗ = (𝑠∗𝑟)∗𝑠∗ 
 
Podemos utilizar o conhecimento já adquirido para escrever uma 
expressão regular capaz de reconhecer qualquer número positivo do conjunto 
dos números inteiros, desde que nos lembremos que os números desse conjunto 
são representados por um ou mais dos seguintes algarismos: 
0, 1, 2, 3, 4, 5, 6, 7, 8, 9. Sendo assim, podemos escrever a seguinte expressão 
regular: 
 
 
14 
Eq.1 (0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)∗ 
A Eq. 1 mostra a concatenação entre um conjunto e seu Kleene, já que 𝑠∗ 
indica zero ocorrências ou mais de cada um dos algarismo. Se consideramos 
que o conjunto vazio está implícito na operação Kleene, essa expressão regular 
representa, entre outros, os seguintes números:0 ,1, 10, 1000, 1234000, etc. 
podendo ser utilizada para representar qualquer um deles (Mogensen, 2010). 
Podemos também excluir outros símbolos do alfabeto para escrever uma 
notação mais amigável. Podemos excluir os símbolos . ? * + ^ $ | [ ] { } ( ) da 
nossa linguagem de exemplo e usá-los para definir um conjunto de símbolos de 
controle, ou metacaracteres. Se fizermos isso, a Eq. 1 poderia ser representada 
por: [0 − 9][0 − 9]∗. Usando essa notação nesse intervalo, poderíamos 
representar todas as combinações de letras do alfabeto português, menos os 
acentos e caracteres especiais pela concatenação de dois intervalos: [𝑎 − 𝑧𝐴_𝑍]. 
Para simplificar ainda mais a notação, podemos também excluir os símbolos +, ? 
e determinar que 𝑠+ representa uma ou mais ocorrência, e também que 𝑠 = 𝑠|𝜺 
representa a união de uma string com a string vazia. Se fizermos isso, a 
representação de um número inteiro positivo qualquer pode ser simplificada para 
[0 − 9]+. Agora é possível expandir esses conceitos para a representação de 
algumas das classes de lexemas mais utilizadas em linguagens formais, 
notadamente no universo das linguagens de programação. Vamos assim tentar 
explicar as expressões regulares que podem ser utilizadas para seu 
reconhecimento (Mogensen, 2010). Por fim, de forma muito simplificada: 
a. Palavras-chaves, ou Keywords. Devem ser reconhecidas por 
expressões regulares exatamente iguais às palavras chaves. Em C, por 
exemplo, a palavra-chave if será reconhecida pela expressão regular if. 
b. Nomes de variáveis. Neste caso, depende-se das regras de formação de 
cada linguagem. Em C, por exemplo, podemos escrever qualquer nome 
de variável, desde que ele comece com uma letra, contenha números e o 
caractere “_” e não utilize nenhum caractere excluído do alfabeto. Estes 
nomes de variáveis podem ser reconhecidos por: [𝑎 − 𝑧𝐴 − 𝑧_][𝑎 − 𝑧𝐴 −
𝑍0 − 9_]∗. 
c. Inteiros. Para a representação de inteiros positivos, podemos usar 
[0 − 9]+; porém, na linguagem C, por exemplo, precisamos representar 
 
 
15 
inteiros positivos e negativos. Assim, a expressão mais correta seria: 
[+|−]? [0 − 9]+, indicando qualquer número inteiro e podendo ter ou não 
os sinais + e – na frente1. 
d. Números de ponto flutuante. Números de ponto flutuante têm uma 
quantidade indefinida de algarismos antes e depois da vírgula (ponto nas 
linguagens de programação). Podem ter os símbolos de + e – e ainda 
podem estar em notação especial, representados por uma mantissa e um 
expoente. Talvez, uma forma de representar esses números seja a 
expressão regular: [+−]? (([0 − 9]+(. [0 − 9]∗)? [0 − 9]+)([eE][+−]? [0 −
9]+)? ) (Mogensen, 2010). 
e. Strings constantes. Neste caso, também precisamos entender as regras 
de formação da linguagem de programação. Na linguagem C, podemos 
representar as strings por: “([a-zA-z0-9]\[a-zA-Z])*” (Mogensen, 2010). 
Sem dúvida, o uso das expressões regulares fugiu do controle da álgebra 
e hoje pertence ao domínio das linguagens de programação; como tal, acaba 
tendo uma sintaxe própria, dependendo do padrão. Por exemplo, o padrão Posix, 
utilizado em máquinas Unix, Linux e Windows, é comum em várias linguagens 
de programação e merece que você pesquise sobre isso. 
Vamos finalizar esta seção com um exercício. Considerando o que vimos 
sobre expressões regulares, vamos escrever a expressão regular capaz de 
identificar um endereço IPv4. Solução: [0-9]{3}\.[0-9]{3}\.[0-9]{3}\.[0-9]{3}. 
TEMA 4 – LEXEMAS E TOKENS 
Devemos considerar o código, texto completo do programa, como sendo 
uma única string composta de strings que denominaremos lexemas. A função do 
analisador léxico será distinguir e identificar esses lexemas e classificá-los em 
tokens. O conceito de lexema em computação deriva da ciência da linguística, e 
representa apenas uma sequência de caracteres, ou símbolos, que juntos 
formam uma unidade sintática que tenha algum significado. Nesta disciplina, 
consideraremos token como sendo uma estrutura de dados que represente um 
lexema e explicite, além de sua classe e valor, informações que auxiliem no 
processo de análise léxica. Considerando a linguagem C, o analisador léxico 
 
1 De fato, para que isso fosse algebricamente correto, precisaríamos de um símbolo para indicar 
que, entre [, ], os símbolos + e – fazem parte do alfabeto utilizado. 
 
 
16 
deverá ser capaz de classificar lexemas diferentes em diversas classes de 
tokens, descritas por sua própria máquina de estados finito, ou expressão 
regular. Entre as classes diferentes da linguagem C, é possível destacar 
(Banahan; Brady; Doran, 1991): 
1. Palavras-chaves, keywords. São palavras que possuem um significado 
específico na linguagem; geralmente definem um comando ou processo 
específico. As keywords fazem parte da linguagem, e não podem ser 
utilizadas pelo programador como identificadores. 
2. Identificadores, identifiers. Palavras que podem ser utilizadas para 
identificação de variáveis e constantes. Em geral, as linguagens de 
programação apresentam apenas uma regra de formação dessas 
palavras. 
3. Operadores, operators. Símbolos utilizados para a realização de 
operações lógicas, aritméticas ou entre caracteres, os quais podem ser 
constituídos de um ou mais caracteres. Assim, o ! =, símbolo que 
representa diferente, na linguagem C, é formado por dois caracteres 
ASCII {!, =}; 
4. Valores numéricos. Valores que representam números inteiros, ou de 
ponto flutuante, positivos ou negativos, nas diversas notações possíveis 
– e, em muitas linguagens, em várias bases. Em C, é possível incluir 
números em decimal, hexadecimal e octal, mas não em binário. 
5. Caracteres especiais. Geralmente usados para limitar comandos, blocos 
ou caracteres. Entre outros, podemos citar: {, }, ,, . , (, ), 𝑒𝑡𝑐. 
6. Caracteres constantes. Caracteres especiais, não gráficos, utilizados 
para funções especiais dentro de strings e declarações. 
7. Comentários. Geralmente são identificados na fase de análise léxica, 
mas são ignorados pelo analisador léxico. Ou seja, precisam ser 
identificados, mas não são transformados em tokens e não são enviados 
para a próxima fase. 
8. Espaços, tabulações e novas linhas. Em geral, esses caracteres são 
ignorados e não são transformados em tokens, nem passados à próxima 
fase do processo de compilação. 
Esta lista não representa todas as possiblidades de classe utilizadas em 
linguagens de programação. Não é sequer a lista completa da linguagem C e 
está longe de ser a única solução possível. Para cada lexema identificado, será 
 
 
17 
preciso gerar uma estrutura de dados para armazenamento. Essencial, porém 
não exclusivamente, são necessárias duas informações: uma classe e um valor, 
de forma que teremos um par que, em geral, será composto por dois valores 
inteiros. Um token na linguagem de programação C/C++ poderia ser 
representado por uma struct: 
typedef struct 
{ 
 int nome; 
 int valor; 
} *Token; 
Não é raro que essa estrutura tenha informações referentes à linha e 
coluna onde aquele determinado token foi localizado no código. Essa informação 
é útil, entre outras coisas, para a identificação de erros. Se assim for, poderíamos 
ter duas estruturas para acrescentar a informação, como pode ser visto no 
fragmento de código a seguir: 
typedef struct { 
 int linha; 
 int coluna; 
} Position; 
 
typedef struct { 
 int nome; 
 int valor; 
 Position posicao; 
} Token; 
Estes tokens identificados e classificados serão armazenados na tabela 
de símbolos. 
4.1 Identificação de tokens 
Do ponto de vista didático, podemos remover um pouco da complexidade 
formal das máquinas de estados finitos determinística e criar um diagrama mais 
objetivo, que chamaremos de diagrama de transição. Usaremos os diagramas 
de transição para representaro processo de reconhecimento de um token. A 
vantagem do uso deste diagrama irá surgir no processo de transformação em 
 
 
18 
código. Geralmente, será possível converter o diagrama de transição em código 
usando apenas uma estrutura switch-case, comum em muitas linguagens de 
programação. 
Representaremos os diagramas de transição por um grafo formado por 
um conjunto de nós. Cada nó representa uma condição específica do processo 
de reconhecimento, e cada transição representa um caractere ou um conjunto 
de caracteres. Para simplificar, vamos representar apenas as transições que 
provocam mudanças de estado. O reconhecimento de lexemas provocará a 
criação do token correspondente, que deverá ser armazenado em uma estrutura 
de dados qualquer. Vamos usar como exemplo a figura abaixo, adaptada do 
trabalho e Aho et al. (2007). 
Figura 3 – Diagrama de transição para reconhecimento de <, >, =, <= e <= 
 
Nesse diagrama, cada estado de reconhecimento está representado por 
dois círculos concêntricos, um par ordenado com a classe do token e seu valor. 
Tanto a classe como o valor são constantes inteiras que, em um compilador real, 
poderiam estar definidas em diretivas de pré-processamento ou em uma tabela 
de valores constantes. O código pode ser muito simples se você fizer uma função 
para cada estado e passar como argumento desta função os símbolos. 
4.2 A complexidade da identificação 
 À primeira vista, o processo de reconhecimento de lexemas, para a 
criação de tokens é simples e depende apenas da capacidade de criação de 
expressões regulares e de máquinas de estado finitas. Entretanto, existem 
4 
0 
< 
1 2 
3 
= 
𝒐𝒖𝒕𝒓𝒐𝒔 
= 
5 6 
7 
> 
𝒐𝒖𝒕𝒓𝒐𝒔 
= 
< 𝒐𝒑, 𝑬𝑸 > 
< 𝒐𝒑, 𝑳𝑬 > 
< 𝒐𝒑, 𝑳𝑻 > 
< 𝒐𝒑, 𝑮𝑻 > 
< 𝒐𝒑, 𝑮𝑬 > 
 
 
19 
algumas considerações que precisam ser feitas. Tome como exemplo a seguinte 
declaração na linguagem C: 
typedef struct { 
 int linha; 
 int coluna; 
} Position 
Tanto o lexema typedef quanto o struct são palavras reservadas da 
linguagem C. Contudo, o typedef, para todos os efeitos, cria uma nova palavra-
chave, no caso Position. Neste caso, o analisador léxico terá que tratar o lexema 
Position como sendo da classe palavra-chave e não da classe identificador 
(Holub, 1990). Estruturas de dados complexas, como funções, objetos, arrays e 
structs, também requerem cuidado extra do analisador léxico. Todos esses tipos 
podem ser utilizados para a criação de novos escopos e de novos lexemas que 
poderão ou não ser identificados como palavras-chave ou identificadores. 
Na identificação de lexemas, ainda precisamos considerar o escopo. Isso 
quer dizer que existem blocos de códigos nos quais determinadas variáveis são 
visíveis. Na maior parte das linguagens, duas regras simples definem o 
mecanismo de escopo: (a) se um nome é declarado dentro de um bloco 𝑥 
qualquer, o nome só é válido neste bloco 𝑥; (b) se um bloco 𝑦 existe aninhado a 
um bloco 𝑥, qualquer nome declarado no bloco 𝑥 será valido no bloco 𝑦, a não 
ser que o nome também seja declarado no bloco 𝑦. Veja, por exemplo, o 
fragmento de código a seguir, em que temos dois lexemas i em escopos 
diferentes: 
#include <stdio.h> 
#include <stdlib.h> 
int main() { 
 int i = 35; 
 for (int i = 0; i < 10; i++){ 
 printf( "%d\n", i ); 
 } 
 printf( "%d\n", i ); 
 system( "pause" ); 
} 
 
 
20 
Se você executar esse código, terá uma contagem de zero até nove, 
seguida do número 35. A primeira declaração int i = 35; cria a variável i em 
memória e lhe atribui o valor 35; esta variável existe, e pode ser acessada dentro 
do bloco definido por int main() {...}. Mas, dentro desse mesmo bloco de código, 
usamos novamente o lexema i, dessa vez para definir uma variável i que só 
existe dentro do bloco definido por for (int i = 0; i < 10; i++){...}. O analisador 
léxico deve registrar os dois lexemas i como identificadores e diferenciar o 
escopo de cada um de acordo com o uso. O analisador léxico ainda terá que 
considerar o escopo estático, quando uma variável é definida antes do tempo de 
execução, e o escopo dinâmico, caso em que a variável será definida em tempo 
de execução. 
Uma das técnica utilizadas para resolver o problema do escopo é criar um 
conjunto de tabelas de símbolos, uma para cada escopo ativo durante o 
processo de compilação. Sempre que um bloco novo é encontrado, uma nova 
tabela de símbolos será criada. Nesse caso, é possível utilizar tanto a binary 
search tree quanto a linked list; muitas vezes a seleção é feita levando-se em 
consideração o tamanho do bloco. 
TEMA 5 – A TABELA DE SÍMBOLOS 
As tabelas de símbolos são estruturas de dados complexas utilizadas 
pelos compiladores para o armazenamento de informações referentes aos 
artefatos utilizados para a construção do programa (Aho et al., 2007). Esses 
artefatos serão relacionados ao código-fonte, ao modelo clássico de tabela de 
símbolos, ou à otimização e eficiência do código, modelo que vem sendo 
adotado em compiladores na segunda década do século XXI. 
A informação armazenada na tabela de símbolos é resultado do processo 
de compilação, de modo que, a cada novo passo, as informações são 
armazenadas, alteradas ou removidas, sempre com vistas a atender melhor o 
passo seguinte. Tradicionalmente, os principais módulos envolvidos nesse 
processo são o analisador léxico, que estamos estudando agora, e os 
analisadores sintático e semântico, que veremos nas próximas rotas. 
Atualmente, as tabelas de símbolos contêm informações sobre tempo de 
execução de blocos de código, pontos de entrada e saída de funções em 
memória e outras informações que podem ser úteis para a melhorar a eficiência 
 
 
21 
do código em execução. Com isso em mente, vamos rever o modelo do processo 
de compilação apresentado na Figura 4. 
Não é difícil perceber que, devido à sua importância, a tabela de símbolos 
requer alguma atenção e deve atender a requisitos de velocidade e eficiência. 
Nesse caso, a velocidade de acesso é indispensável, já que a tabela verdade 
deverá ser acessada; com muita frequência, a melhor solução é que toda a 
tabela esteja armazenada em memória. Isso provoca a necessidade de termos 
uma estrutura de armazenamento que seja de manutenção fácil e também 
flexível. Chega-se a uma estrutura na qual os registros podem ser facilmente 
removidos e em que a ordem e a hierarquia de armazenamento podem ser 
alteradas sem custos exagerados de processamento. 
Figura 4 - Modelagem do processo de compilação 
Existem diversas estruturas de dados que se valem da tabela de símbolos. 
Talvez a estrutura mais simples seja um array de structs. Podemos montar uma 
pilha, usando o array e controlando o tipo e a quantidade de dados em cada 
registro, valendo-se dos campos da structs. Um array é uma estrutura lenta. Uma 
alternativa é o uso das chamadas listas encadeadas, ou linked lists em inglês. 
Nesse caso, cada registro terá o endereço do próximo. Essa estrutura facilita a 
inserção de novos registros, ou até mesmo de blocos de registros, sem grandes 
custos computacionais extras. Também resolve o problema de remoção de 
Analisador 
Léxico 
Analisador 
Sintático 
Analisador 
Semântico 
Gerador de Código 
Intermediário 
Front-end 
 
Otimizador 
de Código 
Gerador d
e Código 
Final 
Back-end 
Tabela 
de 
Símbolos 
 
 
 
22 
blocos de registros, independentemente do seu tamanho. Contudo, não resolve 
o problema da busca reversa, que continua sendo computacionalmente cara. 
O problema da busca de informações na tabela de símbolos pode ser 
resolvido se utilizarmos uma estrutura chamada árvore binária de buscas, em 
inglês binary search tree. Nesse caso, o tempo de busca será praticamente igual 
ao tempo de inserção e não perderemos nenhuma das funcionalidades da lista 
encadeada. Essa estrutura também pode serconstruída a partir de structs; nesse 
caso, tudo que precisamos é garantir a existência de um ponteiro apontado para 
o nó da esquerda e outro apontando para o nó da direita. Considerando os 
fragmentos de código que usamos até o momento, teríamos que fazer a seguinte 
mudança nas nossas structs: 
typedef struct { 
 int linha; 
 int coluna; 
} Position; 
 
struct Token { 
 int ID; 
 int valor; 
 Position posicao; 
 struct Token *esquerda; 
 struct Token *direita; 
}*head; 
Já entendemos os problemas básicos do analisador léxico. Agora nos 
falta apenas aprofundar o processo de identificação. 
FINALIZANDO 
Estudamos as características do analisador léxico, que é o módulo do 
compilador responsável por identificar cada lexema do código fonte. Para 
entender esse processo, analisamos como definir uma linguagem formal a partir 
do Teorema de Kleene. Por sua vez, o Teorema de Kleene nos conduziu ao 
estudo das máquinas de estado determinísticas e das expressões regulares. São 
duas tecnologias que, além de servirem de base para a definição das linguagens 
formais, fundamentais para a programação, também se caracterizam como duas 
 
 
23 
formas eficientes de localizar e categorizar os lexemas em tokens. Uma vez que 
os tokens tenham sido criados, chegamos à tabela de símbolos. 
As máquinas de estado finito determinísticas se destacam nesse processo 
graças à sua facilidade de codificação. Contudo, muitas linguagens de 
programação adotaram algum tipo de expressão regular para a identificação de 
strings, dispensando a necessidade de codificar o processo de identificação de 
strings. Vimos inclusive que existem alguns padrões para o uso de expressões 
regulares em codificação, como o que é adotado no Posix. 
Durante o processo de compilação clássico, os módulos trocam 
informações entre si por meio da tabela de símbolos. Observando a necessidade 
de velocidade no processo de compilação, percebemos que a tabela de símbolos 
deve ser uma estrutura de dados rápida e flexível. Analisando um pouco mais 
detalhadamente o problema de identificar os lexemas, destacamos a existência 
de escopo, de funções e objetos e, principalmente, de comandos, como o typedef 
da linguagem C, os quais permitem que o programador crie novas palavras-
chaves, o que produz um nível extra, dinâmico, de complexidade para o 
processo. Resta-nos observar que, uma vez que todos os lexemas tenham sido 
identificados, essa informação será enviada para o próximo módulo, o analisador 
sintático, que será objeto da nossa próxima rota de aprendizagem. 
 
 
 
24 
REFERÊNCIAS 
AHO, A. V. et al. Compiladores: princípios, técnicas e ferramentas. 2. ed. 
Boston: Pearson Education Inc., 2007. 
HOLUB, A. I. Compiler design in C. Englewood Cliffs: Prentice Hall Software 
Series, 1990. 
MOGENSEN, T. Æ. Basics of Compiler Design. Copenhagen: Department of 
Computer Science University of Copenhagen, 2010. 
PITTS, A. M. Regular Languages and Finite Automata for Part IA of the 
Computer Science Tripos. Cambridge University Computer Laboratory, 
Cambridge, 2013, p. 58. 
STEPHEN Cole Kleene. In: Wikipédia, a enciclopédia livre. Disponível em: 
<https://pt.wikipedia.org/wiki/Stephen_Kleene> Acesso em: 6 fev. 2018.

Outros materiais