Trilha 12 · Linguagens e compiladores

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.

① Intuição

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).

Por que bytecode é mais rápido que tree-walking? A AST tem overhead de ponteiros e dispatch de tipos. O bytecode é um array linear de inteiros — CPU-friendly. Um loop de bytecode (switch no opcode) é 5-10x mais rápido que percorrer uma AST para o mesmo programa.
② Visualização interativa

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.

BYTECODE
0PUSH2empilha 2
1PUSH3empilha 3
2PUSH4empilha 4
3MULpop 4 e 3 → push 12
4ADDpop 12 e 2 → push 14
5STORErespop 14 → res = 14
6LOADrespush res (14)
7PRINTpop e imprime
8HALTfim
PILHA (topo → base)
vazia
MEMÓRIA
{ }
SAÍDA
...
PC=0 PUSH 2empilha 2
③ Explicação técnica

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;
    }
  }
}
Máquina de registradores vs. máquina de pilha: CPython e JVM usam pilha. LuaJIT e Dalvik (Android) usam registradores — acessam operandos por índice, sem push/pop. Registradores são mais rápidos mas o bytecode é maior. Pilha é mais compacto mas usa mais instruções. WebAssembly optou por pilha por ser mais simples de validar formalmente.
④ Projeto para programar

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.

⑤ Exercícios rápidos

Teste sua intuição

Numa VM de pilha, qual sequência de bytecode calcula 3 + 4?
Qual é a principal vantagem do bytecode sobre código de máquina?
Qual afirmação sobre arquiteturas de VMs está correta?
⑥ Aplicações no mundo real

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.

← Anterior: Interpretadores Próxima: Coleta de lixo →