UNPKG

@gobstones/gobstones-lang

Version:
908 lines (817 loc) 31.5 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, 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; } }