Як компілятор може скласти себе?


168

Я досліджую CoffeeScript на веб-сайті http://coffeescript.org/ , і він містить текст

Компілятор CoffeeScript сам написаний на CoffeeScript

Як компілятор може скласти себе, або що означає це твердження?


14
Ще один термін для компілятора, який може скласти сам, - це self-hostingкомпілятор. Дивіться програмісти.stackexchange.com
q/263651/6221

37
Чому не повинен компілятор мати можливість самостійно збирати?
користувач253751

48
Є щонайменше дві копії компілятора. Попередньо складений новий примірник. Новий може бути або не бути ідентичним старому.
bdsl

12
Можливо, вас також зацікавить Git: його вихідний код відстежується, звичайно, у сховищі Git.
Грег д'Еон

7
Це приблизно як запитання "Як принтер Xerox міг надрукувати схеми для себе?" Компілятори складають текст у байт-код. Якщо компілятор може компілювати будь-який корисний байт-код, ви можете написати код компілятора відповідною мовою, а потім передати його через компілятор для отримання результату.
RLH

Відповіді:


219

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

  1. Перший компілятор CoffeeScript написаний на Ruby, створюючи версію 1 CoffeeScript
  2. Вихідний код компілятора CS переписується в CoffeeScript 1
  3. Оригінальний компілятор CS збирає новий код (написаний у CS 1) у версію 2 компілятора
  4. Зміни вносяться у вихідний код компілятора, щоб додати нові мовні функції
  5. Другий компілятор CS (перший написаний у CS) збирає переглянутий новий вихідний код у версію 3 компілятора
  6. Повторіть кроки 4 та 5 для кожної ітерації

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

Цей процес зазвичай називають завантажувальним . Іншим прикладом компілятора завантаження є rustcкомпілятор для мови Rust .


5
Інший шлях для завантаження компілятора - написати інтерпретатор для (підмножини) вашої мови.
Арон

В якості ще однієї альтернативи завантаження з компілятором або інтерпретатором, написаним іншою мовою, самим старошкільним маршрутом було б скласти джерело компілятора вручну. Чак Мур розглядає, як це зробити для інтерпретатора Forth у розділі 9 "Програми, що завантажуються", наприкінці програмування мови, орієнтованої на проблеми ( web.archive.org/web/20160327044521/www.colorforth.com/POL .htm ), виходячи з того, що зробили це двічі раніше вручну. Введення коду тут здійснюється за допомогою передньої панелі, яка дозволяє безпосередньо зберігати значення в адресах пам'яті, керованих тумблерами для бітів.
Джеремі В. Шерман

59

У статті Роздуми про довіру довіри Кен Томпсон, один із авторів Unix, пише захоплюючий (і легко читабельний) огляд того, як компілятор C складається з себе. Подібні поняття можна застосувати до CoffeeScript або будь-якої іншої мови.

Ідея компілятора, який компілює власний код, нечітко схожа на quine : вихідний код, який при виконанні видає вихідний вихідний код. Ось один із прикладів квітки CoffeeScript. Томпсон наводив цей приклад С-квінки:

char s[] = {
    '\t',
    '0',
    '\n',
    '}',
    ';',
    '\n',
    '\n',
    '/',
    '*',
    '\n',
    … 213 lines omitted …
    0
};

/*
 * The string s is a representation of the body
 * of this program from '0'
 * to the end.
 */

main()
{
    int i;

    printf("char\ts[] = {\n");
    for(i = 0; s[i]; i++)
        printf("\t%d,\n", s[i]);
    printf("%s", s);
}

Далі, вам може бути цікаво, як компілятор вчить, що послідовність евакуації на зразок '\n'являє собою код ASCII 10. Відповідь полягає в тому, що десь у компіляторі C є звичайна програма, що інтерпретує букве символів, що містить деякі умови на зразок цього для розпізнавання послідовностей зворотної косої риси:

…
c = next();
if (c != '\\') return c;        /* A normal character */
c = next();
if (c == '\\') return '\\';     /* Two backslashes in the code means one backslash */
if (c == 'r')  return '\r';     /* '\r' is a carriage return */
…

Отже, ми можемо додати до коду одну умову вище ...

if (c == 'n')  return 10;       /* '\n' is a newline */

… Створити компілятор, який знає, що '\n'представляє ASCII 10. Цікаво, що цей компілятор і всі наступні компілятори, складені ним , «знають» це відображення, тому в наступному поколінні вихідного коду ви можете змінити цей останній рядок на

if (c == 'n')  return '\n';

… І це зробить правильно! 10Приходить від компілятора, і більше не повинно бути явно визначені у вихідному коді компілятора. 1

Це один приклад функції мови C, яка була реалізована в коді C. Тепер повторіть цей процес для кожної окремої мовної функції, і у вас є компілятор "самохостингу": компілятор C, написаний на C.


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


7
Хоча це цікава інформація, я не думаю, що вона відповідає на питання. Ваші приклади припускають, що у вас вже є завантажений компілятор, або ж на якій мові написано компілятор C?
Артуро Торрес Санчес

9
@ ArturoTorresSánchez Різні пояснення добре працюють для різних людей. Я не прагну повторювати сказане в інших відповідях. Швидше, я вважаю, що інші відповіді говорять на більш високому рівні, ніж те, як я люблю думати. Я особисто віддаю перевагу конкретній ілюстрації того, як додається одна єдина функція, і дозволяю читачеві екстраполювати від цього замість неглибокого огляду.
200_успіх

5
Гаразд, я розумію вашу перспективу. Просто питання полягає в тому, що більше "як компілятор може скласти себе, якщо компілятор для компіляції не існує", а менше "як додати нові функції до завантаженого компілятора".
Артуро Торрес Санчес

17
Само питання неоднозначне та відкрите. Схоже, деякі люди трактують це як «як може компілятор CoffeeScript скласти себе?». Відповідь, яка наводиться в коментарі, - це "чому б їй не вдалося скласти себе так, як він збирає будь-який код?" Я інтерпретую це як «як може існувати компілятор, що займається хостингом?», І я наводив ілюстрацію того, як компілятора можна навчити одній із власних мовних особливостей. Він відповідає на питання по-іншому, надаючи низькорівневу ілюстрацію того, як воно реалізується.
200_успіх

1
@ ArturoTorresSánchez: "[I] n якою мовою написано компілятор C?" Давно я підтримував оригінальний компілятор C, зазначений у старому додатку K&R (той, що стосується IBM 360.) Багато людей знають, що спочатку був BCPL, потім B, і що C - вдосконалена версія B. Насправді їх було багато частини цього старого компілятора, які ще писалися в B, і ніколи не були переписані на C. Змінні були у формі однієї літери / цифри, арифметику вказівника не передбачалося автоматично масштабувати тощо. Цей старий код свідчив про завантаження з B до C. Перший компілятор "C" був написаний у B.
Eliyahu Skoczylas

29

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

  1. Компілятор CoffeeScript - це програма, яка може компілювати програми, написані на CoffeeScript.
  2. Компілятор CoffeeScript - це програма, написана на CoffeeScript.

Я впевнений, що ви можете погодитись, що і №1, і №2 є правдою. А тепер подивіться на два твердження. Чи бачите ви, що цілком нормально компілятор CoffeeScript мати можливість компілювати компілятор CoffeeScript?

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

Як компілятор може скласти себе, або що означає це твердження?

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


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

6
Ваше твердження 2 є неповним / неточним та дуже оманливим. так як, як каже перша відповідь, перша не була написана кавовим сценарієм .. Це так стосується його питання. А щодо "Як компілятор може скласти себе, або що означає це твердження?" Ви говорите "Так", я думаю, що так (хоча мій розум трохи), я бачу, що він використовується для компіляції більш ранніх версій себе, а не про себе. Але чи використовується він і для складання? Я вважав, що це буде безглуздо.
барлоп

2
@barlop: Змініть заяву 2 на " Сьогодні компілятор CoffeeScript - це програма, написана на CoffeeScript." Чи допомагає вам це краще зрозуміти? Компілятор - це "просто" програма, яка переводить вхід (код) у вихід (програма). Отже, якщо у вас є компілятор для мови Foo, тоді запишіть вихідний код для компілятора Foo мовою самого Foo і подайте це джерело до першого Foo-компілятора, ви отримаєте другий компілятор Foo як вихід. Це робиться на багатьох мовах (наприклад, усі компілятори C, про які я знаю, написані на ... C).
DarkDust

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

3
@pabrams Чому ви це вважаєте? Вихід може бути ідентичним компілятору, який використовується для його створення. Наприклад, якщо я компілюю GCC 6.1 з GCC 6.1, я отримую версію GCC 6.1, складену з GCC 6.1. І тоді, якщо я використовую це для компіляції GCC 6.1, я також отримую версію GCC 6.1, складену з GCC 6.1, яка повинна бути ідентичною (ігнорування речей, як часові позначки).
користувач253751

9

Як компілятор може скласти себе, або що означає це твердження?

Це означає саме це. Перш за все, деякі речі. Ми повинні переглянути чотири об'єкти:

  • Вихідний код будь-якої довільної програми CoffeScript
  • (Згенерована) збірка будь-якої довільної програми CoffeScript
  • Вихідний код компілятора CoffeScript
  • (Згенерована) збірка компілятора CoffeScript

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

Тепер сам компілятор CoffeScript є лише умовною програмою CoffeScript, і, таким чином, її можна скласти компілятором CoffeScript.

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

Введіть процес, який називається завантажувальним .

  1. Ви пишете компілятор вже існуючою мовою (у випадку CoffeScript оригінальний компілятор був написаний у Ruby), який може скласти підмножину нової мови
  2. Ви пишете компілятор, який може скласти підмножину нової мови самою новою мовою. Ви можете використовувати лише мовні функції, які компілятор може виконати з кроку, описаного вище.
  3. Ви використовуєте компілятор з кроку 1 для компіляції компілятора з кроку 2. Це дає вам збірку, яка спочатку була написана на підмножині нової мови, і яка здатна скласти підмножину нової мови.

Тепер вам потрібно додати нові функції. Скажімо, ви реалізували лише while-loops, але також хочете for-loops. Це не проблема, оскільки ви можете переписати будь-яку forпетлю таким чином, щоб вона була while-loop. Це означає, що ви можете використовувати while-loops у вихідному коді вашого компілятора, оскільки збірка, яка є у вас під рукою, може компілювати лише їх. Але ви можете створити функції всередині вашого компілятора, які можуть виправляти і компілювати forз ним. Потім ви використовуєте збірку, яка вже є, і компілюєте нову версію компілятора. А тепер у вас є збірка компілятора, яка також може розбирати і компілювати for-loops! Тепер ви можете повернутися до вихідного файлу вашого компілятора та переписати будь-які whileпетлі, які вам не потрібні, у for-loops.

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

while і for очевидно, були лише приклади, але це працює для будь-якої нової мовної функції, яку ви хочете. І тоді ви опинилися в ситуації, в якій зараз знаходиться CoffeScript: Компілятор компілює себе.

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


5
(Речення "Компілятор CoffeeScript сам написано на CoffeeScript", є правдивим, але "Компілятор може скласти себе" є помилковим.)
pabrams

4
Ні, це абсолютно правда. Компілятор може скласти сам. Це просто не має сенсу. Скажімо, у вас є виконуваний файл, який може компілювати версію X мови. Ви пишете компілятор, який може скласти Версію X + 1, і компілюєте його з компілятором, який у вас є (який є версією X). У вас виходить виконуваний файл, який може компілювати версію X + 1 мови. Тепер ви можете піти і використовувати цей новий виконуваний файл, щоб повторно скласти компілятор. Але з якою метою? У вас вже є виконаний файл, який робить те, що ви хочете. Компілятор може компілювати будь-яку дійсну програму, так що повністю може скомпілювати себе!
Полігном

1
Справді, це не чутно будувати досить багато разів, iirc modern freepascal будує компілятор загалом у 5 разів.
підключення

1
@pabrams Написання "Не чіпай" та "Гарячий предмет. Не чіпай" не має значення для призначеного повідомлення фрази. Поки призначена аудиторія повідомлення (Програмісти) розуміє призначене повідомлення фрази (Складання компілятора може складати її джерело) незалежно від того, як воно написане, ця дискусія є безглуздою. Наразі ваш аргумент недійсний. Якщо ви не в змозі показати, що призначена аудиторія повідомлення - це непрограмісти, тоді і лише тоді ви маєте право.
DarkDestry

2
@pabrams 'Good English' - це англійська мова, яка чітко передає ідеї передбачуваній аудиторії та таким чином, як це задумав письменник чи спікер. Якщо призначена аудиторія - це програмісти, а програмісти це розуміють, її хороша англійська. Скажімо, що "Світло існує як частинки, так і хвилі" принципово еквівалентно "Світло існує як фотони, так і електромагнітні хвилі". Для фізика вони означають буквально те саме. Чи означає це, що ми завжди повинні використовувати довше і чіткіше почуття? Немає! Тому що це ускладнює читання, коли значення вже зрозуміло призначеній аудиторії.
DarkDestry

7

Невелике, але важливе уточнення

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

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


4
  1. Компілятор CoffeeScript вперше був написаний на Ruby.
  2. Потім компілятор CoffeeScript був переписаний на CoffeeScript.

Оскільки версія Ruby компілятора CoffeeScript вже існувала, вона була використана для створення версії CoffeeScript компілятора CoffeeScript.

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

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


3

Тут не справа компіляторів, а питання виразності мови, оскільки компілятор - це лише програма, написана якоюсь мовою.

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

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

3D-принтер - це майже універсальна машина. Ви можете надрукувати весь 3D-принтер за допомогою 3D-принтера (ви не можете побудувати наконечник, який плавить пластик).


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

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

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

@plugwash Див. "Роздуми про довіру довіри" від Кена Томпсона - ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf

3

Доведення за допомогою індукції

Індуктивний крок

N + 1-я версія компілятора написана в X.

Таким чином, він може бути складений п ятою версією компілятора (також написаною в X).

Базовий корпус

Але перша версія компілятора, написана на X, повинна бути складена компілятором для X, який написаний мовою, відмінною від X. Цей етап називається завантаженням компілятора.


1
Перший компілятор для мови X може бути легко записаний на X. Як це можливо, це може бути інтерпретовано цей перший компілятор . (Перекладачем X, написаним мовою, відмінною від X).
Каз

0

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

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

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.