用 typescript 整一个虚拟机(下)

660 阅读6分钟

指令

之前在我们生成函数原型的时候是以 number[] 的形式声明的指令表。

这意味着指令其实就是一个数字。

export type Instructon = number

计算机不就是用来处理一大串 01010 嘛,这个角度看,谁又不是数字呢。

指令根据长度可以分两种,一种是变长,一种是定长。显然前者优势是体积小,后者优势是解析快。在这我们的机器就选择定长的,好写。

参考 lua 的指令集,我们也将指令定位 4 byte 长度,共 32 bit,其中 6 bit 表示操作码,其他 26 bit则表示操作数。

目前我们涉及到的操作码有这么几种。

export enum OpCode {
    // 跳转
    move, jmp,
    // 加载
    loadNil, loadK, loadKX
    // 整数
    iadd, isub, imul, idiv,
    // 函数
    func, call, return
}

而由于不同操作码需要的参数数量和类型都不同,这将操作码分为以下四个类型。

export enum OpMode {
    IABC, IABx, IAsBx, IAx,
}

export type OpModeArgu = {
    [OpMode.IABC]: [number, number, number],
    [OpMode.IABx]: [number, number],
    [OpMode.IAsBx]: [number, number],
    [OpMode.IAx]: [number],
}
  • IABC 表示有三个参数。其中 A 占 8 bit,B、C 分别占 9 bit。

  • IABx 表示有两个参数,其中 A 占 8 bit,Bx 占 18 bit。

  • IAsBx 表示有两个参数,其中 A 占 8 bit,sBx 占 18 bit,不过 sbx 会被解释成有符号整数。

  • IAx 表示有一个参数 —— Ax,占全部 26 bit。

IABC 是最常用的类型,但由于 bit 太小了,9 bit 最大表示 512 ,所以对一些特殊的操作,需要 IABx 这种类型。如果 18 bit 仍不够,可能还需要配合 IAx 指令执行一个操作。还有的操作如跳转,可能向上跳也可能向下跳转,所以需要 IAsBx 提供一个有符号整数。

然后把我们之前声明的操作码,各分配一个类型。

export type OpCodeMode = {
    [OpCode.move]: OpMode.IABC,
    [OpCode.jmp]: OpMode.IAsBx,

    [OpCode.loadNil]: OpMode.IABC,
    [OpCode.loadK]: OpMode.IABx,
    [OpCode.loadKX]: OpMode.IABx,

    [OpCode.iadd]: OpMode.IABC,
    [OpCode.isub]: OpMode.IABC,
    [OpCode.imul]: OpMode.IABC,
    [OpCode.idiv]: OpMode.IABC,
    
    [OpCode.func]: OpMode.IABC,
    [OpCode.call]: OpMode.IABC,
    [OpCode.return]: OpMode.IABC,
}

export const OpCodeMode: OpCodeMode = {
    [OpCode.move]: OpMode.IABC,
    [OpCode.jmp]: OpMode.IAsBx,
    
    [OpCode.loadNil]: OpMode.IABC,
    [OpCode.loadK]: OpMode.IABx,
    [OpCode.loadKX]: OpMode.IABx,

    [OpCode.iadd]: OpMode.IABC,
    [OpCode.isub]: OpMode.IABC,
    [OpCode.imul]: OpMode.IABC,
    [OpCode.idiv]: OpMode.IABC,
    
    [OpCode.func]: OpMode.IABC,
    [OpCode.call]: OpMode.IABC,
    [OpCode.return]: OpMode.IABC,
}

指令方法

根据已经定义好的格式,可以写出方便生成函数,解析的工具函数。

  • 从指令中读取操作码
// 获取指令操作码
const code = (i:Instructon):OpCode => i & 0x3f
  • 根据每一个类型,写一个对应的生成函数
// 根据操作码和参数生成指令
const create = {
    [OpMode.IABC]: (code: OpCode, args: OpModeArgu[OpMode.IABC]): Instructon => {
        const [a, b, c] = args
        const vcode = code & 0x3f
        const va = (a & 0xff) << 6
        const vb = (b & 0x1ff) << 14
        const vc = (c & 0x1ff) << 23
        return vcode + va + vb + vc
    },
    [OpMode.IABx]: (code: OpCode, args: OpModeArgu[OpMode.IABx]): Instructon => {
        const [a, bx] = args
        const vcode = code & 0x3f
        const va = (a & 0xff) << 6
        const vb = bx << 14
        return vcode + va + vb
    },
    [OpMode.IAsBx](code: OpCode, args: OpModeArgu[OpMode.IAsBx]): Instructon {
        const [a, bx] = args
        const vcode = code & 0x3f
        const va = (a & 0x1ff) << 6
        const vb = (bx + MAXARG_sBx) << 14
        return vcode + va + vb
    },
    [OpMode.IAx](code: OpCode, args: OpModeArgu[OpMode.IAx]): Instructon {
        const [a] = args
        const vcode = code & 0x3f
        const va = a << 6
        return vcode + va
    }
}
  • 根据每一个类型,写一个对应的参数解析函数
// 获取指令参数
export const inputs = {
    [OpMode.IABC](ins: Instructon): OpModeArgu[OpMode.IABC] {
        return [
            ins >> 6 & 0xff,
            ins >> 14 & 0x1ff,
            ins >> 23 & 0x1ff
        ]
    },
    [OpMode.IABx](ins: Instructon): OpModeArgu[OpMode.IABx] {
        return [
            ins >> 6 & 0xff,
            ins >> 14,
        ]
    },
    [OpMode.IAsBx](ins: Instructon): OpModeArgu[OpMode.IAsBx] {
        return [
            ins >> 6 & 0xff,
            (ins >> 14) - MAXARG_sBx,
        ]
    },
    [OpMode.IAx](ins: Instructon): OpModeArgu[OpMode.IAx] {
        return [ins >> 6]
    }
}
  • 根据每一个操作码,写一个对应的处理函数。这个函数接受一个指令和栈帧,从指令中读取参数,然后对栈帧进行操作。(具体的内容稍后完善)
const deal:{[key in OpCode]: (i: Instructon, stack: LxCallStack) => void} = {
    // 待完成
}

最后根据以上的工具方法,向往抛出两个函数,分别对应指令的生成和指令的执行。

export const ins = {
    create: <T extends OpCode>(code: T, ...args: GetOpCodeArgu<T>) => {
        const mode: OpMode = OpCodeMode[code]
        return create[mode](code, args as any)
    },
    excute: (i: Instructon, frame: LxCallFrame) => {
        const code: OpCode = i & 0x3f
        return deal[code](i, frame)
    }
}

基础操作( jmp, move )

先处理两个基础的操作码

export type BaseOP = OpCode.move | OpCode.jmp
  • move:将 b 栈索引的上的值拷贝到 a 栈索引上。可以直接调用帧上的 copy 方法就行。

  • jmp : 跳转到当前指令相对位置为 sbx 。可以直接调用帧上的 addPC 方法。

export const dealBase = {
    [OpCode.move]: (i: Instructon, frame: LxCallFrame) => {
        const [a, b, _] = inputs[OpCodeMode[OpCode.move]](i)
        frame.copy(b, a)
    },
    [OpCode.jmp]: (i: Instructon, frame: LxCallFrame) => {
        const [a, sbx] = inputs[OpCodeMode[OpCode.jmp]](i)
        frame.addPC(sbx)
        if (a !== 0) { throw new Error('Op: todo') }
    },
}

整型操作(iadd, isub, imul, idiv)

bc 索引处取出数值,计算出值后写入到 a 索引处。

export type ArithIntOP = OpCode.iadd | OpCode.isub | OpCode.imul | OpCode.idiv

const toInt = (val: LxValue | null) => {
    if (val && val[0] === LxType.int)
        return val
    else
        throw new Error('parse int error')
}

const table = {
    [OpCode.iadd]: (a: LxValue, b: LxValue): [LxType.int, number] => {
        const va = toInt(a)
        const vb = toInt(b)
        return [LxType.int, va[1] + vb[1]]
    },
    [OpCode.isub]: (a: LxValue, b: LxValue): [LxType.int, number] => {
        const va = toInt(a)
        const vb = toInt(b)

        return [LxType.int, va[1] - vb[1]]
    },
    [OpCode.imul]: (a: LxValue, b: LxValue): [LxType.int, number] => {
        const va = toInt(a)
        const vb = toInt(b)

        return [LxType.int, va[1] * vb[1]]
    },
    [OpCode.idiv]: (a: LxValue, b: LxValue): [LxType.int, number] => {
        const va = toInt(a)
        const vb = toInt(b)

        if (!b) throw new Error('state: division by zero')

        return [LxType.int, Math.floor(va[1] / vb[1])]
    },
}

const dealBinaryArith = (op: ArithIntOP) => (i: Instructon, frame: LxCallFrame) => {
    const [a, b, c] = inputs[OpCodeMode[OpCode.move]](i)

    frame.pushValue(b)
    frame.pushValue(c)

    const n = frame.pop()
    const m = frame.pop()

    if (!m || !n) throw new Error('state: val is not exist!')

    const val = table[op](m, n)

    frame.push(val)
    frame.replace(a)
}

export const dealIntArith = {
    [OpCode.iadd]: dealBinaryArith(OpCode.iadd),
    [OpCode.isub]: dealBinaryArith(OpCode.isub),
    [OpCode.imul]: dealBinaryArith(OpCode.imul),
    [OpCode.idiv]: dealBinaryArith(OpCode.idiv),
}

函数操作(func, call, return)

  • func:将原型表上 bx 索引位置的函数原型,生成函数放到 a 栈索引处。

  • call : 从 a 处读取一个函数,并向后继续读取 b 个参数,推入栈顶,结果索引记录在 c 上,调用帧上 call 方法

  • return : 读取 a 上的索引,直接调用帧上 return 方法。

export type FuncOP = OpCode.func | OpCode.call | OpCode.return

export const dealFunc = {
    [OpCode.func]: (i: Instructon, frame: LxCallFrame) => {
        const [a, bx] = inputs[OpCodeMode[OpCode.func]](i)
        frame.loadProto(bx)
        frame.replace(a)
    },
    [OpCode.call]: (i: Instructon, frame: LxCallFrame) => {
        const [a, b, c] = inputs[OpCodeMode[OpCode.call]](i)
        for (let i = a; i <= a + b; i++) {
            frame.pushValue(i)
        }
        frame.call(b, c)
    },
    [OpCode.return]: (i: Instructon, frame: LxCallFrame) => {
        const [a, _] = inputs[OpCodeMode[OpCode.return]](i)
        frame.return(a)
    },
}

读取操作 ( loadNil, loadK, loadKX )

  • loadNil:从 a 处开始的 b 个栈位初始化为 Nil

  • loadK : 从常量表 bx 处读取一个常量,并写入 a

  • loadKX : 取这个指令的 a 为的栈索引, 取下个指令的 ax 为常量索引,进行赋值。

export type LoadOP = OpCode.loadNil | OpCode.loadK | OpCode.loadKX


export const dealLoad = {
    [OpCode.loadNil]: (i: Instructon, frame: LxCallFrame) => {
        const [a, b, _] = inputs[OpCodeMode[OpCode.loadNil]](i)
        frame.push(nil())
        for (let i = a; i < a + b; i++) { frame.copy(-1, i) }
        frame.pop()
    },
    [OpCode.loadK]: (i: Instructon, frame: LxCallFrame) => {
        const [a, bx] = inputs[OpCodeMode[OpCode.loadK]](i)
        frame.getConst(bx)
        frame.replace(a)
    },
    [OpCode.loadKX]: (i: Instructon, frame: LxCallFrame) => {
        const [a, _] = inputs[OpCodeMode[OpCode.loadKX]](i)
        const [ax] = inputs[OpMode.IAx](i)
        frame.getConst(ax)
        frame.replace(a)
    },
}

调整优化

  • 我们可以实现操作码对应的处理函数
const deal:{[key in OpCode]: (i: Instructon, frame: LxCallFrame) => void} = {
    ...dealBase,
    ...dealIntArith,
    ...dealFunc,
    ...dealLoad,
}
  • 由于操作数参数 a 的大小只有 8bit,意味着我们栈索引范围再 0 - 255 之间。而我们用 9bit 的 bc 去栈里找数据的时候,最高位的那个 bit 其实没有用到。这里可以扩展一下,用最高位的那个 bit 去确定是从栈里找,还是去常量表里找。对于整数操作而言,意味着可能省条 loadK 指令。这也是 lua 中的一个性能优化方法。

于是再我们栈帧上添加一个 getRk 方法

export class LxCallFrame extends Stack {
    // ...
    getRk(idx: number) {
        if (idx > 0xff) {
            this.getConst(idx & 0xff)
        } else {
            this.pushValue(idx)
        }
    }
}

再整型算术操作中,用 getRK 代替 pushValue

const dealBinaryArith = (op: ArithIntOP) => (i: Instructon, frame: LxCallFrame) => {
    // ...
    frame.getRK(b)
    frame.getRK(c)
    // ...
}
  • 由于实现了指令执行的函数,所以我们可以为栈帧添加执行指令的的方法了

export class LxCallFrame extends Stack {
    // ...
    // 执行下一条指令
    nextStep() {
        ins.excute(this.fetch(), this)
    }

}

  • 拓展抽象类 LxCallStack 为实体类 LxVM

    • 在构造函数中接受一个主函数原型,生成底部调用帧。(由于帧的初始化需要栈,而在 lxVM 构造的时候,还没有栈,所以还需要调整一下 LxCallStack 的构造函数)

    • nextStep 方法中调用最顶调用帧上的 nextStep

    • terminate 方法结束程序

export abstract class LxCallStack {

    #top: LxCallFrame
    
    constructor(top:(stack:LxCallStack)=> LxCallFrame) {
        this.#top = top(this)
    }
    
    top(){
        return this.#top
    }
    
    push(frame: LxCallFrame) {
        frame.prev = this.#top
        this.#top = frame
    }
    
    pop() {
        const top = this.#top
        if (top.prev) {
            this.#top = top.prev
        } else {
            this.terminate()
        }
        return top
    }
    abstract terminate():void
}


export class LxVM extends LxCallStack {
    
    constructor(main:Proto) {
        super((stack)=>new LxCallFrame(main,stack))
        
    }

    nextStep(){
        this.top().nextStep()
    }

    terminate(){
        console.log('terminate',this)
    }
}

结语

至此,我们的虚拟机已经完成。剩下的也就是最后的临门一脚 —— 将一开始的抽象语法树解析生成对应的函数原型!