在上一次詞法分析的基礎之上,我完成了我的C語言的語法分析器。這次選擇的是用Java來實現,採用了自頂向下的分析方法,其思想是根據輸入token串的最左推導,試圖根據現在的輸入字符來判斷用哪個產生式來進行推導。
使用LL(1)分析法的問題就是對文法的要求較高,要求消除回溯了左遞歸,以致於後來在寫文法的時候遇到各種麻煩,做了各種消除(本來想偷個懶,不通過程序直接手動解決這個問題的,結果還是耗費了不少體力)。
預測分析法的關鍵在於預測分析表的構造。這就要求求出每個變量的FIRST集和FOLLOW集,並且根據這來求出每個產生式的SELECT集。所以只要在順利的理解了求解非終結符的FIRST集和FOLLOW集的算法之後,語法分析的實現其實已經勝利在望了。
圖1.求FIRST集的算法
/**
* 初始化First集
*
* @param expList
* @param mFirst
*/
public static List<String> initFirst(List<Grammar> expList,
List<String> mFirst) {
List<String> right = null;
for (int i = 0; i < expList.size(); i++) { // 初始化FIRST集
right = expList.get(i).getRightPart();
if (right.size() > 0) {
if (terminalSymbolMap.containsKey(right.get(0))) { // 若右部表達式的第一位爲終結符則加入nts的FIRST集
mFirst.add(right.get(0));
}
}
}
return mFirst;
}
/**
* 求非終結符的FIRST集
*
* @param nts
* 非終結符
* @return FIRST集
*/
public static List<String> FIRST(NonTerminalSymbol nts) {
List<Grammar> expList = getMyExp(nts.getValue());
List<String> mFirst = new ArrayList<String>();
List<String> right = null;
mFirst = initFirst(expList, mFirst);// 初始化工作
for (int i = 0; i < expList.size(); i++) {
right = expList.get(i).getRightPart();
if (right.size() > 0) {
if (nonTerminalSymbolMap.containsKey(right.get(0))) {// 若右部表達式的第一位爲非終結符則遞歸
combineList(mFirst,
FIRST(new NonTerminalSymbol(right.get(0))));
int j = 0;
while (j < right.size() - 1 && isEmptyExp(right.get(j))) { // 若X→Y1…Yn∈P,且Y1...Yi-1⇒*ε,
combineList(mFirst,
FIRST(new NonTerminalSymbol(right.get(j + 1))));
j++;
}
}
}
}
if (mFirst.contains("$"))
mFirst.remove("$");
combineList(nts.getFirst(), mFirst);
return mFirst;
}
/**
* 求一個文法串的FIRST集
*
* @param right
* 文法產生式的右部
* @return FIRST集
*/
public static List<String> getNTSStringFirst(List<String> right) {
List<String> mFirst = new ArrayList<String>();
if (right.size() > 0) { // 初始化,FIRST(α)= FIRST(X1);
String curString = right.get(0);
if (terminalSymbolMap.get(curString) != null)
combineList(mFirst, terminalSymbolMap.get(right.get(0))
.getFirst());
else {
combineList(mFirst, FIRST(new NonTerminalSymbol(curString)));
}
}
if (right.size() > 0) {
if (nonTerminalSymbolMap.get(right.get(0)) != null) { // 第一個符號爲非終結符
if (right.size() == 1) { // 直接將這個非終結符的first集加入right的first集
combineList(mFirst, nonTerminalSymbolMap.get(right.get(0))
.getFirst());
} else if (right.size() > 1) {
int j = 0;
while (j < right.size() - 1 && isEmptyExp(right.get(j))) { // 若Xk→ε,FIRST(α)=FIRST(α)∪FIRST(Xk+1)
String tString = right.get(j + 1);
if (nonTerminalSymbolMap.get(tString) != null) {
combineList(mFirst, FIRST(new NonTerminalSymbol(
tString)));
} else {
combineList(mFirst, terminalSymbolMap.get(tString)
.getFirst());
}
j++;
}
}
}
}
return mFirst;
}
/**
* 求非終結符的FOLLOW集
*
* @param nts
* 非終結符
* @return FOLLOW集
*/
public static void FOLLOW() {
for (int i = 0; i < grammarList.size(); i++) {
String left = grammarList.get(i).getLeftPart();
List<String> right = grammarList.get(i).getRightPart();
for (int j = 0; j < right.size(); j++) {
String cur = right.get(j);
if (terminalSymbolMap.containsKey(cur)) // 若是終結符則忽略
continue;
if (j < right.size() - 1) {
List<String> nList = new ArrayList<String>();
for (int m = j + 1; m < right.size(); m++)
nList.add(right.get(m));
List<String> mfirst = getNTSStringFirst(nList);
for (String str : mfirst) {
if (!nonTerminalSymbolMap.get(cur).getFollowList()
.contains(str)) {
combineList(nonTerminalSymbolMap.get(cur)
.getFollowList(), mfirst);
whetherChanged = true;
break;
}
}
boolean isNull = true; // 判斷A→αBβ,且β⇒*ε,其中β能否爲空
for (int k = 0; k < nList.size(); k++) {
if (!isEmptyExp(nList.get(k))) {
isNull = false;
break;
}
}
if (isNull == true) {
for (String str : nonTerminalSymbolMap.get(left)
.getFollowList()) {
if (!nonTerminalSymbolMap.get(cur).getFollowList()
.contains(str)) {
combineList(nonTerminalSymbolMap.get(cur)
.getFollowList(), nonTerminalSymbolMap
.get(left).getFollowList());
whetherChanged = true;
break;
}
}
}
} else if (j == right.size() - 1) { // 若該非終結符位於右部表達式最後一個字符
List<String> mfollow = nonTerminalSymbolMap.get(left)
.getFollowList();
for (String str : mfollow) {
if (!nonTerminalSymbolMap.get(cur).getFollowList()
.contains(str)) {
combineList(nonTerminalSymbolMap.get(cur)
.getFollowList(), mfollow);
whetherChanged = true;
break;
}
}
}
}
}
}
上面的兩張圖很詳細的表述瞭如何求解FIRST集和FOLLOW集的算法,我正是根據這兩個算法將其實現的。在實現的過程中,我引入了一個缺陷,我的FIRST求解用到了遞歸求解,而這樣可以正常運行的條件必須是使用的文法不存在左遞歸,不然就會產生StackOverFlow的錯誤。因此在對FOLLOW集求解的時候,爲了避免文法存在右遞歸,不能再使用同求FIRST集類似的遞歸來求解了。我採用了每執行一遍求FOLLOW集的方法,將所有的非終結符的FOLLOW集都求解出來,如果其中有一個非終結符的FOLLOW集發生了改變,則再執行一遍該方法。根據FIRST集和FOLLOW集就可以很快捷的求出SELECT集,並構造出預測分析表。
/**
* 求一個文法表達式的SELECT集
*
* @param g
* 一個文法表達式
* @return SELECT集
*/
public static List<String> SELECT(Grammar g) {
List<String> mselect = new ArrayList<String>();
if (g.getRightPart().size() > 0) {
if (g.getRightPart().get(0).equals("$")) { // 含有空表達式
mselect = nonTerminalSymbolMap.get(g.getLeftPart())
.getFollowList();
return mselect;
} else {
mselect = getNTSStringFirst(g.getRightPart());
boolean isNull = true;
for (int i = 0; i < g.getRightPart().size(); i++) { // 如果右部可以推出空
if (!isEmptyExp(g.getRightPart().get(i))) {
isNull = false;
break;
}
}
if (isNull) {
combineList(mselect,
nonTerminalSymbolMap.get(g.getLeftPart())
.getFollowList());
}
return mselect;
}
}
return null;
}
/**
* 構造預測分析表
*/
public static void getAnalysisTable() {
List<String> synString = new ArrayList<String>();
synString.add(SYN);
for (int i = 0; i < grammarList.size(); i++) {
Grammar grammar = grammarList.get(i);
if (analysisTable.get(grammar.getLeftPart()) != null) { // 如果多個文法表達式的左部相同,則將其合併
for (String sel : grammar.getSelect()) {
analysisTable.get(grammar.getLeftPart()).put(sel,
grammar.getRightPart());
}
} else {
HashMap<String, List<String>> inMap = new HashMap<String, List<String>>();
for (String sel : grammar.getSelect())
inMap.put(sel, grammar.getRightPart());
analysisTable.put(grammar.getLeftPart(), inMap);
}
NonTerminalSymbol nts = nonTerminalSymbolMap.get(grammar
.getLeftPart());
List<String> msynList = nts.getSynList();
for (int j = 0; j < msynList.size(); j++) {
analysisTable.get(grammar.getLeftPart()).put(msynList.get(j), // 將同步記號集加入分析表中
synString);
}
}
}
下面簡要的說一下我的數據結構。我對終結符、非終結符、文法產生式和單詞封裝到了實體類中。將符號的值與其FIRST集、FOLLOW集或SELECT集進行了關聯。
圖3.主要展現實體類的成員變量
程序中所有的終結符和非終結符分別保存在一個一維的哈希表中,其值作爲HashMap的key,而其值所對應的對象作爲HashMap的的value;所有經過程序處理的僅包含一個產生式右部的產生式保存在一個LIST中,LIST中裝載着文法所對應的Grammar對象;預測分析表保存在一個二維的哈希表中,這個HashMap的key是某一個非終結符,value是其對應的一個一維HashMap,這個一維HashMap的key是終結符,value是某非終結符遇到某終結符時所對應的操作(一個產生式的右部或是同步記號)。
錯誤處理主要是依賴於同步記號集合,所以在語法分析中錯誤處理的關鍵就在於同步記號記號的構造。我的非終結符A的同步記號集合是將A的FIRST集、FOLLOW集、其高層結構的FIRST集組合而成的。在語法分析的時候,如果輸入緩衝區中是終結符a,棧頂爲非終結符,在預測分析表中對應的表項爲空,則忽略終結符a;在預測分析表中對應的表項是synch(定義這樣一個字符串來表示同步記號),則將當前棧頂的非終結符彈出,繼續分析;如果棧頂的終結符和輸入符合a不匹配,則將棧頂終結符彈出。
分析過程部分代碼:
/**
* LL(1)語法分析控制程序,輸出分析樹
*
* @param sentence
* 分析的句子
*/
public static void myParser(List<String> sentence) {
Stack<String> stack = new Stack<String>();
int index = 0;
String curCharacter = null;
stack.push("#");
stack.push("program");
while (!stack.peek().equals("#")) {
if (index < sentence.size()) // 注意下標,否則越界
curCharacter = sentence.get(index);
if (terminalSymbolMap.containsKey(stack.peek())
|| stack.peek().equals("#")) {
if (stack.peek().equals(curCharacter)) {
if (!stack.peek().equals("#")) {
stack.pop();
index++;
}
} else {
System.err.println("當前棧頂符號" + stack.pop() + "與"
+ curCharacter + "不匹配");
}
} else {
List<String> exp = analysisTable.get(stack.peek()).get(
curCharacter);
if (exp != null) {
if (exp.get(0).equals(SYN)) {
System.err.println("遇到SYNCH,從棧頂彈出非終結符" + stack.pop());
} else {
System.out.println(stack.peek() + "->"
+ ListToString(exp));
stack.pop();
for (int j = exp.size() - 1; j > -1; j--) {
if (!exp.get(j).equals("$")
&& !exp.get(j).equals(SYN))
stack.push(exp.get(j));
}
}
} else {
if (index < sentence.size()-1){
System.err.println("忽略" + curCharacter);
index++;
}else {
System.err.println("語法錯誤,缺少 '}' !");
break;
}
}
}
}
}
總體來說,寫完了這次的語法分析,發現寫的代碼還存在着很多的問題,請指正!
鑑於各位需要工程項目,故此上傳到資源中,請自行下載!下載地址