
Esta é a última parte da minha análise do compilador Swift. Vou mostrar como gerar LLVM IR a partir do AST e o que é um frontend real. Se você não leu as partes anteriores, siga os links:
LLVM IR Gen
Para o frontend, este é o passo final. O gerador LLVM IR converte o SIL em uma representação LLVM intermediária. É passado para o back-end para otimização e geração de código de máquina.
Exemplo de implementação
Para gerar uma visualização intermediária, você precisa interagir com a biblioteca LLVM. Está escrito em C ++, mas como você não pode chamá-lo a partir do Swift, é necessário usar a interface C. Mas você não pode simplesmente recorrer à biblioteca C.
Ele precisa ser embrulhado em um módulo. Facilite isso. Aqui está uma boa instrução. Para o LLVM, esse invólucro já existe no domínio público, portanto é mais fácil utilizá-lo.
O wrapper Swift sobre a biblioteca LLVM-C é postado na mesma conta, mas não será usado neste artigo.
Para gerar uma visualização intermediária, a classe LLVMIRGen correspondente foi criada. No inicializador, é necessário o AST criado pelo analisador:
import cllvm class LLVMIRGen { private let ast: ASTNode init(ast: ASTNode) { self.ast = ast }
O método printTo (_, dump) inicia a geração e a salva de forma legível em um arquivo. O parâmetro dump é usado para gerar opcionalmente as mesmas informações para o console:
func printTo(_ fileName: String, dump: Bool) {
Primeiro você precisa criar um módulo. Sua criação, bem como a criação de outras entidades, são colocadas em métodos separados e serão discutidas abaixo. Como esse é C, você precisa gerenciar a memória manualmente. Para remover um módulo da memória, use a função LLVMDisposeModule () :
let module = generateModule() defer { LLVMDisposeModule(module) }
Os nomes de todas as funções e tipos do LLVM começam com o prefixo correspondente. Por exemplo, um ponteiro para um módulo é do tipo LLVMModuleRef e para um construtor é do tipo LLVMBuilderRef . O construtor é uma classe auxiliar (afinal, na inconveniente interface C, classes e métodos comuns estão ocultos), o que ajuda a gerar o IR:
let builder = generateBuilder() defer { LLVMDisposeBuilder(builder) }
A saída do número de colchetes para o console será realizada usando a função de venda padrão. Para entrar em contato com ela, você precisa declarar. Isso acontece no método generateExternalPutsFunction . O módulo é passado para ele porque a declaração precisa ser adicionada a ele. A constante putsFunction armazenará um ponteiro em uma função para que possa ser acessada:
let putsFunction = generateExternalPutsFunction(module: module)
O compilador Swift criou a função principal no estágio SIL. Como o compilador de chaves não possui uma representação intermediária, a função será gerada imediatamente no LLVM IR.
Para fazer isso, use o método generateMainFunction (construtor, módulo, mainInternalGenerator) . A função principal não será chamada. Portanto, você não precisa salvar um ponteiro para ele:
generateMainFunction(builder: builder, module: module) { // ... }
O último parâmetro do método é o fechamento, dentro do qual o AST é convertido no IR LLVM correspondente. Para isso, um método separado handleAST (_, putsFunction, construtor) foi criado :
generateMainFunction(builder: builder, module: module) { handleAST(ast, putsFunction: putsFunction, builder: builder) }
No final do método, a representação intermediária resultante é enviada ao console e salva no arquivo:
if dump { LLVMDumpModule(module) } LLVMPrintModuleToFile(module, fileName, nil)
Agora mais sobre os métodos. O módulo é gerado chamando a função LLVMModuleCreateWithName () com o nome desejado:
private func generateModule() -> LLVMModuleRef { let moduleName = "BraceCompiller" return LLVMModuleCreateWithName(moduleName) }
O construtor é criado ainda mais fácil. Ele não precisa de parâmetros:
private func generateBuilder() -> LLVMBuilderRef { return LLVMCreateBuilder() }
Para declarar uma função, primeiro você precisa alocar memória para seu parâmetro e salvar um ponteiro no Int8. Em seguida, chame LLVMFunctionType () para criar o tipo da função, passando o tipo do valor de retorno, uma matriz de tipos de argumentos (a matriz C é um ponteiro para a sequência correspondente de valores) e seu número. LLVMAddFunction () adiciona a função puts ao módulo e retorna um ponteiro para ele:
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 é criado de maneira semelhante, mas um corpo é adicionado a ele. Como o SIL, consiste em blocos de base. Para fazer isso, chame o método LLVMAppendBasicBlock () , passando a função e o nome do bloco para ele.
Agora o construtor entra em jogo. Ao chamar LLVMPositionBuilderAtEnd (), ele se move para o final do bloco ainda vazio e, dentro do fechamento mainInternalGenerator () , o corpo da função será adicionado a ele.
No final do método, o valor constante 0. é retornado de main.Esta é a última instrução nesta função:
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) }
A geração de IR de acordo com a AST no compilador de parênteses é muito simples, pois a única ação que pode ser realizada nessa "linguagem de programação" é gerar um único número para o console. Você precisa percorrer recursivamente a árvore inteira e, quando encontrar o nó numérico , adicione uma chamada à função de put . Se este nó não estiver presente, a função principal conterá apenas um retorno de valor zero:
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) } }
A chamada de put é gerada usando a função LLVMBuildCall () . Ele precisa passar um construtor, um ponteiro para uma função, argumentos e seu número. LLVMBuildGlobalStringPtr () cria uma constante global para armazenar uma string. Ela será o único argumento:
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") }
Para iniciar a geração de IR do LLVM, você precisa criar uma instância da classe LLVMIRGen e chamar o método printTo (_, dump) :
let llvmIRGen = LLVMIRGen(ast: ast) llvmIRGen.printTo(outputFilePath, dump: false)
Como agora o compilador de parênteses está completamente pronto, você pode iniciá-lo na linha de comando. Para fazer isso, você precisa coletá-lo ( instrução ) e executar o comando:
build/debug/BraceCompiler Example/input.b Example/output.ll
O resultado é esta representação intermediária:
; 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 }
Usando o gerador Swift IR LLVM
O LLVM IR também possui um formulário SSA, mas é de baixo nível e mais como um montador. Uma descrição das instruções pode ser encontrada na documentação .
Os identificadores globais começam com b> @ </ b , local com % . No exemplo acima, a cadeia "5678 \ 00" é armazenada na constante global b> @print </ be usada para chamar a função b> @puts </ b usando a instrução call .
Para ver algo interessante no LLVM IR gerado pelo compilador Swift, você precisa complicar um pouco mais o código. Por exemplo, adicione adição:
let x = 16 let y = x + 7
O sinalizador -emit-ir é responsável por gerar o LLVM IR:
swiftc -emit-ir main.swift
O resultado do comando:
; 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}
A representação intermediária de um compilador real é um pouco mais complicada. Existem operações adicionais, mas as instruções necessárias não são difíceis de encontrar. Aqui, as constantes globais xey são declaradas com nomes malformados:
@"$S4main1xSivp" = hidden global %TSi zeroinitializer, align 8 @"$S4main1ySivp" = hidden global %TSi zeroinitializer, align 8
Aqui começa a definição da função principal :
define i32 @main(i32, i8**) #0 {
Primeiro, o valor 16 é armazenado nele na constante x :
store i64 16, i64* getelementptr inbounds (%TSi, %TSi* @"$S4main1xSivp", i32 0, i32 0), align 8
Em seguida, ele é carregado no registro 3 e usado para chamar a adição juntamente com o 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)
A adição de verificação de estouro retorna a estrutura. Seu primeiro valor é o resultado da adição e o segundo é um sinalizador que indica se houve um estouro.
A estrutura no LLVM é mais como uma tupla no Swift. Ele não possui nomes para os campos e você precisa obter o valor usando a instrução extractvalue . Seu primeiro parâmetro indica os tipos de campos na estrutura, o segundo - a própria estrutura e após a vírgula - o índice do campo, cujo valor precisa ser extraído:
%5 = extractvalue { i64, i1 } %4, 0 %6 = extractvalue { i64, i1 } %4, 1
Agora, o sinal de estouro é armazenado no sexto registro. Este valor é verificado usando a instrução de ramificação. Se houve um estouro, haverá uma transição para o bloco label8 , se não, para label7 :
br i1 %6, label %8, label %7
No primeiro deles, a execução do programa é interrompida por uma chamada para trap () . No segundo, o resultado da adição é armazenado na constante y , e 0 é retornado da função principal :
; <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
Geração de código de montagem
O compilador Swift também pode exibir o código de montagem. Para fazer isso, passe o sinalizador -emit-assembly :
swiftc -emit-assembly main.swift
O resultado do comando:
.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
Tendo entendido o código da representação intermediária descrita acima, você pode encontrar as instruções do assembler que ele gera. Aqui está armazenando 16 em uma constante e carregando-o no registro% rax :
movq $16, _$S4main1xSivp(%rip) movq _$S4main1xSivp(%rip), %rax
Aqui está a adição 7 e o valor da constante. O resultado da adição é colocado no registro% rax :
addq $7, %rax
E é assim que carregar o resultado na constante y se parece com:
movq %rax, -24(%rbp) movq -24(%rbp), %rcx movq %rcx, _$S4main1ySivp(%rip)
Código fonte:
Conclusão
Swift é um compilador bem estruturado e não foi difícil descobrir sua arquitetura geral. Também fiquei surpreso ao usar o LLVM, você pode escrever facilmente sua própria linguagem de programação. Obviamente, o compilador de parênteses é muito primitivo, mas a implementação do Caleidoscópio também é realmente compreensível. Eu recomendo a leitura de pelo menos os três primeiros capítulos do tutorial.
Obrigado a todos que leram. Vou continuar a estudar o compilador Swift e talvez escrever sobre o que aconteceu com ele. Em quais tópicos relacionados a ele você estaria interessado?
Links úteis: