Académique Documents
Professionnel Documents
Culture Documents
În dezvoltarea translatoarelor, sunt implicate cel puţin trei limbaje: limbajul sursă de
translatat, limbajul obiect sau destinaţie şi limbajul gazdă folosit la implementarea translatorului.
Dacă translatarea are loc în mai multe etape, pot exista şi alte limbaje intermediare. Desigur,
limbajul gazdă şi limbajul obiect nu sunt cunoscute de utilizatorul limbajului sursă.
2.1. Diagrame T
Pentru descrierea programelor şi în particular a compilatoarelor, există o reprezentare
schematică consacrată, numită diagramă T, introdusă de Bratman în 1961.
Numele programului
Date de intrare Date de ieşire
Limbajul de
implementare
1
O diagramă T pentru un translator general este de forma:
Numele translatorului
Limbaj sursă Limbaj destinaţie
Limbajul gazdă de
implementare a
translatorului
Cele mai multe compilatoare nu produc cod maşină cu adrese fixe, ci o formă cunoscută
sub numele de "semicompilat", "simbolic binar" sau formă relocatabilă. Rutinele astfel
2
compilate sunt legate cu ajutorul unor programe numite editoare de legături, linker, care pot
fi privite ca ultima etapă în procesul de translatare. Limbajele care permit compilarea
separată a părţilor unui program depind esenţial de existenţa acestor editoare de legături.
Diagramele T pot fi combinate pentru a arăta interdependenţa translatoarelor, editoarelor
de legături etc.
LINK.EXE
Bibliotecă de Cod relocabil L2 PROG.EXE
programe Limbajul gazdă de
implementare a
editorului de legături
Observaţie. Un compilator nu necesită un limbaj ţintă (de asamblare sau limbaj maşină)
real. De exemplu, compilatoarele Java generează cod pentru o maşină virtuală numită "Java
Virtual Machine" (JVM). Interpretorul JVM interpretează apoi instrucţiunile JVM fără nici o
translatare ulterioară.
- Secţiunea de gestionare caractere este cea care comunică cu lumea exterioară, prin sistemul
de operare, pentru citirea caracterelor care formează textul sursă. Cum setul de caractere şi
gestiunea fişierelor variază de la sistem la sistem, această fază este de obicei dependentă de
maşină sau de sistem de operare.
- Analizorul lexical (Scanner) preia textul sursă sub forma unei secvenţe de caractere şi le
grupează în entităţi numite atomi (tokens). Aceştia sunt simboluri ca identificatori, şiruri,
constante numerice, cuvinte cheie cum ar fi while şi if, operatori ca <= etc. Atomilor li se
atribuie coduri lexicale, astfel că, la ieşirea acestei faze, programul sursă apare ca o secvenţă
de asemenea coduri.
3
Cod sursă
Gestiune caractere
Analizor lexical
(Scanner)
Fază analitică
(Front end)
Analizor sintactic
(Parser)
Generator de cod
intermediar
Generator de cod
final
Cod obiect
- Analizorul sintactic (Parser) are ca scop gruparea atomilor rezultaţi în urma analizei lexicale
în structuri sintactice. O structură sintactică poate fi văzută ca un arbore ale cărui noduri
terminale reprezintă atomi, în timp ce nodurile interioare reprezintă şiruri de atomi care
formează o entitate logică. Exemple de structuri sintactice: expresii, instrucţiuni, declaraţii etc.
- Pe durata analizei sintactice, de obicei are loc şi o analiză semantică, adică efectuarea unor
verificări legate de compatibilitatea tipurilor datelor cu operaţiile în care ele sunt implicate, de
respectarea regulilor de vizibilitate impuse de limbajul sursă.
- Generatorul de cod intermediar este o fază sintetică care, în practică, poate fi integrată în faze
anterioare ori poate fi omisă în cazul translatoarelor foarte simple. În această fază are loc
transformarea arborelui sintactic într-o secvenţă de instrucţiuni simple, similare
macroinstrucţiunilor unui limbaj de asamblare. Diferenţa dintre codul intermediar şi un limbaj
de asamblare este în principal aceea că, în codul intermediar nu se specifică registrele utilizate
în operaţii. Exemple de reprezentări pentru codul intermediar: notaţia postfix, instrucţiunile cu
trei adrese etc. Codul intermediar prezintă avantajul de a fi mai uşor de optimizat decât codul
maşină.
4
- Optimizatorul de cod este o fază opţională cu rolul de a modifica porţiuni din codul
intermediar generat, astfel încât programul rezultat să satisfacă anumite criterii de performanţă
vizând timpul de execuţie şi/sau spaţiul de memorie ocupat.
- Generatorul de cod final este faza cea mai importantă din secţiunea "back end" . În această
fază se preia ieşirea de la faza precedentă şi se generează codul obiect, prin decizii privind
locaţiile de memorie pentru date, generarea de coduri de acces pentru aceste date, selectarea
registrelor pentru calcule intermediare şi indexare etc. Astfel, instrucţiunile din codul intermediar
(eventual optimizat) sunt transformate în instrucţiuni maşină (sau de asamblare).
- Unele translatoare continuă cu o fază numită "peephole optimizer" în care se fac încercări de
reducere a unor operaţii inutile prin examinarea în detaliu a unor secvenţe scurte din codul
generat.
- Un translator foloseşte inevitabil o structură de date complexe, numită tabela de simboluri.
Această tabelă memorează informaţii despre simbolurile folosite în programul sursă şi asociază
proprietăţi acestor simboluri (tipul lor, spaţiul de memorie necesar pentru variabile sau valoarea
lor pentru constante). Compilatorul face referire la această tabelă aproape în toate fazele
compilării.
- Tratarea erorilor. Un compilator trebuie să fie capabil să recunoască anumite categorii de erori
care apar în programul sursă. Tratarea unei erori presupune detectarea ei, emiterea unui mesaj
corespunzător şi revenirea din eroare, adică, pe cât posibil, continuarea procesului de compilare
până la epuizarea textului sursă, astfel încât numărul de compilări necesare eliminării tuturor
erorilor dintr-un program să fie cât mai mic. Practic, există erori specifice fiecărei faze de
compilare.
5
Compilatoarele incrementale îmbină calităţile compilatoarelor cu cele ale interpretoarelor.
Programul sursă este divizat de compilator în mici porţiuni numite incremente care prezintă o
oarecare independenţă sintactică şi semantică faţă de restul programului. Compilatorul produce
un cod incremental care este suficient de simplu pentru a satisface restricţiile impuse de un
interpretor. Interpretorul "execută" algoritmul original prin simularea unei maşini virtuale pentru
care codul intermediar numit şi pseudocod este efectiv codul maşină.
Distincţia dintre codul maşină şi pseudocod este ilustrată în figura următoare:
(încărcat) (încărcat)
Interpretor Execuţie
Dacă se parcurge numai etapa 1 de compilare, pseudocodul este intrare în interpretor. Dacă
se parcurge şi etapa 2, rezultatul este un program obiect cu instrucţiuni în cod maşină care poate fi
lansat în execuţie independent.
Desigur, orice maşină reală poate fi văzută ca un interpretor specializat care preia din
programul sursă instrucţiunile una câte una, le analizează şi le "execută" una câte una. Într-o maşină
reală această execuţie este realizată prin hardware, deci mult mai rapid. Se poate conclude că se
pot scrie programe care permit unei maşini reale să emuleze orice altă maşină reală, cu dezavantajul
vitezei reduse. Aceste programe sunt numite emulatoare şi sunt uzual folosite în proiectarea de noi
maşini şi a software-ului care va rula pe acestea.
Una din cele mai cunoscute aplicaţii de compilator incremental portabil este "Pascal–P"
(Zurich 1981) care constă din 3 componente:
- Un compilator Pascal, scris într-un subset foarte complet al limbajului, numit Pascal-P. Scopul
acestui compilator este translatarea programelor sursă Pascal-P într-un limbaj intermediar foarte
bine definit şi documentat, numit P-code, care este "codul maşină" pentru un calculator ipotetic
bazat pe stivă, calculator numit P-machine.
- O versiune compilată a primului compilator, astfel încât primul obiectiv al compilatorului este
compilarea lui însuşi.
- Un interpretor pentru limbajul P-code scris în Pascal. Interpretorul a servit în principal ca model
pentru scrierea unor programe similare pentru alte maşini, în scopul emulării unei maşini
ipotetice P-machine.
3. Analiza lexicală (scanner) C2
Este prima fază a procesului de compilare şi are rolul de a transforma programul sursă, văzut ca
un şir de caractere într-un şir de simboluri numite atomi lexicali (tokens).
Mulţimea tuturori atomilori lexicali detectabili în programul sursă se împarte în clase de atomi:
- clasa identificatorilor
- clasa constantelor întregi (numerelor întregi)
- clasa numerelor reale
- clasa operatorilor
- clasa cuvintelor cheie.
În urma analizei lexicale, fiecare atom lexical identificat primeşte o codificare internă, iar
programul sursă se transformă într-un şir de coduri aranjate în ordinea detectării atomilor.
Deşi rolul principal al analizei lexicale este detectarea atomilor lexicali, putem vorbi de operaţii
conexe analizei lexicale, cum sunt: eliminarea spaţiilor şi comentariilor, numărarea liniilor sursă (pentru
raportarea de erori) etc.
3.1. Descriere
3.1.1. Analiza lexicală ca etapă specifică a compilării
6
Analiza lexicală este o interfaţă între programul sursă şi analizorul sintactic (parser).
Rolul analizorului lexical este asemănător cu cel al analizorului sintactic:
- identificarea conform anumitor reguli a unităţilor distincte în cadrul programului
- semnalarea de erori în cazul abaterii de la aceste reguli
- codificarea unităţilor identificate etc.
Funcţiile analizorului lexical ar putea fi preluate de analizorul sintactic.
Cu toate acestea, în majoritatea cazurilor, se preferă separarea celor două activităţi în faze distincte
din următoarele motive:
a) analizorul lexical este mare consumator de timp deoarece necesită preluarea înregistrare cu
înregistrare a textului de pe suportul extern, acesul la fiecare caracter, comparaţii ale atomilor lexicali cu
mulţimi de caractere cunoscute în vederea clasificării, căutari în tabele etc. De aceea, pentru a obţine un
analizor lexical mai eficient se recomandă implementarea analizorului în limbaj de asamblare, spre
deosebire de celelalte faze în care implementarea se face în limbaje de nivel înalt.
b) textul rezultat în urma analizei lexicale, deci cel primit de analizorul sintactic este mai simplu,
adică sunt eliminate spaţiile şi comentariile, numărul atomilor lexicali este mult mai mic decât numărul
caracterelor din textul sursă.
Analizorul lexical preia astfel sarcina analizei unor construcţii dificile care ar complica şi mai mult
procesul de analiză sintactică.
c) sintaxa atomilor lexicali este mai simplă decât a construcţiilor gramaticale de limbaj, se poate
exprima prin gramatici regulate şi se poate analiza cu ajutorul automatelor finite, existând tehnici mai
simple decât pentru analiza sintactică;
d) prin separarea fazelor, compilatorul poate fi scris modular, deci realizat în echipă;
e) separarea creşte portabiliatatea compilatorului în sensul că pentru o versiune nouă a limbajului
va fi necesar să facem modificări doar la analiza lexicală, nu şi la analiza sintactică.
• clasei cu un număr nedeterminat de elemente (posibil infinit) i se asociază un unic cod intern;
distincţia dintre atomii ce aparţin unei asemenea clase se face prin suplimentarea codului
clasei cu alte informaţii. Astfel, la codul intern se adaugă adresa din tabela de simboluri, în
timp ce în tabela de simboluri se memorează un identificator.
7
De exemplu:
- un simbol a va primi ca şi codificare codul clasei simbolurilor şi o adresă care pointează spre
tabela de simboluri
-o constantă va primi ca şi codificare codul clasei constantelor şi valoarea constantei.
identificator valoare
Informaţiile suplimentare ataşate codului clasei atomului lexical se numesc atribute. În majoritatea
cazurilor, ca şi în cel de sus, este suficient un singur atribut: valoarea constantei (numere întregi, reale) sau
valoarea adresei din tabela de simboluri.
Avantajul codificării atomilor lexicali constă în preluarea unitară de către analizorul sintactic a
datelor furnizate de analizorul lexical, în sensul că analizorul sintactic nu va prelua atomii lexicali (şiruri de
caractere de lungime variabilă), ci numere, codificări ale atomilor.
#ifndef LEX_H
#define LEX_H
typedef enum { NAME, NUMBER, LBRACE, RBRACE, LPAREN, RPAREN, ASSIGN,
SEMICOLON, PLUS, MINUS, ERROR } TOKENT;
typedef struct
{
TOKENT type;
union {
int value; /* type == NUMBER */
char far* name; /* type == NAME */
}info;
} TOKEN;
extern TOKEN lex(); /*functial lex() e definita altundeva)*/
#endif LEX_H
Funcţia lex() este cea care returnează următorul atom din textul sursă.
3.2.2 Observaţii
Unele limbaje de programare , îndeosebi cele vechi, conţin unele particularităţi care îngreunează
procesul de analiză lexicală. De exemplu FORTRAN şi COBOL impun o anumită structură a programului
sursă pe mediul de intrare. Limbajele moderne au în exclusivitate formatul liber pe fişierul de intrare,
aranjarea instrucţiunilor pe linii fiind făcută pe criterii de claritate şi lizibilitate. În ALGOL 68, spaţiile sunt
nesemnificative, ceea ce duce la îngreunarea identificării atomilor în anumite instrucţiuni.
Există limbaje de programare în care cuvintele cheie nu sunt rezervate (PL/I), urmând ca
analizorul lexical să deosebească din context cuvintele cheie de cele rezervate.
Un analizor lexical poate fi construit manual sau cu ajutorul unui generator de analizoare lexicale.
- Construirea manuală a analizorului lexical înseamnă scrierea programului propriu zis pe baza
unor diagrame realizate în prealabil, care precizează structura atomilor din textul sursă. Tehnica manuală
asigură creerea unor analizoare lexicale eficiente, dar scrierea programului e monotonă, prezintă riscul unor
erori, mai ales dacă există un număr mare de stări.
8
Secvenţa următoare prezintă o implementare manuală simplă a unui scanner:
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h> /
#include "lex.h"
9
if (isdigit(c))
*pbuf++ = c;
else
state = 3;
break;
case 3:
token.info.value= atoi(buf);
token.type = NUMBER;
ungetc(c,stdin);
state = 0; return &token;
break;
case 5:
token.type = LBRACE;
state = 0; return &token;
break;
case 7:
token.type = RBRACE;
state = 0; return &token;
break;
case 9:
token.type = LPAREN;
state = 0; return &token;
break;
case 11:
c = getchar();
if (isspace(c))
;
else
state = 12;
break;
case 12:
ungetc(c,stdin);
state = 0;
break;
case 14:
token.type = RPAREN;
state = 0; return &token;
break;
case 16:
token.type = PLUS;
state = 0; return &token;
break;
case 18:
token.type = MINUS;
state = 0; return &token;
break;
case 20:
token.type = ASSIGN;
state = 0; return &token;
break;
case 22:
token.type = SEMICOLON;
state = 0; return &token;
break;
case 24:
c = getchar();
if (isalpha(c)||isdigit(c))
10
*pbuf++ = c;
else
state = 25;
break;
case 25:
*pbuf = (char)0;
dup_str= strdup(buf); /*aloca spatiu*/
token.info.name =dup_str;
token.type = NAME;
ungetc(c,stdin);
state = 0; return &token;
break;
case 99:
if (c==EOF)
return 0;
fprintf(stderr,"Caracter ilegal: \'%c\'\n",c);
token.type = ERROR;
state = 0; return &token;
break;
default:
break; /* Nu se poate intampla */
}
}
int main()
{
TOKEN *t;
while (((t=lex())!=0))
{
printf("%s",token_name[t->type]);
switch(t->type)
{
case NAME:
printf(":%s\n",t ->info.name);
break;
case NUMBER:
printf(":%d\n",t ->info.value);
break;
default:
printf("\n");
break;
}
}
return 0;
}
Fluxul procedurii lex() se poate reprezenta prin diagramele de tranziţii din figura următoare.
11
digit not(digit) { }
1 2 3* 4 5 6 7
digit
letter not(letter|digit) (
23 24 25* 8 9
letter|digit
sp not(sp) )
10 11 12* 13 14
sp
+ - = ;
15 16 17 18 19 20 21 22
1. La preluarea unui nou atom (de exemplu la intrarea în lex() ) folosim starea specială state 0 pentru a
reprezenta faptul că nu am decis încă ce diagramă să urmăm. Alegerea e făcută pe baza următorului
caracter de intrare Uneori, de exemplu pentru atomul LBRACE atomul e recunoscut imediat prin
scanarea ultimului caracter din atom. Pentru alţi atomi însă, de exemplu pentru NUMBER, cunoaştem
lungimea atomului numai după citirea unui extracaracter care nu aparţine numărului (stări notate cu *).
În acest caz, caracterul în plus trebuie returnat la intrare.
2. Dacă citim un caracter care nu corespunde cu o secvenţă acceptată, se returnează atomul special
ERROR.
Diagramele de tranziţie sunt grafuri orientate şi etichetate în care nodurile simbolizează stările, iar
arcele trecerea (tranziţia) dintr-o stare în alta.
- Generarea automată a analizorului lexical presupune conceperea unui program de traducere
(un fel de compilator) care primeşte la intrare într-un limbaj de specificare, atât structura
atomilor lexicali, cât şi eventualele acţiuni semantice care trebuiesc realizate împreună cu
analiza lexicală. Ieşirea unui astfel de compilator va fi un program de analiză lexicală. Un
astfel de compilator poate fi aplicat unei clase mai largi de limbaje.
4. Noţiuni generale de limbaje formale.
C3
12
- Un simbol neterminal special S care apare doar într-o singură producţie din mulţimea
P şi care se numeşte simbol iniţial, simbol de start sau axioma gramaticii. Producţiile
sau regulile de generare din P arată cum pot fi construite toate frazele limbajului
pornind de la simbolul neterminal S.
În A* există un şir care nu conţine nici un simbol din A. Acest simbol, numit şir vid îl
notăm cu ε.
Un limbaj L peste alfabetul A este o submulţime a lui A*. Orice şir din A* care
aparţine şi lui L este un simbol sau cuvânt al limbajului L.
Evident, mulţimea A* este infinită, deci şi limbajul L poate reprezenta o mulţime infinită.
Acest lucru înlătură orice abordare de tip enumerativ în definirea limbajului, fiind necesară o
reprezentare finită a mulţimii infinite.
Se disting două categorii de astfel de reprezentări:
- reprezentarea (finită) sintetică care generează toate cuvintele limbajului şi
corespunde noţiunii de gramatică.
- reprezentarea (finită) analitică care permite recunoaşterea apartenenţei sau
nonapartenenţei unei construcţii la limbajul considerat, reprezentare care corespunde
noţiunii de automat sau analizor.
Se poate defini un limbaj L generat de gramatica G ca fiind alcătuit din acele simboluri
terminale din G, numite propoziţii, care derivă din simbolul iniţial S:
13
L(G) = { s | s ∈ T* şi S ⇒ s}
Două gramatici se spune că sunt echivalente dacă şi numai dacă limbajele generate de
fiecare din acestea sunt identice.
O gramatică se numeşte recursivă dacă permite derivări de forma:
u ⇒+ α u β, unde u ∈N iar α,β ∈ A*
O gramatică este:
- recursivă la stânga dacă: u ⇒+ u w
- recursivă la dreapta dacă: u ⇒+ w u
Exemplu:
Considerăm o gramatică care descrie un set restrâns de operaţii algebrice
G = {N, T, P, S}
N = {S, <expr>, <term>, <fact>}
T = {a, b, c, -, * }
P = {S → <expr>
<expr> → < term> | <expr> - <term>
<term> → <factor> | <term>*<factor>
<factor> → a | b | c }
Să încercăm să vedem dacă expresia a-b*c aparţine sau nu gramaticii.
S→ <expr> → <expr>-<term> → <fact> - <term> → a - <term> → a- <term>*<factor>→
→ a - <factor>* <factor> → a - b* <fact> → a - b * c
Se observă că propoziţia a - b * c s-a obţinut în urma a 11 derivări, substituind la fiecare
pas câte un simbol în forma propoziţională curentă.
14
• gramatici de tipul 3 - se numesc gramatici regulate, în care părţile drepte ale
producţiilor încep cu un terminal. Clasa mulţimilor regulate peste alfabetul A
reprezintă clasa limbajelor regulate L3(A).
Sintaxa unei propoziţii într-un limbaj independent de context se poate reprezenta printr-o
structură de arbore, numit arbore de derivare (sau deducţie). Pentru recunoaşterea unei propoziţii
dintr-un limbaj, este necesar ca arborele asociat să fie unic. În caz contrar, gramatica care
generează limbajul se numeşte ambiguu.
Un limbaj este inerent ambiguu dacă nu poate fi generat decât de o gramatică ambiguă.
Există posibilitatea ca prin modificarea setului de producţii ale unei gramatici ambigue să
se poată elimina ambiguităţile existente, fără ca limbajul generat să sufere vreo modificare.
Producţii vide
Partea dreaptă a unei producţii conţine un şir de terminale sau neterminale.
Uneori este util să se genereze un şir vid, adică un şir ce nu conţine nici un simbol.
Acest şir este notat cu e.
De exemplu, gramatica
<unsigned integer> → <digit> <rest of integer>
<rest of integer> → <digit><rest of integer> | ε
<digit> → 0 | 1 | …|9
defineşte <rest of integer> ca o secvenţă de 0 sau mai multe cifre.
Producţia <rest of integer> → e se numeşte producţie vidă.
În general, dacă pentru un şir σ este valabilă o derivare de forma σ⇒*ε, atunci σ se
numeşte simbol anulabil. Un neterminal este anulabil dacă există o producţie a cărei
definiţie (parte dreaptă) este anulabilă.
Pentru descrierea unui limbaj de programare este necesară adoptarea unui limbaj de
descriere corespunzător, numit metalimbaj. Această idee aparţine lui John Backus şi notaţia
introdusă de el este cunoscută sub numele de BNF (Backus Naur Form).
O producţie defineşte o clasă sintactică (simbol neterminal) sub forma generală:
15
- Notaţia :: = are semnificaţia : "definit prin"
- clasa sintactică, denumită şi partea stângă, corespunde unui simbol neterminal şi este
inclusă între paranteze unghiulare.
- partea de definiţie este denumită şi partea dreaptă
simboluri terminale.
- fiecare clasă sintactică (simbol neterminal) trebuie să apară în partea stângă a unei
singure producţii;
- simbolul de start nu trebuie să apară în partea stângă a nici unei producţii;
Ulterior s-au utilizat variante şi completări la notaţia BNF pentru a se descrie diferite
limbaje de programare.
Pentru a creşte lizibilitatea notaţiilor, s-au adoptat prescurtări inspirate de
metasimbolurile folosite pentru expresii regulate.
Aceste notaţii extinse au denumirea de forma Backus Naur extinsă EBNF.
16
4.3. Automate de recunoaştere. Diagrame de tranziţie.
Pe baza gramaticii limbajului stabilit pentru atomi, analizorul lexical are sarcina să decidă
apartenenţa la limbaj a atomilor detectaţi în fişierul de intrare. Pentru gramatici regulate,
problema apartenenţei la limbaj este decidabilă.
Problema deciziei trebuie completată cu sarcina codificării atomilor lexicali, cu cea a
semnalării şi tratării erorilor.
Gramaticile de descriere a atomilor lexicali oferă analizorului lexical tiparele pentru
identificarea atomilor. Pe baza acestor gramatici, implementarea procesului de recunoaştere a
atomilor se face folosind un model matematic, numit automat de recunoaştere sau automat finit.
Modelul fizic al unui automat finit este o "maşină" cu operaţii foarte simple care are un
cap de citire, o unitate de comandă şi opţional o memorie. Maşina citeşte câte un caracter de pe
banda de intrare şi unitatea de comandă decide în ce stare trece automatul pe baza caracterului
citit. Automatul se poate afla într-un număr finit de stări.
În momentul în care automatul începe citirea unui caracter, acesta se află în starea numită
starea de start. Automatul are un număr de stări numite, stări finale. Un şir x este acceptat de
automat dacă pornind din starea de start, după citirea tuturor caracterelor din şirul de intrare,
automatul ajunge într-o stare finală. Cu alte cuvinte, şirul aparţine limbajului acceptat de automat.
Modelul matematic de reprezentare a automatului finit este acela al diagramelor de
tranziţii.
- Simbolurile care etichetează arcele indică caracterul la citirea căruia automatul va
trece din starea de la care porneşte arcul în starea în care ajunge arcul respectiv.
- Săgeata etichetată cu cuvântul "start" indică nodul de start al diagramei de tranziţii,
ori poate fi o săgeată de intrare neetichetată.
- Pentru a indica orice alt caracter care poate urma la ieşirea unei stări, în afara celor
deja trecute pe arcele care ies din starea respectivă, se va utiliza o etichetă specială
"altceva".
- Diagramele de tranziţii sunt deterministe, adică acelaşi simbol nu poate eticheta
două sau mai multe tranziţii care ies din aceeaşi stare.
- Unei tranziţii, pe lângă simbol i se pot asocia şi anumite acţiuni care se vor executa în
momentul când fluxul de comandă trece prin tranziţia respectivă.
Exemplu:
b a
a
q0 q1
b
În general analizorul lexical este format din mai multe astfel de diagrame de tranziţii care pornesc
din aceeaşi stare de start şi recunosc grupe de atomi. Dacă parcurgând o anumită diagramă se semnalează
eşec, se revine în starea de start şi se trece la următoarea diagramă. Revenirea în starea de start presupune şi
revenirea capului de citire în poziţia anterioară încercării nereuşite. Readucerea capului de citire se poate
face memorând adresa locaţiei cu citirea căreia a început ultima recunoaştere. Dacă prin parcuregerea
17
secvenţială a tuturor diagramelor de tranziţii. se va semnala eşec la toate, înseamnă că s-a gasit o eroare
În cele ce urmează, dăm notaţia BNF a unei gramatici a atomilor lexicali, reprezentative
pentru majoritatea limbajelor de programare. Notam G 0 această gramatică.
18
(G31) :<lit>::= A | ... | Z
(G32): <cif>::= 0 | ... | 9
lit, cif
A21: lit
211 12
cif
A22: cif
221 222
232
+
233
*
= 234
A23:
231 = 236
<
235 > 237
>
=
238 239
19
A24: ;
241 242
blanc
243
altceva
A25: ( * * )
251 252 253 254 255
altceva
312 322
A 0
313 323
B 1
A31: A32:
311 321
… …
Z 9
3127 3210
Din analiza diagramelor, observăm că efectul stratificării constă în existenţa unor tranziţii
condiţionate de terminale care pe nivelul inferior reprezintă diagrame de tranziţii. Acest lucru
înseamnă că nu putem activa o asemenea tranziţie pe nivelul superior decât dacă diagrama de
tranziţie respectivă de pe nivelul inferior a fost parcursă din starea iniţială într-o stare finală.
Deci, un automat aflat pe un nivel inferior trebuie să transmită nivelului superior
informaţia de acceptare a şirului inspectat.
Vom avea deci o asamblare a automatelor ca în figura următoare:
20
A21
A22
A31
A23 A1
A31
A2 A1
A3 A24
A25 A0
Cuplarea automatelor A1, A2, A3 se face în serie, ieşirea unuia fiind intrarea celuilalt.
Pentru a putea descrie funcţionarea automatului A 0 printr-un limbaj de programare, trebuiesc
îndeplinite două condiţii:
- Automatele rezultate din diferite cuplări trebuie să fie deterministe
- Orice simbol primit la intrarea unui automat şi care nu activează nici o tranziţie
trebuie să ducă la o situaţie de neacceptare sau de eroare.
Observaţie:
Există situaţii în care, pentru identificarea unui simbol, un automat consumă un simbol
care aparţine atomului următor. Soluţiile de implementare constau fie în revenirea în şirul de
intrare cu un simbol, fie în generalizarea avansului pentru toate stările finale.
Textul sursă parcurs şi analizat de analizorul lexical este citit de pe suportul de intrare.
Pentru efectuarea acestei operaţii se recomandă utilizarea a 2 zone tampon din următoarele
motive:
- Poate creşte viteza prin umplerea unui tampon când analizorul lucreaza cu celălalt
- Se poate trata simplu cazul în care un atom se continuă dintr-un tampon în altul.
21
Soluţiile concrete de gestiune ale tampoanelor depind de modul de implementare al
analizorului:
1. Utilizarea unui generator de analizoare lexicale: rutinele pentru gestiune sunt incluse
in generator, şi nu se află sub controlul programatorului.
2. Analizorul se scrie intr-un limbaj de nivel inalt. Posibilităţile de gestionarea
tampoanelor sunt cele specifice limbajului.
3. Analizorul se scrie în limbaj de asamblare. Tampoanele se pot gestiona în modul
explicit la cel mai scăzut nivel.
Eficienţa şi efectul cresc de la 1 la 3
22
pa:= pa +1
end
else
if * pa este la sfirsitul tamponului 2 then begin
*incarca tampon 1
pa :=1
end
else *termină analiza lexicală.
Se mai remarcă şi faptul că acelaşi unic test de sfârşit de tanpon rezolvă şi testul de
sfârşit al textului sursă necesar pentru încheierea analizei lexicale.
Dacă limbajul nu are case, acesta poate fi simulat printr-un tablou (indexat prin codul caracterului
de la intrare). Fiecare element al tabloului corespunde unei stări noi şi reprezintă un pointer spre o secvenţă
de cod ce trebuie executată atunci când caracterul curent corespunde indicelui. Porţiunea de
corespunzătoare fiecărei stări se va încheia fie cu luarea în considerare a stării următoare, fie cu salt la
23
b.Transformarea automatului nedeterminist (AFN) în automat finit determinist
(AFD).
c.Minimizarea numărului de stări ale automatului determinist.
b a
a
q0 q1
b
Figura 1
Din fiecare stare iese o singură săgeată etichetată cu un simbol de intrare. Matricea de tranziţii
a b
q0 q1 q0
q1 q1 q0
Dacă renunţăm la unele restricţii şi permitem ca dintr-un nod să iasă mai multe săgeţi etichetate cu
acelaşi simbol de intrare, precum şi săgeţi etichetate cu λ - care vor reprezenta tranziţii independente de
a b
0 1 3
b b
λ
2
Figura 2 a
24
Unei stări şi unui simbol de intrare nu îi mai corespunde o stare ci o mulţime, eventual vidă de
stări.
Matricea de tranziţii pentru acest automat este:
a b λ
0 {0,1} -- {2}
1 -- {2,3} --
2 {2} {3} --
3 -- -- --
Evident, AFD este un caz particular al AFN.
Pornind de la expresii regulate, se pot construi automate finite nedeterministe.
Fie o expresie regulată R peste un alfabet Σ. Algoritmul de mai jos va genera un automat
finit nedeterminist N, care va accepta limbajul definit de R.
Se descompune expresia R în componentele sale elementare (simboluri şi operatori). Se
vor construi automate elementare pentru fiecare simbol, după care, folosind aceste automate, se
vor construi automatele pentru expresiile compuse. Automatele pentru expresiile compuse se
construiesc inductiv, pentru fiecare operaţie : reuniune, concatenare, inchidere. Algoritmul de
construcţie introduce la fiecare pas cel mult 2 stări noi, prin urmare, automatul rezultat va avea
cel mult de 2 ori atâtea stari câte simboluri si operaţii are expresia regulată.
Algoritmul lui Thomson prezentat în continuare, nu este cel mai eficient (un algoritm
mai performant ar genera un AFN cu mai puţine stări pentru aceeaşi expresie regulată). Are însă
avantajul simplităţii, iar după transformarea din automat nedeterminist în automat determinist,
există posibilitatea reducerii numărului de stări ale automatului finit determinist obţinut.
Folosim următoarele notaţii:
i - stare iniţială
f - stare finală
N(Ri) - automatul corespunzător expresiei regulate Ri.
- Pentru λ (simbolul vid notat şi ε) se generează:
λ
i f
a
i f
Pentru fiecare AFN elementar construit, stările vor fi notate cu nume (numere) distincte; dacă un
acelaşi simbol al alfabetului apare de mai multe ori în ER, se va construi pentru fiecare apariţie a sa câte un
25
Descompunerea ER în componente elementare, respectiv compunerea acestora se face
aducând ER la forma postfix, tinând cont că operatorii se evaluează în ordinea următoare:
parantezele, închiderea ( * ), concatenarea si selecţia (|).
- Pentru R1|R2
N(R1)
λ λ
i f
λ
N(R2 λ
Figura 3
Automatul corespunzător expresiei R1 | R2, este N(R1 |R2), obţinut prin creerea a 2 stări noi:
o stare iniţială, diferită de stările iniţiale ale automatelor N(R1) şi N(R2) şi o stare finală diferită de
stările finale din N(R1) şi N(R2), care îşi pierd proprietatea de satre iniţială şi finală.
Limbajul corespunzător expresiei regulate R1 |R2 este: L(R1) ∪ L(R2).
- Pentru R1R2
i N(R1) N(R2) f
λ λ λ
Figura 4
Automatul corespunzător expresiei R1R2 este N( R1R2) pentru care starea iniţială este starea
se start a automatului N(R1) iar starea finală este cea a automatului N(R2). Starea finală a automatului
N(R1) se identifică cu starea se start a automatului N(R2).
Un drum între i şi f va traversa mai întâi automatul N(R 1), după care va trece prin
automatul N(R2). Prin urmare, şirul de simboluri recunoscut va fi un şir din limbajul expresiei R 1
urmat de un şir al limbajului expresiei R2. În consecinţă, limbajul modelat de automat este:
L(R1)L(R2).
- Pentru R1*
i N(R1) f
λ λ
λ
Figura 5
Automatul are 2 stări noi şi ne putem deplasa din starea iniţială i în starea finală f, fie direct
prin tranziţia λ, fie prin automatul N(R1), de un număr oarecare de ori.
Un automat obţinut pe baza algoritmului lui Thomson are următoarele proprietăţi:
26
Aplicaţie: Se consideră expresia regulată R = (aba)*aa . Automatul construit pas cu pas, pornind de
la această expresie este:
λ
a b
2 3 4
λ
λ
λ 7 8 9 f
i 1
a a
λ
λ 5 6 λ
a
λ
Figura 6
5.1.2. Transformarea AFN în AFD
Un AFN se poate transforma într-un automat finit determinist (AFD) care să accepte acelaşi
limbaj ca şi AFN.
Notăm cu s0 starea iniţială a AFN. O stare a AFD va fi compusă dintr-o mulţime de stări {s1, s2,..., sn
} ale AFN.
Noţiunea de λ-închidere se defineşte pentru fiecare mulţime de stări T ale unui automat:
este mulţimea stărilor în care se poate trece din stările mulţimii T pentru un simbol de intrare.
Exemplu: Pentru automatul din Figura 2, prin tranziţii vide, λ-închidere(0) = {0,2}, λ-
închidere(1) = {1}, λ-închidere(0, 3) = {0,2,3} etc.
Notăm: ∑ alfabetul limbajului sursă
Dstări mulţimea stărilor AFD
Dtranz mulţimea tranziţiilor
Pentru implementarea algoritmului putem folosi ca structuri de date două stive şi un şir
de cifre binare indexat de stările automatului. Într-una din stive se ţine evidenţa mulţimii curente
a stărilor nedeterministe iar a doua stivă se utilizează pentru calculul mulţimii de stări următoare.
Vectorul de cifre binare înregistrează dacă o stare este prezentă în stivă, pentru a se evita dublarea
ei. Organizarea acestei structuri ca vector are avantajul timpului de căutare constant al unei stări.
După încheierea procesului de calcul a mulţimii de stări următoare, rolul stivelor se inversează.
Se iniţializează stările AFD căutat Dstări cu un singur element (o stare), şi anume cu
mulţimea stărilor în care se poate ajunge din starea s0 a AFN numai prin tranziţii vide (de fapt λ-
închidere({s0}), care va fi notată cu λ-închidere({s0}).
La început această stare e nemarcată. Totodată, mulţimea tranziţiilor este vidă.
Pentru fiecare stare nemarcată din Dstări şi pentru fiecare simbol din alfabet se caută stările în
care se poate ajunge în AFN pentru simbolul respectiv. Adaugă aceste stări la D stări dacă ele nu sunt
deja incluse în această mulţime, adaugă tranziţia la Ditranz şi marchează starea testată din Dstări.
Algoritmul de obţinere a AFD este:
27
pentru fiecare a ∈ ∑ execută
*fie T = mulţimea stărilor din AFN pentru care ∃ o tranziţie etichetată cu a de la o
stare si ∈ x;
y = λ-închidere(T);
dacă y ∉ Dstări atunci
*adaugă y la Dstări, y - nemarcată
*adaugă tranziţia x → y la Dtranz, dacă nu există deja
€
€
€
sfârşit AFN2AFD
28
Exemplu: Fie AFN din figura 7. Limbajul acceptat este: {a, b, ab, abab, …}.
a b
1 2 3
λ
0 5 λ
a λ
λ
4 7
b 6
λ
Figura 7
29
Dstări = {(0,1,4)* , (2, 5, 7)*, (6, 7), (1, 3, 7)}
- Pentru simbolul b construim mulţimea {3}şi calculăm λ-închidere ({3}) = {1, 3, 7}, dar ea există.
(0,1,4) = A
(2, 5, 7) = B
(6, 7) = C
(1, 3, 7) = D
(2) = E
Matricea şi diagrama de tranziţii pentru AFD parţial definit sunt cele din Figura 8.
Se observă că AFD obţinut acceptă exact limbajul {a, b, ab, abab, …} acceptat de AFN
iniţial.
30
Stările acceptoare (finale) ale AFD obţinut vor fi acele stări x care vor conţine cel puţin o
stare acceptoare a AFN. Starea de start a AFD este cea formată din s0 împreună cu toate stările la
care se poate ajunge din s0 doar prin simbolul λ.
Algoritmul de mai sus este important pentru că dă soluţia pentru simularea unui AFN.
Simularea directă este dificilă, deoarece trebuie simulat calculul "în paralel" al diferitelor
traiectorii ce pot fi urmate în AFN. Folosind algoritmul, se determină mai întâi AFD echivalent şi
apoi se simulează AFD. Această simulare este echivalentă cu construirea analizorului limbajului
generat de gramatică.
a b
a
A B C b
B D E
a
B -- D
A b
C -- --
C
D E --
E -- D Figura 8
31
O stare s a unui AFN este o stare importantă dacă are cel puţin o tranziţie etichetată cu un
simbol diferit de λ. De exemplu, în algoritmul de conversie AFN-AFD, stările importante au fost
cele care au determinat creerea unei noi stări în AFD.
Considerând mulţimile de stări din AFN corespunzătoare la 2 stări din AFD, 2
submulţimi sunt identice dacă:
1. ele au aceleaşi stări importante
2. ambele fie includ, fie exclud stări acceptoare
Algoritmul de minimizare :
Considerăm că avem un AFD notat M, având mulţimea stărilor notată cu S, iar Σ
reprezintă mulţimea de simboluri de intrare. Presupunem că fiecare stare are o tranziţie pentru
fiecare simbol de intrare. Dacă AFD nu îndeplineşte această condiţie, vom crea o stare fictivă,
numită “stare de blocaj”, m din care vom trasa arce spre el însuşi pentru fiecare simbol de intrare.
Pentru toate stările care nu au tranziţii pentru toate simbolurile, tranziţiile lipsă se vor completa
cu arce spre starea m.
Iniţial divizăm mulţimea stărilor AFN în 2 submulţimi, una conţinând stările care nu sunt
finale, celalată conţinând mulţimea stărilor finale. Algoritmul va partiţiona aceste mulţimi astfel
încât două stări din aceeaşi mulţime vor trece în aceeaşi mulţime de stări pentru orice simbol de
intrare.
Considerăm P o partiţie obţinută la un moment dat în procesul de partiţionare, S={s1,s2,
…,sk} una din mulţimile partiţiei şi un simbol de intrare a. Căutăm pentru fiecare stare s i starea în
care trece pentru simbolul a. Dacă stările următoare obţinute aparţin la mulţimi diferite din P,
mulţimea S se va împărţi în submulţimi ale căror elemente duc în aceeaşi submulţime din P.
Procesul de divizare se repetă până când nu mai găsim grupuri ce trebuiesc divizate.
i) Se realizează o partiţionare P a multimii Dstari în două grupuri de stări: F=setul de stări
acceptoare şi Dstari - F = setul de stări non-acceptoare. Printr-o procedură, care se va da mai jos, se
încearcă efectuarea unei noi partiţionări, Pnou, prin descompunerea grupurilor lui P în subgrupuri.
Dacă Pnou ≠ P, se înlocuieşte P cu Pnou şi se repetă procedura de descompunere. Dacă Pnou≡ P,
înseamnă că partiţionarea nu se mai poate face.
procedura partiţionare este
pentru fiecare grup G ∈ P execută
* descompune G în subgrupuri a.î. 2 stări s şi t din G să se afle în acelaşi subgrup dacă
şi numai dacă, pentru toate simbolurile a ∈ ∑, s si t tranzitează în stări aparţinând
aceluiaşi subgrup
* subgrupurile obţinute se pun în Pnou
sfârsit partitionare
ii) Din fiecare grup al partiţiei obţinute în pasul anterior, se alege câte o stare oarecare (stare
reprezentantă). Acestea vor fi stările AFD minimizat. Starea iniţială va fi starea reprezentantă a
grupului ce conţine starea initială s0, iar stările finale vor fi reprezentantele subgrupurilor provenite
din F.
iii) Toate tranziţiile dintre stările automatului iniţial se transformă în tranziţii între
reprezentanţii grupelor respective. Dacă AFD minimizat conţine o stare de blocaj m, adică o stare
care nu este finală şi care tranzitează în ea însăşi pentru toate simbolurile a ∈ ∑ această stare se
elimină. Se vor elimina, de asemenea, stările care nu pot fi atinse plecând din starea iniţială.
Tranziţiile spre stările de blocaj dinspre alte stări devin nedefinite.
Exemplu: fie automatul din figura 9:
32
a a
a b b
A B D E
a
a
b b
C
Figura 9 b
S={A,B,C,D,E}
Prima partiţie: Π = {A,B,C,D} {E}
Pentru a construi Πnou, considerăm mulţimea {E}; această mulţime nu se mai poate
diviza, deci o includem in noua partiţie.
Considerăm acum {A,B,C,D}şi căutăm tranziţiile pentru simbolul a: A -a-> B, B -a-> B, C
-a-> B, D -a-> B, prin urmare simbolul a nu divizează mulţimea.
Considerăm acum b: A -b-> C, B -b-> D, C -b-> C, D -b-> E, aceste tranziţii vor partiţiona
mulţimea în {A, B, C} şi în {D}.
Πnou = ({A, B, C} {D} {E})
{A, B, C} -a-> {B, B, B} {A, B, C} -b-> {C, D, C}
Deoarece D este în altă partiţie, obţinem:
Πnou = ({A, C} {B} {D} {E})
{A, C} -a-> {B, B} {A, C} -b-> {C, C}
Πnou = ({A, C} {B} {D} {E})
În acest moment Πnou = Π, deci automatul minimizat va avea stările {A, C} {B} {D} {E}.
Din mulţimea {A, C} alegem satrea A ca stare reprezentativă. Toate tranziţiile spre C vor deveni
tranziţii spre A, iar celelalte tranziţii le copiem din automatul iniţial. Deci, în acest caz s-a reuşit
minimizarea numărului de stări cu o stare.
5.1.5. Construirea arborelui binar corespunzător ER
C5
a. Un arbore de derivare (parse tree) într-o gramatică G={N, T, P, S} este un arbore
orientat, etichetat, cu următoarele proprietăţi:
- rădăcina arborelui este etichetată cu S;
- nodurile interioare sunt etichetate cu neterminalele gramaticii, iar frunzele cu neterminale sau
terminale;
- pentru orice nod interior etichetat cu A având descendenţi direcţi etichetaţi în ordine de la stânga
la dreapta cu simbolurile din A: X1, X2, … Xk (k ≥ 1) există în P o producţie: A → X1 X2 … Xk .
Şirul simbolurilor care etichetează frunzele arborelui scrise în ordine de la stânga la dreapta
se numeşte frontiera arborelui.
Se poate arăta că oricărei forme propoziţionale α din G îi corespunde cel puţin un arbore de
derivare în G care are ca frontieră pe α. El se numeşte arborele de derivare în G al lui α. .
Fie gramatica: G={{E}, {i, +, *}, P, E}
cu P = { E→E+E
E→E*E
33
E → (E)
E → i }.
Propoziţia: i*(i+i) are următoarea derivare canonică stânga:
E ⇒ E * E ⇒ i * E ⇒ i * (E) ⇒ i * ( E + E) ⇒ i * ( i + E ) ⇒ i * ( i + i )
În figura următoare se prezintă arborele de derivare al acestei propoziţii. Construcţia urmăreşte
derivarea canonică stângă: frontierele arborilor construiţi reproduc formele propoziţionale din
derivarea canonică stângă.
E
E E
*
E )
i (
E E
+
i i
b. Arborele corespunzător ER este un arbore binar care are câte un nod terminal pentru
fiecare simbol ce apare în ER şi câte un nod interior pentru fiecare operator aplicat (concatenare,
închidere, sau). În prealabil ER va fi modificată, în sensul că la sfârşitul ei va fi concatenat un
simbol special, notat cu #, care va servi drept marcator de sfârşit al ER. O asemenea ER modificată
se numeşte ER augmentată.
În funcţie de operatorul înmagazinat într-un nod, nodul se va numi nod-cat dacă operatorul
este concatenare, nod-sau dacă operatorul este sau, nod-stea.
În automatul AFD corespunzător ER augmentate, orice stare de la care va exista o tranziţie
etichetată cu '#' va fi stare acceptoare.
Fiecare simbol din ER va fi numerotat, în ordinea textuală a apariţiei sale în ER. Dacă
acelaşi simbol apare de mai multe ori, fiecare apariţie va avea un număr distinct. Deci, unui simbol
din alfabet îi pot corespunde mai multe numere de poziţie dacă el este repetat în cadrul expresiei.
Numerele atribuite în acest mod se numesc poziţii..
Arborele se obţine aducând ER la forma postfix .
Exemplu:
Fie ER : ( a | b )* a b b
ER augmentată este: ( a | b )* a b b #
a b a b b #
poz. 1 2 3 4 5 6
Fiecare nod al arborelui primeşte câte un identificator unic, pentru a putea fi localizat. În
figura următoare, identificatorii nodurilor au fost notaţi cu Ni, i=1. .13. Arborele corespunzător va fi
cel din figura următoare.
34
N1
•
N2 • N3 #
6
N4 • N5
b
N6 • N7 5
b
N8 N9 4
• a
3
N10 *
N11
|
a N13 b
1 N 2
12
Pentru calculul acestor funcţii este necesar să se determine acele noduri care sunt rădăcini
ale unor subarbori ce pot genera şirul vid. Asemenea noduri se numesc anulabile. Vom defini o
funcţie Anulabil(n) care va returna valoarea logică true dacă n este un nod anulabil, şi false în caz
contrar.
35
Există 2 reguli de bază şi 3 reguli inductive pentru cei 3 operatori. Pe baza acestor reguli,
parcurgând arborele de la frunze spre rădăcină, se pot determina valorile celor 3 funcţii.
1. Frunză cu
eticheta λ true ∅ ∅
2. Frunză eti-
chetată cu i false {i} {i}
n |
3. Anulabil(c1) Primapoz(c1) Ultimapoz(c1)
n •
4. Anulabil(c1) dacă Anulabil(c1) dacă Anulabil(c2)
c1 c2
şi Anulabil(c2) atunci Primapoz(c1) atunci Ultimapoz(c2)
∪ Primapoz(c2) ∪
Ultimapoz(c1)
altfel Primapoz(c1) altfel
Ultimapoz(c2)
c1
36
Observaţii:
- Pentru Ultimapoz regulile sunt similare cu cele de la Primapoz, doar că se înlocuieşte Primapoz
cu Ultimapoz şi se interschimbă c1 cu c2 (unde este cazul).
- Regula 5 pentru Anulabil (n) arată că dacă nodul n este închiderea expresiei prin * atunci
Anulabil (n) este true, deoarece închiderea expresiei prin * generează un limbaj care include cu
certitudine pe λ.
- Regula 4 pentru Primapoz arată că dacă în expresia rs, r generează pe λ (adică Anulabil(c1) =
true) atunci Primapoz (s) "se vede" prin r şi se include în Primapoz (n). În caz contrar, Primapoz
(n) va conţine Primapoz (r).
Funcţiile Primapoz (n) şi Ultimapoz(n), calculate pentru exemplul precedent sunt:
a b
N12 2 N13
1
37
În exemplul dat, având funcţiile Primapoz (n) şi Ultimapoz(n) calculate, se determină
Pozurm(i).
Deci:
Poz.
Pozur
m
1 {1,2,3}
2 {1,2,3}
3 {4}
4 {5}
5 {6}
6 -
Funcţia Pozurm se poate reprezenta ca un graf orientat, având câte un nod pentru fiecare
poziţie şi un arc orientat de la i la j, dacă j este în Pozurm(i).
38
a
a
1
b b #
a b 3 4 5 6
2
a
b
Acest graf este un AFN fără λ pentru ER dată, dacă sunt îndeplinite condiţiile:
- Toate poziţiile din Primapoz (rad) devin stări de start {1,2,3}
- Fiecare arc orientat (i,j) este etichetat cu simbolul din poziţia j
- Starea asociată cu # este singura stare acceptoare
Notând cu rad nodul rădăcină al arborelui şi preluând notaţiile Dtranz şi Dstări de la cursul anterior,
algoritmul de transformare a arborelui ER în AFD este:
procedura ER2AFD este
*iniţializează Dstări cu Primapoz (rad)
*la început stările din Dstări sunt nemarcate
cât timp există stare nemarcată T în Dstări execută
*marchează T
pentru fiecare a ∈ ∑ execută
*fie U=Pozurm (p), unde p ∈ T şi simbolul din poziţia p este a
dacă U ≠∅ şi U ∉ Dstări atunci
*adaugă U ca stare nemarcată la Dstări
€ a
*adaugă tranziţia T U la Dtranz
€
€
sfârşit ER2AFD
Etapele care se parcurg pentru obţinerea AFD pe baza arborelui unei ER sunt:
I.Se determină Primapoz şi Ultimapoz pentru fiecare nod al arborelui.
II.Se calculează Pozurm pentru fiecare poziţie, parcurgând arborele de sus în jos.
III.Se execută procedura ER2AFD.
39
Generatorul este un program care pe baza specificaţiilor de intrare produce tabela de
tranziţii a automatului. Analizorul lexical va fi format dintr-un simulator pentru automat,
împreună cu tabela de tranziţii generată.
Lexemă
Simulatorul Ieşire
automatului
finit
Tabela de
tranziţii
În continuare vom analiza câteva probleme de proiectare specifice pentru cele 2 variante de
automate.
a. Generator care implementează AFN
Considerăm tiparele reprezentate prin r i, i=1,n pentru care se vor construi automatele
nedeterministe N(ri). Automatele parţiale obţinute se combină într-un automat general, , numit
AFN combinat astfel: se introduce o nouă stare de start de la care pleacă tranziţii λ spre toate cele
n automate.
N(r1)
λ
.
s0 .
.
λ
N(rn)
Simularea automatului combinat se bazează pe o variantă a algoritmului iniţial, care
recunoaşte cel mai lung cuvânt din intrare. Aceasta înseamnă că de fiecare dată când automatul
ajunge la o mulţime de stări care conţine o stare acceptoare, va reţine poziţia din intrare şi tiparul
ri asociat cu acea stare, dar simularea continuă până în momentul în care se ajunge în situaţia de
terminare, adică din mulţimea de stări curentă nu există nici o tranziţie pentru simbolul din
intrare. La atingerea condiţiei de terminare, pointerul de avans al intrării se va retrage la poziţia
corespunzătoare ultimei stări acceptoare marcate. Tiparul memorat pentru această poziţie
identifică atomul găsit, iar lexema recunoscută se găseşte între cei doi pointeri. Dacă nu există
stare acceptoare, se poate genera un mesaj de eroare.
Exemplu:
Se consideră următoarele tipare:
40
a {}
abb {}
a*b+ {}
Automatele parţiale corespunzătoare celor 3 expresii sunt:
a
1 2
a b b
3 4 5 6
a b
b
7 8
Combinarea automatelor parţiale în automatul general este:
a
1 2
λ
λ a b b
0 3 4 5 6
a b
λ
b
7 8
Se va analiza şirul de intrare aaba
tipar 1 tipar 3
Mulţimea de stări iniţială este 0,1,3,7. La intrare avem simbolul a, deci se va trece în
mulţimea 2,4,7. Întrucât starea 2 este stare finală, se reţine poziţia 2 în şirul de intrare şi tiparul 1
asociat cu cuvântul recunsocut. Se continuă simularea şi pe baza celui de-al doilea a din intrare se
trece în starea 7, apoi în starea 8 care este stare finală. Pointerul este acum pe simbolul b din
intrare deci se va memora poziţia lui b şi tiparul 3 asociat cu şirul aab. Se continuă simularea
pentru ultimul a la care s-a ajuns şi din acest punct nu mai avem tranziţii, În consecinţă, se revine
la ultima corespondenţă recunoscând lexema aab corespunzătoare celui de-al treilea tipar.
Dacă expresiile regulate ar avea asociate acţiuni semantice, acestea s-ar executa în
momentul recunoaşterii unui tipar. Această operaţie nu se va face în mod automat de fiecare dată
41
când analizorul ajunge într-o stare acceptoare corespunzătoare unui tipar, ci numai atunci când
tiparul se dovedeşte a fi tiparul care realizează cea mai lungă corespondenţă.
b. Generator care implementează AFD
Dacă generatorul furnizează la ieşire un AFD, programul de simulare este asemănător cu
cel pentru AFN din capitolul anterior, adică se va căuta lexema de lungime maximă.
Problema care apare este că s-ar putea ca prin transformarea AFN-> AFD, mulţimea de
stări din AFN corespunzătoare unei stări din AFD să conţină mai multe stări acceptoare. Trebuie
să decidem care stare din cele acceptoare se va alege pentru a determina tiparul asociat. Regula
este că se va alege acea stare care corespunde expresiei regulate aflate mai în faţă, în ordinea
introducerii expresiilor.
Exemplu: Aplicăm algoritmul de conversie automatului generalizat de mai sus:
stare a b tiparul
anunţat
0,1,3,7 2,4,7 8 nimic
2,4,7 7 5,8 a
8 - 8 a*b+
7 7 8 nimic
5,8 - 6,8 a*b+
6,8 - 8 abb
În mulţimea de stări {6,8} ambele stări sunt finale. Deoarece starea 6 este stare terminală
pentru o expresie regulată care apare înaintea expresiei regulate pentru starea finală 8 (ordinea de
introducere a fost a, abb, a*b+), se va alege starea 6 ca stare acceptoare.
Pentru şirul de intrare aaba, algoritmul de simulare va genera aceleaşi puncte de marcare
ca şi la simularea AFN.
6. Analiza sintactică C6
Este etapa din construcţia compilatorului în care se recunosc construcţiile sintactice ale
programului. Regulile care descriu structura sintactică a programelor corecte pentru un anumit limbaj
de programare se exprimă în mod uzual prin gramatici independente de context scrise de exemplu în
notaţia BNF. Utilizarea gramaticilor pentru descrierea limbajelor de programare are urmatoarele
avantaje:
a. gramatica reprezintă o notaţie precisă a unui limbaj, relativ uşor de reţinut;
b. s-au elaborat metode de construcţie manuală sau automată de analizoare sintactice
eficiente pentru limbaje descrise prin gramatici; aceste metode permit şi punerea în
evidenţă a unor ambiguităţi sintactice care ar trece neobservate în faza de definiţie a
limbajului sau la începutul proiectării compilatorului
c. descrierea limbajului printr-o gramatică corectă favorizează procesul de detectare a
erorilor şi de traducere a programului sursă în cod obiect
d. dacă limbajul evoluează în timp, şi apar construcţii de limbaj noi (completări la sintaxă)
care trebuie să efectueze sarcini noi (completari la semantică), construcţiile noi se pot
adăuga mai uşor limbajului iniţial, iar modificările implicate în compilator sunt mai
simple
În cele ce urmează, se vor prezenta principalele metode de analiză sintactică utilizate în
compilatoare. Se vor studia conceptele generale –operaţii cu gramatici, transformări aplicate
gramaticilor-, tehnici de implementare manuală a analizoarelor sintactice precum şi tehnici de
generare automată a analizoarelor sintactice. Metodele prezentate se vor completa cu tehnici specifice
de revenire în caz de eroare.
42
Analizorul sintactic primeşte ca intrare de la analizorul lexical un şir de atomi lexicali şi are
sarcina de a verifica dacă şirul respectiv poate fi generat de gramatică. În caz de eroare va trebui să
semnaleze cât mai clar atât poziţia cât şi cauza erorii şi să apeleze rutine de tratare şi revenire din
erori pentru a putea continua analiza.
Tabela de simboluri
atom
Program Analizor Analizor arbore Restul prg.cod
de cod
lexical sintactic front-end
sursă citire atom
43
acela este locul erorii. Există totuşi probabilitatea ca eroarea să fie chiar în acel loc sau în imediata lui
vecinatate, cel mult câtiva atomi mai în faţă. Natura erorii se precizează prin mesaje de eroare, dacă
există o probabilitate mare de estimare corectă se genereaza un mesaj exact, de exemplu “lipseşte;”,
dacă nu, este de preferat un mesaj mai vag: ”sintax error”.
În ceea ce priveşte metodele de revenire din eroare, există câteva metode generale. În
principiu, compilarea nu poate fi abandonată decât la erori grave, iar analiza trebuie reluată dintr-un
punct cât mai apropiat de locul erorii cu condiţia ca prin revenire să nu se introducă erori noi.
44
Observaţie: Fiecare construcţie exprimabilă printr-o expresie regulată se poate descrie şi
printr-o gramatică independentă de context, respectiv orice automat finit nedeterminist se poate
transforma într-o gramatică care generază acelaşi limbaj.
Exemplu. Fie expresia regulată: (a|b)*abb. Ei îi corespunde un AFN di figura următoare:
a
a b b
0 1 2 3
b
Pe baza acestui AFN se poate construi gramatica:
G: A0 → aA0 | bA0 | aA1
A1 → bA2
A2 → bA3
A3 → λ
Metoda de transformare este:
- Pentru fiecare stare i din automat se crează un neterminal Ai
- Pentru fiecare tranziţie i a → j se crează în gramatică o construcţie de forma Ai → aAj
- Pentru fiecare tranziţie i λ → j se include în gramatică o construcţie de forma Ai → Aj
- Pentru fiecare stare finală i se include în gramatică câte o producţie Ai → λ
- Simbolul corespunzător stării de start devine simbol de start al gramaticii.
Nu există reguli fixe de divizare a unui limbaj în parte lexicală şi nelexicală. De obicei se
descriu prin expresii regulate şi se tratează la expresii regulate construcţiile simple ale unui limbaj:
identificatori, numere, şiruri de caractere, iar în faza de analiză sintactică se tratează construcţiile mai
complexe care se exprimă prin relaţii recursive: instrucţiuni, declaraţii, expresii etc.
6.2.3. Gramatici ambigue. Exemplu de transformare a unei gramatici ambigue într-o gramatică
neambiguă echivalentă
Analizorul sintactic pentru un limbaj generează implicit sau explicit arborele sintactic pentru
şirul de intrare dat. Din această cauză este important ca arborele care se asociază să fie unic.
Considerăm următoarea gramatică :
S → AB|BC
A→D
B→E
C→D
D → a|b
E → a|b
Dacă şirul din intrare este ‘ab’, atunci pentru această propoziţie se pot forma doi arbori:
45
S S
A B B C
E E
D E E D
a b a b
Definiţie: O GIC se numeste ambiguă dacă în limbajul generat există o propoziţie care admite mai
mult decât un arbore sintactic, obţinut prin derivări dinstincte; în caz contrar gramatica este
neambiguă. Cu toate că gramaticile ambigue reprezintă o descriere mai compactă şi mai clară a unui
limbaj, ele sunt în general o piedică, un inconvenient în plus.
Pentru acelaţi limbaj, pot exista atât gramatici ambigue cât şi gramatici neambigue care să-l genereze.
Exemplu: G1: S → i5 | SS
G2: S → i5 | i5S
L(G1)=L(G2)={i5k | k>=1}
G1 este ambiguă deoarece de exemplu pentru i15 avem două derivări stânga:
S → SS → SSS → i5SS → i10S → i15
S → SS → i5S → i5SS → i10S → i15
în timp ce G2 este neambiguă.
Definiţie: Un limbaj se numeşte inerent ambiguu dacă orice gramatică care-l generează este ambiguă.
E * E
E + E
E id
id E E E +
*
id id id id
Ambiguitatea se poate rezolva dacă stabilim care dintre operatorii ‘*’ şi ‘+’ este mai prioritar.
Pentru primul arbore operatorul mai prioritar este ‘*’ iar pentru al doilea este ‘+’. Modificând
gramatica astfel:
G2= < {E,T,F}, {+,*,(,),id}, P2, E >
P2: E → E+T|T
T → T*F | F
F → (E) | id
Obţinem pentru expresie un arbore unic:
46
E
E + T
T T F
*
F id
F
id id
Ambiguitatea s-a eliminat prin fixarea priorităţii mai mari pentru operatorul de ‘*’ faţă de
operatorul ‘+’. În general eliminarea ambiguităţii implică mărirea numărului de neterminale.
În timp ce simbolurile inaccesibile pot fi atât terminale cât şi neterminale, cele nefinalizate pot fi
doar neterminalele gramaticii.
Se poate demonstra că pentru orice GIC cu limbajul generat L(G) ≠ Φ există o gramatică
echivalentă cu ea (deci generează acelaşi limbaj) care nu are simboluri inutile.
Eliminarea simbolurilor inutile din gramatică se poate face în doi paşi:
1. se caută şi se elimină toate simbolurile nefinalizate precum şi relaţiile care le conţin
2. se determină şi se elimină toate simbolurile inaccesibile şi relaţiile corespunzătoare lor
Gramaticile care descriu limbajele de programare sunt obligatoriu fără simboluri inutile.
47
O gramatică este recursivă la stânga dacă are cel puţin un neterminal A pentru care există o
derivare de forma: A =+> A α unde α este un şir oarecare. În mod similar se defineşte recursivitatea la
dreapta a unei gramatici .
Metodele de analiză sintactică descendentă nu pot trata gramaticile recursive la stânga
(analizorul intră în ciclu infinit) rezultând deci că pentru a face o gramatică analizabilă prin această
metodă este necesar să se elimine acest tip de recursivitate. Cazul cel mai simplu îl constituie acela în
care gramatica are producţii de forma:
A→Aα, ceea ce înseamnă recursivitate de stânga imediată. Acest tip de recursivitate se rezolvă simplu
în felul următor:
Având producţiile A → A α | β unde β nu începe cu neterminalul A, se introduce un nou neterminal
A’ iar producţiile se modifică în felul următor:
A → βA’
A’ → α A’ | ε
Se poate arăta uşor că limbajul generat de noua gramatică nu s-a schimbat.
Exemplu:
E→E+T|T
T→T*F|F
F → (E) | id
Producţiile recursive sunt cele pentru E şi T.
=> E → T E’ E’ → + T E’ | ε
T → F T’ T’ → * F T’ | ε
F → (E) | id
48
Ai → δ1 γ | … | δk γ unde Aj → δ1 | … | δk reprezintă toate producţiile corespunzătoare
lui Aj
* elimină recursivitatea stânga imediată pentru Ai, dacă este cazul
end;
end;
Algoritmul funcţionează corect deoarece înainte de iteraţia i oarecare orice producţie de
forma Ak→ Al α cu k< i are în mod obligatoriu l >k. Ca rezultat, la următoarea iteraţie (i), ciclul
interior (cu j) va ridica progresiv limita inferioară m în orice producţie A i → Am α până când m ≥ i.
Eliminând acum recursivitatea imediată pentru Ai, rămâne m > i, adică poate începe următoarea
iteraţie.
Trasarea algoritmului pentru gramatica anterioară: (chiar dacă există producţie vidă pentru A, pentru
gramatica dată nu deranjează)
1. ordonăm producţiile pentru S şi A
2. pentru i=1 bucla pentru j nu are efect (neterminalul S)
pentru i=2 adică neterminalul A
A → Ac | Aad | bd | ε
Introducem un neterminal nou A’ şi obţinem
A → bdA’ | A’
A’ → cA’ | adA’ | ε
⇒ s-a obţinut gramatica
S → Aa | b
A → bdA’ | A’
A’ → cA’ | adA’ | ε
49
Fie gramatica:
S → CB | a; B → Sa | b; C → Bb | Cba
Algoritmul se bazează pe ordonarea neterminalelor urmată de prelucrarea producţiilor astfel încât
să nu apară producţii de forma Ai → Aj α cu j ≤ i.
Ordonăm neterminalele:
A1 = S, A2 = B, A3 = C
Gramatica cu producţii numerotate devine:
1. A1 → A3 A2 2. A1 → a
3. A2 → A1 a 4. A2 → b
5. A3 → A2 b 6. A1 → A3 ba
Producţia 3 este înlocuită cu:
3', 3" A2 → A3A2 a| aa
Producţia 5 este înlocuită cu:
5', 5", 5"' A3 → A3A2 ab| aab | bb
Producţiile 5', 5", 5"' şi 6 se înlocuiesc datorită recursivităţii stângi imediate a lui A 3 cu:
A3 → aab A' | bb A' şi A' → A2 ab A'| ba A' | ε
Renumerotând producţiile, gramatica devine:
0. A' → A2 ab A' 1. A' → ba A' 2. A' → ε
3. A1 → A3 A2 4. A1 → a
5. A2 → A3A2 a 6. A2 → aa 7. A2 → b
8. A3 → aab A' 9. A3 → bb A'
C7
Exemplu de eliminarea recursivităţii stânga dintr-o gramatică
Fie gramatica:
S → CB | a; B → Sa | b; C → Bb | Cba
Algoritmul se bazează pe ordonarea neterminalelor urmată de prelucrarea producţiilor astfel încât
să nu apară producţii de forma Ai → Aj α cu j ≤ i.
Ordonăm neterminalele:
A1 = S, A2 = B, A3 = C
Gramatica cu producţii numerotate devine:
1. A1 → A3 A2 2. A1 → a
3. A2 → A1 a 4. A2 → b
5. A3 → A2 b 6. A1 → A3 ba
Producţia 3 este înlocuită cu:
3', 3" A2 → A3A2 a| aa
Producţia 5 este înlocuită cu:
5', 5", 5"' A3 → A3A2 ab| aab | bb
Producţiile 5', 5", 5"' şi 6 se înlocuiesc datorită recursivităţii stângi imediate a lui A 3 cu:
A3 → aab A' | bb A' şi A' → A2 ab A'| ba A' | ε
Renumerotând producţiile, gramatica devine:
0. A' → A2 ab A' 1. A' → ba A' 2. A' → ε
3. A1 → A3 A2 4. A1 → a
5. A2 → A3A2 a 6. A2 → aa 7. A2 → b
8. A3 → aab A' 9. A3 → bb A'
50
Tipuri de analizoare sintactice
51
Iniţial, simbolul de anticipare este cel mai din stânga atom din şirul de intrare.
Exemplul 1.
Fie gramatica G1 cu producţiile:
S → bAe
A → d | dA
şi cuvântul de intrare: bdde din limbajul generat de gramatică.
Baleierea şirului de intrare, concomitent cu construcţia arborelui decurge în felul următor:
Deoarece obiectivul iniţial al analizorului este S, în primul pas al analizei se propune ca
obiectiv singura structură posibilă, bAe, care conduce la arborele de derivare din fig.1. Operaţia
este de expandare. Obiectivul iniţial, S, este astfel înlocuit de structura formată din trei obiective
succesive: b, A, e, care sunt etichetele descendenţilor imediaţi pentru nodul etichetat cu S.
Obiectivul imediat următor este dat de frunza cea mai din stânga, etichetată cu terminalul
b. Deoarece este un terminal, el trebuie regăsit ca simbol curent în şirul de intrare. Pentru
exemplul dat este adevărat şi analizorul sintactic face o operaţie de avans şi se propune
recunoaşterea unei structuri a lui A din şirul de intrare încă neanalizat. Se încearcă o expandare
cu prima alternativă a lui A: d şi se obţine arborele din figura 2.
S
S
b A
b e
A e E
E
Fig. 1 d Fig. 2
Obiectivul A este înlocuit cu obiectivul d, terminal care trebuie găsit în şirul de intrare.
Deoarece următorul simbol de intrare este d, analizorul face un avans la obiectivul e. În acest
moment nu se mai poate avansa, deoarece primul caracter al şirului neanalizat este d (nu e),
analizorul sintactic sesizează o eroare în luarea deciziilor şi se întoarce la cel mai recent obiectiv
propus şi depăşit, adică A şi propune o altă alternativă: dA.
Datorită acestei întoarceri, are loc o revenire în şirul de la intrare până la punctul în care
se afla analizorul când a propus prima alternativă de expandare pentru A, adică dde.
Expandarea lui A cu dA conduce la arborele din fig. 3. Se înlocuieşte astfel obiectivul A
cu două obiective d şi A. Următorul obiectiv imediat este regăsirea lui d în şirul dde, ceea ce
permite avansul şi trecerea la obiectivul A. Din nou analizorul propune expandarea cu prima
alternativă d, urmată de avansul cu d, conducând la arborele din fig. 4.
S
S
A
A b e
b e E
E
A
A d
d E
E Fig. 4
Fig. 3 d
E
52
Astfel, ultimul obiectiv A este rezolvat şi se raportează succes obiectivului de pe nivelul
superior, care este tot A şi care în acest moment este rezolvat. Următorul obiectiv devine e.
Deoarece şirul de la intrare este chiar e, se face un avans raportându-se succes pentru e şi apoi
pentru S.
Coincidenţa dintre raportarea succesului pentru S şi epuizarea şirului de intrare este
echivalentă cu acceptarea.
Se observă că derivarea stânga a lui S în bdde este echivalentă cu construirea arborelui de
derivare a şirului.
S ⇒bAe ⇒
s
bdAe ⇒bdde
s s
Dacă şirul de intrare ar fi fost bddb, la încercarea de avans cu e s-ar fi înregistrat eşec,
ceea ce ar fi dus la reveniri succesive astfel: întâi o încercare cu o altă alternativă pentru
neterminalul A. Deoarece nu mai există o astfel de alternativă, se revine la obiectivul b, care este
cel mai din stânga descendent a lui S. Dar acesta este un terminal, se raportează eşec pentru
întreaga alternativă bAe. Deoarece aceasta este singura alternativă pentru S, se raportează eşec
pentru S, iar acesta fiind nodul rădăcină, situaţia este echivalentă cu neacceptarea şirului bddb.
Implementarea unui asemenea proces necesită, pentru eventualele reveniri, memorarea
drumului parcurs în arbore, precum şi a punctelor din şirul de la intrare în care s-a început
expandarea neterminalelor.
Deoarece revenirile se fac strict în ordine inversă faţă de cea a axpandărilor şi
avansurilor, mecanismul de implementare va fi cel de stivă.
Stiva poate fi implementată explicit sau implicit, folosind mecanismul de implementare
al recursivităţii existent în multe limbaje de programare.
Observaţie. Dacă producţiile gramaticii ar fi de forma: A → Ad | d, analizorul sintactic
ar executa - pentru orice şir de intrare – un ciclu infinit. Într-adevăr, după avansul peste b,
obiectivul următor devine A, care se expandează în prima sa alternativă, adică Ad, ceea ce fixează
ca obiectiv tot pe A ş.a.m.d. O soluţie ar fi să lăsăm analiza lui Ad ultima şi să încercăm mai întâi
cu d, dar atunci ciclul infinit ar apare pentru şiruri incorecte, de exemplu be. Deci o gramatică
recursivă la stânga poate determina ca un analizor sintactic descendent recursiv, chiar şi pentru
varianta cu reveniri să intre într-un ciclu infinit. Această situaţie apare atunci când expandând un
neterminal recursiv ajungem din nou la expandarea sa, fără să avansăm în şirul de intrare.
53
BOOL sintax_S()
/*analiza sintactica derivata din S */
{ /*incearca alternativa S -> cAd */
if (*token =='c'){
++token;
if (sintax_A())
if (*token =='d'){
++token;
return true;
}
}
return false;
}
BOOL sintax_A()
/*analiza sintactica derivata din A */
TOKEN save; /pentru backtracking */
save = token;
{ /*incearca alternativa A -> ab */
if (*token =='a'){
++token;
if (*token =='b'){
++token;
return true;
}
}
token = save; /*esec, backtracking */
{ /*incearca alternativa A -> a */
if (*token =='a'){
++token;
return true;
}
token = save; /*esec, backtracking */
/* nu mai sunt alternative, esec */
return false;
}
Metoda prezentată are două dezavantaje:
- Nu se poate aplica unei gramatici recursive la stânga
- Backtracking este dificil de implementat, deoarece toate acţiunile compilatorului
până în punctul de revenire (de exemplu actualizarea tabelei de simboluri) trebuiesc
şterse. Soluţia ar fi aplicarea analizorului sintactic asupra unor gramatici pentru care
nu mai este necesar procedeul de backtracking.
54
Revenind la exemplul 1, un şir de intrare bdde este respins pentru ordinea de
încercare d urmată de dA. Astfel, după avansul lui b şi expandarea cu d a lui A, se
avansează cu d şi se verifică existenţa lui e la intrare, ceea ce conduce la eşec
(următorul simbol este d, nu e). Acest lucru se datorează faptului că d este un prefix al
lui dA şi se raportează succes la expandarea cu d a lui A atât în cazul în care alternativa
d este corectă, cât şi în cazul în care structura intrării este dA.
Soluţia pentru această problemă este ordonarea alternativelor de tip β şi βα
astfel încât βα să îl preceadă pe β. Această ordonare permite o optimizare: Dacă pentru
analiza conform cu β se obţine succes, se memorează starea intrării şi se analizează
conform cu α. Dacă se raportează eşec, se revine la situaţia intrării de la sfârşitul lui β şi
se raportează succes.
Mai general, problema se pune pentru alternativele care încep la fel. De
exemplu, A → α β1 | α β2 . Implementarea prezentată ar fi încercat α β1 şi apoi α β2 . Se
observă că α s-ar fi încercat de două ori.
Pentru evitarea acestei situaţii se recurge la factorizare stânga:
A → α A'
A' → β1 | β2
Folosind această metodă, pentru A există o singură posibilitate, iar alternativele
lui A' diferă cu primul simbol, deci este uşor de recunoscut alternativa corectă faţă de
şirul curent de la intrare. Această idee stă la baza analizei sintactice descendente fără
reveniri. Aceasta însă nu se poate implementa pentru orice tip de gramatică
independentă de context. Se pune problema determinării unei astfel de gramatici.
55
Exemplu. Fie producţiile unei gramatici:
tip -> simplu | ^id | array [simplu] of tip
simplu -> integer | char | num pp num
Analizorul sintactic se va compune din 2 proceduri corespunzătoare neterminalelor tip şi
simplu precum şi dintr-o procedură suplimentară notată coresp care va avansa simbolul de
anticipare la următorul caracter din intrare după ce verifică corespondenţa dintre simbolul de
anticipare şi argument.
Procedure coresp(t:atom);
Begin
If sa=t then sa:=urmatorul
Else eroare();
End;
Procedure tip;
Begin
If sa în [integer,char,num] then simplu
Else if sa = ´^´ then begin
Coresp(‘^’); coresp(id);
End
Else
If sa=array then begin
Coresp(array); coresp(‘[‘);
Simplu; (* trateaza tipul indicelui *)
Coresp(‘]’); coresp(of);
Tip;
End
Else eroare();
End; {tip}
Procedure simplu;
Begin
If sa=integer then coresp(integer)
Else if sa=char then coresp(char)
Else if sa=num then begin
Coresp(num); coresp(pp); coresp(num);
Else eroare();
End;
56
Rezultă că analiza sintactică predictivă se bazează pe informaţii despre simbolurile care
pot să fie primele în şirul derivat din partea dreaptă a producţiei. Notând cu α partea dreaptă a
unei producţii, se defineşte ca PRIM(α) mulţimea tuturor simbolurilor terminale care pot să apară
pe prima poziţie în cel puţin un şir derivat din α. Dacă α=ε sau poate genera ε, atunci se include
şi ε în PRIM(α).
Exemplu: PRIM(^id)={^}
PRIM(simplu)={integer, char, num}
Dacă părţile drepte încep cu un terminal (de exemplu cuvânt cheie) acel terminal este
singurul element al mulţimii PRIM şi determinarea se face într-un singur pas.
Considerăm producţia A -> α | β cu două alternative pentru neterminalul A. Analiza
sintactică descendent recursivă de tip predictiv cere ca PRIM(α) şi PRIM(β) să fie disjuncte.
Dacă această cerinţă nu este îndeplinită se poate încerca rezolvarea ei prin factorizare la stânga.
descendent recursiv utilizează implicit o producţie vidă atunci când nu se poate utiliza nici o altă
producţie.
program bloc .
57
bloc CONST id = nr ;
VAR id ;
PROCEDURE id ; bloc ;
instructiune
instructiune id := expresie
CALL id
expresie = expresie
<>
<
<=
>
>=
58
expresie + termen
-
+ -
termen factor
factor identificator
numar
* /
( expresie )
Program anal_sintactic;
Label 99; {abandonarea compilarii}
Const nrcuvcheie=11;
Txmax=100; {lungimea tabelei de simboluri}
idlung=10;
Type codlex =(nedef, ident, numar, plus, minus, inm, impr, oddcl, eq, ne, ge, gt, le, lt, pr_stg, pr_dr,
virgula, punct, punct_virg, atrib, begincl, endcl, ifcl, thencl, whilecl, docl, callcl, constcl,varcl,
proccl);
Obiect=(constanta,variabila,procedura);
Var cl:codlex;
Id:string[10]; {ultimul identif citit de ALEX}
Num:integer; {se considera doar nunere intregi}
ch:char;
cc,ll:integer;
linie: {tamponul de intrare}
cheie: array[1..nrcuvcheie] of codlex;
clsing:array[char] of codlex;
ts: array[0..tmax] of record
nume:string[10];
fel:obiect;
End;
Procedure init_an_lex……
Procedure eroare….
Procedure eroaregrava…..
Procedure carurm….
Procedure anlex…..
Procedure bloc(tx:integer);
Procedure intrare(f:obiect); //obiect:constante,proceduri, variabile
Begin
tx:=tx+1; //eventual se poate verifica depasirea indicelui TS
with ts[tx] do
59
begin
num:=id;
fel:=f;
end;
end;
//cauta id in tab de simb
function pozitie(id:string[10]):integer;
var i:integer;
begin
ts[0].nume:=id; i:=tx;
while(ts[i].nume<>id) do dec(i);
pozitie:=i;
end;
procedure declconst;
begin
if cl=ident then
begin
anlex;
if cl=eq then
begin
anlex;
if cl=numar then
begin
intrare(constanta);
anlex;
end;
else eroare(8); (*lipseste valoare pentru decl de constante*)
end;
else eroare(9); (*lipseste ‘=’ in decl. de const *)
end;
else eroare(10); (*lipseste nume constanta *)
end;
procedure declvar;
begin
if cl=ident then
begin
intrare(variabila);
anlex;
end;
else eroare(11); (* lipseste nume simbolic *)
end;
procedure instructiune;
var i:integer;
procedure expresie;
procedure termen;
procedure factor;
var i:integer;
begin
if cl=ident then
begin
i:=pozitie(id);
if i=0 then eroare(12)
else if ts[i].fel=procedura then eroare(13);
anlex;
60
end;
else if cl=numar then anlex
else if cl=prstinga then
begin
anlex;
expresie;
if cl=prdreapta then anlex;
else eroare(14);
end
else eroare(15); (* eroare de factor *)
end; (* factor *)
begin (* termen *)
factor;
while cl in [inm,impr] do
begin
anlex;
factor;
end;
end;
begin (*expresie*)
if cl in [plus,minus] then anlex;
termen;
while cl in [plus,minus] do begin
anlex;
termen;
end;
end; (*expresie*)
procedure conditie;
begin
if cl=oddcl then begin
anlex;
expresie;
end
else begin
expresie;
if not(cl in [eq,ne,lt,le,gt,ge])then eroare(16);
else begin
anlex;
expresie;
end;
end;
end;
begin (*instructiune*)
if cl=ident then begin
i:=pozitie(id);
if i=0 then eroare(12); (*id nedeclarat*)
if ts[i].fel <>variabila then eroare(17);
anlex;
if cl=atrib then anlex else eroare(18);
expresie;
end
else if cl=callcl then begin
anlex;
if cl<>ident then aroare(19) else begin
i:=pozitie(id);
if i=0 then eroare(12)
61
else if ts[i].fel<>procedura then eroare(20);
anlex;
end;
end
else (*not callcl*)
if cl=begincl then begin
anlex;
instructiune;
while cl=punctvirg do begin
anlex;
instructiune; (* descendenta recursiva*)
end;
if cl=endcl then anlex else eroare(21);
end
else if cl=ifcl then begin
anlex;
conditie;
if cl=thencl then anlex else eroare(22);
instructiune; (* descendenta recursiva*)
if cl=elsecl then begin
anlex;
instructiune;
end;
end
else if cl=whilecl then begin
anlex;
conditie;
if cl=docl then anlex else eroare(23);
instructiune;
end
else eroare(24);
end; (* instructiune *)
begin (* bloc *)
if cl=constcl then
begin
anlex;
declconst;
while (cl=virgula) do
begin
anlex;
declconst;
end;
if cl=punctvirg then anlex else eroare(25);
end;
if cl=varcl then
begin
anlex;
declvar;
while (cl=virgula) do
begin
anlex;
declvar;
end;
if cl=punctvirg then anlex else eroare(25);
end;
62
while cl=procedurecl do
begin
anlex;
if cl=ident then
begin
intrare(procedura);
anlex;
end
else eroare(26);
if cl=punctvirg then anlex else eroare(25);
bloc(tx);
if cl=punctvirg then anlex else eroare(25);
end (*while*)
instructiune;
end; (*bloc*)
w$ tampon de intrare
$
tabela de ASPN M
63
- Tabela de ASPN are liniile etichetate cu neterminalele gramaticii şi coloanele etichetate
cu simbolurile terminale. O intrare în tabela este de forma: M[A,a], unde A este
neterminal, a este terminal, iar M[A,a] poate fi o producţie sau poate fi intrare vidă.
- ‘Ieşire’ reprezintă acţiunile ce se execută la recunoaşterea unei producţii, iar aceste
acţiuni pot fi mesaje de eroare în cazul intrărilor vide, arbori sintactici, cod virtual, etc.
Analizorul va semnala la ieşire recunoaşterea şirului din intrare.
S
$
tamponul de intrare : conţine şirul din intrare terminat de simbolul $
w $
64
Presupunem cunoscută tabela de analiză sintactică predictiv nerecursivă pentru gramatica dată:
Simbol de intrare
Neterm./Term id + * ( ) $
E E->TE' - - E->TE' - -
E' - E'->+TE' - - E'->ε E'->ε
T T->FT' - - T->FT' - -
T' - T'->ε T'->*FT' - T'->ε T'->ε
F F->id - - F->(E) - -
Acţiunile analizorului materializate prin producţii tipărite urmăresc o derivare stânga a şirului
de intrare. Simbolurile de intrare care au fost deja baleiate, urmate de simbolurile gramaticale din stivă
(de la vârf spre bază) sunt forme propoziţionale stânga din derivare.
65
1. dacă X este terminal atunci prim(X)={X} (iniţializare)
2. dacă X poate fi derivat în ε atunci prim(X)=ε
3. dacă X este un neterminal şi X->Y1Y2.. Yk, atunci, prim(Y1) se include cu certitudine în
prim(X) mai puţin eventual ε. Dacă ε ∈ prim(Y1) atunci se include în prim(X) şi prim(Y 2),etc., altfel
calculul se opreşte.
Mulţimile prim pentru neterminalele gramaticii pe baza regulilor de mai sus sunt:
prim(E)={ id , ( }
prim(E’ ) ={ + , ε}
prim(T) = { id , ( }
prim(T’ ) = { * , ε}
prim(F) = { id, ( }
Etapele construirii acestei mulţimi sunt:
E E’ T T’ F
+ * (, id iniţializare
ε regula E’-> ε
(, id regula T->FT’
ε regula T’-> ε
(, id regula E->TE’
Calculul mulţimii urm pentru neterminalele gramaticii se realizează prin aplicarea repetată a
următoarelor reguli până când nu se mai pot adauga noi simboluri.
1 Marcajul $ al sirului de intrare se adauga la urm(S). (iniţializare)
2 Dacă în gramatica există o producţie de forma A -> αBβ, atunci simbolurile din prim(β) mai putin ε
aparţin lui urm(B)
3.Dacă în gramatica există o producţie de forma A -> αB sau o producţie A -> αBβ cu ε ∈ prim(β),
atunci adaugă urm(A) la urm(B).
Pentru exemplul dat avem următoarele etape ale construirii acestei mulţimi:
E E’ T T’ F
$ iniţializare
+ regula E’ -> +T E’
) regula F->(E)
* regula T’ -> * F T’
$,) regula E->TE’
$,) regula E’ -> +T E’
$,),+ regula T->FT’
$,),+ regula T->FT’
$,) $,) +,$,) $,),+ *,$ , ),+ Final
66
- Deoarece T -> F T’ , se adaugă urm(T) la urm T', adică $,),+
- Deoarece ε∈ prim(T') şi T’ -> * F T’ se adaugă urm(T') la urm F, adică $,),+
id + * ( ) $
urm (E) false false false false true true
urm (E’) false false false false true true
urm (T) false true false false true true
urm (T’) false true false false true true
urm (F) false true true false true true
urm(E ) = { $ , ) } urm(T’ ) = { + , ) , $ }
urm(E’) = { $ , ) } urm(F ) = { * , +, ) , $ }
urm(T ) = { + , ) , $ }
Adică:
Pentru fiecare A->α execută:
Pentru fiecare a ∈ prim(A) adaugă A->α la M[A,a]
if ε ∈ prim(α) then
Pentru fiecare b ∈ urm(A) adaugă A->α la M[A,b]
Intrările rămase necompletate vor fi vide şi reprezintă intrări de eroare.
67
Simbol de intrare
Neterminal id + * ( ) $
E E->TE' - - E->TE' - -
E' - E'->+TE' - - E'->ε E'->ε
T T->FT' - - T->FT' - -
T' - T'->ε T'->*FT' - T'->ε T'->ε
F F->id - - F->(E) - -
M[E,id] = E->TE’ , M[E,(] = E->TE’ prin urmare ‘id’ şi ‘(‘ aparţin lui prim(T)
unde:
1= <instr> -> if <expr> then <instr> <altern>
2=<altern> -> else <instr>
<altern> -> ε
Elementul M[<altern>, else] este dublu definit deoarece Urm(<altern>)={else,$}. Cauza este
ambiguitatea gramaticii manifestată la alegerea producţiei ce trebuie aplicată în momentul apariţiei lui
else în intrare, cunoscută ca problema ataşării lui else. Pentru a ataşa else cu cel mai apropiat then care
îl precede alegem producţia <altern> -> else <instr>.
Definiţie: Gramaticile independente de context care permit o analiză descendentă fără reveniri
folosindu-se eventual de următoarele k simboluri din intrare se numesc gramatici LL(k).
Primul L semnifică faptul că preluarea simbolului din intrare se face de la stânga la dreapta,
iar al doilea L faptul că la analiză se doreşte reconstituirea unei derivări stânga. În practică se utilizează
de obicei cazul când k=1, caz în care pentru analiză se foloseşte un singur simbol din intrare, cel
curent.
Observaţie: Aplicând algoritmul de ASPN prezentat pentru o gramatică LL(1), nu rezultă intrări
multiplu definite.
Aplicând algoritmul de construire a tabelei pentru ASPN în cadrul gramaticii LL(1) rezultă o
singură tabelă care va recunoaşte forme propoziţionale corecte din gramatica L(G) şi doar pe acestea,
deci metoda este corectă şi completă. Nici o gramatică ambiguă sau recursivă la stânga nu poate fi
LL(1).
68
Definiţie: O gramatică independentă de context este LL(1) dacă pentru orice neterminal al gramaticii
sunt îndeplinite următoarele două condiţii:
• prim(αi) ∩ prim(αj) = mulţimea vidă, oricare ar fi i,j în intervalul [1,n] cu i<>j;
adică: luând în considerare oricare două părţi drepte, nu se pot deriva din ambele şiruri care
încep cu acelaşi terminal, sau eventual ε
• dacă ε ∈ prim(αi) oricare ar fi i în intervalul[1,n], atunci urm(A)∩prim(αj) este mulţimea
vidă, unde 1<=j=<=n, i<>j; cu alte cuvinte: dacă dintr-o anumită parte dreaptă se poate
deriva şirul vid, atunci din oricare din celelalte părţi drepte nu se pot deriva şiruri care să
înceapă cu un terminal.
69
4. dacă neterminalul din vârful stivei poate genera şirul vid atunci producţia din care derivă ε
se poate aplica implicit în caz de eroare iar prin expandarea neterminalului respectiv la şirul vid el va fi
extras din stivă. Procedând astfel se va întârzia detectarea erorilor fără a se omite cuvintele cheie. Prin
această regulă se va reduce numărul de neterminale care trebuie considerate la tratarea erorilor.
5. dacă terminalul din vârful stivei nu coincide cu terminalul din intrare se elimină terminalul
din vârful stivei şi se afişează un mesaj de eroare. Prin urmare, mulţimea atomilor de sincronizare
pentru terminale este formată din toate celelalte terminale.
simbol de intrare
Netermi- id + * ( ) $
nale
E E’->TE’ - - E->TE’ - Sinc
E’ - E’->e TE’ - - E’ ->ε E’ ->ε
T T->FT’ sinc - T->FT’ sinc Sinc
T’ - T’ ->ε T’->*FT’ - T’ ->ε T’ ->ε
F F->Id sinc sinc F->(E) sinc Sinc
Intrările de sincronizare au fost completate aplicând regula 1 pe baza simbolurilor din mulţimea
următoare.
Analiza sintactică în caz de eroare decurge astfel:
1. în vârful stivei se află neterminalul A:
* se va căuta dacă M[A,a] este vid, atunci se va omite simbolul curent din intrare, a
*dacă M[A,a] conţine atom de sincronizare atunci se va extrage A din vârful stivei şi avansăm
în intrare până când simbolul curent este prezent în mulţimea sinc
2. dacă în vârful stivei este un terminal care nu coincide cu simbolul de intrare curent, atunci
se va omite simbolul din vârful stivei, conform regulii 5
Analiza de mai sus a neglijat problema mesajelor de eroare, care rămâne totuşi importantă
pentru proiectarea compilatorului şi trebuie şi ea tratată.
Un alt mod de tratare a erorilor care se poate aplica este revenirea la nivel de propoziţie.
În acest caz intrările vide din tabela de analiză se completează cu pointeri spre rutine de
tratare a erorilor. Aceste rutine acţionează de obicei asupra simbolului curent din intrare şi în principiu
pot schimba simbolul curent din intrare cu altul, îl şterg sau inserează un simbol nou (cerut de situaţia
din stivă) şi emit în paralel şi un mesaj de eroare. Se pot concepe rutine de tratare care să acţioneze şi
asupra stivei, cu precizarea că eliminarea sau inserarea unui simbol în stivă strică procesul de derivare
şi poate să conducă la situaţii în care şirul final nu corespunde nici unei porpoziţii sau derivări din
limbaj. De asemenea, există pericolul de a intra în ciclu infinit. Acesta se poate evita cu certitudine
70
dacă se adoptă măsura ca în urma oricărei acţiuni de revenire să se avanseze cel puţin un simbol în
intrare, iar dacă intrarea s-a epuizat, atunci să se avanseze în stivă.
Se va prezenta un tip general de analizor numit “cu deplasare şi reducere”. Acesta încearcă să
construiască arborele sintactic pentru şirul de intrare începând de la frunze spre rădăcină (ascendent).
- Procesul poate fi privit ca o reducere a unui şir de terminale la simbolul de start al gramaticii.
- În fiecare pas al reducerii se caută în forma propoziţională curentă localizarea unui subşir
care să corespundă părţii drepte a unei producţii. Acest subşir se înlocuieşte cu partea stângă
a producţiei respective. Dacă subşirul este corect ales, la fiecare pas se va parcurge în sens
invers o derivare dreapta.
8.1. Capete
Un capăt al unui şir este un subşir care corespunde părţii drepte a unei producţii şi a cărui
înlocuire cu neterminalele din partea stângă a producţiei reprezintă un pas în parcurgerea în sens invers a
unei derivări dreapta.
Nu toate subşirurile β care sunt părţi drepte ale unei producţii A->β şi care pot fi localizate ca
subşiruri ale unei forme propoziţionale sunt în acelasi timp şi capete pentru că este posibil ca prin
reducerea lui β la A procesul de reducere să se blocheze fără să se poată face reducerea la simbolul de start
al gramaticii.
Exemplu: S=*> α A ω => α β ω; capătul este format din producţia A-> β şi din propoziţia care urmează
lui α.
Observaţie: şirul ω, situat în dreapta capătului conţine numai terminale.
În cazul în care gramatica este ambiguă, şirul αβω s-ar putea obţine prin mai multe derivări
dreapta. Rezultă că el ar putea să conţină mai multe capete (deci ambiguitatea e o problemă).
Dacă gramatica nu este ambiguă, capătul corespunzător unei anumite forme propoziţionale este
unic. Grafic, procesul de reducere al unui capăt se poate reprezenta astfel:
a A B e
A B c
71
Capătul reprezintă cel mai din stângă subarbore complet, format dintr-un nod şi toţi fiii săi.
Părintele (în cazul nostru A) este nodul cel mai de jos şi cel mai din stânga având toţi fiii prezenţi în arbore
sub formă de frunze. Reducerea unui capăt la partea stângă a producţiei respective se numeşte fasonarea
capătului şi constă din îndepărtarea fiilor lui A din arborele sintactic.
Fasonarea capetelor se poate considera şi ca procesul de parcurgere în sens invers a unei derivări
dreapta. Se porneşte de la un şir ω de analizat. Dacă ω aparţine limbajului descris de gramatica dată,
atunci el se poate obţine dintr-un pas oarecare n al unei derivări dreapta încă necunsocute, dar care are
următoarea formă:
S =d> γ0 =d> γ1 =d> … =d> γn =d> ω
Reconstituirea în sens invers a acestei derivări constă în localizarea în forma propoziţională γn a
unui capăt βn şi înlocuirea lui cu partea stângă a producţiei. A n -> βn, obţinându-se astfel forma
propoziţională γn-1. Apoi se continuă procedeul cu γn-1 etc. Dacă în final s-a ajuns la simbolul de start al
gramaticii, înseamnă că procesul de analiză s-a încheiat cu succes.
Pentru implementarea analizei sintactice bazată pe fasonarea capetelor trebuiesc rezolvate două
probleme:
1. Localizarea subşirului care urmează să fie redus în forma propoziţională curentă
2. În cazul în care gramatica are mai multe producţii cu aceeaşi parte dreaptă, trebuie să decidem care
este producţia ce se va aplica.
72
Ca structură de date de bază se poate utiliza o stivă în care se vor deplasa simbolurile gramaticale
din tamponul de intrare şi se vor localiza capetele.
- Tamponul de intrare conţine şirul de analizat ω.
- Baza stivei şi respectiv extremitatea dreaptă a şirului de intrare vor fi marcate printr-un simbol
special ‘$’.
- La început stiva este goală, conţine doar simbolul ‘$’ iar la intrare şirul este ω$.
- La fiecare pas al analizei, analizorul deplasează în stivă 0 sau mai multe simboluri de intrare,
până în momentul în care în vârful stivei apare un capăt β, apoi se reduce β la partea stângă a
producţiei respective şi se continuă ciclic aceste operaţii, fie până când stiva conţine doar
simbolul de start şi intrarea este goală, fie până când se detectează o eroare.
- În configuraţia finală, stiva va conţine ‘$S’ iar în intrare vom avea simbolul ‘$’. Dacă se
ajunge în această configuraţie fără eroare, se va semnala terminarea cu succes a analizei.
Exemplu:
stiva intrare actiune
$ id1 + id2 * id3 $ deplasare
$ id1 + id2 * id3 $ reducere E->id
$E + id2 * id3 $ deplasare
$E+ id2 * id3 $ deplasare
$ E + id2 * id3 $ reducere E -> id
$E+E * id3 $ deplasare
$E+E* id3 $ deplasare
$ E + E * id3 $ reducere E -> id
$E+E*E $ reducere E -> E * E
$E+E $ reducere E -> E + E
$E $ succes
Observaţie: utilizarea stivei ajută la localizarea capătului pentru că el va fi situat întotdeauna în varful
stivei.
Modalitatea concretă a alegerii uneia dintre acţiuni depinde de tipul concret de analiză, care poate fi de
mai multe feluri:
- bazată pe precedenţa operatorilor
- de tipul left-right
Mulţimea prefixelor formelor propoziţionale dreapta care pot să apară în stiva unui analizor
sintactic cu deplasare şi reducere se numesc prefixe viabile.
Altfel spus, un prefix viabil este un prefix al unei forme propoziţionale dreapta care nu continuă
dincolo de extremitatea dreaptă a celui mai din dreapta capăt al acelei forme propoziţionale.
Consecinţa acestei definiţii este aceea că întotdeauna este posibil să se adauge simboluri
terminale la extremitatea unui prefix viabil pentru a obţine o formă propoziţională dreapta.
Pe parcursul analizei sintactice nu va apare nici o eroare atâta timp cât porţiunea de intrare văzută
până la un anumit punct poate fi redusă la un prefix viabil.
73
8.5. Conflicte în timpul analizei sintactice cu deplasare şi reducere
Observaţie: - Gramaticile în care nu pot apare astfel de conflicte se numesc gramatici LR.
- Clasa gramaticilor pentru care pot să apară astfel de conflicte nu se incadrează în clasa
LR.
Exemplu:
Gramatica instrucţiunii if , care este ambiguă nu este LR.
<instructiune> -> if <expr> then <instr> |
if <expr> then <instr> altceva
stiva intrare
if <expr> then <instr> else … $
Pentru configuraţia dată, nu se poate decide dacă vârful stivei este un capăt sau nu. Avem deci un
conflict deplasare-reducere, întrucât în funcţie de ceea ce urmează în intare, poate fi corect să se reducă if
<expr> then <instr> la <instr> sau ar putea fi corect să se deplaseze else din intrare în stivă , urmat de
căutarea unei alte <instr>, pentru ramura de else.
Deoarece nu se poate lua decizia pe baza unui singur simbol de anticipare, se spune că gramatica
nu este LR1.
În general, nici o gramatică ambiguă (ca cea de sus) nu poate să fie LRk, oricare ar fi k ∈ N*.
Analizorul sintactic cu deplasare-reducere poate fi uşor adaptat pentru a analiza şi gramatici
ambigue ca cea de mai sus prin decizia ca orice conflict de tipul deplasare-reducere să se rezolve în
favoarea deplasării. Se observă că luând această decizie în situaţia de mai sus, analiza decurge corect.
O altă situaţie pentru gramaticile non LR este atunci când se localizează cu certitudine un capăt,
dar conţinutul stivei şi simbolul curent din intrare nu sunt suficiente pentru a determina care producţie
trebuie utilizată pentru reducere (conflict reducere-reducere).
Exemplu: dispunem de un analizor lexical care furnizează atomul lexical id pentru orice
identificator, indiferent de utilizarea acestuia (cazul normal). Presupunem că în limbajul ales apelurile de
proceduri pe de o parte şi referirea elementelor de tablou pe de altă parte, au aceeaşi formă sintactică, adică
un nume şi argumentele între paranteze. Deoarece din punct de vedere semantic traducerea indicilor în
referinţe de tablou diferă substanţial de traducerea argumentelor la apelurile de proceduri şi funcţii, trebuie
să utilizăm producţii diferite pentru a genera listele de indici şi respectiv listele de argumente, ca în
gramatica următoare:
74
Considerăm o instrucţiune care începe cu a(i,j) şi presupunem că am ajuns în situaţia în care
primii trei atomi au fost transferaţi în stivă.
stiva intrare
… ( id id )… $
Este evident că id din vârful stivei trebuie redus, dar nu se ştie care dintre producţii trebuie
utilizată.
Dacă ‘a’ este procedura, ar trebui aleasă pentru reducere producţia (5), iar dacă a este element de
tablou, trebuie aplicată producţia (7).
Pentru a lua o decizie, ar trebui consultată tabela de simboluri (dacă acolo informaţiile sunt
completate).
O soluţie pur sintactică la această situaţie constă în modificarea analizorului lexical astfel încât să
furnizeze un atom distinct (procid) când ‘a’ este un nume de procedură.
Cu modificarea propusă, pentru cazul când în situaţia de mai sus este un apel de procedură,
conţinutul stivei şi al intrării este următorul:
stiva intrare
procid ( id , id ) … $
Astfel, devine clar că reducerea lui id se face prin (5).
Observaţie:
- Decizia privind reducerea este luată pe baza simbolului al treilea de sub vârful stivei
(procid) care nici măcar nu participă la reducere.
- Rezolvarea conflictelor reducere-reducere pe baza modificărilor de gramatică este metoda
generală de rezolvare a acestei situaţii
(1) <instr> -> procid ( <lista_param>)
Acest tip de analiză sintactică se poate aplica doar la o clasă redusă de gramatici, dar este
importantă datorită răspândirii ei şi are avantajul că analizorul se poate construi uşor manual.
Printre alte cerinţe esenţiale, gramatica la care se aplică acest tip de analiză, trebuie să aibă
urmatoarele două proprietaţi:
1. Să nu aibă producţii vide
2. În nici o producţie să nu existe o parte dreaptă care să conţină două neterminale adiacente.
Gramaticile care respectă condiţia 2 se numesc gramatici de operatori
E -> EAE | (E) | -E | id
unde A poate fi:
A -> + | - | * | / | ^
Dacă se substituie A atunci se obţine
E -> E + E | E – E | E * E | E / E | E ^ E | (E) | -E | id
Acest mod de transformare este general şi se va urmări cu precădere.
O gramatică de operatori nu trebuie neapărat să fie o gramatică de expresii.
Metoda are şi importante dezavantaje
- sunt dificil de prelucrat atomi care au 2 precedenţe diferite (de ex. semnul ‘-‘)
- Relaţia dintre analizor şi gramatică nu este întotdeauna biunivocă; este posibil ca analizorul să
accepte şiruri (considerându-le corecte), care nu fac parte din limbajul gramaticii (deci nu se
semnalează toate erorile).
- Clasa de gramatici acceptate este redusă
Datorită simplităţii ei, această tehnică s-a utilizat în multe compilatoare existente pentru analiza
expresiilor, în timp ce instrucţiunile limbajului şi celelalte construcţii de nivel înalt sunt analizate prin alte
metode (de exemplu cu descendenţi recursivi). Există totuşi şi compilatoare bazate complet pe această
tehnică.
75
În analiza sintactica bazată pe precedenţa operatorilor se definesc 3 relaţii de precedenţă disjuncte:
<⋅, =, ⋅> care se stabilesc între anumite perechi de terminale ale gramaticii. Pe baza acestor relaţii se
selectează capătul formei propoziţionale în stiva analizorului. Semnificaţia lor este următoarea:
relatia semnificaţia
a <⋅ b a cedează precedenţa lui b
a=b a are aceeaşi precedenţă cu b
a ⋅> b a are precedenţă faţă de b
Exemplu: se consideră şirul de intrare $ id + id * id $ şi matricea de relaţii de precedenţă redusă de mai jos:
76
id + * $
id > > >
+ < > < >
* < > > >
$ < < <
Observaţie: deoarece neterminalele nu influienţează analiza, nu trebuie făcută distincţie între ele.
În stiva analizorului este suficient să se ţină un singur marcaj reprezentativ pentru orice fel de
terminal pentru a marca doar locul din stivă corespunzator acelui neterminal, folosit pentru a înregistra
atributele semantice corespunzătoare neterminalului respectiv.
Aparent, din exemplul de mai sus s-ar putea deduce că pentru determinarea capătului ar fi
necesară baleierea întregii forme propoziţionale sau, în orice caz, a unei mari porţiuni din ea. Acest lucru
nu este însă necesar, deoarece implementarea concretă se face tot pe baza mecanismului de analiză
sintactică cu deplasare şi reducere, iar relaţiile de precedenţă sunt utile doar pentru a dirija acţiunile
analizorului (deplasare sau reducere). Acţiunile analizorului sunt cele cunoscute :
1. Deplasare: cât timp nu s-a găsit extremitatea dreaptă a capătului, adică, între simbolul terminal cel mai
apropiat de vârful stivei şi simbolul curent din intrare este valabilă una din realaţiile: <⋅ sau =.
2. Reducere: când s-a găsit extremitatea dreaptă a capătului, adică între terminalul cel mai propiat din
vârful stivei şi simbolul de intrare există relaţia ⋅>, se baleiază în sens invers stiva până la întâlnirea
primului marcaj <⋅, după care se face reducere.
3. Acceptare: când ambele simboluri care se compară ( adică vârful stivei şi simbolul curent din intrare)
sunt ‘$’
4. Eroare: dacă se compară 2 simboluri între care nu există relaţie de precedenţă.
Aceste idei privind mecanismul de analiză sintactică bazată pe precedenţa operatorilor sunt cuprinse în
următorul algoritm:
Algoritmul primeşte la intrare şirul w de analizat şi matricea relaţiilor de precedenţă. Dacă w este
corect, se va obţine la ieşire un schelet al arborelui său sintactic, în caz contrar un mesaj de eroare,
datorită substituirii intermediare a neterminalului, motiv pentru care nodurile interioare vor fi
uniforme.
stiva intrare
$ w$
(1) * se poziţionează pointerul de intrare pe primul simbol din w
(2) repeat forever
if *atât simbolul din vârful stivei cât şi simb. curent de intrare sunt $ then return
else
begin
*fie ‘a’ simbolul terminal cel mai apropiat de vârful stivei,
‘b‘ simbolul curent din intrare
if (a <⋅ b) or ( a=b) then
begin {deplasare}
* introdu b în stivă
77
* avansează cu o poziţie pointerul de intrare
end
else if (a ⋅>b) then {reducere}
repeat
* extrage din stivă
until *terminalul din vârful stivei este în relaţie <⋅ cu terminalul cel mai
recent extras
else *eroare
end;
Singura cerinţă care trebuie avută în vedere este aceea ca relaţia de precedenţă să conducă la
analiza corectă a limbajului defninit de gramatică.
Ţinând cont de faptul că analiza sintactică bazată pe precedenţa operatorilor se aplică la gramatici
pentru expresii, sau gramatici similare cu acestea, iar între operatorii din expresii există reguli de prioritate
şi asociativitate riguroase care rezolvă eventualele ambiguităţi, aceste reguli pot reprezenta baza pentru
stabilirea relaţiei de precedenţă.
Pentru cazul operatorilor binari, notaţi cu θ, θ1, θ2 relaţiile de precedenţă pot fi deduse astfel:
1. dacă θ1 este mai prioritar algebric decât θ2, se introduc în tabelă relaţiile
θ1 ⋅> θ2 şi θ2 <⋅ θ1.
2. dacă θ1 şi θ2 au prioritate algebrică egală (inclusiv când reprezintă acelaşi operator) apar
următoarele 2 situaţii:
a. θ1 şi θ2 sunt asociativi la stânga
θ1 ⋅> θ2 şi θ2 ⋅> θ1
b. θ1 şi θ2 sunt asociativi la dreapta
θ1 <⋅ θ2 şi θ2 <⋅ θ1
id ( şi )
θ <⋅ id θ ⋅> )
id ⋅> θ ) ⋅> θ
θ <⋅ ( θ ⋅> $
( <⋅ θ $ ⋅> $
Pentru a asigura reducerea la E a lui id şi a lui (E), între terminalele care nu sunt operatori se
introduc următoarele relaţii:
(=) $ <⋅ ( $ ⋅ id
( <⋅ ( id ⋅> $ ) <⋅ $
( <⋅ id id ⋅> ) ) ⋅> )
Exemplu:
Se consideră gramatica:
E -> E + E | E – E | E * E | E / E | E ^ E | ( E ) | id
Operatorii au următoarele proprietăţi:
78
^ cel mai prioritar, asociativ la stânga
*,/ asociativi la stânga
+.- cea mai mică prioritate, asociativi la stânga
Pe baza relaţiilor de precedenţă de mai sus, rezultă următoarea matrice de precedenţe:
+ - * / ^ id ( ) $
+ ⋅> ⋅> <⋅ <⋅ <⋅ <⋅ <⋅ ⋅> ⋅>
- ⋅> ⋅> <⋅ <⋅ <⋅ <⋅ <⋅ ⋅> ⋅>
* ⋅> ⋅> ⋅> ⋅> <⋅ <⋅ <⋅ ⋅> ⋅>
/ ⋅> ⋅> ⋅> ⋅> <⋅ <⋅ <⋅ ⋅> ⋅>
^ ⋅> ⋅> ⋅> ⋅> <⋅ <⋅ <⋅ ⋅> ⋅>
id ⋅> ⋅> ⋅> ⋅> ⋅> ⋅> ⋅>
( <⋅ <⋅ <⋅ <⋅ <⋅ <⋅ <⋅ =
) ⋅> ⋅> ⋅> ⋅> ⋅> ⋅> ⋅>
$ <⋅ <⋅ <⋅ <⋅ <⋅ <⋅ <⋅
79