1. Проект GNAT#

GNAT - це абревіатура від GNU Ada Translator; це система фронтенду і часу виконання для Ada 95, яка використовує внутрішню частину GCC як генератор коду, і розповсюджується згідно з настановами Фонду вільного програмного забезпечення. GNAT спочатку розроблявся двома командами, які співпрацювали між собою:

  • Команда Нью-Йоркського університету. Ця група під керівництвом професора Роберта Б.К. Дьюара та Едмонда Шонберга відповідала за розробку частини компілятора, що виконує аналіз вихідного коду (фронтенд).

  • Команда Університету штату Флорида. Цю групу, також відому як POSIX Ada Real-Time Team, очолював професор Теодор П. Бейкер (Theodore P. Baker), і вона відповідала за первинну розробку компонентів бібліотеки часу виконання для паралельних конструкцій Ada.

З 1991 по 1994 рік проект NYU спонсорувався урядом США. У серпні 1994 року члени команди NYU створили компанію Ada Core Technologies, Inc., яка надає технічну підтримку промисловим користувачам GNAT і перетворила GNAT на повнофункціональний компілятор промислового рівня: GNAT Pro. Цей компілятор включає в себе сучасний інструментарій та середовище для розробки програмного забезпечення на основі Ada (наприклад, GNAT Programing Studio). Сьогодні Ada Core продовжує інвестувати ресурси для перенесення GNAT на нові архітектури та операційні системи, а також бере активну участь у розробці нової версії Ada (Ada 2005). Ada Core періодично випускає загальнодоступні версії компілятора для широкої спільноти користувачів Ada.

У цьому розділі представлено основні компоненти GNAT. Він має наступну структуру: У підрозділі 1.1 коротко описано GCC; у підрозділі 1.2 представлено основні компоненти компілятора GNAT. Нарешті, у підрозділі 1.3 наведено огляд моделі компіляції GNAT.

1.1. GCC#

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

GCC є центральним елементом програмного забезпечення GNU. Це система компіляторів із кількома фронтендами та широкою підтримкою апаратних платформ. Спочатку розроблена як компілятор для мови C, зараз вона включає фронтенди для C++, Objective-C, Ada, Fortran, Java і treelang. З технічної точки зору, найважливішою перевагою GCC є його здебільшого незалежний від мови та платформ генератор коду, який створює код чудової якості як для CISC, так і для RISC комп’ютерів. Прикметно, що машинні залежності генератора коду становлять менше 10% від загального обсягу коду. Щоб додати підтримку нової платформи до GCC, необхідно алгебраїчно описати кожну машинну інструкцію за допомогою мови переведення регістрів (RTL). Більша частина генерації та оптимізації коду потім використовує RTL, яку GCC при необхідності перетворює на цільову машинну мову. Крім того, GCC створює високоякісний код, який можна порівняти з кодом найкращих комерційних компіляторів.

1.2. Компілятор GNAT#

Перше рішення стосувалося вибору мови, якою має бути написаний компілятор GNAT. GCC повністю написаний на C, але з технічних причин, а також нетехнічних, було немислимо використовувати для самого GNAT щось інше, окрім Ada. Насправді, визначення мови Ada сильно залежить від ієрархічних бібліотек і не може бути надане інакше, ніж в Ada 95, так що для компілятора і оточення є природним використовувати дочірні модулі всюди.

Команда GNAT почала використовувати відносно невелику підмножину Ada 83 і згодом розширювала цю підмножину, коли з’являлися нові можливості. Через шість місяців після початку серйозного кодування вони змогли перейти на GNAT і відмовитися від комерційного компілятора, яким користувалися до цього моменту.

Як тільки було реалізовано більше можливостей Ada 95, вони змогли написати GNAT на Ada 95.

Компілятор GNAT складається з двох основних частин: фронтенду та бекенду (див. Рисунок 1.1). Фронтенд написан на мові Ada 95, а бекенд є бекендом GCC, розширеним для задоволення потреб семантики Ada (наприклад, підтримка винятків).

Компілятор GNAT

Рисунок 1.1: Компілятор GNAT.

Фронтенд реалізує п’яти фаз компіляціі (див. Рисунок 1.2): лексичний аналіз (сканування), синтаксичний аналіз (синтаксичний розбір), семантичний аналіз, розширення та фаза GIGI.

Сканер аналізує введені символи і генерує відповідні токени.

Синтаксичний аналізатор перевіряє синтаксис токенів і створює абстрактне синтаксичне дерево (АСД). Семантичний аналізатор виконує всі статичні перевірки легальності програми та прикрашає АСД семантичними атрибутами. Експандер перетворює високорівневі вузли АСД (вузли, що представляють завдання, захищені об’єкти тощо) в еквівалентні фрагменти АСД, побудовані з більш низькорівневих вузлів абстракції і, якщо потрібно, викликів процедур бібліотеки Ada Run-Time. Оскільки генерація коду вимагає, щоб такі фрагменти містили всі семантичні атрибути, кожна операція розширення повинна супроводжуватися додатковою семантичною обробкою згенерованого дерева (див. стрілку у зворотному напрямку від розширювача до семантичного аналізатора). Наприкінці цього процесу фаза GIGI перетворює AST на дерево, яке зчитується внутрішньою частиною GCC (фаза перетворення GNAT на GNU). Ця фаза насправді є інтерфейсом між інтерфейсом GNAT і внутрішнім інтерфейсом GCC. Для подолання семантичного розриву між мовами Ada і Сі було розширено декілька процедур генерації коду GCC, а також додано інші, так що тягар перекладу також бере на себе GIGI і GCC, коли незручно або неефективно виконувати розширення у зовнішньому інтерфейсі. Наприклад, є дії з генерації коду для винятків, варіаційних частин і доступу до необмежених типів. Згідно з політикою GCC, генератор коду розширюється лише тоді, коли розширення може бути корисним для більш ніж однієї мови.

Фази початкового етапу GNAT

Рисунок 1.2: Фази початкового етапу GNAT.

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

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

1.3. Модель компіляції#

Поняття програмної бібліотеки є одним з фундаментальних внесків Ada в інженерію програмного забезпечення. Бібліотека гарантує збереження типів при компіляції та запобігає створенню непослідовних систем, виключаючи застарілі модулі. У більшості компіляторів Ada бібліотека є складною структурою, яка містить проміжні представлення скомпільованих одиниць, інформацію про залежності між скомпільованими одиницями, таблиці символів тощо. У GNAT обрано інший підхід: окремі файли, з яких складається програма, компілюються окремо, і кожна компіляція створює відповідний об’єктний файл. Ці об’єктні файли потім зв’язуються між собою, визначаючи список об’єктних файлів у програмі. Таким чином, бібліотека Ada складається з набору таких об’єктних файлів (бібліотечного файлу як такого не існує). У наступних розділах ми коротко представимо обидві альтернативи.

Традиційна модель компіляції#

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

У цій моделі створення програми на Ada складається з вибору головної програми (процедури без параметрів, скомпільованої в бібліотеці Ada) та всіх модулів, від яких залежить ця головна програма, і зв’язування їх в єдину виконувану програму. Певний порядок компіляції задається семантикою мови і реалізується засобами бібліотеки Ada. По суті, перед тим, як компілювати модуль, спочатку має бути скомпільовано специфікацію всіх модулів, від яких він залежить. Це дає компілятору Ada достатню свободу у виборі порядку компіляції. Важливим наслідком цієї моделі є поняття застарілого модуля. Якщо модуль перекомпілюється, то модулі, які від нього залежать, стають застарілими, і їх потрібно перекомпілювати. Знову ж таки, бібліотека Ada є структурою даних, що використовується для реалізації цієї вимоги.

У довідковому посібнику з Ada [AAR95, глава 10] є спеціальні посилання на файл бібліотеки, і це часто сприймається як те, що бібліотеку Ada слід представляти за допомогою файлу у звичайному розумінні. Більшість систем Ada дійсно реалізують бібліотеку Ada у такий спосіб. Однак, загальновизнано, що стандарт Ada не вимагає такого підходу до реалізації. З цієї точки зору, бібліотека Ada є концептуальною сутністю, яка може бути реалізована у будь-який спосіб, що підтримує необхідну семантику. Насправді монолітний бібліотечний підхід погано пристосований до багатомовних систем і є причиною деяких незручностей при інтерфейсуванні Ada з іншими мовами.

Модель компіляції GNAT#

У GNAT обрано зовсім інший підхід: вихідні коди компілюються незалежно для створення набору об’єктів, а створений таким чином набір об’єктних файлів подається до біндера/лінкера для генерації результуючого виконуваного файлу (див. Рисунок 1.3).

Загальна структура GNAT

Рисунок 1.3: Загальна структура GNAT.

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

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

Враховуючи швидкість роботи фронтенду GNAT, цей підхід є не менш ефективним, ніж звичайний бібліотечний механізм, і має наступні переваги перед ним:

  1. Компіляція модуля Ada ідентична компіляції модуля або файлу іншою мовою: результатом компіляції одного вихідного коду є один об’єктний файл.

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

  3. Стандартні системні утиліти для копіювання, перейменування та видалення файлів можна використовувати для копіювання, перейменування та видалення об’єктних модулів.

  4. Оскільки GNAT використовує ту саму модель компіляції, що й інші мови, набагато легше створювати програми, в яких різні частини програми написані різними мовами. Крім того, GCC дотримується загальносистемних стандартних домовленостей щодо послідовностей виклику, форматів об’єктних модулів, включаючи налагоджувальну інформацію, і розміщення структур даних, тому також легко інтегрувати Ada з будь-якою мовою, що підтримується GCC. GNAT навіть дозволяє писати багатомовні програми, основна програма яких не написана на Ada.

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

У моделі GNAT вихідний файл містить один модуль компіляції, а компіляція представляється у вигляді серії вихідних файлів, кожен з яких містить один модуль компіляції. Крім того, існує пряме відображення назв модулів у назви файлів, так що за назвою модуля завжди можна визначити назву файлу, який містить вихідні дані для цього модуля. За замовчуванням прийнято наступну угоду щодо іменування файлів: (1) Ім’я файлу - це розширене ім’я модуля, з крапками, заміненими на знак мінус, (2) Розширення .ads використовується для специфікацій, а розширення .adb - для тіл. Тільки тіло створює об’єктний файл, тому той факт, що специфікація і тіло мають однакове ім’я файлу, не викликає труднощів. Об’єктний файл концептуально містить інформацію про бібліотеку Ada для даного джерела (розширення .ali), найважливішим компонентом якої є запис відміток часу модулів компіляції, від яких залежить скомпільований модуль.

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

  1. Відповідна специфікація для тіла.

  2. Батьківська специфікація для специфікації дочірнього модуля.

  3. Специфікації використаних (через with) модулів.

  4. Батьківське тіло для підмодуля.

  5. Тіла підпрограм для розгортання.

  6. Тіла модулів для екземплярів узагальнених модулів.

Ключове розуміння полягає у тому, що у GNAT залежності встановлюються не від однієї одиниці компіляції до іншої, а від об’єктних файлів до відповідних вихідних файлів. У цьому контексті GNAT переосмислює правила «порядку компіляції» Ada як правила «залежності від вихідних файлів». Правила щодо модулів, які є застарілими для інших модулів, також скорговано. Наприклад, правило, яке говорить: Тіло пакета не може бути скомпільовано, доки не буде скомпільовано його специфікацію, переінтерпретовано на таке: «Тіло пакета не може бути скомпільовано: Тіло пакета не може бути скомпільовано, доки не буде доступний вихідний код його специфікації. Цікавим наслідком такого підходу є те, що якщо всі вихідні тексти програми доступні, то фактично немає жодних обмежень на порядок компіляції. Ця особливість полегшує паралельну компіляцію програм на Ada.

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

Зв’язка (binding)#

Ada встановлює правила, які визначають допустимі порядок початкового виконання модулів [AAR95, розділ 10.2]. Також можливо побудувати програми, для яких не існує жодного допустимого порядку початкового виконання. Такі програми є незаконними і повинні бути діагностовані перед запуском. Оскільки ця робота не може бути налагоджена, поки не будуть доступні всі об’єктні файли, GNAT потребує спеціального попереднього зв’язувача (біндера), який встановлює правильну послідовність викликів процедур ініціалізації для специфікацій та тіл (див. рисунок 1.3).

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

  1. Тільки з файлів ALI.

  2. З файлів ALI та будь-якого відповідного вихідного коду, якій можна знайти.

  3. З файлів ALI та всього вихідного коду, які повинний бути доступний.

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

Тому саме цей режим реалізовано у GNAT Binder.

1.4. Підсумок#

У цьому вступному розділі представлено загальну структуру проекту GNAT.

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

Найбільш новим аспектом архітектури GNAT є організація бібліотеки на основі вихідних текстів. У більшості компіляторів Ada бібліотека є монолітною складною структурою, яка містить проміжні представлення скомпільованих модулів. Модель бібліотеки GNAT відповідає традиційній моделі, яку використовували майже всі мови протягом всієї історії мов програмування: немає централізованої бібліотеки, вихідний файл містить єдину одиницю компіляції, а компіляція специфікує вихідний файл і генерує єдиний об’єктний файл. Ця модель повністю відповідає прописаній семантиці, наведеній у стандарті Ada, і в той же час дозволяє використовувати багато відомих засобів керування конфігурацією (наприклад, UNIX make), спрощує побудову багатомовних програм, а також дозволяє паралельну компіляцію програм на мові Ada. Оскільки мова Ada задає правила, які регулюють порядок розробки одиниць компіляції, модель GNAT потребує спеціального попереднього зв’язувача (біндера), який перевіряє об’єктні файли і генерує правильний порядок початкового виконання.