Author | SHA1 | Message | Date |
---|---|---|---|
Forest Belton | d7060781ae | Reorganize codebase and implement liveness check | 3 years ago |
Forest Belton | b7b5514247 | Add basic unit tests | 3 years ago |
Forest Belton | f2d54fde09 | Improve IR format | 3 years ago |
@ -1,240 +0,0 @@ | |||
import type { SSA, SSABinary, SSACopy, SSAUnary } from "./ir" | |||
const reg8 = ["B", "C", "D", "E"] as const | |||
type R8 = typeof reg8[number] | |||
class ASMState { | |||
regs: { [r in R8]: string | null } | |||
syms: { [s: string]: R8 } | |||
stack_offsets: { [s: string]: number } | |||
cur_offset: number | |||
insns: Array<string> | |||
constructor() { | |||
this.regs = { "B": null, "C": null, "D": null, "E": null } | |||
this.syms = {} | |||
this.stack_offsets = {} | |||
this.cur_offset = 0 | |||
this.insns = [] | |||
} | |||
// TODO: Generalize for u16 | |||
get_sym_loc(sym: string): R8 | number | null { | |||
// return register if already allocated | |||
if (typeof this.syms[sym] !== "undefined") { | |||
return this.syms[sym] | |||
} | |||
// return stack offset if already allocated | |||
if (typeof this.stack_offsets[sym] !== "undefined") { | |||
return this.cur_offset - this.stack_offsets[sym] | |||
} | |||
return null | |||
} | |||
alloc_sym_loc(sym: string): R8 | number { | |||
const existing_loc = this.get_sym_loc(sym) | |||
if (existing_loc !== null) { | |||
return existing_loc | |||
} | |||
// look for free register | |||
for (const r8 of reg8) { | |||
if (this.regs[r8] === null) { | |||
this.regs[r8] = sym | |||
this.syms[sym] = r8 | |||
return r8 | |||
} | |||
} | |||
// allocate space on stack | |||
this.cur_offset++ | |||
this.stack_offsets[sym] = this.cur_offset | |||
this.insns.push("ADD SP, -1") | |||
return this.stack_offsets[sym] | |||
} | |||
} | |||
export const convertASM = (ir: Array<SSA>): Array<string> => { | |||
const state = new ASMState() | |||
ir.forEach(ssa => convertASM_SSA(state, ssa)) | |||
for (let sym of Object.keys(state.syms)) { | |||
if (sym.startsWith("temp")) { | |||
continue | |||
} | |||
state.insns.push( | |||
`LD A, ${state.syms[sym]}`, | |||
`LD (${sym}), A` | |||
) | |||
} | |||
for (let sym of Object.keys(state.stack_offsets)) { | |||
if (sym.startsWith("temp")) { | |||
continue | |||
} | |||
state.insns.push( | |||
`LD HL, SP + (${state.cur_offset - state.stack_offsets[sym]})`, | |||
`LD A, (HL)`, | |||
`LD (${sym}), A` | |||
) | |||
} | |||
if (state.cur_offset !== 0) { | |||
state.insns.push(`ADD SP, ${state.cur_offset}`) | |||
} | |||
return state.insns | |||
} | |||
// TODO: Track liveness | |||
const convertASM_SSA = (state: ASMState, ssa: SSA): void => { | |||
if ("source1" in ssa) { // Binary | |||
convertASM_SSA_Binary(state, ssa) | |||
} else if ("op" in ssa) { // Unary | |||
convertASM_SSA_Unary(state, ssa) | |||
} else { // Copy | |||
convertASM_SSA_Copy(state, ssa) | |||
} | |||
} | |||
export const convertASM_SSA_Binary = (state: ASMState, ssa: SSABinary): void => { | |||
const dest = state.alloc_sym_loc(ssa.dest) | |||
if (typeof ssa.source === "number") { | |||
state.insns.push(`LD A, ${ssa.source}`) | |||
} else { | |||
const loc = state.get_sym_loc(ssa.source) | |||
if (loc === null) { | |||
state.insns.push(`LD A, (${ssa.source})`) | |||
} else if (typeof loc === "string") { | |||
state.insns.push(`LD A, ${loc}`) | |||
} else { | |||
state.insns.push( | |||
`LD HL, SP + (${loc})`, | |||
`LD A, (HL)` | |||
) | |||
} | |||
} | |||
switch (ssa.op) { | |||
case "add": | |||
if (typeof ssa.source1 === "number") { | |||
state.insns.push(`ADD ${ssa.source1}`) | |||
} else { | |||
const loc = state.get_sym_loc(ssa.source1) | |||
if (loc === null) { | |||
throw new Error('fuck') | |||
} else if (typeof loc === "string") { | |||
state.insns.push(`ADD ${loc}`) | |||
} else { | |||
throw new Error('fuck') | |||
state.insns.push( | |||
`LD HL, SP + (${loc})` | |||
) | |||
} | |||
} | |||
break | |||
default: | |||
throw new Error(`unsupported binary op \`${ssa.op}'`) | |||
} | |||
if (typeof dest === "string") { | |||
state.insns.push(`LD ${dest}, A`) | |||
} else { | |||
state.insns.push( | |||
`LD HL, SP + (${dest})`, | |||
`LD (HL), A` | |||
) | |||
} | |||
} | |||
export const convertASM_SSA_Unary = (state: ASMState, ssa: SSAUnary): void => { | |||
const dest = state.alloc_sym_loc(ssa.dest) | |||
if (typeof ssa.source == "number") { | |||
state.insns.push(`LD A, ${ssa.source}`) | |||
} else { | |||
const loc = state.get_sym_loc(ssa.source) | |||
if (loc === null) { | |||
state.insns.push(`LD A, (${ssa.source})`) | |||
} else if (typeof loc === "string") { | |||
state.insns.push(`LD A, ${loc}`) | |||
} else { | |||
state.insns.push( | |||
`LD HL, SP + (${loc})`, | |||
`LD A, (HL)` | |||
) | |||
} | |||
} | |||
switch (ssa.op) { | |||
case 'arith_negate': | |||
state.insns.push( | |||
`CPL`, | |||
`INC A`, | |||
) | |||
break | |||
case 'bit_negate': | |||
state.insns.push( | |||
`CPL` | |||
) | |||
break | |||
default: | |||
throw new Error(`unsupported unary op \`'${ssa.op}`) | |||
} | |||
if (typeof dest === "string") { | |||
state.insns.push(`LD ${dest}, A`) | |||
} else { | |||
state.insns.push( | |||
`LD HL, SP + (${dest})`, | |||
`LD (HL), A` | |||
) | |||
} | |||
} | |||
export const convertASM_SSA_Copy = (state: ASMState, ssa: SSACopy): void => { | |||
const dest = state.alloc_sym_loc(ssa.dest) | |||
let source = "" | |||
if (typeof ssa.source == "number") { | |||
source = ssa.source.toString() | |||
} else { | |||
const loc = state.get_sym_loc(ssa.source) | |||
if (loc === null) { | |||
state.insns.push(`LD A, (${ssa.source})`) | |||
source = "A" | |||
} else if (typeof loc === "string") { | |||
source = loc | |||
} else { | |||
state.insns.push( | |||
`LD HL, SP + (${loc})`, | |||
`LD A, (HL)` | |||
) | |||
source = "A" | |||
} | |||
} | |||
if (typeof dest === "string") { | |||
state.insns.push(`LD ${dest}, ${source}`) | |||
} else { | |||
state.insns.push( | |||
`LD HL, SP + (${dest})`, | |||
`LD (HL), ${source}` | |||
) | |||
} | |||
} |
@ -0,0 +1,25 @@ | |||
export const intersect = <A>(xs: Set<A>, ys: Set<A>): Set<A> => { | |||
const out: Set<A> = new Set() | |||
xs.forEach(x => { | |||
if (ys.has(x)) { | |||
out.add(x) | |||
} | |||
}) | |||
return out | |||
} | |||
export const setEquals = <A>(xs: Set<A>, ys: Set<A>): boolean => { | |||
if (xs.size != ys.size) { | |||
return false | |||
} | |||
for (const x of xs) { | |||
if (!ys.has(x)) { | |||
return false | |||
} | |||
} | |||
return true | |||
} |
@ -0,0 +1,43 @@ | |||
export enum LocType { | |||
REGISTER = "register", | |||
TEMPORARY = "temporary", | |||
VARIABLE = "variable", | |||
} | |||
export class Loc { | |||
type: LocType | |||
name: string | |||
constructor(type: LocType, name: string) { | |||
this.type = type | |||
this.name = name | |||
} | |||
toString(): string { | |||
let sigil = '' | |||
switch (this.type) { | |||
case 'register': | |||
sigil = '%' | |||
break | |||
case 'temporary': | |||
sigil = '#' | |||
break | |||
} | |||
return sigil + this.name | |||
} | |||
static vari(name: string): Loc { | |||
return new Loc(LocType.VARIABLE, name) | |||
} | |||
static temp(name: string): Loc { | |||
return new Loc(LocType.TEMPORARY, name) | |||
} | |||
static reg(name: string): Loc { | |||
return new Loc(LocType.REGISTER, name) | |||
} | |||
} |
@ -0,0 +1,45 @@ | |||
import type { Loc } from "./loc" | |||
import type { Operand, SSA } from "./ssa" | |||
export const prettyPrintBlock = (block: Array<SSA>): string => { | |||
return block.map(prettyPrintSSA).join("\n") | |||
} | |||
export const prettyPrintSSA = (ssa: SSA): string => { | |||
let output = "" | |||
if ("source1" in ssa) { | |||
output = `${ppLoc(ssa.dest)} = ${ssa.op}(${ppOp(ssa.source)}, ${ppOp(ssa.source1)})` | |||
} else if ("op" in ssa) { | |||
output = `${ppLoc(ssa.dest)} = ${ssa.op}(${ppOp(ssa.source)})` | |||
} else { | |||
output = `${ppLoc(ssa.dest)} = ${ppOp(ssa.source)}` | |||
} | |||
return output | |||
} | |||
const ppOp = (op: Operand): string => | |||
typeof op === "number" | |||
? op.toString() | |||
: ppLoc(op) | |||
const ppLoc = (loc: Loc): string => { | |||
let output = '' | |||
switch (loc.type) { | |||
case "register": | |||
output = `%${loc.name}` | |||
break | |||
case "temporary": | |||
output = `#${loc.name}` | |||
break | |||
case "variable": | |||
output = loc.name | |||
break | |||
} | |||
return output | |||
} |
@ -0,0 +1,17 @@ | |||
import type { BinaryOp, UnaryOp } from "../ast" | |||
import type { Loc } from "./loc" | |||
export type Operand = Loc | number | |||
type ASSA<Data> = { dest: Loc, source: Operand } & Data | |||
// dest = source | |||
export type SSACopy = ASSA<{}> | |||
// dest = op(source) | |||
export type SSAUnary = ASSA<{ op: UnaryOp }> | |||
// dest = op(source, source1) | |||
export type SSABinary = ASSA<{ op: BinaryOp, source1: Operand }> | |||
export type SSA = SSACopy | SSAUnary | SSABinary |
@ -0,0 +1,33 @@ | |||
import type { Loc } from "./ir/loc" | |||
import type { SSA } from "./ir/ssa" | |||
export type LivenessInfo = Array<Set<Loc>> | |||
export const liveness = (block: Array<SSA>): LivenessInfo => { | |||
const info: LivenessInfo = [] | |||
for (let i = block.length - 1; i >= 0; --i) { | |||
const insn = block[i] | |||
const last = info[i + 1] || new Set() | |||
info[i] = new Set() | |||
if (typeof insn.source !== "number") { | |||
info[i].add(insn.source) | |||
} | |||
if ("source1" in insn && typeof insn.source1 !== "number") { | |||
info[i].add(insn.source1) | |||
} | |||
last.forEach(loc => { | |||
if (loc === insn.dest) { | |||
return | |||
} | |||
info[i].add(loc) | |||
}) | |||
} | |||
return info | |||
} |
@ -0,0 +1,40 @@ | |||
import { expect } from "chai" | |||
import { createGraph, neighbors, vertices } from "../../lib/data/graph" | |||
const g = createGraph((v, e) => { | |||
v("a", true) | |||
v("b", true) | |||
v("c", true) | |||
v("d", true) | |||
e("a", "b") | |||
e("a", "c") | |||
}) | |||
const empty = createGraph((v, e) => { }) | |||
describe("graph", () => { | |||
describe("neighbors", () => { | |||
it("returns neighbors of vertex", () => { | |||
const ns = neighbors(g, "a") | |||
expect(ns).to.deep.equal(new Set(["b", "c"])) | |||
}) | |||
it("returns empty set when vertex has no neighbors", () => { | |||
const ns = neighbors(g, "d") | |||
expect(ns).to.deep.equal(new Set()) | |||
}) | |||
it("throw on invalid vertex", () => { | |||
expect(() => neighbors(empty, "x")).to.throw() | |||
}) | |||
}) | |||
describe("vertices", () => { | |||
it("return all vertices for a graph", () => { | |||
const vs = vertices(g) | |||
expect(vs).to.deep.equal(new Set(["a", "b", "c", "d"])) | |||
}) | |||
}) | |||
}) |
@ -0,0 +1,133 @@ | |||
import { expect } from "chai" | |||
import { convertAssignStmt } from "../../lib/ir/convert" | |||
import { Loc } from "../../lib/ir/loc" | |||
describe("ir", () => { | |||
describe("convertExpr", () => { | |||
it("load with immediate", () => { | |||
const state = { nextID: 0, ssa_stmts: [] } | |||
const ir = convertAssignStmt(state, { | |||
type: "stmt", | |||
stmt_type: "assign", | |||
args: { | |||
name: "x", | |||
expr: 42, | |||
}, | |||
}) | |||
expect(ir).to.deep.equal([ | |||
{ | |||
dest: Loc.vari("x"), | |||
source: 42, | |||
}, | |||
]) | |||
}) | |||
it("load with variable", () => { | |||
const state = { nextID: 0, ssa_stmts: [] } | |||
const ir = convertAssignStmt(state, { | |||
type: "stmt", | |||
stmt_type: "assign", | |||
args: { | |||
name: "x", | |||
expr: "y", | |||
}, | |||
}) | |||
expect(ir).to.deep.equal([ | |||
{ | |||
dest: Loc.vari("x"), | |||
source: Loc.vari("y"), | |||
}, | |||
]) | |||
}) | |||
it("unary op", () => { | |||
const state = { nextID: 0, ssa_stmts: [] } | |||
const ir = convertAssignStmt(state, { | |||
type: "stmt", | |||
stmt_type: "assign", | |||
args: { | |||
name: "x", | |||
expr: { | |||
type: "unary", | |||
op: "arith_negate", | |||
arg: "y" | |||
}, | |||
}, | |||
}) | |||
expect(ir).to.deep.equal([ | |||
{ | |||
dest: Loc.vari("x"), | |||
source: Loc.vari("y"), | |||
op: "arith_negate", | |||
}, | |||
]) | |||
}) | |||
it("binary op", () => { | |||
const state = { nextID: 0, ssa_stmts: [] } | |||
const ir = convertAssignStmt(state, { | |||
type: "stmt", | |||
stmt_type: "assign", | |||
args: { | |||
name: "x", | |||
expr: { | |||
type: "binary", | |||
op: "add", | |||
left: "y", | |||
right: 42, | |||
}, | |||
}, | |||
}) | |||
expect(ir).to.deep.equal([ | |||
{ | |||
dest: Loc.vari("x"), | |||
source: Loc.vari("y"), | |||
source1: 42, | |||
op: "add", | |||
}, | |||
]) | |||
}) | |||
it("nested binary op", () => { | |||
const state = { nextID: 0, ssa_stmts: [] } | |||
const ir = convertAssignStmt(state, { | |||
type: "stmt", | |||
stmt_type: "assign", | |||
args: { | |||
name: "x", | |||
expr: { | |||
type: "binary", | |||
op: "add", | |||
left: "y", | |||
right: { | |||
type: "binary", | |||
op: "subtract", | |||
left: "z", | |||
right: 42, | |||
}, | |||
}, | |||
}, | |||
}) | |||
expect(ir).to.deep.equal([ | |||
{ | |||
dest: Loc.temp("t0"), | |||
source: Loc.vari("z"), | |||
op: "subtract", | |||
source1: 42, | |||
}, | |||
{ | |||
dest: Loc.vari("x"), | |||
source: Loc.vari("y"), | |||
op: "add", | |||
source1: Loc.temp("t0"), | |||
}, | |||
]) | |||
}) | |||
}) | |||
}) |
@ -0,0 +1,66 @@ | |||
import { expect } from "chai" | |||
import { Loc } from "../../lib/ir/loc" | |||
import { prettyPrintSSA } from "../../lib/ir/pretty" | |||
import type { SSA } from "../../lib/ir/ssa" | |||
describe("ir", () => { | |||
describe("prettyPrintSSA", () => { | |||
it("load with immediate", () => { | |||
const ssa: SSA = { | |||
dest: Loc.vari("x"), | |||
source: 42, | |||
} | |||
expect(prettyPrintSSA(ssa)).to.equal("x = 42") | |||
}) | |||
it("load with register", () => { | |||
const ssa: SSA = { | |||
dest: Loc.vari("x"), | |||
source: Loc.reg("A"), | |||
} | |||
expect(prettyPrintSSA(ssa)).to.equal("x = %A") | |||
}) | |||
it("load with temporary", () => { | |||
const ssa: SSA = { | |||
dest: Loc.vari("x"), | |||
source: Loc.temp("t0"), | |||
} | |||
expect(prettyPrintSSA(ssa)).to.equal("x = #t0") | |||
}) | |||
it("load with variable", () => { | |||
const ssa: SSA = { | |||
dest: Loc.vari("x"), | |||
source: Loc.vari("y"), | |||
} | |||
expect(prettyPrintSSA(ssa)).to.equal("x = y") | |||
}) | |||
it("unary op", () => { | |||
const ssa: SSA = { | |||
dest: Loc.vari("x"), | |||
source: Loc.vari("y"), | |||
op: "arith_negate", | |||
} | |||
expect(prettyPrintSSA(ssa)).to.equal("x = arith_negate(y)") | |||
}) | |||
it("binary op", () => { | |||
const ssa: SSA = { | |||
dest: Loc.vari("x"), | |||
source: Loc.vari("y"), | |||
source1: 42, | |||
op: "add", | |||
} | |||
expect(prettyPrintSSA(ssa)).to.equal("x = add(y, 42)") | |||
}) | |||
}) | |||
}) |
@ -0,0 +1,28 @@ | |||
import { expect } from "chai" | |||
import { Loc } from "../lib/ir/loc" | |||
import type { SSA } from "../lib/ir/ssa" | |||
import { liveness } from "../lib/live" | |||
describe("liveness", () => { | |||
it("computes liveness", () => { | |||
const x = [0, 1, 2].map(i => Loc.vari("x" + i)) | |||
const y = [0, 1].map(i => Loc.vari("y" + i)) | |||
const block: Array<SSA> = [ | |||
{ dest: x[0], source: 1 }, | |||
{ dest: x[1], source: x[0], op: "add", source1: x[0] }, | |||
{ dest: x[2], source: x[1], op: "add", source1: x[0] }, | |||
{ dest: y[0], source: x[0], op: "add", source1: x[1] }, | |||
{ dest: y[1], source: y[0], op: "add", source1: x[2] }, | |||
] | |||
const info = liveness(block) | |||
expect(info[0]).to.deep.equal(new Set()) | |||
expect(info[1]).to.deep.equal(new Set([x[0]])) | |||
expect(info[2]).to.deep.equal(new Set([x[0], x[1]])) | |||
expect(info[3]).to.deep.equal(new Set([x[0], x[1], x[2]])) | |||
expect(info[4]).to.deep.equal(new Set([y[0], x[2]])) | |||
}) | |||
}) |