標籤:name 第一個 span osi rac 進一步 添加 規則 程式員
最近看完了<c++ primer>,發現c++11標準庫已經有Regex了(本人落後編譯器多年,現在都已經c++17了),正好我最近想擼個compiler,索性就先擼個詞法分析器,類似flex。項目代碼量很小,一共不到400行,但是如果不用Regex庫,自己寫NFA,DFA滿足最簡陋的正則文法也是挺輕鬆的,但是想要滿足Posix標準的文法,那就相當煩了,比如對大括弧{}的處理,\d{3}表示3位元字等等。那麼為何不直接用flex呢?一是因為flex不能很好地支援c++,第二是想拿c++練練手。下面簡單地介紹下項目實現:
github: https://github.com/Yuandong-Chen/mini-lexer
項目一共就三個cpp檔案和三個與之對應的hpp標頭檔。用過flex的都知道,我們可以定義自己的Regex宏比如: A [a-z],然後再套用這個宏A去構建更複雜的Regex比如: {A}+(ok)\$ 去構建我們的token結構。然後還能在這個token上加上action,比如:{A}+(ok)\$ {printf("ok!\n"); return 1;} 然後對一系列這樣的token,根據他們的優先順序(前後順序),去分割文本內的字串。對外介面是yylex(), yytext,yyleng,yyline,yyin,yyout等等。於是我們就把這一系列的實現分為三個步驟,第一個檔案實現Regex宏的存取,我們用map<string, string>去儲存,標頭檔如下:
1 #pragma once 2 #include <map> 3 4 namespace minilex { 5 6 class MacroHandler 7 { 8 private: 9 std::map<std::string, std::string> macrotab;10 11 public:12 MacroHandler() = default;13 ~MacroHandler() = default;14 std::string expandMacro(const std::string& macroname);15 void addMacro(std::pair<std::string, std::string> macro);16 };17 }
其中addMacro是存宏定義比如 {string("A"), string("[a-z]")}這樣一個pair結構,expandMacro取宏定義,但是會在最外層加上括弧,如果在map裡頭沒發現就返回Null 字元串。具體實現就不貼了,完全是上面的描述,具體參考項目代碼即可。
第二個檔案用於處理類似 {A}+(ok)\$這樣的運算式,已經對給定的字串s,我們根據已有的Regex規則和對應的順序去分割字串,標頭檔如下:
1 #pragma once 2 #include <functional> 3 #include <list> 4 #include <regex> 5 #include "MacroHandler.hpp" 6 7 namespace minilex { 8 9 class RegularExp10 {11 private:12 bool success = false;13 std::string currentMatch = "";14 std::unique_ptr<MacroHandler> macrohp;15 std::list<std::pair<std::unique_ptr<std::regex>, std::string> > regularexps;16 public:17 std::string expandRegularExp(const std::string& rexp);18 std::string extractMacroName(const std::string& rexp, int &index, int max);19 std::string expandMacro(const std::string& macroname);20 public:21 RegularExp(std::unique_ptr<MacroHandler>&& macrorp);22 ~RegularExp() = default;23 void addRegularExp(std::string rexp);24 void removeRegularExp(std::string rexp);25 std::string eat(std::string& txt);26 bool isEaten(){return success;};27 std::string matchPattern(){return currentMatch;};28 };29 30 }
注意到,這裡也有個addRegularExp,我們可以這樣調用 addRegularExp("{A}+(ok)\$"); 我們可以用expandRegularExp函數完全展開Regex, 在這個函數裡頭就會去尋找並替換宏定義,如果A定義為{B},那麼函數會遞迴地解析宏定義,如果出現A A{A}這樣的遞迴宏,函數會無限遞迴調用並拋出棧溢出錯誤。removeRegularExp函數用於動態地移除規則,比如我們添加了"[a-z]+(ok)\$"運算式,但是文本分割到某個程度時,因為某些原因(比如undef等等)我們不需要這條規則了,我們就可以移除這個運算式。eat函數用於吃字串,吃掉的字串返回,吃剩的放在參數中。matchPattern函數告訴我們匹配了哪條Regex(或說規則)。具體實現由於利用了c++11的regex,非常簡單,可以查看項目具體代碼。
第三個檔案就是用於實現yylex()等對外介面了,這裡就貼出yylex這個函數的實現:
1 int MiniLex::yylex() { 2 std::string eaten; 3 std::string pattern; 4 if(yyin.eof() && linebuffer.empty()) 5 { 6 return 0; 7 } 8 9 if(linebuffer.empty())10 {11 std::getline(yyin, linebuffer);12 yyline++;13 }14 15 eaten = reup->eat(linebuffer);16 yyleng = eaten.size();17 if(!reup->isEaten())18 {19 std::cerr<<"STOP, CANNOT INTERPRET STRING: "<<linebuffer<<std::endl;20 return 0;21 }22 23 pattern = reup->matchPattern();24 25 /* you are required to modify the following code for your own purposes */26 if(pattern == std::string("{Digit}+")) {27 std::cerr<<"RECOGNIZE: "<<yyleng<<‘ ‘<<eaten<<std::endl;28 return 1;29 }30 else if(pattern == std::string("{Alpha}+")) {31 std::cerr<<"RECOGNIZE: "<<yyleng<<‘ ‘<<eaten<<std::endl;32 return 2;33 }34 else if(pattern == std::string("{Equal}")) {35 std::cerr<<"RECOGNIZE: "<<yyleng<<‘ ‘<<eaten<<std::endl;36 return 3;37 }38 else if(pattern == std::string("{CAL}")) {39 std::cerr<<"RECOGNIZE: "<<yyleng<<‘ ‘<<eaten<<std::endl;40 return 4;41 }42 else if(pattern == std::string(".")) {43 std::cerr<<"UNRECOGNIZE: "<<yyleng<<‘ ‘<<eaten<<std::endl;44 return 5;45 }46 47 return 0;48 }
這裡有個小的問題,就是無法正確匹配如 [a-z]"\n"[0-9]這樣的Regex,因為我們是一行一行地讀入,一行匹配完了才去讀下一行,並匹配下一個token,但是我想沒人會定義這樣一個奇怪的跨行的token吧。另外我沒有進一步去實現讀取配置文檔產生代碼方式,而是讓程式員直接去修改我們的原始碼,我覺得這樣更加自由,你甚至可以大改特改我們的源檔案,而非估摸一堆古怪的配置文法。
C++11利用regex輕鬆實現詞法分析器mini-lexer