Java實現C的語法分析器(預測分析法)

         在上一次詞法分析的基礎之上,我完成了我的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;
	}

圖2.求FOLLOW集的算法

	/**
	 * 求一個文法串的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;
					}
				}
			}
		}
	}

總體來說,寫完了這次的語法分析,發現寫的代碼還存在着很多的問題,請指正!

鑑於各位需要工程項目,故此上傳到資源中,請自行下載!下載地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章