1. 程式人生 > >編譯原理——基於LR(1)的語法檢查器(一)

編譯原理——基於LR(1)的語法檢查器(一)

前言

  該專案是我在學習編譯原理的時候所完成的一個專案,不同於成熟的yacc語法分析器,我的語法檢查器通過和一個詞法分析器相互配合,啟動之前讀入由BNF正規化和正則表示式所描述的文法與詞法,之後根據給定的文法對給出的程式碼檔案進行檢查,並指出檔案中的錯誤。整個文法的解析以LR(1)為基礎,總共的程式碼量為800左右(不包括詞法分析器),完整的程式碼請移步Configurable-syntactic-analyzer

正文

理論介紹

  一般說來,常用的語法分析技術有兩種,一種是自頂向下的,另一種是自底向上的,對於LL(1)型的文法來說,我們一般使用自頂向下的分析方式例如遞迴下降或者構造預測分析器的方式進行文法的解析,而其中遞迴下降分析法經常在手寫的語法分析器中用到,這種方式對於大部分比較簡單的文法是十分實用的,遞迴下降寫起來也十分的方便。雖然LL(1)的文法足以描述大部分程式設計的構造,但是卻無法描述左遞迴的文法以及二義性的文法,而在程式設計中,左遞迴的文法是經常出現的,例如S->Sab

這樣的產生式便是左遞迴的,因此遞迴下降的方式在此時並不適用,因此我們需要將左遞迴的文法經過某些變換,使它變為非遞迴的文法,但這種變換常常使得文法面目全非,而且實現起來也比較困難,因此我們提出了基於LR(k)的語法分析的概念。
  LR語法分析技術是通過自底向上的方式進行語法分析的,而我們引入這種方法的原因有以下幾點(摘自龍書):
- 對於幾乎所有的程式設計語言,只要能夠寫出該構造的上下文無關文法,就能夠構造出識別該構造的LR語法分析器。
- LR語法分析方法是已知的最通用的無回溯的移入-規約分析技術。
- 一個LR語法分析器可以在對輸入進行從左到右掃描時儘可能早的檢測到錯誤。
- 可以使用LR方法進行語法分析的文法類是可以使用預測方法或LL方法進行語法分析的文法類的真超集。

  由此可見,LR語法分析技術的能力是十分強大的,LR語法分析技術一般來說有三種,一種是SLR(簡單LR技術),LR(規範LR技術),LALR(向前看LR技術)。相較之下,規範LR技術的分析能力最強,但是佔用的系統資源也最多,鑑於規範LR的典型性,因此我的語法檢查器使用的規範LR技術。無論哪一種LR技術,在使用時都要經過如下的幾個步驟:
1. 根據給定的文法構造出該文法的規範LR(k)項集族
2. 根據規範LR(k)項集族構造出語法分析表。
3. 在執行時根據語法分析表進行相應的移入,規約操作。

  而本文則會針對以上三個步驟提供一個具體可行的實現方案,從而構造出一個簡單的語法檢查器。

配置檔案

  對於描述文法的配置檔案,我以龍書上的文法描述方式為基礎,在實際使用的時候做了一些修改,方便我對文法進行解析,首先文法中出現的所有終結符必須在文法之前註明,並且在這些終結符之前寫上統一的標識”%token”並在結尾寫上分號,所有的產生式需要用’ { ‘以及’ } ‘包裹起來。對於文法來說,文法應該寫成增廣文法的形式,即第一個產生式寫成S'->S的形式,其次產生式的箭頭”->”被修改為冒號’ : ‘,符號和或連線符’ | ‘之間無空格,最後在產生式的結尾處應加上分號’ ; ‘。因此,如下的文法格式:

S->L = R | R
L->* R | id
R->L

會被改寫為

%token = * id;
{
S':S;
S:L = R|R;
L:* R|id;
R:L;
}

  %token識別符號後邊的所有符號均應該在詞法分析器的配置檔案中使用正則表示式進行描述,例如上述的文法在配置檔案中就應該被描述為:

id:[_0-9a-zA-Z]+
*:\*
=:=

  由於我的詞法分析器具有優先匹配的功能,因此需要優先匹配的需要寫在前邊。關於詞法分析器的相關理論以及具體實現詳見正則引擎入門以及Configurable-lexical-analyzer

配置檔案的解析

  雖然規範LR技術是本文章的重點,但是在解析配置檔案的時候我依舊採用了遞迴下降的分析方法,這種方法寫起來比較方便,也便於理解。我們解析配置檔案的目的是要將文法讀取進記憶體,使用某種資料結構將文法中各個符號之間的關係展現出來,便於我們下一步的分析,下面展示的三個結構體可以用來表示一個文法的各個結構:

struct _production          //代表一個產生式
{
    char head[20];          //用於記錄產生式的頭
    int p_index;            //記錄產生式的序號
    struct _item *items;        //通過'|'進行分隔的產生式的不同產生體,以連結串列形式記錄。
    struct _production *next;   //下一個產生式
};

struct _item
{
    struct _element *ele;   //以連結串列形式記錄的產生式體中的不同符號
    struct _item *next; //下一個產生式的體
    int *body;          //以序號形式記錄的產生式的體
    int body_len;           //產生式體的長度
};

struct _element
{
    union
    {
        int t_index;            //終結符的序號
        struct _production *pro;//產生式
    }type;
    boolean is_terminator;  //是否為終結符
    char terminator_name[20];//終結符的字串
    struct _element *next;  //下一個符號
};

typedef struct _element element;
typedef struct _item item;
typedef struct _production production;

  在看主要的處理函式之前,我們先來看一下一些簡單的輔助函式以及全域性變數:

    //Recording the productions that are created.
production *pro_list = NULL;
    //Recording the elements that are created.
element *token_list = NULL;
    //The num of tokens and also the index of S'
int token_num = 0;
int transfer_index = 0;
char current_str[64];

int file_ptr = 0;
char *file_buff = NULL;

int advance();
void blank_skip();
void file_load(char *buff, FILE *fp);
void add_t(element *e);
void add_p(production *p);
int is_contain(char *key);
void* find_by_key(char *key);
int match(char *s);

  pro_list記錄了所有構造出來的產生式或者說所有的非終結符,token_list記錄了所有的終結符,token_num記錄終結符的數量,transfer_index記錄終結符和非終結符的數量,current_str記錄了掃描到的字串,file_buff作為檔案緩衝區,file_ptr記錄當前讀取的字元的陣列下標。file_load函式將整個配置檔案一次性地讀入file_buff中,方便我們接下來的分析,blank_skip函式跳過陣列當中所有的無意義的字元,例如空格,製表符等等,直到遇到一個有意義的字元。函式advance讀入合法的標誌符的字元,並將其放入陣列current_str中,直到遇到一個非法字元。函式add_t(add_p)element和(production)加入到token_list(pro_list)中,is_contain查詢token_listpro_list這兩個連結串列中是否有與字串key相同名字的符號,find_by_key將返回具有相應字串名字的符號的指標。match函式檢視file_ptr所指向的或者接下來的幾個字元是否與s相匹配。
  瞭解了所有的輔助性的要素之後我們來看一下主要的分析函式,首先是一個頂層的驅動函式,沒什麼可說的就是載入檔案之後按情況進行解析,邏輯比較簡單:

/*
    Load all the grammar info into a buff,then create the
    structs group to describe the grammar.
*/
void grammar_load(FILE *fp)
{
    file_buff = (char*)calloc(FILE_LOAD_MAX, sizeof(char));
    file_load(file_buff, fp);
    blank_skip();
    if (match("%token"))
        token_analyze();
    blank_skip();
    if (match("{"))
        pro_analyze();
    else
        exception("Illegal grammar form error", "grammar_load");
}

  接下來我們看一下處理終結符的函式,因為所有的終結符均需要在檔案開始處宣告一下。因此需要一個獨立的函式對這些終結符進行處理。同時在文法中有兩個比較特殊的終結符號,一個是空串的符號ε,它用在產生式體中,表示該產生式可能產生空字串,在我們的程式中,我們使用EMPTY來表示它。另一個符號是字串結束符號$,它並不會顯式地出現在文法中,只會出現在輸入串的結尾處,它表示輸入已經到達了終點,沒有更多的輸入了,在程式中我們使用STRING_END表示。ε和$的序號分別被定義為0和1,因此在處理其他的終結符之前要先將兩者加入到記錄終結符的連結串列中:

void token_analyze()
{
    blank_skip();
    element *e = NULL;
    /*
        Add STRING_END and EMPTY token first
    */
    e = (element*)calloc(1, sizeof(element));
    e->is_terminator = TRUE;
    e->type.t_index = transfer_index;
    strcpy(e->terminator_name, "EMPTY");
    add_t(e);

    e = (element*)calloc(1, sizeof(element));
    e->is_terminator = TRUE;
    e->type.t_index = transfer_index;
    strcpy(e->terminator_name, "STRING_END");
    add_t(e);

    while (advance())
    {
        e = (element*)calloc(1, sizeof(element));
        e->is_terminator = TRUE;
        //temp->next = NULL;
        e->type.t_index = transfer_index;
        strcpy(e->terminator_name, current_str);
        add_t(e);
        blank_skip();
    }
    if (!match(";"))
        exception("Illegal grammar form error", "token_analyze");
}

  接下來是處理產生式的函式,它會檢查現在讀取到的產生式的頭是否已經出現過並記錄在了連結串列中,如果沒有就初始化一個,之後再呼叫產生式體的處理函式,反之則把已經初始化的取出,再呼叫處理函式:

void pro_analyze()
{
    int contain;
    production *p = (production*)calloc(1, sizeof(production));
    production *temp_p = NULL;
    blank_skip();
    while (advance())
    {
        contain = is_contain(current_str);
        if (!contain)
        {
            strcpy(p->head, current_str);
            p->p_index = transfer_index;
            if (!match(":"))
                exception("Illegal grammar form error", "pro_analyze");
            add_p(p);
            p->items = items_analyze();
            p = (production*)calloc(1, sizeof(production));
        } else
        {
            temp_p = (production*)find_by_key(current_str);
            if (!match(":"))
                exception("Illegal grammar form error", "pro_analyze");

            temp_p->items = items_analyze();
        }
        blank_skip();
    }
    match("}");
}

  接下來是產生式體的處理函式,它初始化一個item,將具體的符號處理交給elements_analyze,只是簡單地記錄一下item->body的資訊:

item* items_analyze()
{
    item* first_item = (item*)calloc(1, sizeof(item));
    item* temp = first_item;
    while (TRUE)
    {
        temp->ele = elements_analyze();
        temp->body = (int*)calloc(BODY_LENGTH, sizeof(int));
        item_body_count(temp);
        if (match("|"))
        {
            temp->next = (item*)calloc(1, sizeof(item));
            temp = temp->next;
        } else if (match(";"))
            return first_item;
        else
            exception("Illegal grammar form error", "items_analyze");
    }
}

  接下來是產生式體中符號的處理函式,函式的邏輯也比較簡單,就是首先判斷讀取到的這個符號是否已經被記錄,如果是,那對終結符或者非終結符分別進行處理,如果不是,那這個符號只能是非終結符(因為規定所有的終結符必須事先宣告):

element* elements_analyze()
{
    element* first_element = NULL;
    element* temp_e = NULL;
    production* temp_p = NULL;
    int contain;
    while (advance())
    {
        contain = is_contain(current_str);
        if (contain == TOKEN)
        {
            if (first_element == NULL)
            {
                first_element = (element*)find_by_key(current_str);
                temp_e = first_element;
            } else
            {
                temp_e->next = (element*)find_by_key(current_str);
                temp_e = temp_e->next;
            }

        } else if (contain == PRODUCTION)
        {
            if (first_element == NULL)
            {
                first_element = (element*)calloc(1, sizeof(element));
                //first_element->is_terminator = FALSE;
                first_element->type.pro = (production*)find_by_key(current_str);
                temp_e = first_element;
            } else
            {
                temp_e->next = (element*)calloc(1, sizeof(element));
                //temp_e->next->is_terminator = FALSE;
                temp_e->next->type.pro = (production*)find_by_key(current_str);
                temp_e = temp_e->next;
            }
        } else
        {
            temp_p = (production*)calloc(1, sizeof(production));
            temp_p->p_index = transfer_index;
            strcpy(temp_p->head, current_str);
            add_p(temp_p);
            if (first_element == NULL)
            {
                first_element = (element*)calloc(1, sizeof(element));
                first_element->type.pro = temp_p;
                temp_e = first_element;
            } else
            {
                temp_e->next = (element*)calloc(1, sizeof(element));
                temp_e->next->type.pro = temp_p;
                temp_e = temp_e->next;
            }
        }
        blank_skip();
    }
    return first_element;
}

  至此,整個配置檔案便已經被處理完畢了,解析完成之後,連結串列pro_listtoken_list分別記錄了所有的非終結符與終結符,並且它們之間的指標指向與文法的描述是相一致的,這樣的處理是十分方便的,在接下來的文章中我們便會使用這些連結串列構建出LR(1)項集族以及語法分析表。