Lab. 3: 词法分析实验

Lab3 词法分析实验

1、实验目的

​ (1)熟悉 C 语言的词法规则,了解编译器词法分析器的主要功能和实现技术,掌握典型词法分析器构造方法,设计并实现 C 语言词法分析器;
​ (2)了解 Flex 工作原理和基本思想,学习使用工具自动生成词法分析器;
​ (3)掌握编译器从前端到后端各个模块的工作原理,词法分析模块与其他模块之间的交互过程。

2、实验内容

​ 根据C语言词法规则,设计识别C语言所有单词类的词法分析器的确定有限状态自动机,并使用Java,采用数据中心法设计并实现词法分析器。词法分析器的输入为C语言源程序,输出为属性字流。

3、实验过程

3.1 DFA设计

3.1.1 状态设计

​ 状态主要相关以下方面:1、字符常量,字符串常量,标识符识别。2、运算符,界限符识别。3、整数常量,浮点数常量识别。3、特殊状态BACK, EOF, NULL, INITIAL。下面为状态设计。

    public static enum STATE{
        //0、各种终止态
        INITIAL,    //初始态
        IDENTIFIER, //标识符(终止态)
        KEYWORD,    //关键字(终止态)
        INTEGER_CONSTANT,   //整型常量(终止态)
        FLOATING_CONSTANT,  //浮点常量(终止态)
        CHARACTER_CONSTANT, //字符常量(终止态)
        STRING_LITERAL,     //字符串常量(终止态)
        OPERATORS,          //操作符,界符(终止态)
        EOF,                //结束符号(终止态)
        NULL,               //无意义符号,用于空格等状态
        ERROR,              //错误态,识别到错误的情况
        BACK,               //回退一次

        //以下为各个中间态
        //1、识别标识符,字符常量,字符串常量使用的状态

        MidIdentifier,   //标识符中间态
        UPrefix,         //以U|u标识的字/字符串前缀
        LPrefix,         //以L|l标识的字/字符串前缀
        U8Prefix,        //以u8开头的字符串前缀
        MidChar,            //字符常量中间态
        CHAR_WAIT,          //字符常量阻塞态
        EscapeChar,         //含有转义\的字符常量中间态
        EscapeStr,          //含有转义\的字符串常量中间态
        MidStr,             //字符串常量中间态
        STR_WAIT,            //字符串常量阻塞态

        //2、识别整形常量、浮点常量使用的状态
        NoZeroDecInt,             //非0十进制整数
        Zero,                     //以0开头,可能是十进制/十六进制小数,也可能是10,8,16进制整数
        OctInt,                   //8进制整数
        HexInt,                   //16进制整数
        DecFloatFrac,             //十进制浮点数小数部分
        IntSuffix,                 //整数后缀部分
        HexFloatFrac,             //16进制小数部分
        Exponent,                 // e|E|p|P标识处
        OrderPart,                //阶码部分
        FLSuffix,                 //浮点数的F/L后缀

        // 2.1 识别u, l, ul, ull, lu, llu ll 这7种整数后缀的状态
        ULSuffix,
        ULLSuffix,
        LUSuffix,
        LLUSuffix,
        USuffix,
        LSuffix,
        LLSuffix,

        //3、识别运算符,界限符使用的状态
        MidSymbol
    }

​ 根据设置好的状态绘制相应的DFA图。这里需要说明的是,由于状态过多,难以在一张图中绘制完成,并且字符、字符串以及常量的识别部分和数字产量识别部分没有交集,故以此为界限分为两个主要部分。一些特殊状态在下面说明。

在这里插入图片描述

​ 这一部分对应字符常量、字符串常量、标识符以及操作符部分。黄色字体为终止态。

在这里插入图片描述

​ 这一部分对应于整数常量,浮点数常量的识别部分。这里使用了两个特殊的状态 IntSuffixBACK,对应于整形常量可能存在的 u/l 后缀识别以及特殊的需要程序进行控制的BACK状态。IntSuffix 则拓展(左边)如下。首先会根据当前字符选择迁移到 LSuffix 或者 USuffix 状态。后续再与进入的字符进行匹配。

在这里插入图片描述

​ 右半部分对应的是两个特殊状态 ‘EOF’ 和 ‘NULL’,当读取到 ‘\u001a’ 时为EOF,读取到任何无法转移的字符时转移到NULL状态。

​ 特别的是,考虑到类似如字符,字符串产量的识别当识别到单引号/双引号就可以终止,而类似于标识符则需要识别到other才能终止并回退剔除的原因。我添加了一些中间态,以便在扫描过程中所有的终止态能够统一用回退一次的操作进行控制。

3.1.2 DFA编码实现

​ 使用JAVA编码实现DFA。还有一个状态集合STATE,终止态集合Set endStateSet,当前状态curState,字符集通过若干函数判断进行实现,初始状态为STATE.INITIAL,映射关系由成员函数 public void stateTransition(char curCh, String curStr)实现。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

​ 通过以上部分完成了DFA的构造,为了使用该DFA进行识别还需要构造一些数据类以及工具类。

3.2 Token

3.2.1 数据类Token

​ 根据实验要求,最终输出到文件中的内容是具有一定格式的Token字符串,这里构造一个Token类,并重写.toString()方法实现自动根据内容产生Token串。
​ Token类的标识通过一个枚举 enum TokenTypes 实现。

    //属性字类型
    public static enum TokenTypes
    {
        IDENTIFIER,
        KEYWORD,
        INTEGER_CONSTANT,
        FLOATING_CONSTANT,
        CHARACTER_CONSTANT,
        STRING_LITERAL,
        OPERATORS,
        EOF,
        ERROR
    }

​ Token具有内容,类型,id号,起始编号,结束编号,起始行号,起始列号这些属性。

private String content;     //token原内容
private TokenTypes type;    //token类型
private Integer id;         //token序号
private Integer stNum;      //token起始编号
private Integer edNum;      //token结束编号
private Integer stRow;      //token起始行。
private Integer stCol;      //token起始列。

​ 由于DFA会将关键字识别为标识符,在构造Token的时候需要判断标识符是否为关键字,将其转化为对应的关键字类型。此外输出时,KWYWORD类型和OPERATOR类型需要标记为其具体的标记。这两步使用两个函数实现:

//将关键字和运算符映射成具体的类型。
private static String mappingTokenTypeToActualType(Token token)
//将DFA终止状态映射为对应的Token类型
public static TokenTypes mappingDFAEndStateToTokenType(DFA.STATE state)
3.2.2 Tokens工具类TokensBlock

​ 由于一组源代码对应于一组Token,为了便于管理,构造一个TokensBlock集合管理属于同一组源代码的Token。
​ TokensBlock记录对应源代码的文件路径地址,输出tokens标记的文件路径地址,提供添加Token方法。

在这里插入图片描述

3.3 标准元素StdElements

​ 静态类 StdElements 记录所有C语言关键字以及相应的运算符,界符。提供对应的判别方法:

public static boolean isKeyWords(String str)
public static boolean isSymbol(String str)

3.4 扫描器C_CodeScanner

​ C_CodeScanner 能够扫描C语言源代码并生成相应tokens。成员变量如下:

private String srcPath;	
private String[] src;
private DFA dfa;
private int curRowIndex;
private int curColIndex;

private int forePointer;//前向指针
private int backPointer;//后向指针

private TokensBlock tokensBlock;

​ 这里读取文件部分本应使用框架提供的方法,但经过测试,框架提供的 this.srcLines=MiniCCUtil.readFile(iFile); 方法无法读取到文件行末尾的 ‘/r’, ‘/n’ 这类字符,而框架提供的默认词法分析器 antlr 在读取源文件时会读取到这些控制符,并且况且提供的pp在运行时会在每一行的行尾自动添加 “/r/n”(由反编译得到的字节码分析得到),因此,如果使用框架本身提供的读取方法使用DFA识别的结果会导致最终token定位标签(开始编号,结束编号)与 antlr 结果不一致。因此我在 C_CodeScanner 中使用 FileInputStream 的方式来读取文件而不是使用框架提供的 BufferReader 来进行文件读取。这笔部分代码如下:

public String[] readFile(String fPath)
{
    List<String> strList = new ArrayList<>();
    FileInputStream in = null;
    StringBuilder strb = new StringBuilder();
    try{
        in =new FileInputStream(fPath);
    }catch(FileNotFoundException e) {
        System.out.println("can not find it");
        System.exit(1);
    }
    try{
        int cur = 0;
        while((cur = in.read()) != -1){
            //System.out.println(count);
            strb.append((char)cur);
            if((char)cur == '\n') {
                strList.add(strb.toString());
                strb = new StringBuilder();
            }
        }
        strList.add("\u001a");
    }catch(IOException e) {
        e.printStackTrace();
    }
    String[] srcArray = new String[strList.size()];
    for(int i = 0; i < strList.size(); i++) {
        srcArray[i] = strList.get(i);
    }
    return srcArray;
}

​ 扫描器按照行进行扫描。每次扫描过程中先读取当前指示的字符,然后进行状态转移。转移完成后进行一系列判定并更新各个变量。具体说明如下:

1、终态非空:(1)后向指针退回一格,计数器i减一。(2)添加Token,根据DFA状态以及各个指针变量信息构造Token并添加到tokensBlock中。(3)状态重置,调用方法重置状态机,清空Token字符串构造器,让前向指针移动到后向指针+1即: forePointer = backPointer + 1,重设列指针,与forePointer同步。这样在一次扫描后后向指针前移以同步指针。

2、非空且为EOF:完成终止态非空后将计数器回复即可。EOF状态到达文末,不需要回退扫描。

3、终态为空(NULL):状态为NULL时,为识别到一些不具备识别意义的单字符。这里需要调整前向指针来同步。forePointer = baackPointer +1;

4、终态为BACK(转移后):转移后为back状态需要让字符串构造器删除最后一个字符,并且后向指针和计数器i -2 (实际是后退1,但下次循环开始时会自增,这里需要先-2)。

5、终态为BACK(转移前):由于回退了一个字符,这里需要替换被会退掉的字符为空字符来让状态机从前一个状态进行空转移,看是否能转移到一个终止态,如果不行状态机会再次到BACK状态,前移。类似于回溯的做法。

6、其他状态:其他状态只需要字符构造器添加当前的字符即可。

​ 扫描器这一部分和DFA是实现的关键。共同实现了自动代码识别以及分类,完成了词法分析的主要工作。其他数据,方法都为这一目的提供支持。具体的扫描器扫描过程代码如下:

private void rowAnalyse(String code)
    {
        StringBuilder tokenContent = new StringBuilder();
        curColIndex = 0;
        for(int i = 0; i < code.length(); i++)
        {
            char curCh = code.charAt(i);
            if(CDfa.getCurState() == DFA.STATE.BACK)
                curCh = '\0';
            curColIndex = i;
            CDfa.stateTransition(curCh, tokenContent.toString());

            if(CDfa.isEndState() && CDfa.getCurState() != DFA.STATE.NULL && CDfa.getCurState() != DFA.STATE.EOF)    //转移后达到终止态
            {
                backPointer -= 1;
                i--;    //需要回退一次重新确认状态

                //1、添加token
                Token.TokenTypes type = Token.mappingDFAEndStateToTokenType(CDfa.getCurState());
                Token token = new Token(tokenContent.toString(), type, tokensBlock.getTokensSize(), forePointer, backPointer, curRowIndex, curColIndex - (backPointer - forePointer) - 1 );
                tokensBlock.addToken(token);
                //System.out.println(token.toString());

                //2、重置变量
                tokenContent = new StringBuilder();
                forePointer = backPointer + 1;
                CDfa.initDFA();
            }
            else if(CDfa.isEndState() && CDfa.getCurState() == DFA.STATE.EOF)
            {
                backPointer -= 1;
                Token.TokenTypes type = Token.mappingDFAEndStateToTokenType(CDfa.getCurState());
                Token token = new Token(tokenContent.toString(), type, tokensBlock.getTokensSize(), forePointer, backPointer, curRowIndex, curColIndex - (backPointer - forePointer) - 1 );
                tokensBlock.addToken(token);
                CDfa.initDFA();
            }
            else if(CDfa.isEndState() && CDfa.getCurState() == DFA.STATE.NULL)
            {
                forePointer = backPointer + 1;
                CDfa.initDFA();
            }
            else if(CDfa.getCurState() == DFA.STATE.BACK)
            {
                tokenContent.deleteCharAt(tokenContent.length()-1);  //回退
                backPointer -= 2;
                i -= 2;    //需要回退一次重新确认状态
            }
            else {
                tokenContent.append(curCh);
            }

            backPointer += 1;
        }
    }

4、实验运行及结果

4.1 框架装入

​ 完成代码编写和测试后,需要将其装入框架。下图为我的包结构:

在这里插入图片描述

​ 将 MyScanner 包整个装入框架的src文件夹下,形成如下结构:

在这里插入图片描述

​ 为了在框架中便于使用,在bit.minisys.minicc.scanner下创建类 C_JavaScanner ,该类实现框架中的 IMiniCCScanner 接口。将 run 方法重写如下:

在这里插入图片描述

​ 这里输出,创建文件的方式使用的仍然是框架提供的方法。

​ 为了在框架中调用 C_JavaScanner 类,修改 config.xml ,结果如下:

在这里插入图片描述

​ 由于考虑到框架中存在预处理程序,我也并没有写处理注释的情况,这里选择使用默认的预处理程序,在第二步将路径调整为所写的类路径。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【问题描述】 请根据给定的文法设计并实现词法分析程序,从源程序中识别出单词,记录其单词类别和单词值,输入输出及处理要求如下: (1)数据结构和与语法分析程序的接口请自行定义;类别码需按下表格式统一定义; (2)为了方便进行自动评测,输入的被编译源文件统一命名为testfile.txt(注意不要写错文件名);输出的结果文件统一命名为output.txt(注意不要写错文件名),结果文件中每行按如下方式组织: 单词类别码 单词的字符/字符串形式(中间仅用一个空格间隔) 单词的类别码请统一按如下形式定义: 单词名称 类别码 单词名称 类别码 单词名称 类别码 单词名称 类别码 标识符 IDENFR else ELSETK - MINU = ASSIGN 整形常量 INTCON switch SWITCHTK * MULT ; SEMICN 字符常量 CHARCON case CASETK / DIV , COMMA 字符串 STRCON default DEFAULTTK < LSS ( LPARENT const CONSTTK while WHILETK GRE [ LBRACK char CHARTK scanf SCANFTK >= GEQ ] RBRACK void VOIDTK printf PRINTFTK == EQL { LBRACE main MAINTK return RETURNTK != NEQ } RBRACE if IFTK + PLUS : COLON 【输入形式】testfile.txt中的符合文法要求的测试程序。 【输出形式】要求将词法分析结果输出至output.txt中。 【特别提醒】(1)读取的字符串要原样保留着便于输出,特别是数字,这里输出的并不是真正的单词值,其实是读入的字符串,单词值需另行记录。 (2)本次作业只考核对正确程序的处理,但需要为今后可能出现的错误情况预留接口。 (3)在今后的错误处理作业中,需要输出错误的行号,在词法分析的时候注意记录该信息。 (4)单词的类别和单词值以及其他关注的信息,在词法分析阶段获取后,后续的分析阶段会使用,请注意记录;当前要求的输出只是为了便于评测,完成编译器中无需出现这些信息,请设计为方便打开/关闭这些输出的方案。 【文法定义】请到“2020年文法定义(用于查看文法,不计入成绩)”作业中查看文法 【样例输入】 coNst int cONst1 = 001, const2 = -100; const char const3 = '_'; int change1; char change3; int gets1(int var1,int var2){ change1 = var1 + var2; return (change1); } void main(){ printf("Hello World"); printf(gets1(10, 20)); } 【样例输出】 CONSTTK coNst INTTK int IDENFR cONst1 ASSIGN = INTCON 001 COMMA , IDENFR const2 ASSIGN = MINU - INTCON 100 SEMICN ; CONSTTK const CHARTK char IDENFR const3 ASSIGN = CHARCON _ SEMICN ; INTTK int IDENFR change1 SEMICN ; CHARTK char IDENFR change3 SEMICN ; INTTK int IDENFR gets1 LPARENT ( INTTK int IDENFR var1 COMMA , INTTK int IDENFR var2 RPARENT ) LBRACE { IDENFR change1 ASSIGN = IDENFR var1 PLUS + IDENFR var2 SEMICN ; RETURNTK return LPARENT ( IDENFR change1 RPARENT ) SEMICN ; RBRACE } VOIDTK void MAINTK main LPARENT ( RPARENT ) LBRACE { PRINTFTK printf LPARENT ( STRCON Hello World RPARENT ) SEMICN ; PRINT
好的,词法分析器是编译器中的一个重要组成部分,其作用是将源代码转换为单词序列(token),为后续的语法分析器提供输入。以下是一个简单的词法分析器的设计: 1. 定义单词类型 在设计词法分析器之前,需要定义程序中可能出现的单词类型,例如:关键字、标识符、常量、运算符等等。 2. 定义有限自动机 在词法分析器中,使用有限自动机(Finite Automaton,FA)来识别不同的单词类型。有限自动机可以用状态转移图来表示,其中每个状态代表自动机的状态,状态之间的转移代表输入字符后自动机的状态转换。可以使用工具如JFLAP等来创建有限自动机。 3. 实现词法分析器 在实现词法分析器时,可以使用以下步骤: - 读取源代码文件。 - 逐个字符读取源代码,并将其转换为单词序列。 - 对每个单词,使用有限自动机进行识别,并将其分类为不同的单词类型。 - 将识别出的单词存储为单词序列,并返回给语法分析器。 可以使用工具如Lex、Flex等来辅助实现词法分析器。 4. 调试和测试 在实现词法分析器后,需要进行调试和测试。可以使用各种测试用例来测试词法分析器的正确性,并进行调试以解决可能出现的问题。 以上是一个简单的词法分析器的设计,具体的实现方式和细节还需要结合具体的编程语言和需求来进行设计。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值