前言
相信大家已经熟悉了一些jison的写法了.
这个系列将会分为三个系列
- lexer/parser
- AST
- 数据库
下面先写一点简单的'.jison'语法文件
例子
// grammar.jison
%lex
IDENTIFIER ([a-zA-Z][a-zA-Z0-9_]*|(\`[^\`]*\`))
STRING_LITERAL ((\"[^\"]*\")|(\'[^\']*\'))
INT_LITERAL [+-]?[0-9]+
%x COMMENT
%%
\s+ /* skip whitespaces */
"--".* /* skip comments */
"/*" this.begin('comment');
SELECT return 'SELECT';
FROM return 'FROM';
{IDENTIFIER} return 'IDENTIFIER';
{STRING_LITERAL} return 'STRING_LITERAL';
{INT_LITERAL} yytext = parseInt(yytext); return 'INT_LITERAL';
"," return ',';
"*" return '*';
";" return ';';
"." return '.';
. throw new Error('Illegal token: `' + yytext + '`');
<COMMENT>"*/" this.begin('INITIAL');
<COMMENT>\n /* skip new line in comments */
/lex
%token STRING_LITERAL INT_LITERAL IDENTIFIER
%start statements
%%
statements:
statements statement {}
| statement {}
;
statement:
select_statement ';' {}
;
select_statement:
SELECT column_list_or_wild FROM table_name {}
;
column_list_or_wild:
column_list {}
| '*' {}
;
table_name:
table_name '.' IDENTIFIER {}
| IDENTIFIER {}
;
column_list:
column_list ',' IDENTIFIER {}
| IDENTIFIER {}
;
--test.sql
SELECT a,b FROM a;
SELECT * FROM `a`.`b`;
通过jison ./grammar.jison -o parser.js
来生成一个编译器前端.
现在在运行node parser.js test.sql
来看一下. 没有输出, 但说明语法通过了, 是因为我们没有做任何输出处理, 接下来我们就一段段的过刚才的代码.
分析
分词
%lex
用来标识开始lex脚本啦.
IDENTIFIER ([a-zA-Z][a-zA-Z0-9_]*|(\`[^\`]*\`))
STRING_LITERAL ((\"[^\"]*\")|(\'[^\']*\'))
INT_LITERAL [+-]?[0-9]+
知道接下来的%%
之前, 都是用来设置一些option, 然后定义一些RegExp, 以免下面的token描述过于拥挤
%x COMMENT
%%
\s+ /* skip whitespaces */
"--".* /* skip comments */
"/*" this.begin('comment');
SELECT return 'SELECT';
FROM return 'FROM';
{IDENTIFIER} return 'IDENTIFIER';
{STRING_LITERAL} return 'STRING_LITERAL';
{INT_LITERAL} yytext = parseInt(yytext); return 'INT_LITERAL';
"," return ',';
"*" return '*';
";" return ';';
"." return '.';
. throw new Error('Illegal token: `' + yytext + '`');
<COMMENT>"*/" this.begin('INITIAL');
<COMMENT>\n /* skip new line in comments */
/lex
这段定义了我们所有的token. 其中%x COMMENT
表示, 初始的时候, COMMENT
状态的token(<COMMENT>"*/"
)不会被匹配. 而默认的状态是INITIAL
, 所以通过this.begin()
这个API我们可以在各种定义的状态间跳转, 这样就实现了C-Style
多行注释.
yytext
是一个内建的宏, 表示当前token被匹配到的原文.
最后/lex
告诉jison
结束token的匹配
语法
接下来的代码定义了我们的token是如何组合起来的.
%token STRING_LITERAL INT_LITERAL IDENTIFIER
告诉了jison
我们需要取这几个token
的值(不像SELECT
, 我们不需要知道它的原文是什么).
%start statements
告诉了jison
我们的语法分析是从statements
这里进入.
接下来的代码都是比较简单好理解的, 实现了一个最基础的select
语法.
需要注意的是, 我们要处理 aaa, bbb, ccc
这种形式的原文.
我们可以通过
column_list:
column_list ',' IDENTIFIER {}
| IDENTIFIER {}
;
这种方式, 来循环匹配到所有的参数.
解析的过程:
原文: aaa, bbb, ccc
aaa, bbb => column_list , ccc => IDENTIFIER
aaa => column_list, bbb => IDENTIFIER
aaa => IDENTIFIER
好了, 这样我们就有了一个最简单的select
解释器. 接下来的文章我们将逐步完善实现SQL的脚本, 甚至添加一些我们自己的特性.