變量定義
本章中我們用 EBNF 來大致描述我們實現的 C 語言的文法,並實現其中解析變量定義部分。
由於語法分析本身比較複雜,所以我們將它拆分成 3 個部分進行講解,分別是:變量定義、函數定義、表達式。
EBNF 表示
EBNF 是對前一章提到的 BNF 的擴展,它的語法更容易理解,實現起來也更直觀。但真正看起來還是很煩,如果不想看可以跳過。
program ::= {global_declaration}+
global_declaration ::= enum_decl | variable_decl | function_decl
enum_decl ::= 'enum' [id] '{' id ['=' 'num'] {',' id ['=' 'num'} '}'
variable_decl ::= type {'*'} id { ',' {'*'} id } ';'
function_decl ::= type {'*'} id '(' parameter_decl ')' '{' body_decl '}'
parameter_decl ::= type {'*'} id {',' type {'*'} id}
body_decl ::= {variable_decl}, {statement}
statement ::= non_empty_statement | empty_statement
non_empty_statement ::= if_statement | while_statement | '{' statement '}'
| 'return' expression | expression ';'
if_statement ::= 'if' '(' expression ')' statement ['else' non_empty_statement]
while_statement ::= 'while' '(' expression ')' non_empty_statement
其中 expression 相關的內容我們放到後面解釋,主要原因是我們的語言不支持跨函數遞歸,而為了實現自舉,實際上我們也不能使用遞歸(虧我們說了一章的遞歸下降)。
P.S. 我是先寫程序再總結上面的文法,所以實際上它們間的對應關係並不是特別明顯。
解析變量的定義
本章要講解的就是上節文法中的 enum_decl 和 variable_decl 部分。
program()
首先是之前定義過的 program 函數,將它改成:
void program() {
// get next token
next();
while (token > 0) {
global_declaration();
}
}
我知道 global_declaration 函數還沒有出現過,但沒有關係,採用自頂向下的編寫方法就是要不斷地實現我們需要的內容。下面是 global_declaration 函數的內容:
global_declaration()
即全局的定義語句,包括變量定義,類型定義(只支持枚舉)及函數定義。代碼如下:
int basetype; // the type of a declaration, make it global for convenience
int expr_type; // the type of an expression
void global_declaration() {
// global_declaration ::= enum_decl | variable_decl | function_decl
//
// enum_decl ::= 'enum' [id] '{' id ['=' 'num'] {',' id ['=' 'num'} '}'
//
// variable_decl ::= type {'*'} id { ',' {'*'} id } ';'
//
// function_decl ::= type {'*'} id '(' parameter_decl ')' '{' body_decl '}'
int type; // tmp, actual type for variable
int i; // tmp
basetype = INT;
// parse enum, this should be treated alone.
if (token == Enum) {
// enum [id] { a = 10, b = 20, ... }
match(Enum);
if (token != '{') {
match(Id); // skip the [id] part
}
if (token == '{') {
// parse the assign part
match('{');
enum_declaration();
match('}');
}
match(';');
return;
}
// parse type information
if (token == Int) {
match(Int);
}
else if (token == Char) {
match(Char);
basetype = CHAR;
}
// parse the comma seperated variable declaration.
while (token != ';' && token != '}') {
type = basetype;
// parse pointer type, note that there may exist `int ****x;`
while (token == Mul) {
match(Mul);
type = type + PTR;
}
if (token != Id) {
// invalid declaration
printf("%d: bad global declaration\n", line);
exit(-1);
}
if (current_id[Class]) {
// identifier exists
printf("%d: duplicate global declaration\n", line);
exit(-1);
}
match(Id);
current_id[Type] = type;
if (token == '(') {
current_id[Class] = Fun;
current_id[Value] = (int)(text + 1); // the memory address of function
function_declaration();
} else {
// variable declaration
current_id[Class] = Glo; // global variable
current_id[Value] = (int)data; // assign memory address
data = data + sizeof(int);
}
if (token == ',') {
match(',');
}
}
next();
}
看了上面的代碼,能大概理解嗎?這裡我們講解其中的一些細節。
向前看標記
:其中的 if (token == xxx) 語句就是用來向前查看標記以確定使用哪一個產生式,例如只要遇到 enum 我們就知道是需要解析枚舉類型。而如果只解析到類型,如 int identifier 時我們並不能確定 identifier 是一個普通的變量還是一個函數,所以還需要繼續查看後續的標記,如果遇到 ( 則可以斷定是函數了,反之則是變量。
變量類型的表示
:我們的編譯器支持指針類型,那意味著也支持指針的指針,如 int **data;。那麼我們如何表示指針類型呢?前文中我們定義了支持的類型:
// types of variable/function
enum { CHAR, INT, PTR };
所以一個類型首先有基本類型,如 CHAR 或 INT,當它是一個指向基本類型的指針時,如 int *data,我們就將它的類型加上 PTR 即代碼中的:type = type + PTR;。同理,如果是指針的指針,則再加上 PTR。
enum_declaration()
用於解析枚舉類型的定義。主要的邏輯用於解析用逗號(,)分隔的變量,值得注意的是在編譯器中如何保存枚舉變量的信息。
即我們將該變量的類別設置成了 Num,這樣它就成了全局的常量了,而注意到上節中,正常的全局變量的類別則是 Glo,類別信息在後面章節中解析 expression 會使用到。
void enum_declaration() {
// parse enum [id] { a = 1, b = 3, ...}
int i;
i = 0;
while (token != '}') {
if (token != Id) {
printf("%d: bad enum identifier %d\n", line, token);
exit(-1);
}
next();
if (token == Assign) {
// like {a=10}
next();
if (token != Num) {
printf("%d: bad enum initializer\n", line);
exit(-1);
}
i = token_val;
next();
}
current_id[Class] = Num;
current_id[Type] = INT;
current_id[Value] = i++;
if (token == ',') {
next();
}
}
}
其它
其中的 function_declaration 函數我們將放到下一章中講解。match 函數是一個輔助函數:
void match(int tk) {
if (token == tk) {
next();
} else {
printf("%d: expected token: %d\n", line, tk);
exit(-1);
}
}
它將 next 函數包裝起來,如果不是預期的標記則報錯並退出。
代碼
本章的代碼可以在 Github 上下載,也可以直接 clone
git clone -b step-3 https://github.com/lotabout/write-a-C-interpreter
本章的代碼還無法正常運行,因為還有許多功能沒有實現,但如果有興趣的話,可以自己先試著去實現它。
小結
本章的內容應該不難,除了開頭的 EBNF 表達式可能相對不好理解一些,但如果你查看了 EBNF 的具體表示方法後就不難理解了。
剩下的內容就是按部就班地將 EBNF 的產生式轉換成函數的過程,如果你理解了上一章中的內容,相信這部分也不難理解。
下一章中我們將介紹如何解析函數的定義,敬請期待。