# 1973年的C編譯器

到現在為止，我們以逐步漸進的方式開發C編譯器。這樣的開發流程在某種意義上，可以說是走過了C語言的歷史。

看看現在的C語言，充滿了意義不清和過於複雜之處，這些部份如果跳脫歷史來看根本無法理解。現今C語言的難解之處，如果看看早期C的程式碼、了解早期C語言的樣子和後來語言與編譯器的發展，會覺得很多地方茅塞頓開。

C在1972年，為了Unix而開始開發。1972、1973年當時的，也就是C語言歷史中極早期的程式原始碼有留在磁帶裡，該檔案有被讀出並公開放在網路上。我們來看看[當時的C編譯器程式碼](https://github.com/qrush/unix/tree/master/src/c)吧。底下的程式碼，是`printf`格式中，接收訊息並編譯為錯誤訊息顯示的函式：

```c
error(s, p1, p2) {
  extern printf, line, fout, flush, putchar, nerror;
  int f;

  nerror++;
  flush();
  f = fout;
  fout = 1;
  printf("%d: ", line);
  printf(s, p1, p2);
  putchar('\n');
  fout = f;
}
```

有種難以言喻的詭異感，看起來像C又不像C。當時的C就是這樣的語言。讀了這段程式碼後，可以注意到：和我們做的早期編譯器一樣，函式的回傳值或引數是沒有型態的。這裡`s`是指向字串的指標、`p1`和`p2`為整數，但是當時的機器上它們全都是同樣的大小，所以變數像這樣是沒有型態的。

第2行中，`error`中寫了所指涉的全域變數和函式宣告。當時的C編譯器沒有標頭檔也沒有前處理器指令，所以程式設計師得像這樣告訴編譯器變數和函式的存在。

和現在我們的編譯器一樣，只確定函式名稱是否存在，並不會檢查引數的個數或型態是否正確。只要把預定數量的引數推進堆疊後，接著跳到函式本體後函式呼叫就算是成功了。

`fout`是持有輸出目標檔案描述子（file descriptor）的全域變數。在當時，還沒有`fprintf`，如果不是想要輸出到標準輸出（standard out，C語言中的`stdout`）而是想輸出到標準錯誤輸出（standard error，C語言中的`stderr`）的話，就得透過全域變數來切換輸出的目標。

`error`裡呼叫了2次`printf`。第2次的`printf`除了格式化的次串之外還傳了2個值。如此一來，在錯誤訊息只取第1個值時該怎麼辦呢？

其實這個error函式就算引數給少了，還是可以正常運作。回想一下，在當時並不存在函式的引數檢查。`s`、`p1`、`p2`這些引數只是單純從堆疊指標起算的第1、2、3個字組（word），實際上有沒有傳`p2`對應的值編譯器並不在意。而`printf`只會根據第1引數字串中的`%d`或`%s`的個數去存取引數，所以如果訊息只包含1個`%d`時，完全不會去存取`p2`。於是引數個數不一致也完全不會有問題。

早期的這種C編譯器，和現階段的 9cc 有許多類似之處。

我們再來看1個例子。底下的程式是會複製傳進來的字串到分配好的靜態記憶體空間，再回傳指向該位址的指標的函式。也就是靜態版的類似`strdup`的函式。

```c
copy(s)
char s[]; {
  extern tsp;
  char tsp[], otsp[];

  otsp = tsp;
  while(*tsp++ = *s++);
  return(otsp);
}
```

當時還沒有發明像`int *p`這樣的宣告。取而代之的是用像`int p[]`來宣告指標。而在函式的引數表和函式名之間插進了像是變數的定義的東西，這是要宣告`s`為指標型態。

這版早期的C編譯器還有其他值得一提的是：

* 這時候還沒有結構（`struct`）。
* 還沒有`&&`和`||`運算子。這時的`&`和`|`在`if`等條件式中會變成邏輯運算，會依照前後文脈絡不同而執行不同操作。
* `+=`運算子寫成`=+`。在這個文法裡，如果打算把`i`用`-1`代入，在沒有加上空格時寫成`i=-1`時，會有被判斷成`i =- 1`這樣產生意料之外操作的問題。
* 整數型態只有`char`和`int`，並沒有`short`和`long`。也缺乏「函式指標的陣列」這種型態的宣告，無法描述複雜的型態。

除此之外，70年代早期的C編譯器還缺乏了很多功能。但是這個C編譯器，從上述的程式碼也可以看出，是用C寫成的。在那個連結構型態都沒有的年代，C語言已經可以自我編譯（self-hosting）了。

從這些古老的程式碼來看，可以猜想C語言中有些難懂的文法為什麼會變成現在的樣子。`extern`、`auto`、`int`、`char`後一定要接變數名稱，這樣的文法在分析時會比較簡單。而表示指標的`[]`也是如果只出現在變數名稱後面的話，分析起來會比較簡單。從這個早期的編譯器所顯示的發展方向來看，好像可以理解為什麼會演變出現在這些非必要、形式複雜的文法。

1973年當時，Unix 和C語言共同的開發者 Dennis Ritchie 所使用的，正是漸進式的開發方法。他在發展C語言的同時，就使用C語言來開發其編譯器。現在的C語言，並不是在持續追加語言的功能的過程中，因為達到了某個里程碑而完成；而是 Dennis Ritchie 在某個時間點，認為其作為語言的機能十分充足了，才被認為該語言完成了。

我們的編譯器也不是在一開始就要追求完成版。完整的C語言本身就不具有什麼特別的意義，特別去追求恐怕也沒什麼意義。持續開發在每一個時間點都具有合理功能的語言，最終成為C語言，這才是原始C編譯器所採用的正統開發方式。我們有信心地繼續前進吧！

{% hint style="info" %}
## 小知識：Rob Pike的程式設計5原則

9cc 有受到 Rob Pike 對程式設計看法的影響。Rob Pike 是C語言作者 Dennis Ritchie 的前同事，也是Go語言的作者，他和 Unix 的作者 Ken Thompson 一起開發了 Unicode 的 UTF-8。

底下引用 Rob Pike 的「程式設計5原則」（Rob Pike's 5 Rules of Programming）：

1. 你無法預期程式的在哪個部份耗時。瓶頸可能出現在出乎意料的之處，在確定瓶頸出在哪之前，不要盲目猜測或動手腳來增強效能。
2. 一定要量測。不要在進行量測之前做最佳化，就算測了也不要對沒有明顯拖累效能的部份進行調整。
3. 精彩的演算法在 n 數量小的時候很慢，而 n 通常很小。精彩的演算法其常數項大。除非你確定 n 通常夠大，不然別走太精彩的路。（就算 n 夠大，也請先試過規則2。）
4. 精彩的演算法比簡單的演算法容易有 bug，而且較難實作。請用簡單的演算法和資料結構。
5. 資料決定一切。如果選對了資料結構且有好好組織資料，演算法通常是自明（self-evident）的。演匴法不是程式設計的中心，資料結構才是。
{% endhint %}
