Perangkat kompiler Swift. Bagian 4


Ini adalah bagian terakhir dari ulasan saya tentang kompiler Swift. Saya akan menunjukkan kepada Anda bagaimana menghasilkan LLVM IR dari AST dan apa sebenarnya frontend itu. Jika Anda belum membaca bagian sebelumnya, maka ikuti tautannya:



LLVM IR Gen


Untuk frontend, ini adalah langkah terakhir. Generator LLVM IR mengubah SIL menjadi representasi LLVM menengah. Itu diteruskan ke backend untuk optimasi lebih lanjut dan pembuatan kode mesin.


Contoh implementasi


Untuk menghasilkan tampilan perantara, Anda perlu berinteraksi dengan perpustakaan LLVM. Itu ditulis dalam C ++, tetapi karena Anda tidak dapat memanggilnya dari Swift, Anda harus menggunakan antarmuka-C. Tapi Anda tidak bisa hanya beralih ke C-library.


Itu perlu dibungkus dalam sebuah modul. Buat itu mudah. Ini instruksi yang bagus. Untuk LLVM, pembungkus seperti itu sudah ada di domain publik, jadi lebih mudah untuk mengambilnya.


Pembungkus Swift di atas pustaka LLVM-C diposting pada akun yang sama, tetapi itu tidak akan digunakan dalam artikel ini.


Untuk menghasilkan tampilan perantara, kelas LLVMIRGen yang sesuai telah dibuat. Di initializer, dibutuhkan AST yang dibuat oleh parser:


import cllvm class LLVMIRGen { private let ast: ASTNode init(ast: ASTNode) { self.ast = ast } 

Metode printTo (_, dump) memulai pembuatan dan menyimpannya dalam bentuk yang dapat dibaca ke file. Parameter dump digunakan untuk secara opsional menampilkan informasi yang sama ke konsol:


 func printTo(_ fileName: String, dump: Bool) { 

Pertama, Anda perlu membuat modul. Penciptaannya, serta penciptaan entitas lain, dimasukkan ke dalam metode terpisah dan akan dibahas di bawah ini. Karena ini adalah C, Anda perlu mengelola memori secara manual. Untuk menghapus modul dari memori, gunakan fungsi LLVMDisposeModule () :


 let module = generateModule() defer { LLVMDisposeModule(module) } 

Nama semua fungsi dan tipe LLVM dimulai dengan awalan yang sesuai. Misalnya, pointer ke modul adalah tipe LLVMModuleRef , dan untuk builder adalah tipe LLVMBuilderRef . Builder adalah kelas pembantu (setelah semua, di bawah antarmuka-C yang tidak nyaman, kelas dan metode biasa disembunyikan), yang membantu menghasilkan IR:


 let builder = generateBuilder() defer { LLVMDisposeBuilder(builder) } 

Output nomor dari kurung ke konsol akan dilakukan menggunakan fungsi put standar. Untuk menghubunginya, Anda harus menyatakannya. Ini terjadi dalam metode generateExternalPutsFunction . Modul ini diteruskan karena deklarasi perlu ditambahkan padanya. Konstanta putsFunction akan menyimpan pointer ke suatu fungsi sehingga dapat diakses:


 let putsFunction = generateExternalPutsFunction(module: module) 

Kompiler Swift menciptakan fungsi utama pada tahap SIL. Karena brace compiler tidak memiliki representasi perantara, fungsi akan segera dihasilkan di LLVM IR.


Untuk melakukan ini, gunakan metode generateMainFunction (builder, module, mainInternalGenerator) . Fungsi utama tidak akan dipanggil. Karena itu, Anda tidak perlu menyimpan pointer ke sana:


 generateMainFunction(builder: builder, module: module) { // ... } 

Parameter terakhir dari metode ini adalah penutupan, di mana AST dikonversi ke LLVM IR yang sesuai. Untuk ini, metode terpisah handleAST (_, putsFunction, builder) telah dibuat :


 generateMainFunction(builder: builder, module: module) { handleAST(ast, putsFunction: putsFunction, builder: builder) } 

Di akhir metode, representasi perantara yang dihasilkan adalah output ke konsol dan disimpan ke file:


 if dump { LLVMDumpModule(module) } LLVMPrintModuleToFile(module, fileName, nil) 

Sekarang, lebih lanjut tentang metode. Modul ini dihasilkan dengan memanggil fungsi LLVMModuleCreateWithName () dengan nama yang diinginkan:


 private func generateModule() -> LLVMModuleRef { let moduleName = "BraceCompiller" return LLVMModuleCreateWithName(moduleName) } 

Pembangun dibuat lebih mudah. Dia tidak membutuhkan parameter sama sekali:


 private func generateBuilder() -> LLVMBuilderRef { return LLVMCreateBuilder() } 

Untuk mendeklarasikan suatu fungsi, Anda harus terlebih dahulu mengalokasikan memori untuk parameternya dan menyimpan pointer ke Int8 di dalamnya. Selanjutnya, panggil LLVMFunctionType () untuk membuat tipe fungsi, meneruskannya dengan tipe nilai balik, array tipe argumen (C-array adalah pointer ke urutan nilai yang sesuai) dan jumlahnya. LLVMAddFunction () menambahkan fungsi put ke modul dan mengembalikan pointer ke sana:


 private func generateExternalPutsFunction(module: LLVMModuleRef) -> LLVMValueRef { var putParamTypes = UnsafeMutablePointer<LLVMTypeRef?>.allocate(capacity: 1) defer { putParamTypes.deallocate() } putParamTypes[0] = LLVMPointerType(LLVMInt8Type(), 0) let putFunctionType = LLVMFunctionType(LLVMInt32Type(), putParamTypes, 1, 0) return LLVMAddFunction(module, "puts", putFunctionType) } 

main dibuat dengan cara yang serupa, tetapi sebuah body ditambahkan padanya. Seperti SIL, itu terdiri dari blok dasar. Untuk melakukan ini, panggil metode LLVMAppendBasicBlock () , dengan meneruskan fungsi dan nama blok ke sana.


Sekarang pembangun ikut bermain. Dengan memanggil LLVMPositionBuilderAtEnd (), ia bergerak ke ujung blok yang masih kosong, dan di dalam penutupan mainInternalGenerator () , fungsi tubuh akan ditambahkan dengannya.


Pada akhir metode, nilai konstan 0 dikembalikan dari main . Ini adalah instruksi terakhir dalam fungsi ini:


 private func generateMainFunction(builder: LLVMBuilderRef, module: LLVMModuleRef, mainInternalGenerator: () -> Void) { let mainFunctionType = LLVMFunctionType(LLVMInt32Type(), nil, 0, 0) let mainFunction = LLVMAddFunction(module, "main", mainFunctionType) let mainEntryBlock = LLVMAppendBasicBlock(mainFunction, "entry") LLVMPositionBuilderAtEnd(builder, mainEntryBlock) mainInternalGenerator() let zero = LLVMConstInt(LLVMInt32Type(), 0, 0) LLVMBuildRet(builder, zero) } 

Menghasilkan IR menurut AST dalam kompiler tanda kurung sangat sederhana, karena satu-satunya tindakan yang dapat dilakukan dalam "bahasa pemrograman" ini adalah dengan mengeluarkan satu nomor ke konsol. Anda harus melalui seluruh pohon secara rekursif, dan ketika Anda menemukan nomor node, tambahkan panggilan ke fungsi put . Jika simpul ini tidak ada, fungsi utama hanya akan berisi pengembalian nilai nol:


 private func handleAST(_ ast: ASTNode, putsFunction: LLVMValueRef, builder: LLVMBuilderRef) { switch ast { case let .brace(childNode): guard let childNode = childNode else { break } handleAST(childNode, putsFunction: putsFunction, builder: builder) case let .number(value): generatePrint(value: value, putsFunction: putsFunction, builder: builder) } } 

Panggilan panggilan dibuat menggunakan fungsi LLVMBuildCall () . Perlu melewati pembangun, penunjuk ke fungsi, argumen dan nomor mereka. LLVMBuildGlobalStringPtr () menciptakan konstanta global untuk menyimpan string. Dia akan menjadi satu-satunya argumen:


 private func generatePrint(value: Int, putsFunction: LLVMValueRef, builder: LLVMBuilderRef) { let putArgumentsSize = MemoryLayout<LLVMValueRef?>.size let putArguments = UnsafeMutablePointer<LLVMValueRef?>.allocate(capacity: 1) defer { putArguments.deallocate() } putArguments[0] = LLVMBuildGlobalStringPtr(builder, "\(value)", "print") _ = LLVMBuildCall(builder, putsFunction, putArguments, 1, "put") } 

Untuk memulai pembuatan IR LLVM, Anda harus membuat turunan dari kelas LLVMIRGen dan memanggil metode printTo (_, dump) :


 let llvmIRGen = LLVMIRGen(ast: ast) llvmIRGen.printTo(outputFilePath, dump: false) 

Karena sekarang kompiler tanda kurung benar-benar siap, Anda dapat memulainya dari baris perintah. Untuk melakukan ini, Anda harus mengumpulkan ( instruksi ) dan menjalankan perintah:


 build/debug/BraceCompiler Example/input.b Example/output.ll 

Hasilnya adalah representasi perantara ini:


 ; ModuleID = 'BraceCompiller' source_filename = "BraceCompiller" @print = private unnamed_addr constant [5 x i8] c"5678\00" declare i32 @puts(i8*) define i32 @main() { entry: %put = call i32 @puts(i8* getelementptr inbounds ([5 x i8], [5 x i8]* @print, i32 0, i32 0)) ret i32 0 } 

Menggunakan LLVM IR Swift Generator


LLVM IR juga memiliki bentuk SSA, tetapi tingkat rendah dan lebih mirip assembler. Deskripsi instruksi dapat ditemukan dalam dokumentasi .


Pengidentifikasi global dimulai dengan b> @ </ b , lokal dengan % . Pada contoh di atas, string "5678 \ 00" disimpan dalam konstanta global b> @print </ b , dan kemudian digunakan untuk memanggil fungsi b> @puts </ b menggunakan pernyataan panggilan .


Untuk melihat sesuatu yang menarik dalam LLVM IR yang dihasilkan oleh kompiler Swift, Anda perlu menyulitkan kode sedikit lagi. Misalnya, tambahkan tambahan:


 let x = 16 let y = x + 7 

Bendera -emit-ir bertanggung jawab untuk menghasilkan LLVM IR:


 swiftc -emit-ir main.swift 

Hasil dari perintah:


 ; ModuleID = '-' source_filename = "-" target datalayout = "em:o-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-apple-macosx10.14.0" %TSi = type <{ i64 }> @"$S4main1xSivp" = hidden global %TSi zeroinitializer, align 8 @"$S4main1ySivp" = hidden global %TSi zeroinitializer, align 8 @__swift_reflection_version = linkonce_odr hidden constant i16 3 @llvm.used = appending global [1 x i8*] [i8* bitcast (i16* @__swift_reflection_version to i8*)], section "llvm.metadata", align 8 define i32 @main(i32, i8**) #0 { entry: %2 = bitcast i8** %1 to i8* store i64 16, i64* getelementptr inbounds (%TSi, %TSi* @"$S4main1xSivp", i32 0, i32 0), align 8 %3 = load i64, i64* getelementptr inbounds (%TSi, %TSi* @"$S4main1xSivp", i32 0, i32 0), align 8 %4 = call { i64, i1 } @llvm.sadd.with.overflow.i64(i64 %3, i64 7) %5 = extractvalue { i64, i1 } %4, 0 %6 = extractvalue { i64, i1 } %4, 1 br i1 %6, label %8, label %7 ; <label>:7: ; preds = %entry store i64 %5, i64* getelementptr inbounds (%TSi, %TSi* @"$S4main1ySivp", i32 0, i32 0), align 8 ret i32 0 ; <label>:8: ; preds = %entry call void @llvm.trap() unreachable } ; Function Attrs: nounwind readnone speculatable declare { i64, i1 } @llvm.sadd.with.overflow.i64(i64, i64) #1 ; Function Attrs: noreturn nounwind declare void @llvm.trap() #2 attributes #0 = { "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" } attributes #1 = { nounwind readnone speculatable } attributes #2 = { noreturn nounwind } !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} !llvm.linker.options = !{!8, !9, !10} !llvm.asan.globals = !{!11} !0 = !{i32 1, !"Objective-C Version", i32 2} !1 = !{i32 1, !"Objective-C Image Info Version", i32 0} !2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"} !3 = !{i32 4, !"Objective-C Garbage Collection", i32 1536} !4 = !{i32 1, !"Objective-C Class Properties", i32 64} !5 = !{i32 1, !"wchar_size", i32 4} !6 = !{i32 7, !"PIC Level", i32 2} !7 = !{i32 1, !"Swift Version", i32 6} !8 = !{!"-lswiftSwiftOnoneSupport"} !9 = !{!"-lswiftCore"} !10 = !{!"-lobjc"} !11 = !{[1 x i8*]* @llvm.used, null, null, i1 false, i1 true} 

Representasi perantara dari kompiler nyata sedikit lebih rumit. Ada operasi tambahan di dalamnya, tetapi instruksi yang diperlukan tidak sulit ditemukan. Di sini konstanta global x dan y dideklarasikan dengan nama yang cacat:


 @"$S4main1xSivp" = hidden global %TSi zeroinitializer, align 8 @"$S4main1ySivp" = hidden global %TSi zeroinitializer, align 8 

Di sini dimulai definisi fungsi utama :


 define i32 @main(i32, i8**) #0 { 

Pertama, ia menyimpan nilai 16 dalam konstanta x :


 store i64 16, i64* getelementptr inbounds (%TSi, %TSi* @"$S4main1xSivp", i32 0, i32 0), align 8 

Kemudian dimuat ke register 3 dan digunakan untuk memanggil tambahan bersama dengan literal 7:


 %3 = load i64, i64* getelementptr inbounds (%TSi, %TSi* @"$S4main1xSivp", i32 0, i32 0), align 8 %4 = call { i64, i1 } @llvm.sadd.with.overflow.i64(i64 %3, i64 7) 

Overflow memeriksa penambahan struktur pengembalian. Nilai pertama adalah hasil dari penambahan, dan yang kedua adalah bendera yang menunjukkan apakah ada overflow.


Struktur dalam LLVM lebih seperti tuple di Swift. Tidak memiliki nama untuk bidang, dan Anda perlu mendapatkan nilai menggunakan pernyataan extractvalue . Parameter pertama menunjukkan jenis bidang dalam struktur, yang kedua - struktur itu sendiri, dan setelah koma - indeks bidang, nilai yang perlu ditarik:


 %5 = extractvalue { i64, i1 } %4, 0 %6 = extractvalue { i64, i1 } %4, 1 

Sekarang tanda overflow disimpan dalam register keenam. Nilai ini diverifikasi menggunakan instruksi cabang. Jika ada overflow, akan ada transisi ke blok label8 , jika tidak, ke label7 :


 br i1 %6, label %8, label %7 

Dalam yang pertama ini, eksekusi program terganggu oleh panggilan ke perangkap () . Dalam yang kedua, hasil penambahan disimpan dalam konstanta y , dan 0 dikembalikan dari fungsi utama :


 ; <label>:7: ; preds = %entry store i64 %5, i64* getelementptr inbounds (%TSi, %TSi* @"$S4main1ySivp", i32 0, i32 0), align 8 ret i32 0 ; <label>:8: ; preds = %entry call void @llvm.trap() unreachable 

Pembuatan kode perakitan


Kompiler Swift juga dapat menampilkan kode rakitan. Untuk melakukan ini, lewati flag -emit-assembly :


 swiftc -emit-assembly main.swift 

Hasil dari perintah:


  .section __TEXT,__text,regular,pure_instructions .build_version macos, 10, 14 .globl _main .p2align 4, 0x90 _main: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 movq %rsp, %rbp .cfi_def_cfa_register %rbp movq $16, _$S4main1xSivp(%rip) movq _$S4main1xSivp(%rip), %rax addq $7, %rax seto %cl movl %edi, -4(%rbp) movq %rsi, -16(%rbp) movq %rax, -24(%rbp) movb %cl, -25(%rbp) jo LBB0_2 xorl %eax, %eax movq -24(%rbp), %rcx movq %rcx, _$S4main1ySivp(%rip) popq %rbp retq LBB0_2: ud2 .cfi_endproc .private_extern _$S4main1xSivp .globl _$S4main1xSivp .zerofill __DATA,__common,_$S4main1xSivp,8,3 .private_extern _$S4main1ySivp .globl _$S4main1ySivp .zerofill __DATA,__common,_$S4main1ySivp,8,3 .private_extern ___swift_reflection_version .section __TEXT,__const .globl ___swift_reflection_version .weak_definition ___swift_reflection_version .p2align 1 ___swift_reflection_version: .short 3 .no_dead_strip ___swift_reflection_version .linker_option "-lswiftSwiftOnoneSupport" .linker_option "-lswiftCore" .linker_option "-lobjc" .section __DATA,__objc_imageinfo,regular,no_dead_strip L_OBJC_IMAGE_INFO: .long 0 .long 1600 .subsections_via_symbols 

Setelah memahami kode representasi perantara yang dijelaskan di atas, Anda dapat menemukan instruksi assembler yang dihasilkannya. Di sini tersimpan 16 ke konstanta dan memuatnya ke register% rax :


 movq $16, _$S4main1xSivp(%rip) movq _$S4main1xSivp(%rip), %rax 

Berikut adalah penambahan 7 dan nilai konstanta. Hasil penambahan ditempatkan di register% rax :


 addq $7, %rax 

Dan ini adalah bagaimana memuat hasilnya ke dalam konstanta y terlihat seperti:


 movq %rax, -24(%rbp) movq -24(%rbp), %rcx movq %rcx, _$S4main1ySivp(%rip) 

Kode Sumber:



Kesimpulan


Swift adalah kompiler yang terstruktur dengan baik, dan tidak sulit untuk mengetahui arsitektur umumnya. Saya juga terkejut bahwa menggunakan LLVM, Anda dapat dengan mudah menulis bahasa pemrograman Anda sendiri. Tentu saja, kompiler tanda kurung sangat primitif, tetapi implementasi Kaleidoscope juga sangat dimengerti. Saya sarankan membaca setidaknya tiga bab pertama dari tutorial.


Terima kasih kepada semua orang yang membaca. Saya akan terus mempelajari kompiler Swift dan mungkin menulis tentang apa yang terjadi. Topik apa yang berhubungan dengannya yang Anda minati?


Tautan yang bermanfaat:


Source: https://habr.com/ru/post/id440078/


All Articles