Skip to content

Commit 82c0701

Browse files
committed
介紹遞迴下降剖析
1 parent c32258a commit 82c0701

1 file changed

Lines changed: 145 additions & 2 deletions

File tree

book/零.一版/剖析(語法分析).md

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@
237237
238238
乘除式 = 原子式・重複原子式
239239
240-
重複原子式 = *・重複乘除式
241-
| /・重複乘除式
240+
重複原子式 = *・原子式・重複原子式
241+
| /・原子式・重複原子式
242242
| e
243243
244244
原子式 = 數字
@@ -261,3 +261,146 @@
261261
![變數宣告式抽象語法樹](../image/變數宣告式抽象語法樹.png)
262262

263263
這種精簡後,但在語義上沒有損失的語法樹,被稱為抽象語法樹。若語境明確,也可以直接叫語法樹。
264+
265+
## 實作:手寫遞迴下降
266+
267+
先定義剖析器的輸出——抽象語法樹節點的型別。
268+
269+
### 抽象語法樹節點型別定義
270+
271+
```rust
272+
pub type O語法樹 = O咒;
273+
274+
pub struct O咒 {
275+
: Vec<O句>,
276+
}
277+
278+
enum O句 {
279+
變數宣告(O變數宣告),
280+
算式(O算式),
281+
}
282+
283+
struct O變數宣告 {
284+
變數名: String,
285+
算式: O算式,
286+
}
287+
288+
enum O算式 {
289+
變數(String),
290+
數字(i64),
291+
二元運算(O二元運算),
292+
}
293+
294+
struct O二元運算 {
295+
運算子: O運算子,
296+
: Box<O算式>,
297+
: Box<O算式>,
298+
}
299+
```
300+
301+
### 剖析
302+
303+
貧道將每個生成符規則對應到一個剖析函式,剖析函式會從詞陣列的某個位置開始,嘗試找出其對應生成符的一組展開式。
304+
305+
剖析函式有以下形式:
306+
307+
```rust
308+
// 游標是一個索引,指到當前詞陣列尚未被剖析的最前位置
309+
// 應用任何一條規則剖析成功時,回傳 Some(O語法樹節點)
310+
// 所有規則都剖析不了 XXX 生成符時,回傳 None
311+
fn 剖析XXX(&self, 游標) -> Option<O語法樹節點, 剖析後的游標位置(usize)>
312+
```
313+
314+
先來看個簡單例子,``的剖析,``應對到兩條簡單規則
315+
316+
```rust
317+
// 句 = 變數宣告式
318+
// | 算式
319+
320+
fn 剖析句(&self, 游標: usize) -> Option<(O句, usize)> {
321+
// 句 = 變數宣告式
322+
// 若匹配`變數宣告`成功,返回對應語法樹節點
323+
if let Some((變數宣告, 游標)) = self.剖析變數宣告(游標) {
324+
return Some((O句::變數宣告(變數宣告), 游標));
325+
}
326+
327+
// 句 = 算式
328+
// 若匹配`算式`成功,返回對應語法樹節點
329+
if let Some((算式, 游標)) = self.剖析算式(游標) {
330+
return Some((O句::算式(算式), 游標));
331+
}
332+
333+
// 所有規則都無法剖析,返回 None
334+
None
335+
}
336+
```
337+
338+
再來看另一個例子,`變數宣告`的剖析,`變數宣告`只對應一條規則,但是,這條規則需要匹配多個符。
339+
340+
```rust
341+
// 變數宣告式 = "元"・"・"・變數・"="・算式
342+
fn 剖析變數宣告(&self, 游標: usize) -> Option<(O變數宣告, usize)> {
343+
let 游標 = self.消耗(游標, O詞::元)?; // 若匹配不了 "元" ,短路返回 None
344+
let 游標 = self.消耗(游標, O詞::音界)?; // 若匹配不了 "・" ,短路返回 None
345+
let (變數名, 游標) = self.剖析變數(游標)?; // 若匹配不了 變數 ,短路返回 None
346+
let 游標 = self.消耗(游標, O詞::等號)?; // 若匹配不了 "=" ,短路返回 None
347+
let (算式, 游標) = self.剖析算式(游標)?; // 若匹配不了 算式 ,短路返回 None
348+
349+
//
350+
Some((O變數宣告 { 算式, 變數名 }, 游標))
351+
}
352+
```
353+
354+
觀察這兩個剖析函式,可以發現它們的短路規則截然相反
355+
356+
- `剖析句`分成兩個主要`if`區塊,當剖析成功,得到 `Some` 時短路返回語法樹節點。
357+
- 應對的是兩條展開規則,一條展開能匹配詞流就算成功
358+
- 稱此結構為「或」
359+
- `剖析變數宣告`則連續調用了 5 次剖析函式 (`消耗`也是種剖析函式,只是它特別簡單),在剖析失敗,得到 `None` 時短路返回 `None`
360+
- 應對的是:詞流必須完整匹配整條展開式才算匹配成功,一項不匹配就是失敗。
361+
- 但 Rust 提供了 ? 語法糖,所以不用一直 if let 才能知道是不是 Some
362+
- 稱此結構為「且」
363+
364+
語法展開也不外乎這兩個結構,一個在語法規則裡用 `|` 來表示「或」,用 `` 來表示「且」。
365+
366+
最後來看個「或」、「且」結構都用上的語法規則`原子式`,其實作不外乎這兩種結構的組合。
367+
368+
```rust
369+
// 原子式 = 數字
370+
// | 變數
371+
// | "("・算式・")"
372+
fn 剖析原子式(&self, 游標: usize) -> Option<(O算式, usize)> {
373+
// 原子式 = 數字
374+
if let Some((數字, 游標)) = self.剖析數字(游標) {
375+
return Some((O算式::數字(數字), 游標));
376+
}
377+
// 原子式 = 變數
378+
if let Some((變數, 游標)) = self.剖析變數(游標) {
379+
return Some((O算式::變數(變數), 游標));
380+
}
381+
// 原子式 = (算式)
382+
// 此處用上了閉包來讓 ? 語法糖生效
383+
// 也可以選擇多寫一個函式來專門生成`原子式 = (算式)`
384+
if let Some(結果) = (|| -> Option<(O算式, usize)> {
385+
let 游標 = self.消耗(游標, O詞::左括號)?;
386+
let (算式, 游標) = self.剖析算式(游標)?;
387+
let 游標 = self.消耗(游標, O詞::右括號)?;
388+
Some((算式, 游標))
389+
})() {
390+
return Some(結果);
391+
}
392+
None
393+
}
394+
```
395+
396+
其他規則基本按照這兩結構依樣畫葫蘆就行,但`重複原子式``重複乘除式`要處理一下左結合的問題。
397+
398+
音界咒的 9 條語法展開規則都寫成函式後,就可以調用
399+
```
400+
剖析咒(0)
401+
```
402+
來得到整棵語法樹了。注意到,本剖析器第一個呼叫的 `剖析咒()` 是語法樹最頂層的規則,它自頂向下的建構語法樹,因此吾人目前採用的回溯算法可說是一種「自頂向下」的剖析算法。
403+
404+
「自頂向下」剖析有很多種實作方法,如前文的虛擬碼比較像是對每條規則建表,最後再寫一個函式根據表格遞迴呼叫以完成剖析。而給每一個規則都寫一份對應函式的實作法,就被稱為「遞迴下降剖析」,大約是要強調手寫的遞迴函式互相呼叫、越來越深吧。建表法就未必要用遞迴來做,可以用棧(堆疊)來模擬。
405+
406+
每條規則都是手寫的雖然容易有誤,但也有靈活這個優點,除錯時想打印什麼訊息直接加在函式裡就行。從具體語法樹轉換成抽象語法樹也特別好寫,例如前面`剖析變數宣告`的函式,很輕鬆的就只從 5 個具體語法樹節點取出 2 個有用的抽象語法樹節點。

0 commit comments

Comments
 (0)