【编译原理实战】(一)用lex和yacc实现一个计算器

964 阅读3分钟

背景知识

编译原理研究的内容是:词法分析、语法分析、语义分析、中间代码生成等等。第一步,我们先尝试借助小工具完成这些步骤。(之后的文章中,如果有时间的话,再尝试不借助工具自己实现)

  • lex,一个词法分析工具(这个名称本身就是词法(Lexical)的前缀)。它可将.l文件编译生成对应的.c文件。
  • yacc(yet another compile compile),用于编译编译器的编译器,可完成语法分析、语义分析等工作。它可将.y文件编译生成对应的.c文件。
  • 使用c语言编译器(如gcc),将这些生成的.c文件进行编译,得到可执行文件。

工具配置

  • 在Unix、Linux系统中,已经默认安装了它们对应的flex和bison;

可使用flex --version bison --version验证。

  • windows下需要自行安装。

下载地址:winflexbison。 windows下载后可自行解压并配置环境变量: 将含有flex和bison可执行文件所在的目录路径添加到用户变量中的Path

依然可使用--version的版本进行测试是否配置成功。

下文都将以Linux系统为例进行演示。

使用规则

lex和yacc文件的格式是有点类似的:

%{
声明部分
%}
辅助定义部分
%%
规则部分
%%
用户函数部分

可以看到,用%%将各部分隔开,其中,若某一个部分为空,则对应的%%也可以省略。

实现整数加减乘除的计算器

代码

先来看.l文件的写法:

%{   /*%{%}包裹c代码,这部分代码会被原样输出*/
#include <stdio.h>
#include "y.tab.h"   /*.y文件经yacc编译后会生成这个头文件*/

int
yywrap(void)
{
    return 1;
}
%}
%%
"+"     return ADD;  /*定义加减乘除,回车返回执行结果的规则*/
"-"     return SUB;
"*"     return MUL;
"/"     return DIV;
"\n"    return CR;
([1-9][0-9]*)|0 {  /*正则表达式,表示接收非0开头的数字或者0*/
    int temp;
    sscanf(yytext,"%d",&temp);
    yylval.int_value=temp;
    return INT_LITERAL;  /*返回整数运算结果*/
}
[ \t] ;
. {
    fprintf(stderr,"lexical error.\n");
    exit(1);
}
%%

代码中添加了一些后来加上的注释。

再来看.y文件: 这一部分制定了更详细的语法规则,而且定义了函数主体部分。

    %{
    #include <stdio.h>
    #include <stdlib.h>
    #define YYDEBUG 1
    %}
    %union{
        int int_value;
    }
    %token <int_value> INT_LITERAL
    %token ADD SUB MUL DIV CR
    %type <int_value> expression term primary_expression
    %%
    line_list
        : line
        | line_list line
        ;
    line
        : expression CR
        {
            printf(">>%d\n",$1);
        }
    expression
        : term
        | expression ADD term
        {
            $$ = $1 + $3;
        }
        | expression SUB term
        {
            $$ = $1 - $3;
        }
        ;
    term
        : primary_expression 
        | term MUL primary_expression
        {
            $$ = $1 * $3;
        }
        | term DIV primary_expression
        {
            $$ = $1 / $3;
        }
        ;
    primary_expression
        : INT_LITERAL
        ;
    %%
    int
    yyerror(char const *str)
    {
        extern char *yytext;
        fprintf(stderr,"parser error near %s\n",yytext);
        return 0;
    }

    int main(void)
    {
        extern int yyparse(void);
        extern FILE *yyin;
        yyin=stdin;
        if(yyparse()){
            fprintf(stderr,"Error!\n");
            exit(1);
        }
    }

执行

yacc -dv mycalc.y  //d指生成头文件但不指定名称(使用默认名称),v指打印冗余日志信息

lex mycalc.l

cc -o mycalc y.tab.c lex.yy.c

注意:

  • 如果是使用bison命令且不指定--yacc参数,通过-d生成的.c文件不是y.tab开头的,因此c编译时得替换为相应名称,且mycalc.l中包含的头文件名称也要改掉,并且重新编译。
  • 针对头文件名不一致导致编译错误的问题, 依据bison帮助文档中的命令描述:

Output Files:

--defines[=FILE] also produce a header file

-d likewise but cannot specify FILE (for POSIX Yacc)

感觉可以试试用--define指定头文件名字。(我还没试)


如无意外就会看到生成了mycalc可执行文件。

输入./mycalc即可体验。

参考资料

  • 《自制编程语言》 [日] 前桥和弥著,刘卓等译
  • 编译原理课设指导书
  • bison,flex帮助文档