Вы можете удивиться, узнав, что C в основном написан на C, что Java написана на Java и что Typescript написан на ... Typescript! Многим это кажется технологической версией вопроса «что появилось раньше, курица или яйцо?» вопрос. Конечно, это кажется противоречием - если C написано на C, разве C уже не должен существовать?

Почему мы пишем новые языки?

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

Новые парадигмы. Некоторые языки просто стремятся изучить новые парадигмы программирования, предлагая «новый образ мышления». Языки процедурного программирования, такие как C, сильно отличались от объектно-ориентированных языков, таких как Java, или функциональных языков, таких как Lisp. Новые способы мышления о программах приводят к новым уровням выразительности, которые могут просто не существовать в других парадигмах - представьте, что вы пытаетесь реализовать сокращение асинхронной карты в C или объекты в Лиспе. Как всегда, есть инструменты, лучше подходящие для определенных работ, и иногда создание нового инструмента - лучшее решение.

Новые домены. Подобно изучению новых парадигм, некоторые языки просто нацелены на новые домены. Например, HTML, Verilog, Matlab и другие языки были созданы просто для решения очень узкой проблемы и работы в очень конкретной области. Эти языки решают новые проблемы, не выходя за пределы языка общего назначения.

Серьезные улучшения. На многих языках довольно сложно исправить фундаментальные проблемы. Иногда сообщества довольно закрыты, а иногда у языка слишком много багажа и устаревших приложений, использующих его, поэтому вносить изменения было бы непрактично. Такие языки, как Rust, пытаются решить фундаментальные проблемы, такие как безопасность памяти в C, тогда как языки, подобные Python 3, пытаются решить структурные проблемы в своем предшественнике Python 2, в то время как Kotlin пытается удалить шаблон и добавить дополнительную безопасность в Java.

Итак, как и почему мы пишем язык X в X?

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

Первоначальная версия языка написана на другом языке. Для C это был B, а для Typescript - Javascript. Исходный исходный язык на самом деле не имеет большого значения, если вы можете использовать некоторую начальную реализацию своего языка. Эта первоначальная реализация обычно не завершена, но ее достаточно, чтобы вы начали работу с новым языком. Популярные варианты начальной реализации включают C, Scheme или даже генератор компилятора, такой как Yacc / Bison. Это нелегкий подвиг, но в конце концов мы вышли с Language V1, написанным на Some-Other-Language.

Дальше самое интересное. Мы хотим проверить, насколько хороша наша первоначальная реализация, чтобы увидеть, насколько полезен язык, найти ошибки и т. Д. Какой действительно хороший способ протестировать созданный нами компилятор? Конечно, мы могли бы попробовать создать на нем приложение, реализовать веб-сервер или решить любые другие общие проблемы программирования. Вместо этого мы собираемся протестировать наш язык и реализацию, написав наш язык на нашем языке с помощью компилятора версии 1. Эта первоначальная реализация будет немного ухабистой, но важно то, что мы завершили первоначальную реализацию. Это называется начальной загрузкой вашего компилятора. Подобно старому выражению «подтягивание себя с помощью бутстрапов», теперь мы используем наш компилятор для создания нового компилятора, основанного на языке V1 вместо Some-Other-Language, для создания языка V2. В будущем мы продолжим писать наши дополнительные языковые функции на нашем языке, и каждый раз мы будем генерировать новый компилятор.

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

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

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

Что это значит для меня?

Ясно, что идея начальной загрузки компилятора языка не такая уж и дикая, как могло показаться в начале этой статьи - мы буквально просто переписываем наш компилятор после первоначальной версии. Что же это значит для вас, читатель, тогда? В конечном счете, не очень. Если вы заинтересованы в построении языка, теперь вы можете лучше понять, что вам предстоит сделать, прежде чем ваш язык достигнет полной зрелости.

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

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