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

用 Parser Combinator 解析 Cirru 的缩进语法相关推荐

  1. python代码缩进是一种语法吗_Python 为啥用缩进语法,听听Python之父的说的啥

    Python 为什么使用缩进来划分代码块,而不像其它语言使用花括号 {} 或者 "end" 之类的语法? Python 的缩进是一个老生常谈的话题,经常有人提及它,比如Python ...

  2. python中ht_python – 解析HTSQL时处理语法歧义

    我正在编写一个语法来解析HTSQL语法,并坚持如何处理段和除法运算符的/字符重用. described grammar并不是非常正式,所以我一直在关注Python实现的确切输出,从粗略的一瞥似乎是一个 ...

  3. oracle中触发器的语法,解析Oracle触发器的语法

    导读:触发器是一种特殊的存储过程,触发器的执行不是由程序调用,也不是手工启动,而是由事件来触发,Oracle数据库是大家非常熟悉的数据库系统啦,那么Oracle触发器的语法是怎样的呢?下文中将为大家带 ...

  4. mysql ddl 语法解析工具_sharding-sphere之语法解析器

    语法解析器,根据不同类型的语句有不同的语法解析器去解析成成SQLStatement,SQL解析器的类图我用脑图画出来如下: SQLParser.png 可以看到,不同的sql有不同的处理解析器去解析, ...

  5. 中文 NLP (10) -- 句法解析之 转换生成语法 和 依存句法

    多年来 NLP 领域最广泛的两种句法分析理论分别为 转换生成语法和依存句法. 转换生成语法 短语结构文法:形式化定义为 G = (X,V,S,R) 这样一个四元组.X 是词汇集合,称为终结符.V 是标 ...

  6. spark源码解析之scala基本语法

    1. scala初识 spark由scala编写,要解析scala,首先要对scala有基本的了解. 1.1 class vs object A class is a blueprint for ob ...

  7. iOS之深入解析谓词NSPredicate的语法与应用

    一.简介 NSPredicate 的官方解释如下: The NSPredicate class is used to define logical conditions used to constra ...

  8. python怎样缩进语法边界-Python的基础语法

    一.数据 1.1 变量 数据用变量来存放,并用等号对变量赋值. 例:nameStr = "OREO" 其中 nameStr 是变量名称,"OREO" 是变量值. ...

  9. python xpath语法-【python】爬虫: lxml解析库、XPath语法详解

    first item second item third item fourth item lxml

最新文章

  1. 用 easy-json-schema 代替 json-schema 吧
  2. 计组-数据通路的功能和基本结构
  3. SpringMVC学习日记 1.Spring框架
  4. Git 仓库代码迁移步骤记录
  5. 9款基于CSS3 Transitions实现的鼠标经过图标悬停特效
  6. https 非对称加密
  7. easypoi中excel注解开关_easypoi: 入,Word模板导出,通过简单的注解和模板 语言(熟悉的表达式语法),完成以前复杂的写法...
  8. ROS中记录数据与回放
  9. 中文乱码问题:JSP页面的显示问题,获取中文参数值问题
  10. Pyinstaller 打包Pyside2 报错qt.qpa.plugin
  11. CAN网络dbc格式
  12. NCURSES程序设计之拼图游戏
  13. 解决按键精灵助手无法连接Android手机的问题
  14. 国庆回家记之2017
  15. 物体长度测量---------C#+Emgucv
  16. 构建kd树和kd树的搜索
  17. linux文件目录挂载点,挂载点 文件通配符 目录的一些理解
  18. 为啥中国移动免费宽带突然不香了, 背后的猫腻,你知道吗?
  19. seq和ack的理解
  20. C#+winform登陆界面案例

热门文章

  1. 关于流水帐表序列号生成时的并发操作问题
  2. BizTalk开发系列(二十三) BizTalk性能指标参考
  3. zabbix设置邮件报警
  4. C++:位操作基础篇之位操作全面总结
  5. gulp实现打包js/css/img/html文件,并对js/css/img文件加上版本号
  6. Linux集群系统Heartbeat
  7. jQuery hash
  8. 解决 IndyFTp 乱码问题 10.6.0
  9. c#中泛型参数与object参数导致重写无效。
  10. 移植uboot第七步:支持DM9000