前言
在这篇文章中,设计了一套抽象语法树来表示我们的语言,在之后两篇文章中,设计实现了一个虚拟机。
但是虚拟机所执行的是函数原型(类似于解析后的 java 字节码)。所以我们还需要最后临门一脚,由抽象语法树生成函数原型。
整体结构
在抽象语法树里,我们的关注点在于程序逻辑的表达。而在函数原型中,更关心的是虚拟机如何运行。
于是在一步一步解析抽象语法树的过程中,需要考虑指令生成,寄存器分配,局部变量的声明,常量记录。
在实现中,分别将其作为一个模块进行处理。
指令表与常量表
指令表和常量表相对简单,就是把生成的指令,对应的常量记录下来,通过 attach
记录到对应的函数原型中。
// 指令表
export class InstructionList {
#list: number[] = []
add(ins: Instruction) {
this.#list.push(ins)
return this.#list.length - 1
}
attach(proto:Proto){
proto.code = this.#list
}
}
// 常量表
export class ConstantRegister {
#list: LxValue[] = []
regist(val: LxValue) {
const [type, value] = val
const idx = this.#list.findIndex(v =>
v[0] === type && v[1] === value
)
// 判断对应的常量是否以及注册过
if (idx >= 0) {
return idx
} else {
this.#list.push(val)
return this.#list.length - 1
}
}
attach(proto:Proto){
proto.constants = this.#list
return this.#list
}
}
栈分配
正如之前提到的,我们把栈分为两个部分。
我们将这个栈逻辑上拆分为两个部分,前一部分用来实现局部/临时变量的存储,即寄存器。后一部分则实现计算部分。
对于后面计算的部分,由于执行每一条指令的时候,这一部分都会清空。所以只需要记录下需要最大的数量就行了。
export class StackAllocator {
#extra: number = 0
extra(n: number) {
this.#extra = n > this.#extra ? n : this.#extra
}
}
而在前面一部分,因并不知道这些变量何时销毁,即需一个数组记录下栈当前的状态(当前索引的栈是否正在被使用)和申请和释放两个方法。
对于一些指令(call),由于执行的时候,需要保证所有值都是在连续的寄存器中,于是申请时候还需要一个参数 size,返回值则是第一个寄存器的索引。
一方面需要尽量复用原先释放掉的寄存器,一方面又要保证申请到的所有寄存器是连续的。申请方法相对复杂些。
export class StackAllocator {
// ...
#slot: boolean[] = []
alloc(size: number) {
if (size <= 0) throw new Error('alloc : size error!')
let idx = -1
let remains = size
for (let i = 0; i < this.#slot.length; i++) {
if (this.#slot[i] === true) {
idx = -1
remains = size
continue
}
if (idx === -1) {
idx = i
}
remains = remains - 1
if (remains === 0) break
}
idx = idx < 0 ? this.#slot.length : idx
this.#slot = this.#slot.concat(new Array(remains).fill(false))
for (let i = idx; i < idx + size; i++) {
this.#slot[i] = true
}
if (this.#slot.length > 256) {
throw new Error('alloc: stackoverflow!')
}
return idx
}
free(idx: number, size: number = 1) {
for (
let i = idx;
i < idx + size && i < this.#slot.length;
i++
) {
this.#slot[i] = false
}
}
}
最后 attach 方法,将最大栈数量,和其中寄存器数量记录在函数原型中。
export class StackAllocator {
// ...
attach(proto:Proto){
proto.slotSize = this.#slot.length
proto.maxStackSize = this.#slot.length + this.#extra
}
}
局部变量表
当定义一个局部变量时,这个变量是记录到栈上的。于是还需要一个表将变量和栈索引记录下来。
export class LoaclVarTable {
#table = new Map<string, number>()
define(name: string, slot: number) {
if (this.#table.has(name))
throw new Error(`'${name}' has already been declared`)
else
this.#table.set(name, slot)
}
get(name: string) {
const idx = this.#table.get(name)
if (typeof idx != 'number') {
throw new Error(`'${name}' is not defined`)
} else {
return idx
}
}
}
原型生成
抽象一个 ProtoGenerator
类,进行原型生成的工作。其中除了之前已经完成的几个部分外,还需要 protos
字段记录下所有子函数,和 argus
记录下参数列表。
ProtoGenerator
的构造自然需要一个词法树里的 Func
对象。构造函数中如果存在参数,还需要为每一个参数分配一个寄存器。
然后实现 gen
方法,生成对应的函数原型。
export class ProtoGenerator {
#constant = new ConstantRegister()
#stack = new StackAllocator()
#local = new LoaclVarTable()
#instructions = new InstructionList()
#protos: ProtoGenerator[] = []
#argus: Argu[] = []
constructor(fn: Func) {
this.#argus = fn.argu
if(this.#argus.length > 0){
const idx = this.#stack.alloc(this.#argus.length)
this.#argus.forEach((argu,i)=>{
this.#local.define(argu.name,idx + i)
})
}
// todo
}
gen(){
const proto = new Proto()
proto.numParams = this.#argus.length
proto.protos = this.#protos.map(v=>v.gen())
this.#constant.attach(proto)
this.#stack.attach(proto)
this.#instructions.attach(proto)
return proto
}
}
解析语法树
在 ProtoGenerator
的构造过程中,同时进行词法树的解析。
函数 body
是 statement
列表。那么在 ProtoGenerator
上添一个statement
方法专门进行 statement
的解析。
export class ProtoGenerator {
//...
constructor(fn: Func) {
this.#argus = fn.argu
if(this.#argus.length > 0){
const idx = this.#stack.alloc(this.#argus.length)
this.#argus.forEach((argu,i)=>{
this.#local.define(argu.name,idx + i)
})
}
fn.body.forEach(statement => {
this.statement(statement)
});
}
statement(state: Statement){
// todo
}
}
Statement
Statement
是一个虚类,实际上是没有 Statement
的实例。而对于 Statement
的各个子类,同样也分别实现一个方法进行解析。
其中比较特殊的是 Expr
, Expr
会返回一个值,在解析 Expr
之前需要分配一个寄存器用来存这个值,解析完成后释放掉就行。
export class ProtoGenerator {
//...
statement(state: Statement) {
if(state instanceof Expr){
const slot = this.#stack.alloc(1)
this.expr(state,slot)
this.#stack.free(slot)
}else if(state instanceof Return){
this.return(state)
}else if(state instanceof Define){
this.define(state)
}else if(state instanceof Setter){
this.setter(state)
}else{
throw new Error('undfined expr!')
}
}
}
Expr
Expr
也是一个虚类。同之前 Statement
的操作。
export class ProtoGenerator {
//...
expr(expr: Expr, slot: number) {
if(expr instanceof Int){
this.int(expr,slot)
}else if(expr instanceof BinaryIntArith){
this.binaryIntArith(expr,slot)
}else if(expr instanceof Nil){
this.nil(expr,slot)
}else if(expr instanceof Getter){
this.getter(expr,slot)
}else if(expr instanceof Func){
this.func(expr,slot)
}else if(expr instanceof Call){
this.call(expr,slot)
}else{
throw new Error('undfined expr!')
}
}
}
Nil
nil 是最简单的方法了。就是添加一条 loadnil 指令,把 nil 加载到对应寄存器上去就行。
因为 loadnil 会用到额外的一个栈位,还需要调用 extra
方法。
export class ProtoGenerator {
// ...
nil(_: Nil, slot: number) {
this.#stack.extra(1)
this.#instructions.add(ins.create(OpCode.loadNil, slot, 1, 0))
}
}
Int
int 比 nil 多一步,把数值添加到常量表里。然后用 loadk 加载到对应寄存器。
如果常量索引太大,26 个 bit 表示不了。那么调用 loadKX 加载。
export class ProtoGenerator {
// ...
int(int: Int, slot: number) {
this.#stack.extra(1)
const { val } = int
const idx = this.#constant.regist([LxType.int, val])
if (idx < 2 ** 26) {
this.#instructions.add(ins.create(OpCode.loadK, slot, idx))
} else {
this.#instructions.add(ins.create(OpCode.loadKX, slot, 0))
this.#instructions.add(ins.iax(0, idx))
}
}
}
BinaryIntArith
二元操作会有左右两个操作数,为其各分配一个寄存器。分别调用 expr 进行解析。
根据不同的算术符调用不同的指令操作码。
最后释放两个寄存器。
假如操作数直接是 Int 且常量表索引小于 255,还需要进行优化。
export class ProtoGenerator {
// ...
binaryIntArith(binaryArith: BinaryIntArith, slot: number) {
this.#stack.extra(2)
const { arith, a, b } = binaryArith
let op: OpCode = OpCode.iadd
switch (arith) {
case 'add': op = OpCode.iadd; break;
case 'sub': op = OpCode.isub; break;
case 'mul': op = OpCode.imul; break;
case 'div': op = OpCode.idiv; break;
default: throw new Error('error binaryArith')
}
let slotA = 0
if (
a instanceof Int
&& (this.#constant.regist([LxType.int, a.val]) <= 0xff)
) {
slotA = this.#constant.regist([LxType.int, a.val]) + 1 + 0xff
} else {
slotA = this.#stack.alloc(1)
this.expr(a, slotA)
}
let slotB = 0
if (
b instanceof Int
&& (this.#constant.regist([LxType.int, b.val]) <= 0xff)
) {
slotB = this.#constant.regist([LxType.int, b.val]) + 1 + 0xff
} else {
slotB = this.#stack.alloc(1)
this.expr(b, slotB)
}
this.#instructions.add(ins.create(op, slot, slotA, slotB))
if (slotA <= 0xff) { this.#stack.free(slotA) }
if (slotB <= 0xff) { this.#stack.free(slotB) }
}
}
Define
变量注册 — —申请一个寄存器槽位,然后在局部变量表里进行注册。再将默认值解析,写入寄存器内。
export class ProtoGenerator {
// ...
define(def: Define) {
this.#stack.extra(1)
const { name, defVal } = def
const slot = this.#stack.alloc(1)
this.expr(defVal, slot)
this.#local.define(name, slot)
}
}
Setter
根据名字从局部变量表里找到对应寄存器索引。然后调用 expr 将值解析写入寄存器。
export class ProtoGenerator {
// ...
setter(set: Setter) {
this.#stack.extra(1)
const { name, val } = set
const slot = this.#stack.alloc(1)
const target = this.#local.get(name)
this.expr(val, slot)
this.#instructions.add(ins.create(OpCode.move, target, slot, 0))
this.#stack.free(slot)
}
}
Getter
根据名字从局部变量表里找到对应寄存器索引。调用 move 指令将寄存器内的值写入所需的寄存器。
export class ProtoGenerator {
// ...
getter(get: Getter, slot: number) {
this.#stack.extra(1)
const { name } = get
const source = this.#local.get(name)
this.#instructions.add(ins.create(OpCode.move, slot, source, 0))
}
}
Func
根据 Func 对象生成一个 ProtoGenerator
,将其添加进子函数表内,然后生成一条 func 指令,将对应函数原型加载入寄存器。
export class ProtoGenerator {
// ...
func(func: Func, slot: number) {
const proto = new ProtoGenerator(func)
this.#protos.push(proto)
this.#instructions.add(ins.create(OpCode.func, slot, this.#protos.length - 1))
}
}
Call
根据参数数量 n,申请 n + 1 个连续寄存器。分别将所调用的函数和参数写入这些寄存器。后生成 call 指令进行调用。
需要注意,由于虚拟机上的 call 方法会同时消耗 n+1 个栈位。所以需要调用 extra 方法记录下来。
export class ProtoGenerator {
// ...
call(call: Call, slot: number) {
const { fn, inputs } = call
const target = this.#stack.alloc(inputs.length + 1)
this.#stack.extra(inputs.length + 1)
this.expr(fn, target)
inputs.forEach((argu, i) => {
this.expr(argu, target + i + 1)
})
this.#instructions.add(ins.create(OpCode.call, slot, target, inputs.length))
this.#stack.free(target,inputs.length + 1)
}
}
Return
没啥说的,调用 return 方法将对应寄存器的内的值返回就行。
export class ProtoGenerator {
// ...
return(ret: Return) {
const { val } = ret
const slot = this.#stack.alloc(1)
this.expr(val, slot)
this.#instructions.add(ins.create(OpCode.return, slot, 0, 0))
this.#stack.free(slot)
}
}
总结
自此,通过一共四篇文章,整了一个编译器,又整了一个虚拟机。
不过,没有逻辑运算,没有浮点数和字符串,没有循环判断,也没有对象和闭包,只能进行整数的四则混合运算的这个虚拟机和语言着实磕碜。
但不要紧,功能可以不断拓展。下一篇文章即为这个虚拟机添加闭包。