编译原理_词法分析设计原理与实现技术

完整代码已经上传到资源库

实验内容

以下为正则文法所描述的C语言子集单词符号的示例,请补充单词符号:++,
, >>, <<, +=, -=, *=, /=,
&(逻辑与),||(逻辑或),!(逻辑非)等等,给出补充后描述C语言子集单词符号的正则文法,设计并实现其词法分析程序。

以下文法为左线性正则文法:

<标识符>→字母︱ <标识符>字母︱ <标识符>d

<无符号整数>→数字︱ <无符号整数>数字

<单字符分界符>→+ ︱- ︱* ︱; ︱, ︱( ︱) ︱{ ︱}

<双字符分界符>→<大于>=︱<小于>=︱<小于>>︱<感叹号>=︱<等于>=︱<斜竖>*

<小于>→<

<等于>→=

<大于>→>

<斜竖> →/

<感叹号>→!

该语言的保留字 :void、int、float、double、if、else、for、do、while
等等(也可补充)。

设计说明

  • 可将该语言设计成大小写不敏感,也可设计成大小写敏感,用户定义的标识符最长不超过32个字符。

  • 字母为a-z A-Z,数字为0-9。

  • 可以对上述文法进行扩充和改造。

  • “/*…*/“和”//”(一行内)为程序的注释部分。

设计要求

  • 给出各单词符号的类别编码。

  • 词法分析程序应能发现输入串中的错误。

  • 词法分析作为单独一遍编写,词法分析结果为二元式序列组成的中间文件。

  • 设计至少4个测试用例(尽可能完备),并给出测试结果。

任务分析

要设计一个词法分析器,首先要画出它的状态转换图,所以我们需要先分析其正则文法。基于正则文法,定义词法规则,将输入分为不同的单词符号类别。这包括标识符、无符号整数、单字符分界符、双字符分界符、保留字等,然后
为每个单词符号类别分配一个唯一的类别编码,以便在词法分析中使用。

具体到算法设计,首先读取文件到内存,逐个字符分析,若是空白符则跳过,为字母时将连续的字母使用超前搜索组合成为变量或关键字;若是数字,则要判断是否为浮点数,即使用超前搜索的时候,判断扫描到的字符是否为小数点;若是分隔符或者操作符,利用if语句判断并输出,若是其他字符,输出为未定义的字符。

实验过程

定义词法规则

为了保证词法的完整性,我设计了60种单词符号以及对应的助记符。我的词法分类表如下表
单词符号类别 助记符 单词符号类别 助记符


TK_UNDEF 0 KW_VOID 30
KW_AUTO 1 KW_VOLATILE 31
KW_BREAK 2 KW_WHILE 32
KW_CASE 3 KW_INCLUDE 33
KW_CHAR 4 KW_BOOL 34
KW_CONST 5 KW_TRUE 35
KW_CONTINUE 6 KW_DEFINE 36
KW_DEFAULT 7 TK_PLUS 37
KW_DO 8 TK_MINUS 38
KW_DOUBLE 9 TK_STAR 39
KW_ELSE 10 TK_DIVIDE 40
KW_ENUM 11 TK_ASSIGN 41
KW_EXTERN 12 TK_EQ 42
KW_FLOAT 13 TK_LT 43
KW_FOR 14 TK_LEQ 44
KW_GOTO 15 TK_GT 45
KW_IF 16 TK_GEQ 46
KW_INT 17 TK_OPENPA 47
KW_LONG 18 TK_CLOSEPA 48
KW_REGISTER 19 TK_OPENBR 49
KW_RETURN 20 TK_CLOSEBR 50
KW_SHORT 21 TK_BEGIN 51
KW_SIGNED 22 TK_END 52
KW_SIZEOF 23 TK_COMMA 53
KW_STATIC 24 TK_SEMICOLON 54
KW_STRUCT 25 TK_INT 55
KW_SWITCH 26 TK_DOUBLE 56
KW_TYPEDEF 27 TK_POUND 57
KW_UNION 28 TK_SLASH 58
KW_UNSIGNED 29 TK_COLON 59

关键代码解析

token部分

首先是对单词符号的定义与助计符的对应关系,为了使得代码更容易理解和维护,我舍弃了#define的定义方式,选择枚举类型来处理其映射关系,枚举类型是一种用户自定义的数据类型,用于表示一组命名常量,也称为枚举成员。每个枚举成员都有一个与之相关联的整数值,通常从0开始递增。

一些变量的定义

在进行操作之前,我定义了并初始化了一些用于记录不同类型词法单元的计数器和存储容器。

  • ‘ch’ 用于暂存当前处理的字符。

  • ‘buffer’ 用于暂存正在构建的标识符或常量字符串。

  • ‘b’ 用于临时存储字符,通常用于构建数字常量。

  • ‘logical_op’、‘single_op’、‘numer’ 和 ‘other’
    是字符数组,包含了逻辑运算符、单字符分界符、数字字符、以及其他特殊字符的定义。

  • ‘mark’ 数组用于标记字符是否已经处理过,防止重复处理

  • 计数器 ‘kc’ 、‘ic’、‘lc’、‘mc’、‘nc’ 和 ‘oc’
    分别用于记录关键字、标识符、逻辑运算符、单字符分界符、数字值和其他符号的数量。

  • 各个 ‘vector’
    类型的容器(‘k’、‘id’、‘lo’、‘ma’、‘nu’、‘ot’)用于存储不同类型的词法单元,如关键字、标识符、逻辑运算符等。

检查单词是否是关键字

我们定义并维护一个二维字符数组
keywords,其中包含36个关键字。代码使用一个循环遍历 keywords
数组,逐一比较其中的关键字与输入的 buffer
字符串。如果找到匹配的关键字,它将设置 flag
为1,表示找到了关键字,然后跳出循环。最后,它返回
i+1,即匹配到的关键字在数组中的位置加1,作为关键字的类别编码。

主要代码如下:

int isKeyword(char buffer[]) {
    char keywords[36][10] = {
        "auto", "break", "case", "char", "const", "continue", "default",
        "do", "double", "else", "enum", "extern", "float", "for", "goto",
        "if", "int", "long", "register", "return", "short", "signed",
        "sizeof", "static", "struct", "switch", "typedef", "union",
        "unsigned", "void", "volatile", "while","include" ,"bool","true","define",
        
    };
    int i=0, flag = 0;

    for (i = 0; i < 36; ++i) {
        if (strcmp(keywords[i], buffer) == 0) {
            flag = 1;
            break;
        }
    }

    return i+1; //每个keyward对应的类别编码 
    }
}

检查单词是否是数字

分析输入字符 ch
是否为数字或浮点数,并将其识别为标识符(整数或浮点数常量),然后输出词法分析结果。它通过逐个检查字符
ch 是否为数字(‘0’ 到 ‘9’)或特定字符(‘.’、’ ‘、’
n’、‘;’)来识别常量。如果 ch 是数字字符,它将其添加到临时字符串数组 b
中。如果 ch 是空格、换行或分号,并且 b 不为空,则将 b
转换为一个浮点数或整数,添加到向量 nu
中,并输出对应的词法分析结果。这段代码主要用于从输入中提取数字常量,并输出它们的类别编码(55
表示整数常量)和值。

主要代码如下:

if (ch == '0' || ch == '1' || ch == '2' || ch == '3' || ch == '4' || ch == '5' || ch == '6' || ch == '7' || ch == '8' || ch == '9' || ch == '.' || ch == ' ' || ch == '\n' || ch == ';') {
            if (ch == '0' || ch == '1' || ch == '2' || ch == '3' || ch == '4' || ch == '5' || ch == '6' || ch == '7' || ch == '8' || ch == '9') {
                b[aaa++] = ch;
            }
            if ((ch == ' ' || ch == '\n' || ch == ';') && (aaa != 0)) {
                b[aaa] = '\0';
                aaa = 0;
                char arr[30];
                strcpy(arr, b);
                nu.push_back(arr);
                cout << "(" << 55 << ","<< arr << ")" << endl;
                ++nc;
            }
        }

检查单词是否是标识符

先逐个检查字符 ch
是否是字母或数字(isalnum(ch))来识别可能的标识符。如果 ch
是字母或数字字符,它将其添加到临时字符串数组 buffer 中。如果 ch
是空格或换行,并且 buffer 不为空(即已经识别了部分标识符),则将 buffer
终结为字符串。接着,它检查 buffer 是否为关键字(使用 isKeyword
函数),如果是关键字,则将其添加到向量 k
中,并输出关键字的类别编码和值。如果不是关键字,它继续检查是否是用户定义的标识符(以小写字母开头,且未在
mark 数组中标记过)。如果是用户定义的标识符,将其添加到标识符向量 id
中,并输出整个标识符以及标识符类别为
“Identifier”。这段代码主要用于从输入中提取标识符,识别它们的类别,然后输出对应的词法分析结果。

主要代码如下:

if (isalnum(ch)) {
    buffer[j++] = ch;
} else if ((ch == ' ' || ch == '\n') && (j != 0)) {
    buffer[j] = '\0';
    j = 0;
    int t=isKeyword(buffer);
    if ( t!= 0&&t<37) {
        k.push_back(buffer);
        cout << "(" << t << ","<< buffer << ")" << endl;
        ++kc;
    } else {
        if (buffer[0] >= 97 && buffer[0] <= 122) {
            if (mark[buffer[0] - 'a'] != 1) {
                id.push_back(buffer);
                cout << "(" << buffer << ", Identifier)" << endl; // 输出整个标识符
                ++ic;
                mark[buffer[0] - 'a'] = 1;
            }
        }
    }
}

检查单词是否是单操作分隔符

单操作分隔符包括但不限于±*
,因为事先定义了这些分隔符的类型再single_op数组,所以这里通过一个循环遍历
single_op 数组,其中包含8个单字符分界符。如果字符 ch
与数组中的某个分界符匹配,它将输出该分界符的类别编码和值,并将字符 ch
添加到向量 ma 中。此外,它还维护一个 mark
数组来标记已经处理过的字符,以避免重复处理。这段代码主要用于从输入中检测单字符分界符,输出它们的类别编码和值,并维护已处理字符的记录。

主要代码如下:

for (i = 0; i < 8; ++i) {
    if (ch == single_op[i]) {
        int aa = ch;
    if (ch == '+') cout << "(" << TK_PLUS << "," << ch << ")" << endl;
    else if (ch == '-') cout << "(" << TK_MINUS << "," << ch << ")" << endl;
    else if (ch == '*') cout << "(" << TK_STAR << "," << ch << ")" << endl;
    else if (ch == '/') cout << "(" << TK_DIVIDE << "," << ch << ")" << endl;
    else if (ch == '=') cout << "(" << TK_ASSIGN << "," << ch << ")" << endl;
    else if (ch == '(') cout << "(" << TK_OPENPA << "," << ch << ")" << endl;
    else if (ch == ')') cout << "(" << TK_CLOSEPA << "," << ch << ")" << endl;
    else if (ch == ';') cout << "(" << TK_SEMOCOLOM << "," << ch << ")" << endl;
    else cout << "(" << TK_POUND << "," << ch << ")" << endl;
        if (mark[aa] != 1) {
            ma.push_back(ch);
            
            mark[aa] = 1;
            ++mc;
        }
    }
}

其他标识符的处理

对于一些不属于上述类别的符号,这里额外进行处理。利用枚举法,将表1{reference-type=“ref”
reference=“词法分类表”}中剩余的单词进行处理。

部分代码如下:

if (ch == ',') cout << "(" << TK_COMMA << "," << ch << ")" << endl;
else if (ch == '[') cout << "(" << TK_OPENBR << "," << ch << ")" << endl;
else if (ch == ']') cout << "(" << TK_CLOSEBR << "," << ch << ")" << endl;
else if (ch == '{') cout << "(" << TK_BEGIN << "," << ch << ")" << endl;
else if (ch == '}') cout << "(" << TK_END << "," << ch << ")" << endl;
else if (ch == ':') cout << "(" << TK_COLON << "," << ch << ")" << endl;
else if (ch == '\'') cout << "(" << TK_SINGLE_QUOTE << "," << ch << ")" << endl;

出错处理

当遇到一些我们未定义且非法的符号是,程序就会报错,如,̂$等符号的输入。此时程序会输(0,非法符号)

程序的输出

程序读入一个文件然后进行分析,然后按顺序输出其中的单词,格式为(助记符,符号)。

代码测试

程序源代码见附件"编译原理.cpp"。

测试用例一

测试用例一是一个简单的求两数之和的代码,如下:

#include <stdio.h>

int main()
{
    int a, b;
    scanf("%d%d", &a, &b);
    printf("%d\n", a + b);
    return 0;
}

程序运行后的结果如图观察可知,程序正确识别率include、int等关键字,也成功识别了数字0,运算符也正确,在此不多赘述。

测试用例二

见附件"test2.txt"。

第二个用例我增加了程序的复杂程序,使用了一个并行计算pi的程序,具体代码见附件。

测试用例三

见附件"test3.txt"。

第三个用例是基于哲学家问题的C语言程序。它做了很多宏定义,并且代码长度也更长,最终词法分析器仍能正确识别并输出结果,
%89%E7%BB%93%E6%9E%9C&pos_id=img-ze0IgFC0-1705759027753){#测试用例三结果 width=“.6\textwidth”}

测试用例四

见附件"test4.txt"。

第四个用例是我选择了我设计的词法分析器本身。这个txt文件代码超过300行。
可以看到,虽然代码更加复杂,但是因为程序本身的逻辑和运行方式没有变化,所以仍然正确识别出了各类单词并打印结果。

心得体会

  • 词法分析是编译器的第一步

    词法分析是编译器中的第一个重要步骤,它将源代码分割成不同的词法单元。这些词法单元包括标识符、关键字、运算符、分隔符等。这个过程的准确性对编译器的后续步骤至关重要。

  • 正则表达式是强大的工具

    正则表达式是处理文本匹配的强大工具,能够定义复杂的文本模式。在词法分析中,正则表达用于定义各种词法单元的模式,如标识符、整数、运算符等。

  • 模式匹配需要小心

    编写有效的正则表达式需要仔细考虑各种情况和可能的输入。特殊字符和元字符的处理需要格外小心,因为它们具有特殊的含义。转义字符的使用是解决这个问题的关键。

  • 测试是保证正确性的关键

    在开发词法分析器时,编写广泛的测试用例是非常重要的。测试用例应覆盖各种情况,包括正常情况和边界情况。这有助于确保词法分析器在各种情况下都能正确工作。本次实验我通过多个测试用例的调试,及时发现了程序中的很多问题。

  • 词法分析器与后续步骤协同工作

    词法分析器生成的词法单元序列将成为后续编译步骤的输入。因此,词法分析器的准确性和性能对编译器的整体性能和正确性都具有重要影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值