ReactJs开发自制Monkey语言编译器:实现内嵌函数调用以及增加数组类型

几乎所有编程语言都会支持内嵌API调用,这些调用会根据操作系统特点,执行相关的系统调用进而实现一系列功能,例如C语言中支持的printf就是内嵌API,它能帮开发者将信息输入到控制台中,本节将为我们的Monkey编程语言提供类似的内嵌函数支持。我们支持的第一个函数是len, 它用于返回字符串,数组和链表的元素长度。

例如下面代码:

len("hello")

它会返回数值5,也就是字符串”hello”的字符个数。我们看看该功能的实现,在MonkeyEvaluator.js中,增加如下代码:

//change 1
    builtins (name, args) {
        //实现内嵌API
        switch (name) {
            case "len":
            if (args.length != 1) {
                return this.newError("Wrong number of arguments when calling len")
            } 
            switch (args[0].type()) {
                case args[0].STRING_OBJ:
                var props = {}
                props.value = args[0].value.length
                var obj = new Integer(props)
                console.log("API len return: ",obj.inspect())
                return obj
            }
        }

        return this.newError("unknown function call")
    }

一旦有函数调用时,解析器会把函数的名称和参数列表传入上面实现的函数,接着它会判断传入函数名是否属于编译器提供的内在支持API,如果对应不上则返回错误,如果对应上的话,它就会根据相应逻辑,解析输入参数,然后返回相应结果。

然后我们在解析器解析执行函数时,调用上面代码:

eval (node) {
        var props = {}
        switch (node.type) {
        ...
        case "CallExpression":
        ....
        var functionCall = this.eval(node.function)
            if (this.isError(functionCall)) {
                // change 0
                return this.builtins(node.function.tokenLiteral, args)
            }
        ...

当解析器执行函数调用时,如果对应的函数名没有在环境变量对应的符号表中找到,那它会调用buildin函数,将函数名传入,看看对应函数是否属于内嵌函数,如果是,那么就直执行内嵌函数的逻辑,并把结果返回。

完成上面代码后,我们在编辑框中输入代码”len(“hello”);”,然后得到结果如下:

从运行结果可以看出,我们的解析器能够正确的执行内嵌函数len,准确的返回了字符串的字符长度。

接下来,我们将为Monkey语言增加数组支持。完成接下来的代码后,我们的编译器能解析如下代码:

let arr = [1, 2*2, 3+3];

我们首先要做的是增加对数组的词法解析,于是在MonkeyLexer.js中增加如下代码:

initTokenType() {
....
//change 3
  this.LEFT_BRACKET = 26
  this.RIGHT_BRACKET = 27
}
getLiteralByTokenType(type) {
        switch (type) {
        ....
         //change 4
            case this.LEFT_BRACKET:
              return "["
            case this.RIGHT_BRACKET:
              return "]" 
            default:
              return "unknow token"
        ....
}
nextToken () {
        var tok
        this.skipWhiteSpaceAndNewLine() 
        var lineCount = this.lineCount
        var needReadChar = true;
        this.position = this.readPosition

        switch (this.ch) {
        ....
        // change 5
        case '[':
          tok = new Token(this.LEFT_BRACKET, "[", lineCount)
          break
        case ']':
          tok = new Token(this.RIGHT_BRACKET, "]", lineCount)
          break
        ....
        }
        ....
}

我们首先给词法解析器增加了两个token对应中括号两个字符’[‘和’]’,一旦词法解析器读取到这两个字符时,返回相应的token对象。接着我们要增加对数组类型的语法解析,于是在MonkeyCompilerParser.js中添加如下代码:

//change 6
class ArrayLiteral extends Expression {
  constructor(props) {
    super(props)
    this.token = props.token 
    //elements 是Expression 对象列表
    this.elements = props.elements
    this.type = "ArrayLiteral"
  }

  getLiteral() {
     var str = ""
    for (var i = 0; i < this.elements.length; i++) {
      str += this.elements[i].getLiteral()
      if (i < this.elements.length - 1) {
        str += ","
      }
    }

    this.tokenLiteral = str
    return this.tokenLiteral
  }

}

ArrayLiteral用来对应一个语法树节点,它里面的elements对象用来对应数组中的每个元素。接着我们在语法解析表中注册一个关于数组的解析函数:

class MonkeyCompilerParser {
    constructor(lexer) {
    ....
    //change 7
    this.prefixParseFns[this.lexer.LEFT_BRACKET] = 
    this.parseArrayLiteral
    ....
    }

一旦语法解析器读取到左括号对象时,它里面调用parseArrayLiteral来解析接下来的内容,我们看看解析函数的实现:

//change 8
    parseArrayLiteral(caller) {
      var props = {}
      props.token = caller.curToken
      props.elements = caller.parseExpressionList(caller.lexer.RIGHT_BRACKET)
      var obj = new ArrayLiteral(props)
      console.log("parsing array result: ", obj.getLiteral())
      return obj
    }

    // change 9
    parseExpressionList(end) {
      var list = []
      if (this.peekTokenIs(end)) {
        this.nextToken()
        return list 
      }

      this.nextToken()
      list.push(this.parseExpression(this.LOWEST))
      while (this.peekTokenIs(this.lexer.COMMA)) {
        this.nextToken() 
        this.nextToken() //越过,
        list.push(this.parseExpression(this.LOWEST))
      } 

      if (!this.expectPeek(end)) {
        return null
      }

      return list
    }

parseArrayLiteral继续调用函数parseExpressionList来解析数组括号里面的内容,它的解析逻辑跟我们实现函数执行时,解析输入参数的逻辑是一模一样的,数组的每一个元素都是一个表达式对象,他们之间用逗号隔开,代码调用parseExpression解析数组元素,然后越过逗号,如果没有遇到’]’,那表示解析还未结束,继续调用parseExpression来解析后面的数组元素,直到遇到’]’为止。

当所有参数解析完毕后,parseArrayLiteral会构造一个ArrayLiteral对象返回。上面代码完成后,我们在编辑框中输入如下代码:

点击parsing按钮后,所得结果如下:

编译器把数组中元素对应的内容都打印了出来。接下来我们要实现的是访问数组中给定元素。例如代码:

arr[0];
arr[1];
arr[2];
[1,2,3,4][2]

上面代码会把数组中对应下标的元素给返回。Monkey语言支持更复杂的数组元素访问,例如最后一行,在定义了四个元素的数组后,直接访问第3个元素。上面的语法都具有同一种结构,那就是:

<expression>[<expression>]

由此我们专门为其定义一个语法节点,在MonkeyCompilerParser.js中添加如下代码:

// change 10
class IndexExpression extends Expression {
  constructor(props) {
    super(props)
    this.token = props.token 
    //left 也就是[前面的表达式,它可以是变量名,数组,函数调用
    this.left = props.left 
    //index可以是数字常量,变量,函数调用
    this.index = props.index 
    this.tokenLiteral = "(["+this.left.getLiteral() + "]["
      + this.index.getLiteral() + "])"
    this.type = "IndexExpression"
  }
}

接下来我们将把型如”arr[0]”当做一个中序表达式来对待,因此我们在中序解析表中添加对应的解析函数:

registerInfixMap() {
....
// change 13 数组取值具备最高优先级
        this.INDEX = 7
 ....
 // change 11
  this.infixParseFns[this.lexer.LEFT_BRACKET] = 
  this.parseIndexExpression
....
}

// change 12
parseIndexExpression(caller , left) {
      var props = {}
      props.token = caller.curToken
      props.left = left 
      caller.nextToken()
      props.index = caller.parseExpression(caller.LOWEST)
      if (!caller.expectPeek(caller.lexer.RIGHT_BRACKET)) {
        return null 
      }

      var obj = new IndexExpression(props)
      console.log("array indexing:", obj.getLiteral())
      return new IndexExpression(props)
    }
initPrecedencesMap() {
....
 //change 14
  this.precedencesMap[this.lexer.LEFT_BRACKET] = this.INDEX
}

我们先在中序解析表中注册对应的解析函数,解析器会把”[“左边的表达式先解析出来,然后解析”[“后面的表达式,注意到数组取元素操作是所有运算中优先级最高的,所以在设定运算符优先级时,我们把”[“的优先级设置为最高。

上面代码完成后,在编辑框中输入如下代码:

[1,2,3,4][2];

点击parsing按钮后,得到的解析结果如下:

接下来我们看看,如何解析执行数组的访问。在MonkeyEvaluator.js中添加如下代码:

class Array extends BaseObject {
    constructor(props) {
        super(props)
        this.elements = props.elements
    }

    type() {
        return this.ARRAY_OBJ
    }

    inspect() {
        var s = "["
        for (var i = 0; i < this.elements.length; i++) {
            s += this.elements[i].inspect()
            s += ","
        }

        s += "]"
        return s 
    }
}

执行器会把语法节点ArrayLiteral转换成上面的Array符号对象,相应的解析执行代码如下:

eval (node) {
        var props = {}
        switch (node.type) {
        ....
         //change 16
            case "ArrayLiteral":
              var elements = this.evalExpressions(node.elements)
              if (elements.length === 1 && this.isError(elements[0])) {
                  return elements[0]
              }
              var props = {}
              props.elements = elements 
              return new Array(props)
            //change 17
            case "IndexExpression":
              var left = this.eval(node.left)
              if (this.isError(left)) {
                  return left 
              }
              var index = this.eval(node.index)
              if (this.isError(index)) {
                  return index 
              }
              var obj = this.evalIndexExpression(left, index)
              if (obj != null) {
                  console.log("the "+index.value+"th element of array is: " 
                      + obj.inspect())
              }
              return  obj
        ....

当解析器解读到语句”arr[0]”时,就会进入上面代码的IndexExpression分支,它会先解析”[“左边的部分,左边部分不一定就是数组变量名,有可能是一个返回数组对象的函数调用,所以需要先执行它,以确保得到数组对象,然后将index转换为一个整形值,最后调用evalIndexExpression来获得对应下标元素,该函数的实现为:

//change 18
    evalIndexExpression(left, index) {
        if (left.type() === left.ARRAY_OBJ && 
            index.type() === index.INTEGER_OBJ) {
            return this.evalArrayIndexExpression(left, index)
        }
    }
    //change 19
    evalArrayIndexExpression(array, index) {
        var idx = index.value 
        var max = array.elements.length - 1
        if (idx < 0 || idx > max) {
            return null 
        }

        return array.elements[idx]
    }

上面代码实现后,我们把如下代码输入编辑框:

let s = fn () {return [1,2,3,4];};
s()[1];

然后点击parsing按钮,然后我们得到如下结果:

从运行结果看,我们的编译器成功取得了函数返回数组中的第1个元素。最后我们再添加len函数对数组的支持,代码修改如下:

builtins (name, args) {
    ....
    // change 20
    case args[0].ARRAY_OBJ:
    var props = {}
    props.value = args[0].elements.length
    console.log("len of array " + args[0].inspect() + " is " +
                    props.value)
    return new Integer(props)
   ...
}

上面代码添加后,在编辑框中输入如下代码:

let s = fn() {return [1,2,3,4];};
len(s());

运行后所得结果如下:

从上图执行结果看到,编译器执行函数s后返回了数组,然后执行len函数,并成功的获得了数组的长度。

至此,添加内嵌API和为语言增加数组数据结构的内容就全部完成了。

原文发布于微信公众号 - Coding迪斯尼(gh_c9f933e7765d)

原文发表时间:2018-06-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Golang语言之异常处理

在编写Go语言代码的时候,我们应该习惯使用error类型值来表明非正常的状态。作为惯用法,在Go语言标准库代码包中的很多函数和方法也会以返回error类型值来表...

362130
来自专栏H2Cloud

智能指针shared_ptr【无锁设计基于GCC】

1. shared_ptr 介绍   使用过Boost的话对shared_ptr一定有很深的印象。多个shared_ptr指向同一个对象,每个shared_pt...

44330
来自专栏申龙斌的程序人生

零基础学编程006:赋值语句

继续上次的一道练习题: 如何用Python打印这篇看上去很枯燥的《复利数据表》: (1+0.01) ^ 1 = 1.01 (1+0.01) ^ 2 = 1.0...

30250
来自专栏nummy

单例模式

如果想使得某个类从始至终最多只有一个实例,使用new方法会很简单。Python中类是通过new来创建实例的:

10220
来自专栏我的博客

Mysql数据类型以及字段属性大盘点

1、  时间和日期 l  Date:存储日期信息,标准形式YYYY-MM-DD,但是形如20120808以及2012*08*08或者2012!08!08。也就是...

29280
来自专栏salesforce零基础学习

salesforce 零基础学习(十六)Validation Rules & Date/time

上一篇介绍的内容为Formula,其中的Date/time部分未指出,此篇主要介绍Date/time部分以及Validation rules。 本篇参考PDF:...

21770
来自专栏程序员互动联盟

【编程基础】如何了解c语言中的位运算?

计算机的各种运算最小单位是字节,但是有时候只对某个位(bit)感兴趣,C语言提供了一些列位运算符来完成这个任务。这些操作非常重要,尤其是在嵌入式开发中会常常用到...

47450
来自专栏葡萄城控件技术团队

检测字节流是否是UTF8编码

几天前偶尔看到有人发帖子问“如何自动识别判断url中的中文参数是GB2312还是Utf-8编码” 也拜读了wcwtitxu使用巨牛的正则表达式检测UTF8编码的...

29880
来自专栏Titan框架

Titan Framework MongoDB深入理解3

在前两篇文章中,我们介绍了操作Mongo数据库的类型Curd和Finder,下面要理解的是框架内mongoDB操作的条件类型——MongoDBQueryCond...

14300
来自专栏闻道于事

Java之StringBuffer,StringBuilder,Math,Date,SimpleDateFormat,UUID,File

java.lang  类 StringBuffer java.lang.Object java.lang.StringBuffer 所有已实现的接口:...

40460

扫码关注云+社区

领取腾讯云代金券