1. 回顾
在上一篇文章中,介绍了一个基于解析器组合子实现的词法/语法解析器,在处理call的时候,由于其右侧产生式的第一项便是expr,如果不做处理,普通的递归下降解析器将会陷入死循环的情况,因此对其进行了右递归的转换,保证解析器可以正常执行。
2. 左递归语法的处理
由于右递归转换后的描述相较左递归的描述更加复杂且不够自然,因此本章通过引入GLL算法的思想,来为递归下降的解析器支持左递归语法的处理。
2.1 GLL算法及其思想
GLL算法通过引入一个graph-structured stack,提供了可以转移控制流的能力,使其在解析的过程中,可以暂停和恢复,让解析器在多个点位跳转切换,从而避免了左递归导致的死循环,并在生成解析结果后,将其作为左递归产生式的结果返回。
2.2 再探continuation
根据2.1节中对GLL算法的描述,可以看到其引入的graph-structured stack提供了一个和first-class continuation非常相似的操作。上一篇文章中,对于continuation的描述,只是简短的将其定义为“接下来要做的事情”。本节中,会对continuation进行更加细致的定义,即“在当前上下文环境中,接下来要做的事情”。对解析器进行continuation化,不仅仅是提供了一种便于组合和异常处理的方式,更关键的是,在continuation化之后,解析器的控制流将会被结构化的显式呈现,使得程序自身可以随意切换执行点位,甚至可以多次回溯重试。下面,通过一个栗子来演示一下continuation的执行和作用。
2.3 使用continuation在两个函数间切换
(define num-gen
(lambda (yield)
; 定义了一个无限递归的函数, 每次增加1后, 跳出当前控制流
(define infinity-add
(lambda (n yield)
; let/cc将当前表达式的continuation绑定到k上
; 外部在获取到当前的continuation后, 将下一次需要跳转的外部continuation传入
(let ([new-yield (let/cc k (yield `(,n ,k)))])
(infinity-add (add1 n) new-yield))))
(infinity-add 1 yield)))
(define num-access
(lambda (count)
(define num-access-to
(lambda (i num-gen)
(if (< i count)
; 将当前表达式的continuation传给num-gen, 使其在数字+1后可以返回该处,
; 同时将获取下一个数字的continuation一并返回
(match (let/cc k (num-gen k))
[`(,n ,next-num)
(printf "~a " n)
(num-access-to (add1 i) next-num)])
(newline))))
(num-access-to 0 num-gen)))
; 获取从1开始的9个数字
(num-access 9)
; 获取从1开始的5个数字
(num-access 5)
; 执行结果
1 2 3 4 5 6 7 8 9
1 2 3 4 5
上述的栗子中,通过定义num-gen来描述了一个死循环的函数,如果没有continuation将控制流让出,一旦调用该函数,程序则必然会卡死。之后又定义了num-access函数,其通过外部指定的count来有限的获取num-gen产生的结果。在num-access调用num-gen时,其将当前表达式的continuation传给了num-gen,使得num-gen可以将其内部的数字传递到num-access处,同时,也将num-gen接下来的continuation一并传给了num-access,使其可以重新返回num-gen中,并将新的num-access的conitnuation带入,使得num-gen可以再次跳转到num-access处。同时,continuation会包含返回点的上下文环境,新调用的(num-access 5)并不会和上一次的(num-access 9)产生混淆,就如同两个平行世界一般,只可在自己的世界中穿梭。
2.4 continuation与左递归语法
上一节通过一个具体的栗子讲述了continuation的运作流程,那么continuation又如何与左递归语法结合呢?答案便是同num-gen一样,在重复调用的时候跳出控制流,在当前位置挂起,使解析器可以进行接下来的匹配,等匹配完成时,再将其作为结果唤醒挂起点,从而使得左递归语法可以如期匹配执行。
3. 解析流程的具体分析
在讨论了continuation与左递归语法之后,本节通过一个具体的简短示例,来详细分析continuation如何挂起以及恢复,来实现左递归语法的处理。
3.1 示例语法定义
本节中,定义了一个简单的语法,表达式可以由标识符、数字和函数调用构成,其中函数调用以左递归的方式描述。同时,给出一个具体的文本定义:"foo(10)(20)(30)"。
3.2 匹配流程的执行
Expr <- (_, "foo(10)(20)(30)") k-1
其中, <-左侧的Expr表示本次调用的产生式对应的解析器, 右侧通过一个二元向量描述当前的匹配结果及剩余符号流,其最右侧的k-1表示以该二元向量作为参数调用Expr时解析成功的continuation,为便于分析讨论,本栗暂不关心其他异常问题。解析器内部的调用以缩进的方式表示。
Expr <- (_, "foo(10)(20)(30)") k-1
Expr(Expr) <- (_, "foo(10)(20)(30)")
Expr <- (_, "foo(10)(20)(30)") k-2
通过两次调用,解析器便已经触及到左递归的循环,在第3行中,以相同的二元向量调用Expr,如果不做处理,解析器则将陷入死循环,因此,解析器将需要在此处进行挂起,并认为本次匹配失败。
Expr <- (_, "foo(10)(20)(30)") k-1
Expr(Expr) <- (_, "foo(10)(20)(30)")
Expr <- (_, "foo(10)(20)(30)") k-2
-> failure
-> failure
Num <- (_, "foo(10)(20)(30)")
-> failure
Var <- (_, "foo(10)(20)(30)")
-> (foo, "(10)(20)(30)")
-> (foo, "(10)(20)(30)")
由于递归调用Expr失败,则其调用者Expr(Expr)也匹配失败,因此Expr一路向下,分别使用Num和Var进行匹配,最终,Var成功匹配到foo,则最外层的Expr的匹配结果为(foo, "(10)(20)(30)")。
在获取到结果后,此时便可以按顺序将其广播给Expr的continuation,来恢复之前挂起的执行点。
Expr <- (_, "foo(10)(20)(30)") k-1
-> (foo, "(10)(20)(30)")
首先将(foo, "(10)(20)(30)")传给一开始k-1,即最开始的调用,但显然该结果并不满足预期,因此需要继续调用之后的k-2。
Expr <- (_, "foo(10)(20)(30)") k-1
Expr(Expr) <- (_, "foo(10)(20)(30)")
Expr <- (_, "foo(10)(20)(30)") k-2
-> (foo, "(10)(20)(30)")
( <- (foo, "(10)(20)(30)")
-> (foo(, "10)(20)(30)")
Expr <- (foo(, "10)(20)(30)") k-3
Expr(Expr) <- (foo(, "10)(20)(30)")
Expr <- (foo(, "10)(20)(30)") k-4
-> failure
-> failure
Num <- (foo(, "10)(20)(30)")
-> (foo(10, ")(20)(30)")
-> (foo(10, ")(20)(30)")
将(foo, "(10)(20)(30)")传递给k-2后,解析器的控制流切换至第二次使用(_, "foo(10)(20)(30)")调用Expr的地方,即本次的调用返回结果为(foo, "(10)(20)(30)")。之后按顺序匹配剩余的(Expr)部分,解析器会再一次陷入递归挂起的状态,其处理方式与最外侧的Expr一致,挂起并执行后续的匹配。
在经过Num的匹配后,Expr得到其匹配结果(foo(10, ")(20)(30)"),同样,开始将结果广播给本次Expr的continuation,即k-3。
Expr <- (_, "foo(10)(20)(30)") k-1
Expr(Expr) <- (_, "foo(10)(20)(30)")
Expr <- (_, "foo(10)(20)(30)") k-2
-> (foo, "(10)(20)(30)")
( <- (foo, "(10)(20)(30)")
-> (foo(, "10)(20)(30)")
Expr <- (foo(, "10)(20)(30)") k-3
-> (foo(10, ")(20)(30)")
) <- (foo(10, ")(20)(30)")
-> (foo(10), "(20)(30)")
-> (foo(10), "(20)(30)")
-> (foo(10), "(20)(30)")
解析器从k-3恢复后,经过后续的一路匹配,可以成功解析到第二行的Expr(Expr)的结果,其结果同样作为第一行的Expr,之后,再次广播结果给continuation,首先是k-1,同之前一样,该结果并不满足预期,继续调用k-2。
Expr <- (_, "foo(10)(20)(30)") k-1
Expr(Expr) <- (_, "foo(10)(20)(30)")
Expr <- (_, "foo(10)(20)(30)") k-2
-> (foo(10), "(20)(30)")
( <- (foo(10), "(20)(30)")
-> (foo(10)(, "20)(30)")
Expr <- (foo(10)(, "20)(30)") k-5
Expr(Expr) <- (foo(10)(, "20)(30)")
Expr <- (foo(10)(, "20)(30)") k-6
-> failure
-> failure
Num <- (foo(10)(, "20)(30)")
-> (foo(10)(20, ")(30)")
-> (foo(10)(20, ")(30)")
之后的调用与上述相同,Expr解析到(foo(10)(20, ")(30)")的结果后,执行切换到k-5处。
Expr <- (_, "foo(10)(20)(30)") k-1
Expr(Expr) <- (_, "foo(10)(20)(30)")
Expr <- (_, "foo(10)(20)(30)") k-2
-> (foo(10), "(20)(30)")
( <- (foo(10), "(20)(30)")
-> (foo(10)(, "20)(30)")
Expr <- (foo(10)(, "20)(30)") k-5
-> (foo(10)(20, ")(30)")
) <- (foo(10)(20, ")(30)")
-> (foo(10)(20), "(30)")
-> (foo(10)(20), "(30)")
-> (foo(10)(20), "(30)")
Expr <- (_, "foo(10)(20)(30)") k-1
Expr(Expr) <- (_, "foo(10)(20)(30)")
Expr <- (_, "foo(10)(20)(30)") k-2
-> (foo(10)(20), "(30)")
( <- (foo(10)(20), "(30)")
-> (foo(10)(20)(, "30)")
Expr <- (foo(10)(20)(, "30)") k-7
Expr(Expr) <- (foo(10)(20)(, "30)")
Expr <- (foo(10)(20)(, "30)") k-8
-> failure
-> failure
Num <- (foo(10)(20)(, "30)")
-> (foo(10)(20)(30, ")")
-> (foo(10)(20)(30, ")")
Expr <- (_, "foo(10)(20)(30)") k-1
Expr(Expr) <- (_, "foo(10)(20)(30)")
Expr <- (_, "foo(10)(20)(30)") k-2
-> (foo(10)(20), "(30)")
( <- (foo(10)(20), "(30)")
-> (foo(10)(20)(, "30)")
Expr <- (foo(10)(20)(, "30)") k-7
-> (foo(10)(20)(30, ")")
) <- (foo(10)(20)(30, ")")
-> (foo(10)(20)(30), "")
-> (foo(10)(20)(30), "")
-> (foo(10)(20)(30), "")
重复数次后,最外层的Expr解析到了(foo(10)(20)(30), ""),将其传递给k-1后,符合预期,解析至此完成。
4. 总结
根据上节的示例的具体分析,可以看到continuation是如何在左递归语法的解析中工作的,其具有的控制流挂起与恢复能力,可以打破因递归导致的死循环,下一篇中,将会对之前的解析子组合子进行改造,使其能够真正的处理左递归语法的解析与匹配。
hi, 我是快手电商的Kaso.Lu
快手电商无线技术团队正在招贤纳士🎉🎉🎉! 我们是公司的核心业务线, 这里云集了各路高手, 也充满了机会与挑战. 伴随着业务的高速发展, 团队也在快速扩张. 欢迎各位高手加入我们, 一起创造世界级的电商产品~
热招岗位: Android/iOS 高级开发, Android/iOS 专家, Java 架构师, 产品经理(电商背景), 测试开发... 大量 HC 等你来呦~
内部推荐请发简历至 >>>我们的邮箱: hr.ec@kuaishou.com <<<, 备注我的花名成功率更高哦~ 😘