感觉语法分析器在编译器前端是一个较为庞大的东西,因此打算分两篇博客来描述,第一篇着重描述思想,第二篇具体论述实现。
1、语法分析器要做什么
在编写任何一个东西的的时候,都要先弄明白这个玩意儿是做什么的,接受什么输入,产生什么输出。
一个语法分析器要接受词法分析器所产生的词素作为输入,产生一个抽象语法树给中间代码生成器,然后再由中间代码生成器生成中间代码并递交给编译器后端。当然在某些理解中可以把抽象语法树就当做是一种中间代码的表示形式,直接递交给后端。不管怎么说,总之就是语法分析器是一个生成抽象语法树的东西。
值得注意的是,语法分析器不仅要生成抽象语法树,而且还要在这个生成过程中找出各种语法错误并生成和维护符号表。
2、符号表
什么是符号表?符号表有什么用?
所谓符号表就是一个记录各种标识符(也就是终结符号id,词素id)及其属性的表,比如记录一个int变量x的类型为int,它的作用域,记录一个函数名,记录其函数类型,参数列表等。
符号表有作用域,比如一段简单的代码:
public void function()
{
int i=0;
while(true)
{
int i=1;
}
}
因此一个符号表的构造一定是一个树状结构,我们在编译器中用以下结构来描述一个符号表:
package ravaComplier.symTable;
import java.util.*;
public class SymTable {
private SymTable fatherSymTable;
private ArrayList<SymTable> blockSymTables;
private HashMap<String,Symbol> table;
private String blockid;
public SymTable(SymTable st,String str)
{
fatherSymTable=st;
blockid=str;
blockSymTables=new ArrayList<SymTable>();
table=new HashMap<String,Symbol>();
}
public void addSym(Symbol sym)
{
table.put(sym.id, sym);
}
public Symbol getSym(String id)
{
Symbol result=table.get(id);
if(result==null && fatherSymTable!=null)
{
return fatherSymTable.getSym(id);
}
return result;
}
public void addSymTable(SymTable st)
{
blockSymTables.add(st);
}
}
代码很简单以至于注释都懒得写了。
通过fatherSymTable来记录此符号表的父表,用于不断的向上回溯查找符号(getSym)使用。
blockid算是给此表一个id,用于打印调试信息时使用。
addSym在此表增加符号。除此之外还有个addSymTables来加入子表。
另外此类还重载了toString()方法,用于debug信息,限于篇幅这个方法没贴到博客里,可在我上传的资源里拿到完整的源文件。
也许在之后的分析描述写代码的过程中我会发现需要给这个类添加新的函数,到那时再对此类进行补充。
接下来看看简单的Symbol类,也就是表示一个符号的类:
package ravaComplier.symTable;
public class Symbol {
public String id;
public int type;
public Object value;
public Symbol(String i,int t,Object v)
{
id=i;
type=t;
value=v;
}
public static int TYPE_CLASSNAME=0;
public static int TYPE_MEMBERVAR=1;
public static int TYPE_LOCALVAR=2;
public static int TYPE_FUNCNAME=3;
public static int TYPE_CONSFUNC=4;
}
分为3个域,id,也就是标识符,type,枚举值已列出,value,根据不用的枚举值定义了不同的value,之后若用到了再贴代码吧。
总共分为5类符号:类名、成员变量、局部变量、函数名和构造函数名。
当然若之后根据需要,或许会使用新的符号也可以灵活的添加。
3、语法树的表示
一棵语法树不能使用普通的树结构因为每个不同的节点的行为、值太多且不同。语法树中的节点为非终结符号或者终结符号,对于其中的id,我们就让它指向符号表中的符号即可,对于非终结符号,每个非终结符号我们都建立一个新的类来描述其行为,对于非id的终结符号,其信息要么不记录(比如无意义的分好括号等),要么简单记录其类型(比如各种运算符)。
所以这种情况下每一个节点的建立都比较灵活,下面举两个例子:
对于产生式:ops --> bitop | logiop | artmop | cprop
我们建立如下的类来描述ops:
package ravaComplier.syntax.nodes;
public class ops {
private int type;
private Object value; //must be bitop,logiop,artmop,cprop
public ops()
{
//not implements
}
public static int TYPE_BITOP=0;
public static int TYPE_LOGIOP=1;
public static int TYPE_ARTMOP=2;
public static int TYPE_CPROP=3;
}
int描述运算符类型,然后根据响应的类型让value为具体的运算符类。接着给出cprop的类:
package ravaComplier.syntax.nodes;
public class cprop {
public cprop()
{
//not implemets
}
private int type;
public static int TYPE_GREATER = 0;//>
public static int TYPE_GREATEREQUAL=1;//>=;
public static int TYPE_LESS=2;//<;
public static int TYPE_LESSEQUEAL=3;//<=;
public static int TYPE_EQUAL=4;//==
}
这是一个终结符,所以只有一个域来记录运算符的类型。
本篇文章到此就结束了,下篇文章讲着重分析语法树的展开过程。