使用 lex 和 yacc 編譯代碼,第 1 部分:介紹

來源:互聯網
上載者:User

大部分人永遠不需要知道 lex 和 yacc 可以做什麼。為了編譯下載的一些東西,您有時候會需要安裝它們,但是,在大部分情況下,其使用非常簡單。或許偶而有 README 檔案會提及“移位/歸約(shift/reduce)”衝突。無論如何,這些工具仍是 Unix 工具箱頗有價值的一部分,對它們稍做瞭解會大有協助。

實際上,儘管 lex 和 yacc 幾乎總是被同時提到,但是它們可以單獨使用。有很多非常有趣、完全用 lex 編寫的小程式(請參閱 參考資料 中的連結)。使用 yacc 而不使用 lex 的程式比較罕見。

在這兩篇文章的所有內容中,“lex”和“yacc”這兩個名字所代表的也包括這些工具的 GNU 版本 flex 和 bison。給出的代碼應該適用於所有主流版本,比如 MKS yacc。它完全是一個融洽的大家族!

這個系列有兩部分。第一篇文章將使用更為通用的術語介紹 lex 和 yacc,研究它們可以做什麼以及怎樣去做。第二篇文章展示了使用它們編譯的一個真實的應用程式。

 

lex 和 yacc 是一對配對工具。lex 將檔案分解為成組的“記號(tokens)”,大體上類似於單詞。yacc 接受成組的記號,並將它們裝配為高層次的結構,類似於句子。yacc 設計用來處理 lex 的輸出,不過您也可以編寫自己的代碼來完成此任務。同樣,lex 的輸出很大程度上設計用於為某類解析器提供資料。

它們用於讀取結構化格式較好的檔案。例如,可以使用 lex 和 yacc 讀取很多程式設計語言的代碼。同樣也可以使用它們讀取很多具有充分可預知格式的資料檔案。lex 和 yacc 可以用來解析相當簡單和規則的文法。自然語言超出了它們的範圍,但是大部分電腦程式設計語言在它們的範圍之內。

lex 和 yacc 是編譯器的工具。它們的輸出本身是代碼,需要提供給編譯器;通常,要增加另外的使用者代碼來使用 lex 和/或 yacc 產生的程式碼。一些簡單的程式可以不依賴於任何另外的代碼;在一些更大更複雜的程式中,解析器只是很小的一部分。

最好更詳細地研究每個程式。

詞彙分析器不是在科幻展示中可以看到的小發明。它是一個將輸入拆分為經過識別的片斷的程式。例如,一個簡單的詞彙分析器可能會為輸入的單詞進行計數。lex 可以接受規範檔案並構建一個相應的詞彙分析器(用 C 編寫的)。

瞭解它的最好方法或許是研究一個執行個體。下面是摘自 flex 的手冊的一個簡單的 lex 程式。

int num_lines = 0, num_chars = 0;
%%
/n      ++num_lines; ++num_chars;
.       ++num_chars;
%%
main() {
 yylex();
 printf("# of lines = %d, # of chars = %d/n", num_lines, num_chars);
}

這個程式有三個部分,用 %% 符號隔開。第一部分和最後一個部分是普通而古老的 C 代碼。中間是有趣的一部分。它由一系列規則構成,lex 將這些規則翻譯為詞彙分析器。每一個規則依次包含一個Regex以及該Regex得到匹配時要啟動並執行一些代碼。任何沒有得到匹配的文本則簡單地拷貝到標準輸出。所以,如果您的程式嘗試去解析一種語言,那麼重要的是要確保所有可能的輸入都能由 lexer 捕獲;否則,那些被遺漏的內容就會像訊息一樣顯示給使用者。

實際上,清單 1 中的代碼是一個完整的程式;如果您使用 lex 來運行它、編譯它並運行結果,它將去做看起來應該做的事情。

在這種情況下,很容易看到發生了什麼。換行永遠都會匹配第一個規則。任何其他字元都會匹配第二個規則。lex 依次嘗試每一個規則,儘可能地匹配最長的輸入資料流。如果有一些內容根本不匹配任何規則,那麼 lex 將只是把它拷貝到標準輸出;這種行為通常是不受歡迎的。一個簡單的解決方案是,在最後添加一個可以匹配任何內容的規則,這個規則既不做任何事情(如果您比較懶)也不發出任何類型的診斷資訊。注意,lex 優先選擇更長的匹配,即使它們更靠後。所以,假如有這些規則:

u { printf("You!/n"); }
uu { printf("W!/n"); }

並以“uuu”作為輸入,lex 將首先匹配第二個規則,處理完前兩個字母,然後匹配第一個規則。不過,如果有一些內容可以匹配兩個規則中的任意一個,那麼 lex 規範中的次序會決定使用哪一個規則。當一個規則不能被匹配時,有一些版本的 lex 會向您發出警告。

使得 lex 實用而且有趣的原因在於,它可以處理更加複雜的規則。例如,識別 C 標識符的規則可能是類似這樣:

[a-zA-Z_][0-9a-zA-Z_]* { return IDENTIFIER; }

所使用的文法是普通而古老的Regex文法。有一些擴充。其中一個擴充是,您可以為通用的模式命名。您可以在 lex 程式的第一部分中第一個 %% 之前為其中的一部分定義名稱:

DIGIT [0-9]
ALPHA [a-zA-Z]
ALNUM [0-9a-zA-Z]
IDENT [0-9a-zA-Z_]

然後,您可以在規則部分將其名稱放置在大括弧內來反向引用它們:

({ALPHA}|_){IDENT}* { return IDENTIFIER; }

每一個規則都有相應的當Regex得到匹配時去執行的代碼。代碼可以完成任何必需的處理,並且可以可選地返回一個值。如果有解析器要使用 lex 的輸出,那麼這個值將會用到。例如,在那個簡單的行計數程式的例子中,實際上不需要解析器。如果您要使用一個解析器來解釋某些語言中的代碼,那麼您應該向解析器返回一些內容來告知它您得到了什麼記號。您可以只是在一個共用的 include 檔案中使用一個 enum 或者一系列 #define 指令,或者您可以讓 yacc 為您產生預定義值的一個列表。

預設情況下,lex 會從標準輸入進行讀取。您可以非常簡單地將它指向另外一個檔案;從緩衝區中進行讀取稍微有些困難。對此沒有完全標準化的方法;最輕便的方法是開啟一個臨時檔案,將資料寫入到這個臨時檔案,然後將它交給 lexer。下面是完成此任務的部分程式碼範例:

int
doparse(char *s) {
        char buf[15];
        int fd;
        if (!s) {
                return 0;
        }
        strcpy(buf, "/tmp/lex.XXXXX");
        fd = mkstemp(buf);
        if (fd < 0) {
                fprintf(stderr, "couldn't create temporary file: %s/n",
                        strerror(errno));
                return 0;
        }
        unlink(buf);
        write(fd, s, strlen(s));
        lseek(fd, 0, SEEK_SET);
        yyin = fdopen(fd, "r");
        yylex();
 fclose(yyin);
}

此代碼小心地斷開與臨時檔案的連結(讓它仍是開啟的,但已經被刪除)來進行自動清空。更為細心的程式員,或者不是為只有有限空間用於範例程式碼的讀者編寫程式的程式員,可能會考慮增加使用者對 TMPDIR 的選擇。

這樣,您已經將輸入拆分為一連串的記號。現在您需要一些方法來識別高層次的模式。這就是 yacc 要做的:yacc 讓您可以描述希望怎樣處理記號。yacc 文法有幾分類似如下:

value:
    VARIABLE
  | NUMBER
expression:
    value '+' value
  | value '-' value

這意味著運算式可以是幾種格式中的任意一種;
例如,一個變數、一個加號或者一個數字都可以是一個運算式。管道字元( | )表明可供選擇。
lexer 產生的符號稱為 終結符(terminals)或者 記號(tokens)。
從它們裝配而來的內容稱為 非終結符(non-terminals)。
所以,在這個例子中, NUMBER 是一個終結符;lexer 正是產生這種結果。
相反, value 是一個非終結符,它是通過裝配終結符而建立出來的。

類似於 lex 檔案,yacc 檔案也是由使用 %% 標誌隔開的部分構成。與 lex 檔案一樣,yacc 檔案也由三部分構成(其中最後一個部分是可選的),其內容只是將要融入到產生的檔案中的普通的 C 代碼。

yacc 可以識別出記號的模式;例如,如上面例子中所示,它可以識別出一個運算式可能由一個值、一個加號或者減號以及另一個值構成。它還可以採取動作;當解析器達到運算式中的那個條件點時,封裝在 {} 中的代碼塊將會被執行。例如,有人可能會編寫:

expression:
value '+' value { printf("Matched a '+' expression./n"); }  

yacc 檔案的第一部分定義瞭解析器將要處理和產生的對象。在一些情況下,它可以是空的,但是更常見的是,它應該至少包含一些 %token 指令。這些指令用來定義 lexer 可以返回的記號。當使用 -d 選項來運行 yacc 時,它會產生一個定義常量的標頭檔。

({ALPHA}|_){IDENT}* { return IDENTIFIER; }

這樣,我們前面使用以上語句的例子可能會有相應的包含有下面一行的 yacc 文法:

%token IDENTIFIER

yacc 將建立包含有類似下面一行的一個標頭檔(預設名為 y.tab.h ):

#define IDENTIFIER 257

這些數字將會在可能的合法字元範圍之外;這樣,lexer 可以原樣返回單個字元本身,或者返回使用這些定義值的記號。當將代碼從一個系統移植到另一個系統時,這會產生問題:通常,您最好在新的平台上重新運行 lex 和 yacc,而不要移植產生的程式碼。

預設情況下,yacc 產生的解析器將首先嘗試去解析在檔案的規則部分得到的第一個規則的執行個體。您可以通過使用 %start 來指定一個不同的規則以改變這一行為,但是,將頂層的規則安排在此部分的頂部通常是最合理的。

 

接下來的問題是如何處理運算式的組成部分。通常解決此問題的方法是定義一個容納 yacc 將要處理的對象的資料類型。這個資料類型是一個 C union 對象,在 yacc 檔案的第一部分使用 %union 聲明來定義。定義了記號以後,可以為它們指定一個類型。例如,對於一個玩具級的程式設計語言來說,您可以像下面這樣做:

 %union {
 long value;
}
%token <value> NUMBER
%type <value> expression
 

這就表明,當解析器得到 lexer 返回的 NUMBER 記號時,它可以認為全域變數 yylval 的名為 value 的成員已經被賦與了有意義的值。當然,您的 lexer 必須要以某種方式來處理它:

[0-9]+ {
  yylval.value = strtol(yytext, 0, 10);
  return NUMBER;
 }

yacc 允許您通過符號名參考資料表達式的組成部分。當解析非終結符時,進入解析器的組成部分被命名為 $1 、 $2 ,依次類推;它將向高層解析器返回的值名為 $$ 。例如:

expression:NUMBER '+' NUMBER { $ = $1 + $3; }

注意,字面量加號是 $2 ,沒有有意義的值,但它仍然要佔一個位置。不需要指定一個“return”或者任何其他內容:只是分配一個奇妙的名稱 $$ 。 %type 聲明表明了 expression 非終結符還使用了 union 的 value 成員。

在一些情況下,在 %union 聲明中同時有多種類型的對象可能會有協助;就此而言,您必須確保在 %type 和 %token 中聲明的類型是您確實用到的那些。例如,如果您有一個既包括整型值又包括指標型值的 %union 聲明,而且您聲明了一個記號來使用指標類型的值,但是您的 lexer 卻賦與其整型的值……可能會發生不正常的事情。

當然,這不能解決一個最終問題:最前面的非終結符運算式沒有任何地方可以返回,所以您應該處理它產生的值。一種方法是,確保隨時完成所有需要做的工作;另一種方法是,構建一個單獨的大對象(比如說,一個連結起來的條目列表),並在第一個規則結束處的一個全域變數中為它分配一個指標。因而,例如,如果您要將上面的運算式解析器編譯為一個通用的計算機,那麼只需編寫解析器您就可以得到一個對錶達式進行非常仔細的解析的程式,然後就再也不需要和它們打交道了。在理論上這是非常有趣的,而且具有一定的藝術性,但是它並不特別實用。

這個基本的介紹將使您可以自己接著研究 lex 和 yacc;或許會使用它解析出的運算式構建一個 確實 可以做某些事情的簡單計算機。如果稍加研究,您將發現 lex 和 yacc 可以用於問題診斷。下一期,我們將研究問題診斷技術;另外,我們將構建一個可以完成真正的任務的更大更強有力的解析器。

 轉自:http://www.ibm.com/developerworks/cn/linux/l-lexyac.html

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.