Você está na página 1de 6

Compiladores

VISÃO GERAL
Compiladores são programas de computador que traduzem um programa escrito em uma linguagem em um
programa escrito em outra linguagem. Ao mesmo tempo, um compilador é um sistema de software de
grande porte, com muitos componentes e algoritmos internos e interações complexas entre eles. Assim, o
estudo da construção de compiladores é uma introdução às técnicas para a tradução e o aperfeiçoamento
de programas.

Conceitos
Compilador é uma ferramenta que traduz software escrito em uma linguagem para outra. Para esta
tradução, a ferramenta deve entender tanto a forma (ou sintaxe) quanto o conteúdo (ou significado) da
linguagem de entrada, e, ainda, as regras que controlam a sintaxe e o significado na linguagem de saída.
Finalmente, precisa de um esquema de mapeamento de conteúdo da linguagem-fonte para a linguagem-
alvo.

A estrutura de um compilador típico deriva dessas observações simples. O compilador tem um front end
para lidar com a linguagem de origem, e um back end para lidar com a linguagem-alvo. Ligando ambos, ele
tem uma estrutura formal para representar o programa num formato intermediário cujo significado é, em
grande parte, independente de qualquer linguagem. Para melhorar a tradução, compiladores muitas vezes
incluem um otimizador, que analisa e reescreve esse formato intermediário.

Programas de computador são simplesmente sequências de operações abstratas escritas em uma linguagem
de programação — linguagem formal projetada para expressar computação. Linguagens de programação
têm propriedades e significados rígidos — ao contrário de linguagens naturais, como chinês ou português —,
e são projetadas visando expressividade, concisão e clareza. Linguagem natural permite ambiguidade.
Linguagens de programação são projetadas para evitá-la; um programa ambíguo não tem significado.

Essas linguagens são projetadas para especificar computações — registrar a sequência de ações que
executam alguma tarefa ou produzem alguns resultados.

Linguagens de programação são, em geral, projetadas para permitir que os seres humanos expressem
computações como sequências de operações. Processadores de computador, a seguir referidos como
processadores, microprocessadores ou máquinas, são projetados para executar sequências de operações. As
operações que um processador implementa estão, quase sempre, em um nível de abstração muito inferior
ao daquelas especificadas em uma linguagem de programação. Por exemplo, a linguagem de programação
normalmente inclui uma forma concisa de imprimir algum número para um arquivo. Esta única declaração
da linguagem deve ser traduzida literalmente em centenas de operações de máquina antes que possa ser
executada.

A ferramenta que executa estas traduções é chamada compilador. O compilador toma como entrada um
programa escrito em alguma linguagem e produz como saída um programa equivalente. Na noção clássica
de um compilador, o programa de saída é expresso nas operações disponíveis em algum processador
específico, muitas vezes chamado máquina-alvo.

Linguagens “fonte” típicas podem ser C, C++, Fortran, Java, ou ML. Linguagem “alvo” geralmente é o
conjunto de instruções de algum processador.
Alguns compiladores produzem um programa-alvo escrito em uma linguagem de programação orientada a
humanos, em vez da assembly de algum computador. Os programas que esses compiladores produzem
requerem ainda tradução antes que possam ser executados diretamente em um computador. Muitos
compiladores de pesquisa produzem programas C como saída. Como existem compiladores para C na
maioria dos computadores, isto torna o programa-alvo executável em todos estes sistemas ao custo de uma
compilação extra para o alvo final. Compiladores que visam linguagens de programação, em vez de conjunto
de instruções de um computador, frequentemente são chamados tradutores fonte a fonte.

Muitos outros sistemas qualificam-se como compiladores. Por exemplo, um programa de composição
tipográfica que produz PostScript pode assim ser considerado. Ele toma como entrada uma especificação de
como o documento aparece na página impressa e produz como saída um arquivo PostScript. PostScript é
simplesmente uma linguagem para descrever imagens. Como este programa assume uma especificação
executável e produz outra também executável, é um compilador.

O código que transforma PostScript em pixels é


normalmente um interpretador, não um compilador.
Um interpretador toma como entrada uma
Máquina Virtual
especificação executável e produz como saída o
É um simulador para um processador, ou
resultado da execução da especificação.
seja, um interpretador para o conjunto de
Algumas linguagens, como Perl, Scheme e APL, são, instruções da máquina.
com mais frequência, implementadas com
interpretadores do que com compiladores.

Algumas linguagens adotam esquemas de tradução


que incluem tanto compilação quanto interpretação. Java é compilada do código-fonte para um formato
denominado bytecode, uma representação compacta que visa diminuir tempos de download para
aplicativos Máquina Virtual Java. Aplicativos Java são executados usando o bytecode na Máquina Virtual Java
(JVM), um interpretador para bytecode. Para complicar ainda mais as coisas, algumas implementações da
JVM incluem um compilador, às vezes chamado de compilador just-in-time, ou JIT, que, em tempo de
execução, traduz sequências de bytecode muito usadas em código nativo para o computador subjacente.

Interpretadores e compiladores têm muito em comum, e executam muitas das mesmas tarefas. Ambos
analisam o programa de entrada e determinam se é ou não um programa válido; constroem um modelo
interno da estrutura e significado do programa; determinam onde armazenar valores durante a execução.
No entanto, interpretar o código para produzir um resultado é bastante diferente de emitir um programa
traduzido que pode ser executado para produzir o resultado. Este livro concentra-se nos problemas que
surgem na construção de compiladores. No entanto, um implementador de interpretadores pode encontrar
a maior parte do material relevante.

Compiladores são programas grandes e complexos, e geralmente incluem centenas de milhares, ou mesmo
milhões, de linhas de código, organizadas em múltiplos subsistemas e componentes. As várias partes de um
compilador interagem de maneira complexa.

Decisões de projeto tomadas para uma parte do compilador têm ramificações importantes para as outras.
Assim, o projeto e a implementação de um compilador são um exercício substancial em engenharia de
software.

Um bom compilador contém um microcosmo da ciência da computação. Faz uso prático de algoritmos
gulosos (alocação de registradores), técnicas de busca heurística (agendamento de lista), algoritmos de
grafos (eliminação de código morto), programação dinâmica (seleção de instruções), autômatos finitos e
autômatos de pilha (análises léxica e sintática) e algoritmos de ponto fixo (análise de fluxo de dados).

Lida com problemas, como alocação dinâmica, sincronização, nomeação, localidade, gerenciamento da
hierarquia de memória e escalonamento de pipeline. Poucos sistemas de software reúnem tantos
componentes complexos e diversificados. Trabalhar dentro de um compilador fornece experiência em
engenharia de software, difícil de se obter com sistemas menores, menos complicados.

Compiladores desempenham papel fundamental na atividade central da ciência da computação: preparar


problemas para serem solucionados por computador. A maior parte do software é compilada, e a exatidão
desse processo e a eficiência do código resultante têm impacto direto sobre nossa capacidade de construir
sistemas de grande porte. A maioria dos estudantes não se contenta em ler sobre essas ideias; muitas delas
devem ser implementadas para que sejam apreciadas. Assim, o estudo da construção de compiladores é
componente importante de um curso de ciência da computação.

Compiladores demonstram a aplicação bem-sucedida da teoria para problemas práticos. As ferramentas que
automatizam a produção de analisadores léxicos (scanners) e analisadores sintáticos (parsers) aplicam
resultados da teoria de linguagem formal. Estas mesmas ferramentas são utilizadas para pesquisa de texto,
filtragem de website, processamento de textos e interpretadores de linguagem de comandos. A verificação
de tipo e a análise estática aplicam resultados das teorias de reticulados e dos números, e outros ramos da
matemática, para compreender e melhorar programas. Geradores de código utilizam algoritmos de
correspondência de padrão de árvore, parsing, programação dinâmica e correspondência de texto para
automatizar a seleção de instruções.

Ainda assim, alguns problemas que surgem na construção de compiladores são problemas abertos — isto é,
as melhores soluções atuais ainda têm espaço para melhorias. Tentativas de projeto de representações de
alto nível, universais e intermediárias esbarram na complexidade. O método dominante para escalonamento
de instruções é um algoritmo guloso com várias camadas de heurística de desempate. Embora seja evidente
que os compiladores devem usar comutatividade e associatividade para melhorar o código, a maioria deles
que tenta fazer isto, simplesmente reorganiza a expressão em alguma ordem canônica.

Construir um compilador bem-sucedido exige conhecimentos em algoritmos, engenharia e planejamento.


Bons compiladores aproximam as soluções para problemas difíceis. Eles enfatizam eficiência em suas
próprias implementações e no código que geram. Têm estrutura de dados interna e representações de
conhecimento que expõem o nível correto de detalhe — suficientes para permitir otimização forte, mas não
para forçar o compilador a “nadar” em detalhes. A construção de compiladores reúne ideias e técnicas de
toda a extensão da ciência da computação, aplicando-as em um ambiente restrito para resolver alguns
problemas verdadeiramente difíceis.

Princípios fundamentais da compilação


Compiladores são objetos grandes, complexos e cuidadosamente projetados. Embora muitos problemas no
seu projeto sejam passíveis de múltiplas soluções e interpretações, há dois princípios fundamentais que um
construtor de compiladores deve ter em mente o tempo todo. O primeiro é inviolável:

O compilador deve preservar o significado do programa a ser compilado. Exatidão é uma questão
fundamental na programação. O compilador deve preservar a exatidão, implementando fielmente o
“significado” de seu programa de entrada. Este princípio está no cerne do contrato social entre o construtor
e o usuário do compilador. Se o compilador puder ter liberdade com o significado, então, por que
simplesmente não gerar um nop ou um return? Se uma
tradução incorreta é aceitável, por que se esforçar para IR
acertá-la? Um compilador usa algum conjunto
O segundo princípio a ser observado é prático: de estruturas de dados para
representar o código que é
O compilador deve melhorar o programa de entrada de processado. Este formato é chamado
alguma forma perceptível. representação intermediária, ou IR
(Intermediate Representation).
Um compilador tradicional melhora o programa de entrada ao
torná-lo executável diretamente em alguma máquina-alvo.
Outros “compiladores” melhoram suas entradas de diferentes maneiras. Por exemplo, tpic é um programa
que toma a especificação para um desenho escrito na linguagem gráfica pic e a converte para LATEX; a
“melhoria” reside na maior disponibilidade e generalidade do LATEX. Um tradutor fonte a fonte para C deve
produzir código que seja, de certa forma, melhor que o programa de entrada; se não for, por que alguém iria
chamá-lo?

ESTRUTURA DO COMPILADOR
Compilador é um grande e complexo sistema de software. A comunidade tem construído compiladores
desde 1955, e, ao longo de todos esses anos, aprendemos muitas lições sobre como estruturá-los.
Retratamos, antes, um compilador como uma caixa simples que traduz um programa-fonte em um
programa-alvo. Na realidade, é claro, isto é mais complexo que este quadro simplificado.

Como o modelo de caixa simples sugere, um compilador deve entender o programa fonte que toma como
entrada e mapear sua funcionalidade para a máquina-alvo. A natureza distinta dessas duas tarefas dá a
entender uma divisão de trabalho e leva a um projeto que decompõe a compilação em duas partes
principais: front end e back end.

O front end concentra-se na compreensão do programa na linguagem-fonte. Já o back end, no mapeamento


de programas para a máquina-alvo. Essa separação de interesses tem várias implicações importantes para o
projeto e a implementação dos compiladores.

O front end deve codificar seu conhecimento do programa fonte em alguma estrutura para ser usada mais
tarde pelo back end. Essa representação intermediária (IR) torna-se a representação definitiva do
compilador para o código que está sendo traduzido. Em cada ponto da compilação, o compilador terá uma
representação definitiva. Ele pode, na verdade, utilizar várias IRs diferentes à medida que a compilação
prossegue, mas, em cada ponto, uma determinada representação será a IR definitiva. Pensamos na IR
definitiva como a versão do programa passada entre fases independentes do compilador, como a IR passada
do front end para o back end no desenho anterior.

Em um compilador de duas fases, o front end deve garantir que o programa-fonte esteja bem formado, e
mapear aquele código para a IR. Já o back end, mapear o programa em IR para o conjunto de instruções e os
recursos finitos da máquina-alvo. Como este último só processa a IR criada pelo front end, pode assumir que
a IR não contém erros sintáticos ou semânticos.

O compilador pode fazer múltiplas passagens pelo formato IR do código antes de emitir o programa-alvo.
Esta capacidade deve levar a um código melhor, pois o compilador pode, desta forma, estudar o código em
uma fase e registrar detalhes relevantes. Depois, em outras fases, pode usar os fatos registrados para
melhorar a qualidade da tradução. Esta estratégia requer que o conhecimento derivado na primeira
passagem seja registrado na IR, onde outras passagens podem encontrá-lo e utilizá-lo.

Finalmente, a estrutura de duas fases pode simplificar o processo de retargeting do compilador. Podemos
facilmente imaginar a construção de vários back ends para um único front end, a fim de produzir
compiladores que aceitam a mesma linguagem, mas visam diferentes máquinas. De modo semelhante,
podemos imaginar front ends para diferentes linguagens produzindo a mesma IR e usando um back end
comum. Ambos os cenários consideram que uma IR pode atender a várias combinações de fonte e alvo; na
prática, tanto os detalhes específicos da linguagem quanto os detalhes específicos da máquina normalmente
encontram seu lugar na IR.

A introdução de uma IR torna possível acrescentar mais fases à compilação. O construtor de compiladores
pode inserir uma terceira fase entre o front end e o back end. Essa seção do meio, ou otimizador, toma um
programa em IR como entrada e produz outro programa em IR semanticamente equivalente como saída.
Usando a IR como interface, o construtor pode inserir esta terceira fase com o mínimo de rompimento entre
o front end e o back end, o que leva à estrutura de compilador a seguir, chamada compilador de três fases.

O otimizador é um transformador IR-para-IR que tenta melhorar o programa em IR de alguma maneira. Ele
pode fazer uma ou mais passagens pela IR, analisá-la e reescrevê-la. Pode, também, reescrever a IR de um
modo que provavelmente produza um programa-alvo mais rápido, ou menor, pelo back end. E, ainda, ter
outros objetivos, como um programa que produz menos faltas de página ou usa menos energia.

Conceitualmente, a estrutura em três fases representa o compilador otimizante clássico. Na prática, cada
fase é dividida internamente em uma série de passos.

O front end consiste em dois ou três passos que tratam dos detalhes do reconhecimento de programas
válidos na linguagem-fonte e da produção do formato IR inicial do programa. A seção do meio contém
passos que realizam diferentes otimizações. A quantidade e a finalidade desses passos variam de um
compilador para outro. O back end consiste em uma série de passos, cada um levando a IR do programa
mais para perto do conjunto de instruções da máquina-alvo. As três fases e seus passos individuais
compartilham uma infraestrutura comum.

Na prática, a divisão conceitual de um compilador em três fases — front end, seção intermediária, ou
otimizador, e back end — é útil. Os problemas resolvidos por essas fases são diferentes. O front end trata do
entendimento do programa-fonte e do registro dos resultados de sua análise na forma de IR. A seção do
otimizador focaliza a melhoria do formato IR. O back end precisa mapear o programa transformado em IR
para os recursos limitados da máquina alvo de um modo que leve ao uso eficiente desses recursos.
Dessas três fases, o otimizador tem a descrição mais obscura. O termo otimização implica que o compilador
descobre uma solução ótima para algum problema. As questões e os problemas que surgem nesta fase são
tão complexos e tão inter-relacionados que não podem, na prática, ser solucionados de forma ótima. Além
do mais, o comportamento real do código compilado depende das interações entre todas as técnicas
aplicadas no otimizador e o back end. Assim, mesmo que uma única técnica possa ser comprovadamente
ótima, suas interações com outras podem produzir resultados que não são ótimos. Como resultado, um bom
compilador otimizante pode melhorar a qualidade do código em relação a uma versão não otimizada, mas
quase sempre deixará de produzir o código ótimo.

A seção intermediária pode ser um único passo monolítico que aplica uma ou mais otimizações para
melhorar o código, ou ser estruturada como uma série de passos menores com cada um lendo e escrevendo
a IR. A estrutura monolítica pode ser mais eficiente. A de múltiplos passos, pode servir como uma
implementação menos complexa e um método mais simples de depurar o compilador. Esta também cria a
flexibilidade para empregar diferentes conjuntos de otimização em diferentes situações. A escolha entre
essas duas técnicas depende das restrições sob as quais o compilador é construído e opera.

Você também pode gostar