这篇博文拖了好久才写完,其一是语法分析器本身的难度实在有点超出我的预料,以至于反复重构多次才完成,其二是因为弄了个xbox玩,占用了一部分的课余时间= =!。
本篇博文将分为以下几个部分来描述一下语法分析器的具体实现,理论、部分典型节点与结果。
一、语法制导翻译、LL与LL(X)语法、左递归等其它
为什么要写一个语法分析器?语法分析器的作用不仅仅是来检查词素列表中的序列是否是一个我们语言的语句,更重要的是只有借助语法分析器得到的抽象语法树,才能够生成中间代码或者具体的目标代码,这个过程叫做语法制导翻译(syntax-directed translation)。在紫龙书(编译原理第二版)的封面上,一个拿盾的骑士正在和一个喷火龙决斗,其中龙的身上写的是Complexity of Complier Design,而骑士的盾上写的则是Syntax Directed Translation,因此把语法制导翻译当作是编译器前端的核心也不为过。
展开语法树的过程实质上也就是将词素不断地对应到我们语言定义的递推式的过程,换个说法其实也就是不断地展开语言的递推式,使之符合已有词素的过程。这个展开的过程从方法上来讲可分为两种:LL和LR。其中第一个字母代表从左到右读词素序列,第二个字母L代表尝试最先展开最左边的非终结符号,R代表尝试从右边开始将词素归约为非终结符好。换言之,LL是一种自顶向下的展开方法,LR是一种自底向上的归约方法,本文采用的技术为LL,所以以下也以讨论LL为主。
为了使编译器能高效迅速,一个良好的语法设计必须是一个LL(1)语法,什么是LL(1)语法呢?举个例子,当我们面对如下推导式的时候:
ids-> id| ----------1
id.ids| -----------2
ids[expr] | -----------3
this
此时我们读到了一个词素id,是展开成1、2、3中的哪种呢?当然目前我们无法判断,因此需要多读入下一个词素才能进行判断。如果读到的是[,则展开成3。如果读到了.则展开成2,否则展开成1。但问题是有些情况下,多读入一个词素或许还不能进行判断,当一个语言的语法中,只要多读入X个词素就能唯一的确定推导式,则称其为LL(X)文法。很遗憾,我们的语法不是LL(1)语法,虽然有很多推导式的处理技巧可以将一个非LL(1)的语法处理成LL(1)的语法,但这样会失去语法的直观性。考虑再三我在“不是很合理但易于理解的语法” 和 “合理高效的不直观的语法” 之间选择了前者。因此既然我们的语法并非LL(1)的,因此在语法分析的过程中,我们只是不断的去尝试展开,如果不成功,则回溯。虽然这是比较低效的,但文法中的大多数推导式并不复杂,所以处理的时间完全可以接受。
考虑如下 推导式:
expr --> (expr) ------------1
ids| ------------2
number| -----------3
literal| ------------4
func-call| ------------5
expr ops expr|
这个推导式不满足LL(1),假设当前读到了一个id,目前可供选择的有2、4、5,然后又读入了一个“。”,目前可供选择的还是2、4、5,又读入了一个id,可供选择的还是2、4、5,然后读入了一个“(” ,这时候才能确定唯一的展开式func-call。但这个表达式除了不满足LL(1)之外还有其它的问题:左递归。假设expr上来就尝试去展开成5的形式,因为是递归展开的过程,5中最左边的expr又会尝试展开成5的形式,然后这个过程就不断递归下去最终导致stack overflow。虽然有很多方法和技巧可以改变推导式的形式来消除左递归,但是依然本着易于理解的原则,我们在语法分析中通过使用朴素的笨办法来避免这种情况的发生。所谓的笨办法就是:
(1)按优先级先展开1234,然后都失败再展开成5。
(2)设置最大展开深度为200,超过了直接报错。
虽然很笨很低效,但勉强够用了。
二、语法分析器结构
语法分析器在实现上分以下几个部分,第一部分为SyntaxTreeGenerator,负责读入词素,和词法分析器以及后端程序进行交流,算是语法分析器的对外接口。其次使用GlobalVars来存储各种全局数据,记录分析过程中的各种信息。最后就是各种节点,每个节点在分析的过程中若需要其它信息则通过GlobalVars来解耦。接下来通过几个例子来具体说明这些节点是如何展开语法分析的:
(1)id
package ravaComplier.syntax.nodes;
import ravaComplier.lexer.Lexeme;
import ravaComplier.symTable.SymTable;
import ravaComplier.symTable.Symbol;
import ravaComplier.syntax.GlobalVars;
import ravaComplier.syntax.SyntaxTreeGenerator;
/*
* 该类尝试读入词素并生成id节点
*/
public class id {
public SymTable curST;//这个节点的符号表
public id() throws Exception
{
Lexeme lex=SyntaxTreeGenerator.readLexeme();//读一个词素
curST=SyntaxTreeGenerator.getCurTable();//得到当前符号表
if(lex.type==Lexeme.ID)
{
//类型正确
symEntry=SyntaxTreeGenerator.getCurTable().getSym(lex.value.toString());//判断符号表中是否已有此id
if(symEntry==null)
{
firstappear=true;
}
else
{
firstappear=false;
}
value=lex.value.toString();
symEntry=new Symbol(value,2,null);//生成一个入口
}
else
{
//类型错误抛出异常
throw new Exception("ID required!\r\n");
}
GlobalVars.idlist.add(this);//把所有id都添加进idlist里。
}
public String value;
public boolean firstappear;
public type tp;//类型,由调用者赋值
public Symbol symEntry;//指向符号表的条目
}
这个类代表id节点,首先尝试读入词素,如果不是id则发生语法错误。其次需要判断此id是否是第一次出现,在某些时候这个信息很重要(比如变量定义时),然后最后将已经初始化好的id添加到GlobalVars的list中。值得注意的是,GlobalVars里面有很多的list,主要是用于在生成语法树之后用于一些检查。
(2)vardeclare
来个稍微复杂点的,局部变量的定义
package ravaComplier.syntax.nodes;
import java.util.ArrayList;
import ravaComplier.lexer.Lexeme;
import ravaComplier.symTable.SymTable;
import ravaComplier.symTable.Symbol;
import ravaComplier.syntax.SyntaxTreeGenerator;
public class vardeclare {
/*
* var-declare --> type args|type[] args
*
*/
public SymTable curST;
public vardeclare() throws Exception
{
curST=SyntaxTreeGenerator.getCurTable();
tp=new type();
int pos=SyntaxTreeGenerator.savePos();//得到当前分析的位置
Lexeme lex=SyntaxTreeGenerator.readLexeme();//读取下一个词素
arrayDeclare=false;
if(!lex.value.equals("["))
{
SyntaxTreeGenerator.loadPos(pos);//若不是想要的词素则回溯
}
else
{
lex=SyntaxTreeGenerator.readLexeme();
if(!lex.value.equals("]"))
{
SyntaxTreeGenerator.loadPos(pos);
throw new Exception("] expected!");//发生语法错误,数组定义时括号没有闭合。
}
else
{
arrayDeclare=true;
}
}
ags=new args();
ArrayList al=ags.getidsList();//获取参数列表,若args为 a1,a2,a3则返回的列表中含有a1,a2,a3
SymTable st=SyntaxTreeGenerator.getCurTable();
for(int i=0;i<=al.size()-1;i++)
{
id ID=al.get(i).getLastID();//id1.id2.id3则此函数返回id3。
if(ID.firstappear==false)
{
throw new Exception("id declared duplicated!");//定义的变量名已经出现过了,报错
}
st.addSym(ID.symEntry);//将id添加进符号表
ID.symEntry.value=ID;
ID.symEntry.type=Symbol.TYPE_LOCALVAR;
ID.tp=tp;//给此id赋予类型
if(arrayDeclare)
{
ID.tp.isArray=true;
}
}
}
public type tp;
public args ags;
public boolean arrayDeclare;
}
可以看出,id节点中的很多属性都是由其调用者决定的,这点在节点逻辑的编写中体现的尤为明显。
(3)memberfundeclare
来个再复杂点的,成员函数定义:
package ravaComplier.syntax.nodes;
import ravaComplier.lexer.Lexeme;
import ravaComplier.symTable.SymTable;
import ravaComplier.symTable.Symbol;
import ravaComplier.syntax.SyntaxTreeGenerator;
public class memberfuncdeclare {
public SymTable curST;
public memberfuncdeclare() throws Exception
{
/*member-func-declare --> private|public
NUL|static
type func-name( NUL|def-args ) { func-body }*/
curST=SyntaxTreeGenerator.getCurTable();
af=new accessflag();//得到一个accessflag, 即public 或者 private
//尝试读取static ,若没有则回溯。
int pos=SyntaxTreeGenerator.savePos();
Lexeme lex=SyntaxTreeGenerator.readLexeme();
if(lex.type!=Lexeme.STATIC)
{
SyntaxTreeGenerator.loadPos(pos);
}
else
{
isstatic=true;
}
tp=new type();//得到type
fc=new funcname();//得到函数名。
fc.id.symEntry.value=this;
if(fc.id.firstappear==false)
{
//判断函数名是否重复,若重复则报错。
throw new Exception("function name must be unique!");
}
SymTable st=SyntaxTreeGenerator.getCurTable();
st.addSym(fc.id.symEntry);//把这个函数添加进符号表
fc.id.symEntry.type=Symbol.TYPE_FUNCNAME;
SymTable st1=new SymTable(st,fc.id.value+" symtable");//建立一个子表,每个函数都有自己的符号表因为里面变量的作用域和其外不同
SyntaxTreeGenerator.setCurTable(st1);//将子表设置为当前符号表,之后该函数体内的一切分析都使用该表
lex=SyntaxTreeGenerator.readLexeme();
if(!lex.value.toString().equals("("))
{
throw new Exception("( expected!");//语法检查
}
try
{
pos=SyntaxTreeGenerator.savePos();
da=new defargs();//尝试搜寻其后的调用参数,若没有参数则根据上一行存储的位置回滚
}
catch(Exception e)
{
SyntaxTreeGenerator.loadPos(pos);
da=null;
}
lex=SyntaxTreeGenerator.readLexeme();
if(!lex.value.toString().equals(")"))
{
throw new Exception(") expected!");//语法检查
}
lex=SyntaxTreeGenerator.readLexeme();
if(!lex.value.toString().equals("{"))
{
throw new Exception("{ expected!");//语法检查
}
fb=new funcbody();//构造函数体
lex=SyntaxTreeGenerator.readLexeme();
if(!lex.value.toString().equals("}"))
{
throw new Exception("} expected!");//语法检查
}
SyntaxTreeGenerator.setCurTable(st);//函数结束,重置符号表
}
public accessflag af;
public type tp;
public boolean isstatic;
public funcname fc;
public defargs da;
public funcbody fb;
}
通过以上的分析,我们可以总结出每一个节点的构造规则:
1、尝试将此节点按一定的顺序展开
2、其每一个部分当作该节点的成员变量
3、在展开的时候和符号表进行适当的交互
按照类似的思路,我们当我们完成所有节点后,编译器的前端也已经差不多了,下图是上篇博文中日志里的示例程序得到的语法树,可以看到即便是一个简单的示例程序,其语法树也相当复杂。