/* eslint-disable no-underscore-dangle */
import {
    I_PushInteger,
    I_PushString,
    I_PushVariable,
    I_SetVariable,
    I_UnsetVariable,
    I_Label,
    I_Jump,
    I_JumpIfFalse,
    I_JumpIfStructure,
    I_JumpIfTuple,
    I_Call,
    I_Return,
    I_MakeTuple,
    I_MakeList,
    I_MakeStructure,
    I_UpdateStructure,
    I_ReadTupleComponent,
    I_ReadStructureField,
    I_ReadStructureFieldPop,
    I_Add,
    I_Dup,
    I_Pop,
    I_PrimitiveCall,
    I_SaveState,
    I_RestoreState,
    I_TypeCheck,
    Code,
    Instruction,
    IPushInteger,
    IPushString,
    IPushVariable,
    ISetVariable,
    IUnsetVariable,
    IJump,
    IJumpIfFalse,
    IJumpIfStructure,
    IJumpIfTuple,
    ICall,
    IMakeTuple,
    IMakeList,
    IMakeStructure,
    IUpdateStructure,
    IReadTupleComponent,
    IReadStructureField,
    IPrimitiveCall,
    ITypeCheck
} from './instruction';
import {
    V_Tuple,
    V_Structure,
    ValueInteger,
    ValueString,
    ValueTuple,
    ValueList,
    ValueStructure,
    joinTypes,
    TypeAny,
    Value,
    Type
} from './value';
import { GbsRuntimeError } from '@gobstones/gobstones-parser';
import { i18n } from './i18n';
import { RuntimePrimitives, RuntimeState } from './runtime';
import { SourceReader } from '@gobstones/gobstones-parser';

/* Conditions that may occur on runtime */
const RT_ExitProgram = Symbol.for('RT_ExitProgram');

/* Instances of RuntimeCondition represent conditions that may occur
 * during runtime (e.g. program termination or timeout). */
class RuntimeCondition extends Error {
    private tag: symbol;
    public constructor(tag: symbol) {
        super(Symbol.keyFor(tag));
        this.tag = tag;
    }
}

/* Runtime condition to mark the end of an execution */
class RuntimeExitProgram extends RuntimeCondition {
    private returnValue: Value;
    public constructor(returnValue: Value) {
        super(RT_ExitProgram);
        this.returnValue = returnValue;
    }
}

function fail(startPos: SourceReader, endPos: SourceReader, reason: string, args: any[]): void {
    throw new GbsRuntimeError(startPos, endPos, reason, args);
}

/* An instance of Frame represents the local execution context of a
 * function or procedure (a.k.a. "activation record" or "stack frame").
 *
 * It includes:
 * - the name of the current routine:
 *   + 'program' for the main program
 *   + the name of the current procedure or function
 * - the current instruction pointer
 * - a stack of local values
 * - a map from local names to values
 *
 * Each local variable has a type and a value.
 * - The actual type of the current value held by a variable
 *   should always be an instance of the type.
 * - The type of a variable should be the join of all the
 *   types held historically by the variable.
 * - The Frame does not impose these conditions.
 */
export class Frame {
    private _routineName: string;
    private _instructionPointer: number;
    private _variableTypes: Record<string, Type>;
    private _variables: Record<string, Value>;
    private _stack: Value[];
    private _uniqueFrameId: number;
    public constructor(frameId: number, routineName: string, instructionPointer: number) {
        this._routineName = routineName;
        this._instructionPointer = instructionPointer;
        this._variableTypes = {};
        this._variables = {};
        this._stack = [];

        /* The unique frame identifier is used to uniquely identify
         * a function call during a stack trace. This is used in the
         * API to generate snapshots. */
        this._uniqueFrameId = frameId;
    }

    public get routineName(): string {
        return this._routineName;
    }

    public get uniqueFrameId(): number {
        return this._uniqueFrameId;
    }

    public get instructionPointer(): number {
        return this._instructionPointer;
    }

    public set instructionPointer(value: number) {
        this._instructionPointer = value;
    }

    /* Precondition:
     *   Let oldType = this._variableTypes[name]
     *   if this._variableTypes[name] is defined.
     *   Otherwise, let oldType = new TypeAny().
     *   Then the following condition must hold:
     *     type = joinTypes(value.type(), oldType) */
    public setVariable(name: string, type: Type, value: Value): void {
        this._variableTypes[name] = type;
        this._variables[name] = value;
    }

    public unsetVariable(name: string): void {
        delete this._variables[name];
    }

    public getVariableType(name: string): Type {
        if (name in this._variableTypes) {
            return this._variableTypes[name];
        } else {
            return new TypeAny();
        }
    }

    public getVariable(name: string): Value {
        if (name in this._variables) {
            return this._variables[name];
        } else {
            return undefined;
        }
    }

    public stackEmpty(): boolean {
        return this._stack.length === 0;
    }

    public pushValue(value: Value): void {
        this._stack.push(value);
    }

    public stackTop(): Value {
        if (this._stack.length === 0) {
            throw Error('VM: no value at the top of the stack; the stack is empty.');
        }
        return this._stack[this._stack.length - 1];
    }

    public popValue(): Value {
        if (this._stack.length === 0) {
            throw Error('VM: no value to pop; the stack is empty.');
        }
        return this._stack.pop();
    }
}

/*
 * Receives an instance of Code, representing a program for the virtual
 * machine, and sets it up for running.
 *
 * Then it implements the following interface:
 *
 *   vm.run();    Run the program until termination.
 *                If the program returns a value, this method
 *                returns it. Otherwise it returns undefined.
 */
export class VirtualMachine {
    private _code: Code;
    private _labelTargets: Record<string, number>;
    private _nextFrameId: number;
    private _callStack: Frame[];
    private _globalStateStack: RuntimeState[];
    private _primitives: RuntimePrimitives;
    // eslint-disable-next-line @typescript-eslint/ban-types
    private _snapshotCallback?: Function;

    public constructor(code: Code, initialState: RuntimeState) {
        this._code = code;

        /* "this._labelTargets" is a dictionary mapping label names to
         * the corresponding instruction pointers.
         *
         * It is calculated automatically from code.
         */
        this._labelTargets = this._code.labelTargets();

        this._nextFrameId = 0;

        /* A "call stack" is a stack of frames.
         *
         * The topmost element of the stack (i.e. the last element of the list)
         * is the execution context of the current function.
         *
         * The previous element is the execution context of the caller, and so on.
         *
         * During the execution of a program the call stack should never
         * become empty.
         */
        this._callStack = [];
        this._callStack.push(this._newFrame('program', 0 /* instructionPointer */));

        /* The global state is the data that is available globally.
         *
         * In Gobstones, the global state is the board. The VM module
         * should not be aware of the actual implementation or nature of
         * the global state.
         *
         * We have a stack of global states.
         *
         * The instruction 'SaveState' saves the current global state.
         * It should be called whenever entering a user-defined function
         * in Gobstones.
         *
         * The instruction 'RestoreState' restores the previous global state.
         * It should be called whenever leaving a user-defined function
         * in Gobstones.
         */
        this._globalStateStack = [initialState];

        /* The following dictionary maps names of primitives to their
         * implementation.
         *
         * A primitive always receives 1 + n parameters, the first one being
         * the board.
         */
        this._primitives = new RuntimePrimitives();

        /*
         * A "snapshot callback" is a function that takes snapshots.
         *
         *   snapshotCallback(routineName, position, callStack, globalState)
         *
         *   routineName:
         *     It is the name of the routine that triggers the
         *     snapshot, it might be:
         *     - 'program' for the main program,
         *     - the name of a primitive procedure or function,
         *     - the name of a user-defined procedure or function.
         *
         *   position:
         *     The position in the source code for this snapshot.
         *
         *   callStack:
         *     The current call stack.
         *
         *   globalState:
         *     The current global state.
         *
         * Snapshots
         * If _snapshotCallback is undefined, the VM does not take snapshots.
         */
        this._snapshotCallback = undefined;
    }

    public run(): Value {
        return this.runWithTimeout(0);
    }

    /* Run the program, throwing an exception if the given timeout is met.
     * If millisecs is 0, the program is run indefinitely. */
    public runWithTimeout(millisecs: number): Value {
        return this.runWithTimeoutTakingSnapshots(millisecs, undefined);
    }

    /* Restart the program from the beginning, with the given eventValue
     * at the top of the stack.
     *
     * This is used for interactive programs, which work by iteratively
     * making calls to this function.
     */
    public runEventWithTimeout(eventValue: Value, millisecs: number): Value {
        this._callStack = [this._newFrame('program', 0 /* instructionPointer */)];
        this._currentFrame().pushValue(eventValue);
        return this.runWithTimeout(millisecs);
    }

    /* Run the program, throwing an exception if the given timeout is met.
     * If millisecs is 0, the program is run indefinitely.
     *
     * Snapshots are taken:
     * - At the very start of the program.
     * - At the end of the program.
     * - After calling any primitive procedure or function.
     * - Whenever reaching an I_Return instruction from any routine.
     *
     * The snapshotCallback function receives:
     * - The current call stack (list of frames).
     * - The current global state.
     */
    // eslint-disable-next-line @typescript-eslint/ban-types
    public runWithTimeoutTakingSnapshots(millisecs: number, snapshotCallback: Function): Value {
        const startTime = new Date().getTime();
        this._snapshotCallback = snapshotCallback;
        this._takeSnapshot('program');
        try {
            // eslint-disable-next-line no-constant-condition
            while (true) {
                this._step();
                this._timeoutIfNeeded(startTime, millisecs);
            }
        } catch (condition) {
            if (condition.tag === RT_ExitProgram) {
                return condition.returnValue;
            } else {
                throw condition;
            }
        }
    }

    public _newFrame(routineName: string, instructionPointer: number): Frame {
        const frameId = this._nextFrameId;
        this._nextFrameId++;
        return new Frame(frameId, routineName, instructionPointer);
    }

    public _timeoutIfNeeded(startTime: number, millisecs: number): void {
        if (millisecs > 0 && new Date().getTime() - startTime > millisecs) {
            const instruction = this._currentInstruction();
            fail(instruction.startPos, instruction.endPos, 'timeout', [millisecs]);
        }
    }

    public _takeSnapshot(routineName: string): void {
        if (this._snapshotCallback !== undefined) {
            const instruction = this._currentInstruction();
            this._snapshotCallback(
                routineName,
                instruction.startPos,
                this._callStack,
                this.globalState()
            );
        }
    }

    public globalState(): RuntimeState {
        return this._globalStateStack[this._globalStateStack.length - 1];
    }

    public setGlobalState(globalState: RuntimeState): void {
        this._globalStateStack[this._globalStateStack.length - 1] = globalState;
    }

    /* Return the current frame, which is the top of the call stack */
    public _currentFrame(): Frame {
        return this._callStack[this._callStack.length - 1];
    }

    /* Return the current instruction, given by the instruction pointer
     * of the current activation record */
    public _currentInstruction(): Instruction {
        return this._code.at(this._currentFrame().instructionPointer);
    }

    /* Execute a single instruction.
     *
     * If the program finishes, it throws an exception
     *   RuntimeExitProgram(returnValue)
     */
    public _step(): void {
        switch (this._currentInstruction().opcode) {
            case I_PushInteger:
                return this._stepPushInteger();
            case I_PushString:
                return this._stepPushString();
            case I_PushVariable:
                return this._stepPushVariable();
            case I_SetVariable:
                return this._stepSetVariable();
            case I_UnsetVariable:
                return this._stepUnsetVariable();
            case I_Label:
                return this._stepLabel();
            case I_Jump:
                return this._stepJump();
            case I_JumpIfFalse:
                return this._stepJumpIfFalse();
            case I_JumpIfStructure:
                return this._stepJumpIfStructure();
            case I_JumpIfTuple:
                return this._stepJumpIfTuple();
            case I_Call:
                return this._stepCall();
            case I_Return:
                return this._stepReturn();
            case I_MakeTuple:
                return this._stepMakeTuple();
            case I_MakeList:
                return this._stepMakeList();
            case I_MakeStructure:
                return this._stepMakeStructure();
            case I_UpdateStructure:
                return this._stepUpdateStructure();
            case I_ReadTupleComponent:
                return this._stepReadTupleComponent();
            case I_ReadStructureField:
                return this._stepReadStructureField();
            case I_ReadStructureFieldPop:
                return this._stepReadStructureFieldPop();
            case I_Add:
                return this._stepAdd();
            case I_Dup:
                return this._stepDup();
            case I_Pop:
                return this._stepPop();
            case I_PrimitiveCall:
                return this._stepPrimitiveCall();
            case I_SaveState:
                return this._stepSaveState();
            case I_RestoreState:
                return this._stepRestoreState();
            case I_TypeCheck:
                return this._stepTypeCheck();
            default:
                throw Error(
                    'VM: opcode ' +
                        Symbol.keyFor(this._currentInstruction().opcode) +
                        ' not implemented'
                );
        }
    }

    public _stepPushInteger(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IPushInteger;
        frame.pushValue(new ValueInteger(instruction.number));
        frame.instructionPointer++;
    }

    public _stepPushString(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IPushString;
        frame.pushValue(new ValueString(instruction.string));
        frame.instructionPointer++;
    }

    public _stepPushVariable(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IPushVariable;
        const value = frame.getVariable(instruction.variableName);
        if (value === undefined) {
            fail(instruction.startPos, instruction.endPos, 'undefined-variable', [
                instruction.variableName
            ]);
        }
        frame.pushValue(value);
        frame.instructionPointer++;
    }

    public _stepSetVariable(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as ISetVariable;
        const newValue = frame.popValue();

        /* Check that types are compatible */
        const oldType = frame.getVariableType(instruction.variableName);
        const valType = newValue.type();
        const newType = joinTypes(oldType, valType);
        if (newType === undefined) {
            fail(instruction.startPos, instruction.endPos, 'incompatible-types-on-assignment', [
                instruction.variableName,
                oldType,
                valType
            ]);
        }

        /* Proceed with assignment */
        frame.setVariable(instruction.variableName, newType, newValue);
        frame.instructionPointer++;
    }

    public _stepUnsetVariable(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IUnsetVariable;
        frame.unsetVariable(instruction.variableName);
        frame.instructionPointer++;
    }

    public _stepLabel(): void {
        /* Ignore pseudo-instruction */
        const frame = this._currentFrame();
        frame.instructionPointer++;
    }

    public _stepJump(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IJump;
        frame.instructionPointer = this._labelTargets[instruction.targetLabel];
    }

    public _stepJumpIfFalse(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IJumpIfFalse;
        const value = frame.popValue() as ValueStructure; /* Pop the value */
        if (value.tag === V_Structure && value.constructorName === 'False') {
            frame.instructionPointer = this._labelTargets[instruction.targetLabel];
        } else {
            frame.instructionPointer++;
        }
    }

    public _stepJumpIfStructure(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IJumpIfStructure;
        const value = frame.stackTop() as ValueStructure; /* Do not pop the value */
        if (value.tag === V_Structure && value.constructorName === instruction.constructorName) {
            frame.instructionPointer = this._labelTargets[instruction.targetLabel];
        } else {
            frame.instructionPointer++;
        }
    }

    public _stepJumpIfTuple(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IJumpIfTuple;
        const value = frame.stackTop() as ValueTuple; /* Do not pop the value */
        if (value.tag === V_Tuple && value.size() === instruction.size) {
            frame.instructionPointer = this._labelTargets[instruction.targetLabel];
        } else {
            frame.instructionPointer++;
        }
    }

    public _stepCall(): void {
        const callerFrame = this._currentFrame();
        const instruction = this._currentInstruction() as ICall;

        /* Create a new stack frame for the callee */
        const newFrame = this._newFrame(
            instruction.targetLabel,
            this._labelTargets[instruction.targetLabel]
        );
        this._callStack.push(newFrame);

        /* Pop arguments from caller's frame and push them into callee's frame */
        for (let i = 0; i < instruction.nargs; i++) {
            if (callerFrame.stackEmpty()) {
                fail(instruction.startPos, instruction.endPos, 'too-few-arguments', [
                    instruction.targetLabel
                ]);
            }
            newFrame.pushValue(callerFrame.popValue());
        }
    }

    public _stepReturn(): void {
        const innerFrame = this._currentFrame();

        let returnValue;
        if (innerFrame.stackEmpty()) {
            returnValue = undefined;
        } else {
            /* Take a snapshot when leaving a routine other than the program */
            this._takeSnapshot(innerFrame.routineName);

            returnValue = innerFrame.popValue();
            if (!innerFrame.stackEmpty()) {
                throw Error('VM: stack should be empty');
            }
        }

        this._callStack.pop();
        if (this._callStack.length === 0) {
            /* There are no more frames in the call stack, which means
             * that we are returning from the main program. */
            throw new RuntimeExitProgram(returnValue);
        } else {
            /* There are further frames in the call stack, which means
             * that we are returning from a function. */
            const outerFrame = this._currentFrame();
            if (returnValue !== undefined) {
                outerFrame.pushValue(returnValue);
            }
            outerFrame.instructionPointer++;
        }
    }

    public _stepMakeTuple(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IMakeTuple;

        const elements = [];
        for (let i = 0; i < instruction.size; i++) {
            elements.unshift(frame.popValue());
        }
        frame.pushValue(new ValueTuple(elements));
        frame.instructionPointer++;
    }

    public _stepMakeList(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IMakeList;

        const elements = [];
        for (let i = 0; i < instruction.size; i++) {
            elements.unshift(frame.popValue());
        }

        /* Check that the types of the elements are compatible */
        let contentType = new TypeAny();
        let index = 0;
        for (const element of elements) {
            const oldType = contentType;
            const newType = element.type();
            contentType = joinTypes(oldType, newType);
            if (contentType === undefined) {
                fail(
                    instruction.startPos,
                    instruction.endPos,
                    'incompatible-types-on-list-creation',
                    [index, oldType, newType]
                );
            }
            index++;
        }
        frame.pushValue(new ValueList(elements));
        frame.instructionPointer++;
    }

    public _stepMakeStructure(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IMakeStructure;

        const fields = {};
        const n = instruction.fieldNames.length;
        for (let i = 0; i < n; i++) {
            const fieldName = instruction.fieldNames[n - i - 1];
            fields[fieldName] = frame.popValue();
        }
        frame.pushValue(
            new ValueStructure(instruction.typeName, instruction.constructorName, fields)
        );
        frame.instructionPointer++;
    }

    public _stepUpdateStructure(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IUpdateStructure;

        const newFields = {};
        const newFieldNames = [];
        const n = instruction.fieldNames.length;
        for (let i = 0; i < n; i++) {
            const fieldName = instruction.fieldNames[n - i - 1];
            newFields[fieldName] = frame.popValue();
            newFieldNames.unshift(fieldName);
        }

        /* Check that it is a structure and built with the same constructor */
        const structure = frame.popValue() as ValueStructure;
        if (structure.tag !== V_Structure) {
            fail(instruction.startPos, instruction.endPos, 'expected-structure-but-got', [
                instruction.constructorName,
                i18n(Symbol.keyFor(structure.tag))
            ]);
        }
        if (structure.constructorName !== instruction.constructorName) {
            fail(instruction.startPos, instruction.endPos, 'expected-constructor-but-got', [
                instruction.constructorName,
                structure.constructorName
            ]);
        }
        if (structure.typeName !== instruction.typeName) {
            throw Error('VM: UpdateStructure instruction does not match type.');
        }

        /* Check that the types of the fields are compatible */
        for (const fieldName of newFieldNames) {
            const oldType = structure.fields[fieldName].type();
            const newType = newFields[fieldName].type();
            if (joinTypes(oldType, newType) === undefined) {
                fail(
                    instruction.startPos,
                    instruction.endPos,
                    'incompatible-types-on-structure-update',
                    [fieldName, oldType, newType]
                );
            }
        }

        /* Proceed with structure update */
        frame.pushValue(structure.updateFields(newFields));
        frame.instructionPointer++;
    }

    public _stepReadTupleComponent(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IReadTupleComponent;
        const tuple = frame.stackTop() as ValueTuple;
        if (tuple.tag !== V_Tuple) {
            fail(instruction.startPos, instruction.endPos, 'expected-tuple-value-but-got', [
                tuple.type()
            ]);
        }
        if (instruction.index >= tuple.size()) {
            fail(instruction.startPos, instruction.endPos, 'tuple-component-out-of-bounds', [
                tuple.size(),
                instruction.index
            ]);
        }
        frame.pushValue(tuple.components[instruction.index]);
        frame.instructionPointer++;
    }

    public _stepReadStructureFieldGeneric(shouldPopStructure: boolean): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IReadStructureField;
        let structure;
        if (shouldPopStructure) {
            structure = frame.popValue();
        } else {
            structure = frame.stackTop();
        }
        if (structure.tag !== V_Structure) {
            fail(instruction.startPos, instruction.endPos, 'expected-structure-value-but-got', [
                structure.type()
            ]);
        }
        if (!(instruction.fieldName in structure.fields)) {
            fail(instruction.startPos, instruction.endPos, 'structure-field-not-present', [
                structure.fieldNames(),
                instruction.fieldName
            ]);
        }
        frame.pushValue(structure.fields[instruction.fieldName]);
        frame.instructionPointer++;
    }

    public _stepReadStructureField(): void {
        this._stepReadStructureFieldGeneric(false); /* Do not pop the structure */
    }

    public _stepReadStructureFieldPop(): void {
        this._stepReadStructureFieldGeneric(true); /* Pop the structure */
    }

    /* Instruction used for testing/debugging */
    public _stepAdd(): void {
        const frame = this._currentFrame();
        const v1 = frame.popValue() as ValueInteger;
        const v2 = frame.popValue() as ValueInteger;
        frame.pushValue(v1.add(v2));
        frame.instructionPointer++;
    }

    public _stepDup(): void {
        const frame = this._currentFrame();
        const value = frame.popValue();
        frame.pushValue(value);
        frame.pushValue(value);
        frame.instructionPointer++;
    }

    public _stepPop(): void {
        const frame = this._currentFrame();
        frame.popValue();
        frame.instructionPointer++;
    }

    public _stepPrimitiveCall(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as IPrimitiveCall;

        /* Pop arguments from stack */
        const args = [];
        for (let i = 0; i < instruction.nargs; i++) {
            args.unshift(frame.popValue());
        }

        /* Check that the primitive exists */
        if (!this._primitives.isOperation(instruction.primitiveName)) {
            fail(instruction.startPos, instruction.endPos, 'primitive-does-not-exist', [
                instruction.primitiveName
            ]);
        }

        const primitive = this._primitives.getOperation(instruction.primitiveName);

        /* Check that the number of expected parameters coincides with
         * the actual arguments provided */
        if (primitive.argumentTypes.length !== instruction.nargs) {
            fail(instruction.startPos, instruction.endPos, 'primitive-arity-mismatch', [
                instruction.primitiveName,
                primitive.argumentTypes.length,
                instruction.nargs
            ]);
        }

        /* Check that the types of all parameters coincide with the types of the
         * actual arguments */
        for (let i = 0; i < instruction.nargs; i++) {
            const expectedType = primitive.argumentTypes[i];
            const receivedType = args[i].type();
            if (joinTypes(expectedType, receivedType) === undefined) {
                fail(instruction.startPos, instruction.endPos, 'primitive-argument-type-mismatch', [
                    instruction.primitiveName,
                    i + 1,
                    instruction.nargs,
                    expectedType,
                    receivedType
                ]);
            }
        }

        /* Validate the arguments using the primitive-specific validator */
        primitive.validateArguments(
            instruction.startPos,
            instruction.endPos,
            this.globalState(),
            args
        );

        /* Proceed to call the primitive operation */
        const result = primitive.call(this.globalState(), args); /* mutates 'args' */
        if (result !== undefined) {
            frame.pushValue(result);
        }

        /* Take a snapshot after calling the primitive operation */
        this._takeSnapshot(instruction.primitiveName);

        frame.instructionPointer++;
    }

    public _stepSaveState(): void {
        const frame = this._currentFrame();
        this._globalStateStack.push(this.globalState().clone());
        frame.instructionPointer++;
    }

    public _stepRestoreState(): void {
        const frame = this._currentFrame();
        this._globalStateStack.pop();
        if (this._globalStateStack.length === 0) {
            throw Error('RestoreState: the stack of global states is empty.');
        }
        frame.instructionPointer++;
    }

    public _stepTypeCheck(): void {
        const frame = this._currentFrame();
        const instruction = this._currentInstruction() as ITypeCheck;
        const expectedType = instruction.type;
        const receivedType = frame.stackTop().type();
        if (joinTypes(expectedType, receivedType) === undefined) {
            fail(instruction.startPos, instruction.endPos, 'expected-value-of-type-but-got', [
                expectedType,
                receivedType
            ]);
        }
        frame.instructionPointer++;
    }

    /* Return the current dynamic stack of regions */
    public regionStack(): string[] {
        const regionStack = [];
        for (const stackFrame of this._callStack) {
            const instruction = this._code.at(stackFrame.instructionPointer);
            regionStack.push(instruction.startPos.region);
        }
        return regionStack;
    }
}
