Bagaimana obat generik diimplementasikan dalam kompiler modern?

15

Yang saya maksud di sini adalah bagaimana kita beralih dari beberapa template T add(T a, T b) ...ke kode yang dihasilkan? Saya telah memikirkan beberapa cara untuk mencapai hal ini, kami menyimpan fungsi generik dalam AST Function_Nodedan kemudian setiap kali kami menggunakannya, kami menyimpan dalam simpul fungsi asli salinan dirinya sendiri dengan semua jenis Tdiganti dengan jenis yang sedang digunakan. Sebagai contohadd<int>(5, 6) akan menyimpan salinan dari fungsi generik untuk adddan mengganti semua jenis T di copy dengan int.

Jadi akan terlihat seperti:

struct Function_Node {
    std::string name; // etc.
    Type return_type;
    std::vector<std::pair<Type, std::string>> arguments;
    std::vector<Function_Node> copies;
};

Kemudian Anda dapat menghasilkan kode untuk ini dan ketika Anda mengunjungi Function_Nodetempat salinan copies.size() > 0, Anda meminta visitFunctionsemua salinan.

visitFunction(Function_Node& node) {
    if (node.copies.size() > 0) {
        for (auto& node : nodes.copies) {
            visitFunction(node);
        }
        // it's a generic function so we don't want
        // to emit code for this.
        return;
    }
}

Apakah ini akan berhasil? Bagaimana kompiler modern mendekati masalah ini? Saya pikir mungkin cara lain untuk melakukan ini adalah Anda bisa menyuntikkan salinan ke AST sehingga itu berjalan melalui semua fase semantik. Saya juga berpikir mungkin Anda bisa membuatnya dalam bentuk langsung seperti Rust's MIR atau Swifts SIL misalnya.

Kode saya ditulis dalam Java, contohnya di sini adalah C ++ karena agak kurang bertele-tele untuk contoh - tetapi prinsipnya pada dasarnya adalah hal yang sama. Meskipun mungkin ada beberapa kesalahan karena hanya ditulis tangan di kotak pertanyaan.

Perhatikan bahwa maksud saya kompiler modern seperti apa cara terbaik untuk mendekati masalah ini. Dan ketika saya mengatakan generik, saya tidak bermaksud seperti generik Java di mana mereka menggunakan penghapusan tipe.

Jon Flow
sumber
Dalam C ++ (bahasa pemrograman lain memiliki generik, tetapi mereka masing-masing mengimplementasikannya secara berbeda), pada dasarnya ini adalah sistem makro raksasa, waktu kompilasi. Kode aktual dihasilkan menggunakan tipe yang diganti.
Robert Harvey
Mengapa tidak mengetik penghapusan? Perlu diingat bahwa bukan hanya Java yang melakukannya, dan itu bukan teknik yang buruk (tergantung pada kebutuhan Anda).
Andres F.
@AndresF. Saya pikir mengingat cara kerja bahasa saya, itu tidak akan berhasil dengan baik.
Jon Flow
2
Saya pikir Anda harus mengklarifikasi jenis obat apa yang Anda bicarakan. Sebagai contoh, template C ++, generik C # dan generik Java semuanya sangat berbeda satu sama lain. Anda mengatakan bahwa Anda tidak bermaksud generik Java, tetapi Anda tidak mengatakan apa yang Anda maksudkan.
svick
2
Ini benar-benar perlu fokus pada sistem satu bahasa untuk menghindari menjadi terlalu luas
Daenyth

Jawaban:

14

Bagaimana obat generik diimplementasikan dalam kompiler modern?

Saya mengundang Anda untuk membaca kode sumber kompiler modern jika Anda ingin tahu cara kerja kompiler modern. Saya akan mulai dengan proyek Roslyn, yang mengimplementasikan kompiler C # dan Visual Basic.

Secara khusus saya menarik perhatian Anda pada kode di kompiler C # yang mengimplementasikan simbol tipe:

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Symbols

dan Anda mungkin juga ingin melihat kode untuk aturan konversi. Ada banyak hal yang berkaitan dengan manipulasi aljabar dari tipe generik.

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions

Saya berusaha keras untuk membuat yang terakhir mudah dibaca.

Saya telah memikirkan beberapa cara untuk mencapai hal ini, kami menyimpan fungsi generik dalam AST sebagai Function_Node dan kemudian setiap kali kami menggunakannya, kami menyimpan dalam simpul fungsi asli salinan dirinya sendiri dengan semua jenis T yang diganti dengan jenis yang sedang digunakan.

Anda menggambarkan templat , bukan generik . C # dan Visual Basic memiliki generik aktual dalam sistem tipenya.

Secara singkat, mereka bekerja seperti ini.

  • Kita mulai dengan menetapkan aturan untuk apa yang secara formal merupakan tipe pada waktu kompilasi. Sebagai contoh: intadalah tipe, parameter tipe Tadalah tipe, untuk tipe apa punX , tipe array X[]juga tipe, dan seterusnya.

  • Aturan untuk generik melibatkan substitusi. Misalnya, class C with one type parameterbukan tipe. Ini adalah pola untuk membuat tipe. class C with one type parameter called T, under substitution with int for T adalah tipe.

  • Aturan yang menggambarkan hubungan antara jenis - kompatibilitas pada penugasan, bagaimana menentukan jenis ekspresi, dan sebagainya - dirancang dan diimplementasikan dalam kompiler.

  • Bahasa bytecode yang mendukung tipe generik dalam sistem metadata dirancang dan diimplementasikan.

  • Pada saat runtime, kompiler JIT mengubah bytecode menjadi kode mesin; itu bertanggung jawab untuk membangun kode mesin yang sesuai diberikan spesialisasi generik.

Jadi misalnya, di C # saat Anda mengatakan

class C<T> { public void X(T t) { Console.WriteLine(t); } }
...
var c = new C<int>(); 
c.X(123);

kemudian kompiler memverifikasi bahwa dalam C<int>, argumen intadalah substitusi yang valid untuk T, dan menghasilkan metadata dan bytecode yang sesuai. Saat runtime, jitter mendeteksi bahwa a C<int>sedang dibuat untuk pertama kalinya dan menghasilkan kode mesin yang sesuai secara dinamis.

Eric Lippert
sumber
9

Sebagian besar implementasi generik (atau lebih tepatnya: polimorfisme parametrik) memang menggunakan penghapusan tipe. Ini sangat menyederhanakan masalah mengkompilasi kode generik, tetapi hanya bekerja untuk tipe kotak: karena setiap argumen secara efektif adalah pointer buram, kita membutuhkan mekanisme pengiriman VTable atau serupa untuk melakukan operasi pada argumen. Di Jawa:

<T extends Addable> T add(T a, T b) { … }

dapat dikompilasi, ketik-diperiksa, dan dipanggil dengan cara yang sama

Addable add(Addable a, Addable b) { … }

kecuali bahwa obat generik memberikan pemeriksa tipe dengan informasi yang jauh lebih banyak di situs panggilan. Informasi tambahan ini dapat ditangani dengan variabel tipe , terutama ketika tipe generik disimpulkan. Selama pemeriksaan tipe, setiap tipe generik dapat diganti dengan variabel, sebut saja $T1:

$T1 add($T1 a, $T1 b)

Variabel tipe kemudian diperbarui dengan lebih banyak fakta ketika mereka diketahui, hingga dapat diganti dengan tipe konkret. Algoritma pengecekan tipe harus ditulis dengan cara yang mengakomodasi variabel tipe ini bahkan jika mereka belum diselesaikan ke tipe yang lengkap. Di Jawa sendiri hal ini biasanya dapat dilakukan dengan mudah karena jenis argumen sering dikenal sebelum jenis pemanggilan fungsi perlu diketahui. Pengecualian penting adalah ekspresi lambda sebagai argumen fungsi, yang mengharuskan penggunaan variabel tipe tersebut.

Jauh kemudian, pengoptimal mungkin melakukannya menghasilkan kode khusus untuk serangkaian argumen tertentu, ini kemudian secara efektif akan menjadi semacam inlining.

VTable untuk argumen yang diketik secara umum dapat dihindari jika fungsi generik tidak melakukan operasi apa pun pada tipe tersebut, tetapi hanya meneruskannya ke fungsi lain. Misalnya fungsi Haskell call :: (a -> b) -> a -> b; call f x = f xtidak perlu mengotak-kotak xargumen. Namun, ini memang membutuhkan konvensi pemanggilan yang dapat melewati nilai-nilai tanpa mengetahui ukurannya, yang pada dasarnya membatasi itu ke pointer.


C ++ sangat berbeda dari kebanyakan bahasa dalam hal ini. Kelas atau fungsi templated (saya hanya akan membahas fungsi templated di sini) tidak bisa dipanggil sendiri. Alih-alih, templat harus dipahami sebagai fungsi meta waktu kompilasi yang mengembalikan fungsi sebenarnya. Mengabaikan inferensi argumen templat sesaat, pendekatan umum kemudian mengarah pada langkah-langkah ini:

  1. Terapkan templat ke argumen templat yang disediakan. Misalnya memanggil template<class T> T add(T a, T b) { … }sebagai add<int>(1, 2)akan memberi kita fungsi yang sebenarnya int __add__T_int(int a, int b)(atau apa pun pendekatan nama-mangling digunakan).

  2. Jika kode untuk fungsi tersebut telah dihasilkan di unit kompilasi saat ini, lanjutkan. Jika tidak, buat kode seolah-olah suatu fungsi int __add__T_int(int a, int b) { … }telah ditulis dalam kode sumber. Ini melibatkan mengganti semua kemunculan argumen templat dengan nilainya. Ini mungkin merupakan transformasi AST → AST. Kemudian, lakukan pengecekan jenis pada AST yang dihasilkan.

  3. Kompilasi panggilan seolah-olah kode sumber sudah __add__T_int(1, 2).

Perhatikan bahwa templat C ++ memiliki interaksi yang kompleks dengan mekanisme resolusi kelebihan beban, yang tidak ingin saya uraikan di sini. Juga catat bahwa pembuatan kode ini tidak memungkinkan untuk memiliki metode templated yang juga virtual - pendekatan berbasis tipe penghapusan tidak menderita dari pembatasan substansial ini.


Apa artinya ini bagi kompiler dan / atau bahasa Anda? Anda harus berpikir hati-hati tentang jenis obat generik yang ingin Anda tawarkan. Penghapusan tipe tanpa adanya inferensi tipe adalah pendekatan yang paling sederhana jika Anda mendukung tipe kotak. Spesialisasi template tampaknya cukup sederhana, tetapi biasanya melibatkan pembuatan nama dan (untuk beberapa unit kompilasi) duplikasi substansial dalam output, karena template dipakai di situs panggilan, bukan situs definisi.

Pendekatan yang Anda tunjukkan pada dasarnya adalah pendekatan template mirip C ++. Namun, Anda menyimpan templat khusus / instantiated sebagai "versi" dari templat utama. Ini menyesatkan: mereka tidak sama secara konseptual, dan instansi yang berbeda dari suatu fungsi dapat memiliki tipe yang sangat berbeda. Ini akan mempersulit hal-hal dalam jangka panjang jika Anda juga mengizinkan kelebihan fungsi. Sebagai gantinya, Anda akan memerlukan gagasan tentang kumpulan kelebihan yang berisi semua fungsi dan templat yang mungkin berbagi nama. Kecuali untuk mengatasi kelebihan beban, Anda dapat mempertimbangkan berbagai templat instantiated yang berbeda satu sama lain.

amon
sumber