1.1 R(1)项目集族
1.1.1 LR(1)的定义
该定义来自《编译原理及实践》(Kenneth C Louden)
定义1:(LR(1)第一部分定义)
假设有LR(1)项目[A - > α•Xγ, a],其中X是任意符号(终结符和非终结符),那么X有一个到项目[A - > αX•γ, a]的转换。
这个定义实际与WACC对应的的部分是4.2.1节21行CSymbol* CItem::Go(CItem & goItem,CLanguage &L)函数。如果Item为[A - > α•Xγ, a],那么go函数返回X(类型为CSymbol*),goItem为[A - > αX•γ, a]。在后面会有单独一节将该函数。请注意,在LR(1)中,如果两个项目先行符号不同,即使规则和圆点位置相同,这两个项目会分到不同的状态中,而LALR(1)就不这样。
定义2:(LR(1)第二部分定义,也叫闭包定义)
假设有LR(1)项目[A - > α•Bγ,a],其中B是一个非终结符,那么对于每个产生式B –>β和在First(γa)中的每个记号b都有项目[B->•β,b]的ε转换。
这个定义就是计算LR(1)的epsilon闭包,它与4.2.1节的epsilon闭包计算相似。注意它与LR(0)不同的地方,就是闭包的先行符号的计算,First(γa)所得到的b就是闭包项的先行符号。对应WACC的代码ComputerEClosure 函数和ComputerEPrecs 函数。LALR(1)生成的epsilon闭包的方式与LR(1)是一样的,4.2.2节给出的例子Test4同样也可以适用这个定义。
同样适用前面那个例子:
A1->A
A->( A )
A->a
构造LR(1)分析,可以得到如下状态图:
可以看到,与LR(0)文法生成的图表 48相比,图表 410状态有9个,规模比LR(0)大。
1.1.2 LR(1)分析方法
根据DFA可以构造分析算法。一般LR(1)分析算法如下:
(1) 若项目[A→α•aβ, b]属于Ik且GO(Ik,a)= Ij,a为终结符,则置ACTION[k,a]为“把状态j和符号a移进栈”,简记为“sj”。
(2) 若项目[A→α•, a]属于Ik,则置ACTION[k,a]为“用产生式A→α归约”,简记为“rj”;其中假定A→α为文法G’的第j个产生式。
(3) 若项目[S’→S•, $]属于Ik,则置ACTION[k, $]为“接受”,简记为“acc”。
(4) 若GO(Ik, A)= Ij,则置GOTO[k,A]=j。
(5) 分析表中凡不能用规则1到4填入信息的空白栏均填上“出错标志”。
根据该分析算法,得出图表 410所对应的分析表。
状态 | 输 入 | GoTo | |||
( | a | ) | $ | A | |
0 | S2 | S3 | 1 | ||
1 | 接受 | ||||
2 | S5 | S6 | 4 | ||
3 | R2 | ||||
4 | S7 | ||||
5 | S5 | S6 | 8 | ||
6 | R2 | ||||
7 | R1 | ||||
8 | S9 | ||||
9 | R1 |
图表 411
1.2 LALR(1)文法
观察图表 410,可以看到状态2和5、状态4和8、状态7和9、状态3和6中,它们的区别就是它们的先行不同。如果将这些状态合并,则组成LR(0)规模的自动机,而且并没有歧义。这种合并得到的就是LALR(1)分析方法。
1.2.1 LALR(1)两原则
原则[1] LR(1)项目的DFA的状态核心是LR(0)项目的DFA的一个状态。
原则[2] 若具有相同核心的LR(1)项目的DFA的两个状态s1和s2,若在符号X上有一个s1到状态t1的转换,那么X上还有一个从状态s2到t2的转换,且t1和t2具有相同的核心。
将LR(1)中核心相同的状态以及为每个LR(0)项目生成的先行合并起来,构成的DFA就是LALR(1)的DFA。其中,LALR(1)的规则与圆点部分与LR(0)是相同的,而不同的先行部分。实际上LALR(1)先行符号可以从LR(0)的DFA直接计算出来,而不必通过LR(1)的合并得到。
同样适用前面那个例子:
A1->A
A->( A )
A->a
先给出LALR(1)的DFA图。
1.2.2 先行符号传播算法
观察图表 412,我们先给定一个先行符号$添加到状态0中A’-> •A的先行集合中。$成为产生符号(genSymbol)或“假搜索符”。根据4.3.1节LR(1)的定义知道,先行符号可以通过定义1的对X的状态转换由某一状态传播得到的,或通过定义2的epsilon闭包自发生成的。所以,状态0通过闭包将$先行符号传给自己的闭包项。然后再传播给状态1,2,3。使得他们的核心项目均有先行符号$。再看状态2。[A->(•A),$]根据LR(1)定义2,生产闭包项[A - > •(A)]和[A->•a]其中先行符号为FIRST( )$ ) 得到 )为它们的先行集。同时,状态2又将现行符号传播给自己(得到[A->(•A),$/)])和状态3、4。如此下推就可得到图表 412的DFA状态图。
如此,我们可以总结一个算法,该算法就是先行符号传播算法,也是前面所说的反向传播算法。
假定I是一个LR(0)集,K是它的核,X是一个文法符号。对于GO(I,X)核中的每个项目A→αX•ρ,我们要构造它自生的所有先行符号集;同时指出,K中有哪些项目将把它们自生的搜索符传播给GO(I,X)。其中,$是一个假搜索符,用来指示何时出现传播的情况。
PROCEDURE SPONSOR(I,X);
/*I是一个LR(0)集,X是一个文法符号。
实际上我们并不需要项目集I而只需要它的核K*/
FOR I的核中的每个项目B→γ•δ DO
BEGIN
J:= CLOSURE({[B→γ•δ, $]});
求闭包
将first(δ a)传播至闭包项中先行集合中
IF[A→α•Xρ, a] ∈J但a不等于$
THEN GO(I,X)核中的[A→αX•ρ, a]的搜索符a是自生的
IF[A→α•Xρ, $] ∈J
THEN GO(I,X)核中的A→αX•ρ,的搜索符$是从K中的B→γ•δ传播过来
END
至此,上面的算法总结可能毫无意义。我们只要记住,LALR(1)状态的规则与圆点的定义和LR(0)一样,先行符号集要么通过go函数传播得到,要么通过epsilon闭包得到。
1.2.3 Test5测试函数
void test5()
{
CLanguage lang;
//定义一个符号集合V
CSymbol A1(1,"A1");//v1
CSymbol A(2, "A"); //v2
CSymbol LP(3,"("); //v3
CSymbol RP(4,")");//v4
CSymbol a (5,"a");//v5
//一.获得CLanguage之vector<CSymbol>
lang.InsertSymbol(A1); //1
lang.InsertSymbol(A);
lang.InsertSymbol(LP); //3
lang.InsertSymbol(RP); //4
lang.InsertSymbol(a); //5
showsym(lang.symbol); //显示之
//二.获得CLanguage之vector<Rules>
//1.获得rightpart之Vector<CSymbol*>
#define vlang(x) (&lang.symbol[x])
//rule1
vector<CSymbol*> rpart1;
rpart1.push_back(vlang( 2 ));
//2。获得leftpart--取得rules
CRules r1(vlang(1),rpart1);
//3.插入vector<Rules>
showrule( r1 );
//r2
vector<CSymbol*> rpart2;
rpart2.push_back(vlang(3));
rpart2.push_back(vlang(2));
rpart2.push_back(vlang(4));
CRules r2(vlang(2),rpart2);
showrule( r2 );
//r3
vector<CSymbol*> rpart3;
rpart3.push_back(vlang(5));
CRules r3(vlang(2),rpart3);
showrule( r3 );
cout<<" \nThe language's symbol is "<<endl;
showsym( lang.symbol );
lang.InsertRules(r1);
lang.InsertRules(r2);
lang.InsertRules(r3);
cout << "\nThe languge's rules is"<<endl;
for ( int i = 0; i<lang.rules.size(); i++)
showrule(lang.rules[i]);
#undef vlang
//三.获得开始符号
lang.SetStartSymbol(lang.symbol[1]);
cout<<"\nlanguage's startsymbol is "<<lang.startsym->name
<<endl;
//language初始化结束
//计算first集合
//1.计算所有终结符的first集合
//lang.FirstAllTerm();
//2.从rules中计算first集合
lang.ComputerFirstSet();
for ( i = 0; i<lang.symbol.size(); i++)
showFirst(lang.symbol[i],lang);
//设置一个item 并计算其closure
//I0
CItem firstItem(0,0);
CIset aIset(firstItem);
aIset.ComputeAllCore(lang,aIset.Icore);
dispIcore(aIset,lang);
}
注意该例子和Test1函数相比,调用的函数仅仅多了一个方框中的部分。第19行CItem firstItem(0,0)构成项目就是拓展文法A’->•A 。第20行中CIset表示项目集族,初始状态只有一个拓展文法一个项目。其它项目则由aIset.ComputeAllCore(lang,aIset.Icore)根据文法计算出来。
计算结果如下:
$: eposilon is 0 nt is 0 value is 0
A1: eposilon is 0 nt is 0 value is 1
A: eposilon is 0 nt is 0 value is 2
(: eposilon is 0 nt is 0 value is 3
): eposilon is 0 nt is 0 value is 4
a: eposilon is 0 nt is 0 value is 5
Rule: A1->A
Rule: A->( A )
Rule: A->a
The language's symbol is
$: eposilon is 0 nt is 0 value is 0
A1: eposilon is 0 nt is 1 value is 1
A: eposilon is 0 nt is 1 value is 2
(: eposilon is 0 nt is 0 value is 3
): eposilon is 0 nt is 0 value is 4
a: eposilon is 0 nt is 0 value is 5
The languge's rules is
Rule: A1->A
Rule: A->( A )
Rule: A->a
language's startsymbol is A1
$:firstset value is $
A1:firstset value is ( a
A:firstset value is ( a
(:firstset value is (
):firstset value is )
a:firstset value is a
I0:
A1->@A
The precs is :$
.A goto 1
.( shift 2
.a shift 3
I1:
A1->A @
The precs is :$
I2:
A->( @A )
The precs is :$ )
.A goto 4
.( shift 2
.a shift 3
I3:
A->a @
The precs is :$ )
I4:
A->( A @)
The precs is :$ )
.) shift 5
I5:
A->( A ) @
The precs is :$ )
注意框住的部分,它和图表 412表示的自动机是一样的,注意,与YACC一样,WACC只记录核心项,并没有记录闭包项,因为根据核心项可以计算出闭包项,可以节省空间。至此,我们WACC基本完成大部分了。其中核心算法已经实现。下面将依次介绍这些函数。
1.2.4 CIset 类
CIset定义:
class CIset
{
public:
vector <COREITEMTYPE> Icore;
CIset();
CIset(CItem firstItem);
virtual ~CIset();
void ComputeAllCore(CLanguage& L,vector <COREITEMTYPE>& vIcore);
int //返回changes
UnionCore(CLanguage &L,vector <COREITEMTYPE>& vIcore,int CoreInd);//找到新的核心项目并入vIcore,并传播precs,并得到转换函数
void ComputerE(COREITEMTYPE& core,vector<CItem>& Ivec,CLanguage &L);
private:
//Add core what's needed, if needed,Add and return 1, otherwise return 0
int AddCoreNeeded(vector<COREITEMTYPE> &vIcore, COREITEMTYPE& core,
int& newCoInd,CSymbol* &Sy, int index);
bool checkNew( vector<GO>& goVec, GO& ago);
bool checkExist(COREITEMTYPE &Icore, COREITEMTYPE& core, int &Ind);
int //返回changes
Union2Core(vector<CItem>& Vanother,vector<CItem>& Ivec);
};
参考Test5代码,按调用的先后顺序,我们先看CIset构造函数。
CIset::CIset(CItem firstItem)
{
COREITEMTYPE aCore ;
CItem aItem ;
aItem.Dot = firstItem.Dot;
aItem.RuleInd = firstItem.RuleInd;
aItem.precs.insert(0);//插入生成符号"$"
aCore.coreItem.push_back(aItem);
this->Icore.push_back(aCore);
}
该函数作用是添加了一条新的项目,而且插入了一条生成符号。因此,该函数一定只能初始化为拓展项目A’->•A或S’->•S等。上面有一个名为COREITEMTYPE结构,该结构定义如下:
typedef std::pair<CSymbol*,int> GO;//GO(I,X)=J, pair中表示X 和J
class COREITEMTYPE
{
public:
vector<CItem> coreItem; // I 为coreItem
vector<GO> goVec;//J为coreItem 在Iset中的序号--从0开始,X为符号
COREITEMTYPE(){};
~COREITEMTYPE(){};
} ;
coreItemType 用来记忆状态的核心项目,而goVec用来记忆该项目集可能转换到的所有状态。
1.2.5 CIset::ComputeAllCore函数
该函数根据拓展项目和文法构造LALR(1)的专案集。这些项目集与LALR(1)DFA是对应的。其中生成的项目集保存在vector <COREITEMTYPE>中。
void CIset::ComputeAllCore(CLanguage & L, vector <COREITEMTYPE> & vIcore)//Icore为核心集
{
//--------------+算法 +
//
// a)置项目S'->.S为初态项目集的核,然后对其求闭包,closure(S'->.S)初态项目集
int changes = 1;
int CoreInd = 0;
// b)对初态集或其它所构造项目集应用GO(I,X) = closure(J)得到J
//重复b,直到不再出现新的项目集为止
while (changes)
{
changes = 0;
for ( CoreInd = 0; CoreInd< vIcore.size(); CoreInd++)
{
changes += UnionCore(L,vIcore,CoreInd);
}
}
}
我们仍然使用vector来表示集合。注意阴影部分的注释。注释指出的计算闭包和go函数的做法在该段代码中没有体现出来。它放在UnionCore函数中。
观察while(changes)……那段代码,可以发现它与计算First集合和epsilon闭包的代码有些相似之处。它们的共同点从某个集合初始元素出发,生成元素到这个集合中去,直到不能再生成。如果您对离散数学中的关系熟悉,这种算法实际是关系的闭包算法。我们这些函数是在求解某个关系的闭包。
1.2.6 CIset::UnionCore函数
下面是UnionCore的代码。该函数的作用就是产生项目集,并生成相应的先行符号,它调用了ComputerE、go函数。在go函数之后,有对go函数生成的<X,I>序对(其中X是符号,I是某一状态的核心项),
int CIset::UnionCore(CLanguage &L,vector <COREITEMTYPE> &vIcore, int CoreInd)
{
int changes = 0;
vector<CItem> Ivec;
CSymbol* Sy;
int newCoInd = -1;
int i,j = 0;
COREITEMTYPE& aCore = vIcore[CoreInd];
//从一个vIcore中取出coreInd的元素,对其进行闭包运算,并计算precs从每个闭包元素计算Go,
//如果产生的item是vIcore中没有的,那么添加入,否则不添加
//支持多个核心Item,还必须修改下面两个语句
//aCore.coreItem.ComputerEClosure( L, Ivec);
//Ivec[0].ComputerEPrecs(L,Ivec);//计算precs
ComputerE(aCore,Ivec,L);
GO ago;
for ( i = 0; i < Ivec.size(); i++)
{
CItem oCoreItem;
COREITEMTYPE oCore ;
oCore.coreItem.clear();
if ( NULL != (Sy= Ivec[i].Go(oCoreItem, L)))//如果存在X
{
//如果Sy在vIcore[CoreInd].goVec中,说明它是一个符号产生
//多个核心项目,所以加到已有的状态集 中,changes也要加1
oCore.coreItem.push_back(oCoreItem);
//下面的函数保证针对同一符号不会产生多余的状态
changes += AddCoreNeeded(vIcore,oCore,newCoInd,Sy,CoreInd);
//GO ago(Sy,newCoInd);
if ( -1 != newCoInd && Sy != NULL)
{
ago.first = Sy;
ago.second = newCoInd;
//aCore已经发生改变,不可以再使用了,属于野指标问题
//if a new ,add ago in vIcore[CoreInd].govec
if (checkNew(vIcore[CoreInd].goVec,ago))
{
vIcore[CoreInd].goVec.push_back(ago);
changes++;
}
}
}
}
return changes;
}
该函数并不长,有若干个关键的函数,ComputerE、go函数和AddCoreNeeded。大家从注释可以看到这个函数修改过。WACC 使用ComputerE(aCore,Ivec,L)来计算先行和闭包,而不是直接使用ComputerEClosure 和ComputerEPrecs来计算,为什么呢?这是因为ComputerEClosure只能计算一个项目产生的闭包项目。ComputerEPrecs同样也只能计算一个项目epsilon转换得到的先行符号集。但是,实际上除了核心项可以产生闭包项之外,闭包项也可能产生闭包项。所以,ComputerE是这样实现的:
//用于计算每个核心项目的闭包item和precs
void CIset::ComputerE(COREITEMTYPE &core,vector<CItem>& Ivec,CLanguage& L)
{
//为了支持多个核心Item,还必须修改下面两个语句
//aCore.coreItem.ComputerEClosure( L, Ivec);
//Ivec[0].ComputerEPrecs(L,Ivec);//计算precs
//挨个item计算,只要changes != 0 就要继续
//while (changes)
//{
// changes = 0
// changes += 计算
//}
int changes = 1;
int i = 0;
//初始化
for ( i = 0; i < core.coreItem.size(); i++ )
{
Ivec.push_back(core.coreItem[i]);
}
vector<CItem> Vanother;
while (changes)
{
changes = 0;
for ( i = 0; i < Ivec.size(); i++ )
{
Vanother.clear();
Ivec[i].ComputerEClosure(L,Vanother);
Vanother[0].ComputerEPrecs(L,Vanother);
changes += Union2Core(Vanother,Ivec);
}
}
}
阅读框住的部分,Ivec.size()是会随着Union2Core函数的执行变化的。这个for循环就能保证闭包项生成的闭包项也能囊括在Ivec之中。
1.2.7 CItem::Go函数
Go函数较短,如下:
/* C I T E M . G O */
/*-----------------------------------------------------------
Owner: keyuchang
Copy right belong to keyuchang
Date 2008-2-16
Go(I,X) = J, 返回X, 和J, J为一个核心项目,同时将precs
传播出去
--------------------------------------------------------------*/
CSymbol* CItem::Go( CItem & goItem,CLanguage &L)
{
// 算法:
//*this 是包含在I 中的一个项目
//A->α.Xβ对应this, goItem对应A->αX.β
CRules& r = L.rules[this->RuleInd];
if ( Dot >= r.RightPart.size()) return NULL ;//Dot 后无符号
goItem.setDot( Dot+1 );
goItem.setRuleInd( RuleInd );
//并将this的precs 复制给goItem
goItem.precs.SetUnion(goItem.precs.set, precs.set);
return r.RightPart[Dot];
}
这段代码较为简单,不再做解释了。
1.2.8 小结
本章编程时大量使用vector容器来扮演集合的角色。这并不是一种很好的方法,集合计算可以位图操作来实现,或其它数据结构,效率可能会更高,但是使用vector容器可能更好理解。此外,本章所用的例子来自《编译原理及实践》一书。
本章已经介绍LALR(1)的核心部分,之后就进入语法文件解释器部分以及编译器代码自动生成一章,这一章比较简单。读者如果感兴趣,可以在此基础上添加冲突检测和算符优先级分析,便可将WACC做成真正的YACC了。如果读者弄通了本章,实现这两个功能,相信难度不大。