Написание компилятора на своем родном языке


204

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

Но так ли это на самом деле? У меня есть очень смутные воспоминания о чтении языка, первый компилятор которого был написан «сам по себе». Возможно ли это, и если да, то как?



Это очень старый вопрос, но, скажем, я написал интерпретатор языка Foo на Java. Затем с языком foo я написал собственный переводчик. Фу все еще требует права JRE?
Джордж Ксавье

Ответы:


231

Это называется "начальной загрузкой". Сначала вы должны построить компилятор (или интерпретатор) для вашего языка на каком-либо другом языке (обычно Java или C). Как только это будет сделано, вы можете написать новую версию компилятора на языке Foo. Вы используете первый компилятор начальной загрузки для компиляции компилятора, а затем используете этот компилированный компилятор для компиляции всего остального (включая будущие версии самого себя).

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

Примером этого будет Scala. Его первый компилятор был создан в Pizza, экспериментальном языке Мартином Одерским. Начиная с версии 2.0, компилятор был полностью переписан в Scala. С этого момента старый компилятор Pizza может быть полностью исключен из-за того, что новый компилятор Scala можно использовать для компиляции для будущих итераций.


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

2
@piertoni, как правило, проще перенастроить бэкэнд компилятора на новый микропроцессор.
Bstpierre

Например, используйте LLVM в качестве бэкэнда

76

Я вспоминаю, как слушал подкаст Radio Engineering Radio, в котором Дик Габриэль говорил о начальной загрузке оригинального интерпретатора LISP, написав черновую версию LISP на бумаге и вручную собрав ее в машинный код. С тех пор остальные функции LISP были написаны и интерпретированы с помощью LISP.


Все загружено с транзистора генезиса с большим количеством рук

47

Добавление любопытства к предыдущим ответам.

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

make bootstrap

Цель 'bootstrap' не только компилирует GCC, но и компилирует его несколько раз. Он использует программы, скомпилированные в первом раунде, чтобы компилировать себя во второй раз, а затем снова в третий раз. Затем он сравнивает эти второй и третий компиляторы, чтобы убедиться в их безупречном воспроизведении. Это также означает, что он был скомпилирован правильно.

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


12
«Вы должны скомпилировать действительно каждый двоичный файл целевой системы», и все же вы должны начать с двоичного файла gcc, который вы откуда-то получили, потому что исходный код не может компилироваться сам. Интересно, проследили ли вы происхождение каждого двоичного файла gcc, который использовался для перекомпиляции каждого последующего gcc, не могли бы вы вернуться к исходному компилятору C от K & R?
Робру

43

Когда вы пишете свой первый компилятор для C, вы пишете на другом языке. Теперь у вас есть компилятор для C, скажем, на ассемблере. В конце концов, вы придете к тому месту, где вам придется анализировать строки, особенно экранирующие последовательности. Вы напишите код для преобразования \nв символ с десятичным кодом 10 (и \rдо 13 и т. Д.).

После того, как этот компилятор будет готов, вы начнете переопределять его на C. Этот процесс называется « начальной загрузкой ».

Код разбора строки станет:

...
if (c == 92) { // backslash
    c = getc();
    if (c == 110) { // n
        return 10;
    } else if (c == 92) { // another backslash
        return 92;
    } else {
        ...
    }
}
...

Когда это компилируется, у вас есть двоичный файл, который понимает '\ n'. Это означает, что вы можете изменить исходный код:

...
if (c == '\\') {
    c = getc();
    if (c == 'n') {
        return '\n';
    } else if (c == '\\') {
        return '\\';
    } else {
        ...
    }
}
...

Так где же информация о том, что '\ n' - это код для 13? Это в двоичном коде! Это похоже на ДНК: компиляция исходного кода C с этим двоичным файлом унаследует эту информацию. Если компилятор сам компилируется, он передает эти знания своим потомкам. С этого момента нет никакого способа увидеть из одного источника, что будет делать компилятор.

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

void compileFunction(char * name, char * filename, char * code) {
    if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) {
        code = A;
    } else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) {
        code = B;
    }

    ... code to compile the function body from the string in "code" ...
}

Интересными частями являются A и B. A - это исходный код для compileFunctionвключения вируса, вероятно, каким-то образом зашифрованный, так что это не очевидно из поиска в полученном двоичном файле. Это гарантирует, что компиляция с самим компилятором сохранит код внедрения вируса.

B - то же самое для функции, которую мы хотим заменить нашим вирусом. Например, это может быть функция «login» в исходном файле «login.c», которая, вероятно, из ядра Linux. Мы могли бы заменить его версией, которая будет принимать пароль «joshua» для учетной записи root в дополнение к обычному паролю.

Если вы скомпилируете это и распространите как двоичный файл, вы не сможете найти вирус, посмотрев на источник.

Первоначальный источник идеи: https://web.archive.org/web/20070714062657/http://www.acm.org/classics/sep95/


1
Какой смысл второй половины в написании зараженных вирусом компиляторов? :)
mhvelplund

3
@mhvelplund Просто распространяй знания о том, как самозагрузка может тебя убить.
Аарон Дигулла

19

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

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

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

Малоизвестным фактом является то, что компилятор GNU C ++ имеет реализацию, которая использует только подмножество C. Причина в том, что обычно легко найти компилятор C для новой целевой машины, который позволит вам затем собрать из него полный компилятор GNU C ++. Теперь у вас есть возможность загрузить компилятор C ++ на целевой машине.


14

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

Из того, что я помню из «моно», вполне вероятно, что им понадобится добавить несколько вещей к размышлению, чтобы заставить его работать: команда моно продолжает указывать, что некоторые вещи просто невозможны Reflection.Emit; Конечно, команда MS может доказать, что они не правы.

Это имеет несколько реальных преимуществ: это довольно хороший юнит-тест, для начинающих! И у вас есть только один язык для беспокойства (то есть, возможно, эксперт по C # может не знать C ++; теперь вы можете исправить компилятор C #). Но мне интересно, нет ли здесь какой-то профессиональной гордости: они просто хотят, чтобы это был хостинг.

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


Обновление 1

Я только что посмотрел это видео Андерса в PDC, и (около часа спустя) он приводит несколько более веских причин - все о компиляторе как сервисе. Только для записи.


4

Вот дамп (на самом деле трудная тема для поиска):

Это также идея PyPy и Rubinius :

(Я думаю, что это также может относиться к Форту , но я ничего не знаю о Форт.)


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

1

GNAT, компилятор GNU Ada, требует полной компиляции Ada. Это может быть проблемой при переносе его на платформу, где нет готового двоичного файла GNAT.


1
Не понимаю почему? Не существует правила, по которому вы должны загружаться более одного раза (как для каждой новой платформы), вы также можете кросс-компилировать с текущей.
Марко ван де Воорт

1

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

Первый компилятор начальной загрузки обычно написан на C, C ++ или Assembly.


1

Компилятор C # проекта Mono долгое время находился в автономном режиме, что означает, что он был написан на самом C #.

Что я знаю, так это то, что компилятор был запущен как чистый C-код, но как только были реализованы «основные» функции ECMA, они начали переписывать компилятор в C #.

Я не знаю о преимуществах написания компилятора на одном и том же языке, но уверен, что он связан, по крайней мере, с возможностями, которые может предложить сам язык (например, C не поддерживает объектно-ориентированное программирование) ,

Вы можете найти больше информации здесь .


1

Я написал SLIC (Систему языков для реализации компиляторов) сам по себе. Затем рука скомпилировала его в сборку. SLIC имеет много общего, так как это был один компилятор из пяти подъязыков:

  • SYNTAX Parser Язык программирования PPL
  • Язык генерации кода PSEUDO для сканирования дерева на основе GENERATOR LISP 2
  • ISO in Sequence, код PSEUDO, язык оптимизации
  • PSEUDO Макро-подобный язык сборки ассемблерного кода.
  • MACHOP Сборка-машинная инструкция, определяющая язык.

SLIC был вдохновлен CWIC (Компилятор для написания и реализации компиляторов). В отличие от большинства пакетов разработки компиляторов, SLIC и CWIC предназначены для генерации кода со специализированными, специфичными для домена языками. SLIC расширяет генерацию кода CWIC, добавляя подязыки ISO, PSEUDO и MACHOP, отделяя специфику целевой машины от языка генератора обхода дерева.

LISP 2 деревья и списки

Система динамического управления памятью на основе языка генератора LISP 2 является ключевым компонентом. Списки выражаются на языке, заключенном в квадратные скобки, компоненты которого разделены запятыми, то есть списком из трех элементов [a, b, c].

Деревья:

     ADD
    /   \
  MPY     3
 /   \
5     x

представлены списками, первая запись которых является объектом узла:

[ADD,[MPY,5,x],3]

Деревья обычно отображаются с отдельным узлом, предшествующим ветвям:

ADD[MPY[5,x],3]

Разбор с использованием функций генератора на основе LISP 2

Функция генератора - это именованный набор (unparse) => action> пар ...

<NAME>(<unparse>)=><action>;
      (<unparse>)=><action>;
            ...
      (<unparse>)=><action>;

Разреженные выражения - это тесты, которые соответствуют шаблонам дерева и / или типам объектов, разбивая их на части и назначая эти части локальной переменной для обработки ее процедурным действием. Вроде как перегруженная функция, принимающая разные типы аргументов. За исключением () => ... тесты предпринимаются в кодированном порядке. Первым успешно разобрано выполнение соответствующего ему действия. Разреженные выражения - это разборки тестов. ADD [x, y] соответствует дереву ADD с двумя ветвями, присваивая его ветви локальным переменным x и y. Действие может быть простым выражением или блоком ограниченного кода .BEGIN ... .END. Я бы использовал блоки c style {...} сегодня. Соответствие дерева, [], разбор правил может вызывать генераторы, передающие возвращенные результаты в действие:

expr_gen(ADD[expr_gen(x),expr_gen(y)])=> x+y;

В частности, приведенное выше выражение expr_gen unparse соответствует дереву ADD с двумя ветвями. Внутри тестового шаблона будет вызываться генератор одного аргумента, помещенный в ветвь дерева с этой ветвью. Его список аргументов - это локальные переменные, которым назначены возвращаемые объекты Выше unparse указывает две ветки для разборки дерева ADD, рекурсивное нажатие каждой ветви на expr_gen. Левая ветвь возврата помещена в локальные переменные x. Аналогично, правая ветвь передается expr_gen с y возвращаемым объектом. Выше может быть частью числового выражения оценки. Существовали функции ярлыков, называемые векторами, которые были выше, вместо строки узлов вектор узлов мог использоваться с вектором соответствующих действий:

expr_gen(#node[expr_gen(x),expr_gen(y)])=> #action;

  node:   ADD, SUB, MPY, DIV;
  action: x+y, x-y, x*y, x/y;

        (NUMBER(x))=> x;
        (SYMBOL(x))=> val:(x);

Вышеупомянутый более полный вычислитель выражений, присваивающий возврат из левой ветви 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 с использованием векторной записи:

.MACHOP #opnm register,@indirect offset (index): // Instruction's parameters.
.MORG 36, O(18): $/36; // Align to 36 bit boundary print format: 18 bit octal $/36
O(9):  #opcd;          // Op code 9 bit octal print out
 (4):  register;       // 4 bit register field appended print
 (1):  indirect;       // 1 bit appended print
 (4):  index;          // 4 bit index register appended print
O(18): if (#opcd&&3==1) offset // immediate mode use value else
       else offset/36;         // memory address divide by 36
                               // to get word address.
// Vectored entry opcode table:
#opnm := MOVE, MOVEI, MOVEM, MOVES, MOVS, MOVSI, MOVSM, MOVSS,
         MOVN, MOVNI, MOVNM, MOVNS, MOVM, MOVMI, MOVMM, MOVMS,
         IMUL, IMULI, IMULM, IMULB, MUL,  MULI,  MULM,  MULB,
                           ...
         TDO,  TSO,   TDOE,  TSOE,  TDOA, TSOA,  TDON,  TSON;
// corresponding opcode value:
#opcd := 0O200, 0O201, 0O202, 0O203, 0O204, 0O205, 0O206, 0O207,
         0O210, 0O211, 0O212, 0O213, 0O214, 0O215, 0O216, 0O217,
         0O220, 0O221, 0O222, 0O223, 0O224, 0O225, 0O226, 0O227,
                           ...
         0O670, 0O671, 0O672, 0O673, 0O674, 0O675, 0O676, 0O677;

.MORG 36, O (18): $ / 36; выравнивает местоположение по 36-битной границе, печатая адрес $ / 36 слова из 18 бит в восьмеричном виде. 9-битный регистр opcd, 4-битный регистр, косвенный бит и 4-битный индексный регистр объединяются и печатаются, как будто одно 18-битное поле. 18-битный адрес / 36 или непосредственное значение выводится и печатается в восьмеричном виде. Пример MOVEI распечатать с r1 = 1 и r2 = 2:

400020 201082 000005            MOVEI r1,5(r2)

С опцией сборки компилятора вы получите сгенерированный код сборки в списке компиляции.

Свяжите это вместе

Компоновщик 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). Другая - это библиотека времени выполнения компиляторов.

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

Парсер языка программирования

Парсер написан с использованием формулы, написанной в виде простых уравнений.

<name> <formula type operator> <expression> ;

Элементом языка на самом низком уровне является символ. Токены формируются из подмножеств символов языка. Классы символов используются для именования и определения этих подмножеств символов. Определяющим классом символов является символ двоеточия (:). Символы, которые являются членами класса, закодированы в правой части определения. Печатные символы заключены в простые простые строки. Непечатаемые и специальные символы могут быть представлены их числовым порядковым номером. Члены класса разделены альтернативой | оператор. Формула класса заканчивается точкой с запятой. Классы символов могут включать ранее определенные классы:

/*  Character Class Formula                                    class_mask */
bin: '0'|'1';                                                // 0b00000010
oct: bin|'2'|'3'|'4'|'5'|'6'|'7';                            // 0b00000110
dgt: oct|'8'|'9';                                            // 0b00001110
hex: dgt|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f';    // 0b00011110
upr:  'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|
      'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z';   // 0b00100000
lwr:  'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|
      'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z';   // 0b01000000
alpha:  upr|lwr;                                             // 0b01100000
alphanum: alpha|dgt;                                         // 0b01101110

Skip_class 0b00000001 предопределен, но может быть слишком длинным, определяя skip_class.

Итак, класс символов - это список альтернатив, который может быть только символьной константой, порядковым номером символа или ранее определенным классом символов. Как я реализовал классы символов: формуле класса назначается битовая маска класса. (Показано в комментариях выше). Любая формула класса, имеющая какой-либо символьный литерал или порядковый номер, вызывает выделение бита класса. Маска создается путем направления маски (ей) классов включенного класса (классов) вместе с выделенным битом (если есть). Таблица классов создается из классов символов. Запись, индексированная по порядковому номеру символа, содержит биты, указывающие членство класса в классе. Классовое тестирование проводится в линию. Пример кода IA-86 с порядковым номером символа в eax иллюстрирует тестирование класса:

test    byte ptr [eax+_classmap],dgt

Вслед за:

jne      <success>

или

je       <failure>

Примеры кода инструкции IA-86 используются, потому что я думаю, что инструкции IA-86 более широко известны сегодня. Имя класса, вычисляемое по его маске класса, неразрушающим образом и с таблицей классов, индексированной по порядковым номерам символов (в eax). Ненулевой результат указывает на членство в классе. (EAX обнуляется, за исключением al (младшие 8 битов EAX), который содержит символ).

Токены были немного другими в этих старых компиляторах. Ключевые слова не были объяснены как токены. Они просто были сопоставлены строковыми константами в кавычках на языке синтаксического анализатора. Строки в кавычках обычно не сохраняются. Модификаторы могут быть использованы. A + поддерживает соответствие строки. (т. е. + '-' соответствует символу -, сохраняя символ в случае успеха) Операция, (т. е. 'E') вставляет строку в токен. Пробел обрабатывается формулой токена, пропускающей первые символы SKIP_CLASS, пока не будет найдено первое совпадение. Обратите внимание, что явное сопоставление символов skip_class остановит пропуск, позволяя токену начинаться с символа skip_class. В формуле строкового токена пропускаются первые символы skip_class, соответствующие одинарным кавычкам или двойным кавычкам. Интерес представляет сопоставление «символа в строке» в кавычках:

string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];

Первый вариант соответствует любому символу в кавычках. Правильная альтернатива соответствует строке в двойных кавычках, которая может включать символы в двойных кавычках, использующие два символа «вместе» для представления одного символа. Эта формула определяет строки, используемые в ее собственном определении. Внутренняя правая альтернатива '"' $ (-" "". .ANY | "" "" "", "" "") '"' соответствует строке в двойных кавычках. Мы можем использовать один символ «в кавычках» для соответствия символу «двойной кавычки». Однако в строке «двойные» в кавычках, если мы хотим использовать «символ», мы должны использовать два символа, чтобы получить один. Например, во внутренней левой альтернативе, совпадающей с любым символом, кроме кавычки:

-"""" .ANY

отрицательный заглядывание вперед - "" "" используется, если в случае успеха (не совпадает с "символ"), то соответствует .ANY символ (который не может быть "символом, потому что -" "" "исключил эту возможность). Правильная альтернатива берет на себя - "" "" сопоставление "символа и неудача были правильной альтернативой:

"""""",""""

пытается сопоставить два "символа, заменив их одним двойным" используя "," "" для вставки одного символа ". Обе внутренние альтернативы, не прошедшие закрывающий символ кавычки строки, сопоставляются, и MAKSTR [] вызывается для создания строкового объекта. последовательность, цикл при успешном выполнении, оператор используется при сопоставлении последовательности. Формула токена пропускает первые символы класса пропуска (пробел). Как только первое совпадение выполнено, пропуск skip_class отключен. Мы можем вызывать функции, запрограммированные на других языках, используя []. MAKSTR [], MAKBIN [], MAKOCT [], MAKHEX [], MAKFLOAT [] и MAKINT [] являются поставляемой библиотечной функцией, которая преобразует совпавшую строку токена в типизированный объект. Приведенная ниже формула числа иллюстрирует довольно сложное распознавание токена:

number .. "0B" bin $bin MAKBIN[]        // binary integer
         |"0O" oct $oct MAKOCT[]        // octal integer
         |("0H"|"0X") hex $hex MAKHEX[] // hexadecimal integer
// look for decimal number determining if integer or floating point.
         | ('+'|+'-'|--)                // only - matters
           dgt $dgt                     // integer part
           ( +'.' $dgt                  // fractional part?
              ((+'E'|'e','E')           // exponent  part
               ('+'|+'-'|--)            // Only negative matters
               dgt(dgt(dgt|--)|--)|--)  // 1 2 or 3 digit exponent
             MAKFLOAT[] )               // floating point
           MAKINT[];                    // decimal integer

Приведенная выше формула числового токена распознает целые числа и числа с плавающей запятой. Альтернативы всегда успешны. Числовые объекты могут быть использованы в расчетах. Объекты-токены помещаются в стек анализа при успешном выполнении формулы. Показатель степени в (+ 'E' | 'e', ​​'E') интересен. Мы хотим всегда иметь заглавную E для MAKEFLOAT []. Но мы разрешаем заменять его строчными буквами «е», используя «E».

Возможно, вы заметили соответствие класса символов и формулы токена. Формула синтаксического анализа продолжается, добавляя альтернативы обратного отслеживания и операторы построения дерева. Альтернативные операторы с возвратом и без возврата могут не смешиваться в пределах уровня выражения. У вас может не быть (a | b \ c) смешивания без возврата | с альтернативой возврата. (a \ b \ c), (a | b | c) и ((a | b) \ c) действительны. Альтернатива \ backtracking сохраняет состояние анализа перед попыткой его левой альтернативы, а при сбое восстанавливает состояние анализа перед попыткой правильной альтернативы. В последовательности альтернатив первая удачная альтернатива удовлетворяет группе. Дальнейшие альтернативы не предпринимаются. Факторинг и группировка обеспечивают непрерывный анализ. Альтернатива backtrack создает сохраненное состояние синтаксического анализа, прежде чем попытается использовать левую альтернативу. Возврат требуется, когда анализ может выполнить частичное совпадение, а затем потерпеть неудачу:

(a b | c d)\ e

В приведенном выше примере, если возвращается ошибка, предпринимается попытка альтернативного компакт-диска. Если тогда c возвращает ошибку, будет предпринята попытка возврата. Если a завершился успешно, а b потерпел неудачу, анализ будет возвращен и попытка e. Аналогично, неудачный c успешен и b терпит неудачу, анализ возвращается, и альтернатива принимается. Возврат не ограничен формулой. Если какая-либо формула синтаксического анализа делает частичное совпадение в любое время, а затем терпит неудачу, анализ сбрасывается на верхний обратный ход и берется его альтернатива. Ошибка компиляции может возникнуть, если код был выведен в том смысле, что был создан возврат. Возврат устанавливается перед началом компиляции. Возврат ошибки или возврат к ней - ошибка компилятора. Возвраты сложены. Мы можем использовать отрицательный - и положительный? операторы «заглянуть / посмотреть вперед» для тестирования без ускорения разбора. проверка строки - это шаг вперед, требующий сохранения и сброса входного состояния. Забегая вперед, можно было бы проанализировать выражение, которое до частичного сбоя выполнит частичное совпадение. Взгляд в будущее реализован с использованием возврата.

Язык парсера не является ни парсером LL, ни LR. Но язык программирования для написания рекурсивного приличного парсера, в котором вы программируете построение дерева:

:<node name> creates a node object and pushes it onto the node stack.
..           Token formula create token objects and push them onto 
             the parse stack.
!<number>    pops the top node object and top <number> of parstack 
             entries into a list representation of the tree. The 
             tree then pushed onto the parse stack.
+[ ... ]+    creates a list of the parse stack entries created 
             between them:
              '(' +[argument $(',' argument]+ ')'
             could parse an argument list. into a list.

Обычно используемый пример синтаксического анализа - это арифметическое выражение:

Exp = Term $(('+':ADD|'-':SUB) Term!2); 
Term = Factor $(('*':MPY|'/':DIV) Factor!2);
Factor = ( number
         | id  ( '(' +[Exp $(',' Exp)]+ ')' :FUN!2
               | --)
         | '(' Exp ')" )
         (^' Factor:XPO!2 |--);

Использование Exp и Term с помощью цикла создает левостороннее дерево. Фактор, использующий правильную рекурсию, создает правостороннее дерево:

d^(x+5)^3-a+b*c => ADD[SUB[EXP[EXP[d,ADD[x,5]],3],a],MPY[b,c]]

              ADD
             /   \
          SUB     MPY
         /   \   /   \
      EXP     a b     c
     /   \
    d     EXP     
         /   \
      ADD     3
     /   \
    x     5

Вот немного компилятора cc, обновленная версия SLIC с комментариями в стиле c. Типы функций (грамматика, токен, класс символов, генератор, PSEUDO или MACHOP определяются их начальным синтаксисом, следующим за их идентификатором. С этими анализаторами сверху вниз вы начинаете с программы, определяющей формулу:

program = $((declaration            // A program is a sequence of
                                    // declarations terminated by
            |.EOF .STOP)            // End Of File finish & stop compile
           \                        // Backtrack: .EOF failed or
                                    // declaration long-failed.
             (ERRORX["?Error?"]     // report unknown error
                                    // flagging furthest parse point.
              $(-';' (.ANY          // find a ';'. skiping .ANY
                     | .STOP))      // character: .ANY fails on end of file
                                    // so .STOP ends the compile.
                                    // (-';') failing breaks loop.
              ';'));                // Match ';' and continue

declaration =  "#" directive                // Compiler directive.
             | comment                      // skips comment text
             | global        DECLAR[*1]     // Global linkage
             |(id                           // functions starting with an id:
                ( formula    PARSER[*1]     // Parsing formula
                | sequencer  GENERATOR[*1]  // Code generator
                | optimizer  ISO[*1]        // Optimizer
                | pseudo_op  PRODUCTION[*1] // Pseudo instruction
                | emitor_op  MACHOP[*1]     // Machine instruction
                )        // All the above start with an identifier
              \ (ERRORX["Syntax error."]
                 garbol);                    // skip over error.

// Обратите внимание на то, как id отключается и позже объединяется при создании дерева.

formula =   ("==" syntax  :BCKTRAK   // backtrack grammar formula
            |'='  syntax  :SYNTAX    // grammar formula.
            |':'  chclass :CLASS     // character class define
            |".." token   :TOKEN     // token formula
              )';' !2                // Combine node name with id 
                                     // parsed in calling declaration 
                                     // formula and tree produced
                                     // by the called syntax, token
                                     // or character class formula.
                $(-(.NL |"/*") (.ANY|.STOP)); Comment ; to line separator?

chclass = +[ letter $('|' letter) ]+;// a simple list of character codes
                                     // except 
letter  = char | number | id;        // when including another class

syntax  = seq ('|' alt1|'\' alt2 |--);

alt1    = seq:ALT!2 ('|' alt1|--);  Non-backtrack alternative sequence.

alt2    = seq:BKTK!2 ('\' alt2|--); backtrack alternative sequence

seq     = +[oper $oper]+;

oper    = test | action | '(' syntax ')' | comment; 

test    = string | id ('[' (arg_list| ,NILL) ']':GENCALL!2|.EMPTY);

action  = ':' id:NODE!1
        | '!' number:MAKTREE!1
        | "+["  seq "]+" :MAKLST!1;

//     C style comments
comment  = "//" $(-.NL .ANY)
         | "/*" $(-"*/" .ANY) "*/";

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

Я думаю, что я ответил на вопрос. Написав большую часть преемников SLIC, язык cc сам по себе здесь. Для него пока нет компилятора. Но я могу вручную скомпилировать его в ассемблерный код, голые функции asm c или c ++.


0

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

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

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

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


-1

Может быть, вы можете написать BNF, описывающий BNF.


4
Вы действительно можете (это не так уж сложно), но его единственное практическое применение будет в генераторе парсера.
Даниэль Спивак

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

На самом деле вы не можете, так как BNF не может описать себя. Вам нужен вариант, например, используемый в yacc, где нетерминальные символы не заключены в кавычки.
Маркиз Лорн

1
Вы не можете использовать bnf для определения bnf, поскольку <> не может быть распознан. EBNF исправил это путем цитирования константных строковых токенов языка.
ГК
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.