買了本《自制編程語言》,這本書有點難,目前只是看前兩章,估計后面的章節,最近一段時間是不會看了,真的是好難啊!!
由於本人是身處弱校,學校的課程沒有編譯原理這一門課,所以就想看這兩章,了解一下編譯原理,增加一下自己的軟實力。免得被別人鄙視。
一、安裝yacc和lex
我是在Windows下使用這兩個軟件的。所以使用bison代替yacc,用flex代替lex。兩者的下載地址是http://sourceforge.net/projects/winflexbison/ 我的gcc環境是使用以前用過的mingw。我們吧解壓后的flex和bison放到mingw的bin目錄下。這一步就完成了。
二、編譯代碼
先編譯代碼,看一下結果,然后在分析。在這本書中提供的一個網址有書中代碼下載。下載地址 http://avnpc.com/pages/devlang 下載后找到mycalc這個文件夾。然后執行下面進行編譯
1 bison --yacc -dv mycalc.y -o y.tab.c 2 flex mycalc.l 3 gcc -o mycalc y.tab.c lex.yy.c
三、yacc/lex是什么
一般編程語言的語法處理,都會有以下的過程。
1.詞法分析
將源代碼分割成若干個記號的處理。
2.語法分析
即從記號構建分析樹的處理。分析樹也叫作語法樹或抽象語法樹。
3.語義分析
經過語法分析生成的分析樹,並不包含數據類型等語義信息。因此在語義分析階段,會檢查程序中是否含有語法正確但是存在邏輯問題的錯誤。
4.生成代碼
如果是C語言等生成機器碼的編譯器或Java這樣生成字節碼的編譯器,在分析樹構建完畢后會進入代碼生成階段。
例如有下面源代碼
1 if(a==10) 2 { 3 printf("hoge\n"); 4 } 5 else 6 { 7 printf("piyo\n"); 8 }
執行詞法分析后,將被分割為如下的記號(每一塊就是一個記號)
對此進行語法分析后構建的分析樹,如下圖所示
執行詞法分析的程序稱為詞法分析器。lex的工作就是根據詞法規則自動生成詞法分析器。
執行語法分析的程序則稱為解析器。yacc就是根據語法規則自動生成解析器的程序。
四、分析計算器代碼
1.mycalc.l源代碼
1 %{ 2 #include <stdio.h> 3 #include "y.tab.h" 4 5 int 6 yywrap(void) 7 { 8 return 1; 9 } 10 %} 11 %% 12 "+" return ADD; 13 "-" return SUB; 14 "*" return MUL; 15 "/" return DIV; 16 "\n" return CR; 17 ([1-9][0-9]*)|0|([0-9]+\.[0-9]*) { 18 double temp; 19 sscanf(yytext, "%lf", &temp); 20 yylval.double_value = temp; 21 return DOUBLE_LITERAL; 22 } 23 [ \t] ; 24 . { 25 fprintf(stderr, "lexical error.\n"); 26 exit(1); 27 } 28 %%
第一行到第十行是一個定義區塊,lex中用 %{...}%定義,這里面代碼將原樣輸出。
第11行到第28行是一個規則區塊。語法大概就是前面一部分是使用正則表達式后面一部分是返回匹配到后這一部分是類型標記。大括號里面是動作。例如 ([1-9][0-9]*)|0|([0-9]+\.[0-9]*)是匹配小數,然后對這個小數進行sscanf處理后返回一個DOUBLE_LITERAL類型。
2.mycalc.y 源代碼
1 %{ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #define YYDEBUG 1 5 %} 6 %union { 7 int int_value; 8 double double_value; 9 } 10 %token <double_value> DOUBLE_LITERAL 11 %token ADD SUB MUL DIV CR 12 %type <double_value> expression term primary_expression 13 %% 14 line_list 15 : line 16 | line_list line 17 ; 18 line 19 : expression CR 20 { 21 printf(">>%lf\n", $1); 22 } 23 expression 24 : term 25 | expression ADD term 26 { 27 $$ = $1 + $3; 28 } 29 | expression SUB term 30 { 31 $$ = $1 - $3; 32 } 33 ; 34 term 35 : primary_expression 36 | term MUL primary_expression 37 { 38 $$ = $1 * $3; 39 } 40 | term DIV primary_expression 41 { 42 $$ = $1 / $3; 43 } 44 ; 45 primary_expression 46 : DOUBLE_LITERAL 47 ; 48 %% 49 int 50 yyerror(char const *str) 51 { 52 extern char *yytext; 53 fprintf(stderr, "parser error near %s\n", yytext); 54 return 0; 55 } 56 57 int main(void) 58 { 59 extern int yyparse(void); 60 extern FILE *yyin; 61 62 yyin = stdin; 63 if (yyparse()) { 64 fprintf(stderr, "Error ! Error ! Error !\n"); 65 exit(1); 66 } 67 }
上面第13行到第48行,語法規則簡化為下面格式
A : B C | D ;
即A的定義是B與C的組合,或者為D。
上面的過程可以用一個游戲的方式解釋。就是一個數字是定位為DOUBLE_LITERAL類型,通過第45行的規則可以將DOUBLE_LITERAL升級成primary_expression類型,然后通過34行規則可以升級為term類型。又term類型可以升級為expression類型。所以 “2+4” 符合的規則是數字2升級到expression類型,而當數字4升級到term類型時,此時的狀態是 expression ADD term 通過第25行的規則可以得到兩者的結合,得到一個term類型。(PS:這個時候讓我想起了一個動漫,就是數碼寶貝,類型可以進行進化,進化,超進化,究極進化,還可以合體進化。<笑>)上面一個專業的叫法是叫做歸約。
由於歸約情況比較復雜和不好講,我就截書本上的原圖進行講解吧。
至於左結合或右結合是由寫的詞法分析器來決定的。例如給出的代碼,為什么是右結合呢,是因為用到了遞歸,所以會首先和低級的類型進行結合,這就是為什么MUL,DIV是term類型,ADD,SUB是expression類型,就是處理優先級的問題。
對於C或Java有這樣的一個問題
a+++++b;
我們可以分析為a++ + ++b 為什么編譯器還會報錯呢?是因為我們如果定義優先級的話,++優先級大於+。那么在代碼中就是實現為盡量使++在一起,而不是+優先,如果是+優先的話,那么每次都不會結合為++。所以代碼在詞法分析器階段代碼就會被分割成a ++ ++ + b ;這樣幾段。從而錯誤的。由於詞法分析器和解析器是各自獨立的。又因為詞法分析器先於語法分析器運行。
上面的過程就是這樣進行語法分析的。上面的過程雖然簡單,但是如果用代碼實現就有點困難了。我們使用yacc生成的執行文件就是對上面模擬的執行代碼,使用yacc自動生成的。如果我們要自制編程語言的話,那么這個過程就要自己寫了。因為有很多細節問題。不過不多說了,我們先了解這個就行。生成后的代碼文件是y.tab.c y.tab.h 。生成的代碼有幾十K呢,我們要了解這個過程還是比較難的。
五、用代碼實現詞法分析器
該代碼在calc/llparser目錄下
lexicalanalyzer.c

1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <ctype.h> 4 #include "token.h" 5 6 static char *st_line; 7 static int st_line_pos; 8 9 typedef enum { 10 INITIAL_STATUS, 11 IN_INT_PART_STATUS, 12 DOT_STATUS, 13 IN_FRAC_PART_STATUS 14 } LexerStatus; 15 16 void 17 get_token(Token *token) 18 { 19 int out_pos = 0; 20 LexerStatus status = INITIAL_STATUS; 21 char current_char; 22 23 token->kind = BAD_TOKEN; 24 while (st_line[st_line_pos] != '\0') { 25 current_char = st_line[st_line_pos]; 26 if ((status == IN_INT_PART_STATUS || status == IN_FRAC_PART_STATUS) 27 && !isdigit(current_char) && current_char != '.') { 28 token->kind = NUMBER_TOKEN; 29 sscanf(token->str, "%lf", &token->value); 30 return; 31 } 32 if (isspace(current_char)) { 33 if (current_char == '\n') { 34 token->kind = END_OF_LINE_TOKEN; 35 return; 36 } 37 st_line_pos++; 38 continue; 39 } 40 41 if (out_pos >= MAX_TOKEN_SIZE-1) { 42 fprintf(stderr, "token too long.\n"); 43 exit(1); 44 } 45 token->str[out_pos] = st_line[st_line_pos]; 46 st_line_pos++; 47 out_pos++; 48 token->str[out_pos] = '\0'; 49 50 if (current_char == '+') { 51 token->kind = ADD_OPERATOR_TOKEN; 52 return; 53 } else if (current_char == '-') { 54 token->kind = SUB_OPERATOR_TOKEN; 55 return; 56 } else if (current_char == '*') { 57 token->kind = MUL_OPERATOR_TOKEN; 58 return; 59 } else if (current_char == '/') { 60 token->kind = DIV_OPERATOR_TOKEN; 61 return; 62 } else if (isdigit(current_char)) { 63 if (status == INITIAL_STATUS) { 64 status = IN_INT_PART_STATUS; 65 } else if (status == DOT_STATUS) { 66 status = IN_FRAC_PART_STATUS; 67 } 68 } else if (current_char == '.') { 69 if (status == IN_INT_PART_STATUS) { 70 status = DOT_STATUS; 71 } else { 72 fprintf(stderr, "syntax error.\n"); 73 exit(1); 74 } 75 } else { 76 fprintf(stderr, "bad character(%c)\n", current_char); 77 exit(1); 78 } 79 } 80 } 81 82 void 83 set_line(char *line) 84 { 85 st_line = line; 86 st_line_pos = 0; 87 } 88 89 #if 1 90 void 91 parse_line(char *buf) 92 { 93 Token token; 94 95 set_line(buf); 96 97 for (;;) { 98 get_token(&token); 99 if (token.kind == END_OF_LINE_TOKEN) { 100 break; 101 } else { 102 printf("kind..%d, str..%s\n", token.kind, token.str); 103 } 104 } 105 } 106 107 int 108 main(int argc, char **argv) 109 { 110 char buf[1024]; 111 112 while (fgets(buf, 1024, stdin) != NULL) { 113 parse_line(buf); 114 } 115 116 return 0; 117 } 118 #endif
token.h

1 #ifndef TOKEN_H_INCLUDED 2 #define TOKEN_H_INCLUDED 3 4 typedef enum { 5 BAD_TOKEN, 6 NUMBER_TOKEN, 7 ADD_OPERATOR_TOKEN, 8 SUB_OPERATOR_TOKEN, 9 MUL_OPERATOR_TOKEN, 10 DIV_OPERATOR_TOKEN, 11 END_OF_LINE_TOKEN 12 } TokenKind; 13 14 #define MAX_TOKEN_SIZE (100) 15 16 typedef struct { 17 TokenKind kind; 18 double value; 19 char str[MAX_TOKEN_SIZE]; 20 } Token; 21 22 void set_line(char *line); 23 void get_token(Token *token); 24 25 #endif /* TOKEN_H_INCLUDED */
上面使用的方法是DFA(確定有限狀態自動機)
上面的圖有些指向error的箭頭沒有標出,不過這個圖就大概描述了這個過程。可以自己baidu一些狀態機的知識。
有了這兩章的基礎就可以自己寫個分析器了(作用:以后寫應用程序時,要給程序一個配置文件時,可以自己寫個腳本進行解析,方便用戶書寫配置文件。不過現在都使用xml語法了,都還有解析的庫呢。都不知道學了以后還有沒有機會用到實際中呢)。不過循環和判斷就還不能實現。書中后面有講到,不過看到后面一些內容就有一些力不從心了。感覺難難噠!