Sunteți pe pagina 1din 36

CAPITOLUL I.

LIMBAJE FORMALE

1.1. CONCEPTE DE BAZĂ

Cunoaştem unele limbaje de nivel înalt, cum sunt Pascal, Fortran,


Basic, C şi altele. Ne scriem programele în aceste limbaje iar când citim
un program sau o descriere a unui algoritm scris într-unul din ele, de
obicei, putem spune ce face programul sau algoritmul respectiv (cel puţin
atunci când programul este bine scris şi are o lungime rezonabilă).
Totodată ne dăm seama de erorile din programe sau dintr-o secvenţă de
cod scrisă într-unul din aceste limbaje create de om. Următorul exemplu
de linie de cod din Fortran,

IF (IND.EQ.1) N = N + 1 (1.1)

este corect, în timp ce

IF (IND.EQ.1) N = (1.2)

nu este corect. Surprinzător, nu doar noi, presupuse fiinţe inteligente,


descoperim astfel de greşeli, da chiar şi maşinile (care în esenţă sunt
formate dintr-o colecţie de elemente hardware). Cum se realizează acest
lucru? De fapt, noi introducem programul într-o maşină simbol cu simbol.
Când o expresia de forma (1.1) este prezentată calculatorului se transmite
mai întâi litera I urmată de litera F, simbolul (, şi aşa mai departe. Maşina
acceptă aceste simboluri unul câte unul şi după ce s-au introdus toate,
„ştie” cum să procedeze în continuare. Ea poate determina de asemenea
dacă şirul de simboluri primit este „corect” sau nu. Pentru aceasta,
maşina trebuie să cunoască ce înseamnă „corect” şi să fie în stare să
determine dacă şirul (1.1) este „scris corect”, în timp ce (1.2) nu. În acest
capitol vom discuta mai întâi cum se defineşte un limbaj. Această
definiţie ne va permite să determinăm, în mod automat, dacă un şir de
simboluri aparţine sau nu unui limbaj. O astfel de definire complet
formală şi deterministă nu este posibilă pentru toate limbajele – în
particular, pentru limbaje naturale, cum sunt engleza, franceza şi rusa,
este probabil imposibil. Dar ea pare adecvată pentru limbajele de
calculator – până la urmă calculatoarele funcţionează.
Vom începe cu un alfabet – fiecare limbaj presupune un alfabet.
Alfabetul pentru Fortran se compune din 26 de litere: A, B, ..., X, Y, Z; 10
numere: 0, 1, 2, ..., 9, şi alte simboluri cum ar fi: =, +, /, *, (, (spaţiu).
Alfabetul complet, numit şi mulţime de caractere, poate fi regăsit pe
primele pagini ale oricărui manual dedicat limbajului respectiv.

Definiţia 1.1. Un alfabet este o mulţime nevidă şi finită de


simboluri.

De obicei vom nota alfabetul cu simbolul Σ . Definirea unui limbaj


peste un alfabet dat devine acum simplă.

Definiţia 1.2. Un limbaj L peste un alfabet Σ este o mulţime de


şiruri finite de elemente din Σ .
Elementele uni alfabet Σ se mai numesc şi caractere. Şirurile
dintr-un limbaj L se numesc şi propoziţii din L.
Exemplu 1.1. Fie alfabetul Σ , compus doar din două simboluri: 0
şi 1, Σ = {0,1} . Două exemple de limbaj peste Σ sunt:

a) L1 = {0, 01,110} . Acest limbaj conţine doar trei propoziţii: 0, 01 şi

110.
b) L2 = Toate şirurile de 0 şi 1 cu un număr egal de 0-uri şi 1-uri.
Aşadar, 010011 şi 1010 sunt în limbajul L2 , în timp ce 110 şi 010
nu. Evident că limbajul L2 (spre deosebire de L1 ) conţine un număr infinit
de propoziţii.
Pe parcursul acestei lucrări vom folosi deseori şiruri de simboluri.
Fapt pentru care folosim următoarele convenţii de notaţie. Dacă
σ = x1 x2 ...xn şi τ = y1 y2 ... ym sunt două şiruri, atunci concatenarea lor, notată

στ , este definită prin στ = x1 x2 ...xn y1 y2 ... ym . Aşadar, στ se obţine din σ şi

τ prin alipirea şirurilor σ şi τ (în această ordine). De exemplu, dacă

σ = 01011 şi τ = 001 , atunci στ = 01011001 . Se observă că τσ = 00101011 ,

deci, în general στ ≠ τσ . Dacă x este un caracter, atunci x n este definit


prin ca xx...x (de n ori). Observăm că x 2 = xx , y 3 = yyy , etc. Lungimea
unui şir σ = x1 x2 ...xm corespunde numărului de caractere din σ . Astfel,
lungimea lui σ = abaa este 4. Convenabilă este introducerea conceptului
de şir vid. Un astfel de şir nu are nici un caracter; lungimea lui este 0. El
va fi notat întotdeauna cu λ şi reciproc, simbolul λ va fi rezervat pentru
un astfel de şir. Dacă σ este un şir oarecare şi λ este şirul vid, atunci
concatenarea lui σ cu λ va fi întotdeauna σ , σλ = λσ = σ . Din acest
motiv, dacă x este un simbol oarecare, definim x 0 ca fiind şirul vid λ ,
x 0 = λ . Lungimea şirului vid λ este 0, el fiind singurul şir cu această

lungime.
Definiţia 1.3. Fie Σ o mulţime nevidă. Limbajul tuturor şirurilor
finite de elemente din Σ (inclusiv şirul vid) se notează cu Σ* . Limbajul
tuturor şirurilor nevide de elemente din Σ se notează Σ + .

În general, un limbaj L peste un alfabet Σ nu conţine toate şirurile


posibile de elemente din Σ . Aceasta corespunde faptului că nu orice
secvenţă de simboluri formează un program valid. Ceea ce dorim să
facem în continuare este să descriem o regulă pentru a decide dacă un şir
aparţine unui limbaj sau nu. Un astfel de exemplu putea fi considerat
descrierea limbajului L2 din exemplul 1.1. Aici regula este foarte simplă;
mai mult, dat fiind un şir σ de simboluri de 0 şi 1, este uşor de verificat
dacă propoziţia face parte din limbaj sau nu. Pe de altă parte este greu de
imaginat o regulă atât de simplă pentru un limbaj ceva mai real, cum e
Pascal. Ca urmare, regula trebuie să fie sofisticată şi precisă astfel încât,
dat fiind un program în Pascal (văzut ca un şir lung), ea să determine dacă
este sau nu un program valid: Sunt toate BEGIN-uri închise cu END-uri?
Sunt toate expresiile algebrice bine formate? etc.
O metodă utilă şi generală pentru descrierea de limbaje se bazează
pe gramatici. În general, o gramatică este o mulţime de reguli care, dacă
sunt urmate, vor produce o propoziţie corectă într-un limbaj. O descrierea
detaliată şi exactă a conceptului de gramatică este următoarea:

Definiţia 1.4. O gramatică G se compune din patru obiecte:

1. O mulţime Σ nevidă şi finită numită alfabet. Limbajul generat


de gramatica G se va compune din şiruri de simbolurile din Σ .
Elementele din Σ se numesc terminale şi vor fi notate, în
general, cu litere mici a, b, c, etc.
2. O mulţime nevidă şi finită de simboluri N, fiecare dintre ele
diferit de terminalele din Σ ( N ∩ Σ = ∅ ). Elementele din N se
numesc neterminale sau variabile; în general se notează cu
litere mari A, B, C, etc.
3. Un element dedicat S din N. Acest neterminal S se numeşte
simbolul de start.
4. O mulţime P de producţii (numite şi reguli de producţie, reguli
de substituire). Ea este formată din reguli de forma

A → X 1 X 2 ... X n ,

unde A este un neterminal şi X 1 X 2 ... X n este un şir finit de


terminale sau neterminale ( X i ∈ Σ ∪ N ).

Gramaticile astfel definite poartă denumirea de gramatici


independente de context; le vom discuta amănunţit mai târziu.
Şirul X 1 X 2 ... X n , din partea a 4-a a definiţie de mai sus, nu trebuie să
conţină nici un simbol, el poate fi un şir vid λ . Producţia corespunzătoare
ar fi atunci de forma A → λ , şi se numeşte producţie vidă. O gramatică G
cu Σ , N, S şi P, ca în definiţia 1.4., se va nota G = {Σ, N , S , P} .

Exemplu 1.2. Fie următoarea descriere a unui gramatici


G = {Σ, N , S , P} :

1. Mulţimea de terminale Σ (alfabetul) este {a, b, c} .

2. Mulţimea de neterminale N este {S , A, B, C} .

3. Simbolul de start este S.


4. Mulţimea P este formată din următoarele producţii:
S → AaB S→B A → aB A→a

B → bC C → ac C →λ

În ultima producţie λ este şirul vid şi C → λ este o producţie vidă.


Gramatica are trei terminale (a, b, c), patru neterminale (S, A, B, C) şi
şapte producţii.
În continuare vom descrie cum generează o gramatică un limbaj.
Avem nevoie de următoarea terminologie:

Definiţia 1.5. Fie G = {Σ, N , S , P} o gramatică. O formă

propoziţională a lui G este orice şir X 1 X 2 ... X n de simboluri, fiecare X i


fiind un terminal sau neterminal ( X i ∈ Σ ∪ N ).
O formă propoziţională nu trebuie să conţină nici un simbol, ea
poate fi chiar şirul vid λ . În exemplul 1.2. şirurile AaB, AAaCB, aba şi λ
sunt exemple de forme propoziţionale.

Definiţia 1.6. Fie G = {Σ, N , S , P} o gramatică. Presupunem că σ

este o formă propoziţională exprimata astfel: σ = γ 1 Aγ 2 unde γ 1 şi γ 2 sunt


şiruri de terminale sau neterminale (forme propoziţionale) şi A este un
neterminal. Mai presupunem că A → X 1 X 2 ... X n este o producţie din P. Fie
τ forma propoziţională obţinută din σ prin înlocuirea lui A cu X 1 X 2 ... X n ,

τ = γ 1 X 1 X 2 ... X nγ 2 . Vom spunem că forma propoziţională τ este imediat

derivabilă din σ folosind producţia A → X 1 X 2 ... X n . Folosim următoarea


notaţie:

σ ⇒τ
Fie gramatica G = {Σ, N , S , P} din exemplul 1.2. Forma

propoziţională σ = aBabC derivă imediat forma propoziţională τ = abCabC


folosind producţia B → bC :

σ = aBabC ⇒ abCabC

Cu alte cuvinte, a spune că σ ⇒ τ înseamnă că unul dintre neterminalele


din σ poate fi înlocuit cu partea dreaptă a producţie şi devine şirul τ . În
forma propoziţională σ = aBabC am înlocuit B cu bC ( B → bC fiind o
producţie) şi am obţinut τ = abCabC . Ca alt exemplu, fie σ = aaCba .
Aplicând la σ producţia C → λ obţinem

σ = aaCba ⇒ aaba

Aici am substituit C cu partea dreaptă a producţiei C → λ (şirul vid). Şirul


λ , fiind şirul vid, nu are simboluri, deci aaλ ba = aaba , astfel simbolul C

„dispare”.

Definiţia 1.7. Fie σ 1 şi σ 2 două forme propoziţionale ale unei


gramatici G. Presupunem că există o secvenţă de forme propoziţionale
τ 1 ,τ 2 ,...,τ n astfel încât

σ 1 ⇒ τ1 ⇒ τ 2 ⇒ ... ⇒ τ n ⇒ σ 2 .

Vom spunem atunci că σ 1 derivă σ 2 , sau σ 2 este derivabilă din σ 1 şi


notăm

σ 1 ⇒ *σ 2
În exemplul 1.2. avem S ⇒ *aabC deoarece

S⇒ AaB ⇒ aaB ⇒ aabC


↑ ↑ ↑
S → AaB A→a B → bC

Sub fiecare simbol ⇒ am indicat producţia corespunzătoare


folosită din G la derivare.
Fiind dată gramatica G = {Σ, N , S , P} , limbajul L(G) generat de G

este mulţimea tuturor şirurilor de terminale derivabile din simbolul de


start. Formal avem:

Definiţia 1.8. Fie G = {Σ, N , S , P} o gramatică. Limbajul L(G)

generat de G este definit a fi mulţimea de şiruri

L(G ) = {σ | σ ∈ Σ* , S ⇒ *σ } .

Elementele (şirurile) din L(G) se numesc propoziţii din L(G).

În exemplul 1.2. şirurile b, aabac şi bac aparţin toate lui L(G):

S ⇒ B ⇒ bC ⇒ b
S ⇒ AaB ⇒ aaB ⇒ aabC ⇒ aabac
S ⇒ B ⇒ bC ⇒ bac

Observăm că şirul cb nu este în limbajul L(G). Într-adevăr,


începând cu simbolul de start S, prima formă propoziţională derivată din
S va fi AaB sau B. Nici una dintre aceste două propoziţii va deriva într-o
propoziţie care începe cu c.

Exemplu 1.3. Gramatica din acest exemplu va da limbajul tuturor


expresiilor algebrice corect formate care pot fi obţinute folosind
simbolurile a, +, *, (, şi ). Astfel, expresia (a + a) * a va fi în limbaj, în
timp ce (a + a ) *(a + nu. Această gramatică o notatăm G0 şi o vom întâlni
des pe parcursul acestei lucrări. Alfabetul din G0 este Σ = {a, +,*, (, )} ,

mulţimea de neterminale N = { E , T , F } şi simbolul de start este E.

Producţiile din G0 sunt:

1. E → E + T
2. E → T
3. T → T * F
4. T → F
5. F → ( E )
6. F → a

Expresia (a + a) * a face parte din limbajul L(G0 ) , derivarea sa fiind

2 3 4 5 1
E ⇒ T ⇒ T * F ⇒ F * F ⇒( E ) * F ⇒ ( E + T ) * F
2 4 6
⇒(T + T ) * F ⇒( F + T ) * F ⇒(a + T ) * F
4 6 6
⇒( a + F ) * F ⇒( a + a ) * F ⇒ ( a + a ) * a

Numărul deasupra simbolului ⇒ indică producţia folosită la fiecare


derivare. Pentru a vedea că şirul (a + a ) *(a + nu este în L(G0 ) , observăm
că singura posibilitate de apariţie a simbolurilor ) şi ( într-o formă
propoziţională este prin aplicarea producţiei F → ( E ) . De aici, orice şir
derivat din E va avea acelaşi număr de ) ca şi (, iar şirul cu pricina nu are
această proprietate. Literele E, T şi F, care formează mulţimea
neterminalelor, sunt mnemonice pentru expresie, termen şi factor.
Există o serie de convenţii folosite la descrierea de gramatici. În
lipsa unei alte specificaţii, terminalele alfabetului Σ vor fi notate cu litere
mici a, b, c, etc., cu numere 0, 1, 2, ..., 9 sau cu simboluri specifice ca +,
*, (, ). Neterminalele sunt notate cu litere mari A, B, C, ..., şi simbolul de
start cu S. Şirul vid se notează întotdeauna cu λ . Prima producţie va avea
simbolul de start pe partea stângă. Elementele mulţimii Σ ∪ N se vor
numi simboluri gramatice; deci un simbol gramatic este sau terminal sau
neterminal. Dacă

A → α1
A → α2
M
A → αk

sunt toate producţiile cu aceeaşi parte stângă A, atunci, pentru a se salva


spaţiu, ele vor fi scrise sub următoare formă

A → α1 | α 2 | L | α k −1 | α k .

Ca urmare, gramatica din exemplul 1.2. se va scrie

S → AaB | B
A → a | aB
B → bC
C → ac | λ

iar gramatica G0 din exemplul 1.3.


E → E +T |T
T →T *F | F
F → (E) | a

Înţelesul simbolurilor reiasă din context. Dacă nu se specifică în


mod explicit, fiind dată descrierea unei gramatici prin

S → AB
A → BSB | BB |1
B → 0 A1| 0 | λ

tragem concluzia că mulţimea de terminale Σ este {0,1} , mulţimea de

neterminale N = {S , A, B} şi producţiile sunt

S → AB
A → BSB
A → BB
A →1
B → 0 A1
B→0
B→λ

Forma această o vom folosi des pentru descrierea gramaticilor.


Derivarea unei propoziţii într-un limbaj generat de o gramatică se
poate reprezentat grafic printr-un arbore de derivare. Ca exemplu
considerăm gramatica G:

S → aBa B → Sb | bCC C → abb

şi o derivare a propoziţiei aababbabbaba:


S ⇒ aBa ⇒ aSba ⇒ aaBaba ⇒ aabCCaba
⇒ aababbCaba ⇒ aababbabbaba

O derivarea reprezentată schematic printr-un graf, se numeşte arbore de


derivare, figura 1.1.

Figura 1.1. Un arbore de derivare a unui şir.

Obiectele rotunde din graf se numesc noduri. Nodul din vârf, S, se


numeşte rădăcina arborelui. Dacă un nod X este legat de un nod de pe un
nivel mai jos, atunci X se numeşte părintele lui Y şi Y se numeşte fiul lui
X. Nodurile care nu au fii se numesc frunze, nodurile cu fiu/fii şi părinte
se numesc noduri interne. Formarea grafului, numit arbore de derivare,
este evidentă. Dacă A este un neterminal, care se înlocuieşte cu partea
dreaptă a producţiei A → X 1 X 2 L X n , atunci nodul A are ca fii X 1 , X 2 ,..., X n
(de la stânga la dreapta). Rădăcina arborelui este nodul S, unde S este
simbolul de start a gramatici iar frunzele sunt noduri de forma x, unde
x ∈ Σ sunt terminale. Dat fiind un arbore de derivare a unui şir, vom

obţine acest şir prin concatenarea frunzelor arborelui de la stânga la


dreapta.
Figura 1.2. Formarea unui arbore de derivarea

Propoziţia (a + a) * a în limbajul L(G2 ) din exemplul 1.3., are arborele de


derivare din figura 1.3.

Figura 1.3. Arborele de derivare pentru ( a + a ) * a

Exemplu 1.4. Ca un alt exemplu, prezentăm o gramatică pentru


limbajul tuturor constante reale valide din Fortran. În manualul de
Fortran, aceste constante sunt descrise în diferite moduri. Printre care „o
constantă reală este reprezentată printr-un şir de numere; conţine un punct
zecimal şi un exponent reprezentând o putere a lui 10 sau amândouă.
Constante reale pot fi în următoarele formate:

n.n n., .n, n.nE ± s, n.E ± s, .nE ± s, nE ± s .


Exemple sunt: 3.E1, 3.1415768, 31.41592E-01, 314.0749162, .31415E01,
-3.141592E+279, .31415E+01”. Un exemplu de gramatică care generează
un astfel de limbaj este:

S → AB
A → XD
B → EXI | λ
D → .I | I . | I .I | I
X →+|−|λ
I → M | MI
M → 0 |1| 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

Terminalele formează mulţimea Σ = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, E , +, −, .} ,

mulţimea de neterminale este N = {S , A, B, D, I , M , X } şi simbolul de start

este S. Arborele de derivare pentru -3.14E01 este dat în figura 1.4.


Evident, că toate constantele Fortran (şi numai acestea) pot fi derivate
folosind această gramatică.
Bineînţeles că aceasta nu este unica gramatică care poate fi folosită
pentru descrierea acestui limbaj; este posibil să avem două gramatici
distincte G1 şi G2 astfel încât L(G1 ) = L(G2 ) . Vom discuta acest concept
mai târziu. De fapt, întregul limbaj Pascal, ca şi oricare alt limbaj de
calculator, poate fi descris prin gramatica lui. Aceste gramatici sunt lungi
şi includ mai multe sute de producţii. Metodă aceasta de descriere a
limbajului de calculator este numită Backus Normal Form sau BNF.
Notaţia folosită aici diferă puţin de notaţia introdusă mai sus. Săgeata →
este reprezentată prin simbolul ::= şi neterminalele sunt scrise ca fraze
auto explicative închise între < L > . De pildă, o descriere BNF a
constantelor întregi din Fortran (sau oricare alt limbaj) poate fi:
< IntegerConstant > ::= < Sign > < Number >
< Sign > ::= + | − | λ
< Number > ::= < Digit > | < Digit > < Number >
< Digit > ::= 0 |1| 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

Evident, că această formă de descriere a gramaticilor (şi a limbajelor), în


afara convenţiilor de notaţie, este identică cu aceea prezentată anterior.

Figura 1.4. Arbore de derivare pentru -3.14E01

1.2. TIPURI DE GRAMATICI ŞI CLASIFICAREA LOR

Ideea descrierii unui limbaj printr-o gramatică aparţine lui N.


Chomsky. În prima parte a activităţii sale interesul lui s-a îndreptat spre
limbaje naturale, ca engleza şi germana. Vom prezenta în continuare
unele definiţii generale de gramatici.

I. Gramatici de tip 0 sau gramatici fără restricţii. O gramatică fără


restricţii G se compune din patru obiecte: o mulţime nevidă de terminale
Σ , o mulţime nevidă, N, de neterminale, cu N ∩ Σ = ∅ , un simbol de start
S ∈ N şi un ansamblu finit de producţii P. Într-o gramatică fără restricţii

producţiile sunt de forma

u→v

unde u şi v sunt şiruri oarecare de terminale sau neterminale; u ≠ λ .


Această producţie permite înlocuirea arbitrară a unui şir (u) cu un şir (v).
Se spune, că o forma propoziţională σ derivă imediat o formă
propoziţională τ dacă σ = α u β , τ = α vβ şi u → v este o producţie a
gramaticii. Vom nota aceasta prin σ ⇒ τ . Limbajul L(G ) este definit a fi
mulţimea şirurilor de terminale derivabil din simbolul de start S.

Exemplu 1.5. Fie Σ = {a, b, c} alfabetul unei gramatici G,

N = {S , X , Y } mulţimea neterminalelor, S simbolul de start şi mulţimea

producţiilor dată de

1. S → aXYc
2. aX → cad
3. Xc → aXa
4. XYc → Xcc
5. Yc → λ

Un exemplu de derivare în această gramatică ar fi

S ⇒ aXYc ⇒ cadYc ⇒ cad

sau

S ⇒ a XYc ⇒ a Xcc ⇒ aaX ac ⇒ acadac .


În formele propoziţionale de mai sus am subliniat partea stângă a
producţiilor folosite. Gramaticile de acest tip sunt foarte generale; orice
limbaj (mulţime de şiruri) poate fi descris de o gramatică de tip 0. Un
limbaj generat de o gramatică de tip 0 se numeşte limbaj de tip 0.

II. Gramatici de tip 1, sau gramatici dependente de context.


Terminalele, neterminalele şi simbolul de start sunt aceleaşi. Fiecare
producţie va avea acum forma

α Aβ → ασβ

unde A este un neterminal şi σ ≠ λ este un şir oarecare de terminale sau


neterminale. Neterminalul A se înlocuieşte cu σ doar dacă A este
înconjurat, sau în context, de α şi β . Evident că gramaticile dependente
de context sunt şi de tip 0, dar reciproc nu.

III. Gramatici de tip 2 sau gramatici independente de context. Dacă


toate producţiile unei gramatici sunt de forma A → σ , unde A este un
singur neterminal şi σ un şir oarecare de terminale sau neterminale,
atunci gramatica se numeşte gramatică independentă de context, notată
GIC. Gramatici de tip 2 sunt o restricţie a gramaticilor de tip 1: este
necesar ca α = β = λ . Astfel, dată fiind forma propoziţională γ 1 Aγ 2 şi o
producţie A → σ , înlocuim A cu σ pentru a deriva o formă γ 1σγ 2
indiferent de contextul γ 1 şi γ 2 . Acest fapt explică denumirea de
„independent de context”. Obţinem exact definiţia unei gramatici dată de
definiţia 1.4. Strict văzut, gramatici de tip 2 nu sunt chiar restricţii ale
gramaticilor de tip1, deoarece sunt permise producţii de forma A → λ în
una dar nu şi în cealaltă. Acesta ne conduce la un caz special. Vom arăta
într-un capitolul 7, că dată fiind o gramatică independentă de context cu
producţii vide, există o gramatică independentă de context echivalentă
(generând acelaşi limbaj) fără producţii vide, exceptând S → λ (unde S
este simbolul de strat). În particular, dacă G este o gramatică
independentă de context astfel încât λ ∉ L(G ) – limbajul generat de G –
atunci L(G ) = L(G ') , unde G ' este o gramatică independentă de context
fără producţii vide. Astfel, dacă L este un limbaj de tip2 care nu-l conţine
pe λ , atunci este şi de tip 1. Reciproca nu este adevărată: există limbaje
de tip1 care n-au gramatici independente de context. Majoritatea
gramaticilor discutate în această lucrare vor fi independente de context –
ele sunt mult mai uşor de analizat. Toate limbajele actuale de programare,
Pascal, C, etc., sunt independente de context.

IV. Gramatici de tip 3 sau gramatici regulare. Există o subclasă


interesantă de gramatici independente de context, numite gramatici
regulare sau liniare.

Definiţia 1.9. Spunem că o gramatică G este liniară la dreapta sau


regulară la dreapta, dacă fiecare din producţiile sale are una din formele
următoare:

S →λ A → aB A→a.

Unde, S este simbolul de start, λ şirul vid, A, B neterminale arbitrare şi a


un terminal oarecare. O gramatică se numeşte liniară la stânga sau
regulară la stânga dacă toate producţiile sale au una din formele
următoare:
S →λ A → Ba A→a

O gramatică se numeşte regulară (liniară) dacă este regulă (liniară) la


dreaptă sau la stânga.

Evident, gramaticile regulare sunt independente de context. Le vom


studia detaliat în capitolul 2. Această clasificarea a gramaticilor (tip j, j=0,
1, 2, 3) este cunoscută sub denumirea de ierarhie Chomsky.

1.3. REPREZENTAREA LIMBAJELOR PRIN GRAMATICI

Am văzut în paragrafele 1.2. şi 1.3. cum se descrie un limbaj în


termeni de gramatici. Din punct de vedere a ştiinţei calculatoarelor
următoarele două probleme „inverse” sunt mai importante (şi dificile).
I. Fiind dat un şir σ de terminale, aparţine el limbajului?
II. Fiind dat un şir σ dintr-un limbaj, cum a fost derivat?
Importanţa problemei I este evidentă – fiind dat un program scris într-un
limbaj oarecare dorim să determinăm dacă este scris corect. Nu punem
întrebarea dacă programul face ceea ce ar trebui să facă, ci doar dacă este
valid sau nu. Ceea ce dorim să aflăm la acest punct este dacă programul
este „gramatical corect”. Sunt toate instrucţiunile în forma potrivită? Sunt
toate ciclurile DO închise? Se termină toate BEGIN-urle cu END-ul
corespunzător? ş.a.m.d. Examinarea acestui tip de întrebare este
cunoscută sub denumirea de analiză lexicală. Problema II este chiar mai
importantă, ea permiţându-ne să interpretăm „înţelesul” programului şi să
decidem cum va fi executat. Studierea acestei a doua probleme se
numeşte analiză sintactică (eng. parsing). Vom ilustra aceste două puncte
de vedere în următorul exemplu.
Exemplu 1.6. Fie G0 gramatica tuturor expresiilor aritmetice
corecte scrise formate din întregi pozitivi şi simbolurile +, *, (, şi ).
Această gramatică are producţiile

1. E → E + T 3. T → T * F 5. F → ( E )
2. E → T 4. T → F 6. F → a

Simbolul a reprezintă un şir oarecare de întregi pozitivi. Fiind dat o


propoziţie în limbajul L(G0 ) , (5 + 3) * 4 , avem să decidem:

I. Este o propoziţie validă?


II. Ce înseamnă propoziţia?

În II este implicată interpretarea sau evaluarea şirului (5 + 3) * 4 .


Când acest şir va fi introdus, maşina ar trebui să ştie să ia numerele 5 şi
3, să le încarce în memorie sau în registre, să execute adunarea, să ia
rezultatul adunării şi să-l încarce în locaţia potrivită, iar apoi să încarce
numărul 4 şi să execute înmulţirea finală. Toate acestea se producă
complet automat; maşina fiind programată astfel încât, când primeşte
şirul (5 + 3) * 4 , toate operaţiile anterioare să se realizează în ordinea
corectă. Vom arăta în continuare că dacă maşina este capabilă să
reproducă arborele de derivare a propoziţiei, ea poate intui cum să
realizeze cele de mai sus, de exemplu, poate descifra „înţelesul” sau
valoare expresiei.
Figura 1.5. Arbore de derivare pentru (5 + 3) * 4 .

Într-adevăr, presupunând că avem arborele de derivare pentru


(5 + 3) * 4 , ca în figura 1.5., a verifica dacă şirul aparţine limbajului este

simplu: concatenăm frunzele arborelui de la stânga la dreapta şi verificăm


dacă şirul obţinut se potriveşte, simbol cu simbol, celui iniţial. Nu vom
discuta cum se memorează un astfel de arbore, cum se accesează
nodurile, etc. Acestea ţin de implementare şi, în particular, pot depinde de
aspecte hardware ale maşinii. Pretindem că, dat fiind acest arbore de
derivare putem determina înţelesul şirului.
Vom rescrie arborele într-un mod diferit. Fiecare nod va fi
reprezentat printr-o celulă X | n : Simbolul X va fi simbolul gramatic al

nodului, (terminal, neterminal) iar n specifică numărul producţiei folosite


în obţinerea fiilor nodului.
E 2

T 3

T 4 * 0 F 6

F 5 4 0

( 0 E 1 ) 0

E 2 + 0 T 4

T 4 F 6

F 6 3 0

5 0

Figura 1.6. Derivarea lui (5 + 3) * 4 .

Dacă X este un terminal, de exemplu când nodul este frunză, punem n = 0 .


Arborele de derivare pentru (5 + 3) * 4 va arăta atunci ca în figura 1.6. Se

defineşte o funcţie V ( X | n ) = V ( X , n ) pe noduri în felul următor:

 X if n = 0
V ( LeftChild ( X , n)) + V ( RightChild ( X , n)) if n = 1

V ( X , n) = V (Child ( X , n) if n = 2, 4, sau 6
V ( LeftChild ( X , n)) *V ( RightChild ( X , n)) if n = 3

V ( MiddleChild ( X , n) if n = 5
Funcţia V ( X , n) este deci definită pentru acei X şi n pentru care X | n

poate fi nod într-un arbore de derivare a unei expresii aritmetice.


Definiţiile funcţiilor LeftChild, RightChild, MiddleChild şi Child se
subînţeleg. Dat fiind arborele de derivare a unei expresii, pentru aflarea
înţelesului sau valorii acesteia, se evaluează V(Root). În exemplul
discutat, vom avea:

V ( Root ) = V ( E , 2)
= V (T ,3)
= V (T , 4) *V ( F , 6)
= V ( F ,5) *V (4, 0)
= V ( E ,1) * 4
= (V ( E , 2) + V (T , 4)) * 4
= (V (T , 4) + V ( F , 6)) * 4
= (V ( F , 6) + V (3, 0)) * 4
= (V (5, 0) + 3) * 4
= (V (5, 0) + 3) * 4
= (5 + 3) * 4 = 32

Evident, funcţia V va calcula corect valoarea oricărei expresii aritmetice


valide, dacă cunoaşte arborele de derivare a expresiei. Funcţia V este
recursivă, ea se va autoapela la evaluarea unor argumente. S-ar putea ca
să nu fie clar cititorului cum se implementează, în mod complet automat,
pe calculator evaluarea unei astfel de funcţii. Acest lucru, însă, poate fi
realizat, iar implementarea specifică va depinde, la rândul ei, de limbajul
şi de hardware-ul folosi. Nu intrăm în amănuntele acestui subiect. Mai
degrabă vom studia procesului de reconstruire a arborelui de derivare
pentru un şir dat. Motivul introducerii funcţiei V a fost, de a convinge
cititorul că procesul de reconstruire a arborelui de derivare este într-
adevăr folositor şi chiar necesar.
Următoarea problemă se ridică imediat ce discutăm de parsing (de
exemplu despre reconstruirea arborelui de derivare): Fiind dată o
gramatică G şi un şir σ din limbajul L(G ) , atunci poate avea σ doi arbori
distincţi de derivare? Se constată, desigur, că astfel de fenomene exista.

Exemplu 1.7. Fie G gramatica

S→A A → A0 A |1

Figura 1.7. arată doi arbori de derivare distincţi pentru şirul σ = 10101 .

Figura 1.7. Arbori de derivare distincţi pentru şirul σ = 10101 .

Aceasta ne conduce la următorul concept:

Definiţia 1.10. O gramatică G se numeşte ambiguă dacă există o


propoziţie σ în L(G ) cu doi arbori de derivare distincţi. Dacă o gramatică
nu este ambiguă, ea se numeşte neambiguă.

Exemplu 1.8. Următorul este un exemplu concret relativ la


construcţia „ if C1 then C2 else C3 ” din Pascal sau din alt limbaj de
programare. Un fragment a unei gramatici pentru Pascal care foloseşte
această construcţie poate fi dat în felul următor:

< statement > → if < condition > then < statement > |
if < condition > then < statement > else < statement > | (1.3)
< other kind of statement >

Dacă aceste construcţii sunt descrise de gramatica de mai sus, atunci


arborele de derivare a unor propoziţii nu va fi unic. Considerăm
următoarea propoziţie:

if C = 0 then if D = 1 then A := 3 else A := 4 (1.4)

Ea are doi arbori de derivare distincţi, ca în figura 1.8. „Înţelesul” sau


„valoarea” lui σ , diferă pentru fiecare dintre cei doi arbori. Presupunând
că în timpul execuţiei, variabila C are valoarea 1, D=1, A=5 şi se
întâlneşte propoziţia σ . Dacă se admite ca arborele de derivare să fie dat
de figura 1.8.(a), valoarea A = 5 rămâne. Dacă, pe de altă parte, se
consideră arborele din figura 1.8.(b) ca fiind cel corect, valoarea lui A
devine 4. Astfel, la compilarea programelor derivate de la o gramatică
ambiguă, trebuie luată o decizie arbitrară şi anume, care dintre arborii de
derivare se va folosi, adică, ce înţeles se atribuie propoziţiei. În acest
exemplu, este preluat înţelesul dat de arborele din figura 1.8.(a). Regula
arbitrar aleasă, însă fixată, este: „Fiecare else se asociază cu cel mai
apropiat then precedent”.
(a) <statement>

if C=0 then <statement>

if D=1 then A := 3 else A := 4

(b) <statement>

if C=0 then <statement> else A := 4

if D=1 then A := 3

Figura 1.8. Ambiguitate pentru if...then...else.

Observăm că, gramaticile neambigue sunt preferabile celor ambigue. În


general, fiind dată o gramatică G, nu există un algoritm pentru a
determina dacă ea este ambigua sau nu. Unele gramatici speciale permit
însă acest lucru – în capitolul 8 vom arăta că gramatica expresiilor
aritmetice, G0 , este neambiguă.
Se poate întâmpla, ca pentru o gramatică dată G, care generează un limbaj
L(G ) , să existe o altă gramatică diferită care generează acelaşi limbaj,

adică, L(G ') = L(G ) . Următorul exemplu arată acest fenomen.

Exemplu 1.9. Fie gramatica G

S → 0 A0
A→ S |λ
şi gramatica G '

S → 0B
B → 0S | 0

Este evident că ambele gramatici generează acelaşi limbaj L – mulţimea


tuturor şirurilor având un număr par de zerouri. Gramaticile la rândul lor,
sunt total diferite: G ' este o gramatică regulară fără producţii vide, iar G
nu are nici una din aceste proprietăţi.
Se poate da o gramatică ambiguă G şi să existe o altă gramatică G '
astfel încât L(G ) = L(G ') . Rescriem gramatica pentru if...then...else din
exemplul 1.8. în felul următor:

< statement > → S1 | S 2 < other kind of sentence >


S1 → if < condition > then S1 |
if < condition > then S2 else S1 |
(1.5)
< other kind of sentence >
S2 → if < condition > then S 2 else S 2 |
< other kind of sentence >

Această gramatică generează aceeaşi propoziţie ca gramatica din


exemplul 1.8., dar ambiguitatea a fost înlăturată. Aşadar, dacă propoziţia

if C = 0 then if D = 1 then A := 3 else A := 4

este analizată sintactic, înţelesul ei se interpretează ca

if C = 0 then (if D = 1 then A := 3 else A := 4)

opus lui
if C = 0 then (if D = 1 then A := 3) else A := 4

Definiţia 1.11. Un limbaj se numeşte ambiguu în mod inerent dacă


orice gramatică care generează L este ambiguă.

Un exemplu de astfel de limbaj este

L = {a k b k c m d n | k , l , m şi n ≥ 1, cu (k = 1 şi m = n) sau (k = n şi l = m)} .

Şiruri din L sunt aabbcccddd (k = l = 2, m = n = 3) şi aabbbcccdd


(k = n = 2, l = m = 3) . A demonstra că acesta este un limbaj ambiguu în mod

inerent, nu face parte din scopul acestei lucrări.

1.4. O INTRODUCERE ÎN PARSING (ANALIZA SINTACTICĂ)

Considerăm problema reconstruirii arborelui de derivare pentru o


propoziţie dată, adică, problema analizei sintactice. Întreaga lucrare se va
concentra asupra acestei probleme, actualul paragraf fiind doar partea de
introducere. Am putea crede că pentru specificarea arborelui de derivare
este de ajuns folosirea secvenţei de producţii de la derivare. Urmând
această idee, în lipsa unor restricţii, vor apărea dificultăţi. În primul rând,
două secvenţe diferite de producţii pot conduce la acelaşi arbore. De
exemplu, fie gramatica expresiilor aritmetice G0

1. E → E + T 3. T → T * F 5. F → ( E )
2. E → T 4. T → F 6. F → a
şi şirul a + a din acest limbaj. Secvenţele 1, 2, 4, 6, 4, 6 şi 1, 4, 6, 2, 4, 6
sunt diferite şi corespund la două derivări pentru a + a .

1 2 4 6 4 6
E ⇒ E + T ⇒T + T ⇒T + F ⇒T + a ⇒ F + a ⇒ a + a

şi

1 4 6 2 4 6
E ⇒ E + T ⇒ E + F ⇒ E + a ⇒T + a ⇒ F + a ⇒ a + a

Ambele derivări generează acelaşi arbore din figura 1.9.

Figura 1.9. Arbor de derivare pentru a + a .

Poate avea loc şi situaţia inversă: dacă nu specificăm la fiecare pas care
neterminal este înlocuit, aceeaşi secvenţă de producţii poate conduce la
arbori de derivare distincţi. Pentru a vedea asta considerăm gramatica

1. S → A
2. A → A0 A
3. A → 1

Secvenţa de producţii 1, 2, 2, 3, 3, 3 conduce la două derivări distincte:


1 2 2 3 3 3
S ⇒ A ⇒ A0 A ⇒ A0 A0 A ⇒ 10 A0 A ⇒1010 A ⇒ 10101

şi

1 2 2 3 3 3
S ⇒ A ⇒ A0 A ⇒ A0 A0 A ⇒ A0 A01⇒ A0101⇒ 10101

(În fiecare formă propoziţională este subliniat neterminalul care va fi


înlocuit.) Arborii de derivare a acelor două derivări, apar în figura 1.10.,
ei sunt diferiţi.

Figura 1.10. Gramatici ambigue.

Aşadar, pentru a specifica arborele de derivare prin secvenţele de


producţii folosite, este totodată necesară şi specificarea, la fiecare pas, a
neterminalului înlocuit. Aceasta este greu de realizat şi se observă că nu
este necesar deoarece putem specifica dinainte care neterminal va fi ales
pentru înlocuire.
Definiţia 1.12. Fie G o gramatică, σ o propoziţie în limbajul L(G )
şi

S ⇒ σ 1 ⇒ σ 2 ⇒ ... ⇒ σ n = σ (1.6)

derivarea lui σ . Spunem că (1.6) este derivarea la stânga a lui σ , dacă la


fiecare pas σ i ⇒ σ i +1 , neterminalul înlocuit din σ i , este cel mai din stânga.
Spunem că (1.6) este derivarea la dreapta dacă la fiecare pas înlocuim
neterminalul cel mai din dreapta.

De exemplu, derivarea la stânga a lui (a + a) * a în G0 (gramatica


expresiilor aritmetice din exemplul 1.3.) este

1 3 4 5 1 2
E ⇒ T ⇒ T * F ⇒ F * F ⇒( E ) * F ⇒ ( E + T ) * F ⇒
4 6 4 6
(T + T ) + F ⇒( F + T ) * F ⇒(a + T ) * F ⇒(a + F ) * F ⇒
6
( a + a ) * F ⇒( a + a ) * a

Derivarea la dreapta a aceleiaşi propoziţii este atunci

2 3 6 4 5 1
E ⇒ T ⇒ T * F ⇒ T * a ⇒ F * a ⇒( E ) * a ⇒
4 4 6 4
( E + T ) * a ⇒( E + F ) * a ⇒( E + a ) * a ⇒(T + a) * a ⇒
1
( F + a ) * a ⇒(a + a ) * a

Astfel, dacă ştim de dinainte ce fel de derivare se foloseşte (la stânga sau
la dreapta), secvenţa de producţii din derivarea unei propoziţii va descrie
complet arborul de derivare.
Definiţia 1.13. O analiză la stânga a unui şir σ este secvenţa de
producţii folosită în derivare la stânga a lui σ . O analiză la dreapta a lui
σ este inversa secvenţei de producţii folosite în derivarea la stânga a lui

σ.

Motivul folosirii „inversei” din definiţia analizei la dreapta va fi, pe


scurt, discutată în această parte şi detaliat în capitolul 8. Astfel, analiza la
stânga pentru (a + a) * a este: 2, 3, 4, 5, 1, 2, 4, 6, 4, 6, 6. Analiza la
dreapta a aceluiaşi şir va fi: 6, 4, 2, 6, 4, 1, 5, 4, 6, 3, 2. Putem vorbi de o
analiză la stânga şi la dreapta a oricărei forme propoziţionale derivate din
simbolul de start S. De exemplu, analiza la stânga pentru a *( E ) în
gramatica G0 este 2, 3, 4, 6, 5, deoarece

2 3 4 6 5
E ⇒ T ⇒ T * F ⇒ F * F ⇒ a * F ⇒ a *( E )

Toate acestea nu ne dau o metode de determinare a analizei (la


stânga sau la dreapta) a unui şir. Fiind dat un şir de terminale σ = a1a2 ...an ,
singurul lucru pe care-l ştim despre arborele lui de derivare este, că are
simbolul S în vârf iar frunzele din partea de jos formează şirul a1a2 ...an :

Figura 1.11. Arborul de derivare pentru a1a2 ...an .


Există două posibilităţi de a popula interiorul arborelui: una este de a-l
reconstrui de sus în jos (eng. top-down) iar cealaltă de jos în sus (eng.
bottom-up).
În schema de analiză top-down, pornim cu neterminalul S şi
încercăm să descoperim prima producţie S → σ 1 care garantează o
derivarea a lui a1a2 ...an de forma

S ⇒ σ 1 L ⇒ a1a2 ...an .

Odată ce a fost determinată, avem de examinat şirul σ 1 şi de descoperit


care din neterminalele sale, fie acesta A, trebuie înlocuit cu partea dreaptă
a producţiei A → α , producând astfel forma propoziţională σ 2 în
derivarea

S ⇒ σ 1 ⇒ σ 2 L ⇒ a1a2 ...an .

Procedura se repetă până când întreaga derivare este reconstruită. Dacă ne


limităm la derivarea la stânga, de exemplu, la fiecare pas vrem să aflăm
neterminalul cel mai din stânga care va fi înlocuit, dintr-o formă
propoziţională σ , există o modalitate foarte bună pentru efectuarea
acestei alegeri avansate. Ea va fi prezentată în capitolul 6.
În schema de analiză bottom-up, procesul este invers: Începând cu
şirul a1a2 ...an , încercăm să găsim o formă propoziţională σ k astfel încât
derivarea lui a1a2 ...an să aibă forma

S ⇒ L ⇒ σ k ⇒ a1a2 ...an .
Pentru aceasta, trebuie să localizăm un subşir α din a1a2 ...an care
corespunde părţii drepte a unei producţii A → α şi să înlocuim acest α cu
A. (Un astfel de subşir α se numeşte referinţă (eng. handle)). În
continuare repetăm procedura cu şirul σ k în loc de a1a2 ...an . Continuăm
până când ajungem la S. Nu reiese limpede cum se obţine aceasta, dar
este posibil, dacă ne limităm la construirea derivării la stânga. Vom
explica acest proces în capitolul 8. Aşa se explica de ce analiza la dreapta
a fost definit ca fiind inversul secvenţei de producţii folosite în derivarea
la stânga a şirului: În analiza bottom-up determinăm mai întâi ultima
producţie folosită în derivare, iar apoi penultima ş.a.m.d.

PROBLEME

1. Fie G0 limbajul expresiilor algebrice din exemplul 1.3. Daţi


arborele de derivare, analiza la stânga şi la dreapta a următoarelor
şiruri:

a) a + a + a b) (a + a) * a + a *(a + a) c) a * a * a + a * a

Daţi în problemele 2 – 12 o gramatică independentă de context care


descrie limbajul dat şi un arbore de derivare pentru şirul σ 0 indicat.

2. Toate şirurile σ ∈ {0,1}* având un subşir 001; σ 0 = 01001110 .


3. Toate şirurile σ ∈ {0,1}* de lungime impară; σ 0 = 0011011 .
4. L = {a m+ nb m d n | m ≥ 0, n ≥ 0} σ 0 = aaaabccc .
5. L = {a mb n |1 ≤ n ≤ m ≤ 2n} ; σ 0 = aaabb .
6. L = {a mb n | 2 ≤ 2n ≤ m ≤ 3n} ; σ 0 = aaaabb .
7. Mulţimea tuturor şirurilor peste Σ = {0, 1} de lungime impară;
σ 0 = 0010111 .

8. Mulţimea tuturor şirurilor peste Σ = {a, b} în care numărul de a-uri


este egal cu numărul de b-uri; σ 0 = abbaaabb .
9. Mulţimea tuturor şirurilor peste Σ = {a, b} în care este un a mai mult
decât b-uri; σ 0 = aabbbaa .
10. Mulţimea şirurilor peste Σ = {0, 1} în care 1 apare cep puţin de 3 ori;
σ 0 = 001101 .

11. Mulţimea şirurilor peste Σ = {a, b} în care primul simbol este diferit
de ultimul simbol; σ 0 = aababab .
12. Mulţimea tuturor datelor valide pentru anul 1900 până 1999 dat sub
forma NOIEMBRIE 30, 1941. Observăm că anul 1900 nu a fost un
an bisect deci nu a existat un FEBRUARIE 29 în 1900;
σ 0 = OCTOMBRIE 30, 1941 .

În problemele 13 – 15, descrieţi limbajul generat de gramatica dată.

13. S → aaSb | A , A → cAdd | cd


14. S → ASBB | AB , A → aA | b , B → Bc | d
15. S → aSb | aSbb | aSbbb | ab

Fiecare gramatică din problemele 16 – 21 este ambiguă. Găsiţi pentru


aceste gramatici o propoziţie cu doi arbori distincţi de derivare.

16. S → SaSa | b
17. S → aSb | Sb | Sa | a
18. S → aaS | aaaS | a
19. S → aS | aSb | A , A → Aa | a
20. S → AA , A → AAA | a | bA | Ab
21. S → AaA , A → aA | bA | λ
22. Arătaţi că fiecare gramatică stâng liniară are şi o gramatică drept
liniară, şi reciproc.
23. Un fragment al gramatici corespunzătoare construcţiilor
if...then...else, dat în (1.3), a fost ambiguu, propoziţia (1.4) având,
ca în figura 1.8., doi arbori distincţi de derivare. Acest fragment a
fost rescris în (1.5) astfel încât propoziţia (1.4) să aibă arborele de
derivare dat de figura 1.8.(a). Rescrieţi gramatica din (1.3) astfel
încât propoziţia (1.4) să aibă un unic arbore de derivare dat de
figura 1.8.(b).