Você está na página 1de 8

COMPILADORES

4. TABELA DE SÍMBOLOS & ANÁLISE SEMÂNTICA

INTRODUÇÃO

Um compilador usa uma tabela de símbolos (ts) para guardar informações


sobre os nomes declarados em um programa. A ts é pesquisada cada vez que um
nome é encontrado no programa fonte. Alterações são feitas na ts sempre que um
novo nome ou nova informação sobre um nome já existente é obtida.

A gerência da ts de um compilador deve ser implementada de forma a permitir


inserções e consultas da forma mais eficiente possível, além de permitir o
crescimento dinâmico da mesma.

ENTRADAS NA TABELA DE SÍMBOLOS

Cada entrada na ts é a declaração de um nome. O formato das entradas pode


não ser uniforme (embora seja mais fácil manipular entradas uniformes) porque as
informações armazenadas para cada nome podem variar de acordo com o tipo/uso
do nome.

Cada entrada na ts pode ser implementada como um registro ("record" ou


"struct") contendo campos (nome, tipo, classe, tamanho, escopo, etc.) que a
qualificam.

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 1

COMPILADORES
Se o número máximo de caracteres em um nome for limitado e pequeno (até 16
caracteres), o campo nome de uma entrada da ts pode ser simplesmente um arranjo
de caracteres (alocação estática).

Se o número de caracteres de um nome for grande (maior que 16), o campo


nome de uma entrada da ts deve ser um apontador para uma área alocada
dinamicamente.

Figura 4.1: Armazenando nomes na tabela de símbolos

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 2
COMPILADORES

ALOCAÇÃO DE POSIÇÕES DE MEMÓRIA

Informação sobre que endereços de memória serão associados aos nomes


(variáveis) deve ser guardada na ts.

No caso de nomes globais, a estratégia é muito simples: ao primeiro nome


global, associa-se o endereço relativo 0 (zero); ao segundo, o endereço 0 + ‘espaço
ocupado pelo primeiro nome global’; e assim sucessivamente.

No caso de nomes locais (e parâmetros de subprogramas), a alocação é feita


geralmente em uma pilha e o endereçamento pode ser feito de forma semelhante ao
usado com os nomes globais, lembrando-se que o endereço relativo 0 (zero) é, na
realidade, 0 + valor do apontador de pilha na hora de execução do programa.

IMPLEMENTAÇÃO DA TABELA DE SÍMBOLOS

Podemos implementar uma tabela de símbolos como uma lista linear de


registros (mais simples e mais fácil, como mostrado na figura 4.1), onde os nomes
novos são inseridos no fim e a pesquisa sempre se processa do fim para o início.

Convém fazermos algumas observações com relação à busca e inserção de


nomes da ts.

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 3

COMPILADORES

Sempre que encontramos um nome de identificador em um programa fonte, ele


pode estar sendo declarado ou sendo usado pelo programador.

Se estiver sendo declarado temos de analisar duas situações:

• em uma linguagem com estrutura de blocos, devemos pesquisar a ts do fim


até o ponto onde começaram a ser inseridos os símbolos do bloco mais
interno;

• em uma linguagem sem estrutura de blocos, devemos pesquisar a ts do fim


até o início.

Em ambos os casos, o nome não pode estar na ts (se estiver é um símbolo sendo
redeclarado), devendo ser inserido.

Se estiver sendo usado pelo programador, devemos pesquisar a ts do fim até o


início; se não for encontrado é um símbolo indefinido.

Em uma tabela com n entradas, podemos considerar que, na média,


pesquisamos n / 2 nomes para verificar se um dado nome está ou não na ts.

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 4
COMPILADORES

Dado que inserções e consultas à ts levam um tempo proporcional a n, o tempo


total para inserir n nomes e fazer e consultas à ts pode ser considerado

c*n*(n+e)

onde c é uma constante representando o tempo médio necessário para uma


inserção ou consulta (o tempo de pesquisar n / 2 entradas).

Em um programa de tamanho médio, poderíamos ter


n = 100 e e = 1000, de modo que

c * 100 * ( 100 + 1.000 ) = c * 1.100.00

Se c fosse, por exemplo, 0,001 segundo, então o tempo total gasto na gerência
da ts seria de 110 segundos. Pode parecer pouco, mas se considerarmos agora que
n e e foram multiplicados por 10, teríamos o custo multiplicado por 100!

Por essa razão, em geral tabelas de símbolos são implementadas como tabelas
de acesso direto ("hash table").

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 5

COMPILADORES
Uma tabela de acesso direto consiste basicamente de:

1. um arranjo de tamanho fixo com m apontadores para entradas de tabela


(idealmente um número primo);

2. entradas de tabela organizadas em m listas encadeadas separadas,


chamadas buques ("buckets"). Cada entrada da tabela de símbolos aparece
em somente uma dessas listas.

Figura 4.2: Tabela hash de tamanho 100


© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 6
COMPILADORES

Em um tabela hash, o tempo de realização de n inserções e e consultas pode


ser considerado

c*n*(n+e)/m

onde c é uma constante representando o tempo médio necessário para uma


inserção ou consulta.

Desde que m pode ser feito tão grande quanto desejarmos, até mesmo n, esse
método é, via de regra, mais eficiente que listas lineares e é usado na maioria das
implementações de compiladores.

Como exemplo, indicamos abaixo uma função hash boa na maioria das
aplicações de tabelas de símbolos.

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 7

COMPILADORES

Algoritmo Função Hash

Entrada: cadeia s contendo símbolo


Saída: inteiro h contendo índice na tabela hash

início
PRIMO = elementos na tabela de índices;
h = g = 0;

para cada caracter p na cadeia s faça {


h = ( h << 4 ) + p;

g = h .AND. 0xF0000000

se g != 0 então faça {
h = h .XOR. ( g >> 24 );
h = h .XOR. g;
}
}

h = h .MOD. PRIMO;
fim

OBS. << indica deslocamento lógico à esquerda


>> indica deslocamento lógico à direita

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 8
COMPILADORES

ANÁLISE SEMÂNTICA – COMPATIBILIDADE DE TIPOS

Nós já vimos como é feita a verificação semântica de identificadores com


relação à tabela de símbolos. Vamos ver agora como se faz a verificação semântica
de compatibilidade de tipos em expressões e comandos de atribuição.

EXPRESSÕES ARITMÉTICAS

Em uma expressão aritmética do tipo b + c * d, precisamos verificar se:

a) c * d pode ser feito e qual o tipo do resultado produzido;

b) b + ( c * d ) pode ser feito e qual o tipo do resultado produzido.

Uma forma simples de fazer tal verificação é associar ações semânticas às


regras que geram expressões de tal forma que, a cada aplicação de uma regra que
gera uma operação aritmética básica (soma, subtração, multiplicação e divisão, por
exemplo), seja verificada a compatibilidade dos tipos dos operandos envolvidos e
se determine o tipo do resultado para uso posterior. O armazenamento dos tipos
resultantes intermediários é feito em uma estrutura de dados tipo pilha chamada de
Pilha de Controle de Tipos (PcT).

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 9

COMPILADORES
Vejamos um exemplo com a gramática abaixo:

EA ::= T E'
E' ::= + T E' | &
T ::= F T'
T' ::= * F T' | &
F ::= ident | número_inteiro | número_real | ( EA )

Quando o analisador sintático aplicar uma das regras

F ::= ident
F ::= número_inteiro
F ::= número_real

nós sabemos o tipo de cada operando possível (de identificador obtém-se com
consulta à tabela de símbolos, de número é informado pelo analisador léxico).
Vamos simplesmente empilhar o tipo do operando na PcT. A ação semântica
associada a cada regra seria empilhar o tipo do operador na PcT.

Quando o analisador sintático aplicar uma das regras

E' ::= +T E'


T' ::= * F T'

sabemos que uma operação básica de adição/multiplicação foi reconhecida e


podemos verificar a compatibilidade de tipos analisando o topo e subtopo da PcT.
© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 10
COMPILADORES

A ação semântica associada às operações + e * poderia ser:

se topo = inteiro E subtopo = inteiro então


resultado = inteiro
senão se topo = real E subtopo = real então
resultado = real
senão Erro: incompatibilidade de tipos

para linguagens de programação que não admitem misturas de tipos como Pascal.

Se a linguagem admite mistura de tipos, como C, todas as possíveis


combinações devem ser analisadas:

caracter caracter caracter


caracter inteiro inteiro
caracter real real
inteiro caracter inteiro
inteiro versus inteiro resulta inteiro
inteiro real real
real caracter real
real inteiro real
real real real

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 11

COMPILADORES

As regras ficariam:

E' ::= +T E' <AS VerifTipo> ou


T' ::= * F T' <As VerifTipo>

Em termos da PcT, a ação semântica seria:

tipo1 = pop(PcT);
tipo2 = pop(PcT);

se tipo1 = tipo2 = inteiro então


push( PcT, inteiro )
senão se tipo1 = tipo2 = real então
push( PcT, real )
senão Erro: incompatibilidade de tipos

Seguindo-se essa estrutura, ao final da análise de uma expressão aritmética


teremos no topo da PcT (que, se não houver erros de sintaxe na expressão, deve ser
a base) o tipo resultante de toda a expressão.

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 12
COMPILADORES

COMANDOS DE ATRIBUIÇÃO

Fica fácil, agora, verificar a compatibilidade de tipos em comandos de


atribuição do tipo:

<atrib> ::= ID = <ExpressãoAritmética>

comparando-se o tipo resultante na PcT com o tipo do identificador.

A regra ficaria:

<atrib> ::= ID <AS SalvaTipo> = <ExprAritmética> <AS VerifTipoAtrib>

Onde AS_SalvaTipo salva em uma temporário o tipo do identificador, para posterior


comparação com o tipo resultante da expressão, através de AS_VerifTipoAtrib.

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 13

COMPILADORES

EXPRESSÕES RELACIONAIS

Podemos considerar expressões relacionais como:

<ER> ::= <EA> <OpRel> <EA>


<OpRel> ::= < | <= | > | >= | != | =

A verificação de tipos pode ser feita com o uso de duas ações semânticas:

<ER> ::= <EA> <AS1> <OpRel> <EA> <AS2>

<AS1> ::= { tipo1 = tipo da última EA vista }

<AS2> ::= { compare tipo1 com o tipo da última EA


vista}

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 14
COMPILADORES

EXPRESSÕES LÓGICAS

Expressões lógicas podem ser geradas com:

<EL> ::= <TL> <EL'>


<EL'> ::= OR <TL> <EL'> | &
<TL> ::= <FL> <TL'>
<TL'> ::= AND <FL> <TL'> | &
<FL> ::= <ER> | ( <EL> )

Não há verificação de tipos p/ expressões lógicas. O tipo resultante sempre é


verdadeiro ou falso.

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 15

COMPILADORES

COMANDOS DIVERSOS

Muitas linguagens de programação impõe restrições semânticas para outros


comandos. Por exemplo, o comando FOR em Pascal somente admite indexação por
meio de variáveis inteiras.

<cmdFor> ::= FOR ID = <ea> <passo> <ea> DO <cmd>


<passo> ::= TO | DOWNTO

Ações semânticas possíveis seriam:

<cmdFor> ::= FOR ID <AS_1> = <ea> <AS_2> <passo> <ea> <AS_2> DO <cmd>
<passo> ::= TO | DOWNTO

Onde AS_1 verifica se o tipo do identificador é inteiro e AS_2 verifica se o tipo


resultante as <ea> anterior é inteiro.

Outros comandos de uma linguagem de programação podem exigir outros


tipos de verificação semântica.

© UFCG / DSC / PSN, 2005 – Parte 4: Tabela de Símbolos e Análise Semântica – Pág. 16

Você também pode gostar