3 Commits

17 changed files with 2698 additions and 333 deletions
Unified View
  1. +0
    -240
      lib/asm.ts
  2. +6
    -2
      lib/ast.d.ts
  3. +4
    -18
      lib/compile.ts
  4. +4
    -14
      lib/data/graph.ts
  5. +25
    -0
      lib/data/set.ts
  6. +14
    -34
      lib/ir/convert.ts
  7. +43
    -0
      lib/ir/loc.ts
  8. +45
    -0
      lib/ir/pretty.ts
  9. +17
    -0
      lib/ir/ssa.d.ts
  10. +33
    -0
      lib/live.ts
  11. +1
    -0
      lib/main.ts
  12. +2230
    -24
      package-lock.json
  13. +9
    -1
      package.json
  14. +40
    -0
      test/data/graph.spec.ts
  15. +133
    -0
      test/ir/convert.spec.ts
  16. +66
    -0
      test/ir/pretty.spec.ts
  17. +28
    -0
      test/live.spec.ts

+ 0
- 240
lib/asm.ts View File

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

+ 6
- 2
lib/ast.d.ts View File

@ -34,19 +34,23 @@ export type Expr = BinaryExpr | UnaryExpr | BasicExpr
type BinaryExpr = { type BinaryExpr = {
type: "binary", type: "binary",
op: string,
op: BinaryOp,
left: Expr, left: Expr,
right: Expr, right: Expr,
} }
type UnaryExpr = { type UnaryExpr = {
type: "unary", type: "unary",
op: string,
op: UnaryOp,
arg: Expr, arg: Expr,
} }
type BasicExpr = number | string type BasicExpr = number | string
type UnaryOp = "arith_negate" | "bit_negate"
type BinaryOp = "add" | "subtract" | "shift_left" | "shift_right" | "bit_and"
| "bit_xor" | "bit_or"
// Attributes // Attributes
export type Attr = { export type Attr = {
name: string, name: string,

+ 4
- 18
lib/compile.ts View File

@ -1,5 +1,5 @@
import { convertASM } from "./asm"
import { convertIR, prettyPrintIR } from "./ir"
import { convertIR } from "./ir/convert"
import { prettyPrintBlock } from "./ir/pretty"
import { parse } from "./parser" import { parse } from "./parser"
import type { Attr, Decl, Stmt, Type, VarDecl } from "./ast" import type { Attr, Decl, Stmt, Type, VarDecl } from "./ast"
@ -35,25 +35,11 @@ export const compile = (fileName: string, source: string): string => {
const ir = convertIR(stmts) const ir = convertIR(stmts)
console.log("=== IR === ") console.log("=== IR === ")
console.log(ir.map(prettyPrintIR).join("\n"))
console.log(prettyPrintBlock(ir))
// 5. Generate code // 5. Generate code
const insns = convertASM(ir)
let output = ''
if (asmDecls.length > 0) {
output += `SECTION "${fileName} Data", WRAM0\n\n`
output += asmDecls.join("\n")
output += "\n\n"
}
if (insns.length > 0) {
output += `SECTION "${fileName} Code", ROM0\n\n`
output += insns.join("\n")
}
console.log("=== ASM === ")
return output
return ''
} }
export const processDecls = (decls: Array<Decl>): SymbolMap => decls.reduce(processDecl, {}) export const processDecls = (decls: Array<Decl>): SymbolMap => decls.reduce(processDecl, {})

lib/graph/graph.ts → lib/data/graph.ts View File

@ -1,3 +1,5 @@
import { intersect } from "./set"
export type Graph<V> = { export type Graph<V> = {
vertices: { vertices: {
[v: string]: V [v: string]: V
@ -48,14 +50,14 @@ export const maxCardinalitySearch = (g: Graph): Array => {
const weights: { [s: string]: number } = {} const weights: { [s: string]: number } = {}
const ordering: Array<string> = [] const ordering: Array<string> = []
let W = vertices(g)
const W = vertices(g)
const numVertices = W.size const numVertices = W.size
for (let i = 0; i < numVertices; ++i) { for (let i = 0; i < numVertices; ++i) {
const v = findMaxWeight(weights, W) const v = findMaxWeight(weights, W)
ordering.push(v) ordering.push(v)
setIntersect(W, neighbors(g, v)).forEach(x =>
intersect(W, neighbors(g, v)).forEach(x =>
weights[x] = (weights[x] || 0) + 1 weights[x] = (weights[x] || 0) + 1
) )
@ -83,15 +85,3 @@ const findMaxWeight = (weights: { [s: string]: number }, W: Set): string
return maxV return maxV
} }
const setIntersect = <A>(xs: Set<A>, ys: Set<A>): Set<A> => {
const intersection: Set<A> = new Set()
xs.forEach(x => {
if (ys.has(x)) {
intersection.add(x)
}
})
return intersection
}

+ 25
- 0
lib/data/set.ts View File

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

lib/ir.ts → lib/ir/convert.ts View File

@ -1,16 +1,6 @@
import type { AssignStmt, Expr, Stmt } from "./ast"
type Operand = string | number
type ASSA<Data> = { dest: string, source: Operand } & Data
export type SSACopy = ASSA<{ source: Operand }> // dest = source
export type SSAUnary = ASSA<{ op: string }> // dest = op(source)
export type SSABinary = ASSA<{ op: string, source1: Operand }> // dest = op(source, source1)
export type SSA = SSACopy | SSAUnary | SSABinary
import type { AssignStmt, Expr, Stmt } from "../ast"
import { Loc } from "./loc"
import type { Operand, SSA } from "./ssa"
type IRState = { type IRState = {
nextID: number nextID: number
@ -32,14 +22,16 @@ export const convertIR = (stmts: Array): Array => {
} }
export const convertAssignStmt = (state: IRState, stmt: AssignStmt): Array<SSA> => { export const convertAssignStmt = (state: IRState, stmt: AssignStmt): Array<SSA> => {
return convertExpr(state, stmt.args.name, stmt.args.expr)
return convertExpr(state, Loc.vari(stmt.args.name), stmt.args.expr)
} }
export const convertExpr = (state: IRState, dest: string, expr: Expr): Array<SSA> => {
let expr_stmts = []
export const convertExpr = (state: IRState, dest: Loc, expr: Expr): Array<SSA> => {
let expr_stmts: Array<SSA> = []
if (typeof expr === "number" || typeof expr === "string") {
if (typeof expr === "number") {
expr_stmts = [{ dest, source: expr }] expr_stmts = [{ dest, source: expr }]
} else if (typeof expr === "string") {
expr_stmts = [{ dest, source: Loc.vari(expr) }]
} else if (expr.type === "unary") { } else if (expr.type === "unary") {
const [source, stmts] = getSource(state, expr.arg) const [source, stmts] = getSource(state, expr.arg)
stmts.push({ stmts.push({
@ -67,26 +59,14 @@ export const convertExpr = (state: IRState, dest: string, expr: Expr): Array
return expr_stmts return expr_stmts
} }
const getSource = (state: IRState, expr: Expr): [string | number, Array<SSA>] => {
if (typeof expr === "number" || typeof expr === "string") {
const getSource = (state: IRState, expr: Expr): [Operand, Array<SSA>] => {
if (typeof expr === "number") {
return [expr, []] return [expr, []]
} else if (typeof expr === "string") {
return [Loc.vari(expr), []]
} }
const source = `temp${state.nextID++}`
const source = Loc.temp(`t${state.nextID++}`)
const stmts = convertExpr(state, source, expr) const stmts = convertExpr(state, source, expr)
return [source, stmts] return [source, stmts]
} }
export const prettyPrintIR = (ssa: SSA): string => {
let output = ""
if ("source1" in ssa) {
output = `${ssa.dest} = ${ssa.op}(${ssa.source}, ${ssa.source1})`
} else if ("op" in ssa) {
output = `${ssa.dest} = ${ssa.op}(${ssa.source})`
} else {
output = `${ssa.dest} = ${ssa.source}`
}
return output
}

+ 43
- 0
lib/ir/loc.ts View File

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

+ 45
- 0
lib/ir/pretty.ts View File

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

+ 17
- 0
lib/ir/ssa.d.ts View File

@ -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

+ 33
- 0
lib/live.ts View File

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

+ 1
- 0
lib/main.ts View File

@ -5,6 +5,7 @@ import { compile } from "./compile"
const { readFile } = promises const { readFile } = promises
// TODO: Proper command-line argument support
if (process.argv.length !== 3) { if (process.argv.length !== 3) {
console.error("usage: gbuoy <program.gby>") console.error("usage: gbuoy <program.gby>")
process.exit(1) process.exit(1)

+ 2230
- 24
package-lock.json
File diff suppressed because it is too large
View File


+ 9
- 1
package.json View File

@ -1,18 +1,26 @@
{ {
"devDependencies": { "devDependencies": {
"@types/chai": "^4.2.21",
"@types/mocha": "^9.0.0",
"@types/node": "^16.9.1", "@types/node": "^16.9.1",
"chai": "^4.3.4",
"mocha": "^8.4.0",
"peggy": "^1.2.0", "peggy": "^1.2.0",
"ts-mocha": "^8.0.0",
"typescript": "^4.4.3" "typescript": "^4.4.3"
}, },
"name": "gbuoy", "name": "gbuoy",
"version": "1.0.0", "version": "1.0.0",
"main": "gbuoy.js", "main": "gbuoy.js",
"mocha": {
"spec": "test/**/*.spec.ts"
},
"directories": { "directories": {
"lib": "lib" "lib": "lib"
}, },
"scripts": { "scripts": {
"build": "mkdir -p build && peggy -o build/parser.js lib/parser.pegjs && tsc", "build": "mkdir -p build && peggy -o build/parser.js lib/parser.pegjs && tsc",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "ts-mocha"
}, },
"author": "Forest Belton <forest@homolo.gy>", "author": "Forest Belton <forest@homolo.gy>",
"license": "MIT", "license": "MIT",

+ 40
- 0
test/data/graph.spec.ts View File

@ -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"]))
})
})
})

+ 133
- 0
test/ir/convert.spec.ts View File

@ -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"),
},
])
})
})
})

+ 66
- 0
test/ir/pretty.spec.ts View File

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

+ 28
- 0
test/live.spec.ts View File

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

Loading…
Cancel
Save