/**
 * @file environment.c
 * @brief Implements the scope and per-line working state.
 */

#include <inttypes.h>
#include <string.h>

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

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

akerr_ErrorContext *akbasic_environment_init(akbasic_Environment *obj, akbasic_Runtime *runtime, akbasic_Environment *parent)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL environment in init");
    FAIL_ZERO_RETURN(errctx, (runtime != NULL), AKERR_NULLPOINTER, "NULL runtime in environment init");

    PASS(errctx, akbasic_symtab_init(&obj->variables, AKBASIC_MAX_VARIABLES));
    PASS(errctx, akbasic_symtab_init(&obj->functions, AKBASIC_MAX_FUNCTIONS));
    PASS(errctx, akbasic_symtab_init(&obj->labels, AKBASIC_MAX_LABELS));

    obj->parent = parent;
    obj->runtime = runtime;
    obj->forNextVariable = NULL;
    obj->forStepLeaf = NULL;
    obj->forToLeaf = NULL;
    obj->loopFirstLine = 0;
    obj->loopExitLine = 0;
    obj->exiting = false;
    obj->doConditionLeaf = NULL;
    obj->doConditionKind = AKBASIC_LOOPCOND_NONE;
    obj->isDoLoop = false;
    obj->isGenerator = false;
    obj->generatorFn = NULL;
    obj->isEachLoop = false;
    obj->forGeneratorEnv = NULL;
    obj->gosubReturnLine = 0;
    obj->readReturnLine = 0;
    obj->readIdentifierIdx = 0;
    obj->waitingForCommand[0] = '\0';
    obj->errorToken = NULL;
    PASS(errctx, aksl_memset(obj->readIdentifierLeaves, 0, sizeof(obj->readIdentifierLeaves)));
    obj->doLeafPool.next = 0;
    obj->doLeafPool.capacity = AKBASIC_MAX_CONDITION_LEAVES;
    obj->doLeafPool.leaves = obj->doLeafStorage;
    obj->readLeafPool.next = 0;
    obj->readLeafPool.capacity = AKBASIC_MAX_LEAVES;
    obj->readLeafPool.leaves = obj->readLeafStorage;

    PASS(errctx, akbasic_value_zero(&obj->forStepValue));
    PASS(errctx, akbasic_value_zero(&obj->forToValue));
    PASS(errctx, akbasic_value_zero(&obj->returnValue));

    if ( obj->parent != NULL ) {
	obj->lineno = obj->parent->lineno;
	obj->nextline = obj->parent->nextline;
    } else {
	obj->lineno = 0;
	obj->nextline = 0;
    }
    obj->nextvalue = 0;
    PASS(errctx, akbasic_environment_zero_parser(obj));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_zero(akbasic_Environment *obj)
{
    PREPARE_ERROR(errctx);
    int i = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL environment in zero");
    for ( i = 0; i < AKBASIC_MAX_VALUES; i++ ) {
	PASS(errctx, akbasic_value_init(&obj->values[i]));
    }
    obj->nextvalue = 0;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_zero_parser(akbasic_Environment *obj)
{
    PREPARE_ERROR(errctx);
    int i = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL environment in zero_parser");
    for ( i = 0; i < AKBASIC_MAX_LEAVES; i++ ) {
	PASS(errctx, akbasic_leaf_init(&obj->leaves[i], AKBASIC_LEAF_UNDEFINED));
    }
    for ( i = 0; i < AKBASIC_MAX_TOKENS; i++ ) {
	PASS(errctx, akbasic_token_init(&obj->tokens[i]));
    }
    obj->curtoken = 0;
    obj->nexttoken = 0;
    obj->nextleaf = 0;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_new_value(akbasic_Environment *obj, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in new_value");
    FAIL_ZERO_RETURN(errctx, (obj->nextvalue < AKBASIC_MAX_VALUES), AKBASIC_ERR_BOUNDS,
		     "Maximum values per line reached");
    *dest = &obj->values[obj->nextvalue];
    obj->nextvalue += 1;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_new_leaf(akbasic_Environment *obj, akbasic_ASTLeaf **dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in new_leaf");
    FAIL_ZERO_RETURN(errctx, (obj->nextleaf < AKBASIC_MAX_LEAVES), AKBASIC_ERR_BOUNDS,
		     "No more leaves available");
    *dest = &obj->leaves[obj->nextleaf];
    obj->nextleaf += 1;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_wait_for_command(akbasic_Environment *obj, const char *command)
{
    PREPARE_ERROR(errctx);
    size_t length = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && command != NULL), AKERR_NULLPOINTER,
		     "NULL argument in wait_for_command");
    /*
     * The reference panics here. An interpreter library may not take the process
     * with it, so this raises instead -- but it is still a hard failure, because
     * two pending waits in one environment means the block structure is already
     * corrupt.
     */
    FAIL_NONZERO_RETURN(errctx, (obj->waitingForCommand[0] != '\0'), AKBASIC_ERR_STATE,
			"Can't wait on multiple commands in the same environment : %s",
			obj->waitingForCommand);
    PASS(errctx, aksl_strlen(command, &length));
    FAIL_ZERO_RETURN(errctx, (length < sizeof(obj->waitingForCommand)),
		     AKBASIC_ERR_BOUNDS, "Command name '%s' is too long to wait on", command);
    PASS(errctx, aksl_strcpy(obj->waitingForCommand, sizeof(obj->waitingForCommand), command));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_is_waiting_for_any(akbasic_Environment *obj, bool *dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
		     "NULL destination in is_waiting_for_any");
    *dest = false;
    if ( obj == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    if ( obj->waitingForCommand[0] != '\0' ) {
	*dest = true;
	SUCCEED_RETURN(errctx);
    }
    /* The recursive tail becomes a PASS; dest carries the answer back up. */
    PASS(errctx, akbasic_environment_is_waiting_for_any(obj->parent, dest));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command, bool *dest)
{
    PREPARE_ERROR(errctx);
    int cmp = 0;

    FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
		     "NULL destination in is_waiting_for");
    *dest = false;
    if ( obj == NULL || command == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    PASS(errctx, aksl_strcmp(obj->waitingForCommand, command, &cmp));
    if ( cmp == 0 ) {
	*dest = true;
	SUCCEED_RETURN(errctx);
    }
    PASS(errctx, akbasic_environment_is_waiting_for(obj->parent, command, dest));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_stop_waiting(akbasic_Environment *obj, const char *command)
{
    PREPARE_ERROR(errctx);
    int cmp = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && command != NULL), AKERR_NULLPOINTER,
		     "NULL argument in stop_waiting");
    /*
     * The reference ignores `command` and clears unconditionally, which lets an
     * inner block clear an outer block's wait (TODO.md §6 item 3). The
     * argument is honoured here only to the extent of walking to the environment
     * that is actually waiting for it -- clearing the wrong one outright would
     * change observable control flow, so the search stops at the first match and
     * a miss is silently tolerated, exactly as today.
     */
    while ( obj != NULL ) {
	PASS(errctx, aksl_strcmp(obj->waitingForCommand, command, &cmp));
	if ( cmp == 0 ) {
	    obj->waitingForCommand[0] = '\0';
	    SUCCEED_RETURN(errctx);
	}
	obj = obj->parent;
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_get_function(akbasic_Environment *obj, const char *fname, void **dest)
{
    PREPARE_ERROR(errctx);
    char upper[AKBASIC_SYMTAB_MAX_KEY];
    size_t i = 0;
    size_t len = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && fname != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in get_function");
    PASS(errctx, aksl_strlen(fname, &len));
    FAIL_ZERO_RETURN(errctx, (len < sizeof(upper)), AKERR_KEY, "Function '%s' is not defined", fname);
    for ( i = 0; i < len; i++ ) {
	char c = fname[i];
	upper[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c);
    }
    upper[len] = '\0';

    while ( obj != NULL ) {
	akerr_ErrorContext *found = akbasic_symtab_get(&obj->functions, upper, dest, NULL);
	if ( found == NULL ) {
	    SUCCEED_RETURN(errctx);
	}
	found->handled = true;
	IGNORE(akerr_release_error(found));
	obj = obj->parent;
    }
    FAIL_RETURN(errctx, AKERR_KEY, "Function '%s' is not defined", fname);
}

akerr_ErrorContext *akbasic_environment_get_label(akbasic_Environment *obj, const char *label, int64_t *dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && label != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in get_label");
    while ( obj != NULL ) {
	akerr_ErrorContext *found = akbasic_symtab_get(&obj->labels, label, NULL, dest);
	if ( found == NULL ) {
	    SUCCEED_RETURN(errctx);
	}
	found->handled = true;
	IGNORE(akerr_release_error(found));
	obj = obj->parent;
    }
    FAIL_RETURN(errctx, AKBASIC_ERR_UNDEFINED,
		"Unable to find or create label %s in environment", label);
}

akerr_ErrorContext *akbasic_environment_set_label(akbasic_Environment *obj, const char *label, int64_t value)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && label != NULL), AKERR_NULLPOINTER,
		     "NULL argument in set_label");
    /* Only the top-level environment creates labels. */
    while ( obj != NULL && obj->runtime->environment != obj ) {
	obj = obj->parent;
    }
    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKBASIC_ERR_ENVIRONMENT,
		     "Unable to create label in orphaned environment");
    PASS(errctx, akbasic_symtab_set(&obj->labels, label, NULL, value));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_get(akbasic_Environment *obj, const char *varname, akbasic_Variable **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Environment *walk = NULL;
    void *slot = NULL;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && varname != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in environment get");

    *dest = NULL;
    for ( walk = obj; walk != NULL; walk = walk->parent ) {
	akerr_ErrorContext *found = akbasic_symtab_get(&walk->variables, varname, &slot, NULL);
	if ( found == NULL ) {
	    *dest = (akbasic_Variable *)slot;
	    SUCCEED_RETURN(errctx);
	}
	found->handled = true;
	IGNORE(akerr_release_error(found));
    }

    /*
     * Parents do not create variables for their children: only the currently
     * active environment auto-creates. A miss anywhere else returns NULL without
     * error, which the caller is expected to notice.
     */
    if ( obj->runtime->environment != obj ) {
	SUCCEED_RETURN(errctx);
    }

    PASS(errctx, akbasic_environment_create(obj, varname, dest));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Create a variable slot in the given scope, optionally allocating its storage.
 *
 * Shared by akbasic_environment_create() and akbasic_environment_create_empty(),
 * which differ only in whether the new variable's value storage is initialized
 * immediately or left for the caller to set up.
 *
 * @param obj Scope the variable is created in; only this scope is searched or
 *        written to, unlike akbasic_environment_get()'s walk up the parent chain.
 * @param varname Name of the variable to create.
 * @param dest Set to the created (or already-existing) variable.
 * @param initialize When true, the variable's value storage is allocated from
 *        the runtime's value pool; when false, the caller must initialize it
 *        before the variable is evaluated.
 */
static akerr_ErrorContext *environment_create_named(akbasic_Environment *obj, const char *varname,
							akbasic_Variable **dest, bool initialize)
{
    PREPARE_ERROR(errctx);
    akbasic_Variable *variable = NULL;
    int64_t sizes[1] = { 1 };
    void *slot = NULL;
    size_t namelen = 0;
    akerr_ErrorContext *found = NULL;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && varname != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in environment create");

    /*
     * This scope only. Unlike akbasic_environment_get() there is no walk up the
     * parent chain: the caller has already said *which* scope it means, and
     * finding an outer one would put the variable somewhere other than where it
     * was asked for.
     */
    *dest = NULL;
    found = akbasic_symtab_get(&obj->variables, varname, &slot, NULL);
    if ( found == NULL ) {
	*dest = (akbasic_Variable *)slot;
	SUCCEED_RETURN(errctx);
    }
    found->handled = true;
    IGNORE(akerr_release_error(found));

    PASS(errctx, akbasic_runtime_new_variable(obj->runtime, &variable));
    PASS(errctx, aksl_strlen(varname, &namelen));
    FAIL_ZERO_RETURN(errctx, (namelen < sizeof(variable->name)), AKBASIC_ERR_BOUNDS,
		     "Variable name '%s' is too long", varname);
    PASS(errctx, aksl_strcpy(variable->name, sizeof(variable->name), varname));
    variable->valuetype = AKBASIC_TYPE_UNDEFINED;
    variable->mutable_ = true;
    if ( initialize ) {
	PASS(errctx, akbasic_variable_init(variable, &obj->runtime->valuepool, sizes, 1));
    }
    PASS(errctx, akbasic_symtab_set(&obj->variables, varname, variable, 0));
    *dest = variable;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Find a variable in this scope, creating and initializing it if absent.
 *
 * @param obj The scope to search and, on a miss, to create in.
 * @param varname Name including its type suffix.
 * @param dest Output destination populated with the variable.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
akerr_ErrorContext *akbasic_environment_create(akbasic_Environment *obj, const char *varname, akbasic_Variable **dest)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, environment_create_named(obj, varname, dest, true));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Find a variable in this scope, creating it without value storage if absent.
 *
 * The caller must initialize the variable before evaluating it.
 *
 * @param obj The scope to search and, on a miss, to create in.
 * @param varname Name including its type suffix.
 * @param dest Output destination populated with the variable.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
akerr_ErrorContext *akbasic_environment_create_empty(akbasic_Environment *obj, const char *varname,
							akbasic_Variable **dest)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, environment_create_named(obj, varname, dest, false));
    SUCCEED_RETURN(errctx);
}

/*
 * Evaluate an lvalue's subscript list, if it has one, into `subscripts`. A bare
 * identifier yields the single subscript {0}, which is how a scalar is addressed
 * -- every variable is really a one-element array.
 */
akerr_ErrorContext *akbasic_environment_collect_subscripts(akbasic_Environment *obj, akbasic_ASTLeaf *lval, int64_t *subscripts, int *count)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *expr = NULL;
    akbasic_Value *tval = NULL;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && lval != NULL && subscripts != NULL && count != NULL),
		     AKERR_NULLPOINTER, "NULL argument in collect_subscripts");
    *count = 0;
    if ( lval->expr != NULL &&
	 lval->expr->leaftype == AKBASIC_LEAF_ARGUMENTLIST &&
	 lval->expr->operator_ == AKBASIC_TOK_ARRAY_SUBSCRIPT ) {
	for ( expr = lval->expr->right; expr != NULL; expr = expr->next ) {
	    FAIL_ZERO_RETURN(errctx, (*count < AKBASIC_MAX_ARRAY_DEPTH), AKBASIC_ERR_BOUNDS,
			     "More than %d array subscripts", AKBASIC_MAX_ARRAY_DEPTH);
	    PASS(errctx, akbasic_runtime_evaluate(obj->runtime, expr, &tval));
	    FAIL_NONZERO_RETURN(errctx, (tval->valuetype != AKBASIC_TYPE_INTEGER),
				AKBASIC_ERR_TYPE,
				"Array dimensions must evaluate to integer (B)");
	    subscripts[*count] = tval->intval;
	    *count += 1;
	}
    }
    if ( *count == 0 ) {
	subscripts[0] = 0;
	*count = 1;
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Assign into a field, checking the field's declared type as it goes.
 *
 * The type check is the same one a variable gets, and it comes from the same
 * place: the field's suffix said what it holds when the TYPE was declared. What
 * is different is that a *field* name is checked for existence at all, which a
 * variable name never is -- the set of fields is closed and written down, and
 * akbasic_structtype_field() lists them when one is missed.
 */
static akerr_ErrorContext *assign_field(akbasic_Environment *obj, akbasic_ASTLeaf *lval,
					akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_StructField *field = NULL;
    akbasic_Value *slot = NULL;
    void *hostbase = NULL;

    PASS(errctx, akbasic_struct_resolve(obj->runtime, lval, &field, &slot, &hostbase));

    switch ( field->kind ) {
    case AKBASIC_FIELD_STRUCT:
	FAIL_ZERO_RETURN(errctx, (rval->valuetype == AKBASIC_TYPE_STRUCT), AKBASIC_ERR_TYPE,
			 "Incompatible types in assignment to %s", field->name);
	FAIL_ZERO_RETURN(errctx, (rval->structtype == field->typeindex), AKBASIC_ERR_TYPE,
			 "%s is a %s and cannot be assigned a %s", field->name,
			 obj->runtime->structtypes.types[field->typeindex].name,
			 obj->runtime->structtypes.types[rval->structtype].name);
	PASS(errctx, akbasic_struct_copy(obj->runtime, field->typeindex, rval->structbase, slot));
	break;
    case AKBASIC_FIELD_POINTER:
	FAIL_ZERO_RETURN(errctx, (rval->valuetype == AKBASIC_TYPE_POINTER), AKBASIC_ERR_TYPE,
			 "%s is a pointer; POINT it AT a structure rather than assigning one to it",
			 field->name);
	FAIL_ZERO_RETURN(errctx, (rval->structtype == field->typeindex), AKBASIC_ERR_TYPE,
			 "%s points to %s and cannot hold a pointer to %s", field->name,
			 obj->runtime->structtypes.types[field->typeindex].name,
			 obj->runtime->structtypes.types[rval->structtype].name);
	PASS(errctx, akbasic_value_clone(rval, slot));
	break;
    default:
	if ( field->valuetype == AKBASIC_TYPE_INTEGER && rval->valuetype == AKBASIC_TYPE_FLOAT ) {
	    PASS(errctx, akbasic_value_zero(slot));
	    slot->valuetype = AKBASIC_TYPE_INTEGER;
	    slot->intval = (int64_t)rval->floatval;
	    break;
	}
	if ( field->valuetype == AKBASIC_TYPE_FLOAT && rval->valuetype == AKBASIC_TYPE_INTEGER ) {
	    PASS(errctx, akbasic_value_zero(slot));
	    slot->valuetype = AKBASIC_TYPE_FLOAT;
	    slot->floatval = (double)rval->intval;
	    break;
	}
	FAIL_ZERO_RETURN(errctx, (rval->valuetype == field->valuetype), AKBASIC_ERR_TYPE,
			 "Incompatible types in assignment to %s", field->name);
	PASS(errctx, akbasic_value_clone(rval, slot));
	break;
    }
    /*
     * And straight back into the host's own memory, so `FOE@.HP# = 0` changes
     * the game's enemy rather than a copy of it. The conversion refuses what
     * will not fit -- an int16_t field handed 70000 is an error naming the
     * field, because a silent wrap is found three frames later in code that did
     * nothing wrong.
     */
    if ( hostbase != NULL && field->kind == AKBASIC_FIELD_PRIMITIVE ) {
	PASS(errctx, akbasic_host_write_field(obj->runtime, field, hostbase, slot));
    }
    *dest = slot;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_environment_assign(akbasic_Environment *obj, akbasic_ASTLeaf *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Variable *variable = NULL;
    int64_t subscripts[AKBASIC_MAX_ARRAY_DEPTH];
    int subscriptcount = 0;
    akbasic_Value *slot = NULL;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && lval != NULL && rval != NULL && dest != NULL),
		     AKERR_NULLPOINTER, "nil pointer");

    /*
     * A field is addressed by walking the chain, not by looking a name up in a
     * scope -- `E@.POS@.X#` names no variable called `X#`. So it is resolved and
     * assigned here, before the by-name lookup the rest of this function does.
     */
    if ( lval->leaftype == AKBASIC_LEAF_FIELD ) {
	PASS(errctx, assign_field(obj, lval, rval, dest));
	SUCCEED_RETURN(errctx);
    }

    PASS(errctx, akbasic_environment_get(obj, lval->identifier, &variable));
    FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
		     "Identifier %s is undefined", lval->identifier);
    PASS(errctx, akbasic_environment_collect_subscripts(obj, lval, subscripts, &subscriptcount));

    /*
     * Resolve the slot before the type switch. The reference notes that moving
     * this below the switch corrupts the subscript list; here it is simply the
     * clearer order, and the returned pointer is what an assignment expression
     * evaluates to.
     */
    PASS(errctx, akbasic_variable_get_subscript(variable, subscripts, subscriptcount, &slot));

    switch ( lval->leaftype ) {
    case AKBASIC_LEAF_IDENTIFIER_STRUCT:
	/*
	 * **This is the whole of copy-on-assign**, and it has to happen here
	 * rather than in akbasic_value_clone(): a clone copies one slot, and one
	 * slot holds a *reference* to an instance rather than the instance. Going
	 * through clone would therefore alias -- exactly the reference semantics
	 * the language does not have -- so a structure is intercepted before it
	 * reaches that path and its slots are copied one at a time.
	 *
	 * A pointer variable is the opposite and takes the clone: copying a
	 * pointer copies the reference, which is what makes `POINT` the only way
	 * to share and assignment always a copy.
	 */
	FAIL_ZERO_RETURN(errctx, (variable->structtype >= 0), AKBASIC_ERR_STATE,
			 "%s has not been DIMmed AS a type", lval->identifier);
	if ( variable->ispointer ) {
	    FAIL_ZERO_RETURN(errctx, (rval->valuetype == AKBASIC_TYPE_POINTER),
			     AKBASIC_ERR_TYPE,
			     "%s is a pointer; POINT it AT a structure rather than assigning one to it",
			     lval->identifier);
	    FAIL_ZERO_RETURN(errctx, (rval->structtype == variable->structtype),
			     AKBASIC_ERR_TYPE,
			     "%s points to %s and cannot hold a pointer to %s",
			     lval->identifier,
			     obj->runtime->structtypes.types[variable->structtype].name,
			     obj->runtime->structtypes.types[rval->structtype].name);
	    PASS(errctx, akbasic_value_clone(rval, slot));
	    break;
	}
	FAIL_ZERO_RETURN(errctx, (rval->valuetype == AKBASIC_TYPE_STRUCT), AKBASIC_ERR_TYPE,
			 "Incompatible types in variable assignment");
	FAIL_ZERO_RETURN(errctx, (rval->structtype == variable->structtype), AKBASIC_ERR_TYPE,
			 "%s is a %s and cannot be assigned a %s",
			 lval->identifier,
			 obj->runtime->structtypes.types[variable->structtype].name,
			 obj->runtime->structtypes.types[rval->structtype].name);
	PASS(errctx, akbasic_struct_copy(obj->runtime, variable->structtype,
					 rval->structbase, slot));
	*dest = slot;
	SUCCEED_RETURN(errctx);
    case AKBASIC_LEAF_IDENTIFIER_INT:
	if ( rval->valuetype == AKBASIC_TYPE_INTEGER ) {
	    PASS(errctx, akbasic_variable_set_integer(variable, rval->intval, subscripts, subscriptcount));
	} else if ( rval->valuetype == AKBASIC_TYPE_FLOAT ) {
	    PASS(errctx, akbasic_variable_set_integer(variable, (int64_t)rval->floatval, subscripts, subscriptcount));
	} else {
	    FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "Incompatible types in variable assignment");
	}
	break;
    case AKBASIC_LEAF_IDENTIFIER_FLOAT:
	if ( rval->valuetype == AKBASIC_TYPE_INTEGER ) {
	    PASS(errctx, akbasic_variable_set_float(variable, (double)rval->intval, subscripts, subscriptcount));
	} else if ( rval->valuetype == AKBASIC_TYPE_FLOAT ) {
	    PASS(errctx, akbasic_variable_set_float(variable, rval->floatval, subscripts, subscriptcount));
	} else {
	    FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "Incompatible types in variable assignment");
	}
	break;
    case AKBASIC_LEAF_IDENTIFIER_STRING:
	FAIL_NONZERO_RETURN(errctx, (rval->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			    "Incompatible types in variable assignment");
	PASS(errctx, akbasic_variable_set_string(variable, rval->stringval, subscripts, subscriptcount));
	break;
    default:
	FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "Invalid assignment");
    }
    variable->valuetype = rval->valuetype;
    *dest = slot;
    SUCCEED_RETURN(errctx);
}
