YACC文件格式
yacc文件分為三部分:
... definitions ...(%{}%)
%%
... rules ...
%%
... subroutines ...
定義部分
第一部分包括標志(token)定義和C代碼(用“%{”和“%}”括起來)。
如在定義部分定義標志:
%token INTEGER
%token INTEGER
當運行yacc后,會產生頭文件,里面包含該標志的預定義,如:
#ifndef YYSTYPE
#define YYSTYPE int
#endif
#define INTEGER 258
extern YYSTYPE yylval;
#ifndef YYSTYPE
#define YYSTYPE int
#endif
#define INTEGER 258
extern YYSTYPE yylval;
lex使用該頭文件中的標志定義。Yacc調用lex的yylex()來獲得標志(token),與標志對應的值由lex放在變量yylval中。yylval的類型由YYSTYPE決定,YYSTYPE缺省類型是int。如:
[0-9]+ {
yylval = atoi(yytext);
return INTEGER;
}
[0-9]+ {
yylval = atoi(yytext);
return INTEGER;
}
標志0-255被保留作為字符值,一般產生的token標志從258開始。如:
[-+] return *yytext; /* return operator */
返回加號或減號。注意要把減號放在前面,避免被認作是范圍符號。
[-+] return *yytext; /* return operator */
返回加號或減號。注意要把減號放在前面,避免被認作是范圍符號。
對於操作符,可以定義%left和%right:%left表示左相關(left-associative),%right表示右相關(right-associative)。可以定義多組%left或%right,在后面定義的組有更高的優先級。如:
%left ‘+’ ‘-‘
%left ‘*’ ‘/’
上面定義的乘法和除法比加法和減法有更高的優先級。
%left ‘+’ ‘-‘
%left ‘*’ ‘/’
上面定義的乘法和除法比加法和減法有更高的優先級。
改變YYSTYPE的類型。如這樣定義TTSTYPE:
%union
%union
{
int iValue; /* integer value */
char sIndex; /* symbol table index */
nodeType *nPtr; /* node pointer */
};
int iValue; /* integer value */
char sIndex; /* symbol table index */
nodeType *nPtr; /* node pointer */
};
則生成的頭文件中的內容是:
typedef union
typedef union
{
int iValue; /* integer value */
char sIndex; /* symbol table index */
nodeType *nPtr; /* node pointer */
} YYSTYPE;
extern YYSTYPE yylval;
int iValue; /* integer value */
char sIndex; /* symbol table index */
nodeType *nPtr; /* node pointer */
} YYSTYPE;
extern YYSTYPE yylval;
可以把標志(token)綁定到YYSTYPE的某個域。如:
%token <iValue> INTEGER
%type <nPtr> expr
把expr綁定到nPtr,把INTEGER綁定到iValue。yacc處理時會做轉換。如:
expr: INTEGER { $$ = con($1); }
轉換結果為:
yylval.nPtr = con(yyvsp[0].iValue);
其中yyvsp[0]是值棧(value stack)當前的頭部。
%token <iValue> INTEGER
%type <nPtr> expr
把expr綁定到nPtr,把INTEGER綁定到iValue。yacc處理時會做轉換。如:
expr: INTEGER { $$ = con($1); }
轉換結果為:
yylval.nPtr = con(yyvsp[0].iValue);
其中yyvsp[0]是值棧(value stack)當前的頭部。
定義一元減號符有更高的優先級的方法:
%left GE LE EQ NE '>' '<'
%left '+' '-'
%left '*'
%nonassoc UMINUS
%nonassoc的含義是沒有結合性。它一般與%prec結合使用表示該操作有同樣的優先級。如:
expr: '-' expr %prec UMINUS { $$ = node(UMINUS, 1, $2); }
表示該操作的優先級與UMINUS相同,在上面的定義中,UMINUS的優先級高於其他操作符,所以該操作的優先級也高於其他操作符計算。
%left GE LE EQ NE '>' '<'
%left '+' '-'
%left '*'
%nonassoc UMINUS
%nonassoc的含義是沒有結合性。它一般與%prec結合使用表示該操作有同樣的優先級。如:
expr: '-' expr %prec UMINUS { $$ = node(UMINUS, 1, $2); }
表示該操作的優先級與UMINUS相同,在上面的定義中,UMINUS的優先級高於其他操作符,所以該操作的優先級也高於其他操作符計算。
規則部分
規則部分很象BNF語法。
規則中目標或非終端符放在左邊,后跟一個冒號(:),然后是產生式的右邊,之后是對應的動作(用{}包含)。如:
%token INTEGER
%%
program: program expr '\n' { printf("%d\n", $2); }
;
expr: INTEGER {
| expr '-' expr { $$ = $1 - $3; }
;
%%
int yyerror(char *s)
{
fprintf(stderr, "%s\n", s);
return 0;
}
%token INTEGER
%%
program: program expr '\n' { printf("%d\n", $2); }
;
expr: INTEGER {
= $1; }
| expr '+' expr {
= $1 + $3; } | expr '+' expr {
| expr '-' expr { $$ = $1 - $3; }
;
%%
int yyerror(char *s)
{
fprintf(stderr, "%s\n", s);
return 0;
}
其中,$1表示右邊的第一個標記的值,$2表示右邊的第二個標記的值,依次類推。$$表示規約后的值。
第三部分
該部分是函數部分。當yacc解析出錯時,會調用函數yyerror(),用戶可自定義函數的實現。
main函數是調用yacc解析入口函數yyparse()。如:
int main(void)
{
yyparse();
return 0;
}
{
yyparse();
return 0;
}
遞歸的處理
遞歸處理有左遞歸和右遞歸。
左遞歸形式:
list: item
| list ',' item;
list: item
| list ',' item;
右遞歸形式:
list: item
| item ',' list
list: item
| item ',' list
使用右遞歸時,所有的項都壓入堆棧里,才開始規約;而使用左遞歸的話,同一時刻不會有超過三個項在堆棧里。
If-Else的沖突
當有兩個IF一個ELSE時,該ELSE和哪個IF匹配是一個問題。有兩種匹配方法:與第一個匹配和與第二匹配。現代程序語言都讓ELSE與最近的IF匹配,這也是yacc的缺省行為。
雖然yacc行為正確,但為避免警告,可以給IF-ELSE語句比IF語句更高的優先級:
%nonassoc IFX
%nonassoc ELSE
stmt: IF expr stmt %prec IFX
| IF expr stmt ELSE stmt
%nonassoc IFX
%nonassoc ELSE
stmt: IF expr stmt %prec IFX
| IF expr stmt ELSE stmt
出錯處理
當yacc解析出錯時,缺省的行為是調用函數yyerror(),然后從yylex返回一個值。一個更友好的方法是忽略一段錯誤輸入流,繼續開始掃描。這里要涉及到YACC中錯誤保留字error的應用。
Yacc源程序的風格
建議按照如下風格來寫:
(1)終端符名全部用大寫字母,非終端符全部用小寫字母;
(2)把語法規則和語義動作放在不同的行;
(3)把左部相同的規則寫在一起,左部只寫一次,而后面所有規則都寫在豎線“|”之后;
(4)把分號“;”放在規則最后,獨占一行;
(5)用制表符來對齊規則和動作。
(1)終端符名全部用大寫字母,非終端符全部用小寫字母;
(2)把語法規則和語義動作放在不同的行;
(3)把左部相同的規則寫在一起,左部只寫一次,而后面所有規則都寫在豎線“|”之后;
(4)把分號“;”放在規則最后,獨占一行;
(5)用制表符來對齊規則和動作。
語法分析中的錯誤處理
當進行語法分析時發現輸入串有語法錯誤,最好能在報告出錯信息以后繼續進行語法分析,以便發現更多的錯誤。
Yacc處理錯誤的方法是:當發現語法錯誤時,yacc丟掉那些導致錯誤的符號適當調整狀態棧。然后從出錯處的后一個符號處或跳過若干符號直到遇到用戶指定的某個符號時開始繼續分析。Yacc內部有一個保留的終結符error,把它寫在某個產生式的右部,則Yacc就認為這個地方可能發生錯誤,當語法分析的確在這里發生錯誤時,Yacc就用上面介紹的方法處理,如果沒有用到 error的產生式,則 Yacc打印出“Syntax error”,就終止語法分析。
Yacc處理錯誤的方法是:當發現語法錯誤時,yacc丟掉那些導致錯誤的符號適當調整狀態棧。然后從出錯處的后一個符號處或跳過若干符號直到遇到用戶指定的某個符號時開始繼續分析。Yacc內部有一個保留的終結符error,把它寫在某個產生式的右部,則Yacc就認為這個地方可能發生錯誤,當語法分析的確在這里發生錯誤時,Yacc就用上面介紹的方法處理,如果沒有用到 error的產生式,則 Yacc打印出“Syntax error”,就終止語法分析。
下面看兩個使用error的簡單例子:
1.下面的產生式
stat: error
;
使yacc在分析stat推導出的句型時,遇到語法錯誤時跳過出錯的部分,繼續分析(也會打印語法錯誤信息)
stat: error
;
使yacc在分析stat推導出的句型時,遇到語法錯誤時跳過出錯的部分,繼續分析(也會打印語法錯誤信息)
2.下面的產生式
stat: error ';'
;
使yacc碰到語法錯時,跳過輸入串直到碰到下一個分號才繼續開始語法分析。
stat: error ';'
;
使yacc碰到語法錯時,跳過輸入串直到碰到下一個分號才繼續開始語法分析。
嵌入式動作
對於語法分析程序中的每一個語法規則,都有相應的C/C++語句來做一些額外的處理,這個額外的處理就是語法動作。不過語法動作和詞法動作的不同之處在於,語法動作允許嵌入式的語法動作,而詞法動作不行。
盡管yacc的語法分析技術只允許動作在規則的末端,但yacc可以自動模擬嵌入在規則內部的動作。如果在規則內部寫入一個動作,yacc就會創造一個右側為空並且左邊是自動生成的名字規則,使得嵌入的動作進高規則的動作里去,用自動成成的名字代替最初的規則內的動作。
例如: 下面的句子是等價的
thing : A {printf("I am A") ;} B
thing : A fakename B;
fakename : {printf("I am A");}
這種方式將A植作為$1, 規則末端的動作可將嵌入式動作的值作為$2,B的值為$3.
Example:
//L文件: %{ #include "FIRST_TA.H" #include <stdio.h> #include <stdlib.h> %} %% a {return A_STATE;} b {return B_STATE;} c {return C_STATE;} not {return NOT;} %% //Y文件: %{ #include <stdio.h> #include <stdlib.h> %} %token A_STATE B_STATE C_STATE NOT %% program : A_STATE B_STATE { int c, d; c = 20; d = 25; } c_state_not { int e,f; e = 30; f = 35; } | A_STATE B_STATE { int a, b; a = 10; b = 15; } c_state_not : C_STATE NOT{} %% 輸入文件的字符:a, b, c, f, c, not