最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 用 typescript 整一个虚拟机(下)

    正文概述 掘金(坂有桑)   2021-06-26   747

    指令

    之前在我们生成函数原型的时候是以 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 个参数,推入栈顶,调用帧上 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, _] = inputs[OpCodeMode[OpCode.call]](i)
            for (let i = a; i <= a + b; i++) {
                frame.pushValue(i)
            }
            frame.call(b + 1)
        },
        [OpCode.return]: (i: Instructon, frame: LxCallFrame) => {
            const [a, _] = inputs[OpCodeMode[OpCode.return]](i)
            frame.return(a)
        },
    }
    
    

    读取操作 ( loadNil, loadK, loadKX )

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

    结语

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


    起源地下载网 » 用 typescript 整一个虚拟机(下)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元