字符串与数组
在字符串和数组中封装一些方法例如length()
,实现方式非常简单调用原生js
对象的length
属性即可,但是我们的语言中"a".length()
是访问的StringElement#get("length")
获取出FunctionElement
然后运行,所以为了能将js的函数和我们语言的函数连接起来,我们声明一种新的Element
叫做NativeFunctionElement
:
export class NativeFunctionElement extends FunctionElement {
constructor(jsFunction, params) {
// body和ctx都不需要
super(params, null, null);
this.jsFunction = jsFunction;
}
// args : NumberElement / BooleanElement / StringElement / NullElement
call(name, args, _this, _super, ctx) {
try {
// 直接把参数转换成js对象,然后调用jsFunction
var nativeArgs = args.map(e => e.toNative());
// 注意这里的_this还是原Element,没有转换成js对象。因为像array的push操作需要修改的是_this的
var res = this.jsFunction.apply(_this, nativeArgs);
// 返回值也需要是element,道理与_this一样,转换会导致引用类型变化
return res ? res : nil;
} catch (e) {
throw new RuntimeError("Error calling native method " + name + ":" + e.message);
}
}
}
然后就可以声明一系列NativeFunction
了,只要注意入参是js类型,返回值和this是element类型即可。
// 数组类型的this是ArrayElement,this.array指向内部的Element数组,对这个数组操作即可。
const arrayProto = new ProtoElement();
arrayProto.setPro("at", new NativeFunctionElement(function(index){ return this.array[index]; }));
arrayProto.setPro("length", new NativeFunctionElement(function(){ return new NumberElement(this.array.length); }));
arrayProto.setPro("push", new NativeFunctionElement(function(item){ this.array.push(jsObjectToElement(item)); }));
arrayProto.setPro("pop", new NativeFunctionElement(function(){ return this.array.pop(); }));
arrayProto.setPro("shift", new NativeFunctionElement(function(){ return this.array.shift(); }));
arrayProto.setPro("unshift", new NativeFunctionElement(function(item){ this.array.unshift(jsObjectToElement(item)); }));
arrayProto.setPro("join", new NativeFunctionElement(function(str){ return new StringElement(this.array.map(item=>item.toString()).join(str)); }));
// 字符串的this是StringElement,this.value是原生js字符串,注意所有函数都不会修改这个value,而是返回新的StringElement
const stringProto = new ProtoElement();
stringProto.setPro("length", new NativeFunctionElement(function(c){ return new NumberElement(this.value.length);}));
stringProto.setPro("split", new NativeFunctionElement(function(c){ return new ArrayElement(this.value.split(c).map(item => new StringElement(item)));}));
stringProto.setPro("charAt", new NativeFunctionElement(function(index){ return new StringElement(this.value[index]) }));
stringProto.setPro("indexOf", new NativeFunctionElement(function(str){ return new NumberElement(this.value.indexOf(str)) }));
stringProto.setPro("startsWith", new NativeFunctionElement(function(str){ return this.value.startsWith(str) ? trueElement :falseElement }));
stringProto.setPro("endsWith", new NativeFunctionElement(function(str){ return this.value.endsWith(str) ? trueElement :falseElement }));
stringProto.setPro("replaceAll", new NativeFunctionElement(function(src, des){ return new StringElement(this.value.replaceAll(src, des)) }));
stringProto.setPro("substring", new NativeFunctionElement(function(start, end){ return new StringElement(this.value.substring(start, end)) }));
stringProto.setPro("toUpperCase", new NativeFunctionElement(function(){ return new StringElement(this.value.toUpperCase()) }));
stringProto.setPro("toLowerCase", new NativeFunctionElement(function(){ return new StringElement(this.value.toLowerCase()) }));
stringProto.setPro("trim", new NativeFunctionElement(function(){ return new StringElement(this.value.trim()) }));
stringProto.setPro("trimLeft", new NativeFunctionElement(function(){ return new StringElement(this.value.trimLeft()) }));
stringProto.setPro("trimRight", new NativeFunctionElement(function(){ return new StringElement(this.value.trimRight()) }));
stringProto.setPro("toNumber", new NativeFunctionElement(function(){ return isNaN(this.value) ? new NumberElement(NaN) : new NumberElement(parseFloat(this.value)) }));
然后将stringProto
赋值给StringElement
的原型,arrayProto
赋值给ArrayElement
的原型:
export class ArrayElement extends Element {
// array: Element[]
constructor(array) {
super('array');
this.array = array;
this.$$pro$$ = arrayProto;
}
//....
}
export class StringElement extends Element {
constructor(value) {
super('string');
this.value = value;
this.$$pro$$ = stringProto;
}
//....
}
看到这里你能理解为什么在js自己运行显示一些属性的时候,会展示f(){native code}
了吧。
上面修改之后,我们可以来试一下:
Math、Time与Json库
我们想要提供一个简单的Math
库,以支持Math.random()
Math.floor(1.1)
等函数的能力,因为我们语言中一切都是Element
所以需要封装一个含有random
floor
等方法的Element
对象,并把它放到入口函数的Context
中,变量名设置为Math
即可,因为后续还要准备其他库,所以我们先把这些element都放到一个map中,如下:
export const buildIn = new Map();
// Math库
const math = new ProtoElement('Math');
math.set('random', new NativeFunctionElement(function(max) {
if (max === undefined) max = 1;
return new NumberElement(Math.random() * max);
}));
math.set('floor', new NativeFunctionElement(function(num) {
return new NumberElement(Math.floor(num));
}));
math.set('ceil', new NativeFunctionElement(function(num) {
return new NumberElement(Math.ceil(num));
}));
math.set('abs', new NativeFunctionElement(function(num) {
return new NumberElement(Math.abs(num));
}));
buildIn.set("Math", math);
Time
库想要封装2个函数,一个是当前时间Time.now()
返回当前时间戳,第二个是Time.sleep(ms)
,当然如果还想封装别的函数可自行扩展,这里会遇到一个js
才会遇到的问题,就是我们的语言是单线程同步的,js是异步的,如果要在我们的库中引入async、await
将大大增加复杂度,所以为了实现sleep
函数这里引入了一个nodejs
库deasync
这个库有同步的sleep
方法。
// Time库
const time = new ProtoElement("Time");
time.set('now', new NativeFunctionElement(function() { return new NumberElement(new Date().getTime());}));
time.set('sleep', new NativeFunctionElement(function(ms) { require('deasync').sleep(100); }));
Json
库则是为了实现json字符串与Element
的转换,因为我们有jsObjectToElement
这个函数了所以只需要借助js中内置的JSON
库就可以简单封装了。
const json = new ProtoElement("JSON");
json.set("stringify", new NativeFunctionElement(function(obj, opt1, opt2) {
return new StringElement(JSON.stringify(obj, opt1, opt2));
}));
json.set("parse", new NativeFunctionElement(function(str) {
return jsObjectToElement(JSON.parse(str))
}));
buildIn.set("JSON", json);
File与Http
IO
相关的两个常用的能力,这里文件我们使用nodejs内置的fs
模块封装,http
则是使用了一个同步的库sync-request
来实现,需要用npm install sync-request
安装,并且为了简单,我们没有字节流的操作,所有的io仅支持String
类型,代码如下。
// File库
const file = new ProtoElement("File");
file.set("readFile", new NativeFunctionElement(function(filename, charset) {
try {
if (!charset) charset = 'UTF-8';
return new StringElement(fs.readFileSync(filename, charset));
} catch (e) {
throw new RuntimeError(e.message)
}
}));
file.set("writeFile", new NativeFunctionElement(function(filename, content) {
try {
fs.writeFileSync(filename, content);
} catch (e) {
throw new RuntimeError(e.message);
}
}));
file.set("appendFile", new NativeFunctionElement(function(filename, content) {
try {
fs.appendFileSync(filename, content);
} catch (e) {
throw new RuntimeError(e.message);
}
}));
buildIn.set('File', file);
// http
const http = new ProtoElement('Http')
http.set("request", new NativeFunctionElement(function(method, url, options){
try {
var res = request(method, url, options);
var body = res.getBody().toString();
var status = res.statusCode;
return jsObjectToElement({body, status});
} catch(e) {
throw new RuntimeError("http request error " + e.message);
}
}))
buildIn.set("Http", http);
测试:
代码整理
上面所有求值和sdk
的封装都在eval_v6.mjs
文件中了,因为类型声明代码比较多,所以显得整个文件比较冗长,有1k行左右了。我们可以将代码重新整理,将基础的模型类拆分到单独的文件进行精简。同时将lex
parse
eval
sdk
的代码拆分到单独的文件,这样便于阅读和修改。最后整理的内容放到了一个新的github
仓库中。https://github.com/sunwu51/mocha.
最后因为觉得同步的http和sleep接口还是有点膈应,又把代码改成async
+ await
了。详细可以看上面的repo代码。