Seven: Abstracting Simple Virtual Machines

Git repo

npm install @bctnry/seven

(NOTE: The title might suggest it's something very advanced & very state-of-the-art. It's not. It's a very simple library based on a very simple idea and designed for a very specific need.)

应该是在2019年4月份,我被要求为所在公司重新设计他们原有的控制某个系统的DSL。我把新的设计暂时称作Sketch「草图」。Sketch有过6个不同的版本,只有最后一个版本包含一个完整的他们原有系统的子集,也只有这最后一个版本有具体的实现。当时因为某些原因,除了一次demo之外,Sketch6的系统设计并没有得到任何其他的应用;Seven真正的名称是Sketch7,是2019年4月的Sketch6的下一个版本。

Seven基于一个非常简单的想法。假设我们有一个简单的stack vm:

class VM {
    private PC: number = 0;
    private stack: number[] = [];
    private program: Instr[] = [];

    constructor() {}
    
    load(program: Instr[]) { this.program = program; }

    run() {
        while (this.program[this.PC]) {
            let instr = this.program[this.PC];
            switch (instr.type) {
                case ADD: { /* ... */ break; }
                case SUB: { /* ... */ break; }
                case MUL: { /* ... */ break; }
                case DIV: { /* ... */ break; }
                // ...
            }
        }
    }
}

我们可以将对于指令的dispatch实现成table-driven:

    class VM {
        private PC: number = 0;
        private stack: number[] = [];
        private program: Instr[] = [];

        private instrTable: {[type: string]: (m: VM, i: Instr) => any} = {
            ADD: (m, i) => {
                // ...
            },
            SUB: (m, i) => {
                // ...
            },
            // ...
        };
    
        constructor() {}
        
        load(program: Instr[]) { this.program = program; }
    
        run() {
            while (this.program[this.PC]) {
                let instr = this.program[this.PC];
                this.instrTable[instr.type](this, instr);
            }
        }
    }
    

然后将这个table抽象出去:

    class VM {
        private PC: number = 0;
        private stack: number[] = [];
        private program: Instr[] = [];

        private instrTable: {[type: string]: (m: VM, i: Instr) => any} = {}
    
        constructor() {}
        
        load(program: Instr[]) { this.program = program; }
    
        run() {
            while (this.program[this.PC]) {
                let instr = this.program[this.PC];
                this.instrTable[instr.type](this, instr);
            }
        }

        registerInstr(instrType: string, instrHandler: (m: VM, i: Instr) => any) {
            this.instrTable[instrType] = instrHandler;
        }
    }

let myStackVM = new VM();
myStackVM.registerInstr('ADD', (m, i) => { /* ... */ });
myStackVM.registerInstr('SUB', (m, i) => { /* ... */ });
myStackVM.registerInstr('MUL', (m, i) => { /* ... */ });
myStackVM.registerInstr('DIV', (m, i) => { /* ... */ });
// ...

我们可以将这个虚拟机扩充得能够简单描绘绝大多数虚拟机的构造;这个虚拟机将可以接受任意的指令,甚至可以使用指令实时为机器添加新的指令:

myStackVM.registerInstr('HAVOC', (m, i) => {
    m.registerInstr('HAVOC2', (m, i) => {
        // ...
    });
});

Seven的第一个可用版本实际上完成于2020年8月。我因为做到一半不做的side project实在太多,在2021年4月重新将Seven实现了一遍之后才想起来之前已经完成过一次。这个最开始的seven现在在这里可以看到。这个是完全按照上述的想法设计的,连最基础的控制流都需要用户提供实现;目前的seven则是向Sketch 6回归,内置了基本的控制流。

Seven是以「使用者只有vanilla js可用」为前提设计的,所以Seven的虚拟机自带一个Observable store;那是用来作为model控制view用的。预想的用法是,用户将数据转换为seven machine code,这些machine code会调用事先注册的component,这些component会操作store,而store则跟view挂钩;所以在跟别的有自带store的前端框架(e.g. sveltejs)一起用的时候会感觉有点奇怪。因此要是非要跟vue之类的东西一起用,最好是使用框架自己的store,假装这个原本预期的使用方法不存在。要不然要维护两套不同的store还要保证两者之间的同步,很讨厌的。


2021.7.31
Back