语句求值3

8 min read
编译原理解释器sdk封装

字符串与数组

在字符串和数组中封装一些方法例如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}了吧。

image

上面修改之后,我们可以来试一下:

image

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函数这里引入了一个nodejsdeasync这个库有同步的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);

image

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);

测试: image

代码整理

上面所有求值和sdk的封装都在eval_v6.mjs文件中了,因为类型声明代码比较多,所以显得整个文件比较冗长,有1k行左右了。我们可以将代码重新整理,将基础的模型类拆分到单独的文件进行精简。同时将lex parse eval sdk的代码拆分到单独的文件,这样便于阅读和修改。最后整理的内容放到了一个新的github仓库中。https://github.com/sunwu51/mocha.

最后因为觉得同步的http和sleep接口还是有点膈应,又把代码改成async + await了。详细可以看上面的repo代码。