1 弁言
接着上周的文法引见,本周引见的是语法剖析。
以剖析递次为角度,语法剖析分为两种,自顶而下与自底而上。
自顶而下平常采纳递归下落体式格局处置惩罚,称为 LL(k),第一个 L 是指从左到右剖析,第二个 L 指从左最先推导,k 是指超前检察的数目,假如完成了回溯功用,k 就是无限大的,所以带有回溯功用的 LL(k) 险些是最壮大的。LL 系列平常分为 LL(0)、LL(1)、LL(k)、LL(∞)。
自底而上平常采纳移进(shift)规约(reduce)体式格局处置惩罚,称为 LR,第一个 L 也是从左到右剖析,第二个 R 指从右最先推导,而规约时能够发生争执,所以经由历程超前检察一个标记处理争执,就有了 SLR,背面另有功用更强的 LALR(1) LR(1) LR(k)。
经由历程这张图能够看到 LL 家属与 LR 家属的才能局限:
如图所示,不管 LL 照样 LR 都处理不了二义性文法,还好一切计算机语言都属于无二义性文法。
值得一提的是,假如完成了回溯功用的 LL(k) -> LL(∞),那末才能就可以够与 LR(k) 所比肩,而 LL 系列手写起来更易读,所以笔者采纳了 LL 体式格局誊写,本日引见怎样手写无回溯功用的 LL。
别的也有一些依据文法自动天生 parser 的库,比方兼容多语言的
antlr4 或许对 js 支撑比较友爱的
pegjs。
2 精读
递归下落能够理解为走多出口的迷宫:
我们先依据 SQL 语法构造一个迷宫,进迷宫的不是探险家,而是 SQL 语句,这个 SQL 语句会拿上一堆令牌(切分好的 Tokens,概况见 精读:词法剖析),迷宫每行进一步都邑请求按递次给出令牌(交上去就充公),假如走到出口令牌正好交完,就胜利走出了迷宫;假如出迷宫时手上另有令牌,会被迷宫工作人员带走。这个迷宫会有一些分叉,在分岔道上会请求你亮出几个令牌中恣意一个即可经由历程(LL1),有的迷宫许可你失利了存档,只需没有走出迷宫,都能够读档重来(LLk),理论上能够构造一个最宽大的迷宫,只需还没走出迷宫,能够在分叉处恣意读档(LL∞),这个留到下一篇文章引见。
词法剖析
起首对 SQL 举行词法剖析,拿到 Tokens 列表,这些就是探险家 SQL 带上的令牌。
依据上次讲的内容,我们对 select a from b
举行词法剖析,能够拿到四个 Token(疏忽空格与解释)。
Match 函数
递归下落最主要的就是 Match 函数,它就是迷宫中讨取令牌的关卡。每一个 Match 函数只需婚配上当前 Token 便将 Token index 下移一名,假如没有婚配上,则不斲丧 Token:
function match(word: string) {
const currentToken = tokens[tokenIndex] // 拿到当前地点的 Token
if (currentToken.value === word) {
// 假如 Token 婚配上了,则下移一名,同时返回 true
tokenIndex++
return true
}
// 没有婚配上,不斲丧 Token,然则返回 false
return false
}
Match 函数就是精简版的 if else,试想下面一段代码:
if (token[tokenIndex].value === 'select') {
tokenIndex++
} else {
return false
}
if (token[tokenIndex].value === 'a') {
tokenIndex++
} else {
return false
}
经由历程不停对照与挪动 Token 举行推断,等价于下面的 Match 完成:
match('select') && match('a')
如许写出来的语法剖析代码可读性会更强,我们能专注精力在对文法的解读上,而疏忽其他环境要素。
趁便一提,下篇文章笔者会带来更精简的形貌要领:
chain('select', 'a')
让函数式语法更靠近文法情势。
末了这类语法不只形貌更加精简,而且具有 LL(∞) 的查找才能,具有险些最壮大的语法剖析才能。
语法剖析主体函数
既然关卡(Match)已有了,下面最先构造主函数了,能够最先画迷宫了。
举个最简朴的例子,我们想婚配 select a from b
,只须要这么构造主函数:
let tokenIndex = 0
function match() { /* .. */ }
const root = () => match("select") && match("a") && match("from") && match("b")
tokens = lexer("select a from b")
if (root() && tokenIndex === tokens.length) {
// sql 剖析胜利
}
为了简化流程,我们把 tokens、tokenIndex 作为全局变量。起首经由历程 lexer
拿到 select a from b
语句的 Tokens:['select', ' ', 'a', ' ', 'from', ' ', 'b']
,注重在语法剖析历程当中,解释和空格能够消弭,如许能够省去对空格和解释的推断,大大简化代码量。所以终究拿到的 Tokens 是 ['select', 'a', 'from', 'b']
。
很明显如许与我们构造的 Match 行列相吻合,所以这段语句顺遂的走出了迷宫,而且走出迷宫时,Token 正好被消耗完(tokenIndex === tokens.length
)。
如许就完成了最简朴的语法剖析,一共十几行代码。
函数挪用
函数挪用是 JS 最最基础的学问,但用在语法剖析里可就不那末一样了。
斟酌上面最简朴的语句 select a from b
,明显没法胜任真正的 SQL 环境,比方 select [位置] from b
这个位置能够安排恣意用逗号相连的字符串,我们假如将这类 SQL 睁开形貌,将非常复杂,难以浏览。正好函数挪用能够帮我们圆满处理这个题目,我们将这个位置笼统为 selectList
函数,所以主语句革新以下:
const root = () =>
match("select") && selectList() && match("from") && match("b")
这下可否剖析 select a, b, c from table
就看 selectList
这个函数了:
const selectList =
match("a") && match(",") && match("b") && match(",") && match("c")
明显如许做不具备通用性,由于我们将参数名与数目牢固了。斟酌到上期精读学到的文法,我们能够如许形貌 selectList
:
selectList ::= word (',' selectList)?
word ::= [a-zA-Z]
有意绕过了左递归,采纳右递归的写法,因而避开了语法剖析的中心难点。
? 号是可选的意义,与正则的 ? 相似。
这是一个右递归文法,不难看出,这个文法能够云云睁开:
selectList => word (‘,’ selectList)? => a (‘,’ selectList)? => a, word (‘,’ selectList)? => a, b, word (‘,’ selectList)? => a, b, word => a, b, c
我们一下碰到了两个题目:
- 补充 word 函数。
- 怎样形貌可选参数。
同理,应用函数挪用,我们假定具有了可选函数 optional
,与函数 word
,如许能够先把 selectList
函数形貌出来:
const selectList = () => word() && optional(match(",") && selectList())
如许就经由历程可选函数 optional
形貌了文法标记 ?
。
我们来看 word
函数怎样完成。须要简朴革新下 match
使其支撑正则,那末 word
函数能够如许形貌:
const word = () => match(/[a-zA-Z]*/)
而 optional
不是一般的 match
函数,从挪用体式格局就可以看出来,我们提到下一节细致引见。
注重 selectList
函数的尾部,经由历程右递归的体式格局挪用 selectList
,因而能够剖析恣意长度以 ,
支解的字段列表。
Antlr4 支撑左递归,因而文法能够写成 selectList ::= selectList (, word)? | word,用在我们这个简化的代码中会致使客栈溢出。
在引见 optional
函数之前,我们先引出分支函数,由于可选函数是分支函数的一种特别情势(猜猜为何?)。
分支函数
我们先看看函数 word
,实在没有斟酌到函数作为字段的状况,比方 select a, SUM(b) from table
。所以我们须要晋级下 selectList
的形貌:
const selectList = () => field() && optional(match(",") && selectList())
const field = () => word()
这时候注重 field
作为一个字段,也多是文本或函数,我们假定具有函数处置惩罚函数 functional
,那末用文法形貌 field
就是:
field ::= text | functional
|
示意分支,我们用 tree
函数示意分支函数,那末能够云云改写 field
:
const field = () => tree(word(), functional())
那末改怎样示意 tree
呢?根据分支函数的特性,tree
的职责是超前检察,也就是超前检察 word
是不是相符当前 Token 的特性,怎样相符,则此分支能够走通,假如不相符,同理继承尝试 functional
。
若存在 A、B 分支,由因而函数式挪用,若 A 分支为真,则函数客栈退出到上层,若后续尝试失利,则没法再回到分支 B 继承尝试,由于函数栈已退出了。这就是本文开首提到的
回溯 机制,对应迷宫的
存档、读档 机制。要完成回溯机制,要模仿函数实行机制,拿到函数挪用的控制权,这个下篇文章再细致引见。
依据这个特性,我们能够写出 tree
函数:
function tree(...args: any[]) {
return args.some(arg => arg())
}
根据递次实行 tree
的入参,假如有一个函数实行动真,则跳出函数,假如一切函数都返回 false,则这个分支结果为 false。
斟酌到每一个分支都邑斲丧 Token,所以我们须要在实行分支时,先把当前 TokenIndex 保存下来,假如实行胜利则斲丧,实行失利则复原 Token 位置:
function tree(...args: any[]) {
const startTokenIndex = tokenIndex
return args.some(arg => {
const result = arg()
if (!result) {
tokenIndex = startTokenIndex // 实行失利则复原 TokenIndex
}
return result
});
}
可选函数
可选函数就是分支函数的一个惯例,能够形貌为:
func? => func | ε
ε 示意空,也就是这个发生式剖析到这里永久能够剖析胜利,而且不斲丧 Token。借助分支函数 tree
实行失利后复原 TokenIndex 的特性,我们先尝试实行它,实行失利的话,下一个 ε 函数肯定返回 true,而且会重置 TokenIndex 且不斲丧 Token,这与可选的寄义是等价的。
所以能够如许形貌 optional
函数:
const optional = fn => tree(fn, () => true)
基础的运算衔接
上面经由历程对 SQL 语句的实践,发现了 match
婚配单个单词、 &&
衔接、tree
分支、ε
空字符串的发生式这四种基础用法,这是相符下面四个基础文法组合头脑的:
G ::= ε
空字符串发生式,对应 () => true
,不斲丧 Token,老是返回 true
。
G ::= t
单词婚配,对应 match(t)
。
G ::= x y
衔接运算,对应 match(x) && match(y)
。
G ::= x
G ::= y
并运算,对应 tree(x, y)
。
有了这四种基础用法,险些能够形貌一切 SQL 语法。
比方简朴形貌一下 select 语法:
const root = () => match("select") && select() && match("from") && table()
const selectList = () => field() && optional(match(",") && selectList())
const field = () => tree(word, functional)
const word = () => match(/[a-zA-Z]+/)
3 总结
递归下落的 SQL 语法剖析就是一个走迷宫的历程,将 Token 从左到右逐一婚配,终究能找到一条线路完整贴合 Token,则 SQL 剖析圆满结束,这个迷宫采纳空字符串发生式、单词婚配、衔接运算、并运算这四个基础文法组合就足以组成。
控制了这四大宝贝,基础的 SQL 剖析已难不倒你了,下一步须要做这些优化:
- 回溯功用,完成它才能够完成 LL(∞) 的婚配才能。
- 左递归自动消弭,由于经由历程文法转换,会转变文法的结合律与语义,最好能完成左递归自动消弭(左递归在上一篇精读 文法 有申明)。
- 天生语法树,仅婚配语句的正确性是不够的,我们还要依据语义天生语法树。
- 毛病搜检,在毛病的处所给出发起,以至对某些毛病做自动修复,这个在左 SQL 智能提醒时须要用到。
- 毛病恢复。
下篇文章会引见怎样完成回溯,让递归下落到达 LL(∞) 的结果。
从本文不难看出,经由历程函数挪用体式格局我们没法做到 迷宫存档和读档机制,也就是碰到岔道 A B 时,假如 A 胜利了,函数挪用栈就会退出,而背面迷宫探究失利的话,我们没法回到岔道 B 继承探究。而 回溯功用就给予了这个探险者返回岔道 B 的才能。
为了完成这个功用,险些要完整颠覆这篇文章的代码构造构造,不过别忧郁,这四个基础组合头脑还会保存。
下篇文章也会放出一个真正能运转的,完成了 LL(∞) 的代码库,函数形貌更精简,功用(比这篇文章的要领)更壮大,敬请期待。
4 更多议论
假如你想介入议论,请点击这里,每周都有新的主题,周末或周一宣布。