lib.c

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

/**
 * # Builtin functions
 *
 * The core namespace is not an actual module but refers to the set of
 * builtin functions and properties available to `ucode` scripts.
 *
 * @module core
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <time.h>
#include <dlfcn.h>
#include <libgen.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fnmatch.h>
#include <assert.h>

#include "json-c-compat.h"

#include "ucode/lexer.h"
#include "ucode/compiler.h"
#include "ucode/vm.h"
#include "ucode/lib.h"
#include "ucode/source.h"
#include "ucode/program.h"
#include "ucode/platform.h"

static void
format_context_line(uc_stringbuf_t *buf, const char *line, size_t off, bool compact)
{
	unsigned padlen, i;
	const char *p;

	for (p = line, padlen = 0; *p != '\n' && *p != '\0'; p++) {
		if (compact && (p - line) == (ptrdiff_t)off)
			ucv_stringbuf_append(buf, "\033[22m");

		switch (*p) {
		case '\t':
			ucv_stringbuf_append(buf, "    ");
			if (p < line + off)
				padlen += 4;
			break;

		case '\r':
		case '\v':
			ucv_stringbuf_append(buf, " ");
			if (p < line + off)
				padlen++;
			break;

		default:
			ucv_stringbuf_addstr(buf, p, 1);
			if (p < line + off)
				padlen++;
		}
	}

	if (compact) {
		ucv_stringbuf_append(buf, "\033[m\n");

		return;
	}

	ucv_stringbuf_append(buf, "`\n  ");

	if (padlen < strlen("Near here ^")) {
		for (i = 0; i < padlen; i++)
			ucv_stringbuf_append(buf, " ");

		ucv_stringbuf_append(buf, "^-- Near here\n");
	}
	else {
		ucv_stringbuf_append(buf, "Near here ");

		for (i = strlen("Near here "); i < padlen; i++)
			ucv_stringbuf_append(buf, "-");

		ucv_stringbuf_append(buf, "^\n");
	}
}

static char *
source_filename(uc_source_t *src, uint32_t line)
{
	const char *name = src->filename ? basename(src->filename) : "[?]";
	static char buf[sizeof("xxxxxxxxx.uc:0000000000")];
	size_t len = strlen(name);

	if (len > 12)
		snprintf(buf, sizeof(buf), "...%s:%u", name + (len - 9), line);
	else
		snprintf(buf, sizeof(buf), "%12s:%u", name, line);

	return buf;
}

bool
uc_source_context_format(uc_stringbuf_t *buf, uc_source_t *src, size_t off, bool compact)
{
	size_t len, rlen;
	bool truncated;
	char line[256];
	long srcpos;
	int eline;

	srcpos = ftell(src->fp);

	if (srcpos == -1)
		return false;

	fseek(src->fp, 0, SEEK_SET);

	truncated = false;
	eline = 1;
	rlen = 0;

	while (fgets(line, sizeof(line), src->fp)) {
		len = strlen(line);
		rlen += len;

		if (rlen >= off) {
			if (compact)
				ucv_stringbuf_printf(buf, "\033[2;40;97m%17s  %s",
					source_filename(src, eline),
					truncated ? "..." : "");
			else
				ucv_stringbuf_printf(buf, "\n `%s",
					truncated ? "..." : "");

			format_context_line(buf, line, len - (rlen - off) + (truncated ? 3 : 0), compact);
			break;
		}

		truncated = (len > 0 && line[len-1] != '\n');
		eline += !truncated;
	}

	fseek(src->fp, srcpos, SEEK_SET);

	return true;
}

bool
uc_error_context_format(uc_stringbuf_t *buf, uc_source_t *src, uc_value_t *stacktrace, size_t off)
{
	uc_value_t *e, *fn, *file, *line, *byte;
	const char *path;
	size_t idx;

	for (idx = 0; idx < (stacktrace ? ucv_array_length(stacktrace) : 0); idx++) {
		e = ucv_array_get(stacktrace, idx);
		fn = ucv_object_get(e, "function", NULL);
		file = ucv_object_get(e, "filename", NULL);

		if (idx == 0) {
			path = (file && strcmp(ucv_string_get(file), "[stdin]"))
				? ucv_string_get(file) : NULL;

			if (path && fn)
				ucv_stringbuf_printf(buf, "In %s(), file %s, ", ucv_string_get(fn), path);
			else if (fn)
				ucv_stringbuf_printf(buf, "In %s(), ", ucv_string_get(fn));
			else if (path)
				ucv_stringbuf_printf(buf, "In %s, ", path);
			else
				ucv_stringbuf_append(buf, "In ");

			ucv_stringbuf_printf(buf, "line %" PRId64 ", byte %" PRId64 ":\n",
				ucv_int64_get(ucv_object_get(e, "line", NULL)),
				ucv_int64_get(ucv_object_get(e, "byte", NULL)));
		}
		else {
			line = ucv_object_get(e, "line", NULL);
			byte = ucv_object_get(e, "byte", NULL);

			ucv_stringbuf_printf(buf, "  called from %s%s (%s",
				fn ? "function " : "anonymous function",
				fn ? ucv_string_get(fn) : "",
				file ? ucv_string_get(file) : "");

			if (line && byte)
				ucv_stringbuf_printf(buf, ":%" PRId64 ":%" PRId64 ")\n",
					ucv_int64_get(line),
					ucv_int64_get(byte));
			else
				ucv_stringbuf_append(buf, "[C])\n");
		}
	}

	return uc_source_context_format(buf, src, off, false);
}

void
uc_error_message_indent(char **msg) {
	uc_stringbuf_t *buf = xprintbuf_new();
	char *s, *p, *nl;
	size_t len;

	if (!msg || !*msg)
		return;

	s = *msg;
	len = strlen(s);

	while (len > 0 && s[len-1] == '\n')
		s[--len] = 0;

	for (p = s, nl = strchr(p, '\n'); p != NULL;
	     p = nl ? nl + 1 : NULL, nl = p ? strchr(p, '\n') : NULL)
	{
		if (!nl)
			ucv_stringbuf_printf(buf, "  | %s", p);
		else if (nl != p)
			ucv_stringbuf_printf(buf, "  | %.*s\n", (int)(nl - p), p);
		else
			ucv_stringbuf_append(buf, "  |\n");
	}

	ucv_stringbuf_append(buf, "\n");

	*msg = buf->buf;

	free(buf);
	free(s);
}

static char *uc_cast_string(uc_vm_t *vm, uc_value_t **v, bool *freeable) {
	if (ucv_type(*v) == UC_STRING) {
		*freeable = false;

		return _ucv_string_get(v);
	}

	*freeable = true;

	return ucv_to_string(vm, *v);
}

static void
uc_vm_ctx_push(uc_vm_t *vm)
{
	uc_value_t *ctx = NULL;

	if (vm->callframes.count >= 2)
		ctx = vm->callframes.entries[vm->callframes.count - 2].ctx;

	uc_vm_stack_push(vm, ucv_get(ctx));
}

static uc_value_t *
uc_print_common(uc_vm_t *vm, size_t nargs, FILE *fh)
{
	uc_value_t *item;
	size_t reslen = 0;
	size_t len = 0;
	size_t arridx;
	char *p;

	for (arridx = 0; arridx < nargs; arridx++) {
		item = uc_fn_arg(arridx);

		if (ucv_type(item) == UC_STRING) {
			len = ucv_string_length(item);
			reslen += fwrite(ucv_string_get(item), 1, len, fh);
		}
		else if (item != NULL) {
			p = ucv_to_string(vm, item);
			len = strlen(p);
			reslen += fwrite(p, 1, len, fh);
			free(p);
		}
	}

	return ucv_int64_new(reslen);
}


/**
 * Print any of the given values to stdout.
 *
 * The `print()` function writes a string representation of each given argument
 * to stdout and returns the amount of bytes written.
 *
 * String values are printed as-is, integer and double values are printed in
 * decimal notation, boolean values are printed as `true` or `false` while
 * arrays and objects are converted to their JSON representation before being
 * written to the standard output. The `null` value is represented by an empty
 * string so `print(null)` would print nothing. Resource values are printed in
 * the form `<type address>`, e.g. `<fs.file 0x7f60f0981760>`.
 *
 * If resource, array or object values contain a `tostring()` function in their
 * prototypes, then this function is invoked to obtain an alternative string
 * representation of the value.
 *
 * Examples:
 *
 * ```javascript
 * print(1 != 2);                       // Will print 'true'
 * print(0xff);                         // Will print '255'
 * print(2e3);                          // Will print '2000'
 * print(null);                         // Will print nothing
 * print({ hello: true, world: 123 });  // Will print '{ "hello": true, "world": 123 }'
 * print([1,2,3]);                      // Will print '[ 1, 2, 3 ]'
 *
 * print(proto({ foo: "bar" },          // Will print 'MyObj'
 *   { tostring: () => "MyObj" }));     // instead of '{ "foo": "bar" }'
 *
 * ```
 *
 * Returns the amount of bytes printed.
 *
 * @function module:core#print
 *
 * @param {...*} values
 * Arbitrary values to print
 *
 * @returns {number}
 */
static uc_value_t *
uc_print(uc_vm_t *vm, size_t nargs)
{
	return uc_print_common(vm, nargs, vm->output);
}

/**
 * Determine the length of the given object, array or string.
 *
 * Returns the length of the given value.
 *
 *  - For strings, the length is the amount of bytes within the string
 *  - For arrays, the length is the amount of array elements
 *  - For objects, the length is defined as the amount of keys
 *
 * Returns `null` if the given argument is not an object, array or string.
 *
 * @function module:core#length
 *
 * @param {Object|Array|string} x - The input object, array, or string.
 *
 * @returns {?number} - The length of the input.
 *
 * @example
 * length("test")                             // 4
 * length([true, false, null, 123, "test"])   // 5
 * length({foo: true, bar: 123, baz: "test"}) // 3
 * length({})                                 // 0
 * length(true)                               // null
 * length(10.0)                               // null
 */
static uc_value_t *
uc_length(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *arg = uc_fn_arg(0);

	switch (ucv_type(arg)) {
	case UC_OBJECT:
		return ucv_int64_new(ucv_object_length(arg));

	case UC_ARRAY:
		return ucv_int64_new(ucv_array_length(arg));

	case UC_STRING:
		return ucv_int64_new(ucv_string_length(arg));

	default:
		return NULL;
	}
}

static int
uc_uniq_ucv_equal(const void *k1, const void *k2);

static uc_value_t *
uc_index(uc_vm_t *vm, size_t nargs, bool right)
{
	uc_value_t *stack = uc_fn_arg(0);
	uc_value_t *needle = uc_fn_arg(1);
	const char *sstr, *nstr, *p;
	size_t arridx, slen, nlen;
	ssize_t ret = -1;

	switch (ucv_type(stack)) {
	case UC_ARRAY:
		if (right) {
			for (arridx = ucv_array_length(stack); arridx > 0; arridx--) {
				if (uc_uniq_ucv_equal(ucv_array_get(stack, arridx - 1), needle)) {
					ret = (ssize_t)(arridx - 1);
					break;
				}
			}
		}
		else {
			for (arridx = 0, slen = ucv_array_length(stack); arridx < slen; arridx++) {
				if (uc_uniq_ucv_equal(ucv_array_get(stack, arridx), needle)) {
					ret = (ssize_t)arridx;
					break;
				}
			}
		}

		return ucv_int64_new(ret);

	case UC_STRING:
		if (ucv_type(needle) == UC_STRING) {
			sstr = ucv_string_get(stack);
			slen = ucv_string_length(stack);
			nstr = ucv_string_get(needle);
			nlen = ucv_string_length(needle);

			if (slen == nlen) {
				if (memcmp(sstr, nstr, nlen) == 0)
					ret = 0;
			}
			else if (slen > nlen) {
				if (right) {
					p = sstr + slen - nlen;

					do {
						if (memcmp(p, nstr, nlen) == 0) {
							ret = (ssize_t)(p - sstr);
							break;
						}
					}
					while (--p != sstr);
				}
				else if (nlen > 0) {
					p = (const char *)memmem(sstr, slen, nstr, nlen);

					if (p)
						ret = (ssize_t)(p - sstr);
				}
				else {
					ret = 0;
				}
			}
		}

		return ucv_int64_new(ret);

	default:
		return NULL;
	}
}

/**
 * Finds the given value passed as the second argument within the array or
 * string specified in the first argument.
 *
 * Returns the first matching array index or first matching string offset or
 * `-1` if the value was not found.
 *
 * Returns `null` if the first argument was neither an array nor a string.
 *
 * @function module:core#index
 *
 * @param {Array|string} arr_or_str
 * The array or string to search for the value.
 *
 * @param {*} needle
 * The value to find within the array or string.
 *
 * @returns {?number}
 *
 * @example
 * index("Hello hello hello", "ll")          // 2
 * index([ 1, 2, 3, 1, 2, 3, 1, 2, 3 ], 2)   // 1
 * index("foo", "bar")                       // -1
 * index(["Red", "Blue", "Green"], "Brown")  // -1
 * index(123, 2)                             // null
 */
static uc_value_t *
uc_lindex(uc_vm_t *vm, size_t nargs)
{
	return uc_index(vm, nargs, false);
}

/**
 * Finds the given value passed as the second argument within the array or
 * string specified in the first argument.
 *
 * Returns the last matching array index or last matching string offset or
 * `-1` if the value was not found.
 *
 * Returns `null` if the first argument was neither an array nor a string.
 *
 * @function module:core#rindex
 *
 * @param {Array|string} arr_or_str
 * The array or string to search for the value.
 *
 * @param {*} needle
 * The value to find within the array or string.
 *
 * @returns {?number}
 *
 * @example
 * rindex("Hello hello hello", "ll")          // 14
 * rindex([ 1, 2, 3, 1, 2, 3, 1, 2, 3 ], 2)   //  7
 * rindex("foo", "bar")                       // -1
 * rindex(["Red", "Blue", "Green"], "Brown")  // -1
 * rindex(123, 2)                             // null
 */
static uc_value_t *
uc_rindex(uc_vm_t *vm, size_t nargs)
{
	return uc_index(vm, nargs, true);
}

static bool
assert_mutable(uc_vm_t *vm, uc_value_t *val)
{
	if (ucv_is_constant(val)) {
		uc_vm_raise_exception(vm, EXCEPTION_TYPE,
		                      "%s value is immutable",
		                      ucv_typename(val));

		return false;
	}

	return true;
}

static bool
assert_mutable_array(uc_vm_t *vm, uc_value_t *val)
{
	if (ucv_type(val) != UC_ARRAY)
		return false;

	return assert_mutable(vm, val);
}

/**
 * Pushes the given argument(s) to the given array.
 *
 * Returns the last pushed value.
 *
 * @function module:core#push
 *
 * @param {Array} arr
 * The array to push values to.
 *
 * @param {...*} [values]
 * The values to push.
 *
 * @returns {*}
 *
 * @example
 * let x = [ 1, 2, 3 ];
 * push(x, 4, 5, 6);    // 6
 * print(x, "\n");      // [ 1, 2, 3, 4, 5, 6 ]
 */
static uc_value_t *
uc_push(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *arr = uc_fn_arg(0);
	uc_value_t *item = NULL;
	size_t arridx;

	if (!assert_mutable_array(vm, arr))
		return NULL;

	for (arridx = 1; arridx < nargs; arridx++) {
		item = uc_fn_arg(arridx);
		ucv_array_push(arr, ucv_get(item));
	}

	return ucv_get(item);
}

/**
 * Pops the last item from the given array and returns it.
 *
 * Returns `null` if the array was empty or if a non-array argument was passed.
 *
 * @function module:core#pop
 *
 * @param {Array} arr
 * The input array.
 *
 * @returns {*}
 *
 * @example
 * let x = [ 1, 2, 3 ];
 * pop(x);          // 3
 * print(x, "\n");  // [ 1, 2 ]
 */
static uc_value_t *
uc_pop(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *arr = uc_fn_arg(0);

	if (!assert_mutable_array(vm, arr))
		return NULL;

	return ucv_array_pop(arr);
}

/**
 * Pops the first item from the given array and returns it.
 *
 * Returns `null` if the array was empty or if a non-array argument was passed.
 *
 * @function module:core#shift
 *
 * @param {Array} arr
 * The array from which to pop the first item.
 *
 * @returns {*}
 *
 * @example
 * let x = [ 1, 2, 3 ];
 * shift(x);        // 1
 * print(x, "\n");  // [ 2, 3 ]
 */
static uc_value_t *
uc_shift(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *arr = uc_fn_arg(0);

	if (!assert_mutable_array(vm, arr))
		return NULL;

	return ucv_array_shift(arr);
}

/**
 * Add the given values to the beginning of the array passed via first argument.
 *
 * Returns the last value added to the array.
 *
 * @function module:core#unshift
 *
 * @param {Array} arr
 * The array to which the values will be added.
 *
 * @param {...*}
 * Values to add.
 *
 * @returns {*}
 *
 * @example
 * let x = [ 3, 4, 5 ];
 * unshift(x, 1, 2);  // 2
 * print(x, "\n");    // [ 1, 2, 3, 4, 5 ]
 */
static uc_value_t *
uc_unshift(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *arr = uc_fn_arg(0);
	uc_value_t *item;
	size_t i;

	if (!assert_mutable_array(vm, arr))
		return NULL;

	for (i = 1; i < nargs; i++) {
		item = uc_fn_arg(nargs - i);
		ucv_array_unshift(arr, ucv_get(item));
	}

	return (nargs > 1) ? ucv_get(uc_fn_arg(nargs - 1)) : NULL;
}

/**
 * Converts each given numeric value to a byte and return the resulting string.
 * Invalid numeric values or values < 0 result in `\0` bytes, values larger than
 * 255 are truncated to 255.
 *
 * Returns a new strings consisting of the given byte values.
 *
 * @function module:core#chr
 *
 * @param {...number} n1
 * The numeric values.
 *
 * @returns {string}
 *
 * @example
 * chr(65, 98, 99);  // "Abc"
 * chr(-1, 300);     // string consisting of an `0x0` and a `0xff` byte
 */
static uc_value_t *
uc_chr(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *rv = NULL;
	size_t idx;
	int64_t n;
	char *str;

	if (!nargs)
		return ucv_string_new_length("", 0);

	str = xalloc(nargs);

	for (idx = 0; idx < nargs; idx++) {
		n = ucv_to_integer(uc_fn_arg(idx));

		if (n < 0)
			n = 0;
		else if (n > 255)
			n = 255;

		str[idx] = (char)n;
	}

	rv = ucv_string_new_length(str, nargs);
	free(str);

	return rv;
}

/**
 * Raise an exception with the given message and abort execution.
 *
 * @function module:core#die
 *
 * @param {string} msg
 * The error message.
 *
 * @throws {Error}
 * The error with the given message.
 *
 * @example
 * die(msg);
 */
static uc_value_t *
uc_die(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *msg = uc_fn_arg(0);
	bool freeable = false;
	char *s;

	s = msg ? uc_cast_string(vm, &msg, &freeable) : "Died";

	uc_vm_raise_exception(vm, EXCEPTION_USER, "%s", s);

	if (freeable)
		free(s);

	return NULL;
}

/**
 * Check whether the given key exists within the given object value.
 *
 * Returns `true` if the given key is present within the object passed as the
 * first argument, otherwise `false`.
 *
 * @function module:core#exists
 *
 * @param {Object} obj
 * The input object.
 *
 * @param {string} key
 * The key to check for existence.
 *
 * @returns {boolean}
 *
 * @example
 * let x = { foo: true, bar: false, qrx: null };
 * exists(x, 'foo');  // true
 * exists(x, 'qrx');  // true
 * exists(x, 'baz');  // false
 */
static uc_value_t *
uc_exists(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *obj = uc_fn_arg(0);
	uc_value_t *key = uc_fn_arg(1);
	bool found, freeable;
	char *k;

	if (ucv_type(obj) != UC_OBJECT)
		return ucv_boolean_new(false);

	k = uc_cast_string(vm, &key, &freeable);

	ucv_object_get(obj, k, &found);

	if (freeable)
		free(k);

	return ucv_boolean_new(found);
}

/**
 * Terminate the interpreter with the given exit code.
 *
 * This function does not return.
 *
 * @function module:core#exit
 *
 * @param {number} n
 * The exit code.
 *
 * @example
 * exit();
 * exit(5);
 */
static uc_value_t *
uc_exit(uc_vm_t *vm, size_t nargs)
{
	int64_t n = ucv_to_integer(uc_fn_arg(0));

	vm->arg.s32 = (int32_t)n;
	uc_vm_raise_exception(vm, EXCEPTION_EXIT, "Terminated");

	return NULL;
}

/**
 * Query an environment variable or then entire environment.
 *
 * Returns the value of the given environment variable, or - if omitted - a
 * dictionary containing all environment variables.
 *
 * @function module:core#getenv
 *
 * @param {string} [name]
 * The name of the environment variable.
 *
 * @returns {string|Object<string, string>}
 */
static uc_value_t *
uc_getenv(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *key = uc_fn_arg(0), *rv = NULL;
	extern char **environ;
	char **env = environ;
	char *k, *v;

	if (!key) {
		rv = ucv_object_new(vm);

		while (*env) {
			v = strchr(*env, '=');

			if (v) {
				xasprintf(&k, "%.*s", (int)(v - *env), *env);
				ucv_object_add(rv, k, ucv_string_new(v + 1));
				free(k);
			}

			env++;
		}
	}
	else if (ucv_type(key) == UC_STRING) {
		k = ucv_string_get(key);
		v = getenv(k);

		if (v)
			rv = ucv_string_new(v);
	}

	return rv;
}

/**
 * Filter the array passed as the first argument by invoking the function
 * specified in the second argument for each array item.
 *
 * If the invoked function returns a truthy result, the item is retained,
 * otherwise, it is dropped. The filter function is invoked with three
 * arguments:
 *
 * 1. The array value
 * 2. The current index
 * 3. The array being filtered
 *
 * (Note that the `map` function behaves similarly to `filter` with respect
 * to its `fn` parameters.)
 *
 * Returns a new array containing only retained items, in the same order as
 * the input array.
 *
 * @function module:core#filter
 *
 * @param {Array} arr
 * The input array.
 *
 * @param {Function} fn
 * The filter function.
 *
 * @returns {Array}
 *
 * @example
 * // filter out any empty string:
 * a = filter(["foo", "", "bar", "", "baz"], length)
 * // a = ["foo", "bar", "baz"]
 *
 * // filter out any non-number type:
 * a = filter(["foo", 1, true, null, 2.2], function(v) {
 *     return (type(v) == "int" || type(v) == "double");
 * });
 * // a = [1, 2.2]
 */
static uc_value_t *
uc_filter(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *obj = uc_fn_arg(0);
	uc_value_t *func = uc_fn_arg(1);
	uc_value_t *rv, *arr;
	size_t arridx, arrlen;

	if (ucv_type(obj) != UC_ARRAY)
		return NULL;

	arr = ucv_array_new(vm);

	for (arrlen = ucv_array_length(obj), arridx = 0; arridx < arrlen; arridx++) {
		uc_vm_ctx_push(vm);
		uc_vm_stack_push(vm, ucv_get(func));
		uc_vm_stack_push(vm, ucv_get(ucv_array_get(obj, arridx)));
		uc_vm_stack_push(vm, ucv_int64_new(arridx));
		uc_vm_stack_push(vm, ucv_get(obj));

		if (uc_vm_call(vm, true, 3)) {
			ucv_put(arr);

			return NULL;
		}

		rv = uc_vm_stack_pop(vm);

		if (ucv_is_truish(rv))
			ucv_array_push(arr, ucv_get(ucv_array_get(obj, arridx)));

		ucv_put(rv);
	}

	return arr;
}

/**
 * Converts the given hexadecimal string into a number.
 *
 * Returns the resulting integer value or `NaN` if the input value cannot be
 * interpreted as hexadecimal number.
 *
 * @function module:core#hex
 *
 * @param {*} x
 * The hexadecimal string to be converted.
 *
 * @returns {number}
 */
static uc_value_t *
uc_hex(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *val = uc_fn_arg(0);
	char *e, *v;
	int64_t n;

	v = ucv_string_get(val);

	if (!v || !isxdigit(*v))
		return ucv_double_new(NAN);

	n = strtoll(v, &e, 16);

	if (e == v || *e)
		return ucv_double_new(NAN);

	return ucv_int64_new(n);
}

/**
 * Converts the given value to an integer, using an optional base.
 *
 * Returns `NaN` if the value is not convertible.
 *
 * @function module:core#int
 *
 * @param {*} x
 * The value to be converted to an integer.
 *
 * @param {int} [base]
 * The base into which the value is to be converted, the default is 10.
 * Note that the base parameter is ignored if the `x` value is already numeric.
 *
 * @returns {number}
 *
 * @example
 * int("123")         // Returns 123
 * int("123", 10)     // 123
 * int("10 or more")  // 10
 * int("12.3")        // 12
 * int("123", 7)      // 66
 * int("abc", 16)     // 2748
 * int("xyz", 36)     // 44027
 * int(10.10, "2")    // 10, the invalid base is ignored
 * int("xyz", 16)     // NaN, bad value
 * int("1010", "2")   // NaN, bad base
 */
static uc_value_t *
uc_int(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *val = uc_fn_arg(0);
	uc_value_t *base = uc_fn_arg(1);
	char *e, *v;
	int64_t n;

	if (ucv_type(val) == UC_STRING) {
		errno = 0;
		v = ucv_string_get(val);
		n = strtoll(v, &e, base ? ucv_int64_get(base) : 10);

		if (e == v)
			return ucv_double_new(NAN);
	}
	else {
		n = ucv_to_integer(val);
	}

	if (errno == EINVAL || errno == ERANGE)
		return ucv_double_new(NAN);

	return ucv_int64_new(n);
}

/**
 * Joins the array passed as the second argument into a string, using the
 * separator passed in the first argument as glue.
 *
 * Returns `null` if the second argument is not an array.
 *
 * @function module:core#join
 *
 * @param {string} sep
 * The separator to be used in joining the array elements.
 *
 * @param {Array} arr
 * The array to be joined into a string.
 *
 * @returns {?string}
 */
static uc_value_t *
uc_join(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *sep = uc_fn_arg(0);
	uc_value_t *arr = uc_fn_arg(1);
	size_t arrlen, arridx;
	uc_stringbuf_t *buf;

	if (ucv_type(arr) != UC_ARRAY)
		return NULL;

	buf = ucv_stringbuf_new();

	for (arrlen = ucv_array_length(arr), arridx = 0; arridx < arrlen; arridx++) {
		if (arridx > 0)
			ucv_to_stringbuf(vm, buf, sep, false);

		ucv_to_stringbuf(vm, buf, ucv_array_get(arr, arridx), false);
	}

	return ucv_stringbuf_finish(buf);
}

/**
 * Enumerates all object key names.
 *
 * Returns an array of all key names present in the passed object.
 * Returns `null` if the given argument is not an object.
 *
 * @function module:core#keys
 *
 * @param {object} obj
 * The object from which to retrieve the key names.
 *
 * @returns {?Array}
 */
static uc_value_t *
uc_keys(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *obj = uc_fn_arg(0);
	uc_value_t *arr = NULL;

	if (ucv_type(obj) != UC_OBJECT)
		return NULL;

	arr = ucv_array_new(vm);

	ucv_object_foreach(obj, key, val) {
		(void)val;
		ucv_array_push(arr, ucv_string_new(key));
	}

	return arr;
}

/**
 * Convert the given string to lowercase and return the resulting string.
 *
 * Returns `null` if the given argument could not be converted to a string.
 *
 * @function module:core#lc
 *
 * @param {string} s
 * The input string.
 *
 * @returns {?string}
 * The lowercase string.
 *
 * @example
 * lc("HeLLo WoRLd!");  // "hello world!"
 */
static uc_value_t *
uc_lc(uc_vm_t *vm, size_t nargs)
{
	char *str = ucv_to_string(vm, uc_fn_arg(0));
	uc_value_t *rv = NULL;
	char *p;

	if (!str)
		return NULL;

	for (p = str; *p; p++)
		if (*p >= 'A' && *p <= 'Z')
			*p |= 32;

	rv = ucv_string_new(str);

	free(str);

	return rv;
}

/**
 * Transform the array passed as the first argument by invoking the function
 * specified in the second argument for each array item.
 *
 * The mapping function is invoked with three arguments (see examples, below,
 * for some possibly counterintuitive usage):
 *
 * 1. The array value
 * 2. The current index
 * 3. The array being filtered
 *
 * (Note that the `filter` function behaves similarly to `map` with respect
 * to its `fn` parameters.)
 *
 * Returns a new array of the same length as the input array containing the
 * transformed values.
 *
 * @function module:core#map
 *
 * @param {Array} arr
 * The input array.
 *
 * @param {Function} fn
 * The mapping function.
 *
 * @returns {Array}
 *
 * @example
 * // turn into an array of string lengths:
 * a = map(["Apple", "Banana", "Bean"], length);
 * // a = [5, 6, 4]
 *
 * // map to type names:
 * a = map(["foo", 1, true, null, 2.2], type);
 * // a = ["string", "int", "bool", null, "double"]
 *
 * // attempt to naively use built-in 'int' to map an array:
 * a = map(["x", "2", "11", "7"], int)
 * // a = [NaN, NaN, 3, NaN]
 * //
 * // This is a direct result of 'int' being provided the second, index parameter
 * // for its base value in the conversion.
 * //
 * // The resulting calls to 'int' are as follows:
 * //  int("x",  0, [...]) - convert "x"  to base 0, 'int' ignores the third value
 * //  int("2",  1, [...]) - convert "2"  to base 1, digit out of range, so NaN
 * //  int("11", 2, [...]) - convert "11" to base 2, produced unexpected 3
 * //  int("7",  3, [...]) - convert "7"  to base 3, digit out of range, NaN again
 *
 * // remedy this by using an arrow function to ensure the proper base value
 * // (in this case, the default of 10) is passed to 'int':
 * a = map(["x", "2", "1", "7"], (x) => int(x))
 * // a = [NaN, 2, 1, 7]
 *
 * // convert base-2 values:
 * a = map(["22", "1010", "0001", "0101"], (x) => int(x, 2))
 * // a = [NaN, 10, 1, 5]
 */
static uc_value_t *
uc_map(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *obj = uc_fn_arg(0);
	uc_value_t *func = uc_fn_arg(1);
	uc_value_t *arr, *rv;
	size_t arridx, arrlen;

	if (ucv_type(obj) != UC_ARRAY)
		return NULL;

	arr = ucv_array_new(vm);

	for (arrlen = ucv_array_length(obj), arridx = 0; arridx < arrlen; arridx++) {
		uc_vm_ctx_push(vm);
		uc_vm_stack_push(vm, ucv_get(func));
		uc_vm_stack_push(vm, ucv_get(ucv_array_get(obj, arridx)));
		uc_vm_stack_push(vm, ucv_int64_new(arridx));
		uc_vm_stack_push(vm, ucv_get(obj));

		if (uc_vm_call(vm, true, 3)) {
			ucv_put(arr);

			return NULL;
		}

		rv = uc_vm_stack_pop(vm);

		ucv_array_push(arr, rv);
	}

	return arr;
}

/**
 * Without further arguments, this function returns the byte value of the first
 * character in the given string.
 *
 * If an offset argument is supplied, the byte value of the character at this
 * position is returned. If an invalid index is supplied, the function will
 * return `null`. Negative index entries are counted towards the end of the
 * string, e.g. `-2` will return the value of the second last character.
 *
 * Returns the byte value of the character.
 * Returns `null` if the offset is invalid or if the input is not a string.
 *
 * @function module:core#ord
 *
 * @param {string} s
 * The input string.
 *
 * @param {number} [offset]
 * The offset of the character.
 *
 * @returns {?number}
 *
 * @example
 * ord("Abc");         // 65
 * ord("Abc", 0);      // 65
 * ord("Abc", 1);      // 98
 * ord("Abc", 2);      // 99
 * ord("Abc", 10);     // null
 * ord("Abc", -10);    // null
 * ord("Abc", "nan");  // null
 */
static uc_value_t *
uc_ord(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *obj = uc_fn_arg(0);
	const char *str;
	int64_t n = 0;
	size_t len;

	if (ucv_type(obj) != UC_STRING)
		return NULL;

	str = ucv_string_get(obj);
	len = ucv_string_length(obj);

	if (nargs > 1) {
		n = ucv_int64_get(uc_fn_arg(1));

		if (errno == EINVAL)
			return NULL;

		if (n < 0)
			n += len;
	}

	if (n < 0 || (uint64_t)n >= len)
		return NULL;

	return ucv_int64_new((uint8_t)str[n]);
}

/**
 * Query the type of the given value.
 *
 * Returns the type of the given value as a string which might be one of
 * `"function"`, `"object"`, `"array"`, `"double"`, `"int"`, or `"bool"`.
 *
 * Returns `null` when no value or `null` is passed.
 *
 * @function module:core#type
 *
 * @param {*} x
 * The value to determine the type of.
 *
 * @returns {?string}
 */
static uc_value_t *
uc_type(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *v = uc_fn_arg(0);
	uc_type_t t = ucv_type(v);

	switch (t) {
	case UC_CFUNCTION:
	case UC_CLOSURE:
		return ucv_string_new("function");

	case UC_INTEGER:
		return ucv_string_new("int");

	case UC_BOOLEAN:
		return ucv_string_new("bool");

	case UC_NULL:
		return NULL;

	default:
		return ucv_string_new(ucv_typename(v));
	}
}

/**
 * Reverse the order of the given input array or string.
 *
 * If an array is passed, returns the array in reverse order.
 * If a string is passed, returns the string with the sequence of the characters
 * reversed.
 *
 * Returns the reversed array or string.
 * Returns `null` if neither an array nor a string were passed.
 *
 * @function module:core#reverse
 *
 * @param {Array|string} arr_or_str
 * The input array or string.
 *
 * @returns {?(Array|string)}
 *
 * @example
 * reverse([1, 2, 3]);   // [ 3, 2, 1 ]
 * reverse("Abc");       // "cbA"
 */
static uc_value_t *
uc_reverse(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *obj = uc_fn_arg(0);
	uc_value_t *rv = NULL;
	size_t len, arridx;
	const char *str;
	char *dup, *p;

	if (ucv_type(obj) == UC_ARRAY) {
		if (!assert_mutable_array(vm, obj))
			return NULL;

		rv = ucv_array_new(vm);

		for (arridx = ucv_array_length(obj); arridx > 0; arridx--)
			ucv_array_push(rv, ucv_get(ucv_array_get(obj, arridx - 1)));
	}
	else if (ucv_type(obj) == UC_STRING) {
		len = ucv_string_length(obj);
		str = ucv_string_get(obj);
		p = dup = xalloc(len + 1);

		while (len > 0)
			*p++ = str[--len];

		rv = ucv_string_new(dup);

		free(dup);
	}

	return rv;
}


typedef struct {
	uc_vm_t *vm;
	bool ex;
	uc_value_t *fn;
} sort_ctx_t;

static int
default_cmp(uc_value_t *v1, uc_value_t *v2, uc_vm_t *vm)
{
	char *s1, *s2;
	bool f1, f2;
	int res;

	/* when both operands are numeric then compare numerically */
	if ((ucv_type(v1) == UC_INTEGER || ucv_type(v1) == UC_DOUBLE) &&
	    (ucv_type(v2) == UC_INTEGER || ucv_type(v2) == UC_DOUBLE)) {
		ucv_compare(0, v1, v2, &res);

		return res;
	}

	/* otherwise convert both operands to strings and compare lexically */
	s1 = uc_cast_string(vm, &v1, &f1);
	s2 = uc_cast_string(vm, &v2, &f2);

	res = strcmp(s1, s2);

	if (f1) free(s1);
	if (f2) free(s2);

	return res;
}

static int
array_sort_fn(uc_value_t *v1, uc_value_t *v2, void *ud)
{
	uc_value_t *rv, *null = ucv_int64_new(0);
	sort_ctx_t *ctx = ud;
	int res;

	if (!ctx->fn)
		return default_cmp(v1, v2, ctx->vm);

	if (ctx->ex)
		return 0;

	uc_vm_ctx_push(ctx->vm);
	uc_vm_stack_push(ctx->vm, ucv_get(ctx->fn));
	uc_vm_stack_push(ctx->vm, ucv_get(v1));
	uc_vm_stack_push(ctx->vm, ucv_get(v2));

	if (uc_vm_call(ctx->vm, true, 2)) {
		ctx->ex = true;

		return 0;
	}

	rv = uc_vm_stack_pop(ctx->vm);

	ucv_compare(0, rv, null, &res);

	ucv_put(null);
	ucv_put(rv);

	return res;
}

static int
object_sort_fn(const char *k1, uc_value_t *v1, const char *k2, uc_value_t *v2,
               void *ud)
{
	uc_value_t *rv, *null = ucv_int64_new(0);
	sort_ctx_t *ctx = ud;
	int res;

	if (!ctx->fn)
		return strcmp(k1, k2);

	if (ctx->ex)
		return 0;

	uc_vm_ctx_push(ctx->vm);
	uc_vm_stack_push(ctx->vm, ucv_get(ctx->fn));
	uc_vm_stack_push(ctx->vm, ucv_string_new(k1));
	uc_vm_stack_push(ctx->vm, ucv_string_new(k2));
	uc_vm_stack_push(ctx->vm, ucv_get(v1));
	uc_vm_stack_push(ctx->vm, ucv_get(v2));

	if (uc_vm_call(ctx->vm, true, 4)) {
		ctx->ex = true;

		return 0;
	}

	rv = uc_vm_stack_pop(ctx->vm);

	ucv_compare(0, rv, null, &res);

	ucv_put(null);
	ucv_put(rv);

	return res;
}

/**
 * Sort the given array according to the given sort function.
 * If no sort function is provided, a default ascending sort order is applied.
 *
 * The input array is sorted in-place, no copy is made.
 *
 * The custom sort function is repeatedly called until the entire array is
 * sorted. It will receive two values as arguments and should return a value
 * lower than, larger than or equal to zero depending on whether the first
 * argument is smaller, larger or equal to the second argument respectively.
 *
 * Returns the sorted input array.
 *
 * @function module:core#sort
 *
 * @param {Array} arr
 * The input array to be sorted.
 *
 * @param {Function} [fn]
 * The sort function.
 *
 * @returns {Array}
 *
 * @example
 * sort([8, 1, 5, 9]) // [1, 5, 8, 9]
 * sort(["Bean", "Orange", "Apple"], function(a, b) {
 *    return length(a) - length(b);
 * }) // ["Bean", "Apple", "Orange"]
 */
static uc_value_t *
uc_sort(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *val = uc_fn_arg(0);
	uc_value_t *fn = uc_fn_arg(1);
	sort_ctx_t ctx = {
		.vm = vm,
		.fn = fn,
		.ex = false
	};

	if (!assert_mutable(vm, val))
		return NULL;

	switch (ucv_type(val)) {
	case UC_ARRAY:
		ucv_array_sort_r(val, array_sort_fn, &ctx);
		break;

	case UC_OBJECT:
		ucv_object_sort_r(val, object_sort_fn, &ctx);
		break;

	default:
		return NULL;
	}

	return ctx.ex ? NULL : ucv_get(val);
}

/**
 * Removes the elements designated by `off` and `len` from the given array,
 * and replaces them with the additional arguments passed, if any.
 *
 * The array grows or shrinks as necessary.
 *
 * Returns the modified input array.
 *
 * @function module:core#splice
 *
 * @param {Array} arr
 * The input array to be modified.
 *
 * @param {number} off
 * The index to start removing elements.
 *
 * @param {number} [len]
 * The number of elements to remove.
 *
 * @param {...*} [elements]
 * The elements to insert.
 *
 * @returns {*}
 *
 * @example
 * let x = [ 1, 2, 3, 4 ];
 * splice(x, 1, 2, "a", "b", "c");  // [ 1, "a", "b", "c", 4 ]
 * print(x, "\n");                  // [ 1, "a", "b", "c", 4 ]
 */
static uc_value_t *
uc_splice(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *arr = uc_fn_arg(0);
	int64_t ofs = ucv_to_integer(uc_fn_arg(1));
	int64_t remlen = ucv_to_integer(uc_fn_arg(2));
	size_t arrlen, addlen, idx;

	if (!assert_mutable_array(vm, arr))
		return NULL;

	arrlen = ucv_array_length(arr);
	addlen = nargs;

	if (addlen == 1) {
		ofs = 0;
		addlen = 0;
		remlen = arrlen;
	}
	else if (addlen == 2) {
		if (ofs < 0) {
			ofs = arrlen + ofs;

			if (ofs < 0)
				ofs = 0;
		}
		else if ((uint64_t)ofs > arrlen) {
			ofs = arrlen;
		}

		addlen = 0;
		remlen = arrlen - ofs;
	}
	else {
		if (ofs < 0) {
			ofs = arrlen + ofs;

			if (ofs < 0)
				ofs = 0;
		}
		else if ((uint64_t)ofs > arrlen) {
			ofs = arrlen;
		}

		if (remlen < 0) {
			remlen = arrlen - ofs + remlen;

			if (remlen < 0)
				remlen = 0;
		}
		else if ((uint64_t)remlen > arrlen - (uint64_t)ofs) {
			remlen = arrlen - ofs;
		}

		addlen -= 3;
	}

	if (addlen < (uint64_t)remlen) {
		ucv_array_delete(arr, ofs, remlen - addlen);
	}
	else if (addlen > (uint64_t)remlen) {
		for (idx = arrlen; idx > (uint64_t)ofs; idx--)
			ucv_array_set(arr, idx + addlen - remlen - 1,
				ucv_get(ucv_array_get(arr, idx - 1)));
	}

	for (idx = 0; idx < addlen; idx++)
		ucv_array_set(arr, ofs + idx,
			ucv_get(uc_fn_arg(3 + idx)));

	return ucv_get(arr);
}

/**
 * Performs a shallow copy of a portion of the source array, as specified by
 * the start and end offsets. The original array is not modified.
 *
 * Returns a new array containing the copied elements, if any.
 * Returns `null` if the given source argument is not an array value.
 *
 * @function module:core#slice
 *
 * @param {Array} arr
 * The source array to be copied.
 *
 * @param {number} [off]
 * The index of the first element to copy.
 *
 * @param {number} [end]
 * The index of the first element to exclude from the returned array.
 *
 * @returns {Array}
 *
 * @example
 * slice([1, 2, 3])          // [1, 2, 3]
 * slice([1, 2, 3], 1)       // [2, 3]
 * slice([1, 2, 3], -1)      // [3]
 * slice([1, 2, 3], -3, -1)  // [1, 2]
 * slice([1, 2, 3], 10)      // []
 * slice([1, 2, 3], 2, 1)    // []
 * slice("invalid", 1, 2)    // null
 */
static uc_value_t *
uc_slice(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *arr = uc_fn_arg(0);
	uc_value_t *sv = uc_fn_arg(1);
	uc_value_t *ev = uc_fn_arg(2);
	uc_value_t *res = NULL;
	int64_t off, end;
	size_t len;

	if (ucv_type(arr) != UC_ARRAY)
		return NULL;

	len = ucv_array_length(arr);
	off = sv ? ucv_to_integer(sv) : 0;
	end = ev ? ucv_to_integer(ev) : (int64_t)len;

	if (off < 0) {
		off = len + off;

		if (off < 0)
			off = 0;
	}
	else if ((uint64_t)off > len) {
		off = len;
	}

	if (end < 0) {
		end = len + end;

		if (end < 0)
			end = 0;
	}
	else if ((uint64_t)end > len) {
		end = len;
	}

	res = ucv_array_new(vm);

	while (off < end)
		ucv_array_push(res, ucv_get(ucv_array_get(arr, off++)));

	return res;
}

/**
 * Split the given string using the separator passed as the second argument
 * and return an array containing the resulting pieces.
 *
 * If a limit argument is supplied, the resulting array contains no more than
 * the given amount of entries, that means the string is split at most
 * `limit - 1` times total.
 *
 * The separator may either be a plain string or a regular expression.
 *
 * Returns a new array containing the resulting pieces.
 *
 * @function module:core#split
 *
 * @param {string} str
 * The input string to be split.
 *
 * @param {string|RegExp} sep
 * The separator.
 *
 * @param {number} [limit]
 * The limit on the number of splits.
 *
 * @returns {Array}
 *
 * @example
 * split("foo,bar,baz", ",")     // ["foo", "bar", "baz"]
 * split("foobar", "")           // ["f", "o", "o", "b", "a", "r"]
 * split("foo,bar,baz", /[ao]/)  // ["f", "", ",b", "r,b", "z"]
 * split("foo=bar=baz", "=", 2)  // ["foo", "bar=baz"]
 */
static uc_value_t *
uc_split(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *str = uc_fn_arg(0);
	uc_value_t *sep = uc_fn_arg(1);
	uc_value_t *lim = uc_fn_arg(2);
	uc_value_t *arr = NULL;
	const char *p, *sepstr, *splitstr;
	size_t seplen, splitlen, limit;
	int eflags = 0, res;
	regmatch_t pmatch;
	uc_regexp_t *re;

	if (!sep || ucv_type(str) != UC_STRING)
		return NULL;

	arr = ucv_array_new(vm);
	splitlen = ucv_string_length(str);
	p = splitstr = ucv_string_get(str);
	limit = lim ? ucv_uint64_get(lim) : SIZE_MAX;

	if (limit == 0)
		goto out;

	if (ucv_type(sep) == UC_REGEXP) {
		re = (uc_regexp_t *)sep;

		while (limit > 1) {
			res = regexec(&re->regexp, splitstr, 1, &pmatch, eflags);

			if (res == REG_NOMATCH)
				break;

			if (pmatch.rm_so != pmatch.rm_eo) {
				ucv_array_push(arr, ucv_string_new_length(splitstr, pmatch.rm_so));
				splitstr += pmatch.rm_eo;
			}
			else if (*splitstr) {
				ucv_array_push(arr, ucv_string_new_length(splitstr, 1));
				splitstr++;
			}
			else {
				goto out;
			}

			eflags |= REG_NOTBOL;
			limit--;
		}

		ucv_array_push(arr, ucv_string_new(splitstr));
	}
	else if (ucv_type(sep) == UC_STRING) {
		sepstr = ucv_string_get(sep);
		seplen = ucv_string_length(sep);

		if (splitlen == 0) {
			ucv_array_push(arr, ucv_string_new_length("", 0));
		}
		else if (seplen == 0) {
			while (limit > 1 && splitlen > 0) {
				ucv_array_push(arr, ucv_string_new_length(p, 1));

				limit--;
				splitlen--;
				p++;
			}

			if (splitlen > 0)
				ucv_array_push(arr, ucv_string_new_length(p, splitlen));
		}
		else {
			while (limit > 1 && splitlen >= seplen) {
				if (!memcmp(p, sepstr, seplen)) {
					ucv_array_push(arr, ucv_string_new_length(splitstr, p - splitstr));

					p = splitstr = p + seplen;
					splitlen -= seplen;
					limit--;
					continue;
				}

				splitlen--;
				p++;
			}

			ucv_array_push(arr, ucv_string_new_length(splitstr, p - splitstr + splitlen));
		}
	}
	else {
		ucv_put(arr);

		return NULL;
	}

out:
	return arr;
}

/**
 * Extracts a substring out of `str` and returns it. First character is at
 * offset zero.
 *
 *  - If `off` is negative, starts that far back from the end of the string.
 *  - If `len` is omitted, returns everything through the end of the string.
 *  - If `len` is negative, leaves that many characters off the string end.
 *
 * Returns the extracted substring.
 *
 * @function module:core#substr
 *
 * @param {string} str
 * The input string.
 *
 * @param {number} off
 * The starting offset.
 *
 * @param {number} [len]
 * The length of the substring.
 *
 * @returns {string}
 *
 * @example
 * s = "The black cat climbed the green tree";
 * substr(s, 4, 5);      // black
 * substr(s, 4, -11);    // black cat climbed the
 * substr(s, 14);        // climbed the green tree
 * substr(s, -4);        // tree
 * substr(s, -4, 2);     // tr
 */
static uc_value_t *
uc_substr(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *str = uc_fn_arg(0);
	int64_t ofs = ucv_to_integer(uc_fn_arg(1));
	int64_t sublen = ucv_to_integer(uc_fn_arg(2));
	const char *p;
	size_t len;

	if (ucv_type(str) != UC_STRING)
		return NULL;

	p = ucv_string_get(str);
	len = ucv_string_length(str);

	switch (nargs) {
	case 1:
		ofs = 0;
		sublen = len;

		break;

	case 2:
		if (ofs < 0) {
			ofs = len + ofs;

			if (ofs < 0)
				ofs = 0;
		}
		else if ((uint64_t)ofs > len) {
			ofs = len;
		}

		sublen = len - ofs;

		break;

	default:
		if (ofs < 0) {
			ofs = len + ofs;

			if (ofs < 0)
				ofs = 0;
		}
		else if ((uint64_t)ofs > len) {
			ofs = len;
		}

		if (sublen < 0) {
			sublen = len - ofs + sublen;

			if (sublen < 0)
				sublen = 0;
		}
		else if ((uint64_t)sublen > len - (uint64_t)ofs) {
			sublen = len - ofs;
		}

		break;
	}

	return ucv_string_new_length(p + ofs, sublen);
}

/**
 * Returns the current UNIX epoch.
 *
 * @function module:core#time
 *
 * @returns {number}
 *
 * @example
 * time();     // 1598043054
 */
static uc_value_t *
uc_time(uc_vm_t *vm, size_t nargs)
{
	time_t t = time(NULL);

	return ucv_int64_new((int64_t)t);
}

/**
 * Converts the given string to uppercase and returns the resulting string.
 *
 * Returns null if the given argument could not be converted to a string.
 *
 * @function module:core#uc
 *
 * @param {*} str
 * The string to be converted to uppercase.
 *
 * @returns {?string}
 *
 * @example
 * uc("hello");   // "HELLO"
 * uc(123);       // null
 */

static uc_value_t *
uc_uc(uc_vm_t *vm, size_t nargs)
{
	char *str = ucv_to_string(vm, uc_fn_arg(0));
	uc_value_t *rv = NULL;
	char *p;

	if (!str)
		return NULL;

	for (p = str; *p; p++)
		if (*p >= 'a' && *p <= 'z')
			*p &= ~32;

	rv = ucv_string_new(str);

	free(str);

	return rv;
}

/**
 * Converts each given numeric value to an UTF-8 multibyte sequence and returns
 * the resulting string.
 *
 * Invalid numeric values or values outside the range `0`..`0x10FFFF` are
 * represented by the unicode replacement character `0xFFFD`.
 *
 * Returns a new UTF-8 encoded string consisting of unicode characters
 * corresponding to the given numeric codepoints.
 *
 * @function module:core#uchr
 *
 * @param {...number}
 * Numeric values to convert.
 *
 * @returns {string}
 *
 * @example
 * uchr(0x2600, 0x26C6, 0x2601);  // "☀⛆☁"
 * uchr(-1, 0x20ffff, "foo");     // "���"
 */
static uc_value_t *
uc_uchr(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *rv = NULL;
	size_t idx, ulen;
	char *p, *str;
	int64_t n;
	int rem;

	for (idx = 0, ulen = 0; idx < nargs; idx++) {
		n = ucv_to_integer(uc_fn_arg(idx));

		if (errno == EINVAL || errno == ERANGE || n < 0 || n > 0x10FFFF)
			ulen += 3;
		else if (n <= 0x7F)
			ulen++;
		else if (n <= 0x7FF)
			ulen += 2;
		else if (n <= 0xFFFF)
			ulen += 3;
		else
			ulen += 4;
	}

	str = xalloc(ulen);

	for (idx = 0, p = str, rem = ulen; idx < nargs; idx++) {
		n = ucv_to_integer(uc_fn_arg(idx));

		if (errno == EINVAL || errno == ERANGE || n < 0 || n > 0x10FFFF)
			n = 0xFFFD;

		if (!utf8enc(&p, &rem, n))
			break;
	}

	rv = ucv_string_new_length(str, ulen);

	free(str);

	return rv;
}

/**
 * Returns an array containing all values of the given object.
 *
 * Returns null if no object was passed.
 *
 * @function module:core#values
 *
 * @param {*} obj
 * The object from which to extract values.
 *
 * @returns {?Array}
 *
 * @example
 * values({ foo: true, bar: false });   // [true, false]
 */
static uc_value_t *
uc_values(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *obj = uc_fn_arg(0);
	uc_value_t *arr;

	if (ucv_type(obj) != UC_OBJECT)
		return NULL;

	arr = ucv_array_new(vm);

	ucv_object_foreach(obj, key, val) {
		(void)key;
		ucv_array_push(arr, ucv_get(val));
	}

	return arr;
}

static uc_value_t *
uc_trim_common(uc_vm_t *vm, size_t nargs, bool start, bool end)
{
	uc_value_t *str = uc_fn_arg(0);
	uc_value_t *chr = uc_fn_arg(1);
	const char *p, *c;
	size_t len;

	if (ucv_type(str) != UC_STRING ||
		(chr != NULL && ucv_type(chr) != UC_STRING))
		return NULL;

	c = ucv_string_get(chr);
	c = c ? c : " \t\r\n";

	p = ucv_string_get(str);
	len = ucv_string_length(str);

	if (start) {
		while (*p) {
			if (!strchr(c, *p))
				break;

			p++;
			len--;
		}
	}

	if (end) {
		while (len > 0) {
			if (!strchr(c, p[len - 1]))
				break;

			len--;
		}
	}

	return ucv_string_new_length(p, len);
}

/**
 * Trim any of the specified characters in `c` from the start and end of `str`.
 * If the second argument is omitted, trims the characters, ` ` (space), `\t`,
 * `\r`, and `\n`.
 *
 * Returns the trimmed string.
 *
 * @function module:core#trim
 *
 * @param {string} str
 * The string to be trimmed.
 *
 * @param {string} [c]
 * The characters to be trimmed from the start and end of the string.
 *
 * @returns {string}
 */
static uc_value_t *
uc_trim(uc_vm_t *vm, size_t nargs)
{
	return uc_trim_common(vm, nargs, true, true);
}

/**
 * Trim any of the specified characters from the start of the string.
 * If the second argument is omitted, trims the characters ` ` (space), '\t',
 * '\r', and '\n'.
 *
 * Returns the left trimmed string.
 *
 * @function module:core#ltrim
 *
 * @param {string} s
 * The input string.
 *
 * @param {string} [c]
 * The characters to trim.
 *
 * @returns {string}
 *
 * @example
 * ltrim("  foo  \n")     // "foo  \n"
 * ltrim("--bar--", "-")  // "bar--"
 */
static uc_value_t *
uc_ltrim(uc_vm_t *vm, size_t nargs)
{
	return uc_trim_common(vm, nargs, true, false);
}

/**
 * Trim any of the specified characters from the end of the string.
 * If the second argument is omitted, trims the characters ` ` (space), '\t',
 * '\r', and '\n'.
 *
* Returns the right trimmed string.
 *
 * @function module:core#rtrim
 *
 * @param {string} str
 * The input string.
 *
 * @param {string} [c]
 * The characters to trim.
 *
 * @returns {string}
 *
 * @example
 * rtrim("  foo  \n")     // "  foo"
 * rtrim("--bar--", "-")  // "--bar"
 */
static uc_value_t *
uc_rtrim(uc_vm_t *vm, size_t nargs)
{
	return uc_trim_common(vm, nargs, false, true);
}

enum {
	FMT_F_ALT   = (1 << 0),
	FMT_F_ZERO  = (1 << 1),
	FMT_F_LEFT  = (1 << 2),
	FMT_F_SPACE = (1 << 3),
	FMT_F_SIGN  = (1 << 4),
	FMT_F_WIDTH = (1 << 5),
	FMT_F_PREC  = (1 << 6),
};

enum {
	FMT_C_NONE = (1 << 0),
	FMT_C_INT  = (1 << 1),
	FMT_C_UINT = (1 << 2),
	FMT_C_DBL  = (1 << 3),
	FMT_C_CHR  = (1 << 4),
	FMT_C_STR  = (1 << 5),
	FMT_C_JSON = (1 << 6),
};

static void
uc_printf_common(uc_vm_t *vm, size_t nargs, uc_stringbuf_t *buf)
{
	char *s, sfmt[sizeof("%#0- +0123456789.0123456789%")];
	uint32_t conv, flags, width, precision;
	uc_value_t *fmt = uc_fn_arg(0), *arg;
	const char *fstr, *last, *p, *cfmt;
	size_t argidx = 1, argpos, sfmtlen;
	uint64_t u;
	int64_t n;
	double d;

	if (ucv_type(fmt) == UC_STRING)
		fstr = ucv_string_get(fmt);
	else
		fstr = "";

	for (last = p = fstr; *p; p++) {
		if (*p == '%') {
			ucv_stringbuf_addstr(buf, last, p - last);

			last = p++;

			flags = 0;
			width = 0;
			precision = 0;

			argpos = argidx;

			if (*p >= '1' && *p <= '9') {
				while (isdigit(*p))
					width = width * 10 + (*p++ - '0');

				/* if a dollar sign follows, this is an argument index */
				if (*p == '$') {
					argpos = width;
					width = 0;
					p++;
				}

				/* otherwise skip to parsing precision, flags can't possibly follow */
				else {
					flags |= FMT_F_WIDTH;
					goto parse_precision;
				}
			}

			while (*p != '\0' && strchr("#0- +", *p)) {
				switch (*p++) {
				case '#': flags |= FMT_F_ALT;   break;
				case '0': flags |= FMT_F_ZERO;  break;
				case '-': flags |= FMT_F_LEFT;  break;
				case ' ': flags |= FMT_F_SPACE; break;
				case '+': flags |= FMT_F_SIGN;  break;
				}
			}

			if (*p >= '1' && *p <= '9') {
				while (isdigit(*p))
					width = width * 10 + (*p++ - '0');

				flags |= FMT_F_WIDTH;
			}

parse_precision:
			if (*p == '.') {
				p++;

				if (*p == '-') {
					p++;

					while (isdigit(*p))
						p++;
				}
				else {
					while (isdigit(*p))
						precision = precision * 10 + (*p++ - '0');
				}

				flags |= FMT_F_PREC;
			}

			switch (*p) {
			case 'd':
			case 'i':
				conv = FMT_C_INT;
				flags &= ~FMT_F_PREC;
				cfmt = PRId64;
				break;

			case 'o':
				conv = FMT_C_UINT;
				flags &= ~FMT_F_PREC;
				cfmt = PRIo64;
				break;

			case 'u':
				conv = FMT_C_UINT;
				flags &= ~FMT_F_PREC;
				cfmt = PRIu64;
				break;

			case 'x':
				conv = FMT_C_UINT;
				flags &= ~FMT_F_PREC;
				cfmt = PRIx64;
				break;

			case 'X':
				conv = FMT_C_UINT;
				flags &= ~FMT_F_PREC;
				cfmt = PRIX64;
				break;

			case 'e':
				conv = FMT_C_DBL;
				cfmt = "e";
				break;

			case 'E':
				conv = FMT_C_DBL;
				cfmt = "E";
				break;

			case 'f':
				conv = FMT_C_DBL;
				cfmt = "f";
				break;

			case 'F':
				conv = FMT_C_DBL;
				cfmt = "F";
				break;

			case 'g':
				conv = FMT_C_DBL;
				cfmt = "g";
				break;

			case 'G':
				conv = FMT_C_DBL;
				cfmt = "G";
				break;

			case 'c':
				conv = FMT_C_CHR;
				flags &= ~FMT_F_PREC;
				cfmt = "c";
				break;

			case 's':
				conv = FMT_C_STR;
				flags &= ~FMT_F_ZERO;
				cfmt = "s";
				break;

			case 'J':
				conv = FMT_C_JSON;

				if (flags & FMT_F_PREC) {
					flags &= ~FMT_F_PREC;
					precision++;
				}

				cfmt = "s";
				break;

			case '%':
				conv = FMT_C_NONE;
				flags = 0;
				cfmt = "%";
				break;

			case '\0':
				p--;
				/* fall through */

			default:
				continue;
			}

			sfmtlen = 0;
			sfmt[sfmtlen++] = '%';

			if (flags & FMT_F_ALT)   sfmt[sfmtlen++] = '#';
			if (flags & FMT_F_ZERO)  sfmt[sfmtlen++] = '0';
			if (flags & FMT_F_LEFT)  sfmt[sfmtlen++] = '-';
			if (flags & FMT_F_SPACE) sfmt[sfmtlen++] = ' ';
			if (flags & FMT_F_SIGN)  sfmt[sfmtlen++] = '+';

			if (flags & FMT_F_WIDTH)
				sfmtlen += snprintf(&sfmt[sfmtlen], sizeof(sfmt) - sfmtlen, "%" PRIu32, width);

			if (flags & FMT_F_PREC)
				sfmtlen += snprintf(&sfmt[sfmtlen], sizeof(sfmt) - sfmtlen, ".%" PRIu32, precision);

			snprintf(&sfmt[sfmtlen], sizeof(sfmt) - sfmtlen, "%s", cfmt);

			switch (conv) {
			case FMT_C_NONE:
				ucv_stringbuf_addstr(buf, cfmt, strlen(cfmt));
				break;

			case FMT_C_INT:
				argidx++;
				arg = uc_fn_arg(argpos);
				n = ucv_to_integer(arg);

				if (errno == ERANGE)
					n = (int64_t)ucv_to_unsigned(arg);

				ucv_stringbuf_printf(buf, sfmt, n);
				break;

			case FMT_C_UINT:
				argidx++;
				arg = uc_fn_arg(argpos);
				u = ucv_to_unsigned(arg);

				if (errno == ERANGE)
					u = (uint64_t)ucv_to_integer(arg);

				ucv_stringbuf_printf(buf, sfmt, u);
				break;

			case FMT_C_DBL:
				argidx++;
				d = ucv_to_double(uc_fn_arg(argpos));
				ucv_stringbuf_printf(buf, sfmt, d);
				break;

			case FMT_C_CHR:
				argidx++;
				n = ucv_to_integer(uc_fn_arg(argpos));
				ucv_stringbuf_printf(buf, sfmt, (int)n);
				break;

			case FMT_C_STR:
				argidx++;
				arg = uc_fn_arg(argpos);

				switch (ucv_type(arg)) {
				case UC_STRING:
					ucv_stringbuf_printf(buf, sfmt, ucv_string_get(arg));
					break;

				case UC_NULL:
					ucv_stringbuf_append(buf, "(null)");
					break;

				default:
					s = ucv_to_string(vm, arg);
					ucv_stringbuf_printf(buf, sfmt, s ? s : "(null)");
					free(s);
				}

				break;

			case FMT_C_JSON:
				argidx++;
				s = ucv_to_jsonstring_formatted(vm,
					uc_fn_arg(argpos),
					precision > 0 ? (precision > 1 ? ' ' : '\t') : '\0',
					precision > 0 ? (precision > 1 ? precision - 1 : 1) : 0);

				ucv_stringbuf_printf(buf, sfmt, s ? s : "null");
				free(s);
				break;
			}

			last = p + 1;
		}
	}

	ucv_stringbuf_addstr(buf, last, p - last);
}

/**
 * Formats the given arguments according to the given format string.
 *
 * See `printf()` for details.
 *
 * Returns the formatted string.
 *
 * @function module:core#sprintf
 *
 * @param {string} fmt
 * The format string.
 *
 * @param {...*}
 * Arguments to be formatted.
 *
 * @returns {string}
 *
 * @example
 * sprintf("Hello %s", "world");    // "Hello world"
 * sprintf("%08x", 123);            // "0000007b"
 * sprintf("%c%c%c", 65, 98, 99);   // "Abc"
 * sprintf("%g", 10 / 3.0);         // "3.33333"
 * sprintf("%2$d %1$d", 12, 34);    // "34 12"
 * sprintf("%J", [1,2,3]);          // "[1,2,3]"
 */
static uc_value_t *
uc_sprintf(uc_vm_t *vm, size_t nargs)
{
	uc_stringbuf_t *buf = ucv_stringbuf_new();

	uc_printf_common(vm, nargs, buf);

	return ucv_stringbuf_finish(buf);
}

/**
 * Formats the given arguments according to the given format string and outputs
 * the result to stdout.
 *
 * Ucode supports a restricted subset of the formats allowed by the underlying
 * libc's `printf()` implementation, namely it allows the `d`, `i`, `o`, `u`,
 * `x`, `X`, `e`, `E`, `f`, `F`, `g`, `G`, `c` and `s` conversions.
 *
 * Additionally, an ucode specific `J` format is implemented, which causes the
 * corresponding value to be formatted as JSON string. By prefixing the `J`
 * format letter with a precision specifier, the resulting JSON output will be
 * pretty printed. A precision of `0` will use tabs for indentation, any other
 * positive precision will use that many spaces for indentation while a negative
 * or omitted precision specifier will turn off pretty printing.
 *
 * Other format specifiers such as `n` or `z` are not accepted and returned
 * verbatim. Format specifiers including `*` directives are rejected as well.
 *
 * Returns the number of bytes written to the standard output.
 *
 * @function module:core#printf
 *
 * @param {string} fmt
 * The format string.
 *
 * @param {...*}
 * Arguments to be formatted.
 *
 * @returns {number}
 *
 * @example
 * {%
 *   printf("Hello %s\n", "world");  // Hello world
 *   printf("%08x\n", 123);          // 0000007b
 *   printf("%c%c%c\n", 65, 98, 99); // Abc
 *   printf("%g\n", 10 / 3.0);       // 3.33333
 *   printf("%2$d %1$d\n", 12, 34);  // 34 12
 *   printf("%J", [1,2,3]);          // [ 1, 2, 3 ]
 *
 *   printf("%.J", [1,2,3]);
 *   // [
 *   //         1,
 *   //         2,
 *   //         3
 *   // ]
 *
 *   printf("%.2J", [1,2,3]);
 *   // [
 *   //   1,
 *   //   2,
 *   //   3
 *   // ]
 * %}
 */
static uc_value_t *
uc_printf(uc_vm_t *vm, size_t nargs)
{
	uc_stringbuf_t *buf = xprintbuf_new();
	size_t len;

	uc_printf_common(vm, nargs, buf);

	len = fwrite(buf->buf, 1, printbuf_length(buf), vm->output);

	printbuf_free(buf);

	return ucv_int64_new(len);
}

static bool
uc_require_so(uc_vm_t *vm, const char *path, uc_value_t **res)
{
	void (*init)(uc_vm_t *, uc_value_t *);
	uc_value_t *scope;
	struct stat st;
	void *dlh;

	if (stat(path, &st))
		return false;

	dlerror();
	dlh = dlopen(path, RTLD_LAZY|RTLD_LOCAL);

	if (!dlh) {
		uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
		                      "Unable to dlopen file '%s': %s", path, dlerror());

		return true;
	}

	*(void **)(&init) = dlsym(dlh, "uc_module_entry");

	if (!init) {
		uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
		                      "Module '%s' provides no 'uc_module_entry' function", path);

		return true;
	}

	scope = ucv_object_new(vm);

	init(vm, scope);

	*res = scope;

	return true;
}

static uc_value_t *
uc_loadfile(uc_vm_t *vm, size_t nargs);

static uc_value_t *
uc_callfunc(uc_vm_t *vm, size_t nargs);

static bool
uc_require_ucode(uc_vm_t *vm, const char *path, uc_value_t *scope, uc_value_t **res, bool raw_mode)
{
	uc_parse_config_t config = *vm->config, *prev_config = vm->config;
	uc_value_t *closure;
	struct stat st;

	if (stat(path, &st))
		return false;

	config.raw_mode = raw_mode;
	vm->config = &config;

	uc_vm_stack_push(vm, ucv_string_new(path));

	closure = uc_loadfile(vm, 1);

	ucv_put(uc_vm_stack_pop(vm));

	if (closure) {
		uc_vm_stack_push(vm, closure);
		uc_vm_stack_push(vm, NULL);
		uc_vm_stack_push(vm, scope);

		*res = uc_callfunc(vm, 3);

		uc_vm_stack_pop(vm);
		uc_vm_stack_pop(vm);
		uc_vm_stack_pop(vm);
	}

	vm->config = prev_config;

	return true;
}

static bool
uc_require_path(uc_vm_t *vm, const char *path_template, const char *name, uc_value_t **res, bool so_only)
{
	uc_stringbuf_t *buf = xprintbuf_new();
	const char *p, *q, *last;
	uc_value_t *modtable;
	bool rv;

	modtable = ucv_property_get(uc_vm_scope_get(vm), "modules");
	*res = ucv_get(ucv_object_get(modtable, name, &rv));

	if (rv)
		goto out;

	p = strchr(path_template, '*');

	if (!p)
		goto out;

	ucv_stringbuf_addstr(buf, path_template, p - path_template);

	for (q = last = name;; q++) {
		if (*q == '.' || *q == '\0') {
			ucv_stringbuf_addstr(buf, last, q - last);

			if (*q)
				ucv_stringbuf_append(buf, "/");
			else
				ucv_stringbuf_addstr(buf, p + 1, strlen(p + 1));

			if (*q == '\0')
				break;

			last = q + 1;
		}
		else if (!isalnum(*q) && *q != '_') {
			goto out;
		}
	}

	if (!strcmp(p + 1, ".so"))
		rv = uc_require_so(vm, buf->buf, res);
	else if (!strcmp(p + 1, ".uc") && !so_only)
		rv = uc_require_ucode(vm, buf->buf, NULL, res, true);

	if (rv)
		ucv_object_add(modtable, name, ucv_get(*res));

out:
	printbuf_free(buf);

	return rv;
}

uc_value_t *
uc_require_library(uc_vm_t *vm, uc_value_t *nameval, bool so_only)
{
	uc_value_t *search, *se, *res;
	size_t arridx, arrlen;
	const char *name;

	if (ucv_type(nameval) != UC_STRING)
		return NULL;

	name = ucv_string_get(nameval);
	search = ucv_property_get(uc_vm_scope_get(vm), "REQUIRE_SEARCH_PATH");

	if (ucv_type(search) != UC_ARRAY) {
		uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
		                      "Global require search path not set");

		return NULL;
	}

	for (arridx = 0, arrlen = ucv_array_length(search); arridx < arrlen; arridx++) {
		se = ucv_array_get(search, arridx);

		if (ucv_type(se) != UC_STRING)
			continue;

		if (uc_require_path(vm, ucv_string_get(se), name, &res, so_only))
			return res;
	}

	uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
	                      "No module named '%s' could be found", name);

	return NULL;
}

/**
 * Load and evaluate ucode scripts or shared library extensions.
 *
 * The `require()` function expands each member of the global
 * `REQUIRE_SEARCH_PATH` array to a filesystem path by replacing the `*`
 * placeholder with a slash-separated version of the given dotted module name
 * and subsequently tries to load a file at the resulting location.
 *
 * If a file is found at one of the search path locations, it is compiled and
 * evaluated or loaded via the C runtime's `dlopen()` function, depending on
 * whether the found file is a ucode script or a compiled dynamic library.
 *
 * The resulting program function of the compiled/loaded module is then
 * subsequently executed with the current global environment, without a `this`
 * context and without arguments.
 *
 * Finally, the return value of the invoked program function is returned back
 * by `require()` to the caller.
 *
 * By default, modules are cached in the global `modules` dictionary and
 * subsequent attempts to require the same module will return the cached module
 * dictionary entry without re-evaluating the module.
 *
 * To force reloading a module, the corresponding entry from the global
 * `modules` dictionary can be deleted.
 *
 * To preload a module or to provide a "virtual" module without a corresponding
 * filesystem resource, an entry can be manually added to the global `modules`
 * dictionary.
 *
 * Summarized, the `require()` function can be roughly described by the
 * following code:
 *
 * ```
 * function require(name) {
 *     if (exists(modules, name))
 *         return modules[name];
 *
 *     for (const item in REQUIRE_SEARCH_PATH) {
 *         const modpath = replace(item, '*', replace(name, '.', '/'));
 *         const entryfunc = loadfile(modpath, { raw_mode: true });
 *
 *         if (entryfunc) {
 *             const modval = entryfunc();
 *             modules[name] = modval;
 *
 *             return modval;
 *         }
 *     }
 *
 *     die(`Module ${name} not found`);
 * }
 * ```
 *
 * Due to the fact that `require()` is a runtime operation, module source code
 * is only lazily evaluated/loaded upon invoking the first require invocation,
 * which might lead to situations where errors in module sources are only
 * reported much later throughout the program execution. Unless runtime loading
 * of modules is absolutely required, e.g. to conditionally load extensions, the
 * compile time
 * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#named_import|`import` syntax}
 * should be preferred.
 *
 * Returns the module value (typically an object) on success.
 *
 * Throws an exception if the module function threw an exception.
 *
 * Throws an exception if no matching module could be found, if the module
 * contains syntax errors or upon other I/O related problems.
 *
 * @function module:core#require
 *
 * @param {string} name
 * The name of the module to require in dotted notation.
 *
 * @returns {*}
 *
 * @example
 * // Require the `example/acme.uc` or `example/acme.so` module
 * const acme = require('example.acme');
 *
 * // Requiring the same name again will yield the cached instance
 * const acme2 = require('example.acme');
 * assert(acme === acme2);
 *
 * // Deleting the module dictionary entry will force a reload
 * delete modules['example.acme'];
 * const acme3 = require('example.acme');
 * assert(acme !== acme3);
 *
 * // Preloading a "virtual" module
 * modules['example.test'] = {
 *   hello: function() { print("This is the example module\n"); }
 * };
 *
 * const test = require('example.test');
 * test.hello();  // will print "This is the example module"
 */
static uc_value_t *
uc_require(uc_vm_t *vm, size_t nargs)
{
	return uc_require_library(vm, uc_fn_arg(0), false);
}

/**
 * Convert the given IP address string to an array of byte values.
 *
 * IPv4 addresses result in arrays of 4 integers while IPv6 ones in arrays
 * containing 16 integers. The resulting array can be turned back into IP
 * address strings using the inverse `arrtoip()` function.
 *
 * Returns an array containing the address byte values.
 * Returns `null` if the given argument is not a string or an invalid IP.
 *
 * @function module:core#iptoarr
 *
 * @param {string} address
 * The IP address string to convert.
 *
 * @returns {?number[]}
 *
 * @example
 * iptoarr("192.168.1.1")              // [ 192, 168, 1, 1 ]
 * iptoarr("fe80::fc54:ff:fe82:abbd")  // [ 254, 128, 0, 0, 0, 0, 0, 0, 252, 84,
 *                                     //   0, 255, 254, 130, 171, 189 ])
 * iptoarr("foo")                      // null (invalid address)
 * iptoarr(123)                        // null (not a string)
 */
static uc_value_t *
uc_iptoarr(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *ip = uc_fn_arg(0);
	uc_value_t *res;
	union {
		uint8_t u8[4];
		struct in_addr in;
		struct in6_addr in6;
	} a;
	int i;

	if (ucv_type(ip) != UC_STRING)
		return NULL;

	if (inet_pton(AF_INET6, ucv_string_get(ip), &a)) {
		res = ucv_array_new(vm);

		for (i = 0; i < 16; i++)
			ucv_array_push(res, ucv_int64_new(a.in6.s6_addr[i]));

		return res;
	}
	else if (inet_pton(AF_INET, ucv_string_get(ip), &a)) {
		res = ucv_array_new(vm);

		ucv_array_push(res, ucv_int64_new(a.u8[0]));
		ucv_array_push(res, ucv_int64_new(a.u8[1]));
		ucv_array_push(res, ucv_int64_new(a.u8[2]));
		ucv_array_push(res, ucv_int64_new(a.u8[3]));

		return res;
	}

	return NULL;
}

static int
check_byte(uc_value_t *v)
{
	int n;

	if (ucv_type(v) != UC_INTEGER)
		return -1;

	n = ucv_int64_get(v);

	if (n < 0 || n > 255)
		return -1;

	return n;
}

/**
 * Convert the given input array of byte values to an IP address string.
 *
 * Input arrays of length 4 are converted to IPv4 addresses, arrays of length 16
 * to IPv6 ones. All other lengths are rejected. If any array element is not an
 * integer or exceeds the range 0..255 (inclusive), the array is rejected.
 *
 * Returns a string containing the formatted IP address.
 * Returns `null` if the input array was invalid.
 *
 * @function module:core#arrtoip
 *
 * @param {number[]} arr
 * The byte array to convert into an IP address string.
 *
 * @returns {?string}
 *
 * @example
 * arrtoip([ 192, 168, 1, 1 ])   // "192.168.1.1"
 * arrtoip([ 254, 128, 0, 0, 0, 0, 0, 0, 252, 84, 0, 255, 254, 130, 171, 189 ])
 *                               // "fe80::fc54:ff:fe82:abbd"
 * arrtoip([ 1, 2, 3])           // null (invalid length)
 * arrtoip([ 1, "2", -5, 300 ])  // null (invalid values)
 * arrtoip("123")                // null (not an array)
 */
static uc_value_t *
uc_arrtoip(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *arr = uc_fn_arg(0);
	union {
		uint8_t u8[4];
		struct in6_addr in6;
	} a;
	char buf[INET6_ADDRSTRLEN];
	int i, n;

	if (ucv_type(arr) != UC_ARRAY)
		return NULL;

	switch (ucv_array_length(arr)) {
	case 4:
		for (i = 0; i < 4; i++) {
			n = check_byte(ucv_array_get(arr, i));

			if (n < 0)
				return NULL;

			a.u8[i] = n;
		}

		inet_ntop(AF_INET, &a, buf, sizeof(buf));

		return ucv_string_new(buf);

	case 16:
		for (i = 0; i < 16; i++) {
			n = check_byte(ucv_array_get(arr, i));

			if (n < 0)
				return NULL;

			a.in6.s6_addr[i] = n;
		}

		inet_ntop(AF_INET6, &a, buf, sizeof(buf));

		return ucv_string_new(buf);

	default:
		return NULL;
	}
}

/**
 * Match the given string against the regular expression pattern specified as
 * the second argument.
 *
 * If the passed regular expression uses the `g` flag, the return value will be
 * an array of arrays describing all found occurrences within the string.
 *
 * Without the `g` modifier, an array describing the first match is returned.
 *
 * Returns `null` if the pattern was not found within the given string.
 *
 * @function module:core#match
 *
 * @param {string} str
 * The string to be matched against the pattern.
 *
 * @param {RegExp} pattern
 * The regular expression pattern.
 *
 * @returns {?Array}
 *
 * @example
 * match("foobarbaz", /b.(.)/)   // ["bar", "r"]
 * match("foobarbaz", /b.(.)/g)  // [["bar", "r"], ["baz", "z"]]
 */
static uc_value_t *
uc_match(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *subject = uc_fn_arg(0);
	uc_value_t *pattern = uc_fn_arg(1);
	uc_value_t *rv = NULL, *m;
	regmatch_t *pmatch = NULL;
	int eflags = 0, res;
	uc_regexp_t *re;
	bool freeable;
	char *p;
	size_t i;

	if (ucv_type(pattern) != UC_REGEXP || !subject)
		return NULL;

	re = (uc_regexp_t *)pattern;

	pmatch = calloc(1 + re->regexp.re_nsub, sizeof(regmatch_t));

	if (!pmatch)
		return NULL;

	p = uc_cast_string(vm, &subject, &freeable);

	while (true) {
		res = regexec(&re->regexp, p, 1 + re->regexp.re_nsub, pmatch, eflags);

		if (res == REG_NOMATCH)
			break;

		m = ucv_array_new(vm);

		for (i = 0; i < 1 + re->regexp.re_nsub; i++) {
			if (pmatch[i].rm_so != -1)
				ucv_array_push(m,
					ucv_string_new_length(p + pmatch[i].rm_so,
					                      pmatch[i].rm_eo - pmatch[i].rm_so));
			else
				ucv_array_push(m, NULL);
		}

		if (re->global) {
			if (!rv)
				rv = ucv_array_new(vm);

			ucv_array_push(rv, m);

			if (pmatch[0].rm_so != pmatch[0].rm_eo)
				p += pmatch[0].rm_eo;
			else if (*p)
				p++;
			else
				break;

			eflags |= REG_NOTBOL;
		}
		else {
			rv = m;
			break;
		}
	}

	free(pmatch);

	if (freeable)
		free(p);

	return rv;
}

static void
uc_replace_cb(uc_vm_t *vm, uc_value_t *func,
              const char *subject, regmatch_t *pmatch, size_t plen,
              uc_stringbuf_t *resbuf)
{
	uc_value_t *rv;
	size_t i;

	uc_vm_ctx_push(vm);
	uc_vm_stack_push(vm, ucv_get(func));

	for (i = 0; i < plen; i++) {
		if (pmatch[i].rm_so != -1)
			uc_vm_stack_push(vm,
				ucv_string_new_length(subject + pmatch[i].rm_so,
				                      pmatch[i].rm_eo - pmatch[i].rm_so));
		else
			uc_vm_stack_push(vm, NULL);
	}

	if (uc_vm_call(vm, true, i) == EXCEPTION_NONE) {
		rv = uc_vm_stack_pop(vm);

		ucv_to_stringbuf(vm, resbuf, rv, false);

		ucv_put(rv);
	}
}

static void
uc_replace_str(uc_vm_t *vm, uc_value_t *str,
               const char *subject, regmatch_t *pmatch, size_t plen,
               uc_stringbuf_t *resbuf)
{
	bool esc = false;
	char *p, *r;
	uint8_t i;

	for (p = r = ucv_to_string(vm, str); *p; p++) {
		if (esc) {
			switch (*p) {
			case '&':
				if (pmatch[0].rm_so != -1)
					ucv_stringbuf_addstr(resbuf,
						subject + pmatch[0].rm_so,
						pmatch[0].rm_eo - pmatch[0].rm_so);
				break;

			case '`':
				if (pmatch[0].rm_so != -1)
					ucv_stringbuf_addstr(resbuf, subject, pmatch[0].rm_so);
				break;

			case '\'':
				if (pmatch[0].rm_so != -1)
					ucv_stringbuf_addstr(resbuf,
						subject + pmatch[0].rm_eo,
						strlen(subject + pmatch[0].rm_eo));
				break;

			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
				i = *p - '0';
				if (i < plen && pmatch[i].rm_so != -1) {
					ucv_stringbuf_addstr(resbuf,
						subject + pmatch[i].rm_so,
						pmatch[i].rm_eo - pmatch[i].rm_so);
				}
				else {
					ucv_stringbuf_append(resbuf, "$");
					ucv_stringbuf_addstr(resbuf, p, 1);
				}
				break;

			case '$':
				ucv_stringbuf_append(resbuf, "$");
				break;

			default:
				ucv_stringbuf_append(resbuf, "$");
				ucv_stringbuf_addstr(resbuf, p, 1);
			}

			esc = false;
		}
		else if (*p == '$') {
			esc = true;
		}
		else {
			ucv_stringbuf_addstr(resbuf, p, 1);
		}
	}

	free(r);
}

/**
 * Replace occurrences of the specified pattern in the string passed as the
 * first argument.
 *
 * - The pattern value may be either a regular expression or a plain string.
 * - The replace value may be a function which is invoked for each found pattern
 *   or any other value which is converted into a plain string and used as
 *   replacement.
 * - When an optional limit is specified, substitutions are performed only that
 *   many times.
 * - If the pattern is a regular expression and not using the `g` flag, then
 *   only the first occurrence in the string is replaced.
 * - If the `g` flag is used or if the pattern is not a regular expression, all
 *   occurrences are replaced.
 * - If the replace value is a callback function, it is invoked with the found
 *   substring as the first and any capture group values as subsequent
 *   parameters.
 * - If the replace value is a string, specific substrings are substituted
 *   before it is inserted into the result.
 *
 * Returns a new string with the pattern replaced.
 *
 * @function module:core#replace
 *
 * @param {string} str
 * The string in which to replace occurrences.
 *
 * @param {RegExp|string} pattern
 * The pattern to be replaced.
 *
 * @param {Function|string} replace
 * The replacement value.
 *
 * @param {number} [limit]
 * The optional limit of substitutions.
 *
 * @returns {string}
 *
 * @example
 * replace("barfoobaz", /(f)(o+)/g, "[$$|$`|$&|$'|$1|$2|$3]")  // bar[$|bar|foo|baz|f|oo|$3]baz
 * replace("barfoobaz", /(f)(o+)/g, uc)                        // barFOObaz
 * replace("barfoobaz", "a", "X")                              // bXrfoobXz
 * replace("barfoobaz", /(.)(.)(.)/g, function(m, c1, c2, c3) {
 *     return c3 + c2 + c1;
 * })                                                          // raboofzab
 * replace("aaaaa", "a", "x", 3)                               // xxxaa
 * replace("foo bar baz", /[ao]/g, "x", 3)                     // fxx bxr baz
 */
static uc_value_t *
uc_replace(uc_vm_t *vm, size_t nargs)
{
	char *sb = NULL, *pt = NULL, *p, *l;
	uc_value_t *subject = uc_fn_arg(0);
	uc_value_t *pattern = uc_fn_arg(1);
	uc_value_t *replace = uc_fn_arg(2);
	uc_value_t *limitval = uc_fn_arg(3);
	bool sb_freeable, pt_freeable;
	regmatch_t *pmatch = NULL;
	size_t pl, nmatch, limit;
	uc_regexp_t *re = NULL;
	uc_stringbuf_t *resbuf;
	int eflags = 0, res;

	if (!pattern || !subject || !replace)
		return NULL;

	nmatch = 1;

	if (ucv_type(pattern) == UC_REGEXP) {
		re = (uc_regexp_t *)pattern;
		nmatch += re->regexp.re_nsub;
	}

	pmatch = calloc(nmatch, sizeof(regmatch_t));

	if (!pmatch)
		return NULL;

	sb = uc_cast_string(vm, &subject, &sb_freeable);
	resbuf = ucv_stringbuf_new();
	limit = limitval ? ucv_uint64_get(limitval) : SIZE_MAX;

	if (re) {
		p = sb;

		while (limit > 0) {
			res = regexec(&re->regexp, p, nmatch, pmatch, eflags);

			if (res == REG_NOMATCH)
				break;

			ucv_stringbuf_addstr(resbuf, p, pmatch[0].rm_so);

			if (ucv_is_callable(replace))
				uc_replace_cb(vm, replace, p, pmatch, nmatch, resbuf);
			else
				uc_replace_str(vm, replace, p, pmatch, nmatch, resbuf);

			if (pmatch[0].rm_so != pmatch[0].rm_eo)
				p += pmatch[0].rm_eo;
			else if (*p)
				ucv_stringbuf_addstr(resbuf, p++, 1);
			else
				break;

			if (re->global)
				eflags |= REG_NOTBOL;
			else
				break;

			limit--;
		}

		ucv_stringbuf_addstr(resbuf, p, strlen(p));
	}
	else {
		pt = uc_cast_string(vm, &pattern, &pt_freeable);
		pl = strlen(pt);

		l = p = sb;

		while (limit > 0) {
			if (pl == 0 || !strncmp(p, pt, pl)) {
				ucv_stringbuf_addstr(resbuf, l, p - l);

				pmatch[0].rm_so = p - l;
				pmatch[0].rm_eo = pmatch[0].rm_so + pl;

				if (ucv_is_callable(replace))
					uc_replace_cb(vm, replace, l, pmatch, 1, resbuf);
				else
					uc_replace_str(vm, replace, l, pmatch, 1, resbuf);

				if (pl) {
					l = p + pl;
					p += pl - 1;
				}
				else {
					l = p;
				}

				limit--;
			}

			if (!*p++)
				break;
		}

		ucv_stringbuf_addstr(resbuf, l, strlen(l));

		if (pt_freeable)
			free(pt);
	}

	free(pmatch);

	if (sb_freeable)
		free(sb);

	return ucv_stringbuf_finish(resbuf);
}

static struct json_tokener *
uc_json_from_object(uc_vm_t *vm, uc_value_t *obj, json_object **jso)
{
	bool trail = false, eof = false;
	enum json_tokener_error err;
	struct json_tokener *tok;
	uc_value_t *rfn, *rbuf;
	uc_stringbuf_t *buf;

	rfn = ucv_property_get(obj, "read");

	if (!ucv_is_callable(rfn)) {
		uc_vm_raise_exception(vm, EXCEPTION_TYPE,
		                      "Input object does not implement read() method");

		return NULL;
	}

	tok = xjs_new_tokener();

	while (true) {
		uc_vm_stack_push(vm, ucv_get(obj));
		uc_vm_stack_push(vm, ucv_get(rfn));
		uc_vm_stack_push(vm, ucv_int64_new(1024));

		if (uc_vm_call(vm, true, 1) != EXCEPTION_NONE) {
			json_tokener_free(tok);

			return NULL;
		}

		rbuf = uc_vm_stack_pop(vm);

		/* check EOF */
		eof = (rbuf == NULL || (ucv_type(rbuf) == UC_STRING && ucv_string_length(rbuf) == 0));

		/* on EOF, stop parsing unless trailing garbage was detected which handled below */
		if (eof && !trail) {
			ucv_put(rbuf);

			/* Didn't parse a complete object yet, possibly a non-delimitted atomic value
			   such as `null`, `true` etc. - nudge parser by sending final zero byte.
			   See json-c issue #681 <https://github.com/json-c/json-c/issues/681> */
			if (json_tokener_get_error(tok) == json_tokener_continue)
				*jso = json_tokener_parse_ex(tok, "\0", 1);

			break;
		}

		if (trail || *jso) {
			uc_vm_raise_exception(vm, EXCEPTION_SYNTAX,
			                      "Trailing garbage after JSON data");

			json_tokener_free(tok);
			ucv_put(rbuf);

			return NULL;
		}

		if (ucv_type(rbuf) != UC_STRING) {
			buf = xprintbuf_new();
			ucv_to_stringbuf_formatted(vm, buf, rbuf, 0, '\0', 0);

			*jso = json_tokener_parse_ex(tok, buf->buf, printbuf_length(buf));

			trail = (json_tokener_get_error(tok) == json_tokener_success &&
			         json_tokener_get_parse_end(tok) < (size_t)printbuf_length(buf));

			printbuf_free(buf);
		}
		else {
			*jso = json_tokener_parse_ex(tok, ucv_string_get(rbuf), ucv_string_length(rbuf));

			trail = (json_tokener_get_error(tok) == json_tokener_success &&
			         json_tokener_get_parse_end(tok) < ucv_string_length(rbuf));
		}

		ucv_put(rbuf);

		err = json_tokener_get_error(tok);

		if (err != json_tokener_success && err != json_tokener_continue)
			break;
	}

	return tok;
}

static struct json_tokener *
uc_json_from_string(uc_vm_t *vm, uc_value_t *str, json_object **jso)
{
	struct json_tokener *tok = xjs_new_tokener();
	size_t i;
	char *p;

	/* NB: the len + 1 here is intentional to pass the terminating \0 byte
	 * to the json-c parser. This is required to work-around upstream
	 * issue #681 <https://github.com/json-c/json-c/issues/681> */
	*jso = json_tokener_parse_ex(tok, ucv_string_get(str), ucv_string_length(str) + 1);

	if (json_tokener_get_error(tok) == json_tokener_success) {
		p = ucv_string_get(str);

		for (i = json_tokener_get_parse_end(tok); i < ucv_string_length(str); i++) {
			if (!isspace(p[i])) {
				uc_vm_raise_exception(vm, EXCEPTION_SYNTAX,
				                      "Trailing garbage after JSON data");


				json_tokener_free(tok);

				return NULL;
			}
		}
	}

	return tok;
}

/**
 * Parse the given string or resource as JSON and return the resulting value.
 *
 * If the input argument is a plain string, it is directly parsed as JSON.
 *
 * If an array, object or resource value is given, this function will attempt to
 * invoke a `read()` method on it to read chunks of input text to incrementally
 * parse as JSON data. Reading will stop if the object's `read()` method returns
 * either `null` or an empty string.
 *
 * Throws an exception on parse errors, trailing garbage, or premature EOF.
 *
 * Returns the parsed JSON data.
 *
 * @function module:core#json
 *
 * @param {string} str_or_resource
 * The string or resource object to be parsed as JSON.
 *
 * @returns {*}
 *
 * @example
 * json('{"a":true, "b":123}')   // { "a": true, "b": 123 }
 * json('[1,2,')                 // Throws an exception
 *
 * import { open } from 'fs';
 * let fd = open('example.json', 'r');
 * json(fd);                     // will keep invoking `fd.read()` until EOF and
 *                               // incrementally parse each read chunk.
 *
 * let x = proto(
 *     [ '{"foo":', 'true, ', '"bar":', 'false}' ],
 *     { read: function() { return shift(this) } }
 * );
 * json(x);                      // will keep invoking `x.read()` until array
 *                               // is empty incrementally parse each piece
 *
 */
static uc_value_t *
uc_json(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *rv = NULL, *src = uc_fn_arg(0);
	struct json_tokener *tok = NULL;
	enum json_tokener_error err;
	json_object *jso = NULL;

	switch (ucv_type(src)) {
	case UC_STRING:
		tok = uc_json_from_string(vm, src, &jso);
		break;

	case UC_RESOURCE:
	case UC_OBJECT:
	case UC_ARRAY:
		tok = uc_json_from_object(vm, src, &jso);
		break;

	default:
		uc_vm_raise_exception(vm, EXCEPTION_TYPE,
		                      "Passed value is neither a string nor an object");
	}

	if (!tok)
		goto out;

	err = json_tokener_get_error(tok);

	if (err == json_tokener_continue) {
		uc_vm_raise_exception(vm, EXCEPTION_SYNTAX,
		                      "Unexpected end of string in JSON data");

		goto out;
	}
	else if (err != json_tokener_success) {
		uc_vm_raise_exception(vm, EXCEPTION_SYNTAX,
		                      "Failed to parse JSON string: %s",
		                      json_tokener_error_desc(err));

		goto out;
	}

	rv = ucv_from_json(vm, jso);

out:
	if (tok)
		json_tokener_free(tok);

	json_object_put(jso);

	return rv;
}

static char *
include_path(const char *curpath, const char *incpath)
{
	char *dup, *res;
	int len;

	if (*incpath == '/')
		return realpath(incpath, NULL);

	dup = curpath ? strrchr(curpath, '/') : NULL;

	if (dup)
		len = asprintf(&res, "%.*s/%s", (int)(dup - curpath), curpath, incpath);
	else
		len = asprintf(&res, "./%s", incpath);

	if (len == -1)
		return NULL;

	dup = realpath(res, NULL);

	free(res);

	return dup;
}

static uc_value_t *
uc_include_common(uc_vm_t *vm, size_t nargs, bool raw_mode)
{
	uc_value_t *path = uc_fn_arg(0);
	uc_value_t *scope = uc_fn_arg(1);
	uc_value_t *rv = NULL, *sc = NULL;
	uc_closure_t *closure = NULL;
	size_t i;
	char *p;

	if (ucv_type(path) != UC_STRING) {
		uc_vm_raise_exception(vm, EXCEPTION_TYPE,
		                      "Passed filename is not a string");

		return NULL;
	}

	if (scope && ucv_type(scope) != UC_OBJECT) {
		uc_vm_raise_exception(vm, EXCEPTION_TYPE,
		                      "Passed scope value is not an object");

		return NULL;
	}

	/* find calling closure */
	for (i = vm->callframes.count; i > 0; i--) {
		closure = vm->callframes.entries[i - 1].closure;

		if (closure)
			break;
	}

	if (!closure)
		return NULL;

	p = include_path(uc_program_function_source(closure->function)->runpath, ucv_string_get(path));

	if (!p) {
		uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
		                      "Include file not found");

		return NULL;
	}

	if (ucv_prototype_get(scope)) {
		sc = ucv_get(scope);
	}
	else if (scope) {
		sc = ucv_object_new(vm);

		ucv_object_foreach(scope, key, val)
			ucv_object_add(sc, key, ucv_get(val));

		ucv_prototype_set(sc, ucv_get(uc_vm_scope_get(vm)));
	}
	else {
		sc = ucv_get(uc_vm_scope_get(vm));
	}

	if (uc_require_ucode(vm, p, sc, &rv, raw_mode))
		ucv_put(rv);

	ucv_put(sc);
	free(p);

	return NULL;
}

/**
 * Evaluate and include the file at the given path and optionally override the
 * execution scope with the given scope object.
 *
 * By default, the file is executed within the same scope as the calling
 * `include()`, but by passing an object as the second argument, it is possible
 * to extend the scope available to the included file.
 *
 * This is useful to supply additional properties as global variables to the
 * included code. To sandbox included code, that is giving it only access to
 * explicitly provided properties, the `proto()` function can be used to create
 * a scope object with an empty prototype.
 *
 * @function module:core#include
 *
 * @param {string} path
 * The path to the file to be included.
 *
 * @param {Object} [scope]
 * The optional scope object to override the execution scope.
 *
 * @example
 * // Load and execute "foo.uc" immediately
 * include("./foo.uc")
 *
 * // Execute the "supplemental.ucode" in an extended scope and make the "foo"
 * // and "bar" properties available as global variables
 * include("./supplemental.uc", {
 *   foo: true,
 *   bar: 123
 * })
 *
 * // Execute the "untrusted.ucode" in a sandboxed scope and make the "foo" and
 * // "bar" variables as well as the "print" function available to it.
 * // By assigning an empty prototype object to the scope, included code has no
 * // access to other global values anymore.
 * include("./untrusted.uc", proto({
 *   foo: true,
 *   bar: 123,
 *   print: print
 * }, {}))
 */
static uc_value_t *
uc_include(uc_vm_t *vm, size_t nargs)
{
	return uc_include_common(vm, nargs, vm->config && vm->config->raw_mode);
}

/**
 * When invoked with a string value as the first argument, the function acts
 * like `include()` but captures the output of the included file as a string and
 * returns the captured contents.
 *
 * The second argument is treated as the scope.
 *
 * When invoked with a function value as the first argument, `render()` calls
 * the given function and passes all subsequent arguments to it.
 *
 * Any output produced by the called function is captured and returned as a
 * string. The return value of the called function is discarded.
 *
 * @function module:core#render
 *
 * @param {string|Function} path_or_func
 * The path to the file or the function to be rendered.
 *
 * @param {Object|*} [scope_or_fnarg1]
 * The optional scope or the first argument for the function.
 *
 * @param {*} [fnarg2]
 * The second argument for the function.
 *
 * @param {...*} [fnargN]
 * Additional arguments for the function.
 *
 * @returns {string}
 *
 * @example
 * // Renders template file with given scope and captures the output as a string
 * const output = render("./template.uc", { foo: "bar" });
 *
 * // Calls a function, captures the output, and returns it as a string
 * const result = render(function(name) {
 *     printf("Hello, %s!\n", name);
 * }, "Alice");
 */
static uc_value_t *
uc_render(uc_vm_t *vm, size_t nargs)
{
	uc_string_t hdr = { .header = { .type = UC_STRING, .refcount = 1 } };
	uc_string_t *ustr = NULL;
	FILE *mem, *prev;
	size_t len = 0;

	mem = open_memstream((char **)&ustr, &len);

	if (!mem)
		goto out;

	/* reserve space for uc_string_t header... */
	if (fwrite(&hdr, 1, sizeof(hdr), mem) != sizeof(hdr))
		goto out;

	/* divert VM output to memory fd */
	prev = vm->output;
	vm->output = mem;

	/* execute function */
	if (ucv_is_callable(uc_fn_arg(0)))
		(void) uc_vm_call(vm, false, nargs - 1);

	/* execute include */
	else
		(void) uc_include_common(vm, nargs, false);

	/* restore previous VM output */
	vm->output = prev;
	fclose(mem);

	/* update uc_string_t length */
	ustr->length = len - sizeof(*ustr);

	return &ustr->header;

out:
	uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
	                      "Unable to initialize output memory: %s",
	                      strerror(errno));

	if (mem)
		fclose(mem);

	free(ustr);

	return NULL;
}

/**
 * Print any of the given values to stderr. Arrays and objects are converted to
 * their JSON representation.
 *
 * Returns the amount of bytes printed.
 *
 * @function module:core#warn
 *
 * @param {...*} x
 * The values to be printed.
 *
 * @returns {number}
 *
 * @example
 * warn("Hello", "world");  // Print "Helloworld" to stderr
 * warn({ key: "value" });  // Print JSON representation of the object to stderr
 */
static uc_value_t *
uc_warn(uc_vm_t *vm, size_t nargs)
{
	return uc_print_common(vm, nargs, stderr);
}

/**
 * Executes the given command, waits for completion, and returns the resulting
 * exit code.
 *
 * The command argument may be either a string, in which case it is passed to
 * `/bin/sh -c`, or an array, which is directly converted into an `execv()`
 * argument vector.
 *
 *  - If the program terminated normally, a positive integer holding the
 *    program's `exit()` code is returned.
 *  - If the program was terminated by an uncaught signal, a negative signal
 *    number is returned.
 *  - If the optional timeout argument is specified, the program is terminated
 *    by `SIGKILL` after that many milliseconds if it doesn't complete within
 *    the timeout.
 *
 * Omitting the timeout argument or passing `0` disables the command timeout.
 *
 * Returns the program exit code.
 *
 * @function module:core#system
 *
 * @param {string|Array} command
 * The command to be executed.
 *
 * @param {number} [timeout]
 * The optional timeout in milliseconds.
 *
 * @returns {number}
 *
 * @example
 * // Execute through `/bin/sh`
 * // prints "Hello world" to stdout and returns 3
 * system("echo 'Hello world' && exit 3");
 *
 * // Execute argument vector
 * // prints the UNIX timestamp to stdout and returns 0
 * system(["/usr/bin/date", "+%s"]);
 *
 * // Apply a timeout
 * // returns -9
 * system("sleep 3 && echo 'Success'", 1000);
 */
static uc_value_t *
uc_system(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *cmdline = uc_fn_arg(0);
	uc_value_t *timeout = uc_fn_arg(1);
	const char **arglist, *fn;
	sigset_t sigmask, sigomask;
	struct timespec ts;
	size_t i, len;
	int64_t tms;
	pid_t cld;
	int rc;

	if (timeout && (ucv_type(timeout) != UC_INTEGER || ucv_int64_get(timeout) < 0)) {
		uc_vm_raise_exception(vm, EXCEPTION_TYPE,
		                      "Invalid timeout specified");

		return NULL;
	}

	switch (ucv_type(cmdline)) {
	case UC_STRING:
		arglist = xalloc(sizeof(*arglist) * 4);
		arglist[0] = xstrdup("/bin/sh");
		arglist[1] = xstrdup("-c");
		arglist[2] = ucv_to_string(vm, cmdline);
		arglist[3] = NULL;
		break;

	case UC_ARRAY:
		len = ucv_array_length(cmdline);

		if (len == 0) {
			uc_vm_raise_exception(vm, EXCEPTION_TYPE,
			                      "Passed command array is empty");

			return NULL;
		}

		arglist = xalloc(sizeof(*arglist) * (len + 1));

		for (i = 0; i < len; i++)
			arglist[i] = ucv_to_string(vm, ucv_array_get(cmdline, i));

		arglist[i] = NULL;

		break;

	default:
		uc_vm_raise_exception(vm, EXCEPTION_TYPE,
		                      "Passed command is neither string nor array");

		return NULL;
	}

	tms = timeout ? ucv_int64_get(timeout) : 0;

	if (tms > 0) {
		sigemptyset(&sigmask);
		sigaddset(&sigmask, SIGCHLD);

		if (sigprocmask(SIG_BLOCK, &sigmask, &sigomask) < 0) {
			fn = "sigprocmask";
			goto fail;
		}
	}

	cld = fork();

	switch (cld) {
	case -1:
		fn = "fork";
		goto fail;

	case 0:
		execvp(arglist[0], (char * const *)arglist);
		exit(-1);

		break;

	default:
		if (tms > 0) {
			ts.tv_sec = tms / 1000;
			ts.tv_nsec = (tms % 1000) * 1000000;

			while (1) {
				if (sigtimedwait(&sigmask, NULL, &ts) < 0) {
					if (errno == EINTR)
						continue;

					if (errno != EAGAIN) {
						fn = "sigtimedwait";
						goto fail;
					}

					kill(cld, SIGKILL);
				}

				break;
			}
		}

		while (waitpid(cld, &rc, 0) < 0) {
			if (errno == EINTR)
				continue;

			fn = "waitpid";
			goto fail;
		}

		if (tms > 0)
			sigprocmask(SIG_SETMASK, &sigomask, NULL);

		for (i = 0; arglist[i]; i++)
			free((char *)arglist[i]);

		free(arglist);

		if (WIFEXITED(rc))
			return ucv_int64_new(WEXITSTATUS(rc));
		else if (WIFSIGNALED(rc))
			return ucv_int64_new(-WTERMSIG(rc));
		else if (WIFSTOPPED(rc))
			return ucv_int64_new(-WSTOPSIG(rc));

		return NULL;
	}

fail:
	if (tms > 0)
		sigprocmask(SIG_SETMASK, &sigomask, NULL);

	for (i = 0; arglist[i]; i++)
		free((char *)arglist[i]);

	free(arglist);

	uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
	                      "%s(): %s", fn, strerror(errno));

	return NULL;
}

/**
 * Enables or disables VM opcode tracing.
 *
 * When invoked with a positive non-zero level, opcode tracing is enabled and
 * debug information is printed to stderr as the program is executed.
 *
 * Invoking `trace()` with zero as an argument turns off opcode tracing.
 *
 * @function module:core#trace
 *
 * @param {number} level
 * The level of tracing to enable.
 *
 * @example
 * trace(1);   // Enables opcode tracing
 * trace(0);   // Disables opcode tracing
 */
static uc_value_t *
uc_trace(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *level = uc_fn_arg(0);
	uint8_t prev_level;

	if (ucv_type(level) != UC_INTEGER) {
		uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Invalid level specified");

		return NULL;
	}

	prev_level = vm->trace;
	vm->trace = ucv_int64_get(level);

	return ucv_int64_new(prev_level);
}

/**
 * Get or set the prototype of the array or object value `val`.
 *
 * When invoked without a second argument, the function returns the current
 * prototype of the value in `val` or `null` if there is no prototype or if the
 * given value is neither an object nor an array.
 *
 * When invoked with a second prototype argument, the given `proto` value is set
 * as the prototype on the array or object in `val`.
 *
 * Throws an exception if the given prototype value is not an object.
 *
 * @function module:core#proto
 *
 * @param {Array|Object} val
 * The array or object value.
 *
 * @param {Object} [proto]
 * The optional prototype object.
 *
 * @returns {?Object}
 *
 * @example
 * const arr = [1, 2, 3];
 * proto(arr);                 // Returns the current prototype of the array (null by default)
 * proto(arr, { foo: true });  // Sets the given object as the prototype of the array
 */
static uc_value_t *
uc_proto(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *val = uc_fn_arg(0);
	uc_value_t *proto = NULL;

	if (nargs < 2)
		return ucv_get(ucv_prototype_get(val));

	proto = uc_fn_arg(1);

	if (!ucv_prototype_set(val, proto))
		uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Passed value is neither a prototype, resource or object");

	ucv_get(proto);

	return ucv_get(val);
}

/**
 * Pause execution for the given amount of milliseconds.
 *
 * @function module:core#sleep
 *
 * @param {number} milliseconds
 * The amount of milliseconds to sleep.
 *
 * @returns {boolean}
 *
 * @example
 * sleep(1000);                          // Sleeps for 1 second
 */
static uc_value_t *
uc_sleep(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *duration = uc_fn_arg(0);
	struct timeval tv;
	int64_t ms;

	ms = ucv_to_integer(duration);

	if (errno != 0 || ms <= 0)
		return ucv_boolean_new(false);

	tv.tv_sec = ms / 1000;
	tv.tv_usec = (ms % 1000) * 1000;

	select(0, NULL, NULL, NULL, &tv);

	return ucv_boolean_new(true);
}

/**
 * Raise an exception with the given message parameter when the value in `cond`
 * is not truish.
 *
 * When `message` is omitted, the default value is `Assertion failed`.
 *
 * @function module:core#assert
 *
 * @param {*} cond
 * The value to check for truthiness.
 *
 * @param {string} [message]
 * The message to include in the exception.
 *
 * @throws {Error} When the condition is falsy.
 *
 * @example
 * assert(true, "This is true");  // No exception is raised
 * assert(false);                 // Exception is raised with the default message "Assertion failed"
 */
static uc_value_t *
uc_assert(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *cond = uc_fn_arg(0);
	uc_value_t *msg = uc_fn_arg(1);
	bool freeable = false;
	char *s;

	if (!ucv_is_truish(cond)) {
		s = msg ? uc_cast_string(vm, &msg, &freeable) : "Assertion failed";

		uc_vm_raise_exception(vm, EXCEPTION_USER, "%s", s);

		if (freeable)
			free(s);

		return NULL;
	}

	return ucv_get(cond);
}

/**
 * Construct a regular expression instance from the given `source` pattern
 * string and any flags optionally specified by the `flags` argument.
 *
 *  - Throws a type error exception if `flags` is not a string or if the string
 *    in `flags` contains unrecognized regular expression flag characters.
 *  - Throws a syntax error when the pattern in `source` cannot be compiled into
 *    a valid regular expression.
 *
 * Returns the compiled regular expression value.
 *
 * @function module:core#regexp
 *
 * @param {string} source
 * The pattern string.
 *
 * @param {string} [flags]
 * The optional regular expression flags.
 *
 * @returns {RegExp}
 *
 * @example
 * regexp('foo.*bar', 'is');   // equivalent to /foo.*bar/is
 * regexp('foo.*bar', 'x');    // throws a "Type error: Unrecognized flag character 'x'" exception
 * regexp('foo.*(');           // throws a "Syntax error: Unmatched ( or \( exception"
 */
static uc_value_t *
uc_regexp(uc_vm_t *vm, size_t nargs)
{
	bool icase = false, newline = false, global = false, freeable;
	uc_value_t *source = uc_fn_arg(0);
	uc_value_t *flags = uc_fn_arg(1);
	uc_value_t *regex = NULL;
	char *p, *err = NULL;

	if (flags) {
		if (ucv_type(flags) != UC_STRING) {
			uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Given flags argument is not a string");

			return NULL;
		}

		for (p = ucv_string_get(flags); *p; p++) {
			switch (*p) {
			case 'i':
				icase = true;
				break;

			case 's':
				newline = true;
				break;

			case 'g':
				global = true;
				break;

			default:
				uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Unrecognized flag character '%c'", *p);

				return NULL;
			}
		}
	}

	p = uc_cast_string(vm, &source, &freeable);
	regex = ucv_regexp_new(p, icase, newline, global, &err);

	if (freeable)
		free(p);

	if (err) {
		uc_vm_raise_exception(vm, EXCEPTION_SYNTAX, "%s", err);
		ucv_put(regex);
		free(err);

		return NULL;
	}

	return regex;
}

/**
 * Match the given subject against the supplied wildcard (file glob) pattern.
 *
 *  - If a truthy value is supplied as the third argument, case-insensitive
 *    matching is performed.
 *  - If a non-string value is supplied as the subject, it is converted into a
 *    string before being matched.
 *
 * Returns `true` when the value matched the given pattern, otherwise `false`.
 *
 * @function module:core#wildcard
 *
 * @param {*} subject
 * The subject to match against the wildcard pattern.
 *
 * @param {string} pattern
 * The wildcard pattern.
 *
 * @param {boolean} [nocase]
 * Whether to perform case-insensitive matching.
 *
 * @returns {boolean}
 *
 * @example
 * wildcard("file.txt", "*.txt");        // Returns true
 * wildcard("file.txt", "*.TXT", true);  // Returns true (case-insensitive match)
 * wildcard("file.txt", "*.jpg");        // Returns false
 */
static uc_value_t *
uc_wildcard(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *subject = uc_fn_arg(0);
	uc_value_t *pattern = uc_fn_arg(1);
	uc_value_t *icase = uc_fn_arg(2);
	int flags = 0, rv;
	bool freeable;
	char *s;

	if (!subject || ucv_type(pattern) != UC_STRING)
		return NULL;

	if (ucv_is_truish(icase))
		flags |= FNM_CASEFOLD;

	s = uc_cast_string(vm, &subject, &freeable);
	rv = fnmatch(ucv_string_get(pattern), s, flags);

	if (freeable)
		free(s);

	return ucv_boolean_new(rv == 0);
}

/**
 * Determine the path of the source file currently being executed by ucode.
 *
 * @function module:core#sourcepath
 *
 * @param {number} [depth=0]
 * The depth to walk up the call stack.
 *
 * @param {boolean} [dironly]
 * Whether to return only the directory portion of the source file path.
 *
 * @returns {?string}
 *
 * @example
 * sourcepath();         // Returns the path of the currently executed file
 * sourcepath(1);        // Returns the path of the parent source file
 * sourcepath(2, true);  // Returns the directory portion of the grandparent source file path
 */
static uc_value_t *
uc_sourcepath(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *calldepth = uc_fn_arg(0);
	uc_value_t *dironly = uc_fn_arg(1);
	uc_value_t *rv = NULL;
	uc_callframe_t *frame;
	char *path = NULL;
	int64_t depth;
	size_t i;

	depth = ucv_to_integer(calldepth);

	if (errno)
		depth = 0;

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

		if (!frame->closure)
			continue;

		if (depth > 0) {
			depth--;
			continue;
		}

		path = realpath(uc_program_function_source(frame->closure->function)->runpath, NULL);
		break;
	}

	if (path) {
		if (ucv_is_truish(dironly))
			rv = ucv_string_new(dirname(path));
		else
			rv = ucv_string_new(path);

		free(path);
	}

	return rv;
}

static uc_value_t *
uc_min_max(uc_vm_t *vm, size_t nargs, int cmp)
{
	uc_value_t *rv = NULL, *val;
	bool set = false;
	size_t i;

	for (i = 0; i < nargs; i++) {
		val = uc_fn_arg(i);

		if (!set || ucv_compare(cmp, val, rv, NULL)) {
			set = true;
			rv = val;
		}
	}

	return ucv_get(rv);
}

/**
 * Return the smallest value among all parameters passed to the function.
 *
 * @function module:core#min
 *
 * @param {...*} [val]
 * The values to compare.
 *
 * @returns {*}
 *
 * @example
 * min(5, 2.1, 3, "abc", 0.3);            // Returns 0.3
 * min(1, "abc");                         // Returns 1
 * min("1", "abc");                       // Returns "1"
 * min("def", "abc", "ghi");              // Returns "abc"
 * min(true, false);                      // Returns false
 */
static uc_value_t *
uc_min(uc_vm_t *vm, size_t nargs)
{
	return uc_min_max(vm, nargs, I_LT);
}

/**
 * Return the largest value among all parameters passed to the function.
 *
 * @function module:core#max
 *
 * @param {...*} [val]
 * The values to compare.
 *
 * @returns {*}
 *
 * @example
 * max(5, 2.1, 3, "abc", 0.3);            // Returns 5
 * max(1, "abc");                         // Returns 1 (!)
 * max("1", "abc");                       // Returns "abc"
 * max("def", "abc", "ghi");              // Returns "ghi"
 * max(true, false);                      // Returns true
 */
static uc_value_t *
uc_max(uc_vm_t *vm, size_t nargs)
{
	return uc_min_max(vm, nargs, I_GT);
}


/* -------------------------------------------------------------------------
 * The following base64 encoding and decoding routines are taken from
 * https://git.openwrt.org/?p=project/libubox.git;a=blob;f=base64.c
 * and modified for use in ucode.
 *
 * Original copyright and license statements below.
 */

/*
 * base64 - libubox base64 functions
 *
 * Copyright (C) 2015 Felix Fietkau <nbd@openwrt.org>
 *
 * 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.
 */

/*	$OpenBSD: base64.c,v 1.7 2013/12/31 02:32:56 tedu Exp $	*/

/*
 * Copyright (c) 1996 by Internet Software Consortium.
 *
 * Permission to use, copy, modify, and 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 INTERNET SOFTWARE CONSORTIUM DISCLAIMS
 * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE
 * CONSORTIUM 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.
 */

/*
 * Portions Copyright (c) 1995 by International Business Machines, Inc.
 *
 * International Business Machines, Inc. (hereinafter called IBM) grants
 * permission under its copyrights to use, copy, modify, and distribute this
 * Software with or without fee, provided that the above copyright notice and
 * all paragraphs of this notice appear in all copies, and that the name of IBM
 * not be used in connection with the marketing of any product incorporating
 * the Software or modifications thereof, without specific, written prior
 * permission.
 *
 * To the extent it has a right to do so, IBM grants an immunity from suit
 * under its patents, if any, for the use, sale or manufacture of products to
 * the extent that such products are used for performing Domain Name System
 * dynamic updates in TCP/IP networks by means of the Software.  No immunity is
 * granted for any product per se or for any other function of any product.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE.  IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL,
 * DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN
 * IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES.
 */

/* skips all whitespace anywhere.
   converts characters, four at a time, starting at (or after)
   src from base - 64 numbers into three 8 bit bytes in the target area.
   it returns the number of data bytes stored at the target, or -1 on error.
 */

/**
 * Decodes the given base64 encoded string and returns the decoded result.
 *
 *  - If non-whitespace, non-base64 characters are encountered, if invalid
 *    padding or trailing garbage is found, the function returns `null`.
 *  - If a non-string argument is given, the function returns `null`.
 *
 * @function module:core#b64dec
 *
 * @param {string} str
 * The base64 encoded string to decode.
 *
 * @returns {?string}
 *
 * @example
 * b64dec("VGhpcyBpcyBhIHRlc3Q=");         // Returns "This is a test"
 * b64dec(123);                           // Returns null
 * b64dec("XXX");                         // Returns null
 */
static uc_value_t *
uc_b64dec(uc_vm_t *vm, size_t nargs)
{
	enum { BYTE1, BYTE2, BYTE3, BYTE4 } state;
	uc_value_t *str = uc_fn_arg(0);
	uc_stringbuf_t *buf;
	const char *src;
	unsigned int ch;
	uint8_t val;
	size_t off;

	if (ucv_type(str) != UC_STRING)
		return NULL;

	buf = ucv_stringbuf_new();
	src = ucv_string_get(str);
	off = printbuf_length(buf);

	state = BYTE1;

	/* memset the last expected output char to pre-grow the output buffer */
	printbuf_memset(buf, off + (ucv_string_length(str) / 4) * 3, 0, 1);

	while ((ch = (unsigned char)*src++) != '\0') {
		if (isspace(ch))	/* Skip whitespace anywhere. */
			continue;

		if (ch == '=')
			break;

		if (ch >= 'A' && ch <= 'Z')
			val = ch - 'A';
		else if (ch >= 'a' && ch <= 'z')
			val = ch - 'a' + 26;
		else if (ch >= '0' && ch <= '9')
			val = ch - '0' + 52;
		else if (ch == '+')
			val = 62;
		else if (ch == '/')
			val = 63;
		else
			goto err;

		switch (state) {
		case BYTE1:
			buf->buf[off] = val << 2;
			state = BYTE2;
			break;

		case BYTE2:
			buf->buf[off++] |= val >> 4;
			buf->buf[off] = (val & 0x0f) << 4;
			state = BYTE3;
			break;

		case BYTE3:
			buf->buf[off++] |= val >> 2;
			buf->buf[off] = (val & 0x03) << 6;
			state = BYTE4;
			break;

		case BYTE4:
			buf->buf[off++] |= val;
			state = BYTE1;
			break;
		}
	}

	/*
	 * We are done decoding Base-64 chars.  Let's see if we ended
	 * on a byte boundary, and/or with erroneous trailing characters.
	 */

	if (ch == '=') {			/* We got a pad char. */
		ch = (unsigned char)*src++;	/* Skip it, get next. */
		switch (state) {
		case BYTE1:		/* Invalid = in first position */
		case BYTE2:		/* Invalid = in second position */
			goto err;

		case BYTE3:		/* Valid, means one byte of info */
			/* Skip any number of spaces. */
			for (; ch != '\0'; ch = (unsigned char)*src++)
				if (!isspace(ch))
					break;
			/* Make sure there is another trailing = sign. */
			if (ch != '=')
				goto err;
			ch = (unsigned char)*src++;		/* Skip the = */
			/* Fall through to "single trailing =" case. */
			/* FALLTHROUGH */

		case BYTE4:		/* Valid, means two bytes of info */
			/*
			 * We know this char is an =.  Is there anything but
			 * whitespace after it?
			 */
			for (; ch != '\0'; ch = (unsigned char)*src++)
				if (!isspace(ch))
					goto err;

			/*
			 * Now make sure for cases BYTE3 and BYTE4 that the "extra"
			 * bits that slopped past the last full byte were
			 * zeros.  If we don't check them, they become a
			 * subliminal channel.
			 */
			if (buf->buf[off] != 0)
				goto err;
		}
	} else {
		/*
		 * We ended by seeing the end of the string.  Make sure we
		 * have no partial bytes lying around.
		 */
		if (state != BYTE1)
			goto err;
	}

	/* Truncate buffer length to actual output length */
	buf->bpos = off;

	return ucv_stringbuf_finish(buf);

err:
	printbuf_free(buf);

	return NULL;
}

static const char Base64[] =
	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/**
 * Encodes the given string into base64 and returns the resulting string.
 *
 *  - If a non-string argument is given, the function returns `null`.
 *
 * @function module:core#b64enc
 *
 * @param {string} str
 * The string to encode.
 *
 * @returns {?string}
 *
 * @example
 * b64enc("This is a test");  // Returns "VGhpcyBpcyBhIHRlc3Q="
 * b64enc(123);               // Returns null
 */
static uc_value_t *
uc_b64enc(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *str = uc_fn_arg(0);
	unsigned char input[3] = {0};
	uc_stringbuf_t *buf;
	const char *src;
	char output[4];
	size_t len, i;

	if (ucv_type(str) != UC_STRING)
		return NULL;

	buf = ucv_stringbuf_new();
	src = ucv_string_get(str);
	len = ucv_string_length(str);

	while (2 < len) {
		input[0] = (unsigned char)*src++;
		input[1] = (unsigned char)*src++;
		input[2] = (unsigned char)*src++;
		len -= 3;

		output[0] = Base64[input[0] >> 2];
		output[1] = Base64[((input[0] & 0x03) << 4) + (input[1] >> 4)];
		output[2] = Base64[((input[1] & 0x0f) << 2) + (input[2] >> 6)];
		output[3] = Base64[input[2] & 0x3f];

		ucv_stringbuf_addstr(buf, output, sizeof(output));
	}

	/* Now we worry about padding. */
	if (0 != len) {
		/* Get what's left. */
		input[0] = input[1] = input[2] = '\0';
		for (i = 0; i < len; i++)
			input[i] = *src++;

		output[0] = Base64[input[0] >> 2];
		output[1] = Base64[((input[0] & 0x03) << 4) + (input[1] >> 4)];
		output[2] = (len == 1) ? '=' : Base64[((input[1] & 0x0f) << 2) + (input[2] >> 6)];
		output[3] = '=';

		ucv_stringbuf_addstr(buf, output, sizeof(output));
	}

	return ucv_stringbuf_finish(buf);
}

/* End of base64 code.
 * -------------------------------------------------------------------------
 */

static unsigned long
uc_uniq_ucv_hash(const void *k)
{
	union { double d; int64_t i; uint64_t u; } conv;
	uc_value_t *uv = (uc_value_t *)k;
	unsigned int h;
	uint8_t *u8;
	size_t len;

	h = ucv_type(uv);

	switch (h) {
	case UC_STRING:
		u8 = (uint8_t *)ucv_string_get(uv);
		len = ucv_string_length(uv);
		break;

	case UC_INTEGER:
		conv.i = ucv_int64_get(uv);

		if (errno == ERANGE) {
			h *= 2;
			conv.u = ucv_uint64_get(uv);
		}

		u8 = (uint8_t *)&conv.u;
		len = sizeof(conv.u);
		break;

	case UC_DOUBLE:
		conv.d = ucv_double_get(uv);

		u8 = (uint8_t *)&conv.u;
		len = sizeof(conv.u);
		break;

	default:
		u8 = (uint8_t *)&uv;
		len = sizeof(uv);
		break;
	}

	while (len > 0) {
		h = h * 129 + (*u8++) + LH_PRIME;
		len--;
	}

	return h;
}

static int
uc_uniq_ucv_equal(const void *k1, const void *k2)
{
	uc_value_t *uv1 = (uc_value_t *)k1;
	uc_value_t *uv2 = (uc_value_t *)k2;

	if (!ucv_is_scalar(uv1) && !ucv_is_scalar(uv2))
		return (uv1 == uv2);

	/* for the sake of array item uniqueness, treat two NaNs as equal */
	if (ucv_type(uv1) == UC_DOUBLE && ucv_type(uv2) == UC_DOUBLE &&
	    isnan(ucv_double_get(uv1)) && isnan(ucv_double_get(uv2)))
	    return true;

	return ucv_is_equal(uv1, uv2);
}

/**
 * Returns a new array containing all unique values of the given input array.
 *
 *  - The order is preserved, and subsequent duplicate values are skipped.
 *  - If a non-array argument is given, the function returns `null`.
 *
 * @function module:core#uniq
 *
 * @param {Array} array
 * The input array.
 *
 * @returns {?Array}
 *
 * @example
 * uniq([1, true, "foo", 2, true, "bar", "foo"]);       // Returns [1, true, "foo", 2, "bar"]
 * uniq("test");                                        // Returns null
 */
static uc_value_t *
uc_uniq(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *list = uc_fn_arg(0);
	uc_value_t *uniq = NULL;
	struct lh_table *seen;
	unsigned long hash;
	uc_value_t *item;
	size_t i, len;

	if (ucv_type(list) != UC_ARRAY)
		return NULL;

	seen = lh_table_new(16, NULL, uc_uniq_ucv_hash, uc_uniq_ucv_equal);
	uniq = ucv_array_new(vm);

	assert(seen && uniq);

	for (i = 0, len = ucv_array_length(list); i < len; i++) {
		item = ucv_array_get(list, i);
		hash = lh_get_hash(seen, item);

		if (!lh_table_lookup_entry_w_hash(seen, item, hash)) {
			lh_table_insert_w_hash(seen, item, NULL, hash, 0);
			ucv_array_push(uniq, ucv_get(item));
		}
	}

	lh_table_free(seen);

	return uniq;
}

/**
 * A time spec is a plain object describing a point in time, it is returned by
 * the {@link module:core#gmtime|gmtime()} and
 * {@link module:core#localtime|localtime()} functions and expected as parameter
 * by the complementary {@link module:core#timegm|timegm()} and
 * {@link module:core#timelocal|timelocal()} functions.
 *
 * When returned by `gmtime()` or `localtime()`, all members of the object will
 * be initialized, when passed as argument to `timegm()` or `timelocal()`, most
 * member values are optional.
 *
 * @typedef {Object} module:core.TimeSpec
 * @property {number} sec - Seconds (0..60)
 * @property {number} min - Minutes (0..59)
 * @property {number} hour - Hours (0..23)
 * @property {number} mday - Day of month (1..31)
 * @property {number} mon - Month (1..12)
 * @property {number} year - Year (>= 1900)
 * @property {number} wday - Day of week (1..7, Sunday = 7)
 * @property {number} yday - Day of year (1-366, Jan 1st = 1)
 * @property {number} isdst - Daylight saving time in effect (yes = 1)
 */
static uc_value_t *
uc_gettime_common(uc_vm_t *vm, size_t nargs, bool local)
{
	uc_value_t *ts = uc_fn_arg(0), *res;
	time_t t = ts ? (time_t)ucv_to_integer(ts) : time(NULL);
	struct tm *tm = (local ? localtime : gmtime)(&t);

	if (!tm)
		return NULL;

	res = ucv_object_new(vm);

	ucv_object_add(res, "sec", ucv_int64_new(tm->tm_sec));
	ucv_object_add(res, "min", ucv_int64_new(tm->tm_min));
	ucv_object_add(res, "hour", ucv_int64_new(tm->tm_hour));
	ucv_object_add(res, "mday", ucv_int64_new(tm->tm_mday));
	ucv_object_add(res, "mon", ucv_int64_new(tm->tm_mon + 1));
	ucv_object_add(res, "year", ucv_int64_new(tm->tm_year + 1900));
	ucv_object_add(res, "wday", ucv_int64_new(tm->tm_wday ? tm->tm_wday : 7));
	ucv_object_add(res, "yday", ucv_int64_new(tm->tm_yday + 1));
	ucv_object_add(res, "isdst", ucv_int64_new(tm->tm_isdst));

	return res;
}

/**
 * Return the given epoch timestamp (or now, if omitted) as a dictionary
 * containing broken-down date and time information according to the local
 * system timezone.
 *
 * See {@link module:core.TimeSpec|TimeSpec} for a description of the fields.
 *
 * Note that in contrast to the underlying `localtime(3)` C library function,
 * the values for `mon`, `wday`, and `yday` are 1-based, and the `year` is
 * 1900-based.
 *
 * @function module:core#localtime
 *
 * @param {number} [epoch]
 * The epoch timestamp.
 *
 * @returns {module:core.TimeSpec}
 *
 * @example
 * localtime(1647953502);
 * // Returns:
 * // {
 * //     sec: 42,
 * //     min: 51,
 * //     hour: 13,
 * //     mday: 22,
 * //     mon: 3,
 * //     year: 2022,
 * //     wday: 2,
 * //     yday: 81,
 * //     isdst: 0
 * // }
 */
static uc_value_t *
uc_localtime(uc_vm_t *vm, size_t nargs)
{
	return uc_gettime_common(vm, nargs, true);
}

/**
 * Like `localtime()` but interpreting the given epoch value as UTC time.
 *
 * See {@link module:core#localtime|localtime()} for details on the return value.
 *
 * @function module:core#gmtime
 *
 * @param {number} [epoch]
 * The epoch timestamp.
 *
 * @returns {module:core.TimeSpec}
 *
 * @example
 * gmtime(1647953502);
 * // Returns:
 * // {
 * //     sec: 42,
 * //     min: 51,
 * //     hour: 13,
 * //     mday: 22,
 * //     mon: 3,
 * //     year: 2022,
 * //     wday: 2,
 * //     yday: 81,
 * //     isdst: 0
 * // }
 */
static uc_value_t *
uc_gmtime(uc_vm_t *vm, size_t nargs)
{
	return uc_gettime_common(vm, nargs, false);
}

static uc_value_t *
uc_mktime_common(uc_vm_t *vm, size_t nargs, bool local)
{
#define FIELD(name, required) \
	{ #name, required, offsetof(struct tm, tm_##name) }

	const struct {
		const char *name;
		bool required;
		size_t off;
	} fields[] = {
		FIELD(sec, false),
		FIELD(min, false),
		FIELD(hour, false),
		FIELD(mday, true),
		FIELD(mon, true),
		FIELD(year, true),
		FIELD(isdst, false)
	};

	uc_value_t *to = uc_fn_arg(0), *v;
	struct tm tm = { 0 };
	bool exists;
	time_t t;
	size_t i;

	if (ucv_type(to) != UC_OBJECT)
		return NULL;

	for (i = 0; i < ARRAY_SIZE(fields); i++) {
		v = ucv_object_get(to, fields[i].name, &exists);

		if (!exists && fields[i].required)
			return NULL;

		*(int *)((char *)&tm + fields[i].off) = (int)ucv_to_integer(v);
	}

	if (tm.tm_mon > 0)
		tm.tm_mon--;

	if (tm.tm_year >= 1900)
		tm.tm_year -= 1900;

	t = (local ? mktime : timegm)(&tm);

	return (t != (time_t)-1) ? ucv_int64_new((int64_t)t) : NULL;
}

/**
 * Performs the inverse operation of {@link module:core#localtime|localtime()}
 * by taking a broken-down date and time dictionary and transforming it into an
 * epoch value according to the local system timezone.
 *
 * The `wday` and `yday` fields of the given date time specification are
 * ignored. Field values outside of their valid range are internally normalized,
 * e.g. October 40th is interpreted as November 9th.
 *
 * Returns the resulting epoch value or null if the input date time dictionary
 * was invalid or if the date time specification cannot be represented as epoch
 * value.
 *
 * @function module:core#timelocal
 *
 * @param {module:core.TimeSpec} datetimespec
 * The broken-down date and time dictionary.
 *
 * @returns {?number}
 *
 * @example
 * timelocal({ "sec": 42, "min": 51, "hour": 13, "mday": 22, "mon": 3, "year": 2022, "isdst": 0 });
 * // Returns 1647953502
 */
static uc_value_t *
uc_timelocal(uc_vm_t *vm, size_t nargs)
{
	return uc_mktime_common(vm, nargs, true);
}

/**
 * Like `timelocal()` but interpreting the given date time specification as UTC
 * time.
 *
 * See {@link module:core#timelocal|timelocal()} for details.
 *
 * @function module:core#timegm
 *
 * @param {module:core.TimeSpec} datetimespec
 * The broken-down date and time dictionary.
 *
 * @returns {?number}
 *
 * @example
 * timegm({ "sec": 42, "min": 51, "hour": 13, "mday": 22, "mon": 3, "year": 2022, "isdst": 0 });
 * // Returns 1647953502
 */
static uc_value_t *
uc_timegm(uc_vm_t *vm, size_t nargs)
{
	return uc_mktime_common(vm, nargs, false);
}

/**
 * Reads the current second and microsecond value of the system clock.
 *
 * By default, the realtime clock is queried which might skew forwards or
 * backwards due to NTP changes, system sleep modes etc. If a truish value is
 * passed as argument, the monotonic system clock is queried instead, which will
 * return the monotonically increasing time since some arbitrary point in the
 * past (usually the system boot time).
 *
 * Returns a two element array containing the full seconds as the first element
 * and the nanosecond fraction as the second element.
 *
 * Returns `null` if a monotonic clock value is requested and the system does
 * not implement this clock type.
 *
 * @function module:core#clock
 *
 * @param {boolean} [monotonic]
 * Whether to query the monotonic system clock.
 *
 * @returns {?number[]}
 *
 * @example
 * clock();        // [ 1647954926, 798269464 ]
 * clock(true);    // [ 474751, 527959975 ]
 */
static uc_value_t *
uc_clock(uc_vm_t *vm, size_t nargs)
{
	clockid_t id = ucv_is_truish(uc_fn_arg(0)) ? CLOCK_MONOTONIC : CLOCK_REALTIME;
	struct timespec ts;
	uc_value_t *res;

	if (clock_gettime(id, &ts) == -1)
		return NULL;

	res = ucv_array_new(vm);

	ucv_array_set(res, 0, ucv_int64_new((int64_t)ts.tv_sec));
	ucv_array_set(res, 1, ucv_int64_new((int64_t)ts.tv_nsec));

	return res;
}

/**
 * Encodes the given byte string into a hexadecimal digit string, converting
 * the input value to a string if needed.
 *
 * @function module:core#hexenc
 *
 * @param {string} val
 * The byte string to encode.
 *
 * @returns {string}
 *
 * @example
 * hexenc("Hello world!\n");   // "48656c6c6f20776f726c64210a"
 */
static uc_value_t *
uc_hexenc(uc_vm_t *vm, size_t nargs)
{
	const char *hex = "0123456789abcdef";
	uc_value_t *input = uc_fn_arg(0);
	uc_stringbuf_t *buf;
	size_t off, len;
	uint8_t byte;

	if (!input)
		return NULL;

	buf = ucv_stringbuf_new();
	off = printbuf_length(buf);

	ucv_to_stringbuf(vm, buf, input, false);

	len = printbuf_length(buf) - off;

	/* memset the last expected output char to grow the output buffer */
	printbuf_memset(buf, off + len * 2, 0, 1);

	/* translate string into hex back to front to reuse the same buffer */
	while (len > 0) {
		byte = buf->buf[--len + off];
		buf->buf[off + len * 2 + 0] = hex[byte / 16];
		buf->buf[off + len * 2 + 1] = hex[byte % 16];
	}

	/* do not include sentinel `\0` in string length */
	buf->bpos--;

	return ucv_stringbuf_finish(buf);
}

static inline uint8_t
hexval(unsigned char c, bool lo)
{
	return ((c > '9') ? (c - 'a') + 10 : c - '0') << (lo ? 0 : 4);
}

/**
 * Decodes the given hexadecimal digit string into a byte string, optionally
 * skipping specified characters.
 *
 * If the characters to skip are not specified, a default of `" \t\n"` is used.
 *
 * Returns null if the input string contains invalid characters or an uneven
 * amount of hex digits.
 *
 * Returns the decoded byte string on success.
 *
 * @function module:core#hexdec
 *
 * @param {string} hexstring
 * The hexadecimal digit string to decode.
 *
 * @param {string} [skipchars]
 * The characters to skip during decoding.
 *
 * @returns {?string}
 *
 * @example
 * hexdec("48656c6c6f20776f726c64210a");  // "Hello world!\n"
 * hexdec("44:55:66:77:33:44", ":");      // "DUfw3D"
 */
static uc_value_t *
uc_hexdec(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *input = uc_fn_arg(0);
	uc_value_t *skip = uc_fn_arg(1);
	size_t len, off, n, i;
	uc_stringbuf_t *buf;
	unsigned char *p;
	const char *s;

	if (ucv_type(input) != UC_STRING)
		return NULL;

	if (skip && ucv_type(skip) != UC_STRING)
		return NULL;

	p = (unsigned char *)ucv_string_get(input);
	len = ucv_string_length(input);

	s = skip ? (const char *)ucv_string_get(skip) : " \t\n";

	for (i = 0, n = 0; i < len; i++) {
		if (isxdigit(p[i]))
			n++;
		else if (!s || !strchr(s, p[i]))
			return NULL;
	}

	if (n & 1)
		return NULL;

	buf = ucv_stringbuf_new();
	off = printbuf_length(buf);

	/* preallocate the output buffer */
	printbuf_memset(buf, off, 0, n / 2 + 1);

	for (i = 0, n = 0; i < len; i++) {
		if (!isxdigit(p[i]))
			continue;

		buf->buf[off + (n >> 1)] |= hexval(p[i] | 32, n & 1);
		n++;
	}

	/* do not include sentinel `\0` in string length */
	buf->bpos--;

	return ucv_stringbuf_finish(buf);
}

/**
 * Interacts with the mark and sweep garbage collector of the running ucode
 * virtual machine.
 *
 * Depending on the given `operation` string argument, the meaning of `argument`
 * and the function return value differs.
 *
 * The following operations are defined:
 *
 * - `collect` - Perform a complete garbage collection cycle, returns `true`.
 * - `start` - (Re-)start periodic garbage collection, `argument` is an optional
 *             integer in the range `1..65535` specifying the interval.
 *             Defaults to `1000` if omitted. Returns `true` if the periodic GC
 *             was previously stopped and is now started or if the interval
 *             changed. Returns `false` otherwise.
 * - `stop` - Stop periodic garbage collection. Returns `true` if the periodic
 *            GC was previously started and is now stopped, `false` otherwise.
 * - `count` - Count the amount of active complex object references in the VM
 *             context, returns the counted amount.
 *
 * If the `operation` argument is omitted, the default is `collect`.
 *
 * @function module:core#gc
 *
 * @param {string} [operation]
 * The operation to perform.
 *
 * @param {*} [argument]
 * The argument for the operation.
 *
 * @returns {?(boolean|number)}
 *
 * @example
 * gc();         // true
 * gc("start");  // true
 * gc("count");  // 42
 */
static uc_value_t *
uc_gc(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *operation = uc_fn_arg(0);
	uc_value_t *argument = uc_fn_arg(1);
	const char *op = NULL;
	uc_weakref_t *ref;
	int64_t n;

	if (operation != NULL && ucv_type(operation) != UC_STRING)
		return NULL;

	op = ucv_string_get(operation);

	if (!op || !strcmp(op, "collect")) {
		ucv_gc(vm);

		return ucv_boolean_new(true);
	}
	else if (!strcmp(op, "start")) {
		n = argument ? ucv_int64_get(argument) : 0;

		if (errno || n < 0 || n > 0xFFFF)
			return NULL;

		if (n == 0)
			n = GC_DEFAULT_INTERVAL;

		return ucv_boolean_new(uc_vm_gc_start(vm, n));
	}
	else if (!strcmp(op, "stop")) {
		return ucv_boolean_new(uc_vm_gc_stop(vm));
	}
	else if (!strcmp(op, "count")) {
		for (n = 0, ref = vm->values.next; ref != &vm->values; ref = ref->next)
			n++;

		return ucv_uint64_new(n);
	}

	return NULL;
}

/**
 * A parse configuration is a plain object describing options to use when
 * compiling ucode at runtime. It is expected as parameter by the
 * {@link module:core#loadfile|loadfile()} and
 * {@link module:core#loadstring|loadstring()} functions.
 *
 * All members of the parse configuration object are optional and will default
 * to the state of the running ucode file if omitted.
 *
 * @typedef {Object} module:core.ParseConfig
 *
 * @property {boolean} lstrip_blocks
 * Whether to strip whitespace preceding template directives.
 * See {@link tutorial-02-syntax.html#whitespace-handling|Whitespace handling}.
 *
 * @property {boolean} trim_blocks
 * Whether to trim trailing newlines following template directives.
 * See {@link tutorial-02-syntax.html#whitespace-handling|Whitespace handling}.
 *
 * @property {boolean} strict_declarations
 * Whether to compile the code in strict mode (`true`) or not (`false`).
 *
 * @property {boolean} raw_mode
 * Whether to compile the code in plain script mode (`true`) or not (`false`).
 *
 * @property {string[]} module_search_path
 * Override the module search path for compile time imports while compiling the
 * ucode source.
 *
 * @property {string[]} force_dynlink_list
 * List of module names assumed to be dynamic library extensions, allows
 * compiling ucode source with import statements referring to `*.so` extensions
 * not present at compile time.
 */
static void
uc_compile_parse_config(uc_parse_config_t *config, uc_value_t *spec)
{
	uc_value_t *v, *p;
	size_t i, j;
	bool found;

	struct {
		const char *key;
		bool *flag;
		uc_search_path_t *path;
	} fields[] = {
		{ "lstrip_blocks",       &config->lstrip_blocks,       NULL },
		{ "trim_blocks",         &config->trim_blocks,         NULL },
		{ "strict_declarations", &config->strict_declarations, NULL },
		{ "raw_mode",            &config->raw_mode,            NULL },
		{ "module_search_path",  NULL, &config->module_search_path  },
		{ "force_dynlink_list",  NULL, &config->force_dynlink_list  }
	};

	for (i = 0; i < ARRAY_SIZE(fields); i++) {
		v = ucv_object_get(spec, fields[i].key, &found);

		if (!found)
			continue;

		if (fields[i].flag) {
			*fields[i].flag = ucv_is_truish(v);
		}
		else if (fields[i].path) {
			fields[i].path->count = 0;
			fields[i].path->entries = NULL;

			for (j = 0; j < ucv_array_length(v); j++) {
				p = ucv_array_get(v, j);

				if (ucv_type(p) != UC_STRING)
					continue;

				uc_vector_push(fields[i].path, ucv_string_get(p));
			}
		}
	}
}

static uc_value_t *
uc_load_common(uc_vm_t *vm, size_t nargs, uc_source_t *source)
{
	uc_parse_config_t conf = *vm->config;
	uc_program_t *program;
	uc_value_t *closure;
	char *err = NULL;

	uc_compile_parse_config(&conf, uc_fn_arg(1));

	program = uc_compile(&conf, source, &err);
	closure = program ? ucv_closure_new(vm, uc_program_entry(program), false) : NULL;

	uc_program_put(program);

	if (!vm->config || conf.module_search_path.entries != vm->config->module_search_path.entries)
		uc_vector_clear(&conf.module_search_path);

	if (!vm->config || conf.force_dynlink_list.entries != vm->config->force_dynlink_list.entries)
		uc_vector_clear(&conf.force_dynlink_list);

	if (!closure) {
		uc_error_message_indent(&err);

		if (source->buffer)
			uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
				"Unable to compile source string:\n\n%s", err);
		else
			uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
				"Unable to compile source file '%s':\n\n%s", source->filename, err);
	}

	uc_source_put(source);
	free(err);

	return closure;
}

/**
 * Compiles the given code string into a ucode program and returns the resulting
 * program entry function.
 *
 * The optional `options` dictionary overrides parse and compile options.
 *
 *  - If a non-string `code` argument is given, it is implicitly converted to a
 *    string value first.
 *  - If `options` is omitted or a non-object value, the compile options of the
 *    running ucode program are reused.
 *
 * See {@link module:core.ParseConfig|ParseConfig} for known keys within the
 * `options` object. Unrecognized keys are ignored, unspecified options default
 * to those of the running program.
 *
 * Returns the compiled program entry function.
 *
 * Throws an exception on compilation errors.
 *
 * @function module:core#loadstring
 *
 * @param {string} code
 * The code string to compile.
 *
 * @param {module:core.ParseConfig} [options]
 * The options for compilation.
 *
 * @returns {Function}
 *
 * @example
 * let fn1 = loadstring("Hello, {{ name }}", { raw_mode: false });
 *
 * global.name = "Alice";
 * fn1(); // prints `Hello, Alice`
 *
 *
 * let fn2 = loadstring("return 1 + 2;", { raw_mode: true });
 * fn2(); // 3
 */
static uc_value_t *
uc_loadstring(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *code = uc_fn_arg(0);
	uc_source_t *source;
	size_t len;
	char *s;

	if (ucv_type(code) == UC_STRING) {
		len = ucv_string_length(code);
		s = xalloc(len);
		memcpy(s, ucv_string_get(code), len);
	}
	else {
		s = ucv_to_string(vm, code);
		len = strlen(s);
	}

	source = uc_source_new_buffer("[loadstring argument]", s, len);

	if (!source) {
		uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
			"Unable to allocate source buffer: %s",
			strerror(errno));

		return NULL;
	}

	return uc_load_common(vm, nargs, source);
}

/**
 * Compiles the given file into a ucode program and returns the resulting
 * program entry function.
 *
 * See {@link module:core#loadstring|`loadstring()`} for details.
 *
 * Returns the compiled program entry function.
 *
 * Throws an exception on compilation or file I/O errors.
 *
 * @function module:core#loadfile
 *
 * @param {string} path
 * The path of the file to compile.
 *
 * @param {module:core.ParseConfig} [options]
 * The options for compilation.
 *
 * @returns {Function}
 *
 * @example
 * loadfile("./templates/example.uc");  // function main() { ... }
 */
static uc_value_t *
uc_loadfile(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *path = uc_fn_arg(0);
	uc_source_t *source;

	if (ucv_type(path) != UC_STRING)
		return NULL;

	source = uc_source_new_file(ucv_string_get(path));

	if (!source) {
		uc_vm_raise_exception(vm, EXCEPTION_RUNTIME,
			"Unable to open source file %s: %s",
			ucv_string_get(path), strerror(errno));

		return NULL;
	}

	return uc_load_common(vm, nargs, source);
}

/**
 * Calls the given function value with a modified environment.
 *
 * The given `ctx` argument is used as `this` context for the invoked function
 * and the given `scope` value as global environment. Any further arguments are
 * passed to the invoked function as-is.
 *
 * When `ctx` is omitted or `null`, the function will get invoked with `this`
 * being `null`.
 *
 * When `scope` is omitted or `null`, the function will get executed with the
 * current global environment of the running program. When `scope` is set to a
 * dictionary, the dictionary is used as global function environment.
 *
 * When the `scope` dictionary has no prototype, the current global environment
 * will be set as prototype, means the scope will inherit from it.
 *
 * When a scope prototype is set, it is kept. This allows passing an isolated
 * (sandboxed) function scope without access to the global environment.
 *
 * Any further argument is forwarded as-is to the invoked function as function
 * call argument.
 *
 * Returns `null` if the given function value `fn` is not callable.
 *
 * Returns the return value of the invoked function in all other cases.
 *
 * Forwards exceptions thrown by the invoked function.
 *
 * @function module:core#call
 *
 * @param {Function} fn
 * Function value to call.
 *
 * @param {*} [ctx=null]
 * `this` context for the invoked function.
 *
 * @param {Object} [scope=null]
 * Global environment for the invoked function.
 *
 * @param {...*} [arg]
 * Additional arguments to pass to the invoked function.
 *
 * @returns {*}
 *
 * @example
 * // Override this context
 * call(function() { printf("%J\n", this) });            // null
 * call(function() { printf("%J\n", this) }, null);      // null
 * call(function() { printf("%J\n", this) }, { x: 1 });  // { "x": 1 }
 * call(function() { printf("%J\n", this) }, { x: 2 });  // { "x": 2 }
 *
 * // Run with default scope
 * global.a = 1;
 * call(function() { printf("%J\n", a) });                  // 1
 *
 * // Override scope, inherit from current global scope (implicit)
 * call(function() { printf("%J\n", a) }, null, { a: 2 });  // 2
 *
 * // Override scope, inherit from current global scope (explicit)
 * call(function() { printf("%J\n", a) }, null,
 *         proto({ a: 2 }, global));                        // 2
 *
 * // Override scope, don't inherit (pass `printf()` but not `a`)
 * call(function() { printf("%J\n", a) }, null,
 *         proto({}, { printf }));                          // null
 *
 * // Forward arguments
 * x = call((x, y, z) => x * y * z, null, null, 2, 3, 4);   // x = 24
 */
static uc_value_t *
uc_callfunc(uc_vm_t *vm, size_t nargs)
{
	size_t argoff = vm->stack.count - nargs, i;
	uc_value_t *fn_scope, *prev_scope, *res;
	uc_value_t *fn = uc_fn_arg(0);
	uc_value_t *this = uc_fn_arg(1);
	uc_value_t *scope = uc_fn_arg(2);

	if (!ucv_is_callable(fn))
		return NULL;

	if (scope && ucv_type(scope) != UC_OBJECT)
		return NULL;

	if (ucv_prototype_get(scope)) {
		fn_scope = ucv_get(scope);
	}
	else if (scope) {
		fn_scope = ucv_object_new(vm);

		ucv_object_foreach(scope, k, v)
			ucv_object_add(fn_scope, k, ucv_get(v));

		ucv_prototype_set(fn_scope, ucv_get(uc_vm_scope_get(vm)));
	}
	else {
		fn_scope = NULL;
	}

	uc_vm_stack_push(vm, ucv_get(this));
	uc_vm_stack_push(vm, ucv_get(fn));

	for (i = 3; i < nargs; i++)
		uc_vm_stack_push(vm, ucv_get(vm->stack.entries[3 + argoff++]));

	if (fn_scope) {
		prev_scope = ucv_get(uc_vm_scope_get(vm));
		uc_vm_scope_set(vm, fn_scope);
	}

	if (uc_vm_call(vm, true, i - 3) == EXCEPTION_NONE)
		res = uc_vm_stack_pop(vm);
	else
		res = NULL;

	if (fn_scope)
		uc_vm_scope_set(vm, prev_scope);

	return res;
}

/**
 * Set or query process signal handler function.
 *
 * When invoked with two arguments, a signal specification and a signal handler
 * value, this function configures a new process signal handler.
 *
 * When invoked with one argument, a signal specification, this function returns
 * the currently configured handler for the given signal.
 *
 * The signal specification might either be an integer signal number or a string
 * value containing a signal name (with or without "SIG" prefix). Signal names
 * are treated case-insensitively.
 *
 * The signal handler might be either a callable function value or one of the
 * two special string values `"ignore"` and `"default"`. Passing `"ignore"` will
 * mask the given process signal while `"default"` will restore the operating
 * systems default behaviour for the given signal.
 *
 * In case a callable handler function is provided, it is invoked at the
 * earliest  opportunity after receiving the corresponding signal from the
 * operating system. The invoked function will receive a single argument, the
 * number of the signal it is invoked for.
 *
 * Note that within the ucode VM, process signals are not immediately delivered,
 * instead the VM keeps track of received signals and delivers them to the ucode
 * script environment at the next opportunity, usually before executing the next
 * byte code instruction. This means that if a signal is received while
 * performing a computationally expensive operation in C mode, such as a complex
 * regexp match, the corresponding ucode signal handler will only be invoked
 * after that operation concluded and control flow returns to the VM.
 *
 * Returns the signal handler function or one of the special values `"ignore"`
 * or `"default"` corresponding to the given signal specification.
 *
 * Returns `null` if an invalid signal spec or signal handler was provided.
 *
 * Returns `null` if changing the signal action failed, e.g. due to insufficient
 * permission, or when attempting to ignore a non-ignorable signal.
 *
 * @function module:core#signal
 *
 * @param {number|string} signal
 * The signal to query/set handler for.
 *
 * @param {Function|string} [handler]
 * The signal handler to install for the given signal.
 *
 * @returns {Function|string}
 *
 * @example
 * // Ignore signals
 * signal('INT', 'ignore');      // "ignore"
 * signal('SIGINT', 'ignore');   // "ignore" (equivalent to 'INT')
 * signal('sigterm', 'ignore');  // "ignore" (signal names are case insensitive)
 * signal(9, 'ignore');          // null (SIGKILL cannot be ignored)
 *
 * // Restore signal default behavior
 * signal('INT', 'default');     // "default"
 * signal('foobar', 'default');  // null (unknown signal name)
 * signal(-313, 'default');      // null (invalid signal number)
 *
 * // Set custom handler function
 * function intexit(signo) {
 *   printf("I received signal number %d\n", signo);
 *   exit(1);
 * }
 *
 * signal('SIGINT', intexit);    // returns intexit
 * signal('SIGINT') == intexit;  // true
 */
static uc_value_t *
uc_signal(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *signame = uc_fn_arg(0);
	uc_value_t *sighandler = uc_fn_arg(1);
	struct sigaction sa = { 0 };
	char *sigstr;
	int sig;

	if (ucv_type(signame) == UC_INTEGER) {
		sig = (int)ucv_int64_get(signame);

		if (errno || sig < 0 || sig >= UC_SYSTEM_SIGNAL_COUNT)
			return NULL;

		if (!uc_system_signal_names[sig])
			return NULL;
	}
	else if (ucv_type(signame) == UC_STRING) {
		sigstr = ucv_string_get(signame);

		if (!strncasecmp(sigstr, "SIG", 3))
			sigstr += 3;

		for (sig = 0; sig < UC_SYSTEM_SIGNAL_COUNT; sig++)
			if (uc_system_signal_names[sig] &&
			    !strcasecmp(uc_system_signal_names[sig], sigstr))
				break;

		if (sig == UC_SYSTEM_SIGNAL_COUNT)
			return NULL;
	}
	else {
		return NULL;
	}

	/* Query current signal handler state */
	if (nargs < 2) {
		if (sigaction(sig, NULL, &sa) != 0)
			return NULL;

		if (sa.sa_handler == SIG_IGN)
			return ucv_string_new("ignore");

		if (sa.sa_handler == SIG_DFL)
			return ucv_string_new("default");

		return ucv_get(ucv_array_get(vm->signal.handler, sig));
	}

	/* Install new signal handler */
	if (ucv_type(sighandler) == UC_STRING) {
		sigstr = ucv_string_get(sighandler);

		sa.sa_flags = SA_ONSTACK | SA_RESTART;
		sigemptyset(&sa.sa_mask);

		if (!strcmp(sigstr, "ignore"))
			sa.sa_handler = SIG_IGN;
		else if (!strcmp(sigstr, "default"))
			sa.sa_handler = SIG_DFL;
		else
			return NULL;

		if (sigaction(sig, &sa, NULL) != 0)
			return NULL;

		ucv_array_set(vm->signal.handler, sig, NULL);
	}
	else if (ucv_is_callable(sighandler)) {
		if (sigaction(sig, &vm->signal.sa, NULL) != 0)
			return NULL;

		ucv_array_set(vm->signal.handler, sig, ucv_get(sighandler));
	}
	else {
		return NULL;
	}

	return ucv_get(sighandler);
}


const uc_function_list_t uc_stdlib_functions[] = {
	{ "chr",		uc_chr },
	{ "die",		uc_die },
	{ "exists",		uc_exists },
	{ "exit",		uc_exit },
	{ "filter",		uc_filter },
	{ "getenv",		uc_getenv },
	{ "hex",		uc_hex },
	{ "index",		uc_lindex },
	{ "int",		uc_int },
	{ "join",		uc_join },
	{ "keys",		uc_keys },
	{ "lc",			uc_lc },
	{ "length",		uc_length },
	{ "ltrim",		uc_ltrim },
	{ "map",		uc_map },
	{ "ord",		uc_ord },
	{ "pop",		uc_pop },
	{ "print",		uc_print },
	{ "push",		uc_push },
	{ "reverse",	uc_reverse },
	{ "rindex",		uc_rindex },
	{ "rtrim",		uc_rtrim },
	{ "shift",		uc_shift },
	{ "sort",		uc_sort },
	{ "splice",		uc_splice },
	{ "slice",		uc_slice },
	{ "split",		uc_split },
	{ "substr",		uc_substr },
	{ "time",		uc_time },
	{ "trim",		uc_trim },
	{ "type",		uc_type },
	{ "uchr",		uc_uchr },
	{ "uc",			uc_uc },
	{ "unshift",	uc_unshift },
	{ "values",		uc_values },
	{ "sprintf",	uc_sprintf },
	{ "printf",		uc_printf },
	{ "require",	uc_require },
	{ "iptoarr",	uc_iptoarr },
	{ "arrtoip",	uc_arrtoip },
	{ "match",		uc_match },
	{ "replace",	uc_replace },
	{ "json",		uc_json },
	{ "include",	uc_include },
	{ "warn",		uc_warn },
	{ "system",		uc_system },
	{ "trace",		uc_trace },
	{ "proto",		uc_proto },
	{ "sleep",		uc_sleep },
	{ "assert",		uc_assert },
	{ "render",		uc_render },
	{ "regexp",		uc_regexp },
	{ "wildcard",	uc_wildcard },
	{ "sourcepath",	uc_sourcepath },
	{ "min",		uc_min },
	{ "max",		uc_max },
	{ "b64dec",		uc_b64dec },
	{ "b64enc",		uc_b64enc },
	{ "uniq",		uc_uniq },
	{ "localtime",	uc_localtime },
	{ "gmtime",		uc_gmtime },
	{ "timelocal",	uc_timelocal },
	{ "timegm",		uc_timegm },
	{ "clock",		uc_clock },
	{ "hexdec",		uc_hexdec },
	{ "hexenc",		uc_hexenc },
	{ "gc",			uc_gc },
	{ "loadstring",	uc_loadstring },
	{ "loadfile",	uc_loadfile },
	{ "call",		uc_callfunc },
	{ "signal",		uc_signal },
};


void
uc_stdlib_load(uc_value_t *scope)
{
	uc_function_list_register(scope, uc_stdlib_functions);
}

uc_cfn_ptr_t
uc_stdlib_function(const char *name)
{
	size_t i;

	for (i = 0; i < ARRAY_SIZE(uc_stdlib_functions); i++)
		if (!strcmp(uc_stdlib_functions[i].name, name))
			return uc_stdlib_functions[i].func;

	return NULL;
}