Как компилятор может скомпилировать себя?

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

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

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

9 ответов


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

  1. первый компилятор CoffeeScript написан на Ruby, производя версию 1 В CoffeeScript
  2. исходный код компилятора CS переписывается в CoffeeScript 1
  3. исходный компилятор CS компилирует новый код (написанный на CS 1) в версию 2 компилятора
  4. в исходный код компилятора вносятся изменения для добавления новых языковых функций
  5. второй компилятор CS (первый написанный на CS) компилирует пересмотренный новый исходный код в версию 3 компилятора
  6. повторите шаги 4 и 5 для каждого итерация

примечание: Я не уверен, как именно нумеруются версии CoffeeScript, это был просто пример.

этот процесс обычно называется загрузки. Другим примером загрузочного компилятора является rustc компилятор для язык Руст.


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

идея компилятора, который компилирует свой собственный код, смутно похожа на Куайн: исходный код, который при выполнении выдает исходный исходный код. вот один пример в код 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';

... и это будет правильно! The 10 исходит от компилятора и больше не нуждается в явном определении в источнике компилятора код.1

это один из примеров функции языка C, которая была реализована в коде C. Теперь повторите этот процесс для каждой функции языка, и у вас есть компилятор "self-hosting": компилятор C, написанный на C.


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


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

  1. компилятор CoffeeScript-это программа, которая может компилировать программы, написанные в CoffeeScript.
  2. компилятор CoffeeScript-это программа, написанная на CoffeeScript.

Я уверен, вы можете согласиться, что оба #1 и #2 верны. Теперь взгляните на два утверждения. Теперь вы видите, что для компилятора CoffeeScript совершенно нормально компилировать компилятор CoffeeScript?

компилятору все равно что он компилирует. Пока это программа, написанная в CoffeeScript, она может ее скомпилировать. И компилятор CoffeeScript сам по себе является такой программой. Компилятор CoffeeScript не заботится о том, что он компилирует сам компилятор CoffeeScript. Все ее видят некоторые в CoffeeScript код. Период.

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

Да, это именно то, что означает это утверждение, и я надеюсь, что теперь вы видите, как это утверждение истинно.


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

это означает именно это. Во-первых, нужно кое-что обдумать. Есть четыре объекта, на которые нам нужно посмотреть:

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

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

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

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

введите процесс под названием загрузки.

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

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

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

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

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


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

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

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


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

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

enter image description here Это известно как компилятор self-hosting.

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


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

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

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

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


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

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

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

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

базовый

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


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

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

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

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