代码生成 —— 由抽象语法树到函数原型

667 阅读5分钟

前言

这篇文章中,设计了一套抽象语法树来表示我们的语言,在之后两篇文章中,设计实现了一个虚拟机。

但是虚拟机所执行的是函数原型(类似于解析后的 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 的构造过程中,同时进行词法树的解析。

函数 bodystatement 列表。那么在 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)
    }
}

总结

自此,通过一共四篇文章,整了一个编译器,又整了一个虚拟机。

不过,没有逻辑运算,没有浮点数和字符串,没有循环判断,也没有对象和闭包,只能进行整数的四则混合运算的这个虚拟机和语言着实磕碜。

但不要紧,功能可以不断拓展。下一篇文章即为这个虚拟机添加闭包。