简单函数绘图语言解释器:词法分析器

伟大的极客总是听命于自己

:创造是极客唯一的属性

前言

这是笔者的编译原理大作业,从词法分析到语法分析写了两天,然而语义分析却差点要了我的老命,结果最后查出来是个小bug:一个标识符放在了错误的位置,然而这却几乎使我放弃了语义分析的工作。由此观之,写程序要有一个良好的心态。

下面给出解释器要求:
简单函数绘图语言共四种语句:坐标平移(ORIGIN)、比例设置(SCALE)、旋转变换(ROT)、循环绘图(FOR-DRAW);其中前三个都是坐标设置,最后一个语句才能绘制出图形。举例如下:
在这里插入图片描述
解释器能够对以上语句进行解释,并绘制出相应的图形。
本项目的开发平台为VS2017,采用C语言编写。

解释器的开发过程为:词法分析器—>语法分析器—>语义分析器兼主程序。
这一节笔者会给出词法分析器的代码实现。

一、基本思路

词法分析核心:打开源文件,过滤无用字符,然后进行记号的识别。
关于记号识别的具体细节:设立一个全局的缓冲区,每次识别新记号前对其刷新。每次从源文件读入一个字符,根据词法规则的DFA对其进行判断,或者得到正确的匹配、或者允许继续读取、或者识别出一个错误的单词。

二、代码实现

1.词法分析器头文件scanner.h

#pragma once
#ifndef SCANNER_H
#define SCANNER_H

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<ctype.h>
#include<stdarg.h>
#include<string.h>


//1.变量定义
extern FILE *P_File; //指向扫描文件的句柄
//*****************************************************************************************************************


/*2.结构定义
  Token_Type:枚举类型,记录所有记号种类
  Token:记号的结构性质,包括种类、原始字符串、属性值(限常量)、地址值(限函数)
  TokenTab:由Token组成的数组,主要记录了字符类的记号(这类记号比较多,包括保留字、参数、函数和常量名,使用这个数组进行匹配区分)
*/

enum Token_Type                           //记号种类
{
	ORIGIN, SCALE, ROT, IS, TO,               //保留字
	STEP, DRAW, FOR, FROM,                   //保留字
	T,                                    //参数
	SEMICO, L_BRACKET, R_BRACKET, COMMA,     //分隔符号
	PLUS, MINUS, MUL, DIV, POWER,             //运算符
	FUNC,                                 //函数
	CONST_ID,                             //常数
	NONTOKEN,                             //空记号
	ERRTOKEN                              //出错记号
};

extern struct Token                              //记号与符号表结构
{
	enum Token_Type type;                      //记号的类型
	char *lexeme;                         //构成记号的字符串,最大长度为100
	double value;                         //若为常数,则是常数的值
	double(*FuncPtr)(double);            //若为函数,则是函数的指针
};

static char LetterTab[][10] = {
	"PI","E","T","SIN","COS","TAN","LN","EXP","SQRT","ORIGIN","SCALE","ROT","IS","FOR","FROM","TO","STEP","DRAW"
};

static struct Token TokenTab[] =                  //符号表内容
{
	{ CONST_ID,LetterTab[0],3.1415926,NULL },
	{ CONST_ID,LetterTab[1],2.71828,NULL },
	{ T,LetterTab[2],0.0,NULL },
	{ FUNC,LetterTab[3],0.0,sin },
	{ FUNC,LetterTab[4],0.0,cos },
	{ FUNC,LetterTab[5],0.0,tan },
	{ FUNC,LetterTab[6],0.0,log },
	{ FUNC,LetterTab[7],0.0,exp },
	{ FUNC,LetterTab[8],0.0,sqrt },
	{ ORIGIN,LetterTab[9],0.0,NULL },
	{ SCALE,LetterTab[10],0.0,NULL },
	{ ROT,LetterTab[11],0.0,NULL },
	{ IS,LetterTab[12],0.0,NULL },
	{ FOR,LetterTab[13],0.0,NULL },
	{ FROM,LetterTab[14],0.0,NULL },
	{ TO,LetterTab[15],0.0,NULL },
	{ STEP,LetterTab[16],0.0,NULL },
	{ DRAW,LetterTab[17],0.0,NULL },
};
//*****************************************************************************************************************

//3.函数定义
extern int initScanner(char fileName[]);//初始词法分析器,即打开源文件
extern int closeScanner();//关闭词法分析器,即关闭源文件句柄
extern struct Token GetToken();//从源文件中获得一个记号(词语)
//接下来的是一些辅助函数
char GetChar();//从源文件中读一个字符
void AddInTokenBuffer(char ch);//如果读入的字符不是空字符,则可以加入字符缓冲区
int PutChar(char ch);//若前面记号缓冲区中已构成一个合法记号,而加入这个字符之后变得不合法了,那么这个字符不属于上一个记号,理应重新写回文件等待下一轮识别
//*****************************************************************************************************************

#endif

2.词法分析器实现文件scanner.cpp

#include"scanner.h"

static char TokenBuffer[100]; //记号缓冲区,用于存放待匹配的字符串,最大长度为100
FILE *P_File=NULL; //指向扫描文件的句柄

//主要函数1:初始词法分析器,即打开源文件
extern int initScanner(char fileName[])
{
	if ((P_File = fopen(fileName, "r")) == NULL)
		return 0;
	return 1;
}

//主要函数2:关闭词法分析器,即关闭源文件句柄
extern int closeScanner()
{
	fclose(P_File);
	return 1;
}

//主要函数3:从源文件中获得一个记号(词语),具体记号类型在Token_Type内有定义
extern struct Token GetToken()
{
	struct Token token = { ERRTOKEN,TokenBuffer,0.0,NULL };//默认记号为出错token
	char ch;//依次存放源文件中的一个字符,用于记号检测
	strcpy(TokenBuffer, "\0");//每次获取记号前将缓冲区清空
	//token.lexeme = TokenBuffer;//将待检测的记号原始字符串指向记号缓冲区

	do {//过滤源文件中的空白字符
		ch = GetChar();
	} while (ch == ' ' || ch == '\n' || ch == '\t');
	AddInTokenBuffer(ch);
	
	//记号识别中心逻辑
	if (ch >= 'A'&&ch <= 'Z') {//以字母开头,只能为保留字、参数、函数名、常量名,即TokenTab表中的记号
		while (((ch = GetChar()) >= 'A'&&ch <= 'Z')) //往后把属于字母的字符送入记号缓冲区,以进行letter识别
			AddInTokenBuffer(ch);

		PutChar(ch);//该字符不属于字母,应送回源文件
		for (int i = 0; i < 18; i++) //TokenTab表中共有十八项,逐一比对
			if (!strcmp(token.lexeme, TokenTab[i].lexeme)) {
				token.type = TokenTab[i].type;
				token.FuncPtr = TokenTab[i].FuncPtr;
				token.value = TokenTab[i].value;
				break;
			}
	}
		
	else if (ch == EOF) {//到文件末尾,返回NONTOKEN
		token.type = NONTOKEN;
	}

	else if (ch >= '0'&&ch <= '9') {//识别常数字面量
		while (((ch = GetChar()) >= '0'&&ch <= '9') || ch == '.') {//允许出现一个小数点
			AddInTokenBuffer(ch);
			if (ch == '.')
				break;
		}
		if (ch == '.')
			while ((ch = GetChar()) >= '0'&&ch <= '9')//小数点出现之后还可以跟数字
				AddInTokenBuffer(ch);

		PutChar(ch);//该字符不属于数字,应送回源文件

		//识别成功,填充记号结构
		token.type = CONST_ID;
		token.value = atof(token.lexeme);//转换为数值
	}

	else {//剩下的就只有分隔符和运算符了,或者出错
		switch (ch) {
		case ',':token.type = COMMA;  break;//逗号
		case ';':token.type = SEMICO;  break;//分号
		case '(':token.type = L_BRACKET;  break;//左括号
		case ')':token.type = R_BRACKET;  break;//右括号
		//以上是分隔符,以下是运算符
		case '+':token.type = PLUS;  break;//右括号
		//以下的运算符皆有其对应的两重可识别符,如减号‘-’的两重形式‘--’是注释
		case '-':
			if ((ch = GetChar()) == '-') {//注释,忽略这一行的剩余输入
				while (GetChar() != '\n');
				token = GetToken();
			}
			else {//减法运算
				PutChar(ch);
				token.type = MINUS;
			}
			break;

		case '*':
			if ((ch = GetChar()) == '*') {//乘方运算
				AddInTokenBuffer(ch);
				token.type = POWER;
			}
			else {//乘法运算
				PutChar(ch);
				token.type = MUL;
			}
			break;

		case '/':
			if ((ch = GetChar()) == '/') {//注释,忽略这一行的剩余输入
				while (GetChar() != '\n');
				token = GetToken();
			}
			else {//除法运算
				PutChar(ch);
				token.type = DIV;
			}
			break;

		default:;//默认是出错,什么也不做
		}
	}
	return token;
}
//**********************************************主要函数结束***********************************************************************************

//**********************************************辅助函数开始***********************************************************************************

//辅助函数1:从源文件中读一个字符并将其转换为大写(如果是字母的话)
char GetChar() {
	return toupper(getc(P_File));
}

//辅助函数2:把已读字符退回到源文件中
int PutChar(char ch) {
	return ungetc(ch,P_File);
}

//辅助函数3:把字符添加到TokenBuffer缓冲区内
void AddInTokenBuffer(char ch) {
	TokenBuffer[strlen(TokenBuffer)+1] = '\0';
	TokenBuffer[strlen(TokenBuffer)] = ch;
}

3.词法分析测试主程序

扫描二维码关注公众号,回复: 8727013 查看本文章
extern "C" {
#include"scanner.h"
}
void main()
{
	struct Token token;
	char sourceFile[100];
	printf("please input the source:");
	scanf("%s", sourceFile);
	if (!initScanner(sourceFile)) {//初始化词法分析器
		printf("can't open source File\n");
		return;
	}

	printf("Type\t\tLexeme\t\tValue\t\tFunc\n");
	printf("--------------------------------------------------------------------------\n");
	while (1) {
		token = GetToken();
		if (token.type != NONTOKEN)
			printf("%d\t\t%s\t\t%f\t\t%x\n", token.type, token.lexeme, token.value, token.FuncPtr);
		else
			break;
	}
	printf("--------------------------------------------------------------------------\n");
	closeScanner();
	system("pause");
}

三、运行效果

在这里插入图片描述

发布了24 篇原创文章 · 获赞 19 · 访问量 6894

猜你喜欢

转载自blog.csdn.net/tch3430493902/article/details/103224784