如何手搓一个简易编译器(1)——前端

431 阅读4分钟

本篇依托于2023年华为毕昇杯CompilerBagel组参赛作品和本院lab教程。

受比赛时间限制,性能优化做得比较少,只先考虑满足正确性。

仓库地址:CompilerBagel

课程网站:NJU软件学院编译原理课程,涉及本院课程教程的地方不再赘述。另外,也可以在b站关注ant-hengxin,我们伟大的蚂蚁老师,并观看相关课程视频。

image-20230926010443166

好用的编译网站:godbolt.org

语言:java(使用语言识别工具antlr4)

目标翻译高级语言:sysy(简易删减版的c)

中间代码:LLVM

汇编语言:risc-v

注:语言要求和测试用例均可在华为毕昇杯的官网上找到,感兴趣的话,也可以来参加这项蛮有趣且在我心中含金量很高的比赛。

antlr

首先,将一个高级语言翻译成汇编语言,先要分析它的词法和语法,就和学习英语需要理解单词和句型一样。

我们使用antlr进行词法和语法的分析,我们编写的.g4文件可参考/src/main/java/antlr文件夹。

在词法和语法方面,在课程实验之外,我们实现增加了浮点数的内容。在编写完.g4文件之后,可利用下载好的antlr包(工具)自动生成辅助类。

仿LLVM API构建

参考/IRBuilder /Instruction

比赛中,是不允许调用LLVM的API的(显然,这是一个编译系统设计赛)。

因此,我们选择手搓了一个简陋版LLVM API的系统。

如何存储基本块、指令的信息?我们构建了一个Module-FunctionBlock-BaseBlock-Instruction-Operand/Operator的自上而下的系统存储,以及针对每个指令,我们输出对应的LLVM 代码String(在emit方法中)

如何设计API?我们直接对标了lab中使用过的API,可见下面的部分API文档,介绍了部分常用的API功能,如生成函数、添加指令、声明变量、模块跳转等。

输出LLVM IR 到文件

 PrintModuleToFile(module, "test.ll");

创建模块

 // 创建module
 IRModule module = IRModuleCreateWithName("module");
 // 初始化IRBuilder,后续将使用这个builder去生成LLVM IR
 IRBuilder builder = IRCreateBuilder();
 // 可以通过下面的语句为LLVM的int型和float型重命名方便以后使用
 Type int32Type = IRInt32Type();
 Type floatType = IRFloatType();

创建全局变量/局部变量

 // TODO:
 // 局部变量 
 // 为变量分配内存地址
 ValueRef IRBuildAlloca(builder, type , "_tmp");
 // 将valueRef存入pointer指向的内存中
 ValueRef IRBuildStore(builder, valueRef, pointer);
 ​
 //从内存中将值取出
 ValueRef value = IRBuildLoad(builder, /*pointer: ValueRef*/pointer, /*varName:String*/"value");
 ​
 // 全局变量
 // 申明全局变量
 ValueRef IRAddGlobal(module , type , "globalVarName");
 // 初始化全局变量
 void IRSetInitializer(module , valueRef , valueRef);

生成函数

● 先生成返回值类型(Type)

● 多个参数时需先生成函数的参数类型,再生成函数类型

● 用生成的函数类型去生成函数

 // 生成返回值类型
 Type returnType = int32Type;
 ​
 // 生成函数参数类型
 List<Type> params = new ArrayList<>();
 params.add(int32Type);
 params.add(floatType);
 ​
 // 生成函数类型
 Type funcType = new FunctionType(params, returnType);
 ​
 // 生成函数,即向之前创建的module中添加函数
 FunctionBlock function = IRAddFunction(module, "main", funcType);

创建基本块并添加指令

 // 通过如下语句在函数中加入基本块,一个函数可以加入多个基本块
 BaseBlock block1 = IRAppendBasicBlock(function, "mainEntry");
 // 选择要在哪个基本块后追加指令
 IRPositionBuilderAtEnd(builder, block1);
 // TODO:Add, Sub, Mul, (F/S)Div, Br, ... 
 ValueRef IRBuildAdd(builder, lhsValRef, rhsValRef, name); 
 ​
 // 决定跳转到哪个块
 IRBuildBr(builder, block1);
 ​
 //条件跳转指令,选择跳转到哪个块
 IRBuildCondBr(builder, 
 /*condition:ValueRef*/ condition, 
 /*ifTrue:BaseBlock*/ ifTrue, 
 /*ifFalse:BaseBlock*/ ifFalse);
 ​
 //生成比较指令
 ValueRef condition = IRBuildICmp(builder, /*这是个int型常量,表示比较的方式*/IRIntEQ, n, zero, "condition = n == 0");
 /* 上面参数中的常量包含如下取值
     IRIntEQ,
     IRIntNE,
     IRIntUGT,
     IRIntUGE,
     IRIntULT,
     IRIntULE,
     IRIntSGT,
     IRIntSGE,
     IRIntSLT,
     IRIntSLE,
 */
     
 //函数返回指令
 LLVMBuildRet(builder, /*result:ValueRef*/result);

visitor的改写

参考IRGenVisitor.java

这里采用visitor遍历语法树的方式生成类LLVM中间代码。

课程实验中,我们逐步实现了返回常数的主函数、局部变量定义、加减乘除、函数定义和调用、全局变量定义、条件表达式、控制流(无条件跳转、条件跳转、if、while、break、continue)、一维数组。

相较于课程实验,我们增加了浮点数、类型转换、多维数组的访问和初始化等内容。