Sei sulla pagina 1di 29

1

TABELLA DEI SIMBOLI


Structure of a Compiler
Scanner Parser
Semantic
Analyzer
Code
Generator
Optimizer
SYMBOL TABLE
2
Ruolo della tabella dei simboli
La tabella dei simboli una delle pi importanti
strutture dati di un compilatore
Creata dal front-end per contenere le
informazioni sul programma oggetto
IR si riferisce agli oggetti tramite puntatori alle
voci corrispondenti nella tabella di simboli
Questo fatto fin quasi alla fine della compilazione
Cos la tabella dei simboli utilizzata durante
lintera trasformazione
Il contenuto pu essere modificato nel processo di
ottimizzazione
Scope
Una (sintatticamente definita) allinterno di
un programma nella quale gli identificatori
(I nomi) possono essere dichiarati..
E.g. funzioni, istruzioni blocco.
Gli scope possono essere innestati in alcuni linguaggi
3
Alcune considerazioni ..
Static scope in linguaggi strutturati a blocchi
Lo scope di un identificatore include il blocco di
definizione e i blocchi in esso contenuti che non
contengono una ulteriore dichiarazione
dellidentificatore
Nested declaration di funzioni
Direttive per la promozione ad un scope globale
Block Scoping of Variables
{
int x, y, z ;
x y z /* some code */
{
int y ;
. x y z . /* some more code */
{
int z ;
x = y + z ; */
In generale: al punto p
quale dichiarazione di x
valida? Come il compilatore
tiene traccia di ci?
4
Block Scoping of Variables
Blocchi nested
Se la variabile x utilizzata nel blocco B e dichiarata in B allora la
sua dichiarazione utilizzata.
Altrimento se x dichiarata nel blocco C esterno a B ma non in nessun
altro blocco allinterno di C ed esterno a B viene utilizzata la
dichiarazione di x in C
{ /* this is block C */
int x, y, z ;
x y z /* some code */
{ /* this is block D */
int y ;
. x y z . /* some code */
{ /* this is block B */
int z ;
x = y + z ; /* which ones are used??? */
Ogni differente variabile
richiede una propria entry
nella symbol table. Cio
avremo pi entri per x etc.
Scope of Variable Declaration
Static Scope
Lo scope determinato dalle relazioni di nesting del programma sorgente,
e.g. C, JAVA, Pascal.
Dynamic scope
Lo scope determinato dalle relazioni che scaturiscono dalle chiamante a
run time. E.g. Lisp.
5
Lexical vs. Dynamic Scoping
program whichone (input , output ) ;
var r : real ;
procedure show ;
begin write ( r : 5:3 ) end ;
procedure small ;
var r : real ;
begin r := 0.125 ; show end ;
begin
r := 0.25 ;
show ; small ; writeln ;
end
Questo
programma
chiama
show ; small
?
Lexical vs. Dynamic Scoping
program whichone (input , output ) ;
var r : real ;
procedure show ;
begin write ( r : 5:3 ) end ;
procedure small ;
var r : real ;
begin r := 0.125 ; show end ;
begin
r := 0.25 ;
show ; small ; writeln ;
end
output? ?
Con static
scoping,
0.250 0.250.
Con dynamic
scoping,
0.250 0.125.
6
Lifetime of Variable
Lifetime is the time during execution of a program when a
given variable first becomes visible to when it is last visible.
Also called extent.
The lifetime of a global variable covers the entire
execution (unless it is temporarily superseded in some
places).
The lifetime of a local or automatic variable is usually an
activation of the program unit within which it is declared.
Some languages permit variables to be local to blocks
also.
Lifetime and Scope Examples
In C, scope of automatic variable is procedure or block where it is
declared to end of program unit
In PL/1 scope encompasses entire relevant program unit
In Pascal, a variable in outermost scope is visible everywhere in
program except where another variable with same name is declared
(and in the routines it contains)
Fortran has common blocks, static memory that is visible in all routines
where it is declared. Scope encompasses all those routines.
Local static variables in C and SAVEd variables in Fortran have global
lifetime but are only visible in certain regions, so scope may be a file
or a procedure
Dynamic variables have a lifetime that extends from their point of
allocation to the point(s) of their destruction.
7
Interfaccia della tabella dei simboli
openScope()
closeScope()
enterSymbol(nome, tipo)
retrieveSymbol(nome)
declaredLocally(nome)
void builtSymbolTable () {processNode(ASTroot)}
void processNode(node) {
switch (kind(node) {
case block: {symtab.openScope(); break;}
case def: {symtab.enterSymbol(node.name, node.type);
break;}
case ref: {symb = symtab.retriveSymbol(node.name)
if (symb == null) errore;
break:}
} foreach (c : node.getCildren() ) processNode(c)
If (kind(node) == block) symtab.coseScope();
}
8
Contenuto della tabella dei simboli
La tabella dei simboli memorizza informazioni su:
Identificatori
Etichette
Valori numerici (costanti)
Stringhe di caratteri
Variabili generate temporaneamente dal compilatore
Le tabelle di simboli possono essere possono separate (una per ogni scope
o una singola
Linguaggi struttuturati a blocchi e
scope
Regole comuni
I nomi accessibili ad un certo punto di un programma sono
quelli definiti nel current scope e tutti quelli definiti negli
open scope
Se un nome dichiarato in pi di un scope valido quello
nello scope pi interno (innermost scope)
Le nuove dichiarazioni possono essere fatte solo nel current
scope.
Regole singolari
possibile accedere a defiizioni in outrmost global scope
(extern in C, public static field in Java)
9
Una o pi tabelle dei sinboli
Una tabella per ogni scope
Gli scope sono aperti e chiusi con una strategia LIFO
Svantaggi: pu essere necessario ricercare in pi
tabelle un nome
Ununica tabella
Insieme al nome viene inserito nella tabella il nome
dello scope o il livello di profondit dello scope
Pu essere utilizzato uno stack per ogni nome comune, la
ricerca di un nome si limita alla ricerca nel top della
specifica pila
Tecniche delle implementazioni
Lista semplice non ordinara: compatta ma lenta in pratica
O(n) complessita delle ricerca
O (1) complessita dellinserimento
Lista semplice ordinata: semplice, buona se le tabelle sono note in
anticipo
O(log n) complessita delle ricerca binaria
O(n) complessita dellinserimento
Binary search tree: approccio meno semplice
O(log n) complessita media delle ricerca O(n) worst case
Ol(og n) complessita dellinserimento
Albero binario bilanciato
Con un albero bilanciato otteniamo i tempi previsti
Su un ingresso arbitrario l'albero non necessariamente bilanciato
Che cosa succede se le variabili sono in ordine alfabetico
Utilizzare un algoritmo di bilanciamento approssimativa
Complica l'attuazione
Spazio overhead direttamente proporzionale al numero di elementi nella
tabella O
10
Implementazione della tabella dei
simboli
Implementazioni semplici basare su array sono troppo
semplici nei casi reali
Lapproccio pi comune usa un hashing aperto (open
hashing)
La funzione Hash basata su identificativo
Un numero di funzioni sono conosciute
Gli entry della tabella dei simboli usualmente puntano ad
altre strutture per salvare stringhe di caratteri (nome della
variabile, const char )
Consente la stessa dimensione per ogni entry
Hash table
O(1) complessita media delle ricerca
O(1) complessita media dellinserimento
Caso peggiore molto improbabile
Tale approccio adottato nella maggior
parte dei compilatori
O(n + m) spazio dove n il numero di simboli e m il
numero di voci della tabella di hash
11
TABELLE HASH
Le tabelle hash sono strutture di dati che consentono la ricerca, linserzione e in certi
casi la cancellazione di n chiavi in tempomedio costante, e tempo pessimo (n).
Poich tutte le chiavi sono sequenze binarie, esse possono essere interpretate come
numer interi.
Dimensionare la tabella in base al numero di elementi attesi ed utilizzare una
speciale funzione (funzione hash) per indicizzare la tabella
Una funzione hash h ha come dominio linsieme C di tutte le possibili chiavi, e come
codominio linsieme {0, ..., m-1} per un dato intero m. Essa trasforma quindi ogni
possibile chiave K di C in un indirizzo hash h(K) tra 0 e m-1, inteso come posizione
per K in un array A[0..m-1]. Idealmente la chiave K sar memorizzata in A[h(K)].
Una funzione hash quindi una funzione che data una chiave k e C restituisce la
posizione della tabella in cui lelemento con chiave k viene memorizzato
h : C [0, 1, . . .m 1]
N.B: la dimensione m della tabella pu non coincidere con la |C|, anzi in generale
m < |C|
12
Lidea quella di definire una funzione daccesso che
permettadi ottenere la posizione di un elemento in data la sua
chiave
Con lhashing, un elemento con chiave k viene memorizzato
nella cella h(k)
Pro
riduciamo lo spazio necessario per memorizzare la tabella
Contro:
perdiamo la corrispondenza tra chiavi e posizioni in tabella
le tabelle hash possono soffrire del fenomeno delle collisioni
Le chiavi possibili sono in genere moltissime e non note a priori (per
esempio i codici di login in un sistema, o le variabili che un utente inserir in
un programma, o i cognomi dei clienti di una ditta), quindi si ha |C| >> m.
Dunque estremamente complesso definitre una funzione hash che
grarantisca una corrispondenza biunivoca fra chiave e indirizzo hash.
Cio e difficile avere la garanzia che
K1 = K2 h(K1) = h(K2)
Nasce quindi un problema di collisioni: (K1 = K2 si pu avere h(K1) = h(K2))
per solo una delle chiavi (tipicamente la prima che si presentata, diciamo
K1) potr essere allocata in A[h(K1)], e le altre saranno poste altrove.
13
Collisioni
Due chiavi k1 e k2 collidono quando corrispondono alla stessa
posizione della tabella, ossia quando h(k1) = h(k2)
Soluzione ideale: eliminare del tutto le collisioni
scegliendounopportuna (= perfetta) funzione hash. Una
funzione hash si dice perfetta se iniettiva, cio se per ogni
k1, k2 e C K1 = K2 h(K1) = h(K2)
Deve essere |U|<=m
Se |U| > m, evitare del tutto le collisioni impossibile (Ad
es.supponente di dover inserire un nuovo elemento in una
tabella piena)
Problemi da affrontare
Dobbiamo quindi affrontare tre problemi:
1. scegliere la dimensione m;
2. Scegliere la funzione h;
3. Risolvere le collisioni.
14
Scelta di m e definizione del fattore di
carico
In ogni istante si indica con n il numero di chiave
presenti nella tabella: dunque n varia durante
lapplicazione mentre la dimensione m del vettore A
fissata allinizio.
Si definisce il fattore di carico = n/m e si sceglie
m in modo che presumibilmente non superi 0.9: se
ci dovesse accadere si raddoppia la dimensione di
A e si riallocano tutte le chiavi nel nuovo vettore.
In genere m si sceglie come potenza di due o come
numero primo
Funzione hash
Tra tanti metodi ne indichiamo due, usati rispettivamente per m potenza di
2 o per m primo. In entrambi i casi, se si ammette che tutte le sequenze di
bit delle chiavi siano equiprobabili, per ogni chiave K lindirizzo hash h(K)
ha valore tra 0 e m-1 con pari probabilit 1/m. In questo caso la funzione
hash si dice semplicemente uniforme.
1. Poniamo che sia m=2
s
, quindi le posizioni di A sono ndirizzate con s bit.
La sequenza binaria che rappresenta la chiave K divisa in parti di s
bit ciascuna, e tra esse sicalcola lo XOR bit a bit per ottenere una
sequenza di s bit che rappresenta h(K). Si noti che tutti i bit della
chiave contribuiscono a formare h(K).
2. Poniamo che m sia un numero primo. Per ogni chiave K si pone h(K)=K
mod m (cio h(K) il resto della divisione tra K e m).
15
Funzione hash
Una buona funzione hash deve:
1. essere facile da calcolare (costo costante)
2. soddisfare il requisito di uniformit semplice: ogni
chiave deve avere la stessa probabilit di vedersi
assegnata una qualsiasi posizione ammissibile,
indipendentemente da altri valori hash gi
assegnati
Sia P(k) la probabilit che sia estratta una chiave k
tale che h(k) = j , allora
E
k:h(k)=j
P(k) = (1/m) per j = 0, . . . ,m 1
Una buona funzione hash
Il requisito di uniformit semplice difficile da
verificare perch raramente nota la funzione di
distribuzione di probabilit con cui vengono estratte
le chiave (la funzione Pr)
Nella pratica per possibile usare delle
euristiche per realizzare delle funzioni hash con
buone prestazioni:
Metodo della divisione
Metodo della moltiplicazione
16
Metodo della divisione
Consiste nellassociare alla chiave k il valore hash
h(k) = k mod m
Semplice e veloce, ma occorre evitare certi valori di m;
m non dovrebbe essere una potenza di 2
Se m = 2p, h(k) rappresenta solo i p bit meno significativi di k.
Questo limita la casualit di h, in quanto funzione di una
porzione (di dimensione logaritmica) della chiave
Bisogna rendere la funzione h dipendente da tutti i bit della
chiave; una buona scelta per m un numero primo non troppo
vicino ad una potenza di due
Metodo della moltiplicazione
Consiste nellassociare alla chiave k il valore hash
h(k) = m (kA kA)
kA kA la parte frazionaria di kA
Ha il vantaggio che il valore di m non critico; di
solito si sceglie m = 2p
Per quanto riguarda il valore di A, in letteratura
viene suggerito un valore prossimo a ( \5 1)/2
17
Risoluzione delle collisioni
Una possibile alternativa: utilizzare una buona
funzione hash (per minimizzare le collisioni) e
prevedere nel contempo deimetodi di risoluzione
delle collisioni
Metodi classici di risoluzione delle collisioni:
Liste di collisione: gli elementi collidenti sono contenuti in
liste esterne alla tabella; T[i ] punta alla lista di
elementitali che h(k) = i
Indirizzamento aperto: tutti gli elementi sono contenuti
nella tabella; se una cella occupata, se ne cerca
unaltra libera
Risoluzione delle collisioni per
concatenazione (chaining)
Gli elementi collidenti vengono inseriti nella stessa
posizione della tabella in una lista concatenata
18
Costo della ricerca: analisi nel caso
peggiore
Data una tabella A con m posizioni ed n elementi,
quanto tempo richiede la ricerca di un elemento
data la sua chiave?
Caso peggiore: tutte le chiavi vengono inserite nella
stessa posizione della tabella creando ununica lista
di collisione di lunghezza n
In questo caso il tempo di ricerca (n) (ossia il costo
della ricerca nella lista di collisione) + il tempo di
calcolo di h
Costo della ricerca: analisi del caso
medio
Si definisce fattore di carico il rapporto tra il numero n degli
elementi memorizzati e la dimensione m della tabella
o = n/m
Nellipotesi di uniformit semplice della funzione hash o il
numero medio di elementi memorizzati in ogni lista
concatenata
o < 1 molte posizioni disponibili rispetto agli elementi
memorizzati
o = 1 numero di elementi memorizzati uguale alla dimensione
della tabella
o > 1 situazione attesa: molti elementi memorizzati rispetto alla
dimensione della tabella
19
Analisi nel caso medio
Il comportamento nel caso medio dipende da come
la funzione hash distribuisce le chiavi sulle m
posizioni della tabella
Ipotesi:
uniformit semplice della funzione di hash
h(k) calcolata in O(1)
cos che il costo della ricerca di un elemento con chiave
k dipenda esclusivamente dalla lunghezza della lista
A[h(k)]
Indirizzamento Aperto
La rappresentazione non fa uso di puntatori
Le collisioni vengono gestite memorizzando elementi
collidenti in altre posizione della tabella
Invece di seguire le liste di collisione, calcoliamo la
sequenza di posizioni da esaminare
Il fattore di carico non pu mai superare 1
Si usa meno memoria rispetto allar appresentazione
con liste di collisione perch non ci sono puntatori
20
Indirizzamento Aperto
Prevede che si usi solo lo spazio della tabella, senza uso di
zone di trabocco, allocando gli elementi che determinano
collisioni in posizioni diverse da quella che loro competerebbe
Supponiamo di voler inserire un elemento con chiave k e la sua
posizione naturale h(k) sia gi occupata
Cerchiamo la cella vuota (se c) scandendo le celle secondo
una sequenza di indici; ad esempio:
c(k, 0) c(k, 1) . . . c(k,m)
c(k, 0) = h(k) c(k, 1) = h(k) + 1 . . .c(k,m) = h(k) + m
Indirizzamento Aperto
Per inserire una nuova chiave si esamina una
successione di posizioni della tabella, si esegue una
scansione, finch non si trova una posizione vuota in
cui inserire la chiave
La sequenza di posizioni esaminate dipende dalla
chiave che deve essere inserita
Estendiamo la funzione hash in modo che possa
tener conto anche del numero di posizioni gi
esaminate
h : C {0, 1, . . . ,m 1} {0, 1, . . . ,m 1}
21
Operazione di Inserimento
Hash-Insert(T, k) {
i = 0
do {
j = h(k, i )
if (A[j] = null || A[j] = deleted}
{A[j] = k;
return j}
else i = i + 1
}while (i != m)
error overflow sulla tabella hash
Operazione di Ricerca
Hash-Search(T, k){
i = 0
do {
j = h(k, i )
if (A[j ] = k) return j
I = i + 1
} while (A[j ] != null && i != m)
return null
22
Tecniche di scansione: Scansione Lineare
Sia h : C {0, 1, . . . ,m 1} una funzione hash
ordinaria
Il metodo di scansione lineare usa la funzione hash (estesa)
definita come
h(k, i ) =( h(k) + i)mod m
h(k, 0) = h(k) mod m
h(k, 1) = (h(k) + 1) mod m,
h(k, 2) = (h(k) + 2) mod m
La scansione lineare presenta un fenomeno conosciuto come
agglomerazione primaria
Le posizioni occupate della tabella si accumulano per lunghi
tratti, aumentando cos` il tempo medio di ricerca
Tecniche di scansione: Scansione Lineare
Inoltre ...
La prima posizione esaminata determina lintera
sequenza di scansione; quindi abbiamo solo m
sequenze di scansione distinte
Il numero ottimo m! ed dato dallipotesi di
unformit della funzione hash: ognuna delle m!
permutazioni di (h, . . . ,m 1) equiprobabile
Siamo molto lontani dal numero ottimo
23
Tecniche di scansione: Scansione
Quadratica
Sia h : C U {0, 1, . . . ,m 1} una funzione hash ordinaria
Il metodo di scansione quadratica usa la funzione hash (estesa) definita
come
h(k, i ) = (h(k) + c1*i + c2*i
2
)mod m
dove, c1 e c2 sono delle costanti ausiliarie (con c2 != 0)
Un esempio:
h(k, i ) = ( (h(k) + c1*i + c2*i
2
)mod m dove, c1 = c2 = 1
h(k, 0) = h(k),
h(k, 1) = h(k) + 1 + 1 = h0(k) + 2,
h(k, 2) = h(k) + 2 + 4 = h0(k) + 6,
h(k, 3) = h(k) + 3 + 9 = h0(k) + 12,
h(k, 4) = h(k) + 4 + 16 = h0(k) + 20
Cosa succede se m = 20? Viene scandita solo una porzione (in
realt 1/4) della tabella
Elimina il problema dellagglomerazione primaria, ma ...
1. viene usata lintera tabella; solo per alcune combinazioni di
c1, c2 ed m; se m = 2
p
una buona scelta c1 = c2 = 1/2,
perch ivalori h(k, i) per i e [0,m 1] sono tutti distinti
2. h(k1, 0) = h(k2, 0) implica h(k1, i) = h(k2, i)
questo porta ad una forma di addensamento (pi lieve
rispetto a quella primaria) detta agglomerazione secondaria
di nuovo, la prima posizione determina lintera sequenza di
scansione ed abbiamo solo m sequenze di scansione distinte
24
Hashing Doppio
Lhashing doppio usa una funzione hash (estesa) della forma
h(k, i ) = (h
1
(k) + ih
2
(k))mod m
dove h
1
, h
2
sono delle funzioni hash (ordinarie) ausiliarie
La prima posizione esaminata A[h
1
(k)] mod m; ogni posizione esaminata
successivamente distanziata dalla precedente di una quantit h
2
(k) mod
m
La sequenza di scansione dipende da k in due modi: a seconda della
chiave, possono variare sia la posizione iniziale che il passo
Lhashing doppio non soffre di fenomeni di agglomerazione perch il passo
casuale
inoltre ...
Ogni possibile coppia (h
1
(k), h
2
(k)) produce una sequenza discansione
distinta: abbiamo O(m
2
) sequenze di scansione distinte ed in questo senso
migliore sia della scansione lineare che quadratica
Hashing Doppio
Il valore di h
2
(k) deve essere primo con la dimensione m della
tabella per ogni chiave da cercare
Infatti se MCD(h
2
(k),m) = d > 1 per qualche k, allora la ricerca
di tale chiave andrebbe ad esaminare solo una porzione
(1/d) della tabella
Se m una potenza di 2, basta definire h
2
in maniera tale che
restituisca sempre un numero dispari
Altro modo scegliere m primo e porre h
1
(k) = k mod m,
h
2
(k) = 1 + (k mod m0) dove m0 appena pi piccolo di m
(ad esempio m0 = m 1 oppure m0 = m 2)
25
Two level hash table
Spazio dei nomi
Il nome di un simbolo non muta durante la compilazione
Bench un scope pu essere aperto o chiuso il simbolo deve
persistere in memoria
I nomi possono essere di dimenzioni svariate (da 1 a molti
caratteri)
Una lista ordinata dei nomi pu essere mantenuta
Ci fa preferire uno spazio dei nomi logico
P R O V A S 1 S 2 X
5 2 2 1
26
Implementazione della tabella dei
simboli
Per ogni nome sono memorizzati
Nome: un riferimento allo spazio dei nomi logico
Tipo: informazioni associate con il simbolo
Hash: collegamento doppio con i simboli che forniscono lo
stesso hash code
Var: riferimento alle altre dichiarazioni dello stesso nome
Level: collegamento a simboli dello stesso livello
Depth: memorizza la profondita del nesting di un simbolo
Void openScope() {
depth = depth +1;
scopeDisplay(depth) = null
}
Void closeScope() {
foreach (symb in scopeDisplay(depth)) do {
prevsym = symb.var
delete(symb)
if (prevsym != null) add(prevsym)
} depth = depth +1;
}
Symnol retrieveSymbol(name) {
sym = hashTable.get(name)
while (sym != null) {
if (sym.name == name) return sym
sym = sym.hash
}
return null
}
27
Void enterSymbol () {
oldsym =retriveSymbol(name)
if (oldsym != null) && oldsym.depth == depth)
errore (duplicate symbol)
newsym = createNewSymbol(name,tyoe)
newsym.level = scopeDisplay(depth)
newsym.depth = depth
scopeDisplay(depth) = newsym
if (oldsym == null) add(newsym)
else {
delete(oldsym)
add(newsym)
} newsym.var = oldsym
}
Esempio fare copia
HASHTABLE
f function(float, float) v L h
z float v L h
x float v L h
x int v L h
f Void function v L h
28
Ancora .
Strutturre e record: nomi dei campi
Overloanding di funzioni e operatori
Dichiarazioni implicite
Contenuto della tabella dei simboli
Ci sono una variet di generi tipici di identificatori
Le variabili scalari, array e strutture (record), le procedure
e le funzioni,
Alcuni tipi di informazioni memorizzate per identificatori
l nome o il valore
Il tipo di dati
Le Dimensioni e iinformazioni di dimensionalit (per gli
array)
scoping
Tipi di risultati, i parametri (parametri formali), i prototipi
Le informazioni salvate dipendono dal genere di
oggetto, per tale motivo si ha la necessita che le entri
della tabella dei simboli abbiano un formato flessibile
29
Tipo di dato nella tabella dei simboli
Ad esempio lentry per un array definito come nel
seguito richiede una descrizione che include il tipo
degli elementi
Array A [1 .. 100] of mytype ;
Mytype is a record
username: char string ;
emailaddress: char string ;
acctdetails: array ( 1: 5) of integer
usage: pointer to array ( 1:12) of reals
Descrittori delle strutture
Il descrittore type deve essere un puntatore ad un
TypeDescriptor
Il TypeDescriptor una struttura dati che consente
di modellare i diversi tipi presenti nei linguaggi
Esempi di descrittori per alcuni tipi di dato

Potrebbero piacerti anche