一、詞法剖析器產生器LEX的用法
1.1 Lex概述
程式設計語言從機器語言發展到今天的象pascal, C等這樣的進階語言,使人們可以擺脫與機器有關的細節進行程式設計。但是用進階語言寫程式時程式員必須在程式中詳盡地告訴電腦系統怎樣去解決某個問題,這在某種程度上說也是一件很複雜的工作。
人們希望有新的語言——非常進階的語言,用這種語言程式員僅僅需要告訴電腦系統要解決什麼問題,電腦系統能自動地從這個問題的描述去尋求解決問題的途徑,或者說將這個問題的描述自動地轉換成用某種進階語言如C、FORTRAN表示的程式。這個程式就可以解決給定的問題,這種希望雖然還沒有能夠完全變成現實,但是在某些具體的問題領域裡已經部分地實現了。
這裡要介紹的Lex和下章要介紹的Yacc就是在編譯器設計這個領域裡的兩種非常進階的語言。用它們可以很方便的描述詞法分析器和文法分析器,並自動產生出相應的進階語言(C)的程式。
Lex是一個詞法分析器(掃描器)的自動產生系統,它的1.1。
Lex來源程式是用一種面向問題的語言寫成的。這個語言的核心是正規運算式(正規式),用它描述輸入串的詞法結構。在這個語言中使用者還可以描述當某一個詞形被識別出來時要完成的動作,例如在進階語言的詞法分析器中,當識別出一個關鍵字時,它應該向文法分析器返回該關鍵字的內部編碼。Lex並不是一個完整的語言,它只是某種進階語言(稱為lex的宿主語言)的擴充,因此lex沒有為描述動作設計新的語言,而是藉助其宿主語言來描述動作。我們只介紹C作為lex的宿主語言時的使用方法,在Unix系統中,FORTRAN語言的一種改進形式Ratfor也可以做lex的宿主語言。
圖1.1 Lex
Lex自動地表示把輸入串詞法結構的正規式及相應的動作轉換成一個宿主語言的程式,即詞法剖析器,它有一個固定的名字yyler,在這裡yyler是一個C語言的程式。
Yylex將識別出輸入串中的詞形,並且在識別出某詞形時完成指定的動作。
看一個簡單的例子:寫一個lex來源程式,將輸入串中的小寫字母轉換成相應的大定字母。
程式如下:
%%
[a-z]printf(“%c”.yytext[0]+'A'-'a');
上述程式中的第一行%%是一個分界符,表示識別規則的開始。第二行就是識別規則。左邊是識別小寫字母的正規式。右邊就是識別出小寫字母時採取的動作:將小寫字母轉換成相應的大寫字母。
Lex的工作原理是將來源程式中的正規式轉換成相應的確定有限自動機,而相應的動作則插入到yylox中適當的地方,控制流程由該確定有限自動機的解譯器掌握,不同的來源程式,這個解譯器是相同的。關於lex工作原理的詳細情況請參考[3],這裡不多介紹。
1.2 lex來源程式的格式
lex來源程式的一般格式是:
{輔助定義的部分}
%%
{識別規則部分}
%%
{使用者子程式部分}
其中用花括弧起來的各部分都不是必須有的。當沒有“使用者子程式部分”時,第二個%%也可以省去。第一個%%是必須的,因為它標誌著識別規則部分的開始,最短的合法的lex來源程式是:
%%
它的作用是將輸入串照原樣抄到輸出檔案中。
識別規則都分是Lex來源程式的核心。它是一張表,左邊一列是正規式,右邊一列是相應的動作。下面是一條典型的識別規則:
integer printf("found keywcrd INT");
這條規則的意思是在輸入串中尋找詞形“integer”,每當與之匹配成功時,就列印出“foundkeyword INT”這句話。
注意在識別規則中,正規式與動作之間必須用空格分隔開。動作部分如果只是一個簡單的C運算式,則可以寫在正規式右邊同一行中,如果動作需要佔兩行以上,則須用花括弧括起來,否則會出錯。上倒也可以寫成:
integer {printf("found keyword INT");}
下面先介紹識別規則部分的寫法,再介紹其餘部分。
1.3 Lex用的正規式
一個正規式表示一個字串的集合。正規式由本文字元與正規式運算子組成.本文字元組成基本的正規式,表示某一個符號串;
正規式運算子則將基本的正規式組合成為複雜的正規式,表示字串的集合。
例如:
ab
僅表示字串ab,而
(a b)+
表示字串的集合:
{ab,abab,ababab,…)。
Lex中的正規式運算子有下列十六種:
”\[ ]∧ -?•*+| ()/${} %<>
上述運算子需要作為本文字元出現在正規式中時,必須藉助於雙引號”或反斜線\,具體用法是;
xyz“++”或xyz\+\+
表示字串xyz++
為避免死記上述十多個運算子,建議在使用非數字或字母字元時都用雙引號或反斜線。
要表示雙引號本身可用\”,要表示反外線用”\”或\\
前面說過,在識別規則中空格表示正規式的結束,因此要在正規式中引進空格必須藉助雙引號或反斜線,但出現在方括弧[]之內的空格是例外。
幾個特殊符號:
\n是斷行符號換行(newline)
\t是tab
\b是退格(back space)
下面按照運算子的功能分別介紹上述正規式運算子。
1.字元的集合
用方括弧對可以表示字元的集合。正規式
[a b c]
與單個字元a或b或c匹配
在方括弧中大多數的運算子都不起作用,只有\-和∧例外。
運算子----表示字元的範圍,例如
[a-z 0-9 <>-]
表示由所有小寫字母,所有數字、角括弧及底線組成的字元集合。
如果某字元集合中包括-在內,則必須把它寫在第一個或最後一個位置上,如
[-+0-9]
與所有數字和加號或減號匹配
在字元集合中,運算子∧必須寫在第一個位置即緊接在左方括弧之後,它的作用是求方括弧中除∧之外的字元組成的字元集合相對於電腦的字元集的補集,例如 [∧abc]與除去a、b和c以外的任何符號匹配。
運算子\在方括弧中同樣發揮解除運算子作用的功能。
2.與任一字元匹配的正規式
運算子。形成的正規式與除斷行符號分行符號以外的任一字元匹配。
在lex的正規式中,也可以用八位元字與\一起表示字元,如
[\40-\176]
與ASCII字元集中所有在八進位 40(空格)到八進位176(~)之間的可列印字元匹配。
3.可有可無的運算式
運算得?指出正規式中可有可無的子式,例如
ab?c
與ac或abc匹配,即b是可有可無的。
4.閉包運算
運算子*和十是 Lex正規式中的閉包運算子,它們表示正規式中某子式的重複,例如"a*"表示由0個或多個a組成的字串的集合,而"a+"表示由1個或多個a組成的字串的集合,下面兩個正規式是常用的:
[a-z]+
[A-Za-z][A-Za-z 0-9]*
第一個是所有由小寫字母組成的字串的集合,第二個是由字母開頭的字母數字串組成的集合。
5、選擇和字元組
運算子|表示選擇:
(ab|cd)
與ab或cd匹配
運算子()表示一組字元,注意()與[ ]的區別。(ab)表示字串ab,而[ab]則表示單個字元a或b。
圓括弧()用於表示複雜的正規式,例如:
(ab|cd+)?(ef)*
與abefef, efef, cdef, cddd匹配,但不與abc, abcd或abcdef匹配。
6、上下文相關性
lex可以識別一定範圍的上下文,因此可在一定程度上表示上下文相關性。
若某正規式的第一個字元是∧,則僅當該正規出現在一行的開始處時才被匹配,一行的開始處是指整個輸入串的開始或者緊接在一個斷行符號換行之後,注意∧還有另一個作作即求補,∧的這兩種用法不可能發生矛盾。
若某正規式的最後一個字元是$,則僅當該運算式出現在一行的結尾處時才被匹配,一行的結尾處是指該運算式之後緊接一個斷行符號換行。
運算子/指出某正規式是否被匹配取決於它的後文,例如: ab/cd,僅在ab之後緊接cd的情況下才與ab匹配。$其實是/的一個特殊情形,例如下面兩個正規式等價:ab$,ab/\ n
某正規式是否被匹配,或者匹配後執行什麼樣的動作也可能取決於該運算式的前文,前文相關性的處理方法在後面專門討論,將用到運算子"<>"
7、重複和輔助定義
當被{}括起來的是數字對時,{}表示重複;當它括起來的是一個名字時,則表示輔助定義的展開。例如:a{1,5} ,表示集合{a.aa.aaa.aaaa.aaaaa}.{digit}則與預先定義的名叫dight的串匹配,並將有定義插入到它在正規式中出現的位置上,輔助定義在後面專門討論。
最後,符號%的作用是作為lex來源程式的段間分隔字元。
1.4 Lex來源程式中的動作
前面說過當Lex識別出一個詞形時,要完成相應的動作。這一節敘述Lex為描述動作提供的協助。
首先應指出,輸入串中那些不與任何識別規則中的正規式匹配的字串將被原樣照望抄到輸出檔案中去。因此如果使用者不僅僅是希望照抄輸出,就必須為每一個可能的詞形提供識別規則,並在其中提供相應的動作。用lex為工具寫程式語言的詞法分析器時尤其要注意。最簡單的一種動作是濾掉輸入中的某些字串,這種動作用C的空語句“;”來實現。
例:濾掉輸入申中所有空格、tab和斷行符號分行符號,相應的識別規則如下:
[\t\n];
如果相鄰的幾條規則的動作都相同,則可以用|表示動作部分,它指出該規則的動作與下一條規則的動作相同。例如上倒也可以寫成:
“ ”|
“\t”|
“\n”;
注意\t和\n中的雙引號可以去掉。
外部字元數組yytext的內容是當前被某規則匹配的字串,例如正規式[a-z]+與所有由小寫字母組成的字串匹配,要想知道與它匹配的具體字串是什麼,可用下述規則:
[a-z]+ printf(“% s”, yytext);
動作printf(“%s”,yytext)就是將字元數組yytext的內容列印出來,這個動作用得很頻繁,Lex提供了一個宏ECHO來表示它,因此上述識別規則可以寫成:
[a-z]+ECHO;
請注意,上面說過預設的動作就是將輸入串原樣抄到輸出檔案中,那麼上述規則起什麼作用呢?這一點將在“規則的二義性”一節中解釋。
有時有必要知道被匹配的字串中的字元個數,外部變數yyleng就表示當前yytext中字元的個數。例如要對輸入串中單詞的個數和字元的個數進行計數(單詞假定是由大寫或小寫字母組成的字串),可用下述規則:
[a-zA-Z]+ {words++;
Chars+=yyleng;}
注意被匹配的字串的第一個字元和最後一個字元分別是
yytext[0]和yytext[yyleng-1]
下面介紹三個Lex提供的在寫動作時可能用到的C函數
l.yymore()
當需下一次被匹配的字串被添加在當前識別出的字串後面,即不使下一次的輸入替換yytext中已有的內容而是接在它的內容之後,必須在當前的動作中調用yymore( )
例:假設一個語言規定它的字串括在兩個雙引號之間,如果某字串中含有雙引號,則在它前面加上反斜線\。用一個正規式來表達該字串的定義很不容易,不如用下面較簡明的正規式與yymore()配合來識別:
\" [∧"]*{
if(yytext[yyleng-1]
= =’\\’yymore( );
else
…normal user processing
}
當輸入串為”abc\"def”時,上述規則首先與前五個字元”abc\匹配,然後調用yymore( )使餘下部分”def被添加在前一部分之後,注意作為字串結尾標誌的那個雙引號由”normal user proessing”部分負責處理
2.yyless(n)
如果當前匹配的字串的末尾部分需要重新處理,那麼可以調用 yyless(n)將這部分子串“退回”給輸入串,下次再匹配處理。yyless(n)中的n是不退回的字元個數,即退回的字元個數是yyleng-n。
例;在C語言中串“=-a”具有二義性,假定要把它解釋為“=-a”同時給出資訊,可用下面的識別規則:
=-[a-zA-Z]{
printf(“Operator(=-)
ambiguous\n”);
yyless(yyleng-1);
…action for=-…
}
上面的規則先列印出一條說明出現二義性的資訊,將運算子後面的字母返回給輸入串,最後將運算子按“=-”處理.另外,如果希望把“=- a”解釋為”=- a”,這隻需要把負號與字母一起退回給輸入串等候下次處理,用下面的規則即可:
=-[a-zA-Z]{
printf(“Operator(=-)
ambiguous\n”);
yyless (yyleng-1);
…action for = …
}
3. yywrap ( )
當Lex處理到輸入串的檔案尾時,自動地調用yywrap(),如果 yywrap()傳回值是 1,那麼Lex就認為對輸入的處理完全結束,如果yywrap()返回的值是0,Lex就認為有新的輸入串等待處理。
Lex自動提供一個yywrap(),它總是返回1,如果使用者希望有一個返回0的yywrap( ),那麼就可以在”使用者子程式部分”自己寫一個 yywrap(),它將取代Lex自動提供的那個yywrap(),在使用者自己寫的ywrap()中,使用者還可以作其他的一些希望在輸入檔案結束處要作的動作,如列印表格、輸出統計結果等,使用yywrap()的例子在後面舉出。
1. 5識別規則的二義性
有時Lex的程式中可能有多於一條規則與同一個字串匹配,這就是規則的二義性,在這種情況下,Lex有兩個處理原則:
1)能匹配最多字元的規則優先
2)在能匹配相同數目的字元的規則中,先給出的規則優先
例:設有兩規則按下面次序給出:
integer kegword action…
[a-z]+ identifier action…
如果輸入是integers,則它將被當成標識符處理,因為規則integer只能匹配7個字元,而[a-z]+能匹配8個字元;如果輸入串是integer,那麼它將被當作關鍵字處理,因為兩條規則都能與之匹配,但規則integer先給出。
1.6 lex來源程式中的輔助定義部分
Lex來源程式的第一部分是輔助定義,到目前為止我們只涉及到怎樣寫第二部分,即識別規則部分的寫法,現在來看第一部分的寫法。在Lex來源程式中,使用者為方便起見,需要一些輔助定義,如用一個名字代表一個複雜的正規式。輔助定義必須在第一個%%之前結出,並且必須從第一列開始寫,輔助定義的文法是:
name translation
例如用名字IDENT來代表標識符的正規式的輔助定義為
IDENT [a-zA-Z][a-zA-Z0-9]*
輔助定義在識別規則中的使用方式是用運算子{ }將 name括起來,Lex自動地用 translation去替換它,例如上述標識符的輔助定義的使用為:
{IDENT}action for identifer…
下面我們用輔助定義的手段來寫一段識別FORTRAN語言中整數和實數的Lex來源程式:
D [0一9]
E [DEde][-+]?{ D}+
%%
{D}+ printf(“integer”);
{D}+"."{D}*({E})? |
{D}*"."{D}+({E})? |
{D}+{E} printf( "real" );
請注意在輔助定義部分中可以使用前面的輔助定義。例如:定義E時使用了D,但所用的輔助定義必須是事先已定義過的,不能出現迴圈定義。上面的規則只是說明輔助定義的用法,並不是識別FORTRAN中數的全部規則,因為它不能處理類似35.EQ.I這樣的問題,即會把35.EQ.I中的35.E當作實數,怎麼解決這種問題請讀者思考。
除了上面介紹的輔助定義之外,使用者還需要在Lex來源程式中使用變數,還需要有一些自己寫的子程式。前面已經見過兩個常用的變數即yytext和yylong,也介紹過幾個Lex提供的子程式yymore,yyless和yywrap,現在介紹使用者如何自己定義變數和寫子程式。
Lex是把使用者寫的Lex來源程式轉換成一個C語言的程式yylex,在轉換過程中,Lex是把使用者自己的變數定義和子程式照抄到yylex中去,lex規定屬於下面三種情況之一的內容就照抄過去;
1)以一個空格或tab起頭的行,若不是 Lex的識別規則的一部分,則被照抄到 Lex產生的程式中去。如果這樣的行出現在第一個%%之前,它所含的定義就是全域的,即Lex產生的程式中的所有函數都可使用它。如果這樣的行緊接在第一個%%之後但在所有識別規則之前,它們就是局部的,將被抄到涉及它的動作相應的代碼中去。注意這些行必須符合C語言的文法,並且必須出現在所有識別規則之前。
這一規定的一個附帶的作用是使使用者可以為Lex來源程式或Lex產生的詞法分析器提供住解,當然註解必須符合C語言文法。
2)所有被界於兩行%{和%}之間的行,無論出現在哪裡也無論是什麼內容都被照抄過去,要注意 %{和%}必須分別單獨佔據一行.例如;
%{
# defineENDOFFILE 0
#include “head.h”
int flag
%}
提供上面的措施主要因為在C語言中有一些行如上例中的宏定義或檔案蘊含行必須從第一列開始寫。
3)出現在第二個%%之後的任何內容,不論其格式如何,均被照抄過去。
1.7 怎樣在Unix系統中使用Lex假定已經寫好了一個Lex來源程式。怎樣在Unix系統中從它得到一個詞法分析器呢?
Lex自動地把Lex來源程式轉換成一個C語言的可啟動並執行程式,這個可啟動並執行程式放在叫Lex.yy.c的檔案中,這個C語言程式再經過C編譯,即可運行。
例,有一名叫source的Lex來源程式,第一步用下面的命令將它轉換成lex.yy.c:
$ lex source
($是 Unix的提示符)。Lex.yy.c再用下面的命令編譯即得到可啟動並執行目標代碼 a.
out:
$cc lex.yy.c-ll
上面的命令列中的一11是調用Lex的庫,是必須使用的,請參看[1]。
這一節內容請讀者參看[4]中的lex(1)
Lex可以很方便地與Yacc配合使用,這將在下一章中介紹。
$1.8例子
這一節舉兩個例子看看Lex來源程式的寫法
1.將輸入串中所有能被7整除的整數加3,其餘部分照原樣輸出,先看下面的Lex來源程式:
%%
int k;
[0-9]+{
scanf(-1, yytext,“%d”,&k);
if(k % 7 = =0)
printf(“%d”,k+3);
else
printf(“ % d”, k);
}
上面的程式還有不足的地方,如對負整數,只是將其絕對值加上3,而且象X7,49.63這樣
的項也做了修改,下面對上面的來源程式稍作修改就避免了這些問題。
%%
int k;
-?[0-9]+{
scanf(-1,yytext,“%d”,&k);
printf(“%d”,k%7= =0?k+3;k);
}
-?[0-9]+ ECHO;
[A-Za-z][A-Za-z0-9]+ ECHO;
2.下一個例子統計輸人串中各種不同長度的單詞的個數,統計結果在數組lengs中,單詞定義為由字母組成的串,來源程式如下;
int lengs [100];
%%
[a-z]+ lengs[yyleng]++;
•|
\n ;
%%
yywrap ( )
{
int i ;
printf(“Length No.words \n”) ;
for(i=0;i<100;i++)
if(lengs[i]>0)
ptintf(“%5d % 10d\n”,i, lengs[i];
return (1);
}
在上面的流程式中,當Lex讀入輸入串時,它只統計而不輸出,到輸入串讀入完畢後,才在調用 yywrap()時輸出統計結果,為此使用者自己提供了yywrap(),注意yywrap()的最後一個語句是傳回值1。
1.9 再談上下文相關性的處理
在$3中介紹Lex用的正規式時提到了上下文相關性的表示,這裡再詳細介紹Lex提供的處理上下文相關的措施。要處理的問題是某些規則在不同的上下文中要採取不同的動作,或者說同樣的字串在不同的上下文中有不同的解釋。例如在程式設計語言中,同一個等號“=”,在說明部分表示為變數賦初值,這時的動作應是修改符號表內容;而在語句部分等號就是指派陳述式的賦值號,這時又應該產生相應於指派陳述式的代碼。因此要依據等號所處的上下文來判斷它的含義。Lex提供了兩種主要的方法,
1)使用標誌來區分不同的上下文。
標誌是使用者定義的變數,使用者在不同的上下文中為它置不同的值,以區分它在哪個上下文中,這樣識別規則就可以根據標誌當前值決定在哪個上下文中並採取相應的動作。
例:將輸入串照原樣輸出,但對magic這個詞,當它出現在以字母a開頭的行中,將其改為first,出現在以b開頭的行中將其改為second,出現在以c開頭的行中則改為third。
使用標誌flag的Lex來源程式如下;
int flag ;
%%
∧a {flag='a';ECHO;}
∧b {flag='b'; ECHO;}
∧c {flag='c'; ECHO;}
\n {flag=o; ECHO;}
magic{
switch (flag)
{
case 'a' : printf (“first”); break;
case 'b': printf (“second”); break;
case 'c' : printf (“third”); break;
default; ECHO; break;
}
}
2)使用開始條件來區分不同上下文
在Lex來源程式中使用者可以用名字定義不同的開始條件。當把某個開始條件立置於某條識別規則之前時,只有在Lex處於這個開始條件下這條規則才起使用,否則等於沒有這條規則。Lex當前所處的開始條件可以隨時由使用者程式(即Lex動作)改變。
開始條件由使用者在Lex來源程式的“輔助定義部分”定義,文法是
%Start name1 name2 name3…
其中Start可以縮寫成S或s。開始條件名字的順序可以任意給出,有很多開始條件時也可以由多個%Start行來定義它們。
開始條件在識別規則中的使用方法是把它用角括弧括起來放在識別規則的正規式左邊:
<name1>expression
要進入開始條件如Name1,在動作中用語句
BEGIN name1
它將Lex所處的當前開始條件改成 name1
要恢複正常狀態,用語句
BEGIN 0
它將 Lex恢複到 Lex解譯器的初始條件
一條規則也可以在幾個開始條件下都起作用,如
<name1 ,name2,name3> rule
使rule在三個不同的開始條件下都起作用。要使一條規則在所有開始條件下都起作用,就不在它前面附加任何開始條件。
例:解決1)中的問題,這次用開始條件,Lex來源程式如下:
%start AA BB CC
%%
∧a {ECHO; BEGIN AA;}
∧b {ECHO; BEGIN BB;}
∧c {ECHO; BEGIN CC;}
\n {ECHO; BEGIN 0;}
<AA>magic printf (“first”) ;
<BB>magic Printf(“second”);
<CC>magic Printf(“third”)i
1.10 Lex來源程式格式總結
為使用方便起見,將Lex來源程式的格式,Lex的正規式的格式等總錄於此.
Lex來源程式的一般格式為:
{definitions}
%%
{rules}
%%
{user subroutines}
輔助定義部分包括以下項目;
1)輔助定義,格式為:
name translation
2)直接按照抄的代碼,格式為:
空格 代碼
3)直接照抄的代碼,格式為:
%{
代碼
%}
4)開始條件,格式為:
%S namel name2…
還有幾個其他項目,不常使用故略去。
識別規則部分的格式是
expression action
其中expression必須與action用空格分開,動作如果多於一行,要用花括弧括起來。
Lex用的正規式用的運算子有以下一些:
x 字元x
“x”字元x,若為運算子,則不起運算子作用
\x 同上
[xy] 字元x或y
[x-z] 字元x,或y,或z
[∧x] 除x以外的所有字元
. 除斷行符號換行外的所有字元
∧x 出現在一行開始處的x
<y>x 當 Lex處於開始條件 y時, x
X$ 出現在一行末尾處的x
x? 可有可無的 X
x* 0個或多個x
x+ 1個或多個x
x|y X或y
(x)字元x
x/y 字元x但僅當其後緊隨y
{xx} 輔助定義XX的展開
x(m,n)m到n個x