3 如何机算?
机算和手算的算法是一样的,不过要使用机算的话,要将抽象的概念转换为具象代码,还是有一定的难度,但大家不用灰心,我们一步一步地来。
3.1 Language定义
Language与上面的文法是对应的,知道上面文法的定义为一个四元组(VN, VT, P, S),如何将文法定义具象至代码呢?C/C++没有元组的概念,但是不用着急,我们可以先将元组的特性都抛开,定义一个数据结构,然后针对这种数据结构设计一套操作,让这种结构看起来像元组。读者很快可以想到,定义一个结构体或类就可以了,实际上,WACC确实是这样做的。
如下面所示,定义Language类。
class CLanguage
{
public:
//string 是待计算的序列
bool FirstFromVect(vector<CSymbol*>& str,CFirstSet& vSet, int &changes);
void InsertSymbol(CSymbol& vSymbol);
void InsertRules(CRules& vRules);
bool ComputerFirstSet();
CLanguage(){ gensymbol = CSymbol(0,"$"); symbol.push_back(gensymbol);};
CLanguage(vector<CSymbol>& symbol,
vector<CRules>& rules);
void SetStartSymbol(CSymbol& start);
CSymbol gensymbol; //genarate symbol is $
vector<CSymbol> symbol;
vector<CRules> rules;
CSymbol *startsym;
void CopyVect(vector<CSymbol*> &stDes,
vector<CSymbol*> &stSource);
private:
void FirstAllTerm();// 由computerFirstSet调用
};
我们可以看到灰色底框框住的三个域sysmbol,rules 和startsym就是对应文法中元组的符号,产生式和开始符号。在这里,我们没有弄出四元组出来,符号是终结符还是非终结符符,采用一个符号中设置一个标志来区分(在后面Csysmbol有定义)。而且,是否为终结符还是非终结符,是通过计算得出来的,不用规定。此外,大家可能很奇怪gensymbol,这个符号一定会被指定为$,计算LALR(1)同心集时候才使用的。rules就是产生式集合。Rule英文含义是规则,在编译原理中与产生式(produce)同义,我们代码中使用rules术语来代替产生式术语。主要考虑是rules单词简单,不容易产生歧义。
在这里我们可以看到,元组的概念我们使用类来表示,集合我们使用vector容器来表示。我们只要保证插入时没有相同元素就可以保证集合的寓意。规定一组操作InsertSymbol和InsertRules,还有其它的一些函数现在不用理会。Language源于文法的抽象定义,但又与其有所不同,在坚持原则的基础上,又灵活多变,是咱中国人的强项哦,呵呵。
3.2 Symbol定义
class CSymbol
{
public:
int value;
std::string name ;
CSymbol(){};
CSymbol( int value,
std::string name, bool isNT = false ,
bool isEposilon = false );
void setEpsilon(bool isEposilon);
void setNT(bool isNT);
CFirstSet FirstSet;
bool isNT;//如果是非终结符,其值为true,否则为false
bool isEposilon;
};
在该类定义中,value是symbol的核心,一个symbol对应一个value,而且是唯一对应,以至于可以直接由value确定哪个symbol。其中gensymbol的value值为0,这是规定的。name是symbol对应的字符串,主要用于测试,打印name我们就知道给symbol的含义了,不然测试时我们还要记住冷冰冰的数字所代表的哪个symbol,那只会让我们痛苦。FirstSet用来该符号的First集。可从名字知道它的含义哦。上面的一节说过我们没有特意定义终结符和非终结符,它们是使用一个标志位来区分的,该标志位就是isNT,其中NT就是not termintor的意思(不是终结者机器人哦)。此外,也没有定义epslion,我们使用isEposilon表示first集是否包含epsilon。采用isEposilon可能含义不准确。呵呵。其操作的也很简单,看名字就知道含义了。
注意:symbol的实例均加入到Language的symbol集合中了,Rules等其它类只能操作Language的symbol集合中对外隐射出来的句柄,在WACC中使用的是引用或指针来表示symbol集合的元素的句柄。呵呵。(实际上也可以使用整数,可能会更安全些,但俺就是这样实现的)
3.3 Rules定义
class CRules
{
public:
CSymbol *LeftPart;
vector<CSymbol*> RightPart;
int RuleInd;
CRules(){};
CRules(CSymbol *LeftPart,
vector<CSymbol*>& RP);
private:
void insert(CSymbol *RightSymbol);
};
自然,在CRules定义中,LeftPart和RightPart与文法中产生式的左部和右部概念是对应的,含义也很清楚,所以没有注释。命名规范的变量和函数,可以省去注释,即使注释,也是无效的注释。这种不需要注释的代码往往也称自注释代码。在很多编程规范中都有这样的要求。WACC并不算太规范,很多地方是没达到规范要求的。呵呵。这里有RuleInd的含义是规则索引号,可能让大家看的奇怪,这个变量旁边应该要有注释的。RuleInd是用来指示Rule在Rule集合中的编号,唯一一个RuleInd对应唯一的Rules。注意,insert仅供CRules构造函数使用,不应暴露给类外,否则没办法保证Rule的左部右部完整语义。在一条规则中LeftPart一定不能为空,而RightPart可空。设置类成员的访问权限,保证语义是其一条妙处。
在CRule的构造函数中,LeftPart设置为isNT属性的。
3.4 计算First集
注意,为了便于测试,以上一些成员函数和成员变量绝大多数地定义成public,实际上有些函数只是内部使用不应该暴露给类外的成员。
3.4.1 从测试函数开始
下面是一个计算First集的测试函数,名字为test1( )
void test1()
{
CLanguage lang;
//定义一个符号集合V
CSymbol stmt_sequence(1,"stmt_sequence");
CSymbol stmt(2,"stmt");
CSymbol stmt_seq(3,"stmt_seq");
CSymbol semicolon(4,";");
CSymbol s(5,"s");
//一.获得CLanguage之vector<CSymbol>
vector<CSymbol> vSym;
lang.InsertSymbol(stmt_sequence);//1
lang.InsertSymbol(stmt); //2
lang.InsertSymbol(stmt_seq); //3
lang.InsertSymbol(semicolon); //4
lang.InsertSymbol(s); //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 ));
cout<<vlang(2)<<" part"<<endl;
cout<<rpart1[0]<<endl;
rpart1.push_back(vlang( 3 ));
//2。获得leftpart--取得rules
CRules r1(vlang(1),rpart1);
//3.插入vector<Rules>
showrule( r1 );
//r2
vector<CSymbol*> rpart2;
rpart2.push_back(vlang(4));
rpart2.push_back(vlang(1));
CRules r2(vlang(3),rpart2);
showrule( r2 );
//r3
vector<CSymbol*> rpart3;
CRules r3(vlang(3),rpart3);
showrule( r3 );
//r4
vector<CSymbol*> rpart4;
rpart4.push_back(vlang(5));
CRules r4(vlang(2),rpart4);
showrule(r4);
cout<<" \nThe language's symbol is "<<endl;
showsym( lang.symbol );
lang.InsertRules(r1);
lang.InsertRules(r2);
lang.InsertRules(r3);
lang.InsertRules(r4);
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();
lang.ComputerFirstSet();
for ( i = 0; i<lang.symbol.size(); i++)
showFirst(lang.symbol[i],lang);
//2.从rules中计算first集合
}
在main函数中直接调用test1()就可以了。如下所示:
void main()
{
cout<<"\n----Test1-------- "<<endl;
test1();
#if 0
cout<<"\n------Test2------ "<<endl;
test2();
cout<<"\n------Test21----- "<<endl;
test21();
cout<<"\n---------+Test3+----"<<endl;
test3();
cout<<"\n*******===Test4====***"<<endl;
test4();
cout<<"\n*******===Test5====***"<<endl;
test7();
test5();
test6();
test8();
test9();
test10();
test11();
#endif
}
注意,#if 0 ……. #endif 是一种预编译命令(框起来的部分),在它们之间的代码不会被编译进去,而不需直接删除代码或注释之,同时恢复起来也十分方便。是俺十分喜欢使用的一种删除命令。
执行结果如下:
----Test1--------
$: eposilon is 0 nt is 0 value is 0
stmt_sequence: eposilon is 0 nt is 0 value is 1
stmt: eposilon is 0 nt is 0 value is 2
stmt_seq: eposilon is 0 nt is 0 value is 3
;: eposilon is 0 nt is 0 value is 4
s: eposilon is 0 nt is 0 value is 5
00033A78 part
00033A78
Rule: stmt_sequence->stmt stmt_seq
Rule: stmt_seq->; stmt_sequence
Rule: stmt_seq->
Rule: stmt->s
The language's symbol is
$: eposilon is 0 nt is 0 value is 0
stmt_sequence: eposilon is 0 nt is 1 value is 1
stmt: eposilon is 0 nt is 1 value is 2
stmt_seq: eposilon is 1 nt is 1 value is 3
;: eposilon is 0 nt is 0 value is 4
s: eposilon is 0 nt is 0 value is 5
The languge's rules is
Rule: stmt_sequence->stmt stmt_seq
Rule: stmt_seq->; stmt_sequence
Rule: stmt_seq->
Rule: stmt->s
language's startsymbol is stmt_sequence
$:firstset value is $
stmt_sequence:firstset value is s
stmt:firstset value is s
stmt_seq:firstset value is ; epsilon
;:firstset value is ;
s:firstset value is s
由阴影部分可见,结果和我们手算的得出的结果是相同的。大家可以看到,在这里构造符号和规则十分麻烦。但是这仅仅是在初期测试算法是否正确的一种手段,最后大家可以看到,WACC最终是读取文件来构造符号和规则的。
后面的章节我们依次介绍该测试函数,游历计算First集的整个过程。注意测试代码中的阴影部分以及戴圈数字1,2,3,4.
3.4.2 CSymbol构造函数
在Test1()中第三行,我们看到,它首先定义了一个lang,类型为CLanguage,在默认的CLanguage构造函数中,会隐式构造一个symbol,称为gensymbol,即产生符号。序号为0,名为$并将它压入自己的symbol集合中,参见Language定义。
因此,在1中,不能以0开始,只能以1开始。0已经被gensymbol占用。此外,value值即符号出现的序号。CSymbol stmt_sequence(1,"stmt_sequence");表示创建stmt_sequence的符号,序号为1,名字为"stmt_sequence" ,其它的依次类推。
CSymbol 构造函数如下所示:
定义 :
CSymbol( int value,
std::string name, bool isNT = false ,
bool isEposilon = false );
实现:
CSymbol::CSymbol( int value,
std::string name, bool isNT ,
bool isEposilon )
{
this->value = value;
this->name = name;
this->isNT = isNT;
this->isEposilon = isEposilon;
}
可知,在Symbol构建过程中,isNT,和isEposilon是默认为false的,也就是说,是否包含eposilon,是否是非终结符,在创建时并没有确定下来。那么这两个属性是在什么时候确定的呢?不急,慢慢来。
3.4.3 CLanguage的 InsertSymbol 函数
上面说过,CRules以及下一章会讲到CItem等类,操作的symbol均是句柄,是Language中symbol集合对外映像出来的引用或指针。实例均在Language中symbol成员所包含的集合中。上面创建的symbol都是临时的对象,只有通过InsertSymbol函数插入Language中symbol集合中才能进行后续计算。实例与映像的关系可能很难理解,读者可以复习一下C/C++教材,慢慢体会。
Test1函数第14~18行,阴影部分戴圈2,注意执行操作的顺序与符号的序号是一致的,不要乱序,否则会有问题。我们现在测试的部分是WACC的底层初级部分,要注意这注意那,并不健壮。但是如果和其它部分结合起来,还是相对健壮的,是容错的。呵呵,大家现在还是要耐点心。
InsertSymbol实现很简单,仅一个语句
void CLanguage::InsertSymbol(CSymbol& vSymbol)
{
symbol.push_back(vSymbol);
}
但这里应该注意一点,可能是在编程时容易出错的地方,symbol定义为vector,而stl中vector每次push_back可能会导致vector空间重新分配,所以不要随意对vector中的元素取指针或引用,如果要使用,必须保证vector的中的元素已经稳定,不再进行插入或删除操作了。
3.4.4 CRules构造函数
CRules构造函数如下:
CRules::CRules(CSymbol *LeftPart,vector<CSymbol*>&
RP)
{
vector<CSymbol*>::iterator i;
this->LeftPart = LeftPart;
for ( i = RP.begin(); i!=RP.end(); i++)
{
insert(*i);
}
this->LeftPart->setNT(true);
if ( (this->RightPart).empty() )
{
this->LeftPart->setEpsilon( true );
}
}
在Test1函数中,第三十行戴圈3中 ,以及之后的代码中均可以看到CRule的构造函数的使用。在这里,你可以看到设置isNT的函数setNT(第10行)和setEpsilon(第13行)。现在您应该明白了凡是在左部的符号均设置为非终结符(not terminator)而如果右边为空,则左部first集一定包含epsilon的,而包含epsilon的不一定右部为空。所以设置epsilon的地方除此之外,还有其它地方要使用setEpsilon函数。这在后面的讲到的函数中会涉及到的。
3.4.5 CLanguage 的ComputerFirstSet 函数
参见Test1函数戴圈字4,约74行,我们就可以看到ComputerFirstSet函数。这个函数就是计算First集的。与2.1.3节的算法对应起来,我们来看看这个函数的代码
bool CLanguage::ComputerFirstSet()
{
FirstAllTerm();//找出所有terminate symbol 的first集合
int changes = 1; //初始化为1
bool isEpsi ;
int rIndex ; //rules index
//if there is any changes do it cycle
while ( changes )
{
changes = 0;
//for each rule choice A->X1X2X3...Xn
for ( rIndex = 0; rIndex < this->rules.size(); rIndex++ )
{
isEpsi = FirstFromVect((this->rules[rIndex]).RightPart,
(this->rules[rIndex]).LeftPart->FirstSet, changes);
(this->rules[rIndex]).LeftPart->setEpsilon(isEpsi);
}
}
return false;
}
这段代码基本与2.1.3节的算法描述是一致的。注意框住的函数名FirstFromVect,该函数作用就是计算一条规则产生的First集。第一个参数是规则的右部,第二个参数就是LeftPart的First集。还有一个参数是change。用来表示First集是否可增大。它还有一个返回值表示右部元素是否均包含epsilon,如果包含,则将LeftPart也要设置epsilon。
FirstFromVect函数如下实现:
bool CLanguage::FirstFromVect(vector<CSymbol*>& str,CFirstSet& vFirst,
int& changes)
{
int k = 1-1;
int n = str.size();
bool bContinue = true;
while ( bContinue && k<= n-1 )
{
//add first(Xk)-epsilon to vFirst
changes += vFirst.SetUnion( vFirst.set, str[ k ]->FirstSet.set );
//if epsilon NOT in First(Xk) then Continue = false
if ( !(str[ k ]->isEposilon) )
{
bContinue = false;
}
k = k + 1;
}
return bContinue;//from the vector we know wether it has epsilon
}
在上面的框中,您是否看到2.1.3节的算法的影子了呢?
至此,First集合计算已经接近尾声。该函数还包含了FirstAllTerm ,它的作用正如注释所说,用来找出terminate symbol的first集。这里不再赘述。
3.5 小结
本章的带着读者从文法等基础概念,游历到实际的代码的全过程。采用的C++面向对象的思想,而不是纯C的过程编程方法。思维如何从抽象到具象的平滑过渡,或从具象平滑到抽象。是研究编译原理的难点,也是一种乐趣。国外的大师能在具象与抽象自由穿梭,能在哲学与具体问题之间架设通畅的桥梁。软件中的对象思想以及模块化的思想,均可能来自工程管理的经验,但引入计算机领域便掀起了一场革命。而目前计算机领域大多数的新技术均是美国发起的,而非我国。很可能与东方人(包括我国和日本)喜欢沉迷于具体问题有关。我国的“道”(哲学)与“艺”(技术)是脱节的。学哲学的人只能在抽象的世界里徘徊,学成后只能到学校去教书。而学技术的人恰恰相反,往往在具象中不能自拔,只能算一匠,而非大师。
在使用C++编程应该注意这样的顺序,先确定数据结构,数据结构与实际问题的对象是密切相关的,数据结构定义得如何直接影响后期的框架。再是定义接口和框架,这个过程较为抽象。最后才实现各个函数的算法。框架搭得好,后面的实现就完全是体力活了。很多程序员有最大的一个毛病就是一开始就写代码,从不去设计,不去做模块化工作。编码中途不能被打断,往往要熬夜加班才能勉强将一个小功能的程序做好,如果中途睡了一觉,睡醒了坐在计算机旁就不知道后面的代码该怎么写了。这就是不会抽象,只能在具象中徘徊的表现。这样的程序员不具备做大型程序开发的能力。
当然,WACC并不算是个大程序,算是抛砖引玉,引领大家进门而已。此外,大家要注意类的封装性,慎用全局变量。养成测试的习惯,写的代码尽早测试尽早解决Bug。避免在后期这些Bug引起“蝴蝶效应”。