`

Bison-Flex 笔记

阅读更多

Bison-Flex 笔记

FLEX

什么是 FLEX ?它是一个自动化工具,可以按照定义好的规则自动生成一个 C 函数 yylex() ,也成为扫描器( Scanner )。这个 C 函数把文本串作为输入,按照定义好的规则分析文本串中的字符,找到符合规则的一些字符序列后,就执行在规则中定义好的动作( Action )。例如在规则中可以这样定义:如果遇到一个换行字符 \n ,那么就把行计数器的值加一。

Flex 文件就是一个文本文件,内容包括定义好的一系列词法规则。文件的命名习惯上以小写字母 l(L) 来作为文件后缀。如果为了清晰,也可以用 .flx 或者 .flex 作为文件的后缀名。 Flex 文件完成后,就执行下列命令:

$ flex example.flex

这个命令执行后将生成一个 C 文件,默认文件名为 lex.yy.c 。这个 C 文件主要内容就是函数 yylex() 的定义。

如果要直接将这个文件编译成为一个可执行程序,还有一些要注意的地方。如果在 Flex 文件中没有提供 main() 函数的定义,那么这个 C 文件中不会有 main() 函数。此时单独编译这个 C 文件的时候,一定要加上 -lfl 的连接库参数;若提供了 main() 函数,就不必要提供这个连接库参数了。连接库 libfl 提供了一个缺省的 main 函数。缺省的 main() 函数中只是简单地调用 yyflex() 函数,而自己提供的 main() 函数则可以根据需要加入许多其他的处理代码。

Flex 文件

词法规范定义文件给出了单词构成规则。词法文件在习惯上用字母 l( L 的小写 ) 来作为后缀。 Flex 文件由三个部分组成。或者说三个段。三个段之间用两个 %% 分隔。

定义段 (definitions)

%%

规则段 (rules)

%%

用户代码段 (user code)

定义段 (definitions section)

定义段包含着一些简单名字的定义 (name definitions) ,旨在简化扫描器的规范。定义名字的方法如下 :

name definition

名字可以由字母或下划线开头,后跟零个或多个字母、数字、下划线、或短横线。名字的定义则从其后的第一个非空白字符 (non-white-space) 开始直到行尾。下面是一个例子,定义了一个名字 DIGIT ,其定义就是指一个数字,如下所示:

DIGIT [0-9]

当在后面引用这个名字时,用一对花括号 ({}) 括住该名字即可。它会被展开成一对圆括号括住的该名字的定义 , :

{name} 展开成 (definition)

例如:

{DIGIT}+"."{DIGIT}*

就等价于:

([0-9])+"."([0-9])*

定义段中还可以加入启动条件 (start conditions) 的声明。顾名思义,启动条件就如同 C 语言中的条件编译一样,根据指定的启动条件去激活一条规则,并用这条规则去匹配读入的字符。关于启动条件,后面还有更详细的介绍。

规则段 (rules section)

规则由模式 (pattern) 动作 (action) 两个部分组成。模式 就是一个正则表达式, FLEX 加入了一些自己的扩展。而动作 一般就是一些 C 语句。模式指出了一个单词是如何构成的,当分析出一个符合该规则的单词时,就执行相应的动作。

模式一定要位于一行的开头处,不能有缩进。而动作的开头一定要与模式在同一行。当动作是用一对花括号 {} 括起来时,可以将左花括号放在与规则相同的行,而其余部分则可以从下一行开始。

用户代码段 (user code)

所有用户代码都被原样拷贝到文件 lex.yy.c 中。在这里可以定义一些辅助函数或代码,供扫描器 yylex() 调用,或者调用扫描器(一般来说就是 main() 了)。这一部分是可有可无的。如果没有的话, Flex 文件中第二个 %% 是可以省略的。

在定义段或者规则段中,任何一行有缩进的文本 或者包含在一对 %{ %} 之间的文本 ,都被原样拷贝到最后生成的 C 代码文件中(当然 %{ %} 会被移走)。在书写时 %{ %} 都必须在一行的开始处,不能缩进。

在规则段中,第一条规则之前的任何未缩进的文本或者在 %{ %} 之间的文本,可以用来为扫描器声明一些本地变量和代码。一旦进入扫描器的代码,这些代码就会被执行。规则段内其他的缩进的文本或者 %{ %} 之间的文本还是被原样拷贝输出,但是他们的含义是尚未有明确定义,很可能引起编译时( compile-time )错误(这一特性是为了与 POSIX 兼容而提供的)。

在定义段中,没有缩进的注释也会被原样拷贝到最后生成的 C 代码文件中,例如以 /* 开始的一行注释,直到遇到 */ ,这中间的文本会被原样拷贝输出。

模式及其分类

模式采用正则表达式来书写。正则表达式大致可以分为如下几类(从上到下,优先级依次递减):

1 )单字符匹配

* ‘x’ 匹配字符 x

* ‘.’ 匹配任意一个字符(字节),除了换行符。

* ‘[xyz]’ 匹配单个字符,这个字符是方括号中给出的字符类( character class )中的一个。

* ‘[abj-oZ]’ 匹配单个字符,这个字符是方括号中给出的字符类中的一个。与上一方式的区别是指定字符类时用到了一个范围表示法: j-o ,这表示按照 26 个英文字母的顺序,从字母 j 开始一直到字母 o 6 个字母。这里减号( - )表示范围。如果减号本身也要作为一个匹配字符时,最好用转义字符( \ )去除其特殊含义。由于花括号( {} )在模式中用来引用名字,以及作为模式定义之后的动作( Action )定义块的首尾界定符,因此如果要在字符类中匹配花括号,必须用转义字符( \ )去除其特殊含义。下面这个例子定义了一个所有可打印字符的字符类:

[[:alnum:][:blank:]]\t+\-*/&!_'?@^`~$\\()%|.;[\]\{\}:,#<>=]

* ‘[^A-Z]’ 匹配单个字符,这个字符必须是方括号中给定字符类以外的字符。在方括号内开始处的特殊符号( ^ )表示否定。当字符 ^ 不在字符类的开始处时,并不具有特殊含义,而是一个普通字符。

* ‘[^A-Z\n]’ 匹配单个字符,这个字符不可以是方括号中给出的字符类中的字符。与上一方式的不同在于,这里多了一个换行符,也就是说所匹配的字符不能是 26 个大写字母,也不能是换行符。

根据上面的描述,在表达字符分类时,除了直接用字符以及字符范围来表达外,还有一种叫做字符类表达式 的,也有同样的作用,常见的一些表达式如下:

[:alnum:] [:alpha:] [:blank:] [:cntrl:] [:digit:] [:graph:]

[:lower:] [:print:] [:punct:] [:space:] [:upper:] [:xdigit:]

每一个表达式都指示了一个字符分类,而且其名称与标准 C 函数 isXXXX 的名字对应。例如, [:alnum:] 就指示了那些经由函数 isalnum() 检查后返回 true 的字符,也就是任何的字母或者数字。注意,有些系统上没有给出 C 函数 isblank() 的定义,所以 flex 自己定义了 [:blank:] 为一个空格或者一个 tab

下面所举的几个例子,都是等价的:

[[:alnum:]]

[[:alpha:][:digit:]]

[[:alpha:]0-9]

[a-zA-Z0-9]

应该注意字符类表达式的写法。一个字符类表达式是由一对 [: :] 包住的,作为一个整体,在书写时不可与外层的 [] 混淆。

2 )重复模式的匹配

* ‘r*’ r 是一个正则表达式,特殊字符 `*' 表示 0 个或多个。因此这个模式表示匹配 0 个或多个 r

* ‘r+’ r 是一个正则表达式,特殊字符 `+' 表示 1 个或多个。因此这个模式表示匹配 1 个或多个 r

* ‘r?’ r 是一个正则表达式,特殊字符 `?' 表示 0 个或 1 个。因此这个模式表示匹配 0 个或 1 r 。(从另一个角度看,就是说模式 r 是可选的)

* ‘r{2,5}’ r 是一个正则表达式, {2,5} 表示 2 个到 5 个。因此这个模式表示匹配 2 个到 5 r 。也就是说可以匹配 `rr' `rrr' `rrrr' `rrrrr' 四种重复的模式。

* ‘r{2,}’ r 是一个正则表达式, {2,} 省略了第二个数字,表示至少 2 个,不设上限。因此这个模式表示匹配 2 个及以上个 r 。也就是说至少可以匹配 `rr' ,还可以匹配 `rrr' `rrrr' 等无限多种重复的模式。

* ‘r{4}’ r 是一个正则表达式, {4} 只有一个数字,表示 4 个。因此这个模式确切地匹配 4 r ,即 `rrrr'

3 )名字替换

* ‘{name}’ 这里 name 就是在前面的定义段给出的名字。这个模式将用这个名字的定义来匹配。

4 )平凡( plain )文本串的匹配

* ‘“[xyz]\″foo”’ 这个模式用来确切地匹配文本串: [xyz]\″foo 。注意最外层的单引号所包含的是整个模式表达式,也就是说,当希望匹配字串 [xyz]\″foo 时,在书写规则时该字串必须用双引号括住。

5 )特殊单字符的匹配

* ‘\x’ x 是一个 `a' `b' `f' `n' `r' `t' `v' 时,它就解释为 ANSI-C 中的 \x 。否则就仍然作为一个普通字符 x (一般用于诸如 `*' 字符的转义字符)。

* ‘\0’ 匹配一个 NUL 字符( ASCII 码值为 0 )。

* ‘\123’ 匹配一个字符,其值用八进制表示为 123

* ‘\x2a’ 匹配一个字符,其值用十六进制表示为 2a

6 )组合模式的匹配

* ‘(r)’ 匹配规则表达式 r ,圆括号可以提高其优先级。

* ‘rs’ 匹配规则表达式 r ,其后紧跟着表达式 s 。这称为联接 (concatenation)

* ‘r|s’ 或者匹配规则表达式 r ,或者匹配表达式 s

* ‘r/s’ 匹配模式 r ,但是要求其后紧跟着模式 s 。当需要判断本次匹配是否为 最长匹配( longest match )时,模式 s 匹配的文本也会被包括进来,但完成判断后开始执行对应的动作( action )之前,这些与模式 s 相配的文本会被返还给输入。所以动作( action )只能看到模式 r 匹配到的文本。这种模式类型叫做尾部上下文( trailing context )。(有些 r/s’ 组合是 flex 不能识别的;请参看后面 deficiencies/bugs 一节中的 dangerous trailing context 的内容。)

* ‘^r’ 匹配模式 r ,但是这个模式只出现在一行的开始处。也就是说,刚开始扫描时遇到的,或者说在刚扫描完一个换行字符后紧接着遇到的。

* ‘r$’ 匹配模式 r ,但是这个模式只在一行的尾部。也就是说,该模式就出现在换行之前。这个模式等价于 r/\n 。注意, flex 中的换行( newline )的概念,就是 C 编译器中所使用的 \n flex 也采用同样的符号和解释。在 DOS 系统中,可能必须由你自己滤除输入中的 \r ,或者明确地在模式中写成 r/\r\n 来代替 r$ 。(在 unix 系统中换行是用一个字节 \n 表示的,而 DOS/Windows 则采用两个字节 \r\n 来表示换行。)

7 )有启动条件( Start Condition )的模式匹配

* ‘<s>r’ 匹配模式 r ,但需要启动条件 s (后面后关于启动条件的讨论)。模式 <s1,s2,s3>r’ 是类似的,匹配模式 r ,只要有三个启动条件 s1 s2 s3 中的任一个即可。(启动条件简单来说,类似于 C 语言中的条件编译,满足了某个条件才启动这个模式参与匹配,否则不会启动该模式参与匹配。)

* ‘<*>r’ 匹配模式 r ,在任何启动条件下都参与匹配,即使是排斥性的条件。

[ 上述还需要从实践中体会其含义 ]

8 )文件尾匹配

* ‘<<EOF>>’ 匹配文件尾,即遇到了文件尾部。一般说来,都应该在模式中加入文件尾模式。这样可以有机会在文件扫描完成时增加一些额外的处理。

* ‘<s1,s2><<EOF>>’ 在有启动条件 s1 或者 s2 的情况下,匹配文件尾部。

一些常见规则的编写(待续)

1 )双引号字符串。

[\"]({SAFECHAR}|{RESTCHAR}|[_])*[\"]

这里需要注意的地方是中间的重复模式的写法: (r)* r 可以是一个组合模式。中间的两个名称 SAFECHAR RESTCHAR 是在定义段给出的两个字符类。

[ 此处应在实用中不断添加 ]

=========================================

创建一个简单的扫描器

下列例子来自于 Flex 的手册。并在 Windows+Cygwin+bison+flex+gcc 的环境下编译运行。

(1) 编辑 Flex 语法文件

/* name: example.flex */

int num_lines = 0, num_chars = 0;

%%

\n ++num_lines; ++num_chars;

. ++num_chars;

%%

int main()

{

yylex();

printf("# of lines = %d, # of chars = %d\n", num_lines, num_chars);

return 0;

}

(2) 生成扫描器的 C 文件

$ flex example.flex

The output is lex.yy.c

(3) 编译生成的 C 文件

编译时失败,出现了如下的问题:

# gcc -g -Wall -lfl -o scan lex.yy.c

lex.yy.c:959: warning: 'yyunput' defined but not used

/cygdrive/c/DOCUME~1/ADMINI~1.78B/LOCALS~1/Temp/ccHwCWNb.o: In function `main':

/cygdrive/c/home/sandbox/flex_exam_1/example.l:9: multiple definition of `_main'

/usr/lib/gcc/i686-pc-cygwin/3.4.4/../../../libfl.a(libmain.o):(.text+0x0): first defined here

/cygdrive/c/DOCUME~1/ADMINI~1.78B/LOCALS~1/Temp/ccHwCWNb.o: In function `yylex':

/cygdrive/c/home/sandbox/flex_exam_1/lex.yy.c:692: undefined reference to `_yywrap'

/cygdrive/c/DOCUME~1/ADMINI~1.78B/LOCALS~1/Temp/ccHwCWNb.o: In function `input':

/cygdrive/c/home/sandbox/flex_exam_1/lex.yy.c:1041: undefined reference to `_yywrap'

collect2: ld returned 1 exit status

上述消息指出两个问题:

1 )函数 yywrap 没有定义。

2 )自定义函数 main 与连接库 fl 中的定义冲突了。

第一个问题的解决办法是在第一段(定义段)中加上一个选项指令:

%option noyywrap

第二个问题的解决办法就是用 gcc 编译时不连接 fl 库,如下所示:

# flex example.flex

# ls

example.flex lex.yy.c

# gcc -g -Wall -o scan lex.yy.c

lex.yy.c:977: warning: 'yyunput' defined but not used

# ls

example.flex lex.yy.c scan.exe

# ./scan.exe

789

234

345# of lines = 2, # of chars = 11

修改过的代码如下:

%option noyywrap <==== 防止出现 yywrap 的问题

%{

int num_lines = 0, num_chars = 0;

%}

%%

\n ++num_lines; ++num_chars;

. ++num_chars;

%%

int main()

{

yylex();

printf("# of lines = %d, # of chars = %d\n",

num_lines, num_chars);

return 0;

}

更改扫描器 yylex() 的名字

我们还可以更改 Flex 自动生成的词法分析函数 yylex() 的名字、参数以及返回值,也就是说 yylex 这个名字仅仅是一个默认的名称,是可以改成其他名称的。方法很简单,只需要对宏 YY_DECL 做一个重定义即可:

#define YY_DECL float lexscan (float a, float b)

上述的宏定义就表明:当运行 Flex 生成 C 代码时,词法分析函数的名字叫做 lexscan (不再是 yylex 了),有两个浮点型参数 a b ,函数的返回值是浮点型。

如果与 Bison 联用的话,还是不要更改的好,因为 Bison 要求词法分析函数的名称是 yylex [ 应该也是可以改的,但其实际的方法还需在实践中得来。 ]

词法分析函数 yylex() 会使用全局变量 yyin 读取字符。

一些思考

1 )在 H248 协议的 BNF 文本中,需要分析很多的数字,有十六进制的,有十进制的,有长的数字也有短的数字。虽然在 H248 协议看来,各种不同的数字有着不同的意义,但是在 Flex 词法扫描器看来,它们有什么不同呢?特别是同样的一个 0xab 这样的只有两位数字的十六进制数,在 H248 协议和 BISON 看来,其有不同的含义和类型,但是在 Flex 看来却没有什么不同。假设 Bison 分别将其定义为 Token_A Token_B ,那么当 Flex 分析出这么一个单词时,返回给 Bison 的数字类型是 A 还是 B

2 )在 H248 协议中,有一种表达式是由多个参数组成的,其中每个参数至多出现一次,且参数间次序是任意的。此外其中有两个参数是必须的。这种情况下如何给出 Bison 文法规则定义呢?

文法分析概览

利用 BNF 写出的文法规则,可以用来对输入的文本进行文法分析。一条 BNF 文法规则,左边是一个非终结符( Symbol 或者 non-terminal ),右边则定义该非终结符是如何构成的,也称为产生式( Production ),产生式中可能包含非终结符,也可能包含终结符( terminal ),也可能二者都有。在所有文法规则中,必有一个开始的规则,该规则左边的部分叫做开始符号( start symbol )。一个规则的写法如下:

Symbol := Production

下面是一个 BNF 文法定义的例子。 FN fractional number 的意思, DL digit list 的意思, S start symbol

S := '-' FN | FN

FN := DL | DL '.' DL

DL := D | D DL

D := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

一个非终结符可能有多个产生式,相互间用竖线( | )隔开。

每一条 BNF 产生式,都有自己的启动集( start set )。启动集里的元素就是每个 Production 中的第一个部分,比如上例 S 规则的启动集就是 {'-'} 以及 {FN}

利用 BNF 文法来分析目标文本,其分析方法比较流行的有几种,下面作一概述 [Garshol 03]

LL(k) 分析

LL 分析又称为自顶向下的分析( top-down parsing ),也有叫递归下降分析( recursive-descent parsing )。也是最简单的一种分析方式。它工作的方式类似于找出一个产生式可以从哪一个终结符开始。

当分析时,从起始符号开始,比较输入中的第一个终结符和启动集,看哪一个产生式规则被使用了。当然,两个启动集之间不能拥有同一个终结符。如果有的话,就没有办法决定选择哪个产生式规则了。

Ll 文法通常用数字来分类,比如 LL(1) LL(0) 等。这个数字告诉你,在一个文法规则中的任何点可以允许一次察看的终结符的最大数量。 LL(0) 就不需要看任何终结符,分析器总是可以选择正确的产生式规则。它只适用于所有的非终结符都只有一个产生规则。只有一个产生规则意味着只有一个字符串。 [ 不用看当前的终结符是什么就可以决定是哪一个产生规则,说明这个规则是为一个固定的字符串所写的。 ] 这种文法是没有什么意义的。

最常见也是比较有用的事 LL(1) 文法。它只需要看一个终结符,然后就可以决定使用哪一个产生规则。而 LL(2) 则可以查看两个终结符,还有 LL(k) 文法等等。对于某个固定的 k 值,也存在着根本不是 LL(k) 的文法,而且还很普遍。

下面来分析一下本章开头给出的例子。首先看下面这条规则:

D := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

上述规则有十个产生式,每个产生式的启动集是一个数字终结符构成的集合 {'0'} {'1'} …… {'9'} 。这是一个很好的 LL(1) 文法,因为我们只要看一个终结符,就可以选择一个正确的产生式。例如,如果看到一个终结符,其内容是 3 ,那么就采用上面第四个产生式,即 D := '3'

接下来分析 DL 规则。

DL := D | D DL

上述规则有两个产生式,启动集是 {D} {D} 。很不幸,两个产生式的启动集相同。这就表示只看第一个输入中的第一个终结符不能选择正确的产生式。

然而可以通过欺骗来绕过这个问题:如果输入中第二个终结符不是一个数字,那么就选择第一个产生式,但如果两者都是数字就必须选择第二个产生式。换句话说,这意味着这是一条好的 LL(2) 文法规则。实际上这里有些东西被简化了。

再分析下 FN 规则吧。它的情况更糟糕。

FN := DL | DL '.' DL

它有两条产生式,而且启动集相同,均为 {DL} 。然而这次不像 DL 规则那么幸运了。咋一看,似乎通过 LL(2) 可以分辨应该使用哪一个产生式。但是很不幸,我们无法确定在读到终结符 ('.') 之前,需要读多少个数字才算是 DL 符号的最后一个数字。 [ 想想吧,分析器这么工作着:读入第一个终结符,一看是相同的 DL 符号,那么就读第二个终结符吧;读入第二个终结符,两者合起来一看,还是一样的 DL 符号;读入第三个终结符,前三个终结符合起来看,仍然是相同的 DL 符号。但是 DL 符号表指示数字表示没有长度限制的。 ] 没有任何一个给定的 k 值,这都不符合 LL(k) 文法,因为数字表总能突破这个 k 的长度。

最后看看启动符号规则。有点意外,它产生规则的选择很简单。

S := '-' FN | FN

它有两个产生规则,两者的启动集是 {'-'} {FN} 。因此,如果输入中第一个终结符是 '-' ,那么就选择第一个产生式,否则选择第二个产生式。所以这是一个 LL(1) 文法。

从上述的 LL 分析看,只有 FN DL 规则引起了问题。但是不必绝望。大部分的非 LL(k) 文法都可以容易地转换为 LL(1) 文法。下面以当前的这个例子来看看如何转换有问题的 FN DL

对于 FN 符号来说,它的两个产生式都开始于 DL ,但是第二个产生式其后续的是一个小数点终结符 ('.') ,以及另外一个数字表。那么这很容易解决:可以将 FN 改变为一个产生式,其以 DL 开始,后跟一个 FP fractional part )符号。而 FP 符号则定义成或者为空,或者为小数点后跟着一个数字表,如下所示:

FN := DL FP

FP := @ | '.' DL

上述 @ 符号表示为空。现在 FN 文法没有任何问题了,因为它现在只有一个产生式。而 FP 也不会有问题,因为它的两个产生式的启动集是不同的:前者是输入的尾端,后者是小数点终结符。

DL 符号就不是好啃的核桃了,原因在于其递归和至少需要一个 D 符号。解决方案就是,给 DL 一个产生式,由一个 D 后跟一个 DR digit rest )构成;而 DR 则有两个产生式,一个是 D DR (表示更多的数字),一个是 @ (表示没有更多的数字)。最后本章开头的例子被转换成下面的一个完全的 LL(1) 文法了:

S := '-' FN | FN

FN := DL FP

FP := @ | '.' DL

DL := D DR

DR := @ | D DR

D := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

LR 分析

Lr 分析也叫自底向上的分析( bottom-up parsing ),或者叫移进 - 归约分析( shift-reduce parsing ),相比 LL 分析难度更大些。它的基本原理是,首先收集输入,直到它发现可以据此利用一个符号对收集到的输入序列进行归约。可以与数学里面解方程式时的消元法进行类比。这听起来似乎很难。下面还是以一个例子来澄清。例子中将分析字符串 3.14 ,看看是怎样从文法产生出来的。

S := '-' FN | FN

FN := DL | DL '.' DL

DL := D | D DL

D := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

首先从输入中读入 3

3

然后看看是否可以将其归约为一个符号( Symbol ,即非终结符)。实际上可以归约,就是说用 D 符号的产生式可以得到当前读入的字符串(这也是成为产生式的原因)。

很快发现,从 DL 符号可以产生符号 D ,于是又可以归约成 DL 。(实际上还可以进一步地归约成 FN ,于是这里就产生了歧义,到底应该归约成哪一个呢?这表明这个文法定义是二义性的,我们在这里就忽略这个问题,直接选择 DL 作为归约结果吧。)接着从输入中读入一个小数点,并试图进行归约:

D ==> 规约到 DL

DL ==> 读入下一个字符

DL . ==> 规约到?

但是这次的归约尝试失败了,因为没有任何符号的定义可以产生这种形式的字符串。也就是说,这种形式不能规约到任何符号。

所以接着我们读入下一个字符 1 。这次可以将数字 1 归约到 D 符号。接着再读入一个字符 4 4 可以归约到 D ,继续归约到 DL 。这两次的读入和规约形成了 D Dl 这个序列,而这个序列可以归约到 DL

DL . ==> 读入下一个字符 1

DL . 1 ==> 1 归约到 D

DL . D ==> 读入下一个字符 4

DL . D 4 ==> 4 归约到 D

DL . D D ==> 4 继续归约到 DL

DL . D DL ==> D DL 归约到 DL

察看文法我们可以很快地注意到, FN 能产生 DL . Dl 这种形式的序列,所以可以做一个归约。然后注意到 FN 可以从 S 符号产生,所以可以归约到 S ,然后停止,整个分析结束。

DL . DL ==> 归约到 FN

FN ==> 规约到 S

S ==> 分析结束

可能你已经注意到,我们经常可以选择是否现在就做归约,还是等到读入更多的符号后再作不同的归约。移进 - 归约分析算法有很多不同的变种,按照复杂度和能力递增的顺序是: LR(0) SLR LALR LR(1) LR(1) 通常需要一个巨大的分析表,在实践上不具有实用性,因此 LALR 是最常使用的算法。 SLR LR(0) 对于大部分的程序语言来说还不够强大。

Bison 分析器的算法 1

Bison 适合上下文无关文法( Context-free grammar ),并采用 LALR(1) 算法 [Donnelly 06] 的文法。

bison 读入一个终结符( token ),它会将该终结符及其语意值一起压入堆栈。这个堆栈叫做分析器堆栈 parser stack )。把一个 token 压入堆栈通常叫做移进 shifting )。

例如,假设一个中缀计算器已经读入 '1 + 5 * ' ,下一个准备读入的是 '3' ,那么这个栈里就有四个元素,每个元素都是移进的一个终结符。

但堆栈并不是每读入一个终结符就分配一个栈元素给它。当已经移进的后 n 个终结符和组( groupings )与一个文法规则相匹配时,它们会被根据那个规则结合起来。这叫做归约( reduction )。栈中的那些终结符和组会被单个的组( grouping )替换。那个组的符号就是那个规则的结果。执行该规则的相应的动作( Action )也是归约处理的一部分,这个动作会计算这个组的语意值。

例如,如果中缀计算器的分析器堆栈包含: 1 + 5 * 3 ,并且下一个输入字符是换行符,那么上述后 3 个元素可以按照下面规则归约到 15

expr: expr '*' expr;

于是堆栈中就只包含下面三个元素了: 1 + 15 。此刻,另一个规约也可以执行,其结果是一个单值 16 。然后这个新行终结符就可以被移进了。

分析器通过移进和归约尝试着缩减整个输入到单个的组。这个组的符号就是文法中的起始符号( start-symbol )。

终结符预读

Bison 分析器并不总是在后 n 个终结符与组匹配某一规则时立即就进行归约。这种策略对于大部分语言来说并不合适。相反,当可以进行归约时,分析器有时会 预读 looks ahead )下一个终结符来决定做什么。

当一个终结符被读进来后,并不会立即移进堆栈,而是首先作为一个预读终结符 look-ahead token )。此后,分析器开始对栈上的终结符和组执行一个或多个归约,而预读终结符仍然放在一边。当没有归约可做时,这个预读终结符才会被移进堆栈。这并不表示所有可能的归约都已经做了,这要取决于预读终结符的类型,一些规则可能选择推迟它们的使用。 </

分享到:
评论

相关推荐

    Bison and Flex for windows source

    压缩包中的“Bison-Flex 笔记 - Bison-Flex - 自然之道.htm”和“Bison-Flex 笔记 - Bison-Flex - 自然之道.files”是学习资料,可能包含了一些实用教程和示例,对于初学者来说非常有价值。通过这些资料,用户可以...

    flex和bison官方手册

    Flex和Bison是两个在计算机科学领域,特别是编程语言解析器构建中广泛使用的工具。它们主要用于解析源代码,转换输入的语法结构,并生成相应的解析器。让我们深入了解一下这两个工具及其应用。 Flex,全称为“Fast ...

    gsoap网络资源汇总和相关连接

    #### 二、Bison-Flex笔记 **知识点:** 1. **Bison和Flex简介:** - Bison是一种通用的解析器生成器,用于构建语法解析器。 - Flex是一个灵活的词法分析器生成器,用于识别输入文本中的词法单元。 2. **使用Bison...

    petalinux创建笔记-长期维护版1

    【PetaLinux 创建笔记-长期维护版1】 PetaLinux 是一个专为Xilinx FPGA(现场可编程门阵列)系统芯片设计的嵌入式Linux系统开发工具包。该工具包提供了一整套流程,包括从项目初始化到构建完整的Linux系统镜像。...

    YACC(LEX)语法分析器学习笔记(Word格式)

    9. **Bison和Flex的高级特性**:除了基础用法,笔记可能还会深入到Bison的YYERROR和YYACCEPT等函数,Flex的缓冲区管理、多状态词法分析等高级主题。 10. **实践项目**:为了巩固理论知识,学习笔记可能会提供一个...

    New SNMP开发笔记

    ### 新SNMP开发笔记知识点详解 #### 一、配置Ubuntu的编译环境 在进行SNMP(简单网络管理协议)相关的开发工作之前,首先需要在Ubuntu操作系统上搭建一个适合的编译环境。这一过程主要涉及以下几个步骤: 1. **...

    modern_compiler_implemention:现代编译原理 - C 语言描述 学习笔记

    modern_compiler_implemention现代编译原理 - C 语言描述 笔记随书附带的代码:构建首先要安装 flex, bison, cmake 等工具sudo apt install -y flex bison

    ( WinXP Ubuntu10.10双系统下搭建开发环境笔记

    sudo apt-get install git-core gnupg flex bison gperf build-essential \ zip curl zlib1g-dev libc6-dev libncurses5-dev x11proto-core-dev \ libx11-dev libreadline6-dev libgl1-mesa-dev tofrodos python-...

    配置xen环境及hadoop集群环境的学习笔记

    ### 配置XEN环境及Hadoop集群环境学习笔记 #### XEN虚拟机的安装配置 **XEN** 是一种开源虚拟化技术,允许在一台物理机器上运行多个操作系统实例,这些实例通常被称为“域”(Domains)。XEN 的安装配置涉及到安装...

    IPC摄像头开发笔记.docx

    在安装gsoap之前,需要先安装一些编译工具,包括build-essential、libgtk2.0-dev、libglib2.0-dev、checkinstall、flex、bison、openssl和libssl-dev等。然后,下载gsoap源码,解压缩并编译安装。 4. 使用gsoap生成...

    android驱动开发笔记

    - **Ubuntu下的安装包**:为了搭建完整的Android开发环境,还需要安装一系列依赖包,如`flex`、`bison`、`gperf`等用于解析、语法分析等任务;`libesd0-dev`、`libwxgtk2.3-dev`等用于图形界面开发;`build-...

    lex与yacc学习笔记(词法分析,语法分析)(适合初学)

    ### lex与yacc学习笔记(词法分析,语法分析) #### 一、概述 本文旨在为初学者提供一套详尽的教程,通过介绍词法分析器lex和语法分析器yacc的基本概念及其应用实例,帮助读者更好地理解这两种工具在编译原理中的...

    山东大学软件学院编译原理学习笔记

    同时,笔记也会展开讨论编译器的构造工具,如Flex和Bison等,以及这些工具是如何辅助编译器开发的。 此外,实际的编译原理课程和学习笔记还将涵盖编译器的前端和后端设计、编译器的各个阶段如何协同工作,以及现代...

    boa移植笔记

    * **编译Boa**:执行`make`进行编译,若需词法和语法分析模块,可在Linux环境下使用bison和flex工具。 * **优化Boa**:使用`arm-none-linux-gnueabi-strip boa`命令对Boa进行链接器优化,进一步减小体积。 #### 三...

    irrd-legacy:Internet路由注册表守护程序

    IRRd取决于bison,flex和glib2开发文件。 要构建和安装发行版,请执行以下命令: git clone https://github.com/irrdnet/irrd.git cd irrd/src ./autogen.sh ./configure make make install Ubuntu 12笔记 sudo ...

    WinXP+VS2008+qgis2.6.0源码调试笔记(离线安装)

    目前已有通过QGIS源码编译安装Qgis的总结文档,本人在WinXP环境下,并且离线的状态下...文档中详细介绍了如何通过cmake生成build文件,如何为最新版的flex_bison定制vs2008生成规则,并详细介绍了整个编译和安装过程。

    multiparse:在同一个静态库中包含多个解析器

    Lex和Yacc传统上设计为输出独立程序,因此该项目使用Flex和Bison扩展。笔记要通过单词词典在morse.y中创建规则,请使用以下命令: awk ' { word=$1; gsub(/./, "& ", word); printf("%s { $$ = \"%s\"; } %%merge ...

    编译原理教程

    这些文档可能会用到Bison、Flex这样的工具,或者讲解自底向上、自顶向下的解析策略。 另外,压缩包里还有一个名为"程序员之家www.phome.asia.txt"的文本文件,它可能包含了一个网站的链接或介绍,这个网站可能提供...

    深入理解openwrt架构

    然后,需要安装必要的软件包,包括gcc、g++、binutils、patch、bzip2、flex、bison、make、autoconf、gettext、texinfo、unzip、sharutils、subversion、libncurses5-dev、ncurses-term、zlib1g-dev、git-core、gawk...

    编译原理 词法分析器.zip

    - **Flex和Bison**:开源工具,分别用于生成词法分析器和语法分析器,广泛用于C和C++项目中。 - **ANTLR**:一种强大的解析器生成器,支持多种语言,可以生成词法分析器、语法分析器和抽象语法树。 深入学习词法...

Global site tag (gtag.js) - Google Analytics