首先,我们展现出他的运行截图,作为项目的产出
分析:实现的原理
想要解决复杂的事情,就要先从简单的事情入手,因此,最好的办法,就是先分析基本的四则运算
- 只涉及两个数字
- 乘除的运算优先级强于加减
而在这个基础上,我们发现,复杂的运算,实际上就是对于他们的组合,比如
1 + 2 * 3 + 5
实际上就可以分解为如下的过程(模拟)
- 1 + 2 * 3 + 5
- 1 + 6 + 5
- 7 + 5
- 12
实现:以什么样的方式构建这棵树
这里,我们就需要去分析整个的过程。
而使用的工具,称之为 yacc & lex,他们是相互协作的软件。
(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] { ; }
. { ; }
%%
需要注意的事情在于
- Lex 程序分为三个部分,他们之间使用
%%符号作分隔 - %option 指明了一些我们想要开启的选项
- /* 注释 */
%{%}之间包含的C语言代码会被直接拷贝走- 每一个部分都是可以省略的(比如这里,我们第三部分就是省略的)
- yytext 包含着识别出的词法块的全部内容,而 yylval 储存着处理后的结果
(尤其注意,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);
}
需要注意的事情在于
-
Yacc 程序一样是三个部分,采用
%%分隔 -
第一部分是纯C代码,会被直接拷贝
-
%token 声明了会在 lex 中使用的词法块(我们前面提供的那些常量,就是在这里确定的)
(所以,不要自作主张的去在 lex 里面写 enum,这会引发严重的错误)
-
%union 可以帮助 yylval 匹配多个类型(联合体在这里发挥了重要的用途)
-
每条规则,注意,越靠前,优先级越好(自下而上)
-
yyparse 函数,就会帮助我们完成全部的输入 - 解析 - 输出工作
-
yyerror 函数可以输出错误的信息,其中 s 来自于 yacc ,代表错误的消息, 我们可以由此结合出一些自己的自定义提示(字符串的组合这些)
写在最后
感谢 PostgreSQL 社区的无私帮助,他们的回复帮了我很大的忙。
感谢《编译器构造:C语言描述》,他用C语言实现的微型语法解析器和词法解析器让我很好地理解了 yacc & lex 的原理,在实践意义上,他好于《编译原理》
感谢应急管理大学袁国铭老师的信任,所以我可以自然地开展我的研究。
最后,感谢阅读这篇文章的你,你们为我带来了巨大的动力!