前言
这篇文章主要来看看SLR语法分析,他是一个LR语法分析。其中L表示最左向右扫描,R表示最右推导序列。
在本篇文章中,我们会接触 移入-归约 技术。并且还会接触到LR文法类,它是最大的、可以构造出相应移入-归约语法分析器的文法类。
一、归约
我们可以将自底向上语法分析过程看成将一个符号串“归约”为文法开始符号的过程。一个与某产生式体相匹配的特定子串被替换为该产生式头部的非终结符号 。
在自底向上的语法分析过程中,关键问题是 何时进行归约以及用哪个产生式进行归约
对于文法:
词法单元序列 id * id 机型自底向上的分析过程:
这个序列中从输入串 id * id 开始,第一次归约使用产生式 ,将最左边的id归约为F,得到串 F id。第二次归约将F归约为T,生成 T id。
现在(上图中的第四步)我们可以选择是对串T,还是对第二个id进行归约,其中T是产生式 的体,而第二个id是产生式 的体。在这里我们并没有将T归约为E,而是将id归约为F,得到了串 T * F 。然后这个串被归约为T。最后则是将T归约为E,由于E是文法的开始符号,因此整个归约过程便结束了。
因此自底向上的语法分析目的就是反向构造一个推到过程,而 归约正是最右推到的反向过程:
对输入进行从左到右的扫描,并在扫描过程中进行自底向上的语法分析,就可以反向构造出一个最右推到。
句柄剪枝
非正式的讲,“句柄”是和某个产生式体匹配的子串.
比如上面的归约过程:
最右句型 |
归约目标子串 |
归约用的产生式 |
句柄 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- |
- |
- |
在上表中,虽然T是产生式的体,但符号T并不是句型的句柄。如果我们把T替换为E之后,发现E为开始符号,那我们也就不能从单一的开始符号推导得到 。
因此:
和某个产生式体匹配的最左子串不一定是句柄。
正式的对句柄下一个定义:
如果存在产生式 ,最右句型 的一个句柄满足如下条件:
将该句型中的子串替换为A之后得到的串 ,串是最右句型所处最右推导序列中位于之前的最右句型(稍微转一下弯,这里说的是最右推导的最右句型序列之前,那也就是上面所说的处于归约序列之后的子串)。
同时为了方便起见,我们就把产生式 的体 称作句柄。
由于文法存在二义性,因此对于同一个最右句型可能存在多个句柄;而对于无二义性的文法而言,那么该文法的每个最右句型有且只有一个句柄。
而这里提到的句柄剪枝的目的,便是寻找合适的句柄。使得我们可以按照这个过程得到一个 只包含开始符号 的最右句型,因此我们就可以说语法分析过程结束。
而将归约过程中用的产生式反向排序,我们就可以得到输入串的一个最右推导。
移入-归约语法分析技术
它使用一个栈来保存文法符号,并用一个输入缓冲区来存放将要进行语法分析的其余符号。句柄在被识别之前,总是出现在栈的顶部。
在对输入串的一次从左到右扫描过程中,语法分析器将零个或多个输入符号移动到栈的顶端,直到它可以对栈顶的一个文法符号串(比如)进行归约为止。将 归约为某个产生式的头部(即 ,归约为A)。
语法分析器不断重复这个循环,直到它检测到一个语法错误,或者栈中包含了开始符号且输入缓冲区为空。
下表是对输入串“id*id”进行语法分析时,对应的文法符号为:
移入-归约步骤:
栈 |
输入 |
动作 |
$ |
$ |
移入(栈中) |
$ $$id_{1}$ |
$*id_{2}$$ $ |
按照进行归约 |
$ F |
$ |
按照进行归约 |
$ T |
$ |
移入(栈中) |
$ T* |
$ |
移入(栈中) |
$ |
$ |
按照进行归约 |
$T*F |
$ |
按照进行归约 |
$T |
$ |
按照进行归约 |
$E |
$ |
接受 |
最后一行我们看到了开始符号E,并且此时输入缓冲区为空。因此我们认为此次移入归约过程结束。
移入-归约语法分析器一共有4种可能的动作:
- 1、移入:将下一个输入符号移入到栈中(句柄);
- 2、归约:语法分析器在栈中确定适当的产生式;
- 3、接受:栈中包含开始符号,并且输入缓冲区为空;
- 4、报错:发现语法错误;
句柄总是出现在栈的顶端,绝不会出现在栈的中间。
移入-归约语法分析中的冲突
假如知道了栈中的所有内容,以及接下来输入串中的k个输入符号:
在这一节中,我们主要学习了句柄,以及移入-归约的相关知识点。
二、SLR(简单LR技术)
目前最流行的自底向上语法分析器都是基于所谓的LR(k)语法分析概念。 其中“L”表示对输入进行从左到右的扫描,“R”表示反向构造出一个最右推导序列,k表示在做出语法分析决定时向前看k个输入符号。目前我们只考虑k小于等于1的清空。
我们一共有三种LR技术:SLR、规范LR、LALR。本节主要讲SLR,下一篇文章讲规范LR和LALR。
项(item)和LR(0)自动机
一个LR语法分析器通过维护一些状态,用这些状态来表明我们在语法分析过程中所处的位置,从而做出移入-归约决定。这些状态代表了“项”(item)的集合。
一个文法G的一个LR(0)项是G的一个产生式再加上一个位于它产生式体中某处的点。比如对于产生式 的四个项为:
产生式 只生成一个项
一个项可以表示为 一对整数,第一个整数是基础文法的产生式编号,第二个整数是点的位置。
项指明了在语法分析过程中,我们已经看到了产生式的哪些部分(由点号来区分)。比如对于项 表明我们希望接下来在输入中看到一个从XYZ推导得到的串;项 表明我们刚刚从输入中看到了一个可以由X推导得到的串,并且希望接下来能从YZ中推导得到串;项 表明我们已经看到了产生式体XYZ,已经可以将其归约为A了。
LR(0)自动机的每个状态代表了规范LR(0)项集族中的一个项集(而项集又由多个项组合而成)。对于文法:
我们构造的LR(0)自动机如下(此时看不懂并不重要,因为这里面涉及到的闭包和GOTO尚未学习,在下面学习了闭包和GOTO函数之后可以对照着看看该自动机):
为了构造一个文法的规范LR(0)项集族,我们需要定一个 增广文法和两个函数:CLOSURE和GOTO 。如果文法G的开始符号为S,那么G的增广文法就是在文法G的基础上新增一个产生式 。
引入新的增广产生式的目的是 告诉语法分析器何时应该停止语法分析并宣称接受输入符号串 。
也就是说当且仅当语法分析器要使用规则 进行归约时,输入符号串被接受。
这儿我归纳三个概念:
- 项:单一的个体,在SLR中用一对整数表示(第一个整数是文法的产生式编号,第二个整数是点号在产生式的位置);
- 项集:多个项的集合称为项集;
- 项集族:多个项集的集合称为项集族;
项集的闭包
如果 I 是 文法G 的一个项集(项的集合,其数量大于等于1个)。那么构造CLOSURE(I)的规则如下:
这个可能比较抽象,可以对照上图 项集来理解这两个规则, 中白色部分即规则1的方式添加到CLOSURE( )中;而灰色部分则是因为点号右边存在非终结符,并且有对应的产生式,那么我们就可以应用规则2 循环将其添加到CLOSURE( ) 中。
使用项集闭包的目的就是为了希望能够找到对应的产生式可以推导出点号右边的符号,而项集就是收集所有可能出现的产生式集合。从归约的角度来看,比如对于产生式 ,此时我们希望能够出现对应的产生式( )能够归约点号右边即将出现的符号。 反过来看,比如id可以归约为F,F可以归约T,T可以归约为E,而最后E可以归约为 ,则归约过程结束。
下面是一个简单的closure函数实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| SetOfItems SLR::CanonicalLR::closure(SetOfItems& set) { SLR::ContextFreeGrammar grammer = this->grammer; SetOfItems result(set); /// SLR::SetOfItems::items_iterator /// 循环当前项集,需要注意的是是result集合自己在发生变动,同时也是该集合在循环。也就是说如果有新的项被添加到集合内之后,还会继续查看这个新项的产生式情况 for (SLR::items_iterator itr = result.begin(); itr != result.end(); ++itr) { Item item = *itr; /// 在书中我们知道项的表示方式是:数对 /// item.id: 一个数表示当前项在产生式集合中和的下标(第几个具体的产生式) /// item.position: 另一个数是表示点号在产生式体中的下标 SLR::Production production = grammer.productions[item.id]; /// 没有做越界保护 vector<Symbol *>bodies = production.bodies; if (item.position >= bodies.size()) { /// 说明此时点号在产生式的最右边,也就是说此时我们已经可以把产生式体归约为产生式头部了,无需继续添加 continue; } Symbol* symbol = production.bodies[item.position]; bool isTerminal = symbol->isTerminal(); if (isTerminal) { continue; } /// 此时点号右边是非终结符,需要查看该非终结符是否有对应的产生式 int id = symbol->identifier(); vector<Production> productions = grammer.productions; bool contain_grammer = false; for (vector<Production>::iterator itr = productions.begin(); itr != productions.end(); ++itr) { int idx = itr-productions.begin(); if ((itr->header).identifier() == id) { Item new_item(idx,0); result.push_back(new_item); } } } return result; }
|
我们通过观察上图可以知道,如果点在最左边的某个产生式(比如上面的 )被加入到项集族 I 中,那么该产生式所有依据closure
函数添加到项集中的项(比如上图中灰色区域部分的项)我们是没有必要全部列出来的,因此我们将上图中白色区域中的项称为 内核项,灰色区域的项称为 非内核项。
- 内核项:包括初始项,以及所有点不在最左边的项;
- 非内核项:除了初始项之外,所有点在最左边的项;
通过求闭包时加入的项不可能是内核项,因此我们抛弃所有非内核项,就可以用很少的内存来表示真正感兴趣项的集合。
GOTO函数
函数GOTO(I,X)
,其中 I 是项集族中的一个项集,X为一个文法符号。该函数用于定义一个文法的LR(0)自动机中的转换,这个自动机的状态对应于项集族中的项集,而GOTO函数描述了当输入为X时,离开状态X的转换。
比如我截取了上面大图中的一个部分 :
它就表示了函数 。 下面的代码大致的实现了一下GOTO函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| SetOfItems SLR::CanonicalLR::lr_goto(SetOfItems& set, Symbol *symbol, int *offset) { int index = set.cacheForSymbol(symbol->identifier); if (index >= 0) { if (offset != nullptr) { *offset = index; } return item_set[index]; } ContextFreeGrammar grammer = this->grammer; SetOfItems result; for (SLR::items_iterator itr = set.begin(); itr != set.end(); ++itr) { Item item = *itr; /// 在书中我们知道项的表示方式是:数对 /// item.id: 一个数表示当前项在产生式集合中和的下标(第几个具体的产生式) /// item.position: 另一个数是表示点号在产生式体中的下标 uint idx = item.id; if (idx >= grammer.productions.size()) { return set; /// 异常 } Production production = grammer.productions[idx]; vector<Symbol *> bodies = production.bodies; if (item.position >= bodies.size()) { continue; /// 点号在产生式的最右边; } Symbol *current_sym = bodies[item.position]; if (current_sym->identifier() != symbol->identifier()) { continue; } Item new_item(idx,item.position+1); /// 点号右移 int index = contain_item(new_item); if (index >= 0) { /// 如果生成的item已经存在于某一个项集中,则返回当前项集 return item_set[index]; } /// 新生成的item并未存在,将该项放入新的项集中 std::string key = format("%d_%d",item.id,item.position); item_map.insert(make_pair(key, item_set.size())); result.push_back(new_item); } if (result.count() > 0) { if (offset) { *offset = item_set.size(); item_set.push_back(result); set.setCache(*offset, symbol->identifier()); } } return result; }
|
LR(0)自动机的用法(帮助我们进行移入-归约操作)
下面是通过使用CLOSURE函数和GOTO函数构造规范LR(0)项集族的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void SLR::CanonicalLR::items() { /// 获取增广文法的初始化产生式,并构造初始的项集 SetOfItems set; Item augmented_item(0,0); set.push_back(augmented_item); (this->item_set).push_back(set); /// 初始项集族 /// 遍历每一个项集 for (vector<SetOfItems>::iterator vitr = (this->item_set).begin(); vitr != (this->item_set).end(); ++vitr) { /// 遍历当前文法所有的符号 for (vector<SLR::Symbol *>::iterator itr = grammer.symbols.begin(); itr != grammer.symbols.end() ++itr) { SetOfItems result = lr_goto(*vitr, *itr); int contain = contain_set(result); if (contain == -1) { item_set.push_back(result); } } } }
|
SLR的中心思想是根据文法构造出LR(0)自动机。这个自动机的状态是规范LR(0)项集族中的元素,而它的转换由GOTO函数给出。我们再来看看LR(0)自动机
LR(0)自动机可以帮助我们决定何时进行移入,何时进行规约操作。 假设文法符号串 γ 使得LR(0)自动机从开始状态0运行到某个状态 j 。此时如果下一个输入符号为 a,并且状态 j 有一个在 a 上的转换(即存在GOTO(j, a)),那么此时我们就 移入 a;否则我们就选择 归约 动作。我们可以通过查看状态 j (项集)内各个项点号的位置,来决定如何进行归约操作(具体可以看本节开头部分关于点号存在不同位置所表达的含义)。
比如, 的点号在最右边,它的意思是说我们已经收到期望归约所需的全部符号,可以进行归约了;
而 的点号右边存在符号”+”,说明此时我们收到了符号E,如果想要以该产生式进行归约的话,我期望下一个输入串是”+”。因此当输入“+”号,LR(0)自动机认为此时需要做移入操作;而当输入为非”+”号时,LR(0)将以项集中第一个项进行归约操作。
ACTION函数
在前面我们已经知道如何编写和使用GOTO函数,但是并不知道ACTION函数是如何实现的。那么现在我们就来具体的看看是如何构造ACTION函数的:
- 1)、如果项 在状态i中。并且GOTO(Ii, a) 为新状态 j,那么 ACTION(i,a) 函数对应的输出值为 “移入 j”;
- 2)、如果项 在状态i中,那么对于FOLLOW(A)中所有的a,将 ACTION(i,a) 的输出值为 “归约 A -> α”。这里A不是增广文法的开始符号;
- 3)、如果增广文法的开始项,比如 在状态i中,那么 ACTION(i, $) 输出值为 “接受”;
如果使用上诉规则时,发生了任何冲突动作。我们就说这个文法不是SLR(1)的。
ACTION(i,a)
存在两个参数,分别是表示状态的i,以及 终结符号 a(下面我已经把ACTION函数的伪代码实现已列出)。而ACTION函数的返回值有四种可能,分别是:
- 移入:语法分析器采取的动作是把输入符号 a 高效的移入栈中;
- 归约:语法分析器的动作是把栈顶元素高效地归约为对应产生式的头部(比如产生式,将β归约为A);
- 接受:语法分析器接受输入并完成语法分析过程;
- 报错:语法分析器在它的输入中发现了一个错误,并执行某个纠正错误动作;
下面是Action函数的一个伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| SLR::LR_Action* SLR::CanonicalLR::action(SetOfItems& set, SLR::Symbol *symbol) { if (symbol->isTerminal() == false) /// 非终结符直接返回 { return nullptr; } LR_Action *action = set.actionForSymbol(symbol->identifier()); if (action != nullptr) { return action; } int offset = -1; SetOfItems result = lr_goto(set,symbol, &offset); if (result != set) /// 不同的两个状态,说明goto函数有效,此时应该做移入操作 { action = new ShiftAction(offset); set.setActionForSymbol(symbol->identifier(), action); return action; } ContextFreeGrammar grammer = this->grammer; SetOfItems result; for (SLR::items_iterator itr = set.begin(); itr != set.end(); ++itr) { Item item = *itr; /// 在书中我们知道项的表示方式是:数对 /// item.id: 一个数表示当前项在产生式集合中和的下标(第几个具体的产生式) /// item.position: 另一个数是表示点号在产生式体中的下标 uint idx = item.id; if (idx >= grammer.productions.size()) { continue; /// 异常 } Production production = grammer.productions[idx]; vector<Symbol *> bodies = production.bodies; if (item.position != bodies.size()) { continue; } /// 点号在最左边,并且当前产生式为产生式集合中的第一个,即增广文法的产生式 if (idx == 0) { action = new AcceptAction(); set.setActionForSymbol(symbol->identifier(), action); return action; } /// 点号在产生式的最右边的一般情况 vector<Symbol *> syms = grammer.FOLLOW(&(production.header)); for (vector<Symbol *>::iterator itr = syms.begin(); itr != syms.end(); ++itr) { if (symbol -> identifier() == (*itr)->identifier()) { action = new ReduceAction(&production); set.setActionForSymbol(symbol->identifier(), action); return action; } } } action = new ErrorAction(); set.setActionForSymbol(symbol->identifier(), action); return action; }
|
LR语法分析算法
下图是一个LR语法分析器的示意图,它由一个输入、一个输出、一个栈、一个驱动程序和一个语法分析表组成,其中语法分析表包括了两个部分,分别是ACTION和GOTO:
- 1)、栈中保存的是LR(0)自动机中的状态,各个状态都和某个项集对应;
- 2)、语法分析表是随着语法分析器的不同而变化的。GOTO对应于自动机的转换;而ACTION则表示当前的动作;
我们定义LR语法分析器的配置如下:
其中第一个分量表示栈中的内容(上图中左侧的栈,其中sm 为栈顶 ),第二个分量是余下的输入串。我们可以来看看ACTION函数和LR语法分析器配置是如何配合使用的:
- :那么语法分析器就执行一次移入动作,将下一个状态s移入栈中,此时的配置为:
:那么语法分析器执行一次归约动作,其中r是β的长度,$s = GOTO[s_{m-r}, A]$
语法分析器首先将r个状态符号弹出栈,使得状态 sm-r 位于栈顶,然后语法分析器将 s 压入栈中。 在一个归约动作中,当前输入符号不发生改变 。
: 表示语法分析过程完成;
: 表明语法分析器发生了一个语法错误;
两个LR语法分析器之间唯一的区别是他们的语法分析表ACTION表项和GOTO表项中包含的信息不同;
现在我们可以综合上面已有的知识来写一个简单的SLR语法分析程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| class CanonicalLR { private: ContextFreeGrammar grammer; vector <SetOfItems> item_set; map<string,int> item_map; /// key: item.id+item.position; value: item_set index public: SetOfItems closure(SetOfItems& set); SetOfItems lr_goto(SetOfItems& set, SLR::Symbol *symbol, int *offset); SLR::LR_Action* SLR::CanonicalLR::action(SetOfItems& set, SLR::Symbol *symbol); void items(); int contain_set(SetOfItems& set); /// 不包含返回-1,包含返回非负数 int contain_item(Item& item); /// 不包含返回-1,包含返回非负数 friend class SLRAnalyser; }; class SLRAnalyser { vector<Symbol *> inputTokens; /// 输入串 stack<int> stack_; CanonicalLR lr; public: /// 出现错误时,信息记录 struct Error{ Symbol *sym; stack<int> stck; Error(Symbol *s, stack<int>& sk):sym(s),stck(sk){} Error() { sym = nullptr; } }; private: Error error; public: SLRAnalyser() { stack_.push(0); } SLRAnalyser::Error slrError() { /// 当出现异常时,获取错误信息 return error; } vector<Production> start() { vector<Production> result; int i = 0; Symbol *input_sym = inputTokens[i]; while (1) { int idx = stack_.top(); SetOfItems set = lr.item_set[idx]; LR_Action *action = lr.action(set, input_sym); if (action->isShiftAction()) /// 移入 { ShiftAction *shift_action = dynamic_cast<ShiftAction *>(action); int dst = shift_action->opNum; stack_.push(dst); i++; input_sym = inputTokens[i]; } else if (action->isReduceAction()) /// 归约 { stack_.pop(); ReduceAction *reduce_action = dynamic_cast<ReduceAction *>(action); Symbol *sym = &(reduce_action->production->header); SetOfItems c_set = lr.item_set[stack_.top()]; int dst; lr.lr_goto(c_set, sym, &dst); stack_.push(dst); result.push_back(reduce_action->production); } else if (action->isAcceptAction()) /// 接受 { error.sym = nullptr; stack<int> sk; error.stck = sk; break; } else /// 报错 { SLRAnalyser::Error err(input_sym, stack_); error = err; break; } } return result; } };
|
可行前缀
在结束SLR相关内容之前,我们来看看“可行前缀”。可以出现在移入-归约语法分析器栈中的最右句型前缀被称为 可行前缀 ,详细定义为:
一个可行前缀是一个最右句型的前缀,并且该前缀没有越过该句型最右句柄的右端。
因此我们可以在可行前缀之后增加一些终结符号来得到一个最右句型。这里再说一下上面的提到的几个概念:
比如对于一个推导过程 ,对于产生式 ,我们就可以说前缀 对于句柄 是有效的,因为前缀 并未包含 ,所以是一个可行前缀。
这里的称为最右句型(因为对于句型而言,w为终结符号,最右边的非终结符号为A);
这里 作为产生式 的产生式体,而w不能进行归约,而最右边可以进行的只有,因此它就是该句型的最右句柄。而是作为该句柄的最右端。
现在综合上面所有的信息来看可行前缀,即该前缀是最右句型的一个前缀,而且该前缀没有包含完整的句柄,因为如果包含了完整的句柄之后,那么该最右句型就可以将产生式体归约为产生式头。
而可行前缀的作用是:可行前缀信息可以帮助我们决定是进行归约还是移入操作。比如当我们遇到了可行前缀之后,很明显目前还不能进行归约操作,因为进行归约操作的句柄还不完整。因此遇到可行前缀之后大多数情况下是要进行移入操作的;只有当句柄的最右端为空串时,此时应该做归约操作。
结语
在这篇文章中,我们主要是接触了一些LR语法分析的基本概念。以及最简单的SLR语法分析技术,并且给出了相应的伪代码,包括有CLOSURE\GOTO\ACTION等等(关于相关类之间的关系在我的Github上能找到)。最后介绍了一个很重要的概念——可行前缀,这在后续的两个LR语法分析器中都有涉及。
说个题外话,之前在学习这方面知识的时候一直在思考这个产生式(包括对应的语法分析树)具体的作用是什么,体现在了什么地方。很显然在日常开发工作中基本上是不需要关心的。直到最近我在看clang文档-Grammar Additions时,里面看到了关于”@”相关语法的产生式,瞬间感觉到了亲切感:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| objc-at-expression : '@' (string-literal | encode-literal | selector-literal | protocol-literal | object-literal) ; object-literal : ('+' | '-')? numeric-constant | character-constant | boolean-constant | array-literal | dictionary-literal ; boolean-constant : '__objc_yes' | '__objc_no' | 'true' | 'false' /* boolean keywords. */ ; array-literal : '[' assignment-expression-list ']' ; assignment-expression-list : assignment-expression (',' assignment-expression-list)? | /* empty */ ; dictionary-literal : '{' key-value-list '}' ; key-value-list : key-value-pair (',' key-value-list)? | /* empty */ ; key-value-pair : assignment-expression ':' assignment-expression ;
|