Trilha 12 · Linguagens e compiladores

Interpretadores

A forma mais direta de executar código: percorrer a AST nodo a nodo, avaliando expressões e modificando um ambiente de variáveis.

① Intuição

Executar a árvore diretamente

Um interpretador tree-walking pega a AST e a percorre recursivamente: ao encontrar um BinOp(+), avalia os dois filhos e retorna a soma. Ao encontrar um If, avalia a condição e executa o branch correto. Um ambiente (environment) guarda as variáveis — é basicamente um dicionário {nome: valor}.

Interpretadores são simples de implementar (centenas de linhas de código), portáteis e fáceis de depurar. A desvantagem: são mais lentos que compiladores, pois percorrem a AST toda vez que o código executa — mesmo em loops que rodam milhões de vezes.

Python, Ruby e PHP começaram como interpretadores tree-walking puros. Hoje todos compilam para bytecode primeiro (para evitar re-parsear), mas a execução ainda é "interpretada" no sentido de que não gera código de máquina diretamente.
② Visualização interativa

Interprete programas passo a passo

Escolha um programa e clique em Próximo ou Executar. Acompanhe a linha atual, o ambiente de variáveis e a saída.

CÓDIGO FONTE
0let x = 10
1let y = 3
2let soma = x + y
3let produto = x * y
4print soma
5print produto
VARIÁVEIS (ambiente)
{ }
SAÍDA
...
Passo 1/8Início do programa. Ambiente de variáveis vazio.
③ Explicação técnica

Tree-walking interpreter

// Tree-walking interpreter — executa a AST diretamente

function evaluate(node, env) {
  switch (node.type) {
    case "Num":
      return node.value;

    case "Id":
      if (!(node.name in env)) throw new Error(`Undefined: ${node.name}`);
      return env[node.name];

    case "BinOp": {
      const l = evaluate(node.left, env);
      const r = evaluate(node.right, env);
      if (node.op === "+") return l + r;
      if (node.op === "-") return l - r;
      if (node.op === "*") return l * r;
      if (node.op === "/") return l / r;
    }

    case "Let":
      env[node.name] = evaluate(node.value, env);
      return undefined;

    case "If":
      if (evaluate(node.cond, env)) executeBlock(node.then, env);
      else if (node.else)            executeBlock(node.else, env);
      return undefined;

    case "While":
      while (evaluate(node.cond, env)) executeBlock(node.body, env);
      return undefined;

    case "Print":
      console.log(evaluate(node.expr, env));
      return undefined;
  }
}

Escopos léxicos

// Escopos léxicos — cada bloco tem seu ambiente

function makeEnv(parent = null) {
  const bindings = {};
  return {
    get(name) {
      if (name in bindings)  return bindings[name];
      if (parent)             return parent.get(name); // busca no pai
      throw new Error(`Undefined: ${name}`);
    },
    set(name, value) { bindings[name] = value; },
  };
}

// Ao entrar num bloco (if, while, função):
const outer = makeEnv();
outer.set("x", 10);

const inner = makeEnv(outer);  // inner vê x do outer
inner.set("y", 5);
inner.get("x");  // → 10 (sobe pela cadeia de escopos)
Closures: quando uma função é definida, ela captura uma referência ao ambiente em que foi criada — não uma cópia. Isso é uma closure. Se a função modifica variáveis do escopo externo, a mudança persiste. Implementar closures corretamente é um dos desafios centrais de interpretar linguagens funcionais.
④ Projeto para programar

Implemente um interpretador

Mini projeto: implemente evaluate(ast, env) para suportar: números, variáveis, as 4 operações aritméticas, comparações (==, !=, <, >), e print. Teste com 5 expressões diferentes.

Projeto principal: adicione suporte a let, if/else, while, e funções com parâmetros e retorno. Implemente escopos léxicos corretamente (funções devem capturar o escopo onde foram definidas). Teste com recursão (fatorial).

Desafio extra: implemente um debugger para seu interpretador: adicione breakpoints (pontos de parada), inspeção de variáveis, e execução passo a passo com step-in/step-over. É assim que debuggers de linguagens interpretadas funcionam.

⑤ Exercícios rápidos

Teste sua intuição

Como um tree-walking interpreter executa código?
O que é o ambiente (environment) num interpretador?
Por que interpretadores geralmente são mais lentos que compiladores?
⑥ Aplicações no mundo real

Onde você encontra isso

🐍

CPython

O interpretador oficial de Python compila para bytecode (.pyc) e depois interpreta o bytecode numa VM chamada CPython. O tree-walking puro foi substituído por bytecode na versão 1.5 (1997) para ganhar velocidade.

📜

Bash e shell scripts

Shells Unix (bash, zsh, fish) são interpretadores puros: leem o script linha a linha, parseiam e executam imediatamente. Não há passo de compilação — por isso scripts de 1000 linhas são perceptivelmente mais lentos.

🎮

Lua em jogos

Lua é a linguagem de script embutida em jogos como World of Warcraft e Roblox. É compilada para bytecode e interpretada por uma VM compacta (~150kb) que pode ser embutida em qualquer aplicação C.

📊

Cel (Google)

Common Expression Language — linguagem interpretada usada em Kubernetes, Firebase e Cloud IAM para avaliar expressões de política de segurança em tempo real sem riscos de execução arbitrária.

← Anterior: A Árvore Sintática Próxima: Bytecode e VMs →