关于编译器 parsing 的理论知识我没有完整学, 就是补过一些片段
所以这篇文章里可能有用理论知识很容易解释的一些问题, 我并没有看到
而且 Cirru 的语法坚持要用缩进, 现有的方案是难以让我满足的
这些天用 Go 重写了 Cirru 的 parser, 后面会对思路做一些解释
旧的方案
解析缩进部分
之前一个版本的 Cirru parser 解析缩进的方案比较原始, 就是解析文本块
比如说下面这样一个文本块, 我想要按照缩进解析到到一个嵌套的关系:
a
b
c
d
e
f
(a (b ((c)) (d)))
(e (f))
我从前的做法是对文本进行分割, 先得到 a
和 e
开头的两块文本
然后取出 a
的文本块, 按照缩进进行处理, 得到 a
, 以及缩进以后的文本:
a
b
c
d
b
c
d
然后按照缩进一步步解析, 到缩进消失也就解析完了
解析文本部分
而文本部分, 我记录了一个状态机, 每次读取一个字符时都有一个状态用于判断
比如说, 对于包含字符串的这样一段代码, 每个字符解析都有状态:
a "b \c d"
token 'a'
token ' '
token '"' -> string
string 'b'
string ' '
string '\' -> escape
escape 'c' -> string
string ' '
string 'd'
string '"' -> token
我还在上面标记了状态转移的步骤, 特定的符号会触发状态的转移
对于括号的缩进, 我原先采取的是 Lispy 教程里的方案分开处理的:
= a (str 1)
具体的做法用的是高阶函数, 这里不展开了
后来渐渐想到这个方法有不好的地方, 当初也是为了技能不够而折衷的
就是, 我解析一段文本, 混用了好几种方式来处理, 更难定位 bug
另一个, 就是我一定要获取整个文件, 先扫描好缩进, 然后才能开始解析
而通常的解析器可以接收流的数据进行解析, 比如接收网络传送文件的同时进行解析
新的方案
Cirru 的语法规则就几条, 引号和转义, 括号, 缩进, 另外两个特殊规则(, $
)
特殊规则是在语法树解析完成后做的, 构建树的过程先不考虑
引号和转义已经能用状态机解决, 那么我想到的方案是把缩进也用状态机表示
至于括号, 括号对应的是一个嵌套关系的改变, 实际上没有状态改变
那么怎么把缩进用状态表示呢, 前面的几个字段可不足以表达缩进啊
因此我引入了一个字段 level
, 配合每一行的缩进数量 buffer
来判断状态
比如上边的缩进语法, 我做一些简化, 然后用状态表示出来
a
b
c
d
e
f
其中 level
对应前一个缩进的长度, len(buffer)
对应当前的缩进:
level len(buffer) ->
0 0 ->
0 2 -> push
2 6 -> push push
6 4 -> pop
4 0 -> pop pop
0 2 -> pop
这里的 pop push
对应跳出当前数组和创建数组的操作, 对应表达式的嵌套
而括号的行为也和这里的 pop push
对应:
= a (str 1)
token '='
token ' '
token 'a'
token ' '
token '(' -> push
token 's'
token 't'
token 'r'
token ' '
token '1'
token ')' -> pop
当有一个嵌套的数组的结构, 有个指针用于跳到上级和创建新数组
就可以读取文件, 生成一棵 Cirru 的语法树, 包含嵌套的表达式
由于 Cirru 的语法简单, 这样一个简单的状态机就完成了:
https://github.com/Cirru/parser
考虑假如以后会增加其他的语法的话, 我猜想是直接添加状态来扩展了
比如转义的内容, 要支持 "\u02aa"
之类稍微复杂的语法
当然也可能 Cirru 太不实用, 我完全没有添加语法的需求
Cirru 现在出于非常原始的状态, 我并不清楚能做些什么