/*
 * QBE Helper Functions
 *
 * Thin C wrappers called from QBE-generated code for operations
 * that are too complex to inline: float arithmetic, float comparison,
 * string comparison, bitwise ops on floats, and boolean conversion.
 */

#include "quickjs-internal.h"
#include <math.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

/* Non-inline wrappers for static inline functions in quickjs.h */
JSValue qbe_new_float64(JSContext *ctx, double d) {
  return __JS_NewFloat64(ctx, d);
}

/* Comparison op IDs (must match qbe.cm float_cmp_op_id values) */
enum {
  QBE_CMP_EQ = 0,
  QBE_CMP_NE = 1,
  QBE_CMP_LT = 2,
  QBE_CMP_LE = 3,
  QBE_CMP_GT = 4,
  QBE_CMP_GE = 5
};

/* Generic comparison helper matching MACH eq/ne/lt/le/gt/ge semantics. */
JSValue cell_rt_cmp(JSContext *ctx, int op, JSValue a, JSValue b) {
  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 QBE_CMP_EQ: return JS_NewBool(ctx, ia == ib);
    case QBE_CMP_NE: return JS_NewBool(ctx, ia != ib);
    case QBE_CMP_LT: return JS_NewBool(ctx, ia < ib);
    case QBE_CMP_LE: return JS_NewBool(ctx, ia <= ib);
    case QBE_CMP_GT: return JS_NewBool(ctx, ia > ib);
    case QBE_CMP_GE: return JS_NewBool(ctx, ia >= ib);
    default: return JS_NewBool(ctx, 0);
    }
  }

  /* Fast path: identity after chasing forward pointers (matches MACH). */
  {
    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 == QBE_CMP_EQ || op == QBE_CMP_LE || op == QBE_CMP_GE)
        return JS_TRUE;
      if (op == QBE_CMP_NE)
        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 QBE_CMP_EQ: return JS_NewBool(ctx, da == db);
    case QBE_CMP_NE: return JS_NewBool(ctx, da != db);
    case QBE_CMP_LT: return JS_NewBool(ctx, da < db);
    case QBE_CMP_LE: return JS_NewBool(ctx, da <= db);
    case QBE_CMP_GT: return JS_NewBool(ctx, da > db);
    case QBE_CMP_GE: return JS_NewBool(ctx, da >= db);
    default: return JS_NewBool(ctx, 0);
    }
  }

  if (mist_is_text(a) && mist_is_text(b)) {
    int cmp = js_string_compare_value(ctx, a, b, FALSE);
    switch (op) {
    case QBE_CMP_EQ: return JS_NewBool(ctx, cmp == 0);
    case QBE_CMP_NE: return JS_NewBool(ctx, cmp != 0);
    case QBE_CMP_LT: return JS_NewBool(ctx, cmp < 0);
    case QBE_CMP_LE: return JS_NewBool(ctx, cmp <= 0);
    case QBE_CMP_GT: return JS_NewBool(ctx, cmp > 0);
    case QBE_CMP_GE: return JS_NewBool(ctx, cmp >= 0);
    default: return JS_NewBool(ctx, 0);
    }
  }

  if (JS_IsNull(a) && JS_IsNull(b)) {
    if (op == QBE_CMP_EQ || op == QBE_CMP_LE || op == QBE_CMP_GE)
      return JS_TRUE;
    return JS_FALSE;
  }

  if (JS_IsBool(a) && JS_IsBool(b)) {
    int ba = JS_VALUE_GET_BOOL(a);
    int bb = JS_VALUE_GET_BOOL(b);
    switch (op) {
    case QBE_CMP_EQ: return JS_NewBool(ctx, ba == bb);
    case QBE_CMP_NE: return JS_NewBool(ctx, ba != bb);
    case QBE_CMP_LT: return JS_NewBool(ctx, ba < bb);
    case QBE_CMP_LE: return JS_NewBool(ctx, ba <= bb);
    case QBE_CMP_GT: return JS_NewBool(ctx, ba > bb);
    case QBE_CMP_GE: return JS_NewBool(ctx, ba >= bb);
    default: return JS_NewBool(ctx, 0);
    }
  }

  if (op == QBE_CMP_EQ)
    return JS_NewBool(ctx, 0);
  if (op == QBE_CMP_NE)
    return JS_NewBool(ctx, 1);

  JS_RaiseDisrupt(ctx, "cannot compare: operands must be same type");
  return JS_EXCEPTION;
}

/* ============================================================
   Bitwise not on non-int (float -> int32 -> ~)
   ============================================================ */

JSValue qbe_bnot(JSContext *ctx, JSValue v) {
  int32_t i;
  JS_ToInt32(ctx, &i, v);
  return JS_NewInt32(ctx, ~i);
}

/* ============================================================
   Bitwise binary ops on floats (convert both to int32, apply, re-tag)
   ============================================================ */

JSValue qbe_bitwise_and(JSContext *ctx, JSValue a, JSValue b) {
  int32_t ia, ib;
  JS_ToInt32(ctx, &ia, a);
  JS_ToInt32(ctx, &ib, b);
  return JS_NewInt32(ctx, ia & ib);
}

JSValue qbe_bitwise_or(JSContext *ctx, JSValue a, JSValue b) {
  int32_t ia, ib;
  JS_ToInt32(ctx, &ia, a);
  JS_ToInt32(ctx, &ib, b);
  return JS_NewInt32(ctx, ia | ib);
}

JSValue qbe_bitwise_xor(JSContext *ctx, JSValue a, JSValue b) {
  int32_t ia, ib;
  JS_ToInt32(ctx, &ia, a);
  JS_ToInt32(ctx, &ib, b);
  return JS_NewInt32(ctx, ia ^ ib);
}

/* Concat helper matching MACH_CONCAT semantics exactly. */
JSValue cell_rt_concat(JSContext *ctx, JSValue left, JSValue right, int self_assign) {
  if (self_assign) {
    if (JS_IsPtr(left)) {
      JSText *s = (JSText *)chase(left);
      int slen = (int)s->length;
      int rlen = js_string_value_len(right);
      int cap = (int)objhdr_cap56(s->hdr);
      if (objhdr_type(s->hdr) == OBJ_TEXT
          && !(s->hdr & OBJHDR_S_MASK)
          && slen + rlen <= cap) {
        for (int i = 0; i < rlen; i++)
          string_put(s, slen + i, js_string_value_get(right, i));
        s->length = slen + rlen;
        return left;
      }
    }
    JSValue res = JS_ConcatStringGrow(ctx, left, right);
    if (JS_IsException(res))
      return JS_EXCEPTION;
    return res;
  }

  JSValue res = JS_ConcatString(ctx, left, right);
  if (JS_IsException(res))
    return JS_EXCEPTION;
  return res;
}

/* ============================================================
   cell_rt_* — Runtime support for QBE-compiled code
   ============================================================ */

#include <dlfcn.h>
#include <stdio.h>

/* --- Property access --- */

typedef struct {
  void *dl_handle;
  JSContext *ctx;
  JSGCRef *vals;
  int count;
} AOTLiteralPool;
typedef struct {
  const char *name;
  JSValue key;
} AOTKeyCacheEntry;
typedef struct {
  void *dl_handle;
  int64_t fn_idx;
  JSValue code;
} AOTCodeCacheEntry;

typedef struct AOTGCRefChunk AOTGCRefChunk;

typedef struct {
  void *current_dl_handle;
  AOTLiteralPool lit_pool;
  AOTKeyCacheEntry *key_cache;
  int key_cache_count;
  int key_cache_cap;
  AOTCodeCacheEntry *code_cache;
  int code_cache_count;
  int code_cache_cap;
  AOTGCRefChunk **gc_ref_chunks;
  int gc_ref_chunk_count;
  int aot_depth;
  JSValue pending_callee_frame;
  int pending_is_tail;
} NativeRTState;

static NativeRTState *native_state(JSContext *ctx) {
  NativeRTState *st = (NativeRTState *)ctx->native_state;
  if (st) return st;
  st = js_mallocz_rt(sizeof(*st));
  if (!st) {
    JS_RaiseOOM(ctx);
    return NULL;
  }
  ctx->native_state = st;
  return st;
}

static void aot_clear_lit_pool(JSContext *ctx, NativeRTState *st) {
  if (!st) return;
  if (st->lit_pool.vals) {
    for (int i = 0; i < st->lit_pool.count; i++)
      JS_DeleteGCRef(ctx, &st->lit_pool.vals[i]);
    free(st->lit_pool.vals);
  }
  st->lit_pool.dl_handle = NULL;
  st->lit_pool.ctx = NULL;
  st->lit_pool.vals = NULL;
  st->lit_pool.count = 0;
}

static int aot_load_lit_pool(JSContext *ctx, NativeRTState *st, void *dl_handle) {
  aot_clear_lit_pool(ctx, st);
  st->lit_pool.dl_handle = dl_handle;
  st->lit_pool.ctx = ctx;
  if (!dl_handle)
    return 1;

  int *count_ptr = (int *)dlsym(dl_handle, "cell_lit_count");
  const char **table_ptr = (const char **)dlsym(dl_handle, "cell_lit_table");
  int count = count_ptr ? *count_ptr : 0;
  if (count <= 0 || !table_ptr)
    return 1;

  st->lit_pool.vals = (JSGCRef *)calloc((size_t)count, sizeof(JSGCRef));
  if (!st->lit_pool.vals) {
    JS_RaiseOOM(ctx);
    return 0;
  }
  st->lit_pool.count = 0;

  for (int i = 0; i < count; i++) {
    const char *cstr = table_ptr[i] ? table_ptr[i] : "";
    JS_AddGCRef(ctx, &st->lit_pool.vals[i]);
    st->lit_pool.count = i + 1;
    st->lit_pool.vals[i].val = js_key_new(ctx, cstr);
    if (JS_IsException(st->lit_pool.vals[i].val)) {
      aot_clear_lit_pool(ctx, st);
      return 0;
    }
  }
  return 1;
}

static JSValue aot_lit_from_index(JSContext *ctx, int64_t lit_idx) {
  NativeRTState *st = native_state(ctx);
  if (!st) return JS_EXCEPTION;
  if (lit_idx < 0) {
    JS_RaiseDisrupt(ctx, "literal index out of range");
    return JS_EXCEPTION;
  }

  if (st->lit_pool.dl_handle != st->current_dl_handle || st->lit_pool.ctx != ctx) {
    if (!aot_load_lit_pool(ctx, st, st->current_dl_handle))
      return JS_EXCEPTION;
  }

  if (lit_idx >= st->lit_pool.count) {
    JS_RaiseDisrupt(ctx, "literal index out of range");
    return JS_EXCEPTION;
  }
  return st->lit_pool.vals[lit_idx].val;
}

/* Convert a static C string to an interned JSValue key.
   Uses a small per-actor cache keyed by C-string pointer to avoid
   repeated UTF-8 decoding in hot property paths. */
static JSValue aot_key_from_cstr(JSContext *ctx, const char *name) {
  NativeRTState *st = native_state(ctx);
  if (!st) return JS_EXCEPTION;
  if (!name)
    return JS_NULL;

  for (int i = 0; i < st->key_cache_count; i++) {
    if (st->key_cache[i].name == name)
      return st->key_cache[i].key;
  }

  JSValue key = js_key_new(ctx, name);
  if (JS_IsNull(key))
    return JS_RaiseDisrupt(ctx, "invalid property key");

  if (st->key_cache_count >= st->key_cache_cap) {
    int new_cap = st->key_cache_cap ? (st->key_cache_cap * 2) : 64;
    AOTKeyCacheEntry *new_cache =
        (AOTKeyCacheEntry *)realloc(st->key_cache, (size_t)new_cap * sizeof(*new_cache));
    if (!new_cache)
      return JS_RaiseOOM(ctx);
    st->key_cache = new_cache;
    st->key_cache_cap = new_cap;
  }

  st->key_cache[st->key_cache_count].name = name;
  st->key_cache[st->key_cache_count].key = key;
  st->key_cache_count++;
  return key;
}

static JSValue cell_rt_load_field_key(JSContext *ctx, JSValue obj, JSValue key) {
  if (JS_IsFunction(obj)) {
    JS_RaiseDisrupt(ctx, "cannot read property of function");
    return JS_EXCEPTION;
  }
  return JS_GetProperty(ctx, obj, key);
}

JSValue cell_rt_load_field_lit(JSContext *ctx, JSValue obj, int64_t lit_idx) {
  JSValue key = aot_lit_from_index(ctx, lit_idx);
  if (JS_IsException(key))
    return JS_EXCEPTION;
  return cell_rt_load_field_key(ctx, obj, key);
}

static int cell_rt_store_field_key(JSContext *ctx, JSValue val, JSValue obj,
                                   JSValue key) {
  int ret = JS_SetProperty(ctx, obj, key, val);
  return (ret < 0 || JS_HasException(ctx)) ? 0 : 1;
}

int cell_rt_store_field_lit(JSContext *ctx, JSValue val, JSValue obj,
                            int64_t lit_idx) {
  JSValue key = aot_lit_from_index(ctx, lit_idx);
  if (JS_IsException(key))
    return 0;
  return cell_rt_store_field_key(ctx, val, obj, key);
}

JSValue cell_rt_access_lit(JSContext *ctx, int64_t lit_idx) {
  return aot_lit_from_index(ctx, lit_idx);
}

JSValue cell_rt_load_dynamic(JSContext *ctx, JSValue obj, JSValue key) {
  if (JS_IsInt(key))
    return JS_GetPropertyNumber(ctx, obj, (uint32_t)JS_VALUE_GET_INT(key));
  return JS_GetProperty(ctx, obj, key);
}

int cell_rt_store_dynamic(JSContext *ctx, JSValue val, JSValue obj,
                          JSValue key) {
  int ret = 0;
  JSValue nr = JS_NULL;
  if (JS_IsInt(key)) {
    nr = JS_SetPropertyNumber(ctx, obj, (uint32_t)JS_VALUE_GET_INT(key), val);
    return JS_IsException(nr) ? 0 : 1;
  } else if (JS_IsArray(obj) && !JS_IsInt(key)) {
    JS_RaiseDisrupt(ctx, "array index must be a number");
    return 0;
  } else if (JS_IsBool(key) || JS_IsNull(key) || JS_IsArray(key) || JS_IsFunction(key)) {
    JS_RaiseDisrupt(ctx, "object key must be text");
    return 0;
  } else {
    ret = JS_SetProperty(ctx, obj, key, val);
    return (ret < 0 || JS_HasException(ctx)) ? 0 : 1;
  }
}

/* --- Env-based variable lookup (env at frame slot 0) --- */

JSValue cell_rt_access_env(JSContext *ctx, JSValue env, int64_t lit_idx) {
  JSValue key = aot_lit_from_index(ctx, lit_idx);
  if (JS_IsException(key))
    return JS_EXCEPTION;
  /* Try env first (linear scan for stoned records) */
  if (!JS_IsNull(env) && JS_IsRecord(env)) {
    JSRecord *rec = (JSRecord *)chase(env);
    uint64_t mask = objhdr_cap56(rec->mist_hdr);
    for (uint64_t i = 1; i <= mask; i++) {
      if (js_key_equal(rec->slots[i].key, key))
        return rec->slots[i].val;
    }
  }
  /* Fallback to global object */
  JSValue gobj = ctx->global_obj;
  if (JS_IsRecord(gobj)) {
    JSRecord *rec = (JSRecord *)chase(gobj);
    uint64_t mask = objhdr_cap56(rec->mist_hdr);
    for (uint64_t i = 1; i <= mask; i++) {
      if (js_key_equal(rec->slots[i].key, key))
        return rec->slots[i].val;
    }
  }
  const char *kstr = JS_ToCString(ctx, key);
  JS_RaiseDisrupt(ctx, "'%s' is not defined", kstr ? kstr : "?");
  if (kstr) JS_FreeCString(ctx, kstr);
  return JS_EXCEPTION;
}

/* --- GC-managed AOT frame stack ---
   Each native dispatch loop pushes a GC ref so the GC can find and
   update the current frame pointer when it moves objects.
   cell_rt_refresh_fp re-derives the slot pointer after any GC call. */

// Keep GC roots for native frames in stable heap chunks (no fixed depth cap).
#define AOT_GC_REF_CHUNK_SIZE 1024
typedef struct AOTGCRefChunk {
  JSGCRef refs[AOT_GC_REF_CHUNK_SIZE];
  uint8_t inited[AOT_GC_REF_CHUNK_SIZE];
} AOTGCRefChunk;

int cell_rt_native_active(JSContext *ctx) {
  NativeRTState *st = (NativeRTState *)ctx->native_state;
  return st ? (st->aot_depth > 0) : 0;
}

static int ensure_aot_gc_ref_slot(JSContext *ctx, NativeRTState *st, int depth_index) {
  if (depth_index < 0)
    return 0;
  int needed_chunks = (depth_index / AOT_GC_REF_CHUNK_SIZE) + 1;
  if (needed_chunks <= st->gc_ref_chunk_count)
    return 1;
  AOTGCRefChunk **new_chunks =
      (AOTGCRefChunk **)realloc(st->gc_ref_chunks,
                                (size_t)needed_chunks * sizeof(*new_chunks));
  if (!new_chunks) {
    JS_RaiseOOM(ctx);
    return 0;
  }
  st->gc_ref_chunks = new_chunks;
  for (int i = st->gc_ref_chunk_count; i < needed_chunks; i++) {
    st->gc_ref_chunks[i] = (AOTGCRefChunk *)calloc(1, sizeof(AOTGCRefChunk));
    if (!st->gc_ref_chunks[i]) {
      JS_RaiseOOM(ctx);
      return 0;
    }
  }
  st->gc_ref_chunk_count = needed_chunks;
  return 1;
}

static inline JSGCRef *aot_gc_ref_at(NativeRTState *st, int depth_index) {
  int chunk_index = depth_index / AOT_GC_REF_CHUNK_SIZE;
  int slot_index = depth_index % AOT_GC_REF_CHUNK_SIZE;
  return &st->gc_ref_chunks[chunk_index]->refs[slot_index];
}

static inline uint8_t *aot_gc_ref_inited_at(NativeRTState *st, int depth_index) {
  int chunk_index = depth_index / AOT_GC_REF_CHUNK_SIZE;
  int slot_index = depth_index % AOT_GC_REF_CHUNK_SIZE;
  return &st->gc_ref_chunks[chunk_index]->inited[slot_index];
}

static inline void aot_gc_ref_activate(JSContext *ctx, NativeRTState *st, int depth_index) {
  JSGCRef *ref = aot_gc_ref_at(st, depth_index);
  uint8_t *inited = aot_gc_ref_inited_at(st, depth_index);
  if (!*inited) {
    JS_AddGCRef(ctx, ref);
    *inited = 1;
  }
}

JSValue *cell_rt_enter_frame(JSContext *ctx, int64_t nr_slots) {
  NativeRTState *st = native_state(ctx);
  if (!st) return NULL;
  if (!ensure_aot_gc_ref_slot(ctx, st, st->aot_depth)) {
    return NULL;
  }
  JSFrameRegister *frame = alloc_frame_register(ctx, (int)nr_slots);
  if (!frame) return NULL;
  aot_gc_ref_activate(ctx, st, st->aot_depth);
  JSGCRef *ref = aot_gc_ref_at(st, st->aot_depth);
  ref->val = JS_MKPTR(frame);
  st->aot_depth++;
  return (JSValue *)frame->slots;
}

/* Push an already-allocated frame onto the active AOT frame stack. */
static int cell_rt_push_existing_frame(JSContext *ctx, JSValue frame_val) {
  NativeRTState *st = native_state(ctx);
  if (!st) return 0;
  if (!ensure_aot_gc_ref_slot(ctx, st, st->aot_depth))
    return 0;
  aot_gc_ref_activate(ctx, st, st->aot_depth);
  JSGCRef *ref = aot_gc_ref_at(st, st->aot_depth);
  ref->val = frame_val;
  st->aot_depth++;
  return 1;
}

JSValue *cell_rt_refresh_fp(JSContext *ctx) {
  NativeRTState *st = native_state(ctx);
  if (!st) return NULL;
  if (st->aot_depth <= 0) {
    fprintf(stderr, "[BUG] cell_rt_refresh_fp: aot_depth=%d\n", st->aot_depth);
    abort();
  }
  JSValue val = aot_gc_ref_at(st, st->aot_depth - 1)->val;
  JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(val);
  if (!frame) {
    fprintf(stderr, "[BUG] cell_rt_refresh_fp: frame is NULL at depth=%d val=%lld\n",
            st->aot_depth, (long long)val);
    abort();
  }
  return (JSValue *)frame->slots;
}

/* Combined refresh + exception check in a single call. */
JSValue *cell_rt_refresh_fp_checked(JSContext *ctx) {
  NativeRTState *st = native_state(ctx);
  if (!st) return NULL;
  if (JS_HasException(ctx))
    return NULL;
  if (st->aot_depth <= 0) {
    fprintf(stderr, "[BUG] cell_rt_refresh_fp_checked: aot_depth=%d\n", st->aot_depth);
    abort();
  }
  JSValue val = aot_gc_ref_at(st, st->aot_depth - 1)->val;
  JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(val);
  if (!frame) {
    fprintf(stderr, "[BUG] cell_rt_refresh_fp_checked: frame is NULL\n");
    abort();
  }
  return (JSValue *)frame->slots;
}

void cell_rt_leave_frame(JSContext *ctx) {
  NativeRTState *st = native_state(ctx);
  if (!st) return;
  if (st->aot_depth <= 0) {
    fprintf(stderr, "[BUG] cell_rt_leave_frame underflow\n");
    abort();
  }
  st->aot_depth--;
  aot_gc_ref_at(st, st->aot_depth)->val = JS_NULL;
}

/* --- Function creation and calling --- */

typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp);

/* ============================================================
   Dispatch loop — the core of native function execution.
   Each compiled cell_fn_N returns to this loop when it needs
   to call another function (instead of recursing via C stack).
   ============================================================ */

void cell_rt_signal_call(JSContext *ctx, void *fp, int64_t frame_slot) {
  NativeRTState *st = native_state(ctx);
  if (!st) return;
  JSValue *slots = (JSValue *)fp;
  st->pending_callee_frame = slots[frame_slot];
  st->pending_is_tail = 0;
}

void cell_rt_signal_tail_call(JSContext *ctx, void *fp, int64_t frame_slot) {
  NativeRTState *st = native_state(ctx);
  if (!st) return;
  JSValue *slots = (JSValue *)fp;
  st->pending_callee_frame = slots[frame_slot];
  st->pending_is_tail = 1;
}

static int cell_check_call_arity(JSContext *ctx, JSFunction *fn, int argc) {
  if (unlikely(fn->length >= 0 && argc > fn->length)) {
    char buf[KEY_GET_STR_BUF_SIZE];
    JS_RaiseDisrupt(ctx, "too many arguments for %s: expected %d, got %d",
                    JS_KeyGetStr(ctx, buf, KEY_GET_STR_BUF_SIZE, fn->name),
                    fn->length, argc);
    return 0;
  }
  return 1;
}

JSValue cell_rt_apply(JSContext *ctx, JSValue fn_val, JSValue arg_val) {
  if (!mist_is_function(fn_val))
    return fn_val;

  JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val);
  if (!mist_is_array(arg_val)) {
    if (!cell_check_call_arity(ctx, fn, 1))
      return JS_EXCEPTION;
    return JS_CallInternal(ctx, fn_val, JS_NULL, 1, &arg_val, 0);
  }

  JSArray *arr = JS_VALUE_GET_ARRAY(arg_val);
  int len = arr->len;
  if (!cell_check_call_arity(ctx, fn, len))
    return JS_EXCEPTION;
  if (len == 0)
    return JS_CallInternal(ctx, fn_val, JS_NULL, 0, NULL, 0);

  JSValue args[len];
  for (int i = 0; i < len; i++)
    args[i] = arr->values[i];
  return JS_CallInternal(ctx, fn_val, JS_NULL, len, args, 0);
}

static inline void cell_copy_args_0_4(JSValue *fp, JSValue *argv, int copy) {
  /* fp[0] is `this`; copy args into fp[1..4] */
  switch (copy) {
  case 4: fp[4] = argv[3];
  case 3: fp[3] = argv[2];
  case 2: fp[2] = argv[1];
  case 1: fp[1] = argv[0];
  case 0: break;
  default: break;
  }
}

static inline void cell_sync_dl_from_native_fn(NativeRTState *st, JSFunction *fn) {
  st->current_dl_handle = JS_VALUE_GET_CODE(fn->u.cell.code)->u.native.dl_handle;
}

/* Entry point called from JS_CallInternal / JS_Call / MACH_INVOKE
   for JS_FUNC_KIND_NATIVE functions. */
JSValue cell_native_dispatch(JSContext *ctx, JSValue func_obj,
                             JSValue this_obj, int argc, JSValue *argv) {
  NativeRTState *st = native_state(ctx);
  if (!st) return JS_EXCEPTION;
  JSFunction *f = JS_VALUE_GET_FUNCTION(func_obj);
  JSCode *f_code = JS_VALUE_GET_CODE(FN_READ_CODE(f));
  cell_compiled_fn fn = (cell_compiled_fn)f_code->u.native.fn_ptr;
  int nr_slots = f_code->u.native.nr_slots;
  int arity = f->length;
  void *prev_dl_handle = st->current_dl_handle;
  st->current_dl_handle = f_code->u.native.dl_handle;

#define RETURN_DISPATCH(v)                                                     \
  do {                                                                         \
    atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);         \
    st->current_dl_handle = prev_dl_handle;                                    \
    return (v);                                                                \
  } while (0)

  if (!cell_check_call_arity(ctx, f, argc))
    RETURN_DISPATCH(JS_EXCEPTION);

  /* Root func_obj across allocation — GC can move it */
  JSGCRef func_ref;
  JS_PushGCRef(ctx, &func_ref);
  func_ref.val = func_obj;

  /* Allocate initial frame */
  JSValue *fp = cell_rt_enter_frame(ctx, nr_slots);
  if (!fp) {
    JS_PopGCRef(ctx, &func_ref);
    RETURN_DISPATCH(JS_EXCEPTION);
  }

  /* Re-derive func_obj after potential GC */
  func_obj = func_ref.val;
  JS_PopGCRef(ctx, &func_ref);

  /* Set up frame: this in slot 0, args in slots 1..N */
  fp[0] = this_obj;
  int copy = (argc < arity) ? argc : arity;
  if (copy < 0) copy = argc; /* variadic: copy all */
  if (copy > nr_slots - 1)
    copy = nr_slots - 1;
  if (unlikely(copy > 4)) {
    JS_RaiseDisrupt(ctx, "native calls support at most 4 arguments");
    RETURN_DISPATCH(JS_EXCEPTION);
  }
  if (copy > 0 && argv)
    cell_copy_args_0_4(fp, argv, copy);

  /* Link function to frame for closure access */
  JSFrameRegister *frame = (JSFrameRegister *)((char *)fp - offsetof(JSFrameRegister, slots));
  frame->function = func_obj;

  int base_depth = st->aot_depth; /* remember entry depth for return detection */

  for (;;) {
    st->pending_callee_frame = 0;
    st->pending_is_tail = 0;
    if (atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed) >= 1)
      atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);

    /* Keep closure creation bound to the currently executing native module. */
    if (JS_IsFunction(frame->function)) {
      JSFunction *cur_fn = JS_VALUE_GET_FUNCTION(frame->function);
      if (cur_fn->kind == JS_FUNC_KIND_NATIVE)
        st->current_dl_handle = JS_VALUE_GET_CODE(FN_READ_CODE(cur_fn))->u.native.dl_handle;
    }

    JSValue result = fn(ctx, fp);

    /* Re-derive frame after potential GC */
    if (st->aot_depth <= 0) {
      fprintf(stderr, "[BUG] native dispatch lost frame depth after fn call\n");
      abort();
    }
    JSValue frame_val = aot_gc_ref_at(st, st->aot_depth - 1)->val;
    frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
    fp = (JSValue *)frame->slots;

    if (st->pending_callee_frame != 0) {
      /* Function signaled a call — dispatch it */
      JSValue callee_frame_val = st->pending_callee_frame;
      st->pending_callee_frame = 0;
      int pending_is_tail = st->pending_is_tail;
      st->pending_is_tail = 0;
      JSGCRef callee_ref;
      JS_PushGCRef(ctx, &callee_ref);
      callee_ref.val = callee_frame_val;
      JSFrameRegister *callee_fr = (JSFrameRegister *)JS_VALUE_GET_PTR(callee_ref.val);
      int callee_argc = JS_VALUE_GET_INT(callee_fr->address);
      if (callee_argc < 0)
        callee_argc = 0;
      JSValue callee_fn_val = callee_fr->function;

      if (!JS_IsFunction(callee_fn_val)) {
        JS_RaiseDisrupt(ctx, "not a function");
        {
          int ret_info = JS_VALUE_GET_INT(frame->address);
          int ret_slot = ret_info & 0xFFFF;
          if (ret_slot != 0xFFFF)
            fp[ret_slot] = JS_EXCEPTION;
        }
        /* Resume caller with exception pending */
        JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function);
        fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_fn))->u.native.fn_ptr;
        cell_sync_dl_from_native_fn(st, exc_fn);
        JS_PopGCRef(ctx, &callee_ref);
        continue;
      }

      JSFunction *callee_fn = JS_VALUE_GET_FUNCTION(callee_fn_val);
      if (!cell_check_call_arity(ctx, callee_fn, callee_argc)) {
        int ret_info = JS_VALUE_GET_INT(frame->address);
        int ret_slot = ret_info & 0xFFFF;
        if (ret_slot != 0xFFFF)
          fp[ret_slot] = JS_EXCEPTION;
        JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function);
        fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_fn))->u.native.fn_ptr;
        cell_sync_dl_from_native_fn(st, exc_fn);
        JS_PopGCRef(ctx, &callee_ref);
        continue;
      }

      if (callee_fn->kind == JS_FUNC_KIND_NATIVE) {
        /* Native-to-native call — no C stack growth */
        cell_compiled_fn callee_ptr = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(callee_fn))->u.native.fn_ptr;

        if (pending_is_tail) {
          /* Tail call: replace current frame with the prepared callee frame. */
          JSValue saved_caller = frame->caller;

          /* Pop old frame */
          cell_rt_leave_frame(ctx);

          callee_fr = (JSFrameRegister *)JS_VALUE_GET_PTR(callee_ref.val);
          callee_fr->function = callee_fn_val;
          callee_fr->caller = saved_caller;
          callee_fr->address = JS_NewInt32(ctx, 0);

          if (!cell_rt_push_existing_frame(ctx, callee_ref.val)) {
            JS_PopGCRef(ctx, &callee_ref);
            RETURN_DISPATCH(JS_EXCEPTION);
          }
          frame = (JSFrameRegister *)JS_VALUE_GET_PTR(aot_gc_ref_at(st, st->aot_depth - 1)->val);
          fp = (JSValue *)frame->slots;
          fn = callee_ptr;
          cell_sync_dl_from_native_fn(st, callee_fn);
        } else {
          /* Regular call: link caller and push prepared callee frame. */
          int ret_info = JS_VALUE_GET_INT(frame->address);
          int resume_seg = ret_info >> 16;
          int ret_slot = ret_info & 0xFFFF;

          /* Save return address in caller */
          frame->address = JS_NewInt32(ctx, (resume_seg << 16) | ret_slot);

          callee_fr = (JSFrameRegister *)JS_VALUE_GET_PTR(callee_ref.val);
          callee_fr->function = callee_fn_val;
          callee_fr->caller = JS_MKPTR(frame);
          callee_fr->address = JS_NewInt32(ctx, 0);

          if (!cell_rt_push_existing_frame(ctx, callee_ref.val)) {
            /* Resume caller with exception pending */
            frame_val = aot_gc_ref_at(st, st->aot_depth - 1)->val;
            frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
            fp = (JSValue *)frame->slots;
            JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function);
            fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_fn))->u.native.fn_ptr;
            cell_sync_dl_from_native_fn(st, exc_fn);
            JS_PopGCRef(ctx, &callee_ref);
            continue;
          }
          frame = (JSFrameRegister *)JS_VALUE_GET_PTR(aot_gc_ref_at(st, st->aot_depth - 1)->val);
          fp = (JSValue *)frame->slots;
          fn = callee_ptr;
          cell_sync_dl_from_native_fn(st, callee_fn);
        }
      } else {
        /* Non-native callee (C function, register VM, etc.) —
           call it via the standard path and store the result */
        JSValue ret;
        if (callee_fn->kind == JS_FUNC_KIND_C)
          ret = js_call_c_function(ctx, callee_fn_val, callee_fr->slots[0],
                                   callee_argc, &callee_fr->slots[1]);
        else
          ret = JS_CallInternal(ctx, callee_fn_val, callee_fr->slots[0],
                                callee_argc, &callee_fr->slots[1], 0);

        /* Re-derive frame after call */
        frame_val = aot_gc_ref_at(st, st->aot_depth - 1)->val;
        frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
        fp = (JSValue *)frame->slots;

        if (JS_IsException(ret)) {
          /* Non-native callee threw — resume caller with exception pending.
             Tag the pending return slot with JS_EXCEPTION so generated code
             can branch without an extra JS_HasException C call. */
          if (!JS_HasException(ctx))
            JS_Disrupt(ctx);
          int ret_info = JS_VALUE_GET_INT(frame->address);
          int ret_slot = ret_info & 0xFFFF;
          if (ret_slot != 0xFFFF)
            fp[ret_slot] = JS_EXCEPTION;
          /* fn and fp still point to the calling native function's frame.
             Just resume it — it will detect JS_EXCEPTION in the return slot. */
          JSFunction *exc_fn = JS_VALUE_GET_FUNCTION(frame->function);
          fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_fn))->u.native.fn_ptr;
          cell_sync_dl_from_native_fn(st, exc_fn);
          JS_PopGCRef(ctx, &callee_ref);
          continue;
        }
        /* Clear stale exception */
        if (JS_HasException(ctx))
          JS_GetException(ctx);

        if (pending_is_tail) {
          /* Tail call to non-native: return its result up the chain */
          /* Pop current frame and return to caller */
          if (st->aot_depth <= base_depth) {
            cell_rt_leave_frame(ctx);
            JS_PopGCRef(ctx, &callee_ref);
            RETURN_DISPATCH(ret);
          }
          /* Pop current frame, return to caller frame */
          cell_rt_leave_frame(ctx);
          if (st->aot_depth < base_depth) {
            JS_PopGCRef(ctx, &callee_ref);
            RETURN_DISPATCH(ret);
          }
          frame_val = aot_gc_ref_at(st, st->aot_depth - 1)->val;
          frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
          fp = (JSValue *)frame->slots;
          int ret_info = JS_VALUE_GET_INT(frame->address);
          int ret_slot = ret_info & 0xFFFF;
          if (ret_slot != 0xFFFF)
            fp[ret_slot] = ret;
          /* Resume caller */
          JSFunction *caller_fn = JS_VALUE_GET_FUNCTION(frame->function);
          fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(caller_fn))->u.native.fn_ptr;
          cell_sync_dl_from_native_fn(st, caller_fn);
        } else {
          /* Regular call: store result and resume current function */
          int ret_info = JS_VALUE_GET_INT(frame->address);
          int ret_slot = ret_info & 0xFFFF;
          if (ret_slot != 0xFFFF)
            fp[ret_slot] = ret;
          /* fn stays the same — we resume the same function at next segment */
          JSFunction *cur_fn = JS_VALUE_GET_FUNCTION(frame->function);
          fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(cur_fn))->u.native.fn_ptr;
          cell_sync_dl_from_native_fn(st, cur_fn);
        }
      }
      JS_PopGCRef(ctx, &callee_ref);
      continue;
    }

    /* No pending call — function returned a value or exception */
    if (result == JS_EXCEPTION) {
      /* Exception: pop this frame and propagate to caller.
         The caller's generated code has exception checks at resume points. */
      if (!JS_HasException(ctx))
        JS_Disrupt(ctx);

      if (st->aot_depth <= base_depth) {
        cell_rt_leave_frame(ctx);
        RETURN_DISPATCH(JS_EXCEPTION);
      }
      cell_rt_leave_frame(ctx);
      if (st->aot_depth < base_depth) {
        RETURN_DISPATCH(JS_EXCEPTION);
      }

      /* Resume caller and tag the return slot with JS_EXCEPTION. */
      frame_val = aot_gc_ref_at(st, st->aot_depth - 1)->val;
      frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
      fp = (JSValue *)frame->slots;
      int ret_info = JS_VALUE_GET_INT(frame->address);
      int ret_slot = ret_info & 0xFFFF;
      if (ret_slot != 0xFFFF)
        fp[ret_slot] = JS_EXCEPTION;

      JSFunction *exc_caller_fn = JS_VALUE_GET_FUNCTION(frame->function);
      fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(exc_caller_fn))->u.native.fn_ptr;
      cell_sync_dl_from_native_fn(st, exc_caller_fn);
      continue;
    }

    /* Normal return — pop frame and store result in caller */
    if (st->aot_depth <= base_depth) {
      cell_rt_leave_frame(ctx);
      RETURN_DISPATCH(result);
    }
    cell_rt_leave_frame(ctx);
    if (st->aot_depth < base_depth) {
      RETURN_DISPATCH(result);
    }

    /* Return to caller frame */
    frame_val = aot_gc_ref_at(st, st->aot_depth - 1)->val;
    frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
    fp = (JSValue *)frame->slots;
    int ret_info = JS_VALUE_GET_INT(frame->address);
    int ret_slot = ret_info & 0xFFFF;
    if (ret_slot != 0xFFFF)
      fp[ret_slot] = result;

    JSFunction *caller_fn = JS_VALUE_GET_FUNCTION(frame->function);
    fn = (cell_compiled_fn)JS_VALUE_GET_CODE(FN_READ_CODE(caller_fn))->u.native.fn_ptr;
    cell_sync_dl_from_native_fn(st, caller_fn);
    continue;
  }

#undef RETURN_DISPATCH
}

static JSValue aot_get_or_create_native_code(JSContext *ctx, NativeRTState *st,
                                             void *dl_handle, int64_t fn_idx,
                                             int arity, uint16_t nr_slots) {
  for (int i = 0; i < st->code_cache_count; i++) {
    AOTCodeCacheEntry *e = &st->code_cache[i];
    if (e->dl_handle == dl_handle && e->fn_idx == fn_idx)
      return e->code;
  }

  char name[64];
  snprintf(name, sizeof(name), "cell_fn_%lld", (long long)fn_idx);
  void *fn_ptr = dlsym(dl_handle, name);
  if (!fn_ptr)
    return JS_RaiseDisrupt(ctx, "native function %s not found in dylib", name);

  JSCode *code = ct_alloc(ctx, sizeof(JSCode), 8);
  if (!code)
    return JS_EXCEPTION;
  memset(code, 0, sizeof(*code));
  code->header = objhdr_make(0, OBJ_CODE, 0, 0, 0, 0);
  code->kind = JS_CODE_KIND_NATIVE;
  code->arity = (int16_t)arity;
  code->u.native.fn_ptr = fn_ptr;
  code->u.native.dl_handle = dl_handle;
  code->u.native.nr_slots = nr_slots;
  JSValue code_obj = JS_MKPTR(code);

  if (st->code_cache_count >= st->code_cache_cap) {
    int new_cap = st->code_cache_cap ? (st->code_cache_cap * 2) : 128;
    AOTCodeCacheEntry *new_cache =
        (AOTCodeCacheEntry *)realloc(st->code_cache, (size_t)new_cap * sizeof(*new_cache));
    if (!new_cache)
      return JS_RaiseOOM(ctx);
    st->code_cache = new_cache;
    st->code_cache_cap = new_cap;
  }

  st->code_cache[st->code_cache_count].dl_handle = dl_handle;
  st->code_cache[st->code_cache_count].fn_idx = fn_idx;
  st->code_cache[st->code_cache_count].code = code_obj;
  st->code_cache_count++;
  return code_obj;
}

/* Create a native function object from a compiled fn_idx.
   Called from QBE-generated code during function creation. */
JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx, void *outer_fp,
                              int64_t nr_args, int64_t nr_slots) {
  NativeRTState *st = native_state(ctx);
  if (!st) return JS_EXCEPTION;
  if (!st->current_dl_handle)
    return JS_RaiseDisrupt(ctx, "no native module loaded");

  JSValue code_obj = aot_get_or_create_native_code(
      ctx, st, st->current_dl_handle, fn_idx, (int)nr_args, (uint16_t)nr_slots);
  if (JS_IsException(code_obj))
    return JS_EXCEPTION;

  /* Get the current frame as outer_frame for closures */
  JSValue outer_frame = JS_NULL;
  if (st->aot_depth > 0)
    outer_frame = aot_gc_ref_at(st, st->aot_depth - 1)->val;

  /* Read env from the parent frame's function object */
  JSValue env = JS_NULL;
  if (outer_fp) {
    JSFrameRegister *parent = (JSFrameRegister *)((char *)outer_fp - offsetof(JSFrameRegister, slots));
    if (JS_IsFunction(parent->function)) {
      JSFunction *parent_fn = JS_VALUE_GET_FUNCTION(parent->function);
      if (parent_fn->kind == JS_FUNC_KIND_NATIVE)
        env = parent_fn->u.cell.env_record;
    }
  }

  return js_new_native_function_with_code(ctx, code_obj, (int)nr_args, outer_frame, env);
}

/* --- Frame-based function calling ---
   Still used by QBE-generated code for building call frames
   before signaling the dispatch loop. */

JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int64_t nargs) {
  if (!JS_IsFunction(fn)) {
    JS_RaiseDisrupt(ctx, "not a function");
    return JS_EXCEPTION;
  }
  int nr_slots = (int)nargs + 2;
  JSFunction *f = JS_VALUE_GET_FUNCTION(fn);
  if (f->kind == JS_FUNC_KIND_NATIVE && JS_VALUE_GET_CODE(FN_READ_CODE(f))->u.native.nr_slots > nr_slots)
    nr_slots = JS_VALUE_GET_CODE(FN_READ_CODE(f))->u.native.nr_slots;
  JSFrameRegister *new_frame = alloc_frame_register(ctx, nr_slots);
  if (!new_frame) return JS_EXCEPTION;
  new_frame->function = fn;
  new_frame->address = JS_NewInt32(ctx, (int)nargs);
  return JS_MKPTR(new_frame);
}

JSValue cell_rt_goframe(JSContext *ctx, JSValue fn, int64_t nargs) {
  return cell_rt_frame(ctx, fn, nargs);
}

/* --- Array push/pop --- */

JSValue cell_rt_push(JSContext *ctx, JSValue arr, JSValue val) {
  JS_ArrayPush(ctx, &arr, val);
  return arr;
}

JSValue cell_rt_pop(JSContext *ctx, JSValue arr) {
  return JS_ArrayPop(ctx, arr);
}

/* --- Delete --- */

JSValue cell_rt_delete(JSContext *ctx, JSValue obj, JSValue key) {
  int ret = JS_DeleteProperty(ctx, obj, key);
  if (ret < 0)
    return JS_EXCEPTION;
  return JS_NewBool(ctx, ret >= 0);
}

static JSValue cell_rt_delete_key(JSContext *ctx, JSValue obj, JSValue key) {
  int ret = JS_DeleteProperty(ctx, obj, key);
  if (ret < 0)
    return JS_EXCEPTION;
  return JS_NewBool(ctx, ret >= 0);
}

JSValue cell_rt_delete_lit(JSContext *ctx, JSValue obj, int64_t lit_idx) {
  JSValue key = aot_lit_from_index(ctx, lit_idx);
  if (JS_IsException(key))
    return JS_EXCEPTION;
  return cell_rt_delete_key(ctx, obj, key);
}

static int cell_rt_tol_eq_inner(JSContext *ctx, JSValue a, JSValue b,
                                JSValue tol) {
  if (JS_IsNumber(a) && JS_IsNumber(b) && JS_IsNumber(tol)) {
    double da, db, dt;
    JS_ToFloat64(ctx, &da, a);
    JS_ToFloat64(ctx, &db, b);
    JS_ToFloat64(ctx, &dt, tol);
    return fabs(da - db) <= dt;
  }
  if (JS_IsText(a) && JS_IsText(b) && JS_IsBool(tol) && JS_VALUE_GET_BOOL(tol)) {
    return js_string_compare_value_nocase(ctx, a, b) == 0;
  }
  /* Fallback to standard equality */
  if (a == b) return 1;
  if (JS_IsText(a) && JS_IsText(b))
    return js_string_compare_value(ctx, a, b, 1) == 0;
  if (JS_IsNumber(a) && JS_IsNumber(b)) {
    double da, db;
    JS_ToFloat64(ctx, &da, a);
    JS_ToFloat64(ctx, &db, b);
    return da == db;
  }
  return 0;
}

JSValue cell_rt_eq_tol(JSContext *ctx, JSValue a, JSValue b, JSValue tol) {
  return JS_NewBool(ctx, cell_rt_tol_eq_inner(ctx, a, b, tol));
}

JSValue cell_rt_ne_tol(JSContext *ctx, JSValue a, JSValue b, JSValue tol) {
  return JS_NewBool(ctx, !cell_rt_tol_eq_inner(ctx, a, b, tol));
}

int cell_rt_is_actor(JSContext *ctx, JSValue v) {
  int result = 0;
  if (mist_is_record(v) && !JS_IsNull(ctx->actor_sym))
    result = JS_HasPropertyKey(ctx, v, ctx->actor_sym) > 0;
  return result;
}

/* --- Exception checking ---
   After potentially-throwing runtime calls, QBE-generated code needs to
   check for pending exceptions and branch to the disruption handler. */

void cell_rt_clear_exception(JSContext *ctx) {
  if (JS_HasException(ctx))
    JS_GetException(ctx);
}

/* --- Disruption --- */

/* Disrupt: silently set exception flag like the bytecode VM does.
   Does NOT call JS_ThrowTypeError — that would print to stderr
   even when a disruption handler will catch it. */
void cell_rt_disrupt(JSContext *ctx) {
  JS_RaiseDisrupt(ctx, "type error in native code");
}

/* --- in: key in obj --- */

JSValue cell_rt_in(JSContext *ctx, JSValue key, JSValue obj) {
  int has = JS_HasProperty(ctx, obj, key);
  return JS_NewBool(ctx, has > 0);
}

/* --- regexp: create regex from pattern and flags --- */

JSValue cell_rt_regexp(JSContext *ctx, const char *pattern, const char *flags) {
  JSValue argv[2];
  argv[0] = JS_NewString(ctx, pattern);
  argv[1] = JS_NewString(ctx, flags);
  JSValue re = js_regexp_constructor(ctx, JS_NULL, 2, argv);
  if (JS_IsException(re))
    return JS_EXCEPTION;
  return re;
}

/* --- Module entry point ---
   Loads a native .cm module from a dylib handle.
   Looks up cell_main, builds a heap-allocated frame, and
   records active module handle in per-actor native state. */

/* Helper: run a native module's entry point through the dispatch loop.
   Creates a temporary JS_FUNC_KIND_NATIVE function so that the full
   dispatch loop (tail calls, closures, etc.) works for module-level code. */
static JSValue native_module_run(JSContext *ctx, void *dl_handle,
                                 cell_compiled_fn entry, int nr_slots,
                                 JSValue env) {
  NativeRTState *st = native_state(ctx);
  if (!st) return JS_EXCEPTION;
  void *prev_handle = st->current_dl_handle;
  st->current_dl_handle = dl_handle;

  /* Create a native function object for the entry point, with env on the function */
  JSValue func_obj = js_new_native_function(ctx, (void *)entry, dl_handle,
                                            (uint16_t)nr_slots, 0, JS_NULL, env);
  if (JS_IsException(func_obj)) {
    st->current_dl_handle = prev_handle;
    return JS_EXCEPTION;
  }

  /* Clear any stale exception left by a previous interpreted run */
  if (JS_HasException(ctx))
    JS_GetException(ctx);

  JSValue result = cell_native_dispatch(ctx, func_obj, JS_NULL, 0, NULL);
  st->current_dl_handle = prev_handle;
  return result;
}

JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle, JSValue env) {
  cell_compiled_fn fn = (cell_compiled_fn)dlsym(dl_handle, "cell_main");
  if (!fn)
    return JS_RaiseDisrupt(ctx, "cell_main not found in native module dylib");

  /* Try to read nr_slots from the module (exported by emitter) */
  int *slots_ptr = (int *)dlsym(dl_handle, "cell_main_nr_slots");
  int nr_slots = slots_ptr ? *slots_ptr : 512;

  return native_module_run(ctx, dl_handle, fn, nr_slots, env);
}

/* Load a native module from a dylib handle, trying a named symbol first.
   Falls back to cell_main if the named symbol is not found. */
JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name, JSValue env) {
  cell_compiled_fn fn = NULL;
  const char *used_name = NULL;
  if (sym_name) {
    fn = (cell_compiled_fn)dlsym(dl_handle, sym_name);
    if (fn) used_name = sym_name;
  }
  if (!fn) {
    fn = (cell_compiled_fn)dlsym(dl_handle, "cell_main");
    used_name = "cell_main";
  }
  if (!fn)
    return JS_RaiseDisrupt(ctx, "symbol not found in native module dylib");

  /* Try to read nr_slots from the module */
  char slots_sym[128];
  snprintf(slots_sym, sizeof(slots_sym), "%s_nr_slots", used_name);
  int *slots_ptr = (int *)dlsym(dl_handle, slots_sym);
  int nr_slots = slots_ptr ? *slots_ptr : 512;

  return native_module_run(ctx, dl_handle, fn, nr_slots, env);
}

void cell_rt_free_native_state(JSContext *ctx) {
  NativeRTState *st = (NativeRTState *)ctx->native_state;
  if (!st) return;

  aot_clear_lit_pool(ctx, st);

  for (int ci = 0; ci < st->gc_ref_chunk_count; ci++) {
    AOTGCRefChunk *chunk = st->gc_ref_chunks[ci];
    if (!chunk) continue;
    for (int si = 0; si < AOT_GC_REF_CHUNK_SIZE; si++) {
      if (chunk->inited[si]) {
        JS_DeleteGCRef(ctx, &chunk->refs[si]);
        chunk->inited[si] = 0;
        chunk->refs[si].val = JS_NULL;
      }
    }
    free(chunk);
  }

  free(st->gc_ref_chunks);
  free(st->key_cache);
  free(st->code_cache);
  js_free_rt(st);
  ctx->native_state = NULL;
}

/* Backward-compat: uses RTLD_DEFAULT (works when dylib opened with RTLD_GLOBAL) */
JSValue cell_rt_module_entry(JSContext *ctx) {
  void *handle = dlopen(NULL, RTLD_LAZY);
  return cell_rt_native_module_load(ctx, handle, JS_NULL);
}
