第11步:return
本節會加上return
的支援,讓我們可以編譯底下的程式碼:
a = 3;
b = 5 * 6 - 8;
return a + b / 2;
return
也可以出現在程式的中途。就像普通的C語言,程式執行到最初的return
就會結束,從函式中返回。舉例來說底下的程式會回傳最初return
的值,也就是5:
return 5;
return 8;
為了實作這個功能,我們先來想想加上return
的文法會長什麼樣吧。至今為止我們的陳述都只有式子,新的文法則需要接受return <式子>;
這樣的陳述。所以文法會變成像這樣:
program = stmt*
stmt = expr ";"
| "return" expr ";"
...
實作上,標記解析器、分析器、指令產生器全部都需要逐步修改。
首先讓標記解析器可以識別return
這個標記,以TK_RETURN
型的標記來代表。像return
或while
、int
這類文法上有特別意義的標記(非關鍵字)數量非常有限,像這樣以不同型態來代表不同標記的作法會比較簡潔。
接下來,要判斷標記是否為return
,似乎標記解吸器只要判斷剩餘的輸入字串是否以 return 開頭就可以了,但這樣的話像returnx
這樣的標記會被標記解吸器誤判為return
和x
。所以,除了輸入要以 return 開頭,還需要判斷接下來下一個字不是標記的一部份。
判斷所給定的文字是否為組成標記的字,也就是英文字母、數字或底線的函式如下所示:
int is_alnum(char c) {
return ('a' <= c && c <= 'z') ||
('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') ||
(c == '_');
}
利用這個函式,在tokenize裡加上底下的程式碼,就可以把return
做標記解析為TK_RETURN
型了:
if (strncmp(p, "return", 6) == 0 && !is_alnum(p[6])) {
tokens[i].ty = TK_RETURN;
tokens[i].str = p;
i++;
p += 6;
continue;
}
接著對分析器動手,讓其可以分析包含有TK_RETURN
型的標記列。為此,我們先加上代表return
的結點型態ND_RETURN
。然後,修改讀取陳述的函式,讓其可以分析return
句的文法。如往例,只要把文法直接對應到函式上,就可以成功分析文法了。新的stmt
函式如下所示:
Node *stmt() {
Node *node;
if (consume(TK_RETURN)) {
node = calloc(1, sizeof(Node));
node->kind = ND_RETURN;
node->lhs = expr();
} else {
node = expr();
}
if (!consume(';'))
error_at(tokens[pos].str, "不是';'標記");
return node;
}
ND_RETURN
型的結點只有在這裡可以產生,所以在此我們不寫新的函式,而是直接在該處進行malloc
並設定其值。
最後修改指令產生器,讓其可以輸出對應ND_RETURN
結點的合適指令。新的gen
函式的一部份如下所示:
void gen(Node *node) {
if (node->kind == ND_RETURN) {
gen(node->lhs);
printf(" pop rax\n");
printf(" mov rsp, rbp\n");
printf(" pop rbp\n");
printf(" ret\n");
return;
}
...
上述程式碼的gen(node->lhs)
函式呼叫中,會輸出將作為return
回傳值式子的指令。該段指令應該會在堆疊頂部留下1個值。gen(node->lhs)
後接著的指令,會把該值從堆疊中彈出並放在 RAX,然後從函式中回傳。
到前一章為止所實作的功能,在函式的最後一定會輸出1個ret
指令。照本章所說明的方法實作return
後,就會在return
句以外還會輸出多的ret
指令。也可以這些指令統整起來,但此處為了實作簡單,我們允許其輸出複數的ret
指令。在現在這個時間點,太在意這些瑣碎的細節也沒用,重要的是以實作簡單為優先。雖然能寫難的程式是很有用的能力,但有時讓程式不要變得太難,是更有用的能力。
Last updated
Was this helpful?