Dispositif de compilateur Swift. Partie 4


Ceci est la dernière partie de mon examen du compilateur Swift. Je vais vous montrer comment générer LLVM IR à partir d'AST et ce qu'est un vrai frontend. Si vous n'avez pas lu les parties précédentes, suivez les liens:



LLVM IR Gen


Pour le frontend, c'est la dernière étape. Le générateur IR LLVM convertit le SIL en une représentation LLVM intermédiaire. Il est transmis au backend pour une optimisation et une génération de code machine supplémentaires.


Exemple d'implémentation


Pour générer une vue intermédiaire, vous devez interagir avec la bibliothèque LLVM. Il est écrit en C ++, mais comme vous ne pouvez pas l'appeler depuis Swift, vous devez utiliser l'interface C. Mais vous ne pouvez pas simplement vous tourner vers la bibliothèque C.


Il doit être enveloppé dans un module. Rendez-le facile. Voici une bonne instruction. Pour LLVM, un tel wrapper existe déjà dans le domaine public, il est donc plus facile de le prendre.


Le wrapper Swift sur la bibliothèque LLVM-C est publié sur le même compte, mais il ne sera pas utilisé dans cet article.


Pour générer une vue intermédiaire, la classe LLVMIRGen correspondante a été créée. Dans l'initialiseur, il prend l'AST créé par l'analyseur:


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

La méthode printTo (_, dump) démarre la génération et l'enregistre sous une forme lisible dans un fichier. Le paramètre de vidage est utilisé pour éventuellement afficher les mêmes informations sur la console:


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

Vous devez d'abord créer un module. Sa création, ainsi que la création d'autres entités, font l'objet de méthodes distinctes et seront discutées ci-dessous. Comme il s'agit de C, vous devez gérer la mémoire manuellement. Pour supprimer un module de la mémoire, utilisez la fonction LLVMDisposeModule () :


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

Les noms de toutes les fonctions et types LLVM commencent par le préfixe correspondant. Par exemple, un pointeur vers un module est de type LLVMModuleRef et vers un générateur est de type LLVMBuilderRef . Le générateur est une classe d'assistance (après tout, sous l'interface C peu pratique, les classes et méthodes ordinaires sont cachées), ce qui aide à générer des IR:


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

La sortie du nombre de parenthèses à la console sera effectuée en utilisant la fonction de vente standard. Pour la contacter, vous devez le déclarer. Cela se produit dans la méthode generateExternalPutsFunction . Le module lui est transmis car la déclaration doit y être ajoutée. La constante putFunction stockera un pointeur sur une fonction afin qu'elle soit accessible:


 let putsFunction = generateExternalPutsFunction(module: module) 

Le compilateur Swift a créé la fonction principale dans l'étape SIL. Étant donné que le compilateur d'accolades n'a pas une telle représentation intermédiaire, la fonction sera générée immédiatement dans LLVM IR.


Pour ce faire, utilisez la méthode generateMainFunction (générateur, module, mainInternalGenerator) . La fonction principale ne sera pas appelée. Par conséquent, vous n'avez pas besoin d'enregistrer un pointeur dessus:


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

Le dernier paramètre de la méthode est la fermeture, à l'intérieur de laquelle l'AST est converti en IR LLVM correspondant. Pour cela, une méthode distincte handleAST (_, putFunction, builder) a été créée :


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

À la fin de la méthode, la représentation intermédiaire résultante est sortie dans la console et enregistrée dans le fichier:


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

Maintenant, plus sur les méthodes. Le module est généré en appelant la fonction LLVMModuleCreateWithName () avec le nom souhaité:


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

Le générateur est créé encore plus facilement. Il n'a pas du tout besoin de paramètres:


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

Pour déclarer une fonction, vous devez d'abord allouer de la mémoire à son paramètre et y enregistrer un pointeur vers Int8. Ensuite, appelez LLVMFunctionType () pour créer le type de la fonction, en lui passant le type de la valeur de retour, un tableau de types d'arguments (le tableau C est un pointeur vers la séquence de valeurs correspondante) et leur nombre. LLVMAddFunction () ajoute la fonction put au module et lui renvoie un pointeur:


 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 est créée de manière similaire, mais un corps y est ajouté. Comme SIL, il se compose de blocs de base. Pour ce faire, appelez la méthode LLVMAppendBasicBlock () en lui passant la fonction et le nom du bloc.


Maintenant, le constructeur entre en jeu. En appelant LLVMPositionBuilderAtEnd (), il se déplace à la fin du bloc encore vide, et à l'intérieur de la fermeture mainInternalGenerator () , le corps de la fonction sera ajouté avec lui.


A la fin de la méthode, la valeur constante 0 est renvoyée par main . C'est la dernière instruction de cette fonction:


 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) } 

La génération d'IR selon AST dans le compilateur de parenthèses est très simple, car la seule action qui peut être effectuée dans ce "langage de programmation" est de sortir un seul numéro sur la console. Vous devez parcourir récursivement l'ensemble de l'arborescence et, lorsque vous trouvez le nœud numérique , ajoutez un appel à la fonction put . Si ce nœud n'est pas présent, la fonction principale ne contiendra qu'un retour de valeur nulle:


 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) } } 

L'appel put est généré à l'aide de la fonction LLVMBuildCall () . Il doit passer un constructeur, un pointeur sur une fonction, des arguments et leur nombre. LLVMBuildGlobalStringPtr () crée une constante globale pour contenir une chaîne. Elle sera le seul argument:


 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") } 

Pour démarrer la génération LLVM IR, vous devez créer une instance de la classe LLVMIRGen et appeler la méthode printTo (_, dump) :


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

Puisque maintenant le compilateur de parenthèses est complètement prêt, vous pouvez le démarrer à partir de la ligne de commande. Pour ce faire, vous devez le collecter ( instruction ) et exécuter la commande:


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

Le résultat est cette représentation intermédiaire:


 ; 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 } 

Utilisation de LLVM IR Swift Generator


LLVM IR a également une forme SSA, mais il est de bas niveau et ressemble plus à un assembleur. Une description des instructions se trouve dans la documentation .


Les identificateurs globaux commencent par b> @ </ b , locaux par % . Dans l'exemple ci-dessus, la chaîne "5678 \ 00" est stockée dans la constante globale b> @print </ b , puis utilisée pour appeler la fonction b> @puts </ b à l'aide de l'instruction call .


Afin de voir quelque chose d'intéressant dans l'IR LLVM généré par le compilateur Swift, vous devez compliquer un peu plus le code. Par exemple, ajoutez un ajout:


 let x = 16 let y = x + 7 

L' indicateur -emit-ir est responsable de la génération de LLVM IR:


 swiftc -emit-ir main.swift 

Le résultat de la commande:


 ; 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} 

La représentation intermédiaire d'un vrai compilateur est un peu plus compliquée. Il contient des opérations supplémentaires, mais les instructions nécessaires ne sont pas difficiles à trouver. Ici, les constantes globales x et y sont déclarées avec des noms mal formés:


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

Ici commence la définition de la fonction principale :


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

Tout d'abord, la valeur 16 y est stockée dans la constante x :


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

Ensuite, il est chargé dans le registre 3 et utilisé pour appeler l'addition avec le littéral 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) 

L'ajout de vérification de débordement renvoie la structure Sa première valeur est le résultat de l'addition, et la seconde est un indicateur qui indique s'il y a eu un débordement.


La structure dans LLVM ressemble plus à un tuple dans Swift. Il n'a pas de nom pour les champs et vous devez obtenir la valeur à l'aide de l' instruction extractvalue . Son premier paramètre indique les types de champs dans la structure, le second - la structure elle-même et après la virgule - l'index du champ, dont la valeur doit être extraite:


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

Le signe de débordement est maintenant stocké dans le sixième registre. Cette valeur est vérifiée à l'aide de l'instruction de branchement. S'il y a eu un débordement, il ira au bloc label8 , sinon, à label7 :


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

Dans le premier, l'exécution du programme est interrompue par un appel à trap () . Dans le second, le résultat de l'addition est stocké dans la constante y , et 0 est renvoyé par la fonction principale :


 ; <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 

Génération de code d'assemblage


Le compilateur Swift peut également afficher le code assembleur. Pour ce faire, passez l' indicateur -emit-assembly :


 swiftc -emit-assembly main.swift 

Le résultat de la commande:


  .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 

Ayant compris le code de la représentation intermédiaire décrite ci-dessus, vous pouvez trouver les instructions d'assembleur qu'il génère. Voici stocker 16 dans une constante et le charger dans le registre% rax :


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

Voici l'addition 7 et la valeur de la constante. Le résultat de l'addition est placé dans le registre% rax :


 addq $7, %rax 

Et voici à quoi ressemble le chargement du résultat dans la constante y :


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

Code source:



Conclusion


Swift est un compilateur bien structuré, et il n'a pas été difficile de comprendre son architecture générale. J'ai également été surpris qu'en utilisant LLVM, vous pouvez facilement écrire votre propre langage de programmation. Bien sûr, le compilateur de parenthèses est très primitif, mais l'implémentation de Kaleidoscope est également vraiment compréhensible. Je recommande de lire au moins les trois premiers chapitres du tutoriel.


Merci à tous ceux qui ont lu. Je vais continuer à étudier le compilateur Swift et peut-être écrire sur ce qui en est sorti. Quels sujets liés à lui vous intéresseraient?


Liens utiles:


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


All Articles