Swift编译器设备。 第4部分


这是我对Swift编译器的回顾的最后一部分。 我将向您展示如何从AST生成LLVM IR,以及真正的前端是什么。 如果您尚未阅读前面的部分,请单击以下链接:



LLVM IR Gen


对于前端,这是最后一步。 LLVM IR生成器将SIL转换为中间LLVM表示形式。 它被传递到后端,以进一步优化和生成机器代码。


实施实例


为了生成中间视图,您需要与LLVM库进行交互。 它是用C ++编写的,但是由于无法从Swift调用它,因此必须使用C接口。 但是您不能只求助于C库。


它需要包装在一个模块中。 轻松点 这是一个很好的指示。 对于LLVM,此类包装器已经存在于公共领域中,因此使用起来更容易。


LLVM-C库上的Swift包装器被发布在同一帐户上,但本文中将不再使用。


为了生成中间视图,创建了相应的LLVMIRGen类。 在初始化程序中,它采用解析器创建的AST:


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

printTo(_,dump)方法开始生成并将其以可读形式保存到文件中。 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) } 

从括号到控制台的数字输出将使用标准的puts功能进行。 为了与她联系,您需要声明它。 这发生在generateExternalPutsFunction方法中。 该模块被传递给它,因为需要向其添加声明。 putsFunction常量将存储指向函数的指针,以便可以对其进行访问:


 let putsFunction = generateExternalPutsFunction(module: module) 

Swift编译器在SIL阶段创建了主要功能。 由于大括号编译器没有这种中间表示,因此该函数将立即在LLVM IR中生成。


为此,请使用generateMainFunction方法(builder,module,mainInternalGenerator)函数不会被调用。 因此,您无需保存指向它的指针:


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

该方法的最后一个参数是闭包,闭包内部将AST转换为相应的LLVM IR。 为此, 已经创建了一个单独的方法handleAST(_,putsFunction,builder)


 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数组是指向相应值序列的指针)及其编号传递给该函数。 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) } 

main是通过类似的方式创建的,但是将主体添加到其中。 像SIL一样,它由基本模块组成。 为此,请调用LLVMAppendBasicBlock()方法,并将函数和块的名称传递给该方法。


现在,构建器开始起作用。 通过调用LLVMPositionBuilderAtEnd(),它将移到仍然为空的块的末尾,并在闭包mainInternalGenerator()内部, 将向函数体添加它。


在方法的最后,从main返回常量0,这是该函数的最后一条指令:


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

在括号编译器中根据AST生成IR的过程非常简单,因为可以用这种“编程语言”执行的唯一操作是将单个数字输出到控制台。 您需要递归遍历整个树,并且在找到数字节点时,向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(_,dump)方法:


 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 Swift Generator


LLVM IR也具有SSA格式,但是它是低级的,更像是汇编程序。 有关说明的说明可在文档中找到。


全局标识符以b> @ </ b开头,本地以开头。 在上面的示例中,字符串“ 5678 \ 00”存储在全局常量b> @print </ b中 ,然后用于使用call语句调用函数b> @puts </ b


为了在Swift编译器生成的LLVM IR中看到有趣的东西,您需要使代码复杂一些。 例如,添加以下内容:


 let x = 16 let y = x + 7 

-emit-ir标志负责生成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} 

实际编译器的中间表示有点复杂。 其中有其他操作,但是并不难找到必要的说明。 在这里,全局常量xy的名称格式错误:


 @"$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中的结构更像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编译器还可以显示汇编代码。 为此,请传递-emit-assembly标志:


 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,您可以轻松编写自己的编程语言,这也让我感到惊讶。 当然,括号编译器是非常原始的,但是万花筒的实现也确实可以理解。 我建议至少阅读本教程的前三章。


感谢所有阅读的人。 我将继续研究Swift编译器,也许还会写一些有关它的内容。 您会对与他有关的哪些主题感兴趣?


有用的链接:


Source: https://habr.com/ru/post/zh-CN440078/


All Articles