R(1)项目集族

本文详细介绍了LR(1)项目集族和LALR(1)文法,包括LR(1)的定义、闭包计算、分析算法以及LALR(1)的两原则。通过对LR(1)分析表的构造,展示了如何通过闭包传播算法生成LALR(1)的DFA。同时,文中提供了相关测试函数的代码示例,进一步阐述了LR(1)和LALR(1)的核心概念及其在编译原理中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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.121行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)分析,可以得到如下状态图:

图表 410

可以看到,与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图。

图表 412

1.2.2 先行符号传播算法

观察图表 412,我们先给定一个先行符号$添加到状态0中A’-> •A的先行集合中。$成为产生符号(genSymbol)或“假搜索符”。根据4.3.1LR(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了。如果读者弄通了本章,实现这两个功能,相信难度不大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值