日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

用 Parser Combinator 解析 Cirru 的缩进语法

發布時間:2025/7/14 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 用 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 表示一下大概的意思:

(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, 除了適合擴展, 在性能和錯誤提示上都需要加強
目前的版本主要為了學習研究目的, 未來再考慮改進的事情

《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀

總結

以上是生活随笔為你收集整理的用 Parser Combinator 解析 Cirru 的缩进语法的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。