Skip to content

Commit

Permalink
實作:術的編譯
Browse files Browse the repository at this point in the history
  • Loading branch information
MROS committed Oct 16, 2024
1 parent 1425e5f commit 355f4b9
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 1 deletion.
4 changes: 4 additions & 0 deletions book/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export default defineConfig({
text: "精五真言生成(二)術的施展",
link: "/零.二版/精五真言生成(二)術的施展.md",
},
{
text: "精五真言生成(三)實作:術的編譯",
link: "/零.二版/精五真言生成(三)實作:術的編譯.md",
},
],
},
{
Expand Down
Binary file modified book/image/參數精五棧圖解.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified book/image/精五棧圖解.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified book/image/精五棧圖解fp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## 區域變數

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

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

Expand Down
254 changes: 254 additions & 0 deletions book/零.二版/精五真言生成(三)實作:術的編譯.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
本章將結合實際程式碼來一觀術是如何被編譯的。

## 計算棧的大小

究竟要分配多大的棧空間給一個術呢?

```音界
術.甲(子、丑)【
元.天=1
元.地=1
元.玄=1
元.黃=1
```

再回顧這張圖:

![加入參數的精五棧圖解](../image/參數精五棧圖解.png)

兩個參數+四個區域變數+舊棧禎底+返回位址,總共要 8 個字長,在 64 位元系統中,就是 8 * 64 = 256 位元,也就是 64 位元組。

### 考慮區塊

參數數量、舊棧禎底、返回位址需要的空間都是雷打不動,但若法咒中帶有區塊,區域變數的情況會稍微複雜一些:

```音界
術.甲()【
若(...)【
元.天=1
元.地=1
】或若(...)【
元.玄=1
】不然【
元.黃=1
```

「若」語句可以有多分支,但僅有其中一支會執行,因此不需要為每一個分支中的區域變數都分配空間,分的空間夠用量最大的分支使用就好。在上例中,第一條分支有兩個區域變數,其餘分支都僅一個區域變數,那分配兩個整數的空間就足以應付了。

```音界
術.甲()【
若(...)【
元.天=1
元.地=1
// 做其他事
若(...)【
元.玄=1
// 做其他事
若(...)【
元.黃=1
```
即使是獨立的幾個「若」語句也一樣,不同區塊內的區域變數的作用域不相同,不可能同時被使用,這個例子仍然只需要分配兩個整數的空間。

### 實作:計算需分配多少空間給區域變數
```rust
fn 區塊區域變數數量(區塊: &Vec<O句>) -> usize {
let mut 頂層計數 = 0;
let mut 塊內計數 = 0;

forin 區塊 {
match 句 {
O句::變數宣告(_) => {
頂層計數 += 1;
}
O句::若(若) => 塊內計數 = max(塊內計數, 若區域變數數量(若)),
_ => {}
}
}

頂層計數 + 塊內計數
}

fn 若區域變數數量(若: &O若) -> usize {
let mut 計數 = 區塊區域變數數量(&.區塊);
for 或若 in &.或若列表 {
計數 = max(計數, 區塊區域變數數量(&或若.區塊))
}

match &.不然 {
Some(O不然 { 區塊 }) => 計數 = max(計數, 區塊區域變數數量(&區塊)),
None => {}
}
計數
}

pub fn 術內區域變數數量(術: &O術宣告) -> usize {
區塊區域變數數量(&.術體)
}
```

## 術開頭

計算棧的大小後,增長棧(修改`sp`),推入返回地址與舊棧幀底`fp`後。

```rust
writeln!(真言檔, "{}:", 術.術名)?;

let 區域變數數量 = 術內區域變數數量(&術);

let 棧初始大小 = (術.形參.len() + 區域變數數量 + 2) * 字長;
// 增長棧
writeln!(真言檔, "\taddi sp, sp, -{}", 棧初始大小)?;
// 儲存返回地址
writeln!(真言檔, "\tsd ra, {}(sp)", 棧初始大小 - 字長)?;
// 儲存舊棧底(fp)
writeln!(真言檔, "\tsd s0, {}(sp)", 棧初始大小 - 字長 * 2)?;
// 更新 s0 為現在的棧底(s0 就是 fp)
writeln!(真言檔, "\taddi s0, sp, {}", 棧初始大小)
```

## 索引變數位址

當需要擷取變數,必須先知曉變數究竟是在棧中(參數或區域變數)還是在數據段(全域變數),才能夠生成正確的讀寫指令。

貧道在實作中以`O變數位址`來分辨變數所在何處:

```rust
#[derive(Clone, Copy, Display)]
enum O棧中類型 {
區域變數,
實參,
}
use O棧中類型::*;

#[derive(Clone, Copy)]
enum O變數位址 {
全域,

棧中(usize, O棧中類型),
}

impl O變數位址 {
// 從記憶體載入暫存器中
fn 載入(&self, 真言檔: &mut File, 暫存器名: &str, 變數名: &str) -> io::Result<()> {
match self {
O變數位址::全域 => {
writeln!(真言檔, "# 載入全域變數「{}」", 變數名)?;
writeln!(真言檔, "\tld {}, {}", 暫存器名, 變數名)
}
O變數位址::棧中(偏移, 棧中類型) => {
writeln!(真言檔, "# 載入{}「{}」", 棧中類型, 變數名)?;
writeln!(真言檔, "\tld {}, -{}(s0)", 暫存器名, 偏移)
}
}
}

// 從暫存器寫到記憶體
fn 寫出(&self, 真言檔: &mut File, 暫存器名: &str, 變數名: &str) -> io::Result<()> {
match self {
O變數位址::全域 => {
panic!("目前語法不會使全域變數的值被更改");
}
O變數位址::棧中(偏移, 棧中類型) => {
writeln!(真言檔, "# 寫出{}「{}」", 棧中類型, 變數名)?;
writeln!(真言檔, "\tsd {}, -{}(s0)", 暫存器名, 偏移)
}
}
}
}
```
參數與區域變數都在棧中,其擷取方式一致。

### 記錄變數的棧中位址

以符號表來記錄各個變數的所在位置,注意到變數表是一個可持久化 Trie 樹雜湊表,那是因為在區塊中的區域變數可以覆蓋外部變數,但離開區塊之後,區塊內的變數又不再作用,故直接在進入區塊時生成變數表的不可變副本,是最容易的實作方式。

```rust
// 目前不支援術中術
// 符號檢查有通過的話,術一定都存在的,不需要記錄
#[derive(Clone)]
struct O符號表 {
變數表: rpds::HashTrieMap<String, O變數位址>,
計數: usize, // 當下術內有幾個實參跟區域變數
}

impl O符號表 {
fn new() -> Self {
Self {
變數表: HashTrieMap::new(),
計數: 1,
}
}
fn 錄入全域變數(&mut self, 變數名: &String) {
self.變數表.insert_mut(變數名.clone(), O變數位址::全域);
}
fn 錄入棧中變數(&mut self, 變數名: &String, 棧中類型: O棧中類型) {
self.變數表.insert_mut(
變數名.clone(),
O變數位址::棧中(字長 * (self.計數 + 2), 棧中類型),
);
self.計數 += 1;
}
fn 取得變數位址(&self, 變數名: &String) -> O變數位址 {
match self.變數表.get(變數名) {
Some(變數位址) => *變數位址,
None => {
panic!(
"編譯器內部錯誤:未在符號檢查階段檢查到未宣告變數「{}」",
變數名
)
}
}
}
}
```

有了符號表,要記錄變數位址就很容易了,在頂層宣告遇到變數宣告,就將變數錄入全域,在術中遇到,就將變數錄入棧中。

至於參數要在術的開頭就記錄參數位址,並從參數暫存器寫入棧中。
```rust
// 將術的參數加入符號表
// 並將參數從 a0~a7 寫入棧中
for (編號, 參名) in.形參.iter().enumerate() {
符號表.錄入棧中變數(參名, 實參);
符號表
.取得變數位址(參名)
.寫出(真言檔, &format!("a{}", 編號), 參名)?;
}
```

## 計算

計算與零.一版保持一致,依然採用堆疊機,不管完成任何計算,棧會增加一個整數大小的空間,存放計算結果。如若用不到計算結果了,得記得將其彈出棧,以維護棧的大小。

```rust
// 計算結束時,棧頂 = t0 = 計算結果
fn 計算(
真言檔: &mut File, 算式: &O算式, 符號表: &O符號表
) -> io::Result<()> {
match 算式 {
O算式::二元運算(二元運算) => {
Self::計算(真言檔, 二元運算..as_ref(), 符號表)?;
Self::計算(真言檔, 二元運算..as_ref(), 符號表)?;
Self::二元運算(真言檔, &二元運算.運算子)
}
O算式::數字(數) => Self::數字入棧(真言檔, 數),
O算式::變數(變數) => Self::變數入棧(真言檔, 變數, 符號表),
O算式::施術(施術) => Self::施術(真言檔, 施術, 符號表),
}
}
```

相比零.一版,此處增加了一種算式——`施術`,依然可用堆疊機來完成,先分別計算各個實參,將實參的值壓入棧中,等實參都計算完畢後,將實參值載入參數暫存器後,施術(call)。

### 計算施術

0 comments on commit 355f4b9

Please sign in to comment.