系列文章目录
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 Analysis)、语法分析(Syntax Analysis)、语义分析(Semantic Analysis)以及中间代码的生成(IR Generation)相关的内容,请参看前面的章节。
为了方便起见,我们自己定义一种很简单的语言(名为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
各文件的内容大体如下:
这个简单的项目只包含了一个模块:
以下是跟项目组织结构相关的部分CMake脚本,与前一章的CMake脚本类似。
(1) 项目根目录(示例):
# CMakeLists.txt
...
project ("CalculatorCompiler")
...
add_subdirectory ("src")
这里创建了一个项目(project),并把src目录下的子项目加入进来。
(2) src目录(示例):
# src/CMakeLists.txt
...
add_executable(CalculatorCompiler ...)
...
这是src目录下的子项目,用来构建CalculatorCompiler程序。
我们需要做一些与LLVM相关的配置,才能顺利地使用LLVM,具体做法请参看前面章节。
词法分析器、语法分析器、语义分析器、IR生成器的相关代码已在前面章节介绍,本章的重点是组装这些分析器和生成器:
main函数(示例):
...
static llvm::cl::opt input(llvm::cl::Positional, llvm::cl::desc(""), 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代码。大致流程如下:
我们知道,在编译(链接)的时候,是需要把代码跟runtime一起编译(链接)的。比如,C++编译器(链接器)就会把程序模块与libc++进行链接。我们的CalculatorCompiler也提供了一个runtime。在生成可执行程序的时候,我们也需要把自己的计算器程序模块与runtime一起编译。
CalculatorCompiler提供的runtime很简单,只提供了两个函数,其作用仅仅是处理输入输出而已。我们可以把它们当做工具来用。runtime的代码如下(示例):
#include
// 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;
}
}
首先,我们先把自己的编译器编译出来。
用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 ..
在编译之前,我们可以用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
运行一下我们自己的编译器,确保它已经编译好了(示例):
src/CalculatorCompiler --help
输出log如下(示例):
OVERVIEW: CalculatorCompiler - a tiny simple compiler
USAGE: CalculatorCompiler [options]
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语言写一个计算器程序,然后用我们自己的编译器把它编译出来。
我们用自己定义的SimpleLang语言写一个很简单计算器程序。这个程序简单到只有一行代码(示例):
with a: a*3
它的作用很简单,就是把用户输入的数字a乘以3,并计算出结果。当然,我们也可以写个稍微复杂一点的计算器,具体请参看前面的章节。
第一步,用我们自己的编译器来编译计算器源码(示例):
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代码中有三个函数
其中@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,它是一个可执行程序,也就是我们最终想要的计算器。
我们现在来试试自己的计算器(示例):
./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