# 第1步：創造能編譯1個整數的語言

想像一下最簡單的C語言的子集合。不知道讀者想到的是什麼樣的語言？是只有`main`函式的語言嗎？還是只有只有一個式子所構成的語言？

如果仔細想想，所謂最簡單的子集合，應該是由1個整數所構成的語言。也是在這一步我們要實作的語言。

在這一步所做出的編譯器，會從輸入讀入1個整數，然後輸出將其作為結束碼的組合語言。也就是輸入是像42這樣的整數，讀到之後編譯器會輸出下面這樣的組合語言：

```
.intel_syntax noprefix
.global main

main:
        mov rax, 42
        ret
```

`.intel_syntax noprefix`代表的是在眾多組合語言的寫法中，指定本書所使用的 Intel 語法的組合語言指令。這次製作的編譯器在第一行請務必加上這行。其他行的指令的說明請參照前一章的介紹。

這時讀者可能會想說：「這樣的程式才不算編譯器」，說實話筆者也這樣想。但是，這個程式可以對應由1個整數構成的語言，並且根據該數值輸出對應的程式，照定義來說它有充分的資格能作為一個編譯器。而這樣一個簡單的程式，只要一路改寫很快就能做很複雜的事情了，但首先得先完成這一步。

這一步，從整體開發流程來看，其實非常重要。因為以後的開發是以這一步開發的成果為骨幹進行的。這一步在編譯器本身之外，還會寫 Makefile、製作單元測試（unit test）、設定git repository。我們一個一個來看該怎麼完成這些作業。

此外，本書所做的編譯器的名字叫做 9cc，cc 是 C compiler 的簡稱。9這個數字沒有什麼特別的意義，但是筆者之前所寫的編譯器叫做 8cc，所以作為下一個作品就稱為 9cc。讀者可以照自己的喜好取名，但是請不要想名字想太久就一直沒有開始動手寫。包含 GitHub repository 在內，名字以後可以再改，可以暫時先隨便取一個名字沒有關係。

{% hint style="info" %}

#### 小知識：Intel 語法與 AT\&T 語法

本書所用的 Intel 語法以外，還有 AT\&T 語法這個以 Unix 為中心廣為人知的組合語言寫法。gcc 或 objdump 預設都是輸出 AT\&T 語法的組合語言。

AT\&T 語法的結果是放在第二引數，也就是兩個引數要反過來寫。暫存器需要加上`%`引用符號寫成像`%rax`。數值則要加上`$`寫成像`$42`。

除此之外，參照記憶體也有其獨特的寫法，是用`()`取代`[]`。參考兩者的對比的範例：

```
mov rbp, rsp   // Intel
mov %rsp, %rbp // AT&T

mov rax, 8     // Intel
mov $8, %rax   // AT&T

mov [rbp + rcx * 4 - 8], rax // Intel
mov %rax, -8(rbp, rcx, 4)    // AT&T
```

這次我們要製作的編譯器為了容易閱讀採用 Intel 語法。Intel 指令集的說明也是使用 Intel 語法，所以也有著可以直接照手冊的說明來寫指令的好處。語法的功能 Intel 語法和 AT\&T 語法都是一樣的，無論用哪種方法來寫，輸出的機械碼都一樣。
{% endhint %}

## 製作編譯器本體

編譯器的輸入通常都是檔案，但現階段處理開關檔還稍嫌麻煩，我們直接從指令的第1引數來輸入程式碼。以下是從第1引數取值，再把其加到固定的組合語言指令裡的簡單C程式碼：

{% code title="9cc.c" %}

```c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
  if (argc != 2) {
    fprintf(stderr, "引數數量錯誤\n");
    return 1;
  }

  printf(".intel_syntax noprefix\n");
  printf(".global main\n");
  printf("main:\n");
  printf("  mov rax, %d\n", atoi(argv[1]));
  printf("  ret\n");
  return 0;
}
```

{% endcode %}

建立一個名為 9cc 的資料夾，把上面的程式碼存成 9cc.c 這個檔案放在資料夾內。然後照以下的指令執行 9cc 確認其運作：

```
$ gcc -o 9cc 9cc.c
$ ./9cc 123 > tmp.s
```

第1行是編譯 9cc.c 做出可執行的 9cc 檔案。第二行是輸入123給 9cc 來輸出組合語言，然後把結果寫進 tmp.s 這個檔案裡。現在來看看 tmp.s 的內容：

```
$ cat tmp.s
.intel_syntax noprefix
.global main
main:
  mov rax, 123
  ret
```

輸出結果看來沒什麼問題。如果把這個組合語言檔給組譯器就可以輸出可執行檔了。

在 Unix 裡 cc（或 gcc）不只是 C/C++，同時也是很多語言的前端（front-end），會根據輸入檔案的副檔名來執行對應的編譯器或組譯器。所以，就像編譯 9cc 時一樣，把 .s 副檔名的組合語言檔案輸入給 gcc，就會執行組譯。以下就是執行組譯器輸出執行檔的範例：

```
$ gcc -o tmp tmp.s
$ ./tmp
$ echo $?
123
```

在 shell 中可以用`$?`來使用前一個指令的結束碼。在上面的例子中，顯示和我們給 9cc 的123一致的結果，也就代表程式有正常運作。讀者也試著在123以外從0\~255的範圍內輸入看看（Unix 的程式結束碼的範圍為0\~255），實際確認看看 9cc 正常運作的結果。

## 製作單元測試

寫程式自娛時沒寫過測試的讀者想必也是大有人在，但是本書在擴充編譯器的同時會寫測試程式碼用的程式。雖然一開始可能會覺得寫測試很麻煩，但想必馬上就會感受到測試的重要性了。不寫測試的話，每次都得非得手動操作測試才行，這比寫測試還要來的麻煩多了。

筆者認為寫測試常讓人感到麻煩之處大概是來自於：測試的框架（framework）太過於殺雞用牛刀、測試的觀念常常太過制式而教條化等。例如說 JUnit 這樣的測試框架雖然有很多方便的功能，但是要引進該框架得先記得使用方法，顯得很麻煩。因此，本章我們不會引進測試框架。取而代之的是用 shell 腳本寫一個簡單的手寫「測試框架」來寫測試。

以下就是名為 test.sh 的測試用 shell 腳本。shell 函式`try`會從引數中把輸入值和預期的結果兩個引數抓下來、把9cc的結果拿去組譯、把結果和期待的值做比較。在這個 shell 腳本中，定義完`try`之後，會用0和42兩個值來確認是否有正常編譯：

{% code title="test.sh" %}

```bash
#!/bin/bash
try() {
  expected="$1"
  input="$2"

  ./9cc "$input" > tmp.s
  gcc -o tmp tmp.s
  ./tmp
  actual="$?"

  if [ "$actual" = "$expected" ]; then
    echo "$input => $actual"
  else
    echo "$input => $expected expected, but got $actual"
    exit 1
  fi
}

try 0 0
try 42 42

echo OK
```

{% endcode %}

請把上述內容存成 test.sh，並下`chmod a+x test.sh`來追加執行權限，然後實際執行 test.sh 看看。如果沒有出現什麼錯誤的話，如下最後 test.sh 會顯示 OK 並結束：

```
$ ./test.sh
0 => 0
42 => 42
OK
```

如果有發生錯誤的話，test.sh 不會顯示 OK，而是會顯示失敗的測試預期值和實際上的結果：

```
$ ./test.sh
0 => 0
42 expected, but got 123
```

想要用測試腳本來除錯的時候，下`bash -x`來執行 test.sh 看看。加上`-x`參數時，`bash`會顯示如下的執行紀錄：

```
$ bash -x test.sh
+ try 0 0
+ expected=0
+ input=0
+ gcc -o 9cc 9cc.c
+ ./9cc 0
+ gcc -o tmp tmp.s
+ ./tmp
+ actual=0
+ '[' 0 '!=' 0 ']'
+ try 42 42
+ expected=42
+ input=42
+ gcc -o 9cc 9cc.c
+ ./9cc 42
+ gcc -o tmp tmp.s
+ ./tmp
+ actual=42
+ '[' 42 '!=' 42 ']'
+ echo OK
OK
```

我們在本書所使用的「測試框架」就只是這樣一個 shell 腳本而已。這腳本和 JUnit 等正式的測試框架比起來看起來可能是過於簡單了，但是就是這個簡單的腳本，和 9cc 本體的簡單程度才能取得平衡，所以簡單點才剛剛好。單元測試的重點，其實就只是自己執行自己寫的程式碼、機械化地比較執行結果而已。所以不要想太難，重要的是執行測試。

## 用`make`來建構

這本書從頭到尾，讀者應該會編譯 9cc 上百次、甚至上千次吧。編出 9cc 的執行檔、然後執行測試腳本這個過程不會變，交給工具來做要方便得多。`make`這個指令就是為了這個目的的標準工具。

`make`會在其執行的目錄底下，尋找名為 Makefile 的檔案並讀取，然後執行該檔案裏面的指令。Makefile 由以冒號結尾的規則，和規則所對應的指令列們構成。底下的 Makefile 就是這一步想要執行的指令的自動化：

{% code title="Makefile" %}

```
CFLAGS=-std=c11 -g -static

9cc: 9cc.c

test: 9cc
        ./test.sh

clean:
        rm -f 9cc *.o *~ tmp*

.PHONY: test clean
```

{% endcode %}

請把上述的檔案在 9cc.c 的同一個目錄底下存成名為 Makefile 的檔案。如此一來，就可以執行`make`來編出 9cc、執行`make test`來執行測試了。`make`可以知道檔案之間的相依性，所以每當修改 9cc.c 之後，不需要在`make test`前先執行`make`。只要`make`發現 9cc 比 9cc.c 舊，在執行測試前就會重編 9cc。

`make clean`是清除暫存檔的規則。雖然也可以用rm來手動清除，但是如果不小心砍到不想砍的檔案也很麻煩，所以基於提高效率的目的也寫在 Makefile 裡。

此外，在寫 Makefile 時有件事必須注意的是，縮排必須為 tab。如果是4個或8個空白的話會出錯。雖然這是滿不順手的一個文法，但`make`是在1970年代開發的古老工具，這已經被當成是一個傳統。

## 用 git 做版本管理

本書以 git 作為版本管理系統。通過本書會一步一步地完成編譯器，請為這其中的每一步，都做一個 git commit ，並且寫上 commit message。Commit message 可以用中文寫（譯註：原文為日文）沒有關係，請用1行總結來描述實際上做了什麼樣的變更。如果想要寫1行以上的詳細說明時，在第1行後隔一行的空行後再寫下說明。

用 git 做版本管理的只有大家親手所寫的檔案。執行 9cc 所輸出生成的檔案，只要重新執行指令就會再次產生，不需要列為版本管理的對象。反而，如果把這些檔案列入的話，每一個 commit 會有太多不必要的變更，所以有必要從版本管理中除去，不要讓這些檔案進到 repository 裡。

git 可以在名為 .gitignore 的檔案中，寫要被排除在版本管理之外的檔名格式。在 9cc.c 的同一個目錄底下把地下的內容存成 .gitignore 來讓 git 能無視暫存檔案或編輯器的備份檔案：

{% code title=".gitignore" %}

```
*~
*.o
tmp*
9cc
```

{% endcode %}

第一次使用 git 的讀者們，請告訴 git 你的名字和 email 信箱吧，你跟 git 講的名字和信箱會顯示在 git commit 上。底下是以筆者的名字和信箱設定的範例，請讀者設定自己的名字和信箱：

```
$ git config --global user.name "Rui Ueyama"
$ git config --global user.email "ruiu@cs.stanford.edu"
```

要 commit 到 git 上，首先得先把變更的檔案以`git add`加入。因為這次是最初的 commit，首先先以`git init`新增一個 repository，然後再把至今為止所寫的所有檔案都以`git add`加入：

```
$ git init
Initialized empty Git repository in /home/ruiu/9cc
$ git add 9cc.c test.sh Makefile .gitignore
```

然後，執行`git commit`：

```
$ git commit -m "做出可以編譯一個整數的編譯器"
```

`-m`是用來指定 commit message 的參數。沒有加上`-m`的話，git 會啟動編輯器。執行`git log -p`來確認 commit 有成功：

```
$ git log -p
commit 0942e68a98a048503eadfee46add3b8b9c7ae8b1 (HEAD -> master)
Author: Rui Ueyama <ruiu@cs.stanford.edu>
Date:   Sat Aug 4 23:12:31 2018 +0000

    做出可以編譯一個整數的編譯器

diff --git a/9cc.c b/9cc.c
new file mode 100644
index 0000000..e6e4599
--- /dev/null
+++ b/9cc.c
@@ -0,0 +1,16 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(int argc, char **argv) {
+  if (argc != 2) {
...
```

最後，把做好的 git repository 上傳到 Github 上吧。並沒有一定要上傳到 GitHub 的必要，但也沒有不上傳到 GitHub 的理由，但上傳到 GitHub 可以作為備份。要上傳到 GitHub 的話，先新增一個 repository（範例為以 rui314 帳號新增一個叫 9cc 的 repository），然後依下列指令把該 repository 作為 remote repository 加入：

```
$ git remote add origin git@github.com:rui314/9cc.git
```

然後，執行`git push`的話，就會把手邊的 repository 給 push 到 GitHub 上。執行`git push`之後，請打開瀏覽器看看 GitHub 上你的程式碼有沒有成功上傳。

到此為止第1步的編譯器就做完了。這一步的編譯器以編譯器來說是一個簡單過頭的程式，但包含了所有充份作為一個編譯器的要素。雖然讀者可能還無法相信，但今後我們會對這個編譯器追加功能，讓其成長為成熟的C編譯器。首先請好好品味一下所完成的這一步吧。

> 參考實作： [f722daaaae060611](https://github.com/rui314/chibicc/commit/f722daaaae0606115df4ace5a852da23c1a5b0f3)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://koshizuow.gitbook.io/compilerbook/calculator_level_language/step1.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
