Cara menulis kompiler yang sangat dasar

214

Kompiler tingkat lanjut seperti gccmengkompilasi kode ke dalam file yang dapat dibaca mesin sesuai dengan bahasa di mana kode telah ditulis (misalnya C, C ++, dll). Bahkan, mereka menginterpretasikan arti dari masing-masing kode sesuai dengan perpustakaan dan fungsi dari bahasa yang sesuai. Koreksi saya jika saya salah.

Saya ingin lebih memahami kompiler dengan menulis kompiler yang sangat mendasar (mungkin dalam C) untuk mengkompilasi file statis (misalnya Hello World dalam file teks). Saya mencoba beberapa tutorial dan buku, tetapi semuanya untuk kasus praktis. Mereka berurusan dengan menyusun kode dinamis dengan makna yang terhubung dengan bahasa yang sesuai.

Bagaimana saya bisa menulis kompiler dasar untuk mengubah teks statis menjadi file yang dapat dibaca mesin?

Langkah selanjutnya adalah memperkenalkan variabel ke dalam kompiler; bayangkan kita ingin menulis kompiler yang hanya mengkompilasi beberapa fungsi bahasa.

Memperkenalkan tutorial dan sumber daya yang praktis sangat dihargai :-)

Googlebot
sumber
Sudahkah Anda mencoba lex / flex dan yacc / bison?
mouviciel
15
@ mouviciel: Itu bukan cara yang baik untuk belajar tentang membangun kompiler. Alat-alat itu melakukan banyak kerja keras untuk Anda, jadi Anda tidak pernah benar-benar melakukannya dan belajar bagaimana melakukannya.
Mason Wheeler
11
@Mat menariknya, pertama tautan Anda memberi 404, sedangkan tautan kedua sekarang ditandai sebagai duplikat dari pertanyaan ini .
Ruslan

Jawaban:

326

Intro

Kompiler tipikal melakukan langkah-langkah berikut:

  • Parsing: teks sumber dikonversi ke pohon sintaksis abstrak (AST).
  • Resolusi referensi ke modul lain (C menunda langkah ini sampai menghubungkan).
  • Validasi semantik: menghilangkan pernyataan yang benar secara sintaksis yang tidak masuk akal, misalnya kode yang tidak dapat dijangkau atau deklarasi rangkap.
  • Transformasi yang setara dan optimisasi tingkat tinggi: AST ditransformasikan untuk mewakili komputasi yang lebih efisien dengan semantik yang sama. Ini termasuk misalnya perhitungan awal dari subekspresi umum dan ekspresi konstan, menghilangkan penugasan lokal yang berlebihan (lihat juga SSA ), dll.
  • Pembuatan kode: AST diubah menjadi kode linier tingkat rendah, dengan lompatan, alokasi register, dan sejenisnya. Beberapa panggilan fungsi dapat diuraikan pada tahap ini, beberapa loop terbuka, dll.
  • Optimalisasi lubang telepon: kode tingkat rendah dipindai untuk inefisiensi lokal sederhana yang dihilangkan.

Kebanyakan kompiler modern (misalnya, gcc dan dentang) ulangi dua langkah terakhir sekali lagi. Mereka menggunakan bahasa tingkat rendah menengah-platform tapi independen untuk pembuatan kode awal. Kemudian bahasa itu dikonversi menjadi kode platform-spesifik (x86, ARM, dll) melakukan hal yang kira-kira sama dengan cara yang dioptimalkan platform. Ini termasuk misalnya penggunaan instruksi vektor bila memungkinkan, penataan ulang instruksi untuk meningkatkan efisiensi prediksi cabang, dan sebagainya.

Setelah itu, kode objek siap untuk dihubungkan. Sebagian besar kompiler kode asli tahu cara memanggil tautan untuk menghasilkan yang dapat dieksekusi, tetapi itu bukan langkah kompilasi per se. Dalam bahasa seperti Java dan tautan C # mungkin benar-benar dinamis, dilakukan oleh VM pada waktu pengambilan.

Ingat dasar-dasarnya

  • Buat itu bekerja
  • Jadikan itu indah
  • Jadikan efisien

Urutan klasik ini berlaku untuk semua pengembangan perangkat lunak, tetapi menanggung pengulangan.

Berkonsentrasilah pada langkah pertama dari urutan. Buat hal paling sederhana yang mungkin bisa berhasil.

Baca buku!

Baca Buku Naga oleh Aho dan Ullman. Ini klasik dan masih cukup berlaku hingga saat ini.

Desain Kompiler Modern juga dipuji.

Jika hal ini terlalu sulit bagi Anda sekarang, bacalah beberapa pengantar tentang penguraian terlebih dahulu; biasanya parsing pustaka termasuk intro dan contoh.

Pastikan Anda merasa nyaman bekerja dengan grafik, terutama pohon. Hal-hal ini adalah program-program yang dibuat dari pada tingkat logis.

Definisikan bahasa Anda dengan baik

Gunakan notasi apa pun yang Anda inginkan, tetapi pastikan Anda memiliki deskripsi yang lengkap dan konsisten dari bahasa Anda. Ini termasuk sintaks dan semantik.

Saatnya untuk menulis cuplikan kode dalam bahasa baru Anda sebagai kasus uji untuk kompiler masa depan.

Gunakan bahasa favorit Anda

Tidak apa-apa menulis kompiler dengan Python atau Ruby atau bahasa apa pun yang mudah bagi Anda. Gunakan algoritma sederhana yang Anda pahami dengan baik. Versi pertama tidak harus cepat, efisien, atau lengkap fitur. Hanya perlu cukup benar dan mudah dimodifikasi.

Tidak apa-apa untuk menulis tahapan kompiler yang berbeda dalam bahasa yang berbeda, jika diperlukan.

Bersiaplah untuk menulis banyak tes

Seluruh bahasa Anda harus dicakup oleh uji kasus; secara efektif itu akan ditentukan oleh mereka. Kenal baik-baik dengan kerangka pengujian pilihan Anda. Tulis tes dari hari pertama. Berkonsentrasi pada tes 'positif' yang menerima kode yang benar, yang bertentangan dengan deteksi kode yang salah.

Jalankan semua tes secara teratur. Perbaiki tes yang rusak sebelum melanjutkan. Memalukan untuk berakhir dengan bahasa yang tidak jelas yang tidak dapat menerima kode yang valid.

Buat parser yang bagus

Generator Parser banyak . Pilih apa pun yang Anda inginkan. Anda juga dapat menulis parser Anda sendiri dari awal, tetapi itu hanya layak jika sintaks bahasa Anda sudah mati sederhana.

Parser harus mendeteksi dan melaporkan kesalahan sintaksis. Tulis banyak kasus uji, baik positif maupun negatif; gunakan kembali kode yang Anda tulis saat mendefinisikan bahasa.

Output parser Anda adalah pohon sintaksis abstrak.

Jika bahasa Anda memiliki modul, output parser mungkin merupakan representasi paling sederhana dari 'kode objek' yang Anda hasilkan. Ada banyak cara sederhana untuk membuang pohon ke file dan dengan cepat memuatnya kembali.

Buat validator semantik

Kemungkinan besar bahasa Anda memungkinkan konstruksi yang benar secara sintaksis yang mungkin tidak masuk akal dalam konteks tertentu. Contohnya adalah deklarasi duplikat dari variabel yang sama atau melewati parameter dari tipe yang salah. Validator akan mendeteksi kesalahan seperti itu melihat pohon.

Validator juga akan menyelesaikan referensi ke modul lain yang ditulis dalam bahasa Anda, memuat modul lain ini dan digunakan dalam proses validasi. Misalnya, langkah ini akan memastikan bahwa jumlah parameter yang diteruskan ke fungsi dari modul lain sudah benar.

Sekali lagi, tulis dan jalankan banyak test case. Kasus-kasus sepele sangat diperlukan dalam pemecahan masalah seperti pintar dan kompleks.

Buat kode

Gunakan teknik paling sederhana yang Anda tahu. Seringkali tidak apa-apa untuk langsung menerjemahkan konstruksi bahasa (seperti ifpernyataan) ke templat kode yang parametrik ringan, tidak seperti templat HTML.

Sekali lagi, abaikan efisiensi dan konsentrasi pada kebenaran.

Menargetkan VM level rendah independen-platform

Saya kira Anda mengabaikan hal-hal tingkat rendah kecuali jika Anda tertarik pada detail perangkat keras tertentu. Detail-detail ini berdarah dan kompleks.

Pilihan Anda:

  • LLVM: memungkinkan pembuatan kode mesin yang efisien, biasanya untuk x86 dan ARM.
  • CLR: target .NET, sebagian besar berbasis x86 / Windows; memiliki JIT yang bagus.
  • JVM: menargetkan dunia Java, cukup multiplatform, memiliki JIT yang baik.

Abaikan optimasi

Optimalisasi sulit. Hampir selalu optimisasi prematur. Hasilkan kode yang tidak efisien tetapi benar. Terapkan seluruh bahasa sebelum Anda mencoba mengoptimalkan kode yang dihasilkan.

Tentu saja, optimasi sepele boleh saja diperkenalkan. Tetapi hindari hal-hal yang licik dan berbulu sebelum kompiler Anda stabil.

Terus?

Jika semua ini tidak terlalu menakutkan bagi Anda, silakan lanjutkan! Untuk bahasa yang sederhana, setiap langkah mungkin lebih sederhana dari yang Anda kira.

Melihat 'Hello world' dari program yang dibuat oleh kompiler Anda mungkin sepadan dengan usaha.

9000
sumber
45
Ini adalah salah satu jawaban terbaik yang pernah saya lihat.
gahooa
11
Saya pikir Anda melewatkan bagian dari pertanyaan ... OP ingin menulis kompiler yang sangat dasar . Saya pikir Anda melampaui sangat mendasar di sini.
marco-fiset
22
@ marco-fiset , sebaliknya, saya pikir ini adalah jawaban yang luar biasa yang memberi tahu OP bagaimana melakukan kompiler yang sangat dasar, sambil menunjukkan jebakan untuk menghindari dan mendefinisikan fase yang lebih maju.
smci
6
Ini adalah salah satu jawaban terbaik yang pernah saya lihat di seluruh dunia Stack Exchange. Pujian!
Andre Terra
3
Melihat 'Hello world' dari program yang dibuat oleh kompiler Anda mungkin sepadan dengan usaha. - INDEED
slier
27

Jack Crenshaw's Let's Build a Compiler , walaupun belum selesai, adalah pengantar dan tutorial yang mudah dibaca.

Konstruksi Kompilator Nicklaus Wirth adalah buku teks yang sangat bagus tentang dasar-dasar konstruksi kompiler sederhana. Dia berfokus pada keturunan rekursif top-down, yang, mari kita hadapi itu, BANYAK lebih mudah daripada lex / yacc atau flex / bison. Kompilator PASCAL asli yang ditulis kelompoknya dilakukan dengan cara ini.

Orang lain telah menyebutkan berbagai buku Naga.

John R. Strohm
sumber
1
Salah satu hal yang menyenangkan tentang Pascal, adalah bahwa segala sesuatu harus didefinisikan atau dideklarasikan sebelum digunakan. Karena itu dapat dikompilasi dalam satu pass. Turbo Pascal 3.0 adalah salah satu contohnya, dan ada banyak dokumentasi tentang internal di sini .
tcrosley
1
PASCAL secara khusus dirancang dengan kompilasi satu jalur dan menghubungkannya dalam pikiran. Buku kompiler Wirth menyebutkan kompiler multipass, dan menambahkan bahwa ia tahu kompiler PL / I yang mengambil 70 (ya, tujuh puluh) lintasan.
John R. Strohm
Deklarasi wajib sebelum tanggal penggunaan kembali ke ALGOL. Tony Hoare mendapatkan telinganya kembali oleh komite ALGOL ketika ia mencoba menyarankan menambahkan aturan tipe default, mirip dengan apa yang FORTRAN miliki. Mereka sudah tahu tentang masalah yang bisa terjadi ini, dengan kesalahan ketik pada nama dan aturan default membuat bug yang menarik.
John R. Strohm
1
Berikut ini adalah versi buku yang lebih diperbarui dan selesai oleh penulis aslinya sendiri: stack.nl/~marcov/compiler.pdf Harap edit jawaban Anda dan tambahkan ini :)
soneta
16

Saya benar-benar mulai dengan menulis kompiler untuk Brainfuck . Ini adalah bahasa yang cukup bodoh untuk diprogram, tetapi hanya memiliki 8 instruksi untuk diterapkan. Ini tentang sesederhana yang Anda bisa dapatkan dan ada instruksi C yang setara di luar sana untuk perintah yang terlibat jika Anda menemukan sintaks yang tidak sesuai.

Insinyur Dunia
sumber
7
Tapi kemudian, setelah Anda memiliki kompiler BF Anda siap, Anda harus menulis kode Anda di dalamnya :(
500 - Internal Server Error
@ 500-InternalServerError menggunakan metode subset C
World Engineer
12

Jika Anda benar-benar ingin menulis kode yang dapat dibaca mesin saja dan tidak ditargetkan ke mesin virtual, maka Anda harus membaca manual Intel dan mengerti

  • Sebuah. Menautkan dan Memuat kode yang dapat dijalankan

  • b. Format COFF dan PE (untuk windows), atau memahami format ELF (untuk Linux)

  • c. Memahami format file .COM (lebih mudah daripada PE)
  • d. Memahami assembler
  • e. Memahami kompiler dan mesin pembuatan kode dalam kompiler.

Jauh lebih sulit dilakukan daripada dikatakan. Saya sarankan Anda untuk membaca Compiler dan Juru Bahasa dalam C ++ sebagai titik awal (Oleh Ronald Mak). Atau, "mari kita buat kompiler" oleh Crenshaw adalah OK.

Jika Anda tidak ingin melakukan itu, Anda juga bisa menulis VM Anda sendiri dan menulis pembuat kode yang ditargetkan untuk VM itu.

Kiat: Pelajari Flex dan Bison PERTAMA. Kemudian lanjutkan untuk membangun kompiler / VM Anda sendiri.

Semoga berhasil!

Aniket Inge
sumber
7
Saya pikir menargetkan LLVM dan bukan kode mesin nyata adalah tentang cara terbaik yang tersedia saat ini.
9000
Saya setuju, saya telah mengikuti LLVM untuk beberapa waktu sekarang dan saya harus mengatakan itu adalah salah satu hal terbaik yang telah saya lihat selama bertahun-tahun dalam hal upaya programmer yang diperlukan untuk menargetkan itu!
Aniket Inge
2
Bagaimana dengan MIPS dan gunakan spim untuk menjalankannya? Atau MIX ?
@MichaelT Saya belum pernah menggunakan MIPS tapi saya yakin itu akan baik.
Aniket Inge
Set instruksi @PrototypeStark RISC, prosesor dunia nyata yang masih digunakan saat ini (memahaminya akan diterjemahkan ke dalam sistem tertanam). Set instruksi lengkap ada di wikipedia . Melihat di internet, ada banyak contoh dan digunakan di banyak kelas akademik sebagai target untuk pemrograman bahasa mesin. Ada sedikit aktivitas di SO .
10

Pendekatan DIY untuk kompiler sederhana bisa terlihat seperti ini (setidaknya seperti itulah proyek uni saya):

  1. Tentukan Grammar bahasa. Bebas konteks.
  2. Jika tata bahasa Anda belum LL (1), lakukan sekarang. Perhatikan, bahwa beberapa aturan yang tampak ok dalam tata bahasa CF polos mungkin menjadi jelek. Mungkin bahasa Anda terlalu rumit ...
  3. Tulis Lexer yang memotong aliran teks menjadi token (kata, angka, literal).
  4. Tulis parser keturunan rekursif top-down untuk tata bahasa Anda, yang menerima atau menolak input.
  5. Tambahkan generasi pohon sintaksis ke parser Anda.
  6. Tulis pembuat kode mesin dari pohon sintaks.
  7. Untung & Bir, atau Anda dapat mulai berpikir bagaimana melakukan parser yang lebih pintar atau menghasilkan kode yang lebih baik.

Seharusnya ada banyak literatur yang menjelaskan setiap langkah secara terperinci.

Merusak
sumber
Poin 7 adalah apa yang ditanyakan OP.
Florian Margaine
7
1-5 tidak relevan dan tidak layak mendapatkan perhatian sedekat itu. 6 adalah bagian yang paling menarik. Sayangnya, sebagian besar buku mengikuti pola yang sama, setelah buku naga yang terkenal itu, terlalu banyak memberi perhatian pada penguraian dan meninggalkan perubahan kode di luar jangkauan.
SK-logic