项目的完整代码在 C2j-Compilerjava
在上一篇,已经成功的构建了有限状态自动机,可是这个自动机还存在两个问题:node
这一节就要解决这两个问题git
看上一节那个例子的一个节点github
e -> t . t -> t . * f
这时候经过状态节点0输入t跳转到这个节点,可是这时候状态机没法分清是根据推导式1作reduce仍是根据推导式2作shift操做,这种状况就称之为shift / reduce矛盾。算法
在以前的LL(1)语法分析过程当中,有一个FOLLOW set,也就是指的是,对某个非终结符,根据语法推导表达式构建出的全部能够跟在该非终结符后面的终结符集合,咱们称做该非终结符的FOLLOW set.数据结构
以前的博文目录闭包
FOLLOW(s) = {EOI} FOLLOW(e) = {EOI, },+} FOLLOW(t) = {EOI, }, + , * } FOLLOW(f) = {EOI, }, +, * }
也就是说若是当前的输入字符属于e的FOLLOW SET,那么就能够根据第一个推导式作reduce操做app
若是构建的状态机,出现reduce / shift矛盾的节点均可以根据上面的原则处理的话,那么这种语法,咱们称之为SLR(1)语法。ide
可是若是当前的输入字符,既属于第一个推导式的FLLOW SET,又是第二个推导式 . 右边的符号,这样shift /reduce矛盾就难以解决了。学习
当咱们根据一个输入符号来判断是否能够进行reduce操做时,只须要判断在咱们作完了reduce操做后,当前的输入符号是否可以合法的跟在reduce后的非终结符的后面,也就是只要收集只要该符号可以被reduce到退回它的节点的全部路径的能跟在后面的终结符
这种能合法的跟在某个非终结符后面的符号集合,咱们称之为look ahead set, 它是FOLLOW set的子集。
在给出LookAhead Set的算法前要先明确两个个概念:
对一个给定的非终结符,经过一系列语法推导后,能出如今推导最左端的全部终结符的集合,统称为该非终结符的FIRST SET
若是一个非终结符,它能够推导出空集,那么这样的非终结符咱们称之为nullable的非终结符
nullable在以前SyntaxProductionInit里的初始化时已经赋值了
在前面的陈述后,为了可以解决shift/reduce矛盾,就须要一个lookAhead Set,固然在构建LookAhead Set前,就须要先有First Set
First Set构建都在FirstSetBuilder类里实现
这些就是用代码将上面的逻辑实现而已
这时候以前在SyntaxProductionInit初始化用到的symbolMap、symbolArray两个数据结构终于派上用场了
public void buildFirstSets() { while (runFirstSetPass) { runFirstSetPass = false; Iterator<Symbols> it = symbolArray.iterator(); while (it.hasNext()) { Symbols symbol = it.next(); addSymbolFirstSet(symbol); } } ConsoleDebugColor.outlnPurple("First sets :"); debugPrintAllFirstSet(); ConsoleDebugColor.outlnPurple("First sets end"); } private void addSymbolFirstSet(Symbols symbol) { if (Token.isTerminal(symbol.value)) { if (!symbol.firstSet.contains(symbol.value)) { symbol.firstSet.add(symbol.value); } return ; } ArrayList<int[]> productions = symbol.productions; for (int[] rightSize : productions) { if (rightSize.length == 0) { continue; } if (Token.isTerminal(rightSize[0]) && !symbol.firstSet.contains(rightSize[0])) { runFirstSetPass = true; symbol.firstSet.add(rightSize[0]); } else if (!Token.isTerminal(rightSize[0])) { int pos = 0; Symbols curSymbol; do { curSymbol = symbolMap.get(rightSize[pos]); if (!symbol.firstSet.containsAll(curSymbol.firstSet)) { runFirstSetPass = true; for (int j = 0; j < curSymbol.firstSet.size(); j++) { if (!symbol.firstSet.contains(curSymbol.firstSet.get(j))) { symbol.firstSet.add(curSymbol.firstSet.get(j)); } } } pos++; } while (pos < rightSize.length && curSymbol.isNullable); } } }
[S -> a .r B, C] r -> r1
r是一个非终结符,a, B是0个或多个终结符或非终结符的集合。
在自动机进入r -> r1所在的节点时,若是采起的是reduce操做,那么自动机的节点将会退回[S -> a .r B, C]这个推导式所在的节点,因此要正确的进行reduce操做就要保证当前的输入字符,必须属于FIRST(B)
因此推导式2的look ahead集合就是FIRST(B),若是B是空,那么2的look ahead集合就等于C, 若是B是nullable的,那么推导式2的look ahead集合就是FIRST(B) ∪ C
计算LookAhead set在每个production的方法里
public ArrayList<Integer> computeFirstSetOfBetaAndc() { ArrayList<Integer> set = new ArrayList<>(); for (int i = dotPos + 1; i < right.size(); i++) { set.add(right.get(i)); } ProductionManager manager = ProductionManager.getInstance(); ArrayList<Integer> firstSet = new ArrayList<>(); if (set.size() > 0) { for (int i = 0; i < set.size(); i++) { ArrayList<Integer> lookAhead = manager.getFirstSetBuilder().getFirstSet(set.get(i)); for (int s : lookAhead) { if (!firstSet.contains(s)) { firstSet.add(s); } } if (!manager.getFirstSetBuilder().isSymbolNullable(set.get(i))) { break; } if (i == lookAhead.size() - 1) { //beta is composed by nulleable terms firstSet.addAll(this.lookAhead); } } } else { firstSet.addAll(lookAhead); } return firstSet; }
居然计算了Lookahead Set,那么在计算闭包时,每一个节点里的推导式都要加上LookAhead Set以便以后求语法分析表
private void makeClosure() { ConsoleDebugColor.outlnPurple("==== state begin make closure sets ===="); Stack<Production> productionStack = new Stack<>(); for (Production production : productions) { productionStack.push(production); } while (!productionStack.isEmpty()) { Production production = productionStack.pop(); ConsoleDebugColor.outlnPurple("production on top of stack is : "); production.debugPrint(); production.debugPrintBeta(); if (Token.isTerminal(production.getDotSymbol())) { ConsoleDebugColor.outlnPurple("Symbol after dot is not non-terminal, ignore and process next item"); continue; } int symbol = production.getDotSymbol(); ArrayList<Production> closures = productionManager.getProduction(symbol); ArrayList<Integer> lookAhead = production.computeFirstSetOfBetaAndc(); Iterator<Production> it = closures.iterator(); while (it.hasNext()) { Production oldProduct = it.next(); Production newProduct = oldProduct.cloneSelf(); newProduct.addLookAheadSet(lookAhead); if (!closureSet.contains(newProduct)) { closureSet.add(newProduct); productionStack.push(newProduct); removeRedundantProduction(newProduct); } else { ConsoleDebugColor.outlnPurple("the production is already exist!"); } } } debugPrintClosure(); ConsoleDebugColor.outlnPurple("==== make closure sets end ===="); }
removeRedundantProduction是处理冗余的产生式,好比
1. [t -> . t * f, {* EOI}] 2. [t -> .t * f {EOI}]
这样就能够认为产生式1能够覆盖产生式2
private void removeRedundantProduction(Production product) { boolean removeHappended = true; while (removeHappended) { removeHappended = false; Iterator it = closureSet.iterator(); while (it.hasNext()) { Production item = (Production) it.next(); if (product.isCover(item)) { removeHappended = true; closureSet.remove(item); break; } } } }
到如今咱们已经算出了LookAhead Set,已经能够正确的计算语法分析表了,可是还有一个问题就是,如今的自动机节点过多,很是影响效率,因此下面的任务就是压缩有限状态自动机
在咱们以前构造的LR(1)有限自动机里,若是根据C语言的推导式,应该会产生600多个状态节点,可是是由于以前在构造状态节点时,若是相同的推导式可是它的lookAhead Sets不同,就认为这是两个不同的产生式。
下面是对状态节点的equals的重写
@Override public boolean equals(Object obj) { return checkProductionEqual(obj, false); } public boolean checkProductionEqual(Object obj, boolean isPartial) { ProductionsStateNode node = (ProductionsStateNode) obj; if (node.productions.size() != this.productions.size()) { return false; } int equalCount = 0; for (int i = 0; i < node.productions.size(); i++) { for (int j = 0; j < this.productions.size(); j++) { if (!isPartial) { if (node.productions.get(i).equals(this.productions.get(j))) { equalCount++; break; } } else { if (node.productions.get(i).productionEquals(this.productions.get(j))) { equalCount++; break; } } } } return equalCount == node.productions.size(); }
因此对这些推导式相同可是LookAhead Sets不一样的节点,就能够进行合并,以达到压缩节点数量的目的
合并类似的节点最好的地方,天然就是在添加节点和节点之间的跳转关系的时候了
public void addTransition(ProductionsStateNode from, ProductionsStateNode to, int on) { /* Compress the finite state machine nodes */ if (isTransitionTableCompressed) { from = getAndMergeSimilarStates(from); to = getAndMergeSimilarStates(to); } HashMap<Integer, ProductionsStateNode> map = transitionMap.get(from); if (map == null) { map = new HashMap<>(); } map.put(on, to); transitionMap.put(from, map); }
getAndMergeSimilarStates的逻辑也很简单,遍历当前的全部节点,找出类似,把编号大的合并到小的节点上
private ProductionsStateNode getAndMergeSimilarStates(ProductionsStateNode node) { Iterator<ProductionsStateNode> it = stateList.iterator(); ProductionsStateNode currentNode = null, returnNode = node; while (it.hasNext()) { currentNode = it.next(); if (!currentNode.equals(node) && currentNode.checkProductionEqual(node, true)) { if (currentNode.stateNum < node.stateNum) { currentNode.stateMerge(node); returnNode = currentNode; } else { node.stateMerge(currentNode); returnNode = node; } break; } } if (!compressedStateList.contains(returnNode)) { compressedStateList.add(returnNode); } return returnNode; }
public void stateMerge(ProductionsStateNode node) { if (!this.productions.contains(node.productions)) { for (int i = 0; i < node.productions.size(); i++) { if (!this.productions.contains(node.productions.get(i)) && !mergedProduction.contains(node.productions.get(i)) ) { mergedProduction.add(node.productions.get(i)); } } } }
这一节的贴的代码应该是到如今五篇里最多,可是主要的就是
解决shift/reduce矛盾
主要在于构造一个lookahead sets,也就是当前的输入符号是否可以合法的跟在reduce后的非终结符的后面压缩有限状态自动机节点
压缩节点在于合并推导式同样可是lookahead sets不同的节点
下一篇的内容比较少,也就是能够正式构造出语法分析表和根据表驱动的语法分析,也就表明语法分析阶段的结束
另外的github博客:https://dejavudwh.cn/