feat(xod-project, xod-arduino): introduce variadic-pass nodes

This commit is contained in:
Evgeny Kochetkov
2021-03-04 16:40:40 +03:00
parent 16dd97cea6
commit f8e0c0b637
15 changed files with 1525 additions and 134 deletions

View File

@@ -794,6 +794,7 @@ const transformProjectWithImpls = def(
R.map(XP.extractBoundInputsToConstNodes(path)),
R.chain(XP.flatten(R.__, path)),
R.map(XP.expandVariadicNodes(path)),
R.map(XP.expandVariadicPassNodes(path)),
R.map(XP.linkifyPatchRecursively(path)),
R.chain(XP.autoresolveTypes(path)),
R.unless(

View File

@@ -23,7 +23,7 @@ const getMarkerNodesErrorMap = (predicate, validator, errorType) => patch => {
mergeAllWithConcat,
R.map(R.objOf(R.__, { [errorType]: [err] }))
)(markerNodeIds),
R.always({ [errorType]: [] }),
R.always({}),
validator(patch)
);
};
@@ -35,12 +35,30 @@ const getMarkerNodesErrorMap = (predicate, validator, errorType) => patch => {
//
// =============================================================================
// :: Patch -> Map NodeId (Map ErrorType [Error])
export const getVariadicMarkersErrorMap = getMarkerNodesErrorMap(
R.pipe(XP.getNodeType, XP.isVariadicPath),
XP.validatePatchForVariadics,
'validatePatchForVariadics'
);
// :: Patch -> Project -> Map NodeId (Map ErrorType [Error])
export const getVariadicMarkersErrorMap = (patch, project) =>
R.compose(
foldEither(
err =>
R.compose(
R.fromPairs,
R.map(markerNodeId => [
markerNodeId,
{ validatePatchForVariadics: [err] },
]),
R.map(XP.getNodeId),
R.filter(
R.pipe(
XP.getNodeType,
R.either(XP.isVariadicPath, XP.isVariadicPassPath)
)
),
XP.listNodes
)(patch),
R.always({})
),
XP.validatePatchForVariadics
)(project, patch);
// :: Patch -> Map NodeId (Map ErrorType [Error])
export const getAbstractMarkersErrorMap = getMarkerNodesErrorMap(

View File

@@ -198,6 +198,18 @@
"@/variadic-3": {
"description": "Makes three rightmost inputs of the patch node containing this node variadic",
"path": "@/variadic-3"
},
"@/variadic-pass-1": {
"description": "Makes the rightmost input of the patch node containing this node variadic, passing the arity level down to the variadic nodes to which they link",
"path": "@/variadic-pass-1"
},
"@/variadic-pass-2": {
"description": "Makes two rightmost inputs of the patch node containing this node variadic, passing the arity level down to the variadic nodes to which they link",
"path": "@/variadic-pass-2"
},
"@/variadic-pass-3": {
"description": "Makes three rightmost inputs of the patch node containing this node variadic, passing the arity level down to the variadic nodes to which they link",
"path": "@/variadic-pass-3"
}
}
}

View File

@@ -22,7 +22,7 @@ const nodeIdLens = R.lens(Node.getNodeId, R.assoc('id'));
// helpers for creating nodes inside expanded patch
const createAdditionalValueTerminalGroups = (
export const createAdditionalValueTerminalGroups = (
patch,
desiredArityLevel,
originalTerminalNodes,

View File

@@ -0,0 +1,231 @@
import * as R from 'ramda';
import { Maybe } from 'ramda-fantasy';
import { explodeEither, isAmong } from 'xod-func-tools';
import { def } from './types';
import * as Pin from './pin';
import * as Node from './node';
import * as Link from './link';
import * as Patch from './patch';
import * as Project from './project';
import {
getExpandedVariadicPatchPath,
isVariadicPassPath,
} from './patchPathUtils';
import { createAdditionalValueTerminalGroups } from './expandVariadicNodes';
const expandPassPatch = R.curry((desiredArityLevel, patch) => {
const expandedPatchPath = R.compose(
getExpandedVariadicPatchPath(desiredArityLevel),
Patch.getPatchPath
)(patch);
// :: {
// acc :: [Pin],
// value :: [Pin],
// shared :: [Pin],
// outputs :: [Pin],
// }
const variadicPins = R.compose(
R.map(R.sortBy(Pin.getPinOrder)),
explodeEither,
Patch.computeVariadicPins
)(patch);
// :: [Node]
const originalTerminalNodes = R.compose(
R.filter(Node.isPinNode),
Patch.listNodes
)(patch);
// :: [ [Node] ]
const additionalValueTerminalGroups = createAdditionalValueTerminalGroups(
patch,
desiredArityLevel,
originalTerminalNodes,
variadicPins
);
const variadicPinKeys = R.compose(R.map(Pin.getPinKey), R.prop('value'))(
variadicPins
);
const linksFromVariadicOutputs = R.compose(
R.filter(R.pipe(Link.getLinkOutputNodeId, isAmong(variadicPinKeys))),
Patch.listLinks
)(patch);
const nodesConnectedToVariadicInputs = R.compose(
R.map(nodeId => Patch.getNodeByIdUnsafe(nodeId, patch)),
R.uniq,
R.map(Link.getLinkInputNodeId)
)(linksFromVariadicOutputs);
const arityLens = R.lens(Node.getNodeArityLevel, Node.setNodeArityLevel);
const variadicNodesWithAddedArity = R.map(
R.over(arityLens, R.add(desiredArityLevel - 1)),
nodesConnectedToVariadicInputs
);
const linksFromAdditionalTerminalsToNodesWithAddedArity = R.compose(
R.chain(terminalGroupIndex =>
R.map(link => {
const inputNodeId = Link.getLinkInputNodeId(link);
const inputPinKey = R.compose(
R.over(Pin.variadicPinKeySuffixLens, R.add(terminalGroupIndex)),
Link.getLinkInputPinKey
)(link);
const outputNodeId = R.compose(
Pin.addVariadicPinKeySuffix(terminalGroupIndex),
Link.getLinkOutputNodeId
)(link);
const outputPinKey = Link.getLinkOutputPinKey(link);
return Link.createLink(
inputPinKey,
inputNodeId,
outputPinKey,
outputNodeId
);
})(linksFromVariadicOutputs)
),
R.range(1) // [1; desiredArityLevel)
)(desiredArityLevel);
const markerNode = R.compose(
R.find(R.pipe(Node.getNodeType, isVariadicPassPath)),
Patch.listNodes
)(patch);
return R.compose(
Patch.dissocNode(markerNode),
Patch.upsertLinks(linksFromAdditionalTerminalsToNodesWithAddedArity),
Patch.upsertNodes([
...R.unnest(additionalValueTerminalGroups),
...variadicNodesWithAddedArity,
]),
Patch.setPatchPath(expandedPatchPath)
)(patch);
});
//
// expand all patches(starting from a specified entry patch)
//
const traverseExpandableQueue = (
queue,
processed,
acc,
processQueueElement
) => {
if (R.isEmpty(queue)) return acc;
const [currentQueueElement, ...restQueueElements] = queue;
if (R.contains(currentQueueElement, processed))
return traverseExpandableQueue(
restQueueElements,
processed,
acc,
processQueueElement
);
const { result, additionalQueueElements } = processQueueElement(
currentQueueElement,
acc
);
const updatedQueue = R.compose(
R.uniq,
R.concat(restQueueElements),
R.difference(R.__, processed)
)(additionalQueueElements);
return traverseExpandableQueue(
updatedQueue,
R.append(currentQueueElement, processed),
result,
processQueueElement
);
};
export default def(
'expandVariadicPassNodes :: PatchPath -> Project -> Project',
(entryPatchPath, initialProject) =>
traverseExpandableQueue(
[entryPatchPath],
[],
initialProject,
(currentPatchPath, project) => {
const initialPatch = Project.getPatchByPathUnsafe(
currentPatchPath,
project
);
const nodesToExpand = R.compose(
R.filter(
R.compose(
Patch.isVariadicPassPatch,
Project.getPatchByPathUnsafe(R.__, project),
Node.getNodeType
)
),
R.filter(R.pipe(Node.getNodeArityLevel, al => al > 1)),
Patch.listNodes
)(initialPatch);
// TODO: short-cirquit if nodesToExpand is empty?
const expandedPatches = R.compose(
R.map(({ patchPath, desiredArityLevel }) =>
R.compose(
expandPassPatch(desiredArityLevel),
Project.getPatchByPathUnsafe(patchPath)
)(project)
),
R.reject(({ patchPath, desiredArityLevel }) =>
R.compose(
Maybe.isJust,
Project.getPatchByPath(
getExpandedVariadicPatchPath(desiredArityLevel, patchPath)
)
)(project)
),
R.uniq,
R.map(
R.applySpec({
patchPath: Node.getNodeType,
desiredArityLevel: Node.getNodeArityLevel,
})
)
)(nodesToExpand);
const updatedPatch = R.compose(
Patch.upsertNodes(R.__, initialPatch),
R.map(node =>
R.compose(
Node.setNodeArityLevel(1),
Node.setNodeType(
getExpandedVariadicPatchPath(
Node.getNodeArityLevel(node),
Node.getNodeType(node)
)
)
)(node)
)
)(nodesToExpand);
const additionalQueueElements = R.compose(
R.map(Node.getNodeType),
Patch.listNodes
)(updatedPatch);
return {
result: Project.upsertPatches(
[updatedPatch, ...expandedPatches],
project
),
additionalQueueElements,
};
}
)
);

View File

@@ -63,7 +63,6 @@ export {
listVariadicValuePins,
listVariadicAccPins,
listVariadicSharedPins,
validatePatchForVariadics,
getArityStepFromPatch,
isVariadicPatch,
isAbstractPatch,
@@ -127,6 +126,7 @@ export {
default as extractBoundInputsToConstNodes,
} from './extractBoundInputsToConstNodes';
export { default as expandVariadicNodes } from './expandVariadicNodes';
export { default as expandVariadicPassNodes } from './expandVariadicPassNodes';
export * from './patchPathUtils';
export * from './versionUtils';
export * from './xodball';

View File

@@ -202,6 +202,12 @@ export default {
note: `A variadic-${arityStep} patch with ${outputCount} outputs should have at least ${minInputs} inputs`,
trace,
}),
NOT_ENOUGH_VARIADIC_PASS_INPUTS: ({ trace, arityStep }) => ({
title: 'Too few variadic inputs',
note: `A variadic-pass-${arityStep} patch should have at least ${arityStep} inputs`,
solution: 'Add inputs or delete the marker to continue.',
trace,
}),
WRONG_VARIADIC_PIN_TYPES: ({ accPinLabels, outPinLabels, trace }) => ({
title: 'Invalid variadic patch',
note: `Types of inputs ${accPinLabels.join(
@@ -214,6 +220,24 @@ export default {
note: `A variadic patch should have at least one output`,
trace,
}),
VARIADIC_PASS_CONNECTED_TO_NON_VARIADIC_NODE: ({
trace,
variadicPassPinLabel,
connectedNodeType,
}) => ({
title: 'Variadic input links to scalar node',
note: `A variadic-pass input ${variadicPassPinLabel} is linked to a non-variadic node ${connectedNodeType}`,
trace,
}),
VARIADIC_PASS_CONNECTED_TO_NON_VARIADIC_PIN: ({
trace,
variadicPassPinLabel,
connectedNodeType,
}) => ({
title: 'Variadic input links to scalar pin of a variadic node',
note: `A variadic-pass input ${variadicPassPinLabel} is linked to a non-variadic pin of node ${connectedNodeType}`,
trace,
}),
// Transpile
IMPLEMENTATION_NOT_FOUND: ({ patchPath, trace }) => ({

View File

@@ -44,6 +44,7 @@ import {
isBuiltInLibName,
isLocalMarker,
isVariadicPath,
isVariadicPassPath,
isExpandedVariadicPatchBasename,
getArityStepFromPatchPath,
getSpecializationPatchPath,
@@ -1226,7 +1227,12 @@ export const patchListEqualsBy = def(
*/
const findVariadicPatchPath = def(
'findVariadicPatchPath :: Patch -> Maybe PatchPath',
R.compose(Maybe, R.find(isVariadicPath), R.map(Node.getNodeType), listNodes)
R.compose(
Maybe,
R.find(R.either(isVariadicPath, isVariadicPassPath)),
R.map(Node.getNodeType),
listNodes
)
);
/**
@@ -1237,6 +1243,11 @@ export const isVariadicPatch = def(
R.compose(Maybe.isJust, findVariadicPatchPath)
);
export const isVariadicPassPatch = def(
'isVariadicPassPatch :: Patch -> Boolean',
R.compose(foldMaybe(false, isVariadicPassPath), findVariadicPatchPath)
);
/**
* Get arity step (1/2/3) from Patch by checking for
* existing of variadic node and extract its arity step from
@@ -1256,7 +1267,7 @@ const checkArityMarkersAmount = def(
R.compose(
R.equals(1),
R.length,
R.filter(isVariadicPath),
R.filter(R.either(isVariadicPath, isVariadicPassPath)),
R.map(Node.getNodeType),
listNodes
)
@@ -1277,10 +1288,12 @@ export const computeVariadicPins = def(
return fail('TOO_MANY_VARIADIC_MARKERS', { trace: [patchPath] });
}
const isVariadicPass = isVariadicPassPatch(patch);
const outputs = listOutputPins(patch);
const outputCount = outputs.length;
if (outputCount === 0) {
if (!isVariadicPass && outputCount === 0) {
return fail('VARIADIC_HAS_NO_OUTPUTS', { trace: [patchPath] });
}
@@ -1293,13 +1306,18 @@ export const computeVariadicPins = def(
getArityStepFromPatch
)(patch);
if (inputCount - arityStep < outputCount) {
const minInputs = R.add(outputCount, arityStep);
const notEnoughVariadicInputs = inputCount - arityStep < outputCount;
if (!isVariadicPass && notEnoughVariadicInputs) {
return fail('NOT_ENOUGH_VARIADIC_INPUTS', {
trace: [patchPath],
arityStep,
outputCount,
minInputs,
minInputs: outputCount + arityStep,
});
} else if (isVariadicPass && inputCount < arityStep) {
return fail('NOT_ENOUGH_VARIADIC_PASS_INPUTS', {
trace: [patchPath],
arityStep,
});
}
@@ -1309,30 +1327,30 @@ export const computeVariadicPins = def(
R.slice(0, R.negate(arityStep))
)(inputs);
const pinLabelsOfNonEqualPinTypes = R.compose(
R.reject(R.isNil),
mapIndexed((accPin, idx) => {
const curPinType = Pin.getPinType(accPin);
const outPinType = Pin.getPinType(outputs[idx]);
return curPinType === outPinType
? null
: [Pin.getPinLabel(accPin), Pin.getPinLabel(outputs[idx])];
})
)(accPins);
if (!isVariadicPass) {
const pinLabelsOfNonEqualPinTypes = R.compose(
R.reject(R.isNil),
mapIndexed((accPin, idx) => {
const curPinType = Pin.getPinType(accPin);
const outPinType = Pin.getPinType(outputs[idx]);
return curPinType === outPinType
? null
: [Pin.getPinLabel(accPin), Pin.getPinLabel(outputs[idx])];
})
)(accPins);
if (notEmpty(pinLabelsOfNonEqualPinTypes)) {
const accPinLabels = R.pluck(0, pinLabelsOfNonEqualPinTypes);
const outPinLabels = R.pluck(1, pinLabelsOfNonEqualPinTypes);
return fail('WRONG_VARIADIC_PIN_TYPES', {
trace: [patchPath],
accPinLabels,
outPinLabels,
});
if (notEmpty(pinLabelsOfNonEqualPinTypes)) {
const accPinLabels = R.pluck(0, pinLabelsOfNonEqualPinTypes);
const outPinLabels = R.pluck(1, pinLabelsOfNonEqualPinTypes);
return fail('WRONG_VARIADIC_PIN_TYPES', {
trace: [patchPath],
accPinLabels,
outPinLabels,
});
}
}
const sharedPins = R.slice(0, R.negate(R.add(accPins.length, arityStep)))(
inputs
);
const sharedPins = R.slice(0, -(accPins.length + arityStep), inputs);
return Either.of({
acc: accPins,
@@ -1372,22 +1390,6 @@ export const listVariadicSharedPins = def(
R.compose(R.pluck('shared'), computeVariadicPins)
);
/**
* Checks a patch for variadic marker existence.
* If it has variadic marker — compute and validate variadic Pins,
* and then return Either Error Patch.
* If not - just return Either.Right Patch.
*/
export const validatePatchForVariadics = def(
'validatePatchForVariadics :: Patch -> Either Error Patch',
patch =>
R.ifElse(
isVariadicPatch,
R.compose(R.map(R.always(patch)), computeVariadicPins),
Either.of
)(patch)
);
/**
* Computes and returns map of pins for Node with additional Pins,
* if Node has `arityLevel > 1` and Patch has a variadic markers.

View File

@@ -208,11 +208,22 @@ const variadicRegExp = new RegExp(`^${PATCH_NODES_LIB_NAME}/variadic-([1-3])`);
// :: PatchPath -> Boolean
export const isVariadicPath = R.test(variadicRegExp);
const variadicPassRegExp = new RegExp(
`^${PATCH_NODES_LIB_NAME}/variadic-pass-([1-3])`
);
// :: PatchPath -> Boolean
export const isVariadicPassPath = R.test(variadicPassRegExp);
const variadicOrPassRegExp = new RegExp(
`^${PATCH_NODES_LIB_NAME}/variadic-(?:pass-)?([1-3])`
);
// :: PatchPath -> ArityStep
export const getArityStepFromPatchPath = R.compose(
x => parseInt(x, 10),
R.nth(1),
R.match(variadicRegExp)
R.match(variadicOrPassRegExp)
);
// :: NonZeroNaturalNumber -> PatchPath

View File

@@ -324,6 +324,28 @@ export const addVariadicPinKeySuffix = def(
(index, key) => `${key}-$${index}`
);
const parseVariadicPinKey = pinKey => {
const [, base, , indexStr = '0'] = R.match(
/([A-Za-z0-9_-]*)(-\$(\d+))?$/,
pinKey
);
return {
base,
index: parseInt(indexStr, 10),
};
};
export const variadicPinKeySuffixLens = R.lens(
R.pipe(parseVariadicPinKey, R.prop('index')),
(newIndex, pinKey) => {
const { base } = parseVariadicPinKey(pinKey);
if (newIndex === 0) return base;
return addVariadicPinKeySuffix(newIndex, base);
}
);
/**
* (!) This function should be called only for variadic pins,
* that should have an updated label.

View File

@@ -736,6 +736,118 @@ const checkPatchForDeadLinksAndPins = def(
}
);
export const validatePatchForVariadics = def(
'validatePatchForVariadics :: Project -> Patch -> Either Error Patch',
(project, validatedPatch) => {
if (!Patch.isVariadicPatch(validatedPatch)) {
return Either.of(validatedPatch);
}
return Patch.computeVariadicPins(validatedPatch).chain(
({ value: variadicPins }) => {
// `computeVariadicPins` checks for all possible errors in "regular" variadic patches.
// We only need to do additional checks for variadic-pass patches.
if (!Patch.isVariadicPassPatch(validatedPatch)) {
return Either.of(validatedPatch);
}
const normalizedPinLabelsByPinKey = R.compose(
R.map(Pin.getPinLabel),
R.indexBy(Pin.getPinKey),
Pin.normalizeEmptyPinLabels,
Patch.listPins
)(validatedPatch);
const variadicPassPinsByKey = R.indexBy(Pin.getPinKey, variadicPins);
return R.compose(
R.map(R.head),
R.sequence(Either.of),
R.map(link => {
const variadicPassPinLabel = R.compose(
R.prop(R.__, normalizedPinLabelsByPinKey),
Link.getLinkOutputNodeId
)(link);
return R.compose(
foldMaybe(
// If we got here, some variadic pins
// are connected to a dead node.
// That's a job for other validators.
Either.of(validatedPatch),
([connectedNodeType, connectedPatch]) => {
if (!Patch.isVariadicPatch(connectedPatch)) {
return fail(
'VARIADIC_PASS_CONNECTED_TO_NON_VARIADIC_NODE',
{
variadicPassPinLabel,
connectedNodeType,
trace: [Patch.getPatchPath(validatedPatch)],
}
);
}
const connectedPatchVariadicPins = Patch.computeVariadicPins(
connectedPatch
);
if (Either.isLeft(connectedPatchVariadicPins)) {
// That's a job for a validator of that particular patch
return Either.of(validatedPatch);
}
return connectedPatchVariadicPins.chain(
({ value: variadicPinsOfConnectedNode }) => {
const inputPinKey = R.compose(
R.set(Pin.variadicPinKeySuffixLens, 0),
Link.getLinkInputPinKey
)(link);
const isInputPinVariadic = R.compose(
R.contains(inputPinKey),
R.map(Pin.getPinKey)
)(variadicPinsOfConnectedNode);
if (isInputPinVariadic) return Either.of(validatedPatch);
return fail(
'VARIADIC_PASS_CONNECTED_TO_NON_VARIADIC_PIN',
{
variadicPassPinLabel,
connectedNodeType,
trace: [Patch.getPatchPath(validatedPatch)],
}
);
}
);
}
),
R.chain(connectedNode => {
const connectedNodeType = Node.getNodeType(connectedNode);
const maybeConnectedPatch = getPatchByPath(
connectedNodeType,
project
);
return R.sequence(Maybe.of, [
Maybe.of(connectedNodeType),
maybeConnectedPatch,
]);
}),
Patch.getNodeById(R.__, validatedPatch),
Link.getLinkInputNodeId
)(link);
}),
R.filter(
R.pipe(Link.getLinkOutputNodeId, R.has(R.__, variadicPassPinsByKey))
),
Patch.listLinks
)(validatedPatch);
}
);
}
);
/**
* Checks `patch` content to be valid:
*
@@ -756,7 +868,7 @@ export const validatePatchContents = def(
.chain(Patch.validateAbstractPatch)
.chain(Patch.validateConstructorPatch)
.chain(Patch.validateRecordPatch)
.chain(Patch.validatePatchForVariadics)
.chain(validatePatchForVariadics(project))
.chain(Patch.validateBuses)
);

View File

@@ -1,110 +1,54 @@
import R from 'ramda';
import { assert } from 'chai';
import { loadXodball } from './helpers';
import * as H from './helpers';
import * as XP from '../src';
import * as Node from '../src/node';
import * as Link from '../src/link';
import * as Patch from '../src/patch';
import * as Project from '../src/project';
// assume that nodes have an unique combination of
// type, label and position
const calculateNodeIdForStructuralComparison = node => {
const type = XP.getNodeType(node);
const label = XP.getNodeLabel(node);
const position = XP.getNodePosition(node);
import expandVariadicNodes from '../src/expandVariadicNodes';
return `${type}~~~${label}~~~${position.x}_${position.y}`;
};
describe('expandVariadicNodes', () => {
it('expands a simple variadic patch', () => {
const project = loadXodball('./fixtures/expanding.xodball');
const expandedProject = expandVariadicNodes('@/main', project);
const project = H.loadXodball('./fixtures/expanding.xodball');
const expandedProject = XP.expandVariadicNodes('@/main', project);
assert.deepEqual(
Project.getPatchByPathUnsafe('@/my-variadic', expandedProject),
Project.getPatchByPathUnsafe('@/my-variadic', project),
XP.getPatchByPathUnsafe('@/my-variadic', expandedProject),
XP.getPatchByPathUnsafe('@/my-variadic', project),
'expanded patch should not change'
);
const expected = loadXodball('./fixtures/expanding.expected.xodball');
const expected = H.loadXodball('./fixtures/expanding.expected.xodball');
assert.sameMembers(
Project.listPatchPaths(expandedProject),
Project.listPatchPaths(expected)
XP.listPatchPaths(expandedProject),
XP.listPatchPaths(expected)
);
assert.deepEqual(
Project.getPatchByPathUnsafe('@/main', expandedProject),
Project.getPatchByPathUnsafe('@/main', expected),
XP.getPatchByPathUnsafe('@/main', expandedProject),
XP.getPatchByPathUnsafe('@/main', expected),
'expanded node type should be updated'
);
const expandedPatch = Project.getPatchByPathUnsafe(
const expandedPatch = XP.getPatchByPathUnsafe(
'@/my-variadic-$5',
expandedProject
);
const expectedExpandedPatch = Project.getPatchByPathUnsafe(
const expectedExpandedPatch = XP.getPatchByPathUnsafe(
'@/my-variadic-$5',
expected
);
assert.deepEqual(
Patch.listPins(expandedPatch),
Patch.listPins(expectedExpandedPatch),
'pins are equal'
);
const [
expandedPatchNonTerminalNodes,
expectedExpandedPatchNonTerminalNodes,
] = R.map(
R.compose(
R.sortBy(R.pipe(Node.getNodePosition, R.prop('x'))),
R.reject(Node.isPinNode),
Patch.listNodes
),
[expandedPatch, expectedExpandedPatch]
);
const omitIds = R.map(R.dissoc('id'));
assert.deepEqual(
omitIds(expandedPatchNonTerminalNodes),
omitIds(expectedExpandedPatchNonTerminalNodes),
'non-terminal nodes are structurally equal'
);
// because nodes are structurally equal,
// we can compare links by replacing node ids
const correspondingExpectedNodeIds = R.compose(
R.fromPairs,
R.map(R.map(Node.getNodeId)),
R.zip
)(expandedPatchNonTerminalNodes, expectedExpandedPatchNonTerminalNodes);
const replaceId = id => correspondingExpectedNodeIds[id] || id;
const expectedExpandedPatchLinks = R.compose(omitIds, Patch.listLinks)(
H.assertPatchesAreStructurallyEqual(
calculateNodeIdForStructuralComparison,
expandedPatch,
expectedExpandedPatch
);
const expandedPatchLinks = R.compose(
omitIds,
R.map(link => {
const inputPinKey = Link.getLinkInputPinKey(link);
const inputNodeId = R.compose(replaceId, Link.getLinkInputNodeId)(link);
const outputPinKey = Link.getLinkOutputPinKey(link);
const outputNodeId = R.compose(replaceId, Link.getLinkOutputNodeId)(
link
);
return Link.createLink(
inputPinKey,
inputNodeId,
outputPinKey,
outputNodeId
);
}),
Patch.listLinks
)(expandedPatch);
assert.deepEqual(
expandedPatchLinks,
expectedExpandedPatchLinks,
'links are structurally equal'
);
});
});

View File

@@ -0,0 +1,52 @@
import { assert } from 'chai';
import * as H from './helpers';
import * as XP from '../src';
// assume that nodes have an unique combination of
// type, label and position
const calculateNodeIdForStructuralComparison = node => {
const type = XP.getNodeType(node);
const label = XP.getNodeLabel(node);
const position = XP.getNodePosition(node);
return `${type}~~~${label}~~~${position.x}_${position.y}`;
};
describe('expandVariadicPassNodes', () => {
it('expands a variadic-pass patch', () => {
const project = H.loadXodball('./fixtures/expanding-variadic-pass.xodball');
const expandedProject = XP.expandVariadicPassNodes('@/main', project);
assert.deepEqual(
XP.getPatchByPathUnsafe('@/my-variadic-pass', expandedProject),
XP.getPatchByPathUnsafe('@/my-variadic-pass', project),
'expanded patch should not change'
);
const expected = H.loadXodball(
'./fixtures/expanding-variadic-pass.expected.xodball'
);
assert.sameMembers(
XP.listPatchPaths(expandedProject),
XP.listPatchPaths(expected)
);
assert.deepEqual(
XP.getPatchByPathUnsafe('@/main', expandedProject),
XP.getPatchByPathUnsafe('@/main', expected),
'expanded node type should be updated'
);
H.assertPatchesAreStructurallyEqual(
calculateNodeIdForStructuralComparison,
XP.getPatchByPathUnsafe('@/my-variadic-pass-$4', expected),
XP.getPatchByPathUnsafe('@/my-variadic-pass-$4', expected)
);
H.assertPatchesAreStructurallyEqual(
calculateNodeIdForStructuralComparison,
XP.getPatchByPathUnsafe('@/my-nested-variadic-pass-$4', expected),
XP.getPatchByPathUnsafe('@/my-nested-variadic-pass-$4', expected)
);
});
});

View File

@@ -0,0 +1,679 @@
{
"patches": {
"@/main": {
"nodes": {
"HJBtpIRzu": {
"id": "HJBtpIRzu",
"type": "@/my-variadic-pass-$4",
"position": {
"x": 1,
"y": 1,
"units": "slots"
},
"boundLiterals": {
"Hyo8pL0zu": "11",
"SkTITICfO": "12",
"Hyo8pL0zu-$1": "21",
"SkTITICfO-$1": "22",
"Hyo8pL0zu-$2": "31",
"SkTITICfO-$2": "32",
"Hyo8pL0zu-$3": "41",
"SkTITICfO-$3": "42"
}
}
},
"path": "@/main"
},
"@/my-variadic-pass": {
"nodes": {
"Skq3nUAGd": {
"id": "Skq3nUAGd",
"type": "@/my-nested-variadic-pass",
"position": {
"x": 3,
"y": 3,
"units": "slots"
}
},
"rJMIpICz_": {
"id": "rJMIpICz_",
"type": "xod/patch-nodes/variadic-pass-2",
"position": {
"x": 6,
"y": 3,
"units": "slots"
}
},
"Hyo8pL0zu": {
"id": "Hyo8pL0zu",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 3,
"y": 1,
"units": "slots"
}
},
"SkTITICfO": {
"id": "SkTITICfO",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 4,
"y": 1,
"units": "slots"
}
},
"rJ3d68AzO": {
"id": "rJ3d68AzO",
"type": "xod/patch-nodes/output-number",
"position": {
"x": 3,
"y": 5,
"units": "slots"
}
}
},
"links": {
"S1VDTURGO": {
"id": "S1VDTURGO",
"output": {
"nodeId": "Hyo8pL0zu",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "Hk5io80G_"
}
},
"SyHPTIRf_": {
"id": "SyHPTIRf_",
"output": {
"nodeId": "SkTITICfO",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "HkH6nIAf_"
}
},
"ByRuT8CfO": {
"id": "ByRuT8CfO",
"output": {
"nodeId": "Skq3nUAGd",
"pinKey": "ry-bhIRfu"
},
"input": {
"nodeId": "rJ3d68AzO",
"pinKey": "__in__"
}
}
},
"path": "@/my-variadic-pass"
},
"@/my-nested-variadic-pass": {
"nodes": {
"BJYOjU0fd": {
"id": "BJYOjU0fd",
"type": "@/foo",
"position": {
"x": 0,
"y": 2,
"units": "slots"
},
"boundLiterals": {
"H10LiLCGu": "42",
"HJzkaICfu": "1337"
},
"arityLevel": 2
},
"Hk5io80G_": {
"id": "Hk5io80G_",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 3,
"y": 0,
"units": "slots"
}
},
"ry-bhIRfu": {
"id": "ry-bhIRfu",
"type": "xod/patch-nodes/output-number",
"position": {
"x": 0,
"y": 4,
"units": "slots"
}
},
"HkH6nIAf_": {
"id": "HkH6nIAf_",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 4,
"y": 0,
"units": "slots"
}
},
"rkOfaICMO": {
"id": "rkOfaICMO",
"type": "xod/patch-nodes/variadic-pass-2",
"position": {
"x": 7,
"y": 2,
"units": "slots"
}
}
},
"links": {
"Sy_W2UAMO": {
"id": "Sy_W2UAMO",
"output": {
"nodeId": "BJYOjU0fd",
"pinKey": "rk7UiIRf_"
},
"input": {
"nodeId": "ry-bhIRfu",
"pinKey": "__in__"
}
},
"Skf7aUAG_": {
"id": "Skf7aUAG_",
"output": {
"nodeId": "Hk5io80G_",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "HJzkaICfu-$1"
}
},
"BkmQTU0f_": {
"id": "BkmQTU0f_",
"output": {
"nodeId": "HkH6nIAf_",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "H10LiLCGu-$1"
}
}
},
"path": "@/my-nested-variadic-pass"
},
"@/foo": {
"nodes": {
"SyjVsIRfO": {
"id": "SyjVsIRfO",
"type": "xod/patch-nodes/utility",
"position": {
"x": 8,
"y": 3,
"units": "slots"
}
},
"B1vrs8AzO": {
"id": "B1vrs8AzO",
"type": "xod/patch-nodes/not-implemented-in-xod",
"position": {
"x": 1,
"y": 3,
"units": "slots"
}
},
"BJTSsU0Mu": {
"id": "BJTSsU0Mu",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 1,
"y": 1,
"units": "slots"
},
"label": "ACC"
},
"rk7UiIRf_": {
"id": "rk7UiIRf_",
"type": "xod/patch-nodes/output-number",
"position": {
"x": 1,
"y": 5,
"units": "slots"
}
},
"H10LiLCGu": {
"id": "H10LiLCGu",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 4,
"y": 1,
"units": "slots"
},
"label": "V1"
},
"rJJypU0GO": {
"id": "rJJypU0GO",
"type": "xod/patch-nodes/variadic-2",
"position": {
"x": 5,
"y": 3,
"units": "slots"
}
},
"HJzkaICfu": {
"id": "HJzkaICfu",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 7,
"y": 1,
"units": "slots"
},
"label": "V2"
}
},
"path": "@/foo",
"attachments": [
{
"filename": "patch.cpp",
"encoding": "utf-8",
"content": "\nnode {\n void evaluate(Context ctx) {}\n}\n"
}
]
},
"@/my-variadic-pass-$4": {
"nodes": {
"Skq3nUAGd": {
"id": "Skq3nUAGd",
"type": "@/my-nested-variadic-pass-$4",
"position": {
"x": 3,
"y": 3,
"units": "slots"
}
},
"Hyo8pL0zu": {
"id": "Hyo8pL0zu",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 3,
"y": 1,
"units": "slots"
}
},
"SkTITICfO": {
"id": "SkTITICfO",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 4,
"y": 1,
"units": "slots"
}
},
"rJ3d68AzO": {
"id": "rJ3d68AzO",
"type": "xod/patch-nodes/output-number",
"position": {
"x": 3,
"y": 5,
"units": "slots"
}
},
"Hyo8pL0zu-$1": {
"id": "Hyo8pL0zu-$1",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 5,
"y": 0,
"units": "slots"
}
},
"SkTITICfO-$1": {
"id": "SkTITICfO-$1",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 6,
"y": 0,
"units": "slots"
}
},
"Hyo8pL0zu-$2": {
"id": "Hyo8pL0zu-$2",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 7,
"y": 0,
"units": "slots"
}
},
"SkTITICfO-$2": {
"id": "SkTITICfO-$2",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 8,
"y": 0,
"units": "slots"
}
},
"Hyo8pL0zu-$3": {
"id": "Hyo8pL0zu-$3",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 9,
"y": 0,
"units": "slots"
}
},
"SkTITICfO-$3": {
"id": "SkTITICfO-$3",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 10,
"y": 0,
"units": "slots"
}
}
},
"links": {
"S1VDTURGO": {
"id": "S1VDTURGO",
"output": {
"nodeId": "Hyo8pL0zu",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "Hk5io80G_"
}
},
"SyHPTIRf_": {
"id": "SyHPTIRf_",
"output": {
"nodeId": "SkTITICfO",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "HkH6nIAf_"
}
},
"ByRuT8CfO": {
"id": "ByRuT8CfO",
"output": {
"nodeId": "Skq3nUAGd",
"pinKey": "ry-bhIRfu"
},
"input": {
"nodeId": "rJ3d68AzO",
"pinKey": "__in__"
}
},
"BywLCLCfO": {
"id": "BywLCLCfO",
"output": {
"nodeId": "Hyo8pL0zu-$1",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "Hk5io80G_-$1"
}
},
"BklD8CL0zd": {
"id": "BklD8CL0zd",
"output": {
"nodeId": "SkTITICfO-$1",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "HkH6nIAf_-$1"
}
},
"ryWPI0LCzu": {
"id": "ryWPI0LCzu",
"output": {
"nodeId": "Hyo8pL0zu-$2",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "Hk5io80G_-$2"
}
},
"SJzDL0UAfu": {
"id": "SJzDL0UAfu",
"output": {
"nodeId": "SkTITICfO-$2",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "HkH6nIAf_-$2"
}
},
"HyQvLA8Rfd": {
"id": "HyQvLA8Rfd",
"output": {
"nodeId": "Hyo8pL0zu-$3",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "Hk5io80G_-$3"
}
},
"H1Ew808AGO": {
"id": "H1Ew808AGO",
"output": {
"nodeId": "SkTITICfO-$3",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "HkH6nIAf_-$3"
}
}
},
"path": "@/my-variadic-pass-$4"
},
"@/my-nested-variadic-pass-$4": {
"nodes": {
"BJYOjU0fd": {
"id": "BJYOjU0fd",
"type": "@/foo",
"position": {
"x": 0,
"y": 2,
"units": "slots"
},
"boundLiterals": {
"H10LiLCGu": "42",
"HJzkaICfu": "1337"
},
"arityLevel": 5
},
"Hk5io80G_": {
"id": "Hk5io80G_",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 3,
"y": 0,
"units": "slots"
}
},
"ry-bhIRfu": {
"id": "ry-bhIRfu",
"type": "xod/patch-nodes/output-number",
"position": {
"x": 0,
"y": 4,
"units": "slots"
}
},
"HkH6nIAf_": {
"id": "HkH6nIAf_",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 4,
"y": 0,
"units": "slots"
}
},
"Hk5io80G_-$1": {
"id": "Hk5io80G_-$1",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 5,
"y": 0,
"units": "slots"
}
},
"HkH6nIAf_-$1": {
"id": "HkH6nIAf_-$1",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 6,
"y": 0,
"units": "slots"
}
},
"Hk5io80G_-$2": {
"id": "Hk5io80G_-$2",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 7,
"y": 0,
"units": "slots"
}
},
"HkH6nIAf_-$2": {
"id": "HkH6nIAf_-$2",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 8,
"y": 0,
"units": "slots"
}
},
"Hk5io80G_-$3": {
"id": "Hk5io80G_-$3",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 9,
"y": 0,
"units": "slots"
}
},
"HkH6nIAf_-$3": {
"id": "HkH6nIAf_-$3",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 10,
"y": 0,
"units": "slots"
}
}
},
"links": {
"Sy_W2UAMO": {
"id": "Sy_W2UAMO",
"output": {
"nodeId": "BJYOjU0fd",
"pinKey": "rk7UiIRf_"
},
"input": {
"nodeId": "ry-bhIRfu",
"pinKey": "__in__"
}
},
"Skf7aUAG_": {
"id": "Skf7aUAG_",
"output": {
"nodeId": "Hk5io80G_",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "HJzkaICfu-$1"
}
},
"BkmQTU0f_": {
"id": "BkmQTU0f_",
"output": {
"nodeId": "HkH6nIAf_",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "H10LiLCGu-$1"
}
},
"SJHPU0LAzO": {
"id": "SJHPU0LAzO",
"output": {
"nodeId": "Hk5io80G_-$1",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "HJzkaICfu-$2"
}
},
"Sy8w8RUAGu": {
"id": "Sy8w8RUAGu",
"output": {
"nodeId": "HkH6nIAf_-$1",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "H10LiLCGu-$2"
}
},
"HyDvIAI0Gu": {
"id": "HyDvIAI0Gu",
"output": {
"nodeId": "Hk5io80G_-$2",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "HJzkaICfu-$3"
}
},
"H1uP8AURfu": {
"id": "H1uP8AURfu",
"output": {
"nodeId": "HkH6nIAf_-$2",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "H10LiLCGu-$3"
}
},
"S1KvURIRGO": {
"id": "S1KvURIRGO",
"output": {
"nodeId": "Hk5io80G_-$3",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "HJzkaICfu-$4"
}
},
"S1cw80LRG_": {
"id": "S1cw80LRG_",
"output": {
"nodeId": "HkH6nIAf_-$3",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "H10LiLCGu-$4"
}
}
},
"path": "@/my-nested-variadic-pass-$4"
}
},
"name": ""
}

View File

@@ -0,0 +1,283 @@
{
"patches": {
"@/main": {
"nodes": {
"HJBtpIRzu": {
"id": "HJBtpIRzu",
"type": "@/my-variadic-pass",
"position": {
"x": 1,
"y": 1,
"units": "slots"
},
"boundLiterals": {
"Hyo8pL0zu": "11",
"SkTITICfO": "12",
"Hyo8pL0zu-$1": "21",
"SkTITICfO-$1": "22",
"Hyo8pL0zu-$2": "31",
"SkTITICfO-$2": "32",
"Hyo8pL0zu-$3": "41",
"SkTITICfO-$3": "42"
},
"arityLevel": 4
}
},
"path": "@/main"
},
"@/my-variadic-pass": {
"nodes": {
"Skq3nUAGd": {
"id": "Skq3nUAGd",
"type": "@/my-nested-variadic-pass",
"position": {
"x": 3,
"y": 3,
"units": "slots"
}
},
"rJMIpICz_": {
"id": "rJMIpICz_",
"type": "xod/patch-nodes/variadic-pass-2",
"position": {
"x": 6,
"y": 3,
"units": "slots"
}
},
"Hyo8pL0zu": {
"id": "Hyo8pL0zu",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 3,
"y": 1,
"units": "slots"
}
},
"SkTITICfO": {
"id": "SkTITICfO",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 4,
"y": 1,
"units": "slots"
}
},
"rJ3d68AzO": {
"id": "rJ3d68AzO",
"type": "xod/patch-nodes/output-number",
"position": {
"x": 3,
"y": 5,
"units": "slots"
}
}
},
"links": {
"S1VDTURGO": {
"id": "S1VDTURGO",
"output": {
"nodeId": "Hyo8pL0zu",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "Hk5io80G_"
}
},
"SyHPTIRf_": {
"id": "SyHPTIRf_",
"output": {
"nodeId": "SkTITICfO",
"pinKey": "__out__"
},
"input": {
"nodeId": "Skq3nUAGd",
"pinKey": "HkH6nIAf_"
}
},
"ByRuT8CfO": {
"id": "ByRuT8CfO",
"output": {
"nodeId": "Skq3nUAGd",
"pinKey": "ry-bhIRfu"
},
"input": {
"nodeId": "rJ3d68AzO",
"pinKey": "__in__"
}
}
},
"path": "@/my-variadic-pass"
},
"@/my-nested-variadic-pass": {
"nodes": {
"BJYOjU0fd": {
"id": "BJYOjU0fd",
"type": "@/foo",
"position": {
"x": 0,
"y": 2,
"units": "slots"
},
"boundLiterals": {
"H10LiLCGu": "42",
"HJzkaICfu": "1337"
},
"arityLevel": 2
},
"Hk5io80G_": {
"id": "Hk5io80G_",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 3,
"y": 0,
"units": "slots"
}
},
"ry-bhIRfu": {
"id": "ry-bhIRfu",
"type": "xod/patch-nodes/output-number",
"position": {
"x": 0,
"y": 4,
"units": "slots"
}
},
"HkH6nIAf_": {
"id": "HkH6nIAf_",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 4,
"y": 0,
"units": "slots"
}
},
"rkOfaICMO": {
"id": "rkOfaICMO",
"type": "xod/patch-nodes/variadic-pass-2",
"position": {
"x": 7,
"y": 2,
"units": "slots"
}
}
},
"links": {
"Sy_W2UAMO": {
"id": "Sy_W2UAMO",
"output": {
"nodeId": "BJYOjU0fd",
"pinKey": "rk7UiIRf_"
},
"input": {
"nodeId": "ry-bhIRfu",
"pinKey": "__in__"
}
},
"Skf7aUAG_": {
"id": "Skf7aUAG_",
"output": {
"nodeId": "Hk5io80G_",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "HJzkaICfu-$1"
}
},
"BkmQTU0f_": {
"id": "BkmQTU0f_",
"output": {
"nodeId": "HkH6nIAf_",
"pinKey": "__out__"
},
"input": {
"nodeId": "BJYOjU0fd",
"pinKey": "H10LiLCGu-$1"
}
}
},
"path": "@/my-nested-variadic-pass"
},
"@/foo": {
"nodes": {
"SyjVsIRfO": {
"id": "SyjVsIRfO",
"type": "xod/patch-nodes/utility",
"position": {
"x": 8,
"y": 3,
"units": "slots"
}
},
"B1vrs8AzO": {
"id": "B1vrs8AzO",
"type": "xod/patch-nodes/not-implemented-in-xod",
"position": {
"x": 1,
"y": 3,
"units": "slots"
}
},
"BJTSsU0Mu": {
"id": "BJTSsU0Mu",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 1,
"y": 1,
"units": "slots"
},
"label": "ACC"
},
"rk7UiIRf_": {
"id": "rk7UiIRf_",
"type": "xod/patch-nodes/output-number",
"position": {
"x": 1,
"y": 5,
"units": "slots"
}
},
"H10LiLCGu": {
"id": "H10LiLCGu",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 4,
"y": 1,
"units": "slots"
},
"label": "V1"
},
"rJJypU0GO": {
"id": "rJJypU0GO",
"type": "xod/patch-nodes/variadic-2",
"position": {
"x": 5,
"y": 3,
"units": "slots"
}
},
"HJzkaICfu": {
"id": "HJzkaICfu",
"type": "xod/patch-nodes/input-number",
"position": {
"x": 7,
"y": 1,
"units": "slots"
},
"label": "V2"
}
},
"path": "@/foo",
"attachments": [
{
"filename": "patch.cpp",
"encoding": "utf-8",
"content": "\nnode {\n void evaluate(Context ctx) {}\n}\n"
}
]
}
},
"name": ""
}