lex词法分析器

编译原理中,将源代码分解为若干Token的过程叫做词法分析。Lex(LEXical compiler)是一个词法分析工具,我们使用Lex,只需按照其规则像定义配置文件一样定义好.l(或.lex)文件然后执行Lex程序就能生成我们想要的词法分析器的C语言代码了。Lex原本由贝尔实验室开发,主要用于早期的Unix操作系统,Flex则是Lex的一个现代的替代品,它的基本用法和Lex是完全相同的,因此现在我们常说的Lex其实就是Flex程序(后文所说的Lex均指Flex程序),它最初由Vern Paxson于1987年用C语言写成。

本篇笔记我们主要学习Lex的基本用法,阅读本文需要正则表达式的前置知识。Lex一般结合Yacc使用,因此建议本文和Yacc章节一同观看。

安装Flex

一些Linux发行版会自带Flex程序,如果系统没有预装Flex,我们直接通过软件源等方式安装即可。Lex文件建议使用Vim作为编辑器,Vim对Lex语法的高亮支持良好。此外我们可以查看Flex工具的手册,里面介绍了相关的命令行选项。

man flex

Lex的使用

Lex使用起来不复杂,但是比较难用文字描述。这里我们直接从例子入手,看看Lex是如何使用的。

我们使用Lex编写一个最简单的例子,这个例子能够区分文本和整数型数字。我们的需求很简单,定义纯数字为INT类型,带有英文字符的为TEXT类型,空格和制表符忽略,例如:输入abc 123 ab1,对应TEXT INT TEXT

demo.l

%{
#include <stdio.h>
%}
%%
[0-9]+ {
    printf("INT:%s\n", yytext);
}
[a-zA-Z0-9]+ {
    printf("TEXT:%s\n", yytext);
}
[ \t]+ {}
%%
int main(void)
{
    yylex();
    return 0;
}
int yywrap(void)
{
    return 1;
}

执行以下命令,将Lex源代码文件编译为可执行文件。

lex demo.l && gcc lex.yy.c

首先我们要知道,执行Lex程序,会把demo.l编译成lex.yy.c,然后我们使用GCC编译这个C语言源代码文件即可。

第1行和第3行出现了%{%}这两个标记,这部分的C语言代码Lex会原样输出到lex.yy.c,这一部分可以写include语句以及定义一些函数、全局变量等操作。

18行定义了yywrap()函数,我们只要知道把它加上就好了,在这里不对这个函数做深入分析,在后文有一些说明。

4行和12行分别有一个%%,这个是Lex文件的区块分隔符。Lex文件分为三个区块:

  • 定义区块:可以使用%{%}放置C语言代码,为正则表达式命名等。
  • 规则区块:用正则表达式描述Token和对应操作。注意规则区块的写法要求:一个正则表达式后面跟随空格,然后接上C语言代码,多行C语言代码需要使用{}包含。
  • 用户代码区块:用户代码区块可以编写任意C代码,也会原样输出,与定义区块不同,用户代码区块无需使用%{%}

5-11行我们定义了若干正则表达式和对应的操作,这里只是使用prinf()进行输出。注意:

  1. yytext,这个就是当前Token的字符串。
  2. 观察5行和8行的正则表达式,我们可以发现,实际上[a-zA-Z0-9]+的结果是包含[0-9]+的,但是我们要记住,Lex会识别正则表达式的定义顺序,我们先定义的[0-9]+,那么遇到123这种字符串,就会优先匹配。
  3. 注意最后的[ \t]+,这个正则表达式实际上是用来匹配空格和制表符的,但是其后面的C语言代码是空的,这意味着我们的词法分析器会忽略空格和制表符。

13行在用户代码区块中,我们定义了一个main函数并调用yylex(),这个函数就代表整个Lex词法分析器。

我们尝试运行一下:

这确实是我们想要的结果。但是运行一下就会发现,我们并没有编写scanf啊,这个程序为什么会接收输入?

这是Lex的默认行为,从标准输入读取字符串,接着往后看就明白了。

Lex全局变量和函数

上面我们看到,实际上Lex工具将.l文件编译成了一个C语言源文件,我们可以打开这个文件观看它都定义了哪些内容。

如果我们想在我们的程序中使用Lex该怎么办呢?Lex在生成的.c文件中,定义了很多全局变量和函数。我们调用Lex或者向Lex传递数据,实际上就需要通过这些全局变量和函数来实现。

全局变量

  • FILE *yyinFILE *yyout:这两个变量是Lex的输入和输出流指针,如果用户未对其进行定义,默认指向stdinstdout
  • char *yytext:存放当前被识别的Token。
  • ECHO:Lex预定义的宏,将当前识别的Token输出到yyout,其实上面例子printf()就可以使用ECHO替代。

yylex

int yylex(void):词法分析程序,它自动移动yyinyyout。在定义匹配动作时,用户可用return语句结束yylex(),此处返回值必须是一个整数。

由于yylex()的运行环境都是以全局变量的方式保存,因此,在下一次调用yylex()时,它可从上次扫描的断点处继续扫描,在语法分析时我们可利用这一特性。

若用户未定义相应的return语句,则yylex()继续分析被扫描的文件直到碰到文件结束标志EOF。在读到EOF时,yylex()调用yywrap()函数(该函数用户必须提供),若该函数返回非0值,则yylex()返回0而结束。否则,yylex()继续对yyin指向的文件扫描。因此,yywrap()可以用来实现扫描多个文件。

yymore

yymore():将当前Token保留在yytext中,分析器下次扫描识别的Token将加追加在yytext中,下面是一个例子。

hello {printf(“%s!”,yytext);yymore();}
world {printf(“%s!”,yytext);}

当输入串为”helloworld”时,将输出hello!helloworld!

yyless

yyless(int n):回退当前识别的Token中n个字符到输入中,也就是说,下次扫描还会扫到刚刚回退的Token。

unput

unput(char c):回退字符c到输入,下次会扫描到字符c。

input

input():让分析器从输入缓冲区中读取当前字符,并将yyin指向下一字符。

yyterminate

yyterminate():中断对当前文件的分析,将yyin指向EOF。

yyrestart

yyrestart(FILE * file):重新设置分析器的扫描文件为file

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap