/*
 * QuickJS Javascript Engine
 *
 * Copyright (c) 2017-2025 Fabrice Bellard
 * Copyright (c) 2017-2025 Charlie Gordon
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

#include "quickjs-internal.h"



/* ---- Compile-time constant pool entry ---- */
/* Stores raw data during compilation; converted to JSValues when loading into context */
typedef enum { MACH_CP_INT, MACH_CP_FLOAT, MACH_CP_STR } MachCPType;
typedef struct {
  MachCPType type;
  union {
    int32_t ival;   /* integer constant */
    double fval;    /* float constant */
    char *str;      /* owned C string */
  };
} MachCPoolEntry;

/* ---- Compiled output (context-free) ---- */
typedef struct MachCode {
  uint16_t arity;
  uint16_t nr_close_slots;
  uint16_t nr_slots;
  uint16_t entry_point;

  uint32_t cpool_count;
  MachCPoolEntry *cpool;

  uint32_t instr_count;
  MachInstr32 *instructions;

  uint32_t func_count;
  struct MachCode **functions;

  char *name;  /* owned C string, or NULL */
  MachLineEntry *line_table;  /* [instr_count], parallel to instructions[] */
  char *filename;             /* source filename (sys_malloc'd) */
  uint16_t disruption_pc;     /* start of disruption handler (0 = none) */
} MachCode;

/* ---- Helpers ---- */

static JSValue *mach_materialize_cpool(JSContext *ctx, MachCPoolEntry *entries, int count) {
  if (count == 0) { sys_free(entries); return NULL; }
  JSValue *cpool = js_malloc_rt(count * sizeof(JSValue));
  for (int i = 0; i < count; i++) {
    switch (entries[i].type) {
    case MACH_CP_INT:
      cpool[i] = JS_NewInt32(ctx, entries[i].ival);
      break;
    case MACH_CP_FLOAT:
      cpool[i] = JS_NewFloat64(ctx, entries[i].fval);
      break;
    case MACH_CP_STR:
      cpool[i] = js_key_new(ctx, entries[i].str);
      break;
    }
  }
  return cpool;
}

/* ---- Link pass: resolve GETNAME to GETINTRINSIC or GETENV ---- */

static void mach_link_code(JSContext *ctx, JSCodeRegister *code, JSValue env) {
  if (!JS_IsNull(env) && !JS_IsStone(env)) {
    fprintf(stderr, "mach_link_code: ERROR env not stone (code=%s file=%s)\n",
            code->name_cstr ? code->name_cstr : "<unknown>",
            code->filename_cstr ? code->filename_cstr : "<unknown>");
    abort();
  }
  JSGCRef env_ref;
  JS_PushGCRef(ctx, &env_ref);
  env_ref.val = env;
  for (uint32_t i = 0; i < code->instr_count; i++) {
    MachInstr32 instr = code->instructions[i];
    if (MACH_GET_OP(instr) != MACH_GETNAME) continue;
    int a = MACH_GET_A(instr);
    int bx = MACH_GET_Bx(instr);
    int in_env = 0;
    if (!JS_IsNull(env_ref.val) && (uint32_t)bx < code->cpool_count) {
      in_env = JS_HasProperty(ctx, env_ref.val, code->cpool[bx]);
    }
    code->instructions[i] = MACH_ABx(in_env ? MACH_GETENV : MACH_GETINTRINSIC, a, bx);
  }
  for (uint32_t i = 0; i < code->func_count; i++)
    if (code->functions[i]) mach_link_code(ctx, code->functions[i], env_ref.val);
  JS_PopGCRef(ctx, &env_ref);
}

/* Free a MachCode tree (compiled but not yet loaded) */
void JS_FreeMachCode(MachCode *mc) {
  if (!mc) return;
  sys_free(mc->instructions);
  for (uint32_t i = 0; i < mc->cpool_count; i++) {
    if (mc->cpool[i].type == MACH_CP_STR)
      sys_free(mc->cpool[i].str);
  }
  sys_free(mc->cpool);
  for (uint32_t i = 0; i < mc->func_count; i++)
    JS_FreeMachCode(mc->functions[i]);
  sys_free(mc->functions);
  sys_free(mc->name);
  sys_free(mc->line_table);
  sys_free(mc->filename);
  sys_free(mc);
}

/* ---- MachCode binary serialization ---- */

static size_t mach_serialized_size(MachCode *mc) {
  /* Header: 6 x uint16_t + 1 x uint16_t padding = 14 bytes (but let's be explicit) */
  size_t sz = 6 * sizeof(uint16_t); /* arity, nr_close_slots, nr_slots, entry_point, disruption_pc, padding */

  /* name */
  sz += sizeof(uint32_t);
  if (mc->name) sz += strlen(mc->name);

  /* filename */
  sz += sizeof(uint32_t);
  if (mc->filename) sz += strlen(mc->filename);

  /* cpool */
  sz += sizeof(uint32_t); /* cpool_count */
  for (uint32_t i = 0; i < mc->cpool_count; i++) {
    sz += 1; /* type tag */
    switch (mc->cpool[i].type) {
      case MACH_CP_INT:   sz += sizeof(int32_t); break;
      case MACH_CP_FLOAT: sz += sizeof(double); break;
      case MACH_CP_STR:
        sz += sizeof(uint32_t);
        sz += strlen(mc->cpool[i].str);
        break;
    }
  }

  /* instructions + line_table */
  sz += sizeof(uint32_t); /* instr_count */
  sz += mc->instr_count * sizeof(MachInstr32);
  sz += mc->instr_count * sizeof(MachLineEntry);

  /* nested functions */
  sz += sizeof(uint32_t); /* func_count */
  for (uint32_t i = 0; i < mc->func_count; i++)
    sz += mach_serialized_size(mc->functions[i]);

  return sz;
}

static void mach_serialize_write(MachCode *mc, uint8_t **p) {
  uint8_t *w = *p;

  /* Header fields */
  memcpy(w, &mc->arity, 2);          w += 2;
  memcpy(w, &mc->nr_close_slots, 2); w += 2;
  memcpy(w, &mc->nr_slots, 2);       w += 2;
  memcpy(w, &mc->entry_point, 2);    w += 2;
  memcpy(w, &mc->disruption_pc, 2);  w += 2;
  uint16_t pad = 0;
  memcpy(w, &pad, 2);                w += 2;

  /* name */
  uint32_t name_len = mc->name ? (uint32_t)strlen(mc->name) : 0;
  memcpy(w, &name_len, 4); w += 4;
  if (name_len) { memcpy(w, mc->name, name_len); w += name_len; }

  /* filename */
  uint32_t fn_len = mc->filename ? (uint32_t)strlen(mc->filename) : 0;
  memcpy(w, &fn_len, 4); w += 4;
  if (fn_len) { memcpy(w, mc->filename, fn_len); w += fn_len; }

  /* cpool */
  memcpy(w, &mc->cpool_count, 4); w += 4;
  for (uint32_t i = 0; i < mc->cpool_count; i++) {
    uint8_t tag = (uint8_t)mc->cpool[i].type;
    *w++ = tag;
    switch (mc->cpool[i].type) {
      case MACH_CP_INT:
        memcpy(w, &mc->cpool[i].ival, 4); w += 4;
        break;
      case MACH_CP_FLOAT:
        memcpy(w, &mc->cpool[i].fval, 8); w += 8;
        break;
      case MACH_CP_STR: {
        uint32_t slen = (uint32_t)strlen(mc->cpool[i].str);
        memcpy(w, &slen, 4); w += 4;
        memcpy(w, mc->cpool[i].str, slen); w += slen;
        break;
      }
    }
  }

  /* instructions */
  memcpy(w, &mc->instr_count, 4); w += 4;
  memcpy(w, mc->instructions, mc->instr_count * sizeof(MachInstr32));
  w += mc->instr_count * sizeof(MachInstr32);

  /* line_table (write zeros if NULL) */
  if (mc->line_table) {
    memcpy(w, mc->line_table, mc->instr_count * sizeof(MachLineEntry));
  } else {
    memset(w, 0, mc->instr_count * sizeof(MachLineEntry));
  }
  w += mc->instr_count * sizeof(MachLineEntry);

  /* nested functions */
  memcpy(w, &mc->func_count, 4); w += 4;
  *p = w;
  for (uint32_t i = 0; i < mc->func_count; i++)
    mach_serialize_write(mc->functions[i], p);
}

uint8_t *JS_SerializeMachCode(MachCode *mc, size_t *out_size) {
  if (!mc) return NULL;
  size_t sz = mach_serialized_size(mc);
  uint8_t *buf = sys_malloc(sz);
  if (!buf) return NULL;
  uint8_t *p = buf;
  mach_serialize_write(mc, &p);
  *out_size = sz;
  return buf;
}

#define DESER_CHECK(cond) do { if (!(cond)) goto fail; } while(0)

static MachCode *mach_deserialize_read(const uint8_t **p, const uint8_t *end) {
  const uint8_t *r = *p;

  /* Need at least the fixed header: 6 * uint16_t = 12 bytes */
  if (r + 12 > end) return NULL;

  MachCode *mc = sys_malloc(sizeof(MachCode));
  if (!mc) return NULL;
  memset(mc, 0, sizeof(MachCode));

  memcpy(&mc->arity, r, 2);          r += 2;
  memcpy(&mc->nr_close_slots, r, 2); r += 2;
  memcpy(&mc->nr_slots, r, 2);       r += 2;
  memcpy(&mc->entry_point, r, 2);    r += 2;
  memcpy(&mc->disruption_pc, r, 2);  r += 2;
  r += 2; /* skip padding */

  /* name */
  DESER_CHECK(r + 4 <= end);
  uint32_t name_len;
  memcpy(&name_len, r, 4); r += 4;
  DESER_CHECK(r + name_len <= end);
  if (name_len) {
    mc->name = sys_malloc(name_len + 1);
    memcpy(mc->name, r, name_len);
    mc->name[name_len] = '\0';
    r += name_len;
  }

  /* filename */
  DESER_CHECK(r + 4 <= end);
  uint32_t fn_len;
  memcpy(&fn_len, r, 4); r += 4;
  DESER_CHECK(r + fn_len <= end);
  if (fn_len) {
    mc->filename = sys_malloc(fn_len + 1);
    memcpy(mc->filename, r, fn_len);
    mc->filename[fn_len] = '\0';
    r += fn_len;
  }

  /* cpool */
  DESER_CHECK(r + 4 <= end);
  memcpy(&mc->cpool_count, r, 4); r += 4;
  if (mc->cpool_count) {
    mc->cpool = sys_malloc(mc->cpool_count * sizeof(MachCPoolEntry));
    for (uint32_t i = 0; i < mc->cpool_count; i++) {
      DESER_CHECK(r + 1 <= end);
      uint8_t tag = *r++;
      mc->cpool[i].type = (MachCPType)tag;
      switch (tag) {
        case MACH_CP_INT:
          DESER_CHECK(r + 4 <= end);
          memcpy(&mc->cpool[i].ival, r, 4); r += 4;
          break;
        case MACH_CP_FLOAT:
          DESER_CHECK(r + 8 <= end);
          memcpy(&mc->cpool[i].fval, r, 8); r += 8;
          break;
        case MACH_CP_STR: {
          DESER_CHECK(r + 4 <= end);
          uint32_t slen;
          memcpy(&slen, r, 4); r += 4;
          DESER_CHECK(r + slen <= end);
          mc->cpool[i].str = sys_malloc(slen + 1);
          memcpy(mc->cpool[i].str, r, slen);
          mc->cpool[i].str[slen] = '\0';
          r += slen;
          break;
        }
        default:
          goto fail;
      }
    }
  }

  /* instructions */
  DESER_CHECK(r + 4 <= end);
  memcpy(&mc->instr_count, r, 4); r += 4;
  DESER_CHECK(r + mc->instr_count * sizeof(MachInstr32) <= end);
  mc->instructions = sys_malloc(mc->instr_count * sizeof(MachInstr32));
  memcpy(mc->instructions, r, mc->instr_count * sizeof(MachInstr32));
  r += mc->instr_count * sizeof(MachInstr32);

  /* line_table */
  DESER_CHECK(r + mc->instr_count * sizeof(MachLineEntry) <= end);
  mc->line_table = sys_malloc(mc->instr_count * sizeof(MachLineEntry));
  memcpy(mc->line_table, r, mc->instr_count * sizeof(MachLineEntry));
  r += mc->instr_count * sizeof(MachLineEntry);

  /* nested functions */
  DESER_CHECK(r + 4 <= end);
  memcpy(&mc->func_count, r, 4); r += 4;
  if (mc->func_count) {
    mc->functions = sys_malloc(mc->func_count * sizeof(MachCode *));
    for (uint32_t i = 0; i < mc->func_count; i++) {
      mc->functions[i] = mach_deserialize_read(&r, end);
      if (!mc->functions[i]) {
        mc->func_count = i; /* only free what we allocated */
        goto fail;
      }
    }
  }

  *p = r;
  return mc;

fail:
  JS_FreeMachCode(mc);
  return NULL;
}

#undef DESER_CHECK

MachCode *JS_DeserializeMachCode(const uint8_t *data, size_t size) {
  if (!data || size == 0) return NULL;
  const uint8_t *p = data;
  const uint8_t *end = data + size;
  MachCode *mc = mach_deserialize_read(&p, end);
  if (mc && p != end) {
    /* Trailing data — treat as error */
    JS_FreeMachCode(mc);
    return NULL;
  }
  return mc;
}

/* Load a MachCode into a JSCodeRegister (materializes JSValues, needs ctx) */
JSCodeRegister *JS_LoadMachCode(JSContext *ctx, MachCode *mc, JSValue env) {
  /* Protect env from GC — materialize/link calls can trigger collection */
  JSGCRef env_ref;
  JS_PushGCRef(ctx, &env_ref);
  env_ref.val = env;

  JSCodeRegister *code = js_mallocz_rt(sizeof(JSCodeRegister));
  code->arity = mc->arity;
  code->nr_close_slots = mc->nr_close_slots;
  code->nr_slots = mc->nr_slots;
  code->entry_point = mc->entry_point;
  code->instr_count = mc->instr_count;
  code->instructions = mc->instructions;  /* transfer ownership */
  mc->instructions = NULL;

  /* Materialize cpool: raw -> JSValue */
  code->cpool_count = mc->cpool_count;
  code->cpool = mach_materialize_cpool(ctx, mc->cpool, mc->cpool_count);

  /* Recursively load nested functions */
  code->func_count = mc->func_count;
  if (mc->func_count > 0) {
    code->functions = js_malloc_rt(mc->func_count * sizeof(JSCodeRegister *));
    for (uint32_t i = 0; i < mc->func_count; i++)
      code->functions[i] = JS_LoadMachCode(ctx, mc->functions[i], env_ref.val);
  } else {
    code->functions = NULL;
  }

  /* Intern function name */
  code->name = mc->name ? js_key_new(ctx, mc->name) : JS_NULL;

  /* Transfer debug info */
  code->line_table = mc->line_table;
  mc->line_table = NULL;
  code->filename_cstr = mc->filename ? js_strdup_rt(mc->filename) : NULL;
  code->name_cstr = mc->name ? js_strdup_rt(mc->name) : NULL;

  code->disruption_pc = mc->disruption_pc;

  /* Link: resolve GETNAME to GETENV/GETINTRINSIC */
  mach_link_code(ctx, code, env_ref.val);

  JS_PopGCRef(ctx, &env_ref);
  return code;
}

/* Free a JSCodeRegister and all nested functions */
static void js_free_code_register(JSCodeRegister *code) {
  if (!code) return;
  js_free_rt(code->instructions);
  js_free_rt(code->cpool);
  for (uint32_t i = 0; i < code->func_count; i++) {
    js_free_code_register(code->functions[i]);
  }
  js_free_rt(code->functions);
  js_free_rt(code->line_table);
  js_free_rt(code->filename_cstr);
  js_free_rt(code->name_cstr);
  js_free_rt(code);
}


/* ============================================================
   MACH VM — register-based bytecode interpreter
   ============================================================ */

/* Allocate a JSFrameRegister on the GC heap */
JSFrameRegister *alloc_frame_register(JSContext *ctx, int slot_count) {
  size_t size = sizeof(JSFrameRegister) + slot_count * sizeof(JSValue);
  JSFrameRegister *frame = js_malloc(ctx, size);
  if (!frame) return NULL;

  /* cap56 = slot count (used by gc_object_size) */
  frame->header = objhdr_make(slot_count, OBJ_FRAME, 0, 0, 0, 0);
  frame->function = JS_NULL;
  frame->caller = JS_NULL;
  frame->address = JS_NewInt32(ctx, 0);

  /* Initialize slots to null */
  for (int i = 0; i < slot_count; i++) {
    frame->slots[i] = JS_NULL;
  }

  return frame;
}

/* Create a register-based function from JSCodeRegister */
JSValue js_new_register_function(JSContext *ctx, JSCodeRegister *code, JSValue env, JSValue outer_frame) {
  /* Protect env and outer_frame from GC — js_mallocz can trigger
     collection which moves heap objects, invalidating stack-local copies */
  JSGCRef env_ref, frame_ref;
  JS_PushGCRef(ctx, &env_ref);
  env_ref.val = env;
  JS_PushGCRef(ctx, &frame_ref);
  frame_ref.val = outer_frame;

  JSFunction *fn = js_mallocz(ctx, sizeof(JSFunction));
  if (!fn) {
    JS_PopGCRef(ctx, &frame_ref);
    JS_PopGCRef(ctx, &env_ref);
    return JS_EXCEPTION;
  }

  fn->header = objhdr_make(0, OBJ_FUNCTION, 0, 0, 0, 0);
  fn->kind = JS_FUNC_KIND_REGISTER;
  fn->length = code->arity;
  fn->name = code->name;
  fn->u.reg.code = code;
  fn->u.reg.env_record = env_ref.val;
  fn->u.reg.outer_frame = frame_ref.val;

  JS_PopGCRef(ctx, &frame_ref);
  JS_PopGCRef(ctx, &env_ref);
  return JS_MKPTR(fn);
}

/* Binary operations helper */
static JSValue reg_vm_binop(JSContext *ctx, int op, JSValue a, JSValue b) {
  /* Fast path for integers */
  if (JS_VALUE_IS_BOTH_INT(a, b)) {
    int32_t ia = JS_VALUE_GET_INT(a);
    int32_t ib = JS_VALUE_GET_INT(b);
    switch (op) {
    case MACH_ADD: {
      int64_t r = (int64_t)ia + (int64_t)ib;
      if (r >= INT32_MIN && r <= INT32_MAX)
        return JS_NewInt32(ctx, (int32_t)r);
      return JS_NewFloat64(ctx, (double)r);
    }
    case MACH_SUB: {
      int64_t r = (int64_t)ia - (int64_t)ib;
      if (r >= INT32_MIN && r <= INT32_MAX)
        return JS_NewInt32(ctx, (int32_t)r);
      return JS_NewFloat64(ctx, (double)r);
    }
    case MACH_MUL: {
      int64_t r = (int64_t)ia * (int64_t)ib;
      if (r >= INT32_MIN && r <= INT32_MAX)
        return JS_NewInt32(ctx, (int32_t)r);
      return JS_NewFloat64(ctx, (double)r);
    }
    case MACH_DIV:
      if (ib == 0) return JS_NULL;
      if (ia % ib == 0) return JS_NewInt32(ctx, ia / ib);
      return JS_NewFloat64(ctx, (double)ia / (double)ib);
    case MACH_MOD:
      if (ib == 0) return JS_NULL;
      return JS_NewInt32(ctx, ia % ib);
    case MACH_EQ:
      return JS_NewBool(ctx, ia == ib);
    case MACH_NEQ:
      return JS_NewBool(ctx, ia != ib);
    case MACH_LT:
      return JS_NewBool(ctx, ia < ib);
    case MACH_LE:
      return JS_NewBool(ctx, ia <= ib);
    case MACH_GT:
      return JS_NewBool(ctx, ia > ib);
    case MACH_GE:
      return JS_NewBool(ctx, ia >= ib);
    case MACH_BAND:
      return JS_NewInt32(ctx, ia & ib);
    case MACH_BOR:
      return JS_NewInt32(ctx, ia | ib);
    case MACH_BXOR:
      return JS_NewInt32(ctx, ia ^ ib);
    case MACH_SHL:
      return JS_NewInt32(ctx, ia << (ib & 31));
    case MACH_SHR:
      return JS_NewInt32(ctx, ia >> (ib & 31));
    case MACH_USHR:
      return JS_NewInt32(ctx, (uint32_t)ia >> (ib & 31));
    default:
      break;
    }
  }

  /* String concat for ADD */
  if (op == MACH_ADD && mist_is_text(a) && mist_is_text(b))
    return JS_ConcatString(ctx, a, b);

  /* Comparison ops allow mixed types — return false for mismatches */
  if (op >= MACH_EQ && op <= MACH_GE) {
    /* Fast path: identical values (chase pointers for forwarded objects) */
    {
      JSValue ca = JS_IsPtr(a) ? JS_MKPTR(chase(a)) : a;
      JSValue cb = JS_IsPtr(b) ? JS_MKPTR(chase(b)) : b;
      if (ca == cb) {
        if (op == MACH_EQ || op == MACH_LE || op == MACH_GE) return JS_TRUE;
        if (op == MACH_NEQ) return JS_FALSE;
      }
    }
    if (JS_IsNumber(a) && JS_IsNumber(b)) {
      double da, db;
      JS_ToFloat64(ctx, &da, a);
      JS_ToFloat64(ctx, &db, b);
      switch (op) {
      case MACH_EQ: return JS_NewBool(ctx, da == db);
      case MACH_NEQ: return JS_NewBool(ctx, da != db);
      case MACH_LT: return JS_NewBool(ctx, da < db);
      case MACH_LE: return JS_NewBool(ctx, da <= db);
      case MACH_GT: return JS_NewBool(ctx, da > db);
      case MACH_GE: return JS_NewBool(ctx, da >= db);
      default: break;
      }
    }
    /* String comparisons */
    if (mist_is_text(a) && mist_is_text(b)) {
      int cmp = js_string_compare_value(ctx, a, b, FALSE);
      switch (op) {
      case MACH_EQ: return JS_NewBool(ctx, cmp == 0);
      case MACH_NEQ: return JS_NewBool(ctx, cmp != 0);
      case MACH_LT: return JS_NewBool(ctx, cmp < 0);
      case MACH_LE: return JS_NewBool(ctx, cmp <= 0);
      case MACH_GT: return JS_NewBool(ctx, cmp > 0);
      case MACH_GE: return JS_NewBool(ctx, cmp >= 0);
      default: break;
      }
    }
    /* Null comparisons */
    if (JS_IsNull(a) && JS_IsNull(b)) {
      if (op == MACH_EQ || op == MACH_LE || op == MACH_GE)
        return JS_TRUE;
      return JS_FALSE;
    }
    /* Boolean comparisons */
    if (JS_IsBool(a) && JS_IsBool(b)) {
      int ba = JS_VALUE_GET_BOOL(a);
      int bb = JS_VALUE_GET_BOOL(b);
      switch (op) {
      case MACH_EQ: return JS_NewBool(ctx, ba == bb);
      case MACH_NEQ: return JS_NewBool(ctx, ba != bb);
      case MACH_LT: return JS_NewBool(ctx, ba < bb);
      case MACH_LE: return JS_NewBool(ctx, ba <= bb);
      case MACH_GT: return JS_NewBool(ctx, ba > bb);
      case MACH_GE: return JS_NewBool(ctx, ba >= bb);
      default: break;
      }
    }
    /* Different types: EQ→false, NEQ→true, others→false */
    if (op == MACH_NEQ) return JS_NewBool(ctx, 1);
    return JS_NewBool(ctx, 0);
  }

  /* Numeric operations — both must be numeric */
  if (JS_IsNumber(a) && JS_IsNumber(b)) {
    double da, db;
    JS_ToFloat64(ctx, &da, a);
    JS_ToFloat64(ctx, &db, b);
    switch (op) {
    case MACH_ADD: {
      double r = da + db;
      if (!isfinite(r)) return JS_NULL;
      return JS_NewFloat64(ctx, r);
    }
    case MACH_SUB: {
      double r = da - db;
      if (!isfinite(r)) return JS_NULL;
      return JS_NewFloat64(ctx, r);
    }
    case MACH_MUL: {
      double r = da * db;
      if (!isfinite(r)) return JS_NULL;
      return JS_NewFloat64(ctx, r);
    }
    case MACH_DIV: {
      if (db == 0.0) return JS_NULL;
      double r = da / db;
      if (!isfinite(r)) return JS_NULL;
      return JS_NewFloat64(ctx, r);
    }
    case MACH_MOD: {
      if (db == 0.0) return JS_NULL;
      double r = fmod(da, db);
      if (!isfinite(r)) return JS_NULL;
      return JS_NewFloat64(ctx, r);
    }
    case MACH_POW: {
      double r = pow(da, db);
      if (!isfinite(r) && isfinite(da) && isfinite(db)) return JS_NULL;
      return JS_NewFloat64(ctx, r);
    }
    case MACH_BAND: case MACH_BOR: case MACH_BXOR:
    case MACH_SHL: case MACH_SHR: case MACH_USHR: {
      int32_t ia = (int32_t)da;
      int32_t ib = (int32_t)db;
      switch (op) {
      case MACH_BAND: return JS_NewInt32(ctx, ia & ib);
      case MACH_BOR: return JS_NewInt32(ctx, ia | ib);
      case MACH_BXOR: return JS_NewInt32(ctx, ia ^ ib);
      case MACH_SHL: return JS_NewInt32(ctx, ia << (ib & 31));
      case MACH_SHR: return JS_NewInt32(ctx, ia >> (ib & 31));
      case MACH_USHR: return JS_NewInt32(ctx, (uint32_t)ia >> (ib & 31));
      default: break;
      }
    }
    default: break;
    }
  }

  /* Type mismatch — disrupt */
  return JS_ThrowTypeError(ctx, "type mismatch in binary operation");
}


#ifdef HAVE_ASAN
void __asan_on_error(void) {
  JSContext *ctx = __asan_js_ctx;
  if (!ctx) return;
  if (JS_IsNull(ctx->reg_current_frame)) return;
  JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(ctx->reg_current_frame);
  uint32_t cur_pc = ctx->current_register_pc;
  fprintf(stderr, "\n=== ASAN error: VM stack trace ===\n");
  int is_first = 1;
  while (frame) {
    if (!mist_is_function(frame->function)) break;
    JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
    const char *func_name = NULL;
    const char *file = NULL;
    uint16_t line = 0;
    uint32_t pc = is_first ? cur_pc : 0;
    if (fn->kind == JS_FUNC_KIND_REGISTER && fn->u.reg.code) {
      JSCodeRegister *code = fn->u.reg.code;
      file = code->filename_cstr;
      func_name = code->name_cstr;
      if (!is_first)
        pc = (uint32_t)(JS_VALUE_GET_INT(frame->address) >> 16);
      if (code->line_table && pc < code->instr_count)
        line = code->line_table[pc].line;
    }
    fprintf(stderr, "  %s (%s:%u)\n",
            func_name ? func_name : "<anonymous>",
            file ? file : "<unknown>", line);
    if (JS_IsNull(frame->caller)) break;
    frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller);
    is_first = 0;
  }
  fprintf(stderr, "=================================\n");
}
#endif

/* Main register VM execution loop */
JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
                                 JSValue this_obj, int argc, JSValue *argv,
                                 JSValue env, JSValue outer_frame) {
  JSGCRef frame_ref;
  JSFrameRegister *frame;
  uint32_t pc;
  JSValue result;

  /* Resume path: if VM was suspended, restore state and jump into dispatch */
  if (ctx->suspended) {
    ctx->suspended = 0;
    JS_AddGCRef(ctx, &frame_ref);
    frame_ref.val = ctx->suspended_frame_ref.val;
    ctx->suspended_frame_ref.val = JS_NULL;
    frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
    JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
    code = fn->u.reg.code;
    env = fn->u.reg.env_record;
    pc = ctx->suspended_pc;
    result = JS_NULL;
#ifdef HAVE_ASAN
    __asan_js_ctx = ctx;
#endif
    goto vm_dispatch;
  }

  {
  /* Normal path: set up a new call */
  /* Protect env and outer_frame from GC — alloc_frame_register can trigger
     collection which moves heap objects, invalidating stack-local copies */
  JSGCRef env_gc, of_gc;
  JS_PushGCRef(ctx, &env_gc);
  env_gc.val = env;
  JS_PushGCRef(ctx, &of_gc);
  of_gc.val = outer_frame;

  /* Protect argv and this_obj from GC using JSGCRef.
     alloc_frame_register and js_new_register_function can trigger GC. */
  int nargs_copy = (argc < code->arity) ? argc : code->arity;
  JSGCRef this_gc;
  JS_PushGCRef(ctx, &this_gc);
  this_gc.val = this_obj;
  JSGCRef arg_gcs[nargs_copy > 0 ? nargs_copy : 1];
  for (int i = 0; i < nargs_copy; i++) {
    JS_PushGCRef(ctx, &arg_gcs[i]);
    arg_gcs[i].val = argv[i];
  }

  /* Allocate initial frame */
  frame = alloc_frame_register(ctx, code->nr_slots);
  if (!frame) {
    for (int i = nargs_copy - 1; i >= 0; i--) JS_PopGCRef(ctx, &arg_gcs[i]);
    JS_PopGCRef(ctx, &this_gc);
    JS_PopGCRef(ctx, &of_gc);
    JS_PopGCRef(ctx, &env_gc);
    return JS_EXCEPTION;
  }

  /* Protect frame from GC */
  JS_AddGCRef(ctx, &frame_ref);
  frame_ref.val = JS_MKPTR(frame);
#ifdef HAVE_ASAN
  __asan_js_ctx = ctx;
#endif

  /* Setup initial frame — wrap top-level code in a function object so that
     returning from a called register function can read code/env from frame */
  JSValue top_fn = js_new_register_function(ctx, code, env_gc.val, of_gc.val);
  env = env_gc.val; /* refresh — GC may have moved env during allocation */
  frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
  frame->function = top_fn;
  frame->slots[0] = this_gc.val;  /* slot 0 = this */

  /* Copy arguments from GC-safe refs */
  for (int i = 0; i < nargs_copy; i++) {
    frame->slots[1 + i] = arg_gcs[i].val;
  }
  for (int i = nargs_copy - 1; i >= 0; i--) JS_PopGCRef(ctx, &arg_gcs[i]);
  JS_PopGCRef(ctx, &this_gc);
  JS_PopGCRef(ctx, &of_gc);
  JS_PopGCRef(ctx, &env_gc);

  pc = code->entry_point;
  result = JS_NULL;
  } /* end normal path block */

vm_dispatch:
  /* Execution loop — 32-bit instruction dispatch */
  for (;;) {
#ifndef NDEBUG
    if (pc >= code->instr_count) {
      fprintf(stderr, "mach VM: pc %u overran code->instr_count %u (missing RETURN/RETNIL?)\n",
              pc, code->instr_count);
      result = JS_ThrowInternalError(ctx, "pc overrun");
      goto done;
    }
#endif

    MachInstr32 instr;
    int op, a, b, c;

#ifdef __GNUC__
    /* Computed goto dispatch — each opcode gets its own branch predictor entry */
    /* Use a macro to generate consistent dispatch table entries and labels */
#define DT(x) [x] = &&op_##x
    static const void *dispatch_table[256] = {
      [0 ... 255] = &&op_DEFAULT,
      DT(MACH_LOADK), DT(MACH_LOADI),
      DT(MACH_LOADNULL), DT(MACH_LOADTRUE),
      DT(MACH_LOADFALSE), DT(MACH_MOVE),
      DT(MACH_ADD), DT(MACH_SUB),
      DT(MACH_MUL), DT(MACH_DIV),
      DT(MACH_MOD), DT(MACH_POW),
      DT(MACH_NEG),
      DT(MACH_EQ), DT(MACH_NEQ),
      DT(MACH_LT), DT(MACH_LE),
      DT(MACH_GT), DT(MACH_GE),
      DT(MACH_LNOT), DT(MACH_BNOT),
      DT(MACH_BAND), DT(MACH_BOR),
      DT(MACH_BXOR), DT(MACH_SHL),
      DT(MACH_SHR), DT(MACH_USHR),
      DT(MACH_GETFIELD), DT(MACH_SETFIELD),
      DT(MACH_GETINDEX), DT(MACH_SETINDEX),
      DT(MACH_GETNAME), DT(MACH_GETINTRINSIC),
      DT(MACH_GETENV),
      DT(MACH_GETUP), DT(MACH_SETUP),
      DT(MACH_JMP), DT(MACH_JMPTRUE),
      DT(MACH_JMPFALSE), DT(MACH_JMPNULL),
      DT(MACH_RETURN), DT(MACH_RETNIL),
      DT(MACH_NEWOBJECT), DT(MACH_NEWARRAY),
      DT(MACH_CLOSURE),
      DT(MACH_THROW), DT(MACH_PUSH), DT(MACH_POP),
      DT(MACH_DELETE), DT(MACH_DELETEINDEX),
      DT(MACH_HASPROP), DT(MACH_REGEXP),
      DT(MACH_EQ_TOL), DT(MACH_NEQ_TOL),
      DT(MACH_NOP),
      DT(MACH_CONCAT),
      DT(MACH_EQ_INT), DT(MACH_NE_INT),
      DT(MACH_LT_INT), DT(MACH_LE_INT),
      DT(MACH_GT_INT), DT(MACH_GE_INT),
      DT(MACH_EQ_FLOAT), DT(MACH_NE_FLOAT),
      DT(MACH_LT_FLOAT), DT(MACH_LE_FLOAT),
      DT(MACH_GT_FLOAT), DT(MACH_GE_FLOAT),
      DT(MACH_EQ_TEXT), DT(MACH_NE_TEXT),
      DT(MACH_LT_TEXT), DT(MACH_LE_TEXT),
      DT(MACH_GT_TEXT), DT(MACH_GE_TEXT),
      DT(MACH_EQ_BOOL), DT(MACH_NE_BOOL),
      DT(MACH_IS_IDENTICAL),
      DT(MACH_IS_INT), DT(MACH_IS_NUM),
      DT(MACH_IS_TEXT), DT(MACH_IS_BOOL),
      DT(MACH_IS_NULL),
      DT(MACH_NOT), DT(MACH_AND), DT(MACH_OR),
      DT(MACH_BITNOT), DT(MACH_BITAND),
      DT(MACH_BITOR), DT(MACH_BITXOR),
      DT(MACH_LOAD_FIELD), DT(MACH_STORE_FIELD),
      DT(MACH_LOAD_INDEX), DT(MACH_STORE_INDEX),
      DT(MACH_LOAD_DYNAMIC), DT(MACH_STORE_DYNAMIC),
      DT(MACH_NEWRECORD),
      DT(MACH_FRAME), DT(MACH_SETARG),
      DT(MACH_INVOKE), DT(MACH_GOFRAME),
      DT(MACH_GOINVOKE),
      DT(MACH_JMPNOTNULL),
      DT(MACH_DISRUPT),
      DT(MACH_IN),
      DT(MACH_IS_ARRAY), DT(MACH_IS_FUNC),
      DT(MACH_IS_RECORD), DT(MACH_IS_STONE),
      DT(MACH_LENGTH), DT(MACH_IS_PROXY),
    };
#undef DT
#define VM_DECODE()  do { \
      instr = code->instructions[pc++]; \
      op = MACH_GET_OP(instr); \
      a = MACH_GET_A(instr); \
      b = MACH_GET_B(instr); \
      c = MACH_GET_C(instr); \
    } while(0)
#define VM_DISPATCH() do { VM_DECODE(); goto *dispatch_table[op]; } while(0)
/* VM_CASE: dual-label — goto target for computed goto + case for switch fallback */
#define VM_CASE(x) op_##x: case x
#define VM_DEFAULT op_DEFAULT: default
#define VM_BREAK() VM_DISPATCH()
#else
#define VM_DECODE() do { \
      instr = code->instructions[pc++]; \
      op = MACH_GET_OP(instr); \
      a = MACH_GET_A(instr); \
      b = MACH_GET_B(instr); \
      c = MACH_GET_C(instr); \
    } while(0)
#define VM_DISPATCH() do { VM_DECODE(); } while(0)
#define VM_CASE(x) case x
#define VM_DEFAULT default
#define VM_BREAK() break
#endif

    VM_DECODE();
#ifdef __GNUC__
    goto *dispatch_table[op];
#endif
    switch (op) {
    VM_CASE(MACH_NOP):
      VM_BREAK();

    VM_CASE(MACH_LOADK): {
      int bx = MACH_GET_Bx(instr);
      if (bx < (int)code->cpool_count)
        frame->slots[a] = code->cpool[bx];
      VM_BREAK();
    }

    VM_CASE(MACH_LOADI):
      frame->slots[a] = JS_NewInt32(ctx, MACH_GET_sBx(instr));
      VM_BREAK();

    VM_CASE(MACH_LOADNULL):
      frame->slots[a] = JS_NULL;
      VM_BREAK();

    VM_CASE(MACH_LOADTRUE):
      frame->slots[a] = JS_TRUE;
      VM_BREAK();

    VM_CASE(MACH_LOADFALSE):
      frame->slots[a] = JS_FALSE;
      VM_BREAK();

    VM_CASE(MACH_MOVE):
      frame->slots[a] = frame->slots[b];
      VM_BREAK();

    /* Arithmetic — mcode guarantees both operands are numbers */
    VM_CASE(MACH_ADD): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        int64_t r = (int64_t)JS_VALUE_GET_INT(left) + (int64_t)JS_VALUE_GET_INT(right);
        frame->slots[a] = (r >= INT32_MIN && r <= INT32_MAX) ? JS_NewInt32(ctx, (int32_t)r) : JS_NewFloat64(ctx, (double)r);
      } else {
        double da, db, r;
        JS_ToFloat64(ctx, &da, left);
        JS_ToFloat64(ctx, &db, right);
        r = da + db;
        frame->slots[a] = !isfinite(r) ? JS_NULL : JS_NewFloat64(ctx, r);
      }
      VM_BREAK();
    }
    VM_CASE(MACH_SUB): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        int64_t r = (int64_t)JS_VALUE_GET_INT(left) - (int64_t)JS_VALUE_GET_INT(right);
        frame->slots[a] = (r >= INT32_MIN && r <= INT32_MAX) ? JS_NewInt32(ctx, (int32_t)r) : JS_NewFloat64(ctx, (double)r);
      } else {
        double da, db, r;
        JS_ToFloat64(ctx, &da, left);
        JS_ToFloat64(ctx, &db, right);
        r = da - db;
        frame->slots[a] = !isfinite(r) ? JS_NULL : JS_NewFloat64(ctx, r);
      }
      VM_BREAK();
    }
    VM_CASE(MACH_MUL): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        int64_t r = (int64_t)JS_VALUE_GET_INT(left) * (int64_t)JS_VALUE_GET_INT(right);
        frame->slots[a] = (r >= INT32_MIN && r <= INT32_MAX) ? JS_NewInt32(ctx, (int32_t)r) : JS_NewFloat64(ctx, (double)r);
      } else {
        double da, db, r;
        JS_ToFloat64(ctx, &da, left);
        JS_ToFloat64(ctx, &db, right);
        r = da * db;
        frame->slots[a] = !isfinite(r) ? JS_NULL : JS_NewFloat64(ctx, r);
      }
      VM_BREAK();
    }
    VM_CASE(MACH_DIV): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        int32_t ia = JS_VALUE_GET_INT(left), ib = JS_VALUE_GET_INT(right);
        if (ib != 0 && ia % ib == 0)
          frame->slots[a] = JS_NewInt32(ctx, ia / ib);
        else if (ib != 0)
          frame->slots[a] = JS_NewFloat64(ctx, (double)ia / (double)ib);
        else
          frame->slots[a] = JS_NULL;
      } else {
        double da, db, r;
        JS_ToFloat64(ctx, &da, left);
        JS_ToFloat64(ctx, &db, right);
        if (db == 0.0) { frame->slots[a] = JS_NULL; }
        else {
          r = da / db;
          frame->slots[a] = !isfinite(r) ? JS_NULL : JS_NewFloat64(ctx, r);
        }
      }
      VM_BREAK();
    }
    VM_CASE(MACH_MOD): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        int32_t ib = JS_VALUE_GET_INT(right);
        frame->slots[a] = (ib != 0) ? JS_NewInt32(ctx, JS_VALUE_GET_INT(left) % ib) : JS_NULL;
      } else {
        double da, db, r;
        JS_ToFloat64(ctx, &da, left);
        JS_ToFloat64(ctx, &db, right);
        if (db == 0.0) { frame->slots[a] = JS_NULL; }
        else {
          r = fmod(da, db);
          frame->slots[a] = !isfinite(r) ? JS_NULL : JS_NewFloat64(ctx, r);
        }
      }
      VM_BREAK();
    }
    VM_CASE(MACH_POW): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        double r = pow((double)JS_VALUE_GET_INT(left), (double)JS_VALUE_GET_INT(right));
        if (!isfinite(r)) frame->slots[a] = JS_NULL;
        else if (r >= INT32_MIN && r <= INT32_MAX && r == (int32_t)r)
          frame->slots[a] = JS_NewInt32(ctx, (int32_t)r);
        else
          frame->slots[a] = JS_NewFloat64(ctx, r);
      } else {
        double da, db, r;
        JS_ToFloat64(ctx, &da, left);
        JS_ToFloat64(ctx, &db, right);
        r = pow(da, db);
        frame->slots[a] = (!isfinite(r) && isfinite(da) && isfinite(db)) ? JS_NULL : JS_NewFloat64(ctx, r);
      }
      VM_BREAK();
    }

    /* Comparison — inline integer fast paths */
    VM_CASE(MACH_EQ): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) == JS_VALUE_GET_INT(right));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_EQ, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_NEQ): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) != JS_VALUE_GET_INT(right));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_NEQ, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_LT): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) < JS_VALUE_GET_INT(right));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_LT, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_LE): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) <= JS_VALUE_GET_INT(right));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_LE, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_GT): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) > JS_VALUE_GET_INT(right));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_GT, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_GE): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) >= JS_VALUE_GET_INT(right));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_GE, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }

    /* Bitwise — inline integer fast paths */
    VM_CASE(MACH_BAND): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewInt32(ctx, JS_VALUE_GET_INT(left) & JS_VALUE_GET_INT(right));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_BAND, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_BOR): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewInt32(ctx, JS_VALUE_GET_INT(left) | JS_VALUE_GET_INT(right));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_BOR, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_BXOR): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewInt32(ctx, JS_VALUE_GET_INT(left) ^ JS_VALUE_GET_INT(right));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_BXOR, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_SHL): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewInt32(ctx, JS_VALUE_GET_INT(left) << (JS_VALUE_GET_INT(right) & 31));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_SHL, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_SHR): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewInt32(ctx, JS_VALUE_GET_INT(left) >> (JS_VALUE_GET_INT(right) & 31));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_SHR, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_USHR): {
      JSValue left = frame->slots[b], right = frame->slots[c];
      if (JS_VALUE_IS_BOTH_INT(left, right)) {
        frame->slots[a] = JS_NewInt32(ctx, (uint32_t)JS_VALUE_GET_INT(left) >> (JS_VALUE_GET_INT(right) & 31));
      } else {
        JSValue res = reg_vm_binop(ctx, MACH_USHR, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) goto disrupt;
        frame->slots[a] = res;
      }
      VM_BREAK();
    }

    VM_CASE(MACH_EQ_TOL):
    VM_CASE(MACH_NEQ_TOL): {
      /* A=dest, B=base, C=3; args in R(B), R(B+1), R(B+2) */
      JSValue left = frame->slots[b];
      JSValue right = frame->slots[b + 1];
      JSValue tol = frame->slots[b + 2];
      BOOL is_eq_op = (op == MACH_EQ_TOL);
      if (JS_IsNumber(left) && JS_IsNumber(right) && JS_IsNumber(tol)) {
        double da, db, dt;
        JS_ToFloat64(ctx, &da, left);
        JS_ToFloat64(ctx, &db, right);
        JS_ToFloat64(ctx, &dt, tol);
        BOOL eq = fabs(da - db) <= dt;
        frame->slots[a] = JS_NewBool(ctx, is_eq_op ? eq : !eq);
      } else if (mist_is_text(left) && mist_is_text(right) && JS_VALUE_GET_TAG(tol) == JS_TAG_BOOL && JS_VALUE_GET_BOOL(tol)) {
        BOOL eq = js_string_compare_value_nocase(ctx, left, right) == 0;
        frame->slots[a] = JS_NewBool(ctx, is_eq_op ? eq : !eq);
      } else {
        /* Fall through to standard eq/neq */
        JSValue res = reg_vm_binop(ctx, is_eq_op ? MACH_EQ : MACH_NEQ, left, right);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (JS_IsException(res)) { goto disrupt; }
        frame->slots[a] = res;
      }
      VM_BREAK();
    }

    VM_CASE(MACH_NEG): {
      JSValue v = frame->slots[b];
      if (JS_IsInt(v)) {
        int32_t i = JS_VALUE_GET_INT(v);
        if (i == INT32_MIN)
          frame->slots[a] = JS_NewFloat64(ctx, -(double)i);
        else
          frame->slots[a] = JS_NewInt32(ctx, -i);
      } else {
        double d;
        JS_ToFloat64(ctx, &d, v);
        frame->slots[a] = JS_NewFloat64(ctx, -d);
      }
      VM_BREAK();
    }

    VM_CASE(MACH_LNOT): {
      int bval = JS_ToBool(ctx, frame->slots[b]);
      frame->slots[a] = JS_NewBool(ctx, !bval);
      VM_BREAK();
    }

    VM_CASE(MACH_BNOT): {
      int32_t i;
      JS_ToInt32(ctx, &i, frame->slots[b]);
      frame->slots[a] = JS_NewInt32(ctx, ~i);
      VM_BREAK();
    }

    VM_CASE(MACH_GETFIELD): {
      JSValue obj = frame->slots[b];
      JSValue key = code->cpool[c];
      /* Non-proxy functions (arity != 2) can't have properties read */
      if (mist_is_function(obj)) {
        JSFunction *fn_chk = JS_VALUE_GET_FUNCTION(obj);
        if (fn_chk->length != 2) {
          JS_ThrowTypeError(ctx, "cannot read property of non-proxy function");
          frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
          goto disrupt;
        }
      }
      JSValue val = JS_GetProperty(ctx, obj, key);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(val)) goto disrupt;
      frame->slots[a] = val;
      VM_BREAK();
    }

    VM_CASE(MACH_SETFIELD): {
      /* R(A)[K(B)] = R(C) */
      JSValue obj = frame->slots[a];
      JSValue key = code->cpool[b];
      JSValue val = frame->slots[c];
      int ret = JS_SetProperty(ctx, obj, key, val);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (ret < 0) goto disrupt;
      mach_resolve_forward(&frame->slots[a]);
      VM_BREAK();
    }

    VM_CASE(MACH_GETINDEX): {
      /* R(A) = R(B)[R(C)] — mcode guarantees R(C) is int */
      JSValue obj = frame->slots[b];
      JSValue val = JS_GetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(frame->slots[c]));
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(val)) goto disrupt;
      frame->slots[a] = val;
      VM_BREAK();
    }

    VM_CASE(MACH_SETINDEX): {
      /* R(A)[R(B)] = R(C) — mcode guarantees R(B) is int */
      JSValue obj = frame->slots[a];
      JSValue val = frame->slots[c];
      JSValue r = JS_SetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(frame->slots[b]), val);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(r)) goto disrupt;
      mach_resolve_forward(&frame->slots[a]);
      VM_BREAK();
    }

    VM_CASE(MACH_GETINTRINSIC): {
      int bx = MACH_GET_Bx(instr);
      JSValue key = code->cpool[bx];
      JSValue val = JS_GetProperty(ctx, ctx->global_obj, key);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsNull(val)) {
        int has = JS_HasProperty(ctx, ctx->global_obj, key);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        if (has <= 0) {
          char buf[128];
          JS_KeyGetStr(ctx, buf, sizeof(buf), key);
          JS_ThrowReferenceError(ctx, "'%s' is not defined", buf);
          goto disrupt;
        }
      }
      frame->slots[a] = val;
      VM_BREAK();
    }

    VM_CASE(MACH_GETENV): {
      /* Read env fresh from frame->function — C local env can go stale after GC */
      int bx = MACH_GET_Bx(instr);
      JSValue key = code->cpool[bx];
      JSValue cur_env = JS_VALUE_GET_FUNCTION(frame->function)->u.reg.env_record;
      JSValue val = JS_GetProperty(ctx, cur_env, key);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      frame->slots[a] = val;
      VM_BREAK();
    }

    VM_CASE(MACH_GETNAME): {
      /* Runtime fallback: try env then global (should not appear in linked code) */
      int bx = MACH_GET_Bx(instr);
      JSValue key = code->cpool[bx];
      JSValue val = JS_NULL;
      JSValue cur_env = JS_VALUE_GET_FUNCTION(frame->function)->u.reg.env_record;
      if (!JS_IsNull(cur_env)) {
        val = JS_GetProperty(ctx, cur_env, key);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      }
      if (JS_IsNull(val) || JS_IsException(val)) {
        val = JS_GetProperty(ctx, ctx->global_obj, key);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      }
      frame->slots[a] = val;
      VM_BREAK();
    }

    VM_CASE(MACH_GETUP): {
      /* R(A) = outer_frame[B].slots[C] — walk lexical scope chain */
      int depth = b;
      JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
      JSFrameRegister *target = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.reg.outer_frame);
      if (!target) {
        fprintf(stderr, "GETUP: NULL outer_frame at depth 0! pc=%d a=%d depth=%d slot=%d nr_slots=%d instr=0x%08x\n",
          pc-1, a, depth, c, code->nr_slots, instr);
        result = JS_ThrowInternalError(ctx, "GETUP: NULL outer_frame");
        goto disrupt;
      }
      for (int d = 1; d < depth; d++) {
        fn = JS_VALUE_GET_FUNCTION(target->function);
        JSFrameRegister *next = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.reg.outer_frame);
        if (!next) {
          fprintf(stderr, "GETUP: NULL outer_frame at depth %d! pc=%d a=%d depth=%d slot=%d nr_slots=%d instr=0x%08x\n",
            d, pc-1, a, depth, c, code->nr_slots, instr);
          result = JS_ThrowInternalError(ctx, "GETUP: NULL outer_frame at depth %d", d);
          goto disrupt;
        }
        target = next;
      }
      frame->slots[a] = target->slots[c];
      VM_BREAK();
    }

    VM_CASE(MACH_SETUP): {
      /* outer_frame[B].slots[C] = R(A) — walk lexical scope chain */
      int depth = b;
      JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
      JSFrameRegister *target = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.reg.outer_frame);
      for (int d = 1; d < depth; d++) {
        fn = JS_VALUE_GET_FUNCTION(target->function);
        target = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.reg.outer_frame);
      }
      target->slots[c] = frame->slots[a];
      VM_BREAK();
    }

    VM_CASE(MACH_JMP): {
      int offset = MACH_GET_sJ(instr);
      pc = (uint32_t)((int32_t)pc + offset);
      if (offset < 0) {
        int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
        if (pf == 2) {
          result = JS_ThrowInternalError(ctx, "interrupted");
          goto done;
        }
        if (pf == 1) {
          if (ctx->vm_call_depth > 0)
            atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);
          else
            goto suspend;
        }
      }
      VM_BREAK();
    }

    VM_CASE(MACH_JMPTRUE): {
      JSValue v = frame->slots[a];
      int cond;
      if (v == JS_TRUE) cond = 1;
      else if (v == JS_FALSE || v == JS_NULL) cond = 0;
      else cond = JS_ToBool(ctx, v);
      if (cond) {
        int offset = MACH_GET_sBx(instr);
        pc = (uint32_t)((int32_t)pc + offset);
        if (offset < 0) {
          int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
          if (pf == 2) {
            result = JS_ThrowInternalError(ctx, "interrupted");
            goto done;
          }
          if (pf == 1) {
            if (ctx->vm_call_depth > 0)
              atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);
            else
              goto suspend;
          }
        }
      }
      VM_BREAK();
    }

    VM_CASE(MACH_JMPFALSE): {
      JSValue v = frame->slots[a];
      int cond;
      if (v == JS_TRUE) cond = 1;
      else if (v == JS_FALSE || v == JS_NULL) cond = 0;
      else cond = JS_ToBool(ctx, v);
      if (!cond) {
        int offset = MACH_GET_sBx(instr);
        pc = (uint32_t)((int32_t)pc + offset);
        if (offset < 0) {
          int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
          if (pf == 2) {
            result = JS_ThrowInternalError(ctx, "interrupted");
            goto done;
          }
          if (pf == 1) {
            if (ctx->vm_call_depth > 0)
              atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);
            else
              goto suspend;
          }
        }
      }
      VM_BREAK();
    }

    VM_CASE(MACH_JMPNULL): {
      if (JS_IsNull(frame->slots[a])) {
        int offset = MACH_GET_sBx(instr);
        pc = (uint32_t)((int32_t)pc + offset);
      }
      VM_BREAK();
    }

    VM_CASE(MACH_RETURN):
      result = frame->slots[a];
      if (JS_IsNull(frame->caller)) goto done;
      {
#ifdef VALIDATE_GC
        const char *callee_name = "?";
        const char *callee_file = "?";
        {
          JSFunction *callee_fn = JS_VALUE_GET_FUNCTION(frame->function);
          if (callee_fn->kind == JS_FUNC_KIND_REGISTER && callee_fn->u.reg.code) {
            if (callee_fn->u.reg.code->name_cstr) callee_name = callee_fn->u.reg.code->name_cstr;
            if (callee_fn->u.reg.code->filename_cstr) callee_file = callee_fn->u.reg.code->filename_cstr;
          }
        }
#endif
        JSFrameRegister *caller = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller);
        frame->caller = JS_NULL;
        frame = caller;
        frame_ref.val = JS_MKPTR(frame);
        int ret_info = JS_VALUE_GET_INT(frame->address);
        JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
        code = fn->u.reg.code;
        env = fn->u.reg.env_record;
        pc = ret_info >> 16;
        int ret_slot = ret_info & 0xFFFF;
        if (ret_slot != 0xFFFF) {
#ifdef VALIDATE_GC
          if (JS_IsPtr(result)) {
            void *rp = JS_VALUE_GET_PTR(result);
            if ((uint8_t *)rp < ctx->heap_base || (uint8_t *)rp >= ctx->heap_free) {
              if (!is_ct_ptr(ctx, rp))
                fprintf(stderr, "VALIDATE_GC: stale RETURN into slot %d, ptr=%p heap=[%p,%p) fn_slots=%d pc=%u callee=%s (%s) caller=%s (%s)\n",
                        ret_slot, rp, (void*)ctx->heap_base, (void*)ctx->heap_free, code->nr_slots, pc,
                        callee_name, callee_file,
                        code->name_cstr ? code->name_cstr : "?",
                        code->filename_cstr ? code->filename_cstr : "?");
            }
          }
#endif
          frame->slots[ret_slot] = result;
        }
      }
      VM_BREAK();

    VM_CASE(MACH_RETNIL):
      result = JS_NULL;
      if (JS_IsNull(frame->caller)) goto done;
      {
        JSFrameRegister *caller = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller);
        frame->caller = JS_NULL;
        frame = caller;
        frame_ref.val = JS_MKPTR(frame);
        int ret_info = JS_VALUE_GET_INT(frame->address);
        JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
        code = fn->u.reg.code;
        env = fn->u.reg.env_record;
        pc = ret_info >> 16;
        int ret_slot = ret_info & 0xFFFF;
        if (ret_slot != 0xFFFF) frame->slots[ret_slot] = result;
      }
      VM_BREAK();

    VM_CASE(MACH_NEWOBJECT): {
      JSValue obj = JS_NewObject(ctx);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(obj)) { goto disrupt; }
      frame->slots[a] = obj;
      VM_BREAK();
    }

    VM_CASE(MACH_NEWARRAY): {
      JSValue arr = JS_NewArrayCap(ctx, b);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(arr)) { goto disrupt; }
      frame->slots[a] = arr;
      VM_BREAK();
    }

    VM_CASE(MACH_CLOSURE): {
      int bx = MACH_GET_Bx(instr);
      if ((uint32_t)bx < code->func_count) {
        JSCodeRegister *fn_code = code->functions[bx];
        /* Read env fresh from frame->function — C local can be stale */
        JSValue cur_env = JS_VALUE_GET_FUNCTION(frame->function)->u.reg.env_record;
        JSValue fn_val = js_new_register_function(ctx, fn_code, cur_env, frame_ref.val);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        frame->slots[a] = fn_val;
      } else {
        frame->slots[a] = JS_NULL;
      }
      VM_BREAK();
    }

    VM_CASE(MACH_PUSH): {
      /* push R(B) onto array R(A) — mcode guarantees R(A) is array */
      JSValue arr = frame->slots[a];
      JSValue val = frame->slots[b];
      JSGCRef arr_gc;
      JS_PushGCRef(ctx, &arr_gc);
      arr_gc.val = arr;
      int rc = JS_ArrayPush(ctx, &arr_gc.val, val);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      JS_PopGCRef(ctx, &arr_gc);
      if (rc < 0) goto disrupt;
      if (arr_gc.val != arr) frame->slots[a] = arr_gc.val;
      VM_BREAK();
    }

    VM_CASE(MACH_POP): {
      /* R(A) = pop last element from array R(B) — mcode guarantees R(B) is array */
      JSValue arr = frame->slots[b];
      JSValue val = JS_ArrayPop(ctx, arr);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(val)) goto disrupt;
      frame->slots[a] = val;
      VM_BREAK();
    }

    VM_CASE(MACH_DELETE): {
      JSValue obj = frame->slots[b];
      JSValue key = code->cpool[c];
      int ret = JS_DeleteProperty(ctx, obj, key);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (ret < 0) goto disrupt;
      frame->slots[a] = JS_NewBool(ctx, ret >= 0);
      VM_BREAK();
    }

    VM_CASE(MACH_DELETEINDEX): {
      JSValue obj = frame->slots[b];
      JSValue key = frame->slots[c];
      int ret = JS_DeleteProperty(ctx, obj, key);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (ret < 0) goto disrupt;
      frame->slots[a] = JS_NewBool(ctx, ret >= 0);
      VM_BREAK();
    }

    VM_CASE(MACH_HASPROP): {
      JSValue obj = frame->slots[b];
      JSValue key = frame->slots[c];
      int has = JS_HasProperty(ctx, obj, key);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      frame->slots[a] = JS_NewBool(ctx, has > 0);
      VM_BREAK();
    }

    VM_CASE(MACH_REGEXP): {
      JSValue argv[2];
      argv[0] = code->cpool[b]; /* pattern */
      argv[1] = code->cpool[c]; /* flags */
      JSValue re = js_regexp_constructor(ctx, JS_NULL, 2, argv);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(re)) goto disrupt;
      frame->slots[a] = re;
      VM_BREAK();
    }

    VM_CASE(MACH_THROW):
      goto disrupt;

    /* === New mcode-derived opcodes === */

    /* Text concatenation */
    VM_CASE(MACH_CONCAT): {
      JSValue res = JS_ConcatString(ctx, frame->slots[b], frame->slots[c]);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(res)) goto disrupt;
      frame->slots[a] = res;
      VM_BREAK();
    }

    /* Typed integer comparisons */
    VM_CASE(MACH_EQ_INT):
      frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) == JS_VALUE_GET_INT(frame->slots[c]));
      VM_BREAK();
    VM_CASE(MACH_NE_INT):
      frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) != JS_VALUE_GET_INT(frame->slots[c]));
      VM_BREAK();
    VM_CASE(MACH_LT_INT):
      frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) < JS_VALUE_GET_INT(frame->slots[c]));
      VM_BREAK();
    VM_CASE(MACH_LE_INT):
      frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) <= JS_VALUE_GET_INT(frame->slots[c]));
      VM_BREAK();
    VM_CASE(MACH_GT_INT):
      frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) > JS_VALUE_GET_INT(frame->slots[c]));
      VM_BREAK();
    VM_CASE(MACH_GE_INT):
      frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) >= JS_VALUE_GET_INT(frame->slots[c]));
      VM_BREAK();

    /* Typed float comparisons */
    VM_CASE(MACH_EQ_FLOAT): VM_CASE(MACH_NE_FLOAT):
    VM_CASE(MACH_LT_FLOAT): VM_CASE(MACH_LE_FLOAT):
    VM_CASE(MACH_GT_FLOAT): VM_CASE(MACH_GE_FLOAT): {
      double da, db;
      JS_ToFloat64(ctx, &da, frame->slots[b]);
      JS_ToFloat64(ctx, &db, frame->slots[c]);
      int r;
      switch (op) {
      case MACH_EQ_FLOAT: r = (da == db); break;
      case MACH_NE_FLOAT: r = (da != db); break;
      case MACH_LT_FLOAT: r = (da <  db); break;
      case MACH_LE_FLOAT: r = (da <= db); break;
      case MACH_GT_FLOAT: r = (da >  db); break;
      case MACH_GE_FLOAT: r = (da >= db); break;
      default: r = 0; break;
      }
      frame->slots[a] = JS_NewBool(ctx, r);
      VM_BREAK();
    }

    /* Typed text comparisons */
    VM_CASE(MACH_EQ_TEXT): VM_CASE(MACH_NE_TEXT):
    VM_CASE(MACH_LT_TEXT): VM_CASE(MACH_LE_TEXT):
    VM_CASE(MACH_GT_TEXT): VM_CASE(MACH_GE_TEXT): {
      int cmp = js_string_compare_value(ctx, frame->slots[b], frame->slots[c], FALSE);
      int r;
      switch (op) {
      case MACH_EQ_TEXT: r = (cmp == 0); break;
      case MACH_NE_TEXT: r = (cmp != 0); break;
      case MACH_LT_TEXT: r = (cmp <  0); break;
      case MACH_LE_TEXT: r = (cmp <= 0); break;
      case MACH_GT_TEXT: r = (cmp >  0); break;
      case MACH_GE_TEXT: r = (cmp >= 0); break;
      default: r = 0; break;
      }
      frame->slots[a] = JS_NewBool(ctx, r);
      VM_BREAK();
    }

    /* Typed bool comparisons */
    VM_CASE(MACH_EQ_BOOL):
      frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_BOOL(frame->slots[b]) == JS_VALUE_GET_BOOL(frame->slots[c]));
      VM_BREAK();
    VM_CASE(MACH_NE_BOOL):
      frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_BOOL(frame->slots[b]) != JS_VALUE_GET_BOOL(frame->slots[c]));
      VM_BREAK();

    /* Identity check */
    VM_CASE(MACH_IS_IDENTICAL): {
      JSValue va = JS_IsPtr(frame->slots[b]) ? JS_MKPTR(chase(frame->slots[b])) : frame->slots[b];
      JSValue vb = JS_IsPtr(frame->slots[c]) ? JS_MKPTR(chase(frame->slots[c])) : frame->slots[c];
      frame->slots[a] = JS_NewBool(ctx, va == vb);
      VM_BREAK();
    }

    /* Type checks */
    VM_CASE(MACH_IS_INT):
      frame->slots[a] = JS_NewBool(ctx, JS_IsInt(frame->slots[b]));
      VM_BREAK();
    VM_CASE(MACH_IS_NUM):
      frame->slots[a] = JS_NewBool(ctx, JS_IsNumber(frame->slots[b]));
      VM_BREAK();
    VM_CASE(MACH_IS_TEXT):
      frame->slots[a] = JS_NewBool(ctx, mist_is_text(frame->slots[b]));
      VM_BREAK();
    VM_CASE(MACH_IS_BOOL):
      frame->slots[a] = JS_NewBool(ctx, JS_IsBool(frame->slots[b]));
      VM_BREAK();
    VM_CASE(MACH_IS_NULL):
      frame->slots[a] = JS_NewBool(ctx, JS_IsNull(frame->slots[b]));
      VM_BREAK();
    VM_CASE(MACH_IS_ARRAY):
      frame->slots[a] = JS_NewBool(ctx, mist_is_array(frame->slots[b]));
      VM_BREAK();
    VM_CASE(MACH_IS_FUNC):
      frame->slots[a] = JS_NewBool(ctx, mist_is_function(frame->slots[b]));
      VM_BREAK();
    VM_CASE(MACH_IS_RECORD):
      frame->slots[a] = JS_NewBool(ctx, mist_is_record(frame->slots[b]));
      VM_BREAK();
    VM_CASE(MACH_IS_STONE):
      frame->slots[a] = JS_NewBool(ctx, mist_is_stone(frame->slots[b]));
      VM_BREAK();
    VM_CASE(MACH_LENGTH): {
      JSValue v = frame->slots[b];
      if (mist_is_array(v)) {
        JSArray *arr = JS_VALUE_GET_ARRAY(v);
        frame->slots[a] = JS_NewInt32(ctx, (int32_t)arr->len);
      } else if (MIST_IsImmediateASCII(v)) {
        frame->slots[a] = JS_NewInt32(ctx, MIST_GetImmediateASCIILen(v));
      } else {
        /* fallback to C for text/blob/function (still a GC safepoint) */
        JSValue res = JS_CellLength(ctx, v);
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        frame->slots[a] = res;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_IS_PROXY): {
      JSValue v = frame->slots[b];
      int is_proxy = 0;
      if (mist_is_function(v)) {
        JSFunction *fn = JS_VALUE_GET_FUNCTION(v);
        is_proxy = (fn->length == 2);
      }
      frame->slots[a] = JS_NewBool(ctx, is_proxy);
      VM_BREAK();
    }
    /* Logical */
    VM_CASE(MACH_NOT): {
      int bval = JS_ToBool(ctx, frame->slots[b]);
      frame->slots[a] = JS_NewBool(ctx, !bval);
      VM_BREAK();
    }
    VM_CASE(MACH_AND): {
      JSValue left = frame->slots[b];
      if (!JS_ToBool(ctx, left))
        frame->slots[a] = left;
      else
        frame->slots[a] = frame->slots[c];
      VM_BREAK();
    }
    VM_CASE(MACH_OR): {
      JSValue left = frame->slots[b];
      if (JS_ToBool(ctx, left))
        frame->slots[a] = left;
      else
        frame->slots[a] = frame->slots[c];
      VM_BREAK();
    }

    /* Bitwise (mcode names — delegate to same code as legacy) */
    VM_CASE(MACH_BITNOT): {
      int32_t i;
      JS_ToInt32(ctx, &i, frame->slots[b]);
      frame->slots[a] = JS_NewInt32(ctx, ~i);
      VM_BREAK();
    }
    VM_CASE(MACH_BITAND): VM_CASE(MACH_BITOR): VM_CASE(MACH_BITXOR): {
      int32_t ia, ib;
      JS_ToInt32(ctx, &ia, frame->slots[b]);
      JS_ToInt32(ctx, &ib, frame->slots[c]);
      int32_t r;
      if (op == MACH_BITAND) r = ia & ib;
      else if (op == MACH_BITOR) r = ia | ib;
      else r = ia ^ ib;
      frame->slots[a] = JS_NewInt32(ctx, r);
      VM_BREAK();
    }

    /* Property access (mcode names) */
    VM_CASE(MACH_LOAD_FIELD): {
      JSValue obj = frame->slots[b];
      JSValue key = code->cpool[c];
      if (mist_is_function(obj)) {
        JS_ThrowTypeError(ctx, "cannot read property of function");
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        goto disrupt;
      }
      JSValue val = JS_GetProperty(ctx, obj, key);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(val)) goto disrupt;
      frame->slots[a] = val;
      VM_BREAK();
    }
    VM_CASE(MACH_STORE_FIELD): {
      JSValue obj = frame->slots[a];
      JSValue key = code->cpool[b];
      JSValue val = frame->slots[c];
      int ret = JS_SetProperty(ctx, obj, key, val);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (ret < 0) goto disrupt;
      mach_resolve_forward(&frame->slots[a]);
      VM_BREAK();
    }
    VM_CASE(MACH_LOAD_INDEX): {
      /* R(A) = R(B)[R(C)] — mcode guarantees R(C) is int */
      JSValue obj = frame->slots[b];
      JSValue val = JS_GetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(frame->slots[c]));
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(val)) goto disrupt;
      frame->slots[a] = val;
      VM_BREAK();
    }
    VM_CASE(MACH_STORE_INDEX): {
      /* R(A)[R(B)] = R(C) — mcode guarantees R(B) is int */
      JSValue obj = frame->slots[a];
      JSValue val = frame->slots[c];
      JSValue r = JS_SetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(frame->slots[b]), val);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(r)) goto disrupt;
      mach_resolve_forward(&frame->slots[a]);
      VM_BREAK();
    }
    VM_CASE(MACH_LOAD_DYNAMIC): {
      JSValue obj = frame->slots[b];
      JSValue key = frame->slots[c];
      JSValue val;
      if (JS_IsInt(key))
        val = JS_GetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(key));
      else
        val = JS_GetProperty(ctx, obj, key);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(val)) goto disrupt;
      frame->slots[a] = val;
      VM_BREAK();
    }
    VM_CASE(MACH_STORE_DYNAMIC): {
      JSValue obj = frame->slots[a];
      JSValue key = frame->slots[b];
      JSValue val = frame->slots[c];
      int ret;
      if (JS_IsInt(key)) {
        JSValue r = JS_SetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(key), val);
        ret = JS_IsException(r) ? -1 : 0;
      } else if (mist_is_array(obj)) {
        JS_ThrowTypeError(ctx, "array index must be a number");
        ret = -1;
      } else if (JS_IsBool(key) || JS_IsNull(key) || mist_is_array(key) || mist_is_function(key)) {
        JS_ThrowTypeError(ctx, "object key must be text");
        ret = -1;
      } else {
        ret = JS_SetProperty(ctx, obj, key, val);
      }
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (ret < 0) goto disrupt;
      mach_resolve_forward(&frame->slots[a]);
      VM_BREAK();
    }

    /* New record */
    VM_CASE(MACH_NEWRECORD): {
      JSValue obj = b > 0 ? JS_NewObjectCap(ctx, b) : JS_NewObject(ctx);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      if (JS_IsException(obj)) goto disrupt;
      frame->slots[a] = obj;
      VM_BREAK();
    }

    /* Decomposed function calls (inlined from qbe_helpers) */
    VM_CASE(MACH_FRAME):
    VM_CASE(MACH_GOFRAME): {
      /* A=frame_slot, B=func_reg, C=argc */
      JSValue func_val = frame->slots[b];
      if (!mist_is_function(func_val)) {
        JS_ThrowTypeError(ctx, "not a function");
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        goto disrupt;
      }
      int nr = c + 2; /* argc + this + func overhead */
      JSFrameRegister *call_frame = alloc_frame_register(ctx, nr);
      if (!call_frame) {
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        goto disrupt;
      }
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      func_val = frame->slots[b]; /* re-read after GC */
      call_frame->function = func_val;
      frame->slots[a] = JS_MKPTR(call_frame);
      VM_BREAK();
    }
    VM_CASE(MACH_SETARG): {
      /* A=frame_slot, B=arg_idx, C=val_reg */
      JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]);
      fr->slots[b] = frame->slots[c];
      VM_BREAK();
    }
    VM_CASE(MACH_INVOKE): {
      /* A=frame_slot, B=result_slot */
      JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]);
      int nr = (int)objhdr_cap56(fr->header);
      int c_argc = (nr >= 2) ? nr - 2 : 0;
      JSValue fn_val = fr->function;
      JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val);

      if (fn->kind == JS_FUNC_KIND_REGISTER) {
        /* Register function: switch frames inline (fast path) */
        JSCodeRegister *fn_code = fn->u.reg.code;
        JSFrameRegister *new_frame = alloc_frame_register(ctx, fn_code->nr_slots);
        if (!new_frame) {
          frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
          goto disrupt;
        }
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]);
        fn_val = fr->function;
        fn = JS_VALUE_GET_FUNCTION(fn_val);
        fn_code = fn->u.reg.code;
        new_frame->function = fn_val;
        /* Copy this + args from call frame to new frame */
        int copy_count = (c_argc < fn_code->arity) ? c_argc : fn_code->arity;
        new_frame->slots[0] = fr->slots[0]; /* this */
        for (int i = 0; i < copy_count; i++)
          new_frame->slots[1 + i] = fr->slots[1 + i];
        /* Save return info */
        frame->address = JS_NewInt32(ctx, (pc << 16) | b);
        new_frame->caller = JS_MKPTR(frame);
        frame = new_frame;
        frame_ref.val = JS_MKPTR(frame);
        code = fn_code;
        env = fn->u.reg.env_record;
        pc = code->entry_point;
      } else {
        /* C or bytecode function: args already in fr->slots (GC-protected via frame chain) */
        ctx->reg_current_frame = frame_ref.val;
        ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
        ctx->vm_call_depth++;
        JSValue ret;
        if (fn->kind == JS_FUNC_KIND_C)
          ret = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]);
        else
          ret = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1], 0);
        ctx->vm_call_depth--;
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        ctx->reg_current_frame = JS_NULL;
        if (JS_IsException(ret)) goto disrupt;
#ifdef VALIDATE_GC
        if (JS_IsPtr(ret)) {
          void *rp = JS_VALUE_GET_PTR(ret);
          if ((uint8_t *)rp < ctx->heap_base || (uint8_t *)rp >= ctx->heap_free) {
            if (!is_ct_ptr(ctx, rp)) {
              int magic = (fn->kind == JS_FUNC_KIND_C) ? fn->u.cfunc.magic : -1;
              void *cfp = (fn->kind == JS_FUNC_KIND_C) ? (void *)fn->u.cfunc.c_function.generic : NULL;
              fprintf(stderr, "VALIDATE_GC: stale INVOKE result into slot %d, ptr=%p heap=[%p,%p) fn_slots=%d pc=%u kind=%d magic=%d cfunc=%p caller=%s (%s)\n",
                      b, rp, (void*)ctx->heap_base, (void*)ctx->heap_free, code->nr_slots, pc - 1, fn->kind,
                      magic, cfp,
                      code->name_cstr ? code->name_cstr : "?",
                      code->filename_cstr ? code->filename_cstr : "?");
            }
          }
        }
#endif
        frame->slots[b] = ret;
      }
      VM_BREAK();
    }
    VM_CASE(MACH_GOINVOKE): {
      /* Tail call: replace current frame with callee */
      JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]);
      int nr = (int)objhdr_cap56(fr->header);
      int c_argc = (nr >= 2) ? nr - 2 : 0;
      JSValue fn_val = fr->function;
      JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val);

      if (fn->kind == JS_FUNC_KIND_REGISTER) {
        JSCodeRegister *fn_code = fn->u.reg.code;
        int current_slots = (int)objhdr_cap56(frame->header);

        if (fn_code->nr_slots <= current_slots) {
          /* FAST PATH: reuse current frame — no allocation */
          int copy_count = (c_argc < fn_code->arity) ? c_argc : fn_code->arity;
          frame->slots[0] = fr->slots[0]; /* this */
          for (int i = 0; i < copy_count; i++)
            frame->slots[1 + i] = fr->slots[1 + i];
          /* Null out remaining slots (locals/temps) */
          for (int i = 1 + copy_count; i < current_slots; i++)
            frame->slots[i] = JS_NULL;
          frame->function = fn_val;
          /* caller stays the same — we're reusing this frame */
          code = fn_code;
          env = fn->u.reg.env_record;
          pc = code->entry_point;
        } else {
          /* SLOW PATH: callee needs more slots, must allocate */
          JSFrameRegister *new_frame = alloc_frame_register(ctx, fn_code->nr_slots);
          if (!new_frame) {
            frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
            goto disrupt;
          }
          frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
          fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]);
          fn_val = fr->function;
          fn = JS_VALUE_GET_FUNCTION(fn_val);
          fn_code = fn->u.reg.code;
          new_frame->function = fn_val;
          int copy_count = (c_argc < fn_code->arity) ? c_argc : fn_code->arity;
          new_frame->slots[0] = fr->slots[0]; /* this */
          for (int i = 0; i < copy_count; i++)
            new_frame->slots[1 + i] = fr->slots[1 + i];
          new_frame->caller = frame->caller;
          frame->caller = JS_NULL;
          frame = new_frame;
          frame_ref.val = JS_MKPTR(frame);
          code = fn_code;
          env = fn->u.reg.env_record;
          pc = code->entry_point;
        }
      } else {
        /* C/bytecode function: call it, then return result to our caller */
        ctx->reg_current_frame = frame_ref.val;
        ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
        ctx->vm_call_depth++;
        JSValue ret;
        if (fn->kind == JS_FUNC_KIND_C)
          ret = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]);
        else
          ret = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1], 0);
        ctx->vm_call_depth--;
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
        ctx->reg_current_frame = JS_NULL;
        if (JS_IsException(ret)) goto disrupt;
        /* Tail-return: act like MACH_RETURN with the result */
        result = ret;
        if (JS_IsNull(frame->caller)) goto done;
        JSFrameRegister *caller = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller);
        frame->caller = JS_NULL;
        frame = caller;
        frame_ref.val = JS_MKPTR(frame);
        int ret_info = JS_VALUE_GET_INT(frame->address);
        JSFunction *ret_fn = JS_VALUE_GET_FUNCTION(frame->function);
        code = ret_fn->u.reg.code;
        env = ret_fn->u.reg.env_record;
        pc = ret_info >> 16;
        int ret_slot = ret_info & 0xFFFF;
        if (ret_slot != 0xFFFF) frame->slots[ret_slot] = ret;
      }
      VM_BREAK();
    }

    /* Jump if not null */
    VM_CASE(MACH_JMPNOTNULL): {
      if (!JS_IsNull(frame->slots[a])) {
        int offset = MACH_GET_sBx(instr);
        pc = (uint32_t)((int32_t)pc + offset);
      }
      VM_BREAK();
    }

    /* Disrupt (mcode alias) */
    VM_CASE(MACH_DISRUPT):
      goto disrupt;

    /* Has-property check (mcode name) */
    VM_CASE(MACH_IN): {
      JSValue key = frame->slots[b];
      JSValue obj = frame->slots[c];
      int has = JS_HasProperty(ctx, obj, key);
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
      frame->slots[a] = JS_NewBool(ctx, has > 0);
      VM_BREAK();
    }

    VM_DEFAULT:
      result = JS_ThrowInternalError(ctx, "unknown register VM opcode %d: %s", op, mach_opcode_names[op]);
      goto done;
    }
    continue;

  disrupt:
    /* Save debug info for stack traces */
    ctx->reg_current_frame = frame_ref.val;
    ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
    /* Search frame chain for a disruption handler.
       Use frame_pc to track each frame's execution point:
       - For the faulting frame, it's the current pc.
       - For unwound caller frames, read from frame->address.
       Keep frame_ref.val at the faulting frame and preserve caller
       links so the trace walk can show the full call chain. */
    frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);  /* re-chase after GC */
    {
      uint32_t frame_pc = pc;
      for (;;) {
        JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
        code = fn->u.reg.code;
        /* Only enter handler if we're not already inside it */
        if (code->disruption_pc > 0 && frame_pc < code->disruption_pc) {
          env = fn->u.reg.env_record;
          pc = code->disruption_pc;
          ctx->disruption_reported = FALSE;
          frame_ref.val = JS_MKPTR(frame);        /* root handler frame for GC */
          ctx->current_exception = JS_NULL;
          break;
        }
        if (JS_IsNull(frame->caller)) {
          if (!ctx->disruption_reported) {
            /* Use faulting frame (frame_ref.val) for the header */
            JSFrameRegister *fault = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
            JSFunction *fault_fn = JS_VALUE_GET_FUNCTION(fault->function);
            JSCodeRegister *fault_code = fault_fn->u.reg.code;
            const char *fn_name = fault_code->name_cstr ? fault_code->name_cstr : "<anonymous>";
            const char *file = fault_code->filename_cstr ? fault_code->filename_cstr : "<unknown>";
            uint16_t line = 0, col = 0;
            uint32_t fpc = pc > 0 ? pc - 1 : 0;
            if (fault_code->line_table && fpc < fault_code->instr_count) {
              line = fault_code->line_table[fpc].line;
              col  = fault_code->line_table[fpc].col;
            }
            fprintf(stderr, "unhandled disruption in %s (%s:%u:%u)\n", fn_name, file, line, col);
            /* Walk intact caller chain for stack trace */
            {
              JSFrameRegister *trace_frame = fault;
              int first = 1;
              while (trace_frame) {
                if (!mist_is_function(trace_frame->function)) break;
                JSFunction *trace_fn = JS_VALUE_GET_FUNCTION(trace_frame->function);
                if (trace_fn->kind == JS_FUNC_KIND_REGISTER && trace_fn->u.reg.code) {
                  JSCodeRegister *tc = trace_fn->u.reg.code;
                  uint32_t tpc = first ? fpc
                                       : (uint32_t)(JS_VALUE_GET_INT(trace_frame->address) >> 16);
                  uint16_t tl = 0, tcol = 0;
                  if (tc->line_table && tpc < tc->instr_count) {
                    tl = tc->line_table[tpc].line;
                    tcol = tc->line_table[tpc].col;
                  }
                  fprintf(stderr, "    at %s (%s:%u:%u)\n",
                          tc->name_cstr ? tc->name_cstr : "<anonymous>",
                          tc->filename_cstr ? tc->filename_cstr : "<unknown>", tl, tcol);
                }
                if (JS_IsNull(trace_frame->caller)) break;
                trace_frame = (JSFrameRegister *)JS_VALUE_GET_PTR(trace_frame->caller);
                first = 0;
              }
            }
            ctx->disruption_reported = TRUE;
          }
          frame_ref.val = JS_MKPTR(frame);        /* update root for GC / done */
          result = JS_Throw(ctx, JS_NULL);
          frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
          goto done;
        }
        /* Advance to caller — keep chain intact (no nulling caller links) */
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller);
        frame_pc = (uint32_t)(JS_VALUE_GET_INT(frame->address) >> 16);
      }
    }
  }

suspend:
  ctx->suspended = 1;
  ctx->suspended_pc = pc;
  ctx->suspended_frame_ref.val = frame_ref.val;
  result = JS_SUSPENDED;
  JS_DeleteGCRef(ctx, &frame_ref);
#ifdef HAVE_ASAN
  __asan_js_ctx = NULL;
#endif
  return result;

done:
#ifdef HAVE_ASAN
  __asan_js_ctx = NULL;
#endif
  ctx->reg_current_frame = JS_NULL;
  if (JS_IsException(result)) {
    ctx->reg_current_frame = frame_ref.val;
    ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
  }
  JS_DeleteGCRef(ctx, &frame_ref);
  return result;
}

JSValue JS_ResumeRegisterVM(JSContext *ctx) {
  if (!ctx->suspended)
    return JS_ThrowInternalError(ctx, "no suspended VM to resume");
  /* ctx->suspended is set; JS_CallRegisterVM will take the resume path */
  return JS_CallRegisterVM(ctx, NULL, JS_NULL, 0, NULL, JS_NULL, JS_NULL);
}

/* ============================================================
   MCODE Lowering — mcode JSON IR → MachInstr32
   ============================================================ */

typedef struct {
  MachInstr32 *code;
  int code_count, code_cap;
  MachCPoolEntry *cpool;
  int cpool_count, cpool_cap;
  MachLineEntry *lines;
  int nr_slots;
  struct { const char *name; int pc; } *labels;
  int label_count, label_cap;
  struct { int pc; const char *label; int is_sJ; int reg_a; } *patches;
  int patch_count, patch_cap;
  int *flat_to_pc;
  int flat_count;
} McodeLowerState;

static void ml_emit(McodeLowerState *s, MachInstr32 instr, int line, int col) {
  if (s->code_count >= s->code_cap) {
    int nc = s->code_cap ? s->code_cap * 2 : 64;
    s->code = sys_realloc(s->code, nc * sizeof(MachInstr32));
    s->lines = sys_realloc(s->lines, nc * sizeof(MachLineEntry));
    s->code_cap = nc;
  }
  s->lines[s->code_count] = (MachLineEntry){(uint16_t)line, (uint16_t)col};
  s->code[s->code_count++] = instr;
}

static int ml_cpool_int(McodeLowerState *s, int32_t val) {
  for (int i = 0; i < s->cpool_count; i++)
    if (s->cpool[i].type == MACH_CP_INT && s->cpool[i].ival == val) return i;
  if (s->cpool_count >= s->cpool_cap) {
    int nc = s->cpool_cap ? s->cpool_cap * 2 : 16;
    s->cpool = sys_realloc(s->cpool, nc * sizeof(MachCPoolEntry));
    s->cpool_cap = nc;
  }
  s->cpool[s->cpool_count] = (MachCPoolEntry){.type = MACH_CP_INT, .ival = val};
  return s->cpool_count++;
}

static int ml_cpool_float(McodeLowerState *s, double val) {
  for (int i = 0; i < s->cpool_count; i++)
    if (s->cpool[i].type == MACH_CP_FLOAT && s->cpool[i].fval == val) return i;
  if (s->cpool_count >= s->cpool_cap) {
    int nc = s->cpool_cap ? s->cpool_cap * 2 : 16;
    s->cpool = sys_realloc(s->cpool, nc * sizeof(MachCPoolEntry));
    s->cpool_cap = nc;
  }
  s->cpool[s->cpool_count] = (MachCPoolEntry){.type = MACH_CP_FLOAT, .fval = val};
  return s->cpool_count++;
}

static int ml_cpool_str(McodeLowerState *s, const char *str) {
  if (!str) { fprintf(stderr, "ml_cpool_str: NULL string\n"); str = ""; }
  for (int i = 0; i < s->cpool_count; i++)
    if (s->cpool[i].type == MACH_CP_STR && strcmp(s->cpool[i].str, str) == 0) return i;
  if (s->cpool_count >= s->cpool_cap) {
    int nc = s->cpool_cap ? s->cpool_cap * 2 : 16;
    s->cpool = sys_realloc(s->cpool, nc * sizeof(MachCPoolEntry));
    s->cpool_cap = nc;
  }
  char *dup = sys_malloc(strlen(str) + 1);
  memcpy(dup, str, strlen(str) + 1);
  s->cpool[s->cpool_count] = (MachCPoolEntry){.type = MACH_CP_STR, .str = dup};
  return s->cpool_count++;
}

static void ml_label(McodeLowerState *s, const char *name) {
  if (s->label_count >= s->label_cap) {
    int nc = s->label_cap ? s->label_cap * 2 : 32;
    s->labels = sys_realloc(s->labels, nc * sizeof(s->labels[0]));
    s->label_cap = nc;
  }
  s->labels[s->label_count].name = name;
  s->labels[s->label_count].pc = s->code_count;
  s->label_count++;
}

static void ml_patch(McodeLowerState *s, int pc, const char *label, int is_sJ, int reg_a) {
  if (s->patch_count >= s->patch_cap) {
    int nc = s->patch_cap ? s->patch_cap * 2 : 32;
    s->patches = sys_realloc(s->patches, nc * sizeof(s->patches[0]));
    s->patch_cap = nc;
  }
  s->patches[s->patch_count].pc = pc;
  s->patches[s->patch_count].label = label;
  s->patches[s->patch_count].is_sJ = is_sJ;
  s->patches[s->patch_count].reg_a = reg_a;
  s->patch_count++;
}

static int ml_int(cJSON *arr, int idx) {
  return (int)cJSON_GetArrayItem(arr, idx)->valuedouble;
}

/* Lower one function's mcode instructions to MachInstr32 */
static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
  McodeLowerState s = {0};
  int nr_args = (int)cJSON_GetNumberValue(
      cJSON_GetObjectItemCaseSensitive(fobj, "nr_args"));
  int nr_close = (int)cJSON_GetNumberValue(
      cJSON_GetObjectItemCaseSensitive(fobj, "nr_close_slots"));
  s.nr_slots = (int)cJSON_GetNumberValue(
      cJSON_GetObjectItemCaseSensitive(fobj, "nr_slots"));
  if (s.nr_slots > 255) {
    cJSON *nm_chk = cJSON_GetObjectItemCaseSensitive(fobj, "name");
    const char *fn_name = nm_chk ? cJSON_GetStringValue(nm_chk) : "<anonymous>";
    fprintf(stderr, "ERROR: function '%s' has %d slots (max 255). "
            "Ensure the streamline optimizer ran before mach compilation.\n",
            fn_name, s.nr_slots);
    return NULL;
  }
  int dis_raw = (int)cJSON_GetNumberValue(
      cJSON_GetObjectItemCaseSensitive(fobj, "disruption_pc"));
  cJSON *nm = cJSON_GetObjectItemCaseSensitive(fobj, "name");
  const char *name = nm ? cJSON_GetStringValue(nm) : NULL;
  cJSON *fn_j = cJSON_GetObjectItemCaseSensitive(fobj, "filename");
  const char *fname = fn_j ? cJSON_GetStringValue(fn_j) : filename;
  cJSON *instrs = cJSON_GetObjectItemCaseSensitive(fobj, "instructions");
  int n = instrs ? cJSON_GetArraySize(instrs) : 0;

  s.flat_to_pc = sys_malloc((n + 1) * sizeof(int));
  s.flat_count = n;

  { cJSON *it = instrs ? instrs->child : NULL;
  for (int i = 0; it; i++, it = it->next) {
    s.flat_to_pc[i] = s.code_count;
    if (cJSON_IsString(it)) {
      ml_label(&s, it->valuestring);
      continue;
    }
    int sz = cJSON_GetArraySize(it);
    const char *op = cJSON_GetArrayItem(it, 0)->valuestring;
    int line = ml_int(it, sz - 2);
    int col = ml_int(it, sz - 1);

#define A1 ml_int(it,1)
#define A2 ml_int(it,2)
#define A3 ml_int(it,3)
#define A4 ml_int(it,4)
#define EM(instr) ml_emit(&s, (instr), line, col)
#define ABC3(opc) EM(MACH_ABC(opc, A1, A2, A3))
#define AB2(opc)  EM(MACH_ABC(opc, A1, A2, 0))

    if (strcmp(op, "access") == 0) {
      int dest = A1;
      cJSON *val = cJSON_GetArrayItem(it, 2);
      if (cJSON_IsNumber(val)) {
        double dv = val->valuedouble;
        int32_t iv = (int32_t)dv;
        if ((double)iv == dv && iv >= -32768 && iv <= 32767)
          EM(MACH_AsBx(MACH_LOADI, dest, iv));
        else if ((double)iv == dv)
          EM(MACH_ABx(MACH_LOADK, dest, ml_cpool_int(&s, iv)));
        else
          EM(MACH_ABx(MACH_LOADK, dest, ml_cpool_float(&s, dv)));
      } else if (cJSON_IsString(val)) {
        EM(MACH_ABx(MACH_LOADK, dest, ml_cpool_str(&s, val->valuestring)));
      } else if (cJSON_IsObject(val)) {
        const char *vn = cJSON_GetStringValue(
            cJSON_GetObjectItemCaseSensitive(val, "name"));
        EM(MACH_ABx(MACH_GETNAME, dest, ml_cpool_str(&s, vn)));
      }
    }
    else if (strcmp(op, "int") == 0) {
      int dest = A1, v = A2;
      if (v >= -32768 && v <= 32767)
        EM(MACH_AsBx(MACH_LOADI, dest, v));
      else
        EM(MACH_ABx(MACH_LOADK, dest, ml_cpool_int(&s, v)));
    }
    else if (strcmp(op, "true") == 0)    { EM(MACH_ABC(MACH_LOADTRUE, A1, 0, 0)); }
    else if (strcmp(op, "false") == 0)   { EM(MACH_ABC(MACH_LOADFALSE, A1, 0, 0)); }
    else if (strcmp(op, "null") == 0)    { EM(MACH_ABC(MACH_LOADNULL, A1, 0, 0)); }
    else if (strcmp(op, "move") == 0)    { AB2(MACH_MOVE); }
    /* Text */
    else if (strcmp(op, "concat") == 0)  { ABC3(MACH_CONCAT); }
    /* Generic arithmetic */
    else if (strcmp(op, "add") == 0)     { ABC3(MACH_ADD); }
    else if (strcmp(op, "subtract") == 0) { ABC3(MACH_SUB); }
    else if (strcmp(op, "multiply") == 0) { ABC3(MACH_MUL); }
    else if (strcmp(op, "divide") == 0)  { ABC3(MACH_DIV); }
    else if (strcmp(op, "modulo") == 0)  { ABC3(MACH_MOD); }
    else if (strcmp(op, "pow") == 0)     { ABC3(MACH_POW); }
    else if (strcmp(op, "negate") == 0)  { AB2(MACH_NEG); }
    /* Typed integer comparisons */
    else if (strcmp(op, "eq_int") == 0)  { ABC3(MACH_EQ_INT); }
    else if (strcmp(op, "ne_int") == 0)  { ABC3(MACH_NE_INT); }
    else if (strcmp(op, "lt_int") == 0)  { ABC3(MACH_LT_INT); }
    else if (strcmp(op, "le_int") == 0)  { ABC3(MACH_LE_INT); }
    else if (strcmp(op, "gt_int") == 0)  { ABC3(MACH_GT_INT); }
    else if (strcmp(op, "ge_int") == 0)  { ABC3(MACH_GE_INT); }
    /* Typed float comparisons */
    else if (strcmp(op, "eq_float") == 0) { ABC3(MACH_EQ_FLOAT); }
    else if (strcmp(op, "ne_float") == 0) { ABC3(MACH_NE_FLOAT); }
    else if (strcmp(op, "lt_float") == 0) { ABC3(MACH_LT_FLOAT); }
    else if (strcmp(op, "le_float") == 0) { ABC3(MACH_LE_FLOAT); }
    else if (strcmp(op, "gt_float") == 0) { ABC3(MACH_GT_FLOAT); }
    else if (strcmp(op, "ge_float") == 0) { ABC3(MACH_GE_FLOAT); }
    /* Typed text comparisons */
    else if (strcmp(op, "eq_text") == 0) { ABC3(MACH_EQ_TEXT); }
    else if (strcmp(op, "ne_text") == 0) { ABC3(MACH_NE_TEXT); }
    else if (strcmp(op, "lt_text") == 0) { ABC3(MACH_LT_TEXT); }
    else if (strcmp(op, "le_text") == 0) { ABC3(MACH_LE_TEXT); }
    else if (strcmp(op, "gt_text") == 0) { ABC3(MACH_GT_TEXT); }
    else if (strcmp(op, "ge_text") == 0) { ABC3(MACH_GE_TEXT); }
    /* Typed bool comparisons */
    else if (strcmp(op, "eq_bool") == 0) { ABC3(MACH_EQ_BOOL); }
    else if (strcmp(op, "ne_bool") == 0) { ABC3(MACH_NE_BOOL); }
    /* Special comparisons */
    else if (strcmp(op, "is_identical") == 0) { ABC3(MACH_IS_IDENTICAL); }
    else if (strcmp(op, "eq_tol") == 0) {
      /* A=dest, B=a, C=b; tolerance in operand 4 */
      int dest = A1, ra = A2, rb = A3, rt = A4;
      /* Move to consecutive scratch slots at end of frame */
      int base = s.nr_slots;
      s.nr_slots += 3;
      EM(MACH_ABC(MACH_MOVE, base, ra, 0));
      EM(MACH_ABC(MACH_MOVE, base + 1, rb, 0));
      EM(MACH_ABC(MACH_MOVE, base + 2, rt, 0));
      EM(MACH_ABC(MACH_EQ_TOL, dest, base, 3));
    }
    else if (strcmp(op, "ne_tol") == 0) {
      int dest = A1, ra = A2, rb = A3, rt = A4;
      int base = s.nr_slots;
      s.nr_slots += 3;
      EM(MACH_ABC(MACH_MOVE, base, ra, 0));
      EM(MACH_ABC(MACH_MOVE, base + 1, rb, 0));
      EM(MACH_ABC(MACH_MOVE, base + 2, rt, 0));
      EM(MACH_ABC(MACH_NEQ_TOL, dest, base, 3));
    }
    /* Type checks */
    else if (strcmp(op, "is_int") == 0)  { AB2(MACH_IS_INT); }
    else if (strcmp(op, "is_num") == 0)  { AB2(MACH_IS_NUM); }
    else if (strcmp(op, "is_text") == 0) { AB2(MACH_IS_TEXT); }
    else if (strcmp(op, "is_bool") == 0)   { AB2(MACH_IS_BOOL); }
    else if (strcmp(op, "is_null") == 0)   { AB2(MACH_IS_NULL); }
    else if (strcmp(op, "is_array") == 0)  { AB2(MACH_IS_ARRAY); }
    else if (strcmp(op, "is_func") == 0)   { AB2(MACH_IS_FUNC); }
    else if (strcmp(op, "is_record") == 0) { AB2(MACH_IS_RECORD); }
    else if (strcmp(op, "is_stone") == 0)  { AB2(MACH_IS_STONE); }
    else if (strcmp(op, "length") == 0)    { AB2(MACH_LENGTH); }
    else if (strcmp(op, "is_proxy") == 0)  { AB2(MACH_IS_PROXY); }
    /* Logical */
    else if (strcmp(op, "not") == 0)     { AB2(MACH_NOT); }
    else if (strcmp(op, "and") == 0)     { ABC3(MACH_AND); }
    else if (strcmp(op, "or") == 0)      { ABC3(MACH_OR); }
    /* Bitwise */
    else if (strcmp(op, "bitnot") == 0)  { AB2(MACH_BITNOT); }
    else if (strcmp(op, "bitand") == 0)  { ABC3(MACH_BITAND); }
    else if (strcmp(op, "bitor") == 0)   { ABC3(MACH_BITOR); }
    else if (strcmp(op, "bitxor") == 0)  { ABC3(MACH_BITXOR); }
    else if (strcmp(op, "shl") == 0)     { ABC3(MACH_SHL); }
    else if (strcmp(op, "shr") == 0)     { ABC3(MACH_SHR); }
    else if (strcmp(op, "ushr") == 0)    { ABC3(MACH_USHR); }
    /* Property access */
    else if (strcmp(op, "load_field") == 0) {
      int dest = A1, obj = A2;
      cJSON *key_item = cJSON_GetArrayItem(it, 3);
      if (cJSON_IsString(key_item)) {
        int ki = ml_cpool_str(&s, key_item->valuestring);
        if (ki <= 255) {
          EM(MACH_ABC(MACH_LOAD_FIELD, dest, obj, ki));
        } else {
          /* cpool index > 255: load key via LOADK, then use dynamic access */
          int tmp = s.nr_slots++;
          EM(MACH_ABx(MACH_LOADK, tmp, ki));
          EM(MACH_ABC(MACH_LOAD_DYNAMIC, dest, obj, tmp));
        }
      } else {
        /* key is a register — fall back to dynamic access */
        int key_reg = (int)key_item->valuedouble;
        EM(MACH_ABC(MACH_LOAD_DYNAMIC, dest, obj, key_reg));
      }
    }
    else if (strcmp(op, "store_field") == 0) {
      int obj = A1, val = A2;
      cJSON *key_item = cJSON_GetArrayItem(it, 3);
      if (cJSON_IsString(key_item)) {
        int ki = ml_cpool_str(&s, key_item->valuestring);
        if (ki <= 255) {
          EM(MACH_ABC(MACH_STORE_FIELD, obj, ki, val));
        } else {
          /* cpool index > 255: load key via LOADK, then use dynamic access */
          int tmp = s.nr_slots++;
          EM(MACH_ABx(MACH_LOADK, tmp, ki));
          EM(MACH_ABC(MACH_STORE_DYNAMIC, obj, tmp, val));
        }
      } else {
        /* key is a register — fall back to dynamic access */
        int key_reg = (int)key_item->valuedouble;
        EM(MACH_ABC(MACH_STORE_DYNAMIC, obj, key_reg, val));
      }
    }
    else if (strcmp(op, "load_index") == 0)   { ABC3(MACH_LOAD_INDEX); }
    else if (strcmp(op, "store_index") == 0) {
      /* mcode: store_index obj val idx → VM: R(A)[R(B)] = R(C) */
      EM(MACH_ABC(MACH_STORE_INDEX, A1, A3, A2));
    }
    else if (strcmp(op, "load_dynamic") == 0)  { ABC3(MACH_LOAD_DYNAMIC); }
    else if (strcmp(op, "store_dynamic") == 0) {
      /* mcode: store_dynamic obj val key → VM: R(A)[R(B)] = R(C) */
      EM(MACH_ABC(MACH_STORE_DYNAMIC, A1, A3, A2));
    }
    /* Delete */
    else if (strcmp(op, "delete") == 0) {
      int dest = A1, obj = A2;
      cJSON *key_item = cJSON_GetArrayItem(it, 3);
      if (cJSON_IsString(key_item)) {
        int ki = ml_cpool_str(&s, key_item->valuestring);
        if (ki <= 255) {
          EM(MACH_ABC(MACH_DELETE, dest, obj, ki));
        } else {
          int tmp = s.nr_slots++;
          EM(MACH_ABx(MACH_LOADK, tmp, ki));
          EM(MACH_ABC(MACH_DELETEINDEX, dest, obj, tmp));
        }
      } else {
        int key_reg = (int)key_item->valuedouble;
        EM(MACH_ABC(MACH_DELETEINDEX, dest, obj, key_reg));
      }
    }
    /* Array/Object creation */
    else if (strcmp(op, "array") == 0) {
      EM(MACH_ABC(MACH_NEWARRAY, A1, A2, 0));
    }
    else if (strcmp(op, "record") == 0) {
      EM(MACH_ABC(MACH_NEWRECORD, A1, A2, 0));
    }
    /* Push/Pop */
    else if (strcmp(op, "push") == 0) {
      EM(MACH_ABC(MACH_PUSH, A1, A2, 0));
    }
    else if (strcmp(op, "pop") == 0) {
      EM(MACH_ABC(MACH_POP, A1, A2, 0));
    }
    /* Closure access */
    else if (strcmp(op, "get") == 0) {
      /* mcode: get dest slot level → GETUP A=dest B=level C=slot */
      EM(MACH_ABC(MACH_GETUP, A1, A3, A2));
    }
    else if (strcmp(op, "put") == 0) {
      /* mcode: put src slot level → SETUP A=src B=level C=slot */
      EM(MACH_ABC(MACH_SETUP, A1, A3, A2));
    }
    /* Function creation */
    else if (strcmp(op, "function") == 0) {
      EM(MACH_ABx(MACH_CLOSURE, A1, A2));
    }
    /* Decomposed function calls */
    else if (strcmp(op, "frame") == 0) {
      EM(MACH_ABC(MACH_FRAME, A1, A2, A3));
    }
    else if (strcmp(op, "setarg") == 0) {
      EM(MACH_ABC(MACH_SETARG, A1, A2, A3));
    }
    else if (strcmp(op, "invoke") == 0 || strcmp(op, "tail_invoke") == 0) {
      EM(MACH_ABC(MACH_INVOKE, A1, A2, 0));
    }
    else if (strcmp(op, "goframe") == 0) {
      EM(MACH_ABC(MACH_GOFRAME, A1, A2, A3));
    }
    else if (strcmp(op, "goinvoke") == 0) {
      EM(MACH_ABC(MACH_GOINVOKE, A1, 0, 0));
    }
    /* Control flow */
    else if (strcmp(op, "jump") == 0) {
      const char *lbl = cJSON_GetArrayItem(it, 1)->valuestring;
      int pc_now = s.code_count;
      EM(MACH_sJ(MACH_JMP, 0));
      ml_patch(&s, pc_now, lbl, 1, 0);
    }
    else if (strcmp(op, "jump_true") == 0) {
      int reg = A1;
      const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring;
      int pc_now = s.code_count;
      EM(MACH_AsBx(MACH_JMPTRUE, reg, 0));
      ml_patch(&s, pc_now, lbl, 0, reg);
    }
    else if (strcmp(op, "jump_false") == 0) {
      int reg = A1;
      const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring;
      int pc_now = s.code_count;
      EM(MACH_AsBx(MACH_JMPFALSE, reg, 0));
      ml_patch(&s, pc_now, lbl, 0, reg);
    }
    else if (strcmp(op, "jump_not_null") == 0) {
      int reg = A1;
      const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring;
      int pc_now = s.code_count;
      EM(MACH_AsBx(MACH_JMPNOTNULL, reg, 0));
      ml_patch(&s, pc_now, lbl, 0, reg);
    }
    /* Return / error */
    else if (strcmp(op, "return") == 0) {
      EM(MACH_ABC(MACH_RETURN, A1, 0, 0));
    }
    else if (strcmp(op, "disrupt") == 0) {
      EM(MACH_ABC(MACH_DISRUPT, 0, 0, 0));
    }
    /* Misc */
    else if (strcmp(op, "in") == 0) {
      EM(MACH_ABC(MACH_IN, A1, A2, A3));
    }
    else if (strcmp(op, "regexp") == 0) {
      int dest = A1;
      const char *pat = cJSON_GetArrayItem(it, 2)->valuestring;
      const char *flg = cJSON_GetArrayItem(it, 3)->valuestring;
      EM(MACH_ABC(MACH_REGEXP, dest, ml_cpool_str(&s, pat), ml_cpool_str(&s, flg)));
    }
    else {
      /* Unknown opcode — emit NOP */
      EM(MACH_ABC(MACH_NOP, 0, 0, 0));
    }
  } }
  /* Sentinel for flat_to_pc */
  s.flat_to_pc[n] = s.code_count;

#undef A1
#undef A2
#undef A3
#undef A4
#undef EM
#undef ABC3
#undef AB2

  /* Resolve pending jump patches */
  for (int i = 0; i < s.patch_count; i++) {
    int target = -1;
    for (int j = 0; j < s.label_count; j++) {
      if (strcmp(s.labels[j].name, s.patches[i].label) == 0) {
        target = s.labels[j].pc;
        break;
      }
    }
    if (target < 0) {
      fprintf(stderr, "mcode_lower: unknown label '%s'\n", s.patches[i].label);
      continue;
    }
    int offset = target - (s.patches[i].pc + 1);
    if (s.patches[i].is_sJ) {
      int old_op = MACH_GET_OP(s.code[s.patches[i].pc]);
      s.code[s.patches[i].pc] = MACH_sJ(old_op, offset);
    } else {
      int old_op = MACH_GET_OP(s.code[s.patches[i].pc]);
      s.code[s.patches[i].pc] = MACH_AsBx(old_op, s.patches[i].reg_a, offset);
    }
  }

  MachCode *mc = sys_malloc(sizeof(MachCode));
  memset(mc, 0, sizeof(MachCode));
  mc->arity = nr_args;
  mc->nr_close_slots = nr_close;
  mc->nr_slots = s.nr_slots;
  mc->entry_point = 0;
  mc->instr_count = s.code_count;
  mc->instructions = s.code;
  mc->cpool_count = s.cpool_count;
  mc->cpool = s.cpool;
  mc->line_table = s.lines;
  if (name && name[0]) {
    mc->name = sys_malloc(strlen(name) + 1);
    memcpy(mc->name, name, strlen(name) + 1);
  }
  if (fname) {
    mc->filename = sys_malloc(strlen(fname) + 1);
    memcpy(mc->filename, fname, strlen(fname) + 1);
  }
  mc->disruption_pc = 0;
  if (dis_raw > 0 && dis_raw < s.flat_count)
    mc->disruption_pc = s.flat_to_pc[dis_raw];

  sys_free(s.flat_to_pc);
  sys_free(s.labels);
  sys_free(s.patches);
  return mc;
}

/* Assign nested functions to each MachCode based on CLOSURE instructions.
   all_funcs is the flat array from the mcode JSON; compiled[i] is the
   compiled MachCode for all_funcs[i]. mc is a compiled MachCode (main or
   one of the flat functions). Scans its instructions for MACH_CLOSURE Bx
   and builds mc->functions with remapped local indices. */
static void mcode_assign_children(MachCode *mc, MachCode **compiled, int total) {
  /* Count unique CLOSURE references */
  int *refs = NULL;
  int ref_count = 0, ref_cap = 0;
  for (uint32_t i = 0; i < mc->instr_count; i++) {
    if (MACH_GET_OP(mc->instructions[i]) == MACH_CLOSURE) {
      int bx = MACH_GET_Bx(mc->instructions[i]);
      /* Check if already in refs */
      int found = 0;
      for (int j = 0; j < ref_count; j++) {
        if (refs[j] == bx) { found = 1; break; }
      }
      if (!found) {
        if (ref_count >= ref_cap) {
          ref_cap = ref_cap ? ref_cap * 2 : 8;
          refs = sys_realloc(refs, ref_cap * sizeof(int));
        }
        refs[ref_count++] = bx;
      }
    }
  }
  if (ref_count == 0) { sys_free(refs); return; }

  /* Build local functions array (preserve original order by flat index) */
  /* Sort refs by value to maintain consistent ordering */
  for (int i = 0; i < ref_count - 1; i++)
    for (int j = i + 1; j < ref_count; j++)
      if (refs[i] > refs[j]) { int t = refs[i]; refs[i] = refs[j]; refs[j] = t; }

  mc->func_count = ref_count;
  mc->functions = sys_malloc(ref_count * sizeof(MachCode *));
  for (int i = 0; i < ref_count; i++)
    mc->functions[i] = (refs[i] < total) ? compiled[refs[i]] : NULL;

  /* Remap CLOSURE Bx: flat index → local index */
  for (uint32_t i = 0; i < mc->instr_count; i++) {
    if (MACH_GET_OP(mc->instructions[i]) == MACH_CLOSURE) {
      int a = MACH_GET_A(mc->instructions[i]);
      int bx = MACH_GET_Bx(mc->instructions[i]);
      for (int j = 0; j < ref_count; j++) {
        if (refs[j] == bx) {
          mc->instructions[i] = MACH_ABx(MACH_CLOSURE, a, j);
          break;
        }
      }
    }
  }
  sys_free(refs);
}


/* Compile mcode JSON IR to MachCode binary.
   mcode_json has: functions[], main{}, filename, name */
MachCode *mach_compile_mcode(cJSON *mcode_json) {
  const char *filename = cJSON_GetStringValue(
      cJSON_GetObjectItemCaseSensitive(mcode_json, "filename"));
  cJSON *funcs_arr = cJSON_GetObjectItemCaseSensitive(mcode_json, "functions");
  int func_count = funcs_arr ? cJSON_GetArraySize(funcs_arr) : 0;

  cJSON *main_obj = cJSON_GetObjectItemCaseSensitive(mcode_json, "main");

  /* Slot compression is handled by the streamline optimizer before mach
     compilation.  mcode_lower_func() asserts nr_slots <= 255. */

  /* Compile all flat functions */
  MachCode **compiled = NULL;
  if (func_count > 0) {
    compiled = sys_malloc(func_count * sizeof(MachCode *));
    memset(compiled, 0, func_count * sizeof(MachCode *));
    { cJSON *fobj = funcs_arr->child;
    for (int i = 0; fobj; i++, fobj = fobj->next)
      compiled[i] = mcode_lower_func(fobj, filename);
    }
  }

  /* Compile main */
  MachCode *main_code = mcode_lower_func(main_obj, filename);

  /* Assign nested functions to each compiled unit */
  for (int i = 0; i < func_count; i++)
    mcode_assign_children(compiled[i], compiled, func_count);
  mcode_assign_children(main_code, compiled, func_count);

  sys_free(compiled);
  return main_code;
}


/* ============================================================
   MACH Public API
   ============================================================ */

/* Print a single constant pool value for dump output */
static void dump_cpool_value(JSContext *ctx, JSValue val) {
  uint32_t tag = JS_VALUE_GET_TAG(val);

  if (JS_IsPtr(val)) {
    void *ptr = JS_VALUE_GET_PTR(val);
    objhdr_t hdr = *(objhdr_t *)ptr;
    uint8_t mist_type = objhdr_type(hdr);
    if (mist_type == OBJ_TEXT) {
      const char *str = JS_ToCString(ctx, val);
      if (str) {
        printf("\"%s\"", str);
        JS_FreeCString(ctx, str);
      } else {
        printf("<string>");
      }
      return;
    }
    printf("<ptr type=%d>", mist_type);
    return;
  }

  switch (tag) {
  case JS_TAG_INT:
    printf("%d", JS_VALUE_GET_INT(val));
    break;
  case JS_TAG_BOOL:
    printf("%s", JS_VALUE_GET_BOOL(val) ? "true" : "false");
    break;
  case JS_TAG_NULL:
    printf("null");
    break;
  case JS_TAG_SHORT_FLOAT:
    printf("%g", JS_VALUE_GET_FLOAT64(val));
    break;
  case JS_TAG_STRING_IMM: {
    const char *str = JS_ToCString(ctx, val);
    if (str) {
      printf("\"%s\"", str);
      JS_FreeCString(ctx, str);
    } else {
      printf("<imm string>");
    }
    break;
  }
  default:
    printf("<tag=%d>", tag);
    break;
  }
}

/* (labels removed in new format) */

/* Internal helper to dump JSCodeRegister (32-bit instruction format) */
static void dump_register_code(JSContext *ctx, JSCodeRegister *code, int indent) {
  char pad[64];
  int pad_len = indent * 2;
  if (pad_len > 60) pad_len = 60;
  memset(pad, ' ', pad_len);
  pad[pad_len] = '\0';

  /* Function header */
  const char *name = "<anonymous>";
  if (!JS_IsNull(code->name)) {
    const char *n = JS_ToCString(ctx, code->name);
    if (n) name = n;
  }
  printf("%sFunction: %s\n", pad, name);
  printf("%s  Arity: %d, Slots: %d, Close: %d\n", pad,
         code->arity, code->nr_slots, code->nr_close_slots);
  if (!JS_IsNull(code->name)) {
    JS_FreeCString(ctx, name);
  }
  if (code->disruption_pc > 0)
    printf("%s  Disruption handler at: %d\n", pad, code->disruption_pc);

  /* Constant pool */
  if (code->cpool_count > 0) {
    printf("%s\n%sConstant Pool (%d entries):\n", pad, pad, code->cpool_count);
    for (uint32_t i = 0; i < code->cpool_count; i++) {
      printf("%s  [%d]: ", pad, i);
      dump_cpool_value(ctx, code->cpool[i]);
      printf("\n");
    }
  }

  /* Instructions */
  printf("%s\n%sInstructions (%d):\n", pad, pad, code->instr_count);
  for (uint32_t i = 0; i < code->instr_count; i++) {
    MachInstr32 instr = code->instructions[i];
    int op = MACH_GET_OP(instr);
    int a = MACH_GET_A(instr);
    int b = MACH_GET_B(instr);
    int c = MACH_GET_C(instr);
    const char *op_name = (op < MACH_OP_COUNT) ? mach_opcode_names[op] : "???";
    if (!op_name) op_name = "???";

    printf("%s  %3d: %-14s ", pad, i, op_name);

    switch (op) {
    /* No operands */
    case MACH_NOP:
    case MACH_RETNIL:
      break;

    /* A only */
    case MACH_LOADNULL:
    case MACH_LOADTRUE:
    case MACH_LOADFALSE:
      printf("r%d", a);
      break;

    /* ABx: load constant */
    case MACH_LOADK: {
      int bx = MACH_GET_Bx(instr);
      printf("r%d, #%d", a, bx);
      if (bx >= 0 && (uint32_t)bx < code->cpool_count) {
        printf("         ; ");
        dump_cpool_value(ctx, code->cpool[bx]);
      }
      break;
    }

    /* AsBx: load small int */
    case MACH_LOADI:
      printf("r%d, %d", a, MACH_GET_sBx(instr));
      break;

    /* A, B: move, unary ops */
    case MACH_MOVE:
    case MACH_NEG:
    case MACH_LNOT:
    case MACH_BNOT:
      printf("r%d, r%d", a, b);
      break;

    /* A, B, C: arithmetic, comparison, bitwise */
    case MACH_ADD: case MACH_SUB: case MACH_MUL: case MACH_DIV:
    case MACH_MOD: case MACH_POW:
    case MACH_EQ: case MACH_NEQ: case MACH_LT: case MACH_LE:
    case MACH_GT: case MACH_GE:
    case MACH_BAND: case MACH_BOR: case MACH_BXOR:
    case MACH_SHL: case MACH_SHR: case MACH_USHR:
      printf("r%d, r%d, r%d", a, b, c);
      break;

    case MACH_EQ_TOL: case MACH_NEQ_TOL:
      printf("r%d, r%d, %d", a, b, c);
      break;

    /* Property access */
    case MACH_GETFIELD:
      printf("r%d, r%d, #%d", a, b, c);
      if ((uint32_t)c < code->cpool_count) {
        printf("      ; ");
        dump_cpool_value(ctx, code->cpool[c]);
      }
      break;
    case MACH_SETFIELD:
      printf("r%d, #%d, r%d", a, b, c);
      if ((uint32_t)b < code->cpool_count) {
        printf("      ; ");
        dump_cpool_value(ctx, code->cpool[b]);
      }
      break;
    case MACH_GETINDEX:
    case MACH_SETINDEX:
      printf("r%d, r%d, r%d", a, b, c);
      break;

    /* ABx: name/intrinsic/env access */
    case MACH_GETNAME:
    case MACH_GETINTRINSIC:
    case MACH_GETENV: {
      int bx = MACH_GET_Bx(instr);
      printf("r%d, #%d", a, bx);
      if ((uint32_t)bx < code->cpool_count) {
        printf("         ; ");
        dump_cpool_value(ctx, code->cpool[bx]);
      }
      break;
    }

    /* Closure access */
    case MACH_GETUP:
    case MACH_SETUP:
      printf("r%d, depth=%d, slot=%d", a, b, c);
      break;

    /* isJ: unconditional jump */
    case MACH_JMP: {
      int offset = MACH_GET_sJ(instr);
      printf("%+d", offset);
      printf("         ; -> %d", (int)i + 1 + offset);
      break;
    }

    /* iAsBx: conditional jumps */
    case MACH_JMPTRUE:
    case MACH_JMPFALSE:
    case MACH_JMPNULL: {
      int offset = MACH_GET_sBx(instr);
      printf("r%d, %+d", a, offset);
      printf("      ; -> %d", (int)i + 1 + offset);
      break;
    }

    /* Return / throw */
    case MACH_RETURN:
    case MACH_THROW:
      printf("r%d", a);
      break;

    /* Object/array creation */
    case MACH_NEWOBJECT:
      printf("r%d", a);
      break;
    case MACH_NEWARRAY:
      printf("r%d, %d", a, b);
      break;

    /* Push/Pop */
    case MACH_PUSH:
      printf("r%d, r%d", a, b);
      break;
    case MACH_POP:
      printf("r%d, r%d", a, b);
      break;

    /* Closure */
    case MACH_CLOSURE: {
      int bx = MACH_GET_Bx(instr);
      printf("r%d, func#%d", a, bx);
      break;
    }

    default:
      printf("0x%08x", instr);
      break;
    }
    printf("\n");
  }

  /* Nested functions */
  if (code->func_count > 0) {
    printf("%s\n%sNested Functions (%d):\n", pad, pad, code->func_count);
    for (uint32_t i = 0; i < code->func_count; i++) {
      printf("%s  [%d]:\n", pad, i);
      if (code->functions[i]) {
        dump_register_code(ctx, code->functions[i], indent + 2);
      } else {
        printf("%s    <null>\n", pad);
      }
    }
  }
}

JSValue JS_RunMachBin(JSContext *ctx, const uint8_t *data, size_t size, JSValue env) {
  MachCode *mc = JS_DeserializeMachCode(data, size);
  if (!mc)
    return JS_ThrowSyntaxError(ctx, "failed to deserialize MACH bytecode");

  JSGCRef env_ref;
  JS_PushGCRef(ctx, &env_ref);
  env_ref.val = env;

  JSCodeRegister *code = JS_LoadMachCode(ctx, mc, env_ref.val);
  JS_FreeMachCode(mc);

  JSValue result = JS_CallRegisterVM(ctx, code, ctx->global_obj, 0, NULL, env_ref.val, JS_NULL);
  JS_PopGCRef(ctx, &env_ref);
  return result;
}

JSValue JS_RunMachMcode(JSContext *ctx, const char *json_str, size_t len, JSValue env) {
  (void)len;
  cJSON *mcode = cJSON_Parse(json_str);
  if (!mcode)
    return JS_ThrowSyntaxError(ctx, "failed to parse mcode JSON");

  MachCode *mc = mach_compile_mcode(mcode);
  cJSON_Delete(mcode);
  if (!mc)
    return JS_ThrowInternalError(ctx, "mcode compilation failed");

  JSGCRef env_ref;
  JS_PushGCRef(ctx, &env_ref);
  env_ref.val = env;

  JSCodeRegister *code = JS_LoadMachCode(ctx, mc, env_ref.val);
  JS_FreeMachCode(mc);

  JSValue result = JS_CallRegisterVM(ctx, code, ctx->global_obj, 0, NULL, env_ref.val, JS_NULL);
  JS_PopGCRef(ctx, &env_ref);
  return result;
}

void JS_DumpMachBin(JSContext *ctx, const uint8_t *data, size_t size, JSValue env) {
  MachCode *mc = JS_DeserializeMachCode(data, size);
  if (!mc) {
    printf("Failed to deserialize MACH bytecode\n");
    return;
  }

  JSGCRef env_ref;
  JS_PushGCRef(ctx, &env_ref);
  env_ref.val = env;

  JSCodeRegister *code = JS_LoadMachCode(ctx, mc, env_ref.val);
  JS_FreeMachCode(mc);

  dump_register_code(ctx, code, 0);
  JS_PopGCRef(ctx, &env_ref);
}

