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

spz / spz

.pdf
Скачиваний:
33
Добавлен:
23.02.2016
Размер:
5.16 Mб
Скачать

7

породжується текст результуючої програми. Результатом цього етапу є об'єктний код.

Рис. 1. Загальна схема роботи компілятора

Крім того, у складі компілятора присутня частина, відповідальна за аналіз і виправлення помилок, яка при наявності помилки в тексті вихідної програми повинна максимально повно інформувати користувача про тип помилки і місце її виникнення. У кращому випадку компілятор може запропонувати користувачу варіант виправлення помилки.

Ці етапи, у свою чергу, складаються з більш дрібних етапів, названих фазами компіляції. Склад фаз компіляції приведений у самому загальному виді, їхня конкретна реалізація і процес взаємодії можуть, звичайно, розрізнятися в залежності від версії компілятора. Однак у тому чи іншому

8

виді всі представлені фази практично завжди присутні в кожному конкретному компіляторі.

Компілятор з погляду теорії формальних мов виконує дві основні функції.

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

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

Далі дається перелік основних фаз (частин) компіляції і короткий опис їх функцій. Більш докладна інформація дана в підрозділах, що відповідають цим фазам.

Лексичний аналіз (сканер) — це частина компілятора, що читає літери програми початковою мовою і будує з них слова (лексеми) початкової мови. На вхід лексичного аналізатора надходить текст початкової програми, а вихідна інформація передається для подальшої обробки компілятором на етапі синтаксичного розбору. З теоретичної точки зору лексичний аналізатор не є обов'язково необхідною частиною компілятора. Однак існують причини, що визначають його присутність практично у всіх компіляторах.

Синтаксичний розбір — це основна частина компілятора на етапі аналізу. Вона виконує виділення синтаксичних конструкцій у тексті вихідної програми, обробленому лексичним аналізатором. На цій же фазі компіляції перевіряється синтаксична правильність програми. Синтаксичний розбір відіграє головну роль — роль розпізнавача тексту вхідної мови програмування.

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

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

9

Генерація коду — це фаза, безпосередньо зв'язана з породженням команд, що складають речення вихідної мови й у цілому текст результуючої програми. Це основна фаза на етапі синтезу результуючої програми.

Крім безпосереднього породження тексту результуючої програми, генерація звичайно містить у собі також оптимізацію — процес, зв'язаний з обробкою уже породженого тексту. Іноді оптимізацію виділяють в окрему фазу компіляції, тому що вона впливає на якість і ефективність результуючої програми.

Таблиці ідентифікаторів (іноді — «таблиці символів») — це спеціальним образом організовані набори даних, які служать для збереження інформації про елементи початкової програми, що потім використовуються для породження тексту результуючої програми. Таблиця ідентифікаторів у конкретній реалізації компілятора може бути одна, або таких таблиць може бути кілька. Елементами початкової програми, інформацію про які потрібно зберігати в процесі компіляції, є перемінні, константи, функції і т.п. - конкретний склад набору елементів залежить від використовуваної вхідної мови програмування. Поняття «таблиці» зовсім не припускає, що це сховище даних повинне бути організоване саме у виді таблиць або інших масивів інформації.

Розглянемо загальні аспекти технічної організації представлених фаз компіляції.

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

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

Далі розглянуті технічні питання реалізації основних фаз компіляції, що тісно зв'язані з поняттям проходу.

Поняття проходу. Багатохідні і однопрохідні компілятори

Процес компіляції програм складається з декількох фаз.

У реальних компіляторах склад цих фаз може відрізнятися від розглянутого — деякі з них можуть бути розбиті на складові, інші, навпроти, об'єднані в одну фазу. Порядок виконання фаз компіляції також може мінятися в різних варіантах компіляторів. В одному випадку компілятор переглядає текст початкової програми, відразу виконує усі фази компіляції й одержує результат — об'єктний код. В іншому варіанті він виконує над вихідним текстом тільки деякі з фаз компіляції й одержує

10

не кінцевий результат, а набір проміжних даних. Ці дані потім знову обробляються. І цей процес може повторюватися декілька разів.

Реальні компілятори, як правило виконують трансляцію тексту початкової програми за декілька проходів.

Прохід - це процес послідовного читання компілятором даних з зовнішньої пам’яті, їх обробки і розміщення результату у зовнішню пам’ять.

Частіше всього один прохід включає в себе виконання одної або декількох фаз компіляції. Результатом проміжних проходів є внутрішнє представлення початкової програми, а результатом останнього проходу – результуюча об’єктна програма.

Вякості зовнішньої пам’яті можуть виступати любі носії інформації

оперативна пам’ять комп’ютера, накопичувачі на магнітних дисках,

магнітні стрічки і т.д. Сучасні компілятори використовують для зберігання оперативну пам’ять і тільки у випадку її нестачі використовують накопичувачі на жорстких магнітних дисках.

При виконанні кожного проходу компілятору доступна інформація, яка була одержана в результаті всіх попередніх проходів. Як правило, він використовує інформацію, одержану при останньому проході, але при необхідності може звертатися і до даних попередніх проходів аж до початкової програми. Інформація, яка отримується компілятором при виконанні проходів, недоступна користувачу. Вона або зберігається в ОП, або в тимчасових файлах на диску, які знищуються після завершення роботи компілятора. Тому людина, яка працює з компілятором не знає скільки він зробив проходів. Але кількість виконуваних проходів – важлива технічна характеристика компілятора. Солідні фірми – розробники компіляторів звичайно вказують це в описі свого продукту.

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

Але скоротити кількість проходів не завжди вдається. Вона визначається граматикою і семантикою початкової мови. Чим складніше граматика і чим більше варіантів пропонують семантичні правила, тим більше проходів буде виконувати компілятор. Саме тому компілятори з мови Паскаль працюють швидше за компілятори з мови С – граматика мови Паскаль більш проста, а семантичні правила більш жорсткі.

Реальні компілятори виконують від двох до п’яти проходів. Таким чином реальні компілятори є багатопрохідними. Найбільш розповсюдженими є двохта трьохпрохідні компілятори. Тоді перший прохід – це лексичний аналіз, другий – синтаксичний розбір і семантичний аналіз, третій – генерація коду.

Системне програмне забезпечення.

1

Таблиці ідентифікаторів. Організація таблиць ідентифікаторів

1.Призначення та особливості побудови таблиць ідентифікаторів.

2.Найпростіші методики побудови таблиць ідентифікаторів.

3.Побудова таблиць ідентифікаторів методом бінарного дерева

Призначення йособливості побудовитаблиць ідентифікаторів

Перевірка правильності семантики і генерація коду вимагають знання характеристик змінних, констант, функцій і інших елементів, що зустрічаються у програмі вхідною мовою. Усі ці елементи у вхідній програмі, як правило, позначаються ідентифікаторами. Виділення ідентифікаторів і інших елементів вхідної програми відбувається на фазі лексичного аналізу. Їхні характеристики визначаються на фазах синтаксичного розбору, семантичного аналізу і підготовки до генерації коду. Склад можливих характеристик і методи їх визначення залежать від семантики вхідної мови.

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

Для цієї мети, як було сказано вище, у компіляторах використовуються спеціальні сховища даних, називані таблицями символів або таблицями ідентифікаторів.

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

Склад інформації, збереженої в таблиці ідентифікаторів для кожного елемента вхідної програми, залежить від семантики вхідної мови і типу елемента. Наприклад, у таблицях ідентифікаторів може зберігатися наступна інформація:

для змінних:

-ім'я змінної;

-тип даних змінною;

-область пам'яті, зв'язана із змінною;

для констант:

-назва константи (якщо воно є);

-значення константи;

-тип даних константи (якщо потрібно);

для функцій:

-ім'я функції;

-кількість і типи формальних аргументів функції;

Системне програмне забезпечення.

2

-тип результату, що повертається;

-адреса коду функції.

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

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

Найпростіші методи побудови таблиць ідентифікаторів

Найпростіший спосіб організації таблиці полягає в тому, щоб додавати елементи в порядку їх надходження. Тоді таблиця ідентифікаторів буде представляти невпорядкований масив інформації, кожна комірка якого буде містити даніпро відповідний елементітаблиці.

Пошук потрібного елемента в таблиці буде в цьому випадку полягати в послідовному порівнянні шуканого елемента з кожним елементом таблиці, поки не буде знайдений придатний. Тоді, якщо за одиницю прийняти час, затрачуваний компілятором на порівняння двох елементів (як правило, це порівняння рядків), то для таблиці, що містить N елементів, у середньому буде виконане N/2 порівнянь.

Заповнення такої таблиці буде відбуватися елементарно просто — додаванням нового елемента в її кінець, і час, необхідний на додавання елемента (Тз), не буде залежати від числа елементів у таблиці N. Але якщо N дуже велике, то пошук зажадає значних витрат часу. Час пошуку (Тn) у такій таблиці можна оцінити як Тn = O(N). Оскільки пошук у таблиці ідентифікаторів є найчастіше виконуваною компілятором операцією, а кількість різних ідентифікаторів навіть у реальній вхідній програмі досить велике (від декількох сотень до декількох тисяч елементів), то такий спосіб організації таблиць ідентифікаторів є неефективним.

Системне програмне забезпечення.

3

Пошук може бути виконаний більш ефективно, якщо елементи таблиці упорядковані (відсортовані) відповідно до деякого природного порядку. У нашому випадку, коли пошук буде здійснюватися по імені ідентифікатора, найбільш природно розташувати елементи таблиці в прямому чи зворотному алфавітному порядку. Ефективним методом пошуку в упорядкованому списку з N елементів є бінарний або логарифмічний пошук. Символ, який варто знайти, порівнюється з елементом (N+l)/2 у середині таблиці. Якщо цей елемент не є шуканим, то ми повинні переглянути тільки блок елементів, пронумерованих від 1 до (N+l)/2-l, чи блок елементів від (N+l)/2+1 до N у залежності від того, менше чи більше шуканий елемент від того, з яким його порівняли. Потім процес повторюється над потрібним блоком у два рази меншого розміру. Так продовжується доти, поки або елемент не буде знайдений, або алгоритм не дійде до чергового блоку, що містить один чи два елементи (з якими уже можна виконати пряме порівняння шуканого елемента).

Тому що на кожнім кроці число елементів, що можуть містити шуканий елемент,скорочується наполовину, томаксимальне число порівняньдорівнює

l+log2(N)

Тоді час пошуку елемента в таблиці ідентифікаторів можна оцінити як

Тn = O(log2N).

Наприклад: при N=128 бінарний пошук вимагає якнайбільше 8 порівнянь, а пошук у неупорядкованій таблиці – у середньому – 64 порівняння.

Метод називають “бінарним пошуком”, оскільки на кожнім кроці обсяг розглянутої інформації скорочується в два рази, “логарифмічним”, оскільки час, затрачуваний на пошук потрібного елемента в масиві, має логарифмічну залежність від загальної кількості елементом у ньому.

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

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

Т3 = O(N*log2 N) + k*O(N2).

Системне програмне забезпечення.

4

Тут k — деякий коефіцієнт, що відображає співвідношення між часом, затрачуваними комп'ютером на виконання операції порівняння й часом операції перенесення даних.

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

Побудова таблиць ідентифікаторів по методу бінарного дерева

Можна скоротити час пошуку шуканого елемента в таблиці ідентифікаторів, не збільшуючи значно час, необхідний на її заповнення. Для цього треба відмовитися від організації таблиці у виді безперервного масиву даних.

Існує метод побудови таблиць, при якому таблиця має форму бінарного дерева. Кожен вузол дерева являє собою елемент таблиці, причому кореневий вузол є першим елементом, зустрінутим при заповненні таблиці. Дерево називається бінарним, тому що кожна вершина в ньому може мати не більш двох віток (і, отже, не більш двох нижче лежачих вершин ). Для визначеності будемо називати дві вітки «права» і «ліва».

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

Перший ідентифікатор, як уже було сказано, міститься у вершину дерева. Усі подальші ідентифікатори попадають у дерево по наступному алгоритму.

Крок І. Вибрати черговий ідентифікатор із вхідного потоку даних.. Якщо чергового ідентифікатора не має, та побудова дерева закінчена.

Крок 2. Зробити поточним вузлом дерева кореневу вершину.

Крок 3. Порівняти черговий ідентифікатор з ідентифікатором, що міститься в поточному вузлі дерева.

Крок 4. Якщо черговий ідентифікатор менше, то перейти до кроку 5, якщо дорівнює - повідомити про помилку і припинити виконання алгоритму (двох однакових ідентифікаторів бути не може!), інакше — перейти до кроку 7.

1 Какминимумпридобавленииновогоидентификаторавтаблицукомпилятордолженпроверить,существуетилинеттам такойидентификатор,таккаквбольшинствеязыковпрограммирования ниодинидентификатор неможет быть описан болееодногоразаСледовательно, каждая операциядобавления новогоэлементавлечет,какправило;неменееодной операциипоиска

Системне програмне забезпечення.

5

Крок 5. Якщо у поточного вузла існує ліва вершина, то зробити її поточним вузлом і повернутися до кроку 3, інакше — перейти до кроку 6.

Крок 6. Створити нову вершину, помістити в неї черговий ідентифікатор, зробити цю нову вершину лівою вершиною поточного вузла і повернутися до кроку 1.

Крок 7. Якщо у поточного вузла існує права вершина, то зробити її поточним вузлом і повернутися до кроку 3, інакше — перейти до кроку 8.

Крок 8. Створити нову вершину, помістити в неї черговий ідентифікатор, зробити цю нову вершину правою вершиною поточного вузла і повернутися до кроку 1.

Розглянемо як приклад послідовність ідентифікаторів GA, D1, М22, Е, А12, ВР, F. На мал. проілюстрований весь процес побудови бінарного дерева для цієї послідовності ідентифікаторів.

GA

 

GA

GA

 

 

 

GA

 

 

 

 

 

 

 

D1

 

M22

 

D1

M22

1

D1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2

 

3

 

 

E

4

 

 

 

 

 

 

 

 

GA

 

GA

 

 

 

GA

 

 

 

 

 

 

D1

M22

D1

 

M22

 

D1

M22

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

A12

E

 

A12

 

E

 

 

 

 

 

 

 

 

 

 

 

 

A12

E

 

 

 

 

 

 

 

 

 

 

 

 

 

5

 

BC

 

 

 

BC

 

 

 

 

 

 

 

 

F

6

7

Рис. 13.2. Покрокове заповнення бінарного дерева для послідовності ідентифікаторів

GA, D1, М22, Е, А12, ВР, F

Системне програмне забезпечення.

6

Пошук потрібного елемента в дереві виконується по алгоритму, схожому з алгоритмом заповнення дерева.

Крок I. Зробити поточним вузлом дерева кореневу вершину.

Крок 2. Порівняти шуканий ідентифікатор з ідентифікатором, що міститься вузлі дерева.

Крок 3. Якщо ідентифікатори збігаються, то шуканий ідентифікатор знайдений, алгоритм завершується, інакше – треба перейти до кроку 4.

Крок 4. Якщо черговий ідентифікатор менше, то перейти до кроку 5, інакше - перейти до кроку 6.

Крок 5. Якщо в поточного вузла існує ліва вершина, то зробити її поточним вузлом і повернутися до кроку 2, інакше шуканий ідентифікатор не знайдений, алгоритм завершується.

Крок 6. Якщо в поточного вузла існує права вершина, то зробити її поточним вузлом і повернутися до кроку 2, інакше шуканий ідентифікатор не знайдений, алгоритм завершується.

Наприклад, зробимо пошук у дереві, зображеному на мал., ідентифікатора А12. Беремо кореневу вершину (вона стає поточним вузлом), порівнюємо ідентифікатори GA і А12. Шуканий ідентифікатор менше — поточним вузлом стає ліва вершина D1. Знову порівнюємо ідентифікатори. Шуканий ідентифікатор менше — поточним вузлом стає ліва вершина А12. При наступному порівнянні шуканий ідентифікатор знайдений.

Якщо шукати відсутній ідентифікатор — наприклад, АН, — то пошук знову піде від кореневої вершини. Порівнюємо ідентифікатори GA і АН. Шуканий ідентифікатор менше — поточним вузлом стає ліва вершина D1. Знову порівнюємо ідентифікатори. Шуканий ідентифікатор менше — поточним вузлом стає ліва вершина А12. Шуканий ідентифікатор менше, але ліва вершина у вузла А12 відсутня, тому в даному випадку шуканий ідентифікатор не знайдений.

Для даного методу число необхідних порівнянь і форма дерева, що вийшло, багато в чому залежать від того порядку, у якому надходять ідентифікатори. Наприклад, якщо в розглянутому вище прикладі замість послідовності ідентифікаторів GA, D1, М22, Е, А12, ВР, F узяти послідовність А12, GA, D1, М22, Е ВР, F то отримане дерево буде мати, інший вид. А якщо як приклад узяти послідовність ідентифікаторів А, В, С.

D, Е,

F, то дерево виродиться в упорядкований односпрямований зв'язний

список.

Ця особливість є недоліком даного методу організації таблиць

Соседние файлы в папке spz