🚀 Pipeline de Compilação com LLVM

Da Representação à Execução: O Caminho Completo

Você está prestes a descobrir como transformar sua representação intermediária LLVM IR em programas executáveis reais que rodam nativamente em processadores. Este é o momento onde vemos o poder completo do ecossistema LLVM - uma infraestrutura madura que automatiza tarefas complexas como otimização, geração de código nativo, e linkagem.

O pipeline de compilação LLVM é uma orquestração sofisticada de múltiplas ferramentas especializadas, cada uma focada em uma transformação específica. Compreender este pipeline permite que você aproveite décadas de pesquisa em compiladores sem precisar reimplementar tudo do zero. É como ter um time de especialistas em otimização trabalhando para você!

Nesta jornada, você explorará cada fase do pipeline, desde IR não otimizado até executáveis nativos, descobrindo as ferramentas disponíveis, como configurá-las, e como integrá-las em seu próprio compilador. Prepare-se para completar sua compreensão do ciclo completo de compilação! 🎯


🎯 O Que Você Descobrirá Nesta Jornada

🔧 Ferramentas do Ecossistema

Você conhecerá as principais ferramentas do LLVM e compreenderá o papel de cada uma no pipeline. Descobrirá como usar opt para otimizações, llc para geração de código, llvm-as e llvm-dis para conversão entre formatos, e como orquestrar tudo com clang como driver.

Aprenderá também sobre ferramentas auxiliares essenciais como llvm-link para combinar módulos, lli para interpretação JIT, e llvm-config para configuração de builds. Este conhecimento é fundamental para automatizar seu processo de compilação.

Otimizações em Ação

Dominará o sistema de passes de otimização do LLVM, compreendendo como diferentes níveis de otimização (O0, O1, O2, O3) afetam código gerado. Descobrirá como aplicar passes individuais, criar sequências personalizadas, e medir impacto de otimizações.

Explorará categorias importantes de otimizações incluindo análise de fluxo de dados, transformações de loops, inlining de funções, e eliminação de código morto. Esta compreensão permite que você faça escolhas informadas sobre trade-offs entre tempo de compilação e qualidade de código.


🏗️ Visão Geral do Pipeline

Fases da Compilação

O processo completo de transformar código fonte em executável passa por múltiplas fases, cada uma com responsabilidades bem definidas. Compreender esta separação de responsabilidades é fundamental para aproveitar o pipeline efetivamente.

flowchart TD
    A["Código Fonte<br/>Sua linguagem"] --> B["Frontend<br/>Análise Léxica/Sintática/Semântica"]
    B --> C["AST<br/>Representação Abstrata"]
    C --> D["Gerador IR<br/>Seu código"]
    
    D --> E["LLVM IR<br/>Texto (.ll)"]
    E --> F["Assembler<br/>llvm-as"]
    F --> G["LLVM Bitcode<br/>Binário (.bc)"]
    
    G --> H["Otimizador<br/>opt"]
    H --> I["LLVM IR Otimizado<br/>Bitcode (.bc)"]
    
    I --> J["Compilador<br/>llc"]
    J --> K["Assembly<br/>Nativo (.s)"]
    
    K --> L["Assembler<br/>as"]
    L --> M["Objeto<br/>Nativo (.o)"]
    
    M --> N["Linker<br/>ld"]
    N --> O["Executável<br/>Final"]
    
    P["Bibliotecas<br/>Sistema/Runtime"] --> N

    style A fill:#e3f2fd,stroke:#1976d2
    style C fill:#fff3e0,stroke:#f57c00
    style E fill:#e8f5e9,stroke:#388e3c
    style I fill:#f3e5f5,stroke:#9c27b0
    style K fill:#e1f5fe,stroke:#0288d1
    style O fill:#ffebee,stroke:#d32f2f

    classDef yourCode fill:#ffe0b2,stroke:#ef6c00,stroke-width:3px
    class D yourCode

Zonas de Responsabilidade:

Sua Responsabilidade (Frontend + Gerador IR): Você implementa análise léxica, sintática e semântica para sua linguagem, construindo uma AST válida. Depois, implementa gerador que transforma esta AST em LLVM IR. Esta é toda a parte específica da sua linguagem.

LLVM Cuida do Resto (Middle-end + Backend): Uma vez que você produziu IR válido, LLVM assume. Otimizador transforma IR em versões mais eficientes. Compilador de backend traduz IR para assembly nativo. Assembler e linker produzem executável final. Tudo isso acontece automaticamente.

💡 Separação de Responsabilidades

Esta separação é extremamente poderosa. Você pode focar completamente em semântica da sua linguagem - regras de tipos, escopo, resolução de símbolos - sem se preocupar com detalhes de x86 versus ARM, alocação de registradores, ou scheduling de instruções.

Quando nova arquitetura surge (como RISC-V), você não precisa modificar nada. Quando LLVM adiciona nova otimização sofisticada, seu compilador se beneficia automaticamente. Esta é a magia de uma infraestrutura bem projetada com interfaces limpas.

Formatos Intermediários

LLVM trabalha com dois formatos principais para representação intermediária, cada um com características e usos específicos:

LLVM IR Textual (.ll): Formato legível por humanos que você pode editar em qualquer editor de texto. Ideal para debugging, aprendizado, e inspeção manual. Pode ser facilmente gerado por printf/print statements em seu gerador. Arquivos tendem a ser grandes mas são completamente portáteis.

LLVM Bitcode (.bc): Formato binário compacto otimizado para processamento por ferramentas. Significativamente menor que formato textual. Mais rápido para ler/escrever. Usado internamente pelo pipeline para comunicação entre ferramentas. Não é legível por humanos diretamente.

Conversão entre formatos é trivial:

# Textual para Bitcode
llvm-as programa.ll -o programa.bc

# Bitcode para Textual
llvm-dis programa.bc -o programa.ll

Durante desenvolvimento, trabalhe principalmente com formato textual para facilitar debugging. Em produção, use bitcode para performance.


🔍 Ferramentas Essenciais do LLVM

opt: O Otimizador

opt é o motor de otimizações do LLVM. Recebe IR como entrada e produz IR otimizado como saída, aplicando sequências configuráveis de passes de transformação.

🎛️ Uso Básico do opt

Aplicar nível de otimização padrão:

opt -O2 programa.ll -S -o programa_opt.ll

Flags importantes:

  • -O0: Sem otimizações (útil para debugging)
  • -O1: Otimizações básicas, compilação rápida
  • -O2: Otimizações agressivas, nível recomendado
  • -O3: Otimizações muito agressivas, pode aumentar código
  • -Os: Otimizar para tamanho de código
  • -Oz: Otimizar agressivamente para tamanho
  • -S: Emitir IR textual (sem isso, gera bitcode)

Aplicar passes específicos:

# Constant folding + dead code elimination
opt -constprop -dce programa.ll -S -o programa_opt.ll

# Ver lista de todos os passes disponíveis
opt -print-passes

Visualizar transformações:

opt -O2 -print-after-all programa.ll -S -o programa_opt.ll 2>&1 | less

Isso mostra IR após cada pass, permitindo que você veja exatamente como código é transformado progressivamente.

llc: Compilador de Backend

llc transforma LLVM IR em código assembly nativo para arquitetura alvo. Este é o ponto onde representação independente de arquitetura se torna específica de plataforma.

✅ Compilação Básica

Gerar assembly para arquitetura atual:

llc programa.ll -o programa.s

Especificar arquitetura alvo:

# x86-64
llc -march=x86-64 programa.ll -o programa_x86.s

# ARM 64-bit
llc -march=aarch64 programa.ll -o programa_arm.s

# RISC-V 64-bit
llc -march=riscv64 programa.ll -o programa_riscv.s

Otimizações de backend:

llc -O2 programa.ll -o programa.s

Note que otimizações de llc são diferentes das de opt. Enquanto opt faz otimizações no nível de IR (independente de arquitetura), llc faz otimizações específicas de máquina como scheduling de instruções e alocação de registradores.

lli: Interpretador JIT

lli interpreta ou compila just-in-time LLVM IR, permitindo execução direta sem gerar arquivos intermediários. Extremamente útil para testes rápidos durante desenvolvimento.

# Executar programa diretamente
lli programa.ll

# Passar argumentos de linha de comando
lli programa.ll arg1 arg2 arg3

# Usar interpretação (mais lento, mas útil para debugging)
lli -force-interpreter programa.ll

Vantagens do lli:

  • Ciclo edit-test muito rápido (sem esperar compilação completa)
  • Útil para validar correção de IR rapidamente
  • Pode imprimir informações de debug detalhadas

Limitações:

  • Performance inferior a executáveis nativos
  • Nem todas as features podem funcionar identicamente
  • Dependências de sistema podem causar problemas

⚙️ Passes de Otimização

Categorias de Passes

LLVM organiza otimizações em categorias baseadas em escopo e técnica. Compreender estas categorias ajuda a selecionar otimizações apropriadas e entender trade-offs.

📊 Principais Categorias

Passes de Análise: Não modificam código, apenas coletam informações. Exemplos incluem análise de dominância, análise de alias, e análise de dependências. Outros passes usam estas informações para tomar decisões informadas.

Passes de Transformação: Modificam IR para melhorar performance ou tamanho. Dividem-se em várias subcategorias:

Scalar Optimizations: Operam em nível de instruções individuais. Incluem constant folding, constant propagation, dead code elimination, common subexpression elimination. São rápidas e aplicadas extensivamente.

Interprocedural Passes: Analisam múltiplas funções simultaneamente. Incluem inlining, devirtualization, e análise de escape. Mais caras mas podem encontrar otimizações que passes locais perdem.

Loop Passes: Especializadas em otimizar loops. Incluem loop invariant code motion, loop unrolling, loop vectorization, loop interchange. Extremamente importantes porque loops dominam tempo de execução na maioria dos programas.

Link-Time Optimization (LTO): Aplica otimizações interprocedurais agressivas vendo programa completo após linking. Pode inline através de fronteiras de módulos e eliminar código morto globalmente.

Passes Comuns e Seus Efeitos

Vamos explorar alguns passes importantes em detalhe, com exemplos concretos de transformações:

🔄 Constant Propagation e Folding

Pass: -constprop e -ipsccp

O que faz: Substitui variáveis cujos valores são conhecidos em tempo de compilação pelos valores constantes, e avalia operações em constantes.

Exemplo:

Antes:

define i32 @exemplo() {
  %a = add i32 5, 3
  %b = mul i32 %a, 2
  %c = add i32 %b, 10
  ret i32 %c
}

Depois:

define i32 @exemplo() {
  ret i32 26
}

Toda a computação foi avaliada em tempo de compilação: (5+3)*2+10 = 26.

🗑️ Dead Code Elimination

Pass: -dce (Dead Code Elimination) e -adce (Aggressive DCE)

O que faz: Remove instruções cujos resultados nunca são usados, e blocos básicos que nunca são alcançados.

Exemplo:

Antes:

define i32 @exemplo(i32 %x) {
  %a = add i32 %x, 5
  %b = mul i32 %a, 2       ; nunca usado
  %c = add i32 %x, 10
  ret i32 %c
}

Depois:

define i32 @exemplo(i32 %x) {
  %c = add i32 %x, 10
  ret i32 %c
}

Cálculo de %b foi eliminado porque resultado nunca é utilizado.

🔁 Common Subexpression Elimination

Pass: -early-cse e -gvn (Global Value Numbering)

O que faz: Identifica expressões que são computadas múltiplas vezes com mesmos operandos e elimina cálculos redundantes.

Exemplo:

Antes:

define i32 @exemplo(i32 %x, i32 %y) {
  %a = mul i32 %x, %y
  %b = add i32 %a, 10
  %c = mul i32 %x, %y      ; expressão repetida
  %d = add i32 %c, 20
  ret i32 %d
}

Depois:

define i32 @exemplo(i32 %x, i32 %y) {
  %a = mul i32 %x, %y
  %b = add i32 %a, 10      ; mantido mas não usado
  %d = add i32 %a, 20      ; reusa %a
  ret i32 %d
}

Segunda multiplicação foi eliminada, reutilizando resultado da primeira.

Loop Optimizations

Loops são importantes para performance porque executam repetidamente. LLVM oferece passes sofisticados especializados em loops:

💡 Loop Invariant Code Motion (LICM)

Pass: -licm

O que faz: Move computações que não dependem de índice do loop para fora do loop.

Exemplo:

Antes:

define void @exemplo(i32* %arr, i32 %n, i32 %x, i32 %y) {
entry:
  br label %loop

loop:
  %i = phi i32 [ 0, %entry ], [ %i.next, %loop ]
  %sum = mul i32 %x, %y           ; invariante!
  %addr = getelementptr i32, i32* %arr, i32 %i
  store i32 %sum, i32* %addr
  %i.next = add i32 %i, 1
  %cond = icmp slt i32 %i.next, %n
  br i1 %cond, label %loop, label %exit

exit:
  ret void
}

Depois:

define void @exemplo(i32* %arr, i32 %n, i32 %x, i32 %y) {
entry:
  %sum = mul i32 %x, %y           ; movido para fora!
  br label %loop

loop:
  %i = phi i32 [ 0, %entry ], [ %i.next, %loop ]
  %addr = getelementptr i32, i32* %arr, i32 %i
  store i32 %sum, i32* %addr
  %i.next = add i32 %i, 1
  %cond = icmp slt i32 %i.next, %n
  br i1 %cond, label %loop, label %exit

exit:
  ret void
}

Multiplicação agora executa uma vez em vez de N vezes.

🌀 Loop Unrolling

Pass: -loop-unroll

O que faz: Replica corpo do loop múltiplas vezes, reduzindo overhead de controle e expondo mais oportunidades de otimização.

Exemplo:

Antes:

for (i = 0; i < 4; i++) {
  arr[i] = i * 2;
}

Depois (unroll factor 2):

for (i = 0; i < 4; i += 2) {
  arr[i] = i * 2;
  arr[i+1] = (i+1) * 2;
}

Metade das iterações, mas cada iteração faz trabalho dobrado.

Vantagens:

  • Menos branches (overhead reduzido)
  • Mais instruções independentes (paralelismo de instrução)
  • Melhor aproveitamento de pipeline do processador

Desvantagens:

  • Código maior (cache de instruções pode sofrer)
  • Tempo de compilação aumentado

🎯 Níveis de Otimização

Escolhendo o Nível Apropriado

LLVM oferece níveis de otimização pré-configurados que equilibram tempo de compilação, tamanho de código, e performance:

-O0: Sem Otimizações

  • Compilação mais rápida
  • Debug mais fácil (correspondência direta com código fonte)
  • Performance muito inferior
  • Uso: Desenvolvimento ativo, debugging

-O1: Otimizações Básicas

  • Compilação ainda rápida
  • Otimizações locais simples
  • Melhoria moderada de performance
  • Uso: Desenvolvimento quando O0 é muito lento

-O2: Otimizações Agressivas

  • Tempo de compilação moderado
  • Maioria das otimizações importantes
  • Excelente performance
  • Uso: Releases, nível recomendado

-O3: Otimizações Máximas

  • Compilação mais lenta
  • Pode aumentar tamanho de código
  • Performance ligeiramente melhor que O2
  • Uso: Hot paths, computação intensiva

📏 Otimizações para Tamanho

Além de níveis focados em performance, há opções para minimizar tamanho:

-Os (Optimize for Size): Aplica otimizações que não aumentam código e desabilita aquelas que aumentam significativamente (como loop unrolling agressivo). Útil para sistemas embarcados com memória limitada.

-Oz (Optimize Aggressively for Size): Mais agressivo que -Os, podendo desabilitar otimizações de performance que aumentam código. Apropriado quando tamanho é absolutamente crítico.

Exemplo de uso:

opt -Os programa.ll -S -o programa_small.ll
llc -Os programa_small.ll -o programa_small.s

Medindo Impacto de Otimizações

Para tomar decisões informadas sobre otimizações, você precisa medir objetivamente seu impacto:

Tempo de Compilação:

time opt -O2 programa.ll -o programa_opt.bc

Tamanho de Código:

# Comparar tamanhos de IR
wc -l programa.ll programa_opt.ll

# Comparar tamanhos de executável
ls -lh programa programa_opt

Performance de Execução:

time ./programa_opt < input.txt

⚠️ Armadilhas Comuns

Variabilidade: Execute múltiplas vezes e use mediana. Variações de milissegundos podem ser ruído do sistema.

Efeitos de Cache: Primeira execução pode ser mais lenta. Descarte ou execute warm-up antes de medir.

Input Representativo: Teste com dados realistas. Performance pode variar drasticamente com diferentes inputs.

Comparação Justa: Use mesmas condições (mesma máquina, mesma carga) para todas as medições.


🔗 Linking e Geração de Executável

Do Assembly ao Executável

Após llc gerar assembly nativo, ainda precisamos assemblá-lo em código objeto e linkar com bibliotecas do sistema:

flowchart LR
    A["Assembly<br/>.s"] --> B["Assembler<br/>as/nasm"]
    B --> C["Código Objeto<br/>.o"]
    
    D["Runtime Library<br/>crt0.o, crti.o"] --> E["Linker<br/>ld/lld"]
    F["Bibliotecas Sistema<br/>libc, libm"] --> E
    C --> E
    
    E --> G["Executável<br/>Final"]

    style A fill:#e3f2fd,stroke:#1976d2
    style C fill:#fff3e0,stroke:#f57c00
    style G fill:#e8f5e9,stroke:#388e3c

Processo Manual:

# 1. Gerar assembly
llc -O2 programa.ll -o programa.s

# 2. Assemblá-lo em objeto
as programa.s -o programa.o

# 3. Linkar
ld programa.o -o programa -lc -dynamic-linker /lib64/ld-linux-x86-64.so.2

Este processo manual é educativo mas tedioso. Na prática, use clang como driver.

Usando Clang como Driver

clang orquestra automaticamente todo o pipeline, detectando formato de input e aplicando transformações apropriadas:

🚀 Compilação Simplificada com Clang

Compilar LLVM IR diretamente para executável:

clang programa.ll -o programa

Com otimizações:

clang -O2 programa.ll -o programa

Especificar arquitetura alvo:

clang --target=x86_64-linux-gnu programa.ll -o programa

Ver comandos executados (útil para debugging):

clang -v programa.ll -o programa

Gerar apenas assembly (parar antes de assemblagem):

clang -S programa.ll -o programa.s

Gerar apenas objeto (parar antes de linking):

clang -c programa.ll -o programa.o

🛠️ Integrando LLVM em Seu Compilador

Arquitetura de Integração

Seu compilador deve gerar LLVM IR e então invocar ferramentas LLVM para completar compilação. Há várias estratégias para esta integração:

🏗️ Estratégia 1: Geração de Arquivos

Abordagem: Seu compilador escreve IR textual para arquivo .ll, depois invoca clang ou llc via system calls para processar.

Vantagens:

  • Implementação mais simples
  • IR intermediário pode ser inspecionado facilmente
  • Debugging simplificado

Desvantagens:

  • I/O de arquivos adiciona overhead
  • Múltiplos processos (menos eficiente)
  • Dados intermediários em disco

Implementação:

// Gerar IR
std::ofstream output("programa.ll");
generator.generateIR(ast, output);
output.close();

// Compilar para executável
system("clang -O2 programa.ll -o programa");

🔗 Estratégia 2: API LLVM Direta

Abordagem: Seu compilador linka contra bibliotecas LLVM e usa API C++ diretamente, mantendo tudo em memória.

Vantagens:

  • Performance máxima (sem I/O)
  • Controle fino sobre processo
  • Integração mais profunda

Desvantagens:

  • Curva de aprendizado íngreme
  • Compilador depende de bibliotecas LLVM
  • API pode mudar entre versões

Implementação:

#include "llvm/IR/Module.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/Support/TargetSelect.h"
#include "llvm/Target/TargetMachine.h"

// Inicialização
llvm::InitializeNativeTarget();
llvm::InitializeNativeTargetAsmPrinter();

// Criar módulo
llvm::LLVMContext context;
llvm::Module module("meu_programa", context);
llvm::IRBuilder<> builder(context);

// Gerar IR programaticamente
// ... código de geração ...

// Escrever IR para arquivo
std::error_code EC;
llvm::raw_fd_ostream output("programa.ll", EC);
module.print(output, nullptr);

Configurando Build System

Para usar API LLVM, seu projeto precisa linkar contra bibliotecas apropriadas. LLVM fornece llvm-config para descobrir flags necessários:

# Descobrir flags de compilação
llvm-config --cxxflags

# Descobrir flags de linking
llvm-config --ldflags --libs core support

# Descobrir versão instalada
llvm-config --version

Exemplo de Makefile:

CXX = clang++
CXXFLAGS = $(shell llvm-config --cxxflags)
LDFLAGS = $(shell llvm-config --ldflags)
LIBS = $(shell llvm-config --libs core support)

meu_compilador: main.o codegen.o
    $(CXX) -o $@ $^ $(LDFLAGS) $(LIBS)

%.o: %.cpp
    $(CXX) -c -o $@ $< $(CXXFLAGS)

Exemplo de CMake:

find_package(LLVM REQUIRED CONFIG)

include_directories(${LLVM_INCLUDE_DIRS})
add_definitions(${LLVM_DEFINITIONS})

add_executable(meu_compilador main.cpp codegen.cpp)

llvm_map_components_to_libnames(llvm_libs support core)
target_link_libraries(meu_compilador ${llvm_libs})

🎨 Debugging e Inspeção de IR

Visualizando IR Gerado

Inspecionar IR gerado é fundamental para validar correção e diagnosticar problemas:

💡 Técnicas de Inspeção

1. Ler IR Textual Diretamente:

cat programa.ll | less

2. Ver Transformações Passo a Passo:

opt -O2 -print-after-all programa.ll -S -o programa_opt.ll 2>&1 | less

3. Visualizar Grafo de Fluxo de Controle:

opt -dot-cfg programa.ll
dot -Tpng .main.dot -o cfg.png

4. Ver Assembly Gerado:

llc programa.ll -o programa.s
cat programa.s

5. Comparar Antes e Depois de Otimizações:

diff -u programa.ll programa_opt.ll | less

Verificação de IR

LLVM inclui verificador que detecta IR malformado:

# Verificar módulo
opt -verify programa.ll -o /dev/null

# Se houver erros, eles serão reportados

Erros comuns detectados:

  • Blocos básicos sem terminador
  • Uso de valores não definidos
  • Tipos incompatíveis em operações
  • PHI nodes incorretas
  • Referências a funções não declaradas

⚠️ Debugging IR Inválido

Quando verificador reporta erro, siga este processo:

1. Identifique função/bloco problemático:

Mensagem de erro indica localização. Use editor para navegar até essa parte do IR.

2. Examine contexto:

Olhe instruções anteriores e posteriores. Frequentemente problema é uso de valor que não foi definido ou branch para bloco que não existe.

3. Trace de volta para gerador:

Identifique que parte do seu código gerou IR problemático. Adicione prints de debug para ver que nós da AST estão sendo processados.

4. Corrija e re-verifique:

Após correção, sempre re-verifique. Um erro pode esconder outros.


🚀 Automatizando o Pipeline

Scripts de Build

Para facilitar desenvolvimento, crie scripts que automatizam processo completo:

📝 Script Bash Simples

#!/bin/bash
# compile.sh - Pipeline completo de compilação

set -e  # Parar em qualquer erro

PROGRAM=$1
OPT_LEVEL=${2:-O2}  # Default O2

echo "Compilando $PROGRAM..."

# Gerar LLVM IR
./meu_compilador $PROGRAM -o ${PROGRAM}.ll

echo "Otimizando com -${OPT_LEVEL}..."

# Otimizar
opt -${OPT_LEVEL} ${PROGRAM}.ll -S -o ${PROGRAM}_opt.ll

echo "Gerando executável..."

# Compilar para executável
clang ${PROGRAM}_opt.ll -o ${PROGRAM}.exe

echo "Sucesso! Executável: ${PROGRAM}.exe"

# Cleanup (opcional)
rm ${PROGRAM}.ll ${PROGRAM}_opt.ll

# Executar (opcional)
if [ "$3" == "run" ]; then
    echo "Executando..."
    ./${PROGRAM}.exe
fi

Uso:

./compile.sh meu_programa.src O2 run

Makefile Completo

Para projetos maiores, Makefile oferece gerenciamento de dependências automático:

# Makefile para compilador

CXX = clang++
CXXFLAGS = -std=c++17 -Wall $(shell llvm-config --cxxflags)
LDFLAGS = $(shell llvm-config --ldflags)
LIBS = $(shell llvm-config --libs core support)

# Compilador
COMPILER = meu_compilador
SOURCES = main.cpp lexer.cpp parser.cpp semantic.cpp codegen.cpp
OBJECTS = $(SOURCES:.cpp=.o)

# Programas de teste
TESTS = $(wildcard tests/*.src)
TEST_EXES = $(TESTS:.src=.exe)

all: $(COMPILER)

$(COMPILER): $(OBJECTS)
    $(CXX) -o $@ $^ $(LDFLAGS) $(LIBS)

%.o: %.cpp
    $(CXX) -c -o $@ $< $(CXXFLAGS)

# Compilar programa fonte
%.ll: %.src $(COMPILER)
    ./$(COMPILER) $< -o $@

# Otimizar IR
%_opt.ll: %.ll
    opt -O2 $< -S -o $@

# Gerar executável
%.exe: %_opt.ll
    clang $< -o $@

# Compilar e executar testes
test: $(TEST_EXES)
    @for exe in $(TEST_EXES); do \
        echo "Executando $$exe..."; \
        ./$$exe || exit 1; \
    done
    @echo "Todos os testes passaram!"

clean:
    rm -f $(OBJECTS) $(COMPILER)
    rm -f tests/*.ll tests/*_opt.ll tests/*.exe

.PHONY: all test clean

🎯 Boas Práticas e Recomendações

Durante Desenvolvimento

Checklist de Desenvolvimento

1. Comece Simples:

  • Gere IR textual primeiro (mais fácil de debugar)
  • Use -O0 durante desenvolvimento
  • Teste cada construção isoladamente antes de combinar

2. Valide Sempre:

  • Execute opt -verify após cada geração
  • Compare IR com exemplos gerados por Clang
  • Use lli para testes rápidos

3. Incremente Gradualmente:

  • Implemente features uma por vez
  • Mantenha suite de testes atualizada
  • Documente decisões de design

4. Otimize Quando Necessário:

  • Profile antes de otimizar
  • Meça impacto objetivamente
  • Considere trade-offs de tempo de compilação

Para Produção

Quando seu compilador estiver maduro, considere estas melhorias:

Performance de Compilação:

  • Cache IR gerado quando possível
  • Paralelizar compilação de múltiplos arquivos
  • Use bitcode em vez de textual para comunicação entre fases

Qualidade de Código:

  • Ative LTO para builds de release
  • Use -O3 para código crítico
  • Considere Profile-Guided Optimization (PGO) para hot paths

Experiência do Usuário:

  • Forneça mensagens de erro claras
  • Ofereça flags para controlar otimizações
  • Documente options de linha de comando

📚 Recursos Adicionais

📖 Documentação Essencial

LLVM Documentation:

Tutoriais Práticos:

Comunidade: