Sunteți pe pagina 1din 21

2. Analiza lexicală

2.1. Funcţiile unui analizor lexical

Analiza lexicală este prima fază a procesului de compilare, fiind efectuată de analizorul lexical.

Analizorul lexical are la intrare programul sursă şi produce la ieşire atomi lexicali. (figura 2.1).

Programsursă

la ieşire atomi lexicali . (figura 2.1). Programsurs ă Analizor lexical Atomi lexicali Figura 2.1. Funcţia

Analizor lexical

lexicali . (figura 2.1). Programsurs ă Analizor lexical Atomi lexicali Figura 2.1. Funcţia unui analizor lexical

Atomi lexicali

Figura 2.1. Funcţia unui analizor lexical

În multe privinţe, analiza lexicală este faza cea mai simplă a procesului de compilare. În ciuda acestei simplităţi însă, ea are la bază elemente teoretice şi procedee practice necesare pentru scrierea unui software performant.

În esenţă, analizorul lexical preia şirul continuu de caractere de la intrare şi îl divide în cuvinte, care pot fi recunoscute de analizorul sintactic, care se execută în următoarea fază a procesului de compilare.

Exemple de atomi sunt: identificatori (nume de variabile folosite de programator), cuvinte cheie (for, while, begin, end etc.), numere şi secvenţe de caractere speciale.

Se numeşte lexemă, un şir de caractere de la intrare care este în curs de analizare. Iniţial, lexema curentă este şirul vid. La ea se adaugă pe rând caractere de la intrare, până când va corespunde cu un atom al limbajului, ori se va semnala eroare prin epuizarea tuturor posibilităţilor.

Atomii se împart în clase de atomi, iar pentru fiecare clasă se alocă un cod, astfel încât programul sursă se transformă în final într-un şir de coduri aranjate în ordinea detectării atomilor.

În plus, analizorul lexical elimină din textul sursă comentariile şi spaţiile (blankuri, tabulatori, comentarii, alte caractere de control). De asemenea, el numerotează liniile textului sursă pentru a permite raportarea de erori,detectează şi semnalează erori lexicale.

Deşi funcţiile analizorului lexical ar putea incluse în analizorul sintactic, în majoritatea aplicaţiilor, ele sunt faze distincte, în principal datorită faptului că analizorul lexical este mare consumator de timp. Din acest motiv, de multe ori, el se implementează în limbaj de asamblare, spre deosebire de celelalte faze ale compilării care se implementează în limbaje de nivel înalt.

Ieşirea analizorului lexical este mult mai condensată, deoarece şirul de caractere de la intrare este divizat în cuvinte, multe caractere redundante din punctul de vedere al compilării sunt eliminate, astfel încât analizorul sintactic va prelucra un volum de date mai redus.

De asemenea, bazele teoretice ale analizei lexicale sunt mai simple decât ale analizei sintactice, astfel încât în această fază, se pot folosi soluţii software mai puţin complexe.

Suplimentar, prin separarea fazelor, compilatorul poate fi scris modular şi realizat în echipă, iar capacitatea de reutilizare a codului creşte.

O privire de ansamblu asupra analizei lexicale este ilustrată în figura 2.2:

2

Program sursă

begin

x:=1;

Gestionare caractere
Gestionare
caractere

b

= 1

; \n

e

g i

n \n

x :

Analizor lexical
Analizor
lexical

cuv.cheie

id

atribuire

numar

 

x

:=

7

begin

Figura 2.2. Analiza lexicală

Pentru a parcurge textul sursă, se foloseşte un bloc de gestionare caractere, care citeşte intrarea şi o transmite linie cu linie la intrarea analizorului lexical. În continuare, analizorul lexical identifică atomii lexicali.

2.2. Codificarea atomilor lexicali.

Analizorul lexical împarte atomii în clase de atomi şi atribuie fiecărui atom detectat un cod.

Exemple de clase de atomi sunt: cuvinte cheie, identificatori, operatori, numere etc. Unele dintre aceste clase conţin un număr cunoscut de elemente (de exemplu clasa cuvintelor cheie, clasa operatorilor etc.), altele conţin un număr

3

posibil infinit de elemente (de exemplu identificatori, numere etc.).

Codul conţine două câmpuri: codul lexical şi un atribut.

Atributul conţine informaţii suplimentare:

este valoarea atomului, dacă acesta este de tip numeric

este adresa unde se memorează, în cazul în care este un identificator.

Codul lexical este un număr întreg care identifică atomul şi care este stabilit astfel:

1. Pentru clase de atomi cu un număr cunoscut de elemente, fiecare atom este asociat cu un număr distinct, care identifică complet atomul. În aceste cazuri prezenţa atributului nu este necesară.

De exemplu: pentru "begin"se poate asocia codul 100; pentru "while" codul 101 etc.

2. Pentru clase de atomi cu un număr posibil infinit de elemente, codul este un câmp care identifică clasa, el fiind acelaşi pentru toţi atomii clasei respective. Înm aceste cazuri, codul este urmat obligatoriu de un atribut.

De exemplu:

codul unui identificator se compune din codul clasei identificatorilor ,(de exemplu 200) şi o adresă care pointează spre tabela de simboluri;

codul unui număr întreg se compune din codul clasei numerelor întregi, (de exemplu 300) şi valoarea numărului.

Exemplul este ilustrat în figura 2.3.

4

cuvânt cheie

begin

clasa

identificator alfa

clasa

atribut

Atomul numeric 5

clasa

atribut

Cod lexical

100 200 adresa 300 5
100
200
adresa
300
5

Pointează spre

tabela de

simboluri unde

s-a memorat

atomul alfa

Figura 2.3. Exemplu de cod lexical

Codificarea atomilor lexicali are avantajul că, la intrarea analizorului sintactic, în loc de o secvenţă de şiruri de caractere cu lungime variabilă, se prezintă o codificare compactă a atomilor.

2.3. Tipuri de date folosite pentru definirea atomilor lexicali.

Atomii sunt definiţi de obicei folosind un tip enumerativ. De exemplu:

enum tip_atom {if, then, else, plus,

sau prin folosirea macrodefiniţiei #define:

#define if 156 #define then 257 #define else 258

5

};

Deoarece identificatorii şi atomii numerici sunt asociaţi cu un atribut care poate fi un pointer sau o valoare, se poate folosi fie o structură, fie o uniune:

struct inregistrare

{

tip_atom valoare; //codul atomului char * stringval; // atriburul identificatorului int numval; // atributul unui număr };

struct inregistrare

{

tip_atom valoare; union { char * stringval; int numval; } atribute; };

2.4. Comunicarea analizorului lexical cu celelalte componente ale compilatorului

Analizorul lexical comunică cu tabela de simboluri şi cu analizorul sintactic.

Comunicaţia cu tabela de simboluri presupune operaţii de scriere a codului pentru identificatori.

Pentru comunicaţia cu analizorul sintactic, analizorul lexical poate folosi unul din următoarele procedee:

1. Analiza lexicală se execută într-o trecere separată şi el produce la ieşire codul tuturor atomilor din programul sursă. Această ieşire este de fapt o formă codificată a programului sursă, care se poate scrie într-un fişier, ori se poate păstra în memorie (figura 2.4).

6

Program sursă Analizor Prima lexical trecere Coduri de atomi Fişier Memorie Următoarea Analizor trecere
Program sursă
Analizor
Prima
lexical
trecere
Coduri de
atomi
Fişier
Memorie
Următoarea
Analizor
trecere
sintactic

Figura 2.4. Trecere separată pentru analiza lexicală

3. Analizorul sintactic apelează analizorul lexical în momentul în care are nevoie de un nou atom. La fiecare apel, analizorul lexical trimite un singur atom la intrarea analizorului sintactic. Această implementare este mai avantajoasă, deoarece nu se mai produce o formă codificată a programului sursă (figura 2.5).

Analizor sintactic
Analizor
sintactic

Codul

gettoken()

atomului

următor

Program sursă
Program
sursă

Analizor

lexical

Figura 2.5. Analizorul sintactic apelează analizorul lexical

7

4. Cele două analizoare funcţionează în regim de corutină, fiind simultan active.

Soluţia preferată este situaţia în care analizorul lexical este apelat de analizorul sintactic, folosind de exemplu o funcţie an_lex. Analizorul lexical poate să returneze codul lexical al atomululi, iar atributele lui pot fi variabile globale, vizibile în alte părţi ale compilatorului.

sunt variabile

De exemplu, dacă stringval şi numval globale:

char * stringval; // global int numval; // global

şi presupunând că în textul sursă se află următoarea linie de program:

a[index] = 4 + 2

după primul apel al analizorului lexical, variabila globală stringval va conţine "a", se returnează codul
după primul apel al analizorului lexical, variabila globală
stringval va conţine "a", se returnează codul clasei
identificatorilor, iar pointerul de intrare va avansa:
a
[
i
n
d
e
x
]
=
7
+
2
Pointerul de intrare

Figura 2.6. Poziţionarea pointerului pe următorul atom

Un fişier header an_lex.h, destinat unui analizor lexical, poate conţine următoarele:

typedef enum { nume, numar, acol_l, acol_r, par_l, par_r, atrib,

8

pct_v, plus, minus, eroare } tip_atom; typedef struct

{

tip_atom tip;

union

int valoare; /* tipul numar */ char * nume; /* tipul identificator */ }atribut;

{

}

atom;

extern atom an_lex(); /*functia an_lex() e definita

altundeva)*/

2.5. Construirea unui analizor lexical.

Analizorul efectuează următoarele acţiuni:

recunoaşte atomii lexicali;

produce la ieşire codul atomilor şi tributul lor;

generează eroare în caz de eşec;

scrie datele colectate în această fază (identificatori, constante) în tablela de simboluri.

Un analizor lexical poate fi construit manual, ori folosind metoda automatelor finite.

Indiferente de metdoa folosită, construirea analizoarelor lexicale are la bază gramatici regulate.

Construirea manuală a analizorului lexical înseamnă scrierea programului propriu zis pe baza unor diagrame de tranziţii, care precizează structura atomilor din textul sursă. Aceste diagrame reprezintă automate cu stări finite. Metoda 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.

9

Există numeroase instrumente software care automatizează proiectarea unui analizor lexical. Acestea se numesc generatoare de analizoare lexicale. În prezent, rareori se scriu manual analizoare lexicale.

Un generator de analizoare lexicale este un program care primeşte la intrare, într-un limbaj de specificare, structura atomilor lexicali şi eventualele acţiuni semantice care vor trebui executate simultan cu analiza lexicală. Ieşirea unui astfel de program este un program de analiză lexicală.

2.6. Construirea manuală a unui analizor lexical

Un

analizor

lexical

manual

următoarele acţiuni:

trebuie

aibă

în

vedere

1. Eliminarea spaţiilor ( blankuri, tabulatori, CR, etc.);

2. Eliminarea comentariilor;

3. Colectare atomi şi identificarea clasei pentru fiecare atom;

4. Generarea atributelor şi scrierea în tabela de simboluri atunci când e cazul.

Dacă atomul id reprezintă un identificator, iar atomul num reprezintă un număr, analizorul lexical va clasifica o secvenţă de intrare de forma:

E

:=

M

*

astfel:

C ** 2

id, pointer la tabela de simboluri pentru E assign_op, := id, pointer la tabela de simboluri pentru M mult_op, * id, pointer la tabela de simboluri pentru C exp_op, ** num, 2

10

şi transformă această secvenţă în următorul şir de atomi:

id := id * id ** num

În continuare se prezintă un exemplu de pseudocod pentru un analizor lexical:

function alex

 

:

integer;

var

lexbuf

:

array [0

100]

of char;

 

c

:

char;

begin

loop begin citeste un caracter in c; if c este un blank sau tabulator then nici o actiune else if c este newline then lineno := lineno + 1 else if c este o cifra then begin set tokenval la valoarea cifrei si a cifrelor urmatoare; return NUM

end

else if c este o litera then begin pune c si urmatoarele litere sau/si cifre in lexbuf; p := lookup(lexbuf); if p = 0 then p := insert(lexbuf, ID); tokenval := p; return adresa din tabel p

end else /* atomul e un singur caracter */ set tokenval to NONE; /* nu sunt atribute*/ return codul intregului care codeaza caracterul c

end

end

Acest pseudocod nu face distinţie între identificatori şi cuvinte cheie. El se poate completa cu o acţiune de căutare într-

11

un tabel al cuvintelor cheie, care să permită recunoaşterea cuvintelor cheie.

2.6.1. Gestionarea textului sursă

Textul sursă se citeşte linie cu linie, iar pentru memorarea unei linii se foloseşte o zonă tampon (buffer), a cărui dimensiune corespunde cu lungimea fizică a liniei de intrare. Sfârşitul textului sursă este marcat de EOF.

Pentu a localiza un atom lexical (lexema curentă), se pot folosi doi pointeri, numiţi pointer de început p i şi pointer de anticipare p a .

La început, ambii pointeri p i şi p a indică primul caracter al lexemei curente. În continuare, pointerul p a avansează, până când analizorul identifică atomul. În acest moment, şirul de caractere cuprins între cei doi pointeri este chiar atomul lexical identificat.

După detectarea unui atom, pointerul de anticipare se poate poziţiona fie pe ultimul caracter al lexemei curente, fie pe primul caracter al lexemei următoare. În a doua situaţie nu mai este necesară returnarea la intrare a extracaterului necesar pentru recunoaşterea anumitor atomi.

După prelucrarea lexemei curente, p i este adus în aceeaşi poziţie cu pointerul de anticipare şi se continuă cu analiza unui nou atom.

În figura 2.7 se reprezintă felul în care se folosesc cei doi pointeri pentru identificarea unui atom.

12

b e g i n \n x : = 1 ; p i p a
b
e
g
i
n
\n
x
:
=
1
;
p i
p a
b
e
g
i
n
\n
x
:
=
1
;
p i
p a
b
e
g
i
n
\n
x
:
=
1
;
p i
p a

Figura 2.7. Folosirea a doi pointeri pentru identificarea unui atom

2.6.2. Construirea diagramelor de tranziţii

Pentru proiectarea unui analizor lexical manual se folosesc diagrame de tranziţii, care conţin:

O mulţime de stări,reprezentate cu cercuri;

Starea finală reprezentată cu linie dublă:

Arce care reprezintă tranziţiile analizorului dintr-o stare în alta. Arcele încep într-o stare şi se pot termina în aceeaşi stare, ori într-o altă stare;

Starea finală este starea în care s-a recunoscut un atom;

Arcele sunt etichetate cu simboluri, care indică ce caracter de la intrare determină trecerea analizorului din starea de la care porneşte arcul în starea în care ajunge acel arc.

13

Astfel de diagrame de tranziţii sunt asociate cu automate cu stări finite, care vor fi prezentate în capitolul următor. Deoarece reprezentarea este intuitivă, aceste diagrame se pot construi de fapt fără a cunoaşte teoria care stă la baza lor. Din acest motiv, în continuare se exemplifică felul în care se construies aceste diagrame de tranziţii în scopul implementării manuale a unui analizor lexical, urmând ca în capitolul 4 să fie reluate unele noţiuni mai în detaliu, pe măsură ce se definesc noţiuni teoretice noi.

Exemplu. Se construiesc diagramele de tranziţii care stabilesc funcţionarea unui analizor lexical manual care recunoaşte următorii atomi: numere întregi, identificatori, separatori: ";", paranteze:"(", ")", acolade: "{", "}", spaţii, operatori: "+", "-", "=".

Diagramele de tranziţii sunt reprezentate în figura 2.8.

Observaţii.

Stările iniţiale ale fiecărei diagrame sunt de fapt reunite într- o singură stare iniţială, referită în continuare ca "stare0". Aceasta semnifică faptul că nu s-a decis încă ce diagramă se va urma. Alegerea diagramei se face pe baza caracterului de la intrare.

Uneori, de exemplu pentru atomul "{", atomul e recunoscut imediat prin citirea ultimului caracter din acesta. Pentru alţi atomi însă, de exemplu pentru numar, se cunoaşte lungimea atomului numai după citirea unui extracaracter, care nu aparţine numărului (această situaţie apare în toate stările notate cu *). În acest caz, caracterul citit în plus trebuie returnat la intrare.

Dacă se citeşte un caracter care nu corespunde cu nici o secvenţă acceptată, se returnează atomul special eroare.

14

digit not(digit) 2 1 3* digit
digit
not(digit)
2
1
3*
digit
{ 4 5 } 6 7 letter not(letter|digit) 23 24 25*
{
4
5
}
6
7
letter
not(letter|digit)
23
24
25*

letter|digit

( 8 9 sp not(sp) 10 11 12*
(
8
9
sp
not(sp)
10
11
12*
sp ) 13 14 - 17 18 21 ; 22
sp
)
13
14
-
17
18
21
;
22
+ 15 16 = 19 20
+
15
16
=
19
20

Figura 2.8. Diagrame de tranziţii

15

2.6.3. Scrierea codului pentru diagramele de tranziţii

unui

analizor lexical se pot implementa fie prin instrucţiunea case, fie printr-o succesiune de instrucţiuni if.

Pentru fiecare stare se scrie câte o secvenţă de program distinctă.

Dacă starea nu este finală, adică există arce care ies din ea, se citeşte următorul caracter de la intrare, care va produce o tranziţie spre starea următoare. Tranziţia este posibilă numai în cazul în care, starea curentă conţine un arc de ieşire etichetat caracterul citit de la intrare. Dacă există un astfel de arc, se trece la secvenţa de program a stării următoare. Dacă nu există un astfel de arc, iar starea curentă nu este finală, se încearcă traversarea unei alte diagrame, folosind pointerul de început al lexemei curente. În cazul în care s-au epuizat toate posibilităţile, se apelează o procedură de eroare.

Secvenţa următoare prezintă o implementare manuală simplă a unui analizor lexical, scris pe baza diagramelor de tranziţii din figura 2.8.

Diagramele

de

tranziţii

care

descriu

funcţionarea

#include <stdio.h> #include <ctype.h> #include <stdlib.h> #include <string.h> #include "alex.h"

static int stare = 0;

#define maxbuf 256 static char buf[maxbuf];

static char

static char *token_nume[] =

*pbuf;

{

"nume", "numar", "acol_l", "acol_r",

16

"par_l", "par_r", "atrib", "pct_v", "plus", "minus", "error" }; static TOKEN token;

char *

dup_str ;

/* Acest cod nu e complet. Nu se testeaza depasirea bufferului, etc*/ TOKEN *alex()

{

char c;

while (1)

switch(stare)

{

case 0: /* pentru unul din 1,4,6,8,10,13,15,17,19,21,23 */ pbuf = buf; c = getchar(); if (isspace(c)) stare = 11; else if (isdigit(c))

{

*pbuf++ = c; stare = 2;

}

else if (isalpha(c))

{

*pbuf++ = c; stare = 24;

}

else switch(c)

{

case '{': stare = 5; break; case '}': stare = 7; break; case '(': stare = 9; break; case ')': stare = 14; break; case '+': stare = 16; break; case '-': stare = 18; break; case '=': stare = 20; break; case ';': stare = 22; break; default:

stare = 99; break;

}

break;

17

case 2:

c

= getchar(); if (isdigit(c)) *pbuf++ = c; else stare = 3; break;

case 3:

token.info.valoare= atoi(buf); token.type = numar; ungetc(c,stdin); stare = 0; return &token;

break;

case 5:

token.type = acol_l;

stare = 0; return &token;

break;

case 7:

token.type = acol_r; stare = 0; return &token;

break;

case 9:

token.type = par_l;

stare = 0; return &token;

break;

case 11:

c = getchar();

if (isspace(c));

else

stare = 12;

break;

case 12:

ungetc(c,stdin);

stare = 0;

break;

case 14:

token.type = par_r; stare = 0; return &token;

break;

case 16:

token.type = plus;

18

stare = 0; return &token; break; case 18:

token.type = minus; stare = 0; return &token; break; case 20:

token.type = atrib; stare = 0; return &token; break; case 22:

token.type = pct_v; stare = 0; return &token; break; case 24:

c = getchar(); if (isalpha(c)||isdigit(c)) *pbuf++ = c;

else stare = 25; break; case 25:

*pbuf = (char)0; dup_str= strdup(buf); token.info.nume =dup_str;

token.type = nume; ungetc(c,stdin); stare = 0; return &token; break; case 99:

if (c==EOF) return 0; fprintf(stderr,"Caracter ilegal: \'%c\'\n",c); token.type = error; stare = 0; return &token; break; default:

break; /* Nu se poate intampla */

}

}

int main()

19

{

TOKEN *t; while (((t=alex())!=0))

{

printf("%s",token_nume[t->type]);

switch(t->type)

{

case nume:

printf(":%s\n",t ->info.nume); break;

case numar:

printf(":%d\n",t ->info.valoare); break; default:

printf("\n");

break;

}

}

return 0;

}

Observaţie. Pentru scrierea (chiar şi manuală) a unor analizoare lexicale performante se folosesc noţiuni legate de expresiile regulate, care se vor prezenta în capitolul următor. Folosind expresiile regulate pentru a specifica cuvintele unui limbaj, se pot implementa diagramele de tranziţii care descriu funcţionarea unui analizor lexical manual.

Pentru generarea automată a analizoarelor lexicale se folosesc noţiuni teoretice suplimentare care vor fi descrise în capitolul 5.

20