#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include "ant.h"
#include "gc.h"
#include "errors.h"
#include "arena.h"
#include "runtime.h"
#include "internal.h"
#include "silver/engine.h"
#include "descriptors.h"

#include "modules/collections.h"
#include "modules/symbol.h"

static map_registry_entry_t *map_registry = NULL;
static size_t map_registry_count = 0;
static size_t map_registry_cap = 0;

static set_registry_entry_t *set_registry = NULL;
static size_t set_registry_count = 0;
static size_t set_registry_cap = 0;

static void register_map(map_entry_t **head, jsoff_t obj_offset) {
  if (!head) return;
  if (map_registry_count >= map_registry_cap) {
    size_t new_cap = map_registry_cap ? map_registry_cap * 2 : 64;
    map_registry_entry_t *new_reg = realloc(map_registry, new_cap * sizeof(map_registry_entry_t));
    if (!new_reg) return;
    map_registry = new_reg;
    map_registry_cap = new_cap;
  }
  map_registry[map_registry_count].head = head;
  map_registry[map_registry_count].obj_offset = obj_offset;
  map_registry_count++;
}

static void register_set(set_entry_t **head, jsoff_t obj_offset) {
  if (!head) return;
  if (set_registry_count >= set_registry_cap) {
    size_t new_cap = set_registry_cap ? set_registry_cap * 2 : 64;
    set_registry_entry_t *new_reg = realloc(set_registry, new_cap * sizeof(set_registry_entry_t));
    if (!new_reg) return;
    set_registry = new_reg;
    set_registry_cap = new_cap;
  }
  set_registry[set_registry_count].head = head;
  set_registry[set_registry_count].obj_offset = obj_offset;
  set_registry_count++;
}

static const char *jsval_to_key(ant_t *js, jsval_t val) {
  if (vtype(val) == T_STR) {
    jsoff_t len;
    jsoff_t off = vstr(js, val, &len);
    return (char *)&js->mem[off];
  }
  return js_str(js, val);
}

map_entry_t **get_map_from_obj(ant_t *js, jsval_t obj) {
  jsval_t map_val = js_get_slot(js, obj, SLOT_MAP);
  if (vtype(map_val) == T_UNDEF) return NULL;
  return (map_entry_t **)(uintptr_t)js_getnum(map_val);
}

set_entry_t **get_set_from_obj(ant_t *js, jsval_t obj) {
  jsval_t set_val = js_get_slot(js, obj, SLOT_SET);
  if (vtype(set_val) == T_UNDEF) return NULL;
  return (set_entry_t **)(uintptr_t)js_getnum(set_val);
}

static weakmap_entry_t **get_weakmap_from_obj(ant_t *js, jsval_t obj) {
  jsval_t wm_val = js_get_slot(js, obj, SLOT_DATA);
  if (vtype(wm_val) == T_UNDEF) return NULL;
  return (weakmap_entry_t **)(uintptr_t)js_getnum(wm_val);
}

static weakset_entry_t **get_weakset_from_obj(ant_t *js, jsval_t obj) {
  jsval_t ws_val = js_get_slot(js, obj, SLOT_DATA);
  if (vtype(ws_val) == T_UNDEF) return NULL;
  return (weakset_entry_t **)(uintptr_t)js_getnum(ws_val);
}

static map_iterator_state_t *get_map_iter_state(ant_t *js, jsval_t obj) {
  jsval_t state_val = js_get_slot(js, obj, SLOT_ITER_STATE);
  if (vtype(state_val) == T_UNDEF) return NULL;
  return (map_iterator_state_t *)(uintptr_t)js_getnum(state_val);
}

static set_iterator_state_t *get_set_iter_state(ant_t *js, jsval_t obj) {
  jsval_t state_val = js_get_slot(js, obj, SLOT_ITER_STATE);
  if (vtype(state_val) == T_UNDEF) return NULL;
  return (set_iterator_state_t *)(uintptr_t)js_getnum(state_val);
}

static jsval_t map_set(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "Map.set() requires 2 arguments");
  
  jsval_t this_val = js->this_val;
  map_entry_t **map_ptr = get_map_from_obj(js, this_val);
  if (!map_ptr) return js_mkerr(js, "Invalid Map object");
  const char *key_str = jsval_to_key(js, args[0]);
  
  map_entry_t *entry;
  HASH_FIND_STR(*map_ptr, key_str, entry);
  if (entry) {
    entry->value = args[1];
  } else {
    entry = ant_calloc(sizeof(map_entry_t));
    if (!entry) return js_mkerr(js, "out of memory");
    entry->key = strdup(key_str);
    entry->value = args[1];
    HASH_ADD_STR(*map_ptr, key, entry);
  }
  
  return this_val;
}

static jsval_t map_get(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "Map.get() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  map_entry_t **map_ptr = get_map_from_obj(js, this_val);
  if (!map_ptr) return js_mkundef();
  
  const char *key_str = jsval_to_key(js, args[0]);
  
  map_entry_t *entry;
  HASH_FIND_STR(*map_ptr, key_str, entry);
  return entry ? entry->value : js_mkundef();
}

static jsval_t map_has(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "Map.has() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  map_entry_t **map_ptr = get_map_from_obj(js, this_val);
  
  if (!map_ptr) return js_false;
  const char *key_str = jsval_to_key(js, args[0]);
  
  map_entry_t *entry;
  HASH_FIND_STR(*map_ptr, key_str, entry);
  return js_bool(entry != NULL);
}

static jsval_t map_delete(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "Map.delete() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  map_entry_t **map_ptr = get_map_from_obj(js, this_val);
  
  if (!map_ptr) return js_false;
  const char *key_str = jsval_to_key(js, args[0]);
  
  map_entry_t *entry;
  HASH_FIND_STR(*map_ptr, key_str, entry);
  if (entry) {
    HASH_DEL(*map_ptr, entry);
    free(entry->key);
    free(entry);
    return js_true;
  }
  return js_false;
}

static jsval_t map_clear(ant_t *js, jsval_t *args, int nargs) {
  jsval_t this_val = js->this_val;
  map_entry_t **map_ptr = get_map_from_obj(js, this_val);
  if (!map_ptr) return js_mkundef();
  
  map_entry_t *entry, *tmp;
  HASH_ITER(hh, *map_ptr, entry, tmp) {
    HASH_DEL(*map_ptr, entry);
    free(entry->key);
    free(entry);
  }
  *map_ptr = NULL;
  
  return js_mkundef();
}

static jsval_t map_size(ant_t *js, jsval_t *args, int nargs) {
  jsval_t this_val = js->this_val;
  map_entry_t **map_ptr = get_map_from_obj(js, this_val);
  if (!map_ptr) return js_mknum(0);
  
  return js_mknum((double)HASH_COUNT(*map_ptr));
}

static jsval_t map_forEach(ant_t *js, jsval_t *args, int nargs) {
  jsval_t this_val = js->this_val;
  map_entry_t **map_ptr = get_map_from_obj(js, this_val);
  
  if (nargs < 1 || vtype(args[0]) != T_FUNC)
    return js_mkerr(js, "forEach requires a callback function");
  
  jsval_t callback = args[0];
  
  if (map_ptr && *map_ptr) {
    map_entry_t *entry, *tmp;
    HASH_ITER(hh, *map_ptr, entry, tmp) {
      jsval_t k = js_mkstr(js, entry->key, strlen(entry->key));
      jsval_t call_args[3] = { entry->value, k, this_val };
      jsval_t result = sv_vm_call(js->vm, js, callback, js_mkundef(), call_args, 3, NULL, false);
      if (is_err(result)) return result;
    }
  }
  
  return js_mkundef();
}

static jsval_t map_iter_next(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  jsval_t this_val = js->this_val;
  
  map_iterator_state_t *state = get_map_iter_state(js, this_val);
  if (!state) return js_mkerr(js, "Invalid iterator");
  
  jsval_t result = js_mkobj(js);
  
  if (!state->current) {
    js_set(js, result, "done", js_true);
    js_set(js, result, "value", js_mkundef());
    return result;
  }
  
  map_entry_t *entry = state->current;
  jsval_t value;
  
  switch (state->type) {
    case ITER_TYPE_MAP_VALUES:
      value = entry->value;
      break;
    case ITER_TYPE_MAP_KEYS:
      value = js_mkstr(js, entry->key, strlen(entry->key));
      break;
    case ITER_TYPE_MAP_ENTRIES: {
      jsval_t pair = js_mkarr(js);
      js_arr_push(js, pair, js_mkstr(js, entry->key, strlen(entry->key)));
      js_arr_push(js, pair, entry->value);
      value = pair;
      break;
    }
    default:
      value = js_mkundef();
  }
  
  state->current = entry->hh.next;
  
  js_set(js, result, "value", value);
  js_set(js, result, "done", js_false);
  return result;
}

static jsval_t create_map_iterator(ant_t *js, jsval_t map_obj, iter_type_t type) {
  map_entry_t **map_ptr = get_map_from_obj(js, map_obj);
  
  map_iterator_state_t *state = ant_calloc(sizeof(map_iterator_state_t));
  if (!state) return js_mkerr(js, "out of memory");
  
  state->head = map_ptr;
  state->current = map_ptr ? *map_ptr : NULL;
  state->type = type;
  
  jsval_t iter = js_mkobj(js);
  js_set_slot(js, iter, SLOT_ITER_STATE, ANT_PTR(state));
  js_set(js, iter, "next", js_mkfun(map_iter_next));
  
  js_set_sym(js, iter, get_iterator_sym(), js_mkfun(sym_this_cb));
  
  return iter;
}

static jsval_t map_values(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  return create_map_iterator(js, js->this_val, ITER_TYPE_MAP_VALUES);
}

static jsval_t map_keys(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  return create_map_iterator(js, js->this_val, ITER_TYPE_MAP_KEYS);
}

static jsval_t map_entries(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  return create_map_iterator(js, js->this_val, ITER_TYPE_MAP_ENTRIES);
}

static jsval_t map_iterator(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  return create_map_iterator(js, js->this_val, ITER_TYPE_MAP_ENTRIES);
}

static jsval_t set_iter_next(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  jsval_t this_val = js->this_val;
  
  set_iterator_state_t *state = get_set_iter_state(js, this_val);
  if (!state) return js_mkerr(js, "Invalid iterator");
  
  jsval_t result = js_mkobj(js);
  
  if (!state->current) {
    js_set(js, result, "done", js_true);
    js_set(js, result, "value", js_mkundef());
    return result;
  }
  
  set_entry_t *entry = state->current;
  jsval_t value;
  
  if (state->type == ITER_TYPE_SET_ENTRIES) {
    jsval_t pair = js_mkarr(js);
    js_arr_push(js, pair, entry->value);
    js_arr_push(js, pair, entry->value);
    value = pair;
  } else {
    value = entry->value;
  }
  
  state->current = entry->hh.next;
  
  js_set(js, result, "value", value);
  js_set(js, result, "done", js_false);
  return result;
}

static jsval_t create_set_iterator(ant_t *js, jsval_t set_obj, iter_type_t type) {
  set_entry_t **set_ptr = get_set_from_obj(js, set_obj);
  
  set_iterator_state_t *state = ant_calloc(sizeof(set_iterator_state_t));
  if (!state) return js_mkerr(js, "out of memory");
  
  state->head = set_ptr;
  state->current = set_ptr ? *set_ptr : NULL;
  state->type = type;
  
  jsval_t iter = js_mkobj(js);
  js_set_slot(js, iter, SLOT_ITER_STATE, ANT_PTR(state));
  js_set(js, iter, "next", js_mkfun(set_iter_next));
  
  js_set_sym(js, iter, get_iterator_sym(), js_mkfun(sym_this_cb));
  
  return iter;
}

static jsval_t set_add(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "Set.add() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  set_entry_t **set_ptr = get_set_from_obj(js, this_val);
  if (!set_ptr) return js_mkerr(js, "Invalid Set object");
  
  const char *key_str = jsval_to_key(js, args[0]);
  
  set_entry_t *entry;
  HASH_FIND_STR(*set_ptr, key_str, entry);
  
  if (!entry) {
    entry = ant_calloc(sizeof(set_entry_t));
    if (!entry) return js_mkerr(js, "out of memory");
    entry->value = args[0];
    entry->key = strdup(key_str);
    HASH_ADD_KEYPTR(hh, *set_ptr, entry->key, strlen(entry->key), entry);
  }
  
  return this_val;
}

static jsval_t set_has(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "Set.has() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  set_entry_t **set_ptr = get_set_from_obj(js, this_val);
  if (!set_ptr) return js_false;
  
  const char *key_str = jsval_to_key(js, args[0]);
  
  set_entry_t *entry;
  HASH_FIND_STR(*set_ptr, key_str, entry);
  return js_bool(entry != NULL);
}

static jsval_t set_delete(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "Set.delete() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  set_entry_t **set_ptr = get_set_from_obj(js, this_val);
  if (!set_ptr) return js_false;
  
  const char *key_str = jsval_to_key(js, args[0]);
  set_entry_t *entry;
  HASH_FIND_STR(*set_ptr, key_str, entry);
  
  if (entry) {
    HASH_DEL(*set_ptr, entry);
    free(entry->key);
    free(entry);
    return js_true;
  }
  return js_false;
}

static jsval_t set_clear(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  jsval_t this_val = js->this_val;
  set_entry_t **set_ptr = get_set_from_obj(js, this_val);
  if (!set_ptr) return js_mkundef();
  
  set_entry_t *entry, *tmp;
  HASH_ITER(hh, *set_ptr, entry, tmp) {
    HASH_DEL(*set_ptr, entry);
    free(entry->key);
    free(entry);
  }
  *set_ptr = NULL;
  
  return js_mkundef();
}

static jsval_t set_size(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  jsval_t this_val = js->this_val;
  set_entry_t **set_ptr = get_set_from_obj(js, this_val);
  if (!set_ptr) return js_mknum(0);
  
  return js_mknum((double)HASH_COUNT(*set_ptr));
}

static jsval_t set_values(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  return create_set_iterator(js, js->this_val, ITER_TYPE_SET_VALUES);
}

static jsval_t set_entries(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  return create_set_iterator(js, js->this_val, ITER_TYPE_SET_ENTRIES);
}

static jsval_t set_iterator(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  return create_set_iterator(js, js->this_val, ITER_TYPE_SET_VALUES);
}

static jsval_t set_forEach(ant_t *js, jsval_t *args, int nargs) {
  jsval_t this_val = js->this_val;
  set_entry_t **set_ptr = get_set_from_obj(js, this_val);
  
  if (nargs < 1 || vtype(args[0]) != T_FUNC)
    return js_mkerr(js, "forEach requires a callback function");
  
  jsval_t callback = args[0];
  
  if (set_ptr && *set_ptr) {
    set_entry_t *entry, *tmp;
    HASH_ITER(hh, *set_ptr, entry, tmp) {
      jsval_t call_args[3] = { entry->value, entry->value, this_val };
      jsval_t result = sv_vm_call(js->vm, js, callback, js_mkundef(), call_args, 3, NULL, false);
      if (is_err(result)) return result;
    }
  }
  
  return js_mkundef();
}

static jsval_t weakmap_set(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "WeakMap.set() requires 2 arguments");
  
  jsval_t this_val = js->this_val;
  weakmap_entry_t **wm_ptr = get_weakmap_from_obj(js, this_val);
  if (!wm_ptr) return js_mkerr(js, "Invalid WeakMap object");
  
  if (vtype(args[0]) != T_OBJ)
    return js_mkerr(js, "WeakMap key must be an object");
  
  jsval_t key_obj = args[0];
  
  weakmap_entry_t *entry;
  HASH_FIND(hh, *wm_ptr, &key_obj, sizeof(jsval_t), entry);
  if (entry) {
    entry->value = args[1];
  } else {
    entry = ant_calloc(sizeof(weakmap_entry_t));
    if (!entry) return js_mkerr(js, "out of memory");
    entry->key_obj = key_obj;
    entry->value = args[1];
    HASH_ADD(hh, *wm_ptr, key_obj, sizeof(jsval_t), entry);
  }
  
  return this_val;
}

static jsval_t weakmap_get(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "WeakMap.get() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  weakmap_entry_t **wm_ptr = get_weakmap_from_obj(js, this_val);
  if (!wm_ptr) return js_mkundef();
  
  if (vtype(args[0]) != T_OBJ) return js_mkundef();
  
  jsval_t key_obj = args[0];
  weakmap_entry_t *entry;
  HASH_FIND(hh, *wm_ptr, &key_obj, sizeof(jsval_t), entry);
  return entry ? entry->value : js_mkundef();
}

static jsval_t weakmap_has(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "WeakMap.has() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  weakmap_entry_t **wm_ptr = get_weakmap_from_obj(js, this_val);
  if (!wm_ptr) return js_false;
  
  if (vtype(args[0]) != T_OBJ) return js_false;
  
  jsval_t key_obj = args[0];
  weakmap_entry_t *entry;
  HASH_FIND(hh, *wm_ptr, &key_obj, sizeof(jsval_t), entry);
  return js_bool(entry != NULL);
}

static jsval_t weakmap_delete(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "WeakMap.delete() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  weakmap_entry_t **wm_ptr = get_weakmap_from_obj(js, this_val);
  if (!wm_ptr) return js_false;
  
  if (vtype(args[0]) != T_OBJ) return js_false;
  
  jsval_t key_obj = args[0];
  weakmap_entry_t *entry;
  HASH_FIND(hh, *wm_ptr, &key_obj, sizeof(jsval_t), entry);
  if (entry) {
    HASH_DEL(*wm_ptr, entry);
    free(entry);
    return js_true;
  }
  return js_false;
}

static jsval_t weakset_add(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "WeakSet.add() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  weakset_entry_t **ws_ptr = get_weakset_from_obj(js, this_val);
  if (!ws_ptr) return js_mkerr(js, "Invalid WeakSet object");
  
  if (vtype(args[0]) != T_OBJ)
    return js_mkerr(js, "WeakSet value must be an object");
  
  jsval_t value_obj = args[0];
  
  weakset_entry_t *entry;
  HASH_FIND(hh, *ws_ptr, &value_obj, sizeof(jsval_t), entry);
  
  if (!entry) {
    entry = ant_calloc(sizeof(weakset_entry_t));
    if (!entry) return js_mkerr(js, "out of memory");
    entry->value_obj = value_obj;
    HASH_ADD(hh, *ws_ptr, value_obj, sizeof(jsval_t), entry);
  }
  
  return this_val;
}

static jsval_t weakset_has(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "WeakSet.has() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  weakset_entry_t **ws_ptr = get_weakset_from_obj(js, this_val);
  if (!ws_ptr) return js_false;
  
  if (vtype(args[0]) != T_OBJ) return js_false;
  
  jsval_t value_obj = args[0];
  weakset_entry_t *entry;
  HASH_FIND(hh, *ws_ptr, &value_obj, sizeof(jsval_t), entry);
  return js_bool(entry != NULL);
}

static jsval_t weakset_delete(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "WeakSet.delete() requires 1 argument");
  
  jsval_t this_val = js->this_val;
  weakset_entry_t **ws_ptr = get_weakset_from_obj(js, this_val);
  if (!ws_ptr) return js_false;
  
  if (vtype(args[0]) != T_OBJ) return js_false;
  
  jsval_t value_obj = args[0];
  weakset_entry_t *entry;
  HASH_FIND(hh, *ws_ptr, &value_obj, sizeof(jsval_t), entry);
  
  if (entry) {
    HASH_DEL(*ws_ptr, entry);
    free(entry);
    return js_true;
  }
  return js_false;
}

static jsval_t builtin_WeakRef(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1 || vtype(args[0]) != T_OBJ) {
    return js_mkerr(js, "WeakRef target must be an object");
  }
  
  jsval_t wr_obj = js_mkobj(js);
  jsval_t wr_proto = js_get_ctor_proto(js, "WeakRef", 7);
  if (is_special_object(wr_proto)) js_set_proto(js, wr_obj, wr_proto);
  js_set_slot(js, wr_obj, SLOT_DATA, args[0]);
  
  return wr_obj;
}

static jsval_t weakref_deref(ant_t *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  jsval_t this_val = js->this_val;
  if (vtype(this_val) != T_OBJ) return js_mkundef();
  
  jsval_t target = js_get_slot(js, this_val, SLOT_DATA);
  if (vtype(target) != T_OBJ) return js_mkundef();
  
  return target;
}

static jsval_t builtin_FinalizationRegistry(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 1 || (vtype(args[0]) != T_FUNC && vtype(args[0]) != T_CFUNC)) {
    return js_mkerr(js, "FinalizationRegistry callback must be a function");
  }
  
  jsval_t fr_obj = js_mkobj(js);
  jsval_t fr_proto = js_get_ctor_proto(js, "FinalizationRegistry", 20);
  if (is_special_object(fr_proto)) js_set_proto(js, fr_obj, fr_proto);
  
  js_set_slot(js, fr_obj, SLOT_MAP, mkarr(js));
  js_set_slot(js, fr_obj, SLOT_DATA, args[0]);
  
  return fr_obj;
}

static jsval_t finreg_register(ant_t *js, jsval_t *args, int nargs) {
  jsval_t this_val = js->this_val;
  if (vtype(this_val) != T_OBJ) return js_mkundef();
  
  if (nargs < 1 || vtype(args[0]) != T_OBJ) {
    return js_mkerr(js, "FinalizationRegistry.register target must be an object");
  }
  
  jsval_t target = args[0];
  jsval_t held_value = nargs > 1 ? args[1] : js_mkundef();
  jsval_t unregister_token = nargs > 2 ? args[2] : js_mkundef();
  
  if (vdata(target) == vdata(held_value) && vtype(held_value) == T_OBJ) {
    return js_mkerr(js, "target and held value must not be the same");
  }
  
  jsval_t registrations = js_get_slot(js, this_val, SLOT_MAP);
  if (vtype(registrations) != T_ARR) return js_mkundef();
  
  jsval_t entry = mkarr(js);
  jsoff_t len = js_arr_len(js, registrations);
  
  char idx[16];
  size_t idx_len = uint_to_str(idx, sizeof(idx), 0);
  js_setprop(js, entry, js_mkstr(js, idx, idx_len), target);
  idx_len = uint_to_str(idx, sizeof(idx), 1);
  js_setprop(js, entry, js_mkstr(js, idx, idx_len), held_value);
  idx_len = uint_to_str(idx, sizeof(idx), 2);
  js_setprop(js, entry, js_mkstr(js, idx, idx_len), unregister_token);
  js_setprop(js, entry, js->length_str, tov(3.0));
  
  idx_len = uint_to_str(idx, sizeof(idx), len);
  js_setprop(js, registrations, js_mkstr(js, idx, idx_len), entry);
  js_setprop(js, registrations, js->length_str, tov((double)(len + 1)));
  
  return js_mkundef();
}

static jsval_t finreg_unregister(ant_t *js, jsval_t *args, int nargs) {
  jsval_t this_val = js->this_val;
  if (vtype(this_val) != T_OBJ) return js_false;
  
  if (nargs < 1 || vtype(args[0]) != T_OBJ) {
    return js_mkerr(js, "FinalizationRegistry.unregister token must be an object");
  }
  
  jsval_t token = args[0];
  jsval_t registrations = js_get_slot(js, this_val, SLOT_MAP);
  if (vtype(registrations) != T_ARR) return js_false;
  
  jsoff_t len = js_arr_len(js, registrations);
  bool removed = false;
  
  for (jsoff_t i = 0; i < len; i++) {
    jsval_t entry = js_arr_get(js, registrations, i);
    if (vtype(entry) != T_ARR) continue;
    jsval_t entry_token = js_arr_get(js, entry, 2);
    if (vtype(entry_token) == T_OBJ && vdata(entry_token) == vdata(token)) {
      char idx[16];
      size_t idx_len = uint_to_str(idx, sizeof(idx), i);
      js_setprop(js, registrations, js_mkstr(js, idx, idx_len), js_mkundef());
      removed = true;
    }
  }
  
  return js_bool(removed);
}

static jsval_t map_groupBy(ant_t *js, jsval_t *args, int nargs) {
  if (nargs < 2) return js_mkerr_typed(js, JS_ERR_TYPE, "Map.groupBy requires 2 arguments");
  
  jsval_t items = args[0];
  jsval_t callback = args[1];
  
  if (vtype(callback) != T_FUNC && vtype(callback) != T_CFUNC)
    return js_mkerr_typed(js, JS_ERR_TYPE, "callback is not a function");
  
  jsval_t map_obj = js_mkobj(js);
  jsoff_t obj_offset = (jsoff_t)vdata(map_obj);
  
  jsval_t map_proto = js_get_ctor_proto(js, "Map", 3);
  if (is_special_object(map_proto)) js_set_proto(js, map_obj, map_proto);
  
  map_entry_t **map_head = ant_calloc(sizeof(map_entry_t *));
  if (!map_head) return js_mkerr(js, "out of memory");
  *map_head = NULL;
  register_map(map_head, obj_offset);
  js_set_slot(js, map_obj, SLOT_MAP, ANT_PTR(map_head));
  
  jsoff_t len = js_arr_len(js, items);
  for (jsoff_t i = 0; i < len; i++) {
    jsval_t val = js_arr_get(js, items, i);
    jsval_t cb_args[2] = { val, tov((double)i) };
    jsval_t key = sv_vm_call(js->vm, js, callback, js_mkundef(), cb_args, 2, NULL, false);
    if (is_err(key)) return key;
    
    const char *key_str = jsval_to_key(js, key);
    
    map_entry_t *entry;
    HASH_FIND_STR(*map_head, key_str, entry);
    jsval_t group;
    if (entry) {
      group = entry->value;
    } else {
      group = js_mkarr(js);
      entry = ant_calloc(sizeof(map_entry_t));
      if (!entry) return js_mkerr(js, "out of memory");
      entry->key = strdup(key_str);
      entry->value = group;
      HASH_ADD_STR(*map_head, key, entry);
    }
    js_arr_push(js, group, val);
  }
  
  return map_obj;
}

static jsval_t builtin_Map(ant_t *js, jsval_t *args, int nargs) {
  if (vtype(js->new_target) == T_UNDEF) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "Map constructor requires 'new'");
  }
  
  jsval_t map_obj = js->this_val;
  if (vtype(map_obj) != T_OBJ) map_obj = js_mkobj(js);
  if (is_err(map_obj)) return map_obj;
  jsoff_t obj_offset = (jsoff_t)vdata(map_obj);
  
  jsval_t map_proto = js_get_ctor_proto(js, "Map", 3);
  jsval_t instance_proto = js_instance_proto_from_new_target(js, map_proto);
  
  if (is_special_object(instance_proto)) js_set_proto(js, map_obj, instance_proto);
  
  map_entry_t **map_head = ant_calloc(sizeof(map_entry_t *));
  if (!map_head) return js_mkerr(js, "out of memory");
  *map_head = NULL;
  
  register_map(map_head, obj_offset);
  if (vtype(js->new_target) == T_FUNC || vtype(js->new_target) == T_CFUNC) {
    js_set_slot(js, map_obj, SLOT_CTOR, js->new_target);
  }
  js_set_slot(js, map_obj, SLOT_MAP, ANT_PTR(map_head));
  
  if (nargs == 0 || vtype(args[0]) != T_ARR) return map_obj;
  
  jsval_t iterable = args[0];
  jsoff_t length = js_arr_len(js, iterable);
  
  for (jsoff_t i = 0; i < length; i++) {
    jsval_t entry = js_arr_get(js, iterable, i);
    if (vtype(entry) != T_ARR) continue;
    
    jsoff_t entry_len = js_arr_len(js, entry);
    if (entry_len < 2) continue;
    
    jsval_t key = js_arr_get(js, entry, 0);
    jsval_t value = js_arr_get(js, entry, 1);
    const char *key_str = jsval_to_key(js, key);
    
    map_entry_t *map_entry;
    HASH_FIND_STR(*map_head, key_str, map_entry);
    if (map_entry) {
      map_entry->value = value;
      continue;
    }
    
    map_entry = ant_calloc(sizeof(map_entry_t));
    if (!map_entry) return js_mkerr(js, "out of memory");
    map_entry->key = strdup(key_str);
    map_entry->value = value;
    HASH_ADD_STR(*map_head, key, map_entry);
  }
  
  return map_obj;
}

static jsval_t builtin_Set(ant_t *js, jsval_t *args, int nargs) {
  if (vtype(js->new_target) == T_UNDEF) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "Set constructor requires 'new'");
  }
  
  jsval_t set_obj = js->this_val;
  if (vtype(set_obj) != T_OBJ) set_obj = js_mkobj(js);
  if (is_err(set_obj)) return set_obj;
  jsoff_t obj_offset = (jsoff_t)vdata(set_obj);
  
  jsval_t set_proto = js_get_ctor_proto(js, "Set", 3);
  jsval_t instance_proto = js_instance_proto_from_new_target(js, set_proto);
  
  if (is_special_object(instance_proto)) js_set_proto(js, set_obj, instance_proto);
  
  set_entry_t **set_head = ant_calloc(sizeof(set_entry_t *));
  if (!set_head) return js_mkerr(js, "out of memory");
  *set_head = NULL;
  
  register_set(set_head, obj_offset);
  if (vtype(js->new_target) == T_FUNC || vtype(js->new_target) == T_CFUNC) {
    js_set_slot(js, set_obj, SLOT_CTOR, js->new_target);
  }
  js_set_slot(js, set_obj, SLOT_SET, ANT_PTR(set_head));
  
  if (nargs == 0 || vtype(args[0]) != T_ARR) return set_obj;
  
  jsval_t iterable = args[0];
  jsoff_t length = js_arr_len(js, iterable);
  
  for (jsoff_t i = 0; i < length; i++) {
    jsval_t value = js_arr_get(js, iterable, i);
    const char *key_str = jsval_to_key(js, value);
    
    set_entry_t *entry;
    HASH_FIND_STR(*set_head, key_str, entry);
    if (entry) continue;
    
    entry = ant_calloc(sizeof(set_entry_t));
    if (!entry) return js_mkerr(js, "out of memory");
    entry->value = value;
    entry->key = strdup(key_str);
    HASH_ADD_KEYPTR(hh, *set_head, entry->key, strlen(entry->key), entry);
  }
  
  return set_obj;
}

static jsval_t builtin_WeakMap(ant_t *js, jsval_t *args, int nargs) {
  if (vtype(js->new_target) == T_UNDEF) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "WeakMap constructor requires 'new'");
  }
  
  jsval_t wm_obj = js->this_val;
  if (vtype(wm_obj) != T_OBJ) wm_obj = js_mkobj(js);
  if (is_err(wm_obj)) return wm_obj;
  
  jsval_t wm_proto = js_get_ctor_proto(js, "WeakMap", 7);
  jsval_t instance_proto = js_instance_proto_from_new_target(js, wm_proto);
  
  if (is_special_object(instance_proto)) js_set_proto(js, wm_obj, instance_proto);
  
  weakmap_entry_t **wm_head = ant_calloc(sizeof(weakmap_entry_t *));
  if (!wm_head) return js_mkerr(js, "out of memory");
  *wm_head = NULL;
  
  if (vtype(js->new_target) == T_FUNC || vtype(js->new_target) == T_CFUNC) {
    js_set_slot(js, wm_obj, SLOT_CTOR, js->new_target);
  }
  js_set_slot(js, wm_obj, SLOT_DATA, ANT_PTR(wm_head));
  
  if (nargs == 0 || vtype(args[0]) != T_ARR) return wm_obj;
  
  jsval_t iterable = args[0];
  jsoff_t length = js_arr_len(js, iterable);
  
  for (jsoff_t i = 0; i < length; i++) {
    jsval_t entry = js_arr_get(js, iterable, i);
    if (vtype(entry) != T_ARR) continue;
    
    jsoff_t entry_len = js_arr_len(js, entry);
    if (entry_len < 2) continue;
    
    jsval_t key = js_arr_get(js, entry, 0);
    jsval_t value = js_arr_get(js, entry, 1);
    
    if (vtype(key) != T_OBJ) return js_mkerr(js, "WeakMap key must be an object");
    
    weakmap_entry_t *wm_entry;
    HASH_FIND(hh, *wm_head, &key, sizeof(jsval_t), wm_entry);
    if (wm_entry) {
      wm_entry->value = value;
      continue;
    }
    
    wm_entry = ant_calloc(sizeof(weakmap_entry_t));
    if (!wm_entry) return js_mkerr(js, "out of memory");
    wm_entry->key_obj = key;
    wm_entry->value = value;
    HASH_ADD(hh, *wm_head, key_obj, sizeof(jsval_t), wm_entry);
  }
  
  return wm_obj;
}

static jsval_t builtin_WeakSet(ant_t *js, jsval_t *args, int nargs) {
  if (vtype(js->new_target) == T_UNDEF) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "WeakSet constructor requires 'new'");
  }
  
  jsval_t ws_obj = js->this_val;
  if (vtype(ws_obj) != T_OBJ) ws_obj = js_mkobj(js);
  if (is_err(ws_obj)) return ws_obj;
  
  jsval_t ws_proto = js_get_ctor_proto(js, "WeakSet", 7);
  jsval_t instance_proto = js_instance_proto_from_new_target(js, ws_proto);
  
  if (is_special_object(instance_proto)) js_set_proto(js, ws_obj, instance_proto);
  
  weakset_entry_t **ws_head = ant_calloc(sizeof(weakset_entry_t *));
  if (!ws_head) return js_mkerr(js, "out of memory");
  *ws_head = NULL;
  
  if (vtype(js->new_target) == T_FUNC || vtype(js->new_target) == T_CFUNC) {
    js_set_slot(js, ws_obj, SLOT_CTOR, js->new_target);
  }
  js_set_slot(js, ws_obj, SLOT_DATA, ANT_PTR(ws_head));
  
  if (nargs == 0 || vtype(args[0]) != T_ARR) return ws_obj;
  
  jsval_t iterable = args[0];
  jsoff_t length = js_arr_len(js, iterable);
  
  for (jsoff_t i = 0; i < length; i++) {
    jsval_t value = js_arr_get(js, iterable, i);
    
    if (vtype(value) != T_OBJ) return js_mkerr(js, "WeakSet value must be an object");
    
    weakset_entry_t *entry;
    HASH_FIND(hh, *ws_head, &value, sizeof(jsval_t), entry);
    if (entry) continue;
    
    entry = ant_calloc(sizeof(weakset_entry_t));
    if (!entry) return js_mkerr(js, "out of memory");
    entry->value_obj = value;
    HASH_ADD(hh, *ws_head, value_obj, sizeof(jsval_t), entry);
  }
  
  return ws_obj;
}

void init_collections_module(void) {
  ant_t *js = rt->js;
  
  jsval_t glob = js->global;
  jsval_t object_proto = js->object;
  
  jsval_t iter_sym = get_iterator_sym();
  jsval_t tag_sym = get_toStringTag_sym();
  
  jsval_t map_proto = js_mkobj(js);
  js_set_proto(js, map_proto, object_proto);
  js_set(js, map_proto, "set", js_mkfun(map_set));
  js_set(js, map_proto, "get", js_mkfun(map_get));
  js_set(js, map_proto, "has", js_mkfun(map_has));
  js_set(js, map_proto, "delete", js_mkfun(map_delete));
  js_set(js, map_proto, "clear", js_mkfun(map_clear));
  js_set_getter_desc(js, map_proto, "size", 4, js_mkfun(map_size), JS_DESC_C);
  js_set(js, map_proto, "entries", js_mkfun(map_entries));
  js_set(js, map_proto, "keys", js_mkfun(map_keys));
  js_set(js, map_proto, "values", js_mkfun(map_values));
  js_set(js, map_proto, "forEach", js_mkfun(map_forEach));
  js_set_sym(js, map_proto, iter_sym, js_mkfun(map_iterator));
  js_set_sym(js, map_proto, tag_sym, js_mkstr(js, "Map", 3));
  
  jsval_t map_ctor = js_mkobj(js);
  js_set_slot(js, map_ctor, SLOT_CFUNC, js_mkfun(builtin_Map));
  js_mkprop_fast(js, map_ctor, "prototype", 9, map_proto);
  js_mkprop_fast(js, map_ctor, "name", 4, ANT_STRING("Map"));
  js_set_descriptor(js, map_ctor, "name", 4, 0);
  js_set(js, map_ctor, "groupBy", js_mkfun(map_groupBy));
  js_define_species_getter(js, map_ctor);
  js_set(js, glob, "Map", js_obj_to_func(map_ctor));
  
  jsval_t set_proto = js_mkobj(js);
  js_set_proto(js, set_proto, object_proto);
  js_set(js, set_proto, "add", js_mkfun(set_add));
  js_set(js, set_proto, "has", js_mkfun(set_has));
  js_set(js, set_proto, "delete", js_mkfun(set_delete));
  js_set(js, set_proto, "clear", js_mkfun(set_clear));
  js_set_getter_desc(js, set_proto, "size", 4, js_mkfun(set_size), JS_DESC_C);
  js_set(js, set_proto, "values", js_mkfun(set_values));
  js_set(js, set_proto, "keys", js_mkfun(set_values));
  js_set(js, set_proto, "entries", js_mkfun(set_entries));
  js_set(js, set_proto, "forEach", js_mkfun(set_forEach));
  js_set_sym(js, set_proto, iter_sym, js_mkfun(set_iterator));
  js_set_sym(js, set_proto, tag_sym, js_mkstr(js, "Set", 3));
  
  jsval_t set_ctor = js_mkobj(js);
  js_set_slot(js, set_ctor, SLOT_CFUNC, js_mkfun(builtin_Set));
  js_mkprop_fast(js, set_ctor, "prototype", 9, set_proto);
  js_mkprop_fast(js, set_ctor, "name", 4, ANT_STRING("Set"));
  js_set_descriptor(js, set_ctor, "name", 4, 0);
  js_define_species_getter(js, set_ctor);
  js_set(js, glob, "Set", js_obj_to_func(set_ctor));
  
  jsval_t weakmap_proto = js_mkobj(js);
  js_set_proto(js, weakmap_proto, object_proto);
  js_set(js, weakmap_proto, "set", js_mkfun(weakmap_set));
  js_set(js, weakmap_proto, "get", js_mkfun(weakmap_get));
  js_set(js, weakmap_proto, "has", js_mkfun(weakmap_has));
  js_set(js, weakmap_proto, "delete", js_mkfun(weakmap_delete));
  js_set_sym(js, weakmap_proto, tag_sym, js_mkstr(js, "WeakMap", 7));
  
  jsval_t weakmap_ctor = js_mkobj(js);
  js_set_slot(js, weakmap_ctor, SLOT_CFUNC, js_mkfun(builtin_WeakMap));
  js_mkprop_fast(js, weakmap_ctor, "prototype", 9, weakmap_proto);
  js_mkprop_fast(js, weakmap_ctor, "name", 4, ANT_STRING("WeakMap"));
  js_set_descriptor(js, weakmap_ctor, "name", 4, 0);
  js_set(js, glob, "WeakMap", js_obj_to_func(weakmap_ctor));
  
  jsval_t weakset_proto = js_mkobj(js);
  js_set_proto(js, weakset_proto, object_proto);
  js_set(js, weakset_proto, "add", js_mkfun(weakset_add));
  js_set(js, weakset_proto, "has", js_mkfun(weakset_has));
  js_set(js, weakset_proto, "delete", js_mkfun(weakset_delete));
  js_set_sym(js, weakset_proto, tag_sym, js_mkstr(js, "WeakSet", 7));
  
  jsval_t weakset_ctor = js_mkobj(js);
  js_set_slot(js, weakset_ctor, SLOT_CFUNC, js_mkfun(builtin_WeakSet));
  js_mkprop_fast(js, weakset_ctor, "prototype", 9, weakset_proto);
  js_mkprop_fast(js, weakset_ctor, "name", 4, ANT_STRING("WeakSet"));
  js_set_descriptor(js, weakset_ctor, "name", 4, 0);
  js_set(js, glob, "WeakSet", js_obj_to_func(weakset_ctor));
  
  jsval_t weakref_proto = js_mkobj(js);
  js_set_proto(js, weakref_proto, object_proto);
  js_set(js, weakref_proto, "deref", js_mkfun(weakref_deref));
  js_set_sym(js, weakref_proto, tag_sym, js_mkstr(js, "WeakRef", 7));
  
  jsval_t weakref_ctor = js_mkobj(js);
  js_set_slot(js, weakref_ctor, SLOT_CFUNC, js_mkfun(builtin_WeakRef));
  js_mkprop_fast(js, weakref_ctor, "prototype", 9, weakref_proto);
  js_mkprop_fast(js, weakref_ctor, "name", 4, ANT_STRING("WeakRef"));
  js_set_descriptor(js, weakref_ctor, "name", 4, 0);
  js_set(js, glob, "WeakRef", js_obj_to_func(weakref_ctor));
  
  jsval_t finreg_proto = js_mkobj(js);
  js_set_proto(js, finreg_proto, object_proto);
  js_set(js, finreg_proto, "register", js_mkfun(finreg_register));
  js_set(js, finreg_proto, "unregister", js_mkfun(finreg_unregister));
  js_set_sym(js, finreg_proto, tag_sym, js_mkstr(js, "FinalizationRegistry", 20));
  
  jsval_t finreg_ctor = js_mkobj(js);
  js_set_slot(js, finreg_ctor, SLOT_CFUNC, js_mkfun(builtin_FinalizationRegistry));
  js_mkprop_fast(js, finreg_ctor, "prototype", 9, finreg_proto);
  js_mkprop_fast(js, finreg_ctor, "name", 4, ANT_STRING("FinalizationRegistry"));
  js_set_descriptor(js, finreg_ctor, "name", 4, 0);
  js_set(js, glob, "FinalizationRegistry", js_obj_to_func(finreg_ctor));
}

void collections_gc_reserve_roots(void (*op_val)(void *, jsval_t *), void *ctx) {
  for (size_t i = 0; i < map_registry_count; i++) {
    jsval_t map_obj = mkval(T_OBJ, map_registry[i].obj_offset);
    op_val(ctx, &map_obj); map_entry_t **head = map_registry[i].head;
    if (head && *head) {
      map_entry_t *entry, *tmp;
      HASH_ITER(hh, *head, entry, tmp) op_val(ctx, &entry->value);
    }
  }
  
  for (size_t i = 0; i < set_registry_count; i++) {
    jsval_t set_obj = mkval(T_OBJ, set_registry[i].obj_offset);
    op_val(ctx, &set_obj); set_entry_t **head = set_registry[i].head;
    if (head && *head) {
      set_entry_t *entry, *tmp;
      HASH_ITER(hh, *head, entry, tmp) op_val(ctx, &entry->value);
    }
  }
}

static void free_map_entries(map_entry_t **head) {
  if (!head || !*head) return;
  map_entry_t *entry, *tmp;
  HASH_ITER(hh, *head, entry, tmp) {
    HASH_DEL(*head, entry);
    free(entry->key);
    free(entry);
  }
}

static void free_set_entries(set_entry_t **head) {
  if (!head || !*head) return;
  set_entry_t *entry, *tmp;
  HASH_ITER(hh, *head, entry, tmp) {
    HASH_DEL(*head, entry);
    free(entry->key);
    free(entry);
  }
}

void collections_gc_update_roots(jsoff_t (*weak_off)(void *ctx, jsoff_t old), GC_OP_VAL_ARGS) {
  size_t write_idx = 0;
  
  for (size_t i = 0; i < map_registry_count; i++) {
    jsoff_t old_off = map_registry[i].obj_offset;
    jsoff_t new_off = weak_off(ctx, old_off);
    
    if (new_off == (jsoff_t)~0) {
      free_map_entries(map_registry[i].head);
      free(map_registry[i].head);
      continue;
    }
    
    map_registry[i].obj_offset = new_off;
    
    map_entry_t **head = map_registry[i].head;
    if (head && *head) {
      map_entry_t *entry, *tmp;
      HASH_ITER(hh, *head, entry, tmp) op_val(ctx, &entry->value);
    }
    
    if (write_idx != i) map_registry[write_idx] = map_registry[i];
    write_idx++;
  }
  map_registry_count = write_idx;
  
  write_idx = 0;
  for (size_t i = 0; i < set_registry_count; i++) {
    jsoff_t old_off = set_registry[i].obj_offset;
    jsoff_t new_off = weak_off(ctx, old_off);
    
    if (new_off == (jsoff_t)~0) {
      free_set_entries(set_registry[i].head);
      free(set_registry[i].head);
      continue;
    }
    
    set_registry[i].obj_offset = new_off;
    
    set_entry_t **head = set_registry[i].head;
    if (head && *head) {
      set_entry_t *entry, *tmp;
      HASH_ITER(hh, *head, entry, tmp) op_val(ctx, &entry->value);
    }
    
    if (write_idx != i) set_registry[write_idx] = set_registry[i];
    write_idx++;
  }
  set_registry_count = write_idx;
}

void cleanup_collections_module(void) {
  for (size_t i = 0; i < map_registry_count; i++) {
    free_map_entries(map_registry[i].head);
    free(map_registry[i].head);
  }
  free(map_registry);
  map_registry = NULL;
  map_registry_count = 0;
  map_registry_cap = 0;
  
  for (size_t i = 0; i < set_registry_count; i++) {
    free_set_entries(set_registry[i].head);
    free(set_registry[i].head);
  }
  free(set_registry);
  set_registry = NULL;
  set_registry_count = 0;
  set_registry_cap = 0;
}

size_t collections_get_external_memory(void) {
  size_t total = 0;
  
  total += map_registry_cap * sizeof(map_registry_entry_t);
  for (size_t i = 0; i < map_registry_count; i++) {
    total += sizeof(map_entry_t *);
    if (map_registry[i].head) {
      map_entry_t *entry, *tmp;
      HASH_ITER(hh, *map_registry[i].head, entry, tmp) {
        total += sizeof(map_entry_t);
        if (entry->key) total += strlen(entry->key) + 1;
      }
    }
  }
  
  total += set_registry_cap * sizeof(set_registry_entry_t);
  for (size_t i = 0; i < set_registry_count; i++) {
    total += sizeof(set_entry_t *);
    if (set_registry[i].head) {
      set_entry_t *entry, *tmp;
      HASH_ITER(hh, *set_registry[i].head, entry, tmp) {
        total += sizeof(set_entry_t);
        if (entry->key) total += strlen(entry->key) + 1;
      }
    }
  }
  
  return total;
}

#undef CLEANUP_REGISTRY
