Я изучаю CoffeeScript на веб-сайте http://coffeescript.org/, и у него есть текст
Компилятор CoffeeScript сам написан на CoffeeScript
Как компилятор может компилировать себя или что означает это выражение?
Я изучаю CoffeeScript на веб-сайте http://coffeescript.org/, и у него есть текст
Компилятор CoffeeScript сам написан на CoffeeScript
Как компилятор может компилировать себя или что означает это выражение?
Первое издание компилятора не может быть сгенерировано машиной из определенного для него языка программирования; ваше замешательство понятно. Более поздняя версия компилятора с более языковыми функциями (с исходным кодом, переписанным в первой версии нового языка) может быть построена первым компилятором. Затем эта версия может скомпилировать следующий компилятор и так далее. Вот пример:
Примечание. Я не уверен, как именно нумеруются версии CoffeeScript, что было просто примером.
Этот процесс обычно называется bootstrapping. Другим примером компилятора начальной загрузки является rustc
, компилятор для Язык ржавчины.
В статье Размышления о доверии Trust Кен Томпсон, один из создателей 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. Теперь повторите этот процесс для каждой отдельной функции языка, и у вас есть компилятор "self-hosting": компилятор C, написанный на C.
1 Твист сюжета, описанный в статье, состоит в том, что, поскольку компилятор может быть "обучен" таким фактам, он также может быть неправильно обучен для генерации троянных исполняемых файлов способом, который трудно обнаружить, и такой акт саботажа может сохраняться во всех компиляторах, созданных испорченным компилятором.
Вы уже получили очень хороший ответ, однако я хочу предложить вам другую перспективу, которая, надеюсь, будет для вас просветляющей. Давайте сначала установим два факта, с которыми мы можем согласиться:
Я уверен, что вы можете согласиться с тем, что оба # 1 и # 2 верны. Теперь рассмотрим два утверждения. Вы видите теперь, что компилятор CoffeeScript вполне нормально компилировать компилятор CoffeeScript?
Компилятору все равно, что он компилирует. Пока это программа, написанная на CoffeeScript, она может ее скомпилировать. И сам компилятор CoffeeScript просто оказывается такой программой. Компилятору CoffeeScript не волнует, что сам компилятор CoffeeScript он компилирует. Все, что он видит, - это код CoffeeScript. Период.
Как компилятор может компилировать себя или что означает это выражение?
Да, это именно то, что означает это утверждение, и я надеюсь, что теперь вы можете видеть, как это утверждение истинно.
Как компилятор может компилировать себя или что означает это выражение?
Это означает именно это. Прежде всего, нужно рассмотреть некоторые вещи. Нам нужно рассмотреть четыре объекта:
Теперь должно быть очевидно, что вы можете использовать сгенерированную сборку - исполняемый файл - компилятора CoffeScript для компиляции любой произвольной программы CoffeScript и сгенерировать сборку для этой программы.
Теперь сам компилятор CoffeScript является просто произвольной программой CoffeScript и, следовательно, может быть скомпилирован компилятором CoffeScript.
Кажется, что ваше замешательство проистекает из того факта, что, когда вы создаете свой собственный новый язык, у вас нет компилятора, который вы можете использовать для компиляции вашего компилятора. Это, безусловно, выглядит как проблема куриного яйца, правильно?
Представьте процесс загрузочный.
Теперь вам нужно добавить новые функции. Скажем, вы только реализовали while
-loops, но также хотите for
-loops. Это не проблема, так как вы можете переписать любой for
-loop таким образом, что это while
-loop. Это означает, что вы можете использовать while
-loops в исходном коде вашего компилятора, поскольку собранная вами сборка может только скомпилировать их. Но вы можете создавать функции внутри вашего компилятора, которые могут сбрасывать и компилировать for
-loops с ним. Затем вы используете уже имеющуюся сборку и компилируете новую версию компилятора. И теперь у вас есть сборка компилятора, который также может анализировать и компилировать for
-loops! Теперь вы можете вернуться к исходному файлу своего компилятора и переписать все while
-loops, которые вы не хотите в for
-loops.
Промойте и повторите, пока все желательные языковые функции не будут скомпилированы вместе с компилятором.
while
и for
, очевидно, были только примерами, но это работает для любой новой функции языка, которую вы хотите. И тогда вы находитесь в ситуации, когда CoffeScript находится сейчас: компилятор компилируется.
Там много литературы. Размышления о Trusting Trust - это классик, которому все заинтересованы в том, что эта тема должна читаться хотя бы один раз.
Здесь термин компилятор замалчивает тот факт, что есть два файла. Один из них представляет собой исполняемый файл, который принимает в качестве входных файлов, написанных в CoffeScript, и в качестве выходного файла выдает другой исполняемый файл, связанный объектный файл или общую библиотеку. Другой - исходный файл CoffeeScript, который просто описывает процедуру компиляции CoffeeScript.
Вы применяете первый файл ко второму, создавая третью, которая способна выполнять тот же акт компиляции, что и первый (возможно, больше, если второй файл определяет функции, не реализованные первым), и поэтому может заменить сначала, если вы этого желаете.
Поскольку Ruby-версия компилятора CoffeeScript уже существовала, она использовалась для создания версии CoffeeScript компилятора CoffeeScript.
Это называется компилятор самообслуживания.
Это чрезвычайно распространено и обычно возникает из-за стремления автора использовать свой собственный язык для поддержания роста языка.
Это не вопрос компиляторов, а вопрос выразительности языка, поскольку компилятор - это просто программа, написанная на каком-то языке.
Когда мы говорим, что "язык написан/реализован", мы фактически имеем в виду, что реализован компилятор или интерпретатор для этого языка. Существуют языки программирования, в которых вы можете писать программы, которые реализуют язык (являются компиляторами/интерпретаторами для одного языка). Эти языки называются универсальными языками.
Чтобы понять это, подумайте о металлическом токарном станке. Это инструмент, используемый для формирования металла. Возможно, используя этот инструмент, создать другой, идентичный инструмент, создав его части. Таким образом, этот инструмент является универсальной машиной. Конечно, первый был создан с использованием других средств (других инструментов) и, вероятно, был более низкого качества. Но первый был использован для создания новых с большей точностью.
3D-принтер - это почти универсальная машина. Вы можете распечатать весь 3D-принтер с помощью 3D-принтера (вы не можете создать наконечник, который плавит пластик).
n + 1-я версия компилятора написана в X.
Таким образом, он может быть скомпилирован n-й версией компилятора (также написан на X).
Но первая версия компилятора, написанная на X, должна быть скомпилирована компилятором для X, написанным на языке, отличном от X. Этот шаг называется загрузочным компилятором.
Компиляторы берут высокоуровневую спецификацию и превращают ее в низкоуровневую реализацию, например, могут выполняться на аппаратном уровне. Следовательно, нет никакой связи между форматом спецификации и фактическим исполнением, кроме семантики языка, на который нацеливается.
Перекрестные компиляторы перемещаются из одной системы в другую, кросс-язычные компиляторы компилируют спецификацию одного языка в другую спецификацию языка.
В основном компиляция - это просто перевод, и уровень обычно является языком более высокого уровня для более низкого уровня языка, но существует много вариантов.
Компиляторы начальной загрузки наиболее запутанны, конечно, потому что они компилируют язык, на котором они написаны. Не забывайте начальный шаг в начальной загрузке, который требует, по крайней мере, минимальной существующей версии, которая является исполняемой. Многие загрузочные компиляторы сначала работают с минимальными функциями языка программирования и добавляют дополнительные сложные языковые функции в будущее, пока новая функция может быть выражена с использованием предыдущих функций. Если бы это было не так, это потребовало бы, чтобы эта часть "компилятора" была разработана на другом языке заранее.