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"按钮,开始解析,接着打开控制台,我们就能看到相应的输出结果:

这里写图片描述

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

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

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


这里写图片描述
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容