Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Зубенко, Омельчук - Програмування. Поглиблений курс

.pdf
Скачиваний:
58
Добавлен:
07.03.2016
Размер:
5 Мб
Скачать
☆

Розділ IV. АЛГОРИТМИ

if(!*prog) return; /*кінець виразу*/ while(isspace(*prog)) ++prog; /*пропустити пробіли,

символи табуляції й порожнього рядка*/

if(strchr("+-*/%^=()", *prog)){ tokType=DELIMITER;

/*перейти до наступного символу*/ *temp++ = *prog++;

}

else if(isalpha(*prog)) { while(!isdelim(*prog)) *temp++ = *prog++; tokType=VARIABLE;

}

else if(isdigit(*prog)) { while(!isdelim(*prog)) *temp++ = *prog++; tokType=NUMBER;

}

*temp='\0';

}

/*Повернення значення ІСТИНА, якщо символ с є роздільником*/ int CSimpleParser::isdelim(char c)

{

if(strchr("+-/*%^=()", c)||c==9||c=='\r'||c==0) return 1;

return 0;

}

Розглянемо наведені вище методи докладніше. Після кількох ініціалі- зацій метод getToken() перевіряє, чи не досягнутий символ кінця ряд- ка ('\0'), який завершує вираз. Якщо у виразі ще є нерозібрана части- на, то метод getToken() спочатку пропускає пробіли, якщо вони є. Піс- ля цього prog указує на число, змінну, операцію або, якщо вираз заве- ршується пробілами, – то на символ кінця рядка ('\0'). Якщо черговий символ є операцією, то він повертається у вигляді рядка, збереженого в глобальній змінній token, а змінній tokType, що містить тип отрима- ної лексеми, присвоюється значення DELIMITER. Якщо ж наступний си- мвол є літерою, то він вважається іменем змінної й повертається в ря- дковій змінній token. При цьому tokType одержує значення VARIABLE. У випадку, коли черговий символ є цифрою, зчитується все число, при- чому воно міститься в змінній token, а його типом є NUMBER. Нарешті, якщо наступний символ не є жодним із перерахованих вище, то вва-

551

ПРОГРАМУВАННЯ

жається, що досягнутий кінець виразу. У цьому випадку token містить порожній рядок, повернення якого означає кінець виразу.

Як уже було сказано раніше, щоб не ускладнювати код цього мето- ду, були опущені деякі засоби контролю за помилками та зроблені де- які припущення. Наприклад, будь-який нерозпізнаний символ завер- шує вираз. Крім того, у даній версії програми імена змінних можуть мати будь-яку довжину, але значущою є тільки перша літера. Відпо- відно до вимог конкретного завдання можна ускладнити засоби кон- тролю за помилками й додати інші подробиці.

Для кращого розуміння принципу дії методу getToken() нижче на- ведені лексеми, що повертаються ним, і типи лексем для вхідного ви-

разу А * В–(W+10):

Лексема

Тип лексеми

A

 

VARIABLE

*

 

DELIMITER

B

 

VARIABLE

-

 

DELIMITER

(

 

DELIMITER

W

 

VARIABLE

+

 

DELIMITER

10

 

NUMBER

)

 

DELIMITER

0

(кінець рядка)

0(нуль)

Варто пам'ятати, що атрибут token завжди містить рядок, який за- вершується символом кінця рядка ('\0'), навіть якщо цей рядок скла-

дається тільки з одного символу ■ Після того, як у результаті роботи лексичного аналізу лексеми роз-

пізнано, інформація про них збирається й запам'ятовується в одній із кількох таблиць (таблиці ідентифікаторів). Зміст цієї інформації зале- жить від мови (у даному випадку – мови арифметичних виразів).

Таблиця ідентифікаторів (змінних) забезпечує можливість швидко-

го додавання нових ідентифікаторів, нових відомостей про них і швидкого пошуку інформації для даного ідентифікатора.

У нашому випадку для спрощення вважатимемо, що застосовують- ся лише однолітерні змінні латинського алфавіту без урахування регі- стра. Тому нам потрібна така таблиця ідентифікаторів, де кожна змінна зберігається в одному елементі масиву з 26 компонентами ти- пу double (у складнішому випадку для реалізації таблиці застосовують інші методи – зазвичай це хеш-таблиці). Отже, у вихідний текст ана- лізатора додамо такий фрагмент:

552

Розділ IV. АЛГОРИТМИ

double vars[26]; /*26 змінних, A-Z*/

Ці змінні первісно ініціалізуються 0.0 у конструкторі класу

CSimpleParser.

Крім цього, знадобиться метод для одержання значення заданої змін- ної. Оскільки імена змінних є літерами від А до Z, то їх можна викорис- тати для індексації масиву vars, віднімаючи код ASCII літери А від імені змінної. Нижче описано метод findVar(), що повертає значення змінної:

/*Одержання значення змінної*/

double CSimpleParser::findVar(char *s)

{

if(!isalpha(*s)){

serror(1); return 0.0;

}

return vars[toupper(*token)-'A'];

}

Цей метод приймає імена будь-якої довжини, але тільки перший символ є значущим. Дане обмеження за потреби можна змінити.

Розглянемо метод atom(), що обробляє числа та змінні:

/*Одержання значення числа або змінної*/ void CSimpleParser::atom(double *answer)

{

switch(tokType) { case VARIABLE:

*answer=findVar(token);

getToken();

return; case NUMBER:

*answer=atof(token);

getToken();

return;

default:

serror(0);

}

}

4.5.2. СИНТАКСИЧНИЙ АНАЛІЗ

Синтаксичний аналіз – це процес, в якому досліджується низка ле- ксем і встановлюється, чи відповідає вона граматичним правилам, що описують синтаксис мови. На виході парсер видає синтаксичне дерево й таблиці (ідентифікаторів, типів).

553

ПРОГРАМУВАННЯ

Зазвичай фази лексичного й синтаксичного аналізу взаємодіють між собою в межах одного проходження програми. При цьому основ- ною є фаза синтаксичного аналізу, який, у свою чергу, звертається до лексичного аналізу за черговим термінальним символом.

Теоретичні відомості про КВ- та LL(k)-граматики, побудову множи- ни FIRSTk (w) можна знайти в підрозд. 1.4.4.

Більшість відомих методів аналізу належать до одного з двох кла- сів, один з яких об'єднує низхідні, а інший – висхідні алгоритми. Похо- дження цих термінів пов'язане з тим, як утворюються вузли синтак- сичного дерева: від аксіом граматики до термінальних символів (низ- хідні парсери) чи від термінальних символів до аксіом граматики (ви-

східні парсери).

Знизхідними парсерами пов'язані LL-граматики, які можуть бути проаналізовані достатньо простим для реалізації методом рекурсив- ного спуску.

Звисхідними парсерами пов'язані LR(1)-граматики [7], що задають ширший клас мов (за їхньою допомогою визначається синтаксис більшо- сті мов програмування). Для них розроблені спеціальні програми автома- тичної побудови парсерів. Найвідоміші з них – утиліти Lex та Yacc [144].

Lex – програма для генерації лексичних аналізаторів. Вона є стан- дартним генератором лексичних аналізаторів у ОС Unix. Lex зчитує вхідний потік, що описує лексичний аналізатор, і повертає на виході код аналізатора мовою програмування C.

Yacc (від англ. Yet Another Compiler Compiler – ще один компілятор компіляторів) – програма для генерації парсерів у Unix-системах. Розро- блена С. Джонсоном у компанії AT&T. Вона отримує на вхід LR(1)- граматику, описану в нотації БНФ, і генерує код парсера мовою про- грамування C. Пізніше програма була переписана іншими мовами, та-

кими як Ada, Java, C# тощо. Існує Стандарт IEEE POSIX P1003.2, що визначає як функціональність, так і вимоги до програм типу Lex i Yacc.

4.5.3. ОБЧИСЛЕННЯ ВИРАЗІВ

Розглянемо складні арифметичні вирази, що описуються КВ-граматикою G (див. підрозд. 1.4.4), і на їхньому прикладі покажемо, як будується парсер і обчислюються значення виразів. Вираз потрібно спочатку проаналізувати й отримати синтаксичне дерево, потім шляхом рекурсивного обходу цього дерева обчислюється значення виразу.

Спочатку звернемося до граматики G1, що є частиною граматики G і задає в ній вирази без присвоювань. Граматика G1 є LL(1)-

554

Розділ IV. АЛГОРИТМИ

граматикою. Для синтаксичного аналізу за такою граматикою може бути застосований метод рекурсивного спуску (без повернень).

Побудуємо для граматики G1 парсер і програму для обчислення зна- чень виразів. Розглянемо спочатку загальний алгоритм і окремі мето- ди, застосовані в ньому.

Щоб зрозуміти, як саме програма обчислює вираз, розглянемо вираз 7

– 2 * 3. Припустимо, що покажчик prog указує на початок виразу.

При виклику методу evalExp() – вхідної точки програми – зі вхідно- го рядка вибирається лексема. Якщо лексема є порожнім рядком, то метод друкує повідомлення "Немає виразу" і завершується. Однак у нашому випадку лексемою є число 7. Оскільки це непорожній рядок, то викликається метод evalExp1() (аналіз: <вираз> → <доданок> <опе- рація1> <доданок>). Потім метод evalExp1() викликає evalExp2() (ана-

ліз: <доданок> → <множник> <операція2> <множник>); evalExp2() викликає evalExp3() (аналіз: <множник> → <степінь> <операція3> <множник>); той, у свою чергу, – evalExp4() (аналіз: <степінь> → <операція1> (<вираз>)). Потім метод evalExp4() перевіряє, чи не є лек- сема унарним плюсом або мінусом. У розглядуваному випадку це не так, тому викликається метод evalExp5() (аналіз: <степінь> → (<ви- раз>)| <змінна> | <число>). У цей момент evalExp5() може рекурсивно викликати або evalExp1() (у випадку виразу, вкладеного в дужки), або atom(), щоб визначити значення числа. Оскільки лексема не є ві- дкриваючою дужкою, то виконується метод atom() і змінній *answer присвоюється значення 7. Потім відбувається перехід до наступної лексеми й повернення з ланцюжка викликів методів. Лексемою стає операція –, а керування повертається методу evalExp1().

Те, що відбувається далі, дуже важливо. Оскільки поточною лексе- мою є символ -, то він зберігається у змінній ор. Потім аналізатор ви- бирає наступну лексему, і спуск по ланцюжку починається знов. Як і раніше, викликається метод atom(). Отримане значення 2 поверта- ється змінною *answer, і зчитується лексема *. Це викликає повернен- ня по ланцюжку до evalExp2(), де зчитується остання лексема 3. У цей момент відбувається перша арифметична операція – множення 2 на 3. Отриманий результат повертається методу evalExp1(), де від- бувається віднімання. У результаті віднімання отримуємо 1.

Повернемося тепер до граматики G, що задає складні арифмети- чні вирази.

Побудуємо значення функції FIRST1 для правих частин правил цієї граматики:

555

ПРОГРАМУВАННЯ

FIRST1(<присвоювання>) = {a, …, z, A, …, Z}

FIRST1(<вираз>) = FIRST1(<доданок> [<операція1> <доданок>]) = {+, -, (, a, …, z, A, …, Z, 0, …9}

FIRST1(<множник > [<операція2> <множник>]) = {+, -, (, a, …, z, A, …, Z, 0, …9}

FIRST1((<вираз>)) = { (} FIRST1(<змінна>) = {a, …, z, A, …, Z} FIRST1(<число>) = {0, …, 9}.

Очевидно, що граматика G не є LL(1)-граматикою. Це випливає з того, що перетин множин

FIRST1 (<присвоювання>)∩FIRST1(<вираз>) = {a, …, z, A, …, Z}

непорожній, отже, для неї метод рекурсивного спуску не буде працювати. Існують алгоритми, що перетворюють неLL(1)- на LL(1)-граматики.

Проте вони достатньо складні, тому для синтаксичного аналізу за неLL(1)-граматикою зручніше скористатися методом рекурсивного спуску з поверненням. При цьому лексичний аналізатор матиме дода- тковий метод, який у всіх неоднозначних ситуаціях перед початком аналізу запам'ятовує поточний стан лексичного аналізатора й намага- ється продовжити виведення за першою з можливих альтернатив. Якщо альтернатива невірна, то повертається до запам'ятованого ста- ну й переглядає наступну. Якщо всі варіанти продовження виведення завершилися невдачею, то повертається повідомлення про помилку.

З технічного погляду це все, що потрібно аналізатору для коректної обробки змінних. Однак поки немає способу присвоїти змінним зна- чення. Часто це робиться за межами аналізатора, але в аналізаторі можна розглядати знак рівності як знак операції присвоювання й обробляти його частиною аналізатора. Цього можна досягти кількома способами. Один із них – додати в аналізатор метод evalExp6() (ана-

ліз: <присвоювання>→ <змінна>=<вираз>).

Приклад 4.32. Синтаксичний аналіз:

Лістинг.

/*Обробка присвоювання*/

void CSimpleParser::evalExp6(double *answer)

{

int slot;

char ttokType; char tempToken[80];

if(tokType==VARIABLE) {

/*зберегти стару лексему*/

556

Розділ IV. АЛГОРИТМИ

strcpy(tempToken, token); ttokType=tokType; /*обчислити індекс змінної*/ slot=toupper(*token) – 'A';

getToken();

if(*token != '=') {

putback(); /*повернути поточну лексему*/

/*відновити стару лексему – це не присвоювання*/ strcpy(token, tempToken);

tokType=ttokType;

}

else {

getToken(); /*одержати наступну частину виразу*/ evalExp1(answer);

vars[slot]=*answer;

return;

}

}

evalExp1(answer);

}

У цьому методі доводиться заглядати вперед, щоб визначити, чи є насправді присвоювання. Це пов'язано з тим, що ім'я змінної завжди перебуває перед оператором присвоювання, але наявність імені змін- ної не гарантує, що за нею піде присвоювання. Інакше кажучи, ана- лізатор сприйме вираз А=100 як присвоювання, причому він може визначити, що А/10 ним не є. Для цього метод evalExp6() зчитує зі вхідного потоку наступну лексему. Якщо ця лексема не є знаком рів- ності, то вона за допомогою методу putback() повертається у вхідний потік для наступного використання:

/*Повернення лексеми у вхідний потік*/ void CSimpleParser::putback(void)

{

char *t;

t=token;

for(; *t; t++) prog-і;

}

■

Приклад 4.33. Повний текст програми обчислення складних виразів:

Лістинг.

557

ПРОГРАМУВАННЯ

/*Даний модуль містить рекурсивний спадний парсер, що розпізнає змінні*/

#include <iostream> #include <fstream> #include <string>

#define DELIMITER 1

#define

VARIABLE

2

#define

NUMBER

3

using namespace std;

class CSimpleParser { public:

CSimpleParser()

{

prog=new (char[100]);//[100]; if(!prog) {

printf("Помилка при виділенні пам'яті.\n"); exit(1);

};

for(int i=0; i<26; i++) vars[i]=0.0;

};

void evalExp(double *answer);

char *prog; /*містить вираз, що аналізується*/

private:

void setProg(char *pr)

{

prog=pr;

};

void setToken(char tok[80])

{

strcpy(token,tok);

};

void evalExp1(double *answer); void evalExp2(double *answer); void evalExp3(double *answer); void evalExp4(double *answer); void evalExp5(double *answer); void evalExp6(double *result); void atom(double *answer); void getToken();

558

Розділ IV. АЛГОРИТМИ

void putback();

void serror(int error); int isdelim(char c);

/*Одержання значення змінної*/ double findVar(char *s);

char token[80]; char tokType;

double vars[26]; /*26 змінних, A-Z*/

};

/*Обробка присвоювання*/

void CSimpleParser::evalExp6(double *answer)

{

int slot; char ttokType;

char tempToken[80];

if(tokType==VARIABLE) { /*зберегти стару лексему*/ strcpy(tempToken, token); ttokType=tokType; /*обчислити індекс змінної*/ slot=toupper(*token) – 'A'; getToken();

if(*token != '=') {

putback(); /*повернути поточну лексему*/ /*відновити стару лексему – це не присвоювання*/ strcpy(token, tempToken);

tokType=ttokType;

}

else {

getToken(); /*одержати наступну частину виразу*/ evalExp1(answer);

vars[slot]=*answer;

return;

}

}

evalExp1(answer);

}

/*Одержання значення змінної*/

double CSimpleParser::findVar(char *s)

{

if(!isalpha(*s)){

serror(1);

559

ПРОГРАМУВАННЯ

return 0.0;

}

return vars[toupper(*token)-'A'];

}

/*Точка входження аналізатора*/

void CSimpleParser::evalExp(double *answer) /*точка входження аналізатора*/

{

getToken(); if(!*token) { serror(2); return;

}

evalExp6(answer);

if(*token) serror(0); /*остання лексема має бути нулем*/

}

/*Додавання або віднімання двох доданків*/ void CSimpleParser::evalExp1(double *answer)

{

double temp; register char op;

evalExp2(answer); while((op=*token)=='-'||op=='+') {

getToken();

CSimpleParser(token);

evalExp2(&temp); switch(op) {

case '+': *answer=*answer+temp; break;

case '-':

*answer=*answer – temp; break;

}

}

}

/*Множення або ділення двох множників*/ void CSimpleParser::evalExp2(double *answer)

{

double temp; register char op;

evalExp3(answer);

560