O que é um compilador

Um compilador é um programa que transforma código fonte escrito por humanos em código de máquina que o processador consegue executar. Embora pareça simples, esse processo envolve várias etapas bem definidas, organizadas em um pipeline:

  1. Frontend (parsing): lê o código fonte, verifica a sintaxe e constrói uma estrutura interna chamada AST (Abstract Syntax Tree).
  2. Representação intermediária (IR): a AST é convertida em uma forma intermediária, independente da linguagem de origem e da arquitetura alvo.
  3. Otimizações: a IR passa por diversas transformações que melhoram a performance sem alterar o comportamento do programa.
  4. Backend (code generation): a IR otimizada é traduzida em instruções nativas da arquitetura alvo (x86, ARM, RISC-V, etc.).
  5. Binário: o resultado final é um executável ou biblioteca que roda diretamente no hardware.

Essa separação em etapas permite que diferentes linguagens compartilhem o mesmo backend e as mesmas otimizações, desde que gerem a mesma IR. Esse princípio modular é exatamente o que o LLVM explora.


O que é LLVM

LLVM (originalmente Low Level Virtual Machine, embora o nome hoje seja tratado como uma marca própria) é uma coleção de módulos e ferramentas para criação de compiladores. Mais do que um compilador específico, é uma infraestrutura de compiladores (compiler infrastructure) projetada para ser reutilizável e extensível.

O projeto nasceu em 2004 na Universidade de Illinois, como tese de doutorado de Chris Lattner, sob orientação de Vikram Adve. Desde o início, o LLVM foi baseado no conceito de SSA (Static Single Assignment), uma forma de representar programas onde cada variável é atribuída exatamente uma vez, o que simplifica drasticamente a análise e a otimização de código.


Componentes do LLVM

O ecossistema LLVM vai muito além de um único compilador. Seus principais componentes incluem:

  • LLVM Core: as bibliotecas fundamentais que definem a IR, o sistema de tipos, o otimizador e os backends de geração de código para diversas arquiteturas.
  • Clang: o compilador de C, C++ e Objective-C construído sobre o LLVM. Conhecido por mensagens de erro claras e tempos de compilação competitivos.
  • LLDB: debugger de próxima geração que substitui o GDB em muitos fluxos de trabalho, especialmente no ecossistema Apple.
  • MLIR (Multi-Level Intermediate Representation): framework para definir e otimizar representações intermediárias em diferentes níveis de abstração, muito usado em compiladores de machine learning.
  • Polly: otimizador baseado em poliedros, focado em transformações avançadas de loops e paralelismo automático.
  • OpenMP: implementação de suporte a paralelização via diretivas OpenMP dentro do ecossistema LLVM/Clang.

Essa modularidade é o que torna o LLVM tão popular: você pode usar apenas as partes que precisa para construir o seu próprio compilador ou ferramenta de análise.


LLVM IR: o coração do projeto

A representação intermediária (IR) é o componente central que conecta frontends a backends. Pense nela como um assembly universal: legível por humanos, independente da arquitetura alvo, e rica o suficiente para permitir otimizações sofisticadas.

Um exemplo simples de LLVM IR para uma função que soma dois inteiros:

define i32 @soma(i32 %a, i32 %b) {
entry:
  %result = add i32 %a, %b
  ret i32 %result
}

Alguns pontos a observar:

  • i32 indica um inteiro de 32 bits.
  • %a, %b e %result são registradores virtuais no formato SSA (cada um recebe valor uma única vez).
  • A função é declarada de forma explícita, com tipos em todas as posições.

A IR pode existir em três formas: texto legível (.ll), bitcode binário (.bc) e representação em memória durante a compilação. Essa flexibilidade permite que ferramentas diferentes consumam e produzam IR em qualquer estágio do pipeline.


Kaleidoscope: aprendendo compiladores na prática

Kaleidoscope é uma linguagem de brinquedo criada para fins didáticos no tutorial oficial do LLVM. Ela é propositalmente simples, elegante e visualmente clara, com apenas um tipo de dado: números de ponto flutuante de 64 bits (double).

Veja um exemplo que calcula o enésimo número de Fibonacci:

# Calcula o enésimo número de Fibonacci
def fib(x)
  if x < 3 then
    1
  else
    fib(x-1) + fib(x-2)

fib(40)

Apesar da simplicidade, implementar um compilador para Kaleidoscope ensina os fundamentos completos de construção de compiladores:

  • Lexing: transformar uma sequência de caracteres em tokens (palavras-chave, identificadores, operadores).
  • Parsing: organizar os tokens em uma árvore de sintaxe abstrata (AST) que representa a estrutura do programa.
  • AST: a estrutura de dados central que conecta o frontend ao restante do pipeline.
  • Code generation com LLVM: percorrer a AST e emitir chamadas à API do LLVM para gerar IR correspondente.
  • JIT compilation: compilar e executar código em tempo real usando o motor JIT do LLVM, sem precisar gerar um executável em disco.

O tutorial guia o leitor passo a passo, do lexer mais básico até a adição de estruturas de controle, variáveis mutáveis e otimizações. Ao final, você tem um compilador funcional com JIT que roda em poucas centenas de linhas de C++.


Por que isso importa

Entender compiladores não é apenas uma curiosidade acadêmica. O LLVM está presente em projetos que milhões de desenvolvedores usam diariamente:

  • Rust usa o LLVM como backend do rustc, o que permite que o compilador de Rust gere código nativo altamente otimizado para dezenas de arquiteturas.
  • Swift foi projetado desde o início em torno do LLVM, também por Chris Lattner.
  • Chromium e o motor V8 do JavaScript exploram técnicas de compilação JIT que compartilham conceitos com o LLVM.
  • Diversas linguagens como Julia, Kotlin Native e Zig também utilizam o LLVM em seus pipelines de compilação.

Compreender como compiladores funcionam ajuda a escrever código melhor: você entende por que certas construções são mais eficientes, como o compilador otimiza loops, por que inlining importa, e o que acontece entre o return que você escreve e a instrução ret que o processador executa.

Além disso, a modularidade do LLVM abriu as portas para inovação em áreas como compiladores de GPU, DSLs (Domain-Specific Languages) e até compilação de modelos de machine learning com ferramentas como o MLIR.


Referências