编译原理实验一(C-语言词法分析器的编写C语言版本)
一、tiny词法分析程序源代码阅读笔记:
重要变量和函数:
①变量和函数:
A.要计算的唯一特性是词法或是被识别的记号的串值 |
变量t o k e n S t r i n g |
B.扫描程序使用3个全程变量 |
文件变量s o u r c e和l i s t i n g,整型变量l i n e n o |
C.存储当前行 |
char lineBuf[BUFLEN]; |
C.当前单词类型 |
c u r r e n t T o k e n |
D.所要计算的唯一特性——词法或是被识别的记号的串值。 |
t o k e n S t r i n g |
E.标志变量被用作指示是否将一个字符增加到t o k e n S t r i n g之上 |
s a v e |
F.完成位于由g e t T o k e n的主要循环识别的标识符之后的保留字的查找 |
TokenType reservedLookup (char* s) |
G.进行词法分析,返回下一个合法单词的类型 |
TokenType getToken(void) |
D.按格式打印一个单词 |
void printToken( TokenType token, const char* tokenString ) |
E.从行缓冲中输入下一个非空格字符,当行缓冲为空则输入下一行字符串。 |
static int getNextChar(void) |
F.不读取下一个字符,在行缓冲中回退一个字符。 |
static void ungetNextChar(void) |
②数据结构
A.状态类型(枚举) |
typedef enum { START,INASSIGN,INCOMMENT,INNUM,INID,DONE } StateType; |
B.单词类型(枚举) |
typedef enum{ ENDFILE,ERROR, IF,THEN,ELSE,END,REPEAT,UNTIL,READ,WRITE, ID,NUM, ASSIGN,EQ,LT,PLUS,MINUS,TIMES,OVER,LPAREN,RPAREN,SEMI } TokenType; |
C.保留字的查找表(结构体类型) |
static struct { char* str; TokenType tok; } reservedWords[MAXRESERVED] = {{"if",IF},{"then",THEN},{"else",ELSE},{"end",END}, {"repeat",REPEAT},{"until",UNTIL},{"read",READ}, {"write",WRITE}}; |
有START,INASSIGN,INCOMMENT,INNUM,INID,DONE六个状态
其状态转换图:
![](https://img-blog.csdnimg.cn/20201107093402361.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk2Njc1,size_16,color_FFFFFF,t_70)
总结:
cifa词法分析程序的主要思路是:
①从主函数环境变量中获取输入文件的名称,并判断其是否存在
②循环调用getToken函数进行词法分析,直到函数返回值位ENDFILE即到达文件末尾
③getToken函数中的分析思路:
- 初始状态位START, tokenStringIndex 为0表示从输入字符串的第一个字符开始分析
- 调用getNextChar()来获取下一个位置的字符
- 在对应的状态下,根据字符的类型和DFA中的转换关系,得到当前词法单元的类型,并更新状态。
- 在分析过程中将每个字符保存到tokenString中。
- 直到状态STATE为DONE则该词法单元分析结束。tokenString中加入一个空字符表示词法单元的尾部。并且如果该词法单元为标识符则查找相应的保留字。
二、C-词法分析器实验报告:
1.C-词法规则
①语言的关键字:
else if int return void while
所有的关键字都是保留字,并且必须是小写。
②专用符号:
+ - * / < <= > >= == != = ; , ( ) [ ] { } /* */
③其他标记是I D和N U M,通过下列正则表达式定义:
ID = letter letter*
NUM = digit digit*
letter = a|..|z|A|..|Z
digit = 0|..|9
小写和大写字母是有区别的。
④空格由空白、换行符和制表符组成。空格通常被忽略,除了它必须分开 I D、N U M关键字。
⑤注释用通常的C语言符号/ * . . . * /围起来。注释可以放在任何空白出现的位置 (即注释
不能放在标记内)上,且可以超过一行。注释不能嵌套。
整理:
保留字 |
特殊符号 |
其他 |
else |
+ |
ID(letter letter* ) letter = a|..|z|A|..|Z |
if |
- |
int |
* |
return |
/ |
void |
< |
while |
<= |
> |
>= |
== |
!= |
= |
; |
NUM(digit digit*) digit = 0|..|9 |
, |
( |
) |
[ |
] |
{ |
} |
/* |
/* |
- DFA:
①注释状态转换图:
![](https://img-blog.csdnimg.cn/20201107093535113.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk2Njc1,size_16,color_FFFFFF,t_70)
②专用符号、ID、NUM状态转换图:
![](https://img-blog.csdnimg.cn/20201107093554204.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk2Njc1,size_16,color_FFFFFF,t_70)
注意:
①关键字:
除了数量减少以及关键词内容有所改变以外,关键字都是由字母小写构成,其中不添加其他符号,所以我们在关键字处理方式可以使用Tiny编译器的处理方式。
②专用符号:
-
在tiny编译器的基础上,增添了有两个符号构成的符号,例如<=、>=、==、!=等,这需要在构建C-词法分析状态转换图时需要注意;
-
<=是被当做小于等于而不是小于和等于两个词素的原因是我们默认遵循最长子串原则,其中具体构建方法见下一步。
③其他标记ID/NUM:
NUM只包括数字、ID只包括大小写字母,且字母区分大小写。
④注释:
C-语言的注释前置符号和后置符号都又两个符号构成 ’/’ ’*’ ;处理该符号时需要注意。
- 代码实现:
工程结构:
main.cpp C-编译器的的主程序
scan.c 实现C-编译器的扫描功能
scan.h C-编译器的scan接口
①scan.h
定义了单词类型、状态转换图中的状态名称、输入文件路径、打印分析过程的标记,
声明了分析方法。
#ifndef SCAN_H_INCLUDED
#define SCAN_H_INCLUDED
#define MAXRESERVED 6
#define BUFLEN 256
#define MAXTOKENLEN 40
/**单词类型 */
typedef enum
{ENDFILE, ERROR,
/** 保留字 */
IF,ELSE,INT,RETURN,VOID,WHILE,
/** 多字符单词的Token */
ID,NUM,
/** 特殊标记符 + - * / < > <= >= = , ; ( ) [ ] { } 注释符号左右 */
ASSIGN,PLUS, MINUS, MULT, DIVI, LT, GT, LTE, GTE, DH, FH, EQ, NEQ, LSP, RSP, LMP, RMP, LLP, RLP, LCOM, RCOM
} TokenType;
/** 状态名称*/
typedef enum { START, INNUM, INID, INGTE, INLTE , INEQ, INNEQ, NILCOM, INCOMMENT, INLCOM, INRCOM, DONE } StateType;
FILE* source; /** 源代码文件 */
FILE* listing; /** 输出文件 */
FILE* code; /* code text file for TM simulator */
/**************************************************/
/*********** Flags for tracing ************/
/**************************************************/
/* EchoSource = TRUE causes the source program to
* be echoed to the listing file with line numbers
* during parsing
*/
int EchoSource;
/* TraceScan = TRUE causes token information to be
* printed to the listing file as each token is
* recognized by the scanner
*/
int TraceScan;
/*****************词法分析函数*****************************/
TokenType getToken(void);
#endif // SCAN_H_INCLUDED
②scan.c
包含数据结构:
static struct { char* str; TokenType tok; } reservedWords[MAXRESERVED] |
用一个结构体类型来描述保留字,str代表保留字的值,tok代表保留字类型。 |
包含方法:
static TokenType reservedLookup (char* s) |
查找某标识符是否为保留字,若为保留字则返回保留字类型 |
char isAlpha(char c) |
判断c是否为字符 |
char isDigit(char c) |
判断c是否为数字 |
static int getNextChar(void) |
从输入缓冲中得到下一个字符 |
static void ungetNextChar(void) |
在输入缓冲中回退一个字符 |
void printToken( TokenType token, const char* tokenString ) |
打印分析后的单词 |
TokenType getToken(void) |
分析输入缓冲中的源代码,返回分析后的单词类型 |
getToken的代码:
/****************************************/
/** 扫描器的主要方法 */
/****************************************/
/** 返回缓冲行中下一要分析的字符
*/
TokenType getToken(void)
{
/* index for storing into tokenString */
int tokenStringIndex = 0;
/* holds current token to be returned */
TokenType currentToken;
/* current state - always begins at START */
StateType state = START;
/* flag to indicate save to tokenString */
int save;
while (state != DONE){
int c = getNextChar();
save = 1;
switch (state)
{ case START:
if (isDigit(c))
state = INNUM;
else if (isAlpha(c)){
state = INID;
}
else if (c == '>')
state = INGTE;
else if (c == '<')
state = INLTE;
else if (c == '=')
state = INEQ;
else if (c == '!')
state = INNEQ;
else if ((c == ' ') || (c == '\t') || (c == '\n') || (c == 13) )
save = 0;
else if (c == '/'){
save = 0;
state = INLCOM;
}
else
{ state = DONE;
switch (c){
case EOF:
save = 0;
currentToken = ENDFILE;
break;
case '+':
currentToken = PLUS;
break;
case '-':
currentToken = MINUS;
break;
case '*':
currentToken = MULT;
break;
case ',':
currentToken = DH;
break;
case ';':
currentToken = FH;
break;
case '(':
currentToken = LSP;
break;
case ')':
currentToken = RSP;
break;
case '[':
currentToken = LMP;
break;
case ']':
currentToken = RMP;
break;
case '{':
currentToken = LLP;
break;
case '}':
currentToken = RLP;
break;
default:
currentToken = ERROR;
Error = 1;
break;
}
}
break;
case INLCOM://! '/'
if (c =='*'){//! "/*"组合出现,表示注释,此时字符串不保存
state = INCOMMENT;
save = 0;
}
else{
state = DONE;
ungetNextChar();//!如果没有配对成功,表示为DIVI除法运算符号,并返回上一个符号
currentToken = DIVI;
}
break;
case INCOMMENT://!注释
if (c == '*')
state = INRCOM;
break;
case INRCOM://!可能为右注释
if (c == '/'){//!注释结束
state = DONE;
}
else//!否则返回注释状态
state = INCOMMENT;
break;
case INID:
if (!isAlpha(c))
{
ungetNextChar();
save = 0;//!最后一个字符不重复保存
state = DONE;
currentToken = ID;
}
break;
case INNUM:
if (!isDigit(c))
{
ungetNextChar();
save = 0;//!最后一个字符不重复保存
state = DONE;
currentToken = NUM;
}
break;
case INGTE:
if (c == '='){// >=
state = DONE;
currentToken = GTE;
}
else{
state = DONE;
ungetNextChar();//!如果没有配对成功,则是>
currentToken = GT;
}
break;
case INLTE:
if (c == '='){// <=
state = DONE;
currentToken = LTE;
}
else{
state = DONE;
ungetNextChar();//!如果没有配对成功,表示为DIVI除法,并返回上一个符号
currentToken = LT;
}
break;
case INEQ:
if (c == '='){// ==
state = DONE;
currentToken = EQ;
}
else{
state = DONE;
ungetNextChar();//!如果没有配对成功则为=
currentToken = ASSIGN;
}
break;
case INNEQ:
if (c == '='){//! !=
state = DONE;
currentToken = NEQ;
}
else{//!不合法
fprintf(listing,"Scanner Bug: state= %d\n",state);
state = DONE;
save = 0;
ungetNextChar();//!如果没有配对成功,则单个字符!不合法ERROR
currentToken = ERROR;
Error = 1;
}
break;
case DONE:break;
default: /* should never happen */
fprintf(listing,"Scanner Bug: state= %d\n",state);
state = DONE;
currentToken = ERROR;
Error = 1;
break;
}
if ((save) && (tokenStringIndex <= MAXTOKENLEN))
tokenString[tokenStringIndex++] = (char) c;
if (state == DONE){
tokenString[tokenStringIndex] = '\0';
if (currentToken == ID)//! 如果是标识符则判断是否为保留字(查表)
currentToken = reservedLookup(tokenString);
}
}
// if (TraceScan) {
fprintf(listing,"\t%d: ",lineno);
printToken(currentToken,tokenString);
// }
return currentToken;
} /* end getToken */
③main.c主函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "scan.h"
int main(int argc, char * argv[] )
{
char pgm[120]; /* source code file name */
if (argc != 2)
{ fprintf(stderr,"usage: %s <filename>\n",argv[0]);
return 0;
}
strcpy(pgm,argv[1]) ;
source = fopen(pgm,"r");
if (source==NULL){
fprintf(stderr,"File %s not found\n",pgm);
return 0;
}
listing = stdout; /**输出结果到屏幕 */
fprintf(listing,"\nC- COMPILATION: %s\n",pgm);
while (getToken()!=ENDFILE);
return 0;
}
- 测试
①正例:
![](https://img-blog.csdnimg.cn/20201107094004837.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk2Njc1,size_16,color_FFFFFF,t_70)
![](https://img-blog.csdnimg.cn/20201107094012290.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk2Njc1,size_16,color_FFFFFF,t_70)
![](https://img-blog.csdnimg.cn/20201107094024370.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk2Njc1,size_16,color_FFFFFF,t_70)
![](https://img-blog.csdnimg.cn/20201107094037334.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk2Njc1,size_16,color_FFFFFF,t_70)
②反例(其中!和float是没有定义的专用符号或关键字)
![](https://img-blog.csdnimg.cn/20201107094053913.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk2Njc1,size_16,color_FFFFFF,t_70)
![](https://img-blog.csdnimg.cn/20201107094101636.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk2Njc1,size_16,color_FFFFFF,t_70)
1.总结:
实现词法分析设计的主要工作:
(1)从源程序文件中读入字符。
(2)统计行数和列数用于错误单词的定位。
(3)删除空格类字符,包括回车、制表符空格。
(4)按拼写单词,并用(内码,属性)二元式表示。(属性值——token 的机内表示)
(5)如果发现错误则报告出错
2.心得
(1)本次实验最主要的是通过对源代码进行分析得到一个个的单词,通过编程实现后对词法分析这一过程更加的熟悉了。
(2)实验过程中第一个问题是源代码的输入。解决方式:利用文本文件”txt”以字符串的形式存储代码,利用fgets等c语言函数进行代码的逐行读取,并分析。
(3)实验过程分析出“注释”后,打印时打印出“Unknown token”和toekn值,后来改成了打印“Unknown token”和toeknString,也就是其值。
(4)实验中回车(13)一开始没有处理,导致识别出ERROR,但是也打印不出来,这里花费的时间比较多,后来把回车和空格,换行符,制表符在一起进行判断,并将save置为0,不保存该字符。
(5)输入源代码样式有待进一步测试。