参考编译器介绍
参考了2020级学长的SysY编译器设计,其采用Java进行编写,使用LLVM IR作为中间代码
总体结构
总体结构按照编译顺序,即词法分析、语法分析、语义分析、代码生成四个部分,每一个部分单独设置文件包,然后在入口程序分别执行,即四个部分采用四遍运行。然后每一个部分接收上一个部分的输入,然后把输出传递给下一个部分。
接口设计
接口设计采用类似工厂模式的设计方法,即每一个部分都有一个入口程序,然后在入口程序中调用相应的分析器,分析器中调用相应的工厂类。
编译器总体设计
总体结构
常规地分为词法分析,语法分析,语义分析(符号表生成),代码生成(仅生成LLVM IR中间代码)几个阶段,每个阶段读一遍。
每个阶段的处理结果将保存在Data包下的static变量中,以便后面的阶段复用
出错处理也融合于前端的三个阶段中
接口设计
每个阶段对应一个分析器,向外提供analyze方法与分析器的单例getter,每个阶段会生成Data包中的各个中间成果的数据,并输出评测所需的结果至目标文件
错误处理方面,使用一个错误信息类CompileError和错误处理类CompileErrorHandler来规范化程序中出现的错误(包括错误种类,发生的行数以及规范化输出)
文件组织
| Compiler.java | config.json | error.txt | lexer.txt | llvm_ir.txt | parser.txt | symbol.txt | testfile.txt | | +---CompileErrors | CompileError.java | CompileErrorHandler.java | +---Datas | AstNode.java | MiddleData.java | SymbolTable.java | Token.java | +---Lexical | CheckTools.java | Lexer.java | +---LLVMGenerate | Instruction.java | LLVMer.java | | +---Semantic | Semanticer.java | \---Syntax CheckTools.java Parser.java
词法分析
编码前
实现功能
提取记录token、类别、所在行号列号和所在源文件
Class Token
- 信息数组(token, name, lineNumber,
columnNumber)
- enum TokenType记录token类别
Class Lexer
- token分析函数analyze(包括过滤空白符和调用自动机)
- next方法用自动机解析token
解析流程读到下一个token时,columnNumber++
读入字符流,从第一行开始走
不断调用next函数,过滤无效信息,返回第一个有效token
读到\n时,lineno++
语法分析
编码前
递归下降子程序法分析
- 在每个分析函数的末尾输出语法树节点信息
多产生式
- 分析每个产生式的First集来决定用哪条推导规则
左递归问题
- 主要是exp簇文法,将文法改写成右递归
- 此时语法树被修改,需要在每次解析下列文法的非最后一项时多输出一个左式的Exp
- 同时注意语法树结构已经被更改,一个xxExp的子节点并非迭代的,而是在同一层
MulExp → UnaryExp | MulExp ('*' | '/' | '%') UnaryExp AddExp → MulExp | AddExp ('+' | '−') MulExp RelExp → AddExp | RelExp ('<' | '>' | '<=' | '>=') AddExp EqExp → RelExp | EqExp ('==' | '!=') RelExp LAndExp → EqExp | LAndExp '&&' EqExp LOrExp → LAndExp | LOrExp '||' LAndExp
回溯问题
- 预读几个字符后即可决定采用哪个式子进行推导
- 仅在分析到终结符之后才moveToNextSym()(将分析指针移到下一个token)
编码后
对于文法
Stmt → LVal '=' Exp ';' | [Exp] ';'
由于Exp也可以推出LVal,故无法通过常规的预读终结符以回避回溯的方法来做
故在Stmt的解析中,若第一个读到的字符是一个Ident,则 预读一个非终结符(LVal),判断其后面的终结符是否为 ‘=’,若是则解析为
Stmt → LVal '=' Exp ';' ;否则解析为Stmt → Exp; (此时的Exp经过多步推导推导出来的是LVal)剩余的
Stmt → Exp; 的情况全部交给else处理(不严谨,但在规定的测试点范围内不会出错)重构:把 输出语法树信息 改成 建立语法树
AstNode数据结构:
public class AstNode extends MiddleData { // root of AstNode, <CompUnit> public static AstNode root; public String token; public ArrayList<AstNode> children = new ArrayList<AstNode>(); public int lineno; public AstNode(String token) { this.token = token; } public void addChild(AstNode child) { children.add(child); } /** * @Description 删除最后一个节点并返回,用于左递归文法改造 * 后需要在cur和最后节点中间插入一个AstNode的情况 **/ public void insertLast(AstNode nodeToInsert) { AstNode last = children.get(children.size() - 1); children.remove(children.size() - 1); children.add(nodeToInsert); nodeToInsert.addChild(last); } public void output(FileWriter writer) { try { for (AstNode child : children) { child.output(writer); } writer.write(this.token + "\n"); } catch(IOException e) { System.out.println(e.getMessage()); } } }
e.g. MulExp的语法树构建
private void MulExp(AstNode parent) { AstNode cur = new AstNode("<MulExp>"); UnaryExp(cur); while (curSym.tokenType == MULT || curSym.tokenType == DIV || curSym.tokenType == MOD) { // Because I shift Left Iteration to right one, // So I need to output one more <MulExp> after // each <UnaryExp> except the last one AstNode tmp = new AstNode("<MulExp>"); cur.insertLast(tmp); // output("<MulExp>"); TerminalSym(cur); UnaryExp(cur); } parent.addChild(cur); // output("<MulExp>"); }
语义分析
编码前
Symbol数据结构设计
public class Symbol { public int id; public int tableId; public String token; public SymbolType symType; public ConstType conType; public BType bType; public int value; // for var // for func public ReturnType rtType; public int paraCount; public ArrayList<FuncParameter> paras; } class FuncParameter { public Btype bType; public SymbolType symType; public String token; }
public SymbolTable{ public int id; public int parentId; public ArrayList<int> childrenId; public HashMap<String, Symbol> content; }
插入时检查作用域和重定义问题
符号表建立
分析时,维护一个当前的最大层数maxLayer、当前层数curLayer和当前层的父层数parentLayer
前序遍历Parser分析出来的的AST
- <Block>(FuncDef下遇到的第一个Block除外,此时在FuncFParams处迭代层数)
- 新建SymbolTable(maxLayer + 1, curLayer),更新当前层数信息
- <VarDecl>
- 检查当前层中是否有同名变量
- 判断数组or变量,然后在当前符号表中添加一条变量记录(new Symbol())
- <ConstDecl>
- 同上,增加常量纪录
- <FuncDef>
- 检查重定义
- 判断returnType,添加函数记录
- <FuncFParams>
- 层数迭代一次,然后准备记录参数
- 子<FuncFParam>:添加变量记录
- 由于除开全局变量和函数定义以外,仅<Block>会推导出<Decl>,故在进入<MainFuncDef>的<Block>之后推导<Stmt>时,只分析含有<Stmt>和<Block>的分支,否则直接跳出去
- Block
- ‘if’
- ‘for’
编码后
主要是对错误处理的修改,见下
错误处理
语义分析
注:此处说的First不是First集,而是当前AstNode的第一个直接子节点,其他序数词同理
- b: 名字重定义√
- 每次解析符号时,解析到Ident时判断当前层有无同名符号
- c: 未定义的名字√
- Stmt, ConstDef, InitVal, ConstInitVal, FuncRParam, PrimaryExp → Exp / ConstExp
- 对Exp和LVal → Ident进行迭代解析
- Exp: 迭代至UnaryExp时,若First是Ident则 在最外层 寻找该符号对应的函数是否存在,不在则报错Ident所在行号;其他情况继续迭代
- LVal: 若Fisrt是Ident,则 从当前层到最外层依次 找该符号存在与否,不在则报错
- d: 函数调用参数个数不匹配√
- UnaryExp → Ident时,若Ident存在,但它是个变量 或 此处调用所使用的形参个数不等于实参个数则报错
- e: 函数调用参数类型不匹配
- 编码前
- UnaryExp → Ident时,若Ident存在,则依次与Ident的Symbol中的参数的类型进行比较,但是不急着加入错误列表。若参数个数相同,则报错e,否则报错d
- 编码后
- 函数实参全是以Exp的形式存在的,需要特别判断其类型
- 定义全局的RParamVarType和RParamBType
- 通过向下迭代遍历实参来确定其VarType和BType
- 只需要判断实参对应的Exp的第一个变量即可,因为本实验规定不会出现Exp中变量类型不匹配的问题(如数组 + 变量)
- f: 无返回值函数存在不匹配的return语句√
- 定义funcSymbol和isInFunction记录当前遍历位置是否在函数内部
- 解析到Stmt时,若First是return且isInFunction == true 且 funcSymbol.rtType == VOID,则判断second(可能不存在)是否为Exp,是则报错f,报在return所在行号
- g: 有返回值函数缺少return√
- 定义bool hasReturn来记录当前函数是否有返回值
- 解析到Stmt时,若First是return且isInFunction == true 且 funcSymbol.rtType != VOID,则令hasReturn = true
- 在FuncDef中解析Block的时候,若遍历到”RBRACE }” 且 hasReturn == false则报错
- h: 不能改变常量的值√
- Stmt, FosStmt → LVal = …
- Stmt → ‘for’ …: 解析for时,迭代解析ForStmt
- 判断First是否为LVal,先判断错误c,若存在当前常量则判断是不是常量,是的话直接报错h(文法保证First是LVal的都是赋值语句)
- l: printf格式字符与表达式个数不匹配√
- Stmt → printf时,遍历子节点
- StringConst: 解析里面有多少个”%d”和”%c”,记录个数
- 统计遇到过多少个Exp
- 二者比较即可
- m: 非循环块中使用break和continue
- 定义loopDepth记录当前所在的循环层数
- loopDepth在进入for循环的Stmt时+1,离开(For语句遍历结束)时-1
- 若在loopDepth == 0时解析到break / continue,则报错m
中间代码生成
LLVM IR 简单实例
定义局部变量时, 实际上是直接分配一块内存空间(alloca),生成的变量其实是一个地址 (i32 *)
void函数也需要一个不带参数的return函数作为结束
LLVM IR结构

继承自Value的类只能作为值被使用
继承自User的类可以引用其他Value或User
满足SSA,一个变量(寄存器)只被赋值一次
接口设计
依旧是用递归下降子程序法,遍历一遍语法树(类似语义分析,但是我在语义分析设计时不是很系统化且没有考虑属性传递)
用一个
codeGenerate(AstNode node) 作为入口,根据node的token再分发到对应处理函数里属性传递 & 寄存器设计
对于综合属性,可以为语法树结点Astnode添加值寄存器(如果是常量,直接存值也行)valueReg和地址寄存器addressReg(变量取值和赋值时会用到)
综合属性的传递实际上是寄存器或常量的传递,后序遍历即可获取
// psudo generate(a.child); a.setPropertyX(a.child.propertyX);
常量表达式
√
编码前
对每一个Exp,迭代计算其子xxExp节点的值,并存在子节点的valueReg中(先给它赋一个新的寄存器值)然后根据op生成对应
然后写一个op转换器,生成对应的llvm运算指令
编码后
由于语法分析阶段中对双目表达式的特殊处理(左递归文法),一个xxExp的全部同级运算会处于同一层里,不是像SysY文法文档里写的那样
注意Character为转义字符的情况!
else if (first.token.startsWith("<Character>")) { // char String charStr = first.children.get(0).token.split("'")[1]; // CHRCON 'c' if (charStr.length() > 1 && charStr.charAt(0) == '\\') { int asciiCode = 0; switch (charStr.charAt(1)) { case 'a' -> asciiCode = 7; case 'b' -> asciiCode = 8; case 't' -> asciiCode = 9; case 'n' -> asciiCode = 10; case 'v' -> asciiCode = 11; case 'f' -> asciiCode = 12; case '"' -> asciiCode = 34; case '\'' -> asciiCode = 39; case '\\' -> asciiCode = 92; } node.setValueReg(Integer.toString(asciiCode)); } else { node.setValueReg(Integer.toString(charStr.charAt(0))); }
局部变量(无数组)初始化
√
编码前
VarDef / ConstDef: 判断其直接子节点的个数以区别var和array
注意:变量不使用值寄存器valueReg,一切存取都对地址寄存器操作
变量:开一个空间,分配地址寄存器,store初值 if exists(char的话要截断)
在Symbol类中加入地址寄存器编号属性,便于load的时候用
编码后
const变量要当作常量处理
右值直接计算,存到右值节点的valueReg里,再存到const变量的符号表里
设置传递属性
isCalcDirectly ,表明这个值直接计算而非生成中间代码局部变量(无数组)赋值
√
仅文法
Stmt → LVal '=' Exp | getint'('')' | getchar'('')' ';' 可以给变量赋值LVal作为左值:不进generate了,在Stmt函数那儿找Ident对应的Symbol,如果是var就把Exp / getint / getchar 的valueReg store到ident的addressReg,若是数组则找到ident对应的数组addressReg,计算元素下标存到下标Exp的valueReg,然后GEP取出数组元素的地址,再store右值
LVal 作为右值(
PrimaryExp -> LVal),这种情况给LVal设置一下addressReg, llvmType和symbol,非数组则先把值load到新寄存器里,如果是char类型就扩展(zext),再更新<PrimaryExp>的valueReg数组作为函数参数时??
getchar和getint的区别?文法保证getchar仅给char赋值,getint仅给int赋值,直接生成一个无参函数调用即可
先将 getchar 返回值从 int 类型截断为 char 类型再赋值给 char 类型变量
const变量的取值和Number一样,把const的symbol中的valueReg存到节点里
块与符号表 & 符号引用
√
与语义分析一致,遍历语法树时维护一个当前层数,由于符号表设计中添加了一个HashSet来快速获取对应符号的信息,故只需要一层层往上根据ident的token找到Symbol信息即可
全局变量初始化
√
Global Variety无寄存器,直接就是用@<ident>作为标识符
全局变量不能读寄存器的值,而是需要把值直接算出来赋给全局变量
所以对于layer == 1的变量,赋值不用存addressReg,存一个valueReg就行
// Assign if (layer == 1) { output("@" + ident.token + " = dso_local global i32 " + initExp.valueReg + "\n"); } // Load (LVal) if (ident.layer == 1) { output("%v" + this.addressReg + " = load i32, i32* @" + ident.token + "\n"); }
函数声明
main函数
public void MainFuncDef (AstNode ast) { output("\ndefine dso_local i32 @main() {\n"); generate(ast.getChild().get(4)); //Block output("}\n"); }
自定义函数
编码前:
FuncDef的Block需要设置一个ArrayList<String> paramIdentTokens,由FuncFParams传递给它,进Block后检查这个参数的size是否为0,不为0就在这个Block开头把参数的空间和addressReg分配了,把参数的valueReg load到addressReg里
在第一层符号表中查这个函数的Symbol,然后根据rtType属性判断返回值
在参数表里依次分配参数的值寄存器(记得给Symbol设置),然后填写FuncFParams节点的paramIdentSymbols
然后在block里一开始就给每个参数进行空间分配和寄存器分配(parentLayer == 1)
先开个寄存器,然后alloca对应参数类型的空间
然后把这个寄存器作为参数的地址寄存器,并将实参传进来的的值寄存器store到地址寄存器
编码后:
其实语义分析已经分析好了函数的形参表信息,直接用这个生成中间代码的形参表,然后把他们的token加到paramIdentTokens即可
函数调用
e.g.
call void @funcName (i32 %v14...)编码前:
在UnaryExp中,去第一层符号表里查函数名,找到rtType,然后解析FuncRParams
在FuncRParams中把各Exp的值全部求解出来,记录寄存器
每求出来一个就加到这个FuncRParams的valueReg的末尾(从第二个参数起,计算Exp前先加一个”,”)
最后在UnaryExp中输出call指令,同时如果函数有返回值则保存的UnaryExp节点的value中
编码后:
注意,若返回值为i8则需转成i32
int与char
编码前
符号表处再维护一个Type信息,表明这个符号在LLVM IR中的类型(i32, i8, i32*, i8*)

int: i32, char: i8
定义变量时,根据拿到的ident的类型来确定寄存器的类型
用常量赋值时可以不用管,编译器自动类型转换
用变量参与运算或赋值时,需要手动类型转换
转换策略是将i8 → i32作为表达式的值参与运算(zext i8 xx to i32)
给LVal赋值时,先判断LVal对应的ident的BType是不是char(包括数组元素),是的话截断(trunc i32 xx to i8)后再store,不是的话直接store
编码后
函数参数、函数返回值都要转换
Array
编码前
初始化 √
- 全局
- 局部
- const数组
将ConstInitVal的变量各自计算出真值后直接存到Symbol的valueArray里- 仍然需要显式在中间代码中定义
- 记得在symbol中记录数组size
对元素赋值 √
取元素值 √
- const数组元素(ConstExp不存在这种情况)
函数实参
函数形参
alloca [size x i32 / i8]
size可能为0
需要在Symbol中额外记录一个数组size,这需要在初始化数组的时候计算
初始化操作:
- 全局数组:类比FuncRParams,ConstInit的valueReg存一个初始化字符串序列([i32 1, i32 2, i32 0]),通过计算Exp的真值生成 √
- 局部数组:不懂…感觉只能拿一个指针寄存器一个个取数组元素的地址然后store √
InitVal: 在VarDef / ConstDef里遍历到的时候,先给InitVal节点设置addressReg为当前数组的AddressReg,在InitVal的时候逐个遍历直接在Def节点这里跨一个initVal遍历就好 √
注意:局部char类型数要把整片空间全部初始化完,没显式初始化的部分置0
若赋值为字符串:逐字符遍历字符串token,一个赋值一次 √

inbounds: 用于检查数组是否越界
函数传数组参数:
顺带判断参数类型
define dso_local i32 @f(i32* %f0) { %v0 = alloca i32* store i32* %f0, i32** %v0 %v2 = load i32*, i32** %v0 ; v2 是 %f0对应的参数的addressReg %v3 = getelementptr inbounds i32, i32* %v2, i32 1 store i32 2, i32* %v3 } define dso_local i32 @main() { { %v1 = alloca [5 x i32] ; 使用 GEP 获取指向数组第一个元素的指针 %v1_first_element = getelementptr inbounds [5 x i32], [5 x i32]* %v1, i32 0, i32 0 ; 将指向第一个元素的指针传递给函数 @f call void @f(i32* %v1_first_element) }
编码后
注意getelementptr的第三个参数的type应该固定为i32,而非根据数组类型变成i8
字符串解析的时候也要处理转义字符
分支语句与Cond处理
- Cond → LOrExp
- LOrExp → LAndExp | LOrExp '||' LAndExp
- LAndExp → EqExp | LAndExp '&&' EqExp
- EqExp → RelExp | EqExp ('==' | '!=') RelExp
- RelExp → AddExp | RelExp ('<' | '>' | '<=' | '>=') AddExp
首先是逻辑运算的结果的位数转换,RelExp和EqExp的结果是i1的
这里决定将EqExp和RelExp的结果直接转成i32,然后向上传值
标签设置
编码前
加一个全局的labelId变量
Stmt →'if' '(' Cond ')' Stmt [ 'else' Stmt ] 在这里设3个Label,第一个Stmt前、第二个Stmt前、整个语句之后
AstNode加三个属性,trueId, falseId, endId,分别对应上面这三个label的id
然后在Cond的node里传入这三个属性,并接着传给每一个LOrExp
为了保证短路求值,从左往右依次计算,发现下面的逻辑Exp值为1则直接跳转trueId对应的label,否则跳转到下一个逻辑Exp的开头label(分配一个新的labelId,然后输出标签)
LAndExp类似,遵循短路求值的规则就可以了
跳转指令的生成一定是在LAndExp的最后一个EqExp里


private void LOrExp(AstNode node) { ArrayList<AstNode> children = node.children; for (int i = 0; i < children.size() - 2; i += 2) { String nextExpLabel = "label" + this.labelId++; AstNode curExp = children.get(i); curExp.setTrueLabel(node.trueLabel); curExp.setFalseLabel("%" + nextExpLabel); generate(curExp); output("\n" + Instruction.tabs() + nextExpLabel + ":\n"); } AstNode lastExp = children.get(children.size() - 1); lastExp.setTrueLabel(node.trueLabel); lastExp.setFalseLabel(node.falseLabel); generate(lastExp); }
// Last EqExp of a LAndExp which should generate the instruction of // condition check and jump if (node.token.equals("<EqExp>") && node.trueLabel != null && node.falseLabel != null) { String notZeroReg = "%v" + this.varRegId++; Instruction.notZeroInstruction(notZeroReg, node.valueReg); Instruction.beZeroJumpInstruction(notZeroReg, node.trueLabel, node.falseLabel); }
在Stmt分析函数中判断if,分别在if后面的三个Stmt(如果没有else则endId = falseId)前设置labelId,然后传给Cond,并在第一个Stmt后面加一个无条件跳转到end label的跳转语句
编码后
无修改
for循环
编码前
第一个forStmt先生成
第二个Cond在开头生成,F是for循环之后,T就接着执行
第三个forStmt放在Block末尾生成,同时无条件跳转Cond
编码后
使用startLabel (for循环体开始), endLabel (for循环结束后), condLabel(条件判断开头)和loopUpdateLabel(循环量更新开头)
由于会出现Cond和ForStmt不存在的情况,需要特殊处理这类情况的标签
loopStmtNode和condLabel先为空
若解析完for循环括号内的语法树后,condLabel为空:将condLabel置为startLabel,并在开头先跳转到startLabel处
若loopUpdate节点不为null:解析完循环体后跳转到loopUpdateLabel,生成loopUpdateLabel并解析loopStmtNode
对于break和continue:需要在for循环体的Stmt节点中传递一个trueLabel和falseLabel,并且每次都给Stmt的第一个节点传入(复用逻辑exp的,但是不会冲突)
trueLabel即continue应该跳转的地方(loopUpdateLabel或者CondLabel),falseLabel即break应该跳转的地方(endLabel)



