diff --git a/src/@types/execution.d.ts b/src/@types/execution.d.ts index ee96e30..54f2709 100644 --- a/src/@types/execution.d.ts +++ b/src/@types/execution.d.ts @@ -26,6 +26,11 @@ export type IParsedElementInstruction = { /** Type definition of the parsed element entry returned on fetching next element. */ export type IParsedElement = IParsedElementArgument | IParsedElementInstruction; +export type IPCRoutineCall = { + type: '__callroutine__'; + nodeID: string; +}; + /* Type definition for program counter override signals. */ export type TPCOverride = | '__rollback__' @@ -36,4 +41,5 @@ export type TPCOverride = | '__goinnerlast__' | '__goup__' | '__repeat__' + | IPCRoutineCall | null; diff --git a/src/execution/interpreter/index.ts b/src/execution/interpreter/index.ts index c874b9d..2bbce83 100644 --- a/src/execution/interpreter/index.ts +++ b/src/execution/interpreter/index.ts @@ -1,5 +1,11 @@ import { addGlobalSymbol, getGlobalSymbol } from '../scope'; -import { setPCOverride, clearPCOverride, setExecutionItem, getNextElement } from '../parser'; +import { + setPCOverride, + clearPCOverride, + setExecutionItem, + getNextElement, + invokeRoutineByNodeID, +} from '../parser'; import { ElementData, @@ -60,6 +66,15 @@ export function releaseProgramCounter(): void { clearPCOverride(); } +/** + * Invokes a routine by its node ID for the current execution item. + * @param routineNodeID syntax tree node ID of the routine + * @returns `true` if routine exists, else `false` + */ +export function invokeRoutine(routineNodeID: string): boolean { + return invokeRoutineByNodeID(routineNodeID); +} + /** * Runs a process, routine, or crumb stack from start to end. * @param nodeID syntax tree node ID of the starting node diff --git a/src/execution/parser/index.spec.ts b/src/execution/parser/index.spec.ts index 680bef7..49951c9 100644 --- a/src/execution/parser/index.spec.ts +++ b/src/execution/parser/index.spec.ts @@ -9,6 +9,7 @@ import { setPCOverride, clearPCOverride, stackTrace, + invokeRoutineByNodeID, } from '.'; import { @@ -1074,6 +1075,263 @@ describe('Parser', () => { }); }); + describe('routine call', () => { + test('single routine call', () => { + resetSyntaxTree(); + generateFromSnapshot({ + process: [ + { + elementName: 'process', + argMap: null, + scope: [ + { + elementName: 'box-boolean', + argMap: { + name: { elementName: 'value-string' }, + value: { elementName: 'value-boolean' }, + }, + }, + { + elementName: 'box-number', + argMap: { + name: { elementName: 'value-string' }, + value: { elementName: 'value-number' }, + }, + }, + ], + }, + ], + routine: [ + { + elementName: 'routine', + argMap: { name: { elementName: 'value-string' } }, + scope: [ + { + elementName: 'box-string', + argMap: { + name: { elementName: 'value-string' }, + value: { elementName: 'value-string' }, + }, + }, + ], + }, + ], + crumbs: [], + }); + + const processNode = getProcessNodes()[0]; + const routineNode = getRoutineNodes()[0]; + + setExecutionItem(processNode.nodeID); + + const results: string[] = []; + let guard = 0; + while (guard < 100) { + const next = getNextElement(); + if (next === null) break; + results.push(next.instance.name); + + if (next.instance.name === 'box-boolean') { + invokeRoutineByNodeID(routineNode.nodeID); + } + guard++; + } + + expect(results).toEqual([ + 'process', + 'value-string', + 'value-boolean', + 'box-boolean', + 'value-string', + 'routine', + 'value-string', + 'value-string', + 'box-string', + 'routine', + 'value-string', + 'value-number', + 'box-number', + 'process', + ]); + }); + + test('nested routine call', () => { + resetSyntaxTree(); + generateFromSnapshot({ + process: [ + { + elementName: 'process', + argMap: null, + scope: [ + { + elementName: 'box-boolean', + argMap: { + name: { elementName: 'value-string' }, + value: { elementName: 'value-boolean' }, + }, + }, + ], + }, + ], + routine: [ + { + // Routine A + elementName: 'routine', + argMap: { name: { elementName: 'value-string' } }, + scope: [ + { + elementName: 'box-string', + argMap: { + name: { elementName: 'value-string' }, + value: { elementName: 'value-string' }, + }, + }, + ], + }, + { + // Routine B + elementName: 'routine', + argMap: { name: { elementName: 'value-string' } }, + scope: [ + { + elementName: 'box-number', + argMap: { + name: { elementName: 'value-string' }, + value: { elementName: 'value-number' }, + }, + }, + ], + }, + ], + crumbs: [], + }); + + const processNode = getProcessNodes()[0]; + const routineNodes = getRoutineNodes(); + const routineA = routineNodes[0]; + const routineB = routineNodes[1]; + + setExecutionItem(processNode.nodeID); + + const results: string[] = []; + let guard = 0; + while (guard < 100) { + const next = getNextElement(); + if (next === null) break; + results.push(next.instance.name); + + if (next.instance.name === 'box-boolean') { + invokeRoutineByNodeID(routineA.nodeID); + } else if (next.instance.name === 'box-string') { + invokeRoutineByNodeID(routineB.nodeID); + } + guard++; + } + + expect(results).toEqual([ + 'process', + 'value-string', + 'value-boolean', + 'box-boolean', + 'value-string', + 'routine', + 'value-string', + 'value-string', + 'box-string', + 'value-string', + 'routine', + 'value-string', + 'value-number', + 'box-number', + 'routine', + 'routine', + 'process', + ]); + }); + + test('multi process isolation', () => { + resetSyntaxTree(); + generateFromSnapshot({ + process: [ + { + elementName: 'process', + argMap: null, + scope: [ + { + elementName: 'box-boolean', + argMap: { + name: { elementName: 'value-string' }, + value: { elementName: 'value-boolean' }, + }, + }, + ], + }, + { + elementName: 'process', + argMap: null, + scope: [ + { + elementName: 'box-number', + argMap: { + name: { elementName: 'value-string' }, + value: { elementName: 'value-number' }, + }, + }, + ], + }, + ], + routine: [ + { + elementName: 'routine', + argMap: { name: { elementName: 'value-string' } }, + scope: [ + { + elementName: 'box-string', + argMap: { + name: { elementName: 'value-string' }, + value: { elementName: 'value-string' }, + }, + }, + ], + }, + ], + crumbs: [], + }); + + const processNodes = getProcessNodes(); + const routineNode = getRoutineNodes()[0]; + + // Execute Process 1 + setExecutionItem(processNodes[0].nodeID); + const results1: string[] = []; + let guard = 0; + while (guard < 100) { + const next = getNextElement(); + if (next === null) break; + results1.push(next.instance.name); + if (next.instance.name === 'box-boolean') { + invokeRoutineByNodeID(routineNode.nodeID); + } + guard++; + } + + // Execute Process 2 (Reset should have happened) + setExecutionItem(processNodes[1].nodeID); + const results2: string[] = []; + guard = 0; + while (guard < 100) { + const next = getNextElement(); + if (next === null) break; + results2.push(next.instance.name); + guard++; + } + + expect(results1).toContain('box-string'); + expect(results2).not.toContain('box-string'); + expect(results2).toContain('box-number'); + }); + }); + describe('execution call frame stack trace', () => { test('verify stack trace', () => { resetSyntaxTree(); diff --git a/src/execution/parser/index.ts b/src/execution/parser/index.ts index 9de30bf..b7fd17e 100644 --- a/src/execution/parser/index.ts +++ b/src/execution/parser/index.ts @@ -1,4 +1,4 @@ -import type { IParsedElement, TPCOverride } from '../../@types/execution'; +import type { IParsedElement, TPCOverride, IPCRoutineCall } from '../../@types/execution'; import type { TData } from '../../@types/data'; import { @@ -39,6 +39,11 @@ interface IFrame { | null; } +interface ICallFrame { + frames: IFrame[]; + returnNode: TreeNodeStatement | TreeNodeBlock | null; +} + /** Type definition for each root element's parsing state table. */ type TProgramMapEntry = { /** Execution call frame stack. */ @@ -49,6 +54,7 @@ type TProgramMapEntry = { pcHandler: (() => void)[]; /** Signal to override program counter's normal sequence. */ pcOverride: TPCOverride; + callFrames: ICallFrame[]; }; /** Maintains the parsing state tables. */ @@ -96,6 +102,7 @@ function _reset(): void { pc: null, pcHandler: [], pcOverride: null, + callFrames: [], }; } @@ -105,6 +112,7 @@ function _reset(): void { pc: null, pcHandler: [], pcOverride: null, + callFrames: [], }; } @@ -114,6 +122,7 @@ function _reset(): void { pc: null, pcHandler: [], pcOverride: null, + callFrames: [], }; } @@ -271,6 +280,20 @@ export function clearPCOverride(): void { _programMap[_executionItem.bucket][_executionItem.node.nodeID].pcOverride = null; } +export function invokeRoutineByNodeID(nodeID: string): boolean { + if (_executionItem === null) return false; + + const routineNode = _routineNodes.find((n) => n.nodeID === nodeID); + if (!routineNode) return false; + + _programMap[_executionItem.bucket][_executionItem.node.nodeID].pcOverride = { + type: '__callroutine__', + nodeID, + }; + + return true; +} + /** * Returns the next node in execution sequence. * @returns - element entry if present, else `null` @@ -299,6 +322,26 @@ export function getNextElement(): IParsedElement | null { * Successful end of execution */ + if (executionItemEntry.callFrames.length > 0) { + const callFrame = executionItemEntry.callFrames.pop()!; + executionItemEntry.frames = callFrame.frames; + + if (callFrame.returnNode !== null) { + executionItemEntry.frames.push({ + node: callFrame.returnNode, + pages: null, + }); + executionItemEntry.pc = callFrame.returnNode; + } else { + executionItemEntry.pc = + executionItemEntry.frames.length === 0 + ? null + : executionItemEntry.frames[executionItemEntry.frames.length - 1].node; + } + + return getNextElement(); + } + _reset(); return null; } else { @@ -440,6 +483,28 @@ export function getNextElement(): IParsedElement | null { ]; } nextNode = null; + } else if ( + executionItemEntry.pcOverride && + typeof executionItemEntry.pcOverride === 'object' && + (executionItemEntry.pcOverride as IPCRoutineCall).type === '__callroutine__' + ) { + const callSignal = executionItemEntry.pcOverride as IPCRoutineCall; + const routineNode = _routineNodes.find((n) => n.nodeID === callSignal.nodeID); + + if (!routineNode) { + throw Error(`Routine node "${callSignal.nodeID}" not found`); + } + + executionItemEntry.callFrames.push({ + frames: [...frames], + returnNode: (currentNode as TreeNodeStatement | TreeNodeBlock).afterConnection, + }); + + executionItemEntry.frames = [{ node: routineNode, pages: null }]; + + executionItemEntry.pc = routineNode; + executionItemEntry.pcOverride = null; + return; } if (executionItemEntry.pcOverride !== '__rollback__i') { diff --git a/src/index.ts b/src/index.ts index 81a8c1e..4e69ea4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,6 +71,7 @@ export { overrideProgramCounter, releaseProgramCounter, run, + invokeRoutine, } from './execution/interpreter'; // == LIBRARY ======================================================================================