UNPKG

@gobstones/gobstones-lang

Version:
588 lines (539 loc) 20.4 kB
/* 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); } }); } }