Skip to content

Commit

Permalink
crash course
Browse files Browse the repository at this point in the history
  • Loading branch information
5HT committed Apr 6, 2024
1 parent 34372a4 commit 45f86aa
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 13 deletions.
93 changes: 93 additions & 0 deletions MinCaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -659,12 +659,105 @@ AppCls (а не AppDir), щоб викликати функцію через ї
Генерація байткоду і компіляція бінарного коду
----------------------------------------------

Після перетворення вкладених функцій ми згенеруємо SPARC асемблер. Однак,
оскільки згенерувати асемблер SPARC надто складно без підготовки, ми спочатку
згенеруємо код віртуальної машини, подібний до асемблеру SPARC. Його основні «віртуальні» аспекти:

Нескінченна кількість змінних (замість кінцевої кількості регістрів)
Вирази if-then-else і виклики функцій замість розгалужень і переходів
Цей віртуальний проміжний асемблер визначено у sparcAsm.ml. SparcAsm.exp
відповідає кожній інструкції SPARC (крім if). Послідовність інструкцій
SparcAsm.t є або Ans, яка повертає значення в кінці функції, або визначенням змінної Let.
Інші інструкції «Forget», «Save» та «Restore» будуть пояснені пізніше.

Virtual.f, Virtual.h і Virtual.g — це три функції, які перетворюють програми,
перетворені за допомогою конверсії вкладених функцій, у код віртуальної машини.
Virtual.f перекладає всю програму (список функцій верхнього рівня та вираз
основної процедури), Virtual.h перекладає кожну функцію верхнього рівня,
а Virtual.g перекладає вираз. Сенс цих перекладів полягає в тому, щоб
зробити явним доступ до пам’яті для створення, читання та запису в закриття,
кортежі та масиви. Такі структури даних, як замикання, кортежі та масиви,
розміщуються в області пам’яті, що називається купою, адреса якої запам’ятовується
в спеціальному регістрі SparcAsm.reg_hp.

Наприклад, у випадку читання масиву Closure.Get зміщення зсувається відповідно
до розміру елемента, який потрібно завантажити. У створенні кортежу Closure.Tuple
кожен елемент зберігається з вирівняними числами з плаваючою комою (8 байт),
а початкова адреса використовується як значення кортежу. Створення закриття
Closure.MakeCls зберігає адресу (мітку) тіла функції зі значеннями її вільних
змінних, також піклуючись про вирівнювання, і використовує початкову адресу
як значення закриття. Відповідно, на початку кожної функції верхнього рівня
ми завантажуємо значення вільних змінних із закриття. У цьому процесі ми
припускаємо, що кожна програма на основі закриття функції (AppCls) встановлює
адресу закриття для реєстрації SparcAsm.reg_cl.

До речі, оскільки збірка SPARC не підтримує миттєві числа з плаваючою комою,
нам потрібно створити постійну таблицю в пам’яті. Для цього Virtual.g записує
константи з плаваючою комою в глобальну змінну Virtual.data, які включені
Virtual.f у всю програму SparcAsm.Prog.

Оптимізація безпосередніх операндів
-----------------------------------

В асемблері SPARC більшість цілочисельних операцій можуть приймати ціле
число в межах 13 бітів (не менше ніж -4096 і менше ніж 4096) як другий операнд.
Оптимізація за допомогою цієї функції реалізована в Simm13.g і Simm13.g'. Вони
подібні до постійного згортання та видалення непотрібних визначень, за винятком
того, що цільовою мовою є віртуальна збірка SPARC, а константи обмежені 13-бітними цілими числами.

Алокація регістрів
------------------

Результат перекладу
[Оновлення від 17 вересня 2008 р.: розподільник реєстрів тепер використовує простіший алгоритм. Він пропускає відстеження (ToSpill і NoSpill), пояснене нижче.]

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

По-перше, як умова виклику функцій, ми призначаємо аргументи від першого регістра до останнього. (Компілятор MinCaml не підтримує занадто багато аргументів, які не вміщуються в регістри. Їх мають обробляти програмісти, наприклад, за допомогою кортежів.) Ми встановлюємо значення, що повертаються, до першого регістру. Вони обробляються в RegAlloc.h, який виділяє регістри в кожній функції верхнього рівня.

Після цього ми розподіляємо резисти в тілах функцій і основній програмі. RegAlloc.g приймає послідовність інструкцій із відображенням regenv (від змінних до регістрів), що представляє поточне призначення регістру, і повертає послідовність інструкцій після виділення регістру. Основна політика розподілу реєстрів полягає в тому, щоб «уникати регістрів, яким призначаються живі змінні (що ще будуть використані).» SparcAsm.fv обчислює «змінні, які ще потрібно використовувати». Однак, при розподілі регістрів в e1 Let(x, e1, e2), не тільки e1, але також і «продовжувана» послідовність інструкцій e2 повинна бути врахована для обчислення «змінних, які будуть використані». З цієї причини RegAlloc.g і RegAlloc.g', які розподіляють регістри у виразах, також беруть послідовність інструкцій, що все ще триває, cont і використовують її для обчислення живих змінних.

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

Таким чином, коли змінну x потрібно зберегти, RegAlloc.g повертає значення типу даних ToSpill, що представляє цю потребу, і таким чином повертається до визначення x, щоб вставити віртуальну інструкцію Save. Крім того, оскільки ми хочемо видалити x із набору «живих змінних» у точці, де x розливається, ми вставляємо віртуальну інструкцію Forget to exclude x із набору вільних змінних. З цією метою ToSpill зберігає не лише (список) розповсюджених змінних, але й послідовність інструкцій e, у яку було вставлено Forget. Таким чином, після збереження x ми повторюємо виділення регістру для e.

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

Якщо збереження непотрібне, ми повертаємо виділену регістром послідовність інструкцій e' (з новим regenv) у значенні іншого типу даних NoSpill.

Рано чи пізно буде використано розлиту змінну, і в цьому випадку RegAlloc.g' (функція, яка виділяє регістри у виразах) викликає виключення, оскільки не може знайти змінну в regenv. Цей виняток обробляється функцією RegAlloc.g'_and_restore, де вставляється віртуальна інструкція Restore, яка відновлює значення змінної з пам'яті в регістр.

При розподілі регістрів ми не тільки «уникаємо живих регістрів», але й намагаємося «зменшити непотрібне переміщення в майбутньому». Це називається націлюванням на регістр або об’єднанням регістрів. Наприклад, якщо визначена змінна буде другим аргументом виклику функції, ми намагаємося розмістити її у другому регістрі. Для іншого прикладу ми намагаємося виділити змінну, яка буде повернена як результат функції в першому регістрі. Вони реалізовані в RegAlloc.target. Для цього RegAlloc.g і RegAlloc.g' також беруть регістр dest, де буде встановлено результат обчислення.

Генерація асемблерного коду
---------------------------

Нарешті ми підійшли до фінального етапу: генерація асемблера для gcc. Оскільки ми вже
завершили найскладнішу частину (а саме алокація регістрів), ми можемо
просто вивести SparcAsm.t як справжній асемблер SPARC без особливих труднощів.

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

Єдиним нетривіальним, важливим процесом у цьому модулі є оптимізація
хвостового виклику. Він реалізує виклики функцій (кінцеві виклики),
які після цього нічого не мають і не повертаються, за допомогою простих
інструкцій переходу замість викликів. Завдяки цій оптимізації рекурсивні
функції, такі як gcd, компілюються в той самий код, що й цикли. Для цього
функція Emit.g, яка генерує збірку для послідовностей інструкцій, а також
функція Emit.g', яка генерує збірку для кожної інструкції, приймає
значення типу даних Emit.dest, яке представляє, чи ми знаходимося
в хвістовій позиції. Якщо dest дорівнює Tail, ми виконуємо кінцевий
виклик іншої функції за допомогою інструкції переходу або встановлюємо
результат обчислення в перший регістр і повертаємо за допомогою
інструкції ret SPARC. Якщо dest є NonTail(x), ми встановлюємо результат обчислення на x.

Нарешті, ми реалізуємо основну процедуру stub.c, яка виділяє пам'ять в купі
та стеку викликів функцій, реалізовуємо зовнішні функції libmincaml.S,
необхідні для тестових програм, а потім отримуємо компілятор MinCaml, який працює!

23 changes: 10 additions & 13 deletions src/vm/insts.ml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

type inst =
| UNIT (* terminator *)
| ADD | SUB | MUL | DIV | MOD | NOT | NEG (* binary and ALU ops *)
Expand All @@ -13,19 +14,6 @@ type inst =
| Lref of string
| Ldef of string [@@deriving show]

let instsmap =
[| UNIT
; ADD ; SUB ; MUL ; DIV ; MOD ; NOT ; NEG
; LT ; GT ; EQ
; JUMP_IF_ZERO ; JUMP ; CALL ; RET ; HALT
; DUP ; DUP0 ; POP0 | POP1
; CONST0 ; CONST
; GET ; PUT ; ARRAY_MAKE
; FRAME_RESET (* o l n *) ; JIT_SETUP
; RAND_INT ; READ_INT ; READ_STRING
; PRINT_INT ; PRINT_NEWLINE ; PRINT_STRING
|]

let index_of instr = match instr with
| UNIT -> 0 | ADD -> 1 | SUB -> 2 | MUL -> 3
| DIV -> 4 | MOD -> 5 | NOT -> 6 | NEG -> 7
Expand All @@ -38,6 +26,15 @@ let index_of instr = match instr with
| PRINT_INT -> 30 | PRINT_NEWLINE -> 31 | PRINT_STRING -> 32
| _ -> 33

let instsmap =
[| UNIT ; ADD ; SUB ; MUL ; DIV ; MOD ; NOT ; NEG
; LT ; GT ; EQ ; JUMP_IF_ZERO ; JUMP ; CALL ; RET ; HALT
; DUP ; DUP0 ; POP0 | POP1 ; CONST0 ; CONST
; GET ; PUT ; ARRAY_MAKE ; FRAME_RESET ; JIT_SETUP
; RAND_INT ; READ_INT ; READ_STRING
; PRINT_INT ; PRINT_NEWLINE ; PRINT_STRING
|]

module Printer = struct
let pp_inst_map () =
ignore
Expand Down

0 comments on commit 45f86aa

Please sign in to comment.