Skip to content

Commit 355f4b9

Browse files
committed
實作:術的編譯
1 parent 1425e5f commit 355f4b9

File tree

6 files changed

+259
-1
lines changed

6 files changed

+259
-1
lines changed

book/.vitepress/config.mts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ export default defineConfig({
9797
text: "精五真言生成(二)術的施展",
9898
link: "/零.二版/精五真言生成(二)術的施展.md",
9999
},
100+
{
101+
text: "精五真言生成(三)實作:術的編譯",
102+
link: "/零.二版/精五真言生成(三)實作:術的編譯.md",
103+
},
100104
],
101105
},
102106
{
190 Bytes
Loading

book/image/精五棧圖解.png

-129 Bytes
Loading

book/image/精五棧圖解fp.png

581 Bytes
Loading

book/零.二版/精五真言生成(一)棧與區域變數.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## 區域變數
88

9-
咒執行的過程中,一個術可能被施展多次,最簡單的例子就是遞迴術,術中有術,層層嵌套。由於執行次數可能取決於法咒的輸入,編譯器無法知曉究竟一個術在法咒執行過程中究竟會執行幾次,因此不可能在編譯期就將樹中區域變數的記憶體分配完
9+
咒執行的過程中,一個術可能被施展多次,最簡單的例子就是遞迴術,術中有術,層層嵌套。由於執行次數可能取決於法咒的輸入,編譯器無法知曉一個術在法咒執行過程中究竟會執行幾次,因此不可能在編譯期就將術中區域變數的記憶體分配完
1010

1111
那只好在執行期動態分配記憶體了,分配到哪裡呢?[老樣子](../零.一版/精五真言生成.md),放到棧上,畢竟術的施展天然就像棧這種資料結構。
1212

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
本章將結合實際程式碼來一觀術是如何被編譯的。
2+
3+
## 計算棧的大小
4+
5+
究竟要分配多大的棧空間給一個術呢?
6+
7+
```音界
8+
術.甲(子、丑)【
9+
元.天=1
10+
元.地=1
11+
元.玄=1
12+
元.黃=1
13+
14+
```
15+
16+
再回顧這張圖:
17+
18+
![加入參數的精五棧圖解](../image/參數精五棧圖解.png)
19+
20+
兩個參數+四個區域變數+舊棧禎底+返回位址,總共要 8 個字長,在 64 位元系統中,就是 8 * 64 = 256 位元,也就是 64 位元組。
21+
22+
### 考慮區塊
23+
24+
參數數量、舊棧禎底、返回位址需要的空間都是雷打不動,但若法咒中帶有區塊,區域變數的情況會稍微複雜一些:
25+
26+
```音界
27+
術.甲()【
28+
若(...)【
29+
元.天=1
30+
元.地=1
31+
】或若(...)【
32+
元.玄=1
33+
】不然【
34+
元.黃=1
35+
36+
37+
```
38+
39+
「若」語句可以有多分支,但僅有其中一支會執行,因此不需要為每一個分支中的區域變數都分配空間,分的空間夠用量最大的分支使用就好。在上例中,第一條分支有兩個區域變數,其餘分支都僅一個區域變數,那分配兩個整數的空間就足以應付了。
40+
41+
```音界
42+
術.甲()【
43+
若(...)【
44+
元.天=1
45+
元.地=1
46+
47+
48+
// 做其他事
49+
50+
若(...)【
51+
元.玄=1
52+
53+
54+
// 做其他事
55+
56+
若(...)【
57+
元.黃=1
58+
59+
60+
```
61+
即使是獨立的幾個「若」語句也一樣,不同區塊內的區域變數的作用域不相同,不可能同時被使用,這個例子仍然只需要分配兩個整數的空間。
62+
63+
### 實作:計算需分配多少空間給區域變數
64+
```rust
65+
fn 區塊區域變數數量(區塊: &Vec<O句>) -> usize {
66+
let mut 頂層計數 = 0;
67+
let mut 塊內計數 = 0;
68+
69+
forin 區塊 {
70+
match 句 {
71+
O句::變數宣告(_) => {
72+
頂層計數 += 1;
73+
}
74+
O句::若(若) => 塊內計數 = max(塊內計數, 若區域變數數量(若)),
75+
_ => {}
76+
}
77+
}
78+
79+
頂層計數 + 塊內計數
80+
}
81+
82+
fn 若區域變數數量(若: &O若) -> usize {
83+
let mut 計數 = 區塊區域變數數量(&.區塊);
84+
for 或若 in &.或若列表 {
85+
計數 = max(計數, 區塊區域變數數量(&或若.區塊))
86+
}
87+
88+
match &.不然 {
89+
Some(O不然 { 區塊 }) => 計數 = max(計數, 區塊區域變數數量(&區塊)),
90+
None => {}
91+
}
92+
計數
93+
}
94+
95+
pub fn 術內區域變數數量(術: &O術宣告) -> usize {
96+
區塊區域變數數量(&.術體)
97+
}
98+
```
99+
100+
## 術開頭
101+
102+
計算棧的大小後,增長棧(修改`sp`),推入返回地址與舊棧幀底`fp`後。
103+
104+
```rust
105+
writeln!(真言檔, "{}:", 術.術名)?;
106+
107+
let 區域變數數量 = 術內區域變數數量(&術);
108+
109+
let 棧初始大小 = (術.形參.len() + 區域變數數量 + 2) * 字長;
110+
// 增長棧
111+
writeln!(真言檔, "\taddi sp, sp, -{}", 棧初始大小)?;
112+
// 儲存返回地址
113+
writeln!(真言檔, "\tsd ra, {}(sp)", 棧初始大小 - 字長)?;
114+
// 儲存舊棧底(fp)
115+
writeln!(真言檔, "\tsd s0, {}(sp)", 棧初始大小 - 字長 * 2)?;
116+
// 更新 s0 為現在的棧底(s0 就是 fp)
117+
writeln!(真言檔, "\taddi s0, sp, {}", 棧初始大小)
118+
```
119+
120+
## 索引變數位址
121+
122+
當需要擷取變數,必須先知曉變數究竟是在棧中(參數或區域變數)還是在數據段(全域變數),才能夠生成正確的讀寫指令。
123+
124+
貧道在實作中以`O變數位址`來分辨變數所在何處:
125+
126+
```rust
127+
#[derive(Clone, Copy, Display)]
128+
enum O棧中類型 {
129+
區域變數,
130+
實參,
131+
}
132+
use O棧中類型::*;
133+
134+
#[derive(Clone, Copy)]
135+
enum O變數位址 {
136+
全域,
137+
138+
棧中(usize, O棧中類型),
139+
}
140+
141+
impl O變數位址 {
142+
// 從記憶體載入暫存器中
143+
fn 載入(&self, 真言檔: &mut File, 暫存器名: &str, 變數名: &str) -> io::Result<()> {
144+
match self {
145+
O變數位址::全域 => {
146+
writeln!(真言檔, "# 載入全域變數「{}」", 變數名)?;
147+
writeln!(真言檔, "\tld {}, {}", 暫存器名, 變數名)
148+
}
149+
O變數位址::棧中(偏移, 棧中類型) => {
150+
writeln!(真言檔, "# 載入{}「{}」", 棧中類型, 變數名)?;
151+
writeln!(真言檔, "\tld {}, -{}(s0)", 暫存器名, 偏移)
152+
}
153+
}
154+
}
155+
156+
// 從暫存器寫到記憶體
157+
fn 寫出(&self, 真言檔: &mut File, 暫存器名: &str, 變數名: &str) -> io::Result<()> {
158+
match self {
159+
O變數位址::全域 => {
160+
panic!("目前語法不會使全域變數的值被更改");
161+
}
162+
O變數位址::棧中(偏移, 棧中類型) => {
163+
writeln!(真言檔, "# 寫出{}「{}」", 棧中類型, 變數名)?;
164+
writeln!(真言檔, "\tsd {}, -{}(s0)", 暫存器名, 偏移)
165+
}
166+
}
167+
}
168+
}
169+
```
170+
參數與區域變數都在棧中,其擷取方式一致。
171+
172+
### 記錄變數的棧中位址
173+
174+
以符號表來記錄各個變數的所在位置,注意到變數表是一個可持久化 Trie 樹雜湊表,那是因為在區塊中的區域變數可以覆蓋外部變數,但離開區塊之後,區塊內的變數又不再作用,故直接在進入區塊時生成變數表的不可變副本,是最容易的實作方式。
175+
176+
```rust
177+
// 目前不支援術中術
178+
// 符號檢查有通過的話,術一定都存在的,不需要記錄
179+
#[derive(Clone)]
180+
struct O符號表 {
181+
變數表: rpds::HashTrieMap<String, O變數位址>,
182+
計數: usize, // 當下術內有幾個實參跟區域變數
183+
}
184+
185+
impl O符號表 {
186+
fn new() -> Self {
187+
Self {
188+
變數表: HashTrieMap::new(),
189+
計數: 1,
190+
}
191+
}
192+
fn 錄入全域變數(&mut self, 變數名: &String) {
193+
self.變數表.insert_mut(變數名.clone(), O變數位址::全域);
194+
}
195+
fn 錄入棧中變數(&mut self, 變數名: &String, 棧中類型: O棧中類型) {
196+
self.變數表.insert_mut(
197+
變數名.clone(),
198+
O變數位址::棧中(字長 * (self.計數 + 2), 棧中類型),
199+
);
200+
self.計數 += 1;
201+
}
202+
fn 取得變數位址(&self, 變數名: &String) -> O變數位址 {
203+
match self.變數表.get(變數名) {
204+
Some(變數位址) => *變數位址,
205+
None => {
206+
panic!(
207+
"編譯器內部錯誤:未在符號檢查階段檢查到未宣告變數「{}」",
208+
變數名
209+
)
210+
}
211+
}
212+
}
213+
}
214+
```
215+
216+
有了符號表,要記錄變數位址就很容易了,在頂層宣告遇到變數宣告,就將變數錄入全域,在術中遇到,就將變數錄入棧中。
217+
218+
至於參數要在術的開頭就記錄參數位址,並從參數暫存器寫入棧中。
219+
```rust
220+
// 將術的參數加入符號表
221+
// 並將參數從 a0~a7 寫入棧中
222+
for (編號, 參名) in.形參.iter().enumerate() {
223+
符號表.錄入棧中變數(參名, 實參);
224+
符號表
225+
.取得變數位址(參名)
226+
.寫出(真言檔, &format!("a{}", 編號), 參名)?;
227+
}
228+
```
229+
230+
## 計算
231+
232+
計算與零.一版保持一致,依然採用堆疊機,不管完成任何計算,棧會增加一個整數大小的空間,存放計算結果。如若用不到計算結果了,得記得將其彈出棧,以維護棧的大小。
233+
234+
```rust
235+
// 計算結束時,棧頂 = t0 = 計算結果
236+
fn 計算(
237+
真言檔: &mut File, 算式: &O算式, 符號表: &O符號表
238+
) -> io::Result<()> {
239+
match 算式 {
240+
O算式::二元運算(二元運算) => {
241+
Self::計算(真言檔, 二元運算..as_ref(), 符號表)?;
242+
Self::計算(真言檔, 二元運算..as_ref(), 符號表)?;
243+
Self::二元運算(真言檔, &二元運算.運算子)
244+
}
245+
O算式::數字(數) => Self::數字入棧(真言檔, 數),
246+
O算式::變數(變數) => Self::變數入棧(真言檔, 變數, 符號表),
247+
O算式::施術(施術) => Self::施術(真言檔, 施術, 符號表),
248+
}
249+
}
250+
```
251+
252+
相比零.一版,此處增加了一種算式——`施術`,依然可用堆疊機來完成,先分別計算各個實參,將實參的值壓入棧中,等實參都計算完畢後,將實參值載入參數暫存器後,施術(call)。
253+
254+
### 計算施術

0 commit comments

Comments
 (0)