精读《手写 SQL 编译器 - 文法引见》

1 弁言

文法用来形貌言语的语法划定规矩,所以不仅能够用在编程言语上,也可用在汉语、英语上。

2 精读

我们将一块语法划定规矩称为 发生式,运用 “Left → Right” 示意恣意发生式,用 “Left => Right” 示意发生式的推导历程,比方关于发生式:

E → i
E → E + E

我们举行推导时,能够如许示意:E => E + E => i + E => i + i + E => i + i + i

也有运用 Left : Right 示意发生式的例子,比方 ANTLR。
BNF 范式经由过程 Left ::= Right 示意发生式。

举个例子,比方 SELECT * FROM table 能够被表达为:

S → SELECT * FROM table

固然这是最牢固的语法,实在场景中,* 能够被替换为其他单词,而 table 不只能够有其他名字,还多是个子表达式。

平经常使用大写的 S 示意文法的开首,称为最先标记。

闭幕符与非闭幕符

下面为了轻易誊写,运用 BNF 范式示意文法。

闭幕符就是语句的闭幕,读到它示意发生式剖析完毕,相反,非闭幕符就是一个新发生式的最先,比方:

<selectStatement> ::= SELECT <selectList> FROM <tableName>

<selectList> ::= <selectField> [ , <selectList> ]

<tableName> ::= <tableName> [ , <tableList> ]

一切 ::= 号左边的都黑白闭幕符,所以 selectList 黑白闭幕符,剖析 selectStatement 时碰到了 selectList 将会进入 selectList 发生式,而剖析到一般 SELECT 单词就不会继承剖析。

关于有二义性的文法,能够经由过程 上下文相干文法 体式格局形貌,也就是在发生式左边补全前提,处置惩罚二义性:

aBc -> a1c | a2c
dBe -> d3e

平常发生式左边都黑白闭幕符,大写字母黑白闭幕符,小写字母是闭幕符。

上面示意,非闭幕符 Bac 之间时,能够剖析为 12,而在 de 之间时,剖析为 3。但我们能够增添一个非闭幕符让发生式可读性更好:

B -> 1 | 2
C -> 3

如许就将上下文相干文法转换为了上下文无关文法。

上下文无关文法

依据是不是依靠上下文,文法分为 上下文相干文法上下文无关文法,平常来讲 上下文相干文法 都能够转换为一堆 上下文无关文法 来处置惩罚,而用顺序处置惩罚 上下文无关文法 相对轻松。

SQL 的文法就是上下文相干文法,在正式引见 SQL 文法之前,举一个简朴的例子,比方我们形貌等号(=)的文法:

SELECT
  CASE
    WHEN bee = 'red' THEN 'ANGRY'
    ELSE 'NEUTRAL'
  END AS BeeState
FROM bees;

SELECT * from bees WHERE bee = 'red';

上面两个 SQL 中,等号前后的关键字取决于当前是在 CASE WHEN 语句里,照样在 WHERE 语句里,所以我们以为等号所在位置的文法是上下文相干的。

然则当我们将文法粒度变细,将 CASE WHENWHERE 区块离别交由两块文法处置惩罚,将等号这个通用的表达式抽离出来,就能够不关心上下文了,这类体式格局称为 上下文无关文法

附上一个 mysql 上下文无关文法鸠合

左推导与右推导

上面提到的推导标记 => 在现实运转历程当中,显然有两种方向左和右:

E + E => ?

从最左边的 E 最先剖析,称为左推导,对语法剖析来讲是自顶向下的体式格局,经常使用要领是递归下落。

从最右侧的 E 最先剖析,称为右推导,对语法剖析来讲是自底向上的体式格局,经常使用要领是移进、规约。

右推导历程比左推导历程庞杂,所以假如斟酌手写,最好运用左推导的体式格局。

左推导的分支展望

比方 select <selectList>selectList 发生式,它能够示意为:

<SelectList> ::= <SelectList> , <SelectField>
               | <SelectField>

由于它能够睁开:SelectList => SelectList , a => SelectList , b, a => c, b, a。

但顺序执行时,读到这里会进入死循环,由于 SelectList 能够被无穷睁开,这就是左递归题目。

消弭左递归

消弭左递归平常经由过程转化为右递归的体式格局,由于左递归完整不斲丧 Token,而右递归能够经由过程斲丧 Token 的体式格局跳出死循环。

Token 见上一期精读
精读《手写 SQL 编译器 – 词法剖析》

<SelectList> ::= <SelectField> <G>

<G> ::= , <SelectList>
      | null

这现实上是一个通用处置惩罚,能够笼统出来:

E → E + F
E → F
E → FG
G → + FG
G → null

不过我们也不难发明,经由过程通用体式格局消弭左递归后的文法更难以浏览,这是由于用死循环的体式格局诠释题目更轻易让人明白,但会致使机械崩溃。

笔者发起此处不要僵硬的套公式,在套了公式后,再对发生式做一些润饰,让其更具有语义:

<SelectList> ::= <SelectField>
               | , <SelectList>

提取左公因式

即便是上下文无关的文法,经由过程递归下落体式格局,很多时刻也必需从左向右超前检察 K 个字符才肯定运用哪一个发生式,这类文法称为 LL(k)。

但假如每次超前检察的内容都有很多字符雷同,会致使第二次最先的超前检察反复剖析字符串,影响机能。最理想的状况是,每次超前检察都不会对已肯定的字符反复检察,处置惩罚要领是提取左公因式。

想象以下的 sql 文法:

<Field> ::= <Text> as <Text>
          | <Text> as<String>
          | <Text> <Text>
          | <Text>

实在 Text 自身也是比较庞杂的发生式,最坏的状况须要对 Text 一连婚配六遍。我们将 Text 公因式提取出来就能够仅婚配一遍,由于无论是何种 Field 发生式,都一定先碰到 Text:

<Field> ::= <Text> <F>

<F> ::= <G>
      | <Text>

<G> ::= as <H>

<H> ::= <space> <Text>
      | <String>

和消弭左递归一样,提取左公因式也会下降文法的可读性,须要举行工资修复。不过提取左公因式的修复没方法在文法中处置惩罚,在后面的 “函数式” 处置惩罚环节是有方法处置惩罚的,敬请期待。

连系优先级

对 SQL 的文法来讲不存在优先级的观点,所以从某种程度来讲,SQL 的语法庞杂度还不如基础的加减乘除。

3 总结

在完成语法剖析前,须要运用文法形貌 SQL 的语法,文法形貌就是语法剖析的骨干营业代码。

下一篇将引见语法剖析相干学问,协助你一步步打造本身的 SQL 编译器。

4 更多议论

议论地点是:
精读《手写 SQL 编译器 – 文法引见》 · Issue #94 · dt-fe/weekly

假如你想介入议论,请点击这里,每周都有新的主题,周末或周一宣布。

    原文作者:黄子毅
    原文地址: https://segmentfault.com/a/1190000015646040
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞