原文地址:蘋果梨的博客html
咱們的解析器已經能夠處理基本的加減乘除運算並支持括號了。可是隨着功能愈來愈多,可能出現的錯誤也愈來愈多。不重視錯誤處理的話,碰到非法的表達式時會出現什麼結果,咱們徹底是沒法預料的。因此本章就打個岔,給解析器加上一套錯誤處理邏輯。這知識和編譯原理關係不大,不感興趣的朋友能夠略過。python
本章還會順帶聊一聊負數的解析,用遞歸的方式處理負數能夠作的很簡單,想複雜點也能夠作的很複雜。若是是用調度場算法處理表達式中的負數的話,推薦看看這一篇文章(英文),我就不深刻分析了。負數解析不涉及到編譯原理相關的新知識,不感興趣也能夠略過。git
想要把正在解析的表達式,和解析中遇到的錯誤配對關聯起來,在C語言裏固然是用結構體最方便啦:github
typedef struct {
const char *expStr;
int errType;
} slm_expr;
複製代碼
而後咱們要對如今的代碼作修改,把全部傳遞const char **expStr
參數的地方改爲傳遞slm_expr *e
,固然函數體裏代碼也要作對應的修改。算法
是否是有點熟悉?ObjC裏的objc_msgSend就是這麼玩的,python等部分語言裏也是把self當作類成員函數的第一個參數。express
作完了準備以後咱們就要開始錯誤處理了,以number
函數爲例,咱們須要在出現不指望字符時報錯:bash
int number(slm_expr *e) {
if (*e->expStr < '0' || *e->expStr > '9') {
e->errType = SLM_EXPRESSION_ERROR_TYPE_EXPECT_DIGIT;
return 0;
}
int result = *e->expStr - '0';
(e->expStr)++;
return result;
}
複製代碼
能夠看到咱們報錯的手段就是在結構體裏把errType
標記成對應的錯誤,而後馬上終止解析。固然只終止當前函數的解析是不夠的,上層函數發現下層函數解析出錯了,應該遞歸的終止解析。咱們以expr
函數爲例:app
int expr(slm_expr *e) {
int result = term(e);
if (e->errType) return 0;
while (*e->expStr == '+' || *e->expStr == '-') {
char op = *e->expStr;
(e->expStr)++;
int t = term(e);
if (e->errType) return 0;
if (op == '+') {
result += t;
} else {
result -= t;
}
}
return result;
}
複製代碼
能夠看到每次在調用term
函數後,咱們都須要判斷下它有沒有設置過errType
,有的話就須要遞歸終止解析。固然你們會發現,對errType
的操做都是比較固定的模式,因此咱們用個宏定義來讓代碼看上去簡潔點:函數
#define TRY(func) func; if (e->errType) return 0;
#define THROW(error) e->errType = error; return 0;
複製代碼
用宏定義替換完代碼後,咱們的錯誤處理差很少就作完了,完整代碼參照SlimeExpressionC-chapter3.1。不得不說沒有提供try...catch...
語法的語言寫錯誤處理是多麼的蛋疼😂,若是是用高級語言那麼這段邏輯會優雅不少。固然用goto
語句來實現錯誤處理也是可行的,可是一是難以閱讀,二是容易玩脫,感興趣的朋友能夠本身試試。spa
負號的優先級是怎樣的?咱們來先看一個截圖:
可見在常見的C語言編譯器裏面,負數出如今表達式中間是能夠的,且負號優先級是比乘除法還高的。
關於C語言裏運算符的優先級,你們能夠參考這一篇文章:C運算符優先級
第三行炸了是由於後綴自減運算符優先級是最高的,因此--
被識別成了自減運算符。而自減運算符是不能應用在常量上的,因此出現了編譯錯誤。其實像第四行同樣用空格把兩個減號斷開一下,就又能夠正常編譯了。
我在解析器裏就不打算支持自增自減運算符了,由於我我的十分討厭人問我a---a
到底該解析成什麼,因此從根源上杜絕這個問題。😜
爲何C語言要設計成這樣呢?實際上是由於這樣的設計,對於文法和遞歸解析來講是最容易的。按照這樣的設計,負號應該是數字解析中的一部分,因此咱們把解析數字用的文法改進成這樣:
number -> '-' digit | digit
digit -> '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
複製代碼
這個邏輯十分簡單,咱們就不須要把它拆成兩個函數來寫了,事實證實寫在一個函數裏會更簡潔些:
int number(slm_expr *e) {
int hasMinus = 0;
if (*e->expStr == '-') {
(e->expStr)++;
hasMinus = 1;
}
if (*e->expStr < '0' || *e->expStr > '9') {
THROW(SLM_EXPRESSION_ERROR_TYPE_EXPECT_DIGIT);
}
int result = *e->expStr - '0';
(e->expStr)++;
if (hasMinus) {
result *= -1;
}
return result;
}
複製代碼
是的,支持負數只須要改這麼一個函數,完整的代碼參照:SlimeExpressionC-chapter3.2
上一小節提到的文法是解析負數的最簡單文法,那麼複雜點的場景要怎麼處理?
咱們舉個例子:
1+-1
,1--1
這類寫法總歸不太符合正常習慣
-1+1
,-1-2
,1-(-1)
這類寫法就正常些
總結起來就是,負號應該只出如今一個表達式(expr
)的首個數字裏。若是想要實現這樣的功能,咱們的文法要怎麼設計呢?那可麻煩了去了……
在遞歸邏輯裏,若是想要記住一個狀態,那麼只能一步步的把狀態傳遞下去,一種方式就是用文法進行傳遞,那麼文法大概會設計成這麼個樣子:
expr -> firstTerm {'+' term | '-' term}
firstTerm -> fisrtFactor {'*' factor | '/' factor | '%' factor}
term -> factor {'*' factor | '/' factor | '%' factor}
fisrtFactor -> fisrtNumber | '(' expr ')'
factor -> number | '(' expr ')'
firstNumber -> '-' digit | digit
number -> digit
digit -> '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
複製代碼
看個人表情……每一級向下傳遞都得多寫一個產生式,咱們如今的文法才這麼簡單就直接產生式數量double了,之後出現了函數解析、變量名解析之類的還不得原地爆炸?不敢想不敢想。
固然還有另外一種方式,就是經過context傳遞狀態。在面向對象的語言裏那就是經過實例的屬性/成員變量去傳遞狀態,在咱們的C代碼裏那就是給結構體再加一個布爾值變量isFisrtNumber
咯。
具體的作法就是在進入expr
函數時,把isFisrtNumber
置爲true,在解析完第一個數字後,再把isFisrtNumber
置爲false,只有在isFisrtNumber
爲true的時候,解析數字才支持以負號開始。
等等,那萬一之後咱們支持變量了,i+-1
裏的-1
的確是第一個數字啊,這時候咋辦?改代碼唄,第一個變量解析完以後也把isFisrtNumber
置爲false。
等等,那萬一之後咱們支持函數了,f(1)+-1
裏的-1
好像也有問題啊,咋辦?再改……
等等,那expr
是會嵌套解析的,咱們要不要搞一個堆棧記錄每一層的isFisrtNumber
?……
總之,各類各樣的問題會接踵而至,就是這樣喵。因此呢,你們應該也明白了爲何我說上一小節提到的文法是解析負數的最簡單文法。感興趣的同窗能夠本身試試實現這種複雜的負號解析邏輯,我這裏就不嘗試實現了。今天的入門課也就到這裏,但願能夠拓寬一下你們的思路。