Aula 15: Análise Semântica: Tabelas de Símbolos
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), opcionalmentelookupLocal(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: inserirxno escopo global. - Ao processar
procedure f: abrir escopo def, inserirfno escopo global (categoria procedimento). - Ao processar
var y: inseriryno escopo def. - Em
x := 1: buscax→ encontra no escopo global. - Em
y := 2: buscay→ encontra no escopo def. - Ao sair de
f: fechar escopo def. - Em
x := 0: buscax→ 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