何时应当创建一个DSL?
毫无疑问,当你需要解析一些语法结构,比如SQL语句。
yylex
https://www.gnu.org/software/bison/manual/html_node/Lexical.html
yylex通常是作为bison的一部分输入
bison
BISON
Standford
bison gammmar.y
gammar.y会生成garmmar.tab.c, 注意,目录名会被忽略掉。
如果使用c++,则后缀应当是.ypp.
为了兼容性,定义yacc通常唤起bison -y。
语法结构
%{
C Declarations(optional)
%}
Bison Declarations
%%
Grammar Rules
%%
Additional C Code(optional)
第一条规则是语法的Start规则。
例子:
%{
#include <stdio.h>
#include <assert.h>
static int Pop();
static int Top();
static void Push(int val);
%}
%token T_Int
%%
S : S E '\n' { printf("= %d\n", Top()); }
|
;
E : E E '+' { Push(Pop() + Pop()); }
| E E '-' { int op2 = Pop(); Push(Pop() - op2); }
| E E '*' { Push(Pop() * Pop()); }
| E E '/' { int op2 = Pop(); Push(Pop() / op2); }
| T_Int { Push(yylval); }
;
%%
static int stack[100], count = 0;
static int Pop() {
assert(count > 0);
return stack[--count];
}
static int Top() {
assert(count > 0);
return stack[count-1];
}
static void Push(int val) {
assert(count < sizeof(stack)/sizeof(*stack));
stack[count++] = val;
}
int main() {
return yyparse(); // bison会生成yyparse函数,从标准输入读取数据,然后解析符号,如果遇到错误会调用yyerror,默认情况下打印错婿消息
}
flex
%{
#include "y.tab.h"
%}
%%
[0-9]+ { yylval = atoi(yytext); return T_Int;}
[-+*/\n] { return yytext[0];}
. { /* ignore everything else */ }
Makefile
calc: lex.yy.o y.tab.o
gcc -o calc lex.yy.o y.tab.o -ly -ll
lex.yy.c:calc.l y.tab.c
flex calc.l
y.tab.c: calc.y
bison -vdty calc.y
生成式的规则:
left_side: right_side1 { action1 }
| right_side2 { action2 }
| right_side3 { action3 }
;
其中左边是非终结式,右边是左边的合法展开式。
对于终结式(不可展开),可以使用yylval来获取token,对于非终结式,可以使用栈来操作属性。
注意: %union{}指令就是用于定义yylval的类型的。
%union {
int intValue;
double doubleValue;
char *stringValue;
}
%token指令可以指定yylval的类型
%token <intValue>T_Int <doubleValue>T_Double T_While T_Return
来自GNU的文档
bison是如何调用yylex的
yylex函数返回的值必须是一个正整数,0和负数意味着输入结束。
int
yylex (void)
{
…
if (c == EOF) /* Detect end-of-input. */
return YYEOF;
…
else if (c == '+' || c == '-')
return c; /* Assume token kind for '+' is '+'. */
…
else
return INT; /* Return the kind of the token. */
…
}
特殊符号: YYEOF表示输入结束,没有符号了。
YYUNDEF 表示非法定义 “invalid token”
YYerror 表示parser应当进入错误恢复模式,但是不需要展示错误信息
Flex & Bison Book
正则表达式:使用flex实现wc程序
flex语法分为3个部分
%{
C Declarations
%}
%%
Tokens
%%
C Codes
a.l文件:
%{
int chars=0;
int words=0;
int lines=0;
%}
%%
[a-zA-Z]+ { words++;chars += strlen(yytext);}
\n { chars++;lines++;}
. {chars++;}
%%
int main(int argc,char** argv){
yylex();
printf("%8d%8d%8d\n",lines,words,chars);
}
flex a.l
gcc lex.yy.c -lfl
echo yes|./a.out
注意,如果你使用的gcc找不到-lfl, 可以加上-L…/flex/lib
可以通过brew install flex来安装flex所需的依赖。
flex匹配优先级:按最长匹配,所有.是最短的,其次,按先后顺序选择匹配关系。
flex处理输入时,首先尝试匹配最长的规则,直到没有一个规则能够匹配;如果有几个规则的长度都是相同的,则选取靠前的那个。
如
"+"
"="
"+="
+=将被优先选择而不是+.
flex正则表达式规则:
1."…" 表示字面量,不需要按正则表达式处理,其他的规则同一般的正则表达式
2./xxx 表示前向匹配,仅当前面是/xxx时匹配。如 0/1,则只有01中的0能够匹配,02不能匹配,0也不能匹配
一个计算器的例子:flex+bison组合
flex忽略:
"+" { return ADD; }
[0-9]+ { return NUMBER; }
[ \t] { /* ignore whitespace */ }
在上面的例子中,当bison调用yylex()来获取下一个符号时,如果遇到空白符号,yylex会跳过这个符号继续执行下一个。
token和value
每一个token都具有一个token类型和token的值,bison会生成类型枚举在.h文件中,从258开始(避免与8位的ascii编码冲突).
%{
enum yytokentype{
NUMBER = 258,
ADD = 259,
SUB = 260,
EOF=264,
}
%}
%%
"+" { return ADD;}
"-" { return SUB;}
[0-9]+ { yylval = atoi(yytext);return NUMBER;}
\n { return EOL;}
[ ] { /*ignore*/ }
%%
int main(int argc,char **argv){
int tok;
while(ton = yylex()){
printf("%d",tok)
if(tok == NUMBER) printf(" = %d\n", yylval);
else printf("\n");
}
}
bison代码
%{
#include <stdio.h>
%}
%token NUMBER
%token ADD SUB MUL DIV ABS
%token EOL
%%
calclist: calclist exp EOL { printf("= %d\n",$1); }
;
exp: factor
| exp ADD factor {$$ = $1 + $3;}
| exp SUB factor {$$ = $1 - $3;}
;
....
%%
int main(){
yyparse();
}
int yyerror(char* s){
fprintf(stderr,"error:%s\n",s);
}
%token指令用于告诉bison定义token,任何在语法中没有通过%token定义的词语,都视为语法的rule的一部分而不是token。
每个rule都有值,叫做$$,$i是子规则中的值。
如果一条rule的子规则没有action,默认的action就是
$$ = $1
flex和bison共同编译
1.flex中的token定义代码要删除,换成bison自动生成的token定义header引入,同时引入yylval的结构定义
2.flex中的main程序要去掉
bison -d test.y
flex test.l
gcc -o test test.tab.c lex.yy.c -lfl
如果定义
expr: NUMBER
| expr ADD expr { $$ = $1 + $3 ;}
| expr SUB expr { $$ = $1 - $3;}
;
bison会指出语法是有歧义的,比如1+2-3。理论上来说,bison会拒绝有歧义的语法定义,但是可以通过指定优先级来消除歧义。
通过将上面的expr修改成含有终结式的语法,可以消除歧义
expr:term
| expr ADD term { $$ = $1 + $3 ;}
| expr SUB term { $$ = $1 - $3;}
;
term: NUMBER
;
%%
flex的IO选择
flex读入的流时yyin,默认情况下是stdin.需要在yylex()之前设置。
默认的flex main代码:
int main() {
while (yylex() != 0) ;
return 0;
}
大部分情况下你应当自定义自己的main函数。
读取多个文件:
for(...){
f = fopen(...)
yyrestart(f)
yylex()
fclose(f)
}
yyrestart将yyin设置为f。
flex会检测yyin,如果是console,它启用按字符读取;如果是文件,就按块读取。所以,输入的类型会影响flex的io性能。
输出:默认情况下,如果一个规则没有action,flex会将其写入到yyout
但是建议通过%option nodefault
关闭这种行为。
%option yylineno
定义yylineno可用
%option case-insensitive
大小写不敏感
如何匹配注释
使用正则表达式来匹配注释会相当啰嗦和复杂,在flex中可以通过BEGIN(MODE), BEGIN(INITIAL)来切换模式,并且通过<MODE>pattern
来指定pattern生效的范围
%option nodefault
%x COMMENT
"/*" {BEGIN(COMMENT);}
<COMMENT>"*/" {BEGIN(INITIAL);}
<COMMENT>([^*]|\n)+|. /*ignore*/
使用Bison
在Bison的语法中,终结符号即可以用token来定义,也可以用字符串字面量
expr: epxr '+' term
关于shift/reduce: 每当parser读入一个token,不能满足一个rule时,这个token会被压入栈中,称为shift;当发现一个满足的规则时,就从栈中弹出所有的token,这个过程称为reduce。
Action就是在reduce时执行的。
关于LALR(1): 表示Look Ahead Left to Rigth with 1 token ahead
在使用这个模式时,Bison最多向前查询一个token,如果不能确定规则,就报错误。
AST:语法里面的某些规则是为了消除歧义的,但是创建AST之后,这些不用的规则应当消除。
声明:
/* interface to the lexer */
extern int yylineno; /* from lexer */
void yyerror(char *s, ...);
%union {
struct ast *a;
double d;
}
%token <d> NUMBER
%type <a> exp factor term
%union声明yylval的结构,token中的<d>
告诉bison这个token应当设置到哪个域, %type则是设置rule的域。
flex程序
/* recognize tokens for the calculator */ %option noyywrap nodefault yylineno
%{
# include "fb3-1.h"
# include "fb3-1.tab.h"
%}
/* float exponent */
EXP ([Ee][-+]?[0-9]+)
%%
"+" |
"-" |
"*" |
"/" |
"|" |
"(" |
")" { return yytext[0]; }
[0-9]+"."[0-9]*{EXP}? |
"."?[0-9]+{EXP}? { yylval.d = atof(yytext); return NUMBER; }
\n "//".* [ \t]
.
%%
{ return EOL; }
{ /* ignore whitespace */ }
{ yyerror("Mystery character %c\n", *yytext); }
这里直接返回了字符,而且使用yylval.d而不是yylval来进行赋值。
y.output 使用--report=all
来生成一个log文件。用于调试bison。
Sql Create语句的解析
略
优先级和结合性
%left '+' '-'
%left '*' '/'
%right '='
%nonassoc '|' UNMINUS
%%
'-' exp %prec UMINUS {...}
上面的语法定义了’+‘和’-‘具有左结合性,并且优先级最低, ‘*’ 和’-‘具有左结合性并且优先级比’+’ '-'高, '|'不结合,具有最高优先级,(还可以使用 %right)
注意, '-'作为负数符号时,优先级是最高的,因此此时不能当成运算符来看待,所以使用%prec在查询优先级时使用UNMINUS这个伪token.
什么时候应当使用优先级:答案是仅在表达式中使用,其他语句的优先级可能会引起不知名的错误。你应当尽量调整语法来避免shift/reduce冲突。
RPN
Reversed Polish Notation 逆波兰表达法
expr: NAME { emit("NAME %s",$1);}
| INTNUM { emit("NUMBER %d",$1);}
| expr '+' expr { emit("ADD");}
| expr '-' expr { emit("SUB");}
...
实际上,逆波兰表达式与栈息息相关。遇到终结式时,往栈中压入表达式结果;在遇到非终结式时,往栈中压入操作符号,然后结合栈上最近的两个元素进行结果计算,新的结果仍然压入栈中。
实际上,也可以选择保留栈上的操作符,最终RPN可以很容易地转换成AST。
对于SELECT a,b,c FROM d
,RPN如下:
rpn: NAME a
rpn: NAME b
rpn: NAME c
rpn: TABLE d
rpn: SELECT 3
其实NAME不一定就是终结式,这种情况下,NAME可以含有一个前缀,因此可以表示成
rpn: REF t
rpn: COL a
rpn: TABLE 2
相关的上下文信息都在栈中。
Flex再探
yywrap() 如果开发者重新定义了yywrap,则flex使用新的yywrap
BEGIN name
切换到新的状态。flex的起始状态是0,也就是INITIAL,其他状态通过%s, %x来定义.
注意,BEGIN是一个无参的宏,因此也可以使用BEGIN(name)
的形式。
left context: flex通过<STATE>xxx
的形式来定义在特定state执行的动作。^xxx
则是匹配行的开头。
可以通过switch来模拟left context
%{
int flag=0
}%
%%
a {flag = 1;}
b {flag = 2;}
zzz {
switch(flag){
case 1: ....;break;
case 2: .....;break
}
flag=0
}
right context: xxx$
表示遇到换行才匹配(\n), xxx/rigth
表示遇到right才匹配,注意/只能用在末尾。
abcde { yyless(3);}
与 abc/de
有相同的作用,就是将匹配回退两个字符,也就是说,匹配玩模式之后,将两个字符重新放入进行下一次匹配。
模式定义:模式定义允许你对常用的正则表达式进行命名然后引用 NAME expression
比如: DIGIT [0-9]
,然后通过{DIGIT}
进行引用。
ECHO:一个宏, fprintf(yyout,"%s",yytext)
默认情况下,如果一个字符不匹配任何模式,就是用ECHO, %option nodefault
将行为替换为abort.
yylex(): 当yylex遇到一个没有ACTION,在调用yylex()时会跳过这个token.
yywrap(): 当lex遇到文件结束时,调用yywrap来询问是否还要继续扫描,默认yywrap返回1,表示不扫描。返回0,表示下一个文件已经准备好。可以通过%option noyywrap
来避免这种行为。