
هذا هو الجزء الأخير من تقييمي للمترجم السريع. سأريكم كيفية توليد LLVM IR من AST وما هي الواجهة الحقيقية. إذا لم تكن قد قرأت الأجزاء السابقة ، فاتبع الروابط:
LLVM IR Gen
بالنسبة للواجهة الأمامية ، هذه هي الخطوة الأخيرة. يقوم مولد LLVM IR بتحويل SIL إلى تمثيل LLVM وسيط. يتم تمريره إلى الواجهة الخلفية لمزيد من التحسين وإنشاء رمز الجهاز.
مثال التنفيذ
لإنشاء عرض وسيط ، تحتاج إلى التفاعل مع مكتبة LLVM. إنه مكتوب بلغة C ++ ، ولكن بما أنه لا يمكنك الاتصال به من Swift ، فيجب عليك استخدام C-interface. لكن لا يمكنك فقط الانتقال إلى مكتبة C.
يجب أن تكون ملفوفة في وحدة نمطية. اجعلها سهلة هنا تعليمات جيدة. بالنسبة لـ LLVM ، يوجد مثل هذا المجمع بالفعل في المجال العام ، لذلك من الأسهل تناوله.
يتم نشر مجمّع Swift على مكتبة LLVM-C على نفس الحساب ، لكن لن يتم استخدامه في هذه المقالة.
لإنشاء عرض وسيط ، تم إنشاء فئة LLVMIRGen المقابلة. في أداة التهيئة ، يستغرق الأمر AST الذي تم إنشاؤه بواسطة المحلل اللغوي:
import cllvm class LLVMIRGen { private let ast: ASTNode init(ast: ASTNode) { self.ast = ast }
تقوم طريقة printTo (_ ، dump) ببدء الإنشاء وحفظه في نموذج قابل للقراءة إلى ملف. يتم استخدام معلمة التفريغ لإخراج المعلومات نفسها اختياريًا إلى وحدة التحكم:
func printTo(_ fileName: String, dump: Bool) {
تحتاج أولاً إلى إنشاء وحدة نمطية. يتم إنشاء هذا إنشاء ، وكذلك إنشاء كيانات أخرى ، في أساليب منفصلة وسيتم مناقشتها أدناه. نظرًا لأن هذا هو C ، فأنت بحاجة إلى إدارة الذاكرة يدويًا. لإزالة وحدة نمطية من الذاكرة ، استخدم الدالة LLVMDisposeModule () :
let module = generateModule() defer { LLVMDisposeModule(module) }
تبدأ أسماء جميع وظائف LLVM وأنواعها بالبادئة المقابلة. على سبيل المثال ، مؤشر إلى وحدة نمطية من النوع LLVMModuleRef ، ومنشئ إلى النوع LLVMBuilderRef . الباني عبارة عن فئة مساعدة (بعد كل شيء ، وتحت واجهة C غير مريحة ، يتم إخفاء الفئات والأساليب العادية) ، مما يساعد على توليد IR:
let builder = generateBuilder() defer { LLVMDisposeBuilder(builder) }
سيتم تنفيذ إخراج الرقم من الأقواس إلى وحدة التحكم باستخدام وظيفة الوضع القياسي. من أجل الاتصال بها ، تحتاج إلى إعلان ذلك. يحدث هذا في أسلوب createExternalPutsFunction. يتم تمرير الوحدة النمطية إليها لأنه يجب إضافة الإعلان إليها. يقوم ثابت putsFunction بتخزين مؤشر إلى دالة بحيث يمكن الوصول إليها:
let putsFunction = generateExternalPutsFunction(module: module)
إنشاء مترجم سويفت الوظيفة الرئيسية في مرحلة سيل. نظرًا لأن المحول البرمجي للثني لا يحتوي على مثل هذا التمثيل الوسيط ، سيتم إنشاء الوظيفة فورًا في LLVM IR.
للقيام بذلك ، استخدم أسلوب createMainFunction (منشئ ، وحدة ، mainInternalGenerator) . لن يتم استدعاء الوظيفة الرئيسية . لذلك ، لا تحتاج إلى حفظ مؤشر إليه:
generateMainFunction(builder: builder, module: module) { // ... }
المعلمة الأخيرة للأسلوب هي الإغلاق ، حيث يتم تحويل AST إلى IR LLVM المطابق. لهذا الغرض ، تم إنشاء مؤشر أسلوب منفصل (_ ، putsFunction ، باني) :
generateMainFunction(builder: builder, module: module) { handleAST(ast, putsFunction: putsFunction, builder: builder) }
في نهاية الطريقة ، يتم إخراج التمثيل الوسيط الناتج إلى وحدة التحكم وحفظه في الملف:
if dump { LLVMDumpModule(module) } LLVMPrintModuleToFile(module, fileName, nil)
الآن المزيد عن الأساليب. يتم إنشاء الوحدة النمطية عن طريق استدعاء دالة LLVMModuleCreateWithName () بالاسم المطلوب:
private func generateModule() -> LLVMModuleRef { let moduleName = "BraceCompiller" return LLVMModuleCreateWithName(moduleName) }
يتم إنشاء البناء أسهل. إنه لا يحتاج إلى معلمات على الإطلاق:
private func generateBuilder() -> LLVMBuilderRef { return LLVMCreateBuilder() }
لإعلان الوظيفة ، تحتاج أولاً إلى تخصيص ذاكرة للمعلمة الخاصة بها وحفظ مؤشر إلى Int8 فيها. بعد ذلك ، اتصل LLVMFunctionType () لإنشاء نوع الوظيفة ، لتمريرها نوع القيمة المرجعة ، صفيف أنواع الوسيطة (C-array هو مؤشر إلى تسلسل القيم المقابل) ورقمها. يضيف LLVMAddFunction () الدالة puts إلى الوحدة النمطية وتقوم بإرجاع مؤشر إليها:
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) }
يتم إنشاء الرئيسي بطريقة مماثلة ، ولكن يتم إضافة الجسم إليها. مثل SIL ، يتكون من كتل الأساس. للقيام بذلك ، استدعاء الأسلوب LLVMAppendBasicBlock () لتمرير الدالة واسم الكتلة إليه.
الآن باني يأتي في اللعب. عن طريق استدعاء LLVMPositionBuilderAtEnd () ، ينتقل إلى نهاية الكتلة الفارغة ، وداخل الإغلاق mainInternalGenerator () ، ستتم إضافة جسم الوظيفة به.
في نهاية هذه الطريقة ، يتم إرجاع القيمة الثابتة 0 من main . هذه هي التعليمة الأخيرة في هذه الوظيفة:
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) }
إن إنشاء IR وفقًا لـ AST في برنامج التحويل البرمجي للأقواس بسيط للغاية ، لأن الإجراء الوحيد الذي يمكن القيام به في "لغة البرمجة" هذه هو إخراج رقم واحد إلى وحدة التحكم. تحتاج إلى الانتقال بشكل متكرر عبر الشجرة بأكملها ، وعندما تجد عقدة الأرقام ، أضف مكالمة إلى وظيفة puts . إذا كانت هذه العقدة غير موجودة ، فستحتوي الوظيفة الرئيسية فقط على قيمة صفرية:
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) } }
يتم إنشاء استدعاء puts باستخدام دالة LLVMBuildCall () . يحتاج إلى تمرير منشئ ومؤشر إلى دالة وسيطات وعددها. LLVMBuildGlobalStringPtr () بإنشاء ثابت عمومي للاحتفاظ سلسلة. ستكون الحجة الوحيدة:
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") }
لبدء إنشاء LLVM IR ، تحتاج إلى إنشاء مثيل لفئة LLVMIRGen واستدعاء طريقة printTo (_ ، تفريغ) :
let llvmIRGen = LLVMIRGen(ast: ast) llvmIRGen.printTo(outputFilePath, dump: false)
نظرًا لأن برنامج التحويل البرمجي للأقواس جاهز تمامًا ، يمكنك تشغيله من سطر الأوامر. للقيام بذلك ، تحتاج إلى جمعها ( تعليمات ) وتنفيذ الأمر:
build/debug/BraceCompiler Example/input.b Example/output.ll
والنتيجة هي هذا التمثيل الوسيط:
; 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 }
باستخدام LLVM IR سويفت مولد
لدى LLVM IR أيضًا نموذج SSA ، لكنه منخفض المستوى ويشبه المجمع. يمكن العثور على وصف للتعليمات في الوثائق .
تبدأ المعرفات العامة بـ b> @ </ b ، والمحلية بـ ٪ . في المثال أعلاه ، يتم تخزين السلسلة "5678 \ 00" في الثابت العمومي b>print </ b ، ثم يتم استخدامها لاستدعاء الدالة b>puts </ b باستخدام عبارة الاستدعاء .
من أجل رؤية شيء مثير للاهتمام في LLVM IR التي تم إنشاؤها بواسطة برنامج التحويل البرمجي Swift ، تحتاج إلى تعقيد الرمز أكثر قليلاً. على سبيل المثال ، أضف الإضافة:
let x = 16 let y = x + 7
علامة الأشعة تحت الحمراء هي المسؤولة عن توليد LLVM IR:
swiftc -emit-ir main.swift
نتيجة الأمر:
; 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}
التمثيل المتوسط للمترجم الحقيقي أكثر تعقيدًا قليلاً. هناك عمليات إضافية في ذلك ، ولكن ليس من الصعب العثور على التعليمات اللازمة. يتم تعريف الثوابت العالمية x و y بأسماء تالفة:
@"$S4main1xSivp" = hidden global %TSi zeroinitializer, align 8 @"$S4main1ySivp" = hidden global %TSi zeroinitializer, align 8
هنا يبدأ تعريف الوظيفة الرئيسية :
define i32 @main(i32, i8**) #0 {
أولاً ، يتم تخزين القيمة 16 به في الثابت x :
store i64 16, i64* getelementptr inbounds (%TSi, %TSi* @"$S4main1xSivp", i32 0, i32 0), align 8
بعد ذلك ، يتم تحميله في السجل 3 ويستخدم للاتصال بالإضافة إلى الحرفي 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)
التحقق من التدفق الزائد يعود هيكل. قيمته الأولى هي نتيجة الإضافة ، والثاني هو إشارة تشير إلى ما إذا كان هناك تجاوز سعة.
الهيكل في LLVM يشبه tuple في Swift. ليس لها أسماء للحقول ، وستحتاج إلى الحصول على القيمة باستخدام عبارة extractvalue . تشير المعلمة الأولى إلى أنواع الحقول في البنية ، والثانية - البنية نفسها ، وبعد الفاصلة - فهرس الحقل ، التي يجب سحب قيمتها:
%5 = extractvalue { i64, i1 } %4, 0 %6 = extractvalue { i64, i1 } %4, 1
الآن يتم تخزين علامة الفائض في السجل السادس. يتم التحقق من هذه القيمة باستخدام تعليمة الفرع. إذا كان هناك تجاوز سعة ، فسيكون هناك انتقال إلى كتلة label8 ، إن لم يكن ، إلى label7 :
br i1 %6, label %8, label %7
في أول هذه العناصر ، يتم مقاطعة تنفيذ البرنامج عن طريق استدعاء trap () . في الثانية ، يتم تخزين نتيجة الإضافة في الثابت y ، ويتم إرجاع 0 من الوظيفة الرئيسية :
; <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
توليد رمز التجميع
يمكن لبرنامج التحويل البرمجي لـ Swift أيضًا عرض رمز التجميع. للقيام بذلك ، قم بتمرير علامة التجميع :
swiftc -emit-assembly main.swift
نتيجة الأمر:
.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
بعد فهم رمز التمثيل الوسيط الموضح أعلاه ، يمكنك العثور على إرشادات المجمّع التي تنشئها. هنا يتم تخزين 16 في ثابت وتحميله في سجل٪ rax :
movq $16, _$S4main1xSivp(%rip) movq _$S4main1xSivp(%rip), %rax
هنا هو الجمع 7 وقيمة الثابت. يتم وضع نتيجة الإضافة في سجل ٪ rax :
addq $7, %rax
وهذه هي الطريقة التي يبدو بها تحميل النتيجة في ثابت y :
movq %rax, -24(%rbp) movq -24(%rbp), %rcx movq %rcx, _$S4main1ySivp(%rip)
كود المصدر:
الخاتمة
Swift هو مترجم جيد التنظيم ، ولم يكن من الصعب معرفة بنيته العامة. لقد فوجئت أيضًا باستخدام LLVM ، يمكنك بسهولة كتابة لغتك البرمجية الخاصة. بالطبع ، يعد مجمع الأقواس بدائيًا للغاية ، ولكن تطبيق Kaleidoscope مفهوم أيضًا حقًا. أوصي بقراءة ما لا يقل عن الفصول الثلاثة الأولى من البرنامج التعليمي.
شكرا لكل من يقرأ سأستمر في دراسة برنامج التحويل البرمجي لـ Swift وربما أكتب عما جاء منه. ما هي المواضيع المتعلقة به التي سوف تكون مهتمة في؟
روابط مفيدة: