Skip to content

Commit

Permalink
Merge pull request #411 from jvalue/composite-blocktypes
Browse files Browse the repository at this point in the history
Composite blocktypes: Basic functionality
  • Loading branch information
rhazn authored Aug 30, 2023
2 parents d08b6ba + 9fba159 commit 458b64e
Show file tree
Hide file tree
Showing 41 changed files with 731 additions and 179 deletions.
115 changes: 115 additions & 0 deletions libs/execution/src/lib/blocks/block-execution-util.ts
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.`);
}
33 changes: 29 additions & 4 deletions libs/execution/src/lib/blocks/block-executor-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@

import { strict as assert } from 'assert';

import { BlockDefinition, Registry } from '@jvalue/jayvee-language-server';
import {
BlockDefinition,
Registry,
isCompositeBlocktypeDefinition,
} from '@jvalue/jayvee-language-server';

import { BlockExecutor } from './block-executor';
import { BlockExecutorClass } from './block-executor-class';
// eslint-disable-next-line import/no-cycle
import {
createCompositeBlockExecutor,
getInputType,
getOutputType,
} from './composite-block-executor';

export const blockExecutorRegistry = new Registry<BlockExecutorClass>();

Expand All @@ -20,12 +30,27 @@ export function getRegisteredBlockExecutors(): BlockExecutorClass[] {
}

export function createBlockExecutor(block: BlockDefinition): BlockExecutor {
const blockType = block.type.ref?.name;
const blockType = block.type.ref;
assert(blockType !== undefined);
const blockExecutor = blockExecutorRegistry.get(blockType);

if (
!blockExecutorRegistry.get(blockType.name) &&
isCompositeBlocktypeDefinition(block.type.ref)
) {
const executorClass = createCompositeBlockExecutor(
getInputType(block.type.ref),
getOutputType(block.type.ref),
block,
);

blockExecutorRegistry.register(block.type.ref.name, executorClass);
}

const blockExecutor = blockExecutorRegistry.get(blockType.name);

assert(
blockExecutor !== undefined,
`No executor was registered for block type ${blockType}`,
`No executor was registered for block type ${blockType.name}`,
);

return new blockExecutor();
Expand Down
219 changes: 219 additions & 0 deletions libs/execution/src/lib/blocks/composite-block-executor.ts
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;
}
1 change: 1 addition & 0 deletions libs/execution/src/lib/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './block-executor';
export * from './block-executor-class';
export * from './block-executor-registry';
export * from './execution-result';
export * from './block-execution-util';
Loading

0 comments on commit 458b64e

Please sign in to comment.