用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(4)- 語法分析1:EBNF和遞歸下降文法
用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(1)- 目標和前言
用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(2)- 簡介和設計
用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(3)- 詞法分析
項目github地址及源碼:
https://github.com/yunwei37/tryC
這一章開始進入解釋器的核心部分: 語法分析器;
我們來看看兩個概念,EBNF和遞歸下降文法,以及如何用這兩個方法來計算tryC中的表達式。
基本概念
就像之前所說的那樣,語法分析指將詞法分析得到的標記流(token)進行分析,組成事先定義好的有意義的語句。那麼如何完成這樣一個工作呢?我們可以藉助一個叫“BNF”的數學工具。
BNF與上下文無關文法
Backus-Naur符號(就是衆所周知的BNF或Backus-Naur Form)是描述語言的形式化的數學方法,由John Backus (也許是Peter Naur)開發,最早用於描述Algol 60編程語言的語法。
BNF類似一種數學遊戲:從一個符號開始(叫做起始標誌,實例中常用S表示),然後給出替換前面符號的規則。
BNF語法定義的語言是一個字符串集合,可以按照下述規則書寫,這些規則叫做書寫規範(產生式規則),例如一個四則運算表達式可以表示爲:
exp -> exp op exp | ( exp ) | number
op -> + | - | * | /
其中’|'用於表示可選擇的不同項,"->"用於表示推導規則,從產生式左邊的符號可以推導出產生式右邊的符號;
要解析一個表達式,我們可以完成這樣一個替換:對於
(3+2)*4
可以替換爲
exp => exp op exp => exp * number
=> ( exp ) * number
=> ( exp op exp ) * number
=> ( number + number ) * number
其中我們把能夠被替換的符號叫做非終結符,不能被替換的叫做終結符。
上下文無關文法就是說,這個文法中所有的產生式左邊只有一個非終結符,就像上面寫的那個文法一樣。通常我們在編譯器構建中使用的都是上下文無關文法。
EBNF
EBNF是基本巴科斯範式(BNF)元語法符號表示法的一種擴展,主要對BNF中常見的兩種情況,即重複項和可選項添加了相應的語法規則,如用方括號"[ … ]"
表示可選部分,用花括號"{ … }"表示重複出現的部分,如上面那個文法可以改寫爲:
exp -> exp { op exp } | ( exp ) | number
op -> + | - | * | /
又比如對於if語句可以寫成:
if-stmt -> if ( exp ) statement; [ else statement; ]
遞歸下降文法
遞歸下降分析法也很簡單,就是用函數的調用來模擬BNF的替換過程,我們只需要爲每個非終結符定義一個分解函數,它就能從起始非終結符開始,不斷地調用非終結符的分解函數,不斷地對非終結符進行分解,直到匹配輸入的終結符。
當然,遞歸下降分析並不是對於所有的文法都能正常使用的,例如經典的左遞歸問題:比如這樣一個文法
exp -> exp { op exp } | ( exp ) | number
op -> + | - | * | /
對於exp的替換需要調用exp的分解函數,而exp的分解函數一進來就調用它自身(即最左邊的符號),就會導致無限遞歸。這時就需要對文法進行改造。
實際上,EBNF文法就是爲了映射遞歸下降分析法的具體程序實現而設計的,因此我們這裏就用EBNF文法來實現遞歸下降分析。
來看看怎樣用遞歸下降文法計算tryC中的表達式
上面說了一大堆,現在看看實際的計算表達式的實現是怎樣的呢
算術表達式
tryC中需要計算四則運算表達式的EBNF文法如下:
exp -> term { addop term }
term -> factor { mulop factor }
factor -> number | ( exp )
addop -> + | -
mulop -> * | /
這裏對文法進行了一定的改造,讓它能夠正確表達四則運算的優先級,同時避免了左遞歸的問題,具體可以自己試着驗證一下。
tryC中算術表達式具體的代碼實現(就是上述文法直接轉換過來的啦):
double term() {
double temp = factor();
while (token == '*' || token == '/') {
if (token == '*') {
match('*');
temp *= factor();
}
else {
match('/');
temp /= factor();
}
}
return temp;
}
double factor() {
double temp = 0;
if (token == '(') {
match('(');
temp = expression();
match(')');
}
else if(token == Num || token == Char){
temp = token_val.val;
match(Num);
}
else if (token == Sym) {
temp = token_val.ptr->value;
match(Sym);
}
else if (token == FuncSym) {
return function();
}
else if (token == ArraySym) {
symbol* ptr = token_val.ptr;
match(ArraySym);
match('[');
int index = (int)expression();
if (index >= 0 && index < ptr->value) {
temp = ptr->pointer.list[index].value;
}
match(']');
}
return temp;
}
double expression() {
double temp = term();
while (token == '+' || token == '-') {
if (token == '+') {
match('+');
temp += term();
}
else {
match('-');
temp -= term();
}
}
return temp;
}
布爾表達式
tryC中同樣設計了布爾表達式:
tryC中需要計算四則運算表達式的EBNF文法如下:
boolOR = boolAND { '||' boolAND }
boolAND = boolexp { '||' boolexp }
boolexp -> exp boolop exp | ( boolOR ) | !boolexp
boolop -> > | < | >= | <= | ==
代碼實現:
int boolexp() {
if (token == '(') {
match('(');
int result = boolOR();
match(')');
return result;
}
else if (token == '!') {
match('!');
return boolexp();
}
double temp = expression();
if (token == '>') {
match('>');
return temp > expression();
}
else if (token == '<') {
match('<');
return temp < expression();
}
else if (token == GreatEqual) {
match(GreatEqual);
return temp >= expression();
}
else if (token == LessEqual) {
match(LessEqual);
return temp <= expression();
}
else if (token == Equal) {
match(Equal);
return temp == expression();
}
return 0;
}
int boolAND() {
int val = boolexp();
while (token == AND) {
match(AND);
if (val == 0) return 0; // short cut
val = val & boolexp();
if (val == 0) return 0;
}
return val;
}
int boolOR() {
int val = boolAND();
while (token == OR) {
match(OR);
if (val == 1) return 1; // short cut
val = val | boolAND();
}
return val;
}
一些重要概念回顧
- 終結符/非終結符
- BNF/EBNF
- 上下文無關文法
- 遞歸下降法
可對照源碼查看
https://github.com/yunwei37/tryC