編譯原理_計算器_flex、bison實現_(從零開始)
目標:參考範例程式, 用 Flex 和 Bison 實現一個功能更為強大的計算器,包含以下運算:
a) 加、減、乘、除運算
b) 乘方、開方運算
c) 位運算
– 與 & 、或 |、非 ~…
d) 階乘運算 !
e)sin cos tan
sin(SIN*pi/180.0)//把角度變成弧度即把180度變成π
如果要寫實驗報告的話,請先看報告書的要求,一邊截圖一邊程式設計,免得寫報告的時候,浪費時間。
P.S. 這篇文章只能助你改程式,並不能讓你從零獨自編寫出程式 附贈安裝配置環境的教程(畢竟很多人卡在第一步嘛( ̄▽ ̄)/)
一 環境配置:Windows/Ubuntu+flex、bison
\1 使用Windows+CodeBlocks+flex、bison的環境(有兩種方式)
方式1 藉助codeblcoks編譯、執行。
flex_bison 下載 百度雲密碼:usk6
flex_bison 備用下載連結
1)下載百度雲裡的flex和bison。放到windows環境下。
2)把.l檔案和.y檔案複製到該檔案下
3)在2)資料夾的位址列(也就是下圖畫紅圈的地方),輸入cmd
4)在cmd裡輸入
flex -ocalc.c calc.l
bison -ocalc.tab.h calc.y //注意-o後面沒有空格
這樣,會生成兩個檔案,calc.tab.h 和 calc.c
然後,把生成的.c檔案(calc.c),丟進 codeblocks
方式2 配置MinGW直接在cmd下編譯、執行。
flex_bison 下載 百度雲密碼:usk6
flex_bison 備用下載連結
1)下載百度雲裡的flex和bison。放到windows環境下。
2)把.l檔案和.y檔案複製到該檔案下
3)把%codeblocks%\MinGW\bin新增到 電腦\屬性\高階系統設定\環境變數\PATH(即把codeblocks的編譯器的路徑放到環境變數PATH裡)
4)在2)資料夾的位址列(也就是下圖畫紅圈的地方),輸入cmd
flex calc.l
bison -o calc.tab.h calc.y //注意-o後面有沒有空格都可以 ==
gcc -o aa lex.yy.c calc.tab.h //編譯
aa //執行aa.exe
這種方式,會生成兩個檔案,calc.tab.h 、calc.tab.c 和 calc.c
這樣,就直接在cmd介面,執行程式,而不要通過codeblocks。
\2 使用Ubuntu+flex、bison的環境,來編譯、執行。
vm12+ubuntukylin16.04 虛擬機器安裝ヾ(o◕∀◕)ノヾ (❁´︶`❁)
然後,在ubuntu安裝flex、bison並完成編譯
老版本的ubuntu可能這樣安裝不了,這種情況,我只能說。。。。。重灌一下ubuntu?(逃
ubuntu下開啟終端,安裝flex、bison:
sudo apt-get install flex bison //安裝flex和bison
flex -h
bison -h //如果有提示資訊表示安裝成功
編譯和執行:
cd ........./calcSimple //移動到程式的當前目錄
bison -d calc.y
flex calc.l
/*
-lm在提示pow未定義引用時新增。
編譯lex.yy.c calc.tab.c 用-o輸出到calc
*/
gcc -o calc lex.yy.c calc.tab.c -lm
./calc //執行calc
如果有 正確的 Makefile檔案 的話,直接輸入:
sudo make
./calc
二 一個簡單的示例程式碼calcSimple下載
calc.l
%option noyywrap
%{
/*
* 一個簡單計算器的Lex詞法檔案
*/
void yyerror(char*);
#include "calc.tab.h"
%}
%%
/* a-z為變數 */
[a-z] {
yylval = *yytext - 'a';
return VARIABLE;
}
/* 整數 */
[0-9]+ {
yylval = atoi(yytext);
return INTEGER;
}
/* 運算子 */
[-+()=/*\n] {return *yytext;}
/* 空白被忽略 */
[ \t] ;
/* 其他字元都是非法的 */
. yyerror("無效的輸入字元");
%%
calc.y
%token INTEGER VARIABLE
%left '+' '-'
%left '*' '/'
%{
/*for Visual studio */
/* #define __STDC__ 0 */
#include <stdio.h>
void yyerror(char*);
int yylex(void);
int sym[26];
%}
%%
program:
program statement '\n'
|
;
statement:
expr {printf("%d\n", $1);}
|VARIABLE '=' expr {sym[$1] = $3;}
;
expr:
INTEGER
|VARIABLE{$$ = sym[$1];}
|expr '+' expr {$$ = $1 + $3;}
|expr '-' expr {$$ = $1 - $3;}
|expr '*' expr {$$ = $1 * $3;}
|expr '/' expr {$$ = $1 / $3;}
|'('expr')' {$$ = $2;}
;
%%
void yyerror(char* s)
{
fprintf(stderr, "%s\n", s);
}
int main(void)
{
printf("A simple calculator.\n");
yyparse();
return 0;
}
三 把 calcSimple 修改成 完整版的計算器 全攻略
此小節簡略說明一下.l檔案和.y檔案,如果想更多的瞭解這個程式的意義,請看下文
在calcSimple的基礎上,
在.l檔案裡新增:(注意中/英文的標點符號不一樣)
/* 運算子 */
[-+()=/*!\n] {return *yytext;}
在.y檔案裡新增文法部分:
|expr ‘!’ {int i,s=1;for(int i=1;i<=$2;i++)s*=i;$$=s;}
這裡用到了c語言,所以要在.y程式第二部分即%{}%裡面新增#include<stdio.h>
然後在.y檔案開頭新增 %right ‘!’
這裡表示左/右結合性,以及運算子優先順序,越是在下面優先順序越高
四 非常重要的 兩個學長學姐的 示例程式。
五 理解 .l
檔案和 .y
檔案
\1 查閱龍書(編譯原理)中文第二版(P86和P170 )
(lex和yacc是Unix的軟體,而flex和bison是其在ubantu(linux下)的相容版本)
P86 詳細解釋了flex(lex)軟體的 程式碼。也就是calc.l檔案的詳細解釋
P170 詳細解釋了bison(yacc)軟體的 程式碼。也就是calc.y檔案的詳細解釋
\2 老師課件 上的解釋
連結: 老師課件
2-詞法分析-RE-Lex.pptx
YACC.pptx
實驗-補充-LEX.pdf
\1 詞法分析
首先來看flex的使用:
簡單來說分為兩步:
1 先定義一個flex的輸入檔案,描述詞法。
2 用flex程式處理這個檔案,生成對應的C語言原始碼檔案。
(一般flex的輸入檔案以.l檔案結尾, 比如這個檔案calc.l)
檔案分成三個部分
第一部分是從 %{ 到 }% 標記的部分。
這個部分會原封不動的複製到flex的生成程式碼中。
檔案開頭定義了一個YYSTYPE巨集。
每個TOKEN可以有一個lval值屬性,
YYSTYPE定義型別就是token的lval的型別。
_EasyTData是我們的web服務層和web頁面層公用的通用資料結構。
然後就是一些要include的標頭檔案,第一部分就完了。
lex的輸入檔案的第二部分,是從 % } 到 % % 之間的部分,
這部分用正則表示式定義了一些資料型別。
比如int num string ignore_char identifier等。
注意這裡使用的正則表示式的形式是ERE而不是BRE。
ERE與BRE比較明顯的區別就是,
ERE使用+表示字元重複一次以上,*表示字元重複0次以上。
BRE使用{1,}這種方式表示字元重a
檔案的第三部分,是% % 到% % 的部分。
這裡定義了詞法分析器在解析的處理動作。
yytext是一個flex內部的識別符號,表示匹配到的字串。
上文介紹了,lval也是一個內部識別符號,表示TOKEN的值。
json2tdata_是識別符號的字首, 在執行flex的時候,用-P指定。
flex輸入檔案寫完之後,使用下面這條命令,
就可以把flex的輸入檔案轉換為C語言的原始碼了。
flex calc.l//生成lex.yy.c
\2 語法分析
語法分析是使用bison工具。
使用bison工具也是分為兩步,
第一步寫bison的輸入檔案,第二步用bison程式生成C語言原始碼。
(bison的輸入檔案一般用.y作為字尾名。)
和flex的詞法分析輸入檔案類似,bison的輸入檔案也是分成3部分。
第一部分% {和% }之間,是原封不動拷貝到輸出的C語言原始檔中的。
json2tdata_lex這個函式是flex生成的。
json2tdata_error是用來處理錯誤資訊的函式。
通過定義和實現這個函式你可以把錯誤資訊寫到任何地方。
與flex類似,json2tdata也是自定義的字首。
第二部分是%token INT NUM STRING R_BRACKET COLON
SEMICOLON COMMA IDENTIFIER TRUE FALSE NIL這一行,
這一行的作用就是宣告在flex中定義的那些TOKEN。
第三部分是% % % %包圍的部分。
這部分就是語法的推導過程。
可以比較輕鬆的看出,這部分主要就是採用BNF對語法進行描述。
比如Array, 它有兩種形式。
第一種是 L_BRACKET ELEMENTS R_BRACKET,
第二種則是L_BRACKET R_BRACKET,
這表示一個空的Array。
Bison能夠完全支援LR(1)文法。
這種文法的特點是隻要多向前看一個TOKEN,
就能夠決定如何解析。
因此如果bison告訴你語法ambiguous的時候,
可以想一想如何把自己的文法改成LR(1)型文法。
另外,每一條規則的後面可以用{}來定義解析的動作
bison用$$表示規則左邊的物件,
用$1 $2 $3 等依次表示規則右邊的物件。
七 編譯、執行的時候,常見錯誤以及對策
1) ……shift/reduce conflict……
最常見的情況是:在.l 和.y檔案中沒有新增相應 符號,或者沒有寫優先順序
2) 在原來的只能用整數的示例程式裡新增 小數 的功能
在 .l 和 .y 檔案裡新增 #define YYSTYPE double
在.l檔案裡 atoi(yytext)改為 atof(yytext)
//一般會有錯誤提示,按照錯誤提示一個個改就好了。
為所有用到整數型的地方,新增強制型別轉換 (int)
3) pow的未定義引用
兩種可能
.y檔案裡沒有新增math.h標頭檔案
gcc -o calc lex.yy.c calc.tab.c -lm //沒有新增-lm
原因:Linux下用math.h庫的pow()函式,
gcc編譯的時候報錯返回:對‘pow’未定義的引用
查了下資料,需要在gcc編譯的時候加上-lm引數才能正常編譯。
這是為什麼呢?再查了下資料:
使用math.h中宣告的庫函式還有一點特殊之處,
gcc命令列必須加-lm選項,因為數學函式位於libm.so庫檔案中
(這些庫檔案通常位於/lib目錄下),-lm選項告訴編譯器,
我們程式中用到的數學函式要到這個庫檔案裡找。
本書用到的大部分庫函式(例如printf)位於libc.so庫檔案中,
使用libc.so中的庫函式在編譯時不需要加-lc選項,
當然加了也不算錯,因為這個選項是gcc的預設選項。
以上,如有疏漏,敬請指正。
八 原始碼
a.l 檔案
%{
/*
* 一個簡單計算器的Lex詞法檔案
*/
int yywrap();
#define YYSTYPE double
void yyerror(char*);
#include "a.tab.h"
%}
%%
/* a-z為變數 */
[a-z] {
yylval = *yytext - 'a';
return VARIABLE;
}
/*16進位制數*/
0x\.?[a-f0-9]+|0x[a-f0-9]+\.[a-f0-9]* {
yylval=atof(yytext);
return HEXADECIMAL;
}
/* 整數或者小數 */
\.?[0-9]+|[0-9]+\.[0-9]* {
yylval = atof(yytext);
return INTEGER;
}
/* 運算子 */
[-+()=/*&|~!^@\n] {return *yytext;}
/* 三角函式 */
sin {
return SIN;
}
cos {
return COS;
}
tan {
return TAN;
}
/* 空白被忽略 */
[ \t] ;
/* 其他字元都是非法的 */
. yyerror("無效的輸入字元");
%%
int yywrap()
{return 1;}
a.y 檔案
%token HEXADECIMAL INTEGER VARIABLE SIN COS TAN
%left '+' '-'
%left '*' '/'
%left '&'
%left '|'
%left '^'
%right '@''~'
%left '!'
%{
/*for Visual studio */
/* #define __STDC__ 0 */
#include <stdio.h>
#include <math.h>
#define YYSTYPE double
#define pi 3.1415926
void yyerror(char*);
int yylex(void);
double sym[26];
%}
%%
program:
program statement '\n'
|
;
statement:
expr {printf("%lf\n", $1);}
|VARIABLE '=' expr {sym[(int)$1] = $3;}
;
expr:
INTEGER
|HEXADECIMAL
|VARIABLE{$$ = sym[(int)$1];}
|expr '+' expr {$$ = $1 + $3;}
|expr '-' expr {$$ = $1 - $3;}
|expr '*' expr {$$ = $1 * $3;}
|expr '/' expr {$$ = $1 / $3;}
|expr '&' expr {$$ = (int)$1 & (int)$3;}
|expr '|' expr {$$ = (int)$1 | (int)$3;}
|'~' expr {$$ = ~(int)$2;}
|'@' expr {$$ = sqrt($2);}
|expr '@' expr {$$ = $1*sqrt($3);}
|expr '!' {int i=1,s=1;for(;i<=$2;i++)s*=i;$$=s;}
|expr '^' expr {$$=pow($1,$3);}
|'('expr')' {$$ = $2;}
|SIN'('expr')' {$$ = sin($3*pi/180.0);}
|COS'('expr')' {$$ = cos($3*pi/180.0);}
|TAN'('expr')' {$$ = tan($3*pi/180.0);}
;
%%
void yyerror(char* s)
{
fprintf(stderr, "%s\n", s);
}
int main(void)
{
printf("A simple calculator.\n可以用的運算子:+-*/&|~!^@ \n要注意的是三角函式使用時要加括號。 例:sin(60)\n");
yyparse();
return 0;
}
Makefile 檔案(只在linux下可用,注意檔名得是Makefile,大小寫敏感)
all: prog clean
prog: 1a.l 1a.y
flex 1a.l
bison -d 1a.y
gcc -o a lex.yy.c 1a.tab.c -lm
clean:
rm lex.yy.c 1a.tab.c 1a.tab.h