/*
 * 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.
 */

#define BLOB_IMPLEMENTATION
#define NOTA_IMPLEMENTATION
#define WOTA_IMPLEMENTATION
#include "quickjs-internal.h"

// #define DUMP_BUDDY
#ifdef DUMP_BUDDY
#include "buddy_debug.c"
#endif

#ifdef HEAP_CHECK
void heap_check_fail(void *ptr, JSContext *ctx) {
  uint8_t *p = (uint8_t *)ptr;
  fprintf(stderr, "\n=== HEAP_CHECK: invalid heap pointer ===\n");
  fprintf(stderr, "  pointer:   %p\n", ptr);
  fprintf(stderr, "  heap:      [%p, %p)\n",
          (void *)ctx->heap_base, (void *)ctx->heap_free);
  fprintf(stderr, "  ct_pool:   [%p, %p)\n",
          (void *)ctx->ct_base, (void *)ctx->ct_end);
  if (!JS_IsNull(ctx->reg_current_frame)) {
    fprintf(stderr, "  stack trace:\n");
    JSFrame *frame = (JSFrame *)JS_VALUE_GET_PTR(ctx->reg_current_frame);
    uint32_t pc = ctx->current_register_pc;
    int first = 1;
    while (frame) {
      objhdr_t hdr = *(objhdr_t *)JS_VALUE_GET_PTR(frame->function);
      if (objhdr_type(hdr) != OBJ_FUNCTION) break;
      JSFunction *fn = (JSFunction *)JS_VALUE_GET_PTR(frame->function);
      const char *name = NULL, *file = NULL;
      uint16_t line = 0;
      if (fn->kind == JS_FUNC_KIND_REGISTER && fn->u.reg.code) {
        JSCodeRegister *code = fn->u.reg.code;
        file = code->filename_cstr;
        name = code->name_cstr;
        if (!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",
              name ? name : "<anonymous>",
              file ? file : "<unknown>", line);
      if (JS_IsNull(frame->caller)) break;
      frame = (JSFrame *)JS_VALUE_GET_PTR(frame->caller);
      first = 0;
      pc = 0;
    }
  }
  fprintf(stderr, "=======================================\n");
  fflush(stderr);
  abort();
}
#endif

static inline JS_BOOL JS_IsInteger (JSValue v) {
  if (JS_VALUE_GET_TAG(v) == JS_TAG_INT) return true;
  if (JS_VALUE_GET_TAG(v) != JS_TAG_SHORT_FLOAT) return false;

  double d = JS_VALUE_GET_FLOAT64(v);
  return d == (double)(int64_t)d;
}

JSClassID js_class_id_alloc = JS_CLASS_INIT_COUNT;

/* === Public API wrappers (non-inline, for quickjs.h declarations) ===
   These delegate to the static inline versions in quickjs-internal.h. */

JS_BOOL JS_IsStone(JSValue v) { return mist_is_stone(v); }
JS_BOOL JS_IsArray(JSValue v) { return mist_is_array(v); }
JS_BOOL JS_IsRecord(JSValue v) { return mist_is_record(v); }
JS_BOOL JS_IsFunction(JSValue v) { return mist_is_function(v); }
JS_BOOL JS_IsBlob(JSValue v) { return mist_is_blob(v); }
JS_BOOL JS_IsText(JSValue v) { return mist_is_text(v); }

JS_BOOL JS_IsCode(JSValue v) {
  return mist_is_gc_object(v) && objhdr_type(*chase(v)) == OBJ_CODE;
}

JS_BOOL JS_IsForwarded(JSValue v) {
  return mist_is_gc_object(v) && objhdr_type(*chase(v)) == OBJ_FORWARD;
}

JS_BOOL JS_IsFrame(JSValue v) {
  return mist_is_gc_object(v) && objhdr_type(*chase(v)) == OBJ_FRAME;
}

uint64_t get_text_hash (JSText *text) {
  uint64_t len = objhdr_cap56 (text->hdr);
  size_t word_count = (len + 1) / 2;

  if (objhdr_s (text->hdr)) {
    /* Stoned text: check for cached hash */
    if (text->length != 0) return text->length;
    /* Compute and cache hash using content length from header */
    text->length = fash64_hash_words (text->packed, word_count, len);
    if (!text->length) text->length = 1;
    return text->length;
  } else {
    /* Pre-text: compute hash on the fly */
    return fash64_hash_words (text->packed, word_count, len);
  }
}

/* Pack UTF-32 characters into 64-bit words (2 chars per word) */
void pack_utf32_to_words (const uint32_t *utf32, uint32_t len, uint64_t *packed) {
  for (uint32_t i = 0; i < len; i += 2) {
    uint64_t hi = utf32[i];
    uint64_t lo = (i + 1 < len) ? utf32[i + 1] : 0;
    packed[i / 2] = (hi << 32) | lo;
  }
}

/* Compare two packed UTF-32 texts for equality */
int text_equal (JSText *a, const uint64_t *packed_b, uint32_t len_b) {
  uint32_t len_a = (uint32_t)objhdr_cap56 (a->hdr);
  if (len_a != len_b) return 0;
  size_t word_count = (len_a + 1) / 2;
  return memcmp (a->packed, packed_b, word_count * sizeof (uint64_t)) == 0;
}

int JS_IsPretext (JSValue v) {
  if (!JS_IsText (v)) return 0;
  JSText *text = (JSText *)JS_VALUE_GET_PTR (v);
  return !objhdr_s (text->hdr);
}

JSValue *JS_PushGCRef (JSContext *ctx, JSGCRef *ref) {
  ref->prev = ctx->top_gc_ref;
  ctx->top_gc_ref = ref;
  ref->val = JS_NULL;
  return &ref->val;
}

JSValue JS_PopGCRef (JSContext *ctx, JSGCRef *ref) {
  ctx->top_gc_ref = ref->prev;
  return ref->val;
}

JSValue *JS_AddGCRef (JSContext *ctx, JSGCRef *ref) {
  ref->prev = ctx->last_gc_ref;
  ctx->last_gc_ref = ref;
  ref->val = JS_NULL;
  return &ref->val;
}

void JS_DeleteGCRef (JSContext *ctx, JSGCRef *ref) {
  JSGCRef **pref, *ref1;
  pref = &ctx->last_gc_ref;
  for (;;) {
    ref1 = *pref;
    if (ref1 == NULL)
      abort ();
    if (ref1 == ref) {
      *pref = ref1->prev;
      break;
    }
    pref = &ref1->prev;
  }
}

/* JS_FRAME/JS_ROOT/JS_LOCAL helper functions */
JSGCRef *JS_GetGCFrame (JSContext *ctx) {
  return ctx->top_gc_ref;
}

JSLocalRef *JS_GetLocalFrame (JSContext *ctx) {
  return ctx->top_local_ref;
}

void JS_PushLocalRef (JSContext *ctx, JSLocalRef *ref) {
  ref->prev = ctx->top_local_ref;
  ctx->top_local_ref = ref;
}

void JS_RestoreFrame (JSContext *ctx, JSGCRef *gc_frame, JSLocalRef *local_frame) {
  ctx->top_gc_ref = gc_frame;
  ctx->top_local_ref = local_frame;
}

void *ct_alloc (JSContext *ctx, size_t bytes, size_t align) {
  /* Align the request */
  bytes = (bytes + align - 1) & ~(align - 1);

  /* Check if we have space in the constant text pool */
  if (ctx->ct_base && ctx->ct_free + bytes <= ctx->ct_end) {
    void *ptr = ctx->ct_free;
    ctx->ct_free += bytes;
    return ptr;
  }

  /* No pool space - allocate a page */
  size_t page_size = sizeof (CTPage) + bytes;
  CTPage *page = malloc (page_size);
  if (!page) return NULL;

  page->next = ctx->ct_pages;
  page->size = bytes;
  ctx->ct_pages = page;

  return page->data;
}

/* Free all constant text pool pages */
void ct_free_all (JSContext *ctx) {
  CTPage *page = ctx->ct_pages;
  while (page) {
    CTPage *next = page->next;
    free (page);
    page = next;
  }
  ctx->ct_pages = NULL;
}

int ct_resize (JSContext *ctx) {
  uint32_t new_size, new_threshold;
  uint32_t *new_hash;
  JSText **new_array;

  if (ctx->ct_size == 0) {
    /* Initial allocation */
    new_size = CT_INITIAL_SIZE;
  } else {
    /* Double the size */
    new_size = ctx->ct_size * 2;
  }
  new_threshold = new_size * 3 / 4; /* 75% load factor */

  /* Allocate new hash table (use runtime malloc, not bump allocator) */
  new_hash = js_malloc_rt (new_size * sizeof (uint32_t));
  if (!new_hash) return -1;
  memset (new_hash, 0, new_size * sizeof (uint32_t));

  /* Allocate new text array (one extra for 1-based indexing) */
  new_array = js_malloc_rt ((new_size + 1) * sizeof (JSText *));
  if (!new_array) {
    js_free_rt(new_hash);
    return -1;
  }
  memset (new_array, 0, (new_size + 1) * sizeof (JSText *));

  /* Rehash existing entries */
  if (ctx->ct_count > 0) {
    uint32_t mask = new_size - 1;
    for (uint32_t id = 1; id <= ctx->ct_count; id++) {
      JSText *text = ctx->ct_array[id];
      new_array[id] = text;

      /* Compute hash and find slot */
      uint64_t hash = get_text_hash (text);
      uint32_t slot = hash & mask;
      while (new_hash[slot] != 0)
        slot = (slot + 1) & mask;
      new_hash[slot] = id;
    }
  }

  /* Free old tables */
  if (ctx->ct_hash) js_free_rt (ctx->ct_hash);
  if (ctx->ct_array) js_free_rt (ctx->ct_array);

  ctx->ct_hash = new_hash;
  ctx->ct_array = new_array;
  ctx->ct_size = new_size;
  ctx->ct_resize_threshold = new_threshold;

  return 0;
}

void *js_realloc (JSContext *ctx, void *ptr, size_t size) {
  void *new_ptr;

  /* Align size to 8 bytes */
  size = (size + 7) & ~7;

  if (!ptr) {
    /* New allocation */
    new_ptr = js_malloc (ctx, size);
    if (!new_ptr) return NULL;
    return new_ptr;
  }

  /* Bump allocator: just allocate new space.
     Caller is responsible for protecting ptr and copying data. */
  new_ptr = js_malloc (ctx, size);
  return new_ptr;
}

JSValue intern_text_to_value (JSContext *ctx, const uint32_t *utf32, uint32_t len) {
  /* Pack UTF-32 for hashing and comparison */
  size_t word_count = (len + 1) / 2;
  uint64_t *packed = alloca (word_count * sizeof (uint64_t));
  pack_utf32_to_words (utf32, len, packed);

  uint64_t hash = fash64_hash_words (packed, word_count, len);

  /* Look up in hash table */
  uint32_t mask = ctx->ct_size - 1;
  uint32_t slot = hash & mask;

  while (ctx->ct_hash[slot] != 0) {
    uint32_t id = ctx->ct_hash[slot];
    JSText *existing = ctx->ct_array[id];
    if (text_equal (existing, packed, len)) {
      /* Found existing entry */
      return JS_MKPTR (existing);
    }
    slot = (slot + 1) & mask;
  }

  /* Not found - create new entry */
  if (ctx->ct_count >= ctx->ct_resize_threshold) {
    if (ct_resize (ctx) < 0) return JS_NULL; /* OOM */
    /* Recompute slot after resize */
    mask = ctx->ct_size - 1;
    slot = hash & mask;
    while (ctx->ct_hash[slot] != 0)
      slot = (slot + 1) & mask;
  }

  /* Allocate JSText in constant text pool */
  size_t text_size = sizeof (JSText) + word_count * sizeof (uint64_t);
  JSText *text = ct_alloc (ctx, text_size, 8);
  if (!text) return JS_NULL; /* OOM */

  /* Initialize the text */
  text->hdr = objhdr_make (len, OBJ_TEXT, false, false, false, true); /* s=1 for stoned */
  text->length = hash;                                                /* Store hash in length field for stoned text */
  memcpy (text->packed, packed, word_count * sizeof (uint64_t));

  /* Add to intern table */
  uint32_t new_id = ++ctx->ct_count;
  ctx->ct_hash[slot] = new_id;
  ctx->ct_array[new_id] = text;

  return JS_MKPTR (text);
}

JSValue js_key_new (JSContext *ctx, const char *str) {
  size_t len = strlen (str);

  /* Try immediate ASCII first (≤7 ASCII chars) */
  if (len <= MIST_ASCII_MAX_LEN) {
    JSValue imm = MIST_TryNewImmediateASCII (str, len);
    if (!JS_IsNull (imm)) return imm;
  }

  /* Check length limit */
  if (len > JS_KEY_MAX_LEN) return JS_NULL;

  /* Convert UTF-8 to UTF-32 for interning - use stack buffer */
  const uint8_t *p = (const uint8_t *)str;
  const uint8_t *p_end = p + len;
  uint32_t utf32_buf[JS_KEY_MAX_LEN];
  uint32_t utf32_len = 0;

  while (p < p_end && utf32_len < JS_KEY_MAX_LEN) {
    uint32_t c;
    const uint8_t *p_next;
    if (*p < 0x80) {
      c = *p++;
    } else {
      c = unicode_from_utf8 (p, p_end - p, &p_next);
      if (c == (uint32_t)-1) {
        c = 0xFFFD; /* replacement char for invalid UTF-8 */
        p++;
      } else {
        p = p_next;
      }
    }
    utf32_buf[utf32_len++] = c;
  }

  return intern_text_to_value (ctx, utf32_buf, utf32_len);
}

/* JS_NewAtomString - creates JSValue string from C string (for compatibility)
 */
JSValue JS_NewAtomString (JSContext *ctx, const char *str) {
  return js_key_new (ctx, str);
}

JSValue js_key_new_len (JSContext *ctx, const char *str, size_t len) {
  /* Try immediate ASCII first (≤7 ASCII chars) */
  if (len <= MIST_ASCII_MAX_LEN) {
    JSValue imm = MIST_TryNewImmediateASCII (str, len);
    if (!JS_IsNull (imm)) return imm;
  }

  /* Check length limit */
  if (len > JS_KEY_MAX_LEN) return JS_NULL;

  /* Convert UTF-8 to UTF-32 for interning */
  const uint8_t *p = (const uint8_t *)str;
  const uint8_t *p_end = p + len;
  uint32_t utf32_buf[JS_KEY_MAX_LEN];
  uint32_t utf32_len = 0;

  while (p < p_end && utf32_len < JS_KEY_MAX_LEN) {
    uint32_t c;
    const uint8_t *p_next;
    if (*p < 0x80) {
      c = *p++;
    } else {
      c = unicode_from_utf8 (p, p_end - p, &p_next);
      if (c == (uint32_t)-1) {
        c = 0xFFFD;
        p++;
      } else {
        p = p_next;
      }
    }
    utf32_buf[utf32_len++] = c;
  }

  return intern_text_to_value (ctx, utf32_buf, utf32_len);
}

uint64_t js_key_hash (JSValue key) {
  if (MIST_IsImmediateASCII (key)) {
    /* Hash immediate ASCII the same way as heap strings for consistency */
    int len = MIST_GetImmediateASCIILen (key);
    if (len == 0) return 1;
    size_t word_count = (len + 1) / 2;
    uint64_t packed[4]; /* Max 7 chars = 4 words */
    for (size_t i = 0; i < word_count; i++) {
      uint32_t c0 = (i * 2 < (size_t)len) ? MIST_GetImmediateASCIIChar (key, i * 2) : 0;
      uint32_t c1 = (i * 2 + 1 < (size_t)len) ? MIST_GetImmediateASCIIChar (key, i * 2 + 1) : 0;
      packed[i] = ((uint64_t)c0 << 32) | c1;
    }
    uint64_t h = fash64_hash_words (packed, word_count, len);
    return h ? h : 1;
  }

  if (!JS_IsPtr (key)) return 0;

  /* Use chase to follow forwarding pointers */
  void *ptr = chase (key);
  objhdr_t hdr = *(objhdr_t *)ptr;
  uint8_t type = objhdr_type (hdr);

  if (type == OBJ_TEXT) {
    /* For JSText (stoned strings), use get_text_hash */
    JSText *text = (JSText *)ptr;
    return get_text_hash (text);
  }
  if (type == OBJ_RECORD) {
    JSRecord *rec = (JSRecord *)ptr;
    uint32_t rec_id = REC_GET_REC_ID(rec);
    if (rec_id == 0) return 0;
    return fash64_hash_one (rec_id);
  }

  return 0;
}

JS_BOOL js_key_equal (JSValue a, JSValue b) {
  if (a == b) return TRUE;

  /* Use chase to follow forwarding pointers */
  if (MIST_IsImmediateASCII (a)) {
    if (MIST_IsImmediateASCII (b)) return FALSE;
    if (!JS_IsPtr (b)) return FALSE;
    JSText *tb = (JSText *)chase (b);
    if (objhdr_type (tb->hdr) != OBJ_TEXT) return FALSE;
    return JSText_equal_ascii (tb, a);
  }
  if (MIST_IsImmediateASCII (b)) {
    if (!JS_IsPtr (a)) return FALSE;
    JSText *ta = (JSText *)chase (a);
    if (objhdr_type (ta->hdr) != OBJ_TEXT) return FALSE;
    return JSText_equal_ascii (ta, b);
  }

  if (!JS_IsPtr (a) || !JS_IsPtr (b)) return FALSE;

  void *pa = chase (a);
  void *pb = chase (b);
  objhdr_t ha = *(objhdr_t *)pa;
  objhdr_t hb = *(objhdr_t *)pb;
  uint8_t type_a = objhdr_type (ha);
  uint8_t type_b = objhdr_type (hb);

  if (type_a != type_b) return FALSE;
  if (type_a == OBJ_RECORD) return FALSE; /* pointer equality handled above */
  if (type_a == OBJ_TEXT)
    return JSText_equal ((JSText *)pa, (JSText *)pb);

  return FALSE;
}

JS_BOOL js_key_equal_str (JSValue a, const char *str) {
  size_t len = strlen (str);

  if (MIST_IsImmediateASCII (a)) {
    int imm_len = MIST_GetImmediateASCIILen (a);
    if ((size_t)imm_len != len) return FALSE;
    for (int i = 0; i < imm_len; i++) {
      if (MIST_GetImmediateASCIIChar (a, i) != str[i]) return FALSE;
    }
    return TRUE;
  }

  if (!JS_IsPtr (a)) return FALSE;
  JSText *ta = (JSText *)chase (a);
  if (objhdr_type (ta->hdr) != OBJ_TEXT) return FALSE;
  uint64_t txt_len = objhdr_cap56 (ta->hdr);
  if (txt_len != len) return FALSE;

  /* Compare character by character (UTF-32 vs ASCII) */
  for (size_t i = 0; i < len; i++) {
    if (string_get (ta, i) != (uint32_t)(unsigned char)str[i])
      return FALSE;
  }
  return TRUE;
}

int rec_find_slot (JSRecord *rec, JSValue k) {
  uint64_t mask = objhdr_cap56 (rec->mist_hdr);
  uint64_t h64 = js_key_hash (k);
  uint64_t slot = (h64 & mask);
  if (slot == 0) slot = 1; /* slot 0 is reserved */

  uint64_t first_tomb = 0;

  for (uint64_t i = 0; i <= mask; i++) {
    JSValue slot_key = rec->slots[slot].key;

    if (rec_key_is_empty (slot_key)) {
      /* Empty slot - key not found */
      return first_tomb ? -(int)first_tomb : -(int)slot;
    }
    if (rec_key_is_tomb (slot_key)) {
      /* Tombstone - remember for insertion but keep searching */
      if (first_tomb == 0) first_tomb = slot;
    } else if (js_key_equal (slot_key, k)) {
      /* Found it */
      return (int)slot;
    }

    slot = (slot + 1) & mask;
    if (slot == 0) slot = 1;
  }

  /* Table full (shouldn't happen with proper load factor) */
  return first_tomb ? -(int)first_tomb : -1;
}

JSValue rec_get (JSContext *ctx, JSRecord *rec, JSValue k) {
  if (rec_key_is_empty (k) || rec_key_is_tomb (k)) return JS_NULL;

  /* Walk prototype chain */
  JSRecord *p = rec;
  while (p) {
    int slot = rec_find_slot (p, k);
    if (slot > 0) { return p->slots[slot].val; }
    p = JS_IsNull (p->proto) ? NULL : JS_VALUE_GET_RECORD (p->proto);
  }
  return JS_NULL;
}

int rec_resize (JSContext *ctx, JSValue *pobj, uint64_t new_mask) {
  /* Protect the source object with a GC ref in case js_malloc triggers GC */
  JSGCRef obj_ref;
  JS_AddGCRef (ctx, &obj_ref);
  obj_ref.val = *pobj;

  JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (*pobj);
  uint64_t old_mask = objhdr_cap56 (rec->mist_hdr);

  /* Allocate new record with larger capacity - may trigger GC! */
  size_t slots_size = sizeof (slot) * (new_mask + 1);
  size_t total_size = sizeof (JSRecord) + slots_size;
  if (total_size >= 100000) {
    fprintf(stderr, "LARGE_REC_RESIZE: new_mask=%llu total=%zu old_mask=%llu\n",
            (unsigned long long)new_mask, total_size, (unsigned long long)old_mask);
  }

  JSRecord *new_rec = js_malloc (ctx, total_size);
  if (!new_rec) {
    JS_DeleteGCRef (ctx, &obj_ref);
    return -1;
  }

  /* Re-get record from GC ref - it may have moved during GC */
  rec = (JSRecord *)JS_VALUE_GET_OBJ (obj_ref.val);
  old_mask = objhdr_cap56 (rec->mist_hdr);

  /* Initialize new record */
  new_rec->mist_hdr = objhdr_make (new_mask, OBJ_RECORD, false, false, false, false);
  new_rec->proto = rec->proto;  /* JSValue — copies directly */
  new_rec->len = 0;

  /* Initialize all slots to empty */
  for (uint64_t i = 0; i <= new_mask; i++) {
    new_rec->slots[i].key = JS_NULL;
    new_rec->slots[i].val = JS_NULL;
  }

  /* Copy slot[0] (class_id, rec_id, opaque) */
  new_rec->slots[0].key = rec->slots[0].key;
  new_rec->slots[0].val = rec->slots[0].val;

  /* Rehash all valid entries from old to new */
  for (uint64_t i = 1; i <= old_mask; i++) {
    JSValue k = rec->slots[i].key;
    if (!rec_key_is_empty (k) && !rec_key_is_tomb (k)) {
      /* Insert into new record using linear probing */
      uint64_t h64 = js_key_hash (k);
      uint64_t slot = (h64 & new_mask);
      if (slot == 0) slot = 1;

      while (!rec_key_is_empty (new_rec->slots[slot].key)) {
        slot = (slot + 1) & new_mask;
        if (slot == 0) slot = 1;
      }

      new_rec->slots[slot].key = k;
      new_rec->slots[slot].val = rec->slots[i].val;
      new_rec->len++;
    }
  }

  /* Install forward header at old location so stale references can find the new record */
  rec->mist_hdr = objhdr_make_fwd (new_rec);

  /* Update caller's JSValue to point to new record */
  *pobj = JS_MKPTR (new_rec);

  JS_DeleteGCRef (ctx, &obj_ref);
  return 0;
}

int rec_set_own (JSContext *ctx, JSValue *pobj, JSValue k, JSValue val) {
  if (rec_key_is_empty (k) || rec_key_is_tomb (k)) {
    return -1;
  }

  JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (*pobj);
  int slot = rec_find_slot (rec, k);

  if (slot > 0) {
    /* Existing key - replace value */
    rec->slots[slot].val = val;
    return 0;
  }

  /* New key - check if resize needed (75% load factor) */
  uint32_t mask = (uint32_t)objhdr_cap56 (rec->mist_hdr);
  if ((rec->len + 1) * 4 > mask * 3) {
    /* Over 75% load factor - resize. Protect key and value in case GC runs. */
    JSGCRef k_ref, val_ref;
    JS_AddGCRef (ctx, &k_ref);
    JS_AddGCRef (ctx, &val_ref);
    k_ref.val = k;
    val_ref.val = val;

    uint32_t new_mask = (mask + 1) * 2 - 1;
    if (rec_resize (ctx, pobj, new_mask) < 0) {
      JS_DeleteGCRef (ctx, &val_ref);
      JS_DeleteGCRef (ctx, &k_ref);
      return -1;
    }

    /* Re-get values after resize (they may have moved during GC) */
    k = k_ref.val;
    val = val_ref.val;
    JS_DeleteGCRef (ctx, &val_ref);
    JS_DeleteGCRef (ctx, &k_ref);

    /* Re-get rec after resize (pobj now points to new record) */
    rec = (JSRecord *)JS_VALUE_GET_OBJ (*pobj);
    /* Re-find slot after resize */
    slot = rec_find_slot (rec, k);
  }

  /* Insert at -slot */
  int insert_slot = -slot;
  rec->slots[insert_slot].key = k;
  rec->slots[insert_slot].val = val;
  rec->len++;

#ifdef VALIDATE_GC
  /* Verify the just-inserted key is findable */
  int verify = rec_find_slot (rec, k);
  if (verify <= 0) {
    fprintf (stderr, "rec_set_own: INSERTED KEY NOT FINDABLE! slot=%d insert_slot=%d len=%u\n",
            verify, insert_slot, (uint32_t)rec->len);
    abort ();
  }
#endif

  return 0;
}

JSRecord *js_new_record_class (JSContext *ctx, uint32_t initial_mask, JSClassID class_id) {
  if (initial_mask == 0) initial_mask = JS_RECORD_INITIAL_MASK;

  /* Allocate record + inline slots in one bump allocation */
  size_t slots_size = sizeof (slot) * (initial_mask + 1);
  size_t total_size = sizeof (JSRecord) + slots_size;

  JSRecord *rec = js_malloc (ctx, total_size);
  if (!rec) return NULL;

  rec->mist_hdr = objhdr_make (initial_mask, OBJ_RECORD, false, false, false, false);
  rec->proto = JS_NULL;
  rec->len = 0;

  /* Initialize all slots to empty (JS_NULL) */
  for (uint32_t i = 0; i <= initial_mask; i++) {
    rec->slots[i].key = JS_NULL;
    rec->slots[i].val = JS_NULL;
  }

  /* slots[0] is reserved: store class_id (low 32) and rec_id (high 32) in key */
  uint32_t rec_id = ++ctx->rec_key_next;
  rec->slots[0].key = (JSValue)class_id | ((JSValue)rec_id << 32);
  rec->slots[0].val = 0; /* opaque pointer, initially NULL */

  return rec;
}

int JS_SetPropertyInternal (JSContext *ctx, JSValue this_obj, JSValue prop, JSValue val) {
  if (!JS_IsRecord (this_obj)) {
    return -1;
  }
  /* Use a local copy that rec_set_own can update if resize happens */
  JSValue obj = this_obj;
  return rec_set_own (ctx, &obj, prop, val);
}


void *js_malloc_rt (size_t size) {
  return malloc(size);
}

void js_free_rt (void *ptr) {
  free (ptr);
}

void *js_realloc_rt (void *ptr, size_t size) {
  return realloc (ptr, size);
}

void *js_mallocz_rt (size_t size) {
  void *ptr;
  ptr = js_malloc_rt (size);
  if (!ptr) return NULL;
  return memset (ptr, 0, size);
}

char *js_strdup_rt (const char *str) {
  if (!str) return NULL;
  size_t len = strlen(str) + 1;
  char *dup = js_malloc_rt(len);
  if (dup) memcpy(dup, str, len);
  return dup;
}

/* Throw out of memory in case of error */
void *js_malloc (JSContext *ctx, size_t size) {
  /* Align size to 8 bytes */
  size = (size + 7) & ~7;

#ifdef FORCE_GC_AT_MALLOC
  /* Force GC on every allocation for testing */
  {
    int need_space = (uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end;
    if (ctx_gc(ctx, need_space, size) < 0) {
      JS_ThrowOutOfMemory(ctx);
      return NULL;
    }
    /* If still no room after GC, grow and retry (same logic as normal path) */
    if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
      size_t live = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base);
      size_t need = live + size;
      size_t ns = ctx->current_block_size;
      while (ns < need && ns < buddy_max_block(&ctx->rt->buddy))
        ns *= 2;
      ctx->next_block_size = ns;
      if (ctx_gc (ctx, 1, size) < 0) {
        JS_ThrowOutOfMemory (ctx);
        return NULL;
      }
      ctx->next_block_size = ctx->current_block_size;
      if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
        JS_ThrowOutOfMemory (ctx);
        return NULL;
      }
    }
  }
#else
  /* Check if we have space in current block */
  if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
    /* Trigger GC to reclaim memory */
    if (ctx_gc (ctx, 1, size) < 0) {
      JS_ThrowOutOfMemory (ctx);
      return NULL;
    }
    /* Re-check after GC — if still no room, grow and retry.
       The second GC is cheap: data was just compacted so there is
       almost no garbage to skip. */
    if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
      size_t live = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base);
      size_t need = live + size;
      size_t ns = ctx->current_block_size;
      while (ns < need && ns < buddy_max_block(&ctx->rt->buddy))
        ns *= 2;
#ifdef DUMP_GC
      printf ("    growing %zu -> %zu for %zu byte alloc (live %zu)\n",
              ctx->current_block_size, ns, size, live);
      fflush (stdout);
#endif
      ctx->next_block_size = ns;
      if (ctx_gc (ctx, 1, size) < 0) {
        JS_ThrowOutOfMemory (ctx);
        return NULL;
      }
      /* The retry pass relocates compacted data — 0% recovery is expected.
         Reset next_block_size so the poor-recovery heuristic inside ctx_gc
         doesn't cascade into further unnecessary doubling. */
      ctx->next_block_size = ctx->current_block_size;
      if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
        JS_ThrowOutOfMemory (ctx);
        return NULL;
      }
    }
  }
#endif

  void *ptr = ctx->heap_free;
  ctx->heap_free = (uint8_t *)ctx->heap_free + size;
  return ptr;
}

/* Throw out of memory in case of error */
void *js_mallocz (JSContext *ctx, size_t size) {
  void *ptr = js_malloc (ctx, size);
  if (!ptr) return NULL;
  return memset (ptr, 0, size);
}

void js_free (JSContext *ctx, void *ptr) {
  /* Bump allocator doesn't free individual allocations - GC handles it */
  (void)ctx;
  (void)ptr;
}

/* Parser memory functions - use system allocator to avoid GC issues */


/* Forward declarations for ppretext_end */
JSText *js_alloc_string (JSContext *ctx, int max_len);
static inline void string_put (JSText *p, int idx, uint32_t c);

PPretext *ppretext_init (int capacity) {
  PPretext *p = pjs_malloc (sizeof (PPretext));
  if (!p) return NULL;
  if (capacity <= 0) capacity = 32;
  p->data = pjs_malloc (capacity * sizeof (uint32_t));
  if (!p->data) { pjs_free (p); return NULL; }
  p->len = 0;
  p->cap = capacity;
  return p;
}

void ppretext_free (PPretext *p) {
  if (p) {
    pjs_free (p->data);
    pjs_free (p);
  }
}

no_inline PPretext *ppretext_realloc (PPretext *p, int new_cap) {
  uint32_t *new_data = pjs_realloc (p->data, new_cap * sizeof (uint32_t));
  if (!new_data) return NULL;
  p->data = new_data;
  p->cap = new_cap;
  return p;
}

PPretext *ppretext_putc (PPretext *p, uint32_t c) {
  if (p->len >= p->cap) {
    int new_cap = max_int (p->len + 1, p->cap * 3 / 2);
    if (!ppretext_realloc (p, new_cap)) return NULL;
  }
  p->data[p->len++] = c;
  return p;
}

JSValue ppretext_end (JSContext *ctx, PPretext *p) {
  if (!p) return JS_EXCEPTION;
  int len = p->len;
  if (len == 0) {
    ppretext_free (p);
    return JS_KEY_empty;
  }

  /* Allocate heap string (single allocation) */
  JSText *str = js_alloc_string (ctx, len);
  if (!str) {
    ppretext_free (p);
    return JS_EXCEPTION;
  }
  for (int i = 0; i < len; i++) {
    string_put (str, i, p->data[i]);
  }
  str->hdr = objhdr_set_cap56 (str->hdr, len);
  str->hdr = objhdr_set_s (str->hdr, true);

  ppretext_free (p);
  return JS_MKPTR (str);
}

no_inline int js_realloc_array (JSContext *ctx, void **parray, int elem_size, int *psize, int req_size) {
  int new_size;
  void *new_array;
  void *old_array = *parray;
  int old_size = *psize;

  /* XXX: potential arithmetic overflow */
  new_size = max_int (req_size, old_size * 3 / 2);

  /* Protect source object with a GC ref before allocating (GC may move it) */
  JSGCRef src_ref;
  JS_PushGCRef (ctx, &src_ref);
  if (old_array) {
    src_ref.val = JS_MKPTR (old_array);
  }

  new_array = js_malloc (ctx, new_size * elem_size);
  if (!new_array) {
    JS_PopGCRef (ctx, &src_ref);
    return -1;
  }

  /* Get possibly-moved source pointer after GC */
  if (old_array) {
    old_array = (void *)chase (src_ref.val);
    memcpy (new_array, old_array, old_size * elem_size);
  }
  JS_PopGCRef (ctx, &src_ref);

  *psize = new_size;
  *parray = new_array;
  return 0;
}



/* Append a JSValue string to a PPretext (parser pretext) */
PPretext *ppretext_append_jsvalue (PPretext *p, JSValue str) {
  int len = js_string_value_len (str);
  for (int i = 0; i < len; i++) {
    uint32_t c = js_string_value_get (str, i);
    p = ppretext_putc (p, c);
    if (!p) return NULL;
  }
  return p;
}

/* Append an integer to a PPretext */
PPretext *ppretext_append_int (PPretext *p, int n) {
  char buf[16];
  int len = snprintf (buf, sizeof (buf), "%d", n);
  for (int i = 0; i < len; i++) {
    p = ppretext_putc (p, buf[i]);
    if (!p) return NULL;
  }
  return p;
}

/* Convert a JSValue string to a property key.
   Returns the value as-is for immediates and heap texts.
   No allocation — cannot trigger GC. */
JSValue js_key_from_string (JSContext *ctx, JSValue val) {
  if (MIST_IsImmediateASCII (val)) return val;
  if (JS_IsText (val)) return val;
  return JS_NULL;
}

static JSClass const js_std_class_def[] = {
  { NULL, NULL, NULL },                                               /* placeholder (was JS_CLASS_ERROR, now unused) */
  { "regexp", js_regexp_finalizer, NULL },                            /* JS_CLASS_REGEXP */
  { "blob", NULL, NULL },                                             /* JS_CLASS_BLOB - registered separately */
};

static int init_class_range (JSContext *ctx, JSClass const *tab, int start, int count) {
  JSClassDef cm_s, *cm = &cm_s;
  int i, class_id;

  for (i = 0; i < count; i++) {
    class_id = i + start;
    memset (cm, 0, sizeof (*cm));
    cm->finalizer = tab[i].finalizer;
    if (JS_NewClass1 (ctx, class_id, cm, tab[i].class_name) < 0) return -1;
  }
  return 0;
}


/* Create a new buddy pool of the given size */
static BuddyPool *buddy_pool_new(size_t pool_size) {
  BuddyPool *pool = js_mallocz_rt(sizeof(BuddyPool));
  if (!pool) return NULL;

  pool->base = mmap(NULL, pool_size, PROT_READ | PROT_WRITE,
                    MAP_ANON | MAP_PRIVATE, -1, 0);
  if (pool->base == MAP_FAILED) {
    js_free_rt(pool);
    return NULL;
  }
  pool->total_size = pool_size;

  /* Compute max_order = log2(pool_size) */
  uint8_t order = BUDDY_MIN_ORDER;
  while ((1ULL << order) < pool_size)
    order++;
  pool->max_order = order;
  pool->alloc_count = 0;

  for (int i = 0; i < BUDDY_MAX_LEVELS; i++)
    pool->free_lists[i] = NULL;

  /* One free block spanning the entire pool */
  BuddyBlock *blk = (BuddyBlock *)pool->base;
  blk->order = pool->max_order;
  blk->is_free = 1;
  blk->next = NULL;
  blk->prev = NULL;
  int level = pool->max_order - BUDDY_MIN_ORDER;
  pool->free_lists[level] = blk;

  return pool;
}

/* Try to allocate from a specific pool */
static void *buddy_pool_alloc(BuddyPool *pool, uint8_t order) {
  int level = order - BUDDY_MIN_ORDER;
  int levels = pool->max_order - BUDDY_MIN_ORDER + 1;

  /* Search for a free block at this level or higher */
  int found = -1;
  for (int i = level; i < levels; i++) {
    if (pool->free_lists[i]) {
      found = i;
      break;
    }
  }
  if (found < 0)
    return NULL;

  /* Remove the block from its free list */
  BuddyBlock *blk = pool->free_lists[found];
  pool->free_lists[found] = blk->next;
  if (blk->next)
    blk->next->prev = NULL;

  /* Split down to the target level */
  uint8_t cur_order = BUDDY_MIN_ORDER + found;
  while (cur_order > order) {
    cur_order--;
    int split_level = cur_order - BUDDY_MIN_ORDER;
    /* Right half becomes a free buddy */
    uint8_t *right = (uint8_t *)blk + (1ULL << cur_order);
    BuddyBlock *rblk = (BuddyBlock *)right;
    rblk->order = cur_order;
    rblk->is_free = 1;
    rblk->prev = NULL;
    rblk->next = pool->free_lists[split_level];
    if (rblk->next)
      rblk->next->prev = rblk;
    pool->free_lists[split_level] = rblk;
  }

  pool->alloc_count++;
  return (void *)blk;
}

/* Allocate a block of the given size */
static void *buddy_alloc(BuddyAllocator *b, size_t size) {
  /* Lazy-init: create first pool on demand */
  if (!b->pools) {
    BuddyPool *pool = buddy_pool_new(b->initial_size);
    if (!pool) {
      fprintf(stderr, "buddy_alloc: initial pool mmap failed\n");
      abort();
    }
    pool->next = NULL;
    b->pools = pool;
    b->total_mapped = pool->total_size;
    b->next_pool_size = b->initial_size * 2;
  }

  /* Compute order from size */
  uint8_t order = BUDDY_MIN_ORDER;
  while ((1ULL << order) < size)
    order++;

  /* Walk pools, try to allocate from each */
  for (BuddyPool *pool = b->pools; pool; pool = pool->next) {
    if (order <= pool->max_order) {
      void *ptr = buddy_pool_alloc(pool, order);
      if (ptr) {
#ifdef DUMP_BUDDY
        buddy_dump(pool, "alloc", (uint8_t *)ptr, order);
#endif
        return ptr;
      }
    }
  }

  /* No pool could satisfy — create a new one */
  size_t new_pool_size = b->next_pool_size;
  size_t needed = 1ULL << order;
  if (new_pool_size < needed)
    new_pool_size = needed;
  /* Check cap */
  if (b->cap && b->total_mapped + new_pool_size > b->cap)
    return NULL;

  BuddyPool *pool = buddy_pool_new(new_pool_size);
  if (!pool)
    return NULL;

  /* Prepend to list */
  pool->next = b->pools;
  b->pools = pool;
  b->total_mapped += pool->total_size;
  b->next_pool_size = new_pool_size * 2;

  void *ptr = buddy_pool_alloc(pool, order);
#ifdef DUMP_BUDDY
  if (ptr)
    buddy_dump(pool, "alloc", (uint8_t *)ptr, order);
#endif
  return ptr;
}

/* Free a block and coalesce with its buddy if possible */
static void buddy_free(BuddyAllocator *b, void *ptr, size_t size) {
  uint8_t order = BUDDY_MIN_ORDER;
  while ((1ULL << order) < size)
    order++;

  /* Find the pool containing ptr */
  BuddyPool *pool = NULL;
  BuddyPool **prev_link = &b->pools;
  for (BuddyPool *p = b->pools; p; prev_link = &p->next, p = p->next) {
    if ((uint8_t *)ptr >= p->base &&
        (uint8_t *)ptr < p->base + p->total_size) {
      pool = p;
      break;
    }
  }
  if (!pool) {
    fprintf(stderr, "buddy_free: ptr %p not in any pool\n", ptr);
    abort();
  }

  uint8_t *block = (uint8_t *)ptr;

#ifdef DUMP_BUDDY
  buddy_dump(pool, "free", block, order);
#endif

  /* Coalesce with buddy */
  while (order < pool->max_order) {
    size_t offset = block - pool->base;
    size_t buddy_offset = offset ^ (1ULL << order);
    uint8_t *buddy_addr = pool->base + buddy_offset;

    /* Check if buddy is on the free list at this level */
    int level = order - BUDDY_MIN_ORDER;
    BuddyBlock *buddy = NULL;
    for (BuddyBlock *p = pool->free_lists[level]; p; p = p->next) {
      if ((uint8_t *)p == buddy_addr) {
        buddy = p;
        break;
      }
    }
    if (!buddy)
      break;

    /* Remove buddy from free list */
    if (buddy->prev)
      buddy->prev->next = buddy->next;
    else
      pool->free_lists[level] = buddy->next;
    if (buddy->next)
      buddy->next->prev = buddy->prev;

    /* Merge: take the lower address */
    if (buddy_addr < block)
      block = buddy_addr;
    order++;
  }

  /* Add merged block to free list */
  int level = order - BUDDY_MIN_ORDER;
  BuddyBlock *blk = (BuddyBlock *)block;
  blk->order = order;
  blk->is_free = 1;
  blk->prev = NULL;
  blk->next = pool->free_lists[level];
  if (blk->next)
    blk->next->prev = blk;
  pool->free_lists[level] = blk;

  pool->alloc_count--;

  /* Release empty pools (but keep at least one) */
  if (pool->alloc_count == 0 && b->pools != pool) {
    *prev_link = pool->next;
    b->total_mapped -= pool->total_size;
    munmap(pool->base, pool->total_size);
    js_free_rt(pool);
  } else if (pool->alloc_count == 0 && pool->next) {
    /* pool is the head but not the only one — unlink it */
    b->pools = pool->next;
    b->total_mapped -= pool->total_size;
    munmap(pool->base, pool->total_size);
    js_free_rt(pool);
  }
}

/* Destroy buddy allocator and free all pools */
void buddy_destroy (BuddyAllocator *b) {
  BuddyPool *pool = b->pools;
  while (pool) {
    BuddyPool *next = pool->next;
    munmap(pool->base, pool->total_size);
    js_free_rt(pool);
    pool = next;
  }
  b->pools = NULL;
  b->total_mapped = 0;
}

/* Maximum block size the buddy allocator can hand out */
static size_t buddy_max_block(BuddyAllocator *b) {
  return b->cap ? b->cap : SIZE_MAX;
}

/* ============================================================
   Heap block allocation wrappers
   ============================================================ */

static void *heap_block_alloc(JSRuntime *rt, size_t size) {
  return buddy_alloc(&rt->buddy, size);
}

static void heap_block_free(JSRuntime *rt, void *ptr, size_t size) {
  buddy_free(&rt->buddy, ptr, size);
}

/* ============================================================
   Bump Allocator and Cheney GC
   ============================================================ */

/* Forward declarations for GC helpers */
int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size);
JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end);
void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end);
size_t gc_object_size (void *ptr);

/* Alignment for GC object sizes - must match max alignment requirement */
#define GC_ALIGN 8
static inline size_t gc_align_up (size_t n) { return (n + GC_ALIGN - 1) & ~(GC_ALIGN - 1); }

/* Get size of a heap object based on its type */
size_t gc_object_size (void *ptr) {
  objhdr_t hdr = *(objhdr_t *)ptr;
  uint8_t type = objhdr_type (hdr);
  uint64_t cap = objhdr_cap56 (hdr);

  switch (type) {
  case OBJ_ARRAY: {
    /* JSArray + inline values array. Cap is element capacity. */
    size_t values_size = sizeof (JSValue) * cap;
    return gc_align_up (sizeof (JSArray) + values_size);
  }
  case OBJ_TEXT: {
    /* JSText: header + pad + hdr + length + packed chars */
    size_t word_count = (cap + 1) / 2;
    return gc_align_up (sizeof (JSText) + word_count * sizeof (uint64_t));
  }
  case OBJ_RECORD: {
    /* JSRecord + inline tab. Cap is mask, so tab size is mask+1 entries. */
    size_t tab_size = sizeof (JSRecordEntry) * (cap + 1);
    return gc_align_up (sizeof (JSRecord) + tab_size);
  }
  case OBJ_BLOB: {
    /* JSBlob + inline bit data. cap56 = capacity in bits. */
    size_t word_count = (cap + 63) / 64;
    return gc_align_up (sizeof (JSBlob) + word_count * sizeof (word_t));
  }
  case OBJ_FUNCTION:
    return gc_align_up (sizeof (JSFunction));
  case OBJ_FRAME: {
    /* JSFrame + slots array. cap56 stores slot count */
    uint64_t slot_count = cap;
    return gc_align_up (sizeof (JSFrame) + slot_count * sizeof (JSValue));
  }
  default:
    /* Unknown type - fatal error, heap is corrupt or scan desync'd */
    fflush(stdout);
    fprintf (stderr, "gc_object_size: unknown type %d at %p, hdr=0x%llx cap=%llu\n",
             type, ptr, (unsigned long long)hdr, (unsigned long long)cap);
    /* Dump surrounding memory for debugging */
    uint64_t *words = (uint64_t *)ptr;
    fprintf (stderr, "  words: [0]=0x%llx [1]=0x%llx [2]=0x%llx [3]=0x%llx\n",
             (unsigned long long)words[0], (unsigned long long)words[1],
             (unsigned long long)words[2], (unsigned long long)words[3]);
    fflush(stderr);
    abort ();
  }
}
static inline int ptr_in_range (void *p, uint8_t *b, uint8_t *e) {
  uint8_t *q = (uint8_t *)p;
  return q >= b && q < e;
}

JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) {
  if (!JS_IsPtr (v)) return v;

  for (;;) {
    void *ptr = JS_VALUE_GET_PTR (v);
    if (!ptr_in_range (ptr, from_base, from_end)) return v;

    objhdr_t *hdr_ptr = (objhdr_t *)ptr;
    objhdr_t hdr = *hdr_ptr;
    uint8_t type = objhdr_type (hdr);

    if (type == OBJ_FORWARD) {
      void *t = objhdr_fwd_ptr (hdr);

      /* If it already points into to-space, it's a GC forward. */
      if (ptr_in_range (t, to_base, *to_free)) return JS_MKPTR (t);

      /* Otherwise it's a growth-forward (still in from-space). Chase. */
      v = JS_MKPTR (t);
      continue;
    }

    if (type != OBJ_ARRAY && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_FRAME && type != OBJ_BLOB) {
      fprintf (stderr, "gc_copy_value: invalid type %d at %p (hdr=0x%llx)\n",
               type, ptr, (unsigned long long)hdr);
      fflush (stderr);
      abort ();
    }

    size_t size = gc_object_size (hdr_ptr);
    if (*to_free + size > to_end) {
      fprintf (stderr, "gc_copy_value: out of to-space, need %zu bytes\n", size);
      abort ();
    }

    void *new_ptr = *to_free;
    memcpy (new_ptr, hdr_ptr, size);
    *to_free += size;

    *hdr_ptr = objhdr_make_fwd (new_ptr);

    return JS_MKPTR (new_ptr);
  }
}

/* Recursively scan a code tree's cpools to arbitrary nesting depth */
static void gc_scan_code_tree (JSContext *ctx, JSCodeRegister *code,
                               uint8_t *from_base, uint8_t *from_end,
                               uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) {
  for (uint32_t i = 0; i < code->cpool_count; i++)
    code->cpool[i] = gc_copy_value (ctx, code->cpool[i], from_base, from_end, to_base, to_free, to_end);
  code->name = gc_copy_value (ctx, code->name, from_base, from_end, to_base, to_free, to_end);
  for (uint32_t i = 0; i < code->func_count; i++)
    if (code->functions[i])
      gc_scan_code_tree (ctx, code->functions[i], from_base, from_end, to_base, to_free, to_end);
}

/* Scan a copied object and update its internal references */
void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *from_end,
                            uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) {
  objhdr_t hdr = *(objhdr_t *)ptr;
  uint8_t type = objhdr_type (hdr);

  switch (type) {
  case OBJ_ARRAY: {
    JSArray *arr = (JSArray *)ptr;
    for (uint32_t i = 0; i < arr->len; i++) {
      arr->values[i] = gc_copy_value (ctx, arr->values[i], from_base, from_end, to_base, to_free, to_end);
    }
    break;
  }
  case OBJ_RECORD: {
    JSRecord *rec = (JSRecord *)ptr;
    uint32_t mask = (uint32_t)objhdr_cap56 (rec->mist_hdr);
#ifdef DUMP_GC_DETAIL
    printf("    record: slots=%u used=%u proto=0x%llx\n", mask + 1, (uint32_t)rec->len, (unsigned long long)rec->proto);
    fflush(stdout);
#endif
    /* Copy prototype */
    rec->proto = gc_copy_value (ctx, rec->proto, from_base, from_end, to_base, to_free, to_end);
    /* Copy table entries — skip slot[0] which stores packed metadata
       (class_id | rec_id << 32), not JSValues */
    for (uint32_t i = 1; i <= mask; i++) {
      JSValue k = rec->slots[i].key;
      if (!rec_key_is_empty (k) && !rec_key_is_tomb (k)) {
        rec->slots[i].key = gc_copy_value (ctx, k, from_base, from_end, to_base, to_free, to_end);
        rec->slots[i].val = gc_copy_value (ctx, rec->slots[i].val, from_base, from_end, to_base, to_free, to_end);
      }
    }
    break;
  }
  case OBJ_FUNCTION: {
    JSFunction *fn = (JSFunction *)ptr;
    /* Scan the function name */
    fn->name = gc_copy_value (ctx, fn->name, from_base, from_end, to_base, to_free, to_end);
    if (fn->kind == JS_FUNC_KIND_REGISTER && fn->u.reg.code) {
      /* Scan code tree to arbitrary nesting depth */
      gc_scan_code_tree (ctx, fn->u.reg.code, from_base, from_end, to_base, to_free, to_end);
      /* Scan outer_frame and env_record */
      fn->u.reg.outer_frame = gc_copy_value (ctx, fn->u.reg.outer_frame, from_base, from_end, to_base, to_free, to_end);
      fn->u.reg.env_record = gc_copy_value (ctx, fn->u.reg.env_record, from_base, from_end, to_base, to_free, to_end);
    }
    break;
  }
  case OBJ_TEXT:
  case OBJ_BLOB:
    /* No internal references to scan */
    break;
  case OBJ_FRAME: {
    /* JSFrame - scan function, caller, address, and slots */
    JSFrame *frame = (JSFrame *)ptr;
    frame->function = gc_copy_value (ctx, frame->function, from_base, from_end, to_base, to_free, to_end);
    frame->caller = gc_copy_value (ctx, frame->caller, from_base, from_end, to_base, to_free, to_end);
    frame->address = gc_copy_value (ctx, frame->address, from_base, from_end, to_base, to_free, to_end);
    /* Scan all slots */
    uint64_t slot_count = objhdr_cap56 (frame->header);
    for (uint64_t i = 0; i < slot_count; i++) {
      JSValue sv = frame->slots[i];
#ifdef VALIDATE_GC
      if (JS_IsPtr (sv)) {
        void *sp = JS_VALUE_GET_PTR (sv);
        if (!is_ct_ptr (ctx, sp) && ptr_in_range (sp, from_base, from_end)) {
          objhdr_t sh = *(objhdr_t *)sp;
          uint8_t st = objhdr_type (sh);
          if (st != OBJ_FORWARD && st != OBJ_ARRAY && st != OBJ_TEXT &&
              st != OBJ_RECORD && st != OBJ_FUNCTION && st != OBJ_FRAME && st != OBJ_BLOB) {
            const char *fname = "?";
            const char *ffile = "?";
            uint16_t fnslots = 0;
            if (JS_IsPtr (frame->function)) {
              objhdr_t fh = *(objhdr_t *)JS_VALUE_GET_PTR (frame->function);
              if (objhdr_type (fh) == OBJ_FUNCTION) {
                JSFunction *fn = (JSFunction *)JS_VALUE_GET_PTR (frame->function);
                if (fn->kind == JS_FUNC_KIND_REGISTER && fn->u.reg.code) {
                  if (fn->u.reg.code->name_cstr) fname = fn->u.reg.code->name_cstr;
                  if (fn->u.reg.code->filename_cstr) ffile = fn->u.reg.code->filename_cstr;
                  fnslots = fn->u.reg.code->nr_slots;
                }
              }
            }
            fprintf (stderr, "VALIDATE_GC: frame %p slot[%llu]=%p bad type %d (hdr=0x%llx) fn=%s (%s) nr_slots=%d\n",
                     ptr, (unsigned long long)i, sp, st, (unsigned long long)sh, fname, ffile, fnslots);
            fflush (stderr);
          }
        }
      }
#endif
      frame->slots[i] = gc_copy_value (ctx, sv, from_base, from_end, to_base, to_free, to_end);
    }
    break;
  }
  default:
    /* Unknown type during scan - fatal error */
    fprintf (stderr, "gc_scan_object: unknown object type %d at %p\n", type, ptr);
    abort ();
  }
}

/* Cheney copying GC - collect garbage and compact live objects
   allow_grow: if true, grow heap when recovery is poor
   alloc_size: the allocation that triggered GC — used to size the new block */
int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
#ifdef DUMP_GC_TIMING
  struct timespec gc_t0, gc_t1;
  clock_gettime(CLOCK_MONOTONIC, &gc_t0);
#endif
  JSRuntime *rt = ctx->rt;
  size_t old_used = ctx->heap_free - ctx->heap_base;
  size_t old_heap_size = ctx->current_block_size;

  /* Save OLD heap bounds before allocating new block
     Use heap_free (not heap_end) as from_end - only the portion up to heap_free
     contains valid objects. Beyond heap_free is uninitialized garbage. */
  uint8_t *from_base = ctx->heap_base;
  uint8_t *from_end = ctx->heap_free;

#ifdef DUMP_GC_DETAIL
  printf("ctx_gc: from_base=%p from_end=%p size=%zu\n", (void*)from_base, (void*)from_end, old_heap_size);
#endif

  /* Size the new block.  Start at current_block_size (guaranteed >= used
     portion, so all live data fits).  Only grow when:
       - next_block_size was bumped by the poor-recovery heuristic, or
       - alloc_size alone exceeds the block (rare large allocation).
     Crucially, do NOT add live_est to the sizing — it counts garbage
     and causes exponential heap growth even with excellent recovery. */
  size_t new_size = ctx->current_block_size;
  if (allow_grow) {
    if (ctx->next_block_size > new_size)
      new_size = ctx->next_block_size;
    while (new_size < alloc_size && new_size < buddy_max_block(&ctx->rt->buddy))
      new_size *= 2;
  }
  uint8_t *new_block = heap_block_alloc (rt, new_size);
  if (!new_block) {
    /* Try with same size */
    new_size = ctx->current_block_size;
    new_block = heap_block_alloc (rt, new_size);
    if (!new_block) return -1;
  }

  uint8_t *to_base = new_block;
  uint8_t *to_free = new_block;
  uint8_t *to_end = new_block + new_size;

#ifdef VALIDATE_GC
  /* Pre-GC: walk live frame chain and check for bad slot values */
  if (JS_IsPtr (ctx->reg_current_frame)) {
    JSFrame *cf = (JSFrame *)JS_VALUE_GET_PTR (ctx->reg_current_frame);
    while (cf) {
      objhdr_t cfh = cf->header;
      while (objhdr_type (cfh) == OBJ_FORWARD) {
        cf = (JSFrame *)objhdr_fwd_ptr (cfh);
        cfh = cf->header;
      }
      if (objhdr_type (cfh) != OBJ_FRAME) break;
      uint64_t sc = objhdr_cap56 (cfh);
      for (uint64_t si = 0; si < sc; si++) {
        JSValue sv = cf->slots[si];
        if (JS_IsPtr (sv)) {
          void *sp = JS_VALUE_GET_PTR (sv);
          if (!is_ct_ptr (ctx, sp) && ptr_in_range (sp, from_base, from_end)) {
            objhdr_t th = *(objhdr_t *)sp;
            void *orig_sp = sp;
            while (objhdr_type (th) == OBJ_FORWARD) {
              sp = objhdr_fwd_ptr (th);
              if (!ptr_in_range (sp, from_base, from_end)) break;
              th = *(objhdr_t *)sp;
            }
            uint8_t tt = objhdr_type (th);
            if (tt != OBJ_FORWARD && tt != OBJ_ARRAY && tt != OBJ_TEXT &&
                tt != OBJ_RECORD && tt != OBJ_FUNCTION && tt != OBJ_FRAME && tt != OBJ_BLOB) {
              const char *fn_name = "?";
              JSValue fn_v = cf->function;
              if (JS_IsPtr (fn_v)) {
                objhdr_t fnh = *(objhdr_t *)JS_VALUE_GET_PTR (fn_v);
                while (objhdr_type (fnh) == OBJ_FORWARD) {
                  fn_v = JS_MKPTR (objhdr_fwd_ptr (fnh));
                  fnh = *(objhdr_t *)JS_VALUE_GET_PTR (fn_v);
                }
                if (objhdr_type (fnh) == OBJ_FUNCTION) {
                  JSFunction *fnp = (JSFunction *)JS_VALUE_GET_PTR (fn_v);
                  if (fnp->kind == JS_FUNC_KIND_REGISTER && fnp->u.reg.code && fnp->u.reg.code->name_cstr)
                    fn_name = fnp->u.reg.code->name_cstr;
                }
              }
              fprintf (stderr, "VALIDATE_GC: pre-gc frame %p slot[%llu] -> %p (chased %p) bad type %d (hdr=0x%llx) fn=%s\n",
                       (void *)cf, (unsigned long long)si, orig_sp, sp, tt, (unsigned long long)th, fn_name);
              fflush (stderr);
            }
          }
        }
      }
      if (JS_IsNull (cf->caller)) break;
      cf = (JSFrame *)JS_VALUE_GET_PTR (cf->caller);
    }
  }
#endif

  /* Copy roots: global object, class prototypes, exception, etc. */
#ifdef DUMP_GC_DETAIL
  printf("  roots: global_obj = 0x%llx\n", (unsigned long long)ctx->global_obj); fflush(stdout);
  if (JS_IsPtr(ctx->global_obj)) {
    void *gptr = JS_VALUE_GET_PTR(ctx->global_obj);
    printf("    ptr=%p in_from=%d is_stone=%d\n", gptr,
           ((uint8_t*)gptr >= from_base && (uint8_t*)gptr < from_end),
           is_ct_ptr(ctx, gptr));
    fflush(stdout);
  }
#endif
  ctx->global_obj = gc_copy_value (ctx, ctx->global_obj, from_base, from_end, to_base, &to_free, to_end);
#ifdef DUMP_GC_DETAIL
  printf("  after copy: global_obj = 0x%llx\n", (unsigned long long)ctx->global_obj); fflush(stdout);
#endif
#ifdef DUMP_GC_DETAIL
  printf("  roots: regexp_ctor\n"); fflush(stdout);
#endif
  ctx->regexp_ctor = gc_copy_value (ctx, ctx->regexp_ctor, from_base, from_end, to_base, &to_free, to_end);
#ifdef DUMP_GC_DETAIL
  printf("  roots: throw_type_error\n"); fflush(stdout);
#endif
  ctx->throw_type_error = gc_copy_value (ctx, ctx->throw_type_error, from_base, from_end, to_base, &to_free, to_end);

  /* Copy current exception if pending */
#ifdef DUMP_GC_DETAIL
  printf("  roots: current_exception\n"); fflush(stdout);
#endif
  ctx->current_exception = gc_copy_value (ctx, ctx->current_exception, from_base, from_end, to_base, &to_free, to_end);

  /* Copy actor identity key */
  ctx->actor_sym = gc_copy_value (ctx, ctx->actor_sym, from_base, from_end, to_base, &to_free, to_end);

  /* Copy class prototypes */
#ifdef DUMP_GC_DETAIL
  printf("  roots: class_proto (count=%d)\n", ctx->class_count); fflush(stdout);
#endif
  for (int i = 0; i < ctx->class_count; i++) {
    ctx->class_proto[i] = gc_copy_value (ctx, ctx->class_proto[i], from_base, from_end, to_base, &to_free, to_end);
  }

  /* Copy register VM current frame */
#ifdef DUMP_GC_DETAIL
  printf("  roots: reg_current_frame\n"); fflush(stdout);
#endif
  ctx->reg_current_frame = gc_copy_value (ctx, ctx->reg_current_frame, from_base, from_end, to_base, &to_free, to_end);

  /* Copy JS_PUSH_VALUE/JS_POP_VALUE roots */
#ifdef DUMP_GC_DETAIL
  printf("  roots: top_gc_ref\n"); fflush(stdout);
#endif
  for (JSGCRef *ref = ctx->top_gc_ref; ref != NULL; ref = ref->prev) {
    ref->val = gc_copy_value (ctx, ref->val, from_base, from_end, to_base, &to_free, to_end);
  }

  /* Copy JS_LOCAL roots (update C locals through pointers) */
#ifdef DUMP_GC_DETAIL
  printf("  roots: top_local_ref\n"); fflush(stdout);
#endif
  for (JSLocalRef *ref = ctx->top_local_ref; ref != NULL; ref = ref->prev) {
    *ref->ptr = gc_copy_value (ctx, *ref->ptr, from_base, from_end, to_base, &to_free, to_end);
  }

  /* Copy JS_AddGCRef/JS_DeleteGCRef roots */
#ifdef DUMP_GC_DETAIL
  printf("  roots: last_gc_ref\n"); fflush(stdout);
#endif
  for (JSGCRef *ref = ctx->last_gc_ref; ref != NULL; ref = ref->prev) {
    ref->val = gc_copy_value (ctx, ref->val, from_base, from_end, to_base, &to_free, to_end);
  }

  /* Copy auto-rooted C call argv arrays */
  for (CCallRoot *r = ctx->c_call_root; r != NULL; r = r->prev) {
    for (int i = 0; i < r->argc; i++) {
      r->argv[i] = gc_copy_value (ctx, r->argv[i], from_base, from_end, to_base, &to_free, to_end);
    }
  }

  /* Cheney scan: scan copied objects to find more references */
  uint8_t *scan = to_base;
#ifdef DUMP_GC_DETAIL
  printf("  scan: to_base=%p to_free=%p to_end=%p\n", (void*)to_base, (void*)to_free, (void*)to_end);
  fflush(stdout);
#endif
  while (scan < to_free) {
#ifdef DUMP_GC_DETAIL
    objhdr_t scan_hdr = *(objhdr_t *)scan;
    printf("  scan %p: type=%d hdr=0x%llx", (void*)scan, objhdr_type(scan_hdr), (unsigned long long)scan_hdr);
    fflush(stdout);
#endif
    size_t obj_size = gc_object_size (scan);
#ifdef DUMP_GC_DETAIL
    printf(" size=%zu\n", obj_size);
    fflush(stdout);
#endif
    gc_scan_object (ctx, scan, from_base, from_end, to_base, &to_free, to_end);
    scan += obj_size;
  }

#ifdef VALIDATE_GC
  {
    JSRecord *grec = JS_VALUE_GET_RECORD(ctx->global_obj);
    uint32_t mask = (uint32_t)objhdr_cap56(grec->mist_hdr);
    for (uint32_t i = 1; i <= mask; i++) {
      JSValue k = grec->slots[i].key;
      if (!rec_key_is_empty(k) && !rec_key_is_tomb(k)) {
        if (!JS_IsPtr(k)) {
          fprintf(stderr, "VALIDATE_GC: global slot[%u] key is not a pointer (tag=0x%llx)\n",
                  i, (unsigned long long)k);
        } else {
          void *kp = JS_VALUE_GET_PTR(k);
          if (!ptr_in_range(kp, to_base, to_free) && !is_ct_ptr(ctx, kp)) {
            fprintf(stderr, "VALIDATE_GC: global slot[%u] key=%p outside valid ranges\n", i, kp);
          }
        }
        JSValue v = grec->slots[i].val;
        if (JS_IsPtr(v)) {
          void *vp = JS_VALUE_GET_PTR(v);
          if (!ptr_in_range(vp, to_base, to_free) && !is_ct_ptr(ctx, vp)) {
            fprintf(stderr, "VALIDATE_GC: global slot[%u] val=%p outside valid ranges\n", i, vp);
          }
        }
      }
    }
  }
#endif

  /* Return old block (in poison mode, just poison it and leak) */
  heap_block_free (rt, from_base, old_heap_size);

  /* Update context with new block */
  size_t new_used = to_free - to_base;

  /* Update GC stats */
  ctx->gc_count++;
  ctx->gc_bytes_copied += new_used;
  size_t recovered = old_used > new_used ? old_used - new_used : 0;

#ifdef DUMP_GC_TIMING
  clock_gettime(CLOCK_MONOTONIC, &gc_t1);
  double gc_ms = (gc_t1.tv_sec - gc_t0.tv_sec) * 1000.0 +
                 (gc_t1.tv_nsec - gc_t0.tv_nsec) / 1e6;
  fprintf(stderr, "GC #%u: %.2f ms | copied %zu KB | old %zu KB -> new %zu KB | recovered %zu KB (%.0f%%)\n",
          ctx->gc_count, gc_ms,
          new_used / 1024, old_used / 1024, new_size / 1024,
          recovered / 1024,
          old_used > 0 ? (100.0 * recovered / old_used) : 0.0);
#endif

  ctx->heap_base = to_base;
  ctx->heap_free = to_free;
  ctx->heap_end = to_end;
  ctx->current_block_size = new_size;

#ifdef DUMP_GC_DETAIL
  /* Verify global_obj is in valid heap range after GC */
  if (JS_IsPtr(ctx->global_obj)) {
    void *gptr = JS_VALUE_GET_PTR(ctx->global_obj);
    if ((uint8_t*)gptr < to_base || (uint8_t*)gptr >= to_free) {
      printf("  WARNING: global_obj=%p outside [%p, %p) after GC!\n",
             gptr, (void*)to_base, (void*)to_free);
      fflush(stdout);
    }
  }
#endif

  /* If <40% recovered, grow next block size for future allocations.
     First poor recovery: double.  Consecutive poor: quadruple.
     Skip under FORCE_GC_AT_MALLOC — forced GC on every allocation creates
     artificially poor recovery (no time to accumulate garbage), which
     would cause runaway exponential heap growth. */
#ifdef DUMP_GC
  int will_grow = 0;
#endif
#ifdef FORCE_GC_AT_MALLOC
  if (0) {
#else
  if (allow_grow && recovered > 0 && old_used > 0 && recovered < old_used * 2 / 5) {
#endif
    size_t factor = ctx->gc_poor_streak >= 1 ? 4 : 2;
    size_t grown = new_size * factor;
    if (grown <= buddy_max_block(&ctx->rt->buddy)) {
      ctx->next_block_size = grown;
#ifdef DUMP_GC
      will_grow = 1;
#endif
    }
    ctx->gc_poor_streak++;
  } else {
    ctx->gc_poor_streak = 0;
  }

#ifdef DUMP_GC
  {
    /* Walk to-space and tally memory by object type */
    static const char *type_names[] = {
      [OBJ_ARRAY]    = "array",
      [OBJ_TEXT]     = "text",
      [OBJ_RECORD]   = "record",
      [OBJ_FUNCTION] = "function",
      [OBJ_FRAME]    = "frame",
    };
    size_t type_bytes[8] = {0};
    size_t type_count[8] = {0};
    uint8_t *p = to_base;
    while (p < to_free) {
      uint8_t t = objhdr_type (*(objhdr_t *)p);
      size_t sz = gc_object_size (p);
      type_bytes[t] += sz;
      type_count[t]++;
      p += sz;
    }

    printf ("\nGC: %zu -> %zu bytes, recovered %zu (%.1f%%)%s\n",
            old_heap_size,
            new_size,
            recovered,
            old_used > 0 ? (recovered * 100.0 / old_used) : 0.0,
            will_grow ? ", will grow (poor recovery)" : "");
    printf ("    live %zu / %zu bytes (%.0f%% full):",
            new_used, new_size,
            new_size > 0 ? (new_used * 100.0 / new_size) : 0.0);
    for (int i = 0; i < 7; i++) {
      if (type_count[i] == 0) continue;
      printf ("  %s %zu(%zu)", type_names[i], type_bytes[i], type_count[i]);
    }
    printf ("\n");
    fflush (stdout);
  }
#endif

  /* Check memory limit — kill actor if heap exceeds cap */
  if (ctx->heap_memory_limit > 0 && ctx->current_block_size > ctx->heap_memory_limit) {
#ifdef ACTOR_TRACE
    void *crt = ctx->user_opaque;
    if (crt)
      fprintf(stderr, "[ACTOR_TRACE] heap %zu > limit %zu, OOM\n",
              ctx->current_block_size, ctx->heap_memory_limit);
#endif
    return -1;
  }

  return 0;
}

JSRuntime *JS_NewRuntime (void) {
  JSRuntime *rt;

  rt = malloc (sizeof (JSRuntime));
  if (!rt) return NULL;
  memset (rt, 0, sizeof (*rt));

  rt->buddy.initial_size = BUDDY_DEFAULT_POOL;
  rt->buddy.next_pool_size = BUDDY_DEFAULT_POOL;

  return rt;
}

void JS_SetMemoryLimit (JSRuntime *rt, size_t limit) {
  rt->malloc_limit = limit;
}

void JS_SetPoolSize (JSRuntime *rt, size_t initial, size_t cap) {
  rt->buddy.initial_size = initial;
  rt->buddy.next_pool_size = initial;
  rt->buddy.cap = cap;
}

/* Helpers to call system memory functions (for memory allocated by external libs) */

#define malloc(s) malloc_is_forbidden (s)
#define free(p) free_is_forbidden (p)
#define realloc(p, s) realloc_is_forbidden (p, s)

void JS_SetPauseFlag (JSContext *ctx, int value) {
  atomic_store_explicit (&ctx->pause_flag, value, memory_order_relaxed);
}

int JS_GetVMCallDepth(JSContext *ctx) {
  return ctx->vm_call_depth;
}

void JS_SetHeapMemoryLimit(JSContext *ctx, size_t limit) {
  ctx->heap_memory_limit = limit;
}

/* Allocate a string using bump allocation from context heap.
   Note: the string contents are uninitialized */
JSText *js_alloc_string (JSContext *ctx, int max_len) {
  JSText *str;
  /* Allocate packed UTF-32: 2 chars per 64-bit word. */
  size_t data_words = (max_len + 1) / 2;
  size_t size = sizeof (JSText) + data_words * sizeof (uint64_t);

  str = js_malloc (ctx, size);
  if (unlikely (!str)) {
    JS_ThrowOutOfMemory (ctx);
    return NULL;
  }
  /* Initialize objhdr_t with OBJ_TEXT type and capacity in cap56 */
  str->hdr = objhdr_make (max_len, OBJ_TEXT, false, false, false, false);
  str->length = 0; /* length starts at 0, capacity is in hdr */
  /* Zero packed data so odd-length strings have deterministic padding.
     js_malloc is a bump allocator and does not zero memory; without this,
     the last word's unused low 32 bits contain garbage, causing
     fash64_hash_words and JSText_equal (memcmp) to produce inconsistent
     results for different allocations of the same text content. */
  memset (str->packed, 0, data_words * sizeof (uint64_t));

  return str;
}

void JS_FreeRuntime (JSRuntime *rt) {
  /* Destroy buddy allocator */
  buddy_destroy (&rt->buddy);
  sys_free (rt);
}

/* Forward declarations for intrinsics */
static void JS_AddIntrinsicBaseObjects (JSContext *ctx);
static void JS_AddIntrinsicRegExp (JSContext *ctx);

JSContext *JS_NewContextRawWithHeapSize (JSRuntime *rt, size_t heap_size) {
  JSContext *ctx;

  /* Round up to buddy allocator minimum */
  size_t min_size = 1ULL << BUDDY_MIN_ORDER;
  if (heap_size < min_size) heap_size = min_size;

  /* Round up to power of 2 for buddy allocator */
  size_t actual = min_size;
  while (actual < heap_size && actual < buddy_max_block(&rt->buddy)) {
    actual <<= 1;
  }
  heap_size = actual;

  ctx = js_mallocz_rt (sizeof (JSContext));

  if (!ctx) return NULL;
  ctx->trace_hook = NULL;
  ctx->rt = rt;

  /* Bootstrap class_array and class_proto together via JS_NewClass1 */
  if (init_class_range (ctx, js_std_class_def, JS_CLASS_OBJECT, countof (js_std_class_def)) < 0) {
    js_free_rt (ctx->class_array);
    js_free_rt (ctx->class_proto);
    js_free_rt (ctx);
    return NULL;
  }

  ctx->regexp_ctor = JS_NULL;

  /* Initialize register VM frame root */
  ctx->reg_current_frame = JS_NULL;
  ctx->c_call_root = NULL;

  /* Initialize VM suspend/resume state */
  ctx->suspended = 0;
  ctx->suspended_pc = 0;
  ctx->vm_call_depth = 0;
  ctx->heap_memory_limit = 0;
  JS_AddGCRef(ctx, &ctx->suspended_frame_ref);
  ctx->suspended_frame_ref.val = JS_NULL;

  /* Initialize per-context execution state (moved from JSRuntime) */
  ctx->current_exception = JS_NULL;
  ctx->actor_sym = JS_NULL;

  /* Initialize constant text pool (avoids overflow pages for common case) */
  {
    size_t ct_pool_size = 64 * 1024; /* 64KB initial CT pool */
    ctx->ct_base = js_malloc_rt (ct_pool_size);
    if (!ctx->ct_base) {
      js_free_rt (ctx->class_array);
      js_free_rt (ctx->class_proto);
      js_free_rt (ctx);
      return NULL;
    }
    ctx->ct_free = ctx->ct_base;
    ctx->ct_end = ctx->ct_base + ct_pool_size;
  }

  /* Initialize constant text intern table */
  ctx->ct_pages = NULL;
  ctx->ct_array = NULL;
  ctx->ct_hash = NULL;
  ctx->ct_count = 0;
  ctx->ct_size = 0;
  ctx->ct_resize_threshold = 0;
  if (ct_resize (ctx) < 0) {
    js_free_rt (ctx->class_array);
    js_free_rt (ctx->class_proto);
    js_free_rt (ctx);
    return NULL;
  }

  /* Allocate initial heap block for bump allocation */
  ctx->current_block_size = heap_size;
  ctx->next_block_size = ctx->current_block_size;
  ctx->heap_base = heap_block_alloc (rt, ctx->current_block_size);
  if (!ctx->heap_base) {
    js_free_rt (ctx->ct_hash);
    js_free_rt (ctx->ct_array);
    js_free_rt (ctx->class_array);
    js_free_rt (ctx->class_proto);
    js_free_rt (ctx);
    return NULL;
  }
  ctx->heap_free = ctx->heap_base;
  ctx->heap_end = ctx->heap_base + ctx->current_block_size;

#ifdef DUMP_GC_DETAIL
  printf("Context init: heap_base=%p heap_end=%p size=%zu\n", (void*)ctx->heap_base, (void*)ctx->heap_end, ctx->current_block_size);
#endif

#ifdef DUMP_GC_DETAIL
  printf("After BasicObjects: heap_base=%p heap_end=%p heap_free=%p\n", (void*)ctx->heap_base, (void*)ctx->heap_end, (void*)ctx->heap_free);
#endif

  return ctx;
}

JSContext *JS_NewContextRaw (JSRuntime *rt) {
  return JS_NewContextRawWithHeapSize (rt, 1ULL << BUDDY_MIN_ORDER);
}

JSContext *JS_NewContext (JSRuntime *rt) {
  JSContext *ctx = JS_NewContextRaw (rt);
  if (!ctx) return NULL;
  JS_AddIntrinsicBaseObjects (ctx);
  JS_AddIntrinsicRegExp (ctx);
  obj_set_stone (JS_VALUE_GET_RECORD (ctx->global_obj));
  return ctx;
}

JSContext *JS_NewContextWithHeapSize (JSRuntime *rt, size_t heap_size) {
  JSContext *ctx = JS_NewContextRawWithHeapSize (rt, heap_size);
  if (!ctx) return NULL;
  JS_AddIntrinsicBaseObjects (ctx);
  JS_AddIntrinsicRegExp (ctx);
  obj_set_stone (JS_VALUE_GET_RECORD (ctx->global_obj));
  return ctx;
}

void *JS_GetContextOpaque (JSContext *ctx) { return ctx->user_opaque; }

void JS_SetContextOpaque (JSContext *ctx, void *opaque) {
  ctx->user_opaque = opaque;
}

void JS_SetActorSym (JSContext *ctx, JSValue sym) {
  ctx->actor_sym = sym;
}

JSValue JS_GetActorSym (JSContext *ctx) {
  return ctx->actor_sym;
}

void JS_SetClassProto (JSContext *ctx, JSClassID class_id, JSValue obj) {
  assert (class_id < ctx->class_count);
  set_value (ctx, &ctx->class_proto[class_id], obj);
}

JSValue JS_GetClassProto (JSContext *ctx, JSClassID class_id) {
  assert (class_id < ctx->class_count);
  return ctx->class_proto[class_id];
}

void JS_FreeContext (JSContext *ctx) {
  JSRuntime *rt = ctx->rt;
  int i;

  JS_DeleteGCRef(ctx, &ctx->suspended_frame_ref);

  for (i = 0; i < JS_NATIVE_ERROR_COUNT; i++) {
  }
  for (i = 0; i < ctx->class_count; i++) {
  }
  js_free_rt (ctx->class_array);
  js_free_rt (ctx->class_proto);

  /* Free constant text pool and intern table */
  ct_free_all (ctx);
  if (ctx->ct_base) js_free_rt (ctx->ct_base);
  js_free_rt (ctx->ct_hash);
  js_free_rt (ctx->ct_array);

  /* Free heap block */
  if (ctx->heap_base) {
    heap_block_free (rt, ctx->heap_base, ctx->current_block_size);
    ctx->heap_base = NULL;
    ctx->heap_free = NULL;
    ctx->heap_end = NULL;
  }

  js_free_rt (ctx);
}

JSRuntime *JS_GetRuntime (JSContext *ctx) { return ctx->rt; }

void JS_SetMaxStackSize (JSContext *ctx, size_t stack_size) {
  ctx->stack_limit = stack_size;
}

JSText *js_alloc_string (JSContext *ctx, int max_len);

static __maybe_unused void JS_DumpChar (FILE *fo, int c, int sep) {
  if (c == sep || c == '\\') {
    fputc ('\\', fo);
    fputc (c, fo);
  } else if (c >= ' ' && c <= 126) {
    fputc (c, fo);
  } else if (c == '\n') {
    fputc ('\\', fo);
    fputc ('n', fo);
  } else {
    fprintf (fo, "\\u%04x", c);
  }
}

__maybe_unused void JS_DumpString (JSRuntime *rt, const JSText *p) {
  int i;

  if (p == NULL) {
    printf ("<null>");
    return;
  }
  putchar ('"');
  for (i = 0; i < (int)JSText_len (p); i++) {
    JS_DumpChar (stdout, string_get (p, i), '"');
  }
  putchar ('"');
}

static inline BOOL JS_IsEmptyString (JSValue v) {
  return v == JS_EMPTY_TEXT;
}

/* JSClass support */

/* a new class ID is allocated if *pclass_id != 0 */
JSClassID JS_NewClassID (JSClassID *pclass_id) {
  JSClassID class_id;
  class_id = *pclass_id;
  if (class_id == 0) {
    class_id = js_class_id_alloc++;
    *pclass_id = class_id;
  }
  return class_id;
}

JSClassID JS_GetClassID (JSValue v) {
  JSRecord *rec;
  if (!JS_IsRecord (v)) return JS_INVALID_CLASS_ID;
  rec = JS_VALUE_GET_RECORD (v);
  return REC_GET_CLASS_ID(rec);
}

BOOL JS_IsRegisteredClass (JSContext *ctx, JSClassID class_id) {
  return (class_id < ctx->class_count
          && ctx->class_array[class_id].class_id != 0);
}

/* create a new object internal class. Return -1 if error, 0 if
   OK. The finalizer can be NULL if none is needed. */
int JS_NewClass1 (JSContext *ctx, JSClassID class_id, const JSClassDef *class_def, const char *name) {
  int new_size, i;
  JSClass *cl, *new_class_array;
  JSValue *new_class_proto;

  if (class_id >= (1 << 16)) return -1;
  if (class_id < ctx->class_count && ctx->class_array[class_id].class_id != 0)
    return -1;

  if (class_id >= ctx->class_count) {
    new_size = max_int (JS_CLASS_INIT_COUNT,
                        max_int (class_id + 1, ctx->class_count * 3 / 2));

    /* reallocate the class array */
    new_class_array
      = js_realloc_rt (ctx->class_array, sizeof (JSClass) * new_size);
    if (!new_class_array) return -1;
    memset (new_class_array + ctx->class_count, 0, (new_size - ctx->class_count) * sizeof (JSClass));
    ctx->class_array = new_class_array;

    /* reallocate the class proto array */
    new_class_proto
      = js_realloc_rt (ctx->class_proto, sizeof (JSValue) * new_size);
    if (!new_class_proto) return -1;
    for (i = ctx->class_count; i < new_size; i++)
      new_class_proto[i] = JS_NULL;
    ctx->class_proto = new_class_proto;

    ctx->class_count = new_size;
  }
  cl = &ctx->class_array[class_id];
  cl->class_id = class_id;
  cl->class_name = name; /* name is already a const char* */
  cl->finalizer = class_def->finalizer;
  return 0;
}

int JS_NewClass (JSContext *ctx, JSClassID class_id, const JSClassDef *class_def) {
  /* class_name is stored directly as const char* */
  return JS_NewClass1 (ctx, class_id, class_def, class_def->class_name);
}

JSValue js_new_string8_len (JSContext *ctx, const char *buf, int len) {
  JSText *str;
  int i;

  /* Empty string - return immediate empty */
  if (len <= 0) { return MIST_TryNewImmediateASCII ("", 0); }

  /* Try immediate ASCII for short strings (≤7 chars) */
  if (len <= MIST_ASCII_MAX_LEN) {
    JSValue imm = MIST_TryNewImmediateASCII (buf, len);
    if (!JS_IsNull (imm)) return imm;
  }

  /* Fall back to heap string */
  str = js_alloc_string (ctx, len);
  if (!str) return JS_ThrowMemoryError (ctx);
  for (i = 0; i < len; i++)
    string_put (str, i, buf[i]);
  str->length = len;

  return pretext_end (ctx, str);
}

JSValue js_new_string8 (JSContext *ctx, const char *buf) {
  return js_new_string8_len (ctx, buf, strlen (buf));
}

/* GC-safe substring: takes JSValue (which must be rooted by caller) */
static JSValue js_sub_string (JSContext *ctx, JSText *p, int start, int end) {
  int i;
  int len = end - start;
  if (start == 0 && end == (int)JSText_len (p)) {
    return JS_MKPTR (p);
  }

  /* Root the source string as a JSValue so it survives js_alloc_string GC */
  JSGCRef src_ref;
  JS_PushGCRef (ctx, &src_ref);
  src_ref.val = JS_MKPTR (p);

  JSText *str = js_alloc_string (ctx, len);
  if (!str) {
    JS_PopGCRef (ctx, &src_ref);
    return JS_EXCEPTION;
  }

  /* Re-chase p after allocation */
  p = JS_VALUE_GET_STRING (src_ref.val);

  for (i = 0; i < len; i++)
    string_put (str, i, string_get (p, start + i));
  str->length = len;

  JS_PopGCRef (ctx, &src_ref);
  return pretext_end (ctx, str);
}

/* Substring from a JSValue (handles both immediate ASCII and heap strings) */
static JSValue js_sub_string_val (JSContext *ctx, JSValue src, int start, int end) {
  int len = end - start;
  if (len <= 0) return JS_NewString (ctx, "");

  if (MIST_IsImmediateASCII (src)) {
    char buf[MIST_ASCII_MAX_LEN + 1];
    for (int i = 0; i < len; i++)
      buf[i] = (char)MIST_GetImmediateASCIIChar (src, start + i);
    return js_new_string8_len (ctx, buf, len);
  }

  /* Heap string — fast path for short ASCII substrings (avoids heap alloc) */
  JSText *p = JS_VALUE_GET_STRING (src);
  if (len <= MIST_ASCII_MAX_LEN) {
    char buf[MIST_ASCII_MAX_LEN];
    int all_ascii = 1;
    for (int i = 0; i < len; i++) {
      uint32_t c = string_get (p, start + i);
      if (c >= 0x80) { all_ascii = 0; break; }
      buf[i] = (char)c;
    }
    if (all_ascii) {
      JSValue imm = MIST_TryNewImmediateASCII (buf, len);
      if (!JS_IsNull (imm)) return imm;
    }
  }

  /* Heap string — delegate to existing js_sub_string */
  return js_sub_string (ctx, p, start, end);
}

/* Allocate a new pretext (mutable JSText) with initial capacity */
JSText *pretext_init (JSContext *ctx, int capacity) {
  if (capacity <= 0) capacity = 16;
  JSText *s = js_alloc_string (ctx, capacity);
  if (!s) return NULL;
  s->length = 0;
  return s;
}

/* Reallocate a pretext to hold new_len characters */
static no_inline JSText *pretext_realloc (JSContext *ctx, JSText *s, int new_len) {
  if (new_len > JS_STRING_LEN_MAX) {
    JS_ThrowInternalError (ctx, "string too long");
    return NULL;
  }
  int old_cap = (int)objhdr_cap56 (s->hdr);
  int old_len = (int)s->length;
  /* Grow by 50%, ensuring we have at least new_len capacity */
  int new_cap = max_int (new_len, old_cap * 3 / 2);

  /* Protect source object with a GC ref before allocating (GC may move it) */
  JSGCRef src_ref;
  JS_PushGCRef (ctx, &src_ref);
  src_ref.val = JS_MKPTR (s);

  /* Allocate new string - this may trigger GC */
  JSText *new_str = js_alloc_string (ctx, new_cap);
  if (!new_str) {
    JS_PopGCRef (ctx, &src_ref);
    return NULL;
  }

  /* Get possibly-moved source pointer after GC */
  s = (JSText *)chase (src_ref.val);
  JS_PopGCRef (ctx, &src_ref);

  /* Copy data from old string to new */
  new_str->length = old_len;
  for (int i = 0; i < old_len; i++) {
    string_put (new_str, i, string_get (s, i));
  }

  return new_str;
}

no_inline JSText *pretext_putc_slow (JSContext *ctx, JSText *s, uint32_t c) {
  int len = (int)s->length;
  int cap = (int)objhdr_cap56 (s->hdr);
  if (unlikely (len >= cap)) {
    s = pretext_realloc (ctx, s, len + 1);
    if (!s) return NULL;
  }
  string_put (s, len, c);
  s->length++;
  return s;
}

/* 0 <= c <= 0x10ffff */
JSText *pretext_putc (JSContext *ctx, JSText *s, uint32_t c) {
  int len = (int)s->length;
  int cap = (int)objhdr_cap56 (s->hdr);
  if (likely (len < cap)) {
    string_put (s, len, c);
    s->length++;
    return s;
  }
  return pretext_putc_slow (ctx, s, c);
}

static JSText *pretext_write8 (JSContext *ctx, JSText *s, const uint8_t *p, int len) {
  int cur_len = (int)s->length;
  int cap = (int)objhdr_cap56 (s->hdr);
  if (cur_len + len > cap) {
    s = pretext_realloc (ctx, s, cur_len + len);
    if (!s) return NULL;
  }
  for (int i = 0; i < len; i++) {
    string_put (s, cur_len + i, p[i]);
  }
  s->length += len;
  return s;
}

/* appending an ASCII string */
static JSText *pretext_puts8 (JSContext *ctx, JSText *s, const char *str) {
  return pretext_write8 (ctx, s, (const uint8_t *)str, strlen (str));
}

static JSText *pretext_concat (JSContext *ctx, JSText *s, const JSText *p, uint32_t from, uint32_t to) {
  if (to <= from) return s;
  int len = (int)(to - from);
  int cur_len = (int)s->length;
  int cap = (int)objhdr_cap56 (s->hdr);
  if (cur_len + len > cap) {
    /* Root p across pretext_realloc which can trigger GC */
    JSGCRef p_ref;
    JS_PushGCRef (ctx, &p_ref);
    p_ref.val = JS_MKPTR ((void *)p);
    s = pretext_realloc (ctx, s, cur_len + len);
    p = (const JSText *)chase (p_ref.val);
    JS_PopGCRef (ctx, &p_ref);
    if (!s) return NULL;
  }
  for (int i = 0; i < len; i++) {
    string_put (s, cur_len + i, string_get (p, (int)from + i));
  }
  s->length += len;
  return s;
}

JSText *pretext_concat_value (JSContext *ctx, JSText *s, JSValue v) {
  if (MIST_IsImmediateASCII (v)) {
    int len = MIST_GetImmediateASCIILen (v);
    char buf[8];
    for (int i = 0; i < len; i++)
      buf[i] = MIST_GetImmediateASCIIChar (v, i);
    return pretext_write8 (ctx, s, (const uint8_t *)buf, len);
  }
  if (JS_IsText (v)) {
    JSText *p = JS_VALUE_GET_STRING (v);
    return pretext_concat (ctx, s, p, 0, (uint32_t)JSText_len (p));
  }
  /* Slow path: v needs conversion — root s across JS_ToString which can
     allocate and trigger GC */
  JSGCRef s_ref;
  JS_PushGCRef (ctx, &s_ref);
  s_ref.val = JS_MKPTR (s);

  JSValue v1 = JS_ToString (ctx, v);

  s = (JSText *)chase (s_ref.val);  /* re-fetch after possible GC */
  JS_PopGCRef (ctx, &s_ref);

  if (JS_IsException (v1)) return NULL;

  if (MIST_IsImmediateASCII (v1)) {
    int len = MIST_GetImmediateASCIILen (v1);
    char buf[8];
    for (int i = 0; i < len; i++)
      buf[i] = MIST_GetImmediateASCIIChar (v1, i);
    s = pretext_write8 (ctx, s, (const uint8_t *)buf, len);
    return s;
  }

  JSText *p = JS_VALUE_GET_STRING (v1);
  s = pretext_concat (ctx, s, p, 0, (uint32_t)JSText_len (p));
  return s;
}

/* Finalize a pretext into an immutable JSValue string */
JSValue pretext_end (JSContext *ctx, JSText *s) {
  if (!s) return JS_EXCEPTION;
  int len = (int)s->length;
  if (len == 0) {
    js_free (ctx, s);
    return JS_KEY_empty;
  }
  /* Promote short ASCII strings to immediate values */
  if (len <= MIST_ASCII_MAX_LEN) {
    char buf[MIST_ASCII_MAX_LEN];
    int all_ascii = 1;
    for (int i = 0; i < len; i++) {
      uint32_t c = string_get (s, i);
      if (c >= 0x80) { all_ascii = 0; break; }
      buf[i] = (char)c;
    }
    if (all_ascii) {
      JSValue imm = MIST_TryNewImmediateASCII (buf, len);
      if (!JS_IsNull (imm)) return imm;
    }
  }
  /* Set final length in capacity field and clear length for hash storage */
  s->hdr = objhdr_set_cap56 (s->hdr, len);
  s->length = 0;
  s->hdr = objhdr_set_s (s->hdr, true); /* mark as stone */
  return JS_MKPTR (s);
}

/* create a string from a UTF-8 buffer */
JSValue JS_NewStringLen (JSContext *ctx, const char *buf, size_t buf_len) {
  if (buf_len > JS_STRING_LEN_MAX)
    return JS_ThrowInternalError (ctx, "string too long");

  /* Try immediate ASCII first (<=7 ASCII chars) */
  if (buf_len <= MIST_ASCII_MAX_LEN) {
    JSValue ret = MIST_TryNewImmediateASCII (buf, buf_len);
    if (!JS_IsNull (ret)) return ret;
  }

  /* Count actual codepoints for allocation */
  const uint8_t *p = (const uint8_t *)buf;
  const uint8_t *end = p + buf_len;
  int codepoint_count = 0;
  while (p < end) {
    if (*p < 128) {
      p++;
      codepoint_count++;
    } else {
      const uint8_t *next;
      int c = unicode_from_utf8 (p, (int)(end - p), &next);
      if (c < 0) {
        /* Invalid UTF-8 byte, treat as single byte */
        p++;
      } else {
        p = next;
      }
      codepoint_count++;
    }
  }

  JSText *str = js_alloc_string (ctx, codepoint_count);
  if (!str) return JS_ThrowMemoryError (ctx);

  /* Decode UTF-8 to UTF-32 */
  p = (const uint8_t *)buf;
  int i = 0;
  while (p < end) {
    uint32_t c;
    if (*p < 128) {
      c = *p++;
    } else {
      const uint8_t *next;
      int decoded = unicode_from_utf8 (p, (int)(end - p), &next);
      if (decoded < 0) {
        /* Invalid UTF-8 byte, use replacement char or the byte itself */
        c = *p++;
      } else {
        c = (uint32_t)decoded;
        p = next;
      }
    }
    string_put (str, i++, c);
  }
  str->length = codepoint_count;
  return pretext_end (ctx, str);
}

JSValue JS_ConcatString3 (JSContext *ctx, const char *str1, JSValue str2, const char *str3) {
  JSText *b;
  int len1, len3, str2_len;
  JSGCRef str2_ref;

  if (!JS_IsText (str2)) {
    str2 = JS_ToString (ctx, str2);
    if (JS_IsException (str2)) goto fail;
  }

  /* Root str2 — pretext_init/pretext_write8/pretext_concat_value allocate
     and can trigger GC, which would move the heap string str2 points to */
  JS_PushGCRef (ctx, &str2_ref);
  str2_ref.val = str2;

  str2_len = js_string_value_len (str2_ref.val);
  len1 = strlen (str1);
  len3 = strlen (str3);

  b = pretext_init (ctx, len1 + str2_len + len3);
  if (!b) goto fail_pop;

  b = pretext_write8 (ctx, b, (const uint8_t *)str1, len1);
  if (!b) goto fail_pop;
  b = pretext_concat_value (ctx, b, str2_ref.val);
  if (!b) goto fail_pop;
  b = pretext_write8 (ctx, b, (const uint8_t *)str3, len3);
  if (!b) goto fail_pop;

  JS_PopGCRef (ctx, &str2_ref);
  return pretext_end (ctx, b);

fail_pop:
  JS_PopGCRef (ctx, &str2_ref);
fail:
  return JS_EXCEPTION;
}

/* return (NULL, 0) if exception. */
/* return pointer into a JSText with a live ref_count */
/* cesu8 determines if non-BMP1 codepoints are encoded as 1 or 2 utf-8
 * sequences */
const char *JS_ToCStringLen2 (JSContext *ctx, size_t *plen, JSValue val1, BOOL cesu8) {
  JSGCRef val_ref;
  char *q, *ret;
  size_t size;
  int i, len;

  JS_PushGCRef (ctx, &val_ref);

  if (!JS_IsText (val1)) {
    val_ref.val = JS_ToString (ctx, val1);
    if (JS_IsException (val_ref.val)) goto fail;
  } else {
    val_ref.val = val1;
  }

  /* Handle immediate ASCII strings */
  if (MIST_IsImmediateASCII (val_ref.val)) {
    len = MIST_GetImmediateASCIILen (val_ref.val);
    ret = js_malloc_rt (len + 1);  /* Use non-GC heap for returned C string */
    if (!ret) goto fail;
    /* Re-read from val_ref after potential GC */
    for (i = 0; i < len; i++) {
      ret[i] = MIST_GetImmediateASCIIChar (val_ref.val, i);
    }
    ret[len] = '\0';
    if (plen) *plen = len;
    JS_PopGCRef (ctx, &val_ref);
    return ret;
  }

  /* Handle heap strings (JSText) */
  JSText *str = JS_VALUE_GET_STRING (val_ref.val);
  len = (int)JSText_len (str);

  /* Calculate UTF-8 size */
  size = 0;
  for (i = 0; i < len; i++) {
    uint32_t c = string_get (str, i);
    if (c < 0x80)
      size += 1;
    else if (c < 0x800)
      size += 2;
    else if (c < 0x10000)
      size += 3;
    else
      size += 4;
  }

  ret = js_malloc_rt (size + 1);  /* Use non-GC heap for returned C string */
  if (!ret) goto fail;

  /* str pointer is still valid - no GC triggered by js_malloc_rt */
  /* Re-extract for safety in case code above changes */
  str = JS_VALUE_GET_STRING (val_ref.val);
  q = ret;
  for (i = 0; i < len; i++) {
    uint32_t c = string_get (str, i);
    q += unicode_to_utf8 ((uint8_t *)q, c);
  }
  *q = '\0';

  if (plen) *plen = size;

  JS_PopGCRef (ctx, &val_ref);
  return ret;

fail:
  JS_PopGCRef (ctx, &val_ref);
  if (plen) *plen = 0;
  return NULL;
}

void JS_FreeCString (JSContext *ctx, const char *ptr) {
  /* Free C string allocated from non-GC heap */
  js_free_rt ((void *)ptr);
  (void)ctx;
  (void)ptr;
}

/* Helper for string value comparison (handles immediate and heap strings) */
int js_string_compare_value (JSContext *ctx, JSValue op1, JSValue op2, BOOL eq_only) {
  (void)ctx;
  if (eq_only && op1 == op2) return 0;

  int len1 = js_string_value_len (op1);
  int len2 = js_string_value_len (op2);

  if (eq_only && len1 != len2) return 1;

  int len = min_int (len1, len2);

  for (int i = 0; i < len; i++) {
    uint32_t c1 = js_string_value_get (op1, i);
    uint32_t c2 = js_string_value_get (op2, i);
    if (c1 != c2) { return (c1 < c2) ? -1 : 1; }
  }

  if (len1 == len2) return 0;
  return (len1 < len2) ? -1 : 1;
}

int js_string_compare_value_nocase (JSContext *ctx, JSValue op1, JSValue op2) {
  (void)ctx;
  int len1 = js_string_value_len (op1);
  int len2 = js_string_value_len (op2);
  if (len1 != len2) return 1;
  for (int i = 0; i < len1; i++) {
    uint32_t c1 = js_string_value_get (op1, i);
    uint32_t c2 = js_string_value_get (op2, i);
    if (c1 >= 'A' && c1 <= 'Z') c1 += 32;
    if (c2 >= 'A' && c2 <= 'Z') c2 += 32;
    if (c1 != c2) return 1;
  }
  return 0;
}

JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2) {
  if (unlikely (!JS_IsText (op1))) {
    /* Root op2 across JS_ToString which can trigger GC */
    JSGCRef op2_guard;
    JS_PushGCRef (ctx, &op2_guard);
    op2_guard.val = op2;
    op1 = JS_ToString (ctx, op1);
    op2 = op2_guard.val;
    JS_PopGCRef (ctx, &op2_guard);
    if (JS_IsException (op1)) {
      return JS_EXCEPTION;
    }
  }
  if (unlikely (!JS_IsText (op2))) {
    /* Root op1 across JS_ToString which can trigger GC */
    JSGCRef op1_guard;
    JS_PushGCRef (ctx, &op1_guard);
    op1_guard.val = op1;
    op2 = JS_ToString (ctx, op2);
    op1 = op1_guard.val;
    JS_PopGCRef (ctx, &op1_guard);
    if (JS_IsException (op2)) {
      return JS_EXCEPTION;
    }
  }

  int len1 = js_string_value_len (op1);
  int len2 = js_string_value_len (op2);
  int new_len = len1 + len2;
  JSValue ret_val = JS_NULL;

  /* Try to create immediate ASCII if short enough and all ASCII */
  if (new_len <= MIST_ASCII_MAX_LEN) {
    char buf[8];
    BOOL all_ascii = TRUE;
    for (int i = 0; i < len1 && all_ascii; i++) {
      uint32_t c = js_string_value_get (op1, i);
      if (c >= 0x80)
        all_ascii = FALSE;
      else
        buf[i] = (char)c;
    }
    for (int i = 0; i < len2 && all_ascii; i++) {
      uint32_t c = js_string_value_get (op2, i);
      if (c >= 0x80)
        all_ascii = FALSE;
      else
        buf[len1 + i] = (char)c;
    }
    if (all_ascii) { ret_val = MIST_TryNewImmediateASCII (buf, new_len); }
  }

  if (JS_IsNull (ret_val)) {
    /* Protect op1 and op2 from GC during allocation */
    JSGCRef op1_ref, op2_ref;
    JS_PushGCRef (ctx, &op1_ref);
    op1_ref.val = op1;
    JS_PushGCRef (ctx, &op2_ref);
    op2_ref.val = op2;

    JSText *p = js_alloc_string (ctx, new_len);
    if (!p) {
      JS_PopGCRef (ctx, &op2_ref);
      JS_PopGCRef (ctx, &op1_ref);
      return JS_EXCEPTION;
    }

    /* Get possibly-moved values after GC */
    op1 = op1_ref.val;
    op2 = op2_ref.val;
    JS_PopGCRef (ctx, &op2_ref);
    JS_PopGCRef (ctx, &op1_ref);

    /* Copy characters using string_put/get */
    for (int i = 0; i < len1; i++) {
      string_put (p, i, js_string_value_get (op1, i));
    }
    for (int i = 0; i < len2; i++) {
      string_put (p, len1 + i, js_string_value_get (op2, i));
    }
    p->length = new_len;
    ret_val = pretext_end (ctx, p);
  }

  return ret_val;
}

/* WARNING: proto must be an object or JS_NULL */
JSValue JS_NewObjectProtoClass (JSContext *ctx, JSValue proto_val, JSClassID class_id) {
  JSGCRef proto_ref;
  JS_PushGCRef (ctx, &proto_ref);
  proto_ref.val = proto_val;

  JSRecord *rec = js_new_record_class (ctx, 0, class_id);

  proto_val = proto_ref.val;  /* Get potentially-updated value after GC */
  JS_PopGCRef (ctx, &proto_ref);

  if (!rec) return JS_EXCEPTION;

  /* Set prototype if provided */
  if (JS_IsRecord (proto_val)) {
    rec->proto = proto_val;
  }

  return JS_MKPTR (rec);
}

JSValue JS_NewObjectClass (JSContext *ctx, int class_id) {
  return JS_NewObjectProtoClass (ctx, ctx->class_proto[class_id], class_id);
}

JSValue JS_NewObjectProto (JSContext *ctx, JSValue proto) {
  return JS_NewObjectProtoClass (ctx, proto, JS_CLASS_OBJECT);
}

/* Create an intrinsic array with specified capacity
   Uses bump allocation - values are inline after the JSArray struct */
JSValue JS_NewArrayLen (JSContext *ctx, uint32_t len) {
  JSArray *arr;
  uint32_t cap;

  cap = len > 0 ? len : JS_ARRAY_INITIAL_SIZE;
  size_t values_size = sizeof (JSValue) * cap;
  size_t total_size = sizeof (JSArray) + values_size;

  arr = js_malloc (ctx, total_size);
  if (!arr) return JS_EXCEPTION;

  arr->mist_hdr = objhdr_make (cap, OBJ_ARRAY, false, false, false, false);
  arr->len = len;

  /* Initialize all values to null (values[] is inline flexible array member) */
  for (uint32_t i = 0; i < cap; i++) {
    arr->values[i] = JS_NULL;
  }

  return JS_MKPTR (arr);
}

JSValue JS_NewArray (JSContext *ctx) { return JS_NewArrayLen (ctx, 0); }

/* Create array with pre-allocated capacity but len=0 (for push-fill patterns) */
JSValue JS_NewArrayCap (JSContext *ctx, uint32_t cap) {
  if (cap == 0) cap = JS_ARRAY_INITIAL_SIZE;
  size_t values_size = sizeof (JSValue) * cap;
  size_t total_size = sizeof (JSArray) + values_size;
  JSArray *arr = js_malloc (ctx, total_size);
  if (!arr) return JS_EXCEPTION;
  arr->mist_hdr = objhdr_make (cap, OBJ_ARRAY, false, false, false, false);
  arr->len = 0;
  for (uint32_t i = 0; i < cap; i++)
    arr->values[i] = JS_NULL;
  return JS_MKPTR (arr);
}

JSValue JS_NewObject (JSContext *ctx) {
  /* inline JS_NewObjectClass(ctx, JS_CLASS_OBJECT); */
  return JS_NewObjectProtoClass (ctx, ctx->class_proto[JS_CLASS_OBJECT], JS_CLASS_OBJECT);
}

/* Create object with pre-allocated hash table for n properties */
JSValue JS_NewObjectCap (JSContext *ctx, uint32_t n) {
  /* slot 0 is reserved, so need n+1 slots minimum.
     Hash table needs ~2x entries for good load factor.
     mask must be power-of-2 minus 1. */
  uint32_t need = (n + 1) * 2;
  uint32_t mask = JS_RECORD_INITIAL_MASK;
  while (mask + 1 < need) mask = (mask << 1) | 1;

  JSGCRef proto_ref;
  JS_PushGCRef (ctx, &proto_ref);
  proto_ref.val = ctx->class_proto[JS_CLASS_OBJECT];

  JSRecord *rec = js_new_record_class (ctx, mask, JS_CLASS_OBJECT);

  JSValue proto_val = proto_ref.val;
  JS_PopGCRef (ctx, &proto_ref);

  if (!rec) return JS_EXCEPTION;

  if (JS_IsRecord (proto_val))
    rec->proto = proto_val;

  return JS_MKPTR (rec);
}


/* Note: at least 'length' arguments will be readable in 'argv' */
static JSValue JS_NewCFunction3 (JSContext *ctx, JSCFunction *func, const char *name, int length, JSCFunctionEnum cproto, int magic) {
  JSValue func_obj;
  JSFunction *f;

  func_obj = js_new_function (ctx, JS_FUNC_KIND_C);
  if (JS_IsException (func_obj)) return func_obj;
  f = JS_VALUE_GET_FUNCTION (func_obj);
  f->u.cfunc.c_function.generic = func;
  f->u.cfunc.cproto = cproto;
  f->u.cfunc.magic = magic;
  f->length = length;
  if (name) {
    JSGCRef fobj_ref;
    JS_PushGCRef (ctx, &fobj_ref);
    fobj_ref.val = func_obj;
    JSValue key = js_key_new (ctx, name);
    func_obj = fobj_ref.val;
    JS_PopGCRef (ctx, &fobj_ref);
    f = JS_VALUE_GET_FUNCTION (func_obj);  /* re-chase after allocation */
    f->name = key;
  } else {
    f->name = JS_KEY_empty;
  }
  return func_obj;
}

/* Note: at least 'length' arguments will be readable in 'argv' */
JSValue JS_NewCFunction2 (JSContext *ctx, JSCFunction *func, const char *name, int length, JSCFunctionEnum cproto, int magic) {
  return JS_NewCFunction3 (ctx, func, name, length, cproto, magic);
}

/* free_property is defined earlier as a stub since shapes are removed */

/* GC-safe array growth function.
   Takes JSValue* pointer to a GC-tracked location (like &argv[n]).
   Allocates new array, copies data, installs forward header at old location. */
static int js_array_grow (JSContext *ctx, JSValue *arr_ptr, word_t min_cap) {
  JSArray *arr = JS_VALUE_GET_ARRAY (*arr_ptr);
  word_t old_cap = js_array_cap (arr);
  if (min_cap <= old_cap) return 0;

  if (objhdr_s (arr->mist_hdr)) {
    JS_ThrowInternalError (ctx, "cannot grow a stoned array");
    return -1;
  }

  word_t new_cap = old_cap ? old_cap : JS_ARRAY_INITIAL_SIZE;
  while (new_cap < min_cap && new_cap <= JS_ARRAY_MAX_CAP / 2)
    new_cap *= 2;
  if (new_cap > JS_ARRAY_MAX_CAP) new_cap = JS_ARRAY_MAX_CAP;
  if (new_cap < min_cap) {
    JS_ThrowRangeError (ctx, "array capacity overflow");
    return -1;
  }

  size_t total_size = sizeof (JSArray) + sizeof (JSValue) * new_cap;
  JSArray *new_arr = js_malloc (ctx, total_size);
  if (!new_arr) return -1;

  /* Re-chase arr via arr_ptr (GC may have moved it during js_malloc) */
  arr = JS_VALUE_GET_ARRAY (*arr_ptr);

  new_arr->mist_hdr = objhdr_make (new_cap, OBJ_ARRAY, false, false, false, false);
  new_arr->len = arr->len;

  JSValue old_ptr = *arr_ptr;
  for (word_t i = 0; i < arr->len; i++)
    new_arr->values[i] = arr->values[i];
  for (word_t i = arr->len; i < new_cap; i++)
    new_arr->values[i] = JS_NULL;

  /* Install forward header at old location */
  arr->mist_hdr = objhdr_make_fwd (new_arr);

  /* Update the tracked JSValue to point to new array */
  *arr_ptr = JS_MKPTR (new_arr);

  /* Fix self-references: update elements that pointed to the old array */
  for (word_t i = 0; i < new_arr->len; i++) {
    if (new_arr->values[i] == old_ptr)
      new_arr->values[i] = *arr_ptr;
  }

  return 0;
}

/* GC-safe array push. Takes JSValue* to GC-tracked location. */
static int js_intrinsic_array_push (JSContext *ctx, JSValue *arr_ptr, JSValue val) {
  JSArray *arr = JS_VALUE_GET_ARRAY (*arr_ptr);

  if (objhdr_s (arr->mist_hdr)) {
    JS_ThrowInternalError (ctx, "cannot push to a stoned array");
    return -1;
  }

  if (arr->len >= js_array_cap (arr)) {
    /* Root val across js_array_grow which can trigger GC */
    JSGCRef val_ref;
    JS_PushGCRef (ctx, &val_ref);
    val_ref.val = val;
    if (js_array_grow (ctx, arr_ptr, arr->len + 1) < 0) {
      JS_PopGCRef (ctx, &val_ref);
      return -1;
    }
    val = val_ref.val;
    JS_PopGCRef (ctx, &val_ref);
    arr = JS_VALUE_GET_ARRAY (*arr_ptr);  /* re-chase after grow */
  }

  arr->values[arr->len++] = val;
  return 0;
}

/* GC-safe array set. Takes JSValue* to GC-tracked location. */
static int js_intrinsic_array_set (JSContext *ctx, JSValue *arr_ptr, word_t idx, JSValue val) {
  JSArray *arr = JS_VALUE_GET_ARRAY (*arr_ptr);

  if (objhdr_s (arr->mist_hdr)) {
    JS_ThrowInternalError (ctx, "cannot set on a stoned array");
    return -1;
  }

  if (idx >= js_array_cap (arr)) {
    /* Root val across js_array_grow which can trigger GC */
    JSGCRef val_ref;
    JS_PushGCRef (ctx, &val_ref);
    val_ref.val = val;
    if (js_array_grow (ctx, arr_ptr, idx + 1) < 0) {
      JS_PopGCRef (ctx, &val_ref);
      return -1;
    }
    val = val_ref.val;
    JS_PopGCRef (ctx, &val_ref);
    arr = JS_VALUE_GET_ARRAY (*arr_ptr);  /* re-chase after grow */
  }

  if (idx >= arr->len) {
    for (word_t i = arr->len; i < idx; i++)
      arr->values[i] = JS_NULL;
    arr->len = idx + 1;
  }

  arr->values[idx] = val;
  return 0;
}

/* Allocate a new function object */
JSValue js_new_function (JSContext *ctx, JSFunctionKind kind) {
  JSFunction *func = js_mallocz (ctx, sizeof (JSFunction));
  if (!func) return JS_EXCEPTION;
  func->header = objhdr_make (0, OBJ_FUNCTION, false, false, false, false);
  func->kind = kind;
  func->name = JS_NULL;
  func->length = 0;
  return JS_MKPTR (func);
}

/* WARNING: obj is freed */
JSValue JS_Throw (JSContext *ctx, JSValue obj) {
  ctx->current_exception = obj;
  return JS_EXCEPTION;
}

/* return the pending exception (cannot be called twice). */
JSValue JS_GetException (JSContext *ctx) {
  JSValue val = ctx->current_exception;
  ctx->current_exception = JS_NULL;
  return val;
}

JS_BOOL JS_HasException (JSContext *ctx) {
  return !JS_IsNull (ctx->current_exception);
}

/* in order to avoid executing arbitrary code during the stack trace
   generation, we only look at simple 'name' properties containing a
   string. */
static const char *get_func_name (JSContext *ctx, JSValue func) {
  if (!JS_IsRecord (func)) return NULL;
  JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (func);

  /* Create "name" key as immediate ASCII string */
  JSValue name_key = MIST_TryNewImmediateASCII ("name", 4);

  int slot = rec_find_slot (rec, name_key);
  if (slot <= 0) return NULL;

  JSValue val = rec->slots[slot].val;
  if (!JS_IsText (val)) return NULL;
  return JS_ToCString (ctx, val);
}

static const char *const native_error_name[JS_NATIVE_ERROR_COUNT] = {
  "EvalError",
  "RangeError",
  "ReferenceError",
  "SyntaxError",
  "TypeError",
  "URIError",
  "InternalError",
  "AggregateError",
};

/* Print backtrace to stderr from a JS array of {fn, file, line, col} objects. */
void print_backtrace (JSContext *ctx, JSValue stack, const char *filename, int line_num, int col_num) {
  if (filename) {
    fprintf (stderr, "    at %s", filename);
    if (line_num != -1) fprintf (stderr, ":%d:%d", line_num, col_num);
    fputc ('\n', stderr);
  }

  int64_t n = 0;
  JS_GetLength(ctx, stack, &n);
  for (int i = 0; i < (int)n; i++) {
    JSValue fr = JS_GetPropertyNumber(ctx, stack, i);
    JSValue fn_val = JS_GetPropertyStr(ctx, fr, "fn");
    JSValue file_val = JS_GetPropertyStr(ctx, fr, "file");
    const char *fn = JS_ToCString(ctx, fn_val);
    const char *file = JS_ToCString(ctx, file_val);
    int32_t ln = 0, cn = 0;
    JSValue line_v = JS_GetPropertyStr(ctx, fr, "line");
    JSValue col_v = JS_GetPropertyStr(ctx, fr, "col");
    JS_ToInt32(ctx, &ln, line_v);
    JS_ToInt32(ctx, &cn, col_v);
    fprintf (stderr, "    at %s (%s:%d:%d)\n",
             fn ? fn : "<anonymous>", file ? file : "<unknown>", ln, cn);
    if (fn) JS_FreeCString(ctx, fn);
    if (file) JS_FreeCString(ctx, file);
  }
}

/* Print error message + backtrace to stderr, then set exception flag.
   No Error object is created — just print and flag. */
JSValue JS_ThrowError2 (JSContext *ctx, JSErrorEnum error_num, const char *fmt, va_list ap, BOOL add_backtrace) {
  char buf[256];
  vsnprintf (buf, sizeof (buf), fmt, ap);
  fprintf (stderr, "%s: %s\n", native_error_name[error_num], buf);
  if (add_backtrace) {
    JSValue stack = JS_GetStack(ctx);
    ctx->reg_current_frame = JS_NULL;
    print_backtrace (ctx, stack, NULL, 0, 0);
  }
  return JS_Throw (ctx, JS_TRUE);
}

static JSValue JS_ThrowError (JSContext *ctx, JSErrorEnum error_num, const char *fmt, va_list ap) {
  return JS_ThrowError2 (ctx, error_num, fmt, ap, TRUE);
}

JSValue __attribute__ ((format (printf, 2, 3)))
JS_ThrowSyntaxError (JSContext *ctx, const char *fmt, ...) {
  JSValue val;
  va_list ap;

  va_start (ap, fmt);
  val = JS_ThrowError (ctx, JS_SYNTAX_ERROR, fmt, ap);
  va_end (ap);
  return val;
}

JSValue __attribute__ ((format (printf, 2, 3)))
JS_ThrowTypeError (JSContext *ctx, const char *fmt, ...) {
  JSValue val;
  va_list ap;

  va_start (ap, fmt);
  val = JS_ThrowError (ctx, JS_TYPE_ERROR, fmt, ap);
  va_end (ap);
  return val;
}

JSValue __attribute__ ((format (printf, 2, 3)))
JS_ThrowReferenceError (JSContext *ctx, const char *fmt, ...) {
  JSValue val;
  va_list ap;

  va_start (ap, fmt);
  val = JS_ThrowError (ctx, JS_REFERENCE_ERROR, fmt, ap);
  va_end (ap);
  return val;
}

JSValue __attribute__ ((format (printf, 2, 3)))
JS_ThrowRangeError (JSContext *ctx, const char *fmt, ...) {
  JSValue val;
  va_list ap;

  va_start (ap, fmt);
  val = JS_ThrowError (ctx, JS_RANGE_ERROR, fmt, ap);
  va_end (ap);
  return val;
}

JSValue __attribute__ ((format (printf, 2, 3)))
JS_ThrowInternalError (JSContext *ctx, const char *fmt, ...) {
  JSValue val;
  va_list ap;

  va_start (ap, fmt);
  val = JS_ThrowError (ctx, JS_INTERNAL_ERROR, fmt, ap);
  va_end (ap);
  return val;
}

JSValue JS_ThrowOutOfMemory (JSContext *ctx) {
  /* Simplified: no re-entry guard needed with copying GC */
  JS_ThrowInternalError (ctx, "out of memory");
  return JS_EXCEPTION;
}

static JSValue JS_ThrowTypeErrorNotAnObject (JSContext *ctx) {
  return JS_ThrowTypeError (ctx, "not an object");
}

static JSValue JS_ThrowTypeErrorInvalidClass (JSContext *ctx, int class_id) {
  const char *name = ctx->class_array[class_id].class_name;
  return JS_ThrowTypeError (ctx, "%s object expected", name ? name : "unknown");
}

void JS_ThrowInterrupted (JSContext *ctx) {
  JS_ThrowInternalError (ctx, "interrupted");
}

/* Return an Object, JS_NULL or JS_EXCEPTION in case of exotic object. */
JSValue JS_GetPrototype (JSContext *ctx, JSValue obj) {
  JSValue val;
  if (JS_IsRecord (obj)) {
    JSRecord *p;
    p = JS_VALUE_GET_OBJ (obj);
    p = JS_OBJ_GET_PROTO (p);
    if (!p)
      val = JS_NULL;
    else
      val = JS_MKPTR (p);
  } else {
    /* Primitives have no prototype */
    val = JS_NULL;
  }
  return val;
}

/* Get property from object using JSRecord-based lookup */
JSValue JS_GetProperty (JSContext *ctx, JSValue obj, JSValue prop) {
  if (JS_IsNull (obj)) return JS_NULL;
  if (JS_IsException (obj)) return JS_EXCEPTION;

  if (unlikely (!JS_IsRecord (obj))) {
    if (mist_is_blob (obj)) {
      JSValue proto = ctx->class_proto[JS_CLASS_BLOB];
      if (!JS_IsNull (proto) && JS_IsRecord (proto))
        return rec_get (ctx, JS_VALUE_GET_RECORD (proto), prop);
      return JS_NULL;
    }
    return JS_NULL;
  }

  /* All objects are JSRecords now */
  JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (obj);
  return rec_get (ctx, rec, prop);
}

/* GC-SAFE: Collects keys to stack buffer before any allocation.
   Returns a JSValue array of text keys. */
JSValue JS_GetOwnPropertyNames (JSContext *ctx, JSValue obj) {
  uint32_t mask, count, i;

  if (!JS_IsRecord (obj)) {
    JS_ThrowTypeErrorNotAnObject (ctx);
    return JS_EXCEPTION;
  }

  JSRecord *rec = JS_VALUE_GET_OBJ (obj);
  mask = (uint32_t)objhdr_cap56 (rec->mist_hdr);

  /* Count text keys first */
  count = 0;
  for (i = 1; i <= mask; i++) {
    if (JS_IsText (rec->slots[i].key)) count++;
  }

  if (count == 0) return JS_NewArrayLen (ctx, 0);

  /* Root obj — JS_NewArrayLen allocates and can trigger GC, which
     moves the record.  Re-read keys from the moved record after. */
  JSGCRef obj_ref;
  JS_PushGCRef (ctx, &obj_ref);
  obj_ref.val = obj;

  JSValue arr = JS_NewArrayLen (ctx, count);
  if (JS_IsException (arr)) {
    JS_PopGCRef (ctx, &obj_ref);
    return JS_EXCEPTION;
  }

  /* Re-read record pointer after possible GC */
  rec = JS_VALUE_GET_OBJ (obj_ref.val);
  mask = (uint32_t)objhdr_cap56 (rec->mist_hdr);

  uint32_t idx = 0;
  for (i = 1; i <= mask; i++) {
    JSValue k = rec->slots[i].key;
    if (JS_IsText (k)) {
      JS_SetPropertyNumber (ctx, arr, idx++, k);
    }
  }

  JS_PopGCRef (ctx, &obj_ref);
  return arr;
}

/* Return -1 if exception,
   FALSE if the property does not exist, TRUE if it exists. If TRUE is
   returned, the property descriptor 'desc' is filled present.
   Now uses JSRecord-based lookup. */
int JS_GetOwnPropertyInternal (JSContext *ctx,
                                      JSValue *desc,
                                      JSRecord *p,
                                      JSValue prop) {
  JSRecord *rec = (JSRecord *)p;
  int slot = rec_find_slot (rec, prop);

  if (slot > 0) {
    if (desc)
      *desc = rec->slots[slot].val;
    return TRUE;
  }
  return FALSE;
}

int JS_GetOwnProperty (JSContext *ctx, JSValue *desc, JSValue obj, JSValue prop) {
  if (!JS_IsRecord (obj)) {
    JS_ThrowTypeErrorNotAnObject (ctx);
    return -1;
  }
  return JS_GetOwnPropertyInternal (ctx, desc, JS_VALUE_GET_OBJ (obj), prop);
}

/* GC-SAFE: Only calls rec_find_slot and reads prototype pointers.
   return -1 if exception otherwise TRUE or FALSE */
int JS_HasProperty (JSContext *ctx, JSValue obj, JSValue prop) {
  JSRecord *p;
  int ret;
  if (unlikely (!JS_IsRecord (obj))) return FALSE;
  p = JS_VALUE_GET_OBJ (obj);
  for (;;) {
    ret = JS_GetOwnPropertyInternal (ctx, NULL, p, prop);
    if (ret != 0) return ret;
    if (JS_IsNull (p->proto)) break;
    p = JS_VALUE_GET_RECORD (p->proto);
  }
  return FALSE;
}

static uint32_t js_string_get_length (JSValue val) {
  if (JS_IsPtr (val)) {
    void *ptr = JS_VALUE_GET_PTR (val);
    /* Check objhdr_t at offset 8 for type */
    objhdr_t hdr = *(objhdr_t *)ptr;
    if (objhdr_type (hdr) == OBJ_TEXT) {
      /* String (JSText or JSText) */
      return (uint32_t)objhdr_cap56 (hdr);
    }
    return 0;
  } else if (MIST_IsImmediateASCII (val)) {
    return MIST_GetImmediateASCIILen (val);
  } else {
    return 0;
  }
}

JSValue JS_GetPropertyValue (JSContext *ctx, JSValue this_obj, JSValue prop) {
  JSValue ret;
  uint32_t prop_tag = JS_VALUE_GET_TAG (prop);

  if (JS_IsNull (this_obj)) {
    return JS_NULL;
  }

  if (prop_tag == JS_TAG_INT) {
    int idx = JS_VALUE_GET_INT (prop);
    return JS_GetPropertyNumber (ctx, this_obj, idx);
  }

  if (prop_tag == JS_TAG_SHORT_FLOAT) {
    double d = JS_VALUE_GET_FLOAT64 (prop);
    uint32_t idx = (uint32_t)d;
    if (d != (double)idx) return JS_NULL;
    return JS_GetPropertyNumber (ctx, this_obj, idx);
  }

  /* Check for string property (immediate or heap) */
  if (JS_IsText (prop)) {
    /* Intrinsic arrays don't support string keys */
    if (JS_IsArray (this_obj)) {
      return JS_NULL;
    }
    /* Use text directly as key */
    JSValue key = js_key_from_string (ctx, prop);
    ret = JS_GetProperty (ctx, this_obj, key);
    /* key is the original text or immediate */
    return ret;
  }

  /* Handle object keys directly via objkey map */
  if (JS_IsRecord (prop)) {
    /* Intrinsic arrays don't support object keys */
    if (!JS_IsRecord (this_obj)) {
      return JS_NULL;
    }
    JSRecord *rec = JS_VALUE_GET_RECORD (this_obj);
    JSValue val = rec_get (ctx, rec, prop);
    return val;
  }

  /* Unknown type -> null */
  return JS_NULL;
}

JSValue JS_SetPropertyNumber (JSContext *js, JSValue obj, int idx, JSValue val) {
  if (JS_IsText (obj)) {
    return JS_ThrowTypeError (js, "strings are immutable");
  }
  if (!JS_IsArray (obj)) {
    return JS_ThrowInternalError (js,
                                  "cannot set with a number on a non array");
  }

  if (idx < 0) {
    return JS_ThrowRangeError (js, "array index out of bounds");
  }

  /* Root obj since js_intrinsic_array_set may trigger GC during array grow */
  JSGCRef obj_ref;
  JS_PushGCRef (js, &obj_ref);
  obj_ref.val = obj;
  if (js_intrinsic_array_set (js, &obj_ref.val, (word_t)idx, val) < 0) {
    JS_PopGCRef (js, &obj_ref);
    return JS_EXCEPTION;
  }
  JS_PopGCRef (js, &obj_ref);
  return val;
}

JSValue JS_GetPropertyNumber (JSContext *js, JSValue obj, int idx) {
  if (JS_IsArray (obj)) {
    JSArray *a = JS_VALUE_GET_ARRAY (obj);
    int len = a->len;
    if (idx < 0 || idx >= len) { return JS_NULL; }
    return a->values[idx];
  }

  if (JS_IsText (obj)) {
    uint32_t len = js_string_get_length (obj);
    if (idx < 0 || (uint32_t)idx >= len) { return JS_NULL; }
    return js_sub_string_val (js, obj, idx, idx + 1);
  }

  return JS_NULL;
}

JSValue JS_GetPropertyStr (JSContext *ctx, JSValue this_obj, const char *prop) {
  if (JS_VALUE_GET_TAG (this_obj) != JS_TAG_PTR) return JS_NULL;

  size_t len = strlen (prop);
  JSValue key;
  JSValue ret;
  JSGCRef obj_ref;

  JS_PushGCRef (ctx, &obj_ref);
  obj_ref.val = this_obj;

  key = JS_NewStringLen (ctx, prop, len);
  if (JS_IsException (key)) {
    JS_PopGCRef (ctx, &obj_ref);
    return JS_EXCEPTION;
  }
  ret = JS_GetProperty (ctx, obj_ref.val, key);
  JS_PopGCRef (ctx, &obj_ref);
  return ret;
}

/* JS_Invoke - invoke a method on an object by name */
JSValue JS_Invoke (JSContext *ctx, JSValue this_val, JSValue method, int argc, JSValue *argv) {
  JSGCRef this_ref;
  JS_PushGCRef (ctx, &this_ref);
  this_ref.val = this_val;
  JSValue func = JS_GetProperty (ctx, this_ref.val, method);
  if (JS_IsException (func)) { JS_PopGCRef (ctx, &this_ref); return JS_EXCEPTION; }
  if (!JS_IsFunction (func)) {
    JS_PopGCRef (ctx, &this_ref);
    return JS_NULL; /* Method not found or not callable */
  }
  JSValue ret = JS_Call (ctx, func, this_ref.val, argc, argv);
  JS_PopGCRef (ctx, &this_ref);
  return ret;
}

/* GC-SAFE: May trigger GC if record needs to resize */
int JS_SetProperty (JSContext *ctx, JSValue this_obj, JSValue prop, JSValue val) {
  if (!JS_IsRecord (this_obj)) {
    if (JS_IsNull (this_obj)) {
      JS_ThrowTypeError (ctx, "cannot set property of null");
    } else {
      JS_ThrowTypeError (ctx, "cannot set property on a primitive");
    }
    return -1;
  }

  /* All objects are now records - use record set */
  JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (this_obj);

  if (unlikely (obj_is_stone (rec))) {
    JS_ThrowTypeError (ctx, "object is stone");
    return -1;
  }

  /* Use a local copy that rec_set_own can update if resize happens */
  JSValue obj = this_obj;
  return rec_set_own (ctx, &obj, prop, val);
}

/* GC-SAFE: Protects this_obj and val in case key creation triggers GC */
int JS_SetPropertyStr (JSContext *ctx, JSValue this_obj, const char *prop, JSValue val) {
  /* Protect this_obj and val in case key creation triggers GC */
  JSGCRef obj_ref, val_ref;
  JS_AddGCRef (ctx, &obj_ref);
  JS_AddGCRef (ctx, &val_ref);
  obj_ref.val = this_obj;
  val_ref.val = val;

  /* Create JSValue key from string - use js_key_new for interned stone keys */
  JSValue key = js_key_new (ctx, prop);
  if (JS_IsException (key)) {
    JS_DeleteGCRef (ctx, &val_ref);
    JS_DeleteGCRef (ctx, &obj_ref);
    return -1;
  }

  int ret = JS_SetProperty (ctx, obj_ref.val, key, val_ref.val);
  JS_DeleteGCRef (ctx, &val_ref);
  JS_DeleteGCRef (ctx, &obj_ref);
  return ret;
}

/* Set property with JSValue prop/key - handles int, string, object keys */
int JS_SetPropertyValue (JSContext *ctx, JSValue this_obj, JSValue prop, JSValue val) {
  uint32_t prop_tag = JS_VALUE_GET_TAG (prop);

  if (prop_tag == JS_TAG_INT) {
    int idx = JS_VALUE_GET_INT (prop);
    JSValue ret = JS_SetPropertyNumber (ctx, this_obj, idx, val);
    if (JS_IsException (ret)) return -1;
    return 0;
  }

  if (JS_IsText (prop)) {
    JSValue key = js_key_from_string (ctx, prop);
    return JS_SetProperty (ctx, this_obj, key, val);
  }

  if (JS_IsRecord (prop)) {
    return JS_SetProperty (ctx, this_obj, prop, val);
  }

  return -1;
}

/* Property access with JSValue key - supports object keys directly */
JSValue JS_GetPropertyKey (JSContext *ctx, JSValue this_obj, JSValue key) {
  if (JS_IsRecord (key)) {
    if (!JS_IsRecord (this_obj)) return JS_NULL;
    JSRecord *rec = JS_VALUE_GET_RECORD (this_obj);
    return rec_get (ctx, rec, key);
  }

  /* For string keys, use text directly as key */
  if (JS_IsText (key)) {
    JSValue prop_key = js_key_from_string (ctx, key);
    return JS_GetProperty (ctx, this_obj, prop_key);
  }

  /* For other types, try to use the value directly as a key */
  return JS_GetProperty (ctx, this_obj, key);
}

/* rec_set_own calls rec_resize which can move the record.
   JS_SetProperty uses a local copy so the caller's JSValue is NOT updated;
   the VM must call mach_resolve_forward after store operations. */
int JS_SetPropertyKey (JSContext *ctx, JSValue this_obj, JSValue key, JSValue val) {
  if (JS_IsRecord (key)) {
    if (!JS_IsRecord (this_obj)) {
      JS_ThrowTypeError (ctx, "cannot set property on this value");
      return -1;
    }
    JSRecord *rec = JS_VALUE_GET_RECORD (this_obj);
    if (obj_is_stone (rec)) {
      JS_ThrowTypeError (ctx, "cannot modify frozen object");
      return -1;
    }
    return rec_set_own (ctx, &this_obj, key, val);
  }

  /* For string keys, use text directly as key */
  if (JS_IsText (key)) {
    JSValue prop_key = js_key_from_string (ctx, key);
    return JS_SetPropertyInternal (ctx, this_obj, prop_key, val);
  }

  /* For other types, use the key directly */
  return JS_SetPropertyInternal (ctx, this_obj, key, val);
}

/* GC-SAFE: no allocations.
   String keys pass through js_key_from_string (no interning). */
int JS_HasPropertyKey (JSContext *ctx, JSValue obj, JSValue key) {
  if (JS_IsRecord (key)) {
    if (!JS_IsRecord (obj)) return FALSE;
    JSRecord *rec = JS_VALUE_GET_RECORD (obj);
    /* Check own and prototype chain */
    while (rec) {
      if (rec_find_slot (rec, key) > 0) return TRUE;
      if (JS_IsNull (rec->proto)) break;
      rec = JS_VALUE_GET_RECORD (rec->proto);
    }
    return FALSE;
  }

  /* For string keys, use text directly as key */
  if (JS_IsText (key)) {
    JSValue prop_key = js_key_from_string (ctx, key);
    return JS_HasProperty (ctx, obj, prop_key);
  }

  /* For other types, use directly */
  return JS_HasProperty (ctx, obj, key);
}

/* GC-SAFE: Only calls rec_find_slot and modifies slots directly */
int JS_DeletePropertyKey (JSContext *ctx, JSValue obj, JSValue key) {
  if (JS_IsRecord (key)) {
    if (!JS_IsRecord (obj)) return FALSE;
    JSRecord *rec = JS_VALUE_GET_RECORD (obj);
    if (obj_is_stone (rec)) {
      JS_ThrowTypeError (ctx, "cannot modify frozen object");
      return -1;
    }
    int slot = rec_find_slot (rec, key);
    if (slot <= 0) return FALSE;
    /* Delete by marking as tombstone */
    rec->slots[slot].key = JS_EXCEPTION; /* tombstone */
    rec->slots[slot].val = JS_NULL;
    rec->len--;
    return TRUE;
  }

  /* For string keys, use text directly as key */
  if (JS_IsText (key)) {
    JSValue prop_key = js_key_from_string (ctx, key);
    return JS_DeleteProperty (ctx, obj, prop_key);
  }

  /* For other types, use directly */
  return JS_DeleteProperty (ctx, obj, key);
}

/* compute the property flags. For each flag: (JS_PROP_HAS_x forces
   it, otherwise def_flags is used)
   Note: makes assumption about the bit pattern of the flags
*/

int JS_DeleteProperty (JSContext *ctx, JSValue obj, JSValue prop) {
  JSRecord *rec;
  int slot;

  /* Arrays do not support property deletion */
  if (JS_IsArray (obj)) {
    JS_ThrowTypeError (ctx, "cannot delete array element");
    return -1;
  }

  if (JS_VALUE_GET_TAG (obj) != JS_TAG_PTR) {
    JS_ThrowTypeErrorNotAnObject (ctx);
    return -1;
  }

  rec = (JSRecord *)JS_VALUE_GET_OBJ (obj);
  if (obj_is_stone (rec)) {
    JS_ThrowTypeError (ctx, "cannot delete property of stone object");
    return -1;
  }

  slot = rec_find_slot (rec, prop);
  if (slot > 0) {
    /* Delete by marking as tombstone */
    rec->slots[slot].key = JS_EXCEPTION; /* tombstone */
    rec->slots[slot].val = JS_NULL;
    rec->len--;
    /* tombs tracking removed - not needed with copying GC */
    return TRUE;
  }
  return TRUE; /* property not found = deletion succeeded */
}

BOOL JS_IsCFunction (JSValue val, JSCFunction *func, int magic) {
  JSFunction *f;
  if (!JS_IsFunction (val)) return FALSE;
  f = JS_VALUE_GET_FUNCTION (val);
  if (f->kind == JS_FUNC_KIND_C)
    return (f->u.cfunc.c_function.generic == func
            && f->u.cfunc.magic == magic);
  else
    return FALSE;
}

void JS_SetOpaque (JSValue obj, void *opaque) {
  JSRecord *p;
  if (JS_VALUE_GET_TAG (obj) == JS_TAG_PTR) {
    p = JS_VALUE_GET_OBJ (obj);
    REC_SET_OPAQUE(p, opaque);
  }
}

/* return NULL if not an object of class class_id */
void *JS_GetOpaque (JSValue obj, JSClassID class_id) {
  JSRecord *p;
  if (JS_VALUE_GET_TAG (obj) != JS_TAG_PTR) return NULL;
  p = JS_VALUE_GET_OBJ (obj);
  if (REC_GET_CLASS_ID(p) != class_id) return NULL;
  return REC_GET_OPAQUE(p);
}

void *JS_GetOpaque2 (JSContext *ctx, JSValue obj, JSClassID class_id) {
  void *p = JS_GetOpaque (obj, class_id);
  if (unlikely (!p)) { JS_ThrowTypeErrorInvalidClass (ctx, class_id); }
  return p;
}

void *JS_GetAnyOpaque (JSValue obj, JSClassID *class_id) {
  JSRecord *p;
  if (!JS_IsRecord (obj)) {
    *class_id = 0;
    return NULL;
  }
  p = JS_VALUE_GET_OBJ (obj);
  *class_id = REC_GET_CLASS_ID(p);
  return REC_GET_OPAQUE(p);
}

int JS_ToBool (JSContext *ctx, JSValue val) {
  uint32_t tag = JS_VALUE_GET_TAG (val);

  /* Check for pointer types first (new tagging system) */
  if (JS_IsPtr (val)) {
    void *ptr = JS_VALUE_GET_PTR (val);
    objhdr_t hdr = *(objhdr_t *)ptr;
    if (objhdr_type (hdr) == OBJ_TEXT) {
      /* String (JSText or JSText) - truthy if non-empty */
      BOOL ret = objhdr_cap56 (hdr) != 0;
      return ret;
    }
    /* Objects (record, array, function) are truthy */
    return 1;
  }

  switch (tag) {
  case JS_TAG_INT:
    return JS_VALUE_GET_INT (val) != 0;
  case JS_TAG_BOOL:
    return JS_VALUE_GET_BOOL (val);
  case JS_TAG_NULL:
    return 0;
  case JS_TAG_EXCEPTION:
    return -1;
  case JS_TAG_STRING_IMM: {
    BOOL ret = MIST_GetImmediateASCIILen (val) != 0;
    return ret;
  }
  default:
    if (JS_TAG_IS_FLOAT64 (tag)) {
      double d = JS_VALUE_GET_FLOAT64 (val);
      return d != 0;  /* NaN impossible in short floats */
    } else {
      return TRUE;
    }
  }
}



/* return an exception in case of memory error. Return JS_NAN if
   invalid syntax */
/* XXX: directly use js_atod() */
JSValue js_atof (JSContext *ctx, const char *str, const char **pp, int radix, int flags) {
  const char *p, *p_start;
  int sep, is_neg;
  BOOL is_float;
  int atod_type = flags & ATOD_TYPE_MASK;
  char buf1[64], *buf;
  int i, j, len;
  BOOL buf_allocated = FALSE;
  JSValue val;
  JSATODTempMem atod_mem;

  /* optional separator between digits */
  sep = (flags & ATOD_ACCEPT_UNDERSCORES) ? '_' : 256;

  p = str;
  p_start = p;
  is_neg = 0;
  if (p[0] == '+') {
    p++;
    p_start++;
    if (!(flags & ATOD_ACCEPT_PREFIX_AFTER_SIGN)) goto no_radix_prefix;
  } else if (p[0] == '-') {
    p++;
    p_start++;
    is_neg = 1;
    if (!(flags & ATOD_ACCEPT_PREFIX_AFTER_SIGN)) goto no_radix_prefix;
  }
  if (p[0] == '0') {
    if ((p[1] == 'x' || p[1] == 'X') && (radix == 0 || radix == 16)) {
      p += 2;
      radix = 16;
    } else if ((p[1] == 'o' || p[1] == 'O') && radix == 0
               && (flags & ATOD_ACCEPT_BIN_OCT)) {
      p += 2;
      radix = 8;
    } else if ((p[1] == 'b' || p[1] == 'B') && radix == 0
               && (flags & ATOD_ACCEPT_BIN_OCT)) {
      p += 2;
      radix = 2;
    } else if ((p[1] >= '0' && p[1] <= '9') && radix == 0
               && (flags & ATOD_ACCEPT_LEGACY_OCTAL)) {
      int i;
      sep = 256;
      for (i = 1; (p[i] >= '0' && p[i] <= '7'); i++)
        continue;
      if (p[i] == '8' || p[i] == '9') goto no_prefix;
      p += 1;
      radix = 8;
    } else {
      goto no_prefix;
    }
    /* there must be a digit after the prefix */
    if (to_digit ((uint8_t)*p) >= radix) goto fail;
  no_prefix:;
  } else {
  no_radix_prefix:
    if (!(flags & ATOD_INT_ONLY) && (atod_type == ATOD_TYPE_FLOAT64)
        && strstart (p, "Infinity", &p)) {
      double d = 1.0 / 0.0;
      if (is_neg) d = -d;
      val = JS_NewFloat64 (ctx, d);
      goto done;
    }
  }
  if (radix == 0) radix = 10;
  is_float = FALSE;
  p_start = p;
  while (to_digit ((uint8_t)*p) < radix
         || (*p == sep && (radix != 10 || p != p_start + 1 || p[-1] != '0')
             && to_digit ((uint8_t)p[1]) < radix)) {
    p++;
  }
  if (!(flags & ATOD_INT_ONLY)) {
    if (*p == '.' && (p > p_start || to_digit ((uint8_t)p[1]) < radix)) {
      is_float = TRUE;
      p++;
      if (*p == sep) goto fail;
      while (to_digit ((uint8_t)*p) < radix
             || (*p == sep && to_digit ((uint8_t)p[1]) < radix))
        p++;
    }
    if (p > p_start
        && (((*p == 'e' || *p == 'E') && radix == 10)
            || ((*p == 'p' || *p == 'P')
                && (radix == 2 || radix == 8 || radix == 16)))) {
      const char *p1 = p + 1;
      is_float = TRUE;
      if (*p1 == '+') {
        p1++;
      } else if (*p1 == '-') {
        p1++;
      }
      if (is_digit ((uint8_t)*p1)) {
        p = p1 + 1;
        while (is_digit ((uint8_t)*p)
               || (*p == sep && is_digit ((uint8_t)p[1])))
          p++;
      }
    }
  }
  if (p == p_start) goto fail;

  buf = buf1;
  buf_allocated = FALSE;
  len = p - p_start;
  if (unlikely ((len + 2) > sizeof (buf1))) {
    buf = js_malloc_rt (len + 2); /* no exception raised */
    if (!buf) goto mem_error;
    buf_allocated = TRUE;
  }
  /* remove the separators and the radix prefixes */
  j = 0;
  if (is_neg) buf[j++] = '-';
  for (i = 0; i < len; i++) {
    if (p_start[i] != '_') buf[j++] = p_start[i];
  }
  buf[j] = '\0';

  if (flags & ATOD_ACCEPT_SUFFIX) {
    if (*p == 'n') {
      p++;
      atod_type = ATOD_TYPE_BIG_INT;
    } else {
      if (is_float && radix != 10) goto fail;
    }
  } else {
    if (atod_type == ATOD_TYPE_FLOAT64) {
      if (is_float && radix != 10) goto fail;
    }
  }

  switch (atod_type) {
  case ATOD_TYPE_FLOAT64: {
    double d;
    d = js_atod (buf, NULL, radix, is_float ? 0 : JS_ATOD_INT_ONLY, &atod_mem);
    /* return int or float64 */
    val = JS_NewFloat64 (ctx, d);
  } break;
  default:
    abort ();
  }

done:
  if (buf_allocated) js_free_rt (buf);
  if (pp) *pp = p;
  return val;
fail:
  val = JS_NAN;
  goto done;
mem_error:
  val = JS_ThrowOutOfMemory (ctx);
  goto done;
}

JSValue JS_ToNumber (JSContext *ctx, JSValue val) {
  uint32_t tag;
  JSValue ret;

  /* Handle pointer types (new tagging system) */
  if (JS_IsPtr (val)) {
    void *ptr = JS_VALUE_GET_PTR (val);
    objhdr_t hdr = *(objhdr_t *)ptr;
    if (objhdr_type (hdr) == OBJ_TEXT) {
      /* String */
      return JS_ThrowTypeError (ctx, "cannot convert text to a number");
    }
    /* Objects */
    return JS_ThrowTypeError (ctx, "cannot convert object to number");
  }

  tag = JS_VALUE_GET_NORM_TAG (val);
  switch (tag) {
  case JS_TAG_SHORT_FLOAT:
  case JS_TAG_INT:
  case JS_TAG_EXCEPTION:
    ret = val;
    break;
  case JS_TAG_BOOL:
  case JS_TAG_NULL:
    ret = JS_NewInt32 (ctx, JS_VALUE_GET_INT (val));
    break;
  case JS_TAG_STRING_IMM:
    return JS_ThrowTypeError (ctx, "cannot convert text to a number");
  default:
    ret = JS_NAN;
    break;
  }
  return ret;
}

static __exception int __JS_ToFloat64 (JSContext *ctx, double *pres, JSValue val) {
  double d;
  uint32_t tag;

  val = JS_ToNumber (ctx, val);
  if (JS_IsException (val)) goto fail;
  tag = JS_VALUE_GET_NORM_TAG (val);
  switch (tag) {
  case JS_TAG_INT:
    d = JS_VALUE_GET_INT (val);
    break;
  case JS_TAG_FLOAT64:
    d = JS_VALUE_GET_FLOAT64 (val);
    break;
  default:
    abort ();
  }
  *pres = d;
  return 0;
fail:
  *pres = NAN;
  return -1;
}

int JS_ToFloat64 (JSContext *ctx, double *pres, JSValue val) {
  uint32_t tag;

  tag = JS_VALUE_GET_TAG (val);
  if (tag == JS_TAG_INT) {
    *pres = JS_VALUE_GET_INT (val);
    return 0;
  } else if (JS_TAG_IS_FLOAT64 (tag)) {
    *pres = JS_VALUE_GET_FLOAT64 (val);
    return 0;
  } else {
    return __JS_ToFloat64 (ctx, pres, val);
  }
}

/* Note: the integer value is satured to 32 bits */
int JS_ToInt32Sat (JSContext *ctx, int *pres, JSValue val) {
  uint32_t tag;
  int ret;

redo:
  tag = JS_VALUE_GET_NORM_TAG (val);
  switch (tag) {
  case JS_TAG_INT:
  case JS_TAG_BOOL:
  case JS_TAG_NULL:
    ret = JS_VALUE_GET_INT (val);
    break;
  case JS_TAG_EXCEPTION:
    *pres = 0;
    return -1;
  case JS_TAG_FLOAT64: {
    double d = JS_VALUE_GET_FLOAT64 (val);
    /* NaN impossible in short floats */
    if (d < INT32_MIN)
      ret = INT32_MIN;
    else if (d > INT32_MAX)
      ret = INT32_MAX;
    else
      ret = (int)d;
  } break;
  default:
    val = JS_ToNumber (ctx, val);
    if (JS_IsException (val)) {
      *pres = 0;
      return -1;
    }
    goto redo;
  }
  *pres = ret;
  return 0;
}

int JS_ToInt32Clamp (JSContext *ctx, int *pres, JSValue val, int min, int max, int min_offset) {
  int res = JS_ToInt32Sat (ctx, pres, val);
  if (res == 0) {
    if (*pres < min) {
      *pres += min_offset;
      if (*pres < min) *pres = min;
    } else {
      if (*pres > max) *pres = max;
    }
  }
  return res;
}

int JS_ToInt64Sat (JSContext *ctx, int64_t *pres, JSValue val) {
  uint32_t tag;

redo:
  tag = JS_VALUE_GET_NORM_TAG (val);
  switch (tag) {
  case JS_TAG_INT:
  case JS_TAG_BOOL:
  case JS_TAG_NULL:
    *pres = JS_VALUE_GET_INT (val);
    return 0;
  case JS_TAG_EXCEPTION:
    *pres = 0;
    return -1;
  case JS_TAG_FLOAT64: {
    double d = JS_VALUE_GET_FLOAT64 (val);
    /* NaN impossible in short floats */
    if (d < INT64_MIN)
      *pres = INT64_MIN;
    else if (d >= 0x1p63) /* must use INT64_MAX + 1 because INT64_MAX cannot
                             be exactly represented as a double */
      *pres = INT64_MAX;
    else
      *pres = (int64_t)d;
  }
    return 0;
  default:
    val = JS_ToNumber (ctx, val);
    if (JS_IsException (val)) {
      *pres = 0;
      return -1;
    }
    goto redo;
  }
}

int JS_ToInt64Clamp (JSContext *ctx, int64_t *pres, JSValue val, int64_t min, int64_t max, int64_t neg_offset) {
  int res = JS_ToInt64Sat (ctx, pres, val);
  if (res == 0) {
    if (*pres < 0) *pres += neg_offset;
    if (*pres < min)
      *pres = min;
    else if (*pres > max)
      *pres = max;
  }
  return res;
}

/* Same as JS_ToInt32() but with a 64 bit result. Return (<0, 0)
   in case of exception */
int JS_ToInt64 (JSContext *ctx, int64_t *pres, JSValue val) {
  uint32_t tag;
  int64_t ret;

redo:
  tag = JS_VALUE_GET_NORM_TAG (val);
  switch (tag) {
  case JS_TAG_INT:
  case JS_TAG_BOOL:
  case JS_TAG_NULL:
    ret = JS_VALUE_GET_INT (val);
    break;
  case JS_TAG_FLOAT64: {
    JSFloat64Union u;
    double d;
    int e;
    d = JS_VALUE_GET_FLOAT64 (val);
    u.d = d;
    /* we avoid doing fmod(x, 2^64) */
    e = (u.u64 >> 52) & 0x7ff;
    if (likely (e <= (1023 + 62))) {
      /* fast case */
      ret = (int64_t)d;
    } else if (e <= (1023 + 62 + 53)) {
      uint64_t v;
      /* remainder modulo 2^64 */
      v = (u.u64 & (((uint64_t)1 << 52) - 1)) | ((uint64_t)1 << 52);
      ret = v << ((e - 1023) - 52);
      /* take the sign into account */
      if (u.u64 >> 63) ret = -ret;
    } else {
      ret = 0; /* also handles NaN and +inf */
    }
  } break;
  default:
    val = JS_ToNumber (ctx, val);
    if (JS_IsException (val)) {
      *pres = 0;
      return -1;
    }
    goto redo;
  }
  *pres = ret;
  return 0;
}

/* return (<0, 0) in case of exception */
int JS_ToInt32 (JSContext *ctx, int32_t *pres, JSValue val) {
  uint32_t tag;
  int32_t ret;

redo:
  tag = JS_VALUE_GET_NORM_TAG (val);
  switch (tag) {
  case JS_TAG_INT:
  case JS_TAG_BOOL:
  case JS_TAG_NULL:
    ret = JS_VALUE_GET_INT (val);
    break;
  case JS_TAG_FLOAT64: {
    JSFloat64Union u;
    double d;
    int e;
    d = JS_VALUE_GET_FLOAT64 (val);
    u.d = d;
    /* we avoid doing fmod(x, 2^32) */
    e = (u.u64 >> 52) & 0x7ff;
    if (likely (e <= (1023 + 30))) {
      /* fast case */
      ret = (int32_t)d;
    } else if (e <= (1023 + 30 + 53)) {
      uint64_t v;
      /* remainder modulo 2^32 */
      v = (u.u64 & (((uint64_t)1 << 52) - 1)) | ((uint64_t)1 << 52);
      v = v << ((e - 1023) - 52 + 32);
      ret = v >> 32;
      /* take the sign into account */
      if (u.u64 >> 63) ret = -ret;
    } else {
      ret = 0; /* also handles NaN and +inf */
    }
  } break;
  default:
    *pres = 0;
    return -1;
    val = JS_ToNumber (ctx, val);
    if (JS_IsException (val)) {
      *pres = 0;
      return -1;
    }
    goto redo;
  }
  *pres = ret;
  return 0;
}

#define MAX_SAFE_INTEGER (((int64_t)1 << 53) - 1)

/* convert a value to a length between 0 and MAX_SAFE_INTEGER.
   return -1 for exception */
static __exception int JS_ToLength (JSContext *ctx, int64_t *plen, JSValue val) {
  int res = JS_ToInt64Clamp (ctx, plen, val, 0, MAX_SAFE_INTEGER, 0);
  return res;
}

static JSValue js_dtoa2 (JSContext *ctx, double d, int radix, int n_digits, int flags) {
  char static_buf[128], *buf, *tmp_buf;
  int len, len_max;
  JSValue res;
  JSDTOATempMem dtoa_mem;
  len_max = js_dtoa_max_len (d, radix, n_digits, flags);

  /* longer buffer may be used if radix != 10 */
  if (len_max > sizeof (static_buf) - 1) {
    tmp_buf = js_malloc (ctx, len_max + 1);
    if (!tmp_buf) return JS_EXCEPTION;
    buf = tmp_buf;
  } else {
    tmp_buf = NULL;
    buf = static_buf;
  }
  len = js_dtoa (buf, d, radix, n_digits, flags, &dtoa_mem);
  res = js_new_string8_len (ctx, buf, len);
  js_free (ctx, tmp_buf);
  return res;
}

JSValue JS_ToString (JSContext *ctx, JSValue val) {
  uint32_t tag;
  char buf[32];

  /* Handle pointer types (new tagging system) */
  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) {
      /* String - return as-is */
      return val;
    }
    /* Objects (record, array, function) */
    return JS_KEY_true;
  }

  tag = JS_VALUE_GET_NORM_TAG (val);
  switch (tag) {
  case JS_TAG_STRING_IMM:
    return val;
  case JS_TAG_INT: {
    size_t len;
    len = i32toa (buf, JS_VALUE_GET_INT (val));
    return js_new_string8_len (ctx, buf, len);
  } break;
  case JS_TAG_BOOL:
    return JS_VALUE_GET_BOOL (val) ? JS_KEY_true : JS_KEY_false;
  case JS_TAG_NULL:
    return JS_KEY_null;
  case JS_TAG_EXCEPTION:
    return JS_EXCEPTION;
  case JS_TAG_SHORT_FLOAT:
    return js_dtoa2 (ctx, JS_VALUE_GET_FLOAT64 (val), 10, 0, JS_DTOA_FORMAT_FREE);
  default:
    return js_new_string8 (ctx, "[unsupported type]");
  }
}

static JSValue JS_ToStringCheckObject (JSContext *ctx, JSValue val) {
  uint32_t tag = JS_VALUE_GET_TAG (val);
  if (tag == JS_TAG_NULL) return JS_ThrowTypeError (ctx, "null is forbidden");
  return JS_ToString (ctx, val);
}

static JSValue JS_ToQuotedString (JSContext *ctx, JSValue val1) {
  int i, len;
  uint32_t c;
  JSText *b;
  char buf[16];
  JSGCRef val_ref;

  JSValue val = JS_ToStringCheckObject (ctx, val1);
  if (JS_IsException (val)) return val;

  /* Root val — pretext_init/pretext_putc allocate and can trigger GC,
     which would move the heap string val points to */
  JS_PushGCRef (ctx, &val_ref);
  val_ref.val = val;

  /* Use js_string_value_len to handle both immediate and heap strings */
  len = js_string_value_len (val_ref.val);

  b = pretext_init (ctx, len + 2);
  if (!b) goto fail;

  b = pretext_putc (ctx, b, '\"');
  if (!b) goto fail;
  for (i = 0; i < len; i++) {
    c = js_string_value_get (val_ref.val, i);
    switch (c) {
    case '\t':
      c = 't';
      goto quote;
    case '\r':
      c = 'r';
      goto quote;
    case '\n':
      c = 'n';
      goto quote;
    case '\b':
      c = 'b';
      goto quote;
    case '\f':
      c = 'f';
      goto quote;
    case '\"':
    case '\\':
    quote:
      b = pretext_putc (ctx, b, '\\');
      if (!b) goto fail;
      b = pretext_putc (ctx, b, c);
      if (!b) goto fail;
      break;
    default:
      if (c < 32 || is_surrogate (c)) {
        snprintf (buf, sizeof (buf), "\\u%04x", c);
        b = pretext_puts8 (ctx, b, buf);
        if (!b) goto fail;
      } else {
        b = pretext_putc (ctx, b, c);
        if (!b) goto fail;
      }
      break;
    }
  }
  b = pretext_putc (ctx, b, '\"');
  if (!b) goto fail;
  JS_PopGCRef (ctx, &val_ref);
  return pretext_end (ctx, b);
fail:
  JS_PopGCRef (ctx, &val_ref);
  return JS_EXCEPTION;
}

#define JS_PRINT_MAX_DEPTH 8

typedef struct {
  JSRuntime *rt;
  JSContext *ctx; /* may be NULL */
  JSPrintValueOptions options;
  JSPrintValueWrite *write_func;
  void *write_opaque;
  int level;
  JSRecord *print_stack[JS_PRINT_MAX_DEPTH]; /* level values */
} JSPrintValueState;

static void js_print_value (JSPrintValueState *s, JSValue val);

static void js_putc (JSPrintValueState *s, char c) {
  s->write_func (s->write_opaque, &c, 1);
}

static void js_puts (JSPrintValueState *s, const char *str) {
  s->write_func (s->write_opaque, str, strlen (str));
}

static void __attribute__ ((format (printf, 2, 3)))
js_printf (JSPrintValueState *s, const char *fmt, ...) {
  va_list ap;
  char buf[256];

  va_start (ap, fmt);
  vsnprintf (buf, sizeof (buf), fmt, ap);
  va_end (ap);
  s->write_func (s->write_opaque, buf, strlen (buf));
}

static void js_print_float64 (JSPrintValueState *s, double d) {
  JSDTOATempMem dtoa_mem;
  char buf[32];
  int len;
  len = js_dtoa (buf, d, 10, 0, JS_DTOA_FORMAT_FREE | JS_DTOA_MINUS_ZERO, &dtoa_mem);
  s->write_func (s->write_opaque, buf, len);
}

static void js_dump_char (JSPrintValueState *s, int c, int sep) {
  if (c == sep || c == '\\') {
    js_putc (s, '\\');
    js_putc (s, c);
  } else if (c >= ' ' && c <= 126) {
    js_putc (s, c);
  } else if (c == '\n') {
    js_putc (s, '\\');
    js_putc (s, 'n');
  } else {
    js_printf (s, "\\u%04x", c);
  }
}

static void js_print_string_rec (JSPrintValueState *s, JSValue val, int sep, uint32_t pos) {
  if (MIST_IsImmediateASCII (val)) {
    /* Immediate ASCII string */
    int len = MIST_GetImmediateASCIILen (val);
    if (pos < s->options.max_string_length) {
      uint32_t i, l;
      l = min_uint32 (len, s->options.max_string_length - pos);
      for (i = 0; i < l; i++) {
        js_dump_char (s, MIST_GetImmediateASCIIChar (val, i), sep);
      }
    }
  } else if (JS_IsPtr (val) && objhdr_type (*(objhdr_t *)JS_VALUE_GET_PTR (val)) == OBJ_TEXT) {
    /* Heap text (JSText) */
    JSText *p = (JSText *)JS_VALUE_GET_PTR (val);
    uint32_t i, len;
    if (pos < s->options.max_string_length) {
      len = min_uint32 ((uint32_t)JSText_len (p), s->options.max_string_length - pos);
      for (i = 0; i < len; i++) {
        js_dump_char (s, string_get (p, i), sep);
      }
    }
  } else {
    js_printf (s, "<invalid string tag %d>", (int)JS_VALUE_GET_TAG (val));
  }
}

static void js_print_string (JSPrintValueState *s, JSValue val) {
  int sep = '\"';
  js_putc (s, sep);
  js_print_string_rec (s, val, sep, 0);
  js_putc (s, sep);
  if (js_string_get_length (val) > s->options.max_string_length) {
    uint32_t n = js_string_get_length (val) - s->options.max_string_length;
    js_printf (s, "... %u more character%s", n, n > 1 ? "s" : "");
  }
}


static void js_print_value (JSPrintValueState *s, JSValue val) {
  uint32_t tag = JS_VALUE_GET_NORM_TAG (val);
  const char *str;

  /* Handle pointer types first (new tagging system) */
  if (JS_IsPtr (val)) {
    void *ptr = JS_VALUE_GET_PTR (val);
    /* Check objhdr_t at offset 8 for type */
    objhdr_t hdr = *(objhdr_t *)ptr;
    uint8_t mist_type = objhdr_type (hdr);

    if (mist_type == OBJ_TEXT) {
      /* String (JSText or JSText) */
      js_print_string (s, val);
      return;
    }
    return;
  }

  switch (tag) {
  case JS_TAG_INT:
    js_printf (s, "%d", JS_VALUE_GET_INT (val));
    break;
  case JS_TAG_BOOL:
    if (JS_VALUE_GET_BOOL (val))
      str = "true";
    else
      str = "false";
    goto print_str;
  case JS_TAG_NULL:
    str = "null";
    goto print_str;
  case JS_TAG_EXCEPTION:
    str = "exception";
    goto print_str;
  print_str:
    js_puts (s, str);
    break;
  case JS_TAG_SHORT_FLOAT:
    js_print_float64 (s, JS_VALUE_GET_FLOAT64 (val));
    break;
  case JS_TAG_STRING_IMM:
    js_print_string (s, val);
    break;
  default:
    js_printf (s, "[unknown tag %d]", tag);
    break;
  }
}

void JS_PrintValueSetDefaultOptions (JSPrintValueOptions *options) {
  memset (options, 0, sizeof (*options));
  options->max_depth = 2;
  options->max_string_length = 1000;
  options->max_item_count = 100;
}

static void JS_PrintValueInternal (JSRuntime *rt, JSContext *ctx, JSPrintValueWrite *write_func, void *write_opaque, JSValue val, const JSPrintValueOptions *options) {
  JSPrintValueState ss, *s = &ss;
  if (options)
    s->options = *options;
  else
    JS_PrintValueSetDefaultOptions (&s->options);
  if (s->options.max_depth <= 0)
    s->options.max_depth = JS_PRINT_MAX_DEPTH;
  else
    s->options.max_depth = min_int (s->options.max_depth, JS_PRINT_MAX_DEPTH);
  if (s->options.max_string_length == 0)
    s->options.max_string_length = UINT32_MAX;
  if (s->options.max_item_count == 0) s->options.max_item_count = UINT32_MAX;
  s->rt = rt;
  s->ctx = ctx;
  s->write_func = write_func;
  s->write_opaque = write_opaque;
  s->level = 0;
  js_print_value (s, val);
}

void JS_PrintValueRT (JSRuntime *rt, JSPrintValueWrite *write_func, void *write_opaque, JSValue val, const JSPrintValueOptions *options) {
  JS_PrintValueInternal (rt, NULL, write_func, write_opaque, val, options);
}

void JS_PrintValue (JSContext *ctx, JSPrintValueWrite *write_func, void *write_opaque, JSValue val, const JSPrintValueOptions *options) {
  JS_PrintValueInternal (ctx->rt, ctx, write_func, write_opaque, val, options);
}

void js_dump_value_write (void *opaque, const char *buf, size_t len) {
  FILE *fo = opaque;
  fwrite (buf, 1, len, fo);
}

/* print_atom removed - atoms no longer used */

__maybe_unused void JS_DumpValue (JSContext *ctx, const char *str, JSValue val) {
  printf ("%s=", str);
  JS_PrintValue (ctx, js_dump_value_write, stdout, val, NULL);
  printf ("\n");
}

__maybe_unused void JS_DumpObjectHeader (JSRuntime *rt) {
  printf ("%14s %4s %4s %14s %s\n", "ADDRESS", "REFS", "SHRF", "PROTO", "CONTENT");
}

/* for debug only: dump an object without side effect */
__maybe_unused void JS_DumpObject (JSRuntime *rt, JSRecord *rec) {
  JSPrintValueOptions options;

  printf ("%14p ", (void *)rec);
  /* Print prototype from JSRecord */
  if (!JS_IsNull (rec->proto)) {
    printf ("%14p ", JS_VALUE_GET_PTR (rec->proto));
  } else {
    printf ("%14s ", "-");
  }

  JS_PrintValueSetDefaultOptions (&options);
  options.max_depth = 1;
  options.show_hidden = TRUE;
  options.raw_dump = TRUE;
  JS_PrintValueRT (rt, js_dump_value_write, stdout, JS_MKPTR (rec), &options);

  printf ("\n");
}

__maybe_unused void JS_DumpGCObject (JSRuntime *rt,
                                            objhdr_t *p) {
  if (objhdr_type (*p) == OBJ_RECORD) {
    JS_DumpObject (rt, (JSRecord *)p);
  } else {
    switch (objhdr_type (*p)) {
    case OBJ_ARRAY:
      printf ("[array]");
      break;
    case OBJ_RECORD:
      printf ("[record]");
      break;
    default:
      printf ("[unknown %d]", objhdr_type (*p));
      break;
    }
    printf ("\n");
  }
}

/* Simplified equality: no NaN (becomes null), no coercion, no SameValue distinction */
BOOL js_strict_eq (JSContext *ctx, JSValue op1, JSValue op2) {
  /* Fast path: identical values */
  if (op1 == op2) return TRUE;

  int tag1 = JS_VALUE_GET_NORM_TAG (op1);
  int tag2 = JS_VALUE_GET_NORM_TAG (op2);

  /* Different types are never equal (no coercion) */
  /* Special case: INT and FLOAT can be equal */
  if (tag1 != tag2) {
    if (!((tag1 == JS_TAG_INT || tag1 == JS_TAG_FLOAT64) &&
          (tag2 == JS_TAG_INT || tag2 == JS_TAG_FLOAT64)))
      return FALSE;
  }

  switch (tag1) {
  case JS_TAG_INT:
  case JS_TAG_FLOAT64: {
    /* Numbers: unpack and compare */
    double d1 = (tag1 == JS_TAG_INT) ? (double)JS_VALUE_GET_INT (op1)
                                     : JS_VALUE_GET_FLOAT64 (op1);
    double d2 = (tag2 == JS_TAG_INT) ? (double)JS_VALUE_GET_INT (op2)
                                     : JS_VALUE_GET_FLOAT64 (op2);
    return d1 == d2;
  }
  case JS_TAG_STRING_IMM:
    /* Immediate text vs immediate text (handled by op1 == op2 fast path) */
    /* or vs heap text */
    if (JS_IsText (op2))
      return js_string_compare_value (ctx, op1, op2, TRUE) == 0;
    return FALSE;
  case JS_TAG_PTR:
    /* Heap text vs heap text or vs immediate text */
    if (JS_IsText (op1) && JS_IsText (op2))
      return js_string_compare_value (ctx, op1, op2, TRUE) == 0;
    /* Records/objects: pointer equality (op1 == op2 handles same object) */
    return FALSE;  /* Different objects */
  case JS_TAG_BOOL:
  case JS_TAG_NULL:
    /* Already handled by op1 == op2 fast path */
    return FALSE;
  default:
    return FALSE;
  }
}

BOOL JS_StrictEq (JSContext *ctx, JSValue op1, JSValue op2) {
  return js_strict_eq (ctx, op1, op2);
}

static JSValue js_throw_type_error (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  return JS_ThrowTypeError (ctx, "invalid property access");
}

JSValue js_call_c_function (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv) {
  JSFunction *f = JS_VALUE_GET_FUNCTION (func_obj);
  JSCFunctionEnum cproto = f->u.cfunc.cproto;
  JSCFunctionType func = f->u.cfunc.c_function;
  JSValue ret_val;

  /* Auto-root argv: copy to C stack and register as GC root.
     GC will update arg_copy[] in-place, so C functions can read
     argv[n] across allocation points without manual guarding. */
  JSValue arg_copy[argc > 0 ? argc : 1];
  if (argc > 0)
    memcpy (arg_copy, argv, argc * sizeof (JSValue));

  CCallRoot root = { arg_copy, argc, ctx->c_call_root };
  ctx->c_call_root = &root;

  if (unlikely (ctx->trace_hook) && (ctx->trace_type & JS_HOOK_CALL)) {
    js_debug dbg;
    js_debug_info (ctx, func_obj, &dbg);
    ctx->trace_hook (ctx, JS_HOOK_CALL, &dbg, ctx->trace_data);
  }

#ifdef VALIDATE_GC
  uint8_t *pre_heap_base = ctx->heap_base;
#endif

  switch (cproto) {
  case JS_CFUNC_generic:
    ret_val = func.generic (ctx, this_obj, argc, arg_copy);
    break;
  case JS_CFUNC_generic_magic:
    ret_val = func.generic_magic (ctx, this_obj, argc, arg_copy, f->u.cfunc.magic);
    break;
  case JS_CFUNC_f_f: {
    double d1;
    if (unlikely (JS_ToFloat64 (ctx, &d1, arg_copy[0]))) {
      ret_val = JS_EXCEPTION;
      break;
    }
    ret_val = JS_NewFloat64 (ctx, func.f_f (d1));
  } break;
  case JS_CFUNC_f_f_f: {
    double d1, d2;
    if (unlikely (JS_ToFloat64 (ctx, &d1, arg_copy[0]))) {
      ret_val = JS_EXCEPTION;
      break;
    }
    if (unlikely (JS_ToFloat64 (ctx, &d2, arg_copy[1]))) {
      ret_val = JS_EXCEPTION;
      break;
    }
    ret_val = JS_NewFloat64 (ctx, func.f_f_f (d1, d2));
  } break;
  /* Fixed-arity fast paths — args passed by value at dispatch time */
  case JS_CFUNC_0:
    ret_val = func.f0 (ctx, this_obj);
    break;
  case JS_CFUNC_1:
    ret_val = func.f1 (ctx, this_obj, arg_copy[0]);
    break;
  case JS_CFUNC_2:
    ret_val = func.f2 (ctx, this_obj, arg_copy[0], arg_copy[1]);
    break;
  case JS_CFUNC_3:
    ret_val = func.f3 (ctx, this_obj, arg_copy[0], arg_copy[1], arg_copy[2]);
    break;
  case JS_CFUNC_4:
    ret_val = func.f4 (ctx, this_obj, arg_copy[0], arg_copy[1], arg_copy[2], arg_copy[3]);
    break;
  /* Pure functions (no this_val) */
  case JS_CFUNC_PURE:
    ret_val = func.pure (ctx, argc, arg_copy);
    break;
  case JS_CFUNC_PURE_0:
    ret_val = func.pure0 (ctx);
    break;
  case JS_CFUNC_PURE_1:
    ret_val = func.pure1 (ctx, arg_copy[0]);
    break;
  case JS_CFUNC_PURE_2:
    ret_val = func.pure2 (ctx, arg_copy[0], arg_copy[1]);
    break;
  case JS_CFUNC_PURE_3:
    ret_val = func.pure3 (ctx, arg_copy[0], arg_copy[1], arg_copy[2]);
    break;
  case JS_CFUNC_PURE_4:
    ret_val = func.pure4 (ctx, arg_copy[0], arg_copy[1], arg_copy[2], arg_copy[3]);
    break;
  default:
    abort ();
  }

#ifdef VALIDATE_GC
  if (ctx->heap_base != pre_heap_base && JS_IsPtr (ret_val)) {
    void *rp = JS_VALUE_GET_PTR (ret_val);
    if (!is_ct_ptr (ctx, rp) &&
        ((uint8_t *)rp < ctx->heap_base || (uint8_t *)rp >= ctx->heap_free)) {
      /* Note: f is stale after GC (func_obj was passed by value), so we
         cannot read f->name here. Just report the pointer. */
      fprintf (stderr, "VALIDATE_GC: C function returned stale ptr=%p "
               "heap=[%p,%p) after GC\n", rp,
               (void *)ctx->heap_base, (void *)ctx->heap_free);
      fflush (stderr);
    }
  }
#endif

  ctx->c_call_root = root.prev;

  if (unlikely (ctx->trace_hook) && (ctx->trace_type & JS_HOOK_RET))
    ctx->trace_hook (ctx, JS_HOOK_RET, NULL, ctx->trace_data);

  return ret_val;
}

/* Central function dispatcher — replaces the old bytecode VM entry point.
   Now dispatches to MACH (register VM), MCODE, or C functions. */
JSValue JS_CallInternal (JSContext *ctx, JSValue func_obj, JSValue this_obj,
                         int argc, JSValue *argv, int flags) {
  (void)flags;
  if (js_poll_interrupts (ctx)) return JS_EXCEPTION;
  if (!JS_IsFunction (func_obj))
    return JS_ThrowTypeError (ctx, "not a function");
  JSFunction *f = JS_VALUE_GET_FUNCTION (func_obj);
  switch (f->kind) {
  case JS_FUNC_KIND_C:
  case JS_FUNC_KIND_C_DATA:
    return js_call_c_function (ctx, func_obj, this_obj, argc, argv);
  case JS_FUNC_KIND_REGISTER:
    return JS_CallRegisterVM (ctx, f->u.reg.code, this_obj, argc, argv,
                              f->u.reg.env_record, f->u.reg.outer_frame);
  default:
    return JS_ThrowTypeError (ctx, "not a function");
  }
}

JSValue JS_Call (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv) {
  if (js_poll_interrupts (ctx)) return JS_EXCEPTION;
  if (unlikely (!JS_IsFunction (func_obj)))
    return JS_ThrowTypeError (ctx, "not a function");
  JSFunction *f = JS_VALUE_GET_FUNCTION (func_obj);
  if (unlikely (f->length >= 0 && argc > f->length)) {
    char buf[KEY_GET_STR_BUF_SIZE];
    return JS_ThrowTypeError (ctx, "too many arguments for %s: expected %d, got %d",
      JS_KeyGetStr (ctx, buf, KEY_GET_STR_BUF_SIZE, f->name), f->length, argc);
  }
  switch (f->kind) {
  case JS_FUNC_KIND_C:
    return js_call_c_function (ctx, func_obj, this_obj, argc, argv);
  case JS_FUNC_KIND_REGISTER:
    return JS_CallRegisterVM (ctx, f->u.reg.code, this_obj, argc, argv,
                              f->u.reg.env_record, f->u.reg.outer_frame);
  default:
    return JS_ThrowTypeError (ctx, "not a function");
  }
}

/*******************************************************************/
/* runtime functions & objects */

int check_exception_free (JSContext *ctx, JSValue obj) {
  return JS_IsException (obj);
}

JSValue find_key (JSContext *ctx, const char *name) {
  /* Create an interned JSValue key from C string */
  return js_key_new (ctx, name);
}

static int JS_InstantiateFunctionListItem (JSContext *ctx, JSValue *obj_ptr, JSValue key, const JSCFunctionListEntry *e) {
  JSValue val;

  switch (e->def_type) {
  case JS_DEF_ALIAS: /* using autoinit for aliases is not safe */
  {
    JSValue key1 = find_key (ctx, e->u.alias.name);
    switch (e->u.alias.base) {
    case -1:
      val = JS_GetProperty (ctx, *obj_ptr, key1);
      break;
    case 0:
      val = JS_GetProperty (ctx, ctx->global_obj, key1);
      break;
    default:
      abort ();
    }
    /* key1 is interned, no need to free */
  } break;
  case JS_DEF_CFUNC:
    val = JS_NewCFunction2 (ctx, e->u.func.cfunc.generic, e->name, e->u.func.length, e->u.func.cproto, e->magic);
    break;
  case JS_DEF_PROP_INT32:
    val = JS_NewInt32 (ctx, e->u.i32);
    break;
  case JS_DEF_PROP_INT64:
    val = JS_NewInt64 (ctx, e->u.i64);
    break;
  case JS_DEF_PROP_DOUBLE:
    val = __JS_NewFloat64 (ctx, e->u.f64);
    break;
  case JS_DEF_PROP_UNDEFINED:
    val = JS_NULL;
    break;
  case JS_DEF_PROP_STRING:
    val = JS_NewAtomString (ctx, e->u.str);
    break;

  case JS_DEF_OBJECT:
    val = JS_NewObject (ctx);
    if (JS_IsException (val)) return -1;
    JS_SetPropertyFunctionList (ctx, val, e->u.prop_list.tab, e->u.prop_list.len);
    break;
  default:
    abort ();
  }
  JS_SetPropertyInternal (ctx, *obj_ptr, key, val);
  return 0;
}

int JS_SetPropertyFunctionList (JSContext *ctx, JSValue obj, const JSCFunctionListEntry *tab, int len) {
  int i, ret;

  /* Root obj since allocations in the loop can trigger GC */
  JSGCRef obj_ref;
  obj_ref.val = obj;
  obj_ref.prev = ctx->last_gc_ref;
  ctx->last_gc_ref = &obj_ref;

  for (i = 0; i < len; i++) {
    const JSCFunctionListEntry *e = &tab[i];
    JSValue key = find_key (ctx, e->name);
    if (JS_IsNull (key)) {
      ctx->last_gc_ref = obj_ref.prev;
      return -1;
    }
    ret = JS_InstantiateFunctionListItem (ctx, &obj_ref.val, key, e);
    /* key is interned, no need to free */
    if (ret) {
      ctx->last_gc_ref = obj_ref.prev;
      return -1;
    }
  }
  ctx->last_gc_ref = obj_ref.prev;
  return 0;
}

__exception int js_get_length32 (JSContext *ctx, uint32_t *pres, JSValue obj) {
  int tag = JS_VALUE_GET_TAG (obj);

  /* Fast path for intrinsic arrays */
  if (JS_IsArray (obj)) {
    JSArray *arr = JS_VALUE_GET_ARRAY (obj);
    *pres = arr->len;
    return 0;
  }

  if (JS_IsFunction (obj)) {
    JSFunction *fn = JS_VALUE_GET_FUNCTION (obj);
    *pres = fn->length;
    return 0;
  }

  if (mist_is_blob (obj)) {
    JSBlob *bd = (JSBlob *)chase (obj);
    *pres = (uint32_t)bd->length;
    return 0;
  }

  if (tag == JS_TAG_STRING || tag == JS_TAG_STRING_IMM) {
    JSText *p = JS_VALUE_GET_STRING (obj);
    *pres = JSText_len (p);
    return 0;
  }

  JSValue len_val;
  len_val = JS_GetProperty (ctx, obj, JS_KEY_length);
  if (JS_IsException (len_val)) {
    *pres = 0;
    return -1;
  }
  return JS_ToUint32 (ctx, pres, len_val);
}

__exception int js_get_length64 (JSContext *ctx, int64_t *pres, JSValue obj) {
  /* Fast path for intrinsic arrays */
  if (JS_IsArray (obj)) {
    JSArray *arr = JS_VALUE_GET_ARRAY (obj);
    *pres = arr->len;
    return 0;
  }
  JSValue len_val;
  len_val = JS_GetProperty (ctx, obj, JS_KEY_length);
  if (JS_IsException (len_val)) {
    *pres = 0;
    return -1;
  }
  return JS_ToLength (ctx, pres, len_val);
}

int JS_GetLength (JSContext *ctx, JSValue obj, int64_t *pres) {
  return js_get_length64 (ctx, pres, obj);
}

void free_arg_list (JSContext *ctx, JSValue *tab, uint32_t len) {
  (void)ctx;
  (void)len;
  js_free_rt(tab);
}

/* XXX: should use ValueArray */
JSValue *build_arg_list (JSContext *ctx, uint32_t *plen, JSValue *parray_arg) {
  uint32_t len, i;
  JSValue *tab;

  /* Fast path for intrinsic arrays */
  if (JS_IsArray (*parray_arg)) {
    JSArray *arr = JS_VALUE_GET_ARRAY (*parray_arg);
    len = arr->len;
    if (len > JS_MAX_LOCAL_VARS) {
      JS_ThrowRangeError (
        ctx, "too many arguments in function call (only %d allowed)", JS_MAX_LOCAL_VARS);
      return NULL;
    }
    tab = js_mallocz_rt (sizeof (tab[0]) * max_uint32 (1, len));
    if (!tab) return NULL;
    arr = JS_VALUE_GET_ARRAY (*parray_arg);
    for (i = 0; i < len; i++) {
      tab[i] = arr->values[i];
    }
    *plen = len;
    return tab;
  }

  JS_ThrowTypeError (ctx, "not an array");
  return NULL;
}


static JSValue js_array_includes (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  JSValue found = js_cell_array_find (ctx, this_val, argc, argv);
  if (JS_IsException (found)) return JS_EXCEPTION;

  if (JS_IsNull (found)) return JS_NewBool (ctx, FALSE);
  return JS_NewBool (ctx, TRUE);
}

/* return < 0 if exception or TRUE/FALSE */
static int js_is_regexp (JSContext *ctx, JSValue obj);

/* RegExp */

void js_regexp_finalizer (JSRuntime *rt, JSValue val) {
  JSRegExp *re = JS_GetOpaque (val, JS_CLASS_REGEXP);
  if (re) {
    js_free_rt (re->pattern);
    js_free_rt (re->bytecode);
    js_free_rt (re);
  }
  (void)rt;
}

/* create a string containing the RegExp bytecode */
JSValue js_compile_regexp (JSContext *ctx, JSValue pattern, JSValue flags) {
  const char *str;
  int re_flags, mask;
  uint8_t *re_bytecode_buf;
  size_t i, len;
  int re_bytecode_len;
  JSValue ret;
  char error_msg[64];

  re_flags = 0;
  if (!JS_IsNull (flags)) {
    str = JS_ToCStringLen (ctx, &len, flags);
    if (!str) return JS_EXCEPTION;
    /* XXX: re_flags = LRE_FLAG_OCTAL unless strict mode? */
    for (i = 0; i < len; i++) {
      switch (str[i]) {
      case 'd':
        mask = LRE_FLAG_INDICES;
        break;
      case 'g':
        mask = LRE_FLAG_GLOBAL;
        break;
      case 'i':
        mask = LRE_FLAG_IGNORECASE;
        break;
      case 'm':
        mask = LRE_FLAG_MULTILINE;
        break;
      case 's':
        mask = LRE_FLAG_DOTALL;
        break;
      case 'u':
        mask = LRE_FLAG_UNICODE;
        break;
      case 'v':
        mask = LRE_FLAG_UNICODE_SETS;
        break;
      case 'y':
        mask = LRE_FLAG_STICKY;
        break;
      default:
        goto bad_flags;
      }
      if ((re_flags & mask) != 0) {
      bad_flags:
        JS_FreeCString (ctx, str);
        goto bad_flags1;
      }
      re_flags |= mask;
    }
    JS_FreeCString (ctx, str);
  }

  /* 'u' and 'v' cannot be both set */
  if ((re_flags & LRE_FLAG_UNICODE_SETS) && (re_flags & LRE_FLAG_UNICODE)) {
  bad_flags1:
    return JS_ThrowSyntaxError (ctx, "invalid regular expression flags");
  }

  str = JS_ToCStringLen2 (
    ctx, &len, pattern, !(re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)));
  if (!str) return JS_EXCEPTION;
  re_bytecode_buf = lre_compile (&re_bytecode_len, error_msg, sizeof (error_msg), str, len, re_flags, ctx);
  JS_FreeCString (ctx, str);
  if (!re_bytecode_buf) {
    JS_ThrowSyntaxError (ctx, "%s", error_msg);
    return JS_EXCEPTION;
  }

  ret
    = js_new_string8_len (ctx, (const char *)re_bytecode_buf, re_bytecode_len);
  js_free (ctx, re_bytecode_buf);
  return ret;
}

/* create a RegExp object from a string containing the RegExp bytecode
   and the source pattern */
JSValue js_regexp_constructor_internal (JSContext *ctx, JSValue pattern, JSValue bc) {
  JSValue obj;
  JSRecord *p;
  JSRegExp *re;
  const char *pat_cstr;
  size_t pat_len;
  int bc_len, i;

  /* sanity check - need strings for pattern and bytecode */
  if (!JS_IsText (bc) || !JS_IsText (pattern)) {
    JS_ThrowTypeError (ctx, "string expected");
  fail:
    return JS_EXCEPTION;
  }

  /* Root pattern and bc across allocating calls */
  JSGCRef pat_ref, bc_ref;
  JS_PushGCRef (ctx, &pat_ref);
  pat_ref.val = pattern;
  JS_PushGCRef (ctx, &bc_ref);
  bc_ref.val = bc;

  obj = JS_NewObjectProtoClass (ctx, ctx->class_proto[JS_CLASS_REGEXP], JS_CLASS_REGEXP);
  if (JS_IsException (obj)) {
    JS_PopGCRef (ctx, &bc_ref);
    JS_PopGCRef (ctx, &pat_ref);
    goto fail;
  }

  /* Root obj across allocating calls */
  JSGCRef obj_ref;
  JS_PushGCRef (ctx, &obj_ref);
  obj_ref.val = obj;

#define REGEXP_CLEANUP() do { JS_PopGCRef (ctx, &obj_ref); JS_PopGCRef (ctx, &bc_ref); JS_PopGCRef (ctx, &pat_ref); } while(0)

  /* Allocate JSRegExp off-heap (not on GC heap) so opaque pointer stays valid after GC */
  re = js_malloc_rt (sizeof(JSRegExp));
  if (!re) { REGEXP_CLEANUP (); JS_ThrowOutOfMemory (ctx); goto fail; }
  p = JS_VALUE_GET_OBJ (obj_ref.val);
  REC_SET_OPAQUE(p, re);
  re->pattern = NULL;
  re->bytecode = NULL;

  /* Extract pattern as UTF-8 C string */
  pat_cstr = JS_ToCStringLen (ctx, &pat_len, pat_ref.val);
  if (!pat_cstr) { REGEXP_CLEANUP (); goto fail; }
  re->pattern = js_malloc_rt (pat_len + 1);
  if (!re->pattern) {
    JS_FreeCString (ctx, pat_cstr);
    REGEXP_CLEANUP ();
    goto fail;
  }
  memcpy (re->pattern, pat_cstr, pat_len + 1);
  re->pattern_len = (uint32_t)pat_len;
  JS_FreeCString (ctx, pat_cstr);

  /* Extract bytecode as raw bytes via string_get (not JS_ToCStringLen
     which UTF-8 encodes and would mangle bytes >= 128) */
  bc = bc_ref.val;
  if (MIST_IsImmediateASCII (bc)) {
    bc_len = MIST_GetImmediateASCIILen (bc);
    re->bytecode = js_malloc_rt (bc_len);
    if (!re->bytecode) { REGEXP_CLEANUP (); goto fail; }
    for (i = 0; i < bc_len; i++)
      re->bytecode[i] = (uint8_t)MIST_GetImmediateASCIIChar (bc, i);
  } else {
    JSText *bc_str = (JSText *)chase (bc_ref.val);
    bc_len = (int)JSText_len (bc_str);
    re->bytecode = js_malloc_rt (bc_len);
    if (!re->bytecode) { REGEXP_CLEANUP (); goto fail; }
    for (i = 0; i < bc_len; i++)
      re->bytecode[i] = (uint8_t)string_get (bc_str, i);
  }
  re->bytecode_len = (uint32_t)bc_len;

  {
    JSValue key = JS_KEY_STR (ctx, "lastIndex");
    obj = obj_ref.val; /* re-read after JS_KEY_STR allocation */
    JS_SetPropertyInternal (ctx, obj, key, JS_NewInt32 (ctx, 0));
  }
  obj = obj_ref.val;
  REGEXP_CLEANUP ();
  return obj;
}
#undef REGEXP_CLEANUP

static JSRegExp *js_get_regexp (JSContext *ctx, JSValue obj, BOOL throw_error) {
  if (JS_VALUE_GET_TAG (obj) == JS_TAG_PTR) {
    JSRecord *p = JS_VALUE_GET_OBJ (obj);
    if (REC_GET_CLASS_ID(p) == JS_CLASS_REGEXP) return (JSRegExp *)REC_GET_OPAQUE(p);
  }
  if (throw_error) { JS_ThrowTypeErrorInvalidClass (ctx, JS_CLASS_REGEXP); }
  return NULL;
}

/* return < 0 if exception or TRUE/FALSE */
static int js_is_regexp (JSContext *ctx, JSValue obj) {
  JSValue m;

  if (!mist_is_gc_object (obj)) return FALSE;
  m = JS_GetPropertyStr (ctx, obj, "Symbol.match");
  if (JS_IsException (m)) return -1;
  if (!JS_IsNull (m)) return JS_ToBool (ctx, m);
  return js_get_regexp (ctx, obj, FALSE) != NULL;
}

JSValue js_regexp_constructor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  JSValue pattern, flags, bc, val;
  JSValue pat, flags1;
  JSRegExp *re;
  int pat_is_regexp;

  pat = argv[0];
  flags1 = argv[1];
  pat_is_regexp = js_is_regexp (ctx, pat);
  if (pat_is_regexp < 0) return JS_EXCEPTION;
  /* If called with a regexp and no flags, just return a copy */
  if (pat_is_regexp && JS_IsNull (flags1)) {
    re = js_get_regexp (ctx, pat, FALSE);
    if (re) return pat;
  }
  re = js_get_regexp (ctx, pat, FALSE);
  if (re) {
    pattern = JS_NewString (ctx, re->pattern);
    if (JS_IsException (pattern)) goto fail;
    if (JS_IsNull (flags1)) {
      bc = js_new_string8_len (ctx, (const char *)re->bytecode, re->bytecode_len);
      if (JS_IsException (bc)) goto fail;
      goto no_compilation;
    } else {
      flags = JS_ToString (ctx, flags1);
      if (JS_IsException (flags)) goto fail;
    }
  } else {
    flags = JS_NULL;
    if (pat_is_regexp) {
      pattern = JS_GetProperty (ctx, pat, JS_KEY_source);
      if (JS_IsException (pattern)) goto fail;
      if (JS_IsNull (flags1)) {
        flags = JS_GetProperty (ctx, pat, JS_KEY_flags);
        if (JS_IsException (flags)) goto fail;
      } else {
        flags = flags1;
      }
    } else {
      pattern = pat;
      flags = flags1;
    }
    if (JS_IsNull (pattern)) {
      pattern = JS_KEY_empty;
    } else {
      val = pattern;
      pattern = JS_ToString (ctx, val);
      if (JS_IsException (pattern)) goto fail;
    }
  }
  bc = js_compile_regexp (ctx, pattern, flags);
  if (JS_IsException (bc)) goto fail;
no_compilation:
  return js_regexp_constructor_internal (ctx, pattern, bc);
fail:
  return JS_EXCEPTION;
}

static JSValue js_regexp_compile (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  JSRegExp *re1, *re;
  JSValue pattern1, flags1;
  JSValue bc, pattern;
  const char *pat_cstr;
  size_t pat_len;
  int bc_len, i;

  re = js_get_regexp (ctx, this_val, TRUE);
  if (!re) return JS_EXCEPTION;
  pattern1 = argv[0];
  flags1 = argv[1];
  re1 = js_get_regexp (ctx, pattern1, FALSE);
  if (re1) {
    if (!JS_IsNull (flags1))
      return JS_ThrowTypeError (ctx, "flags must be undefined");
    pattern = JS_NewString (ctx, re1->pattern);
    if (JS_IsException (pattern)) goto fail;
    bc = js_new_string8_len (ctx, (const char *)re1->bytecode, re1->bytecode_len);
    if (JS_IsException (bc)) goto fail;
  } else {
    bc = JS_NULL;
    if (JS_IsNull (pattern1))
      pattern = JS_KEY_empty;
    else
      pattern = JS_ToString (ctx, pattern1);
    if (JS_IsException (pattern)) goto fail;
    bc = js_compile_regexp (ctx, pattern, flags1);
    if (JS_IsException (bc)) goto fail;
  }
  /* Free old C buffers */
  js_free_rt (re->pattern);
  re->pattern = NULL;
  js_free_rt (re->bytecode);
  re->bytecode = NULL;

  /* Extract pattern as UTF-8 C string */
  pat_cstr = JS_ToCStringLen (ctx, &pat_len, pattern);
  if (!pat_cstr) goto fail;
  re->pattern = js_malloc_rt (pat_len + 1);
  if (!re->pattern) {
    JS_FreeCString (ctx, pat_cstr);
    goto fail;
  }
  memcpy (re->pattern, pat_cstr, pat_len + 1);
  re->pattern_len = (uint32_t)pat_len;
  JS_FreeCString (ctx, pat_cstr);

  /* Extract bytecode as raw bytes */
  if (MIST_IsImmediateASCII (bc)) {
    bc_len = MIST_GetImmediateASCIILen (bc);
    re->bytecode = js_malloc_rt (bc_len);
    if (!re->bytecode) goto fail;
    for (i = 0; i < bc_len; i++)
      re->bytecode[i] = (uint8_t)MIST_GetImmediateASCIIChar (bc, i);
  } else {
    JSText *bc_str = (JSText *)JS_VALUE_GET_PTR (bc);
    bc_len = (int)JSText_len (bc_str);
    re->bytecode = js_malloc_rt (bc_len);
    if (!re->bytecode) goto fail;
    for (i = 0; i < bc_len; i++)
      re->bytecode[i] = (uint8_t)string_get (bc_str, i);
  }
  re->bytecode_len = (uint32_t)bc_len;

  {
    JSValue key = JS_KEY_STR (ctx, "lastIndex");
    int ret = JS_SetProperty (ctx, this_val, key, JS_NewInt32 (ctx, 0));
    if (ret < 0) return JS_EXCEPTION;
  }
  return this_val;
fail:
  return JS_EXCEPTION;
}

JSValue js_regexp_toString (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  JSValue pattern, flags;

  if (!mist_is_gc_object (this_val)) return JS_ThrowTypeErrorNotAnObject (ctx);

  JSText *b = pretext_init (ctx, 0);
  if (!b) return JS_EXCEPTION;

  /* Root b across allocating calls (JS_GetProperty can trigger GC) */
  JSGCRef b_ref;
  JS_PushGCRef (ctx, &b_ref);
  b_ref.val = JS_MKPTR (b);

  b = pretext_putc (ctx, b, '/');
  if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
  b_ref.val = JS_MKPTR (b);

  pattern = JS_GetProperty (ctx, this_val, JS_KEY_source);
  b = (JSText *)chase (b_ref.val);
  b = pretext_concat_value (ctx, b, pattern);
  if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
  b_ref.val = JS_MKPTR (b);

  b = pretext_putc (ctx, b, '/');
  if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
  b_ref.val = JS_MKPTR (b);

  flags = JS_GetProperty (ctx, this_val, JS_KEY_flags);
  b = (JSText *)chase (b_ref.val);
  b = pretext_concat_value (ctx, b, flags);
  if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }

  JS_PopGCRef (ctx, &b_ref);
  return pretext_end (ctx, b);
}

int lre_check_timeout (void *opaque) {
  JSContext *ctx = opaque;
  return atomic_load_explicit (&ctx->pause_flag, memory_order_relaxed) >= 2;
}

void *lre_realloc (void *opaque, void *ptr, size_t size) {
  (void)opaque;
  /* No JS exception is raised here */
  return js_realloc_rt (ptr, size);
}

/* Convert UTF-32 JSText to UTF-16 buffer for regex engine.
   Returns allocated uint16_t buffer (via js_malloc_rt) that must be freed with js_free_rt.
   Sets *out_len to number of uint16 code units. */
static uint16_t *js_string_to_utf16 (JSContext *ctx, JSText *str, int *out_len) {
  int len = (int)JSText_len (str);
  /* Worst case: each UTF-32 char becomes 2 UTF-16 surrogates */
  uint16_t *buf = js_malloc_rt (len * 2 * sizeof (uint16_t));
  if (!buf) { JS_ThrowOutOfMemory (ctx); return NULL; }

  int j = 0;
  for (int i = 0; i < len; i++) {
    uint32_t c = string_get (str, i);
    if (c < 0x10000) {
      buf[j++] = (uint16_t)c;
    } else {
      /* Encode as surrogate pair */
      c -= 0x10000;
      buf[j++] = (uint16_t)(0xD800 | (c >> 10));
      buf[j++] = (uint16_t)(0xDC00 | (c & 0x3FF));
    }
  }
  *out_len = j;
  return buf;
}

static JSValue js_regexp_exec (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  JSRegExp *re = js_get_regexp (ctx, this_val, TRUE);
  JSText *str;
  JSGCRef str_ref, this_ref;
  JSValue ret, res, val, groups, captures_arr, match0;
  uint8_t *re_bytecode;
  uint8_t **capture, *str_buf;
  uint16_t *utf16_buf = NULL;
  int rc, capture_count, shift, i, re_flags;
  int utf16_len = 0;
  int64_t last_index;
  const char *group_name_ptr;

  if (!re) return JS_EXCEPTION;

  /* Root this_val across allocating calls */
  JS_PushGCRef (ctx, &this_ref);
  this_ref.val = this_val;

  JS_PushGCRef (ctx, &str_ref);
  str_ref.val = JS_ToString (ctx, argv[0]);
  if (JS_IsException (str_ref.val)) {
    JS_PopGCRef (ctx, &str_ref);
    JS_PopGCRef (ctx, &this_ref);
    return JS_EXCEPTION;
  }
  /* Ensure str_val is a heap string for JS_VALUE_GET_STRING */
  if (MIST_IsImmediateASCII (str_ref.val)) {
    int imm_len = MIST_GetImmediateASCIILen (str_ref.val);
    JSText *hs = js_alloc_string (ctx, imm_len > 0 ? imm_len : 1);
    if (!hs) {
      JS_PopGCRef (ctx, &str_ref);
      JS_PopGCRef (ctx, &this_ref);
      return JS_EXCEPTION;
    }
    for (int ci = 0; ci < imm_len; ci++)
      string_put (hs, ci, MIST_GetImmediateASCIIChar (str_ref.val, ci));
    hs->hdr = objhdr_set_cap56 (hs->hdr, imm_len);
    hs->length = 0;
    hs->hdr = objhdr_set_s (hs->hdr, true);
    str_ref.val = JS_MKPTR (hs);
  }

  ret = JS_EXCEPTION;
  res = JS_NULL;
  groups = JS_NULL;
  captures_arr = JS_NULL;
  match0 = JS_NULL;
  capture = NULL;

  val = JS_GetPropertyStr (ctx, this_ref.val, "lastIndex");
  if (JS_IsException (val) || JS_ToLength (ctx, &last_index, val))
    goto fail;

  /* Re-chase re after allocating calls (JS_ToString, js_alloc_string, JS_GetPropertyStr) */
  re = js_get_regexp (ctx, this_ref.val, TRUE);
  re_bytecode = re->bytecode;
  re_flags = lre_get_flags (re_bytecode);
  if ((re_flags & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) == 0) last_index = 0;

  capture_count = lre_get_capture_count (re_bytecode);

  if (capture_count > 0) {
    capture = js_malloc_rt (sizeof (capture[0]) * capture_count * 2);
    if (!capture) { JS_ThrowOutOfMemory (ctx); goto fail; }
  }

  /* Refresh str after potential GC from js_malloc */
  str = JS_VALUE_GET_STRING (str_ref.val);

  /* Convert UTF-32 string to UTF-16 for regex engine (uses js_malloc_rt, no GC) */
  utf16_buf = js_string_to_utf16 (ctx, str, &utf16_len);
  if (!utf16_buf) goto fail;
  shift = 1; /* UTF-16 mode */
  str_buf = (uint8_t *)utf16_buf;

  /* Refresh str after potential GC from js_string_to_utf16 */
  str = JS_VALUE_GET_STRING (str_ref.val);
  if (last_index > (int)JSText_len (str)) {
    rc = 2;
  } else {
    rc = lre_exec (capture, re_bytecode, str_buf, last_index, (int)JSText_len (str), shift, ctx);
  }

  if (rc != 1) {
    if (rc >= 0) {
      if (rc == 2 || (re_flags & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY))) {
        if (JS_SetPropertyStr (ctx, this_ref.val, "lastIndex", JS_NewInt32 (ctx, 0))
            < 0)
          goto fail;
      }
      ret = JS_NULL;
      goto done;
    }
    if (rc == LRE_RET_TIMEOUT)
      JS_ThrowInterrupted (ctx);
    else
      JS_ThrowInternalError (ctx, "out of memory in regexp execution");
    goto fail;
  }

  if (re_flags & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) {
    if (JS_SetPropertyStr (ctx, this_ref.val, "lastIndex", JS_NewInt32 (ctx, (capture[1] - str_buf) >> shift))
        < 0)
      goto fail;
  }

  res = JS_NewObjectProto (ctx, JS_NULL);
  if (JS_IsException (res)) goto fail;

  /* Root res, captures_arr, groups, match0 across allocating calls */
  JSGCRef res_ref, cap_ref, grp_ref, m0_ref;
  JS_PushGCRef (ctx, &res_ref);
  res_ref.val = res;
  JS_PushGCRef (ctx, &cap_ref);
  cap_ref.val = JS_NULL;
  JS_PushGCRef (ctx, &grp_ref);
  grp_ref.val = JS_NULL;
  JS_PushGCRef (ctx, &m0_ref);
  m0_ref.val = JS_NULL;

#define REGEXP_RESULT_CLEANUP() do { \
  JS_PopGCRef (ctx, &m0_ref); \
  JS_PopGCRef (ctx, &grp_ref); \
  JS_PopGCRef (ctx, &cap_ref); \
  JS_PopGCRef (ctx, &res_ref); \
} while(0)

  {
    int cap_groups = (capture_count > 1) ? (capture_count - 1) : 0;
    captures_arr = JS_NewArrayLen (ctx, cap_groups);
    if (JS_IsException (captures_arr)) { REGEXP_RESULT_CLEANUP (); goto fail; }
    cap_ref.val = captures_arr;
  }

  group_name_ptr = lre_get_groupnames (re_bytecode);
  if (group_name_ptr) {
    groups = JS_NewObjectProto (ctx, JS_NULL);
    if (JS_IsException (groups)) { REGEXP_RESULT_CLEANUP (); goto fail; }
    grp_ref.val = groups;
  }

  {
    int match_start = -1;
    int match_end = -1;

    for (i = 0; i < capture_count; i++) {
      const char *name = NULL;
      uint8_t **m = &capture[2 * i];
      int start = -1;
      int end = -1;
      JSValue s;

      if (group_name_ptr && i > 0) {
        if (*group_name_ptr) name = group_name_ptr;
        group_name_ptr += strlen (group_name_ptr) + 1;
      }

      if (m[0] && m[1]) {
        start = (m[0] - str_buf) >> shift;
        end = (m[1] - str_buf) >> shift;
      }

      s = JS_NULL;
      if (start != -1) {
        str = JS_VALUE_GET_STRING (str_ref.val);
        s = js_sub_string (ctx, str, start, end);
        if (JS_IsException (s)) { REGEXP_RESULT_CLEANUP (); goto fail; }
      }

      if (i == 0) {
        match_start = start;
        match_end = end;
        match0 = s;
        m0_ref.val = match0;
        continue;
      }

      if (name) {
        groups = grp_ref.val;
        if (JS_SetPropertyStr (ctx, groups, name, s) < 0) {
          REGEXP_RESULT_CLEANUP ();
          goto fail;
        }
      }

      captures_arr = cap_ref.val;
      if (JS_IsException (JS_SetPropertyNumber (ctx, captures_arr, (uint32_t)(i - 1), s))) {
        REGEXP_RESULT_CLEANUP ();
        goto fail;
      }
    }

    if (match_start < 0) match_start = 0;
    if (match_end < match_start) match_end = match_start;

    res = res_ref.val;
    if (JS_SetPropertyStr (ctx, res, "index", JS_NewInt32 (ctx, match_start))
        < 0) {
      REGEXP_RESULT_CLEANUP ();
      goto fail;
    }
    res = res_ref.val;
    if (JS_SetPropertyStr (ctx, res, "end", JS_NewInt32 (ctx, match_end)) < 0) {
      REGEXP_RESULT_CLEANUP ();
      goto fail;
    }

    res = res_ref.val;
    match0 = m0_ref.val;
    if (JS_SetPropertyStr (ctx, res, "match", match0) < 0) { REGEXP_RESULT_CLEANUP (); goto fail; }

    res = res_ref.val;
    captures_arr = cap_ref.val;
    if (JS_SetPropertyStr (ctx, res, "captures", captures_arr) < 0) { REGEXP_RESULT_CLEANUP (); goto fail; }

    groups = grp_ref.val;
    if (!JS_IsNull (groups)) {
      res = res_ref.val;
      if (JS_SetPropertyStr (ctx, res, "groups", groups) < 0) { REGEXP_RESULT_CLEANUP (); goto fail; }
    } else {
      res = res_ref.val;
      JS_SetPropertyStr (ctx, res, "groups", JS_NULL);
    }
  }

  ret = res_ref.val;
  REGEXP_RESULT_CLEANUP ();

done:
  JS_PopGCRef (ctx, &str_ref);
  JS_PopGCRef (ctx, &this_ref);
  js_free_rt (capture);
  js_free_rt (utf16_buf);
  return ret;

fail:
  JS_PopGCRef (ctx, &str_ref);
  JS_PopGCRef (ctx, &this_ref);
  js_free_rt (capture);
  js_free_rt (utf16_buf);
  return JS_EXCEPTION;
}

static const JSCFunctionListEntry js_regexp_proto_funcs[] = {
  JS_CFUNC_DEF ("exec", 1, js_regexp_exec),
  JS_CFUNC_DEF ("compile", 2, js_regexp_compile),
  JS_CFUNC_DEF ("toString", 0, js_regexp_toString),
};

void JS_AddIntrinsicRegExpCompiler (JSContext *ctx) {
  ctx->compile_regexp = js_compile_regexp;
}

static void JS_AddIntrinsicRegExp (JSContext *ctx) {
  JSValue obj;

  JS_AddIntrinsicRegExpCompiler (ctx);

  ctx->class_proto[JS_CLASS_REGEXP] = JS_NewObject (ctx);
  JS_SetPropertyFunctionList (ctx, ctx->class_proto[JS_CLASS_REGEXP], js_regexp_proto_funcs, countof (js_regexp_proto_funcs));
  obj = JS_NewCFunction2 (ctx, js_regexp_constructor, "RegExp", 2, JS_CFUNC_generic, 0);
  JS_SetPropertyStr (ctx, ctx->global_obj, "RegExp", obj);
  ctx->regexp_ctor = obj;
}



/* Convert a cJSON node to a JSValue */
static JSValue cjson_to_jsvalue (JSContext *ctx, const cJSON *item) {
  if (!item || cJSON_IsNull (item)) return JS_NULL;
  if (cJSON_IsFalse (item)) return JS_FALSE;
  if (cJSON_IsTrue (item)) return JS_TRUE;
  if (cJSON_IsNumber (item)) return JS_NewFloat64 (ctx, item->valuedouble);
  if (cJSON_IsString (item)) return JS_NewString (ctx, item->valuestring);
  if (cJSON_IsArray (item)) {
    int n = cJSON_GetArraySize (item);
    JSGCRef arr_ref;
    JS_AddGCRef (ctx, &arr_ref);
    arr_ref.val = JS_NewArrayLen (ctx, n);
    for (int i = 0; i < n; i++) {
      cJSON *child = cJSON_GetArrayItem (item, i);
      /* Evaluate recursive call before reading arr_ref.val — the call
         allocates and can trigger GC which moves the array */
      JSValue elem = cjson_to_jsvalue (ctx, child);
      JS_SetPropertyNumber (ctx, arr_ref.val, i, elem);
    }
    JSValue result = arr_ref.val;
    JS_DeleteGCRef (ctx, &arr_ref);
    return result;
  }
  if (cJSON_IsObject (item)) {
    JSGCRef obj_ref;
    JS_AddGCRef (ctx, &obj_ref);
    obj_ref.val = JS_NewObject (ctx);
    for (cJSON *child = item->child; child; child = child->next) {
      JSValue val = cjson_to_jsvalue (ctx, child);
      JS_SetPropertyStr (ctx, obj_ref.val, child->string, val);
    }
    JSValue result = obj_ref.val;
    JS_DeleteGCRef (ctx, &obj_ref);
    return result;
  }
  return JS_NULL;
}

JSValue JS_ParseJSON (JSContext *ctx, const char *buf, size_t buf_len,
                      const char *filename) {
  (void)filename;
  (void)buf_len;
  cJSON *root = cJSON_Parse (buf);
  if (!root)
    return JS_ThrowSyntaxError (ctx, "JSON parse error");
  JSValue result = cjson_to_jsvalue (ctx, root);
  cJSON_Delete (root);
  return result;
}

JSValue JS_ParseJSON2 (JSContext *ctx, const char *buf, size_t buf_len,
                       const char *filename, int flags) {
  (void)flags;
  return JS_ParseJSON (ctx, buf, buf_len, filename);
}


typedef struct JSONStringifyContext {
  JSContext *ctx;
  JSValue replacer_func;
  JSValue stack;
  JSValue property_list;
  JSValue gap;
  JSValue empty;
  JSGCRef b_root;  /* GC root for buffer - use JSC_B_GET/SET macros */
  BOOL compact_arrays;
  BOOL in_compact_array;
} JSONStringifyContext;

/* Macros to access the buffer from the rooted JSValue */
#define JSC_B_GET(jsc) JS_VALUE_GET_STRING((jsc)->b_root.val)
#define JSC_B_SET(jsc, ptr) ((jsc)->b_root.val = JS_MKPTR(ptr))
#define JSC_B_PUTC(jsc, c) do { \
  JSText *_b = pretext_putc(ctx, JSC_B_GET(jsc), c); \
  if (!_b) goto exception; \
  JSC_B_SET(jsc, _b); \
} while(0)
#define JSC_B_CONCAT(jsc, v) do { \
  JSText *_b = pretext_concat_value(ctx, JSC_B_GET(jsc), v); \
  if (!_b) goto exception; \
  JSC_B_SET(jsc, _b); \
} while(0)

static JSValue js_json_check (JSContext *ctx, JSONStringifyContext *jsc, JSValue holder, JSValue val, JSValue key) {
  JSValue v;
  JSValue args[2];

  /* check for object.toJSON method */
  /* ECMA specifies this is done only for Object and BigInt */
  if (mist_is_gc_object (val)) {
    JSValue f = JS_GetProperty (ctx, val, JS_KEY_toJSON);
    if (JS_IsException (f)) goto exception;
    if (JS_IsFunction (f)) {
      v = JS_Call (ctx, f, val, 1, &key);
      val = v;
      if (JS_IsException (val)) goto exception;
    } else {
    }
  }

  if (!JS_IsNull (jsc->replacer_func)) {
    args[0] = key;
    args[1] = val;
    v = JS_Call (ctx, jsc->replacer_func, holder, 2, args);
    val = v;
    if (JS_IsException (val)) goto exception;
  }

  switch (JS_VALUE_GET_NORM_TAG (val)) {
  case JS_TAG_PTR: /* includes arrays (OBJ_ARRAY) via mist_hdr */
    if (JS_IsFunction (val)) break;
    /* fall through */
  case JS_TAG_STRING_IMM:
  case JS_TAG_INT:
  case JS_TAG_FLOAT64:
  case JS_TAG_BOOL:
  case JS_TAG_NULL:
  case JS_TAG_EXCEPTION:
    return val;
  default:
    break;
  }
  return JS_NULL;

exception:
  return JS_EXCEPTION;
}

/* Check if val is already on the visited stack (circular reference detection).
   Uses identity comparison (===) since we're checking for the same object. */
static BOOL json_stack_has (JSContext *ctx, JSValue stack, JSValue val) {
  if (!JS_IsArray (stack)) return FALSE;
  JSArray *arr = JS_VALUE_GET_ARRAY (stack);
  for (word_t i = 0; i < arr->len; i++) {
    if (JS_StrictEq (ctx, arr->values[i], val))
      return TRUE;
  }
  return FALSE;
}

static int js_json_to_str (JSContext *ctx, JSONStringifyContext *jsc, JSValue holder, JSValue val, JSValue indent) {
  JSValue v;
  int64_t i, len;
  int ret;
  BOOL has_content;
  JSGCRef val_ref, indent_ref, indent1_ref, sep_ref, sep1_ref, tab_ref, prop_ref, v_ref;

  /* Root all values that can be heap pointers and survive across GC points */
  JS_PushGCRef (ctx, &val_ref);
  JS_PushGCRef (ctx, &indent_ref);
  JS_PushGCRef (ctx, &indent1_ref);
  JS_PushGCRef (ctx, &sep_ref);
  JS_PushGCRef (ctx, &sep1_ref);
  JS_PushGCRef (ctx, &tab_ref);
  JS_PushGCRef (ctx, &prop_ref);
  JS_PushGCRef (ctx, &v_ref);

  val_ref.val = val;
  indent_ref.val = indent;
  indent1_ref.val = JS_NULL;
  sep_ref.val = JS_NULL;
  sep1_ref.val = JS_NULL;
  tab_ref.val = JS_NULL;
  prop_ref.val = JS_NULL;
  v_ref.val = JS_NULL;

  /* Heap strings are JS_TAG_PTR but must be quoted, not iterated as objects */
  if (JS_IsText (val_ref.val) && !MIST_IsImmediateASCII (val_ref.val)) {
    val_ref.val = JS_ToQuotedString (ctx, val_ref.val);
    if (JS_IsException (val_ref.val)) goto exception;
    goto concat_value;
  }

  if (mist_is_gc_object (
        val_ref.val)) { /* includes arrays (OBJ_ARRAY) since they have JS_TAG_PTR */
    if (json_stack_has (ctx, jsc->stack, val_ref.val)) {
      JS_ThrowTypeError (ctx, "circular reference");
      goto exception;
    }
    indent1_ref.val = JS_ConcatString (ctx, indent_ref.val, jsc->gap);
    if (JS_IsException (indent1_ref.val)) goto exception;
    if (!JS_IsEmptyString (jsc->gap) && !jsc->in_compact_array) {
      sep_ref.val = JS_ConcatString3 (ctx, "\n", indent1_ref.val, "");
      if (JS_IsException (sep_ref.val)) goto exception;
      sep1_ref.val = js_new_string8 (ctx, " ");
      if (JS_IsException (sep1_ref.val)) goto exception;
    } else {
      sep_ref.val = jsc->empty;
      sep1_ref.val = jsc->empty;
    }
    if (JS_ArrayPush (ctx, &jsc->stack, val_ref.val) < 0) goto exception;
    ret = JS_IsArray (val_ref.val);
    if (ret < 0) goto exception;
    if (ret) {
      if (js_get_length64 (ctx, &len, val_ref.val)) goto exception;
      /* Check if this is a leaf array for compact mode.
         Leaf = every element is a primitive or string (no arrays or objects). */
      BOOL was_compact = jsc->in_compact_array;
      if (jsc->compact_arrays && !jsc->in_compact_array && !JS_IsEmptyString (jsc->gap)) {
        BOOL is_leaf = TRUE;
        for (i = 0; i < len && is_leaf; i++) {
          v = JS_GetPropertyNumber (ctx, val_ref.val, i);
          if (JS_IsException (v)) goto exception;
          if (JS_IsArray (v) > 0) {
            is_leaf = FALSE;
          } else if (mist_is_gc_object (v) && !JS_IsText (v)) {
            is_leaf = FALSE;
          }
        }
        if (is_leaf) jsc->in_compact_array = TRUE;
      }
      JSC_B_PUTC (jsc, '[');
      for (i = 0; i < len; i++) {
        if (i > 0) {
          JSC_B_PUTC (jsc, ',');
        }
        if (jsc->in_compact_array && !was_compact) {
          if (i > 0) JSC_B_PUTC (jsc, ' ');
        } else {
          JSC_B_CONCAT (jsc, sep_ref.val);
        }
        v = JS_GetPropertyNumber (ctx, val_ref.val, i);
        if (JS_IsException (v)) goto exception;
        v_ref.val = v; /* root v — JS_ToString below can trigger GC */
        /* XXX: could do this string conversion only when needed */
        prop_ref.val = JS_ToString (ctx, JS_NewInt64 (ctx, i));
        if (JS_IsException (prop_ref.val)) goto exception;
        v = js_json_check (ctx, jsc, val_ref.val, v_ref.val, prop_ref.val);
        prop_ref.val = JS_NULL;
        if (JS_IsException (v)) goto exception;
        if (JS_IsNull (v)) v = JS_NULL;
        if (js_json_to_str (ctx, jsc, val_ref.val, v, indent1_ref.val)) goto exception;
      }
      if (len > 0 && !JS_IsEmptyString (jsc->gap) && !jsc->in_compact_array) {
        JSC_B_PUTC (jsc, '\n');
        JSC_B_CONCAT (jsc, indent_ref.val);
      }
      JSC_B_PUTC (jsc, ']');
      jsc->in_compact_array = was_compact;
    } else {
      if (!JS_IsNull (jsc->property_list))
        tab_ref.val = jsc->property_list;
      else
        tab_ref.val = JS_GetOwnPropertyNames (ctx, val_ref.val);
      if (JS_IsException (tab_ref.val)) goto exception;
      if (js_get_length64 (ctx, &len, tab_ref.val)) goto exception;
      JSC_B_PUTC (jsc, '{');
      has_content = FALSE;
      for (i = 0; i < len; i++) {
        prop_ref.val = JS_GetPropertyNumber (ctx, tab_ref.val, i);
        if (JS_IsException (prop_ref.val)) goto exception;
        v = JS_GetPropertyValue (ctx, val_ref.val, prop_ref.val);
        if (JS_IsException (v)) goto exception;
        v = js_json_check (ctx, jsc, val_ref.val, v, prop_ref.val);
        if (JS_IsException (v)) goto exception;
        if (!JS_IsNull (v)) {
          v_ref.val = v; /* root v — allocations below can trigger GC */
          if (has_content) {
            JSC_B_PUTC (jsc, ',');
          }
          prop_ref.val = JS_ToQuotedString (ctx, prop_ref.val);
          if (JS_IsException (prop_ref.val)) {
            goto exception;
          }
          JSC_B_CONCAT (jsc, sep_ref.val);
          JSC_B_CONCAT (jsc, prop_ref.val);
          JSC_B_PUTC (jsc, ':');
          JSC_B_CONCAT (jsc, sep1_ref.val);
          if (js_json_to_str (ctx, jsc, val_ref.val, v_ref.val, indent1_ref.val)) goto exception;
          has_content = TRUE;
        }
      }
      if (has_content && !JS_IsEmptyString (jsc->gap) && !jsc->in_compact_array) {
        JSC_B_PUTC (jsc, '\n');
        JSC_B_CONCAT (jsc, indent_ref.val);
      }
      JSC_B_PUTC (jsc, '}');
    }
    v = JS_ArrayPop (ctx, jsc->stack);
    if (JS_IsException (v)) goto exception;
    goto done;
  }
  switch (JS_VALUE_GET_NORM_TAG (val_ref.val)) {
  case JS_TAG_STRING_IMM:
    val_ref.val = JS_ToQuotedString (ctx, val_ref.val);
    if (JS_IsException (val_ref.val)) goto exception;
    goto concat_value;
  case JS_TAG_FLOAT64:
    if (!isfinite (JS_VALUE_GET_FLOAT64 (val_ref.val))) { val_ref.val = JS_NULL; }
    goto concat_value;
  case JS_TAG_INT:
  case JS_TAG_BOOL:
  case JS_TAG_NULL:
  concat_value: {
    JSText *_b = pretext_concat_value (ctx, JSC_B_GET (jsc), val_ref.val);
    if (!_b) goto exception_ret;
    JSC_B_SET (jsc, _b);
    goto done;
  }
  default:
    goto done;
  }

done:
  JS_PopGCRef (ctx, &v_ref);
  JS_PopGCRef (ctx, &prop_ref);
  JS_PopGCRef (ctx, &tab_ref);
  JS_PopGCRef (ctx, &sep1_ref);
  JS_PopGCRef (ctx, &sep_ref);
  JS_PopGCRef (ctx, &indent1_ref);
  JS_PopGCRef (ctx, &indent_ref);
  JS_PopGCRef (ctx, &val_ref);
  return 0;

exception_ret:
  JS_PopGCRef (ctx, &v_ref);
  JS_PopGCRef (ctx, &prop_ref);
  JS_PopGCRef (ctx, &tab_ref);
  JS_PopGCRef (ctx, &sep1_ref);
  JS_PopGCRef (ctx, &sep_ref);
  JS_PopGCRef (ctx, &indent1_ref);
  JS_PopGCRef (ctx, &indent_ref);
  JS_PopGCRef (ctx, &val_ref);
  return -1;

exception:
  JS_PopGCRef (ctx, &prop_ref);
  JS_PopGCRef (ctx, &tab_ref);
  JS_PopGCRef (ctx, &sep1_ref);
  JS_PopGCRef (ctx, &sep_ref);
  JS_PopGCRef (ctx, &indent1_ref);
  JS_PopGCRef (ctx, &indent_ref);
  JS_PopGCRef (ctx, &val_ref);
  return -1;
}

JSValue JS_JSONStringify (JSContext *ctx, JSValue obj, JSValue replacer, JSValue space0, BOOL compact_arrays) {
  JSONStringifyContext jsc_s, *jsc = &jsc_s;
  JSValue val, v, space, ret, wrapper;
  int res;
  int64_t i, j, n;
  JSGCRef obj_ref;
  JSLocalRef *saved_local_frame = JS_GetLocalFrame (ctx);

  /* Root obj since GC can happen during stringify setup */
  JS_PushGCRef (ctx, &obj_ref);
  obj_ref.val = obj;

  jsc->ctx = ctx;
  jsc->replacer_func = JS_NULL;
  jsc->stack = JS_NULL;
  jsc->property_list = JS_NULL;
  jsc->gap = JS_NULL;
  jsc->empty = JS_KEY_empty;
  jsc->compact_arrays = compact_arrays;
  jsc->in_compact_array = FALSE;
  ret = JS_NULL;
  wrapper = JS_NULL;

  /* Root all jsc fields that hold heap objects — GC can fire during
     stringify and would move these objects without updating the struct */
  JSLocalRef lr_stack, lr_plist, lr_gap, lr_replacer;
  lr_stack.ptr = &jsc->stack;
  JS_PushLocalRef (ctx, &lr_stack);
  lr_plist.ptr = &jsc->property_list;
  JS_PushLocalRef (ctx, &lr_plist);
  lr_gap.ptr = &jsc->gap;
  JS_PushLocalRef (ctx, &lr_gap);
  lr_replacer.ptr = &jsc->replacer_func;
  JS_PushLocalRef (ctx, &lr_replacer);

  /* Root the buffer for GC safety */
  JS_PushGCRef (ctx, &jsc->b_root);
  {
    JSText *b_init = pretext_init (ctx, 0);
    if (!b_init) goto exception;
    JSC_B_SET (jsc, b_init);
  }
  jsc->stack = JS_NewArray (ctx);
  if (JS_IsException (jsc->stack)) goto exception;
  if (JS_IsFunction (replacer)) {
    jsc->replacer_func = replacer;
  } else {
    res = JS_IsArray (replacer);
    if (res < 0) goto exception;
    if (res) {
      /* XXX: enumeration is not fully correct */
      jsc->property_list = JS_NewArray (ctx);
      if (JS_IsException (jsc->property_list)) goto exception;
      if (js_get_length64 (ctx, &n, replacer)) goto exception;
      for (i = j = 0; i < n; i++) {
        JSValue present;
        v = JS_GetPropertyNumber (ctx, replacer, i);
        if (JS_IsException (v)) goto exception;
        if (mist_is_gc_object (v)) {
          /* Objects are not valid property list items */
          continue;
        } else if (JS_IsNumber (v)) {
          v = JS_ToString (ctx, v);
          if (JS_IsException (v)) goto exception;
        } else if (!JS_IsText (v)) {
          continue;
        }
        present
          = js_array_includes (ctx, jsc->property_list, 1, (JSValue *)&v);
        if (JS_IsException (present)) {
          goto exception;
        }
        if (!JS_ToBool (ctx, present)) {
          JS_SetPropertyNumber (ctx, jsc->property_list, j++, v);
        } else {
        }
      }
    }
  }
  space = space0;
  if (JS_IsNumber (space)) {
    int n;
    if (JS_ToInt32Clamp (ctx, &n, space, 0, 10, 0)) goto exception;
    jsc->gap = js_new_string8_len (ctx, "          ", n);
  } else if (JS_IsText (space)) {
    JSText *p = JS_VALUE_GET_STRING (space);
    jsc->gap = js_sub_string (ctx, p, 0, min_int ((int)JSText_len (p), 10));
  } else {
    jsc->gap = jsc->empty;
  }
  if (JS_IsException (jsc->gap)) goto exception;
  wrapper = JS_NewObject (ctx);
  if (JS_IsException (wrapper)) goto exception;
  if (JS_SetPropertyInternal (ctx, wrapper, JS_KEY_empty, obj_ref.val)
      < 0)
    goto exception;
  val = obj_ref.val;

  val = js_json_check (ctx, jsc, wrapper, val, jsc->empty);
  if (JS_IsException (val)) goto exception;
  if (JS_IsNull (val)) {
    ret = JS_NULL;
    goto done1;
  }
  if (js_json_to_str (ctx, jsc, wrapper, val, jsc->empty)) goto exception;

  ret = pretext_end (ctx, JSC_B_GET (jsc));
  goto done;

exception:
  ret = JS_EXCEPTION;
done1:
done:
  JS_PopGCRef (ctx, &jsc->b_root);
  /* Restore local refs pushed for jsc fields */
  ctx->top_local_ref = saved_local_frame;
  JS_PopGCRef (ctx, &obj_ref);
  return ret;
}

/* ============================================================================
 * Cell Script Native Global Functions
 * ============================================================================
 * These functions implement the core Cell script primitives:
 * - text: string conversion and manipulation
 * - number: number conversion and math utilities
 * - array: array creation and manipulation
 * - object: object creation and manipulation
 * - fn: function utilities
 * ============================================================================
 */

/* ----------------------------------------------------------------------------
 * number function and sub-functions
 * ----------------------------------------------------------------------------
 */

/* number(val, format) - convert to number */
static JSValue js_cell_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  JSValue val = argv[0];
  int tag = JS_VALUE_GET_TAG (val);

  /* Handle boolean */
  if (tag == JS_TAG_BOOL) {
    return JS_NewInt32 (ctx, JS_VALUE_GET_BOOL (val) ? 1 : 0);
  }

  /* Handle number - return as-is */
  if (tag == JS_TAG_INT || tag == JS_TAG_FLOAT64) {
    return val;
  }

  /* Handle string (immediate ASCII or heap JSText) */
  if (JS_IsText (val)) {
    const char *str = JS_ToCString (ctx, val);
    if (!str) return JS_EXCEPTION;

    JSValue result;

    /* Check for format argument */
    if (argc > 1 && JS_VALUE_GET_TAG (argv[1]) == JS_TAG_INT) {
      /* Radix conversion */
      int radix = JS_VALUE_GET_INT (argv[1]);
      if (radix < 2 || radix > 36) {
        JS_FreeCString (ctx, str);
        return JS_NULL;
      }
      char *endptr;
      long long n = strtoll (str, &endptr, radix);
      if (endptr == str || *endptr != '\0') {
        JS_FreeCString (ctx, str);
        return JS_NULL;
      }
      result = JS_NewInt64 (ctx, n);
    } else if (argc > 1
               && (JS_VALUE_GET_TAG (argv[1]) == JS_TAG_STRING
                   || JS_VALUE_GET_TAG (argv[1]) == JS_TAG_STRING_IMM)) {
      /* Format string */
      const char *format = JS_ToCString (ctx, argv[1]);
      if (!format) {
        JS_FreeCString (ctx, str);
        return JS_EXCEPTION;
      }

      char *clean = js_malloc (ctx, strlen (str) + 1);
      if (!clean) {
        JS_FreeCString (ctx, format);
        JS_FreeCString (ctx, str);
        return JS_EXCEPTION;
      }

      const char *p = str;
      char *q = clean;

      if (strcmp (format, "u") == 0) {
        /* underbar separator */
        while (*p) {
          if (*p != '_') *q++ = *p;
          p++;
        }
      } else if (strcmp (format, "d") == 0 || strcmp (format, "l") == 0) {
        /* comma separator */
        while (*p) {
          if (*p != ',') *q++ = *p;
          p++;
        }
      } else if (strcmp (format, "s") == 0) {
        /* space separator */
        while (*p) {
          if (*p != ' ') *q++ = *p;
          p++;
        }
      } else if (strcmp (format, "v") == 0) {
        /* European style: period separator, comma decimal */
        while (*p) {
          if (*p == '.') {
            p++;
            continue;
          }
          if (*p == ',') {
            *q++ = '.';
            p++;
            continue;
          }
          *q++ = *p++;
        }
      } else if (strcmp (format, "b") == 0) {
        *q = '\0';
        char *endptr;
        long long n = strtoll (str, &endptr, 2);
        js_free (ctx, clean);
        JS_FreeCString (ctx, format);
        JS_FreeCString (ctx, str);
        if (endptr == str) return JS_NULL;
        return JS_NewInt64 (ctx, n);
      } else if (strcmp (format, "o") == 0) {
        *q = '\0';
        char *endptr;
        long long n = strtoll (str, &endptr, 8);
        js_free (ctx, clean);
        JS_FreeCString (ctx, format);
        JS_FreeCString (ctx, str);
        if (endptr == str) return JS_NULL;
        return JS_NewInt64 (ctx, n);
      } else if (strcmp (format, "h") == 0) {
        *q = '\0';
        char *endptr;
        long long n = strtoll (str, &endptr, 16);
        js_free (ctx, clean);
        JS_FreeCString (ctx, format);
        JS_FreeCString (ctx, str);
        if (endptr == str) return JS_NULL;
        return JS_NewInt64 (ctx, n);
      } else if (strcmp (format, "t") == 0) {
        *q = '\0';
        char *endptr;
        long long n = strtoll (str, &endptr, 32);
        js_free (ctx, clean);
        JS_FreeCString (ctx, format);
        JS_FreeCString (ctx, str);
        if (endptr == str) return JS_NULL;
        return JS_NewInt64 (ctx, n);
      } else if (strcmp (format, "j") == 0) {
        /* JavaScript style prefix */
        js_free (ctx, clean);
        JS_FreeCString (ctx, format);
        int radix = 10;
        const char *start = str;
        if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) {
          radix = 16;
          start = str + 2;
        } else if (str[0] == '0' && (str[1] == 'o' || str[1] == 'O')) {
          radix = 8;
          start = str + 2;
        } else if (str[0] == '0' && (str[1] == 'b' || str[1] == 'B')) {
          radix = 2;
          start = str + 2;
        }
        if (radix != 10) {
          char *endptr;
          long long n = strtoll (start, &endptr, radix);
          JS_FreeCString (ctx, str);
          if (endptr == start) return JS_NULL;
          return JS_NewInt64 (ctx, n);
        }
        double d = strtod (str, NULL);
        JS_FreeCString (ctx, str);
        return JS_NewFloat64 (ctx, d);
      } else {
        /* Unknown format, just copy */
        strcpy (clean, str);
        q = clean + strlen (clean);
      }
      *q = '\0';

      double d = strtod (clean, NULL);
      js_free (ctx, clean);
      JS_FreeCString (ctx, format);
      JS_FreeCString (ctx, str);
      if (isnan (d)) return JS_NULL;
      return JS_NewFloat64 (ctx, d);
    } else {
      /* Default: parse as decimal */
      char *endptr;
      double d = strtod (str, &endptr);
      JS_FreeCString (ctx, str);
      if (endptr == str || isnan (d)) return JS_NULL;
      result = JS_NewFloat64 (ctx, d);
    }

    return result;
  }

  return JS_NULL;
}

/* number.whole(n) - truncate to integer */
static JSValue js_cell_number_whole (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double d;
  if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL;
  return JS_NewFloat64 (ctx, trunc (d));
}

/* number.fraction(n) - get fractional part */
static JSValue js_cell_number_fraction (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double d;
  if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL;
  return JS_NewFloat64 (ctx, d - trunc (d));
}

/* number.floor(n, place) - floor with optional decimal place */
static JSValue js_cell_number_floor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double d;
  if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL;
  if (argc < 2 || JS_IsNull (argv[1])) {
    return JS_NewFloat64 (ctx, floor (d));
  }
  int place;
  if (JS_ToInt32 (ctx, &place, argv[1])) return JS_NULL;
  if (place == 0) return JS_NewFloat64 (ctx, floor (d));
  double mult = pow (10, -place);
  return JS_NewFloat64 (ctx, floor (d * mult) / mult);
}

/* number.ceiling(n, place) - ceiling with optional decimal place */
static JSValue js_cell_number_ceiling (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double d;
  if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL;
  if (argc < 2 || JS_IsNull (argv[1])) {
    return JS_NewFloat64 (ctx, ceil (d));
  }
  int place;
  if (JS_ToInt32 (ctx, &place, argv[1])) return JS_NULL;
  if (place == 0) return JS_NewFloat64 (ctx, ceil (d));
  double mult = pow (10, -place);
  return JS_NewFloat64 (ctx, ceil (d * mult) / mult);
}

/* number.abs(n) - absolute value */
static JSValue js_cell_number_abs (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;
  int tag = JS_VALUE_GET_TAG (argv[0]);
  if (tag != JS_TAG_INT && tag != JS_TAG_FLOAT64) return JS_NULL;
  double d;
  if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL;
  return JS_NewFloat64 (ctx, fabs (d));
}

/* number.round(n, place) - round with optional decimal place */
static JSValue js_cell_number_round (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double d;
  if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL;
  if (argc < 2 || JS_IsNull (argv[1])) {
    return JS_NewFloat64 (ctx, round (d));
  }
  int place;
  if (JS_ToInt32 (ctx, &place, argv[1])) return JS_NULL;
  if (place == 0) return JS_NewFloat64 (ctx, round (d));
  double mult = pow (10, -place);
  return JS_NewFloat64 (ctx, round (d * mult) / mult);
}

/* number.sign(n) - return sign (-1, 0, 1) */
static JSValue js_cell_number_sign (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double d;
  if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL;
  if (d < 0) return JS_NewInt32 (ctx, -1);
  if (d > 0) return JS_NewInt32 (ctx, 1);
  return JS_NewInt32 (ctx, 0);
}

/* number.trunc(n, place) - truncate with optional decimal place */
static JSValue js_cell_number_trunc (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double d;
  if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL;
  if (argc < 2 || JS_IsNull (argv[1])) {
    return JS_NewFloat64 (ctx, trunc (d));
  }
  int place;
  if (JS_ToInt32 (ctx, &place, argv[1])) return JS_NULL;
  if (place == 0) return JS_NewFloat64 (ctx, trunc (d));
  double mult = pow (10, -place);
  return JS_NewFloat64 (ctx, trunc (d * mult) / mult);
}

/* number.min(...vals) - minimum value */
static JSValue js_cell_number_min (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc == 0) return JS_NULL;
  double result;
  if (JS_ToFloat64 (ctx, &result, argv[0])) return JS_NULL;
  for (int i = 1; i < argc; i++) {
    double d;
    if (JS_ToFloat64 (ctx, &d, argv[i])) return JS_NULL;
    if (d < result) result = d;
  }
  return JS_NewFloat64 (ctx, result);
}

/* number.max(...vals) - maximum value */
static JSValue js_cell_number_max (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc == 0) return JS_NULL;
  double result;
  if (JS_ToFloat64 (ctx, &result, argv[0])) return JS_NULL;
  for (int i = 1; i < argc; i++) {
    double d;
    if (JS_ToFloat64 (ctx, &d, argv[i])) return JS_NULL;
    if (d > result) result = d;
  }
  return JS_NewFloat64 (ctx, result);
}

/* number.remainder(dividend, divisor) - remainder after division */
static JSValue js_cell_number_remainder (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;
  double dividend, divisor;
  if (JS_ToFloat64 (ctx, &dividend, argv[0])) return JS_NULL;
  if (JS_ToFloat64 (ctx, &divisor, argv[1])) return JS_NULL;
  if (divisor == 0) return JS_NULL;
  return JS_NewFloat64 (ctx,
                        dividend - (trunc (dividend / divisor) * divisor));
}

/* ----------------------------------------------------------------------------
 * text function and sub-functions
 * ----------------------------------------------------------------------------
 */

/* Helper: convert number to string with radix */
static JSValue js_cell_number_to_radix_string (JSContext *ctx, double num, int radix) {
  if (radix < 2 || radix > 36) return JS_NULL;

  /* For base 10, handle floating point properly */
  if (radix == 10) {
    char buf[64];
    /* Check if it's an integer */
    if (trunc (num) == num && num >= -9007199254740991.0
        && num <= 9007199254740991.0) {
      snprintf (buf, sizeof (buf), "%.0f", num);
    } else {
      /* Use %g to get a reasonable representation without trailing zeros */
      snprintf (buf, sizeof (buf), "%.15g", num);
    }
    return JS_NewString (ctx, buf);
  }

  /* For other radixes, use integer conversion */
  static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
  char buf[70];
  int len = 0;
  int negative = 0;
  int64_t n = (int64_t)trunc (num);

  if (n < 0) {
    negative = 1;
    n = -n;
  }

  if (n == 0) {
    buf[len++] = '0';
  } else {
    while (n > 0) {
      buf[len++] = digits[n % radix];
      n /= radix;
    }
  }

  if (negative) { buf[len++] = '-'; }

  /* Reverse the string */
  char result[72];
  int j = 0;
  for (int i = len - 1; i >= 0; i--) {
    result[j++] = buf[i];
  }
  result[j] = '\0';

  return JS_NewString (ctx, result);
}

/* Helper: add separator every n digits from right */
static char *add_separator (JSContext *ctx, const char *str, char sep, int n) {
  if (n <= 0) {
    char *result = js_malloc (ctx, strlen (str) + 1);
    if (result) strcpy (result, str);
    return result;
  }

  int negative = (str[0] == '-');
  const char *start = negative ? str + 1 : str;

  /* Find decimal point */
  const char *decimal = strchr (start, '.');
  int int_len = decimal ? (int)(decimal - start) : (int)strlen (start);

  int num_seps = (int_len - 1) / n;
  int result_len = strlen (str) + num_seps + 1;
  char *result = js_malloc (ctx, result_len);
  if (!result) return NULL;

  char *q = result;
  if (negative) *q++ = '-';

  int count = int_len % n;
  if (count == 0) count = n;

  for (int i = 0; i < int_len; i++) {
    if (i > 0 && count == 0) {
      *q++ = sep;
      count = n;
    }
    *q++ = start[i];
    count--;
  }

  if (decimal) {
    strcpy (q, decimal);
  } else {
    *q = '\0';
  }

  return result;
}

/* Helper: format number with format string */
static JSValue js_cell_format_number (JSContext *ctx, double num, const char *format) {
  int separation = 0;
  char style = '\0';
  int places = 0;
  int i = 0;

  /* Parse separation digit */
  if (format[i] >= '0' && format[i] <= '9') {
    separation = format[i] - '0';
    i++;
  }

  /* Parse style letter */
  if (format[i]) {
    style = format[i];
    i++;
  } else {
    return JS_NULL;
  }

  /* Parse places digits */
  if (format[i] >= '0' && format[i] <= '9') {
    places = format[i] - '0';
    i++;
    if (format[i] >= '0' && format[i] <= '9') {
      places = places * 10 + (format[i] - '0');
      i++;
    }
  }

  /* Invalid if more characters */
  if (format[i] != '\0') return JS_NULL;

  char buf[128];
  char *result_str = NULL;

  switch (style) {
  case 'e': {
    /* Exponential */
    if (places > 0)
      snprintf (buf, sizeof (buf), "%.*e", places, num);
    else
      snprintf (buf, sizeof (buf), "%e", num);
    return JS_NewString (ctx, buf);
  }
  case 'n': {
    /* Number - scientific for extreme values */
    if (fabs (num) >= 1e21 || (fabs (num) < 1e-6 && num != 0)) {
      snprintf (buf, sizeof (buf), "%e", num);
    } else if (places > 0) {
      snprintf (buf, sizeof (buf), "%.*f", places, num);
    } else {
      snprintf (buf, sizeof (buf), "%g", num);
    }
    return JS_NewString (ctx, buf);
  }
  case 's': {
    /* Space separated */
    if (separation == 0) separation = 3;
    snprintf (buf, sizeof (buf), "%.*f", places, num);
    result_str = add_separator (ctx, buf, ' ', separation);
    if (!result_str) return JS_EXCEPTION;
    JSValue ret = JS_NewString (ctx, result_str);
    js_free (ctx, result_str);
    return ret;
  }
  case 'u': {
    /* Underbar separated */
    snprintf (buf, sizeof (buf), "%.*f", places, num);
    if (separation > 0) {
      result_str = add_separator (ctx, buf, '_', separation);
      if (!result_str) return JS_EXCEPTION;
      JSValue ret = JS_NewString (ctx, result_str);
      js_free (ctx, result_str);
      return ret;
    }
    return JS_NewString (ctx, buf);
  }
  case 'd':
  case 'l': {
    /* Decimal/locale with comma separator */
    if (separation == 0) separation = 3;
    if (places == 0 && style == 'd') places = 2;
    snprintf (buf, sizeof (buf), "%.*f", places, num);
    result_str = add_separator (ctx, buf, ',', separation);
    if (!result_str) return JS_EXCEPTION;
    JSValue ret = JS_NewString (ctx, result_str);
    js_free (ctx, result_str);
    return ret;
  }
  case 'v': {
    /* European style: comma decimal, period separator */
    snprintf (buf, sizeof (buf), "%.*f", places, num);
    /* Replace . with , */
    for (char *p = buf; *p; p++) {
      if (*p == '.') *p = ',';
    }
    if (separation > 0) {
      result_str = add_separator (ctx, buf, '.', separation);
      if (!result_str) return JS_EXCEPTION;
      JSValue ret = JS_NewString (ctx, result_str);
      js_free (ctx, result_str);
      return ret;
    }
    return JS_NewString (ctx, buf);
  }
  case 'i': {
    /* Integer base 10 */
    if (places == 0) places = 1;
    int64_t n = (int64_t)trunc (num);
    int neg = n < 0;
    if (neg) n = -n;
    snprintf (buf, sizeof (buf), "%lld", (long long)n);
    int len = strlen (buf);
    /* Pad with zeros */
    if (len < places) {
      memmove (buf + (places - len), buf, len + 1);
      memset (buf, '0', places - len);
    }
    if (separation > 0) {
      result_str = add_separator (ctx, buf, '_', separation);
      if (!result_str) return JS_EXCEPTION;
      if (neg) {
        char *final = js_malloc (ctx, strlen (result_str) + 2);
        if (!final) {
          js_free (ctx, result_str);
          return JS_EXCEPTION;
        }
        final[0] = '-';
        strcpy (final + 1, result_str);
        js_free (ctx, result_str);
        JSValue ret = JS_NewString (ctx, final);
        js_free (ctx, final);
        return ret;
      }
      JSValue ret = JS_NewString (ctx, result_str);
      js_free (ctx, result_str);
      return ret;
    }
    if (neg) {
      memmove (buf + 1, buf, strlen (buf) + 1);
      buf[0] = '-';
    }
    return JS_NewString (ctx, buf);
  }
  case 'b': {
    /* Binary */
    if (places == 0) places = 1;
    return js_cell_number_to_radix_string (ctx, num, 2);
  }
  case 'o': {
    /* Octal */
    if (places == 0) places = 1;
    int64_t n = (int64_t)trunc (num);
    snprintf (buf, sizeof (buf), "%llo", (long long)(n < 0 ? -n : n));
    /* Uppercase and pad */
    for (char *p = buf; *p; p++)
      *p = toupper (*p);
    int len = strlen (buf);
    if (len < places) {
      memmove (buf + (places - len), buf, len + 1);
      memset (buf, '0', places - len);
    }
    if (n < 0) {
      memmove (buf + 1, buf, strlen (buf) + 1);
      buf[0] = '-';
    }
    return JS_NewString (ctx, buf);
  }
  case 'h': {
    /* Hexadecimal */
    if (places == 0) places = 1;
    int64_t n = (int64_t)trunc (num);
    snprintf (buf, sizeof (buf), "%llX", (long long)(n < 0 ? -n : n));
    int len = strlen (buf);
    if (len < places) {
      memmove (buf + (places - len), buf, len + 1);
      memset (buf, '0', places - len);
    }
    if (n < 0) {
      memmove (buf + 1, buf, strlen (buf) + 1);
      buf[0] = '-';
    }
    return JS_NewString (ctx, buf);
  }
  case 't': {
    /* Base32 */
    if (places == 0) places = 1;
    return js_cell_number_to_radix_string (ctx, num, 32);
  }
  }

  return JS_NULL;
}

/* Forward declaration for blob helper */
static JSBlob *checked_get_blob (JSContext *ctx, JSValue val);

/* modulo(dividend, divisor) - result has sign of divisor */
static JSValue js_cell_modulo (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;

  double dividend, divisor;
  if (JS_ToFloat64 (ctx, &dividend, argv[0])) return JS_NULL;
  if (JS_ToFloat64 (ctx, &divisor, argv[1])) return JS_NULL;

  /* If either operand is NaN, return null */
  if (isnan (dividend) || isnan (divisor)) return JS_NULL;

  /* If divisor is 0, return null */
  if (divisor == 0) return JS_NULL;

  /* If dividend is 0, return 0 */
  if (dividend == 0) return JS_NewFloat64 (ctx, 0.0);

  /* modulo = dividend - (divisor * floor(dividend / divisor)) */
  double result = dividend - (divisor * floor (dividend / divisor));

  return JS_NewFloat64 (ctx, result);
}

/* not(bool) - negate a boolean value */
static JSValue js_cell_not (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  if (!JS_IsBool (argv[0])) return JS_NULL;

  return JS_NewBool (ctx, !JS_ToBool (ctx, argv[0]));
}

/* neg(number) - negate a number */
static JSValue js_cell_neg (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  double num;
  if (JS_ToFloat64 (ctx, &num, argv[0])) return JS_NULL;

  if (isnan (num)) return JS_NULL;

  return JS_NewFloat64 (ctx, -num);
}

/* character(value) - get character from text or codepoint */
JSValue js_cell_character (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NewString (ctx, "");

  JSValue arg = argv[0];
  int tag = JS_VALUE_GET_TAG (arg);

  /* Handle string - return first character */
  if (JS_IsText (arg)) {
    if (js_string_value_len (arg) == 0) return JS_NewString (ctx, "");
    return js_sub_string_val (ctx, arg, 0, 1);
  }

  /* Handle integer - return character from codepoint */
  if (tag == JS_TAG_INT) {
    int32_t val = JS_VALUE_GET_INT (arg);
    if (val < 0 || val > 0x10FFFF) return JS_NewString (ctx, "");

    uint32_t codepoint = (uint32_t)val;
    if (codepoint < 0x80) {
      char buf[2] = { (char)codepoint, '\0' };
      return JS_NewString (ctx, buf);
    }
    /* Create single-codepoint UTF-32 string */
    JSText *str = js_alloc_string (ctx, 1);
    if (!str) return JS_EXCEPTION;
    string_put (str, 0, codepoint);
    str->length = 1;
    return pretext_end (ctx, str);
  }

  /* Handle float - convert to integer if non-negative and within range */
  if (tag == JS_TAG_FLOAT64) {
    double d = JS_VALUE_GET_FLOAT64 (arg);
    if (isnan (d) || d < 0 || d > 0x10FFFF || d != trunc (d))
      return JS_NewString (ctx, "");

    uint32_t codepoint = (uint32_t)d;
    if (codepoint < 0x80) {
      char buf[2] = { (char)codepoint, '\0' };
      return JS_NewString (ctx, buf);
    }
    /* Create single-codepoint UTF-32 string */
    JSText *str = js_alloc_string (ctx, 1);
    if (!str) return JS_EXCEPTION;
    string_put (str, 0, codepoint);
    str->length = 1;
    return pretext_end (ctx, str);
  }

  return JS_NewString (ctx, "");
}

/* text(arg, format) - main text function */
static JSValue js_cell_text (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  JSValue arg = argv[0];
  int tag = JS_VALUE_GET_TAG (arg);

  /* Handle string / rope */
  if (JS_IsText (arg)) {
    JSValue str = JS_ToString (ctx, arg); /* owned + flattens rope */
    if (JS_IsException (str)) return JS_EXCEPTION;

    if (argc == 1) return str;

    if (argc >= 2) {
      int tag1 = JS_VALUE_GET_TAG (argv[1]);
      if (tag1 == JS_TAG_INT || tag1 == JS_TAG_FLOAT64) {
        int len = js_string_value_len (str);
        int from, to;

        if (JS_ToInt32 (ctx, &from, argv[1]))
          return JS_EXCEPTION;

        if (from < 0) from += len;
        if (from < 0) from = 0;
        if (from > len) from = len;

        to = len;
        if (argc >= 3) {
          if (JS_ToInt32 (ctx, &to, argv[2]))
            return JS_EXCEPTION;
          if (to < 0) to += len;
          if (to < 0) to = 0;
          if (to > len) to = len;
        }

        if (from > to)
          return JS_NULL;

        return js_sub_string_val (ctx, str, from, to);
      }
    }

    return str;
  }

  /* Handle blob - convert to text representation */
  if (mist_is_blob (arg)) {
    JSBlob *bd = checked_get_blob (ctx, arg);
    if (!objhdr_s (bd->mist_hdr))
      return JS_ThrowTypeError (ctx, "text: blob must be stone");

    char format = '\0';
    if (argc > 1) {
      const char *fmt = JS_ToCString (ctx, argv[1]);
      if (!fmt) return JS_EXCEPTION;
      format = fmt[0];
      JS_FreeCString (ctx, fmt);
    }

    size_t byte_len = (bd->length + 7) / 8;
    const uint8_t *data = (const uint8_t *)bd->bits;

    if (format == 'h') {
      static const char hex[] = "0123456789abcdef";
      char *result = js_malloc (ctx, byte_len * 2 + 1);
      if (!result) return JS_EXCEPTION;
      for (size_t i = 0; i < byte_len; i++) {
        result[i * 2] = hex[(data[i] >> 4) & 0xF];
        result[i * 2 + 1] = hex[data[i] & 0xF];
      }
      result[byte_len * 2] = '\0';
      JSValue ret = JS_NewString (ctx, result);
      js_free (ctx, result);
      return ret;
    } else if (format == 'b') {
      char *result = js_malloc (ctx, bd->length + 1);
      if (!result) return JS_EXCEPTION;
      for (size_t i = 0; i < (size_t)bd->length; i++) {
        size_t byte_idx = i / 8;
        size_t bit_idx = i % 8;
        result[i] = (data[byte_idx] & (1u << bit_idx)) ? '1' : '0';
      }
      result[bd->length] = '\0';
      JSValue ret = JS_NewString (ctx, result);
      js_free (ctx, result);
      return ret;
    } else if (format == 'o') {
      size_t octal_len = ((size_t)bd->length + 2) / 3;
      char *result = js_malloc (ctx, octal_len + 1);
      if (!result) return JS_EXCEPTION;
      for (size_t i = 0; i < octal_len; i++) {
        int val = 0;
        for (int j = 0; j < 3; j++) {
          size_t bit_pos = i * 3 + (size_t)j;
          if (bit_pos < (size_t)bd->length) {
            size_t byte_idx = bit_pos / 8;
            size_t bit_idx = bit_pos % 8;
            if (data[byte_idx] & (1u << bit_idx)) val |= (1 << j);
          }
        }
        result[i] = (char)('0' + val);
      }
      result[octal_len] = '\0';
      JSValue ret = JS_NewString (ctx, result);
      js_free (ctx, result);
      return ret;
    } else if (format == 't') {
      static const char b32[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
      size_t b32_len = ((size_t)bd->length + 4) / 5;
      char *result = js_malloc (ctx, b32_len + 1);
      if (!result) return JS_EXCEPTION;
      for (size_t i = 0; i < b32_len; i++) {
        int val = 0;
        for (int j = 0; j < 5; j++) {
          size_t bit_pos = i * 5 + (size_t)j;
          if (bit_pos < (size_t)bd->length) {
            size_t byte_idx = bit_pos / 8;
            size_t bit_idx = bit_pos % 8;
            if (data[byte_idx] & (1u << bit_idx)) val |= (1 << j);
          }
        }
        result[i] = b32[val & 31];
      }
      result[b32_len] = '\0';
      JSValue ret = JS_NewString (ctx, result);
      js_free (ctx, result);
      return ret;
    } else {
      if (bd->length % 8 != 0)
        return JS_ThrowTypeError (ctx,
                                  "text: blob not byte-aligned for UTF-8");
      return JS_NewStringLen (ctx, (const char *)data, byte_len);
    }
  }

  /* Handle number */
  if (tag == JS_TAG_INT || tag == JS_TAG_FLOAT64) {
    double num;
    if (JS_ToFloat64 (ctx, &num, arg)) return JS_EXCEPTION;

    if (argc > 1) {
      int tag1 = JS_VALUE_GET_TAG (argv[1]);
      if (tag1 == JS_TAG_INT) {
        int radix = JS_VALUE_GET_INT (argv[1]);
        return js_cell_number_to_radix_string (ctx, num, radix);
      }
      if (tag1 == JS_TAG_STRING || tag1 == JS_TAG_STRING_IMM) {
        const char *format = JS_ToCString (ctx, argv[1]);
        if (!format) return JS_EXCEPTION;
        JSValue result = js_cell_format_number (ctx, num, format);
        JS_FreeCString (ctx, format);
        return result;
      }
    }

    return js_cell_number_to_radix_string (ctx, num, 10);
  }

  /* Handle array */
  if (JS_IsArray (arg)) {
    int64_t len;
    JSGCRef arg_ref;
    JS_AddGCRef(ctx, &arg_ref);
    arg_ref.val = arg;
    if (js_get_length64 (ctx, &len, arg_ref.val)) {
      JS_DeleteGCRef(ctx, &arg_ref);
      return JS_EXCEPTION;
    }

    const char *separator = "";
    BOOL sep_alloc = FALSE;

    if (argc > 1 && JS_VALUE_IS_TEXT (argv[1])) {
      separator = JS_ToCString (ctx, argv[1]);
      if (!separator) return JS_EXCEPTION;
      sep_alloc = TRUE;
    }

    JSText *b = pretext_init (ctx, 0);
    if (!b) {
      if (sep_alloc) JS_FreeCString (ctx, separator);
      return JS_EXCEPTION;
    }

    /* Root b across allocating calls (JS_GetPropertyNumber, JS_ToString) */
    JSGCRef b_ref;
    JS_AddGCRef (ctx, &b_ref);
    b_ref.val = JS_MKPTR (b);

    for (int64_t i = 0; i < len; i++) {
      if (i > 0 && separator[0]) {
        b = (JSText *)chase (b_ref.val);
        b = pretext_puts8 (ctx, b, separator);
        if (!b) goto array_fail;
        b_ref.val = JS_MKPTR (b);
      }

      b = (JSText *)chase (b_ref.val);  /* re-chase before use */
      JSValue item = JS_GetPropertyNumber (ctx, arg_ref.val, i);
      if (JS_IsException (item)) goto array_fail;

      if (!JS_VALUE_IS_TEXT (item)) {
        if (sep_alloc) JS_FreeCString (ctx, separator);
        JS_DeleteGCRef (ctx, &b_ref);
        JS_DeleteGCRef (ctx, &arg_ref);
        return JS_ThrowTypeError (ctx, "text: array element is not a string");
      }

      JSValue item_str = JS_ToString (ctx, item);
      if (JS_IsException (item_str)) goto array_fail;

      b = (JSText *)chase (b_ref.val);  /* re-chase after JS_ToString */
      b = pretext_concat_value (ctx, b, item_str);
      if (!b) goto array_fail;
      b_ref.val = JS_MKPTR (b);
    }

    b = (JSText *)chase (b_ref.val);
    if (sep_alloc) JS_FreeCString (ctx, separator);
    JS_DeleteGCRef (ctx, &b_ref);
    JS_DeleteGCRef (ctx, &arg_ref);
    return pretext_end (ctx, b);

  array_fail:
    if (sep_alloc) JS_FreeCString (ctx, separator);
    JS_DeleteGCRef (ctx, &b_ref);
    JS_DeleteGCRef (ctx, &arg_ref);
    return JS_EXCEPTION;
  }

  /* Handle function - return source or native stub */
  if (JS_IsFunction (arg)) {
    JSFunction *fn = JS_VALUE_GET_FUNCTION (arg);

    const char *pref = "function ";
    const char *suff = "() {\n    [native code]\n}";
    const char *name = "";
    const char *name_cstr = NULL;

    if (!JS_IsNull (fn->name)) {
      name_cstr = JS_ToCString (ctx, fn->name);
      if (name_cstr) name = name_cstr;
    }

    size_t plen = strlen (pref);
    size_t nlen = strlen (name);
    size_t slen = strlen (suff);

    char *result = js_malloc (ctx, plen + nlen + slen + 1);
    if (!result) {
      if (name_cstr) JS_FreeCString (ctx, name_cstr);
      return JS_EXCEPTION;
    }

    memcpy (result, pref, plen);
    memcpy (result + plen, name, nlen);
    memcpy (result + plen + nlen, suff, slen + 1);

    JSValue ret = JS_NewString (ctx, result);
    js_free (ctx, result);
    if (name_cstr) JS_FreeCString (ctx, name_cstr);
    return ret;
  }

  return JS_ToString (ctx, arg);
  return JS_ThrowInternalError (ctx, "Could not convert to text. Tag is %d", tag);
}

/* text.lower(str) - convert to lowercase */
JSValue js_cell_text_lower (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;
  if (!JS_VALUE_IS_TEXT (argv[0])) return JS_NULL;

  /* Handle immediate ASCII - no GC concern */
  if (MIST_IsImmediateASCII (argv[0])) {
    int len = MIST_GetImmediateASCIILen (argv[0]);
    JSText *b = pretext_init (ctx, len);
    if (!b) return JS_EXCEPTION;
    for (int i = 0; i < len; i++) {
      uint32_t c = MIST_GetImmediateASCIIChar (argv[0], i);
      if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
      b = pretext_putc (ctx, b, c);
      if (!b) return JS_EXCEPTION;
    }
    return pretext_end (ctx, b);
  }

  /* Heap text: must re-chase after GC points */
  int len = (int)JSText_len (JS_VALUE_GET_STRING (argv[0]));
  JSText *b = pretext_init (ctx, len);
  if (!b) return JS_EXCEPTION;

  for (int i = 0; i < len; i++) {
    JSText *p = JS_VALUE_GET_STRING (argv[0]);  /* Re-chase each iteration */
    uint32_t c = string_get (p, i);
    if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
    b = pretext_putc (ctx, b, c);
    if (!b) return JS_EXCEPTION;
  }

  return pretext_end (ctx, b);
}

/* text.upper(str) - convert to uppercase */
JSValue js_cell_text_upper (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;
  if (!JS_VALUE_IS_TEXT (argv[0])) return JS_NULL;

  /* Handle immediate ASCII - no GC concern */
  if (MIST_IsImmediateASCII (argv[0])) {
    int len = MIST_GetImmediateASCIILen (argv[0]);
    JSText *b = pretext_init (ctx, len);
    if (!b) return JS_EXCEPTION;
    for (int i = 0; i < len; i++) {
      uint32_t c = MIST_GetImmediateASCIIChar (argv[0], i);
      if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
      b = pretext_putc (ctx, b, c);
      if (!b) return JS_EXCEPTION;
    }
    return pretext_end (ctx, b);
  }

  /* Heap text: must re-chase after GC points */
  int len = (int)JSText_len (JS_VALUE_GET_STRING (argv[0]));
  JSText *b = pretext_init (ctx, len);
  if (!b) return JS_EXCEPTION;

  for (int i = 0; i < len; i++) {
    JSText *p = JS_VALUE_GET_STRING (argv[0]);  /* Re-chase each iteration */
    uint32_t c = string_get (p, i);
    if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
    b = pretext_putc (ctx, b, c);
    if (!b) return JS_EXCEPTION;
  }

  return pretext_end (ctx, b);
}

/* text.trim(str, reject) - trim whitespace or custom characters */
static JSValue js_cell_text_trim (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;
  if (!JS_IsText (argv[0])) return JS_NULL;

  JSValue str = argv[0];
  int start = 0;
  int end = js_string_value_len (str);

  if (argc > 1 && !JS_IsNull (argv[1])) {
    /* Custom trim with reject characters */
    const char *reject = JS_ToCString (ctx, argv[1]);
    if (!reject) return JS_EXCEPTION;
    size_t reject_len = strlen (reject);

    while (start < end) {
      uint32_t c = js_string_value_get (str, start);
      int found = 0;
      for (size_t i = 0; i < reject_len; i++) {
        if (c == (uint8_t)reject[i]) {
          found = 1;
          break;
        }
      }
      if (!found) break;
      start++;
    }
    while (end > start) {
      uint32_t c = js_string_value_get (str, end - 1);
      int found = 0;
      for (size_t i = 0; i < reject_len; i++) {
        if (c == (uint8_t)reject[i]) {
          found = 1;
          break;
        }
      }
      if (!found) break;
      end--;
    }
    JS_FreeCString (ctx, reject);
  } else {
    /* Default: trim whitespace */
    while (start < end && lre_is_space (js_string_value_get (str, start)))
      start++;
    while (end > start && lre_is_space (js_string_value_get (str, end - 1)))
      end--;
  }

  return js_sub_string_val (ctx, str, start, end);
}

/* text.codepoint(str) - get first codepoint */
JSValue js_cell_text_codepoint (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;
  if (!JS_IsText (argv[0])) return JS_NULL;

  /* Handle immediate strings directly */
  if (MIST_IsImmediateASCII (argv[0])) {
    int plen = MIST_GetImmediateASCIILen (argv[0]);
    if (plen == 0) return JS_NULL;
    uint32_t c = MIST_GetImmediateASCIIChar (argv[0], 0);
    return JS_NewInt32 (ctx, c);
  }

  /* Heap string */
  JSText *p = JS_VALUE_GET_STRING (argv[0]);
  int plen = (int)JSText_len (p);
  if (plen == 0) {
    return JS_NULL;
  }

  uint32_t c = string_get (p, 0);
  /* Handle surrogate pairs */
  if (c >= 0xD800 && c <= 0xDBFF && plen > 1) {
    uint32_t c2 = string_get (p, 1);
    if (c2 >= 0xDC00 && c2 <= 0xDFFF) {
      c = 0x10000 + ((c - 0xD800) << 10) + (c2 - 0xDC00);
    }
  }

  return JS_NewInt32 (ctx, c);
}

/* Helpers (C, not C++). Put these above js_cell_text_replace in the same C
 * file. */

static JSText *pt_concat_value_to_string_free (JSContext *ctx, JSText *b, JSValue v) {
  /* Root b across JS_ToString which can allocate and trigger GC */
  JSGCRef b_ref, s_ref;
  JS_PushGCRef (ctx, &b_ref);
  b_ref.val = JS_MKPTR (b);

  JSValue s = JS_ToString (ctx, v);
  if (JS_IsException (s)) {
    JS_PopGCRef (ctx, &b_ref);
    return NULL;
  }

  /* Root s — pretext_concat_value can trigger GC and move the heap string */
  JS_PushGCRef (ctx, &s_ref);
  s_ref.val = s;

  b = (JSText *)chase (b_ref.val);  /* re-fetch after possible GC */
  b = pretext_concat_value (ctx, b, s_ref.val);

  JS_PopGCRef (ctx, &s_ref);
  JS_PopGCRef (ctx, &b_ref);
  return b;
}

/* Build replacement for a match at `found`.
 * - If replacement is a function: call it as (match_text, found)
 * - Else if replacement exists: duplicate it
 * - Else: empty string
 * Returns JS_EXCEPTION on error, JS_NULL if callback returned null, or any
 * JSValue. This function CONSUMES match_val if it calls a function (it will
 * free it via args cleanup), otherwise it will free match_val before
 * returning.
 */
static JSValue make_replacement (JSContext *ctx, int argc, JSValue *argv, int found, JSValue match_val) {
  JSValue rep;

  if (argc > 2 && JS_IsFunction (argv[2])) {
    JSValue args[2];
    args[0] = match_val;
    args[1] = JS_NewInt32 (ctx, found);
    rep = JS_Call (ctx, argv[2], JS_NULL, 2, args);
    return rep;
  }


  if (argc > 2) return argv[2];
  return JS_KEY_empty;
}

static int JS_IsRegExp (JSContext *ctx, JSValue v) {
  if (!mist_is_gc_object (v)) return 0;

  JSValue exec = JS_GetPropertyStr (ctx, v, "exec");
  if (JS_IsException (exec)) return -1;

  int ok = JS_IsFunction (exec);
  return ok;
}

/* text.replace(text, target, replacement, limit)
 *
 * Return a new text in which the target is replaced by the replacement.
 *
 * target: string (pattern support not implemented here; non-string => null)
 * replacement: string or function(match_text, start_pos) -> string|null
 * limit: max number of replacements (default unlimited). Limit includes null
 * matches.
 *
 * Empty target semantics:
 *   Replace at every boundary: before first char, between chars, after last
 * char. Example: replace("abc", "", "-") => "-a-b-c-" Boundaries count toward
 * limit even if replacement returns null.
 */

static JSValue js_cell_text_replace (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;

  if (!JS_IsText (argv[0]))
    return JS_NULL;

  int target_is_regex = 0;
  {
    if (JS_IsText (argv[1])) {
      target_is_regex = 0;
    } else if (mist_is_gc_object (argv[1]) && JS_IsRegExp (ctx, argv[1])) {
      target_is_regex = 1;
    } else {
      return JS_NULL;
    }
  }

  if (!JS_VALUE_IS_TEXT (argv[0]))
    return JS_ThrowInternalError (ctx, "Replace must have text in arg0.");

  int len = js_string_value_len (argv[0]);

  int32_t limit = -1;
  if (argc > 3 && !JS_IsNull (argv[3])) {
    if (JS_ToInt32 (ctx, &limit, argv[3])) { return JS_NULL; }
    if (limit < 0) limit = -1;
  }

  JSText *b = pretext_init (ctx, len);
  if (!b) return JS_EXCEPTION;

  /* Root b across all allocating calls */
  JSGCRef b_ref;
  JS_PushGCRef (ctx, &b_ref);
  b_ref.val = JS_MKPTR (b);

/* Macro to re-chase b from GC ref before use */
#define B_RECHASE() b = (JSText *)chase (b_ref.val)

/* Macro to update b_ref after b changes */
#define B_UPDATE(new_b) do { b = (new_b); b_ref.val = JS_MKPTR (b); } while(0)
#define B_CLEANUP() JS_PopGCRef (ctx, &b_ref)

  if (!target_is_regex) {
    if (!JS_VALUE_IS_TEXT (argv[1])) {
      B_CLEANUP ();
      return JS_ThrowInternalError (
        ctx, "Second arg of replace must be pattern or text.");
    }

    int t_len = js_string_value_len (argv[1]);

    if (t_len == 0) {
      int32_t count = 0;

      for (int boundary = 0; boundary <= len; boundary++) {
        if (limit >= 0 && count >= limit) break;

        JSValue match = JS_KEY_empty;
        if (JS_IsException (match)) { B_CLEANUP (); goto fail_str_target; }

        JSValue rep = make_replacement (ctx, argc, argv, boundary, match);
        if (JS_IsException (rep)) { B_CLEANUP (); goto fail_str_target; }

        count++;

        if (!JS_IsNull (rep)) {
          B_RECHASE ();
          B_UPDATE (pt_concat_value_to_string_free (ctx, b, rep));
          if (!b) { B_CLEANUP (); goto fail_str_target; }
        }

        if (boundary < len) {
          JSValue ch = js_sub_string_val (ctx, argv[0], boundary, boundary + 1);
          if (JS_IsException (ch)) { B_CLEANUP (); goto fail_str_target; }
          B_RECHASE ();
          B_UPDATE (pretext_concat_value (ctx, b, ch));
          if (!b) { B_CLEANUP (); goto fail_str_target; }
        }
      }

      B_RECHASE ();
      B_CLEANUP ();
      return pretext_end (ctx, b);
    }

    int pos = 0;
    int32_t count = 0;

    while (pos <= len - t_len && (limit < 0 || count < limit)) {
      int found = -1;

      /* Search for pattern using character-by-character comparison */
      for (int i = pos; i <= len - t_len; i++) {
        int match = 1;
        for (int j = 0; j < t_len; j++) {
          if (js_string_value_get (argv[0], i + j) != js_string_value_get (argv[1], j)) {
            match = 0;
            break;
          }
        }
        if (match) {
          found = i;
          break;
        }
      }
      if (found < 0) break;

      if (found > pos) {
        int sub_len = found - pos;
        JSText *sub_str = js_alloc_string (ctx, sub_len);
        if (!sub_str) { B_CLEANUP (); goto fail_str_target; }
        for (int i = 0; i < sub_len; i++) {
          string_put (sub_str, i, js_string_value_get (argv[0], pos + i));
        }
        sub_str->length = sub_len;
        JSValue sub = pretext_end (ctx, sub_str);
        if (JS_IsException (sub)) { B_CLEANUP (); goto fail_str_target; }
        B_RECHASE ();
        B_UPDATE (pretext_concat_value (ctx, b, sub));
        if (!b) { B_CLEANUP (); goto fail_str_target; }
      }

      /* Build match substring manually */
      JSText *match_str = js_alloc_string (ctx, t_len);
      if (!match_str) { B_CLEANUP (); goto fail_str_target; }
      for (int i = 0; i < t_len; i++) {
        string_put (match_str, i, js_string_value_get (argv[0], found + i));
      }
      match_str->length = t_len;
      JSValue match = pretext_end (ctx, match_str);
      if (JS_IsException (match)) { B_CLEANUP (); goto fail_str_target; }

      JSValue rep = make_replacement (ctx, argc, argv, found, match);
      if (JS_IsException (rep)) { B_CLEANUP (); goto fail_str_target; }

      count++;

      if (!JS_IsNull (rep)) {
        B_RECHASE ();
        B_UPDATE (pt_concat_value_to_string_free (ctx, b, rep));
        if (!b) { B_CLEANUP (); goto fail_str_target; }
      }

      pos = found + t_len;
    }

    if (pos < len) {
      int sub_len = len - pos;
      JSText *sub_str = js_alloc_string (ctx, sub_len);
      if (!sub_str) { B_CLEANUP (); goto fail_str_target; }
      for (int i = 0; i < sub_len; i++) {
        string_put (sub_str, i, js_string_value_get (argv[0], pos + i));
      }
      sub_str->length = sub_len;
      JSValue sub = pretext_end (ctx, sub_str);
      if (JS_IsException (sub)) { B_CLEANUP (); goto fail_str_target; }
      B_RECHASE ();
      B_UPDATE (pretext_concat_value (ctx, b, sub));
      if (!b) { B_CLEANUP (); goto fail_str_target; }
    }

    B_RECHASE ();
    B_CLEANUP ();
    return pretext_end (ctx, b);

  fail_str_target:
    return JS_EXCEPTION;
  }

  /* Regex target - root rx across allocating calls */
  JSGCRef rx_ref;
  JS_PushGCRef (ctx, &rx_ref);
  rx_ref.val = argv[1];

#define RX_CLEANUP() do { JS_PopGCRef (ctx, &rx_ref); B_CLEANUP (); } while(0)
#define RX_VAL (rx_ref.val)

  JSValue orig_last_index = JS_GetPropertyStr (ctx, RX_VAL, "lastIndex");
  if (JS_IsException (orig_last_index)) { RX_CLEANUP (); goto fail_rx; }
  int have_orig_last_index = 1;

  int pos = 0;
  int32_t count = 0;

  while (pos <= len && (limit < 0 || count < limit)) {
    if (JS_SetPropertyStr (ctx, RX_VAL, "lastIndex", JS_NewInt32 (ctx, 0)) < 0) {
      RX_CLEANUP (); goto fail_rx;
    }

    JSValue sub_str = js_sub_string_val (ctx, argv[0], pos, len);
    if (JS_IsException (sub_str)) { RX_CLEANUP (); goto fail_rx; }

    JSValue exec_res
      = JS_Invoke (ctx, RX_VAL, JS_KEY_exec, 1, (JSValue *)&sub_str);
    if (JS_IsException (exec_res)) { RX_CLEANUP (); goto fail_rx; }

    if (JS_IsNull (exec_res)) {
      break;
    }

    JSValue idx_val = JS_GetPropertyStr (ctx, exec_res, "index");
    if (JS_IsException (idx_val)) { RX_CLEANUP (); goto fail_rx; }

    int32_t local_index = 0;
    if (JS_ToInt32 (ctx, &local_index, idx_val)) { RX_CLEANUP (); goto fail_rx; }

    if (local_index < 0) local_index = 0;
    int found = pos + local_index;
    if (found < pos) found = pos;
    if (found > len) {
      break;
    }

    JSValue match = JS_GetPropertyStr (ctx, exec_res, "match");
    if (JS_IsException (match)) { RX_CLEANUP (); goto fail_rx; }

    JSValue end_val = JS_GetPropertyStr (ctx, exec_res, "end");
    if (JS_IsException (end_val)) { RX_CLEANUP (); goto fail_rx; }

    int32_t end = 0;
    if (JS_ToInt32 (ctx, &end, end_val)) { RX_CLEANUP (); goto fail_rx; }

    int match_len = end - local_index;
    if (match_len < 0) match_len = 0;

    if (found > pos) {
      JSValue prefix = js_sub_string_val (ctx, argv[0], pos, found);
      if (JS_IsException (prefix)) { RX_CLEANUP (); goto fail_rx; }
      B_RECHASE ();
      B_UPDATE (pretext_concat_value (ctx, b, prefix));
      if (!b) { RX_CLEANUP (); goto fail_rx; }
    }

    JSValue rep = make_replacement (ctx, argc, argv, found, match);
    if (JS_IsException (rep)) { RX_CLEANUP (); goto fail_rx; }

    count++;

    if (!JS_IsNull (rep)) {
      B_RECHASE ();
      B_UPDATE (pt_concat_value_to_string_free (ctx, b, rep));
      if (!b) { RX_CLEANUP (); goto fail_rx; }
    }

    pos = found + match_len;
    if (match_len == 0) {
      if (pos < len)
        pos++;
      else
        break;
    }
  }

  if (pos < len) {
    JSValue tail = js_sub_string_val (ctx, argv[0], pos, len);
    if (JS_IsException (tail)) { RX_CLEANUP (); goto fail_rx; }
    B_RECHASE ();
    B_UPDATE (pretext_concat_value (ctx, b, tail));
    if (!b) { RX_CLEANUP (); goto fail_rx; }
  }

  if (have_orig_last_index)
    JS_SetPropertyStr (ctx, RX_VAL, "lastIndex", orig_last_index);

  B_RECHASE ();
  RX_CLEANUP ();
  return pretext_end (ctx, b);

fail_rx:
  if (!JS_IsNull (orig_last_index) && !JS_IsException (orig_last_index)) {
    JS_SetPropertyStr (ctx, argv[1], "lastIndex", orig_last_index);
  } else {
  }
  return JS_EXCEPTION;
}

#undef RX_CLEANUP
#undef RX_VAL
#undef B_RECHASE
#undef B_UPDATE
#undef B_CLEANUP

/* text.search(str, target, from) - find substring or regex match */
static JSValue js_cell_text_search (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;

  if (!JS_IsText (argv[0])) return JS_NULL;

  int target_is_regex = 0;
  if (JS_IsText (argv[1])) {
    target_is_regex = 0;
  } else if (mist_is_gc_object (argv[1]) && JS_IsRegExp (ctx, argv[1])) {
    target_is_regex = 1;
  } else {
    return JS_NULL;
  }

  JSValue str = argv[0];
  int len = js_string_value_len (str);

  int from = 0;
  if (argc > 2 && !JS_IsNull (argv[2])) {
    if (JS_ToInt32 (ctx, &from, argv[2])) {
      return JS_NULL;
    }
    if (from < 0) from += len;
    if (from < 0) from = 0;
  }
  if (from > len) {
    return JS_NULL;
  }

  if (!target_is_regex) {
    JSValue target = argv[1];
    int t_len = js_string_value_len (target);

    int result = -1;
    if (len >= t_len) {
      for (int i = from; i <= len - t_len; i++) {
        int match = 1;
        for (int j = 0; j < t_len; j++) {
          if (js_string_value_get (str, i + j) != js_string_value_get (target, j)) {
            match = 0;
            break;
          }
        }
        if (match) {
          result = i;
          break;
        }
      }
    }

    if (result == -1) return JS_NULL;
    return JS_NewInt32 (ctx, result);
  }

  /* Regex target - root rx and str across allocating calls */
  JSGCRef rx_ref, str_ref;
  JS_PushGCRef (ctx, &rx_ref);
  rx_ref.val = argv[1];
  JS_PushGCRef (ctx, &str_ref);
  str_ref.val = str;

#define SEARCH_CLEANUP() do { JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &rx_ref); } while(0)

  JSValue orig_last_index = JS_GetPropertyStr (ctx, rx_ref.val, "lastIndex");
  if (JS_IsException (orig_last_index)) {
    SEARCH_CLEANUP ();
    return JS_EXCEPTION;
  }
  int have_orig_last_index = 1;

  if (JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", JS_NewInt32 (ctx, 0)) < 0)
    goto fail_rx_search;

  JSValue sub_str = js_sub_string_val (ctx, str_ref.val, from, len);
  if (JS_IsException (sub_str)) goto fail_rx_search;

  JSValue exec_res = JS_Invoke (ctx, rx_ref.val, JS_KEY_exec, 1, (JSValue *)&sub_str);
  if (JS_IsException (exec_res)) goto fail_rx_search;

  if (JS_IsNull (exec_res)) {
    if (have_orig_last_index)
      JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index);
    SEARCH_CLEANUP ();
    return JS_NULL;
  }

  JSValue idx_val = JS_GetPropertyStr (ctx, exec_res, "index");
  if (JS_IsException (idx_val)) {
    goto fail_rx_search;
  }

  int32_t local_index = 0;
  if (JS_ToInt32 (ctx, &local_index, idx_val)) {
    goto fail_rx_search;
  }

  if (local_index < 0) local_index = 0;

  if (have_orig_last_index)
    JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index);

  SEARCH_CLEANUP ();
  return JS_NewInt32 (ctx, from + local_index);

fail_rx_search:
  if (!JS_IsNull (orig_last_index) && !JS_IsException (orig_last_index)) {
    JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index);
  }
  SEARCH_CLEANUP ();
  return JS_EXCEPTION;
}
#undef SEARCH_CLEANUP


/* text_extract(text, pattern, from?, to?) - return array of matches or null
   - literal pattern: [match]
   - regexp pattern: [full_match, cap1, cap2, ...]
*/
static JSValue js_cell_text_extract (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;

  if (!JS_IsText (argv[0]))
    return JS_NULL;

  JSValue str = argv[0];
  int len = js_string_value_len (str);

  int from = 0;
  if (argc >= 3 && !JS_IsNull (argv[2])) {
    if (JS_ToInt32 (ctx, &from, argv[2])) return JS_EXCEPTION;
    if (from < 0) from += len;
    if (from < 0) from = 0;
    if (from > len) from = len;
  }

  int to = len;
  if (argc >= 4 && !JS_IsNull (argv[3])) {
    if (JS_ToInt32 (ctx, &to, argv[3])) return JS_EXCEPTION;
    if (to < 0) to += len;
    if (to < 0) to = 0;
    if (to > len) to = len;
  }

  if (from > to) return JS_NULL;

  /* RegExp path: convert new exec result record -> classic array */
  if (mist_is_gc_object (argv[1]) && JS_IsRegExp (ctx, argv[1])) {
    /* Root rx, str, out across allocating calls */
    JSGCRef rx_ref, str_ref, out_ref;
    JS_PushGCRef (ctx, &rx_ref);
    rx_ref.val = argv[1];
    JS_PushGCRef (ctx, &str_ref);
    str_ref.val = str;
    JS_PushGCRef (ctx, &out_ref);
    out_ref.val = JS_NULL;

#define EXT_CLEANUP() do { JS_PopGCRef (ctx, &out_ref); JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &rx_ref); } while(0)

    JSValue orig_last_index = JS_GetPropertyStr (ctx, rx_ref.val, "lastIndex");
    if (JS_IsException (orig_last_index)) { EXT_CLEANUP (); return JS_EXCEPTION; }

    if (JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", JS_NewInt32 (ctx, 0)) < 0)
      goto fail_rx;

    JSValue sub_str;
    if (from == 0 && to == len) {
      sub_str = str_ref.val;
    } else {
      sub_str = js_sub_string_val (ctx, str_ref.val, from, to);
      if (JS_IsException (sub_str)) goto fail_rx;
    }

    JSGCRef exec_ref;
    JS_PushGCRef (ctx, &exec_ref);
    exec_ref.val = JS_Invoke (ctx, rx_ref.val, JS_KEY_exec, 1, (JSValue *)&sub_str);
    if (JS_IsException (exec_ref.val)) { JS_PopGCRef (ctx, &exec_ref); goto fail_rx; }

    if (JS_IsNull (exec_ref.val)) {
      JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index);
      JS_PopGCRef (ctx, &exec_ref);
      EXT_CLEANUP ();
      return JS_NULL;
    }

    /* Build result array */
    JSValue out = JS_NewArray (ctx);
    if (JS_IsException (out)) {
      JS_PopGCRef (ctx, &exec_ref);
      goto fail_rx;
    }
    out_ref.val = out;

    /* out[0] = exec_res.match */
    JSValue match0 = JS_GetPropertyStr (ctx, exec_ref.val, "match");
    if (JS_IsException (match0)) {
      JS_PopGCRef (ctx, &exec_ref);
      goto fail_rx;
    }
    out = out_ref.val;
    if (JS_IsException (JS_SetPropertyNumber (ctx, out, 0, match0))) {
      JS_PopGCRef (ctx, &exec_ref);
      goto fail_rx;
    }

    /* Append capture groups from exec_res.captures */
    JSValue caps = JS_GetPropertyStr (ctx, exec_ref.val, "captures");
    JS_PopGCRef (ctx, &exec_ref); /* exec_ref no longer needed */
    if (!JS_IsException (caps) && JS_IsArray (caps)) {
      int64_t caps_len = 0;
      if (js_get_length64 (ctx, &caps_len, caps) == 0 && caps_len > 0) {
        for (int64_t i = 0; i < caps_len; i++) {
          JSValue cap = JS_GetPropertyNumber (ctx, caps, i);
          if (JS_IsException (cap)) {
            goto fail_rx;
          }
          out = out_ref.val;
          if (JS_IsException (JS_SetPropertyNumber (ctx, out, i + 1, cap))) {
            goto fail_rx;
          }
        }
      }
    }

    JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index);
    out = out_ref.val;
    EXT_CLEANUP ();
    return out;

  fail_rx:
    if (!JS_IsNull (orig_last_index) && !JS_IsException (orig_last_index)) {
      JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index);
    }
    EXT_CLEANUP ();
    return JS_EXCEPTION;
  }
#undef EXT_CLEANUP

  /* Literal text path */
  JSValue needle_val = JS_ToString (ctx, argv[1]);
  if (JS_IsException (needle_val)) return JS_EXCEPTION;
  str = argv[0]; /* refresh after potential GC */

  int needle_len = js_string_value_len (needle_val);

  /* Find needle in str[from..to) */
  int pos = -1;
  if (needle_len == 0) {
    pos = from;
  } else if (needle_len <= (to - from)) {
    int limit = to - needle_len;
    for (int i = from; i <= limit; i++) {
      int j = 0;
      for (; j < needle_len; j++) {
        if (js_string_value_get (str, i + j) != js_string_value_get (needle_val, j))
          break;
      }
      if (j == needle_len) { pos = i; break; }
    }
  }

  if (pos < 0) return JS_NULL;

  JSGCRef arr_ref;
  JS_PushGCRef (ctx, &arr_ref);
  arr_ref.val = JS_NewArrayLen (ctx, 1);
  if (JS_IsException (arr_ref.val)) { JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; }
  str = argv[0]; /* refresh after potential GC */

  JSValue match = js_sub_string_val (ctx, str, pos, pos + needle_len);
  if (JS_IsException (match)) { JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; }

  JSValue arr = arr_ref.val;
  JS_PopGCRef (ctx, &arr_ref);

  if (JS_IsException (JS_SetPropertyNumber (ctx, arr, 0, match)))
    return JS_EXCEPTION;

  return arr;
}

/* format(text, collection, transformer) - string interpolation
 * Finds {name} or {name:format} patterns and substitutes from collection.
 * Collection can be array (index by number) or record (index by key).
 * Transformer can be function(value, format) or record of functions.
 */
static JSValue js_cell_text_format (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;
  if (!JS_IsText (argv[0])) return JS_NULL;

  JSValue text_val = argv[0];
  JSValue collection = argv[1];
  JSValue transformer = argc > 2 ? argv[2] : JS_NULL;

  int is_array = JS_IsArray (collection);
  int is_record = JS_IsRecord (collection);
  if (!is_array && !is_record) return JS_NULL;

  int len = js_string_value_len (text_val);

  /* Root text_val, collection, transformer BEFORE any allocation */
  JSGCRef res_ref, text_ref, coll_ref, xform_ref;
  JS_PushGCRef (ctx, &res_ref);
  JS_PushGCRef (ctx, &text_ref);
  JS_PushGCRef (ctx, &coll_ref);
  JS_PushGCRef (ctx, &xform_ref);
  res_ref.val = JS_NULL;
  text_ref.val = text_val;
  coll_ref.val = collection;
  xform_ref.val = transformer;

#define FMT_CLEANUP() do { \
  JS_PopGCRef (ctx, &xform_ref); \
  JS_PopGCRef (ctx, &coll_ref); \
  JS_PopGCRef (ctx, &text_ref); \
  JS_PopGCRef (ctx, &res_ref); \
} while(0)

  JSText *result = pretext_init (ctx, len);
  if (!result) { FMT_CLEANUP(); return JS_EXCEPTION; }
  res_ref.val = JS_MKPTR (result);

  int pos = 0;
  while (pos < len) {
    text_val = text_ref.val;
    /* Find next '{' */
    int brace_start = -1;
    for (int i = pos; i < len; i++) {
      if (js_string_value_get (text_val, i) == '{') {
        brace_start = i;
        break;
      }
    }

    if (brace_start < 0) {
      /* No more braces, copy rest of string */
      JSValue tail = js_sub_string_val (ctx, text_ref.val, pos, len);
      if (JS_IsException (tail)) { FMT_CLEANUP(); return JS_EXCEPTION; }
      result = (JSText *)chase (res_ref.val);
      result = pretext_concat_value (ctx, result, tail);
      if (result) res_ref.val = JS_MKPTR (result);
      break;
    }

    /* Copy text before brace */
    if (brace_start > pos) {
      JSValue prefix = js_sub_string_val (ctx, text_ref.val, pos, brace_start);
      if (JS_IsException (prefix)) { FMT_CLEANUP(); return JS_EXCEPTION; }
      result = (JSText *)chase (res_ref.val);
      result = pretext_concat_value (ctx, result, prefix);
      if (!result) { FMT_CLEANUP(); return JS_EXCEPTION; }
      res_ref.val = JS_MKPTR (result);
    }

    /* Find closing '}' */
    text_val = text_ref.val;
    int brace_end = -1;
    for (int i = brace_start + 1; i < len; i++) {
      if (js_string_value_get (text_val, i) == '}') {
        brace_end = i;
        break;
      }
    }

    if (brace_end < 0) {
      /* No closing brace, copy '{' and continue */
      JSValue ch = js_sub_string_val (ctx, text_ref.val, brace_start, brace_start + 1);
      if (JS_IsException (ch)) { FMT_CLEANUP(); return JS_EXCEPTION; }
      result = (JSText *)chase (res_ref.val);
      result = pretext_concat_value (ctx, result, ch);
      if (!result) { FMT_CLEANUP(); return JS_EXCEPTION; }
      res_ref.val = JS_MKPTR (result);
      pos = brace_start + 1;
      continue;
    }

    /* Extract content between braces */
    JSValue middle = js_sub_string_val (ctx, text_ref.val, brace_start + 1, brace_end);
    if (JS_IsException (middle)) { FMT_CLEANUP(); return JS_EXCEPTION; }

    /* Split on ':' to get name and format_spec */
    int middle_len = js_string_value_len (middle);

    int colon_pos = -1;
    for (int i = 0; i < middle_len; i++) {
      if (js_string_value_get (middle, i) == ':') {
        colon_pos = i;
        break;
      }
    }

    JSValue name_val, format_spec;
    if (colon_pos >= 0) {
      name_val = js_sub_string_val (ctx, middle, 0, colon_pos);
      format_spec = js_sub_string_val (ctx, middle, colon_pos + 1, middle_len);
    } else {
      name_val = middle;
      format_spec = JS_KEY_empty;
    }

    /* Get value from collection — protect with GCRef since JS_Call below can trigger GC */
    JSGCRef cv_ref;
    JS_PushGCRef (ctx, &cv_ref);
    cv_ref.val = JS_NULL;
    if (is_array) {
      int name_len = js_string_value_len (name_val);
      int32_t idx = 0;
      int valid = (name_len > 0);
      for (int ni = 0; ni < name_len && valid; ni++) {
        uint32_t ch = js_string_value_get (name_val, ni);
        if (ch >= '0' && ch <= '9')
          idx = idx * 10 + (ch - '0');
        else
          valid = 0;
      }
      if (valid && idx >= 0) {
        cv_ref.val = JS_GetPropertyNumber (ctx, coll_ref.val, (uint32_t)idx);
      }
    } else {
      cv_ref.val = JS_GetProperty (ctx, coll_ref.val, name_val);
    }

    /* Try to get substitution */
    JSValue substitution = JS_NULL;
    int made_substitution = 0;

    if (!JS_IsNull (xform_ref.val)) {
      if (JS_IsFunction (xform_ref.val)) {
        JSValue args[2] = { cv_ref.val, format_spec };
        JSValue result_val = JS_Call (ctx, xform_ref.val, JS_NULL, 2, args);
        if (JS_IsText (result_val)) {
          substitution = result_val;
          made_substitution = 1;
        }
      } else if (JS_IsRecord (xform_ref.val)) {
        JSValue func = JS_GetProperty (ctx, xform_ref.val, format_spec);
        if (JS_IsFunction (func)) {
          JSValue result_val = JS_Call (ctx, func, JS_NULL, 1, &cv_ref.val);
          if (JS_IsText (result_val)) {
            substitution = result_val;
            made_substitution = 1;
          }
        }
      }
    }

    if (!made_substitution && JS_IsNumber (cv_ref.val) && !JS_IsNull (format_spec)) {
      JSValue text_method = JS_GetPropertyStr (ctx, cv_ref.val, "text");
      if (JS_IsFunction (text_method)) {
        JSValue result_val = JS_Call (ctx, text_method, cv_ref.val, 1, &format_spec);
        if (JS_IsText (result_val)) {
          substitution = result_val;
          made_substitution = 1;
        }
      }
    }

    if (!made_substitution && !JS_IsNull (cv_ref.val)) {
      JSValue conv_text_val = JS_ToString (ctx, cv_ref.val);
      if (JS_IsText (conv_text_val)) {
        substitution = conv_text_val;
        made_substitution = 1;
      }
    }
    JS_PopGCRef (ctx, &cv_ref);

    result = (JSText *)chase (res_ref.val);
    if (made_substitution) {
      result = pretext_concat_value (ctx, result, substitution);
      if (!result) { FMT_CLEANUP(); return JS_EXCEPTION; }
      res_ref.val = JS_MKPTR (result);
    } else {
      /* No substitution — treat the '{' as a literal character and rescan
         from brace_start + 1 so that real placeholders like {0} inside
         the skipped range are still found. */
      JSValue ch = js_sub_string_val (ctx, text_ref.val, brace_start, brace_start + 1);
      if (JS_IsException (ch)) { FMT_CLEANUP(); return JS_EXCEPTION; }
      result = (JSText *)chase (res_ref.val);
      result = pretext_concat_value (ctx, result, ch);
      if (!result) { FMT_CLEANUP(); return JS_EXCEPTION; }
      res_ref.val = JS_MKPTR (result);
      pos = brace_start + 1;
      continue;
    }

    pos = brace_end + 1;
  }

  result = (JSText *)chase (res_ref.val);
  FMT_CLEANUP();
#undef FMT_CLEANUP
  return pretext_end (ctx, result);
}

/* print(args...) - print arguments to stdout */
static JSValue js_print (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  for (int i = 0; i < argc; i++) {
    const char *str = JS_ToCString (ctx, argv[i]);
    if (str) {
      fputs (str, stdout);
      JS_FreeCString (ctx, str);
    }
    if (i < argc - 1) fputc (' ', stdout);
  }
  fputc ('\n', stdout);
  return JS_NULL;
}

static JSValue js_stacktrace (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  (void)this_val; (void)argc; (void)argv;
  JSValue stack = JS_GetStack(ctx);
  int64_t n = 0;
  JS_GetLength(ctx, stack, &n);
  for (int i = 0; i < (int)n; i++) {
    JSValue fr = JS_GetPropertyNumber(ctx, stack, i);
    JSValue fn_val = JS_GetPropertyStr(ctx, fr, "fn");
    JSValue file_val = JS_GetPropertyStr(ctx, fr, "file");
    const char *fn = JS_ToCString(ctx, fn_val);
    const char *file = JS_ToCString(ctx, file_val);
    int32_t line = 0, col = 0;
    JS_ToInt32(ctx, &line, JS_GetPropertyStr(ctx, fr, "line"));
    JS_ToInt32(ctx, &col, JS_GetPropertyStr(ctx, fr, "col"));
    printf("    at %s (%s:%d:%d)\n", fn ? fn : "<anonymous>", file ? file : "<unknown>", line, col);
    if (fn) JS_FreeCString(ctx, fn);
    if (file) JS_FreeCString(ctx, file);
  }
  return JS_NULL;
}

static JSValue js_caller_info (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  (void)this_val;
  int depth = 0;
  if (argc > 0) JS_ToInt32(ctx, &depth, argv[0]);

  JSValue stack = JS_GetStack(ctx);

  int64_t n = 0;
  JS_GetLength(ctx, stack, &n);

  /* depth 0 = immediate caller of caller_info, which is frame index 1
     (frame 0 is caller_info itself) */
  int idx = depth + 1;
  if (idx >= (int)n) idx = (int)n - 1;
  if (idx < 0) idx = 0;

  if (n > 0) return JS_GetPropertyNumber(ctx, stack, idx);
  return JS_NULL;
}

/* ----------------------------------------------------------------------------
 * array function and sub-functions
 * ----------------------------------------------------------------------------
 */

/* array(arg, arg2, arg3, arg4) - main array function */
static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  (void)this_val;
  if (argc < 1) return JS_NULL;

  JSValue arg = argv[0];

  /* array(number) - create array of size */
  /* array(number, initial_value) - create array with initial values */
  if (JS_IsNumber (arg)) {
    if (!JS_IsInteger (arg))
      return JS_ThrowTypeError (ctx, "Array expected an integer.");

    int len = JS_VALUE_GET_INT (arg);
    if (len < 0) return JS_NULL;

    JSGCRef result_ref;
    JSValue result = JS_NewArrayLen (ctx, len);
    if (JS_IsException (result)) return result;

    if (argc > 1 && JS_IsFunction (argv[1])) {
      /* Fill with function results - GC-safe */
      JSGCRef func_ref;
      JS_PushGCRef (ctx, &func_ref);
      func_ref.val = argv[1];
      int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (func_ref.val))->length;

      if (arity >= 1) {
        for (int i = 0; i < len; i++) {
          JSValue idx_arg = JS_NewInt32 (ctx, i);
          JS_PUSH_VALUE (ctx, result);
          JSValue val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 1, &idx_arg, 0);
          JS_POP_VALUE (ctx, result);
          if (JS_IsException (val)) { JS_PopGCRef (ctx, &func_ref); return JS_EXCEPTION; }
          JSArray *out = JS_VALUE_GET_ARRAY (result);  /* re-chase after call */
          out->values[i] = val;
        }
      } else {
        for (int i = 0; i < len; i++) {
          JS_PUSH_VALUE (ctx, result);
          JSValue val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 0, NULL, 0);
          JS_POP_VALUE (ctx, result);
          if (JS_IsException (val)) { JS_PopGCRef (ctx, &func_ref); return JS_EXCEPTION; }
          JSArray *out = JS_VALUE_GET_ARRAY (result);  /* re-chase after call */
          out->values[i] = val;
        }
      }
      JS_PopGCRef (ctx, &func_ref);
    } else if (argc > 1) {
      /* Fill with value */
      JSArray *out = JS_VALUE_GET_ARRAY (result);
      for (int i = 0; i < len; i++)
        out->values[i] = argv[1];
    }
    return result;
  }

  /* array(array) - copy */
  /* array(array, function) - map */
  /* array(array, another_array) - concat */
  /* array(array, from, to) - slice */
  if (JS_IsArray (arg)) {
    /* Root input array and arg1 for GC safety in this section */
    JSGCRef arg0_ref, arg1_ref;
    JS_PushGCRef (ctx, &arg0_ref);
    JS_PushGCRef (ctx, &arg1_ref);
    arg0_ref.val = argv[0];
    arg1_ref.val = argc > 1 ? argv[1] : JS_NULL;

    JSArray *arr = JS_VALUE_GET_ARRAY (arg0_ref.val);
    int len = arr->len;

    if (argc < 2 || JS_IsNull (argv[1])) {
      /* Copy */
      JSValue result = JS_NewArrayLen (ctx, len);
      if (JS_IsException (result)) {
        JS_PopGCRef (ctx, &arg1_ref);
        JS_PopGCRef (ctx, &arg0_ref);
        return result;
      }
      arr = JS_VALUE_GET_ARRAY (arg0_ref.val);  /* re-chase after allocation */
      JSArray *out = JS_VALUE_GET_ARRAY (result);
      for (int i = 0; i < len; i++) {
        out->values[i] = arr->values[i];
      }
      out->len = len;
      JS_PopGCRef (ctx, &arg1_ref);
      JS_PopGCRef (ctx, &arg0_ref);
      return result;
    }

    if (JS_IsFunction (arg1_ref.val)) {
      /* Map - GC-safe: root result throughout, use rooted refs for func and array */
      int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (arg1_ref.val))->length;

      int reverse = argc > 2 && JS_ToBool (ctx, argv[2]);
      JSValue exit_val = argc > 3 ? argv[3] : JS_NULL;

      JSGCRef result_ref;
      JS_PushGCRef (ctx, &result_ref);
      result_ref.val = JS_NewArrayLen (ctx, len);
      if (JS_IsException (result_ref.val)) {
        JS_PopGCRef (ctx, &result_ref);
        JS_PopGCRef (ctx, &arg1_ref);
        JS_PopGCRef (ctx, &arg0_ref);
        return result_ref.val;
      }

      int out_idx = 0;
#define MAP_STORE(val) do { \
  JSArray *out = JS_VALUE_GET_ARRAY (result_ref.val); \
  out->values[out_idx++] = (val); \
} while(0)
#define MAP_ERR() do { JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return JS_EXCEPTION; } while(0)

      if (arity >= 2) {
        if (reverse) {
          for (int i = len - 1; i >= 0; i--) {
            arr = JS_VALUE_GET_ARRAY (arg0_ref.val);
            if (i >= (int)arr->len) continue;
            JSValue args[2] = { arr->values[i], JS_NewInt32 (ctx, i) };
            JSValue val = JS_CallInternal (ctx, arg1_ref.val, JS_NULL, 2, args, 0);
            if (JS_IsException (val)) { MAP_ERR (); }
            if (!JS_IsNull (exit_val) && js_strict_eq (ctx, val, exit_val)) break;
            MAP_STORE (val);
          }
        } else {
          for (int i = 0; i < len; i++) {
            arr = JS_VALUE_GET_ARRAY (arg0_ref.val);
            if (i >= (int)arr->len) break;
            JSValue args[2] = { arr->values[i], JS_NewInt32 (ctx, i) };
            JSValue val = JS_CallInternal (ctx, arg1_ref.val, JS_NULL, 2, args, 0);
            if (JS_IsException (val)) { MAP_ERR (); }
            if (!JS_IsNull (exit_val) && js_strict_eq (ctx, val, exit_val)) break;
            MAP_STORE (val);
          }
        }
      } else {
        if (reverse) {
          for (int i = len - 1; i >= 0; i--) {
            arr = JS_VALUE_GET_ARRAY (arg0_ref.val);
            if (i >= (int)arr->len) continue;
            JSValue item = arr->values[i];
            JSValue val = JS_CallInternal (ctx, arg1_ref.val, JS_NULL, 1, &item, 0);
            if (JS_IsException (val)) { MAP_ERR (); }
            if (!JS_IsNull (exit_val) && js_strict_eq (ctx, val, exit_val)) break;
            MAP_STORE (val);
          }
        } else {
          for (int i = 0; i < len; i++) {
            arr = JS_VALUE_GET_ARRAY (arg0_ref.val);
            if (i >= (int)arr->len) break;
            JSValue item = arr->values[i];
            JSValue val = JS_CallInternal (ctx, arg1_ref.val, JS_NULL, 1, &item, 0);
            if (JS_IsException (val)) { MAP_ERR (); }
            if (!JS_IsNull (exit_val) && js_strict_eq (ctx, val, exit_val)) break;
            MAP_STORE (val);
          }
        }
      }
#undef MAP_STORE
#undef MAP_ERR
      /* Truncate if early exit produced fewer elements */
      JSArray *out = JS_VALUE_GET_ARRAY (result_ref.val);
      out->len = out_idx;
      JSValue result = result_ref.val;
      JS_PopGCRef (ctx, &result_ref);
      JS_PopGCRef (ctx, &arg1_ref);
      JS_PopGCRef (ctx, &arg0_ref);
      return result;
    }

    if (JS_IsArray (arg1_ref.val)) {
      /* Concat */
      JSArray *arr2 = JS_VALUE_GET_ARRAY (arg1_ref.val);
      int len2 = arr2->len;

      JSValue result = JS_NewArrayLen (ctx, len + len2);
      if (JS_IsException (result)) {
        JS_PopGCRef (ctx, &arg1_ref);
        JS_PopGCRef (ctx, &arg0_ref);
        return result;
      }
      /* Re-chase arrays after allocation */
      arr = JS_VALUE_GET_ARRAY (arg0_ref.val);
      arr2 = JS_VALUE_GET_ARRAY (arg1_ref.val);
      JSArray *out = JS_VALUE_GET_ARRAY (result);

      for (int i = 0; i < len; i++) {
        out->values[i] = arr->values[i];
      }
      for (int i = 0; i < len2; i++) {
        out->values[len + i] = arr2->values[i];
      }
      out->len = len + len2;
      JS_PopGCRef (ctx, &arg1_ref);
      JS_PopGCRef (ctx, &arg0_ref);
      return result;
    }

    if (JS_IsNumber (argv[1])) {
      /* Slice */
      if (!JS_IsInteger (argv[1])) {
        JS_PopGCRef (ctx, &arg1_ref);
        JS_PopGCRef (ctx, &arg0_ref);
        return JS_NULL;
      }
      int from = JS_VALUE_GET_INT (argv[1]);
      int to;
      if (argc > 2 && !JS_IsNull (argv[2])) {
        if (!JS_IsNumber (argv[2]) || !JS_IsInteger (argv[2])) {
          JS_PopGCRef (ctx, &arg1_ref);
          JS_PopGCRef (ctx, &arg0_ref);
          return JS_NULL;
        }
        to = JS_VALUE_GET_INT (argv[2]);
      } else {
        to = len;
      }

      if (from < 0) from += len;
      if (to < 0) to += len;
      if (from < 0 || from > len || to < 0 || to > len || from > to) {
        JS_PopGCRef (ctx, &arg1_ref);
        JS_PopGCRef (ctx, &arg0_ref);
        return JS_NULL;
      }

      int slice_len = to - from;
      JSValue result = JS_NewArrayLen (ctx, slice_len);
      if (JS_IsException (result)) {
        JS_PopGCRef (ctx, &arg1_ref);
        JS_PopGCRef (ctx, &arg0_ref);
        return result;
      }
      /* Re-chase arrays after allocation */
      arr = JS_VALUE_GET_ARRAY (arg0_ref.val);
      JSArray *out = JS_VALUE_GET_ARRAY (result);

      for (int i = 0; i < slice_len; i++) {
        out->values[i] = arr->values[from + i];
      }
      out->len = slice_len;
      JS_PopGCRef (ctx, &arg1_ref);
      JS_PopGCRef (ctx, &arg0_ref);
      return result;
    }

    JS_PopGCRef (ctx, &arg1_ref);
    JS_PopGCRef (ctx, &arg0_ref);
    return JS_NULL;
  }

  /* array(object) - keys */
  if (JS_IsRecord (arg)) {
    /* Return object keys */
    return JS_GetOwnPropertyNames (ctx, arg);
  }

  /* array(text) - split into characters */
  /* array(text, separator) - split by separator */
  /* array(text, length) - dice into chunks */
  if (JS_VALUE_IS_TEXT (arg)) {
    int len = js_string_value_len (arg);

    if (argc < 2 || JS_IsNull (argv[1])) {
      /* Split into characters */
      JSGCRef arr_ref, str_ref;
      JS_PushGCRef (ctx, &arr_ref);
      JS_PushGCRef (ctx, &str_ref);
      str_ref.val = arg;
      arr_ref.val = JS_NewArrayLen (ctx, len);
      if (JS_IsException (arr_ref.val)) {
        JS_PopGCRef (ctx, &str_ref);
        JS_PopGCRef (ctx, &arr_ref);
        return JS_EXCEPTION;
      }
      for (int i = 0; i < len; i++) {
        JSValue ch = js_sub_string_val (ctx, str_ref.val, i, i + 1);
        if (JS_IsException (ch)) {
          JS_PopGCRef (ctx, &str_ref);
          JS_PopGCRef (ctx, &arr_ref);
          return JS_EXCEPTION;
        }
        JS_SetPropertyNumber (ctx, arr_ref.val, i, ch);
      }
      JSValue result = arr_ref.val;
      JS_PopGCRef (ctx, &str_ref);
      JS_PopGCRef (ctx, &arr_ref);
      return result;
    }

    if (JS_VALUE_IS_TEXT (argv[1])) {
      /* Split by separator */
      const char *cstr = JS_ToCString (ctx, arg);
      const char *sep = JS_ToCString (ctx, argv[1]);
      if (!cstr || !sep) {
        if (cstr) JS_FreeCString (ctx, cstr);
        if (sep) JS_FreeCString (ctx, sep);
        return JS_EXCEPTION;
      }

      size_t sep_len = strlen (sep);

      /* Count the number of parts first */
      int64_t count = 0;
      if (sep_len == 0) {
        count = len;
      } else {
        const char *pos = cstr;
        const char *found;
        count = 1;
        while ((found = strstr (pos, sep)) != NULL) {
          count++;
          pos = found + sep_len;
        }
      }

      /* GC-protect result and arg across allocating calls */
      JSGCRef result_ref, arg_ref;
      JS_PushGCRef (ctx, &result_ref);
      JS_PushGCRef (ctx, &arg_ref);
      arg_ref.val = arg;
      result_ref.val = JS_NewArrayLen (ctx, count);
      if (JS_IsException (result_ref.val)) {
        JS_PopGCRef (ctx, &arg_ref);
        JS_PopGCRef (ctx, &result_ref);
        JS_FreeCString (ctx, cstr);
        JS_FreeCString (ctx, sep);
        return result_ref.val;
      }

      int64_t idx = 0;
      const char *pos = cstr;
      const char *found;

      if (sep_len == 0) {
        for (int i = 0; i < len; i++) {
          JSValue ch = js_sub_string_val (ctx, arg_ref.val, i, i + 1);
          JS_SetPropertyNumber (ctx, result_ref.val, idx++, ch);
        }
      } else {
        while ((found = strstr (pos, sep)) != NULL) {
          JSValue part = JS_NewStringLen (ctx, pos, found - pos);
          JS_SetPropertyNumber (ctx, result_ref.val, idx++, part);
          pos = found + sep_len;
        }
        JSValue part = JS_NewString (ctx, pos);
        JS_SetPropertyNumber (ctx, result_ref.val, idx++, part);
      }

      JS_PopGCRef (ctx, &arg_ref);
      JS_PopGCRef (ctx, &result_ref);
      JS_FreeCString (ctx, cstr);
      JS_FreeCString (ctx, sep);
      return result_ref.val;
    }

    if (mist_is_gc_object (argv[1]) && JS_IsRegExp (ctx, argv[1])) {
      /* Split by regex (manual "global" iteration; ignore g flag semantics) */
      /* Root rx, result, arg across allocating calls */
      JSGCRef rx_ref, res_ref, arg_ref;
      JS_PushGCRef (ctx, &rx_ref);
      rx_ref.val = argv[1];
      JS_PushGCRef (ctx, &res_ref);
      res_ref.val = JS_NULL;
      JS_PushGCRef (ctx, &arg_ref);
      arg_ref.val = arg;

#define RXS_CLEANUP() do { JS_PopGCRef (ctx, &arg_ref); JS_PopGCRef (ctx, &res_ref); JS_PopGCRef (ctx, &rx_ref); } while(0)

      JSValue result = JS_NewArray (ctx);
      if (JS_IsException (result)) { RXS_CLEANUP (); return result; }
      res_ref.val = result;

      /* Save & restore lastIndex to avoid mutating caller-visible state */
      JSValue orig_last_index = JS_GetPropertyStr (ctx, rx_ref.val, "lastIndex");
      if (JS_IsException (orig_last_index)) {
        RXS_CLEANUP ();
        return JS_EXCEPTION;
      }

      int pos = 0;
      int64_t out_idx = 0;

      while (pos <= len) {
        if (JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", JS_NewInt32 (ctx, 0)) < 0)
          goto fail_rx_split;

        JSValue sub_str = js_sub_string_val (ctx, arg_ref.val, pos, len);
        if (JS_IsException (sub_str)) goto fail_rx_split;

        JSValue exec_res
          = JS_Invoke (ctx, rx_ref.val, JS_KEY_exec, 1, (JSValue *)&sub_str);
        if (JS_IsException (exec_res)) goto fail_rx_split;

        if (JS_IsNull (exec_res)) {
          JSValue tail = js_sub_string_val (ctx, arg_ref.val, pos, len);
          if (JS_IsException (tail)) goto fail_rx_split;
          result = res_ref.val;
          if (JS_ArrayPush (ctx, &result, tail) < 0) { res_ref.val = result; goto fail_rx_split; }
          res_ref.val = result;
          break;
        }

        JSValue idx_val = JS_GetPropertyStr (ctx, exec_res, "index");
        if (JS_IsException (idx_val)) goto fail_rx_split;

        int32_t local_index = 0;
        if (JS_ToInt32 (ctx, &local_index, idx_val)) goto fail_rx_split;
        if (local_index < 0) local_index = 0;

        int found = pos + local_index;
        if (found < pos) found = pos;
        if (found > len) {
          JSValue tail = js_sub_string_val (ctx, arg_ref.val, pos, len);
          if (JS_IsException (tail)) goto fail_rx_split;
          result = res_ref.val;
          JS_SetPropertyNumber (ctx, result, out_idx++, tail);
          break;
        }

        JSValue end_val = JS_GetPropertyStr (ctx, exec_res, "end");
        if (JS_IsException (end_val)) goto fail_rx_split;

        int32_t end = 0;
        if (JS_ToInt32 (ctx, &end, end_val)) goto fail_rx_split;

        int match_len = end - local_index;
        if (match_len < 0) match_len = 0;

        JSValue part = js_sub_string_val (ctx, arg_ref.val, pos, found);
        if (JS_IsException (part)) goto fail_rx_split;
        result = res_ref.val;
        if (JS_ArrayPush (ctx, &result, part) < 0) { res_ref.val = result; goto fail_rx_split; }
        res_ref.val = result;

        pos = found + match_len;
        if (match_len == 0) {
          if (found >= len) {
            JSValue empty = JS_NewStringLen (ctx, "", 0);
            if (JS_IsException (empty)) goto fail_rx_split;
            result = res_ref.val;
            if (JS_ArrayPush (ctx, &result, empty) < 0) { res_ref.val = result; goto fail_rx_split; }
            res_ref.val = result;
            break;
          }
          pos = found + 1;
        }
      }

      JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index);
      result = res_ref.val;
      RXS_CLEANUP ();
      return result;

    fail_rx_split:
      if (!JS_IsException (orig_last_index)) {
        JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index);
      }
      RXS_CLEANUP ();
      return JS_EXCEPTION;
    }
#undef RXS_CLEANUP

    if (JS_VALUE_IS_NUMBER (argv[1])) {
      /* Dice into chunks */
      int chunk_len;
      if (JS_ToInt32 (ctx, &chunk_len, argv[1]))
        return JS_NULL;
      if (chunk_len <= 0)
        return JS_NULL;

      int64_t count = (len + chunk_len - 1) / chunk_len;
      /* GC-protect result and arg across allocating calls */
      JSGCRef result_ref, arg_ref;
      JS_PushGCRef (ctx, &result_ref);
      JS_PushGCRef (ctx, &arg_ref);
      arg_ref.val = arg;
      result_ref.val = JS_NewArrayLen (ctx, count);
      if (JS_IsException (result_ref.val)) {
        JS_PopGCRef (ctx, &arg_ref);
        JS_PopGCRef (ctx, &result_ref);
        return result_ref.val;
      }

      int64_t idx = 0;
      for (int i = 0; i < len; i += chunk_len) {
        int end = i + chunk_len;
        if (end > len) end = len;
        JSValue chunk = js_sub_string_val (ctx, arg_ref.val, i, end);
        if (JS_IsException (chunk)) {
          JS_PopGCRef (ctx, &arg_ref);
          JS_PopGCRef (ctx, &result_ref);
          return JS_EXCEPTION;
        }
        JS_SetPropertyNumber (ctx, result_ref.val, idx++, chunk);
      }

      JS_PopGCRef (ctx, &arg_ref);
      JS_PopGCRef (ctx, &result_ref);
      return result_ref.val;
    }

    return JS_NULL;
  }

  return JS_NULL;
}

/* array.reduce(arr, fn, initial, reverse) */
static JSValue js_cell_array_reduce (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;
  if (!JS_IsArray (argv[0])) return JS_NULL;
  if (!JS_IsFunction (argv[1])) return JS_NULL;

  JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]);
  word_t len = arr->len;

  int reverse = argc > 3 && JS_ToBool (ctx, argv[3]);
  JSGCRef acc_ref;
  JSValue acc;

  if (argc < 3 || JS_IsNull (argv[2])) {
    if (len == 0) return JS_NULL;
    if (len == 1) return arr->values[0];

    if (reverse) {
      acc = arr->values[len - 1];
      for (word_t i = len - 1; i > 0; i--) {
        arr = JS_VALUE_GET_ARRAY (argv[0]);
        if (i - 1 >= arr->len) continue;
        JSValue args[2] = { acc, arr->values[i - 1] };
        JS_PUSH_VALUE (ctx, acc);
        JSValue new_acc = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0);
        JS_POP_VALUE (ctx, acc);
        if (JS_IsException (new_acc)) return JS_EXCEPTION;
        acc = new_acc;
      }
    } else {
      acc = arr->values[0];
      for (word_t i = 1; i < len; i++) {
        arr = JS_VALUE_GET_ARRAY (argv[0]);
        if (i >= arr->len) break;
        JSValue args[2] = { acc, arr->values[i] };
        JS_PUSH_VALUE (ctx, acc);
        JSValue new_acc = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0);
        JS_POP_VALUE (ctx, acc);
        if (JS_IsException (new_acc)) return JS_EXCEPTION;
        acc = new_acc;
      }
    }
  } else {
    if (len == 0) return argv[2];
    acc = argv[2];

    if (reverse) {
      for (word_t i = len; i > 0; i--) {
        arr = JS_VALUE_GET_ARRAY (argv[0]);
        if (i - 1 >= arr->len) continue;
        JSValue args[2] = { acc, arr->values[i - 1] };
        JS_PUSH_VALUE (ctx, acc);
        JSValue new_acc = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0);
        JS_POP_VALUE (ctx, acc);
        if (JS_IsException (new_acc)) return JS_EXCEPTION;
        acc = new_acc;
      }
    } else {
      for (word_t i = 0; i < len; i++) {
        arr = JS_VALUE_GET_ARRAY (argv[0]);
        if (i >= arr->len) break;
        JSValue args[2] = { acc, arr->values[i] };
        JS_PUSH_VALUE (ctx, acc);
        JSValue new_acc = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0);
        JS_POP_VALUE (ctx, acc);
        if (JS_IsException (new_acc)) return JS_EXCEPTION;
        acc = new_acc;
      }
    }
  }

  return acc;
}

/* array.for(arr, fn, reverse, exit) - GC-safe */
static JSValue js_cell_array_for (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;
  if (!JS_IsArray (argv[0])) return JS_NULL;
  if (!JS_IsFunction (argv[1])) return JS_NULL;

  JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]);

  word_t len = arr->len;
  if (len == 0) return JS_NULL;

  int reverse = argc > 2 && JS_ToBool (ctx, argv[2]);
  JSValue exit_val = argc > 3 ? argv[3] : JS_NULL;

  /* Determine function arity */
  int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (argv[1]))->length;

  if (reverse) {
    for (word_t i = len; i > 0; i--) {
      arr = JS_VALUE_GET_ARRAY (argv[0]);
      if (i - 1 >= arr->len) continue;
      JSValue result;
      if (arity == 1) {
        JSValue item = arr->values[i - 1];
        result = JS_CallInternal (ctx, argv[1], JS_NULL, 1, &item, 0);
      } else {
        JSValue args[2];
        args[0] = arr->values[i - 1];
        args[1] = JS_NewInt32 (ctx, (int32_t)(i - 1));
        result = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0);
      }
      if (JS_IsException (result)) return JS_EXCEPTION;
      if (!JS_IsNull (exit_val) && js_strict_eq (ctx, result, exit_val))
        return result;
    }
  } else {
    for (word_t i = 0; i < len; i++) {
      arr = JS_VALUE_GET_ARRAY (argv[0]);
      if (i >= arr->len) break;
      JSValue result;
      if (arity == 1) {
        JSValue item = arr->values[i];
        result = JS_CallInternal (ctx, argv[1], JS_NULL, 1, &item, 0);
      } else {
        JSValue args[2];
        args[0] = arr->values[i];
        args[1] = JS_NewInt32 (ctx, (int32_t)i);
        result = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0);
      }
      if (JS_IsException (result)) return JS_EXCEPTION;
      if (!JS_IsNull (exit_val) && js_strict_eq (ctx, result, exit_val))
        return result;
    }
  }

  return JS_NULL;
}

/* array.find(arr, fn_or_value, reverse, from) */
static JSValue js_cell_array_find (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;
  if (!JS_IsArray (argv[0])) return JS_NULL;

  JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]);
  word_t len = arr->len;

  int reverse = argc > 2 && JS_ToBool (ctx, argv[2]);
  int32_t from;
  if (argc > 3 && !JS_IsNull (argv[3])) {
    if (JS_ToInt32 (ctx, &from, argv[3])) return JS_NULL;
  } else {
    from = reverse ? (int32_t)(len - 1) : 0;
  }

  if (!JS_IsFunction (argv[1])) {
    JSValue target = argv[1];
    if (reverse) {
      for (int32_t i = from; i >= 0; i--) {
        arr = JS_VALUE_GET_ARRAY (argv[0]);
        if ((word_t)i >= arr->len) continue;
        if (js_strict_eq (ctx, arr->values[i], target))
          return JS_NewInt32 (ctx, i);
      }
    } else {
      for (word_t i = (word_t)from; i < len; i++) {
        arr = JS_VALUE_GET_ARRAY (argv[0]);
        if (i >= arr->len) break;
        if (js_strict_eq (ctx, arr->values[i], target))
          return JS_NewInt32 (ctx, (int32_t)i);
      }
    }
    return JS_NULL;
  }

  /* Use function predicate — re-chase after each call */
  int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (argv[1]))->length;

  if (arity == 2) {
    if (reverse) {
      for (int32_t i = from; i >= 0; i--) {
        arr = JS_VALUE_GET_ARRAY (argv[0]);
        if ((word_t)i >= arr->len) continue;
        JSValue args[2] = { arr->values[i], JS_NewInt32 (ctx, i) };
        JSValue result = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0);
        if (JS_IsException (result)) return JS_EXCEPTION;
        if (JS_ToBool (ctx, result)) return JS_NewInt32 (ctx, i);
      }
    } else {
      for (word_t i = (word_t)from; i < len; i++) {
        arr = JS_VALUE_GET_ARRAY (argv[0]);
        if (i >= arr->len) break;
        JSValue args[2] = { arr->values[i], JS_NewInt32 (ctx, (int32_t)i) };
        JSValue result = JS_CallInternal (ctx, argv[1], JS_NULL, 2, args, 0);
        if (JS_IsException (result)) return JS_EXCEPTION;
        if (JS_ToBool (ctx, result)) return JS_NewInt32 (ctx, (int32_t)i);
      }
    }
  } else {
    if (reverse) {
      for (int32_t i = from; i >= 0; i--) {
        arr = JS_VALUE_GET_ARRAY (argv[0]);
        if ((word_t)i >= arr->len) continue;
        JSValue item = arr->values[i];
        JSValue result = JS_CallInternal (ctx, argv[1], JS_NULL, 1, &item, 0);
        if (JS_IsException (result)) return JS_EXCEPTION;
        if (JS_ToBool (ctx, result)) return JS_NewInt32 (ctx, i);
      }
    } else {
      for (word_t i = (word_t)from; i < len; i++) {
        arr = JS_VALUE_GET_ARRAY (argv[0]);
        if (i >= arr->len) break;
        JSValue item = arr->values[i];
        JSValue result = JS_CallInternal (ctx, argv[1], JS_NULL, 1, &item, 0);
        if (JS_IsException (result)) return JS_EXCEPTION;
        if (JS_ToBool (ctx, result)) return JS_NewInt32 (ctx, (int32_t)i);
      }
    }
  }

  return JS_NULL;
}

/* array.filter(arr, fn) - GC-safe */
static JSValue js_cell_array_filter (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;
  if (!JS_IsArray (argv[0])) return JS_NULL;
  if (!JS_IsFunction (argv[1])) return JS_NULL;

  /* Protect input array and function throughout the loop */
  JSGCRef input_ref, func_ref, result_ref;
  JS_PushGCRef (ctx, &input_ref);
  JS_PushGCRef (ctx, &func_ref);
  JS_PushGCRef (ctx, &result_ref);
  input_ref.val = argv[0];
  func_ref.val = argv[1];

  JSArray *arr = JS_VALUE_GET_ARRAY (input_ref.val);
  word_t len = arr->len;

  result_ref.val = JS_NewArray (ctx);
  if (JS_IsException (result_ref.val)) {
    JS_PopGCRef (ctx, &result_ref);
    JS_PopGCRef (ctx, &func_ref);
    JS_PopGCRef (ctx, &input_ref);
    return JS_EXCEPTION;
  }

  int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (func_ref.val))->length;

  for (word_t i = 0; i < len; i++) {
    /* Re-chase input array each iteration (it may have moved) */
    arr = JS_VALUE_GET_ARRAY (input_ref.val);
    if (i >= arr->len) break;
    JSValue item = arr->values[i];

    JSValue val;
    if (arity >= 2) {
      JSValue args[2] = { item, JS_NewInt32 (ctx, (int32_t)i) };
      val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 2, args, 0);
    } else if (arity == 1) {
      val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 1, &item, 0);
    } else {
      val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 0, NULL, 0);
    }

    if (JS_IsException (val)) {
      JS_PopGCRef (ctx, &result_ref);
      JS_PopGCRef (ctx, &func_ref);
      JS_PopGCRef (ctx, &input_ref);
      return JS_EXCEPTION;
    }

    if (JS_VALUE_GET_TAG (val) == JS_TAG_BOOL) {
      if (JS_VALUE_GET_BOOL (val)) {
        /* Re-read item after the call (GC may have moved the input array) */
        arr = JS_VALUE_GET_ARRAY (input_ref.val);
        if (i < arr->len) {
          item = arr->values[i];
          if (js_intrinsic_array_push (ctx, &result_ref.val, item) < 0) {
            JS_PopGCRef (ctx, &result_ref);
            JS_PopGCRef (ctx, &func_ref);
            JS_PopGCRef (ctx, &input_ref);
            return JS_EXCEPTION;
          }
        }
      }
    } else {
      JS_PopGCRef (ctx, &result_ref);
      JS_PopGCRef (ctx, &func_ref);
      JS_PopGCRef (ctx, &input_ref);
      return JS_NULL;
    }
  }

  JSValue result = result_ref.val;
  JS_PopGCRef (ctx, &result_ref);
  JS_PopGCRef (ctx, &func_ref);
  JS_PopGCRef (ctx, &input_ref);
  return result;
}

/* array.sort(arr, select) - GC-safe */
static JSValue js_cell_array_sort (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  (void)this_val;
  if (argc < 1) return JS_NULL;
  if (!JS_IsArray (argv[0])) return JS_NULL;

  /* GC-safe: root argv[0] */
  JSGCRef arr_ref;
  JS_PushGCRef (ctx, &arr_ref);
  arr_ref.val = argv[0];

  JSArray *arr = JS_VALUE_GET_ARRAY (arr_ref.val);
  word_t len = arr->len;

  JSValue result = JS_NewArrayLen (ctx, len);
  if (JS_IsException (result)) { JS_PopGCRef (ctx, &arr_ref); return result; }

  if (len == 0) { JS_PopGCRef (ctx, &arr_ref); return result; }

  /* Root result across allocating calls */
  JSGCRef result_ref;
  JS_PushGCRef (ctx, &result_ref);
  result_ref.val = result;

  /* Re-chase arr after allocation */
  arr = JS_VALUE_GET_ARRAY (arr_ref.val);
  len = arr->len;

  /* Use alloca for temporary working arrays - they won't move during GC */
  JSValue *items = alloca (sizeof (JSValue) * len);
  double *keys = alloca (sizeof (double) * len);
  char **str_keys = NULL;
  int is_string = 0;

  /* Extract items and keys - re-chase arrays as needed */
  for (word_t i = 0; i < len; i++) {
    /* Re-chase input and key arrays each iteration */
    arr = JS_VALUE_GET_ARRAY (arr_ref.val);
    if (i >= arr->len) break;
    items[i] = arr->values[i];

    JSValue key;
    if (argc < 2 || JS_IsNull (argv[1])) {
      key = items[i];
    } else if (JS_VALUE_GET_TAG (argv[1]) == JS_TAG_INT) {
      /* Numeric index - use for nested arrays */
      int32_t idx;
      JS_ToInt32 (ctx, &idx, argv[1]);
      if (JS_IsArray (items[i])) {
        JSArray *nested = JS_VALUE_GET_ARRAY (items[i]);
        if (idx >= 0 && (word_t)idx < nested->len)
          key = nested->values[idx];
        else
          key = JS_NULL;
      } else {
        key = JS_GetPropertyNumber (ctx, items[i], idx);
        /* Re-read items[i] after potential GC */
        arr = JS_VALUE_GET_ARRAY (arr_ref.val);
        items[i] = arr->values[i];
      }
    } else if (JS_VALUE_IS_TEXT (argv[1])) {
      JSValue prop_key = js_key_from_string (ctx, argv[1]);
      /* Re-read items[i] (js_key_from_string no longer allocates, but re-read is harmless) */
      arr = JS_VALUE_GET_ARRAY (arr_ref.val);
      items[i] = arr->values[i];
      key = JS_GetProperty (ctx, items[i], prop_key);
    } else if (argc >= 2 && JS_IsArray (argv[1])) {
      /* Re-chase key array */
      JSArray *key_arr = JS_VALUE_GET_ARRAY (argv[1]);
      if (i < key_arr->len)
        key = key_arr->values[i];
      else
        key = JS_NULL;
    } else {
      key = items[i];
    }

    if (JS_IsException (key)) {
      if (str_keys) {
        for (word_t j = 0; j < i; j++)
          JS_FreeCString (ctx, str_keys[j]);
        js_free (ctx, str_keys);
      }
      JS_PopGCRef (ctx, &result_ref);
      JS_PopGCRef (ctx, &arr_ref);
      return JS_EXCEPTION;
    }

    int key_tag = JS_VALUE_GET_TAG (key);
    if (key_tag == JS_TAG_INT || key_tag == JS_TAG_FLOAT64 || key_tag == JS_TAG_SHORT_FLOAT) {
      JS_ToFloat64 (ctx, &keys[i], key);
      if (i == 0) is_string = 0;
    } else if (JS_VALUE_IS_TEXT (key)) {
      if (i == 0) {
        is_string = 1;
        str_keys = alloca (sizeof (char *) * len);
      }
      if (is_string) { str_keys[i] = (char *)JS_ToCString (ctx, key); }
    } else {
      if (str_keys) {
        for (word_t j = 0; j < i; j++)
          JS_FreeCString (ctx, str_keys[j]);
      }
      JS_PopGCRef (ctx, &result_ref);
      JS_PopGCRef (ctx, &arr_ref);
      return JS_NULL;
    }
  }

  /* Re-read all items from GC-safe source after key extraction (GC may have moved them) */
  arr = JS_VALUE_GET_ARRAY (arr_ref.val);
  for (word_t i = 0; i < len && i < arr->len; i++)
    items[i] = arr->values[i];

  /* Create index array using alloca */
  int *indices = alloca (sizeof (int) * len);
  for (word_t i = 0; i < len; i++)
    indices[i] = (int)i;

  /* Simple insertion sort (stable) */
  for (word_t i = 1; i < len; i++) {
    int temp = indices[i];
    int j = (int)i - 1;
    while (j >= 0) {
      int cmp;
      if (is_string) {
        cmp = strcmp (str_keys[indices[j]], str_keys[temp]);
      } else {
        double a = keys[indices[j]], b = keys[temp];
        cmp = (a > b) - (a < b);
      }
      if (cmp <= 0) break;
      indices[j + 1] = indices[j];
      j--;
    }
    indices[j + 1] = temp;
  }

  /* Build sorted array directly into output - re-chase result */
  JSArray *out = JS_VALUE_GET_ARRAY (result_ref.val);
  for (word_t i = 0; i < len; i++) {
    out->values[i] = items[indices[i]];
  }
  out->len = len;

  /* Cleanup string keys only (alloca frees automatically) */
  if (str_keys) {
    for (word_t i = 0; i < len; i++)
      JS_FreeCString (ctx, str_keys[i]);
  }

  result = result_ref.val;
  JS_PopGCRef (ctx, &result_ref);
  JS_PopGCRef (ctx, &arr_ref);
  return result;
}

/* ----------------------------------------------------------------------------
 * object function and sub-functions
 * ----------------------------------------------------------------------------
 */

/* object(arg, arg2) - main object function */
static JSValue js_cell_object (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  JSValue arg = argv[0];

  /* object(object) - shallow mutable copy */
  if (mist_is_gc_object (arg) && !JS_IsArray (arg) && !JS_IsFunction (arg)) {
    if (argc < 2 || JS_IsNull (argv[1])) {
      /* Shallow copy - root arg, result, keys across allocating calls */
      JSGCRef arg_ref, res_ref, keys_ref;
      JS_PushGCRef (ctx, &arg_ref);
      arg_ref.val = arg;
      JS_PushGCRef (ctx, &res_ref);
      res_ref.val = JS_NewObject (ctx);
      JS_PushGCRef (ctx, &keys_ref);
      keys_ref.val = JS_NULL;

#define OBJ_COPY_CLEANUP() do { JS_PopGCRef (ctx, &keys_ref); JS_PopGCRef (ctx, &res_ref); JS_PopGCRef (ctx, &arg_ref); } while(0)

      if (JS_IsException (res_ref.val)) { OBJ_COPY_CLEANUP (); return JS_EXCEPTION; }

      keys_ref.val = JS_GetOwnPropertyNames (ctx, arg_ref.val);
      if (JS_IsException (keys_ref.val)) {
        OBJ_COPY_CLEANUP ();
        return JS_EXCEPTION;
      }
      uint32_t len;
      if (js_get_length32 (ctx, &len, keys_ref.val)) {
        OBJ_COPY_CLEANUP ();
        return JS_EXCEPTION;
      }

      for (uint32_t i = 0; i < len; i++) {
        JSValue key = JS_GetPropertyNumber (ctx, keys_ref.val, i);
        JSValue val = JS_GetProperty (ctx, arg_ref.val, key);
        if (JS_IsException (val)) {
          OBJ_COPY_CLEANUP ();
          return JS_EXCEPTION;
        }
        JS_SetProperty (ctx, res_ref.val, key, val);
      }
      JSValue result = res_ref.val;
      OBJ_COPY_CLEANUP ();
      return result;
    }
#undef OBJ_COPY_CLEANUP

    /* object(object, another_object) - combine */
    if (mist_is_gc_object (argv[1]) && !JS_IsArray (argv[1])) {
      JSGCRef arg_ref, arg2_ref, res_ref, keys_ref;
      JS_PushGCRef (ctx, &arg_ref);
      arg_ref.val = arg;
      JS_PushGCRef (ctx, &arg2_ref);
      arg2_ref.val = argv[1];
      JS_PushGCRef (ctx, &res_ref);
      res_ref.val = JS_NewObject (ctx);
      JS_PushGCRef (ctx, &keys_ref);
      keys_ref.val = JS_NULL;

#define OBJ_COMBINE_CLEANUP() do { JS_PopGCRef (ctx, &keys_ref); JS_PopGCRef (ctx, &res_ref); JS_PopGCRef (ctx, &arg2_ref); JS_PopGCRef (ctx, &arg_ref); } while(0)

      if (JS_IsException (res_ref.val)) { OBJ_COMBINE_CLEANUP (); return JS_EXCEPTION; }

      /* Copy from first object */
      keys_ref.val = JS_GetOwnPropertyNames (ctx, arg_ref.val);
      if (JS_IsException (keys_ref.val)) {
        OBJ_COMBINE_CLEANUP ();
        return JS_EXCEPTION;
      }
      uint32_t len;
      if (js_get_length32 (ctx, &len, keys_ref.val)) {
        OBJ_COMBINE_CLEANUP ();
        return JS_EXCEPTION;
      }
      for (uint32_t i = 0; i < len; i++) {
        JSValue key = JS_GetPropertyNumber (ctx, keys_ref.val, i);
        JSValue val = JS_GetProperty (ctx, arg_ref.val, key);
        if (JS_IsException (val)) {
          OBJ_COMBINE_CLEANUP ();
          return JS_EXCEPTION;
        }
        JS_SetProperty (ctx, res_ref.val, key, val);
      }

      /* Copy from second object */
      keys_ref.val = JS_GetOwnPropertyNames (ctx, arg2_ref.val);
      if (JS_IsException (keys_ref.val)) {
        OBJ_COMBINE_CLEANUP ();
        return JS_EXCEPTION;
      }
      if (js_get_length32 (ctx, &len, keys_ref.val)) {
        OBJ_COMBINE_CLEANUP ();
        return JS_EXCEPTION;
      }
      for (uint32_t i = 0; i < len; i++) {
        JSValue key = JS_GetPropertyNumber (ctx, keys_ref.val, i);
        JSValue val = JS_GetProperty (ctx, arg2_ref.val, key);
        if (JS_IsException (val)) {
          OBJ_COMBINE_CLEANUP ();
          return JS_EXCEPTION;
        }
        JS_SetProperty (ctx, res_ref.val, key, val);
      }
      JSValue result = res_ref.val;
      OBJ_COMBINE_CLEANUP ();
      return result;
    }
#undef OBJ_COMBINE_CLEANUP

    /* object(object, array_of_keys) - select */
    if (JS_IsArray (argv[1])) {
      JSGCRef arg_ref, res_ref, karr_ref;
      JS_PushGCRef (ctx, &arg_ref);
      arg_ref.val = arg;
      JS_PushGCRef (ctx, &res_ref);
      res_ref.val = JS_NewObject (ctx);
      JS_PushGCRef (ctx, &karr_ref);
      karr_ref.val = argv[1];

#define OBJ_SEL_CLEANUP() do { JS_PopGCRef (ctx, &karr_ref); JS_PopGCRef (ctx, &res_ref); JS_PopGCRef (ctx, &arg_ref); } while(0)

      if (JS_IsException (res_ref.val)) { OBJ_SEL_CLEANUP (); return JS_EXCEPTION; }

      JSArray *keys = JS_VALUE_GET_ARRAY (karr_ref.val);
      int len = keys->len;

      for (int i = 0; i < len; i++) {
        keys = JS_VALUE_GET_ARRAY (karr_ref.val);  /* re-chase each iteration */
        if (i >= (int)keys->len) break;
        JSValue key = keys->values[i];
        if (JS_IsText (key)) {
          JSValue prop_key = js_key_from_string (ctx, key);
          int has = JS_HasProperty (ctx, arg_ref.val, prop_key);
          if (has > 0) {
            JSValue val = JS_GetProperty (ctx, arg_ref.val, prop_key);
            if (!JS_IsException (val)) {
              JS_SetProperty (ctx, res_ref.val, prop_key, val);
            }
          }
        }
      }
      JSValue result = res_ref.val;
      OBJ_SEL_CLEANUP ();
      return result;
    }
#undef OBJ_SEL_CLEANUP
  }

  /* object(array_of_keys) - set with true values */
  /* object(array_of_keys, value) - value set */
  /* object(array_of_keys, function) - functional value set */
  if (JS_IsArray (arg)) {
    JSArray *keys = JS_VALUE_GET_ARRAY (arg);
    int len = keys->len;

    int is_func = argc >= 2 && JS_IsFunction (argv[1]);

    /* Root keys array and func/value BEFORE JS_NewObject which may trigger GC.
       argv[] is on the C stack and is NOT a GC root, so after any allocation
       that triggers GC, argv[] values become dangling pointers. */
    JSGCRef keys_ref, func_ref, result_ref;
    JS_PushGCRef (ctx, &keys_ref);
    keys_ref.val = arg;  /* use already-read arg, not argv[0] */
    JS_PushGCRef (ctx, &func_ref);
    func_ref.val = argc >= 2 ? argv[1] : JS_NULL;
    JS_PushGCRef (ctx, &result_ref);
    result_ref.val = JS_NULL;

    JSValue result = JS_NewObject (ctx);
    if (JS_IsException (result)) {
      JS_PopGCRef (ctx, &result_ref);
      JS_PopGCRef (ctx, &func_ref);
      JS_PopGCRef (ctx, &keys_ref);
      return result;
    }
    result_ref.val = result;

    for (int i = 0; i < len; i++) {
      keys = JS_VALUE_GET_ARRAY (keys_ref.val);
      if (i >= (int)keys->len) break;
      JSValue key = keys->values[i];
      if (JS_IsText (key)) {
        /* Use text directly as key */
        JSValue prop_key = js_key_from_string (ctx, key);
        JSValue val;
        if (argc < 2 || JS_IsNull (func_ref.val)) {
          val = JS_TRUE;
        } else if (is_func) {
          JSValue arg_key = key;
          val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 1, &arg_key, 0);
          if (JS_IsException (val)) {
            JS_PopGCRef (ctx, &result_ref);
            JS_PopGCRef (ctx, &func_ref);
            JS_PopGCRef (ctx, &keys_ref);
            return JS_EXCEPTION;
          }
        } else {
          val = func_ref.val;
        }
        JS_SetProperty (ctx, result_ref.val, prop_key, val);
        /* prop_key is interned, no need to free */
      }
    }
    result = JS_PopGCRef (ctx, &result_ref);
    JS_PopGCRef (ctx, &func_ref);
    JS_PopGCRef (ctx, &keys_ref);
    return result;
  }

  return JS_NULL;
}

/* ----------------------------------------------------------------------------
 * fn function and sub-functions
 * ----------------------------------------------------------------------------
 */

/* fn.apply(func, args) - arity is enforced in JS_CallInternal */
static JSValue js_cell_fn_apply (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;
  if (!JS_IsFunction (argv[0])) return argv[0];

  if (argc < 2)
    return JS_CallInternal (ctx, argv[0], JS_NULL, 0, NULL, 0);

  if (!JS_IsArray (argv[1]))
    return JS_CallInternal (ctx, argv[0], JS_NULL, 1, &argv[1], 0);

  JSArray *arr = JS_VALUE_GET_ARRAY (argv[1]);
  int len = arr->len;

  if (len == 0)
    return JS_CallInternal (ctx, argv[0], JS_NULL, 0, NULL, 0);

  JSValue *args = js_malloc (ctx, sizeof (JSValue) * len);
  if (!args) return JS_EXCEPTION;
  arr = JS_VALUE_GET_ARRAY (argv[1]);  /* re-chase after malloc */

  for (int i = 0; i < len; i++) {
    args[i] = arr->values[i];
  }

  JSValue result = JS_CallInternal (ctx, argv[0], JS_NULL, len, args, 0);

  js_free (ctx, args);
  return result;
}

/* ============================================================================
 * Blob Intrinsic Type
 * ============================================================================
 */

/* Get JSBlob* from a JSValue, chasing forwards. Returns NULL if not a blob. */
static JSBlob *checked_get_blob (JSContext *ctx, JSValue val) {
  if (!mist_is_blob (val)) return NULL;
  return (JSBlob *)chase (val);
}

/* Allocate a new JSBlob on the GC heap with given capacity in bits. */
static JSValue js_new_heap_blob (JSContext *ctx, size_t capacity_bits) {
  size_t word_count = (capacity_bits + 63) / 64;
  size_t total = sizeof (JSBlob) + word_count * sizeof (word_t);
  JSBlob *bd = js_mallocz (ctx, total);
  if (!bd) return JS_EXCEPTION;
  bd->mist_hdr = objhdr_make (capacity_bits, OBJ_BLOB, false, false, false, false);
  bd->length = 0;
  return JS_MKPTR (bd);
}

/* Grow a blob using the forward-pointer pattern.
   *pblob is updated to point to the new, larger blob. */
static int blob_grow (JSContext *ctx, JSValue *pblob, size_t need_bits) {
  JSGCRef blob_ref;
  JS_PushGCRef (ctx, &blob_ref);
  blob_ref.val = *pblob;

  /* Growth: double until enough, minimum 64 bits */
  JSBlob *old = (JSBlob *)chase (blob_ref.val);
  size_t old_cap = objhdr_cap56 (old->mist_hdr);
  size_t new_cap = old_cap == 0 ? 64 : old_cap * 2;
  while (new_cap < need_bits) new_cap *= 2;

  /* Allocate new blob — may trigger GC */
  size_t word_count = (new_cap + 63) / 64;
  size_t total = sizeof (JSBlob) + word_count * sizeof (word_t);
  JSBlob *nb = js_mallocz (ctx, total);
  if (!nb) {
    JS_PopGCRef (ctx, &blob_ref);
    return -1;
  }

  /* Re-derive old after potential GC */
  old = (JSBlob *)chase (blob_ref.val);

  /* Copy header, length, and bit data */
  nb->mist_hdr = objhdr_make (new_cap, OBJ_BLOB, false, false, false, false);
  nb->length = old->length;
  size_t old_words = (old_cap + 63) / 64;
  if (old_words > 0)
    memcpy (nb->bits, old->bits, old_words * sizeof (word_t));

  /* Install forward pointer at old location */
  old->mist_hdr = objhdr_make_fwd (nb);

  /* Update caller's JSValue */
  *pblob = JS_MKPTR (nb);
  JS_PopGCRef (ctx, &blob_ref);
  return 0;
}

/* Ensure blob has capacity for total_need bits. May grow and update *pblob. */
static int blob_ensure_cap (JSContext *ctx, JSValue *pblob, size_t total_need) {
  JSBlob *bd = (JSBlob *)chase (*pblob);
  size_t cap = objhdr_cap56 (bd->mist_hdr);
  if (total_need <= cap) return 0;
  return blob_grow (ctx, pblob, total_need);
}

/* blob() constructor */
static JSValue js_blob_constructor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  /* blob() - empty blob */
  if (argc == 0) {
    return js_new_heap_blob (ctx, 0);
  }
  /* blob(capacity) - blob with initial capacity in bits */
  if (argc == 1 && JS_IsNumber (argv[0])) {
    int64_t capacity_bits;
    if (JS_ToInt64 (ctx, &capacity_bits, argv[0]) < 0) return JS_EXCEPTION;
    if (capacity_bits < 0) capacity_bits = 0;
    return js_new_heap_blob (ctx, (size_t)capacity_bits);
  }
  /* blob(length, logical/random) - blob with fill or random */
  if (argc == 2 && JS_IsNumber (argv[0])) {
    int64_t length_bits;
    if (JS_ToInt64 (ctx, &length_bits, argv[0]) < 0) return JS_EXCEPTION;
    if (length_bits < 0) length_bits = 0;

    if (JS_IsBool (argv[1])) {
      int is_one = JS_ToBool (ctx, argv[1]);
      JSValue bv = js_new_heap_blob (ctx, (size_t)length_bits);
      if (JS_IsException (bv)) return bv;
      JSBlob *bd = (JSBlob *)chase (bv);
      bd->length = length_bits;
      if (is_one && length_bits > 0) {
        size_t bytes = (length_bits + 7) / 8;
        memset (bd->bits, 0xFF, bytes);
        size_t trail = length_bits & 7;
        if (trail) ((uint8_t *)bd->bits)[bytes - 1] &= (1 << trail) - 1;
      }
      return bv;
    }
    if (JS_IsFunction (argv[1])) {
      JSGCRef bv_ref;
      JS_PushGCRef (ctx, &bv_ref);
      bv_ref.val = js_new_heap_blob (ctx, (size_t)length_bits);
      if (JS_IsException (bv_ref.val)) {
        JS_PopGCRef (ctx, &bv_ref);
        return JS_EXCEPTION;
      }
      JSBlob *bd = (JSBlob *)chase (bv_ref.val);
      bd->length = length_bits;

      size_t bits_written = 0;
      while (bits_written < (size_t)length_bits) {
        JSValue randval = JS_Call (ctx, argv[1], JS_NULL, 0, NULL);
        if (JS_IsException (randval)) {
          JS_PopGCRef (ctx, &bv_ref);
          return JS_EXCEPTION;
        }

        int64_t fitval;
        JS_ToInt64 (ctx, &fitval, randval);

        size_t bits_to_use = length_bits - bits_written;
        if (bits_to_use > 52) bits_to_use = 52;

        bd = (JSBlob *)chase (bv_ref.val);  /* re-derive after JS_Call */
        uint8_t *data = (uint8_t *)bd->bits;
        for (size_t j = 0; j < bits_to_use; j++) {
          size_t bit_pos = bits_written + j;
          size_t byte_idx = bit_pos / 8;
          size_t bit_idx = bit_pos % 8;
          if (fitval & (1LL << j))
            data[byte_idx] |= (uint8_t)(1 << bit_idx);
          else
            data[byte_idx] &= (uint8_t)~(1 << bit_idx);
        }
        bits_written += bits_to_use;
      }
      JSValue ret = bv_ref.val;
      JS_PopGCRef (ctx, &bv_ref);
      return ret;
    }
    return JS_ThrowTypeError (ctx, "Second argument must be boolean or random function");
  }
  /* blob(blob, from, to) - copy from another blob */
  if (argc >= 1 && mist_is_blob (argv[0])) {
    JSBlob *src = checked_get_blob (ctx, argv[0]);
    if (!src)
      return JS_ThrowTypeError (ctx, "blob constructor: argument 1 not a blob");
    int64_t from = 0, to = (int64_t)src->length;
    if (argc >= 2 && JS_IsNumber (argv[1])) {
      JS_ToInt64 (ctx, &from, argv[1]);
      if (from < 0) from = 0;
    }
    if (argc >= 3 && JS_IsNumber (argv[2])) {
      JS_ToInt64 (ctx, &to, argv[2]);
      if (to < from) to = from;
      if (to > (int64_t)src->length) to = (int64_t)src->length;
    }
    size_t copy_bits = (size_t)(to - from);
    JSGCRef src_ref;
    JS_PushGCRef (ctx, &src_ref);
    src_ref.val = argv[0];
    JSValue bv = js_new_heap_blob (ctx, copy_bits);
    if (JS_IsException (bv)) {
      JS_PopGCRef (ctx, &src_ref);
      return JS_EXCEPTION;
    }
    src = (JSBlob *)chase (src_ref.val);  /* re-derive after alloc */
    JSBlob *dst = (JSBlob *)chase (bv);
    if (copy_bits > 0)
      copy_bits_fast (src->bits, dst->bits, (size_t)from, (size_t)to - 1, 0);
    dst->length = copy_bits;
    JS_PopGCRef (ctx, &src_ref);
    return bv;
  }
  /* blob(text) - create blob from UTF-8 string */
  if (argc == 1 && JS_IsText (argv[0])) {
    const char *str = JS_ToCString (ctx, argv[0]);
    if (!str) return JS_EXCEPTION;
    size_t len = strlen (str);
    JSValue bv = js_new_heap_blob (ctx, len * 8);
    if (JS_IsException (bv)) {
      JS_FreeCString (ctx, str);
      return bv;
    }
    JSBlob *bd = (JSBlob *)chase (bv);
    if (len > 0)
      memcpy (bd->bits, str, len);
    bd->length = len * 8;
    JS_FreeCString (ctx, str);
    return bv;
  }
  return JS_ThrowTypeError (ctx, "blob constructor: invalid arguments");
}

/* blob.write_bit(logical) */
static JSValue js_blob_write_bit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "write_bit(logical) requires 1 argument");
  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "write_bit: not called on a blob");
  if (objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "write_bit: cannot write (maybe stone or OOM)");

  int bit_val;
  if (JS_IsNumber (argv[0])) {
    int32_t num;
    JS_ToInt32 (ctx, &num, argv[0]);
    if (num != 0 && num != 1)
      return JS_ThrowTypeError (ctx, "write_bit: value must be true, false, 0, or 1");
    bit_val = num;
  } else {
    bit_val = JS_ToBool (ctx, argv[0]);
  }

  if (blob_ensure_cap (ctx, &this_val, bd->length + 1) < 0)
    return JS_ThrowTypeError (ctx, "write_bit: cannot write (maybe stone or OOM)");
  bd = (JSBlob *)chase (this_val);
  uint8_t *data = (uint8_t *)bd->bits;
  size_t idx = bd->length;
  if (bit_val)
    data[idx >> 3] |= (1 << (idx & 7));
  else
    data[idx >> 3] &= ~(1 << (idx & 7));
  bd->length++;
  return JS_NULL;
}

/* blob.write_blob(second_blob) */
static JSValue js_blob_write_blob (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "write_blob(second_blob) requires 1 argument");
  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "write_blob: not called on a blob");
  if (objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "write_blob: cannot write to stone blob or OOM");
  JSBlob *second = checked_get_blob (ctx, argv[0]);
  if (!second)
    return JS_ThrowTypeError (ctx, "write_blob: argument must be a blob");

  size_t src_len = second->length;
  if (src_len == 0) return JS_NULL;

  /* Root both blobs across potential growth allocation */
  JSGCRef this_ref, arg_ref;
  JS_PushGCRef (ctx, &this_ref);
  JS_PushGCRef (ctx, &arg_ref);
  this_ref.val = this_val;
  arg_ref.val = argv[0];

  bd = (JSBlob *)chase (this_ref.val);
  if (blob_ensure_cap (ctx, &this_ref.val, bd->length + src_len) < 0) {
    JS_PopGCRef (ctx, &arg_ref);
    JS_PopGCRef (ctx, &this_ref);
    return JS_ThrowTypeError (ctx, "write_blob: cannot write to stone blob or OOM");
  }
  bd = (JSBlob *)chase (this_ref.val);
  second = (JSBlob *)chase (arg_ref.val);

  copy_bits_fast (second->bits, bd->bits, 0, src_len - 1, bd->length);
  bd->length += src_len;

  JS_PopGCRef (ctx, &arg_ref);
  JS_PopGCRef (ctx, &this_ref);
  return JS_NULL;
}

/* blob.write_number(number) - write dec64 */
static JSValue js_blob_write_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "write_number(number) requires 1 argument");
  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd)
    return JS_ThrowTypeError (ctx, "write_number: not called on a blob");
  if (objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "write_number: cannot write to stone blob or OOM");

  double d;
  if (JS_ToFloat64 (ctx, &d, argv[0]) < 0) return JS_EXCEPTION;

  if (blob_ensure_cap (ctx, &this_val, bd->length + 64) < 0)
    return JS_ThrowTypeError (ctx, "write_number: cannot write to stone blob or OOM");
  bd = (JSBlob *)chase (this_val);
  copy_bits_fast (&d, bd->bits, 0, 63, bd->length);
  bd->length += 64;
  return JS_NULL;
}

/* blob.write_fit(value, len) */
static JSValue js_blob_write_fit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2)
    return JS_ThrowTypeError (ctx, "write_fit(value, len) requires 2 arguments");

  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "write_fit: not called on a blob");
  if (objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "write_fit: value doesn't fit or stone blob");

  int64_t value;
  int32_t len;
  if (JS_ToInt64 (ctx, &value, argv[0]) < 0) return JS_EXCEPTION;
  if (JS_ToInt32 (ctx, &len, argv[1]) < 0) return JS_EXCEPTION;
  if (len < 1 || len > 64)
    return JS_ThrowTypeError (ctx, "write_fit: value doesn't fit or stone blob");

  /* Check if value fits in len bits with sign */
  if (len < 64) {
    int64_t max = (1LL << (len - 1)) - 1;
    int64_t min = -(1LL << (len - 1));
    if (value < min || value > max)
      return JS_ThrowTypeError (ctx, "write_fit: value doesn't fit or stone blob");
  }

  if (blob_ensure_cap (ctx, &this_val, bd->length + len) < 0)
    return JS_ThrowTypeError (ctx, "write_fit: value doesn't fit or stone blob");
  bd = (JSBlob *)chase (this_val);
  copy_bits_fast (&value, bd->bits, 0, len - 1, bd->length);
  bd->length += len;
  return JS_NULL;
}

/* blob.write_text(text) */
static JSValue js_blob_write_text (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "write_text(text) requires 1 argument");

  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "write_text: not called on a blob");
  if (objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "write_text: cannot write to stone blob or OOM");

  const char *str = JS_ToCString (ctx, argv[0]);
  if (!str) return JS_EXCEPTION;
  size_t slen = strlen (str);
  size_t need = 64 + slen * 8;

  if (blob_ensure_cap (ctx, &this_val, bd->length + need) < 0) {
    JS_FreeCString (ctx, str);
    return JS_ThrowTypeError (ctx, "write_text: cannot write to stone blob or OOM");
  }
  bd = (JSBlob *)chase (this_val);

  /* Write 64-bit length prefix */
  int64_t text_len = (int64_t)slen;
  copy_bits_fast (&text_len, bd->bits, 0, 63, bd->length);
  bd->length += 64;

  /* Write raw text bytes */
  if (slen > 0) {
    copy_bits_fast (str, bd->bits, 0, slen * 8 - 1, bd->length);
    bd->length += slen * 8;
  }

  JS_FreeCString (ctx, str);
  return JS_NULL;
}

/* blob.write_pad(block_size) */
static JSValue js_blob_write_pad (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "write_pad(block_size) requires 1 argument");

  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "write_pad: not called on a blob");
  if (objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "write_pad: cannot write");

  int32_t block_size;
  if (JS_ToInt32 (ctx, &block_size, argv[0]) < 0) return JS_EXCEPTION;
  if (block_size <= 0)
    return JS_ThrowTypeError (ctx, "write_pad: cannot write");

  /* Write a 1 bit, then zeros to align to block_size */
  size_t after_one = bd->length + 1;
  size_t rem = after_one % block_size;
  size_t pad_zeros = rem > 0 ? block_size - rem : 0;
  size_t total_pad = 1 + pad_zeros;

  if (blob_ensure_cap (ctx, &this_val, bd->length + total_pad) < 0)
    return JS_ThrowTypeError (ctx, "write_pad: cannot write");
  bd = (JSBlob *)chase (this_val);

  uint8_t *data = (uint8_t *)bd->bits;
  /* Write the 1 bit */
  size_t idx = bd->length;
  data[idx >> 3] |= (1 << (idx & 7));
  bd->length++;

  /* Zero bits are already 0 from js_mallocz, but if we grew we need to be safe */
  for (size_t i = 0; i < pad_zeros; i++) {
    idx = bd->length;
    data[idx >> 3] &= ~(1 << (idx & 7));
    bd->length++;
  }
  return JS_NULL;
}

/* blob.w16(value) - write 16-bit value */
static JSValue js_blob_w16 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "w16(value) requires 1 argument");

  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "w16: not called on a blob");
  if (objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "w16: cannot write");

  int32_t value;
  if (JS_ToInt32 (ctx, &value, argv[0]) < 0) return JS_EXCEPTION;

  int16_t short_val = (int16_t)value;
  if (blob_ensure_cap (ctx, &this_val, bd->length + 16) < 0)
    return JS_ThrowTypeError (ctx, "w16: cannot write");
  bd = (JSBlob *)chase (this_val);

  uint8_t *data = (uint8_t *)bd->bits;
  size_t bit_off = bd->length;
  if ((bit_off & 7) == 0) {
    memcpy (data + (bit_off >> 3), &short_val, sizeof (int16_t));
  } else {
    copy_bits_fast (&short_val, bd->bits, 0, 15, bit_off);
  }
  bd->length += 16;
  return JS_NULL;
}

/* blob.w32(value) - write 32-bit value */
static JSValue js_blob_w32 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "w32(value) requires 1 argument");

  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "w32: not called on a blob");
  if (objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "w32: cannot write");

  int32_t value;
  if (JS_ToInt32 (ctx, &value, argv[0]) < 0) return JS_EXCEPTION;

  if (blob_ensure_cap (ctx, &this_val, bd->length + 32) < 0)
    return JS_ThrowTypeError (ctx, "w32: cannot write");
  bd = (JSBlob *)chase (this_val);

  uint8_t *data = (uint8_t *)bd->bits;
  size_t bit_off = bd->length;
  if ((bit_off & 7) == 0) {
    memcpy (data + (bit_off >> 3), &value, sizeof (int32_t));
  } else {
    copy_bits_fast (&value, bd->bits, 0, 31, bit_off);
  }
  bd->length += 32;
  return JS_NULL;
}

/* blob.wf(value) - write float */
static JSValue js_blob_wf (JSContext *ctx, JSValue this_val, JSValue arg0) {
  if (JS_IsNull (arg0))
    return JS_ThrowTypeError (ctx, "wf(value) requires 1 argument");

  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "wf: not called on a blob");
  if (objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "wf: cannot write");

  float f;
  double d;
  if (JS_ToFloat64 (ctx, &d, arg0) < 0) return JS_EXCEPTION;
  f = (float)d;

  if (blob_ensure_cap (ctx, &this_val, bd->length + 32) < 0)
    return JS_ThrowTypeError (ctx, "wf: cannot write");
  bd = (JSBlob *)chase (this_val);

  uint8_t *data = (uint8_t *)bd->bits;
  size_t bit_off = bd->length;
  if ((bit_off & 7) == 0) {
    memcpy (data + (bit_off >> 3), &f, sizeof (f));
  } else {
    copy_bits_fast (&f, bd->bits, 0, 31, bit_off);
  }
  bd->length += 32;
  return JS_NULL;
}

/* blob.read_logical(from) */
static JSValue js_blob_read_logical (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "read_logical(from) requires 1 argument");
  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd)
    return JS_ThrowTypeError (ctx, "read_logical: not called on a blob");
  if (!objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "read_logical: blob must be stone");
  int64_t pos;
  if (JS_ToInt64 (ctx, &pos, argv[0]) < 0)
    return JS_ThrowInternalError (ctx, "must provide a positive bit");
  if (pos < 0 || (size_t)pos >= bd->length)
    return JS_ThrowRangeError (ctx, "read_logical: position must be non-negative");
  uint8_t *data = (uint8_t *)bd->bits;
  int bit_val = (data[pos >> 3] >> (pos & 7)) & 1;
  return JS_NewBool (ctx, bit_val);
}

/* blob.read_blob(from, to) */
JSValue js_blob_read_blob (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "read_blob: not called on a blob");
  if (!objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "read_blob: blob must be stone");

  int64_t from = 0;
  int64_t to = (int64_t)bd->length;

  if (argc >= 1) {
    if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
    if (from < 0) from = 0;
  }
  if (argc >= 2) {
    if (JS_ToInt64 (ctx, &to, argv[1]) < 0) return JS_EXCEPTION;
    if (to > (int64_t)bd->length) to = (int64_t)bd->length;
  }
  if (from >= to) return js_new_heap_blob (ctx, 0);

  size_t copy_bits = (size_t)(to - from);

  /* Root source blob across new allocation */
  JSGCRef src_ref;
  JS_PushGCRef (ctx, &src_ref);
  src_ref.val = this_val;

  JSValue bv = js_new_heap_blob (ctx, copy_bits);
  if (JS_IsException (bv)) {
    JS_PopGCRef (ctx, &src_ref);
    return bv;
  }
  bd = (JSBlob *)chase (src_ref.val);
  JSBlob *dst = (JSBlob *)chase (bv);
  copy_bits_fast (bd->bits, dst->bits, (size_t)from, (size_t)to - 1, 0);
  dst->length = copy_bits;
  JS_PopGCRef (ctx, &src_ref);
  return bv;
}

/* blob.read_number(from) */
static JSValue js_blob_read_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "read_number(from) requires 1 argument");
  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "read_number: not called on a blob");
  if (!objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "read_number: blob must be stone");

  double from_d;
  if (JS_ToFloat64 (ctx, &from_d, argv[0]) < 0) return JS_EXCEPTION;
  size_t from = (size_t)from_d;
  if (from_d < 0 || from + 64 > bd->length)
    return JS_ThrowRangeError (ctx, "read_number: out of range");

  double d;
  copy_bits_fast (bd->bits, &d, from, from + 63, 0);
  return JS_NewFloat64 (ctx, d);
}

/* blob.read_fit(from, len) */
static JSValue js_blob_read_fit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2)
    return JS_ThrowTypeError (ctx, "read_fit(from, len) requires 2 arguments");

  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "read_fit: not called on a blob");
  if (!objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "read_fit: blob must be stone");

  int64_t from;
  int32_t len;
  if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
  if (JS_ToInt32 (ctx, &len, argv[1]) < 0) return JS_EXCEPTION;

  if (from < 0)
    return JS_ThrowRangeError (ctx, "read_fit: position must be non-negative");
  if (len < 1 || len > 64 || from + len > (int64_t)bd->length)
    return JS_ThrowRangeError (ctx, "read_fit: out of range or invalid length");

  int64_t value = 0;
  copy_bits_fast (bd->bits, &value, (size_t)from, (size_t)(from + len - 1), 0);

  /* Sign extend if necessary */
  if (len < 64 && (value & (1LL << (len - 1)))) {
    int64_t mask = ~((1LL << len) - 1);
    value |= mask;
  }

  return JS_NewInt64 (ctx, value);
}

/* blob.read_text(from) */
JSValue js_blob_read_text (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "read_text: not called on a blob");
  if (!objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "read_text: blob must be stone");

  int64_t from = 0;
  if (argc >= 1) {
    if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
  }

  /* Need at least 64 bits for length prefix */
  if (from < 0 || (size_t)from + 64 > bd->length)
    return JS_ThrowRangeError (ctx, "read_text: out of range or invalid encoding");

  int64_t raw_len = 0;
  copy_bits_fast (bd->bits, &raw_len, (size_t)from, (size_t)from + 63, 0);
  if (raw_len < 0)
    return JS_ThrowRangeError (ctx, "read_text: out of range or invalid encoding");

  size_t slen = (size_t)raw_len;
  size_t after_len = (size_t)from + 64;
  if (after_len + slen * 8 > bd->length)
    return JS_ThrowRangeError (ctx, "read_text: out of range or invalid encoding");

  char *str = sys_malloc (slen + 1);
  if (!str) return JS_ThrowOutOfMemory (ctx);
  if (slen > 0)
    copy_bits_fast (bd->bits, str, after_len, after_len + slen * 8 - 1, 0);
  str[slen] = '\0';

  JSValue result = JS_NewString (ctx, str);
  sys_free (str);
  return result;
}

/* blob.pad?(from, block_size) */
static JSValue js_blob_pad_q (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2)
    return JS_ThrowTypeError (ctx, "pad?(from, block_size) requires 2 arguments");
  JSBlob *bd = checked_get_blob (ctx, this_val);
  if (!bd) return JS_ThrowTypeError (ctx, "pad?: not called on a blob");
  if (!objhdr_s (bd->mist_hdr))
    return JS_ThrowTypeError (ctx, "pad?: blob must be stone");

  int64_t from;
  int32_t block_size;
  if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
  if (JS_ToInt32 (ctx, &block_size, argv[1]) < 0) return JS_EXCEPTION;
  if (block_size <= 0) return JS_FALSE;

  /* Check: length is aligned to block_size, distance from..length is valid */
  if (bd->length % block_size != 0) return JS_FALSE;
  int64_t diff = (int64_t)bd->length - from;
  if (diff <= 0 || diff > block_size) return JS_FALSE;

  /* First bit must be 1, rest must be 0 */
  uint8_t *data = (uint8_t *)bd->bits;
  int bit = (data[from >> 3] >> (from & 7)) & 1;
  if (!bit) return JS_FALSE;
  for (size_t i = (size_t)from + 1; i < bd->length; i++) {
    bit = (data[i >> 3] >> (i & 7)) & 1;
    if (bit) return JS_FALSE;
  }
  return JS_TRUE;
}

static const JSCFunctionListEntry js_blob_proto_funcs[] = {
  /* Write methods */
  JS_CFUNC_DEF ("write_bit", 1, js_blob_write_bit),
  JS_CFUNC_DEF ("write_blob", 1, js_blob_write_blob),
  JS_CFUNC_DEF ("write_number", 1, js_blob_write_number),
  JS_CFUNC_DEF ("write_fit", 2, js_blob_write_fit),
  JS_CFUNC_DEF ("write_text", 1, js_blob_write_text),
  JS_CFUNC_DEF ("write_pad", 1, js_blob_write_pad),
  JS_CFUNC1_DEF ("wf", js_blob_wf),
  JS_CFUNC_DEF ("w16", 1, js_blob_w16),
  JS_CFUNC_DEF ("w32", 1, js_blob_w32),

  /* Read methods */
  JS_CFUNC_DEF ("read_logical", 1, js_blob_read_logical),
  JS_CFUNC_DEF ("read_blob", 2, js_blob_read_blob),
  JS_CFUNC_DEF ("read_number", 1, js_blob_read_number),
  JS_CFUNC_DEF ("read_fit", 2, js_blob_read_fit),
  JS_CFUNC_DEF ("read_text", 1, js_blob_read_text),
  JS_CFUNC_DEF ("pad?", 2, js_blob_pad_q),
};

/* ============================================================================
 * Blob external API functions (called from other files via cell.h)
 * ============================================================================
 */

/* Initialize blob - called during context setup (but we do it in
 * JS_AddIntrinsicBaseObjects now) */
JSValue js_core_blob_use (JSContext *js) {
  return JS_GetPropertyStr (js, js->global_obj, "blob");
}

/* Create a new blob from raw data, stone it, and return as JSValue */
JSValue js_new_blob_stoned_copy (JSContext *js, void *data, size_t bytes) {
  size_t bits = bytes * 8;
  JSValue bv = js_new_heap_blob (js, bits);
  if (JS_IsException (bv)) return bv;
  JSBlob *bd = (JSBlob *)chase (bv);
  if (bytes > 0)
    memcpy (bd->bits, data, bytes);
  bd->length = bits;
  bd->mist_hdr = objhdr_set_s (bd->mist_hdr, true);
  return bv;
}

/* Get raw data pointer from a blob (must be stone) - returns byte count.
   WARNING: returned pointer is into GC heap — do NOT hold across allocations. */
void *js_get_blob_data (JSContext *js, size_t *size, JSValue v) {
  JSBlob *bd = checked_get_blob (js, v);
  if (!bd) {
    JS_ThrowReferenceError (js, "get_blob_data: not called on a blob");
    return NULL;
  }
  *size = (bd->length + 7) / 8;

  if (!objhdr_s (bd->mist_hdr)) {
    JS_ThrowReferenceError (js, "attempted to read data from a non-stone blob");
    return NULL;
  }

  if (bd->length % 8 != 0) {
    JS_ThrowReferenceError (
      js,
      "attempted to read data from a non-byte aligned blob [length is %llu]",
      (unsigned long long)bd->length);
    return NULL;
  }

  return (void *)bd->bits;
}

/* Get raw data pointer from a blob (must be stone) - returns bit count.
   WARNING: returned pointer is into GC heap — do NOT hold across allocations. */
void *js_get_blob_data_bits (JSContext *js, size_t *bits, JSValue v) {
  JSBlob *bd = checked_get_blob (js, v);
  if (!bd) {
    JS_ThrowReferenceError (js, "get_blob_data_bits: not called on a blob");
    return NULL;
  }
  if (!objhdr_s (bd->mist_hdr)) {
    JS_ThrowReferenceError (js, "attempted to read data from a non-stone blob");
    return NULL;
  }

  if (bd->length % 8 != 0) {
    JS_ThrowReferenceError (js, "attempted to read data from a non-byte aligned blob");
    return NULL;
  }

  if (bd->length == 0) {
    JS_ThrowReferenceError (js, "attempted to read data from an empty blob");
    return NULL;
  }

  *bits = bd->length;
  return (void *)bd->bits;
}

/* Check if a value is a blob */
int js_is_blob (JSContext *js, JSValue v) {
  return mist_is_blob (v);
}

/* ============================================================================
 * eval() function - compile and execute code with environment
 * ============================================================================
 */

/* mach_load(blob, env?) - deserialize and execute binary blob */
static JSValue js_mach_load (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "mach_load requires a blob argument");

  size_t data_size;
  void *data = js_get_blob_data (ctx, &data_size, argv[0]);
  if (!data) return JS_EXCEPTION;

  MachCode *mc = JS_DeserializeMachCode ((const uint8_t *)data, data_size);
  if (!mc)
    return JS_ThrowSyntaxError (ctx, "mach_load: failed to deserialize bytecode");

  JSValue env = (argc >= 2 && mist_is_gc_object (argv[1])) ? argv[1] : JS_NULL;

  JSCodeRegister *code = JS_LoadMachCode (ctx, mc, env);
  JS_FreeMachCode (mc);
  return JS_CallRegisterVM (ctx, code, ctx->global_obj, 0, NULL, env, JS_NULL);
}

/* mach_eval_mcode(name, mcode_json, env?) - compile mcode IR and run via register VM */
static JSValue js_mach_eval_mcode (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2 || !JS_IsText (argv[0]) || !JS_IsText (argv[1]))
    return JS_ThrowTypeError (ctx, "mach_eval_mcode requires (name, mcode_json) text arguments");

  const char *name = JS_ToCString (ctx, argv[0]);
  if (!name) return JS_EXCEPTION;

  const char *json_str = JS_ToCString (ctx, argv[1]);
  if (!json_str) {
    JS_FreeCString (ctx, name);
    return JS_EXCEPTION;
  }

  cJSON *mcode = cJSON_Parse (json_str);
  JS_FreeCString (ctx, json_str);

  if (!mcode) {
    JS_FreeCString (ctx, name);
    return JS_ThrowSyntaxError (ctx, "mach_eval_mcode: failed to parse mcode JSON");
  }

  /* Set filename on the mcode root if not present */
  if (!cJSON_GetObjectItemCaseSensitive (mcode, "filename"))
    cJSON_AddStringToObject (mcode, "filename", name);

  /* Compile mcode IR → MachCode binary */
  MachCode *mc = mach_compile_mcode (mcode);
  cJSON_Delete (mcode);

  if (!mc) {
    JS_FreeCString (ctx, name);
    return JS_ThrowInternalError (ctx, "mach_eval_mcode: compilation failed");
  }

  JSValue env = (argc >= 3 && mist_is_gc_object (argv[2])) ? argv[2] : JS_NULL;

  JSCodeRegister *code = JS_LoadMachCode (ctx, mc, env);
  JS_FreeMachCode (mc);
  JSValue result = JS_CallRegisterVM (ctx, code, ctx->global_obj, 0, NULL, env, JS_NULL);
  JS_FreeCString (ctx, name);
  return result;
}

/* mach_dump_mcode(name, mcode_json, env?) - compile mcode IR and dump bytecode disassembly */
static JSValue js_mach_dump_mcode (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2 || !JS_IsText (argv[0]) || !JS_IsText (argv[1]))
    return JS_ThrowTypeError (ctx, "mach_dump_mcode requires (name, mcode_json) text arguments");

  const char *name = JS_ToCString (ctx, argv[0]);
  if (!name) return JS_EXCEPTION;

  const char *json_str = JS_ToCString (ctx, argv[1]);
  if (!json_str) {
    JS_FreeCString (ctx, name);
    return JS_EXCEPTION;
  }

  cJSON *mcode = cJSON_Parse (json_str);
  JS_FreeCString (ctx, json_str);

  if (!mcode) {
    JS_FreeCString (ctx, name);
    return JS_ThrowSyntaxError (ctx, "mach_dump_mcode: failed to parse mcode JSON");
  }

  if (!cJSON_GetObjectItemCaseSensitive (mcode, "filename"))
    cJSON_AddStringToObject (mcode, "filename", name);

  MachCode *mc = mach_compile_mcode (mcode);
  cJSON_Delete (mcode);

  if (!mc) {
    JS_FreeCString (ctx, name);
    return JS_ThrowInternalError (ctx, "mach_dump_mcode: compilation failed");
  }

  JSValue env = (argc >= 3 && mist_is_gc_object (argv[2])) ? argv[2] : JS_NULL;

  /* Serialize to binary then dump */
  size_t bin_size;
  uint8_t *bin = JS_SerializeMachCode (mc, &bin_size);
  JS_FreeMachCode (mc);
  JS_DumpMachBin (ctx, bin, bin_size, env);
  sys_free (bin);

  JS_FreeCString (ctx, name);
  return JS_NULL;
}

/* gc_stats() — return {count, bytes_copied, heap_size, ct_pages} and reset counters */
static JSValue js_gc_stats (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  JSValue obj = JS_NewObject (ctx);
  if (JS_IsException (obj)) return obj;
  JS_SetPropertyStr (ctx, obj, "count", JS_NewInt64 (ctx, (int64_t)ctx->gc_count));
  JS_SetPropertyStr (ctx, obj, "bytes_copied", JS_NewInt64 (ctx, (int64_t)ctx->gc_bytes_copied));
  JS_SetPropertyStr (ctx, obj, "heap_size", JS_NewInt64 (ctx, (int64_t)ctx->current_block_size));
  /* Count CT overflow pages */
  int ct_page_count = 0;
  for (CTPage *p = (CTPage *)ctx->ct_pages; p; p = p->next) ct_page_count++;
  JS_SetPropertyStr (ctx, obj, "ct_pages", JS_NewInt32 (ctx, ct_page_count));
  ctx->gc_count = 0;
  ctx->gc_bytes_copied = 0;
  return obj;
}

/* mach_compile_mcode_bin(name, mcode_json) - compile mcode IR to serialized binary blob */
static JSValue js_mach_compile_mcode_bin (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2 || !JS_IsText (argv[0]) || !JS_IsText (argv[1]))
    return JS_ThrowTypeError (ctx, "mach_compile_mcode_bin requires (name, mcode_json) text arguments");

  const char *name = JS_ToCString (ctx, argv[0]);
  if (!name) return JS_EXCEPTION;

  const char *json_str = JS_ToCString (ctx, argv[1]);
  if (!json_str) {
    JS_FreeCString (ctx, name);
    return JS_EXCEPTION;
  }

  cJSON *mcode = cJSON_Parse (json_str);
  JS_FreeCString (ctx, json_str);

  if (!mcode) {
    JS_FreeCString (ctx, name);
    return JS_ThrowSyntaxError (ctx, "mach_compile_mcode_bin: failed to parse mcode JSON");
  }

  if (!cJSON_GetObjectItemCaseSensitive (mcode, "filename"))
    cJSON_AddStringToObject (mcode, "filename", name);

  MachCode *mc = mach_compile_mcode (mcode);
  cJSON_Delete (mcode);
  JS_FreeCString (ctx, name);

  if (!mc)
    return JS_ThrowInternalError (ctx, "mach_compile_mcode_bin: compilation failed");

  size_t size = 0;
  uint8_t *data = JS_SerializeMachCode (mc, &size);

  JS_FreeMachCode (mc);

  if (!data)
    return JS_ThrowInternalError (ctx, "mach_compile_mcode_bin: serialization failed");

  JSValue blob = js_new_blob_stoned_copy (ctx, data, size);
  sys_free (data);
  return blob;
}

/* ============================================================================
 * stone() function - deep freeze with blob support
 * ============================================================================
 */

/* stone(object) - deep freeze an object */
static JSValue js_cell_stone (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  JSValue obj = argv[0];

  if (mist_is_blob (obj)) {
    JSBlob *bd = (JSBlob *)chase (obj);
    bd->mist_hdr = objhdr_set_s (bd->mist_hdr, true);
    return obj;
  }

  if (mist_is_gc_object (obj)) {
    JSRecord *rec = JS_VALUE_GET_RECORD (obj);
    obj_set_stone (rec);
    return obj;
  }

  if (JS_IsArray (obj)) {
    JSArray *arr = JS_VALUE_GET_ARRAY (obj);
    arr->mist_hdr = objhdr_set_s (arr->mist_hdr, true);
    return obj;
  }

  return JS_NULL;
}

/* ============================================================================
 * reverse() function - reverse an array
 * ============================================================================
 */

static JSValue js_cell_reverse (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  JSValue value = argv[0];

  /* Handle arrays */
  if (JS_IsArray (value)) {
    /* GC-safe: root argv[0] */
    JSGCRef arr_ref;
    JS_PushGCRef (ctx, &arr_ref);
    arr_ref.val = argv[0];

    JSArray *arr = JS_VALUE_GET_ARRAY (arr_ref.val);
    int len = arr->len;

    JSValue result = JS_NewArrayLen (ctx, len);
    if (JS_IsException (result)) { JS_PopGCRef (ctx, &arr_ref); return result; }
    arr = JS_VALUE_GET_ARRAY (arr_ref.val);  /* re-chase after allocation */
    JSArray *out = JS_VALUE_GET_ARRAY (result);
    for (int i = len - 1, j = 0; i >= 0; i--, j++) {
      out->values[j] = arr->values[i];
    }
    out->len = len;
    JS_PopGCRef (ctx, &arr_ref);
    return result;
  }

  /* Handle strings */
  if (JS_IsText (value)) {
    int len = js_string_value_len (value);
    if (len == 0) return JS_NewString (ctx, "");
    JSText *str = js_alloc_string (ctx, len);
    if (!str) return JS_EXCEPTION;
    for (int i = 0; i < len; i++) {
      string_put (str, i, js_string_value_get (value, len - 1 - i));
    }
    str->length = len;
    return pretext_end (ctx, str);
  }

  /* Handle blobs */
  if (mist_is_blob (value)) {
    /* Blobs need proper blob reversal support - return null for now */
    return JS_NULL;
  }

  return JS_NULL;
}

/* ============================================================================
 * proto() function - get prototype of an object
 * ============================================================================
 */

static JSValue js_cell_pop (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  JSValue obj = argv[0];
  if (!JS_IsArray (obj)) return JS_NULL;

  JSArray *arr = JS_VALUE_GET_ARRAY (obj);

  if (arr->len == 0) return JS_NULL;

  /* Transfer ownership: take value without dup, clear slot, decrement len */
  JSValue last = arr->values[arr->len - 1];
  arr->values[arr->len - 1] = JS_NULL;
  arr->len--;
  return last;
}

JSValue JS_Stone (JSContext *ctx, JSValue this_val) {
  return js_cell_stone (ctx, this_val, 1, &this_val);
}

/* GC-safe push: takes pointer to array JSValue, updates it if array grows.
   Returns 0 on success, -1 on error. */
int JS_ArrayPush (JSContext *ctx, JSValue *arr_ptr, JSValue val) {
  if (!JS_IsArray (*arr_ptr)) {
    JS_ThrowTypeError (ctx, "not an array");
    return -1;
  }
  return js_intrinsic_array_push (ctx, arr_ptr, val);
}

JSValue JS_ArrayPop (JSContext *ctx, JSValue obj) {
  if (!JS_IsArray (obj)) return JS_ThrowTypeError (ctx, "not an array");
  return js_cell_pop (ctx, JS_NULL, 1, &obj);
}

/* C API: array(arg0, arg1, arg2, arg3)
   - array(number) or array(number, fill_value_or_fn) - create array
   - array(array) - copy
   - array(array, fn, reverse, exit) - map
   - array(array, array2) - concat
   - array(array, from, to) - slice
   - array(object) - keys
   - array(text) or array(text, sep_or_len) - split */
JSValue JS_Array (JSContext *ctx, JSValue arg0, JSValue arg1, JSValue arg2, JSValue arg3) {
  JSValue argv[4] = { arg0, arg1, arg2, arg3 };
  int argc = 4;
  if (JS_IsNull (arg3)) argc = 3;
  if (JS_IsNull (arg2)) argc = 2;
  if (JS_IsNull (arg1)) argc = 1;
  return js_cell_array (ctx, JS_NULL, argc, argv);
}

/* C API: filter(arr, fn) - returns new filtered array */
JSValue JS_ArrayFilter (JSContext *ctx, JSValue arr, JSValue fn) {
  JSValue argv[2] = { arr, fn };
  return js_cell_array_filter (ctx, JS_NULL, 2, argv);
}

/* C API: sort(arr, selector) - returns new sorted array */
JSValue JS_ArraySort (JSContext *ctx, JSValue arr, JSValue selector) {
  JSValue argv[2] = { arr, selector };
  return js_cell_array_sort (ctx, JS_NULL, 2, argv);
}

/* C API: find(arr, target_or_fn, reverse, from) - returns index or null */
JSValue JS_ArrayFind (JSContext *ctx, JSValue arr, JSValue target_or_fn, JSValue reverse, JSValue from) {
  JSValue argv[4] = { arr, target_or_fn, reverse, from };
  int argc = 4;
  if (JS_IsNull (from)) argc = 3;
  if (JS_IsNull (reverse)) argc = 2;
  return js_cell_array_find (ctx, JS_NULL, argc, argv);
}

/* C API: arrfor(arr, fn, reverse, exit) - iterate array */
JSValue JS_ArrFor (JSContext *ctx, JSValue arr, JSValue fn, JSValue reverse, JSValue exit_val) {
  JSValue argv[4] = { arr, fn, reverse, exit_val };
  int argc = 4;
  if (JS_IsNull (exit_val)) argc = 3;
  if (JS_IsNull (reverse)) argc = 2;
  return js_cell_array_for (ctx, JS_NULL, argc, argv);
}

/* C API: reduce(arr, fn, initial, reverse) - reduce array */
JSValue JS_ArrayReduce (JSContext *ctx, JSValue arr, JSValue fn, JSValue initial, JSValue reverse) {
  JSValue argv[4] = { arr, fn, initial, reverse };
  int argc = 4;
  if (JS_IsNull (reverse)) argc = 3;
  if (JS_IsNull (initial)) argc = 2;
  return js_cell_array_reduce (ctx, JS_NULL, argc, argv);
}

/* ============================================================
   C API Wrappers for Cell Intrinsic Functions
   ============================================================ */

/* C API: stone(val) - make value immutable */
JSValue JS_CellStone (JSContext *ctx, JSValue val) {
  return js_cell_stone (ctx, JS_NULL, 1, &val);
}

/* C API: length(val) - get length of array/text/blob */
JSValue JS_CellLength (JSContext *ctx, JSValue val) {
  return js_cell_length (ctx, JS_NULL, 1, &val);
}

/* C API: reverse(val) - reverse array or text */
JSValue JS_CellReverse (JSContext *ctx, JSValue val) {
  return js_cell_reverse (ctx, JS_NULL, 1, &val);
}

/* C API: proto(obj) - get prototype */
JSValue JS_CellProto (JSContext *ctx, JSValue obj) {
  return js_cell_proto (ctx, JS_NULL, 1, &obj);
}

/* C API: splat(val) - convert to array */
JSValue JS_CellSplat (JSContext *ctx, JSValue val) {
  return js_cell_splat (ctx, JS_NULL, 1, &val);
}

/* C API: meme(obj, deep) - clone object */
JSValue JS_CellMeme (JSContext *ctx, JSValue obj, JSValue deep) {
  JSValue argv[2] = { obj, deep };
  int argc = JS_IsNull (deep) ? 1 : 2;
  return js_cell_meme (ctx, JS_NULL, argc, argv);
}

/* C API: apply(fn, args) - apply function to array of args */
JSValue JS_CellApply (JSContext *ctx, JSValue fn, JSValue args) {
  JSValue argv[2] = { fn, args };
  return js_cell_fn_apply (ctx, JS_NULL, 2, argv);
}

/* C API: call(fn, this, args...) - call function */
JSValue JS_CellCall (JSContext *ctx, JSValue fn, JSValue this_val, JSValue args) {
  JSValue argv[3] = { fn, this_val, args };
  int argc = JS_IsNull (args) ? 2 : 3;
  return js_cell_call (ctx, JS_NULL, argc, argv);
}

/* C API: modulo(a, b) - modulo operation */
JSValue JS_CellModulo (JSContext *ctx, JSValue a, JSValue b) {
  JSValue argv[2] = { a, b };
  return js_cell_modulo (ctx, JS_NULL, 2, argv);
}

/* C API: neg(val) - negate number */
JSValue JS_CellNeg (JSContext *ctx, JSValue val) {
  return js_cell_neg (ctx, JS_NULL, 1, &val);
}

/* C API: not(val) - logical not */
JSValue JS_CellNot (JSContext *ctx, JSValue val) {
  return js_cell_not (ctx, JS_NULL, 1, &val);
}

/* Text functions */

/* C API: text(val) - convert to text */
JSValue JS_CellText (JSContext *ctx, JSValue val) {
  return js_cell_text (ctx, JS_NULL, 1, &val);
}

/* C API: lower(text) - convert to lowercase */
JSValue JS_CellLower (JSContext *ctx, JSValue text) {
  return js_cell_text_lower (ctx, JS_NULL, 1, &text);
}

/* C API: upper(text) - convert to uppercase */
JSValue JS_CellUpper (JSContext *ctx, JSValue text) {
  return js_cell_text_upper (ctx, JS_NULL, 1, &text);
}

/* C API: trim(text, chars) - trim whitespace or specified chars */
JSValue JS_CellTrim (JSContext *ctx, JSValue text, JSValue chars) {
  JSValue argv[2] = { text, chars };
  int argc = JS_IsNull (chars) ? 1 : 2;
  return js_cell_text_trim (ctx, JS_NULL, argc, argv);
}

/* C API: codepoint(text, idx) - get codepoint at index */
JSValue JS_CellCodepoint (JSContext *ctx, JSValue text, JSValue idx) {
  JSValue argv[2] = { text, idx };
  int argc = JS_IsNull (idx) ? 1 : 2;
  return js_cell_text_codepoint (ctx, JS_NULL, argc, argv);
}

/* C API: replace(text, pattern, replacement) - replace in text */
JSValue JS_CellReplace (JSContext *ctx, JSValue text, JSValue pattern, JSValue replacement) {
  JSValue argv[3] = { text, pattern, replacement };
  return js_cell_text_replace (ctx, JS_NULL, 3, argv);
}

/* C API: search(text, pattern, from) - search in text */
JSValue JS_CellSearch (JSContext *ctx, JSValue text, JSValue pattern, JSValue from) {
  JSValue argv[3] = { text, pattern, from };
  int argc = JS_IsNull (from) ? 2 : 3;
  return js_cell_text_search (ctx, JS_NULL, argc, argv);
}

/* C API: extract(text, from, to) - extract substring
   Internally, js_cell_text_extract expects (text, pattern, from, to)
   but for simple substring extraction we don't need a pattern */
JSValue JS_CellExtract (JSContext *ctx, JSValue text, JSValue from, JSValue to) {
  if (!JS_IsText (text)) return JS_NULL;

  JSGCRef text_ref;
  JS_PushGCRef (ctx, &text_ref);
  text_ref.val = text;

  int len = js_string_value_len (text_ref.val);

  int from_idx = 0;
  int to_idx = len;

  if (!JS_IsNull (from)) {
    if (JS_ToInt32 (ctx, &from_idx, from)) {
      JS_PopGCRef (ctx, &text_ref);
      return JS_EXCEPTION;
    }
    if (from_idx < 0) from_idx += len;
    if (from_idx < 0) from_idx = 0;
    if (from_idx > len) from_idx = len;
  }

  if (!JS_IsNull (to)) {
    if (JS_ToInt32 (ctx, &to_idx, to)) {
      JS_PopGCRef (ctx, &text_ref);
      return JS_EXCEPTION;
    }
    if (to_idx < 0) to_idx += len;
    if (to_idx < 0) to_idx = 0;
    if (to_idx > len) to_idx = len;
  }

  if (from_idx > to_idx) {
    JS_PopGCRef (ctx, &text_ref);
    return JS_NULL;
  }
  if (from_idx == to_idx) {
    JS_PopGCRef (ctx, &text_ref);
    return JS_NewString (ctx, "");
  }

  /* Create result string */
  int result_len = to_idx - from_idx;
  JSText *str = js_alloc_string (ctx, result_len);
  if (!str) {
    JS_PopGCRef (ctx, &text_ref);
    return JS_EXCEPTION;
  }
  for (int i = 0; i < result_len; i++) {
    string_put (str, i, js_string_value_get (text_ref.val, from_idx + i));
  }
  str->length = result_len;
  JS_PopGCRef (ctx, &text_ref);
  return pretext_end (ctx, str);
}

/* C API: character(codepoint) - create single character text */
JSValue JS_CellCharacter (JSContext *ctx, JSValue codepoint) {
  return js_cell_character (ctx, JS_NULL, 1, &codepoint);
}

/* Number functions */

/* C API: number(val) - convert to number */
JSValue JS_CellNumber (JSContext *ctx, JSValue val) {
  return js_cell_number (ctx, JS_NULL, 1, &val);
}

/* C API: abs(num) - absolute value */
JSValue JS_CellAbs (JSContext *ctx, JSValue num) {
  return js_cell_number_abs (ctx, JS_NULL, 1, &num);
}

/* C API: sign(num) - sign of number (-1, 0, 1) */
JSValue JS_CellSign (JSContext *ctx, JSValue num) {
  return js_cell_number_sign (ctx, JS_NULL, 1, &num);
}

/* C API: floor(num) - floor */
JSValue JS_CellFloor (JSContext *ctx, JSValue num) {
  return js_cell_number_floor (ctx, JS_NULL, 1, &num);
}

/* C API: ceiling(num) - ceiling */
JSValue JS_CellCeiling (JSContext *ctx, JSValue num) {
  return js_cell_number_ceiling (ctx, JS_NULL, 1, &num);
}

/* C API: round(num) - round to nearest integer */
JSValue JS_CellRound (JSContext *ctx, JSValue num) {
  return js_cell_number_round (ctx, JS_NULL, 1, &num);
}

/* C API: trunc(num) - truncate towards zero */
JSValue JS_CellTrunc (JSContext *ctx, JSValue num) {
  return js_cell_number_trunc (ctx, JS_NULL, 1, &num);
}

/* C API: whole(num) - integer part */
JSValue JS_CellWhole (JSContext *ctx, JSValue num) {
  return js_cell_number_whole (ctx, JS_NULL, 1, &num);
}

/* C API: fraction(num) - fractional part */
JSValue JS_CellFraction (JSContext *ctx, JSValue num) {
  return js_cell_number_fraction (ctx, JS_NULL, 1, &num);
}

/* C API: min(a, b) - minimum of two numbers */
JSValue JS_CellMin (JSContext *ctx, JSValue a, JSValue b) {
  JSValue argv[2] = { a, b };
  return js_cell_number_min (ctx, JS_NULL, 2, argv);
}

/* C API: max(a, b) - maximum of two numbers */
JSValue JS_CellMax (JSContext *ctx, JSValue a, JSValue b) {
  JSValue argv[2] = { a, b };
  return js_cell_number_max (ctx, JS_NULL, 2, argv);
}

/* C API: remainder(a, b) - remainder after division */
JSValue JS_CellRemainder (JSContext *ctx, JSValue a, JSValue b) {
  JSValue argv[2] = { a, b };
  return js_cell_number_remainder (ctx, JS_NULL, 2, argv);
}

/* Object functions */

/* C API: object(proto, props) - create object */
JSValue JS_CellObject (JSContext *ctx, JSValue proto, JSValue props) {
  JSValue argv[2] = { proto, props };
  int argc = JS_IsNull (props) ? 1 : 2;
  if (JS_IsNull (proto)) argc = 0;
  return js_cell_object (ctx, JS_NULL, argc, argv);
}

/* C API: format(text, collection, transformer) - string interpolation */
JSValue JS_CellFormat (JSContext *ctx, JSValue text, JSValue collection, JSValue transformer) {
  JSValue argv[3] = { text, collection, transformer };
  int argc = JS_IsNull (transformer) ? 2 : 3;
  return js_cell_text_format (ctx, JS_NULL, argc, argv);
}

/* ============================================================
   Helper Functions for C API
   ============================================================ */

/* Create an array from a list of JSValues */
JSValue JS_NewArrayFrom (JSContext *ctx, int count, JSValue *values) {
  JSGCRef arr_ref;
  JS_PushGCRef (ctx, &arr_ref);
  arr_ref.val = JS_NewArrayLen (ctx, count);
  if (JS_IsException (arr_ref.val)) {
    JS_PopGCRef (ctx, &arr_ref);
    return JS_EXCEPTION;
  }
  for (int i = 0; i < count; i++) {
    JS_SetPropertyNumber (ctx, arr_ref.val, i, values[i]);
  }
  JSValue result = arr_ref.val;
  JS_PopGCRef (ctx, &arr_ref);
  return result;
}

/* Print a JSValue text to stdout */
void JS_PrintText (JSContext *ctx, JSValue val) {
  if (!JS_IsText (val)) {
    /* Try to convert to string first */
    val = JS_ToString (ctx, val);
    if (JS_IsException (val) || !JS_IsText (val)) {
      printf ("[non-text value]");
      return;
    }
  }
  const char *str = JS_ToCString (ctx, val);
  if (str) {
    printf ("%s", str);
    JS_FreeCString (ctx, str);
  }
}

/* Print a JSValue text to stdout with newline */
void JS_PrintTextLn (JSContext *ctx, JSValue val) {
  JS_PrintText (ctx, val);
  printf ("\n");
}

/* Format and print - convenience function */
void JS_PrintFormatted (JSContext *ctx, const char *fmt, int count, JSValue *values) {
  JSValue fmt_str = JS_NewString (ctx, fmt);
  JSValue arr = JS_NewArrayFrom (ctx, count, values);
  JSValue result = JS_CellFormat (ctx, fmt_str, arr, JS_NULL);
  JS_PrintText (ctx, result);
}

static JSValue js_cell_push (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  if (!JS_IsArray (argv[0])) return JS_NULL;

  for (int i = 1; i < argc; i++) {
    if (js_intrinsic_array_push (ctx, &argv[0], argv[i]) < 0)
      return JS_EXCEPTION;
  }

  return JS_NULL;
}

static JSValue js_cell_proto (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  (void)this_val;
  if (argc < 1) return JS_NULL;

  JSValue obj = argv[0];

  if (JS_IsArray (obj)) {
    return JS_ThrowTypeError (ctx, "arrays do not have prototypes");
  }

  if (!mist_is_gc_object (obj)) return JS_NULL;

  JSValue proto = JS_GetPrototype (ctx, obj);
  if (JS_IsException (proto)) return JS_NULL;

  /* If prototype is Object.prototype, return null */
  if (mist_is_gc_object (proto)) {
    JSValue obj_proto = ctx->class_proto[JS_CLASS_OBJECT];
    if (mist_is_gc_object (obj_proto)
        && JS_VALUE_GET_OBJ (proto) == JS_VALUE_GET_OBJ (obj_proto)) {
      return JS_NULL;
    }
  }

  return proto;
}

static JSValue js_cell_meme (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  JSValue proto = JS_NULL;
  if (argc > 0 && !JS_IsNull (argv[0])) proto = argv[0];

  JSValue result = JS_NewObjectProto (ctx, proto);
  if (JS_IsException (result)) return result;

  if (argc < 2) return result;

  /* Root result across allocating calls */
  JSGCRef result_ref;
  JS_PushGCRef (ctx, &result_ref);
  result_ref.val = result;

/* Helper function to apply a single mixin */
#define APPLY_MIXIN(mix_val)                                            \
  do {                                                                  \
    if (!mist_is_gc_object (mix_val) || JS_IsNull (mix_val) || JS_IsArray (mix_val)) \
      break;                                                            \
    JSGCRef _mix_ref;                                                   \
    JS_PushGCRef (ctx, &_mix_ref);                                      \
    _mix_ref.val = mix_val;                                             \
    JSValue _keys = JS_GetOwnPropertyNames (ctx, _mix_ref.val);         \
    if (JS_IsException (_keys)) {                                       \
      JS_PopGCRef (ctx, &_mix_ref);                                     \
      JS_PopGCRef (ctx, &result_ref);                                   \
      return JS_EXCEPTION;                                              \
    }                                                                   \
    uint32_t _len;                                                      \
    if (js_get_length32 (ctx, &_len, _keys)) {                          \
      JS_PopGCRef (ctx, &_mix_ref);                                     \
      JS_PopGCRef (ctx, &result_ref);                                   \
      return JS_EXCEPTION;                                              \
    }                                                                   \
    for (uint32_t j = 0; j < _len; j++) {                               \
      JSValue _key = JS_GetPropertyNumber (ctx, _keys, j);              \
      JSValue val = JS_GetProperty (ctx, _mix_ref.val, _key);           \
      if (JS_IsException (val)) {                                       \
        JS_PopGCRef (ctx, &_mix_ref);                                   \
        JS_PopGCRef (ctx, &result_ref);                                 \
        return JS_EXCEPTION;                                            \
      }                                                                 \
      JS_SetProperty (ctx, result_ref.val, _key, val);                  \
    }                                                                   \
    JS_PopGCRef (ctx, &_mix_ref);                                       \
  } while (0)

  /* Process all arguments starting from argv[1] as mixins */
  for (int i = 1; i < argc; i++) {
    JSValue mixins = argv[i];

    if (JS_IsArray (mixins)) {
      /* Array of mixins - root the array across calls */
      JSGCRef mixins_ref;
      JS_PushGCRef (ctx, &mixins_ref);
      mixins_ref.val = mixins;
      int64_t len;
      if (js_get_length64 (ctx, &len, mixins_ref.val)) {
        JS_PopGCRef (ctx, &mixins_ref);
        JS_PopGCRef (ctx, &result_ref);
        return JS_EXCEPTION;
      }

      for (int64_t j = 0; j < len; j++) {
        JSValue mix = JS_GetPropertyNumber (ctx, mixins_ref.val, j);
        if (JS_IsException (mix)) {
          JS_PopGCRef (ctx, &mixins_ref);
          JS_PopGCRef (ctx, &result_ref);
          return JS_EXCEPTION;
        }
        APPLY_MIXIN (mix);
      }
      JS_PopGCRef (ctx, &mixins_ref);
    } else if (mist_is_gc_object (mixins) && !JS_IsNull (mixins)) {
      /* Single mixin object */
      APPLY_MIXIN (mixins);
    }
  }

#undef APPLY_MIXIN

  result = result_ref.val;
  JS_PopGCRef (ctx, &result_ref);
  return result;
}

/* ============================================================================
 * splat() function - flatten object with prototype chain
 * ============================================================================
 */

static JSValue js_cell_splat (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  JSValue obj = argv[0];
  if (!mist_is_gc_object (obj) || JS_IsNull (obj)) return JS_NULL;

  /* Root obj, result, current, keys across allocating calls */
  JSGCRef obj_ref, res_ref, cur_ref, keys_ref;
  JS_PushGCRef (ctx, &obj_ref);
  obj_ref.val = obj;
  JS_PushGCRef (ctx, &res_ref);
  res_ref.val = JS_NewObject (ctx);
  JS_PushGCRef (ctx, &cur_ref);
  cur_ref.val = obj_ref.val; /* use rooted value, not stale local */
  JS_PushGCRef (ctx, &keys_ref);
  keys_ref.val = JS_NULL;

#define SPLAT_CLEANUP() do { JS_PopGCRef (ctx, &keys_ref); JS_PopGCRef (ctx, &cur_ref); JS_PopGCRef (ctx, &res_ref); JS_PopGCRef (ctx, &obj_ref); } while(0)

  if (JS_IsException (res_ref.val)) { SPLAT_CLEANUP (); return JS_EXCEPTION; }

  /* Walk prototype chain and collect text keys */
  while (!JS_IsNull (cur_ref.val)) {
    keys_ref.val = JS_GetOwnPropertyNames (ctx, cur_ref.val);
    if (JS_IsException (keys_ref.val)) {
      SPLAT_CLEANUP ();
      return JS_EXCEPTION;
    }
    uint32_t len;
    if (js_get_length32 (ctx, &len, keys_ref.val)) {
      SPLAT_CLEANUP ();
      return JS_EXCEPTION;
    }

    for (uint32_t i = 0; i < len; i++) {
      JSValue key = JS_GetPropertyNumber (ctx, keys_ref.val, i);
      int has = JS_HasProperty (ctx, res_ref.val, key);
      if (has < 0) {
        SPLAT_CLEANUP ();
        return JS_EXCEPTION;
      }
      if (!has) {
        JSValue val = JS_GetProperty (ctx, cur_ref.val, key);
        if (JS_IsException (val)) {
          SPLAT_CLEANUP ();
          return JS_EXCEPTION;
        }
        int tag = JS_VALUE_GET_TAG (val);
        if (mist_is_gc_object (val) || JS_IsNumber (val) || tag == JS_TAG_STRING
            || tag == JS_TAG_STRING_IMM || tag == JS_TAG_BOOL) {
          JS_SetProperty (ctx, res_ref.val, key, val);
        }
      }
    }

    cur_ref.val = JS_GetPrototype (ctx, cur_ref.val);
  }

  /* Call to_data if present */
  JSValue to_data = JS_GetPropertyStr (ctx, obj_ref.val, "to_data");
  if (JS_IsFunction (to_data)) {
    JSValue args[1] = { res_ref.val };
    JSValue extra = JS_Call (ctx, to_data, obj_ref.val, 1, args);
    if (!JS_IsException (extra) && mist_is_gc_object (extra)) {
      keys_ref.val = JS_GetOwnPropertyNames (ctx, extra);
      if (!JS_IsException (keys_ref.val)) {
        uint32_t len;
        if (!js_get_length32 (ctx, &len, keys_ref.val)) {
          for (uint32_t i = 0; i < len; i++) {
            JSValue key = JS_GetPropertyNumber (ctx, keys_ref.val, i);
            JSValue val = JS_GetProperty (ctx, extra, key);
            JS_SetProperty (ctx, res_ref.val, key, val);
          }
        }
      }
    }
  }

  JSValue result = res_ref.val;
  SPLAT_CLEANUP ();
  return result;
}
#undef SPLAT_CLEANUP

/* ============================================================================
 * length() function
 * ============================================================================
 */

static JSValue js_cell_length (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;

  JSValue val = argv[0];

  /* null returns null */
  if (JS_IsNull (val)) return JS_NULL;

  /* Functions return arity (accessed directly, not via properties) */
  if (JS_IsFunction (val)) {
    JSFunction *f = JS_VALUE_GET_FUNCTION (val);
    return JS_NewInt32 (ctx, f->length);
  }

  /* Strings return codepoint count */
  if (MIST_IsImmediateASCII (val)) {
    return JS_NewInt32 (ctx, MIST_GetImmediateASCIILen (val));
  }
  if (JS_IsPtr (val) && objhdr_type (*chase (val)) == OBJ_TEXT) {
    JSText *p = (JSText *)chase (val);
    return JS_NewInt32 (ctx, (int)JSText_len (p));
  }

  /* Check for blob */
  if (mist_is_blob (val)) {
    JSBlob *bd = (JSBlob *)chase (val);
    return JS_NewInt64 (ctx, bd->length);
  }

  /* Arrays return element count */
  if (JS_IsArray (val)) {
    JSArray *arr = JS_VALUE_GET_ARRAY (val);
    return JS_NewInt32 (ctx, arr->len);
  }

  /* Objects with length property */
  if (mist_is_gc_object (val)) {
    JSValue len = JS_GetPropertyStr (ctx, val, "length");
    if (!JS_IsException (len) && !JS_IsNull (len)) {
      if (JS_IsFunction (len)) {
        JSValue result = JS_Call (ctx, len, val, 0, NULL);
        return result;
      }
      if (JS_VALUE_IS_NUMBER (len)) return len;
    } else if (JS_IsException (len)) {
      return len;
    }
  }

  return JS_NULL;
}

/* ============================================================================
 * call() function - call a function with explicit this and arguments
 * ============================================================================
 */

/* call(func, this_val, args_array) */
static JSValue js_cell_call (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "call requires a function argument");

  if (!JS_IsFunction (argv[0]))
    return JS_ThrowTypeError (ctx, "first argument must be a function");

  JSGCRef this_ref;
  JS_PushGCRef (ctx, &this_ref);
  this_ref.val = argc >= 2 ? argv[1] : JS_NULL;

  if (argc < 3 || JS_IsNull (argv[2])) {
    JSValue ret = JS_CallInternal (ctx, argv[0], this_ref.val, 0, NULL, 0);
    JS_PopGCRef (ctx, &this_ref);
    return ret;
  }

  if (!JS_IsArray (argv[2])) {
    JS_PopGCRef (ctx, &this_ref);
    return JS_ThrowTypeError (ctx, "third argument must be an array");
  }

  uint32_t len;
  JSValue *tab = build_arg_list (ctx, &len, &argv[2]);
  if (!tab) {
    JS_PopGCRef (ctx, &this_ref);
    return JS_EXCEPTION;
  }

  JSValue ret = JS_CallInternal (ctx, argv[0], this_ref.val, len, tab, 0);
  free_arg_list (ctx, tab, len);
  JS_PopGCRef (ctx, &this_ref);
  return ret;
}

/* ============================================================================
 * is_* type checking functions
 * ============================================================================
 */

/* is_array(val) */
static JSValue js_cell_is_array (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  return JS_NewBool (ctx, JS_IsArray (argv[0]));
}

/* is_blob(val) */
static JSValue js_cell_is_blob (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  return JS_NewBool (ctx, mist_is_blob (argv[0]));
}

/* is_data(val) - check if object is a plain object (data record) */
static JSValue js_cell_is_data (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  JSValue val = argv[0];
  if (!mist_is_gc_object (val)) return JS_FALSE;
  if (JS_IsArray (val)) return JS_FALSE;
  if (JS_IsFunction (val)) return JS_FALSE;
  if (mist_is_blob (val)) return JS_FALSE;
  /* Check if it's a plain object (prototype is Object.prototype or null) */
  return JS_TRUE;
}

/* is_function(val) */
static JSValue js_cell_is_function (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  return JS_NewBool (ctx, JS_IsFunction (argv[0]));
}

/* is_logical(val) - check if value is a boolean (true or false) */
static JSValue js_cell_is_logical (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  return JS_NewBool (ctx, JS_VALUE_GET_TAG (argv[0]) == JS_TAG_BOOL);
}

/* is_integer(val) */
static JSValue js_cell_is_integer (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  JSValue val = argv[0];
  int tag = JS_VALUE_GET_TAG (val);
  if (tag == JS_TAG_INT) return JS_TRUE;
  if (tag == JS_TAG_FLOAT64) {
    double d = JS_VALUE_GET_FLOAT64 (val);
    return JS_NewBool (ctx, isfinite (d) && trunc (d) == d);
  }
  return JS_FALSE;
}

/* is_null(val) */
static JSValue js_cell_is_null (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  return JS_NewBool (ctx, JS_IsNull (argv[0]));
}

/* is_number(val) */
static JSValue js_cell_is_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  return JS_NewBool (ctx, JS_IsNumber (argv[0]));
}

/* is_object(val) - true for records */
static JSValue js_cell_is_object (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  return JS_NewBool (ctx, mist_is_record (argv[0]));
}

/* is_actor(val) - true for actor objects (have actor_sym property) */
static JSValue js_cell_is_actor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  JSValue val = argv[0];
  if (!mist_is_record (val)) return JS_FALSE;
  if (JS_IsNull (ctx->actor_sym)) return JS_FALSE;
  int has = JS_HasPropertyKey (ctx, val, ctx->actor_sym);
  return JS_NewBool (ctx, has > 0);
}

/* is_stone(val) - check if value is immutable */
static JSValue js_cell_is_stone (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  (void)this_val;
  if (argc < 1) return JS_FALSE;

  return JS_NewBool (ctx, JS_IsStone (argv[0]));
}

/* is_text(val) */
static JSValue js_cell_is_text (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  return JS_NewBool (ctx, JS_IsText (argv[0]));
}

static JSValue js_cell_is_letter (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_FALSE;
  JSValue val = argv[0];
  if (!JS_IsText (val)) return JS_FALSE;
  if (js_string_value_len (val) != 1) return JS_FALSE;
  uint32_t c = js_string_value_get (val, 0);
  return JS_NewBool (ctx, (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'));
}

/* is_proto(val, master) - check if val has master in prototype chain */
static JSValue js_cell_is_proto (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_FALSE;
  JSValue val = argv[0];
  JSValue master = argv[1];

  if (!mist_is_gc_object (val) || JS_IsNull (master)) return JS_FALSE;

  /* Walk prototype chain */
  JSValue proto = JS_GetPrototype (ctx, val);
  while (!JS_IsNull (proto) && !JS_IsException (proto)) {
    /* If master is a function with prototype property, check that */
    if (JS_IsFunction (master)) {
      JSValue master_proto = JS_GetPropertyStr (ctx, master, "prototype");
      if (!JS_IsException (master_proto) && !JS_IsNull (master_proto)) {
        JSRecord *p1 = JS_VALUE_GET_OBJ (proto);
        JSRecord *p2 = JS_VALUE_GET_OBJ (master_proto);
        if (p1 == p2) {
          return JS_TRUE;
        }
      } else if (!JS_IsException (master_proto)) {
      }
    }
    /* Also check if proto == master directly */
    if (mist_is_gc_object (master)) {
      JSRecord *p1 = JS_VALUE_GET_OBJ (proto);
      JSRecord *p2 = JS_VALUE_GET_OBJ (master);
      if (p1 == p2) {
        return JS_TRUE;
      }
    }

    JSValue next = JS_GetPrototype (ctx, proto);
    proto = next;
  }
  if (JS_IsException (proto)) return proto;
  return JS_FALSE;
}


/* logical(val) — false for 0/false/"false"/null, true for 1/true/"true", null otherwise */
static JSValue js_cell_logical(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1) return JS_NULL;
  JSValue v = argv[0];
  if (JS_IsNull(v) || (JS_IsInt(v) && JS_VALUE_GET_INT(v) == 0) ||
      (JS_IsBool(v) && !JS_VALUE_GET_BOOL(v))) return JS_FALSE;
  if ((JS_IsInt(v) && JS_VALUE_GET_INT(v) == 1) ||
      (JS_IsBool(v) && JS_VALUE_GET_BOOL(v))) return JS_TRUE;
  if (JS_IsText(v)) {
    char buf[8];
    JS_KeyGetStr(ctx, buf, sizeof(buf), v);
    if (strcmp(buf, "false") == 0) return JS_FALSE;
    if (strcmp(buf, "true") == 0) return JS_TRUE;
  }
  return JS_NULL;
}

/* starts_with(str, prefix) — search(str, prefix) == 0 */
static JSValue js_cell_starts_with(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;
  JSValue args[3] = { argv[0], argv[1], JS_NULL };
  JSValue pos = js_cell_text_search(ctx, JS_NULL, 2, args);
  if (JS_IsInt(pos) && JS_VALUE_GET_INT(pos) == 0) return JS_TRUE;
  return JS_FALSE;
}

/* ends_with(str, suffix) — search(str, suffix, -length(suffix)) != null */
static JSValue js_cell_ends_with(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;
  JSValue len_val = js_cell_length(ctx, JS_NULL, 1, &argv[1]);
  int slen = JS_IsInt(len_val) ? JS_VALUE_GET_INT(len_val) : 0;
  JSValue offset = JS_NewInt32(ctx, -slen);
  JSValue args[3] = { argv[0], argv[1], offset };
  JSValue pos = js_cell_text_search(ctx, JS_NULL, 3, args);
  if (!JS_IsNull(pos)) return JS_TRUE;
  return JS_FALSE;
}

/* every(arr, pred) — find(arr, x => !pred(x)) == null */
static JSValue js_cell_every (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;
  if (!JS_IsArray (argv[0]) || !JS_IsFunction (argv[1])) return JS_NULL;
  JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]);
  for (int i = 0; i < arr->len; i++) {
    JSValue elem = arr->values[i];
    JSValue r = JS_CallInternal (ctx, argv[1], JS_NULL, 1, &elem, 0);
    arr = JS_VALUE_GET_ARRAY (argv[0]);
    if (JS_IsException (r)) return r;
    if (!JS_ToBool (ctx, r)) return JS_FALSE;
  }
  return JS_TRUE;
}

/* some(arr, pred) — find(arr, pred) != null */
static JSValue js_cell_some(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 2) return JS_NULL;
  JSValue r = js_cell_array_find(ctx, JS_NULL, argc, argv);
  if (JS_IsException(r)) return r;
  if (!JS_IsNull(r)) return JS_TRUE;
  return JS_FALSE;
}

static void js_set_global_cfunc(JSContext *ctx, const char *name, JSCFunction *func, int length) {
  JSGCRef ref;
  JS_PushGCRef(ctx, &ref);
  ref.val = JS_NewCFunction(ctx, func, name, length);
  JS_SetPropertyStr(ctx, ctx->global_obj, name, ref.val);
  JS_PopGCRef(ctx, &ref);
}

static void JS_AddIntrinsicBaseObjects (JSContext *ctx) {
  ctx->throw_type_error = JS_NewCFunction (ctx, js_throw_type_error, NULL, 0);
  ctx->global_obj = JS_NewObject (ctx);

  /* Cell Script global functions: text, number, array, object, fn */
  {
    JSValue text_func = JS_NewCFunction (ctx, js_cell_text, "text", 3);
    JS_SetPropertyStr (ctx, ctx->global_obj, "text", text_func);

    JSValue number_func = JS_NewCFunction (ctx, js_cell_number, "number", 2);
    JS_SetPropertyStr (ctx, ctx->global_obj, "number", number_func);

    JSValue array_func = JS_NewCFunction (ctx, js_cell_array, "array", 4);
    JS_SetPropertyStr (ctx, ctx->global_obj, "array", array_func);

    JSValue object_func = JS_NewCFunction (ctx, js_cell_object, "object", 2);
    JS_SetPropertyStr (ctx, ctx->global_obj, "object", object_func);

    /* Blob intrinsic type */
    {
      JSClassDef blob_class = {
        .class_name = "blob",
        .finalizer = NULL,
      };
      JS_NewClass (ctx, JS_CLASS_BLOB, &blob_class);
      ctx->class_proto[JS_CLASS_BLOB] = JS_NewObject (ctx);
      JS_SetPropertyFunctionList (ctx, ctx->class_proto[JS_CLASS_BLOB], js_blob_proto_funcs, countof (js_blob_proto_funcs));

      JSValue blob_ctor = JS_NewCFunction2 (ctx, js_blob_constructor, "blob", 3, JS_CFUNC_generic, 0);
      JS_SetPropertyStr (ctx, ctx->global_obj, "blob", blob_ctor);
    }

    /* Core functions - using GC-safe helper */
    js_set_global_cfunc(ctx, "mach_load", js_mach_load, 2);
    js_set_global_cfunc(ctx, "mach_eval_mcode", js_mach_eval_mcode, 3);
    js_set_global_cfunc(ctx, "mach_dump_mcode", js_mach_dump_mcode, 3);
    js_set_global_cfunc(ctx, "mach_compile_mcode_bin", js_mach_compile_mcode_bin, 2);
    js_set_global_cfunc(ctx, "gc_stats", js_gc_stats, 0);
    js_set_global_cfunc(ctx, "stone", js_cell_stone, 1);
    js_set_global_cfunc(ctx, "length", js_cell_length, 1);
    js_set_global_cfunc(ctx, "call", js_cell_call, 3);

    /* is_* type checking functions */
    js_set_global_cfunc(ctx, "is_array", js_cell_is_array, 1);
    js_set_global_cfunc(ctx, "is_blob", js_cell_is_blob, 1);
    js_set_global_cfunc(ctx, "is_data", js_cell_is_data, 1);
    js_set_global_cfunc(ctx, "is_function", js_cell_is_function, 1);
    js_set_global_cfunc(ctx, "is_logical", js_cell_is_logical, 1);
    js_set_global_cfunc(ctx, "is_integer", js_cell_is_integer, 1);
    js_set_global_cfunc(ctx, "is_null", js_cell_is_null, 1);
    js_set_global_cfunc(ctx, "is_number", js_cell_is_number, 1);
    js_set_global_cfunc(ctx, "is_object", js_cell_is_object, 1);
    js_set_global_cfunc(ctx, "is_actor", js_cell_is_actor, 1);
    js_set_global_cfunc(ctx, "is_stone", js_cell_is_stone, 1);
    js_set_global_cfunc(ctx, "is_text", js_cell_is_text, 1);
    js_set_global_cfunc(ctx, "is_proto", js_cell_is_proto, 2);
    js_set_global_cfunc(ctx, "is_letter", js_cell_is_letter, 1);

    /* Utility functions */
    js_set_global_cfunc(ctx, "apply", js_cell_fn_apply, 2);
    js_set_global_cfunc(ctx, "replace", js_cell_text_replace, 4);
    js_set_global_cfunc(ctx, "lower", js_cell_text_lower, 1);
    js_set_global_cfunc(ctx, "upper", js_cell_text_upper, 1);
    js_set_global_cfunc(ctx, "trim", js_cell_text_trim, 2);
    js_set_global_cfunc(ctx, "codepoint", js_cell_text_codepoint, 1);
    js_set_global_cfunc(ctx, "search", js_cell_text_search, 3);
    js_set_global_cfunc(ctx, "extract", js_cell_text_extract, 4);
    js_set_global_cfunc(ctx, "format", js_cell_text_format, 3);
    js_set_global_cfunc(ctx, "reduce", js_cell_array_reduce, 4);
    js_set_global_cfunc(ctx, "arrfor", js_cell_array_for, 4);
    js_set_global_cfunc(ctx, "find", js_cell_array_find, 4);
    js_set_global_cfunc(ctx, "filter", js_cell_array_filter, 2);
    js_set_global_cfunc(ctx, "sort", js_cell_array_sort, 2);

    /* Number utility functions */
    js_set_global_cfunc(ctx, "whole", js_cell_number_whole, 1);
    js_set_global_cfunc(ctx, "fraction", js_cell_number_fraction, 1);
    js_set_global_cfunc(ctx, "floor", js_cell_number_floor, 2);
    js_set_global_cfunc(ctx, "ceiling", js_cell_number_ceiling, 2);
    js_set_global_cfunc(ctx, "abs", js_cell_number_abs, 1);
    js_set_global_cfunc(ctx, "round", js_cell_number_round, 2);
    js_set_global_cfunc(ctx, "sign", js_cell_number_sign, 1);
    js_set_global_cfunc(ctx, "trunc", js_cell_number_trunc, 2);
    js_set_global_cfunc(ctx, "min", js_cell_number_min, 2);
    js_set_global_cfunc(ctx, "max", js_cell_number_max, 2);
    js_set_global_cfunc(ctx, "remainder", js_cell_number_remainder, 2);
    js_set_global_cfunc(ctx, "character", js_cell_character, 2);
    js_set_global_cfunc(ctx, "modulo", js_cell_modulo, 2);
    js_set_global_cfunc(ctx, "neg", js_cell_neg, 1);
    js_set_global_cfunc(ctx, "not", js_cell_not, 1);
    js_set_global_cfunc(ctx, "reverse", js_cell_reverse, 1);
    js_set_global_cfunc(ctx, "proto", js_cell_proto, 1);
    js_set_global_cfunc(ctx, "splat", js_cell_splat, 1);

    /* pi - mathematical constant (no GC concern for immediate float) */
    JS_SetPropertyStr(ctx, ctx->global_obj, "pi",
      JS_NewFloat64(ctx, 3.14159265358979323846264338327950288419716939937510));

    js_set_global_cfunc(ctx, "push", js_cell_push, 2);
    js_set_global_cfunc(ctx, "pop", js_cell_pop, 1);
    js_set_global_cfunc(ctx, "meme", js_cell_meme, 2);

    /* Additional builtins */
    js_set_global_cfunc(ctx, "logical", js_cell_logical, 1);
    js_set_global_cfunc(ctx, "starts_with", js_cell_starts_with, 2);
    js_set_global_cfunc(ctx, "ends_with", js_cell_ends_with, 2);
    js_set_global_cfunc(ctx, "every", js_cell_every, 2);
    js_set_global_cfunc(ctx, "some", js_cell_some, 2);

    /* fn record with apply property */
    {
      JSGCRef fn_ref;
      JS_PushGCRef(ctx, &fn_ref);
      fn_ref.val = JS_NewObject(ctx);
      JSValue apply_fn = JS_NewCFunction(ctx, js_cell_fn_apply, "apply", 2);
      JS_SetPropertyStr(ctx, fn_ref.val, "apply", apply_fn);
      JS_SetPropertyStr(ctx, ctx->global_obj, "fn", fn_ref.val);
      JS_PopGCRef(ctx, &fn_ref);
    }

    /* I/O functions */
    js_set_global_cfunc(ctx, "print", js_print, -1);  /* variadic: length < 0 means no arg limit */
    js_set_global_cfunc(ctx, "stacktrace", js_stacktrace, 0);
    js_set_global_cfunc(ctx, "caller_info", js_caller_info, 1);
  }
}

#define STRLEN(s) (sizeof (s) / sizeof (s[0]))
#define CSTR "<native C>"

static inline void key_to_buf (JSContext *ctx, JSValue key, char *dst, int cap, const char *fallback) {
  if (JS_IsNull (key)) {
    strncpy (dst, fallback, cap);
    dst[cap - 1] = 0;
    return;
  }
  JS_KeyGetStr (ctx, dst, cap, key);
  if (dst[0] == 0) {
    strncpy (dst, fallback, cap);
    dst[cap - 1] = 0;
  }
}

void js_debug_info (JSContext *js, JSValue fn, js_debug *dbg) {
  *dbg = (js_debug){ 0 };

  if (!JS_IsFunction (fn)) return;

  JSFunction *f = JS_VALUE_GET_FUNCTION (fn);
  dbg->unique = (int)(uintptr_t)f;

  JSValue name_key = JS_NULL;
  if (!JS_IsNull (f->name))
    name_key = f->name;

  key_to_buf (js, name_key, dbg->name, sizeof (dbg->name), "<anonymous>");

  if (f->kind == JS_FUNC_KIND_C || f->kind == JS_FUNC_KIND_C_DATA) {
    strncpy (dbg->filename, "<native C>", sizeof (dbg->filename));
    dbg->filename[sizeof (dbg->filename) - 1] = 0;
    dbg->what = "C";
    dbg->param_n = f->length;
    dbg->vararg = 1;
    dbg->line = 0;
    dbg->source = (const uint8_t *)CSTR;
    dbg->srclen = STRLEN (CSTR);
  }
}

void js_debug_sethook (JSContext *ctx, js_hook hook, int type, void *user) {
  ctx->trace_hook = hook;
  ctx->trace_type = type;
  ctx->trace_data = user;
}

/* ============================================================================
 * Cell Script Module: json
 * Provides json.encode() and json.decode() using pure C implementation
 * ============================================================================
 */

static JSValue js_cell_json_encode (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "json.encode requires at least 1 argument");

  BOOL pretty = argc <= 1 || JS_ToBool (ctx, argv[1]);
  JSValue space = pretty ? JS_NewInt32 (ctx, 2) : JS_NULL;
  JSValue replacer = JS_NULL;
  if (argc > 2 && JS_IsFunction (argv[2]))
    replacer = argv[2];
  else if (argc > 3 && JS_IsArray (argv[3]))
    replacer = argv[3];
  JSValue result = JS_JSONStringify (ctx, argv[0], replacer, space, pretty);
  return result;
}

static JSValue js_cell_json_decode (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  if (argc < 1)
    return JS_ThrowTypeError (ctx, "json.decode requires at least 1 argument");

  if (!JS_IsText (argv[0]))
    return JS_ThrowTypeError (ctx, "couldn't parse text: not a string");

  const char *str = JS_ToCString (ctx, argv[0]);
  if (!str) return JS_EXCEPTION;

  size_t len = strlen (str);
  JSValue result = JS_ParseJSON (ctx, str, len, "<json>");
  JS_FreeCString (ctx, str);

  /* Apply reviver if provided */
  if (argc > 1 && JS_IsFunction (argv[1]) && !JS_IsException (result)) {
    /* Create wrapper object to pass to reviver */
    JSValue wrapper = JS_NewObject (ctx);
    JS_SetPropertyStr (ctx, wrapper, "", result);

    JSValue holder = wrapper;
    JSValue key = JS_KEY_empty;
    JSValue args[2] = { key, JS_GetProperty (ctx, holder, key) };
    JSValue final = JS_Call (ctx, argv[1], holder, 2, args);
    result = final;
  }

  return result;
}

static const JSCFunctionListEntry js_cell_json_funcs[] = {
  JS_CFUNC_DEF ("encode", 1, js_cell_json_encode),
  JS_CFUNC_DEF ("decode", 1, js_cell_json_decode),
};

JSValue js_core_json_use (JSContext *ctx) {
  JSGCRef obj_ref;
  JS_PushGCRef (ctx, &obj_ref);
  obj_ref.val = JS_NewObject (ctx);
  JS_SetPropertyFunctionList (ctx, obj_ref.val, js_cell_json_funcs, countof (js_cell_json_funcs));
  JSValue result = obj_ref.val;
  JS_PopGCRef (ctx, &obj_ref);
  return result;
}

/* ============================================================================
 * Cell Script Module: nota
 * Provides nota.encode() and nota.decode() for NOTA binary serialization
 * ============================================================================
 */

static int nota_get_arr_len (JSContext *ctx, JSValue arr) {
  int64_t len;
  JS_GetLength (ctx, arr, &len);
  return (int)len;
}

typedef struct NotaVisitedNode {
  JSGCRef ref;
  struct NotaVisitedNode *next;
} NotaVisitedNode;

typedef struct NotaEncodeContext {
  JSContext *ctx;
  NotaVisitedNode *visited_list;
  NotaBuffer nb;
  int cycle;
  JSGCRef *replacer_ref; /* pointer to GC-rooted ref */
} NotaEncodeContext;

static void nota_stack_push (NotaEncodeContext *enc, JSValueConst val) {
  NotaVisitedNode *node = (NotaVisitedNode *)sys_malloc (sizeof (NotaVisitedNode));
  JS_PushGCRef (enc->ctx, &node->ref);
  node->ref.val = JS_DupValue (enc->ctx, val);
  node->next = enc->visited_list;
  enc->visited_list = node;
}

static void nota_stack_pop (NotaEncodeContext *enc) {
  NotaVisitedNode *node = enc->visited_list;
  enc->visited_list = node->next;
  JS_FreeValue (enc->ctx, node->ref.val);
  JS_PopGCRef (enc->ctx, &node->ref);
  sys_free (node);
}

static int nota_stack_has (NotaEncodeContext *enc, JSValueConst val) {
  NotaVisitedNode *node = enc->visited_list;
  while (node) {
    if (JS_StrictEq (enc->ctx, node->ref.val, val))
      return 1;
    node = node->next;
  }
  return 0;
}

static JSValue nota_apply_replacer (NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) {
  if (!enc->replacer_ref || JS_IsNull (enc->replacer_ref->val)) return JS_DupValue (enc->ctx, val);

  JSValue args[2] = { JS_DupValue (enc->ctx, key), JS_DupValue (enc->ctx, val) };
  JSValue result = JS_Call (enc->ctx, enc->replacer_ref->val, holder, 2, args);
  JS_FreeValue (enc->ctx, args[0]);
  JS_FreeValue (enc->ctx, args[1]);

  if (JS_IsException (result)) return JS_DupValue (enc->ctx, val);
  return result;
}

static char *js_do_nota_decode (JSContext *js, JSValue *tmp, char *nota, JSValue holder, JSValue key, JSValue reviver) {
  int type = nota_type (nota);
  JSValue ret2;
  long long n;
  double d;
  int b;
  char *str;
  uint8_t *blob;

  switch (type) {
    case NOTA_BLOB:
      nota = nota_read_blob (&n, (char **)&blob, nota);
      *tmp = js_new_blob_stoned_copy (js, blob, n);
      sys_free (blob);
      break;
    case NOTA_TEXT:
      nota = nota_read_text (&str, nota);
      *tmp = JS_NewString (js, str);
      sys_free (str);
      break;
    case NOTA_ARR:
      nota = nota_read_array (&n, nota);
      *tmp = JS_NewArrayLen (js, n);
      for (int i = 0; i < n; i++) {
        nota = js_do_nota_decode (js, &ret2, nota, *tmp, JS_NewInt32 (js, i), reviver);
        JS_SetPropertyNumber (js, *tmp, i, ret2);
      }
      break;
    case NOTA_REC:
      nota = nota_read_record (&n, nota);
      *tmp = JS_NewObject (js);
      for (int i = 0; i < n; i++) {
        JSGCRef prop_key_ref, sub_val_ref;
        JS_PushGCRef (js, &prop_key_ref);
        JS_PushGCRef (js, &sub_val_ref);
        nota = nota_read_text (&str, nota);
        prop_key_ref.val = JS_NewString (js, str);
        sub_val_ref.val = JS_NULL;
        nota = js_do_nota_decode (js, &sub_val_ref.val, nota, *tmp, prop_key_ref.val, reviver);
        JS_SetPropertyStr (js, *tmp, str, sub_val_ref.val);
        JS_PopGCRef (js, &sub_val_ref);
        JS_PopGCRef (js, &prop_key_ref);
        sys_free (str);
      }
      break;
    case NOTA_INT:
      nota = nota_read_int (&n, nota);
      *tmp = JS_NewInt64 (js, n);
      break;
    case NOTA_SYM:
      nota = nota_read_sym (&b, nota);
      if (b == NOTA_PRIVATE) {
        JSGCRef inner_ref, obj_ref2;
        JS_PushGCRef (js, &inner_ref);
        JS_PushGCRef (js, &obj_ref2);
        inner_ref.val = JS_NULL;
        nota = js_do_nota_decode (js, &inner_ref.val, nota, holder, JS_NULL, reviver);
        obj_ref2.val = JS_NewObject (js);
        if (!JS_IsNull (js->actor_sym))
          JS_SetPropertyKey (js, obj_ref2.val, js->actor_sym, inner_ref.val);
        JS_CellStone (js, obj_ref2.val);
        *tmp = obj_ref2.val;
        JS_PopGCRef (js, &obj_ref2);
        JS_PopGCRef (js, &inner_ref);
      } else {
        switch (b) {
          case NOTA_NULL: *tmp = JS_NULL; break;
          case NOTA_FALSE: *tmp = JS_NewBool (js, 0); break;
          case NOTA_TRUE: *tmp = JS_NewBool (js, 1); break;
          default: *tmp = JS_NULL; break;
        }
      }
      break;
    default:
    case NOTA_FLOAT:
      nota = nota_read_float (&d, nota);
      *tmp = JS_NewFloat64 (js, d);
      break;
  }

  if (!JS_IsNull (reviver)) {
    JSValue args[2] = { JS_DupValue (js, key), JS_DupValue (js, *tmp) };
    JSValue revived = JS_Call (js, reviver, holder, 2, args);
    JS_FreeValue (js, args[0]);
    JS_FreeValue (js, args[1]);
    if (!JS_IsException (revived)) {
      JS_FreeValue (js, *tmp);
      *tmp = revived;
    } else {
      JS_FreeValue (js, revived);
    }
  }

  return nota;
}

static void nota_encode_value (NotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key) {
  JSContext *ctx = enc->ctx;
  JSGCRef replaced_ref, keys_ref, elem_ref, prop_ref;
  JS_PushGCRef (ctx, &replaced_ref);
  replaced_ref.val = nota_apply_replacer (enc, holder, key, val);
  int tag = JS_VALUE_GET_TAG (replaced_ref.val);

  switch (tag) {
    case JS_TAG_INT:
    case JS_TAG_FLOAT64: {
      double d;
      JS_ToFloat64 (ctx, &d, replaced_ref.val);
      nota_write_number (&enc->nb, d);
      break;
    }
    case JS_TAG_STRING: {
      const char *str = JS_ToCString (ctx, replaced_ref.val);
      nota_write_text (&enc->nb, str);
      JS_FreeCString (ctx, str);
      break;
    }
    case JS_TAG_BOOL:
      if (JS_VALUE_GET_BOOL (replaced_ref.val)) nota_write_sym (&enc->nb, NOTA_TRUE);
      else nota_write_sym (&enc->nb, NOTA_FALSE);
      break;
    case JS_TAG_NULL:
      nota_write_sym (&enc->nb, NOTA_NULL);
      break;
    case JS_TAG_PTR: {
      if (JS_IsText (replaced_ref.val)) {
        const char *str = JS_ToCString (ctx, replaced_ref.val);
        nota_write_text (&enc->nb, str);
        JS_FreeCString (ctx, str);
        break;
      }

      if (js_is_blob (ctx, replaced_ref.val)) {
        size_t buf_len;
        void *buf_data = js_get_blob_data (ctx, &buf_len, replaced_ref.val);
        if (buf_data == (void *)-1) {
          JS_PopGCRef (ctx, &replaced_ref);
          return;
        }
        nota_write_blob (&enc->nb, (unsigned long long)buf_len * 8, (const char *)buf_data);
        break;
      }

      if (JS_IsArray (replaced_ref.val)) {
        if (nota_stack_has (enc, replaced_ref.val)) {
          enc->cycle = 1;
          break;
        }
        nota_stack_push (enc, replaced_ref.val);
        int arr_len = nota_get_arr_len (ctx, replaced_ref.val);
        nota_write_array (&enc->nb, arr_len);
        JS_PushGCRef (ctx, &elem_ref);
        for (int i = 0; i < arr_len; i++) {
          elem_ref.val = JS_GetPropertyNumber (ctx, replaced_ref.val, i);
          JSValue elem_key = JS_NewInt32 (ctx, i);
          nota_encode_value (enc, elem_ref.val, replaced_ref.val, elem_key);
        }
        JS_PopGCRef (ctx, &elem_ref);
        nota_stack_pop (enc);
        break;
      }

      JSValue adata = JS_NULL;
      if (!JS_IsNull (ctx->actor_sym)) {
        int has = JS_HasPropertyKey (ctx, replaced_ref.val, ctx->actor_sym);
        if (has > 0) adata = JS_GetPropertyKey (ctx, replaced_ref.val, ctx->actor_sym);
      }
      if (!JS_IsNull (adata)) {
        nota_write_sym (&enc->nb, NOTA_PRIVATE);
        nota_encode_value (enc, adata, replaced_ref.val, JS_NULL);
        JS_FreeValue (ctx, adata);
        break;
      }
      JS_FreeValue (ctx, adata);
      if (nota_stack_has (enc, replaced_ref.val)) {
        enc->cycle = 1;
        break;
      }
      nota_stack_push (enc, replaced_ref.val);

      JSValue to_json = JS_GetPropertyStr (ctx, replaced_ref.val, "toJSON");
      if (JS_IsFunction (to_json)) {
        JSValue result = JS_Call (ctx, to_json, replaced_ref.val, 0, NULL);
        if (!JS_IsException (result)) {
          nota_encode_value (enc, result, holder, key);
        } else {
          nota_write_sym (&enc->nb, NOTA_NULL);
        }
        nota_stack_pop (enc);
        break;
      }

      JS_PushGCRef (ctx, &keys_ref);
      keys_ref.val = JS_GetOwnPropertyNames (ctx, replaced_ref.val);
      if (JS_IsException (keys_ref.val)) {
        nota_write_sym (&enc->nb, NOTA_NULL);
        nota_stack_pop (enc);
        JS_PopGCRef (ctx, &keys_ref);
        break;
      }
      int64_t plen64;
      if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) {
        nota_write_sym (&enc->nb, NOTA_NULL);
        nota_stack_pop (enc);
        JS_PopGCRef (ctx, &keys_ref);
        break;
      }
      uint32_t plen = (uint32_t)plen64;

      JS_PushGCRef (ctx, &prop_ref);
      JS_PushGCRef (ctx, &elem_ref);
      uint32_t non_function_count = 0;
      for (uint32_t i = 0; i < plen; i++) {
        elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
        prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val);
        if (!JS_IsFunction (prop_ref.val)) non_function_count++;
      }

      nota_write_record (&enc->nb, non_function_count);
      for (uint32_t i = 0; i < plen; i++) {
        elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
        prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val);
        if (!JS_IsFunction (prop_ref.val)) {
          const char *prop_name = JS_ToCString (ctx, elem_ref.val);
          nota_write_text (&enc->nb, prop_name ? prop_name : "");
          nota_encode_value (enc, prop_ref.val, replaced_ref.val, elem_ref.val);
          JS_FreeCString (ctx, prop_name);
        }
      }
      JS_PopGCRef (ctx, &elem_ref);
      JS_PopGCRef (ctx, &prop_ref);
      JS_PopGCRef (ctx, &keys_ref);
      nota_stack_pop (enc);
      break;
    }
    default:
      nota_write_sym (&enc->nb, NOTA_NULL);
      break;
  }
  JS_PopGCRef (ctx, &replaced_ref);
}

void *value2nota (JSContext *ctx, JSValue v) {
  JSGCRef val_ref, key_ref;
  JS_PushGCRef (ctx, &val_ref);
  JS_PushGCRef (ctx, &key_ref);
  val_ref.val = v;

  NotaEncodeContext enc_s, *enc = &enc_s;
  enc->ctx = ctx;
  enc->visited_list = NULL;
  enc->cycle = 0;
  enc->replacer_ref = NULL;

  nota_buffer_init (&enc->nb, 128);
  key_ref.val = JS_NewString (ctx, "");
  nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val);

  if (enc->cycle) {
    JS_PopGCRef (ctx, &key_ref);
    JS_PopGCRef (ctx, &val_ref);
    nota_buffer_free (&enc->nb);
    return NULL;
  }

  JS_PopGCRef (ctx, &key_ref);
  JS_PopGCRef (ctx, &val_ref);
  void *data_ptr = enc->nb.data;
  enc->nb.data = NULL;
  nota_buffer_free (&enc->nb);
  return data_ptr;
}

JSValue nota2value (JSContext *js, void *nota) {
  if (!nota) return JS_NULL;
  JSGCRef holder_ref, key_ref, ret_ref;
  JS_PushGCRef (js, &holder_ref);
  JS_PushGCRef (js, &key_ref);
  JS_PushGCRef (js, &ret_ref);
  holder_ref.val = JS_NewObject (js);
  key_ref.val = JS_NewString (js, "");
  ret_ref.val = JS_NULL;
  js_do_nota_decode (js, &ret_ref.val, nota, holder_ref.val, key_ref.val, JS_NULL);
  JSValue result = ret_ref.val;
  JS_PopGCRef (js, &ret_ref);
  JS_PopGCRef (js, &key_ref);
  JS_PopGCRef (js, &holder_ref);
  return result;
}

static JSValue js_nota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
  if (argc < 1) return JS_ThrowTypeError (ctx, "nota.encode requires at least 1 argument");

  JSGCRef val_ref, replacer_ref, key_ref;
  JS_PushGCRef (ctx, &val_ref);
  JS_PushGCRef (ctx, &replacer_ref);
  JS_PushGCRef (ctx, &key_ref);
  val_ref.val = argv[0];
  replacer_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;

  NotaEncodeContext enc_s, *enc = &enc_s;
  enc->ctx = ctx;
  enc->visited_list = NULL;
  enc->cycle = 0;
  enc->replacer_ref = &replacer_ref;

  nota_buffer_init (&enc->nb, 128);
  key_ref.val = JS_NewString (ctx, "");
  nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val);

  JSValue ret;
  if (enc->cycle) {
    nota_buffer_free (&enc->nb);
    ret = JS_ThrowReferenceError (ctx, "Tried to encode something to nota with a cycle.");
  } else {
    size_t total_len = enc->nb.size;
    void *data_ptr = enc->nb.data;
    ret = js_new_blob_stoned_copy (ctx, (uint8_t *)data_ptr, total_len);
    nota_buffer_free (&enc->nb);
  }

  JS_PopGCRef (ctx, &key_ref);
  JS_PopGCRef (ctx, &replacer_ref);
  JS_PopGCRef (ctx, &val_ref);
  return ret;
}

static JSValue js_nota_decode (JSContext *js, JSValueConst self, int argc, JSValueConst *argv) {
  if (argc < 1) return JS_NULL;

  size_t len;
  unsigned char *nota = js_get_blob_data (js, &len, argv[0]);
  if (nota == (unsigned char *)-1) return JS_EXCEPTION;
  if (!nota) return JS_NULL;

  JSGCRef holder_ref, key_ref, ret_ref, reviver_ref;
  JS_PushGCRef (js, &holder_ref);
  JS_PushGCRef (js, &key_ref);
  JS_PushGCRef (js, &ret_ref);
  JS_PushGCRef (js, &reviver_ref);

  reviver_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
  holder_ref.val = JS_NewObject (js);
  key_ref.val = JS_NewString (js, "");
  ret_ref.val = JS_NULL;

  js_do_nota_decode (js, &ret_ref.val, (char *)nota, holder_ref.val, key_ref.val, reviver_ref.val);

  JSValue result = ret_ref.val;
  JS_PopGCRef (js, &reviver_ref);
  JS_PopGCRef (js, &ret_ref);
  JS_PopGCRef (js, &key_ref);
  JS_PopGCRef (js, &holder_ref);
  return result;
}

static const JSCFunctionListEntry js_nota_funcs[] = {
  JS_CFUNC_DEF ("encode", 1, js_nota_encode),
  JS_CFUNC_DEF ("decode", 1, js_nota_decode),
};

JSValue js_core_nota_use (JSContext *js) {
  JSGCRef export_ref;
  JS_PushGCRef (js, &export_ref);
  export_ref.val = JS_NewObject (js);
  JS_SetPropertyFunctionList (js, export_ref.val, js_nota_funcs, sizeof (js_nota_funcs) / sizeof (JSCFunctionListEntry));
  JSValue result = export_ref.val;
  JS_PopGCRef (js, &export_ref);
  return result;
}

/* ============================================================================
 * Cell Script Module: wota
 * Provides wota.encode() and wota.decode() for WOTA binary serialization
 * ============================================================================
 */

typedef struct ObjectRef {
  void *ptr;
  struct ObjectRef *next;
} ObjectRef;

typedef struct WotaEncodeContext {
  JSContext *ctx;
  ObjectRef *visited_stack;
  WotaBuffer wb;
  int cycle;
  JSValue replacer;
} WotaEncodeContext;

static void wota_stack_push (WotaEncodeContext *enc, JSValueConst val) {
  (void)enc; (void)val;
  /* Cycle detection disabled for performance */
}

static void wota_stack_pop (WotaEncodeContext *enc) {
  if (!enc->visited_stack) return;

  ObjectRef *top = enc->visited_stack;
  enc->visited_stack = top->next;
  sys_free (top);
}

static int wota_stack_has (WotaEncodeContext *enc, JSValueConst val) {
  (void)enc; (void)val;
  return 0;
  /* Cycle detection disabled for performance */
}

static void wota_stack_free (WotaEncodeContext *enc) {
  while (enc->visited_stack) {
    wota_stack_pop (enc);
  }
}

static JSValue wota_apply_replacer (WotaEncodeContext *enc, JSValueConst holder, JSValue key, JSValueConst val) {
  if (JS_IsNull (enc->replacer)) return JS_DupValue (enc->ctx, val);
  JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (enc->ctx, key);
  JSValue args[2] = { key_val, JS_DupValue (enc->ctx, val) };
  JSValue result = JS_Call (enc->ctx, enc->replacer, holder, 2, args);
  JS_FreeValue (enc->ctx, args[0]);
  JS_FreeValue (enc->ctx, args[1]);
  if (JS_IsException (result)) return JS_DupValue (enc->ctx, val);
  return result;
}

static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValue key);

static void encode_object_properties (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder) {
  JSContext *ctx = enc->ctx;

  /* Root the input value to protect it during property enumeration */
  JSGCRef val_ref, keys_ref;
  JS_PushGCRef (ctx, &val_ref);
  JS_PushGCRef (ctx, &keys_ref);
  val_ref.val = JS_DupValue (ctx, val);

  keys_ref.val = JS_GetOwnPropertyNames (ctx, val_ref.val);
  if (JS_IsException (keys_ref.val)) {
    wota_write_sym (&enc->wb, WOTA_NULL);
    JS_FreeValue (ctx, val_ref.val);
    JS_PopGCRef (ctx, &keys_ref);
    JS_PopGCRef (ctx, &val_ref);
    return;
  }
  int64_t plen64;
  if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) {
    JS_FreeValue (ctx, keys_ref.val);
    JS_FreeValue (ctx, val_ref.val);
    wota_write_sym (&enc->wb, WOTA_NULL);
    JS_PopGCRef (ctx, &keys_ref);
    JS_PopGCRef (ctx, &val_ref);
    return;
  }
  uint32_t plen = (uint32_t)plen64;
  uint32_t non_function_count = 0;

  /* Allocate GC-rooted arrays for props and keys */
  JSGCRef *prop_refs = sys_malloc (sizeof (JSGCRef) * plen);
  JSGCRef *key_refs = sys_malloc (sizeof (JSGCRef) * plen);
  for (uint32_t i = 0; i < plen; i++) {
    JS_PushGCRef (ctx, &prop_refs[i]);
    JS_PushGCRef (ctx, &key_refs[i]);
    prop_refs[i].val = JS_NULL;
    key_refs[i].val = JS_NULL;
  }

  for (uint32_t i = 0; i < plen; i++) {
    /* Store key into its GCRef slot immediately so it's rooted before
       JS_GetProperty can trigger GC and relocate the string. */
    key_refs[i].val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
    JSValue prop_val = JS_GetProperty (ctx, val_ref.val, key_refs[i].val);
    if (!JS_IsFunction (prop_val)) {
      if (i != non_function_count) {
        key_refs[non_function_count].val = key_refs[i].val;
        key_refs[i].val = JS_NULL;
      }
      prop_refs[non_function_count].val = prop_val;
      non_function_count++;
    } else {
      JS_FreeValue (ctx, prop_val);
      JS_FreeValue (ctx, key_refs[i].val);
      key_refs[i].val = JS_NULL;
    }
  }
  JS_FreeValue (ctx, keys_ref.val);
  wota_write_record (&enc->wb, non_function_count);
  for (uint32_t i = 0; i < non_function_count; i++) {
    size_t klen;
    const char *prop_name = JS_ToCStringLen (ctx, &klen, key_refs[i].val);
    wota_write_text_len (&enc->wb, prop_name ? prop_name : "", prop_name ? klen : 0);
    wota_encode_value (enc, prop_refs[i].val, val_ref.val, key_refs[i].val);
    JS_FreeCString (ctx, prop_name);
    JS_FreeValue (ctx, prop_refs[i].val);
    JS_FreeValue (ctx, key_refs[i].val);
  }
  /* Pop all GC refs in reverse order */
  for (int i = plen - 1; i >= 0; i--) {
    JS_PopGCRef (ctx, &key_refs[i]);
    JS_PopGCRef (ctx, &prop_refs[i]);
  }
  sys_free (prop_refs);
  sys_free (key_refs);
  JS_FreeValue (ctx, val_ref.val);
  JS_PopGCRef (ctx, &keys_ref);
  JS_PopGCRef (ctx, &val_ref);
}

static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValue key) {
  JSContext *ctx = enc->ctx;
  JSValue replaced;
  if (!JS_IsNull (enc->replacer) && !JS_IsNull (key))
    replaced = wota_apply_replacer (enc, holder, key, val);
  else
    replaced = JS_DupValue (enc->ctx, val);

  int tag = JS_VALUE_GET_TAG (replaced);
  switch (tag) {
    case JS_TAG_INT: {
      int32_t d;
      JS_ToInt32 (ctx, &d, replaced);
      wota_write_int_word (&enc->wb, d);
      break;
    }
    case JS_TAG_FLOAT64: {
      double d;
      if (JS_ToFloat64 (ctx, &d, replaced) < 0) {
        wota_write_sym (&enc->wb, WOTA_NULL);
        break;
      }
      wota_write_float_word (&enc->wb, d);
      break;
    }
    case JS_TAG_STRING: {
      size_t plen;
      const char *str = JS_ToCStringLen (ctx, &plen, replaced);
      wota_write_text_len (&enc->wb, str ? str : "", str ? plen : 0);
      JS_FreeCString (ctx, str);
      break;
    }
    case JS_TAG_BOOL:
      wota_write_sym (&enc->wb, JS_VALUE_GET_BOOL (replaced) ? WOTA_TRUE : WOTA_FALSE);
      break;
    case JS_TAG_NULL:
      wota_write_sym (&enc->wb, WOTA_NULL);
      break;
    case JS_TAG_PTR: {
      if (JS_IsText (replaced)) {
        size_t plen;
        const char *str = JS_ToCStringLen (ctx, &plen, replaced);
        wota_write_text_len (&enc->wb, str ? str : "", str ? plen : 0);
        JS_FreeCString (ctx, str);
        break;
      }
      if (js_is_blob (ctx, replaced)) {
        size_t buf_len;
        void *buf_data = js_get_blob_data (ctx, &buf_len, replaced);
        if (buf_data == (void *)-1) {
          JS_FreeValue (ctx, replaced);
          return;
        }
        if (buf_len == 0) {
          wota_write_blob (&enc->wb, 0, "");
        } else {
          wota_write_blob (&enc->wb, (unsigned long long)buf_len * 8, (const char *)buf_data);
        }
        break;
      }
      if (JS_IsArray (replaced)) {
        if (wota_stack_has (enc, replaced)) {
          enc->cycle = 1;
          break;
        }
        wota_stack_push (enc, replaced);
        int64_t arr_len;
        JS_GetLength (ctx, replaced, &arr_len);
        wota_write_array (&enc->wb, arr_len);
        for (int64_t i = 0; i < arr_len; i++) {
          JSValue elem_val = JS_GetPropertyNumber (ctx, replaced, i);
          wota_encode_value (enc, elem_val, replaced, JS_NewInt32 (ctx, (int32_t)i));
          JS_FreeValue (ctx, elem_val);
        }
        wota_stack_pop (enc);
        break;
      }
      JSValue adata = JS_NULL;
      if (!JS_IsNull (ctx->actor_sym)) {
        int has = JS_HasPropertyKey (ctx, replaced, ctx->actor_sym);
        if (has > 0) adata = JS_GetPropertyKey (ctx, replaced, ctx->actor_sym);
      }
      if (!JS_IsNull (adata)) {
        wota_write_sym (&enc->wb, WOTA_PRIVATE);
        wota_encode_value (enc, adata, replaced, JS_NULL);
        JS_FreeValue (ctx, adata);
        break;
      }
      JS_FreeValue (ctx, adata);
      if (wota_stack_has (enc, replaced)) {
        enc->cycle = 1;
        break;
      }
      wota_stack_push (enc, replaced);
      JSValue to_json = JS_GetPropertyStr (ctx, replaced, "toJSON");
      if (JS_IsFunction (to_json)) {
        JSValue result = JS_Call (ctx, to_json, replaced, 0, NULL);
        JS_FreeValue (ctx, to_json);
        if (!JS_IsException (result)) {
          wota_encode_value (enc, result, holder, key);
          JS_FreeValue (ctx, result);
        } else
          wota_write_sym (&enc->wb, WOTA_NULL);
        wota_stack_pop (enc);
        break;
      }
      JS_FreeValue (ctx, to_json);
      encode_object_properties (enc, replaced, holder);
      wota_stack_pop (enc);
      break;
    }
    default:
      wota_write_sym (&enc->wb, WOTA_NULL);
      break;
  }
  JS_FreeValue (ctx, replaced);
}

static char *decode_wota_value (JSContext *ctx, char *data_ptr, JSValue *out_val, JSValue holder, JSValue key, JSValue reviver) {
  uint64_t first_word = *(uint64_t *)data_ptr;
  int type = (int)(first_word & 0xffU);
  switch (type) {
    case WOTA_INT: {
      long long val;
      data_ptr = wota_read_int (&val, data_ptr);
      *out_val = JS_NewInt64 (ctx, val);
      break;
    }
    case WOTA_FLOAT: {
      double d;
      data_ptr = wota_read_float (&d, data_ptr);
      *out_val = JS_NewFloat64 (ctx, d);
      break;
    }
    case WOTA_SYM: {
      int scode;
      data_ptr = wota_read_sym (&scode, data_ptr);
      if (scode == WOTA_PRIVATE) {
        JSGCRef inner_ref, obj_ref2;
        JS_PushGCRef (ctx, &inner_ref);
        JS_PushGCRef (ctx, &obj_ref2);
        inner_ref.val = JS_NULL;
        data_ptr = decode_wota_value (ctx, data_ptr, &inner_ref.val, holder, JS_NULL, reviver);
        obj_ref2.val = JS_NewObject (ctx);
        if (!JS_IsNull (ctx->actor_sym))
          JS_SetPropertyKey (ctx, obj_ref2.val, ctx->actor_sym, inner_ref.val);
        JS_CellStone (ctx, obj_ref2.val);
        *out_val = obj_ref2.val;
        JS_PopGCRef (ctx, &obj_ref2);
        JS_PopGCRef (ctx, &inner_ref);
      } else if (scode == WOTA_NULL) *out_val = JS_NULL;
      else if (scode == WOTA_FALSE) *out_val = JS_NewBool (ctx, 0);
      else if (scode == WOTA_TRUE) *out_val = JS_NewBool (ctx, 1);
      else *out_val = JS_NULL;
      break;
    }
    case WOTA_BLOB: {
      long long blen;
      char *bdata = NULL;
      data_ptr = wota_read_blob (&blen, &bdata, data_ptr);
      *out_val = bdata ? js_new_blob_stoned_copy (ctx, (uint8_t *)bdata, (size_t)blen) : js_new_blob_stoned_copy (ctx, NULL, 0);
      if (bdata) sys_free (bdata);
      break;
    }
    case WOTA_TEXT: {
      char *utf8 = NULL;
      data_ptr = wota_read_text (&utf8, data_ptr);
      *out_val = JS_NewString (ctx, utf8 ? utf8 : "");
      if (utf8) sys_free (utf8);
      break;
    }
    case WOTA_ARR: {
      long long c;
      data_ptr = wota_read_array (&c, data_ptr);
      JSGCRef arr_ref;
      JS_PushGCRef (ctx, &arr_ref);
      arr_ref.val = JS_NewArrayLen (ctx, c);
      for (long long i = 0; i < c; i++) {
        JSGCRef elem_ref;
        JS_PushGCRef (ctx, &elem_ref);
        elem_ref.val = JS_NULL;
        JSValue idx_key = JS_NewInt32 (ctx, (int32_t)i);
        data_ptr = decode_wota_value (ctx, data_ptr, &elem_ref.val, arr_ref.val, idx_key, reviver);
        JS_SetPropertyNumber (ctx, arr_ref.val, i, elem_ref.val);
        JS_PopGCRef (ctx, &elem_ref);
      }
      *out_val = arr_ref.val;
      JS_PopGCRef (ctx, &arr_ref);
      break;
    }
    case WOTA_REC: {
      long long c;
      data_ptr = wota_read_record (&c, data_ptr);
      JSGCRef obj_ref;
      JS_PushGCRef (ctx, &obj_ref);
      obj_ref.val = JS_NewObject (ctx);
      for (long long i = 0; i < c; i++) {
        char *tkey = NULL;
        size_t key_len;
        data_ptr = wota_read_text_len (&key_len, &tkey, data_ptr);
        if (!tkey) continue;
        JSGCRef prop_key_ref, sub_val_ref;
        JS_PushGCRef (ctx, &prop_key_ref);
        JS_PushGCRef (ctx, &sub_val_ref);
        prop_key_ref.val = JS_NewStringLen (ctx, tkey, key_len);
        sub_val_ref.val = JS_NULL;
        data_ptr = decode_wota_value (ctx, data_ptr, &sub_val_ref.val, obj_ref.val, prop_key_ref.val, reviver);
        JS_SetPropertyStr (ctx, obj_ref.val, tkey, sub_val_ref.val);
        JS_PopGCRef (ctx, &sub_val_ref);
        JS_PopGCRef (ctx, &prop_key_ref);
        sys_free (tkey);
      }
      *out_val = obj_ref.val;
      JS_PopGCRef (ctx, &obj_ref);
      break;
    }
    default:
      data_ptr += 8;
      *out_val = JS_NULL;
      break;
  }
  if (!JS_IsNull (reviver)) {
    JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (ctx, key);
    JSValue args[2] = { key_val, JS_DupValue (ctx, *out_val) };
    JSValue revived = JS_Call (ctx, reviver, holder, 2, args);
    JS_FreeValue (ctx, args[0]);
    JS_FreeValue (ctx, args[1]);
    if (!JS_IsException (revived)) {
      JS_FreeValue (ctx, *out_val);
      *out_val = revived;
    } else
      JS_FreeValue (ctx, revived);
  }
  return data_ptr;
}

void *value2wota (JSContext *ctx, JSValue v, JSValue replacer, size_t *bytes) {
  JSGCRef val_ref, rep_ref;
  JS_PushGCRef (ctx, &val_ref);
  JS_PushGCRef (ctx, &rep_ref);
  val_ref.val = v;
  rep_ref.val = replacer;

  WotaEncodeContext enc_s, *enc = &enc_s;

  enc->ctx = ctx;
  enc->visited_stack = NULL;
  enc->cycle = 0;
  enc->replacer = rep_ref.val;
  wota_buffer_init (&enc->wb, 16);
  wota_encode_value (enc, val_ref.val, JS_NULL, JS_NULL);
  if (enc->cycle) {
    wota_stack_free (enc);
    wota_buffer_free (&enc->wb);
    JS_PopGCRef (ctx, &rep_ref);
    JS_PopGCRef (ctx, &val_ref);
    return NULL;
  }
  wota_stack_free (enc);
  size_t total_bytes = enc->wb.size * sizeof (uint64_t);
  void *wota = sys_realloc (enc->wb.data, total_bytes);
  if (bytes) *bytes = total_bytes;
  JS_PopGCRef (ctx, &rep_ref);
  JS_PopGCRef (ctx, &val_ref);
  return wota;
}

JSValue wota2value (JSContext *ctx, void *wota) {
  JSGCRef holder_ref, result_ref;
  JS_PushGCRef (ctx, &holder_ref);
  JS_PushGCRef (ctx, &result_ref);
  result_ref.val = JS_NULL;
  holder_ref.val = JS_NewObject (ctx);
  decode_wota_value (ctx, wota, &result_ref.val, holder_ref.val, JS_NULL, JS_NULL);
  JSValue result = result_ref.val;
  JS_PopGCRef (ctx, &result_ref);
  JS_PopGCRef (ctx, &holder_ref);
  return result;
}

static JSValue js_wota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
  if (argc < 1) return JS_ThrowTypeError (ctx, "wota.encode requires at least 1 argument");
  size_t total_bytes;
  void *wota = value2wota (ctx, argv[0], JS_IsFunction (argv[1]) ? argv[1] : JS_NULL, &total_bytes);
  JSValue ret = js_new_blob_stoned_copy (ctx, wota, total_bytes);
  sys_free (wota);
  return ret;
}

static JSValue js_wota_decode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
  if (argc < 1) return JS_NULL;
  size_t len;
  uint8_t *buf = js_get_blob_data (ctx, &len, argv[0]);
  if (buf == (uint8_t *)-1) return JS_EXCEPTION;
  if (!buf || len == 0) return JS_ThrowTypeError (ctx, "No blob data present");
  JSValue reviver = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
  char *data_ptr = (char *)buf;
  JSGCRef result_ref, holder_ref, empty_key_ref;
  JS_PushGCRef (ctx, &result_ref);
  JS_PushGCRef (ctx, &holder_ref);
  JS_PushGCRef (ctx, &empty_key_ref);
  result_ref.val = JS_NULL;
  holder_ref.val = JS_NewObject (ctx);
  empty_key_ref.val = JS_NewString (ctx, "");
  decode_wota_value (ctx, data_ptr, &result_ref.val, holder_ref.val, empty_key_ref.val, reviver);
  JSValue result = result_ref.val;
  JS_PopGCRef (ctx, &empty_key_ref);
  JS_PopGCRef (ctx, &holder_ref);
  JS_PopGCRef (ctx, &result_ref);
  return result;
}

static const JSCFunctionListEntry js_wota_funcs[] = {
  JS_CFUNC_DEF ("encode", 2, js_wota_encode),
  JS_CFUNC_DEF ("decode", 2, js_wota_decode),
};

JSValue js_core_wota_use (JSContext *ctx) {
  JSGCRef exports_ref;
  JS_PushGCRef (ctx, &exports_ref);
  exports_ref.val = JS_NewObject (ctx);
  JS_SetPropertyFunctionList (ctx, exports_ref.val, js_wota_funcs, sizeof (js_wota_funcs) / sizeof (js_wota_funcs[0]));
  JSValue result = exports_ref.val;
  JS_PopGCRef (ctx, &exports_ref);
  return result;
}

static JSValue js_math_e (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double power = 1.0;
  if (argc > 0 && !JS_IsNull (argv[0])) {
    if (JS_ToFloat64 (ctx, &power, argv[0]) < 0) return JS_EXCEPTION;
  }
  return JS_NewFloat64 (ctx, exp (power));
}

/* ============================================================================
 * Cell Script Module: math/radians
 * Provides trigonometric and math functions using radians
 * ============================================================================
 */

static JSValue js_math_rad_arc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, acos (x));
}

static JSValue js_math_rad_arc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, asin (x));
}

static JSValue js_math_rad_arc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, atan (x));
}

static JSValue js_math_rad_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, cos (x));
}

static JSValue js_math_rad_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, sin (x));
}

static JSValue js_math_rad_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, tan (x));
}

static JSValue js_math_ln (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, log (x));
}

static JSValue js_math_log10 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, log10 (x));
}

static JSValue js_math_log2 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, log2 (x));
}

static JSValue js_math_power (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x, y;
  if (argc < 2) return JS_NULL;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  if (JS_ToFloat64 (ctx, &y, argv[1]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, pow (x, y));
}

static JSValue js_math_root (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x, n;
  if (argc < 2) return JS_NULL;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  if (JS_ToFloat64 (ctx, &n, argv[1]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, pow (x, 1.0 / n));
}

static JSValue js_math_sqrt (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, sqrt (x));
}

static const JSCFunctionListEntry js_math_radians_funcs[]
  = { JS_CFUNC_DEF ("arc_cosine", 1, js_math_rad_arc_cosine),
      JS_CFUNC_DEF ("arc_sine", 1, js_math_rad_arc_sine),
      JS_CFUNC_DEF ("arc_tangent", 1, js_math_rad_arc_tangent),
      JS_CFUNC_DEF ("cosine", 1, js_math_rad_cosine),
      JS_CFUNC_DEF ("sine", 1, js_math_rad_sine),
      JS_CFUNC_DEF ("tangent", 1, js_math_rad_tangent),
      JS_CFUNC_DEF ("ln", 1, js_math_ln),
      JS_CFUNC_DEF ("log", 1, js_math_log10),
      JS_CFUNC_DEF ("log2", 1, js_math_log2),
      JS_CFUNC_DEF ("power", 2, js_math_power),
      JS_CFUNC_DEF ("root", 2, js_math_root),
      JS_CFUNC_DEF ("sqrt", 1, js_math_sqrt),
      JS_CFUNC_DEF ("e", 1, js_math_e) };

JSValue js_core_math_radians_use (JSContext *ctx) {
  JSGCRef obj_ref;
  JS_PushGCRef (ctx, &obj_ref);
  obj_ref.val = JS_NewObject (ctx);
  JS_SetPropertyFunctionList (ctx, obj_ref.val, js_math_radians_funcs, countof (js_math_radians_funcs));
  JSValue result = obj_ref.val;
  JS_PopGCRef (ctx, &obj_ref);
  return result;
}

/* ============================================================================
 * Cell Script Module: math/degrees
 * Provides trigonometric and math functions using degrees
 * ============================================================================
 */

#define DEG2RAD (3.14159265358979323846264338327950288419716939937510 / 180.0)
#define RAD2DEG (180.0 / 3.14159265358979323846264338327950288419716939937510)

static JSValue js_math_deg_arc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, acos (x) * RAD2DEG);
}

static JSValue js_math_deg_arc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, asin (x) * RAD2DEG);
}

static JSValue js_math_deg_arc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, atan (x) * RAD2DEG);
}

static JSValue js_math_deg_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, cos (x * DEG2RAD));
}

static JSValue js_math_deg_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, sin (x * DEG2RAD));
}

static JSValue js_math_deg_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, tan (x * DEG2RAD));
}

static const JSCFunctionListEntry js_math_degrees_funcs[]
  = { JS_CFUNC_DEF ("arc_cosine", 1, js_math_deg_arc_cosine),
      JS_CFUNC_DEF ("arc_sine", 1, js_math_deg_arc_sine),
      JS_CFUNC_DEF ("arc_tangent", 1, js_math_deg_arc_tangent),
      JS_CFUNC_DEF ("cosine", 1, js_math_deg_cosine),
      JS_CFUNC_DEF ("sine", 1, js_math_deg_sine),
      JS_CFUNC_DEF ("tangent", 1, js_math_deg_tangent),
      JS_CFUNC_DEF ("ln", 1, js_math_ln),
      JS_CFUNC_DEF ("log", 1, js_math_log10),
      JS_CFUNC_DEF ("log2", 1, js_math_log2),
      JS_CFUNC_DEF ("power", 2, js_math_power),
      JS_CFUNC_DEF ("root", 2, js_math_root),
      JS_CFUNC_DEF ("sqrt", 1, js_math_sqrt),
      JS_CFUNC_DEF ("e", 1, js_math_e) };

JSValue js_core_math_degrees_use (JSContext *ctx) {
  JSGCRef obj_ref;
  JS_PushGCRef (ctx, &obj_ref);
  obj_ref.val = JS_NewObject (ctx);
  JS_SetPropertyFunctionList (ctx, obj_ref.val, js_math_degrees_funcs, countof (js_math_degrees_funcs));
  JSValue result = obj_ref.val;
  JS_PopGCRef (ctx, &obj_ref);
  return result;
}

/* ============================================================================
 * Cell Script Module: math/cycles
 * Provides trigonometric and math functions using cycles (0-1 = full rotation)
 * ============================================================================
 */

#define TWOPI (2.0 * 3.14159265358979323846264338327950288419716939937510)

static JSValue js_math_cyc_arc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, acos (x) / TWOPI);
}

static JSValue js_math_cyc_arc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, asin (x) / TWOPI);
}

static JSValue js_math_cyc_arc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, atan (x) / TWOPI);
}

static JSValue js_math_cyc_cosine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, cos (x * TWOPI));
}

static JSValue js_math_cyc_sine (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, sin (x * TWOPI));
}

static JSValue js_math_cyc_tangent (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
  double x;
  if (JS_ToFloat64 (ctx, &x, argv[0]) < 0) return JS_EXCEPTION;
  return JS_NewFloat64 (ctx, tan (x * TWOPI));
}

static const JSCFunctionListEntry js_math_cycles_funcs[]
  = { JS_CFUNC_DEF ("arc_cosine", 1, js_math_cyc_arc_cosine),
      JS_CFUNC_DEF ("arc_sine", 1, js_math_cyc_arc_sine),
      JS_CFUNC_DEF ("arc_tangent", 1, js_math_cyc_arc_tangent),
      JS_CFUNC_DEF ("cosine", 1, js_math_cyc_cosine),
      JS_CFUNC_DEF ("sine", 1, js_math_cyc_sine),
      JS_CFUNC_DEF ("tangent", 1, js_math_cyc_tangent),
      JS_CFUNC_DEF ("ln", 1, js_math_ln),
      JS_CFUNC_DEF ("log", 1, js_math_log10),
      JS_CFUNC_DEF ("log2", 1, js_math_log2),
      JS_CFUNC_DEF ("power", 2, js_math_power),
      JS_CFUNC_DEF ("root", 2, js_math_root),
      JS_CFUNC_DEF ("sqrt", 1, js_math_sqrt),
      JS_CFUNC_DEF ("e", 1, js_math_e) };

JSValue js_core_math_cycles_use (JSContext *ctx) {
  JSGCRef obj_ref;
  JS_PushGCRef (ctx, &obj_ref);
  obj_ref.val = JS_NewObject (ctx);
  JS_SetPropertyFunctionList (ctx, obj_ref.val, js_math_cycles_funcs, countof (js_math_cycles_funcs));
  JSValue result = obj_ref.val;
  JS_PopGCRef (ctx, &obj_ref);
  return result;
}
/* Public API: get stack trace as JS array of {fn, file, line, col} objects.
   Does NOT clear reg_current_frame — caller is responsible if needed.
   Two-phase approach for GC safety: Phase 1 collects raw C data on the C stack
   (pointers into stable JSCodeRegister fields), Phase 2 allocates JS objects. */
JSValue JS_GetStack(JSContext *ctx) {
  if (JS_IsNull(ctx->reg_current_frame)) return JS_NewArray(ctx);

  /* Phase 1: collect raw frame data (no JS allocations) */
  struct { const char *fn; const char *file; uint16_t line, col; } frames[64];
  int count = 0;
  JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(ctx->reg_current_frame);
  uint32_t cur_pc = ctx->current_register_pc;
  int is_first = 1;

  while (frame && count < 64) {
    if (!JS_IsFunction(frame->function)) break;
    JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);

    if (fn->kind == JS_FUNC_KIND_REGISTER && fn->u.reg.code) {
      JSCodeRegister *code = fn->u.reg.code;
      uint32_t pc = is_first ? cur_pc : (uint32_t)(JS_VALUE_GET_INT(frame->address) >> 16);
      frames[count].fn = code->name_cstr;
      frames[count].file = code->filename_cstr;
      frames[count].line = 0;
      frames[count].col = 0;
      if (code->line_table && pc < code->instr_count) {
        frames[count].line = code->line_table[pc].line;
        frames[count].col = code->line_table[pc].col;
      }
      count++;
    }

    if (JS_IsNull(frame->caller)) break;
    frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller);
    is_first = 0;
  }

  /* Phase 2: create JS objects (frame data is on C stack, safe across GC) */
  JSGCRef arr_ref;
  JS_PushGCRef(ctx, &arr_ref);
  arr_ref.val = JS_NewArray(ctx);
  for (int i = 0; i < count; i++) {
    JSGCRef item_ref;
    JS_PushGCRef(ctx, &item_ref);
    item_ref.val = JS_NewObject(ctx);
    JS_SetPropertyStr(ctx, item_ref.val, "fn",
      JS_NewString(ctx, frames[i].fn ? frames[i].fn : "<anonymous>"));
    JS_SetPropertyStr(ctx, item_ref.val, "file",
      JS_NewString(ctx, frames[i].file ? frames[i].file : "<unknown>"));
    JS_SetPropertyStr(ctx, item_ref.val, "line", JS_NewInt32(ctx, frames[i].line));
    JS_SetPropertyStr(ctx, item_ref.val, "col", JS_NewInt32(ctx, frames[i].col));
    JS_SetPropertyNumber(ctx, arr_ref.val, i, item_ref.val);
    JS_PopGCRef(ctx, &item_ref);
  }
  JSValue result = arr_ref.val;
  JS_PopGCRef(ctx, &arr_ref);
  return result;
}
