【工具】yylex和ANTLR

何时应当创建一个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来避免这种行为。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值