No último artigo examinamos a teoria do padrão Interpreter, aprendemos o que é uma árvore AST e como abstrair expressões terminais e não terminais. Desta vez, vamos nos afastar da teoria e ver como esse padrão é aplicado em projetos comerciais sérios que todos usamos todos os dias!
Spoiler: Você pode estar usando o padrão Interpreter agora mesmo, apenas lendo este texto no seu navegador!
Um dos exemplos mais marcantes e, talvez, mais importantes do uso desse padrão na indústria é o JavaScript. A linguagem, que originalmente foi criada “no joelho”, hoje funciona em bilhões de dispositivos justamente graças ao conceito de interpretação.
10 dias que mudaram a Internet
A história do JavaScript está cheia de lendas. Em 1995, Brendan Eich, enquanto trabalhava na Netscape Communications, recebeu a tarefa de criar uma linguagem de script simples que pudesse ser executada diretamente em um navegador (Netscape Navigator) para tornar as páginas da web interativas. A administração queria algo com uma sintaxe semelhante ao então super popular Java, mas destinado não a engenheiros profissionais, mas a web designers.
Eich teve apenas 10 dias para escrever o primeiro protótipo da linguagem, que então se chamava Mocha (depois LiveScript, e só depois JavaScript por razões de marketing). A pressa não foi acidental: a Microsoft estava logo atrás, que ao mesmo tempo preparava ativamente sua própria linguagem de script VBScript para ser incorporada no navegador Internet Explorer. A Netscape precisava liberar urgentemente sua resposta para não perder na iminente guerra dos navegadores.
Simplesmente não havia tempo para escrever um compilador complexo em código de máquina. A solução óbvia e mais rápida para Eich foi a arquitetura do clássico Interpreter.
O primeiro intérprete (SpiderMonkey) funcionou assim:
- Ele lê o código-fonte do texto do script na página.
- O analisador léxico dividiu o texto em tokens.
- O analisador construiu uma Árvore de Sintaxe Abstrata (AST). Em termos do padrão Interpreter, esta árvore consistia em expressões terminais (strings, números como 42) e não terminais (chamadas de função, instruções como If, While).
- Então a máquina virtual “atravessou” esta árvore passo a passo, executando as instruções embutidas nela em cada nó (chamando um método semelhante a Interpret()).
Contexto e Objetos
Lembra do objeto Context que tivemos que passar para o método Interpret(Context context) na implementação clássica? O intérprete precisa dele para armazenar o estado atual da memória.
No caso do JavaScript, o papel deste contexto no nível superior é desempenhado por um objeto global (por exemplo, uma janela em um navegador). Quando seu nó AST tenta, digamos, escrever texto na tela via document.write(“Hello”), o interpretador acessa seu contexto (o objeto document) e chama a API interna do navegador desejada.
É graças ao interpretador que o JavaScript é capaz de interagir tão facilmente com o DOM (Document Object Model) – todos eles são apenas objetos em um contexto que são acessados por nós de árvore.
Evolução do intérprete: Compilação JIT
Historicamente, JS em navegadores permaneceu por muito tempo um intérprete “puro”. E isso tinha uma grande desvantagem – velocidade lenta. Analisar a árvore e percorrer cada nó lentamente cada vez que o script era executado tornava aplicativos da Web complexos mais lentos.
Com o advento do mecanismo V8 do Google (integrado ao Chrome) em 2008, ocorreu uma revolução. Os engenheiros perceberam que um intérprete não é suficiente para a web moderna. O mecanismo se tornou mais complexo: ele ainda constrói a árvore AST, mas agora usa compilação JIT (Just-In-Time).
Os mecanismos JS modernos (V8, SpiderMonkey) funcionam como um pipeline complexo:
- O interpretador base rápido e burro começa a executar seu código JS instantaneamente, sem sequer esperar que ele seja compilado (o padrão clássico ainda funciona aqui).
- Paralelamente, o mecanismo monitora seções “quentes” do código (loops ou funções que são chamadas milhares de vezes).
- Essas seções são compiladas pelo compilador JIT diretamente no código de máquina otimizado, ignorando o interpretador lento.
Foi essa combinação do início instantâneo do interpretador e do poder computacional de compilação que permitiu que o JavaScript dominasse o mundo, tornando-se a linguagem dos servidores (Node.js) e dos aplicativos móveis (React Native).
Intérprete na indústria de jogos
Apesar do domínio do C++ na computação pesada, o padrão Interpreter é um padrão da indústria no desenvolvimento de jogos para a criação de lógica de jogos. Para que? Para que os designers de jogos possam fazer jogos sem o risco de “deixar cair” o motor ou a necessidade de recompilá-lo constantemente.
Um excelente exemplo histórico é o UnrealScript – a linguagem na qual a lógica dos jogos Unreal Tournament e Gears of War foi escrita no Unreal Engine 1, 2 e 3. O texto foi compilado em um bytecode de máquina abstrato compacto, que foi então passo a passo (interpretado) pela máquina virtual do motor.
Scripts gráficos visuais (Blueprints)
Hoje, o texto foi substituído pela programação visual – o sistema Blueprints no Unreal Engine 4 e 5.
Se você já abriu um Blueprint no Unreal Engine, viu muitos nós conectados por fios. Arquitetonicamente, todo o gráfico do Blueprints é uma enorme Árvore de Sintaxe Abstrata (AST) desenhada na tela:
- Expressões de Terminal: Nós constantes. Por exemplo, um nó que simplesmente armazena o número 42 ou uma string. Eles retornam um valor específico quando interpretados.
- Expressões não terminais: Nós de computação (Adicionar) ou nós de controle de fluxo (Filial). Eles têm entradas de argumentos, que o intérprete avalia primeiro recursivamente antes de produzir o resultado como um pino de saída.
E o papel do contexto aqui é desempenhado pela memória de uma instância de um objeto de jogo específico (Ator). A Máquina Interpretadora “caminha” com segurança por esse gráfico, solicitando dados e realizando transições.
Onde mais o Interpretador é usado?
O padrão de intérprete pode ser encontrado em quase todos os sistemas complexos onde instruções dinâmicas precisam ser executadas. Aqui estão apenas alguns exemplos de software comercial:
- Linguagens de programação interpretadas (Python, Ruby, PHP). Todo o seu tempo de execução é baseado no padrão clássico. Por exemplo, a implementação de referência do CPython primeiro analisa seu script .py em um AST, compila-o em bytecode e, em seguida, uma enorme máquina virtual (loop de computação) interpreta esse bytecode passo a passo.
- Java Virtual Machine (JVM). Inicialmente, o código Java é compilado não em instruções de máquina, mas em bytecode. Quando você executa o aplicativo, a JVM atua como um intérprete (embora com compilação JIT agressiva, assim como na V8).
- Bancos de dados e SQL Quando você emite uma consulta SQL (SELECT * FROM users) no PostgreSQL ou MySQL, o mecanismo de banco de dados atua como um intérprete. Ele realiza análises lexicais, constrói uma árvore de consulta AST, gera um plano de execução e, em seguida, literalmente “interpreta” esse plano iterando nas linhas das tabelas.
- Expressões regulares (RegEx). Qualquer mecanismo de expressão regular analisa internamente um padrão de string (por exemplo, ^\d{3}-\d{2}$) em um gráfico de estado (NFA/DFA Automata), pelo qual o interpretador interno passa, combinando cada caractere de entrada com os vértices deste gráfico.
- Unity Shader Graph / Unreal Material Editor – interpreta nós visuais em código de shader modular (GLSL/HLSL).
- Nós de geometria do Blender – interpreta operações matemáticas e geométricas para gerar modelos 3D de forma processual em tempo real.
Total
O padrão Interpreter já ultrapassou o escopo de “escrever sua própria calculadora”. Este é o padrão mais poderoso da indústria. Desde mecanismos JavaScript que executam gigabytes de código nos bastidores dos navegadores todos os dias até designers de jogos que permitem construir lógica complexa sem conhecimento de C++, os intérpretes continuam sendo um dos conceitos de arquitetura mais importantes no desenvolvimento de TI moderno.