HNU编译原理实验三cminus_compiler-2022-fall

Source

前言:实验难度再次提升,不过会一个就可以做其他部分,很多都是相同的,个人认为更难的还是配置环境部分,真的会折磨死人

lab3 实验报告

实验要求

  1. 第一部分: 了解LLVM IR。通过clang生成的.ll,了解LLVM IR与c代码的对应关系。完成1.3
  2. 第二部分: 了解LightIR。通过助教提供的c++例子,了解LightIR的c++接口及实现。完成2.3
  3. 第三部分: 理解Visitor Pattern。
  4. 实验报告:在report.md回答3个问题

实验设计

问题1: cpp与.ll的对应

请描述你的cpp代码片段和.ll的每个BasicBlock的对应关系。描述中请附上两者代码。

根据 1.2 gcd例子: 利用clang生成的.ll 进行学习

输入clang -S -emit-llvm gcd_array.c指令:

在这里插入图片描述

查看 gcd_array.ll 文件,并输入lli gcd_array.ll; echo $?指令,验证 gcd_array.ll 的正确性:

在这里插入图片描述

可以看到,返回结果为18,运行gcd_array.c 查看运行结果:

在这里插入图片描述

可以得出其运行后,输出结果也为18。 因此,可以验证 gcd_array.ll 文件正确地对应了 gcd_array.c 文件。

补充assign_hand.ll

查看 assign.c 文件

int main(){
    
      
  int a[10];
  a[0] = 10;
  a[1] = a[0] * 2;
  return a[1];
}

很明显运行结果返回20,下面根据上面的测试样例和精简的IR Reference手册,补充补充 assign_hand.ll

; ModuleID = 'assign.c'
source_filename = "assign.c"
; target的开始
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
;target的结束

; 全局main函数的定义
define dso_local i32 @main() #0{
	;第一个基本块的开始
	;语句:int a[10];创建空间,并返回指针
	%1 = alloca [10 x i32]	
	
	;语句:a[0] = 10;获取a[0]指针
	%2 = getelementptr inbounds [10 x i32], [10 x i32]* %1, i64 0, i64 0
	; 将10存入a[0]中
	store i32 10, i32* %2
	
	;语句:a[1] = a[0] * 2;
	%3 = load i32, i32* %2	; 取出a[0],存到%3中
	%4 = mul i32 %3, 2		; 将值乘以2,存在%4中
	%5 = getelementptr inbounds [10 x i32], [10 x i32]* %1, i64 0, i64 1	;获取a[1]指针
	store i32 %4, i32* %5	; 写入a[1]中
	
	; 语句:return a[1]
	ret i32 %4				; 将a[0]返回
}

结果验证:

运行assign.c ,输入命令:gcc -o assign assign.c ./assign;echo $?,结果如下:

在这里插入图片描述

运行刚才补充的assign_hand.ll ,输入命令lli assign_hand.ll; echo $? ,运行结果如下:

在这里插入图片描述

可以看到,结果一样,都为20,说明补充正确。assign_hand.ll文件正确地对应了 assign.c 文件。

补充fun_hand.ll

查看 fun.c 文件

int callee(int a){
    
      
  return 2 * a;
}
int main(){
    
      
  return callee(110);
}

很明显运行结果返回220,下面根据上面的测试样例和精简的IR Reference手册,补充补充 fun_hand.ll

; ModuleID = 'fun.c'
source_filename = "fun.c"
; target的开始
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
;target的结束
    
; 语句:int callee(int a)。callee 函数的定义
define dso_local i32 @callee(i32) #0{
    
      
    ; 语句:return 2 * a
    %2 = mul i32 %0, 2		; 将参数%0乘以2存入%2中
	ret i32 %2				; 返回%2
}

; main 函数的定义
define dso_local i32 @main() #0{
    
      
    ; 语句:return callee(110);
	%1 = call i32 @callee(i32 110)	; 调用函数callee,并将结果存到%1中
	ret i32 %1					; 返回%1
}

结果验证:

运行fun.c ,输入命令:gcc -o fun fun.c ./fun;echo $?,结果如下:

在这里插入图片描述

运行刚才补充的fun_hand.ll ,输入命令lli fun_hand.ll; echo $? ,运行结果如下:

在这里插入图片描述

这里有一些要注意的地方。第一点是对callee函数定义时define dso_local i32 @callee(i32)传入的参数如果只有一个int型,可以直接传入i32,默认传入参数为%0,如果写的是i32 %0会报错,初步认为应该和llvm版本有关。

第二点就是 %2 = mul i32 %0, 2这一条语句,为什么不能用%1进行赋值,会报错

在这里插入图片描述

程序也会建议使用%2赋值

补充if_hand.ll

查看 fun.c 文件

int main(){
    
      
  float a = 5.555;
  if(a > 1)
    return 233;
  return 0;
}

很明显运行结果返回233,下面根据上面的测试样例和精简的IR Reference手册,补充补充 fun_hand.ll

; ModuleID = 'if.c'
source_filename = "if.c"
; target的开始
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
;target的结束

; main 函数的定义
define dso_local i32 @main() #0{
    
      
	;语句:; float a = 5.555;
	%1 = alloca float      ;为a开辟栈空间
	store float 0x40163851E0000000, float* %1  ;将浮点数5.555写入a指向的内存空间
	
	; 语句:if(a > 1)
	%2 = load float, float* %1		; 取出%1所指向的空间中的值赋给%2
	%3 = fcmp ugt float %2, 1.000	; 将其和1进行比较,返回结果到%3中,是布尔型
	br i1 %3, label %lebel_1, label %lebel_2	; 如果%31,则跳转到%4,否则跳转到%5

lebel_1:	; 语句:return 233;
	ret i32 233		; 返回233

lebel_2:	; 语句:return 0;
	ret i32 0		; 返回0
}

结果验证:

运行if.c ,输入命令:gcc -o if if.c ./if;echo $?,结果如下:

在这里插入图片描述

运行刚才补充的if_hand.ll ,输入命令lli if_hand.ll; echo $? ,运行结果如下:

在这里插入图片描述

这里有一个难点就是给float a = 5.555;赋值。如果直接使用store float 5.555, float* %1会报错。 输入clang -S -emit-llvm if.c指令,看了一下样例,发现要写成浮点数的机器表达形式。 即

5.555的二进制表示:101.10001110000101000111101011100001010001111010111
尾数正规化 :1.0110001110000101000111101011100001010001111010111*22
E=2, exp = E + Bias =129,二进制表示为10000001
符号位为0
有效数字M等于 01100011100001010001111
故5.555的IEEE表示为
0 10000001 01100011100001010001111
转换为十六进制为0x40b1c28f
但样例给出的是0x40163851E0000000,后经验证发现样例给的是double型的IEEE标识,但程序和精简的IR Reference手册都说明了float是单精度的,所以我觉得这里还是有点问题

补充while.ll

查看 while.c 文件

int main(){
    
      
  int a;
  int i;
  a = 10;
  i = 0;
  while(i < 10){
    
      
    i = i + 1;
    a = a + i;
  }
  return a;
}

很明显运行结果返回65,下面根据上面的测试样例和精简的IR Reference手册,补充补充 fun_hand.ll

; ModuleID = 'if.c'
source_filename = "if.c"
; target的开始
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
;target的结束

; main 函数的定义
define dso_local i32 @main() #0{
    
      
	;为变量a和i开辟栈空间
	%1=alloca i32
	%2=alloca i32
	;100写入a和i指向的内存空间
	store i32 10,i32* %1
	store i32 0,i32* %2
	;跳到while条件判断中
	br label %label_1

;i < 10
label_1:
	;取出i的值赋给%3
	%3=load i32, i32* %2
	;将i和10进行比较,得到的结果为布尔型赋给%4
	%4 = icmp ult i32 %3, 10
	;如果%41跳到label_2,否则跳到%label_3
	br i1 %4,label %label_2,label %label_3

;循环体的内容	
label_2:
	;i=i+1
	%5=add i32 %3 , 1
	store i32 %5,i32* %2
	;a=a+1,先取出a的值再相加再存回a里
	%6=load i32,i32* %1
	%7=add i32 %6,%5
	store i32 %7,i32* %1
	;跳回条件判断
	br label %label_1
;return a
label_3:
	;取出a的值
	%8=load i32,i32* %1
	;返回a
	ret i32 %8
}

结果验证:

运行while.c ,输入命令:gcc -o while while.c ./while;echo $?,结果如下:

在这里插入图片描述

运行刚才补充的while_hand.ll ,输入命令lli while_hand.ll; echo $? ,运行结果如下:

在这里插入图片描述

补充assign_generator.cpp

首先查看样例gcd_array_generator.cpp

#include "BasicBlock.h"
#include "Constant.h"
#include "Function.h"
#include "IRBuilder.h"
#include "Module.h"
#include "Type.h"

#include <iostream>
#include <memory>

#ifdef DEBUG  // 用于调试信息,大家可以在编译过程中通过" -DDEBUG"来开启这一选项
#define DEBUG_OUTPUT std::cout << __LINE__ << std::endl;  // 输出行号的简单示例
#else
#define DEBUG_OUTPUT
#endif

#define CONST_INT(num) \
    ConstantInt::get(num, module)

#define CONST_FP(num) \
    ConstantFP::get(num, module) // 得到常数值的表示,方便后面多次用到

int main() {
  auto module = new Module("Cminus code");  // module name是什么无关紧要
  auto builder = new IRBuilder(nullptr, module);
  Type *Int32Type = Type::get_int32_type(module);

  // 全局数组,x,y
  auto *arrayType = ArrayType::get(Int32Type, 1);
  auto initializer = ConstantZero::get(Int32Type, module);
  auto x = GlobalVariable::create("x", module, arrayType, false, initializer);// 参数解释:  名字name,所属module,全局变量类型type,
  auto y = GlobalVariable::create("y", module, arrayType, false, initializer);//          是否是常量定义(cminus中没有常量概念,应全都是false),初始化常量(ConstantZero类)

  // gcd函数
  // 函数参数类型的vector
  std::vector<Type *> Ints(2, Int32Type);

  //通过返回值类型与参数类型列表得到函数类型
  auto gcdFunTy = FunctionType::get(Int32Type, Ints);

  // 由函数类型得到函数
  auto gcdFun = Function::create(gcdFunTy,
                                 "gcd", module);

  // BB的名字在生成中无所谓,但是可以方便阅读
  auto bb = BasicBlock::create(module, "entry", gcdFun);
  
  builder->set_insert_point(bb);                        // 一个BB的开始,将当前插入指令点的位置设在bb
  
  auto retAlloca = builder->create_alloca(Int32Type);   // 在内存中分配返回值的位置
  auto uAlloca = builder->create_alloca(Int32Type);     // 在内存中分配参数u的位置
  auto vAlloca = builder->create_alloca(Int32Type);     // 在内存中分配参数v的位置 

  std::vector<Value *> args;  // 获取gcd函数的形参,通过Function中的iterator
  for (auto arg = gcdFun->arg_begin(); arg != gcdFun->arg_end(); arg++) {
    args.push_back(*arg);   // * 号运算符是从迭代器中取出迭代器当前指向的元素
  }

  builder->create_store(args[0], uAlloca);  // 将参数u store下来
  builder->create_store(args[1], vAlloca);  // 将参数v store下来

  auto vLoad = builder->create_load(vAlloca);           // 将参数v load上来
  auto icmp = builder->create_icmp_eq(vLoad, CONST_INT(0));  // v和0的比较,注意ICMPEQ

  auto trueBB = BasicBlock::create(module, "trueBB", gcdFun);    // true分支
  auto falseBB = BasicBlock::create(module, "falseBB", gcdFun);  // false分支
  auto retBB = BasicBlock::create(
      module, "", gcdFun);  // return分支,提前create,以便true分支可以br

  auto br = builder->create_cond_br(icmp, trueBB, falseBB);  // 条件BR
  DEBUG_OUTPUT // 我调试的时候故意留下来的,以醒目地提醒你这个调试用的宏定义方法
  builder->set_insert_point(trueBB);  // if true; 分支的开始需要SetInsertPoint设置
  auto uLoad = builder->create_load(uAlloca);
  builder->create_store(uLoad, retAlloca);
  builder->create_br(retBB);  // br retBB

  builder->set_insert_point(falseBB);  // if false
  uLoad = builder->create_load(uAlloca);
  vLoad = builder->create_load(vAlloca);
  auto div = builder->create_isdiv(uLoad, vLoad);  // SDIV - div with S flag
  auto mul = builder->create_imul(div, vLoad);  // MUL - mul
  auto sub = builder->create_isub(uLoad, mul);  // the same
  auto call = builder->create_call(gcdFun, {vLoad, sub}); // 创建call指令
  // {vLoad, sub} - 参数array
  builder->create_store(call, retAlloca);
  builder->create_br(retBB);  // br retBB

  builder->set_insert_point(retBB);  // ret分支
  auto retLoad = builder->create_load(retAlloca);
  builder->create_ret(retLoad);

  // funArray函数
  auto Int32PtrType = Type::get_int32_ptr_type(module);   // 单个参数的类型,指针
  std::vector<Type *> IntPtrs(2, Int32PtrType);           // 参数列表类型
  auto funArrayFunType = FunctionType::get(Int32Type, IntPtrs);   // 函数类型
  auto funArrayFun = Function::create(funArrayFunType, "funArray", module);
  bb = BasicBlock::create(module, "entry", funArrayFun);
  builder->set_insert_point(bb);
  auto upAlloca = builder->create_alloca(Int32PtrType);   // u的存放
  auto vpAlloca = builder->create_alloca(Int32PtrType);   // v的存放
  auto aAlloca = builder->create_alloca(Int32Type);       // a的存放
  auto bAlloca = builder->create_alloca(Int32Type);       // b的存放
  auto tempAlloca = builder->create_alloca(Int32Type);    // temp的存放

  std::vector<Value *> args1;  //获取funArrayFun函数的形参,通过Function中的iterator
  for (auto arg = funArrayFun->arg_begin(); arg != funArrayFun->arg_end(); arg++) {
    args1.push_back(*arg); // * 号运算符是从迭代器中取出迭代器当前指向的元素
  }
  builder->create_store(args1[0], upAlloca);              // 将参数u store下来
  builder->create_store(args1[1], vpAlloca);              // 将参数v store下来

  auto u0pLoad = builder->create_load(upAlloca);          // 读取u
  auto u0GEP = builder->create_gep(u0pLoad, {CONST_INT(0)});  // GEP: 获取u[0]地址
  auto u0Load = builder->create_load(u0GEP);              // 从u[0]地址 读取u[0]
  builder->create_store(u0Load, aAlloca);                 // 将u[0] 写入 a
  auto v0pLoad = builder->create_load(vpAlloca);          // 同上
  auto v0GEP = builder->create_gep(v0pLoad, {CONST_INT(0)});
  auto v0Load = builder->create_load(v0GEP);
  builder->create_store(v0Load, bAlloca);

  auto aLoad = builder->create_load(aAlloca);
  auto bLoad = builder->create_load(bAlloca);
  icmp = builder->create_icmp_lt(aLoad, bLoad);
  trueBB = BasicBlock::create(module, "trueBB", funArrayFun);
  falseBB = BasicBlock::create(module, "falseBB", funArrayFun);
  builder->create_cond_br(icmp, trueBB, falseBB);

  builder->set_insert_point(trueBB);
  builder->create_store(aLoad, tempAlloca);
  builder->create_store(bLoad, aAlloca);
  auto tempLoad = builder->create_load(tempAlloca);
  builder->create_store(tempLoad, bAlloca);
  builder->create_br(falseBB);  // 注意在下一个BB之前要Br一下

  builder->set_insert_point(falseBB);
  aLoad = builder->create_load(aAlloca);
  bLoad = builder->create_load(bAlloca);
  call = builder->create_call(gcdFun, {aLoad, bLoad});
  builder->create_ret(call);


  // main函数
  auto mainFun = Function::create(FunctionType::get(Int32Type, {}),
                                  "main", module);
  bb = BasicBlock::create(module, "entry", mainFun);
  // BasicBlock的名字在生成中无所谓,但是可以方便阅读
  builder->set_insert_point(bb);

  retAlloca = builder->create_alloca(Int32Type);
  builder->create_store(CONST_INT(0), retAlloca);             // 默认 ret 0

  auto x0GEP = builder->create_gep(x, {CONST_INT(0), CONST_INT(0)});  // GEP: 这里为什么是{0, 0}呢? (实验报告相关)
  builder->create_store(CONST_INT(90), x0GEP);
  auto y0GEP = builder->create_gep(y, {CONST_INT(0), CONST_INT(0)});  // GEP: 这里为什么是{0, 0}呢? (实验报告相关)
  builder->create_store(CONST_INT(18), y0GEP);

  x0GEP = builder->create_gep(x, {CONST_INT(0), CONST_INT(0)});
  y0GEP = builder->create_gep(y, {CONST_INT(0), CONST_INT(0)});
  call = builder->create_call(funArrayFun, {x0GEP, y0GEP});           // 为什么这里传的是{x0GEP, y0GEP}呢?

  builder->create_ret(call);
  // 给这么多注释了,但是可能你们还是会弄很多bug
  // 所以强烈建议配置AutoComplete,效率会大大提高!
  // 别人配了AutoComplete,只花1小时coding
  // 你没有配AutoComplete,找method花5小时,debug花5小时,肯定哭唧唧!
  // 最后,如果猜不到某个IR指令对应的C++的函数,建议把指令翻译成英语然后在method列表中搜索一下
  // 最后的最后,这个例子只涉及到了一点基本的指令生成,
  // 对于额外的指令,包括数组,在之后的实验中可能需要大家好好搜索一下思考一下,
  // 还有涉及到的C++语法,可以在gitlab上发issue提问或者向大家提供指导,会有bonus哦!
  // 对于这个例子里的代码风格/用法,如果有好的建议也欢迎提出!
  std::cout << module->print();
  delete module;
  return 0;
}

按照gcd_array_generator.cpp来补充assign_generator.cpp

#include "BasicBlock.h"
#include "Constant.h"
#include "Function.h"
#include "IRBuilder.h"
#include "Module.h"
#include "Type.h"

#include <iostream>
#include <memory>

#ifdef DEBUG  // 用于调试信息,大家可以在编译过程中通过" -DDEBUG"来开启这一选项
#define DEBUG_OUTPUT std::cout << __LINE__ << std::endl;  // 输出行号的简单示例
#else
#define DEBUG_OUTPUT
#endif

#define CONST_INT(num) \
    ConstantInt::get(num, module)

#define CONST_FP(num) \
    ConstantFP::get(num, module) // 得到常数值的表示,方便后面多次用到
    
int main(){
	auto module = new Module("Cminus code");  // module name是什么无关紧要
	auto builder = new IRBuilder(nullptr, module);      // 创建IRBuilder,该类提供了独立的接口创建各种 IR 指令
	Type* Int32Type = Type::get_int32_type(module);

	// main函数
	auto mainFun = Function::create(FunctionType::get(Int32Type, {}),   /* 创建 main 函数 */	
	auto bb = BasicBlock::create(module, "entry", mainFun); 
    builder->set_insert_point(bb);  
    
    auto retAlloca = builder->create_alloca(Int32Type);  //分配一个保存返回值的内容
    
    auto *arrayType = ArrayType::get(Int32Type, 10);  //数组类型,参数依次是数组元素的类型Int32Type,数组元素个数10
    auto a=builder->create_alloca(arrayType);  //数组a[10]

    auto a0GEP = builder->create_gep(a, {CONST_INT(0), CONST_INT(0)}); //获取a[0]的地址
    builder->create_store(CONST_INT(10), a0GEP);  //将整数10写入a[0]的地址
    auto a0Load = builder->create_load(a0GEP);  //读取a[0]
    auto m=builder->create_imul(a0Load,CONST_INT(2)); //a[0]*2

    auto a1GEP = builder->create_gep(a, {CONST_INT(0), CONST_INT(1)});  //获取a[1]的地址
    builder->create_store(m, a1GEP); //将a[0]*2写入a[1]的地址
    auto a1Load = builder->create_load(a1GEP); //获取a[1]的值

    builder->create_store(a1Load, retAlloca); //将a[1]的值写入retAlloca地址中
    auto retLoad=builder->create_load(retAlloca); //读取retAlloca地址的值
    builder->create_ret(retLoad);  //创建ret指令,即返回

    std::cout << module->print();
    delete module;
    return 0;   
}

结果验证:

输入指令make,编译通过后输入指令./stu_assign_generator>assign.ll

在这里插入图片描述

assign.ll结果如下:

define i32 @main() {
    
      
label_entry:
  %op0 = alloca i32
  %op1 = alloca [10 x i32]
  %op2 = getelementptr [10 x i32], [10 x i32]* %op1, i32 0, i32 0
  store i32 10, i32* %op2
  %op3 = load i32, i32* %op2
  %op4 = mul i32 %op3, 2
  %op5 = getelementptr [10 x i32], [10 x i32]* %op1, i32 0, i32 1
  store i32 %op4, i32* %op5
  %op6 = load i32, i32* %op5
  store i32 %op6, i32* %op0
  %op7 = load i32, i32* %op0
  ret i32 %op7
}

输入 lli assign.ll; echo $?指令进行测试,结果如下:

在这里插入图片描述

输出结果为 20,结果正确。

只有一个basicblock,对应关系为:

auto bb = BasicBlock::create(module, "entry", mainFun);创建名为entry的基本块,并将其存到变量bb中,对应.ll文件中的label_entry

补充fun_generator.cpp

fun_generator.cpp 代码如下:

#include "BasicBlock.h"
#include "Constant.h"
#include "Function.h"
#include "IRBuilder.h"
#include "Module.h"
#include "Type.h"

#include <iostream>
#include <memory>

#ifdef DEBUG  // 用于调试信息,大家可以在编译过程中通过" -DDEBUG"来开启这一选项
#define DEBUG_OUTPUT std::cout << __LINE__ << std::endl;  // 输出行号的简单示例
#else
#define DEBUG_OUTPUT
#endif

#define CONST_INT(num) \
    ConstantInt::get(num, module)

#define CONST_FP(num) \
    ConstantFP::get(num, module) // 得到常数值的表示,方便后面多次用到
int main() {
  auto module = new Module("fun code");
  auto builder = new IRBuilder(nullptr, module); //该类提供了独立的接口创建各种 IR 指令
  Type *Int32Type = Type::get_int32_type(module);
  
  //函数callee
  std::vector<Type *> Ints(1, Int32Type); // 函数参数类型的vector
  auto calleeFunTy = FunctionType::get(Int32Type, Ints); //通过返回值类型与参数类型列表得到函数类型
  auto calleeFun = Function::create(calleeFunTy,"callee", module);   // 由函数类型得到函数
  
  auto bb = BasicBlock::create(module, "entry", calleeFun);
  builder->set_insert_point(bb);                 // 一个BB的开始,将当前插入指令点的位置设在bb

  auto aAlloca = builder->create_alloca(Int32Type);  // 在内存中分配参数a的位置
 
  std::vector<Value *> args;  // 获取函数的形参,通过Function中的iterator
  for (auto arg = calleeFun->arg_begin(); arg != calleeFun->arg_end(); arg++) {
    args.push_back(*arg);   // * 号运算符是从迭代器中取出迭代器当前指向的元素
  }
  builder->create_store(args[0], aAlloca);  // 将参数a store下来
  auto aLoad=builder->create_load(aAlloca); //获取参数a的值
  auto m=builder->create_imul(aLoad,CONST_INT(2)); //a*2
  
  auto retAlloca = builder->create_alloca(Int32Type);  //分配一个保存返回值的内容
  builder->create_store(m, retAlloca); //将m的值写入retAlloca地址中
  auto retLoad=builder->create_load(retAlloca); //读取retAlloca地址的值
  builder->create_ret(retLoad);  //创建ret指令,即返回

  // main函数
  auto mainFun = Function::create(FunctionType::get(Int32Type, {}),"main", module);
  bb = BasicBlock::create(module, "entry", mainFun); //创建BasicBlock
  builder->set_insert_point(bb);  //将当前插入指令点的位置设在bb
  
  auto cs= builder->create_alloca(Int32Type); // 在内存中分配参数cs的位置
  builder->create_store(CONST_INT(110), cs);  //将整数10写入cs的地址
  auto csLoad = builder->create_load(cs);  //读取cs
  auto call = builder->create_call(calleeFun, {csLoad}); //创建call指令
  builder->create_ret(call); //创建ret指令,返回

  std::cout << module->print();
  delete module;
  return 0;
}

结果验证:

输入指令make,编译通过后输入指令./stu_fun_generator >fun.ll

内容如下:

define i32 @callee(i32 %arg0) {
    
      
label_entry:
  %op1 = alloca i32
  store i32 %arg0, i32* %op1
  %op2 = load i32, i32* %op1
  %op3 = mul i32 %op2, 2
  %op4 = alloca i32
  store i32 %op3, i32* %op4
  %op5 = load i32, i32* %op4
  ret i32 %op5
}
define i32 @main() {
    
      
label_entry:
  %op0 = alloca i32
  store i32 110, i32* %op0
  %op1 = load i32, i32* %op0
  %op2 = call i32 @callee(i32 %op1)
  ret i32 %op2
}

输入 lli fun.ll; echo $?指令进行测试,结果如下:

在这里插入图片描述

结果为220,输出正确

创建了两个块,分别为 auto bb = BasicBlock::create(module, "entry", calleeFun);bb = BasicBlock::create(module, "entry", mainFun); ,其分别对应.ll文件中 callee 函数中的标签label_entrymain函数中的标签label_entry

补充if_generator.cpp

if_generator.cpp代码如下:

#include "BasicBlock.h"
#include "Constant.h"
#include "Function.h"
#include "IRBuilder.h"
#include "Module.h"
#include "Type.h"

#include <iostream>
#include <memory>

#ifdef DEBUG  // 用于调试信息,大家可以在编译过程中通过" -DDEBUG"来开启这一选项
#define DEBUG_OUTPUT std::cout << __LINE__ << std::endl;  // 输出行号的简单示例
#else
#define DEBUG_OUTPUT
#endif

#define CONST_INT(num) \
    ConstantInt::get(num, module)

#define CONST_FP(num) \
    ConstantFP::get(num, module) // 得到常数值的表示,方便后面多次用到
    
int main() {
  auto module = new Module("if code");
  auto builder = new IRBuilder(nullptr, module); //该类提供了独立的接口创建各种 IR 指令
  Type *Int32Type = Type::get_int32_type(module);
  Type *FloatType = Type::get_float_type(module);
  
  // main函数
  auto mainFun = Function::create(FunctionType::get(Int32Type, {}),"main", module);
  auto bb = BasicBlock::create(module, "entry", mainFun); //创建BasicBlock
  builder->set_insert_point(bb);  //将当前插入指令点的位置设在bb
  
  auto retAlloca = builder->create_alloca(Int32Type);   // 在内存中分配返回值的位置
  
  auto aAlloca=builder->create_alloca(FloatType); //在内存中分配参数a的位置
  builder->create_store(CONST_FP(5.555), aAlloca);  //将常数5.555写入a的地址
  auto aLoad = builder->create_load(aAlloca);  //读取a
  
  auto cmp = builder->create_fcmp_gt(aLoad, CONST_FP(1.000));  //判断a是否大于1
  auto trueBB = BasicBlock::create(module, "trueBB", mainFun);    // true分支
  auto falseBB = BasicBlock::create(module, "falseBB", mainFun);  // false分支
  auto retBB = BasicBlock::create(module, "", mainFun);  // return分支,提前create,以便true分支可以br
  auto br = builder->create_cond_br(cmp, trueBB, falseBB);  // 条件BR
  
  builder->set_insert_point(trueBB);  // if true; 分支的开始需要SetInsertPoint设置
  builder->create_store(CONST_INT(233), retAlloca); //将整数233存储到返回值的地址中
  builder->create_br(retBB);  // br retBB
  
  builder->set_insert_point(falseBB);  // if false
  builder->create_store(CONST_INT(0), retAlloca);  //将整数0存储到返回值的地址中
  builder->create_br(retBB);  // br retBB

  builder->set_insert_point(retBB);  // ret分支
  auto retLoad = builder->create_load(retAlloca);  //获取返回值地址中值
  builder->create_ret(retLoad);  //创建ret指令,返回

  std::cout << module->print();
  delete module;
  return 0;
}

结果验证:

输入指令make,编译通过后输入指令./stu_if_generator >if.ll

内容如下:

define i32 @main() {
    
      
label_entry:
  %op0 = alloca i32
  %op1 = alloca float
  store float 0x40163851e0000000, float* %op1
  %op2 = load float, float* %op1
  %op3 = fcmp ugt float %op2,0x3ff0000000000000
  br i1 %op3, label %label_trueBB, label %label_falseBB
label_trueBB:                                                ; preds = %label_entry
  store i32 233, i32* %op0
  br label %label4
label_falseBB:                                                ; preds = %label_entry
  store i32 0, i32* %op0
  br label %label4
label4:                                                ; preds = %label_trueBB, %label_falseBB
  %op5 = load i32, i32* %op0
  ret i32 %op5
}

输入 lli if.ll; echo $?指令进行测试,结果如下:

在这里插入图片描述

结果为233,输出正确

创建了4个basicblock,对应关系为:

  1. auto bb = BasicBlock::create(module, "entry", mainFun);对应main函数中的标签label_entry
  2. auto trueBB = BasicBlock::create(module, "trueBB", mainFun);对应标签label_trueBB
  3. auto falseBB = BasicBlock::create(module, "falseBB", mainFun);对应标签label_falseBB
  4. auto retBB = BasicBlock::create(module, "", mainFun);对应标签label4

补充while_generator.cpp

#include "BasicBlock.h"
#include "Constant.h"
#include "Function.h"
#include "IRBuilder.h"
#include "Module.h"
#include "Type.h"

#include <iostream>
#include <memory>

#ifdef DEBUG  // 用于调试信息,大家可以在编译过程中通过" -DDEBUG"来开启这一选项
#define DEBUG_OUTPUT std::cout << __LINE__ << std::endl;  // 输出行号的简单示例
#else
#define DEBUG_OUTPUT
#endif

#define CONST_INT(num) \
    ConstantInt::get(num, module)

#define CONST_FP(num) \
    ConstantFP::get(num, module) // 得到常数值的表示,方便后面多次用到
    
int main() {
    
      
  auto module = new Module("while code");
  auto builder = new IRBuilder(nullptr, module); //该类提供了独立的接口创建各种 IR 指令
  Type *Int32Type = Type::get_int32_type(module);

  // main函数
  auto mainFun = Function::create(FunctionType::get(Int32Type, {
    
      }),"main", module);
  auto bb = BasicBlock::create(module, "entry", mainFun); //创建BasicBlock
  builder->set_insert_point(bb);  //将当前插入指令点的位置设在bb
  
  auto retAlloca = builder->create_alloca(Int32Type);   // 在内存中分配返回值的位置
  
  auto aAlloca=builder->create_alloca(Int32Type); //在内存中分配参数a的位置
  auto iAlloca=builder->create_alloca(Int32Type); //在内存中分配参数i的位置
  
  builder->create_store(CONST_INT(10), aAlloca);  //将10写入a的地址
  builder->create_store(CONST_INT(0), iAlloca);  //将0写入i的地址
  
  
  auto condBB = BasicBlock::create(module, "condBB", mainFun);  // cond分支,用于while循环的判断
  auto trueBB = BasicBlock::create(module, "trueBB", mainFun);    // true分支,条件为真时
  auto retBB = BasicBlock::create(module, "retBB", mainFun);  // ret分支,当循环条件不满足时
  builder->create_br(condBB); 
  
  builder->set_insert_point(condBB); // cond分支判断条件,分支的开始需要SetInsertPoint设置
  auto iLoad = builder->create_load(iAlloca);  //获取i地址中的值
  auto cmp = builder->create_icmp_lt(iLoad, CONST_INT(10));  //判断i是否小于10
  auto br = builder->create_cond_br(cmp, trueBB, retBB);  // 条件BR
   
  builder->set_insert_point(trueBB);  // if true; 
  auto aLoad = builder->create_load(aAlloca);  //从a地址读取a
  iLoad = builder->create_load(iAlloca);  //从i地址读取i
  auto add1=builder->create_iadd(iLoad,CONST_INT(1)); //进行i+1
  builder->create_store(add1, iAlloca); //把i+1的值写入i的地址
  iLoad = builder->create_load(iAlloca); //从i地址读取i
  auto add2=builder->create_iadd(iLoad,aLoad);//进行i+a
  builder->create_store(add2, aAlloca); //把i+a的值写入a的地址
  builder->create_br(condBB);  // br condBB
  
  builder->set_insert_point(retBB);  // ret分支
  aLoad = builder->create_load(aAlloca);  //从a地址读取a
  builder->create_store(aLoad,retAlloca ); //把a的值写入返回值的地址
  auto retLoad = builder->create_load(retAlloca);  //获取返回值地址中值
  builder->create_ret(retLoad);  //创建ret指令,返回

  std::cout << module->print();
  delete module;
  return 0;
}

结果验证:

输入指令make,编译通过后输入指令./stu_while_generator >while.ll

内容如下:

define i32 @main() {
    
      
label_entry:
  %op0 = alloca i32
  %op1 = alloca i32
  %op2 = alloca i32
  store i32 10, i32* %op1
  store i32 0, i32* %op2
  br label %label_condBB
label_condBB:                                                ; preds = %label_entry, %label_trueBB
  %op3 = load i32, i32* %op2
  %op4 = icmp slt i32 %op3, 10
  br i1 %op4, label %label_trueBB, label %label_retBB
label_trueBB:                                                ; preds = %label_condBB
  %op5 = load i32, i32* %op1
  %op6 = load i32, i32* %op2
  %op7 = add i32 %op6, 1
  store i32 %op7, i32* %op2
  %op8 = load i32, i32* %op2
  %op9 = add i32 %op8, %op5
  store i32 %op9, i32* %op1
  br label %label_condBB
label_retBB:                                                ; preds = %label_condBB
  %op10 = load i32, i32* %op1
  store i32 %op10, i32* %op0
  %op11 = load i32, i32* %op0
  ret i32 %op11
}

输入 lli while.ll; echo $?指令进行测试,结果如下:

在这里插入图片描述

结果为65,输出正确

创建了4个basicblock,对应关系为:

  1. auto bb = BasicBlock::create(module, "entry", mainFun);对应main函数中的标签label_entry
  2. auto condBB = BasicBlock::create(module, "condBB", mainFun);对应标签label_condBB
  3. auto trueBB = BasicBlock::create(module, "trueBB", mainFun);对应标签label_trueBB
  4. auto retBB = BasicBlock::create(module, "retBB", mainFun);对应标签label_retBB

问题2: Visitor Pattern

请指出visitor.cpp中,treeVisitor.visit(exprRoot)执行时,以下几个Node的遍历序列:numberA、numberB、exprC、exprD、exprE、numberF、exprRoot。

序列请按如下格式指明:

exprRoot->numberF->exprE->numberA->exprD

答:

visitor.cpp进行分析:

class AddSubNode : public Node {
    
        // Specific element class #1
 public:
  // Resolved at runtime, it calls the treeVisitor's overloaded function,
  // corresponding to AddSubNode.
  int accept(TreeVisitor& treeVisitor) override {
    
      
    return treeVisitor.visit(*this);
  }
  Node& leftNode;
  Node& rightNode;
  std::string op;
  AddSubNode(Node& left, Node& right, std::string op): leftNode(left), rightNode(right), op(op){
    
      }
};

对于加减结点,按照“左中右”的顺序为:左操作数(子结点),中间符号(当前结点),右操作数(子结点)。

class NumberNode : public Node {
    
        // Specific element class #2
 public:
  // Resolved at runtime, it calls the treeVisitor's overloaded function,
  // corresponding to NumberNode.
  int accept(TreeVisitor& treeVisitor) override {
    
      
    return treeVisitor.visit(*this);
  }
  int number;
  NumberNode(int number){
    
      
    this->number = number;
  }
};

而数字结点仅自身。

class MulDivNode : public Node {
    
        // Specific element class #3
 public:
  // Resolved at runtime, it calls the treeVisitor's overloaded function,
  // corresponding to MulDivNode.
  int accept(TreeVisitor& treeVisitor) override {
    
      
    return treeVisitor.visit(*this);
  }
  Node& leftNode;
  Node& rightNode;
  std::string op;
  MulDivNode(Node& left, Node& right, std::string op): leftNode(left), rightNode(right), op(op){
    
      }
};

对于乘除结点,按照“左中右”的顺序为:左操作数(子结点),中间符号(当前结点),右操作数(子结点)。

查看 visit 函数代码

int visit(AddSubNode& node) override {
    
      
    auto right = node.rightNode.accept(*this);
    auto left = node.leftNode.accept(*this);
    if (node.op == "add") {
    
      
      return left + right;
    } 
    else {
    
      
      return left - right;
    }
  }

对于加减操作,先遍历右子树再遍历左子树,如果操作符是‘add’就进行加操作,否则进行减操作

int visit(MulDivNode& node) override {
    
      
    auto left = node.leftNode.accept(*this);
    auto right = node.rightNode.accept(*this);
    if (node.op == "mul") {
    
      
      return left * right;
    } 
    else {
    
      
      return left / right;
    }
  }
};

对于乘除操作,先遍历左子树再遍历右子树,如果操作符是‘mul’进行乘操作,否则进行减操作。

int visit(NumberNode& node) override {
    return node.number;
  }

数字节点直接返回它的值

下面查看main函数

  // construct the expression nodes and the tree
  // the expression: 4 * 2 - 2 / 4 + 5
  auto numberA = NumberNode(4);
  auto numberB = NumberNode(2);
  auto exprC = MulDivNode(numberA, numberB, "mul");
  auto exprD = MulDivNode(numberB, numberA, "div");
  auto exprE = AddSubNode(exprC, exprD, "sub");
  auto numberF = NumberNode(5);
  auto exprRoot = AddSubNode(exprE, numberF, "add");

实际上就自底向上构建了一个树:

在这里插入图片描述

然后调用visit函数:

 TreeVisitorCalculator treeVisitor;
  // traverse the tree and calculate
  int result = treeVisitor.visit(exprRoot);
  std::cout << "4 * 2 - 2 / 4 + 5 evaluates: " << result << std::endl;

进入visit函数的具体访问过程如下:

  1. 根节点 exprRoot 是AddSubNode,按照访问顺序先访问右子树,即numberF
  2. numberF是NumberNode,直接返回其节点的值5,故开始访问exprRoot的左子树
  3. 由于exprRoot的第一个左子节点exprE是AddSubNode,所以要遍历完AddSubNode才能返回结果,下面访问exprE的右子树
  4. exprE的右子节点是exprD,是MulDivNode,所以先访问左子树,即numberB,是NumberNode,返回其值2
  5. 接着遍历exprD的右子树,即numberA,是NumberNode,返回其值4,得到两个子树的值后将计算结果2/4存在exprD中
  6. 接着访问exprE的左子树,即exprC,是MulDivNode,所以先遍历左子树,是numberA,是NumberNode,返回其值4
  7. 接着访问exprC的右子树,即numberB,是NumberNode,返回其值2,接着将运算结果2*4的值存在exprC中
  8. 得到了exprC的值(值为8)和exprD的值(值为0.5),回到上一层将exprC-exprD的值存入exprE中
  9. 得到了exprE的值和numberF的值,将exprE+numberF的值存入根节点,遍历结束,最终结果为13(这里有个小细节,计算结果是12.5,但是结果使用int型保存,所以四舍五入为13)

故最终的访问顺序如下:exprRoot->numberF->exprE->exprD->numberB->numberA->exprC->numberA->numberB

问题3: getelementptr

请给出IR.md中提到的两种getelementptr用法的区别,并稍加解释:

  • %2 = getelementptr [10 x i32], [10 x i32]* %1, i32 0, i32 %0
  • %2 = getelementptr i32, i32* %1 i32 %0

答:

  • 概念:getelementptr指令用于获取数组结构的元素的地址。 它仅执行地址计算,并且不访问内存。
  • 格式: <result> = getelementptr <type>, <type>* <ptrval> [, <type> <idx>]
  • 参数解释:第一个参数是计算基础类型,第二第三个参数表示索引开始的指针类型及指针,[]表示可重复参数,里面表示的数组索引的偏移类型及偏移值。
  1. %2 = getelementptr [10 x i32], [10 x i32]* %1, i32 0, i32 %0

下面结合一个例子来说明,这是assign.c

int main(){
    
      
  int a[10];
  a[0] = 10;
  a[1] = a[0] * 2;
  return a[1];
}

clang生成的.ll文件如下:

define i32 @main() #0 {
    
      
  %1 = alloca i32, align 4
  %2 = alloca [10 x i32], align 16
  store i32 0, i32* %1, align 4
  %3 = getelementptr inbounds [10 x i32], [10 x i32]* %2, i64 0, i64 0
  store i32 10, i32* %3, align 16
  %4 = getelementptr inbounds [10 x i32], [10 x i32]* %2, i64 0, i64 0
  %5 = load i32, i32* %4, align 16
  %6 = mul nsw i32 %5, 2
  %7 = getelementptr inbounds [10 x i32], [10 x i32]* %2, i64 0, i64 1
  store i32 %6, i32* %7, align 4
  %8 = getelementptr inbounds [10 x i32], [10 x i32]* %2, i64 0, i64 1
  %9 = load i32, i32* %8, align 4
  ret i32 %9
}

很明显%3 = getelementptr inbounds [10 x i32], [10 x i32]* %2, i64 0, i64 0这一条语句正是getelementptr用法的第一种,他对应c语言中的取出a[0]的地址空间。再结合上面的参数说明可知,第一种用法的指针的类型为[10 x i32]* ,指向的是数组a[],然后第二个i32 0代表的数组的首地址位置,即a[0],第三个i32 0代表的是从首地址开始的偏移量。即如果是这么表示%2 = getelementptr [20 x i32], [20 x i32]* %1, i32 2, i32 %0 ,即取出a[2+%0]的位置的地址空间赋给%2。

如果是二维数组a[10][10],要想对a[0][1]赋值,首先要得到其位置,用getelementptr表示为:

%3 = getelementptr inbounds [10 x [10 x i32]], [10 x [10 x i32]]* %2, i64 0, i64 0
%4 = getelementptr inbounds [10 x i32], [10 x i32]* %3, i64 0, i64 1

首先取出a[0][]的位置,赋给%3,再从a[0][]取出a[0][1]的位置赋给%4

三维数组以此类推

2.%2 = getelementptr i32, i32* %1 i32 %0

指针类型为i32* ,即int*,如果放在c中就代表一个一维数组的意思。偏移量为%0,也是获得一个数组a[%0]位置的方法

总结:这两种getelementptr实际上是针对不同的指针类型[10 x i32]*i32*,同样取出a[%0]的方法。

此外,这两种的用法实际上能算作是同一种用法,区别是在于前者是[10 x i32]*型,指针所指向的地址仍旧可以视为指针,从而除指针自身以[10 x i32]为单位的偏移外,还有偏移单位为i32的偏移;而后者是i32*,仅存在i32这一单位的偏移。

而且,这种不仅适用于数组的寻址,也适用于结构体、向量等数据组织方式的寻址,方法和数组的类似。

实验难点

  1. 在if_hand.ll给float a = 5.555;赋值。如果直接使用store float 5.555, float* %1会报错。 输入clang -S -emit-llvm if.c指令,看了一下样例,发现要写成浮点数的机器表达形式。 即

5.555的二进制表示:101.10001110000101000111101011100001010001111010111
尾数正规化 :1.0110001110000101000111101011100001010001111010111*22
E=2, exp = E + Bias =129,二进制表示为10000001
符号位为0
有效数字M等于 01100011100001010001111
故5.555的IEEE表示为
0 10000001 01100011100001010001111
转换为十六进制为0x40b1c28f
但样例给出的是0x40163851E0000000,后经验证发现样例给的是double型的IEEE标识,但程序和精简的IR Reference手册都说明了float是单精度的,所以我觉得这里还是有点问题

  1. 考察两种getelementptr用法除了查阅精简的IR Reference手册,还可以上网查阅其他资料,最重要的是可以利用clang生成不同的.ll文件进行比较验证

实验反馈

实验我认为最难的地方不在于实验本身,在于环境配置。一开始实验文档说llvm的版本要10.0.1,但是用指令下出来的是6.0版本,我就想先试着用一下先,当编译到fun_hand.ll时会报错,给同学的10.0.1版本的却不会报错,我就以为是llvm版本的问题,然后就下载10.0.1的版本,但是gitee上的方法都没啥用,第一个docker下载好了不懂怎么用,只能用第二个自己搭建的方法,但足足花费了4个小时来编译llvm!

实验本身还是挺有意思的,可以根据案例来仿写其他的,但是有些内容要一直重复写就会很烦,如果有错了找半天都找不到。