語法分析器初步學習——LISP語法分析ios
本文參考自vczh的《如何手寫語法分析器》。數據結構
LISP的表達式是按照前綴的形式寫的,好比(1+2)*(3+4)在LISP中會寫成(*(+ 1 2)(+ 3 4)),1 + 2會寫成(+ 1 2)。函數
LISP語言的語法以下形式:學習
1.Operator = 「+」 | 「-」 | 「*」 | 「/」測試
2.Expression = <數字> | 」(」Expression」)」 | 「(」Operator Expression Expression」)」spa
咱們根據以上兩條語法規則來寫代碼:設計
// LISP語法分析器 #include <iostream> #include <string> using namespace std; // 檢測是不是空白符 bool IsBlank(char ch) { return ch == ' ' || ch == '\t'; } // 檢測Text是不是Stream的前綴 // 若是是前綴,則返回true,並將pos前移Text.size()個字符 // 若是不是前綴,則返回false // 此函數一開始會過濾掉Stream開頭的空格 bool IsPrefix(const string& Stream, int& pos, const string& Text) { int read = pos; // 過濾空白符 while (IsBlank(Stream[read])) { ++read; } // 不能寫爲: // while (IsBlank(Stream[read++])); // 由於這樣寫會致使read至少加1 if (Stream.substr(read, Text.size()) == Text) // 若是是前綴 { pos = read + Text.size(); return true; } else { return false; } } // 檢測Stream開頭是不是操做符+、-、*、/ // 是的話,函數返回實際的操做符,並將pos便宜到操做符以後 // 不然返回0 // 判斷語法1:Operator = 「+」 | 「-」 | 「*」 | 「/」 char IsOperator(const string& Stream, int& pos) { if (IsPrefix(Stream, pos, "+") || IsPrefix(Stream, pos, "-") || IsPrefix(Stream, pos, "*") || IsPrefix(Stream, pos, "/")) // 若是開頭是操做符 { return Stream[pos - 1]; // 若是是的話,pos已經向前偏移了 } else { return 0; } } // 表達式結構體 struct Expression { int Result; // 返回表達式結果 string Error; // 返回錯誤信息,沒錯誤則爲空 int Start; // 錯誤發生的位置 Expression() : Result(0), Start(0) {} }; // 檢測Stream開頭是不是數字,若是是,則將pos便宜到數字以後 // 函數返回Expression // 判斷語法2中的第一部分:Expression = <數字> Expression GetNumber(const string& Stream, int& pos) { Expression Result; bool GotNumber = false; int read = pos; // 過濾空白符 while (IsBlank(Stream[read])) { ++read; } while (true) { // 依次讀入一個字符 char ch = Stream[read]; if (ch >= '0' && ch <= '9') { Result.Result = Result.Result * 10 + ch - '0'; GotNumber = true; ++read; } else { break; } } if (GotNumber) { pos = read; } else { Result.Error = "這裏須要數字"; Result.Start = read; } return Result; } // 檢測Stream開頭是不是表達式 // 若是是,則將pos前移到表達式後 // 實現語法2:Expression = <數字> | 「(」Expression「)」 | 「(」Operator Expression Expression「)」 Expression GetExpression(const string& Stream, int& pos) { int read = pos; // 檢測開頭是不是數字 // 語法2第一部分:Expression = <數字> Expression Result = GetNumber(Stream, read); if (!Result.Error.empty()) // 若是開頭不是數字 { if (IsPrefix(Stream, read, "(")) // 檢測是否"("開頭 { // 將Result的Error清空 Result.Error.clear(); char Operator = 0; if ((Operator = IsOperator(Stream, read)) != 0) // 若是是操做符,語法2第三部分:Expression = 「(」Operator Expression Expression「)」 { // 獲取左參數 // 遞歸調用 Expression left = GetExpression(Stream, read); if (!left.Error.empty()) { return left; } // 保持當前read int rightRead = read; // 獲取右參數 // 遞歸調用 Expression right = GetExpression(Stream, read); if (!right.Error.empty()) { return right; } // 根據操做符Operator進行計算 switch (Operator) { case '+': Result.Result = left.Result + right.Result; break; case '-': Result.Result = left.Result - right.Result; break; case '*': Result.Result = left.Result * right.Result; break; case '/': if (right.Result == 0) // 除數爲0 { Result.Error = "除數爲0"; Result.Start = rightRead; } else { Result.Result = left.Result / right.Result; } break; default: // 這種狀況不會發生,由於前提是Operator,因此只有+、-、*、/四種狀況 Result.Error = "未知的操做符"; Result.Start = read; return Result; } } else // 若是不是操做符 // 語法2的第二部分:Expression = 「(」Expression「)」 { // 獲取表達式 Result = GetExpression(Stream, read); // 若是獲取失敗,則直接返回 if (!Result.Error.empty()) { return Result; } } // 檢測是否有配套的")" if (!IsPrefix(Stream, read, ")")) { Result.Error = "此處缺乏右括號"; Result.Start = read; } } } // 若是沒有出錯,則更新pos if (Result.Error.empty()) { pos = read; } // 檢測是否有配套")"時,若是不存在,能夠直接將Result返回 // 這樣在後面就不用檢測是否出錯了,由於前面凡是出錯的狀況 // 都返回了,這樣就不用檢測了,而直接更新pos: pos = read return Result; } // 測試 int main() { while (true) { string Stream; cout << "輸入一個LISP表達式" << endl; getline(cin, Stream); int pos = 0; if (IsPrefix(Stream, pos, "exit")) { break; } pos = 0; Expression Result = GetExpression(Stream, pos); if (!Result.Error.empty()) { cout << "表達式錯誤" << endl; cout << "位置:" << Result.Start << endl; cout << "錯誤信息:" << Result.Error << endl; } else { cout << "結果:" << Result.Result << endl; } } return 0; }
下面對程序代碼解釋以下:3d
數據結構code
程序中用string型的字符串Stream來存儲用戶輸入的表達式,int型的pos 做爲當前掃描的位置。blog
Expression結構體用來標識語法二中的Expression,Expression結構體能夠標識<數字> | 」(」Expression」)」 | 「(」Operator Expression Expression」)」三種形式的任意一種。Result元素用來記錄正確表達式時的計算結果,Error用來當語法分析表達式時遇到的錯誤時,將錯誤信息記錄下來。Start用來記錄錯誤的位置,準確來說是用來記錄最後正確的下一個位置,由於錯誤字符串前的空白符會被忽略。
函數
IsBlank:用來檢測char字符是否爲空白符。
IsPrefix:用來檢測Text字符串是不是Stream從pos起頭的前綴。
IsOperator:用來檢測Stream從pos起頭,是不是以操做符+、-、*、/開頭的。
GetNumber:用來檢測Stream從pos起頭,是不是以數字開頭的。
GetExpression:用來檢測Stream從pos開頭,是否以表達式開頭。
語法分析器的關鍵在於寫出正確的語法,而後根據語法導出代碼。語法定義中存在遞歸定義,代碼中相應地也會出現遞歸調用。代碼應該一一對應的對應到語法定義。
設計的巧妙之處
pos和read:pos用來標識掃描的位置,pos只用來記錄正確分析的位置。在IsPrefix、GetNumber、GetExpression等函數中,都會在定義一個read,初始化爲pos,用來做爲實際的掃描遊標,只有在正確分析後,read纔會用來更新pos,不然read會返回給Result的Start,用來記錄錯誤發生的位置,而且直接返回Result。用read來代替pos的內部操做,能夠防止當分析失敗時,還要講pos還原的問題。
Expression結構體:Expression結構包含三個元素,Start和Error字段能夠很地記錄錯誤發生的位置以及錯誤信息。這樣能夠很好的處理髮生錯誤的狀況。
以上是最爲簡單的遞歸降低語法分析,更多的語法分析方面的知識有待進一步學習。