在 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, 除了适合扩展, 在性能和错误提示上都须要增强目前的版本主要为了学习研究目的, 将来再考虑改进的事情