Interpreter
Define uma representação para a gramática de uma linguagem e um interpretador que usa essa representação para processar sentenças — cada regra da gramática vira uma classe, e a árvore sintática é construída compondo essas classes.
Intenção
Dada uma linguagem, definir uma representação para a sua gramática e um interpretador que usa essa representação para processar sentenças da linguagem. Cada regra da gramática é mapeada para uma classe; expressões complexas são formadas compondo objetos dessas classes em árvores sintáticas — que são, por isso, Composites.
Catalogado pelo GoF (1994) como padrão comportamental, o Interpreter é mais didático do que amplamente aplicado em projetos modernos. Ele é útil para linguagens simples e pequenas DSLs onde a gramática tem poucas regras e a expressividade manual de classes compensa o custo. Para gramáticas reais — com tokens complexos, precedência de operadores, tratamento de erros léxicos e sintáticos — use ferramentas dedicadas: parser generators (ANTLR, PEG.js, Tree-sitter), combinadores de parser (Parsimmon, Chevrotain) ou gramáticas PEG/BNF. O padrão de classes do Interpreter se torna inviável rapidamente à medida que a gramática cresce.
Problema
Imagine um sistema de configuração que aceita filtros escritos como
expressões matemáticas com variáveis: a + b * 3, onde
a e b vêm de um contexto (registro, sessão,
configuração). A abordagem ingênua avalia a expressão como string em
runtime — insegura e inflexível:
// Abordagem ingênua — NÃO faça isso:
function avaliar(expressao: string, contexto: Record<string, number>): number {
// eval() executa código arbitrário — risco de injeção de código.
// Não há validação, sem tipagem, sem mensagens de erro úteis.
return eval(expressao.replace(/([a-z]+)/g, (_, v) => String(contexto[v] ?? 0)));
}
console.log(avaliar('a + b * 3', { a: 5, b: 4 })); // 17 (mas perigoso!)
// Pior: avaliar('process.exit(1)', {}) derruba o processo.
O Interpreter resolve isso mapeando cada regra da gramática para uma classe.
A expressão é representada como uma árvore de objetos — construída
manualmente ou por um parser simples. Cada nó sabe como se interpretar
dado um contexto, e a avaliação é feita por chamadas polimórficas na
árvore — sem eval(), sem código arbitrário.
Solução
O Interpreter organiza o código em quatro participantes:
-
AbstractExpression (interface): declara o método
interpretar(contexto): Tque todas as expressões devem implementar. O tipo de retorno depende da linguagem: número, boolean, string. O contexto carrega o estado externo necessário para a avaliação (valores de variáveis, registros de dados, configurações). -
TerminalExpression: implementa a interpretação de um
símbolo terminal da gramática — um símbolo que não se expande em outros.
Exemplos:
Numero(retorna um valor literal) eVariavel(consulta o contexto). São as folhas da árvore sintática. -
NonTerminalExpression: implementa uma regra da gramática
que referencia outras expressões. Exemplos:
Soma,Subtracao,Multiplicacao. Cada NonTerminalExpression contém referências às sub-expressões (esquerda e direita) e delega a interpretação a elas antes de combinar os resultados. Como cada NonTerminalExpression contém outrasIExpressao, a estrutura é um Composite. -
Context: carrega a informação global necessária para a
interpretação — tipicamente os valores das variáveis. Em TypeScript, pode
ser um simples
Record<string, number>; em sistemas mais complexos, um objeto com métodos de consulta e estado acumulado.
A avaliação é feita chamando expressaoRaiz.interpretar(contexto):
o método percorre recursivamente a árvore, dos nós internos até as folhas,
e retorna o resultado combinado. A gramática está implícita na estrutura
das classes — não há um parser generator, apenas objetos compostos.
Estrutura
«interface»
IExpressao
┌────────────────────────────────────────┐
│ + interpretar(ctx: Contexto): number │
└────────────────────────────────────────┘
▲
┌───────────┼────────────────────────────────┐
│ │ │ │ │
Numero Variavel Soma Subtracao Multiplicacao
(Terminal)(Terminal) (NonTerminal)(NonTerminal)(NonTerminal)
│ │
│ retorna │ consulta ctx[nome]
│ this.valor│
│ │
└─ não referencia outras IExpressao (folha)
Soma / Subtracao / Multiplicacao:
┌───────────────────────────────────────────────┐
│ - esquerda: IExpressao │
│ - direita: IExpressao │
│ + interpretar(ctx): number │
│ return esquerda.interpretar(ctx) │
│ OP direita.interpretar(ctx) │
└───────────────────────────────────────────────┘
(Composite interno — cada nó pode conter outros nós)
Contexto:
{ a: 5, b: 4 } (Record<string, number>)
Fluxo para "a + (b * 3)", ctx = {a:5, b:4}:
Soma
├── Variavel('a') → interpretar(ctx) = 5
└── Multiplicacao
├── Variavel('b') → interpretar(ctx) = 4
└── Numero(3) → interpretar(ctx) = 3
→ 4 * 3 = 12
→ 5 + 12 = 17
Exemplos de código
Exemplo 1 — Expressões matemáticas com variáveis e contexto
Implementação completa com a interface IExpressao, duas
expressões terminais (Numero e Variavel) e três
não-terminais (Soma, Subtracao,
Multiplicacao). O contexto é um simples mapa de variáveis.
A árvore é construída manualmente — em sistemas reais, um parser faria
esse trabalho.
// ── Contexto ──────────────────────────────────────────────────
// Mapa de variáveis disponíveis durante a interpretação.
type Contexto = Record<string, number>;
// ── AbstractExpression ────────────────────────────────────────
interface IExpressao {
interpretar(ctx: Contexto): number;
}
// ── TerminalExpressions ───────────────────────────────────────
// Numero: folha que retorna um valor literal.
class Numero implements IExpressao {
constructor(private readonly valor: number) {}
interpretar(_ctx: Contexto): number {
return this.valor;
}
}
// Variavel: folha que consulta o contexto pelo nome.
class Variavel implements IExpressao {
constructor(private readonly nome: string) {}
interpretar(ctx: Contexto): number {
if (!(this.nome in ctx)) {
throw new Error(`Variável '${this.nome}' não definida no contexto`);
}
return ctx[this.nome];
}
}
// ── NonTerminalExpressions ────────────────────────────────────
class Soma implements IExpressao {
constructor(
private readonly esquerda: IExpressao,
private readonly direita: IExpressao
) {}
interpretar(ctx: Contexto): number {
return this.esquerda.interpretar(ctx) + this.direita.interpretar(ctx);
}
}
class Subtracao implements IExpressao {
constructor(
private readonly esquerda: IExpressao,
private readonly direita: IExpressao
) {}
interpretar(ctx: Contexto): number {
return this.esquerda.interpretar(ctx) - this.direita.interpretar(ctx);
}
}
class Multiplicacao implements IExpressao {
constructor(
private readonly esquerda: IExpressao,
private readonly direita: IExpressao
) {}
interpretar(ctx: Contexto): number {
return this.esquerda.interpretar(ctx) * this.direita.interpretar(ctx);
}
}
// ── Uso ──────────────────────────────────────────────────────
// Expressão: a + (b * 3) - 1
// AST construída manualmente — em produção, um parser faria isso.
const expressao: IExpressao = new Subtracao(
new Soma(
new Variavel('a'),
new Multiplicacao(new Variavel('b'), new Numero(3))
),
new Numero(1)
);
// Contexto 1
const ctx1: Contexto = { a: 5, b: 4 };
console.log(expressao.interpretar(ctx1));
// 5 + (4 * 3) - 1 = 16
// Contexto 2 — mesma expressão, outros valores
const ctx2: Contexto = { a: 10, b: 2 };
console.log(expressao.interpretar(ctx2));
// 10 + (2 * 3) - 1 = 15
// Expressão composta: (a - b) * (a + b) → diferença de quadrados
const difQuadrados: IExpressao = new Multiplicacao(
new Subtracao(new Variavel('a'), new Variavel('b')),
new Soma(new Variavel('a'), new Variavel('b'))
);
// a=5, b=3 → (5-3)*(5+3) = 2*8 = 16
console.log(difQuadrados.interpretar({ a: 5, b: 3 })); // 16
<?php
// ── AbstractExpression ────────────────────────────────────────
interface IExpressao
{
/** @param array<string, float> $ctx */
public function interpretar(array $ctx): float;
}
// ── TerminalExpressions ───────────────────────────────────────
class Numero implements IExpressao
{
public function __construct(private readonly float $valor) {}
public function interpretar(array $ctx): float
{
return $this->valor;
}
}
class Variavel implements IExpressao
{
public function __construct(private readonly string $nome) {}
public function interpretar(array $ctx): float
{
if (!array_key_exists($this->nome, $ctx)) {
throw new \RuntimeException(
"Variável '{$this->nome}' não definida no contexto"
);
}
return (float) $ctx[$this->nome];
}
}
// ── NonTerminalExpressions ────────────────────────────────────
class Soma implements IExpressao
{
public function __construct(
private readonly IExpressao $esquerda,
private readonly IExpressao $direita
) {}
public function interpretar(array $ctx): float
{
return $this->esquerda->interpretar($ctx)
+ $this->direita->interpretar($ctx);
}
}
class Subtracao implements IExpressao
{
public function __construct(
private readonly IExpressao $esquerda,
private readonly IExpressao $direita
) {}
public function interpretar(array $ctx): float
{
return $this->esquerda->interpretar($ctx)
- $this->direita->interpretar($ctx);
}
}
class Multiplicacao implements IExpressao
{
public function __construct(
private readonly IExpressao $esquerda,
private readonly IExpressao $direita
) {}
public function interpretar(array $ctx): float
{
return $this->esquerda->interpretar($ctx)
* $this->direita->interpretar($ctx);
}
}
// ── Uso ──────────────────────────────────────────────────────
// Expressão: a + (b * 3) - 1
$expressao = new Subtracao(
new Soma(
new Variavel('a'),
new Multiplicacao(new Variavel('b'), new Numero(3))
),
new Numero(1)
);
// Contexto 1
$ctx1 = ['a' => 5, 'b' => 4];
echo $expressao->interpretar($ctx1) . "\n";
// 5 + (4 * 3) - 1 = 16
// Contexto 2 — mesma expressão, outros valores
$ctx2 = ['a' => 10, 'b' => 2];
echo $expressao->interpretar($ctx2) . "\n";
// 10 + (2 * 3) - 1 = 15
// Diferença de quadrados: (a - b) * (a + b)
$difQuadrados = new Multiplicacao(
new Subtracao(new Variavel('a'), new Variavel('b')),
new Soma(new Variavel('a'), new Variavel('b'))
);
echo $difQuadrados->interpretar(['a' => 5, 'b' => 3]) . "\n"; // 16
Exemplo 2 — Mini-DSL de filtros booleanos
O Interpreter também é adequado para pequenas DSLs de consulta ou filtro.
Neste exemplo, a linguagem tem três regras: Igual (terminal —
verifica se um campo de um registro tem determinado valor), E
(AND) e Ou (OR) como não-terminais. O contexto é o registro
sendo avaliado. A expressão filtra uma lista de registros sem
eval() e com composição tipo-segura.
// ── Contexto: um registro de dados ───────────────────────────
type Registro = Record<string, string | number>;
// ── AbstractExpression ────────────────────────────────────────
interface ICondicao {
avaliar(registro: Registro): boolean;
}
// ── TerminalExpression: campo = valor ─────────────────────────
class Igual implements ICondicao {
constructor(
private readonly campo: string,
private readonly valor: string | number
) {}
avaliar(registro: Registro): boolean {
return registro[this.campo] === this.valor;
}
}
// ── NonTerminalExpression: E (AND) ────────────────────────────
class E implements ICondicao {
constructor(
private readonly esquerda: ICondicao,
private readonly direita: ICondicao
) {}
avaliar(registro: Registro): boolean {
// Short-circuit: se esquerda for falsa, direita não é avaliada.
return this.esquerda.avaliar(registro) && this.direita.avaliar(registro);
}
}
// ── NonTerminalExpression: Ou (OR) ────────────────────────────
class Ou implements ICondicao {
constructor(
private readonly esquerda: ICondicao,
private readonly direita: ICondicao
) {}
avaliar(registro: Registro): boolean {
return this.esquerda.avaliar(registro) || this.direita.avaliar(registro);
}
}
// ── Uso ──────────────────────────────────────────────────────
// DSL: status = 'ativo' AND (plano = 'pro' OR plano = 'enterprise')
const filtro: ICondicao = new E(
new Igual('status', 'ativo'),
new Ou(
new Igual('plano', 'pro'),
new Igual('plano', 'enterprise')
)
);
const registros: Registro[] = [
{ status: 'ativo', plano: 'pro' },
{ status: 'ativo', plano: 'free' },
{ status: 'inativo', plano: 'pro' },
{ status: 'ativo', plano: 'enterprise' },
];
const resultado = registros.filter(r => filtro.avaliar(r));
console.log(resultado);
// [{ status: 'ativo', plano: 'pro' }, { status: 'ativo', plano: 'enterprise' }]
// A árvore de filtro pode ser construída dinamicamente a partir de
// uma configuração JSON — sem eval(), com total controle sobre o que
// a linguagem pode expressar.
<?php
// ── AbstractExpression ────────────────────────────────────────
interface ICondicao
{
/** @param array<string, string|float> $registro */
public function avaliar(array $registro): bool;
}
// ── TerminalExpression: campo = valor ─────────────────────────
class Igual implements ICondicao
{
public function __construct(
private readonly string $campo,
private readonly string|float $valor
) {}
public function avaliar(array $registro): bool
{
return isset($registro[$this->campo])
&& $registro[$this->campo] === $this->valor;
}
}
// ── NonTerminalExpression: E (AND) ────────────────────────────
class E implements ICondicao
{
public function __construct(
private readonly ICondicao $esquerda,
private readonly ICondicao $direita
) {}
public function avaliar(array $registro): bool
{
// Short-circuit nativo do PHP para &&.
return $this->esquerda->avaliar($registro)
&& $this->direita->avaliar($registro);
}
}
// ── NonTerminalExpression: Ou (OR) ────────────────────────────
class Ou implements ICondicao
{
public function __construct(
private readonly ICondicao $esquerda,
private readonly ICondicao $direita
) {}
public function avaliar(array $registro): bool
{
return $this->esquerda->avaliar($registro)
|| $this->direita->avaliar($registro);
}
}
// ── Uso ──────────────────────────────────────────────────────
// DSL: status = 'ativo' AND (plano = 'pro' OR plano = 'enterprise')
$filtro = new E(
new Igual('status', 'ativo'),
new Ou(
new Igual('plano', 'pro'),
new Igual('plano', 'enterprise')
)
);
$registros = [
['status' => 'ativo', 'plano' => 'pro'],
['status' => 'ativo', 'plano' => 'free'],
['status' => 'inativo', 'plano' => 'pro'],
['status' => 'ativo', 'plano' => 'enterprise'],
];
$resultado = array_filter($registros, fn($r) => $filtro->avaliar($r));
print_r(array_values($resultado));
// [
// ['status' => 'ativo', 'plano' => 'pro'],
// ['status' => 'ativo', 'plano' => 'enterprise']
// ]
Quando usar
- Para DSLs pequenas e estáveis: filtros de consulta, expressões de regras de negócio, mini-linguagens de configuração ou templates com poucas construções. O número de classes de expressão deve ser pequeno — até dez regras é razoável; além disso, prefira um parser generator.
- Quando a gramática é bem conhecida e raramente muda: o Interpreter é fácil de estender com novos operadores (novas classes), mas qualquer mudança na estrutura central da gramática pode cascatear por todas as classes existentes.
- Para fins didáticos e prototipagem: o padrão torna a estrutura de uma gramática explícita no código de classes — útil para aprender como parsers funcionam internamente antes de usar ferramentas automatizadas.
-
Quando segurança é crítica: uma DSL baseada em
Interpreter executa apenas as operações que você definiu — sem risco
de code injection via
eval(). Sistemas de regras de negócio, configurações de filtragem e engines de validação se beneficiam desse controle explícito.
Quando evitar
- Gramáticas complexas: cada regra vira uma classe; uma gramática com 20 regras gera 20 classes — e a interação entre elas fica rapidamente difícil de manter. Use ANTLR, PEG.js, Chevrotain ou Tree-sitter para qualquer linguagem com precedência de operadores não-trivial, recuperação de erros ou tokens complexos.
- Quando desempenho é crítico: a recursão por toda a árvore a cada avaliação pode ser lenta para expressões complexas ou avaliadas em alta frequência. Compiladores reais não interpretam a AST diretamente — compilam para bytecode ou código nativo.
- Quando o padrão já existe na linguagem: muitas linguagens têm formas idiomáticas de compor predicados — closures, combinadores funcionais, expressões regulares. O Interpreter baseado em classes frequentemente é mais verboso do que uma solução funcional.
Prós e contras
Prós
- A gramática fica explícita no código — cada regra é uma classe com nome descritivo, fácil de localizar.
- Aberto para novas regras: adicionar um operador é criar uma nova classe que implementa a interface — sem modificar as expressões existentes.
- Avaliação segura e controlada: sem eval(), sem code injection, sem acesso a APIs não autorizadas.
- A árvore de expressão pode ser construída dinamicamente (a partir de JSON, banco de dados ou UI) — permitindo DSLs configuráveis em runtime.
- Separação clara entre a estrutura da expressão e o mecanismo de avaliação.
Contras
- Explosão de classes para gramáticas não-triviais — uma classe por regra é inviável acima de ~10-15 regras.
- Sem suporte nativo a erros léxicos ou sintáticos: construir a árvore manualmente não detecta sentenças inválidas — exige um parser separado.
- Desempenho inferior a bytecode ou código compilado — a recursão interpretada na AST é ordens de magnitude mais lenta que código nativo.
- Dificuldade em implementar precedência de operadores, associatividade e agrupamento sem um parser dedicado.
Armadilhas comuns
1. Confundir Interpreter com Composite
O Interpreter usa o Composite internamente — a árvore sintática
é um Composite onde as NonTerminalExpressions são os nós compostos e as
TerminalExpressions são as folhas. A diferença é de intenção: o Composite
organiza estruturas parte-todo e trata folhas e compostos uniformemente;
o Interpreter interpreta sentenças de uma gramática específica. Quando
alguém adiciona um método interpretar() a um Composite
existente, ele está essencialmente aplicando o Interpreter sobre a
estrutura do Composite.
2. Crescimento descontrolado de classes
A armadilha de escala mais crítica: cada nova regra da gramática exige
uma nova classe. Uma DSL de filtros que começa com
Igual, E e Ou parece razoável.
Mas ao adicionar Maior, Menor,
Contém, Começa, Nao,
Entre, In — o número de classes cresce
rapidamente. Quando esse ponto é atingido, migre para um combinador
de parsers ou um parser generator.
Regra prática: se a gramática tem mais de 8 a 10 classes de expressão, avalie seriamente o uso de uma biblioteca de parsing dedicada. O Interpreter como padrão de classes é para DSLs genuinamente pequenas — não para linguagens reais.
3. Ausência de tratamento de erros na construção da AST
Construir a árvore manualmente não valida a sentença. Uma expressão
com new Soma(null, new Numero(3)) compila sem erros mas
explode em runtime com um NullPointerException. Em TypeScript, use
tipos não-anuláveis e strict mode para mitigar; em PHP, declare tipos
na interface. Para validação completa de sentenças de entrada externa
(strings digitadas pelo usuário), um parser com mensagens de erro
úteis é indispensável.
4. Interpreter vs Composite — quando usar qual
Use o Composite quando o foco é a estrutura hierárquica parte-todo e a operação principal é uniforme sobre todos os nós (ex.: calcular tamanho total de uma pasta). Use o Interpreter quando o foco é avaliar sentenças de uma linguagem com regras gramaticais explícitas — onde cada tipo de nó tem semântica diferente de avaliação (Soma soma, Multiplicacao multiplica, Variavel consulta o contexto). Na prática, o Interpreter é sempre um Composite; mas nem todo Composite é um Interpreter.
Padrões relacionados
O Interpreter se relaciona diretamente com padrões estruturais e comportamentais que organizam e percorrem hierarquias:
O Composite é a fundação estrutural do Interpreter: a
árvore sintática abstrata (AST) é um Composite onde as
NonTerminalExpressions são nós compostos e as TerminalExpressions são
folhas. O método interpretar() percorre essa árvore de
forma recursiva — exatamente como o método operation()
percorre um Composite. O Visitor é o complemento
natural do Interpreter: enquanto o Interpreter define uma única operação
(interpretar()) diretamente em cada classe de expressão,
o Visitor separa as operações das classes — permitindo adicionar novos
comportamentos (imprimir, otimizar, compilar) à mesma AST sem modificá-la.
Em compiladores reais, a AST é construída com o Composite, interpretada
com o Interpreter em fases iniciais e depois percorrida por múltiplos
passes de análise e transformação implementados como Visitors.