自己去创建一个编程语言吧(3)

上一节讲了flex,今天咱们讲bison

flex是分析词法的,例如你的关键字、操作符号等,bison是分析语法的,就是你定义的语法方式,也就是说,把你定义的关键字、符号这些东西按照一定的格式排列后,需要表达的意思

Bison和Flex在语法方面非常相似,一般把这种语法叫做BNF

在介绍Bison前,我们先简单说说BNF

1.什么是BNF文法
BNF是 Backus-Naur Form 的缩写,中文叫巴科斯范式,它是用来描述计算机语言语法的符号集

在BNF文法中

尖括号(<>)内包含的为必选项。
方括号([])内包含的为可选项。
大括号({})内包含的为可重复0至无数次的项。
竖线(|)表示在其左右两边任选一项。
(::=)的意思是被定义为。

比如,这个就是JAVA一部分的BFN描述

<switch statement> ::= switch ( <expression> ) <switch block>
<switch block> ::= { <switch block statement groups><switch labels> }
<switch block statement groups> ::= <switch block statement group> | <switch block statement groups> <switch block statement group>

2.Bison
Bison和FLEX一样,也是一个单独的文件,但通常和FLEX一起使用,通常用.y来作为文件的扩展名
为了编写一个语法分析器,需要一定的方法来描述语法分析器所使用的把一系列记号转换为语法分析数的规则

声明部分
%%
语法规则
%%
语言附加部分

‘%%’, ‘%{’ 和 ‘%}’ 是出现在每个Bison语法文件中的标点,用于分隔各个部分。

声明部分:可以使用C定义操作中使用的类型和变量。可以使用预处理器命令定义在那里使用的宏,并使用 #include它们包含执行任何这些操作的头文件。

Bison声明声明了终端符号和非终端符号的名称,还可以描述运算符的优先级以及各种符号的语义值的数据类型。

语法规则:定义了如何从其各个部分构造每个非终结符。

附加的C代码可以包含您要使用的任何C代码。通常,yylex这里有词法分析器的定义,以及语法规则中的动作所调用的子例程。在一个简单的程序中,程序的所有其余部分都可以放在这里。

<exp> ::= <factor>
    | <exp> + <factor>

<factor> ::= <number>
    | <factor> * <number>

每一行就是一条规则,用来说明如何组建语法树的分支。::=记作“变成“、|记作“或者“。规则带有递归性质,即这个树分支可以由相似的分支组成;规则还有依赖关系,从上面中可以看出是依赖,从而形成了优先级。

其实这样讲,即使把整个Bison全部说一次,也未见得都能明白,举个简单的例子看看

为了更加贴近编译语言所需要的代码,我写一个解析函数的例子,这个例子的功能,是从一个文件中a.txt里读取你定义好的一个名为print的函数,函数的参数可以是数字的集合,可以无限的多,然后计算它们的和并打印出来

下面代码无所谓能不能看懂,只要能正常运行就行

//a.txt
print(1,2,3,4,5,6,7,8)

我们要解析的就是以上内容,print里的数字可以无限多

然后在创建flex文件,在其中定义我们要使用的类型,记住,你使用的所有类型、字符、都需要在里面定义,否则在bison中不会被识别

//test-flex.l
%{
#include "header.h"
#include <io.h>

#include "test-bison.tab.h"
extern int yyerror(char*);
%}

%%

[0-9]+	     {yylval.vINTEGER =atoi(yytext);  return NUMBER; }
"print"		{return PRINT;}
"("    			{return '(';}
")"    			{return ')';}
","				{return(',');}
%%

int yywrap()
{
	return 1;
}

以上内容定义了一个词法解析的文件,[0-9]+表示输入的是数字,返回的标识符是NUMBER,这个可以随便起名,print返回PRINT,其他使用的符号原样返回

然后就是最主要的了,bison文件

 %{
#include "header.h"
extern int yylex();

#define YYDEBUG 1

int yyerror(char* str)
{
	printf(str);
	return(1);
}
void add(vector<int>* list)
{
	int ret=0;
	for (std::vector<int>::iterator it = list->begin(); it != list->end(); ++it) {
		ret+=(*it);
	}
	printf("%d\n",ret);
}

%}

%token <vINTEGER>NUMBER
%token PRINT
%type <vLIST> add_list
%start start
%%

start:print_definition	
	 
print_definition:PRINT '(' add_list ')'
				{
					add($3);
				} 			
			    ;


add_list:NUMBER{
					$$ = new vector<int>();
					$$->push_back($1);
				}| add_list ',' NUMBER
				{
					$1->push_back($3);
				}
				;
%%

为了让bison中的类型可用,还需要定义这些类型

#ifndef HEADER
#define HEADER
#include "stdafx.h"
#include <string>
#include <iostream>
#include<vector>
using namespace std;
typedef struct YYSTYPE
{
	int  vINTEGER;
	vector<int>* vLIST;
} YYSTYPE;

#endif

然后在再来个入口文件

#include "stdafx.h"
extern int yyparse();
extern FILE* yyin;
extern void yyrestart(FILE* F);
int main()
{
	FILE* file;
	file = fopen("a.txt", "r");
	yyin = file;
	yyrestart(yyin);
	yyparse();
}

ok,需要的文件都齐全了,然后执行

win_bison -d -v  test-bison.y
win_flex --nounistd test-Flex.l

如果什么都没有输出的话,就表示生成完成了

然后会发现,生成了lex.yy.c,test-bison.tab.c,test-bison.tab.h,这几个文件就是解析所用的文件
然后,编译运行就行了,正常的话,你会输出
在这里插入图片描述

你可能会发现,从表面上看,bison好像是个正则分析工具,事实上,它包含了编译原理中最核心的一部分,根本不是三言两语可以说明白的,但是,从解决问题的角度出发,如果你要使用它,也不见得要把它所有的语法和原理都搞明白,你只要记住几个常用的关键字、和bison的语法结构,就足够完成你的工作

bison具体结构解释

%token
token通常在写一个语言的时候,也就是把他作为定义关键子的方法,或者是一种数据的类型,例如我在上例中使用的

%token <vINTEGER>NUMBER

意思就是NUMBER这个标识,是vINTEGER类型,当然也可以不定义任何类型,单纯表示它是个token

%token PRINT

%type
它表示一个表达式所有返回的类型

%type <vLIST> add_list

%left
这个比较简单,我上面未提到,它表示你的操作数的结合性,下面代码意思是+,-操作是左结合

%left '-' '+'

%start
表示你要解析的第一个表达式是哪里开始,也就是bison的入口

%YYSTYPE
是个C结构,里面定义需要的数据类型

int yyerror(char str)*
编译错误的时候输出的内容

%{ %}中的内容,将会原模原样的复制到C文件中

首先

yacc开始解析文件后,首先会从start开始,start使用的表达式
为print_definition,所以定位至print_definition,print_definition的规则是以 PRINT 开头,后面为(add_list)的规则。

add_list 比较重要,可以实现print里面无限添加数字,就是因为add_list是一个递归,递归的方式形式如下。|表示或者的意思

XXX:aaa{
		
	}|XXX',' aaa
	{
		
	}

$x表示里面的第几个参数,例如例子中,$1就表示那个NUMBER

push_back($1);

$$表示这个表达式返回什么行了,下面这句就是表示add_list是返回vector*

$$ = new vector<int>();

add_list的类型说明在上面

%type <vLIST> add_list

add_list首先解析到()中的数据第一个,然后,表达式符合
add_list,就去解析add_list,文件中的(1,2,3,4,5,6,7,8)符合用逗号排列的方式,然后就递归操作,继续添加,直到全部完成为止

然后执行add($3);把刚才解析的vector*传入add,打印出结果

这就是BISON基本的执行方式和规则,如果你要深入了解,可以看下面
http://dinosaur.compilertools.net/这个讲的比较详细

ok,这就是解析一个程序文件所需要的基本条件了

如果你想要了看一个实例的话,可以看我在开发menthol时所编写的bison

下次就开始讲讲如何写真正的编程语言

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值