miniSysY 语言是在 SysY 语言 基础上进行一些修改得到的 C 语言子集。编译原理实验课剩下的工作就是从编译一个简单的 main() 函数开始,逐渐扩充对 miniSysY 语言文法的支持,实现一个较为完整的编译器。由于我们没有系统地学习过汇编语言,我们只需要将 miniSysY 代码编译到 LLVM IR。
在后续的实验中,你可以使用自动生成工具(如 ANTLR、flex/bison 等)来生成你的编译器前端代码,也可以选择手工实现前端的词法分析和语法分析代码。
关于评测:你需要从 $input 读取输入文件,将编译生成的文本格式的 LLVM IR 输出到 $ir 中,评测机会使用 lli 解释执行该文件,并评测生成的 IR 代码是否正确。请在 judge.toml 的 run 中使用 $input 代替输入文件路径,$ir 代替输出文件路径。如:./compiler < $input > $ir 或 ./compiler $input $ir 等。
miniSysY 实验共有 8 个 lab 和一个挑战实验。
8 个 lab 会以每周一个的速度放出,所有实验的评测和提交实验报告截止时间为第 17 周周末(2022 年 1 月 2 日)。期末会放出示例编译器。部分 lab 包括多个 part,只是为了方便你迭代实现和调试,在评测时你可以选择一部分已经实现的 part 进行评测,以降低等待时间。需要注意的是,你需要在一次评测中通过所有 part 的测试点才算完成一个 lab。
挑战实验选做其中一部分即可,得分不会溢出。为了便于你设计架构,挑战实验会提前放出内容。
- 仅有
main函数的编译器及注释(10%) - 常量表达式运算(10%)
- 局部变量定义与赋值(10%)
if语句与条件表达式(10%)- 作用域与全局变量(10%)
- 循环(10%)
- 数组(10%)
- 函数(10%)
- 挑战实验(20%)
mem2reg(20%)- 多维数组(10%)
- 函数内联(10%)
- 短路求值(10%)
为了便于你设计架构,现给出 miniSysY 的全部文法如下:
CompUnit -> [CompUnit] (Decl | FuncDef)
Decl -> ConstDecl | VarDecl
ConstDecl -> 'const' BType ConstDef { ',' ConstDef } ';'
BType -> 'int'
ConstDef -> Ident { '[' ConstExp ']' } '=' ConstInitVal
ConstInitVal -> ConstExp
| '{' [ ConstInitVal { ',' ConstInitVal } ] '}'
VarDecl -> BType VarDef { ',' VarDef } ';'
VarDef -> Ident { '[' ConstExp ']' }
| Ident { '[' ConstExp ']' } '=' InitVal
InitVal -> Exp
| '{' [ InitVal { ',' InitVal } ] '}'
FuncDef -> FuncType Ident '(' [FuncFParams] ')' Block
FuncType -> 'void' | 'int'
FuncFParams -> FuncFParam { ',' FuncFParam }
FuncFParam -> BType Ident ['[' ']' { '[' Exp ']' }]
Block -> '{' { BlockItem } '}'
BlockItem -> Decl | Stmt
Stmt -> LVal '=' Exp ';'
| [Exp] ';'
| Block
| 'if' '(' Cond ')' Stmt [ 'else' Stmt ]
| 'while' '(' Cond ')' Stmt
| 'break' ';'
| 'continue' ';'
| 'return' [Exp] ';'
Exp -> AddExp
Cond -> LOrExp
LVal -> Ident {'[' Exp ']'}
PrimaryExp -> '(' Exp ')' | LVal | Number
UnaryExp -> PrimaryExp
| Ident '(' [FuncRParams] ')'
| UnaryOp UnaryExp
UnaryOp -> '+' | '-' | '!' // 注:保证 '!' 仅出现在 Cond 中
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
ConstExp -> AddExp // 在语义上额外约束这里的 AddExp 必须是一个可以在编译期求出值的常量其中 Ident 和 Number 的详细定义见下。
miniSysY 语言中有两种注释,包括以 // 开头的单行注释和包裹在 /*、*/ 中的多行注释。
- 单行注释:以
//开始,直到换行符结束,不包括换行符。 - 多行注释:以
/*开始,直到第一次出现*/时结束,包括*/。
Ident -> Nondigit
| Ident Nondigit
| Ident Digit
Nondigit -> '_' | 'a' | 'b' | ... | 'z' | 'A' | 'B' | ... | 'Z'
Digit -> '0' | '1' | ... | '9'对于同名标识符的规定:
- 全局变量和局部变量的作用域可以重叠,重叠部分局部变量优先;
- 同名局部变量的作用域不能重叠;
- 变量名可以与函数名相同。
Number 可以表示八进制、十进制、十六进制数字,文法如下:
Number -> decimal-const | octal-const | hexadecimal-const
decimal-const -> nonzero-digit | decimal-const digit
octal-const -> '0' | octal-const octal-digit
hexadecimal-const -> hexadecimal-prefix hexadecimal-digit
| hexadecimal-const hexadecimal-digit
hexadecimal-prefix -> '0x' | '0X'
nonzero-digit -> '1' | '2' | ... | '9'
octal-digit -> '0' | '1' | ... | '7'
digit -> '0' | nonzero-digit
hexadecimal-digit -> '0' | '1' | ... | '9'
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f'
| 'A' | 'B' | 'C' | 'D' | 'E' | 'F'在将 Number 翻译成 LLVM IR 中的常量数字时,你需要注意进制的转换。输入保证 Number 转换成十进制后范围为 0 <= Number <= 2147483647,不会出现范围之外的数字。
- 一个 miniSysY 程序由单个文件组成,对应 EBNF 表示中的一个
CompUnit。在该CompUnit中,必须存在且仅存在一个标识为main、无参数、返回类型为int的FuncDef。 CompUnit的顶层变量/常量声明语句(对应Decl)、函数定义(对应FuncDef)都不可以重复定义同名标识符(Ident),即便标识符的类型不同也不允许。CompUnit的变量/常量/函数声明的作用域从该声明处开始到文件结尾。
- 全局变量声明中指定的初值表达式必须是常量表达式。
- 常量或变量声明中指定的初值要与该常量或变量的类型一致。
- 未显式初始化的局部变量,其值是不确定的;而未显式初始化的全局变量,其值均被初始化为 0。
ConstDef用于定义常量。在Ident后、=之前是可选的数组维度和各维长度的定义部分,在=之后是初始值。ConstDef的数组维度和各维长度的定义部分不存在时,表示定义单个变量。此时=右边必须是单个初始数值。ConstDef的数组维度和各维长度的定义部分存在时,表示定义数组。其语义和 C 语言一致,miniSysY 基础实验只要求支持一维数组和二维数组。比如[2*3][8/2]表示二维数组,第一和第二维长度分别为 6 和 4,每维的下界从 0 编号。ConstDef中表示各维长度的ConstExp都必须能在编译时求值到非负整数。在声明数组时各维长度都需要显式给出,而不允许是未知的。- 当
ConstDef定义的是数组时,=右边的ConstInitVal表示常量初始化器。全局常量数组的ConstInitVal中的ConstExp必须是常量表达式。局部常量数组的ConstInitVal中的ConstExp必须是能在编译时求值的int型表达式。 ConstInitVal初始化器必须是以下三种情况之一:- 一对花括号
{},表示所有元素初始为 0; - 数组维数和各维长度完全对应的初始值,如
int a[3] = {1, 2, 3};、int a[3][2] = { {1, 2}, {3, 4}, {5, 6} };; - 花括号中初始值少于对应维度元素个数,该维其余部分将被隐式初始化为 0,如
int a[5] = {1, 2};、int a[4][3] = { {1, 2, 3}, {4, 5}, {} };。
- 一对花括号
VarDef用于定义变量。当不含有=和初始值时,其运行时实际初值未定义。VarDef的数组维度和各维长度的定义部分不存在时,表示定义单个变量。存在时,和ConstDef类似,表示定义数组。VarDef中表示各维长度的ConstExp必须能求值到非负整数。- 全局变量数组的
InitVal中的Exp必须是常量表达式。局部变量数组InitVal中的Exp可以是任何符合语义的表达式。
FuncFParam定义一个函数的一个形式参数。当Ident后面的可选部分存在时,表示该形式参数为一个数组。- 当
FuncFParam为数组定义时,其第一维的长度省去(用方括号[]表示),而第二维则需要用表达式指明长度,长度是编译时可求值的常量表达式。 - 函数实参的语法是
Exp。对于int类型的参数,遵循按值传递;对于数组类型的参数,则形参接收的是实参数组的地址,并通过地址间接访问实参数组中的元素。 - 可以将二维数组的一部分传到形参数组中,如定义了
int a[4][3],可以将a[1]作为一个包含三个元素的一维数组传递给类型为int[]的形参。
FuncDef表示函数定义。其中的FuncType指明返回类型。- 当返回类型为
int时,函数内所有分支都应当含有带有Exp的return语句。不含有return语句的分支的返回值未定义; - 当返回值类型为
void时,函数内只能出现不带返回值的return语句。
- 当返回类型为
Block表示语句块。语句块会创建作用域,语句块内声明的变量的生命周期在该语句块内。- 语句块内可以再次定义与语句块外同名的变量或常量(通过
Decl语句),其作用域从定义处开始到该语句块尾结束,它隐藏语句块外的同名变量或常量。
Stmt中的if类型语句遵循就近匹配。- 单个
Exp可以作为Stmt。Exp会被求值,所求的值会被丢弃。
LVal表示具有左值的表达式,可以为变量或者某个数组元素。- 当
LVal表示数组时,方括号个数必须和数组变量的维数相同。
Exp代表int类型的表达式,Cond代表条件表达式。单目运算符!只会在Cond中出现。LVal必须是当前作用域内、该Exp语句之前有定义的变量或常量;对于赋值号左边的LVal必须是变量。- miniSysY 算符的优先级与结合性与 C 语言一致,文法中已经体现出了优先级与结合性的定义。
Cond中的短路求值在基础实验中不作要求,挑战实验中会有相关内容。
在 lab3 及之后的 lab 中,评测样例会调用 miniSysY 运行时库的函数来完成输入输出。运行时库提供一系列 I/O 函数,用于在程序中表达输入/输出需求,这些库函数不用在程序中声明即可调用,因此在 lab3 之后,你的编译器需要支持在调用这些库函数时直接翻译成对应的 LLVM IR 形式的调用,但不需要检查这些库函数参数的合法性。评测时评测机会将运行时库链接并进行评测,在本地调试时怎样链接运行时库会在后续实验中说明。
int getint();:输入一个整数,返回对应的整数值。int n; n = getint();
int getch();:输入一个字符,返回字符对应的 ASCII 码值。int n; n = getch();
int getarray(int []);:输入一串整数,第 1 个整数代表后续要输入的整数个数,该个数通过返回值返回;后续的整数通过传入的数组参数返回。getarray()不会检查调用者提供的数组是否有足够的空间容纳输入的一串整数。int a[10][10]; int n; n = getarray(a[0]);
void putint(int);:输出一个整数的值。int n = 10; putint(n); putint(11);
void putch(int);:输出一个 ASCII 码对应的字符。传入的整数参数取值范围为0~255。int n = 10; putch(n);
void putarray(int, int[]);第 1 个参数表示要输出的整数个数(假设为 N),后面应该跟上要输出的 N 个整数的数组。putarray()在输出时会在整数之间安插空格。int n = 2; int a[2] = {2, 3}; putarray(n, a);