几乎所有编程语言都会支持内嵌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");",然后得到结果如下:

屏幕快照 2018-06-25 上午10.33.20.png
从运行结果可以看出,我们的解析器能够正确的执行内嵌函数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 6class 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对象返回。上面代码完成后,我们在编辑框中输入如下代码:

屏幕快照 2018-06-25 上午10.50.15.png
点击parsing按钮后,所得结果如下:
编译器把数组中元素对应的内容

屏幕快照 2018-06-25 上午10.51.01.png
都打印了出来。接下来我们要实现的是访问数组中给定元素。例如代码:
arr[0];arr[1];arr[2];[1,2,3,4][2]
上面代码会把数组中对应下标的元素给返回。Monkey语言支持更复杂的数组元素访问,例如最后一行,在定义了四个元素的数组后,直接访问第3个元素。上面的语法都具有同一种结构,那就是:
<expression>[<expression>]
由此我们专门为其定义一个语法节点,在MonkeyCompilerParser.js中添加如下代码:
// change 10class 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 12parseIndexExpression(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按钮后,得到的解析结果如下:

屏幕快照 2018-06-25 上午11.45.39.png
接下来我们看看,如何解析执行数组的访问。在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按钮,然后我们得到如下结果:
从运行结果

屏幕快照 2018-06-26 上午8.39.53.png
看,我们的编译器成功取得了函数返回数组中的第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());运行后所得结果如下:

屏幕快照 2018-06-26 上午8.46.31.png
从上图执行结果看到,编译器执行函数s后返回了数组,然后执行len函数,并成功的获得了数组的长度。
至此,添加内嵌API和为语言增加数组数据结构的内容就全部完成了。
作者:望月从良
链接:https://www.jianshu.com/p/a3e6aaa15212
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
几乎所有编程语言都会支持内嵌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");",然后得到结果如下:

屏幕快照 2018-06-25 上午10.33.20.png
从运行结果可以看出,我们的解析器能够正确的执行内嵌函数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 6class 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对象返回。上面代码完成后,我们在编辑框中输入如下代码:

屏幕快照 2018-06-25 上午10.50.15.png
点击parsing按钮后,所得结果如下:
编译器把数组中元素对应的内容

屏幕快照 2018-06-25 上午10.51.01.png
都打印了出来。接下来我们要实现的是访问数组中给定元素。例如代码:
arr[0];arr[1];arr[2];[1,2,3,4][2]
上面代码会把数组中对应下标的元素给返回。Monkey语言支持更复杂的数组元素访问,例如最后一行,在定义了四个元素的数组后,直接访问第3个元素。上面的语法都具有同一种结构,那就是:
<expression>[<expression>]
由此我们专门为其定义一个语法节点,在MonkeyCompilerParser.js中添加如下代码:
// change 10class 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 12parseIndexExpression(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按钮后,得到的解析结果如下:

屏幕快照 2018-06-25 上午11.45.39.png
接下来我们看看,如何解析执行数组的访问。在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按钮,然后我们得到如下结果:
从运行结果

屏幕快照 2018-06-26 上午8.39.53.png
看,我们的编译器成功取得了函数返回数组中的第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());运行后所得结果如下:

屏幕快照 2018-06-26 上午8.46.31.png
从上图执行结果看到,编译器执行函数s后返回了数组,然后执行len函数,并成功的获得了数组的长度。
至此,添加内嵌API和为语言增加数组数据结构的内容就全部完成了。
作者:望月从良
链接:https://www.jianshu.com/p/a3e6aaa15212
随时随地看视频