diff --git a/flowming/src/components/Flow/FlowContent.tsx b/flowming/src/components/Flow/FlowContent.tsx
index c3ecec2..eacf4f9 100644
--- a/flowming/src/components/Flow/FlowContent.tsx
+++ b/flowming/src/components/Flow/FlowContent.tsx
@@ -25,7 +25,7 @@ import { NodeBlock } from '../Toolbar/ToolbarTypes';
import ContextMenu from './ContextMenu';
import { useDnD } from '../../context/DnDContext';
import { useFlowExecutorState } from '../../context/FlowExecutorContext';
-import { Expression, Variable } from '../../models';
+import { Expression, Variable, ExpressionElement } from '../../models';
import { decisionEdgeLabels } from './Nodes/Conditional';
import FilenameEditor from '../FilenameEditor';
import { useCollaboration } from '../../context/CollaborationContext';
@@ -659,6 +659,40 @@ const FlowContent: React.FC = () => {
if (!needsUpdate) return;
+ // Helper to recursively update ExpressionElement lists (array index expressions)
+ const updateElements = (elements: any[]): ExpressionElement[] => {
+ return elements.flatMap((rawElem: any) => {
+ // Ensure we are working with an ExpressionElement instance (objects coming from Yjs/serialization won't have class methods)
+ const elem: ExpressionElement = rawElem instanceof ExpressionElement
+ ? rawElem
+ : ExpressionElement.fromObject(rawElem);
+
+ if (elem.type !== 'variable') {
+ // Propagate into nested function calls, if any
+ if (elem.isFunction() && elem.nestedExpression) {
+ elem.nestedExpression.rightSide = updateElements(elem.nestedExpression.rightSide);
+ }
+ return [elem];
+ }
+
+ const match = variables.find(v => v.id === elem.variable?.id);
+ if (!match) return []; // Variable deleted -> drop element
+
+ const clonedVar = match.clone();
+ if (elem.variable?.indexExpression && clonedVar.type === 'array') {
+ const newIdx = updateElements(elem.variable.indexExpression as ExpressionElement[]);
+ if (newIdx.length > 0) clonedVar.indexExpression = newIdx;
+ }
+
+ // If index expression became empty, remove the whole variable element
+ if (elem.variable?.indexExpression && (!clonedVar.indexExpression || clonedVar.indexExpression.length === 0)) {
+ return [];
+ }
+
+ return [new ExpressionElement(elem.id, elem.type, clonedVar.toString(), clonedVar)];
+ });
+ };
+
setNodes(prevNodes => prevNodes.map(node => {
if ((node.type === 'AssignVariable' || node.type === 'Conditional' || node.type === 'Output') && node.data.expression) {
try {
@@ -668,13 +702,59 @@ const FlowContent: React.FC = () => {
const expression = Expression.fromObject(currentExprData);
expression.updateVariables(variables);
+ // Cleanup for left-side Variable (especially array element assignments)
+ if (expression.leftSide instanceof Variable) {
+ const leftSideId = expression.leftSide.id;
+ const leftMatch = variables.find(v => v.id === leftSideId);
+
+ if (!leftMatch) {
+ expression.leftSide = undefined; // Variable removed entirely
+ } else {
+ const leftClone = leftMatch.clone();
+
+ // Recursively update its index expression (if any)
+ if (expression.leftSide.indexExpression && expression.leftSide.indexExpression.length > 0) {
+ const updatedIdx = updateElements(expression.leftSide.indexExpression as ExpressionElement[]);
+ if (updatedIdx.length > 0) {
+ leftClone.indexExpression = updatedIdx;
+ }
+ }
+
+ // If after update the index expression is gone, drop the left side reference
+ if (expression.leftSide.indexExpression && (!leftClone.indexExpression || leftClone.indexExpression.length === 0)) {
+ expression.leftSide = undefined;
+ } else {
+ expression.leftSide = leftClone;
+ }
+ }
+ } else if (Array.isArray(expression.leftSide)) {
+ const cleanedLeft = updateElements(expression.leftSide as ExpressionElement[]);
+ if (cleanedLeft.length > 0) {
+ expression.leftSide = cleanedLeft;
+ } else {
+ expression.leftSide = undefined;
+ }
+ }
+
return { ...node, data: { ...node.data, expression: expression.toObject() } };
} catch {
return { ...node, data: { ...node.data, expression: null } };
}
} else if (node.type === 'Input' && node.data.variable) {
const updatedVariable = variables.find(v => v.id === node.data.variable?.id);
- return { ...node, data: { ...node.data, variable: updatedVariable ? JSON.parse(JSON.stringify(updatedVariable)) : undefined } };
+ if (updatedVariable) {
+ const varCopy = updatedVariable.clone();
+ if (node.data.variable.indexExpression && node.data.variable.indexExpression.length > 0) {
+ const updatedIdx = updateElements(node.data.variable.indexExpression as ExpressionElement[]);
+ if (updatedIdx.length > 0) {
+ varCopy.indexExpression = updatedIdx;
+ } else {
+ return { ...node, data: { ...node.data, variable: undefined } };
+ }
+ }
+ return { ...node, data: { ...node.data, variable: varCopy } };
+ }
+ return { ...node, data: { ...node.data, variable: undefined } };
}
return node;
@@ -862,6 +942,16 @@ const FlowContent: React.FC = () => {
// TODO: cannot move draggable elements in expression builder if its >= 2 lines height (only horizontally)
// Timer: https://www.timeanddate.com/countdown/generic?iso=20250616T12&p0=%3A&font=cursive
+
+ // IDEA: Declare variable block could not be a block, and a simply display in some corner of the flow
+ // It would be displayed as a square with a form just like the one in the editor for Declare
+ // In this way, adding a "irrelevant" block wouldn't be needed and you would have a global, easy-to-access
+ // form to view and modify declared variables across the whole program
+ // (and also, it would be easier to add a new variable to the program)
+ // Does order of DeclareVariable nodes matter in the flow currently? I don't think so
+
+ // TODO: stricter type checking between float and integer?
+ // TODO: stricter display format for float (.0)
>
diff --git a/flowming/src/components/Flow/Nodes/AssignVariable.tsx b/flowming/src/components/Flow/Nodes/AssignVariable.tsx
index d59a63d..1681184 100644
--- a/flowming/src/components/Flow/Nodes/AssignVariable.tsx
+++ b/flowming/src/components/Flow/Nodes/AssignVariable.tsx
@@ -65,17 +65,26 @@ export class AssignVariableProcessor implements NodeProcessor {
}
const AssignVariable = memo(function AssignVariableComponent({ data, id: _nodeId }: { data: AssignVariableNode; id: string }) {
- const { isHovered, isSelected, isHighlighted, isCodeHighlighted, expression, width, height, visualId, isError, hasBreakpoint, isBreakpointTriggered, leftSideIndex } = data as AssignVariableNode & { leftSideIndex?: string };
+ const { isHovered, isSelected, isHighlighted, isCodeHighlighted, expression, width, height, visualId, isError, hasBreakpoint, isBreakpointTriggered } = data as AssignVariableNode;
const buildDisplayString = () => {
if (!expression) return null;
const exprInstance = expression instanceof Expression ? expression : Expression.fromObject(expression);
+
+ // When the stored expression is effectively empty (no left or right side)
+ if (exprInstance.isEmpty()) return null;
+
// For assign variable expressions, leftSide is a Variable
if (exprInstance.leftSide instanceof Variable) {
- const leftName = exprInstance.leftSide.name;
- const leftDisplay = leftSideIndex ? `${leftName}[${leftSideIndex}]` : leftName;
+ const leftVar = exprInstance.leftSide;
+ let leftDisplay = leftVar.name;
+ if (leftVar.indexExpression && leftVar.indexExpression.length > 0) {
+ const idxStr = new Expression(undefined, leftVar.indexExpression as any).toString();
+ leftDisplay = `${leftVar.name}[${idxStr}]`;
+ }
const right = exprInstance.rightSide.map((e: any) => (e as ExpressionElement).toString()).join(' ');
- return `${leftDisplay} = ${right}`;
+ const str = `${leftDisplay} = ${right}`;
+ return str.trim() === '' ? null : str;
}
// Fallback
return exprInstance.toString();
@@ -124,7 +133,7 @@ const AssignVariable = memo(function AssignVariableComponent({ data, id: _nodeId
)}
- {expression ? (
+ {displayString ? (
{displayString}
diff --git a/flowming/src/components/Flow/Nodes/Input.tsx b/flowming/src/components/Flow/Nodes/Input.tsx
index 6e61f19..30c0461 100644
--- a/flowming/src/components/Flow/Nodes/Input.tsx
+++ b/flowming/src/components/Flow/Nodes/Input.tsx
@@ -2,10 +2,13 @@ import { Handle, Position, ReactFlowInstance } from '@xyflow/react';
import { memo } from 'react';
import { getNodeStyles } from '../../../utils/nodeStyles';
import { BaseNode, NodeProcessor } from './NodeTypes';
-import { IVariable, VariableType, Variable } from '../../../models/Variable';
+import { VariableType, Variable } from '../../../models/Variable';
+import { IVariable } from '../../../models/IVariable';
import { IValuedVariable, ValuedVariable } from '../../../models/ValuedVariable';
import { Badge } from '@/components/ui/badge';
import BreakpointIndicator from './BreakpointIndicator';
+import { Expression } from '../../../models/Expression';
+import { ExpressionElement } from '../../../models/ExpressionElement';
interface InputNode extends BaseNode {
variable?: IVariable;
@@ -39,7 +42,7 @@ export class InputProcessor implements NodeProcessor {
if (data.variable) {
const input = await this.showInputDialog(
`Enter value for ${data.variable.name}`,
- data.variable.type as 'string' | 'integer' | 'float' | 'boolean', // TODO: array
+ (data.variable.type === 'array' ? data.variable.arraySubtype : data.variable.type) as 'string' | 'integer' | 'float' | 'boolean',
`Please enter a ${data.variable.type} value for the variable "${data.variable.name}" (Block ID: ${data.visualId}).`,
`Enter ${data.variable.type} value...`
);
@@ -47,7 +50,8 @@ export class InputProcessor implements NodeProcessor {
if (input !== null && data.variable) {
// Convert the raw string input to the correct type based on the variable definition
let convertedValue: any = input;
- switch (data.variable.type) {
+ const valueType = data.variable.type === 'array' ? data.variable.arraySubtype : data.variable.type;
+ switch (valueType) {
case 'integer': {
const parsed = parseInt(input, 10);
if (isNaN(parsed)) {
@@ -80,14 +84,59 @@ export class InputProcessor implements NodeProcessor {
// Convert IVariable to Variable before passing to fromVariable
const variableObj = Variable.fromObject(data.variable);
- const newValuedVariable = ValuedVariable.fromVariable(variableObj, convertedValue);
-
- // Overwrite the existing variable
- const existingIndex = currentValuedVariables.findIndex(v => v.id === newValuedVariable.id);
- if (existingIndex !== -1) {
- currentValuedVariables[existingIndex] = newValuedVariable;
+
+ // If variable is an array and has an index expression, handle element assignment
+ if (variableObj.type === 'array' && variableObj.indexExpression && variableObj.indexExpression.length > 0) {
+ try {
+ // Evaluate index expression (must resolve to integer)
+ const indexExpr = new Expression(undefined, variableObj.indexExpression);
+ const idx = indexExpr.calculateValue(variableObj.indexExpression, 'integer', currentValuedVariables);
+
+ if (typeof idx !== 'number' || !Number.isInteger(idx)) {
+ throw new Error(`Array index for variable "${variableObj.name}" must evaluate to an integer.`);
+ }
+
+ // Locate existing valued variable or create a default one
+ let existingVarIdx = currentValuedVariables.findIndex(v => v.id === variableObj.id);
+ let existingValuedVar: ValuedVariable;
+ if (existingVarIdx !== -1) {
+ existingValuedVar = currentValuedVariables[existingVarIdx];
+ } else {
+ existingValuedVar = ValuedVariable.fromVariable(variableObj, null);
+ }
+
+ // Ensure array is initialized with correct size
+ const arraySize = variableObj.arraySize || (Array.isArray(existingValuedVar.value) ? existingValuedVar.value.length : 0);
+ if (idx < 0 || idx >= arraySize) {
+ throw new Error(`Array index ${idx} is out of bounds for "${variableObj.name}" (size ${arraySize}).`);
+ }
+
+ // Create updated array value
+ const updatedArray = Array.isArray(existingValuedVar.value) ? [...existingValuedVar.value] : new Array(arraySize).fill(null);
+ updatedArray[idx] = convertedValue;
+
+ const newValuedVariable = new ValuedVariable(variableObj.id, variableObj.type, variableObj.name, variableObj.nodeId, updatedArray, variableObj.arraySubtype, variableObj.arraySize);
+
+ if (existingVarIdx !== -1) {
+ currentValuedVariables[existingVarIdx] = newValuedVariable;
+ } else {
+ currentValuedVariables.push(newValuedVariable);
+ }
+ } catch (err) {
+ console.error(err);
+ throw err;
+ }
} else {
- currentValuedVariables.push(newValuedVariable);
+ // Scalar variable assignment
+ const newValuedVariable = ValuedVariable.fromVariable(variableObj, convertedValue);
+
+ // Overwrite the existing variable
+ const existingIndex = currentValuedVariables.findIndex(v => v.id === newValuedVariable.id);
+ if (existingIndex !== -1) {
+ currentValuedVariables[existingIndex] = newValuedVariable;
+ } else {
+ currentValuedVariables.push(newValuedVariable);
+ }
}
}
}
@@ -99,6 +148,20 @@ export class InputProcessor implements NodeProcessor {
const Input = memo(function InputComponent({ data, id: _nodeId }: { data: InputNode; id: string }) {
const { isHovered, isSelected, isHighlighted, isCodeHighlighted, variable, width, height, visualId, isError, hasBreakpoint, isBreakpointTriggered } = data;
+ // Helper to stringify index expression for display
+ const getIndexString = (indexExpr?: any[]): string | null => {
+ if (!indexExpr || indexExpr.length === 0) return null;
+ try {
+ // Ensure all elements are ExpressionElement instances
+ const elems = (indexExpr as any[]).map((e: any): ExpressionElement =>
+ e instanceof ExpressionElement ? e : ExpressionElement.fromObject(e)
+ );
+ return new Expression(undefined, elems as any).toString();
+ } catch {
+ return null;
+ }
+ };
+
return (
- {variable.name} = {variable.type}(👤)
+ {variable.name}
+ {variable.type === 'array' && (
+ <>
+ [
+ {getIndexString(variable.indexExpression) || ''}
+ ]
+ >
+ )}
+ {' '}= {(variable.type === 'array' ? variable.arraySubtype : variable.type)}(👤)
) : (
diff --git a/flowming/src/components/ImportExport.tsx b/flowming/src/components/ImportExport.tsx
index 6f0dc59..44f1126 100644
--- a/flowming/src/components/ImportExport.tsx
+++ b/flowming/src/components/ImportExport.tsx
@@ -4,7 +4,7 @@ import { useVariables } from '../context/VariablesContext';
import { useFilename } from '../context/FilenameContext';
import { useFlowExecutorActions, useFlowExecutorState } from '../context/FlowExecutorContext';
import { SelectedNodeContext } from '../context/SelectedNodeContext';
-import { Variable } from '../models';
+import { Variable, ExpressionElement } from '../models';
import { Button } from './ui/button';
import { Download, Upload, FileX } from 'lucide-react';
import { useDebugger } from '../context/DebuggerContext';
@@ -117,6 +117,9 @@ const ImportExport: React.FC = () => {
// Stop execution before importing
stop();
+ // Reset selected node so DetailsTab shows System Settings during import
+ setSelectedNode(null);
+
const input = document.createElement('input');
input.type = 'file';
input.accept = '.flowming';
@@ -172,7 +175,14 @@ const ImportExport: React.FC = () => {
});
}
- setNodes(flowData.nodes || []);
+ // Restore nodes and convert any indexExpression objects back to ExpressionElement instances
+ const restoredNodes = (flowData.nodes || []).map((n: any) => {
+ if (n.type === 'Input' && n.data?.variable?.indexExpression) {
+ n.data.variable.indexExpression = n.data.variable.indexExpression.map((elem: any) => ExpressionElement.fromObject(elem));
+ }
+ return n;
+ });
+ setNodes(restoredNodes);
setEdges(flowData.edges || []);
// Fit and center the view for the imported diagram
@@ -189,7 +199,7 @@ const ImportExport: React.FC = () => {
};
input.click();
- }, [setNodes, setEdges, getNodes, updateNodeVariables, deleteNodeVariables, fitView, setFilename, stop]);
+ }, [setNodes, setEdges, getNodes, updateNodeVariables, deleteNodeVariables, fitView, setFilename, stop, setSelectedNode]);
return (
diff --git a/flowming/src/components/Panel/Tabs/DebuggerTab.tsx b/flowming/src/components/Panel/Tabs/DebuggerTab.tsx
index f982eaf..76b1107 100644
--- a/flowming/src/components/Panel/Tabs/DebuggerTab.tsx
+++ b/flowming/src/components/Panel/Tabs/DebuggerTab.tsx
@@ -47,6 +47,22 @@ const DebuggerTab = () => {
}
};
+ /**
+ * Formats a variable value for display.
+ * Arrays are shown with brackets and comma-separated elements (e.g., [1, 2, 3]).
+ * Non-arrays are stringified as-is.
+ */
+ const formatValue = (value: any): string => {
+ if (Array.isArray(value)) {
+ return `[${value.map(v => formatValue(v)).join(', ')}]`;
+ }
+ // Properly format strings with quotes
+ if (typeof value === 'string') {
+ return JSON.stringify(value);
+ }
+ return String(value);
+ };
+
return (
{/* Status header */}
@@ -111,7 +127,7 @@ const DebuggerTab = () => {
{variable.name}:
- {variable.value}
+ {formatValue(variable.value)}
@@ -144,7 +160,7 @@ const DebuggerTab = () => {
→
- {change.value}
+ {formatValue(change.value)}
diff --git a/flowming/src/components/Panel/Tabs/editors/ConditionalEditor.tsx b/flowming/src/components/Panel/Tabs/editors/ConditionalEditor.tsx
index 91e867c..ff1cbb8 100644
--- a/flowming/src/components/Panel/Tabs/editors/ConditionalEditor.tsx
+++ b/flowming/src/components/Panel/Tabs/editors/ConditionalEditor.tsx
@@ -44,7 +44,6 @@ import {
ExpressionDropArea,
} from './shared/DragAndDropComponents';
import ArrayIndexDialog from '@/components/ui/ArrayIndexDialog';
-import { parseArrayAccess, parseExpressionString } from '@/utils/expressionParsing';
// Available operators for expression building
const operators = [
@@ -104,10 +103,11 @@ interface FunctionExpressionElementProps {
removeExpressionElement: (id: string) => void;
disabled: boolean;
side: 'left' | 'right';
+ onEdit?: (element: ExpressionElement) => void;
}
// Function block component for nesting
-const FunctionExpressionElement: React.FC = ({ element, removeExpressionElement, disabled, side }) => {
+const FunctionExpressionElement: React.FC = ({ element, removeExpressionElement, disabled, side, onEdit }) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: element.id, disabled });
const nestedDropId = `nested-${side}-${element.id}`;
@@ -141,6 +141,7 @@ const FunctionExpressionElement: React.FC = ({ e
removeExpressionElement={removeExpressionElement}
disabled={disabled}
side={side}
+ onEdit={onEdit}
/>
) : (
= ({ e
index={idx}
removeExpressionElement={removeExpressionElement}
disabled={disabled}
+ onEdit={onEdit}
/>
)
))
@@ -188,9 +190,7 @@ const ConditionalEditor = () => {
// State for editing existing array access elements
const [editingElement, setEditingElement] = useState(null);
const [initialExpression, setInitialExpression] = useState(null);
- const [initialRangeStart, setInitialRangeStart] = useState(null);
- const [initialRangeEnd, setInitialRangeEnd] = useState(null);
- const [initialTab, setInitialTab] = useState<'single' | 'range'>('single');
+ const [initialTab, setInitialTab] = useState<'single'>('single');
const reactFlowInstance = useReactFlow();
@@ -320,7 +320,26 @@ const ConditionalEditor = () => {
setExpression(prev => {
if (!prev) return null;
const newExpr = prev.clone();
+
+ // Remove from right side using built-in helper
newExpr.removeElement(id);
+
+ // Remove from left side (array of ExpressionElement)
+ if (Array.isArray(newExpr.leftSide)) {
+ const recursiveRemove = (list: ExpressionElement[]): ExpressionElement[] => {
+ return list.flatMap(e => {
+ if (e.id === id) {
+ return [];
+ }
+ if (e.isFunction() && e.nestedExpression) {
+ e.nestedExpression.rightSide = recursiveRemove(e.nestedExpression.rightSide);
+ }
+ return [e];
+ });
+ };
+ newExpr.leftSide = recursiveRemove(newExpr.leftSide);
+ }
+
return newExpr;
});
}
@@ -460,7 +479,7 @@ const ConditionalEditor = () => {
}
// Intercept array variable drag
- if (activeDraggableItem.type === 'variable' && activeDraggableItem.variable?.type === 'array') {
+ if (activeDraggableItem.type === 'variable' && activeDraggableItem.variable?.type === 'array' && (!activeDraggableItem.variable.indexExpression || activeDraggableItem.variable.indexExpression.length === 0)) {
const overLocation = getElementLocationInfo(over.id, expression);
if (overLocation && overLocation.side && (overLocation.isMainDropArea || overLocation.isNestedDropArea || overLocation.isMainExpressionElement || overLocation.isNestedExpressionElement)) {
setArrayVariableForIndex(activeDraggableItem.variable);
@@ -574,7 +593,7 @@ const ConditionalEditor = () => {
setExpression(prev => {
if (!prev) return null;
const newExpr = prev.clone();
- if (indexSide === 'left' && Array.isArray(newExpr.leftSide)) {
+ if (side === 'left' && Array.isArray(newExpr.leftSide)) {
newExpr.leftSide.push(element as any);
} else {
newExpr.rightSide.push(element);
@@ -586,14 +605,9 @@ const ConditionalEditor = () => {
// Handle editing array access elements
const handleEditArrayAccess = (element: ExpressionElement) => {
- if (!element.value.includes('[') || !element.value.includes(']')) return;
-
- const parsed = parseArrayAccess(element.value);
- if (!parsed) return;
-
- // Find the array variable
- const arrayVar = getAllVariables().find(v => v.name === parsed.arrayName && v.type === 'array');
- if (!arrayVar) return;
+ if (!element.variable || !(element.variable instanceof Variable)) return;
+ const elemVar = element.variable as Variable;
+ if (elemVar.type !== 'array') return;
// Determine which side the element is on
let elementSide: 'left' | 'right' = 'right';
@@ -605,20 +619,14 @@ const ConditionalEditor = () => {
}
setEditingElement(element);
- setArrayVariableForIndex(arrayVar);
+ setArrayVariableForIndex(elemVar);
setIndexSide(elementSide);
- if (parsed.isRange && parsed.rangeStart && parsed.rangeEnd) {
- setInitialTab('range');
- setInitialRangeStart(parseExpressionString(parsed.rangeStart, getAllVariables()));
- setInitialRangeEnd(parseExpressionString(parsed.rangeEnd, getAllVariables()));
- setInitialExpression(null);
- } else if (parsed.indexExpression) {
- setInitialTab('single');
- setInitialExpression(parseExpressionString(parsed.indexExpression, getAllVariables()));
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
- }
+ setInitialTab('single');
+ const idxExpr = elemVar.indexExpression && elemVar.indexExpression.length > 0
+ ? elemVar.indexExpression.map(e => e.clone())
+ : [];
+ setInitialExpression(new Expression(undefined, idxExpr));
setIsIndexDialogOpen(true);
};
@@ -626,63 +634,47 @@ const ConditionalEditor = () => {
// Handle dialog submission for editing
const handleDialogSubmit = (_: 'single', expr: Expression) => {
if (arrayVariableForIndex && expression) {
- const exprStr = expr.toString();
- const newValue = `${arrayVariableForIndex.name}[${exprStr}]`;
-
- if (editingElement) {
- // Replace existing element
- setExpression(prev => {
- if (!prev) return null;
- const newExpr = prev.clone();
- const elementToUpdate = newExpr.findElement(editingElement.id);
- if (elementToUpdate) {
- elementToUpdate.value = newValue;
- }
- return newExpr;
- });
- } else {
- // Add new element
- const element = new ExpressionElement(crypto.randomUUID(), 'literal', newValue);
- setExpression(prev => {
- if (!prev) return null;
- const newExpr = prev.clone();
- if (indexSide === 'left' && Array.isArray(newExpr.leftSide)) {
- newExpr.leftSide.push(element as any);
- } else {
- newExpr.rightSide.push(element);
- }
- return newExpr;
- });
+ if (expr.isEmpty()) {
+ // Ignore empty index
+ setEditingElement(null);
+ setIsIndexDialogOpen(false);
+ setArrayVariableForIndex(null);
+ return;
}
- }
-
- // Reset state
- setEditingElement(null);
- setInitialExpression(null);
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
- setIsIndexDialogOpen(false);
- };
-
- // Handle dialog submission for range editing
- const handleDialogSubmitRange = (_: 'range', start: Expression, end: Expression) => {
- if (arrayVariableForIndex && expression) {
- const newValue = `${arrayVariableForIndex.name}[${start.toString()}:${end.toString()}]`;
+ // Build variable clone with index expression directly
+ const idxExpr = expr.rightSide.map(e => e.clone());
+ const variableClone = arrayVariableForIndex.clone();
+ (variableClone as any).indexExpression = idxExpr;
if (editingElement) {
- // Replace existing element
+ // Replace existing element (search both sides)
setExpression(prev => {
if (!prev) return null;
const newExpr = prev.clone();
- const elementToUpdate = newExpr.findElement(editingElement.id);
- if (elementToUpdate) {
- elementToUpdate.value = newValue;
+ // TODO: refactor findElement?
+ const findInList = (list: ExpressionElement[]): ExpressionElement | undefined => {
+ for (const el of list) {
+ if (el.id === editingElement!.id) return el;
+ if (el.isFunction() && el.nestedExpression) {
+ const found = findInList(el.nestedExpression.rightSide);
+ if (found) return found;
+ }
+ }
+ return undefined;
+ };
+ let elem = newExpr.findElement(editingElement!.id);
+ if (!elem && Array.isArray(newExpr.leftSide)) {
+ elem = findInList(newExpr.leftSide);
+ }
+ if (elem) {
+ elem.type = 'variable';
+ elem.setVariable(variableClone);
}
return newExpr;
});
} else {
// Add new element
- const element = new ExpressionElement(crypto.randomUUID(), 'literal', newValue);
+ const element = new ExpressionElement(crypto.randomUUID(), 'variable', '', variableClone);
setExpression(prev => {
if (!prev) return null;
const newExpr = prev.clone();
@@ -699,8 +691,6 @@ const ConditionalEditor = () => {
// Reset state
setEditingElement(null);
setInitialExpression(null);
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
setIsIndexDialogOpen(false);
};
@@ -708,8 +698,6 @@ const ConditionalEditor = () => {
const handleDialogCancel = () => {
setEditingElement(null);
setInitialExpression(null);
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
setIsIndexDialogOpen(false);
};
@@ -752,6 +740,7 @@ const ConditionalEditor = () => {
removeExpressionElement={removeExpressionElement}
disabled={isRunning}
side='left'
+ onEdit={handleEditArrayAccess}
/>
);
}
@@ -815,6 +804,7 @@ const ConditionalEditor = () => {
removeExpressionElement={removeExpressionElement}
disabled={isRunning}
side='right'
+ onEdit={handleEditArrayAccess}
/>
) : (
{
px-2 py-1 m-1 rounded text-sm shadow-lg cursor-grabbing
${activeDraggableItem.type === 'variable' ? 'bg-blue-100' :
activeDraggableItem.type === 'operator' ? 'bg-red-100' :
- activeDraggableItem.type === 'literal' && activeDraggableItem.value.includes('[') && activeDraggableItem.value.includes(']') ? 'bg-blue-100' :
activeDraggableItem.type === 'literal' ? 'bg-green-100' : 'bg-purple-100'}
`}>
{activeDraggableItem.value}
@@ -1195,10 +1184,10 @@ const ConditionalEditor = () => {
variableName={arrayVariableForIndex?.name || ''}
onCancel={handleDialogCancel}
onSubmit={handleDialogSubmit}
- onSubmitRange={handleDialogSubmitRange}
+ onSubmitRange={undefined as any}
initialExpression={initialExpression || undefined}
- initialRangeStart={initialRangeStart || undefined}
- initialRangeEnd={initialRangeEnd || undefined}
+ initialRangeStart={undefined as any}
+ initialRangeEnd={undefined as any}
initialTab={initialTab}
/>
diff --git a/flowming/src/components/Panel/Tabs/editors/InputEditor.tsx b/flowming/src/components/Panel/Tabs/editors/InputEditor.tsx
index 94dcfc8..29d833b 100644
--- a/flowming/src/components/Panel/Tabs/editors/InputEditor.tsx
+++ b/flowming/src/components/Panel/Tabs/editors/InputEditor.tsx
@@ -12,6 +12,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
+import { Button } from "@/components/ui/button";
+import ArrayIndexDialog from "@/components/ui/ArrayIndexDialog";
+import { Variable } from "../../../../models/Variable";
+import { Expression, ExpressionElement } from "../../../../models";
const InputEditor = () => {
const { selectedNode } = useContext(SelectedNodeContext);
@@ -23,6 +27,18 @@ const InputEditor = () => {
const reactFlowInstance = useReactFlow();
const { isRunning } = useFlowExecutorState();
+ // Additional state for array index handling
+ // leftSideIndexExprElems stores the structured index expression (array element tokens)
+ const [leftSideIndexExprElems, setLeftSideIndexExprElems] = useState(null);
+ const [isIndexDialogOpen, setIsIndexDialogOpen] = useState(false);
+ const [arrayVariableForIndex, setArrayVariableForIndex] = useState(null);
+ const [initialExpression, setInitialExpression] = useState(null);
+ const [initialRangeStart, setInitialRangeStart] = useState(null);
+ const [initialRangeEnd, setInitialRangeEnd] = useState(null);
+ const [initialTab, setInitialTab] = useState<'single' | 'range'>('single');
+ // Track whether the dialog is editing the index of the currently selected variable
+ const [isEditingIndex, setIsEditingIndex] = useState(false);
+
// Update the node data when variable changes for Input node
useEffect(() => {
if (selectedNode?.type === 'Input' && !isInitialLoadRef.current) {
@@ -31,8 +47,15 @@ const InputEditor = () => {
let updatedData;
if (selectedVariable) {
+ let variableClone = selectedVariable.clone();
+
+ // Attach index expression if applicable (array variable with index)
+ if (selectedVariable.type === 'array' && leftSideIndexExprElems && leftSideIndexExprElems.length > 0) {
+ (variableClone as any).indexExpression = leftSideIndexExprElems.map(e => e.clone());
+ }
+
updatedData = {
- variable: selectedVariable
+ variable: variableClone,
};
} else {
updatedData = { variable: null };
@@ -41,7 +64,7 @@ const InputEditor = () => {
// Update the node data
reactFlowInstance.updateNodeData(selectedNode.id, updatedData);
}
- }, [leftSideVariable, reactFlowInstance, selectedNode, getAllVariables]);
+ }, [leftSideVariable, leftSideIndexExprElems, reactFlowInstance, selectedNode, getAllVariables]);
// Load input data when the selected node changes or when its data changes (for collaboration)
useEffect(() => {
@@ -62,57 +85,216 @@ const InputEditor = () => {
// Initialize with existing variable if available
if (selectedNode.data.variable && selectedNode.data.variable.id) {
setLeftSideVariable(selectedNode.data.variable.id);
+
+ // Load stored index if present (array variables)
+ if (selectedNode.data.variable.indexExpression && selectedNode.data.variable.indexExpression.length > 0) {
+ try {
+ // Ensure every element is an ExpressionElement instance (can be plain objects after Yjs sync)
+ const elems = selectedNode.data.variable.indexExpression.map((e: any): ExpressionElement => {
+ return e instanceof ExpressionElement ? e : ExpressionElement.fromObject(e);
+ });
+ setLeftSideIndexExprElems(elems.map((el: ExpressionElement) => el.clone()));
+ } catch {
+ setLeftSideIndexExprElems(null);
+ }
+ } else {
+ setLeftSideIndexExprElems(null);
+ }
} else if (nodeChanged) {
// Only reset when it's a new node (not when collaborative data is cleared)
setLeftSideVariable('');
+ setLeftSideIndexExprElems(null);
}
}
} else {
previousNodeIdRef.current = null;
setLeftSideVariable('');
+ setLeftSideIndexExprElems(null);
}
isInitialLoadRef.current = false;
}, [selectedNode, selectedNode?.data?.variable]);
+ // Updated handler for variable change to support arrays
+ const handleVariableChange = (value: string) => {
+ if (value === '__CLEAR__') {
+ setLeftSideVariable('');
+ setLeftSideIndexExprElems(null);
+ if (selectedNode) {
+ reactFlowInstance.updateNodeData(selectedNode.id, { variable: null });
+ }
+ return;
+ }
+
+ const variable = getAllVariables().find(v => v.id === value);
+ if (!variable) return;
+
+ setLeftSideVariable(variable.id);
+
+ if (variable.type === 'array') {
+ setArrayVariableForIndex(variable);
+ setIsEditingIndex(true);
+
+ // Prefill dialog if index already exists
+ if (leftSideIndexExprElems && leftSideIndexExprElems.length > 0) {
+ setInitialTab('single');
+ setInitialExpression(new Expression(undefined, leftSideIndexExprElems.map(e => e.clone())));
+ setInitialRangeStart(null);
+ setInitialRangeEnd(null);
+ } else {
+ setInitialTab('single');
+ setInitialExpression(new Expression(undefined, []));
+ }
+
+ // Defer opening the dialog until after the Select dropdown has closed
+ setTimeout(() => setIsIndexDialogOpen(true), 0);
+ } else {
+ setLeftSideIndexExprElems(null);
+ // Update node data immediately for non-array variables
+ const variableClone = variable.clone();
+ reactFlowInstance.updateNodeData(selectedNode!.id, { variable: variableClone });
+ }
+ };
+
+ // Dialog submit handlers
+ const handleDialogSubmitSingle = (_: 'single', expr: Expression) => {
+ if (!arrayVariableForIndex || !selectedNode) return;
+
+ if (expr.isEmpty()) {
+ // Treat empty as cancel
+ handleDialogCancel();
+ return;
+ }
+
+ setLeftSideIndexExprElems(expr.rightSide.map(e => e.clone()));
+
+ const variableClone = arrayVariableForIndex.clone();
+ (variableClone as any).indexExpression = expr.rightSide.map(e => e.clone());
+
+ reactFlowInstance.updateNodeData(selectedNode.id, { variable: variableClone });
+
+ setIsIndexDialogOpen(false);
+ setArrayVariableForIndex(null);
+ setIsEditingIndex(false);
+ };
+
+ const handleDialogSubmitRange = (_: 'range', start: Expression, end: Expression) => {
+ // For Input block we treat range the same as single expression string representation
+ if (!arrayVariableForIndex || !selectedNode) return;
+
+ if (start.isEmpty() || end.isEmpty()) {
+ handleDialogCancel();
+ return;
+ }
+
+ // Range not supported for actual element assignment in Input node; store as string only
+ const variableClone = arrayVariableForIndex.clone();
+ reactFlowInstance.updateNodeData(selectedNode.id, { variable: variableClone });
+
+ setIsIndexDialogOpen(false);
+ setArrayVariableForIndex(null);
+ setIsEditingIndex(false);
+ };
+
+ const handleDialogCancel = () => {
+ setIsIndexDialogOpen(false);
+ setArrayVariableForIndex(null);
+
+ if (isEditingIndex && (!leftSideIndexExprElems || leftSideIndexExprElems.length === 0)) {
+ // User canceled before providing an index -> deselect variable
+ setLeftSideVariable('');
+ }
+
+ setIsEditingIndex(false);
+ };
+
// Don't render if not an Input node
if (!selectedNode || selectedNode.type !== 'Input') return null;
const allVariables = getAllVariables();
return (
-
-
- {/* Variable selection */}
-
-
- Input Variable
-
-
-
-
-
- During execution, the program will prompt for this variable's value.
-
-
-
-
+ <>
+ {/* Array index dialog */}
+
+
+
+
+ {/* Variable selection */}
+
+
+ Input Variable
+
+
+
+
+ {/* Index display / edit button for arrays */}
+ {(() => {
+ const selectedVar = allVariables.find(v => v.id === leftSideVariable);
+ if (selectedVar && selectedVar.type === 'array') {
+ return (
+
+ );
+ }
+ return null;
+ })()}
+
+
+
+ During execution, the program will prompt for this variable's value.
+
+
+
+
+ >
);
};
diff --git a/flowming/src/components/Panel/Tabs/editors/OutputEditor.tsx b/flowming/src/components/Panel/Tabs/editors/OutputEditor.tsx
index 9d09624..fecc887 100644
--- a/flowming/src/components/Panel/Tabs/editors/OutputEditor.tsx
+++ b/flowming/src/components/Panel/Tabs/editors/OutputEditor.tsx
@@ -37,7 +37,6 @@ import {
} from './shared/DragAndDropComponents';
import { CSS } from '@dnd-kit/utilities';
import ArrayIndexDialog from '@/components/ui/ArrayIndexDialog';
-import { parseArrayAccess, parseExpressionString } from '@/utils/expressionParsing';
// Available operators for expression building
const operators = [
@@ -67,10 +66,11 @@ interface FunctionExpressionElementProps {
element: ExpressionElement;
removeExpressionElement: (id: string) => void;
disabled: boolean;
+ onEdit?: (element: ExpressionElement) => void;
}
// Function block component for nesting
-const FunctionExpressionElement: React.FC = ({ element, removeExpressionElement, disabled }) => {
+const FunctionExpressionElement: React.FC = ({ element, removeExpressionElement, disabled, onEdit }) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: element.id, disabled });
const nestedDropId = `nested-${element.id}`;
@@ -103,6 +103,7 @@ const FunctionExpressionElement: React.FC = ({ e
element={nestedElem}
removeExpressionElement={removeExpressionElement}
disabled={disabled}
+ onEdit={onEdit}
/>
) : (
= ({ e
index={idx}
removeExpressionElement={removeExpressionElement}
disabled={disabled}
+ onEdit={onEdit}
/>
)
))
@@ -149,9 +151,7 @@ const OutputEditor = () => {
// State for editing existing array access elements
const [editingElement, setEditingElement] = useState(null);
const [initialExpression, setInitialExpression] = useState(null);
- const [initialRangeStart, setInitialRangeStart] = useState(null);
- const [initialRangeEnd, setInitialRangeEnd] = useState(null);
- const [initialTab, setInitialTab] = useState<'single' | 'range'>('single');
+ const [initialTab, setInitialTab] = useState<'single'>('single');
const reactFlowInstance = useReactFlow();
@@ -399,7 +399,7 @@ const OutputEditor = () => {
}
// Intercept array variable drag
- if (activeDraggableItem.type === 'variable' && activeDraggableItem.variable?.type === 'array') {
+ if (activeDraggableItem.type === 'variable' && activeDraggableItem.variable?.type === 'array' && (!activeDraggableItem.variable.indexExpression || activeDraggableItem.variable.indexExpression.length === 0)) {
const overLocation = getElementLocationInfo(over.id, expression);
// Check if it's a valid drop target
if (overLocation && (overLocation.isMainDropArea || overLocation.isNestedDropArea || overLocation.isMainExpressionElement || overLocation.isNestedExpressionElement)) {
@@ -513,29 +513,19 @@ const OutputEditor = () => {
// Handle editing array access elements
const handleEditArrayAccess = (element: ExpressionElement) => {
- if (!element.value.includes('[') || !element.value.includes(']')) return;
-
- const parsed = parseArrayAccess(element.value);
- if (!parsed) return;
-
- // Find the array variable
- const arrayVar = getAllVariables().find(v => v.name === parsed.arrayName && v.type === 'array');
- if (!arrayVar) return;
+ if (!element.variable || !(element.variable instanceof Variable)) return;
+ const elemVar = element.variable as Variable;
+ if (elemVar.type !== 'array') return;
+ const arrayVar = getAllVariables().find(v => v.id === elemVar.id && v.type === 'array') || elemVar;
+
setEditingElement(element);
setArrayVariableForIndex(arrayVar);
-
- if (parsed.isRange && parsed.rangeStart && parsed.rangeEnd) {
- setInitialTab('range');
- setInitialRangeStart(parseExpressionString(parsed.rangeStart, getAllVariables()));
- setInitialRangeEnd(parseExpressionString(parsed.rangeEnd, getAllVariables()));
- setInitialExpression(null);
- } else if (parsed.indexExpression) {
- setInitialTab('single');
- setInitialExpression(parseExpressionString(parsed.indexExpression, getAllVariables()));
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
- }
+ setInitialTab('single');
+ const idxExpr = elemVar.indexExpression && elemVar.indexExpression.length > 0
+ ? elemVar.indexExpression.map(e => e.clone())
+ : [];
+ setInitialExpression(new Expression(undefined, idxExpr));
setIsIndexDialogOpen(true);
};
@@ -543,44 +533,18 @@ const OutputEditor = () => {
// Handle dialog submission for editing
const handleDialogSubmit = (_: 'single', expr: Expression) => {
if (arrayVariableForIndex) {
- const exprStr = expr.toString();
- const newValue = `${arrayVariableForIndex.name}[${exprStr}]`;
-
- if (editingElement) {
- // Replace existing element
- setExpression(prev => {
- if (!prev) return null;
- const newExpr = prev.clone();
- const elementToUpdate = newExpr.findElement(editingElement.id);
- if (elementToUpdate) {
- elementToUpdate.value = newValue;
- }
- return newExpr;
- });
- } else {
- // Add new element
- const element = new ExpressionElement(crypto.randomUUID(), 'literal', newValue);
- setExpression(prev => {
- if (!prev) return null;
- const newExpr = prev.clone();
- newExpr.addElement(element);
- return newExpr;
- });
+ if (expr.isEmpty()) {
+ // Ignore empty index
+ // TODO: ideally error message and block action
+ setEditingElement(null);
+ setIsIndexDialogOpen(false);
+ setArrayVariableForIndex(null);
+ return;
}
- }
-
- // Reset state
- setEditingElement(null);
- setInitialExpression(null);
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
- setIsIndexDialogOpen(false);
- };
-
- // Handle dialog submission for range editing
- const handleDialogSubmitRange = (_: 'range', start: Expression, end: Expression) => {
- if (arrayVariableForIndex) {
- const newValue = `${arrayVariableForIndex.name}[${start.toString()}:${end.toString()}]`;
+ // Build variable clone with index expression directly
+ const idxExpr = expr.rightSide.map(e => e.clone());
+ const variableClone = arrayVariableForIndex.clone();
+ (variableClone as any).indexExpression = idxExpr;
if (editingElement) {
// Replace existing element
@@ -589,13 +553,14 @@ const OutputEditor = () => {
const newExpr = prev.clone();
const elementToUpdate = newExpr.findElement(editingElement.id);
if (elementToUpdate) {
- elementToUpdate.value = newValue;
+ elementToUpdate.type = 'variable';
+ elementToUpdate.setVariable(variableClone);
}
return newExpr;
});
} else {
// Add new element
- const element = new ExpressionElement(crypto.randomUUID(), 'literal', newValue);
+ const element = new ExpressionElement(crypto.randomUUID(), 'variable', '', variableClone);
setExpression(prev => {
if (!prev) return null;
const newExpr = prev.clone();
@@ -608,8 +573,6 @@ const OutputEditor = () => {
// Reset state
setEditingElement(null);
setInitialExpression(null);
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
setIsIndexDialogOpen(false);
};
@@ -617,8 +580,6 @@ const OutputEditor = () => {
const handleDialogCancel = () => {
setEditingElement(null);
setInitialExpression(null);
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
setIsIndexDialogOpen(false);
};
@@ -656,6 +617,7 @@ const OutputEditor = () => {
element={element}
removeExpressionElement={removeExpressionElement}
disabled={isRunning}
+ onEdit={handleEditArrayAccess}
/>
);
}
@@ -905,7 +867,6 @@ const OutputEditor = () => {
px-2 py-1 m-1 rounded text-sm shadow-lg cursor-grabbing
${activeDraggableItem.type === 'variable' ? 'bg-blue-100' :
activeDraggableItem.type === 'operator' ? 'bg-red-100' :
- activeDraggableItem.type === 'literal' && activeDraggableItem.value.includes('[') && activeDraggableItem.value.includes(']') ? 'bg-blue-100' :
activeDraggableItem.type === 'literal' ? 'bg-green-100' : 'bg-purple-100'}
`}>
{activeDraggableItem.value}
@@ -919,10 +880,10 @@ const OutputEditor = () => {
variableName={arrayVariableForIndex?.name || ''}
onCancel={handleDialogCancel}
onSubmit={handleDialogSubmit}
- onSubmitRange={handleDialogSubmitRange}
+ onSubmitRange={undefined as any}
initialExpression={initialExpression || undefined}
- initialRangeStart={initialRangeStart || undefined}
- initialRangeEnd={initialRangeEnd || undefined}
+ initialRangeStart={undefined as any}
+ initialRangeEnd={undefined as any}
initialTab={initialTab}
/>
diff --git a/flowming/src/components/Panel/Tabs/editors/VariableAssignmentEditor.tsx b/flowming/src/components/Panel/Tabs/editors/VariableAssignmentEditor.tsx
index 82d3c7e..8767173 100644
--- a/flowming/src/components/Panel/Tabs/editors/VariableAssignmentEditor.tsx
+++ b/flowming/src/components/Panel/Tabs/editors/VariableAssignmentEditor.tsx
@@ -44,7 +44,6 @@ import {
ExpressionDropArea
} from './shared/DragAndDropComponents';
import ArrayIndexDialog from '@/components/ui/ArrayIndexDialog';
-import { parseArrayAccess, parseExpressionString } from '@/utils/expressionParsing';
// Available operators for expression building
const operators = [
@@ -74,10 +73,11 @@ interface FunctionExpressionElementProps {
element: ExpressionElement;
removeExpressionElement: (id: string) => void;
disabled: boolean;
+ onEdit?: (element: ExpressionElement) => void;
}
// Function block component for nesting
-const FunctionExpressionElement: React.FC = ({ element, removeExpressionElement, disabled }) => {
+const FunctionExpressionElement: React.FC = ({ element, removeExpressionElement, disabled, onEdit }) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: element.id, disabled });
const nestedDropId = `nested-${element.id}`;
@@ -110,6 +110,7 @@ const FunctionExpressionElement: React.FC = ({ e
element={nestedElem}
removeExpressionElement={removeExpressionElement}
disabled={disabled}
+ onEdit={onEdit}
/>
) : (
= ({ e
index={idx}
removeExpressionElement={removeExpressionElement}
disabled={disabled}
+ onEdit={onEdit}
/>
)
))
@@ -154,15 +156,13 @@ const VariableAssignmentEditor = () => {
const [isIndexDialogOpen, setIsIndexDialogOpen] = useState(false);
const [arrayVariableForIndex, setArrayVariableForIndex] = useState(null);
// Left-side (assignment target) index state
- const [leftSideIndexString, setLeftSideIndexString] = useState(null);
+ const [leftSideIndexExprElems, setLeftSideIndexExprElems] = useState(null);
const [isEditingLeftSideIndex, setIsEditingLeftSideIndex] = useState(false);
// State for editing existing array access elements
const [editingElement, setEditingElement] = useState(null);
const [initialExpression, setInitialExpression] = useState(null);
- const [initialRangeStart, setInitialRangeStart] = useState(null);
- const [initialRangeEnd, setInitialRangeEnd] = useState(null);
- const [initialTab, setInitialTab] = useState<'single' | 'range'>('single');
+ const [initialTab, setInitialTab] = useState<'single'>('single');
const reactFlowInstance = useReactFlow();
@@ -208,32 +208,33 @@ const VariableAssignmentEditor = () => {
}
// Reset index state if variable type changed to non-array
- if (varInstance.type !== 'array' && leftSideIndexString) {
- setLeftSideIndexString(null);
+ if (varInstance.type !== 'array' && leftSideIndexExprElems && leftSideIndexExprElems.length > 0) {
+ setLeftSideIndexExprElems(null);
}
// Only update if the left side has changed or if the expression is null
if (!expression || (expression.leftSide instanceof Variable && expression.leftSide.id !== varInstance.id)) {
- // Preserve right side if expression already exists and has one
- setExpression(new Expression(varInstance, expression?.rightSide || []));
+ // Preserve right side and indexExpression if exists
+ const varClone = varInstance.clone();
+ if (leftSideIndexExprElems && leftSideIndexExprElems.length > 0) {
+ (varClone as any).indexExpression = leftSideIndexExprElems.map(e => e.clone());
+ }
+ setExpression(new Expression(varClone, expression?.rightSide || []));
}
} else { // AssignVariable selected, but no variable chosen for its LHS, so reset expression
if (expression !== null) setExpression(null);
}
}
- }, [leftSideVariable, selectedNode, getAllVariables, expression, leftSideIndexString]);
+ }, [leftSideVariable, selectedNode, getAllVariables, expression, leftSideIndexExprElems]);
// Update the node data when expression changes
useEffect(() => {
if (selectedNode?.type === 'AssignVariable' && !isInitialLoadRef.current) {
- const updatedData: any = {
+ reactFlowInstance.updateNodeData(selectedNode.id, {
expression: expression ? expression.toObject() : null,
- leftSideIndex: leftSideIndexString || undefined,
- };
-
- reactFlowInstance.updateNodeData(selectedNode.id, updatedData);
+ });
}
- }, [expression, leftSideIndexString, reactFlowInstance, selectedNode]);
+ }, [expression, reactFlowInstance, selectedNode]);
// Load assignment data when the selected node changes or when its data changes (for collaboration)
useEffect(() => {
@@ -256,24 +257,32 @@ const VariableAssignmentEditor = () => {
const allVariables = getAllVariables();
try {
- const varId = selectedNode.data.expression.leftSide.id;
- // Check if the variable with this ID still exists
- if (allVariables.some(v => v.id === varId)) {
+ const exprObj = selectedNode.data.expression;
+ const exprLoaded = Expression.fromObject(exprObj);
+
+ // Verify variable still exists
+ if (exprLoaded.leftSide instanceof Variable) {
+ const varId = exprLoaded.leftSide.id;
+ if (!allVariables.some(v => v.id === varId)) throw new Error("Variable no longer exists");
setLeftSideVariable(varId);
- // Load stored leftSideIndex if present
- if (selectedNode.data.leftSideIndex) {
- setLeftSideIndexString(selectedNode.data.leftSideIndex as string);
+
+ if (exprLoaded.leftSide.indexExpression && exprLoaded.leftSide.indexExpression.length > 0) {
+ const clonedElems = exprLoaded.leftSide.indexExpression.map(e => e.clone());
+ setLeftSideIndexExprElems(clonedElems);
} else {
- setLeftSideIndexString(null);
+ setLeftSideIndexExprElems(null);
}
- // Try to recreate the expression
- const leftVar = allVariables.find(v => v.id === varId);
- if (leftVar) {
- const rightSide = selectedNode.data.expression.rightSide?.map((elem: any) =>
- ExpressionElement.fromObject(elem)
- ) || [];
- setExpression(new Expression(leftVar, rightSide));
+
+ // Replace leftSide reference with current variable definition + keep indexExpression
+ const currentVar = allVariables.find(v => v.id === varId)?.clone();
+ if (currentVar) {
+ if (exprLoaded.leftSide.indexExpression && currentVar.type === 'array') {
+ currentVar.indexExpression = exprLoaded.leftSide.indexExpression.map(e => e.clone());
+ }
+ exprLoaded.leftSide = currentVar;
}
+
+ setExpression(exprLoaded);
}
} catch (error) {
console.error('Error creating expression:', error);
@@ -282,19 +291,46 @@ const VariableAssignmentEditor = () => {
// Only reset form state when it's a new node (not when collaborative data is cleared)
setLeftSideVariable('');
setExpression(null);
- setLeftSideIndexString(null);
+ setLeftSideIndexExprElems(null);
}
}
} else {
previousNodeIdRef.current = null;
setLeftSideVariable('');
setExpression(null);
- setLeftSideIndexString(null);
+ setLeftSideIndexExprElems(null);
}
isInitialLoadRef.current = false;
}, [selectedNode, selectedNode?.data?.expression, getAllVariables]);
+ // Update indexExpression whenever the index string changes.
+ useEffect(() => {
+ if (!leftSideVariable) return;
+
+ const selectedVar = getAllVariables().find(v => v.id === leftSideVariable);
+ if (!selectedVar || selectedVar.type !== 'array') return;
+
+ const idxExprElems = leftSideIndexExprElems ? leftSideIndexExprElems.map(e => e.clone()) : undefined;
+
+ setExpression(prev => {
+ if (!prev) return prev;
+
+ if (!(prev.leftSide instanceof Variable)) return prev;
+
+ // Only update when the index expression actually changed to avoid unnecessary re-renders
+ const prevSerialized = JSON.stringify(prev.leftSide.indexExpression ?? []);
+ const newSerialized = JSON.stringify(idxExprElems ?? []);
+ if (prevSerialized === newSerialized) return prev;
+
+ const newExpr = prev.clone();
+ if (newExpr.leftSide instanceof Variable) {
+ newExpr.leftSide.indexExpression = idxExprElems;
+ }
+ return newExpr;
+ });
+ }, [leftSideIndexExprElems, leftSideVariable]);
+
// Expression building functions
const addExpressionElement = (element: ExpressionElement) => {
if (selectedNode?.type === 'AssignVariable' && expression && !isRunning) {
@@ -457,7 +493,7 @@ const VariableAssignmentEditor = () => {
}
// Intercept array variable drag
- if (activeDraggableItem.type === 'variable' && activeDraggableItem.variable?.type === 'array') {
+ if (activeDraggableItem.type === 'variable' && activeDraggableItem.variable?.type === 'array' && (!activeDraggableItem.variable.indexExpression || activeDraggableItem.variable.indexExpression.length === 0)) {
const overLocation = getElementLocationInfo(over.id, expression);
// Check if it's a valid drop target
if (overLocation && (overLocation.isMainDropArea || overLocation.isNestedDropArea || overLocation.isMainExpressionElement || overLocation.isNestedExpressionElement)) {
@@ -569,11 +605,17 @@ const VariableAssignmentEditor = () => {
}
};
+ // Helper to derive a string representation when needed (UI only)
+ const getIndexString = () =>
+ leftSideIndexExprElems && leftSideIndexExprElems.length > 0
+ ? new Expression(undefined, leftSideIndexExprElems).toString()
+ : null;
+
// Modify variable dropdown change handler to support array index dialog
const handleLeftVariableChange = (value: string) => {
if (value === '__CLEAR__') {
setLeftSideVariable('');
- setLeftSideIndexString(null);
+ setLeftSideIndexExprElems(null);
return;
}
@@ -586,28 +628,17 @@ const VariableAssignmentEditor = () => {
// Open index dialog immediately
setArrayVariableForIndex(variable);
setIsEditingLeftSideIndex(true);
- // Prefill if already had an index
- if (leftSideIndexString) {
- const parsed = parseArrayAccess(`${variable.name}[${leftSideIndexString}]`);
- if (parsed) {
- if (parsed.isRange && parsed.rangeStart && parsed.rangeEnd) {
- setInitialTab('range');
- setInitialRangeStart(parseExpressionString(parsed.rangeStart, getAllVariables()));
- setInitialRangeEnd(parseExpressionString(parsed.rangeEnd, getAllVariables()));
- setInitialExpression(null);
- } else if (parsed.indexExpression) {
- setInitialTab('single');
- setInitialExpression(parseExpressionString(parsed.indexExpression, getAllVariables()));
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
- }
- }
+ // Prefill dialog with existing index if any
+ if (leftSideIndexExprElems && leftSideIndexExprElems.length > 0) {
+ setInitialTab('single');
+ setInitialExpression(new Expression(undefined, leftSideIndexExprElems.map(e => e.clone())));
} else {
- setLeftSideIndexString(null);
+ setInitialTab('single');
+ setInitialExpression(new Expression(undefined, []));
}
setIsIndexDialogOpen(true);
} else {
- setLeftSideIndexString(null);
+ setLeftSideIndexExprElems(null);
}
};
@@ -617,64 +648,26 @@ const VariableAssignmentEditor = () => {
if (expr.isEmpty()) {
// Treat as cancel – clear variable selection
setLeftSideVariable('');
- setLeftSideIndexString(null);
+ setLeftSideIndexExprElems(null);
} else {
- setLeftSideIndexString(expr.toString());
+ setLeftSideIndexExprElems(expr.rightSide.map(e => e.clone()));
}
setIsEditingLeftSideIndex(false);
setIsIndexDialogOpen(false);
return;
}
if (arrayVariableForIndex) {
- const exprStr = expr.toString();
- const newValue = `${arrayVariableForIndex.name}[${exprStr}]`;
-
- if (editingElement) {
- // Replace existing element
- setExpression(prev => {
- if (!prev) return null;
- const newExpr = prev.clone();
- const elementToUpdate = newExpr.findElement(editingElement.id);
- if (elementToUpdate) {
- elementToUpdate.value = newValue;
- }
- return newExpr;
- });
- } else {
- // Add new element
- const element = new ExpressionElement(crypto.randomUUID(), 'literal', newValue);
- setExpression(prev => {
- if (!prev) return null;
- const newExpr = prev.clone();
- newExpr.addElement(element);
- return newExpr;
- });
- }
- }
-
- // Reset state
- setEditingElement(null);
- setInitialExpression(null);
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
- setIsIndexDialogOpen(false);
- };
-
- // Handle dialog submission for range editing
- const handleDialogSubmitRange = (_: 'range', start: Expression, end: Expression) => {
- if (isEditingLeftSideIndex && arrayVariableForIndex) {
- if (start.isEmpty() || end.isEmpty()) {
- setLeftSideVariable('');
- setLeftSideIndexString(null);
- } else {
- setLeftSideIndexString(`${start.toString()}:${end.toString()}`);
+ if (expr.isEmpty()) {
+ // Ignore empty index
+ setEditingElement(null);
+ setIsIndexDialogOpen(false);
+ setArrayVariableForIndex(null);
+ return;
}
- setIsEditingLeftSideIndex(false);
- setIsIndexDialogOpen(false);
- return;
- }
- if (arrayVariableForIndex) {
- const newValue = `${arrayVariableForIndex.name}[${start.toString()}:${end.toString()}]`;
+ const elemVar = arrayVariableForIndex as Variable;
+ const idxExpr = expr.rightSide.map(e => e.clone());
+ const variableClone = elemVar.clone();
+ variableClone.indexExpression = idxExpr;
if (editingElement) {
// Replace existing element
@@ -683,13 +676,14 @@ const VariableAssignmentEditor = () => {
const newExpr = prev.clone();
const elementToUpdate = newExpr.findElement(editingElement.id);
if (elementToUpdate) {
- elementToUpdate.value = newValue;
+ elementToUpdate.type = 'variable';
+ elementToUpdate.setVariable(variableClone);
}
return newExpr;
});
} else {
// Add new element
- const element = new ExpressionElement(crypto.randomUUID(), 'literal', newValue);
+ const element = new ExpressionElement(crypto.randomUUID(), 'variable', '', variableClone);
setExpression(prev => {
if (!prev) return null;
const newExpr = prev.clone();
@@ -702,8 +696,6 @@ const VariableAssignmentEditor = () => {
// Reset state
setEditingElement(null);
setInitialExpression(null);
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
setIsIndexDialogOpen(false);
};
@@ -711,11 +703,9 @@ const VariableAssignmentEditor = () => {
const handleDialogCancel = () => {
setEditingElement(null);
setInitialExpression(null);
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
setIsIndexDialogOpen(false);
- if (isEditingLeftSideIndex && !leftSideIndexString) {
+ if (isEditingLeftSideIndex && leftSideIndexExprElems && leftSideIndexExprElems.length === 0) {
// User canceled before providing an index -> deselect variable
setLeftSideVariable('');
setIsEditingLeftSideIndex(false);
@@ -725,28 +715,17 @@ const VariableAssignmentEditor = () => {
// Handler to edit existing array access elements on RHS (dragged literals)
const handleEditArrayAccess = (element: ExpressionElement) => {
- if (!element.value.includes('[') || !element.value.includes(']')) return;
+ if (!element.variable || !(element.variable instanceof Variable)) return;
+ if (element.variable.type !== 'array' || !element.variable.indexExpression || element.variable.indexExpression.length === 0) return;
- const parsed = parseArrayAccess(element.value);
- if (!parsed) return;
-
- const arrayVar = getAllVariables().find(v => v.name === parsed.arrayName && v.type === 'array');
- if (!arrayVar) return;
+ const elemVar = element.variable as Variable;
+ const arrayVar = getAllVariables().find(v => v.id === elemVar.id && v.type === 'array') || elemVar;
setEditingElement(element);
setArrayVariableForIndex(arrayVar);
- if (parsed.isRange && parsed.rangeStart && parsed.rangeEnd) {
- setInitialTab('range');
- setInitialRangeStart(parseExpressionString(parsed.rangeStart, getAllVariables()));
- setInitialRangeEnd(parseExpressionString(parsed.rangeEnd, getAllVariables()));
- setInitialExpression(null);
- } else if (parsed.indexExpression) {
- setInitialTab('single');
- setInitialExpression(parseExpressionString(parsed.indexExpression, getAllVariables()));
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
- }
+ setInitialTab('single');
+ setInitialExpression(new Expression(undefined, elemVar.indexExpression!.map(e => e.clone())));
setIsIndexDialogOpen(true);
};
@@ -763,10 +742,10 @@ const VariableAssignmentEditor = () => {
variableName={arrayVariableForIndex?.name || ''}
onCancel={handleDialogCancel}
onSubmit={handleDialogSubmit}
- onSubmitRange={handleDialogSubmitRange}
+ onSubmitRange={undefined as any}
initialExpression={initialExpression || undefined}
- initialRangeStart={initialRangeStart || undefined}
- initialRangeEnd={initialRangeEnd || undefined}
+ initialRangeStart={undefined as any}
+ initialRangeEnd={undefined as any}
initialTab={initialTab}
/>
{
setArrayVariableForIndex(selectedVar);
setIsEditingLeftSideIndex(true);
// Prefill dialog with existing index if any
- if (leftSideIndexString) {
- const parsed = parseArrayAccess(`${selectedVar.name}[${leftSideIndexString}]`);
- if (parsed) {
- if (parsed.isRange && parsed.rangeStart && parsed.rangeEnd) {
- setInitialTab('range');
- setInitialRangeStart(parseExpressionString(parsed.rangeStart, getAllVariables()));
- setInitialRangeEnd(parseExpressionString(parsed.rangeEnd, getAllVariables()));
- setInitialExpression(null);
- } else if (parsed.indexExpression) {
- setInitialTab('single');
- setInitialExpression(parseExpressionString(parsed.indexExpression, getAllVariables()));
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
- }
- }
+ if (leftSideIndexExprElems && leftSideIndexExprElems.length > 0) {
+ setInitialTab('single');
+ setInitialExpression(new Expression(undefined, leftSideIndexExprElems.map(e => e.clone())));
} else {
setInitialTab('single');
setInitialExpression(new Expression(undefined, []));
- setInitialRangeStart(null);
- setInitialRangeEnd(null);
}
setIsIndexDialogOpen(true);
}}
>
- {leftSideIndexString ? `[${leftSideIndexString}]` : '[]'}
+ {getIndexString() ? `[${getIndexString()}]` : '[]'}
);
}
@@ -879,18 +844,19 @@ const VariableAssignmentEditor = () => {
element={element}
removeExpressionElement={removeExpressionElement}
disabled={isRunning}
+ onEdit={handleEditArrayAccess}
/>
);
}
return (
-
+
);
})}
diff --git a/flowming/src/components/Panel/Tabs/editors/VariableDeclarationEditor.tsx b/flowming/src/components/Panel/Tabs/editors/VariableDeclarationEditor.tsx
index f7b57ba..45a5505 100644
--- a/flowming/src/components/Panel/Tabs/editors/VariableDeclarationEditor.tsx
+++ b/flowming/src/components/Panel/Tabs/editors/VariableDeclarationEditor.tsx
@@ -22,6 +22,7 @@ const VariableDeclarationEditor = () => {
const [variables, setVariables] = useState([]);
const [arraySizeInputs, setArraySizeInputs] = useState>({});
const [arraySizeErrors, setArraySizeErrors] = useState>({});
+ const [nameErrors, setNameErrors] = useState>({});
const isInitialLoadRef = useRef(true);
const updateTimeoutRef = useRef(null);
const previousNodeIdRef = useRef(null);
@@ -138,6 +139,15 @@ const VariableDeclarationEditor = () => {
updates.arraySize = v.arraySize !== undefined && v.arraySize > 0 ? v.arraySize : 10;
}
+ if (field === 'name') {
+ const newName = String(value);
+ const isValid = Variable.isValidName(newName);
+ setNameErrors(prevErrs => ({ ...prevErrs, [id]: !isValid }));
+ if (!isValid) {
+ return v; // Skip invalid update
+ }
+ }
+
return v.update(updates);
}
return v;
@@ -163,40 +173,51 @@ const VariableDeclarationEditor = () => {
{variables.map((variable) => (
-
-
-
-
updateVariable(variable.id, 'name', e.target.value)}
- placeholder="Variable name"
- className="flex-1"
- disabled={isRunning}
- />
-
- {variables.length > 1 && (
-
diff --git a/flowming/src/models/Expression.ts b/flowming/src/models/Expression.ts
index a7b4064..c055d67 100644
--- a/flowming/src/models/Expression.ts
+++ b/flowming/src/models/Expression.ts
@@ -1,8 +1,8 @@
-import { Variable, VariableType, ValueTypeMap } from './Variable';
+import { Variable, VariableType, ValueTypeMap, ArraySubtype } from './Variable';
import { ExpressionElement } from './ExpressionElement';
import { ValuedVariable } from './ValuedVariable';
import { buildAST } from './ExpressionParser';
-import { IVariable } from './Variable';
+import type { IVariable } from './IVariable';
export const operators = ['+', '-', '*', '/', '!', '%', '&&', '||', '==', '!=', '>', '<', '>=', '<=', '(', ')'];
export type IOperator = typeof operators[number];
@@ -46,13 +46,19 @@ interface BinaryOpNode extends BaseASTNode {
right: ExpressionASTNode;
}
+interface MemberAccessNode extends BaseASTNode {
+ type: 'MemberAccess';
+ object: IdentifierNode;
+ property: ExpressionASTNode;
+}
+
interface FunctionCallNode extends BaseASTNode {
type: 'FunctionCall';
functionName: 'integer' | 'string' | 'float' | 'boolean';
argument: ExpressionASTNode;
}
-type ExpressionASTNode = LiteralNode | IdentifierNode | UnaryOpNode | BinaryOpNode | FunctionCallNode;
+type ExpressionASTNode = LiteralNode | IdentifierNode | UnaryOpNode | BinaryOpNode | FunctionCallNode | MemberAccessNode;
export class Expression implements IExpression {
@@ -60,7 +66,11 @@ export class Expression implements IExpression {
rightSide: ExpressionElement[];
equality?: IEquality;
- constructor(leftSide: Variable | ExpressionElement[] | undefined, rightSide: ExpressionElement[] = [], equality?: IEquality) {
+ constructor(
+ leftSide: Variable | ExpressionElement[] | undefined,
+ rightSide: ExpressionElement[] = [],
+ equality?: IEquality
+ ) {
if(leftSide != undefined){
if (equality && (!Array.isArray(leftSide) || leftSide.some(e => !(e instanceof ExpressionElement)))) {
throw new Error('leftSide must be an array of ExpressionElement instances');
@@ -96,7 +106,8 @@ export class Expression implements IExpression {
calculateValue(
exprElements: ExpressionElement[],
exprTyping: VariableType | null,
- currentValuedVariables: ValuedVariable[]
+ currentValuedVariables: ValuedVariable[],
+ arraySubtype?: ArraySubtype
): ValueTypeMap[VariableType] {
try {
if (exprElements.length === 0) {
@@ -123,6 +134,14 @@ export class Expression implements IExpression {
if (exprTyping === 'integer' && result.type === 'float') {
return Math.floor(Number(result.value));
}
+
+ if (exprTyping === 'array') {
+ // Make sure that the result is the correct type for the array subtype
+ if (result.type !== arraySubtype) {
+ throw new Error(`Type mismatch: Cannot assign a value of type '${result.type}' to a variable of type '${arraySubtype}'.`);
+ }
+ return result.value;
+ }
throw new Error(`Type mismatch: Cannot assign a value of type '${result.type}' to a variable of type '${exprTyping}'.`);
}
@@ -147,6 +166,37 @@ export class Expression implements IExpression {
case 'Identifier':
const v = currentValuedVariables.find(vv => vv.id === node.variable.id);
if (!v) throw new Error(`Variable "${node.name}" does not have a value assigned.`);
+
+ // If the identifier represents an array element (indexExpression present), evaluate the index and return that element
+ if (node.variable.type === 'array' && node.variable.indexExpression && node.variable.indexExpression.length > 0) {
+ let idx: number;
+ try {
+ idx = this.calculateValue(
+ node.variable.indexExpression as any,
+ 'integer',
+ currentValuedVariables
+ ) as number;
+ } catch (err: any) {
+ const msg = err?.message ?? '';
+ if (msg.includes('Type mismatch')) {
+ throw new Error(`Array index for "${node.name}" must be an integer.`);
+ }
+ // Propagate all other errors untouched
+ throw err;
+ }
+ if (!Number.isInteger(idx)) { // This should never happen, as the index expression is already validated to be an integer in calculateValue
+ throw new Error(`Array index for "${node.name}" must be an integer.`);
+ }
+
+ const arr = Array.isArray(v.value) ? v.value : [];
+ const size = v.arraySize ?? arr.length;
+ if (idx < 0 || idx >= size) {
+ throw new Error(`Array index ${idx} is out of bounds for "${node.name}" (size ${size}).`);
+ }
+ const subtype = v.arraySubtype!;
+ return { value: arr[idx], type: subtype };
+ }
+
return { value: v.value, type: v.type };
case 'FunctionCall':
@@ -281,10 +331,124 @@ export class Expression implements IExpression {
* and the calculated value from the expression and the current values for variables
*/
assignValue(currentValuedVariables: ValuedVariable[]): ValuedVariable {
- if (!(this.leftSide instanceof Variable)) throw new Error('leftSide must be a Variable instance');
+ // NOTE: operations between arrays will be always single values, as you must always specify an index for the array
+ // This means that operations like concatenation and so on are not allowed between whole arrays
+ // So the return value will always be a single value (and we only care for type checking for the subtype)
- const value = this.calculateValue(this.rightSide, this.leftSide.type, currentValuedVariables);
- return new ValuedVariable(this.leftSide.id, this.leftSide.type, this.leftSide.name, this.leftSide.nodeId, value);
+ // NOTE: calculateValue for array element returns the whole array with the corresponding index modified
+ // the variable value holds the whole array (for debugging purposes and future implementations)
+
+ if (this.leftSide instanceof Variable) {
+ // Array variable assignment requires indexExpression stored in Variable
+ if (this.leftSide.type === 'array') {
+ if (!this.leftSide.indexExpression || this.leftSide.indexExpression.length === 0) {
+ throw new Error(`Cannot assign directly to array variable "${this.leftSide.name}". Use array index access instead (e.g., ${this.leftSide.name}[index] = value).`);
+ }
+ return this.#assignArrayValue(this.leftSide, this.leftSide.indexExpression, currentValuedVariables);
+ }
+
+ const value = this.calculateValue(this.rightSide, this.leftSide.type, currentValuedVariables, this.leftSide.arraySubtype);
+ return new ValuedVariable(
+ this.leftSide.id,
+ this.leftSide.type,
+ this.leftSide.name,
+ this.leftSide.nodeId,
+ value as Exclude, // Type is already validated as non-array above
+ this.leftSide.arraySubtype,
+ this.leftSide.arraySize
+ );
+ } else {
+ throw new Error('leftSide must be a Variable instance');
+ }
+ }
+
+ /**
+ * Handles array index assignments like arr[i-2] = value
+ *
+ * Note: This method performs complex AST operations which can take significant time (non-trivial).
+ * (this is not a problem for now)
+ *
+ * @param arrayVarDef The array variable definition
+ * @param indexExprElements The array access expression elements
+ * @param currentValuedVariables The current state of variables
+ * @returns The updated ValuedVariable with the modified array
+ */
+ #assignArrayValue(arrayVarDef: Variable, indexExprElements: ExpressionElement[], currentValuedVariables: ValuedVariable[]): ValuedVariable {
+ try {
+ // Construct tokens: Variable token, '[' token, ...indexExprElements, ']' token
+ const arrayVarElement = new ExpressionElement(crypto.randomUUID(), 'variable', arrayVarDef.name, arrayVarDef);
+ const openBracket = new ExpressionElement(crypto.randomUUID(), 'operator', '[');
+ const closeBracket = new ExpressionElement(crypto.randomUUID(), 'operator', ']');
+
+ const tokens: ExpressionElement[] = [arrayVarElement, openBracket, ...indexExprElements, closeBracket];
+ const leftAST = buildAST(tokens);
+
+ if (leftAST.type !== 'MemberAccess') {
+ throw new Error('Left side of array assignment must be an array access expression (e.g., arr[index])');
+ }
+
+ const memberAccess = leftAST as MemberAccessNode;
+
+ // Find (or lazily initialize) the target array variable in the current valued variables set
+ let arrayVar = currentValuedVariables.find(vv => vv.id === memberAccess.object.variable.id);
+
+ // If the array variable has not been initialized yet (i.e., it has been declared but no
+ // valued-variable instance exists in the current execution scope), create it on the fly so
+ // that the assignment can proceed. This situation can arise when the DeclareVariable node
+ // has executed but, due to asynchronous propagation of node data, the valued variable did
+ // not reach this node before the assignment executes.
+ if (!arrayVar) {
+ arrayVar = ValuedVariable.fromVariable(memberAccess.object.variable, null);
+ currentValuedVariables.push(arrayVar);
+ }
+
+ // Validate array structure and constraints
+ if (!Array.isArray(arrayVar.value)) {
+ throw new Error(`Variable "${memberAccess.object.name}" is not a valid array.`);
+ }
+
+ const subtype = arrayVar.arraySubtype;
+ const arraySize = arrayVar.arraySize;
+ if (!subtype) throw new Error(`Array "${memberAccess.object.name}" does not have a defined subtype.`);
+ if (!arraySize || arraySize < 1) throw new Error(`Array "${memberAccess.object.name}" does not have a valid size defined.`);
+
+ // Enforce fixed-length constraint
+ if (arrayVar.value.length !== arraySize) {
+ throw new Error(`Array "${memberAccess.object.name}" has incorrect length: expected ${arraySize}, but got ${arrayVar.value.length}.`);
+ }
+
+ // Evaluate the index expression
+ const indexResult = this.#evaluateAST(memberAccess.property, currentValuedVariables);
+ if (indexResult.type !== 'integer') {
+ throw new Error(`Array index for "${memberAccess.object.name}" must be an integer, but got ${indexResult.type}.`);
+ }
+ const index = indexResult.value;
+
+ // Bounds checking
+ if (index < 0 || index >= arraySize) {
+ throw new Error(`Array index ${index} is out of bounds for "${memberAccess.object.name}" (fixed size ${arraySize}).`);
+ }
+
+ // Evaluate the right side to get the new value
+ const newValue = this.calculateValue(this.rightSide, subtype, currentValuedVariables);
+
+ // Create a copy of the array and update the specified index
+ const updatedArray = [...arrayVar.value];
+ updatedArray[index] = newValue;
+
+ // Return the updated ValuedVariable with the modified array
+ return new ValuedVariable(
+ arrayVar.id,
+ arrayVar.type,
+ arrayVar.name,
+ arrayVar.nodeId,
+ updatedArray,
+ arrayVar.arraySubtype,
+ arrayVar.arraySize
+ );
+ } catch (e) {
+ throw e;
+ }
}
/**
@@ -411,20 +575,6 @@ export class Expression implements IExpression {
return this;
}
- /**
- * Inserts an element at a specific position
- */
- insertElementAt(element: ExpressionElement, index: number): void {
- this.rightSide.splice(index, 0, element);
- }
-
- /**
- * Updates the variable in the leftSide of the expression
- */
- updateLeftSide(variable: Variable): void {
- this.leftSide = variable;
- }
-
/**
* Updates the expression with new values
*/
@@ -441,16 +591,40 @@ export class Expression implements IExpression {
* Updates the variables in the expression
*/
updateVariables(variables: Variable[]): void {
+ // Helper to update ExpressionElement lists based on current variable set, removing missing vars
+ const processElements = (elements: ExpressionElement[]): ExpressionElement[] => {
+ return elements.flatMap(elem => {
+ if (elem.type !== 'variable') return [elem];
+ const vMatch = variables.find(vv => vv.id === elem.variable?.id);
+ if (!vMatch) return []; // Variable deleted -> remove element
+ const elemVarClone = vMatch.clone();
+ if (elem.variable?.indexExpression && elemVarClone.type === 'array') {
+ elemVarClone.indexExpression = processElements(elem.variable.indexExpression as ExpressionElement[]);
+ }
+ if (elem.variable?.indexExpression && (!elemVarClone.indexExpression || elemVarClone.indexExpression.length === 0)) {
+ return []; // index expression emptied -> remove the variable element entirely
+ }
+ return [new ExpressionElement(elem.id, elem.type, elemVarClone.toString(), elemVarClone)];
+ });
+ };
+
// Update the leftSide variable if it exists in the variable list
- if(this.leftSide != undefined){
+ if (this.leftSide != undefined) {
if (this.leftSide instanceof Variable) {
+ const existingIndexExpr = this.leftSide.indexExpression;
const leftSideVariable = variables.find(v => v.id === (this.leftSide as Variable).id);
if (!leftSideVariable) {
- // If no left side variable is found, expression is invalid
- // TODO: do not unmount expression when LHS variable deselected or not found (?)
- throw new Error(`Variable with id ${this.leftSide.id} not found`);
+ // If no left side variable is found, expression is invalid
+ // TODO: do not unmount expression when LHS variable deselected or not found (?)
+ throw new Error(`Variable with id ${this.leftSide.id} not found`);
+ }
+
+ // Preserve array index expression (if any) when replacing the variable reference.
+ const updatedVar = leftSideVariable.clone();
+ if (existingIndexExpr && updatedVar.type === 'array') {
+ updatedVar.indexExpression = existingIndexExpr;
}
- this.leftSide = leftSideVariable;
+ this.leftSide = updatedVar;
} else {
this.leftSide = this.leftSide?.flatMap(e => {
if (e.type !== 'variable') return [e];
@@ -458,7 +632,16 @@ export class Expression implements IExpression {
// If no variable is found, delete expression element (by flattening [])
if (!variable) return [];
- const expressionElement = new ExpressionElement(e.id, e.type, variable.name, variable);
+ // Preserve indexExpression if present
+ const updatedVar = variable.clone();
+ if (e.variable?.indexExpression && updatedVar.type === 'array') {
+ updatedVar.indexExpression = processElements(e.variable.indexExpression as ExpressionElement[]);
+ }
+ if (e.variable?.indexExpression && (!updatedVar.indexExpression || updatedVar.indexExpression.length === 0)) {
+ // index became empty -> remove entire variable element
+ return [];
+ }
+ const expressionElement = new ExpressionElement(e.id, e.type, updatedVar.toString(), updatedVar);
return [expressionElement];
});
}
@@ -470,7 +653,16 @@ export class Expression implements IExpression {
// If no variable is found, delete expression element (by flattening [])
if (!variable) return [];
- const expressionElement = new ExpressionElement(e.id, e.type, variable.name, variable);
+ // Preserve indexExpression if present
+ const updatedVar = variable.clone();
+ if (e.variable?.indexExpression && updatedVar.type === 'array') {
+ updatedVar.indexExpression = processElements(e.variable.indexExpression as ExpressionElement[]);
+ }
+ if (e.variable?.indexExpression && (!updatedVar.indexExpression || updatedVar.indexExpression.length === 0)) {
+ // index became empty -> remove entire variable element
+ return [];
+ }
+ const expressionElement = new ExpressionElement(e.id, e.type, updatedVar.toString(), updatedVar);
return [expressionElement];
});
}
@@ -481,9 +673,11 @@ export class Expression implements IExpression {
isEmpty(): boolean {
if(this.leftSide != undefined){
if(this.equality) {
+ // For conditionals, leftSide is ExpressionElement[]
return (this.leftSide as ExpressionElement[]).length === 0 && this.rightSide.length === 0;
}
- return !(this.leftSide as Variable) && this.rightSide.length === 0;
+ // Array access assignment is ExpressionElement[] (?)
+ return (!(this.leftSide as Variable) || (this.leftSide as ExpressionElement[]).length === 0) && this.rightSide.length === 0;
}
return this.rightSide.length === 0;
diff --git a/flowming/src/models/ExpressionElement.ts b/flowming/src/models/ExpressionElement.ts
index b59d8b6..2cb71af 100644
--- a/flowming/src/models/ExpressionElement.ts
+++ b/flowming/src/models/ExpressionElement.ts
@@ -1,35 +1,28 @@
-import { IVariable, Variable } from './Variable';
+import { Variable } from './Variable';
+import type { IVariable } from './IVariable';
import { Expression } from './Expression';
+import type { IExpressionElement } from './IExpressionElement';
export type ExpressionElementType = 'variable' | 'operator' | 'literal' | 'function';
-
-export interface IExpressionElement {
- id: string;
- type: ExpressionElementType;
- value: string;
- variable?: IVariable;
- nestedExpression?: any;
-}
-
export class ExpressionElement implements IExpressionElement {
id: string;
type: ExpressionElementType;
value: string;
- variable?: Variable;
+ variable?: IVariable;
nestedExpression?: Expression;
constructor(
id: string,
type: ExpressionElementType,
value: string,
- variableOrNested?: Variable | Expression
+ variableOrNested?: IVariable | Expression
) {
this.id = id;
this.type = type;
this.value = value;
if (type === 'variable' && variableOrNested instanceof Variable) {
this.variable = variableOrNested;
- this.value = variableOrNested.name;
+ this.value = variableOrNested.toString();
} else if (type === 'function' && variableOrNested instanceof Expression) {
this.nestedExpression = variableOrNested;
}
@@ -38,12 +31,12 @@ export class ExpressionElement implements IExpressionElement {
/**
* Sets the associated variable for this element
*/
- setVariable(variable: Variable): void {
+ setVariable(variable: IVariable): void {
if (this.type !== 'variable') {
throw new Error('Cannot set variable on non-variable element');
}
this.variable = variable;
- this.value = variable.name;
+ this.value = variable.toString();
}
/**
@@ -95,7 +88,7 @@ export class ExpressionElement implements IExpressionElement {
this.id,
this.type,
this.value,
- this.type === 'variable' ? this.variable : this.type === 'function' ? this.nestedExpression?.clone() : undefined
+ this.type === 'variable' ? this.variable?.clone() : this.type === 'function' ? this.nestedExpression?.clone() : undefined
);
if (this.type === 'function' && this.nestedExpression) {
cloned.nestedExpression = this.nestedExpression.clone();
@@ -106,12 +99,12 @@ export class ExpressionElement implements IExpressionElement {
/**
* Creates an object representation of the expression element
*/
- toObject(): IExpressionElement {
+ toObject(): any {
return {
id: this.id,
type: this.type,
value: this.value,
- variable: this.variable?.toObject(),
+ variable: this.variable ? this.variable.toObject() : undefined,
nestedExpression: this.nestedExpression?.toObject()
};
}
diff --git a/flowming/src/models/ExpressionParser.ts b/flowming/src/models/ExpressionParser.ts
index e2a94d3..5179171 100644
--- a/flowming/src/models/ExpressionParser.ts
+++ b/flowming/src/models/ExpressionParser.ts
@@ -30,13 +30,19 @@ export interface BinaryOpNode extends BaseASTNode {
right: ExpressionASTNode;
}
+export interface MemberAccessNode extends BaseASTNode {
+ type: 'MemberAccess';
+ object: IdentifierNode;
+ property: ExpressionASTNode;
+}
+
export interface FunctionCallNode extends BaseASTNode {
type: 'FunctionCall';
functionName: 'integer' | 'string' | 'float' | 'boolean';
argument: ExpressionASTNode;
}
-export type ExpressionASTNode = LiteralNode | IdentifierNode | UnaryOpNode | BinaryOpNode | FunctionCallNode;
+export type ExpressionASTNode = LiteralNode | IdentifierNode | UnaryOpNode | BinaryOpNode | FunctionCallNode | MemberAccessNode;
// Parser Implementation
export function buildAST(elements: ExpressionElement[]): ExpressionASTNode {
@@ -161,8 +167,20 @@ export function buildAST(elements: ExpressionElement[]): ExpressionASTNode {
return parseLiteral(tok.value);
}
if (tok.isVariable() && tok.variable) {
- next();
- return { type: 'Identifier', name: tok.variable.name, variable: tok.variable };
+ const idNode: IdentifierNode = { type: 'Identifier', name: tok.variable.name, variable: tok.variable };
+ next(); // Consume identifier token
+
+ if (peek()?.value === '[') {
+ if (idNode.variable.type !== 'array') {
+ throw new Error(`Variable "${idNode.name}" is not an array and cannot be indexed.`);
+ }
+ next(); // Consume '['
+ const indexExpr = parseEquality();
+ expectOperator(']');
+ return { type: 'MemberAccess', object: idNode, property: indexExpr };
+ }
+
+ return idNode;
}
if (tok.isFunction()) {
const fnTok = next();
diff --git a/flowming/src/models/IExpressionElement.ts b/flowming/src/models/IExpressionElement.ts
new file mode 100644
index 0000000..6fa46c2
--- /dev/null
+++ b/flowming/src/models/IExpressionElement.ts
@@ -0,0 +1,19 @@
+import type { IVariable, ExpressionElementType } from '.';
+
+export interface IExpressionElement {
+ id: string;
+ type: ExpressionElementType;
+ value: string;
+ variable?: IVariable;
+ nestedExpression?: any;
+
+ // Methods
+ setVariable(variable: IVariable): void;
+ toString(): string;
+ isVariable(): boolean;
+ isOperator(): boolean;
+ isLiteral(): boolean;
+ isFunction(): boolean;
+ clone(): IExpressionElement;
+ toObject(): any;
+}
\ No newline at end of file
diff --git a/flowming/src/models/IVariable.ts b/flowming/src/models/IVariable.ts
new file mode 100644
index 0000000..ce7a22a
--- /dev/null
+++ b/flowming/src/models/IVariable.ts
@@ -0,0 +1,19 @@
+import type { VariableType, ArraySubtype, IExpressionElement } from '.';
+
+export interface IVariable {
+ id: string;
+ type: VariableType;
+ name: string;
+ nodeId: string; // ID of the node that declared this variable
+ arraySubtype?: ArraySubtype; // Only applicable when type is 'array'
+ arraySize?: number; // Only applicable when type is 'array'
+ indexExpression?: IExpressionElement[]; // Only applicable when type is 'array'
+
+ // Methods
+ toString(): string;
+ toDeclarationString(): string;
+ update(updates: Partial): IVariable;
+ clone(): IVariable;
+ isEqual(other: IVariable): boolean;
+ toObject(): any;
+}
\ No newline at end of file
diff --git a/flowming/src/models/ValuedVariable.ts b/flowming/src/models/ValuedVariable.ts
index eb06b7b..0cc1194 100644
--- a/flowming/src/models/ValuedVariable.ts
+++ b/flowming/src/models/ValuedVariable.ts
@@ -1,4 +1,5 @@
-import { IVariable, ValueTypeMap, Variable, VariableType, ArraySubtype } from "./Variable";
+import { ValueTypeMap, Variable, VariableType, ArraySubtype } from "./Variable";
+import { IVariable } from './IVariable';
export interface IValuedVariable extends IVariable {
value: ValueTypeMap[T];
@@ -22,10 +23,18 @@ export class ValuedVariable extends Variable implements
return `${this.name}: ${this.value}`;
}
+ /**
+ * Clones the valued variable
+ */
+ clone(): ValuedVariable {
+ const clonedValue = Array.isArray(this.value) ? [...this.value] : this.value;
+ return new ValuedVariable(this.id, this.type as T, this.name, this.nodeId, clonedValue as ValueTypeMap[T], this.arraySubtype, this.arraySize);
+ }
+
/**
* Creates an object representation of the valued variable
*/
- toObject(): IValuedVariable {
+ toObject(): any {
return {
id: this.id,
type: this.type,
@@ -71,7 +80,7 @@ export class ValuedVariable extends Variable implements
initializedValue = false as ValueTypeMap[T];
break;
case 'array':
- // Initialize array with default values based on subtype and size
+ // Array is initialized by filling size with default values based on subtype
const size = variable.arraySize || 10;
const subtype = variable.arraySubtype || 'integer';
let defaultValue: any;
diff --git a/flowming/src/models/Variable.ts b/flowming/src/models/Variable.ts
index 11f7130..7b0b2b3 100644
--- a/flowming/src/models/Variable.ts
+++ b/flowming/src/models/Variable.ts
@@ -1,3 +1,7 @@
+import type { IVariable } from './IVariable';
+import { ExpressionElement } from './ExpressionElement';
+import type { IExpressionElement } from './IExpressionElement';
+
// Available variable types
export const variableTypes = [
'integer',
@@ -27,16 +31,6 @@ export type ValueTypeMap = {
array: any[];
};
-// Define the variable structure
-export interface IVariable {
- id: string;
- type: VariableType;
- name: string;
- nodeId: string; // ID of the node that declared this variable
- arraySubtype?: ArraySubtype; // Only applicable when type is 'array'
- arraySize?: number; // Only applicable when type is 'array'
-}
-
export class Variable implements IVariable {
id: string;
type: VariableType;
@@ -44,17 +38,18 @@ export class Variable implements IVariable {
nodeId: string;
arraySubtype?: ArraySubtype;
arraySize?: number;
+ indexExpression?: IExpressionElement[];
- constructor(id: string, type: VariableType, name: string, nodeId: string, arraySubtype?: ArraySubtype, arraySize?: number) {
+ constructor(id: string, type: VariableType, name: string, nodeId: string, arraySubtype?: ArraySubtype, arraySize?: number, indexExpression?: IExpressionElement[]) {
this.id = id;
this.type = type;
this.name = name;
this.nodeId = nodeId;
+ this.indexExpression = indexExpression;
// Ensure array metadata is always valid when the variable is an array
if (type === 'array') {
- // Fallback to integer subtype if none provided (should not normally happen)
- this.arraySubtype = arraySubtype ?? 'integer';
+ this.arraySubtype = arraySubtype;
// Enforce a positive, non-zero size. Default to 1 if invalid.
const validSize = arraySize !== undefined && arraySize >= 1 ? arraySize : 1;
this.arraySize = validSize;
@@ -68,6 +63,12 @@ export class Variable implements IVariable {
* Creates a string representation of the variable
*/
toString(): string {
+ // Render array element access when an index expression exists
+ if (this.indexExpression && this.indexExpression.length > 0) {
+ // Manually join to avoid circular dependency on Expression.toString()
+ const idxExprStr = this.indexExpression.map(e => (e as ExpressionElement).toString()).join(' ');
+ return `${this.name}[${idxExprStr}]`;
+ }
return this.name;
}
@@ -106,7 +107,8 @@ export class Variable implements IVariable {
updates.name ?? this.name,
updates.nodeId ?? this.nodeId,
updates.hasOwnProperty('arraySubtype') ? updates.arraySubtype : this.arraySubtype,
- updates.hasOwnProperty('arraySize') ? updates.arraySize : this.arraySize
+ updates.hasOwnProperty('arraySize') ? updates.arraySize : this.arraySize,
+ updates.hasOwnProperty('indexExpression') ? updates.indexExpression : this.indexExpression
);
}
@@ -114,7 +116,8 @@ export class Variable implements IVariable {
* Clones the variable
*/
clone(): Variable {
- return new Variable(this.id, this.type, this.name, this.nodeId, this.arraySubtype, this.arraySize);
+ const clonedIndexExpr = this.indexExpression ? this.indexExpression.map(e => e.clone()) : undefined;
+ return new Variable(this.id, this.type, this.name, this.nodeId, this.arraySubtype, this.arraySize, clonedIndexExpr);
}
/**
@@ -127,14 +130,15 @@ export class Variable implements IVariable {
/**
* Creates an object representation of the variable
*/
- toObject(): IVariable {
+ toObject(): any {
return {
id: this.id,
type: this.type,
name: this.name,
nodeId: this.nodeId,
arraySubtype: this.arraySubtype,
- arraySize: this.arraySize
+ arraySize: this.arraySize,
+ indexExpression: this.indexExpression ? this.indexExpression.map(e => e.toObject()) : undefined,
};
}
@@ -142,7 +146,17 @@ export class Variable implements IVariable {
* Creates a Variable from a plain object
*/
static fromObject(obj: IVariable): Variable {
- return new Variable(obj.id, obj.type as VariableType, obj.name, obj.nodeId, obj.arraySubtype, obj.arraySize);
+ const idxExpr = obj.indexExpression
+ ? obj.indexExpression.map(e => ExpressionElement.fromObject(e))
+ : undefined;
+ return new Variable(obj.id, obj.type as VariableType, obj.name, obj.nodeId, obj.arraySubtype, obj.arraySize, idxExpr);
+ }
+
+ /**
+ * Checks if a variable name is valid (no reserved characters like '[' or ']').
+ */
+ static isValidName(name: string): boolean {
+ return !(/\[|\]/.test(name));
}
}
diff --git a/flowming/src/models/index.ts b/flowming/src/models/index.ts
index e25bc62..2f9c712 100644
--- a/flowming/src/models/index.ts
+++ b/flowming/src/models/index.ts
@@ -1,3 +1,6 @@
-export * from './Variable';
+export * from './Expression';
export * from './ExpressionElement';
-export * from './Expression';
\ No newline at end of file
+export * from './IExpressionElement';
+export * from './Variable';
+export * from './IVariable';
+export * from './ValuedVariable';
\ No newline at end of file
diff --git a/flowming/src/models/pythonAST.ts b/flowming/src/models/pythonAST.ts
index 8490217..19836ba 100644
--- a/flowming/src/models/pythonAST.ts
+++ b/flowming/src/models/pythonAST.ts
@@ -82,6 +82,13 @@ export interface CallExpression extends Expression {
arguments: Expression[];
}
+// arr[index]
+export interface SubscriptExpression extends Expression {
+ type: 'SubscriptExpression';
+ object: Identifier;
+ index: Expression;
+}
+
// Special node for print (Python-specific convenience)
export interface PrintStatement extends Statement {
type: 'PrintStatement';
diff --git a/flowming/src/tests/ArrayCodeGeneration.test.ts b/flowming/src/tests/ArrayCodeGeneration.test.ts
new file mode 100644
index 0000000..7146f21
--- /dev/null
+++ b/flowming/src/tests/ArrayCodeGeneration.test.ts
@@ -0,0 +1,187 @@
+import { describe, it, expect } from 'vitest';
+import { generatePythonCode } from '../utils/codeGeneration';
+import { FlowNode } from '../components/Flow/FlowTypes';
+import { Edge } from '@xyflow/react';
+import { Expression } from '../models/Expression';
+import { ExpressionElement } from '../models/ExpressionElement';
+import { Variable } from '../models/Variable';
+
+// Helper functions
+const createNode = (
+ id: string,
+ type: string,
+ data: object = {},
+ position = { x: 0, y: 0 }
+): FlowNode => ({
+ id,
+ type,
+ position,
+ data: { label: `Node ${id}`, ...data },
+ selected: false,
+ dragging: false,
+ width: 100,
+ height: 50,
+});
+
+const createEdge = (
+ source: string,
+ target: string,
+ label?: 'yes' | 'no'
+): Edge => {
+ const edge: Edge = {
+ id: `e${source}-${target}`,
+ source,
+ target,
+ };
+ if (label) {
+ edge.data = { conditionalLabel: label };
+ }
+ return edge;
+};
+
+const cleanCode = (code: string) => {
+ // Removes comments and empty lines for easier assertions
+ return code
+ .split('\n')
+ .filter(line => !line.trim().startsWith('#'))
+ .join('\n')
+ .trim();
+};
+
+// arr : integer[5]
+const arrVar = new Variable('var-arr', 'array', 'arr', 'n1', 'integer', 5);
+// i : integer
+const idxVar = new Variable('var-i', 'integer', 'i', 'n1');
+
+const createLiteralEl = (value: any) => new ExpressionElement('lit', 'literal', String(value));
+const createVarEl = (v: Variable) => new ExpressionElement('v', 'variable', v.name, v);
+const createOpEl = (op: string) => new ExpressionElement('op', 'operator', op);
+
+describe('codeGeneration – arrays', () => {
+ it('should generate an assignment to an array element arr[i] = 5', () => {
+ const arrIdxVar = arrVar.clone();
+ arrIdxVar.indexExpression = [createVarEl(idxVar)];
+
+ const nodes: FlowNode[] = [
+ createNode('1', 'Start'),
+ createNode('2', 'AssignVariable', {
+ expression: new Expression(arrIdxVar, [createLiteralEl(5)]).toObject(),
+ }),
+ createNode('3', 'End'),
+ ];
+
+ const edges: Edge[] = [
+ createEdge('1', '2'),
+ createEdge('2', '3'),
+ ];
+
+ const code = generatePythonCode(nodes, edges);
+ expect(cleanCode(code)).toBe('arr[i] = 5');
+ });
+
+ it('should generate input into an array element', () => {
+ const arrIdxVar = arrVar.clone();
+ arrIdxVar.indexExpression = [createVarEl(idxVar)];
+
+ const nodes: FlowNode[] = [
+ createNode('1', 'Start'),
+ createNode('2', 'Input', { variable: arrIdxVar }),
+ createNode('3', 'End'),
+ ];
+ const edges: Edge[] = [createEdge('1', '2'), createEdge('2', '3')];
+
+ const code = generatePythonCode(nodes, edges);
+ const expected = "arr[i] = int(input(\"Enter the value of 'arr' by keyboard\"))";
+ expect(cleanCode(code)).toBe(expected);
+ });
+
+ it('should generate output of an array element', () => {
+ const arrIdxVar = arrVar.clone();
+ arrIdxVar.indexExpression = [createVarEl(idxVar)];
+ const expr = new Expression(undefined, [createVarEl(arrIdxVar)]);
+
+ const nodes: FlowNode[] = [
+ createNode('1', 'Start'),
+ createNode('2', 'Output', { expression: expr.toObject() }),
+ createNode('3', 'End'),
+ ];
+ const edges: Edge[] = [createEdge('1', '2'), createEdge('2', '3')];
+
+ const code = generatePythonCode(nodes, edges);
+ expect(cleanCode(code)).toBe('print(arr[i])');
+ });
+
+ it('should generate a conditional based on an array element', () => {
+ const arrIdxVar = arrVar.clone();
+ arrIdxVar.indexExpression = [createVarEl(idxVar)];
+ const condExpr = new Expression([createVarEl(arrIdxVar)], [createLiteralEl(0)], '>');
+
+ const nodes: FlowNode[] = [
+ createNode('1', 'Start'),
+ createNode('2', 'Conditional', { expression: condExpr.toObject() }),
+ createNode('3', 'End'),
+ ];
+ const edges: Edge[] = [
+ createEdge('1', '2'),
+ createEdge('2', '3', 'yes'),
+ createEdge('2', '3', 'no'),
+ ];
+
+ const code = generatePythonCode(nodes, edges);
+ expect(cleanCode(code)).toContain('if (arr[i] > 0):');
+ });
+
+ it('should ignore DeclareVariable for array variable in generated code', () => {
+ const nodes: FlowNode[] = [
+ createNode('1', 'Start'),
+ createNode('2', 'DeclareVariable', { variable: arrVar.toObject() }),
+ createNode('3', 'End'),
+ ];
+ const edges: Edge[] = [createEdge('1', '2'), createEdge('2', '3')];
+
+ const code = generatePythonCode(nodes, edges);
+ expect(cleanCode(code)).toBe('');
+ });
+
+ // Complex index expression scenarios
+
+ it('should generate assignment with arithmetic index arr[i + 1] = 0', () => {
+ const arrIdxVar = arrVar.clone();
+ arrIdxVar.indexExpression = [createVarEl(idxVar), createOpEl('+'), createLiteralEl(1)];
+
+ const nodes: FlowNode[] = [
+ createNode('1', 'Start'),
+ createNode('2', 'AssignVariable', {
+ expression: new Expression(arrIdxVar, [createLiteralEl(0)]).toObject(),
+ }),
+ createNode('3', 'End'),
+ ];
+ const edges: Edge[] = [createEdge('1', '2'), createEdge('2', '3')];
+
+ const code = generatePythonCode(nodes, edges);
+ expect(cleanCode(code)).toBe('arr[(i + 1)] = 0');
+ });
+
+ it('should generate assignment with function and arithmetic in index', () => {
+ // arr[int((i * 2) - 1)] = 3
+ const arrIdxVar = arrVar.clone();
+ const innerExpr = new Expression(undefined, [
+ createVarEl(idxVar), createOpEl('*'), createLiteralEl(2), createOpEl('-'), createLiteralEl(1)
+ ]);
+ const funcEl = new ExpressionElement('fn', 'function', 'integer', innerExpr);
+ arrIdxVar.indexExpression = [funcEl];
+
+ const nodes: FlowNode[] = [
+ createNode('1', 'Start'),
+ createNode('2', 'AssignVariable', {
+ expression: new Expression(arrIdxVar, [createLiteralEl(3)]).toObject(),
+ }),
+ createNode('3', 'End'),
+ ];
+ const edges: Edge[] = [createEdge('1', '2'), createEdge('2', '3')];
+
+ const code = generatePythonCode(nodes, edges);
+ const expected = 'arr[int(((i * 2) - 1))] = 3';
+ expect(cleanCode(code)).toBe(expected);
+ });
+});
\ No newline at end of file
diff --git a/flowming/src/tests/ArrayExpression.test.ts b/flowming/src/tests/ArrayExpression.test.ts
new file mode 100644
index 0000000..c869023
--- /dev/null
+++ b/flowming/src/tests/ArrayExpression.test.ts
@@ -0,0 +1,130 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { Expression } from '../models/Expression';
+import { ExpressionElement, ExpressionElementType } from '../models/ExpressionElement';
+import { Variable } from '../models/Variable';
+import { ValuedVariable } from '../models/ValuedVariable';
+
+// Helper utilities
+let idCounter = 0;
+const freshId = () => `elem-${idCounter++}`;
+beforeEach(() => { idCounter = 0; });
+
+const createElement = (
+ type: ExpressionElementType,
+ value: string,
+ variableOrNested?: Variable | Expression
+): ExpressionElement => new ExpressionElement(freshId(), type, value, variableOrNested);
+
+const createLiteral = (value: any) => createElement('literal', String(value));
+const createVariableEl = (variable: Variable) => createElement('variable', variable.name, variable);
+const createOperator = (op: string) => createElement('operator', op);
+const createFunctionEl = (fn: string, nested: Expression) => createElement('function', fn, nested);
+
+// Base array variable "arr" : integer[3]
+const arrVar = new Variable('var-arr', 'array', 'arr', 'node-1', 'integer', 3);
+// Index variable "i" : integer
+const idxVar = new Variable('var-idx', 'integer', 'i', 'node-1');
+
+// Valued variables providing runtime context
+const valuedArr = new ValuedVariable('var-arr', 'array', 'arr', 'node-1', [0, 1, 2], 'integer', 3);
+const valuedIdx = new ValuedVariable('var-idx', 'integer', 'i', 'node-1', 1);
+
+const defaultContext = [valuedArr.clone(), valuedIdx.clone()];
+
+const makeArrayVariableWithIndex = (indexElements: ExpressionElement[]) => {
+ const v = arrVar.clone();
+ v.indexExpression = indexElements;
+ return v;
+};
+
+describe('Array expressions – evaluation and assignment', () => {
+
+ describe('Reading array elements', () => {
+ it('should evaluate arr[i] correctly using a variable index', () => {
+ const arrIdxVar = makeArrayVariableWithIndex([createVariableEl(idxVar)]);
+ const exprElements = [createVariableEl(arrIdxVar)];
+ const expr = new Expression(undefined, exprElements);
+ expect(expr.calculateValue(exprElements, 'integer', defaultContext)).toBe(1); // arr[1] == 1
+ });
+
+ it('should evaluate arr[2] correctly using a literal index', () => {
+ const arrIdxVar = makeArrayVariableWithIndex([createLiteral(2)]);
+ const exprElements = [createVariableEl(arrIdxVar)];
+ const expr = new Expression(undefined, exprElements);
+ expect(expr.calculateValue(exprElements, 'integer', defaultContext)).toBe(2);
+ });
+
+ it('should throw on out-of-bounds access', () => {
+ const arrIdxVar = makeArrayVariableWithIndex([createLiteral(3)]); // size is 3 ⇒ max idx 2
+ const expr = new Expression(undefined, [createVariableEl(arrIdxVar)]);
+ expect(() => expr.calculateValue(expr.rightSide, 'integer', defaultContext))
+ .toThrow('out of bounds');
+ });
+
+ it('should throw when index is not an integer', () => {
+ const arrIdxVar = makeArrayVariableWithIndex([createLiteral("'a'")]);
+ const expr = new Expression(undefined, [createVariableEl(arrIdxVar)]);
+ expect(() => expr.calculateValue(expr.rightSide, 'integer', defaultContext))
+ .toThrow('must be an integer');
+ });
+ });
+
+ describe('Writing array elements', () => {
+ it('should assign to arr[0] = 10', () => {
+ const arrIdxVar = makeArrayVariableWithIndex([createLiteral(0)]);
+ const assignExpr = new Expression(arrIdxVar, [createLiteral(10)]);
+ const updated = assignExpr.assignValue([valuedArr.clone()]);
+ expect(updated.value).toEqual([10, 1, 2]);
+ });
+
+ it('should assign to arr[i] using variable index', () => {
+ const arrIdxVar = makeArrayVariableWithIndex([createVariableEl(idxVar)]);
+ const assignExpr = new Expression(arrIdxVar, [createLiteral(42)]);
+ const updated = assignExpr.assignValue(defaultContext);
+ const expected = [0, 42, 2]; // i == 1
+ expect(updated.value).toEqual(expected);
+ });
+
+ it('should throw on type mismatch when assigning string into integer array', () => {
+ const arrIdxVar = makeArrayVariableWithIndex([createLiteral(1)]);
+ const assignExpr = new Expression(arrIdxVar, [createLiteral("'hello'")]);
+ expect(() => assignExpr.assignValue(defaultContext))
+ .toThrow('Type mismatch');
+ });
+
+ it('should assign using arithmetic index expression arr[i + 1] = 7', () => {
+ const complexIdx = [createVariableEl(idxVar), createOperator('+'), createLiteral(1)];
+ const arrIdxVar = makeArrayVariableWithIndex(complexIdx);
+ const assignExpr = new Expression(arrIdxVar, [createLiteral(7)]);
+ const ctx = [
+ new ValuedVariable('var-arr', 'array', 'arr', 'node-1', [0,1,2], 'integer', 3),
+ valuedIdx.clone(),
+ ];
+ const updated = assignExpr.assignValue(ctx);
+ expect(updated.value).toEqual([0,1,7]); // i==1 so index 2
+ });
+
+ it('should assign using function & arithmetic in index arr[int((i*2)-1)] = 9', () => {
+ const innerExpr = new Expression(undefined, [
+ createVariableEl(idxVar), createOperator('*'), createLiteral(2), createOperator('-'), createLiteral(1)
+ ]);
+ const funcEl = createFunctionEl('integer', innerExpr);
+ const arrIdxVar = makeArrayVariableWithIndex([funcEl]);
+ const assignExpr = new Expression(arrIdxVar, [createLiteral(9)]);
+
+ const ctx = [
+ new ValuedVariable('var-arr', 'array', 'arr', 'node-1', [0,1,2], 'integer', 3),
+ valuedIdx.clone(),
+ ];
+ const updated = assignExpr.assignValue(ctx);
+ // i=1 -> (1*2)-1 =1 => int(1)=1
+ expect(updated.value).toEqual([0,9,2]);
+ });
+
+ it('should throw when assigning to whole array variable without index', () => {
+ const assignExpr = new Expression(arrVar.clone(), [createLiteral(5)]);
+ expect(() => assignExpr.assignValue(defaultContext))
+ .toThrow('Cannot assign directly to array variable');
+ });
+ });
+});
\ No newline at end of file
diff --git a/flowming/src/utils/codeGeneration.ts b/flowming/src/utils/codeGeneration.ts
index a80f442..4e0a600 100644
--- a/flowming/src/utils/codeGeneration.ts
+++ b/flowming/src/utils/codeGeneration.ts
@@ -1,10 +1,10 @@
import { FlowNode } from '../components/Flow/FlowTypes';
import { Edge } from '@xyflow/react';
import {
- Program, Statement, Expression as PyExpression, AssignmentStatement, IfStatement, WhileStatement, BlockStatement, Identifier, Literal, BinaryExpression, PrintStatement, UnsupportedNode, CallExpression, ASTNode, BreakStatement
+ Program, Statement, Expression as PyExpression, AssignmentStatement, IfStatement, WhileStatement, BlockStatement, Identifier, Literal, BinaryExpression, PrintStatement, UnsupportedNode, CallExpression, ASTNode, BreakStatement, SubscriptExpression
} from '../models/pythonAST';
-import { Expression as DiagramExpression, ExpressionElement, Variable, IOperator, IExpression as DiagramIExpression } from '../models';
-import { buildAST, ExpressionASTNode as DiagramASTNode, UnaryOpNode, BinaryOpNode as DiagramBinaryOpNode, FunctionCallNode as DiagramFunctionCallNode, IdentifierNode as DiagramIdentifierNode, LiteralNode as DiagramLiteralNode } from '../models/ExpressionParser';
+import { Expression as DiagramExpression, ExpressionElement, Variable, IOperator, IExpression as DiagramIExpression, VariableType } from '../models';
+import { buildAST, ExpressionASTNode as DiagramASTNode, UnaryOpNode, BinaryOpNode as DiagramBinaryOpNode, FunctionCallNode as DiagramFunctionCallNode, IdentifierNode as DiagramIdentifierNode, LiteralNode as DiagramLiteralNode, MemberAccessNode } from '../models/ExpressionParser';
// TODO: check bugs images + extensive automated testing
@@ -38,6 +38,11 @@ const convertDiagramASTToPythonAST = (diagramNode: DiagramASTNode, nodeId: strin
case 'Identifier': {
const identifier = diagramNode as DiagramIdentifierNode;
+ if (identifier.variable.type === 'array' && identifier.variable.indexExpression && identifier.variable.indexExpression.length > 0) {
+ const indexExpr = convertDiagramExpressionToAST(identifier.variable.indexExpression as ExpressionElement[], nodeId, cfg);
+ const objId: Identifier = { type: 'Identifier', name: identifier.name, diagramNodeId: nodeId } as Identifier;
+ return { type: 'SubscriptExpression', object: objId, index: indexExpr, diagramNodeId: nodeId } as any;
+ }
return { type: 'Identifier', name: identifier.name, diagramNodeId: nodeId } as Identifier;
}
@@ -104,6 +109,14 @@ const convertDiagramASTToPythonAST = (diagramNode: DiagramASTNode, nodeId: strin
} as CallExpression;
}
+ case 'MemberAccess': {
+ const member = diagramNode as MemberAccessNode;
+ const obj = convertDiagramASTToPythonAST(member.object, nodeId, cfg);
+ const idx = convertDiagramASTToPythonAST(member.property, nodeId, cfg);
+ if (obj.type === 'UnsupportedNode' || idx.type === 'UnsupportedNode') return obj.type==='UnsupportedNode'?obj:idx;
+ return { type: 'SubscriptExpression', object: obj as Identifier, index: idx, diagramNodeId: nodeId } as any;
+ }
+
default:
return unsupportedNode(`Unknown diagram AST node type: ${(diagramNode as any).type}`);
}
@@ -128,6 +141,16 @@ const convertDiagramExpressionToAST = (elements: ExpressionElement[], nodeId: st
}
};
+// Helper to convert Variable (left side) into Python target (Identifier or SubscriptExpression)
+const variableToTarget = (v: Variable, nodeId: string, cfg: CFG): Identifier | SubscriptExpression => {
+ if (v.type === 'array' && v.indexExpression && v.indexExpression.length > 0) {
+ const idxAst = convertDiagramExpressionToAST(v.indexExpression as ExpressionElement[], nodeId, cfg);
+ const objId: Identifier = { type: 'Identifier', name: v.name, diagramNodeId: nodeId } as Identifier;
+ return { type: 'SubscriptExpression', object: objId, index: idxAst, diagramNodeId: nodeId } as any;
+ }
+ return { type: 'Identifier', name: v.name, diagramNodeId: nodeId } as Identifier;
+};
+
// Build a Control-Flow Graph (CFG) from nodes and edges
interface CFG {
nodesMap: Map;
@@ -252,7 +275,7 @@ const generateNodeStatements = (node: FlowNode, nodeId: string, cfg: CFG): State
if (node.data?.expression) {
const diag = DiagramExpression.fromObject(node.data.expression as DiagramIExpression);
if (diag.leftSide instanceof Variable) {
- const target: Identifier = { type: 'Identifier', name: diag.leftSide.name, diagramNodeId: nodeId, visualId };
+ const target = variableToTarget(diag.leftSide, nodeId, cfg);
const val = convertDiagramExpressionToAST(diag.rightSide, nodeId, cfg);
stmts.push(
val.type !== 'UnsupportedNode'
@@ -266,7 +289,7 @@ const generateNodeStatements = (node: FlowNode, nodeId: string, cfg: CFG): State
case 'Input': {
if (node.data?.variable) {
const v = node.data.variable as Variable;
- const target: Identifier = { type: 'Identifier', name: v.name, diagramNodeId: nodeId, visualId };
+ const target = variableToTarget(v, nodeId, cfg);
const promptText = `Enter the value of '${v.name}' by keyboard`;
const promptLiteral: Literal = { type: 'Literal', value: promptText, raw: JSON.stringify(promptText), diagramNodeId: nodeId };
@@ -280,8 +303,13 @@ const generateNodeStatements = (node: FlowNode, nodeId: string, cfg: CFG): State
} as CallExpression;
let value: PyExpression = baseCall;
- // TODO: new data types here
- if (v.type === 'integer') {
+ // Handle type conversion.
+ let vType: VariableType = v.type;
+ if (v.type === 'array') {
+ vType = v.arraySubtype as VariableType;
+ }
+
+ if (vType === 'integer') {
// int(input())
value = {
type: 'CallExpression',
@@ -290,7 +318,7 @@ const generateNodeStatements = (node: FlowNode, nodeId: string, cfg: CFG): State
diagramNodeId: nodeId,
visualId
} as CallExpression;
- } else if (v.type === 'float') {
+ } else if (vType === 'float') {
// float(input())
value = {
type: 'CallExpression',
@@ -299,7 +327,7 @@ const generateNodeStatements = (node: FlowNode, nodeId: string, cfg: CFG): State
diagramNodeId: nodeId,
visualId
} as CallExpression;
- } else if (v.type === 'boolean') {
+ } else if (vType === 'boolean') {
// bool(input())
value = {
type: 'CallExpression',
@@ -801,6 +829,10 @@ const generateCodeFromASTNode = (astNode: ASTNode, indentLevel = 0): string => {
const callArgs = callExpr.arguments.map(arg => generateCodeFromASTNode(arg)).join(', ');
return `${generateCodeFromASTNode(callExpr.callee)}(${callArgs})`;
+ case 'SubscriptExpression':
+ const sub = astNode as SubscriptExpression;
+ return `${generateCodeFromASTNode(sub.object)}[${generateCodeFromASTNode(sub.index)}]`;
+
case 'UnsupportedNode':
const unsupported = astNode as UnsupportedNode;
const blockId = unsupported.visualId || unsupported.diagramNodeId || 'N/A';
diff --git a/flowming/src/utils/expressionParsing.ts b/flowming/src/utils/expressionParsing.ts
deleted file mode 100644
index b60de93..0000000
--- a/flowming/src/utils/expressionParsing.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-// Utility helpers for parsing expression-related strings
-// Intended to be used by the panel editors to avoid code duplication
-
-import { Expression, ExpressionElement, Variable } from "@/models";
-import { operators } from "@/models/Expression";
-
-// Parses array access literal strings (e.g., "arr[i]" or "arr[0:5]") and returns parts
-export function parseArrayAccess(value: string): {
- arrayName: string;
- indexExpression?: string;
- rangeStart?: string;
- rangeEnd?: string;
- isRange: boolean;
-} | null {
- const match = value.match(/^(.+?)\[(.+)\]$/);
- if (!match) return null;
- const arrayName = match[1];
- const indexPart = match[2];
-
- if (indexPart.includes(":")) {
- const [start, end] = indexPart.split(":");
- if (start !== undefined && end !== undefined) {
- return {
- arrayName,
- rangeStart: start.trim(),
- rangeEnd: end.trim(),
- isRange: true,
- };
- }
- }
- return {
- arrayName,
- indexExpression: indexPart.trim(),
- isRange: false,
- };
-}
-
-// Converts a raw string expression back into an Expression instance
-// Requires the current list of variables to resolve identifiers
-export function parseExpressionString(exprStr: string, allVariables: Variable[]): Expression {
- const elements: ExpressionElement[] = [];
-
- const splitTokens = (s: string): string[] => {
- const toks: string[] = [];
- let current = "";
- let depth = 0;
- for (let i = 0; i < s.length; i++) {
- const ch = s[i];
- if (ch === " " && depth === 0) {
- if (current !== "") {
- toks.push(current);
- current = "";
- }
- continue;
- }
- if (ch === "(") depth++;
- if (ch === ")") depth--;
- current += ch;
- }
- if (current.trim() !== "") toks.push(current);
- return toks;
- };
-
- const tokens = splitTokens(exprStr.trim());
- const functionRegex = /^(integer|string|float|boolean)\((.*)\)$/;
-
- tokens.forEach((tok) => {
- // Function call
- const funcMatch = tok.match(functionRegex);
- if (funcMatch) {
- const funcName = funcMatch[1];
- const inner = funcMatch[2];
- const nestedExpr = parseExpressionString(inner, allVariables); // recursion
- elements.push(
- new ExpressionElement(crypto.randomUUID(), "function", funcName, nestedExpr)
- );
- return;
- }
-
- // Operator token
- if (operators.includes(tok as any)) {
- elements.push(new ExpressionElement(crypto.randomUUID(), "operator", tok));
- return;
- }
-
- // Variable or literal
- const variable = allVariables.find((v) => v.name === tok);
- if (variable) {
- elements.push(
- new ExpressionElement(
- crypto.randomUUID(),
- "variable",
- variable.name,
- variable
- )
- );
- } else {
- elements.push(new ExpressionElement(crypto.randomUUID(), "literal", tok));
- }
- });
-
- return new Expression(undefined, elements);
-}
\ No newline at end of file