Интуитивно кажется, что компилятор языка Foo
сам по себе не может быть написан на Foo. Более конкретно, первый компилятор для языка Foo
не может быть написан на Foo, но любой последующий компилятор может быть написан для Foo
.
Но так ли это на самом деле? У меня есть очень смутные воспоминания о чтении языка, первый компилятор которого был написан «сам по себе». Возможно ли это, и если да, то как?
Ответы:
Это называется "начальной загрузкой". Сначала вы должны построить компилятор (или интерпретатор) для вашего языка на каком-либо другом языке (обычно Java или C). Как только это будет сделано, вы можете написать новую версию компилятора на языке Foo. Вы используете первый компилятор начальной загрузки для компиляции компилятора, а затем используете этот компилированный компилятор для компиляции всего остального (включая будущие версии самого себя).
Большинство языков действительно создаются таким образом, отчасти потому, что разработчикам языков нравится использовать язык, который они создают, а также потому, что нетривиальный компилятор часто служит полезным эталоном для определения того, насколько «полным» может быть язык.
Примером этого будет Scala. Его первый компилятор был создан в Pizza, экспериментальном языке Мартином Одерским. Начиная с версии 2.0, компилятор был полностью переписан в Scala. С этого момента старый компилятор Pizza может быть полностью исключен из-за того, что новый компилятор Scala можно использовать для компиляции для будущих итераций.
источник
Я вспоминаю, как слушал подкаст Radio Engineering Radio, в котором Дик Габриэль говорил о начальной загрузке оригинального интерпретатора LISP, написав черновую версию LISP на бумаге и вручную собрав ее в машинный код. С тех пор остальные функции LISP были написаны и интерпретированы с помощью LISP.
источник
Добавление любопытства к предыдущим ответам.
Вот цитата из руководства Linux From Scratch , на шаге, с которого начинается сборка компилятора GCC из его источника. (Linux From Scratch - это способ установить Linux, который радикально отличается от установки дистрибутива тем, что вам нужно скомпилировать каждый отдельный двоичный файл целевой системы.)
Такое использование цели «bootstrap» мотивируется тем фактом, что компилятор, который используется для построения набора инструментов целевой системы, может не иметь ту же версию целевого компилятора. Поступая таким образом, вы обязательно получите в целевой системе компилятор, который может компилироваться сам.
источник
Когда вы пишете свой первый компилятор для C, вы пишете на другом языке. Теперь у вас есть компилятор для C, скажем, на ассемблере. В конце концов, вы придете к тому месту, где вам придется анализировать строки, особенно экранирующие последовательности. Вы напишите код для преобразования
\n
в символ с десятичным кодом 10 (и\r
до 13 и т. Д.).После того, как этот компилятор будет готов, вы начнете переопределять его на C. Этот процесс называется « начальной загрузкой ».
Код разбора строки станет:
Когда это компилируется, у вас есть двоичный файл, который понимает '\ n'. Это означает, что вы можете изменить исходный код:
Так где же информация о том, что '\ n' - это код для 13? Это в двоичном коде! Это похоже на ДНК: компиляция исходного кода C с этим двоичным файлом унаследует эту информацию. Если компилятор сам компилируется, он передает эти знания своим потомкам. С этого момента нет никакого способа увидеть из одного источника, что будет делать компилятор.
Если вы хотите спрятать вирус в исходном коде какой-либо программы, вы можете сделать это следующим образом: получить исходный код компилятора, найти функцию, которая компилирует функции, и заменить ее следующим:
Интересными частями являются A и B. A - это исходный код для
compileFunction
включения вируса, вероятно, каким-то образом зашифрованный, так что это не очевидно из поиска в полученном двоичном файле. Это гарантирует, что компиляция с самим компилятором сохранит код внедрения вируса.B - то же самое для функции, которую мы хотим заменить нашим вирусом. Например, это может быть функция «login» в исходном файле «login.c», которая, вероятно, из ядра Linux. Мы могли бы заменить его версией, которая будет принимать пароль «joshua» для учетной записи root в дополнение к обычному паролю.
Если вы скомпилируете это и распространите как двоичный файл, вы не сможете найти вирус, посмотрев на источник.
Первоначальный источник идеи: https://web.archive.org/web/20070714062657/http://www.acm.org/classics/sep95/
источник
Вы не можете написать компилятор сам по себе, потому что вам не с чем компилировать исходный код. Есть два подхода к решению этой проблемы.
Наименее предпочтительным является следующее. Вы пишете минимальный компилятор на ассемблере (yuck) для минимального набора языка, а затем используете этот компилятор для реализации дополнительных возможностей языка. Создайте свой путь до тех пор, пока у вас не будет компилятора со всеми возможностями языка для себя. Болезненный процесс, который обычно выполняется только тогда, когда у вас нет другого выбора.
Предпочтительным подходом является использование кросс-компилятора. Вы изменяете серверную часть существующего компилятора на другом компьютере, чтобы создать выходные данные, которые запускаются на целевом компьютере. Тогда у вас есть хороший полноценный компилятор, работающий на целевой машине. Наиболее популярным для этого является язык Си, так как существует множество существующих компиляторов с подключаемыми бэкэндами, которые можно заменить.
Малоизвестным фактом является то, что компилятор GNU C ++ имеет реализацию, которая использует только подмножество C. Причина в том, что обычно легко найти компилятор C для новой целевой машины, который позволит вам затем собрать из него полный компилятор GNU C ++. Теперь у вас есть возможность загрузить компилятор C ++ на целевой машине.
источник
Как правило, сначала вам нужно, чтобы компилятор работал (если он был примитивным), и тогда вы можете начать думать о том, чтобы сделать его автономным. На самом деле это считается важной вехой в некоторых языках.
Из того, что я помню из «моно», вполне вероятно, что им понадобится добавить несколько вещей к размышлению, чтобы заставить его работать: команда моно продолжает указывать, что некоторые вещи просто невозможны
Reflection.Emit
; Конечно, команда MS может доказать, что они не правы.Это имеет несколько реальных преимуществ: это довольно хороший юнит-тест, для начинающих! И у вас есть только один язык для беспокойства (то есть, возможно, эксперт по C # может не знать C ++; теперь вы можете исправить компилятор C #). Но мне интересно, нет ли здесь какой-то профессиональной гордости: они просто хотят, чтобы это был хостинг.
Не совсем компилятор, но я недавно работал над системой, которая является хостингом; генератор кода используется для генерации генератора кода ... поэтому, если схема меняется, я просто запускаю ее на себя: новая версия. Если есть ошибка, я просто возвращаюсь к более ранней версии и пытаюсь снова. Очень удобно и очень легко поддерживать.
Обновление 1
Я только что посмотрел это видео Андерса в PDC, и (около часа спустя) он приводит несколько более веских причин - все о компиляторе как сервисе. Только для записи.
источник
Вот дамп (на самом деле трудная тема для поиска):
Болтовня
С
Это также идея PyPy и Rubinius :
(Я думаю, что это также может относиться к Форту , но я ничего не знаю о Форт.)
источник
GNAT, компилятор GNU Ada, требует полной компиляции Ada. Это может быть проблемой при переносе его на платформу, где нет готового двоичного файла GNAT.
источник
На самом деле, большинство компиляторов написаны на языке, который они компилируют, по причинам, указанным выше.
Первый компилятор начальной загрузки обычно написан на C, C ++ или Assembly.
источник
Компилятор C # проекта Mono долгое время находился в автономном режиме, что означает, что он был написан на самом C #.
Что я знаю, так это то, что компилятор был запущен как чистый C-код, но как только были реализованы «основные» функции ECMA, они начали переписывать компилятор в C #.
Я не знаю о преимуществах написания компилятора на одном и том же языке, но уверен, что он связан, по крайней мере, с возможностями, которые может предложить сам язык (например, C не поддерживает объектно-ориентированное программирование) ,
Вы можете найти больше информации здесь .
источник
Я написал SLIC (Систему языков для реализации компиляторов) сам по себе. Затем рука скомпилировала его в сборку. SLIC имеет много общего, так как это был один компилятор из пяти подъязыков:
SLIC был вдохновлен CWIC (Компилятор для написания и реализации компиляторов). В отличие от большинства пакетов разработки компиляторов, SLIC и CWIC предназначены для генерации кода со специализированными, специфичными для домена языками. SLIC расширяет генерацию кода CWIC, добавляя подязыки ISO, PSEUDO и MACHOP, отделяя специфику целевой машины от языка генератора обхода дерева.
LISP 2 деревья и списки
Система динамического управления памятью на основе языка генератора LISP 2 является ключевым компонентом. Списки выражаются на языке, заключенном в квадратные скобки, компоненты которого разделены запятыми, то есть списком из трех элементов [a, b, c].
Деревья:
представлены списками, первая запись которых является объектом узла:
Деревья обычно отображаются с отдельным узлом, предшествующим ветвям:
Разбор с использованием функций генератора на основе LISP 2
Функция генератора - это именованный набор (unparse) => action> пар ...
Разреженные выражения - это тесты, которые соответствуют шаблонам дерева и / или типам объектов, разбивая их на части и назначая эти части локальной переменной для обработки ее процедурным действием. Вроде как перегруженная функция, принимающая разные типы аргументов. За исключением () => ... тесты предпринимаются в кодированном порядке. Первым успешно разобрано выполнение соответствующего ему действия. Разреженные выражения - это разборки тестов. ADD [x, y] соответствует дереву ADD с двумя ветвями, присваивая его ветви локальным переменным x и y. Действие может быть простым выражением или блоком ограниченного кода .BEGIN ... .END. Я бы использовал блоки c style {...} сегодня. Соответствие дерева, [], разбор правил может вызывать генераторы, передающие возвращенные результаты в действие:
В частности, приведенное выше выражение expr_gen unparse соответствует дереву ADD с двумя ветвями. Внутри тестового шаблона будет вызываться генератор одного аргумента, помещенный в ветвь дерева с этой ветвью. Его список аргументов - это локальные переменные, которым назначены возвращаемые объекты Выше unparse указывает две ветки для разборки дерева ADD, рекурсивное нажатие каждой ветви на expr_gen. Левая ветвь возврата помещена в локальные переменные x. Аналогично, правая ветвь передается expr_gen с y возвращаемым объектом. Выше может быть частью числового выражения оценки. Существовали функции ярлыков, называемые векторами, которые были выше, вместо строки узлов вектор узлов мог использоваться с вектором соответствующих действий:
Вышеупомянутый более полный вычислитель выражений, присваивающий возврат из левой ветви expr_gen к x и правой ветви к y. Соответствующий вектор действия, выполненный для x и y, вернулся. Последние пары действий unparse => соответствуют объектам чисел и символов.
Символ и атрибуты символа
Символы могут иметь именованные атрибуты. val: (x) получить доступ к атрибуту val объекта символа, содержащегося в x. Обобщенный стек таблиц символов является частью SLIC. Таблицу SYMBOL можно нажимать и выдвигать, предоставляя локальные символы для функций. Вновь созданные символы заносятся в верхнюю таблицу символов. Поиск символов просматривает стек таблицы символов сначала из верхней таблицы вниз по стеку.
Генерация машинно-независимого кода
Язык генератора SLIC создает объекты инструкций PSEUDO, добавляя их в список кодов секций. .FLUSH вызывает запуск своего списка кодов PSEUDO, удаляя каждую инструкцию PSEUDO из списка и вызывая ее. После выполнения память объектов PSEUDO освобождается. Процедурные органы действий PSEUDO и GENERATOR в основном являются одним и тем же языком, за исключением их результатов. PSEUDO предназначены для работы в качестве макросов сборки, обеспечивающих машинно-независимую секвенизацию кода. Они обеспечивают отделение конкретной целевой машины от языка генератора обхода дерева. PSEUDO вызывают функции MACHOP для вывода машинного кода. MACHOP используются для определения псевдоопераций сборки (таких как dc, определение константы и т. Д.) И машинных инструкций или семейства похожих форматированных инструкций с использованием векторной записи. Они просто преобразуют свои параметры в последовательность битовых полей, составляющих инструкцию. Вызовы MACHOP должны выглядеть как сборка и обеспечивать форматирование печати полей, когда сборка отображается в списке компиляции. В коде примера я использую комментирование в стиле c, которое можно легко добавить, но не на оригинальных языках. MACHOP производят код в немного адресуемой памяти. Линкер SLIC обрабатывает вывод компилятора. MACHOP для инструкций пользовательского режима DEC-10 с использованием векторной записи: MACHOP производят код в немного адресуемой памяти. Линкер SLIC обрабатывает вывод компилятора. MACHOP для инструкций пользовательского режима DEC-10 с использованием векторной записи: MACHOP производят код в немного адресуемой памяти. Линкер SLIC обрабатывает вывод компилятора. MACHOP для инструкций пользовательского режима DEC-10 с использованием векторной записи:
.MORG 36, O (18): $ / 36; выравнивает местоположение по 36-битной границе, печатая адрес $ / 36 слова из 18 бит в восьмеричном виде. 9-битный регистр opcd, 4-битный регистр, косвенный бит и 4-битный индексный регистр объединяются и печатаются, как будто одно 18-битное поле. 18-битный адрес / 36 или непосредственное значение выводится и печатается в восьмеричном виде. Пример MOVEI распечатать с r1 = 1 и r2 = 2:
С опцией сборки компилятора вы получите сгенерированный код сборки в списке компиляции.
Свяжите это вместе
Компоновщик SLIC поставляется в виде библиотеки, которая обрабатывает связывание и разрешение символов. Целевое форматирование выходного файла загрузки должно быть записано для целевых машин и связано с библиотекой библиотеки компоновщика.
Язык генератора способен записывать деревья в файл и читать их, что позволяет реализовать многопроходный компилятор.
Краткое резюме генерации кода и происхождения
Сначала я рассмотрел генерацию кода, чтобы убедиться, что он понимает, что SLIC был настоящим компилятором компилятора. SLIC был вдохновлен CWIC (компилятором для написания и реализации компиляторов), разработанным в корпорации Systems Development в конце 1960-х годов. В CWIC были только языки SYNTAX и GENERATOR, производящие числовой байтовый код из языка GENERATOR. Байт-код был помещен или помещен (термин, используемый в документации CWIC) в буферы памяти, связанные с именованными разделами и записанные с помощью оператора .FLUSH. Документ ACM о CWIC доступен из архивов ACM.
Успешная реализация основного языка программирования
В конце 1970-х SLIC использовался для написания кросс-компилятора COBOL. Завершено примерно за 3 месяца в основном одним программистом. Я работал немного с программистом по мере необходимости. Другой программист написал библиотеку времени выполнения и MACHOP для целевого мини-компьютера TI-990. Этот компилятор COBOL компилировал значительно больше строк в секунду, чем собственный компилятор COBOL DEC-10, написанный на ассемблере.
Больше компилятору тогда обычно говорили о
Большая часть написания компилятора с нуля - это библиотека времени выполнения. Вам нужна таблица символов. Вам нужен ввод и вывод. Динамическое управление памятью и т. Д. Написание библиотеки времени выполнения для компилятора может оказаться более трудоемким, чем написание компилятора. Но с SLIC эта библиотека времени выполнения является общей для всех компиляторов, разрабатываемых в SLIC. Обратите внимание, что есть две библиотеки времени выполнения. Один для целевой машины языка (например, COBOL). Другая - это библиотека времени выполнения компиляторов.
Я думаю, я установил, что это не генераторы парсеров. Так что теперь с небольшим пониманием серверной части я могу объяснить язык программирования анализатора.
Парсер языка программирования
Парсер написан с использованием формулы, написанной в виде простых уравнений.
Элементом языка на самом низком уровне является символ. Токены формируются из подмножеств символов языка. Классы символов используются для именования и определения этих подмножеств символов. Определяющим классом символов является символ двоеточия (:). Символы, которые являются членами класса, закодированы в правой части определения. Печатные символы заключены в простые простые строки. Непечатаемые и специальные символы могут быть представлены их числовым порядковым номером. Члены класса разделены альтернативой | оператор. Формула класса заканчивается точкой с запятой. Классы символов могут включать ранее определенные классы:
Skip_class 0b00000001 предопределен, но может быть слишком длинным, определяя skip_class.
Итак, класс символов - это список альтернатив, который может быть только символьной константой, порядковым номером символа или ранее определенным классом символов. Как я реализовал классы символов: формуле класса назначается битовая маска класса. (Показано в комментариях выше). Любая формула класса, имеющая какой-либо символьный литерал или порядковый номер, вызывает выделение бита класса. Маска создается путем направления маски (ей) классов включенного класса (классов) вместе с выделенным битом (если есть). Таблица классов создается из классов символов. Запись, индексированная по порядковому номеру символа, содержит биты, указывающие членство класса в классе. Классовое тестирование проводится в линию. Пример кода IA-86 с порядковым номером символа в eax иллюстрирует тестирование класса:
Вслед за:
или
Примеры кода инструкции IA-86 используются, потому что я думаю, что инструкции IA-86 более широко известны сегодня. Имя класса, вычисляемое по его маске класса, неразрушающим образом и с таблицей классов, индексированной по порядковым номерам символов (в eax). Ненулевой результат указывает на членство в классе. (EAX обнуляется, за исключением al (младшие 8 битов EAX), который содержит символ).
Токены были немного другими в этих старых компиляторах. Ключевые слова не были объяснены как токены. Они просто были сопоставлены строковыми константами в кавычках на языке синтаксического анализатора. Строки в кавычках обычно не сохраняются. Модификаторы могут быть использованы. A + поддерживает соответствие строки. (т. е. + '-' соответствует символу -, сохраняя символ в случае успеха) Операция, (т. е. 'E') вставляет строку в токен. Пробел обрабатывается формулой токена, пропускающей первые символы SKIP_CLASS, пока не будет найдено первое совпадение. Обратите внимание, что явное сопоставление символов skip_class остановит пропуск, позволяя токену начинаться с символа skip_class. В формуле строкового токена пропускаются первые символы skip_class, соответствующие одинарным кавычкам или двойным кавычкам. Интерес представляет сопоставление «символа в строке» в кавычках:
Первый вариант соответствует любому символу в кавычках. Правильная альтернатива соответствует строке в двойных кавычках, которая может включать символы в двойных кавычках, использующие два символа «вместе» для представления одного символа. Эта формула определяет строки, используемые в ее собственном определении. Внутренняя правая альтернатива '"' $ (-" "". .ANY | "" "" "", "" "") '"' соответствует строке в двойных кавычках. Мы можем использовать один символ «в кавычках» для соответствия символу «двойной кавычки». Однако в строке «двойные» в кавычках, если мы хотим использовать «символ», мы должны использовать два символа, чтобы получить один. Например, во внутренней левой альтернативе, совпадающей с любым символом, кроме кавычки:
отрицательный заглядывание вперед - "" "" используется, если в случае успеха (не совпадает с "символ"), то соответствует .ANY символ (который не может быть "символом, потому что -" "" "исключил эту возможность). Правильная альтернатива берет на себя - "" "" сопоставление "символа и неудача были правильной альтернативой:
пытается сопоставить два "символа, заменив их одним двойным" используя "," "" для вставки одного символа ". Обе внутренние альтернативы, не прошедшие закрывающий символ кавычки строки, сопоставляются, и MAKSTR [] вызывается для создания строкового объекта. последовательность, цикл при успешном выполнении, оператор используется при сопоставлении последовательности. Формула токена пропускает первые символы класса пропуска (пробел). Как только первое совпадение выполнено, пропуск skip_class отключен. Мы можем вызывать функции, запрограммированные на других языках, используя []. MAKSTR [], MAKBIN [], MAKOCT [], MAKHEX [], MAKFLOAT [] и MAKINT [] являются поставляемой библиотечной функцией, которая преобразует совпавшую строку токена в типизированный объект. Приведенная ниже формула числа иллюстрирует довольно сложное распознавание токена:
Приведенная выше формула числового токена распознает целые числа и числа с плавающей запятой. Альтернативы всегда успешны. Числовые объекты могут быть использованы в расчетах. Объекты-токены помещаются в стек анализа при успешном выполнении формулы. Показатель степени в (+ 'E' | 'e', 'E') интересен. Мы хотим всегда иметь заглавную E для MAKEFLOAT []. Но мы разрешаем заменять его строчными буквами «е», используя «E».
Возможно, вы заметили соответствие класса символов и формулы токена. Формула синтаксического анализа продолжается, добавляя альтернативы обратного отслеживания и операторы построения дерева. Альтернативные операторы с возвратом и без возврата могут не смешиваться в пределах уровня выражения. У вас может не быть (a | b \ c) смешивания без возврата | с альтернативой возврата. (a \ b \ c), (a | b | c) и ((a | b) \ c) действительны. Альтернатива \ backtracking сохраняет состояние анализа перед попыткой его левой альтернативы, а при сбое восстанавливает состояние анализа перед попыткой правильной альтернативы. В последовательности альтернатив первая удачная альтернатива удовлетворяет группе. Дальнейшие альтернативы не предпринимаются. Факторинг и группировка обеспечивают непрерывный анализ. Альтернатива backtrack создает сохраненное состояние синтаксического анализа, прежде чем попытается использовать левую альтернативу. Возврат требуется, когда анализ может выполнить частичное совпадение, а затем потерпеть неудачу:
В приведенном выше примере, если возвращается ошибка, предпринимается попытка альтернативного компакт-диска. Если тогда c возвращает ошибку, будет предпринята попытка возврата. Если a завершился успешно, а b потерпел неудачу, анализ будет возвращен и попытка e. Аналогично, неудачный c успешен и b терпит неудачу, анализ возвращается, и альтернатива принимается. Возврат не ограничен формулой. Если какая-либо формула синтаксического анализа делает частичное совпадение в любое время, а затем терпит неудачу, анализ сбрасывается на верхний обратный ход и берется его альтернатива. Ошибка компиляции может возникнуть, если код был выведен в том смысле, что был создан возврат. Возврат устанавливается перед началом компиляции. Возврат ошибки или возврат к ней - ошибка компилятора. Возвраты сложены. Мы можем использовать отрицательный - и положительный? операторы «заглянуть / посмотреть вперед» для тестирования без ускорения разбора. проверка строки - это шаг вперед, требующий сохранения и сброса входного состояния. Забегая вперед, можно было бы проанализировать выражение, которое до частичного сбоя выполнит частичное совпадение. Взгляд в будущее реализован с использованием возврата.
Язык парсера не является ни парсером LL, ни LR. Но язык программирования для написания рекурсивного приличного парсера, в котором вы программируете построение дерева:
Обычно используемый пример синтаксического анализа - это арифметическое выражение:
Использование Exp и Term с помощью цикла создает левостороннее дерево. Фактор, использующий правильную рекурсию, создает правостороннее дерево:
Вот немного компилятора cc, обновленная версия SLIC с комментариями в стиле c. Типы функций (грамматика, токен, класс символов, генератор, PSEUDO или MACHOP определяются их начальным синтаксисом, следующим за их идентификатором. С этими анализаторами сверху вниз вы начинаете с программы, определяющей формулу:
// Обратите внимание на то, как id отключается и позже объединяется при создании дерева.
Следует отметить, как язык синтаксического анализатора обрабатывает комментирование и исправление ошибок.
Я думаю, что я ответил на вопрос. Написав большую часть преемников SLIC, язык cc сам по себе здесь. Для него пока нет компилятора. Но я могу вручную скомпилировать его в ассемблерный код, голые функции asm c или c ++.
источник
Да, вы можете написать компилятор для языка на этом языке. Нет, вам не нужен первый компилятор для этого языка для начальной загрузки.
То, что вам нужно для начальной загрузки - это реализация языка. Это может быть либо компилятор, либо интерпретатор.
Исторически языки обычно рассматривались как интерпретируемые или компилируемые языки. Переводчики были написаны только для первых, а компиляторы - только для вторых. Поэтому, как правило, если компилятор будет написан для языка, первый компилятор будет написан на каком-то другом языке для его начальной загрузки, а затем, по желанию, компилятор будет переписан для предметного языка. Но вместо этого можно написать переводчика на другой язык.
Это не просто теоретическое. Я сейчас занимаюсь этим сам. Я работаю над компилятором для языка, Salmon, который я разработал сам. Сначала я создал компилятор Salmon на C, а сейчас я пишу компилятор на Salmon, поэтому я могу заставить работать компилятор Salmon, даже не имея компилятора для Salmon, написанного на любом другом языке.
источник
Может быть, вы можете написать BNF, описывающий BNF.
источник