Написання компілятора своєю мовою


204

Інтуїтивно, здається, що компілятор для мови Fooсам не може бути записаний у Foo. Більш конкретно, перший компілятор для мови Fooне може бути записаний у Foo, але будь-який наступний компілятор може бути записаний для Foo.

Але чи це насправді правда? У мене є дуже нечіткий спогад про читання про мову, перший упорядник написаний "сам". Чи можливо це, і якщо так, то як?



Це дуже старе питання, але скажіть, що я написав перекладача для мови Foo на Java. Тоді, мовою, я написав, що це власний перекладач. Foo все ще вимагатиме JRE, правда?
Джордж Ксав'є

Відповіді:


231

Це називається "завантаження". Спочатку потрібно створити компілятор (або інтерпретатор) для вашої мови якоюсь іншою мовою (як правило, Java або C). Як тільки це буде зроблено, ви можете написати нову версію компілятора мовою Foo. Ви використовуєте перший компілятор завантаження для компіляції компілятора, а потім використовуєте цей компільований компілятор для компіляції всього іншого (включаючи майбутні версії самого себе).

Більшість мов справді створені таким чином, частково тому, що мовні дизайнери люблять використовувати мову, яку вони створюють, а також тому, що нетривіальний компілятор часто служить корисним орієнтиром того, наскільки "повною" може бути мова.

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


Можливо, дурне питання: Якщо ви хочете перенести ваш компілятор до іншої архітектури мікропроцесора, завантажувальна установка повинна перезапуститись з робочого компілятора для цієї архітектури. Чи це правильно? Якщо це правильно, це означає, що краще зберегти перший компілятор, оскільки це може бути корисно для порту вашого компілятора до інших архітектур (особливо, якщо він написаний якоюсь універсальною мовою, як C)?
піртоні

2
@piertoni, як правило, було б простіше просто повернути перехідний компілятор до нового мікропроцесора.
bstpierre

Використовуйте LLVM як бекенд, наприклад

76

Пригадую, слухав подкаст Software Engineering Radio, в якому Дік Габріель говорив про завантаження оригінального інтерпретатора LISP, написавши версію з голими кістками у LISP на папері та вручну зібравши його в машинний код. Відтоді решта особливостей LISP були записані та інтерпретовані LISP.


Все завантажено з транзистора генезису з великою кількістю рук

47

Додавання цікавості до попередніх відповідей.

Ось цитата з посібника Linux від Scratch , на етапі, коли починається створення компілятора GCC з його джерела. (Linux From Scratch - це спосіб встановити Linux, який кардинально відрізняється від встановлення дистрибутива, оскільки вам доведеться компілювати справді кожен двійковий код цільової системи.)

make bootstrap

Ціль «завантажувального завантаження» не просто компілює GCC, а компілює її кілька разів. Він використовує програми, складені в першому раунді, щоб скласти себе вдруге, а потім знову втретє. Потім він порівнює ці другий і третій збірки, щоб переконатися, що він може відтворити себе бездоганно. Це також означає, що вона була складена правильно.

Таке використання цілі "bootstrap" мотивоване тим, що компілятор, який використовується для побудови ланцюжка інструментів цільової системи, може не мати тієї самої версії цільового компілятора. Таким чином, у цільовій системі потрібно отримати компілятор, який може скласти сам.


12
"вам потрібно скласти справді кожен окремий бінарний файл цільової системи", і все ж ви повинні почати з бінарного файлу gcc, який ви отримали звідкись, тому що джерело не може зібрати себе. Цікаво, якби ви відстежили лінійку кожного бінарного файлу gcc, який був використаний для перекомпіляції кожного наступного gcc, чи повернетесь ви до оригінального компілятора C & R?
robru

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? Це в бінарному! Це як ДНК: Компіляція вихідного коду С з цим двійковим файлом успадкує цю інформацію. Якщо компілятор складе сам, він передасть ці знання своїм нащадкам. З цього моменту неможливо побачити лише з джерела, що буде робити компілятор.

Якщо ви хочете сховати вірус у джерелі якоїсь програми, ви можете це зробити так: Отримайте джерело компілятора, знайдіть функцію, яка компілює функції, і замініть його на цей:

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.c", який, ймовірно, з ядра Linux. Ми могли б замінити його версією, яка прийме пароль "joshua" для кореневого облікового запису на додаток до звичайного пароля.

Якщо ви компілюєте це і поширюєте його як двійкове, не знайдеться способу знайти вірус, поглянувши на джерело.

Оригінальне джерело ідеї: 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 :

(Я думаю, це може стосуватися і Forth , але я нічого не знаю про Forth.)


Перше посилання на статтю, що нібито стосується Smalltalk, наразі вказує на сторінку без видимої корисної та негайної інформації.
nbro

1

GNAT, компілятор GNU Ada, вимагає, щоб компілятор Ada був повністю побудований. Це може бути болем при перенесенні його на платформу, де немає доступного бінарного файлу GNAT.


1
Я не бачу чому? Не існує правила, з яким ви повинні завантажуватись неодноразово (як і для кожної нової платформи), також ви можете схрещувати компіляцію з поточною.
Марко ван де Ворт

1

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

Перший компілятор завантаження зазвичай записується на C, C ++ або зборці.


1

Компілятор проекту C # Mono вже давно "влаштовує хостинг". Це означає, що він написаний у C # сам.

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

Мені невідомі переваги написання компілятора на тій же мові, але я впевнений, що це стосується принаймні тих функцій, які може запропонувати сама мова (наприклад, C не підтримує об'єктно-орієнтоване програмування) .

Ви можете знайти більше інформації тут .


1

Я написав SLIC (Система мов впровадження компіляторів) у себе. Потім рука склала його на збірку. У SLIC є багато, оскільки це був єдиний компілятор з п'яти підмов:

  • Мова програмування SYNTAX Parser PPL
  • Генерація мови на основі PSEUDO-коду на основі дерев, що повзають по деревах
  • ISO в послідовності, код 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) => дія> пар ...

<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;

Зокрема, вищевказаний unprse expr_gen відповідає двому гілці ADD. У межах тестового шаблону з цим гілкою буде викликатися генератор одного аргументу, розміщений у гілці дерева. Список аргументів, хоча це локальні змінні, присвоєні поверненим об'єктам. Вище розпарювання вказує дві гілки - розбирання дерева 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, а правої гілки - у. Відповідний вектор дії, виконаний на x і y, повернувся. Останній непарний => пар дій відповідає числовим і символьним об'єктам.

Символ та ознаки символу

Символи, можливо, назвали атрибути. val: (x) доступ до атрибуту val об'єкта символів, що міститься у x. Узагальнений стек таблиць символів є частиною SLIC. Таблиця SYMBOL може бути натиснута та висунута, надаючи локальні символи для функцій. Новостворений символ каталогізується у верхній таблиці символів. Пошук символів шукає стек таблиці символів з верхньої таблиці спочатку назад вниз по стеку.

Створення незалежного машинного коду

Мова генератора SLIC виробляє об'єкти інструкцій PSEUDO, додаючи їх до списку кодів розділів. A .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-бітний опдд, 4-бітний регістр, непрямий біт та 4-розрядний індексний регістр поєднуються та друкуються так, ніби єдине 18-бітове поле. 18-бітова адреса / 36 або негайне значення виводиться і друкується у вісімці. Приклад MOVEI роздруковується з r1 = 1 і r2 = 2:

400020 201082 000005            MOVEI r1,5(r2)

За допомогою параметра збирання компілятора ви отримуєте згенерований код складання у списку компіляції.

Зв’яжіть це разом

Лінкер SLIC постачається у вигляді бібліотеки, яка обробляє роздільну здатність зв'язку та символів. Форматування файлу вихідного завантаження для цільового, хоча має бути записане для цільових машин і пов'язане з бібліотекою бібліотек лінкерів.

Мова генератора здатна записувати дерева у файл та читати їх, що дозволяє реалізовувати компілятор багатопрохідного режиму.

Короткі літні генерації коду та їх походження

Я спершу пережив генерацію коду, щоб переконатися, що зрозумів, що SLIC був справжнім компілятором. SLIC був натхненний CWIC (Компілятор для написання та реалізації компіляторів), розроблений корпорацією System Development в кінці 1960-х. У CWIC були лише мови SYNTAX і GENERATOR, що створюють числовий байт-код з мови GENERATOR. Байт-код був розміщений або посаджений (термін, що використовується в документації на CWIC) в буфери пам'яті, пов'язані з названими розділами і виписаний оператором .FLUSH. Папір ACM на CWIC доступний в архівах ACM.

Успішно реалізується основна мова програмування

Наприкінці 1970-х SLIC був використаний для написання перехресного компілятора COBOL. Виконується приблизно за 3 місяці здебільшого одним програмістом. Я трохи працював з програмістом у міру необхідності. Інший програміст написав бібліотеку виконання та MACHOPs для цільового міні-комп'ютера TI-990. Цей компілятор 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.

Підсумок: Клас символів - це список альтернативних варіантів, який може бути лише символьною константою, порядковим символом або раніше визначеним класом символів. Як я реалізував класи символів: Формулі класу присвоюється бітова маска класу. (Показано в коментарях вище) Будь-яка формула класу, яка має будь-який буквальний чи порядковий символ, призводить до виділення біта класу. Маска робиться шляхом упорядкування масок (класів) включених класів (класів) разом з виділеним бітом (якщо такий є). З класів символів створюється таблиця класів. Запис, індексований порядковим порядком символу, містить біти, що вказують на членство персонажа в класі. Тестування класу проводиться в режимі inline. Приклад коду IA-86 з порядковим символом символу в eax ілюструє тестування класу:

test    byte ptr [eax+_classmap],dgt

Після цього:

jne      <success>

або

je       <failure>

Приклади кодів інструкцій IA-86 використовуються, тому що я думаю, що інструкції IA-86 сьогодні широко відомі. Ім'я класу, що оцінює його маску класу, не руйнує AND з таблицею класів, індексованою порядковими символами (в eax). Ненульовий результат вказує на приналежність до класу. (EAX нульовий, за винятком al (низьких 8 біт EAX), що містить символ).

Токени були дещо іншими в цих старих компіляторах. Ключові слова не були пояснені як лексеми. Вони просто узгоджувались цитованими рядковими константами мовою розбору. Котировані рядки зазвичай не зберігаються. Можливо використовувати модифікатори. A + підтримує відповідність рядку. (тобто + '-' відповідає а - символу, що зберігає символ при успішному виконанні). Операція (тобто "E") вставляє рядок у маркер. Білий пробіл обробляється формулою лексеми, пропускаючи провідні символи SKIP_CLASS, поки не буде зроблено перший збіг. Зауважте, що явна відповідність символу skip_class зупинить пропуск, що дозволить маркеру починатись із символу skip_class. Формула лексеми рядка пропускає провідні символи skip_class, що відповідають одному запитуваному символу цитати або подвійному котируваному рядку. Цікавим є відповідність "символу в" цитованому рядку:

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

Перша альтернатива відповідає будь-якій символіці, що цитується окремо. Правильна альтернатива відповідає рядку з цитатами подвійної лапки, яка може включати символи подвійної цитати, використовуючи два "символи разом, щоб представити один" символ. Ця формула визначає рядки, використані у власному визначенні. Внутрішня права альтернатива '"' $ (-" "" ".ANY |" "" "" "" "" ")" "" відповідає подвійному котируваному рядку. Ми можемо використовувати один "котируваний символ", щоб відповідати подвійній цитаті "символ. Однак у двомісному" цитувальному рядку, якщо ми хочемо використовувати "символ, ми повинні використовувати два" символи, щоб отримати один. Наприклад, у внутрішній лівій альтернативній відповідності будь-якому символу, крім цитати:

-"""" .ANY

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

"""""",""""

намагається співставити два "символи, замінюючи їх одним подвійним" символом "," "", щоб вставити thw єдиний "символ. Обидва внутрішні альтернативи, у разі відмови символу цитати, що завершується, збігаються, і 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" замінювати його на "E".

Можливо, ви помітили послідовність класів символів та формули лексеми. Формула розбору продовжує додавання альтернатив зворотного відтворення та операторів побудови дерев. Альтернативні оператори зворотного відстеження та неприйняття не можуть змішуватися в межах рівня вираження. Можливо, у вас (a | b \ c) не змішується неповторний трек | з альтернативою зворотного відстеження. (a \ b \ c), (a | b | c) та ((a | b) \ c) є дійсними. Альтернатива зворотного відстеження зберігає стан розбору перед спробою лівої альтернативи, а після відмови відновлює стан розбору перед спробою правої альтернативи. У послідовності альтернатив перша успішна альтернатива задовольняє групу. Подальші альтернативи не намагаються. Факторинг та групування передбачають безперервний просунутий синтаксичний аналіз. Альтернатива зворотного треку створює збережений стан розбору, перш ніж спробувати його ліву альтернативу. Зворотний трек потрібен, коли синтаксичний аналіз може отримати часткове збіг, а потім не вдасться:

(a b | c d)\ e

У вищесказаному, якщо повертається збій, робиться спробу альтернативного CD. Якщо потім c повертає помилку, буде здійснена спроба альтернативи зворотного треку. Якщо це вдасться і не вдасться, синтаксис розбору буде відкликаний і здійснено спробу. Аналогічно, невдалий c успішний і b не виконаний синтаксичний аналіз аналізується, і застосовується альтернатива e. Зворотний трек не обмежується лише формулою. Якщо будь-яка формула синтаксичного аналізу в будь-який час робить часткову відповідність, а потім не відповідає, синтаксичний розбір повертається до верхнього зворотного треку та приймається його альтернатива. Помилка компіляції може статися, якщо код виведений у сенсі, що створено зворотній трек. Перед запуском компіляції встановлюється зворотний трек. Повернення відмови або зворотного відстеження до нього - це збій компілятора. Зворотні композиції складаються з накопиченням. Ми можемо використовувати негативні - і позитивні? зазирнути / дивитись вперед операторів для тестування без просування синтаксичного аналізу. тест рядка - це заглянути вперед, потребує збереження та скидання стану введення. Погляд вперед був би синтаксичним синтаксисом, який дає часткове збіг, перш ніж провалитися. Погляд вперед реалізується за допомогою зворотного відстеження.

Мова аналізатора не є ні 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.

// Зауважте, як ідентифікатор факту вимикається та пізніше поєднується під час створення дерева.

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, тут мова самої куб. Для цього ще немає компілятора. Але я можу вручну компілювати його в код складання, голий asm c або c ++ функції.


0

Так, ви можете написати компілятор для мови цією мовою. Ні, вам не потрібен перший компілятор для цієї мови для завантаження.

Що потрібно для завантаження - це реалізація мови. Це може бути або компілятор, або перекладач.

Історично мови зазвичай вважалися або інтерпретованими мовами, або складеними мовами. Перекладачі писалися лише для перших, а упорядники - лише для другого. Тому зазвичай, якщо компілятор збирався писати для мови, перший компілятор буде написаний якоюсь іншою мовою для завантаження, тоді, необов'язково, компілятор буде переписаний для мови теми. Але написати перекладача на іншій мові, натомість, це варіант.

Це не просто теоретично. В даний час я, мабуть, роблю це сам. Я працюю над компілятором для мови, лосось, який я розробив сам. Я спершу створив компілятор лосося на 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.