diff --git a/FEATURES.md b/FEATURES.md index 1767c08d..b5d78839 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -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 diff --git a/src/data_formats/parsing/quest/opcodes.ts b/src/data_formats/parsing/quest/opcodes.ts index b628343e..0fa3e80b 100644 --- a/src/data_formats/parsing/quest/opcodes.ts +++ b/src/data_formats/parsing/quest/opcodes.ts @@ -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, [] diff --git a/src/scripting/ValueSet.test.ts b/src/scripting/ValueSet.test.ts new file mode 100644 index 00000000..93d2365f --- /dev/null +++ b/src/scripting/ValueSet.test.ts @@ -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]); +}); diff --git a/src/scripting/ValueSet.ts b/src/scripting/ValueSet.ts new file mode 100644 index 00000000..bbac94d3 --- /dev/null +++ b/src/scripting/ValueSet.ts @@ -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 { + const vs = this; + let int_i = 0; + let value = NaN; + + return { + next(): IteratorResult { + 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++ }; + }, + }; + } +} diff --git a/src/scripting/data_flow_analysis.test.ts b/src/scripting/data_flow_analysis.test.ts index 4f0d0d2e..c0ff4a72 100644 --- a/src/scripting/data_flow_analysis.test.ts +++ b/src/scripting/data_flow_analysis.test.ts @@ -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[] { diff --git a/src/scripting/data_flow_analysis.ts b/src/scripting/data_flow_analysis.ts index e1eeed49..54bb98cb 100644 --- a/src/scripting/data_flow_analysis.ts +++ b/src/scripting/data_flow_analysis.ts @@ -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 = 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(); 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, + 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): 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: