LLVM: компілятор своїми руками. Введення

Уявімо собі, що в один прекрасний день вам прийшла в голову ідея процесора власного, ні на що не схожої архітектури, і вам дуже захотілося цю ідею реалізувати «в залізі». На щастя, в цьому немає нічого неможливого. Трохи верилога, і ось ваша ідея реалізована. Вам вже сняться чудові сни про те, як Intel розорилася, Microsoft спішно переписує Windows під вашу архітектуру, а Linux-співтовариство вже написало під ваш мікропроцесор свіжу версію системи з досить ненудними шпалерами.
Однак, для всього цього не вистачає однієї дрібниці: компілятора!
Так, я знаю, що багато хто не вважають наявність компілятора чимось важливим, вважаючи, що всі повинні програмувати на асемблері. Якщо ви теж так вважаєте, я не буду з вами сперечатися, просто не читайте далі.
Якщо ви хочете, щоб для вашої оригінальної архітектури був доступний хоча б мову З, прошу під кат.
У статті розглядається застосування інфраструктури компіляторів LLVM для побудови власних рішень на її основі.
Область застосування LLVM не обмежується розробкою компіляторів для нових процесорів, інфраструктура компіляторів LLVM також може застосовуватися для розробки нових компіляторів мов програмування, нових алгоритмів оптимізації та специфічних інструментів статичного аналізу коду (пошук помилок, збір статистики тощо).
Наприклад, ви можете використовувати якийсь стандартний процесор (наприклад, ARM) в поєднанні з спеціалізованим співпроцесора (наприклад, матричний FPU), в цьому випадку вам може знадобитися модифікувати існуючий компілятор для ARM так, щоб він міг генерувати код для вашого FPU.
Також цікавим застосуванням LLVM може бути генерація вихідних текстів на мові високого рівня («переклад» з однієї мови на іншу). Наприклад, можна написати генератор коду на Verilog з вихідного коду на С.



КДПВ


Чому LLVM?
На сьогоднішній день існує лише два реалістичних шляху розробки компілятора для власної архітектури: використання GCC або використання LLVM. Інші проекти компіляторів з відкритим вихідним кодом або не досягли того ступеня розвитку, як GCC і LLVM, або застаріли і перестали розвиватися, вони не володіють розвиненими алгоритмами оптимізації, і можуть не забезпечувати повної сумісності навіть зі стандартом мови, не кажучи вже про підтримку інших мов програмування. Розробка власного компілятора “з нуля", це дуже нераціональний шлях, т. к. існуючі опенсорсные рішення вже реалізують фронтенд компілятора з безліччю дуже нетривіальних алгоритмів оптимізації, які, до того ж, добре протестовані і використовуються вже тривалий час.
Який з цих двох open-source проектів вибрати в якості основи для свого компілятора? GCC (GNU Compiler Collection) є більш старим проектом, перший реліз якого відбувся в 1987 році, його автором є Річард Столлман, відомий діяч open-source руху [4]. Він підтримує безліч мов програмування: C, C++, Objective C, Fortran, Java, Ada, Go. Також існують фронтенды для багатьох інших мов програмування, які не включені в основну збірку. Компілятор GCC підтримує велику кількість процесорних архітектур і операційних систем, і є в даний час найбільш поширеним компілятором. Сам GCC написаний на мові С.
LLVM значно «молодше», його перший реліз відбувся в 2003 році, він (а точніше, його фронтенд Clang) підтримує мови програмування C, C++, Objective-C and Objective-C++, і також має фронтенды для мов Common Lisp, ActionScript, Ada, D, Fortran, OpenGL Shading Language, Go, Haskell, Java bytecode, Julia, Swift, Python, Ruby, Rust, Scala, C# і Lua. Він розроблений в університеті Іллінойсу, США, і є основним компілятором для розробки під операційну систему OS X. LLVM написаний на мові С++ (С++11 для останніх релізів) [5].

Відносна «молодість» LLVM не є недоліком, він достатньо зрілий, щоб у ньому не було критичних багів, і при цьому він не несе в собі величезного вантажу застарілих архітектурних рішень, як GCC. Модульна структура компілятор дозволяє використовувати фронтенд LLVM-GCC, який забезпечує повну підтримку стандартів GCC, при цьому генерація коду цільової платформи буде здійснюватися LLC (бекенд LLVM). Також можна використовувати Clang — оригінальний фронтенд LLVM.
LLVM добре документований, і для нього велику кількість прикладів коду.

Модульна архітектура компілятора
Інфраструктура компіляторів LLVM складається з різних інструментів, розглядати їх усі в рамках даної статті не має сенсу. Обмежимося основними інструментами, які утворюють як такий компілятор.
Компілятор LLVM, як і деякі інші компілятори, складається з фронтенда, оптимізатора (миддленда), і бекенду. Така структура дозволяє розділити розробку компілятора нової мови програмування, розроблення методів оптимізації та розробку кодогенератора для конкретного процесора (такі компілятори називають «перенацеливаемыми», retargetable).
Сполучною ланкою між ними є проміжний мова LLVM, асемблер «віртуальної машини». Фронтенд (наприклад, Clang) перетворює текст програми на мові високого рівня в текст на проміжному мовою, оптимізатор (opt) виробляє над ним різні оптимізації, а бекенд (llc) генерує код цільового процесора (на асемблері або безпосередньо у вигляді бінарного файлу). Крім цього, LLVM може працювати в режимі JIT (just-in-time) компіляції, коли компіляція відбувається безпосередньо під час виконання програми.
Проміжне представлення програми може бути як у вигляді текстового файлу на мові асемблера LLVM, так і у вигляді двійкового формату, «биткода». За замовчуванням clang генерує саме биткод (файл .bc), але для налагодження і вивчення LLVM нам потрібно буде генерувати текстове проміжне представлення на асемблері LLVM (він також називається IR-кодом, від слів Intermediate Representation, проміжне представлення).


Рис. 1. Модульна архітектура компілятора

Крім перерахованих вище програм, LLVM включає в себе і інші утиліти [6].
Отже, напишемо найпростішу програму на C.
int foo(int x, int y) {
return x + y;
}

І скомпилируем її:
clang-3.5 -c add.c -O0 --target=xcore -emit-llvm -S -o add_o0.ll
 

Деякі пояснення:
-c додати.c — вхідний файл;
-O0 — рівень оптимізації 0, оптимізація відсутня;
--target=xcore — ядро процесора xcore не має яких-небудь складних особливостей при компіляції IR-код, тому є ідеальним об'єктом для досліджень. Це ядро має розрядність 32, і clang вирівнює всі змінні по межах 32-бітних слів;
-emit-llvm -S — вказівка clang-у згенерувати файл llvm в текстовому вигляді (на асемблері LLVM);
-o add_o0.ll — вихідний файл
Подивимося на результат:
; ModuleID = 'add.c'
 
target datalayout = "e-m:e-p:32:32-i1:8:32-i8:8:32-i16:16:32-i64:32-f64:32-a:0:32-n32"
 
target triple = "xcore"
 

 
; Function Attrs: nounwind
 
define i32 @foo(i32 %x, i32 %y) #0 {
 
%1 = alloca i32, align 4
 
%2 = alloca i32, align 4
 
store i32 %x, i32* %1, align 4
 
store i32 %y, i32* %2, align 4
 
%3 = load i32* %1, align 4
 
%4 = load i32* %2, align 4
 
%5 = add nsw i32 %3, %4
 
ret i32 %5
 
}
 

 
attributes #0 = { nounwind "less-precise-fpmad"="false" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "unsafe-fp-math"="false" "use-soft-float"="false" }
 

 
!llvm.ident = !{!0}
 
!xcore.typestrings = !{!1}
 

 
!0 = metadata !{metadata !"Ubuntu clang version 3.5.0-4ubuntu2~trusty2 (tags/RELEASE_350/final) (based on LLVM 3.5.0)"}
 
!1 = metadata !{i32 (i32, i32)* @foo, metadata !"f{si}(si,si)"}
 

Виглядає досить складно, чи не так? Однак давайте розберемося, що тут написано. Отже:
target datalayout = «e-m:e-p:32:32-i1:8:32-i8:8:32-i16:16:32-i64:32-f64:32-a:0:32-n32»
опис розрядності змінних і самих основних особливостей архітектури. e — little-endian архітектура. Для big-endian тут була б буква E. m:e — mangling, угоду про перетворення імен. Нам поки що це не знадобиться. p:32:32 — покажчики мають 32 біта і вирівняні по межах 32-бітових слів. i1:8:32 — булеві змінні (i1) виражаються 8-ибитными значеннями і вирівняні по межах 32-бітових слів. Далі аналогічно для цілочисельних змінних i8 — i64 (від 8 до 64 біт відповідно), і f64 — для дійсних змінних подвійної точності. a:0:32 — агрегатні змінні (тобто масиви і структури) мають вирівнювання 32 біта, n32 — розрядність чисел, що обробляються АЛП процесора (native integer width).
Далі йде назва таргету (тобто цільового процесора): target triple = «xcore». Хоча код IR часто називають «машинно-незалежних», насправді ми бачимо, це не так. Він відображає деякі особливості цільової архітектури. Це є однією з причин, по якій ми повинні вказувати цільову архітектуру не тільки для бекенду, але і для фронтенда.
Далі слід код функції foo:
define i32 @foo(i32 %x, i32 %y) #0 {
 
%1 = alloca i32, align 4
 
%2 = alloca i32, align 4
 
store i32 %x, i32* %1, align 4
 
store i32 %y, i32* %2, align 4
 
%3 = load i32* %1, align 4
 
%4 = load i32* %2, align 4
 
%5 = add nsw i32 %3, %4
 
ret i32 %5
 
}
 

Код досить громіздкий, хоча вихідна функція дуже проста. Ось що він робить.
Відразу відзначимо, що всі імена змінних в LLVM мають префікс або % (для локальних змінних), або @ — для глобальних. У даному прикладі всі локальні змінні.
%1 = alloca i32, align 4 — виділяє на стеку 4 байти для змінної, покажчиком на цю область є вказівник %1.
store i32 %x, i32* %1, align 4 — копіює у виділену область один з аргументів функції (%x).
%3 = load i32* %1, align 4 — витягує значення в змінну %3. Тепер у %3 зберігається локальна копія %x.
Робить те ж саме для аргументу %y
%5 = add nsw i32 %3, %4 — складає локальні копії %x %y, поміщає результат у %5. Є ще атрибут nsw, але він поки що для нас не важливий.
Повертає значення %5.
З наведеного прикладу видно, що при нульовому рівні оптимізації clang генерує код, буквально слідуючи вихідного коду, створює локальні копії всіх аргументів і не робить жодних спроб видалити надлишкові команди. Це може здатися поганим властивістю компілятора, але насправді це дуже корисна особливість при налагодженні програм і при налагодженні коду самого компілятора.
Подивимося, що станеться, якщо поміняти рівень оптимізації на O1:
define i32 @foo(i32 %x, i32 %y) #0 {
 
%1 = add nsw i32 %y, %x
 
ret i32 %1
 
}
 

Ми бачимо, що жодної зайвої команди не залишилося. Тепер програма складає безпосередньо аргументи функції та повертає результат.
Є і більш високі рівні оптимізації, але в цьому конкретному випадку вони не дадуть кращого результату, т. к. максимальний рівень оптимізації вже досягнутий.
Інша частина LLVM-коду (атрибути, метадані), несе в собі службову інформацію, яка поки нам нецікава.

Структура коду LLVM
Структура коду LLVM дуже проста. Код програми складається з модулів, компілятор обробляє по одному модулю. В модулі є глобальні оголошення (змінні, константи і оголошення заголовків функцій, що знаходяться в інших модулях) і функції. Функції мають аргументи і повертаються типи. Функції складаються з базових блоків. Базовий блок — це послідовність команд асемблера LLVM, що має одну точку входу і одну точку виходу. Базовий блок не містить усередині себе ніяких розгалужень і циклів, він виконується строго послідовно від початку до кінця і повинен закінчуватися терминирующей командою (з поверненням функції або переходом на інший блок).
І, нарешті, базовий блок складається з команд асемблера. Список команд наведено в документації на LLVM [7].
Отже, ще раз: базовий блок має одну точку доступу, позначену міткою, і обов'язково повинен закінчуватися командою безумовного переходу br або командою повернення ret. Перед ними може бути команда умовного переходу, в цьому випадку вона повинна бути безпосередньо перед терминирующей командою. Базовий блок має список predecessors — базових блоків, з яких керування може приходити на нього, і successors — базових блоків, яким він може передавати управління. На основі цієї інформації будується CFG — Flow Control Graph, граф потоку управління, найважливіша структура, що представляє програму в компіляторі.
Розглянемо тестовий приклад на мові С:
Нехай вихідний код на мові С має цикл:
//функція обчислює суму 10 елементів масиву
int for_loop(int x[]) {
int sum = 0;
for(int i = 0; i < 10; i++) {
sum += x[i];
}
return sum;
}

Його CFG має вигляд:

Ще одним видом графів у LLVM є DAG — directed acyclic graph, спрямований ациклічний граф, який представляє собою базовий блок.
Він являє команди асемблера і залежності між ними. На малюнку нижче наведений DAG базового блоку, що представляє тіло циклу в прикладі вище, при рівні оптимізації -O1:


SSA-форма
Ключовою особливістю IR-коду, що відрізняє його від мов високого рівня є те, що він представлений у так званій SSA-формі (Static Single Assignment form). Ця особливість дуже важлива для розуміння при розробці компілятора на платформі LLVM, тому приділимо їй деяку увагу. Якщо формулювати коротко, то в SSA-формі кожній змінній присвоюється значення рівно один раз і тільки в одній точці програми. Всі алгоритми оптимізації та перетворення IR-коду працюють тільки з такою формою.
Однак, як перетворити звичайну програму на мові високого рівня в таку форму? Адже у звичайних мовах програмування значення змінної може присвоюватися кілька разів у різних місцях програми, або, наприклад, у циклі.
Для реалізації такої поведінки програми використовується один з двох прийомів. Перший прийом полягає у використанні пар команд load/store, як у наведеному вище коді. Обмеження єдиного присвоювання поширюється тільки на іменовані змінні LLVM, і не поширюється на комірки пам'яті, на які посилаються покажчики. Тобто, можна необмежену кількість разів виробляти запис у комірку пам'яті командою store, і формальне правило SSA буде дотримано, так як покажчик на клітинку не змінюється. Цей спосіб використовується зазвичай при рівні оптимізації -O0, і ми не будемо на ньому детально зупинятися. Другий прийом використовує φ-функції, ще одну цікаву концепцію IR-коду.

Код в SSA-формі: load/store
Скористаємося тестовим прикладом з попереднього розділу.
Скомпилируем з рівнем оптимізації -O0:
define i32 @for_loop(i32* %x) #0 {
 
%1 = alloca i32*, align 4
 
%sum = alloca i32, align 4
 
%i = alloca i32, align 4
 
store i32* %x, i32** %1, align 4
 
store i32 0, i32* %sum, align 4
 
store i32 0, i32* %i, align 4
 
br label %2
 

 
; :2 ; preds = %12, %0
 
%3 = load i32* %i, align 4
 
%4 = icmp slt i32 %3, 10
 
br i1 %4, label %5, label %15
 

 
; :5 ; preds = %2
 
%6 = load i32* %i, align 4
 
%7 = load i32** %1, align 4
 
%8 = getelementptr inbounds i32* %7, i32 %6
 
%9 = load i32* %8, align 4
 
%10 = load i32* %sum, align 4
 
%11 = add nsw i32 %10, %9
 
store i32 %11, i32* %sum, align 4
 
br label %12
 

 
; :12 ; preds = %5
 
%13 = load i32* %i, align 4
 
%14 = add nsw i32 %13, 1
 
store i32 %14, i32* %i, align 4
 
br label %2
 

 
; :15 ; preds = %2
 
%16 = load i32* %sum, align 4
 
ret i32 %16
 
}
 

Тут ми бачимо те, про що було написано вище: змінна циклу, це просто комірка пам'яті, на яку посилається вказівник %i.

Код в SSA-формі: φ-функції
Тепер спробуємо рівень оптимізації O1:
define i32 @for_loop(i32* nocapture readonly %x) #0 {
 
br label %1
 

 
; :1 ; preds = %1, %0
 
%i.02 = phi i32 [ 0, %0 ], [ %5, %1 ]
 
%sum.01 = phi i32 [ 0, %0 ], [ %4, %1 ]
 
%2 = getelementptr inbounds i32* %x, i32 %i.02
 
%3 = load i32* %2, align 4, !tbaa !2
 
%4 = add nsw i32 %3, %sum.01
 
%5 = add nsw i32 %i.02, 1
 
%exitcond = icmp eq i32 %5, 10
 
br i1 %exitcond, label %6, label %1
 

 
; :6 ; preds = %1
 
ret i32 %4
 
}
 

Тут ми бачимо, що змінної циклу є %i.02 (імена змінних в LLVM можуть містити точки), і це не дороговказ, а звичайна 32-бітна мінлива, а присвоювання значення відбувається за допомогою функції phi i32 [ 0, %0 ], [ %5, %1 ]. Це означає, що функція повертає значення 0, якщо перехід відбувся з базового блоку %0 (перший базовий блок у функції), і значення змінної %5, якщо перехід відбувся з базового блоку %1 (тобто з вихідної точки цього ж базового блоку). Таким чином, генератор IR-коду позбувся двох присвоювань змінній, суворо дотримуючись формальних правил SSA. Далі видно, що подібним чином відбувається присвоювання змінної %sum.01.
Отже, сенс φ-функції полягає в тому, що її значення залежить від того, з якого базового блоку був проведений перехід на неї. φ-функції можуть перебувати тільки на початку базового блоку. Якщо їх кілька, вони повинні слідувати безперервно, починаючи з першої інструкції базового блоку.

Moar optimization!
Що буде при рівні оптимізації -O2?
define i32 @for_loop(i32* nocapture readonly %x) #0 {
 
%1 = load i32* %x, align 4, !tbaa !2
 
%2 = getelementptr inbounds i32* %x, i32 1
 
%3 = load i32* %2, align 4, !tbaa !2
 
%4 = add nsw i32 %3, %1
 
%5 = getelementptr inbounds i32* %x, i32 2
 
%6 = load i32* %5, align 4, !tbaa !2
 
%7 = add nsw i32 %6, %4
 
%8 = getelementptr inbounds i32* %x, i32 3
 
%9 = load i32* %8, align 4, !tbaa !2
 
%10 = add nsw i32 %9, %7
 
%11 = getelementptr inbounds i32* %x, i32 4
 
%12 = load i32* %11, align 4, !tbaa !2
 
%13 = add nsw i32 %12, %10
 
%14 = getelementptr inbounds i32* %x, i32 5
 
%15 = load i32* %14, align 4, !tbaa !2
 
%16 = add nsw i32 %15, %13
 
%17 = getelementptr inbounds i32* %x, i32 6
 
%18 = load i32* %17, align 4, !tbaa !2
 
%19 = add nsw i32 %18, %16
 
%20 = getelementptr inbounds i32* %x, i32 7
 
%21 = load i32* %20, align 4, !tbaa !2
 
%22 = add nsw i32 %21, %19
 
%23 = getelementptr inbounds i32* %x, i32 8
 
%24 = load i32* %23, align 4, !tbaa !2
 
%25 = add nsw i32 %24, %22
 
%26 = getelementptr inbounds i32* %x, i32 9
 
%27 = load i32* %26, align 4, !tbaa !2
 
%28 = add nsw i32 %27, %25
 
ret i32 %28
 
}
 

Оптимізатор розгорнув цикл. Взагалі, оптимізатор IR-коду в LLVM дуже інтелектуальний, він вміє не тільки розгортати цикли, але й спрощувати нетривіальні конструкції, обчислювати константные значення, навіть якщо вони не присутні в коді в явному вигляді, і робити інші складні перетворення коду.

Компонування IR-коду
Реальні програми складаються не з одного модуля. Традиційний компілятор компілює модулі окремо, перетворюючи їх в об'єктні файли, а потім передає їх компоновщику (линкеру), який об'єднує їх в один виконуваний файл. LLVM теж дозволяє так робити.
Але LLVM має також можливість компонування IR-коду. Найпростіше розглянути це на прикладі. Нехай є два вихідних модуля: foo.c і bar.c:
//bar.h
#ifndef BAR_H
#define BAR_H
int bar(int x, int k);
#endif

//bar.c
int bar(int x, int k) {
return x * x * k;
}

//foo.c
#include "bar.h"
int foo(int x, int y) {
return bar(x, 2) + bar(y, 3);
}

Якщо програма буде скомпільована «традиційним», оптимізатор не зможе зробити з неї практично нічого: при компіляції foo.c компілятор не знає, що знаходиться всередині функції bar, і може надійти єдиним очевидним способом, вставити виклики bar().
Але якщо ми скомпонуємо IR-код, то ми отримаємо один модуль, який після оптимізації з рівнем -O2 буде виглядати так (для ясності заголовок модуля і метадані опущені):
define i32 @foo(i32 %x, i32 %y) #0 {
 
%1 = shl i32 %x, 1
 
%2 = mul i32 %1, %x
 
%3 = mul i32 %y, 3
 
%4 = mul i32 %3, %y
 
%5 = add nsw i32 %4, %2
 
ret i32 %5
 
}
 

 
; Function Attrs: nounwind readnone
 
define i32 @bar(i32 %x, i32 %k) #0 {
 
%1 = mul nsw i32 %x %x
 
%2 = mul nsw i32 %1, %k
 
ret i32 %2
 
}
 

Тут видно, що у функції foo не відбувається ніяких викликів, компілятор переніс у неї вміст bar() повністю, попутно підставивши константные значення k. Хоча функція bar() залишилася в модулі, вона буде виключена при компіляції виконуваного файлу, за умови, що вона не викликається ніде більше в програмі.
Потрібно відзначити, що в GCC також є можливість перегляду та оптимізації проміжного коду (LTO, link-time optimization) [6].
Зрозуміло, оптимізація LLVM не вичерпується оптимізацією IR-коду. Всередині бекенду також відбуваються різні оптимізації на різних стадіях перетворення IR-коду в машинне подання. Частина таких оптимізацій LLVM виробляє самостійно, але розробник бекенду може (і повинен) розробити власні алгоритми оптимізації, які дозволять в повній мірі використовувати особливості архітектури процесора.

Генерація коду цільової платформи
Розробка компілятора для оригінальної архітектури процесора, це, в основному, розробка бекенду. Втручання в алгоритми фронтенда, як правило, не є необхідним, або, у всякому разі, вимагає досить вагомих підстав. Якщо проаналізувати вихідний код Clang, можна побачити, що велика частина «особливих» алгоритмів припадає на процесори x86 і PowerPC з їх нестандартними форматами дійсних чисел. Для більшості інших процесорів потрібно вказати лише розміри базових типів і endianness (big-endian або little-endian). Найчастіше можна просто знайти аналогічний (у плані розрядності) процесор серед безлічі підтримуваних.
Генерація коду для цільової платформи відбувається в бэкенде LLVM, LLC. LLC підтримує безліч різних процесорів, і ви можете на його основі зробити генератор коду для вашого власного оригінального процесора. Ця задача спрощується ще й завдяки тому, що весь вихідний код, включаючи модулі для кожної підтримуваної архітектури, повністю відкритий і доступний для вивчення.
Саме генератор коду для цільової платформи (target) є найбільш трудомістким завданням при розробці компілятора на основі інфрастуктури LLVM. Я вирішив не зупинятися тут докладно на особливостях реалізації бекенду, так як вони істотним чином залежать від архітектури цільового процесора. Втім, якщо у шановної аудиторії хабра виникне інтерес до даної теми, я готовий описати ключові моменти розробки бекенду в наступній статті.

Висновок
В рамках невеликої статті неможливо розглянути докладно ні архітектуру LLVM, ні синтаксис мови LLVM IR, ні процес розробки бекенду. Однак ці питання докладно висвітлюються в документації. Автор скоріше намагався дати загальне уявлення про інфраструктуру компіляторів LLVM, зробивши упор на те, що ця платформа є сучасною, потужною, універсальною, і незалежною ні від вхідної мови, ні від цільової архітектури процесора, дозволяючи реалізувати і те, і інше за бажанням розробника.
Крім розглянутих, LLVM містить і інші утиліти, однак їх розгляд виходить за рамки статті.
LLVM дозволяє реалізувати бекенд для будь-якої архітектури (див. примітку), включаючи архітектури з конвеєризацією, з позачерговим виконанням команд, з різними варіантами паралелізації, VLIW, для класичних і стекових архітектур, загалом, для будь-яких варіантів.
Незалежно від того, наскільки нестандартні рішення лежать в основі процесорної архітектури, це всього лише питання написання більшого чи меншого обсягу коду.
приміткадля будь-якої, в межах розумного. Навряд чи можливо реалізувати компілятор мови для 4-хбитной архітектури, т. к. стандарт мови явно вимагає, щоб розрядність була не менше 8.


Література

Компілятори
[1] Книга дракона
[2] Вірт Н. Побудова компіляторів
GCC
[3] gcc.gnu.org — сайт проекту GCC
[4] Richard M. Stallman and the GCC Developer Community. GNU Compiler Collection Internals
LLVM
[5] http://llvm.org/ — сайт проекту LLVM
[6] http://llvm.org/docs/GettingStarted.html Getting Started with the LLVM System
[7] http://llvm.org/docs/LangRef.html LLVM Language Reference Manual
[8] http://llvm.org/docs/WritingAnLLVMBackend.html Writing An LLVM Backend
[9] http://llvm.org/docs/WritingAnLLVMPass.html Writing An LLVM Pass
[10] Chen Chung-Shu. Creating an LLVM Backend for the Cpu0 Architecture
[11] Mayur Pandey, Suyog Sarda. LLVM Cookbook
[12] Bruno Cardoso Lopes. Getting Started with LLVM Core Libraries
[13] Suyog Sarda, Mayur Pandey. LLVM Essentials
Автор буде радий відповісти на ваші запитання в коментарях і в приват.
Прохання про всі помічені опечатки повідомляти в лічку. Заздалегідь спасибі.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.