在 Parsec 當中是存在解析縮進語法的方案的, 然而我沒深刻了解過
等了解之後, 也許會有其餘的想法, 到時候再考慮不遲
Cirru 縮進解析已經實現, 修了些 bug 具體實現可能和文中有區別 https://github.com/Cirru/parser-combinator.cljcss
這篇文章主要是整理一下我用"解析器組合子"解析縮進的知識
解析器組合子大概是 Parser Combinator 中文翻譯, 應該仍是準確吧
Cirru 語法解析器前面用的是 Tricky 的函數式編程作的, 有點像 State Monad
不過我當時搞不清楚 LL 和 LR 的區別, 如今看其實兩個都不符合html
關於編譯器的知識我一直在積累, 但沒有學成體系, 只是零星的
上週我寫 WebAssembly 的 S-expression 解析器, 解析成 JSON
忽然想明白了 Parser Combinator, 就嘗試寫了下, 結果然的有用
可是用的是 CirruScript 加 immutable-js, 以爲有點吃力
因而想到嘗試一下用解析器組合子解析 Cirru 的縮進
此次用的是 Clojure, 斷斷續續花了一個星期, 終於跑通了測試git
這是我期間和昨天整理的資源, 大概梳理了一下語法解析是怎麼回事github
Parser Combinator 在語法解析的當中處於怎樣的位置?
爲何全部的教科書中都不同意手寫自底向上的語法分析器?
shift reduce,預測分析,遞歸降低和LL(K) LR(K) SLR 以 LALR 的關係?express
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 explainedjson
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 理論app
具體的原理這裏解釋不了, 建議看上邊的文章, 雖然是英文, 還好懂
我只是大概解釋一下, 方便後面解釋我是怎麼解析縮進的函數式編程
在解析器組合子當中, 好比要解析字母 a
, 要先定一個對應的解析器
好比用 Clojure 表示一下大概的意思:
(def read-a [code] (if (= (subs code 0 1) "a") (subs code 1) nil))
對於字符串 code
, 取第一個字符, 判斷是不是 a
若是是 a
, 就返回後面的內容, 若是不是 a
, 就返回錯誤, 好比 nil
思路是這樣, 但 Haskell 用 State Monad, 就是內部隱藏一個 State
而我在 Clojure 實際上定義了一整個 Map 存儲我須要的數據:
(def initial-state { :code "code" :failed false :value nil :indentation 0 :msg "initial" })
其中 :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" 是生成調試消息用的, 能夠先忽略)
(defn helper-chain [state parsers] (log-nothing "helper-chain" state) (if (> (count parsers) 0) (let [parser (first parsers) result (parser state)] (if (:failed result) (fail state "failed apply chaining") (recur (assoc result :value (conj (into [] (:value state)) (:value result))) (rest parsers)))) state)) (defn combine-chain [& parsers] (just "combine-chain" (fn [state] (helper-chain (assoc state :value []) parsers)))) (defn combine-times [parser n] (just "combine-times" (fn [state] (let [method (apply combine-chain (repeat n parser))] (method state)))))
總之按照這樣的思路, 就能把解析器越寫越大, 作更復雜的解析
另外要注意的是遞歸生成的預測會很是複雜, 調試很難
我其實是寫了比較複雜的 log 系統用於調試的, 看一下簡單的例子:
https://gist.github.com/jiyinyiyong/0568487a4ab31716186f
這只是解析表達式的, 並且是簡單的 Cirru 語法
對於縮進, 並且若是加上更復雜的語法, 這個 log 會很是很是長
另外有個後面用到的 parser 要先解釋一下, 就是 peek
peek
意思是預覽後續的內容, 但不是真的把 :value
做爲解析的一部分
也就是說, 嘗試解析一次, 把 :failed
結果拷貝過來, 而 :code
不影響
(defn combine-peek [parser] (just "combine-peek" (fn [state] (let [result (parser state)] (if (:failed result) (fail state "peek failed") state)))))
以及 combine-value
函數, 專門處理處理 :value
用來說每一個單獨 Parser 解析的結果處理成整個 Parser 想要獲得的值
因爲每一個組合獲得的 Parser 邏輯可能不一樣, 這裏傳入函數去處理的
(defn combine-value [parser handler] (just "combine-value" (fn [state] (let [result (parser state)] (assoc result :value (handler (:value result) (:failed result)))))))
最初解析縮進的思路是, 模擬括號的解析, 每次解析 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
是參考正則裏的習慣, 表示零個或者多個, 這裏是零個或多個空行
(defn parse-line-breaks [state] ((just "parse-line-breaks" (combine-value (combine-chain (combine-star parse-empty-line) parse-newline) (fn [value is-failed] nil))) state))
而後是重要的函數 parse-indentation
匹配換行加縮進
其中縮進的具體的值, 經過 combine-value
進行一次處理
因此這個函數主要作的事情, 就是在發現縮進時把具體的縮進讀出來
這個值就能夠和上邊 State 的 Map 裏的縮進數據作對比了
(defn parse-indentation [state] ((just "parse-indentation" (combine-value (combine-chain (combine-value parse-line-breaks (fn [value is-failed] nil)) (combine-value (combine-star parse-two-blanks) (fn [value is-failed] (count value)))) (fn [value is-failed] (if is-failed 0 (last value))))) state))
當解析出來的行縮進值大於 State 中保存的縮進時, 表示存在縮進
這裏作的就是生成一個成功的狀態, 而且 :indentation
的值加一
也就是說這後面的解析, 以新的一個縮進值做爲基準了
同時 :code
內容在執行一次縮進解析時並不改變, 也就不影響多層縮進解析
因此解析縮進其實是在 State 上操做, 而不是跟字符串同樣 eat 字符
(def parse-indent (just "parse-indent" (fn [state] (let [result (parse-indentation state)] (if (> (:value result) (:indentation result)) (assoc state :indentation (+ (:indentation result) 1) :value nil) (fail result "no indent"))))))
反縮進的解析參考上邊的原理, 只是在大小的對比上取反就能夠了
(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 掉換行和空格相關的字符串了, 從而進行後續的解析
(def parse-align (just "parse-align" (fn [state] (let [result (parse-indentation state)] (if (= (:value result) (:indentation state)) (assoc result :value nil) (fail result "not aligned"))))))
解析縮進的關鍵代碼就是按照上邊所說了, 已經知足 Cirru 的須要
此外作的就是 block-line
和 inner-block
相關的抽象
我把一個行(以及緊跟的由於縮進而包含進來的行)稱爲 block-line
整個程序代碼實際上就是一組 block-line
爲內容的列表block-line
內部的縮進的不少行, 稱爲 inner-block
而後 inner-block
實際上也就是基於不一樣縮進的 block-line
組合而成
(defn parse-inner-block [state] ((just "parse-inner-block" (combine-value (combine-chain parse-indent (combine-value (combine-optional parse-indentation) (fn [value is-failed] nil)) (combine-alternate parse-block-line parse-align) parse-unindent) (fn [value is-failed] (if is-failed nil (filter some? (nth value 2)))))) state)) (defn parse-block-line [state] ((just "parse-block-line" (combine-value (combine-chain (combine-alternate parse-item parse-whitespace) (combine-optional parse-inner-block)) (fn [value is-failed] (let [main (into [] (filter some? (first value))) nested (into [] (last value))] (if (some? nested) (concat main nested) main))))) state))
整理這樣的思路, 整個按照縮進組織的程序代碼就組合出來了
注意 block-line
之間須要有 indent-align
做爲換行分割的
我專門寫了 combine-alternate
表示間隔替代的兩個 Parser
整體就這樣, 獲得的一個 parser-program
的 Parser
(defn parse-program [state] ((just "parse-program" (combine-value (combine-chain (combine-optional parse-line-breaks) (combine-alternate parse-block-line parse-align) parse-line-eof) (fn [value is-failed] (if is-failed nil (filter some? (nth value 1)))))) state))
大體解釋完了, 應該仍是很難懂的. 我也不打算寫到很是清楚了
對這個解析的方案有興趣的話, 能夠在微博或者微信上找我私聊
這個方案只是從實踐上驗證了用 Parser Combinator 解析縮進的方案一個能用的 Parser, 除了適合擴展, 在性能和錯誤提示上都須要增強目前的版本主要爲了學習研究目的, 將來再考慮改進的事情