03-语法解析

词法解析(flex)和语法解析(bison)

自然语言中语言学习包含:单词和语法。

在编程语言中也包含单词和语法,分别有flex和bison来实现。

所以flex和bison的作用就是:用C语言来实现另外一种语言(SQL、python等)

flex :LEX 词法分析 (lexical analysis)

单词提取:提取编程语言占用的各种保留字、操作符等等语言的元素

lex把每个扫描出来的单词叫做token,token可以有很多类。对比自然语言中名词、形容词等,而汽车就是属于名词类型的一个具体token。在编程语言token类型时有限的:关键字。

lex工具会帮我们生成一个yylex函数,yacc通过调用这个函数来得知拿到的token是什么类型的,但是token的类型是在yacc中定义的。

lex的输入文件一般会被命名成 .l文件,通过执行命令 lex XX.l 我们得到输出的文件是lex.yy.c

bison :YACC 语法分析syntactic analysis

语法表达:如何使用LEX中的“单词”组合成语法

yacc会帮我们生成一个yyparse函数,这个函数会不断调用上面的yylex函数来得到token的类型。

yacc的输入文件一般会被命名成 .y文件,通过yacc -d XX.y我们得到的输出文件是y.tab.h y.tab.c,前者包含了lex需要的token类型定义,需要被include进 .l文件中

lex和yacc的输入文件格式

Definition section
%%
Rules section
%%
C code section

.l和.y的文件格式都是分成三段,用%%来分割,含义如下:

  • Definition Section

可以放C语言的各种各种include,define等声明语句,但是要用%{ %}括起来。

如果是.l文件,可以放预定义的正则表达式:minus “-” 还要放token的定义,方法是:代号 正则表达式。然后到了,Rules Section就可以通过{符号} 来引用正则表达式

如果是.y文件,可以放token的定义,如:%token INTEGER PLUS ,这里的定一个的每个token都可以在y.tab.h中看到

  • Rules section

.l文件在这里放置的rules就是每个正则表达式要对应的动作,一般是返回一个token

.y文件在这里放置的rules就是满足一个语法描述时要执行的动作

不论是.l文件还是.y文件这里的动作都是用{}扩起来的,用C语言来描述,这些代码可以做你任何想要做的事情

  • C code Section

main函数,yyerror函数等的定义

YACC声明部分

标识符 含义
%start 指定起始规则
%token 定义文法中的终结符
%left 左结合
%right 右结合
%nonassoc 不能结合
%union: 声明标识出的符号值可能拥有的所有C类型
%type: 声明一些有特定语义值类型的非终结符

实现步骤

  • 在.y中定义token类型。token既会被lex使用到,也会被.y文件中的BNF使用到。

  • 在.l文件(就是lex的输入文件)中写词汇分析代码。

定义方式是:正则表达式–>对应操作。如果和yacc一起来使用的话,对应的操作通常是返回一个token类型,这个token的类型要在yacc中提前定义好。

  • 写BNF。定义语言的规约方式。

BNF

用于对编程语言、文档格式、指令集、或者通信协议等的语法定义

在YAGG中书写方式如下:

  <symbol> : __expression1__ {operation}
  
  		 | __expression2__ {operation}

operation 是 满足语法时要执行的C语言代码,这里的C语言代码可以使用一些变量,他们是:$$ 1 ! [ ] ( h t t p s : / / g . y u q u e . c o m / g r / l a t e x ? 2 1 ![](https://g.yuque.com/gr/latex?2%E7%AD%89%E7%AD%89%E3%80%82#card=math&code=2%E7%AD%89%E7%AD%89%E3%80%82) 1![](https://g.yuque.com/gr/latex?2代表规约的结果,就是表达式symbol的值,$1代表的是前面 __expression1__中出现的各个word

例子

  expr2: expr3 { $$ == $1; }
  	   | expr2 PLUS expr3 { $$ = plus($1, $3); }
  	   | expr2 MINUS expr3 { $$ = minus($1, $3); }
  ;
  • expr2 expr3都是BNF中定义的non-terminal

    • PLUS和MINUS都是.y中定义的token类
    • plus和minus 是事先定义好的C语言函数
    • expr2表示递归调用
    • “|” 表示或的情况

移进(shift)和规约(reduce)

  • yacc 在内部维护着两个堆栈;一个分析栈和一个内容栈。分析栈中保存着终结符和非终结符,并且代表当前剖析状态。内容栈是一个YYSTYPE 元素的数组,对于分析栈中的每一个元素都保存着一个对应的值。例如,当yylex 返回一个INTEGER标记时,y acc 把这个标记移入分析栈。同时,相应的yylval 值将会被移入内容栈中。分析栈和内容栈的内容总是同步的,因此从栈中找到对应于一个标记的值是很容易实现的。

  • expr: expr1 '+' expr2 { $$ = $1 + $3; }来说,在分析栈中我们其实用左式替代了右式。在本例中,我们弹出expr1 '+' expr2 然后压入expr。 我们通过弹出三个成员,压入一个成员缩小的堆栈。在我们的C 代码中可以用通过相对地址访问内容栈中的值,“ $1”代表右式中的第一个成员,“ $2”代表第二个,后面的以此类推。“ $$ ”表示缩小后的堆栈的顶部。在上面的动作中,把对应两个表达式的值相加,弹出内容栈中的三个成员,然后把造得到的和压入堆栈中。这样,分析栈和内容栈中的内容依然是同步的。

移进:用一个寄存符号的先进后出栈,把输入符号一个一个地移进到分析栈里,这就是移进。上述的expr1expr2

规约:当栈顶形成某个产生式的表达式时,即把栈顶的这一部分替换成(归约为)该产生式的左部符号。上述中的expr

移进规约冲突

当移进和规约同事满足时,不知道应该执行移进还是规约。

  expr:
       expr - expr
       | expr * expr
       | - expr

对于输入:

  • - 1 * 2

解析完1后,可以继续移进 * ,或者根据规则 expr:-expr 规约为 -1。也就是说,解析方式有两种:

-1 * 2   --->     = (-1)*2

-1 * 2   --->     = -(1*2)

解决方法

定义优先级:先定义的优先级低,后声明的优先级高,同行的优先级相同

  %left '+' '-'
  %right '*' '/'
  %nonassoc '<'

注意:语法解析中很多都是移进规约冲突导致失败,在修改该处代码时需要学习相关语法

编译

  LEX编译	
       flex XXX.l  (编译为lex.yy.c)
       gcc lex.yy.c -lfl  (默认编译为a.out)
       ./a.out
  YACC编译
       bison -d XXX.y   (编译为XXX.tab.c和XXX.tab.h)
       gcc XXX.tab.c   (默认编译为a.out)
       ./a.out

示例

实现简单的计算器语法

calc.y代码如下:

  %{
  #include <stdio.h>
  int yyerror(char *s);
  #include "lex.yy.c"
  %}
  
  %token NUM
  %token ADD SUB MUL DIV
  %token LP RP
  %token EOL
  
  %%
  start:
   | start expr EOL { printf("= %d\n> ", $2); }
   | start EOL { printf("> "); } 
  ;
  
  expr: term { $$ = $1; }
   | expr ADD term { $$ = $1 + $3; }
   | expr SUB term { $$ = $1 - $3; }
   ;
  term: factor { $$ = $1; }
   | term MUL factor { $$ = $1 * $3; }
   | term DIV factor { $$ = $1 / $3; }
   ;
  
  factor: NUM { $$ = $1; }
   | LP expr RP { $$ = $2; }
   ;
  %%
  
  int main(int argc, char **argv)
  {
    printf("> ");
    yyparse();
    return 0;
  }
  
  int yyerror(char *s)
  {
    fprintf(stderr, "error: %s\n", s);
  }
  int yywrap()
  {
  return 1; 
  }

calc.l代码如下:

  %{
  # include "y.tab.h"
  int yylval;
  int lexerror(char *s);
  %}
  
  %%
  "+" { return ADD;}
  "-" { return SUB;}
  "*" { return MUL;}
  "/" { return DIV;}
  "(" { return LP;}
  ")" { return RP;}
  ^[-+][0-9]+ { yylval = atoi(yytext); return NUM;}
  [0-9]+ {yylval = atoi(yytext); return NUM;}
  [\n] { return EOL;}
  [ \t] {/* ignore white space */}
  . { }
  %%
  
  int lexerror(char *s)
  {
      fprintf(stderr, "flex error: %s\n", s);
  }

使用步骤:

  • 生成lex.yy.c、y.tab.c、y.tab.h文件
  yacc -d calc.y
  lex calc.l
  • 编译
  gcc -o calc y.tab.c
  • 执行
  ./calc

生成lex.yy.c、y.tab.c、y.tab.h文件中相关信息解释

Lex 变量

  • yyin

FILE* 类型。 它指向 lexer 正在解析的当前文件。

  • yyout

FILE* 类型。 它指向记录 lexer 输出的位置。 缺省情况下,yyin 和 yyout 都指向标准输入和输出。

  • yytext

匹配模式的文本存储在这一变量中(char*)。

  • yyleng

给出匹配模式的长度。

  • yylineno

提供当前的行数信息。 (lexer不一定支持。)

Lex 函数

  • yylex()

开始分析函数,由 Lex 自动生成。

  • yywrap()

在文件(或输入)的末尾调用。 如果函数的返回值是1,就停止解析。 因此它可以用来解析多个文件。 代码可以写在第三段,这就能够解析多个文件。 方法是使用 yyin 文件指针(见上表)指向不同的文件,直到所有的文件都被解析。 最后,yywrap() 可以返回 1 来表示解析的结束。

  • yyless(int n)

可以用来送回除了前n个字符外的所有读出标记。

  • yymore()

Lexer 将下一个标记附加到当前标记后。

上一篇:tab标签页的切换


下一篇:数据库设计