指令
之前在我们生成函数原型的时候是以 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)
从 b
、c
索引处取出数值,计算出值后写入到 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 的b
、c
去栈里找数据的时候,最高位的那个 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)
}
}
结语
至此,我们的虚拟机已经完成。剩下的也就是最后的临门一脚 —— 将一开始的抽象语法树解析生成对应的函数原型!
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!