Tem um assunto que as gerações novas de dev parecem ter esquecido: o que um compilador realmente faz.
A maioria hoje vive num modo “piloto automático”: roda um comando no terminal e, por magia, a linguagem “executa”, vira um .exe, ou sobe um servidor HTTP. A abstração vai ficando cada vez mais alta — e aí a gente tenta “otimizar a cagada” quando a aplicação começa a morrer em carga.
O resultado é previsível: virou padrão falar de escalabilidade horizontal antes de encarar o que realmente importa: gargalos locais, alocação, cópia, cache, branch, syscalls e I/O. Muita gente prefere adicionar instâncias do que reduzir desperdício.
Recentemente eu escrevi sobre um benchmark que fiz na minha linguagem: usei ponteiros no lugar de cópias no heap, tirei o garbage collector do caminho e, com mudanças que parecem “apenas conceitos do Rust”, eu vi um salto de 70k requests/seg para perto de 1 milhão na mesma aplicação. Isso não é “milagre”. É consequência direta de entender o motor embaixo do capô.
E foi justamente escrevendo uma linguagem que eu passei a enxergar o processo com mais clareza. Então hoje eu quero falar disso: como compiladores transformam seu código (geralmente porco) em algo que o computador realmente executa.
Spoiler: nada do que você escreve é executado do jeito que você escreveu.
Do texto para a AST: o começo da desmontagem
O primeiro passo é simples e brutal:
O compilador lê seus arquivos.
Constrói uma estrutura chamada AST (Abstract Syntax Tree).
A AST é o “seu código”, mas já separado por significado: expressões, blocos, chamadas, tipos, declarações. Não existe mais “texto”, existe estrutura.
Isso é importante porque a partir daqui o compilador começa a fazer o que ele faz melhor: reduzir ambiguidade e reorganizar o programa para virar máquina.
Do AST para um IR: seu código vira outra coisa
Depois da AST, o próximo passo é gerar um IR (Intermediate Representation).
Em linguagens modernas, esse caminho pode ter camadas intermediárias, tipo HIR/MIR, onde o compilador vai progressivamente “abaixando” o nível do programa:
resolve tipos,
expande açúcar sintático,
normaliza fluxo de controle,
decide representações de dados,
prepara tudo para otimização.
Aqui acontece uma quebra psicológica importante: a “forma humana” do código morre.
Aquele seu monte de arquivos, funções e abstrações vira algo mais próximo de instruções lineares. Não é assembly ainda, mas já parece. E o mais relevante:
muita coisa vira alocação e movimentação de memória,
o fluxo vira blocos com saltos claros,
a tipagem deixa de ser “conceito” e vira layout,
aquele “código elegante” vira uma sequência que dá pra ler “de cima pra baixo”.
E sim: nessa fase você pode ter um arquivão com milhares ou milhões de linhas de IR, porque agora o compilador quer um programa explícito.
Backend e Linker: o mundo real entra na conversa
Quando eu falo que uso LLVM, eu estou falando do backend que várias linguagens usam (Rust, Zig, etc.). Nessa etapa entram as coisas “do mundo real”:
geração de código para a arquitetura (x86, ARM…),
regras do sistema operacional,
chamada de bibliotecas nativas,
ABI/FFI,
e o linker, que cola tudo em um binário final.
Esse detalhe é gigantesco: um backend forte te dá portabilidade e acesso ao ecossistema do C/C++.
E aqui vai minha opinião sem romantismo:
perder compatibilidade com C/C++ hoje é um erro.
C/C++ é o esgoto e a fundação ao mesmo tempo: HTTP, sockets, CUDA, drivers, bibliotecas de compressão, criptografia, parsing… boa parte do “mundo útil” existe nesse universo. Ter FFI/ABI decente não é luxo — é sobrevivência.
Dá pra fazer um backend próprio? Dá.
Mas você paga caro: perde maturidade, perde suporte, perde tooling, perde um século de engenharia. E principalmente: você se isola do planeta.
A pergunta que ninguém escapa: “como C++ foi escrito em C++?”
Eu sempre tive essa questão na cabeça:
como um compilador de C++ foi escrito em C++ se antes não existia C++?
A resposta é mais simples do que parece: bootstrapping.
O primeiro compilador nasce em outra linguagem (ou num subconjunto). Assim que ele fica funcional, você reescreve o compilador na própria linguagem.
É um trabalho de corno? É.
Mas também é um rito de passagem: você prova que a linguagem se sustenta e vira capaz de evoluir por conta própria.
E por que muitos compiladores são em C? Porque C é:
simples o suficiente pra controlar baixo nível,
próximo do hardware,
universal,
pragmático,
e tem compatibilidade brutal com tudo.
Além disso, a realidade é essa: a gente usa ferramentas antigas porque elas são boas. Tem compilador com DNA dos anos 80 rodando produção moderna — e isso não é vergonha, é sinal de maturidade.
Por que entender isso muda sua performance?
Porque sem entender o motor, performance vira chute.
Você fica tentando adivinhar onde está o gargalo com base em sintoma:
“vamos subir mais instâncias”
“vamos jogar num load balancer”
“vamos aumentar o cache”
“vamos trocar linguagem”
E muitas vezes o problema era:
cópia desnecessária,
alocação em loop,
cache miss,
branch ruim,
lock mal colocado,
syscalls demais.
Se você entende o pipeline, você para de tratar performance como superstição e passa a tratar como engenharia.
Pra IA não é diferente. Ela conhece sintaxe e padrões, mas não carrega automaticamente as limitações reais de cada linguagem, runtime e toolchain.
Quem resolve isso é o operador: você limita o espaço de busca, direciona a implementação, evita armadilhas conhecidas.
É por isso que dev experiente extrai muito mais de IA do que dev novato. Não é “dom”. É repertório do que dá errado no mundo real.
E é aqui que entra uma provocação:
Se as IAs ganharem linguagens feitas pra elas — linguagens sem o legado humano, sem as ambiguidades históricas — o que acontece?
A gente vai parar em algo meio “assembly moderno” que humanos odeiam, mas máquinas e modelos adoram?
Uma coisa que eu vi acontecendo com o Claude Opus 4.5 durante o desenvolvimento da minha linguagem: ele quase sempre prefere depurar em nível baixo.
Ele desce pro IR, pro .ll, pro assembly.
E isso acelera demais:
ele enxerga as instruções que o hardware vai mastigar,
ele identifica custo real (e não “suposição de alto nível”),
ele acha bug estrutural que no código fonte fica mascarado,
ele corrige de forma cirúrgica.
Pra performance isso é ouro. Porque performance está muito mais perto de:
layout,
instrução,
registrador,
salto,
memória,
do que de “padrões bonitos” na superfície.
E tem outro ponto: modelos gostam de código linear.
Mesmo que o treinamento seja cheio de Python, o que eles fazem melhor é lidar com baixa ambiguidade:
diretivas claras,
tipagem explícita,
otimização local,
sequência determinística.
No fim, até linguagem dinâmica vira tipo “real” em algum momento. A diferença é que com um pipeline transparente, isso fica explícito, rastreável e testável.
O próximo passo: IA integrada ao compilador, com feedback brutal
Quando você junta:
biblioteca nativa de testes,
compilador com IR inspecionável,
execução automatizada,
integração via MCP,
você cria um loop que eu, sinceramente, nunca tinha visto funcionando desse jeito:
o código vira IR analisável,
os erros explodem como dados,
o modelo não perde tempo brigando com ambiente,
a correção vem orientada pelo que o hardware realmente vai fazer.
Isso, pra mim, é usar IA de verdade no desenvolvimento. Não como “autocomplete com ego”, mas como um agente que opera em cima de um processo transparente, determinístico e mensurável.
E se isso virar padrão, o papo de “IA não vai substituir dev” muda de forma. Porque talvez ela não substitua o dev que escreve código como humano… mas ela pode dominar um mundo onde o dev escreve o que a máquina quer ler — e o compilador mostra a verdade sem maquiagem.

