本篇博客说一说在语法树生成之后、后端代码生成之前所需要的一些必要检查工作,以及其实现的简单思路。
因为语法树的构建是从左到右逐渐扫描词法分析器所提供的词素,所以当某些情况下我们需要整个语法树的信息来判断程序本身是否有问题,就没法把这个判断过程放在语法树的分析中进行。虽然这个说法有点抽象和笼统,简而言之就是我们需要构建完语法树之后,再重新审视整个语法树并修改其中的某些节点,我们把这个过程称作为静态(编译期)的检查过程。
本篇主要讲述显而易见的3个静态检查的例子:函数调用检查、表达式类型回填以及赋值检查。
1、函数调用检查
考虑如下代码片段:
public int func1()
{
int a,b;
a=1;
b=2;
return func2(a,b);
}
public int func2()
{
//do nothing
}
很明显,这段代码是错误的,因为func2的调用不需要任何参数,但在func1里调用func2时尝试传入了2个参数。
现在思考在构成语法树的过程中能否发现这个错误。
答案是否定的,原因在于以下几点:1、这段代码完全符合语法,即可以由第一篇博文中的推导式展开。2、在调用func2时还没有得到任何和func2有关的信息,因此无法判断这个调用是否正确。
基于这两点,我们把对函数调用语句的判断放在静态检查中做,具体思路如下:
(1)在每一个funccall节点里把节点存放在GlobalVars中的一个ArrayList中
public class GlobalVars {
public static classname cn;
public static String classname;
public static boolean inWhile;
public static ArrayList<funccall> funcCheckList=new ArrayList<funccall>();//检查函数调用是否正确
}
然后在funccall的解析中加上:
GlobalVars.funcCheckList.add(this);
(2)新建一个StaticChecker类,该类中建立funccallChecker方法,使用这个方法做调用检查:
public static void funccallChecker() throws Exception
{
HashMap<String,Symbol> functions=SyntaxTreeGenerator.getFunctions();
for(funccall fc: GlobalVars.funcCheckList)
{
Symbol sym=functions.get(fc.fn.toString());
if(sym==null)
{
throw new Exception("function check fail: error func name");
}
ArrayList<ids> al=fc.ag.getidsList();
memberfuncdeclare mfc=(memberfuncdeclare)sym.value;
ArrayList<type> tplist=mfc.getagtypelist();
if(al.size()!= tplist.size())
{
throw new Exception("function check fail: error args number");
}
for(int i=0;i<=al.size()-1;i++)
{
if(type.isSametype(tplist.get(i), al.get(i).tp)==false)
{
throw new Exception("function check fail: error args type");
}
}
}
}
首先检查此函数是否存在,若不存在则抛出异常。
其次检查从funccall里得到的调用参数列表和函数所要求的参数是否相符。
再检查每个位置的参数是否对应。
接着给出getFunction函数,实现原理就是从符号表最顶层(在一个类定义时所有函数的定义必然出现在最顶层)得到所有函数的符号。
public static HashMap<String,Symbol> getFunctions()
{
return root.getFunctions();
}
root是符号表的根表,然后再给出Symtalbe的getFunctions函数,该函数得到此级符号表的所有函数:
public HashMap<String,Symbol> getFunctions()
{
HashMap<String,Symbol> result=new HashMap<String,Symbol>();
for(String name:table.keySet())
{
Symbol sym=table.get(name);
if(sym.type==Symbol.TYPE_FUNCNAME)
{
result.put(name, sym);
}
}
return result;
}
2、 expr类型回填
考虑如下展开式:
expr --> func-call
这是语言中所用到的展开式的一部分。
每一个expr都必须要有一个类型,比如int,double,object等,但在当使用此展开式进行展开时,我们无法立刻确定此expr的类型,因为关于funccall的任何信息都可能是未知的。
所以在语法树构建完成后需要对这类的expr进行类型的回填。再考虑如下展开式:
expr --> (expr)
该expr类型就是括号中expr的类型,因此若存在一个expr的类型为空,则由于表达式中类型传递这一机制的存在,很可能导致很多expr类型都为空,因此需要回填所有类型为空的expr的类型。
expr类型回填的思路如下:
(1)首先将所有语法树中的expr节点存在全局变量中,代码和funccall的存储类似。
(2)其次过滤掉其中类型不为空的节点:
private static void UnnullFilter(){
for(expr ep:GlobalVars.exprlist)
{
if(ep.tp!=null)
{
GlobalVars.exprlist.remove(ep);
}
}
}
(3)回填过程分为两步,首先回填函数调用类型的expr,设置其类型为函数类型,其次不断的尝试填充其余expr的类型,直到List为空为止:
private static void setExprType()
{
for(expr ep:GlobalVars.exprlist)
{
if(ep.Type==4)
{
ep.tp=SyntaxTreeGenerator.getFuncType(ep.fc.toString());
}
GlobalVars.exprlist.remove(ep);
}
while(GlobalVars.exprlist.size()!=0)
{
for(expr ep:GlobalVars.exprlist)
{
ep.settype();
GlobalVars.exprlist.remove(ep);
}
}
}
3、setValue类型检查
考虑setValue也就是赋值的表达式的产生式:
setvalue--> ids = expr
若我们把ids的类型和expr类型一致则认为这是一个合法的setvalue,则在产生语法树的时候我们无法判断该产生式是否合法,因为expr的类型并不能在构建语法树的时候确定,因此需要在静态检查中检查setValue是否合法。
首先将所有setValue的值存放在GlobalVars中,然后在上面的基础上逐一检查即可。
代码如下:
private static void SetValueType() throws Exception
{
for(setvalue sv:GlobalVars.svlist)
{
if(!type.isSametype(sv.ep.tp, sv.is.tp))
{
throw new Exception("type doesn't match");
}
}
}
总结:
所有和expr相关的类型检查如果放在编译期做的话,则要在语法树构建完成之后进行静态检查。除以上列出的三点之外,还需要对return value进行检查等。