-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #411 from jvalue/composite-blocktypes
Composite blocktypes: Basic functionality
- Loading branch information
Showing
41 changed files
with
731 additions
and
179 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg | ||
// | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
import { | ||
BlockDefinition, | ||
collectParents, | ||
} from '@jvalue/jayvee-language-server'; | ||
|
||
import { ExecutionContext } from '../execution-context'; | ||
import { Logger } from '../logger'; | ||
import { IOTypeImplementation, NONE } from '../types'; | ||
|
||
// eslint-disable-next-line import/no-cycle | ||
import { createBlockExecutor } from './block-executor-registry'; | ||
import * as R from './execution-result'; | ||
|
||
export interface ExecutionOrderItem { | ||
block: BlockDefinition; | ||
value: IOTypeImplementation | null; | ||
} | ||
|
||
/** | ||
* Executes an ordered list of blocks in sequence, using outputs from previous blocks as inputs for downstream blocks. | ||
* | ||
* @param executionContext The context the blocks are executed in, e.g., a pipeline or composite block | ||
* @param executionOrder An ordered list of blocks so that blocks that need inputs are after blocks that produce these inputs | ||
* @param initialInputValue An initial input that was produced outside of this block chain, e.g., as input to a composite block | ||
* | ||
* @returns The ordered blocks and their produced outputs or an error on failure | ||
*/ | ||
export async function executeBlocks( | ||
executionContext: ExecutionContext, | ||
executionOrder: ExecutionOrderItem[], | ||
initialInputValue: IOTypeImplementation | undefined = undefined, | ||
): Promise<R.Result<ExecutionOrderItem[]>> { | ||
let isFirstBlock = true; | ||
|
||
for (const blockData of executionOrder) { | ||
const block = blockData.block; | ||
const parentData = collectParents(block).map((parent) => | ||
executionOrder.find((blockData) => parent === blockData.block), | ||
); | ||
let inputValue = | ||
parentData[0]?.value === undefined ? NONE : parentData[0]?.value; | ||
|
||
const useExternalInputValueForFirstBlock = | ||
isFirstBlock && inputValue === NONE && initialInputValue !== undefined; | ||
|
||
if (useExternalInputValueForFirstBlock) { | ||
inputValue = initialInputValue; | ||
} | ||
|
||
executionContext.enterNode(block); | ||
|
||
const executionResult = await executeBlock( | ||
inputValue, | ||
block, | ||
executionContext, | ||
); | ||
if (R.isErr(executionResult)) { | ||
return executionResult; | ||
} | ||
const blockResultData = executionResult.right; | ||
blockData.value = blockResultData; | ||
|
||
executionContext.exitNode(block); | ||
isFirstBlock = false; | ||
} | ||
return R.ok(executionOrder); | ||
} | ||
|
||
export async function executeBlock( | ||
inputValue: IOTypeImplementation | null, | ||
block: BlockDefinition, | ||
executionContext: ExecutionContext, | ||
): Promise<R.Result<IOTypeImplementation | null>> { | ||
if (inputValue == null) { | ||
executionContext.logger.logInfoDiagnostic( | ||
`Skipped execution because parent block emitted no value.`, | ||
{ node: block, property: 'name' }, | ||
); | ||
return R.ok(null); | ||
} | ||
|
||
const blockExecutor = createBlockExecutor(block); | ||
|
||
const startTime = new Date(); | ||
|
||
let result: R.Result<IOTypeImplementation | null>; | ||
try { | ||
result = await blockExecutor.execute(inputValue, executionContext); | ||
} catch (unexpectedError) { | ||
return R.err({ | ||
message: `An unknown error occurred: ${ | ||
unexpectedError instanceof Error | ||
? unexpectedError.stack ?? unexpectedError.message | ||
: JSON.stringify(unexpectedError) | ||
}`, | ||
diagnostic: { node: block, property: 'name' }, | ||
}); | ||
} | ||
|
||
logExecutionDuration(startTime, executionContext.logger); | ||
|
||
return result; | ||
} | ||
|
||
export function logExecutionDuration(startTime: Date, logger: Logger): void { | ||
const endTime = new Date(); | ||
const executionDurationMs = Math.round( | ||
endTime.getTime() - startTime.getTime(), | ||
); | ||
logger.logDebug(`Execution duration: ${executionDurationMs} ms.`); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
219 changes: 219 additions & 0 deletions
219
libs/execution/src/lib/blocks/composite-block-executor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg | ||
// | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
import { strict as assert } from 'assert/strict'; | ||
|
||
import { | ||
BlockDefinition, | ||
BlocktypePipeline, | ||
BlocktypeProperty, | ||
CompositeBlocktypeDefinition, | ||
EvaluationContext, | ||
IOType, | ||
InternalValueRepresentation, | ||
Valuetype, | ||
createValuetype, | ||
evaluateExpression, | ||
evaluatePropertyValue, | ||
getBlocksInTopologicalSorting, | ||
getIOType, | ||
isCompositeBlocktypeDefinition, | ||
} from '@jvalue/jayvee-language-server'; | ||
|
||
import { ExecutionContext } from '../execution-context'; | ||
import { IOTypeImplementation, NONE } from '../types'; | ||
|
||
// eslint-disable-next-line import/no-cycle | ||
import { executeBlocks } from './block-execution-util'; | ||
import { AbstractBlockExecutor, BlockExecutor } from './block-executor'; | ||
import { BlockExecutorClass } from './block-executor-class'; | ||
import * as R from './execution-result'; | ||
|
||
export function createCompositeBlockExecutor( | ||
inputType: IOType, | ||
outputType: IOType, | ||
block: BlockDefinition, | ||
): BlockExecutorClass<BlockExecutor<IOType, IOType>> { | ||
assert( | ||
block.type.ref, | ||
`Blocktype reference missing for block ${block.name}.`, | ||
); | ||
|
||
assert( | ||
isCompositeBlocktypeDefinition(block.type.ref), | ||
`Blocktype is not a composite block for block ${block.name}.`, | ||
); | ||
|
||
const blockTypeReference = block.type.ref; | ||
|
||
return class extends AbstractBlockExecutor< | ||
typeof inputType, | ||
typeof outputType | ||
> { | ||
public static readonly type = blockTypeReference.name; | ||
|
||
constructor() { | ||
super(inputType, outputType); | ||
} | ||
|
||
async doExecute( | ||
input: IOTypeImplementation<typeof inputType>, | ||
context: ExecutionContext, | ||
): Promise<R.Result<IOTypeImplementation<typeof outputType> | null>> { | ||
context.logger.logDebug( | ||
`Executing composite block of type ${ | ||
block.type.ref?.name ?? 'undefined' | ||
}`, | ||
); | ||
|
||
this.addVariablesToContext(block, blockTypeReference.properties, context); | ||
|
||
const executionOrder = getBlocksInTopologicalSorting( | ||
blockTypeReference, | ||
).map((block) => { | ||
return { block: block, value: NONE }; | ||
}); | ||
|
||
const executionResult = await executeBlocks( | ||
context, | ||
executionOrder, | ||
input, | ||
); | ||
|
||
if (R.isErr(executionResult)) { | ||
const diagnosticError = executionResult.left; | ||
context.logger.logErrDiagnostic( | ||
diagnosticError.message, | ||
diagnosticError.diagnostic, | ||
); | ||
} | ||
|
||
this.removeVariablesFromContext(blockTypeReference.properties, context); | ||
|
||
if (R.isOk(executionResult)) { | ||
// The last block always pipes into the output if it exists | ||
const pipeline = getPipeline(blockTypeReference); | ||
const lastBlock = pipeline.blocks.at(-1); | ||
|
||
const blockExecutionResult = R.okData(executionResult).find( | ||
(result) => result.block.name === lastBlock?.ref?.name, | ||
); | ||
|
||
assert( | ||
blockExecutionResult, | ||
`No execution result found for composite block ${ | ||
block.type.ref?.name ?? 'undefined' | ||
}`, | ||
); | ||
|
||
return R.ok(blockExecutionResult.value); | ||
} | ||
|
||
return R.ok(null); | ||
} | ||
|
||
private removeVariablesFromContext( | ||
properties: BlocktypeProperty[], | ||
context: ExecutionContext, | ||
) { | ||
properties.forEach((prop) => | ||
context.evaluationContext.deleteValueForReference(prop.name), | ||
); | ||
} | ||
|
||
private addVariablesToContext( | ||
block: BlockDefinition, | ||
properties: BlocktypeProperty[], | ||
context: ExecutionContext, | ||
) { | ||
properties.forEach((blocktypeProperty) => { | ||
const valueType = createValuetype(blocktypeProperty.valueType); | ||
|
||
assert( | ||
valueType, | ||
`Can not create valuetype for blocktype property ${blocktypeProperty.name}`, | ||
); | ||
|
||
const propertyValue = this.getPropertyValueFromBlockOrDefault( | ||
blocktypeProperty.name, | ||
valueType, | ||
block, | ||
properties, | ||
context.evaluationContext, | ||
); | ||
|
||
assert( | ||
propertyValue !== undefined, | ||
`Can not get value for blocktype property ${blocktypeProperty.name}`, | ||
); | ||
|
||
context.evaluationContext.setValueForReference( | ||
blocktypeProperty.name, | ||
propertyValue, | ||
); | ||
}); | ||
} | ||
|
||
private getPropertyValueFromBlockOrDefault( | ||
name: string, | ||
valueType: Valuetype, | ||
block: BlockDefinition, | ||
properties: BlocktypeProperty[], | ||
evaluationContext: EvaluationContext, | ||
): InternalValueRepresentation | undefined { | ||
const propertyFromBlock = block.body.properties.find( | ||
(property) => property.name === name, | ||
); | ||
|
||
if (propertyFromBlock !== undefined) { | ||
const value = evaluatePropertyValue( | ||
propertyFromBlock, | ||
evaluationContext, | ||
valueType, | ||
); | ||
|
||
if (value !== undefined) { | ||
return value; | ||
} | ||
} | ||
|
||
const propertyFromBlockType = properties.find( | ||
(property) => property.name === name, | ||
); | ||
|
||
if (propertyFromBlockType?.defaultValue === undefined) { | ||
return; | ||
} | ||
|
||
return evaluateExpression( | ||
propertyFromBlockType.defaultValue, | ||
evaluationContext, | ||
); | ||
} | ||
}; | ||
} | ||
|
||
function getPipeline(block: CompositeBlocktypeDefinition): BlocktypePipeline { | ||
assert( | ||
block.pipes[0], | ||
`Composite block ${block.name} must have exactly one pipeline.`, | ||
); | ||
return block.pipes[0]; | ||
} | ||
|
||
export function getInputType(block: CompositeBlocktypeDefinition): IOType { | ||
assert( | ||
block.inputs.length === 1, | ||
`Composite block ${block.name} must have exactly one input.`, | ||
); | ||
return block.inputs[0] ? getIOType(block.inputs[0]) : IOType.NONE; | ||
} | ||
|
||
export function getOutputType(block: CompositeBlocktypeDefinition): IOType { | ||
assert( | ||
block.outputs.length === 1, | ||
`Composite block ${block.name} must have exactly one output.`, | ||
); | ||
return block.outputs[0] ? getIOType(block.outputs[0]) : IOType.NONE; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.