Added ValueSet, an efficient integer set.

This commit is contained in:
Daan Vanden Bosch 2019-08-01 19:37:26 +02:00
parent d9ff4308e0
commit 76c3169fd3
6 changed files with 343 additions and 106 deletions

View File

@ -28,7 +28,7 @@ Features that are in ***bold italics*** are planned and not yet implemented.
## Area Selection
- Dropdown to switch area
- Dropdown menu to switch area
## Simple Quest Properties
@ -51,6 +51,7 @@ Features that are in ***bold italics*** are planned and not yet implemented.
- ***Top-down view (orthogonal view might suffice?)***
- ***Add "shadow" to entities to more easily see where floating entities are positioned***
- ***MVP: a single line***
- ***Show positions and radii from the relevant script instructions***
## NPC/object manipulation

View File

@ -2922,16 +2922,16 @@ export class Opcode {
false,
[]
));
static readonly unknown_f8e6 = (OPCODES[0xf8e6] = new Opcode(
static readonly move_coords_object = (OPCODES[0xf8e6] = new Opcode(
0xf8e6,
"unknown_f8e6",
"move_coords_object ",
[{ type: Type.Register }, { type: Type.Register }],
false,
[]
));
static readonly unknown_f8e7 = (OPCODES[0xf8e7] = new Opcode(
static readonly at_coords_call_ex = (OPCODES[0xf8e7] = new Opcode(
0xf8e7,
"unknown_f8e7",
"at_coords_call_ex",
[{ type: Type.Register }, { type: Type.Register }],
false,
[]

View File

@ -0,0 +1,55 @@
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]);
});

112
src/scripting/ValueSet.ts Normal file
View File

@ -0,0 +1,112 @@
/**
* Represents a set of integers.
*/
export class ValueSet {
/**
* Open intervals [start, end[.
*/
private intervals: { start: number; end: number }[] = [];
size(): number {
return this.intervals.reduce((acc, i) => acc + i.end - i.start, 0);
}
/**
* Sets this ValueSet to the given integer.
*
* @param value integer value
*/
set_value(value: number): ValueSet {
this.intervals = [{ start: value, end: value + 1 }];
return this;
}
/**
* Sets this ValueSet to the values in the given interval.
*
* @param start lower bound, inclusive
* @param end upper bound, exclusive
*/
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}[.`
);
if (end !== start) {
this.intervals = [{ start, end }];
}
return this;
}
union(other: ValueSet): ValueSet {
let i = 0;
outer: for (const b of other.intervals) {
while (i < this.intervals.length) {
const a = this.intervals[i];
if (b.end < a.start) {
this.intervals.splice(i, 0, b);
i++;
continue outer;
} else if (b.start <= a.end) {
a.start = Math.min(a.start, b.start);
let j = i;
while (j < this.intervals.length) {
if (b.end > this.intervals[j].start) {
a.end = this.intervals[j].end;
j++;
} else {
break;
}
}
this.intervals.splice(i + 1, j - i - 1);
a.end = Math.max(a.end, b.end);
i++;
continue outer;
} else {
i++;
}
}
this.intervals.push(b);
}
return this;
}
[Symbol.iterator](): Iterator<number> {
const vs = this;
let int_i = 0;
let value = NaN;
return {
next(): IteratorResult<number> {
let done = true;
if (int_i < vs.intervals.length) {
if (isNaN(value)) {
value = vs.intervals[int_i].start;
done = false;
} else if (value >= vs.intervals[int_i].end) {
int_i++;
if (int_i < vs.intervals.length) {
value = vs.intervals[int_i].start;
done = false;
}
} else {
done = false;
}
}
return { done, value: value++ };
},
};
}
}

View File

@ -9,14 +9,14 @@ test("single instruction", () => {
`);
const cfg = create_control_flow_graph(im);
expect(cfg.nodes.length).toBe(1);
expect(cfg.blocks.length).toBe(1);
expect(cfg.nodes[0].start).toBe(0);
expect(cfg.nodes[0].end).toBe(1);
expect(cfg.nodes[0].branch_type).toBe(BranchType.Return);
expect(cfg.nodes[0].from.length).toBe(0);
expect(cfg.nodes[0].to.length).toBe(0);
expect(cfg.nodes[0].branch_labels.length).toBe(0);
expect(cfg.blocks[0].start).toBe(0);
expect(cfg.blocks[0].end).toBe(1);
expect(cfg.blocks[0].branch_type).toBe(BranchType.Return);
expect(cfg.blocks[0].from.length).toBe(0);
expect(cfg.blocks[0].to.length).toBe(0);
expect(cfg.blocks[0].branch_labels.length).toBe(0);
});
test("single unconditional jump", () => {
@ -28,21 +28,21 @@ test("single unconditional jump", () => {
`);
const cfg = create_control_flow_graph(im);
expect(cfg.nodes.length).toBe(2);
expect(cfg.blocks.length).toBe(2);
expect(cfg.nodes[0].start).toBe(0);
expect(cfg.nodes[0].end).toBe(1);
expect(cfg.nodes[0].branch_type).toBe(BranchType.Jump);
expect(cfg.nodes[0].from.length).toBe(0);
expect(cfg.nodes[0].to.length).toBe(1);
expect(cfg.nodes[0].branch_labels.length).toBe(1);
expect(cfg.blocks[0].start).toBe(0);
expect(cfg.blocks[0].end).toBe(1);
expect(cfg.blocks[0].branch_type).toBe(BranchType.Jump);
expect(cfg.blocks[0].from.length).toBe(0);
expect(cfg.blocks[0].to.length).toBe(1);
expect(cfg.blocks[0].branch_labels.length).toBe(1);
expect(cfg.nodes[1].start).toBe(0);
expect(cfg.nodes[1].end).toBe(1);
expect(cfg.nodes[1].branch_type).toBe(BranchType.Return);
expect(cfg.nodes[1].from.length).toBe(1);
expect(cfg.nodes[1].to.length).toBe(0);
expect(cfg.nodes[1].branch_labels.length).toBe(0);
expect(cfg.blocks[1].start).toBe(0);
expect(cfg.blocks[1].end).toBe(1);
expect(cfg.blocks[1].branch_type).toBe(BranchType.Return);
expect(cfg.blocks[1].from.length).toBe(1);
expect(cfg.blocks[1].to.length).toBe(0);
expect(cfg.blocks[1].branch_labels.length).toBe(0);
});
test("single conditional jump", () => {
@ -55,28 +55,28 @@ test("single conditional jump", () => {
`);
const cfg = create_control_flow_graph(im);
expect(cfg.nodes.length).toBe(3);
expect(cfg.blocks.length).toBe(3);
expect(cfg.nodes[0].start).toBe(0);
expect(cfg.nodes[0].end).toBe(1);
expect(cfg.nodes[0].branch_type).toBe(BranchType.ConditionalJump);
expect(cfg.nodes[0].from.length).toBe(0);
expect(cfg.nodes[0].to.length).toBe(2);
expect(cfg.nodes[0].branch_labels.length).toBe(1);
expect(cfg.blocks[0].start).toBe(0);
expect(cfg.blocks[0].end).toBe(1);
expect(cfg.blocks[0].branch_type).toBe(BranchType.ConditionalJump);
expect(cfg.blocks[0].from.length).toBe(0);
expect(cfg.blocks[0].to.length).toBe(2);
expect(cfg.blocks[0].branch_labels.length).toBe(1);
expect(cfg.nodes[1].start).toBe(1);
expect(cfg.nodes[1].end).toBe(2);
expect(cfg.nodes[1].branch_type).toBe(BranchType.Return);
expect(cfg.nodes[1].from.length).toBe(1);
expect(cfg.nodes[1].to.length).toBe(0);
expect(cfg.nodes[1].branch_labels.length).toBe(0);
expect(cfg.blocks[1].start).toBe(1);
expect(cfg.blocks[1].end).toBe(2);
expect(cfg.blocks[1].branch_type).toBe(BranchType.Return);
expect(cfg.blocks[1].from.length).toBe(1);
expect(cfg.blocks[1].to.length).toBe(0);
expect(cfg.blocks[1].branch_labels.length).toBe(0);
expect(cfg.nodes[2].start).toBe(0);
expect(cfg.nodes[2].end).toBe(1);
expect(cfg.nodes[2].branch_type).toBe(BranchType.Return);
expect(cfg.nodes[2].from.length).toBe(1);
expect(cfg.nodes[2].to.length).toBe(0);
expect(cfg.nodes[2].branch_labels.length).toBe(0);
expect(cfg.blocks[2].start).toBe(0);
expect(cfg.blocks[2].end).toBe(1);
expect(cfg.blocks[2].branch_type).toBe(BranchType.Return);
expect(cfg.blocks[2].from.length).toBe(1);
expect(cfg.blocks[2].to.length).toBe(0);
expect(cfg.blocks[2].branch_labels.length).toBe(0);
});
test("single call", () => {
@ -89,31 +89,31 @@ test("single call", () => {
`);
const cfg = create_control_flow_graph(im);
expect(cfg.nodes.length).toBe(3);
expect(cfg.blocks.length).toBe(3);
expect(cfg.nodes[0].start).toBe(0);
expect(cfg.nodes[0].end).toBe(1);
expect(cfg.nodes[0].branch_type).toBe(BranchType.Call);
expect(cfg.nodes[0].from.length).toBe(0);
expect(cfg.nodes[0].to.length).toBe(1);
expect(cfg.nodes[0].branch_labels.length).toBe(1);
expect(cfg.blocks[0].start).toBe(0);
expect(cfg.blocks[0].end).toBe(1);
expect(cfg.blocks[0].branch_type).toBe(BranchType.Call);
expect(cfg.blocks[0].from.length).toBe(0);
expect(cfg.blocks[0].to.length).toBe(1);
expect(cfg.blocks[0].branch_labels.length).toBe(1);
expect(cfg.nodes[1].start).toBe(1);
expect(cfg.nodes[1].end).toBe(2);
expect(cfg.nodes[1].branch_type).toBe(BranchType.Return);
expect(cfg.nodes[1].from.length).toBe(1);
expect(cfg.nodes[1].to.length).toBe(0);
expect(cfg.nodes[1].branch_labels.length).toBe(0);
expect(cfg.blocks[1].start).toBe(1);
expect(cfg.blocks[1].end).toBe(2);
expect(cfg.blocks[1].branch_type).toBe(BranchType.Return);
expect(cfg.blocks[1].from.length).toBe(1);
expect(cfg.blocks[1].to.length).toBe(0);
expect(cfg.blocks[1].branch_labels.length).toBe(0);
expect(cfg.nodes[2].start).toBe(0);
expect(cfg.nodes[2].end).toBe(1);
expect(cfg.nodes[2].branch_type).toBe(BranchType.Return);
expect(cfg.nodes[2].from.length).toBe(1);
expect(cfg.nodes[2].to.length).toBe(1);
expect(cfg.nodes[2].branch_labels.length).toBe(0);
expect(cfg.blocks[2].start).toBe(0);
expect(cfg.blocks[2].end).toBe(1);
expect(cfg.blocks[2].branch_type).toBe(BranchType.Return);
expect(cfg.blocks[2].from.length).toBe(1);
expect(cfg.blocks[2].to.length).toBe(1);
expect(cfg.blocks[2].branch_labels.length).toBe(0);
});
test("conditional branch with fall-through", () => {
test("conditional jump with fall-through", () => {
const im = to_instructions(`
0:
jmp_> r1, r2, 1
@ -124,28 +124,28 @@ test("conditional branch with fall-through", () => {
`);
const cfg = create_control_flow_graph(im);
expect(cfg.nodes.length).toBe(3);
expect(cfg.blocks.length).toBe(3);
expect(cfg.nodes[0].start).toBe(0);
expect(cfg.nodes[0].end).toBe(1);
expect(cfg.nodes[0].branch_type).toBe(BranchType.ConditionalJump);
expect(cfg.nodes[0].from.length).toBe(0);
expect(cfg.nodes[0].to.length).toBe(2);
expect(cfg.nodes[0].branch_labels.length).toBe(1);
expect(cfg.blocks[0].start).toBe(0);
expect(cfg.blocks[0].end).toBe(1);
expect(cfg.blocks[0].branch_type).toBe(BranchType.ConditionalJump);
expect(cfg.blocks[0].from.length).toBe(0);
expect(cfg.blocks[0].to.length).toBe(2);
expect(cfg.blocks[0].branch_labels.length).toBe(1);
expect(cfg.nodes[1].start).toBe(1);
expect(cfg.nodes[1].end).toBe(2);
expect(cfg.nodes[1].branch_type).toBe(BranchType.None);
expect(cfg.nodes[1].from.length).toBe(1);
expect(cfg.nodes[1].to.length).toBe(1);
expect(cfg.nodes[1].branch_labels.length).toBe(0);
expect(cfg.blocks[1].start).toBe(1);
expect(cfg.blocks[1].end).toBe(2);
expect(cfg.blocks[1].branch_type).toBe(BranchType.None);
expect(cfg.blocks[1].from.length).toBe(1);
expect(cfg.blocks[1].to.length).toBe(1);
expect(cfg.blocks[1].branch_labels.length).toBe(0);
expect(cfg.nodes[2].start).toBe(0);
expect(cfg.nodes[2].end).toBe(2);
expect(cfg.nodes[2].branch_type).toBe(BranchType.Return);
expect(cfg.nodes[2].from.length).toBe(2);
expect(cfg.nodes[2].to.length).toBe(0);
expect(cfg.nodes[2].branch_labels.length).toBe(0);
expect(cfg.blocks[2].start).toBe(0);
expect(cfg.blocks[2].end).toBe(2);
expect(cfg.blocks[2].branch_type).toBe(BranchType.Return);
expect(cfg.blocks[2].from.length).toBe(2);
expect(cfg.blocks[2].to.length).toBe(0);
expect(cfg.blocks[2].branch_labels.length).toBe(0);
});
function to_instructions(assembly: string): InstructionSegment[] {

View File

@ -1,10 +1,11 @@
import {
Instruction,
InstructionSegment,
Opcode,
Segment,
SegmentType,
Instruction,
} from "../data_formats/parsing/quest/bin";
import { ValueSet } from "./ValueSet";
export enum BranchType {
None,
@ -36,38 +37,93 @@ export class BasicBlock {
}
export class ControlFlowGraph {
readonly nodes: BasicBlock[] = [];
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.
*/
// Mapping of labels to basic blocks.
const label_blocks = new Map<number, BasicBlock>();
for (const segment of segments) {
const blocks = create_basic_blocks(segment);
if (blocks.length) {
cfg.nodes.push(...blocks);
for (const label of segment.labels) {
label_blocks.set(label, blocks[0]);
}
}
create_basic_blocks(cfg, label_blocks, segment);
}
link_blocks(cfg, label_blocks);
return cfg;
}
function create_basic_blocks(segment: InstructionSegment): BasicBlock[] {
const blocks: BasicBlock[] = [];
/**
* 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++;
}
return find_value_set(block, inst_idx, register);
} else {
return new ValueSet();
}
}
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>,
segment: InstructionSegment
) {
const len = segment.instructions.length;
let start = 0;
let first_block = true;
for (let i = start; i < len; i++) {
for (let i = 0; i < len; i++) {
const inst = segment.instructions[i];
let branch_type: BranchType;
@ -145,20 +201,33 @@ function create_basic_blocks(segment: InstructionSegment): BasicBlock[] {
}
}
blocks.push(new BasicBlock(segment, start, i + 1, branch_type, branch_labels));
const block = new BasicBlock(segment, start, i + 1, branch_type, branch_labels);
for (let j = block.start; j < block.end; j++) {
cfg.instructions.set(block.segment.instructions[j], block);
}
cfg.blocks.push(block);
if (first_block) {
for (const label of segment.labels) {
label_blocks.set(label, block);
}
first_block = false;
}
start = i + 1;
}
return blocks;
}
function link_blocks(cfg: ControlFlowGraph, label_blocks: Map<number, BasicBlock>): void {
// Pairs of calling block and block to which callees should return to.
const callers: [BasicBlock, BasicBlock][] = [];
for (let i = 0; i < cfg.nodes.length; i++) {
const block = cfg.nodes[i];
const next_block = cfg.nodes[i + 1];
for (let i = 0; i < cfg.blocks.length; i++) {
const block = cfg.blocks[i];
const next_block = cfg.blocks[i + 1];
switch (block.branch_type) {
case BranchType.Return: