Как компилятор сам компилируется?

168

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

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

Как компилятор может компилировать себя сам или что означает это утверждение?

AlexanderRD
источник
14
Еще один термин для компилятора, который может компилировать себя - это self-hostingкомпилятор. См programmers.stackexchange.com/q/263651/6221
oɔɯǝɹ
37
Почему компилятор не должен быть в состоянии скомпилировать себя?
user253751 19.06.16
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 ), основанный на том, что сделал это дважды прежде, чем вручную. Ввод кода здесь осуществляется через переднюю панель, которая позволяет напрямую сохранять значения по адресам памяти, управляемым тумблерами для битов.
Джереми У. Шерман
60

В статье « Размышления о доверии» Кен Томпсон, один из создателей Unix, пишет увлекательный (и легко читаемый) обзор того, как компилятор C сам компилируется. Подобные понятия могут быть применены к CoffeeScript или любому другому языку.

Идея компилятора, который компилирует свой собственный код, в некоторой степени похожа на quine : исходный код, который при выполнении выдает на выходе исходный исходный код. Вот один из примеров стиля CoffeeScript. Томпсон привел этот пример C-quine:

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);
}

Далее вам может быть интересно, как компилятору учат, что escape-последовательность, подобная '\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.


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

200_success
источник
7
Хотя это интересная информация, я не думаю, что она отвечает на вопрос. В ваших примерах предполагается, что у вас уже есть загрузочный компилятор, или на каком языке написан компилятор C?
Артуро Торрес Санчес
9
@ ArturoTorresSánchez Различные объяснения хорошо работают для разных людей. Я не собираюсь повторять сказанное в других ответах. Скорее я нахожу, что другие ответы говорят на более высоком уровне, чем мне нравится думать. Лично я предпочитаю конкретную иллюстрацию того, как добавляется одна функция, и позволить читателю экстраполировать ее, а не поверхностный обзор.
200 успехов
5
Хорошо, я понимаю вашу точку зрения. Просто вопрос в том, «как компилятор может компилироваться сам, если компилятор для компиляции не существует», а не в том, «как добавить новые функции в загрузочный компилятор».
Артуро Торрес Санчес
17
Сам вопрос неоднозначный и открытый. Похоже, что некоторые люди интерпретируют это как «как компилятор CoffeeScript сам компилируется?». Легкомысленный ответ, как дано в комментарии, «почему он не должен быть в состоянии скомпилировать себя так же, как он компилирует какой-либо код? Я истолковываю это как «как может появиться компилятор с автономным размещением?», И привел иллюстрацию того, как компилятору можно научить об одной из его собственных особенностей языка. Он отвечает на вопрос по-другому, предоставляя низкоуровневую иллюстрацию того, как он реализован.
200_success
1
@ ArturoTorresSánchez: «На каком языке написан компилятор C?» Давным-давно я поддерживал оригинальный компилятор C, упомянутый в старом приложении K & R (для IBM 360.) Многие люди знают, что сначала был BCPL, затем B, и что C был улучшенной версией B. На самом деле, было много части этого старого компилятора, которые все еще были написаны на B и никогда не переписывались на C. Переменные имели форму одной буквы / цифры, арифметика указателей не предполагалась автоматически масштабируемой и т. д. Этот старый код свидетельствовал о начальная загрузка с B на C. Первый компилятор "C" был написан на B.
Элиягу Скочилас
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, которая должна быть идентичной (игнорируя такие вещи, как метки времени).
user253751
9

Как компилятор может компилировать себя сам или что означает это утверждение?

Это означает именно это. Прежде всего, некоторые вещи, чтобы рассмотреть. Нам нужно рассмотреть четыре объекта:

  • Исходный код любой произвольной программы CoffeScript
  • (Сгенерированная) сборка любой произвольной программы CoffeScript
  • Исходный код компилятора CoffeScript
  • (Сгенерированная) сборка компилятора CoffeScript

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

Теперь сам компилятор CoffeScript - это просто произвольная программа CoffeScript, и, следовательно, он может быть скомпилирован компилятором CoffeScript.

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

Введите процесс, называемый начальной загрузкой .

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

Теперь вам нужно добавить новые функции. Скажем, вы реализовали только while-loops, но также хотите for-loops. Это не проблема, поскольку вы можете переписать любой for-loop таким образом, чтобы он был while-loop. Это означает, что вы можете использовать только while-loops в исходном коде вашего компилятора, поскольку имеющаяся у вас сборка может только компилировать его. Но вы можете создавать функции внутри вашего компилятора, которые могут создавать и компилировать forциклы с ним. Затем вы используете уже имеющуюся сборку и компилируете новую версию компилятора. И теперь у вас есть сборка компилятора, который также может анализировать и компилировать for-loops! Теперь вы можете вернуться к исходному файлу вашего компилятора и переписать любые while-loops, которые вам не нужны, в for-loops.

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

whileи, forочевидно, были только примеры, но это работает для любой новой языковой функции, которую вы хотите. И тогда вы попадаете в ситуацию, в которой находится CoffeScript: компилятор сам компилируется.

Там много литературы. Размышления о доверии - это классика, которую каждый, интересующийся этой темой, должен прочитать хотя бы один раз.

Polygnome
источник
5
(Предложение «Сам компилятор CoffeeScript написан на CoffeeScript», верно, но «Компилятор может компилировать себя» неверно.)
pabrams
4
Нет, это полностью верно. Компилятор может компилировать себя. Это просто не имеет смысла. Допустим, у вас есть исполняемый файл, который может скомпилировать версию X языка. Вы пишете компилятор, который может скомпилировать версию X + 1, и скомпилировать ее с имеющимся у вас компилятором (то есть версией X). В итоге вы получите исполняемый файл, который может скомпилировать версию X + 1 языка. Теперь вы можете использовать этот новый исполняемый файл для повторной компиляции компилятора. Но для чего? У вас уже есть исполняемый файл, который делает то, что вы хотите. Компилятор может скомпилировать любую допустимую программу, поэтому он может полностью скомпилировать себя!
Полигном
1
На самом деле, сборку довольно часто делают неслыханно, iirc modern freepascal собирает компилятор всего 5 раз.
plugwash
1
@pabrams Надпись «Не трогай» и «Горячий объект. Не трогай» не имеет никакого значения для предполагаемого сообщения фразы. Пока целевая аудитория сообщения (программисты) понимает предполагаемое сообщение фразы (сборка компилятора может компилировать ее источник) независимо от того, как оно написано, это обсуждение бессмысленно. В настоящее время ваш аргумент неверен. Если вы не можете показать, что целевая аудитория сообщения не является программистами, тогда и только тогда вы правы.
DarkDestry
2
@pabrams «Хороший английский» - это английский язык, который четко передает идеи предполагаемой аудитории и так, как задумал писатель или докладчик. Если предполагаемая аудитория - программисты, и программисты понимают это, это хороший английский. Сказать «Свет существует как частицы и волны» в основном эквивалентно «Свет существует как фотоны и электромагнитные волны». Для физика они означают буквально одно и то же. Означает ли это, что мы всегда должны использовать более длинную и четкую форму? Нет! Потому что это затрудняет чтение, когда смысл уже понятен предполагаемой аудитории.
DarkDestry
7

Небольшое, но важное уточнение

Здесь термин компилятор скрывает тот факт, что задействованы два файла. Одним из них является исполняемый файл, который принимает в качестве входных файлов, написанных на CoffeScript, и создает в качестве выходного файла другой исполняемый файл, объектный файл с возможностью связи или общую библиотеку. Другой - это исходный файл CoffeeScript, в котором описывается процедура компиляции CoffeeScript.

Вы применяете первый файл ко второму, создавая третий, который способен выполнять тот же акт компиляции, что и первый (возможно, больше, если второй файл определяет функции, не реализованные первым), и поэтому может заменить первый, если вы так хочется.

nbro
источник
4
  1. Компилятор CoffeeScript был впервые написан на Ruby.
  2. Компилятор CoffeeScript был затем переписан на CoffeeScript.

Поскольку версия компилятора CoffeeScript на Ruby уже существует, она использовалась для создания версии CoffeeScript компилятора CoffeeScript.

введите описание изображения здесь Это известно как самодостаточный компилятор .

Это чрезвычайно распространено и обычно является результатом желания автора использовать свой собственный язык для поддержания роста этого языка.

Тревор Хикки
источник
3

Здесь дело не в компиляторах, а в выразительности языка, поскольку компилятор - это просто программа, написанная на каком-то языке.

Когда мы говорим, что «язык написан / реализован», мы фактически имеем в виду, что реализован компилятор или интерпретатор для этого языка. Существуют языки программирования, на которых вы можете писать программы, которые реализуют язык (являются компиляторами / интерпретаторами для одного и того же языка). Эти языки называются универсальными языками .

Чтобы понять это, подумайте о металлическом токарном станке. Это инструмент, используемый для придания формы металлу. Возможно, используя только этот инструмент, создать другой, идентичный инструмент, создав его части. Таким образом, этот инструмент является универсальной машиной. Конечно, первый был создан с использованием других средств (других инструментов) и, вероятно, был более низкого качества. Но первый использовался для создания новых с более высокой точностью.

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

Paul92
источник
Мне нравится токарная аналогия. В отличие от аналогии с токарным станком, недостатки первой итерации компилятора передаются всем последующим компиляторам. Например, вышеупомянутый ответ упоминает о добавлении функции for-loop, когда оригинальный компилятор использует только циклы while. Выходные данные понимают циклы for, но реализация выполняется с циклами while. Если оригинальная реализация цикла while ошибочна или неэффективна, то так будет всегда!
@ Physics-Compute это просто неправильно. При отсутствии злостных дефектов обычно не распространяются при компиляции.
plugwash
Переводы сборки, безусловно, передаются от итерации к итерации, пока перевод сборки не будет исправлен. Новые функции, которые строят старые функции, не изменяют базовую реализацию. Подумайте об этом немного.
@plugwash См. «Размышления о доверии к доверию» Кена Томпсона - ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf
3

Доказательство по индукции

Индуктивный шаг

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

Таким образом, он может быть скомпилирован n-й версией компилятора (также написанной на X).

Базовый вариант

Но первая версия компилятора, написанная на X, должна быть скомпилирована компилятором для X, написанным на языке, отличном от X. Этот шаг называется начальной загрузкой компилятора.

Гай Арго
источник
1
Самый первый компилятор компилятора для языка X может быть легко написан на X. Это возможно, так как этот первый компилятор можно интерпретировать . (Переводчиком X, написанным на языке, отличном от X).
Каз
0

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

Кросс-компиляторы перемещаются из одной системы в другую, кросс-языковые компиляторы компилируют одну языковую спецификацию в другую языковую спецификацию.

По сути, компиляция - это просто перевод, и уровень, как правило, от уровня языка выше уровня языка ниже, но есть много вариантов.

Само собой разумеется, что компиляторы начальной загрузки являются наиболее запутанными, поскольку они компилируют язык, на котором они написаны. Не забудьте начальный шаг начальной загрузки, который требует как минимум минимальной существующей версии, которая является исполняемой. Многие загрузочные компиляторы сначала работают с минимальными функциями языка программирования и добавляют дополнительные сложные языковые функции, если новая функция может быть выражена с использованием предыдущих функций. Если бы это было не так, потребовалось бы, чтобы эта часть «компилятора» была заранее разработана на другом языке.

nbro
источник