关于编译器 parsing 的理论知识我没有完整学, 就是补过一些片段
所以这篇文章里可能有用理论知识很容易解释的一些问题, 我并没有看到
而且 Cirru 的语法坚持要用缩进, 现有的方案是难以让我满足的
这些天用 Go 重写了 Cirru 的 parser, 后面会对思路做一些解释

旧的方案

解析缩进部分

之前一个版本的 Cirru parser 解析缩进的方案比较原始, 就是解析文本块
比如说下面这样一个文本块, 我想要按照缩进解析到到一个嵌套的关系:

a
  b
      c
    d
e
  f
(a (b ((c)) (d)))
(e (f))

我从前的做法是对文本进行分割, 先得到 ae 开头的两块文本
然后取出 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 现在出于非常原始的状态, 我并不清楚能做些什么


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者


引用和评论

0 条评论