Bytecode e Máquinas Virtuais
Em vez de executar a AST diretamente ou gerar código de máquina, muitas linguagens compilam para bytecode — um formato intermediário executado por uma VM de pilha.
O meio-termo entre interpretar e compilar
Interpretar a AST é simples mas lento. Compilar para código de máquina é rápido mas difícil de portar entre sistemas operacionais. Bytecode é o meio-termo: um formato binário compacto de instruções simples, executado por uma Máquina Virtual (VM) que é portátil.
A VM mais comum é uma máquina de pilha: operações pop dois valores da pilha,
executam uma operação e empilham o resultado. PUSH 3; PUSH 4; MUL → pilha = [12].
É o modelo usado por JVM (Java), CPython, WebAssembly e CLR (.NET).
Execute bytecode passo a passo
Veja o Program Counter avançar, a pilha mudar a cada instrução e a memória de variáveis se preencher. Escolha um programa e use Próximo ou Executar.
Compilador de AST para bytecode
// Compilar "x = 2 + 3 * 4" para bytecode de pilha // Compilação: percorre a AST e emite instruções function compile(node) { switch (node.type) { case "Num": emit("PUSH", node.value); break; case "Id": emit("LOAD", node.name); break; case "BinOp": compile(node.left); compile(node.right); emit(opcode(node.op)); // ADD, SUB, MUL, DIV break; case "Let": compile(node.value); emit("STORE", node.name); break; } } // Resultado para "x = 2 + 3 * 4": PUSH 2 // pilha: [2] PUSH 3 // pilha: [2, 3] PUSH 4 // pilha: [2, 3, 4] MUL // pilha: [2, 12] ADD // pilha: [14] STORE x // pilha: [] mem: {x:14}
O loop principal da VM
// VM de pilha — o loop principal (fetch-decode-execute) function run(bytecode) { let pc = 0; // program counter const stack = []; // pilha de operandos const mem = {}; // variáveis while (true) { const { op, arg } = bytecode[pc++]; // FETCH + DECODE switch (op) { // EXECUTE case "PUSH": stack.push(arg); break; case "ADD": stack.push(stack.pop() + stack.pop()); break; case "MUL": stack.push(stack.pop() * stack.pop()); break; case "STORE": mem[arg] = stack.pop(); break; case "LOAD": stack.push(mem[arg]); break; case "JUMP": pc = arg; break; case "JUMP_IF_FALSE": if (!stack.pop()) pc = arg; break; case "PRINT": console.log(stack.pop()); break; case "HALT": return; } } }
Construa uma VM completa
Mini projeto: implemente a VM de pilha com 10 instruções: PUSH, POP, ADD, SUB, MUL, DIV, LOAD, STORE, PRINT, HALT. Escreva o bytecode para "imprimir a soma dos 10 primeiros números" à mão.
Projeto principal: adicione JUMP e JUMP_IF_FALSE e escreva um compilador de expressões simples (AST → bytecode). Use-o para compilar e executar um programa com if/else e while loop.
Desafio extra: adicione CALL e RET para suporte a funções. Implemente um call frame (pilha de chamadas com variáveis locais por função). Teste recursão com fatorial e Fibonacci. É exatamente como JVM implementa invocação de métodos.
Teste sua intuição
Onde você encontra isso
JVM — Java, Kotlin, Scala
A JVM é a VM mais usada no mundo. Kotlin e Scala compilam para o mesmo bytecode JVM que Java. Isso significa que bibliotecas Java funcionam em Kotlin sem adaptação — a portabilidade é uma feature de design.
WebAssembly
Wasm é bytecode para o browser — uma VM de pilha executada pelo motor JavaScript. C++, Rust e Go compilam para Wasm e rodam no browser com performance próxima ao nativo. É o futuro do código de alta performance na web.
CPython bytecode
import dis; dis.dis(lambda: 1+2) mostra o bytecode Python. Os .pyc são caches do bytecode compilado — evitam re-parsear na próxima execução. Python 3.12 refatorou todo o bytecode para ter instruções mais especializadas.
.NET CLR / MSIL
C#, F# e VB.NET compilam para MSIL (Microsoft Intermediate Language), bytecode executado pelo CLR (.NET Runtime). O .NET Native compila MSIL para código de máquina antecipadamente (AOT) para apps UWP e iOS.