1. 程式人生 > >手把手教你做一個 C 語言編譯器(5):變數定義

手把手教你做一個 C 語言編譯器(5):變數定義

本章中我們用 EBNF 來大致描述我們實現的 C 語言的文法,並實現其中解析變數定義部分。

由於語法分析本身比較複雜,所以我們將它拆分成 3 個部分進行講解,分別是:變數定義、函式定義、表示式。

本系列:

EBNF 表示

EBNF 是對前一章提到的 BNF 的擴充套件,它的語法更容易理解,實現起來也更直觀。但真正看起來還是很煩,如果不想看可以跳過。

C
12345678910111213141516171819202122 program::={global_declaration}+global_declaration::=enum_decl|variable_decl|function_declenum_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_statementnon_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 函式,將它改成:

C
1234567 voidprogram(){// get next tokennext();while(token>0){global_declaration();}}

我知道 global_declaration 函式還沒有出現過,但沒有關係,採用自頂向下的編寫方法就是要不斷地實現我們需要的內容。下面是 global_declaration 函式的內容:

global_declaration()

即全域性的定義語句,包括變數定義,型別定義(只支援列舉)及函式定義。程式碼如下:

C
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283 intbasetype;// the type of a declaration, make it global for convenienceintexpr_type;// the type of an expressionvoidglobal_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 '}'inttype;// tmp, actual type for variableinti;// tmpbasetype=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 partmatch('{');enum_declaration();match('}');}match(';');return;}// parse type informationif(token==Int){match(Int);}elseif(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 declarationprintf("%d: bad global declaration\n",line);exit(-1);}if(current_id[Class]){// identifier existsprintf("%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 functionfunction_declaration();}else{// variable declarationcurrent_id[Class]=Glo;// global variablecurrent_id[Value]=(int)data;// assign memory addressdata=data+sizeof(int);}if(token==','){match(',');}}next();}

看了上面的程式碼,能大概理解嗎?這裡我們講解其中的一些細節。

向前看標記 :其中的 if (token == xxx) 語句就是用來向前檢視標記以確定使用哪一個產生式,例如只要遇到 enum 我們就知道是需要解析列舉型別。而如果只解析到型別,如 int identifier 時我們並不能確定 identifier 是一個普通的變數還是一個函式,所以還需要繼續檢視後續的標記,如果遇到 ( 則可以斷定是函數了,反之則是變數。

變數型別的表示 :我們的編譯器支援指標型別,那意味著也支援指標的指標,如 int **data;。那麼我們如何表示指標型別呢?前文中我們定義了支援的型別:

C
12 // types of variable/functionenum{CHAR,INT,PTR};

所以一個型別首先有基本型別,如 CHAR 或 INT,當它是一個指向基本型別的指標時,如int *data,我們就將它的型別加上 PTR 即程式碼中的:type = type + PTR;。同理,如果是指標的指標,則再加上 PTR

enum_declaration()

用於解析列舉型別的定義。主要的邏輯用於解析用逗號(,)分隔的變數,值得注意的是在編譯器中如何儲存列舉變數的資訊。

即我們將該變數的類別設定成了 Num,這樣它就成了全域性的常量了,而注意到上節中,正常的全域性變數的類別則是 Glo,類別資訊在後面章節中解析 expression 會使用到。

C
123456789101112131415161718192021222324252627282930 voidenum_declaration(){// parse enum [id] { a = 1, b = 3, ...}inti;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 函式是一個輔助函式:

C
12345678 voidmatch(inttk){if(token==tk){next();}else{printf("%d: expected token: %d\n",line,tk);exit(-1);}}

它將 next 函式包裝起來,如果不是預期的標記則報錯並退出。

程式碼

本章的程式碼可以在 Github 上下載,也可以直接 clone

1 git clone-bstep-3https://github.com/lotabout/write-a-C-interpreter

本章的程式碼還無法正常執行,因為還有許多功能沒有實現,但如果有興趣的話,可以自己先試著去實現它。

小結

本章的內容應該不難,除了開頭的 EBNF 表示式可能相對不好理解一些,但如果你查看了 EBNF 的具體表示方法後就不難理解了。

剩下的內容就是按部就班地將 EBNF 的產生式轉換成函式的過程,如果你理解了上一章中的內容,相信這部分也不難理解。

下一章中我們將介紹如何解析函式的定義,敬請期待。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式