Trilha 12 · Linguagens e compiladores

A Árvore Sintática Abstrata (AST)

Como uma lista de tokens vira uma árvore que representa a estrutura do código — capturando precedência, aninhamento e hierarquia.

① Intuição

Estrutura, não sequência

Uma lista de tokens é só uma sequência linear — não captura que * tem prioridade sobre +, nem que parênteses agrupam subexpressões. O parser resolve isso: consome a lista de tokens e constrói uma Árvore Sintática Abstrata (AST).

Na AST, cada nodo representa uma construção da linguagem. Uma expressão 1 + 2 * 3 vira uma árvore onde * é filho de + — o que garante que a multiplicação acontece antes. A ordem de operações emerge naturalmente da estrutura da gramática.

AST ≠ Parse Tree: a Parse Tree (ou CST — Concrete Syntax Tree) inclui todos os nodos da gramática, incluindo tokens "sem significado" como parênteses e vírgulas. A AST abstrai — guarda só o que importa para a semântica. Parênteses em (1+2)*3 não viram nodo na AST: sua informação está na posição dos nodos filhos.
② Visualização interativa

Explore a AST de expressões

Edite a expressão ou escolha um exemplo. Veja a AST construída em tempo real como árvore visual. Cada tipo de nodo tem uma cor.

Expressão:
*
+
1
2
3
Operador binário
Unário
Unário
Literal numérico
Identificador
Erro
③ Explicação técnica

Parser descendente recursivo

// Recursive descent parser — um método por nível de precedência

class Parser {
  constructor(tokens) { this.tokens = tokens; this.pos = 0; }
  peek()  { return this.tokens[this.pos]; }
  next()  { return this.tokens[this.pos++]; }

  // Cada método lida com um nível de precedência
  parseExpr()   { return this.parseAddSub(); }

  parseAddSub() {
    let left = this.parseMulDiv();
    while (["+", "-"].includes(this.peek()?.value)) {
      const op = this.next().value;
      left = { type: "BinOp", op, left, right: this.parseMulDiv() };
    }
    return left;
  }

  parseMulDiv() {
    let left = this.parsePrimary();
    while (["*", "/"].includes(this.peek()?.value)) {
      const op = this.next().value;
      left = { type: "BinOp", op, left, right: this.parsePrimary() };
    }
    return left;
  }

  parsePrimary() {
    const t = this.next();
    if (t.type === "NUMBER") return { type: "Num", value: t.value };
    if (t.type === "IDENT")  return { type: "Id",  value: t.value };
    if (t.value === "(")  {
      const expr = this.parseExpr();
      this.next(); // consume ")"
      return expr;
    }
  }
}

Gramática BNF

// Gramática BNF para expressões aritméticas
// (define a estrutura que o parser reconhece)

expr       ::= comparison
comparison ::= addition (('<' | '>' | '==' | '!=') addition)?
addition   ::= term (('+' | '-') term)*
term       ::= unary (('*' | '/') unary)*
unary      ::= '-' unary | primary
primary    ::= NUMBER | IDENTIFIER | '(' expr ')'

// "1 + 2 * 3" → parser gera:
//   BinOp(+, Num(1), BinOp(*, Num(2), Num(3)))
// Ordem de operações emerge naturalmente da gramática!
Erros de parsing: bons parsers produzem mensagens de erro úteis, não apenas "syntax error". Estratégias de recuperação (panic mode, error productions) permitem continuar o parsing após um erro para reportar múltiplos problemas de uma vez — como TypeScript faz ao sublinhar vários erros simultaneamente sem parar no primeiro.
④ Projeto para programar

Construa um parser de expressões

Mini projeto: implemente um parser recursivo descendente para expressões aritméticas com os 4 operadores, parênteses e variáveis. Retorne a AST como objeto JS. Teste: (2 + 3) * 4, a - b / c.

Projeto principal: estenda o parser para suportar statements: let x = expr, if (cond) block else block, while (cond) block, e print expr. Gere a AST completa de um programa de 10 linhas.

Desafio extra: implemente um pretty printer: dada uma AST, regenere o código fonte formatado (com indentação correta para blocos if/while). Isso é como ferramentas como Prettier funcionam — transformam AST em código formatado.

⑤ Exercícios rápidos

Teste sua intuição

O que é uma AST (Abstract Syntax Tree)?
Em 1 + 2 * 3, como a precedência de operadores aparece na AST?
Qual é a principal diferença entre lexer e parser?
⑥ Aplicações no mundo real

Onde você encontra isso

⚛️

JSX e Babel

Babel parseia JavaScript/JSX em uma AST (formato estandardizado: ESTree/Babel AST), transforma os nodos (ex: converte JSX em React.createElement), e regenera o código. Todos os transpilers funcionam assim.

🔧

Codemods

Ferramentas como jscodeshift transformam código em escala: "substitua todos os callbacks por async/await", "renomeie esse módulo em 10.000 arquivos". Elas manipulam a AST diretamente — seguro e preciso.

📋

TypeScript checker

TypeScript parseia TypeScript em AST, depois faz type checking na AST — resolve tipos, verifica atribuições, infere genéricos. A AST é a representação central de todo o tooling do TypeScript.

🛡️

SAST (análise estática)

Ferramentas de segurança como Semgrep escrevem regras em "AST patterns": os.system($X) encontra qualquer chamada que passe input dinâmico para o shell — mesmo que o nome da variável mude.

← Anterior: Tokens e Lexers Próxima: Interpretadores →