Sebagian besar kompiler memiliki arsitektur berikut:

Pada artikel ini, saya akan membedah arsitektur ini secara rinci, elemen demi elemen.
Kita dapat mengatakan bahwa artikel ini adalah tambahan untuk sumber daya yang ada dalam jumlah besar pada topik kompiler. Ini adalah sumber otonom yang akan memungkinkan Anda untuk memahami dasar-dasar desain dan implementasi bahasa pemrograman.
Target pembaca artikel ini adalah orang-orang yang idenya tentang pekerjaan penyusun sangat terbatas (maksimal adalah bahwa mereka terlibat dalam penyusunan). Namun, saya berharap pembaca memahami struktur data dan algoritma.
Artikel ini sama sekali tidak ditujukan untuk kompiler produksi modern dengan jutaan baris kode - tidak, ini adalah kursus singkat "kompiler untuk boneka" yang membantu untuk memahami apa itu kompiler.
Pendahuluan
Saat ini saya sedang mengerjakan bahasa sistem
Krug yang terinspirasi oleh Rust and Go. Dalam artikel itu saya akan merujuk ke Krug sebagai contoh untuk menggambarkan pikiran saya. Krug sedang dalam pengembangan, tetapi sudah tersedia di
https://github.com/krug-lang di
gudang dan
krug repositori. Bahasa ini tidak terlalu khas dibandingkan dengan arsitektur kompiler yang biasa, yang sebagian mengilhami saya untuk menulis artikel - tetapi lebih pada nanti.
Saya segera memberi tahu Anda bahwa saya sama sekali bukan spesialis dalam penyusun! Saya tidak memiliki gelar doktor, dan saya tidak menjalani pelatihan formal - saya mempelajari segala sesuatu yang dijelaskan dalam artikel saya sendiri di waktu luang saya. Saya juga harus mengatakan bahwa saya tidak menggambarkan pendekatan yang sebenarnya, satu-satunya yang benar untuk membuat kompiler, tetapi saya menyajikan metode dasar yang cocok untuk membuat kompiler "mainan" kecil.
Frontend
Mari kita kembali ke diagram di atas: panah di sebelah kiri diarahkan ke bidang frontend adalah bahasa yang terkenal dan dicintai seperti C. Frontend terlihat seperti ini: analisis leksikal -> pengurai.
Analisis leksikal
Ketika saya mulai mempelajari kompiler dan desain bahasa, saya diberitahu bahwa analisis leksikal sama dengan tokenization. Kami akan menggunakan deskripsi ini. Penganalisa mengambil input data dalam bentuk string atau aliran karakter dan mengenali pola di dalamnya, yang memotong token.
Dalam hal kompiler, ia menerima program tertulis pada input. Itu dibaca menjadi string dari file, dan alat analisa tokenizes kode sumbernya.
enum TokenType { Identifier, Number, }; struct Token { std::string Lexeme; TokenType type; // ... // It's also handy to store things in here // like the position of the token (start to end row:col) };
Dalam fragmen ini, ditulis dalam bahasa berbentuk-C, Anda dapat melihat struktur yang mengandung leksem tersebut, serta TokenType, yang berfungsi untuk mengenali token ini.
Catatan: artikel ini bukan instruksi untuk membuat bahasa dengan contoh - tetapi untuk pemahaman yang lebih baik, saya akan memasukkan potongan kode dari waktu ke waktu.
Analisis biasanya merupakan komponen kompiler paling sederhana. Seluruh frontend, sebenarnya, cukup sederhana dibandingkan dengan potongan puzzle lainnya. Meskipun itu sangat tergantung pada pekerjaan Anda.
Ambil bagian kode C berikut:
int main() { printf("Hello world!\n"); return 0; }
Setelah membacanya dari file ke baris dan melakukan pemindaian linier, Anda mungkin dapat mengiris token. Kami mengidentifikasi token dengan cara alami - melihat int adalah "kata", dan 0 dalam pernyataan pengembalian adalah "angka". Alat analisis leksikal melakukan prosedur yang sama seperti yang kita lakukan - nanti kita akan memeriksa proses ini secara lebih rinci. Misalnya, analisis angka:
0xdeadbeef β HexNumber ( ) 1231234234 β WholeNumber ( ) 3.1412 β FloatingNumber ( ) 55.5555 β FloatingNumber ( ) 0b0001 β BinaryNumber ( )
Mendefinisikan kata bisa sulit. Sebagian besar bahasa mendefinisikan kata sebagai urutan huruf dan angka, dan pengidentifikasi biasanya dimulai dengan huruf atau garis bawah. Sebagai contoh:
123foobar := 3 person-age := 5 fmt.Println(123foobar)
Di Go, kode ini tidak akan dianggap benar dan akan diuraikan ke dalam token berikut:
Number(123), Identifier(foobar), Symbol(:=), Number(3) ...
Sebagian besar pengidentifikasi yang dijumpai terlihat seperti ini:
foo_bar __uint8_t fooBar123
Analisator harus menyelesaikan masalah lain, misalnya, dengan spasi, komentar multiline dan single-line, pengidentifikasi, angka, sistem angka dan pemformatan angka (misalnya, 1_000_000) dan pengkodean (misalnya, dukungan untuk UTF8 alih-alih ASCII).
Dan jika Anda berpikir Anda dapat menggunakan ekspresi reguler - lebih baik tidak sepadan. Jauh lebih mudah untuk menulis analisa dari awal, tetapi saya sangat merekomendasikan membaca
artikel ini dari raja dan dewa kita Rob Pike. Alasan Regex tidak cocok untuk kami dijelaskan dalam banyak artikel lain, jadi saya akan menghilangkan poin ini. Selain itu, menulis analisa jauh lebih menarik daripada menyiksa diri sendiri dengan ekspresi panjang yang diunggah ke regex101.com pada jam 5:24 pagi. Dalam bahasa pertama saya, saya menggunakan fungsi
split(str)
untuk tokenization - dan saya tidak pergi jauh.
Parsing
Parsing agak lebih rumit daripada analisis leksikal. Ada banyak parser dan parser-generator - di sini permainan dimulai dengan cara yang besar.
Parser dalam kompiler biasanya mengambil input dalam bentuk token dan membangun pohon tertentu - pohon sintaksis abstrak atau pohon parsing. Secara alami, mereka mirip, tetapi memiliki beberapa perbedaan.
Tahapan-tahapan ini dapat direpresentasikan sebagai fungsi:
fn lex(string input) []Token {...} fn parse(tokens []Token) AST {...} let input = "int main() { return 0; }"; let tokens = lex(input); let parse_tree = parse(tokens); // ....
Biasanya, kompiler dibangun dari banyak komponen kecil yang mengambil input, mengubahnya, atau mengubahnya menjadi output yang berbeda. Ini adalah salah satu alasan mengapa bahasa fungsional sangat cocok untuk membuat kompiler. Alasan lainnya adalah pembandingan yang sangat baik dan perpustakaan standar yang cukup luas. Fakta menyenangkan: implementasi pertama dari kompiler
Rust ada di Ocaml.
Saya menyarankan Anda untuk menjaga komponen ini sesederhana dan otonom mungkin - modularitas akan sangat memudahkan proses. Menurut pendapat saya, hal yang sama dapat dikatakan tentang banyak aspek lain dari pengembangan perangkat lunak.
Pohon
Pohon parsing
Apa ini? Juga dikenal sebagai parsing tree, pohon tebal ini berfungsi untuk memvisualisasikan program sumber. Mereka berisi semua informasi (atau sebagian besar) tentang program input, biasanya sama dengan yang dijelaskan dalam tata bahasa bahasa Anda. Setiap simpul pohon akan mengikuti atau tidak mengikuti, misalnya, NumberConstant atau StringConstant.
Pohon sintaksis abstrak
Seperti namanya, ASD adalah pohon sintaksis
abstrak . Pohon parsing berisi banyak (sering berlebihan) informasi tentang program Anda, dan dalam kasus ASD, itu tidak diperlukan. ASD tidak memerlukan informasi yang tidak berguna tentang struktur dan tata bahasa, yang tidak mempengaruhi semantik program.
Misalkan pohon Anda memiliki ekspresi seperti ((5 + 5) -3) +2. Di pohon parsing, Anda akan menyimpannya sepenuhnya, bersama dengan tanda kurung, operator, dan nilai 5, 5, 3, dan 2. Tetapi Anda dapat dengan mudah mengaitkan dengan ASD - kita hanya perlu mengetahui nilai, operator, dan urutannya.
Gambar di bawah ini menunjukkan pohon untuk ekspresi a + b / c.
ASD dapat direpresentasikan sebagai berikut:
interface Expression { ... }; struct UnaryExpression { Expression value; char op; }; struct BinaryExpression { Expression lhand, rhand; string op; // string because the op could be more than 1 char. }; interface Node { ... }; // or for something like a variable struct Variable : Node { Token identifier; Expression value; };
Pandangan ini sangat terbatas, tetapi saya harap Anda dapat melihat bagaimana node Anda akan terstruktur. Untuk penguraian, Anda dapat menggunakan prosedur berikut:
Node parseNode() { Token current = consume(); switch (current.lexeme) { case "var": return parseVariableNode(); // ... } panic("unrecognized input!"); } Node n = parseNode(); if (n != null) { // append to some list of top level nodes? // or append to a block of nodes! }
Saya harap Anda mendapatkan inti tentang bagaimana langkah-demi-langkah parsing dari node yang tersisa akan dilanjutkan, dimulai dengan konstruksi bahasa tingkat tinggi. Bagaimana tepatnya parser dengan keturunan rekursif diimplementasikan, Anda perlu belajar sendiri.
Tata bahasa
Mem-parsing dalam ADS dari sejumlah token bisa jadi sulit. Biasanya Anda harus mulai dengan tata bahasa Anda. Intinya, tata bahasa menentukan struktur bahasa Anda. Ada beberapa bahasa untuk mendefinisikan bahasa yang dapat menggambarkan (atau menguraikan) sendiri.
Contoh bahasa untuk menentukan bahasa adalah
bentuk diperpanjang dari Backus-Naur (RBNF). Ini adalah variasi
BNF dengan kurung sudut yang lebih sedikit. Ini adalah contoh RBNF dari artikel Wikipedia:
digit excluding zero = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; digit = "0" | digit excluding zero ;
Aturan produksi didefinisikan: mereka menunjukkan templat terminal mana yang "non-terminal". Terminal adalah bagian dari alfabet, misalnya token atau 0 dan 1 pada contoh di atas adalah terminal. Non-terminal adalah kebalikannya, mereka berada di sisi kiri aturan produksi, dan mereka dapat dianggap sebagai variabel atau "penunjuk bernama" untuk kelompok terminal dan non-terminal.
Banyak bahasa memiliki spesifikasi yang mengandung tata bahasa. Misalnya, untuk
Go ,
Rust, dan
D.Analisis Keturunan Rekursif
Turunan rekursif adalah yang termudah dari banyak pendekatan parsing.
Analisis keturunan rekursif - turun, berdasarkan prosedur rekursif. Jauh lebih mudah untuk menulis parser, karena tata bahasa Anda tidak
meninggalkan rekursi . Dalam sebagian besar bahasa "mainan", teknik ini cukup untuk parsing. GCC menggunakan parser descending yang ditulis tangan, meskipun YACC pernah digunakan sebelumnya.
Namun, parsing bahasa-bahasa ini dapat menyebabkan masalah. Secara khusus, C, di mana
foo * bar
dapat diartikan sebagai
int foo = 3; int bar = 4; foo * bar; // unused expression
atau bagaimana
typedef struct { int b; } foo; foo* bar; bar.b = 3;
Implementasi Dentang juga menggunakan penganalisa keturunan rekursif:
Karena ini adalah kode C ++ reguler, turunan rekursif memudahkan pemula untuk memahaminya. Ini mendukung aturan khusus dan hal-hal aneh lainnya yang dibutuhkan C / C ++ dan membantu Anda dengan mudah mendiagnosis dan memperbaiki kesalahan.Penting juga memperhatikan pendekatan lain:
- turun LL, keturunan rekursif
- ascending LR, shift, keturunan naik
Generator Parser
Cara lain yang bagus. Tentu saja, ada juga kelemahannya - tetapi ini bisa dikatakan tentang pilihan lain yang dibuat programmer saat membuat perangkat lunak.
Generator Parser bekerja sangat cepat. Menggunakannya lebih mudah daripada menulis analisis Anda sendiri dan mendapatkan hasil yang berkualitas - meskipun mereka tidak sangat ramah pengguna dan tidak selalu menampilkan pesan kesalahan. Selain itu, Anda harus belajar cara menggunakan generator parser, dan saat mempromosikan kompiler, Anda mungkin harus melepas generator parser.
Contoh generator parser adalah
ANTLR , ada banyak lainnya.
Saya pikir alat ini cocok untuk mereka yang tidak ingin menghabiskan waktu menulis sebuah frontend, dan yang lebih suka menulis tengah dan backend dari kompiler / juru bahasa dan menganalisis apa pun.
Aplikasi parsing
Jika Anda masih tidak mengerti diri sendiri. Bahkan frontend kompiler (lex / parse) dapat digunakan untuk memecahkan masalah lain:
- penyorotan sintaksis
- Penguraian HTML / CSS untuk mesin rendering
- transpiler: TypeScript, CoffeeScript
- penghubung
- REGEX
- analisis data antarmuka
- Penguraian URL
- alat pemformatan seperti gofmt
- SQL parsing dan lainnya.
Pertengahan
Analisis semantik! Analisis semantik bahasa adalah salah satu tugas yang paling sulit saat membuat kompiler.
Anda perlu memastikan bahwa semua program input bekerja dengan benar. Dalam bahasa Krug saya, aspek yang terkait dengan analisis semantik belum dimasukkan, dan tanpanya, programmer akan selalu diminta untuk menulis kode yang benar. Pada kenyataannya, ini tidak mungkin - dan kami selalu menulis, mengkompilasi, terkadang menjalankan, memperbaiki kesalahan. Spiral ini tidak ada habisnya.
Selain itu, kompilasi program tidak mungkin tanpa analisis kebenaran semantik pada tahap kompilasi yang sesuai.
Saya pernah menemukan grafik tentang persentase front-end, midland dan backend. Kemudian tampak seperti
F: 20% M: 20%: B: 60%
Hari ini seperti itu
F: 5% M: 60% B: 35%
Frontend terutama berkaitan dengan generator, dan dalam bahasa tanpa konteks yang tidak memiliki dualitas tata bahasa, mereka dapat diselesaikan dengan cukup cepat - keturunan rekursif akan membantu di sini.
Dengan teknologi LLVM, sebagian besar pekerjaan pengoptimalan dapat diunggah ke kerangka kerja, yang menghadirkan sejumlah optimasi yang sudah jadi.
Langkah selanjutnya adalah analisis semantik, bagian penting dari fase kompilasi.
Misalnya, di Rust, dengan model manajemen memorinya, kompiler bertindak sebagai mesin besar dan kuat yang melakukan berbagai jenis analisis statis pada formulir pengantar. Bagian dari tugas ini adalah mengubah input data menjadi bentuk yang lebih nyaman untuk analisis.
Untuk alasan ini, analisis semantik memainkan peran penting dalam arsitektur kompiler, dan pekerjaan persiapan yang melelahkan seperti mengoptimalkan perakitan yang dihasilkan atau membaca data input di ASD dilakukan untuk Anda.
Bagian Semantik
Dalam proses analisis semantik, kebanyakan kompiler melakukan sejumlah besar "lintasan semantik" pada SDA atau bentuk abstrak ekspresi kode lainnya.
Artikel ini memberikan detail tentang sebagian besar lintasan yang dibuat dalam kompiler .NET C #.
Saya tidak akan mempertimbangkan setiap bagian, terutama karena mereka bervariasi tergantung pada bahasa, tetapi beberapa langkah dijelaskan di bawah ini di Krug.
Iklan Tingkat Atas
Kompiler akan melalui semua pengumuman "tingkat atas" dalam modul dan mengetahui keberadaannya. Dia tidak akan masuk lebih dalam ke blok - dia hanya akan menyatakan struktur, fungsi, dll. tersedia dalam satu atau modul lain.
Resolusi Nama / Simbol
Kompiler melewati semua blok kode dalam fungsi, dll. dan menyelesaikannya - yaitu, menemukan karakter yang memerlukan izin. Ini adalah pass umum, dan dari sinilah kesalahan
No simbol XYZ seperti itu biasanya datang ketika mengkompilasi kode Go.
Melakukan pass ini bisa sangat sulit, terutama jika ada dependensi melingkar dalam diagram dependensi Anda. Beberapa bahasa tidak mengizinkannya, misalnya, Go akan membuat kesalahan jika salah satu paket Anda membentuk sebuah loop, seperti bahasa Krug saya. Ketergantungan siklik dapat dianggap sebagai efek samping dari arsitektur yang buruk.
Loop dapat ditentukan dengan memodifikasi DFS dalam diagram dependensi, atau dengan menggunakan
algoritma Tarjan (seperti yang dilakukan oleh Krug) untuk mendefinisikan (banyak) loop.
Ketikkan Inference
Compiler memeriksa semua variabel dan menampilkan tipenya. Jenis inferensi di Krug sangat lemah, itu hanya menghasilkan variabel berdasarkan nilai-nilai mereka. Ini sama sekali bukan sistem yang aneh, seperti yang dapat Anda temukan dalam bahasa fungsional seperti Haskell.
Jenis dapat diturunkan menggunakan proses "penyatuan", atau "penyatuan jenis". Untuk sistem tipe yang lebih sederhana, implementasi yang sangat sederhana dapat digunakan.
Jenis diterapkan di Krug seperti ini:
interface Type {}; struct IntegerType : Type { int width; bool signed; }; struct FloatingType : Type { int width; }; struct ArrayType : Type { Type base_type; uint64 length; };
Anda juga dapat memiliki inferensi tipe sederhana, di mana Anda menetapkan tipe ke node ekspresi, misalnya,
IntegerConstantNode
bisa dari tipe IntegerType (64). Dan kemudian Anda bisa mendapatkan fungsi
unify(t1, t2)
, yang akan memilih tipe terluas yang dapat digunakan untuk menyimpulkan tipe ekspresi yang lebih kompleks, katakanlah, yang biner. Jadi itu masalah menugaskan variabel di sebelah kiri ke nilai dari jenis yang diberikan di sebelah kanan.
Saya pernah menulis
jenis cor sederhana di Go, yang menjadi implementasi prototipe untuk Krug.
Pass Mutability
Krug (seperti Rust) secara default adalah bahasa yang tidak dapat diubah, yaitu, variabel tetap tidak berubah kecuali ditentukan lain:
let x = 3; x = 4; // BAD! mut y = 5; y = 6; // OK!
Compiler memeriksa semua blok dan fungsi dan memeriksa bahwa βvariabel-variabelnya benarβ, yaitu, kita tidak mengubah apa yang tidak diikuti, dan bahwa semua variabel yang dikirimkan ke fungsi tertentu konstan atau dapat diubah jika diperlukan.
Ini dilakukan dengan bantuan informasi simbolik yang telah dikumpulkan melalui lintasan sebelumnya. Tabel simbol berdasarkan hasil dari semantic pass berisi nama token dan tanda-tanda variabilitas variabel. Mungkin berisi data lain, misalnya, dalam C ++ tabel dapat menyimpan informasi tentang apakah simbol itu eksternal atau statis.
Tabel karakter
Tabel karakter, atau "tusukan," adalah tabel untuk menemukan karakter yang digunakan dalam program Anda. Satu tabel dibuat untuk setiap cakupan, dan semuanya berisi informasi tentang karakter yang ada dalam lingkup tertentu.
Informasi ini mencakup properti seperti nama simbol, jenis, tanda mutabilitas, keberadaan komunikasi eksternal, lokasi dalam memori statis, dan sebagainya.
Lingkup
Ini adalah konsep penting dalam bahasa pemrograman. Tentu saja, bahasa Anda tidak harus memungkinkan untuk membuat cakupan bersarang, semuanya dapat ditempatkan dalam satu namespace umum!
Meskipun mewakili ruang lingkup adalah tugas yang menarik untuk arsitektur kompiler, dalam sebagian besar bahasa mirip C, ruang lingkup berperilaku (atau) seperti struktur data tumpukan.
Biasanya kita membuat dan menghancurkan cakupan, dan mereka biasanya digunakan untuk mengelola nama, yaitu, mereka memungkinkan kita untuk menyembunyikan (membayangi) variabel:
{ // push scope let x = 3; { // push scope let x = 4; // OK! } // pop scope } // pop scope
Itu dapat direpresentasikan secara berbeda:
struct Scope { Scope* outer; SymbolTable symbols; }
Offtopic kecil, tapi saya sarankan membaca tentang
tumpukan spageti . Ini adalah struktur data yang digunakan untuk menyimpan area visibilitas di ASD node dari blok yang berlawanan.
Jenis sistem
Banyak bagian berikut ini dapat dikembangkan menjadi artikel yang terpisah, tetapi bagi saya tampaknya judul ini paling pantas. Saat ini banyak informasi tersedia tentang sistem jenis, serta varietas sistem itu sendiri, di mana banyak salinan rusak. Saya tidak akan
mempelajari secara mendalam topik ini, tinggalkan saja tautan ke
artikel yang bagus oleh Steve Klabnik .
Jenis sistem adalah apa yang disediakan dan ditentukan secara semantik dalam kompiler menggunakan representasi kompiler dan analisis representasi ini.
Kepemilikan
Konsep ini semakin banyak digunakan dalam pemrograman. Prinsip-prinsip semantik kepemilikan dan gerakan tertanam dalam bahasa
Rust , dan saya berharap mereka akan muncul dalam bahasa lain. Rust melakukan berbagai jenis analisis statis, yang memeriksa untuk melihat apakah input memenuhi seperangkat aturan mengenai memori: siapa yang memiliki memori mana, ketika memori dihancurkan, dan berapa banyak referensi (atau pinjaman) yang ada pada nilai atau memori ini.
Keindahan Rust terletak pada kenyataan bahwa semua ini dilakukan selama kompilasi, di dalam kompiler, sehingga programmer tidak harus berurusan dengan pengumpulan sampah atau penghitungan tautan. Semua semantik ini ditugaskan untuk sistem tipe dan dapat disediakan bahkan sebelum program disajikan dalam bentuk file biner yang lengkap.
Saya tidak bisa mengatakan bagaimana semuanya bekerja di bawah tenda, tetapi semua ini adalah hasil analisis statis dan penelitian hebat oleh tim Mozilla dan peserta proyek
Cyclone .
Grafik Aliran Kontrol
Untuk mewakili aliran program, kami menggunakan grafik aliran kontrol (CFG), yang berisi semua jalur yang dapat diikuti oleh eksekusi program. Ini digunakan dalam analisis semantik untuk mengecualikan bagian kode yang menganggur, yaitu, blok, fungsi, dan bahkan modul yang tidak akan pernah tercapai selama eksekusi program. Grafik juga dapat digunakan untuk mengidentifikasi siklus yang tidak dapat diinterupsi. Atau untuk mencari kode yang tidak dapat diakses, misalnya, ketika Anda memanggil "panik" (memanggil panik), atau kembali dalam satu lingkaran, dan kode di luar lingkaran tidak dijalankan.
Analisis aliran data memainkan peran penting selama fase semantik kompiler, jadi saya sarankan membaca tentang jenis analisis yang dapat Anda lakukan, bagaimana mereka bekerja dan optimasi apa yang dapat dilakukan.
Backend
Bagian terakhir dari skema arsitektur kami.Kami telah melakukan sebagian besar pekerjaan menghasilkan binari yang dapat dieksekusi. Ini bisa dilakukan dengan berbagai cara, yang akan kita bahas di bawah ini.
- , . , , «».
, . , , . , , , . , .
, . , ++ β Cfront β C.
JavaScript. TypeScript , , , , .
«» , , , , Β« Β» . β , , .
LLVM
LLVM: Rust, Swift, C/C++ (clang), D, Haskell.
Β« Β», , . , LLVM . , . , , , , 1, 4, 8 16-. , , - .
-
β , β , .
Go β , LLVM ( ). Go , Windows, Linux MacOS. , Krug -.
. , LLVM, , , LLVM , .
, , , LLVM, IR, , , , ( ).
. , , , . IR ( ) «» fprintf .
8cc .
. β Java: ,
JVM , , Kotlin.
, Java . , . , .
, JVM JIT , JIT-, .
, ! , , . - , , .
Godbolt β , , . , , .
, , (strip the debug symbols), , GCC. , - .
. . , . production-.
rwmj , 8 , 80% . 1971-!
, Rust.
IR
(intermediate representation, IR) , . , , .
IR . , , , .
IR, «», IR . , SSA β Static Single Assignment, , .
Go IR SSA. IR LLVM SSA, .
SSA , , (constant propagation), ( ) .
, . , , , , . ( 16 32), , (spill to the stack).
β ( ). , , .
:
- (graph colouring) β (NP- ). , (liveness ranges) .
- β .
. , . , , .
-, , . , .
fn main() int { let x = 0; { let x = 0; { let x = 0; } } return 0; }
, ( - :) ) , . , .
LLDB
DWARF . LLVM , DWARF GNU-. , , , .
(Foreign Function Interface, FFI )
libc , , . , ?
β . , ( .s/.asm)? ? ,
Jai . , .
(CaaS)
API-. , Krug-, . , , .
, , , . , API-.
production- CaaS. Microsofts Roslyn, , . , , , , API-, , , Rust
RLS .
Krug β β Caasper CaaS-.
Caasper (, , ), , krug, . , , (bootstrap) , .
Krug JavaScript, Go*, , , Krug. JavaScript , yarn/npm.
* Go () , JS.Caasper
.
Github Krug, D LLVM. YouTube-
.
Krug ()
.
Tautan yang bermanfaat