Created yaml file with opcode definitions (70% complete).

This commit is contained in:
Daan Vanden Bosch 2019-08-03 23:23:55 +02:00
parent 83d32dfe99
commit fba6ac3e10
11 changed files with 9348 additions and 5106 deletions

View File

@ -116,8 +116,13 @@ Features that are in ***bold italics*** are planned and not yet implemented.
- ***Figure out how they work*** - ***Figure out how they work***
## Non-BlueBurst Support
- Support different sets of instructions (older versions had no stack)
## Bugs ## Bugs
- [Script Object Code](#script-object-code): Correctly deal with stack arguments (e.g. when a function expects a u32, pushing a u8, u16, u32 or register value is ok) (when a function expects a register reference, arg_pushb should be used)
- [3D View](#3d-view): Random Type Box 1 and Fixed Type Box objects aren't rendered correctly - [3D View](#3d-view): Random Type Box 1 and Fixed Type Box objects aren't rendered correctly
- [3D View](#3d-view): Some objects are only partially loaded (they consist of several seperate models) - [3D View](#3d-view): Some objects are only partially loaded (they consist of several seperate models)
- Forest Switch - Forest Switch

View File

@ -0,0 +1,112 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Phantasy Star Online Instruction Set",
"type": "object",
"required": ["opcodes"],
"additionalProperties": false,
"properties": {
"opcodes": {
"type": "array",
"description": "List of every known opcode.",
"items": {
"$ref": "#/definitions/opcode"
}
}
},
"definitions": {
"opcode": {
"type": "object",
"required": ["code", "params"],
"additionalProperties": false,
"properties": {
"code": {
"type": "integer",
"minimum": 0,
"maximum": 63999,
"description": "Unique byte representation of the opcode."
},
"mnemonic": {
"type": "string",
"pattern": "^[a-z][a-z0-9=<>!_]+$",
"description": "Unique string representation of the opcode."
},
"doc": {
"type": "string",
"description": "Opcode documentation."
},
"params": {
"type": "array",
"description": "Opcode parameters. Whether or not the stack is used is determined by the stack property.",
"items": {
"$ref": "#/definitions/param"
}
},
"stack": {
"enum": ["push", "pop"],
"description": "Stack interaction. \"push\" if the instruction takes immediate arguments and pushes its arguments onto the stack. \"pop\" if the instruction doesn't take immediate arguments but pops its arguments off the stack."
}
}
},
"param": {
"type": "object",
"required": ["type"],
"additionalProperties": false,
"properties": {
"type": {
"$ref": "#/definitions/param_type"
},
"doc": {
"type": "string",
"description": "Parameter-specific documentation."
},
"access": {
"$ref": "#/definitions/access"
},
"reg_tup": {
"type": "array",
"minItems": 1,
"description": "Specifies the way the referenced registers will be interpreted. Should only be specified if the parameter type is \"reg_tup_ref\".",
"items": {
"type": "object",
"required": ["type", "access"],
"additionalProperties": false,
"properties": {
"type": {
"$ref": "#/definitions/param_type"
},
"doc": {
"type": "string"
},
"access": {
"$ref": "#/definitions/access"
}
}
}
}
}
},
"param_type": {
"type": "string",
"enum": [
"any",
"byte",
"word",
"dword",
"float",
"label",
"i_label",
"d_label",
"string",
"reg_ref",
"reg_tup_ref",
"reg_ref_var",
"pointer"
]
},
"access": {
"type": "string",
"enum": ["read", "write", "read_write"],
"description": "Specifies the way the instruction accesses the referenced register(s)."
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -624,7 +624,7 @@ function parse_instruction_arguments(cursor: Cursor, opcode: Opcode): Arg[] {
case Type.F32: case Type.F32:
args.push({ value: cursor.f32(), size: 4 }); args.push({ value: cursor.f32(), size: 4 });
break; break;
case Type.Register: case Type.RegRef:
args.push({ value: cursor.u8(), size: 1 }); args.push({ value: cursor.u8(), size: 1 });
break; break;
case Type.ILabel: case Type.ILabel:
@ -708,7 +708,7 @@ function write_object_code(
case Type.F32: case Type.F32:
cursor.write_f32(arg.value); cursor.write_f32(arg.value);
break; break;
case Type.Register: case Type.RegRef:
cursor.write_u8(arg.value); cursor.write_u8(arg.value);
break; break;
case Type.ILabel: case Type.ILabel:

File diff suppressed because it is too large Load Diff

View File

@ -388,7 +388,7 @@ class Assembler {
switch (param.type) { switch (param.type) {
case Type.U8: case Type.U8:
case Type.Register: case Type.RegRef:
this.add_instruction(Opcode.arg_pushb, [arg]); this.add_instruction(Opcode.arg_pushb, [arg]);
break; break;
case Type.U16: case Type.U16:
@ -503,7 +503,7 @@ class Assembler {
break; break;
case TokenType.Register: case TokenType.Register:
match = param.type === Type.Register; match = param.type === Type.RegRef;
this.parse_register(token, args); this.parse_register(token, args);
break; break;
case TokenType.String: case TokenType.String:
@ -543,7 +543,7 @@ class Assembler {
case Type.F32: case Type.F32:
type_str = "a float"; type_str = "a float";
break; break;
case Type.Register: case Type.RegRef:
type_str = "a register reference"; type_str = "a register reference";
break; break;
case Type.ILabel: case Type.ILabel:

View File

@ -37,164 +37,169 @@ export class BasicBlock {
export class ControlFlowGraph { export class ControlFlowGraph {
readonly blocks: BasicBlock[] = []; readonly blocks: BasicBlock[] = [];
readonly instructions: Map<Instruction, BasicBlock> = new Map();
private readonly instructions_to_block: Map<Instruction, BasicBlock> = new Map();
private readonly labels_to_block = new Map<number, BasicBlock>();
get_block_for_instuction(instruction: Instruction): BasicBlock | undefined {
return this.instructions_to_block.get(instruction);
}
get_block_for_label(label: number): BasicBlock | undefined {
return this.labels_to_block.get(label);
}
static create(segments: InstructionSegment[]): ControlFlowGraph { static create(segments: InstructionSegment[]): ControlFlowGraph {
const cfg = new 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) { for (const segment of segments) {
create_basic_blocks(cfg, label_blocks, segment); this.create_basic_blocks(cfg, segment);
} }
link_blocks(cfg, label_blocks); this.link_blocks(cfg);
return cfg; return cfg;
} }
}
function create_basic_blocks( private static create_basic_blocks(cfg: ControlFlowGraph, segment: InstructionSegment) {
cfg: ControlFlowGraph, const len = segment.instructions.length;
label_blocks: Map<number, BasicBlock>, let start = 0;
segment: InstructionSegment let first_block = true;
) {
const len = segment.instructions.length;
let start = 0;
let first_block = true;
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const inst = segment.instructions[i]; const inst = segment.instructions[i];
let branch_type: BranchType; let branch_type: BranchType;
let branch_labels: number[]; let branch_labels: number[];
switch (inst.opcode) { switch (inst.opcode) {
// Return. // Return.
case Opcode.ret: case Opcode.ret:
branch_type = BranchType.Return; 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 = []; branch_labels = [];
break; break;
} else {
// 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;
}
}
const block = new BasicBlock(segment, start, i + 1, branch_type, branch_labels);
for (let j = block.start; j < block.end; j++) {
cfg.instructions_to_block.set(block.segment.instructions[j], block);
}
cfg.blocks.push(block);
if (first_block) {
for (const label of segment.labels) {
cfg.labels_to_block.set(label, block);
}
first_block = false;
}
start = i + 1;
}
}
private static link_blocks(cfg: ControlFlowGraph): void {
// Pairs of calling block and block to which callees should return to.
const callers: [BasicBlock, BasicBlock][] = [];
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:
continue; continue;
} case BranchType.Call:
} if (next_block) {
callers.push([block, next_block]);
const block = new BasicBlock(segment, start, i + 1, branch_type, branch_labels); }
break;
for (let j = block.start; j < block.end; j++) { case BranchType.None:
cfg.instructions.set(block.segment.instructions[j], block); case BranchType.ConditionalJump:
} if (next_block) {
block.link_to(next_block);
cfg.blocks.push(block); }
break;
if (first_block) {
for (const label of segment.labels) {
label_blocks.set(label, block);
} }
first_block = false; for (const label of block.branch_labels) {
} const to_block = cfg.labels_to_block.get(label);
start = i + 1; if (to_block) {
} block.link_to(to_block);
}
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.blocks.length; i++) {
const block = cfg.blocks[i];
const next_block = cfg.blocks[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) { for (const [caller, ret] of callers) {
link_returning_blocks(label_blocks, ret, caller); link_returning_blocks(cfg.labels_to_block, ret, caller);
}
} }
} }

View File

@ -69,6 +69,9 @@ export class ValueSet {
return this; return this;
} }
/**
* Doesn't take into account interger overflow.
*/
scalar_add(s: number): ValueSet { scalar_add(s: number): ValueSet {
for (const int of this.intervals) { for (const int of this.intervals) {
int.start += s; int.start += s;
@ -78,10 +81,16 @@ export class ValueSet {
return this; return this;
} }
/**
* Doesn't take into account interger overflow.
*/
scalar_sub(s: number): ValueSet { scalar_sub(s: number): ValueSet {
return this.scalar_add(-s); return this.scalar_add(-s);
} }
/**
* Doesn't take into account interger overflow.
*/
scalar_mul(s: number): ValueSet { scalar_mul(s: number): ValueSet {
for (const int of this.intervals) { for (const int of this.intervals) {
int.start *= s; int.start *= s;
@ -93,6 +102,7 @@ export class ValueSet {
/** /**
* Integer division. * Integer division.
* Doesn't take into account interger overflow.
*/ */
scalar_div(s: number): ValueSet { scalar_div(s: number): ValueSet {
for (const int of this.intervals) { for (const int of this.intervals) {

View File

@ -1,7 +1,12 @@
import { InstructionSegment, SegmentType, Opcode } from "../../data_formats/parsing/quest/bin"; import { InstructionSegment, Opcode, SegmentType } from "../../data_formats/parsing/quest/bin";
import { assemble } from "../assembly"; import { assemble } from "../assembly";
import { ControlFlowGraph } from "./ControlFlowGraph"; import { ControlFlowGraph } from "./ControlFlowGraph";
import { register_values } from "./register_values"; import {
MAX_REGISTER_VALUE,
MIN_REGISTER_VALUE,
register_values,
REGISTER_VALUES,
} from "./register_values";
test(`${register_values.name} trivial case`, () => { test(`${register_values.name} trivial case`, () => {
const im = to_instructions(` const im = to_instructions(`
@ -46,6 +51,19 @@ test(`${register_values.name} two code paths`, () => {
expect(values.get(1)).toBe(222); expect(values.get(1)).toBe(222);
}); });
test(`${register_values.name} loop`, () => {
const im = to_instructions(`
0:
addi r10, 5
jmpi_< r10, 500, 0
ret
`);
const cfg = ControlFlowGraph.create(im);
const values = register_values(cfg, im[0].instructions[2], 10);
expect(values.size()).toBe(REGISTER_VALUES);
});
test(`${register_values.name} leta and leto`, () => { test(`${register_values.name} leta and leto`, () => {
const im = to_instructions(` const im = to_instructions(`
0: 0:
@ -56,15 +74,15 @@ test(`${register_values.name} leta and leto`, () => {
const cfg = ControlFlowGraph.create(im); const cfg = ControlFlowGraph.create(im);
const r0 = register_values(cfg, im[0].instructions[2], 0); const r0 = register_values(cfg, im[0].instructions[2], 0);
expect(r0.size()).toBe(Math.pow(2, 32)); expect(r0.size()).toBe(REGISTER_VALUES);
expect(r0.min()).toBe(-Math.pow(2, 31)); expect(r0.min()).toBe(MIN_REGISTER_VALUE);
expect(r0.max()).toBe(Math.pow(2, 31) - 1); expect(r0.max()).toBe(MAX_REGISTER_VALUE);
const r1 = register_values(cfg, im[0].instructions[2], 1); const r1 = register_values(cfg, im[0].instructions[2], 1);
expect(r1.size()).toBe(Math.pow(2, 32)); expect(r1.size()).toBe(REGISTER_VALUES);
expect(r1.min()).toBe(-Math.pow(2, 31)); expect(r1.min()).toBe(MIN_REGISTER_VALUE);
expect(r1.max()).toBe(Math.pow(2, 31) - 1); expect(r1.max()).toBe(MAX_REGISTER_VALUE);
}); });
test(`${register_values.name} rev`, () => { test(`${register_values.name} rev`, () => {
@ -147,8 +165,8 @@ test(`${register_values.name} get_random`, () => {
const v1 = register_values(cfg, im[0].instructions[5], 10); const v1 = register_values(cfg, im[0].instructions[5], 10);
expect(v0.size()).toBe(1); expect(v1.size()).toBe(1);
expect(v0.get(0)).toBe(20); expect(v1.get(0)).toBe(20);
const v2 = register_values(cfg, im[0].instructions[7], 10); const v2 = register_values(cfg, im[0].instructions[7], 10);

View File

@ -1,9 +1,16 @@
import { Instruction, Opcode } from "../../data_formats/parsing/quest/bin"; import {
Instruction,
Opcode,
RegTupRefType,
TYPE_REG_REF,
TYPE_REG_REF_VAR,
} from "../../data_formats/parsing/quest/bin";
import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph"; import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph";
import { ValueSet } from "./ValueSet"; import { ValueSet } from "./ValueSet";
const MIN_REGISTER_VALUE = -Math.pow(2, 31); export const MIN_REGISTER_VALUE = -Math.pow(2, 31);
const MAX_REGISTER_VALUE = Math.pow(2, 31) - 1; export const MAX_REGISTER_VALUE = Math.pow(2, 31) - 1;
export const REGISTER_VALUES = Math.pow(2, 32);
/** /**
* Computes the possible values of a register at a specific instruction. * Computes the possible values of a register at a specific instruction.
@ -13,7 +20,7 @@ export function register_values(
instruction: Instruction, instruction: Instruction,
register: number register: number
): ValueSet { ): ValueSet {
const block = cfg.instructions.get(instruction); const block = cfg.get_block_for_instuction(instruction);
if (block) { if (block) {
let inst_idx = block.start; let inst_idx = block.start;
@ -26,13 +33,18 @@ export function register_values(
inst_idx++; inst_idx++;
} }
return find_values(block, inst_idx, register); return find_values(new Set(), block, inst_idx, register);
} else { } else {
return new ValueSet(); return new ValueSet();
} }
} }
function find_values(block: BasicBlock, end: number, register: number): ValueSet { function find_values(
path: Set<BasicBlock>,
block: BasicBlock,
end: number,
register: number
): ValueSet {
let values = new ValueSet(); let values = new ValueSet();
for (let i = block.start; i < end; i++) { for (let i = block.start; i < end; i++) {
@ -42,22 +54,17 @@ function find_values(block: BasicBlock, end: number, register: number): ValueSet
switch (instruction.opcode) { switch (instruction.opcode) {
case Opcode.let: case Opcode.let:
if (args[0].value === register) { if (args[0].value === register) {
values = find_values(block, i, args[1].value); values = find_values(new Set(path), block, i, args[1].value);
} }
break; break;
case Opcode.leti: case Opcode.leti:
case Opcode.letb: case Opcode.letb:
case Opcode.letw: case Opcode.letw:
case Opcode.sync_leti:
if (args[0].value === register) { if (args[0].value === register) {
values.set_value(args[1].value); values.set_value(args[1].value);
} }
break; 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: case Opcode.set:
if (args[0].value === register) { if (args[0].value === register) {
values.set_value(1); values.set_value(1);
@ -70,7 +77,7 @@ function find_values(block: BasicBlock, end: number, register: number): ValueSet
break; break;
case Opcode.rev: case Opcode.rev:
if (args[0].value === register) { if (args[0].value === register) {
const prev_vals = find_values(block, i, register); const prev_vals = find_values(new Set(path), block, i, register);
const prev_size = prev_vals.size(); const prev_size = prev_vals.size();
if (prev_size === 0 || (prev_size === 1 && prev_vals.get(0) === 0)) { if (prev_size === 0 || (prev_size === 1 && prev_vals.get(0) === 0)) {
@ -84,45 +91,89 @@ function find_values(block: BasicBlock, end: number, register: number): ValueSet
break; break;
case Opcode.addi: case Opcode.addi:
if (args[0].value === register) { if (args[0].value === register) {
values = find_values(block, i, register); values = find_values(new Set(path), block, i, register);
values.scalar_add(args[1].value); values.scalar_add(args[1].value);
} }
break; break;
case Opcode.subi: case Opcode.subi:
if (args[0].value === register) { if (args[0].value === register) {
values = find_values(block, i, register); values = find_values(new Set(path), block, i, register);
values.scalar_sub(args[1].value); values.scalar_sub(args[1].value);
} }
break; break;
case Opcode.muli: case Opcode.muli:
if (args[0].value === register) { if (args[0].value === register) {
values = find_values(block, i, register); values = find_values(new Set(path), block, i, register);
values.scalar_mul(args[1].value); values.scalar_mul(args[1].value);
} }
break; break;
case Opcode.divi: case Opcode.divi:
if (args[0].value === register) { if (args[0].value === register) {
values = find_values(block, i, register); values = find_values(new Set(path), block, i, register);
values.scalar_div(args[1].value); values.scalar_div(args[1].value);
} }
break; break;
case Opcode.if_zone_clear:
if (args[0].value === register) {
values.set_interval(0, 1);
}
break;
case Opcode.get_difflvl:
case Opcode.get_slotnumber:
if (args[0].value === register) {
values.set_interval(0, 3);
}
break;
case Opcode.get_random: case Opcode.get_random:
if (args[1].value === register) { if (args[1].value === register) {
// TODO: undefined values. // TODO: undefined values.
const min = find_values(block, i, args[0].value).min() || 0; const min = find_values(new Set(path), block, i, args[0].value).min() || 0;
const max = Math.max( const max = Math.max(
find_values(block, i, args[0].value + 1).max() || 0, find_values(new Set(path), block, i, args[0].value + 1).max() || 0,
min + 1 min + 1
); );
values.set_interval(min, max - 1); values.set_interval(min, max - 1);
} }
break; break;
default:
// Assume any other opcodes that write to the register can produce any value.
{
const params = instruction.opcode.params;
const len = Math.min(args.length, params.length);
for (let j = 0; j < len; j++) {
const param = params[j];
const val = args[j].value;
if (param.write) {
if (
(param.type instanceof RegTupRefType &&
register >= val &&
register < val + param.type.types.length) ||
(param.type === TYPE_REG_REF && val.includes(register)) ||
(param.type === TYPE_REG_REF_VAR && val.includes(register))
) {
values.set_interval(MIN_REGISTER_VALUE, MAX_REGISTER_VALUE);
break;
}
}
}
}
break;
} }
} }
if (values.size() === 0) { if (values.size() === 0) {
path.add(block);
for (const from of block.from) { for (const from of block.from) {
values.union(find_values(from, from.end, register)); // Bail out from loops.
if (path.has(from)) {
values.set_interval(MIN_REGISTER_VALUE, MAX_REGISTER_VALUE);
break;
}
values.union(find_values(new Set(path), from, from.end, register));
} }
} }

View File

@ -113,7 +113,7 @@ function args_to_strings(params: Param[], args: Arg[]): string[] {
} }
break; break;
case Type.Register: case Type.RegRef:
arg_strings.push("r" + arg.value); arg_strings.push("r" + arg.value);
break; break;
case Type.String: case Type.String: