--- title: C in four function (c4) Compiler tags: Computer Science,System Software description: 2019.08.05 --- C in four function (c4) Compiler === ###### 2019.08.21 *Copyright © 2019 by srhuang* 在閱讀 Compiler 相關材料,到真正自己實作 Compiler 之間,還有一個重要的步驟就是 Code Tracing. 挑選一個經典且重要的 source code 來 trace 會幫助我們從理論層面下到實作層面,但我還是會建議在 Code Tracing 之前還是要有點理論基礎,否則在茫茫程式碼海中,很容易迷失自我的。 c4 Compiler 不僅是個 C self-host compiler,它也啟發了許多人去撰寫自己的 Compiler,甚至是個了解 Compiler 很好的入門磚。關於如何 From Source code to Binary,你將會有著具體的實作方法的脈絡,會輔助我們去了解更多編譯器相關的理論,以及為何要學習數學。讓我們用欣賞程式碼的角度,來把玩一下 c4,這也將是我們之後自幹編譯器的基礎。 ## Source Code [c4](https://github.com/rswier/c4) ## 網友註解 * 原始碼加上註解:https://github.com/comzyh/c4/blob/comment/c4.c * 目前我覺得寫得最好的分析文章:https://www.zhihu.com/question/28249756 * 中間代碼的解釋:https://blog.csdn.net/fishmei2/article/details/50777701 ## 背景知識 * [Operator-precedence parser](https://en.wikipedia.org/wiki/Operator-precedence_parser) * [Recursive descent parser](https://en.wikipedia.org/wiki/Recursive_descent_parser) * [BNF/EBNF](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form) * Compilation: * Lexical Analyzer. * Syntax Analyzer. * Semantic Analyzer. * Code Genarator. ## 擴充 * 利用 c4 實作 JIT Compiler:https://github.com/EarlGray/c4 * AST + Codegen —> c5:https://github.com/rswier/c4/commit/d8e61a829c031d887c203515d0ceb5d54dd7318e * 手把手教你建構 C 語言編譯器:https://github.com/lotabout/write-a-C-interpreter * Jserv AMaCC:https://github.com/jserv/amacc ## 延伸閱讀 * Tiny C Compiler (TCC):https://bellard.org/tcc/ * Human Resource Machine - Tomorrow Corporation:https://tomorrowcorporation.com/humanresourcemachine ## c4 特色 * Self-Host Compiler. * Support three types: INT, CHAR, PTR, pointer to INT or CHAR. * Support if-else and while. * Support enum. * Variable Declarations should NOT be assigned. * Local Variables Declaration 必須在 Function 裡面的最開頭位置。 * single-pass compiler. * 最後生成虛擬機的代碼。 ## c4 不支援項目 * 前置處理器。 * do-while, for, switch. * forward declaration. * struct. * heap allocation. ## Code Analyze ### Tokens ```clike=30 // tokens and classes (operators last and in precedence order) enum { Num = 128, Fun, Sys, Glo, Loc, Id, Char, Else, Enum, If, Int, Return, Sizeof, While, Assign, Cond, Lor, Lan, Or, Xor, And, Eq, Ne, Lt, Gt, Le, Ge, Shl, Shr, Add, Sub, Mul, Div, Mod, Inc, Dec, Brak }; ``` * 為何 enum 是從 128 開始的呢? 主要是因為 [ASCII Code ](https://zh.wikipedia.org/wiki/ASCII) 定義的範圍是 0~127 共 128 字元,所以我們自訂的 token number 必須從 128 開始編號。 ### Virtual Machine (Opcode) ```clike=37 // opcodes enum { LEA ,IMM ,JMP ,JSR ,BZ ,BNZ ,ENT ,ADJ ,LEV ,LI ,LC ,SI ,SC ,PSH , OR ,XOR ,AND ,EQ ,NE ,LT ,GT ,LE ,GE ,SHL ,SHR ,ADD ,SUB ,MUL ,DIV ,MOD , OPEN,READ,CLOS,PRTF,MALC,FREE,MSET,MCMP,EXIT }; ``` Generally, LDR is used to load something from ==memory== into a ==register==, and STR is used to store something from a ==register== to a ==memory== address. 參考資料:[MEMORY INSTRUCTIONS: LOAD AND STORE](https://azeria-labs.com/memory-instructions-load-and-store-part-4/) * sp:stack frame。 * a:register 暫存器。 * bp:概念跟 rbp(32 bits)/ebp(64 bits) 相同。 * pc:program counter,指向當前指令。 ![](https://i.imgur.com/pcp5DfL.png) Opcode | Fully Name | Description :-:|:-:|:-:| LEA |Load Effective Address| 利用 bp 將相對位址存入暫存器 a IMM | Immediate Value | 將 immediate value or global address 存入暫存器 a JMP | Jump | 跳轉至目標指令的絕對位址 JSR | Jump to SubRoutine | Push return address into sp and JMP BZ | Branch if Zero | 後面接的是目標指令的絕對位址 BNZ | Branch if NOT Zero | 後面接的是目標指令的絕對位址 ENT | Enter SubRoutine | Push bp and bp points to sp,sp 會預留 local variables 的位置 ADJ | Stack Adjust | 根據目前 sp 的相對位置 LEV | Leave SubRoutine | sp=bp, bp points to old bp, 讀取 return address to pc, sp++釋放空間 LI | Load INT | 將暫存器 a 所指的位址取值,存入暫存器 a LC | Load CHAR | 將暫存器 a 所指的位址取值,存入暫存器 a SI | Store INT | 將暫存器 a 取值,存入 stack frame sp 所指的位址,然後 sp++ SC | Store CHAR | 將暫存器 a 取值,存入 stack frame sp 所指的位址,然後 sp++,最後再存入暫存器 a PSH | Push into Stack Frame | 將暫存器 a 的值 push into Stack Frame OR-MODE | Arithmetic operation | pop from Stack Frame and calculate with register value, and then restore into regrister. OPEN-MEMCMP | System Call | 使用 Stack Frame sp 當作參數傳遞,並且 pop Stack EXIT | exit program | 利用 Stack Frame sp 當作回傳值 ### Symbol Table 由於 c4 不支援 struct,因此使用 enum 來定義 symbol table(called id for c4)。 ```clike=45 // identifier offsets (since we can't create an ident struct) enum { Tk, Hash, Name, Class, Type, Val, HClass, HType, HVal, Idsz }; ``` ### Initialization Symbol Table and Stack Frame 的大小都是 256KB。 ```clike=346 poolsz = 256*1024; // arbitrary size if (!(sym = malloc(poolsz))) { printf("could not malloc(%d) symbol area\n", poolsz); return -1; } if (!(le = e = malloc(poolsz))) { printf("could not malloc(%d) text area\n", poolsz); return -1; } if (!(data = malloc(poolsz))) { printf("could not malloc(%d) data area\n", poolsz); return -1; } if (!(sp = malloc(poolsz))) { printf("could not malloc(%d) stack area\n", poolsz); return -1; } memset(sym, 0, poolsz); memset(e, 0, poolsz); memset(data, 0, poolsz); ``` * sym:symbol table. * e:Assembly Code(IR) output. * le:last print IR. * sp:Stack Frame. * data:data section. ### Keywords ```clike=356 p = "char else enum if int return sizeof while " "open read close printf malloc free memset memcmp exit void main"; i = Char; while (i <= While) { next(); id[Tk] = i++; } // add keywords to symbol table ``` 根據 enum 從 Char 到 While 一一加入 symbol table 中。在 `next()` function 中 `id` 將會指到 `sym`. ```clike=75 id = sym; ``` 另外,`next()` function 是根據 `p` 所指的 String 做 parsing token 的動作。 ```clike=52 while (tk = *p) { ``` ### next() : Lexical Analyzer 順著 code flow,接下來我們來深入分析 c4 是如何 parsing tokens。 ```clike=48 void next() { char *pp; while (tk = *p) { ``` 最外層的 while 大部分情況都不會是終止條件,主要是由 while 內部直接 return,因此 `next()` 的邏輯是處理完一個 token 後就會直接 return。 ```clike=70 else if ((tk >= 'a' && tk <= 'z') || (tk >= 'A' && tk <= 'Z') || tk == '_') { pp = p - 1; while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_') tk = tk * 147 + *p++; tk = (tk << 6) + (p - pp); id = sym; while (id[Tk]) { if (tk == id[Hash] && !memcmp((char *)id[Name], pp, p - pp)) { tk = id[Tk]; return; } id = id + Idsz; } id[Name] = (int)pp; id[Hash] = tk; tk = id[Tk] = Id; return; } ``` 這段是主要處理 Keywords。第一步先算出該 token 的 hash value,然後利用 hash value and name 找尋 symbol table,如果有找到就回傳該 token enum value;如果找不到就新增 id(symbol),並且設定 token enum value 為 Id(133)。這邊最容易混淆的是 `tk` 這個 variable:在進入此 code block 之前,`tk=*p`;然後在算 hash value 的時候,`tk` 代表的是 hash value;最後要 return 前,`tk` 紀錄的是 token enum value。 ```clike=356 p = "char else enum if int return sizeof while " "open read close printf malloc free memset memcmp exit void main"; i = Char; while (i <= While) { next(); id[Tk] = i++; } // add keywords to symbol table ``` 從 `next()` 新增 id(symbol) 之後,會再重新設定 token enum value。 ```clike=359 i = OPEN; while (i <= EXIT) { next(); id[Class] = Sys; id[Type] = INT; id[Val] = i++; } // add library to symbol table ``` 接下來處理 system call library,從 `next()` 新增 id(symbol) 之後,會再設定 token class, type, and value。 ```clike=360 next(); id[Tk] = Char; // handle void type next(); idmain = id; // keep track of main ``` 處理 `void` and `main` keyword,另外使用 `idmain` 紀錄 `main` `在 symbol table 的位置。 ```clike=363 if (!(lp = p = malloc(poolsz))) { printf("could not malloc(%d) source area\n", poolsz); return -1; } if ((i = read(fd, p, poolsz-1)) <= 0) { printf("read() returned %d\n", i); return -1; } p[i] = 0; close(fd); ``` 讀取 source code 到 `p(lp=p)` 所指的位址,並且關閉 FD。 ```clike=368 // parse declarations line = 1; next(); while (tk) { ``` 在 `main()` 中,以上面這個 while loop 來 parsing tokens,直到 `tk`=‘\0’。 到目前為止,我們已經掌握 Lexical Analyzer 的前置作業,以及載入 source code,和 Parsing 流程。雖然 `next()` 並沒有參數和回傳值,但其實是利用 pointer `p` 當作 input,global variable `tk` 當作 output。接下來要進入 Lexical Anlayzer 的核心探討。 除了前面已經討論過的 keywords parsing 部分,接下來來看看處理 Number 的部分。 ```clike=85 else if (tk >= '0' && tk <= '9') { if (ival = tk - '0') { while (*p >= '0' && *p <= '9') ival = ival * 10 + *p++ - '0'; } else if (*p == 'x' || *p == 'X') { while ((tk = *++p) && ((tk >= '0' && tk <= '9') || (tk >= 'a' && tk <= 'f') || (tk >= 'A' && tk <= 'F'))) ival = ival * 16 + (tk & 15) + (tk >= 'A' ? 9 : 0); } else { while (*p >= '0' && *p <= '7') ival = ival * 8 + *p++ - '0'; } tk = Num; return; } ``` 這三個條件分別處理的是十進位、十六進位、八進位。回傳計算後的 `ival` and `tk=Num`。 這邊處理 16 進位的方式相當有趣,主要是如何把 A~F 轉成 10~15,為何只需要 +9 就可以將 16 進位英文的部份轉成數字?關鍵在於 'A' 的 ASCII code = 65 = '0100 0001',而數字 10 的 2 進位是 '1010',由於 16 進位並不會使用的 MSB 的 4 bits,因此 `(tk & 15)` 濾掉前面 4 bits。觀察從 A 轉成數字 10 LSB 的差距:'1010' - '0001' = '1001' = 9。 ```clike=95 else if (tk == '/') { if (*p == '/') { ++p; while (*p != 0 && *p != '\n') ++p; } else { tk = Div; return; } } ``` 這段是處理 Div(/) or comment(//),回傳`tk=Div`。 ```clike=105 else if (tk == '\'' || tk == '"') { pp = data; while (*p != 0 && *p != tk) { if ((ival = *p++) == '\\') { if ((ival = *p++) == 'n') ival = '\n'; } if (tk == '"') *data++ = ival; } ++p; if (tk == '"') ival = (int)pp; else tk = Num; return; } ``` 這段主要是處理 String,並且將字串存入 data section 中。回傳 data 起始位置 `ival = (int)pp;`,如果 ==tk\=\`== 將 token 設為 Num,`tk=Num`。 值得一提的是,'\n' 需要特別處理,因為在字串處理中,'\n' 會被當成兩個字元,但其實在 ASCII 中他是 LF(換行字元),ASCII code = 10。 ```clike=117 else if (tk == '=') { if (*p == '=') { ++p; tk = Eq; } else tk = Assign; return; } else if (tk == '+') { if (*p == '+') { ++p; tk = Inc; } else tk = Add; return; } else if (tk == '-') { if (*p == '-') { ++p; tk = Dec; } else tk = Sub; return; } else if (tk == '!') { if (*p == '=') { ++p; tk = Ne; } return; } else if (tk == '<') { if (*p == '=') { ++p; tk = Le; } else if (*p == '<') { ++p; tk = Shl; } else tk = Lt; return; } else if (tk == '>') { if (*p == '=') { ++p; tk = Ge; } else if (*p == '>') { ++p; tk = Shr; } else tk = Gt; return; } else if (tk == '|') { if (*p == '|') { ++p; tk = Lor; } else tk = Or; return; } else if (tk == '&') { if (*p == '&') { ++p; tk = Lan; } else tk = And; return; } else if (tk == '^') { tk = Xor; return; } else if (tk == '%') { tk = Mod; return; } else if (tk == '*') { tk = Mul; return; } else if (tk == '[') { tk = Brak; return; } else if (tk == '?') { tk = Cond; return; } else if (tk == '~' || tk == ';' || tk == '{' || tk == '}' || tk == '(' || tk == ')' || tk == ']' || tk == ',' || tk == ':') return; ``` Operator 的部分會轉成 token enum value,其他符號的部分就直接依照 ASCII Code 回傳,這就是為何 token enum value 要從 128 開始,因為不能和 ASCII Code 產生衝突啊! 關於 c4 Lexical Analyzer 的程式碼分析就告一段落了,經過 `next()` 後就完成 token parser,並且取得 token enum value(`tk`)(and`ival` if need)。 ### expr(), stmt() : Syntax Analyzer + Semantic Analyzer + CodeGen #### Background * Syntax Analyzer (Parser) * is the process of analysing a string of symbols, conforming to the rules of a formal grammar(e.g. CFG). * 這一步我們稱為語法分析(==Syntactic analysis==),而負責做這項工作的程式我們稱之為語法分析器(==Parser==)。 * ==Parser== 要做的工作就是,讀取 Scanner 分析出來的 Token,然後建立並返回一棵 ==Parse tree==。 * A ==parse tree== or parsing tree or derivation tree or concrete syntax tree is an ordered, rooted tree that represents the syntactic structure of a string according to some context-free grammar. * Types of Parser * [Top-down parsing](https://en.wikipedia.org/wiki/Top-down_parsing) : LL parsers and ==recursive-descent parser== are examples of top-down parsers. * [Bottom-up parsing](https://en.wikipedia.org/wiki/Bottom-up_parsing) : ==Operator-precedence parser==. * c4 主要是採用 recursive-descent parser (`stmt()` and recursion call in `expr()`),但是在運算式的部分採用 Operator-precedence parser (which is in the `expr()`)。 * More info for Operator-precedence parser : https://en.wikipedia.org/wiki/Operator-precedence_parser * Semantic Analyzer * It usually includes ==type checking==, or makes sure a ==variable is declared== before use which is impossible to describe in the extended Backus–Naur form and thus not easily detected during parsing. * Semantic analysis adds semantic information to the parse tree and ==builds the symbol table==. * This phase performs semantic checks such as ==type checking== (checking for type errors), or ==object binding== (associating variable and function references with their definitions), or ==definite assignment== (requiring all local variables to be initialized before use), rejecting incorrect programs or issuing warnings. [Wiki](https://en.wikipedia.org/wiki/Compiler#Front_end) * c4 在 `next()` Lexical analyzer 的時候就有包含 semantic analyzer,這時候會建立 symbol table;在 `main()` 會檢查是否重複定義;在 `expr()` 會做 ==type checking== and ==undefined variable checking==,這些部分也是屬於 semantic analyzer。 * Virtual Machine (c4) * sp:stack frame。 * a:register 暫存器。 * bp:概念跟 rbp(32 bits)/ebp(64 bits) 相同。 * pc:program counter,指向當前指令。 * instuction:OPCODE。 #### expr() 分成兩個部分:第一部分利用 recursive descent (parsing),檢查 symbol table (semantic Analyzer),直接產生 IR (Code Generator);如果有需要就會進行第二部分,Operator precedence (parsing),主要處理四則運算。 ```clike=137 //第一部分 if (!tk) { printf("%d: unexpected eof in expression\n", line); exit(-1); } ``` 其實更好作法是使用 switch,但是 c4 並不支援 switch,因此才會採用多個 if-else。 ```clike=217 //第二部分 while (tk >= lev) { ``` ##### Num 數字處理 ```clike=139 else if (tk == Num) { *++e = IMM; *++e = ival; next(); ty = INT; } ``` 直接 output IR。 ##### String 字串處理 先來回顧一下 ==next()== 對於字串的處理:將字串存入 data section 中。回傳 data 起始位置 `ival = (int)pp;`。 ```clike=140 else if (tk == '"') { *++e = IMM; *++e = ival; next(); while (tk == '"') next(); data = (char *)((int)data + sizeof(int) & -sizeof(int)); ty = PTR; } ``` 這邊的 data 有特別做 alignment [[Wiki](https://en.wikipedia.org/wiki/Data_structure_alignment)],最後輸出 IR。 ##### Sizeof Operator ```clike=145 else if (tk == Sizeof) { next(); if (tk == '(') next(); else { printf("%d: open paren expected in sizeof\n", line); exit(-1); } ty = INT; if (tk == Int) next(); else if (tk == Char) { next(); ty = CHAR; } while (tk == Mul) { next(); ty = ty + PTR; } if (tk == ')') next(); else { printf("%d: close paren expected in sizeof\n", line); exit(-1); } *++e = IMM; *++e = (ty == CHAR) ? sizeof(char) : sizeof(int); ty = INT; } ``` 直接計算 sizeof 的大小。 ##### 變數處理 ```clike=153 else if (tk == Id) { d = id; next(); if (tk == '(') { next(); t = 0; while (tk != ')') { expr(Assign); *++e = PSH; ++t; if (tk == ',') next(); } next(); if (d[Class] == Sys) *++e = d[Val]; else if (d[Class] == Fun) { *++e = JSR; *++e = d[Val]; } else { printf("%d: bad function call\n", line); exit(-1); } if (t) { *++e = ADJ; *++e = t; } ty = d[Type]; } else if (d[Class] == Num) { *++e = IMM; *++e = d[Val]; ty = INT; } else { if (d[Class] == Loc) { *++e = LEA; *++e = loc - d[Val]; } else if (d[Class] == Glo) { *++e = IMM; *++e = d[Val]; } else { printf("%d: undefined variable\n", line); exit(-1); } *++e = ((ty = d[Type]) == CHAR) ? LC : LI; } } ``` 第一個 `if` 處理 function call,第二個 `else if` 處理 enum,第三個 `else` 處理變數。 ```clike=154 d = id; next(); ``` lookahead 的技巧。 ```clike=155 if (tk == '(') { next(); t = 0; while (tk != ')') { expr(Assign); *++e = PSH; ++t; if (tk == ',') next(); } next(); if (d[Class] == Sys) *++e = d[Val]; else if (d[Class] == Fun) { *++e = JSR; *++e = d[Val]; } else { printf("%d: bad function call\n", line); exit(-1); } if (t) { *++e = ADJ; *++e = t; } ty = d[Type]; } ``` 第一個 `while` 處理參數(argument)的部分,並且產生 push into stack 的 IR;接下來的兩個 `if` 分別處理system call and function call:system call 不做 binding 的動作,只保留 Symbol name;function call 則使用 JSR 指令來跳躍到該函式的真實位址。最後根據參數的數量調整 stack frame pointer。 ```clike=167 else { if (d[Class] == Loc) { *++e = LEA; *++e = loc - d[Val]; } else if (d[Class] == Glo) { *++e = IMM; *++e = d[Val]; } else { printf("%d: undefined variable\n", line); exit(-1); } *++e = ((ty = d[Type]) == CHAR) ? LC : LI; } ``` Local Variable 的部分利用 LEA 將 local variable load 到 register 中,位址是相對位址。Global Variable 放在 data section,value 紀錄的是 data 的位址。最後根據 type 來決定 LC or LI。 ##### 括號處理 ```clike=174 else if (tk == '(') { next(); if (tk == Int || tk == Char) { t = (tk == Int) ? INT : CHAR; next(); while (tk == Mul) { next(); t = t + PTR; } if (tk == ')') next(); else { printf("%d: bad cast\n", line); exit(-1); } expr(Inc); ty = t; } else { expr(Assign); if (tk == ')') next(); else { printf("%d: close paren expected\n", line); exit(-1); } } } ``` 第一個 `if` 處理 type casting,`else` 則是處理括號的優先權。 ##### dereference/address-of ```clike=188 else if (tk == Mul) { next(); expr(Inc); if (ty > INT) ty = ty - PTR; else { printf("%d: bad dereference\n", line); exit(-1); } *++e = (ty == CHAR) ? LC : LI; } else if (tk == And) { next(); expr(Inc); if (*e == LC || *e == LI) --e; else { printf("%d: bad address-of\n", line); exit(-1); } ty = ty + PTR; } ``` recursive-descent call `expr()`,然後 check type 是否為一階 pointer type (semantic analyzer),dereference semantic analyze 的過程為 `ty=ty-PTR`。 address-of 的部分直接把 LI/LC command 砍掉,register 所存的就會是位址;相較於 derefernece 而言,register 存的是真正的值。 ##### Unary Operation ```clike=198 else if (tk == '!') { next(); expr(Inc); *++e = PSH; *++e = IMM; *++e = 0; *++e = EQ; ty = INT; } else if (tk == '~') { next(); expr(Inc); *++e = PSH; *++e = IMM; *++e = -1; *++e = XOR; ty = INT; } ``` `expr()` 回傳的值會存在 register,因此這邊要產生的 IR 要先把結果 Push 回 stack,然後再設定 register value,最後做運算,結果會 restore into register。 ```clike=200 else if (tk == Add) { next(); expr(Inc); ty = INT; } else if (tk == Sub) { next(); *++e = IMM; if (tk == Num) { *++e = -ival; next(); } else { *++e = -1; *++e = PSH; expr(Inc); *++e = MUL; } ty = INT; } ``` 處理 expr 開頭是 +/- 的 case。 ```clike=206 else if (tk == Inc || tk == Dec) { t = tk; next(); expr(Inc); if (*e == LC) { *e = PSH; *++e = LC; } else if (*e == LI) { *e = PSH; *++e = LI; } else { printf("%d: bad lvalue in pre-increment\n", line); exit(-1); } *++e = PSH; *++e = IMM; *++e = (ty > PTR) ? sizeof(int) : sizeof(char); *++e = (t == Inc) ? ADD : SUB; *++e = (ty == CHAR) ? SC : SI; } ``` pre-increment/pre-decrement:透過插入 Push command 將 register 的值存入 stack 中。 `expr()` 第一部分處理的都是 expression 開頭的部分,接著第二部分將會展示如何使用 operator-precedence 處理 operator 優先權。 ##### Assign ```clike=220 if (tk == Assign) { next(); if (*e == LC || *e == LI) *e = PSH; else { printf("%d: bad lvalue in assignment\n", line); exit(-1); } expr(Assign); *++e = ((ty = t) == CHAR) ? SC : SI; } ``` 將暫存器的位址 push 到 stack 中,做完後面的運算後,結果通常都會在暫存器中,因此透過 SC/SI 將暫存器的值存到 stack 中的位址。 ```clike=225 else if (tk == Cond) { next(); *++e = BZ; d = ++e; expr(Assign); if (tk == ':') next(); else { printf("%d: conditional missing colon\n", line); exit(-1); } *d = (int)(e + 3); *++e = JMP; d = ++e; expr(Cond); *d = (int)(e + 1); } ``` 處理類似 `x=y?a:b` 判斷式,保留 BZ 指令後面要填位址的空間,然後進行 recursive descent parser for expression,然後塞入 JMP 指令,且一樣保留後面要填位址的空間,展開最後的 expression 後,再填入要 JMP 的位址。 ```clike=234 else if (tk == Lor) { next(); *++e = BNZ; d = ++e; expr(Lan); *d = (int)(e + 1); ty = INT; } else if (tk == Lan) { next(); *++e = BZ; d = ++e; expr(Or); *d = (int)(e + 1); ty = INT; } ``` 通常牽扯到 Branch/Jump 指令的都會使用類似這種方法:先保留目標位址,等待後面的指令都確定後,才會知道目標位址。 ```clike=236 else if (tk == Or) { next(); *++e = PSH; expr(Xor); *++e = OR; ty = INT; } else if (tk == Xor) { next(); *++e = PSH; expr(And); *++e = XOR; ty = INT; } else if (tk == And) { next(); *++e = PSH; expr(Eq); *++e = AND; ty = INT; } else if (tk == Eq) { next(); *++e = PSH; expr(Lt); *++e = EQ; ty = INT; } else if (tk == Ne) { next(); *++e = PSH; expr(Lt); *++e = NE; ty = INT; } else if (tk == Lt) { next(); *++e = PSH; expr(Shl); *++e = LT; ty = INT; } else if (tk == Gt) { next(); *++e = PSH; expr(Shl); *++e = GT; ty = INT; } else if (tk == Le) { next(); *++e = PSH; expr(Shl); *++e = LE; ty = INT; } else if (tk == Ge) { next(); *++e = PSH; expr(Shl); *++e = GE; ty = INT; } else if (tk == Shl) { next(); *++e = PSH; expr(Add); *++e = SHL; ty = INT; } else if (tk == Shr) { next(); *++e = PSH; expr(Add); *++e = SHR; ty = INT; } ``` 這邊運算子的概念就是先將數值 push into stack frame,然後利用 operator-precedence 展開後面的 expression,展開的結果通常會存在 register,運算相關的 command 通常是拿 register 和 stack 做運算,然後將結果存回 register。 ```clike=247 else if (tk == Add) { next(); *++e = PSH; expr(Mul); if ((ty = t) > PTR) { *++e = PSH; *++e = IMM; *++e = sizeof(int); *++e = MUL; } *++e = ADD; } ``` 這邊同時處理 pointer 的加法:因為一次 add 一個「單位」。 ```clike= int *a; c=a+b; ``` 接下來來看 SUB: ```clike=252 else if (tk == Sub) { next(); *++e = PSH; expr(Mul); if (t > PTR && t == ty) { *++e = SUB; *++e = PSH; *++e = IMM; *++e = sizeof(int); *++e = DIV; ty = INT; } else if ((ty = t) > PTR) { *++e = PSH; *++e = IMM; *++e = sizeof(int); *++e = MUL; *++e = SUB; } else *++e = SUB; } ``` 發現多了一個 `if` 判斷式,這是在處理指標相減計算中間的個數。 ```clike=261 else if (tk == Inc || tk == Dec) { if (*e == LC) { *e = PSH; *++e = LC; } else if (*e == LI) { *e = PSH; *++e = LI; } else { printf("%d: bad lvalue in post-increment\n", line); exit(-1); } *++e = PSH; *++e = IMM; *++e = (ty > PTR) ? sizeof(int) : sizeof(char); *++e = (tk == Inc) ? ADD : SUB; *++e = (ty == CHAR) ? SC : SI; *++e = PSH; *++e = IMM; *++e = (ty > PTR) ? sizeof(int) : sizeof(char); *++e = (tk == Inc) ? SUB : ADD; next(); } ``` post-increment 的做法就是先改變 variable 在 stack 中的值,然後再回復成原來的值,並且存在 register 中,最後再進行運算。 ```clike=272 else if (tk == Brak) { next(); *++e = PSH; expr(Assign); if (tk == ']') next(); else { printf("%d: close bracket expected\n", line); exit(-1); } if (t > PTR) { *++e = PSH; *++e = IMM; *++e = sizeof(int); *++e = MUL; } else if (t < PTR) { printf("%d: pointer type expected\n", line); exit(-1); } *++e = ADD; *++e = ((ty = t - PTR) == CHAR) ? LC : LI; } ``` 這部分是處理 array index 的取值到 register 中,中間必須處理一個 array 單位乘以 index,以計算最終位址。 #### stmt() 利用 recursive-descent 處理 "if-else", "while", "return", "{}", ";"。這部分的 source code 非常直觀,所以就不做贅述了。 ### main() : declaration `main()` 的上半部前面已經討論過了,接下來研究下半部,while-loop 的內部。 ```clike=372 bt = INT; // basetype if (tk == Int) next(); else if (tk == Char) { next(); bt = CHAR; } else if (tk == Enum) { next(); if (tk != '{') next(); if (tk == '{') { next(); i = 0; while (tk != '}') { if (tk != Id) { printf("%d: bad enum identifier %d\n", line, tk); return -1; } next(); if (tk == Assign) { next(); if (tk != Num) { printf("%d: bad enum initializer\n", line); return -1; } i = ival; next(); } id[Class] = Num; id[Type] = INT; id[Val] = i++; if (tk == ',') next(); } next(); } } ``` 前面兩個 `if` 分別處理 INT and CHAR,最後一個 `if` 處理 enum:使用 local variable `i` 來處理 value of enum id。 ```clike=396 while (tk != ';' && tk != '}') { ``` 處理 Global variables and Functions。 ```clike=399 if (tk != Id) { printf("%d: bad global declaration\n", line); return -1; } if (id[Class]) { printf("%d: duplicate global definition\n", line); return -1; } ``` check 是否有重複宣告,是屬於 semantic analyzer 的一部份。 ```clike=403 if (tk == '(') { // function ``` 這邊開始處理 Function declaration。 ```clike=404 id[Class] = Fun; id[Val] = (int)(e + 1); ``` Symbol table 的 class 設定為 Fun 類型,然後將 value 設定為 IR command 的 下個指令(Function 第一個指令)的位址。 ```clike=407 while (tk != ')') { ty = INT; if (tk == Int) next(); else if (tk == Char) { next(); ty = CHAR; } while (tk == Mul) { next(); ty = ty + PTR; } if (tk != Id) { printf("%d: bad parameter declaration\n", line); return -1; } if (id[Class] == Loc) { printf("%d: duplicate parameter definition\n", line); return -1; } id[HClass] = id[Class]; id[Class] = Loc; id[HType] = id[Type]; id[Type] = ty; id[HVal] = id[Val]; id[Val] = i++; next(); if (tk == ',') next(); } ``` 這邊會檢查是否重複宣告 parameter。然後將 Symbol table 的資訊 (Class, type, Val) 備份到 HClass, HType, HVal,這邊是實作在 function 內部更改 local variable's value 將不會影響到 Function 以外的 scope,parameter 也算是 Function local variables。 ```clike=422 loc = ++i; ``` 紀錄 local variable 開始的 order,前面的 order 是 parameter 個數。 ```clike=424 while (tk == Int || tk == Char) { bt = (tk == Int) ? INT : CHAR; next(); while (tk != ';') { ty = bt; while (tk == Mul) { next(); ty = ty + PTR; } if (tk != Id) { printf("%d: bad local declaration\n", line); return -1; } if (id[Class] == Loc) { printf("%d: duplicate local definition\n", line); return -1; } id[HClass] = id[Class]; id[Class] = Loc; id[HType] = id[Type]; id[Type] = ty; id[HVal] = id[Val]; id[Val] = ++i; next(); if (tk == ',') next(); } next(); } ``` 處理 Function Local Variables declaration,一樣會備份 Symbol table 資訊。 ```clike=440 *++e = ENT; *++e = i - loc; while (tk != '}') stmt(); *++e = LEV; ``` 處理 Function 內部的 statement,直到遇到屬於 function 的 `}`。`i-loc` 代表 local variables 的個數。 ```clike=443 id = sym; // unwind symbol table locals while (id[Tk]) { if (id[Class] == Loc) { id[Class] = id[HClass]; id[Type] = id[HType]; id[Val] = id[HVal]; } id = id + Idsz; } ``` 回復所有 local variable 的 symbol table 資訊。 ```clike=453 else { id[Class] = Glo; id[Val] = (int)data; data = data + sizeof(int); } ``` 處理 global declaration,symbol table 的 value 存的是 data section 的位址。 ```clike=463 if (!(pc = (int *)idmain[Val])) { printf("main() not defined\n"); return -1; } if (src) return 0; ``` 最後檢查 `main()` function 是否有 define,以及是否要 run IR command on VM。 ## Conclusion c4 compiler 是個很好理解編譯器實作的入門磚,但還是會建議先把理論基礎建立起來,包含 compilation, assembly, linker, loader, stack frame, function call。這些理論剛開始讀來會相當生硬,但是透過理解 c4 compiler,將會對這些理論有更深刻的認知。 我們盡量 trace 每個細節,但難免有些遺漏之處或是理解錯誤的地方,如有任何建議,請再 Mail [lukyandy3162@gmail.com](mailto:lukyandy3162@gmail.com) 告知我,感謝!