From 83d32dfe99196678b172a05416b9557d116eae84 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Fri, 2 Aug 2019 00:13:19 +0200 Subject: [PATCH] Added basic algorithm to compute the possible values of a register at a specific program point. --- src/data_formats/parsing/quest/opcodes.ts | 3 + src/scripting/ValueSet.test.ts | 55 ------ .../ControlFlowGraph.test.ts} | 16 +- .../ControlFlowGraph.ts} | 81 ++------- .../data_flow_analysis/ValueSet.test.ts | 84 +++++++++ .../{ => data_flow_analysis}/ValueSet.ts | 97 ++++++++-- .../register_values.test.ts | 168 ++++++++++++++++++ .../data_flow_analysis/register_values.ts | 130 ++++++++++++++ 8 files changed, 489 insertions(+), 145 deletions(-) delete mode 100644 src/scripting/ValueSet.test.ts rename src/scripting/{data_flow_analysis.test.ts => data_flow_analysis/ControlFlowGraph.test.ts} (91%) rename src/scripting/{data_flow_analysis.ts => data_flow_analysis/ControlFlowGraph.ts} (88%) create mode 100644 src/scripting/data_flow_analysis/ValueSet.test.ts rename src/scripting/{ => data_flow_analysis}/ValueSet.ts (53%) create mode 100644 src/scripting/data_flow_analysis/register_values.test.ts create mode 100644 src/scripting/data_flow_analysis/register_values.ts diff --git a/src/data_formats/parsing/quest/opcodes.ts b/src/data_formats/parsing/quest/opcodes.ts index 0fa3e80b..2c618700 100644 --- a/src/data_formats/parsing/quest/opcodes.ts +++ b/src/data_formats/parsing/quest/opcodes.ts @@ -136,6 +136,7 @@ export class Opcode { false, [] )); + // Sets a register to the memory offset of a register. Not used by Sega. static readonly leta = (OPCODES[0x0c] = new Opcode( 0x0c, "leta", @@ -143,6 +144,7 @@ export class Opcode { false, [] )); + // Sets a register to the memory offset of a label. Not used by Sega. static readonly leto = (OPCODES[0x0d] = new Opcode( 0x0d, "leto", @@ -2404,6 +2406,7 @@ export class Opcode { false, [] )); + // Resets all registers to 0 (may have to change areas?). static readonly reset_map = (OPCODES[0xf89b] = new Opcode(0xf89b, "reset_map", [], false, [])); static readonly disp_chl_retry_menu = (OPCODES[0xf89c] = new Opcode( 0xf89c, diff --git a/src/scripting/ValueSet.test.ts b/src/scripting/ValueSet.test.ts deleted file mode 100644 index 93d2365f..00000000 --- a/src/scripting/ValueSet.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ValueSet } from "./ValueSet"; - -test("empty", () => { - const vs = new ValueSet(); - - expect(vs.size()).toBe(0); -}); - -test("set_value", () => { - const vs = new ValueSet(); - vs.set_value(100); - vs.set_value(4); - vs.set_value(24324); - - expect(vs.size()).toBe(1); - expect([...vs]).toEqual([24324]); -}); - -test("union", () => { - const c = new ValueSet() - .union(new ValueSet().set_value(21)) - .union(new ValueSet().set_value(4968)); - - expect(c.size()).toBe(2); - expect([...c]).toEqual([21, 4968]); -}); - -test("union of intervals", () => { - const a = new ValueSet() - .union(new ValueSet().set_interval(10, 13)) - .union(new ValueSet().set_interval(14, 17)); - - expect(a.size()).toBe(6); - expect([...a]).toEqual([10, 11, 12, 14, 15, 16]); - - a.union(new ValueSet().set_interval(13, 14)); - - expect(a.size()).toBe(7); - expect([...a]).toEqual([10, 11, 12, 13, 14, 15, 16]); - - a.union(new ValueSet().set_interval(1, 3)); - - expect(a.size()).toBe(9); - expect([...a]).toEqual([1, 2, 10, 11, 12, 13, 14, 15, 16]); - - a.union(new ValueSet().set_interval(30, 33)); - - expect(a.size()).toBe(12); - expect([...a]).toEqual([1, 2, 10, 11, 12, 13, 14, 15, 16, 30, 31, 32]); - - a.union(new ValueSet().set_interval(20, 22)); - - expect(a.size()).toBe(14); - expect([...a]).toEqual([1, 2, 10, 11, 12, 13, 14, 15, 16, 20, 21, 30, 31, 32]); -}); diff --git a/src/scripting/data_flow_analysis.test.ts b/src/scripting/data_flow_analysis/ControlFlowGraph.test.ts similarity index 91% rename from src/scripting/data_flow_analysis.test.ts rename to src/scripting/data_flow_analysis/ControlFlowGraph.test.ts index c0ff4a72..2ccfafaf 100644 --- a/src/scripting/data_flow_analysis.test.ts +++ b/src/scripting/data_flow_analysis/ControlFlowGraph.test.ts @@ -1,13 +1,13 @@ -import { InstructionSegment, SegmentType } from "../data_formats/parsing/quest/bin"; -import { assemble } from "./assembly"; -import { create_control_flow_graph, BranchType } from "./data_flow_analysis"; +import { InstructionSegment, SegmentType } from "../../data_formats/parsing/quest/bin"; +import { assemble } from "../assembly"; +import { BranchType, ControlFlowGraph } from "./ControlFlowGraph"; test("single instruction", () => { const im = to_instructions(` 0: ret `); - const cfg = create_control_flow_graph(im); + const cfg = ControlFlowGraph.create(im); expect(cfg.blocks.length).toBe(1); @@ -26,7 +26,7 @@ test("single unconditional jump", () => { 1: ret `); - const cfg = create_control_flow_graph(im); + const cfg = ControlFlowGraph.create(im); expect(cfg.blocks.length).toBe(2); @@ -53,7 +53,7 @@ test("single conditional jump", () => { 1: ret `); - const cfg = create_control_flow_graph(im); + const cfg = ControlFlowGraph.create(im); expect(cfg.blocks.length).toBe(3); @@ -87,7 +87,7 @@ test("single call", () => { 1: ret `); - const cfg = create_control_flow_graph(im); + const cfg = ControlFlowGraph.create(im); expect(cfg.blocks.length).toBe(3); @@ -122,7 +122,7 @@ test("conditional jump with fall-through", () => { nop ret `); - const cfg = create_control_flow_graph(im); + const cfg = ControlFlowGraph.create(im); expect(cfg.blocks.length).toBe(3); diff --git a/src/scripting/data_flow_analysis.ts b/src/scripting/data_flow_analysis/ControlFlowGraph.ts similarity index 88% rename from src/scripting/data_flow_analysis.ts rename to src/scripting/data_flow_analysis/ControlFlowGraph.ts index 54bb98cb..4968d15b 100644 --- a/src/scripting/data_flow_analysis.ts +++ b/src/scripting/data_flow_analysis/ControlFlowGraph.ts @@ -1,11 +1,10 @@ import { + Instruction, InstructionSegment, Opcode, Segment, SegmentType, - Instruction, -} from "../data_formats/parsing/quest/bin"; -import { ValueSet } from "./ValueSet"; +} from "../../data_formats/parsing/quest/bin"; export enum BranchType { None, @@ -39,81 +38,21 @@ export class BasicBlock { export class ControlFlowGraph { readonly blocks: BasicBlock[] = []; readonly instructions: Map = new Map(); -} -export function create_control_flow_graph(segments: InstructionSegment[]): ControlFlowGraph { - const cfg = new ControlFlowGraph(); - // Mapping of labels to basic blocks. - const label_blocks = new Map(); + static create(segments: InstructionSegment[]): ControlFlowGraph { + const cfg = new ControlFlowGraph(); + // Mapping of labels to basic blocks. + const label_blocks = new Map(); - for (const segment of segments) { - create_basic_blocks(cfg, label_blocks, segment); - } - - link_blocks(cfg, label_blocks); - return cfg; -} - -/** - * Computes the possible values of a register at a specific instruction. - */ -export function register_value_set( - cfg: ControlFlowGraph, - instruction: Instruction, - register: number -): ValueSet { - const block = cfg.instructions.get(instruction); - - if (block) { - let inst_idx = block.start; - - while (inst_idx < block.end) { - if (block.segment.instructions[inst_idx] === instruction) { - break; - } - - inst_idx++; + for (const segment of segments) { + create_basic_blocks(cfg, label_blocks, segment); } - return find_value_set(block, inst_idx, register); - } else { - return new ValueSet(); + link_blocks(cfg, label_blocks); + return cfg; } } -function find_value_set(block: BasicBlock, end: number, register: number): ValueSet { - let values = new ValueSet(); - - for (let i = block.start; i < end; i++) { - const instruction = block.segment.instructions[i]; - const args = instruction.args; - - switch (instruction.opcode) { - case Opcode.let: - if (args[0].value === register) { - values = find_value_set(block, i, args[1].value); - } - break; - case Opcode.leti: - case Opcode.letb: - case Opcode.letw: - case Opcode.leto: - if (args[0].value === register) { - values.set_value(args[1].value); - } - break; - } - } - - if (values.size() === 0) { - for (const from of block.from) { - values.union(find_value_set(from, from.end, register)); - } - } - - return values; -} - function create_basic_blocks( cfg: ControlFlowGraph, label_blocks: Map, diff --git a/src/scripting/data_flow_analysis/ValueSet.test.ts b/src/scripting/data_flow_analysis/ValueSet.test.ts new file mode 100644 index 00000000..ec5f7c57 --- /dev/null +++ b/src/scripting/data_flow_analysis/ValueSet.test.ts @@ -0,0 +1,84 @@ +import { ValueSet } from "./ValueSet"; + +test("empty", () => { + const vs = new ValueSet(); + + expect(vs.size()).toBe(0); +}); + +test("get", () => { + const vs = new ValueSet().set_interval(10, 13).union(new ValueSet().set_interval(20, 22)); + + expect(vs.size()).toBe(7); + expect(vs.get(0)).toBe(10); + expect(vs.get(1)).toBe(11); + expect(vs.get(2)).toBe(12); + expect(vs.get(3)).toBe(13); + expect(vs.get(4)).toBe(20); + expect(vs.get(5)).toBe(21); + expect(vs.get(6)).toBe(22); +}); + +test("has", () => { + const vs = new ValueSet().set_interval(-20, 13).union(new ValueSet().set_interval(20, 22)); + + expect(vs.size()).toBe(37); + expect(vs.has(-9001)).toBe(false); + expect(vs.has(-21)).toBe(false); + expect(vs.has(-20)).toBe(true); + expect(vs.has(13)).toBe(true); + expect(vs.has(14)).toBe(false); + expect(vs.has(19)).toBe(false); + expect(vs.has(20)).toBe(true); + expect(vs.has(22)).toBe(true); + expect(vs.has(23)).toBe(false); + expect(vs.has(9001)).toBe(false); +}); + +test("set_value", () => { + const vs = new ValueSet(); + vs.set_value(100); + vs.set_value(4); + vs.set_value(24324); + + expect(vs.size()).toBe(1); + expect([...vs]).toEqual([24324]); +}); + +test("union", () => { + const c = new ValueSet() + .union(new ValueSet().set_value(21)) + .union(new ValueSet().set_value(4968)); + + expect(c.size()).toBe(2); + expect([...c]).toEqual([21, 4968]); +}); + +test("union of intervals", () => { + const a = new ValueSet() + .union(new ValueSet().set_interval(10, 12)) + .union(new ValueSet().set_interval(14, 16)); + + expect(a.size()).toBe(6); + expect([...a]).toEqual([10, 11, 12, 14, 15, 16]); + + a.union(new ValueSet().set_interval(13, 13)); + + expect(a.size()).toBe(7); + expect([...a]).toEqual([10, 11, 12, 13, 14, 15, 16]); + + a.union(new ValueSet().set_interval(1, 2)); + + expect(a.size()).toBe(9); + expect([...a]).toEqual([1, 2, 10, 11, 12, 13, 14, 15, 16]); + + a.union(new ValueSet().set_interval(30, 32)); + + expect(a.size()).toBe(12); + expect([...a]).toEqual([1, 2, 10, 11, 12, 13, 14, 15, 16, 30, 31, 32]); + + a.union(new ValueSet().set_interval(20, 21)); + + expect(a.size()).toBe(14); + expect([...a]).toEqual([1, 2, 10, 11, 12, 13, 14, 15, 16, 20, 21, 30, 31, 32]); +}); diff --git a/src/scripting/ValueSet.ts b/src/scripting/data_flow_analysis/ValueSet.ts similarity index 53% rename from src/scripting/ValueSet.ts rename to src/scripting/data_flow_analysis/ValueSet.ts index bbac94d3..56eab3b4 100644 --- a/src/scripting/ValueSet.ts +++ b/src/scripting/data_flow_analysis/ValueSet.ts @@ -1,14 +1,46 @@ /** - * Represents a set of integers. + * Represents a sorted set of integers. */ export class ValueSet { /** - * Open intervals [start, end[. + * Closed intervals [start, end]. */ private intervals: { start: number; end: number }[] = []; size(): number { - return this.intervals.reduce((acc, i) => acc + i.end - i.start, 0); + return this.intervals.reduce((acc, i) => acc + i.end - i.start + 1, 0); + } + + get(i: number): number | undefined { + for (const { start, end } of this.intervals) { + const size = end - start + 1; + + if (i < size) { + return start + i; + } else { + i -= size; + } + } + + return undefined; + } + + min(): number | undefined { + return this.intervals.length ? this.intervals[0].start : undefined; + } + + max(): number | undefined { + return this.intervals.length ? this.intervals[this.intervals.length - 1].end : undefined; + } + + has(value: number): boolean { + for (const int of this.intervals) { + if (int.start <= value && value <= int.end) { + return true; + } + } + + return false; } /** @@ -17,7 +49,7 @@ export class ValueSet { * @param value integer value */ set_value(value: number): ValueSet { - this.intervals = [{ start: value, end: value + 1 }]; + this.intervals = [{ start: value, end: value }]; return this; } @@ -25,16 +57,47 @@ export class ValueSet { * Sets this ValueSet to the values in the given interval. * * @param start lower bound, inclusive - * @param end upper bound, exclusive + * @param end upper bound, inclusive */ set_interval(start: number, end: number): ValueSet { if (end < start) throw new Error( - `Interval upper bound should be greater than lower bound, got [${start}, ${end}[.` + `Interval upper bound should be greater than or equal to lower bound, got [${start}, ${end}].` ); - if (end !== start) { - this.intervals = [{ start, end }]; + this.intervals = [{ start, end }]; + return this; + } + + scalar_add(s: number): ValueSet { + for (const int of this.intervals) { + int.start += s; + int.end += s; + } + + return this; + } + + scalar_sub(s: number): ValueSet { + return this.scalar_add(-s); + } + + scalar_mul(s: number): ValueSet { + for (const int of this.intervals) { + int.start *= s; + int.end *= s; + } + + return this; + } + + /** + * Integer division. + */ + scalar_div(s: number): ValueSet { + for (const int of this.intervals) { + int.start = Math.floor(int.start / s); + int.end = Math.floor(int.end / s); } return this; @@ -47,11 +110,11 @@ export class ValueSet { while (i < this.intervals.length) { const a = this.intervals[i]; - if (b.end < a.start) { + if (b.end < a.start - 1) { this.intervals.splice(i, 0, b); i++; continue outer; - } else if (b.start <= a.end) { + } else if (b.start <= a.end + 1) { a.start = Math.min(a.start, b.start); let j = i; @@ -80,6 +143,18 @@ export class ValueSet { return this; } + to_array(): number[] { + let array: number[] = []; + + for (const { start, end } of this.intervals) { + for (let i = start; i <= end; i++) { + array.push(i); + } + } + + return array; + } + [Symbol.iterator](): Iterator { const vs = this; let int_i = 0; @@ -93,7 +168,7 @@ export class ValueSet { if (isNaN(value)) { value = vs.intervals[int_i].start; done = false; - } else if (value >= vs.intervals[int_i].end) { + } else if (value > vs.intervals[int_i].end) { int_i++; if (int_i < vs.intervals.length) { diff --git a/src/scripting/data_flow_analysis/register_values.test.ts b/src/scripting/data_flow_analysis/register_values.test.ts new file mode 100644 index 00000000..3e8bdcdb --- /dev/null +++ b/src/scripting/data_flow_analysis/register_values.test.ts @@ -0,0 +1,168 @@ +import { InstructionSegment, SegmentType, Opcode } from "../../data_formats/parsing/quest/bin"; +import { assemble } from "../assembly"; +import { ControlFlowGraph } from "./ControlFlowGraph"; +import { register_values } from "./register_values"; + +test(`${register_values.name} trivial case`, () => { + const im = to_instructions(` + 0: + ret + `); + const cfg = ControlFlowGraph.create(im); + const values = register_values(cfg, im[0].instructions[0], 6); + + expect(values.size()).toBe(0); +}); + +test(`${register_values.name} single assignment`, () => { + const im = to_instructions(` + 0: + leti r6, 1337 + ret + `); + const cfg = ControlFlowGraph.create(im); + const values = register_values(cfg, im[0].instructions[1], 6); + + expect(values.size()).toBe(1); + expect(values.get(0)).toBe(1337); +}); + +test(`${register_values.name} two code paths`, () => { + const im = to_instructions(` + 0: + jmp_> r1, r2, 1 + leti r10, 111 + jmp 2 + 1: + leti r10, 222 + 2: + ret + `); + const cfg = ControlFlowGraph.create(im); + const values = register_values(cfg, im[2].instructions[0], 10); + + expect(values.size()).toBe(2); + expect(values.get(0)).toBe(111); + expect(values.get(1)).toBe(222); +}); + +test(`${register_values.name} leta and leto`, () => { + const im = to_instructions(` + 0: + leta r0, r100 + leto r1, 100 + ret + `); + const cfg = ControlFlowGraph.create(im); + const r0 = register_values(cfg, im[0].instructions[2], 0); + + expect(r0.size()).toBe(Math.pow(2, 32)); + expect(r0.min()).toBe(-Math.pow(2, 31)); + expect(r0.max()).toBe(Math.pow(2, 31) - 1); + + const r1 = register_values(cfg, im[0].instructions[2], 1); + + expect(r1.size()).toBe(Math.pow(2, 32)); + expect(r1.min()).toBe(-Math.pow(2, 31)); + expect(r1.max()).toBe(Math.pow(2, 31) - 1); +}); + +test(`${register_values.name} rev`, () => { + const im = to_instructions(` + 0: + leti r0, 10 + leti r1, 50 + get_random r0, r10 + rev r10 + leti r0, -10 + leti r1, 50 + get_random r0, r10 + rev r10 + leti r10, 0 + rev r10 + ret + `); + const cfg = ControlFlowGraph.create(im); + const v0 = register_values(cfg, im[0].instructions[4], 10); + + expect(v0.size()).toBe(1); + expect(v0.get(0)).toBe(0); + + const v1 = register_values(cfg, im[0].instructions[8], 10); + + expect(v1.size()).toBe(2); + expect(v1.to_array()).toEqual([0, 1]); + + const v2 = register_values(cfg, im[0].instructions[10], 10); + + expect(v2.size()).toBe(1); + expect(v2.get(0)).toBe(1); +}); + +/** + * Test an instruction taking a register and an integer. + * The instruction will be called with arguments r99, 15. r99 will be set to 10 or 20. + */ +function test_branched(opcode: Opcode, ...expected: number[]): void { + test(`${register_values.name} ${opcode.mnemonic}`, () => { + const im = to_instructions(` + 0: + leti r99, 10 + jmpi_= r0, 100, 1 + leti r99, 20 + 1: + ${opcode.mnemonic} r99, 15 + ret + `); + const cfg = ControlFlowGraph.create(im); + const values = register_values(cfg, im[1].instructions[1], 99); + + expect(values.size()).toBe(expected.length); + expect(values.to_array()).toEqual(expected); + }); +} + +test_branched(Opcode.addi, 25, 35); +test_branched(Opcode.subi, -5, 5); +test_branched(Opcode.muli, 150, 300); +test_branched(Opcode.divi, 0, 1); + +test(`${register_values.name} get_random`, () => { + const im = to_instructions(` + 0: + leti r0, 20 + leti r1, 20 + get_random r0, r10 + leti r1, 19 + get_random r0, r10 + leti r1, 25 + get_random r0, r10 + ret + `); + const cfg = ControlFlowGraph.create(im); + const v0 = register_values(cfg, im[0].instructions[3], 10); + + expect(v0.size()).toBe(1); + expect(v0.get(0)).toBe(20); + + const v1 = register_values(cfg, im[0].instructions[5], 10); + + expect(v0.size()).toBe(1); + expect(v0.get(0)).toBe(20); + + const v2 = register_values(cfg, im[0].instructions[7], 10); + + expect(v2.size()).toBe(5); + expect(v2.to_array()).toEqual([20, 21, 22, 23, 24]); +}); + +function to_instructions(assembly: string): InstructionSegment[] { + const { object_code, warnings, errors } = assemble(assembly.split("\n")); + + expect(warnings).toEqual([]); + expect(errors).toEqual([]); + + return object_code.filter( + segment => segment.type === SegmentType.Instructions + ) as InstructionSegment[]; +} diff --git a/src/scripting/data_flow_analysis/register_values.ts b/src/scripting/data_flow_analysis/register_values.ts new file mode 100644 index 00000000..5f6ca994 --- /dev/null +++ b/src/scripting/data_flow_analysis/register_values.ts @@ -0,0 +1,130 @@ +import { Instruction, Opcode } from "../../data_formats/parsing/quest/bin"; +import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph"; +import { ValueSet } from "./ValueSet"; + +const MIN_REGISTER_VALUE = -Math.pow(2, 31); +const MAX_REGISTER_VALUE = Math.pow(2, 31) - 1; + +/** + * Computes the possible values of a register at a specific instruction. + */ +export function register_values( + cfg: ControlFlowGraph, + instruction: Instruction, + register: number +): ValueSet { + const block = cfg.instructions.get(instruction); + + if (block) { + let inst_idx = block.start; + + while (inst_idx < block.end) { + if (block.segment.instructions[inst_idx] === instruction) { + break; + } + + inst_idx++; + } + + return find_values(block, inst_idx, register); + } else { + return new ValueSet(); + } +} + +function find_values(block: BasicBlock, end: number, register: number): ValueSet { + let values = new ValueSet(); + + for (let i = block.start; i < end; i++) { + const instruction = block.segment.instructions[i]; + const args = instruction.args; + + switch (instruction.opcode) { + case Opcode.let: + if (args[0].value === register) { + values = find_values(block, i, args[1].value); + } + break; + case Opcode.leti: + case Opcode.letb: + case Opcode.letw: + if (args[0].value === register) { + values.set_value(args[1].value); + } + break; + case Opcode.leta: + case Opcode.leto: + if (args[0].value === register) { + values.set_interval(MIN_REGISTER_VALUE, MAX_REGISTER_VALUE); + } + break; + case Opcode.set: + if (args[0].value === register) { + values.set_value(1); + } + break; + case Opcode.clear: + if (args[0].value === register) { + values.set_value(0); + } + break; + case Opcode.rev: + if (args[0].value === register) { + const prev_vals = find_values(block, i, register); + const prev_size = prev_vals.size(); + + if (prev_size === 0 || (prev_size === 1 && prev_vals.get(0) === 0)) { + values.set_value(1); + } else if (values.has(0)) { + values.set_interval(0, 1); + } else { + values.set_value(0); + } + } + break; + case Opcode.addi: + if (args[0].value === register) { + values = find_values(block, i, register); + values.scalar_add(args[1].value); + } + break; + case Opcode.subi: + if (args[0].value === register) { + values = find_values(block, i, register); + values.scalar_sub(args[1].value); + } + break; + case Opcode.muli: + if (args[0].value === register) { + values = find_values(block, i, register); + values.scalar_mul(args[1].value); + } + break; + case Opcode.divi: + if (args[0].value === register) { + values = find_values(block, i, register); + values.scalar_div(args[1].value); + } + break; + case Opcode.get_random: + if (args[1].value === register) { + // TODO: undefined values. + const min = find_values(block, i, args[0].value).min() || 0; + const max = Math.max( + find_values(block, i, args[0].value + 1).max() || 0, + min + 1 + ); + values.set_interval(min, max - 1); + } + break; + } + } + + if (values.size() === 0) { + for (const from of block.from) { + values.union(find_values(from, from.end, register)); + } + } + + return values; +}