• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            woaidongmao

            文章均收錄自他人博客,但不喜標(biāo)題前加-[轉(zhuǎn)貼],因其丑陋,見(jiàn)諒!~
            隨筆 - 1469, 文章 - 0, 評(píng)論 - 661, 引用 - 0
            數(shù)據(jù)加載中……

            使用 yacc 和 lex 編寫(xiě)文本分析器

            關(guān)鍵字: RSCT 內(nèi)存調(diào)試 Expect 正則表達(dá)式 Tcl

                   UNIX&reg; 中,許多操作系統(tǒng)組成部分都依賴于文本分析,從用來(lái)與系統(tǒng)進(jìn)行交互的 shell,到諸如 awk or Perl 等各種常用的工具和命令,再到用來(lái)構(gòu)建軟件和應(yīng)用程序的 C 編譯器。您可以在 UNIX 應(yīng)用程序(以及其他的應(yīng)用程序)中使用分析器來(lái)構(gòu)建簡(jiǎn)單的配置分析器,甚至構(gòu)建最終的目標(biāo):您自己的編程語(yǔ)言。

              本文將研究使用 lex/flex yacc/bison 工具構(gòu)建分析器所需的步驟。首先構(gòu)建一個(gè)簡(jiǎn)單的計(jì)算器,然后深入地研究如何采用相同的原則進(jìn)行文本分析。分析文本,即理解和提取文本中的關(guān)鍵部分,是許多應(yīng)用程序中一個(gè)重要的部分。在 UNIX? 中,許多操作系統(tǒng)組成部分都依賴于文本分析,從用來(lái)與系統(tǒng)進(jìn)行交互的 shell,到諸如 awk or Perl 等各種常用的工具和命令,再到用來(lái)構(gòu)建軟件和應(yīng)用程序的 C 編譯器。您可以在 UNIX 應(yīng)用程序(以及其他的應(yīng)用程序)中使用分析器來(lái)構(gòu)建簡(jiǎn)單的配置分析器,甚至構(gòu)建最終的目標(biāo):您自己的編程語(yǔ)言。

              開(kāi)始之前

            UNIX? 程序員常常發(fā)現(xiàn)他們需要去理解文本和其他一些具有靈活的標(biāo)準(zhǔn)化格式的結(jié)構(gòu)。通過(guò)使用 lex yacc 工具,您可以構(gòu)建一個(gè)分析引擎,根據(jù)特定的規(guī)則來(lái)處理文本。然后,可以將它集成到您的應(yīng)用程序中以完成各項(xiàng)工作,從配置分析到構(gòu)建您自己的編程語(yǔ)言。在學(xué)習(xí)了本教程之后,您將了解如何定義詞法元素、編寫(xiě) yacc 規(guī)則,并使用相應(yīng)的規(guī)則機(jī)制來(lái)構(gòu)建和定義各種不同的分析引擎和應(yīng)用程序。

              關(guān)于本教程

              在 UNIX 中,有許多用來(lái)理解和提取文本的方法。您可以使用 grepawkPerl 和其他的解決方案。但有的時(shí)候,您需要理解和提取結(jié)構(gòu)化的但格式不受限制的數(shù)據(jù)。在這種情況下,UNIX lex yacc 工具就很有用處了。前面提到的那些工具,如 awkPerl 以及 shell 和許多其他的編程語(yǔ)言,都使用 lex yacc 來(lái)生成分析應(yīng)用程序以分析和理解文本,并將其轉(zhuǎn)換為所需的信息或數(shù)據(jù)結(jié)構(gòu)。

            Lex 是一種詞法分析工具,它可以用來(lái)從源文本識(shí)別特定結(jié)構(gòu)的文本字符串。Yacc 是一種語(yǔ)法分析器,它可以讀取文本并用來(lái)將單詞序列轉(zhuǎn)換為便于處理的結(jié)構(gòu)化的格式。

              在本教程中,首先您將研究如何使用 lex yacc 來(lái)構(gòu)建一個(gè)計(jì)算器。使用該計(jì)算器作為示例,您將進(jìn)一步研究 lex yacc 系統(tǒng)生成的輸出和信息,并學(xué)習(xí)如何使用它來(lái)分析其他類(lèi)型的信息。

              先決條件

              要使用在本教程中的示例,您需要使用到下列工具:

            Lex:這個(gè)工具是大多數(shù) UNIX 操作系統(tǒng)的標(biāo)準(zhǔn)組件。GNU flex 工具提供了相同的功能。

            Yacc:這個(gè)工具是大多數(shù) UNIX 操作系統(tǒng)的標(biāo)準(zhǔn)組件。GNU bison 工具提供了相同的功能。

            C 編譯器:任何標(biāo)準(zhǔn)的 C 編譯器都可以,其中包括 Gnu CC

            Make 工具:這個(gè)工具是使用示例 Makefile 來(lái)簡(jiǎn)化構(gòu)建過(guò)程所必需的。

              可以從 GNU Web 站點(diǎn)或本地的 GNU 鏡像站點(diǎn)下載 GNU 工具。

              使用 lex 進(jìn)行詞法分析

              編寫(xiě)文本分析器的第一步是要能夠識(shí)別所讀取的內(nèi)容。有許多不同的方法可以完成這項(xiàng)任務(wù),但是最簡(jiǎn)單的方法是使用 lex,它是將輸入信息轉(zhuǎn)換為一系列標(biāo)記的工具。

              什么是詞法分析?

              當(dāng)使用編程語(yǔ)言編寫(xiě)程序或在命令行中輸入命令時(shí),您是否想過(guò)究竟執(zhí)行了什么操作將您輸入的內(nèi)容轉(zhuǎn)換為一組指令呢?

              這個(gè)處理過(guò)程非常簡(jiǎn)單,卻又相當(dāng)復(fù)雜。它很復(fù)雜,這是因?yàn)閷?duì)于可能輸入的信息,表面上看起來(lái)似乎存在無(wú)限種可能的組合和序列。例如,要使用 Perl 語(yǔ)言遍歷一個(gè)哈希表,您可以使用如清單 1 所示的序列。

              清單 1. Perl 中遍歷一個(gè)哈希表

            foreach $key (keys %hash)
            {
            ...
            }

              其中的每一項(xiàng)都是有意義的,雖然方式有所不同,這正是該處理過(guò)程的簡(jiǎn)單明了之處。清單 1 中所示的表達(dá)式存在一個(gè)對(duì)應(yīng)的結(jié)構(gòu),也就是說(shuō),與人類(lèi)語(yǔ)言一樣,編程語(yǔ)言中也存在著特定的規(guī)則。因此,如果將輸入分解為您所看到的和該信息結(jié)構(gòu)的組合,那么對(duì)該內(nèi)容的分析過(guò)程則相當(dāng)簡(jiǎn)單。

              要理解提供給文本分析應(yīng)用程序的信息,通常有兩個(gè)階段。第一個(gè)階段是識(shí)別輸入的或提供給應(yīng)用程序的內(nèi)容是什么。您必須能夠從輸入源中識(shí)別關(guān)鍵字、短語(yǔ)或字符序列,以便能夠確定對(duì)其進(jìn)行何種處理。第二個(gè)處理階段是理解該信息的結(jié)構(gòu),即語(yǔ)法,以便對(duì)輸入進(jìn)行驗(yàn)證和操作。有關(guān)語(yǔ)法的一個(gè)很好的示例是,大多數(shù)編程語(yǔ)言中圓括號(hào)的使用。很明顯,下面的表達(dá)式是錯(cuò)誤的:

            { function)( {

              其中,大括號(hào)不匹配,而圓括號(hào)的出現(xiàn)順序錯(cuò)誤。為了讓分析器理解和識(shí)別表達(dá)式,那么分析器必須知道正確的序列,以及匹配該序列后應(yīng)該進(jìn)行何種操作。

              詞法分析首先進(jìn)行識(shí)別輸入數(shù)據(jù)的處理,并且可以使用 lex 工具來(lái)完成該處理過(guò)程。

            lex 工具

            lex 工具(或 GNU 工具 flex)使用一個(gè)配置文件來(lái)生成相應(yīng)的 C 源代碼,然后,可以用它來(lái)創(chuàng)建獨(dú)立的應(yīng)用程序,或者在您自己的應(yīng)用程序中使用它。配置文件定義了需要在待分析的文件中查找的字符序列,以及當(dāng)發(fā)現(xiàn)該序列之后應(yīng)該進(jìn)行什么操作。該文件的格式十分簡(jiǎn)單,即指定輸入序列和相應(yīng)的結(jié)果,用空格(或制表符)隔開(kāi)。例如:

            sequence do-something

              清單 2 顯示了一個(gè)非常簡(jiǎn)單的定義,它可以接受單詞并根據(jù)提供的單詞打印出一個(gè)字符串。

              清單 2. 簡(jiǎn)單的 lex 定義

            %{
            #include <stdio.h>
            %}
            %%
            begin printf("Startedn");
            hello printf("Hello yourself!n");
            thanks printf("Your welcomen");
            end printf("Stoppedn");
            %%

              代碼中的第一塊由 %{...%} 定義,表示將其中的文本插入到生成的 C 源代碼中。在本示例中,因?yàn)楹竺媸褂昧?span lang="EN-US"> printf() 函數(shù),所以必須確保包含了 stdio.h Header

              代碼中的第二塊由 %% 序列標(biāo)識(shí),其中包含了要識(shí)別的字符串輸入和相應(yīng)結(jié)果的定義。在上述的這些情況下,對(duì)于一個(gè)簡(jiǎn)單的單詞,將打印出合適的響應(yīng)。

              生成 C 源代碼

              要生成能夠真正分析輸入文本的 C 源代碼,可以對(duì)清單 1 所示的文件運(yùn)行 lex(或 flex)。Lex/flex 文件點(diǎn)號(hào)后綴為 'l',所以上面的文件可能名為 exampleA.l。要生成相應(yīng)的 C 源代碼:

            $ flex exampleA.l

              不管您使用哪種工具,其輸出都將為 lex.yy.c。缺乏勇氣的人往往不敢仔細(xì)研究這個(gè)文件,分析器內(nèi)部的處理過(guò)程的確非常復(fù)雜,并且建立在基于表格的復(fù)雜分析系統(tǒng)的基礎(chǔ)上,該系統(tǒng)可以根據(jù)原始 lex 定義中的定義對(duì)輸入文本進(jìn)行匹配。因?yàn)榇嬖谶@樣的關(guān)聯(lián),所以該代碼相當(dāng)占用內(nèi)存,特別是對(duì)于那些很大且很復(fù)雜的文件。

              與 lex 相比,flex 的優(yōu)點(diǎn)在于它提供了大量附加的選項(xiàng)以改進(jìn)性能(針對(duì)內(nèi)存或速度)、調(diào)試選項(xiàng)和對(duì)掃描器行為更好的控制(例如,可以忽略某些情況)。在生成 C 源代碼時(shí),通過(guò)使用 -l 命令行選項(xiàng),您可以生成與原始的 lex 工具生成的源代碼非常接近的 C 源代碼。

              既然已經(jīng)有了 C 源代碼,那么您可以將其編譯為相應(yīng)的應(yīng)用程序以測(cè)試該處理過(guò)程:

            $ gcc -o exampleA lex.yy.c -lfl

            flex 庫(kù)(使用 -lfl 進(jìn)行包含,而 lex 則使用 -ll)包含執(zhí)行分析代碼的簡(jiǎn)單的 main() 函數(shù)。當(dāng)運(yùn)行生成的應(yīng)用程序時(shí),它將等待輸入。清單 3 顯示了該應(yīng)用程序的輸入(和輸出)。

              清單 3. 簡(jiǎn)單 lex 應(yīng)用程序的輸入/輸出

            $ exampleA
            begin
            Started
            hello
            Hello yourself!
            end
            Stopped
            thanks
            Your welcome
            hello thanks
            Hello yourself!
            Your welcome
            hello Robert
            Hello yourself!
            Robert

              對(duì)于單行輸入 ('begin'),該應(yīng)用程序根據(jù)您所提供的命令進(jìn)行響應(yīng),在本示例中,將打印出單詞 'Started'。對(duì)于包含多個(gè)識(shí)別單詞的行,該應(yīng)用程序分別運(yùn)行多個(gè)命令,并以空格隔開(kāi)。對(duì)于無(wú)法識(shí)別的標(biāo)記(包括空白字符),僅對(duì)其進(jìn)行回顯。

              這個(gè)示例介紹了該系統(tǒng)的基本操作,但是您僅僅使用了標(biāo)準(zhǔn)的單詞。要使用其他的組合,如字符和元素,有各種不同的解決方案可供使用。

              識(shí)別元素

              可識(shí)別的元素可以不是前面介紹的固定字符串,標(biāo)識(shí)符支持正則表達(dá)式和特殊(例如標(biāo)點(diǎn)符號(hào))字符,如清單 4 所示。

              清單 4. 正則表達(dá)式和特殊字符

            %{
            #include <stdio.h>
            %}
            %%
            [a-z] printf("Lowercase wordn");
            [A-Z] printf("Uppercase wordn");
            [a-ZA-Z] printf("Wordn");
            [0-9] printf("Integern");
            [0-9.] printf("Floatn");
            ";" printf("Semicolonn");
            "(" printf("Open parenthesesn");
            ")" printf("Close parenthesesn");
            %%

              清單 4 中的示例應(yīng)該是一目了然的,并且對(duì)于需要進(jìn)行分析的任何正則表達(dá)式或特殊字符都可以使用相同的原則。

              真正的標(biāo)記化

              前面的示例所構(gòu)建的 C 源代碼實(shí)質(zhì)上是獨(dú)立的。盡管使用這種方法沒(méi)有什么問(wèn)題,但是對(duì)于分析包含多個(gè)單詞、短語(yǔ)或序列的給定指令中的文本或其他條目的情況,這種方法并不是很有價(jià)值。

              使用這種方式分析序列需要一個(gè)語(yǔ)法分析器,而它將定義標(biāo)記序列。但是語(yǔ)法分析器必須知道要分析的是什么標(biāo)記。在 lex 定義中,當(dāng)識(shí)別標(biāo)記時(shí)所進(jìn)行的操作是回顯字符串,如果要返回一個(gè)識(shí)別標(biāo)記,那么需要對(duì)該操作進(jìn)行更改。例如,您可以如清單 5 所示對(duì)原始示例進(jìn)行重寫(xiě)。

            %{
            #include <stdio.h>
            #include <y.tab.h>
            %}
            %%
            begin return BEGIN;
            hello return HELLO;
            thanks return THANKS;
            end return END;
            %%

              現(xiàn)在,當(dāng)識(shí)別了 'hello' 標(biāo)記時(shí),不再是打印一個(gè)字符串,而是返回標(biāo)記的名稱。在 yacc 中,將使用這個(gè)名稱來(lái)建立相應(yīng)的語(yǔ)法。

              在這個(gè)示例中,沒(méi)有顯式地定義這些標(biāo)記名稱。實(shí)際上是在 y.tab.h 文件中指定了這些標(biāo)記名稱,而在分析 yacc 語(yǔ)法文件時(shí)由 yacc 自動(dòng)生成該文件。

              提取變量數(shù)據(jù)

              如果要提取一個(gè)值(例如,需要能夠讀取一個(gè)數(shù)值或字符串),那么您必須指定如何將輸入轉(zhuǎn)換為所需的類(lèi)型。通常由于 lex 中的各種限制,這種做法并不是很有價(jià)值,但是當(dāng)使用 yacc 工具來(lái)構(gòu)建一個(gè)完整的分析器時(shí),這是至關(guān)重要的。

              通常可以使用兩個(gè)變量來(lái)交換信息,yytext 變量包含了在分析過(guò)程中由 lex 讀取的原始數(shù)據(jù),而 yylval 則用于在兩個(gè)系統(tǒng)之間交換實(shí)際的值。在本教程后面的內(nèi)容中,將更詳細(xì)地介紹這種結(jié)合是如何進(jìn)行的,但是出于識(shí)別信息的目的,您可能使用類(lèi)似下面的 lex 定義:

            [0-9] yylval=atoi(yytext); return NUMBER;

              在上面的代碼行中,使用了標(biāo)準(zhǔn)的 atoi() 函數(shù)將匹配正則表達(dá)式(并保存于 yytext 中)的輸入字符串片段轉(zhuǎn)換為一個(gè)整數(shù),并將 yylval 的值設(shè)置為該整數(shù)。請(qǐng)注意,盡管對(duì)值進(jìn)行了轉(zhuǎn)換,但您仍然需要返回值的類(lèi)型,以便 yacc 知道究竟是什么類(lèi)型并可以根據(jù)自己的定義來(lái)使用這個(gè)標(biāo)記。

              讓我們通過(guò)查看 yacc 如何定義語(yǔ)法結(jié)構(gòu),以此來(lái)了解這項(xiàng)工作是如何進(jìn)行的。

              使用 yacc 進(jìn)行語(yǔ)法分析

              語(yǔ)法定義用于定義標(biāo)記序列以及應(yīng)得到的結(jié)果。它通常與 lex 聯(lián)合使用,由 lex 讀取相應(yīng)的文本,并且兩者一同構(gòu)成整個(gè)分析器。

            yacc 的基本語(yǔ)法

            yacc 語(yǔ)法定義的基本結(jié)構(gòu)是定義預(yù)期的標(biāo)記序列。例如,您可以定義表達(dá)式 A + B,其中 A B 都是使用下列定義的數(shù)值:

            NUMBER PLUSTOKEN NUMBER { printf("%fn",($1+$3)); }

            NUMBER 是一個(gè)數(shù)值的識(shí)別標(biāo)記,并且 PLUSTOKEN 是加號(hào)的識(shí)別標(biāo)記。

              大括號(hào)中的代碼定義了識(shí)別這個(gè)序列后進(jìn)行何種操作,在本示例中,給出的是將這兩個(gè)數(shù)值相加的 C 代碼。請(qǐng)注意,使用命名法 $1 表示該語(yǔ)法表達(dá)式中的第一項(xiàng)標(biāo)記,$3 表示其中的第三項(xiàng)。

              對(duì)于待識(shí)別的語(yǔ)法序列,您必須對(duì)序列進(jìn)行命名,如清單 6 所示。

              清單 6. 對(duì)序列命名

            addexpr: NUMBER PLUSTOKEN NUMBER
            {
            printf("%fn",($1+$3));
            }
            ;

              語(yǔ)法組的名稱為 addexpr,并且這個(gè)組定義以一個(gè)分號(hào)作為結(jié)束。

              一個(gè)語(yǔ)法定義可以包含多個(gè)序列(以及相關(guān)的操作),而這些序列都是一個(gè)特殊的組中的一部分。例如在計(jì)算器中,加法和減法處理是類(lèi)似的操作,所以可以將它們集中作為一組。一個(gè)組中不同的定義可以使用管道符號(hào) (|) 進(jìn)行分隔(請(qǐng)參見(jiàn)清單 7)。

              清單 7. 一個(gè)組中不同的定義使用管道符號(hào)進(jìn)行分隔

            addexpr: NUMBER PLUSTOKEN NUMBER
            {
            printf("%fn",($1+$3));
            }
            | NUMBER MINUSTOKEN NUMBER
            {
            printf("%fn",($1-$3));
            }
            ;

              您已經(jīng)了解了基本的語(yǔ)法規(guī)范,那么還需要了解如何組合多個(gè)序列以及它們的優(yōu)先級(jí)。

              語(yǔ)法序列和優(yōu)先級(jí)

              在大多數(shù)語(yǔ)言中,無(wú)論是文本型、數(shù)學(xué)運(yùn)算型,還是編程型的語(yǔ)言,通常對(duì)于不同的短語(yǔ)都有某種優(yōu)先級(jí)或重要性,這將使得它們與類(lèi)似的但不同的組合相比具有相應(yīng)的優(yōu)先權(quán)。

              例如,在大多數(shù)編程語(yǔ)言的數(shù)學(xué)運(yùn)算表達(dá)式中,乘法具有比加法或減法更高的優(yōu)先級(jí)。例如,表達(dá)式:4+5*6 將計(jì)算為: 4+30

              這將得到最終的計(jì)算結(jié)果 34。其原因是首先進(jìn)行乘法操作(5 乘以 6 等于 30),然后將其結(jié)果用于最后的計(jì)算過(guò)程,即加上 4 后得到結(jié)果 34

              在 yacc 中,通過(guò)定義多個(gè)語(yǔ)法序列組并將它們連接在一起,就可以定義相應(yīng)的優(yōu)先級(jí),不同表達(dá)式在組中的順序可以幫助定義其優(yōu)先級(jí)。在清單 8 中,您可以看到用于定義上述行為的代碼。

              清單 8. 連接語(yǔ)法并提供優(yōu)先級(jí)

            add_expr: mul_expr
            | add_expr PLUS mul_expr { $$ = $1 + $3; }
            | add_expr MINUS mul_expr { $$ = $1 - $3; }
            ;
            mul_expr: primary
            | mul_expr MUL primary { $$ = $1 * $3; }
            | mul_expr DIV primary { $$ = $1 / $3; }
            ;
            primary: NUMBER { $$ = $1; }
            ;

            yacc 執(zhí)行所有表達(dá)式計(jì)算都是從左到右進(jìn)行處理的,所以對(duì)于與原始示例類(lèi)似的復(fù)合表達(dá)式(例如,4+5*6),yacc 將根據(jù)相應(yīng)的規(guī)則查找最佳匹配,即與表達(dá)式中的一部分匹配。

              根據(jù)規(guī)則所定義的順序,自上而下進(jìn)行匹配處理。所以,該處理過(guò)程首先試圖匹配 add_expr 中的語(yǔ)法規(guī)則。然而,因?yàn)槌朔ň哂懈叩膬?yōu)先級(jí),所以這些規(guī)則表明 yacc 應(yīng)該首先嘗試 mul_expr。這將迫使 yacc 首先根據(jù)乘法語(yǔ)句進(jìn)行匹配(在 mul_expr 中進(jìn)行定義)。如果匹配失敗,然后它將嘗試 add_expr 塊中的下一條規(guī)則,依此類(lèi)推,直到該表達(dá)式被解析,或者沒(méi)能找到任何匹配并生成一個(gè)錯(cuò)誤。

              當(dāng)使用上述的規(guī)則集對(duì)公式 4+5*6 進(jìn)行計(jì)算時(shí),yacc 首先嘗試 add_expr,然后被重定向到 mul_expr(作為匹配的第一條規(guī)則),并且又被重定向到 primaryprimary 規(guī)則設(shè)置了表達(dá)式中相應(yīng)數(shù)字的值。$$ 有效地設(shè)置了那部分表達(dá)式的返回值。

              在匹配了這些數(shù)字之后,yacc 根據(jù)下面這行定義識(shí)別了一個(gè)乘法表達(dá)式:| mul_expr MUL primary { $$ = $1 * $3; },它設(shè)置了原始表達(dá)式中的 5*6 部分的返回值。Yacc 生成代碼以執(zhí)行相應(yīng)的計(jì)算(提取前面由 primary 規(guī)則設(shè)置的值),并使用計(jì)算的結(jié)果值替換輸入表達(dá)式的原始部分。這就意味著 yacc 現(xiàn)在需要匹配下面的表達(dá)式: 4+30

              它可以使用下面的規(guī)則匹配這個(gè)表達(dá)式:| add_expr PLUS mul_expr { $$ = $1 + $3; },這將最終計(jì)算出最后的結(jié)果 34

              徹底地研究語(yǔ)法定義

              語(yǔ)法定義設(shè)置了相應(yīng)的規(guī)則,用于分析輸入標(biāo)記及其格式和布局,并將其轉(zhuǎn)換為可以使用的信息。在任何時(shí)候都請(qǐng)記住,這里定義的規(guī)則集是告訴 yacc 如何識(shí)別輸入并執(zhí)行一段 C 代碼。yacc 工具生成所需的 C 代碼來(lái)分析該信息,而并不是由 yacc 來(lái)完成分析工作。

              大括號(hào)中的代碼是 quasi-C 源代碼。Yacc 根據(jù)您給出的定義將原始文件轉(zhuǎn)換為相應(yīng)的 C 源代碼以及分析具體內(nèi)容所需的其他的代碼。

              除了定義如何分析輸入內(nèi)容的規(guī)則集之外,還有一些附加函數(shù)和選項(xiàng)用以幫助定義其他的 C 源代碼。完整的 yacc 定義文件的基本格式類(lèi)似與 lex/flex 所使用的格式,如下面的清單 9 所示。

              清單 9. Yacc 文件格式

            %{
            /* Global and header definitions required */
            %}
            /* Declarations (Optional token definitions) */
            %%
            /* Parsing ruleset definitions
            %%
            /* Additional C source code */

              全局/Header 定義與 lex/flex 文件中所使用的定義相同,如果您在分析和處理輸出時(shí)使用了特殊的函數(shù),那么它們都是必需的。

              標(biāo)記定義不僅描述了預(yù)期的標(biāo)記,還描述了分析處理過(guò)程中的優(yōu)先級(jí)。通過(guò)迫使 yacc 按照特定的順序檢查標(biāo)記,這可以調(diào)整 yacc 對(duì)規(guī)則的處理方式,并且還影響到在與規(guī)則進(jìn)行比較時(shí)如何對(duì)表達(dá)式進(jìn)行處理。

              例如,您通常可以定義 + 標(biāo)記具有左優(yōu)先級(jí),也就是說(shuō),首先對(duì)這個(gè)標(biāo)記左邊的任何表達(dá)式進(jìn)行匹配,這樣一來(lái),表達(dá)式 4+5+6 首先會(huì)計(jì)算 4+5,然后計(jì)算 9+6

              然而,您可能希望為一些標(biāo)記指定右優(yōu)先級(jí),例如,計(jì)算邏輯 NOT,(例如,! (expr))您可能希望先計(jì)算 expr,然后對(duì)通過(guò) ! 標(biāo)記對(duì)其表達(dá)式的值取反。

              除了控制規(guī)則之間的優(yōu)先級(jí)關(guān)系之外,還可以指定標(biāo)記的優(yōu)先級(jí),這樣您就可以精確地控制如何對(duì)輸入進(jìn)行計(jì)算。

              有三種主要的標(biāo)記類(lèi)型,%token(沒(méi)有優(yōu)先級(jí)差別),%left(標(biāo)記左側(cè)的輸入具有優(yōu)先級(jí))和 %right(標(biāo)記右側(cè)的輸入具有優(yōu)先級(jí))。例如,清單 10 根據(jù)適當(dāng)?shù)膬?yōu)先級(jí)定義了若干標(biāo)記:

              清單 10. 根據(jù)適當(dāng)?shù)膬?yōu)先級(jí)定義若干標(biāo)記

            %token EQUALS POWER
            %left PLUS MINUS
            %left MULTIPLY DIVIDE
            %right NOT

              請(qǐng)注意,以自上而下優(yōu)先級(jí)遞增的順序指定標(biāo)記的優(yōu)先級(jí),并且同一行中的標(biāo)記具有相同的優(yōu)先級(jí)。在清單 10 所示的示例中,MULTIPLY DIVIDE 具有相同的優(yōu)先級(jí),該優(yōu)先級(jí)要高于 PLUS MINUS 的優(yōu)先級(jí)。

              請(qǐng)注意,如果在規(guī)則中使用這里沒(méi)有定義的任何標(biāo)記,則將引發(fā)一個(gè)錯(cuò)誤。

              自定義初始化和錯(cuò)誤

              您將要定義的兩個(gè)關(guān)鍵的函數(shù)是 main() 函數(shù)(可以將自定義的初始化和其他的處理放在這個(gè)函數(shù)中),以及 yyerror() 函數(shù)(當(dāng)對(duì)于輸入信息無(wú)法找到一條分析規(guī)則時(shí)將打印出一個(gè)錯(cuò)誤)。由 yacc 生成的進(jìn)行實(shí)際分析工作的函數(shù)稱為 yyparse()。自定義函數(shù)的典型定義如清單 11 所示。

             清單 11. 分析器的自定義函數(shù)

            #include <stdio.h>
            #include <ctype.h>
            char *progname;
            double yylval;
            main( argc, argv )
            char *argv[];
            {
            progname = argv[0];
            yyparse();
            }
            yyerror( s )
            char *s;
            {
            fprintf( stderr ,"%s: %sn" , progname , s );
            }

            yyerror() 函數(shù)接受一個(gè)字符串,并且在引發(fā)錯(cuò)誤的時(shí)候?qū)⑵渑c程序名稱合并在一起作為輸出。

              將語(yǔ)法編譯為 C 代碼

            Yacc(最初是 yet another compiler compiler 的縮寫(xiě))和 GNU bison 工具都接受一個(gè)語(yǔ)法定義文件作為輸入,并生成創(chuàng)建分析器所需的 C 源代碼,而該分析器將對(duì)適當(dāng)?shù)妮斎脒M(jìn)行處理。

              通常,語(yǔ)法定義文件的擴(kuò)展名為 .y

              當(dāng)您運(yùn)行 yacc 時(shí),缺省情況下它將創(chuàng)建一個(gè)名為 yy.tab.c 的文件。如果 yacc lex 一同使用,那么您還需要生成一個(gè) C Header 文件,該文件包含了這兩個(gè)系統(tǒng)中識(shí)別的標(biāo)記的宏定義。除了 C 源代碼之外,如果還要生成 Header 文件,可以使用 -d 命令行選項(xiàng):

            $ yacc -d calcparser.y

              盡管 bison 可以執(zhí)行相同的基本操作,但缺省情況下,它所生成的文件使用源文件名稱的前綴作為其標(biāo)識(shí)。所以,在上面的示例中,使用 bison 將生成 calcparser.tab.c calcparser.tab.h 文件。

              這個(gè)區(qū)別是很重要的,不僅因?yàn)槟枰谰烤挂幾g那個(gè)文件,而且還因?yàn)槟枰?span lang="EN-US"> lex.l 定義中導(dǎo)入正確的 Header 文件,以便它能夠讀取正確的標(biāo)記定義。

              當(dāng)編譯一個(gè) lex/yacc 應(yīng)用程序時(shí),一般的過(guò)程如下:

              對(duì)分析器定義運(yùn)行 yacc

              對(duì)詞法定義運(yùn)行 lex

              編譯生成的 yacc 源文件。

              編譯生成的 lex 源文件。

              編譯任何其他的模塊。

              將 lexyacc 和其他源鏈接為一個(gè)可執(zhí)行文件。

              我傾向于使用一個(gè)與清單 12 所示類(lèi)似的 Makefile

              清單 12. 簡(jiǎn)單的 lex/yacc Makefile

            YFLAGS = -d
            PROGRAM = calc
            OBJS = calcparse.tab.o lex.yy.o fmath.o const.o
            SRCS = calcparse.tab.c lex.yy.c fmath.c const.c
            CC = gcc
            all: $(PROGRAM)
            .c.o: $(SRCS)
            $(CC) -c $*.c -o $@ -O
            calcparse.tab.c: calcparse.y
            bison $(YFLAGS) calcparse.y
            lex.yy.c: lex.l
            flex lex.l
            calc: $(OBJS)
            $(CC) $(OBJS) -o $@ -lfl -lm
            clean:; rm -f $(OBJS) core *~ #* *.o $(PROGRAM)
            y.* lex.yy.* calcparse.tab.*

              與 lex 進(jìn)行數(shù)據(jù)交換

            lex yacc 之間數(shù)據(jù)交換的主要方法是當(dāng)使用 yacc 創(chuàng)建 C 源代碼和生成包含每個(gè)標(biāo)記的 C 宏代碼的 Header 文件時(shí)生成的標(biāo)記定義。

              然而,如果您需要交換的數(shù)據(jù)不僅僅是一個(gè)標(biāo)記,那么您必須設(shè)置一個(gè)可以包含需要交換的信息的值。

              這個(gè)處理過(guò)程分為兩個(gè)階段。首先,您應(yīng)該根據(jù)具體需要定義 YYSTYPE。這是用于在這兩個(gè)系統(tǒng)之間進(jìn)行數(shù)據(jù)交換的缺省值。對(duì)于一個(gè)基本的計(jì)算器,您可能需要將其定義為整數(shù)(實(shí)際上這正是缺省的情況)、浮點(diǎn)數(shù)或雙精度數(shù)。如果您需要交換文本,那么可以將它設(shè)置為一個(gè)字符指針或數(shù)組。如果需要同時(shí)交換這兩種信息,那么您應(yīng)該創(chuàng)建一個(gè)可以包含這兩種值的聯(lián)合結(jié)構(gòu)。

              然后,您還應(yīng)該在 yacc lex 定義中聲明 yylval 變量(和其他任何需要共享的變量)為該類(lèi)型。

              例如,在這兩個(gè)文件的 Header 塊中,您需要指定這個(gè)類(lèi)型(請(qǐng)參見(jiàn)清單 13)。

              清單 13. 指定具體的類(lèi)型

            %{
            ...
            #define YYSTYPE double
            ...
            %}

              在 lex 定義中,您還需要將 yylval 變量定義為這個(gè)類(lèi)型的外部變量:

            extern YYSTYPE yylval;

              最后,在 yacc 文件的自定義 C 初始化塊中,您需要定義這個(gè)變量:

            YYSTYPE yylval;

              現(xiàn)在,您就可以在由這兩個(gè)系統(tǒng)生成的 C 源代碼中進(jìn)行數(shù)據(jù)交換了。

              構(gòu)建一個(gè)計(jì)算器

              您可以使用前面部分中演示的技術(shù)來(lái)構(gòu)建一個(gè)常規(guī)表達(dá)式計(jì)算器。

              計(jì)算器的基本概念

              您可以構(gòu)建一個(gè)能夠處理表達(dá)式的程序,如清單 14 所示。

              清單 14. 能夠處理表達(dá)式的程序

            4+5
            (4+5)*6
            2^3/6
            sin(1)+cos(PI)

              通過(guò)定義附加的標(biāo)記和語(yǔ)法規(guī)則,您可以進(jìn)一步擴(kuò)展它的功能以包括各種函數(shù)和方程式,不僅僅是標(biāo)準(zhǔn) C 語(yǔ)言和數(shù)學(xué)庫(kù)中的函數(shù),您還可以自己進(jìn)行特殊的定義。

              為了能夠分析所有這些不同的元素,第一步需要定義可以由詞法分析組件識(shí)別的標(biāo)記。清單 15 給出了完整的 lex 定義。

              清單 15. 高級(jí)計(jì)算器的 Lex 文件

            %{
            #define YYSTYPE double
            #include "calcparse.tab.h"
            #include <math.h>
            extern double yylval;
            %}
            D [0-9.]
            %%
            [ t] { ; }
            log return LOG;
            pi return PIVAL;
            sin return SIN;
            cos return COS;
            tan return TAN;
            and return AND;
            not return NOT;
            xor return XOR;
            or return OR;
            reg return REGA;
            ans return ANS;
            fix return FIX;
            sci return SCI;
            eng return ENG;
            const return CONST;
            bintodec return BINTODEC;
            dectobin return DECTOBIN;
            {D}+ { sscanf( yytext, "%lf", &yylval ); return NUMBER ; }
            [a-zA-Z_]+ return IDENT;
            "[" return OPENREG;
            "]" return CLOSEREG;
            "<<" return LEFTSHIFT;
            ">>" return RIGHTSHIFT;
            "++" return INC;
            "--" return DEC;
            "+" return PLUS;
            "-" return MINUS;
            "~" return UNARYMINUS;
            "/" return DIV;
            "*" return MUL;
            "^" return POW;
            "!" return FACT;
            "(" return OPENBRACKET;
            ")" return CLOSEBRACKET;
            "%" return MOD;
            "^^" return XOR;
            "!!" return NOT;
            "=" return ASSIGN;
            "&&" return LAND;
            "||" return OR;
            "|" return IOR;
            "&" return AND;
            "~~" return COMPLEMENT;
            "n" return EOLN;

              這里有大量的標(biāo)記,其中許多都是自說(shuō)明的。這些標(biāo)記包括基本數(shù)學(xué)操作、大量的函數(shù)(sincos 等等)和邏輯運(yùn)算符。另請(qǐng)注意,對(duì)于數(shù)值使用了雙精度類(lèi)型,使用 sscanf() 將數(shù)字字符串和小數(shù)點(diǎn)解析為適當(dāng)?shù)碾p精度值。

              計(jì)算器語(yǔ)法

              根據(jù)前面部分中介紹的標(biāo)記,存在大量的語(yǔ)法規(guī)則用于分析這些標(biāo)記。語(yǔ)法分析器的完整代碼如清單 16 所示。讓我們更仔細(xì)地看一下其中一些重要的部分以及該系統(tǒng)是如何工作的。

              清單 16. 計(jì)算器語(yǔ)法文件

            %{
            #include <alloca.h>
            #include <math.h>
            #include <stdlib.h>
            #include <stddef.h>
            #include <ctype.h>
            #define YYSTYPE double
            double calcfact();
            double reg[99];
            double ans;
            char format[20];
            %}
            %token NUMBER SPACE MOD RIGHTSHIFT LEFTSHIFT SEMICOLON SIN EOLN PIVAL
            %token PLUS MINUS DIV MUL POW OPENBRACKET CLOSEBRACKET UNARYMINUS
            %token COS TAN ASIN ACOS ATAN FACT INC DEC LAND OR COMPLEMENT
            %token NOT XOR ASSIGN IOR AND OPENREG CLOSEREG REGA ANS FIX SCI ENG
            %token CONST
            %left PLUS MINUS
            %left MUL DIV
            %left UNARYMINUS
            %left LAND OR XOR NOT AND IOR
            %left LOG
            %%
            list: /* nothing */
            | list EOLN
            | list expr EOLN
            { printf( format , (double) $2 ); ans=$2; }
            ;
            expr: conditional_expr
            ;
            conditional_expr: logical_or_expr
            ;
            logical_or_expr: logical_and_expr
            | logical_or_expr OR logical_and_expr
            { $$ = (int) $1 || (int) $3; }
            ;
            logical_and_expr: inclusive_or_expr
            | logical_and_expr LAND inclusive_or_expr
            { $$ = (int) $1 && (int) $3; }
            ;
            inclusive_or_expr: exclusive_or_expr
            | inclusive_or_expr IOR exclusive_or_expr
            { $$ = (int) $1 | (int) $3; }
            ;
            exclusive_or_expr: and_expr
            | exclusive_or_expr XOR and_expr
            { $$ = (int) $1 ^ (int) $3; }
            ;
            and_expr: shift_expr
            | and_expr AND shift_expr
            { $$ = (int) $1 & (int) $3; }
            ;
            shift_expr: pow_expr
            | shift_expr LEFTSHIFT pow_expr
            { $$ = (int) $1 << (int) $3; }
            | shift_expr RIGHTSHIFT pow_expr
            { $$ = (int) $1 >>(int) $3; }
            ;
            pow_expr: add_expr
            | pow_expr POW add_expr { $$ = pow($1,$3); }
            ;
            add_expr: mul_expr
            | add_expr PLUS mul_expr { $$ = $1 + $3; }
            | add_expr MINUS mul_expr { $$ = $1 - $3; }
            ;
            mul_expr: unary_expr
            | mul_expr MUL unary_expr { $$ = $1 * $3; }
            | mul_expr DIV unary_expr { $$ = $1 / $3; }
            | mul_expr MOD unary_expr { $$ = fmod($1,$3); }
            ;
            unary_expr: assign_expr
            | MINUS primary %prec UNARYMINUS { $$ = -$2; }
            | INC unary_expr { $$ = $2+1; }
            | DEC unary_expr { $$ = $2-1; }
            | NOT unary_expr { $$ = !$2; }
            | LOG unary_expr { $$ = log($2); }
            ;
            assign_expr: postfix_expr
            | REGA OPENREG expr CLOSEREG ASSIGN postfix_expr
            { reg[(int)$3]=$6; $$=$6; }
            | REGA OPENREG expr CLOSEREG
            { $$=reg[(int)$3]; }
            | REGA
            { int i;
            for(i=0;i<100;i++)
            if (reg[i]!=0)
            printf("%02d = %.2fn",i,reg[i]);
            $$=0;
            }
            ;
            postfix_expr: primary
            | postfix_expr INC { $$ = $1+1; }
            | postfix_expr DEC { $$ = $1-1; }
            | postfix_expr FACT
            { $$ = calcfact((unsigned long int)$1); }
            ;
            primary: NUMBER { $$ = $1; }
            | PIVAL { $$ = M_PI; }
            | OPENBRACKET expr CLOSEBRACKET { $$ = $2; }
            | ANS { $$ = ans; }
            | CONST OPENBRACKET expr CLOSEBRACKET { $$ = constval($3); }
            | set_format
            ;
            set_format: function_call
            | FIX OPENBRACKET expr CLOSEBRACKET
            { sprintf(format,"%%.%dfn",(int)$3); $$=0; }
            | FIX { sprintf(format,"%%fn"); $$=0; }
            | SCI OPENBRACKET expr CLOSEBRACKET
            { sprintf(format,"%%.%dgn",(int)$3); $$=0; }
            | SCI { sprintf(format,"%%gn"); $$=0; }
            | ENG OPENBRACKET expr CLOSEBRACKET
            { sprintf(format,"%%.%den",(int)$3); $$=0; }
            | ENG { sprintf(format,"%%en"); $$=0; }
            ;
            function_call: SIN OPENBRACKET expr CLOSEBRACKET
            { $$ = (cos($3)*tan($3)); }
            | COS OPENBRACKET expr CLOSEBRACKET
            { $$ = cos($3); }
            | TAN OPENBRACKET expr CLOSEBRACKET
            { $$ = tan($3); }
            | ASIN OPENBRACKET expr CLOSEBRACKET
            { $$ = asin($3); }
            | ACOS OPENBRACKET expr CLOSEBRACKET
            { $$ = acos($3); }
            | ATAN OPENBRACKET expr CLOSEBRACKET
            { $$ = atan($3); }
            ;
            %%
            #include <stdio.h>
            #include <ctype.h>
            char *progname;
            double yylval;
            main( argc, argv )
            char *argv[];
            {
            progname = argv[0];
            strcpy(format,"%gn");
            yyparse();
            }
            yyerror( s )
            char *s;
            {
            warning( s , ( char * )0 );
            yyparse();
            }
            warning( s , t )
            char *s , *t;
            {
            fprintf( stderr ,"%s: %sn" , progname , s );
            if ( t )
            fprintf( stderr , " %sn" , t );
            }

              對(duì)于應(yīng)用程序中其他的部分,有三個(gè)全局結(jié)構(gòu)可供使用:

            reg 數(shù)組用作通用存儲(chǔ)寄存器,可以在其中存放計(jì)算過(guò)程的值和結(jié)果。

            ans 變量包含上次計(jì)算的值。

            format 用于保存打印結(jié)果時(shí)所使用的輸出格式。

              僅當(dāng)輸入中包含行結(jié)束字符(以 EOLN 標(biāo)記進(jìn)行標(biāo)識(shí))時(shí),才打印出計(jì)算結(jié)果。這樣就可以在單行中輸入很長(zhǎng)的計(jì)算式,并且在對(duì)相應(yīng)的內(nèi)容進(jìn)行了分析和處理后,再打印出結(jié)果值。這項(xiàng)操作使用全局 format 變量所指定的格式,并且將結(jié)果存儲(chǔ)在 ans 中。

              分析器的主要部分是識(shí)別片段并計(jì)算結(jié)果的組合,直到最后將比較復(fù)雜的計(jì)算過(guò)程中不同的部分解析為最終的值。

              設(shè)置輸出格式

              格式就是用于 printf() 的一個(gè)合適的格式字符串,可以通過(guò)使用函數(shù)方式的調(diào)用來(lái)設(shè)置輸出的精度和格式。例如,要設(shè)置固定小數(shù)點(diǎn)的輸出以限制小數(shù)點(diǎn)右邊的數(shù)字,您可以使用 fix(3),或者可以重新設(shè)置缺省格式: fix()

              在清單 17 的序列中,您可以看到相應(yīng)的效果。

              清單 17. 輸出格式

            $ calc
            1/3
            0.333333
            fix(3)
            0.000
            1/3
            0.333

              因?yàn)樵摳袷阶址侨值模⑶覂H由一條規(guī)則來(lái)打印出結(jié)果,這就意味著您可以很容易地使用該格式字符串來(lái)控制輸出。

              計(jì)算器寄存器

              寄存器實(shí)際上是一個(gè)浮點(diǎn)值數(shù)組,可以通過(guò)使用一個(gè)數(shù)值引用來(lái)訪問(wèn)該數(shù)組。這部分代碼中的分析工作由清單 18 中所示的代碼片段完成。

              清單 18. 計(jì)算器中的寄存器

            assign_expr: postfix_expr
            | REGA OPENREG expr CLOSEREG ASSIGN postfix_expr
            { reg[(int)$3]=$6; $$=$6; }
            | REGA OPENREG expr CLOSEREG
            { $$=reg[(int)$3]; }
            | REGA
            { int i;
            for(i=0;i<100;i++)
            if (reg[i]!=0)
            printf("%02d = %.2fn",i,reg[i]);
            $$=0;
            }
            ;

              第一條規(guī)則允許進(jìn)行賦值,并且該分析器允許使用表達(dá)式對(duì)寄存器進(jìn)行引用,但對(duì)于相應(yīng)的值只能使用 postfix_expr。這將限制所能夠?qū)拇嫫髦付ǖ闹担缜鍐?span lang="EN-US"> 19 中的序列所示。

              清單 19. 限制能夠?qū)拇嫫髦付ǖ闹?

            reg[0]=26
            26
            reg[0]
            26
            reg[1]=(4+5)*sin(1)
            7.57324
            reg[1]
            9
            reg[4+5]=29
            29
            reg[9]
            29

              請(qǐng)注意,中間的表達(dá)式 (4+5)*sin(1) 返回正確的結(jié)果,但僅將該表達(dá)式開(kāi)始部分的值賦給了寄存器。這是因?yàn)樵撘?guī)則僅允許使用匹配 postfix_assign 規(guī)則的表達(dá)式進(jìn)行賦值,并且實(shí)際上由 primary 規(guī)則最終描述帶圓括號(hào)的語(yǔ)句的規(guī)則。因?yàn)椴辉试S將整行輸入作為賦值的一部分,所以分析器匹配了第一個(gè)片段 ((4+5)) 并將這個(gè)值賦給了寄存器。因?yàn)閷?duì)寄存器賦值時(shí)返回了寄存器的值,所以其他的計(jì)算過(guò)程可以繼續(xù)進(jìn)行并打印出正確的結(jié)果。

              有一個(gè)簡(jiǎn)單的解決方案可以處理這個(gè)問(wèn)題。作為一條簡(jiǎn)單的輸入規(guī)則,您需要使用圓括號(hào)將對(duì)寄存器的賦值語(yǔ)句括起來(lái):

            reg[1]=((4+5)*sin(1))

              這并不需要對(duì)相應(yīng)的規(guī)則進(jìn)行任何更改,因?yàn)樵诂F(xiàn)有的分析器中可以使用上面的方法,但可能需要對(duì)文檔進(jìn)行相應(yīng)的更改。

              當(dāng)前的系統(tǒng)還有一個(gè)優(yōu)點(diǎn),那就是您可以同時(shí)對(duì)下一個(gè)寄存器賦值。這是因?yàn)閷?duì)寄存器賦值時(shí)會(huì)返回該寄存器的結(jié)果。因此,下面的序列可以正常工作(請(qǐng)參見(jiàn)清單 20)。

              清單 20. 寄存器的結(jié)果

            reg[1]=(45+reg[2]=(3+4))
            52
            reg[1]
            52
            reg[2]
            7

              擴(kuò)展 lex yacc 的原則

              這個(gè)計(jì)算器演示了一些基本處理,但是關(guān)于 lex yacc 工具以及如何使用它們,還有很多的內(nèi)容。

              使用計(jì)算器分析器

              因?yàn)榉治鲚斎胄畔⒌囊?guī)則和對(duì)信息的處理可以單獨(dú)地進(jìn)行設(shè)置和控制,所以實(shí)際上可以獨(dú)立地改變信息提取和處理的方式。

              當(dāng)我第一次接觸 lex yacc 時(shí),我的初始目標(biāo)是構(gòu)建一個(gè)逆波蘭表示法 (RPN) 計(jì)算器。使用 RPN,您需要首先提供相應(yīng)的數(shù)值,然后是運(yùn)算符,例如,要將兩個(gè)數(shù)值相加在一起,您應(yīng)該使用: 4 5 +

              對(duì)于有些人來(lái)說(shuō),這種序列更有意義,特別是當(dāng)您希望編寫(xiě)出與學(xué)校里學(xué)到的加法運(yùn)算相同的計(jì)算過(guò)程時(shí):

            4
            5 +
            ----
            9
            ----

              對(duì)于更加復(fù)雜的計(jì)算過(guò)程,您可以將數(shù)值載入到堆棧中,然后使用相應(yīng)的參數(shù),即自動(dòng)地放回到堆棧中的計(jì)算結(jié)果。4+5*6 可以編寫(xiě)為: 4 5 6 * +

              對(duì)于計(jì)算機(jī)來(lái)說(shuō),這種表示方式更加直截了當(dāng)。您可以使用堆棧來(lái)實(shí)現(xiàn) RPN 計(jì)算器。當(dāng)分析器識(shí)別了一個(gè)數(shù)值,會(huì)將它壓入堆棧,并且當(dāng)發(fā)現(xiàn)了一個(gè)運(yùn)算符時(shí),從堆棧中彈出相應(yīng)的值,然后執(zhí)行計(jì)算過(guò)程。它簡(jiǎn)化了在執(zhí)行常規(guī)表達(dá)式分析中所需的大量的處理和復(fù)雜的分析過(guò)程。

              然而,再增加一點(diǎn)點(diǎn)工作,您就可以創(chuàng)建一個(gè)將常規(guī)表達(dá)式輸入轉(zhuǎn)換為 RPN 的分析器。甚至更加完善,您還可以將 RPN 轉(zhuǎn)換為標(biāo)準(zhǔn)的表達(dá)式格式。

              例如,將表達(dá)式轉(zhuǎn)換為 RPN

            $ equtorpn
            4+5*6
            4 5 6 * +

              其中有趣的是,分析規(guī)則中定義的優(yōu)先級(jí)順序(其 yacc 分析器是這里所介紹的計(jì)算器的簡(jiǎn)化版本)將產(chǎn)生滿足上述示例的 RPN

              您可以將 equtorpn 工具的輸出作為 RPN 計(jì)算器的輸入,并獲得相同的結(jié)果:

            $ equtorpn|rpn
            4+5*6
            34

              有關(guān) rpnequtorpn rpntoequ 應(yīng)用程序的完整示例和代碼及其工作方式的更詳細(xì)的討論,請(qǐng)?jiān)L問(wèn) MCslp Coalface Web 站點(diǎn)(請(qǐng)參見(jiàn)參考資料)。

              使用計(jì)算器分析器

              因?yàn)榉治鲚斎胄畔⒌囊?guī)則和對(duì)信息的處理可以單獨(dú)地進(jìn)行設(shè)置和控制,所以實(shí)際上可以獨(dú)立地改變信息提取和處理的方式。

              當(dāng)我第一次接觸 lex yacc 時(shí),我的初始目標(biāo)是構(gòu)建一個(gè)逆波蘭表示法 (RPN) 計(jì)算器。使用 RPN,您需要首先提供相應(yīng)的數(shù)值,然后是運(yùn)算符,例如,要將兩個(gè)數(shù)值相加在一起,您應(yīng)該使用: 4 5 +

              對(duì)于有些人來(lái)說(shuō),這種序列更有意義,特別是當(dāng)您希望編寫(xiě)出與學(xué)校里學(xué)到的加法運(yùn)算相同的計(jì)算過(guò)程時(shí):

            4
            5 +
            ----
            9
            ----

              對(duì)于更加復(fù)雜的計(jì)算過(guò)程,您可以將數(shù)值載入到堆棧中,然后使用相應(yīng)的參數(shù),即自動(dòng)地放回到堆棧中的計(jì)算結(jié)果。4+5*6 可以編寫(xiě)為: 4 5 6 * +

              對(duì)于計(jì)算機(jī)來(lái)說(shuō),這種表示方式更加直截了當(dāng)。您可以使用堆棧來(lái)實(shí)現(xiàn) RPN 計(jì)算器。當(dāng)分析器識(shí)別了一個(gè)數(shù)值,會(huì)將它壓入堆棧,并且當(dāng)發(fā)現(xiàn)了一個(gè)運(yùn)算符時(shí),從堆棧中彈出相應(yīng)的值,然后執(zhí)行計(jì)算過(guò)程。它簡(jiǎn)化了在執(zhí)行常規(guī)表達(dá)式分析中所需的大量的處理和復(fù)雜的分析過(guò)程。

              然而,再增加一點(diǎn)點(diǎn)工作,您就可以創(chuàng)建一個(gè)將常規(guī)表達(dá)式輸入轉(zhuǎn)換為 RPN 的分析器。甚至更加完善,您還可以將 RPN 轉(zhuǎn)換為標(biāo)準(zhǔn)的表達(dá)式格式。

              例如,將表達(dá)式轉(zhuǎn)換為 RPN

            $ equtorpn
            4+5*6
            4 5 6 * +

              其中有趣的是,分析規(guī)則中定義的優(yōu)先級(jí)順序(其 yacc 分析器是這里所介紹的計(jì)算器的簡(jiǎn)化版本)將產(chǎn)生滿足上述示例的 RPN

              您可以將 equtorpn 工具的輸出作為 RPN 計(jì)算器的輸入,并獲得相同的結(jié)果:

            $ equtorpn|rpn
            4+5*6
            34

              有關(guān) rpnequtorpn rpntoequ 應(yīng)用程序的完整示例和代碼及其工作方式的更詳細(xì)的討論,請(qǐng)?jiān)L問(wèn) MCslp Coalface Web 站點(diǎn)(請(qǐng)參見(jiàn)參考資料)。

              進(jìn)行適當(dāng)?shù)奈谋痉治?

              這些示例介紹了如何構(gòu)建一個(gè)表達(dá)式分析器,而該分析器可以將數(shù)學(xué)表達(dá)式轉(zhuǎn)換為可以進(jìn)行計(jì)算的表達(dá)式。該分析器說(shuō)明了序列、分析規(guī)則和諸如優(yōu)先級(jí)之類(lèi)的問(wèn)題的重要性。在文本處理系統(tǒng)中也可以應(yīng)用相同的規(guī)則,但是處理文本可能更加復(fù)雜,除非您建立與計(jì)算器示例中同樣嚴(yán)格的強(qiáng)制規(guī)則。

              清單 21 顯示了創(chuàng)建簡(jiǎn)單的 set/get 狀態(tài)分析器的 lex 文件,而清單 22 顯示了分析其輸出的 yacc 規(guī)則。

              清單 21. 使用 lex 進(jìn)行基于文本的標(biāo)記化

            %{
            #include <stdio.h>
            #include "text.tab.h"
            %}
            %%
            set return SET;
            state return STATE;
            mode return MODE;
            get return GET;
            [a-zA-Z]+ { yylval=strdup(yytext); return STRING; }
            %%

              清單 22. 分析文本的 yacc 規(guī)則

            %{
            #include <stdlib.h>
            #define YYSTYPE char *
            char mode[20];
            char state[20];
            %}
            %token SET STATE MODE GET STRING EOLN
            %%
            list: /* nothing */
            | list getorset
            ;
            getorset: getvalue
            | setvalue
            ;
            setvalue:
            SET STATE STRING { strcpy(state,$3); printf("State setn"); }
            | SET MODE STRING { strcpy(mode,$3); printf("Mode setn"); }
            ;
            getvalue:
            GET STATE { printf("State: %sn",state); }
            | GET MODE { printf("Mode: %sn",mode); }
            ;
            %%
            #include <stdio.h>
            #include <ctype.ha>
            char *progname;
            main( argc, argv )
            char *argv[];
            {
            progname = argv[0];
            yyparse();
            }
            yyerror( s )
            char *s;
            {
            fprintf( stderr ,"%s: %sn" , progname , s );
            }

              在計(jì)算器示例中,必須將輸入文本轉(zhuǎn)換為一個(gè)數(shù)值,而這個(gè)示例與計(jì)算器示例有所不同,您可以直接使用輸入字符串,而無(wú)需進(jìn)行轉(zhuǎn)換(請(qǐng)參見(jiàn)清單 23)。

              清單 23. 直接使用輸入字符串

            $ textparser
            get state
            State:
            set state bob
            State set
            get state
            State: bob

              更加復(fù)雜的文本分析器也都構(gòu)建于相同的基本原則,并且可用于完成各種任務(wù),從配置文件分析器,到構(gòu)建您自己的腳本語(yǔ)言,甚至是可進(jìn)行編譯的語(yǔ)言。

              語(yǔ)言編譯器

              語(yǔ)言編譯器(例如 C 編譯器)可以接受您的輸入,并將輸入轉(zhuǎn)換為指定的目標(biāo)平臺(tái)的原始匯編語(yǔ)言。這就解釋了為什么最初的編譯器都是特定于體系結(jié)構(gòu)的,為什么編寫(xiě)一個(gè)為可選的平臺(tái)生成代碼的編譯器(交叉編譯)相當(dāng)容易。編譯器僅為可選的平臺(tái)生成匯編代碼。例如,要將兩個(gè)數(shù)值相加到一起,您可以對(duì)前面使用的規(guī)則集進(jìn)行更改,以便生成如下所示的 Intel x86 匯編代碼:

            add_expr: mul_expr
            | add_expr PLUS mul_expr
            { printf("MOV ax,%dnADD ax,%dn",$1,$3); }

              為了使匯編轉(zhuǎn)換處理過(guò)程能夠構(gòu)建完整的語(yǔ)言,可以將其他的結(jié)構(gòu)(如循環(huán)、遞歸、函數(shù)、變量名稱和其他的元素)添加到該系統(tǒng)中。

              總結(jié)

              通過(guò) lex yacc 的組合,您可以生成構(gòu)建分析器的代碼。lex 工具提供了識(shí)別文本片段和元素的方法,并可以返回相應(yīng)的標(biāo)記。yacc 工具可以使用一系列描述輸入格式的規(guī)則來(lái)處理這些標(biāo)記組成的結(jié)構(gòu),并且定義了處理識(shí)別的序列的方法。在這兩種情況下,這些工具都使用了一個(gè)配置文件,在生成構(gòu)建合適的分析應(yīng)用程序的 C 源代碼時(shí)將對(duì)該文件進(jìn)行處理。

              在本教程中,您看到了關(guān)于如何使用這些應(yīng)用程序?yàn)橛?jì)算器應(yīng)用程序構(gòu)建簡(jiǎn)單的分析器的示例。通過(guò)這些處理,您了解了標(biāo)記化、規(guī)則集的重要性,以及規(guī)則集如何互相作用以提供完整的分析解決方案。您了解了如何使用這些基本原則進(jìn)行其他的處理,如文本分析器、甚至是構(gòu)建您自己的編程語(yǔ)言。

             

            posted on 2008-11-22 22:21 肥仔 閱讀(4360) 評(píng)論(1)  編輯 收藏 引用 所屬分類(lèi): LEX & YACC

            評(píng)論

            # re: 使用 yacc 和 lex 編寫(xiě)文本分析器  回復(fù)  更多評(píng)論   

            不錯(cuò),挺牛逼的這個(gè)
            2012-12-23 20:51 | Night
            国产精品99久久精品爆乳| 7777久久亚洲中文字幕| 久久综合综合久久97色| 人妻精品久久久久中文字幕69 | 青青青青久久精品国产h| 久久男人Av资源网站无码软件 | 久久91精品国产91久久小草 | 国内精品九九久久久精品| 久久涩综合| 尹人香蕉久久99天天拍| 久久综合色区| 久久久久久av无码免费看大片| 伊人久久精品无码二区麻豆| 99久久精品国产一区二区三区| 久久精品人人做人人爽电影 | 久久精品蜜芽亚洲国产AV| 狠狠综合久久综合中文88| 久久精品无码免费不卡| 久久久久久极精品久久久| 狼狼综合久久久久综合网| 久久天天躁狠狠躁夜夜不卡| 久久精品国产免费观看三人同眠| 国产精品热久久毛片| 国产精品久久久久影院色| 久久99精品国产自在现线小黄鸭| 亚洲午夜久久久影院| 久久国产精品无码一区二区三区| 久久亚洲日韩看片无码| 日韩亚洲国产综合久久久| 亚洲欧洲精品成人久久曰影片| 久久精品亚洲精品国产欧美| 免费久久人人爽人人爽av| 日韩精品久久无码人妻中文字幕| 一级女性全黄久久生活片免费 | 亚洲精品高清久久| 久久99久久99精品免视看动漫| 亚洲AV乱码久久精品蜜桃| 国产视频久久| 99久久99久久精品国产片果冻| 欧美国产精品久久高清| 亚洲日韩中文无码久久|