Aula 02: Introdução aos Compiladores
Objetivos
Ao final desta aula, você será capaz de:
- Definir formalmente o conceito de compilador e a propriedade de preservação de semântica.
- Diferenciar modelos de execução: compilação (AOT), interpretação e híbridos (bytecode + JIT).
- Construir e interpretar T-Diagrams (diagramas de lápide) e entender bootstrapping.
- Descrever a anatomia do compilador em front-end e back-end.
Motivação e contexto em Engenharia de Computação
Na prática, compiladores e máquinas virtuais são a infraestrutura que permite:
- Desenvolver firmware portável para várias famílias de microcontroladores.
- Extrair desempenho de arquiteturas modernas (pipelines, caches, múltiplos núcleos).
- Criar linguagens de domínio específico (DSLs) para redes, gráficos, bancos de dados e sistemas embarcados.
Entender compiladores é entender como transformar especificações de alto nível em código executável. Ferramentas como GCC, Clang, JVM, .NET e V8 são, no fundo, pipelines de compilação.
Conteúdo
1. O que é um compilador?
Em essência, um compilador é um tradutor que preserva o significado do programa.
Definição formal. Um compilador \(C\) mapeia um programa \(P_S\) escrito em uma linguagem fonte \(L_S\) para um programa \(P_T\) em uma linguagem alvo \(L_T\):
\[C : L_S \to L_T\]
A propriedade fundamental é a preservação da semântica: para toda entrada válida, o comportamento observável do programa compilado deve ser equivalente ao do programa fonte:
\[\forall \text{ input } \in \text{Inputs}: \quad \text{Exec}(P_S, \text{input}) \equiv \text{Exec}(P_T, \text{input})\]
Ou seja, os resultados e efeitos colaterais (E/S, mudanças de estado) devem coincidir.
Por que compilar?
- Produtividade: linguagens de alto nível são mais expressivas e concisas.
- Portabilidade: o mesmo código fonte pode ser compilado para x86, ARM, RISC-V, etc.
- Otimização: o compilador aplica transformações que melhoram desempenho, tamanho ou consumo de energia de forma sistemática.
2. Modelos de execução
Existem três modelos principais de como o código fonte é executado. O diagrama abaixo resume o fluxo de cada um.
flowchart TB
subgraph AOT [Compilação AOT]
A1[Fonte] --> A2[Compilador]
A2 --> A3[Objeto/Link]
A3 --> A4[Executável]
A4 --> A5[Execução direta]
end
subgraph Interp [Interpretação]
B1[Fonte] --> B2[Intérprete]
B2 --> B3[Execução passo a passo]
end
subgraph JIT [Híbrido Bytecode + JIT]
C1[Fonte] --> C2[Compilador]
C2 --> C3[Bytecode]
C3 --> C4[VM]
C4 --> C5[Interpretação ou JIT]
C5 --> C6[Código nativo]
end
A) Compilação pura (Ahead-of-Time, AOT)
O código é totalmente traduzido antes da execução.
- Exemplos: C, C++, Rust, Go, Haskell.
- Fluxo: Fonte (.c) → Compilador → Objeto (.o) → Linker → Executável → Execução.
- Vantagens: desempenho máximo, detecção de erros em tempo de compilação.
- Desvantagens: tempo de compilação, executável dependente da plataforma.
B) Interpretação pura
Não há geração de código de máquina; um intérprete lê o código fonte (ou uma representação intermediária como AST) e executa as ações em tempo real.
- Exemplos: Bash, versões antigas de PHP/Ruby, Python (modelo conceitual).
- Fluxo: Fonte → Intérprete (ciclo leia–decode–execute).
- Vantagens: flexibilidade (ex.: eval), portabilidade, ciclo rápido de desenvolvimento.
- Desvantagens: overhead de interpretação (pode ser ordens de grandeza mais lento que código nativo).
C) Híbridos (bytecode e JIT)
Modelo muito usado na prática. O código fonte é compilado para uma linguagem intermediária (bytecode); uma máquina virtual executa esse bytecode, podendo ainda compilar trechos “quentes” para código nativo em tempo de execução (JIT).
- Exemplos: Java (JVM), C# (.NET CLR), JavaScript (V8, SpiderMonkey).
- Fluxo: Fonte → Compilador → Bytecode → VM (interpretação + JIT quando necessário) → Código nativo.
- Just-In-Time (JIT): a VM identifica código executado frequentemente e o compila para máquina nativa para ganho de desempenho, mantendo a portabilidade do bytecode.
3. Diagramas de lápide (T-Diagrams)
T-Diagrams são uma notação visual para representar compiladores e intérpretes e para raciocinar sobre cross-compilation e bootstrapping.
Cada “T” tem três partes:
- Topo esquerdo: linguagem fonte (o que o programa recebe).
- Topo direito: linguagem alvo (o que o programa produz).
- Base: linguagem em que o programa está implementado.
flowchart LR
subgraph T1 [Compilador C para ASM em C]
A[ C ] --> B[ ASM ]
C[ escrito em C ]
end
subgraph T2 [Intérprete Python em C]
D[ Python ] --> E[ execução ]
F[ escrito em C ]
end
Exemplo: Um compilador que traduz programas em C para Assembly, ele mesmo escrito em C, é representado por um T com “C” à esquerda, “ASM” à direita e “C” na base.
Bootstrapping: O primeiro compilador de C foi escrito em Assembly (ou outro linguagem já disponível). Depois, o compilador foi reescrito em C e compilado por si mesmo, gerando um novo compilador em C. Esse processo é o “bootstrapping” do compilador.
4. Anatomia do compilador: front-end e back-end
Para fins de estudo e implementação, o compilador é dividido em duas grandes partes:
flowchart LR
subgraph front [Front-end Análise]
F1[Léxico]
F2[Sintático]
F3[Semântico]
F1 --> F2 --> F3
end
subgraph back [Back-end Síntese]
B1[IR]
B2[Otimização]
B3[Codegen]
B1 --> B2 --> B3
end
front --> AST[AST]
AST --> back
Front-end (análise)
Objetivo: entender o programa e verificar correção.
- Léxico: agrupar caracteres em tokens (“as palavras existem?”:
if,while,42). - Sintático: verificar estrutura gramatical e construir a árvore (“a frase faz sentido?”:
if (x > 10) ...). - Semântico: verificar significado (“posso somar int com string?”, “a variável x foi declarada?”).
Saída: AST (Árvore Sintática Abstrata) decorada com tipos e referências.
Back-end (síntese)
Objetivo: gerar código eficiente para a máquina alvo.
- Geração de código intermediário (IR): traduzir a AST para uma representação intermediária (ex.: código de três endereços, LLVM IR).
- Otimização: transformar a IR para melhorar desempenho ou tamanho sem alterar o comportamento observável.
- Geração de código alvo: mapear a IR para assembly ou código de máquina, incluindo alocação de registradores e respeito à ABI.
Do ponto de vista de Engenharia de Computação, é no back-end que decisões sobre registradores, instruções e layout de memória impactam desempenho, consumo de energia e adequação à arquitetura.
Conexões com o projeto Mini-Pascal
No projeto da disciplina, o compilador Mini-Pascal segue o modelo fonte → compilador → bytecode JVM. As etapas do projeto percorrem o pipeline: etapas 2 e 3 (léxico e sintático manual), 4 (ferramentas ANTLR), 5 e 6 (semântica: escopo e tipos), 7 (geração de bytecode). Ao longo do curso, voltaremos a esta visão para justificar decisões de design.
Resumo
- Compilador = tradutor que preserva semântica entre linguagem fonte e alvo.
- AOT, interpretação e híbridos (bytecode + JIT) são os principais modelos de execução.
- T-Diagrams ajudam a representar compiladores, intérpretes e bootstrapping.
- Front-end (léxico, sintático, semântico) e back-end (IR, otimização, codegen) organizam as fases do compilador.
Perguntas de reflexão
- Em que situações você escolheria compilação AOT em vez de interpretação ou modelo híbrido?
- Por que a preservação de semântica é especialmente crítica em compiladores para sistemas de tempo real?
- Como o design da linguagem fonte (abstrações, generics, reflexão) impacta a complexidade do compilador?
Referências
- Aho et al. (2006). Compilers: Principles, Techniques, and Tools. Caps 1.1–1.2.
- Cooper & Torczon (2011). Engineering a Compiler. Cap. 1.
Materiais da aula
Última atualização: 08/03/2026