@gobstones/gobstones-lang
Version:
432 lines • 16 kB
JavaScript
/* eslint-disable no-underscore-dangle */
import { Runner } from './runner';
import { i18n, i18nWithLanguage } from './i18n';
import { apiboardFromJboard, apiboardToJboard, gbbFromJboard, gbbToJboard } from './board_formats';
import { ValueStructure } from './value';
import { N_PatternStructure, N_PatternTimeout } from '@gobstones/gobstones-parser';
import { boolFromValue, RuntimeState } from './runtime';
const DEFAULT_INFINITE_LOOP_TIMEOUT = 3000; /* millisecs */
const DEFAULT_LANGUAGE = 'es';
/* load a board in the API format into a fresh RuntimeState */
function apiboardToState(apiboard) {
const state = new RuntimeState();
state.load(apiboardToJboard(apiboard));
return state;
}
/* Dump a RuntimeState to a board in the API format */
const apiboardFromState = (state) => apiboardFromJboard(state.dump());
/* Backwards-compatible type/value with special cases for some types */
function apivalueFromValue(value) {
const composedValue = (componentKind) => {
const elements = value[componentKind].map((it) => {
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.asNumber()
};
}
else if (value.isBoolean()) {
return {
type: i18n('TYPE:Bool'),
value: boolFromValue(value)
};
}
else if (value.isString()) {
return {
type: i18n('TYPE:String'),
value: value.string
};
}
else if (value.isTuple()) {
return composedValue('components');
}
else if (value.isList()) {
return composedValue('elements');
}
else if (value.isStructure()) {
return {
type: value.typeName,
value: value.toString()
};
}
else {
return {
type: value.type().toString(),
value: value.toString()
};
}
}
class GobstonesInterpreterError {
constructor(exception) {
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 {
constructor(exception) {
super(exception);
}
}
class ExecutionError extends GobstonesInterpreterError {
constructor(exception, snapshots, regionStack) {
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 {
constructor(finalBoard, snapshots, returnValue) {
this.finalBoard = finalBoard;
this.snapshots = snapshots;
this.returnValue = apivalueFromValue(returnValue);
/* Actual return value */
this.actualReturnValue = returnValue;
}
}
class InteractiveExecutionResult {
constructor(state) {
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);
}
_hasInit(state) {
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;
}
_hasTimeout(state) {
return this.timeout !== undefined;
}
_collectKeyNames(state) {
const keys = [];
for (const branch of state.runner.symbolTable.program.branches) {
const p = branch.pattern;
if (p.tag === N_PatternStructure &&
p.constructorName.value !== i18n('CONS:INIT')) {
keys.push(p.constructorName.value);
}
}
return keys;
}
_timeoutValue(state) {
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
_onInitFunction(state) {
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
_onTimeoutFunction(state) {
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
_onKeyFunction(state) {
return (keyCode) => 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. */
_onEvent(state, eventValue) {
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 {
constructor(runner) {
this._runner = runner;
this._snapshots = [];
}
takeSnapshot(routineName, position, callStack, globalState) {
if (this._shouldTakeSnapshot(routineName, callStack)) {
this._snapshots.push(this._snapshot(routineName, position, callStack, globalState));
}
}
snapshots() {
return this._snapshots;
}
_snapshot(routineName, position, callStack, globalState) {
const 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;
}
_shouldTakeSnapshot(routineName, callStack) {
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);
}
}
_noAtomicRoutines(routineNameStack) {
for (const routineName of routineNameStack) {
if (this._isAtomic(routineName)) {
return false;
}
}
return true;
}
_isAtomic(routineName) {
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;
}
}
_isRecorded(routineName) {
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 {
constructor(state) {
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);
}
_resultForProgram(state) {
return {
alias: 'program',
interpret: (board) => {
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());
}
});
}
};
}
_resultForInteractiveProgram(state) {
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());
}
})
};
}
_collectDeclarations(runner) {
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 {
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);
}
});
}
}
//# sourceMappingURL=api.js.map