/*
 * Copyright (C) 2020-2021 Jo-Philipp Wich <jo@mein.io>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
#include <math.h>

#include "vm.h"
#include "compiler.h"
#include "lib.h" /* format_error_context() */

#undef __insn
#define __insn(_name) #_name,

static const char *insn_names[__I_MAX] = {
	__insns
};

static const uc_insn_definition insn_defs[__I_MAX] = {
	[I_NOOP] = { 0, 0, 0 },

	[I_LOAD] = { 0, 1, 4 },
	[I_LOAD8] = { 0, 1, -1 },
	[I_LOAD16] = { 0, 1, -2 },
	[I_LOAD32] = { 0, 1, -4 },

	[I_LREXP] = { 0, 1, 4 },
	[I_LNULL] = { 0, 1, 0 },
	[I_LTRUE] = { 0, 1, 0 },
	[I_LFALSE] = { 0, 1, 0 },
	[I_LTHIS] = { 0, 1, 0 },

	[I_LLOC] = { 0, 1, 4 },
	[I_LVAR] = { 0, 1, 4 },
	[I_LUPV] = { 0, 1, 4 },
	[I_LVAL] = { 2, 1, 0 },

	[I_CLFN] = { 0, 1, 4, true },
	[I_ARFN] = { 0, 1, 4, true },

	[I_SLOC] = { 0, 0, 4 },
	[I_SUPV] = { 0, 0, 4 },
	[I_SVAR] = { 0, 0, 4 },
	[I_SVAL] = { 3, 1, 0 },

	[I_ULOC] = { 1, 0, 4 },
	[I_UUPV] = { 1, 0, 4 },
	[I_UVAR] = { 1, 0, 4 },
	[I_UVAL] = { 3, 1, 1 },

	[I_NARR] = { 0, 1, 4 },
	[I_PARR] = { -1, 0, 4 },
	[I_MARR] = { 1, 0, 0 },

	[I_NOBJ] = { 0, 1, 4 },
	[I_SOBJ] = { -1, 0, 4 },
	[I_MOBJ] = { 1, 0, 0 },

	[I_ADD] = { 2, 1, 0 },
	[I_SUB] = { 2, 1, 0 },
	[I_MUL] = { 2, 1, 0 },
	[I_DIV] = { 2, 1, 0 },
	[I_MOD] = { 2, 1, 0 },
	[I_LSHIFT] = { 2, 1, 0 },
	[I_RSHIFT] = { 2, 1, 0 },
	[I_BAND] = { 2, 1, 0 },
	[I_BXOR] = { 2, 1, 0 },
	[I_BOR] = { 2, 1, 0 },
	[I_EQ] = { 2, 1, 0 },
	[I_NE] = { 2, 1, 0 },
	[I_EQS] = { 2, 1, 0 },
	[I_NES] = { 2, 1, 0 },
	[I_LT] = { 2, 1, 0 },
	[I_GT] = { 2, 1, 0 },
	[I_IN] = { 2, 1, 0 },

	[I_JMP] = { 0, 0, -4, true },
	[I_JMPZ] = { 1, 0, -4, true },

	[I_COPY] = { 0, 1, 1 },
	[I_POP] = { 1, 0, 0 },
	[I_CUPV] = { 1, 0, 0 },

	[I_PLUS] = { 1, 1, 0 },
	[I_MINUS] = { 1, 1, 0 },

	[I_RETURN] = { 1, 0, 0 },
	[I_CALL] = { -2, 1, 4 },
	[I_MCALL] = { -3, 1, 4 },

	[I_NEXTK] = { 2, 2, 0 },
	[I_NEXTKV] = { 2, 3, 0 },

	[I_PRINT] = { 1, 0, 0 }
};

static const char *exception_type_strings[] = {
	[EXCEPTION_SYNTAX] = "Syntax error",
	[EXCEPTION_RUNTIME] = "Runtime error",
	[EXCEPTION_TYPE] = "Type error",
	[EXCEPTION_REFERENCE] = "Reference error",
	[EXCEPTION_USER] = "Error",
};


static void
uc_vm_reset_stack(uc_vm *vm)
{
	while (vm->stack.count > 0) {
		vm->stack.count--;
		uc_value_put(vm->stack.entries[vm->stack.count]);
		vm->stack.entries[vm->stack.count] = NULL;
	}
}

static json_object *
uc_vm_callframe_pop(uc_vm *vm);

static void
uc_vm_reset_callframes(uc_vm *vm)
{
	while (vm->callframes.count > 0)
		uc_value_put(uc_vm_callframe_pop(vm));
}

void uc_vm_init(uc_vm *vm, uc_parse_config *config)
{
	char *s = getenv("TRACE");

	vm->exception.type = EXCEPTION_NONE;
	vm->exception.message = NULL;

	vm->trace = s ? strtoul(s, NULL, 0) : 0;

	vm->config = config;

	vm->open_upvals = NULL;

	uc_vm_reset_stack(vm);
}

void uc_vm_free(uc_vm *vm)
{
	uc_upvalref *ref;

	uc_value_put(vm->exception.stacktrace);
	free(vm->exception.message);

	while (vm->open_upvals) {
		ref = vm->open_upvals->next;
		uc_value_put(vm->open_upvals->header.jso);
		vm->open_upvals = ref;
	}

	uc_vm_reset_callframes(vm);
	uc_vm_reset_stack(vm);
	uc_vector_clear(&vm->stack);
	uc_vector_clear(&vm->callframes);
}

static uc_chunk *
uc_vm_frame_chunk(uc_callframe *frame)
{
	return frame->closure ? &frame->closure->function->chunk : NULL;
}

static uc_callframe *
uc_vm_current_frame(uc_vm *vm)
{
	return uc_vector_last(&vm->callframes);
}

static uc_chunk *
uc_vm_current_chunk(uc_vm *vm)
{
	return uc_vm_frame_chunk(uc_vm_current_frame(vm));
}

static enum insn_type
uc_vm_decode_insn(uc_vm *vm, uc_callframe *frame, uc_chunk *chunk)
{
	enum insn_type insn;

#ifndef NDEBUG
	uint8_t *end = chunk->entries + chunk->count;
#endif

	assert(frame->ip < end);

	insn = frame->ip[0];
	frame->ip++;

	assert(frame->ip + abs(insn_defs[insn].operand_bytes) <= end);

	switch (insn_defs[insn].operand_bytes) {
	case 0:
		break;

	case -1:
		vm->arg.s8 = frame->ip[0] - 0x7f;
		frame->ip++;
		break;

	case -2:
		vm->arg.s16 = (
			frame->ip[0] * 0x100 +
			frame->ip[1]
		) - 0x7fff;
		frame->ip += 2;
		break;

	case -4:
		vm->arg.s32 = (
			frame->ip[0] * 0x1000000 +
			frame->ip[1] * 0x10000 +
			frame->ip[2] * 0x100 +
			frame->ip[3]
		) - 0x7fffffff;
		frame->ip += 4;
		break;

	case 1:
		vm->arg.u8 = frame->ip[0];
		frame->ip++;
		break;

	case 4:
		vm->arg.u32 = (
			frame->ip[0] * 0x1000000 +
			frame->ip[1] * 0x10000 +
			frame->ip[2] * 0x100 +
			frame->ip[3]
		);
		frame->ip += 4;
		break;

	default:
		fprintf(stderr, "Unhandled operand format: %d\n", insn_defs[insn].operand_bytes);
		abort();
	}

	return insn;
}


static void
uc_vm_frame_dump(uc_vm *vm, uc_callframe *frame)
{
	uc_chunk *chunk = uc_vm_frame_chunk(frame);
	uc_function *function;
	uc_closure *closure;
	uc_upvalref *ref;
	json_object *v;
	size_t i;

	fprintf(stderr, "  [*] CALLFRAME[%zx]\n",
		frame - vm->callframes.entries);

	fprintf(stderr, "   |- stackframe %zu/%zu\n",
		frame->stackframe, vm->stack.count);

	fprintf(stderr, "   |- ctx %s\n",
		json_object_to_json_string(frame->ctx));

	if (chunk) {
		fprintf(stderr, "   |- %zu constants\n",
			chunk->constants.isize);

		for (i = 0; i < chunk->constants.isize; i++) {
			v = uc_chunk_get_constant(chunk, i);

			fprintf(stderr, "   | [%zu] %s\n",
				i,
				json_object_to_json_string(v));

			uc_value_put(v);
		}

		closure = frame->closure;
		function = closure->function;

		fprintf(stderr, "   `- %zu upvalues\n",
			function->nupvals);

		for (i = 0; i < function->nupvals; i++) {
			ref = closure->upvals[i];
			v = uc_chunk_debug_get_variable(chunk, 0, i, true);

			if (ref->closed)
				fprintf(stderr, "     [%zu] <%p> %s {closed} %s\n",
					i,
					ref,
					json_object_to_json_string(v),
					json_object_to_json_string(ref->value));
			else
				fprintf(stderr, "     [%zu] <%p> %s {open[%zu]} %s\n",
					i,
					ref,
					json_object_to_json_string(v),
					ref->slot,
					json_object_to_json_string(vm->stack.entries[ref->slot]));

			uc_value_put(v);
		}
	}
}

void
uc_vm_stack_push(uc_vm *vm, json_object *value)
{
	uc_vector_grow(&vm->stack);

	uc_value_put(vm->stack.entries[vm->stack.count]);

	vm->stack.entries[vm->stack.count] = value;
	vm->stack.count++;

	if (vm->trace)
		fprintf(stderr, "  [+%zd] %s\n",
			vm->stack.count - 1,
			json_object_to_json_string(value));
}

json_object *
uc_vm_stack_pop(uc_vm *vm)
{
	json_object *rv;

	vm->stack.count--;
	rv = vm->stack.entries[vm->stack.count];
	vm->stack.entries[vm->stack.count] = NULL;

	if (vm->trace)
		fprintf(stderr, "  [-%zd] %s\n",
			vm->stack.count,
			json_object_to_json_string(rv));

	return rv;
}

json_object *
uc_vm_stack_peek(uc_vm *vm, size_t offset)
{
	return vm->stack.entries[vm->stack.count + (-1 - offset)];
}

static void
uc_vm_stack_set(uc_vm *vm, size_t offset, json_object *value)
{
	if (vm->trace)
		fprintf(stderr, "  [!%zu] %s\n",
			offset, json_object_to_json_string(value));

	uc_value_put(vm->stack.entries[offset]);
	vm->stack.entries[offset] = value;
}

static void
uc_vm_call_native(uc_vm *vm, json_object *ctx, uc_cfunction *fptr, bool mcall, size_t nargs)
{
	json_object *res = NULL;
	uc_callframe *frame;

	/* add new callframe */
	uc_vector_grow(&vm->callframes);

	frame = &vm->callframes.entries[vm->callframes.count++];
	frame->stackframe = vm->stack.count - nargs - 1;
	frame->cfunction = fptr;
	frame->closure = NULL;
	frame->ctx = ctx;
	frame->mcall = mcall;

	if (vm->trace)
		uc_vm_frame_dump(vm, frame);

	res = fptr->cfn(vm, nargs);

	/* reset stack */
	uc_value_put(uc_vm_callframe_pop(vm));

	/* push return value */
	if (!vm->exception.type)
		uc_vm_stack_push(vm, res);
	else
		uc_value_put(res);
}

static bool
uc_vm_call_function(uc_vm *vm, json_object *ctx, json_object *fno, bool mcall, size_t argspec)
{
	size_t i, j, stackoff, nargs = argspec & 0xffff, nspreads = argspec >> 16;
	uc_callframe *frame = uc_vm_current_frame(vm);
	json_object *ellip, *arg;
	uc_function *function;
	uc_closure *closure;
	uint16_t slot, tmp;

	/* XXX: make dependent on stack size */
	if (vm->callframes.count >= 1000) {
		uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Too much recursion");

		return false;
	}

	stackoff = vm->stack.count - nargs - 1;

	/* argument list contains spread operations, we need to reshuffle the stack */
	if (nspreads > 0) {
		/* create temporary array */
		ellip = xjs_new_array_size(nargs);

		/* pop original stack values and push to temp array in reverse order */
		for (i = 0; i < nargs; i++)
			json_object_array_add(ellip, uc_vm_stack_pop(vm));

		/* for each spread value index ... */
		for (i = 0, slot = nargs; i < nspreads; i++) {
			/* decode stack depth value */
			tmp = frame->ip[0] * 0x100 + frame->ip[1];
			frame->ip += 2;

			/* push each preceeding non-spread value to the stack */
			for (j = slot; j > tmp + 1; j--)
				uc_vm_stack_push(vm, uc_value_get(json_object_array_get_idx(ellip, j - 1)));

			/* read spread value at index... */
			slot = tmp;
			arg = uc_value_get(json_object_array_get_idx(ellip, slot));

			/* ... ensure that it is an array type ... */
			if (!json_object_is_type(arg, json_type_array)) {
				uc_vm_raise_exception(vm, EXCEPTION_TYPE,
				                      "(%s) is not iterable",
				                      json_object_to_json_string(arg));

				return false;
			}

			/* ... and push each spread array value as argument to the stack */
			for (j = 0; j < json_object_array_length(arg); j++)
				uc_vm_stack_push(vm, uc_value_get(json_object_array_get_idx(arg, j)));

			uc_value_put(arg);
		}

		/* push remaining non-spread arguments to the stack */
		for (i = slot; i > 0; i--)
			uc_vm_stack_push(vm, uc_value_get(json_object_array_get_idx(ellip, i - 1)));

		/* free temp array */
		uc_value_put(ellip);

		/* update arg count */
		nargs = vm->stack.count - stackoff - 1;
	}

	/* is a native function */
	if (uc_object_is_type(fno, UC_OBJ_CFUNCTION)) {
		uc_vm_call_native(vm, ctx, uc_object_as_cfunction(fno), mcall, nargs);

		return true;
	}

	if (!uc_object_is_type(fno, UC_OBJ_CLOSURE)) {
		uc_vm_raise_exception(vm, EXCEPTION_TYPE, "left-hand side is not a function");

		return false;
	}

	closure = uc_object_as_closure(fno);
	function = closure->function;

	/* fewer arguments on stack than function expects => pad */
	if (nargs < function->nargs) {
		for (i = nargs; i < function->nargs; i++) {
			if (function->vararg && (i + 1) == function->nargs)
				uc_vm_stack_push(vm, xjs_new_array_size(0));
			else
				uc_vm_stack_push(vm, NULL);
		}
	}

	/* more arguments on stack than function expects... */
	else if (nargs > function->nargs - function->vararg) {
		/* is a vararg function => pass excess args as array */
		if (function->vararg) {
			ellip = xjs_new_array_size(nargs - (function->nargs - 1));

			for (i = function->nargs; i <= nargs; i++)
				json_object_array_add(ellip, uc_vm_stack_peek(vm, nargs - i));

			for (i = function->nargs; i <= nargs; i++)
				uc_vm_stack_pop(vm);

			uc_vm_stack_push(vm, ellip);
		}

		/* static amount of args => drop excess values */
		else {
			for (i = function->nargs; i < nargs; i++)
				uc_value_put(uc_vm_stack_pop(vm));
		}
	}

	uc_vector_grow(&vm->callframes);

	frame = &vm->callframes.entries[vm->callframes.count++];
	frame->stackframe = stackoff;
	frame->cfunction = NULL;
	frame->closure = closure;
	frame->ctx = ctx;
	frame->ip = function->chunk.entries;
	frame->mcall = mcall;

	if (vm->trace)
		uc_vm_frame_dump(vm, frame);

	return true;
}

static uc_source *last_source = NULL;
static size_t last_srcpos = 0;

static void
uc_dump_insn(uc_vm *vm, uint8_t *pos, enum insn_type insn)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	uc_chunk *chunk = uc_vm_frame_chunk(frame);
	size_t msglen = 0, srcpos;
	json_object *cnst = NULL;
	char *msg = NULL;

	srcpos = uc_function_get_srcpos(frame->closure->function, pos - chunk->entries);

	if (last_srcpos == 0 || last_source != frame->closure->function->source || srcpos != last_srcpos) {
		format_source_context(&msg, &msglen,
			frame->closure->function->source,
			srcpos, true);

		fprintf(stderr, "%s", msg);
		free(msg);

		last_source = frame->closure->function->source;
		last_srcpos = srcpos;
	}

	fprintf(stderr, "%08zx  %s", pos - chunk->entries, insn_names[insn]);

	switch (insn_defs[insn].operand_bytes) {
	case 0:
		break;

	case -1:
		fprintf(stderr, " {%s%hhd}", vm->arg.s8 < 0 ? "" : "+", vm->arg.s8);
		break;

	case -2:
		fprintf(stderr, " {%c0x%hx}",
			vm->arg.s16 < 0 ? '-' : '+',
			vm->arg.s16 < 0 ? -(unsigned)vm->arg.s16 : vm->arg.s16);
		break;

	case -4:
		fprintf(stderr, " {%c0x%x}",
			vm->arg.s32 < 0 ? '-' : '+',
			vm->arg.s32 < 0 ? -(unsigned)vm->arg.s32 : vm->arg.s32);
		break;

	case 1:
		fprintf(stderr, " {%hhu}", vm->arg.u8);
		break;

	case 2:
		fprintf(stderr, " {0x%hx}", vm->arg.u16);
		break;

	case 4:
		fprintf(stderr, " {0x%x}", vm->arg.u32);
		break;

	default:
		fprintf(stderr, " (unknown operand format: %d)", insn_defs[insn].operand_bytes);
		break;
	}

	switch (insn) {
	case I_LOAD:
	case I_LVAR:
	case I_SVAR:
		cnst = uc_chunk_get_constant(uc_vm_frame_chunk(uc_vector_last(&vm->callframes)), vm->arg.u32);

		fprintf(stderr, "\t; %s", cnst ? json_object_to_json_string(cnst) : "null");
		uc_value_put(cnst);
		break;

	case I_LLOC:
	case I_LUPV:
	case I_SLOC:
	case I_SUPV:
		cnst = uc_chunk_debug_get_variable(chunk, pos - chunk->entries, vm->arg.u32, (insn == I_LUPV || insn == I_SUPV));

		fprintf(stderr, "\t; %s", cnst ? json_object_to_json_string(cnst) : "(?)");
		uc_value_put(cnst);
		break;

	case I_ULOC:
	case I_UUPV:
		cnst = uc_chunk_debug_get_variable(chunk, pos - chunk->entries, vm->arg.u32 & 0x00ffffff, (insn == I_UUPV));
		/* fall through */

	case I_UVAR:
		if (!cnst)
			cnst = uc_chunk_get_constant(uc_vm_frame_chunk(uc_vector_last(&vm->callframes)), vm->arg.u32 & 0x00ffffff);

		fprintf(stderr, "\t; %s (%s)",
			cnst ? json_object_to_json_string(cnst) : "(?)",
			insn_names[vm->arg.u32 >> 24]);

		uc_value_put(cnst);
		break;

	case I_UVAL:
		fprintf(stderr, "\t; (%s)", insn_names[vm->arg.u32]);
		break;

	default:
		break;
	}

	fprintf(stderr, "\n");
}

static int
uc_vm_exception_tostring(json_object *jso, struct printbuf *pb, int level, int flags)
{
	bool strict = (level > 0) || (flags & JSON_C_TO_STRING_STRICT);
	json_object *message = json_object_object_get(jso, "message");

	return sprintbuf(pb, "%s",
		strict ? json_object_to_json_string(message) : json_object_get_string(message));
}

static bool
uc_vm_handle_exception(uc_vm *vm)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	uc_chunk *chunk = NULL;
	json_object *exo;
	size_t i, pos;

	if (!frame->closure)
		return false;

	chunk = uc_vm_frame_chunk(frame);
	pos = frame->ip - chunk->entries;

	/* iterate the known exception ranges, see if the current ip falls into any of them */
	for (i = 0; i < chunk->ehranges.count; i++) {
		/* skip nonmatching ranges */
		if (pos < chunk->ehranges.entries[i].from ||
		    pos >= chunk->ehranges.entries[i].to)
			continue;

		/* we found a matching range... first unwind stack */
		while (vm->stack.count > frame->stackframe + chunk->ehranges.entries[i].slot)
			uc_value_put(uc_vm_stack_pop(vm));

		/* prepare exception object and expose it to user handler code */
		exo = xjs_new_object();

		json_object_object_add(exo, "type", xjs_new_string(exception_type_strings[vm->exception.type]));
		json_object_object_add(exo, "message", xjs_new_string(vm->exception.message));
		json_object_object_add(exo, "stacktrace", uc_value_get(vm->exception.stacktrace));

		json_object_set_serializer(exo, uc_vm_exception_tostring, NULL, NULL);
		uc_vm_stack_push(vm, exo);

		/* reset exception information */
		free(vm->exception.message);

		vm->exception.type = EXCEPTION_NONE;
		vm->exception.message = NULL;

		/* jump to exception handler */
		if (chunk->ehranges.entries[i].target >= chunk->count) {
			uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "jump target out of range");
			return false;
		}

#if 0
		if (vm->trace && chunk->entries + chunk->ehranges.entries[i].target > frame->ip) {
			while (frame->ip < chunk->entries + chunk->ehranges.entries[i].target) {
				fprintf(stderr, "(eh:skip) [%p:%zu] ", chunk, frame->ip - chunk->entries);
				uc_dump_insn(vm, frame->ip, uc_vm_decode_insn(vm, frame, chunk));
			}
		}
#endif

		frame->ip = chunk->entries + chunk->ehranges.entries[i].target;

		return true;
	}

	return false;
}

static json_object *
uc_vm_capture_stacktrace(uc_vm *vm, size_t i)
{
	json_object *stacktrace, *entry, *last = NULL;
	uc_function *function;
	uc_callframe *frame;
	size_t off, srcpos;
	char *name;

	stacktrace = xjs_new_array();

	for (; i > 0; i--) {
		frame = &vm->callframes.entries[i - 1];
		entry = xjs_new_object();

		if (frame->closure) {
			function = frame->closure->function;

			off = (frame->ip - uc_vm_frame_chunk(frame)->entries) - 1;
			srcpos = uc_function_get_srcpos(function, off);

			json_object_object_add(entry, "filename", xjs_new_string(function->source->filename));
			json_object_object_add(entry, "line", xjs_new_int64(uc_source_get_line(function->source, &srcpos)));
			json_object_object_add(entry, "byte", xjs_new_int64(srcpos));
		}

		if (i > 1) {
			if (frame->closure) {
				if (frame->closure->function->name)
					name = frame->closure->function->name;
				else if (frame->closure->is_arrow)
					name = "[arrow function]";
				else
					name = "[anonymous function]";
			}
			else {
				name = frame->cfunction->name;
			}

			json_object_object_add(entry, "function", xjs_new_string(name));
		}

		if (!json_object_equal(last, entry)) {
			json_object_array_add(stacktrace, entry);
			last = entry;
		}
		else {
			uc_value_put(entry);
		}
	}

	return stacktrace;
}

static json_object *
uc_vm_get_error_context(uc_vm *vm)
{
	json_object *stacktrace;
	uc_callframe *frame;
	uc_chunk *chunk;
	size_t offset, len = 0, i;
	char *msg = NULL;

	/* skip to first non-native function call frame */
	for (i = vm->callframes.count; i > 0; i--)
		if (vm->callframes.entries[i - 1].closure)
			break;

	frame = &vm->callframes.entries[i - 1];

	if (!frame->closure)
		return NULL;

	chunk = uc_vm_frame_chunk(frame);
	offset = uc_function_get_srcpos(frame->closure->function, (frame->ip - chunk->entries) - 1);
	stacktrace = uc_vm_capture_stacktrace(vm, i);

	if (offset)
		format_error_context(&msg, &len, frame->closure->function->source, stacktrace, offset);
	else
		xasprintf(&msg, "At offset %zu", (frame->ip - chunk->entries) - 1);

	json_object_object_add(json_object_array_get_idx(stacktrace, 0), "context", xjs_new_string(msg));

	free(msg);

	return stacktrace;
}

void __attribute__((format(printf, 3, 0)))
uc_vm_raise_exception(uc_vm *vm, uc_exception_type_t type, const char *fmt, ...)
{
	va_list ap;

	vm->exception.type = type;

	free(vm->exception.message);

	va_start(ap, fmt);
	xvasprintf(&vm->exception.message, fmt, ap);
	va_end(ap);

	uc_value_put(vm->exception.stacktrace);
	vm->exception.stacktrace = uc_vm_get_error_context(vm);
}


static void
uc_vm_insn_load(uc_vm *vm, enum insn_type insn)
{
	switch (insn) {
	case I_LOAD:
		uc_vm_stack_push(vm, uc_chunk_get_constant(uc_vm_current_chunk(vm), vm->arg.u32));
		break;

	case I_LOAD8:
		uc_vm_stack_push(vm, xjs_new_int64(vm->arg.s8));
		break;

	case I_LOAD16:
		uc_vm_stack_push(vm, xjs_new_int64(vm->arg.s16));
		break;

	case I_LOAD32:
		uc_vm_stack_push(vm, xjs_new_int64(vm->arg.s32));
		break;

	default:
		break;
	}
}

static void
uc_vm_insn_load_regexp(uc_vm *vm, enum insn_type insn)
{
	bool icase = false, newline = false, global = false;
	json_object *jstr = uc_chunk_get_constant(uc_vm_current_chunk(vm), vm->arg.u32);
	const char *str;
	uc_regexp *re;
	char *err;

	if (!json_object_is_type(jstr, json_type_string) || json_object_get_string_len(jstr) < 2) {
		uc_vm_stack_push(vm, NULL);
		uc_value_put(jstr);

		return;
	}

	str = json_object_get_string(jstr);

	global  = (*str & (1 << 0));
	icase   = (*str & (1 << 1));
	newline = (*str & (1 << 2));

	re = uc_regexp_new(++str, icase, newline, global, &err);

	uc_value_put(jstr);

	if (re)
		uc_vm_stack_push(vm, re->header.jso);
	else
		uc_vm_raise_exception(vm, EXCEPTION_SYNTAX, "%s", err);
}

static void
uc_vm_insn_load_null(uc_vm *vm, enum insn_type insn)
{
	uc_vm_stack_push(vm, NULL);
}

static void
uc_vm_insn_load_bool(uc_vm *vm, enum insn_type insn)
{
	uc_vm_stack_push(vm, xjs_new_boolean(insn == I_LTRUE));
}

static void
uc_vm_insn_load_var(uc_vm *vm, enum insn_type insn)
{
	json_object *name, *val = NULL;
	uc_prototype *scope, *next;

	scope = vm->globals;
	name = uc_chunk_get_constant(uc_vm_current_chunk(vm), vm->arg.u32);

 	while (json_object_get_type(name) == json_type_string) {
		if (json_object_object_get_ex(scope->header.jso, json_object_get_string(name), &val))
			break;

		next = scope->parent;

		if (!next) {
			if (vm->config->strict_declarations) {
				uc_vm_raise_exception(vm, EXCEPTION_REFERENCE,
				                      "access to undeclared variable %s",
				                      json_object_get_string(name));
			}

			break;
		}

		scope = next;
	}

	uc_value_put(name);

	uc_vm_stack_push(vm, uc_value_get(val));
}

static void
uc_vm_insn_load_val(uc_vm *vm, enum insn_type insn)
{
	json_object *k = uc_vm_stack_pop(vm);
	json_object *v = uc_vm_stack_pop(vm);

	switch (json_object_get_type(v)) {
	case json_type_object:
	case json_type_array:
		uc_vm_stack_push(vm, uc_getval(v, k));
		break;

	default:
		uc_vm_raise_exception(vm, EXCEPTION_REFERENCE,
		                      "left-hand side expression is %s",
		                      v ? "not an array or object" : "null");

		break;
	}


	uc_value_put(k);
	uc_value_put(v);
}

static void
uc_vm_insn_load_upval(uc_vm *vm, enum insn_type insn)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	uc_upvalref *ref = frame->closure->upvals[vm->arg.u32];

	if (ref->closed)
		uc_vm_stack_push(vm, uc_value_get(ref->value));
	else
		uc_vm_stack_push(vm, uc_value_get(vm->stack.entries[ref->slot]));
}

static void
uc_vm_insn_load_local(uc_vm *vm, enum insn_type insn)
{
	uc_callframe *frame = uc_vm_current_frame(vm);

	uc_vm_stack_push(vm, uc_value_get(vm->stack.entries[frame->stackframe + vm->arg.u32]));
}

static uc_upvalref *
uc_vm_capture_upval(uc_vm *vm, size_t slot)
{
	uc_upvalref *curr = vm->open_upvals;
	uc_upvalref *prev = NULL;
	uc_upvalref *created;

	while (curr && curr->slot > slot) {
		prev = curr;
		curr = curr->next;
	}

	if (curr && curr->slot == slot) {
		if (vm->trace)
			fprintf(stderr, "  {+%zu} <%p> %s\n",
				slot,
				curr,
				json_object_to_json_string(vm->stack.entries[slot]));

		return curr;
	}

	created = uc_upvalref_new(slot);
	created->next = curr;

	if (vm->trace)
		fprintf(stderr, "  {*%zu} <%p> %s\n",
			slot,
			created,
			json_object_to_json_string(vm->stack.entries[slot]));

	if (prev)
		prev->next = created;
	else
		vm->open_upvals = created;

	return created;
}

static void
uc_vm_close_upvals(uc_vm *vm, size_t slot)
{
	uc_upvalref *ref;

	while (vm->open_upvals && vm->open_upvals->slot >= slot) {
		ref = vm->open_upvals;
		ref->value = uc_value_get(vm->stack.entries[ref->slot]);
		ref->closed = true;

		if (vm->trace)
			fprintf(stderr, "  {!%zu} <%p> %s\n",
				ref->slot,
				ref,
				json_object_to_json_string(ref->value));

		vm->open_upvals = ref->next;
		json_object_put(ref->header.jso);
	}
}

static void
uc_vm_insn_load_closure(uc_vm *vm, enum insn_type insn)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	json_object *fno = uc_chunk_get_constant(uc_vm_current_chunk(vm), vm->arg.u32);
	uc_function *function = uc_object_as_function(fno);
	uc_closure *closure = uc_closure_new(function, insn == I_ARFN);
	volatile int32_t uv;
	size_t i;

	uc_vm_stack_push(vm, closure->header.jso);

	for (i = 0; i < function->nupvals; i++) {
		uv = (
			frame->ip[0] * 0x1000000 +
			frame->ip[1] * 0x10000 +
			frame->ip[2] * 0x100 +
			frame->ip[3]
		) - 0x7fffffff;

		if (uv < 0)
			closure->upvals[i] = uc_vm_capture_upval(vm, frame->stackframe - (uv + 1));
		else
			closure->upvals[i] = frame->closure->upvals[uv];

		uc_value_get(closure->upvals[i]->header.jso);

		frame->ip += 4;
	}
}

static void
uc_vm_insn_store_var(uc_vm *vm, enum insn_type insn)
{
	json_object *name, *v = uc_vm_stack_pop(vm);
	uc_prototype *scope, *next;

	scope = vm->globals;
	name = uc_chunk_get_constant(uc_vm_current_chunk(vm), vm->arg.u32);

 	while (json_object_get_type(name) == json_type_string) {
		if (json_object_object_get_ex(scope->header.jso, json_object_get_string(name), NULL))
			break;

		next = scope->parent;

		if (!next) {
			if (vm->config->strict_declarations) {
				uc_vm_raise_exception(vm, EXCEPTION_REFERENCE,
				                      "access to undeclared variable %s",
				                      json_object_get_string(name));
			}

			break;
		}

		scope = next;
	}

	if (scope && json_object_get_type(name) == json_type_string)
		json_object_object_add(scope->header.jso, json_object_get_string(name), uc_value_get(v));

	uc_value_put(name);
	uc_vm_stack_push(vm, v);
}

static void
uc_vm_insn_store_val(uc_vm *vm, enum insn_type insn)
{
	json_object *v = uc_vm_stack_pop(vm);
	json_object *k = uc_vm_stack_pop(vm);
	json_object *o = uc_vm_stack_pop(vm);

	const char *typenames[] = {
		[json_type_string] = "string",
		[json_type_int] = "integer",
		[json_type_double] = "double",
		[json_type_boolean] = "boolean",
		[json_type_null] = "null"
	};

	switch (json_object_get_type(o)) {
	case json_type_object:
	case json_type_array:
		uc_vm_stack_push(vm, uc_setval(o, k, v));
		break;

	default:
		uc_vm_raise_exception(vm, EXCEPTION_TYPE,
		                      "attempt to set property on %s value",
		                      typenames[json_object_get_type(o)]);
	}

	uc_value_put(o);
	uc_value_put(k);
}

static void
uc_vm_insn_store_upval(uc_vm *vm, enum insn_type insn)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	uc_upvalref *ref = frame->closure->upvals[vm->arg.u32];
	json_object *val = uc_value_get(uc_vm_stack_peek(vm, 0));

	if (ref->closed) {
		uc_value_put(ref->value);
		ref->value = val;
	}
	else {
		uc_vm_stack_set(vm, ref->slot, val);
	}
}

static void
uc_vm_insn_store_local(uc_vm *vm, enum insn_type insn)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	json_object *val = uc_value_get(uc_vm_stack_peek(vm, 0));

	uc_vm_stack_set(vm, frame->stackframe + vm->arg.u32, val);
}

static json_object *
uc_vm_value_bitop(uc_vm *vm, enum insn_type operation, json_object *value, json_object *operand)
{
	json_object *rv = NULL;
	int64_t n1, n2;
	double d;

	if (uc_cast_number(value, &n1, &d) == json_type_double)
		n1 = isnan(d) ? 0 : (int64_t)d;

	if (uc_cast_number(operand, &n2, &d) == json_type_double)
		n2 = isnan(d) ? 0 : (int64_t)d;

	switch (operation) {
	case I_LSHIFT:
		rv = xjs_new_int64(n1 << n2);
		break;

	case I_RSHIFT:
		rv = xjs_new_int64(n1 >> n2);
		break;

	case I_BAND:
		rv = xjs_new_int64(n1 & n2);
		break;

	case I_BXOR:
		rv = xjs_new_int64(n1 ^ n2);
		break;

	case I_BOR:
		rv = xjs_new_int64(n1 | n2);
		break;

	default:
		break;
	}

	return rv;
}

static json_object *
uc_vm_value_arith(uc_vm *vm, enum insn_type operation, json_object *value, json_object *operand)
{
	json_object *rv = NULL;
	enum json_type t1, t2;
	const char *s1, *s2;
	size_t len1, len2;
	int64_t n1, n2;
	double d1, d2;
	char *s;

	if (operation > I_MOD)
		return uc_vm_value_bitop(vm, operation, value, operand);

	if (operation == I_ADD &&
	    (json_object_is_type(value, json_type_string) ||
	     json_object_is_type(operand, json_type_string))) {
		s1 = value ? json_object_get_string(value) : "null";
		s2 = operand ? json_object_get_string(operand) : "null";
		len1 = strlen(s1);
		len2 = strlen(s2);
		s = xalloc(len1 + len2 + 1);

		snprintf(s, len1 + len2 + 1, "%s%s", s1, s2);

		rv = xjs_new_string(s);

		free(s);

		return rv;
	}

	t1 = uc_cast_number(value, &n1, &d1);
	t2 = uc_cast_number(operand, &n2, &d2);

	if (t1 == json_type_double || t2 == json_type_double) {
		d1 = (t1 == json_type_double) ? d1 : (double)n1;
		d2 = (t2 == json_type_double) ? d2 : (double)n2;

		switch (operation) {
		case I_ADD:
		case I_PLUS:
			rv = uc_double_new(d1 + d2);
			break;

		case I_SUB:
			rv = uc_double_new(d1 - d2);
			break;

		case I_MUL:
			rv = uc_double_new(d1 * d2);
			break;

		case I_DIV:
			if (d2 == 0.0)
				rv = uc_double_new(INFINITY);
			else if (isnan(d2))
				rv = uc_double_new(NAN);
			else if (!isfinite(d2))
				rv = uc_double_new(isfinite(d1) ? 0.0 : NAN);
			else
				rv = uc_double_new(d1 / d2);

			break;

		case I_MOD:
			rv = uc_double_new(NAN);
			break;

		default:
			uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
			                      "undefined arithmetic operation %d",
			                      operation);
			break;
		}
	}
	else {
		switch (operation) {
		case I_ADD:
		case I_PLUS:
			rv = xjs_new_int64(n1 + n2);
			break;

		case I_SUB:
			rv = xjs_new_int64(n1 - n2);
			break;

		case I_MUL:
			rv = xjs_new_int64(n1 * n2);
			break;

		case I_DIV:
			if (n2 == 0)
				rv = uc_double_new(INFINITY);
			else
				rv = xjs_new_int64(n1 / n2);

			break;

		case I_MOD:
			rv = xjs_new_int64(n1 % n2);
			break;

		default:
			uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
			                      "undefined arithmetic operation %d",
			                      operation);
			break;
		}
	}

	return rv;
}

static void
uc_vm_insn_update_var(uc_vm *vm, enum insn_type insn)
{
	json_object *name, *val, *inc = uc_vm_stack_pop(vm);
	uc_prototype *scope, *next;

	scope = vm->globals;
	name = uc_chunk_get_constant(uc_vm_current_chunk(vm), vm->arg.u32 & 0x00FFFFFF);

	assert(json_object_is_type(name, json_type_string));

 	while (true) {
		if (json_object_object_get_ex(scope->header.jso, json_object_get_string(name), &val))
			break;

		next = scope->parent;

		if (!next) {
			if (vm->config->strict_declarations) {
				uc_vm_raise_exception(vm, EXCEPTION_REFERENCE,
				                      "access to undeclared variable %s",
				                      json_object_get_string(name));
			}

			break;
		}

		scope = next;
	}

	val = uc_vm_value_arith(vm, vm->arg.u32 >> 24, val, inc);

	json_object_object_add(scope->header.jso, json_object_get_string(name), uc_value_get(val));
	uc_vm_stack_push(vm, val);

	uc_value_put(name);
	uc_value_put(inc);
}

static void
uc_vm_insn_update_val(uc_vm *vm, enum insn_type insn)
{
	json_object *inc = uc_vm_stack_pop(vm);
	json_object *k = uc_vm_stack_pop(vm);
	json_object *v = uc_vm_stack_pop(vm);
	json_object *val = NULL;

	switch (json_object_get_type(v)) {
	case json_type_object:
	case json_type_array:
		val = uc_getval(v, k);
		uc_vm_stack_push(vm, uc_setval(v, k, uc_vm_value_arith(vm, vm->arg.u8, val, inc)));
		break;

	default:
		uc_vm_raise_exception(vm, EXCEPTION_REFERENCE,
		                      "left-hand side expression is %s",
		                      v ? "not an array or object" : "null");

		break;
	}

	uc_value_put(val);
	uc_value_put(inc);
	uc_value_put(v);
	uc_value_put(k);
}

static void
uc_vm_insn_update_upval(uc_vm *vm, enum insn_type insn)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	size_t slot = vm->arg.u32 & 0x00FFFFFF;
	uc_upvalref *ref = frame->closure->upvals[slot];
	json_object *inc = uc_vm_stack_pop(vm);
	json_object *val;

	if (ref->closed)
		val = ref->value;
	else
		val = vm->stack.entries[ref->slot];

	val = uc_vm_value_arith(vm, vm->arg.u32 >> 24, val, inc);

	uc_vm_stack_push(vm, val);

	uc_value_put(inc);

	if (ref->closed) {
		uc_value_put(ref->value);
		ref->value = uc_value_get(uc_vm_stack_peek(vm, 0));
	}
	else {
		uc_vm_stack_set(vm, ref->slot, uc_value_get(uc_vm_stack_peek(vm, 0)));
	}
}

static void
uc_vm_insn_update_local(uc_vm *vm, enum insn_type insn)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	size_t slot = vm->arg.u32 & 0x00FFFFFF;
	json_object *inc = uc_vm_stack_pop(vm);
	json_object *val;

	val = uc_vm_value_arith(vm, vm->arg.u32 >> 24,
	                        vm->stack.entries[frame->stackframe + slot], inc);

	uc_vm_stack_push(vm, val);

	uc_value_put(inc);
	uc_vm_stack_set(vm, frame->stackframe + slot, uc_value_get(uc_vm_stack_peek(vm, 0)));
}

static void
uc_vm_insn_narr(uc_vm *vm, enum insn_type insn)
{
	json_object *arr = xjs_new_array_size(vm->arg.u32);

	uc_vm_stack_push(vm, arr);
}

static void
uc_vm_insn_parr(uc_vm *vm, enum insn_type insn)
{
	json_object *arr = uc_vm_stack_peek(vm, vm->arg.u32);
	size_t idx;

	for (idx = 0; idx < vm->arg.u32; idx++)
		json_object_array_add(arr, uc_vm_stack_peek(vm, vm->arg.u32 - idx - 1));

	for (idx = 0; idx < vm->arg.u32; idx++)
		uc_vm_stack_pop(vm);

	//uc_vm_shrink(state, vm->arg.u32);
}

static void
uc_vm_insn_marr(uc_vm *vm, enum insn_type insn)
{
	json_object *src = uc_vm_stack_pop(vm);
	json_object *dst = uc_vm_stack_peek(vm, 0);
	size_t i;

	if (!json_object_is_type(src, json_type_array)) {
		uc_vm_raise_exception(vm, EXCEPTION_TYPE,
		                      "(%s) is not iterable",
		                      json_object_to_json_string(src));

		return;
	}

	for (i = 0; i < json_object_array_length(src); i++)
		json_object_array_add(dst, uc_value_get(json_object_array_get_idx(src, i)));

	uc_value_put(src);
}

static void
uc_vm_insn_nobj(uc_vm *vm, enum insn_type insn)
{
	json_object *arr = xjs_new_object();

	uc_vm_stack_push(vm, arr);
}

static void
uc_vm_insn_sobj(uc_vm *vm, enum insn_type insn)
{
	json_object *obj = uc_vm_stack_peek(vm, vm->arg.u32);
	size_t idx;

	for (idx = 0; idx < vm->arg.u32; idx += 2) {
		json_object_object_add(obj,
			json_object_get_string(uc_vm_stack_peek(vm, vm->arg.u32 - idx - 1)),
			uc_value_get(uc_vm_stack_peek(vm, vm->arg.u32 - idx - 2)));
	}

	for (idx = 0; idx < vm->arg.u32; idx++)
		uc_value_put(uc_vm_stack_pop(vm));
}

static void
uc_vm_insn_mobj(uc_vm *vm, enum insn_type insn)
{
	json_object *src = uc_vm_stack_pop(vm);
	json_object *dst = uc_vm_stack_peek(vm, 0);
	char *istr;
	size_t i;

	switch (json_object_get_type(src)) {
	case json_type_object:
		; /* a label can only be part of a statement and a declaration is not a statement */
		json_object_object_foreach(src, k, v)
			json_object_object_add(dst, k, uc_value_get(v));

		uc_value_put(src);
		break;

	case json_type_array:
		for (i = 0; i < json_object_array_length(src); i++) {
			xasprintf(&istr, "%zu", i);
			json_object_object_add(dst, istr, uc_value_get(json_object_array_get_idx(src, i)));
			free(istr);
		}

		uc_value_put(src);
		break;

	default:
		uc_vm_raise_exception(vm, EXCEPTION_TYPE,
		                      "Value (%s) is not iterable",
		                      json_object_to_json_string(src));

		break;
	}
}

static void
uc_vm_insn_arith(uc_vm *vm, enum insn_type insn)
{
	json_object *r2 = uc_vm_stack_pop(vm);
	json_object *r1 = uc_vm_stack_pop(vm);
	json_object *rv;

	rv = uc_vm_value_arith(vm, insn, r1, r2);

	uc_value_put(r1);
	uc_value_put(r2);

	uc_vm_stack_push(vm, rv);
}

static void
uc_vm_insn_plus_minus(uc_vm *vm, enum insn_type insn)
{
	struct json_object *v = uc_vm_stack_pop(vm);
	bool is_sub = (insn == I_MINUS);
	enum json_type t;
	int64_t n;
	double d;

	t = uc_cast_number(v, &n, &d);

	json_object_put(v);

	switch (t) {
	case json_type_int:
		uc_vm_stack_push(vm, xjs_new_int64(is_sub ? -n : n));
		break;

	default:
		uc_vm_stack_push(vm, uc_double_new(is_sub ? -d : d));
		break;
	}
}

static void
uc_vm_insn_bitop(uc_vm *vm, enum insn_type insn)
{
	json_object *r2 = uc_vm_stack_pop(vm);
	json_object *r1 = uc_vm_stack_pop(vm);
	json_object *rv;

	rv = uc_vm_value_bitop(vm, insn, r1, r2);

	uc_value_put(r1);
	uc_value_put(r2);

	uc_vm_stack_push(vm, rv);
}

static void
uc_vm_insn_complement(uc_vm *vm, enum insn_type insn)
{
	struct json_object *v = uc_vm_stack_pop(vm);
	int64_t n;
	double d;

	if (uc_cast_number(v, &n, &d) == json_type_double)
		n = isnan(d) ? 0 : (int64_t)d;

	json_object_put(v);

	uc_vm_stack_push(vm, xjs_new_int64(~n));
}

static void
uc_vm_insn_rel(uc_vm *vm, enum insn_type insn)
{
	json_object *r2 = uc_vm_stack_pop(vm);
	json_object *r1 = uc_vm_stack_pop(vm);
	bool res = false;

	switch (insn) {
	case I_LT:
		res = uc_cmp(TK_LT, r1, r2);
		break;

	case I_GT:
		res = uc_cmp(TK_GT, r1, r2);
		break;

	case I_EQ:
		res = uc_cmp(TK_EQ, r1, r2);
		break;

	case I_NE:
		res = uc_cmp(TK_NE, r1, r2);
		break;

	default:
		break;
	}

	uc_value_put(r1);
	uc_value_put(r2);

	uc_vm_stack_push(vm, xjs_new_boolean(res));
}

static void
uc_vm_insn_in(uc_vm *vm, enum insn_type insn)
{
	json_object *r2 = uc_vm_stack_pop(vm);
	json_object *r1 = uc_vm_stack_pop(vm);
	json_object *item;
	size_t arrlen, arridx;
	bool found = false;
	const char *key;

	switch (json_object_get_type(r2)) {
	case json_type_array:
		for (arridx = 0, arrlen = json_object_array_length(r2);
		     arridx < arrlen; arridx++) {
			item = json_object_array_get_idx(r2, arridx);

			if (uc_cmp(TK_EQ, r1, item)) {
				found = true;
				break;
			}
		}

		break;

	case json_type_object:
		key = r1 ? json_object_get_string(r1) : "null";
		found = json_object_object_get_ex(r2, key, NULL);
		break;

	default:
		found = false;
	}

	uc_value_put(r1);
	uc_value_put(r2);

	uc_vm_stack_push(vm, xjs_new_boolean(found));
}

static void
uc_vm_insn_equality(uc_vm *vm, enum insn_type insn)
{
	json_object *r2 = uc_vm_stack_pop(vm);
	json_object *r1 = uc_vm_stack_pop(vm);
	bool equal = uc_eq(r1, r2);

	uc_value_put(r1);
	uc_value_put(r2);

	uc_vm_stack_push(vm, xjs_new_boolean((insn == I_EQS) ? equal : !equal));
}

static void
uc_vm_insn_not(uc_vm *vm, enum insn_type insn)
{
	json_object *r1 = uc_vm_stack_pop(vm);

	uc_vm_stack_push(vm, xjs_new_boolean(!uc_val_is_truish(r1)));
	uc_value_put(r1);
}

static void
uc_vm_insn_jmp(uc_vm *vm, enum insn_type insn)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	uc_chunk *chunk = uc_vm_frame_chunk(frame);
	int32_t addr = vm->arg.s32;

	/* ip already has been incremented */
	addr -= 5;

	if (frame->ip + addr < chunk->entries ||
	    frame->ip + addr >= chunk->entries + chunk->count) {
		uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "jump target out of range");
		return;
	}

	frame->ip += addr;
}

static void
uc_vm_insn_jmpz(uc_vm *vm, enum insn_type insn)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	uc_chunk *chunk = uc_vm_frame_chunk(frame);
	json_object *v = uc_vm_stack_pop(vm);
	int32_t addr = vm->arg.s32;

	/* ip already has been incremented */
	addr -= 5;

	if (frame->ip + addr < chunk->entries ||
	    frame->ip + addr >= chunk->entries + chunk->count) {
		uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "jump target out of range");
		return;
	}

	if (!uc_val_is_truish(v))
		frame->ip += addr;

	uc_value_put(v);
}

static void
uc_vm_insn_next(uc_vm *vm, enum insn_type insn)
{
	json_object *k = uc_vm_stack_pop(vm);
	json_object *v = uc_vm_stack_pop(vm);
	struct lh_entry *curr;
	int64_t n;

	switch (json_object_get_type(v)) {
	case json_type_object:
		curr = k ? json_object_get_userdata(k) : json_object_get_object(v)->head;

		if (curr) {
			if (!k)
				k = xjs_new_string("[key]");

			json_object_set_userdata(k, curr->next, NULL);

			uc_vm_stack_push(vm, xjs_new_string(curr->k));

			if (insn == I_NEXTKV)
				uc_vm_stack_push(vm, uc_value_get((json_object *)curr->v));

			uc_vm_stack_push(vm, k);
			uc_value_put(v);

			return;
		}

		break;

	case json_type_array:
		if (!k)
			k = xjs_new_int64(0);

		n = json_object_get_int64(k);

		if (json_object_is_type(k, json_type_int) && n < json_object_array_length(v)) {
			json_object_int_inc(k, 1);

			if (insn == I_NEXTKV)
				uc_vm_stack_push(vm, xjs_new_int64(n));

			uc_vm_stack_push(vm, uc_value_get(json_object_array_get_idx(v, n)));

			uc_vm_stack_push(vm, k);
			uc_value_put(v);

			return;
		}

		break;

	default:
		break;
	}

	uc_vm_stack_push(vm, NULL);
	uc_vm_stack_push(vm, NULL);

	if (insn == I_NEXTKV)
		uc_vm_stack_push(vm, NULL);

	uc_value_put(k);
	uc_value_put(v);
}

static void
uc_vm_insn_close_upval(uc_vm *vm, enum insn_type insn)
{
	uc_vm_close_upvals(vm, vm->stack.count - 1);
	uc_value_put(uc_vm_stack_pop(vm));
}

static void
uc_vm_insn_call(uc_vm *vm, enum insn_type insn)
{
	json_object *fno = uc_value_get(uc_vm_stack_peek(vm, vm->arg.u32 & 0xffff));
	json_object *ctx = NULL;

	if (!uc_object_is_type(fno, UC_OBJ_CLOSURE) || !uc_object_as_closure(fno)->is_arrow)
		ctx = NULL;
	else if (vm->callframes.count > 0)
		ctx = uc_value_get(uc_vm_current_frame(vm)->ctx);

	uc_vm_call_function(vm, ctx, fno, false, vm->arg.u32);
}

static void
uc_vm_insn_mcall(uc_vm *vm, enum insn_type insn)
{
	size_t key_slot = vm->stack.count - (vm->arg.u32 & 0xffff) - 1;
	json_object *ctx = vm->stack.entries[key_slot - 1];
	json_object *key = vm->stack.entries[key_slot];
	json_object *fno = uc_getval(ctx, key);

	uc_vm_stack_set(vm, key_slot, fno);

	/* arrow functions as method calls inherit the parent ctx */
	if (uc_object_is_type(fno, UC_OBJ_CLOSURE) && uc_object_as_closure(fno)->is_arrow)
		ctx = uc_vm_current_frame(vm)->ctx;

	uc_vm_call_function(vm, uc_value_get(ctx), uc_value_get(fno), true, vm->arg.u32);
}

static void
uc_vm_insn_print(uc_vm *vm, enum insn_type insn)
{
	json_object *v = uc_vm_stack_pop(vm);
	const char *p;
	size_t len;

	switch (json_object_get_type(v)) {
	case json_type_object:
	case json_type_array:
		p = json_object_to_json_string_ext(v, JSON_C_TO_STRING_NOSLASHESCAPE|JSON_C_TO_STRING_SPACED);
		len = strlen(p);
		break;

	case json_type_string:
		p = json_object_get_string(v);
		len = json_object_get_string_len(v);
		break;

	case json_type_null:
		p = "";
		len = 0;
		break;

	default:
		p = json_object_get_string(v);
		len = strlen(p);
	}

	fwrite(p, 1, len, stdout);

	uc_value_put(v);
}

static json_object *
uc_vm_callframe_pop(uc_vm *vm)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	json_object *retval;

	/* close upvalues */
	uc_vm_close_upvals(vm, frame->stackframe);

	if (vm->stack.count > frame->stackframe)
		retval = uc_vm_stack_pop(vm);
	else
		retval = NULL;

	/* reset function stack frame */
	while (vm->stack.count > frame->stackframe)
		uc_value_put(uc_vm_stack_pop(vm));

	/* for method calls, release context as well */
	if (frame->mcall)
		uc_value_put(uc_vm_stack_pop(vm));

	/* release function */
	uc_value_put(frame->closure ? frame->closure->header.jso : NULL);
	uc_value_put(frame->cfunction ? frame->cfunction->header.jso : NULL);

	/* release context */
	uc_value_put(frame->ctx);

	vm->callframes.count--;

	return retval;
}

static void
uc_vm_output_exception(uc_vm *vm)
{
	if (vm->exception.type == EXCEPTION_USER)
		fprintf(stderr, "%s\n", vm->exception.message);
	else
		fprintf(stderr, "%s: %s\n",
			    exception_type_strings[vm->exception.type] ? exception_type_strings[vm->exception.type] : "Error",
			    vm->exception.message);

	fprintf(stderr, "%s\n\n",
		json_object_get_string(
			json_object_object_get(
				json_object_array_get_idx(vm->exception.stacktrace, 0), "context")));
}

static uc_vm_status_t
uc_vm_execute_chunk(uc_vm *vm)
{
	uc_callframe *frame = uc_vm_current_frame(vm);
	uc_chunk *chunk = uc_vm_frame_chunk(frame);
	json_object *retval;
	enum insn_type insn;

	while (chunk) {
		if (vm->trace)
			uc_dump_insn(vm, frame->ip, (insn = uc_vm_decode_insn(vm, frame, chunk)));
		else
			insn = uc_vm_decode_insn(vm, frame, chunk);

		switch (insn) {
		case I_LOAD:
		case I_LOAD8:
		case I_LOAD16:
		case I_LOAD32:
			uc_vm_insn_load(vm, insn);
			break;

		case I_LREXP:
			uc_vm_insn_load_regexp(vm, insn);
			break;

		case I_LNULL:
			uc_vm_insn_load_null(vm, insn);
			break;

		case I_LTRUE:
		case I_LFALSE:
			uc_vm_insn_load_bool(vm, insn);
			break;

		case I_LTHIS:
			uc_vm_stack_push(vm, uc_value_get(frame->ctx));
			break;

		case I_LVAR:
			uc_vm_insn_load_var(vm, insn);
			break;

		case I_LVAL:
			uc_vm_insn_load_val(vm, insn);
			break;

		case I_LUPV:
			uc_vm_insn_load_upval(vm, insn);
			break;

		case I_LLOC:
			uc_vm_insn_load_local(vm, insn);
			break;

		case I_CLFN:
		case I_ARFN:
			uc_vm_insn_load_closure(vm, insn);
			break;

		case I_NARR:
			uc_vm_insn_narr(vm, insn);
			break;

		case I_PARR:
			uc_vm_insn_parr(vm, insn);
			break;

		case I_MARR:
			uc_vm_insn_marr(vm, insn);
			break;

		case I_NOBJ:
			uc_vm_insn_nobj(vm, insn);
			break;

		case I_SOBJ:
			uc_vm_insn_sobj(vm, insn);
			break;

		case I_MOBJ:
			uc_vm_insn_mobj(vm, insn);
			break;

		case I_SVAR:
			uc_vm_insn_store_var(vm, insn);
			break;

		case I_SVAL:
			uc_vm_insn_store_val(vm, insn);
			break;

		case I_SUPV:
			uc_vm_insn_store_upval(vm, insn);
			break;

		case I_SLOC:
			uc_vm_insn_store_local(vm, insn);
			break;

		case I_UVAR:
			uc_vm_insn_update_var(vm, insn);
			break;

		case I_UVAL:
			uc_vm_insn_update_val(vm, insn);
			break;

		case I_UUPV:
			uc_vm_insn_update_upval(vm, insn);
			break;

		case I_ULOC:
			uc_vm_insn_update_local(vm, insn);
			break;

		case I_ADD:
		case I_SUB:
		case I_MUL:
		case I_DIV:
		case I_MOD:
			uc_vm_insn_arith(vm, insn);
			break;

		case I_PLUS:
		case I_MINUS:
			uc_vm_insn_plus_minus(vm, insn);
			break;

		case I_LSHIFT:
		case I_RSHIFT:
		case I_BAND:
		case I_BXOR:
		case I_BOR:
			uc_vm_insn_bitop(vm, insn);
			break;

		case I_COMPL:
			uc_vm_insn_complement(vm, insn);
			break;

		case I_EQS:
		case I_NES:
			uc_vm_insn_equality(vm, insn);
			break;

		case I_EQ:
		case I_NE:
		case I_LT:
		case I_GT:
			uc_vm_insn_rel(vm, insn);
			break;

		case I_IN:
			uc_vm_insn_in(vm, insn);
			break;

		case I_NOT:
			uc_vm_insn_not(vm, insn);
			break;

		case I_JMP:
			uc_vm_insn_jmp(vm, insn);
			break;

		case I_JMPZ:
			uc_vm_insn_jmpz(vm, insn);
			break;

		case I_NEXTK:
		case I_NEXTKV:
			uc_vm_insn_next(vm, insn);
			break;

		case I_COPY:
			uc_vm_stack_push(vm, uc_value_get(uc_vm_stack_peek(vm, vm->arg.u8)));
			break;

		case I_POP:
			uc_value_put(uc_vm_stack_pop(vm));
			break;

		case I_CUPV:
			uc_vm_insn_close_upval(vm, insn);
			break;

		case I_CALL:
			uc_vm_insn_call(vm, insn);
			frame = uc_vm_current_frame(vm);
			chunk = frame->closure ? uc_vm_frame_chunk(frame) : NULL;
			break;

		case I_MCALL:
			uc_vm_insn_mcall(vm, insn);
			frame = uc_vm_current_frame(vm);
			chunk = frame->closure ? uc_vm_frame_chunk(frame) : NULL;
			break;

		case I_RETURN:
			retval = uc_vm_callframe_pop(vm);

			if (vm->callframes.count == 0) {
				uc_value_put(retval);

				return STATUS_OK;
			}

			uc_vm_stack_push(vm, retval);

			frame = uc_vector_last(&vm->callframes);
			chunk = uc_vm_frame_chunk(frame);
			break;

		case I_PRINT:
			uc_vm_insn_print(vm, insn);
			break;

		default:
			uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "unknown opcode %d", insn);
			break;
		}

		/* previous instruction raised exception */
		if (vm->exception.type != EXCEPTION_NONE) {
			/* walk up callframes until something handles the exception or the root is reached */
			while (!uc_vm_handle_exception(vm)) {
				/* no further callframe to pop, report unhandled exception and terminate */
				if (vm->callframes.count == 1) {
					uc_vm_output_exception(vm);

					return ERROR_RUNTIME;
				}

				/* if VM returned into native function, don't bubble up */
				if (!chunk)
					return ERROR_RUNTIME;

				/* no exception handler in current function, pop callframe */
				uc_value_put(uc_vm_callframe_pop(vm));

				/* resume execution at topmost remaining callframe */
				frame = uc_vector_last(&vm->callframes);
				chunk = uc_vm_frame_chunk(frame);
			}
		}
	}

	return STATUS_OK;
}

static uc_vm_status_t
uc_vm_preload(uc_vm *vm, json_object *modules)
{
	json_object *requirefn, *module, *name;
	uc_exception_type_t ex;
	size_t i;

	if (!json_object_is_type(modules, json_type_array))
		return STATUS_OK;

	requirefn = uc_prototype_lookup(vm->globals, "require");

	if (!uc_object_is_type(requirefn, UC_OBJ_CFUNCTION))
		return STATUS_OK;

	for (i = 0; i < json_object_array_length(modules); i++) {
		name = json_object_array_get_idx(modules, i);

		uc_vm_stack_push(vm, uc_value_get(requirefn));
		uc_vm_stack_push(vm, uc_value_get(name));

		ex = uc_vm_call(vm, false, 1);

		if (ex)
			return ERROR_RUNTIME;

		module = uc_vm_stack_pop(vm);

		uc_value_put(uc_setval(vm->globals->header.jso, name, module));
	}

	return STATUS_OK;
}

uc_vm_status_t
uc_vm_execute(uc_vm *vm, uc_function *fn, uc_prototype *globals, json_object *modules)
{
	uc_closure *closure = uc_closure_new(fn, false);
	uc_callframe *frame;
	uc_vm_status_t rv;

	vm->globals = globals;
	uc_value_get(globals ? globals->header.jso : NULL);

	uc_vector_grow(&vm->callframes);

	frame = &vm->callframes.entries[vm->callframes.count++];
	frame->closure = closure;
	frame->stackframe = 0;
	frame->ip = uc_vm_frame_chunk(frame)->entries;

	if (vm->trace) {
		size_t msglen = 0;
		char *msg = NULL;

		format_source_context(&msg, &msglen,
			fn->source, 0, true);

		fprintf(stderr, "%s", msg);
		free(msg);

		uc_vm_frame_dump(vm, frame);
	}

	//uc_vm_stack_push(vm, closure->header.jso);
	uc_vm_stack_push(vm, NULL);

	rv = uc_vm_preload(vm, modules);

	if (rv != STATUS_OK)
		uc_vm_output_exception(vm);
	else
		rv = uc_vm_execute_chunk(vm);

	uc_value_put(vm->globals->header.jso);
	vm->globals = NULL;

	return rv;
}

uc_exception_type_t
uc_vm_call(uc_vm *vm, bool mcall, size_t nargs)
{
	json_object *ctx = mcall ? uc_value_get(uc_vm_stack_peek(vm, nargs - 1)) : NULL;
	json_object *fno = uc_value_get(uc_vm_stack_peek(vm, nargs));

	if (uc_vm_call_function(vm, ctx, fno, mcall, nargs & 0xffff)) {
		if (!uc_object_is_type(fno, UC_OBJ_CFUNCTION))
			uc_vm_execute_chunk(vm);
	}

	return vm->exception.type;
}
