從詞法分析到正規表示式(1)

漆楚衡發表於2015-03-27

從詞法分析到正規表示式(1)

本文所有程式碼在vs2013下通過編譯

從詞法分析到正規表示式(2)

上週二的上機編譯原理上機課上老師要求我們自己找一些編譯原理原始碼並熟悉之,我當時找了這份,大概閱讀了一下,發現其實挺簡單的,主要就是分類到具體的模式上,而模式是寫死的。

當時我也開始自己敲一個詞法分析器,從上向下地實現,沒寫多少。

不過一直想著要自己實現一下,隨後在週六開始實現,到昨天,歷時六天,一步步演化,終於形成了一個簡單的正規表示式匹配器。

這個實現完全是從具體應用逐步抽象出來的,因為能力有限,停留在了深度優先搜尋,沒有抽象到自動機的程度。

偶然間聯絡到了《SICP》的4.3節,就仿製其中的思想實現了最終的版本。

一開始時沒有太多的想法,也就是想自己再實現一遍別人的程式碼。

一個總介面是這個樣子的

bool tokenAnalysis(Token &token, Code &code) {
    return
        try_blank(token, code)            ||
        try_comment(token, code)        ||
        try_identifier(token, code)        ||
        try_operator(token, code)        ||
        try_bounder(token, code)        ||
        try_numerical(token, code)        ||
        try_char_literal(token, code)    ||
        try_string_literal(token, code)    ||
        dummyAnalysiser(token, code);
}

其中的各個try函式內部就是具體實現了。

比如對於行//註釋,我們要識別//作為開始,再讀取,直到'\n'

引入正則

各個詞法單元都是用文法定義的,而且還是正則的,那麼何不採用正則的概念來實現。

於是開始實現三個正則的操作star(*), connect, either(|)

這是三個閉合在正則下的操作,它們收到正則的模式做為引數,再返回一個正則模式。總結說,它們是組合手段

接下來還有非組合手段,也就是基本操作

基本操作本質上只需要一個謂詞:is_char,用於判斷當前字元是不是某個字元。(當時是這麼想的,後來發現還要一個epsilon

這裡,要注意C++裡函式不是一級公民,也沒有閉包,所以上述的四個“函式”操作的物件都要通過仿函式來實現(後來發現因為用的都是指標,過載()反而不方便,成員函式就好)。

一套基本介面

class Pattern {
public:
    virtual bool operator () (std::string &readed, Code &code) = 0;
    virtual ~Pattern() { }

    Pattern *star();
    Pattern *either(Pattern *another);
    Pattern *connect(Pattern *another);
};

typedef Pattern *PatPtr;

extern const PatPtr epsilon;

//basic compound function

PatPtr star(PatPtr);

PatPtr either(PatPtr pat1, PatPtr pat2);

PatPtr connect(PatPtr pat1, PatPtr pat2);

三個組合手段都分別有獨立函式和成員函式形式,當時沒多想,可以這麼寫就這麼寫了。

可以看到幾個函式用的都是指標。一開始時也不是這樣的,好久沒用C++了,沒有反應過來C++跟C#還有Java不一樣,C++的物件可以在棧上,而函式傳參時,一是值拷貝,二是Pattern是抽象父類,子類物件要傳遞給父類在空間分配上會有問題。

組合操作

star提供的組合方法就是將被star物件的模式成員儘可能多地重複。這裡其實沒有正確實現,考慮正規表示式a*a,因為這個演算法不檢查star的每一種可能,不會正確匹配。不過對於C的簡單詞法,這樣就夠了。

class Star : public Pattern {
    const shared_ptr<Pattern> pat_ptr;
public:
    Star(PatPtr pat_ptr) : pat_ptr(pat_ptr) { }

    bool operator () (string &readed, Code &code) {
        string rd = "";
        while ((*pat_ptr)(rd, code)) {
            ;
        }
        readed.append(rd);
        return true;
    }
};

PatPtr lexical::star(PatPtr pat) { return new Star(pat); }

either物件持有兩個pattern,當地一個失敗時嘗試第二個。當第二個失敗時返回失敗。

connect物件持有兩個pattern,當第一個成功時嘗試第二個。任何一個失敗都導致either失敗。

基本謂詞

基本模式謂詞有is_char,還有一些擴充:is_not_char,one_of(string target_set),not_one_of

這三者雖然被實現為基本模式,不過可以看成是is_char的“語法糖”。比如

is_not_char(c) ==> either of everyone in alphabet except c

這些除了epsilon,所有基本模式都是面向單個字元的,所以抽成兩部分。

template<typename Pred> // Pred take outter information
class CharAnalysiser : public Pattern {
    Pred predicate;
public :
    CharAnalysiser(Pred predicate) : predicate(predicate) { }
    bool operator () (string &readed, Code &code) {
        char first = code.next_char();
        if (predicate(first)) {
            readed.push_back(first);
            return true;
        }
        code.put_back(first);
        return false;
    }
};

struct IsCharPred {
    const char c;
public :
    IsCharPred(const char &c) : c(c) { }
    bool operator () (char first) {
        return c == first;
    }
};

PatPtr lexical::is_char(const char &c) {
    return new CharAnalysiser<IsCharPred>(IsCharPred(c));

這樣,可以通過正則思想來指定識別模式的一套函式介面就寫好了,給幾個例子

識別字元字面量

const PatPtr pat_ptr_char_literal =
    connect
    (
        is_char('\''),
        connect
        (
            either
            (
                is_not_char('\\'),
                connect
                (
                    is_char('\\'),
                    not_one_of("")    //is_any_char
                )
            ),
            is_char('\'')
        )
    );

bool lexical::try_char_literal(Token &token, Code &code) {
    token.kind = CHAR;
    token.word = "";

    return (*pat_ptr_char_literal)(token.word, code);
}

識別整數和浮點字面量

PatPtr make_pat_ptr_numerical() {
    PatPtr number = one_of(numbers)->connect(one_of(numbers)->star());
    PatPtr float_suffix = is_char('.')->connect(number->star());

    return number->connect(float_suffix->either(lexical::epsilon));
}

const PatPtr numerical = make_pat_ptr_numerical();

bool lexical::try_numerical(Token &token, Code &code) {
    token.kind = NUMERICAL;
    token.word = "";

    return (*numerical)(token.word, code);
}

總結

相對於最開始的c語言詞法分析程式

  • 優點:這套程式抽象了識別的具體內容,化如何做為做什麼,相對改進了編寫難度,降低了出錯的可能性。
  • 缺點:
    1. 執行效率應該有很大損失
    2. 當我得意洋洋地把上面的字元字面量的程式給同學看的時候,她沒認出來這是C++……

針對缺點2,想到不如提供一個更徹底的介面,我開始試圖實現正規表示式

相關文章