diff --git a/src/data_formats/parsing/quest/opcodes.ts b/src/data_formats/parsing/quest/opcodes.ts index 8ccd66be..b628343e 100644 --- a/src/data_formats/parsing/quest/opcodes.ts +++ b/src/data_formats/parsing/quest/opcodes.ts @@ -122,10 +122,34 @@ export class Opcode { false, [] )); - static readonly unknown_0a = (OPCODES[0x0a] = new Opcode(0x0a, "unknown_0a", [], false, [])); - static readonly unknown_0b = (OPCODES[0x0b] = new Opcode(0x0b, "unknown_0b", [], false, [])); - static readonly unknown_0c = (OPCODES[0x0c] = new Opcode(0x0c, "unknown_0c", [], false, [])); - static readonly unknown_0d = (OPCODES[0x0d] = new Opcode(0x0d, "unknown_0d", [], false, [])); + static readonly letb = (OPCODES[0x0a] = new Opcode( + 0x0a, + "letb", + [{ type: Type.Register }, { type: Type.U8 }], + false, + [] + )); + static readonly letw = (OPCODES[0x0b] = new Opcode( + 0x0b, + "letw", + [{ type: Type.Register }, { type: Type.U16 }], + false, + [] + )); + static readonly leta = (OPCODES[0x0c] = new Opcode( + 0x0c, + "leta", + [{ type: Type.Register }, { type: Type.Register }], + false, + [] + )); + static readonly leto = (OPCODES[0x0d] = new Opcode( + 0x0d, + "leto", + [{ type: Type.Register }, { type: Type.U16 /* ILabel or DLabel */ }], + false, + [] + )); static readonly unknown_0e = (OPCODES[0x0e] = new Opcode(0x0e, "unknown_0e", [], false, [])); static readonly unknown_0f = (OPCODES[0x0f] = new Opcode(0x0f, "unknown_0f", [], false, [])); static readonly set = (OPCODES[0x10] = new Opcode( @@ -1262,7 +1286,7 @@ export class Opcode { )); static readonly sync_register = (OPCODES[0xef] = new Opcode(0xef, "sync_register", [], false, [ { type: Type.Register }, - { type: Type.U32 }, + { type: Type.U32 /* TODO: Can be U32 or Register. */ }, ])); static readonly send_regwork = (OPCODES[0xf0] = new Opcode( 0xf0, diff --git a/src/scripting/data_flow_analysis.test.ts b/src/scripting/data_flow_analysis.test.ts new file mode 100644 index 00000000..4f0d0d2e --- /dev/null +++ b/src/scripting/data_flow_analysis.test.ts @@ -0,0 +1,160 @@ +import { InstructionSegment, SegmentType } from "../data_formats/parsing/quest/bin"; +import { assemble } from "./assembly"; +import { create_control_flow_graph, BranchType } from "./data_flow_analysis"; + +test("single instruction", () => { + const im = to_instructions(` + 0: + ret + `); + const cfg = create_control_flow_graph(im); + + expect(cfg.nodes.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); +}); + +test("single unconditional jump", () => { + const im = to_instructions(` + 0: + jmp 1 + 1: + ret + `); + const cfg = create_control_flow_graph(im); + + expect(cfg.nodes.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.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); +}); + +test("single conditional jump", () => { + const im = to_instructions(` + 0: + jmp_= r1, r2, 1 + ret + 1: + ret + `); + const cfg = create_control_flow_graph(im); + + expect(cfg.nodes.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.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.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); +}); + +test("single call", () => { + const im = to_instructions(` + 0: + call 1 + ret + 1: + ret + `); + const cfg = create_control_flow_graph(im); + + expect(cfg.nodes.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.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.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); +}); + +test("conditional branch with fall-through", () => { + const im = to_instructions(` + 0: + jmp_> r1, r2, 1 + nop + 1: + nop + ret + `); + const cfg = create_control_flow_graph(im); + + expect(cfg.nodes.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.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.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); +}); + +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.ts b/src/scripting/data_flow_analysis.ts new file mode 100644 index 00000000..e1eeed49 --- /dev/null +++ b/src/scripting/data_flow_analysis.ts @@ -0,0 +1,477 @@ +import { + Instruction, + InstructionSegment, + Opcode, + Segment, + SegmentType, +} from "../data_formats/parsing/quest/bin"; + +export enum BranchType { + None, + Return, + Jump, + ConditionalJump, + Call, +} + +export class BasicBlock { + readonly from: BasicBlock[] = []; + readonly to: BasicBlock[] = []; + + constructor( + readonly segment: InstructionSegment, + readonly start: number, + readonly end: number, + readonly branch_type: BranchType, + /** + * Either jumps or calls, depending on `branch_type`. + */ + readonly branch_labels: number[] + ) {} + + link_to(other: BasicBlock): void { + this.to.push(other); + other.from.push(this); + } +} + +export class ControlFlowGraph { + readonly nodes: BasicBlock[] = []; +} + +export function create_control_flow_graph(segments: InstructionSegment[]): ControlFlowGraph { + const cfg = new ControlFlowGraph(); + /** + * 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]); + } + } + } + + link_blocks(cfg, label_blocks); + return cfg; +} + +function create_basic_blocks(segment: InstructionSegment): BasicBlock[] { + const blocks: BasicBlock[] = []; + const len = segment.instructions.length; + let start = 0; + + for (let i = start; i < len; i++) { + const inst = segment.instructions[i]; + + let branch_type: BranchType; + let branch_labels: number[]; + + switch (inst.opcode) { + // Return. + case Opcode.ret: + branch_type = BranchType.Return; + branch_labels = []; + break; + + // Unconditional jump. + case Opcode.jmp: + branch_type = BranchType.Jump; + branch_labels = [inst.args[0].value]; + break; + + // Conditional jumps. + case Opcode.jmp_on: + case Opcode.jmp_off: + branch_type = BranchType.ConditionalJump; + branch_labels = [inst.args[0].value]; + break; + case Opcode.jmp_e: + case Opcode.jmpi_e: + case Opcode.jmp_ne: + case Opcode.jmpi_ne: + case Opcode.ujmp_g: + case Opcode.ujmpi_g: + case Opcode.jmp_g: + case Opcode.jmpi_g: + case Opcode.ujmp_l: + case Opcode.ujmpi_l: + case Opcode.jmp_l: + case Opcode.jmpi_l: + case Opcode.ujmp_ge: + case Opcode.ujmpi_ge: + case Opcode.jmp_ge: + case Opcode.jmpi_ge: + case Opcode.ujmp_le: + case Opcode.ujmpi_le: + case Opcode.jmp_le: + case Opcode.jmpi_le: + branch_type = BranchType.ConditionalJump; + branch_labels = [inst.args[2].value]; + break; + case Opcode.switch_jmp: + branch_type = BranchType.ConditionalJump; + branch_labels = inst.args.slice(1).map(a => a.value); + break; + + // Calls. + case Opcode.call: + branch_type = BranchType.Call; + branch_labels = [inst.args[0].value]; + break; + case Opcode.va_call: + branch_type = BranchType.Call; + branch_labels = [inst.args[0].value]; + break; + case Opcode.switch_call: + branch_type = BranchType.Call; + branch_labels = inst.args.slice(1).map(a => a.value); + break; + + // All other opcodes. + default: + if (i === len - 1) { + branch_type = BranchType.None; + branch_labels = []; + break; + } else { + continue; + } + } + + blocks.push(new BasicBlock(segment, start, i + 1, branch_type, branch_labels)); + 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]; + + switch (block.branch_type) { + case BranchType.Return: + continue; + case BranchType.Call: + if (next_block) { + callers.push([block, next_block]); + } + break; + case BranchType.None: + case BranchType.ConditionalJump: + if (next_block) { + block.link_to(next_block); + } + break; + } + + for (const label of block.branch_labels) { + const to_block = label_blocks.get(label); + + if (to_block) { + block.link_to(to_block); + } + } + } + + for (const [caller, ret] of callers) { + link_returning_blocks(label_blocks, ret, caller); + } +} + +/** + * Links returning blocks to their callers. + */ +function link_returning_blocks( + label_blocks: Map, + ret: BasicBlock, + block: BasicBlock +): void { + for (const label of block.branch_labels) { + const sub_block = label_blocks.get(label); + + if (sub_block) { + if (sub_block.branch_type === BranchType.Return) { + sub_block.link_to(ret); + } + + link_returning_blocks(label_blocks, ret, sub_block); + } + } +} + +///////////////// +// Crap: // +///////////////// + +class DfState { + private registers: DataView; + + constructor(other?: DfState) { + if (other) { + this.registers = new DataView(other.registers.buffer.slice(0)); + } else { + this.registers = new DataView(new ArrayBuffer(2 * 4 * 256)); + } + } + + get_min(register: number): number { + return this.registers.getInt32(2 * register); + } + + get_max(register: number): number { + return this.registers.getInt32(2 * register + 1); + } + + set(register: number, min: number, max: number): void { + this.registers.setInt32(2 * register, min); + this.registers.setInt32(2 * register + 1, max); + } + + // getf(register: number): number { + // return this.registers.getFloat32(2 * register); + // } + + // setf(register: number, value: number): void { + // this.registers.setFloat32(2 * register, value); + // this.registers.setFloat32(2 * register + 1, value); + // } +} + +/** + * @param segments mapping of labels to segments. + */ +function data_flow( + label_holder: any, + segments: Map, + entry_label: number, + entry_state: DfState +): void { + const segment = segments.get(entry_label); + if (!segment || segment.type !== SegmentType.Instructions) return; + + let out_states: DfState[] = [new DfState(entry_state)]; + + for (const instruction of segment.instructions) { + const args = instruction.args; + + for (const state of out_states) { + switch (instruction.opcode) { + case Opcode.let: + case Opcode.flet: + state.set( + args[0].value, + state.get_min(args[1].value), + state.get_max(args[1].value) + ); + break; + case Opcode.leti: + case Opcode.letb: + case Opcode.letw: + case Opcode.leta: + case Opcode.sync_leti: + case Opcode.sync_register: + state.set(args[0].value, args[1].value, args[1].value); + break; + case Opcode.leto: + { + const info = label_holder.get_info(args[1].value); + state.set(args[0].value, info ? info.offset : 0, info ? info.offset : 0); + } + break; + case Opcode.set: + state.set(args[0].value, 1, 1); + break; + case Opcode.clear: + state.set(args[0].value, 0, 0); + break; + case Opcode.leti: + case Opcode.letb: + case Opcode.letw: + case Opcode.leta: + case Opcode.sync_leti: + case Opcode.sync_register: + state.set(args[0].value, args[1].value, args[1].value); + break; + // case Opcode.fleti: + // state.setf(args[0].value, args[1].value); + // break; + case Opcode.rev: + { + const reg = args[0].value; + const max = state.get_min(reg) <= 0 && state.get_max(reg) >= 0 ? 1 : 0; + const min = state.get_min(reg) === 0 && state.get_max(reg) === 0 ? 1 : 0; + state.set(reg, min, max); + } + break; + // case Opcode.add: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) + state.get_min(args[1].value)); + // } + // break; + // case Opcode.addi: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) + args[1].value); + // } + // break; + // case Opcode.sub: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) - state.get_min(args[1].value)); + // } + // break; + // case Opcode.subi: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) - args[1].value); + // } + // break; + // case Opcode.mul: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) * state.get_min(args[1].value)); + // } + // break; + // case Opcode.muli: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) * args[1].value); + // } + // break; + // case Opcode.div: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) / state.get_min(args[1].value)); + // } + // break; + // case Opcode.divi: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) / args[1].value); + // } + // break; + // case Opcode.and: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) & state.get_min(args[1].value)); + // } + // break; + // case Opcode.andi: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) & args[1].value); + // } + // break; + // case Opcode.or: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) | state.get_min(args[1].value)); + // } + // break; + // case Opcode.ori: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) | args[1].value); + // } + // break; + // case Opcode.xor: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) ^ state.get_min(args[1].value)); + // } + // break; + // case Opcode.xori: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) ^ args[1].value); + // } + // break; + // case Opcode.mod: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) % state.get_min(args[1].value)); + // } + // break; + // case Opcode.modi: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) % args[1].value); + // } + // break; + // case Opcode.shift_left: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) << state.get_min(args[1].value)); + // } + // break; + // case Opcode.shift_right: + // { + // const reg = args[0].value; + // state.set(reg, state.get_min(reg) >> state.get_min(args[1].value)); + // } + // break; + // case Opcode.fadd: + // { + // const reg = args[0].value; + // state.setf(reg, state.getf(reg) + state.getf(args[1].value)); + // } + // break; + // case Opcode.faddi: + // { + // const reg = args[0].value; + // state.setf(reg, state.getf(reg) + args[1].value); + // } + // break; + // case Opcode.fsub: + // { + // const reg = args[0].value; + // state.setf(reg, state.getf(reg) - state.getf(args[1].value)); + // } + // break; + // case Opcode.fsubi: + // { + // const reg = args[0].value; + // state.setf(reg, state.getf(reg) - args[1].value); + // } + // break; + // case Opcode.fmul: + // { + // const reg = args[0].value; + // state.setf(reg, state.getf(reg) * state.getf(args[1].value)); + // } + // break; + // case Opcode.fmuli: + // { + // const reg = args[0].value; + // state.setf(reg, state.getf(reg) * args[1].value); + // } + // break; + // case Opcode.fdiv: + // { + // const reg = args[0].value; + // state.setf(reg, state.getf(reg) / state.getf(args[1].value)); + // } + // break; + // case Opcode.fdivi: + // { + // const reg = args[0].value; + // state.setf(reg, state.getf(reg) / args[1].value); + // } + // break; + } + } + } +}