|
237 | 237 |
|
238 | 238 | 乘除式 = 原子式・重複原子式 |
239 | 239 |
|
240 | | -重複原子式 = *・重複乘除式 |
241 | | - | /・重複乘除式 |
| 240 | +重複原子式 = *・原子式・重複原子式 |
| 241 | + | /・原子式・重複原子式 |
242 | 242 | | e |
243 | 243 |
|
244 | 244 | 原子式 = 數字 |
|
261 | 261 |  |
262 | 262 |
|
263 | 263 | 這種精簡後,但在語義上沒有損失的語法樹,被稱為抽象語法樹。若語境明確,也可以直接叫語法樹。 |
| 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