Projeto do Compilador - Tabela de Símbolos
A tabela de símbolos é uma estrutura de dados importante criada e mantida por compiladores para armazenar informações sobre a ocorrência de várias entidades, como nomes de variáveis, nomes de funções, objetos, classes, interfaces, etc. A tabela de símbolos é usada tanto pela análise quanto pela síntese partes de um compilador.
Uma tabela de símbolos pode servir aos seguintes propósitos, dependendo do idioma em mãos:
Para armazenar os nomes de todas as entidades em um formulário estruturado em um só lugar.
Para verificar se uma variável foi declarada.
Para implementar a verificação de tipo, verificando as atribuições e expressões no código-fonte estão semanticamente corretas.
Para determinar o escopo de um nome (resolução de escopo).
Uma tabela de símbolos é simplesmente uma tabela que pode ser linear ou uma tabela hash. Ele mantém uma entrada para cada nome no seguinte formato:
<symbol name, type, attribute>
Por exemplo, se uma tabela de símbolos tiver que armazenar informações sobre a seguinte declaração de variável:
static int interest;
então ele deve armazenar a entrada, como:
<interest, int, static>
A cláusula de atributo contém as entradas relacionadas ao nome.
Implementação
Se um compilador deve lidar com uma pequena quantidade de dados, a tabela de símbolos pode ser implementada como uma lista não ordenada, que é fácil de codificar, mas só é adequada para pequenas tabelas. Uma tabela de símbolos pode ser implementada de uma das seguintes maneiras:
- Lista linear (classificada ou não)
- Árvore de pesquisa binária
- Tabela de hash
Entre todas, as tabelas de símbolos são implementadas principalmente como tabelas hash, onde o próprio símbolo do código-fonte é tratado como uma chave para a função hash e o valor de retorno é a informação sobre o símbolo.
Operações
Uma tabela de símbolos, linear ou hash, deve fornecer as seguintes operações.
inserir()
Esta operação é mais frequentemente utilizada pela fase de análise, ou seja, a primeira metade do compilador onde os tokens são identificados e os nomes são armazenados na tabela. Esta operação é usada para adicionar informações na tabela de símbolos sobre nomes exclusivos que ocorrem no código-fonte. O formato ou estrutura em que os nomes são armazenados depende do compilador em mãos.
Um atributo para um símbolo no código-fonte é a informação associada a esse símbolo. Essas informações contêm o valor, estado, escopo e tipo do símbolo. A função insert () pega o símbolo e seus atributos como argumentos e armazena as informações na tabela de símbolos.
Por exemplo:
int a;
deve ser processado pelo compilador como:
insert(a, int);
olho para cima()
A operação lookup () é usada para pesquisar um nome na tabela de símbolos para determinar:
- se o símbolo existe na tabela.
- se for declarado antes de ser usado.
- se o nome for usado no escopo.
- se o símbolo for inicializado.
- se o símbolo for declarado várias vezes.
O formato da função lookup () varia de acordo com a linguagem de programação. O formato básico deve corresponder ao seguinte:
lookup(symbol)
Este método retorna 0 (zero) se o símbolo não existir na tabela de símbolos. Se o símbolo existe na tabela de símbolos, ele retorna seus atributos armazenados na tabela.
Gerenciamento do escopo
Um compilador mantém dois tipos de tabelas de símbolos: a global symbol table que pode ser acessado por todos os procedimentos e scope symbol tables que são criados para cada escopo no programa.
Para determinar o escopo de um nome, as tabelas de símbolos são organizadas em estrutura hierárquica, conforme mostrado no exemplo abaixo:
. . .
int value=10;
void pro_one()
{
int one_1;
int one_2;
{ \
int one_3; |_ inner scope 1
int one_4; |
} /
int one_5;
{ \
int one_6; |_ inner scope 2
int one_7; |
} /
}
void pro_two()
{
int two_1;
int two_2;
{ \
int two_3; |_ inner scope 3
int two_4; |
} /
int two_5;
}
. . .
O programa acima pode ser representado em uma estrutura hierárquica de tabelas de símbolos:
A tabela de símbolos globais contém nomes para uma variável global (valor int) e dois nomes de procedimento, que devem estar disponíveis para todos os nós filhos mostrados acima. Os nomes mencionados na tabela de símbolos pro_one (e todas as suas tabelas filho) não estão disponíveis para os símbolos pro_two e suas tabelas filho.
Esta hierarquia da estrutura de dados da tabela de símbolos é armazenada no analisador semântico e sempre que um nome precisa ser pesquisado em uma tabela de símbolos, ele é pesquisado usando o seguinte algoritmo:
primeiro, um símbolo será pesquisado no escopo atual, ou seja, na tabela de símbolos atual.
se um nome for encontrado, a pesquisa é concluída, caso contrário, ele será pesquisado na tabela de símbolos pai até,
o nome foi encontrado ou a tabela de símbolos global foi pesquisada para o nome.