diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..ec62a91 --- /dev/null +++ b/404.html @@ -0,0 +1,627 @@ + + + +
+ + + + + + + + + + + + + + +Accipit IR 的语法和结构由抽象语法定义.
+注意: 一般情况下,你的任务并不会涉及 parse Accipit IR,本文档的主要目的是让你熟悉 Accipit IR 的语法并方便你进行后续的 debug 和 test.
+以下记号会在定义抽象语法的语法规则时用到:
+sym ::= expr1 | expr2 | ... | exprn
,其中 expr 是任意合法的语法表达式.<sym>
.<empty>
标记.'a'
'string'
.[ character-set ]
,表示匹配集合任意一个且仅一个字符,合法的集合包括:单个字符 'c'
;一个范围内的字符 'c1'-'c2'
(c1 和 c2 之间的字符,包含两个端点);两个或多个字符的并 ['c1' 'c2' 'c3' ... 'cn']
.expr *
表示 0 个或多个符合 expr 字符串的拼接.expr +
表示 1 个或多个符合 expr 字符串的拼接.expr ?
表示 0 个或 1 个符合 expr 字符串的拼接.expr1 | expr2
表示任何符合 expr1 或者 expr2 的字符串.{ expr }
表示符合 expr 的字符串.expr1 expr2
表示两个字符串的拼接,第一个符合 expr1,第二个符合 expr2.以及一些高级的函数,他们不难使用上面的表达式定义:
+list(expr)
表示 0 个或者多个符合 expr 的字符串拼接.nonempty_list(expr)
表示 1 个或者多个符合 expr 的字符串拼接.separated_list(expr, character)
表示 0 个或者多个符合 expr 的字符串,其间以字符 character 分隔. 例如符合 separected_list(expr, ',')
的字符串有: <empty>
,expr
,expr,expr,expr
.separated_nonempty_list(expr, character)
含义类似 separated_list,但是不允许空字符串 <empty>
.由于本文档说明按模块划分,所以可能存在部分交叉引用
+以下的常用的符号在所有章节中都是共享的:
+digit ::= ['0'-'9']
+letter ::= ['a'-'z' 'A'-'Z']
+ident ::= ['%' '#' '@'] [<letter> '-' '_' '.'] [<letter> <digit> '_' '.']*
+ | <digit>+
+
digit 定义了数字字符集合.
+letter 定义了字母数字集合,简单起见只包含拉丁字母.
+ident 定义了标识符 (identifier) 集合,你可以理解为 Accipit IR 内部所使用各种不同结构,值的名字. +具体来说有两种命名习惯:
+%foo
@DivisionByZero
%a.really.long.identifier
.%12
@2
%0
.int_const ::= '-'? <digit>+
+none_const ::= 'none'
+unit_const ::= '()'
+const ::= <int_const> | <none_const> | <unit_const>
+
除了可以用上述具名或匿名的标识符来引用某个值,Accipit IR 还有常数值.
+int_const
定义了 32 位有符号整数常数,我们只考虑普通的十进制整数的文本形式.
none_const
是一个特殊的常数,用于 offset 指令(见下).
unit_const
是单值类型 ()
的常数.
符号 (symbol) 包含所有标识符,代表了中间代码中的变量,包括带有名称的和匿名的临时变量. +而 IR 中合法的值 (value) 既可以是符号,表示对应变量的值;也可以是常数,表示常数本身的值.
+下面我们定义 Accipit IR 的各个结构.
+Accipit IR 中的众多实体按类型区分,类型关乎到程序的合法性、执行的过程.
+ +i32
,32 位带符号整数.
单值类型 ()
,读作 unit,和 C 语言中的空类型 void 类似.
+()
类型只有一个值 ()
,并且用于表示没有说明有意义的值可以作为指令的结果、函数的返回值.
指针类型,由被指的类型 (pointee type) 加上后缀 * 表示.
+例如 i32 *
表示指向 i32
类型的指针类型.
函数类型,类似于函数声明,例如:
+i32
参数,一个 i32
返回值 fn(i32, i32) -> i32
.i32
返回值 fn() -> i32
.i32
参数,无返回值 fn(i32) -> ()
.fn(i32*) -> i32*
,接受一个 i32*
参数,返回一个 i32*
类型的返回值.Accipit IR 的代码由一系列指令 (instruction) 组成.
+valuebinding ::= 'let' <symbol> '=' {<binexpr> | <gep> | <fncall> | <alloca> | <load> | <store>}
+terminator ::= <jmp> | <br> | <ret>
+
所有的指令具有两种形式:
+<symbol>
,并给它赋值,即将右侧指令的值赋给左侧 <symbol>
代表的变量.注意
+出于某种神秘的原因,我们规定每个变量只能在定义的时候被赋值一次,也就是说,每条指令左侧的变量 <symbol>
在对应的作用域内要求是唯一的,至于为什么,你可以参考附录:从四元组到静态单赋值形式.
+所以,我们在语法上用 let
来暗示这一点.
+有一些相应的翻译技巧处理源代码出现多次赋值的情况,详细请看附录:SysY 结构与 Accipit IR 的对应
binop ::= 'add' | 'sub' | 'mul' | 'div' | 'mod' |
+ 'and' | 'or' | 'xor' |
+ 'lt' | 'gt' | 'le' | 'ge' | 'eq' | 'ne'
+binexpr ::= <binop> <value> ',' <value>
+
数值计算指令操作符中不包含单目运算符,例如 lnot (logic not) 和 neg (numeric negation),因为他们是多余的:
+not %src
等价于 xor %src: i32, -1
.lnot %src
等价于 eq %src, 0
.neg %src
等价于 sub 0, %src
.这种转换很容易在前端生成中间代码时实现,并且使得这些计算有一个统一的表示形式 (canonical form),这将有利于后端代码生成.
+接受两个 i32
类型操作数,返回一个 i32
类型的值.
alloca ::= 'alloca' <type> ',' <int_const>
+load ::= 'load' <symbol>
+store ::= 'store' <value> ',' <symbol>
+
alloca 指令的作用是为局部变量开辟栈空间,并获得一个指向 <type>
类型,长度为 <int_const>
的指针.
+可以理解为,在栈上定义一个数组 <type>[<int_const>]
,并获取数组首元素的地址.
+或者类比 C 代码 int *a = (int *)malloc(100 * sizeof(int))
, 对应 let %a = alloca i32, 100
,只不过 alloca 分配的是栈空间,返回的是栈上的地址.
load 指令接受一个指针类型 T*
的符号,返回一个 T
类型的值.
store 指令接受一个类型 T
的值,将其存入一个 T*
类型的符号,并返回 unit 类型的值.
offset 指令的语义比较复杂,它是用于计算地址偏移量的,我们也可以将它归为 Memory Instructions. +在大家比较熟悉的 C 语言中,可能涉及到高维数组、结构体等等寻址比较复杂的结构,这是 offset 尝试解决的问题.
+出于简化考虑,Accipit IR 都使用普通的指针类型表示一维数组和高维数组,数组上的偏移计算使用若干组 size 来标记每个维度上的大小,若干组 index 来标记每个维度上的偏移量:
+offset 指令有一个类型标注,用来表明数组中元素类型;
+一共有 2n + 1
个参数,其中第一个参数是一个指针,表示基地址;
+后 2n
个参数每两个一组, 每一组的形式为 [index < size]
其中 index 表示该维度上的偏移量,size 表示该维度的大小.
例如 C 语言中声明数组 int g[3][2][5]
,获取地址 &g[x][y][z]
时,可以使用 C 代码 (int *)g + x * 2 * 5 + y * 5 + z
,对应的 offset 指令为 offset i32, %g.addr, [%x < 3], [%y < 2], [%z < 5]
.
当然,可能会出现高位数组有一维不知道大小或者在单个指针上算偏移的情况,在这种情况下,对应的维度使用 none 标记:
+int g[][5]
获取地址 &g[x][y]
,可以使用 C 代码 (int *)g + y * 5
.
+对应 offset 指令为 offset i32, %g.addr, [%x < none], [%y < 5]
.
+其中 %g.addr
是数组首地址对应的 value,%x
和 %y
分别是数组下标 x
和 y
对应的 value.int *p
获取地址 p + 10
,对应 offset 指令为 offset i32, %p, [10 < none]
.
+其中,%p
是指针 p
对应的 value,10
是字面量 10 在对应的 value.为什么要有 size 这个参数作为一个下标的上界:
+假设基地址变量 <symbol>
是指针类型 T*,则要求标注的数组中元素类型 <type>
必须为 T.
假设,有 n
组偏移量和维度 [index_0 < size_0], [index_1 < size_1], ... [index_{n-1} < size_{n-1}]
,
+为了保证语义的合法性,所有 index 都必须是整数类型,且要求运行时的值必须为非负整数;所有 size 中只有 size_0
可以为 none
,除此之外必须是一个正整数常数.
fncall 指令进行函数调用,符号 <symbol>
必须是被调用的函数,后跟一个参数列表.
如果被调用的函数 <symbol>
是 fn(T_1, T_2, ..., T_{n-1}) -> T_0
类型,那么参数列表的参数必须依次为 T_1
T_2
... T_{n-1}
类型,这条指令讲返回一个 T_0
类型的值。
假设 <symbol>
没有参数,即为 fn() -> T
类型,那么可以不写参数列表,即 let %ret = call <symbol>
.
假设 <symbol>
的返回值是 unit,即 fn(T_1, T_2, ...) -> ()
类型,那么返回值也为 unit 类型.
br ::= 'br' <value> ',' 'label' <ident> ',' 'label' <ident>
+jmp ::= 'jmp' 'label' <ident>
+ret ::= 'ret' <value>
+
br 进行条件跳转,接受的 <value>
应当是 i32
类型.
+若为 true,跳转到第一个 <label>
标记的基本块起始处执行;
+若为 false,跳转到第二个 <label>
标记的基本块起始处执行.
jmp 进行无条件跳转,跳转到 <label>
标记的基本块起始处执行.
ret 进行函数范围,并将 <value>
作为返回值,返回值的类型应当与函数签名一致.
函数是一段连续的指令序列
+plist ::= separated_list({<ident> ':' <type>}, ',')
+fun ::= 'fn' <ident> '(' <plist> ')' '->' <type> {';' | <body>}
+
函数包含函数头以及可选的函数体.
+如果没有函数体,则以一个分号 ;
结尾,例如 fn getchar() -> i32;
.
函数头以关键字 fn
开头,包含函数命令和参数列表和返回值,参数列表必须显式地标出每个参数的类型.
函数体由一系列基本块 (basic block) 组成,每个基本块有一个标号 (label) 和基本块中的指令序列,其中最后一条指令必须是 terminator.
+下面是一个阶乘函数的例子: +
fn %factorial(%n: i32) -> i32 {
+%Lentry:
+ /* Create a stack slot of i32 type as the space of the return value.
+ * if n equals 1, store `1` to this address, i.e. `return 1`,
+ * otherwise, do recursive call, i.e. return n * factorial(n - 1).
+ */
+ let %ret.addr = alloca i32, 1
+ let %cmp = eq %n, 0
+ br %cmp, label %Ltrue, label %Lfalse
+%Ltrue:
+ let %6 = store 1, %ret.addr
+ jmp label %Lret
+%Lfalse:
+ let %9 = sub %n, 1
+ let %res = call %factorial, %9
+ let %11 = mul %9, %res
+ let %12 = store %11, %ret.addr
+ jmp label %Lret
+%Lret:
+ let %ret.val = load %ret.addr
+ ret %ret.val
+}
+
声明全局变量 <symbol>
,变量具有 <type>
类型和 <int_const>
.
和 alloca 类似,<type>
是全局变量可存储的元素类型,<symbol>
的类型是对应的指针类型 <type> *
.
例如:
+ +则 %a
为 i32*
类型,所指向的地址能存放 2 个 i32
类型的元素.
Accipit IR 中注释的规范与 C 语言类似, 如下:
+//
开始, 直到换行符结束, 不包括换行符./*
开始, 直到第一次出现 */
时结束, 包括结束处 */
.为方便起见,我们默认不会出现嵌套注释.
+TBD
+IR 的设计参考了以下课程与资料:
+flex/bison 默认从 stdin
读入输入, 由于 \n
也会视为输入的一部分, 你需要手动输入 EOF
结束, 这在 bash
上是 Ctrl+D
.
如果你想从文件里输入的话 (比如 ./compiler test.in
), 这里提供一个供参考的解决方法:
extern int yyparse();
+
+extern FILE* yyin;
+
+int main(int argc, char **argv) {
+ yyin = fopen(argv[1], "r");
+ yyparse();
+ return 0;
+}
+
alloca
指令但是没有 free
¶Accipit IR 的定位是平台无关的中间代码,在显式地表达前端语义的同时,在一些形式又接近底层的汇编(例如控制流跳转,指令的操作码等).
+alloca
指令的语义是分配栈上的空间,用于存放局部变量.
+alloca
指令告诉了编译器后端局部变量需要的空间,并在汇编中由函数的 prologue 部分完成,即 sub sp, sp, #size
.
+在函数体中,使用 sp + #offset
的形式访问局部变量的地址.
+因此在退出函数时,epilogue 部分复原栈指针即完成了释放局部变量空间的动作.
本条目包含一些阅读材料、前端语言和 IR 规范等辅助性内容
+ + + + + + + + + + + + + +提示:你可以跳过阅读这一部分,但是阅读该部分可能有助于你理解本实验中间代码的设计.
+常见的平台无关中间代码是线性的一串指令.
+在早期,指令的设计风格通常是四元组 (quads) 形式,例如 x = y binop z
.
+其中有操作码 binop
,两个源变量 y
和 z
,以及一个目标变量 x
,因此被称为“四元组”.
+一种可能的四元组风格 IR 设计形如下:
类型 | +格式 | +说明 | +
---|---|---|
数据流指令 | +x = y binop z |
+将y和z的双目运算结果存放到x中 | +
+ | x = #k |
+将常量k加载到x中 | +
+ | x = *y |
+将y所指向的值存放到x中 | +
+ | *x = y |
+将y的值存放到x指向的位置中 | +
对应的常见的实现方式如下:
+class Instruction {
+ // all possible opcode.
+ enum Opcode { ... };
+ Opcode op;
+ // id of destination variable.
+ int dst;
+ // id of first and second source variable.
+ int src0, src1;
+
+ // instructions connected as a linked list.
+ Instruction *next;
+}
+
其中 Opcode
是所有可能的指令操作码,指令的源变量 src0
和 src1
、指令的目标变量 dst
使用一个整数作为索引(或者使用一个变量名的字符串),next
以链表的形式顺序连接下一条指令.
四元组看似很简单,但是有一个比较严重的问题,就是不太方便做代码优化,请看下面这条例子:
+ +代码的优化经常需要追踪数据流,也就是追踪四元组中两个源变量的值是由哪条指令进行的赋值,又被哪些指令使用.
+我们一步一步看.
+首先是 y = a add 1
这条指令,似乎很显然,不是吗?
+源变量 a
和常数 1
.
但是遇到 x = y sub b
时,我们很快遇到了麻烦: 这条指令需要的 y
的值是哪里被赋值的,或者说 y
最新的值在哪里,是 y = a add 1
还是 y = x add b
?
接下来的 y = x add b
,源变量 b
还是上一条指令 x = y sub b
中用到的那个 b
吗?还是有其他指令为 b 赋了新值?
某一条和它们隔得很远的指令 result = x add y
,它们的 x
和 y
又从哪里来?
你会发现,我们一直需要知道某个源变量最新的赋值发生在哪里,这意味这:
+上述这种“某个源变量最新的赋值发生在哪里”关系被称为使用-定义链 (use-def chain),静态单赋值形式 (SSA) 的优点之一就在于它能较好地维护 use-def chain.
+SSA 的一个特点是每个变量仅赋值一次,如果存在某个多次赋值的变量 x
,你需要对它重命名,例如把第 0 次、第 1 次...第 n 次重新赋值重命名为 x.0
x.1
... x.n
,并且使用变量 x
也可以也必须准确地指明是重命名后的 x.0
x.1
... x.n
之中的哪个.
+因此,上面的代码需要写成:
经过这么一番改造,指令的实现大致如下所示:
+class Instruction {
+ // all possible opcode.
+ enum Opcode { ... };
+ Opcode op;
+
+ // first and second source variable.
+ // `use` of the `definition` of src0 and src1.
+ Instruction *src0, *src1;
+
+ // instructions connected as a linked list.
+ Instruction *next;
+}
+
进一步地,我们就可以直接忽略繁琐的重命名,直接简化为:
+// somewhere `a` is defined as `%0`
+%0 = ...
+// somewhere `b` is defines as `%1`
+%1 = ...
+
+%2 = %0 add 1
+%3 = %2 sub %1
+%4 = %3 add %1
+...
+
+%i_dont_know_the_number = %3 add %4
+
因此我们可以发现,SSA 风格的指令中,指令使用 (use) 的操作数 src0
和 src1
直接指向了变量的定义 (definition) 处,因此指令之间就像一个图一样标记了数据的流动。
+使用 SSA 风格相比四元组风格具有以下优点:
减少“复制”操作,例如这是四元组风格: +
+ SSA 语境下,由于指令就是指本身,指向指令t1 = #1
的指针实际上就是指向常数值 #1
,因此复制操作是冗余的,上面这段代码可表示为:
+
+方便优化,例如基于迭代的数据流分析可以直接沿着 use-def chain 追踪,这种方式是“稀疏”的,因为不用像经典方法那样在每个程序点维护一个包含所有变量的稠密集合.
+如果你有兴趣,可以阅读 Cliff Click 的 From Quads to Graphs,以及 SSA Book。
+ + + + + + + + + + + + + +在这一部分我们关注 SysY 语言最基本的结构.
+在 Accipit 中,你能看到两种形式的局部变量:
+前一种局部变量是顶层变量 (top level variable). +取这个名字,是因为他们在 Accipit IR 中定义了一个新的符号,这个符号在定义时被赋的值就是变量的值:
+/// SysY source code:
+/// int result = lhs + rhs;
+/// `lhs` and `rhs` are local variables.
+let %result = add %lhs, %rhs
+
在 RISC-V 和 ARM 之类的 RISC 指令集中,指令集的操作数和目标数都只能是寄存器.
+想象一下,假如上面这段 IR 最后翻译到 RISC-V 汇编为 add t0,t1, t2
,那么 %result
对应目标寄存器 t0
,%lhs
和 %rhs
分别对应源寄存器 t1
和 t2
.
+源操作数 %lhs
和 %rhs
的值就是局部变量 lhs
和 rhs
的值,结果 %result
就是加法的值,存放了源代码变量 result
的值.
+这种行为和真实的指令集中的寄存器有些类似,但是和有限数量的物理寄存器不同的是,IR 中的符号可以有无限多,也就是说对应的“局部变量”可以无限多,因此称其为“虚拟寄存器”.
后一种局部变量是取地址变量 (address taken variable).
+取这个名字,是因为他们不像顶层变量有一个新的符号,他们只有局部变量所对应的地址,只能通过 alloca
指令创建:
let %result.var.addr = alloca i32, 1
+let %lhs.var.addr = alloca i32, 1
+let %rhs.var.addr = alloca i32, 1
+
在这里我们通过 alloca
创建三个局部变量,由于这三个局部变量都是 i32
类型,因此得到的结果 %result.var.addr
%lhs.var.addr
和 %rhs.var.addr
这三个虚拟寄存器的值都是 i32*
类型,代表这三个局部变量的地址.
+我们并不知道这三个局部变量叫什么名字,也不关心这三个局部变量叫什么名字,我们只需要知道这三个局部变量的地址是什么:%result.var.addr
%lhs.var.addr
和 %rhs.var.addr
.
+通过这些地址,我们就能够对这些局部变量读写:
// read value from the address `%lhs.var.addr`
+let %lhs.var.load.0 = load %lhs.var.addr
+// write constant int `1` to address `%lhs.var.addr`
+let %3 = store 1, %lhs.var.addr
+// read again
+let %lhs.var.load.1 = load %rhs.var.addr
+
回想 SSA 形式的限制:“出于某种神秘的原因,我们规定每个变量只能在定义的时候被赋值一次”,如果你重复赋值,就会发生错误:
+ +但是,SysY 源代码中的局部变量都是可以多次赋值的,alloca
指令以及后一种取地址形式的局部变量是为了绕开 SSA 形式的限制,方便你实现“多次赋值”.
比较常见的作法是显式地使用 alloca
为所有变量分配栈空间,包括函数参数,当这些变量作为指令的操作数时,先使用一个 load
将他们读入临时变量(此时创建了一个新的 top level variable),然后将这个临时变量用于运算;当这些变量作为指令的目标时,使用一个 store
将代表指令结果的临时变量(运算结果对应的 top level variable)存入地址:
/// `lhs`, `rhs` and `result` are local variables, initialized by constant.
+int lhs = 1;
+int rhs = 2;
+int result = 0;
+// fist assignment to `result`
+result = lhs + rhs;
+// second assignment to `result`
+result = result + 1;
+
首先为局部变量分配栈空间:
+ +然后使用 store
指令完成这些局部变量的初始化:
let %store.lhs = store 1, %lhs.addr
+let %store.rhs = store 2, %rhs.addr
+let %store.result = store 0, %result.addr
+
store
产生的顶层变量/临时变量没什么意义,所以他们的符号可以是匿名的,简化为用数字表示的匿名值:
第一次赋值,操作数需要局部变量 lhs
和 rhs
,因此需要 load
指令读取它们的值:
同样的,load
产生的顶层变量/临时变量只是计算中的中间结果,是临时的值,所以他们的符号最好是匿名的,简化为用数字表示的匿名值.
赋值的目标变量是 result
,因此需要 store
将计算的中间结果写入地址:
第二次赋值,操作数需要局部变量 result
和 常数 1
,因此需要 load
指令读取 lhs
的值,常数会被内联到加法计算中:
同样的,load
产生的顶层变量/临时变量只是计算中的中间结果,是临时的值,所以他们的符号最好是匿名的,简化为用数字表示的匿名值.
赋值的目标变量是 result
,因此需要 store
将计算的中间结果写入地址:
这样我们就在不破坏 SSA 形式限制的情况下,完成了变量的多次赋值,你会发现,在这种处理模式下,运算 / load / store 所定义的 top level variable 往往是中间的临时变量. +完整的代码清单如下:
+// allocate
+let %lhs.addr = alloca i32, 1
+let %rhs.addr = alloca i32, 1
+let %result.addr = alloca i32, 1
+// initialize
+let %0 = store 1, %lhs.addr
+let %1 = store 2, %rhs.addr
+let %2 = store 0, %result.addr
+// result = lhs + rhs
+let %3 = load %lhs.addr
+let %4 = load %rhs.addr
+let %5 = add %3, %4
+let %6 = store %5, %result.addr
+// result = result + 1
+let %7 = load %result.addr
+let %8 = add %7, 1
+let %9 = store %8, %result.addr
+
或者你可以不关心名字,全部简写为:
+// allocate
+let %0 = alloca i32, 1
+let %1 = alloca i32, 1
+let %2 = alloca i32, 1
+// initialize
+let %3 = store 1, %0
+let %4 = store 2, %1
+let %5 = store 0, %2
+// result = lhs + rhs
+let %6 = load %0
+let %7 = load %1
+let %8 = add %3, %4
+let %9 = store %8, %2
+// result = result + 1
+let %10 = load %2
+let %11 = add %10, 1
+let %12 = store %11, %2
+
由于 SSA 形式的特性,常量不需要先“复制”给某一个临时的临时变量,而是直接内联在指令中:
+例如:
+ +变成:
+ +也就是说,指令哪里要用常量,你可以直接把常量插入在那个地方.
+一个函数原型,在 Accipit IR 中能翻译到等价的 fn
声明:
变成:
+ +由于没有函数体,函数参数的名字无关紧要,因此你也可以略去参数名,只保留参数类型:
+ +由于 SysY 的运行时库无需声明即可使用,你可以选择在读入的 SysY 源代码前先“拼接”上运行时库函数的声明,让你的编译器前端帮你完成从运行时库函数声明到 Accipit IR 函数声明的过程; +或者你可以手动在生成 Accipit IR 时构造并插入所有运行时函数的声明.
+类似于汇编,Accipit IR 由一连串指令构成,这些指令一个接一个地顺序执行. +指令按组划分为基本块,每个基本块的终结指令都代表控制流的转移.
+if-then-else
分支¶让我们来看一个简单的函数,其中包括一个控制流:
+ +首先,请铭记,控制流的转移是通过基本块之间的跳转实现的.
+基本块内是按顺序执行的指令序列,它们不改变控制流!
+只有基本块内的最后一条指令(终结指令)才能改变控制流,进行跳转.
+最常见的是条件跳转终结指令 br
,根据 %cond
决定控制流跳转到哪个 label:
和无条件跳转终结指令 jmp
,直接跳转到某个基本块:
fn max(%a: i32, %b: i32) -> i32 {
+%entry:
+ let %a.addr = alloca i32, 1
+ let %b.addr = alloca i32, 1
+ let %0 = store %a, %a.addr
+ let %1 = store %b, %b.addr
+ let %retval = alloca i32, 1
+ let %2 = load %a.addr
+ let %3 = load %b.addr
+ let %4 = gt %2, %3
+ br %4, label %btrue, label %bfalse
+%btrue: // preds = %2
+ let %5 = store %a, %retval
+ br label %end
+%bfalse: // preds = %2
+ let %6 = store %b, %retval
+ br label %end
+%end: // preds = %btrue, %bfalse
+ let %7 = load %retval
+ ret %7
+}
+
在上面这个例子中,有 4 个基本块.
+第一个是函数的入口,局部变量使用 alloca
分配栈空间.
+两个参数 %a
%b
使用 gt
指令比较大小,结果将作为 br
跳转的标志位.
+接下来根据不同分支的选择,%a
或者 %b
会被写入返回值临时变量的地址 %retval
.
+每个分支最后都会有一条无条件跳转 jmp
合并控制流到最后的基本块 %end
,返回值将从 %retval
读取并返回.
++SysY 官方的运行时库规范见这里. +
+
+编译实践课所使用的 SysY 运行时库和官方定义略有不同: 实践课的getch
定义了遇到EOF
时的行为, 同时计时函数的定义比官方定义更加简单.
SysY 运行时库提供一系列 I/O 函数, 计时函数等用于在 SysY 程序中表达输入/输出, 计时等功能需求. 由于 SysY 并不具备 include
和函数声明的语法, 这些库函数无需在 SysY 程序中声明, 即可在 SysY 的 函数中使用.
你可以从这里获取 SysY 运行时库的相关实现, 详情见仓库的 README.
+SysY 运行时库提供一系列 I/O 函数, 支持对整数, 字符以及一串整数的输入和输出.
+以下未被列出的函数将不会出现在任何 SysY 评测用例中.
+函数声明: int getint()
描述: 从标准输入读取一个整数, 返回对应的整数值. 如果未能读取到任何整数 (例如遇到了 EOF
), 则返回值未定义.
示例:
+ +函数声明: int getch()
描述: 从标准输入读取一个字符, 返回字符对应的 ASCII 码值. 如果读取到了 EOF
, 则返回 -1
.
示例:
+ +函数声明: int getarray(int[])
描述: 从标准输入读取一串整数, 其中第一个整数代表后续出现整数的个数, 该数值通过返回值返回; 后续的整数通过传入的数组参数返回.
++++
getarray
函数只获取传入数组的起始地址, 而不检查调用者提供的数组是否有足够的空间容纳输入的一串整数.
示例:
+ +函数声明: void putint(int)
描述: 输出一个整数的值.
+示例:
+ +将输出: 101010
.
函数声明: void putch(int)
描述: 将整数参数的值作为 ASCII 码, 输出该 ASCII 码对应的字符.
+++传入的整数参数取值范围应为 0 到 255,
+putch
不检查参数的合法性.
示例:
+ +将输出换行符.
+函数声明: void putarray(int, int[])
描述: 第 1 个参数指定了输出整数的个数 (假设为 N
), 第 2 个参数指向的数组中包含 N 个整数. putarray
在输出时会在整数之间安插空格.
+++
putarray
函数不检查参数的合法性.
示例:
+ +将输出: 2: 2 3
.
SysY 运行时库提供 starttime
和 stoptime
“函数”, 用于测量 SysY 中某段代码的运行时间. 在一个 SysY 程序中, 可以插入多对 starttime
, stoptime
调用, 以此来获得每对调用之间的代码的执行时长, 并在 SysY 程序执行结束后得到这些计时的累计执行时长.
你需要注意:
+starttime
和 stoptime
只会出现在课程提供的性能测试用例中.
starttime
, stoptime
不支持嵌套调用的形式, 即不支持:
这样的调用执行序列.
+下面分别介绍所提供的计时函数的访问接口.
+函数声明: void starttime()
.
描述: 开启计时器. 此函数应和 stoptime()
联用.
函数声明: void stoptime()
.
描述: 停止计时器. 此函数应和 starttime()
联用.
程序会在最后结束的时候, 整体输出每个计时器所花费的时间, 并统计所有计时器的累计值. 格式为 Timer#编号: 时-分-秒-微秒
.
示例:
+void foo(int n) {
+ starttime();
+ int i = 0;
+ while (i < n) {
+ // do something...
+ i = i + 1;
+ }
+ stoptime();
+}
+
+int main() {
+ starttime();
+ int i = 0;
+ while (i < 3) {
+ // do something...
+ i = i + 1;
+ }
+ stoptime();
+ foo(2);
+ return 0;
+}
+
输出 (仅作示例):
+ + + + + + + + + + + + + + +SysY语言是全国大学生计算机系统能力大赛中编译系统设计赛要实现的编程语言.
+你可以在这里找到正式的 SysY 的文法和语义约束. +本实验所使用的 SysY 语言和官方定义略有不同. 我们对SysY语言做了一些修改, 具体如下:
+float
类型, 即不需要实现浮点数类型. ConstDecl
,即不需要实现常量声明。同时数组仅支持整数字面量声明维度。InitVal
的嵌套, 即不需要实现数组的初始化.SysY 语言的文法采用扩展的 Backus 范式 (EBNF, Extended Backus-Naur Form) 表示, 其中:
+[...]
表示方括号内包含的项可被重复 0 次或 1 次.{...}
表示花括号内包含的项可被重复 0 次或多次.IDENT
, INT_CONST
这样的大写记号. 其余均为非终结符.SysY 语言的文法表示如下, CompUnit
为开始符号:
CompUnit ::= [CompUnit] (Decl | FuncDef);
+
+BType ::= "int";
+Decl ::= VarDecl;
+
+
+VarDecl ::= BType VarDef {"," VarDef} ";";
+VarDef ::= IDENT "=" InitVal
+ | IDENT {"[" INT_CONST "]"};
+InitVal ::= Exp;
+
+FuncDef ::= FuncType IDENT "(" [FuncFParams] ")" Block;
+FuncType ::= "void" | "int";
+FuncFParams ::= FuncFParam {"," FuncFParam};
+FuncFParam ::= BType IDENT ["[" "]" {"[" INT_CONST "]"}];
+
+Block ::= "{" {BlockItem} "}";
+BlockItem ::= Decl | Stmt;
+Stmt ::= LVal "=" Exp ";"
+ | Exp ";"
+ | Block
+ | "if" "(" Exp ")" Stmt ["else" Stmt]
+ | "while" "(" Exp ")" Stmt
+ | "break" ";"
+ | "continue" ";"
+ | "return" [Exp] ";";
+
+Exp ::= LOrExp;
+LVal ::= IDENT {"[" Exp "]"};
+PrimaryExp ::= "(" Exp ")" | LVal | Number;
+Number ::= INT_CONST;
+UnaryExp ::= PrimaryExp | IDENT "(" [FuncRParams] ")" | UnaryOp UnaryExp;
+UnaryOp ::= "+" | "-" | "!";
+FuncRParams ::= Exp {"," Exp};
+
+MulExp ::= UnaryExp | MulExp ("*" | "/" | "%") UnaryExp;
+AddExp ::= MulExp | AddExp ("+" | "-") MulExp;
+
+RelExp ::= AddExp | RelExp ("<" | ">" | "<=" | ">=") AddExp;
+EqExp ::= RelExp | EqExp ("==" | "!=") RelExp;
+LAndExp ::= EqExp | LAndExp "&&" EqExp;
+LOrExp ::= LAndExp | LOrExp "||" LAndExp;
+
SysY 语言中标识符 IDENT
(identifier) 的规范如下:
其中, identifier-nondigit
为下划线, 小写英文字母或大写英文字母; digit
为数字 0 到 9.
关于其他信息, 请参考 ISO/IEC 9899 第 51 页关于标识符的定义.
+对于同名标识符, SysY 中有以下约定:
+SysY 语言中数值常量可以是整型数 INT_CONST
(integer-const), 其规范如下:
数值常量的范围为 \([0, 2^{31} - 1]\), 不包含负号.
+SysY 语言中注释的规范与 C 语言一致, 如下:
+//
开始, 直到换行符结束, 不包括换行符./*
开始, 直到第一次出现 */
时结束, 包括结束处 */
.关于其他信息, 请参考 ISO/IEC 9899 第 66 页关于注释的定义.
+下面, 我们进一步给出 SysY 语言的语义约束.
+CompUnit
. 在该 CompUnit
中, 必须存在且仅存在一个标识为 main
, 无参数, 返回类型为 int
的 FuncDef
(函数定义). main
函数是程序的入口点.CompUnit
的顶层变量/常量声明语句 (对应 Decl
), 函数定义 (对应 FuncDef
) 都不可以重复定义同名标识符 (IDENT
), 即便标识符的类型不同也不允许.CompUnit
的变量/常量/函数声明的作用域从该声明处开始, 直到文件结尾.VarDef
用于定义变量. 当不含有 =
和初始值时, 其运行时实际初值未定义.VarDef
的数组维度和各维长度的定义部分不存在时, 表示定义单个变量; 存在时, 表示定义多维数组FuncFParam
定义函数的一个形式参数. 当 IDENT
后面的可选部分存在时, 表示定义数组类型的形参.FuncFParam
为数组时, 其第一维的长度省去 (用方括号 []
表示), 而后面的各维则需要用表达式指明长度, 其长度必须是常量.Exp
. 对于 int
或 bool
类型的参数, 遵循按值传递的规则; 对于数组类型的参数, 形参接收的是实参数组的地址, 此后可通过地址间接访问实参数组中的元素.int a[4][3]
, 则 a[1]
是包含三个元素的一维数组, a[1]
可以作为实参, 传递给类型为 int[]
的形参.FuncDef
表示函数定义. 其中的 FuncType
指明了函数的返回类型.int
或 bool
时, 函数内的所有分支都应当含有带有 Exp
的 return
语句. 不含有 return
语句的分支的返回值未定义.void
时, 函数内只能出现不带返回值的 return
语句.FuncDef
中形参列表 (FuncFParams
) 的每个形参声明 (FuncFParam
) 用于声明 int
类型的参数, 或者是元素类型为 int
的多维数组. FuncFParam
的语义参见前文.Block
表示语句块. 语句块会创建作用域, 语句块内声明的变量的生存期在该语句块内.Decl
语句), 其作用域从定义处开始到该语句块尾结束, 它覆盖了语句块外的同名变量或常量.Stmt ::= LVal "=" Exp ";"
+ | [Exp] ";"
+ | Block
+ | "if" "(" Exp ")" Stmt ["else" Stmt]
+ | "while" "(" Exp ")" Stmt
+ | "break" ";"
+ | "continue" ";"
+ | "return" [Exp] ";";
+
Stmt
中的 if
型语句遵循就近匹配的原则.Exp
可以作为 Stmt
. Exp
会被求值, 所求的值会被丢弃.LVal
表示具有左值的表达式, 可以为变量或者某个数组元素.LVal
表示数组时, 方括号个数必须和数组变量的维数相同 (即定位到元素). 若 LVal
表示的数组作为数组参数参与函数调用, 则数组的方括号个数可以不与维数相同.LVal
表示单个变量时, 不能出现后面的方括号.Exp
在 SysY 中代表 int
型表达式. 当 Exp
出现在表示条件判断的位置时 (例如 if
和 while
), 表达式值为 0 时为假, 非 0 时为真.LOrExp
, 当其左右操作数有任意一个非 0 时, 表达式的值为 1, 否则为 0; 对于 LAndExp
, 当其左右操作数有任意一个为 0 时, 表达式的值为 0, 否则为 1. 上述两种表达式均满足 C 语言中的短路求值规则.LVal
必须是当前作用域内, 该 Exp
语句之前曾定义过的变量或常量. 赋值号左边的 LVal
必须是变量.IDENT "(" FuncRParams ")"
, 其中的 FuncRParams
表示实际参数. 实际参数的类型和个数必须与 IDENT
对应的函数定义的形参完全匹配.