Reactjs开发自制编程语言Monkey的编译器:语法解析

前面章节中,我们完成了词法解析器的开发。词法解析的目的是把程序代码中的各个字符串进行识别分类,把不同字符串归纳到相应的分类中,例如数字构成的字符串统一归类为INTEGER,字符构成的字符串,如果不是关键字的话,那么他们统一被归纳为IDENTIFIER。

例如下面这条语句:

let foo = 1234;

语句经过词法解析器解析后,就会转变为:

LET IDENTIFIER ASSIGN_SIGN INTEGER SEMI

完成上面工作后,词法解析器的任务就完成了,接下来就轮到词法解析器出场。词法解析器的作用是,判断上面的分类组合是否合法。显然上面分类的组合次序是合法的,但是对于下面语句:

let foo + 1234;

词法解析得到的分类组合为:

LET IDENTIFIER PLUS_SIGN INTEGER SEMI

显然上面的组合是错误的,语法解析器就是要检测到上面这些错误组合。如果组合是正确的,那么语法解析器还会根据组合所形成的逻辑关系构造出一种数据结构叫抽象语法树,其本质就是一种多叉树,有了这种数据结构,编译器就可以为
代码生成二进制指令,或者直接对程序进行解释执行。

事实上,每一句代码的背后都遵循着严谨的逻辑结构。例如当你看到关键字 let 时,你一定知道,在后面跟着的必须是一个字符串变量,如果let 后面跟着一个数字,那就是一种语法错误。这种规则的表现方式就叫语法表达式,例如let 语句的语法表达式如下:

LetStatement := LET IDENTIFIER ASSIGN_SIGN EXPRESSION SEMI

EXPRESSION 用来表示可以放在等号后面进行赋值的代码字符串,它可以是一个数字,一个变量字符串,也可以是一串复杂的算术表达式。上面这种语法表达式也叫Backus-Naur 范式,其中Backus是IBM的研究员,是他发明了第一个编译器,用来编译Fortan 语言。大家注意看,语法表达式其实隐含着一种递归结构,上面表达式中右边的EXPRESSION 其实还可以继续分解,相关的内容我们会在后面给出。

上面的语法表达式其实也可以对应成一颗多叉树,树的父节点就是左边的LetStatment,右边的五个分类对应于叶子节点,其中EXPRESSION有可以继续分解,于是它自己就是多叉树中,一颗子树的父节点。在后续的课程中,我们会用代码亲自绘制出对应的多叉树。

链接:http://tomcopeland.blogs.com/EcmaScript.html 描述的就是javascript语言的语法表达式,有兴趣的同学可以点进去看看。

语法解析的本质就是,先让词法解析器把代码字符串解析成各种分类的组合,然后根据早已给定的语法表达式所定义的语法规则,看看分类的组合方式是否符合语法表达式的规定。我们本节将实现一个简单的语法解析器,它的作用是能解析let 语句,例如:

let foo = 1234;
let x = y;

语法解析器在实现语法解析时,一般有两种策略,一种叫自顶向下,一种是自底向上。我们将采取自顶向下的做法,语法解析是编译原理中最为抽象的模块,一定得通过代码调试来加深理解,以下就是我们实现let 语句解析的语法解析器代码,首先在本地目录src下面新建一个文件名为MonkeyCompilerParser.js的文件,并添加如下代码:

class Node {
    constructor (props) {
        this.tokenLiteral = ""
    }
    getLiteral() {
        return this.tokenLiteral
    }
}

class Statement extends Node{ 
    statementNode () {
        return this
    }
}

class Expression extends Node{
    constructor(props) {
        super(props)
        this.tokenLiteral = props.token.getLiteral()
    }
    expressionNode () {
        return this
    }
}

class Identifier extends Expression {
    constructor(props) {
        super(props)
        this.tokenLiteral = props.token.getLiteral()
        this.token = props.token
        this.value = ""
    }
}

由于语法解析的结果是要构造一颗多叉树,因此类Node用来表示多叉树的叶子节点,Statement 和 Expression依次继承Node,注意看Expression的代码,我们要解析的语句形式如下:

let foo = 1234;

它对应的语法表达式为:

LET IDENTIFIER ASSIGN_SIGN INTEGER SEMI

我们前面提到EXPRESSION 可以表示一个变量,一个整数,或者是一个复杂的算式表达式,对于上面我们要解析的语句,等号后面是1234,它对应的分类就是INTEGER,于是我们可以猜测,上面Expression类的构造函数constructor中,props.token对应的就是INTEGER,于是getLiteral()得到的就是分类INTEGER对应的数字字符串,也就是1234.

Identifier类对应的就是语法表达式LET 后面的IDENTIFIER分类,对应我们给出的例子,let 后面跟着变量字符串foo,于是我们可以猜测,Identifier类的构造函数中,props.token 对应的就是 IDENTIFIER , token.getLiteral() 得到的就是变量字符串 “foo”

我们继续添加相应代码:

class LetStatement extends Statement {
    constructor(props) {
        super(props)
        this.token = props.token
        this.name = props.identifier
        this.value = props.expression
        var s = "This is a Let statement,left is an identifer:"
        s += props.identifer.getLiteral()
        s += " right size is value of "
        s += this.value.getLiteral()
        this.tokenLiteral = s
    }
}

LetStatement类用来表示与let 相关的语句,从语法表达式可以看成,let 语句由两个关键部分组成,一个是let关键字后面的变量,一个是等号后面的数值或者是变量,或者是算术表达式。因此在上面的LetStatement类中,props.token 对应的就是关键字 LET,props.identifier对应的就是类Identifier的实例,其实也就是let关键字后面的变量,props.expression 对应的是等号后面的成分,对应到我们的具体实例中,它就是一个数字,也就是INTEGER.

我们继续添加代码:

class Program {
    constructor () {
        this.statements = []
    }

    getLiteral() {
        if (this.statements.length > 0) {
            return this.statements[0].tokenLiteral()
        } else {
            return ""
        }
    }
}

Program 类是对整个程序代码的抽象,它由一系列Statement组成,Statement基本可以理解为一句以分号结束的代码。于是整个程序就是由很多条以分号结束的语句代码的集合。当然有一些不已分号结束的语句也是Statement,例如:

if (x == 10) {...}
else {...}

此类语句也是属于Statement。 接着我们就进入解析器的实现部分:

class MonkeyCompilerParser {
    constructor(lexer) {
        this.lexer = lexer
        this.lexer.lexing()
        this.tokenPos = 0
        this.curToken = null
        this.peekToken = null
        this.nextToken()
        this.nextToken()
        this.program = new Program()
    }

    nextToken() {
        /* 一次必须读入两个token,这样我们才了解当前解析代码的意图 例如假设当前解析的代码是 5; 那么peekToken就对应的就是 分号,这样解析器就知道当前解析的代码表示一个整数 */
        this.curToken = this.peekToken
        this.peekToken = this.lexer.tokens[this.tokenPos]
        this.tokenPos++
    }

    parseProgram() {
        while (this.curToken.getType() !== this.lexer.EOF) {
            var stmt = this.parseStatement()
            if (stmt !== null) {
                this.program.statements.push(stmt)
            }
            this.nextToken()
        }
        return this.program
    }

    parseStatement() {
        switch (this.curToken.getType()) {
            case this.lexer.LET:
              return this.parseLetStatement()
            default:
              return null
        }
    }

    parseLetStatement() {
       var props = {}
       props.token = this.curToken
       //expectPeek 会调用nextToken将curToken转换为
       //下一个token
       if (!this.expectPeek(this.lexer.IDENTIFIER)) {
          return null
       }
       var identProps = {}
       identProps.token = this.curToken
       identProps.value = this.curToken.getLiteral()
       props.identifer = new Identifier(identProps)

       if (!this.expectPeek(this.lexer.ASSIGN_SIGN)) {
           return null
       }

       if (!this.expectPeek(this.lexer.INTEGER)) {
           return null
       }

       var exprProps = {}
       exprProps.token = this.curToken
       props.expression = new Expression(exprProps)

       if (!this.expectPeek(this.lexer.SEMICOLON)) {
           return null
       }

       var letStatement = new LetStatement(props)
       return letStatement
    }

    curTokenIs (tokenType) {
        return this.curToken.getType() === tokenType
    }

    peekTokenIs(tokenType) {
        return this.peekToken.getType() === tokenType
    }

    expectPeek(tokenType) {
        if (this.peekTokenIs(tokenType)) {
            this.nextToken()
            return true
        } else {
            return false
        }
    }
}

解析器在构造时,需要传入词法解析器,因为解析器解析的内容是经过词法解析器处理后的结果,也就是一系列token的组合。它在构造函数中,先调用解析器的lexing()接口,先对代码进行词法解析,词法解析会把源代码解析成一系列token的组合,curToken用于指向词法解析器对代码进行解析后得到的token数组中的某一个,而peekToken指向curToken指向token的下一个token。

接着连续两次调用nextToken,目的是让curToken指向词法解析器解析得到的token数组中的第一个token,peekToken指向第二个token,当parseProgram被调用时,程序就启动了词法解析的过程。在该函数中,每次取出一个token,如果当前token代表的不是程序结束标志的话,它就调用parseStatement来解析一条以语句。

在parseStatement中,它会根据当前读入的token类型来进行不同的操作,如果读到的当前token是一个关键字let,那意味着,解析器当前读到了一条以let开始的变量定义语句,于是解析器接下来就要检测后面一系列token的组合关系是否符合let 语句语法表达式指定的规范,负责这个检测任务的就是函数parseLetStatement()。

parseLetStatement函数的实现逻辑严格遵守语法表达式的规定。

LetStatement := LET IDENTIFIER ASSIGN_SIGN EXPRESSION SEMI

我们看上面的表达式,它表明,一个let 语句必须以let 关键字开头,然后必须跟着一个变量字符串,接着必须跟着一个等号,然后等号右边是一个算术表达式,最后必须以分号结尾,这个组合关系只要有某部分不对应,那么就出现了语法错误。

在调用parseLetStatement之前的函数parseStatement里面的switch 语句里已经判断第一步,也就是语句确实是以关键字let开始之后,才会进入parseLetStatement,

if (!this.expectPeek(this.lexer.IDENTIFIER)) {
          return null
       }

上面代码用于判断,跟着关键字let 后面的是不是变量字符串,也就是对应的token是否是IDENTIFIER,如果不是,解析出错直接返回。如果是就用当前的token构建一个Identifier类,并把它作为初始化LetStatement类的一部分。接下来就得判断跟着的是否是等号了:

if (!this.expectPeek(this.lexer.ASSIGN_SIGN)) {
           return null
       }

上面代码片段就是用来判断跟在变量字符串后面的是否是等号,如果不是,那么语法错误,直接返回。在等号后面必须跟着一个算术表达式,算术表达式又可以分解为一个数字常量字符串,一个变量字符串,或者是由变量字符串和数字常量字符串结合各种运算符所组成的算术式子,由于为了简单起见,我们现在只支持等号后面跟着数字常量表达式,也就是我们现在的解析器只能支持解析类似如下的语句:

let foo = 1234;
let x = 2;

对于型如以下合法的let语句解析,我们将在后续章节再给出:

let foo = bar;
let bar = 2*3 + foo / 2;

如果等号后面跟着的字符串确实是一个数字常量字符串,那么我们就构造一个Expression类,这个类会成为LetStatement类的组成部分。根据语法表达式规定,let 语句最后要以分号结尾,因此代码片段:

if (!this.expectPeek(this.lexer.SEMICOLON)) {
           return null
       }

其作用就是用于判断末尾是否是分号,如果不是的话,那就出现了语法错误。

上面代码完成后,我们需要在MonkeyCompilerIDE 组件中引入语法解析器,并将用户在编辑框中输入的代码提交给解析器进行解析,因此相关改动如下:

import MonkeyCompilerParser from './MonkeyCompilerParser'
class MonkeyCompilerIDE extends Component {
    ....
    // change here
    onLexingClick () {
      this.lexer = new MonkeyLexer(this.inputInstance.getContent())
      this.parser = new MonkeyCompilerParser(this.lexer)
      this.parser.parseProgram()
      this.program = this.parser.program
      for (var i = 0; i < this.program.statements.length; i++) {
          console.log(this.program.statements[i].getLiteral())
      }
    }
    .... 
render () {
        // change here
        return (
          <bootstrap.Panel header="Monkey Compiler" bsStyle="success">
            <MonkeyCompilerEditer 
             ref={(ref) => {this.inputInstance = ref}}
             keyWords={this.lexer.getKeyWords()}/>
            <bootstrap.Button onClick={this.onLexingClick.bind(this)} 
             style={{marginTop: '16px'}}
             bsStyle="danger">
              Parsing
            </bootstrap.Button>
          </bootstrap.Panel>
          );
    }   
}

一旦用户点击下面红色按钮时,解析器就启动了语法解析过程,解析完后,解析器会返回一个Program类,该类里面包含了解析器把语句解析后所得到的结果,Program类里面的statments数组存储的就是每条语句被语法解析器解析后的结果,我们通过遍历statements数组里面每个元素,由于他们都继承自类Node,因此他们都实现了getLiteral接口,通过这个接口,我们可以把解析器对每条语句的解析结果输出到控制台上。

上面代码完成后,加载页面,在编辑框中输入如下内容:


然后点击下方的红色”Parsing”按钮,开始解析,接着打开控制台,我们就能看到相应的输出结果:


由于语法解析是编译原理中较为抽象难理解的部分,大家一定要根据视频讲解,对代码进行亲自调试,唯有如此,你才能对语法解析有比较深入和直观的了解。

更详细的讲解和代码调试演示过程,请点击链接

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom