@gobstones/gobstones-lang
Version:
908 lines (817 loc) • 31.5 kB
text/typescript
/* 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;
}
}