de conteúdos
Introduction 1.1
Parte I 1.2
Introdução e Visão Geral Sobre Compiladores 1.2.1
Estrutura de um Compilador 1.2.2
Analise Léxica 1.2.3
Analise Sintática 1.2.4
Analise Semântica 1.2.5
Parte II 1.3
Contruindo o primeiro analisador léxico com JFlex 1.3.1
Utilizando o JFlex e Java CUP 1.3.2
Criando Gramáticas Livres de Contexto com o JFlap 1.3.3
Exemplo de uso de tradução dirigida por sintaxe 1.3.4
Referências 1.4
2
Introduction
Introdução
O compilador é uma ferramenta fantástica, uma área de estudo em Ciências da
Computação desafiadora e muito interessante.
O objetivo desse ebook é dar uma visão geral sobre o processo de compilação, abordando
de forma teórica, na primeira parte, todas as etapas envolvidas na compilação de um
programa. Na segunda parte, o objetivo é demonstrar de forma prática como podemos
construir um compilador entendendo como alguns processos funcionam.
3
Parte I
Introdução
A construção de compiladores é um estudo sobre técnicas de tradução e aperfeiçoamento
de programas, quase todo o software utilizado é traduzido de alguma forma por um
compilador. Os compiladores são projetados de maneira cuidadosa e envolvem processos
grandes e complexos que incluem milhares de linhas de código organizados em
componentes que interagem de maneira a gerar uma compilação eficiente e rápida.
4
Introdução e Visão Geral Sobre Compiladores
O Compilador
O compilador é um software complexo que converte uma linguagem fonte, ou linguagem
origem, em uma linguagem destino, ou linguagem-objeto, ou seja, converte um programa
originado de uma linguagem de programação para uma linguagem que possa ser entendida
e executada por um computador. Durante a compilação são executadas tarefas que fazem
a tradução de uma linguagem em outra.
5
Introdução e Visão Geral Sobre Compiladores
Essas ferramentas geram códigos que podem ser incluídos no projeto do compilador. Um
exemplo são os geradores de analisadores léxicos, que com base em expressões regulares
geram um algoritmo capaz de identificar os elementos léxicos de uma linguagem de
programação.
Linguagens de programação
Podemos definir uma linguagem comumente chamada de língua ou idioma como um meio
de comunicação entre pessoas. Em programação definimos a linguagem como o meio de
comunicação entre o pensamento humano e as ações de um computador.
As linguagens de programação são projetadas para permitir que seres humanos expressem
processos computacionais em uma sequência de operações, tal como ler um arquivo e
imprimir o seu conteúdo em algum dispositivo, numa impressora por exemplo.
6
Introdução e Visão Geral Sobre Compiladores
Uma linguagem de programação é considera de alto nível quando sua representação está
próxima do domínio da aplicação e do problema a ser resolvido. Os computadores por sua
vez possuem sua própria linguagem denominada de baixo nível ou linguagem de máquina.
O processo de tradução de uma linguagem de alto nível para linguagem de baixo nível é
feito através de softwares conhecidos como compiladores e tem como entrada uma
linguagem fonte, alto nível, e como saída uma linguagem-objeto, baixo nível.
7
Introdução e Visão Geral Sobre Compiladores
Os primeiros passos para tornar a linguagem mais inteligível por humanos ocorreu na
década de 50 com o desenvolvimento de linguagens simbólicas ou assembly. As instruções
em Assembly representavam mnemônicos das instruções de máquina, mais tarde surgiram
ferramentas conhecidas como macro assemblers que permitiam abreviaturas
parametrizadas de uma sequência de instruções Assembly.
Os programadores que utilizam linguagem de baixo nível têm mais controle sobre a
execução de seus programas e podem produzir um código mais eficiente, mas esses
programas são difíceis de serem escritos e executados em outras máquinas.
8
Introdução e Visão Geral Sobre Compiladores
dados, controle de fluxo, etc. que facilitam a escrita de programas. Esses recursos são
traduzidos para linguagens de baixo nível e executadas diretamente nos processadores
através de instruções de máquina.
Muitas vezes temos que tomar a decisão de utilizar uma determinada linguagem de
programação em um projeto, embora possa parecer fácil aprender uma nova linguagem,
obter um nível considerável de expertise não é trivial. Não existe uma linguagem melhor ou
pior e não é confortável fazer comparações diretas, o processo de escolha de uma
linguagem deve analisar os recursos que cada uma oferece como solução aos problemas
que o domínio da aplicação possui.
Quando geramos um compilador para uma linguagem de programação nós já temos uma
linguagem que pode ser utilizada e manutenida, vamos supor que criamos uma linguagem
chamada Legal, essa linguagem foi concebida com base em uma já existente, Python por
exemplo - uma característica muito comum no surgimento de uma nova linguagem é ela ser
proposta com base em outras - é importante ressaltar que, quando a linguagem Legal já
estiver completa e com ela pudermos gerar seu próprio compilador, nós teremos um ciclo
autossuficiente.
Tradutores
Os tradutores são sistemas que aceitam como entrada um programa escrito em uma
linguagem e produzem como resultado um programa equivalente na mesma linguagem ou
em linguagens diferentes. Os tradutores podem ser classificados em:
9
Introdução e Visão Geral Sobre Compiladores
Um interpretador pode ser entendido como um processo que, ao invés de visar um conjunto
de instruções de um processador, visa produzir o efeito de sua execução. Eles normalmente
interpretam uma representação intermediária do programa fonte.
No esquema abaixo podemos ter uma ideia macro das diferenças de funcionamento de um
compilador e um interpretador.
10
Introdução e Visão Geral Sobre Compiladores
Estrutura de um compilador
O processo de compilação é muito complexo, existindo uma estrutura básica que divide
esse processo em fases, essas fases estão representadas por duas tarefas conhecidas
como análise e síntese.
Essa divisão de fases tem como objetivo dar uma visão explicita e detalhada do processo
de compilação. A tarefa de análise também chamada de front-end divide o programa fonte
em partes e impõe uma estrutura gramatical sobre elas, uma das principais
responsabilidades da tarefa de análise é garantir que a sintaxe e semântica do programa
fonte estejam corretos. A tarefa de síntese constrói o programa objeto a partir da
representação criada na tarefa de análise. A síntese é conhecida como back-end.
11
Introdução e Visão Geral Sobre Compiladores
É importante destacar que esse estrutura dividida em seis fases é apenas uma
representação didática e tem como objetivo demostrar como o compilador funciona,
outras literaturas podem apresentar outros modelos com mais ou menos etapas.
O processo de compilação inicia com o analisador léxico que varre todo o programa fonte
e transforma o texto em um fluxo de tokens, e nessa fase é criada a tabela de símbolos.
Logo em seguida vem a análise sintática que lê o fluxo de tokens e valida a estrutura do
programa criando a árvore sintática. A terceira fase é a análise semântica que é
responsável por garantir as regras semânticas. Todas essas fases fazem parte da tarefa de
análise.
A próxima fase e a geração de código intermediário, que cria uma abstração do código,
logo após vem a fase de otimização do código e por fim, a geração do código objeto
que tem como objetivo gerar o código de baixo nível baseado na arquitetura da máquina
alvo. Essas fases fazem parte da tarefa de síntese.
Termos
12
Introdução e Visão Geral Sobre Compiladores
[ˆ6] Bytecode: é uma representação de código-fonte que será interpretada por uma máquina
virtual.
[ˆ7] Açúcar sintático ou Syntactic sugar: É uma forma de tornar uma construção sintática
mais expressiva e simples de ler sem afetar seu comportamento.
13
Estrutura de um Compilador
Estrutura de um Compilador
Os compiladores operem em uma sequencia de fases, cada uma transformando o
programa fonte em uma representação para a etapa seguinte. Nesse capítulo vamos ver de
uma forma resumida cada etapa do processo de compilação.
Analise Léxica
É a primeira fase do processo de compilação, também é conhecida como leitura ou
scanning. O objetivo nessa fase é identificar unidades léxicas ou lexemas que compõem o
programa. O analisador léxico lê todos os caracteres do programa fonte e verifica se eles
pertencem ao alfabeto da linguagem. Caso um caractere não pertença ao alfabeto da
linguagem deve ser gerado um erro léxico.
De uma forma resumida a análise léxica deve quebrar o texto do programa fonte em
lexemas, verificar a categoria ao qual eles pertencem e produzir uma sequencia de
símbolos léxicos chamados como tokens.
Lexema Categoria
14
Estrutura de um Compilador
Nessa fase o processamento de uma linguagem pode ser feito por gramáticas regulares
podendo ser formalmente descrito por expressões regulares. As rotinas que processam
essa linguagem modelam algoritmos construídos a partir de autômatos finitos.
Analise Sintática
A analise sintática tem como objeto validar a gramática do programa, nessa etapa o objetivo
é reconhecer se a estrutura gramatical do código fonte esta de acordo com as regras
sintáticas da linguagem.
Nessa etapa é feita uma varredura na sequência de tokens recebidas do analisador léxico e
produzida uma estrutura de dados em formato de árvore conhecida como árvore sintática. A
árvore sintática representa a hierarquia do programa fonte. Caso uma construção seja
reconhecida com inválida um erro sintético deve ser gerado.
if (a – 10 > b * 2) a = b;
O analisador sintático deve ser capaz de analisar essa linha e de reconhecê-la como válida
ou inválida. Para isso ele precisa conhecer a estrutura formada palavra reservada if , a
expressão que esta entre ( e ) e o comando de atribuição a = b . Após essa validação
deve ser montada a árvore sintática.
Mesmo com uma boa técnica de detecção de erros o analisador sintático deve ser capaz de
recuperá-los e continuar o processo de compilação identificando o maior número possível.
15
Estrutura de um Compilador
Analise Semântica
O objetivo dessa etapa é verificar se a semântica do programa fonte tem consistência. Para
isso é utiliza a árvore sintática e as informações contidas na tabela de símbolos.
Por exemplo, a verificação de tipo em uma operação de soma onde cada operando é
verificado com cada operador.
Veja:
var a, b: int;
a = b + '2';
Pode ser necessário que nessa fase alguns tipos de dados sejam convertidos para outros
tipos, essa operação é conhecida como coerção.
Suponha que a variável position seja declarada como inteiro, nesse caso deverá deve ser
identificado um erro de tipo interrompendo o processo de compilação. Caso position ,
initial e rate sejam declarados como ponto flutuante o inteiro 60 deve ser convertido
A tabela de símbolos é muito importante nessa etapa pois é através dela que é possível
recuperar informações sobre os identificadores que são utilizadas para avaliar as regras
semânticas.
16
Estrutura de um Compilador
Nesse fase é gerado uma sequência de código denominada código intermediário, que
posteriormente em outras fases irá gerar o código objeto. Por ventura essa fase pode não
existir e a compilação pode ser feita diretamente para o código objeto, isso é comum em
compiladores auto residentes.
Veja o exemplo:
T1 = inttofloat(60);
T2 = id3 * t1
T3 = id2 + t2
id1 = t3
Otimização de Código
Nessa fase o objetivo é otimizar o código em termos de velocidade de execução e consumo
de memória. Essa etapa não depende da arquitetura de máquina e tem como objetivo fazer
transformações no código intermediário afim obter um código objeto mais otimizado.
T1 = id3 * 60.0
id1 = id2 * t1
Algumas compiladores permitem que seja parametrizado o nível de otimização, como por
exemplo o gcc através das flags de otimização -O2 , -O3 por exemplo.
17
Estrutura de um Compilador
Desse momento deve ser feito a seleção de registradores e reserva de memória para
contantes e variáveis. Essa é uma etapa muito importante pois a produção de código objeto
eficiente deve ter uma cuidadosa seleção de registradores.
O programa objeto reflete as instruções de baixo nível da plataforma que está sendo
utilizada, pode-se fazer necessário um gerador de código para cada plataforma.
Após essa fase concluída, para que o programa possa ser executado o código objeto é
linkeditado com outros programas objetos ou recursos para posteriormente ser carreado na
memória e executado pelo sistema operacional.
18
Estrutura de um Compilador
Tabela de símbolos
Durante toda a fase de compilação algumas informações são armazenadas em uma tabela
chamada de tabela de símbolos, que nada mais é do que uma estrutura de dodos em forma
de lista ou dicionário. As informações coletadas nessa tabela dependem do tipo de tradutor
desenvolvido e do programa objeto a ser gerado.
O analisador léxico coleta informações sobre os tokens e seus atributos, para tokens que
influenciam decisões de análise gramatical, como por exemplo identificadores, é criado uma
entrada na tabela de símbolos, das quais as informações são mantidas para posterior uso.
19
Estrutura de um Compilador
Podemos armazenar na tabela de símbolos também informações sobre a linha e coluna que
o token foi examinado para em caso de erro o compilador passa informar a posição da
falha.
Árvore Sintática
A árvore sintática é uma estrutura de dados em forma de árvore ou grafo que representa
sequencia hierarquica da linguagem de programação. Essa esturura permite represetnar
cada elemento do programa, os demais passos do compilador consitem em visatar os nos
dessa estrutura em uma determinada ordem
Termos
[ˆ1] Sintático: tem o sentido de verificação de forma, estrutura sem referência a significado.
20
Estrutura de um Compilador
[ˆ3] Linkeditor ou Linker: é um programa que reúne módulos compilados e arquivos para
criar o programa executável.
[ˆ4] Registradores: são áreas no processador que possuem uma grande velocidade e são
utilizados para armazenar valores. Existem vários tipos de registradores com finalidade
especificas.
21
Analise Léxica
Analise Léxica
A análise léxica também conhecida como scanner ou leitura é a primeira fase de um
processo de compilação e sua função é fazer a leitura do programa fonte, caractere a
caractere, agrupar os caracteres em lexemas e produzir uma sequência de símbolos léxicos
conhecidos como tokens.
A sequência de tokens é enviada para ser processada pela analise sintática que é a
próxima fase do processo de compilação .
Visão geral
A análise léxica pode ser dividida em duas etapas, a primeira chamada de escandimento
que é uma simples varredura removendo comentários e espaços em branco, e a segunda
etapa, a analise léxica propriamente dita onde o texto é quebrado em lexemas.
Padrão: é a forma que os lexemas de uma cadeia de caracteres pode assumir. No caso
de palavras reservadas é a sequência de caracteres que formam a palavra reservada,
no caso de identificadores são os caracteres que formam os nomes das variáveis e
funções.
A tabela abaixo mostra os exemplos de uso dos termos durante a análise léxica.
22
Analise Léxica
Constante
<numero, 18> Dígitos numéricos 0.6, 18, 0.009
numérica
<literal, Constante
"Olá"> Caracteres entre "" “Olá Mundo”
literal
Exemplo 1
onde:
Exemplo 2
const PI = 3.1416
onde:
23
Analise Léxica
Para implementar um analisador léxico é necessário ter uma descrição dos lexemas, então,
podemos escrever o código que irá identificar a ocorrência de cada lexema e identificar
cada cadeia de caractere casando com o padrão.
Tokens
Os tokens são símbolos léxicos reconhecidos através de um padrão.
Tokens simples: são tokens que não têm valor associado pois a classe do token já a
descreve. Exemplo: palavras reservadas, operadores, delimitadores: <if,> , <else> ,
<+,> .
Tokens com argumento: são tokens que têm valor associado e corresponde a
elementos da linguagem definidos pelo programador. Exemplo: identificadores,
constantes numéricas - <id, 3> , <numero, 10> , <literal, Olá Mundo> .
Onde o nome do token corresponde a uma classificação do token, por exemplo: numero,
identificador, const. E o valor do atributo corresponde a um valor qualquer que pode ser
atribuído ao token, por exemplo o valor de entrada na tabela de símbolos.
<id, 15> <=, > <id, 20> <*, > <id,30>, <(>, <)> <+, > <numero, 2>
24
Analise Léxica
<numero, 2> : token número, com valor para o atributo 2 indicado o valor do número
(constante numérica).
Exemplo 1
Código fonte
Sequência de tokens
<while,> <id,7> <<,> <numero,10> <do,> <id,7> <:=,> <id,12> <+,> <id, 7> <;, >
Tabela de símbolos
Entrada Informações
7 indice - variável inteira
Exemplo 2
Código fonte
Sequência de tokens
<id, 1> <=, > <id, 2> <+, > <id, 3> <*, > <numero, 60>
Tabela de símbolos
25
Analise Léxica
Entrada Informações
Exemplo 3
Código fonte
a[index] = 4 + 2
Sequência de tokens
<id, 1> <[,> <id, 2> <],> <=,> <numero, 4> <+,> <numero, 2>
Tabela de símbolos
Entrada Informações
1 a - variável inteira
Exemplo 4
Suponha que tenhamos a seguinte linha de código:
position = initial + rate * 60
Mapeamento de tokens.
position id Identificador 1
= <=, simbolo>
initial id Identificador 2
+ <+, operador>
rate id Identificador 3
* <*, operador>
60 numero Inteiro 4
26
Analise Léxica
<id, 1> <=> <id, 2> <+> <id, 3> <*> <numero, 4>
x = 0
while (x < 10) {
x++;
}
Erros léxicos
A análise léxica é muito prematura para identificar alguns erros de compilação, veja o
exemplo abaixo:
27
Analise Léxica
fi (a == “123”) ...
O analisador léxico não consegue identificar o erro da instrução listada acima, pois ele não
consegue identificar que em determinada posição deve ser declarado a palavra reservada
if ao invés de fi . Essa verificação somente é possível ser feita na análise sintática.
Uma situação comum de erro léxico é a presença de caracteres que não pertencem a
nenhum padrão conhecido da linguagem, como por exemplo o caractere ¢ . Nesse caso o
analisador léxico deve sinalizar um erro informando a posição desse caractere.
Expressões regulares
-> Expressões regulares ou regex são uma forma simples e flexível de identificar cadeias
de caracteres em palavras. Elas são escritas em uma linguagem formal que pode ser
interpretada por um processador de expressão regular que examina o texto e identifica
partes que casam com a especificação dada, são muito utilizadas para validar entradas de
dados, fazer buscas, e extrair informações de textos. As expressões regulares não validam
dados, apenas verificam se um texto está em uma determinado padrão. <-
As expressões regulares são formadas por metacarateres que definem padrões para obter
o casamento entre uma regex e um texto.
Metacaracteres
28
Analise Léxica
Quantificadores
Casar
29
Analise Léxica
Veja os exemplos:
A regex \d{5}-\d{3} é utilizada pra validar CEP. Essa regex casa com os padrões de
texto 89900-000 e 87711-000 mas não casa com os padrões 87711-00077 e
89900000 . A regex é formada pelo metacaractere \d e o quantificador {5}
30
Analise Léxica
Embora no exemplo seja simples implementar um analisador léxico, essa tarefa podem ser
muito trabalhosa, como essa complexidade é frequente na evolução de uma linguagem de
programação surgiriam ferramentas que apoiam esse tipo de desenvolvimento.
Flex – http://flex.sourceforge.net/
JFlex – http://jflex.de/download.html
Turbo Pascal Lex/Yacc - http://www.musikwissenschaft.uni-mainz.de/~ag/tply/
Flex++ - http://www.kohsuke.org/flex++bison++/
CSLex – versão C#, derivada do Jlex - http://www.cybercom.net/~zbrad/DotNet/Lex
O ponto de partida para a criar uma especificação usando a linguagem lex é criar uma
especificação de expressões regulares que descrevem os itens léxicos que são aceitos.
Regras de Tradução: Nessa seção são vinculada regras que correspondentes a ações
em cada expressão regular valida na linguagem.
31
Analise Léxica
onde: Padrão é uma expressão regular que pode ser reconhecida pelo analisador léxico
Ação é um fragmento de código que vai se invocado quando a expressão é reconhecida.
Os geradores de analisadores léxicos geram rotinas para fazer a análise léxica de uma
linguagem de programação a partir de um arquivo de especificações contendo basicamente
expressões regulares que descrevem os tokens. Essas rotinas representam algoritmos de
autômatos finitos - DFA e NFA.
É possível fazer a identificação de cada token através do seu padrão, após esse processo é
gerado um arquivo fonte com a implementação do analisador léxico baseado em uma
autômato finito que transforma os padrões de entrada em um diagrama de estados de
transição.
Termos
[ˆ1] - Autômato finito: Envolvem estados e as transições entre estados de acordo com a
determinadas entradas.
[ˆ2] - Autômato finito determinístico - DFA: É um autômato finito onde cada símbolo de
entrada possui no máximo uma saída, ou seja, para cada entrada existe um estado onde o
pode transitar a partir de seu estado atual.
[ˆ3] - Autômato finito não determinístico - NFA: É um autômato finito onde um símbolo de
entrada tem duas ou mais saídas, ou seja, pode estar em vários estados ao mesmo tempo,
isso possibilita ao algoritmo tentar adivinhar algo sobre a entrada.
4 - Expressões regulares - é uma notação - linguagem - utilizada para descrever padrões
32
Analise Sintática
Analise Sintática
O Analisador sintático também conhecido como parser tem como tarefa principal determinar
se o programa de entrada representado pelo fluxo de tokens possui as sentenças válidas
para a linguagem de programação.
Visão geral
A sintaxe é a parte da gramática que estuda a disposição das palavras na frase e das frases
em um discurso. Essa etapa no processo de compilação deve reconhecer as forma do
programa fonte e determinar se ele é valido ou não.
Esse modelo pode ser definido utilizando gramáticas livres de contexto que representam
uma gramática formal e pode ser escrita através de algoritmos fazem a derivação de todas
as possíveis construções da linguagem.
Alfabeto: {w, h, i, l, e, +, 1, 2, 3}
Símbolos: 1, 5, +, w
Sentença: while, 123, +1
Linguagem: {while, 123, +1}
33
Analise Sintática
Já o analisador sintático vê o mesmo texto como uma sequência de sentenças que deve
satisfazer as regras gramaticais. É através da gramática que podemos validar expressões
criadas na linguagem de programação.
O analisador sintático agrupa os tokens em frases gramaticais usadas pelo compilador com
o objetivo de criar uma saída que representa a estrutura hierarquia do programa fonte.
Veja no quadro abaixo as especificações de entrada e saída das etapas vistas até o
momento:
34
Analise Sintática
Entende-se por regras gramáticas as formas como podemos descrever a estrutura sintática
do programa.
Veja um exemplo no diagrama abaixo demostrando esse processo de compilação visto das
etapas estudadas até o momento.
35
Analise Sintática
36
Analise Sintática
Outra aplicação de GLC são os DTD - Definição de Tipos de Documentos - utilizados por
arquivos XML que descreve as tags de uma forma natural, as tags deve estar aninhas afim
de lidar com o significado do texto.
Veja o exemplo:
<produto><codigo</codigo></produto>
Essa mesma forma em uma Gramática Livre de Contexto pode ser expressada da seguinte
maneira:
declaração → if ( expressão ) then declaração else declaração ;
A definição formal de uma gramática livre de contexto pode ser representada através dos
seguintes componentes:
G = (N, T, P, S)
Onde:
Terminologias:
Símbolo inicial: É a variável, simbolo não terminal, que representa o inicio da definição
da linguagem.
37
Analise Sintática
{A} → {α}
Onde:
P = {
S → aSb
S → λ
}
Essa gramatica é formada pelas terminais a e b , que são os tokens da linguagem, como
regras de produção nos temos aSb que obriga ter um a e b nas extremidades da
palavras, o simbolo λ que significa vazio.
Derivações
A derivação é a substituição do conjunto de símbolos não terminais por símbolos terminais
começando pelo símbolo inicial, ao final desse processo o resultado é a forma como a
linguagem deve assumir.
Durante a derivação devemos aplicar as regras de produção para substituir cada simbolo
não terminal por um simbolo terminal, isso permite identificar se certa cadeias de caracteres
pertence a linguagem, as regras expandem todas as produções possíveis. Como resultado
desse processo temos a árvore de derivação.
Tipos de derivação:
38
Analise Sintática
Árvore de derivação
É uma estrutura em formato de árvore que representa a derivação de uma sentença ou
conjunto de sentenças, essa estrutura ira gera a árvores de analise sintática que representa
o programa fonte, e é o resultado da analise sintática, essa estrutura facilita é muito
utilizada nas etapas seguinda da compilação.
P = {
S → aSb
S → λ
}
39
Analise Sintática
Ambiguidade:
Certas gramaticas permitem que uma mesma sentença tenha mas de uma árvore de
derivação, isso torna a gramática inadequada para a linguagem de programação, pois o
compilador não pode determinar a estrutura desse programa fonte. Duas derivações podem
gerar uma unica árvore sintática, mas duas árvores sintáticas não podem ser geradas por
uma derivacao.
40
Analise Sintática
p {
E → E + E
E → E * E
E → (E)
E → x
E → λ
}
Observe que duas árvores sintáticas foram geradas para essa sentença, logo temos uma
ambiguidades.
P {
E → T + E | T
T → x * T
E → x
E → (E) * T
E → (T)
E → λ
}
Não existe algoritmo capaz de eliminar a ambiguidade, nesses casos é necessário aplicas a
técnicas de eliminação de ambiguidade.
41
Analise Sintática
Veja o exemplo
Forma de Backus-Naur
42
Analise Sintática
Seleção:
Opcional:
ae abcde
(α)* o que estiver entre parentese pode repetir um numero qualquer de vezes e pode não
ser usado
43
Analise Sintática
ac abc
abbc
abbc abbb...c
(α)+ o que estiver entre parentese pode repetir um numero qualquer de vezes
abbc abbb...c
Outro tipo de notação usuada para representar gramáticas é a notação de grafos sintáticos.
Esta notação tem o mesmo poder de expressão de BNF, porém define uma representação
visual para as regras de uma gramática livre de contexto.
44
Analise Sintática
Na parte 2 desse e-book é apresentado o software JFLAP que pode ser utilizado para
criar GLC e fazer as derivações a fim de entender melhor esse conceito.
Exemplo 01 – Linguagem ab
Definir a gramática:
P {
S → aSb
S → λ
}
Identificação terminologias:
45
Analise Sintática
Descrição
Símbolos terminais a, b
Símbolos não terminais: S
Símbolo inicial: S
Regra de produção: P
1 → aSb
2 → aaSbb
3 → aabb
Com a gramática acima é possível dizer que palavra aab pertence linguagem?
Identificação terminologias:
Descrição
Símbolos terminais a, b
Símbolos não terminais: S, A, B
Símbolo inicial: S
Regra de produção: P
1 → AB
2 → aAB
3 → abB
4 → abb
46
Analise Sintática
Identificação terminologias:
Descrição
Símbolos terminais a, b
Símbolos não terminais: S, A, B
Símbolo inicial: S
Regra de produção: P
1 → aaAb
2 → aaaAb
3 → aaab
Com a gramática acima é possível dizer que palavra abb pertence linguagem? E o aabb
pertence a linguagem?
Exemplo 03 Linguagem a
Definir da gramática:
47
Analise Sintática
Identificação terminologias
Descrição
Símbolos terminais a
Símbolos não terminais: S, A, B, C
Símbolo inicial: S
Regra de produção: P
Olhando para esse gramatica nos podemos concluir que ela somente gerar linguagem
formas por a .
P {
E → E + T
E → E - T
E → T
T → T * F
T → T / F
T → F
F → (E)
F → x
}
Identificação terminologias
Descrição
Símbolos terminais +, -, *, /, (, ), x
Regra de produção: P
48
Analise Sintática
1 → T
2 → T * F
3 → F * F
4 → (E) * F
5 → (E + T) * F
6 → (E + T) * F
7 → (T + T) * F
8 → (F + T) * F
9 → (x + T) * F
10 → (x + F) * F
10 → (x + x) * F
11 → (x + x) * x
Identificação terminologias:
Descrição
Símbolos terminais (,), ,, ,id
Regra de produção: P
49
Analise Sintática
1 → exibir(X)
2 → exibir(Y)
3 → exibir(Y, Z)
4 → exibir(Z, Z)
5 → exibir(valor, Z)
6 → exibir(valor, desconto)
50
Analise Semântica
Analise Semântica
Até o momento vimos as etapas de análise léxica, que quebra o programa fonte em tokens
e a analise sintática, que valida as regras a sintaxe da linguagem de programação.
Não é possível representar com expressões regulares ou com uma gramática livre de
contexto regras como: todo identificador deve ser declarado antes de ser usado. Muitas
verificações devem ser realizadas com meta-informações e com elementos que estão
presentes em vários pontos do código fonte, distantes uns dos outros. O analisador
semântico utiliza a árvore sintática e a tabela de símbolos para fazer as analise semantica.
É importante ressaltar que muitos dos erros semanticos tem origem de regras
dependentes da linguagem de programacao.
As validações que não podem ser executadas pelas etapas anteriores devem ser
executadas durante a análise semântica a fim de garantir que o programa fonte estaja
coerente e o mesmo possa ser convertido para linguagem de máquina.
Essa etapa também captura informações sobre o programa fonte para que as fases
subsequentes gerar o código objeto, um importante componente da analise semântica é a
verificação de tipos, nela o compilador verifica se cada operador recebe os operandos
permitidos e especificados na linguagem fonte.
Um exemplo que ilustra muito bem essa etapa de validação de tipos é a atribuição de
objetos de tipos ou classe diferentes. Em alguns casos, o compilador realiza a conversão
automática de um tipo para outro que seja adequado à aplicação do operador. Por exemplo
a expressão.
var s: String;
s := 2 + ‘2’;
51
Analise Semântica
No exemplo acima o analisador semântico de ter uma série de preocupações para validar o
significado de cada regra de produção.
i := a + b;
Os tipo de dados são muito importantes nessa etapa da compilação, eles são uma notações
que as linguagens de programação utilizam para representar um conjunto de valores. Com
base nos tipos o analisador semântico pode definir quais valores podem ser manipulados,
isso é conhecido com type checking.
Inferência de tipos
O sistema de tipos de dados podem ser divididos em dois grupos: sistemas dinâmicos e
sistemas estáticos. Muitas das linguagens utilizam o sistema estático, esse sistema é
predominante em linguagens compiladas, pois essa informação é utilizada durante a
compilação e simplifica o trabalho do compilador.
52
Analise Semântica
O compilador deve garantir que variáveis e funções estejam declaradas em locais que
podem ser acessados onde esses identificadores estão sendo utilizados.
Uma classe com funções declaradas como privadas e essas funções sendo utilizadas fora
da classe.
class Foo {
private $x;
Foo().sum()
Veja o exemplo da variável contador, ele deve ser declarada em cada escopo onde esta
sendo utilizada.
def foo():
cont = 0
def bar():
cont = 1
def dop():
cont = 3
bar()
dop()
print(cont)
Compatibilidade de tipos:
53
Analise Semântica
Verificar se os tipos de dados declarado nas variáveis e funções estão sendo utilizados e
atribuidas corretamente, por exemplo: operações matemáticas devem ser realizadas com
números, atribuições de valores para variávies.
var i : int;
var s : string;
s = "john";
i := f;
Em alguns casos pode ser efetuada a conversão de tipos, essa operacao é conhecida
como cast, e pode ser feito de forma explicita no código ou pelo proprio compilador.
Essa verificação é muito importante pois ele deve garantir que os identificadores sejam
unicos não havendo na tabela de simbolos uma entrada para o mesmo identificador:
var i : int;
var i : int64;
func foo() {
fmt.Println(math.pi)
}
54
Parte II
Introdução
O estudo de compiladores deve misturar teoria e prática, é importante entender todos os
componentes envolvidos nesse processo e como podemos implementar cada um deles.
Nessa parte do ebook nós vamos aplicar os conhecimentos adquiridos até o momento
utilizando ferramentas e escrevendo algoritmos relacionados a componentes do projeto de
um compilador.
55
Contruindo o primeiro analisador léxico com JFlex
Instalando o JFlex
Faça o download do JFlex nesse link: http://jflex.de/download.html
Nesse primeiro exemplo nós vamos usar o JFlex integrado com a IDE Eclipse.
Você pode adicionar a biblioteca jflex-1.6.1.jar na configuração de Build Path do seu projeto
no Eclipse.
Build Path do projeto. Você também deve crair uma pacote chamado br.com.johnidouglas .
Se preferir você pode criar um projeto usando o Maven e adicionar a seguinte dependência.
<dependency>
<groupId>de.jflex</groupId>
<artifactId>jflex</artifactId>
<version>1.6.1</version>
</dependency>
56
Contruindo o primeiro analisador léxico com JFlex
Em seguida vamos criar a classe que ira gerar o analisador léxico a cada nova alteração no
arquivo de especificação. Isso evita o uso da linha de comando para gerar a classe Java
responsável por implementar o algoritmo de reconhecimento de tokens.
package br.com.johnidouglas.lexicalanalyzer;
import java.io.File;
import java.nio.file.Paths;
jflex.Main.generate(sourceCode);
}
}
57
Contruindo o primeiro analisador léxico com JFlex
package br.com.johnidouglas.lexicalanalyzer;
%%
%{
%}
%class LexicalAnalyzer
%type void
%%
Para isso crie uma nova classe chamada LanguageSextaFase e inclua o seguinte código
nela.
58
Contruindo o primeiro analisador léxico com JFlex
package br.com.johnidouglas.lexicalanalyzer;
import java.io.IOException;
import java.io.StringReader;
}
}
if - Palavra reservada if
- Espaço em branco
2 - Número Inteiro
- Espaço em branco
+ - Operador de soma
- Espaço em branco
3 - Número Inteiro
+ - Operador de soma
a - Identificador
- Espaço em branco
then - Palavra reservada then
59
Contruindo o primeiro analisador léxico com JFlex
Vamos aprimorar o nosso analisador léxico para que ele reconheça os tokens pertencentes
a linguagem de programação Pascal.
Crie uma classe chamada GeneratorPascal , para isso você deve criar um novo pacote Java
dentro do projeto lexicalanalyzer , vamos chamar esse pacote de pascal -
br.com.johnidouglas.lexicalanalyzer.pascal .
package br.com.johnidouglas.lexicalanalyzer.pascal;
import java.io.File;
import java.nio.file.Paths;
jflex.Main.generate(sourceCode);
}
}
60
Contruindo o primeiro analisador léxico com JFlex
Agora vamos criar um arquivo de código chamado program.pas esse arquivo vai conter o
código fonte do programa escrito em Pascal.
Vamos criar uma classe chamada PascalToken essa classe vai representar um token
reconhecido na linguagem e deve ter duas propriedades, name , value , line e column .
package br.com.johnidouglas.lexicalanalyzer.pascal;
61
Contruindo o primeiro analisador léxico com JFlex
package br.com.johnidouglas.lexicalanalyzer.pascal;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Paths;
PascalToken token;
62
Contruindo o primeiro analisador léxico com JFlex
package br.com.johnidouglas.lexicalanalyzer.pascal;
import java_cup.runtime.*;
%%
%{
%}
%public
%class LexicalAnalyzer
%type PascalToken
%line
%column
inteiro = 0|[1-9][0-9]*
brancos = [\n| |\t]
program = "program"
%%
. { throw new RuntimeException("Caractere inválido " + yytext() + " na linha " + yylin
e + ", coluna " +yycolumn); }
Você deve ter percebido que alguns erros ocorreram, isso por que nosso arquivo de
especificação esta incompleto, ou seja, não possui todos os tokens da linguagem Pascal.
63
Contruindo o primeiro analisador léxico com JFlex
64
Utilizando o JFlex e Java CUP
A construção de uma linguagem de programação deve iniciar pela definição léxica, abaixo é
apresentado um fragmento de código escrito na linguagem que vamos construir que vai se
chamarSextaFase.
program <{
@age INT:
@name STR:
&imprmir(^idade; ^nome) [
if TT [
$idade -> ^idade:
$nome -> ^nome:
]
if FF [
$nome -> ^nome:
]
]
%imprimir(~sdfsd~; 12; 12):
}>
65
Utilizando o JFlex e Java CUP
Variáveis devem ser declaradas com o sinal @ , seguido do nome formado por um
conjunto de letras e seu respectivo tipo: INT ou STR ;
A declaração de funções deve ser precedida pelo sinal & seguido por um conjunto de
letras que formam o nome;
Os parâmetros das funções devem ser identificados pelo sinal ^ seguido por um
conjunto de letras;
As constantes verdadeiro e falso são identificados respectivamente por TT e FF ;
Os bloco de inicio e fim de uma instrução é identificado por [ e ']';
Operações de atribuição são identificadas pelo símbolos -> ;
A strings devem estar entre o sinal de ~ ;
As variáveis deve ser usadas colocando o sinal $ na frente do nome;
As funções são chamadas colocando o sinal % na frente do nome;
Nas funções os parâmetros são separados por ; tando na declaração como na
chamada;
Agora vamos criar o arquivo de especificação léxica. Dentro do projeto crie um arquivo
chamado Lexer.lex com o seguinte conteúdo.
import java_cup.runtime.Symbol;
%%
%cup
%public
%class Lexer
%line
%column
DIGIT = [0-9]
LETTER = [a-zA-Z_]
COMMENT = \|\|.*\n
STRING = \~{LETTER}+\~
INTEGER = {DIGIT}+
VARIABLE = @{LETTER}+
ASSIGNMENT = \${LETTER}+
FUNCTION = &{LETTER}+
FUNCTION_PARAMS = \^{LETTER}+
CALL_FUNCTION = %{LETTER}+
IGNORE = [\n|\s|\t\r]
%%
<YYINITIAL> {
66
Utilizando o JFlex e Java CUP
{IGNORE} {}
{COMMENT} {}
Para que o analisador léxico desenvolvido com o JFlex funcione corretamente em conjunto
com o analisador sintático Java Cup as seguintes instruções devem ser incluídas no arquivo
lex.
identificados.
67
Utilizando o JFlex e Java CUP
return new Symbol(sym.INICIO) - Cada token identificado deve retornar uma instancia
da classe Symbol que representa uma constante numérica. Essa constante é gerada
automaticamente pelo Java Cup.
Abaixo um exemplo de uma gramática que pode ser utilizada para montar uma frase.
P {
SENTENÇA → SN SV
SN → ARTIGO SUBSTANTIVO
SV → VERBO COMPLEMENTO
COMPLEMENTO → ARTIGO SUBSTANTIVO | SUBSTANTIVO
ARTIGO → o
SUBSTANTIVO → aluno | compiladores
VERBO → estudou
}
Com as regras de produção acima é possível validar a seguinte frase: o aluno estudou
compiladores .
Vamos criar um arquivo com as especificações sintáticas, esse arquivo deve ser chamado
de Parser.cup e deve ter o seguinte conteúdo.
<nome do pacote>
import java_cup.runtime.*;
import java.util.*;
import java.io.*;
parser code {:
:};
68
Utilizando o JFlex e Java CUP
Caso tenha sido criado um pacote no seu projeto Java, você deve substituir no arquivo
Parser.cup a tag <nome do pacote> pelo nome do pacote ou remove-la caso não
Esse arquivo contém as especificações sintáticas, que nada mais é do que a gramática da
linguagem de programação. Da mesma forma que o analisador léxico gerado pelo Jflex, o
analisador sintático também irá gerar código Java para validar a sintaxe da linguagem de
programação.
69
Utilizando o JFlex e Java CUP
Para finalizar o projeto da linguagem de programação vamos criar uma classe que vai
executar o compilador.
import java.io.FileReader;
import java.nio.file.Paths;
try {
Parser p = new Parser(new Lexer(new FileReader(sourcecode)));
Object result = p.parse().value;
}
}
Você pode alterar o nome da classe Java para outro qualquer, nesse exemplo nos
estamos utilizando o nome Main .
Todo o código para o projeto do compilador esta pronto, agora no precisamos gerar o
analisador léxico e o analisador sintático. Isso deve ser feito através da linha de comando
utilizando o JFlex e o Java Cup.
Acesse o programa de linha de comando e navegue até o diretório onde os arquivos lex e
cup estão.
Por exemplo:
cd /home/johni/Projects/compiler/sextafase-language/src/
Depois você deve localizar o diretório onde os arquivo jar do JFlex e Java Cup estão, e
executar as seguintes linhas de código.
70
Utilizando o JFlex e Java CUP
71
Criando Gramáticas Livres de Contexto com o JFlap
Para mais detalhes dos recursos e de como usar o JFALP acesse o tutorial oficial disponível
em http://www.jflap.org/tutorial/.
O JFLAP é um projeto escrito em Java, portanto você precisar ter o Java instalado para
executar esse aplicativo.
A ultima versão dispensável o JFLAP é a 8, nos exemplos abaixo vamos utilizar essa
versão.
Neste tópico vamos abordar como podemos utilizar o JFLAP para construir gramáticas e
verificar a arvore de derivação gerada por determinada gramatica. Nós podemos verificar se
a gramática esta de acordo com uma determinada sentença.
72
Criando Gramáticas Livres de Contexto com o JFlap
Observe que temos um menu com varias opções, oque nos interessa nesse momento é o
menu Grammar. Clicando nessa opção, a seguinte tela sera averta, esse vai ser nosso
ambiente de trabalho.
Data a seguinte gramatica `G = ({S}, {a, b}, P, S), inclua as seguintes regras de produção no
JFLAP.
S → aSa
S → bSb
S → λ
73
Criando Gramáticas Livres de Contexto com o JFlap
Acesse o menu Input -> Brute Force Parser , a seguinte tela ira abrir.
74
Criando Gramáticas Livres de Contexto com o JFlap
Com ela nos podemos realizar os testes e verificar a validade de uma sentença.
No campo Input digite a seguinte sentença aabb , clique no botão Set depois clique no
botão Complet. Observe que uma mensagem aparece informado se a sentença é invalida
ou invalida.
75
Criando Gramáticas Livres de Contexto com o JFlap
Observe que a sentença aabb não é valida para a gramatica desse exemplo.
Clique no botão Change e inclua a seguinte sentença aabbaa . Novamente clique no botão
Set depois clique no botão Complet. Como essa sentença é valida o resultado foi o
seguinte:
76
Criando Gramáticas Livres de Contexto com o JFlap
77
Criando Gramáticas Livres de Contexto com o JFlap
Observe que você pode utilizar o botoes Step, Undo, Reset e Complete para acompanhar o
resultado da derivação da gramatica.
Nas etapas seguintes nos vamos utilizar vários exemplos de gramaticas em sequencia,
para melhor entender esse conceito. Você pode fazer os testes das gramaticas no JFLAP,
verificar o resultado de cada uma, e acompanhar a derivação.
78
Criando Gramáticas Livres de Contexto com o JFlap
S → SS
S → (S)
S → ()
S → λ
Sentenças:
()() - Válida
()()) - Inválida
(())() - Válida
S → SS
S → ()
S → (S)
S → []
S → [S]
S → λ
Sentenças:
() - Válida
[] - Válida
[()()] - Válida
[([()])] - Válida
[(]) - Inválida
[(([[]]))] - Válida
S → aSb
S → ab
S → λ
79
Criando Gramáticas Livres de Contexto com o JFlap
Sentenças:
ab -Válida
abab - Inválida
aaabbb - Valida
S → SaSc
S → bSc
S → SaS
S → Sb
S → bS
S → λ
Sentenças:
ac - Válida
aac - Válida
aaac - Válida
bbbbc - Válida
acd - Inválida
aaaaa - Válida
S → x
S → xAx
S → (S)
A → +
A → *
Sentenças:
x+x - Válida
x*x - Valida
x-x - Inválida
+x - Inválida
80
Criando Gramáticas Livres de Contexto com o JFlap
O JFLAP é um software completo, você utilizar ele para validar gramáticas mais complexas.
81
Exemplo de uso de tradução dirigida por sintaxe
Implementando um interpretador de
operações matemáticas básicas
Nesse etapa nós vamos construir um interpretador de operações matemáticas básicas, o
objetivo é demonstrar como podemos implementar um tradutor dirigido por sintaxe.
Vamos criar um analisador léxico com o JFlex e um analisador sintático com o Java Cup,
ambas a bibliotecas pode ser baixadas nesse link - http://jflex.de/download.html.
A tradução dirigida por sintaxe tem como objetivo associar a execução de cada regra da
gramatical uma ação semântica. Nesse projeto a ação semântica será avaliar a operação
matemática e executar ela, interrompendo a operação e exibindo o seu resultado quando
encontrar o simbolo de final de instrução.
Nós vamos utilizar um projeto Java, você pode utilizar qualquer IDE de desenvolvimento de
sua preferencia. Vamos omitir os detalhes do projeto e focar somente nas partes relevantes
para mostrar como um tradutor dirigido por sintaxe pode ser implementado.
Vamos começar definido as regras léxicas do nosso projeto, basicamente vamos avaliar as
operações matemáticas básicas lidas de um arquivo, caso encontro um sinal de ; deve
ser impresso o resultado da expressão.
(2 * 1) * 9; 1+ 3;
(1 + 3 + 5) * 8;
82
Exemplo de uso de tradução dirigida por sintaxe
import java_cup.runtime.*;
%%
%class Lexer
%unicode
%cup
%line
%column
%{
private Symbol symbol(int type) {
return new Symbol(type, yyline, yycolumn);
}
private Symbol symbol(int type, Object value) {
return new Symbol(type, yyline, yycolumn, value);
}
%}
LineTerminator = \r|\n|\r\n
InputCharacter = [^\r\n]
WhiteSpace = {LineTerminator} | [ \t\f]
Digit = [0-9]
Number = {Digit} {Digit}*
Letter = [a-zA-Z]
%%
<YYINITIAL> {
{Number} { return symbol(Sym.NUMBER, new Integer(Integer.parseInt(yytext())
)); }
{WhiteSpace} {}
}
83
Exemplo de uso de tradução dirigida por sintaxe
Vamos utilizar Gramática Livres de Contexto para definir as regras sintáticas da nossa
linguagem de programação.
Observe que cada regra sintática tem uma ação vinculada. Por exemplo expr:e SEMI {:
System.out.println(">> " + e); :} , O código System.out.println(">> " + e); referente a
expressão sintática expr:e SEMI será executa quando esse expressão for avaliada pelo
compilador.
import java_cup.runtime.*;
parser code {:
:}
84
Exemplo de uso de tradução dirigida por sintaxe
O código utilizada para executar o interpenetrador pode ser visto abaixo. Observe que ele
abre o arquivo calc.l .
import java.io.FileNotFoundException;
import java.io.FileReader;
Acesse o programa de linha de comando e navegue até o diretório onde os arquivos lex e
cup estão.
Por exemplo:
cd /home/johni/Projects/interpreter/calc/src/
Depois você deve localizar o diretório onde os arquivos jar do JFlex e Java Cup estão, e
executar as seguintes linhas de código.
Você pode executar o projeto e ver que cada expressão matemática esta sendo avaliada no
momento que ele é encontrada.
85
Exemplo de uso de tradução dirigida por sintaxe
86
Referências
Referências
DELAMARO, M. E. Como construir um Compilador: utilizando ferramentas Java. São Paulo:
Novatec, 2004.
FISCHER, Charles N.; CYTRON, Ron K.; LEBLANC, Richard J. Crafting a compiler. Boston:
Addison-Wesley, 2010.
SANTOS, Pedro Reis; LANGLOIS, Thibault. Compiladores: Da Teoria à Prática. 1ª. ed.
Lisboa: FCA, 2014.
SETHI, Ravi; ULLMAN, Jeffrey D.; LAM, Monica S. Compiladores: Princípios, Técnicas e
Ferramentas. 2. ed. São Paulo: Pearson Addison Wesley, 2008.
TORCZON, Linda; KEITH, Cooper. Construindo Compiladores. 2ª. ed. Rio de Janeiro:
Elsevier, 2014.
HOPCRAFT, Johu E; MOTWAM, Rajew. ULLMAN, Jeffrey D.; Introducao a teoria dos
automatos, Lingagens e Computacao,
87