UNPKG

@gobstones/gobstones-lang

Version:
725 lines 28.4 kB
/* 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 } from './instruction'; import { V_Tuple, V_Structure, ValueInteger, ValueString, ValueTuple, ValueList, ValueStructure, joinTypes, TypeAny } from './value'; import { GbsRuntimeError } from '@gobstones/gobstones-parser'; import { i18n } from './i18n'; import { RuntimePrimitives } from './runtime'; /* 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 { constructor(tag) { super(Symbol.keyFor(tag)); this.tag = tag; } } /* Runtime condition to mark the end of an execution */ class RuntimeExitProgram extends RuntimeCondition { constructor(returnValue) { super(RT_ExitProgram); this.returnValue = returnValue; } } function fail(startPos, endPos, reason, args) { 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 { constructor(frameId, routineName, instructionPointer) { 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; } get routineName() { return this._routineName; } get uniqueFrameId() { return this._uniqueFrameId; } get instructionPointer() { return this._instructionPointer; } set instructionPointer(value) { 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) */ setVariable(name, type, value) { this._variableTypes[name] = type; this._variables[name] = value; } unsetVariable(name) { delete this._variables[name]; } getVariableType(name) { if (name in this._variableTypes) { return this._variableTypes[name]; } else { return new TypeAny(); } } getVariable(name) { if (name in this._variables) { return this._variables[name]; } else { return undefined; } } stackEmpty() { return this._stack.length === 0; } pushValue(value) { this._stack.push(value); } stackTop() { 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]; } popValue() { 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 { constructor(code, initialState) { 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; } run() { 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. */ runWithTimeout(millisecs) { 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. */ runEventWithTimeout(eventValue, millisecs) { 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 runWithTimeoutTakingSnapshots(millisecs, snapshotCallback) { 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; } } } _newFrame(routineName, instructionPointer) { const frameId = this._nextFrameId; this._nextFrameId++; return new Frame(frameId, routineName, instructionPointer); } _timeoutIfNeeded(startTime, millisecs) { if (millisecs > 0 && new Date().getTime() - startTime > millisecs) { const instruction = this._currentInstruction(); fail(instruction.startPos, instruction.endPos, 'timeout', [millisecs]); } } _takeSnapshot(routineName) { if (this._snapshotCallback !== undefined) { const instruction = this._currentInstruction(); this._snapshotCallback(routineName, instruction.startPos, this._callStack, this.globalState()); } } globalState() { return this._globalStateStack[this._globalStateStack.length - 1]; } setGlobalState(globalState) { this._globalStateStack[this._globalStateStack.length - 1] = globalState; } /* Return the current frame, which is the top of the call stack */ _currentFrame() { return this._callStack[this._callStack.length - 1]; } /* Return the current instruction, given by the instruction pointer * of the current activation record */ _currentInstruction() { return this._code.at(this._currentFrame().instructionPointer); } /* Execute a single instruction. * * If the program finishes, it throws an exception * RuntimeExitProgram(returnValue) */ _step() { 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'); } } _stepPushInteger() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); frame.pushValue(new ValueInteger(instruction.number)); frame.instructionPointer++; } _stepPushString() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); frame.pushValue(new ValueString(instruction.string)); frame.instructionPointer++; } _stepPushVariable() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); const value = frame.getVariable(instruction.variableName); if (value === undefined) { fail(instruction.startPos, instruction.endPos, 'undefined-variable', [ instruction.variableName ]); } frame.pushValue(value); frame.instructionPointer++; } _stepSetVariable() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); 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++; } _stepUnsetVariable() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); frame.unsetVariable(instruction.variableName); frame.instructionPointer++; } _stepLabel() { /* Ignore pseudo-instruction */ const frame = this._currentFrame(); frame.instructionPointer++; } _stepJump() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); frame.instructionPointer = this._labelTargets[instruction.targetLabel]; } _stepJumpIfFalse() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); const value = frame.popValue(); /* Pop the value */ if (value.tag === V_Structure && value.constructorName === 'False') { frame.instructionPointer = this._labelTargets[instruction.targetLabel]; } else { frame.instructionPointer++; } } _stepJumpIfStructure() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); const value = frame.stackTop(); /* Do not pop the value */ if (value.tag === V_Structure && value.constructorName === instruction.constructorName) { frame.instructionPointer = this._labelTargets[instruction.targetLabel]; } else { frame.instructionPointer++; } } _stepJumpIfTuple() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); const value = frame.stackTop(); /* Do not pop the value */ if (value.tag === V_Tuple && value.size() === instruction.size) { frame.instructionPointer = this._labelTargets[instruction.targetLabel]; } else { frame.instructionPointer++; } } _stepCall() { const callerFrame = this._currentFrame(); const instruction = this._currentInstruction(); /* 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()); } } _stepReturn() { 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++; } } _stepMakeTuple() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); const elements = []; for (let i = 0; i < instruction.size; i++) { elements.unshift(frame.popValue()); } frame.pushValue(new ValueTuple(elements)); frame.instructionPointer++; } _stepMakeList() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); 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++; } _stepMakeStructure() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); 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++; } _stepUpdateStructure() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); 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(); 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++; } _stepReadTupleComponent() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); const tuple = frame.stackTop(); 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++; } _stepReadStructureFieldGeneric(shouldPopStructure) { const frame = this._currentFrame(); const instruction = this._currentInstruction(); 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++; } _stepReadStructureField() { this._stepReadStructureFieldGeneric(false); /* Do not pop the structure */ } _stepReadStructureFieldPop() { this._stepReadStructureFieldGeneric(true); /* Pop the structure */ } /* Instruction used for testing/debugging */ _stepAdd() { const frame = this._currentFrame(); const v1 = frame.popValue(); const v2 = frame.popValue(); frame.pushValue(v1.add(v2)); frame.instructionPointer++; } _stepDup() { const frame = this._currentFrame(); const value = frame.popValue(); frame.pushValue(value); frame.pushValue(value); frame.instructionPointer++; } _stepPop() { const frame = this._currentFrame(); frame.popValue(); frame.instructionPointer++; } _stepPrimitiveCall() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); /* 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++; } _stepSaveState() { const frame = this._currentFrame(); this._globalStateStack.push(this.globalState().clone()); frame.instructionPointer++; } _stepRestoreState() { const frame = this._currentFrame(); this._globalStateStack.pop(); if (this._globalStateStack.length === 0) { throw Error('RestoreState: the stack of global states is empty.'); } frame.instructionPointer++; } _stepTypeCheck() { const frame = this._currentFrame(); const instruction = this._currentInstruction(); 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 */ regionStack() { const regionStack = []; for (const stackFrame of this._callStack) { const instruction = this._code.at(stackFrame.instructionPointer); regionStack.push(instruction.startPos.region); } return regionStack; } } //# sourceMappingURL=vm.js.map