@gobstones/gobstones-lang
Version:
588 lines (539 loc) • 20.4 kB
text/typescript
/* eslint-disable no-underscore-dangle */
import { Runner } from './runner';
import { i18n, i18nWithLanguage } from './i18n';
import {
apiboardFromJboard,
apiboardToJboard,
APIJBoard,
gbbFromJboard,
gbbToJboard,
JBoard
} from './board_formats';
import { Value, ValueInteger, ValueString, ValueStructure } from './value';
import {
ASTDef,
ASTPatternStructure,
GbsInterpreterException,
N_PatternStructure,
N_PatternTimeout,
SourceReader,
Token
} from '@gobstones/gobstones-parser';
import { boolFromValue, RuntimeState } from './runtime';
import { Frame } from './vm';
const DEFAULT_INFINITE_LOOP_TIMEOUT = 3000; /* millisecs */
const DEFAULT_LANGUAGE = 'es';
interface InterpreterInternalState {
infiniteLoopTimeout: number;
language: string;
runner: Runner;
}
interface Snapshot {
contextNames: string[];
board: APIJBoard;
region: string;
regionStack: string | string[];
}
interface Program {
alias: string;
interpret: (board: APIJBoard) => void;
}
interface Declaration {
alias: string;
name: string;
}
/* load a board in the API format into a fresh RuntimeState */
function apiboardToState(apiboard: APIJBoard): RuntimeState {
const state = new RuntimeState();
state.load(apiboardToJboard(apiboard));
return state;
}
/* Dump a RuntimeState to a board in the API format */
const apiboardFromState = (state: RuntimeState): APIJBoard => apiboardFromJboard(state.dump());
/* Backwards-compatible type/value with special cases for some types */
function apivalueFromValue(value: Value): { type: string; value: number | boolean | string } {
const composedValue = (
componentKind: string
): { type: string; value: number | boolean | string } => {
const elements = value[componentKind].map((it: Value) => {
const apiValue = apivalueFromValue(it);
const value2 = apiValue ? apiValue.value : undefined;
return value2;
});
return {
type: value.type().toString(),
value: elements
};
};
if (value === undefined) {
return undefined;
}
if (value.isInteger()) {
return {
type: i18n('TYPE:Integer'),
value: (value as ValueInteger).asNumber()
};
} else if (value.isBoolean()) {
return {
type: i18n('TYPE:Bool'),
value: boolFromValue(value as ValueStructure)
};
} else if (value.isString()) {
return {
type: i18n('TYPE:String'),
value: (value as ValueString).string
};
} else if (value.isTuple()) {
return composedValue('components');
} else if (value.isList()) {
return composedValue('elements');
} else if (value.isStructure()) {
return {
type: (value as ValueStructure).typeName,
value: value.toString()
};
} else {
return {
type: value.type().toString(),
value: value.toString()
};
}
}
class GobstonesInterpreterError {
public message: string;
public reason: { code: string; detail: string[] };
public on: {
range: { start: { row: number; column: number }; end: { row: number; column: number } };
region: string;
regionStack: string | string[];
};
public constructor(exception: GbsInterpreterException) {
this.message = exception.message;
this.reason = {
code: exception.reason,
detail: exception.args
};
this.on = {
range: {
start: {
row: exception.startPos.line,
column: exception.startPos.column
},
end: {
row: exception.endPos.line,
column: exception.endPos.column
}
},
region: exception.startPos.region,
regionStack: undefined
};
}
}
class ParseError extends GobstonesInterpreterError {
public constructor(exception: GbsInterpreterException) {
super(exception);
}
}
class ExecutionError extends GobstonesInterpreterError {
public snapshots: Snapshot[];
public constructor(
exception: GbsInterpreterException,
snapshots: Snapshot[],
regionStack: string | string[]
) {
super(exception);
const isTimeout = this.reason.code === 'timeout';
this.snapshots = isTimeout ? [snapshots[snapshots.length - 1]] : snapshots;
this.on.region =
typeof regionStack === 'string' ? regionStack : regionStack[regionStack.length - 1];
this.on.regionStack = regionStack;
}
}
class NormalExecutionResult {
public finalBoard: JBoard;
public snapshots: Snapshot[];
public returnValue: { type: string; value: string | number | boolean };
public actualReturnValue: Value;
public constructor(finalBoard, snapshots: Snapshot[], returnValue: Value) {
this.finalBoard = finalBoard;
this.snapshots = snapshots;
this.returnValue = apivalueFromValue(returnValue);
/* Actual return value */
this.actualReturnValue = returnValue;
}
}
class InteractiveExecutionResult {
public keys: Token[];
public timeout: Token;
// eslint-disable-next-line @typescript-eslint/ban-types
public onInit: Function;
// eslint-disable-next-line @typescript-eslint/ban-types
public onKey: Function;
// eslint-disable-next-line @typescript-eslint/ban-types
public onTimeout: Function;
public constructor(state: InterpreterInternalState) {
this.keys = this._collectKeyNames(state);
this.timeout = this._timeoutValue(state);
this.onInit = this._onInitFunction(state);
this.onKey = this._onKeyFunction(state);
this.onTimeout = this._onTimeoutFunction(state);
}
public _hasInit(state: InterpreterInternalState): boolean {
for (const branch of state.runner.symbolTable.program.branches) {
const p = branch.pattern;
if (p.tag === N_PatternStructure && p.constructorName.value === i18n('CONS:INIT')) {
return true;
}
}
return false;
}
public _hasTimeout(state): boolean {
return this.timeout !== undefined;
}
public _collectKeyNames(state: InterpreterInternalState): Token[] {
const keys = [];
for (const branch of state.runner.symbolTable.program.branches) {
const p: { tag: symbol } = branch.pattern;
if (
p.tag === N_PatternStructure &&
(p as ASTPatternStructure).constructorName.value !== i18n('CONS:INIT')
) {
keys.push((p as ASTPatternStructure).constructorName.value);
}
}
return keys;
}
public _timeoutValue(state: InterpreterInternalState): Token {
for (const branch of state.runner.symbolTable.program.branches) {
if (branch.pattern.tag === N_PatternTimeout) {
return branch.pattern.timeout;
}
}
return undefined;
}
/* Return a function that, when called, continues running
* the interactive program feeding it with the INIT event.
*
* If the interactive program does not have an entry for the
* INIT event, the returned function has no effect.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
public _onInitFunction(state: InterpreterInternalState): Function {
if (this._hasInit(state)) {
return () =>
i18nWithLanguage(state.language, () =>
this._onEvent(
state,
new ValueStructure(i18n('TYPE:Event'), i18n('CONS:INIT'), {})
)
);
} else {
return () =>
i18nWithLanguage(state.language, () => apiboardFromState(state.runner.globalState));
}
}
/* Return a function that, when called, continues running
* the interactive program feeding it with the TIMEOUT event.
*
* If the interactive program does not have an entry for the
* TIMEOUT event, the returned function has no effect.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
public _onTimeoutFunction(state: InterpreterInternalState): Function {
if (this._hasTimeout(state)) {
return () =>
i18nWithLanguage(state.language, () =>
this._onEvent(
state,
new ValueStructure(i18n('TYPE:Event'), i18n('CONS:TIMEOUT'), {})
)
);
} else {
return () =>
i18nWithLanguage(state.language, () => apiboardFromState(state.runner.globalState));
}
}
/* Return a function that, when called with a key code, continues running
* the interactive program feeding it with the given key event.
*
* If the interactive program does not have an entry for the given
* key, this results in a runtime error.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
public _onKeyFunction(state: InterpreterInternalState): Function {
return (keyCode: string) =>
i18nWithLanguage(state.language, () =>
this._onEvent(state, new ValueStructure(i18n('TYPE:Event'), keyCode, {}))
);
}
/* Continue running the interactive program feeding it with the given
* eventValue.
* On success, return a Board.
* On failure, return an ExecutionError. */
public _onEvent(state: InterpreterInternalState, eventValue: Value): APIJBoard {
return i18nWithLanguage(state.language, () => {
try {
state.runner.executeEventWithTimeout(eventValue, state.infiniteLoopTimeout);
return apiboardFromState(state.runner.globalState);
} catch (exception) {
if (exception.isGobstonesException === undefined) {
throw exception;
}
return new ExecutionError(exception, [], state.runner.regionStack());
}
});
}
}
class SnapshotTaker {
private _runner: Runner;
private _snapshots: Snapshot[];
public constructor(runner: Runner) {
this._runner = runner;
this._snapshots = [];
}
public takeSnapshot(
routineName: string,
position: SourceReader,
callStack: Frame[],
globalState: RuntimeState
): void {
if (this._shouldTakeSnapshot(routineName, callStack)) {
this._snapshots.push(this._snapshot(routineName, position, callStack, globalState));
}
}
public snapshots(): Snapshot[] {
return this._snapshots;
}
public _snapshot(
routineName: string,
position: SourceReader,
callStack: Frame[],
globalState: RuntimeState
): Snapshot {
const snapshot: Snapshot = {
contextNames: [],
board: apiboardFromState(globalState),
region: position.region,
regionStack: this._runner.regionStack()
};
for (const stackFrame of callStack) {
let name = stackFrame.routineName;
if (name !== 'program') {
name = name + '-' + stackFrame.uniqueFrameId.toString();
}
snapshot.contextNames.push(name);
}
return snapshot;
}
public _shouldTakeSnapshot(routineName: string, callStack: Frame[]): boolean {
const routineNameStack = [];
for (const stackFrame of callStack) {
routineNameStack.push(stackFrame.routineName);
}
if (this._runner.primitives.isProcedure(routineName)) {
/* A primitive procedure must be recorded if there are no
* atomic routines anywhere in the call stack. */
return this._noAtomicRoutines(routineNameStack);
} else {
/* Other routines must be recorded if they have the 'recorded'
* attribute, and, moreover, there are no atomic routines other
* than the last one in the call stack. */
routineNameStack.pop();
return this._isRecorded(routineName) && this._noAtomicRoutines(routineNameStack);
}
}
public _noAtomicRoutines(routineNameStack): boolean {
for (const routineName of routineNameStack) {
if (this._isAtomic(routineName)) {
return false;
}
}
return true;
}
public _isAtomic(routineName): boolean {
if (routineName === 'program') {
return false;
} else if (this._runner.primitives.isProcedure(routineName)) {
/* Primitive procedure */
return false;
} else if (this._runner.symbolTable.isProcedure(routineName)) {
/* User-defined procedure */
return false;
} else {
/* Function */
return true;
}
}
public _isRecorded(routineName): boolean {
if (routineName === 'program') {
return true;
} else if (this._runner.primitives.isProcedure(routineName)) {
/* Primitive procedure */
return true;
} else if (this._runner.symbolTable.isProcedure(routineName)) {
/* User-defined procedure */
return false;
} else {
/* Function */
return false;
}
}
}
class ParseResult {
public program: Program;
public declarations: Declaration[];
public getAttributes: (globalName: string) => Record<string, ASTDef>;
public constructor(state: InterpreterInternalState) {
if (state.runner.symbolTable.program === undefined) {
this.program = undefined;
} else if (state.runner.symbolTable.isInteractiveProgram()) {
this.program = this._resultForInteractiveProgram(state);
} else {
this.program = this._resultForProgram(state);
}
this.declarations = this._collectDeclarations(state.runner);
this.getAttributes = (globalName) => state.runner.symbolTable.getAttributes(globalName);
}
public _resultForProgram(state: InterpreterInternalState): Program {
return {
alias: 'program',
interpret: (board: APIJBoard) => {
const snapshotTaker = new SnapshotTaker(state.runner);
return i18nWithLanguage(state.language, () => {
try {
state.runner.compile();
state.runner.executeWithTimeoutTakingSnapshots(
apiboardToState(board),
state.infiniteLoopTimeout,
snapshotTaker.takeSnapshot.bind(snapshotTaker)
);
const finalBoard = apiboardFromState(state.runner.globalState);
const returnValue = state.runner.result;
return new NormalExecutionResult(
finalBoard,
snapshotTaker.snapshots(),
returnValue
);
} catch (exception) {
if (exception.isGobstonesException === undefined) {
throw exception;
}
return new ExecutionError(
exception,
snapshotTaker.snapshots(),
state.runner.regionStack()
);
}
});
}
};
}
public _resultForInteractiveProgram(state: InterpreterInternalState): Program {
return {
alias: 'interactiveProgram',
interpret: (board) =>
i18nWithLanguage(state.language, () => {
try {
state.runner.compile();
state.runner.initializeVirtualMachine(apiboardToState(board));
return new InteractiveExecutionResult(state);
} catch (exception) {
if (exception.isGobstonesException === undefined) {
throw exception;
}
return new ExecutionError(exception, [], state.runner.regionStack());
}
})
};
}
public _collectDeclarations(runner: Runner): Declaration[] {
const declarations = [];
for (const name of runner.symbolTable.allProcedureNames()) {
if (runner.primitives.isProcedure(name)) {
continue; /* Skip primitive procedures */
}
declarations.push({
alias: 'procedureDeclaration',
name
});
}
for (const name of runner.symbolTable.allFunctionNames()) {
if (runner.primitives.isFunction(name)) {
continue; /* Skip primitive functions */
}
declarations.push({
alias: 'functionDeclaration',
name
});
}
return declarations;
}
}
export class GobstonesInterpreterAPI {
public config: {
setLanguage: (code: string) => void;
setInfiniteLoopTimeout: (milliseconds: number) => void;
setXGobstonesEnabled: (isEnabled: boolean) => void;
};
public gbb: {
/* Convert a string representing a board in GBB format
* to a board in the "API" format. */
read: (gbb: string) => APIJBoard;
/* Convert a board in the "API" format to a string representing
* a board in GBB format. */
write: (apiboard: APIJBoard) => string;
};
public getAst: (sourceCode: string) => any;
public parse: (sourceCode: string) => any;
// eslint-disable-next-line @typescript-eslint/ban-types
public _withState: (sourceCode: string, useLinter: boolean, action: Function) => any;
public constructor() {
/* Internal state of the interpreter */
const state = {
infiniteLoopTimeout: DEFAULT_INFINITE_LOOP_TIMEOUT,
language: DEFAULT_LANGUAGE,
runner: new Runner()
};
this.config = {
setLanguage(code) {
state.language = code;
},
setInfiniteLoopTimeout(milliseconds) {
state.infiniteLoopTimeout = milliseconds;
},
setXGobstonesEnabled(isEnabled) {
/* TODO */
}
};
this.gbb = {
/* Convert a string representing a board in GBB format
* to a board in the "API" format. */
read: (gbb) => apiboardFromJboard(gbbToJboard(gbb)),
/* Convert a board in the "API" format to a string representing
* a board in GBB format. */
write: (apiboard) => gbbFromJboard(apiboardToJboard(apiboard))
};
this.getAst = (sourceCode) =>
this._withState(sourceCode, false, (_state) =>
_state.runner.abstractSyntaxTree.toMulangLike()
);
this.parse = (sourceCode) =>
this._withState(sourceCode, true, (_state) => new ParseResult(_state));
this._withState = (sourceCode, useLinter, action) =>
i18nWithLanguage(state.language, () => {
try {
state.runner.initialize();
state.runner.parse(sourceCode);
/* Disable checking whether there is a main 'program' present. */
state.runner.enableLintCheck('source-should-have-a-program-definition', false);
if (useLinter) state.runner.lint();
return action(state);
} catch (exception) {
if (exception.isGobstonesException === undefined) {
throw exception;
}
return new ParseError(exception);
}
});
}
}