mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 15:28:29 +08:00
Fixed an assembler and a disassembler bug.
This commit is contained in:
parent
edc6428a3d
commit
df2bb7a6ab
28
src/core/util.ts
Normal file
28
src/core/util.ts
Normal file
@ -0,0 +1,28 @@
|
||||
export function arrays_equal<T>(
|
||||
a: T[],
|
||||
b: T[],
|
||||
equal: (element_a: T, element_b: T) => boolean = (a, b) => a === b,
|
||||
): boolean {
|
||||
const len = a.length;
|
||||
|
||||
if (len !== b.length) return false;
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (!equal(a[i], b[i])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function array_buffers_equal(a: ArrayBuffer, b: ArrayBuffer): boolean {
|
||||
if (a.byteLength !== b.byteLength) return false;
|
||||
|
||||
const a_arr = new Uint8Array(a);
|
||||
const b_arr = new Uint8Array(b);
|
||||
|
||||
for (let i = 0; i < a_arr.length; i++) {
|
||||
if (a_arr[i] !== b_arr[i]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@ -59,26 +59,32 @@ export function assemble(
|
||||
}
|
||||
|
||||
class Assembler {
|
||||
private lexer = new AssemblyLexer();
|
||||
private readonly lexer = new AssemblyLexer();
|
||||
private line_no!: number;
|
||||
private tokens!: Token[];
|
||||
private object_code!: Segment[];
|
||||
// The current segment.
|
||||
/**
|
||||
* The current segment.
|
||||
*/
|
||||
private segment?: Segment;
|
||||
private warnings!: AssemblyWarning[];
|
||||
private errors!: AssemblyError[];
|
||||
// Encountered labels.
|
||||
/**
|
||||
* Encountered labels.
|
||||
*/
|
||||
private labels!: Set<number>;
|
||||
private section!: SegmentType;
|
||||
private first_section_marker = true;
|
||||
private prev_line_had_label = false;
|
||||
|
||||
constructor(private assembly: string[], private manual_stack: boolean) {}
|
||||
constructor(private readonly assembly: string[], private readonly manual_stack: boolean) {}
|
||||
|
||||
assemble(): {
|
||||
object_code: Segment[];
|
||||
warnings: AssemblyWarning[];
|
||||
errors: AssemblyError[];
|
||||
} {
|
||||
// Reset all state.
|
||||
this.line_no = 1;
|
||||
this.object_code = [];
|
||||
this.warnings = [];
|
||||
@ -87,16 +93,20 @@ class Assembler {
|
||||
// Need to cast SegmentType.Instructions because of TypeScript bug.
|
||||
this.section = SegmentType.Instructions as SegmentType;
|
||||
this.first_section_marker = true;
|
||||
this.prev_line_had_label = false;
|
||||
|
||||
// Tokenize and assemble line by line.
|
||||
for (const line of this.assembly) {
|
||||
this.tokens = this.lexer.tokenize_line(line);
|
||||
|
||||
if (this.tokens.length > 0) {
|
||||
const token = this.tokens.shift()!;
|
||||
let has_label = false;
|
||||
|
||||
switch (token.type) {
|
||||
case TokenType.Label:
|
||||
this.parse_label(token);
|
||||
has_label = true;
|
||||
break;
|
||||
case TokenType.CodeSection:
|
||||
case TokenType.DataSection:
|
||||
@ -158,6 +168,8 @@ class Assembler {
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
this.prev_line_had_label = has_label;
|
||||
}
|
||||
|
||||
this.line_no++;
|
||||
@ -214,7 +226,7 @@ class Assembler {
|
||||
|
||||
private add_string(str: string): void {
|
||||
if (!this.segment) {
|
||||
// Unadressable data, technically valid.
|
||||
// Unaddressable data, technically valid.
|
||||
const string_segment: StringSegment = {
|
||||
labels: [],
|
||||
type: SegmentType.String,
|
||||
@ -277,14 +289,20 @@ class Assembler {
|
||||
|
||||
const next_token = this.tokens.shift();
|
||||
|
||||
if (this.prev_line_had_label) {
|
||||
this.object_code[this.object_code.length - 1].labels.push(label);
|
||||
}
|
||||
|
||||
switch (this.section) {
|
||||
case SegmentType.Instructions:
|
||||
this.segment = {
|
||||
type: SegmentType.Instructions,
|
||||
labels: [label],
|
||||
instructions: [],
|
||||
};
|
||||
this.object_code.push(this.segment);
|
||||
if (!this.prev_line_had_label) {
|
||||
this.segment = {
|
||||
type: SegmentType.Instructions,
|
||||
labels: [label],
|
||||
instructions: [],
|
||||
};
|
||||
this.object_code.push(this.segment);
|
||||
}
|
||||
|
||||
if (next_token) {
|
||||
if (next_token.type === TokenType.Ident) {
|
||||
@ -300,12 +318,14 @@ class Assembler {
|
||||
|
||||
break;
|
||||
case SegmentType.Data:
|
||||
this.segment = {
|
||||
type: SegmentType.Data,
|
||||
labels: [label],
|
||||
data: new ArrayBuffer(0),
|
||||
};
|
||||
this.object_code.push(this.segment);
|
||||
if (!this.prev_line_had_label) {
|
||||
this.segment = {
|
||||
type: SegmentType.Data,
|
||||
labels: [label],
|
||||
data: new ArrayBuffer(0),
|
||||
};
|
||||
this.object_code.push(this.segment);
|
||||
}
|
||||
|
||||
if (next_token) {
|
||||
if (next_token.type === TokenType.Int) {
|
||||
@ -321,12 +341,14 @@ class Assembler {
|
||||
|
||||
break;
|
||||
case SegmentType.String:
|
||||
this.segment = {
|
||||
type: SegmentType.String,
|
||||
labels: [label],
|
||||
value: "",
|
||||
};
|
||||
this.object_code.push(this.segment);
|
||||
if (!this.prev_line_had_label) {
|
||||
this.segment = {
|
||||
type: SegmentType.String,
|
||||
labels: [label],
|
||||
value: "",
|
||||
};
|
||||
this.object_code.push(this.segment);
|
||||
}
|
||||
|
||||
if (next_token) {
|
||||
if (next_token.type === TokenType.String) {
|
||||
@ -529,10 +551,11 @@ class Assembler {
|
||||
length: token.len,
|
||||
message: "Expected an argument.",
|
||||
});
|
||||
} else {
|
||||
if (param.type.kind !== Kind.ILabelVar && param.type.kind !== Kind.RegRefVar) {
|
||||
param_i++;
|
||||
}
|
||||
} else if (
|
||||
param.type.kind !== Kind.ILabelVar &&
|
||||
param.type.kind !== Kind.RegRefVar
|
||||
) {
|
||||
param_i++;
|
||||
}
|
||||
|
||||
should_be_arg = true;
|
||||
@ -636,7 +659,7 @@ class Assembler {
|
||||
|
||||
switch (param.type.kind) {
|
||||
case Kind.Byte:
|
||||
type_str = "a 8-bit integer";
|
||||
type_str = "an 8-bit integer";
|
||||
break;
|
||||
case Kind.Word:
|
||||
type_str = "a 16-bit integer";
|
||||
|
@ -6,9 +6,67 @@ import { BufferCursor } from "../../core/data_formats/cursor/BufferCursor";
|
||||
import { parse_bin, write_bin } from "../../core/data_formats/parsing/quest/bin";
|
||||
import { assemble } from "./assembly";
|
||||
import { disassemble } from "./disassembly";
|
||||
import { Instruction, object_code_equal, SegmentType } from "./instructions";
|
||||
import { Opcode } from "./opcodes";
|
||||
|
||||
// Roundtrip test.
|
||||
test("assembling dissambled object code with manual stack management should result in the same object code", () => {
|
||||
test("vararg instructions should be disassembled correctly", () => {
|
||||
const asm = disassemble([
|
||||
{
|
||||
type: SegmentType.Instructions,
|
||||
labels: [0],
|
||||
instructions: [
|
||||
new Instruction(Opcode.SWITCH_JMP, [
|
||||
{ value: 90, size: 1 },
|
||||
{ value: 100, size: 2 },
|
||||
{ value: 101, size: 2 },
|
||||
{ value: 102, size: 2 },
|
||||
]),
|
||||
new Instruction(Opcode.RET, []),
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(asm).toEqual(
|
||||
`.code
|
||||
|
||||
0:
|
||||
switch_jmp r90, 100, 101, 102
|
||||
ret
|
||||
`.split("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
// Round-trip test.
|
||||
test("assembling disassembled object code with manual stack management should result in the same IR", () => {
|
||||
const orig_buffer = readFileSync("test/resources/quest27_e.bin");
|
||||
const orig_bytes = prs_decompress(new BufferCursor(orig_buffer, Endianness.Little));
|
||||
const bin = parse_bin(orig_bytes);
|
||||
|
||||
const { object_code, warnings, errors } = assemble(disassemble(bin.object_code, true), true);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
expect(warnings).toEqual([]);
|
||||
|
||||
expect(object_code_equal(object_code, bin.object_code)).toBe(true);
|
||||
});
|
||||
|
||||
// Round-trip test.
|
||||
test("assembling disassembled object code with automatic stack management should result in the same IR", () => {
|
||||
const orig_buffer = readFileSync("test/resources/quest27_e.bin");
|
||||
const orig_bytes = prs_decompress(new BufferCursor(orig_buffer, Endianness.Little));
|
||||
const bin = parse_bin(orig_bytes);
|
||||
|
||||
const { object_code, warnings, errors } = assemble(disassemble(bin.object_code, false), false);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
expect(warnings).toEqual([]);
|
||||
|
||||
expect(object_code_equal(object_code, bin.object_code)).toBe(true);
|
||||
// expect(object_code).toBe(bin.object_code);
|
||||
});
|
||||
|
||||
// Round-trip test.
|
||||
test("assembling disassembled object code with manual stack management should result in the same object code", () => {
|
||||
const orig_buffer = readFileSync("test/resources/quest27_e.bin");
|
||||
const orig_bytes = prs_decompress(new BufferCursor(orig_buffer, Endianness.Little));
|
||||
const bin = parse_bin(orig_bytes);
|
||||
@ -43,13 +101,13 @@ test("assembling dissambled object code with manual stack management should resu
|
||||
expect(matching_bytes).toBe(orig_bytes.size);
|
||||
});
|
||||
|
||||
// Roundtrip test.
|
||||
// Round-trip test.
|
||||
test("disassembling assembled assembly code with automatic stack management should result the same assembly code", () => {
|
||||
const orig_buffer = readFileSync("test/resources/quest27_e.bin");
|
||||
const orig_bytes = prs_decompress(new BufferCursor(orig_buffer, Endianness.Little));
|
||||
const orig_asm = disassemble(parse_bin(orig_bytes).object_code);
|
||||
const orig_asm = disassemble(parse_bin(orig_bytes).object_code, false);
|
||||
|
||||
const { object_code, warnings, errors } = assemble(orig_asm);
|
||||
const { object_code, warnings, errors } = assemble(orig_asm, false);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
expect(warnings).toEqual([]);
|
||||
|
@ -128,6 +128,20 @@ function add_type_to_args(params: Param[], args: Arg[]): ArgWithType[] {
|
||||
args_with_type.push({ ...args[i], type: params[i].type });
|
||||
}
|
||||
|
||||
// Deal with varargs.
|
||||
const last_param = params[params.length - 1];
|
||||
|
||||
if (
|
||||
last_param &&
|
||||
(last_param.type.kind === Kind.ILabelVar || last_param.type.kind === Kind.RegRefVar)
|
||||
) {
|
||||
const len = args.length;
|
||||
|
||||
for (let i = args_with_type.length; i < len; i++) {
|
||||
args_with_type.push({ ...args[i], type: last_param.type });
|
||||
}
|
||||
}
|
||||
|
||||
return args_with_type;
|
||||
}
|
||||
|
||||
@ -138,7 +152,7 @@ function args_to_strings(params: Param[], args: ArgWithType[], stack: boolean):
|
||||
const type = params[i].type;
|
||||
const arg = args[i];
|
||||
|
||||
if (arg == null) {
|
||||
if (arg == undefined) {
|
||||
arg_strings.push("");
|
||||
continue;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Kind, Opcode } from "./opcodes";
|
||||
import { array_buffers_equal, arrays_equal } from "../../core/util";
|
||||
|
||||
/**
|
||||
* Instruction invocation.
|
||||
@ -83,3 +84,34 @@ export type StringSegment = {
|
||||
labels: number[];
|
||||
value: string;
|
||||
};
|
||||
|
||||
export function object_code_equal(a: Segment[], b: Segment[]): boolean {
|
||||
return arrays_equal(a, b, segments_equal);
|
||||
}
|
||||
|
||||
function segments_equal(a: Segment, b: Segment): boolean {
|
||||
if (a.type !== b.type || !arrays_equal(a.labels, b.labels)) return false;
|
||||
|
||||
switch (a.type) {
|
||||
case SegmentType.Instructions:
|
||||
return arrays_equal(
|
||||
a.instructions,
|
||||
(b as InstructionSegment).instructions,
|
||||
instructions_equal,
|
||||
);
|
||||
|
||||
case SegmentType.Data:
|
||||
return array_buffers_equal(a.data, (b as DataSegment).data);
|
||||
|
||||
case SegmentType.String:
|
||||
return a.value === (b as StringSegment).value;
|
||||
}
|
||||
}
|
||||
|
||||
function instructions_equal(a: Instruction, b: Instruction): boolean {
|
||||
return a.opcode.code === b.opcode.code && arrays_equal(a.args, b.args, args_equal);
|
||||
}
|
||||
|
||||
function args_equal(a: Arg, b: Arg): boolean {
|
||||
return a.value === b.value && a.size === b.size;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user