??xml version="1.0" encoding="utf-8" standalone="yes"?>
在这里我单地介绍一下用法。假讑֤家觉得vlppQVczh Library++Q也是GacUI用的那个库)的WString啊Listq些东西在调试器里面昄出来的东西太丑,可以用以下三步来修改它?/p>
1Q去http://gac.codeplex.com/SourceControl/changeset/view/99419#2395529下蝲我写的那个natvis文g。这个文件在整个zip包里面的位置是Common\vlpp.natvis
2Q把q个文g复制到C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Packages\Debugger\VisualizersQ如果用默认安装\径的话)
3Q重启你最喜爱的Visual Studio 2012Q就可以看到下面的东西了Q?/p>
List<Nullable<vint>>的互相嵌套也是如此的完美
如果大家惛_自己的Customized Visualizer的话Q可以去参考微软良心提供的文档http://msdn.microsoft.com/en-us/library/vstudio/jj620914.aspxQ然后按照上面的步骤写自qnatvis文g。在q里我把我的natvis贴上来,以供参考:
<?xml version="1.0" encoding="utf-8"?> <AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> <Type Name="vl::ObjectString<wchar_t>"> <DisplayString>{{ size={length}, buffer={buffer+start,su} }}</DisplayString> <StringView>buffer+start,su</StringView> <Expand> <Item Name="[size]">length</Item> <ArrayItems> <Size>length</Size> <ValuePointer>buffer+start</ValuePointer> </ArrayItems> </Expand> </Type> <Type Name="vl::ObjectString<char>"> <DisplayString>{{ size={length}, buffer={buffer+start,s} }}</DisplayString> <StringView>buffer+start,s</StringView> <Expand> <Item Name="[size]">length</Item> <ArrayItems> <Size>length</Size> <ValuePointer>buffer+start</ValuePointer> </ArrayItems> </Expand> </Type> <Type Name="vl::collections::List<*,*>"> <AlternativeType Name="vl::collections::SortedList<*,*>"/> <AlternativeType Name="vl::collections::Array<*,*>"/> <DisplayString>{{ size={count} }}</DisplayString> <Expand> <Item Name="[size]">count</Item> <ArrayItems> <Size>count</Size> <ValuePointer>buffer</ValuePointer> </ArrayItems> </Expand> </Type> <Type Name="vl::collections::Dictionary<*,*,*,*>"> <AlternativeType Name="vl::collections::Group<*,*,*,*>"/> <DisplayString>{{ size={keys.count} }}</DisplayString> <Expand> <Item Name="[size]">keys.count</Item> <Item Name="Keys">keys</Item> <Item Name="Values">values</Item> </Expand> </Type> <Type Name="vl::Ptr<*>"> <AlternativeType Name="vl::ComPtr<*>"/> <DisplayString Condition="reference == 0">[empty]</DisplayString> <DisplayString Condition="reference != 0">{*reference}</DisplayString> <Expand> <Item Condition="reference != 0" Name="[ptr]">reference</Item> </Expand> </Type> <Type Name="vl::Lazy<*>"> <DisplayString Condition="internalValue.reference == 0">[empty]</DisplayString> <DisplayString Condition="internalValue.reference != 0 && internalValue.reference->evaluated == false">[not evaluated]</DisplayString> <DisplayString Condition="internalValue.reference != 0 && internalValue.reference->evaluated == true">{internalValue.reference->value}</DisplayString> <Expand> <Item Condition="internalValue.reference != 0 && internalValue.reference->evaluated == true" Name="[value]">internalValue.reference->value</Item> </Expand> </Type> <Type Name="vl::ObjectBox<*>"> <DisplayString>{object}</DisplayString> <Expand> <ExpandedItem>object</ExpandedItem> </Expand> </Type> <Type Name="vl::Nullable<*>"> <DisplayString Condition="object == 0">[empty]</DisplayString> <DisplayString Condition="object != 0">{*object}</DisplayString> <Expand> <ExpandedItem Condition="object != 0">*object</ExpandedItem> </Expand> </Type> </AutoVisualizer>
所以在q里我们先把几条文法的最后的状态机都列出来Q大图)Q?/p>
接下来的q一步,是要对所有靠非终l符QExp啊Termq些Q进行蟩转的transition都执行上一文章所说的传说中的交叉链接。在产生链接的时候,我们lshift和reduce的边分别加上shift和reduce。而shift和reduce是有参数?#8212;—是被shift走的状态的id。这样可以在parse的时候匹配和处理状态堆栈。在q里我门对e3->e1q一步做一下操作做Z子。红色的Ҏ被删掉的Q而粗壮的l色Ҏ被新加进ȝQ?/p>
U色的边变成了两条绿色的边,U色的边附带的信息则被复制到了绿色的reduce边上。当我们使用q个状态机的时候,shift(s3)pC往堆栈里面压入s3Qreduce(s3)pCZ堆栈里面弹出(s3)。当然弹Z一定会成功Q所以如果不成功的话Q这条边׃能在当时使用。因此这也就是ؓ什么在e3跌{到t0之后Qt1知道往回蟩的是e1而不是别的什么地?#8212;—如同ؓ什么C++的函数执行完之后L知道如何跌{回调用它的地方一?#8212;—因ؓ把信息推入了堆栈?/p>
那现在我们就来看一下,当所有的非终l符跌{都处理掉之后Q会变成什么样子吧Q这个图真是复杂和ؕ到我不想dQ,Z让图变得不那么糟p,我把shift都变成Ԍreduce都变成绿Ԍ
在添加shift和reduce边之前,每一条边都是有输入token的。但是我们刚刚添加上ȝshift和reduce边却是不输入token的,所以他们是epsilon边,下一步就是要消除他们。上面这个图消除了epsilon边之后,会变成一个状态很,但是每一条边附带的信息都会非常多Q而且像n1q种l常到达的状态(因ؓ四则q算里面有很多数字)恢复射出无数条辏V到了这里这个状态机已经再也M出来了。所以我下面只拿两个例子来甅R下面要展示的是用Exp来parse单独的一个数字会走的边,当然是Exp –> Term –> Factor –> Number了:
׃被处理成Q?/p>
注意边上面的信息是要按顺序重新叠加在一L。当所有的epsilon辚wL了之后,我们得C最l的一个状态机。最重要的一件事情出C。我们知道,发明LR和LALRq种东西基本上是ؓ了处理左递归的,所以这U图可以在去除epsilon边的q程中自动发现左递归。这是怎么做到的呢Q只要在去除epsilon边的时候,发现了一条完全由shiftq种epsilon边组成的环,那么左递归发C。ؓ了方便,我们可以只处理直接左递归——是q种环的长度?的。不包含间接左递归的问法是很容易写出来的。当然这U环q不一定是首尾相接的,譬如说我们在处理e0的时候就会发现e0->t0->t0q种环(当然严格来说环只有最后一截的q个部分Q。我们的E序要很好地应对q种情况。因为我们只接受直接左递归Q所以类DU结构的epsilon路径可以直接的抛弃他Q因为t0->t0会被t0状态单独处理掉。因此这样做q不会漏掉什么?/p>
l心的朋友可能会发现Q这个结构的图是不能直接处理右递归的(M左递归和右递归总要有一个会让你的状态机傻逼就是了Q)。关于如何处理有递归Q其实内容也不复杂)地方法会?#8220;下篇”描述出来。那处理左递归有什么用呢?举个例子Q我们的e0->e2是一个左递归Q而他会在之前的步骤被处理成shift(e0->e0)和reduce(e1->e2)。我们要Cshift和reduce的对应关p,那么当我们找C个左递归的shift之后Q我们就可以把对应的reducel标记成“left-recursive-reduce”。这是一个在构造语法树的时候,非常关键的一U构造指令?/p>
处理完这些之后,我们可以把左递归的shift边全部删掉,最后把token和state都统l处理成q箋的数字,做成一张[state, token] –> [transitions]的二l表Q每一个表的元素是transition的列表。ؓ什么是q样呢?因ؓ我们对一个state输入一个token之后Q由于保存着state的堆栈(你还记得吗?shift==pushQreduce==popQ的栈顶若干个元素的不同Q可能会C通的路线。于是最后我们就得到了这么一张图?/p>
下面q张囑֏以通过q行gac.codeplex.com上面的Common\UnitTest\UnitTest.slnQVS2012限定Q之后,在Common\UnitTest\TestFiles\Parsing.Calculator.Table.txt里面扑ֈ。这一l文仉是我在测试状态机的时候log下来的?/p>
如果大家有VS2012的话Q通过q行我准备的几个输入Q譬如说“1*2+3*4”Q就可以在Parsing.Calculator.[2].txt里面扑ֈ所有状态蟩转的轨迹。因为我们L需要parse一个ExpQ所以我们从22: Exp.RootStart开始。我们假设token stream的第一个和最后一个分别是$TokenBegin?TokenFinish。上囄$TryReduce是ؓ了应对右递归而设计出来的一U特D输入。由于四则运里面ƈ没有右递归Q所以这一列就是空的:
StartState: 22[Exp.RootStart]
$TokenBegin => 23[Exp.Start]
State Stack:
NUMBER[1] => 2[Number.1]
State Stack: 23[Exp.Start], 21[Term.Start], 19[Factor.Start]
Shift 23[Exp]
Shift 21[Term]
Shift 19[Factor]
Assign value
Create NumberExpression
MUL[*] => 5[Term.3]
State Stack: 23[Exp.Start]
Reduce 19[Factor]
Using
Reduce 21[Term]
Using
LR-Reduce 21[Term]
Assign firstOperand
Setter binaryOperator = Mul
Create BinaryExpression
NUMBER[2] => 2[Number.1]
State Stack: 23[Exp.Start], 5[Term.3], 19[Factor.Start]
Shift 5[Term]
Shift 19[Factor]
Assign value
Create NumberExpression
ADD[+] => 10[Exp.3]
State Stack:
Reduce 19[Factor]
Using
Reduce 5[Term]
Assign secondOperand
Reduce 23[Exp]
Using
LR-Reduce 23[Exp]
Assign firstOperand
Setter binaryOperator = Add
Create BinaryExpression
NUMBER[3] => 2[Number.1]
State Stack: 10[Exp.3], 21[Term.Start], 19[Factor.Start]
Shift 10[Exp]
Shift 21[Term]
Shift 19[Factor]
Assign value
Create NumberExpression
MUL[*] => 5[Term.3]
State Stack: 10[Exp.3]
Reduce 19[Factor]
Using
Reduce 21[Term]
Using
LR-Reduce 21[Term]
Assign firstOperand
Setter binaryOperator = Mul
Create BinaryExpression
NUMBER[4] => 2[Number.1]
State Stack: 10[Exp.3], 5[Term.3], 19[Factor.Start]
Shift 5[Term]
Shift 19[Factor]
Assign value
Create NumberExpression
$TokenFinish => 11[Exp.RootEnd]
State Stack:
Reduce 19[Factor]
Using
Reduce 5[Term]
Assign secondOperand
Reduce 10[Exp]
Assign secondOperand
我们把所有蟩转过的transition的信息都记录下来Q就可以构造语法苏了。我们想象一下,在执行这些指令的时候,遇到NUMBER[4]q于获得了一个内容ؓ4的tokenQshift的话是往堆栈里面pushq一个状态的名字Q而reduce则是弹出?/p>
相对应的Q因为每一个文法都会创Z个对象,所以我们在初始化的时候,要先放一个空对象在堆栈上。shift一ơ就再创Z个空的对象pushq去Qreduce的时候就把栈的对象弹出来作?#8220;待处理对?#8221;Qusing了就把待处理对象和栈对象合qӞleft-reduce是把栈对象弹出来作ؓ待处理对象的同时Qpush一个空对象q去。assign fieldName是?#8220;待处理对?#8221;保存到栈对象的叫做fieldName的成员变量里面去。如果栈对象ؓI,那么被保存的对象是刚刚输入的那个token了。因此我们从头到执行一遍之后,可以得C面的一颗语法树Q?/p>
BinaryExpression {
binaryOperator = [Add]
firstOperand = BinaryExpression {
binaryOperator = [Mul]
firstOperand = NumberExpression {
value = [1]
}
secondOperand = NumberExpression {
value = [2]
}
}
secondOperand = BinaryExpression {
binaryOperator = [Mul]
firstOperand = NumberExpression {
value = [3]
}
secondOperand = NumberExpression {
value = [4]
}
}
}
基本上parsing的过E就l束了。在“下篇”——也就是(六)——里面Q我会讲q如何处理右递归Q然后这个系列基本上p完结了?/p>
像?a href="http://www.shnenglu.com/vczh/archive/2008/05/22/50763.html" target="_blank">构造正则表辑ּ引擎》一般给状态机d信息的方法,是把一些附加的数据加到状态与状态之间的跌{头里面厅Rؓ了Ş象的表达q个事情Q我拿W一文章的四则q算式子来D例。在q里我ؓ了大家方便,重复一下这个文法的内容Q除M语树书声明)Q?/p>
token NAME = "[a-zA-Z_]/w*"; token NUMBER = "/d+(./d+)"; token ADD = "/+"; token SUB = "-"; token MUL = "/*"; token DIV = "//"; token LEFT = "/("; token RIGHT = "/)"; token COMMA = ","; rule NumberExpression Number = NUMBER : value; rule FunctionExpression Call = NAME : functionName "(" [ Exp : arguments { "," Exp : arguments } ] ")"; rule Expression Factor = !Number | !Call; rule Expression Term = !Factor; = Term : firstOperand "*" Factor : secondOperand as BinaryExpression with { binaryOperator = "Mul" }; = Term : firstOperand "/" Factor : secondOperand as BinaryExpression with { binaryOperator = "Div" }; rule Expression Exp = !Term; = Exp : firstOperand "+" Term : secondOperand as BinaryExpression with { binaryOperator = "Add" }; = Exp : firstOperand "-" Term : secondOperand as BinaryExpression with { binaryOperator = "Sub" };
那么我们把这个文发{成状态机之后Q要l蟩转加上什么呢Q从直觉上来_跌{的时候我们会有六U要q的事情Q?br />1、CreateQ这个文法创建的语法树节Ҏ某个cd的(区别于在q一ȝq个问法创徏一个返回什么类型的语法树节点)
2、SetQ给创徏的语法树节点的某个成员变量设|一个指定的?br />3、AssignQ给创徏的语法树节点的某个成员变量设|这一ơ蟩转的W号产生的语法树节点Q譬如说Exp = Exp: firstOperand “+” Term: secondOperandQ走Term的时候,一个语法树节点׃被assignl那个叫做secondOperand的成员变量)
4、UsingQ用这一ơ蟩转的W号产生的语法树节点来做q次文法的返回|譬如说Factor = !Number | !Callerq一条)
5、ShiftQ略
6、ReduceQ略
在这里我们ƈ没有标记整个文法从哪一个非l结W开始,因ؓ在实际过E中Q其实分析师可以从Q何一个文法开始的。譬如说写IDE的时候,我们可能在某些情况下仅仅只需要分析一个表辑ּ。所以考虑到每一个非l结W都有可能被用到Q因此我们的“Token开?#8221;?#8220;Token结?#8221;׃在每一个非l结W的状态机中都出现。因此在W一步创建Epsilon PDAQ下推自动机Q的时候,可以先直接生成。在q里我们拿Exp做例子:
双虚U代表的是Token和Token结束,qƈ不是我们现在兛_的事情。在剩下的{换中Q实现是h输入的{换,而虚U则是没有输入的转换Q一般称为epsilon边)?/p>
在这里我们要明确一个概?#8212;—分析路径。分析\径代表的是token?#8220;?#8221;q状态机的时候,状态是如何跌{的。因此对于实际的分析q程Q分析\径其实就是分析树的一U表辑Ş式。而在状态机里面Q分析\径则代表一条从开始到l尾的可能的路径。譬如说在这里,分析路径可以有三条:
$e –> e1 –> e2 –> e$
$e –> e3 –> e8 –> e7 –> e6 –> e5 –> e4 –> e$
$e –> e9 –> e14 –> e13 –> e12 –> e11 –> e10 –> e$
因此我们可以清楚Q一条\径上是不能出现多个create的,否则你就不知道应该创建的是什么了。当然create和using都不能同时出玎ͼusing也不能有多个。而且׃create和set都是在描q这个非l结W(在这里是ExpQ所创徏的语法树节点的类型和属性,跟执行他们的时机无关Q所以其实在同一条分析\径里面,create和set攑֜哪里都没关系。就譬如说在上面的第二条分析路径里面Qcreate是在e6->e5里面标记出来的。就他UdCe3->e8Q做的事情也一栗反正只要一条\径上标记了createQ那么他在这条\径被定之后Q就一定会create所指定的具体类型的语法树节炏V这是相当重要的Q因为在后面的分析中Q我们很可能需要移动create和set的具体位|?/p>
跟上一文章说的一P接下来的一步就是去除epsilon边了。结果如下:
面对q种状态机Q去除epsilon边就不能跟处理正则表辑ּ一L单的去除了。首先,所有的l结状?#8212;—也就是所有经q或者不l过epsilon边之后,通过“Token结?#8221;W号q接到最后一个状态的状态,在这里分别是e2、e6和e12——都是不能删掉的。而且所有的“Token开?#8221;?#8220;Token结?#8221;——也就是图里面?转换——是不能带有信息的。所以我们就会看到e6后面的信息全部被UdCe7->e6q条边上面。由于create和set的流动性,我们q么做对于状态机的定义完全没有媄响?/p>
Cq里q没完,因ؓq个状态机q是有很多冗余的状态的。譬如说e8和e14、e7和e13、e2和e6和e12实际上是可以合ƈ的。合q的{略其实十分单:
1、如果我们有跌{e0->e1和e0->e2Qƈ且两个蟩转所携带的token输入和信息完全一致的话,那么e1和e2可以合q?br />2、如果我们有跌{e1->e0和e2->e0Qƈ且两个蟩转所携带的token输入和信息完全一致的话,那么e1和e2可以合q?/p>
所以对于e8和e14我们是完全可以合q的。那么e7和e13怎么办呢Q根据create和set的流动性,我们只要把这两个东西挪到他的前面一个或者若q个跌{去,那这两个状态就可以合ƈ了。ؓ了让法更加的简单,我们遇到两个跌{cM的时候,L先挪动create和setQ然后再看看是不是真的可以合q。所以这一步处理完之后׃变成下面q个样子Q?/p>
我们在不改变状态机语义的情况下Q把Exp的三个状态机最l压~成了这个样子。看q上一文章的同学们都知道Q下一步就是要把所有的状态机l统都连接v来了。关于在q接的时候如何具体操作{换附带的信息、以及做Z个确定性的下推状态机的所有事情将在下一文章详l解释。大家敬h待?/p>
故事׃状态机开始。文法我׃重复了,见上一文章。现在我们从状态机开始。第一个状态机是直接从文法变过来的Q?/p>
然后我们把所有的非终l符跌{都通过Shift和Reduceq接到该非终l符所代表的状态机的状态上面,׃变成下面的图。具体的做法是,对于每一条非l结W的跌{Q譬如说S0 –> Symbol –> S1。首先抹掉这条蟩转。然后增加两条边Q分别是S0到Symbol的v始节点,操作是Shift<S0>。还有从Symbol的终l节点到S0Q操作是Pop<S0> Reduce。Shift<S>{于把状态Slpush到堆栈里Q然后Pop<S>{于在状态里面弹出内ҎS的栈元素。如果失败了怎么办呢Q那׃能用q条跌{。跟上图一P所有输?跌{到Finish的边Q操作都是要Pop<Null>的。在刚开始分析的时候,堆栈有一个Null|用来代表“语法分析从这里开?#8221;?/p>
q个囄_虚边代表所有跟左递归有关的蟩转。这些边是成对的Q分别是左递归跌{的Shift和Reduce。如果不是ؓ了实现高性能的语法分析的话,其实q个状态机已经_了。这个图跟语法分析的“状态蟩转轨q?#8221;有很大的关系。虽然IDList0你不知道W一步要跌{到IDList0q是ID0Q不q没关系Q现在我们先假设我们可以通过某种秘的方法来预测到。那么,当输入是A,B,C$的时候,状态蟩转轨q就会是如下的样子:
Z么要q么做呢Q我们把q幅图想象成?br>1Q想做的头表示push一个状?br>2Q向下的头表示修改当前状?br>3Q向右的状态表Cpop一个状态ƈ修改当前状?/p>
因此当输入到B的时候,到达ID1Qƈ跌{到IDList1。这个时候IDList1【左辏V的所有【还留在堆栈里】的状态时Null和IDList0Q当前状态IDList1Q输入剩?C$。这个图特别的有用。当我们分析完ƈ且把构造语法树的指令附着在这些箭头上面之后,按顺序执行这些指令就可以构造出一颗完整的语法树了?/p>
但是在实际操作里面,我们q没有办法预?#8220;q里要左递归两次”Q也没办法在多次reduce的时候选择I竟要从哪里跛_哪里。所以实际上我们要学习从EpsilonNFA到DFA的那个计过E,把Shift和Reduce当成EpsilonQ把吃掉一个token当成非Epsilon边,然后执行我之前写的?a href="http://www.shnenglu.com/vczh/archive/2008/05/22/50763.html" target="_blank">构造可配置词法分析?/a>》一文中的那个去Epsilon边算法(如何从Nondeterministic到DeterministicQ以及相关的Look AheadQ是下一文章的内容Q,然后可以把状态机变成q样Q?/p>
上面_体的Pop<IDList0>表示Q这一个Pop是对应于那个左递归Shifting操作的。实际上q是做了一个怎样的变化呢Q从“物理解释”上来Ԍ其实是把“状态蟩转轨q?#8221;里面那些除了左递归shifting之外的所有不吃掉token的边都去掉了Q?/p>
在这里我们可以看刎ͼZ么当堆栈是IDList0, IDList0和IDList0, IDList3的时候,从ID0都可以通过吃掉一?#8221;,”从而蟩转到IDList3。在上面q张“状态蟩转轨q?#8221;里面Q这两个事情都发生了Q分别是W一条向左的头和第二条向左的方向。而且q两条边刚好对应于上囑ָ有蓝色粗体文字的跌{Q属于左递归Reducing操作?/p>
所以,其实在这个时候,我们同时解决?#8220;应该在什么时候进行左递归Shifting”的问题。只要当左递归Reducing已发生,我们立刻在轨q上面补上一条左递归Shifting好了。因此,我们在一开始做parsing的时候,Ҏ不需要预先做左递归Shifting。所以当刚刚输入A的时候,“状态蟩转轨q?#8221;是这样子的:
然后遇到一?#8221;,”Q发C?#8220;做漏”了一个左递归ShiftingQ因此就变成下面q个样子Q?/p>
q也是上一文章那个Fake-Shift所做的事情了?/p>
Q对学术感兴的人可以去wiki一?#8220;下推自动?#8221;Q?/p>
下推自动机和有限自动机的区别是,下推自动机扩展成普通的自动机的时候,他的状态的数目是无限的Q废话)。但是无限的东西是没办法用编E来表达的,那怎么办呢Q那加入一个不定长度的“状态描q?#8221;。下面我举一个简单的文法Q?/p>
ID = NAME
IDList = ID | IDList “,” ID
q样构成了一个简单的文法Q用来分析带逗号分割的名字列表的。那么写成状态机是如下的Ş式:
ID0 = ?NAME
ID1 = NAME ?br>IDList0 = ?(ID | IDList “," ID)
IDList1 = (ID | IDList “,” ID) ?br>IDList2 = (ID | IDList ?“,” ID)
IDList3 = (ID | IDList “,” ?ID)
ID0 –> NAME –> ID1
IDList0 –> ID –> IDList1
IDList0 –> IDList –> IDList2
IDList2 –> “,” –> IDList3
IDList3 –> ID –> IDList1
可以很容易的看出QID0和IDList0是文法的起始状态,而ID1和IDList1是文法的l结状态,L囑֦下:
QPowerPointd复制到LiveWriter里面是一q图面简直太方便了)
但是q样q没完。IDList0跛_IDList2的时候的输入“IDList”其实q不够,因ؓ用作输入的token其实只有NAME?,"两种。下一步即演C如何从q个状态机~程名副其实的下推状态机?/p>
在这里我先介l几个概c第一个是U进Q第二个是规U。ؓ什么要用这两个名字呢?因ؓ大部分h看的傻逼清华大学出版社的低能编译原理课本都是这么讲的,黑化分别叫Shift和Reduce。好了,什么是Shift呢?IDList0跛_IDList2的时候,要移qIDList。IDList3跛_IDList1Q要U进到ID。IDList0跛_IDList1也要U进到ID。这也就是说Q?strong>状态{Uȝq一条非l结W的边的时候会U进到另一条文法的状态机?/strong>。ID1和IDList1作ؓID和IDList的终l节点,要根?#8220;从那里移q来?#8221;分别规约然后跌{?#8220;IDList2或者IDList1”。这也就是说Q?strong>一旦你到达了一条闻法的状态机的终l状态,p开始规U然后蟩转到上一U的状态了?/p>
1、大部分情况下都是真的需要有语法?br>2、如果要直接q回计算l果之类的事情的话,只需要写一个visitorq行一下语法树好了,除去自动生成的代码以外(反正q不用h写,不计入代PQ代码量基本上没什么区?br>3、加入语法树可以让文法本w描qv来更单,如果要让E序员把文法单独攑֜一边,然后自己写完整的语义函数来让他生成语法树的话Q会让大部分情况Q需要语法树Q变得特别复杂,而少数情况(不需要语法树Q又没有获得什么好处?/p>
管cMyaccq样的东西的是不包含语法树的内容而要你自己写的,但是用v来难道不是很隑֏吗?
现在转入正题。这一文章讲的主要是构造符可的问题。想要把W号表构造的好是一件很ȝ的问题。我曄试q很多种ҎQ包括强cd的符可Q弱cd的符可Q基于map的符可{等Q最后还是挑选了跟Visual Studio自带的用来读pdb文g的DIAcd中的IDIASymbolQ?a title="http://msdn.microsoft.com/en-us/library/w0edf0x4.aspx" target="_blank">http://msdn.microsoft.com/en-us/library/w0edf0x4.aspxQ基本上一Ll构Q所有的W号都只有这么一个symbolc,然后包罗万象Q什么都有。ؓ什么最后选择q么做呢Q因为在做语义分析的时候,其实做的最多的事情不是构造符可Q而是查询W号表。如果符可是强cd的画Q譬如说cd要一个类Q变量要一个类Q函数要一个类之类的,L需要到处cast来cast去,也找不到什么好Ҏ来在完成相同事情的情况下Q保留强cd而不在代码里面出现cast。ؓ什么语法树p用visitor来解册个问题,而符可׃行呢Q因为通常我们在处理语法树的时候都是递归的Ş式,而符可q不是。在一个上下文里面Q实际上我们是知道那个symbol对象I竟是什么东西的Q譬如说我们查询了一个变量的typeQ那q返回D定只能是type了)。这个时候我们要cast才能用,本n也只是浪费表情而已。这个时候,visitor模式׃是和面对q种情况了。如果硬要用visitor模式来写Q会D语义分析的代码分散得q于谱D可读性几乎就丧失了。这是一个辩证的问题Q大家可以好好体会体会?/p>
说了q么一大段Q实际上是怎么样呢Q让我们来看“文法规则”本n的符可吧。既然这个新的可配置语法分析器也是通过parse一个文本Ş式的文法规则来生成parserQ那实际上就跟编译器一栯l历那么多阶D,其中肯定有符可Q?/p>
class ParsingSymbol : public Object { public: enum SymbolType { Global, EnumType, ClassType, // descriptor == base type ArrayType, // descriptor == element type TokenType, EnumItem, // descriptor == parent ClassField, // descriptor == field type TokenDef, // descriptor == token type RuleDef, // descriptor == rule type }; public: ~ParsingSymbol(); ParsingSymbolManager* GetManager(); SymbolType GetType(); const WString& GetName(); vint GetSubSymbolCount(); ParsingSymbol* GetSubSymbol(vint index); ParsingSymbol* GetSubSymbolByName(const WString& name); ParsingSymbol* GetDescriptorSymbol(); ParsingSymbol* GetParentSymbol(); bool IsType(); ParsingSymbol* SearchClassSubSymbol(const WString& name); ParsingSymbol* SearchCommonBaseClass(ParsingSymbol* classType); };
因ؓ文法规则本n的东西也不多Q所以这里的symbol只能是上面记载的9U对象。这些对象其实特别的怼Q所以我们可以看出唯一的区别就是当GetTypeq回g一L时候,GetDescriptorSymbolq回的对象的意思也不一栗而这个区别记载在了enum SymbolType的注释里面。实际上q种做法在面对超U复杂的W号表(考虑一下C++~译器)的时候ƈ不太好。那好的做法I竟是什么呢Q看上面IDIASymbol的链接,那就是答案?/p>
不可否认Q微软设计出来的API大部分还是很有道理的Q除了Win32的原生GUI部分?/p>
我们q可以看刎ͼq个ParsingSymbolcȝ几乎所有成员函数都是用来查询这个Symbol的内容的。这里还有两个特D的函数Q就是SearchClassSubSymbol和SearchCommonBaseClass——当且仅当symbol是ClassType的时候才起作用。ؓ什么有了GetSubSymbolByNameQ还要这两个api呢?因ؓ我们在搜索一个类的成员的时候,是要搜烦他的父类的。而一个类的父cȝsub symbolq不是类自己的sub symbolQ所以就有了q两个api。所谓的sub symbol的意思现在也很明了了。enumcd里面的值就是enum的sub symbolQ成员变量是cȝsub symbolQM只要是声明在一个符号内部的W号都是q个W号的sub symbol。这是所有符号都有的共性?/p>
当然Q有了ParsingSymbolQ还要有他的manager才可以完成整个符可的操作:
class ParsingSymbolManager : public Object { public: ParsingSymbolManager(); ~ParsingSymbolManager(); ParsingSymbol* GetGlobal(); ParsingSymbol* GetTokenType(); ParsingSymbol* GetArrayType(ParsingSymbol* elementType); ParsingSymbol* AddClass(const WString& name, ParsingSymbol* baseType, ParsingSymbol* parentType=0); ParsingSymbol* AddField(const WString& name, ParsingSymbol* classType, ParsingSymbol* fieldType); ParsingSymbol* AddEnum(const WString& name, ParsingSymbol* parentType=0); ParsingSymbol* AddEnumItem(const WString& name, ParsingSymbol* enumType); ParsingSymbol* AddTokenDefinition(const WString& name); ParsingSymbol* AddRuleDefinition(const WString& name, ParsingSymbol* ruleType); ParsingSymbol* CacheGetType(definitions::ParsingDefinitionType* type, ParsingSymbol* scope); bool CacheSetType(definitions::ParsingDefinitionType* type, ParsingSymbol* scope, ParsingSymbol* symbol); ParsingSymbol* CacheGetSymbol(definitions::ParsingDefinitionGrammar* grammar); bool CacheSetSymbol(definitions::ParsingDefinitionGrammar* grammar, ParsingSymbol* symbol); ParsingSymbol* CacheGetType(definitions::ParsingDefinitionGrammar* grammar); bool CacheSetType(definitions::ParsingDefinitionGrammar* grammar, ParsingSymbol* type); };
q个ParsingSymbolManager有着W号表管理器的以下两个典型作?/p>
1、创建符受?br>2、讲W号与语法树的对象绑定v来。譬如说我们在一个context下面推导了一个expression的类型,那下ơ对于同Lcontext同样的expression׃需要再推导一ơ了Q语义分析有很多个passQ对同一个expression求类型的操作l常会重复很多次Q,把它cache下来可以了?br>3、搜索符受具体到q个W号表,q个功能被做q了ParsingSymbol里面?br>4、保存根节点。GetGlobal函数是q这个作用的。所有的根符号都属于globalW号的sub symbol?/p>
因此我们可以怎么使用他呢Q首先看上面的Add函数。这些函C仅会帮你在一个符可里面d一个sub symbolQ还会替你做一些检查,譬如说阻止你d相同名字的sub symbol之类的。语法树很复杂的时候,很多时候我们有很多不同的方法来l一个符h加子W号Q譬如说C#的成员变量和成员函数。成员变量不能同名,成员函数可以Q但是成员函数和成员变量却不能同名。这个时候我们就需要把q些d操作装hQ这h可以在处理语法树Q声明一个函数的Ҏ可以有很多,所以添加函数符L地方也可以有很多Q的时候不需要重复写验证逻辑?/p>
其次是Cache函数。其实Cache函数q么写,不是用来直接调用的。D个例子,在分析一个文法的时候,我们需要把一?#8220;cd”语法树{成一?#8220;cd”W号Q譬如说要决定一个文法要create什么类型的语法树节点的时候)。因此就有了下面的函敎ͼ
ParsingSymbol* FindType(Ptr<definitions::ParsingDefinitionType> type, ParsingSymbolManager* manager, ParsingSymbol* scope, collections::List<Ptr<ParsingError>>& errors) { ParsingSymbol* result=manager->CacheGetType(type.Obj(), scope); if(!result) { FindTypeVisitor visitor(manager, (scope?scope:manager->GetGlobal()), errors); type->Accept(&visitor); result=visitor.result; manager->CacheSetType(type.Obj(), scope, result); } return result; }
很明显,q个函数做的事情是Q查询一个ParsingDefinitionType节点有没有被查询q,如果有直接用cacheQ没有的话再从头计算他然后cacheh。因此这些Cache函数是l类似FindType的函数用的,而语义分析的代码则直接用FindTypeQ而不是Cache函数Q来获取一个类型的W号。聪明的朋友们可以看出来Q这U写法蕴含着一个条Ӟ是语法树创建完׃会改了(废话Q当然不会改Q)?/p>
q一的内容p到这里了。现在的q度是正在写文法生成状态机的算法。下一文章应该讲的就是状态机I竟是怎么q作的了。文法所需要的状态机叫做下推状态机Qpush down automatonQ,跟regex用的NFA和DFA不太一P理解h略有隑ֺ。所以我想需要用单独的一文章来通俗的讲一讌Ӏ?/p>
其实说到开发语法分析器Q我?007q就已经开始在思考类似的问题了。当时C++q处于用的不太熟l的时候,隑օ会做Z些傻逼的事情Q不qȝ来说当年的ideaq是能用的。从那时候开始,我ؓ了锻D己,一直在实现各种不同的语a。所以给自己开发一个可配置语法分析器也是在所隑օ的事情了。于是就有:
W一版:http://hi.baidu.com/geniusvczh/archive/tag/syngram%E6%97%A5%E5%BF%97
W二版:http://www.shnenglu.com/vczh/archive/2009/04/06/79122.html
W三版:http://www.shnenglu.com/vczh/archive/2009/12/13/103101.html
q有W三版的教程Q?a title="http://www.shnenglu.com/vczh/archive/2010/04/28/113836.html" href="http://www.shnenglu.com/vczh/archive/2010/04/28/113836.html">http://www.shnenglu.com/vczh/archive/2010/04/28/113836.html
上面的所有分析器都致力于在C++里面可以通过直接描述文法和一些语义行为来让系l可以迅速构造出一个针对特定目的的用v来方便的语法分析器,而“第三版”就是到目前为止q在用的一个版本。至于ؓ什么我要做一个新的——也是W四版—?a href="http://www.shnenglu.com/vczh/archive/2012/10/30/194052.html" target="_blank">之前的文?/a>已经说了?/p>
而今天,W四版的开发已l开始了有好几天。如果大家关心进度的话,可以?a target="_blank">GacUI的Codeplex面下蝲代码Q然后阅读Common\Source\Parsing下面的源文g。对应的单元试可以在Common\UnitTest\UnitTest\TestParsing.cpp里找到?/p>
于是今天p说关于构造语法树的事情?/p>
用C++写过parser的h都知道,构造语法树以及语义分析用的W号表是一件极其繁琐,而且一不小心就Ҏ写出的事情。但是根据我写过无穷多棵语法树以及构造过无穷多个W号表以及附带的副作用,,啊不Q经验,做这个事情还是有一些方法的?/p>
在介l这个方法之前,首先要说一句,做完下面的所有事情是肯定要疯掉的Q所以这一ơ的可配|语法分析器我已l决定了一定要TMD写出一个生成语法树的C++代码的工兗?/p>
一颗语法树Q其实就是一大堆互相l承的类。一切成熟的语法树结构所h的共同特征,不是他的成员怎么安排Q而是他一定会附带一?a target="_blank">visitor模式的机制。至于什么是visitor模式Q大家请自行参考设计模式,我就不多说废话了。这一ơ的可配|语法分析器是带有一个描q性语法的。也是_跟Antlr或者Yacc一P首先在一个文本文仉面准备好语法树结构和文法规则Q然后我的工具会帮你生成一个内存中的语法分析器Q以及用C++描述的语法树的声明和实现文g。这个描q性语法就cM下面的这个大家熟悉到不能再熟悉的带函数的四则q算表达式结构:
class Expression
{
}
class NumberExpression : Expression
{
token value;
}
class BinaryExpression : Expression
{
enum BinaryOperator
{
Add,
Sub,
Mul,
Div,
}
Expression firstOperand;
Expression secondOperand;
BinaryOperator binaryOperator;
}
class FunctionExpression : Expression
{
token functionName;
Expression[] arguments;
}
token NAME = "[a-zA-Z_]/w*";
token NUMBER = "/d+(./d+)";
token ADD = "/+";
token SUB = "-";
token MUL = "/*";
token DIV = "http://";
token LEFT = "/(";
token RIGHT = "/)";
token COMMA = ",";
rule NumberExpression Number
= NUMBER : value;
rule FunctionExpression Call
= NAME : functionName "(" [ Exp : arguments { "," Exp : arguments } ] ")";
rule Expression Factor
= !Number | !Call;
rule Expression Term
= !Factor;
= Term : firstOperand "*" Factory : secondOperand as BinaryExpression with { binaryOperator = "Mul" };
= Term : firstOperand "/" Factory : secondOperand as BinaryExpression with { binaryOperator = "Div" };
rule Expression Exp
= !Term;
= Exp : firstOperand "+" Term : secondOperand as BinaryExpression with { binaryOperator = "Add" };
= Exp : firstOperand "-" Term : secondOperand as BinaryExpression with { binaryOperator = "Sub" };
上面的语法树声明借用的C#语法Q描qv来特别简单。但是要在C++里面辑ֈ可以使用的程度,肯定要有一个自带的visitor模式。所以出来之后的代码大概qg下面q个样子Q?/p>
class Expression;
class NumberExpression;
class BinaryExpression;
class FunctionExpression;
class Expression : public ParsingTreeCustomBase
{
public:
class IVisitor : public Interface
{
public:
virtual void Visit(NumberExpression* node)=0;
virtual void Visit(BinaryExpression* node)=0;
virtual void Visit(FunctionExpression* node)=0;
};
virtual void Accept(IVisitor* visitor)=0;
};
class NumberExpression : public Expression
{
public:
TokenValue value;
void Accept(IVisitor* visitor){visitor->Visit(this);}
};
class BinaryExpression : public Expression
{
public:
enum BinaryOperator
{
Add, Sub, Mul, Div,
};
Ptr<Expression> firstOperator;
Ptr<Expression> secondOperator;
BinaryOperator binaryOperator;
void Accept(IVisitor* visitor){visitor->Visit(this);}
};
class FunctionExpression : public Expression
{
public:
TokenValue functionName;
List<Ptr<Expression>> arguments;
void Accept(IVisitor* visitor){visitor->Visit(this);}
};
Z么要q样做呢Q学习过面向对象开发方法的都知道,把一个明显是l承l构的东西写成一堆union/struct和一个enum来判断他们,是不对的。第一个不好的地方是Q如果其中的成员需要构造函数和析构函数Q那unionq不了了,struct׃定会造成大量的内存浪贏V因Z颗语法树是可以很大的。其ơ,当语法树的结构(主要是添加删除了新的语法树类型)之后Q我们根本不可能保证我们所有的swtich(node->enumType)语句都接受到了正的更新?/p>
那要如何解决q两个问题呢Q答案之一是使用visitor模式。尽刚开始写h的时候可能会有点别扭Q但是我们只要把原本是swtichl构的代码做一?a target="_blank">Continuation Passing Style变换Q就可以写出使用visitor的版本了。在q里我做一个小的演示Q如何把一个“把上面的语法树q原成四则运式子的函数”给用Expression::IVisitor的框架下实现出来Q?/p>
class FunctionExpression : public Expression
{
public:
TokenValue functionName;
List<Ptr<Expression>> arguments;
void Accept(IVisitor* visitor){visitor->Visit(this);}
};
class ExpressionPrinter : public Expression::IVisitor
{
public:
WString result;
void Visit(NumberExpression* node)
{
result+=node->value.stringValue;
}
void Visit(BinaryExpression* node)
{
result+=L"(";
node->firstOperand->Accept(this);
switch(binaryOperator)
{
case Add: result+=L" + "; break;
case Sub: result+=L" - "; break;
case Mul: result+=L" * "; break;
case Div: result+=L" / "; break;
}
node->secondOperand->Accept(this);
result+=L")";
}
void Visit(FunctionExpression* node)
{
result+=node->functionName.stringValue+L"(";
for(int i=0;i<arguments.Count();i++)
{
if(i>0) result+=L", ";
arguments[i]->Accept(this);
}
result+=L")";
}
};
WString PrintExpression(Ptr<Expression> expression)
{
ExpressionPrinter printer;
expression->Accept(&printer);
return printer.result;
}
其实大家可以看到Q用了visitor模式Q代码量其实也没有多大变化,本来是递归的地方还是递归Q本来该计算什么还计算什么,唯一不同的就是原本这个“函数”的参数和返回值都跑到了一个visitorcȝ成员变量里面M。当ӞZ便于使用Q一般来说我们会把原本的函数的原型写出来Qƈ且在里面调用visitor模式Q就像上面的PrintExpression函数一栗如果我们高兴的话,完全可以在ExpressionPrinterq个visitorc里面用PrintExpressionQ无非就是在里面构造新的ExpressionPrinter然后获取l构|了。一般来_visitorc都是非常的轻量U的Q在C的CPU性能下面Q构造多几个完全不会带来多大影响?/p>
可配|语法分析器既然拥有一个描q性语法,那么我肯定也针对q个描述性语法写了一颗语法树的。这颗语法树的代码在Common\Source\Parsing\ParsingDefinition.h里面Q而ParsingLogging.cpp则是跟上面说的一P用visitor的方法写了一个庞大的把语法树转回描述性语法的函数。这个函数非常有用,不仅可以用来打logQ还可以用来保存E序生成的一个语法规则(反正可以parse回来Q所以保存成文本是一件特别方便的事情Q,甚至是生成错误消息的片段{等?/p>
今天先讲到q里了。现在的可配|语法分析器的开发进度是正在写语义分析的部分。等到语义分析写完了Q我会再写一纪事来说明开发语义分析程序和构造符可的一般做法?/p>
Z么要重写vlpp的这一部分呢?因ؓl过多次可配|语法分析器的开发,我感觉到了C++直接用来表达文法有很多弱点:
1、C++自n的类型系l导致表辑և来的文法会有很多噪音。当然这q不是C++的错Q而是通用的语a做这U事情L会有点噪音的。无论是?a target="_blank">Monadic Parser Combinators using C# 3.0
》也好,我大微Y研究院的ZHaskell?a target="_blank">Parsec也好Q还是boost?a target="_blank">spirit也好Q甚xF#?a target="_blank">Fsyacc也好Q都在展CZparser combinatorq个强大的概늚同时Q也暴露Zparser combinator的弱点:在语法分析结果和语言的数据结构的l合斚w特别的麻烦。这里的ȝ不仅在于会给文法造成很多噪音Q而且复杂的parserq会使得你的l构特别的臃肿(参考Antlr的某些复杂的应用Q这里就不一一列D了)?/p>2、难以维护。如果直接用C++描述一个强cd文法的话Q势必是要借助parser combinatorq个概念的。概忉|w是很厉害的Q而且实现的好的话开发效率会特别的高。但是对于C++q种非函数式语言来说Qparser combinatorq种特别函数式的描述攑֜C++里面׃多出很多ȝQ譬如闭包的语法不够漂亮啦、没有垃圾收集器的问题导致rule与rule的@环引用问题还要自行处理啦Q在很早以前的一博客论证过了,只要是带完整闭包功能的语aQ都一定不能是用引用计数来处理内存Q而必要一个垃圾收集器的)。尽我一直以来都q是没做q方面的bugQ但是由于(主要是用来处理何时应该delete对象部分的)逻辑复杂Q导致数据结构必Mؓdelete对象的部分让步,代码l护h也相当的蛋疼?/p>
3、有些优化无法做。D个简单的例子Qparser combinator根本没办法处理左递归。没有左递归Q写h些文法来也是特别的蛋疹{还有合q共同前~{等的优化也不能做,q导致我们必Mؓ了性能牺牲本来已l充满了噪音的文法的表达Q{而h工作文法的共同前~合ƈQ文法看h更׃?/p>
当然上面三个理由看v来好像不太直观,那我׃D一个典型的例子。大家应该还记得我以前写q一个叫?a href="http://www.shnenglu.com/vczh/archive/2011/03/20/142261.html" target="_blank">NativeX的语aQ还l它做了一?a href="http://www.shnenglu.com/vczh/archive/2011/02/25/140618.html" target="_blank">带智能提C的~辑?/a>Q还?a href="http://www.shnenglu.com/vczh/archive/2010/11/07/132876.html" target="_blank">q里?a href="http://www.shnenglu.com/vczh/archive/2010/12/05/135505.html" target="_blank">q里Q。NativeX是一个C++实现的C+template+concept mapping的语aQ语法分析器当然是用上一个版本的可配|语法分析器来做的。文法规则很复杂Q但是被C++q么以表达,更加复杂了Q?a target="_blank">.\Library\Scripting\Languages\NativeX\NativeXParser.cppQ,已经C不仔l看无法维护的地步了?/p>
lg所qͼ做一个新的可配置语法分析器出来理由充分,势在必得。但是Ş式上是什么样子的呢?上面说过我以前给NativeX写过一个带提示的编辑器。这个编辑器用的是WinFormQ那当然也是用C#写的Q因此那个对性能要求高到谱的NativeX~辑器用的语法分析器当然也是用C#写的。流E大概如下:
1、用C#按照要求声明语法树结?br>2、用我的库用C#写一个文?br>3、我的库会执行这个文法,生成一大段C#写的{h的递归下降语法分析器的代码
当时我把q个q程记录在了q篇博客文章里面?/p>
因此现在有一个计划,q个新的可配|语法分析器当然q是要完全用C++Q但是这p正则表达式一P
1、首先语法树l构和文法都声明在一个字W串里面
2、可配置语法分析器可以在内存中动态执行这D|法,q按照给定的语法树结构给Z个在内存中的动态的数据l构
3、可配置语法分析器当然还要附带一个命令行工具Q用来读文法生成C++代码Q包括自带Visitor模式的语法树l构Q和C++写的递归下降语法分析?/p>
所以现在就有一个草E,是那个“声明在字符串里面”的语法树结构和文法的说明。这是一个很有意思的q程?/p>
首先Q这个可配置语法分析器需要在内存中表达语法树l构Q和一个可以执行然后生动态数据结构的文法。因此我们在使用它的时候,可以选择直接在内存中堆出语法树结构和文法的描qͼ而不是非得用那个字符串的表达形式。当Ӟ字符串的表达形式肯定是十分紧凑的Q但q不是必ȝQ只是推荐的?/p>
其次Qparseq个“语法树l构和文法都声明”当然也需要一个语法分析器是不是?所以我们可以用上面的方法,通过直接在内存中堆出文法来用自己构造出一个自q语法分析器?/p>
再者,有了一个内存中的语法分析器之后Q我可以将上面W三步的命o行工具做出来Q然后用它来描述自己的文法,产生ZDC++写的递归下降语法分析器,用来分析“语法树l构和文法都声明”,然后有了一对C++代码文g?/p>
最后,把生出来的q对C++代码文g加进去,我们有了一个C++直接写,而不是在内存中动态构造出来的“语法树l构和文法都声明”的分析器了。然后这个分析器可以替换掉命o行工具里面那个原先动态构造出来的语法分析器。当焉个动态构造出来的语法分析器这个时候已l没用了Q因为有了生成的C++语法分析器,我们可以直接用“语法树l构和文法都声明”来描述自己Q得到这么一个描q的字符Ԍ然后随时都可以用q个字符串来动态生成语法分析器了?/p>
总而言之就?br>1、实现可配置语法分析器,可以直接用数据结构做Z个生动态数据结构的parser combinatorQ记为PC?br>2、用PC做一个“语法树l构和文法都声明”的语法分析器。这个“语法树l构和文法都声明”记为PC Grammar?br>3、PC Grammar当然可以用来表达PC Grammar自己Q这h们就得到了一个专门用来说明什么是合法的“语法树l构和文法都声明”的描述的字W串的这么个文法Q记为PC Grammar Syntax Definition?br>4、通过q䆾满PC Grammar要求的PC Grammar Syntax DefinitionQ我们就可以用PC来解释PC Grammar Syntax DefinitionQ动态生一个解释PC Grammar的语法分析器
5、有了PC Grammar的语法分析器PC Grammar Parser (in memory version)Q之后我们就可以把“文?>C++代码”的代码生成器做出来Q称之ؓPC Grammar C++ Codegen?br>6、有了PC Grammar C++ CodegenQ我们就可以用他dPC Grammar Syntax DefinitionQ生一个直接用C++写的PC Grammar的语法分析器Q叫做PC Grammar Parser (C++ version)?/p>
到此为止Q我们获得的东西?br>1、PC QParser CombinatorQ?br>2、PC Grammar
3、PC Grammar Syntax Definition
4、PC Grammar Parser (in memory version)
5、PC Grammar Parser (C++ version)
6、PC Grammar C++ Codegen
其中Q?????都是可以执行的,2是一个“标准”。到了这一步,我们可以用PC Grammar Parser (C++ version)来替换掉PC Grammar C++ Codegen里面的PC Grammar Parser (in memory version)了。这pgcc要编译一个小~译器来~译自己得到一个完整的gcc一栗这个过E还可以用来试PC Grammar C++ Codegen是否写的_好?/p>
那么“语法树l构和文法都声明”到地是什么样子的呢?我这里给Z个简单的文法Q就是用来parse诸如int、vl::collections::List<WString>、int*、int&、int[]、void(int, WString, double*)的这些类型的字符串了。下面首先展C如何用q个描述来解决上面的“类型”的语法书声明:
class Type{}
class DecoratedType : Type
{
enum Decoration
{
Pointer,
Reference,
Array,
}
Decoration decoration;
Type elementType;
}
class PrimitiveType : Type
{
token name;
}
class GenericType : Type
{
Type type;
Type[] arguments;
}
class SubType : Type
{
Type type;
token name;
}
class FunctionType : Type
{
Type returnType;
Type[] arguments;
}
然后是声明语法分析器所需要的词法元素Q用正则表达式来描述Q?/p>
token SYMBOL = <|>|\[|\]|\(|\)|,|::|\*|&
token NAME = [a-zA-Z_]\w*
q里只需要两Utoken可以了。接下来是两种{h的对于这个文法的描述Q用来展C全部的功能?/p>
========================================================
Type SubableType = NAME[name] as PrimitiveType
= SubableType[type] '<' Type[arguments] { ',' Type[arguments] } '>' as GenericType
= SubableType[type] '::' NAME[name] as SubType
Type Type = @SubableType
= Type[elementType](
( '*' {decoration = DecoratedType::Pointer}
| '&' {decoration = DecoratedType::Reference}
| '[' ']' {decoration = ecoratedType::Array}
)
) as DecoratedType
= Type[returnType] '(' Type[arguments] { ',' Type[arguments] } ')' as FunctionType
========================================================
rule PrimitiveType PrimitiveType = NAME[name]
rule GenericType GenericType = SubableType[type] '<' Type[arguments] { ',' Type[arguments] } '>'
rule SubType SubType = SubableType[type] :: NAME[name]
rule Type SubableType = @PrimitiveType | @GenericType | @SubType
rule DecoratedType DecoratedType = Type[elementType] '*' {decoration = DecoratedType::Pointer}
= Type[elementType] '&' {decoration = DecoratedType::Reference}
= Type[elementType] '[' ']' {decoration = DecoratedType::Array}
rule FunctionType FunctionType = Type[returnType] '(' Type[arguments] { ',' Type[arguments] } ')'
rule Type Type = @SubableType | @DecoratedType | @FunctionType
========================================================
如果整套pȝ开发出来的话,那么我就会提供一个叫做ParserGen.exe的命令行工具Q把上面的字W串转换Z?strong>可读的、等价与q段文法的、用递归下降Ҏ来描q的、C++写出来的语法分析器和语法树声明了?/p>
我写了一个HttpUtility库来实现C++操作http/https服务的功能,q䆾代码可以在这里获得:
HttpUtility.hQ?a style="color: ; text-decoration: underline" target="_blank">http://gac.codeplex.com/SourceControl/changeset/view/95641#2295555
HttpUtility.cppQ?a title="http://gac.codeplex.com/SourceControl/changeset/view/95641#2295554" style="color: ; text-decoration: underline" target="_blank">http://gac.codeplex.com/SourceControl/changeset/view/95641#2295554
使用的时候很单,只需要HttpRequest里面填满了参敎ͼ然后可以用HttpQuery参数获得一个HttpResponsecdQ这个类型里面写满了http服务器的q回倹{返回内容和cookie{的数据。譬如说用来post来登陆鸟H,然后拿到cookie之后查询首页的所有帖子,大概可以这么写Q?/p>
WString NestleGetSession(const WString& username, const WString& password, const WString& apiKey, const WString& apiSecret)
{
WString body=L"api_key="+apiKey+L"&api_secret="+apiSecret+L"&username="+username+L"&password="+password;
HttpRequest request;
HttpResponse response;
request.SetHost(L"https://www.niaowo.me/account/token/");
request.method=L"POST";
request.contentType=L"application/x-www-form-urlencoded";
request.SetBodyUtf8(body);
HttpQuery(request, response);
if(response.statusCode==200)
{
return response.cookie;
}
else
{
return L"";
}
}
WString NestleGetXml(const WString& path, const WString& cookie)
{
HttpRequest request;
HttpResponse response;
request.SetHost(L"https://www.niaowo.me"+path+L".xml");
request.method=L"GET";
request.cookie=cookie;
request.acceptTypes.Add(L"application/xml");
HttpQuery(request, response);
if(response.statusCode==200)
{
return response.GetBodyUtf8();
}
else
{
return L"";
}
}
于是我们l于获得了一个保存在vl::WString的xml字符串了Q那怎么办呢Q这个时候需要出动IXMLDOMDocument2来解析我们的xml。只要装了IE的计机上都是有IXMLDOMDocument2的,而不装IE的Windows PC是不存在的,因此我们L可以大胆的用。当Ӟ用IXMLDOMDocument直接来遍历什么东西特别的慢,所以我们需要的是xpath。xpath对于xmlpregex对于字符串一P可以直接查询出我们要的东ѝ首先看一下如何操作IXMLDOMDocument2接口Q?/p>
IXMLDOMNodeList* XmlQuery(IXMLDOMNode* pDom, const WString& xpath)
{
IXMLDOMNodeList* nodes=0;
BSTR xmlQuery=SysAllocString(xpath.Buffer());
if(xmlQuery)
{
HRESULT hr=pDom->selectNodes(xmlQuery, &nodes);
if(FAILED(hr))
{
nodes=0;
}
SysFreeString(xmlQuery);
}
return nodes;
}
WString XmlReadString(IXMLDOMNode* node)
{
WString result;
BSTR text=0;
HRESULT hr=node->get_text(&text);
if(SUCCEEDED(hr))
{
const wchar_t* candidateItem=text;
result=candidateItem;
SysFreeString(text);
}
return result;
}
void XmlReadMultipleStrings(IXMLDOMNodeList* textNodes, List<WString>& candidates, int max)
{
candidates.Clear();
while((int)candidates.Count()<max)
{
IXMLDOMNode* textNode=0;
HRESULT hr=textNodes->nextNode(&textNode);
if(hr==S_OK)
{
candidates.Add(XmlReadString(textNode));
textNode->Release();
}
else
{
break;
}
}
}
IXMLDOMDocument2* XmlLoad(const WString& content)
{
IXMLDOMDocument2* pDom=0;
HRESULT hr=CoCreateInstance(__uuidof(DOMDocument60), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pDom));
if(SUCCEEDED(hr))
{
pDom->put_async(VARIANT_FALSE);
pDom->put_validateOnParse(VARIANT_FALSE);
pDom->put_resolveExternals(VARIANT_FALSE);
BSTR xmlContent=SysAllocString(content.Buffer());
if(xmlContent)
{
VARIANT_BOOL isSuccessful=0;
hr=pDom->loadXML(xmlContent, &isSuccessful);
if(!(SUCCEEDED(hr) && isSuccessful==VARIANT_TRUE))
{
pDom->Release();
pDom=0;
}
SysFreeString(xmlContent);
}
}
return pDom;
}
有了q几个函C后,我们可以干下面的事情,譬如说从鸟窝首页下蝲W一늚所有topic的标题:
WString xml=NestleGetXml(L”/topics”, cookie);
IXMLDOMDocument2* pDom=XmlLoad(xml);
List<WString> titles;
IXMLNodeList* nodes=XmlQuery(pDom, L”/hash/topics/topic/title/text()”);
XmlReadMultipleStrings(nodes, titles, 100);
Z么上面的xpath是hash/topics/topic/title/text()?因ؓq个xml的内容大概类gQ?br /><hash>
<topics>
<topic>
<title>TITLE</title>
…
剩下的大家就ȝ代码吧。这个故事告诉我们,只要有一个合适的装QC++写vq些本来应该让C#来写的东西也不是那么的烦人的Q啊哈哈哈哈?/p>