1. 程式人生 > >編譯原理_計算器_flex、bison實現_(從零開始)

編譯原理_計算器_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
位址列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
位址列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