import { Instruction, InstructionSegment, Segment, SegmentType, AsmToken } from "../instructions"; import { OP_ADD, OP_ADDI, OP_AND, OP_ANDI, OP_ARG_PUSHB, OP_ARG_PUSHL, OP_ARG_PUSHR, OP_ARG_PUSHW, OP_ARG_PUSHA, OP_ARG_PUSHS, OP_CALL, OP_CLEAR, OP_DIV, OP_DIVI, OP_EXIT, OP_FADD, OP_FADDI, OP_FDIV, OP_FDIVI, OP_FMUL, OP_FMULI, OP_FSUB, OP_FSUBI, OP_JMP, OP_LET, OP_LETB, OP_LETI, OP_LETW, OP_MOD, OP_MODI, OP_MUL, OP_MULI, OP_NOP, OP_OR, OP_ORI, OP_RET, OP_REV, OP_SET, OP_SHIFT_LEFT, OP_SHIFT_RIGHT, OP_SUB, OP_SUBI, OP_SYNC, OP_THREAD, OP_XOR, OP_XORI, OP_JMP_E, OP_JMPI_E, OP_JMP_ON, OP_JMP_OFF, OP_JMP_NE, OP_JMPI_NE, OP_UJMP_G, OP_UJMPI_G, OP_JMP_G, OP_JMPI_G, OP_UJMP_L, OP_UJMPI_L, OP_JMP_L, OP_JMPI_L, OP_UJMP_GE, OP_UJMPI_GE, OP_JMP_GE, OP_JMPI_GE, OP_UJMP_LE, OP_UJMPI_LE, OP_JMP_LE, OP_JMPI_LE, OP_STACK_POP, OP_STACK_PUSH, OP_STACK_PUSHM, OP_STACK_POPM, Kind, OP_WINDOW_MSG, OP_ADD_MSG, OP_WINEND, OP_LETA, OP_FLET, OP_FLETI, } from "../opcodes"; import { VirtualMachineMemoryBuffer, VirtualMachineMemory } from "./memory"; import { ComparisonOperation, numeric_ops, comparison_ops, rest, BinaryNumericOperation, andsecond, andreduce, } from "./utils"; import { VirtualMachineIO } from "./io"; import { VMIOStub } from "./VMIOStub"; const REGISTERS_BASE_ADDRESS = 0x00a954b0; const REGISTER_COUNT = 256; const REGISTER_SIZE = 4; const VARIABLE_STACK_LENGTH = 16; // TODO: verify this value const ARG_STACK_SLOT_SIZE = 4; const ARG_STACK_LENGTH = 8; const STRING_ARG_STORE_ADDRESS = 0x00a92700; const STRING_ARG_STORE_SIZE = 1024; // TODO: verify this value export enum ExecutionResult { Ok, WaitingVsync, Halted, } export class VirtualMachine { private memory = new VirtualMachineMemory(); private registers = this.memory.allocate( REGISTER_SIZE * REGISTER_COUNT, REGISTERS_BASE_ADDRESS, )!; private string_arg_store = this.memory.allocate( STRING_ARG_STORE_SIZE, STRING_ARG_STORE_ADDRESS, ); private object_code: Segment[] = []; private label_to_seg_idx: Map = new Map(); private thread: Thread[] = []; private thread_idx = 0; private window_msg_open = false; constructor(private io: VirtualMachineIO = new VMIOStub()) {} /** * Halts and resets the VM, then loads new object code. */ load_object_code(object_code: Segment[]): void { this.halt(); this.registers.zero(); this.string_arg_store.zero(); this.object_code = object_code; this.label_to_seg_idx.clear(); let i = 0; for (const segment of this.object_code) { for (const label of segment.labels) { this.label_to_seg_idx.set(label, i); } i++; } } /** * Schedules concurrent execution of the code at the given label. */ start_thread(label: number): void { const seg_idx = this.label_to_seg_idx.get(label); const segment = seg_idx == undefined ? undefined : this.object_code[seg_idx]; if (segment == undefined) { throw new Error(`Unknown label ${label}.`); } if (segment.type !== SegmentType.Instructions) { throw new Error( `Label ${label} points to a ${SegmentType[segment.type]} segment, expecting ${ SegmentType[SegmentType.Instructions] }.`, ); } this.thread.push( new Thread( this.io, new ExecutionLocation(seg_idx!, 0), this.memory.allocate(ARG_STACK_SLOT_SIZE * ARG_STACK_LENGTH), true, ), ); } private dispose_thread(thread_idx: number): void { this.thread[thread_idx].dispose(); this.thread.splice(thread_idx, 1); } /** * Executes the next instruction if one is scheduled. */ execute(): ExecutionResult { let srcloc: AsmToken | undefined; try { const exec = this.thread[this.thread_idx]; const inst = this.get_next_instruction_from_thread(exec); if (inst.asm && inst.asm.mnemonic) { srcloc = inst.asm.mnemonic; } return this.execute_instruction(exec, inst); } catch (err) { if (!(err instanceof Error)) { err = new Error(String(err)); } this.halt(); this.io.error(err, srcloc); return ExecutionResult.Halted; } } private execute_instruction(exec: Thread, inst: Instruction): ExecutionResult { if (this.thread.length === 0) return ExecutionResult.Halted; if (this.thread_idx >= this.thread.length) return ExecutionResult.WaitingVsync; const arg_vals = inst.args.map(arg => arg.value); // eslint-disable-next-line const [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7] = arg_vals; // helper for conditional jump opcodes const conditional_jump_args: ( cond: ComparisonOperation, ) => [Thread, number, ComparisonOperation, number, number] = cond => [ exec, arg2, cond, arg0, arg1, ]; switch (inst.opcode.code) { case OP_NOP.code: break; case OP_RET.code: this.pop_call_stack(this.thread_idx, exec); break; case OP_SYNC.code: this.thread_idx++; break; case OP_EXIT.code: this.halt(); break; case OP_THREAD.code: this.start_thread(arg0); break; // integer lets case OP_LET.code: this.set_register_signed(arg0, this.get_register_signed(arg1)); break; case OP_LETI.code: this.set_register_signed(arg0, arg1); break; case OP_LETB.code: this.set_register_byte(arg0, arg1); break; case OP_LETW.code: this.set_register_word(arg0, arg1); break; case OP_LETA.code: this.set_register_unsigned(arg0, this.get_register_address(arg0)); break; // float lets case OP_FLET.code: this.set_register_float(arg0, this.get_register_float(arg1)); break; case OP_FLETI.code: this.set_register_float(arg0, arg1); break; case OP_SET.code: this.set_register_signed(arg0, 1); break; case OP_CLEAR.code: this.set_register_signed(arg0, 0); break; case OP_REV.code: this.set_register_signed(arg0, this.get_register_signed(arg0) === 0 ? 1 : 0); break; case OP_CALL.code: this.push_call_stack(exec, arg0); break; case OP_JMP.code: this.jump_to_label(exec, arg0); break; case OP_ARG_PUSHR.code: // deref given register ref exec.push_arg(this.get_register_signed(arg0), Kind.DWord); break; case OP_ARG_PUSHL.code: exec.push_arg(inst.args[0].value, Kind.DWord); break; case OP_ARG_PUSHB.code: exec.push_arg(inst.args[0].value, Kind.Byte); break; case OP_ARG_PUSHW.code: exec.push_arg(inst.args[0].value, Kind.Word); break; case OP_ARG_PUSHA.code: // push address of register exec.push_arg(this.get_register_address(inst.args[0].value), Kind.DWord); break; case OP_ARG_PUSHS.code: { // store string and push its address const string_arg = arg0 as string; this.string_arg_store.write_string_utf16_at( 0, string_arg, string_arg.length * 2, ); exec.push_arg(this.string_arg_store.address, Kind.String); } break; // arithmetic operations case OP_ADD.code: case OP_FADD.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.add); break; case OP_ADDI.code: case OP_FADDI.code: this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.add); break; case OP_SUB.code: case OP_FSUB.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.sub); break; case OP_SUBI.code: case OP_FSUBI.code: this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.sub); break; case OP_MUL.code: case OP_FMUL.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.mul); break; case OP_MULI.code: case OP_FMULI.code: this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.mul); break; case OP_DIV.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.idiv); break; case OP_FDIV.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.div); break; case OP_DIVI.code: this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.idiv); break; case OP_FDIVI.code: this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.div); break; case OP_MOD.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.mod); break; case OP_MODI.code: this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.mod); break; // bit operations case OP_AND.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.and); break; case OP_ANDI.code: this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.and); break; case OP_OR.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.or); break; case OP_ORI.code: this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.or); break; case OP_XOR.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.xor); break; case OP_XORI.code: this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.xor); break; // shift operations case OP_SHIFT_LEFT.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.shl); break; case OP_SHIFT_RIGHT.code: this.do_numeric_op_with_register(arg0, arg1, numeric_ops.shr); break; // conditional jumps case OP_JMP_ON.code: // all eq 1? this.conditional_jump( exec, arg0, comparison_ops.eq, 1, ...rest(arg_vals).map(reg => this.get_register_signed(reg)), ); break; case OP_JMP_OFF.code: // all eq 0? this.conditional_jump( exec, arg0, comparison_ops.eq, 0, ...rest(arg_vals).map(reg => this.get_register_signed(reg)), ); break; case OP_JMP_E.code: this.signed_conditional_jump_with_register( ...conditional_jump_args(comparison_ops.eq), ); break; case OP_JMPI_E.code: this.signed_conditional_jump_with_literal( ...conditional_jump_args(comparison_ops.eq), ); break; case OP_JMP_NE.code: this.signed_conditional_jump_with_register( ...conditional_jump_args(comparison_ops.neq), ); break; case OP_JMPI_NE.code: this.signed_conditional_jump_with_literal( ...conditional_jump_args(comparison_ops.neq), ); break; case OP_UJMP_G.code: this.unsigned_conditional_jump_with_register( ...conditional_jump_args(comparison_ops.gt), ); break; case OP_UJMPI_G.code: this.unsigned_conditional_jump_with_literal( ...conditional_jump_args(comparison_ops.gt), ); break; case OP_JMP_G.code: this.signed_conditional_jump_with_register( ...conditional_jump_args(comparison_ops.gt), ); break; case OP_JMPI_G.code: this.signed_conditional_jump_with_literal( ...conditional_jump_args(comparison_ops.gt), ); break; case OP_UJMP_L.code: this.unsigned_conditional_jump_with_register( ...conditional_jump_args(comparison_ops.lt), ); break; case OP_UJMPI_L.code: this.unsigned_conditional_jump_with_literal( ...conditional_jump_args(comparison_ops.lt), ); break; case OP_JMP_L.code: this.signed_conditional_jump_with_register( ...conditional_jump_args(comparison_ops.lt), ); break; case OP_JMPI_L.code: this.signed_conditional_jump_with_literal( ...conditional_jump_args(comparison_ops.lt), ); break; case OP_UJMP_GE.code: this.unsigned_conditional_jump_with_register( ...conditional_jump_args(comparison_ops.gte), ); break; case OP_UJMPI_GE.code: this.unsigned_conditional_jump_with_literal( ...conditional_jump_args(comparison_ops.gte), ); break; case OP_JMP_GE.code: this.signed_conditional_jump_with_register( ...conditional_jump_args(comparison_ops.gte), ); break; case OP_JMPI_GE.code: this.signed_conditional_jump_with_literal( ...conditional_jump_args(comparison_ops.gte), ); break; case OP_UJMP_LE.code: this.unsigned_conditional_jump_with_register( ...conditional_jump_args(comparison_ops.lte), ); break; case OP_UJMPI_LE.code: this.unsigned_conditional_jump_with_literal( ...conditional_jump_args(comparison_ops.lte), ); break; case OP_JMP_LE.code: this.signed_conditional_jump_with_register( ...conditional_jump_args(comparison_ops.lte), ); break; case OP_JMPI_LE.code: this.signed_conditional_jump_with_literal( ...conditional_jump_args(comparison_ops.lte), ); break; // variable stack operations case OP_STACK_PUSH.code: this.push_variable_stack(exec, arg0, 1); break; case OP_STACK_POP.code: this.pop_variable_stack(exec, arg0, 1); break; case OP_STACK_PUSHM.code: this.push_variable_stack(exec, arg0, arg1); break; case OP_STACK_POPM.code: this.pop_variable_stack(exec, arg0, arg1); break; case OP_WINDOW_MSG.code: if (!this.window_msg_open) { const args = exec.fetch_args(inst); const str = this.deref_string(args[0]); this.window_msg_open = true; this.io.window_msg(str); } break; case OP_ADD_MSG.code: if (this.window_msg_open) { const args = exec.fetch_args(inst); const str = this.deref_string(args[0]); this.io.add_msg(str); } break; case OP_WINEND.code: if (this.window_msg_open) { this.window_msg_open = false; this.io.winend(); } break; default: throw new Error(`Unsupported instruction: ${inst.opcode.mnemonic}.`); } // advance instruction "pointer" if (exec.call_stack.length) { const top = exec.call_stack_top(); const segment = this.object_code[top.seg_idx] as InstructionSegment; // move to next instruction if (++top.inst_idx >= segment.instructions.length) { // segment ended, move to next segment if (++top.seg_idx >= this.object_code.length) { // eof this.dispose_thread(this.thread_idx); } else { top.inst_idx = 0; } } } if (this.thread.length === 0) return ExecutionResult.Halted; if (this.thread_idx >= this.thread.length) return ExecutionResult.WaitingVsync; return ExecutionResult.Ok; } /** * Signal to the VM that a vsync has happened. */ vsync(): void { if (this.thread_idx >= this.thread.length) { this.thread_idx = 0; } } /** * Halts execution of all threads. */ halt(): void { for (const thread of this.thread) { thread.dispose(); } this.window_msg_open = false; this.thread = []; this.thread_idx = 0; } public get_register_signed(reg: number): number { return this.registers.i32_at(REGISTER_SIZE * reg); } private set_register_signed(reg: number, value: number): void { this.registers.write_i32_at(REGISTER_SIZE * reg, value); } public get_register_unsigned(reg: number): number { return this.registers.u32_at(REGISTER_SIZE * reg); } private set_register_unsigned(reg: number, value: number): void { this.registers.write_u32_at(REGISTER_SIZE * reg, value); } public get_register_word(reg: number): number { return this.registers.u16_at(REGISTER_SIZE * reg); } private set_register_word(reg: number, value: number): void { this.registers.write_u16_at(REGISTER_SIZE * reg, value); } public get_register_byte(reg: number): number { return this.registers.u8_at(REGISTER_SIZE * reg); } public set_register_byte(reg: number, value: number): void { this.registers.write_u8_at(REGISTER_SIZE * reg, value) } public get_register_float(reg: number): number { return this.registers.f32_at(REGISTER_SIZE * reg); } private set_register_float(reg: number, value: number): void { this.registers.write_f32_at(REGISTER_SIZE * reg, value); } private do_numeric_op_with_register( reg1: number, reg2: number, op: BinaryNumericOperation, ): void { this.do_numeric_op_with_literal(reg1, this.get_register_signed(reg2), op); } private do_numeric_op_with_literal( reg: number, literal: number, op: BinaryNumericOperation, ): void { this.set_register_signed(reg, op(this.get_register_signed(reg), literal)); } private push_call_stack(exec: Thread, label: number): void { const seg_idx = this.label_to_seg_idx.get(label); if (seg_idx == undefined) { throw new Error(`Invalid label called: ${label}.`); } else { const segment = this.object_code[seg_idx]; if (segment.type !== SegmentType.Instructions) { throw new Error( `Label ${label} points to a ${SegmentType[segment.type]} segment, expecting ${ SegmentType[SegmentType.Instructions] }.`, ); } else { exec.call_stack.push(new ExecutionLocation(seg_idx, -1)); } } } private pop_call_stack(idx: number, exec: Thread): void { exec.call_stack.pop(); if (exec.call_stack.length >= 1) { const top = exec.call_stack_top(); const segment = this.object_code[top.seg_idx]; if (!segment || segment.type !== SegmentType.Instructions) { throw new Error(`Invalid segment index ${top.seg_idx}.`); } } else { // popped off the last return address // which means this is the end of the function this thread was started on // which means this is the end of this thread this.thread.splice(idx, 1); } } private jump_to_label(exec: Thread, label: number): void { const top = exec.call_stack_top(); const seg_idx = this.label_to_seg_idx.get(label); if (seg_idx == undefined) { throw new Error(`Invalid jump label: ${label}.`); } else { top.seg_idx = seg_idx; top.inst_idx = -1; } } private signed_conditional_jump_with_register( exec: Thread, label: number, condition: ComparisonOperation, reg1: number, reg2: number, ): void { this.conditional_jump( exec, label, condition, this.get_register_signed(reg1), this.get_register_signed(reg2), ); } private signed_conditional_jump_with_literal( exec: Thread, label: number, condition: ComparisonOperation, reg: number, literal: number, ): void { this.conditional_jump(exec, label, condition, this.get_register_signed(reg), literal); } private unsigned_conditional_jump_with_register( exec: Thread, label: number, condition: ComparisonOperation, reg1: number, reg2: number, ): void { this.conditional_jump( exec, label, condition, this.get_register_unsigned(reg1), this.get_register_unsigned(reg2), ); } private unsigned_conditional_jump_with_literal( exec: Thread, label: number, condition: ComparisonOperation, reg: number, literal: number, ): void { this.conditional_jump(exec, label, condition, this.get_register_unsigned(reg), literal); } private conditional_jump( exec: Thread, label: number, condition: ComparisonOperation, ...vals: number[] ): void { const chain_cmp = andsecond.bind< null, ComparisonOperation, Parameters, any >(null, condition); if (andreduce(chain_cmp, vals) !== undefined) { this.jump_to_label(exec, label); } } private push_variable_stack(exec: Thread, base_reg: number, num_push: number): void { const end = base_reg + num_push; if (end > REGISTER_COUNT) { throw new Error("Variable stack: Invalid register"); } if (exec.variable_stack.length + num_push > VARIABLE_STACK_LENGTH) { throw new Error("Variable stack: Stack overflow"); } for (let r = base_reg; r < end; r++) { exec.variable_stack.push(this.get_register_unsigned(r)); } } private pop_variable_stack(exec: Thread, base_reg: number, num_pop: number): void { const end = base_reg + num_pop; if (end > REGISTER_COUNT) { throw new Error("Variable stack: Invalid register"); } if (exec.variable_stack.length < num_pop) { throw new Error("Variable stack: Stack underflow"); } for (let r = end - 1; r >= base_reg; r--) { this.set_register_unsigned(r, exec.variable_stack.pop()!); } } private get_next_instruction_from_thread(exec: Thread): Instruction { if (exec.call_stack.length) { const top = exec.call_stack_top(); const segment = this.object_code[top.seg_idx]; if (!segment || segment.type !== SegmentType.Instructions) { throw new Error(`Invalid segment index ${top.seg_idx}.`); } const inst = segment.instructions[top.inst_idx]; if (!inst) { throw new Error( `Invalid instruction index ${top.inst_idx} for segment ${top.seg_idx}.`, ); } return inst; } else { throw new Error(`Call stack is empty.`); } } private get_register_address(reg: number): number { return this.registers.address + reg * REGISTER_SIZE; } private deref_string(address: number): string { const slot = this.memory.get(address); let str: string = ""; if (slot !== undefined) { str = slot.buffer.string_utf16_at(slot.byte_offset, Infinity, true); } return str; } } class ExecutionLocation { constructor(public seg_idx: number, public inst_idx: number) {} } type ArgStackTypeList = [Kind, Kind, Kind, Kind, Kind, Kind, Kind, Kind]; class Thread { /** * Call stack. The top element describes the instruction about to be executed. */ public call_stack: ExecutionLocation[] = []; private arg_stack: VirtualMachineMemoryBuffer; private arg_stack_counter: number = 0; private arg_stack_types: ArgStackTypeList = Array(ARG_STACK_LENGTH).fill( Kind.Any, ) as ArgStackTypeList; public variable_stack: number[] = []; /** * Global or floor-local? */ public global: boolean; constructor( public io: VirtualMachineIO, next: ExecutionLocation, arg_stack: VirtualMachineMemoryBuffer, global: boolean, ) { this.call_stack = [next]; this.global = global; this.arg_stack = arg_stack; } public call_stack_top(): ExecutionLocation { return this.call_stack[this.call_stack.length - 1]; } public push_arg(data: number, type: Kind): void { if (this.arg_stack_counter >= ARG_STACK_LENGTH) { throw new Error("Argument stack: Stack overflow"); } this.arg_stack.write_u32_at(this.arg_stack_counter * ARG_STACK_SLOT_SIZE, data); this.arg_stack_types[this.arg_stack_counter] = type; this.arg_stack_counter++; } public fetch_args(inst: Instruction): number[] { const args: number[] = []; const srcloc: AsmToken | undefined = inst.asm && inst.asm.mnemonic; if (inst.opcode.params.length !== this.arg_stack_counter) { this.io.warning("Argument stack: Argument count mismatch", srcloc); } for (let i = 0; i < inst.opcode.params.length; i++) { const param = inst.opcode.params[i]; if (param.type.kind !== this.arg_stack_types[i]) { this.io.warning("Argument stack: Argument type mismatch", srcloc); } switch (param.type.kind) { case Kind.Byte: args.push(this.arg_stack.u8_at(i * ARG_STACK_SLOT_SIZE)); break; case Kind.Word: args.push(this.arg_stack.u16_at(i * ARG_STACK_SLOT_SIZE)); break; case Kind.DWord: case Kind.String: args.push(this.arg_stack.u32_at(i * ARG_STACK_SLOT_SIZE)); break; default: throw new Error( `Argument stack: Unhandled param kind: Kind.${Kind[param.type.kind]}`, ); } } this.arg_stack_counter = 0; return args; } public dispose(): void { this.arg_stack.free(); } }