Tabela de Símbolos
A tabela de símbolos é uma estrutura de dados central do compilador, projetada para gerenciar informações sobre todos os identificadores (variáveis, funções, parâmetros) encontrados no código-fonte. A sua principal função é dar suporte à análise semântica, permitindo a verificação de escopos, a detecção de redeclarações e a inferência de tipos.
Adicionalmente, ela é uma peça fundamental para a etapa de geração de código, fornecendo os tipos de dados necessários para a tradução do código-fonte para a linguagem C. A implementação utiliza uma hash table com tratamento de colisões por encadeamento e uma estrutura de pilha para gerenciar escopos aninhados, refletindo a natureza de linguagens como Python.
Estrutura de Dados
A implementação é baseada em duas structs
principais: Simbolo
, que armazena os dados de um identificador individual, e TabelaSimbolos
, que representa um escopo contendo múltiplos símbolos.
struct Simbolo
Esta estrutura foi enriquecida para armazenar metadados detalhados sobre cada identificador, essenciais para as fases de análise e tradução.
typedef struct Simbolo {
char* nome; // Nome do identificador (ex: "minha_variavel").
char* tipo; // Categoria do símbolo (ex: "variavel", "funcao", "param").
char* tipo_simbolo; // Tipo de dado do símbolo (ex: "int", "float", "char*", "bool").
bool foi_traduzido; // Flag para controlar a geração de código C.
char* tipo_retorno_funcao; // Armazena o tipo de retorno, aplicável apenas a funções.
struct Simbolo* proximo; // Ponteiro para o próximo símbolo em caso de colisão na hash.
} Simbolo;
struct TabelaSimbolos
Representa um único escopo. A ligação com escopos pais é feita através do ponteiro anterior
, formando uma pilha de escopos.
typedef struct TabelaSimbolos {
Simbolo* tabela[TAM_TABELA]; // A hash table (array de ponteiros para Símbolos).
struct TabelaSimbolos* anterior; // Ponteiro para o escopo pai (superior).
} TabelaSimbolos;
Funcionalidades
Função | Descrição |
---|---|
criar_tabela() |
Aloca e inicializa uma nova tabela de símbolos (um novo escopo). |
destruir_tabela(tabela) |
Libera toda a memória associada a uma tabela e seus símbolos. |
inserir_simbolo(tabela, nome, tipo, tipo_simbolo) |
Adiciona um novo símbolo ao escopo atual, incluindo sua categoria e tipo de dado. |
buscar_simbolo(tabela, nome) |
Busca um símbolo no escopo atual e, se não encontrar, continua a busca recursivamente nos escopos pais até o escopo global. |
buscar_simbolo_no_escopo_atual(tabela, nome) |
Busca um símbolo apenas no escopo fornecido, sem subir para os escopos pais. Essencial para detectar redeclarações. |
empilhar_escopo(tabela_atual) |
Cria um novo escopo (tabela) cujo escopo pai é tabela_atual . |
desempilhar_escopo(tabela_atual) |
Descarta o escopo atual e retorna o ponteiro para o escopo pai. |
imprimir_tabela(tabela) |
Exibe o conteúdo de todos os escopos de forma aninhada para fins de depuração. |
Integração e Análise Semântica
A tabela de símbolos é gerenciada primariamente pelo analisador sintático (parser.y
), que a utiliza para realizar a análise semântica.
Gerenciamento de Escopo
O parser cria e destrói escopos para refletir os blocos de código da linguagem. Um novo escopo é empilhado ao entrar em um bloco (INDENT
) e desempilhado ao sair (DEDENT
). A gramática que gerencia isso é a regra block
:
// parser.y
block:
INDENT stmt_list DEDENT {
$$ = $2;
escopo_atual = desempilhar_escopo(escopo_atual); // Desempilha ao final do bloco
}
;
// A regra de definição de função, por exemplo, empilha o escopo antes de processar o bloco
def_stmt:
DEF ID LPAREN param_list RPAREN COLON {
escopo_atual = empilhar_escopo(escopo_atual); // Empilha para o corpo da função
}
block
...
;
Declaração de Variáveis com Inferência de Tipo
Esta é a funcionalidade semântica mais importante. Quando uma atribuição (=
) é reconhecida, o parser realiza as seguintes ações:
- Verifica se a variável já existe no escopo atual usando
buscar_simbolo_no_escopo_atual
. - Se a variável é nova, o parser chama a função
deduzir_tipo_expr
para analisar a expressão à direita do=
e inferir seu tipo de dado (e.g., uma soma de inteiros resulta emTIPO_INT
, uma divisão resulta emTIPO_FLOAT
). - Finalmente, insere a nova variável na tabela de símbolos com o tipo de dado inferido.
// parser.y - Regra de atribuição
assignment_stmt:
ID ASSIGN expr {
$$ = criarNoOp('=', criarNoId($1), $3);
Simbolo* s = buscar_simbolo_escopo_atual(escopo_atual, $1);
if (!s) { // Só insere se for a primeira vez neste escopo
char* tipo_deduzido = "int"; // Padrão
int tipo_expr = deduzir_tipo_expr($3);
if (tipo_expr == TIPO_FLOAT) {
tipo_deduzido = "float";
} else if (tipo_expr == TIPO_STRING) {
tipo_deduzido = "char*";
} else if (tipo_expr == TIPO_BOOL) {
tipo_deduzido = "bool";
}
// Insere com o tipo de dado correto!
inserir_simbolo(escopo_atual, $1, "variavel", tipo_deduzido);
}
}
;
Uso na Geração de Código
A tabela de símbolos é passada para o módulo de geração de código (gerar_codigo.c
), onde é usada para traduzir o código para C. O campo foi_traduzido
é fundamental aqui para lidar com a declaração de variáveis em C.
Em C, uma variável deve ser declarada (int x;
) antes de seu primeiro uso. Para simular isso, o gerador de código, ao encontrar uma atribuição, consulta a tabela:
- Se
s->foi_traduzido
forfalse
, significa que esta é a primeira vez que a variável aparece. O gerador então imprime a declaração do tipo (ex:int
) antes da atribuição e marca o flag comotrue
. - Nas próximas vezes que a mesma variável for usada, o flag estará
true
, e o gerador de código não imprimirá a declaração de tipo novamente.
// gerar_codigo.c - Trecho da geração de atribuição
if (node->operador == '=') {
if (node->esquerda && node->esquerda->tipo == TIPO_ID) {
Simbolo* s = buscar_simbolo(tabela, node->esquerda->nome);
// Se o símbolo existe e ainda não foi traduzido/declarado
if (s && !s->foi_traduzido){
fprintf(out, "%s ", s->tipo_simbolo); // Imprime "int", "float", etc.
s->foi_traduzido = true; // Marca como traduzido
}
}
gerar_codigo_c(node->esquerda, out, tabela);
fprintf(out, " = ");
gerar_codigo_c(node->direita, out, tabela);
}
Histórico de Versões
Data | Versão | Descrição | Autor | Revisor |
---|---|---|---|---|
02/06/2025 | 1.0 | Adiciona versão inicial do documento de tabela de símbolos. | Brunna Louise | Mariana Letícia |
27/06/2025 | 1.1 | Atualiza documentação para refletir estado atual do código. | Brunna Louise | Genilson Silva |