Boost.Spirit能使我們輕松地編寫出一個簡單腳本的語法解析器,它巧妙利用了元編程並重載了大量的C++操作符使得我們能夠在C++里直接使用類似EBNF的語法構造出一個完整的語法解析器(同時也把C++弄得面目全非-_-)。
關於EBNF的內容大家可以到網上或書店里找:
EBNF基本形式<符號> ::= <表達式> 或 <符號> = <表達式>
表達式里常用的操作符有:
- | 分隔符,表示由它分隔的某一個子表達式都可供選擇
- * 重復,和正則表達式里的*類似,表示它之前的子表達式可重復多次
- - 排除,不允許出現跟在它后面的那個子表達式
- , 串接,連接左右子表達式
- ; 終止符,一條規則定義結束
- '' 字符串
- "" 字符串
- (...) 分組,就是平時括號的功能啦,改變優先級用的。
- (*...*) 注釋
- [...] 可選,綜括號內的子表達式允許出現或不出現
- {...} 重復,大括號內的子表達式可以多次出現
- ?...? 特殊字符,由ISO定義的一些特殊字例如:
只允許賦值的簡單編程語言可以用 EBNF 定義為:
- (* a simple program in EBNF ? Wikipedia *)
- program = 'PROGRAM' , white space , identifier , white space ,
- 'BEGIN' , white space ,
- { assignment , ";" , white space } ,
- 'END.' ;
- identifier = alphabetic character , [ { alphabetic character | digit } ] ;
- number = [ "-" ] , digit , [ { digit } ] ;
- string = '"' , { all characters ? '"' } , '"' ;
- assignment = identifier , ":=" , ( number | identifier | string ) ;
- alphabetic character = "A"|"B"|"C"|"D"|"E"|"F"|"G"|"H"|"I"|"J"|"K"|"L"|"M"|"N"|"O"|"P"|"Q"|"R"|"S"|"T"|"U"|"V"|"W"|"X"|"Y"|"Z" ;
- digit = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9" ;
- white space = ? white space characters ? ;
- all characters = ? all visible characters ? ;
一個語法上正確的程序:
- PROGRAM DEMO1
- BEGIN
- A0:=3;
- B:=45;
- H:=-100023;
- C:=A;
- D123:=B34A;
- BABOON:=GIRAFFE;
- TEXT:="Hello world!";
- END.
這個語言可以輕易的擴展上控制流,算術表達式和輸入/輸出指令。就可以開發出一個小的、可用的編程語言了。
由於C++語法規則的限制,Spirit改變了EBNF中的一部分操作符的使用方式,如:
- 星號重復符(*)由原來的后置改為前置
- 逗號串接符(,)由>>或&&代替
- 中括號可選功能([表達式])改為(!表達式)
- 大括號重復功能({表達式})由重復符(*表達式)替代
- 取消注釋功能
- 取消特殊字符功能
- 同時Spirit又提供了大量的預置解析器加強了它的表達能力,因此可以把Spirit的語法看成是一種EBNF的變種。
版本1.6.x之前的spirit能支持大部分的編譯器。在1.8.0之后,由於spirit加入了很多C++的新特性,使兼容各種不標准的編譯器的工作變得非常郁悶,於是Spirit不再支持不標准的C++編譯器,這意味着VC7.1,BCB2006以及GCC3.1之前版本將不再被支持。(注:據說江湖上有新版Spirit的牛人修改版,可以工作在VC6和VC7上,具體情況不明)
入門
頭文件:
#include <boost/spirit.hpp>
例一,解析一個浮點數
首先,要弄一個關於浮點數的EBNF規則
假設我們的浮點數形式是: [±]xxxx[.xxxx][Ex],其中正負號可有可無,后面的冪可有可無,允許不帶小數點
則對應的EBNF規則是:
digit = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9";
real = ["+"|"-"], digit, [{digit}], [".", digit, [{digit}]], ["E"|"e", ["+"|"-"], digit, {digit}]
那么對應在Spirit里的是什么樣的呢?
- !(ch_p('+')|ch_p('-'))>>+digit_p>>! (ch_p('.')>>+digit_p)>>
- !((ch_p('e')|ch_p('E')) >> !(ch_p('+')|ch_p('-'))>>+digit_p)
在Spirit中,用於匹配表達式的對象叫解析器,如這里的ch_p, digit_p以及由它們和操作符組成的整個或部分都可以稱為解析器。
- !符號代表其后的表達式是可選的,它代替了EBNF里的中括號功能。
- ch_p()是一個Spirit預置的解析器生成函數,這個解析器用於匹配單個字符。
- >>用於代替逗號順序連接后面的解析器
- +符號代表1次或多次重復
- digit_p也是一個Spirit預置的解析器,它匹配數字字符。
這樣,再看上面就好理解了:可選的+-號,接着是數字,再跟着是可選的小數點和數字,最后是可選的E跟一個可接+-號的數字
現在,把這個式子寫到代碼里:
- #include <iostream>
- #include <boost/spirit.hpp>
- using namespace std;
- using namespace boost::spirit;
- int main()
- {
- parse_info<> r = parse("-12.33E-10",
- !(ch_p('+')|ch_p('-'))>>+digit_p>>
- !(ch_p('.')>>+digit_p)>>
- !((ch_p('e')|ch_p('E')) >>
- !(ch_p('+')|ch_p('-'))>>+digit_p)
- );
- cout << "parsed " << (r.full?"successful":"failed") << endl;
- return 0;
- }
這就是Spirit,這個變種的EBNF語法直接就寫在C++代碼里就可以了,實際上它們是由一系列的簡單解析器對象通過重載操作符后組合而成的復雜解析器。
解析器重載的操作符也可以幫我們自動作一些轉換工作,如上面的式子中ch_p('+')|ch_p('-')就可以改成ch_p('+')|'-',只要左邊或右邊的數值其中之一是解析器,它就能自動和另一邊的數值組合。
簡化后如下:
- !(ch_p('+')|'-')>>+digit_p>>!('.'>>+digit_p)>>!((ch_p('e')|'E') >> !(ch_p('+')|'-')>>+digit_p)
parse函數調用解析器來解析指定的字符串,它的原型是:
- parse_info<charT const*> parse(字符串, 解析器);
- parse_info<charT const*> parse(字符串, 解析器1, 解析器2);
第二個版本中的解析器2指出解析時可以忽略的一些字符,比如語句中的空格之類的。
另外,parse還有迭代器的版本
- parse_info parse(IteratorT first, IteratorT last, 解析器);
- parse_info parse(IteratorT first, IteratorT last, 解析器1, 解析器2);
IteratorT可以是任何迭代器類包括字符串指針,前面的這個兩個版本其實只是簡單地包裝了一下這兩個函數。
返回的parse_info類(其中的IteratorT模板默認為char const*)包含了解析結果信息,里面的成員有:
- IteratorT stop; //最后解析的位置
- bool hit; //是否與整個解析器匹配
- bool full; //是否與整個字符串匹配
- std::size_t length; //解析器解析了多少個字符,注意,first+length不一定與stop相同
其實,Spirit已經幫我們准備好了很多解析器,比如上面我們寫得要死的浮點數匹配,只要一個real_p就行了(冷靜,冷靜,上面的一長串到后面還是會用到的)
- parse_info<> r = parse("-12.33E-10",real_p);
Spirit預置的一些原始解析器,它們的名字都是以"xxxx_p"的形式出現。
字符解析器
- ch_p('X') 返回單字符解析器
- range_p('a','z') 返回一個字符范圍解析器,本例中匹配'a'..'z'
- str_p("Hello World") 返回一個字符串解析器
- chseq_p("ABCDEFG") 返回一個字符序列解析器,它可以匹配"ABCDEFG","A B C D E F G","AB CD EFG"等
- anychar_p 匹配任何字符(包括'\0')
- alnum_p 匹配A-Z,a-z,0-9
- alpha_p 匹配字母
- blank_p 匹配空格和TAB
- cntrl_p 匹配控制字符
- digit_p 匹配數字字符
- graph_p 匹配可顯示字符(除空格,回車,TAB等)
- lower_p 匹配小寫字符
- print_p 匹配可打印字符
- punct_p 匹配標點符號
- space_p 匹配空格,回車,換行,TAB
- upper_p 匹配大寫字符
- xdigit_p 匹配十六進制數字符串
- eol_p 匹配行尾
- nothing_p 不匹配任何字符,總是返回Fail(不匹配)
- end_p 匹配結尾
字符解析器支持的操作符
- ~a 排除操作,如~ch_p('x')表示排除'x'字符
- a|b 二選一操作,或稱為聯合,匹配a or b
- a&b 交集,同時匹配a和b
- a-b 差,匹配a但不匹配b
- a^b 異或,匹配a 或 匹配b,但不能兩者同時匹配
- a>>b 序列連接,按順序先匹配a,接下來的字符再匹配b
- a&&b 同上(象C語言一樣,有短路效果,若a不匹配,則b不會被執行)
- a||b 連續或,按順序先匹配a,接下來的字符匹配b(象C語言一樣,有短路效果,若a已匹配,則b不會被執行)
- *a 匹配0次或多次
- +a 匹配1次或多次
- !a 可選,匹配0或1次
- a%b 列表,匹配a b a b a b a...,效果與 a >> *(b >> a)相同
整數解析器 Spirit給我們准備了兩個整數解析器類,對應於有符號數和無符號數int_parser和uint_parser
它們都是模板類,定義如下:
- template <
- typename T = int,
- int Radix = 10,
- unsigned MinDigits = 1,
- int MaxDigits = -1>
- struct int_parser;
- template <
- typename T = unsigned,
- int Radix = 10,
- unsigned MinDigits = 1,
- int MaxDigits = -1>
- struct uint_parser;
模板參數用法:
- T為數字類型
- Radix為進制形式
- MinDigits為最小長度
- MaxDigits為最大長度,如果是-1表示不限制
比如下面這個例子可以匹配象 1,234,567,890 這種形式的數字
- uint_parser<unsigned, 10, 1, 3> uint3_p; // 1..3 digits
- uint_parser<unsigned, 10, 3, 3> uint3_3_p; // exactly 3 digits
- ts_num_p = (uint3_p >> *(',' >> uint3_3_p)); // our thousand separated number parser
Spirit已預置的幾個int_parser/uint_parser的特化版本:
- int_p int_parser<int, 10, 1, -1> const
- bin_p uint_parser<unsigned, 2, 1, -1> const
- oct_p uint_parser<unsigned, 8, 1, -1> const
- uint_p uint_parser<unsigned, 10, 1, -1> const
- hex_p uint_parser<unsigned, 16, 1, -1> const
實數解析器Spirit當然也會給我們准備實數解析器,定義如下:
- template<
- typename T = double,
- typename RealPoliciesT = ureal_parser_policies >
- struct real_parser;
模板參數用法:
- T表示實數類型
- RealRoliciesT是一個策略類,目前不用深究,只要知道它決定了實數解析器的行為就行了。
已預置的實數解析器的特化版本:
- ureal_p real_parser<double, ureal_parser_policies<double=""> > const
- real_p real_parser<double, real_parser_policies<double=""> > const
- strict_ureal_p real_parser<double, strict_ureal_parser_policies<double=""> > const
- strict_real_p real_parser<double, strict_real_parser_policies<double=""> > const
real_p前面實例里已經見過,ureal_p是它的unsigned版本。strict_*則更嚴格地匹配實數(它不匹配整數)
字符串形式為"real,real,real,...real"
參考上面的一堆預置解析器,我們可以這樣組合:
- real_p >> *(',' >> real_p);
- real_p%','
- {
- //用於解析的字符串
- const char *szNumberList = "12.4,1000,-1928,33,30";
- parse_info<> r = parse( szNumberList, real_p % ',' );
- cout << "parsed " << (r.full?"successful":"failed") << endl;
- cout << szNumberList << endl;
- //使用parse_info::stop確定最后解析的位置便於查錯
- cout << string(r.stop - szNumberList, ' ') << '^' << endl;
- }
對於real_p,它要求形式為:void func(double v)的 函數或函數對象,下面我們就來取出這些數字:
- #include <iostream>
- #include <boost/spirit.hpp>
- using namespace std;
- using namespace boost::spirit;
- //定義函數作為解析器的Actor
- void showreal(double v)
- {
- cout << v << endl;
- }
- int main()
- {
- //用於解析的字符串
- const char *szNumberList = "12.4,1000,-1928,33,30";
- //加入函數
- parse_info<> r = parse( szNumberList, real_p[&showreal] % ',' );
- cout << "parsed " << (r.full?"successful":"failed") << endl;
- cout << szNumberList << endl;
- //使用parse_info::stop確定最后解析的位置便於查錯
- cout << string(r.stop - szNumberList, ' ') << '^' << endl;
- return 0;
- }
再寫一個函數對象版本的,這次把這列數字寫到 vector里
- #include <iostream>
- #include <vector>
- #include <boost/spirit.hpp>
- using namespace std;
- using namespace boost::spirit;
- int main()
- {
- // pushreal函數對象,把數字放入vector中
- struct pushreal
- {
- void operator()(double v) const
- {
- m_vec.push_back(v);
- }
- pushreal(vector<double> &vec)
- :m_vec(vec){;}
- private:
- vector<double> &m_vec;
- };
- vector<double> reallist;
- //用於解析的字符串
- const char *szNumberList = "12.4,1000,-1928,33,30";
- //這次用pushreal對象作為Actor
- parse_info<> r = parse( szNumberList, real_p[pushreal(reallist)] % ',' );
- cout << "parsed " << (r.full?"successful":"failed") << endl;
- cout << szNumberList << endl;
- //使用parse_info::stop確定最后解析的位置便於查錯
- cout << string(r.stop - szNumberList, ' ') << '^' << endl;
- //顯示結果
- copy(reallist.begin(),reallist.end(),ostream_iterator<double>(cout," "));
- return 0;
- }
- #include <iostream>
- #include <vector>
- #include <boost/spirit.hpp>
- using namespace std;
- using namespace boost::spirit;
- int main()
- {
- vector<double> reallist;
- //用於解析的字符串
- const char *szNumberList = "12.4,1000,-1928,33,30";
- //使用自帶的push_back_a
- parse_info<> r = parse( szNumberList, real_p[push_back_a(reallist)] % ',' );
- cout << "parsed " << (r.full?"successful":"failed") << endl;
- cout << szNumberList << endl;
- //使用parse_info::stop確定最后解析的位置便於查錯
- cout << string(r.stop - szNumberList, ' ') << '^' << endl;
- //顯示結果
- copy(reallist.begin(),reallist.end(),ostream_iterator<double>(cout," "));
- return 0;
- }
- parse_info<charT const*> parse(字符串, 解析器1, 解析器2);
- //或
- parse_info parse(IteratorT first, IteratorT last, 解析器1, 解析器2);
- parse_info<> r = parse( szNumberList,
- real_p[push_back_a(reallist)] % ',',
- space_p);
- parse_info<> r = parse( szNumberList,
- *real_p[push_back_a(reallist)],
- space_p|ch_p(','));
注:這里的 ref是外部數據,就象上例中的reallist, value_ref是外部數值, value是解析出的數值
- increment_a(ref) 自增 ++ref
- decrement_a(ref) 自減 --ref
- assign_a(ref) 賦值 ref = value
- assign_a(ref, value_ref) 常量賦值 ref = value_ref
- push_back_a(ref) ref.push_back(value)
- push_back_a(ref, value_ref) ref.push_back(value_ref)
- push_front_a(ref) ref.push_front(value)
- push_front_a(ref, value_ref) ref.push_front(value_ref)
- clear_a(ref) ref.clear()
- insert_key_a(ref, value_ref) ref.insert(vt(value, value_ref))
- insert_at_a(ref, key_ref_, value_ref) ref.insert(vt(key_ref,value_ref))
- insert_at_a(ref, key_ref) ref.insert(vt(key_ref,value))
- assign_key_a(ref, value_ref) ref[value] = value_ref
- erase_a(ref) ref.erase(ref,value)
- erase_a(ref, key_ref) ref.erase(ref,key_ref)
- swap_a(aref, bref) 交換aref和bref
例三,四則運算
如果說上面的兩個例子用正則表達式也能輕松搞定了話,那么接下來你就能體會到Spirit的強大威力!
解析四則運算表達式,同樣先要把EBNF規則寫出來:
- //實數或者是括號包圍的子表達式
- 因子 = 實數 | '(' , 表達式 , ')';
- //因子*因子或因子/因子,可連續乘除也可只是一個因子
- 乘除計算 = 因子,{('*',因子)|('/',因子)};
- //加減計算,與上面類似
- 表達式 = 乘除計算,{('+',乘除計算)|('-',乘除計算)};
這個定義已經隱含了優先級:
- 要計算表達式(加減計算),必然要先計算乘除計算;
- 要計算乘除計算,就要先計算因子;
- 要計算因子,要么得到一個數字,要么就要計算括號內的子表達式。
轉成Spirit解析器組合:
- rule<phrase_scanner_t> factor, term, exp;
- factor = real_p | ('(' >> exp >> ')');
- term = factor >> *(('*' >> factor) | ('/' >> factor));
- exp = term >> *(('+' >> term) | ('-' >> term));
這里的rule是一個規則類,它可以作為所有解析器的占位符,定義如下:
- template<
- typename ScannerT = scanner<>,
- typename ContextT = parser_context<>,
- typename TagT = parser_address_tag>
- class rule;
其中的模板參數作用是:
ScannerT 掃描器策略類
它有兩類工作模式,一種是字符模式,一種是語法模式,默認的scanner<>是工作於字符模式的。
ContextT 內容策略類
它決定了rule里的成員變量以及Actor的類型,稍后會有利用這個模板參數來加入自定義的成員變量的例子
TagT 標識策略類
每個rule都有一個id()方法,用於識別不同的rule,TagT就用於決定id()返回的數據(后面會講到)。
這三個策略類可以不按順序地輸入,如
- rule<parser_address_tag,parser_context<>,scanner<> >;
- rule<parser_context<> >;
- rule<scanner<>,parser_address_tag >;
是同一個類。
值得注意的是ScannerT,我們上面沒有使用默認的scanner<>,而是使用了phrase_scanner_t,因為工作於字符模式的掃描器無法與parse的解析器2參數(跳過匹配字符,見上)一同工作,這樣就無法解析含有空格的表達式,這可不完美,所以我們使用的工作於語法模式的phrase_scanner_t。
- #include <iostream>
- #include <vector>
- #include <boost/spirit.hpp>
- using namespace std;
- using namespace boost::spirit;
- int main()
- {
- rule<phrase_scanner_t> factor, term, exp;
- factor = real_p | ('(' >> exp >> ')');
- term = factor >> *(('*' >> factor) | ('/' >> factor));
- exp = term >> *(('+' >> term) | ('-' >> term));
- const char *szExp = "1 + (2 * (3 / (4 + 5)))";
- parse_info<> r = parse( szExp , exp, space_p);
- cout << "parsed " << (r.full?"successful":"failed") << endl;
- return 0;
- }
接下來,要得到這個四則表達式的計算結果,這才是我們要的,於是Spirit自帶的lambda支持:phoenix登場!
頭文件:
#include <boost/spirit/phoenix.hpp>
phoenix提供和與Boost.Lambda類似的功能,它可以直接就地生成匿名函數對象,phoenix使用arg1,arg2,arg3...作為占位符,Boost.Lambda則使用_1,_2,_3...,使用舉例:
- #include <iostream>
- #include <vector>
- #include <boost/spirit.hpp>
- #include <boost/spirit/phoenix.hpp>
- using namespace std;
- using namespace boost::spirit;
- using namespace phoenix;
- int main()
- {
- vector<int> vec(10);
- int i=0;
- //arg1 = var(i)++ 把i++賦值給vec里各單元
- for_each(vec.begin(),vec.end(),arg1 = var(i)++);
- //cout<<arg1<<endl 把vec各單元輸出至cout
- for_each(vec.begin(),vec.end(),cout << arg1 << endl);
- return 0;
- }
這樣我們就可以利用phoenix提供的匿名函數對象作為Actor, 同時利用Spirit提供的closure類為rule添加一個val成員變量存儲計算結果(還記得rule的ContextT策略嗎?)
- #include <iostream>
- #include <vector>
- #include <boost/spirit.hpp>
- #include <boost/spirit/phoenix.hpp>
- using namespace std;
- using namespace boost::spirit;
- using namespace phoenix;
- int main()
- {
- //為rule准備一個val變量,類型為double
- //准確地說:是一個phoenix類(這里的member1),它和其它phoenix類組成lambda表達式,在lambda中可以把它看成是一個double。
- struct calc_closure : boost::spirit::closure<calc_closure, double>
- {
- member1 val;
- };
- //定義ContextT策略為calc_closure::context_t
- rule<phrase_scanner_t, calc_closure::context_t> factor, term, exp;
- //直接使用phoenix的lambda表達式作為Actor
- factor = real_p[factor.val = arg1] | ('(' >> exp[factor.val = arg1] >> ')');
- term = factor[term.val = arg1] >> *(('*' >> factor[term.val *= arg1]) | ('/' >> factor[term.val /= arg1]));
- exp = term[exp.val = arg1] >> *(('+' >> term[exp.val += arg1]) | ('-' >> term[exp.val -= arg1]));
- const char *szExp = "1 + (2 * (3 / (4 + 5)))";
- double result;
- parse_info<> r = parse( szExp , exp[assign_a(result)], space_p);
- cout << szExp;
- if(r.full)
- {
- //成功,得到結果
- cout << " = " << result << endl;
- }
- else
- {
- //失敗,顯示錯誤位置
- cout << endl << string(r.stop - szExp, ' ') << '^' << endl;
- }
- return 0;
- }
感到很神奇?這里有必要多說一下boost::spirit::closure的作用,它的使用方法是:
- struct name : spirit::closure<name, type1, type2, type3,... typen>
- {
- member1 m_name1;
- member2 m_name2;
- member3 m_name3;
- ...
- memberN m_nameN;
- };
一種類型對應一個member,使用name::context_t作為ContextT策略的rule就會含有N個相應的變量,而且這個rule的Actor將會接收到member1對應的數據。
也可以用於語法類,如grammar<t, name::context_t="">,關於語法類,后面章節將會提到。
注:默認最多到member3,要想使用更多數據,在包含Spirit頭文件前預定義PHOENIX_LIMIT和BOOST_SPIRIT_CLOSURE_LIMIT,如
- #define PHOENIX_LIMIT 10
- #define BOOST_SPIRIT_CLOSURE_LIMIT 10
有了上面的知識,再加上一些編程經驗,一個個搞定它們應該不是太難的事,但把所有的規則堆在一起不僅惡心,而且難以維護,於是Spirit提供了語法類 grammar來集中管理。
grammar的定義如下:
- template<
- typename DerivedT,
- typename ContextT = parser_context<> >
- struct grammar;
ContextT參數就是 內容策略類,在 例三中提到過。
編寫一個 語法類框架的基本形式如下:
- struct my_grammar : public grammar<my_grammar>
- {
- template <typename ScannerT>
- struct definition
- {
- rule r;
- definition(my_grammar const& self) { r = /*..define here..*/; }
- rule const& start() const { return r; }
- };
- };
這個類內部必須要有一個 definition類的定義,這個 definition類的模板參數ScannerT由框架使用環境決定。它由兩個重要方法:
- start() const函數:它返回一個rule。使用my_grammar解析時,就從這個rule開始。
- definition構造函數:這里是初始化rule的最好場所。它的self參數是整個my_grammar的實例引用,接下去你會發現這可是個很有用的東西。
下面,我們把例三中的四則運算解析功能放到一個語法類中,然后再用這個語法類與其它解析器合作弄一個簡單的賦值操作出來:
- #include <iostream>
- #include <boost/spirit.hpp>
- #include <boost/spirit/phoenix.hpp>
- using namespace std;
- using namespace boost::spirit;
- using namespace phoenix;
- //closure,為解析器提供存儲策略,見例三
- struct calc_closure : boost::spirit::closure<calc_closure, double>
- {
- member1 val;
- };
- //四則運算語法類,它也使用了closure的內容策略
- struct calculator : public grammar<calculator, calc_closure::context_t>
- {
- //語法類重要成員:struct definition
- template <typename ScannerT>
- struct definition
- {
- // factor, term, exp的rule類型,同例三(ScannerT模板在使用時決定)
- typedef rule<scannert, calc_closure::context_t> rule_type;
- rule_type factor, term, exp;
- // 啟動rule,在這個例子中,它也是遞歸的最頂層,負責把exp的最終結果賦值給框架本身。
- rule rlStart;
- const rule& start() const { return rlStart; }
- //definition的構造函數,self參數引用的是calculator類的實例
- definition(calculator const& self)
- {
- // 四則運算規則定義與例三相同
- factor = real_p[factor.val = arg1] |
- ('(' >> exp[factor.val = arg1] >> ')');
- term = factor[term.val = arg1] >>
- *(('*' >> factor[term.val *= arg1]) |
- ('/' >> factor[term.val /= arg1]));
- exp = term[exp.val = arg1] >>
- *(('+' >> term[exp.val += arg1]) |
- ('-' >> term[exp.val -= arg1]));
- //self.val=arg1也是phoenix的匿名函數:把exp的結果賦值給框架本身(self的作用)
- rlStart = exp[self.val = arg1];
- }
- };
- };
- int main()
- {
- string strVar; //變量名
- double result; //結果
- calculator calc;
- // 賦值語法:變量名 = 表達式
- rule<phrase_scanner_t> rlEqu = (+alpha_p)[assign(strVar)] >> '=' >> calc[assign_a(result)];
- const char *szEqu = "value = 1 + (2 * (3 / (4 + 5)))";
- parse_info<> r = parse( szEqu , rlEqu, space_p);
- if(r.full) //成功,得到結果
- cout << strVar << " = " << result << endl;
- else //失敗,顯示錯誤位置
- cout << endl << string(r.stop - szEqu, ' ') << '^' << endl;
- return 0;
- }
那么,還是先從規則動手。
這里我把變量名的規則放松了一點, 例四里變量名只能用字母,這里除了第一位是字母后面允許使用數字。於是變量名規則寫成(alpha_p >> *(alnum_p))
變量代表的是一個數值,它和實數應該屬於同一級別,所以我們把變量規則加入到factor規則里:
- factor = real_p[factor.val = arg1] |
- // 在表達式中使用變量
- (alpha_p >> *(alnum_p))[/*這里寫什么呢*/]|
- ('(' >> exp[factor.val = arg1] >> ')');
對了,我們只要把變量名和它的數值一一對應起來,那么這里只要把此變量名對應的數值送給factor.val就行了,標准庫里的 map在這里用是再適合不過了。
為了把變量和它的數值放到 map里,main里的rlEqu規則我們也要小改改:
- rule<phrase_scanner_t> rlEqu =
- ((alpha_p >> *(alnum_p))[assign(strVar)] >>
- '=' >> calc[assign_a(result)] ) [ insert_at_a(mapVar,strVar,result) ];
回到factor規則,我們試着把變量名規則的 Actor寫成[factor.val = getvalue(arg1, arg2)],注意所有字符串規則的 Actor都會有兩個參數,它們是兩個迭代器,分別指向起始位置和結束位置。所以這里使用了 phoenix的arg1和arg2占位符。
這個getvalue我們把它寫成一個函數,它從 map中取出變量名對應的數值。
- double getvalue(const char*first, const char*last)
- {
- return mapVar[string(first,last)];
- }
它的要求是這樣地:
1.先按如下形式做一個函數對象
- struct func_impl
- {
- //Param1等對就的是各個輸入參數的類型
- template<typename Param1,typename Param2,...,typename ParamN>
- struct result{
- //定義輸出參數的類型
- typedef returntype type;
- };
- //在這里該干啥干啥
- template<typename Param1,typename Param2,...,typename ParamN>
- returntype operator()(...)
- {
- ...
- }
- };
另外,也可以直接用phoenix::bind把簡單函數包裝起來使用,不過這樣雖然簡單很多,在我們這個例子中卻不便於封裝於是作罷(主要還是想秀一下)。
嗯,動手做吧:
- //適配phoenix的函數對象
- struct getvalue_impl
- {
- template <typename ParamA,typename ParamB> //輸入參數類型
- struct result{
- typedef double type; //返回類型
- };
- //函數主體,其實這里的ParamA和ParamB都是char*
- template <typename ParamA,typename ParamB>
- double operator()(ParamA const& start,ParamB const& end) const
- {
- //返回變量名對應的數值
- return m_mapVar[string(start,end)];
- }
- getvalue_impl(map<string,double
- :m_mapVar(mapVar){;}
- private:
- map<string,double
- };
- // phoenix表達式中能接受的仿函數類型
- const function<getvalue_impl> getValue = getvalue_impl();
- #include <iostream>
- #include <map>
- #include <boost/spirit.hpp>
- #include <boost/spirit/phoenix.hpp>
- #include <boost/spirit/actor.hpp> // insert_at_a需要
- using namespace std;
- using namespace boost::spirit;
- using namespace phoenix;
- struct calc_closure : boost::spirit::closure<calc_closure, double>
- {
- member1 val;
- };
- struct calculator : public grammar<calculator, calc_closure::context_t>
- {
- template <typename ScannerT>
- struct definition
- {
- typedef rule<scannert, calc_closure::context_t> rule_type;
- rule_type factor, term, exp;
- rule rlStart;
- const rule& start() const { return rlStart; }
- definition(calculator const& self)
- {
- factor = real_p[factor.val = arg1] |
- // 允許在表達式中使用變量,結果用calculator::m_getValue從map中取
- (alpha_p >> *(alnum_p))[ factor.val = self.m_getValue(arg1, arg2) ] |
- ('(' >> exp[factor.val = arg1] >> ')');
- term = factor[term.val = arg1] >>
- *(('*' >> factor[term.val *= arg1]) |
- ('/' >> factor[term.val /= arg1]));
- exp = term[exp.val = arg1] >>
- *(('+' >> term[exp.val += arg1]) |
- ('-' >> term[exp.val -= arg1]));
- rlStart = exp[self.val = arg1];
- }
- };
- calculator(map<string,double
- :m_getValue( getvalue_impl(mapVar) ) //初始化,把map傳給m_getValue
- {}
- //適配phoenix的函數對象
- struct getvalue_impl
- {
- template <typename ParamA,typename ParamB> //輸入參數類型
- struct result{
- typedef double type; //返回類型
- };
- //函數主體,其實這里的ParamA和ParamB都是char*
- template <typename ParamA,typename ParamB>
- double operator()(ParamA const& start,ParamB const& end) const
- {
- //返回變量名對應的數值
- return m_mapVar[string(start,end)];
- }
- getvalue_impl(map<string,double
- :m_mapVar(mapVar){;}
- private:
- map<string,double
- };
- // phoenix表達式中能接受的仿函數類型
- const function<getvalue_impl> m_getValue;
- };
- //用來顯示map中變量的值
- void showPair(const pair<string,< span="">double> &val)
- {
- cout << val.first << " = " << val.second << endl;
- }
- int main()
- {
- string strVar;
- double result;
- //用來保存變量和對應的數值
- map<string,double
- //把map傳給語法類,讓解析器知道變量的值
- calculator calc(mapVar);
- // 變量名規則(alpha_p >> +(alnum_p)),除第一位外后面可以跟數字。
- // 整個等式末尾加入insert_at_a的actor,匹配成功后把變量和數值存到map中。
- rule<phrase_scanner_t> rlEqu =
- (
- (alpha_p >> *(alnum_p))[assign(strVar)] >>
- '=' >> calc[assign_a(result)] ) [ insert_at_a(mapVar,strVar,result) ];
- // 多行賦值語句,表達式用使用變量
- const char *szEqus[3] = {
- "PI = 3.1415926",
- "Rad = PI*2.0/3.0",
- "Deg = Rad*180/PI"};
- // 逐句解析
- for(int i=0; i<3; i++) parse(szEqus[i], rlEqu, space_p);
- // 顯示每個變量的數值
- for_each(mapVar.begin(), mapVar.end(), showPair );
- return 0;
- }
試試把szEqus里的變量名中間加個空格,比如改成"R ad = P I*2.0/3.0",這樣的語句居然也能正確解析,這顯然不是我們想要的(要的就是這種效果?!!偶無語...)。
那么怎樣才能解析變量名時不許跳過空格,而解析語句的又允許跳過呢(搞雙重標准)?下面介紹的命令就可以幫上忙了,首先趕快在沒人發現這個錯誤之前把它搞定先:
把所有的 變量名規則(factor規則定義里有一個,rlEqu規則定義里有一個)用 lexeme_d包裹起來:
- lexeme_d[(alpha_p >> *(alnum_p))]
下面介紹各種預置命令 使用形式: 命令[解析器表達式]
lexeme_d
不跳過空白字符,當工作於語法級時,解析器會忽略空白字符,lexeme_d使其臨時工作於字符級
如整數定義應該是: integer = lexeme_d[ !(ch_p('+') | '-') >> +digit ];,這樣可以防止"1 2 345"被解析為"12345"
as_lower_d
忽略大小寫,解析器默認是大小寫敏感的,如果要解析象 PASCAL一樣的大小寫不敏感的語法,使用r = as_lower_d["begin"];(注,里面的參數都得小寫)
no_actions_d
停止觸發 Actor
longest_d
嘗試最長匹配
如number = integer | real;用它匹配123.456時,integer會匹配123直到遇到小數點結束,使用number=longest_d[integer | real];可以避免這個問題。
shortest_d
與 longest_d相反
limit_d
定義范圍,用法 limit_d(min, max)[expression]
如
- uint_parser<int, 10, 2, 2> uint2_p;
- r = lexeme_d
- [
- limit_d(0u, 23u)[uint2_p] >> ':' // Hours 00..23
- >> limit_d(0u, 59u)[uint2_p] >> ':' // Minutes 00..59
- >> limit_d(0u, 59u)[uint2_p] // Seconds 00..59
- ];
定義最小/最大值,用法: min_limit_d(min)[expression]
例七,牛叉型解析器 相對於 Spirit預置的一些 簡單解析器,它也提供了很多功能更強大的“牛叉型”解析器。現介紹如下:
f_ch_p
語法:f_ch_p(ChGenT chgen)
作用:和 ch_p類似,它解析的字符由chgen的返回值決定,chgen是一個類型為"CharT func()"的函數(或函數對象)
例如:char X(){return 'X';} f_ch_p(&X);
f_range_p
語法:f_range_p(ChGenAT first, ChGenBT last)
作用:和 range_p類似,它由first和last兩個函數(或函數對象)的返回值決定解析的字符范圍。
f_chseq_p
語法:f_chseq_p(IterGenAT first, IterGenBT last)
作用:和 chseq_p類似,同樣由first和last兩個函數(或函數對象)的返回值決定起始和終止迭代器。
f_str_p
語法:f_str_p(IterGenAT first, IterGenBT last)
作用:和 str_p類似,參數同 f_chseq_p
if_p
語法:if_p(condition)[then-parser].else_p[else-parser],其中.else_p可以不要
作用:如果condition成立,就使用then-parser,否則用else-parset
例如: if_p("0x")[hex_p] .else_p[uint_p]
for_p
語法:for_p(init, condition, step)[body-parser]
作用:init和step是一個無參數的函數或函數對象,各參數與for的作用類似(先init,再檢查condition,有效則執行body-parser及step,再檢查condition...)
例如: for_p(var(i)=0, var(i) < 10, ++var(i) ) [ int_p[var(sum) += arg1] ]
while_p, do_p
語法:while_p(condition)[body-parser] 及 do_p[body-parser].while_p(condition)
作用:條件循環,直接condition不成立為止。
select_p, select_fail_p
語法:select_p(parser_a , parser_b /* ... */, parser_n);
作用:從左到右接順序測試各解析器,並得到匹配的解析器的序號(0表示匹配parser_a,1匹配parser_b...)
例如:見 switch_p例
switch_p
語法:switch_p(value)[case_p<value_a>(parser_a),case_p<value_b>(parser_b),...,default_p(parser_def)]
作用:按value的值選擇 解析器
例如:下例中匹配的形式為:字符a后是整數,b后是個逗號,c后跟着"bcd",d后什么也沒有。
- int choice = -1;
- rule<> rule_select =
- select_fail_p('a', 'b', 'c', 'd')[assign_a(choice)]
- >> switch_p(var(choice))
- [
- case_p<0>(int_p),
- case_p<1>(ch_p(',')),
- case_p<2>(str_p("bcd")),
- default_p
- ];
語法:c_escape_ch_p
作用:和 ch_p類似,其牛叉的地方在於能 解析C語言里的轉義字符:\b, \t, , \f, , \\, \", \', \xHH, \OOO
例如:confix_p('"', * c_escape_ch_p, '"')
repeat_p
語法、作用:
repeat_p (n) [p] 重復n次執行解析器p
repeat_p (n1, n2) [p] 重復n1到n2次解析器p
repeat_p (n, more) [p] 至少重復n次解析
例如:檢驗是否是有效的文件名
- valid_fname_chars = /*..*/;
- filename = repeat_p(1, 255)[valid_fname_chars];
語法:confix_p(open,expr,close)
作用:解析獨立元素,如C語言里的字符串,注釋等,相當於open >> (expr - close) >> close
例如:解析C注釋 confix_p("/*", *anychar_p, "*/")
comment_p,comment_nest_p
語法:comment_p(open,close),如果close不指定,默認為回車
作用:confix_p的輔助解析器, comment_p遇到第一個close時即返回,而 comment_nest_p要open/close對匹配才返回。
例如:
- comment_p("//") C++風格注釋
- comment_nest_p('{', '}')|comment_nest_p("(*", "*)") pascal風格注釋
語法:list_p(paser,delimiter)
作用:匹配以delimiter作為分隔符的列表
regex_p
語法:regex_p("正則表達式")
作用:使用 正則表達式來匹配字符串(強強聯手啊~~啥也不說了)
symbols類
定義:
- template
- <
- typename T = int,
- typename CharT = char,
- typename SetT = impl::tst<t, chart>
- >
- class symbols;
- symbols<> sym;
- sym = "pineapple", "orange", "banana", "apple", "mango";
- sym.add("hello", 1)("crazy", 2)("world", 3);
例如:
- struct Show{
- void operator()( int n ) const
- {
- cout << n;
- }
- };
- symbols<> sym;
- sym.add("零",0) ("一",1) ("二",2) ("三",3) ("四",4) ("五",5) ("六",6) ("七",7) ("八",8) ("九",9);
- parse("二零零八",*(sym[Show()]));
作用:可以方便地用它來創建一個 解析器
例如:見下例
演示怎樣自己寫一個解析器,解析一個整數
- struct number_parser
- {
- typedef int result_t; //定義解析器結果類型
- //參數是:掃描器,結果
- template <typename ScannerT>
- std::ptrdiff_t operator()(ScannerT const& scan, result_t& result) const
- {
- if (scan.at_end()) //如果結果或出錯,返回-1
- return -1;
- char ch = *scan;
- if (ch < '0' || ch > '9')
- return -1;
- result = 0;
- std::ptrdiff_t len = 0;
- do //解析字符串,得到結果
- {
- result = result*10 + int(ch - '0');
- ++len;
- ++scan;
- } while (!scan.at_end() && (ch = *scan, ch >= '0' && ch <= '9'));
- return len; //返回解析的字符串長度
- }
- };
- //用functor_parser包裝成解析器
- functor_parser<number_parser> number_parser_p;
Spirit也支持生成 抽象語法樹的功能(不過用它來解析C++代碼可就不太合適了,Spirit針對的是輕量的小型腳本)
頭文件
#include <boost/spirit/include/classic_ast.hpp>
使用 AST和之前的解析步驟很相似,一個重要的區別是所有的子規則都應該是 字符串形式的,也就是說real_p,int_p之類的幫不上忙了,我們得自力更生。
我們在 例一中使用過的浮點數解析器這次可以派上用場了。
下面的例子參考了 例四中的解析器規則:
- #include <iostream>
- #include <boost/spirit.hpp>
- #include <boost/spirit/include/classic_ast.hpp>
- using namespace std;
- using namespace boost::spirit;
- struct calculator : public grammar
- {
- template <typename ScannerT>
- struct definition
- {
- typedef rule rule_type;
- rule_type factor, term, exp, str_real_p;
- const rule_type& start() const { return exp; }
- definition(calculator const& self)
- {
- str_real_p = leaf_node_d[
- !(ch_p('+')|'-')>>+digit_p>>
- !('.'>>+digit_p)>>!((ch_p('e')|'E') >>
- !(ch_p('+')|'-')>>+digit_p)
- ];
- factor = str_real_p | inner_node_d[('(' >> exp >> ')')];
- term = factor >> *((root_node_d[ch_p('*')] >> factor)
- | (root_node_d[ch_p('/')] >> factor));
- exp = term >> *((root_node_d[ch_p('+')] >> term)
- | (root_node_d[ch_p('-')] >> term));
- }
- };
- };
- //顯示AST的結構,Indent是縮進寬度
- typedef tree_match<char const*>::container_t container_t;
- void showTree(const container_t& con, int Indent)
- {
- for(container_t::const_iterator itr=con.begin(); itr!=con.end(); ++itr)
- {
- //tree_node: value, children
- //顯示當前值
- cout << string(Indent*4, ' ') << "|--(" <<
- string(itr->value.begin(), itr->value.end()) << ')' << endl;
- //顯示子節點
- showTree(itr->children, Indent+1);
- }
- }
- int main()
- {
- calculator calc;
- const char *szExq = "12 * (24 - 15) / (17 + 6)";
- tree_parse_info<> info = ast_parse(szExq, calc, space_p);
- showTree(info.trees, 0);
- return 0;
- }

這個代碼和之前的代碼主要區別是多了幾個 xxxx_node_d形式的命令,以及使用 ast_parse函數來解析。 tree_parse_info類型 ast_parse的參數與 parse相同,主要區別就在於它的返回值不是parse_info而是tree_parse_info。
tree_parse_info的成員有:
- IteratorT stop;
- bool match;
- bool full;
- std::size_t length;
- typename tree_match<IteratorT, NodeFactoryT, T>::container_t trees;
tree_node有兩個重要的成員:
- children: 子節點,與tree_parse_info里的trees類型相同:std::vector<tree_node<T>>(或std::list<...>)
- value: 數據,類型為模板T,這個參數默認類型是node_val_data<IteratorT, ValueT>
node_val_data<IteratorT, ValueT>的模板參數IteratorT默認是const char*, ValueT是nil_t(空數據,定義為 struct nil_t {};)。
在這個類內部維護着一個vector(或list),它保存着解析出來的腳本字符串,比如上面例子中的"12","*","24"等。node_val_data向外提供的重要方法有:
- begin()/end(): 直接返回內部vector(或list)的begin()和end()
- is_root()/is_root(bool): 取得/設置對應節點的root狀態(由root_node_d命令設置)
- value()/value(const ValueT&)取得/設置用戶自定義數值(默認的nil_t沒法帶數據,必須通過指定NodeFactoryT來改變ValueT類型,馬上會講到)
- id()/id(parser_id): 取得/設置解析此節點的解析器id(還記得rule的TagT策略嗎,下面還會講到)
- typedef node_val_data_factory<double> factory_t;
- my_grammar gram;
- my_skip_grammar skip;
- tree_parse_info<iterator_t, factory_t> i =
- ast_parse<factory_t>(first, last, gram, skip);
我們可以用其它參數代替它以實現更適用的標記, Spirit已准備好的TagT策略有:
parser_tag<N>,它接收一個整數,如
- rule<parser_tag > my_rule;
- assign(rule.id().to_long() == 123);
- rule<dynamic_parser_tag> my_dynrule;
- my_dynrule.set_id(1234); // set my_dynrule's id to 1234
下面介紹Spirit為AST而引入的幾個命令: leaf_node_d
由leaf_node_d命令包裹的規則將被視為一個整體,它還由另一個名字 token_node_d。
嘗試把上例中的leaf_node_d命令去掉,再看解析結果:所有的數字都被折成了一個個字節。
inner_node_d
這個命令會忽略第一個子規則和最后一個子規則,只取中間部分。
把上例中的inner_node_d去掉,那么括號也被參與解析。
root_node_d
這個命令對於 AST至關重要,由root_node_d命令包裹的節點將成為 同一規則中其它節點的父節點。它的工作方式如下:
假設A是前一節點 B是新產生的節點 如果B是根節點 A成為B的第一個子節點 否則,如果A是根節點而B不是,那么 B成為A的最后一個子節點 其它情況 A和B處於同一級
比如這個例子中的“12 * (24 - 15) / (17 + 6)”
對於解析器解析順序是:
exp = term term = 12{factor} *{root} (24 - 15){exp} /{root} (17 + 6){exp} ...
首先解析 12, 然后是 *, 這時發現 *是root,於是 12成為 *的第一個子節點
接着解析 (24 - 15)這個exp,同理, 24成為 -的第一個子節點,然后是 15,它不是root,而前一個是,於是 15成為 -的最后一個子節點。
因為 (24 - 15)這個exp不是root,同樣成為了 *的最后一個子節點。
再解析 /,是root, 於是把前一節點(是 *哦,因為其它的都成了 *的子節點)變成了它的首個子節點。
最后解析 (17+6)這個exp,最終成為了 /的最后一個子節點。
no_node_d
忽略由它包裹的規則,比如例子中的:
- factor = str_real_p | inner_node_d[('(' >> exp >> ')')];
- factor = str_real_p | (no_node_d[ch_p('(')] >> exp >> no_node_d[ch_p(')')]);
這個命令會刪除其規則匹配出的所有節點中偶數位置上的節點,比如:
- rule_t intlist = infix_node_d[ integer >> *(',' >> integer) ];
discard_first_node_d/discard_last_node_d
忽略第一個/最后一個子規則(半個 inner_node_d功能)
我們的Spirit學習先到這里,這些只不過是Spirit里的冰山一角,要發揮Spirit的強大威力,還得繼續前進...
在/libs/spirit/example里有不少“很好很強大”的例子,比如:小型的C語言解釋器,XML解釋器等,大家有興趣可以去研究研究。