Aula 15: Análise Semântica: Tabelas de Símbolos

Data de Publicação

25/03/2026

Data de Modificação

08/03/2026

Objetivos

Ao final desta aula, você será capaz de:

  • Definir o papel da tabela de símbolos como repositório de informações sobre identificadores (tipo, categoria, escopo, endereço).
  • Implementar escopo léxico com pilha de tabelas ou com árvore de escopos.
  • Inserir e consultar símbolos (variáveis, parâmetros, funções) em blocos, procedimentos e funções.
  • Relacionar a tabela de símbolos com a análise semântica e com o projeto Mini-Pascal (Etapa 5).

Motivação em Engenharia de Computação

Sem uma tabela de símbolos adequada, o compilador não consegue:

  • Detectar uso antes de declaração, sombreamento indevido ou conflitos de nome.
  • Representar a estrutura de módulos (visibilidade entre arquivos, namespaces).
  • Fornecer informações para depuração (mapear endereços de memória de volta para nomes de variáveis e funções).

Para IDEs, debuggers e analisadores estáticos, a tabela de símbolos é a peça que conecta código fonte, IR e informações de runtime.

Onde a tabela de símbolos atua no pipeline

flowchart LR
  AST[AST] --> Sem[Análise semântica]
  Sem --> Tab[Tabela de símbolos]
  Sem --> ASTd[AST decorada]
  Tab --> TypeCheck[Verificação de tipos]
  Tab --> Codegen[Geração de código]

A análise semântica percorre a AST (por exemplo, em uma ou mais passadas): ao encontrar declarações, insere entradas na tabela; ao encontrar usos de identificadores, consulta a tabela para obter tipo, categoria e escopo. A tabela é usada tanto para verificação de tipos (Etapa 6) quanto para geração de código (endereços, offsets).

Conteúdo

1. O que é a tabela de símbolos?

A tabela de símbolos é o “banco de dados” do compilador. Ela mapeia nomes (strings) em informações associadas a cada símbolo.

Cada entrada (símbolo) pode conter, conforme a linguagem e a fase:

Atributo Descrição
Nome Identificador (lexema).
Tipo Tipo de dado (int, float, array, registro, ponteiro, função).
Categoria Variável, parâmetro, função, procedimento, constante, tipo.
Escopo Em qual bloco/função/módulo o símbolo foi declarado.
Endereço Offset na pilha, endereço global ou rótulo (para codegen).
Tamanho Tamanho em bytes (para alocação).
Outros Número de parâmetros, tipo de retorno (para funções); dimensões (para arrays).

A implementação típica usa tabela hash por nome dentro de cada escopo, permitindo inserção e busca em tempo esperado \(O(1)\).

2. Gerenciamento de escopo

A maioria das linguagens usa escopo léxico (estático): o escopo de uma variável é determinado pela estrutura do código fonte (blocos, funções, módulos), não pela ordem de execução.

Pilha de tabelas

Uma forma simples e eficiente de implementar escopo léxico:

flowchart TB
  subgraph pilha [Pilha de tabelas]
    T1[Tabela global]
    T2[Tabela função f]
    T3[Tabela bloco interno]
    T1 --> T2 --> T3
  end
  Busca[Busca: do topo à base]
  • Ao entrar em um bloco (função, begin/end, {/}): empilhar uma nova tabela (inicialmente vazia).
  • Ao declarar um nome: inserir na tabela do topo da pilha.
  • Ao usar um nome: buscar do topo para a base; a primeira ocorrência encontrada é a declaração que rege aquele uso (sombreamento).
  • Ao sair do bloco: desempilhar a tabela do topo (as variáveis locais “desaparecem”).

Assim, nomes em blocos mais internos sombreiam nomes de blocos externos; e cada bloco “vê” apenas os nomes do próprio bloco e dos blocos que o contêm.

Árvore de escopos

Alternativa: manter uma árvore de escopos, em que cada nó é um escopo e tem ponteiro para o escopo pai. Inserção continua no escopo atual; a busca sobe pela árvore até encontrar o nome ou chegar à raiz. A árvore de escopos é útil quando se precisa de informações de escopo após o fim do bloco (ex.: para um debugger que lista variáveis visíveis em um frame).

flowchart TB
  Global[Escopo global] --> F1[Escopo f]
  Global --> F2[Escopo g]
  F1 --> B1[Bloco 1]
  B1 --> B2[Bloco 2]
  Busca2[Busca: sobe até a raiz]

3. Inserção e busca: exemplos

Inserção: ao processar var x: integer;, o compilador cria uma entrada com nome x, tipo integer, categoria variável, escopo atual, e (em uma passada posterior ou na geração de código) atribui um offset ou endereço.

Busca: ao processar x := 1;, o compilador busca x na tabela (do escopo atual para os pais). Se encontrar, usa o tipo para verificação e o endereço para codegen; se não encontrar, reporta “identificador não declarado”.

Conflito: em algumas linguagens, redeclaração no mesmo escopo é proibida; ao inserir, se o nome já existir no escopo atual, reporta erro.

4. Estrutura de dados sugerida

Uma implementação típica em linguagem de alto nível:

  • Escopo: um mapa (hash) de nome → Símbolo (objeto com tipo, categoria, escopo, endereço, etc.).
  • Pilha de escopos: lista ou pilha de “Escopo”; o topo é o escopo atual.
  • Funções: openScope(), closeScope(), insert(name, symbol), lookup(name) (busca do topo à base), opcionalmente lookupLocal(name) (apenas no escopo atual).

Para funções e procedimentos, a entrada na tabela pode conter uma “sub-tabela” de parâmetros ou um ponteiro para o escopo da função, usado na verificação de chamadas e na geração de código.

5. Exemplo de fluxo (Mini-Pascal simplificado)

Trecho:

program p;
var x: integer;
procedure f;
  var y: integer;
  begin
    x := 1;
    y := 2;
  end;
begin
  x := 0;
  f;
end.
  • Ao processar var x: inserir x no escopo global.
  • Ao processar procedure f: abrir escopo de f, inserir f no escopo global (categoria procedimento).
  • Ao processar var y: inserir y no escopo de f.
  • Em x := 1: busca x → encontra no escopo global.
  • Em y := 2: busca y → encontra no escopo de f.
  • Ao sair de f: fechar escopo de f.
  • Em x := 0: busca x → encontra no escopo global.

Conexões com o projeto Mini-Pascal

  • Na Etapa 5 você implementa a construção e o uso da tabela de símbolos: abrir/fechar escopos ao percorrer a AST (entrada em blocos, procedimentos, funções), inserir variáveis e parâmetros nas declarações, e fazer lookup nos usos.
  • Um design limpo separa: (1) a estrutura de escopos (pilha ou árvore) e (2) as passadas que a utilizam (preenchimento na primeira passada, verificação de tipos e resolução de nomes em passadas seguintes).
  • A tabela de símbolos será a base da Etapa 6 (verificação de tipos) e da Etapa 7 (endereços/offsets para o bytecode).

Resumo

  • A tabela de símbolos mapeia nomes em atributos (tipo, categoria, escopo, endereço).
  • Escopo léxico é implementado com pilha de tabelas (abrir/fechar escopo, buscar do topo à base) ou árvore de escopos (busca até a raiz).
  • Inserção em declarações, busca em usos; conflitos e “não declarado” são reportados na análise semântica.
  • A tabela é usada por verificação de tipos e geração de código.

Perguntas de reflexão

  • Como você representaria sobrecarga de funções (mesmo nome, assinaturas diferentes) na tabela de símbolos?
  • Que informações adicionais (além de tipo e categoria) podem ser úteis para otimizações ou para um debugger?
  • Em uma linguagem com blocos aninhados mas sem funções, a pilha de tabelas e a árvore de escopos são equivalentes para busca? E para escopo após o fim do bloco?

Referências

  • Aho et al. (2006). Compilers: Principles, Techniques, and Tools. Cap. 2 (tabela de símbolos) e capítulos de análise semântica.
  • Cooper & Torczon (2011). Engineering a Compiler.

Materiais da aula

Última atualização: 08/03/2026

De volta ao topo