Sunteți pe pagina 1din 101

Daniela MARINESCU

2008 2009
REPROGRAFIA UNIVERSITII TRANSILVANIA DIN BRAOV

TEHNICI DE COMPILARE

n acest capitol vom vedea ce este un compilator i vom descrie pe scurt


fazele i componentele unui compilator.
Spus simplu, un compilator este un program care citete un alt program
scris ntr-un anumit limbaj, numit limbaj surs i l traduce ntr-un program
echivalent n alt limbaj, numit limbaj destinaie (vezi Fig. 1.1.). O parte
important a procesului de traducere o constituie i atenionarea
utilizatorului asupra erorilor din programul surs.
1.1. INTRODUCERE
Principiile i tehnicile folosite n scrierea unui compilator sunt att de
cuprinztoare nct multe dintre acestea pot fi folosite cu succes n multe alte cazuri.
Scrierea unui compilator necesit cunotine de programare, arhitectura
calculatoarelor, limbaje formale i algoritmic.
Tehnicile utilizate n construcia compilatoarelor pot folosi i n alte domenii
cum ar fi: editoare de texte, sisteme de regsire a informaiilor i recunoaterea
formelor. De asemenea, limbajele independente de context i definiiile orientate pe
sintax pot fi folosite pentru a construi minilimbaje ca acelea de tiprire sau de
desenare a figurilor. Tehnicile de optimizare a codului au fost folosite n verificarea
programelor i n producerea de programe structurate din unele nestructurate.

Program
surs

Compilator

Program
obiect

Mesaje de
eroare
Figura 1.1
Un compilator este un program care are ca intrare un program surs, scris ntrun limbaj evoluat, iar ca ieire furnizeaz un program obiect. n timpul traducerii,
compilatorul afieaz mesajele de eroare, putnd chiar s corecteze anumite erori.
Exist mii de limbaje de programare, de la cele tradiionale (FORTRAN,
PASCAL) pn la limbaje specializate aprute n diferite domenii ale aplicaiilor
calculatoarelor. Cu ct programul surs este scris ntr-un limbaj mai evoluat (mai
apropiat de limbajul natural), cu att structura compilatorului este mai complicat.
Pag.1

TEHNICI DE COMPILARE

Limbajul obiect poate avea mai multe accepiuni: alt limbaj de programare sau
un limbaj main pentru orice calculator ntre un microprocesor i un supercomputer.
Dei compilatoarele pot fi ntr-un singur pas, multi-pass, load-and-go (evit
trecerea prin faze necesare numai programelor complexe form direct executabil)
i altele, tehnicile de baz folosite sunt aceleai.
Compilatoarele single-pass efectueaz o singur trecere asupra programului
surs, pe cnd cele multi-pass proceseaz de mai multe ori sursa.
Primele compilatoare au nceput s apar prin anul 1950, experimentarea i
implementarea lor fiind fcut independent de grupuri diferite. Primele ncercri de
compilare au nceput cu traducerea expresiilor aritmetice n cod main. n aceast
perioad, compilatoarele erau considerate programe deosebit de dificile. De exemplu,
compilatorul FORTRAN (Backus .a., 1957) a necesitat 18 ani de munc n echip
pentru implementare. De atunci s-au descoperit tehnici pentru rezolvarea
principalelor probleme care apar n compilare, tehnici care au fcut posibil scrierea
unui compilator n cadrul unui proiect studenesc.
Compilatorul se compune din dou pri principale: analiza i sinteza. Partea de
analiz realizeaz descompunerea programului surs n pri constituente i crearea
reprezentrii intermediare. Partea de sintez construiete programul obiect dorit din
reprezentarea intermediar.
n timpul analizei, operaiile implicate de programul surs sunt determinate i
nregistrate ntr-un arbore. De cele mai multe ori se folosete un arbore sintactic n
care fiecare nod reprezint o operaie, iar descendenii nodului sunt operanzi
(argumentele operaiei).
Exemplul 1.1 Arborele sintactic pentru instruciunea de atribuire:
valoare := valoareinit + cant * 40
este urmtorul:
:=

valoare

valoareinit

cant

40

Figura 1.2
Multe instrumente care manipuleaz programe surs execut nti anumite tipuri
de analiz. Exemple de astfel de instrumente sunt:
Pag.2

TEHNICI DE COMPILARE

1. Editoare structurale. Un editor structural are ca intrare un ir de comenzi din


care construiete un program surs. Un editor structural nu execut numai
crearea textului i funcii de modificare precum un editor de texte ordinar, ci el
de asemenea analizeaz textul punndu-l ntr-o structur ierarhic. Deci ieirea
unui astfel de editor este similar cu faza de analiz a unui compilator.
2. Pretty printer. Un astfel de tipritor analizeaz un program i l tiprete ntro form n care structura programului devine foarte vizibil. De exemplu,
comentariile apar scrise cu alte fonturi, iar instruciunile incluse una n cealalt
sunt scrise astfel nct s sugereze ierarhia.
3. Analizor static. Un astfel de program citete un program surs, l analizeaz i
pune n eviden defectele fr s-l execute. De exemplu, un analizor static
depisteaz acele pri ale programului surs care nu vor fi niciodat executate
sau acele variabile care sunt folosite nainte de a fi definite. n plus, el poate
depista anumite erori logice cum ar fi folosirea unei variabile reale drept
pointer.
4. Interpretor. Interpretorul nu produce un program obiect, ci execut operaiile
implicate n programul surs. Pentru o instruciune de atribuire, de exemplu, un
interpretor poate construi un arbore ca cel din Figura 1.2. i apoi s execute
operaiile din noduri, parcurgnd arborele. n rdcin va descoperi c este
greu s execute o atribuire n care membrul drept este o expresie i atunci va
chema o rutin care va calcula valoarea expresiei. Aceast rutin descoper c
are de fcut o adunare n care membrul drept este o expresie. Atunci se
autoapeleaz (se apeleaz recursiv) pentru calculul expresiei din membrul drept
(o nmulire) i apoi adun valoarea la variabila iniial.
Interpretoarele sunt frecvent utilizate pentru a executa limbaje de comand
pentru c fiecare operator executat ntr-un limbaj de comand este, n mod normal, o
apelare a unei rutine complexe ca un editor sau un compilator. Similar, anumite
limbaje de nivel foarte nalt APL sunt interpretate pentru c exist multe lucruri
despre date, ca dimensiunea irurilor sau tablourilor, care nu pot fi deduse n timpul
compilrii.
n mod normal, privim un compilator ca un program care traduce un program
scris intr-un limbaj surs (ca FORTRAN-ul) ntr-un limbaj de asamblare sau n
cod main pentru un anume calculator.
Totui, exist i alte domenii unde tehnicile de compilare pot fi folosite. Partea
de analiz din urmtoarele exemple este similar cu partea de analiz a unui
compilator:
1. Formatarea textelor. Un formator de text are ca intrare un ir de caractere
ntre care majoritatea sunt text sau de tiprire, dar anumite caractere formeaz
comenzi pentru a indica paragrafe, figuri sau structuri matematice ca indici
inferiori sau superiori.
2. Silicon compiler. Un silicon computer are un limbaj surs care este similar cu
un limbaj de programare dar variabilele limbajului nu reprezint locaii n
memorie ci semnale logice (0 sau 1) sau grupe de semnale n circuite de
comutaie. Ieirea este descrierea unui circuit ntr-un limbaj specific.
Pag.3

TEHNICI DE COMPILARE

3. Interpretor logic. Un astfel de interpretor traduce un predicat, coninnd


operatori relaionali i booleeni, n comenzi de cutare ntr-o baz de date a
unor nregistrri satisfcnd predicatul.
Observaia 1.1 n afar de compilator mai sunt necesare i alte programe pentru a
obine un program obiect. Un program surs poate fi mprit n module care sunt
memorate n cmpuri separate. Sarcina adunrii programului surs revine unui
preprocesor. Preprocesorul poate, de asemenea, s desfac macrourile n instruciuni
ale programului surs.
1.2. FAZELE UNUI COMPILATOR
Un compilator opereaz n mai multe faze, fiecare din faze transformnd
programul dintr-o reprezentare n alta. O descompunere tipic a unui compilator este
prezent n Figura 1.3, unele dintre faze fiind grupate mpreun.
Cteodat, att managerul tabelei de simboluri ct i recuperarea erorilor sunt
considerate faze ale compilrii.

1.2.1. Managerul tabelei de simboluri


Una din funciile eseniale ale unui compilator este de a nregistra identificatorii
folosii i de a aduna informaii despre diferitele atribute ale fiecrui identificator.
Aceste atribute pot furniza informaii despre memoria alocat pentru un identificator,
despre scopul su, i n cazul unui nume de procedur- anumite informaii ca:
numrul parametrilor, tipul i metoda de transmitere a acestora (de exemplu prin
referin) i tipul returnat.
O tabel de simboluri este o structur de date coninnd o nregistrare pentru
fiecare identificator, cu cmpuri pentru atributele identificatorului. O astfel de
structur de date trebuie astfel organizat nct s permit stocarea i regsirea rapid
a informaiei.
Cnd un identificator este depistat n faza de analiz lexical, el este imediat
introdus n tabela de simboluri. Totui atributele identificatorului nu pot fi imediat
deduse din faza de analiz lexical.
Exemplul 1.2 ntr-o declaraie Pascal de forma:
Var
valoare, valoareinit, cant : Real;
tipul real nu este cunoscut cnd identificatorii valoare, valoareinit i cant sunt
depistai de analizorul lexical.
Celelalte faze introduc n tabela de simboluri informaii despre identificatori i
apoi utilizeaz aceste informaii n diferite feluri. De exemplu, n timpul analizei
semantice i la generarea codului intermediar este necesar s tim ce tip de
identificatori sunt pentru a putea verifica dac programul surs i utilizeaz corect.
Generatorul de cod cere i utilizeaz informaii detaliate despre memoria alocat unui
identificator.

Pag.4

TEHNICI DE COMPILARE

Program surs

Analizor lexical
(scanner)

Analiza

Analizor sintactic
(parser)

Analizor semantic
Managerul
tabelei de
simboluri

Recuperarea
erorilor

Generatorul codului
intermediar

Sinteza

Optimizarea codului

Generatorul de cod

Program obiect

Figura 1.3

1.2.2.Detectarea i raportarea erorilor


n fiecare faz pot fi descoperite erori. Totui, dup depistarea unei erori o faz
trebuie s continue pentru a depista alte erori din programul surs, deci trebuie s
repare eroarea, ntr-o anumit form care s permit aceast continuare.
n faza de analiz lexical i n cea de analiz sintactic se depisteaz cea mai
mare parte a erorilor detectabile de un compilator. Faza de analiz lexical poate
detecta erorile n cazul n care caracterele rmase din intrare nu formeaz o unitate
lexical a limbajului. Erorile de sintax sunt acelea n care un ir de uniti lexicale
Pag.5

TEHNICI DE COMPILARE

violeaz structura sintactic a limbajului surs (regulile limbajului). n faza de analiz


semantic a compilatorului, analizorul ncearc s detecteze acele construcii corecte
din punct de vedere sintactic- care nu au sens din punct de vedere operaional, de
exemplu dac ncercm s adunm doi identificatori i unul este nume de tablou iar
altul nume de procedur.
Manevrarea erorilor va fi discutat la fiecare faz n parte.

1.2.3.Faza de analiz
Pe parcursul compilrii, forma programului surs se schimb. Vom ilustra acest
lucru considernd traducerea instruciunii:
valoare := valoareinit + cant * 40
( 1.a)
(a) Faza de analiz lexical citete caracterele programului surs i le grupeaz
n uniti lexicale sau token-i, unde fiecare unitate lexical reprezint o secven
logic de caractere ca, de exemplu, un identificator, un cuvnt cheie (if, while, begin),
un caracter sau un multicaracter (:=, <= etc.). Secvena de caractere formnd un
token se numete lexem pentru token. Token-ilor li se adaug valoarea lexical.
De exemplu, cnd identificatorul cant este gsit, analizorul lexical genereaz un
token, id, dar adaug i lexema cant n tabela de simboluri dac ea nu exist
deja.
Astfel, n urma analizei lexicale a instruciunii ( 1.a) se va obine:
id1 := id2 + id3 * 40
( 1.b)
Observaia 1.2. Se poate utiliza un token pentru := dar ngreuneaz expunerea; de
asemenea, token-ul numr pentru constanta 40.
De asemenea, n tabela de simboluri se introduce valoare n momentul
depistrii unitii lexicale id1, valoareinit n momentul depistrii unitii lexicale
id2, respectiv cant n momentul depistrii unitii lexicale id3.
(b) n faza de analiz sintactic se ncearc construirea arborelui de derivaie,
o structur ierarhic a irului de token-i.
O structur tipic de date pentru arbore este prezentat n Figura 1.4.
:=

:=
id1

id

1
id

id2

+
2

*
id3

40

id

num (40)
Figura1.4

n figur, un nod interior este o nregistrare cu un cmp pentru operator i dou


cmpuri coninnd pointeri pentru nregistrarea descendenilor stng i drept. O
Pag.6

TEHNICI DE COMPILARE

frunz este o nregistrare cu dou sau mai multe cmpuri, unul pentru a identifica
token-ul i unul pentru a nregistra informaii despre acel token.
Pentru exemplul considerat, Figura 1.6 arat succesiunea fazelor n procesul
analizei.
n mod obinuit, arborele sintactic pentru instruciunea de atribuire se
construiete conform regulilor gramaticale ale limbajului, care folosesc regulile
definirii unei expresii aritmetice:
1.
Orice identificator este o expresie.
2.
Orice numr este o expresie.
3.
Dac expresie1 i expresie2 sunt expresii, atunci tot expresii sunt i:
expresie1 + expresie2
expresie1 * expresie2
(expresie1)
unde regulile 1 i 2 sunt nerecursive iar regula 3 este recursiv.
Similar, pentru instruciune multe limbaje folosesc regulile de definire:
1. Dac identificator1 este un identificator i expresie2 este o expresie, atunci
identificator1 := expresie2
este o instruciune (de atribuire).
2. Dac expresie1 este o expresie i instruciune2 este o instruciune, atunci
while (expresie1) do instruciune2
i
if (expresie1) then instruciune2
sunt instruciuni.
Atunci arborele sintactic pentru instruciunea de atribuire considerat arat ca n
Figura 1.5 .
Faza de analiz lexical rezolv capetele nnegrite din figur, pentru care se
folosesc gramatici regulate i automate cu numr finit de stri. n faza de analiz
sintactic, definiiile recursive sunt modelate cu ajutorul gramaticilor independente de
context.
n mod uzual nu se folosete un arbore sintactic aa de complicat ci unul de
analiz ca cel din Figura 1.4 .
(c) n faza de analiz semantic, analizorul caut erorile semantice i adun
informaii pentru generarea de cod.
O parte important din analiza semantic este verificarea tipurilor. Analizorul
semantic verific dac fiecare operand cu care opereaz un operator este de tipul
cerut n limbajul de programare surs. Totui specificarea limbajului permite i
anumite corecii, de exemplu cnd o operaie aritmetic este aplicat unui ntreg i
unui real. n acest caz, compilatorul trebuie s converteasc ntregul ntr-un real.
Pentru exemplul considerat de noi ( 1.a), 40 este un ntreg i cant este real, deci
analizorul semantic va converti 40 n real prin apelarea subrutinei IntToReal i deci
adaug un nod n plus la arborele sintactic.
Pag.7

TEHNICI DE COMPILARE

1.2.4.Faza de sintez
(a)
Faza de generare a codului intermediar: dup analiza sintactic i
semantic, anumite compilatoare genereaz o reprezentare intermediar explicit a
programului surs, ca un program pentru o main abstract. Aceast reprezentare
trebuie s aib dou caliti: s fie uor de realizat i uor de tradus n program obiect.
O reprezentare intermediar poate avea o mulime de forme, una din acestea
fiind codul cu trei adrese, n care exist cel mult trei operanzi.
n aceast form, compilatorul poate decide n ce ordine s execute operaiile
(operatorul * este anterior operatorului +).
(b) n faza de optimizare de cod, compilatorul tinde s mbunteasc codul
intermediar n sensul micorrii timpului de execuie. Astfel, n exemplul nostru (
1.a), compilatorul poate deduce c are de fcut o singur conversie pentru constanta
40 n real, deci nu are rost s o memoreze i nici s adauge IntToReal ci s foloseasc
direct 40.0 .
De asemenea, cmp3 este folorit numai o dat pentru a memora i transmite
valoarea sa lui id1. Atunci, devine sigur nlocuirea lui cmp3 prin id1.
(c) Faza final a compilatorului este faza de generare de cod obiect, care
const ntr-un cod main relocatabil sau ntr-un cod de asamblare. Sunt selectate
locaii de memorie pentru fiecare din variabilele folosite n program. Apoi
instruciunile intermediare sunt traduse fiecare n cte o secven de instruciuni n
limbaj de asamblare.
Un aspect deosebit de important este atribuirea variabilelor regitrilor de
memorie. Pentru exemplul nostru ( 1.a), codul de asamblare ar fi:
MOVF
MULF
MOVF
ADDF
MOVF

id3,R2
#40.0,R2
id2,R1
R2,R1
R1,id1

ntr-o instruciune n cod de asamblare, primul i al doilea operand reprezint


sursa, respectiv destinaia. Litera F ataat instruciunii respective specific faptul
c instruciunile se execut cu numere n virgul flotant. Semnul # specific faptul
c 40.0 trebuie tratat ca o constant.
O imagine complet asupra compilrii exemplului considerat (1.a) este dat n
figura urmtoare (Figura 1.6).

Pag.8

TEHNICI DE COMPILARE
valoare := valoareinit + cant * 40

analizor lexical
id1 := id2 + id3 * 40

analizor sintactic
:=
+

id1
id2

*
id3

num(40)

analizor semantic
:=
+

id1
id2

*
id3

IntToReal(40)

generatorul codului
intermediar
temp1 := IntToReal(40)
temp2 := id3 * temp1
temp3 := id2 + temp2
id1 := temp3

optimizarea codului
temp1 := id3 * 40.0
id1 := id2 + temp1

generarea codului
MOVF id3, R2
MULF #40.0, R2
MOVF id2, R1
ADDF R2, R1
MOVF R1, id1

Fig. 1.6. Transformarea unei instruciuni de atribuire aritmetic

Pag.9

TEHNICI DE COMPILARE

1.3.INSTRUMENTE FOLOSITE N CONSTRUCIA COMPILATOARELOR


Pentru c fazele aprute n construcia compilatoarelor sunt, n esen, aceleai,
au aprut o serie de instrumente soft folosibile. Aceste instrumente sunt des referite
drept: compilatoare de compilator, generatoare de compilator sau sisteme de scriere a
translatoarelor.
1. Generatoare de analizor lexical (scanner generators)
Aceste programe genereaz automat un analizor lexical, pe baza unei specificaii
alctuite din expresii regulate i aciuni asociate acestor expresii regulate.

Primul generator de acest tip a fost LEX, realizat n 1975 de M.E.Lesk, sub
sistemul de operare Unix. Pentru MS-DOS exist varianta PCLEX;

Un alt generator de scanner este FLEX al proiectului GNU, unul din cele mai
eficiente generatoare de analizor lexical;

Mai exist i REX la proiectului COCKTAIL, cu un limbaj de specificaie


propriu diferit de LEX i care permite o mai mare libertate de micare.
2. Generatoare de analizor sintactice (parser generators)
Un astfel de generator este un program a crui intrare este o gramatic
indpendent de context iar ieirea este un analizor sintactic.

Un astfel de generator este YACC (Yet Another Compiler - Compiler) creat


n 1970 de S.C.Johnson. Pentru MS-DOS exist varianta PCYACC. Alte astfel de
generatoare sunt:

BISON al proiectului GNU-FSF n sistemul de operare LINUX (versiune


UNIX);

BYACC (Berkeley YACC) n sistemul de operare MS-DOS (versiunea 6.0);

LALR al proiectului COCKTAIL, dezvoltat de Universitatea KarlsruheGermania.


3. Mecanisme de traducere orientate pe sintax (syntax-directed translation
engines)
Acestea sunt rutine de parcurgere a arborelui de analiz i a codului intern. n
acest domeniu sunt programe ale proiectului COCKTAIL, cum ar fi:

AST: generator pentru arborele de sintax abstract;

AG: generator al unui evaluator al atributelor semantice ale componentelor


arborelui sintactic obinut prin AST;

ESTRA: generator pentru transformarea arborelui sintactic atributat.


& 1.4. REZUMAT
Un compilator este un program care traduce un alt program din limbal
surs(limbaj de programare evoluat) n limbaj obiect (cod main sau alt limbaj
evoluat pentru care exist compilator). Pentru a putea efectua aceast traducere nti
se analizeaz programul surs, pentru a verifica corectitudinea gramatical, n faza de
analiz, iar apoi se efectueaz traducerea, n faza de sintez.
Pag.10

TEHNICI DE COMPILARE

Faza de analiz este descompus n: analiza lexical, sintactic i semantic.


Acest lucru se face n funcie de tipul regulilor gramaticale ale limbajului surs care
se verific: reguli de tip 3, de tip 2 sau de tip 1.
Faza de sintez se descompune i ea n: generarea codului intermediar,
optimizarea codului i generarea codului obiect.
n afar de aceste faze mai exist faza de redresare a erorilor i managementul
tabelei de simboluri.
Exerciii
1. Considerai cteva compilatoare la care avei acces. Comparai structura lor i
timpii de execuie pentru acelai program.
2. Descriei, pe scurt, fazele compilrii pentru o instruciune de atribuire de
forma:
A1 := A1 + B * 2 * (C A / 5)

Pag.11

TEHNICI DE COMPILARE

Aceast seciune se refer la specificarea i implementarea analizorului


lexical. Analiza lexical este prima faz a unui compilator i se bazeaz pe
noiunile din teoria limbajelor formale: gramatici de tip 3, automate finite i
expresii regulate. Vom prezenta dou moduri de construcie a unui analizor
lexical:
Un mod simplu de a construi un astfel de analizor este s realizm o
diagram care s specifice structura unitilor lexicale din programul
surs i apoi s traducem aceast diagram ntr-un program care s
identifice uniti lexicale. Astfel pot fi realizate analizoare lexicale
foarte eficiente.
Un alt mod, mai simplu, este de a utiliza limbajul Lex pentru scrierea
unui analizor lexical.
Tehnicile folosite pentru implementarea analizoarelor lexicale pot fi aplicate i n
alte domenii cum ar fi: cuttoare sau sisteme de recuperare a informaiei. n toate
aplicaiile problema esenial este specificarea abloanelor pentru unitile lexicale
precum i crearea de programe care s determine unitile lexicale dup abloane. Un
numr nsemnat de limbaje folosesc expresii regulate pentru a descrie abloane
(pattern-uri).
Pentru specificarea acestor abloane se introduce un limbaj, numit Lex. n acest
limbaj formele sunt specificate prin expresii regulate iar un compilator pentru Lex va
genera un automat finit care recunoate expresiile regulate.
Expresiile regulate au fost folosite i n limbaje de procesare a fiierelor. Aho,
Kernighan i Weinberg [1979] ( AWK ) i nainte Jarvis [1976] au folosit expresiile
regulate pentru a descrie imperfectiuni n circuitele tiprite. Circuitele sunt analizate
digital i convertite n iruri de segmente de dreapt de unghiuri diferite. Analizorul
lexical caut o forma corespunztoare imperfeciunii n irul de segmente de dreapt.
Un avantaj important al generatorului de analizor lexical este acela c pot fi
utilizai cei mai buni algoritmi cunoscui i deci se poate crea un analizor lexical
efficient de ctre o persoan care nu este expert n aceste tehnici.

2.1. ROLUL ANALIZEI LEXICALE


Analiza lexical este prima faz a unui compilator i are principalul rol de a citi
caracterele programului surs i de a produce, la ieire, un ir de uniti lexicale,
numite token-i, pe care compilatorul l analizez n faza de analiz sintactic.
Pag.12

TEHNICI DE COMPILARE

De multe ori implementarea este realizat considernd analizorul lexical drept o


subrutin a analizorului sintactic. n momentul primirii unei comenzi get next
token, analizorul lexical citete caracterele de intrare pn determin un nou token.
token
Analiz
Analiz
Program
Arbore
sintactic
i
lexical
semantic
surs
sintactic
get next token

Tabela de
simboluri

Figura 2.1
n afar de aceast sarcin principal analizorul lexical mai realizeaz alte cteva
aciuni cum ar fi: elimin comentariile, spaiile i caracterele de tip tab sau
newline. De asemenea, analizorul lexical are sarcina de a corela mesajele de eroare
din compilator cu programul surs, deci trebuie s in minte numrul caracterului
newline citit pentru a-l putea asocia cu un mesaj de eroare. La anumite compilatoare,
analizorul lexical trebuie s realizeze o copie a programului surs cu mesajele de
eroare inserate n el.
n anumite situaii analizorul lexical este mprit n dou faze: prima, numit
scanner, iar a doua, numit anali lexical. Scanner-ul realizeaz numai aciuni
simple, ca de exemplu compactarea programului surs, n timp ce analizorul lexical
realizeaz partea cea mai complicat.
Exist cteva raiuni care au condus la separarea fazei de analiz lexical de fazele
de analiz sintactic i semantic i anume:
Simplificarea descrierii limbajului, datorat eliminrii comentariilor i a
spaiilor.
Eficiena compilrii este mbuntit. O analiz lexical separat permite
folosirea unor tehnici care mresc viteza de execuie a compilatorului. Astfel
sunt tehnicile de memorare n zona tampon specializate pentru citirea
caracterelor de intrare i procesarea token-ilor.
Portabilitatea compilatorului crete pentru c particulariti ale alfabetului de
intrare i alte anomalii dependente de dispozitiv pot fi eliminate n faza de
analiz lexical. De exemplu reprezentarea unor simboluri nestandard ca ^ n
Pascal poate fi izolat de analizorul lexical.

2.2. NOIUNILE DE TOKEN, PATTERN I LEXEM


Vom defini anumite elemente cu care opereaz analizorul lexical i anume: token,
lexem i pattern.

Pag.13

TEHNICI DE COMPILARE

O unitate lexical poart numele de token, iar irul de caractere care compun o
unitate lexical formeaz o lexem. Regula care descrie mulimea lexemelor care
formeaz o unitate lexical se numete pattern.
Token-ii sunt tratai ca simboluri terminale ntr-o gramatic pentru limbajul surs n
faza de analiza sintactic.
n cele mai multe limbaje de programare urmtoarele construcii sunt tratate ca
uniti lexicale: cuvnt rezervat, operator, identificator, constant, iruri literale i
semne de punctuaie (paranteze, virgul i punct-virgul).
Prezentm n continuare cteva exemple de token-i, lexeme i pattern-uri.
Token
Lexeme
Descrierea informativ a pattern-ului
const
const
const
if
if
if
relaie
<, <=, >, =, <>
< sau <= sau > sau = sau <>
identificator Pi, alpha, D2, valoare Liter urmat de litere i cifre
numr
3.1416, 0, 6.02E23
Orice constant numeric
literal
mesaj
Orice ir de caractere cuprins ntre
i (cu excepia caracterului )
Cnd un pattern are mai multe lexeme analizorul lexical trebuie s produc
informaii suplimentare despre lexema ntlnit. De exemplu token-ul numr
desemneaz numerele 3,1416 i 0 i 6,02E23 iar pentru generatorul de cod este
important s tie care dintre ele este folosit.
Exist anumite limbaje n care conveniile limbajului mresc dificultatea analizei
lexicale. De exemplu n limbajul Fortran este important construcia n poziie fix a
liniei de intrare. Astfel aliniamentul unei lexeme poate fi important n stabilirea
corectitudinii programului surs (primul caracter poate reprezenta o etichet,
comentariu sau continuarea liniei anterioare).
n limbajele moderne aa numitul format liber de intrare permite construciei s fie
poziionat oriunde n linia de intrare, astefel nct acest aspect devine mai puin
important.
De asemenea tratarea blank-urilor este diferit n funcie de limbaj i anume la
limbaje ca Fortran sau Algol-68 blank-urile nu sunt semnificative cu excepia
apariiei ntr-o constant alfanumeric. De aceea ele pot fi eliminate simplificnd
programul surs. Totui aceste convenii pot complica restul analizei.
De exemplu, recunoaterea unitilor lexicale dintr-o instruciune DO n limbajul
Fortran poate fi foarte dificil. n instruciunea:
DO 5 I = 1.25
nu putem spune dac DO este un cuvnt cheie sau face parte din identificatorul DO5I
dect n momentul cnd este vzut caracterul . n intrare. Pe de alt parte, n
instruciunea:
DO 5 I = 1,25
Pag.14

TEHNICI DE COMPILARE

avem 7 uniti lexicale, dar nu putem ti ce este DO dect n momentul descoperirii


virgulei n irul de intrare. n acest caz trebuie implementat o tehnic de citire n
avans a caracterelor din irul de intrare.
O alt problem poate apare n cazul n care cuvintele cheie nu sunt rezervate. Aici
analizorul trebuie s sesizeze diferena ntre un cuvnt cheie i un identificator definit
de utilizator. Tehnicile folosite pentru aceasta pot fi foarte complexe. Un exemplu n
acest sens este limbajul PL/I n care putem avea o instruciune de genul:
IF THEN THEN THEN = ELSE;
ELSE ELSE = THEN;

2.3.ATRIBUTELE UNITILOR LEXICALE


Atunci cnd mai multe lexeme pot reprezenta aceeai unitate lexical, analizorul
lexical trebuie s furnizeze mai multe informaii referitoare la respectiva lexem. De
exemplu, unitatea lexical ntreg se potrivete pentru amndou irurile 0 sau 1, dar
este esenial pentru generatorul de cod s tie care dintre cele dou iruri trebuie
utilizat.
Informaiile asociate unitilor lexicale se numesc atribute. Trebuie remarcat faptul
c nu toate unitile lexicale au atribute. Aa sunt, de exemplu, operatorii sau semnele
de punctuaie, pentru care exist o singur lexem.
Prezentm un exemplu de token-i i de atribute associate lor n cazul instruciunii
de atribuire din Fortran:
E=M*C**2
n faza de analiz lexical se obin urmtorii 7 tokeni, prezentai mpreun cu
atributele lor:
< id, pointer n tabela de simboluri pentru E>
<op-de-atribuire, >
< id, pointer n tabela de simboluri pentru M >
< op-multiplicativ, >
< id, pointer n tabela de simboluri pentru C >
< op-exponenial, >
< num, valoarea ntreag 2 >
n cazul lexemei num compilatorul poate proceda i altfel, adic s memoreze
constanta 2 n tabela de simboluri i atunci atributul va fi adresa din tabel.
Token-ii influeneaz analiza sintactic n timp ce atributele lor influeneaz
generarea de cod. Din punct de vedere practic un token are un singur atribut, i
anume un pointer pentru intrarea n tabela de simboluri n care sunt pstrate toate
informaiile ataate lexemei respective. Astfel un pointer devine un atribut pentru un
token.

Pag.15

TEHNICI DE COMPILARE

2.4.ERORI LEXICALE
n faza de analiz lexical sunt depistate puine erori, datorit faptului c analizorul
lexical are un punct de vedere foarte restrns asupra programului surs.
De exemplu dac ntr-un program C se ntlnete fi pentru prima dat n urmtorul
context:
fi( a == f(x) )
atunci analizorul lexical nu este capabil s spun dac fi este cuvntul cheie if scris
greit sau identificatorul fi. n acest caz analizorul lexical consider fi drept
identificator i las celorlalte faze ale compilatorului sarcina de a depista eroarea.
n general, analizorul lexical citete caracterele programului surs de la stnga la
dreapta pn cnd se poate forma un token. Presupunem c n acest mod nu poate
forma un token din caracterele citite. Atunci sunt posibile patru tipuri de "reparare" a
erorii:
1. Elimin un caracter strin;
2. Insereaz un caracter care lipsete;
3. nlocuiete un caracter incorect printr-unul corect;
4. Schimb dou caractere adiacente.
Astfel de transformri se fac n ideea de a repara intrarea. Cea mai simpl strategie
de acest fel ar fi de a vedea dac un prefix al intrrii rmase poate fi transformat ntro lexem valid dintr-o singur transformare. Aceast strategie presupune c cele mai
multe erori lexicale sunt rezultatul unei singure erori, presupunere care ns nu este
valabil n practic.
O cale de a gsi erorile n program este de a calcula numrul minim de transformri
de eroare necesare pentru a transforma programul ntr-unul corect din punct de vedere
sintactic. Spunem c un program surs are k erori dac cel mai scurt ir de
transformri de eroare necesare pentru a-l transforma ntr-un program valid are
lungimea k. Aceast noiune de "distan minim" este mai mult teoretic.

2.5.TAMPONAREA INTRRII
Exist trei metode generale de abordare a implementrii unui analizor lexical:
1. Utilizarea unui generator de analizor lexical, cum ar fi limbajul LEX, care
produce analizorul lexical pornind de la o expresie regulat; n acest caz,
generatorul dispune de rutine pentru citirea i tamponarea intrrii.
2. Scrierea analizorului lexical ntr-un limbaj de programare convenional,
utiliznd facilitile de intrare/ieire pe care limbajul respectiv le pune la
dispoziia programatorului.
3. Scrierea analizorului lexical n limbaj de programare, n care gestionarea
intrrilor i ieirilor trebuie fcute de programator.
Dificultatea implementrii crete n ordinea prezentrii celor trei metode. Din
pcate, metoda cea mai greu de implementat este i cea mai rapid n sensul vitezei de
lucru a analizorului. Cum analiza lexical este singura faz a compilatorului n care
Pag.16

TEHNICI DE COMPILARE

programul surs este citit caracter cu caracter, este posibil ca, compilatorul s petreac
o mare parte din timp la nivelul acestei faze, cu toate c celelalte faze sunt mult mai
complexe.
Pentru multe limbaje surs exist momente cnd analizorul lexical trebuie s
priveasc nainte un numr de caractere pentru a-i da seama dac lexema citit pentru
un ablon este valid. Se folosete n acest sens un buffer (zon tampon) mprit n
dou jumti, fiecare putnd conine maxim N caractere, ca n Fig. 2.3 n general, N
este numrul de caractere care ncap ntr-un bloc de disc.
E

eof

forward
nceput_de_lexem

Fig. 2.3. Buffer de intrare


Se citesc N caractere n fiecare jumtate. Dac mai puin de N caractere au rmas
n intrare atunci caracterul special eof este adugat n buffer la sfritul irului de
intrare. Sunt utilizai de asemenea doi pointeri. irul de caractere dintre cei doi
pointeri este lexema curent. Iniial amndoi indic primul caracter al urmtoarei
lexeme care trebuie gsit. Pointerul anticipativ (forward), citete nainte caractere din
intrare pn cnd un ablon este gsit. Atunci cnd lexema urmtoare este determinat,
pointerul anticipativ este setat pe caracterul ei de sfrit. Dup ce lexema este
procesat, amndoi pointerii indic caracterul imediat urmtor acelei lexeme.
Dac pointerul anticipativ este pe cale s treac de una dintre jumti, n cealalt
jumtate sunt citite N caractere noi. Singura problem pe care o ridic aceast schem
este c numrul caracterelor citite nainte este limitat de lungimea bufferului.
Algoritmul pentru pointerul anticipativ (forward) este prezentat n Fig. 2.4.
if forward la sfritul primei jumti
then begin
rencarc a doua jumtate;
forward := forward + 1
end
else if forward la sfritul celei de-a doua jumti
then begin
rencarc prima jumtate;
Fig. 2.4. Algoritm pentru avansarea pointerului anticipativ
mut forward la nceputul primei jumti
end
else forward := forward + 1;

2.5.1.Santinele
Dac folosim schema prezentat n Fig. 2.3. trebuie verificat de fiecare dat dac
pointerul anticipativ a trecut de sfritul unei jumti de buffer pentru a ti dac
trebuie ncrcat cealalt jumtate. Exceptnd momentul cnd a ajuns la sfritul unei
jumti, codul din Fig. 2.4. execut dou teste la fiecare avans al pointerului
Pag.17

TEHNICI DE COMPILARE

anticipativ. Putem reduce aceste dou teste la unul, dac vom plasa la sfritul
fiecrei jumti un caracter special, numit santinel (eof).
Fig. 2.5. prezint tamponarea intrrii cu santinele.
E

eof

eof

eof

forward
nceput_de_lexem

Fig. 2.5. Buffer de intrare cu santinele


Pentru schema din Fig. 2.5. putem folosi codul din Fig. 2.6. de avansare a
pointerului anticipativ. De cele mai multe ori codul din Fig. 2.6. execut un singur test
pentru a vedea dac pointerul indic un carecter eof. Doar cnd s-a ajuns la sfritul
unei jumti sau la sfritul fiierului se fac mai multe teste. Cum ntre fiecare dou
caractere eof se gsesc N caractere de intrare, media numrului de teste pe caracterul
de intrare este foarte apropiat de 1.
forward := forward +1
if forward ^ := eof
then begin
if forward la sfritul primei jumti
then begin
rencarc a doua jumtate;
forward := forward + 1
end
else if forward la sfritul celei de-a doua jumti
then begin
rencarc prima jumtate;
mut forward la nceputul primei jumti
end
else /* am ajuns la sfritul irului de intrare */
termin analiza lexical
end

Fig. 2.6. Avansarea pointerului anticipativ cu santinele

2.6.SPECIFICAREA TOKEN-ILOR
Expresiile regulate sunt o noiune important pentru specificarea token-ilor.
Fiecare pattern produce o mulime de iruri, astfel expresiile regulate servesc pentru
denumirea acestor iruri.
Vom folosi noiunile introduse n cadrul teoriei limbajelor formale: cuvnt, prefix,
sufix, subcuvnt (subir), subcuvnt propriu i subsecven a lui s (un cuvnt format
cu simboluri ale lui s, pstrnd ordinea). Subsecvena se obine dintr-un cuvnt prin
eliminarea unor subcuvinte (de exemplu, aab este subsecven a cuvntului cabacab).
De asemenea, vom folosi operaii cu limbaje cunoscute deja.
Pag.18

TEHNICI DE COMPILARE

Exemplul 2.1 Fie urmtoarele dou alfabete:


L={A,B,,Z,a,b,,z} literele mari i mici
C={0,1,,9}
cifrele din baza 10
Cu aceste alfabete, pe care le putem chiar considera limbaje formate din cuvinte de
lungime 1, putem construi numeroase alte limbaje, cum ar fi:
L C = mulimea literelor i a cifrelor;
(a)
(b) LC = mulimea cuvintelor de lungime 2, formate dintr-o liter urmat de o
cifr;
(c) L4 = mulimea cuvintelor formate din exact patru litere;
(d) L* = mulimea cuvintelor formate din oricte litere (inclusiv cuvntul vid);
(e) L( L C )* = mulimea cuvintelor de lungime cel puin 1, formate din litere i
cifre, primul caracter fiind obligatoriu o liter;
(f)
C+ = mulimea irurilor numerice formate din cel puin o cifr.
Expresiile regulate sunt un mod condensat de reprezentare a limbajelor regulate.
O expresie regulat se poate defini recursiv prin:
1. - este o expresie regulat care indic cuvntul vid.
2. Dac a , atunci a este o expresie regulat care indic mulimea a.
3. Presupunem c r i s sunt expresii regulate indicnd limbajele L(r) i L(s).
Atunci urmtoarele expresii sunt regulate:
3.1. (r)
indic limbajul L(r)
3.2.
3.3.
3.4.

alternarea (r)|(s) indic limbajul L(r) L(s)


concatenarea (r)(s) indic limbajul L(r)L(s)

nchiderea (r)*
indic limbajul L(r)

Observaia 2.1
a) Parantezele exterioare folosite mai sus nu au nici o semnificaie i pot fi eliminate
dac dorim.
b) Operaiile , i | au precedena descrescnd de la cea mai mare ( ) la cea mai
mic ( | ).
c) Toate operaiile sunt asociative iar alternarea este i comutativ.
d) Dac dou expresii regulate r i s indic acelai limbaj, spunem c ele sunt
echivalente i notm r=s. De exemplu, (a | b) (b | a) .
Exist un numr de reguli algebrice pentru expresii regulate care pot fi folosite
pentru a le transforma n alte expresii echivalente, conform tabelului urmtor.
Tabelul 2.1
Regul
Descriere
r|s=s|r
Alternarea este comutativ
r|(s|t)=(r|s)|t
Alternarea este asociativ
(rs)t=r(st)
Concatenarea este asociativ
Pag.19

TEHNICI DE COMPILARE

r(s|t)=(rs)|(rt)
(s|t)r=(sr)|(tr)
r=r
r=r
r*=(r|)*
r**=r*

Concatenarea este distributiv la stnga


i la dreapta fa de alternare
Cuvntul vid este element neutru pentru concatenare

Idempotena nchiderii (produs Kleene)

2.6.1.Definiii regulate
Printr-o notaie convenabil dorim s dm denumiri pentru anumite expresii
regulate i s le dm i definiia, adic modul lor de construcie.
Dac este un alfabet de baz, atunci o definiie regulat este un ir de definiii de
forma:
d1 r1
d 2 r2
...
d n rn
unde fiecare di este un nume distinct i fiecare ri este o expresie regulat peste
simbolurile din {d 1 , d 2 ,..., d n } . Pentru a distinge numele de simboluri, convenim
s punem fiecare nume ntre .
Exemplul 2.1. Identificatorii din Pascal sunt cuvinte formate din litere i cifre,
ncepnd cu o liter. Deci definiiile regulate sunt n acest caz de forma:
litera mare A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P |
| Q | R |S| T | U | V | W | X | Y | Z
litera mic a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | q | r | s | t |
|u|v|w|x|y|z
litera litera mare | litera mic
cifra 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
id litera ( litera | cifra )

Exemplul 2.2. Numerele fr semn din Pascal sunt iruri de forma 5280, 39.37,
6.33E4 sau 1.89E-4. Acestea pot fi construite cu urmtoarele definiii:
cifra 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
cifre cifra ( cifra )
<fracie-opional> . cifre |
<exponent-opional> (E( | | ) cifre ) |
<numr> <cifre><fracie-opional><exponent-opional>
Se impun n practic anumite notaii prescurtate:
1. Semnul "+" pentru una sau mai multe apariii.
Pag.20

TEHNICI DE COMPILARE

r rr
r r |
2. Semnul "?" pentru o apariie sau nici una.
r? r |
De exemplu, relund Exemplul 2.2, putem defini:
cifra 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
cifre cifra
<fracie-opional> (. cifre )?
<exponent-opional> (E( | | ) cifre )?
<numr> <cifre><fracie opional><exponent opional>

3. Semnele "[" i "]" pentru delimitarea unei clase de caractere; n cazul unui
interval cunoscut de simboluri, se poate utiliza semnul "-" pentru precizarea clasei
respective.
De exemplu, [abc] indic expresia a|b|c, unde a,b,c sunt simboluri. Prin [a-z]
indicm expresia a|b|c||z.
Astfel, identificatorii se pot scrie:

id [A Za z][A Za z0 9] .
Observaia 2.2. Nu toate limbajele pot fi descrise prin expresii regulate. n cazul
limbajelor de programare, nu poate fi descris un ntreg limbaj de programare prin
expresii regulate, ci numai anumite pri din el (construcia elementelor de baz ale
limbajului, din simbolurile de baz).

2.7.RECUNOATEREA TOKEN-ILOR
Avnd o specificaie a token-ilor, problema care se pune este de a recunoate aceti
token-i.
Pentru c expresiile regulate sunt forme condensate de descriere a limbajelor
regulate (limbaje de tip 3), recunoaterea unor astfel de limbaje se face cu ajutorul
automatelor finite.
Ne vom folosi de urmtoarele rezultate din teoria limbajelor formale:
1) Construcia unui automat finit nedeterminist (cu -tranziii) dintr-o expresie
regulat.
2) Construcia unui automat finit determinist echivalent cu unul nedeterminist.
3) Construcia expresiilor regulate pornind de la o gramatic de tip 3.
4) Construcia automatului cu numr minim de stri echivalent cu un automat
determinist dat.
n final, recunoaterea token-ilor este realizat de un program care simuleaz
funcionarea unui automat finit determinist, indicat printr-o diagram de tranziie.
Exemplul 2.3 Considerm urmtorul fragment de gramatic:
Pag.21

TEHNICI DE COMPILARE

instr if expr then instr | if expr then instr else instr |


expr term oprel term | term
term id | num
unde terminalele sunt subliniate: if, then, else, oprel, id, num, ele fiind la rndul lor
definite de:

if if
then then
else else
oprel | | | | |
id litera(litera | cifra)*
num cifra+ (.cifra+)?(E(+|-)?cifra+)?
unde litera i cifra au fost definite n Exemplul 2.1.
Pentru acest fragment de limbaj, analizorul lexical recunoate cuvintele cheie if,
then, else i lexemele oprel, id i num. Vom presupune cuvintele cheie ca fiind
rezervate, adic nu pot fi folosite drept identificatori. Vom mai presupune c
lexemele sunt separate prin iruri nenule de caractere blank, tab i newline.
Analizorul lexical va sri spaiile albe folosind expresiile regulate definite de:
delim blank | tab | newline
ws delim+
n cazul descoperirii unei descrieri pentru ws, analizorul nu produce nici un token
ci caut urmtorul token situat dup ws.
inta noastr este de a construi un analizor lexical care s izoleze lexemele pentru
urmtorul token n buffer-ul de intrare i s produc ca ieire o pereche de forma
(token, atribut) folosind tabela de traducere dat n tabelul urmtor.
Tabelul 2.2
Expresie
Token
Atribut
regulat
ws
if
if
then
then
else
else
id
pointer n tabela de simboluri
id
num
pointer n tabela de simboluri
num
<
LT
oprel
<=
LE
oprel
=
EQ
oprel
<>
NE
oprel
>
GT
oprel
>=
GE
oprel
Vom construi nti un graf reprezentnd diagrama de stare sau diagrama
tranziiilor.
Pag.22

TEHNICI DE COMPILARE

n graf, nodurile sunt stri legate ntre ele prin sgei numite arcuri, care sunt
etichetate cu caractere de intrare. Eticheta "altceva" se refer la orice caracter care nu
este indicat prin celelalte arcuri care prsesc acelai nod.
Presupunem c schema este determinist (desigur c se poate implementa i una
nedeterminist).
O anumit stare este considerat stare iniial, simbolizat cu o sgeat de intrare
notat "start", iar anumite arce au i aciuni care trebuiesc executate cnd fluxul de
control parcurge un astfel de arc (de exemplu, citete un nou caracter).
Vom prezenta acum diagramele de tranziie pentru:
a) oprel
b) id i cuvintele cheie
c) num (numr fr semn n Pascal)
d) delim
start

<

>
altceva

return(oprel, LE)

return(oprel, NE)
*

return(oprel, LT)

=
>

return(oprel, EQ)
=
return(oprel, GE)

6
altceva

*
8

return(oprel, GT)

Fig. 2.9. Diagram de tranziie pentru operatori relaionali


liter sau cifr
start

liter

altceva
10

11 * return(gettoken(), install_id())

Fig. 2.10. Diagram de tranziie pentru identificatori i cuvinte cheie


Am notat cu * strile n care trebuie fcut "un pas napoi" n intrare. Cnd este atins
starea final 11, se va executa o anumit procedur pentru a determina dac lexema
care a condus la starea de acceptare este un cuvnt cheie sau un identificator. O cale
de a realiza acest lucru este de a pstra cuvintele cheie ntr-o tabel de cuvinte
rezervate care se iniializeaz nainte de nceperea analizei lexicale.
Funcia install_id() are acces la buffer, unde este pstrat lexema identificatorului.
Se examineaz tabela de cuvinte rezervate i, dac identificatorul este gsit aici,
atunci install_id() returneaz valoarea 0. Dac nu, trebuie cutat n tabela de
identificatori i, dac identificatorul este gsit, install_id() ntoarce perechea adresa,
pointer n tabela de simboluri. Dac lexema nu exist nici aici, atunci ea este instalat
ca o variabil, iar pointerul nou creat este returnat de funcia install_id().
Pag.23

TEHNICI DE COMPILARE

Funcia gettoken() privete i ea n tabela de cuvinte rezervate. Dac lexema este


gsit, atunci ea devine token-ul corespunztor din tabel. n caz contrar, ieirea este
token-ul "id".
Este important de observat faptul c plasarea cuvintelor cheie ntr-o tabel de
simboluri este esenial pentru micorarea numrului de stri din diagrama
tranziiilor, care n caz contrar ar avea cteva sute de stri.
Diagrama de tranziie pentru unitatea lexical numr este prezentat n Fig. 2.11.
Aciunea pe care o execut analizorul atunci cnd o stare final este atins este
apelarea procedurii install_num() care instaleaz lexema citit ntr-o tabel de numere
i ntoarce un pointer ctre adresa de intrare n acea tabel. Analizorul ntoarce
unitatea lexical numr cu acest pointer
cifr ca atribut.
cifr
start

13

12

cifr

.
E

14
16
66

altceva
*

27

77
cifr

cifr
cifr

15

return(numr, install_num())

16

66

17

cifr

altceva

18

77
cifr

19

return(numr,install_num())

*
altceva

24

return(numr, install_num())

Fig. 2.11. Diagram de tranziie pentru numere


n toate cele trei cazuri de mai sus, efectul atingerii unei stri finale este: return(
num,install_num() ).
Lexema pentru o astfel de diagram poate fi cea mai lung posibil.
Cnd una din strile finale (19, 24, 27) este atins, este chemat subrutina install_
num(), care introduce lexema n tabela de numere i scoate la ieire pointerul creat n
tabel. Analizorul furnizeaz token-ul "num" cu acest pointer ca valoare lexical.
Se pot folosi i informaii despre limbaj care nu sunt n definiiile regulate. De
exemplu, cnd apare secvena "1. x ", diagrama ne conduce n starea 14 , cu
urmtorul caracter <. Aici, n loc s returnm ntregul 1, mai degrab am dori s
corectm eroarea n 1.0 x . Astfel de cunotine pot fi folosite i pentru a simplifica
diagrama de tranziie.
O secven de diagrame de tranziie pentru unitile lexicale din exemplul 2.4 este
obinut dac unim diagramele din Fig. 2.9., 2.10. i 2.11. Singura problem care
rmne este tratarea delimitatorilor, problem diferit de celelalte, deoarece
analizorul nu ntoarce nimic n cazul identificrii unui delimitator. O diagram de
tranziie pentru recunoaterea delimitatorilor ar putea fi:
Pag.24

TEHNICI DE COMPILARE
delimitator
start

delimitator
23

altceva
24

25

Fig. 2.12. Diagrama de tranziie pentru delimitatori


Observaia 2.3. Pentru c spaiile goale apar frecvent, punerea diagramei de stare
delim la nceputul celorlalte mbuntete viteza de execuie ntr-o implementare a
diagramei de stare.

2.8.IMPLEMENTAREA UNEI DIAGRAME DE TRANZIIE


Un ir de diagrame de tranziie poate fi convertit ntr-un program care s determine
token-ii specificai de diagram.
Vom prezenta o conversie care s funcioneze pentru orice fel de diagram de
tranziie.
Fiecare stare va produce un segment de program. Dac exist arce care pornesc
dintr-o stare, atunci programul strii citete un caracter i selecteaz un arc dac este
posibil. Este folosit funcia nextchar(), care pentru a citi urmtorul caracter din
buffer-ul de intrare avanseaz pointerul forward la fiecare chemare i returneaz
caracterul citit. Dac exist un arc etichetat cu caracterul citit sau etichetat cu un
caracter-clas (liter, cifr) din care face parte i caracterul citit, atunci controlul este
transferat programului strii n care intr respectivul arc. Dac nu exist un astfel de
arc i starea curent nu este una care indic faptul c un token a fost gsit, atunci o
rutin fail() este apelat pentru a ntoarce pointerul forward la poziia pointerului de
nceput i pentru a ncepe o cutare pentru un token specificat de urmtoarea
diagram de stare (partea nedeterminist). Dac nu exist nici o alt diagram a
tranziiilor de ncercat, funcia fail() cheam o alt subrutin, de descoperire a erorii.
La returnarea unui token vom folosi o variabil global lexical_value, care este
atribuit pointerului returnat de funciile install_id() i install_num() atunci cnd
este gsit un identificator sau, respectiv, un numr. Clasa token-ului este obinut de
funcia principal a analizorului lexical, numit nexttoken().
Se va folosi o instruciune case pentru a gsi starea iniial a urmtoarei diagrame
de stare i dou variabile states i start care pstreaz starea curent i, respectiv,
starea iniial a diagramei de tranziie curente.
Programul pentru starea iniial (0) este modificat aplicnd anterior subrutina de
eliminare a spaiilor goale.
Prezentm n continuare secvene de cod C pentru funcia fail(), care gsete
urmtoarea stare start, i codul C pentru analizorul lexical (funcia nexttoken()).
int state=0, start=0;
int lexical_value; /* pentru a returna a doua component a token-ului*/
int fail()
{ forward=token_beginning;
Pag.25

TEHNICI DE COMPILARE

switch (start)
{ case 0: start=9; break;
case 9: start=12; break;
case 12: recover(); break;
default: /*eroare de compilare*/
}
return start;
}
token nexttoken()
{ while(1)
{switch (state)
{case 0: c=nextchar();/* c este caracterul privit
nainte */
if (c==blank || c==tab || c==newline)
{ state=0;
lexeme_beginning++; /* nceputul
lexemei avanseaz cu 1 */
}
else if (c=='<') state=1;
else if (c=='=') state=5;
else if (c=='>') state=6;
else state=fail();
break;
case 1: c=nextchar();
if (c=='=') state=2;
else if (c=='>') state=3;
else state=fail();
break;
case 2: return (oprel,LE);
case 3: return (oprel,NE);
case 4: retract(1);
return (oprel,LT);
case 5: return (oprel,EQ);
case 6: c=nextchar();
if (c=='=') state=7;
else state=8;
break;
case 7: return (oprel,GE);
case 8: retract(1);
return (oprel,GT);
case 9: c=nextchar();
if (isletter(c)) state=10;
else state=fail();
Pag.26

TEHNICI DE COMPILARE

break;
case 10: c=nextchar();
if (isletter(c)) state=10;
else if (isdigit(c)) state=10;
else state=11;
break;
case 11: retract(1);
return (gettoken(),install_id());
case 12: c=nextchar();
if (isdigit(c)) state=13;
else state=fail();
break;
case 13: c=nextchar();
if (isdigit(c)) state=13;
else if(c=='.') state=14;
else if(c=='E') state=16;
else state=27;
break;
case 14: c=nextchar();
if (isdigit(c)) state=15;
else state=fail();
break;
case 15: c=nextchar();
if (isdigit(c)) state=15;
else if (c=='E') state=16;
else state=24;
break;
case 16: c=nextchar();
if (c=='+' || c=='-') state=17;
else if (isdigit(c)) state=18;
else state=fail();
break;
case 17: c=nextchar();
if (isdigit(c)) state=18;
else state=fail();
break;
case 18: c=nextchar();
if (isdigit(c)) state=18;
else state=19;
case 19: retract(1);
return (num, install_num());
case 24: retract(1);
return (num, install_num());
case 27: retract(1);
Pag.27

TEHNICI DE COMPILARE

return (num, install_num());


}}} /*final funcie nexttoken()*/
Fig. 2.13. Program C pentru analiza lexical
Procedurile install_id() i install_num() furnizeaz atributele corespunztoare de
intrare din tabela de simboluri pentru id i num, variabile globale.
Dac limbajul folosit la implementarea analizorului lexical nu are o instruciune
case, atunci se poate crea un tabel pentru fiecare stare, indexat dup caracter
(exemplu: state1[c], unde c este caracterul de intrare).
Urmtoarea stare n care trebuie s se treac dup citirea unui caracter din intrare
este determinat de codul din Fig. 2.13. Folosind construcia while(1) interogarea va
avea loc pn este ntlnit o instruciune return. Codul nepermind ca o unitate
lexical i un atribut s fie ntoarse simultan, install_id() i install_num() atribuie unei
variabile globale valoarea atributului corespunztor adresei de intrare n tabela de
simboluri a identificatorului sau numrului respectiv.

2.9.UN LIMBAJ PENTRU SPECIFICAREA


ANALIZORULUI LEXICAL
Limbajul LEX a fost introdus de Lesk i Schmidt n 1975, ca un limbaj pentru
specificarea analizorului lexical .
n limbaj sunt combinate forme, care descriu expresii regulate, cu aciuni, ca de
exemplu intrarea n tabela de simboluri.
Specificaiile Lex pot fi utilizate chiar dac nu exist un compilator Lex; aceste
specificaii pot fi transcrise ntr-un program folosind diagrama de tranziie.
n general Lex-ul este utilizat pentru a genera analizorul lexical, a.out, n modul
urmtor:
program surs Lex
lex.1

Lex compiler

lex.yy.C

ir de intrare
a.out

C compiler

a.out

ir de token-i

1. Se creaz un program n limbajul Lex, lex.1, care conine specificarea


analizei lexicale.
2. Programul lex.1 este introdus n compilatorul Lex pentru a produce un
program n limbajul C, lex.yy.c . Programul lex.yy.c este o reprezentare
tabelar a diagramei de tranziie construit din expresia regulat a lui lex.1
mpreun cu o rutina standard care folosete tabela pentru recunoaterea
Pag.28

TEHNICI DE COMPILARE

lexemelor. Aciunile care sunt asociate cu expresiile regulate din lex.1 sunt
secvene de cod C i sunt introduse direct n lex.yy.c.
3. lex.yy.c este executat folosind un compilator C pentru a produce un program
obiect a.out, analizatorul lexical care transform un ir de intrare ntr-un ir
de token-i.

2.9.1. Specificaii Lex


Un program Lex are trei pri:
declaraii
%%
reguli de traducere
%%
proceduri auxiliare
Seciunea de declaraii include declaraii de variabile, constante i definiii
regulate. Definiiile regulate sunt similare cu cele prezentate nainte i sunt folosite
drept componente ale expresiilor regulate care apar n regulile de traducere.
Regulile de traducere ale unui program Lex sunt instruciuni de forma:
p1 {aciune 1}
p2 {aciune 2}
.......................
pn {aciune n}
unde fiecare pi este o expresie regulat i fiecare aciune i este un fragment de
program care descrie ce aciune trebuie executat cnd forma pi produce o lexem.
n Lex , aciunile sunt scrise n limbajul C.
n seciunea a III-a apar procedurile auxiliare care sunt necesare aciunilor. Aceste
proceduri pot fi compilate separat i adaugate apoi la analizorul lexical.
Aceast seciune este opional i dac nu apare atunci dispare i ultimul %% .
Fiind format numai din instruciuni n limbajul C, aceast seciune este introdusa
cuvnt cu cuvnt n fiierul generat.
Analizatorul lexical produce token-ii pentru parser. Pentru a obine valoarea
atributiv a token-ului cu informaii despre lexem, putem folosi o variabila global,
yylval (toi identificatorii din Lex ncep cu yy).
Prezentm un exemplu de program Lex pentru expresiile regulate care descriu
lexemele pentru tokenii fragmentului de gramatica definit anterior:
ws, if, then, else, id, num, <, <=., =, <>, >=, >.
%{
/* definiii ale constantelor LT, LE, EQ, NQ, GT, GE,
IF,THEN, ELSE ,ID, NUM, OPREL*/
%}
/* definiii regulate */
delim [ \t \n]
ws {delim}+
litera [A-Za-z]
cifra [0-9]
Pag.29

TEHNICI DE COMPILARE

id
{litera}({litera}| {cifra})*
num {cifra}+(\.{cifra}+)?(E[+\-]?{cifra}+)?
%%
{ws} {/*nici o actiune i nici un return*/}
if
{return(IF);}
then {return(THEN);}
else {return (ELSE);}
{id} {yylval=install_id( ) ; return (ID);}
{num} {yylval=install_num( ); return (NUM);}
< {yylval=LT; return(OPREL);}
<= {yylval=LE; return(OPREL);}
= {yylval=EQ; return(OPREL);}
<> {yylval=NE; return(OPREL);}
> {yylval=GT; return(OPREL);}
>= {yylval=GE; return(OPREL);}
%%
install_id( ){ /*procedura de instalare a lexemei, a carei prim caracter este pointat
de yytext i a crei lungime este yylong , n tabela de simboluri, i
returnarea unui pointer thereto*/
}
install-num( ){
/*procedura similar de instalare a lexemei care este un numar */
}
Observaia 2.4. n seciunea de declaraii sunt definite constantele declarate folosite
de regulile de traducere. Aceste declaraii sunt cuprinse ntre paranteze speciale de tip
%{ i }%. Orice apare ntre astfel de paranteze este copiat direct n analizatorul
lexical lex.yy.c i nu este tratat ca parte a definiiilor regulate sau a regulilor de
traducere. Exact acelai tratament este aplicat i procedurilor auxiliare din seciunea a
treia (install_id i install_num), acestea sunt copiate n lex.yy.c caracter cu caracter.
De asemenea n aceast seciune sunt incluse i cteva definiii regulate.
Fiecare definiie este format dintr-un nume i o expresie regulat indicat de acel
nume. De exemplu delim se folosete pentru clasa [ \t\u ], adic oricare din cele trei
simboluri: blank, tab(t) sau newline (n).
A doua definiie este a spaiilor albe (ws) care reprezint orice ir nevid de
delim. Se observ c delim este introdus ntre acolade pentru a se distinge de
patternul constand din cinci litere delim.
n definiia literei se folsete deasemenea simbolul de clasa [A-Za-z] care
indic toate literele mari de la A la Z i cele mai mici de la a la z.
Parantezele { } folosite n definiia id sunt folosite numai pentru grupare, iar | este
metasimbolul Lex folosit pentru reuniune.
n definiia num se folosete metasimbolul ? nsemnnd zero sau o apariie i \
pentru [+\-] pentru a nu confunda cu caracterul folosit n descrierea unei clase care
cuprinde un interval.
Exist i alt cale de a face ca un ir de caractere s aib sensuri naturale:
introducerea ntre apostrofuri.
Pag.30

TEHNICI DE COMPILARE

Acum s considerm regulile de traducere din seciunea care urmeaz dup primul
%%. Prima regul spune c dac apare ws nu exist nici o aciune. A doua regul
spune c dac se observ literele if, atunci ntoarce tokenul IF care este o constant
declarat reprezentnd un anume interes pentru parser. Urmtoarele reguli procedeaz
la fel cu tokenul then i else.
n regula pentru id se observ dou aciuni:
-nti variabila yylval ia valoarea install_id( ), o procedura descris n
seciunea a treia. yylval este o variabil a crei definiie apare n ieirea lex, lex.yy.c,
i este de asemenea folosit de parser, reprezentnd valoarea lexical;
-apoi return (id) ntoarce numai codul pentru clasa tokenului.
Nu prezentam detaliile pentru procedura install_id. Totui putem spune a ea
privete n tabela de simboluri pentru lexema produs de patern-ul id. Procedura
folosete dou variabile yytext i yylong.Variabila yytext corespunde acelei variabile
pe care am numit-o nceput de lexem i este un pointer la primul caracter al
lexemei; yyleng este un ntreg artnd ct de lung este lexema.
Presupunem c analizatorul lexical rezultat din programul lex are o intrare
constnd din dou tab-uri, literele if i un blank. Cele doua tab-uri sunt cel mai lung
prefix al unui pattern i anume ws. Pentru ws nu exist nici o aciune aa c
analizorul lexical mut pointerul de nceput de lexem, yytext , la i i ncepe s
caute urmtorul token.
Urmtoarea lexem este if. Aici este posibil s considerm i patternul if i
{id} pentru aceiai lexem i nici una din forme nu este mai lung dect cealalt.
Pentru c patternul if precede patternul {id} conflictul este rezolvat n favoarea
cuvntului cheie if. n general pentru rezolvarea unor asemenea conflicte este bine s
folosim o strategie prin care cuvintele cheie sunt rezervate i listate nainte de
patternul pentru identificatori.
Pentru alt exemplu presupunem c <= sunt primele dou caractere citite.
Strategia Lex de selectare a celui mai lung prefix elimina conflictul dintre <= i <.
2.9.2.Operatorul de privit nainte, lookahead
Exist situaii cnd analizatorul lexical trebuie s priveasc nainte depind
sfritul lexemei pentru a determina tokenul cu siguran. (Ex. DO n Fortran). n
limbajul Lex se folosete operatorul de privit nainte (lookahead) / i atunci un pattern
arat sub forma r1/r2 unde r1 i r2 sunt expresii regulate. Aceast succesiune
definete forma prin expresia regulat r1, dar numai dac este urmat de expresia r2.
De exemplu specificaia Lex pentru a recunoate cuvntul cheie DO din Fortran este:
DO/({litera}|{cifra})*=({litera}|{cifra})*,
Cu aceast specificaie, analizorul lexical va privi nainte pe bufferul su de intrare
cutnd un ir de litere i cifre urmate de =, urmat de un ir de litere i cifre, urmat
de virgul, pentru a fi sigur c nu este vorba de o instruciune de atribuire. Atunci
numai caracterele D i O precednd operatorul / vor forma lexema. Dup ce s-a
precizat lexema, yytext puncteaz pe D i yyleng este egal cu 2.
Pag.31

TEHNICI DE COMPILARE

Observaia 2.5. O astfel de definiie recunoaste DO i dac este urmat de un ir


fr sens ca Z4=6Q, dar nu-l va considera nici o dat ca parte a unui identificator.
Exemplul 2.3. Operatorul de privit nainte poate fi folosit i pentru a rezolva o alt
dificultate a analizei lexicale n Fortran : distincia dintre identificatori i cuvinte
cheie.
O cale de a preciza cuvntul cheie IF n Lex este de a defini contextul sau drept
folosind un operator lookahead. Forma simpla a instruciunii IF este:
IF(condiie) instruciune
n Fortran 77 se introduce alt form:
IF(conditie) then
bloc-then
ELSE
bloc-else
ENDIF
Se observ c fiecare instruciune neetichetat din Fortran ncepe cu o litera i
fiecare parantez dreapt folosit pentru indiciere sau grupare trebuie s fie urmat de
un simbol ca =, + sau , sau alt paranteza dreapt sau sfritul instruciunii. O astfel
de parantez dreapt nu poate fi urmat de o litera. n aceast situaie pentru a
confirma ca IF este un cuvnt cheie i nu un identificator, analizm simbolurile
urmtoare pentru a gsi o parantez dreapt urmat de o litera nainte de un caracter
newline. Patternul pentru cuvntul cheie IF poate fi scris atunci ca:
IF/ \( . * \) {litera}
Observaia 2.6. Punctul nseamn orice caracter far newline i \ din faa
parantezelor i arat Lex-ului c ceea ce urmeaz este un caracter nu un metasimbol
pentru gruparea expresiilor regulate.
O alt cale de a ataca problema este de a vedea dup ce ntlnim IF( dac IF
nu este declarat ca un tablou.
2.9.3. Reguli sintactice pentru Lex
Formatul general este al unui program n Lex este:
definiii
%%
reguli
%%
subrutine utilizator
Una sau toate aceste trei seciuni pot lipsi. Dac lipsete a treia seciune atunci
se poate renuna la al doilea %%, dar primul, %% este esenial pentru marcarea
nceputului regulilor.
2.9.3.1. Sintaxa regulilor
- regulile ncep din prima coloan i sunt formate dintr-un pattern urmat
eventual de o aciune;
- patternurile sunt scrise cu ajutorul expresiilor regulate;
Pag.32

TEHNICI DE COMPILARE

- aciunile sunt fragmente de programe C pentru a fi executate dac patternul


este gsit;
- patternurile sunt separate de aciuni prin spaii sau tab-uri.
Observaia 2.7. Considerm ca intrare un fiier de intrare ncwl.l . Acest program
scaneaz un text introdus de la tastatura sau dintr-un fiier calculnd numrul de
caractere, de cuvinte i de linii. Crearea fiierului C se face cu comanda
lex [opiuni] ncwl.l
Pentru a realiza un program executabil se parcurg dou etape:
- translatarea sursei de ctre Lex ntr-un program realizat n limbajul gazd, cu
comanda:
lex[-tvfn] nume_fisier_sursa;
- compilarea programului generat, de obicei mpreuna cu o bibliotec de
subrutine LEX.
Dup ce programul este compilat n C el se poate lansa n execuie lansnd
simplu nume.exe. Se introduce apoi textul de analizat, terminarea fcndu-se cu
CTRLZ. Dac dorim verificarea scanrii pe un fisier, introducem comanda
nume.exe > fiier.
2.9.3.2. Sintaxa expresiilor regulate
1) Caractere text i caractere operator
O expresie regulat conine caractere text i caractere operator ale
metalimbajului. Literele i cifrele sunt ntotdeauna caractere text. Operatorii sunt:
\ [ ] -?.* +|( ) $/{ }%< >#/
unde # i / nu sunt caractere text dac apar n coloana I.
Cnd se dorete utilizarea acestor caractere operator drept caractere text ele
trebuiesc protejate. Acest lucru se face prin introducerea ntre ghilimele sau prin
introducerea caracterului \ nainte de caracterul propriu-zis. De exemplu:
xyz\+\+
care este echivalent cu
xyz ++
i cu
xyz++
n mod normal blanc-urile i tab-urile ncheie o regul.
Observaia 2.8. Sunt recunoscute:
\n (newline)
\t (tab)
\b (backspace)
Pentru a introduce caracterul \ se folosete \ \ .
2) Clasa de caractere
O clas de caractere se definete cu ajutorul caracterelor [ i ]. Astfel [abc]
indic un caracter care poate fi a, b sau c. Se mai folosesc metasimbolurile -, \ i .
Pag.33

TEHNICI DE COMPILARE

- indic domenii n cadrul unei clase. Astfel [a-z0-9] indic litere mici de la a
la z i cifrele de la 0 la 9.
Observia 2.9. Daca i - apare ntre caracterele unei clase el trebuie s fie primul
sau ultimul caracter din clasa respectiv:

[-+01] sau [+01-]


trebuie s fie primul caracter din clas i se folosete pentru a indica
complementara:
[abc] reprezint toate caractere cu excepia caracterelor a, b i c.
[ \t\u] sau [\b\t\u] reprezint orice caracter diferit
de spaiu, tab sau newline.
\ folosit ntr-o clas poate avea i semnificaia unui cod din octal. De exemplu
[\ 40 - \ 176] reprezint un caracter ASCII de la 40(8) (blanc) pn la 176(8) (tilda).
3) Caracter arbitrar. Acesta se indic prin caracterul i reprezint orice caracter
cu excepia lui newline.
4) Expresii opionale. Aceste se indic prin caracterul ? i
reprezint zero sau o apariie a caracterului precedent. Astfel
ab?c
reprezint ablonul pentru ac sau abc.
4) Expresii repetate. Aceste se indic folosind caracterele
operator *,+, { i }.
De exemplu:
AAA
descrie acelai ablon ca i
A{3}
iar
[a-z] {1,5}*
descrie cuvinte cu litere mici cu lungimea cuprins ntre 1 i 5.
5) Alternare i grupare. Pentru alternare se folosete
caracterul | iar pentru grupare se folosesc parantezele: ( i ).
6) Dependena de context se indic cu ajutorul caracterelor:
, $ i /
^ Dac primul caracter dintr-o expresie regulat este expresia este
recunoscut numai la nceput de linie.
$ Dac ultimul caracter este $ expresia este recunoscut numai cnd este
urmat de un newline
/ Indic contextul drept. Astfel:
ab/cd
reprezint cuvntul ab numai dac este urmat de cuvntul cd
iar
Pag.34

TEHNICI DE COMPILARE

ab$
este echivalent cu
ab/ \n
7) Definiii. Pentru definiii se folosesc tot parantezele { i }.
De exemplu {cifr} reprezint definiia pentru cifr.
8) \( i \) sunt paranteze folosite pentru a indica caracterul care
se repet iar repetarea primului, a celui de-al doilea sau a celui de-al IX-lea caracter
din ablon se face prin \1,\2, sau \9.
Astfel de exemplu:
\([a-z]\)\1
reprezint cuvinte din dou litere identice iar
`
\([a-z]\)\([a-z]\)[a-z]\2\1
reprezint palindroame formate din cinci litere.

2.9.4. Aciuni Lex


La identificarea unei expresii regulate se execut aciunea indicat n limbajul
C. Printre aciuni exist o aciune implicit i anume copierea intrrii la ieire.
Variabile definite implicit de limbajul lex au numele ncepnd cu yy pentru a
nu se confunda cu alte variabile definite. Astfel:
yytext este o variabil de tip tablou care conine lexema
yytext este i pointer n tablou.
yylong este o variabil care conine numrul de caractere al lexemei curente,
astfel ultimul caracter din lexem este yytext [yyleng-1].
Pentru a numra cuvintele i caracterele din cuvintele de intrare se poate folosi
secvena:
[a-z A-Z]+
{words++; chars+ =yyleng}
care va aduna n chars numrul de caractere al cuvntului i va mri coninutul
variabilei words cu 1.
Limbajul Lex mai dispune i de o serie de proceduri implicite a cror nume
ncepe tot cu yy, astfel sunt yymore { } i yyless (n).
yymore{ } -poate fi apelat pentru a indica faptul c urmtoarea expresie de
intrare recunoscut trebuie s fie adugat la sfritul ultimei lexeme.
yyless(n)- arat c se rein numai primele n caractere din yytext , restul fiind
napoiate n intrare.
Aceste proceduri seamn cu folosirea operatorului contextual /. Astel, de exemplu,
dac un ir e definit ca ir de caractere ntre ghilimele i se cere ca apariia n ir s
fie precedat de \ , atunci expresia regulat devine confuz i este preferabil s
scriem:
\[]* {if (yytext[yyleng-1]= =`\\`) yymore ( )
else <<...procesare dorit de utilizator >>
}
Pag.35

TEHNICI DE COMPILARE

care atunci cnd va ntlni abc\def va potrivi numai primele cinci caractere abc\,
dup care apelul lui yymore va face s fie adugat i def. De notat ca semnul
trebuie tratat n <<procesarea dorit de utilizator >>.

2.9.5. Exemple de programe Lex


Exemplul 2.4. Programul ncwl.l este un program Lex care calculeaz numrul de
caractere de cuvinte i de linii ale unui text surs:
%{ int characters = 0;
int words = 0;
int lines = 0;
%}
%%
\n {
++lines;
++characters;
}
[ \t]+
characters += yyleng;
[^ \t\n]+ {
++words;
characters += yyleng;
}
%%
Exempll 2.5. Programul dc1.l transform succesiunile de cifre consecutive n ntregul
corespunztor i returneaz tokenul integer. n plus dac ntlnete unul din
caracterele +,-, * sau caracterul newline atunci afieaz lexema iar spaiile i
caracterele tab le elimin.
%{
#include "ytab.h"
extern int yylval;
%}
%%
[0-9]+

}
[-+/*\n]
[ \t]+

{
yylval = atoi(yytext);
return INTEGER;
return *yytext;
;

2.10.AUTOMATE FINITE
O diagram de tranziie generalizat se numete automat finit. Un automat finit
poate fi determinist (AFD), caz n care arcele care pleac dintr-o stare sunt etichetate
Pag.36

TEHNICI DE COMPILARE

cu simboluri diferite, sau nedeterminist (AFN), unde din anumite stri pleac arce
etichetate cu un acelai simbol. Amndou tipurile de automate pot recunoate foarte
precis mulimile regulate. Deci, automatele finite recunosc exact ceea ce recunosc i
expresiile regulate. n ceea ce privete timpul de lucru, un AFD conduce la o
recunoatere mult mai rapid a unui cuvnt dect un AFN, ns un AFD ocup mult
mai mult spaiu dect un AFN.
n cele ce urmeaz vom prezenta o metod de transformare a expresiilor regulate
n automat finit determinist.
2.10.1.Construirea unui AFD pornind de la o expresie regulat
Vom folosi o expresie regulat r extins, prin concatenarea cu simbolul special #.
Astfel, construind AFD pentru expresia r#, orice stare cu o #-tranziie va fi o stare
final.
Expresia regulat extins va fi reprezentat printr-un arbore sintactic cu simboluri
pe post de frunze i operatori pe post de noduri interioare. Un nod interior poate fi un
nod cat, or sau star, reprezentnd concatenarea, operatorul | sau *. Fig.2.24(a)
reprezint un arbore sintactic pentru o expresie regulat cu nodurile cat marcate prin
puncte. Arborele poate fi construit n aceeai manier n care construim arborele
sintactic pentru o expresie aritmetic. Frunzele pot fi etichetate cu simboluri din
alfabet sau cu . Fiecrei frunze neetichetate cu i asociem un numr natural unic
reprezentnd poziia simbolului respectiv. Un simbol poate avea deci mai multe
poziii. Strile numerotate ale AFN din Fig. 2.24(c) corespund poziiilor simbolurilor
din arborele sintactic al Fig. 2.24(a). Strile neimportante ale AFN sunt reprezentate
prin litere mari din alfabet n Fig. 2.24(c). O stare a unui AFN este important dac
are mcar o a-tranziie cu a .
n timpul construirii AFN dou mulimi de stri pot fi identificate dac au
aceleai stri importante. Astfel, AFD din Fig. 2.24(b) poate fi obinut din AFN
reprezentat n Fig. 2.24(c).
Pentru a construi AFD din expresia regulat r# vom utiliza urmtoarele patru
funcii: nullable, firstpos, lastpos i followpos. Pentru fiecare nod n din arborele
sintactic al expresiei r#, definim:
firstpos(n) ca fiind mulimea poziiilor care se potrivesc cu primul simbol al unui
cuvnt generat de subexpresia de rdcin n
lastpos(n)ca fiind mulimea poziiilor care se potrivesc cu ultimul simbol al unui
cuvnt generat de subexpresia de rdcin n
nainte ns de a afla firstpos i lastpos trebuie s tim care noduri din arbore pot
genera subexpresii pentru limbaje ce conin cuvntul vid. Aceste noduri sunt numite
nullable i pentru fiecare nod n considerm nullable(n ) adevrat, dac n este nod
nullable i fals, n caz contrar.
Pentru a afla valorile acestor funcii, utilizm o regul de baz referitoare la expresiile
formate dintr-un singur simbol i trei reguli inductive care ne permit s determinm
valorile funciilor lucrnd cu arborele sintactic de la frunze spre rdcin. Regulile
Pag.37

TEHNICI DE COMPILARE

corespund celor trei operaii: reuniunea, concatenarea i nchi-derea. Aceste reguli


sunt prezentate n Fig. 2.25.

.
.

.
#
6

a) Arbore sintactic pentru (a|b)*abb#.

b
4
a
3

start

1,2,3

a
1

1,2,3
,4

1,2,3
,5

1,2,3
,6
a

b
2

(b) AFD pentru (a|b)*abb#.

start

(c) AFN pentru (a|b)*abb#.

Fig. 2.24. AFD i AFN construite din expresia (a|b)*abb#.


Nodul n
n este o frunz
etichetat
n este o frunz cu
poziia i
n

c1

firstpos(n)

lastpos(n)

adevrat

fals

{i}

{i}

nullable( c1 )
sau
nullable(c2 )

firstpos( c1 )

firstpos( c2 )

lastpos(c1 )

lastpos( c2 )

nullable( c1 )
i
nullable( c2 )

if nullable(c1 ) then
firstpos( c1 )
firstpos(c2 )
else firstpos( c1 )

if nullable(c1 ) then
lastpos( c1 )
lastpos( c2 )
else lastpos(c2 )

c2

c1
n

nullable(n)

c2

Pag.38

TEHNICI DE COMPILARE
n

firstpos( c1 )

adevrat

lastpos( c1 )

c1

Fig. 2.25. Reguli pentru determinarea funciilor nullable, firstpos i followpos


Definim followpos(i) ca mulimea poziiilor care pot urma dup i n arborele
sintactic. Dou reguli definesc modurile n care o poziie poate urma alteia:
1. dac n este un nod cat cu ramura stng c1, ramura dreapt c2 i i este n
lastpos(c1), atunci toate poziiile din firstpos(c2) sunt n followpos(i)
2. dac n este un nod star i i este n lastpos(n), atunci toate poziiile din
firstpos(n) sunt n followpos(i).
Dac firstpos i lastpos au fost definite pentru toate nodurile, followpos poate fi
definit printr-o parcurgere n adncime a arborelui sintactic.
Exemplul 2.12. Fig. 2.26. prezint valorile funciilor firstpos i lastpos pentru
nodurile arborelui din Fig. 2.24(a); firstpos(n) apare n stnga nodului n i lastpos(n)
n dreapta.
{1,2,3}
{1,2,3}
{1,2,3}
{1,2,3}
{1,2}

{1,2}

{1,2}

{1,2}

{1} a {1}

{6}

{5}

{4}

{3}

{6} # {6}
{5} b {5}

{4} b {4}
{3} a {3}

{2} b {2}

Fig. 2.26. firstpos i lastpos pentru arborele sintactic al expresiei (a|b)*abb#.


S aflm acum followpos pentru arborele din Fig. 2.26. La nodul star, adugm
poziiile 1 i 2 la followpos(1) i followpos(2) conform regulii (2). La nodul printe al
nodului star adugm 3 la followpos(1) i followpos(2) conform regulii (1). La
urmtorul nod cat adugm 4 la followpos(3) conform regulei (1). Apoi adugm 5 la
followpos(4) i 6 la followpos(5) conform regulei (1). Astfel am definit funcia
followpos. Fig. 2.27. prezint funcia followpos sintetizat.
Nod Followpos
1
{1, 2, 3}
2
{1, 2, 3}
3
{4}
4
{5}
5
{6}
6
Fig. 2.27. Funcia followpos.
Pag.39

TEHNICI DE COMPILARE

Funcia followpos poate fi ilustrat construind un graf cu cte un nod pentru


fiecare poziie i arce orientate de la nodul i ctre toate nodurile din followpos(i). Fig.
2.28. reprezint graful funciei followpos din Fig. 2.27.
Este interesant de observat c aceast diagram va deveni un AFN fr _tranziii
pentru expresia regulat n discuie dac:
1. toate poziiile din firstpos (rdcin) sunt stri iniiale
2. fiecare arc (i,,j) l etichetm cu simbolul poziiei j
3. toate poziiile asociate simbolului # sunt fcute stri finale.
1

Fig. 2.28. Graf orientat pentru funcia followpos.


Din aceste motive nu trebuie s ne surprind faptul c putem converti graful
funciei followpos ntr-un AFD. Vom utiliza n acest scop urmtorul algoritm:
Algoritmul 2.5. Construcia AFD dintr-o expresie regulat.
Intrare:
o expresie regulat r.
Ieire:
un AFD. D pentru limbajul L(r).
Metod:
- Se construiete arborele sintactic T pentru expresia regulat extins (r)#
- Se construiesc funciile nullable, firstpos, lastpos i followpos prin
parcurgerea n adncime a arborelui T
- Se construiesc Dstates (mulimea strilor lui D) i Dtran (tabela de
tranziie pentru D) utiliznd procedura din Fig. 2.29. Strile din Dstates sunt
mulimi de poziii; la nceput, fiecare stare este nemarcat i devine marcat
dup ce au fost considerate toate tranziiile din acea stare. Starea iniial a lui D
este firstpos (rdcin), iar strile finale sunt toate strile care conin poziii
asociate simbolului #.
iniial, singura stare nemarcat din Dstates este firstpos(rdcin),
unde rdcin este rdcina arborelui sintactic al expresiei r#;
while exist o stare nemarcat T n Dstates do
begin
marcheaz T;
for fiecare simbol a do
begin
fie U mulimea poziiilor din followpos(p)
pentru o poziie p din T,
astfel nct simbolul de pe poziia p este a;
if U nu este vid i nu este n Dstates
then adaug U la Dstates ca stare nemarcat;
Dtran[T, a] := U
end
end

Fig. 2.29. Construcia unui AFD.


Pag.40

TEHNICI DE COMPILARE

2.11. REZUMAT
Faza de analiz lexical este prima i cea mai simpl dintre fazele de analiz
pentru c regulile gramaticale verificate sunt de tip 3. Intrarea ntr-un analizor lexical
este programul surs, considerat un ir de caractere iar ieirea este un ir de uniti
lexicale. n esen un analizor lexical este un automat finit care recunoate unitile
lexicale: cuvinte cheie, identificatori, etichete, constante numerice, alfanumerice sau
logice, operatori i separatori etc.
Implementarea unui analizor lexical se poate face sciind un program care s
simuleze funcionarea automatului finit care recumoate unitile lexicale sau
folosind generatorul de analizor lexical LEX.
Exerciii
1. Care este alfabeul de intrare pentru urmtoarele limbaje de programare:
a. PASCAL;
b. C;
c. JAVA.
2. Care este convenia utilizrii blanck-urilor n fiecare dintre limbajele de la ex.1.
3. Identificai lexemele din urmtoarele secvene de programe:
a. Prgramul C
#include<iostream.h>
int multa[9],multb[9],multc[9],n,m,i,j,k,gasit;
main()
{cout<<"nr de elemente al multimii A";cin>>n;
for(i=0;i<n;i++)
{cout<<"mult["<<i+1<<"]=";cin>>multa[i];
}
cout<<"nr de elemente al multimii B";cin>>m;
for(i=0;i<m;i++)
{cout<<"mult["<<i+1<<"]=";cin>>multb[i];
}
k=0;
for(i=0;i<n;i++)
{gasit=0;
for(j=0;j<=m && !gasit;j++)
if(multa[i]==multb[j])gasit=1;
if(gasit)multc[k++]=multa[i];
}
cout<<"A intersectat cu B"<<endl;
for(i=0;i<k;i++)cout<<multc[i]<<endl;
return i;
}

b. Programul Java
public class Stiva
{ private int a[];
public Stiva()
{a=new int[0];
}
public Stiva(int q)
{ a=new int[1];
a[0]=q;
}
public Stiva(int q[])
Pag.41

TEHNICI DE COMPILARE
{ a=new int[q.length];
for(int i=0;i<q.length;i++)
a[i]=q[i];
}
public void push(int q)
{if (!this.contains(q))
{int b[]=new int[a.length+1];
b[0]=q;
for (int i=0;i<a.length;i++)
b[i+1]=a[i];
a=b;
}
}
public int pop()
{ return a[0];
}
public boolean contains(int b)
{ for(int i=0;i<a.length;i++)
if (a[i]==b)
return true;
return false;
}
public static void main(String args[])
{Stiva st=new Stiva();
st.push(10);
System.out.println(st.pop());
}
}

c. Programul Pascal
uses crt;
type mat=array[1..10,1..10]of 0..1;
var n:integer;
A,D:mat;
procedure cit_mat(var n:integer;var A:mat);
var i,j:integer;
begin
write('Introduceti nr. de noduri:');
readln(n);
writeln('Introduceti matricea de adiacenta:');
for i:=1 to n do
for j:=1 to n do
begin
write('a[',i,',',j,']=');
readln(a[i,j]);
end;
end;
procedure Roy_W(n:integer;A:mat;var D:mat);
var i,j,k:integer;
begin
D:=A;
for K:=1 to n do
for i:=1 to n do
for j:=1 to n do
if (D[i,j]=0) and(i<>k) and(j<>k) and(j<>k)
then if D[i,k]<=D[k,j]
then D[i,j]:=D[i,k]
else D[i,j]:=D[k,j];
end;
procedure afisare(n:integer;D:mat);
var i,j:integer;
begin
writeln;writeln('Matricea drumurilor :');
for i:=1 to n do
Pag.42

TEHNICI DE COMPILARE
begin
for j:=1 to n do
write('
',D[i,j]);
writeln;
end;
end;
begin{program principal}
clrscr;
cit_mat(n,A);
Roy_W(n,A,D);
afisare(n,D);
readln;
end.

4. Scriei un program C pentru funcia nextchar(), folosind o schem cu


santinele.
5. Descriei limbajul indicat de urmtoarele expresii regulate:
a. 0(0|1)*1;
b. ((|1)0*)*;
c. (0|1)* 0(0|1)(0|1);
d. 0*10*10*;
e. (00|11)* ((01|10) (00|11)*(01|10) (00|11)*)*.
6. Scriei definiii regulate pentru urmtoarele limbaje:
a. Toate cuvintele formate numai din vocalele: a,e i, o, u, n acest ordine;
b. Comentariile din limbajul C;
c. Toate numerele formate din exact 5 cifre n care nici o cifr nu se repet;
d. Toate numerele formate din 4 cifre n care se repet cel mult o cifr;
e. Toate irurile de 0 i 1 care nu conin subirul 011;
7. Scriei un program n limbajul Lex care copiaz un fiier nlocuind fiecare
apariie a unui ir de spaii printr-un singur spaiu.
8. Scriei un program n limbajul Lex care copiaz un program Pascal i
nlocuiete fiecare apariie a cuvntului double prin cuvntul real.
9. Scriei un program n limbajul Lex care numr apariiile cuvintelor: real,
integer, boolean, single, double, dintr-un program Pascal.

Pag.43

TEHNICI DE COMPILARE

Acest capitol prezint metodele tipice de analiz gramatical folosite n


scrierea compilatoarelor. nti vom prezenta conceptele de baz; apoi
tehnicile folosite pentru implementare i n final algoritmii utilizai n
generarea automat a unui analizor sintactic. Deoarece programele pot
conine erori sintactice, vom extinde metodele de analiz astfel nct s
permit tratarea i corectarea erorilor.

3.1. FAZA DE ANALIZ SINTACTIC


Fiecare limbaj de programare are reguli precise care descriu structura sintactic a
programelor bine formate.
Sintaxa unui limbaj de programare se descrie cu reguli IDC, n forma BNF sau
EBNF, care, ns, nu dau o descriere complet a limbajului, pentru c mai sunt i
elemente de sintax dependente de context cum ar fi:
Corespondena dintre parametrii formali i parametrii actuali;
Corespondena dintre tipurile identificatorilor.
Pentru c n cazul gramaticilor IDC exist algoritmi de analiz bine pui la punct de
analiz, dar nu i n cazul gramaticilor DC, se realizeaz analiza sintactic numai
pentru regulile IDC iar restriciile dependente de context sunt analizate n faza de
analiz semantic.
Intrarea n analizorul sintactic este irul unitilor lexicale, obinut n faza de analiz
lexical, iar ieirea este un arbore de sintax folosit n construcii semantice i n
generarea de cod. n afar de arborele sintactic, n faza de analiz sintactic se
afieaz i erorile sintactice.
O metoda general de analiz pentru gramaticile IDC este de a construi un automat
push-down nedeterminist (din forma normal Greibach) i de a simula funcionarea
lui pe irul unitilor lexicale sau de a folosi algoritmul CYK (Cooke-JoungerKasami) pornind de la forma normal Chomsky. Ambele soluii, prezentate n [2],
sunt ineficiente n practic.
Prezentm n continuare alte dou construcii ale unor automate push-down
nedeterministe, unul care construiete arborele sintactic pornind de la rdcin spre
rezultat (analiza sintactic descendent, top-down) i altul care construiete arborele
pornind de la rezultat spre rdcin (analiza sintactic ascendent, bottom-up).

3.1.1.Analiza sintactic top-down nedeterminist


S considerm o gramatic IDC, G=(VN,VT,S,P). Pentru o analiz sintactic de tip
top-down se costruiete un automat push-down, M, care s simuleze, pe stiv, o
Pag.44

TEHNICI DE COMPILARE

derivaie n gramatica G. Dac pe stiv a reuit s deriveze un prefix al irului de


intrare, atunci urmeaz o reducere a stivei i o deplasare pe intrare.
Automatul M este un automat push-down nedeterminist, care are drept simboluri
push-down simbolurile neterminale ale lui G, deci elementele mulimii VN, dintre
care simbolul iniial al gramaticii, S, este i simbol iniial al stivei n automatul M,
mulimea strilor conine o singur stare, q, iar funcia de tranziie, , este definit
dup cum urmeaz:
(q,) (q, , A), dac A este o regul din P;
(q,) (q, a, a).
Tranziiile de tip 1 se folosesc pentru generarea pe stiv a unui prefix al cuvntului
de intrare iar cele de tip 2 pentru reducerea stivei. Cele dou tipuri de tranziii se
folosesc alternativ: nti tranziii de tip 1 pn cnd se pot face reduceri cu -tranziii
de tip 2, apoi din nou tranziii de tip 1 alternnd cu tranziii de tip 2.
Automatul M astfel construit va fi M=({q},VT,VN,,q,S,), care recunoate
limbajul L(G) prin stiv vid.
Exemplul 3.1. Considerm o gramatic IDC care genereaz expresiile aritmetice
formate din identificatori, id , parantezele ( , ) i operatorii +, - , / i * .
Gramatica G va fi atunci G=(VN,VT,E,P), unde VN={E,A}, E este simbolul iniial
al gramaticii, VT={id,(,),+,-,/,*}, iar mulimea regulilor, P, conine urmtoarele
reguli:
E EAE , E (E) , E -E , E id
A+ , A- , A/ , A*
Atunci, pentru a recunoate limbajul expresiilor aritmetice generate de gramatica G
de mai sus, se construiete automatul push-down:
M = ({q},VT,VN,,q,E,), unde funcia de tranziie, , este definit dup cum
urmeaz:
(q, , E) = {(q, EAE), (q, (E) ), (q, -E), (q, id)}
(q, , A) = { (q, +), (q, -), (q, /), (q, *)}
(q, id, id)= {(q, )}
(q, (, ( ) = {(q, )}
(q, ), ) ) = {(q, )}
(q, +, +) = {(q, )}
(q, -, - ) = {(q, )}
(q, /, /) = {(q, )}
(q, *, *) = {(q, )}
Pentru analiza expresiei aritmetice id*(id+id) vom avea urmtoarele configuraii
instantanee:
(q, id *(id+id), E) (q, id *(id+id), EAE) (q, id *(id+id), idAE)
. (q, *(id+id), AE) (q, *(id+id), *E) . (q, (id+id), E)
(q, (id+id), (E) ) . (q, id+id), E) ) (q, id+id), EAE) )
(q, id+id), idAE) ) .(q, +id), AE) ) (q, +id), +E) ) .
. (q, id), E) ) (q, id), id) ) . (q, ), ) ) . (q, , )
Pag.45

TEHNICI DE COMPILARE

n exemplul de mai sus am notat prin . reducerile care se fac n stiva automatului.
Arborele de sintax al expresiei aritmetice se construiete pornind de la rdcin
spre rezultat, fiecare calcul n automatul de mai sus, care nu este reducere, genernd o
arborescen a lui.
E

id

( E

id

id
Figura 3.1.

3.1.2.Analiza sintactic bottom-up nedeterminist


O alt modalitate de analiz sintactic este de tip deplasare-reducere, analiz
ascendent (bottom-up), n care arborele de sintax se construiete de la rezultat spre
rdcin.
Automatul push-down M, care realizeaz analiza, va deplasa nti simbolurile de
pe banda de intrare pe stiv (tranziii de tip 1), pn cnd se formeaz pe stiv partea
drept a unei reguli de rescriere, moment n care se efectueaz o reducere (prin tranziii de tip 2). Dac n final stiva se reduce la simbolul iniial al gramaticii atunci
automatul i golete complet stiva (prin -tranziii de tip 3) i intr ntr-o stare final,
acceptnd cuvntul de pe banda de intrare. Automatul M are dou stri i lucreaz
puin diferit fa de definiia clasic a automatului push-down, n sensul c poate
terge de pe stiv un cuvnt ntreg, nu numai un simbol. n plus, stiva va fi scris
inversat, adic irul reprezentnd coninutul stivei va avea simbolul din vrf drept
cel mai din dreapta simbol. Se poate demonstra c aceast definiie este echivalent
cu cea iniial.
Pentru gramatica G=(VN,VT,S,P), automatul push-down construit conform celor de
mai sus va fi:
M=({q0,q1},VT,VN VT {$},,q0,$,{q1}),
unde funcia de tranziie este definit de:
(q0,a,Z) (q0,a ), oricare ar fi a VT i Z VN VT {$};
(q0,,) (q0,A) dac A este o regul din mulimea regulilor, P.
(q0,,$S) (q1,).
Pag.46

TEHNICI DE COMPILARE

Exemplul 3.2. Considernd aceeai gramatic din Exemplul 3.1, care genereaz
expresiile aritmetice cu cele patru operaii vom obine automatul push-down:
M=({q0,q1}, {id,(,),+,-,/,*}, {E,A,id,(,),+,-,/,*,$}, , q0, $, {q1}),
unde funcia de tranziie, , conine urmtoarele seturi de tranziii:
(q0, id, Z) ( q0, Zid )
(q0, (, Z) ( q0, Z( )
(q0, ), Z) ( q0, Z) )
(pentru orice simbol push-down, Z)
(q0, +, Z) ( q0, Z+ )
I.
(q0, -, Z) ( q0, Z- )
(q0, /, Z) ( q0, Z/ )
(q0, *, Z) ( q0, Z* )

II.

(q0, , EAE) ( q0, E )


(q0, , (E) ) ( q0, E )
(q0, , -E ) ( q0, E )
(q0, , id ) ( q0, E )
(q0, , + ) ( q0, A )
(q0, , - ) ( q0, A )
(q0, , / ) ( q0, A )
(q0, , * ) ( q0, E )

III.

(q0, , $E ) ( q1, )

n recunoaterea aceleiai expresii aritmetice, id*(id+id), se obin aici urmtoarele


configuraii instantanee:
(q0, id *(id+id), $) (q0, *(id+id), $id) (q0, *(id+id), $E)
(q0, (id+id), $E*) (q0, (id+id), $EA) (q0, id+id), $EA()
(q0, +id), $EA(id ) (q0, +id), $EA(E ) (q0, id), $EA(E+)
(q0, id), $EA(EA ) (q0, ), $EA(EAid ) (q0, ), $EA(EAE)
(q0, , $EA(EAE)) ) (q0, , $EA(E) ) (q0, ,$EAE )
(q0, ,$E ) (q1, , )
Se obine acelai arbore sintactic ca n exemplul anterior, numai c n acest caz
construcia lui ncepe de la rezultat spre rdcin.
Observaia 3.1 n ambele exemple cutarea acceptrii trebuie fcut ordonat, printrun algoritm de tip backtracking. Aici, pentru a scurta calculul, am ales numai varianta
care conduce la acceptare. Sigur c acest inconvenient dispare dac automatul pushdown folosit pentru analiz devine determinist. Acest lucru se ntmpl n cazul
limbajelor LL(k) i LR(k), pentru care exist algoritmi mult mai eficieni de analiz.

3.1.3. Rolul analizei sintactice


ntr-un compilator, analizorul sintactic primete, ca intrare un ir de uniti lexicale
de la analizorul lexical (cum se poate vedea i n Fig. 3.2.) i verific dac acest ir
Pag.47

TEHNICI DE COMPILARE

poate fi generat de gramatica asociat limbajului surs. Ne ateptm ca analizorul s


raporteze orice eroare sintactic ntr-un mod inteligibil i s dispun de metode de
tratare a erorilor pentru a putea analiza tot irul de intrare. Ieirea din analizorul
sintactic este arborele sintactic asociat programului surs.
unitate lexical
program
surs

analizor
lexical

analizor
sintactic

arbore

sintactic

cere urmtoarea
unitate lexical

tabela
de
simboluri

Fig. 3.2. Poziia analizorul sintactic ntr-un compilator


Metodele uzual folosite n dezvoltarea compilatoarelor sunt clasificate n metode topdown sau bottom-up. Cum indic i numele, metodele top-down genereaz arborele
sintactic de la rdcin (top) la frunze (down), pe cnd metodele bottom-up
procedeaz exact invers. n ambele cazuri, irul de intrare este citit de la stnga la
dreapta, cte un simbol o dat.

3.1.4 Tratarea erorilor sintactice


Cea mai mare parte a detectrii i tratrii erorilor ntr-un compilator este centrat n
jurul fazei de analiz sintactic. Vom prezenta cteva tehnici de baz pentru tratarea
erorilor sintactice; implementarea lor fiind discutat o dat cu prezentarea metodelor
de analiz sintactic din aceast seciune.
Tratarea erorilor ntr-un analizor sintactic urmrete trei scopuri:

prezena unei erori trebuie raportat clar i precis.

dup fiecare eroare detectat, analizorul sintactic trebuie s corecteze acea


eroare pentru a putea analiza ntreg programul.

tratarea erorilor nu trebuie s ncetineasc procesarea unui program corect.


Exist mai multe strategii generale pe care un analizor gramatical le poate urma pentru
a trata erorile sintactice. Cu toate c nici una din aceste strategii nu a fost unanim
acceptat, cteva metode sunt aplicate global, cum ar fi:

modul panic

nivelul expresie (fraz)

reguli (producii) eronate

corectare global.
Modul panic este metoda cea mai uor de implementat. Atunci cnd descoper o
eroare, analizorul terge simboluri din irul de intrare pn cnd o unitate lexical
Pag.48

TEHNICI DE COMPILARE

dorit este gsit (de obicei un delimitator). Aceast metod sare foarte des peste o
parte semnificativ a intrrii fr s verifice dac exist i alte erori, ns are avantajul
simplitii i al faptului c nu poate intra ntr-un ciclu infinit ca n cazul altor metode.
n situaia n care prezena mai multor erori ntr-o instruciune este rar, metoda poate
fi foarte util.
Nivelul expresie const ntr-o corectare local a intrrii rmase. Astfel, analizorul
poate nlocui un prefix al intrrii rmase cu un ir care-i va permite s continue
analiza. O corecie local tipic este nlocuirea unei virgule prin punct i virgul,
tergerea sau inserarea unui simbol punct_i_virgul. Trebuie avut grij ns ca aceste
nlocuiri s nu duc la un ciclu infinit, cum ar fi, de exemplu, inserare continu a unui
simbol naintea simbolului curent din intrare. Acest tip de nlocuiri pot corecta orice
ir de intrare i a fost folosit n implementarea multor compilatoare. Principalul
dezavantaj este dificultatea de a trata o eroare n situaia n care a aprut nainte de
momentul detectrii ei.
Strategia regulilor eronate poate fi utilizat atunci cnd cunoatem foarte bine erorile
ce pot fi ntlnite. Se utilizeaz o gramatic extins, n care au fost adugate producii
generatoare de structuri greite. Cnd o producie eronat este folosit de analizor,
putem diagnostica eroarea respectiv i preciza construcia greit n programul surs.
Corectarea global const ntr-o secven minimal de modificri care trebuie aplicate
programului surs pentru a obine o intrare corect. Exist algoritmi care, avnd ca
intrare un ir incorect x i o gramatic G, vor genera arborele sintactic pentru irul
asociat y, astfel nct numrul de inserri, tergeri sau nlocuiri de uniti lexicale
necesare pentru a ajunge de la x la y s fie minim. Din pcate, aceste metode sunt n
general prea costisitoare n ceea ce privete spaiul necesar i timpul de lucru, aa c
prezint doar interes teoretic.
Multe structuri ale limbajelor de programare se construiesc recursiv i pot fi definite
cu ajutorul gramaticilor independente de context. De exemplu, putem avea o
instruciune condiional definit printr-o regul ca:
Dac S1 i S2 sunt instruciuni i E este o expresie atunci
(3.1)
if E then S1 else S2 este o instruciune.
Aceast form a instruciunii condiionale nu poate fi specificat utiliznd expresiile
regulate, cum se ntmpla n cazul unitilor lexicale. Pe de alt parte, utiliznd o
variabil sintactic instr pentru clasa instruciunilor i expr pentru clasa expresiilor,
putem exprima (3.1) cu urmtoarea regul (producie) gramatical:
instr if expr then instr else instr

(3.2)

3.1.5 Construirea unei gramatici


Dac o gramatic produce mai muli arbori sintactici pentru o propoziie,
aceasta se numete ambigu. Altfel spus, o gramatic este ambigu dac produce mai
mult dect o derivaie la stnga sau la dreapta pentru o propoziie. Pentru anumite
Pag.49

TEHNICI DE COMPILARE

tipuri de analizoare sintactice, este de dorit ca gramatica s nu fie ambigu. Vom


prezenta n aceast privin o serie de reguli care transform o gramatic ambigu ntro gramatic fr ambiguiti.
Gramaticile pot descrie majoritatea structurilor din limbajele de programare.
Anumite condiii impuse intrrii, cum ar fi cerina ca un identificator s fie declarat
nainte de a fi utilizat, nu pot fi descrise totui cu ajutorul gramaticilor independente
de context. Pentru c fiecare metod de analiz sintactic folosete gramatici de o
anumit form, este posibil ca gramatica iniial s fie rescris. Vom considera n
continuare transformrile care trebuie fcute asupra unei gramatici pentru a se putea
utiliza metodele de analiz top-down.
Uneori, o gramatic ambigu poate fi rescris pentru a se elimina ambiguitatea.
De exemplu, vom elimina ambiguitatea din urmtoarea gramatic :
instr if expr then instr
if expr then instr else instr
(3.3)
altceva
unde altceva reprezint orice alt instruciune.
Gramatica (3.3) este ambigu deoarece cuvntului
if E1 then if E2 then S1 else S2
i putem asocia cei doi arbori sintactici din Fig. 3.3.

(3.4)

instr

if

expr

E1

instr

instr

then

if

expr then
E2

if

instr
S1

else

instr
S2

expr

then

instr

instr

else

S2

E1
if

expr
E2

then

instr
S1

Fig. 3.3. Doi arbori sintactici pentru o propoziie ambigu.


n orice limbaj de programare care are instruciuni condiionale de aceast
form, este preferat primul arbore sintactic. Regula general este potrivete else cu
cel mai apropiat then precedent nepotrivit. Aceast regul poate fi incorporat direct
n gramatic. Putem transforma (3.3) n gramatica fr ambiguiti (3.5).
instr instr_potrivit instr_nepotrivit
(3.5)
instr_potrivit if expr then instr_potrivit else instr_potrivit altceva
instr_nepotrivit if expr then instr if expr then instr_potrivitelse
instr_nepotrivit
Ideea este ca o instruciune care apare ntre un then i else s fie potrivit,
adic nu trebuie s se termine cu un then nepotrivit, urmat de o instruciune, deoarece
else va fi atunci forat s se potriveasc cu acest then. O instruciune potrivit este
Pag.50

TEHNICI DE COMPILARE

fie o instruciune if_then_else fr instruciuni nepotrivite, fie orice alt instruciune


necondiional. Gramatica (3.5) genereaz aceeai mulime de cuvinte ca i (3.3), dar
permite generarea unui singur arbore sintactic pentru (3.4) i anume cel care asociaz
fiecare else cu cel mai apropiat then precedent, nepotrivit.
3.1.5.1. Eliminarea recursiei stngi
O gramatic
este recursiv la stnga dac exist un neterminal A astfel nct s avem
*
o derivaie A A cu oarecare. Analiza top-down nu poate trata gramatici
recursive la stnga, deci o eliminare a recursiei stngi este necesar.
n cazul cel mai simplu, putem nlocui perechea de producii recursive la stnga
A A
prin produciile nerecursive la stnga:
A A
A A
fr a modifica mulimea cuvintelor care deriveaz din A.
Exemplul 3.3. Consideram urmtoarea gramatic pentru expresii aritmetice
EE+TT
TTFF
(3.6)
F (E) id
Prin eliminarea recursiei stngi immediate (producii de forma
A A)
vom obine gramatica cu regulile:
E TE
E + TE
T FT
(3.7)
T FT
F (E) id
Orict de multe A_reguli am avea, putem elimina recursia stng imediat
utiliznd urmtoarea tehnic. Mai nti grupm A_produciile astfel:
A A1 A2 Am 1 2 n
unde i nu ncepe cu A. Apoi nlocuim A_produciile prin
A 1A 2A nA
A 1A 2A mA
Neterminalul A va genera acelai cuvnt ca i nainte, dar nu mai este recursiv la
stnga. Aceast metod nu poate elimina ns recursia stng care implic derivaii n
doi sau mai muli pai. De exemplu, considernd gramatica
S Aa b
A Ac Sd
(3.8)
neterminalul S este recursiv la stnga deoarece S Aa Sda, dar nu este recursiv la
stnga imediat, ci n doi pai.
Algoritmul urmtor va elimina recursia stng dintr-o gramatic, dac aceast
gramatic nu conine
cicluri adic derivaii de forma
+
AA
Pag.51

TEHNICI DE COMPILARE

sau _producii adic producii de forma


A .
Algoritmul 3.1. Eliminarea recursiei stngi.
Intrare: o gramatic G fr cicluri sau _producii.
Ieire: o gramatic G echivalent cu G, far recursii stngi.
Metod: se aplic algoritmul din Fig. 3.4. pentru G. De observat c
G poate avea -producii.
1. Aranjeaz neterminalii ntr-o anumit ordine A1, A2, , An.
2. for i := 1 to n do
begin
for i := 1 to n do
begin
nlocuiete fiecare producie de forma Ai Aj
cu produciile Ai 1 2 k
unde Aj 1 2 k sunt toate Aj-produciile;
end
elimin recursia stng imediat aasociat Ai-produciilor
end

Fig. 3.4. Algoritm de eliminare a recursiei stngi dintr-o gramatic


Exemplul 3.4. S aplicm procedura din Fig. 3.4. pentru gramatica (3.8). Aranjm
neterminalele n ordinea S, A. Nu exist recursie stng imediat n ceea ce privete
S_produciile, deci nu se ntmpl nimic pentru i=1. Pentru i=2, nlocuim
S_produciile n
A Sd i obinem urmtoarele A_producii:
A Ac Aad bd
Eliminnd recursia stng imediat din A-producii obinem gramatic cu urmtoarele
reguli:
S Aab
A bdAA
A CAadA
3.1.5.2. Factorizarea la stnga
Factorizarea la stnga a unei gramatici este util dac folosim o metod de
analiz predictiv. Atunci cnd nu tim ce producie s folosim din dou alternative
pentru a extinde un neterminal A, putem rescrie A_produciile astfel nct s amne
decizia alegerii pn am vzut suficient din intrare pentru a face alegerea corect.
De exemplu, dac avem dou producii
instr if expr then instr else instr
if expr then instr
i n intrare am vzut unitatea lexical if, nu putem spune imediat care dintre producii
va fi aleas pentru a extinde instr.
n general, dac
A 12
Pag.52

TEHNICI DE COMPILARE

sunt A_producii i intrarea ncepe cu un cuvnt nevid derivat din , nu tim dac s
extindem A la 1 sau la 2. Oricum, putem amna decizia extinznd A la A. Apoi,
dup ce am vzut intrarea derivat din , extindem A la 1 sau la 2, n funcie de
noua intrare. Astfel, factorizate la stnga, produciile iniiale devin:
A A
A 12
Algoritmul 3.2. Factorizarea la stnga a unei gramatici
Intrare: o gramatic G
Ieire: o gramatic factorizat la stnga G, echivalent cu G
Metod: pentru fiecare neterminal A se gsete cel mai lung prefix comun pentru
dou sau mai multe din alternativele lui A. Dac , nlocuiete toate A_regulile
A 12n
unde reprezint toate alternativele care nu ncep cu , cu regulile
A A
A 12n
unde A este un neterminal. Se repet acest proces pn nu mai exist dou alternative
ale aceluiai neterminal care s aib un prefix comun.
Exemplul 3.5. S considerm urmtoarea gramatic :
S iEtSiEtSeSa
Eb
(3.9)
Factoriznd la stnga aceast gramatic obinem:
S iEtSSa
S eS
(3.10)
Eb
Astfel, extindem S la iEtSS cu intrarea i i ateptm pn iEtS este vzut n
intrare pentru a decide dac extindem S la eS sau la . Evident, gramaticile (3.9) i
(3.10) sunt amndou ambigue i, cu intrarea e, nu va fi clar ce alternativ s folosim
pentru S.

3.2. ANALIZA SINTACTIC TOP-DOWN DETERMINISTA


Analiza top-down poate fi privit ca o ncercare de a gsi o derivaie la stnga
pentru irul de intrare. nseamn deci, c aceast metod construiete arborele
sintactic de la rdcin i creeaz nodurile n preordine. Metoda poate implica
backtracking, ceea ce nseamn c trebuie s se ntoarc n irul de intrare pentru a
relua citirea.
Exemplul 3.6. Fie gramatica
S cAd
A aba
i cuvntul de intrare w=cad. Mai nti considerm un arbore alctuit dintr-un singur
nod etichetat S. Pointerul anticipativ indic c n intrare, deci folosim producia
S cAd
pentru a extinde arborele i a obine astfel arborele sintactic din
Pag.53

TEHNICI DE COMPILARE

Fig. 3.5.(a). Pointerul anticipativ avanseaz la simbolul a. Putem extinde A folosind


prima alternativ A ab i obinem arborele din Fig. 3.5(b). Pointerul anticipativ
avanseaz la d i se compar d cu urmtoarea frunz, etichetat b. Cum bd, se
raporteaz eec i se ntearce la A pentru a vedea dac are i alt alternativ care poate
fi folosit pentru a produce arborele sintactic. Prin ntoarcere la A, trabuie s resetm
pointerul anticipativ la poziia 2, ceea ce nseamn c procedura pentru A trebuie s
pstreze valoarea pointerului anticipativ ntr-o variabil local. ncercnd apoi
cealalt alternativ pentru A se obine arborele din Fig. 3.5(c) i dup citirea lui d se
obine analiz cu succes a irului de intrare.
S
c

S
d

(a)

S
d
b

(b)

(c)

Fig.3.5. Generarea arborelui sintactic top-down.

3.2.1. Analiza top_down predictiv


n multe cazuri, eliminnd recursia stng din gramatica iniial i apoi
factoriznd la stnga gramatica rezultat, obinem o gramatic pentru care se poate
scrie un analizor predictiv fr backtracking. Pentru a construi un astfel de analizor
trebuie s tim, fiind dat un simbol de intrare a i un neterminal A, care este singura
alternativ dintre produciile lui A ce deriveaz cuvntul care ncepe cu a. De
exemplu, dac avem urmtoarele reguli:
instr if expr then instr else instr
while expr do instr
begin list_instr end
atunci cuvintele cheie if, while sau begin ne spun care alternativ este singura pe care
o putem folosi pentru a gsi o instruciune.
3.2.1.1. Diagrame de tranziie pentru analiza predictiv
Putem crea un plan al analizorului sintactic folosind diagrame de tranziie, la
fel cum am procedat n cazul analizei lexicale. Exist cteva diferene ns, fa de
diagramele de tranziie pentru analizorul lexical. n cazul analizorului sintactic,
pentru fiecare neterminal exist o diagram. Arcele sunt etichetate cu neterminale i
terminale. O tranziie efectuat cu un terminal, a, se poate face dac urmtorul simbol
de intrare este acel terminal, a. O tranziie cu un neterminal A nseamn apelarea
procedurii corespunztoare diagramei de tranziie a lui A.
Pag.54

TEHNICI DE COMPILARE

Pentru a putea construi diagrama de tranziie a unui analizor predictiv dintr-o


gramatic, mai nti eliminm recursia stng din acea gramatic, apoi factorizm la
stnga gramatica rezultat. Apoi, pentru fiecare neterminal A:
1. creem o stare iniial i o stare final
2. creem pentru fiecare producie A X1X2Xn un drum de la starea iniial la
cea final cu arcele etichetate X1, X2, ,Xn.
Analizorul predictiv lucreaz n felul urmtor. ncepe n starea iniial a diagramei
pentru simbolul iniial. Dac, dup anumite aciuni, se afl n starea s care are un arc
etichetat cu simbolul terminal a ctre starea t i urmtorul simbol de intrare este a,
atunci analizorul mut pointerul anticipativ o poziie spre dreapta n intrare i trece n
starea t. Dac ns arcul de la s la t este etichetat cu neterminalul A, analizorul trece n
starea iniial a diagramei pentru A fr a avansa pointerul anticipativ. Dac atinge
starea final a diagramei pentru A, atunci trece imediat n starea t. n sfrit, dac de
la s la t arcul este etichetat , analizorul trece de la s la t fr a avansa pointerul n
intrare.
Un analizor predictiv bazat pe diagrame de tranziie ncearc s potriveasc simboluri
terminale cu cele din intrare, fcnd apeluri recursive atunci cnd un arc este etichetat
cu un neterminal. Aceast metod funcioneaz dac diagramele de tranziie sunt
deterministe, n sensul c nu exist mai multe tranziii de la o stare, etichetate cu
acelai simbol. Dac nedeterminismul nu poate fi eliminat, nu se poate construi un
analizor predictiv i deci trebuie s folosim backtracking pentru a ncerca toate
posibilitile de tranziie.
Exemplul 3.7. Fig. 3.6. conine colecia diagramelor de tranziie pentru gramatica
(3.7.). Singura ambiguitate apare atunci cnd trebuie s alegem ntre a face sau nu o
tranziie cu . Dac interpretm arcele ce ies din starea iniial pentru E astfel:
urmeaz tranziia cu + ori de cte ori simbolul de intrare este + i urmeaz tranziia cu
n rest i facem aceeai interpretare pentru T, atunci ambiguitatea este eliminat.
E:

E :

T
+

E
1
4

T:

T :

10

F
*

T
8

11

T
12

13

F:

14

15

16

17

id

Fig. 3.6. Diagrame de tranziie pentru gramatica (3.7).


Pag.55

TEHNICI DE COMPILARE

Diagramele pot fi simplificate dac le substituim una n cealalt. De exemplu, n Fig.


3.7(a) apelul lui E din diagrama pentru E a fost nlocuit printr-un salt la starea
iniial a lui E, simplificnd i mai mult n Fig. 3.7(b). Putem apoi nlocui n
diagrama lui E tranziia cu E cu diagrama simplificat a lui E i obinem diagrama
pentru E din Fig. 3.7(c) care, simplificat devine diagrama din Fig. 3.7.(d).

+
E :

E :

ur
m
tT
o
ar
+
ea

(a)

E:

(b)
+

E:

(c)
(d)
Fig. 3.7. Diagrame de tranziie simplificate.
Aceeai tehnic o aplicm diagramelor pentru Ti T. Setul rezultat este prezentat n
Fig. 3.8. O implementare n limbajul C a diagramelor din Fig. 3.8. ruleaz cu 20_25
mai repede dect o implementare a diagramelor din Fig. 3.6.
+

E:

T:

F:

14

13

15

16

)
17

id

Fig. 3.8. Diagramele de tranziie pentru expresii aritmetice simplificate.


Pag.56

TEHNICI DE COMPILARE

3.2.2. Analiza predictiv nerecursiv


Este posibil s construim un analizor predictiv nerecursiv prin gestiunea explicit a
unei stive. Problema principal este de a determina ce producie vom folosi pentru un
neterminal. Analizorul din Fig. 3.9. caut producia care va fi folosit ntr-o tabel de
analiz M. Vom vedea cum putem construi aceast tabel.
Intrare

Stiva

X
Y
Z

a +

b $

program de
analiz
predictiv

Ieire

tabela de
analiz
M
Fig. 3.9. Model al analizorului predictiv nerecursiv
Un astfel de analizor conine un buffer de intrare, o stiv, o tabel de analiz i un
set de reguli sau eroare ca ieire. Bufferul de intrare conine irul ce va fi analizat,
terminat prin $.
Stiva conine o secven de simboluri gramaticale cu $ la baz. Iniial, stiva conine
simbolul de start al gramaticii, deasupra lui $.Tabela de analiz este un tablou
bidimensional MA, a, unde A este un neterminal i a este un terminal sau $.
Analiza este controlat de un program care se comport dup cum urmeaz.
Programul consider X, simbolul din vrful stivei i a, simbolul curent de intrare.
Aceste dou simboluri determin aciunea pe care o va face analizorul. Exist trei
posobiliti n acest sens:
1. Dac X a $ analizorul anun analiz cu succes a intrrii.
2. Dac Xa$ analizorul scoate X de pe stiv i deplazeaza pointerul anticipativ
o poziie spre dreapta n intrare.
3. Dac X este un neterminal, programul consult MX, a. Acest element este fie
o X_producie a gramaticii, fie mesaj de eroare.
Dac MX, aX UVW , analizorul nlocuiete X din vrful stivei prin UVW (cu
U n vrf). Ca ieire, analizorul va scoate producia folosit (se poate executa orice alt
cod aici).
Dac MX, a eroare, analizorul apeleaz o rutin de tratare a erorii.
Comportarea analizorului este descris de urmtorul algoritm:
Algoritmul 3.3. Analiza predictiv nerecursiv.
Intrare: un cuvnt w i o tabel de analiz M pentru gramatica G.
Ieire: dac w este n L(G), o derivaie la stnga a lui w; altfel, mesaj de eroare.

Pag.57

TEHNICI DE COMPILARE

Metod: iniial, stiva conine $S, cu S simbolul iniial al gramaticii G n vrf; bufferul
de intrare conine w$. Programul care utilizeaz tabela de analiz M pentru a produce
un arbore sintactic pentru irul de intrare este prezentat n Fig. 3.10.
reseteaz ip la primul simbol din w$;
repeat
fie X simbolul din vrful stivei i a simbolul indicat de ip;
if X este un terminal sau $
then
if X a
then scoate X de pe stiv i avanseaz ip
else eroare()
else X este un neterminal
if MX, a X Y1Y2 Yk
then begin
scoate X de pe stiv;
pune Yk, Yk-1 ,Y1 pe stiv, cu Y1 n vrf;
scoate producia X Y1Y2 Yk;
end
else eroare()
until X $ stiva este vid

Fig. 3.10. Program de analiz predictiv.


Exemplul 3.8. S considerm gramatica (3.7.). Tabela de analiz M, este reprezentat
n Fig. 3.11.. Csuele goale nseamn eroare. Cu intrarea id+idid analizorul execut
secvena de aciuni din Fig. 3.12. Dac observm cu atenie aciunile analizorului,
vom vedea c ieirea este o derivaie la stnga pentru cuvntul de intrare.
NeterSimbol de intrare
minal
$
id

E
E TE
E TE
E
E+TE
E
E
T
TT
TFT
T
T
TFT
T
T
F
Fid
F(E)
Fig. 3.11. Tabela de analiz pentru gramatica din (3.7.).
3.2.2.1.Construirea tabelei de analiz
Pentru construirea tabelei de analiz se folosesc dou funcii asociate
gramaticii G i anume FIRST i FOLLOW.
Dac este un cuvnt format din simboluri ale gramaticii G, atunci FIRST() este
mulimea terminalelor cu care ncep cuvintele derivate din . Dac deriveaz ,
atunci este i el n FIRST().
Dac A este un neterminal, atunci FOLLOW(A) este mulimea terminalelor
care pot *apare imediat dup A ntr-o form propoziional; adic mulimea
terminalelor a pentru care exist o derivaie S Aa, cu i cuvinte oarecare.
Pag.58

TEHNICI DE COMPILARE

Dac A este cel mai din dreapta simbol ntr-o form propoziional, atunci $ este n
FOLLOW(A).
Funciile First i Follow se construiesc conform urmtorilor algoritmi.
Algoritm de calcul al lui FIRST(X):
Pentru fiecare simbol gramatical X, se aplic urmtoarele reguli pn cnd nici un
terminal sau nu mai poate fi adugat unei mulimi:
1. Dac X este simbol terminal, atunci FIRST(X){X}.
2. Dac X este o producie, atunci adaug la FIRST(X).
3. Dac X este neterminal i X Y1Y2Yk este o producie, atunci, dac
pentru un i avem FIRST(Yj ) cu j1,2, ,i1, adaug FIRST(Yi ) la FIRST(X);
Dac FIRST(Yj ) pentru j 1,2,,k atunci adaug la FIRST(X).
Putem acum afla FIRST() pentru orice cuvnt X1X2Xn astfel:
- Toate simbolurile diferite de din FIRST(X1) sunt adugate la
FIRST(X1X2Xn);
- Dac este n FIRST(X1) toate simbolurile diferite de din FIRST(X2) sunt
adugate la FIRST(X1X2Xn);
- Dac este n FIRST(X1) i n FIRST(X2) atunci toate simbolurile diferite de
din FIRST(X3) sunt adugate la FIRST(X1X2Xn) i aa mai departe.
- Dac este n FIRST(Xi) pentru i1,2,,n atunci adaug la
FIRST(X1X2Xn).
Algoritm de calcul al lui FOLLOW(A):
Pentru toate neterminalele A, se aplic urmtoarele reguli pn cnd nimic nu
mai poate fi adugat:
1. Pune $ n FOLLOW(S), unde S este simbolul iniial i $ este marcatorul
de sfrit al intrrii.
2. Dac exist o producie A B, atunci tot ce conine FIRST(), n
afar de simbolul , este adugat la FOLLOW(B).
3. Dac exist o producie A B sau o producie
A B cu
FIRST() atunci tot ce conine FOLLOW(A) este adugat la
FOLLOW(B).
Prezentm n continuare coninutul succesiv al stivei, al benzii de intrare i
regulile furnizate la ieire de analizorul predictiv cu irul de intrare:
id + id id$

Pag.59

TEHNICI DE COMPILARE

STIV
INTRARE
IEIRE
$E
id + id id$
$E T
id + id id$ E TE
$E T F id + id id$ T FT
$E T id id + id id$ F id
$E T
+ id id$
T
$E
+ id id$
E +TE
$E T +
+ id id$
$E T
id id$
T FT
$E T F id id$
F id
$E T id id id$
$E T
id$
T FT
$E T F id$
id$

F id
$E T F id$
$E T id $
T
$
$E T
E
$
$E
$
Fig. 3.12. Aciunile analizorului predictiv cu intrarea id+idid.
Exemplul 3.9. Dac considerm gramatica
E TE
E +TE
T FT
T FT
F (E) id

atunci:
FIRST(E) FIRST(T) FIRST(F) { ( , id }
FIRST(E ) { + , }
FIRST(T ) { , }
FOLLOW(E) FOLLOW(E ) { ) , $ }
FOLLOW(T) FOLLOW(T ) { + , ) , $ }
FOLLOW(F) { + , , ) , $ }.

Algoritmul 3.4. Construcia tabelei de analiz predictiv.


Intrare: o gramatic G.
Ieire: tabela de analiz M asociat gramaticii G.
Metod:
1. Pentru fiecare producie A a gramaticii, aplic paii 2 i 3.
2. Pentru fiecare terminal a din FIRST() adaug A la M [A, a].
3. Dac FIRST() atunci adaug A la M [A, b] pentru fiecare terminal
b din FOLLOW(A). Dac FIRST() i $ FOLLOW(A) atunci
adaug A la M[A, $].
4. Transform toate elementele nedefinite ale lui M n eroare.
Pag.60

TEHNICI DE COMPILARE

Exemplul 3.10. S aplicm algoritmul 3.4. gramaticii de la exemplul 3.9. Deoarece


FIRST(TE )FIRST(T ){ ( , id } producia E TE face ca M [E, ( ] i M[E,id] s
conin E TE. Producia E +TE face ca M[E, + ] s conin E+TE.
Continund algoritmul vom obine tabela de analiz reprezentat n Fig. 3.11.
3.2.2.2. Tratarea erorilor n analiza predictiv
n timpul analizei predictive, o eroare este detectat cnd un terminal din vrful
stivei nu se potrivete cu urmtorul simbol de intrare sau cnd neterminalul A este n
vrful stivei, a este urmtorul simbol de intrare i tabela de analiz nu are definit
elementul M[A, a].
Modul panic de tratare a erorii se bazeaz pe ideea de a sri peste simboluri din
intrare pn cnd o unitate lexical care se afl ntr-o mulime de sincronizare este
gsit. Eficiena acestei metode depinde deci de mulimea de sincronizare. Aceasta
trebuie aleas astfel nct analizorul s treac rapid peste erorile care se ntlnesc cel
mai frecvent n practic. Un mod de alegere ar putea respecta urmtoarele cerine:
1. Putem aduga toate simbolurile din FOLLOW(A) la mulimea sincronizatoare
pentru neterminalul A. Dac srim peste uniti lexicale din intrare pn cnd un
element din FOLLOW(A) este gsit i apoi scoatem A de pe stiv, este foarte posibil
ca analiza s poat continua.
2. Nu este suficient s folosim FOLLOW(A) ca mulime sincronizatoare pentru A.
De exemplu, n limbajul C, instruciunile se termin prin punct_i_virgul i deci
cuvintele cheie cu care ncep instruciunile nu apar n FOLLOW pentru neterminalul
ce genereaz expresii. De obicei, structurile din limbajele de programare au o
organizare ierarhic: expresiile apar n instruciuni, instruciunile n blocuri, etc.
Putem aduga la mulimea sincronizatoare a structurilor inferioare simbolurile cu care
ncep structurile superioare. De exemplu, putem aduga cuvintele cheie cu care ncep
instruciunile la mulimea sincronizatoare pentru neterminalele care genereaz
expresii.
3. Dac adugm simboluri din FIRST(A) la mulimea sincronizatoare a
neterminalului A, este posibil s relum analiza raportat la A dac un simbol din
FIRST(A) apare n intrare.
4. Dac un neterminal poate genera un ir vid, atunci producia respectiv poate fi
utilizat n lips de altceva. n acest fel este posibil s amnm detectarea unei erori,
dar nu se poate ntmpla ca vreo eroare s fie ratat. Aceast tehnic reduce numrul
neterminalelor care trebuie luate n consideraie n timpul tratrii erorilor.
5. Dac un terminal din vrful stivei nu poate fi potrivit, o idee simpl este de a
elimina terminalul de pe stiv i a continua analiza. Ca efect, aceast tehnic face ca
mulimea sincronizatoare a unei uniti lexicale s conin toate celelalte uniti
lexicale.
Exemplul 3.12. Utiliznd simbolurile din FOLLOW i FIRST ca uniti lexicale de
sincronizare pentru gramatica din exemplu 2.26. tabela de analiz se modific ca n
Fig. 3.14., cu sinc indicnd unitatea lexical sincronizatoare obinut din FOLLOW
pentru neterminalul n discuie. Tabela astfel completat va fi folosit n felul urmtor.
Dac analizorul gsete M[A, a] nedefinit, atunci simbolul de intrare a este srit.
Pag.61

TEHNICI DE COMPILARE

Dac M[A, a]sinc, atunci neterminalul din vrful stivei este eliminat n ncercarea
de a relua analiza. Dac unitatea lexical din vrful stivei nu se potrivete cu simbolul
de intrare, unitatea lexical respectiv este eliminat.
NeterSimbol de intrare
minal
$
id

E
sinc
E TE
E TE sinc
E
E TE
E E
T
sinc
sinc
sinc
T FT
T FT
T
T T FT
T T
F
sinc
sinc
F id
F (E)
Fig. 3.14. Tabela de analiz pentru gramatica din (3.7).
Cu intrarea greit )id+id analizorul i mecanismul de tratare a erorilor din
Fig. 3.14. se comport ca n Fig. 3.15.
Nivelul expresie (propoziie) este implementat prin completarea elementelor
nedefinite din tabela de analiz cu pointeri ctre rutine de tratare a erorii. Aceste rutine
pot schimba, insera sau terge simboluri din intrare i afia mesaje de eroare adecvate.
Pot, de asemenea, elimina elemente de pe stiv. n orice caz, trebuie s ne asigurm c
nu exist posibilitatea intrrii ntr-un ciclu infinit.
STIV
$E
$E
$E T
$E T F
$E T id
$E T
$E T F
$E T F
$E T
$E
$E T+
$E T
$E T F
$E T id
$E T
$E
$

INTRARE
) id + id$
id + id$
id + id$
id + id$
id + id$
+ id$
+ id$
+ id$
+ id$
+ id$
+ id$
id$
id$
id$
$
$
$

REMARC
Eroare, sare )
id este n FIRST(E)

eroare, M [F, +] sinc


F este scos de pe stiv

Fig. 3.14. Aciunile de analiz i tratare a erorilor ale analizorului.


Pag.62

TEHNICI DE COMPILARE

3.3. ANALIZA SINTACTIC BOTTOM-UP DETERMINISTA


n aceast seciune este prezentat o tehnic de analiz sintactic bottom-up care poate
fi utilizat pentru a analiza eficient o clas larg de gramatici independente de context.
Tehnica este numit analiza LR(k), unde: L (left-to-right) nseamn parcurgera de la
stnga la dreapta a intrrii; R (rightmost-derivation) nseamn construirea unei
derivaii la dreapta inversat pentru irul de intrare, K reprezint numrul de simboluri
de intrare privite nainte pentru a lua deciziile de analiz. n cazul n care K este omis,
este presupus 1. Analiza LR prezint urmtoarele avantaje:
analizoarele LR pot fi construite pentru a recunoate toate structurile
limbajelor de programare pentru care o gramatic independent de context poate fi
scris.
analiza LR este cea mai general metod de analiz deplasare-reducere fr
backtracking i poate fi implementat la fel de eficient ca orice alt metod
deplasare_reducere.
mulimea gramaticilor care pot fi analizate folosind analiza LR include
mulimea gramaticilor care pot fi analizate utiliznd analiza predictiv.
un analizor LR detecteaz o eroare sintactic att de repede ct este posibil
ntr-o citire de la stnga la dreapta a intrrii.
Pentru a realiza un analizor LR este necesara foarte mult munc dar exist
instrumente specializate (YACC), care genereaz automat un analizor LR pornind de
la o gramatica independenta de context. Daca gramatica conine ambiguiti sau alte
construcii care sunt dificil de analizat atunci generatorul de analizor poate localiza
aceste construcii i poate informa utilizatorul de prezena lor.

3.3.1 Algoritmul de analiz LR


O schem a analizorului LR este prezentat n Fig. 3.16. Acesta const din:
o banda de intrare, care contine un sir de tokeni furnizai de analizorul
lexical;
o ieire, care va conine arborele sintactic;
o stiv;
un program de analiz;
o tabel de analiz fomat din dou pri : aciune i goto.
Programul de analiz este acelai pentru toate analizoarele LR; se modific doar
tabela de analiz. Programul de analiz citete cte un simbol din bufferul de intrare.
Stiva analizorului conine un ir de forma s0X1s1X2Xmsm. cu sm n vrf; fiecare Xi este
un simbol gramatical i fiecare si este un numr ntreg numit stare; fiecare stare
nmagazineaz informaii referitoare la irul din stiv de sub ea i combinaia dintre o
stare din vrful stivei i simbolul curent de intrare determin pe baza tabelei de analiz
aciunea pe care o va face analizorul deplasare-reducere. n implementarea efectiv,

Pag.63

TEHNICI DE COMPILARE

simbolurile gramaticii nu trebuie s apar pe stiv; le vom include ns n discuie


pentru a uura explicarea comportrii analizorului LR.
INTRARE

STIV

sm
Xm
sm-1
Xm-1

s0

a1 .ai an $

Program de
analiz LR

aciune

IEIRE

goto

Fig. 3.16. Modelul analizorului LR.


Programul de analiz este acelai pentru toate analizoarele LR numai tabele de
analiz este diferit de la un analizor la altul.
Exist trei tehnici de a construi tabele de analiz LR. Prima, numit LR simpl
(SLR prescurtat), este cea mai uor de implementat, dar i cea mai puin puternic n
ceea ce privete numrul gramaticilor pe care le poate analiza. A doua, numit LR
canonic este cea mai puternic, dar i cea mai costisitor de implementat. A treia
metod, numit LALR (lookahead LR-privete nainte), este intermediar ntre primele
dou.
Programul de analiz citete caracter cu caracter intrarea. Programul folosete o
stiv pentru a memora un ir de forma s0X1s1X2Xmsm, unde vrful stivei este sm ,
X1,X2,,Xm, sunt simboluri ale gramaticii iar s0,s1sm sunt stri. Programul determin
sm, starea din vrful stivei, i ai, simbolul curent de intrare. Consult apoi aciune[sm,
ai], care poate avea una din valorile:
1. s, unde s este o stare (deplasare),
2. r, unde r este o producie A (reducere),
3. accept, i
4. eroare.
O configuraie a unui analizor LR este o pereche de forma (s0X1s1X2Xmsm ,
aiai+1an$ ), cu prima component reprezentnd coninutul stivei i a doua
reprezentnd cuvntul rmas n intrare. Analizorul i schimb configuraia curent
dup urmtoarele reguli:
1. dac aciune[sm, ai]s, analizorul face o deplasare i trece n configuraia
(s0X1s1X2Xmsmais , ai+1an$ ), adic deplaseaz simbolul de intrare ai pe stiv i
adaug starea s .
2. dac aciune[sm, ai] r, unde r este producia A, atunci analizorul face o
reducere i trece n configuraia
(s0X1s1X2Xm-sm-As , aiai+1an$ ),
unde sgoto[sm-, A] i este lungimea lui . Analizorul scoate deci 2
simboluri de pe stiv , apoi pune pe stiv A, partea stng a produciei cu care reduce,
Pag.64

TEHNICI DE COMPILARE

i s, elementul goto[sm-, A] din tabela de analiz. Secvena Xm-+1Xm scoas de


pe stiv se potrivete ntotdeauna cu , partea dreapt a produciei cu care se reduce.
Ieirea n acest caz este producia folosit pentru reducere.
3. dac aciune[sm, ai]accept, analiza s-a ncheiat cu success.
4. dac aciune[sm, ai]eroare, analizorul a descoperit o eroare i apeleaz o rutin
de tratare a erorii.
Algoritmul 3.5. Algoritm de analiz LR.
Intrare:
un cuvnt de intrare w i o tabel de analiz LR cu funciile aciune i
goto pentru gramatica G.
Ieire:
dac w este n L(G) atunci o derivaie la dreapta pentru w;
altfel eroare.
Metod:
iniial, analizorul are s0 pe stiv (starea iniial) i w$ n bufferul de
intrare. Se execut apoi algoritmul din Fig. 3.17. pn cnd sunt
ntlnite.accept sau eroare.
pune ip pe primul caracter din w$;
repeat forever
begin
fie s starea din vrful stivei i a simbolul indicat de ip;
if aciune[s, a] s then
begin
pune a apoi s pe stiv;
avanseaz ip la urmtorul simbol de intrare;
end
else if aciune[s, a] A then
begin
scoate 2 simboluri de pe stiv;
fie s noua stare din vrful stivei;
pune A apoi goto[s, A] pe stiv;
scrie n ieire producia A ;
end
else if aciune[s, a] accept then
return
else eroare()
end

Fig. 3.17. Algoritmul de analiz LR.


Exemplul 3.13. Fig. 3.18. prezint funciile aciune i goto ale tabelei de analiz LR
pentru urmtoarea gramatic pentru expresii aritmetice, cu operatorii binari + i , ale
crei reguli sunt:
(1) E E + T
(2) E T
(3) E T F
(4) T F
(5) F (E)
(6) F id

Pag.65

TEHNICI DE COMPILARE

aciune

Stare

id

goto
)

E T F
1 2 3

0
s5
s4
1
s6
acc
2
r2 s7
r2 r2
3
r4 r4
r4 r4
4
s5
s4
8 2 3
5
r6 r6
r6 r6
6
s5
s4
9 3
7
s5
s4
10
8
s6
s11
9
r1 s7
r1 r1
10
r3 r3
r3 r3
11
r5 r5
r5 r5
Fig. 3.18. Tabel de analiz pentru gramatica expresiilor aritmetice.
Codurile din tabel sunt:
1. si trece n starea i
2. rj reduce cu producia (j)
3. acc accept
4. elementele nedefinite nseamn eroare.
Cu intrarea idid+id, analizorul se comport ca n Fig. 3.19. n linia (1) analizorul
are 0 pe stiv i id simbolul curent de intrare. Deoarece aciune[0, id]s5, analizorul
deplaseaz id i 5 pe stiv (trece n starea 5) cum se poate vedea n linia (2). Acum
aciune[5, ]r6, ceea ce nseamn c se face o reducere cu regula (6), adic cu Fid.
Sunt scoase dou simboluri de pe stiv i deoarece goto[0, F]3, sunt adugate F i 3
pe stiv. Restul simbolurilor din intrare sunt tratate similar.
STIVA
INTRARE
ACIUNE
(1)0
id id + id $ Deplasare
(2)0 id 5
id + id $ Reduce cu F id
(3)0 F 3
id + id $ Reduce cu T F
(4)0 T 2
id + id $ Deplasare
id + id $ Deplasare
(5)0 T 2 7
+ id $ Reduce cu F id
(6)0 T 2 7 id 5
+ id $ Reduce cu T T F
(7)0 T 2 7 F 10
(8)0 T 2
+ id $ Reduce cu E T
(9)0 E 1
+ id $ Deplasare
(10)0 E 1 + 6
id $ Deplasare
(11)0 E 1 + 6 id 5
$ Reduce cu F id
(12)0 E 1 + 6 F 3
$ Reduce cu T F
(13)0 E 1 + 6 T 9
$ Reduce cu EE+T
(14)0 E 1
$ Accept
Fig. 3.19. Comportarea analizorului LR cu intrarea idid+id.
Pag.66

TEHNICI DE COMPILARE

Punctul cheie al algoritmul il constituie construirea tabelei de analiz care se poate


face pentru gramatici LR. Nu toate gramaticile independente de contxt sunt LR, dar
costruciile tipice limbajelor de programare sunt LR.
ntr-o gramatic LR nu este nevoie sa se analizeze intreaga stiv pentru a putea efectua
o reducere, aceasta informaie fiind furnizat de starea aflat n vrful stivei. n esen
funcia goto reprezint funcia de tranziie a unui automat finit care recunoate
prefixele valide.
O alta surs de informaie pe care o utilizeaz analizorul LR este format de cele k
simboluri privite nainte din irul de intrare. n exemplul anterior tabela de tranziie
folosete l=1, adic un singur simbol de intrare. n general, pentru un k oarecare,
suntem capabili sa recunoatem o reducere numai dac cunoatem contextul drept de
lungime k al intrrii.
3.3.1.1. Construirea tabelei de analiz SLR
Am vzut anterior c exist trei tehnici pentru a construi o tabel de analiz LR.
Prima, pe care o vom discuta n continuare, se numete LR simpl (pe scurt SLR).
Ne vom referi la un analizor care folosete o tabel de analiz SLR ca analizor SLR. O
gramatic pentru care poate fi construit un analizor SLR se numete gramatic SLR.
Celelalte dou metode extind tehnica de analiz SLR folosind informaii suplimentare
referitoare la intrare.
Un item LR(0) al unei gramatici G este o producie a lui G cu un punct ntr-o
anumit poziie n partea dreapt a produciei. Astfel, producia AXYZ genereaz
patru iteme:
A .XYZ
AX.YZ
AXY.Z
A XYZ
Producia A genereaz doar itemul A . Intuitiv, un item indic ct de mult
am vzut dintr-o producie la un moment dat n procesul analizei. De exemplu, primul
item indic faptul c ne ateptm s vedem n intrare un cuvnt derivat din XYZ; al
doilea item indic faptul c tocmai am vzut n intrare un cuvnt derivat din X i ne
ateptm s vedem n continuare un cuvnt derivat din YZ.
Ideea central este de a construi din gramatica limbajului un automat finit determinist
care s recunoasc prefixe viabile. Mulimile de iteme vor reprezenta strile acestui
automat.
O colecie de mulimi de iteme LR(0) reprezint baza de pornire n construirea
analizorului SLR. Pentru a construi aceast colecie vom defini o gramatic extins i
dou funcii, nchidere i goto.
Dac G este o gramatic cu simbolul iniial S, atunci G, gramatica extins a lui G,
este obinut din G prin adugarea unui nou simbol iniial S i a unei producii SS.
Motivul introducerii acestei noi producii este de a indica analizorului cnd trebuie s
se opreasc din analiz. Astfel, recunoaterea cuvntului de intrare se face numai cnd
analizorul va reduce cu regula SS.
Pag.67

TEHNICI DE COMPILARE

Operaia nchidere
Dac I este o mulime de iteme ale gramaticii G, atunci nchidere(I) este o mulime de
iteme construit din I aplicnd urmtoarele reguli:
1. Iniial, toate itemele din I sunt n nchidere(I): nchidere(I):= I
2. Dac AB este n nchidere(I) i B este o producie, atunci adaug
itemul B la nchidere(I) (dac nu este deja acolo). Se aplic aceast regul pn
cnd nu mai putem aduga nici un item nou la nchidere(I).
Intuitiv un item AB este n nchidere(I) arat c, la un anumit moment, n
procesul de analiz, dup ce l-am vzut pe , ne gndim ca am putea vedea un subir
derivat din B. Dac B este o producie ne putem astepta s vedem un ir derivat
din la acest punct. Acesta este motivul includerii produciei B n nchidere(I).
Exemplul 3.14. Fie gramatica extins pentru expresii aritmetice:
E E
E E + T T
T T F F
(3.11)
F (E)id
Dac I este mulimea de iteme {[E.E]}, atunci nchidere(I) va conine itemele:
E E
E E + T
E T
T T F
T F
F (E)
F id
Functia nchidere(I) se calculeaza cu algoritmul din fig. 3.20.
Mutimile de itemuri pot fi mprite n dou clase:
Itemuri nucleu, care includ itemul iniial, S .S, i toate itemurile a cror
punct nu se afl la captul stng;
Itemuri nenucleu, care au punctul la captul drept.
Fiecare mulime de itemuri care ne intereseaz este format din nchiderea unei
mulimi de itemuri nucleu. itemurile adugate fiind nenucleu. Atunci mulimea de
itemuri se poate reprezenta numai prin nucleu, restul putnd fi construite.
function inchidere(I );
begin
J:=I;
repeat
for fiecare item AB din J i fiecare productie gramatical B
astfel incat B. nu este in J do
adauga B.
until nici un item nu mai poate fi adugat
return J;
end
Figura 3.20.

Pag.68

TEHNICI DE COMPILARE

Operaia goto
Pentru o mulime de iteme I i X un simbol al gramaticii, definim goto(I, X)
nchiderea mulimii itemelor de forma [AX] astfel nct [AX] s fie n I.
Intuitiv, dac I este mulimea itemelor valide pentru un prefix viabil , atunci goto(I,X)
este mulimea itemelor valide pentru un prefix viabil X.
Exemplul 3.15. Dac I{[E E], [E E + T] }, atunci
goto(I, +) conine
itemele:
E E + T
T T F
T F
F (E)
F id

goto(I, +) se calculeaz prin examinarea lui I pentru itemele cu + imediat dup


punct, deci numai pentru E E. + T. Se mut punctual dup + i se calculeaz
nchiderea mulimii formate din acest nou item.
Construirea coleciei mulimilor de iteme pentru o gramatic extins
Putem enuna acum algoritmul pentru a construi C, colecia canonic a mulimilor de
iteme LR(0) pentru o gramatic extins G ; algoritmul este prezentat n Fig. 3.21.
procedure iteme(G );
begin
C : {nchidere({[S .S ]})};
repeat
for fiecare mulime de iteme I din C i fiecare simbol gramatical X
astfel nct goto(I, X) nu este vid i nu este n C do
adaug goto(I, X) la C
until nici o mulime de iteme nu mai poate fi adugat la C
end

Fig. 3.21. Construirea mulimilor de iteme.


Exemplul 3.16. Colecia canonic a mulimilor de iteme LR(0) pentru gramatica
(3.11) din exemplul 3.14. este prezentat n Fig. 3.22.
I0:

I1:
I2:

E E
E E + T
E T
T T F
T F
F (E)
F id

E E
EE + T
ET
TT F

I7:
I4:

F (E )
E E + T
E T
T T F
T F
F (E)
F id

I5:

Fid

I6:

EE + T
T T F
T F
F (E)
F id

I8:

TT F
F (E)
F id
F(E)
EE + T

I9:

EE + T
TT F

I10:

TT F

I11:

F(E)

Pag.69

TEHNICI DE COMPILARE

I3:

TF

Fig. 3.22. Colecia canonic a mulimilor de iteme pentru gramatica (3.11).


Funcia goto pentru fiecare mulime de iteme este prezentat sub forma diagramei de
tranziie a unui automat finit determinist:
I0

I1

I6

T
F
(
id

I2

I7

F
(
id

I3

(
I4

id

spre I7

spre I3
spre I4
spre I5
I10
spre I4
spre I5

(
)
I11

I8
+

id

I9

spre I6

spre I2
spre I3

I5

Fig. 3.23. Diagrama de tranziie a unui AFD pentru prefixe viabile.


Dac fiecare stare din Fig. 3.23. este o stare final i I0 este starea iniial, atunci
automatul recunoate exact prefixele viabile ale gramaticii (3.11). Pentru orice
gramatic G, funcia goto definete un automat finit determinist care recunoate
prefixele viabile ale lui G. De fapt, dac considerm un AFN ale crui stri sunt
itemele i o tranziie de la AX la AX este etichetat X, iar o tranziie de la
AB la B este etichetat , atunci nchidere(I) pentru mulimea de iteme I este
exact -nchiderea unei mulimi de stri ale AFN definite n 2.10. Astfel, goto(I, X)
este tranziia cu X de la I n automatul finit determinist construit din AFN cu
algoritmul 2 din ANEXA 1. Privit astfel, procedura iteme(G) din Fig. 3.21. este chiar
procedura de construire a submulimilor de stri aplicat automatului finit
nedeterminist construit din G.
Iteme valide
Spunem c itemul A12 este valid pentru un prefix viabil 1, dac exist o

derivaie la dreapta
Pag.70

TEHNICI DE COMPILARE

SAw12w
Faptul c A12 este valid pentru 1 ne ajut s decidem dac facem o deplasare
sau o reducere atunci cnd gsim 1 pe stiva analizorului. n particular, dac 2,
atunci se sugereaz c nu am gsit nc un subcuvnt ce poate fi redus i deci trebuie
s facem o deplasare. Dac 2, atunci se pare c A1 este o producie cu care ar
trebui s reducem. Desigur, dou iteme valide pot s ne spun lucruri diferite despre
acelai prefix viabil. Unele din aceste conflicte pot fi rezolvate dac privim nainte i
urmtorul simbol de intrare, dar exist conflicte care nu pot fi rezolvate dac vom
folosi metodele de analiz LR pentru a construi tabele de analiz pentru gramatici
arbitrare.
Exemplul 3.18. S considerm gramatica (3.11), ale crei colecie canonic i funcii
goto sunt prezentate n Fig. 3.22. i 3.23. Evident, cuvntul E+T este un prefix viabil
pentru gramatica (3.11). Automatul din Fig. 3.23. se va afla dup citirea acestui prefix
n starea I7, care conine itemele
T T F
F (E)
F id
care sunt exact itemele valide pentru E+T. Pentru a vedea acest lucru s considerm
urmtoarele trei derivaii la dreapta:
E
E E EE E
E+TE+TE+T
E + T F E + T F E + T F
E + T (E) E + T id
Prima derivaie arat validitatea lui TTF, a doua, validitatea lui F(E) i a treia,
validitatea lui Fid pentru prefixul viabil E+T. Se poate demonstra c nu exist alte
iteme valide pentru E+T, dect cele din I7.
Algoritmul construciei tabelei de analiz SLR
Putem prezenta acum modul de construcie al funciilor aciune i goto ale unei tabele
de analiz SLR din automatul finit determinist care recunoate prefixe valide.
Algoritmul 3.6. Construcia tabelei de analiz SLR
Intrare: o gramatic extins G.
Ieire: funciile aciune i goto ale tabelei de analiz SLR pentru G.
Metod:
1. Se construiete C{I0, I1, ,In}, colecia mulimilor de iteme LR(0) pentru G.
2. Starea i este construit din Ii. Aciunile de analiz pentru starea i sunt
determinate n felul urmtor:
a) dac [Aa] este n Ii i goto(Ii, a)Ij atunci definete aciune[i, a]j; n
acest caz a este un terminal
Pag.71

TEHNICI DE COMPILARE

b) dac [A] este n Ii atunci definete aciune[i, a]r (reduce cu rA)


pentru toate simbolurile a din FOLLOW(A), unde A este un neterminal diferit de S
c) dac [SS.] este n Ii atunci definete
aciune[i, $]accept.
Dac n urma aplicrii acestor reguli apar elemente definite multiplu, atunci
gramatica nu este SLR(1) i algoritmul nu poate face o analiz SLR.
3. Funcia goto a tabelei de analiz SLR este definit pentru toi neterminalii, A,
folosind regula:
dac goto(Ii, A)Ij atunci goto[i, A]j.
4. Toate elementele nedefinite dup aplicarea regulilor (2) i (3) sunt
transformate n eroare.
5. Starea iniial a analizorului este cea construit din mulimea itemelor ce
conine [SS].
Exemplul 3.19. S construim tabela SLR pentru gramatica (3.11). Colecia mulimilor
de iteme LR(0) pentru (3.11) este prezentat n Fig. 3.22. Mai nti considerm
mulimea de iteme I0:
E E
E E + T
E T
T T F
T F
F (E)
F id
Itemul F(E) face ca aciune[0,(]s4 (schimb cu starea 4) deoarece goto(I0,()I4;
itemul Fid face ca aciune[0, id ]s5. Celelalte iteme nu au nici o aciune.
Considernd I1:
E E
E E + T
Primul item definete aciune[1,$]accept, iar al doilea aciune[1,+]s6. Apoi,
cosidernd I2:
E T
T T F
Avem aciune[2,$]aciune[2,+]aciune[2,)]reduce cu E T, deoarece avem
FOLLOW(E){$,+,)}. Al doilea item definete aciune[2,]s7. Continund
procedeul vom obine n final tabela de analiz prezentat n Fig. 3.18.
Exemplul 3.20. Orice gramatic SLR este neambigu, dar exist gramatici neambigue
care nu sunt SLR. Considernd gramatica:
SLR|R
L R | id
(3.12)
RL
colecia canonic a mulimilor de iteme LR(0) este prezentat n Fig. 3.24.
Pag.72

TEHNICI DE COMPILARE

I0:

I1:
I :
2

S S
S L R
S R
L R
L id
R
L
S S
SL R
RL

I3:
I:
4

I5:

S R
L R
L R
L id
R L
Lid

I6:

I7:

SL R
L R
L id
R L
LR

I8:

RL

I9:

SL R

Fig. 3.24. Colecia mulimilor de iteme LR(0) pentru gramatica (3.12).

Dac considerm I2, primul item definete aciune[2,]s6, dar FOLLOW(R) conine
i deci al doilea item definete aciune[2,]reduce cu RL. Deci, aciune[2,]
este definit multiplu, ceea ce nseamn c avem un conflict deplasarereducere n
starea 2 cu simbolul de intrare .
3.3.2.Tratarea erorilor n analiza LR
Un analizor LR va detecta o eroare atunci cnd va consulta tabela aciune i va
gsi un element nedefinit. Erorile nu sunt niciodat detectate cnd se consult tabela
goto.
n analiza LR putem implementa modul panic de tratare a erorilor n felul
urmtor. Citim n jos pe stiv pn cnd este gsit o stare s cu goto pentru un
neterminal A. Zero sau mai multe simboluri de intrare sunt atunci ignorate pn cnd
este gsit un simbol a care poate urma dup A. n acest moment analizorul reia
analiza. Ar putea exista mai multe alegeri pentru neterminalul A. n mod normal,
acetia vor fi neterminali reprezentnd pri de program majore, cum ar fi expresii,
instruciuni sau blocuri. De exemplu, dac A este un neterminal instr atunci a ar putea
fi punct-i-virgul sau end.
Aceast metod ncearc s izoleze fraza care conine eroarea sintactic. Nivelul
propoziie de tratare a erorilor este implementat prin examinarea fiecrui element
nedefinit al tabelei aciune i identificarea celei mai frecvente erori de programare care
ar putea s determine apelul acelui element nedefinit. n acest moment, poate fi scris
o rutin de tratare a erorii care s modifice stiva sau intrarea n funcie de aciunea care
a provocat acea eroare.
Exemplul 3.27. S considerm din nou gramatica pentru expresii aritmetice (3.14).
Fig. 3.31. reprezint tabela de analiz LR a acestei gramatici modificat pentru a trata
i corecta erorile. Am schimbat toate strile care apeleaz o reducere cu un simbol de
intrare nlocuind toate elementele nedefinite cu acea reducere. Efectul este de a
ntrzia detectarea erorii pn cnd una sau mai multe reduceri vor fi fcute, dar
eroarea va fi oricum detectat nainte ca o nou deplasare s fie efectuat. Restul
elementelor nedefinite au fost completate cu pointeri ctre rutine de tratare ale erorilor
specifice.
Pag.73

TEHNICI DE COMPILARE

aciune

Stare

id

0
1
2
3
4
5
6
7
8
9

s3
e3
s3
r4
s3
s3
e3
r1
r2
r3

e1
s4
e1
r4
e1
e1
s4
r1
r2
r3

e1
s5
e1
r4
e1
e1
s5
s5
r2
r3

(
s2
e3
s2
r4
s2
s2
e3
r1
r2
r3

goto
)

e2 e1
e2 acc
e2 e1
r4 r4
e2 e1
e2 e1
s9 e4
r1 r1
r2 r2
r3 r3

E
1
6
7
8

Fig. 3.31. Tabel de analiz pentru gramatica (3.14) cu rutine de tratare a erorilor.
Rutinele de tratare a erorilor au urmtoarele semnificaii:
e1: apelat n strile 0, 2, 4 i 5 cnd se atepta un operand, id sau (; n locul
acestora, un operator + sau , sau sfritul intrrii a fost gsit. Pune un id fictiv pe
stiv i apoi adaug starea 3. Afieaz diagnosticul lipsete un operand.
e2: apelat n strile 0, 1, 2, 4 i 5 cnd se gsete o parantez ). terge ) din
intrare. Afieaz diagnosticul parantez neechilibrat.
e3: apelat n strile 1 i 6 cnd se atepta un operator i un id sau ) au fost
gsite. Pune + pe stiv i apoi adaug starea 4. Afieaz diagnosticul lipsete un
operator.
e4: apelat n starea 6 cnd este gsit sfritul intrrii n locul unui operator sau
). Pune ) pe stiv i apoi adaug starea 9. Afieaz diagnosticul lipsete ).
Cu intrarea eronat id + ) analizorul se comport ca n figura 3.32.
Stiv
Intrare Mesaj de eroare i aciune
0
id+)$
0id3
+)$
0E1
+)$
0E1+4
)$
parantez neechilibrat
0E1+4
$
0E1+4id
3
0E1+4E7
0E1

e2 terge ) din intrare


lipsete un operand
e1 pune id i 3 pe stiv

$
$

Fig. 3.32. Aciunile de analiz i tratare a erorilor efectuate de un analizor LR.

Pag.74

TEHNICI DE COMPILARE

3.4.Generatorul de analizor sintactic YACC


3.4.1. Introducere
Yacc este un generator de analizor sintactic de tip LALR. YACC nseamna Yet
Another Compiler - Compiler, reflectnd popularitatea analizoarelor sintactice n anii
1970, cnd S.C.Johnson a creat prima versiune de Yacc.
Pornind de la o gramatica independent de context, Yacc-ul genereaz un set de
tabele pentru un automat de analiza LR(1).
Un analizor syntactic poate fi construi utiliznd Yacc-ul ca n fig.3.33:
Specificaii Yacc
translate.y

Yacc compiler

y.tab.C

intrare
b.out

C compiler

b.out

ieire

Fig. 3.33.
Fiierul translate.y conine specificaiile Yacc. Compilatorul Yacc (care poate
fi apelat n Unix prin comanda yacc translate.y) transform fiierul translate.y ntr-un
program C numit y.tab.c, care este reprezentarea unui analizor sintactic n limbajul C.
Folosind compilatorul C programul este transformat ntr-un program executabil, bout. Programul poate fi compilat n C mpreun cu o subrutin yylex(), scris de
utilizator sau produs de un generator de analizor lexical, Lex. Subrutina yylex()
trebuie s furnizeze urmtorul token din irul de intrare, la fiecare apelare a sa.
Utilizatorul YACC-ului pregtete o specificaie a procesului de intrare, ce
include reguli ce descriu structura de intrare, codul de executat la recunoaterea acestor
reguli i o rutin pentru a efectua procesul de intrare. Clasa specificaiilor acceptate e
foarte general: gramatici LALR(1) cu reguli fr ambiguiti.
YACC-ul este scris ntr-un C portabil, aciunile i rutinele de ieire de asemenea, i
multe din conveniile sintactice ale YACC-ului urmeaz C-ul.
O parte important a procesului de intrare este realizat de analizorul lexical, care
citete fluxul de intrare, recunoate structurile de nivel inferior, token-ii, i i comunic
analizorului sintactic. Din motive istorice, o structura recunoscut de analizorul lexical
se numeste simbol terminal, iar o structura recunoscut de analizorul sintactic simbol
non-terminal. Pentru a evita confuziile, terminalele vor fi referite ca token-i.
Intrarea poate s nu corespund specificaiilor i n aceste cazuri YACC-ul nu
reusete s produc un analizor sintactic. De exemplu, specificaiile pot fi contradictorii
sau ele pot cere un mecanism de recunoatere mai puternic dect cel disponibil n
Pag.75

TEHNICI DE COMPILARE

YACC. n prima situaie e vorba de erori, iar cea de-a doua poate fi n general corectat
fcnd analizorul lexical mai puternic sau rescriind o parte din regulile gramaticale.

3.4.2.Specificaii de baz
Identificatorii ce apar n specificatii refera fie token-i fie simboluri non-terminale.
YACC-ul cere ca numele de token-i s fie declarate. n plus, adesea e de dorit s se
includ analizorul lexical ca parte a fiierului de specificaii, care poate s includ i alte
programe.
Un fiier cu specificaii const din 3 seciuni separate prin %% :
declaratii
%%
reguli
%%
programme C

Seciunea de declaraii poate fi vid, i, dac seciunea de programe e omis, poate fi


omis i al doilea delimitator %%. Astfel, cea mai scurt specificatie YACC este:
%%
reguli

"Blanc"-urile, "tab"-urile i "newline"-urile sunt ignorate, i ele nu pot s apar


n nume sau simboluri multi-caracter rezervate. Comentariile pot s apara oriunde e
legal un nume i ele trebuie incluse ntre /* ... */ .
Seciunea de reguli cuprinde una sau mai multe reguli gramaticale, de forma:
A: CORP;

unde A reprezint un nume de non-terminal, iar CORP reprezint o secven de zero sau
mai multe nume i literale. Dou puncte, i punct i virgula, sunt punctuaii YACC.
Numele pot avea orice lungime i pot fi alctuite din litere, punct, "underscore",
i cifre, dar trebuie s nceap cu o liter. Literele mici sunt distincte de cele mari.
Numele folosite n corpul unei reguli gramaticale pot reprezenta token-i sau nonterminale.
Un literal const dintr-un caracter inclus ntre apostroafe. Ca n C, "backslash"-ul, e un
caracter "escape". Toate secvenele "escape" din C sunt recunoscute: \' n' = "newline",
'\r' = "return", '\'' = apostrof, '\\' = "backslash", '\t' = "tab", '\b' = "backspace", '\f' =
"formfeed", '\xxx' = xxx n octal. Caracterul NUL ('\0' sau 0) nu trebuie folosit niciodat
n regulile gramaticale.
Daca exist mai multe reguli gramaticale cu aceeai parte stng, se poate folosi
bara vertical '|' pentru a evita rescrierea. n plus, punct i virgula de la sfiritul unei
reguli pot fi eliminate cnd urmeaz o bar vertical. Astfel regulile:
A:BCD;
A:EF;
A:G;

pot fi scrise i ca:


A:BCD
|EF
|G
;
Pag.76

TEHNICI DE COMPILARE

Nu e obligatoriu ca toate regulile gramaticale cu aceeai parte stng s apar


mpreun, dar e recomandabil.
Dac un simbol non-terminal recunoate vid, aceasta se scrie de exemplu:
empty :
;
Numele ce reprezint token-i trebuie declarate, i aceasta se face cel mai simplu
folosind cuvntul cheie %token :
% token nume1 nume2 nume3 ...
n seciunea de declaraii. Orice nume nedefinit n seciunea de declaraii e presupus a
reprezenta un non-terminal. Orice simbol non-terminal trebuie s apar n partea stng
a cel puin unei reguli. Dintre simbolurile non-terminale, simbolul de start ("start
simbol") are o importan particular. Analizorul sintactic recunoate de fapt simbolul
de start. Acest simbol reprezint cea mai larg i mai general structur descris de
regulile gramaticale. Simbolul de start este considerat implicit a fi partea stng a
primei reguli gramaticale. E de dorit ns, s se declare simbolul de start n mod
explicit, n seciunea de declaraii, folosind cuvntul cheie %start :
%start simbol_de_start

Sfritul intrrii e semnalat de un token special, numit marcator de sfrit ("endmarker"). Dac token-ii citii pna la "end-marker" formeaz o structur ce se potrivete
cu simbolul de start, dup ce e ntlnit "end-marker"-ul analizorul revine la apelantul
su i accept intrarea.
Dac marcatorul de sfrit e "vzut" n orice alt context atunci exist o eroare.
Analizorul lexical are sarcina de a returna "end-marker"-ul. n mod normal acesta
este o valoare de I/E ca "end-of-file" sau "end-of-record".

3.4.3. Aciuni YACC


Utilizatorul poate asocia fiecrei reguli gramaticale aciuni de realizat cnd
respectiva regula e recunoscut n fluxul de intrare. Aceste aciuni pot returna valori i
pot obine valorile returnate de aciunile precedente. Mai mult, analizorul lexical poate
returna valori pentru token-i.
O aciune este o secven de instruciuni C. Astfel:
A:

'(' B ')' { hello(1, "abc");}

XXX : YYY ZZZ { printf ("a message\n"); flag=25;}

sunt exemple de reguli gramaticale cu aciuni.


Simbolul dolar, '$', este un semnal special pentru YACC. Pentru a returna o
valoare, aciunea seteaz pseudo-variabila $$ la acea valoare. De exemplu, o aciune
care nu face altceva dect s returneze valoarea 1,
e: { $$= 1;}

Pentru a obine valori returnate de aciuni precedente sau de analizorul lexical, se pot
folosi pseudo-variabilele: $1, $2, ... , care refer valorile returnate de componentele din
partea dreapt a regulei curente, citind de la stnga la dreapta. Astfel, pentru:
A:BCD;

$2 are valoarea returnat de C, iar $3 valoarea returnat de D. s consideram:


expr : '(' expr ')' ;
Pag.77

TEHNICI DE COMPILARE

Valoarea returnat de aceasta regula e de obicei valoarea lui expr dintre paranteze.
Acest lucru poate fi precizat astfel:
expr : '(' expr ')'

{ $$=$2;}

Implicit, valoarea unei reguli e valoarea primului sau element, $1. Astfel, regulile
gramaticale de forma:
A : B;

nu au nevoie de aciunea explicit {$$=$1;}.


n multe aplicaii, iesirea nu e realizat direct de aciuni. Se construiete ns o
strucur de date (cum ar fi un arbore de analiz) n memorie, i se aplica transformari
respectivei structuri nainte de a se genera ieirea. Arborii de analiz sunt uor de
construit, dac sunt date rutinele necesare pentru a construi i menine structura dorit.
S presupunem ca exist o funcie node(), scris astfel nct apelul node(L,n1,n2),
creaz un nod cu eticheta L, descendenii n1 i n2, i returneaz indexul nodului nou
creat. Atunci, arborele de analiz poate fi construit cu aciuni ca:
expr : expr '+' expr { $$= node ('+',$1,$3);}

Utilizatorul poate defini alte variabile pentru a fi folosite de aciuni. Declaraiile i


definiiile pot s apar n seciunea de declaraii, incluse ntre marcatorii %{ i %} .
Aceste declaraii i definiii au un domeniu global, deci sunt cunoscute att de
instruciunile aciune ct i de analizorului lexical.
De exemplu:
%{ int var1 = 0;
%}

plasat n seciunea de declaraii face variabila var1 accesibil tuturor aciunilor.


Analizorul YACC folosete numai identificatori ce ncep cu 'yy', motiv pentru care
utilizatorul trebuie s evite astfel de nume.

3.4.4. Analiza lexical


Utilizatorul trebuie s furnizeze un analizor lexical care s citeasca fluxul de
intrare i s comunice token-ii (cu valori, dac se dorete) analizorului sintactic.
Analizorul lexical e o funcie ce returneaz ntregi, numit yylex(). Funcia returneaz
numrul terminalului ("token-number"), reprezentnd tipul de token citit. Dac exist o
valoare asociat cu acel token, ea trebuie atribuit variabilei externe yylval. Analizorul
sintactic i cel lexical trebuie s se "neleag" asupra acestor numere de token, pentru
ca ele s poat comunica corect. Numerele pot fi alese de YACC sau de utilizator. n
ambele cazuri, se foloseste mecanismul #define din C pentru a permite analizorului
lexical s returneze simbolic aceste numere. S presupunem ca numele de token DIGIT
a fost definit n seciunea de declaraii a fiierului de specificaii YACC. Poriunea
relevant a analizorului lexical ar putea arat astfel:
yylex()
{
extern int yylval; int c;
c=getchar();
switch(c){
case '0':
case '1':
.........
case'9' :
Pag.78

TEHNICI DE COMPILARE

yylval=c-'0';
return(DIGIT);
}

Intenia este de a returna un cod de token DIGIT i valoarea sa numeric. Cnd


codul pentru analizorul lexical e plasat n seciunea "programe" a fiierului de
specificaii, identificatorul DIGIT va fi definit ca numrul de token asociat cu token-ul
DIGIT.
Acest mecanism conduce la analizoare lexicale limpezi i uor de modificat.
Singura capcan este necesitatea de a evita orice nume de token care e rezervat sau
semnificativ n C sau n analizor. De exemplu, utilizarea numelor de token if sau while
va cauza, aproape sigur, dificulti mari la compilarea analizorului lexical. Numele de
token error este rezervat pentru tratarea erorilor i trebuie utilizat n mod
corespunztor.
Dup cum s-a menionat, numerele de token pot fi alese de YACC sau de
utilizator. Implicit, ele sunt alese de YACC. Numrul de token implicit pentru un literal
caracter este valoarea numerica a caracterului n setul de caractere local. Celorlalte
nume le sunt asociate numere de token ncepnd cu 257.
Pentru a asigna un numr unui token, inclusiv literalelor, prima apariie a
numelui de token sau a literalului n seciunea de declaraii, poate fi imediat urmat de
un ntreg nenegativ. Acest ntreg este tratat ca fiind numrul de token al numelui sau al
literalului. Numele i literalele ce nu sunt definite prin acest mecanism i pstreaz
definiia lor implicit. Este important ca toate numerele de token s fie distincte.
Marcatorul de sfrit ("end_marker"-ul) trebuie s aib numrul de token 0 (zero) sau
negativ. Acest numr de token nu poate fi redefinit de utilizator. Analizorul lexical
trebuie s fie pregtit s returneze zero sau un numr negativ la ntlnirea sfiritului
intrrii.
Un instrument util pentru constructia de analizoare lexicale este LEX-ul, care
genereaz analizoare potrivite pentru a lucra n armonie cu analizoarele sintactice
generate de YACC.
Prezentm dou variante de program Yacc: varianta I, care folosete subprogramul
yylex() scris n C i varianta II, care folosete lex.yy.c produs de Lex. Programele
construiesc analizorul sintactic pentru gramatica care descrie instruciunile ffectuate
de un calculator de birou, care poate calcula valoarea unor expresii aritmetice:
EE+T|T
TT*F|F
F (E) | DIGIT
unde token-ul DIGIT reprezint o singur cifr cuprins ntre 0 i 9.
Exemplul 3.28. Varianta I
%{
#include <ctype.h>
%}
%token DIGIT
%%
line :
expr \n
;
expr :
expr + term

{printf(%d\n, $1);}
{ $$ = $1 + $3; }
Pag.79

TEHNICI DE COMPILARE

|
term
;
term :
term * factor
|
factor
;
factor :
( expr )
|
DIGIT
;
%%
yylex() {
int c;
c = getchar();
if(isdigit(c)) {
yylval = c - 0;
return DIGIT;
}
return c;
}

{ $$ = $1 + $3; }

{$$ = $2; }

n ipoteza c se folosete fiierul realizat n Lex, yy.lex.c, atunci programul este identic
cu programul anterior cu excepia ultimei seciuni:
Exemplul 3.29. Varianta II
%{
#include <ctype.h>
%}
%token DIGIT
%%
line :
expr \n
;
expr :
expr + term
|
term
;
term :
term * factor
|
factor
;
factor :
( expr )
|
DIGIT
;
%%
#include lex.yy.c

{printf(%d\n, $1);}
{ $$ = $1 + $3; }

{ $$ = $1 + $3; }

{$$ = $2; }

3.4.5. Modul de lucru al analizorului sintactic


Analizorul generat de YACC, pe baza specificaiilor date, este similar cu cel
descris n 3.3., deci este un analizor de tip bottom-up. El simuleaz funcionarea unui
automat push-down determinist, care folosete o tabel de analiz format din dou
pri: partea de aciuni i partea de salt.

3.4.6. Ambiguiti i conflicte


Un set de reguli gramaticale este ambiguu, dac exist un ir de intrare ce poate fi
structurat n dou sau mai multe feluri. De exemplu regula:
Pag.80

TEHNICI DE COMPILARE

expr : exp - expr


spune c putem forma o expresie aritmetica din alte dou expresii i un semn minus
ntre ele. Din nefericire aceast regul nu definete complet felul n care ar putea fi
structurate anumite intrri. De exemplu, pentru expr - expr - expr, regula pemite
structurarea fie ca (expr - expr) - expr , fie ca expr - (expr - expr) , prima fiind numit
asociativitate stng, iar a doua asociativitate dreapt. Cnd ncearca s construiasc
analizorul, YACC-ul detecteaz asemenea ambiguiti. S analizm problema cu care se
confrunt el cnd i se d o intrare de forma expr - expr - expr . Cnd este citit a doua
expresie, intrarea vzuta, expr - expr , se potrivete cu partea dreapt a regulei
gramaticale. Analizorul poate reduce aplicnd aceast regul i obinnd expr (partea
stnga a regulii). Analizorul poate citi apoi partea final a intrrii: - expr , i poate
reduce din nou. n acest caz s-ar folosi asociativitatea stng. Dar, cnd analizorul vede
expr - expr , el ar putea amna aplicarea imediat a regulii i s continue citirea pna ar
vedea: expr - expr - expr . Atunci ar putea aplica regula celor trei simboluri cele mai din
dreapta, reducndu-le la expr. S-ar obine expr - expr , i s-ar putea face nc o reducere.
n acest caz s-ar folosi asociativitatea dreapt. Astfel, la atunci cnd vede expr - expr
analizorul poate ntreprinde dou aciuni legale: o deplasare sau o reducere, i nu are
nici o indicaie pentru alegere. Aceast situaie se numeste conflict
deplasare-reducere.
Se ntmpl ca analizorul s aib de ales ntre dou reduceri legale. O astfel de
situaie este un conflict reducere-reducere. De notat ns c nu exist niciodat
conflicte deplasare-deplasare. YACC-ul reusete totui s produc un analizor i cnd
apar conflicte deplasare-reducere sau reducere-reducere. Dac este necesar, el alege
una dintre aciunile posibile. O regul ce descrie alegerea care trebuie fcut ntr-o
situaie dat, se numete regul de dezambiguizare. YACC-ul utilizeaz dou reguli
de dezambiguizare implicite:
1. ntr-un conflict deplasare-reducere se alege deplasarea.
2. ntr-un conflict reducere-reducere se alege regula gramatical care apare prima
(n secvena de intrare).
Cu alte cuvinte, regula 1 spune c reducerile sunt evitate cnd se poate face o
deplasare, iar regula 2 d un control, relativ dur, utilizatorului asupra
comportamentului analizorului n situaia unui conflict reducere-reducere, acestea din
urm trebuind evitate.
Aciunile din reguli pot de asemenea s genereze conflicte, dac o aciune trebuie
efectuat nainte ca analizorul s poat fi sigur care regul s fie recunoscut. n
aceste cazuri, aplicarea regulilor de dezambiguizare e nepotrivit i conduce la un
analizor incorect. De aceea, YACC-ul raporteaz ntotdeauna numrul de conflicte
deplasare-reducere rezolvate prin regulile 1 i 2.
n general, dac e posibil s se aplice reguli de dezambiguizare pentru a produce
un analizor corect, e posibil i s se rescrie regulile gramaticale astfel nct pentru aceai
intrare, s nu existe conflicte. Din acest motiv, multe dintre generatoarele de analizoare
din trecut au considerat conflictele ca erori fatale. ns aceast rescriere e oarecum
nenatural i produce analizoare mai lente. De aceea YACC-ul poate produce
analizoare i n prezena conflictelor.
Pag.81

TEHNICI DE COMPILARE

3.4.7. Precedena
Exist o situaie n care regulile de rezolvare a conflictelor date mai sus nu sunt
suficiente, i anume n analiza expresiilor arimtetice. Multe dintre construciile folosite
pentru expresii aritmetice pot fi n mod natural descrise prin noiunea de nivele de
precedent ale operatorilor, mpreuna cu informaie legat de asociativitatea stng sau
dreapt. Deci, se pot folosi gramatici ambigue, dar cu reguli de dezambiguizare
potrivite, pentru a crea analizoare mai rapide i mai uor de scris dect cele construite
din gramatici neambigue. Reguli gramaticale de forma:
expr : expr OP expr

i
expr : UNARY expr

scrise pentru toi operatorii unari i binari dorii, formeaz baza specificaiilor n acest
caz. Se creaz o gramatic foarte ambigu, cu multe conflicte. Ca reguli de
dezambiguizare, utilizatorul specific precedena tuturor operatorilor i
asociativitatea operatorilor binari. Aceast informaie este suficient pentru ca
YACC-ul s rezolve conflictele de analiz n acord cu aceste reguli, i s construiasc
un analizor care realizeaz precedenele i asociativitile dorite.
Precedenele i asociativitile sunt ataate token-ilor n seciunea de declaraii,
printr-o serie de linii ce conin un cuvnt cheie YACC: % left, %right sau % nonassoc,
urmat de o lista de token-i. Toi token-ii de pe aceai linie vor fi tratai ca avnd acelai
nivel de preceden i asociativitate. Liniile trebuie date n ordinea cresctoare a
precedenei (a puterii de legare). Astfel regulile:
% left '+' '-'
% left '*' '/'
descriu precedena i asociativitatea celor 4 operatori aritmetici. Plus i minus sunt
asociative la stnga i au precedena mai mic dect stea i "slash", i ele asociative la
stnga. %right e folosit pentru a descrie asociativitatea dreapt pentru operatori, iar
%nonassoc e folosit pentru a descrie operatori cum ar fi .LT. din FORTRAN, care nu
poate asocia cu el nsui. Astfel:
A .LT. B .LT. C
este ilegal. De exemplu, specificaia:
%right '='
%left '+' '-'
%left
%%
expr :
|
|
|
|
|
;

'*' '/'
expr '=' expr
expr '+' expr
expr '-' expr
expr '*' expr
expr '/' expr
NAME

poate fi folosit pentru a structura intrarea:


a=b=c*d -e-f*g
astfel:
a = ( b = ( (( c * d) - e ) - ( f * g)))
Pag.82

TEHNICI DE COMPILARE

Cnd se folosete acest mecanism, n general, trebuie acordat o anumit


preceden i operatorilor unari, deoarece deseori un operator unar i unul binar pot
avea aceai reprezentare simbolic, dar precedene diferite. De exemplu exist -' '-ul unar
i binar. Minus-ului unar i se poate acorda aceeai preceden ca i nmulirii, sau chiar
mai mare, n timp ce minusul binar are o preceden mai mic dect nmulirea.
Cuvntul cheie %prec schimb nivelul de preceden asociat cu o regula gramatical
particular. %prec apare imediat dup corpul regulei, nainte de aciune sau de puncti-virgula de sfrit a regulii, i e urmat de un nume de token sau de un literal. El face ca
precedena regulei s devin cea a urmtorului nume de token sau literal. De exemplu,
pentru ca minusul unar s aib aceeai preceden cu nmulirea, regulile pot s arate
astfel:
%left
%left
%%
expr :
|
|
|
|
|
;

'+' '-'
'*' '/'
expr '+' expr
expr '-' expr
expr '*' expr
expr '/' expr
'-' expr %prec '*'
NAME

Un token declarat cu %left, %right sau %nonassoc nu mai trebuie declarat i cu


%token. Precedenele i asociativitile sunt utilizate de YACC pentru a rezolva
conflictele de analiz, i ele produc reguli de dezambiguizare. Formal, regulile lucreaz
astfel:
1. Se nregistreaz precedenele i asociativitile pentru acei token-i care le
posed.
2. Se asociaz o preceden i o asociativitate cu fiecare regul gramatical, i
anume precedena i asociativitatea ultimului token sau literal din corpul regulei.
Dac este folosit construcia %prec, ea se suprapune peste aceast situaie
implicit. Unele reguli gramaticale pot s nu aib ataate preceden i
asociativitate.
3. Cnd exist un conflict reducere-reducere sau deplasare-reducere, i fie
simbolul de la intrare, fie regula gramaticala nu au precedent i asociativitate, se
folosesc cele dou reguli de dezambiguizare implicite i
conflictele sunt
raportate.
4. Dac exist un conflict deplasare-reducere i att regula gramatical ct i
simbolul de la intrare au preceden i asociativitate ataate, conflictul e rezolvat
n favoarea aciunii ce este asociat cu o preceden mai mare. Dac precedenele
sunt identice, atunci se foloseste asociativitatea. Asociativitatea stng implic
reducere, asociativitatea dreapt implic deplasare, iar non-asociativitatea
implic eroare.
Conflictele rezolvate prin preceden nu sunt raportate. Aceasta nseamn c erorile
n specificarea precedenelor trebuie evitate. Fiierul y.output poate ilustra dac
analizorul lucreaz corect.
Pag.83

TEHNICI DE COMPILARE

3.4.8. Tratarea erorilor


Tratarea erorilor este un lucru dificil, multe din probleme fiind de natur
semantic. Dac e ntlnit o eroare, poate fi necesar, de exemplu, s se memoreze
arborele de analiz, s se modifice sau s se tearg intrri n tabela de simboluri, i, n
general, s se seteze "switch"-uri pentru a se evita generarea de cod n continuare. De
obicei e util ca la ntlnirea primei erori s nu se abandoneze procesarea, ci s se
continue prelucrarea intrrii pentru a gsi eventual i alte erori de sintax. Aceasta
conduce la problema de a face analizorul s "restarteze" dup o eroare. O clas general
de algoritmi care realizeaz acest lucru implica ignorarea unui numr de token-i din
intrare i ncercarea de a ajusta analizorul astfel nct s poat continua procesarea.
Pentru a permite utilizatorului controlul asupra acestui proces, YACC-ul consider
numele de token "error" rezervat pentru tratarea erorilor. Acest token, folosit n reguli,
marcheaz locurile unde pot s apar erori i el poate realiza "refacere" sau "reluare".
Analizorul va lua stri de pe stiv pn cnd intr ntr-o stare unde token-ul "error" e
legal. Atunci se comport ca i cnd "error" ar fi token-ul "look-ahead" curent, i
realizeaz aciunea corespunzatoare. Token-ul "look-ahead" e resetat la token-ul ce a
cauzat eroarea. Dac nu a fost specificat nici o regula special pentru eroare,
procesarea se oprete la prima eroare detectat.
Pentru a preveni o cascada de mesaje de eroare, dup detectarea unei erori,
analizorul rmne n starea de eroare pn cnd au fost citii i deplasai cu succes 3
token-i. Dac se detecteaz o nou eroare cnd analizorul e deja n starea de eroare, nu
se da nici un mesaj i token-ul din intrare este ters. De exemplu, o regul de forma stat
: error, ar nsemna de fapt, c la o eroare de sintax, analizorul ncearc s sar peste
instruciunea (stat) n care a fost vzut eroarea. Mai exact, analizorul analizeaz
nainte, cautnd 3 token-i ce ar putea urma legal unei instruciuni (statement) i ncepe
procesarea cu primul dintre acetia. Dac nsa instruciunile nu sunt suficient de
distincte prin partea lor de nceput, analizorul poate lua un start fals, n mijlocul unei
instructiuni, i sfrete raportnd o a doua eroare care de fapt nu exist.
Acestor reguli speciale li se pot asocia aciuni ce pot ncerca s reiniializeze
tabele, s reclame spaiu n tabela de simboluri, etc.
O alt form a regulilor de eroare apare n aplicaiile interactive, unde poate fi de
dorit ca dup o eroare s se permit reintroducerea liniei. O astfel de regul de eroare ar
putea fi:
input : error '\n' { printf ("reenter last line:");}
input
{ $$=$4;}

Exist ns o dificultate potenial legat de aceast soluie, i anume faptul c


analizorul trebuie s proceseze corect 3 token-i din intrare nainte de a admite ca s-a
resincronizat corect. Dac linia nou introdus conine o eroare n primii 2 token-i,
analizorul terge token-ii respectivi fr s dea nici un mesaj. Acest lucru e inacceptabil.
Din acest motiv, exist un mecanism ce poate fi folosit pentru a fora analizorul s
"cread" ca o eroare a fost complet acoperit. Este vorba de instruciuneayyerrok; , care
utilizat ntr-o aciune reseteaz analizorul la modul normal de lucru (l scoate forat din
starea de eroare). Astfel, ultimul exemplu poate fi scris n felul urmtor:
input: error '\n'
{yyerrok;
Pag.84

TEHNICI DE COMPILARE

printf ("Reenter last line: ");}


input
{$$=$4;}
;

Dup cum s-a menionat, token-ul vzut imediat dup simbolul "error" e token-ul
de intrare la care a fost depistat eroarea. Uneori, acest lucru poate fi nepotrivit. De
exemplu, o aciune de acoperire a unei erori ar putea s ia asupra sa responsabilitatea de
a gsi locul corect pentru reluarea analizei. n acest caz, token-ul "look-ahead"
precedent trebuie ters. Instructiunea: yyclearin; , efectueaz acest lucru. S
presupunem ca aciunea dup eroare era de a apela o rutin de resincronizare sofisticat,
furnizat de utilizator, care a ncercat s avanseze intrarea la nceputul urmtoarei
instruciuni valide. Dup ce aceast rutin a fost apelat, urmtorul token returnat de
yylex() este probabil primul token dintr-o instruciune legal. Vechiul token, ilegal,
trebuie nlturat i starea de eroare trebuie resetat. Acestea potfi realizate astfel:
stat : error { resync();
;

yyerrok;

yyclearin; }

Aceste mecanisme sunt relativ dure, dar permit o refacere simpl a analizorului
din multe erori. Mai mult, cnd este necesar, utilizatorul poate obine controlul pentru a
se ocupa de aciunile de eroare cerute de alte poriuni din program.
3.5. REZUMAT
Analizorul sintactic are ca intrare irul de uniti lexicale obinut n faza de
analiz lexicala, iar ieirea este rspunsul nu, dac irul de intrare nu este corect din
punct de vedere sintactic, sau da, n ipoteza c irul de intrare este corect. n acest
ultim caz analizorul sintactic construiete i un arbore sintactic din irul unitilor
lexicale, arbore pe care l transmite analizurului semantic.
Analiza sintactic poate s fie de tip top-down (descendent) atunci cnd arborele
sintactic este construit pornind de la rdcin spre rezultat, sau de tip bottom-up
(ascendent), atunci cnd arborele sintactic este construit pornind de la rezultat spre
rdcin.
n esen analizorul sintactic este un automat push-down nedeterminist, pentru c
regulile sintactice ale limbajelor de programare verificate de acest analizor sunt reguli
independente de context i deci implementarea este destul de dificil. Se pot construi i
analizoare deterministe pentru anumite gramatici ale limbajelor de programare i anume
pentru gramatici de tip LL(k) i LR(k). n acest capitol au fost prezentai algoritmi de
analiz top-down determinist (analiza predictiv recursiv i nerecursiv) pentru
gramatici de tip LL(1), i algoritmi de analiz bottom-up pentru gramatici de tip LR.
S-au prezentat i problemele de implementare ale algoritmilor i problemele de
tratare a erorilor, la fiecare clas de algoritmi n parte, iar n final s-a prezentat
generatorul analizor sintacticYACC. Acest generator, care poate s apeleze la rndul lui
analizorul lexical construit de LEX, permite realizarea unui analizor sintactic ntr-un
mod foarte simplu.

Pag.85

TEHNICI DE COMPILARE

Exerciii
1. Construii o gramatic pentru expresii pentru fiecare din limbajele urmtoare:
a. Pascal;
b. C;
c. Java.
2. Considerai gramatica:
S( L ) a
LL, S S
a. Care sunt terminalele i care sunt neterminalele din gramatic;
b. Gsii arborele de sintax pentru urmtoarele secvene:
i.
(a,a)
ii.
(a,(a,a))
iii. (a,(a,a))
iv.
(a,((a,a),(a,a)))
c. Construii o derivaie la stnga i una la dreapta pentru fiecare din
secvenele de la 2.b.
3. a. Eliminai recursia stng din gramatica de la exerciiul 2.
b.Construii un analizor predictiv pentru gramatica de la 3.a.
4. Considerai gramatica:
SAS b
ASA a
a. Construii o colecie de itemuri LR(0) pentru aceast gramatic.
b. Construii o tabel de analiz de tip SLR pentru aceast gramatic.
5. Scriei un program Yacc care are ca intrare o expresie aritmetic iar ca ieire va
furniza arborele sintactic al expresiei regulate.
6. Scriei un analizor sintactic Yacc pentru expresii aritmetice.

Pag.86

TEHNICI DE COMPILARE

n faza de analiz sintactic se analizeaz acele structuri ale limbajelor


de programare care pot fi descrise cu ajutorul regulilor independente de
context. Limbajele de programare au ns i structuri dependente de
context, cum ar fi corespondenele dintre parametrii formali i actuali ai
unei funcii, utilizarea tipurilor variabilelor, transmiterea atributelor i
altele. Acestea sunt verificate n faza de analiz semantic.

4.1. ANALIZA SEMANTIC


n faza de analiz semantic se verific o serie de reguli care nu pot fi prinse n
faza de analiz sintactic (transmiterea atributelor, verificarea tipurilor, conversia de
tipuri, corespondena ntre parametrii formali i cei actuali ai procedurilor i
funciilor).
Rezultatul acestei faze este arborele sintactic completat cu informaii semantice.
De cele mai multe ori, fazele de analiz sintactic i semantic sunt implementate
simultan, de aceea vom considera i acest aspect n cele ce urmeaz.
Un compilator trebuie s verifice dac programul surs respect att regulile
sintactice ct i pe cele semantice. Aceast verificare, numit verificare static, ne
asigur c anumite tipuri de erori de programare vor fi detectate i raportate.
Exemple de verificri statice sunt:
Verificarea tipurilor. Un compilator va raporta eroare dac un operator este
aplicat unui operand incompatibil; de exemplu, dac o variabil care desemneaz
numele unui tablou este adunat cu o variabil care desemneaz numele unei
proceduri.
Verificarea transferului controlului. Instruciunile care produc transferul
controlului trebuie s tie cui s transfere acest control; de exemplu, o instruciune
break n C determin prsirea imediat a celei mai interioare instruciuni while, for
sau switch.
Verificarea unicitii. Exist situaii n care un obiect trebuie declarat o singur
dat; de exemplu, n Pascal, un identificator trebuie s fie declarat unic, etichetele din
instruciunile case trebuie s fie distincte i elementele dintr-un tip enumerare nu
trebuie s se repete.
Cea mai important dintre verificri este verificarea tipurilor. Celelalte
verificri statice sunt de rutin i pot fi implementate fr dificultate.

4.1.1. VERIFICAREA TIPURILOR


Compilatorul trebuie s execute multe verificri semantice asupra programului
surs. Multe din acestea sunt realizate n legtur cu analiza sintactic altele sunt
Pag.87

TEHNICI DE COMPILARE

independente de analiza sintactic. Una din aceste verificri care nu este direct este
verificarea tipurilor.
Un verificator de tipuri cerceteaz dac tipul unei construcii este cel ateptat ntrun context anume. Astfel se execut urmtoarele verificri:
Se verific dac numele i valorile sunt folosite n concordan cu regulile
limbajului;
Detecteaz conversiile de tip implicite, insernd o succesiune de
instruciuni acolo unde este necesar aceasta conversie.
De exemplu, operatorul aritmetic mod din Pascal necesit operanzi ntregi, deci
trebuie verificat dac operanzii unei operaii mod sunt ntregi. Similar, trebuie
verificat dac referina este aplicat unui pointer, indexarea este fcut unui tablou,
funciile definite de utilizator sunt aplicate numrului i tipului corect de argumente,
etc.
Un alt exemplu l constituie limbajul FORTRAN, unde variabilele a cror nume
ncepe cu una dintre literele I,J,K,L,M,N, sunt considerate implicit de tipul ntreg,
restul fiind implicit de tip real. n afar de aceste convenii implicite mai pot exista i
declaraii de tip explicite care modific convenia.
n PASCAL toate variabilele sunt declarate anterior utilizrii lor, deci aici se
verific dac sunt corect utilizate. i n limbajul PASCAL i n C exist un sistem de
tipuri mai complicat. Pe lng faptul ca se permite utilizarea tipurilor declarate
(integer, real, character, label, i altele) se pot construi noi tipuri (tabele
multidimensionale de variabile de un anumit tip sau structuri cu elemente de tipuri
arbitrare). Putem avea de asemenea pointeri pentru variabile de tipuri arbitrare. n C se
pot aplica reguli de derivare a noi tipuri n mod recursiv. Astfel, de exemplu, se poate
defini un array de array de funcii care returneaz valori, valori ce reprezint pointeri
pentru ntregi.:
Int ( *func) ( ) [100] [20];
Dei limbajul C are un sistem complex de tipuri, el nu necesit reguli
complicate de verificare a tipurilor. De exemplu tipurile argumentelor unei funcii nu
trebuiesc definite anterior i astfel este posibil s aplelm o funcie cu alte tipuri de
argumente dect se atepta iniial. Sistemul de tipuri este astfel conceput nct s
ajute programatorul s evite erorile semantice i nu s foreze comportarea lor. Astfel
de limbaje se numesc limbaje cu restricii de tip slabe.
n contrast cu acestea exist aa numitele limbaje cu restricii de tip tari, cum
ar fi ALGOL68 i ADA. n principiu astfel de limbaje previn programatorul la
apariia oricrei erori semantice datorate tipului. Dac astfel de sisteme de tipuri
sunt suficient de flexibile, compilatoarele pot deveni foarte mari i complexe, nu
numai din punct de vedere al compilrii, dar i din punct de vedere al timpului de
execuie.
Implementarea verificrii tipurilor se poate face n dou moduri:
Verificarea static a tipurilor, care se face n timpul compilrii;
Verificarea dinamic a tipurilor, care se realizeaz n timpul execuiei
programului. Aceasta este mai puin eficient dect cea static, dar dac un
limbaj permite determinarea tipurilor n faza de execuie atunci se va face
Pag.88

TEHNICI DE COMPILARE

verificare dinamic. SNOBOL4 este un astfel de limbaj. Tipul variabilei poate


fi pstrat ntr-un string calculat n timpul execuiei programului. Verificarea
dinamic a tipurilor este rar folosit n limbajele de programare, n comparaie
cu verificarea static.
O alta problem se refer la conceptul de tipuri echivalente. Aceasta apare atunci
cnd trebuie s verificam dac entitile au acelai tip. De exemplu trebuie s
verificm dac entitatea din membrul stng al unei atribuiri are acelai tip ca expresia
din membrul drept.

4.1.2. Conversii de tipuri


S considerm expresia xi, unde x este de tip real i i de tip ntreg. Deoarece
reprezentarea ntregilor i realilor este diferit i sunt folosite instruciuni main
diferite pentru operaii cu ntregi sau reali, compilatorul trebuie mai nti s
converteasc unul dintre operanzi pentru a se asigura c amndoi sunt de acelai tip.
Verificatorul de tipuri este folosit pentru a insera conversiile de tipuri n
reprezentarea intermediar a programului surs.
O conversie este implicit dac este fcut automat de compilator; aceste
conversii sunt limitate la situaiile n care nu se pierde informaie util (de exemplu,
conversia unui ntreg la real).
O conversie este explicit dac programatorul provoac conversia cu ajutorul
unui operator specific.
Exemplul 4.. S considerm expresiile formate prin aplicarea unui operator op
asupra constantelor i identificatorilor, ca n gramatica din Fig. 4.1; s presupunem c
exist dou tipuri, integer i real, cu integer convertindu-se la real oricnd este
necesar. Atributul E.tip poate fi integer sau real; regulile de verificare a tipului sunt
prezentate n Fig. 4.1.
Producie
E num
E num . num
E id
E E1 op E2

Regul semantic
E.tip : integer
E.tip : real
E.tip : rectip(id.tip)
E.tip : dac E1.tip : integer i E2.tip : integer
atunci integer
altfel dac E1.tip : integer i E2.tip : real
atunci real
altfel dac E1.tip : real i E2.tip : integer
atunci real
altfel dac E1.tip : real i E2.tip : real
atunci real
altfel tip_eroare

Fig.4.1.Reguli de verificarea tipului cu conversia implicit de la integer la real


Pag.89

TEHNICI DE COMPILARE

4.1.3. Caracterele unui nume i alocarea spaiului


Exist o deosebire ntre unitatea lexical id pentru un identificator sau nume,
lexema sau irul de caractere care formeaz numele i atributele acelui nume. Un ir
de caractere poate fi dificil de manevrat i compilatorul utilizeaz de obicei o
reprezentare fix a unui nume, alta dect o lexem. Lexema este necesar doar cnd
nregistrarea din tabela de simboluri pentru acel nume este iniializat i cnd se
verific dac un nume vzut n intrare a fost deja nregistrat n tabel. O reprezentare
uzual pentru un nume este un pointer ctre nregistrarea din tabela unde se afl.
Trebuie reinut lexema complet a unui nume pentru a ne asigura c toate
apariiile acelui nume sunt asociate cu aceiai intrare n tabela de simboluri. Este
necesar ns s distingem ntre apariiile unei lexeme care este declarat n domenii
diferite.
Informaiile referitoare la alocarea spaiului pentru un nume trebuie legate de
acel nume i pstrate n tabela de simboluri. Dac spaiul este alocat static i codul
destinaie este limbaj de asamblare, putem lsa asamblorului sarcina de a aloca spaiu
pentru diferitele nume. Trebuie doar scanat tabela de simboluri dup generarea
codului asamblare pentru program i apoi trebuie generate definiii de date n limbaj
de asamblare pentru fiecare nume care va fi folosit de program. Dac compilatorul
genereaz cod main, poziia fiecrui obiect (relativ la o origine fix cum ar fi
nceputul unei nregistrri de activare) trebuie identificat. n cazul numelor alocate
pe stiv sau heap, compilatorul nu aloc spaiu, ci doar proiecteaz nregistrrile de
activare pentru fiecare procedur, cum am vzut n seciunea anterioar.

4.1.4. Structura de tip list pentru tabela de simboluri


Cea mai simpl i uor de implementat structur de date pentru o tabel de
simboluri este o list liniar de nregistrri. Noile nume sunt adugate n list n
ordinea n care apar. Poziia de sfrit a listei este cunoscut pentru a ti unde va fi
plasat noua nregistrare. Cutarea unui nume se face prin parcurgerea napoi a listei.
Dac se ajunge la nceputul listei fr ca numele s fie gsit, atunci o nregistrare
nou trebuie adugat la captul listei.
ntr-un limbaj structurat pe blocuri, o apariie a unui nume este n domeniul
celei mai apropiate declaraii mbriate a acelui nume. Putem implementa aceast
regul crend o nou nregistrare de fiecare dat cnd un nume este declarat.
Dac tabela de simboluri conine n nume, efortul necesar pentru a insera un nume
nou este acelai dac nu se verific existena acelui nume n tabel. Dac nu sunt
permise nregistrri multiple pentru un nume atunci trebuie verificat ntreaga tabel
pentru a descoperi c numele nu este nregistrat, efortul fiind proporional cu n.
Pentru a gsi datele referitoare la un nume, se verific n medie n2 nume i deci
costul este de asemenea proporional cu n. Astfel, inserarea i identificarea sunt ntrun timp proporional cu n i deci timpul total pentru a insera n nume i a face i
identificri este cel mult cn(ni), unde c reprezint timpul necesar executrii unei
operaii n cod main. ntr-un program de dimensiune medie, putem avea n100 i
Pag.90

TEHNICI DE COMPILARE

i1000 i deci cteva sute de mii de operaii main sunt necesare pentru meninerea
tabelei; aceasta nu constituie neaprat o problem, dar dac n i i sunt multiplicate cu
10, costul se multiplic cu 100 i timpul de lucru cu tabela de simboluri poate ncetini
compilatorul.

4.1.5. Structura de tip hash table pentru tabela de simboluri


Variante ale tehnicilor de cutare cunoscute sub numele de hashing au fost
implementate n multe compilatoare. Deoarece aceast schem permite ca n inserri
i i identificri s fie fcute ntr-un timp proporional cu n(ni)m, pentru orice
constant m, metoda este n general mai eficient dect o list liniar i este metoda
aleas n majoritatea situaiilor. Evident, spaiul necesar memorrii structurii crete cu
m.
O schem uzual de hashing este prezentat n Fig. 4.2. Structura este alctuit
dintr-un tablou fix coninnd m pointeri i m liste nlnuite indicate de ctre cei m
pointeri; fiecare nregistrare din tabela de simboluri apare n exact una din aceste
liste.
Tablou de pointeri, indexat dup
o valoare de hashing
Elemente ale listelor create pentru
diferite nume

cp

poz

ultim

20
stare

ws

32

210

Fig. 4.2. O structur hash table de dimensiune 211.


Pentru a determina dac exist o nregistrare pentru s n tabela de simboluri, se
aplic o funcie h lui s, astfel nct h(s) ntoarce un ntreg ntre 0 i m1. Dac s este
n tabela de simboluri, atunci este n lista numerotat h(s). Dac s nu este n tabel,
atunci o nregistrare nou este creat pentru s i este adugat la captul listei h(s).
Problema care se pune este de a construi o funcie de hashing care s fie uor
de determinat pentru iruri de caractere i s distribuie aceste iruri uniform n cadrul
listelor. O metod de abordare poate fi:
- Se determin un numr ntreg pozitiv h din c1, c2, , ck, caracterele irului s
- Se convertete numrul h ntr-un numr al unei liste, adic ntr-un numr cuprins
ntre 0 i m1 (o metod simpl este de a considera restul mpririi lui h la m).
Pag.91

TEHNICI DE COMPILARE

O tehnic simpl de determinare a lui h este s adunm valorile ntregi ale


caracterelor din ir. O idee mai bun este de a multiplica vechea valoare a lui h cu un
numr constant nainte de a aduna urmtorul caracter la h. Se poate utiliza orice
alt tehnic pentru a determina h.
4.2. REZUMAT
Analiza semantic este ultima dintre fazele de analiz ale unui compilator i ea
verific regulile limbajului de programare care sunt dependente de context i deci nu
pot fi verificate n faza de analiz sintactic. Aceste reguli se refer la corespondena
dintre parametrii formali i acuali ai procedurilor i funciilor, la tipurile variabilelor
cerute de o anumit expresie sau instruciune, la unicitatea definirii unor variabile sau
etichete, la transmiterea controlului n anumite instruciuni cum ar fi instruciunile:
break, while, for, do, etc.
Intrarea ntr-un analizor semantic este arborele de sintax obinut n faza de
analiza sintactic, pe care l completeaz cu informaii semantice. Anumit erori
semantice pot fi corectate n aceast faz prin aa numitele conversii de tip.
Exerciii
1. Descriei tipurile de verificri pe care trebuie s le efectueze un analizor
semantic.
2. Enumerai verificrile care se realizeaz n etapa de verificare de tipuri ntr-un
compilator pentru limbajul:
a. C;
b. Pascal;
c. Java.

Pag.92

TEHNICI DE COMPILARE

n fazele anterioare, faze de analiz, programul surs este descompus n


pri componente. n acest capitol se prezint, pe scurt, faza de sintez a
programului surs, care are drept scop traducerea programului surs
(transformat n faza de analiz) n program obiect.

5.1. GENERAREA CODULUI OBIECT


Faza de generare de cod obiect, numit i faza de sintez a programului, parcurge
arborele de sintax, completat cu informaii semantice, de la rezultat spre rdcin,
genernd la fiecare nod un set de instruciuni. n general, faza de generare de cod este
compus din trei faze:
Faza de generare a codului intermediar;
Faza de optimizare a codului;
Faza de generare a codului obiect propriu-zis.
n faza de generare a codului intermediar compilatorul genereaz o reprezentare
intermediar explicit a programului surs, ca un program pentru o main abstract.
n faza de optimizare de cod compilatorul tinde s mbunteasc codul
intermediar, n sensul micorrii timpului de execuie.
Faza final a compilatorului este faza de generare de cod obiect, care const
ntr-un cod main relocabil sau ntr-un cod de asamblare. Se poate de asemenea
considera drept cod obiect i un cod ntr-un limbaj de programare (de exemplu C)
pentru care exist compilator.

5.1.2.Generarea codului intermediar


Intrarea n faza de sintez o formeaz un arbore sintactic completat cu informaii
semantice. Acesta este parcurs de la rezultat spre rdcin, generndu-se la fiecare nod
o secven de instruciuni n cod obiect. Dup faza de analiz unele compilatoare
genereaz nti o reprezentare intermediar a programului surs. Aceasta poate fi
privit ca un program pentru o main abstract i trebuie s respecte dou condiii:
s fie uor de realizat;
s fie uor de tradus n programul destinaie.
Reprezentarea intermediar poate lua o mulime de forme. n aceast seciune ne
vom ocupa de o reprezentare numit codul cu trei adrese, care seamn cu limbajul
de asamblare al unei maini, n care fiecare locaie de memorie se poate comporta ca
un registru. Codul respectiv const dintr-o secven de instruciuni, fiecare avnd cel
mult trei entiti. Astfel, instruciunea:
id1 = id2 + id3 * 40

(5.1)

corespunde urmtoarei secvene:


temp1 := inttoreal(40)
temp2 := id3 * temp1
temp3 := id2 + temp2

(5.2)
Pag.93

TEHNICI DE COMPILARE

id1 := temp3

Forma intermediar de reprezentare a programului surs are mai multe proprieti.


n primul rnd, fiecare instruciune a codului cu trei adrese conine cel mult un
operator. Astfel, cnd genereaz aceste instruciuni:
compilatorul trebuie s decid asupra ordinii n care se vor efectua operaiile
din instruciunea compilat (nmulirea precede adunarea n programul surs
din (5.1);
compilatorul genereaz nume temporare care pstreaz valoarea rezultat a
fiecrei instruciuni;
instruciunile cu trei adrese pot avea i mai puin de trei operanzi.
pos := init + rata * 40

analizor lexical
id1 := id2 + id3 * 40

analizor sintactic
:=
+

id1
id2

*
id3

num(40)

analizor semantic
:=
+

id1
id2

*
id3

IntToReal(40)

generatorul codului intermediar


temp1 := IntToReal(40)
temp2 := id3 * temp1
temp3 := id2 + temp2
id1 := temp3
optimizarea codului
temp1 := id3 * 40.0
id1 := id2 + temp1
Pag.94

TEHNICI DE COMPILARE

generarea codului
MOVF id3, R2
MULF #40.0, R2
MOVF id2, R1
ADDF R2, R1
MOVF R1, id1

Fig. 5.1. Transformarea unei instruciuni de atribuire aritmetic


Relum exemplul din capitolul I, care prezenta fazele compilrii pentru
instruciunea de atribuire:
pos := init + rata * 40

Aceast instruciune se transform, succesiv, n fiecare faz a compilrii ca n


figura 5.1.
n faza de generare de cod se poate folosi drept cod obiect i un limbaj evoluat
pentru care exist compilator, cum ar fi limbajul C. n aceste caz arborele sintactic
completat cu informaii semantice este parcurs de la rezultat spre rdcin, la fiecare
nod generndu-se una sau mai multe instruciuni C. Asta
nseamn c nu mai este nevoie de fazele de generare de cod intermediar i de
optimizare de cod ( faze care vor fi realizate de compilatorul C).
Arborele sintactic completat cu informaii semantice, care poate fi memorat
printr-o structur de date ca n fig. 5.2., formeaz intrarea n faza de generare de cod
intermediar.
:=

id

id

id

num 40

Fig. 5.2. Structur de date pentru arborele sintactic

5.1.2.Optimizarea codului
Faza de optimizare a codului ncearc s mbunteasc codul intermediar, n
special prin micorarea numrului de instruciuni. Astfel, codul intermediar din (5.2)
devine
temp1 := id3 * 40.0
id1 := id2 + temp1

(5.3)

Compilatorul poate face conversia lui 40 de la ntreg la real n timpul compilrii,


iar de prima instruciune din (5.2) nu mai este nevoie. De asemenea, observm c
temp3 este folosit o singur dat i deci i instruciunea patru din (5.2) poate lipsi.
Rezult astfel reprezentarea optim (5.3).
Pag.95

TEHNICI DE COMPILARE

Exist compilatoare care aloc o mare parte din activitate acestei faze, aa
numitele compilatoare cu optimizare. n plus, exist metode de optimizare simple, care
mresc viteza de execuie a programului destinaie fr a ncetini prea mult viteza
compilatorului.

5.1.3.Generarea codului obiect


Faza final a unui compilator este generarea codului destinaie, constnd de cele
mai multe ori n cod main sau cod asamblare. Locaiile de memorie sunt selectate
pentru fiecare variabil folosit de programul surs. Apoi, instruciunile intermediare
sunt transformate ntr-o secven de instruciuni care execut aceiai operaie. O parte
important este asignarea variabilelor cu regitri. De exemplu, folosind regitrii R1 i
R2, traducerea codului din (5.3) n limbaj de asamblare poate fi:
MOVF
MULF
MOVF
ADDF
MOVF

id3, R2
#40.0, R2
id2, R1
R2, R1
R1, id1

(5.4)

Primul i al doilea operand din fiecare instruciune specific sursa i respectiv


destinaia. Litera F din fiecare instruciune determin lucrul cu numere reale n
virgul flotant.
Codul obiect pentru instruciunea de atribuire (5.1) mut (MOVF) coninutul
adresei id3 n registrul R2, apoi multiplic (MULTF) coninutul lui R2 cu constanta
real 40.0. Semnul # specific faptul c 40.0 trebuie tratat ca o constant. A treia
instruciune mut (MUVF) id2 n R1 i apoi se adun (ADDF) la R1 coninutul lui R2
obinut anterior. La sfrit, valoarea din R1 este mutat (MOVEF) la adresa id1 i
astfel codul reprezint implementarea instruciunii din (1.1).
&5.2. REZUMAT
Faza de generare de cod reprezint de fapt traducerea programului surs n
program obiect. Intrarea n generatorul de cod este arborele sintactic, completat cu
informaii semantice, pe care l parcurge de la rezultat spre radcin, genernd la
fiecare nod cte o secven de instruciuni n cod obiect. Aceast traducere se face n
trei etape: generarea codului intermediar, optimizarea codului i generarea codului
obiect, ultima dintre ele depinznd de limbajul obiect ales pentru scrierea
compilatorului.
Exerciii
1. Generai codul intermediar i apoi codul obiect pentru urmtoarele instruciuni
n limbajul C, presupunnd c variabilele sunt statice i c dispunei de trei
regitri de memorie:
a. x = 1
b. x = y
c. x = x + 1
d. x = a + b + c
e. x = a / (b + c) d * (e +f)
Pag.96

TEHNICI DE COMPILARE

BIBLIOGRAFIE
1. Aho A.V., Sethi ,Ullman D.J., The theory of parsing translation and
compiling, Addison -Wesley Reading, Massachusetts, 1986.
2. Bennett J.P., Introduction to compiling techniques, Mc Grow Hill, London,
1990.
3. Gries D., Compiler construction for digital computers, John Wiley, NewYork, 1971.
4. Hopcroft J.E., Ullman D.J., Formal language and their relation to automata ,
Addison - Wesley Reading, Massachusetts, 1969.
5. Livovschi L. ... Bazele informaticii, Ed. Did. si Ped., Bucuresti 1987.
6. Marcus S. Gramatici si automate finite,Ed. Academiei Romnia , 1964.
7. Orman G. Limbaje formale, Ed. Tehnic, Bucureti, 1982.
8. Paun Gh. Probleme actuale n teoria limbajelor formale , Ed. t. i
Enciclopedic, Bucureti,1984.
9. Marinescu D.Limbaje formale i teoria automatelor, Ed. Univ.
Transilvania, Braov, 2003.
10. Jalobeanu, C., Marinescu D.Bazele teoriei calculului: Limbaje Formale si
Automate, Ed. Albastra, Cluj-Napoca, 2006.
11. Marinescu D.Tehnici de compilare, Ed. Univ. Transilvania, Braov, 2004.
12. Salomaa A., Formal languages, Academic Press, New-York, 1973.
13. Simovici D., Limbaje formale i tehnici de compilare, Ed. Did. si Ped.,
Bucureti , 1978.
14. Tremblay J.P., Sorenson P.G., The Theory and Practice of Compiling, Mc.
Grow-Hill, New-York, 1985.
15. Holub A.I. Compiler Construction in C, Prentice Hall International, 1989.

Pag.97

S-ar putea să vă placă și