完整cmm解释器构造实践(三):语法分析

完整cmm解释器构造实践(三):语法分析

语法树节点



我的语法分析器不仅会判断cmm代码的语法是否正确, 同时会存储分析过程中得到的信息, 完成语法树的构建.

为什么要有语法树呢, 其实还是为了计算机方便做进一步的处理才用的, 语法树里面存储了从代码里面提取的信息, 我们生成语法树之后再通过遍历语法树来得到中间代码. 当然直接遍历语法树并解释执行也是可以的, 我们老师非得让我们有中间代码, 所以我的语法分析只为了生成中间代码而服务的, 虽然我的代码生成器本质上是解释器改造而成的.

那么既然语法树是存储代码信息的, 代码语法树中最小的单元树的节点就很关键了.

怎么定义树的节点, 每个人有不同的看法, 书上也没有仔细讲这部分内容, 我全凭自己的理解来定义的.

下面是我的TreeNode.java中的片段, 这里是TreeNode可能的类型以及成员变量, 注意TreeNode的类型不同, 成员变量存储的信息的意义也不相同, 所以一定要仔细看, 当然你也可以只看一个大概, 然后自己去定义, 因为TreeNode信息在生成代码的阶段还需要使用, 如果不了解其中每个变量在各种情况下的意义, 是没法生成中间代码的. 我这里就不具体解释了, 因为实在太过复杂了. 注释里有一些我当时注明的信息.

    /**
     * 语句块使用链表存储,使用NULL类型的TreeNode作为头部,注意不要使用NULL的节点存储信息,仅仅使用next指向下一个TreeNode
     */
    public static final int NULL = 0;
    /**
     * if语句
     * left存放exp表达式
     * middle存放if条件正确时的TreeNode
     * right存放else的TreeNode,如果有的话
     */
    public static final int IF_STMT = 1;
    /**
     * left存储EXP
     * middle存储循环体
     */
    public static final int WHILE_STMT = 2;
    /**
     * left存储一个VAR
     */
    public static final int READ_STMT = 3;
    /**
     * left存储一个EXP
     */
    public static final int WRITE_STMT = 4;
    /**
     * 声明语句
     * left中存放VAR节点
     * 如果有赋值EXP,则存放中middle中
     */
    public static final int DECLARE_STMT = 5;
    /**
     * 赋值语句
     * left存放var节点
     * middle存放exp节点
     */
    public static final int ASSIGN_STMT = 6;
    /**
     * 复合表达式
     * 复合表达式则形如left middle right
     * 此时datatype为可能为  LOGIC_EXP ADDTIVE_EXP TERM_EXP
     * value==null
     */
    public static final int EXP = 7;
    /**
     * 变量
     * datatype存放变量类型Token.INT 和 REAL
     * value存放变量名
     * left:
     * 在声明语句中变量的left值代表变量长度exp,在其他的调用中变量的left代表变量索引值exp,若为null,则说明是单个的变量,不是数组
     * 不存储值
     */
    public static final int VAR = 8;
    /**
     * 运算符
     * 在datatype中存储操作符类型
     */
    public static final int OP = 9;

    /**
     * 因子
     * 有符号datatype存储TOKEN.PLUS/MINUS,默认为Token.PLUS
     * 只会在left中存放一个TreeNode
     * 如果那个TreeNode是var,代表一个变量/数组元素
     * 如果这个TreeNode是exp,则是一个表达式因子
     * 如果是LITREAL,该LITREAL的value中存放字面值的字符形式
     * EXP为因子时,mDataType存储符号PLUS/MINUS
     */
    public static final int FACTOR = 10;

    /**
     * 字面值
     * value中存放字面值,无符号
     * datatype存放类型,在TOKEN中
     */
    public static final int LITREAL = 11;

    private int type;
    private TreeNode mLeft;
    private TreeNode mMiddle;
    private TreeNode mRight;
    /**
     * {@link TreeNode#getType()}为{@link TreeNode#VAR}时存储变量类型,具体定义在{@link Token}中INT / REAL?????
     * {@link TreeNode#getType()}为{@link TreeNode#OP}时存储操作符类型,具体定义在{@link Token}中 LT GT ......
     * {@link TreeNode#getType()}为{@link TreeNode#EXP}时表示复合表达式
     * {@link TreeNode#getType()}为{@link TreeNode#FACTOR}表示因子,mDataType处存储表达式的前置符号,具体定义在{@link Token}
     * 中PLUS/MINUS, 默认为PLUS
     * {@link TreeNode#getType()}为{@link TreeNode#LITREAL}表示字面值,存储类型
     */
    private int mDataType;
    /**
     * {@link TreeNode#getType()}为{@link TreeNode#FACTOR}时存储表达式的字符串形式的值
     * {@link TreeNode#getType()}为{@link TreeNode#VAR}时存储变量名
     */
    private String value;
    /**
     * 如果是代码块中的代码,则mNext指向其后面的一条语句
     * 普通的顶级代码都是存在linkedlist中,不需要使用这个参数
     */
    private TreeNode mNext;

注意有些成员变量, 比如mDataType可能存储的值其实是在Token.java中定义的常量.

cmm文法



program -> stmt-sequence

stmt-sequence -> statement ; stmt-sequence | statement | ε

statement -> if-stmt | while-stmt | assign-stmt | read-stmt | write-stmt | declare-stmt

stmt-block -> statement | { stmt-sequence }

if-stmt -> if ( exp ) then stmt-block | if ( exp ) then stmt-block else stmt-block

while-stmt -> while ( exp ) stmt-block

assign-stmt -> variable = exp ;

read-stmt -> read variable ;

write-stmt -> write exp ;

declare-stmt -> (int | real) ( (identifier [= exp ]) | (identifier [ exp ]) ) ;

variable -> identifier [ [ exp ] ]

exp -> addtive-exp logical-op addtive-exp | addtive-exp

addtive-exp -> term add-op additive-exp | term

term -> factor mul-op term | factor

factor -> ( exp ) | number | variable | Add-op exp

logical-op -> > | < | >= | <= | <> | ==

add-op -> + | -

mul-op -> * | /

上面是我的语法分析器所使用的cmm文法, 是由很多产生式构成的, 符号->称为推导, 加粗的是终结符, 没有加粗的是非终结符或文法描述用的符号.

上面的文法其实是一个LL(1)文法, 所谓的LL(1)文法, 就是非二义性, 且不含左递归. 说白了就是按照这个文法来检查语句是否合法, 不会出现一句代码出现两个意思, 同时程序在分析的过程中不会陷入无限递归当中. 再说明白一点就是, 我们大家很喜欢这个文法, 因为写程序很方便.

LL(1)文法使用的是自顶向下的分析方法, 第一个L代表从左向右扫描输入串, 第二个L代表使用最左推导. 数字1代表只需要提前向右看一个符号就可以知道如何推导.

递归下降子程序法



自左向右扫描输入串和提前向右看一个符号非常容易实现, 问题在于我们怎么将这种文法的规则转换为代码让程序来自动检查呢.

要知道文法之所以能描述一种语言的所有语法, 就是因为其具有递归的定义方法.

这里就需要介绍递归下降子程序法, 这种方法的核心思想就是利用递归和子程序来完成语法分析.

具体的做法就是让每一个产生式对应一个函数, 产生式中的终结符由函数自身来处理, 而产生式中的非终结符则通过调用相应的函数来处理. 比如对于statement函数, 则只需要根据情况调用if-stmt, while-stmt等函数就可以了, 很形象的说法就是任务层层下发, 最终每个终结符都被处理了.

那么statement函数怎么知道要调用哪个语句对应的函数呢, 这个时候就是提前向右看一个符号来发挥作用了.

比如下面的函数片段, 就是提前取下一个Token, 通过类型来判断应该调用哪个语句的处理函数.

    private static TreeNode parseStmt() throws ParserException {
        switch (getNextTokenType()) {
        case Token.IF: return parseIfStmt();
        case Token.WHILE: return parseWhileStmt();
        case Token.READ: return parseReadStmt();
        case Token.WRITE: return parseWriteStmt();
        case Token.INT: // same as REAL
        case Token.REAL: return parseDeclareStmt();
        case Token.LBRACE: return parseStmtBlock();
        case Token.ID: return parseAssignStmt();
        default:
            throw new ParserException("line " + getNextTokenLineNo() + " : expected token");
        }
    }

这个规则看似简单, 但是是正确的, if语句的开头一定是if, while语句的开头一定是while, 如果下一个符号是一个标识符, 那么一定是一个赋值语句, 如果下一个符号是int或者real, 那么一定是声明语句. 不信可以自己尝试.

在实现”提前向右看一个符号”时, 很多人的代码都混乱不堪, 因为有时候这个符号是当前正在处理的语句的一部分, 有时是下一句代码的开头, 暂时不能处理. 其实在java里面有一种叫做ListIterator的迭代器, 不仅可以往后迭代, 还可以往前退一步, 比如下面检查下一个Token类型的代码:

    /**
     * 获取下一个token的type,如果没有下一个token,则返回{@link Token#NULL}
     * @return
     */
    private static int getNextTokenType() {
        if (iterator.hasNext()) {
            int type = iterator.next().getType();
            iterator.previous();
            return type;
        }
        return Token.NULL;
    }

下面针对if语句的子程序举个例子, 其他的类似, 可以在文章最后的下载地址里面下载完整代码来查看.

    /**
     * if语句
     * @throws ParserException
     */
    private static TreeNode parseIfStmt() throws ParserException {
        TreeNode node = new TreeNode(TreeNode.IF_STMT);
        consumeNextToken(Token.IF);
        consumeNextToken(Token.LPARENT);
        node.setLeft(parseExp());
        consumeNextToken(Token.RPARENT);
        node.setMiddle(parseStmt());
        if (getNextTokenType() == Token.ELSE) {
            consumeNextToken(Token.ELSE);
            node.setRight(parseStmt());
        }
        return node;
    }

这个函数开头就是创建一个IF_STMT类型的TreeNode, 之所以这么明确, 是因为如果它被调用, 那么一定是要处理if语句了, 如果发现者不是if语句, 直接报错就行.

下面两个函数都是用来消耗Token的, 这俩函数在消耗Token之前会检查一下类型是否匹配, 如果不匹配就会抛出异常, 这个异常会一直向上抛, 抛到语法分析器的外面, 由调用语法分析器的人来处理异常.

消耗了一个if和一个左括号后(, 我们解析一个表达式, 而且解析表达式的函数会返回存储表达式的TreeNode给我们, 这个TreeNode实际上是表达式语法树的根, 注意parseExp()这类函数也是会抛异常的. 再消耗一个右括号), 这时我们会解析语句, 注意这个语句是广义上的语句, 也就是说可能是单条语句, 也可能是一个语句块. 随后会判断下一个Token是不是else, 如果是就会对else条件进行相应的处理, 最后将TreeNode返回.

点我下载代码

时间: 2024-10-10 18:19:56

完整cmm解释器构造实践(三):语法分析的相关文章

完整cmm解释器构造实践(二):词法分析

cmm是c的一个子集,保留字只有如下几个 if else while read write int real 特殊符号有如下几个 + - * / = < == <> ( ) ; { } [ ] /* */ 标识符:由数字,字母或下划线组成的字符串,且不能使关键字,第一个字母不能是数字 如果了解c很容易明白上面的是什么意思,也会明白cmm其实有的东西并不多,所以做cmm解释器相对来说比较简单. 上面的特殊符号实际上比较少,我个人实现的时候还对> >= <=等做了相关的支持

Linux课题实践三——字符集总结与分析

Linux课题实践三——字符集总结与分析 20135318  刘浩晨 字符是各种文字和符号的总称,包括各国家文字.标点符号.图形符号.数字等.字符集是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集名称:ASCII字符集.GB2312字符集.BIG5字符集. GB18030字符集.Unicode字符集等. 1.总结ISO.UCS/UTF.GB系列字符集的由来.异同 (1).ISO/IEC ISO/IEC 646:是国际标准化组织(ISO)及国际电工委员会(IEC)联合制定

OWIN的理解和实践(三) –Middleware开发入门

原文:OWIN的理解和实践(三) –Middleware开发入门 上篇我们谈了Host和Server的建立,但Host和Server无法产出任何有实际意义的内容,真正的内容来自于加载于Server的Middleware,本篇我们就着重介绍下Middleware的开发入门. Middleware是什么 如果把HTTP交互理解为一次答题活动,那么Request是问题,Response就是答案,Server是课堂,Middleware就是参与者,注意我这里用的是参与而不是解答,因为我们允许有些Midd

软件构造 第三章第三节 抽象数据型(ADT)

软件构造 第三章第三节 抽象数据型(ADT) Creators(构造器): 创建某个类型的新对象,?个创建者可能会接受?个对象作为参数,但是这个对象的类型不能是它创建对象对应的类型.可能实现为构造函数或静态函数.(通常称为工厂方法) t* ->  T 例子:Integer.valueOf( ) Producers(生产器): 通过接受同类型的对象创建新的对象. T+ , t* -> T 例子:String.concat( ) Observers(观察器): 获取抽象类型的对象然后返回一个不同类

Docker入门实践(三) 基本操作

Docker安装完毕,我们就可以试着来运行一些命令了,看看docker可以干什么. (一) 创建一个容器 首先,让我们运行一个最简单的容器,hello-world.如果安装没有问题,并运行正确的话,应该会出现下面的结果: $ docker run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world c04b14da8d14: Pull comp

结对项目 实践三

一:题目简介 该次实践作业主要实现一个手机的简单应用,计算器.可以在手机平台上实现,这里模拟的安卓环境. 二:结对分工及过程 这次结对的同学是郭淑涛,主要是两人确定了题目后,编写代码,之后在各自的电脑环境平台下进行运行,有问题互相交流,互相学习,也收获很多. 三:代码地址 https://github.com/githubmengqian/partner-project 四:测试情况 首先电脑需要有安卓的eclipse的软件,配置好相应环境后,在平台上建项目,之后进行运行,这里模拟的是安卓环境,

Android最佳性能实践(三)——高性能编码优化

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/42318689 在前两篇文章当中,我们主要学习了Android内存方面的相关知识,包括如何合理地使用内存,以及当发生内存泄露时如何定位出问题的原因.那么关于内存的知识就讨论到这里,今天开始我们将学习一些性能编码优化的技巧. 这里先事先提醒大家一句,本篇文章中讨论的编码优化技巧都是属于一些"微优化",也就是说即使我们都按照本篇文章的技巧来优化代码,在性能方面也是看不出有什么

中小型研发团队架构实践三要点--转

来自微信公众号聊聊架构 作者|张辉清 编辑|雨多田光 如果你正好处在中小型研发团队…… 中小型研发团队很多,而社区在中小型研发团队架构实践方面的探讨却很少.中小型研发团队特别是 50 至 200 人的研发团队,在早期的业务探索阶段,更多关注业务逻辑,快速迭代以验证商业模式,很少去关注技术架构. 这时如果继续按照原有的架构及研发模式,会出现大量的问题,再也无法玩下去了.能不能有一套可直接落地.基于开源.成本低,可快速搭建的中间件及架构升级方案呢? 我是一个有十多年经验的 IT 老兵,曾主导了两家公

Docker实践(三):数据持久化及共享

环境说明: 主机名 操作系统版本 IP地址 docker版本 ubuntu1604 Ubuntu 16.04.5 172.27.9.31 18.09.2 ubuntu安装详见:Ubuntu16.04.5以lvm方式安装全记录docker安装详见:Ubuntu16.04安装Docker?在Linux上运行的Docker有三种不同的方式将数据从 Docker Host挂载到 Docker 容器,并实现数据的读取和存储:volumes.bind mounts.tmpfs.??三者的区别在于数据存储在d