/**
 * @file runtime_functions.c
 * @brief The built-in function implementations.
 *
 * Ported from basicruntime_functions.go. The reference bootstraps its function
 * table by running a BASIC program of DEF statements through the interpreter at
 * startup and then nulling out the expressions so the native handlers take over
 * -- except MOD, SPC and STR, which stay as BASIC expressions. None of that is
 * reproduced: the signatures are data in the dispatch table and all three of
 * those are ordinary native handlers here, which removes the need to run the
 * interpreter before the interpreter is ready.
 */

#include <inttypes.h>
#include <math.h>
#include <stdio.h>

#include <akerror.h>
#include <akstdlib.h>

#include <akbasic/error.h>
#include <akbasic/runtime.h>

#include "verbs.h"

/*
 * Every builtin starts the same way: find the argument list, evaluate the first
 * argument, and hand back a fresh value to write the answer into. Arity is
 * already guaranteed by the parser against the table's arity column.
 */
static akerr_ErrorContext *first_arg(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *fname, akbasic_ASTLeaf **argleaf, akbasic_Value **argvalue, akbasic_Value **out)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *arg = NULL;

    FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NIL leaf");
    arg = akbasic_leaf_first_argument(expr);
    FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
		     "%s expected an argument", fname);
    if ( argvalue != NULL ) {
	PASS(errctx, akbasic_runtime_evaluate(obj, arg, argvalue));
    }
    if ( out != NULL ) {
	PASS(errctx, akbasic_environment_new_value(obj->environment, out));
	PASS(errctx, akbasic_value_zero(*out));
    }
    if ( argleaf != NULL ) {
	*argleaf = arg;
    }
    SUCCEED_RETURN(errctx);
}

/* Coerce an integer-or-float argument to double, the shape every trig call wants. */
static akerr_ErrorContext *arg_as_double(akbasic_Value *value, const char *fname, double *dest)
{
    PREPARE_ERROR(errctx);

    if ( value->valuetype == AKBASIC_TYPE_INTEGER ) {
	*dest = (double)value->intval;
	SUCCEED_RETURN(errctx);
    }
    if ( value->valuetype == AKBASIC_TYPE_FLOAT ) {
	*dest = value->floatval;
	SUCCEED_RETURN(errctx);
    }
    FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "%s expected integer or float", fname);
}

/* The trig family differs only by the libm call, so it is one shape. */
#define DEFINE_MATH_FUNCTION(__cname, __basicname, __call)			\
    akerr_ErrorContext *__cname(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,	\
				akbasic_Value *lval, akbasic_Value *rval,	\
				akbasic_Value **dest)				\
    {										\
	PREPARE_ERROR(errctx);							\
	akbasic_Value *arg = NULL;						\
	akbasic_Value *out = NULL;						\
	double input = 0.0;							\
										\
	(void)lval; (void)rval;							\
	PASS(errctx, first_arg(obj, expr, __basicname, NULL, &arg, &out));	\
	PASS(errctx, arg_as_double(arg, __basicname, &input));			\
	out->valuetype = AKBASIC_TYPE_FLOAT;					\
	out->floatval = __call(input);						\
	*dest = out;								\
	SUCCEED_RETURN(errctx);							\
    }

DEFINE_MATH_FUNCTION(akbasic_fn_atn, "ATN", atan)
DEFINE_MATH_FUNCTION(akbasic_fn_cos, "COS", cos)
DEFINE_MATH_FUNCTION(akbasic_fn_sin, "SIN", sin)
DEFINE_MATH_FUNCTION(akbasic_fn_tan, "TAN", tan)
DEFINE_MATH_FUNCTION(akbasic_fn_log, "LOG", log)

akerr_ErrorContext *akbasic_fn_abs(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "ABS", NULL, &arg, &out));
    if ( arg->valuetype == AKBASIC_TYPE_INTEGER ) {
	out->valuetype = AKBASIC_TYPE_INTEGER;
	out->intval = (arg->intval < 0 ? -arg->intval : arg->intval);
    } else if ( arg->valuetype == AKBASIC_TYPE_FLOAT ) {
	out->valuetype = AKBASIC_TYPE_FLOAT;
	out->floatval = fabs(arg->floatval);
    } else {
	FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "ABS expected integer or float");
    }
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_rad(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;
    double input = 0.0;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "RAD", NULL, &arg, &out));
    PASS(errctx, arg_as_double(arg, "RAD", &input));
    out->valuetype = AKBASIC_TYPE_FLOAT;
    out->floatval = input * (M_PI / 180.0);
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_sgn(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;
    double input = 0.0;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "SGN", NULL, &arg, &out));
    PASS(errctx, arg_as_double(arg, "SGN", &input));
    out->valuetype = AKBASIC_TYPE_INTEGER;
    out->intval = (input < 0.0 ? -1 : (input > 0.0 ? 1 : 0));
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_chr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;
    int64_t codepoint = 0;
    int written = 0;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "CHR", NULL, &arg, &out));
    FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
			"CHR expected an integer codepoint");
    codepoint = arg->intval;
    FAIL_ZERO_RETURN(errctx, (codepoint >= 0 && codepoint <= 0x10FFFF), AKBASIC_ERR_VALUE,
		     "CHR codepoint %" PRId64 " is outside Unicode", codepoint);

    /* UTF-8 encode, matching the reference's string(rune(x)). */
    out->valuetype = AKBASIC_TYPE_STRING;
    if ( codepoint < 0x80 ) {
	out->stringval[written++] = (char)codepoint;
    } else if ( codepoint < 0x800 ) {
	out->stringval[written++] = (char)(0xC0 | (codepoint >> 6));
	out->stringval[written++] = (char)(0x80 | (codepoint & 0x3F));
    } else if ( codepoint < 0x10000 ) {
	out->stringval[written++] = (char)(0xE0 | (codepoint >> 12));
	out->stringval[written++] = (char)(0x80 | ((codepoint >> 6) & 0x3F));
	out->stringval[written++] = (char)(0x80 | (codepoint & 0x3F));
    } else {
	out->stringval[written++] = (char)(0xF0 | (codepoint >> 18));
	out->stringval[written++] = (char)(0x80 | ((codepoint >> 12) & 0x3F));
	out->stringval[written++] = (char)(0x80 | ((codepoint >> 6) & 0x3F));
	out->stringval[written++] = (char)(0x80 | (codepoint & 0x3F));
    }
    out->stringval[written] = '\0';
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_asc(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;
    const unsigned char *text = NULL;
    int64_t codepoint = 0;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "ASC", NULL, &arg, &out));
    FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"ASC expected a string");
    FAIL_ZERO_RETURN(errctx, (arg->stringval[0] != '\0'), AKBASIC_ERR_BOUNDS,
		     "ASC expected a non-empty string");

    /* Decode the first UTF-8 code point, the inverse of CHR's encoder. */
    text = (const unsigned char *)arg->stringval;
    if ( text[0] < 0x80 ) {
	codepoint = text[0];
    } else if ( (text[0] & 0xE0) == 0xC0 ) {
	codepoint = ((int64_t)(text[0] & 0x1F) << 6) |
		    (text[1] & 0x3F);
    } else if ( (text[0] & 0xF0) == 0xE0 ) {
	codepoint = ((int64_t)(text[0] & 0x0F) << 12) |
		    ((int64_t)(text[1] & 0x3F) << 6) |
		    (text[2] & 0x3F);
    } else {
	codepoint = ((int64_t)(text[0] & 0x07) << 18) |
		    ((int64_t)(text[1] & 0x3F) << 12) |
		    ((int64_t)(text[2] & 0x3F) << 6) |
		    (text[3] & 0x3F);
    }
    out->valuetype = AKBASIC_TYPE_INTEGER;
    out->intval = codepoint;
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_rnd(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;
    const int64_t modulus = 2147483648;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "RND", NULL, &arg, &out));
    FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
			"RND expected an integer");
    FAIL_ZERO_RETURN(errctx, (arg->intval > 0), AKBASIC_ERR_VALUE,
		     "RND count %" PRId64 " must be positive", arg->intval);

    if ( !obj->rndseeded ) {
	obj->rndseed = obj->timems % modulus;
	obj->rndseeded = true;
    }
    obj->rndseed = (obj->rndseed * 1103515245 + 12345) % modulus;
    out->valuetype = AKBASIC_TYPE_INTEGER;
    out->intval = (obj->rndseed / 65536) % arg->intval;
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_hex(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;
    int written = 0;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "HEX", NULL, &arg, &out));
    FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
			"HEX expected an integer");
    out->valuetype = AKBASIC_TYPE_STRING;
    /*
     * aksl_snprintf refuses to truncate, which costs nothing here: sixteen hex
     * digits is the longest an int64_t can be and a string value holds far more.
     */
    PASS(errctx, aksl_snprintf(&written, out->stringval, sizeof(out->stringval),
			       "%" PRIx64, arg->intval));
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_str(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "STR", NULL, &arg, &out));
    /*
     * The reference defines this as `"" + X#`, so it renders exactly the way
     * string concatenation does -- an integer as %d and a float as %f.
     */
    PASS(errctx, akbasic_value_to_string(arg, out->stringval, sizeof(out->stringval)));
    out->valuetype = AKBASIC_TYPE_STRING;
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_spc(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;
    int64_t i = 0;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "SPC", NULL, &arg, &out));
    FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
			"SPC expected an integer");
    FAIL_ZERO_RETURN(errctx, (arg->intval >= 0 && arg->intval < AKBASIC_MAX_STRING_LENGTH),
		     AKBASIC_ERR_VALUE,
		     "SPC count %" PRId64 " is outside 0..%d",
		     arg->intval, AKBASIC_MAX_STRING_LENGTH - 1);
    out->valuetype = AKBASIC_TYPE_STRING;
    for ( i = 0; i < arg->intval; i++ ) {
	out->stringval[i] = ' ';
    }
    out->stringval[arg->intval] = '\0';
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_val(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "VAL", NULL, &arg, &out));
    FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"VAL expected a string");
    out->valuetype = AKBASIC_TYPE_FLOAT;
    /*
     * Through the strict converter, never aksl_atof: that family cannot report a
     * failure, so VAL("garbage") would quietly answer 0.0 instead of raising.
     * See TODO.md section 1.9.
     */
    PASS(errctx, aksl_atof(arg->stringval, &out->floatval));
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_len(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *argleaf = NULL;
    akbasic_Value *strval = NULL;
    akbasic_Value *out = NULL;
    akbasic_Variable *variable = NULL;
    size_t length = 0;

    (void)lval; (void)rval;
    /*
     * LEN must not evaluate a non-string argument. `LEN(A#)` on a
     * multi-dimensional array is legal and asks for the element count, but
     * evaluating a bare identifier supplies the single subscript {0}, which a
     * two-dimensional variable rightly rejects. So: inspect the leaf, and only
     * evaluate when it is a string.
     */
    PASS(errctx, first_arg(obj, expr, "LEN", &argleaf, NULL, NULL));
    FAIL_ZERO_RETURN(errctx,
		     (akbasic_leaf_is_identifier(argleaf) || akbasic_leaf_is_literal(argleaf)),
		     AKBASIC_ERR_SYNTAX, "Expected identifier or string literal");

    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_INTEGER;

    if ( argleaf->leaftype == AKBASIC_LEAF_LITERAL_STRING ||
	 argleaf->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ) {
	PASS(errctx, akbasic_runtime_evaluate(obj, argleaf, &strval));
	PASS(errctx, aksl_strlen(strval->stringval, &length));
	out->intval = (int64_t)length;
    } else {
	PASS(errctx, akbasic_environment_get(obj->environment, argleaf->identifier, &variable));
	FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
			 "Identifier %s is undefined", argleaf->identifier);
	out->intval = variable->valuecount;
    }
    *dest = out;
    SUCCEED_RETURN(errctx);
}

/* Fetch argument N (zero-based) of a call, already evaluated. */
static akerr_ErrorContext *nth_arg(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *fname, int n, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *arg = NULL;
    int i = 0;

    FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NIL leaf");
    arg = akbasic_leaf_first_argument(expr);
    for ( i = 0; i < n && arg != NULL; i++ ) {
	arg = arg->next;
    }
    FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
		     "%s is missing argument %d", fname, n + 1);
    PASS(errctx, akbasic_runtime_evaluate(obj, arg, dest));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_instr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *haystack = NULL;
    akbasic_Value *needle = NULL;
    akbasic_Value *out = NULL;
    char *hit = NULL;

    (void)lval; (void)rval;
    PASS(errctx, nth_arg(obj, expr, "INSTR", 0, &haystack));
    PASS(errctx, nth_arg(obj, expr, "INSTR", 1, &needle));
    FAIL_ZERO_RETURN(errctx,
		     (haystack->valuetype == AKBASIC_TYPE_STRING &&
		      needle->valuetype == AKBASIC_TYPE_STRING),
		     AKBASIC_ERR_TYPE, "INSTR expected two strings");
    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_INTEGER;
    /* Not finding it is a successful NULL, which is exactly the -1 answer below. */
    PASS(errctx, aksl_strstr(haystack->stringval, needle->stringval, &hit));
    out->intval = (hit == NULL ? -1 : (int64_t)(hit - haystack->stringval));
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_left(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *source = NULL;
    akbasic_Value *count = NULL;
    akbasic_Value *out = NULL;
    int64_t take = 0;
    size_t sourcelen = 0;

    (void)lval; (void)rval;
    PASS(errctx, nth_arg(obj, expr, "LEFT", 0, &source));
    PASS(errctx, nth_arg(obj, expr, "LEFT", 1, &count));
    FAIL_NONZERO_RETURN(errctx, (source->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"LEFT expected a string");
    FAIL_NONZERO_RETURN(errctx, (count->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
			"LEFT expected an integer count");
    PASS(errctx, aksl_strlen(source->stringval, &sourcelen));
    take = count->intval;
    if ( take < 0 ) {
	take = 0;
    }
    if ( (size_t)take > sourcelen ) {
	take = (int64_t)sourcelen;	/* clamped to LEN, as the README says */
    }
    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_STRING;
    PASS(errctx, aksl_memcpy(out->stringval, source->stringval, (size_t)take));
    out->stringval[take] = '\0';
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_right(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *source = NULL;
    akbasic_Value *count = NULL;
    akbasic_Value *out = NULL;
    int64_t take = 0;
    size_t sourcelen = 0;

    (void)lval; (void)rval;
    PASS(errctx, nth_arg(obj, expr, "RIGHT", 0, &source));
    PASS(errctx, nth_arg(obj, expr, "RIGHT", 1, &count));
    FAIL_NONZERO_RETURN(errctx, (source->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"RIGHT expected a string");
    FAIL_NONZERO_RETURN(errctx, (count->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
			"RIGHT expected an integer count");
    PASS(errctx, aksl_strlen(source->stringval, &sourcelen));
    take = count->intval;
    if ( take < 0 ) {
	take = 0;
    }
    if ( (size_t)take > sourcelen ) {
	take = (int64_t)sourcelen;
    }
    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_STRING;
    PASS(errctx, aksl_memcpy(out->stringval, source->stringval + (sourcelen - (size_t)take),
			     (size_t)take));
    out->stringval[take] = '\0';
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_mid(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *source = NULL;
    akbasic_Value *startval = NULL;
    akbasic_Value *lengthval = NULL;
    akbasic_Value *out = NULL;
    int64_t start = 0;
    int64_t length = 0;
    size_t sourcelen = 0;

    (void)lval; (void)rval;
    PASS(errctx, nth_arg(obj, expr, "MID", 0, &source));
    PASS(errctx, nth_arg(obj, expr, "MID", 1, &startval));
    PASS(errctx, nth_arg(obj, expr, "MID", 2, &lengthval));
    FAIL_NONZERO_RETURN(errctx, (source->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"MID expected a string");
    FAIL_ZERO_RETURN(errctx,
		     (startval->valuetype == AKBASIC_TYPE_INTEGER &&
		      lengthval->valuetype == AKBASIC_TYPE_INTEGER),
		     AKBASIC_ERR_TYPE, "MID expected integer start and length");

    PASS(errctx, aksl_strlen(source->stringval, &sourcelen));
    start = startval->intval;
    length = lengthval->intval;
    FAIL_ZERO_RETURN(errctx, (start >= 0 && (size_t)start <= sourcelen), AKBASIC_ERR_BOUNDS,
		     "MID start %" PRId64 " is outside 0..%zu", start, sourcelen);
    if ( length < 0 ) {
	length = 0;
    }
    if ( (size_t)(start + length) > sourcelen ) {
	length = (int64_t)sourcelen - start;
    }
    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_STRING;
    PASS(errctx, aksl_memcpy(out->stringval, source->stringval + start, (size_t)length));
    out->stringval[length] = '\0';
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_mod(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *left = NULL;
    akbasic_Value *right = NULL;
    akbasic_Value *out = NULL;

    (void)lval; (void)rval;
    PASS(errctx, nth_arg(obj, expr, "MOD", 0, &left));
    PASS(errctx, nth_arg(obj, expr, "MOD", 1, &right));
    FAIL_ZERO_RETURN(errctx,
		     (left->valuetype == AKBASIC_TYPE_INTEGER &&
		      right->valuetype == AKBASIC_TYPE_INTEGER),
		     AKBASIC_ERR_TYPE, "MOD expected two integers");
    FAIL_ZERO_RETURN(errctx, (right->intval != 0), AKBASIC_ERR_VALUE, "DIVISION BY ZERO");
    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_INTEGER;
    /*
     * The reference defines MOD as `X% - (Y% * (X% / Y%))` in BASIC, which with
     * truncating integer division is exactly C's %.
     */
    out->intval = left->intval - (right->intval * (left->intval / right->intval));
    *dest = out;
    SUCCEED_RETURN(errctx);
}

/* SHL, SHR and XOR share a shape: two integers in, one integer out. */
static akerr_ErrorContext *two_integers(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *fname, int64_t *a, int64_t *b, akbasic_Value **out)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *left = NULL;
    akbasic_Value *right = NULL;

    PASS(errctx, nth_arg(obj, expr, fname, 0, &left));
    PASS(errctx, nth_arg(obj, expr, fname, 1, &right));
    FAIL_ZERO_RETURN(errctx,
		     (left->valuetype == AKBASIC_TYPE_INTEGER &&
		      right->valuetype == AKBASIC_TYPE_INTEGER),
		     AKBASIC_ERR_TYPE, "%s expected two integers", fname);
    *a = left->intval;
    *b = right->intval;
    PASS(errctx, akbasic_environment_new_value(obj->environment, out));
    PASS(errctx, akbasic_value_zero(*out));
    (*out)->valuetype = AKBASIC_TYPE_INTEGER;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_shl(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *out = NULL;
    int64_t value = 0;
    int64_t bits = 0;

    (void)lval; (void)rval;
    PASS(errctx, two_integers(obj, expr, "SHL", &value, &bits, &out));
    FAIL_ZERO_RETURN(errctx, (bits >= 0 && bits < 64), AKBASIC_ERR_VALUE,
		     "SHL shift count %" PRId64 " is outside 0..63", bits);
    out->intval = (int64_t)((uint64_t)value << bits);
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_shr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *out = NULL;
    int64_t value = 0;
    int64_t bits = 0;

    (void)lval; (void)rval;
    PASS(errctx, two_integers(obj, expr, "SHR", &value, &bits, &out));
    FAIL_ZERO_RETURN(errctx, (bits >= 0 && bits < 64), AKBASIC_ERR_VALUE,
		     "SHR shift count %" PRId64 " is outside 0..63", bits);
    out->intval = value >> bits;
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_xor(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *out = NULL;
    int64_t left = 0;
    int64_t right = 0;

    (void)lval; (void)rval;
    PASS(errctx, two_integers(obj, expr, "XOR", &left, &right, &out));
    out->intval = left ^ right;
    *dest = out;
    SUCCEED_RETURN(errctx);
}

/*
 * PEEK, POINTER and POINTERVAR reach real memory. In Go these went through
 * unsafe.Pointer; in C they are ordinary casts and are simpler, not harder.
 *
 * They assume a little-endian host: tests/language/functions/pointer.bas sets
 * A# = 255 and expects PEEK(POINTER(A#)) to be 255, which only holds if the low
 * byte of the int64_t comes first.
 */
akerr_ErrorContext *akbasic_fn_peek(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *argleaf = NULL;
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;
    const uint8_t *source = NULL;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "PEEK", &argleaf, NULL, NULL));
    FAIL_ZERO_RETURN(errctx,
		     (argleaf->leaftype == AKBASIC_LEAF_LITERAL_INT ||
		      argleaf->leaftype == AKBASIC_LEAF_IDENTIFIER_INT),
		     AKBASIC_ERR_TYPE, "PEEK expected INTEGER or INTEGER VARIABLE");
    PASS(errctx, akbasic_runtime_evaluate(obj, argleaf, &arg));
    FAIL_ZERO_RETURN(errctx, (arg->valuetype == AKBASIC_TYPE_INTEGER && arg->intval != 0),
		     AKBASIC_ERR_VALUE, "PEEK got NIL pointer or uninitialized variable");
    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    source = (const uint8_t *)(uintptr_t)arg->intval;
    out->valuetype = AKBASIC_TYPE_INTEGER;
    out->intval = (int64_t)(*source);
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_pointer(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *argleaf = NULL;
    akbasic_Value *arg = NULL;
    akbasic_Value *out = NULL;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "POINTER", &argleaf, NULL, NULL));
    FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(argleaf), AKBASIC_ERR_TYPE,
		     "POINTER expected IDENTIFIER");

    /* The address of the live storage, not of a copy of it. */
    obj->eval_clone_identifiers = false;
    ATTEMPT {
	CATCH(errctx, akbasic_runtime_evaluate(obj, argleaf, &arg));
    } CLEANUP {
	obj->eval_clone_identifiers = true;
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_INTEGER;
    switch ( arg->valuetype ) {
    case AKBASIC_TYPE_INTEGER:
	out->intval = (int64_t)(uintptr_t)&arg->intval;
	break;
    case AKBASIC_TYPE_FLOAT:
	out->intval = (int64_t)(uintptr_t)&arg->floatval;
	break;
    case AKBASIC_TYPE_STRING:
	out->intval = (int64_t)(uintptr_t)&arg->stringval;
	break;
    default:
	FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "POINTER expects a INT, FLOAT or STRING variable");
    }
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_pointervar(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *argleaf = NULL;
    akbasic_Variable *variable = NULL;
    akbasic_Value *out = NULL;

    (void)lval; (void)rval;
    PASS(errctx, first_arg(obj, expr, "POINTERVAR", &argleaf, NULL, NULL));
    FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(argleaf), AKBASIC_ERR_TYPE,
		     "POINTERVAR expected IDENTIFIER");
    PASS(errctx, akbasic_environment_get(obj->environment, argleaf->identifier, &variable));
    FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
		     "Identifier %s is undefined", argleaf->identifier);
    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_INTEGER;
    out->intval = (int64_t)(uintptr_t)variable;
    *dest = out;
    SUCCEED_RETURN(errctx);
}
