4

Etapas do Processo de Compilação

Introdução4.1

Nos capítulos anteriores, discutimos em alto nível o funcionamento do processador e o interesse do compilador nesse ecossistema. Estabelecemos que o objetivo central é transformar uma linguagem de alto nível em uma linguagem de baixo nível, a qual será convertida em binários pelo montador. Neste capítulo, aprofundaremos essa discussão detalhando como o processo de compilação é formalmente estruturado e quais responsabilidades recaem sobre cada uma de suas etapas.

Processo de Compilação4.2

O processo de compilação é estruturado em fases distintas que operam em sequência. De maneira ampla, concebemos o compilador como um sistema que recebe um programa fonte e o converte para um programa objeto. Este programa objeto atua como uma representação intermediária que, posteriormente, será unida às bibliotecas compartilhadas do sistema para formar o executável final. O diagrama a seguir resume essa visão macroscópica do sistema de conversão.

Flowchartcluster_processProcessoBCompiladorCPrograma objetoB->CAPrograma fonteA->B

Nesse contexto, é relevante traçar um paralelo com o papel do interpretador. Ao longo da disciplina, veremos que o interpretador compartilha muitas similaridades técnicas com o compilador. A distinção fundamental reside no fato de que o interpretador recebe o código fonte juntamente com as entradas do usuário e executa as instruções imediatamente, em vez de gerar um novo programa para realizar essa tarefa.

Flowchartcluster_processProcessoIInterpretadorSsaídaI->SAprograma fonteA->IEentradaE->I

Para fins de estudo e implementação, essa visão monolítica do compilador é fragmentada em duas grandes áreas, as etapas de análise, conhecidas como front-end, e as etapas de síntese ou geração, denominadas back-end.

Nas disciplinas de construção de compiladores, o foco recai predominantemente sobre o front-end. Esta fase compreende, nesta ordem precisa, a Análise Léxica, a Análise Sintática e a Análise Semântica. Já as etapas de back-end, que abordaremos de forma mais sucinta, encarregam-se da otimização do código gerado e da emissão do código de máquina (seja binário ou assembly). A reutilização de back-ends existentes, como o do LLVM, é uma prática comum na indústria, pois permite que desenvolvedores de novas linguagens evitem o esforço hercúleo de suportar múltiplas arquiteturas de hardware, aproveitando ferramentas validadas por décadas de uso.

Front-end4.2.1

As etapas iniciais, ou front-end, são responsáveis pela análise do código fonte. O objetivo é transformar um texto bruto em uma estrutura de dados rica, capaz de representar o programa de forma que facilite sua manipulação subsequente.

O processo inicia-se com o processamento de uma sequência de caracteres e culmina na geração de uma Representação Intermediária (IR). O fluxo abaixo evidencia a ordem de execução e os artefatos produzidos em cada passo.

CompilerFlowTSTabela de SímbolosINCódigoALAnalisador LéxicoIN->ALFTTokensAL->FTASAnalisador SintáticoFT->ASArS1Árvore de SintaxeArS1:sw->AS:neASeAnalisador SemânticoArS1->ASeArS2Árvore de SintaxeASe->ArS2GCIIRArS2->GCI

Analisador Léxico4.2.1.1

A primeira etapa é a análise léxica, que converte o código fonte (um fluxo de caracteres sem significado inerente) em tokens. Os tokens são blocos de texto classificados com um significado específico para o compilador. Fazendo uma analogia com a língua portuguesa, essa etapa equivaleria à análise morfológica, onde classificamos palavras como substantivos, adjetivos ou preposições.

Considere o trecho de código em C abaixo, que calcula uma posição final baseada em uma posição inicial, uma taxa de variação e um tempo.

position = initial + rate * 60;

Ao ser processado pelo analisador léxico, esse código resulta em uma sequência de tokens. Identificamos elementos como identificadores (nomes de variáveis), operadores (atribuição, matemáticos) e literais numéricos. Na aula específica sobre analisadores léxicos, construiremos uma ferramenta capaz de realizar essa classificação.

LexicalAnalysissrc_rowposition=initial+rate*60tk1<ID, "position">src_row:p1->tk1tk2<OP, assign>src_row:p2->tk2tk3<ID, "initial">src_row:p3->tk3tk4<OP, add>src_row:p4->tk4tk5<ID, "rate">src_row:p5->tk5tk6<OP, *>src_row:p6->tk6tk7<INT, 60>src_row:p7->tk7

É importante notar que, embora o lexema "position" seja identificado, manipular strings constantemente é computacionalmente custoso. Para mitigar isso, utilizamos uma tabela de símbolos. Essa estrutura armazena os nomes das variáveis e strings literais, permitindo que o compilador trabalhe com referências numéricas leves em vez de textos completos.

Analisador Sintático4.2.1.2

Com os tokens identificados, o próximo passo é compreender como eles se organizam. O analisador sintático verifica a estrutura formada pelos tokens. Retomando a analogia com a linguagem natural, se a análise léxica identifica as classes das palavras, a análise sintática identifica o sujeito e o predicado da oração.

Em programação, isso significa reconhecer estruturas gramaticais, como perceber que uma expressão entre parênteses seguida por um bloco de comandos constitui uma instrução if. A disposição dos tokens revela agrupamentos hierárquicos que dão forma ao programa.

O produto dessa etapa é a Árvore de Sintaxe Abstrata (Abstract Syntax Tree - AST). Esta é, sem dúvida, a estrutura de dados mais crítica do compilador. Abaixo, visualizamos a AST para o código exemplificado anteriormente:

AST error: name 'hashlib' is not defined

Estudaremos essa estrutura profundamente nos próximos capítulos. Observe que os nós internos representam operações ou estruturas sintáticas, enquanto os nós folhas contêm os valores ou identificadores. Embora o exemplo mostre uma árvore binária, na prática, os nós da AST podem ter múltiplos filhos, adequando-se à complexidade da instrução representada.

É igualmente relevante mencionar como o analisador lida com falhas. Em vez de abortar a compilação ao primeiro sinal de erro, compiladores robustos implementam estratégias de recuperação, sendo o "Modo Pânico" a mais comum. Nessa abordagem, ao encontrar uma sintaxe inválida, o analisador descarta os tokens seguintes até alcançar um ponto de sincronização seguro, como um ponto e vírgula ou o fechamento de chaves. Isso permite que o processo continue, identificando e reportando múltiplos erros em uma única passagem, o que agiliza significativamente o ciclo de correção por parte do programador.

Analisador Semântico4.2.1.3

Mesmo que uma frase seja gramaticalmente correta, ela pode não fazer sentido. O analisador semântico verifica restrições que as etapas anteriores ignoraram, com destaque para a tipagem das variáveis. É nesta fase que garantimos, por exemplo, que não estamos tentando somar um número a uma string, ou, caso a linguagem permita, que as conversões implícitas necessárias sejam inseridas.

Além da tipagem, o analisador semântico valida escopos. Ele confirma se as variáveis e funções utilizadas foram devidamente declaradas no contexto em que aparecem. Enquanto o analisador sintático aceitaria a soma de duas variáveis quaisquer, o semântico rejeitaria a operação caso uma delas não existisse ou fosse de um tipo incompatível.

Estrutura de dados

Estrutura Vale ressaltar que a análise semântica continua operando sobre a AST. As verificações e anotações são feitas diretamente nessa estrutura, que servirá de base até as etapas finais de geração de código.

Back-end4.2.2

Uma vez que temos uma AST válida, sintática e semanticamente, o foco muda para a eficiência e a tradução final. O back-end realiza otimizações na árvore para melhorar o desempenho e, finalmente, gera o código alvo. Devido à complexidade de otimizar para arquiteturas modernas, muitas vezes delegamos essa tarefa a ferramentas como o LLVM. No entanto, nesta disciplina, utilizaremos uma máquina virtual simplificada para explorar esses conceitos de forma didática.

O diagrama a seguir ilustra o fluxo final, partindo da Representação Intermediária (IR).

VerticalBackendTSTabela de SímbolosRI1IR (Entrada)OIMOtimizador GeralRI1->OIMRI2IR (Otimizada)OIM->RI2GCGerador de CódigoRI2->GCCMA1Código Máquina AlvoGC->CMA1ODMOtimizador EspecíficoCMA1->ODMCMA2Código FinalODM->CMA2

Otimização4.2.2.1

Utilizando a AST, podemos identificar padrões que permitem simplificar o código. Vamos examinar um caso simples de otimização conhecido como Constant Folding (dobra de constantes). Imagine a seguinte árvore:

AST error: name 'hashlib' is not defined

Observe que o nó de multiplicação possui dois números inteiros literais como filhos (20 e 60). Como esses valores são constantes, o resultado da operação será invariavelmente 1200. Calcular isso em tempo de execução seria um desperdício de ciclos de CPU. O compilador pode, portanto, antecipar esse cálculo e substituir a subárvore pelo resultado:

AST error: name 'hashlib' is not defined

Ao realizar essa substituição, o compilador reduz o número de operações que o processador precisará executar, otimizando o programa final sem alterar sua lógica. Embora este seja um exemplo elementar, ele ilustra o princípio geral de buscar eficiência antes da geração do código final.

Geração de código4.2.2.2

A etapa final consiste em percorrer a árvore sintática otimizada e emitir as instruções correspondentes para a arquitetura alvo. Geralmente, utilizamos um padrão de projeto conhecido como Visitor, que navega pelos nós da árvore e gera o código equivalente para cada estrutura encontrada. Nesta disciplina, a geração será direcionada para a nossa máquina virtual educacional, permitindo uma compreensão clara do processo sem a complexidade excessiva de uma CPU física moderna.

Questões4.3

  1. Utilizando a analogia com a linguagem natural apresentada no texto (análise morfológica vs. análise gramatical), explique a diferença fundamental entre as responsabilidades do analisador léxico e do analisador sintático além disso, diga o que cada um produz como saída.

  2. O analisador sintático é capaz de confirmar que a expressão numero + texto está estruturalmente correta (um operador somando dois identificadores). No entanto, isso não garante que o programa esteja correto. Explique como o analisador semântico atua nesse cenário e cite outro tipo de verificação crítica que é responsabilidade exclusiva desta etapa.

  3. Explique por que é vantajoso para um compilador utilizar estratégias como o "Modo Pânico" em vez de interromper o processo ao encontrar o primeiro erro sintático. Como essa estratégia utiliza "pontos de sincronização" para retomar a análise?

  4. Considere a expressão int x = 2 _ 3 + a;. Descreva como o processo de otimização via Constant Folding alteraria a Árvore de Sintaxe Abstrata (AST) dessa expressão antes da geração de código e explique qual é o benefício direto dessa alteração para o programa final.

Próximos passos4.4

No próximo capítulo, iniciaremos nossa imersão técnica pelo front-end do compilador focando na Introdução à análise léxica. Estudaremos como a teoria dos autômatos finitos serve de alicerce para ferramentas consagradas na indústria, como o _**Lex*** e o Flex. Contudo, não nos limitaremos à teoria, pois colocaremos a mão na massa para projetar e implementar o código do nosso próprio analisador léxico, dando o primeiro passo concreto na construção da nossa linguagem.