Aula 02: Introdução aos Compiladores

Data de Publicação

18/03/2026

Data de Modificação

08/03/2026

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?

  1. Produtividade: linguagens de alto nível são mais expressivas e concisas.
  2. Portabilidade: o mesmo código fonte pode ser compilado para x86, ARM, RISC-V, etc.
  3. 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.

  1. Léxico: agrupar caracteres em tokens (“as palavras existem?”: if, while, 42).
  2. Sintático: verificar estrutura gramatical e construir a árvore (“a frase faz sentido?”: if (x > 10) ...).
  3. 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.

  1. 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).
  2. Otimização: transformar a IR para melhorar desempenho ou tamanho sem alterar o comportamento observável.
  3. 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

De volta ao topo