概述:
本次实验的内容是在前两次词法分析和语法分析的基础上,为编译器前段添加错误处理功能。由于这次实验中,参考学长的博客学到了很多思路,并对当前代码做了较大的重构修改,遂决定写下本篇博客做一梳理记录,并会在之后逐步补全完善系列实验内容。
一、问题描述:
1.1任务概览
1.2错误详情
二、实现思路
2.1准备工作:
在第二次的实验中,我们实现了Lexer相关类,其中的关键函数为Lexer.next () 能够从字符串流中提取出一个个Token并存入TokenList中
在第三次的实验中,我们使用递归下降的方法将生成了一颗包含所有终结符和非终结符的抽象语法树AST,以便后续实验使用
2.1.1代码重构
由于在第三次实验中,我偷懒并未新增各种为终结符节点类,导致我在在后续的实验中需要重复遍历语法树时变得很困难,于是在开始本次实验前,我新建了parser包下新建了包含所有非终结符的节点类,并让他们统一继承节点Node类,以便于构建出方便遍历的语法树。
我们按照功能将非终结符分成如下几个大类:
1、表达式相关:存放Exp相关的节点类
2、函数相关:存放Func相关的节点类
3、语句相关:存放Stmt相关的节点类
4、变(常)量定义相关:存放Const、Var相关的节点类
5、其他:存放入Block相关的节点类
此外我还设置了一个NodeToken节点类作为AST语法树的叶节点。他也直接继承自Node类
2.2新增内容
参考学长的博客内容,本次实验的主要实现思路如下:
1、为每个AST节点设置错误检查函数checkError () 函数,该函数在于检查本节点中可能出现的错误类型,并递归调用子节点的错误检查函数完成整棵AST树的错误检查。
2、在进行错误检查时我们需要借助符号表的帮助,因此我们可以选择在错误检查这一遍遍历的时候同时完成符号表的建立。
3、对于符号表的构建,我们选择采用课件上的方法,即栈式符号表。在我们顺序遍历AST的每个节点,如果是进入一个新的语句块,如Block,FuncDef,Loop则新建一个符号表,插入到符号表栈的栈顶。当离开该语句块时,从符号栈中弹出该符号表,并插入到有所有符号表组成的ArrayList中备用。
三、代码细节
3.1构建AST树
3.1.1Node节点
首先新建Node节点作为所以有节点的父类,为其设计四个属性:startLine,endLine,type和children。前两个属性用来记录这个语法成分的起始行和终止行,用于错误处理时输出错误位置信息。type用于记录节点类型,即该节点具体属于哪一个非终结符(或者终结符)。children则是一个存储了当前节点所有子节点的ArrayList。
protected int startLine;
protected int endLine;
protected SyntaxVarType type;
protected ArrayList<Node> children;
为Node节点设置checkError方法,该方法会递归遍历调用子节点的checkError方法,之后在不同的子节点中会重写这一方法
public void checkError() {
// 如果没有子节点即叶子结点
if (children == null) return;
// 否则,递归遍历检查子节点
for (Node child : children) {
child.checkError();
}
}
3.2符号表相关类
3.2.1符号类
Symbol类作为所有符号类的父类具有name和type两个属性,分别用于记录符号的名称和符号类型
protected String name;
protected SymbolType type;
3.2.2符号类派生
对于需要记录到符号表中的符号一共包括下面三类:变量、常量、函数
对于变量与常量,设置SymbolVar和SymbolConst类继承自Symbol类,并设置变量类型、变量维度、变量值、等属性
private ValueType valueType; // 变量类型
private int dimension; // 变量维度
private ArrayList<Integer> valueList; // 变量的值
private ArrayList<Integer> lenList; // 变量各个维度的长度
private boolean isGlobal; // 是否是全局变量
对于函数,设置SymbolFunc类继承自Symbol类,并设置函数返回类型,函数参数类型列表,函数参数维度等属性
private ValueType returnType; // 函数返回类型
private ArrayList<ValueType> FParamTypes; // 函数参数类型
private ArrayList<Integer> FParamDims; // 函数参数维度
3.2.3符号表管理类
为了便于符号表的插入删除和管理,我们使用单例模式创建了一个符号表管理类SymbolManager,它的属性包含一个临时符号表栈,一个符号表列表,当前符号表Id,上一个符号表Id以及其他一些辅助属性
private static final SymbolManager symbolManager = new SymbolManager();
private Stack<SymbolTable> symbolTableStack;
private HashMap<Integer, SymbolTable> symbolTables;
private int currentTableId;
private int latestTableId;
private SymbolFunc latestFunc; // 记录当前所在的函数,用于检查return语句
private int loopDepth; // 记录当前所在的循环深度,用于检查是否出现break、continue语句
private boolean isGlobal; // 记录当前所属位置是否为全局区域(当进入函数内部后转为false)
符号插入:在临时符号表栈的栈顶符号表中进行插入
public boolean addSymbol(Symbol symbol) {
SymbolTable symbolTable = symbolTableStack.peek();
if (symbolTable.getSymbolByName(symbol.getName()) != null) {
return false;
}
symbolTable.addSymbol(symbol);
symbolTables.get(symbolTable.getId()).addSymbol(symbol);
return true;
}
符号查找:现在当前栈顶符号表中查找,如果找不到则继续在外层符号表中查找,直到找到或者最外层符号表返回null位置
public Symbol getSymbolByName(String name) {
Symbol symbol = null;
SymbolTable symbolTable = symbolTableStack.peek();
symbol = symbolTable.getSymbolByName(name);
while(symbol == null && symbolTable.getFatherId() != -1) {
symbolTable = symbolTables.get(symbolTable.getFatherId());
symbol = symbolTable.getSymbolByName(name);
}
return symbol;
}
符号表的创建:在进入一个新的语句块(block,func,loop)时,需要创建符号表,即向临时符号表栈的栈顶插入一个新的符号表
public void enterBlock() {
SymbolTable symbolTable = new SymbolTable();
symbolTable.setId(++latestTableId);
symbolTable.setFatherId(currentTableId);
this.symbolTableStack.push(symbolTable);
this.symbolTables.put(symbolTable.getId(), symbolTable);
currentTableId = symbolTable.getId();
}
弹出符号表:在离开一个语句块时,需要将符号表栈顶的符号表弹出,并插入符号表列表中备用
public void leaveBlock() {
SymbolTable symbolTable = this.symbolTableStack.pop();
symbolTables.put(symbolTable.getId(), symbolTable);
if(!symbolTableStack.isEmpty()) {
currentTableId = symbolTableStack.peek().getId();
}
}
3.3Node中的错误检查(以下方法按所包含的错误顺序排序,不包含任何可能错误类型的节点的checkError方法直接继承父节点Node的checkError即可)
3.3.1 NodeStmt的checkError方法 (包含错误a、f、h1、h2、l、m1、m2)
@Override
public void checkError() {
if(children.get(0) instanceof NodeLVal) {
// 错误检查 h (不能改变常量的值)中的第一种和第二种情况 :<Stmt>→<LVal>‘=’ <Exp>‘;’ <Stmt>→<LVal>‘=’ ‘getint’ ‘(’ ‘)’ ‘;’
NodeLVal lValExp = (NodeLVal) children.get(0);
if (lValExp.isConst()) {
Printer.addErrorMsg(lValExp.getEndLine(), ErrorType.h);
}
super.checkError();
}
else if(children.get(0) instanceof NodeBlock) {
SymbolManager.getInstance().enterBlock();
super.checkError();
SymbolManager.getInstance().leaveBlock();
}
else if(children.get(0) instanceof NodeToken && children.get(0).getType() == SyntaxVarType.FORTK) {
SymbolManager.getInstance().enterLoop();
super.checkError();
SymbolManager.getInstance().leaveLoop();
}
else if(children.get(0) instanceof NodeToken && (children.get(0).getType() == SyntaxVarType.BREAKTK || children.get(0).getType() == SyntaxVarType.CONTINUETK)) {
// 错误检查 m (在非循环块中使用break和continue语句) 中的第一和第二种情况 :<Stmt>→‘break’‘;’ <Stmt>→‘continue’‘;’
if (SymbolManager.getInstance().getLoopDepth() <= 0) {
Printer.addErrorMsg(startLine, ErrorType.m);
}
super.checkError();
}
else if(children.get(0) instanceof NodeToken && (children.get(0).getType() == SyntaxVarType.RETURNTK)) {
// 错误检查 f (无返回值的函数存在不匹配的return语句)中的唯一一种情况 :<Stmt>→‘return’ [Exp] ‘;’
if (children.size() >= 2 && children.get(1) instanceof NodeExp) {
SymbolFunc func = SymbolManager.getInstance().getLatestFunc();
if (func.getReturnType() == ValueType.VOID) {
Printer.addErrorMsg(children.get(0).getStartLine(), ErrorType.f);
}
}
super.checkError();
}
else if(children.get(0) instanceof NodeToken && (children.get(0).getType() == SyntaxVarType.PRINTFTK)) {
// 错误检查 a (格式字符串中出现非法字符报错行号为 <FormatString> 所在行数。)中的唯一一种情况 :<FormatString> → ‘“‘{<Char>}’”
String formatString = ((NodeToken)children.get(2)).getValue();
if (! checkFormatString(formatString)) {
Printer.addErrorMsg(children.get(2).getStartLine(), ErrorType.a);
}
// 错误检查 l (printf中格式字符与表达式个数不匹配)中的唯一一种情况 :<Stmt> → ‘printf’‘(’<FormatString>{‘,’<Exp>}’)’‘;’
int numOfFormatChar = 0;
for (int i = 0; i < formatString.length(); i++) {
if (formatString.charAt(i) == '%' && i+1 < formatString.length() && formatString.charAt(i+1) == 'd') numOfFormatChar++;
}
int numOfExp = 0;
for (int i = 0; i < children.size(); i++) {
if (children.get(i) instanceof NodeExp) numOfExp++;
}
if (numOfFormatChar != numOfExp) {
Printer.addErrorMsg(children.get(0).getStartLine(), ErrorType.l);
super.checkError(); return;
}
super.checkError();
}
else {
super.checkError();
}
}
3.3.2 NodeConstDef的checkError方法(包含错误b1)
@Override
public void checkError() {
this.symbol = createSymbol();
super.checkError();
// 错误检查 b (常量名重复定义) 中的第一个情况 :<ConstDef>→<Ident> …
boolean res = SymbolManager.getInstance().addSymbol(symbol);
if (! res) Printer.addErrorMsg(children.get(0).getEndLine(), ErrorType.b);
}
3.3.3 NodeVarDef的checkError方法(包含错误b2)
@Override
public void checkError() {
this.symbol = createSymbolVar();
super.checkError();
// 错误检查 b(名字重定义)中的第二种情况 :<VarDef>→<Ident> … <Ident> …
boolean insertInToSymbolTable = SymbolManager.getInstance().addSymbol(symbol);
if (! insertInToSymbolTable) Printer.addErrorMsg(children.get(0).getEndLine(), ErrorType.b);
}
3.3.4 NodeFuncDef的checkError方法(包含错误b3、g1)
@Override
public void checkError() {
SymbolManager.getInstance().setGlobal(false); // 接下来不可能出现全局变量的定义
this.symbol = createSymbol();
// 错误检查 b (函数名重复定义)中的第三种情况 :<FuncDef>→<FuncType><Ident> …
boolean res = SymbolManager.getInstance().addSymbol(symbol);
if (! res) Printer.addErrorMsg(children.get(1).getEndLine(), ErrorType.b);
// 设置 SymbolManager 进入函数定义块
SymbolManager.getInstance().enterFuncDef(symbol);
// check children's error
// 因为只有形参checkError之后,形参的symbol才能生成,然后函数symbol的参数类型、维度信息才能获得
// 必须要保证在检查block之前将参数类型、维度信息传给函数的symbol,否则会影响函数体内对函数自调用的检查
for (Node child : children) {
if (child instanceof NodeBlock) { // 在检查Block之前加入参数类型、维度信息
setParamInfo();
}
child.checkError();
}
// 设置 SymbolManager 离开函数定义块
SymbolManager.getInstance().leaveFuncDef();
// 错误检查 g (缺少有返回类型函数的返回语句)中的第一种情况 :<FuncDef> → <FuncType> <Ident> ‘(’ [<FuncFParams>] ‘)’ <Block>
Node returnStmt = null;
Node block = children.get(children.size() - 1); // Block → '{' { BlockItem } '}'
if(block.getChildren().size() > 2) {
Node blockItem = block.getChildren().get(block.getChildren().size() - 2); // BlockItem → Decl | Stmt
Node stmt = blockItem.getChildren().get(0); // Stmt → <ReturnStmt> ';'
for(Node child : stmt.getChildren()) {
if(child instanceof NodeToken && child.getType() == SyntaxVarType.RETURNTK) {
returnStmt = child;
break;
}
}
}
// 检查是否出现缺少return语句的情况
if (returnStmt == null && symbol.getReturnType() != ValueType.VOID) {
Printer.addErrorMsg(endLine, ErrorType.g);
}
}
3.3.5 NodeFuncParam的checkError方法(包含错误b4)
@Override
public void checkError() {
this.symbol = createSymbol();
// 错误检查 b (形参名重复定义)中的第四种情况 :<FuncFParam> → <BType> <Ident> …
boolean res = SymbolManager.getInstance().addSymbol(symbol);
if (! res) Printer.addErrorMsg(children.get(1).getEndLine(), ErrorType.b);
super.checkError();
}
3.3.6 NodeLVal的checkError方法(包含错误c1)
@Override
public void checkError() {
// 错误检查 c (未定义的标识符)中的第一种情况 :<LVal>→<Ident> …
NodeToken ident = ((NodeToken)children.get(0));
if (SymbolManager.getInstance().getSymbolByName(ident.getValue()) == null) {
Printer.addErrorMsg(ident.getStartLine(), ErrorType.c);
}
super.checkError();
}
3.3.7 NodeUnaryExp的checkError方法(包含错误c2、d、e)
@Override
public void checkError() { // UnaryExp → PrimaryExp | Ident '(' [FuncRParams] ')' | UnaryOp UnaryExp
// 函数调用
if (children.get(0) instanceof NodeToken) { // Ident '(' [FuncRParams] ')' (唯一包含可能错误的分支)
NodeToken ident = ((NodeToken)children.get(0));
SymbolFunc symbolFunc = (SymbolFunc)SymbolManager.getInstance().getSymbolByName(ident.getValue());
// 错误检查 c (未定义的函数)中的情况二 :<UnaryExp>→<Ident> …
if (symbolFunc == null) {
Printer.addErrorMsg(ident.getStartLine(), ErrorType.c);
super.checkError(); return;
}
// 函数调用
ArrayList<Integer> FParamDims = symbolFunc.getFParamDims();
ArrayList<Integer> RParamDims = new ArrayList<>();
if (children.size() > 2 && children.get(2) instanceof NodeFuncRParams) {
RParamDims = ((NodeFuncRParams) children.get(2)).getRParamDims();
}
// 错误检查 d (函数参数个数不匹配)中的唯一情况 : <UnaryExp>→<Ident>‘(’[<FuncRParams>]‘)’
if (RParamDims.size() != FParamDims.size()) {
Printer.addErrorMsg(ident.getStartLine(), ErrorType.d);
super.checkError(); return;
}
// 错误检查 e (函数参数类型不匹配)中的唯一情况 : <UnaryExp>→<Ident>‘(’[<FuncRParams>]‘)’
for (int i = 0; i < RParamDims.size(); i++) {
if (! FParamDims.get(i).equals(RParamDims.get(i))) {
Printer.addErrorMsg(ident.getStartLine(), ErrorType.e);
super.checkError(); return; // only print once
}
}
}
super.checkError();
}
3.3.8 错误i、j、k
由于错误i、j、k的处理涉及语法解析内容,故放在Parser类中处理
四、其他
本次实验还做了一些其他规范化处理,比如添加了utils文件夹并添加了Printer类等等,就不在本文中详细介绍了。第一次写博客,能力有限,欢迎各位给出指导和建议orz