用 yacc 与 lex 制作一个四则运算器

381 阅读3分钟

首先,我们展现出他的运行截图,作为项目的产出

图片.png

分析:实现的原理

想要解决复杂的事情,就要先从简单的事情入手,因此,最好的办法,就是先分析基本的四则运算

  1. 只涉及两个数字
  2. 乘除的运算优先级强于加减

而在这个基础上,我们发现,复杂的运算,实际上就是对于他们的组合,比如

1 + 2 * 3 + 5

实际上就可以分解为如下的过程(模拟)

  1. 1 + 2 * 3 + 5
  2. 1 + 6 + 5
  3. 7 + 5
  4. 12

图片.png

实现:以什么样的方式构建这棵树

这里,我们就需要去分析整个的过程。

图片.png

而使用的工具,称之为 yacc & lex,他们是相互协作的软件。

图片.png (Tom St. John 《LEX & YACC Tutorial》)

而 Lex,会根据正则表达式去分解输入的文本块为一个个词法块。

在这里,我们给出我们的实现代码(1.3.l 是我们的 lex 规则文件的名称)

/*
        1.3.l
            Calc Program
        Jing Zhang
*/
%option noyywrap
%option noinput

%{
#include <stdlib.h>
#include "y.tab.h"
%}

%%

"+" { return ADD; }
"-" { return SUB; }
"*" { return MUL; }
"/" { return DIV; }
"|" { return ABS; }
-[0-9]+.[0-9]+ { yylval.number = atof (yytext); return NUMBER;}
[0-9]+.[0-9]+ { yylval.number = atof (yytext); return NUMBER;}
-[0-9]+ { yylval.number = atof (yytext); return NUMBER;}
[0-9]+ { yylval.number = atof (yytext); return NUMBER;}
\n { return NEWLINE; }
[ \t] { ; }
. { ; }
%%

需要注意的事情在于

  1. Lex 程序分为三个部分,他们之间使用 %% 符号作分隔
  2. %option 指明了一些我们想要开启的选项
  3. /* 注释 */
  4. %{ %} 之间包含的C语言代码会被直接拷贝走
  5. 每一个部分都是可以省略的(比如这里,我们第三部分就是省略的)
  6. yytext 包含着识别出的词法块的全部内容,而 yylval 储存着处理后的结果

图片.png (尤其注意,yylval 的类型,需要我们和 yacc 协商后才可以决定(不协商,尽管默认是 int,但是我的个人人实践经历告诉我,这往往会带来一个错误),%union 就是作这个用的)

(可以注意到词法块返回了一些常量,他们在 yacc 中确定,我们通过引入 yacc 生成的 y.tab.h 文件,实现了对接)

(每个人的情况不一样,就像教科书生成的那个 .tab.h 名字就完全和我这里的对不上号,所以,一定要做好随机应变的准备)

而 Yacc,则会根据我们设计的规则,自动去组合排列词法块。

/*
      1-3.y
*/

%{
#include <stdio.h>
int yylex();
int yyerror(char *s);

%}
%token NUMBER
%token ADD SUB MUL DIV ABS
%token NEWLINE
%union 
{
	double number;
}

%%

calclist: /* Empty rule */
| calclist expression NEWLINE { printf (" = %lf\n", $2.number); }
;

expression: factor { $$.number = $1.number; }
| expression ADD factor { $$.number = $1.number + $3.number; }
| expression SUB factor { $$.number = $1.number - $3.number; }
;

factor: term { $$.number = $1.number; }
| factor MUL term { $$.number = $1.number * $3.number; }
| factor DIV term { $$.number = $1.number / $3.number; }
;

term: NUMBER  { $$.number = $1.number;}
| ABS NUMBER ABS { $$.number = $2.number > 0 ? $2.number : - $2.number; }

%%

int main(int argc, char *argv[])
{
	yyparse();
}
int yyerror(char *s)
{
	puts(s);
}

需要注意的事情在于

  1. Yacc 程序一样是三个部分,采用 %% 分隔

  2. 第一部分是纯C代码,会被直接拷贝

  3. %token 声明了会在 lex 中使用的词法块(我们前面提供的那些常量,就是在这里确定的)

    (所以,不要自作主张的去在 lex 里面写 enum,这会引发严重的错误)

  4. %union 可以帮助 yylval 匹配多个类型(联合体在这里发挥了重要的用途)

  5. 每条规则,注意,越靠前,优先级越好(自下而上)

  6. yyparse 函数,就会帮助我们完成全部的输入 - 解析 - 输出工作

  7. yyerror 函数可以输出错误的信息,其中 s 来自于 yacc ,代表错误的消息, 我们可以由此结合出一些自己的自定义提示(字符串的组合这些)

写在最后

感谢 PostgreSQL 社区的无私帮助,他们的回复帮了我很大的忙。

图片.png

感谢《编译器构造:C语言描述》,他用C语言实现的微型语法解析器和词法解析器让我很好地理解了 yacc & lex 的原理,在实践意义上,他好于《编译原理》

图片.png

感谢应急管理大学袁国铭老师的信任,所以我可以自然地开展我的研究。

最后,感谢阅读这篇文章的你,你们为我带来了巨大的动力!