Added basic algorithm to compute the possible values of a register at a specific program point.

This commit is contained in:
Daan Vanden Bosch 2019-08-02 00:13:19 +02:00
parent 76c3169fd3
commit 83d32dfe99
8 changed files with 489 additions and 145 deletions

View File

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

View File

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

View File

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

View File

@ -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<Instruction, BasicBlock> = 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<number, BasicBlock>();
static create(segments: InstructionSegment[]): ControlFlowGraph {
const cfg = new ControlFlowGraph();
// Mapping of labels to basic blocks.
const label_blocks = new Map<number, BasicBlock>();
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<number, BasicBlock>,

View File

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

View File

@ -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<number> {
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) {

View File

@ -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[];
}

View File

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