CompiladoresVisão Geral da Compilação
Compiladores vs Interpretadores
10 min de leitura · 21ª mais cobrada em Compiladores
Um compilador traduz o programa-fonte por completo antes da primeira execução, gerando um artefato independente (código de máquina ou objeto). Um interpretador lê o fonte e o executa passo a passo, sem produzir esse artefato separado. A distinção fixa quando a tradução ocorre e afeta desempenho, detecção de erros e distribuição do programa.
A organização interna do compilador (léxico, sintaxe, semântica e o restante do pipeline) está em Fases do Compilador. Nesta página o assunto é só o modelo de execução: traduz-depois-roda versus interpreta-ao-correr.
Compilador
O compilador consome o texto-fonte e emite um executável que o sistema operacional pode rodar sozinho, sem o compilador presente.
Traduzir uma vez e reutilizar o binário evita repetir análise sintática e semântica a cada execução. Daí a vantagem de desempenho dos programas compilados. Na distribuição, basta enviar o artefato: quem roda não precisa da ferramenta de tradução.
Em C, o trecho
int main() {
return 0;
}
vira programa.exe (ou equivalente). O sistema operacional dispara esse arquivo; o compilador já terminou o trabalho.
Interpretador
O interpretador analisa e executa o fonte no mesmo fluxo, em geral linha a linha ou bloco a bloco, sem gerar executável autônomo. Não há .exe intermediário: cada linha é lida e executada na hora. Quem roda precisa do interpretador instalado (ou embutido), porque não existe artefato separado para o sistema operacional executar sozinho.
O mesmo print(2 + 3) em Python não produz um binário para o SO: a ferramenta lê o comando, avalia a soma e imprime. Fechar o interpretador e “rodar de novo” é reler o fonte.
Linguagem e modelo de implementação são noções distintas. C costuma ser compilado e Python interpretado, mas isso é hábito de ferramenta — existe interpretador de C e existem compiladores de Python. Trabalhamos com o par /.
Por que dois modelos
A tradução de um programa não depende da entrada que vai processar: depende só do texto-fonte. O compilador congela esse trabalho num executável e, daí em diante, cada execução é só consumindo dados. O interpretador recusa esse congelamento: mistura tradução e execução, então cada corrida paga de novo o custo de entender o fonte — em troca de não exigir um passo de geração de binário e de poder começar a rodar assim que a primeira linha estiver pronta.
Por exemplo, o fonte print(2 + 3). No modelo compilado, a ferramenta lê o arquivo inteiro, gera um executável, e só então o sistema operacional roda esse arquivo: a tela mostra 5, e o compilador já saiu de cena. No modelo interpretado, a ferramenta lê print, avalia , imprime 5 e termina — sem gerar arquivo executável no meio.
Compilar antes vs interpretar na hora
O compilador faz tradução antecipada (ahead-of-time): produz um binário que pode rodar muitas vezes sem nova tradução. O interpretador integra tradução e execução: o fonte é processado conforme a execução avança.
Programas compilados tendem a ser mais rápidos na execução; interpretados pagam o custo da tradução a cada trecho executado. Um laço que soma mil vezes ilustra a diferença: traduz o for uma vez e só acumula; reanalisa o corpo a cada volta. Implementações híbridas compilam trechos sob demanda: JIT (just-in-time) gera código nativo durante a execução, como em muitas VMs de Java e em motores JavaScript.
Formalização
Seja o programa fonte, o compilador e o interpretador. O compilador realiza a transformação:
onde é o programa executável. A execução subsequente é expressa por:
sendo o resultado do programa. No caso do interpretador, o processo é dado por:
onde a interpretação e execução ocorrem simultaneamente. Não confunda com : é a ferramenta que produz o executável; é o que o sistema operacional dispara. No interpretador não há para nomear.
Lidas da esquerda para a direita, as setas deixam o invariante explícito: termina em , e só então vem ; vai de a num único passo. O produto é o que permite distribuir e reexecutar sem levar junto.
O mesmo programa nos dois modelos
O fonte abaixo só soma e imprime. Serve para rastrear quando a tradução acontece, sem mudar o resultado .
x = 2
y = 3
print(x + y)
lê as três linhas de uma vez e produz . Nenhuma soma ocorre ainda: não imprime 5, porque tradução não é execução. Só depois o sistema operacional dispara . materializa , , avalia a soma e imprime 5. Uma segunda corrida reutiliza o mesmo : não há nova tradução, só nova execução.
não emite . Lê x = 2 e guarda ; lê y = 3 e guarda ; chega em print(x + y), avalia e imprime 5. Tradução e execução andam no mesmo fluxo: não há um intervalo em que o programa “já está traduzido” e ainda não rodou. Rodar de novo é reler o fonte do zero.
Nos dois casos é 5. O que muda é o artefato e o instante da tradução: paga o custo antes da primeira execução e o amortiza nas seguintes; paga durante cada execução e dispensa o passo de gerar binário. O modelo muda quando se traduz; o valor da soma é o mesmo.
Detecção de erros
Compiladores rejeitam erros de sintaxe e semântica na compilação, antes de rodar. Interpretadores só encontram muitos erros em tempo de execução, quando o fluxo alcança a linha problemática. A diferença está em quando o erro aparece.
Fonte com ; faltando:
int x = 1
analisa o arquivo inteiro. A declaração malformada impede : não há executável, logo não há . O erro aparece antes de qualquer corrida, mesmo que aquela linha nunca fosse “usada” com uma entrada particular. Corrigir o ; é pré-condição para existir .
Fonte com sintaxe válida, mas nome usado fora do ramo que o define:
if False:
x = 1
print(x)
aceita o arquivo: não há a recusar, porque não há passo . Executa o if; a condição é falsa, então x = 1 não roda. Ao interpretar print(x), não está definido e a execução falha com erro de tempo de execução. O mesmo fonte, com print(x) dentro de outro if False:, terminaria sem reclamar: nunca alcançaria a linha. O erro está amarrado ao caminho, não ao arquivo inteiro.
Um compilador de C, diante de um uso de variável sem declaração, recusaria na tradução — o ramo morto não salva o programa, porque olha o texto todo antes de qualquer fluxo. Texto que aceita ainda pode falhar quando roda, ou terminar com um que não resolve o problema.
O que fica fora de
decide se o texto é um programa da linguagem e, se for, emite . Três falhas não entram nessa decisão, porque não são propriedade do texto.
Erro de tempo de execução. A falha só existe quando já consome dados: índice além do fim do vetor, arquivo ausente, memória esgotada. já terminou; quem encontra o problema é a execução .
Aritmética em valor dinâmico. A operação pode ser legal no texto e ainda assim falhar com os números daquela corrida. não substitui os operandos pelos dados de entrada: não divide, não testa zero, não simula estouro. O valor que ainda não está escrito em não é julgado na tradução.
Resultado errado em relação ao problema. compila, termina e produz , mas não é a resposta que o problema pedia. Isso é erro de lógica. conhece a gramática e as regras estáticas da linguagem; não conhece o enunciado, então não compara com a resposta esperada.
O domínio de é o texto , não o par (entrada, especificação). O mesmo vale para quanto à lógica: o interpretador também não compara com o enunciado, e uma divisão por zero só aparece se aquela corrida avaliar o operador.
Duas chamadas da mesma função separam a aritmética dinâmica do resultado errado:
int media(int a, int b) {
return a / b;
}
Dois parâmetros int e um return com divisão. A operação está na gramática e os operandos têm o mesmo tipo. emite . Não há número no fonte para a nem para b, então a tradução não executa conta alguma.
A chamada media(8, 0) pede . O zero chega como argumento. é que encontra a divisão por zero; já tinha terminado. Em return a / 0 o zero já está em , e a tradução pode acusá-lo. Em return a / b o operando ainda não tem valor quando lê o fonte.
A chamada media(2, 4), com divisão inteira, devolve . A média de e é . produziu um resultado e acabou; não é a média que o problema pedia. Para a linguagem, o corpo é uma divisão válida: não lê o identificador media como especificação. Errar a conta em relação ao enunciado fica fora de .
Em Resumo
Compilador traduz antes: e só então ; interpretador junta os dois em .
O binário amortiza a tradução nas corridas seguintes; o interpretador paga a cada trecho; JIT gera código nativo durante a execução.
Sintaxe e semântica estáticas são recusadas na compilação; no interpretador, muitos erros só aparecem no caminho que a execução alcança.
Erro de tempo de execução, aritmética sobre valor que só existe na corrida e resultado errado em relação ao problema ficam, em geral, fora de .