Kapan masuk akal mengkompilasi bahasa saya sendiri ke kode C terlebih dahulu?


34

Ketika mendesain bahasa pemrograman sendiri, kapan masuk akal untuk menulis konverter yang mengambil kode sumber dan mengubahnya menjadi kode C atau C ++ sehingga saya bisa menggunakan kompiler yang ada seperti gcc untuk berakhir dengan kode mesin? Apakah ada proyek yang menggunakan pendekatan ini?



4
Jika Anda melihat melewati C, Anda akan melihat bahwa C # dan Java juga mengkompilasi ke bahasa perantara. Anda diselamatkan karena harus mengulang banyak pekerjaan yang telah dilakukan orang lain dengan menargetkan bahasa perantara alih-alih langsung menuju perakitan.
Casey

1
@emodendroket Namun, C # dan Java mengkompilasi ke IL yang dirancang untuk menjadi IL secara umum dan untuk C # / Java secara khusus, jadi dalam banyak hal CIL dan JVM bytecode lebih masuk akal dan nyaman sebagai IL daripada C yang pernah ada. Ini bukan tentang apakah akan menggunakan bahasa perantara, ini tentang bahasa perantara mana yang digunakan.

1
Lihatlah beberapa implementasi perangkat lunak bebas yang menghasilkan kode C. Dan saya harap Anda akan membuat perangkat lunak implementasi bahasa Anda menjadi gratis.
Basile Starynkevitch

2
Berikut ini tautan yang diperbarui dari komentar @ RobertHarvey: yosefk.com/blog/c-as-an-intermediate-language.html .
Christian Dean

Jawaban:


52

Meneruskan kode C adalah kebiasaan yang sudah sangat mapan. C asli dengan kelas (dan implementasi C ++ awal, kemudian disebut Cfront ) berhasil melakukannya. Beberapa implementasi Lisp atau Skema melakukan hal itu, misalnya Skema Ayam , Skema48 , Bigloo . Beberapa orang diterjemahkan Prolog ke C . Dan begitu pula beberapa versi Mozart (dan ada upaya untuk mengkompilasi bytecode Ocaml ke C ). Sistem kecerdasan buatan J.Pitrat, CAIA, juga bootstrap dan menghasilkan semua kode C-nya. Vala juga diterjemahkan ke C, untuk kode terkait GTK. Buku Queinnec, Lisp In Small Pieces memiliki beberapa bab tentang terjemahan ke C.

Salah satu masalah saat menerjemahkan ke C adalah panggilan berulang-ulang . Standar C tidak menjamin bahwa kompiler C menerjemahkannya dengan benar (menjadi "lompatan dengan argumen", yaitu tanpa memakan tumpukan panggilan), bahkan jika dalam beberapa kasus, versi terbaru GCC (atau Dentang / LLVM) melakukan optimasi itu .

Masalah lainnya adalah pengumpulan sampah . Beberapa implementasi hanya menggunakan pengumpul sampah konservatif Boehm (yang ramah-C ...). Jika Anda ingin mengumpulkan kode sampah (seperti beberapa implementasi Lisp lakukan, misalnya SBCL) yang mungkin menjadi mimpi buruk (Anda ingin dlclosedi Posix).

Namun masalah lain adalah berurusan dengan kelanjutan kelas satu dan panggilan / cc . Tapi trik pintar mungkin dilakukan (lihat di dalam Skema Ayam). Mengakses tumpukan panggilan bisa memerlukan banyak trik (tapi lihat GNU backtrace , dll ....). Kegigihan ortogonal dari kelanjutan (yaitu tumpukan atau benang) akan sulit di C.

Penanganan pengecualian sering merupakan masalah untuk memancarkan panggilan pintar ke longjmp dll ...

Anda mungkin ingin menghasilkan (dalam kode C Anda yang dipancarkan) #linearahan yang sesuai . Ini membosankan dan membutuhkan banyak pekerjaan (Anda akan ingin itu misalnya menghasilkan gdbkode yang lebih mudah -debuggable).

Saya MELT lispy bahasa domain yang spesifik (untuk menyesuaikan atau memperpanjang GCC ) diterjemahkan ke C (sebenarnya untuk miskin C ++ sekarang). Ia memiliki pengumpul sampah penyalinan generasinya sendiri. (Anda mungkin tertarik oleh Qish atau Ravenbrook MPS ). Sebenarnya, GC generasi lebih mudah dalam kode C yang dihasilkan mesin daripada kode C yang ditulis tangan (karena Anda akan menyesuaikan generator kode C Anda untuk penghalang tulis dan mesin GC Anda).

Saya tidak tahu implementasi bahasa apa pun yang diterjemahkan ke kode C ++ asli , yaitu menggunakan beberapa teknik "pengumpulan sampah waktu" untuk memancarkan kode C ++ menggunakan banyak template STL dan menghormati idiom RAII . (tolong beri tahu jika Anda tahu satu).

Apa yang lucu hari ini adalah bahwa (pada desktop Linux saat ini) kompiler C mungkin cukup cepat untuk mengimplementasikan loop read-eval-print- level interaktif yang diterjemahkan ke C: Anda akan memancarkan kode C (beberapa ratus baris) pada setiap pengguna interaksi, Anda akan forkkompilasi menjadi objek bersama, yang kemudian Anda lakukan dlopen. (MELT melakukan semuanya sudah siap, dan biasanya cukup cepat). Semua ini mungkin memerlukan beberapa persepuluh detik dan dapat diterima oleh pengguna akhir.

Jika memungkinkan, saya akan merekomendasikan untuk menerjemahkan ke C, bukan ke C ++, khususnya karena kompilasi C ++ lambat.

Jika Anda menerapkan bahasa Anda, Anda mungkin juga mempertimbangkan (alih-alih memancarkan kode C) beberapa pustaka JIT seperti libjit , GNU lightning , asmjit , atau bahkan LLVM atau GCCJIT . Jika Anda ingin menerjemahkan ke C, Anda terkadang menggunakan tinycc : ia mengkompilasi dengan sangat cepat kode C yang dihasilkan (bahkan dalam memori) untuk memperlambat kode mesin. Tetapi secara umum Anda ingin mengambil keuntungan dari optimasi yang dilakukan oleh kompiler C nyata seperti GCC

Jika Anda menerjemahkan ke bahasa C Anda, pastikan untuk membangun AST seluruh kode C yang dihasilkan dalam memori terlebih dahulu (ini juga membuat lebih mudah untuk menghasilkan semua deklarasi terlebih dahulu, lalu semua definisi dan kode fungsi). Anda dapat melakukan beberapa optimasi / normalisasi dengan cara ini. Anda juga dapat tertarik pada beberapa ekstensi GCC (mis. Goto yang dihitung). Anda mungkin ingin menghindari menghasilkan fungsi C yang sangat besar - misalnya dari seratus ribu garis C yang dihasilkan - (Anda sebaiknya membaginya menjadi bagian-bagian yang lebih kecil) karena mengoptimalkan kompiler C sangat tidak senang dengan fungsi C yang sangat besar (dalam praktiknya, dan secara eksperimental,gcc -Owaktu kompilasi fungsi besar sebanding dengan kuadrat ukuran kode fungsi). Jadi batasi ukuran fungsi C yang Anda buat masing-masing beberapa ribu baris.

Perhatikan bahwa kedua dentang (melalui LLVM ) dan GCC (melalui libgccjit ) C & C ++ compiler menawarkan beberapa cara untuk memancarkan beberapa representasi internal yang cocok untuk compiler ini, namun hal ini kekuatan (atau tidak) lebih sulit daripada memancarkan C (atau C ++) kode, dan khusus untuk setiap kompiler.

Jika mendesain bahasa yang akan diterjemahkan ke C, Anda mungkin ingin memiliki beberapa trik (atau konstruksi) untuk menghasilkan campuran C dengan bahasa Anda. Kertas DSL2011 saya MELT : Bahasa Tertentu Domain Diterjemahkan yang tertanam dalam GCC Compiler akan memberi Anda petunjuk yang bermanfaat.


Apakah Anda mengacu pada "Skema Ayam?"
Robert Harvey

1
Ya. Saya memberikan URL.
Basile Starynkevitch

Apakah relatif praktis untuk membuat mesin virtual, seperti Java atau sesuatu, kompilasi bytecode ke C, kemudian gunakan gcc untuk kompilasi JIT? Atau haruskah mereka langsung beralih dari bytecode ke assembly?
Panzercrisis

1
@ Panzercrisis Sebagian besar kompiler JIT memerlukan backend kode mesin mereka untuk mendukung hal-hal seperti mengganti fungsi dan menambal kode yang ada dengan pintu lompat / perangkap. Selain itu, gcc secara khusus adalah ... secara arsitektur kurang cocok untuk kompilasi JIT dan kasus penggunaan lainnya. Lihatlah libgccjit: gcc.gnu.org/ml/gcc-patches/2013-10/msg00228.html dan gcc.gnu.org/wiki/JIT

1
Materi orientasi luar biasa. Terima kasih!
Capr

7

Masuk akal ketika waktu untuk menghasilkan kode mesin penuh melebihi ketidaknyamanan memiliki langkah perantara mengkompilasi "IL" Anda ke dalam kode mesin menggunakan kompiler C.

Biasanya bahasa khusus domain ditulis dengan cara ini, sistem tingkat yang sangat tinggi digunakan untuk mendefinisikan atau menggambarkan proses yang kemudian dikompilasi ke dalam executable atau dll. Waktu yang dibutuhkan untuk menghasilkan kerja / perakitan yang baik jauh lebih besar daripada menghasilkan C, dan C cukup menutup kode perakitan untuk kinerja, sehingga masuk akal untuk menghasilkan C dan menggunakan kembali keterampilan penulis kompiler C. Perhatikan bahwa ini tidak hanya mengkompilasi, tetapi juga mengoptimalkan - orang-orang yang menulis gcc atau llvm telah menghabiskan banyak waktu membuat kode mesin yang dioptimalkan, itu akan bodoh untuk mencoba menemukan kembali semua kerja keras mereka.

Mungkin lebih bisa diterima untuk menggunakan kembali backend compiler LLVM yang IIRC netral bahasa, jadi Anda menghasilkan instruksi LLVM daripada kode C.


Sepertinya perpustakaan adalah alasan yang cukup menarik untuk mempertimbangkannya juga.
Casey

Ketika Anda mengatakan "IL 'Anda", apa yang Anda maksud? Pohon Sintaksis Abstrak?
Robert Harvey

@ RobertTarvey tidak, maksud saya kode C. Dalam kasus OP, ini adalah Bahasa Menengah di antara bahasa tingkat tinggi dan kode mesinnya sendiri. Saya menuliskannya dalam tanda kutip untuk mencoba dan menyampaikan gagasan ini bahwa ini bukan IL seperti yang digunakan oleh banyak orang (misalnya Microsoft. NET IL misalnya)
gbjbaanb

2

Menulis kompiler untuk menghasilkan kode mesin mungkin tidak jauh lebih sulit daripada menulis yang menghasilkan C (dalam beberapa kasus mungkin lebih mudah), tetapi kompiler yang menghasilkan kode mesin hanya akan dapat menghasilkan program yang bisa dijalankan pada platform tertentu yang ini sudah tertulis; Sebaliknya, sebuah kompiler yang menghasilkan kode C mungkin dapat menghasilkan program untuk platform apa pun yang menggunakan dialek C yang mana kode yang dihasilkan dirancang untuk mendukung. Perhatikan bahwa dalam banyak kasus dimungkinkan untuk menulis kode C yang sepenuhnya portabel dan yang akan berperilaku seperti yang diinginkan tanpa menggunakan perilaku yang tidak dijamin oleh standar C, tetapi kode yang bergantung pada perilaku yang dijamin platform mungkin dapat berjalan lebih cepat pada platform yang membuat jaminan itu daripada kode yang tidak.

Sebagai contoh, anggap suatu bahasa mendukung fitur untuk menghasilkan a UInt32dari empat byte berturut-turut dari yang selaras UInt8[], ditafsirkan dalam mode big-endian. Pada beberapa kompiler, seseorang dapat menulis kode sebagai:

uint32_t dat = *(__packed uint32_t*)p;
return (dat >> 24) | (dat >> 8) | ((uint32_t)dat << 8) | ((uint32_t)dat << 24));

dan minta kompiler menghasilkan operasi memuat kata yang diikuti dengan instruksi reverse-bytes-in-word. Beberapa kompiler, bagaimanapun, tidak akan mendukung pengubah __packed dan jika tidak ada akan menghasilkan kode yang tidak akan berfungsi.

Atau, seseorang dapat menulis kode sebagai:

return dat[3] | ((uint16_t)dat[2] << 8) | ((uint32_t)dat[1] << 16) | ((uint32_t)dat[0] << 24);

kode seperti itu harus bekerja pada platform apa pun, bahkan di mana CHAR_BITStidak 8 (dengan asumsi bahwa setiap oktet data sumber berakhir dalam elemen array yang berbeda), tetapi kode tersebut mungkin tidak berjalan hampir secepat seperti yang non-portabel versi pada platform yang mendukung yang pertama.

Perhatikan bahwa portabilitas sering kali mengharuskan kode menjadi sangat liberal dengan typecast dan konstruksi serupa. Sebagai contoh, kode yang ingin mengalikan dua bilangan bulat 32-bit unsigned dan menghasilkan 32 bit yang lebih rendah dari hasil harus untuk portabilitas ditulis sebagai:

uint32_t result = 1u*x*y;

Tanpa itu 1u, kompiler pada sistem di mana INT_BITS berkisar antara 33 hingga 64 dapat secara sah melakukan apa pun yang diinginkan jika produk x dan y lebih besar dari 2.147.483.647, dan beberapa kompiler cenderung mengambil keuntungan dari peluang tersebut.


1

Anda memiliki beberapa jawaban yang sangat baik di atas tetapi mengingat bahwa, dalam komentar, Anda menjawab pertanyaan, "Mengapa Anda ingin membuat bahasa pemrograman sendiri di tempat pertama?" Dengan "Itu terutama untuk tujuan belajar," Saya akan menjawab dari sudut yang berbeda.

Masuk akal untuk menulis konverter yang mengambil kode sumber dan mengubahnya menjadi kode C atau C ++, sehingga Anda dapat menggunakan kompiler yang ada seperti gcc untuk berakhir dengan kode mesin, jika Anda lebih tertarik untuk belajar tentang leksikal, sintaksis dan analisis semantik daripada Anda belajar tentang pembuatan kode dan optimisasi!

Menulis generator kode mesin Anda sendiri adalah pekerjaan yang cukup signifikan yang dapat Anda hindari dengan mengkompilasi ke kode C, jika itu bukan yang Anda minati!

Namun, jika Anda tertarik dengan program perakitan dan terpesona oleh tantangan mengoptimalkan kode di level terendah, maka tentu saja, tulis pembuat kode sendiri untuk pengalaman belajar!


-7

Itu tergantung pada Sistem Operasi apa yang Anda gunakan jika Anda menggunakan Windows ada Microsoft IL (Bahasa Menengah) yang mengubah kode Anda menjadi bahasa perantara sehingga tidak perlu waktu untuk dikompilasi ke dalam kode mesin. Atau Jika Anda menggunakan Linux ada kompiler terpisah untuk itu

Kembali ke pertanyaan Anda adalah ketika Anda ketika merancang bahasa Anda sendiri, Anda harus memiliki kompiler atau juru bahasa terpisah untuk itu karena mesin tidak tahu bahasa tingkat tinggi. Kode Anda harus dikompilasi ke dalam kode mesin untuk membuatnya berguna untuk mesin


2
Your code should be compiled into machine code to make it useful for machine- Jika kompiler Anda menghasilkan kode c sebagai output, Anda bisa memasukkan kode c ke dalam kompiler ac untuk menghasilkan kode mesin, bukan?
Robert Harvey

iya nih. karena mesin bukan bahasa c
Tayyab Gulsher Vohra

2
Kanan. Jadi pertanyaannya adalah "Kapan masuk akal untuk memancarkan c dan menggunakan kompiler ac, daripada memancarkan bahasa mesin atau kode byte secara langsung?"
Robert Harvey

sebenarnya ia meminta untuk merancang bahasa pemrogramannya di mana ia meminta agar "mengubahnya menjadi kode C atau C ++". Jadi saya menjelaskan ini jika Anda merancang bahasa pemrograman Anda sendiri mengapa Anda harus menggunakan c compiler atau c ++. jika Anda cukup cerdas, Anda harus mendesain sendiri
Tayyab Gulsher Vohra

8
Saya pikir Anda tidak mengerti pertanyaan itu. Lihat yosefk.com/blog/c-as-an-intermediate-language.html
Robert Harvey
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.