第6步:單項加與單項減

減法運算的-運算子,不只可以像在5-3一樣寫在2項中間,也可以像-3一樣寫在單項前面。同理,+運算子也可以像+3這樣用在沒有左項的時候。像這樣只取單項的運算子稱為「單項運算子」(unary operator)。相對的,取2項的運算子就稱為「2項運算子」(binary operator)。

C語言中,除了+-以外,也有取得指標位址的&和提取指標(dereference)的*這些單項運算子。不過這一步我們只會實作+-這兩個而已。

單項+和單項-雖然和2項的+-是一樣的符號,但是定義不同。2項的-的定義是從左邊減去右邊,但是單項根本沒有左邊,照搬2項-的定義的話會無法適用。C語言的單項-是定義為把右邊的正負號反轉的運算。而單項+則是把右邊直接傳回的運算子,這個運算子不是必要的,是和單項-成對存在的贈品。

我們可以合理想像,像+和-這樣,有單項和2項相似但不同定義的同名運算子有很多個。是單項還是2項則需要看前後文脈絡來區分。底下是包含單項+/-的新文法:

expr  = mul ("+" mul | "-" mul)*
mul   = unary ("*" unary | "/" unary)*
unary = ("+" | "-")? term
term  = num | "(" expr ")"

上述文法追加了unary這個非終端符號,mul不是用上term而是改為unaryX?是代表選擇性的,也就是說,X會出現0次或1次的 EBNF 語法。unary = ("+" | "-")? term這條規則的意思,是表示unary這個非終端符號,不論有或沒有1個+/-符號其後都會接上term

我們來驗證-3-(3+5)-3*+5這類式子可以和新的文法對應。底下是-3*+5的語法樹:

我們來依照這個文法修改分析器。照慣例,只要把文法照搬成函式的呼叫我們的分析器應該就改好了。分析unary的函式如下所示:

Node *unary() {
  if (consume('+'))
    return term();
  if (consume('-'))
    return new_node(ND_SUB, new_node_num(0), term());
  return term();
}

在這個階段,分析器把+xx替換、-x0-x替換。所以在這一步我們不用修改指令產生器。

加上幾條測試、和加上單項+/-的程式碼一起 commit 後,這一步就做完了。寫測試的時候,要注意測試的結果要落在0~255之間。在這一步,請利用像-10+20這樣,用上了單項-但是整體的結果是正數的算式。

參考實作: bb5fe99dbad62c95

小知識:單項加減和文法的好壞

單項+運算子在原始的C編譯器裡並不存在,是在1989年 ANSI(美國國家標準協會)在制定C語言標準時,官方才加上去的。因為有單項-,確實有單項+會提升對稱性,但實際上單項+並沒有什麼用處。

同時,在文法裡加上單項+有其副作用。不習慣C語言的人可能會誤把+=運算子錯寫成i =+ 3。如果沒有單項+的話會回報語法錯誤,但是因為有單項+所以這會被解釋為i = +3,編譯器會以為這是正確的代入式默默地接受。像這樣,在擴充文法時發生沒想到的副作用是在設計語言時很常發生的事。

ANSI 制定C語言標準的小組,應該是了解上述問題的前提之下決定加上單項+的,不過讀者們會怎麼想呢?如果你是C語言標準小組的一員,你會贊成還是反對呢?

Last updated