chrysanthemum框架是一個使用C++11標準實現的面向對象的遞歸降低分析器生成框架,框架使用C++編譯器的編譯期推導能力,以及C++操做符重載的能力,構建了一個C++環境中的元語言,使用該元語言,可以使用戶在C++環境中「書寫ABNF範式」,框架可以從這些「ABNF範式」中自動推導並生成對應的匹配器或解析器,從而極大地縮短開發時間。 node
chrysanthemum框架主要包含3個部分,分別爲掃描器、匹配器和語義動做器。基本工做流程以下: git
掃描器 -> 匹配器 -> 語義動做
掃描器主要負責接收一段線性輸入,並將內容提供給匹配器進行匹配,同時(有必要的化)作一些文本的預處理和統計工做。 匹配器是整個框架的核心。匹配器嘗試以一系列完整定義的規範來匹配掃描器提供的輸入,這些規範被稱爲語法規則。成功匹配時,將執行預約義的語義動做(若是有語義動做的話)。最後,匹配器將成功匹配到的文本發送到語義動做器,語義動做器則負責提取出被匹配文本中的信息。 正則表達式
掃描器的任務是順序的將輸入數據流傳遞給匹配器。掃描器包含三個兼容STL標準的前向迭代器,beg、cur和end,其中beg和end描述了待掃描文本的整體區間範圍,而cur則描述了下一個待掃描的字符所在位置。在匹配過程當中,匹配器從掃描器得到數據,並經過掃描器對迭代器進行重定位。掃描器的接口是一個十分簡潔但功能完備的集合,僅包含一個get函數、一個increase函數以及一個get_and_increase函數。 框架提供的掃描器是一個模板類,使用POLICY-BASED DESIGN方法進行設計: express
template <typename Iterator,template <class> class Policy> struct scanner:public Policy<Iterator> { /* ... */ //////////////////// Iterator beg; Iterator end; Iterator cur; ////////////////////////// };
其中模板參數Iterator指定了前向迭代器的類型,而模板參數Policy決定了掃描器的掃描策略,從而決定了掃描器的行爲。 在大多數狀況下,關於這些掃描策略的知識並非必須的。然而對於構造一個符合本框架標準的匹配器時,有關掃描器的基本知識就是必須的了。基於POLICY-BASED DESIGN的設計使得掃描器頗有彈性和擴展性。經過編寫不一樣的掃描策略能夠方便的指定掃描器的行爲。一個例子就是不區分大小寫的策略,這種策略在分析大小寫不敏感的輸入時,是頗有用的。 定義一個合乎規範的掃描器策略,必需要知足下列要求: 編程
以框架提供的字符統計掃描器策略爲例: json
template <typename Iterator> struct line_counter_scanner_policy { ////////////////////////////////////////////// const value_type& do_get(Iterator it) const { return *it; } value_type& do_get(Iterator it) { return *it; } void do_increase(Iterator& it) { ++consumed; if(character_type_traits<value_type>::isLF(*it)) { ++line_no; col_no = 0; } else ++col_no; ++it; } bool at_end(Iterator it,Iterator end) { return it == end; } /////////////////////// std::size_t line_no; std::size_t col_no; std::size_t consumed; };
該掃描器策略可以在匹配過程當中的統計當前行數,列數,與總匹配字符數,這些信息在匹配出錯的時候,可以提供簡單的診斷信息。 數據結構
全部匹配器在概念上遵照一個公共的接口協議與規範。注意,這裏的接口協議指的是C++泛型編程中泛化的接口概念,而非面向對象編程中所指的純虛函數等概念。意即全部符合該接口協議的對象均可認爲是一個概念上完備的匹配器。所以,只要依據協議和規範,任何人均可以輕易的完成一個能與本框架其餘部分完美整合的匹配器。這也是chrysanthemum框架具備高擴展性的主要緣由。 泛化的接口協議與規範的具體要求以下: 框架
匹配器必須實現一個以下形式的成員模板函數。 函數式編程
template bool operator()(Scanner& scan) {/ ... / } 函數
例如,假如咱們想定義一個新的匹配器類型Parser,Parser的聲明必須爲:
struct Parser:public parser_base<Parser> { template <typename Scanner> bool operator()(Scanner& scan) { /* ... */ } /* ... */ };
也就是說,全部的匹配器對象都必須是一個仿函數,其接受一個掃描器的引用做爲一個參數,並返回一個bool值表示匹配的成功與否。之因此要求全部的匹配器對象都必須是仿函數,一方面是由於仿函數可以與框架的函數式編程風格很好的配合,另一個緣由是仿函數能夠被很方便的綁定到std::function對象當中,這種運行期的綁定可以爲框架帶來動態分析能力。
匹配器能夠細分爲:原子匹配器,合成物以及規則。
原子匹配器是匹配器的最小和最基本的單位。chrysanthemum提供了兩種原子匹配器:1)charactor-level 2)literal
字符級匹配器通常只匹配知足某個範圍的一個字符。好比:digit_p用於匹配數字字符 ‘0’ 到 ‘9’中的任一個;lower_p 用於匹配任意小寫字母,alpha_p用於匹配任意字母。基本字符集匹配器與C++庫函數終的 isdigit islower 等函數一一對應。除此以外還有一些特殊的字符級匹配器,好比any_p匹配任意字符,而void_p不消耗任何輸入,並且老是返回true。
字面意義匹配器用於匹配「字面意義」。好比表達式 auto p = _literal("hello world") 產生一個字面意義匹配器p,匹配字面意義的文本「hello world」。 auto q = _literal('x') 匹配單一字符‘x’。
chrysanthemum框架提供了一系列的操做符,這些操做符重載自C++的操做符。經過這些操做符,咱們能夠將低階的的匹配器組裝成複雜的高階的合成物,這些合成物也是匹配器,好比下面的表達式:
lower_p a; upper_p b; auto c = a | b; //框架重載了 | 操做符。 auto d = a | b | digit_p();
表達式a | b 實際上構成了一個新的匹配器c,表示匹配a或者b。更進一步,a和b的類型分別是 upper_p和lower_p,那麼他們所構成的新的合成匹配器表示匹配小寫字母或者大寫字母,所以c的類型實際上等價於 alpha_p。 而匹配器 d 表示匹配字母或數字,等價於預約義的字符級匹配器 alnum_p.
實際上c的類型由兩個操做子a和b的類型已經組合器的類型組合而成。他們所構成的新的合成匹配器的類型爲:
or_p<upper_p,lower_p>
因此上邊表達式也能夠聲明爲:
or_p<upper_p,lower_p> c = a | b;
對於稍簡單的表達式,這種寫法雖然能夠接受,可是在不少狀況下,若是a和b自己也是很是複雜的合成對象,這種顯式的聲明類型的方式,將會給框架的使用者形成極重的思惟和打字的負擔,並且每每最終結果的類型會複雜到使用者也沒法理解的地步。得益於C++11新標準,咱們能夠將這種髒累活交給編譯器去處理,使用auto關鍵字聲明的對象,C++編譯器會自動推導出其類型。
全部的二元操做符,當有至少有一方是匹配器時,另外一方能夠對字符或字符串進行隱式轉換,好比:下面的表達式等價
auto c = _literal('A') | 'B' <=> auto c = _literal('A') | _literal('B')
而
auto c = 'A' | 'B' 不是個匹配器,而是對字符'A'和'B'做按爲與運算。
注:表格中a,b...表示匹配器,A,B...表示a,b...對應的ABNF範式。本文中全部表格均遵循相同的約定。
操做符 | ABNF | 說明 |
---|---|---|
a|b | A|B | 匹配a或者匹配b |
a&b | A B | 先匹配a而後匹配b |
a-b | 無 | 匹配a 但不匹配b |
!a | 無 | 全部不匹配a的 |
能夠看出,差集操做符和求反操做符在ABNF中並無與之對應的表示,這兩個操做符其實是Chrysanthemum框架對ABNF範式的補充,雖然在理論上,這兩個操做符並非必須的,可是在實際的編程操做中倒是極爲有用的。
auto first_digit = _digit() - '0';
從集合的角度來看,_digit()用於匹配集合"0..9"中的任意元素,而從中排除掉「0」後,就是所須要的集合「1..9」。 再如若是你須要一個匹配全部非空白字符的匹配器時,只須要簡單的對space_p匹配器求反便可:
auto not_sapce = !_space();
須要注意的一點是操做符"|"的短路特性:「|」操做符以自左向右先到先得的方式一個一個測試它的操做子,當發現一個正確匹配的操做子後,匹配器就結束匹配,從而完全的中止搜索潛在匹配。這種隱式的短路特性隱式的給與最左邊地選項一最高的優先級。這種短路的特性在C/C++的表達式中一樣存在:好比if(x<3||y<2)這個表達式裏,若是x小於3成立,那麼y<2這個條件根本就不會被測試。短路除了給予選項必要地隱式的優先級規則,還賦予Chrysanthemum分析器非肯定性行爲,從而縮短了執行時間。若是你的選項的位置在與表達式的邏輯沒有關係,那麼儘量的把最可能出現的匹配項放在最前面能夠將效率最大化。
操做符 | ABNF | 說明 |
---|---|---|
*a | 無 | 重複使用a匹配0到任意屢次 |
+a | 無 | 重複使用a匹配1到任意屢次 |
-a | [A] | 配a 0或1次 |
a%b | 無 | 重複使用a匹配一個序列,該序列以b所匹配的內容做爲分隔符 |
_N<M>(a) | M A | 重複使用a匹配M次 |
_repeat<N,M>(a) | N*M A | 重複使用a匹配,至少N次,至多M次 |
能夠看出這組操做符均和循環有關。因爲框架是基於C++的,因此框架提供的操做符也依賴於對C++操做符的重載。因爲C++操做符中並無合適的操做符與 N A 和 N*M A 相對應,因此框架並未提供對應的操做符,而是提供了2個對應的函數_N和_repeat。
上表中的克林星號操做符和加號操做符是框架對ABNF範式的擴充,但在工程實踐當中,這兩個操做符是常常用到,且十分重要的,請注意,與正則表達式不一樣,克林星號操做符和加號操做符是放在匹配器的前面而非後面的,這是C++操做符重載的限制。
咱們能夠很容易的看出下面這幾對匹配器在效果上是等價的:
*a <=> _repeat_p<0,INFINITE>(a) +a <=> _repeat_p<1,INFINITE>(a) -a <=> _repeat_p<0,1>(a) a % b <=> a & *(b&a)
因爲咱們的操做符都是在C++裏定義的,所以必須遵照C/C++的操做符優先級規則。把表達式用括號分組則可超越這個規則。好比,*(a|b)應當被理解爲匹配a或b零到任意屢次。
規則是框架中另一個很是重要的模塊。規則也是匹配器。
規則有2個重要做用,第一是佔位符,第二是在解析文本的過程當中保存上下文。 rule允許咱們在某個時間點聲明一個匹配器,並在之後的某個時間再定義它,這點對於解析複雜的結構遞歸的文本相當重要。
rule<scanner_t,int,no_skip> integer; //聲明一個空的規則 ... some code here.. ... integer %= -(_literal('+') | '-') & +_digit(); //定義一個規則 ... some code here.. ... integer %= +digit(); //從新定義規則
從上面能夠看出,規則rule是模板類。其有3個模板參數,第一個是掃描器類型,第二個是context類型,即規則的上下文類型,最後一個是skiper的類型。注意定義一個規則使用的是 "%=" 操做符。
咱們能夠把rule的context理解爲被解析內容在內存中的表述形式,即數據結構。實際上rule的context實際上被組織成了一個棧,訪問當前的上下文使用rule的cur_ctx()函數,cur_ctx()函數將返回當前上下文的引用。框架提供了一個預約義的no_context類型,當指定了no_context後,rule將再也不具備上下文。後面咱們將在後面的例子終進一步瞭解context以及相關的一些函數。
skiper也是一個解析器,用於匹配在使用規則解析文本以前和解析以後應當被忽略掉的無心義字符。好比在C語言的賦值語句中變量與等號之間,等號與變量之間能夠存在多個空格:
int a = 0,b; b = a;
在上面的例子中,咱們所使用的no_skip參數,是一個框架預約義的類型,用來告訴規則,在其解析以前和解析以後不忽略任何內容。
一個合成的匹配器構成了一個層次結構。分析過程由最頂層的器匹配器開始,它負責代理併爲下層匹配器分配分析任務,這個過程不斷遞歸降低,直至達到原子匹配器爲止。藉由將語義動做附着到這個層次的不少附着點上。咱們能夠將平滑的線性輸入流轉換爲結構性的對象。 附着了語義動做的匹配器,即爲解析器,因其不只具有了匹配能力,同時可以會將匹配到的文本傳遞給語義動做,由語義動做對傳遞過來文本進行進一步深刻的加工和處理。任何匹配器都可以與語義動做綁定。
語義動做的規範和要求相對來講比較簡單。任何符合函數簽名
bool(Iterator,Iterator)
的函數或函數對象均可以做爲語義動做。也就是說語義動做其實是一個接受一對迭代器的函數或函數對象,返回值爲bool類型,用以表示語義動做執行的成功與否。這對迭代器,[first,last),描述了被匹配的文本在數據流中所在的範圍。
假設咱們要解析一個無符號10進制整數,如:12345 構造對應的匹配器以下:
auto uint_p = (_digit() - '0') & *(_digit());
將一個函數或函數對象與之總體掛鉤,咱們就能夠從中讀取到數值。
struct to_int { template <typename It> bool operator()(It first,It last) { std::string str(first,last); i = atoi(str.c_str()); std::cout<<"the num is "<<i<<endl; } int i; }; auto f = to_int(); auto uint_p = ((_digit() - '0') & *(_digit())) <= f;
「<=」符號爲「注入」符號,它將一個函數或函數對象與匹配器掛鉤。這樣,每當uint_p識別出一個有效數值時,函數f將會被調用,而且被匹配的文本的範圍會做爲參數傳遞個f。通常狀況下,f會首先將匹配到的文本進行某種形式的轉換,在這裏則是轉化int整形的一個數字,接着幹什麼事情就有函數f決定了。
咱們將經過一些具體的例子來進一步瞭解chrysanthemum框架。請注意,下面的例子中大量使用了C++11的LAMBDA表達式。
在這個例子中咱們要定義一個IPV4地址的解析器,而且在解析的過程當中驗證IP地址的合法性。形式上,一個IP地址由‘.’分割的4個1-3位數字構成;邏輯上,IP地址的每一個小節都應該 大於等於0 且 小於等於255;解析出的4個數字咱們將放在 std::vector<std::size_t>中。下面是代碼: typedef std::string::iterator IT; //定義迭代器 rule<scanner_t,std::vector<std::size_t>,no_skip> ip_parser; //聲明規則,並指定CONTEXT爲td::vector<std::size_t> typedef scanner<IT,line_counter_scanner_policy> scanner_t; //定義掃描器 //定義規則,首先是1-3位數字:_repeat<1,3>(_digit()) //而後爲它嵌入一個語義動做,每當一個小節被匹配,語義動做被調用 //最後做爲一個總體 % '.',構成列表 ip_parser %= (_repeat<1,3>(_digit()) <= [&ip_parser](IT first,IT last){ std::size_t num = converter<std::size_t>::do_convert(first,last); //轉換 if(num < 0 || num > 255) return false; //驗證正確性 ip_parser.cur_ctx().push_back(num); //填充context return true; }) % '.'; std::string str; std::cout<<"please input ip address"<<std::endl; std::cin>>str; //構造掃描器,指定掃描的範圍 scanner_t scan(str.begin(),str.end()); //開始解析 if(ip_parser(scan) && scan.at_end()) { std::for_each(ip_parser.cur_ctx().begin(),ip_parser.cur_ctx().end(),[](std::size_t i){ std::cout<<i<<" "; }); std::cout<<"OK"<<std::endl; } else { std::cout<<"ERROR at:"<<scan.line_no<<" "<<scan.col_no<<std::endl; }
在這個例子中,咱們將解析一個遞歸定義的列表。列表的定義以下:
一個這種列表的例子是:
{ aaa , {bbb , ccc} , { ddd } ,eee }
實際上咱們能夠把rule的context理解爲被解析內容在內存中的表述形式,即數據結構,所以咱們先爲這種列表設計一個數據結構。能夠很容易的看出,實際上這種列表能夠表達爲一棵樹,書中有兩種節點,一種表明列表自己,一種表明小寫字符串。代碼以下:
enum NODE_TYPE { STRING_NODE = 0, LIST_NODE = 1, }; struct node { NODE_TYPE type; node() {} node(NODE_TYPE t):type(t) {} virtual ~node() {} }; struct list_node:public node { list_node():node(LIST_NODE) {} std::vector<node*> nodes_; void add_child(node* p) { nodes_.push_back(p); } virtual ~list_node() { std::for_each(nodes_.begin(),nodes_.end(),[](node* p){delete p;} ); } }; struct string_node:public node { string_node():node(STRING_NODE) {} virtual ~string_node() {} template <typename IT> void assign(IT first,IT last) { str.assign(first,last); } std::string str; };
前面咱們講到,實際上context在rule內部被維護成了一個棧,每當開始一次匹配的時候,規則會自動新建一個棧,做爲當前的上下文,這樣作的緣由在於列表是遞歸定義的。在列表解析器執行的過程當中,有可能遞歸的調用其自身,所以須要像函數同樣,每次調用過程都新開闢一個上下文。
訪問規則當前的上下文使用cur_ctx()函數。cur_ctx()函數返回當前上下文的引用。
有壓棧必然會有退棧,那麼規則在何時退棧呢?有兩種狀況:
1 當前上下文解析完成後,咱們須要回到上次解析的上下文中,並獲取這次解析的結果,此時須要調用函數pop_ctx(),pop_ctx()將返回當前的上下文,並退棧。 2 當前上下文解析失敗後,規則會自動退棧。
在第一種狀況下,什麼時候退棧由程序決定,第二種狀況下,是自動退棧。
同時,規則提供2個函數,on_init和on_error,用於指定回調函數,這些回調函數會在壓棧和解析失敗退棧時被調用,其參數爲當前的上下文的引用。咱們能夠利用他們作一些上下文的初始化和回收的工做。
總結規則中context的相關行爲以下:
如下是解析器部分的代碼:
//定義迭代器和掃描器的類型 typedef std::string::iterator IT; typedef scanner<IT,line_counter_scanner_policy> scanner_t; //這裏咱們定義一個語法類,並將全部須要用到的解析器所有定義在這個語法類裏面。 struct grammer { //這裏咱們將兩個解析期的第三個參數指定爲_space,所以,它們將自動忽略掉某個元素兩端多餘的空格。 //注意:爲了多態的特性,這裏的context都被指定爲指針。 rule<scanner_t,list_node*,_space> list;//列表的規則 rule<scanner_t,string_node*,_space> str;//字符串的規則 grammer() { //定義on_init 和 on_error回調函數。 list.on_init([](list_node*& p){p=new list_node();}); list.on_error([](list_node*& p){delete p;}); //一個列表以「{」開始,以「}」結束。 //列表中包含以以逗號分開的多個元素,每一個元素是一個字符串或者列表。 list %= '{' & ( str <= [=](IT first,IT last) { //規則str解析成功後,退棧並將結果合併到當前list的上下文中。 list.cur_ctx()->add_child(str.pop_ctx()); return true; } | list <= [=](IT first,IT last) { //規則list解析成功後,首先退棧,而後將結果合併到以前解析的上下文中 auto p = list.pop_ctx(); list.cur_ctx()->add_child(p); return true; } ) % ',' & '}'; //定義on_init 和 on_error回調函數。 str.on_init([](string_node*& p){p=new string_node();}); str.on_error([](string_node*& p){delete p;}); //字符串的規則 str %= +_lower() <= [=](IT first,IT last) { str.cur_ctx()->assign(first,last); return true; }; } //使用語法類。 grammer g; std::string str = "{ aaa , bbb ,{ccc}, {{ddd},eee} } "; std::cout<<str<<std::endl; scanner_t scan(str.begin(),str.end()); if(g.obj(scan)) { std::cout<<"OK"<<std::endl; auto p = g.obj.pop_ctx(); delete p; } else { std::cout<<"FAILED"<<std::endl; }
經過觀察語義動做和分析解析的過程,能夠看出,整個樹形結構(更通常的情形下,稱之爲抽象語法樹ADT)在遞歸降低的解析過程當中自動構建了起來。
在這個例子中,咱們將實現一個簡單的計算器,只支持正整數的加減乘除。 這樣的一個例子包括:
20+ 2 * 3 + 6/2/3 +1000
首先,咱們能夠肯定這樣的計算表達式的EBNF範式(EBNF範式的肯定過程不在本文的討論範圍內,EBNF和ABNF範式基本相似):
factor –> number expr –> term { + term } | term { – term } term –> factor { * factor } | factor { / factor }
而後來考慮將上面的ebnf範式使用chrysanthemum框架翻譯出來,並將計算的過程糅合進解析的過程當中:
struct grammer { typedef std::string::iterator IT; typedef scanner<IT,line_counter_scanner_policy> scanner_t; rule<scanner_t,int,_space> integer; //對應上面的factor rule<scanner_t,int,_space> term; //對應上面的term rule<scanner_t,int,_space> expression;//對應上面的expression grammer() { //對應於上面的factor –> number integer %= (+_digit()) <= [&](IT first,IT last){ integer.cur_ctx() = converter<int>::do_convert(first,last); return true;}; //對應於 expr –> term { + term } // | term { – term } term %= integer <= [&](IT first,IT last){ term.cur_ctx() = integer.pop_ctx(); return true;} //提取左操做數,首先保存在term的上下文中 & *( ('*' & integer ) <= [&](IT first,IT last){ term.cur_ctx() *= integer.pop_ctx(); return true; } //提取右操做數,並計算乘法,結果保存於 term 的上下文中 | ('/' & integer ) <= [&](IT first,IT last){ term.cur_ctx() /= integer.pop_ctx(); return true; } //提取右操做數,並計算除法,結果保存於 term 的上下文中 ); //對應於上面的 expr –> term { + term } // | term { – term } expression %= term <= [&](IT first,IT last){ expression.cur_ctx() = term.pop_ctx(); return true; } //提取左操做數,首先保存在expression的上下文中 & *( ('+' & term ) <= [&](IT first,IT last){ expression.cur_ctx() += term.pop_ctx(); return true; } //提取右操做數,並計算加法,結果保存於 expression 的上下文中 | ('-' & term ) <= [&](IT first,IT last){ expression.cur_ctx() -= term.pop_ctx(); return true; } //提取右操做數,並計算減法,結果保存於 expression 的上下文中 ) ; } int excute(std::string& str) { scanner_t scan(str.begin(),str.end()); if(expression(scan) && scan.at_end()) { std::cout<<"result:"<<expression.pop_ctx()<<std::endl; } else { std::cout<<"syntax error:("<<scan.line_no<<","<<scan.col_no<<")"<<std::endl; } } }; int main() { grammer g; std::string str = " 20+ 2 * 3 + 6/2/3 +1000 "; g.excute(str); str = " 20 +2 +3"; g.excute(str); str = " 20*2*3 "; g.excute(str); str = " 20+ 2 * 3a + 6/2/3 +1000 "; g.excute(str); }
細節請參考 exmaple/json.cc