Desde
quando comecei meus estudos de computação e aprendi a primeira
linguagem de programação, C, eu tenho a curiosidade de entender o
funcionamento de um compilador.
Ficava
fascinado pelo fato de um programa transformar um arquivo texto
contendo construções da linguagem, em outro arquivo contendo código
que uma máquina consegue compreender preservando a semântica, ou
seja, seu significado. Esse é o processo de compilação.
Para
iniciantes, isso fica um pouco abstrato porque o foco inicial é
aprender a programar e não como funciona um compilador.
Finalmente,
no meu 7° período de curso (bem que poderia ser antes!) tive a
oportunidade de, não só entender todo esse processo de compilação,
mas também construir um compilador seguindo as etapas de projeto.
Cursei a disciplina Compiladores ministrada pelo professor Marcus Ramos o qual agradeço a paciência e disponibilidade para tirar
dúvidas e compartilhar seu conhecimento, o que possibilitou a
realização desse trabalho!
A
linguagem usada para a construção foi MiniJava. Ela é uma versão
reduzida do Java. Possui apenas construções simples e comandos
básicos como if, while, System.out.println(), permite criação de
métodos e atributos de classe e possui apenas tipos int, boolean,
objeto e vetor de int.
Como
nosso tempo foi curto (pouco menos de 2 meses, porque antes tivemos
que ver a teoria que envolve a construção de compiladores), algumas
decisões de projeto tiveram de ser tomadas para que pelo menos algo
de cada etapa (análise sintática, análise de contexto e geração
de código) pudesse ser feito.
A análise
sintática consiste em reconhecer os símbolos (tokens) a partir da
leitura de um arquivo texto de entrada e verificar se eles respeitam
as regras gramaticais da linguagem fonte (no caso foi usada uma BNF
estendida que pode ser encontrada aqui).
Na
semântica, já com os tokens corretamente reconhecidos, o programa é
verificado se está correto agora observando o significado das
construções. Basicamente acontece a identificação (reconhecimento
de variáveis e métodos) e vinculação (vincula cada uso de
variável com sua declaração). Possíveis erros são facilmente
identificados e mensagens exatas podem ser emitidas nessas etapas.
E, por
fim, geração de código. Com o programa bem escrito e com o
significado correto perante as regras da linguagem, as instruções
podem ser traduzidas passo a passo para linguagem de máquina.
Gerando o PUSH para reservar espaço para variáveis, STORE para
guardar valores, LOAD para carregar valor de um registrador, JUMP
para desvios, dentre outras. Essas são algumas básicas, cada
máquina possui seu conjunto de instruções.
No nosso
caso, o código de máquina foi gerado com base em uma máquina
abstrata usada a partir do livro base da disciplina (link dele aqui),
o TAM (Triangle Abstract Machine).
Aqui nesse link você encontra o arquivo compactado contendo o executável do compilador em bytecode juntamente com uma classe de exemplo. Também há um manual explicando como que instala e usa o compilador! Fique à vontade para testar e comentar.
Um abraço,
Bruno Pinho.
Aqui nesse link você encontra o arquivo compactado contendo o executável do compilador em bytecode juntamente com uma classe de exemplo. Também há um manual explicando como que instala e usa o compilador! Fique à vontade para testar e comentar.
Um abraço,
Bruno Pinho.