從詞法分析到正規表示式(2)
從詞法分析到正規表示式(2)
本文所有程式碼在vs2013下通過編譯
花了兩天完成模式匹配器,雖然過程坎坷,不過當時自信心膨脹,覺得自己可以搞定一切,而自己所寫的程式在自己看來也是挺厲害的……
自然而然地,就想到能不能把程式的介面再升級一下,畢竟模式就是通過正規表示式的手段構建的,再寫一個函式將正規表示式翻譯成模式應該不難,所以就著手去做了。
首先設計所用的正規表示式,除了基本的三個操作star(*), connect(隱含), either(|)
,還需要萬用字元_
,和轉義字元\
(轉義字和母語C++的轉義字相同簡直是失敗的不可救藥……)。
三個組合手段之間有優先順序
* > connect > |
為了改變組合的優先順序,括號()
也是不可或缺的。
正則直譯器
一開始的正規表示式的解釋函式reg_exp
其實相當簡單,有一個儲存模式的棧,順序逐個讀取字元
- 是
_
就呼叫is_any_char
,is_any_char
很好實現not_one_of("")
- 是
*
就從模式棧中取出一個模式,用star
修飾後放回去 - 是
|
- 遞迴呼叫
reg_exp
,得到right
- 將整個棧用
connect
連線起來,得到left
(最初居然沒注意到要把整個棧串起來) either(left, right)
入棧
- 遞迴呼叫
- 是
(
就遞迴呼叫reg_exp
,結果入棧 - 是
)
就把棧用connect
串起來,返回 - 是
\
就再讀一個字元,入棧 - 其它,入棧
其實可以寫成迭代形式的,稍微想了一下,這個其實跟《資料結構》棧那一章所實現的計算器很像,應該可以用同樣的方法去除遞迴。不過真動手的時候,瞬間迷茫,於是作罷。
缺陷
函式寫好以後就迫不及待地去寫了這種語言的第一個表示式,最簡單的那種,字元字面量
…… ……
寫不出來,因為不支援否定。
於是如果要求除了\
之外的字元,就要用|
把整個字母表除\
的字串起來……
在上面的直譯器里加入否定:
- 是
!
,再讀一個字元,用is_not_char
修飾,入棧
經過不斷的失敗,特別是認識到和母語用同一個轉義字的愚蠢後,我終於寫出了能執行的字元字面量表示式
'(!\\|\\_)'
實際在c++裡面是
"'(!\\\\|\\\\_)'"
接下來把它擴充一下就可以用到字串字面量上了……不對,又寫不出來了……
這次還是否定,因為在字串字面量裡要同時否定兩個字元\
和"
,!
不夠用啊
在直譯器的否定里加入:
- 如果
!
後是(
,則讀取到)
,對讀取的串用not_one_of
修飾,入棧
實際上沒有上面這步,因為整個過程中充滿了BUG,又穿插著上課,所以思維不太連貫,當時因為某個現在想不起來的BUG徹底絕望了,放棄了繼續打補丁。最終決定重寫整個正規表示式解析模組。
正則計算器
首先要搞清楚一個概念性的問題,整個表示式的特殊符號分成三類
- 轉義字
- 擴充符號(“語法糖”)
- 正則運算子
它們在不同的時候被處理
- 轉義字 在讀取原始表示式串時就被處理了,不進入後續計算
- 擴充符號 表示了原始表示式中的一個特殊子串,內部格式自定,這個子串在解析時和普通字元一樣得出一個模式
- 正則運算子 運算子操作的物件是正規表示式(得出的模式)
另一個棘手的問題是connect
,因為它是隱含的,不在原始表示式裡,但是如果希望用和計算器一樣的演算法去計算它,就要把它加上。
綜上,我對原始表示式增加了一個預處理步驟,將正規表示式自身的單元分割工作和後來的語義識別分開。
預處理的輸出是list<RegToken>
,RegToken
即帶型別的單元串。
順帶地,預處理還根據相鄰Token的型別在list
中插入connect
的Token。
list<RegToken> preprocess(const string &primary) { //break to tokens and add connect
list<RegToken> acc;
string::const_iterator iter = primary.begin();
string::const_iterator end = primary.end();
while (iter != end) {
acc.push_back(read_and_tag(iter));
}
list<RegToken>::iterator fst = acc.begin(), snd = acc.begin(); ++snd;
for (; snd != acc.end(); fst = snd, ++snd) {
if (should_insert_connect(fst->second, snd->second)) {
acc.insert(snd, RegToken("", Op_conn));
}
}
return acc;
}
接下來是從list<RegToken>
到模式的組合過程,跟中綴表示式的計算如出一輒
void zip(stack<Pattern> &operands, stack<RegTag> &ops, RegTag op_now) {
while (ops.size() > 0 && (op_cmp(ops.top(), op_now) > 0)) {
bool test = (op_cmp(ops.top(), op_now) > 0);
switch (ops.top()) {
case Op_star :
zip_star(operands);
break;
case Op_or :
zip_or(operands);
break;
case Op_conn :
zip_conn(operands);
break;
default :
throw "zip : op error";
break;
}
ops.pop();
}
if (op_now == Op_right) {
assert(ops.top() == Op_left);
ops.pop();
}
else {
ops.push(op_now);
}
}
Pattern calculator(list<RegToken>::const_iterator begin, const list<RegToken>::const_iterator end) {
stack<Pattern> operands;
stack<RegTag> ops;
while (begin != end) {
if (begin->second == Clause) {
operands.push(cal_clause(begin->first));
}
else {
zip(operands, ops, begin->second);
}
++begin;
}
assert(operands.size() == 1 && ops.size() == 0);
return operands.top();
}
於是,只剩下將普通字元和擴充格式對映到對應得模式構造器上了。
來自《SICP》的啟示
在某節課的走神時,突然《SICP》 4.3節那個神奇的語言模型進入腦海,當時瞬間大腦充血,滿腦子都是實現這個模型的好處(非確定性計算,之前star的問題直接不存在)。
不過下課坐在電腦前才發現自己對於如何實現完全沒有一點概念,探索了一天,一籌莫展。
第二天的上課例行走神,思路又神奇的回到了這個問題,居然有了思路(只是其中的一點點,畢竟C++不是Scheme)
現在想想,當時完全是大腦充血,加上對自己在幹什麼不是很清楚。
事後說起來其實相當簡單,就是想把遞迴轉成迭代。
經歷了很長時間的思考與失敗,終於形成了以下的模型
新的Pattern形式
class AbstractPatternClass {
public :
virtual Context exec(Cursor &code, std::shared_ptr<AbstractPatternClass> succ, Context fail) = 0;
};
typedef std::shared_ptr<AbstractPatternClass> Pattern;
Context
,上下文類有兩類子類,
- 一類是特殊狀態標識
- 一類用於執行時(類比為一組暫存器),儲存
一個
Cursor
一個Current
模式(類比於PC暫存器) 一個succ
模式 一個fail
上下文
succ和fail代表了計算的兩個走向
- 如果當前計算成功,則繼續執行succ
- 如果當前計算失敗,則執行fail
注意到fail是一個上下文,不需要當前上下文給出執行的資訊,而succ是一個模式,它的執行需要指定一些額外的資訊以形成自己的上下文。
整理一下思路
初始(待匹配)模式現在被視為一種不變的結構,即代表了一個正規表示式的機器。
我們通過給初始模式三個引數(資源遊標cursor
,成功指示符succ
,失敗指示符fail
)以啟動機器
機器的一個執行時狀態由一個上下文表示,而執行本身就是上下文的轉換
當一次計算順利進行後,執行的下一步就是由succ指定。
而當計算失敗,我們在fail裡儲存了之前某些需要做出選擇的地方的其他選擇,所以我們跳過去(替換上下文),繼續尋找希望。
succ和fail各自有一個終點,也就是在最初啟動時給出的那兩個。
只要我們給出字串是有限長的而機器又不會不正常地改變遊標,機器就總會執行到兩個終點中的一個。(終於找到了希望 VS 徹底沒希望了)
基本的模式
template<typename Predicate>
struct CharAtom : public AbstractPatternClass {
private :
Predicate pred;
public :
CharAtom(Predicate pred) : pred(pred) { }
Context exec(cursor_succ_fail) {
if (!cursor.is_end() && pred(cursor.get_char())) {
cursor.step_fore();
return make_run_ctx(cursor, succ, get_epsilon(), fail);
}
else {
return fail;
}
}
};
struct CharIsPredicate {
private :
const char target;
public :
CharIsPredicate(const char &target) : target(target) { }
bool operator () (const char &c) { return c == target; }
};
struct EpsilonAtom : public AbstractPatternClass {
public :
Context exec(cursor_succ_fail) { return make_run_ctx(cursor, succ, undefined_pattern, fail); }
};
因為抵達基本元素時,如果成功,當前模式被消耗掉,則只剩下succ了,則用epsilon做為其後繼(佔位)。
而epsilon將undefined_pattern(它會直接返回一個特殊上下文)作為填充,希望永遠不會執行至此。(只要初始succ不會返回一個執行狀態的上下文)
組合模式
struct StarPatternClass : public AbstractPatternClass {
private :
Pattern pattern;
public :
StarPatternClass(Pattern pattern) : pattern(pattern) { }
Context exec(cursor_succ_fail) {
// try from the most longest star-pattern
Context directly_go_to_succ = make_run_ctx(cursor, get_epsilon(), succ, fail);
return make_run_ctx(cursor, pattern, connect(star(pattern), succ), directly_go_to_succ);
}
};
組合模式是真正表現Context跳轉的地方,star模式會首先將匹配擴充到最大,並在這一過程中擴充失敗鏈(棧),如果後繼匹配發生失敗,則會(通過返回fail)退回到短一次的匹配成功處繼續嘗試後繼。
通過star其實就可以看出來整個匹配模型其實只是某種形式的遞迴轉迭代+棧(失敗鏈)。也就是說跟之前的版本沒有本質差別。
領悟到這一點的時候還是有些失望。
不過下面的函式還是讓我有些激動
bool run_loop(Context ctx) {
while (ctx->get_state() == ctx->Running) {
ctx = run_ctx_step_next(ctx);
}
assert(ctx->get_state() != ctx->Undefined);
return ctx->get_state() == ctx->HaltHapply;
}
這就是這一整套機制的表層
bool RegExp::Machine::match_all(const string &source) const {
struct CursorEndPredicate : public CursorPredicate {
public:
bool operator () (Cursor cursor) { return cursor.is_end(); }
};
Context beginning = make_run_ctx(Cursor(source), pattern, get_terminate(new CursorEndPredicate), get_halt_sadly());
return run_loop(beginning);
}
記一些教訓
- 雖然都大三了,還是不會很有效率的Debug,記得最開始時的問題是檔案讀取失敗導致的,我花了很久在其它部分
- 在第一個版本里對於程式碼資源我用的Code(本身儲存程式碼,要求所有改動它的函式引用傳遞),隨後一個Bug是一個函式用了值傳遞……
- 想當然,在第一個版本要組合一些符號串時我用了for_each,當時想自己寫一個通用的累積器,要用到二元函式,然後把binary_function拿來當父類,用ptr_fun轉換函式引數,編譯時發現沒有過載
operator ()
,其實問題很明顯了,不過我一直沒有意識到,最後耐下心來讀msdn才發現binary_function本身確實沒有過載operator ()
,辜負了我的滿心信任……
學習編譯原理後的反思
*4/10/2015 8:21:28 AM *
現在編譯原理課程也在上,也在看《自制程式語言》(兩者的側重點相當不一樣)。
逐漸對自己的這個小作品有了更清晰的認識。
我所用的模式樹
看來就是語法分析樹
。
而最後的正規表示式解析算是自底向上分析
了。
而匹配是直接在分析樹
上完成的,應該算是一個解釋型
的語言了。
值得反思的地方
不論是分析樹
還是自底向上分析
都是上下文無關語言
的範疇。也就是說我用大炮解決了可以用蒼蠅拍解決的問題。
這個作品完全是我在對相關內容沒有深刻了解的情況下開始的,雖然自己在這個過程中收穫了不少,不過還是要說時間花得有點不值。而且在我理解了自己所做的到底在什麼層次的時候,感覺這種閉門造車容易對自己的能力產生偏差,自我感覺過好……
相關文章
- 從詞法分析到正規表示式(1)詞法分析
- JS正規表示式從入門到入土(5)—— 量詞JS
- 正規表示式從入門到入坑
- 正規表示式關鍵詞解析
- 正規表示式語法
- JavaScript正規表示式(2)JavaScript
- oracle 正規表示式2Oracle
- 正規表示式語法(轉)
- Python語法進階(2)- 正規表示式Python
- JS正規表示式從入門到入土(2)—— 元字元和字元類JS字元
- 深入理解正規表示式:從入門到精通
- 正規表示式案例分析 (二)
- 正規表示式案例分析 (一)
- 正規表示式的基本語法
- 正規表示式語法介紹
- 正規表示式教程——語法篇
- 通過js正規表示式例項學習正規表示式基本語法JS
- 深入正規表示式(3):正規表示式工作引擎流程分析與原理釋義
- 《前端竹節》(2)【正規表示式】前端
- 正規表示式查詢相似單詞的方法
- java 正規表示式語法學習Java
- 正規表示式
- JS正規表示式從入門到入土(10)—— 字串物件方法JS字串物件
- python筆記(2) 正規表示式Python筆記
- JavaScript中的正規表示式(2) (轉)JavaScript
- 正規表示式學習(2)---字元特性字元
- 【正規表示式】常用的正規表示式(數字,漢字,字串,金額等的正規表示式)字串
- 從0到1打造正規表示式執行引擎(一)
- js正規表示式基本語法學習JS
- Java正規表示式的語法與示例Java
- OC 正規表示式的語法及使用
- 從 Vue parseHTML 來學習正規表示式VueHTML
- 【JavaScript】正規表示式JavaScript
- php –正規表示式PHP
- 正規表示式 教程
- 正規表示式 split()
- java正規表示式Java
- PHP正規表示式PHP