Lox 語言
你能為某人做的最美好的事情是什麼,比為他們做早餐更好?
安東尼·波登
我們將在本本書的剩餘部分闡明 Lox 語言的每一個陰暗角落,但讓您立即開始為直譯器編寫程式碼,而沒有至少瞥見我們最終將獲得的東西,這似乎很殘酷。
同時,我不想在您接觸文字編輯器之前,就拖著您看完大量的語言法規和規格說明。因此,這將是對 Lox 的溫和友好的介紹。它將省略許多細節和邊角案例。我們稍後有足夠的時間來處理這些。
3 . 1你好,Lox
這是您對Lox的第一個體驗
// Your first Lox program! print "Hello, world!";
正如 `//` 行註解和尾隨的分號所暗示的那樣,Lox 的語法是 C 語言家族的成員。(字串周圍沒有括號,因為 `print` 是一個內建陳述式,而不是一個函式庫函式。)
現在,我不會聲稱 C 有一個*很棒*的語法。如果我們想要一些優雅的東西,我們可能會模仿 Pascal 或 Smalltalk。如果我們想要完全的斯堪的納維亞家具簡約風格,我們會使用 Scheme。這些都有它們的優點。
C 語言的語法反而具有您在語言中通常會發現更有價值的東西:*熟悉度*。我知道您已經很熟悉這種風格,因為我們將用來*實作* Lox 的兩種語言—Java 和 C—也繼承了它。為 Lox 使用類似的語法可以減少您需要學習的東西。
3 . 2高階語言
雖然這本書的篇幅比我希望的要長,但它仍然不夠大到可以容納像 Java 這樣的大型語言。為了在這幾頁中容納 Lox 的兩個完整實作,Lox 本身必須非常精簡。
當我想到小型但有用的語言時,我想到的是像 JavaScript、Scheme 和 Lua 這樣的高階「腳本」語言。在這三種語言中,Lox 看起來最像 JavaScript,主要是因為大多數 C 語法的語言都這樣。正如我們稍後將學到的那樣,Lox 的作用域方法與 Scheme 非常相似。我們在 第三部分中建立的 Lox C 風格很大程度上歸功於 Lua 的乾淨、高效的實作。
Lox 與這三種語言還有其他兩個共同點
3 . 2 . 1動態型別
Lox 是動態型別的。變數可以儲存任何型別的值,單個變數甚至可以在不同的時間儲存不同型別的值。如果您嘗試對錯誤型別的值執行操作—例如,用數字除以字串—那麼該錯誤將在執行時被檢測和報告。
有很多理由喜歡靜態型別,但它們並不能抵消為 Lox 選擇動態型別的務實原因。靜態型別系統需要大量的工作來學習和實作。跳過它會讓您得到一個更簡單的語言和一本更短的書。如果我們將型別檢查延遲到執行時,我們的直譯器將更快地啟動並執行程式碼片段。
3 . 2 . 2自動記憶體管理
高階語言的存在是為了消除容易出錯的低階苦工,還有什麼比手動管理儲存體的分配和釋放更乏味?沒有人會在早晨陽光升起時興奮地說:「我等不及要找出今天我分配的每個記憶體位元組的正確 `free()` 呼叫位置了!」
有兩種主要的記憶體管理技術:引用計數和追蹤垃圾收集(通常簡稱為垃圾收集或 GC)。引用計數器實作起來簡單得多—我認為這就是為什麼 Perl、PHP 和 Python 最初都使用它們的原因。但是,隨著時間的推移,引用計數的局限性變得太麻煩了。所有這些語言最終都添加了完整的追蹤 GC,或者至少足夠多的追蹤 GC 來清理物件週期。
追蹤垃圾收集具有可怕的聲譽。在原始記憶體層級工作*確實*有點令人痛苦。除錯 GC 有時會讓您在夢中看到十六進位傾印。但是,請記住,這本書是關於消除魔法和殺死那些怪物,所以我們*將*編寫自己的垃圾收集器。我認為您會發現該演算法非常簡單且實作起來很有趣。
3 . 3資料型別
在 Lox 的小宇宙中,組成所有物質的原子是內建的資料型別。只有幾個
-
布林值。沒有邏輯您就無法編寫程式碼,沒有布林值您就無法進行邏輯運算。「真」和「假」,軟體的陰陽。與一些重新利用現有型別來表示真和假的古代語言不同,Lox 有一個專用的布林型別。我們可能在這項探險中很艱苦,但我們不是*野蠻人*。
顯然有兩個布林值,每個值都有一個文字。
true; // Not false. false; // Not *not* false.
-
數字。 Lox 只有一種數字:雙精度浮點數。由於浮點數也可以表示範圍廣泛的整數,這涵蓋了很大的範圍,同時保持了簡單性。
功能齊全的語言有很多數字語法—十六進位、科學記號、八進位,各種有趣的東西。我們將採用基本的整數和小數文字。
1234; // An integer. 12.34; // A decimal number.
-
字串。 我們已經在第一個範例中看到了一個字串文字。像大多數語言一樣,它們都用雙引號括起來。
"I am a string"; ""; // The empty string. "123"; // This is a string, not a number.
正如我們在實作它們時看到的那樣,在那個看似無害的 字元序列中隱藏著相當多的複雜性。
-
Nil。 最後一個內建值從未被邀請參加派對,但似乎總是出現。它表示「無值」。在許多其他語言中,它被稱為「null」。在 Lox 中,我們將其拼寫為 `nil`。(當我們實作它時,這將有助於區分我們談論的是 Lox 的 `nil` 還是 Java 或 C 的 `null`。)
語言中沒有空值是有很好的理由的,因為空指標錯誤是我們行業的禍害。如果我們正在使用靜態型別語言,那麼嘗試禁止它是值得的。但是,在動態型別的語言中,消除它通常比擁有它更煩人。
3 . 4運算式
如果內建資料型別及其文字是原子,那麼運算式一定是分子。其中大多數您都會很熟悉。
3 . 4 . 1算術
Lox 具有您從 C 和其他語言中了解和喜歡的基本算術運算子
add + me; subtract - me; multiply * me; divide / me;
運算子兩側的子運算式是運算元。因為有*兩個*,所以這些稱為二元運算子。(它與「二元」的 1 和 0 用法無關。)因為運算子是 固定在運算元*之間*,所以這些也稱為中綴運算子(相對於前綴運算子,其中運算子位於運算元之前,以及後綴運算子,其中運算子位於運算元之後)。
一個算術運算子實際上是中綴和前綴運算子。` - ` 運算子也可以用於對數字求負。
-negateMe;
所有這些運算子都適用於數字,將任何其他型別傳遞給它們都是錯誤的。例外的是 ` + ` 運算子—您也可以將兩個字串傳遞給它來串連它們。
3 . 4 . 2比較和相等
繼續,我們還有一些總是傳回布林結果的運算子。我們可以使用古老的比較運算子來比較數字(並且只能比較數字)。
less < than; lessThan <= orEqual; greater > than; greaterThan >= orEqual;
我們可以測試任何種類的兩個值是否相等或不相等。
1 == 2; // false. "cat" != "dog"; // true.
甚至是不同的型別。
314 == "pi"; // false.
不同型別的值*永遠*不相等。
123 == "123"; // false.
我通常反對隱式轉換。
3 . 4 . 3邏輯運算子
非運算子,一個前綴 `!`,如果其運算元為 true,則傳回 `false`,反之亦然。
!true; // false. !false; // true.
其他兩個邏輯運算子實際上是以運算式形式出現的控制流程結構。一個 `and` 運算式決定兩個值是否都為 true。如果左運算元為 false,則傳回左運算元,否則傳回右運算元。
true and false; // false. true and true; // true.
而 `or` 運算式決定兩個值(或兩者)中的*任何一個*是否為 true。如果左運算元為 true,則傳回左運算元,否則傳回右運算元。
false or false; // false. true or false; // true.
and
和 or
像是控制流程結構的原因是它們會進行短路求值。and
不僅會在左運算元為假時回傳左運算元,在這種情況下甚至不會計算右運算元。相反地(反向推論?),如果 or
的左運算元為真,則會跳過右運算元。
3.4.4優先順序和分組
所有這些運算子的優先順序和結合性都與您從 C 語言中預期的一致。(當我們進入語法分析時,我們會更精確地說明這一點。)在優先順序不符合您期望的情況下,您可以使用 ()
來對內容進行分組。
var average = (min + max) / 2;
由於它們在技術上不是很重要,我已經從我們的小型語言中刪除了其餘典型的運算子組合。沒有位元、移位、模數或條件運算子。我不會為您評分,但如果您在自己實作的 Lox 中新增它們,我會給您加分。
這些是表達式形式(除了幾個與我們稍後將介紹的特定功能相關的形式),所以讓我們更上一層樓。
3.5陳述式
現在我們來看陳述式。表達式的主要工作是產生一個值,而陳述式的工作是產生一個效果。由於根據定義,陳述式不會評估為一個值,因此要使其有用,它們必須以其他方式改變世界—通常是修改某些狀態、讀取輸入或產生輸出。
您已經看過幾種類型的陳述式。第一個是
print "Hello, world!";
print
陳述式會評估一個單一表達式,並將結果顯示給使用者。您也看過一些像是這樣的陳述式
"some expression";
在表達式後面加上分號 (;
) 會將表達式提升為陳述式。這被稱為(相當有想像力地),表達式陳述式。
如果您想在預期只有一個陳述式的地方放入一系列陳述式,您可以將它們包裝在區塊中。
{ print "One statement."; print "Two statements."; }
區塊也會影響作用域,這會將我們帶到下一節 . . .
3.6變數
您可以使用 var
陳述式來宣告變數。如果您省略初始化式,則變數的值預設為 nil
。
var imAVariable = "here is my value"; var iAmNil;
一旦宣告完成,您自然可以使用其名稱來存取和指派變數。
var breakfast = "bagels"; print breakfast; // "bagels". breakfast = "beignets"; print breakfast; // "beignets".
我不會在這裡深入探討變數作用域的規則,因為我們將在後續章節中花費大量的時間來詳細了解這些規則。在大多數情況下,它的運作方式與您從 C 或 Java 中預期的一樣。
3.7控制流程
如果您無法跳過一些程式碼或執行某些程式碼多次,就很難編寫有用的程式。這表示需要控制流程。除了我們已經涵蓋的邏輯運算子之外,Lox 還直接從 C 語言中提取了三個陳述式。
if
陳述式會根據某個條件執行兩個陳述式中的一個。
if (condition) { print "yes"; } else { print "no"; }
while
迴圈會在條件表達式評估為 true 時重複執行主體。
var a = 1; while (a < 10) { print a; a = a + 1; }
最後,我們有 for
迴圈。
for (var a = 1; a < 10; a = a + 1) { print a; }
此迴圈與先前的 while
迴圈執行相同的操作。大多數現代語言也有某種 for-in
或 foreach
迴圈,用於顯式迭代各種序列類型。在真正的語言中,這比我們這裡得到的粗糙 C 樣式 for
迴圈要好得多。Lox 保持基本。
3.8函式
函式呼叫表達式看起來與在 C 語言中的相同。
makeBreakfast(bacon, eggs, toast);
您也可以呼叫函式而不傳遞任何內容給它。
makeBreakfast();
與 Ruby 等語言不同,在這種情況下,括號是強制性的。如果您省略它們,則名稱不會呼叫該函式,而只是引用它。
如果不能定義自己的函式,語言就沒有什麼樂趣。在 Lox 中,您可以使用 fun
來執行此操作。
fun printSum(a, b) { print a + b; }
現在是澄清一些術語的好時機。有些人會將「參數 (parameter)」和「引數 (argument)」混用,對許多人來說,它們是可以互換的。我們將花費大量時間在語意方面進行最細微的區分,因此讓我們精確地使用我們的詞語。從現在開始
-
引數是您在呼叫函式時傳遞給函式的實際值。因此,函式呼叫會有一個引數列表。有時您會聽到 實際參數 (actual parameter) 用於表示這些。
-
參數是一個變數,它會在函式主體內保留引數的值。因此,函式宣告會有一個參數列表。其他人稱這些為 形式參數 (formal parameter) 或簡稱為 形式 (formal)。
函式的主體始終是一個區塊。在其中,您可以使用 return
陳述式來回傳一個值。
fun returnSum(a, b) { return a + b; }
如果執行達到區塊的末尾而沒有遇到 return
,則它會隱式回傳 nil
。
3.8.1閉包
函式在 Lox 中是一級的,這僅表示它們是可以取得引用、儲存在變數中、傳遞等等的真實值。這樣可以運作
fun addPair(a, b) { return a + b; } fun identity(a) { return a; } print identity(addPair)(1, 2); // Prints "3".
由於函式宣告是陳述式,您可以在另一個函式內部宣告區域函式。
fun outerFunction() { fun localFunction() { print "I'm local!"; } localFunction(); }
如果結合區域函式、一級函式和區塊作用域,您會遇到這種有趣的情況
fun returnFunction() { var outside = "outside"; fun inner() { print outside; } return inner; } var fn = returnFunction(); fn();
在這裡,inner()
存取在其主體外部、外圍函式中宣告的區域變數。這樣可以嗎?現在,許多語言都從 Lisp 中借鑒了此功能,您可能知道答案是肯定的。
為了使其運作,inner()
必須「保留」對其使用的任何外圍變數的參照,以便它們在外層函式回傳後仍然存在。我們稱執行此操作的函式為閉包。現在,這個詞通常用於任何一級函式,但如果該函式恰好沒有封閉任何變數,則有點用詞不當。
您可以想像,實作這些會增加一些複雜性,因為我們不能再假設變數作用域的運作方式嚴格像是堆疊,其中區域變數會在函式回傳時立即消失。我們將有一個有趣的時光來學習如何正確且有效地使它們運作。
3.9類別
由於 Lox 具有動態型別、詞法(大致為「區塊」)作用域和閉包,因此它大約有一半是函數式語言。但是您將會看到,它也大約有一半是物件導向語言。這兩種範例都有很多優點,因此我認為值得涵蓋其中一些。
由於類別因未能達到其炒作而受到批評,因此首先讓我解釋一下我為何將它們放入 Lox 和本書中。實際上,有兩個問題
3.9.1為什麼任何語言都可能想要是物件導向的?
現在,像 Java 這樣的物件導向語言已經銷售一空,只在體育館表演,所以不再流行喜歡它們了。為什麼有人會用物件建立新的語言?那不是像在 8 音軌上發行音樂一樣嗎?
確實,90 年代的「所有繼承,所有時間」的狂潮產生了一些可怕的類別層次結構,但物件導向程式設計 (OOP) 仍然相當棒。數十億行的成功程式碼是用 OOP 語言編寫的,將數百萬個應用程式發送給開心的使用者。如今,可能大多數在職程式設計師都在使用物件導向語言。他們不可能都那麼錯誤。
特別是對於動態型別語言而言,物件相當方便。我們需要某種方式來定義複合資料類型,以將一堆東西捆綁在一起。
如果我們也可以將方法附加到這些物件上,那麼我們就無需在我們所有的函式前加上它們操作的資料類型名稱,以避免與不同類型的類似函式發生衝突。例如,在 Racket 中,您最終必須將函式命名為 hash-copy
(複製雜湊表)和 vector-copy
(複製向量),以免它們相互衝突。方法的作用域限定於物件,因此該問題消失了。
3.9.2為什麼 Lox 是物件導向的?
我可以聲稱物件很棒,但它們仍然超出本書的範圍。大多數程式語言書籍,特別是那些試圖實作整個語言的書籍,都會忽略物件。對我來說,這表示這個主題沒有被充分涵蓋。在這樣一個廣泛使用的範式下,這種遺漏讓我感到難過。
鑑於我們許多人整天都在使用物件導向程式語言,世界似乎可以多了解一些關於如何製作它們的文件。你會發現,它其實相當有趣。沒有你想像的那麼困難,但也沒有你認為的那麼簡單。
3 . 9 . 3類別或原型
當談到物件時,實際上存在兩種方法:類別和原型。類別先出現,並且由於 C++、Java、C# 和其他類似語言而更為常見。原型是一種幾乎被遺忘的分支,直到 JavaScript 意外地統治了世界。
在基於類別的語言中,有兩個核心概念:實例和類別。實例儲存每個物件的狀態,並具有對該實例類別的參考。類別包含方法和繼承鏈。要調用實例上的方法,總是會有一層間接層。你查找實例的類別,然後你在那裡找到方法。

基於原型的語言合併了這兩個概念。只有物件—沒有類別—而且每個單獨的物件都可能包含狀態和方法。物件可以直接相互繼承(或在原型術語中“委派給”)。

這表示在某些方面,基於原型的語言比類別更為基礎。它們的實作非常巧妙,因為它們非常簡單。此外,它們可以表達許多不尋常的模式,而類別會引導你避開這些模式。
但我看過許多以基於原型的語言編寫的程式碼—包括我自己設計的一些程式碼。你知道人們通常如何使用原型的所有功能和靈活性嗎? . . . 他們用它們來重新發明類別。
我不知道為什麼會這樣,但人們似乎自然而然地偏好基於類別(經典?優雅?)的風格。原型在語言中確實更簡單,但它們似乎只通過將複雜性推給使用者來實現這一點。因此,對於 Lox,我們將為我們的使用者省去麻煩,直接內建類別。
3 . 9 . 4Lox 中的類別
有足夠的理由了,讓我們看看我們實際上擁有哪些。類別在大多數語言中包含一組功能。對於 Lox,我選擇了我認為最亮的星星。你可以像這樣宣告一個類別及其方法
class Breakfast { cook() { print "Eggs a-fryin'!"; } serve(who) { print "Enjoy your breakfast, " + who + "."; } }
類別的主體包含其方法。它們看起來像函式宣告,但沒有 fun
關鍵字。執行類別宣告時,Lox 會建立一個類別物件並將其儲存在以類別名稱命名的變數中。就像函式一樣,類別在 Lox 中也是一級的。
// Store it in variables. var someVariable = Breakfast; // Pass it to functions. someFunction(Breakfast);
接下來,我們需要一種建立實例的方法。我們可以加入某種 new
關鍵字,但為了保持簡單,在 Lox 中,類別本身就是實例的工廠函式。像函式一樣調用類別,它會產生一個新的自身實例。
var breakfast = Breakfast(); print breakfast; // "Breakfast instance".
3 . 9 . 5實例化和初始化
僅具有行為的類別並不是非常有用。物件導向程式設計背後的概念是將行為和狀態封裝在一起。為此,你需要欄位。像其他動態型別語言一樣,Lox 允許你自由地將屬性添加到物件上。
breakfast.meat = "sausage"; breakfast.bread = "sourdough";
如果欄位不存在,則賦值給欄位會建立它。
如果你想從方法內部存取目前物件上的欄位或方法,請使用舊有的 this
。
class Breakfast { serve(who) { print "Enjoy your " + this.meat + " and " + this.bread + ", " + who + "."; } // ... }
將資料封裝在物件中一部分是確保物件在建立時處於有效狀態。為此,你可以定義一個初始化器。如果你的類別有名為 init()
的方法,則會在建構物件時自動調用該方法。傳遞給類別的任何參數都會轉發到其初始化器。
class Breakfast { init(meat, bread) { this.meat = meat; this.bread = bread; } // ... } var baconAndToast = Breakfast("bacon", "toast"); baconAndToast.serve("Dear Reader"); // "Enjoy your bacon and toast, Dear Reader."
3 . 9 . 6繼承
每個物件導向語言不僅允許你定義方法,還允許你在多個類別或物件之間重複使用它們。為此,Lox 支援單一繼承。當你宣告類別時,你可以使用小於(<
)運算子指定它從哪個類別繼承。
class Brunch < Breakfast { drink() { print "How about a Bloody Mary?"; } }
在這裡,Brunch 是**衍生類別**或**子類別**,而 Breakfast 是**基礎類別**或**父類別**。
父類別中定義的每個方法也可供其子類別使用。
var benedict = Brunch("ham", "English muffin"); benedict.serve("Noble Reader");
甚至 init()
方法也會被繼承。實際上,子類別通常也希望定義自己的 init()
方法。但是也需要調用原始方法,以便父類別可以維護其狀態。我們需要某種方法來調用我們自己實例上的方法,而不會影響我們自己的方法。
與 Java 一樣,你使用 super
來實現這一點。
class Brunch < Breakfast { init(meat, bread, drink) { super.init(meat, bread); this.drink = drink; } }
這就是物件導向的全部內容。我試圖將功能集保持在最小限度。本書的結構確實迫使我做出了一個妥協。Lox 不是純粹的物件導向語言。在真正的 OOP 語言中,每個物件都是類別的實例,即使是數字和布林值等原始值也是如此。
由於我們在開始使用內建型別之後才實作類別,因此這會很困難。因此,原始型別的值並不是類別實例意義上的真實物件。它們沒有方法或屬性。如果我試圖讓 Lox 成為適合真實使用者的真實語言,我會修正這一點。
3 . 10標準函式庫
我們快完成了。這就是整個語言,所以剩下的就是「核心」或「標準」函式庫—直接在直譯器中實作的功能集,並且所有使用者定義的行為都基於此而建立。
這是 Lox 最可悲的部分。它的標準函式庫超出了簡約主義的範疇,並接近徹底的虛無主義。對於本書中的範例程式碼,我們只需要證明程式碼正在執行並按照其應有的方式執行即可。為此,我們已經有了內建的 print
陳述式。
稍後,當我們開始最佳化時,我們將編寫一些基準測試,並查看執行程式碼需要多長時間。這表示我們需要追蹤時間,因此我們將定義一個內建函式 clock()
,該函式返回自程式啟動以來經過的秒數。
並且 . . . 就是這樣了。我知道,對吧?這令人尷尬。
如果你想將 Lox 變成一種實際有用的語言,那麼你應該做的第一件事就是充實它。字串操作、三角函式、檔案 I/O、網路,甚至從使用者讀取輸入都會有所幫助。但是我們不需要本書的任何內容,而且添加它不會教你任何有趣的東西,所以我省略了它。
別擔心,我們將在語言本身中有很多令人興奮的東西讓我們忙碌。
挑戰
-
編寫一些範例 Lox 程式並執行它們(你可以使用我的儲存庫中的 Lox 實作)。嘗試提出我在此處未指定的邊緣情況行為。它是否符合你的預期?為什麼或為什麼不?
-
這個非正式的介紹遺漏了很多未指定的內容。列出你對該語言語法和語意的一些開放性問題。你認為答案應該是什麼?
-
Lox 是一種非常小的語言。你認為它缺少哪些功能,這會使其難以用於實際程式?(當然,除了標準函式庫之外。)
設計注意事項:運算式和陳述式
Lox 同時具有運算式和陳述式。有些語言省略了後者。相反,它們將宣告和控制流程結構也視為運算式。這些「一切都是運算式」的語言往往具有函式式血統,包括大多數 Lisp、SML、Haskell、Ruby 和 CoffeeScript。
為此,對於語言中的每個「類似陳述式」的結構,你需要決定它評估為什麼值。其中一些很容易
-
if
運算式會評估為選擇的任何分支的結果。同樣,switch
或其他多向分支會評估為選擇的任何情況。 -
變數宣告會評估為變數的值。
-
區塊會評估為序列中最後一個運算式的結果。
有些會變得有點奇怪。迴圈應該評估為什麼?CoffeeScript 中的 while
迴圈會評估為一個陣列,其中包含主體評估的每個元素。如果你不需要陣列,這可能很方便,也可能浪費記憶體。
你還必須決定這些類似陳述式的運算式如何與其他運算式組合—你必須將它們放入語法的優先順序表中。例如,Ruby 允許
puts 1 + if true then 2 else 3 end + 4
這是否符合你的預期?它是否符合你的使用者的預期?這會如何影響你設計「陳述式」語法的方式?請注意,Ruby 有一個明確的 end
來告知 if
運算式何時完成。如果沒有它,+ 4
很可能會被解析為 else
子句的一部分。
將每個敘述都轉為表達式會迫使你回答一些棘手的問題,就像上面那樣。相對地,你也會消除一些冗餘。C 語言同時擁有區塊(block)來排序敘述,以及逗號運算子(comma operator)來排序表達式。它也有 if
敘述和 ?:
條件運算子。如果 C 語言中所有東西都是表達式,你就可以將它們統一起來。
捨棄敘述的語言通常也具有隱式回傳—函式會自動回傳其程式碼區塊所評估出的值,而不需要明確的 return
語法。對於小型函式和方法來說,這非常方便。事實上,許多擁有敘述的語言都加入了像 =>
這樣的語法,以便定義其程式碼區塊是單一表達式評估結果的函式。
但讓所有函式都以這種方式運作可能會有點奇怪。如果你不小心,你的函式即使僅打算產生副作用,也會洩漏回傳值。然而,實際上,這些語言的使用者並不覺得這是個問題。
對於 Lox 語言,我加入敘述是基於實際的考量。我為了熟悉度而選擇了類似 C 語言的語法,而且試圖將現有的 C 語言敘述語法解釋為表達式會很快變得非常奇怪。