From ad79f82f1fbcab8db7eedafdb9395076729729d2 Mon Sep 17 00:00:00 2001
From: sfja <sfja2004@gmail.com>
Date: Mon, 31 Mar 2025 02:12:09 +0200
Subject: [PATCH] keyboard interrupts work

---
 vm/asm.c  |  78 ++++++++++++++++++++++++++++++---------
 vm/asm.h  |  28 +++++++++-----
 vm/main.c |  66 ++++++++++++++++-----------------
 vm/vm.c   | 108 +++++++++++++++++++++++++++---------------------------
 vm/vm.h   |   2 +
 5 files changed, 169 insertions(+), 113 deletions(-)

diff --git a/vm/asm.c b/vm/asm.c
index 2e141bd..7ab4fb1 100644
--- a/vm/asm.c
+++ b/vm/asm.c
@@ -51,10 +51,10 @@ Line s_mov8_mi_i(uint16_t dst_imm, uint16_t op2_imm)
         .op2 = (Ex) { .imm = op2_imm },
     };
 }
-Line s_mov8_mi_reg(uint16_t dst_imm, Reg op2_reg)
+Line s_mov8_mi_r(uint16_t dst_imm, Reg op2_reg)
 {
     return (Line) {
-        .ty = LineTy_Mov8_MemImm_Imm,
+        .ty = LineTy_Mov8_MemImm_Reg,
         .dst = (Ex) { .imm = dst_imm },
         .op2 = (Ex) { .reg = (uint16_t)op2_reg },
     };
@@ -120,21 +120,21 @@ Line s_iret(void)
     return (Line) { .ty = LineTy_IRet };
 }
 
-#define DEFINE_BINARY_R_I(FN, LINETY)                                          \
+#define DEFINE_BINARY_I(FN, LINETY)                                            \
     Line s_##FN##_r_i(Reg dst_reg, Reg op1_reg, uint16_t op2_imm)              \
     {                                                                          \
         return (Line) {                                                        \
-            .ty = LineTy_##LINETY##_Reg_Imm,                                   \
+            .ty = LineTy_##LINETY##_Imm,                                       \
             .dst = (Ex) { .reg = (uint16_t)dst_reg },                          \
             .op1 = (Ex) { .reg = (uint16_t)op1_reg },                          \
             .op2 = (Ex) { .imm = op2_imm },                                    \
         };                                                                     \
     }
 
-DEFINE_BINARY_R_I(or, Or)
-DEFINE_BINARY_R_I(and, And)
-DEFINE_BINARY_R_I(add, Add)
-DEFINE_BINARY_R_I(sub, Sub)
+DEFINE_BINARY_I(or, Or)
+DEFINE_BINARY_I(and, And)
+DEFINE_BINARY_I(add, Add)
+DEFINE_BINARY_I(sub, Sub)
 
 static inline void add_dst_reg(uint32_t* ins, uint16_t reg);
 static inline void add_op1_reg(uint32_t* ins, uint16_t reg);
@@ -165,23 +165,37 @@ void assemble_to_binary(uint16_t* out, const Line* lines, size_t lines_size)
     size_t res_labels_size = 0;
 
 #define ADD_LABEL(LABEL)                                                       \
-    unres_labels[unres_labels_size++] = (UnresolvedLabel) { LABEL, ip };       \
-    out[ip++] = 0;
+    {                                                                          \
+        unres_labels[unres_labels_size++] = (UnresolvedLabel) { LABEL, ip };   \
+        out[ip++] = 0;                                                         \
+    }
 
+    printf("assembling...\n");
+    printf("ip op    n data...\n");
     for (size_t i = 0; i < lines_size; ++i) {
+        bool is_label = false;
+        bool is_data = false;
+
         const Line* line = &lines[i];
+        uint16_t ins_ip = ip;
         switch (line->ty) {
             case LineTy_Label: {
                 res_labels[res_labels_size++]
                     = (ResolvedLabel) { line->op1.label, ip * 2 };
+
+                is_label = true;
                 break;
             }
             case LineTy_DataImm: {
                 out[ip++] = line->op1.imm;
+
+                is_data = true;
                 break;
             }
             case LineTy_DataLabel: {
                 ADD_LABEL(line->op1.label);
+
+                is_data = true;
                 break;
             }
             case LineTy_Nop: {
@@ -231,7 +245,7 @@ void assemble_to_binary(uint16_t* out, const Line* lines, size_t lines_size)
                 uint16_t op2 = line->op2.reg;
 
                 uint32_t ins = Op_Mov16;
-                ins |= (op2 & 0xfu) << 6;
+                ins |= (op2 & 0xfu) << 7;
                 ins |= (dst & 0xfu) << 12;
 
                 out[ip++] = (uint16_t)ins;
@@ -282,7 +296,7 @@ void assemble_to_binary(uint16_t* out, const Line* lines, size_t lines_size)
                 uint16_t dst = line->dst.reg;
                 uint16_t op1 = line->op1.imm;
 
-                uint32_t ins = Op_Lit;
+                uint32_t ins = Op_In;
                 set_is_imm(&ins);
                 add_dst_reg(&ins, dst);
 
@@ -314,10 +328,10 @@ void assemble_to_binary(uint16_t* out, const Line* lines, size_t lines_size)
                 out[ip++] = Op_IRet;
                 break;
             }
-            case LineTy_Or_Reg_Imm:
-            case LineTy_And_Reg_Imm:
-            case LineTy_Add_Reg_Imm:
-            case LineTy_Sub_Reg_Imm: {
+            case LineTy_Or_Imm:
+            case LineTy_And_Imm:
+            case LineTy_Add_Imm:
+            case LineTy_Sub_Imm: {
                 uint16_t dst = line->dst.reg;
                 uint16_t op1 = line->op1.reg;
                 uint16_t op2 = line->op2.imm;
@@ -332,14 +346,41 @@ void assemble_to_binary(uint16_t* out, const Line* lines, size_t lines_size)
                 break;
             }
         }
+
+        if (!is_label) {
+            printf("%02x %-5s %d",
+                ins_ip * 2,
+                is_data ? "data" : op_str(out[ins_ip] & 0x3f),
+                ip - ins_ip);
+            for (uint16_t i = 0; i < ip - ins_ip; ++i) {
+                printf(" %02x %c%c%c%c %c%c%c%c  %02x %c%c%c%c %c%c%c%c ",
+                    out[ins_ip + i] & 0xff,
+                    fmt_binary(out[ins_ip + i] & 0xff),
+                    out[ins_ip + i] >> 8,
+                    fmt_binary(out[ins_ip + i] >> 8));
+            }
+            printf("\n");
+        }
     }
 
+    printf("resolving...\n");
+    printf(" l ip  v  data\n");
     for (size_t i = 0; i < unres_labels_size; ++i) {
         bool found = false;
         for (size_t j = 0; j < res_labels_size; ++j) {
             if (res_labels[j].label == unres_labels[i].label) {
                 out[unres_labels[i].ptr] = res_labels[j].ip;
                 found = true;
+
+                printf("%2d %02x %02x  %02x %c%c%c%c %c%c%c%c  %02x %c%c%c%c "
+                       "%c%c%c%c\n",
+                    res_labels[j].label,
+                    unres_labels[i].ptr * 2,
+                    res_labels[j].ip,
+                    out[unres_labels[i].ptr] & 0xff,
+                    fmt_binary(out[unres_labels[i].ptr] & 0xff),
+                    out[unres_labels[i].ptr] >> 8,
+                    fmt_binary(out[unres_labels[i].ptr] >> 8));
                 break;
             }
         }
@@ -349,6 +390,7 @@ void assemble_to_binary(uint16_t* out, const Line* lines, size_t lines_size)
                 unres_labels[i].label);
         }
     }
+    printf("done!\n");
 }
 
 static inline void add_dst_reg(uint32_t* ins, uint16_t reg)
@@ -383,9 +425,9 @@ static inline void set_mov_is_store(uint32_t* ins)
 static inline uint16_t linety_arithm_ins(LineTy ty)
 {
     switch (ty) {
-        case LineTy_Or_Reg_Imm:
+        case LineTy_Or_Imm:
             return Op_Or;
-        case LineTy_And_Reg_Imm:
+        case LineTy_And_Imm:
             return Op_And;
         case LineTy_Add_Reg_Imm:
             return Op_Add;
diff --git a/vm/asm.h b/vm/asm.h
index 3ae460b..75e0022 100644
--- a/vm/asm.h
+++ b/vm/asm.h
@@ -21,10 +21,10 @@ typedef enum {
     LineTy_Lit_Imm,
     LineTy_Lit_Label,
     LineTy_IRet,
-    LineTy_Or_Reg_Imm,
-    LineTy_And_Reg_Imm,
-    LineTy_Add_Reg_Imm,
-    LineTy_Sub_Reg_Imm,
+    LineTy_Or_Imm,
+    LineTy_And_Imm,
+    LineTy_Add_Imm,
+    LineTy_Sub_Imm,
 } LineTy;
 
 typedef struct {
@@ -52,7 +52,7 @@ Line s_nop(void);
 Line s_hlt(void);
 Line s_jmp_l(int op1_label);
 Line s_mov8_mi_i(uint16_t dst_imm, uint16_t op2_imm);
-Line s_mov8_mi_reg(uint16_t dst_imm, Reg op2_reg);
+Line s_mov8_mi_r(uint16_t dst_imm, Reg op2_reg);
 Line s_mov16_r_r(Reg dst_reg, Reg op2_reg);
 Line s_mov16_r_i(Reg dst_reg, uint16_t op2_imm);
 Line s_mov16_r_mr(Reg dst_reg, Reg op2_reg, uint16_t op2_offset);
@@ -61,9 +61,19 @@ Line s_in_i(Reg dst_reg, uint16_t op1_imm);
 Line s_lit_i(uint16_t op1_imm);
 Line s_lit_l(int op1_label);
 Line s_iret(void);
-Line s_or_r_i(Reg dst_reg, Reg op1_reg, uint16_t op2_imm);
-Line s_and_r_i(Reg dst_reg, Reg op1_reg, uint16_t op2_imm);
-Line s_add_r_i(Reg dst_reg, Reg op1_reg, uint16_t op2_imm);
-Line s_sub_r_i(Reg dst_reg, Reg op1_reg, uint16_t op2_imm);
+Line s_or_i(Reg dst_reg, Reg op1_reg, uint16_t op2_imm);
+Line s_and_i(Reg dst_reg, Reg op1_reg, uint16_t op2_imm);
+Line s_add_i(Reg dst_reg, Reg op1_reg, uint16_t op2_imm);
+Line s_sub_i(Reg dst_reg, Reg op1_reg, uint16_t op2_imm);
+
+#define s_push_r(REG) s_add_i(Rsp, Rsp, 2), s_mov16_mr_r(Rsp, 0, REG)
+
+#define s_pop_r(REG) s_mov16_r_mr(REG, Rsp, 2), s_sub_i(Rsp, Rsp, 2)
 
 void assemble_to_binary(uint16_t* out, const Line* lines, size_t lines_size);
+
+#define fmt_binary(VAL)                                                        \
+    (VAL) >> 7 & 1 ? '1' : '0', (VAL) >> 6 & 1 ? '1' : '0',                    \
+        (VAL) >> 5 & 1 ? '1' : '0', (VAL) >> 4 & 1 ? '1' : '0',                \
+        (VAL) >> 3 & 1 ? '1' : '0', (VAL) >> 2 & 1 ? '1' : '0',                \
+        (VAL) >> 1 & 1 ? '1' : '0', (VAL) >> 0 & 1 ? '1' : '0'
diff --git a/vm/main.c b/vm/main.c
index af3b06e..11854b0 100644
--- a/vm/main.c
+++ b/vm/main.c
@@ -187,8 +187,9 @@ extern const bool charset[][ch_height][ch_width];
 
 void sdldevice_set_char(IODevice* io_device, uint16_t offset, uint8_t value)
 {
-    printf("value = %d '%c'\n", value, value);
-    return;
+    printf("value = %d '%c', offset = %d\n", value, value, offset);
+    if (value < 'A' || value > 'C')
+        return;
 
     SdlDevice* device = io_device->self;
     pthread_mutex_lock(&device->mutex);
@@ -238,9 +239,9 @@ void sdldevice_wait_for_interrupt(IODevice* io_device)
 
     pthread_mutex_lock(&device->mutex);
 
-    printf("vm: waiting for interrupt...\n");
+    // printf("vm: waiting for interrupt...\n");
     pthread_cond_wait(&device->interrupt_waiter, &device->mutex);
-    printf("vm: got interrupt!\n");
+    // printf("vm: got interrupt!\n");
 
     pthread_mutex_unlock(&device->mutex);
 }
@@ -297,7 +298,7 @@ void sdldevice_poll_events(SdlDevice* device)
     pthread_mutex_unlock(&device->mutex);
 
     if (should_notify) {
-        printf("sdldevice: interrupt occured!\n");
+        // printf("sdldevice: interrupt occured!\n");
         pthread_cond_signal(&device->interrupt_waiter);
     }
 }
@@ -362,21 +363,16 @@ void memdrive_drive_write(Drive* in_drive, const uint8_t* block, uint16_t i)
 
 __attribute__((unused)) static inline void dump_program(uint16_t* program)
 {
-    for (size_t rip = 0; rip < 80; ++rip) {
-        uint8_t* out = (uint8_t*)program;
-        // clang-format off
-        printf(
-            "out[%2ld] = %c%c%c%c %c%c%c%c\n", rip
-            , out[rip] >> 7 & 1 ? '1' : '0'
-            , out[rip] >> 6 & 1 ? '1' : '0'
-            , out[rip] >> 5 & 1 ? '1' : '0'
-            , out[rip] >> 4 & 1 ? '1' : '0'
-            , out[rip] >> 3 & 1 ? '1' : '0'
-            , out[rip] >> 2 & 1 ? '1' : '0'
-            , out[rip] >> 1 & 1 ? '1' : '0'
-            , out[rip] >> 0 & 1 ? '1' : '0'
-        );
-        // clang-format on
+    for (size_t rip = 20; rip < 60; ++rip) {
+        uint16_t val = program[rip];
+        printf("[%02lx] = %02x %02x  %c%c%c%c %c%c%c%c %c%c%c%c %c%c%c%c "
+               "%s\n",
+            rip * 2,
+            val >> 8,
+            val & 0xff,
+            fmt_binary(val >> 8),
+            fmt_binary(val & 0xff),
+            op_str(val & 0x3f));
     }
 }
 
@@ -384,26 +380,18 @@ int main(void)
 {
     int res;
 
-    SdlDevice io_device;
-    res = sdldevice_construct(&io_device);
-    if (res != 0) {
-        exit(1);
-    }
-
     int main_loop = 1;
     int interrupt_table = 2;
     int keyboard_interrupt = 3;
 
 #define L(LABEL) s_label(LABEL)
-#define s_push_r(REG) s_add_r_i(Rsp, Rsp, 2), s_mov16_mr_r(Rsp, 0, REG)
-#define s_pop_r(REG) s_mov16_r_mr(REG, Rsp, 2), s_sub_r_i(Rsp, Rsp, 2)
 
     Line program_asm[] = {
         // clang-format off
             s_nop(),
 
             // set video character display flag
-            s_or_r_i(Rfl, Rfl, 1 << Fl_Vcd),
+            s_or_i(Rfl, Rfl, 1 << Fl_Vcd),
 
             // print ABC
             s_mov8_mi_i(0x0c00 + 0, 'A'),
@@ -418,18 +406,21 @@ int main(void)
             // load interrupt table
             s_lit_l(interrupt_table),
             // set interrupt flag
-            s_or_r_i(Rfl, Rfl, 1 << Fl_Int),
+            s_or_i(Rfl, Rfl, 1 << Fl_Int),
         L(main_loop),
             s_hlt(),
             s_jmp_l(main_loop),
+            s_nop(),
         L(interrupt_table),
             // size
             s_data_i(1),
             s_data_l(keyboard_interrupt),
             s_nop(),
         L(keyboard_interrupt),
+            s_nop(),
+            s_nop(),
             // clear interrupt flag
-            s_and_r_i(Rfl, Rfl, (uint16_t)~(1 << Fl_Int)),
+            s_and_i(Rfl, Rfl, (uint16_t)~(1 << Fl_Int)),
             // setup stack frame
             s_push_r(Rbp),
             s_mov16_r_r(Rbp, Rsp),
@@ -438,14 +429,15 @@ int main(void)
 
             // read keyboard port
             s_in_i(R0, 0),
-            s_mov8_mi_i(0x0c00 + 4, R0),
+            s_add_i(R0, R0, 'A' - 4),
+            s_mov8_mi_r(0x0c00 + 4, R0),
 
             // tear down frame
             s_pop_r(R0),
             s_mov16_r_r(Rsp, Rbp),
             s_pop_r(Rbp),
             // set interrupt flag
-            s_or_r_i(Rfl, Rfl, 1 << Fl_Int),
+            s_or_i(Rfl, Rfl, 1 << Fl_Int),
             // return from interrupt
             s_iret(),
 
@@ -459,9 +451,17 @@ int main(void)
 
     dump_program(program);
 
+    /*return 0;*/
+
     MemDrive drive;
     memdrive_construct(&drive, (uint64_t*)program, 512);
 
+    SdlDevice io_device;
+    res = sdldevice_construct(&io_device);
+    if (res != 0) {
+        exit(1);
+    }
+
     vm_start(&drive.drive, &io_device.io_device);
 
     sdldevice_destroy(&io_device);
diff --git a/vm/vm.c b/vm/vm.c
index b9c4c2a..8827105 100644
--- a/vm/vm.c
+++ b/vm/vm.c
@@ -8,18 +8,19 @@ typedef struct {
     uint16_t regs[10];
     uint8_t* mem;
     Drive* boot_drive;
-    int16_t seg_count;
-    int16_t seg_size;
+    uint16_t seg_count;
+    uint16_t seg_size;
     IODevice* io_device;
     uint16_t int_table;
     int interrupt_timeout;
     uint16_t keyboard_port_input;
 } VM;
 
-static inline int jump_to_interrupt(VM* vm, uint16_t int_id);
 static inline void run_arithm_ins(VM* vm, uint16_t ins);
+static inline int jump_to_interrupt(VM* vm, uint16_t int_id);
 static inline void maybe_update_vcd(VM* vm, uint16_t addr);
 static inline uint16_t eat_uint16(VM* vm);
+static inline uint16_t read_seg_uint16(VM* vm, uint16_t ptr);
 static inline Op ins_op(uint16_t ins);
 
 static inline Reg ins_dst_reg(uint16_t ins);
@@ -32,8 +33,6 @@ static inline uint16_t ins_reg_val_or_imm(
 static inline uint16_t ins_op1_or_imm(VM* vm, uint16_t ins);
 static inline uint16_t ins_op2_or_imm(VM* vm, uint16_t ins);
 
-static inline const char* op_str(Op op);
-
 void vm_start(Drive* boot_drive, IODevice* io_device)
 {
     const uint16_t seg_count = 16;
@@ -47,7 +46,7 @@ void vm_start(Drive* boot_drive, IODevice* io_device)
         .seg_size = seg_size,
         .io_device = io_device,
         .int_table = 0,
-        .interrupt_timeout = 0,
+        .interrupt_timeout = 10,
         .keyboard_port_input = 0,
     };
 
@@ -67,10 +66,10 @@ void vm_start(Drive* boot_drive, IODevice* io_device)
         uint16_t ins = eat_uint16(vm);
         Op op = ins_op(ins);
 
-        printf("[%3d] = %3d %s\n", *rip - 2, op, op_str(op));
+        /*printf("[%3d] = %3d %s\n", *rip - 2, op, op_str(op));*/
 
-        if (*rip >= 74) {
-            printf("rip >= 74\n");
+        if (*rip >= 300) {
+            printf("killed: rip >= 200\n");
             exit(0);
         }
 
@@ -79,11 +78,12 @@ void vm_start(Drive* boot_drive, IODevice* io_device)
                 break;
             case Op_Hlt:
                 vm->io_device->wait_for_interrupt(vm->io_device);
+                vm->interrupt_timeout = 0;
                 break;
             case Op_Jmp: {
                 bool is_farjump = ins >> 7 & 1;
                 if (is_farjump) {
-                    uint16_t cs = ins_reg_val_or_imm(vm, ins, 8, 12, 0x7);
+                    uint16_t cs = ins_reg_val_or_imm(vm, ins, 8, 13, 0x7);
                     uint16_t op1 = ins_op1_or_imm(vm, ins);
                     *rcs = cs;
                     *rip = op1;
@@ -196,9 +196,8 @@ void vm_start(Drive* boot_drive, IODevice* io_device)
                         vm->regs[dst_reg] = vm->keyboard_port_input;
                         break;
                     default:
-                        fprintf(stderr,
-                            "warning: input,  no device %d\n",
-                            device_id);
+                        fprintf(
+                            stderr, "warning: no input device %d\n", device_id);
                         break;
                 }
                 break;
@@ -210,7 +209,7 @@ void vm_start(Drive* boot_drive, IODevice* io_device)
                 switch (device_id) {
                     default:
                         fprintf(stderr,
-                            "warning: output, no device %d\n",
+                            "warning: no output device %d\n",
                             device_id);
                         break;
                 }
@@ -286,40 +285,6 @@ halt_execution:
     return;
 }
 
-static inline int jump_to_interrupt(VM* vm, uint16_t int_id)
-{
-    uint16_t* rip = &vm->regs[Rip];
-    uint16_t* rsp = &vm->regs[Rsp];
-    uint16_t* rfl = &vm->regs[Rfl];
-    uint16_t* rcs = &vm->regs[Rcs];
-
-    if ((*rfl >> Fl_Int & 1) == 0) {
-        fprintf(stderr, "error: interrupt with unset flag\n");
-        vm->regs[Rfl] |= 1 << Fl_Err;
-        return -1;
-    }
-
-    uint16_t int_table_size = *(uint16_t*)&vm->mem[vm->int_table];
-
-    if (int_id >= int_table_size) {
-        fprintf(stderr, "error: interrupt outside table\n");
-        vm->regs[Rfl] |= 1 << Fl_Err;
-        return -1;
-    }
-
-    uint16_t int_addr = *(uint16_t*)&vm->mem[vm->int_table + int_id * 2 + 2];
-
-    *rsp += 2;
-    *(uint16_t*)&vm->mem[*rsp] = *rcs;
-    *rsp += 2;
-    *(uint16_t*)&vm->mem[*rsp] = *rip;
-
-    *rcs = 0;
-    *rip = int_addr;
-
-    return 0;
-}
-
 static inline void run_arithm_ins(VM* vm, uint16_t ins)
 {
     typedef uint16_t u;
@@ -392,6 +357,40 @@ static inline void run_arithm_ins(VM* vm, uint16_t ins)
     }
 }
 
+static inline int jump_to_interrupt(VM* vm, uint16_t int_id)
+{
+    uint16_t* rip = &vm->regs[Rip];
+    uint16_t* rsp = &vm->regs[Rsp];
+    uint16_t* rfl = &vm->regs[Rfl];
+    uint16_t* rcs = &vm->regs[Rcs];
+
+    if ((*rfl >> Fl_Int & 1) == 0) {
+        fprintf(stderr, "error: interrupt with unset flag\n");
+        vm->regs[Rfl] |= 1 << Fl_Err;
+        return -1;
+    }
+
+    uint16_t int_table_size = *(uint16_t*)&vm->mem[vm->int_table];
+
+    if (int_id >= int_table_size) {
+        fprintf(stderr, "error: interrupt outside table (%d)\n", int_id);
+        vm->regs[Rfl] |= 1 << Fl_Err;
+        return -1;
+    }
+
+    uint16_t int_addr = *(uint16_t*)&vm->mem[vm->int_table + int_id * 2 + 2];
+
+    *rsp += 2;
+    *(uint16_t*)&vm->mem[*rsp] = *rcs;
+    *rsp += 2;
+    *(uint16_t*)&vm->mem[*rsp] = *rip;
+
+    *rcs = 0;
+    *rip = int_addr;
+
+    return 0;
+}
+
 static inline void maybe_update_vcd(VM* vm, uint16_t addr)
 {
     if (!vm->io_device)
@@ -408,13 +407,16 @@ static inline void maybe_update_vcd(VM* vm, uint16_t addr)
 static inline uint16_t eat_uint16(VM* vm)
 {
     uint16_t* rip = &vm->regs[Rip];
-    uint16_t* rcs = &vm->regs[Rcs];
-
-    uint16_t ins = *(uint16_t*)&vm->mem[*rcs * vm->seg_size + *rip];
+    uint16_t ins = read_seg_uint16(vm, *rip);
     *rip += 2;
     return ins;
 }
 
+static inline uint16_t read_seg_uint16(VM* vm, uint16_t ptr)
+{
+    return *(uint16_t*)&vm->mem[vm->regs[Rcs] * vm->seg_size + ptr];
+}
+
 static inline Op ins_op(uint16_t ins)
 {
     return ins & 0x3F;
@@ -466,7 +468,7 @@ static inline uint16_t ins_op2_or_imm(VM* vm, uint16_t ins)
     return ins_reg_val_or_imm(vm, ins, 6, 7, 0x7);
 }
 
-static inline const char* op_str(Op op)
+const char* op_str(Op op)
 {
     switch (op) {
         case Op_Nop:
@@ -532,5 +534,5 @@ static inline const char* op_str(Op op)
         case Op_RMod:
             return "rmod";
     }
-    return "<unknown>";
+    return "---";
 }
diff --git a/vm/vm.h b/vm/vm.h
index a2c9d19..b3c2ff3 100644
--- a/vm/vm.h
+++ b/vm/vm.h
@@ -102,3 +102,5 @@ struct IODevice {
 };
 
 void vm_start(Drive* boot_drive, IODevice* io_device);
+
+const char* op_str(Op op);