LLVM IR部分常用语法——支持Sysy2022中间代码生成

Posted by Linexus on Monday, May 1, 2023

LLVM IR常用语法

加减乘除指令

1.加法指令(add)

加法指令用于对两个数值进行相加。在 LLVM 中,加法指令的语法如下所示:

%result = add <type> <value1>, <value2>

例如,如果我们想将两个整数相加并得到一个整数结果,可以使用以下指令:

%result = add i32 1, 2
%x = add i32 2, 3
%x = add i32 %a, %b
%z = add i32 %x, %y

这里,<type>指定为i32<value1>为整数值1<value2>为整数值2<result>为整数类型i32。各种类型的内存空间大小(以位为单位)如下:

在LLVM中,add指令的<type>参数指定了<value1><value2>的类型,同时也指定了<result>的类型。支持的类型包括:

  • 整数类型:i1, i8, i16, i32, i64, i128等;
  • 浮点类型:half, float, double, fp128等;
  • 向量类型:<n x i8>, <n x i16>, <n x i32>等;
  • 指针类型:i8*, i32*, float*等;
  • 标签类型:metadata

浮点加法

加法指令还有一种形式,可以用于计算两个浮点数之间的差值。语法为:

%result = fadd <type> <value1>, <value2>

2.减法指令(sub)

减法指令用于对两个数值进行相减,语法为:

%result = sub <type> <value1>, <value2>

其中,<type> 表示要进行减法运算的值的数据类型,可以是整数、浮点数等;<value1><value2> 分别表示相减的两个数,可以是常量、寄存器或者其他指令的结果。

下面是一个减法指令的代码示例,将两个整数相减:

%diff = sub i32 %x, %y

浮点减法

减法指令还有一种形式,可以用于计算两个浮点数之间的差值。语法为:

%result = fsub float <value1>, <value2>

3. 乘法指令(mul)

乘法指令用于对两个数值进行相乘,语法为:

%result = mul <type> <value1>, <value2>

下面是一个乘法指令的代码示例,将两个整数相乘:

%prod = mul i32 %x, %y

浮点乘法

减法指令还有一种形式,可以用于计算两个浮点数之间的差值。语法为:

%result = fmul float <value1>, <value2>

4.除法指令(div)

除法指令用于对两个数值进行相除,语法为:

%result = <u/s>div <type> <value1>, <value2>

其中,表示要执行有符号(sdiv)还是无符号(udiv)的除法运算;表示要进行除法运算的值的数据类型,可以是整数、浮点数等;和分别表示相除的两个数,可以是常量、寄存器或者其他指令的结果。

下面是一个除法指令的代码示例,将两个整数相除:

%quot = sdiv i32 %x, %y

如果要进行无符号除法运算,可以使用 udiv 指令:

%quot = udiv i32 %x, %y

浮点除法

<result> = fdiv <ty> <op1>, <op2>

fdiv指令返回其两个操作数的商。示例:

%result = fdiv float 4.0, %var  

模运算指令 (rem)

<result> = <u/s>rem <ty> <op1>, <op2>

urem指令返回其两个参数的无符号除法的余数。srem指令返回其两个操作数的有符号除法的余数。使用该指令的示例:

%result = urem i32 4, %var
%result = srem i32 4, %var

位运算指令

IR有多种位运算指令,包括位与(and)、位或(or)、位异或(xor)、位取反(not)等。这些指令可以对整数类型进行按位操作,并将结果存储到一个新的寄存器中。以下是 IR 中常见的位运算指令及其作用:

这些指令都可以用类似的语法进行使用,其中 <type> 表示要进行位运算的整数的数据类型,可以是 i1、i8、i16、i32、i64 等;<value1><value2> 分别表示要进行位运算的整数,可以是常量、寄存器或其他指令的结果。例如:

%result = and i32 %x, %y
%result = or i32 %x, %y
%result = xor i32 %x, %y
%result = xor i32 %x, -1

shl指令

shl指令是左移运算符,将op1向左按位移动opt2位。如果opt2的位数大于或等于opt1的位数。

<result> = lshr <ty> <op1>, <op2>         ; yields ty:result
<result> = lshr exact <ty> <op1>, <op2>   ; yields ty:result 

下面是例子:

<result> = shl i32 4, %var   ; yields i32: 4 << %var
<result> = shl i32 4, 2      ; yields i32: 16
<result> = shl i32 1, 10     ; yields i32: 1024
<result> = shl i32 1, 32     ; undefined

lshr指令

lshr指令是右移运算符,将opt2向右按位移动opt1位。如果opt2的位数大于或等于opt1的位数。

<result> = lshr <ty> <op1>, <op2>         ; yields ty:result
<result> = lshr exact <ty> <op1>, <op2>   ; yields ty:result 

下面是例子:

<result> = lshr i32 4, 1   ; yields i32:result = 2
<result> = lshr i32 4, 2   ; yields i32:result = 1
<result> = lshr i8  4, 3   ; yields i8:result = 0
<result> = lshr i8 -2, 1   ; yields i8:result = 0x7F
<result> = lshr i32 1, 32  ; undefined

ashr指令

ashr指令是算数右移运算符,将opt2向右按位移动opt1位。如果opt2的位数大于或等于opt1的位数。

<result> = ashr <ty> <op1>, <op2>         ; yields ty:result
<result> = ashr exact <ty> <op1>, <op2>   ; yields ty:result 

这里注意区分算数右移和逻辑右移的区别,算术右移在最高位要补符号位的,而逻辑右移最高位都是零。下面是例子:

<result> = ashr i32 4, 1   ; yields i32:result = 2
<result> = ashr i32 4, 2   ; yields i32:result = 1
<result> = ashr i8  4, 3   ; yields i8:result = 0
<result> = ashr i8 -2, 1   ; yields i8:result = -1
<result> = ashr i32 1, 32  ; undefined

转换指令

2.zext

zext指令将一个整数或布尔值的位数增加,新位数的高位都填充为零,即进行零扩展。zext指令的使用格式如下:

%result = zext <source type> <value> to <destination type>

例如,下面的代码将一个8位整数扩展为16位整数:

%short = add i8 1, 2
%long = zext i8 %short to i16

3.sext

sext指令将一个整数的位数增加,新位数的高位都填充为原有的最高位,即进行符号扩展。sext指令的使用格式与zext指令类似:

%result = sext <source type> <value> to <destination type>

例如,下面的代码将一个8位整数扩展为16位整数:

%short = add i8 -1, 2
%long = sext i8 %short to i16

4.fptosi

fptosi指令将一个浮点数转换为一个带符号整数。转换时,如果浮点数的值超出了目标类型的表示范围,则结果为该类型的最小值或最大值。fptosi指令的使用格式如下:

%result = fptosi <source type> <value> to <destination type>

例如,下面的代码将一个双精度浮点数转换为32位带符号整数:

%double = fadd double 1.0, -2.0
%i32 = fptosi double %double to i32

5.sitofp

sitofp指令将一个带符号整数转换为一个浮点数。sitofp指令的使用格式如下:

%result = sitofp <source type> <value> to <destination type>

例如,下面的代码将一个32位带符号整数转换为单精度浮点数:

%i32 = add i32 1, -2
%float = sitofp i32 %i32 to float

6.bitcast

bitcast指令将一个值的位表示转换为另一个类型的位表示,但是它不会改变值本身。bitcast指令的使用格式如下:

%result = bitcast <source type> <value> to <destination type>

例如,下面的代码将一个64位双精度浮点数转换为64位整数类型:

%double = fadd double 1.0, -2.0
%i64 = bitcast double %double to i64

内存指令

1.alloca

alloca指令用于在栈上分配内存,并返回一个指向新分配的内存的指针。alloca指令的使用格式如下:

%ptr = alloca <type>

其中,<type>是要分配的内存块的类型。例如,下面的代码分配一个包含5个整数的数组:

%array = alloca [5 x i32]

2.load

load指令用于从内存中读取数据,并将其加载到寄存器中。load指令的使用格式如下:

%val = load <type>* <ptr>

其中,<type>是要读取的数据的类型,<ptr>是指向要读取数据的内存块的指针。例如,下面的代码将一个整数数组的第一个元素加载到寄存器中:

%array = alloca [5 x i32]
%ptr = getelementptr [5 x i32], [5 x i32]* %array, i32 0, i32 0
%val = load i32, i32* %ptr

3.store

store指令用于将数据从寄存器中写入内存。store指令的使用格式如下:

store <type> <val>, <type>* <ptr>

其中,<type>是要写入的数据的类型,<val>是要写入的数据的值,<ptr>是指向要写入数据的内存块的指针。例如,下面的代码将一个整数存储到一个整数数组的第一个元素中:

%array = alloca [5 x i32]
%ptr = getelementptr [5 x i32], [5 x i32]* %array, i32 0, i32 0
store i32 42, i32* %ptr

4.getelementptr

getelementptr指令用于计算指针的偏移量,以便访问内存中的数据。getelementptr指令的使用格式如下:

%ptr = getelementptr <type>, <type>* <ptr>, <index type> <idx>, ...

其中,<type>是指针指向的数据类型,<ptr>是指向数据的指针,<index type>是索引的类型,<idx>是索引的值。getelementptr指令可以接受多个索引,每个索引都可以是任意类型的。索引类型必须是整数类型,用于计算偏移量。例如,下面的代码计算一个二维数组中的一个元素的指针:g

%array = alloca [3 x [4 x i32]]
%ptr = getelementptr [3 x [4 x i32]], [3 x [4 x i32]]* %array, i32 1, i32 2

在这个例子中,%array是一个二维数组,%ptr是指向第二行第三列元素的指针。

5.memset

memset指令用于将一段内存区域的内容设置为指定的值。它的基本语法如下:

call void @llvm.memset.p0i8.i64(i8* %dst, i8 %val, i64 %size, i1 0)

其中,第一个参数%dst是要设置的内存区域的起始地址,它应该是指针类型。第二个参数%val是要设置的值,它应该是整型。第三个参数%size是内存区域的大小,它应该是64位整型。最后一个参数是一个布尔值,表示对齐方式。如果它为1,表示按照指针类型对齐;如果它为0,表示不按照指针类型对齐。

下面是一个简单的使用示例,将一个整型数组中的所有元素都设置为0:

define void @set_to_zero(i32* %array, i32 %size) {
entry:
  %zero = alloca i32, align 4
  store i32 0, i32* %zero, align 4
  %array_end = getelementptr i32, i32* %array, i32 %size
  call void @llvm.memset.p0i8.i64(i8* %array, i8 0, i64 sub(i32* %array_end, %array), i1 false)
  ret void
}

控制指令

1.条件分支指令(br)

br指令用于执行条件分支,根据条件跳转到不同的基本块。它的语法如下:

br i1 <cond>, label <iftrue>, label <iffalse>

其中<cond>是条件值,如果其值为真,则跳转到标记为<iftrue>的基本块;否则跳转到标记为<iffalse>的基本块。下面是一个简单的示例:

define i32 @test(i32 %a, i32 %b) {
  %cmp = icmp eq i32 %a, %b
  br i1 %cmp, label %equal, label %notequal

equal:
  ret i32 1

notequal:
  ret i32 0
}

2.函数返回指令 (ret)

ret指令用于从函数中返回一个值。它的语法如下:

ret <type> <value>

其中,<type>是返回值的类型,<value>是返回的值。如果函数没有返回值,则<type>应该是void。下面是一个示例:

define i32 @test(i32 %a, i32 %b) {
  %sum = add i32 %a, %b
  ret i32 %sum
}

3.置反指令 (fneg)

<result> = fneg [fast-math flags]* <ty> <op1>

fneg指令返回其操作数的否定值。例子:

%result = fneg float %val 

其他指令

1.phi

phi指令用于在基本块之间传递值。它的语法如下:

%result = phi <type> [ <value1>, <label1> ], [ <value2>, <label2> ], ...

其中,<type>是要传递的值的类型,<value1>是要传递的第一个值,<label1>是要从中传递第一个值的基本块。其他的<value><label>对也类似。下面是一个示例:

define i32 @test(i32 %a, i32 %b) {
  %cmp = icmp slt i32 %a, %b
  br i1 %cmp, label %if_true, label %if_false

if_true:
  %result1 = add i32 %a, 1
  br label %merge

if_false:
  %result2 = add i32 %b, 1
  br label %merge

merge:
  %result = phi i32 [ %result1, %if_true ], [ %result2, %if_false ]
  ret i32 %result
}

在这个示例中,我们定义了一个函数test,它的功能是比较ab的值,并返回一个结果。在标记%if_true处,我们使用add指令计算a+1的值;在标记%if_false处,我们使用add指令计算b+1的值。然后,在标记%merge处,我们使用phi指令选择一个值。具体来说,如果%cmp的值为true,我们就选择%result1的值(即a+1);否则,我们就选择%result2的值(即b+1)。

2.call

call指令用于调用函数。它的语法如下:

%result = call <type> <function>(<argument list>)

其中,<type>是函数返回值的类型,<function>是要调用的函数的名称,<argument list>是函数参数的列表。下面是一个示例:

declare i32 @printf(i8*, ...)
define i32 @test(i32 %a, i32 %b) {
  %sum = add i32 %a, %b
  %format_str = getelementptr inbounds [4 x i8], [4 x i8]* @.str, i64 0, i64 0
  call i32 (i8*, ...) @printf(i8* %format_str, i32 %sum)
  ret i32 %sum
}

3.icmp和fcmp

icmp指令的操作数类型是整型或整型向量、指针或指针向量。对于指针或指针向量,在做比较运算的时候,都会将其指向的地址值作为整型数值去比较,所以归根结底也还是整型。

fcmp指令要求操作数是浮点值或者浮点向量,这个没有指针类型。

<result> = icmp <cond> <ty> <op1>, <op2>   ; yields i1 or <N x i1>:result
<result> = fcmp <cond> <ty> <op1>, <op2>     ; yields i1 or <N x i1>:result

<cond>这是比较规则,icmpfcmp指令的比较规则不一样。

icmp比较规则:

  1. eq:相等
  2. ne:不等于
  3. ugt:无符号大于
  4. uge:无符号大于或等于
  5. ult:无符号小于
  6. ule:无符号小于或等于
  7. sgt:有符号大于
  8. sge:有符号大于或等于
  9. slt:有符号小于
  10. sle:有符号小于或等于

fcmp比较规则:

  1. false:不比较,总是返回false
  2. oeq:有序且相等
  3. ogt:有序且大于
  4. oge:有序且大于或等于
  5. olt:有序且小于
  6. ole:有序且小于或等于
  7. one:有序且不相等
  8. ord:有序(无 nans)
  9. ueq:无序或相等
  10. ugt:无序或大于
  11. uge:无序或大于等于
  12. ult:无序或小于
  13. ule:无序或小于等于
  14. une:无序或不相等
  15. uno:无序(nans)
  16. true: 不比较,总是返回 true

有序意味着两个操作数都不是QNAN,而无序意味着任何一个操作数都可能是QNAN

QNAN是一个计算机术语,表示不是一个数字(Not a Number),它是一种特殊的浮点数,用于表示未定义或不可表示的值,例如0除以0,负数的平方根,或者无穷大减去无穷大等。

下面是例子:

icmp:
<result> = icmp eq i32 4, 5          ; yields: result=false
<result> = icmp ne float* %X, %X     ; yields: result=false
<result> = icmp ult i16  4, 5        ; yields: result=true
<result> = icmp sgt i16  4, 5        ; yields: result=false
<result> = icmp ule i16 -4, 5        ; yields: result=false
<result> = icmp sge i16  4, 5        ; yields: result=false
fcmp:
<result> = fcmp oeq float 4.0, 5.0    ; yields: result=false
<result> = fcmp one float 4.0, 5.0    ; yields: result=true
<result> = fcmp olt float 4.0, 5.0    ; yields: result=true
<result> = fcmp ueq double 1.0, 2.0   ; yields: result=false

函数语法

函数定义

在LLVM中,一个最基本的函数定义的样子我们之前已经遇到过多次,就是@main函数的样子:

define <ty> @<name>(<param>, ...) {
	...
	[ret <op>] 
}

在函数名之后可以加上参数列表,如:

define i32 @foo(i32 %a, i64 %b) {
	ret i32 0
}

一个函数定义最基本的框架,就是返回值(i32)+函数名(@foo)+参数列表((i32 %a, i64 %b))+函数体({ ret i32 0 })。

除了函数定义之外,还有一种情况十分常见,那就是函数声明。我们在一个编译单元(模块)下,可以使用别的模块的函数,这时候就需要在本模块先声明这个函数,才能保证编译时不出错,从而在链接时正确将声明的函数与别的模块下其定义进行链接。

函数声明也相对比较简单,就是使用declare关键词替换define

declare i32 @printf(i8*, ...)