#include "gc.h"
#include "errors.h"
#include <stdlib.h>

#include "silver/engine.h"
#include "silver/swarm.h"
#include "modules/regex.h"

#include "ops/literals.h"
#include "ops/stack.h"
#include "ops/locals.h"
#include "ops/upvalues.h"
#include "ops/globals.h"
#include "ops/property.h"
#include "ops/optional.h"
#include "ops/private.h"
#include "ops/super.h"
#include "ops/arithmetic.h"
#include "ops/comparison.h"
#include "ops/bitwise.h"
#include "ops/unary.h"
#include "ops/control.h"
#include "ops/calls.h"
#include "ops/returns.h"
#include "ops/exceptions.h"
#include "ops/async.h"
#include "ops/iteration.h"
#include "ops/using.h"
#include "ops/objects.h"
#include "ops/coercion.h"

sv_vm_t *sv_vm_create(ant_t *js, sv_vm_kind_t kind) {
  int stack_size, max_frames;
  sv_vm_limits(kind, &stack_size, &max_frames);

  sv_vm_t *vm = calloc(1, sizeof(*vm));
  if (!vm) return NULL;
  
  vm->js = js;
  vm->fp = -1;
  
  vm->stack_size = stack_size;
  vm->max_frames = max_frames;
  
  vm->suspended_entry_fp = -1;
  vm->suspended_saved_fp = -1;
  
  vm->stack = calloc((size_t)stack_size, sizeof(ant_value_t));
  vm->frames = calloc((size_t)max_frames, sizeof(sv_frame_t));
  
  if (!vm->stack || !vm->frames) { 
    sv_vm_destroy(vm);
    return NULL;
  }
  
  return vm;
}

void sv_vm_destroy(sv_vm_t *vm) {
  if (!vm) return;
  free(vm->stack);
  free(vm->frames);
  free(vm);
}

static bool sv_vm_grow_frames(sv_vm_t *vm) {
  int new_max = vm->max_frames * 2;
  if (new_max > SV_FRAMES_HARD_MAX) new_max = SV_FRAMES_HARD_MAX;
  if (new_max <= vm->max_frames) return false;
  
  sv_frame_t *nf = realloc(vm->frames, (size_t)new_max * sizeof(sv_frame_t));
  if (!nf) return false;
  vm->frames = nf;
  vm->max_frames = new_max;
  
  return true;
}

static bool sv_vm_grow_stack(sv_vm_t *vm) {
  int new_size = vm->stack_size * 2;
  if (new_size > SV_STACK_HARD_MAX) new_size = SV_STACK_HARD_MAX;
  if (new_size <= vm->stack_size) return false;
  
  ant_value_t *old = vm->stack;
  int old_size = vm->stack_size;
  
  ant_value_t *ns = realloc(vm->stack, (size_t)new_size * sizeof(ant_value_t));
  if (!ns) return false;
  
  ptrdiff_t delta = ns - old;
  vm->stack = ns;
  vm->stack_size = new_size;
  
  if (delta != 0) {
    for (int i = 0; i <= vm->fp; i++) {
      if (vm->frames[i].bp) vm->frames[i].bp += delta;
      if (vm->frames[i].lp) vm->frames[i].lp += delta;
    }
    for (sv_upvalue_t *uv = vm->open_upvalues; uv; uv = uv->next) if (
      uv->location != &uv->closed &&
      sv_slot_in_range(old, (size_t)old_size, uv->location)
    ) uv->location += delta;
  }
  
  return true;
}

bool sv_lookup_srcpos(sv_func_t *func, int bc_offset, uint32_t *line, uint32_t *col) {
  if (!func || !func->srcpos || func->srcpos_count <= 0) return false;
  int best = -1;
  for (int i = 0; i < func->srcpos_count; i++) {
    if ((int)func->srcpos[i].bc_offset <= bc_offset) best = i;
    else break;
  }
  if (best < 0) return false;
  if (line) *line = func->srcpos[best].line;
  if (col) *col = func->srcpos[best].col;
  return true;
}

bool sv_lookup_srcspan(sv_func_t *func, int bc_offset, uint32_t *src_off, uint32_t *src_end) {
  if (!func || !func->srcpos || func->srcpos_count <= 0) return false;
  int best = -1;
  for (int i = 0; i < func->srcpos_count; i++) {
    if ((int)func->srcpos[i].bc_offset <= bc_offset) best = i;
    else break;
  }
  
  if (best < 0) return false;
  uint32_t off = func->srcpos[best].src_off;
  uint32_t end = func->srcpos[best].src_end;
  
  if (end < off) end = off;
  if (src_off) *src_off = off;
  if (src_end) *src_end = end;
  
  return true;
}

static ant_offset_t sv_srcpos_to_offset_local(const char *code, ant_offset_t clen, uint32_t line, uint32_t col) {
  ant_offset_t off = 0;
  uint32_t cur = 1;
  
  while (off < clen && cur < line) {
    if (code[off] == '\n') cur++;
    off++;
  }
  
  if (col > 0) off += col - 1;
  if (off > clen) off = clen;
  return off;
}

void js_set_error_site_from_bc(ant_t *js, sv_func_t *func, int bc_offset, const char *filename) {
  if (!js || !func || !func->source || func->source_len <= 0) return;

  uint32_t src_off = 0, src_end = 0;
  if (sv_lookup_srcspan(func, bc_offset, &src_off, &src_end)) {
    ant_offset_t off = (ant_offset_t)src_off;
    ant_offset_t span_len = (ant_offset_t)(src_end > src_off ? (src_end - src_off) : 0);
    if (span_len <= 0 && off < (ant_offset_t)func->source_len) span_len = 1;
    
    js_set_error_site(
      js, func->source, (ant_offset_t)func->source_len,
      filename ? filename : func->filename, off, span_len
    );
    return;
  }

  uint32_t line = 0, col = 0;
  if (sv_lookup_srcpos(func, bc_offset, &line, &col)) {
    ant_offset_t off = sv_srcpos_to_offset_local(func->source, (ant_offset_t)func->source_len, line, col);
    js_set_error_site(
      js, func->source, (ant_offset_t)func->source_len,
      filename ? filename : func->filename, off, 0
    );
  }
}

void js_set_error_site_from_vm_top(ant_t *js) {
  sv_vm_t *vm = sv_vm_get_active(js);
  if (!js || !vm || vm->fp < 0) return;
  
  sv_frame_t *frame = &vm->frames[vm->fp];
  sv_func_t *func = frame->func;
  if (!func) return;
  
  int bc_off = 0;
  if (frame->ip && func->code) bc_off = (int)(frame->ip - func->code);
  js_set_error_site_from_bc(js, func, bc_off, func->filename);
}

// TODO: move to strings.c
static inline bool sv_builder_has_cached_value(const ant_string_builder_t *builder) {
  return builder && builder->cached != js_mkundef();
}

static inline ant_flat_string_t *sv_string_builder_flat_ptr(ant_value_t value) {
  if (vtype(value) != T_STR || str_is_heap_rope(value) || str_is_heap_builder(value)) return NULL;
  return (ant_flat_string_t *)(uintptr_t)vdata(value);
}

static inline ant_string_builder_t *sv_string_builder_heap_ptr(ant_value_t value) {
  if (vtype(value) != T_STR || !str_is_heap_builder(value)) return NULL;
  return ant_str_builder_ptr(value);
}

static inline uint8_t sv_builder_chunk_ascii_state(ant_flat_string_t *flat) {
  if (!flat) return STR_ASCII_UNKNOWN;
  if (flat->is_ascii == STR_ASCII_UNKNOWN)
    flat->is_ascii = str_detect_ascii_bytes(flat->bytes, (size_t)flat->len);
  return flat->is_ascii;
}

static inline void sv_builder_note_ascii(ant_string_builder_t *builder, uint8_t state) {
  if (!builder) return;
  if (state == STR_ASCII_NO) builder->ascii_state = STR_ASCII_NO;
  else if (builder->ascii_state != STR_ASCII_NO && state == STR_ASCII_YES)
    builder->ascii_state = STR_ASCII_YES;
}

static inline void sv_builder_record_flat(ant_string_builder_t *builder, ant_flat_string_t *flat) {
  if (!builder || !flat) return;
  builder->len += flat->len;
  sv_builder_note_ascii(builder, sv_builder_chunk_ascii_state(flat));
}

static ant_value_t sv_builder_normalize_chunk(ant_t *js, ant_value_t value) {
  if (vtype(value) != T_STR) return js_mkerr(js, "string builder expects string chunk");
  return str_materialize(js, value);
}

static ant_value_t sv_builder_push_chunk_value(
  ant_t *js, ant_string_builder_t *builder, ant_value_t chunk
) {
  if (!builder) return js_mkerr(js, "string builder chunk allocation failed");
  ant_builder_chunk_t *node = (ant_builder_chunk_t *)js_type_alloc(
    js, ANT_ALLOC_ROPE, sizeof(*node), _Alignof(ant_builder_chunk_t)
  );
  if (!node) return js_mkerr(js, "string builder chunk allocation failed");
  node->next = NULL;
  node->value = chunk;
  if (builder->chunk_tail) builder->chunk_tail->next = node;
  else builder->head = node;
  builder->chunk_tail = node;
  return js_mkundef();
}

static ant_value_t sv_builder_flush_tail(
  ant_t *js, ant_string_builder_t *builder
) {
  if (!builder || builder->tail_len == 0) return js_mkundef();
  ant_value_t tail = js_mkstr(js, builder->tail, builder->tail_len);
  if (is_err(tail)) return tail;
  ant_value_t push = sv_builder_push_chunk_value(js, builder, tail);
  if (is_err(push)) return push;
  builder->tail_len = 0;
  return js_mkundef();
}

static ant_value_t sv_builder_append_flat(
  ant_t *js, ant_string_builder_t *builder, ant_value_t chunk
) {
  ant_flat_string_t *flat = sv_string_builder_flat_ptr(chunk);
  if (!flat) return js_mkerr(js, "string builder received non-flat string");
  if (flat->len == 0) return js_mkundef();

  if (
    flat->len <= STR_BUILDER_TAIL_CAP &&
    builder->tail_len + flat->len <= STR_BUILDER_TAIL_CAP
  ) {
    memcpy(builder->tail + builder->tail_len, flat->bytes, (size_t)flat->len);
    builder->tail_len = (uint16_t)(builder->tail_len + flat->len);
    sv_builder_record_flat(builder, flat);
    return js_mkundef();
  }

  ant_value_t flush = sv_builder_flush_tail(js, builder);
  if (is_err(flush)) return flush;
  ant_value_t push = sv_builder_push_chunk_value(js, builder, chunk);
  
  if (is_err(push)) return push;
  sv_builder_record_flat(builder, flat);
  
  return js_mkundef();
}

static ant_value_t sv_string_builder_new(ant_t *js) {
  ant_string_builder_t *builder = (ant_string_builder_t *)js_type_alloc(
    js, ANT_ALLOC_ROPE, sizeof(*builder), _Alignof(ant_string_builder_t)
  );
  if (!builder) return js_mkerr(js, "string builder allocation failed");
  memset(builder, 0, sizeof(*builder));
  builder->cached = js_mkundef();
  builder->ascii_state = STR_ASCII_YES;
  return ant_mkbuilder_value(builder);
}

static inline void sv_record_slot_feedback(
  sv_frame_t *frame, sv_func_t *func, uint16_t slot_idx, ant_value_t value
) {
  if (!frame || !func) return;
  if ((int)slot_idx < func->param_count) return;
  sv_tfb_record_local(func, (int)(slot_idx - func->param_count), value);
}

bool sv_slot_has_open_upvalue(sv_vm_t *vm, ant_value_t *slot) {
  if (!vm || !slot) return false;
  for (sv_upvalue_t *uv = vm->open_upvalues; uv; uv = uv->next)
    if (uv->location == slot) return true;
  return false;
}

ant_value_t sv_string_builder_read_value(ant_t *js, ant_value_t value) {
  if (vtype(value) == T_STR && str_is_heap_builder(value))
    return str_materialize(js, value);
  return value;
}

static ant_value_t sv_slot_generic_add_store(
  sv_vm_t *vm, ant_t *js, ant_value_t *slot, ant_value_t lhs, ant_value_t rhs
) {
  vm->stack[vm->sp++] = lhs;
  vm->stack[vm->sp++] = rhs;
  ant_value_t err = sv_op_add(vm, js);
  if (is_err(err)) return err;
  *slot = vm->stack[--vm->sp];
  return js_mkundef();
}

ant_value_t sv_string_builder_flush_slot(
  sv_vm_t *vm, ant_t *js, sv_frame_t *frame, uint16_t slot_idx
) {
  ant_value_t *slot = sv_frame_slot_ptr(frame, slot_idx);
  if (!slot || vtype(*slot) != T_STR || !str_is_heap_builder(*slot)) return js_mkundef();

  ant_value_t out = str_materialize(js, *slot);
  if (is_err(out)) return out;
  *slot = out;
  sv_record_slot_feedback(frame, frame->func, slot_idx, out);
  return out;
}

ant_value_t sv_string_builder_append_slot(
  sv_vm_t *vm, ant_t *js, sv_frame_t *frame,
  sv_func_t *func, uint16_t slot_idx, ant_value_t rhs
) {
  ant_value_t *slot = sv_frame_slot_ptr(frame, slot_idx);
  if (!slot) return js_mkerr(js, "invalid string builder slot");

  ant_value_t lhs = *slot;
  ant_string_builder_t *builder = sv_string_builder_heap_ptr(lhs);

  if (builder) {
    ant_value_t rhs_str = coerce_to_str_concat(js, rhs);
    if (is_err(rhs_str)) return rhs_str;
    rhs_str = sv_builder_normalize_chunk(js, rhs_str);
    if (is_err(rhs_str)) return rhs_str;
    builder->cached = js_mkundef();
    ant_value_t append_err = sv_builder_append_flat(js, builder, rhs_str);
    if (is_err(append_err)) return append_err;
    sv_record_slot_feedback(frame, func, slot_idx, lhs);
    return js_mkundef();
  }

  if (vtype(lhs) == T_NUM && vtype(rhs) == T_NUM) {
    *slot = tov(tod(lhs) + tod(rhs));
    sv_record_slot_feedback(frame, func, slot_idx, *slot);
    return js_mkundef();
  }

  ant_value_t lu = unwrap_primitive(js, lhs);
  ant_value_t ru = unwrap_primitive(js, rhs);
  bool string_concat = is_non_numeric(lu) || is_non_numeric(ru);
  if (!string_concat) {
    ant_value_t add_err = sv_slot_generic_add_store(vm, js, slot, lhs, rhs);
    if (is_err(add_err)) return add_err;
    sv_record_slot_feedback(frame, func, slot_idx, *slot);
    return js_mkundef();
  }

  ant_value_t lhs_str = coerce_to_str_concat(js, lhs);
  if (is_err(lhs_str)) return lhs_str;
  ant_value_t rhs_str = coerce_to_str_concat(js, rhs);
  if (is_err(rhs_str)) return rhs_str;
  lhs_str = sv_builder_normalize_chunk(js, lhs_str);
  if (is_err(lhs_str)) return lhs_str;
  rhs_str = sv_builder_normalize_chunk(js, rhs_str);
  if (is_err(rhs_str)) return rhs_str;

  ant_value_t builder_value = sv_string_builder_new(js);
  if (is_err(builder_value)) return builder_value;
  builder = sv_string_builder_heap_ptr(builder_value);

  ant_value_t append_lhs = sv_builder_append_flat(js, builder, lhs_str);
  if (is_err(append_lhs)) return append_lhs;
  ant_value_t append_rhs = sv_builder_append_flat(js, builder, rhs_str);
  if (is_err(append_rhs)) return append_rhs;
  *slot = builder_value;
  sv_record_slot_feedback(frame, func, slot_idx, *slot);
  
  return js_mkundef();
}

ant_value_t sv_string_builder_append_snapshot_slot(
  sv_vm_t *vm, ant_t *js, sv_frame_t *frame,
  sv_func_t *func, uint16_t slot_idx, ant_value_t lhs, ant_value_t rhs
) {
  ant_value_t *slot = sv_frame_slot_ptr(frame, slot_idx);
  if (!slot) return js_mkerr(js, "invalid string builder slot");

  // the snapshot path is only semantically required if the slot changed while
  // evaluating the RHS. when it did not, delegate to the normal append path so
  // active builders can keep appending in place instead of rebuilding.
  if (*slot == lhs)
    return sv_string_builder_append_slot(vm, js, frame, func, slot_idx, rhs);

  if (vtype(lhs) == T_NUM && vtype(rhs) == T_NUM) {
    *slot = tov(tod(lhs) + tod(rhs));
    sv_record_slot_feedback(frame, func, slot_idx, *slot);
    return js_mkundef();
  }

  ant_value_t lu = unwrap_primitive(js, lhs);
  ant_value_t ru = unwrap_primitive(js, rhs);
  
  bool string_concat = is_non_numeric(lu) || is_non_numeric(ru);
  if (!string_concat) {
    ant_value_t add_err = sv_slot_generic_add_store(vm, js, slot, lhs, rhs);
    if (is_err(add_err)) return add_err;
    sv_record_slot_feedback(frame, func, slot_idx, *slot);
    return js_mkundef();
  }

  ant_value_t lhs_str = coerce_to_str_concat(js, lhs);
  if (is_err(lhs_str)) return lhs_str;
  ant_value_t rhs_str = coerce_to_str_concat(js, rhs);
  if (is_err(rhs_str)) return rhs_str;
  lhs_str = sv_builder_normalize_chunk(js, lhs_str);
  if (is_err(lhs_str)) return lhs_str;
  rhs_str = sv_builder_normalize_chunk(js, rhs_str);
  if (is_err(rhs_str)) return rhs_str;

  ant_value_t builder_value = sv_string_builder_new(js);
  if (is_err(builder_value)) return builder_value;
  ant_string_builder_t *builder = sv_string_builder_heap_ptr(builder_value);

  ant_value_t append_lhs = sv_builder_append_flat(js, builder, lhs_str);
  if (is_err(append_lhs)) return append_lhs;
  ant_value_t append_rhs = sv_builder_append_flat(js, builder, rhs_str);
  if (is_err(append_rhs)) return append_rhs;
  *slot = builder_value;
  sv_record_slot_feedback(frame, func, slot_idx, *slot);
  
  return js_mkundef();
}

void sv_vm_visit_frame_funcs(sv_vm_t *vm, void (*visitor)(void *, sv_func_t *), void *ctx) {
  if (!vm) return;
  for (int i = 0; i <= vm->fp; i++) if (vm->frames[i].func) visitor(ctx, vm->frames[i].func);
}

ant_value_t sv_call_async_closure_dispatch(
  sv_vm_t *vm, ant_t *js, sv_closure_t *closure,
  ant_value_t callee_func, ant_value_t super_val,
  ant_value_t this_val, ant_value_t *args, int argc
) {
  return sv_start_async_closure(vm, js, closure, callee_func, super_val, this_val, args, argc);
}

ant_value_t sv_execute_entry_tla(ant_t *js, sv_func_t *func, ant_value_t this_val) {
  return sv_start_tla(js, func, this_val);
}

static inline void sv_sync_frame_locals(
  sv_vm_t *vm, sv_frame_t **frame, sv_func_t **func,
  ant_value_t **bp, ant_value_t **lp
) {
  *frame = &vm->frames[vm->fp]; *func = (*frame)->func;
  *bp = (*frame)->bp; *lp = (*frame)->lp;
}

static inline void sv_drop_frame_runtime_state(ant_t *js, sv_frame_t *frame) {
if (frame && vtype(frame->arguments_obj) != T_UNDEF) {
  js_arguments_detach(js, frame->arguments_obj);
  frame->arguments_obj = js_mkundef();
}}

static inline ant_value_t sv_stage_frame_args(
  sv_vm_t *vm, ant_t *js, sv_func_t *func, ant_value_t *args, int argc,
  ant_value_t **out_bp, ant_value_t **out_lp
) {
  int arg_slots = (argc > func->param_count) ? argc : func->param_count;
  int need = arg_slots + func->max_locals;
  if (vm->sp + need > vm->stack_size) {
    int args_idx = (
      args && args >= vm->stack && args < vm->stack + vm->stack_size)
      ? (int)(args - vm->stack) : -1;
    while (vm->sp + need > vm->stack_size) {
      if (!sv_vm_grow_stack(vm)) return js_mkerr(js, "stack overflow");
    }
    if (args_idx >= 0) args = &vm->stack[args_idx];
  }

  ant_value_t *base = &vm->stack[vm->sp];
  *out_bp = base;
  *out_lp = base + arg_slots;

  if (argc > 0 && args)
    memmove(base, args, (size_t)argc * sizeof(ant_value_t));
  for (int i = argc; i < arg_slots; i++)
    base[i] = js_mkundef();
  if (func->max_locals > 0)
    for (int i = 0; i < func->max_locals; i++)
      (*out_lp)[i] = js_mkundef();
  vm->sp += need;
  return js_mkundef();
}

static inline void sv_yield_star_store_state(sv_vm_t *vm, ant_value_t *lp, uint16_t base) {
  lp[base + 0] = vm->stack[vm->sp - 3];
  lp[base + 1] = vm->stack[vm->sp - 2];
  lp[base + 2] = vm->stack[vm->sp - 1];
}

static inline void sv_yield_star_push_state(sv_vm_t *vm, ant_value_t *lp, uint16_t base) {
  vm->stack[vm->sp++] = lp[base + 0];
  vm->stack[vm->sp++] = lp[base + 1];
  vm->stack[vm->sp++] = lp[base + 2];
}

static inline void sv_yield_star_clear_state(ant_t *js, ant_value_t *lp, uint16_t base) {
  lp[base + 0] = js_mkundef();
  lp[base + 1] = js_mkundef();
  lp[base + 2] = js_mkundef();
  lp[base + 3] = js_false;
}

static inline ant_value_t sv_yield_star_unpack_result(
  ant_t *js, ant_value_t result, ant_value_t *out_value, bool *out_done
) {
  if (!is_object_type(result))
    return js_mkerr_typed(js, JS_ERR_TYPE, "Iterator result is not an object");

  ant_value_t done = js_mkundef();
  sv_iter_result_unpack(js, result, &done, out_value);
  
  if (is_err(done)) return done;
  if (is_err(*out_value)) return *out_value;
  *out_done = js_truthy(js, done);
  
  return js_mkundef();
}

static inline ant_value_t sv_yield_star_call_method(
  sv_vm_t *vm, ant_t *js, ant_value_t iterator,
  const char *name, ant_value_t arg, ant_value_t *out_value, bool *out_done
) {
  ant_value_t method = js_getprop_fallback(js, iterator, name);
  uint8_t mt = vtype(method);
  
  if (mt != T_FUNC && mt != T_CFUNC) {
    *out_value = js_mkundef();
    *out_done = true;
    return js_mkundef();
  }

  ant_value_t call_args[1] = { arg };
  ant_value_t result = sv_vm_call(vm, js, method, iterator, call_args, 1, NULL, false);
  if (is_err(result)) return result;
  
  return sv_yield_star_unpack_result(js, result, out_value, out_done);
}

static inline ant_value_t sv_yield_star_close_iterator(
  sv_vm_t *vm, ant_t *js, ant_value_t iterator
) {
  ant_value_t close_value = js_mkundef();
  bool close_done = true;
  
  return sv_yield_star_call_method(
    vm, js, iterator, "return", js_mkundef(), 
    &close_value, &close_done
  );
}

static inline ant_value_t sv_yield_star_next(
  sv_vm_t *vm, ant_t *js, ant_value_t *lp, uint16_t base,
  ant_value_t sent, ant_value_t *out_value, bool *out_done
) {
  int tag = (int)js_getnum(lp[base + 2]);
  bool first = js_truthy(js, lp[base + 3]);
  lp[base + 3] = js_false;

  if (tag == SV_ITER_GENERIC) {
    ant_value_t iterator = lp[base + 0];
    ant_value_t next_method = lp[base + 1];
    
    uint8_t nt = vtype(next_method);
    if (nt != T_FUNC && nt != T_CFUNC)
      return js_mkerr(js, "iterator.next is not a function");
      
    ant_value_t call_args[1] = { sent };
    ant_value_t result = sv_vm_call(
      vm, js, next_method, iterator, first ? NULL : call_args, first ? 0 : 1,
      NULL, false
    );
    
    if (is_err(result)) return result;
    return sv_yield_star_unpack_result(js, result, out_value, out_done);
  }

  sv_yield_star_push_state(vm, lp, base);
  ant_value_t status = sv_iter_advance(vm, js, 0, out_value, out_done);
  if (is_err(status)) return status;
  
  sv_yield_star_store_state(vm, lp, base);
  vm->sp -= 3;
  
  return js_mkundef();
}

static inline ant_value_t sv_yield_star_throw(
  sv_vm_t *vm, ant_t *js, ant_value_t *lp, uint16_t base,
  ant_value_t thrown, ant_value_t *out_value, bool *out_done
) {
  int tag = (int)js_getnum(lp[base + 2]);
  if (tag != SV_ITER_GENERIC) return js_throw(js, thrown);

  ant_value_t iterator = lp[base + 0];
  ant_value_t throw_method = js_getprop_fallback(js, iterator, "throw");
  uint8_t tt = vtype(throw_method);
  
  if (tt != T_FUNC && tt != T_CFUNC) {
    ant_value_t close_status = sv_yield_star_close_iterator(vm, js, iterator);
    if (is_err(close_status)) return close_status;
    return js_throw(js, thrown);
  }

  ant_value_t call_args[1] = { thrown };
  ant_value_t result = sv_vm_call(vm, js, throw_method, iterator, call_args, 1, NULL, false);
  if (is_err(result)) return result;
  
  return sv_yield_star_unpack_result(js, result, out_value, out_done);
}

static inline ant_value_t sv_yield_star_return(
  sv_vm_t *vm, ant_t *js, ant_value_t *lp, uint16_t base,
  ant_value_t value, ant_value_t *out_value, bool *out_done
) {
  int tag = (int)js_getnum(lp[base + 2]);
  
  if (tag != SV_ITER_GENERIC) {
    *out_value = value;
    *out_done = true;
    return js_mkundef();
  }

  return sv_yield_star_call_method(vm, js, lp[base + 0], "return", value, out_value, out_done);
}

static inline ant_value_t sv_execute_entry_common(
  sv_vm_t *vm, sv_func_t *func, sv_upvalue_t **upvalues, int upvalue_count,
  ant_value_t callee_func, ant_value_t super_val,
  ant_value_t this_val, ant_value_t *args, int argc, ant_value_t *out_this
) {
  if (!vm || !vm->js || !func) return mkval(T_ERR, 0);
  ant_t *js = vm->js;
  if (vm->fp + 1 >= vm->max_frames && !sv_vm_grow_frames(vm))
    return js_mkerr_typed(js, JS_ERR_RANGE | JS_ERR_NO_STACK, "Maximum call stack size exceeded");

  int saved_fp = vm->fp;
  vm->fp = saved_fp + 1;
  vm->frames[vm->fp].upvalues = upvalues;
  vm->frames[vm->fp].upvalue_count = upvalue_count;
  vm->frames[vm->fp].callee = callee_func;

  ant_value_t result = sv_execute_frame(vm, func, this_val, super_val, args, argc);
  if (out_this) *out_this = vm->frames[vm->fp].this;
  if (!vm->suspended) vm->fp = saved_fp;

  return result;
}

#ifdef ANT_JIT
static inline ant_value_t sv_try_direct_closure_jit(
  sv_vm_t *vm, ant_t *js, sv_func_t *caller_func, uint8_t *caller_ip, sv_frame_t *caller_frame,
  sv_closure_t *closure, ant_value_t jit_this, ant_value_t *call_args, int call_argc
) {
  sv_func_t *callee = closure->func;
  if (!callee) return SV_JIT_RETRY_INTERP;

  if (caller_func && caller_func->type_feedback && caller_ip)
    sv_tfb_record_call_target(caller_func, (int)(caller_ip - caller_func->code), callee);

  if (callee->jit_code) {
    if (caller_frame && caller_ip) caller_frame->ip = caller_ip + 3;
    sv_jit_enter(js);
    ant_value_t jit_result = ((sv_jit_func_t)callee->jit_code)(
      vm, jit_this, js_mkundef(), closure->super_val, 
      call_args, call_argc, closure
    );
    sv_jit_leave(js);
    if (sv_is_jit_bailout(jit_result)) {
      sv_jit_on_bailout(callee);
      return SV_JIT_RETRY_INTERP;
    }
    return jit_result;
  }

  if (callee->is_generator) return SV_JIT_RETRY_INTERP;

  uint32_t cc = ++callee->call_count;
  if (__builtin_expect(cc == SV_TFB_ALLOC_THRESHOLD, 0))
    sv_tfb_ensure(callee);
  if (cc <= SV_JIT_THRESHOLD) return SV_JIT_RETRY_INTERP;

  sv_jit_func_t jit_fn = sv_jit_compile(js, callee, closure);
  if (!jit_fn) {
    callee->call_count = 0;
    callee->back_edge_count = 0;
    return SV_JIT_RETRY_INTERP;
  }

  callee->jit_code = (void *)jit_fn;
  if (caller_frame && caller_ip) caller_frame->ip = caller_ip + 3;
  sv_jit_enter(js);
  ant_value_t jit_result = jit_fn(
    vm, jit_this, js_mkundef(), closure->super_val,
    call_args, call_argc, closure
  );
  sv_jit_leave(js);
  if (sv_is_jit_bailout(jit_result)) {
    sv_jit_on_bailout(callee);
    return SV_JIT_RETRY_INTERP;
  }
  return jit_result;
}
#endif

ant_value_t sv_execute_entry(
  sv_vm_t *vm, sv_func_t *func, ant_value_t this_val, ant_value_t *args, int argc
) {
  return sv_execute_entry_common(vm, func, NULL, 0, js_mkundef(), js_mkundef(), this_val, args, argc, NULL);
}

ant_value_t sv_execute_closure_entry(
  sv_vm_t *vm, sv_closure_t *closure, ant_value_t callee_func, ant_value_t super_val,
  ant_value_t this_val, ant_value_t *args, int argc, ant_value_t *out_this
) {
  if (!closure || !closure->func) return mkval(T_ERR, 0);
  return sv_execute_entry_common(
    vm, closure->func, closure->upvalues, closure->func->upvalue_count, callee_func,
    super_val, this_val, args, argc, out_this
  );
}

ant_value_t sv_execute_frame(sv_vm_t *vm, sv_func_t *func, ant_value_t this, ant_value_t super_val, ant_value_t *args, int argc) {
  ant_t *js = vm->js;
  
  bool resuming = vm->suspended && vm->suspended_resume_pending;
  uint8_t *ip = resuming ? NULL : func->code;
  
  int entry_fp = resuming 
    ? vm->suspended_entry_fp 
    : vm->fp;
  
  ant_value_t vm_result = js_mkundef();
  ant_value_t suspended_resume_value = js_mkundef();
  sv_resume_kind_t suspended_resume_kind = SV_RESUME_NEXT;
  
  js->vm_exec_depth++;

  // TODO: shorthand?
  sv_frame_t *frame = &vm->frames[vm->fp];
  if (!resuming) {
    sv_drop_frame_runtime_state(js, frame);
    frame->ip = ip;
    frame->func = func;
    frame->this = sv_normalize_this_for_frame(js, func, this);
    frame->new_target = js->new_target;
    frame->super_val = super_val;
    frame->prev_sp = vm->sp;
    frame->handler_base = vm->handler_depth;
    frame->handler_top = vm->handler_depth;
    frame->argc = argc;
    frame->completion.kind = SV_COMPLETION_NONE;
    frame->completion.value = js_mkundef();
    frame->with_obj = js_mkundef();
    frame->arguments_obj = js_mkundef();
  } else {
    func = frame->func;
    ip = frame->ip;
    suspended_resume_value = vm->suspended_resume_value;
    suspended_resume_kind = vm->suspended_resume_kind;
    vm->suspended = false;
    vm->suspended_resume_pending = false;
    vm->suspended_resume_is_error = false;
    vm->suspended_resume_kind = SV_RESUME_NEXT;
    vm->suspended_resume_value = js_mkundef();
  }
  
  ant_value_t *entry_bp = NULL;
  ant_value_t *entry_lp = NULL;
  
  if (!resuming) {
  ant_value_t stage_err = sv_stage_frame_args(vm, js, func, args, argc, &entry_bp, &entry_lp);
  if (is_err(stage_err)) {
    vm_result = stage_err;
    goto sv_leave;
  }}
  
  #ifdef ANT_JIT
  if (!resuming && vm->jit_resume.active) {
    ip = func->code + vm->jit_resume.ip_offset;
    frame->ip = ip;
    int64_t rl = 
      vm->jit_resume.n_locals < func->max_locals
      ? vm->jit_resume.n_locals : func->max_locals;
    for (int64_t i = 0; i < rl; i++)
      entry_lp[i] = vm->jit_resume.locals[i];
      
    ant_value_t *old_lp = vm->jit_resume.locals;
    for (sv_upvalue_t *uv = vm->open_upvalues; uv; uv = uv->next) {
    if (uv->location >= old_lp && uv->location < old_lp + rl) {
      ptrdiff_t slot = uv->location - old_lp;
      uv->location = &entry_lp[slot];
    }}
  }
  #endif
  
  if (!resuming) {
    frame->bp = entry_bp;
    frame->lp = entry_lp;
  }

  ant_value_t *bp = frame->bp;
  ant_value_t *lp = frame->lp;

  #ifdef ANT_JIT
  if (!resuming && vm->jit_resume.active) {
    for (int64_t i = 0; i < vm->jit_resume.vstack_sp; i++)
      vm->stack[vm->sp++] = vm->jit_resume.vstack[i];

    int resume_off = (int)vm->jit_resume.ip_offset;
    uint8_t *scan = func->code;
    uint8_t *scan_end = func->code + resume_off;
    typedef struct { uint8_t *catch_ip; int saved_sp; } pending_h;
    
    pending_h h_stack[SV_TRY_MAX];
    int enclosing_count = 0;
    int total_depth = 0;
    bool is_enclosing[SV_TRY_MAX];
    
    while (scan < scan_end) {
      sv_op_t scan_op = (sv_op_t)*scan;
      int scan_sz = sv_op_size[scan_op];
      if (scan_sz == 0) break;
      if (scan_op == OP_TRY_PUSH && total_depth < SV_TRY_MAX) {
        int32_t off = sv_get_i32(scan + 1);
        int catch_off = (int)(scan - func->code) + scan_sz + off;
        is_enclosing[total_depth] = (catch_off > resume_off);
        if (is_enclosing[total_depth]) {
          h_stack[enclosing_count].catch_ip = func->code + catch_off;
          h_stack[enclosing_count].saved_sp = vm->sp;
          enclosing_count++;
        }
        total_depth++;
      } else if (scan_op == OP_TRY_POP && total_depth > 0) {
        total_depth--;
        if (is_enclosing[total_depth]) enclosing_count--;
      }
      scan += scan_sz;
    }
    
    for (int i = 0; i < enclosing_count; i++) {
      if (vm->handler_depth < SV_HANDLER_MAX) {
        sv_handler_t *h = &vm->handler_stack[vm->handler_depth++];
        h->kind = SV_HANDLER_TRY;
        h->ip = h_stack[i].catch_ip;
        h->saved_sp = h_stack[i].saved_sp;
        frame->handler_top = vm->handler_depth;
      }
    }

    vm->jit_resume.active = false;
  }
  #endif

  static const void *dispatch[OP__COUNT] = {
    #define OP_DEF(name, size, n_pop, n_push, f) [OP_##name] = &&L_##name,
    #include "silver/opcode.h"
  };
  
  ant_value_t sv_err;
  ant_value_t  tc_this = js_mkundef();
  
  #define VM_CHECK(expr) ({            \
    frame->ip = ip;                    \
    sv_err = (expr);                   \
    if (is_err(sv_err)) goto sv_throw; \
  })

  #define DISPATCH() goto *dispatch[*ip]
  #define NEXT(n)    ({ ip += (n); DISPATCH(); })

  #ifdef ANT_JIT
  #define JIT_OSR_BACK_EDGE() do {                                          \
    if (!func->jit_compile_failed) {                                        \
      if (!func->type_feedback) sv_tfb_ensure(func);                        \
      if (++func->back_edge_count >= SV_JIT_OSR_THRESHOLD) {                \
      ant_value_t osr_r = sv_jit_try_osr(                                   \
        vm, js, frame, func,                                                \
         (int)(ip - func->code));                                           \
      if (osr_r != SV_JIT_RETRY_INTERP) {                                   \
        if (is_err(osr_r)) { sv_err = osr_r; goto sv_throw; }               \
        vm->sp = frame->prev_sp;                                            \
        if (vm->fp <= entry_fp) {                                           \
          vm_result = osr_r;                                                \
          goto sv_leave;                                                    \
        }                                                                   \
        vm->fp--;                                                           \
        frame = &vm->frames[vm->fp];                                        \
        func = frame->func;                                                 \
        bp = frame->bp;                                                     \
        lp = frame->lp;                                                     \
        ip = frame->ip;                                                     \
        vm->stack[vm->sp++] = osr_r;                                        \
        DISPATCH();                                                         \
      }                                                                     \
      }                                                                     \
    }                                                                       \
  } while (0)
  #else
  #define JIT_OSR_BACK_EDGE() ((void)0)
  #endif
  if (resuming) {
    bool yield_star_resume = ip && (
      *ip == OP_YIELD_STAR_NEXT ||
      *ip == OP_YIELD_STAR_THROW ||
      *ip == OP_YIELD_STAR_RETURN
    );
    if (suspended_resume_kind == SV_RESUME_THROW && !yield_star_resume) {
      sv_err = js_throw(js, suspended_resume_value);
      goto sv_throw;
    }
    if (suspended_resume_kind == SV_RESUME_RETURN && !yield_star_resume) {
      vm->stack[vm->sp++] = suspended_resume_value;
      goto L_RETURN;
    }
    vm->stack[vm->sp++] = suspended_resume_value;
  }
  DISPATCH();

  L_CONST:     { sv_op_const(vm, func, ip);   NEXT(5); }
  L_CONST_I8:  { sv_op_const_i8(vm, ip);      NEXT(2); }
  L_CONST8:    { sv_op_const8(vm, func, ip);  NEXT(2); }
  L_UNDEF:     { sv_op_undef(vm);             NEXT(1); }
  L_NULL:      { sv_op_null(vm);              NEXT(1); }
  L_TRUE:      { sv_op_true(vm);              NEXT(1); }
  L_FALSE:     { sv_op_false(vm);             NEXT(1); }
  L_THIS:      { sv_op_this(vm, frame);       NEXT(1); }
  L_GLOBAL:    { sv_op_global(vm, js);        NEXT(1); }
  L_OBJECT:    { sv_op_object(vm, js, func, ip);        NEXT(1); }
  L_ARRAY:     { sv_op_array(vm, js, ip);     NEXT(3); }
  
  L_REGEXP:   { sv_op_regexp(vm, js);                    NEXT(1); }
  L_CLOSURE:  { VM_CHECK(sv_op_closure(vm, js, frame, func, ip));  NEXT(5); }

  L_POP:      { sv_op_pop(vm);      NEXT(1); }
  L_DUP:      { sv_op_dup(vm);      NEXT(1); }
  L_DUP2:     { sv_op_dup2(vm);     NEXT(1); }
  L_SWAP:     { sv_op_swap(vm);     NEXT(1); }
  L_ROT3L:    { sv_op_rot3l(vm);    NEXT(1); }
  L_ROT3R:    { sv_op_rot3r(vm);    NEXT(1); }
  L_NIP:      { sv_op_nip(vm);      NEXT(1); }
  L_NIP2:     { sv_op_nip2(vm);     NEXT(1); }
  L_INSERT2:  { sv_op_insert2(vm);  NEXT(1); }
  L_INSERT3:  { sv_op_insert3(vm);  NEXT(1); }
  
  L_SWAP_UNDER:  { sv_op_swap_under(vm);   NEXT(1); }
  L_ROT4_UNDER:  { sv_op_rot4_under(vm);   NEXT(1); }

  L_GET_LOCAL:        { VM_CHECK(sv_op_get_local(vm, lp, js, frame, ip));   NEXT(3); }
  L_PUT_LOCAL:        { sv_op_put_local(vm, lp, frame, func, ip);           NEXT(3); }
  L_SET_LOCAL:        { sv_op_set_local(vm, lp, frame, func, ip);           NEXT(3); }
  L_GET_LOCAL8:       { VM_CHECK(sv_op_get_local8(vm, lp, js, frame, ip));  NEXT(2); }
  L_PUT_LOCAL8:       { sv_op_put_local8(vm, lp, frame, func, ip);          NEXT(2); }
  L_SET_LOCAL8:       { sv_op_set_local8(vm, lp, frame, func, ip);          NEXT(2); }
  L_SET_LOCAL_UNDEF:  { sv_op_set_local_undef(frame, lp, ip);               NEXT(3); }
  
  L_GET_LOCAL_CHK:  { VM_CHECK(sv_op_get_local_chk(vm, lp, js, frame, func, ip));  NEXT(7); }
  L_PUT_LOCAL_CHK:  { VM_CHECK(sv_op_put_local_chk(vm, lp, js, frame, func, ip));  NEXT(7); }
  L_GET_SLOT_RAW:   { VM_CHECK(sv_op_get_slot_raw(vm, js, frame, ip));             NEXT(3); }

  L_GET_ARG:  { VM_CHECK(sv_op_get_arg(vm, js, frame, ip));  NEXT(3); }
  L_PUT_ARG:  { sv_op_put_arg(vm, js, frame, ip);            NEXT(3); }
  L_SET_ARG:  { sv_op_set_arg(vm, js, frame, ip);            NEXT(3); }
  L_REST:     { sv_op_rest(vm, frame, js, ip);               NEXT(3); }

  L_GET_UPVAL:    { VM_CHECK(sv_op_get_upval(vm, frame, js, ip));  NEXT(3); }
  L_PUT_UPVAL:    { sv_op_put_upval(vm, frame, ip);                NEXT(3); }
  L_SET_UPVAL:    { sv_op_set_upval(vm, frame, ip);                NEXT(3); }
  L_CLOSE_UPVAL:  { VM_CHECK(sv_op_close_upval(vm, frame, ip));    NEXT(3); }

  L_GET_GLOBAL:        { VM_CHECK(sv_op_get_global(vm, js, func, ip));         NEXT(7); }
  L_GET_GLOBAL_UNDEF:  { sv_op_get_global_undef(vm, js, func, ip);             NEXT(7); }
  L_PUT_GLOBAL:        { VM_CHECK(sv_op_put_global(vm, js, frame, func, ip));  NEXT(5); }

  L_GET_FIELD:     { VM_CHECK(sv_op_get_field(vm, js, func, ip));   NEXT(7); }
  L_GET_FIELD2:    { VM_CHECK(sv_op_get_field2(vm, js, func, ip));  NEXT(7); }
  L_PUT_FIELD:     { VM_CHECK(sv_op_put_field(vm, js, func, ip));   NEXT(7); }
  L_GET_ELEM:      { VM_CHECK(sv_op_get_elem(vm, js, func, ip));    NEXT(1); }
  L_GET_ELEM2:     { VM_CHECK(sv_op_get_elem2(vm, js, func, ip));   NEXT(1); }
  L_PUT_ELEM:      { VM_CHECK(sv_op_put_elem(vm, js));              NEXT(1); }
  L_DEFINE_FIELD:  { sv_op_define_field(vm, js, func, ip);          NEXT(5); }
  L_GET_LENGTH:    { VM_CHECK(sv_op_get_length(vm, js));            NEXT(1); }

  L_GET_FIELD_OPT:  { VM_CHECK(sv_op_get_field_opt(vm, js, func, ip));  NEXT(5); }
  L_GET_ELEM_OPT:   { VM_CHECK(sv_op_get_elem_opt(vm, js, func, ip));   NEXT(1); }

  L_GET_PRIVATE:  { sv_op_get_private(vm, js);  NEXT(1); }
  L_PUT_PRIVATE:  { sv_op_put_private(vm, js);  NEXT(1); }
  L_DEF_PRIVATE:  { sv_op_def_private(vm, js);  NEXT(1); }

  L_GET_SUPER:      { sv_op_get_super(vm, js);             NEXT(1); }
  L_GET_SUPER_VAL:  { sv_op_get_super_val(vm, js, frame);  NEXT(1); }
  L_PUT_SUPER_VAL:  { sv_op_put_super_val(vm, js);         NEXT(1); }

  L_ADD: {
    ant_value_t r = vm->stack[vm->sp - 1], l = vm->stack[vm->sp - 2];
    sv_tfb_record2(func, ip, l, r);
    if (__builtin_expect(vtype(l) == T_NUM && vtype(r) == T_NUM, 1)) {
      vm->sp--; vm->stack[vm->sp - 1] = tov(tod(l) + tod(r)); NEXT(1);
    }
    VM_CHECK(sv_op_add(vm, js)); NEXT(1);
  }

  L_ADD_NUM: {
    ant_value_t r = vm->stack[--vm->sp];
    ant_value_t l = vm->stack[vm->sp - 1];
    vm->stack[vm->sp - 1] = tov(tod(l) + tod(r));
    NEXT(1);
  }
  
  L_SUB: {
    ant_value_t r = vm->stack[vm->sp - 1], l = vm->stack[vm->sp - 2];
    sv_tfb_record2(func, ip, l, r);
    if (__builtin_expect(vtype(l) == T_NUM && vtype(r) == T_NUM, 1)) {
      vm->sp--; vm->stack[vm->sp - 1] = tov(tod(l) - tod(r)); NEXT(1);
    }
    VM_CHECK(sv_op_sub(vm, js)); NEXT(1);
  }

  L_SUB_NUM: {
    ant_value_t r = vm->stack[--vm->sp];
    ant_value_t l = vm->stack[vm->sp - 1];
    vm->stack[vm->sp - 1] = tov(tod(l) - tod(r));
    NEXT(1);
  }

  L_MUL_NUM: {
    ant_value_t r = vm->stack[--vm->sp];
    ant_value_t l = vm->stack[vm->sp - 1];
    vm->stack[vm->sp - 1] = tov(tod(l) * tod(r));
    NEXT(1);
  }

  L_DIV_NUM: {
    ant_value_t r = vm->stack[--vm->sp];
    ant_value_t l = vm->stack[vm->sp - 1];
    vm->stack[vm->sp - 1] = tov(tod(l) / tod(r));
    NEXT(1);
  }
  
  L_MUL:        { sv_tfb_record2(func, ip, vm->stack[vm->sp-2], vm->stack[vm->sp-1]); VM_CHECK(sv_op_mul(vm, js));                      NEXT(1); }
  L_DIV:        { sv_tfb_record2(func, ip, vm->stack[vm->sp-2], vm->stack[vm->sp-1]); VM_CHECK(sv_op_div(vm, js));                      NEXT(1); }
  L_MOD:        { sv_tfb_record2(func, ip, vm->stack[vm->sp-2], vm->stack[vm->sp-1]); VM_CHECK(sv_op_mod(vm, js));                      NEXT(1); }
  L_NEG:        { sv_tfb_record1(func, ip, vm->stack[vm->sp-1]); VM_CHECK(sv_op_neg(vm, js));                                           NEXT(1); }
  L_ADD_LOCAL:  { sv_tfb_record2(func, ip, lp[sv_get_u8(ip+1)], vm->stack[vm->sp-1]); VM_CHECK(sv_op_add_local(vm, lp, js, func, ip));  NEXT(2); }
  
  L_STR_APPEND_LOCAL: { VM_CHECK(sv_op_str_append_local(vm, js, frame, func, ip));           NEXT(3); }
  L_STR_ALC_SNAPSHOT: { VM_CHECK(sv_op_str_append_local_snapshot(vm, js, frame, func, ip));  NEXT(3); }
  L_STR_FLUSH_LOCAL:  { VM_CHECK(sv_op_str_flush_local(vm, js, frame, ip));                  NEXT(3); }

  L_EXP:        { VM_CHECK(sv_op_exp(vm, js));    NEXT(1); }
  L_UPLUS:      { VM_CHECK(sv_op_uplus(vm, js));  NEXT(1); }
  L_INC:        { sv_op_inc(vm);                  NEXT(1); }
  L_DEC:        { sv_op_dec(vm);                  NEXT(1); }
  L_POST_INC:   { sv_op_post_inc(vm);             NEXT(1); }
  L_POST_DEC:   { sv_op_post_dec(vm);             NEXT(1); }
  
  L_INC_LOCAL:  { VM_CHECK(sv_op_inc_local(lp, js, func, ip));  NEXT(2); }
  L_DEC_LOCAL:  { VM_CHECK(sv_op_dec_local(lp, js, func, ip));  NEXT(2); }

  L_EQ:   { sv_op_eq(vm, js);   NEXT(1); }
  L_NE:   { sv_op_ne(vm, js);   NEXT(1); }
  L_SEQ:  { sv_op_seq(vm, js);  NEXT(1); }
  L_SNE:  { sv_op_sne(vm, js);  NEXT(1); }
  
  L_LT: {
    ant_value_t r = vm->stack[vm->sp - 1], l = vm->stack[vm->sp - 2];
    sv_tfb_record2(func, ip, l, r);
    if (__builtin_expect(vtype(l) == T_NUM && vtype(r) == T_NUM, 1)) {
      vm->sp--; vm->stack[vm->sp - 1] = mkval(T_BOOL, tod(l) < tod(r)); NEXT(1);
    }
    VM_CHECK(sv_op_lt(vm, js)); NEXT(1);
  }
  
  L_LE:  { sv_tfb_record2(func, ip, vm->stack[vm->sp-2], vm->stack[vm->sp-1]); VM_CHECK(sv_op_le(vm, js));  NEXT(1); }
  L_GT:  { sv_tfb_record2(func, ip, vm->stack[vm->sp-2], vm->stack[vm->sp-1]); VM_CHECK(sv_op_gt(vm, js));  NEXT(1); }
  L_GE:  { sv_tfb_record2(func, ip, vm->stack[vm->sp-2], vm->stack[vm->sp-1]); VM_CHECK(sv_op_ge(vm, js));  NEXT(1); }
  
  L_INSTANCEOF:        { VM_CHECK(sv_op_instanceof(vm, js, func, ip));  NEXT(3); }
  L_IN:                { VM_CHECK(sv_op_in(vm, js));          NEXT(1); }
  L_IS_NULLISH:        { sv_op_is_nullish(vm);                NEXT(1); }
  L_IS_UNDEF_OR_NULL:  { sv_op_is_undef_or_null(vm);          NEXT(1); }

  L_BAND:  { VM_CHECK(sv_op_band(vm, js));  NEXT(1); }
  L_BOR:   { VM_CHECK(sv_op_bor(vm, js));   NEXT(1); }
  L_BXOR:  { VM_CHECK(sv_op_bxor(vm, js));  NEXT(1); }
  L_BNOT:  { VM_CHECK(sv_op_bnot(vm, js));  NEXT(1); }
  L_SHL:   { VM_CHECK(sv_op_shl(vm, js));   NEXT(1); }
  L_SHR:   { VM_CHECK(sv_op_shr(vm, js));   NEXT(1); }
  L_USHR:  { VM_CHECK(sv_op_ushr(vm, js));  NEXT(1); }

  L_NOT:         { sv_op_not(vm, js);                   NEXT(1); }
  L_TYPEOF:      { sv_op_typeof(vm, js);                NEXT(1); }
  L_VOID:        { sv_op_void(vm);                      NEXT(1); }
  L_DELETE:      { VM_CHECK(sv_op_delete(vm, js));      NEXT(1); }
  L_DELETE_VAR:  { sv_op_delete_var(vm, js, func, ip);  NEXT(5); }

  L_JMP: {
    uint8_t *prev = ip; ip = sv_op_jmp(ip);
    if (ip <= prev) {
      gc_maybe(js);
      JIT_OSR_BACK_EDGE();
    }
    DISPATCH();
  }
  
  L_JMP_FALSE: {
    uint8_t *prev = ip;
    ant_value_t v = vm->stack[--vm->sp];
    if (__builtin_expect(vtype(v) == T_BOOL, 1)) {
      ip = vdata(v) 
        ? ip + sv_op_size[OP_JMP_FALSE]
        : ip + sv_op_size[OP_JMP_FALSE] + sv_get_i32(ip + 1);
    } else {
      ip = js_truthy(js, v) 
        ? ip + sv_op_size[OP_JMP_FALSE]
        : ip + sv_op_size[OP_JMP_FALSE] + sv_get_i32(ip + 1);
    }
    if (ip <= prev) {
      js->prop_refs_len = 0;
      JIT_OSR_BACK_EDGE();
    }
    DISPATCH();
  }
  
  L_JMP_TRUE: {
    uint8_t *prev = ip;
    ant_value_t v = vm->stack[--vm->sp];
    if (__builtin_expect(vtype(v) == T_BOOL, 1)) {
      ip = vdata(v) 
        ? ip + sv_op_size[OP_JMP_TRUE] + sv_get_i32(ip + 1)
        : ip + sv_op_size[OP_JMP_TRUE];
    } else {
      ip = js_truthy(js, v) 
        ? ip + sv_op_size[OP_JMP_TRUE] + sv_get_i32(ip + 1)
        : ip + sv_op_size[OP_JMP_TRUE];
    }
    if (ip <= prev) {
      js->prop_refs_len = 0;
      JIT_OSR_BACK_EDGE();
    }
    DISPATCH();
  }
  
  L_JMP_FALSE_PEEK: { 
    uint8_t *prev = ip; ip = sv_op_jmp_false_peek(vm, js, ip);
    if (ip <= prev) {
      js->prop_refs_len = 0;
      JIT_OSR_BACK_EDGE();
    }
    DISPATCH();
  }
  
  L_JMP_TRUE_PEEK: { 
    uint8_t *prev = ip; ip = sv_op_jmp_true_peek(vm, js, ip);
    if (ip <= prev) {
      js->prop_refs_len = 0;
      JIT_OSR_BACK_EDGE();
    }
    DISPATCH();
  }
  
  L_JMP_NOT_NULLISH: { 
    uint8_t *prev = ip; ip = sv_op_jmp_not_nullish(vm, ip);
    if (ip <= prev) {
      js->prop_refs_len = 0;
      JIT_OSR_BACK_EDGE();
    }
    DISPATCH();
  }
   
  L_JMP8: {
    uint8_t *prev = ip; ip = sv_op_jmp8(ip);
    if (ip <= prev) {
      js->prop_refs_len = 0;
      JIT_OSR_BACK_EDGE();
    }
    DISPATCH();
  }
  
  L_JMP_FALSE8: { 
    uint8_t *prev = ip; ip = sv_op_jmp_false8(vm, js, ip);
    if (ip <= prev) {
      JIT_OSR_BACK_EDGE();
    }
    DISPATCH();
  }
  
  L_JMP_TRUE8:  { 
    uint8_t *prev = ip; ip = sv_op_jmp_true8(vm, js, ip);
    if (ip <= prev) {
      JIT_OSR_BACK_EDGE();
    }
    DISPATCH();
  }

  L_CALL: {
    uint16_t call_argc = sv_get_u16(ip + 1);
    ant_value_t *call_args = &vm->stack[vm->sp - call_argc];
    ant_value_t call_func = vm->stack[vm->sp - call_argc - 1];
    
    bool is_super_call =
      (vtype(frame->super_val) != T_UNDEF && call_func == frame->super_val);
    ant_value_t call_this = is_super_call ? frame->this : js_mkundef();

    if (!is_super_call && vtype(frame->new_target) == T_UNDEF && vtype(call_func) == T_FUNC) {
      sv_closure_t *closure = js_func_closure(call_func);
      if (closure->func != NULL) {
        if (closure->call_flags & (SV_CALL_HAS_BOUND_ARGS | SV_CALL_HAS_SUPER))
          goto call_fallback;
        if (closure->func->is_async || closure->func->is_generator) goto call_fallback;
        #ifdef ANT_JIT
        {
          ant_value_t jit_this = (
            closure->func->is_arrow || vtype(closure->bound_this) != T_UNDEF)
            ? closure->bound_this : js_mkundef();
          ant_value_t jit_result = sv_try_direct_closure_jit(
            vm, js, func, ip, frame, closure, jit_this, call_args, (int)call_argc);
          if (jit_result != SV_JIT_RETRY_INTERP) {
            vm->sp -= call_argc + 1;
            if (is_err(jit_result)) { sv_err = jit_result; goto sv_throw; }
            vm->stack[vm->sp++] = jit_result;
            ip = frame->ip;
            DISPATCH();
          }
        }
        #endif
        if (vm->fp + 1 >= vm->max_frames && !sv_vm_grow_frames(vm)) {
          sv_err = js_mkerr_typed(js, JS_ERR_RANGE | JS_ERR_NO_STACK, "Maximum call stack size exceeded");
          goto sv_throw;
        }
        if (closure->func->is_arrow || vtype(closure->bound_this) != T_UNDEF) call_this = closure->bound_this;
        frame = &vm->frames[vm->fp];
        frame->ip = ip + 3;
        vm->sp -= call_argc + 1;
        vm->fp++;
        frame = &vm->frames[vm->fp];
        func = closure->func;
        frame->func = func;
        frame->callee = call_func;
        frame->this = sv_normalize_this_for_frame(js, func, call_this);
        frame->new_target = js_mkundef();
        frame->super_val = js_mkundef();
        frame->prev_sp = vm->sp;
        frame->handler_base = vm->handler_depth;
        frame->handler_top = vm->handler_depth;
        frame->argc = call_argc;
        frame->arguments_obj = js_mkundef();
        ant_value_t *call_bp = NULL;
        ant_value_t *call_lp = NULL;
        ant_value_t call_stage_err = sv_stage_frame_args(
          vm, js, func, call_args, (int)call_argc, &call_bp, &call_lp
        );
        if (is_err(call_stage_err)) {
          vm->fp--;
          vm->sp += call_argc + 1;
          sv_err = call_stage_err;
          goto sv_throw;
        }
        frame->bp = call_bp;
        frame->lp = call_lp;
        frame->upvalues = closure->upvalues;
        frame->upvalue_count = closure->func->upvalue_count;
        bp = frame->bp;
        lp = frame->lp;
        
        ip = func->code;
        DISPATCH();
      }
    }
    call_fallback:;
    frame->ip = ip;
    ant_value_t super_this_c = call_this;
    ant_value_t call_result = sv_vm_call(
      vm, js, call_func, call_this, call_args, call_argc,
      is_super_call ? &super_this_c : NULL, is_super_call);
    sv_sync_frame_locals(vm, &frame, &func, &bp, &lp);
    vm->sp -= call_argc + 1;
    if (is_err(call_result)) { sv_err = call_result; goto sv_throw; }
    if (is_super_call)
      frame->this = is_object_type(call_result) ? call_result : super_this_c;
    vm->stack[vm->sp++] = call_result;
    NEXT(3);
  }

  L_CALL_METHOD: {
    uint16_t call_argc = sv_get_u16(ip + 1);
    ant_value_t *call_args = &vm->stack[vm->sp - call_argc];
    ant_value_t call_func = vm->stack[vm->sp - call_argc - 1];
    ant_value_t call_this = vm->stack[vm->sp - call_argc - 2];
    
    bool is_super_call =
      (vtype(frame->super_val) != T_UNDEF && call_func == frame->super_val);

    if (!is_super_call && vtype(frame->new_target) == T_UNDEF &&
        vtype(call_func) == T_FUNC) {
      sv_closure_t *closure = js_func_closure(call_func);
      if (closure->func != NULL) {
        if (closure->call_flags & (SV_CALL_HAS_BOUND_ARGS | SV_CALL_HAS_SUPER))
          goto call_method_fallback;
        if (closure->func->is_async || closure->func->is_generator) goto call_method_fallback;
        #ifdef ANT_JIT
        {
          ant_value_t jit_this = (
            closure->func->is_arrow || vtype(closure->bound_this) != T_UNDEF)
            ? closure->bound_this : call_this;
          ant_value_t jit_result = sv_try_direct_closure_jit(
            vm, js, func, ip, frame, closure, jit_this, call_args, (int)call_argc);
          if (jit_result != SV_JIT_RETRY_INTERP) {
            vm->sp -= call_argc + 2;
            if (is_err(jit_result)) { sv_err = jit_result; goto sv_throw; }
            vm->stack[vm->sp++] = jit_result;
            ip = frame->ip;
            DISPATCH();
          }
        }
        #endif
        if (vm->fp + 1 >= vm->max_frames && !sv_vm_grow_frames(vm)) {
          sv_err = js_mkerr_typed(js, JS_ERR_RANGE | JS_ERR_NO_STACK, "Maximum call stack size exceeded");
          goto sv_throw;
        }
        if (closure->func->is_arrow || vtype(closure->bound_this) != T_UNDEF)
          call_this = closure->bound_this;
        frame = &vm->frames[vm->fp];
        frame->ip = ip + 3;
        vm->sp -= call_argc + 2;
        vm->fp++;
        frame = &vm->frames[vm->fp];
        func = closure->func;
        frame->func = func;
        frame->callee = call_func;
        frame->this = sv_normalize_this_for_frame(js, func, call_this);
        frame->new_target = js_mkundef();
        frame->super_val = js_mkundef();
        frame->prev_sp = vm->sp;
        frame->handler_base = vm->handler_depth;
        frame->handler_top = vm->handler_depth;
        frame->argc = call_argc;
        frame->arguments_obj = js_mkundef();
        ant_value_t *call_bp = NULL;
        ant_value_t *call_lp = NULL;
        ant_value_t call_stage_err = sv_stage_frame_args(
          vm, js, func, call_args, (int)call_argc, &call_bp, &call_lp
        );
        if (is_err(call_stage_err)) {
          vm->fp--;
          vm->sp += call_argc + 2;
          sv_err = call_stage_err;
          goto sv_throw;
        }
        frame->bp = call_bp;
        frame->lp = call_lp;
        frame->upvalues = closure->upvalues;
        frame->upvalue_count = closure->func->upvalue_count;
        bp = frame->bp;
        lp = frame->lp;
        ip = func->code;
        DISPATCH();
      }
    }
    call_method_fallback:;
    frame->ip = ip;
    if (is_super_call) js->new_target = frame->new_target;
    ant_value_t super_this_cm = call_this;
    ant_value_t call_result = sv_vm_call(
      vm, js, call_func, call_this, call_args, call_argc,
      is_super_call ? &super_this_cm : NULL, is_super_call);
    sv_sync_frame_locals(vm, &frame, &func, &bp, &lp);
    vm->sp -= call_argc + 2;
    if (is_err(call_result)) { sv_err = call_result; goto sv_throw; }
    if (is_super_call)
      frame->this = is_object_type(call_result) ? call_result : super_this_cm;
    vm->stack[vm->sp++] = call_result;
    NEXT(3);
  }

  L_CALL_ARRAY_INCLUDES: {
    uint16_t call_argc = sv_get_u16(ip + 1);
    ant_value_t *call_args = &vm->stack[vm->sp - call_argc];
    ant_value_t call_func = vm->stack[vm->sp - call_argc - 1];
    ant_value_t call_this = vm->stack[vm->sp - call_argc - 2];
    ant_value_t call_result;

    frame->ip = ip;
    if (js_is_array_includes_builtin(call_func)) {
      call_result = js_array_includes_call(js, call_this, call_args, call_argc);
    } else call_result = sv_vm_call(vm, js, call_func, call_this, call_args, call_argc, NULL, false);
    sv_sync_frame_locals(vm, &frame, &func, &bp, &lp);
    
    vm->sp -= call_argc + 2;
    if (is_err(call_result)) { sv_err = call_result; goto sv_throw; }
    vm->stack[vm->sp++] = call_result;
    
    NEXT(3);
  }

  L_CALL_IS_PROTO: {
    ant_value_t call_arg  = vm->stack[vm->sp - 1];
    ant_value_t call_func = vm->stack[vm->sp - 2];
    ant_value_t call_this = vm->stack[vm->sp - 3];
    ant_value_t call_result;

    if (
      vtype(call_func) == T_CFUNC &&
      js_cfunc_same_entrypoint(call_func, builtin_object_isPrototypeOf)
    ) call_result = sv_isproto_ic_eval(js, call_this, call_arg, func, ip); else {
      ant_value_t call_args[1] = { call_arg };
      frame->ip = ip;
      call_result = sv_vm_call(vm, js, call_func, call_this, call_args, 1, NULL, false);
      sv_sync_frame_locals(vm, &frame, &func, &bp, &lp);
    }

    vm->sp -= 3;
    if (is_err(call_result)) { sv_err = call_result; goto sv_throw; }
    vm->stack[vm->sp++] = call_result;
    NEXT(3);
  }

  L_RE_EXEC_TRUTHY: {
    ant_value_t call_arg  = vm->stack[vm->sp - 1];
    ant_value_t call_func = vm->stack[vm->sp - 2];
    ant_value_t call_this = vm->stack[vm->sp - 3];
    ant_value_t call_result;

    if (!regexp_exec_truthy_try_fast(js, call_func, call_this, call_arg, &call_result)) {
      ant_value_t call_args[1] = { call_arg }; frame->ip = ip;
      ant_value_t raw_result = sv_vm_call(vm, js, call_func, call_this, call_args, 1, NULL, false);
      sv_sync_frame_locals(vm, &frame, &func, &bp, &lp);
      if (is_err(raw_result)) call_result = raw_result;
      else call_result = mkval(T_BOOL, js_truthy(js, raw_result) ? 1 : 0);
    }
    
    vm->sp -= 3;
    if (is_err(call_result)) { sv_err = call_result; goto sv_throw; }
    vm->stack[vm->sp++] = call_result;
    NEXT(1);
  }

  L_TAIL_CALL: {
    uint16_t call_argc = sv_get_u16(ip + 1);
    ant_value_t call_func = vm->stack[vm->sp - call_argc - 1];
    tc_this = js_mkundef();

    if (vm->handler_depth == frame->handler_base &&
        vtype(frame->new_target) == T_UNDEF &&
        vtype(call_func) == T_FUNC) {
      sv_closure_t *closure = js_func_closure(call_func);
      if (closure->func != NULL) {
        if (!closure->func->is_async && !closure->func->is_generator &&
            !(closure->call_flags & (SV_CALL_HAS_BOUND_ARGS | SV_CALL_HAS_SUPER))) {
          if (closure->func->is_arrow || vtype(closure->bound_this) != T_UNDEF)
            tc_this = closure->bound_this;
          goto tail_call_inline;
        }
      }
    }
    ant_value_t *call_args = &vm->stack[vm->sp - call_argc];
    frame->ip = ip;
    ant_value_t call_result = sv_vm_call(
      vm, js, call_func, tc_this, call_args, call_argc, NULL, false);
    sv_sync_frame_locals(vm, &frame, &func, &bp, &lp);
    vm->sp -= call_argc + 1;
    if (is_err(call_result)) { sv_err = call_result; goto sv_throw; }
    vm->stack[vm->sp++] = call_result;
    goto L_RETURN;
  }

  L_TAIL_CALL_METHOD: {
    uint16_t call_argc = sv_get_u16(ip + 1);
    ant_value_t call_func = vm->stack[vm->sp - call_argc - 1];
    tc_this = vm->stack[vm->sp - call_argc - 2];

    if (vm->handler_depth == frame->handler_base &&
        vtype(frame->new_target) == T_UNDEF &&
        vtype(call_func) == T_FUNC) {
      sv_closure_t *closure = js_func_closure(call_func);
      if (closure->func != NULL) {
        if (!closure->func->is_async && !closure->func->is_generator &&
            !(closure->call_flags & (SV_CALL_HAS_BOUND_ARGS | SV_CALL_HAS_SUPER))) {
          if (closure->func->is_arrow || vtype(closure->bound_this) != T_UNDEF)
            tc_this = closure->bound_this;
          goto tail_call_inline;
        }
      }
    }
    ant_value_t *call_args = &vm->stack[vm->sp - call_argc];
    ant_value_t call_result = sv_vm_call(
      vm, js, call_func, tc_this, call_args, call_argc, NULL, false);
    sv_sync_frame_locals(vm, &frame, &func, &bp, &lp);
    vm->sp -= call_argc + 2;
    if (is_err(call_result)) { sv_err = call_result; goto sv_throw; }
    vm->stack[vm->sp++] = call_result;
    goto L_RETURN;
  }

  tail_call_inline: {
    uint16_t call_argc = sv_get_u16(ip + 1);
    ant_value_t *call_args = &vm->stack[vm->sp - call_argc];
    ant_value_t call_func = vm->stack[vm->sp - call_argc - 1];
    sv_closure_t *closure = js_func_closure(call_func);
    if (frame->bp && vm->open_upvalues) sv_close_upvalues_from_slot(vm, frame->bp);
    sv_drop_frame_runtime_state(js, frame);
    vm->sp = frame->prev_sp;
    int arg_slots = (
      (int)call_argc > closure->func->param_count)
      ? (int)call_argc : closure->func->param_count;
    int need = arg_slots + closure->func->max_locals;
    ant_value_t *base = &vm->stack[vm->sp];
    memmove(base, call_args, (size_t)call_argc * sizeof(ant_value_t));
    for (int i = (int)call_argc; i < arg_slots; i++) base[i] = js_mkundef();
    ant_value_t *new_lp = base + arg_slots;
    for (int i = 0; i < closure->func->max_locals; i++) new_lp[i] = js_mkundef();
    vm->sp = frame->prev_sp + need;
    func = closure->func;
    frame->func = func;
    frame->callee = call_func;
    frame->this = sv_normalize_this_for_frame(js, func, tc_this);
    frame->new_target = js_mkundef();
    frame->super_val = js_mkundef();
    frame->argc = call_argc;
    frame->handler_base = vm->handler_depth;
    frame->handler_top = vm->handler_depth;
    frame->arguments_obj = js_mkundef();
    frame->bp = base;
    frame->lp = new_lp;
    frame->upvalues = closure->upvalues;
    frame->upvalue_count = closure->func->upvalue_count;
    bp = frame->bp;
    lp = frame->lp;
    ip = func->code;
    DISPATCH();
  }
  
  L_NEW:       { VM_CHECK(sv_op_new(vm, js, ip));          NEXT(3); }
  L_APPLY:       { VM_CHECK(sv_op_apply(vm, js, ip));              NEXT(3); }
  L_SUPER_APPLY: { VM_CHECK(sv_op_super_apply(vm, js, frame, ip)); NEXT(3); }
  L_NEW_APPLY:   { VM_CHECK(sv_op_new_apply(vm, js, ip));          NEXT(3); }
  L_EVAL:        { VM_CHECK(sv_op_eval(vm, js, frame, ip));        NEXT(5); }

  // TODO: make the methods below DRY
  L_RETURN: {
    ant_value_t r = vm->stack[--vm->sp];
    if (__builtin_expect(vm->handler_depth != frame->handler_base, 0)) {
      uint8_t *finally_ip = sv_vm_unwind_for_return(vm, r);
      if (finally_ip) {
        frame = &vm->frames[vm->fp];
        func = frame->func;
        bp = frame->bp;
        lp = frame->lp;
        ip = finally_ip;
        DISPATCH();
      }
      vm->handler_depth = frame->handler_base;
      frame->handler_top = frame->handler_base;
    }
    vm->sp = frame->prev_sp;
    if (vm->fp <= entry_fp) {
      vm_result = r;
      goto sv_leave;
    }
    sv_drop_frame_runtime_state(js, frame);
    vm->fp--;
    frame = &vm->frames[vm->fp];
    func = frame->func;
    bp = frame->bp;
    lp = frame->lp;
    ip = frame->ip;
    vm->stack[vm->sp++] = r;
    DISPATCH();
  }

  L_RETURN_UNDEF: {
    ant_value_t r = js_mkundef();
    if (__builtin_expect(vm->handler_depth != frame->handler_base, 0)) {
      uint8_t *finally_ip = sv_vm_unwind_for_return(vm, r);
      if (finally_ip) {
        frame = &vm->frames[vm->fp];
        func = frame->func;
        bp = frame->bp;
        lp = frame->lp;
        ip = finally_ip;
        DISPATCH();
      }
      vm->handler_depth = frame->handler_base;
      frame->handler_top = frame->handler_base;
    }
    vm->sp = frame->prev_sp;
    if (vm->fp <= entry_fp) {
      vm_result = r;
      goto sv_leave;
    }
    sv_drop_frame_runtime_state(js, frame);
    vm->fp--;
    frame = &vm->frames[vm->fp];
    func = frame->func;
    bp = frame->bp;
    lp = frame->lp;
    ip = frame->ip;
    vm->stack[vm->sp++] = r;
    DISPATCH();
  }

  L_RETURN_ASYNC: {
    ant_value_t r = vm->stack[--vm->sp];
    if (__builtin_expect(vm->handler_depth != frame->handler_base, 0)) {
      uint8_t *finally_ip = sv_vm_unwind_for_return(vm, r);
      if (finally_ip) {
        frame = &vm->frames[vm->fp];
        func = frame->func;
        bp = frame->bp;
        lp = frame->lp;
        ip = finally_ip;
        DISPATCH();
      }
      vm->handler_depth = frame->handler_base;
      frame->handler_top = frame->handler_base;
    }
    vm->sp = frame->prev_sp;
    if (vm->fp <= entry_fp) {
      vm_result = r;
      goto sv_leave;
    }
    sv_drop_frame_runtime_state(js, frame);
    vm->fp--;
    frame = &vm->frames[vm->fp];
    func = frame->func;
    bp = frame->bp;
    lp = frame->lp;
    ip = frame->ip;
    vm->stack[vm->sp++] = r;
    DISPATCH();
  }

  L_CHECK_CTOR:      { VM_CHECK(sv_op_check_ctor(vm, js));  NEXT(1); }
  L_CHECK_CTOR_RET:  { sv_op_check_ctor_ret(vm, frame);     NEXT(1); }
  
  L_HALT: {
    vm_result = sv_op_halt(vm, frame);
    goto sv_leave;
  }

  L_THROW:       { sv_err = sv_op_throw(vm);                      goto sv_throw; }
  L_THROW_ERROR: { sv_err = sv_op_throw_error(vm, js, func, ip);  goto sv_throw; }
  
  L_TRY_PUSH:    { sv_op_try_push(vm, ip);               NEXT(5); }
  L_TRY_POP:     { sv_op_try_pop(vm);                    NEXT(1); }
  L_CATCH:       { sv_op_catch(vm, sv_err, ip);          NEXT(5); }
  L_FINALLY:     { VM_CHECK(sv_op_finally(vm, js, ip));  NEXT(5); }
  L_NIP_CATCH:   { sv_op_nip_catch(vm);                  NEXT(1); }

  L_FINALLY_RET: {
    uint8_t *resume_ip = NULL;
    ant_value_t completion = js_mkundef();
    sv_finally_ret_t action = sv_op_finally_ret(vm, js, &resume_ip, &completion);
    if (action == SV_FINALLY_RET_ERROR || action == SV_FINALLY_RET_THROW) {
      sv_err = completion;
      goto sv_throw;
    }
    if (action == SV_FINALLY_RET_RETURN) {
      vm->handler_depth = frame->handler_base;
      frame->handler_top = frame->handler_base;
      vm->sp = frame->prev_sp;
      if (vm->fp <= entry_fp) {
        vm_result = completion;
        goto sv_leave;
      }
      sv_drop_frame_runtime_state(js, frame);
      vm->fp--;
      frame = &vm->frames[vm->fp];
      func = frame->func;
      bp = frame->bp;
      lp = frame->lp;
      
      ip = frame->ip;
      vm->stack[vm->sp++] = completion;
      DISPATCH();
    }
    ip = resume_ip;
    DISPATCH();
  }
  
  L_USING_PUSH:                     { VM_CHECK(sv_op_using_push(vm, js, false));          NEXT(1); }
  L_USING_PUSH_ASYNC:               { VM_CHECK(sv_op_using_push(vm, js, true));           NEXT(1); }
  L_DISPOSE_RESOURCE:               { VM_CHECK(sv_op_dispose_resource(vm, js, false));    NEXT(1); }
  L_DISPOSE_RESOURCE_ASYNC:         { VM_CHECK(sv_op_dispose_resource(vm, js, true));     NEXT(1); }
  L_USING_DISPOSE:                  { VM_CHECK(sv_op_using_dispose(vm, js, false, false)); NEXT(1); }
  L_USING_DISPOSE_ASYNC:            { VM_CHECK(sv_op_using_dispose(vm, js, true, false));  NEXT(1); }
  L_USING_DISPOSE_SUPPRESSED:       { VM_CHECK(sv_op_using_dispose(vm, js, false, true));  NEXT(1); }
  L_USING_DISPOSE_ASYNC_SUPPRESSED: { VM_CHECK(sv_op_using_dispose(vm, js, true, true));   NEXT(1); }

  L_FOR_IN:           { VM_CHECK(sv_op_for_in(vm, js));           NEXT(1); }
  L_FOR_OF:           { VM_CHECK(sv_op_for_of(vm, js));           NEXT(1); }
  L_FOR_AWAIT_OF:     { VM_CHECK(sv_op_for_await_of(vm, js));     NEXT(1); }
  L_ITER_NEXT:        { VM_CHECK(sv_op_iter_next(vm, js, ip));    NEXT(2); }
  L_ITER_GET_VALUE:   { sv_op_iter_get_value(vm, js);             NEXT(1); }
  L_ITER_CLOSE:       { sv_op_iter_close(vm, js);                 NEXT(1); }
  L_ITER_CALL:        { VM_CHECK(sv_op_iter_call(vm, js, ip));    NEXT(2); }
  
  L_AWAIT_ITER_NEXT:  {
    sv_await_result_t await_result = sv_op_await_iter_next(vm, js);
    if (await_result.state == SV_AWAIT_ERROR) {
      sv_err = await_result.value;
      goto sv_throw;
    }
    if (await_result.state == SV_AWAIT_SUSPENDED) {
      if (await_result.handoff) vm_result = js_mkundef();
      goto sv_leave;
    }
    NEXT(1);
  }
  
  L_DESTRUCTURE_INIT: { VM_CHECK(sv_op_destructure_init(vm, js)); NEXT(1); }
  L_DESTRUCTURE_NEXT: { VM_CHECK(sv_op_destructure_next(vm, js)); NEXT(1); }
  L_DESTRUCTURE_REST: { VM_CHECK(sv_op_destructure_rest(vm, js)); NEXT(1); }
  L_DESTRUCTURE_CLOSE:{ sv_op_destructure_close(vm, js);          NEXT(1); }

  L_AWAIT: {
    ant_value_t await_val = vm->stack[--vm->sp];
    frame->ip = ip + 1;
    vm->suspended_entry_fp = entry_fp;
    vm->suspended_saved_fp = entry_fp - 1;
    
    sv_await_result_t await_result = sv_await_value(vm, js, await_val);
    if (await_result.state == SV_AWAIT_SUSPENDED && await_result.handoff) {
      vm->suspended_entry_fp = -1;
      vm->suspended_saved_fp = -1;
      vm_result = js_mkundef();
      goto sv_leave;
    }
    
    if (await_result.state == SV_AWAIT_SUSPENDED) goto sv_leave;
    vm->suspended_entry_fp = -1;
    vm->suspended_saved_fp = -1;
    
    if (await_result.state == SV_AWAIT_ERROR) {
      sv_err = await_result.value;
      goto sv_throw;
    }
    
    vm->stack[vm->sp++] = await_result.value;
    NEXT(1);
  }
  
  L_YIELD: {
    ant_value_t yielded = vm->stack[--vm->sp];
    coroutine_t *coro = js->active_async_coro;
    if (!coro || coro->type != CORO_GENERATOR) {
      sv_err = js_mkerr(js, "yield can only be used inside generator functions");
      goto sv_throw;
    }
    coro->yield_value = yielded;
    coro->did_suspend = true;
    vm->suspended = true;
    vm->suspended_entry_fp = entry_fp;
    vm->suspended_saved_fp = entry_fp - 1;
    frame->ip = ip + 1;
    vm_result = yielded;
    goto sv_leave;
  }
  
  L_YIELD_STAR_INIT: {
    uint16_t base = sv_get_u16(ip + 1);
    ant_value_t iterable = vm->stack[--vm->sp];
    vm->stack[vm->sp++] = iterable;
    VM_CHECK(sv_op_for_of(vm, js));
    sv_yield_star_store_state(vm, lp, base);
    vm->sp -= 3;
    lp[base + 3] = js_true;
    NEXT(3);
  }

  L_YIELD_STAR_NEXT:
  L_YIELD_STAR_THROW:
  L_YIELD_STAR_RETURN: {
    coroutine_t *coro = js->active_async_coro;
    if (!coro || coro->type != CORO_GENERATOR) {
      sv_err = js_mkerr(js, "yield can only be used inside generator functions");
      goto sv_throw;
    }

    uint16_t base = sv_get_u16(ip + 1);
    ant_value_t resume_value = vm->stack[--vm->sp];
    ant_value_t yielded = js_mkundef();
    bool done = false;

    if (*ip == OP_YIELD_STAR_THROW || suspended_resume_kind == SV_RESUME_THROW) {
      VM_CHECK(sv_yield_star_throw(vm, js, lp, base, resume_value, &yielded, &done));
    } else if (*ip == OP_YIELD_STAR_RETURN || suspended_resume_kind == SV_RESUME_RETURN) {
      VM_CHECK(sv_yield_star_return(vm, js, lp, base, resume_value, &yielded, &done));
      if (done) {
        sv_yield_star_clear_state(js, lp, base);
        vm->stack[vm->sp++] = yielded;
        goto L_RETURN;
      }
    } else VM_CHECK(sv_yield_star_next(vm, js, lp, base, resume_value, &yielded, &done));

    if (done) {
      sv_yield_star_clear_state(js, lp, base);
      vm->stack[vm->sp++] = yielded;
      NEXT(3);
    }

    coro->yield_value = yielded;
    coro->did_suspend = true;
    vm->suspended = true;
    vm->suspended_entry_fp = entry_fp;
    vm->suspended_saved_fp = entry_fp - 1;
    frame->ip = ip;
    vm_result = yielded;
    goto sv_leave;
  }

  L_SPREAD:              { VM_CHECK(sv_op_spread(vm, js));         NEXT(1); }
  L_DEFINE_METHOD:       { sv_op_define_method(vm, js, func, ip);  NEXT(6); }
  L_DEFINE_METHOD_COMP:  { sv_op_define_method_comp(vm, js, ip);   NEXT(2); }
  L_SET_NAME:            { sv_op_set_name(vm, js, func, ip);       NEXT(5); }
  L_SET_NAME_COMP:       { sv_op_set_name_comp(vm, js);            NEXT(1); }
  L_SET_PROTO:           { sv_op_set_proto(vm, js);                NEXT(1); }
  L_SET_HOME_OBJ:        { sv_op_set_home_obj(vm, js);             NEXT(1); }
  L_APPEND:              { sv_op_append(vm, js);                   NEXT(1); }
  L_COPY_DATA_PROPS:     { sv_op_copy_data_props(vm, js, ip);      NEXT(2); }

  L_DEFINE_CLASS:       { sv_op_define_class(vm, js, func, ip);       NEXT(14); }
  L_DEFINE_CLASS_COMP:  { sv_op_define_class_comp(vm, js, func, ip);  NEXT(14); }
  L_ADD_BRAND:          { sv_op_add_brand(vm);                        NEXT(1);  }

  L_TO_OBJECT:   { VM_CHECK(sv_op_to_object(vm, js));  NEXT(1); }
  L_TO_PROPKEY:  { sv_op_to_propkey(vm, js);           NEXT(1); }
  L_IS_UNDEF:    { sv_op_is_undef(vm);                 NEXT(1); }
  L_IS_NULL:     { sv_op_is_null(vm);                  NEXT(1); }

  L_IMPORT:          { VM_CHECK(sv_op_import(vm, js));                 NEXT(1); }
  L_IMPORT_SYNC:     { VM_CHECK(sv_op_import_sync(vm, js));            NEXT(1); }
  L_IMPORT_DEFAULT:  { sv_op_import_default(vm, js);                   NEXT(1); }
  L_IMPORT_NAMED:    { VM_CHECK(sv_op_import_named(vm, js, func, ip)); NEXT(5); }
  L_EXPORT:          { VM_CHECK(sv_op_export(vm, js, func, ip));       NEXT(5); }
  L_EXPORT_ALL:      { VM_CHECK(sv_op_export_all(vm, js));             NEXT(1); }

  L_ENTER_WITH:   { VM_CHECK(sv_op_enter_with(vm, js, frame));  NEXT(1); }
  L_EXIT_WITH:    { sv_op_exit_with(vm, frame);                 NEXT(1); }

  L_WITH_GET_VAR:  { VM_CHECK(sv_op_with_get_var(vm, js, frame, func, ip));  NEXT(8); }
  L_WITH_PUT_VAR:  { sv_op_with_put_var(vm, js, frame, func, ip);            NEXT(8); }
  L_WITH_DEL_VAR:  { sv_op_with_del_var(vm, js, frame, func, ip);            NEXT(5); }

  L_SPECIAL_OBJ:  { sv_op_special_obj(vm, js, frame, ip);                       NEXT(2); }
  L_EMPTY:        { vm->stack[vm->sp++] = T_EMPTY;                              NEXT(1); }
  L_PUT_CONST:    { func->constants[sv_get_u32(ip + 1)] = vm->stack[--vm->sp];  NEXT(5); }
  
  L_DEBUGGER:  { NEXT(1); }
  L_NOP:       { NEXT(1); }

  L_LABEL:
  L_LINE_NUM:
  L_COL_NUM:
  L_INVALID:
    sv_err = js_mkerr(js, "invalid opcode %d", (int)*ip);
    goto sv_throw;

  sv_throw: {
    uint8_t *catch_ip = sv_vm_throw(vm, sv_err, entry_fp);
    if (catch_ip) {
      frame = &vm->frames[vm->fp];
      func = frame->func;
      bp = frame->bp;
      lp = frame->lp;
      
      ip = catch_ip;
      DISPATCH();
    }
    if (!is_err(sv_err)) {
      vm_result = js_throw(js, sv_err);
      goto sv_leave;
    }
    vm_result = sv_err;
    goto sv_leave;
  }

  sv_leave:
  if (vm->suspended) {
    if (js->vm_exec_depth > 0) js->vm_exec_depth--;
    return vm_result;
  }
  if (vm->open_upvalues) {
  for (int f = vm->fp; f >= entry_fp; f--) {
    ant_value_t *drop_bp = vm->frames[f].bp;
    if (drop_bp) sv_close_upvalues_from_slot(vm, drop_bp);
  }}
  for (int f = vm->fp; f >= entry_fp; f--)
    sv_drop_frame_runtime_state(js, &vm->frames[f]);
  vm->fp = entry_fp;
  vm->sp = vm->frames[entry_fp].prev_sp;
  vm->handler_depth = vm->frames[entry_fp].handler_base;
  if (js->vm_exec_depth > 0) js->vm_exec_depth--;

  return vm_result;

  #undef DISPATCH
  #undef NEXT
  #undef VM_CHECK

  // TODO: use entry_bp/frame->bp
  //       and sv_op_size values
  (void)bp;
  (void)sv_op_size;
}

ant_value_t sv_resume_suspended(sv_vm_t *vm) {
  if (!vm || !vm->suspended || !vm->suspended_resume_pending || vm->fp < 0)
    return mkval(T_ERR, 0);

  // crash-resistance for missing frames 
  if (vm->suspended_entry_fp < 0 || vm->suspended_entry_fp > vm->fp) {
    vm->suspended = false;
    vm->suspended_resume_pending = false;
    vm->suspended_resume_is_error = false;
    vm->suspended_resume_kind = SV_RESUME_NEXT;
    vm->suspended_resume_value = js_mkundef();
    vm->suspended_entry_fp = -1;
    vm->suspended_saved_fp = -1;
    
    return vm->js
      ? js_mkerr(vm->js, "invalid suspended entry frame")
      : mkval(T_ERR, 0);
  }

  int saved_fp = vm->suspended_saved_fp;
  sv_frame_t *frame = &vm->frames[vm->fp];
  
  if (!vm->js || !frame->func || !frame->ip) {
    vm->suspended = false;
    vm->suspended_resume_pending = false;
    vm->suspended_resume_is_error = false;
    vm->suspended_resume_kind = SV_RESUME_NEXT;
    vm->suspended_resume_value = js_mkundef();
    vm->suspended_entry_fp = -1;
    vm->suspended_saved_fp = -1;
    
    return vm->js
      ? js_mkerr(vm->js, "invalid suspended frame state")
      : mkval(T_ERR, 0);
  }

  ant_value_t result = sv_execute_frame(
    vm, frame->func, frame->this, frame->super_val, NULL, frame->argc
  );

  if (!vm->suspended) {
    vm->fp = saved_fp;
    vm->suspended_entry_fp = -1;
    vm->suspended_saved_fp = -1;
  }

  return result;
}
