LLVM系列第十六章:写一个简单的编译器

系列文章目录

LLVM系列第一章:编译LLVM源码
LLVM系列第二章:模块Module
LLVM系列第三章:函数Function
LLVM系列第四章:逻辑代码块Block
LLVM系列第五章:全局变量Global Variable
LLVM系列第六章:函数返回值Return
LLVM系列第七章:函数参数Function Arguments
LLVM系列第八章:算术运算语句Arithmetic Statement
LLVM系列第九章:控制流语句if-else
LLVM系列第十章:控制流语句if-else-phi
LLVM系列第十一章:写一个Hello World
LLVM系列第十二章:写一个简单的词法分析器Lexer
LLVM系列第十三章:写一个简单的语法分析器Parser
LLVM系列第十四章:写一个简单的语义分析器Semantic Analyzer
LLVM系列第十五章:写一个简单的中间代码生成器IR Generator
LLVM系列第十六章:写一个简单的编译器
LLVM系列第十七章:for循环
LLVM系列第十八章:写一个简单的IR处理流程Pass
LLVM系列第十九章:写一个简单的Module Pass
LLVM系列第二十章:写一个简单的Function Pass
LLVM系列第二十一章:写一个简单的Loop Pass
LLVM系列第二十二章:写一个简单的编译时函数调用统计器(Pass)
LLVM系列第二十三章:写一个简单的运行时函数调用统计器(Pass)
LLVM系列第二十四章:用Xcode编译调试LLVM源码
LLVM系列第二十五章:简单统计一下LLVM源码行数
LLVM系列第二十六章:理解LLVMContext
LLVM系列第二十七章:理解IRBuilder
LLVM系列第二十八章:写一个JIT Hello World
LLVM系列第二十九章:写一个简单的常量加法“消除”工具(Pass)

flex&bison系列



前言

在此记录下,基于LLVM写一个简单的编译器的过程,以备查阅。

开发环境的配置请参看 《LLVM系列第一章:编译LLVM源码》。

在本章中,我们来写一个非常简单的编译器。虽然简单,但也要能完成编译工作。它的工作流程如下:

词法分析Lexical
语法分析Syntax
语义分析Semantic
中间代码IR
目标代码

本章内容仅与目标代码(可执行程序)的生成有关,是一个简单的示例而已。其它与词法分析(Lexical Analysis)、语法分析(Syntax Analysis)、语义分析(Semantic Analysis)以及中间代码的生成(IR Generation)相关的内容,请参看前面的章节。

一、SimpleLang语言

为了方便起见,我们自己定义一种很简单的语言(名为SimpleLang)如下(示例):

calc : ("with" ident ("," ident)* ":")? expr ;
expr: term(("+"|"-")term)* ;
term : factor (( "*" | "/") factor)* ;
factor : ident | number | "(" expr ")" ;
ident : ([a-zAZ])+ ;
number : ([0-9])+ ;

这也是我们在前面章节中用到的语言。

二、项目结构

我们把这个简单的项目命名为SimpleCalculator。项目组织结构与前几章的项目类似,具体如下(示例):

% tree -I "build|build-xcode"

.
├── CMakeLists.txt
├── CalculatorCompilerRuntime.cpp
├── README.md
└── src
    ├── AST.h
    ├── CMakeLists.txt
    ├── CalculatorCompiler.cpp
    ├── IRGenerator.cpp
    ├── IRGenerator.h
    ├── Lexer.cpp
    ├── Lexer.h
    ├── Parser.cpp
    ├── Parser.h
    ├── SemanticAnalyzer.cpp
    └── SemanticAnalyzer.h

各文件的内容大体如下:

  1. AST.h,SimpleLang语言的抽象语法树(AST)的定义及实现代码。
  2. Lexer.h和Lexer.cpp,SimpleLang语言的词法分析器(Lexer)的定义及实现代码。
  3. Parser.h和Parser.cpp,SimpleLang语言的语法分析器(Parser)的定义及实现代码。
  4. SemanticAnalyzer.h和SemanticAnalyzer.cpp,SimpleLang语言的语义分析器(Semantic Analyzer)的定义及实现代码。
  5. IRGenerator.h和IRGenerator.cpp,中间代码(Intermediate Representation,即IR)生成器的定义及实现代码。
  6. CalculatorCompiler.cpp,这就是我们的编译器的代码。这是本章的重点。
  7. CalculatorCompilerRuntime.cpp,提供了SimpleLang语言的runtime函数。

三、项目细节

1. 程序模块

这个简单的项目只包含了一个模块:

  1. CalculatorCompiler,一个可执行程序,也就是我们的简单的小编译器

以下是跟项目组织结构相关的部分CMake脚本,与前一章的CMake脚本类似。

(1) 项目根目录(示例):

# CMakeLists.txt

...
project ("CalculatorCompiler")
...
add_subdirectory ("src")

这里创建了一个项目(project),并把src目录下的子项目加入进来。

(2) src目录(示例):

# src/CMakeLists.txt

...
add_executable(CalculatorCompiler ...)
...

这是src目录下的子项目,用来构建CalculatorCompiler程序。

2. 引入LLVM

我们需要做一些与LLVM相关的配置,才能顺利地使用LLVM,具体做法请参看前面章节。

3. Calculator Compiler

词法分析器、语法分析器、语义分析器、IR生成器的相关代码已在前面章节介绍,本章的重点是组装这些分析器和生成器:

  1. src/CalculatorCompiler.cpp,包含了main函数,这就是我们的编译器的代码。它把所有分析器和生成器组装,就能得到一个编译器,就能编译SimpleLang语言了。其实,它只能把我们用SimpleLang语言写的计算器小程序编译为IR代码,我们需要借助LLVM和clang来把IR代码编译成可执行程序。
  2. CalculatorCompilerRuntime.cpp,提供了SimpleLang语言的runtime函数,比如一些输入输出的处理函数。它有点像C或C++标准库(runtime library),提供了一些常用的、基本的函数和类(如数据结构和算法等)。

main函数(示例):

...
static llvm::cl::opt<std::string> input(llvm::cl::Positional, llvm::cl::desc("<input expression>"), llvm::cl::init(""));

int main(int argc, const char** argv)
{
    llvm::InitLLVM llvmInitializer(argc, argv);
    llvm::cl::ParseCommandLineOptions(argc, argv, "SimpleParser - a simple code parser\n");

    Lexer lexer(input);
    Parser parser(lexer);
    AST* tree = parser.Parse();
    if (!tree || parser.HasError())
    {
        llvm::errs() << "Syntax errors occured\n";
        return 1;
    }

    SemanticAnalyzer semanticAnalyzer;
    if (semanticAnalyzer.Analysis(tree))
    {
        llvm::errs() << "Semantic errors occured\n";
        return 1;
    }

    IRGenerator irGenerator;
    irGenerator.Generate(tree);

    return 0;
}

这一段代码跟前一章并没有很大区别。可以看到,这里调用了Lexer做词法分析,调用了Parser做语法分析,调用了SemanticAnalyzer做语义分析,最后调用了IRGenerator来生成IR代码。大致流程如下:

SimpleLang
Lexer
Parser
SemanticAnalyzer
IRGenerator
IR

3. Calculator Compiler Runtime

我们知道,在编译(链接)的时候,是需要把代码跟runtime一起编译(链接)的。比如,C++编译器(链接器)就会把程序模块与libc++进行链接。我们的CalculatorCompiler也提供了一个runtime。在生成可执行程序的时候,我们也需要把自己的计算器程序模块与runtime一起编译。

CalculatorCompiler提供的runtime很简单,只提供了两个函数,其作用仅仅是处理输入输出而已。我们可以把它们当做工具来用。runtime的代码如下(示例):

#include <iostream>

// Export C++ functions as C-style functions
extern "C"
{
    void CalculatorWrite(int result)
    {
        std::cout << "The result is: " << result << std::endl;
    }

    int CalculatorRead(const char* variableName)
    {
        std::cout << "Enter a value for " << variableName << ": ";

        int value;
        std::cin >> value;

        return value;
    }
}

四、编译器

首先,我们先把自己的编译器编译出来。

1. 生成项目文件

用CMake生成项目文件(示例):

mkdir build
cd build

cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ..

输出log如下(示例):

-- The C compiler identification is AppleClang 13.0.0.13000029
-- The CXX compiler identification is AppleClang 13.0.0.13000029
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found ZLIB: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/usr/lib/libz.tbd (found version "1.2.11") 
-- Found LibXml2: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/usr/lib/libxml2.tbd (found version "2.9.4") 
Found LLVM 12.0.1, build type Release
-- Configuring done
-- Generating done
-- Build files have been written to: .../SimpleCalculator/build

如果要生成Xcode项目文件,我们稍微改一下cmake命令的参数即可(示例):

mkdir build-xcode
cd build-xcode

cmake -G Xcode -DCMAKE_BUILD_TYPE=Debug ..

2. 编译出编译器

在编译之前,我们可以用clang-format工具把代码美化一下(示例):

cd /path/to/SimpleCalculator

clang-format -i src/*.cpp src/*.h

用ninja进行编译(示例):

cd /path/to/SimpleCalculator/build

ninja

输出log如下(示例):

[6/6] Linking CXX executable src/CalculatorCompiler

3. 运行编译器

运行一下我们自己的编译器,确保它已经编译好了(示例):

src/CalculatorCompiler --help

输出log如下(示例):

OVERVIEW: CalculatorCompiler - a tiny simple compiler

USAGE: CalculatorCompiler [options] <input expression>

OPTIONS:

Color Options:

  --color     - Use colors in output (default=autodetect)

Generic Options:

  --help      - Display available options (--help-hidden for more)
  --help-list - Display list of available options (--help-list-hidden for more)
  --version   - Display the version of this program

五、计算器

现在,我们用自己定义的SimpleLang语言写一个计算器程序,然后用我们自己的编译器把它编译出来。

1. 计算器源码

我们用自己定义的SimpleLang语言写一个很简单计算器程序。这个程序简单到只有一行代码(示例):

with a: a*3

它的作用很简单,就是把用户输入的数字a乘以3,并计算出结果。当然,我们也可以写个稍微复杂一点的计算器,具体请参看前面的章节。

2. 编译出计算器

第一步,用我们自己的编译器来编译计算器源码(示例):

cd /path/to/SimpleCalculator/build

src/CalculatorCompiler "with a: a*3" > SimpleCompiler.ll

以上命令会把我们的计算器源码编译成IR代码,并保存到SimpleCompiler.ll文件中。用cat命令可以查看其内容(示例):

cat SimpleCompiler.ll

输出如下(示例):

; ModuleID = 'Calculate.Module'
source_filename = "Calculate.Module"

@a.str = private constant [2 x i8] c"a\00"

define i32 @main(i32 %0, i8** %1) {
entry:
  %2 = call i32 @CalculatorRead(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @a.str, i32 0, i32 0))
  %3 = mul nsw i32 %2, 3
  call void @CalculatorWrite(i32 %3)
  ret i32 0
}

declare i32 @CalculatorRead(i8*)

declare void @CalculatorWrite(i32)

注意到以上的IR代码中有三个函数

  1. @main,即我们常提到的main函数
  2. i32 @CalculatorRead(i8*),处理用户输入
  3. void @CalculatorWrite(i32),处理输出

其中@CalculatorRead和@CalculatorWrite没有实现,因为它们的实现在runtime中。

第二步,借助LLVM llc工具把IR代码编译成目标文件(示例):

llc --filetype=obj -o=SimpleCalculator.o SimpleCompiler.ll 

这一步的输出是SimpleCalculator.o文件。

第三步,借助clang把我们的计算器程序模块跟runtime一起编译生成可执行程序(示例):

# Set up C++ standard library and header path
export SDKROOT=$(xcrun --sdk macosx --show-sdk-path)

clang++ -o SimpleCalculator SimpleCalculator.o ../CalculatorCompilerRuntime.cpp

或者(示例):

clang++ -o SimpleCalculator -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk SimpleCalculator.o ../CalculatorCompilerRuntime.cpp

这一步的输出是SimpleCalculator,它是一个可执行程序,也就是我们最终想要的计算器。

3. 运行计算器

我们现在来试试自己的计算器(示例):

./SimpleCalculator

输出(示例):

Enter a value for a: 

我们输入任意一个数字,比如5(示例):

Enter a value for a: 5

输出(示例):

The result is: 15

结果是正确的!

五、总结

我们基于LLVM提供的API,用C++写了一个很简单的编译器,并用它编译出了我们自己的计算器程序。完整源码示例请参看:
https://github.com/wuzhanglin/llvm-simple-calculator-compiler

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值