🔄 Tradução Sistemática: De AST para LLVM IR

A Ponte Entre Abstração e Execução

Chegamos ao momento essencial onde teoria encontra prática de forma sistemática. Você dominou interpretação de ASTs e compreendeu a arquitetura do LLVM IR. Agora precisamos construir a ponte que conecta esses dois mundos - um processo de tradução metódico que transforma cada construção de alto nível em sua representação executável equivalente.

Este não é um processo mágico ou ad-hoc. É uma transformação sistemática e composicional, onde cada tipo de nó da AST possui um padrão de tradução bem definido. Compreender esses padrões permite construir geradores de código robustos e extensíveis que podem crescer naturalmente conforme sua linguagem evolui.

✨ O Poder da Sistematização

A beleza da geração de código para LLVM IR está na sua natureza composicional. Cada construção linguística - expressão aritmética, declaração de variável, estrutura de controle - mapeia para um padrão específico de instruções IR. Ao dominar esses padrões individuais, você pode combiná-los livremente para traduzir programas completos de qualquer complexidade.

Esta abordagem modular significa que adicionar suporte para novas construções linguísticas é questão de definir novos padrões de tradução, sem precisar reescrever geradores existentes. É como ter um conjunto de blocos de construção que se encaixam perfeitamente.


🎯 Estratégia Geral de Tradução

Princípios Fundamentais

Antes de mergulhar em casos específicos, precisamos estabelecer princípios que guiarão todo nosso processo de tradução. Estes princípios não são arbitrários - emergem naturalmente da natureza da AST e das características do LLVM IR.

O primeiro princípio fundamental é a tradução recursiva composicional. A AST é hierárquica por natureza - expressões contêm subexpressões, comandos contêm subcomandos. Nossa tradução deve respeitar e aproveitar esta estrutura. Para traduzir um nó composto, primeiro traduzimos recursivamente seus filhos, depois combinamos os resultados usando o padrão apropriado para o nó pai.

🏗️ Composicionalidade em Ação

Considere tradução de expressão a + b * c. A AST tem raiz de adição com filho esquerdo sendo referência à variável a e filho direito sendo multiplicação de b por c.

Para gerar IR, não processamos esta expressão monoliticamente. Primeiro traduzimos recursivamente filho esquerdo, obtendo instrução que carrega valor de a. Depois traduzimos recursivamente filho direito, o que requer traduzir b e c separadamente, depois emitir multiplicação. Finalmente, emitimos instrução de adição que combina resultados das traduções dos filhos.

Esta abordagem funciona porque LLVM IR também é composicional - instruções produzem valores que podem ser consumidos por outras instruções, formando grafos de dependência que naturalmente refletem estrutura hierárquica da AST.

O segundo princípio é o gerenciamento explícito de contexto. Durante tradução, precisamos rastrear informações que não estão diretamente presentes na AST mas são necessárias para gerar IR correto. Isto inclui mapeamento de variáveis para aloca instructions, informações de tipo, labels de blocos básicos para controle de fluxo, e estado de funções atuais.

O terceiro princípio é geração de IR em forma SSA. LLVM IR usa Static Single Assignment, onde cada variável é atribuída exatamente uma vez. Isto simplifica otimizações mas complica tradução de variáveis mutáveis. Usamos instruções alloca para criar células de memória e instruções load/store para ler e escrever valores, permitindo múltiplas atribuições lógicas enquanto mantemos forma SSA no nível de registradores virtuais.

Estrutura do Gerador de Código

Um gerador de código completo organiza-se em camadas que refletem estrutura da linguagem fonte e do IR alvo. Cada camada tem responsabilidades bem definidas e interfaces claras.

A camada de módulo gerencia estrutura de mais alto nível - declarações de funções globais, variáveis globais, definições de tipo. Esta camada inicializa módulo LLVM, configura tripla de arquitetura alvo, e coordena tradução de declarações top-level.

A camada de função traduz corpo de funções individuais. Para cada função na AST, cria função correspondente no IR, aloca espaço para parâmetros e variáveis locais usando instruções alloca, e coordena tradução de comandos que formam corpo da função. Esta camada gerencia também o builder que emite instruções e rastreia bloco básico atual.

A camada de comando implementa traduções para diferentes tipos de comandos - declarações, atribuições, estruturas de controle, retornos. Cada tipo de comando tem padrão de tradução específico que combina emissão de instruções IR com chamadas recursivas para traduzir subcomponentes.

A camada de expressão traduz expressões para sequências de instruções que computam valores. Esta é tipicamente a camada mais recursiva, pois expressões frequentemente contêm subexpressões profundamente aninhadas. Cada operador aritmético, lógico, ou relacional mapeia para instruções específicas do IR.

graph TB
    A[Gerador de Código] --> B[Camada de Módulo]
    A --> C[Camada de Função]
    A --> D[Camada de Comando]
    A --> E[Camada de Expressão]
    
    B --> F[Declarações Globais]
    B --> G[Definições de Tipo]
    
    C --> H[Criação de Funções]
    C --> I[Alocação de Variáveis]
    C --> J[Tradução de Corpo]
    
    D --> K[Atribuições]
    D --> L[Controle de Fluxo]
    D --> M[Retornos]
    
    E --> N[Operadores Binários]
    E --> O[Operadores Unários]
    E --> P[Chamadas de Função]
    E --> Q[Literais]

    style A fill:#e3f2fd,stroke:#1976d2,stroke-width:3px
    style B fill:#fff3e0,stroke:#f57c00
    style C fill:#e8f5e9,stroke:#388e3c
    style D fill:#f3e5f5,stroke:#7b1fa2
    style E fill:#fce4ec,stroke:#c2185b


🔢 Tradução de Expressões: Fundamentos

Expressões Literais e Variáveis

Começamos com os casos mais simples - literais e referências a variáveis. Estes formam as folhas da árvore de expressões e servem como base para traduções mais complexas.

Literais numéricos inteiros traduzem-se diretamente para constantes LLVM. O tipo exato depende do sistema de tipos da linguagem fonte - inteiros de 32 bits tornam-se i32, inteiros de 64 bits tornam-se i64. O valor da constante é simplesmente o valor do literal. Para um literal como 42, o IR gerado é simplesmente a representação de valor constante que é usado diretamente onde necessário.

Literais de ponto flutuante seguem padrão similar, mas usam tipos float ou double. Literais booleanos em linguagens de alto nível tipicamente mapeiam para inteiros de 1 bit no IR, onde true vira i1 1 e false vira i1 0.

Referências a variáveis são mais sutis. Lembre-se que variáveis em linguagens de alto nível são mutáveis - podem ser atribuídas múltiplas vezes. Mas LLVM IR usa forma SSA onde cada registrador virtual é atribuído exatamente uma vez. Resolvemos esta impedância usando instruções alloca para criar células de memória e load para ler valores.

Quando declaramos variável, criamos espaço na pilha usando alloca. O IR gerado para declaração int x = 42; seria:

%x = alloca i32              ; Aloca espaço para x
store i32 42, i32* %x        ; Armazena valor inicial

Quando referenciamos variável em expressão, carregamos seu valor atual. Para referência como x em expressão, geramos:

%1 = load i32, i32* %x       ; Carrega valor atual de x

🎯 Por Que Alloca + Load/Store?

Você pode se perguntar: por que não simplesmente mapear cada variável para registrador virtual diferente a cada atribuição, mantendo SSA puro? A resposta está em duas considerações:

Simplicidade de geração: Com alloca/load/store, não precisamos nos preocupar com forma SSA durante geração inicial. Cada atribuição é simples store, cada uso é simples load. LLVM tem passe de otimização chamado mem2reg que automaticamente converte aloca’s para registradores SSA puros quando possível.

Endereçamento: Algumas operações requerem endereços de variáveis - passar por referência, operador address-of. Com alloca, cada variável tem endereço real que pode ser passado. Registradores SSA puros não têm endereços.

Esta abordagem é padrão para front-ends LLVM - gerar IR ingênuo mas correto, deixar otimizador limpá-lo.

Operadores Aritméticos Binários

Operadores aritméticos mapeiam diretamente para instruções aritméticas do IR. Para cada operador da linguagem fonte, há instrução correspondente - frequentemente várias, dependendo de tipos dos operandos. Tradução de operadores binários segue padrão consistente: gerar recursivamente código para operando esquerdo, gerar recursivamente código para operando direito, e emitir instrução apropriada que combina os dois valores.

Para expressão a + b * c onde todas variáveis são inteiros:

; Carrega valores das variáveis
%1 = load i32, i32* %a
%2 = load i32, i32* %b
%3 = load i32, i32* %c

; Multiplica b * c
%multmp = mul i32 %2, %3

; Soma a + (b * c)
%addtmp = add i32 %1, %multmp

A ordem das instruções reflete naturalmente avaliação recursiva da AST. Operadores para ponto flutuante usam instruções diferentes com prefixo f, como fadd double %4, %5.

Operadores de Comparação

Comparações produzem valores booleanos (i1 no IR) e usam instruções icmp (integer compare) ou fcmp (floating-point compare). Para expressão x < 10:

%1 = load i32, i32* %x
%icmptmp = icmp slt i32 %1, 10    ; signed less than

Note o predicado slt (signed less than) versus alternativa ult (unsigned less than). Escolha correta depende se tipo é signed ou unsigned.

Operadores Lógicos

Operadores lógicos booleanos requerem atenção especial devido à avaliação em curto-circuito em muitas linguagens. Para negação lógica, usamos simples XOR com true:

%1 = load i1, i1* %flag
%nottmp = xor i1 %1, true

Para AND lógico com avaliação em curto-circuito, criamos estrutura de controle de fluxo:

entry:
  %1 = load i1, i1* %a
  br i1 %1, label %and.rhs, label %and.end

and.rhs:
  %2 = load i1, i1* %b
  br label %and.end

and.end:
  %andtmp = phi i1 [ false, %entry ], [ %2, %and.rhs ]

Se não quisermos avaliação em curto-circuito, podemos usar simples and bitwise.


🏗️ Tradução de Comandos: Construindo Comportamento

Declarações de Variáveis

Uma declaração simples sem inicializador aloca espaço mas deixa conteúdo indefinido. Para declaração com inicializador, adicionamos store após gerar código para o valor inicial.

Otimização importante: Instruções alloca devem ser emitidas no bloco de entrada da função, não no ponto de declaração léxica. Isto permite que otimizador mem2reg funcione efetivamente, convertendo alocas para registradores SSA puros quando possível.

Atribuições

Atribuições avaliam expressão do lado direito e armazenam resultado no local da variável. Para x = y + 1:

%1 = load i32, i32* %y
%addtmp = add i32 %1, 1
store i32 %addtmp, i32* %x

Operadores de atribuição composta (+=, -=) são açúcar sintático que combina operação binária com atribuição. Para x += 5:

%1 = load i32, i32* %x
%addtmp = add i32 %1, 5
store i32 %addtmp, i32* %x

Estruturas Condicionais

Condicionais requerem criação de blocos básicos e instruções de branch condicional. Para código:

if (x > 0) {
    y = 1;
} else {
    y = -1;
}

O IR gerado é:

entry:
  %1 = load i32, i32* %x
  %cmptmp = icmp sgt i32 %1, 0
  br i1 %cmptmp, label %if.then, label %if.else

if.then:
  store i32 1, i32* %y
  br label %if.end

if.else:
  store i32 -1, i32* %y
  br label %if.end

if.end:
  ; Continua execução...

🎯 Observação sobre Blocos Vazios

Note que sempre criamos bloco de merge (if.end) mesmo que não haja código subsequente. Isto mantém IR bem-formado - todo bloco básico precisa de sucessor ou ser terminado por return/unreachable. O bloco de merge serve como ponto de convergência do fluxo de controle. Se condição for constante conhecida em tempo de compilação, otimizador LLVM eliminará branches e blocos não alcançáveis automaticamente.

Estruturas de Repetição

Loops são mais complexos pois requerem múltiplos blocos básicos e back edges no grafo de fluxo de controle. Para loop while:

while (i < 10) {
    sum = sum + i;
    i = i + 1;
}

Gera:

entry:
  br label %while.cond

while.cond:
  %1 = load i32, i32* %i
  %cmptmp = icmp slt i32 %1, 10
  br i1 %cmptmp, label %while.body, label %while.end

while.body:
  %2 = load i32, i32* %sum
  %3 = load i32, i32* %i
  %addtmp1 = add i32 %2, %3
  store i32 %addtmp1, i32* %sum
  
  %4 = load i32, i32* %i
  %addtmp2 = add i32 %4, 1
  store i32 %addtmp2, i32* %i
  
  br label %while.cond

while.end:
  ; Continua...

Break e Continue usam pilha de contexto de loops para saltar para blocos apropriados.


🎯 Tradução de Funções

Declaração e Definição de Funções

Funções no IR consistem em assinatura (tipo de retorno e parâmetros) e corpo opcional. Para função:

function add(x: int, y: int) -> int {
    return x + y;
}

Gera:

define i32 @add(i32 %x, i32 %y) {
entry:
  %x.addr = alloca i32
  %y.addr = alloca i32
  store i32 %x, i32* %x.addr
  store i32 %y, i32* %y.addr
  
  %1 = load i32, i32* %x.addr
  %2 = load i32, i32* %y.addr
  %addtmp = add i32 %1, %2
  ret i32 %addtmp
}

Note que mesmo parâmetros são alocados na pilha. Isto permite que sejam modificados no corpo da função e simplifica geração. Otimizador mem2reg eliminará estas allocas desnecessárias.

Chamadas de Função

Chamadas geram código para argumentos, depois emitem instrução call. Para result = add(5, 10):

%calltmp = call i32 @add(i32 5, i32 10)
store i32 %calltmp, i32* %result

Return Statements

Retornos são simples mas precisam verificar tipo antes de emitir instrução ret.


🎨 Exemplo Completo: Tradução End-to-End

Vamos consolidar tudo com exemplo completo mostrando tradução de programa inteiro. Código fonte:

function factorial(n: int) -> int {
    if (n <= 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

function main() -> int {
    var result: int = factorial(5);
    return result;
}

IR gerado:

; Função factorial
define i32 @factorial(i32 %n) {
entry:
  %n.addr = alloca i32
  store i32 %n, i32* %n.addr
  
  %1 = load i32, i32* %n.addr
  %cmptmp = icmp sle i32 %1, 1
  br i1 %cmptmp, label %if.then, label %if.else

if.then:
  ret i32 1

if.else:
  %2 = load i32, i32* %n.addr
  %3 = load i32, i32* %n.addr
  %subtmp = sub i32 %3, 1
  %calltmp = call i32 @factorial(i32 %subtmp)
  %multmp = mul i32 %2, %calltmp
  ret i32 %multmp
}

; Função main
define i32 @main() {
entry:
  %result = alloca i32
  %calltmp = call i32 @factorial(i32 5)
  store i32 %calltmp, i32* %result
  
  %1 = load i32, i32* %result
  ret i32 %1
}

Após otimizações com opt -O2, o IR torna-se muito mais eficiente, eliminando todas allocas desnecessárias, movendo operações, e marcando call recursiva como tail call.


🎓 Reflexões e Boas Práticas

Separação de Concerns

Mantenha geração de código modular com tradutor de tipos separado, gerenciador de símbolos, emitter de instruções, e validador. Esta organização facilita manutenção e extensão.

Tratamento de Erros

Geração de código pode falhar por várias razões. Implemente tratamento de erros robusto com informação de localização para facilitar debugging.

Testes

Teste gerador de código sistematicamente com testes unitários para cada padrão de tradução, testes de integração para programas completos, testes de execução rodando IR compilado, e testes de otimização.


🎯 Conclusão: Da Teoria à Prática

Você completou uma jornada extraordinária através da tradução sistemática de ASTs para LLVM IR. O que começou como estruturas abstratas representando programas transformou-se em sequências concretas de instruções que computadores podem executar com eficiência máxima.

Os padrões de tradução que você dominou refletem décadas de pesquisa em compiladores e representam escolhas cuidadosas sobre como mapear semântica de alto nível para execução de baixo nível.

✨ O Poder da Sistematização

A beleza desta abordagem está em sua composicionalidade e extensibilidade. Você construiu conjunto de blocos fundamentais que podem ser combinados livremente para suportar linguagens arbitrariamente complexas. LLVM fornece infraestrutura robusta, testada em milhões de linhas de código de produção. Ferramentas como opt e llc transformam seu IR ingênuo mas correto em código de máquina extremamente otimizado!

Em seu projeto integrador, você agora tem todas as ferramentas para transformar programas escritos em sua linguagem em executáveis reais. Esta capacidade - materializar ideias abstratas em software executável - é uma das habilidades mais valiosas em ciência da computação! 🚀

🔬 Casos Avançados e Otimizações

Conversões de Tipo

Muitas linguagens permitem conversões implícitas ou explícitas entre tipos. No IR, precisamos emitir instruções de conversão apropriadas. Para integer para integer, usamos extensão (sext para signed, zext para unsigned) ou truncamento (trunc). Para integer para float, usamos sitofp (signed) ou uitofp (unsigned). Para float para integer, usamos fptosi ou fptoui. Para float para float, usamos fpext (extensão) ou fptrunc (truncamento).

Arrays e Indexação

Arrays introduzem complexidade adicional. Precisamos calcular endereços de elementos e gerenciar layouts de memória. Arrays são tipicamente representados como alloca de múltiplos elementos. A indexação usa GEP (GetElementPtr) para calcular endereço do elemento.

Para arr[i] = 42:

%arrayptr = alloca [10 x i32]
%1 = load i32, i32* %i
%arrayidx = getelementptr [10 x i32], [10 x i32]* %arrayptr, i32 0, i32 %1
store i32 42, i32* %arrayidx

Structs e Acesso a Campos

Structs requerem definição de tipos e acesso via GEP com índices constantes. Primeiro definimos o tipo struct com vetores de tipos de campos. Depois usamos GEP com índice constante para acessar campos específicos.

Strings

Strings são tipicamente representadas como arrays de caracteres (i8) com terminador nulo. Criamos global constant para string, depois retornamos ponteiro para primeiro caractere. String "Hello" vira:

@.str = private unnamed_addr constant [6 x i8] c"Hello\00"

; Uso:
%strptr = getelementptr [6 x i8], [6 x i8]* @.str, i32 0, i32 0

Otimização de Expressões Constantes

Durante geração, podemos detectar expressões totalmente constantes e avalá-las em tempo de compilação usando constant folding. LLVM faz isto automaticamente durante otimização, mas podemos fazer explicitamente para melhorar qualidade do IR inicial.

Geração de Código de Depuração

Para facilitar debugging, podemos emitir metadata de debug que mapeia instruções IR para linhas de código fonte. Isto permite debuggers como GDB mostrarem código fonte original enquanto executam IR compilado, tornando a experiência de desenvolvimento muito mais produtiva.


🔧 Integração com Runtime

Funções Externas

Muitas linguagens precisam chamar funções de runtime - I/O, alocação de memória, etc. Declaramos estas como funções externas. Por exemplo, para printf, declaramos função com tipo variádico que retorna i32 e aceita i8* como primeiro parâmetro.

Para usar printf:

declare i32 @printf(i8*, ...)

define void @print_int(i32 %value) {
  %fmt = getelementptr [4 x i8], [4 x i8]* @.fmt_int, i32 0, i32 0
  call i32 (i8*, ...) @printf(i8* %fmt, i32 %value)
  ret void
}

@.fmt_int = private constant [4 x i8] c"%d\0A\00"

Similar para malloc/free para alocação dinâmica.


🎯 Padrões de Tradução Sistemática

Mapeamento Direto vs Transformação

Há dois estilos principais de tradução. O mapeamento direto traduz cada nó AST para sequência fixa de instruções IR. É simples mas pode gerar código redundante. A transformação primeiro transforma AST para representação intermediária mais próxima do IR, depois traduz. É mais complexo mas produz código melhor.

Para projetos educacionais, mapeamento direto é adequado. Para compiladores de produção, transformação é preferível.

Gerenciamento de Escopo

Precisamos rastrear variáveis em escopos aninhados. Uma pilha de mapas (nome → alloca) funciona bem. Ao entrar em novo escopo, empilha mapa vazio. Ao sair, desempilha. Busca procura do topo para base.

Verificação de Invariantes

Durante geração, verifique invariantes constantemente. Todo bloco básico deve ter terminador. Tipos devem bater. Variáveis devem estar declaradas. Funções devem ter número correto de argumentos. Estas verificações pegam bugs cedo e facilitam debugging.


🎨 Padrões de Implementação

Builder Pattern

Use builder para emitir instruções. Mantém referência a bloco básico atual. Fornece métodos convenientes como CreateAdd, CreateLoad, CreateBr. Abstrai detalhes de baixo nível. Facilita mudanças.

Visitor Pattern

Implemente gerador como visitor sobre AST. Cada tipo de nó tem método visit específico. Navegação recursiva fica encapsulada. Adicionar novos tipos de nó é simples.

Two-Pass Generation

Para linguagens com declarações forward, use duas passadas. Primeira passada coleta todas declarações de tipos e funções. Segunda passada gera corpos. Resolve dependências circulares.


🔍 Debugging de IR

Verificação

LLVM fornece verifyModule e verifyFunction que checam correção de IR. Execute após geração para pegar erros. Mensagens indicam exatamente o problema.

Visualização

Use opt -dot-cfg para gerar grafos de fluxo de controle. Ajuda entender estrutura gerada. Identifica problemas visualmente.

Testes

Crie suite de testes com programas pequenos. Compare IR gerado com esperado. Use FileCheck da LLVM para fazer matching de padrões.


💡 Otimizações Durante Geração

Constant Folding Simples

Avalie operações em constantes durante geração. 2 + 3 vira constante 5 diretamente. Reduz tamanho do IR. Facilita otimizações posteriores.

Dead Code Elimination

Não gere código após return/break. Bloco é unreachable. Otimizador eliminará depois, mas melhor não gerar.

Peephole Optimizations

Combine sequências comuns de instruções. Load seguido de store na mesma local pode ser eliminado. Verifique padrões durante geração.


🎯 Exemplo Prático Completo

Vamos ver tradução completa de programa mais complexo com loops, arrays e funções. Considere programa que calcula soma de array:

function sum_array(arr: int[], n: int) -> int {
    var total: int = 0;
    var i: int = 0;
    
    while (i < n) {
        total = total + arr[i];
        i = i + 1;
    }
    
    return total;
}

O IR gerado incluiria: declaração de função com tipos corretos, bloco de entrada com allocas para parâmetros e variáveis locais, inicialização de total e i, loop while com blocos para condição e corpo, acesso a array usando GEP, atualização de variáveis, e return final.

Este exemplo mostra como padrões individuais se combinam naturalmente para suportar programas complexos.


💻 Implementação Prática em Três Linguagens

Para consolidar seu aprendizado, vamos ver como implementar os padrões de tradução em três linguagens diferentes. Cada uma oferece perspectivas únicas sobre o processo.

Exemplo em C++: Tradutor de Expressões Completo

C++ com a API LLVM oferece interface orientada a objetos rica. Vamos implementar tradutor completo de expressões:

class ExpressionCodeGen {
private:
    llvm::LLVMContext& context;
    llvm::IRBuilder<>& builder;
    llvm::Module* module;
    std::map<std::string, llvm::Value*>& namedValues;
    
public:
    ExpressionCodeGen(llvm::LLVMContext& ctx, llvm::IRBuilder<>& b,
                      llvm::Module* m, std::map<std::string, llvm::Value*>& nv)
        : context(ctx), builder(b), module(m), namedValues(nv) {}
    
    llvm::Value* codegen(ExprNode* expr) {
        if (auto* lit = dynamic_cast<IntLiteralNode*>(expr)) {
            return llvm::ConstantInt::get(
                llvm::Type::getInt32Ty(context), 
                lit->getValue()
            );
        }
        
        if (auto* varRef = dynamic_cast<VarRefNode*>(expr)) {
            llvm::Value* varPtr = namedValues[varRef->getName()];
            if (!varPtr) {
                throw std::runtime_error("Variável não declarada");
            }
            return builder.CreateLoad(
                llvm::Type::getInt32Ty(context),
                varPtr,
                varRef->getName()
            );
        }
        
        if (auto* binOp = dynamic_cast<BinaryOpNode*>(expr)) {
            llvm::Value* left = codegen(binOp->getLeft());
            llvm::Value* right = codegen(binOp->getRight());
            
            switch (binOp->getOp()) {
                case BinOp::Add:
                    return builder.CreateAdd(left, right, "addtmp");
                case BinOp::Sub:
                    return builder.CreateSub(left, right, "subtmp");
                case BinOp::Mul:
                    return builder.CreateMul(left, right, "multmp");
                case BinOp::Div:
                    return builder.CreateSDiv(left, right, "divtmp");
            }
        }
        
        throw std::runtime_error("Tipo de expressão não suportado");
    }
};

Esta implementação mostra claramente o padrão recursivo. Cada tipo de nó é tratado separadamente. Operações compostas chamam recursivamente para subexpressões.

Exemplo em Dart: Gerador de Comandos

Dart oferece sintaxe moderna e suporte a programação funcional. Vamos implementar gerador de comandos:

class StatementCodeGen {
  final llvm.Context context;
  final llvm.IRBuilder builder;
  final llvm.Module module;
  final Map<String, llvm.Value> namedValues;
  
  StatementCodeGen(this.context, this.builder, this.module, this.namedValues);
  
  void generate(StatementNode stmt) {
    if (stmt is VarDeclNode) {
      _generateVarDecl(stmt);
    } else if (stmt is AssignmentNode) {
      _generateAssignment(stmt);
    } else if (stmt is WhileNode) {
      _generateWhile(stmt);
    } else if (stmt is IfNode) {
      _generateIf(stmt);
    }
  }
  
  void _generateVarDecl(VarDeclNode node) {
    final alloca = builder.createAlloca(
      translateType(node.type),
      null,
      node.name
    );
    
    namedValues[node.name] = alloca;
    
    if (node.hasInitializer) {
      final initValue = ExpressionCodeGen(
        context, builder, module, namedValues
      ).generate(node.initializer);
      
      builder.createStore(initValue, alloca);
    }
  }
  
  void _generateAssignment(AssignmentNode node) {
    final value = ExpressionCodeGen(
      context, builder, module, namedValues
    ).generate(node.value);
    
    final varPtr = namedValues[node.varName];
    if (varPtr == null) {
      throw Exception('Variável não declarada: ${node.varName}');
    }
    
    builder.createStore(value, varPtr);
  }
  
  void _generateWhile(WhileNode node) {
    final function = builder.insertBlock.parent;
    
    final condBB = llvm.BasicBlock.create(context, 'while.cond', function);
    final bodyBB = llvm.BasicBlock.create(context, 'while.body');
    final afterBB = llvm.BasicBlock.create(context, 'while.end');
    
    builder.createBr(condBB);
    
    builder.setInsertPoint(condBB);
    final condition = ExpressionCodeGen(
      context, builder, module, namedValues
    ).generate(node.condition);
    builder.createCondBr(condition, bodyBB, afterBB);
    
    function.appendBasicBlock(bodyBB);
    builder.setInsertPoint(bodyBB);
    generate(node.body);
    builder.createBr(condBB);
    
    function.appendBasicBlock(afterBB);
    builder.setInsertPoint(afterBB);
  }
  
  void _generateIf(IfNode node) {
    final function = builder.insertBlock.parent;
    
    final thenBB = llvm.BasicBlock.create(context, 'if.then', function);
    final elseBB = llvm.BasicBlock.create(context, 'if.else');
    final mergeBB = llvm.BasicBlock.create(context, 'if.end');
    
    final condition = ExpressionCodeGen(
      context, builder, module, namedValues
    ).generate(node.condition);
    
    if (node.hasElse) {
      builder.createCondBr(condition, thenBB, elseBB);
    } else {
      builder.createCondBr(condition, thenBB, mergeBB);
    }
    
    builder.setInsertPoint(thenBB);
    generate(node.thenBranch);
    builder.createBr(mergeBB);
    
    if (node.hasElse) {
      function.appendBasicBlock(elseBB);
      builder.setInsertPoint(elseBB);
      generate(node.elseBranch);
      builder.createBr(mergeBB);
    }
    
    function.appendBasicBlock(mergeBB);
    builder.setInsertPoint(mergeBB);
  }
}

A versão Dart mostra como linguagens modernas facilitam organização de código. Métodos privados separam lógica de cada tipo de comando. Pattern matching com is torna dispatch limpo.

Exemplo em C: Gerador de Funções

C com LLVM-C API oferece controle máximo e desempenho. Vamos implementar gerador de funções:

typedef struct {
    LLVMContextRef context;
    LLVMBuilderRef builder;
    LLVMModuleRef module;
    HashMap* named_values;  // nome -> LLVMValueRef
} CodeGenContext;

LLVMValueRef generate_function(CodeGenContext* ctx, FunctionNode* node) {
    // Coleta tipos de parâmetros
    LLVMTypeRef* param_types = malloc(node->num_params * sizeof(LLVMTypeRef));
    for (int i = 0; i < node->num_params; i++) {
        param_types[i] = translate_type(ctx, node->params[i]->type);
    }
    
    // Cria tipo de função
    LLVMTypeRef return_type = translate_type(ctx, node->return_type);
    LLVMTypeRef func_type = LLVMFunctionType(
        return_type,
        param_types,
        node->num_params,
        0  // não variádica
    );
    
    // Cria função no módulo
    LLVMValueRef function = LLVMAddFunction(
        ctx->module,
        node->name,
        func_type
    );
    
    // Define nomes de parâmetros
    for (int i = 0; i < node->num_params; i++) {
        LLVMValueRef param = LLVMGetParam(function, i);
        LLVMSetValueName(param, node->params[i]->name);
    }
    
    // Se há corpo, gera
    if (node->has_body) {
        generate_function_body(ctx, function, node);
    }
    
    free(param_types);
    return function;
}

void generate_function_body(
    CodeGenContext* ctx,
    LLVMValueRef function,
    FunctionNode* node
) {
    // Cria bloco de entrada
    LLVMBasicBlockRef entry_bb = LLVMAppendBasicBlockInContext(
        ctx->context,
        function,
        "entry"
    );
    LLVMPositionBuilderAtEnd(ctx->builder, entry_bb);
    
    // Limpa tabela de variáveis
    hashmap_clear(ctx->named_values);
    
    // Aloca parâmetros
    for (int i = 0; i < node->num_params; i++) {
        LLVMValueRef param = LLVMGetParam(function, i);
        const char* param_name = LLVMGetValueName(param);
        
        LLVMValueRef alloca = create_entry_block_alloca(
            ctx,
            function,
            LLVMTypeOf(param),
            param_name
        );
        
        LLVMBuildStore(ctx->builder, param, alloca);
        hashmap_set(ctx->named_values, param_name, alloca);
    }
    
    // Gera corpo
    for (int i = 0; i < node->num_statements; i++) {
        generate_statement(ctx, node->statements[i]);
    }
    
    // Adiciona return void se necessário
    LLVMTypeRef ret_type = LLVMGetReturnType(func_type);
    if (LLVMGetTypeKind(ret_type) == LLVMVoidTypeKind) {
        if (!block_has_terminator(LLVMGetInsertBlock(ctx->builder))) {
            LLVMBuildRetVoid(ctx->builder);
        }
    }
    
    // Verifica função
    char* error = NULL;
    if (LLVMVerifyFunction(function, LLVMPrintMessageAction)) {
        fprintf(stderr, "Erro ao verificar função %s\n", node->name);
    }
}

LLVMValueRef create_entry_block_alloca(
    CodeGenContext* ctx,
    LLVMValueRef function,
    LLVMTypeRef type,
    const char* name
) {
    LLVMBasicBlockRef entry = LLVMGetEntryBasicBlock(function);
    LLVMValueRef first_inst = LLVMGetFirstInstruction(entry);
    
    LLVMBuilderRef tmp_builder = LLVMCreateBuilderInContext(ctx->context);
    
    if (first_inst) {
        LLVMPositionBuilderBefore(tmp_builder, first_inst);
    } else {
        LLVMPositionBuilderAtEnd(tmp_builder, entry);
    }
    
    LLVMValueRef alloca = LLVMBuildAlloca(tmp_builder, type, name);
    
    LLVMDisposeBuilder(tmp_builder);
    return alloca;
}

A versão C mostra controle direto sobre memória e estruturas. Requer gerenciamento manual de recursos mas oferece máxima eficiência. É ideal para projetos que exigem performance absoluta.


🔄 Comparação Entre Abordagens

Cada linguagem tem trade-offs:

C++: Rica API orientada a objetos. RAII gerencia recursos automaticamente. Templates permitem código genérico. Melhor para projetos grandes e complexos.

Dart: Sintaxe moderna e limpa. Null safety evita bugs. Pattern matching simplifica dispatch. Melhor para prototipagem e projetos educacionais.

C: Controle total e máxima eficiência. Sem overhead de abstrações. Portabilidade máxima. Melhor para sistemas embarcados e projetos de alta performance.

Escolha depende de requisitos do projeto e preferências pessoais. O importante é dominar os conceitos fundamentais - eles transcendem linguagens específicas.


🎨 Exercícios Práticos

Para consolidar aprendizado, implemente os seguintes exercícios:

Exercício 1: Implemente tradução de operador ternário (cond ? true_val : false_val). Use PHI nodes para combinar resultados.

Exercício 2: Adicione suporte para arrays multidimensionais. Calcule offsets corretamente usando GEP aninhados.

Exercício 3: Implemente short-circuit evaluation para operadores lógicos || e &&. Compare performance com versão bitwise.

Exercício 4: Adicione geração de metadata de debug. Teste com GDB para verificar que debugging funciona.

Exercício 5: Implemente constant folding durante geração. Compare tamanho de IR com e sem otimização.

Estes exercícios cobrem aspectos importantes não detalhados no material principal. Completá-los dará compreensão profunda do processo de geração de código.


🎓 Conclusão Final

A tradução sistemática de AST para LLVM IR é arte e ciência. Requer compreensão profunda tanto da linguagem fonte quanto do IR alvo. Mas seguindo padrões estabelecidos e princípios composicionais, podemos construir geradores robustos e extensíveis.

O mais importante é começar simples. Implemente casos básicos primeiro. Teste extensivamente. Adicione complexidade gradualmente. Use otimizador LLVM para limpar código gerado. Não tente gerar IR perfeito inicialmente.

Com prática, você desenvolverá intuição para padrões eficientes. Aprenderá quais construções geram bom código. Reconhecerá oportunidades de otimização. E mais importante, terá capacidade de materializar qualquer linguagem que imaginar em código executável real.

Esta é uma habilidade profundamente valiosa e satisfatória. Boa sorte em sua jornada de construção de compiladores! 🚀