Як написати дуже базовий компілятор


214

Удосконалені компілятори, як gccкомпілюють коди в машиночитані файли відповідно до мови, якою написано код (наприклад, C, C ++ тощо). Насправді вони інтерпретують значення кожного коду відповідно до бібліотеки та функцій відповідних мов. Виправте мене, якщо я помиляюся.

Я хочу краще зрозуміти компілятори, написавши дуже базовий компілятор (можливо, на C) для складання статичного файлу (наприклад, Hello World у текстовому файлі). Я спробував кілька навчальних посібників та книжок, але всі вони призначені для практичних випадків. Вони мають справу зі складанням динамічних кодів зі значеннями, пов'язаними з відповідною мовою.

Як я можу написати базовий компілятор для перетворення статичного тексту в машиночитаний файл?

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

Представляючи практичні підручники та ресурси, високо цінується :-)



Ви пробували lex / flex та yacc / bison?
mouviciel

15
@mouviciel: Це не гарний спосіб дізнатися про створення компілятора. Ці інструменти виконують значну кількість важкої роботи для вас, тому ви ніколи насправді не робите цього та дізнаєтесь, як це робиться.
Мейсон Уілер

11
@ Цікаво, що перший з ваших посилань дає 404, а другий тепер позначений як дублікат цього питання.
Руслан

Відповіді:


326

Вступ

Типовий компілятор виконує такі кроки:

  • Розбір: вихідний текст перетворюється на абстрактне дерево синтаксису (AST).
  • Дозвіл посилань на інші модулі (C відкладає цей крок до з'єднання).
  • Семантична перевірка: вилучення синтаксично правильних висловлювань, які не мають сенсу, наприклад, недоступний код або дублювання декларацій.
  • Еквівалентні перетворення та оптимізація на високому рівні: AST перетворюється на представлення більш ефективного обчислення з однаковою семантикою. Сюди входить, наприклад, ранній підрахунок загальних підвиражень та постійних виразів, усунення надмірних локальних призначень (див. Також SSA ) тощо.
  • Генерація коду: AST перетворюється на лінійний код низького рівня зі стрибками, розподілом регістрів тощо. Деякі виклики функцій можна накреслити на цьому етапі, деякі петлі розкручуються тощо.
  • Оптимізація видовища: код низького рівня сканується на прості локальні неефективність, які усуваються.

Більшість сучасних компіляторів (наприклад, gcc та clang) повторюють останні два кроки ще раз. Вони використовують проміжну низькорівневу, але незалежну від платформи мову для початкового генерування коду. Тоді ця мова перетворюється на специфічний для платформи код (x86, ARM тощо), роблячи приблизно те саме, що оптимізовано для платформи. Це включає, наприклад, використання векторних інструкцій, коли це можливо, впорядкування інструкцій для підвищення ефективності прогнозування галузей тощо.

Після цього об'єктний код готовий до посилання. Більшість компіляторів нативного коду знають, як викликати посилання для створення виконуваного файлу, але це не є етапом компіляції. У таких мовах, як Java та C #, посилання можуть бути абсолютно динамічними, що робиться VM під час завантаження.

Пригадайте основи

  • Зроби так, щоб він працював
  • Зробіть це красивим
  • Зробіть це ефективним

Ця класична послідовність стосується всієї розробки програмного забезпечення, але несе повторення.

Концентруйтесь на першому кроці послідовності. Створіть найпростішу річ, яка могла б спрацювати.

Читай книги!

Прочитайте Книгу Драконів Ахо та Улмана. Це класика і досі досить застосовна.

Сучасний дизайн компілятора також високо оцінений.

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

Переконайтеся, що вам зручно працювати з графіками, особливо з деревами. Ці речі - це те, що програми складаються на логічному рівні.

Добре визначте свою мову

Використовуйте будь-які позначення, які хочете, але переконайтесь, що ви маєте повний і послідовний опис вашої мови. Сюди входить і синтаксис, і семантика.

Давно пора писати фрагменти коду новою мовою як тестові приклади для майбутнього компілятора.

Використовуйте свою улюблену мову

Цілком нормально писати компілятор на Python чи Ruby або будь-якою іншою мовою. Використовуйте прості алгоритми, які ви добре розумієте. Перша версія не повинна бути швидкою, ефективною чи повною. Це потрібно лише бути правильним і легко змінювати.

Також добре писати різні етапи компілятора різними мовами, якщо це потрібно.

Підготуйтеся до написання безлічі тестів

Ваша мова повинна охоплюватися тестовими справами; ефективно це буде визначено ними. Ознайомтеся з бажаними рамками тестування. Пишіть тести з першого дня. Концентруйтеся на "позитивних" тестах, які приймають правильний код, на відміну від виявлення неправильного коду.

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

Створіть хороший аналізатор

Генераторів парсера багато . Вибирайте все, що завгодно. Ви також можете написати власний парсер з нуля, але воно того варте лише, якщо синтаксис вашої мови мертвий простий.

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

Вихід вашого парсера - абстрактне синтаксичне дерево.

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

Створіть смисловий валідатор

Швидше за все, ваша мова передбачає синтаксично правильні конструкції, які можуть не мати сенсу в певних контекстах. Приклад - це дублікат оголошення однієї змінної або передача параметра неправильного типу. Валідатор виявить такі помилки, дивлячись на дерево.

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

Знову ж таки, напишіть і запустіть багато тестових справ. Тривіальні випадки так само незамінні при вирішенні проблем, як розумні та складні.

Створити код

Використовуйте найпростіші методи, які ви знаєте. Часто нормально безпосередньо перекладати мовну конструкцію (як-от ifзаяву) на злегка параметризований шаблон коду, не на відміну від шаблону HTML.

Знову ж таки, ігноруйте ефективність та концентруйтесь на коректності.

Націліться на платформу, незалежну від низькорівневого управління

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

Ваші варіанти:

  • LLVM: дозволяє ефективно генерувати машинний код, як правило, для x86 та ARM.
  • CLR: цілі .NET, в основному на базі x86 / Windows; має хороший JIT.
  • JVM: цільовий Java-світ, досить багатоплатформенний, має хороший JIT.

Ігноруйте оптимізацію

Оптимізація важка. Майже завжди оптимізація передчасна. Створюйте неефективний, але правильний код. Реалізуйте всю мову, перш ніж спробувати оптимізувати отриманий код.

Звичайно, тривіальні оптимізації добре вводити. Але уникайте будь-яких хитрих, волохатих речей, перш ніж ваш компілятор буде стабільним.

І що?

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

Побачити "Hello world" від програми, яку створив ваш компілятор, варто докласти зусиль.


45
Це одна з найкращих відповідей, яку я бачив досі.
gahooa

11
Я думаю, ви пропустили частину питання ... ОП хотів написати дуже базовий компілятор. Я думаю, що ви виходите за рамки дуже базового.
marco-fiset

22
@ marco-fiset , навпаки, я вважаю, що це непересічна відповідь, яка говорить ОП, як зробити дуже базовий компілятор, вказуючи на пастки, щоб уникнути та визначити більш розвинені фази.
smci

6
Це одна з найкращих відповідей, яку я коли-небудь бачив у всьому всесвіті Stack Exchange. Кудо!
Андре Терра

3
Побачити "Привіт світ" програми, яку створив ваш компілятор, варто докласти зусиль. -
INDEED

27

Джек Креншоу " Давайте побудуємо компілятор" , хоча він не закінчений, - це легко читається вступ та навчальний посібник.

Конструкція компілятора Nicklaus Wirth - це дуже хороший підручник з основ простої побудови компілятора. Він зосереджується на рекурсивному спуску зверху вниз, який, припустимо, набагато простіше, ніж lex / yacc або flex / bison. Оригінальний компілятор PASCAL, написаний його групою, був зроблений таким чином.

Інші люди згадували різні книги про Дракона.


1
Одна з приємних речей про Паскаль - це те, що все потрібно визначити або оголосити перед його використанням. Тому його можна скласти за один прохід. Turbo Pascal 3.0 - один із таких прикладів, і тут є багато документації про внутрішні внутрішні ресурси .
tcrosley

1
PASCAL був розроблений спеціально з компіляцією в один прохід і з урахуванням зв'язку. У книзі-компіляторі Вірта згадуються багатопустові компілятори, і додає, що він знав компілятор PL / I, який займав 70 (так, сімдесят) пропусків.
Джон Р. Стром

Обов'язкова декларація перед використанням датується ALGOL. Комітет ALGOL Тоні Хоаре зав'язав вуха, коли він спробував запропонувати додати правила типу за замовчуванням, аналогічні тим, що було у FORTRAN. Вони вже знали про проблеми, які це може створити, а типографічні помилки в іменах та правила за замовчуванням створюють цікаві помилки.
Джон Р. Стром

1
Ось більш оновлена ​​та завершена версія книги самого автора - stack.nl/~marcov/compiler.pdf Будь ласка, відредагуйте свою відповідь та додайте це :)
сонник

16

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


7
Але тоді, коли ваш компілятор BF буде готовий, ви повинні написати в ньому свій код :(
500 - Внутрішня помилка сервера

@ 500-InternalServerError використовують метод підмножини C
World Engineer

12

Якщо ви дійсно хочете писати лише машиночитаний код і не орієнтований на віртуальну машину, тоді вам доведеться прочитати посібники від Intel і зрозуміти

  • а. Зв'язування та завантаження виконуваного коду

  • б. Формати COFF та PE (для Windows), а також зрозуміти формат ELF (для Linux)

  • c. Зрозуміти формати файлів .COM (простіше, ніж PE)
  • г. Зрозумійте збирачів
  • е. Зрозумійте компілятори та двигун генерації коду в компіляторах.

Набагато складніше, ніж сказано. Я пропоную вам прочитати укладачі та перекладачі на C ++ як вихідну точку (Автор Рональд Мак). Крім того, "дозволяє створити компілятор" Креншо добре.

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

Поради: ПОВЕРНЕНЬ вивчіть Flex та Bison. Потім перейдіть до створення власного компілятора / VM.

Щасти!


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

Я погоджуюсь, я вже десь слідкую за LLVM, і я повинен сказати, що це була одна з найкращих речей, які я бачив за роки з точки зору зусиль програміста, необхідних для її націлення!
Анікет Інге

2
Що з MIPS та використовувати spim для його запуску? Або MIX ?

@MichaelT Я не використовував MIPS, але впевнений, що це буде добре.
Анікет Інге

Набір інструкцій @PrototypeStark RISC, реальний процесор, що використовується і сьогодні (розуміючи, що це буде перекладено у вбудовані системи). Повний набір інструкцій знаходиться у wikipedia . Дивлячись в мережі, є багато прикладів, і вона використовується в багатьох академічних класах як мета для машинного програмування мови. Існує трохи активності на ньому в SO .

10

Підхід "Зробіть сам" для простого компілятора може виглядати так (принаймні, так виглядав мій проект uni):

  1. Визначте граматику мови. Без контексту.
  2. Якщо ваша граматика ще не LL (1), зробіть це зараз. Зауважте, що деякі правила, які в звичайній граматиці CF виглядали нормально, можуть вийти некрасивими. Можливо, ваша мова занадто складна ...
  3. Напишіть Lexer, який розрізає потік тексту в лексеми (слова, цифри, букви).
  4. Напишіть для своєї граматики рекурсивний синтаксичний аналізатор зниження, який приймає або відхиляє введення.
  5. Додайте генерацію синтаксичного дерева у свій парсер.
  6. Запишіть генератор машинного коду з дерева синтаксису.
  7. Прибуток і пиво, або ви можете почати думати, як зробити розумніший аналізатор або генерувати кращий код.

Повинно бути багато літератури, яка детально описує кожен крок.


7-й пункт - це те, про що питають ОП.
Флоріан Маргаїн

7
1-5 не мають значення і не заслуговують такої пильної уваги. 6 - найцікавіша частина. На жаль, більшість книг слідують за тією ж схемою, що після сумнозвісної книги-дракона, приділяючи занадто багато уваги розбору та залишаючи код перетворюється поза сферою.
SK-логіка
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.