用 Parser Combinator 解析 Cirru 的缩进语法
在 Parsec 當中是存在解析縮進語法的方案的, 然而我沒深入了解過
等了解以后, 也許會有其他的想法, 到時候再考慮不遲
Cirru 縮進解析已經實現, 修了些 bug 具體實現可能和文中有區別 https://github.com/Cirru/parser-combinator.clj
概覽
這篇文章主要是整理一下我用"解析器組合子"解析縮進的知識
解析器組合子大概是 Parser Combinator 中文翻譯, 應該還是準確吧
Cirru 語法解析器前面用的是 Tricky 的函數式編程做的, 有點像 State Monad
不過我當時搞不清楚 LL 和 LR 的區別, 現在看其實兩個都不符合
關于編譯器的知識我一直在積累, 但沒有學成體系, 只是零星的
上周我寫 WebAssembly 的 S-expression 解析器, 解析成 JSON
突然想明白了 Parser Combinator, 就嘗試寫了下, 結果真的有用
但是用的是 CirruScript 加 immutable-js, 覺得有點吃力
于是想到嘗試一下用解析器組合子解析 Cirru 的縮進
這次用的是 Clojure, 斷斷續續花了一個星期, 終于跑通了測試
LL, LR, Parser Combinator 資源
這是我期間和昨天整理的資源, 大概梳理了一下語法解析是怎么回事
Parser Combinator 在語法解析的當中處于怎樣的位置?
為什么所有的教科書中都不贊成手寫自底向上的語法分析器?
shift reduce,預測分析,遞歸下降和LL(K) LR(K) SLR 以 LALR 的關系?
LL and LR Parsing Demystified
LL and LR in Context: Why Parsing Tools Are Hard
[The difference between top-down parsing and bottom-up parsing
](http://qntm.org/top)
Parser combinators explained
Parsing CSS with Parsec
[Simple Monadic Parser in Haskell
](http://michal.muskala.eu/2015/09/23/simple-monadic-parser-in-haskell.html)
概括說, 語法解析要有一套語法規則, 一個結果, 還有每個字符串
解析的過程就是通過三個信息推導出中間組合的過程, 也就是 parse tree
LL 是先從 Parse Tree 根節點開始, 預測程序所有的可能結構, 排除錯誤的預測
LR 是先從每個字符開始組合, 逐步組合更多, 看最后是否得到單個程序
而 Parser Combinator 是用高階函數遞歸構造 LL 解析, 充分利用遞歸的優勢
實際當中的 Parser 經常因為太復雜, 而不是依據單純 LL 和 LR 理論
Parser Combinator 基礎
具體的原理這里解釋不了, 建議看上邊的文章, 雖然是英文, 還好懂
我只是大概解釋一下, 方便后面解釋我是怎么解析縮進的
在解析器組合子當中, 比如要解析字母 a, 要先定一個對應的解析器
比如用 Clojure 表示一下大概的意思:
對于字符串 code, 取第一個字符, 判斷是否是 a
如果是 a, 就返回后面的內容, 如果不是 a, 就返回錯誤, 比如 nil
思路是這樣, 但 Haskell 用 State Monad, 就是內部隱藏一個 State
而我在 Clojure 實際上定義了一整個 Map 存儲我需要的數據:
其中 :failed 存儲解析成功失敗的狀態, :value 存儲當前局部解析的結果
:code 就是存儲還沒解析的字符串, :msg 是錯誤消息, 調試用的
上邊的 read-a 改成 parse-a 的話, 參數也就改成用對象來寫
解析正確的時候 :code 和 :value 更新成解析 a 以后的值
解析失敗的時候把 :failed 設置為 true, 加上對應的 :msg
單個字符的解析就是這樣, 其他的字符類似, 就是每次取一個字符判斷
然后是組合的問題, 比如 aa, 就是兩個 parse-a 的組合
常見的名字是 many, 就是到第一個 parse-a 的結果繼續嘗試解析
因為每個 parser 的輸入輸出都是 State, 所以前一個結果后一個 Parser 直接用
而 many 也可以把兩個 parser 的 :value 處理成列表, 作為結果
類似也有 option 或者 choice, 比如 parse-a-or-b
解析的原理就是對字符串先用 parse-a, 不匹配就嘗試 parse-b
然后得到結果, 或者是 a 或者是 b, 或者是 :failed true
此外還可以構造比如取反, 零個或多個, 可選, 間隔, 等等不同的匹配方式
發揮想象力, 嘗試組合 parse, 根據返回的 :failed 值決定后續操作
我的語言描述不清楚, 最好加一些圖, 這里我先貼代碼, 可以嘗試看下
大概的意思是連續解析幾個內容, 以此作為新的解析器
(注意代碼中 "log" "just" "wrap" 是生成調試消息用的, 可以先忽略)
總之按照這樣的思路, 就能把解析器越寫越大, 做更復雜的解析
另外要注意的是遞歸生成的預測會非常復雜, 調試很難
我實際上是寫了比較復雜的 log 系統用于調試的, 看一下簡單的例子:
https://gist.github.com/jiyinyiyong/0568487a4ab31716186f
這只是解析表達式的, 而且是簡單的 Cirru 語法
對于縮進, 而且如果加上更復雜的語法, 這個 log 會非常非常長
另外有個后面用到的 parser 要先解釋一下, 就是 peek
peek 意思是預覽后續的內容, 但不是真的把 :value 作為解析的一部分
也就是說, 嘗試解析一次, 把 :failed 結果拷貝過來, 而 :code 不影響
以及 combine-value 函數, 專門處理處理 :value
用來講每個單獨 Parser 解析的結果處理成整個 Parser 想要得到的值
由于每個組合得到的 Parser 邏輯可能不同, 這里傳入函數去處理的
關于縮進
最初解析縮進的思路是, 模擬括號的解析, 每次解析 eat 掉對應縮進的字符串
然而這個方案并不靠譜, 有兩個無法解決的問題
一個是如果出現一次多層縮進, 可能有換行, 但多個縮進是共用換行的
另一個是縮進結束位置, 經常會出現同時多層縮進, 也是共用縮進
這樣的情況就需要用 peek, 也就是查看后續內容而不解析具體結果
最終我想到了一個方案, 可能也有一些 tricky, 但按照原理能運行了
如果對于縮進有更深入的理解的話, 也許有更好的方案
這個方案有幾個要準備的點, 我分開來介紹一遍
首先準備工作是前面 initial-state 當中的 :indentation
這個值表示的是當前解析狀態所處的縮進層級
后面具體的解析過程拿到代碼行的縮進層級, 和這個值對比
那么就能縮進和反縮進就有一個辦法可以識別出來了
縮進的空格, Cirru 限制了使用兩個空格, 因而我直接定義好
(defn parse-two-blanks [state]((just "parse-two-blanks"(combine-value(combine-times parse-whitespace 2)(fn [value is-failed] 1))) state))換行本來就是 \n 字符, 不過為了兼容中間的空行, 做了一些處理
star 是參考正則里的習慣, 表示零個或者多個, 這里是零個或多個空行
然后是重要的函數 parse-indentation 匹配換行加縮進
其中縮進的具體的值, 通過 combine-value 進行一次處理
所以這個函數主要做的事情, 就是在發現縮進時把具體的縮進讀出來
這個值就可以和上邊 State 的 Map 里的縮進數據做對比了
當解析出來的行縮進值大于 State 中保存的縮進時, 表示存在縮進
這里做的就是生成一個成功的狀態, 并且 :indentation 的值加一
也就是說這后面的解析, 以新的一個縮進值作為基準了
同時 :code 內容在執行一次縮進解析時并不改變, 也就不影響多層縮進解析
所以解析縮進實際上是在 State 上操作, 而不是跟字符串一樣 eat 字符
反縮進的解析參考上邊的原理, 只是在大小的對比上取反就可以了
(def parse-unindent(just "parse-unindent"(fn [state](let[result (parse-indentation state)](if(< (:value result) (:indentation result))(assoc state:indentation (- (:indentation result) 1):value nil)(fail result "no unindent"))))))最后, 在行縮進層級和 State 中的縮進值相等時, 說明只是單純的換行
這時, 就可以 eat 掉換行和空格相關的字符串了, 從而進行后續的解析
解析縮進的關鍵代碼就是按照上邊所說了, 已經滿足 Cirru 的需要
此外做的就是 block-line 和 inner-block 相關的抽象
我把一個行(以及緊跟的因為縮進而包含進來的行)稱為 block-line
整個程序代碼實際上就是一組 block-line 為內容的列表
block-line 內部的縮進的很多行, 稱為 inner-block
然后 inner-block 實際上也就是基于不同縮進的 block-line 組合而成
整理這樣的思路, 整個按照縮進組織的程序代碼就組合出來了
注意 block-line 之間需要有 indent-align 作為換行分割的
我專門寫了 combine-alternate 表示間隔替代的兩個 Parser
總體就這樣, 得到的一個 parser-program 的 Parser
大致解釋完了, 應該還是很難懂的. 我也不打算寫到非常清楚了
對這個解析的方案有興趣的話, 可以在微博或者微信上找我私聊
結尾
這個方案只是從實踐上驗證了用 Parser Combinator 解析縮進的方案
一個能用的 Parser, 除了適合擴展, 在性能和錯誤提示上都需要加強
目前的版本主要為了學習研究目的, 未來再考慮改進的事情
總結
以上是生活随笔為你收集整理的用 Parser Combinator 解析 Cirru 的缩进语法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: bootstrap 全局 CSS 样式
- 下一篇: Android Studio导入gith