本博客主要講述如何利用編譯原理的知識實現一個控制檯計算器.若是以前利用棧(在學數據結構的時候)實現過計算器,必定會有所印象,寫一個計算器程序最重要的就是把握運算優先級了.而本文換一個角度,利用文法的知識來實現一個功能齊全的計算器.雖然用編譯原理的理論來作計算器實在有點殺雞焉用宰牛刀的味道,但如此實踐確實是有必要的."合抱之木,生於毫末;九層之臺,起於累土;千里之行,始於足下."git
須要說明的是,本文對於不懂編譯原理的人來講基本算是天書了,因此,請仔細閱讀預備知識.github
須要說明的是,越到後面會愈加現,寫一個程序並非你會了幾種語言的問題,語言只是一個工具,沒有紮實的理論,永遠寫不出好的程序.正則表達式
相信這一部分對於熟悉編譯原理的人沒有難度.如下是一個計算器輸入語句的文法定義express
expression : term | expression + term | expression - term;編程
term : primary_expression | term * primary_expression | term / primary_expression;數據結構
primary_expression : DOUBLE_LITERAL | ( expression ) | - primary_expression;app
以上文法直接運用到yacc是沒問題的,由於yacc生成的是一個LALR(1)解析器,但手工構造就沒那麼幸運了,老司機一眼就能看出來,這是一個左遞歸的BNF,因此須要消除左遞歸.編程語言
expression : term half_expression;ide
half_expression : + term half_expression | - term half_expression | ε;函數
term : primary_expression half_term;
half_term : * primary_expression | / primary_expression | ε;
primary_expression : DOUBLE_LITERAL | ( expression ) | - primary_expression;
求一下這個文法的FIRST和FOLLOW集:
構造一下預測分析表就知道是一個LL(1)文法.
以上是簡單的原理分析.
所謂手工構造就是本身編寫程序,這裏使用的是C語言,固然了,其餘語言也能夠.不過這裏多說一句話,若是完整的實現一個編譯器,必然要涉及將源程序轉化爲機器碼,這是個面向底層的工做,十分適合C語言來處理.並且lex/yacc生成的也是C程序,雖然也有了像JavaCC等其餘語言的詞法/語法分析生成器,但仍是建議使用C語言.
趙裕-GitHub-calculator存放了全部代碼,llparser_version目錄下就是手工構造的源代碼.
手工構造通常採用遞歸降低法(也稱遞歸子程序法),每當遇到一個終結符就會調用一個對應的子函數進行解析,這裏不對源代碼作原理性詳細的分析,由於我假設你已經熟悉了理論層面的知識,只是缺少一個實踐的參照,學習理解這個程序最好的辦法就是clone下來,本身進行單步調試,這是最有效的學習辦法.
如下是歸納性的程序說明:
關於lex/yacc這裏不作過多的介紹,學過編譯原理或多或少都知道一點,O'Reilly出版社的Lex&Yacc是爲數很少系統講解這兩個工具的書籍,須要深刻了解的能夠閱讀閱讀.
一樣,相關代碼放在了lex-yacc_version目錄下面,這裏也不直接給出代碼,相對於手工構造的代碼,lex和yacc的代碼都十分易讀(前提是你十分熟悉正則表達式和文法),只是你在使用這兩個工具的時候必須熟悉他們的一套規則,這兩個工具都有些年頭了,因此有些地方或者說有些設計理念可能不是那麼優雅,但他們確實十分強大!
最後,利用這兩個工具生成的C代碼頗有必要打開看看,通常來講詞法分析手工構造尚可,但語法分析利用工具確實省時省力又高效,因此,十分有必要看看到底生成的了怎樣的代碼.
即便不懂lex/yacc,只要按照以下步驟編譯,應該就能獲得C語言的目標代碼:
使用-dv參數是爲了生成一個輔助文件y.output,這個文件包含不少有用信息,並且若是出現了衝突能夠給出詳細的說明.能夠本身打開看看,如下是該文件的一部分:
1 Grammar 2 3 0 $accept: line_list $end 4 5 1 line_list: line 6 2 | line_list line 7 8 3 line: expression CR 9 4 | error CR 10 11 5 expression: term 12 6 | expression ADD term 13 7 | expression SUB term 14 15 8 term: primary_expression 16 9 | term MUL primary_expression 17 10 | term DIV primary_expression 18 19 11 primary_expression: DOUBLE_LITERAL 20 12 | LP expression RP 21 13 | SUB primary_expression 22 23 24 Terminals, with rules where they appear 25 26 $end (0) 0 27 error (256) 4 28 DOUBLE_LITERAL (258) 11 29 ADD (259) 6 30 SUB (260) 7 13 31 MUL (261) 9 32 DIV (262) 10 33 CR (263) 3 4 34 LP (264) 12 35 RP (265) 12 36 37 38 Nonterminals, with rules where they appear 39 40 $accept (11) 41 on left: 0 42 line_list (12) 43 on left: 1 2, on right: 0 2 44 line (13) 45 on left: 3 4, on right: 1 2 46 expression (14) 47 on left: 5 6 7, on right: 3 6 7 12 48 term (15) 49 on left: 8 9 10, on right: 5 6 7 9 10 50 primary_expression (16) 51 on left: 11 12 13, on right: 8 9 10 13 52 53 54 State 0 55 56 0 $accept: . line_list $end 57 58 error shift, and go to state 1 59 DOUBLE_LITERAL shift, and go to state 2 60 SUB shift, and go to state 3 61 LP shift, and go to state 4 62 63 line_list go to state 5 64 line go to state 6 65 expression go to state 7 66 term go to state 8 67 primary_expression go to state 9 68 69 70 State 1 71 72 4 line: error . CR 73 74 CR shift, and go to state 10 75 76 77 State 2 78 79 11 primary_expression: DOUBLE_LITERAL . 80 81 $default reduce using rule 11 (primary_expression) 82 ......
本文介紹了編譯原理在編寫計算器上的實踐,講的很簡略(由於我沒有講解代碼,也沒有一步一步分析原理,畢竟這不是一兩句話的事),充分理解這些在信息的最好方法就是本身對着代碼敲一遍.
很久沒寫博客了,感受以前寫的好多質量都不夠高,但願從本篇開始本身可以以一個更務實的心態寫一些有水平的東西,作一些有深度的總結.
<<自制編程語言>>,前橋和彌.