From e53993e8f67ae57c927ed2c9313ea4960e5e7fa4 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 2 Oct 2025 11:27:44 -0400 Subject: [PATCH 01/22] Start implementing if statements --- src/strands/ir_builders.js | 2 +- src/strands/strands_api.js | 24 +-- src/strands/strands_conditionals.js | 116 +++++++++-- src/strands/strands_glslBackend.js | 17 ++ src/strands/strands_node.js | 20 ++ src/strands/strands_transpiler.js | 283 +++++++++++++++++++++++++- test/unit/webgl/p5.Shader.js | 295 ++++++++++++++++++++++++++++ 7 files changed, 715 insertions(+), 42 deletions(-) create mode 100644 src/strands/strands_node.js diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index dcecc7d0f9..72684c8cdf 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -2,7 +2,7 @@ import * as DAG from './ir_dag' import * as CFG from './ir_cfg' import * as FES from './strands_FES' import { NodeType, OpCode, BaseType, DataType, BasePriority, OpCodeToSymbol, typeEquals, } from './ir_types'; -import { createStrandsNode, StrandsNode } from './strands_api'; +import { createStrandsNode, StrandsNode } from './strands_node'; import { strandsBuiltinFunctions } from './strands_builtins'; ////////////////////////////////////////////// diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 0bb0ce9866..9390f6376c 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -15,25 +15,12 @@ import { StrandsConditional } from './strands_conditionals' import * as CFG from './ir_cfg' import * as FES from './strands_FES' import { getNodeDataFromID } from './ir_dag' +import { StrandsNode, createStrandsNode } from './strands_node' import noiseGLSL from '../webgl/shaders/functions/noiseGLSL.glsl'; ////////////////////////////////////////////// // User nodes ////////////////////////////////////////////// -export class StrandsNode { - constructor(id, dimension, strandsContext) { - this.id = id; - this.strandsContext = strandsContext; - this.dimension = dimension; - } -} - -export function createStrandsNode(id, dimension, strandsContext, onRebind) { - return new Proxy( - new StrandsNode(id, dimension, strandsContext), - build.swizzleTrap(id, dimension, strandsContext, onRebind) - ); -} export function initGlobalStrandsAPI(p5, fn, strandsContext) { // We augment the strands node with operations programatically @@ -65,13 +52,18 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return createStrandsNode(node.id, node.dimension, strandsContext); } - fn.strandsIf = function(conditionNode, ifBody) { + // Internal methods use p5 static methods; user-facing methods use fn. + // Some methods need to be used by both. + + p5.strandsIf = function(conditionNode, ifBody) { return new StrandsConditional(strandsContext, conditionNode, ifBody); } + fn.strandsIf = p5.strandsIf; - fn.strandsLoop = function(a, b, loopBody) { + p5.strandsLoop = function(a, b, loopBody) { return null; } + fn.strandsLoop = p5.strandsLoop; p5.strandsNode = function(...args) { if (args.length === 1 && args[0] instanceof StrandsNode) { diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index 1ce888cc91..e9eff5b780 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -1,5 +1,7 @@ -import * as CFG from './ir_cfg' +import * as CFG from './ir_cfg'; +import * as DAG from './ir_dag'; import { BlockType } from './ir_types'; +import { StrandsNode, createStrandsNode } from './strands_node'; export class StrandsConditional { constructor(strandsContext, condition, branchCallback) { @@ -11,38 +13,47 @@ export class StrandsConditional { }]; this.ctx = strandsContext; } - + ElseIf(condition, branchCallback) { - this.branches.push({ + this.branches.push({ condition, branchCallback, - blockType: BlockType.ELIF_BODY + blockType: BlockType.ELIF_BODY }); return this; } - + Else(branchCallback = () => ({})) { - this.branches.push({ - condition: null, - branchCallback, - blockType: BlockType.ELSE_BODY + this.branches.push({ + condition: null, + branchCallback, + blockType: BlockType.ELSE_BODY }); - return buildConditional(this.ctx, this); + const phiNodes = buildConditional(this.ctx, this); + + // Convert phi nodes to StrandsNodes for the user + const assignments = {}; + for (const [varName, phiNode] of Object.entries(phiNodes)) { + assignments[varName] = createStrandsNode(phiNode.id, phiNode.dimension, this.ctx); + } + + return assignments; } } function buildConditional(strandsContext, conditional) { const cfg = strandsContext.cfg; - const branches = conditional.branches; + const branches = conditional.branches; const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); const results = []; + const branchBlocks = []; let previousBlock = cfg.currentBlock; for (let i = 0; i < branches.length; i++) { const { condition, branchCallback, blockType } = branches[i]; - + if (condition !== null) { const conditionBlock = CFG.createBasicBlock(cfg, BlockType.IF_COND); CFG.addEdge(cfg, previousBlock, conditionBlock); @@ -51,10 +62,11 @@ function buildConditional(strandsContext, conditional) { previousBlock = conditionBlock; CFG.popBlock(cfg); } - + const branchBlock = CFG.createBasicBlock(cfg, blockType); CFG.addEdge(cfg, previousBlock, branchBlock); - + branchBlocks.push(branchBlock); + CFG.pushBlock(cfg, branchBlock); const branchResults = branchCallback(); results.push(branchResults); @@ -65,7 +77,77 @@ function buildConditional(strandsContext, conditional) { CFG.addEdge(cfg, cfg.currentBlock, mergeBlock); CFG.popBlock(cfg); } + CFG.pushBlock(cfg, mergeBlock); - - return results; -} \ No newline at end of file + + // Create phi nodes for variables that were modified in any branch + const allVariableNames = new Set(); + results.forEach(branchResult => { + if (branchResult && typeof branchResult === 'object') { + Object.keys(branchResult).forEach(varName => allVariableNames.add(varName)); + } + }); + + const mergedAssignments = {}; + for (const varName of allVariableNames) { + // Collect the node IDs for this variable from each branch + const phiInputs = []; + for (let i = 0; i < results.length; i++) { + const branchResult = results[i]; + const branchBlock = branchBlocks[i]; + + if (branchResult && branchResult[varName]) { + phiInputs.push({ + nodeId: branchResult[varName].id, + blockId: branchBlock + }); + } else { + // If this branch didn't modify the variable, we need the original value + // For now, we'll handle this case later when we have variable tracking + // This is a placeholder that will need to be improved + phiInputs.push({ + nodeId: null, // Will need original variable ID + blockId: branchBlock + }); + } + } + + // Create a phi node for this variable + const phiNode = createPhiNode(strandsContext, phiInputs, varName); + mergedAssignments[varName] = phiNode; + } + + return mergedAssignments; +} + +function createPhiNode(strandsContext, phiInputs, varName) { + // For now, create a simple phi node + // We'll need to determine the proper dimension and baseType from the inputs + const validInputs = phiInputs.filter(input => input.nodeId !== null); + if (validInputs.length === 0) { + throw new Error(`No valid inputs for phi node for variable ${varName}`); + } + + // Get dimension and baseType from first valid input + const firstInput = DAG.getNodeDataFromID(strandsContext.dag, validInputs[0].nodeId); + const dimension = firstInput.dimension; + const baseType = firstInput.baseType; + + const nodeData = { + nodeType: 'phi', + dimension, + baseType, + dependsOn: phiInputs.map(input => input.nodeId).filter(id => id !== null), + phiBlocks: phiInputs.map(input => input.blockId), + phiInputs // Store the full phi input information + }; + + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); + + return { + id, + dimension, + baseType + }; +} diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 70552bad1b..7aec1242ef 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -200,6 +200,23 @@ export const glslBackend = { const sym = OpCodeToSymbol[node.opCode]; return `${sym}${val}`; } + case NodeType.PHI: + // Phi nodes represent conditional merging of values + // For now, just use the first valid input as a simple implementation + // TODO: Implement proper phi node control flow + const validInputs = node.dependsOn.filter(id => id !== null); + if (validInputs.length > 0) { + return this.generateExpression(generationContext, dag, validInputs[0]); + } else { + // Fallback: create a default value + const typeName = this.getTypeName(node.baseType, node.dimension); + if (node.dimension === 1) { + return node.baseType === BaseType.FLOAT ? '0.0' : '0'; + } else { + return `${typeName}(0.0)`; + } + } + default: FES.internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`) } diff --git a/src/strands/strands_node.js b/src/strands/strands_node.js new file mode 100644 index 0000000000..ace776ff72 --- /dev/null +++ b/src/strands/strands_node.js @@ -0,0 +1,20 @@ +import { swizzleTrap } from './ir_builders'; + +export class StrandsNode { + constructor(id, dimension, strandsContext) { + this.id = id; + this.strandsContext = strandsContext; + this.dimension = dimension; + } + + copy() { + return createStrandsNode(this.id, this.dimension, this.strandsContext); + } +} + +export function createStrandsNode(id, dimension, strandsContext, onRebind) { + return new Proxy( + new StrandsNode(id, dimension, strandsContext), + swizzleTrap(id, dimension, strandsContext, onRebind) + ); +} \ No newline at end of file diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index dd39a21c87..c86cf7ab9c 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -1,8 +1,10 @@ import { parse } from 'acorn'; -import { ancestor } from 'acorn-walk'; +import { ancestor, recursive } from 'acorn-walk'; import escodegen from 'escodegen'; import { UnarySymbolToName } from './ir_types'; +let blockVarCounter = 0; + function replaceBinaryOperator(codeSource) { switch (codeSource) { case '+': return 'add'; @@ -38,7 +40,7 @@ function nodeIsUniform(ancestor) { const ASTCallbacks = { UnaryExpression(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } - + const unaryFnName = UnarySymbolToName[node.operator]; const standardReplacement = (node) => { @@ -49,7 +51,7 @@ const ASTCallbacks = { } node.arguments = [node.argument] } - + if (node.type === 'MemberExpression') { const property = node.argument.property.name; const swizzleSets = [ @@ -57,11 +59,11 @@ const ASTCallbacks = { ['r', 'g', 'b', 'a'], ['s', 't', 'p', 'q'] ]; - + let isSwizzle = swizzleSets.some(set => [...property].every(char => set.includes(char)) ) && node.argument.type === 'MemberExpression'; - + if (isSwizzle) { node.type = 'MemberExpression'; node.object = { @@ -206,14 +208,278 @@ const ASTCallbacks = { }; node.arguments = [node.right]; }, + IfStatement(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + + // Transform if statement into strandsIf() call + // The condition is evaluated directly, not wrapped in a function + const condition = node.test; + + // Create the then function + const thenFunction = { + type: 'ArrowFunctionExpression', + params: [], + body: node.consequent.type === 'BlockStatement' ? node.consequent : { + type: 'BlockStatement', + body: [node.consequent] + } + }; + + // Start building the call chain: __p5.strandsIf(condition, then) + let callExpression = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '__p5.strandsIf' + }, + arguments: [condition, thenFunction] + }; + + // Always chain .Else() even if there's no explicit else clause + // This ensures the conditional completes and returns phi nodes + let elseFunction; + if (node.alternate) { + elseFunction = { + type: 'ArrowFunctionExpression', + params: [], + body: node.alternate.type === 'BlockStatement' ? node.alternate : { + type: 'BlockStatement', + body: [node.alternate] + } + }; + } else { + // Create an empty else function + elseFunction = { + type: 'ArrowFunctionExpression', + params: [], + body: { + type: 'BlockStatement', + body: [] + } + }; + } + + callExpression = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: callExpression, + property: { + type: 'Identifier', + name: 'Else' + } + }, + arguments: [elseFunction] + }; + + // Analyze which outer scope variables are assigned in any branch + const assignedVars = new Set(); + + const analyzeBlock = (body) => { + if (body.type !== 'BlockStatement') return; + + // First pass: collect variable declarations within this block + const localVars = new Set(); + for (const stmt of body.body) { + if (stmt.type === 'VariableDeclaration') { + for (const decl of stmt.declarations) { + if (decl.id.type === 'Identifier') { + localVars.add(decl.id.name); + } + } + } + } + + // Second pass: find assignments to non-local variables + for (const stmt of body.body) { + if (stmt.type === 'ExpressionStatement' && + stmt.expression.type === 'AssignmentExpression') { + const left = stmt.expression.left; + + if (left.type === 'Identifier') { + // Direct variable assignment: x = value + if (!localVars.has(left.name)) { + assignedVars.add(left.name); + } + } else if (left.type === 'MemberExpression' && + left.object.type === 'Identifier') { + // Property assignment: obj.prop = value + if (!localVars.has(left.object.name)) { + assignedVars.add(left.object.name); + } + } + } + } + }; + + // Analyze all branches for assignments to outer scope variables + analyzeBlock(thenFunction.body); + analyzeBlock(elseFunction.body); + + if (assignedVars.size > 0) { + // Add copying, reference replacement, and return statements to branch functions + const addCopyingAndReturn = (functionBody, varsToReturn) => { + if (functionBody.type === 'BlockStatement') { + // Create temporary variables and copy statements + const tempVarMap = new Map(); // original name -> temp name + const copyStatements = []; + + for (const varName of varsToReturn) { + const tempName = `__copy_${varName}_${blockVarCounter++}`; + tempVarMap.set(varName, tempName); + + // let tempName = originalVar.copy() + copyStatements.push({ + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { type: 'Identifier', name: tempName }, + init: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { type: 'Identifier', name: varName }, + property: { type: 'Identifier', name: 'copy' }, + computed: false + }, + arguments: [] + } + }], + kind: 'let' + }); + } + + // Replace all references to original variables with temp variables + const replaceReferences = (node) => { + if (!node || typeof node !== 'object') return; + + if (node.type === 'Identifier' && tempVarMap.has(node.name)) { + node.name = tempVarMap.get(node.name); + } else if (node.type === 'MemberExpression' && + node.object.type === 'Identifier' && + tempVarMap.has(node.object.name)) { + node.object.name = tempVarMap.get(node.object.name); + } + + // Recursively process all properties + for (const key in node) { + if (node.hasOwnProperty(key) && key !== 'parent') { + if (Array.isArray(node[key])) { + node[key].forEach(replaceReferences); + } else if (typeof node[key] === 'object') { + replaceReferences(node[key]); + } + } + } + }; + + // Apply reference replacement to all statements + functionBody.body.forEach(replaceReferences); + + // Insert copy statements at the beginning + functionBody.body.unshift(...copyStatements); + + // Add return statement with temp variable names + const returnObj = { + type: 'ObjectExpression', + properties: Array.from(varsToReturn).map(varName => ({ + type: 'Property', + key: { type: 'Identifier', name: varName }, + value: { type: 'Identifier', name: tempVarMap.get(varName) }, + kind: 'init', + computed: false, + shorthand: false + })) + }; + + functionBody.body.push({ + type: 'ReturnStatement', + argument: returnObj + }); + } + }; + + addCopyingAndReturn(thenFunction.body, assignedVars); + addCopyingAndReturn(elseFunction.body, assignedVars); + + // Create a block variable to capture the return value + const blockVar = `__block_${blockVarCounter++}`; + + // Replace with a block statement containing: + // 1. The conditional call assigned to block variable + // 2. Assignments from block variable back to original variables + const statements = []; + + // 1. const blockVar = strandsIf().Else() + statements.push({ + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { type: 'Identifier', name: blockVar }, + init: callExpression + }], + kind: 'const' + }); + + // 2. Assignments for each modified variable + for (const varName of assignedVars) { + statements.push({ + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { type: 'Identifier', name: varName }, + right: { + type: 'MemberExpression', + object: { type: 'Identifier', name: blockVar }, + property: { type: 'Identifier', name: varName }, + computed: false + } + } + }); + } + + // Replace the if statement with a block statement + node.type = 'BlockStatement'; + node.body = statements; + } else { + // No assignments, just replace with the call expression + node.type = 'ExpressionStatement'; + node.expression = callExpression; + } + + delete node.test; + delete node.consequent; + delete node.alternate; + }, } - + export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { const ast = parse(sourceString, { ecmaVersion: 2021, locations: srcLocations }); - ancestor(ast, ASTCallbacks, undefined, { varyings: {} }); + + // First pass: transform everything except if statements using normal ancestor traversal + const nonIfCallbacks = { ...ASTCallbacks }; + delete nonIfCallbacks.IfStatement; + ancestor(ast, nonIfCallbacks, undefined, { varyings: {} }); + + // Second pass: transform if statements in post-order using recursive traversal + const postOrderIfTransform = { + IfStatement(node, state, c) { + // First recursively process children + if (node.test) c(node.test, state); + if (node.consequent) c(node.consequent, state); + if (node.alternate) c(node.alternate, state); + + // Then apply the transformation to this node + ASTCallbacks.IfStatement(node, state, []); + } + }; + + recursive(ast, { varyings: {} }, postOrderIfTransform); + const transpiledSource = escodegen.generate(ast); const scopeKeys = Object.keys(scope); const internalStrandsCallback = new Function( @@ -229,6 +495,7 @@ const ASTCallbacks = { transpiledSource.lastIndexOf('}') ).replaceAll(';', '') ); + console.log(internalStrandsCallback.toString()) return () => internalStrandsCallback(p5, ...scopeKeys.map(key => scope[key])); } - \ No newline at end of file + diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index d4e74efa36..323fbdea26 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -438,5 +438,300 @@ suite('p5.Shader', function() { myp5.plane(myp5.width, myp5.height); }).not.toThrowError(); }); + + suite('if statement conditionals', () => { + it('should handle simple if statement with true condition', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 1.0); // true condition + + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.5); // initial gray + + if (condition > 0.5) { + color = myp5.float(1.0); // set to white in if branch + } + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Check that the center pixel is white (condition was true) + const pixelColor = myp5.get(25, 25); + expect(pixelColor[0]).toBeCloseTo(255, 0); // Red channel should be 255 (white) + expect(pixelColor[1]).toBeCloseTo(255, 0); // Green channel should be 255 + expect(pixelColor[2]).toBeCloseTo(255, 0); // Blue channel should be 255 + }); + + it('should handle simple if statement with false condition', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 0.0); // false condition + + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.5); // initial gray + + if (condition > 0.5) { + color = myp5.float(1.0); // set to white in if branch + } + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Check that the center pixel is gray (condition was false, original value kept) + const pixelColor = myp5.get(25, 25); + expect(pixelColor[0]).toBeCloseTo(127, 5); // Red channel should be ~127 (gray) + expect(pixelColor[1]).toBeCloseTo(127, 5); // Green channel should be ~127 + expect(pixelColor[2]).toBeCloseTo(127, 5); // Blue channel should be ~127 + }); + + it('should handle if-else statement', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 0.0); // false condition + + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.5); // initial gray + + if (condition > 0.5) { + color = myp5.float(1.0); // white for true + } else { + color = myp5.float(0.0); // black for false + } + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Check that the center pixel is black (else branch executed) + const pixelColor = myp5.get(25, 25); + expect(pixelColor[0]).toBeCloseTo(0, 5); // Red channel should be ~0 (black) + expect(pixelColor[1]).toBeCloseTo(0, 5); // Green channel should be ~0 + expect(pixelColor[2]).toBeCloseTo(0, 5); // Blue channel should be ~0 + }); + + it('should handle multiple variable assignments in if statement', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 1.0); // true condition + + myp5.getPixelInputs(inputs => { + let red = myp5.float(0.0); + let green = myp5.float(0.0); + let blue = myp5.float(0.0); + + if (condition > 0.5) { + red = myp5.float(1.0); + green = myp5.float(0.5); + blue = myp5.float(0.0); + } + + inputs.color = [red, green, blue, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Check that the center pixel has the expected color (red=1.0, green=0.5, blue=0.0) + const pixelColor = myp5.get(25, 25); + expect(pixelColor[0]).toBeCloseTo(255, 0); // Red channel should be 255 + expect(pixelColor[1]).toBeCloseTo(127, 5); // Green channel should be ~127 + expect(pixelColor[2]).toBeCloseTo(0, 5); // Blue channel should be ~0 + }); + + it('should handle modifications after if statement', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 1.0); // true condition + + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); // start with black + + if (condition > 0.5) { + color = myp5.float(1.0); // set to white in if branch + } else { + color = myp5.float(0.5); // set to gray in else branch + } + + // Modify the color after the if statement + color = color * 0.5; // Should result in 0.5 * 1.0 = 0.5 (gray) + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Check that the center pixel is gray (white * 0.5 = gray) + const pixelColor = myp5.get(25, 25); + expect(pixelColor[0]).toBeCloseTo(127, 5); // Red channel should be ~127 (gray) + expect(pixelColor[1]).toBeCloseTo(127, 5); // Green channel should be ~127 + expect(pixelColor[2]).toBeCloseTo(127, 5); // Blue channel should be ~127 + }); + + it('should handle modifications after if statement in both branches', () => { + myp5.createCanvas(100, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const uv = inputs.uv; + const condition = uv.x > 0.5; // left half false, right half true + let color = myp5.float(0.0); + + if (condition) { + color = myp5.float(1.0); // white on right side + } else { + color = myp5.float(0.8); // light gray on left side + } + + // Multiply by 0.5 after the if statement + color = color * 0.5; + // Right side: 1.0 * 0.5 = 0.5 (medium gray) + // Left side: 0.8 * 0.5 = 0.4 (darker gray) + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Check left side (false condition) + const leftPixel = myp5.get(25, 25); + expect(leftPixel[0]).toBeCloseTo(102, 10); // 0.4 * 255 ≈ 102 + + // Check right side (true condition) + const rightPixel = myp5.get(75, 25); + expect(rightPixel[0]).toBeCloseTo(127, 10); // 0.5 * 255 ≈ 127 + }); + + it('should handle if-else-if chains', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + const value = myp5.uniformFloat(() => 0.5); // middle value + + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); + + if (value > 0.8) { + color = myp5.float(1.0); // white for high values + } else if (value > 0.3) { + color = myp5.float(0.5); // gray for medium values + } else { + color = myp5.float(0.0); // black for low values + } + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Check that the center pixel is gray (medium condition was true) + const pixelColor = myp5.get(25, 25); + expect(pixelColor[0]).toBeCloseTo(127, 5); // Red channel should be ~127 (gray) + expect(pixelColor[1]).toBeCloseTo(127, 5); // Green channel should be ~127 + expect(pixelColor[2]).toBeCloseTo(127, 5); // Blue channel should be ~127 + }); + + it('should handle nested if statements', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + const outerCondition = myp5.uniformFloat(() => 1.0); // true + const innerCondition = myp5.uniformFloat(() => 1.0); // true + + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); + + if (outerCondition > 0.5) { + if (innerCondition > 0.5) { + color = myp5.float(1.0); // white for both conditions true + } else { + color = myp5.float(0.5); // gray for outer true, inner false + } + } else { + color = myp5.float(0.0); // black for outer false + } + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Check that the center pixel is white (both conditions were true) + const pixelColor = myp5.get(25, 25); + expect(pixelColor[0]).toBeCloseTo(255, 0); // Red channel should be 255 (white) + expect(pixelColor[1]).toBeCloseTo(255, 0); // Green channel should be 255 + expect(pixelColor[2]).toBeCloseTo(255, 0); // Blue channel should be 255 + }); + + // Keep one direct API test for completeness + it('should handle direct StrandsIf API usage', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + const conditionValue = myp5.uniformFloat(() => 1.0); // true condition + + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.5); // initial gray + + const assignments = myp5.strandsIf( + conditionValue, + () => { + let tmp = color.copy(); + tmp = myp5.float(1.0); // set to white in if branch + return { color: tmp }; + } + ).Else(() => { + return { color: color }; // keep original in else branch + }); + + color = assignments.color; + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Check that the center pixel is white (condition was true) + const pixelColor = myp5.get(25, 25); + expect(pixelColor[0]).toBeCloseTo(255, 0); // Red channel should be 255 (white) + expect(pixelColor[1]).toBeCloseTo(255, 0); // Green channel should be 255 + expect(pixelColor[2]).toBeCloseTo(255, 0); // Blue channel should be 255 + }); + }); }); }); From 6a9d404606513201e08fbeaef576832d1c2cc1f5 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 2 Oct 2025 15:43:25 -0400 Subject: [PATCH 02/22] Semi functional if statements --- src/strands/ir_builders.js | 13 +- src/strands/ir_cfg.js | 5 + src/strands/ir_types.js | 5 +- src/strands/strands_conditionals.js | 98 +++++++------ src/strands/strands_glslBackend.js | 209 +++++++++++++++++++++++++--- test/unit/webgl/p5.Shader.js | 165 +++++++++++----------- 6 files changed, 354 insertions(+), 141 deletions(-) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 72684c8cdf..9a1f4dda1a 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -199,6 +199,7 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { const mappedDependencies = []; let { dimension, baseType } = typeInfo; + const dag = strandsContext.dag; let calculatedDimensions = 0; let originalNodeID = null; @@ -206,6 +207,7 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { if (dep instanceof StrandsNode) { const node = DAG.getNodeDataFromID(dag, dep.id); originalNodeID = dep.id; + console.log('Setting baseType from StrandsNode:', node.baseType); baseType = node.baseType; if (node.opCode === OpCode.Nary.CONSTRUCTOR) { @@ -220,6 +222,7 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { continue; } else if (typeof dep === 'number') { + console.log('Creating literal node for number:', dep, 'with baseType:', baseType); const { id, dimension } = scalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep); mappedDependencies.push(id); calculatedDimensions += dimension; @@ -458,7 +461,7 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { return Reflect.get(...arguments); } else { for (const set of swizzleSets) { - if ([...property].every(char => set.includes(char))) { + if ([...property.toString()].every(char => set.includes(char))) { const swizzle = [...property].map(char => { const index = set.indexOf(char); return swizzleSets[0][index]; @@ -481,7 +484,7 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { const dim = target.dimension; - // lanes are the underlying values of the target vector + // lanes are the underlying values of the target vector // e.g. lane 0 holds the value aliased by 'x', 'r', and 's' // the lanes array is in the 'correct' order const lanes = new Array(dim); @@ -521,7 +524,7 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { } // The canonical index refers to the actual value's position in the vector lanes - // i.e. we are finding (3,2,1) from .zyx + // i.e. we are finding (3,2,1) from .zyx // We set the correct value in the lanes array for (let j = 0; j < chars.length; j++) { const canonicalIndex = swizzleSet.indexOf(chars[j]); @@ -538,9 +541,9 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { target.id = newID; - // If we swizzle assign on a struct component i.e. + // If we swizzle assign on a struct component i.e. // inputs.position.rg = [1, 2] - // The onRebind callback will update the structs components so that it refers to the new values, + // The onRebind callback will update the structs components so that it refers to the new values, // and make a new ID for the struct with these new values if (typeof onRebind === 'function') { onRebind(newID); diff --git a/src/strands/ir_cfg.js b/src/strands/ir_cfg.js index 7bdcf09382..16988ca1e0 100644 --- a/src/strands/ir_cfg.js +++ b/src/strands/ir_cfg.js @@ -31,6 +31,11 @@ export function popBlock(graph) { graph.currentBlock = graph.blockStack[len-1]; } +export function pushBlockForModification(graph, blockID) { + graph.blockStack.push(blockID); + graph.currentBlock = blockID; +} + export function createBasicBlock(graph, blockType) { const id = graph.nextID++; graph.blockTypes[id] = blockType; diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 67829d6b03..93caf1824f 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -9,6 +9,7 @@ export const NodeType = { STRUCT: 'struct', PHI: 'phi', STATEMENT: 'statement', + ASSIGNMENT: 'assignment', }; export const NodeTypeToName = Object.fromEntries( @@ -22,7 +23,8 @@ export const NodeTypeRequiredFields = { [NodeType.CONSTANT]: ["value", "dimension", "baseType"], [NodeType.STRUCT]: [""], [NodeType.PHI]: ["dependsOn", "phiBlocks", "dimension", "baseType"], - [NodeType.STATEMENT]: ["opCode"] + [NodeType.STATEMENT]: ["opCode"], + [NodeType.ASSIGNMENT]: ["dependsOn"] }; export const StatementType = { @@ -196,6 +198,7 @@ for (const { symbol, opCode, name, arity } of OperatorTable) { export const BlockType = { GLOBAL: 'global', FUNCTION: 'function', + BRANCH: 'branch', IF_COND: 'if_cond', IF_BODY: 'if_body', ELIF_BODY: 'elif_body', diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index e9eff5b780..636f7af9ef 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -1,6 +1,6 @@ import * as CFG from './ir_cfg'; import * as DAG from './ir_dag'; -import { BlockType } from './ir_types'; +import { BlockType, NodeType } from './ir_types'; import { StrandsNode, createStrandsNode } from './strands_node'; export class StrandsConditional { @@ -30,13 +30,13 @@ export class StrandsConditional { blockType: BlockType.ELSE_BODY }); const phiNodes = buildConditional(this.ctx, this); - + // Convert phi nodes to StrandsNodes for the user const assignments = {}; for (const [varName, phiNode] of Object.entries(phiNodes)) { assignments[varName] = createStrandsNode(phiNode.id, phiNode.dimension, this.ctx); } - + return assignments; } } @@ -48,8 +48,15 @@ function buildConditional(strandsContext, conditional) { const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); const results = []; const branchBlocks = []; + const mergedAssignments = {}; + const phiBlockDependencies = {}; - let previousBlock = cfg.currentBlock; + // Create a BRANCH block to handle phi node declarations + const branchBlock = CFG.createBasicBlock(cfg, BlockType.BRANCH); + CFG.addEdge(cfg, cfg.currentBlock, branchBlock); + CFG.addEdge(cfg, branchBlock, mergeBlock); + + let previousBlock = branchBlock; for (let i = 0; i < branches.length; i++) { const { condition, branchCallback, blockType } = branches[i]; @@ -69,6 +76,13 @@ function buildConditional(strandsContext, conditional) { CFG.pushBlock(cfg, branchBlock); const branchResults = branchCallback(); + for (const key in branchResults) { + if (!phiBlockDependencies[key]) { + phiBlockDependencies[key] = [{ value: branchResults[key], blockId: branchBlock }]; + } else { + phiBlockDependencies[key].push({ value: branchResults[key], blockId: branchBlock }); + } + } results.push(branchResults); if (cfg.currentBlock !== branchBlock) { CFG.addEdge(cfg, cfg.currentBlock, mergeBlock); @@ -78,58 +92,62 @@ function buildConditional(strandsContext, conditional) { CFG.popBlock(cfg); } - CFG.pushBlock(cfg, mergeBlock); + // Push the branch block for modification to attach phi nodes there + CFG.pushBlockForModification(cfg, branchBlock); - // Create phi nodes for variables that were modified in any branch - const allVariableNames = new Set(); - results.forEach(branchResult => { - if (branchResult && typeof branchResult === 'object') { - Object.keys(branchResult).forEach(varName => allVariableNames.add(varName)); - } - }); + for (const key in phiBlockDependencies) { + mergedAssignments[key] = createPhiNode(strandsContext, phiBlockDependencies[key], key); + } - const mergedAssignments = {}; - for (const varName of allVariableNames) { - // Collect the node IDs for this variable from each branch - const phiInputs = []; - for (let i = 0; i < results.length; i++) { - const branchResult = results[i]; - const branchBlock = branchBlocks[i]; - - if (branchResult && branchResult[varName]) { - phiInputs.push({ - nodeId: branchResult[varName].id, - blockId: branchBlock - }); - } else { - // If this branch didn't modify the variable, we need the original value - // For now, we'll handle this case later when we have variable tracking - // This is a placeholder that will need to be improved - phiInputs.push({ - nodeId: null, // Will need original variable ID - blockId: branchBlock - }); + CFG.popBlock(cfg); + + // Now add phi assignments to each branch block + for (let i = 0; i < results.length; i++) { + const branchResult = results[i]; + const branchBlockID = branchBlocks[i]; + + CFG.pushBlockForModification(cfg, branchBlockID); + + for (const key in branchResult) { + if (mergedAssignments[key]) { + // Create an assignment statement: phiNode = branchResult[key] + const phiNodeID = mergedAssignments[key].id; + const sourceNodeID = branchResult[key].id; + + // Create an assignment operation node + // Use dependsOn[0] for phiNodeID and dependsOn[1] for sourceNodeID + // This represents: dependsOn[0] = dependsOn[1] (phiNode = sourceNode) + const assignmentNode = { + nodeType: NodeType.ASSIGNMENT, + dependsOn: [phiNodeID, sourceNodeID], + phiBlocks: [] + }; + + const assignmentID = DAG.getOrCreateNode(strandsContext.dag, assignmentNode); + CFG.recordInBasicBlock(cfg, branchBlockID, assignmentID); } } - - // Create a phi node for this variable - const phiNode = createPhiNode(strandsContext, phiInputs, varName); - mergedAssignments[varName] = phiNode; + + CFG.popBlock(cfg); } + CFG.pushBlock(cfg, mergeBlock); + return mergedAssignments; } function createPhiNode(strandsContext, phiInputs, varName) { + console.log('createPhiNode called with varName:', varName, 'phiInputs:', phiInputs); + // For now, create a simple phi node // We'll need to determine the proper dimension and baseType from the inputs - const validInputs = phiInputs.filter(input => input.nodeId !== null); + const validInputs = phiInputs.filter(input => input.value.id !== null); if (validInputs.length === 0) { throw new Error(`No valid inputs for phi node for variable ${varName}`); } // Get dimension and baseType from first valid input - const firstInput = DAG.getNodeDataFromID(strandsContext.dag, validInputs[0].nodeId); + const firstInput = DAG.getNodeDataFromID(strandsContext.dag, validInputs[0].value.id); const dimension = firstInput.dimension; const baseType = firstInput.baseType; @@ -137,7 +155,7 @@ function createPhiNode(strandsContext, phiInputs, varName) { nodeType: 'phi', dimension, baseType, - dependsOn: phiInputs.map(input => input.nodeId).filter(id => id !== null), + dependsOn: phiInputs.map(input => input.value.id).filter(id => id !== null), phiBlocks: phiInputs.map(input => input.blockId), phiInputs // Store the full phi input information }; diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 7aec1242ef..6a6453ed3c 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -42,40 +42,191 @@ const cfgHandlers = { if (nodeType === NodeType.STATEMENT) { glslBackend.generateStatement(generationContext, dag, nodeID); } + if (nodeType === NodeType.ASSIGNMENT) { + glslBackend.generateAssignment(generationContext, dag, nodeID); + } + } + }, + + [BlockType.BRANCH](blockID, strandsContext, generationContext) { + console.log(`Processing BRANCH block ${blockID}`); + const { dag, cfg } = strandsContext; + + // Find all phi nodes in this branch block and declare them + const blockInstructions = cfg.blockInstructions[blockID] || []; + console.log(`Instructions in branch block ${blockID}:`, blockInstructions); + + for (const nodeID of blockInstructions) { + const node = getNodeDataFromID(dag, nodeID); + console.log(`Checking node ${nodeID} with nodeType: ${node.nodeType}`); + + if (node.nodeType === NodeType.PHI) { + // Create a temporary variable for this phi node + const tmp = `T${generationContext.nextTempID++}`; + generationContext.tempNames[nodeID] = tmp; + + console.log(`Declared phi temp variable: ${tmp} for node ${nodeID}`); + + const T = extractNodeTypeInfo(dag, nodeID); + const typeName = glslBackend.getTypeName(T.baseType, T.dimension); + + // Declare the temporary variable + generationContext.write(`${typeName} ${tmp};`); + } } + + // Execute the default block handling for any remaining instructions in this block + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, [BlockType.IF_COND](blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; const conditionID = cfg.blockConditions[blockID]; const condExpr = glslBackend.generateExpression(generationContext, dag, conditionID); - generationContext.write(`if (${condExpr}) {`) - generationContext.indent++; + + generationContext.write(`if (${condExpr})`); this[BlockType.DEFAULT](blockID, strandsContext, generationContext); - generationContext.indent--; - generationContext.write(`}`) - return; }, [BlockType.IF_BODY](blockID, strandsContext, generationContext) { + generationContext.write(`{`); + generationContext.indent++; + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + // Assign values to phi nodes that this branch feeds into + this.assignPhiNodeValues(blockID, strandsContext, generationContext); + + generationContext.indent--; + generationContext.write(`}`); }, [BlockType.ELIF_BODY](blockID, strandsContext, generationContext) { + generationContext.write(`{`); + generationContext.indent++; + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + // Assign values to phi nodes that this branch feeds into + this.assignPhiNodeValues(blockID, strandsContext, generationContext); + + generationContext.indent--; + generationContext.write(`}`); }, [BlockType.ELSE_BODY](blockID, strandsContext, generationContext) { + generationContext.write(`else {`); + generationContext.indent++; + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + // Assign values to phi nodes that this branch feeds into + this.assignPhiNodeValues(blockID, strandsContext, generationContext); + + generationContext.indent--; + generationContext.write(`}`); }, [BlockType.MERGE](blockID, strandsContext, generationContext) { - this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + return this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + const { dag, cfg } = strandsContext; + + // Handle phi nodes specially in merge blocks + const instructions = cfg.blockInstructions[blockID] || []; + for (const nodeID of instructions) { + const node = getNodeDataFromID(dag, nodeID); + + if (node.nodeType !== NodeType.PHI) { + debugger + console.log(`Handling node in merge block`) + // Handle non-phi nodes normally + const nodeType = dag.nodeTypes[nodeID]; + if (shouldCreateTemp(dag, nodeID)) { + const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID); + generationContext.write(declaration); + } + if (nodeType === NodeType.STATEMENT) { + glslBackend.generateStatement(generationContext, dag, nodeID); + } + } + } }, [BlockType.FUNCTION](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, + + assignPhiNodeValues(blockID, strandsContext, generationContext) { + const { dag, cfg } = strandsContext; + + console.log(`assignPhiNodeValues called for blockID: ${blockID}`); + + // Find all phi nodes that this block feeds into + const successors = cfg.outgoingEdges[blockID] || []; + console.log(`Successors for block ${blockID}:`, successors); + + for (const successorBlockID of successors) { + const instructions = cfg.blockInstructions[successorBlockID] || []; + console.log(`Instructions in successor block ${successorBlockID}:`, instructions); + + for (const nodeID of instructions) { + const node = getNodeDataFromID(dag, nodeID); + console.log(`Checking node ${nodeID} with nodeType: ${node.nodeType}`); + + if (node.nodeType === NodeType.PHI) { + console.log(`Found phi node ${nodeID} with phiBlocks:`, node.phiBlocks, 'dependsOn:', node.dependsOn); + + // Find which input of this phi node corresponds to our block + // The phiBlocks array maps to the dependsOn array + const branchIndex = node.phiBlocks?.indexOf(blockID); + console.log(`branchIndex for block ${blockID}:`, branchIndex); + + if (branchIndex !== -1 && branchIndex < node.dependsOn.length) { + const sourceNodeID = node.dependsOn[branchIndex]; + const tempName = generationContext.tempNames[nodeID]; + + console.log(`Assigning phi node: ${tempName} = source ${sourceNodeID}`); + + if (tempName && sourceNodeID !== null) { + const sourceExpr = glslBackend.generateExpression(generationContext, dag, sourceNodeID); + generationContext.write(`${tempName} = ${sourceExpr};`); + } + } + } + } + } + }, + + declarePhiNodesForConditional(blockID, strandsContext, generationContext) { + const { dag, cfg } = strandsContext; + + console.log(`declarePhiNodesForConditional called for blockID: ${blockID}`); + + // Find all phi nodes in the merge blocks that this conditional feeds into + const successors = cfg.outgoingEdges[blockID] || []; + console.log(`Successors for conditional block ${blockID}:`, successors); + + for (const successorBlockID of successors) { + const blockInstructions = cfg.blockInstructions[successorBlockID] || []; + console.log(`Instructions in merge block ${successorBlockID}:`, blockInstructions); + + for (const nodeID of blockInstructions) { + const node = getNodeDataFromID(dag, nodeID); + console.log(`Checking node ${nodeID} with nodeType: ${node.nodeType}`); + + if (node.nodeType === NodeType.PHI) { + // Create a temporary variable for this phi node + const tmp = `T${generationContext.nextTempID++}`; + generationContext.tempNames[nodeID] = tmp; + + console.log(`Declared phi temp variable: ${tmp} for node ${nodeID}`); + + const T = extractNodeTypeInfo(dag, nodeID); + const typeName = glslBackend.getTypeName(T.baseType, T.dimension); + + // Declare the temporary variable + generationContext.write(`${typeName} ${tmp};`); + } + } + } + } } @@ -106,6 +257,20 @@ export const glslBackend = { } }, + generateAssignment(generationContext, dag, nodeID) { + const node = getNodeDataFromID(dag, nodeID); + // dependsOn[0] = phiNodeID, dependsOn[1] = sourceNodeID + const phiNodeID = node.dependsOn[0]; + const sourceNodeID = node.dependsOn[1]; + + const phiTempName = generationContext.tempNames[phiNodeID]; + const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); + + if (phiTempName && sourceExpr) { + generationContext.write(`${phiTempName} = ${sourceExpr};`); + } + }, + generateDeclaration(generationContext, dag, nodeID) { const expr = this.generateExpression(generationContext, dag, nodeID); const tmp = `T${generationContext.nextTempID++}`; @@ -202,21 +367,31 @@ export const glslBackend = { } case NodeType.PHI: // Phi nodes represent conditional merging of values - // For now, just use the first valid input as a simple implementation - // TODO: Implement proper phi node control flow - const validInputs = node.dependsOn.filter(id => id !== null); - if (validInputs.length > 0) { - return this.generateExpression(generationContext, dag, validInputs[0]); + // They should already have been declared as temporary variables + // and assigned in the appropriate branches + if (generationContext.tempNames?.[nodeID]) { + return generationContext.tempNames[nodeID]; } else { - // Fallback: create a default value - const typeName = this.getTypeName(node.baseType, node.dimension); - if (node.dimension === 1) { - return node.baseType === BaseType.FLOAT ? '0.0' : '0'; + // If no temp was created, this phi node only has one input + // so we can just use that directly + const validInputs = node.dependsOn.filter(id => id !== null); + if (validInputs.length > 0) { + return this.generateExpression(generationContext, dag, validInputs[0]); } else { - return `${typeName}(0.0)`; + throw new Error(`No valid inputs for node`) + // Fallback: create a default value + const typeName = this.getTypeName(node.baseType, node.dimension); + if (node.dimension === 1) { + return node.baseType === BaseType.FLOAT ? '0.0' : '0'; + } else { + return `${typeName}(0.0)`; + } } } - + + case NodeType.ASSIGNMENT: + FES.internalError(`ASSIGNMENT nodes should not be used as expressions`) + default: FES.internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`) } diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 323fbdea26..97a475b839 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -442,115 +442,119 @@ suite('p5.Shader', function() { suite('if statement conditionals', () => { it('should handle simple if statement with true condition', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - + const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 1.0); // true condition - + myp5.getPixelInputs(inputs => { let color = myp5.float(0.5); // initial gray - + if (condition > 0.5) { color = myp5.float(1.0); // set to white in if branch } - + inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - + + myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - + // Check that the center pixel is white (condition was true) const pixelColor = myp5.get(25, 25); - expect(pixelColor[0]).toBeCloseTo(255, 0); // Red channel should be 255 (white) - expect(pixelColor[1]).toBeCloseTo(255, 0); // Green channel should be 255 - expect(pixelColor[2]).toBeCloseTo(255, 0); // Blue channel should be 255 + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) + assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 + assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); it('should handle simple if statement with false condition', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - + const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 0.0); // false condition - + myp5.getPixelInputs(inputs => { let color = myp5.float(0.5); // initial gray - + if (condition > 0.5) { color = myp5.float(1.0); // set to white in if branch } - + inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - + + myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - + // Check that the center pixel is gray (condition was false, original value kept) const pixelColor = myp5.get(25, 25); - expect(pixelColor[0]).toBeCloseTo(127, 5); // Red channel should be ~127 (gray) - expect(pixelColor[1]).toBeCloseTo(127, 5); // Green channel should be ~127 - expect(pixelColor[2]).toBeCloseTo(127, 5); // Blue channel should be ~127 + assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); it('should handle if-else statement', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - + const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 0.0); // false condition - + myp5.getPixelInputs(inputs => { let color = myp5.float(0.5); // initial gray - + if (condition > 0.5) { color = myp5.float(1.0); // white for true } else { color = myp5.float(0.0); // black for false } - + inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - + + myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - + // Check that the center pixel is black (else branch executed) const pixelColor = myp5.get(25, 25); - expect(pixelColor[0]).toBeCloseTo(0, 5); // Red channel should be ~0 (black) - expect(pixelColor[1]).toBeCloseTo(0, 5); // Green channel should be ~0 - expect(pixelColor[2]).toBeCloseTo(0, 5); // Blue channel should be ~0 + assert.approximately(pixelColor[0], 0, 5); // Red channel should be ~0 (black) + assert.approximately(pixelColor[1], 0, 5); // Green channel should be ~0 + assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0 }); it('should handle multiple variable assignments in if statement', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - + const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 1.0); // true condition - + myp5.getPixelInputs(inputs => { let red = myp5.float(0.0); let green = myp5.float(0.0); let blue = myp5.float(0.0); - + if (condition > 0.5) { red = myp5.float(1.0); green = myp5.float(0.5); blue = myp5.float(0.0); } - + inputs.color = [red, green, blue, 1.0]; return inputs; }); }, { myp5 }); - + + myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - + // Check that the center pixel has the expected color (red=1.0, green=0.5, blue=0.0) const pixelColor = myp5.get(25, 25); expect(pixelColor[0]).toBeCloseTo(255, 0); // Red channel should be 255 @@ -560,69 +564,71 @@ suite('p5.Shader', function() { it('should handle modifications after if statement', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - + const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 1.0); // true condition - + myp5.getPixelInputs(inputs => { let color = myp5.float(0.0); // start with black - + if (condition > 0.5) { color = myp5.float(1.0); // set to white in if branch } else { color = myp5.float(0.5); // set to gray in else branch } - + // Modify the color after the if statement color = color * 0.5; // Should result in 0.5 * 1.0 = 0.5 (gray) - + inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - + + myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - + // Check that the center pixel is gray (white * 0.5 = gray) const pixelColor = myp5.get(25, 25); - expect(pixelColor[0]).toBeCloseTo(127, 5); // Red channel should be ~127 (gray) - expect(pixelColor[1]).toBeCloseTo(127, 5); // Green channel should be ~127 - expect(pixelColor[2]).toBeCloseTo(127, 5); // Blue channel should be ~127 + assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); it('should handle modifications after if statement in both branches', () => { myp5.createCanvas(100, 50, myp5.WEBGL); - + const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { const uv = inputs.uv; const condition = uv.x > 0.5; // left half false, right half true let color = myp5.float(0.0); - + if (condition) { color = myp5.float(1.0); // white on right side } else { color = myp5.float(0.8); // light gray on left side } - + // Multiply by 0.5 after the if statement color = color * 0.5; // Right side: 1.0 * 0.5 = 0.5 (medium gray) // Left side: 0.8 * 0.5 = 0.4 (darker gray) - + inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - + + myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - + // Check left side (false condition) const leftPixel = myp5.get(25, 25); expect(leftPixel[0]).toBeCloseTo(102, 10); // 0.4 * 255 ≈ 102 - + // Check right side (true condition) const rightPixel = myp5.get(75, 25); expect(rightPixel[0]).toBeCloseTo(127, 10); // 0.5 * 255 ≈ 127 @@ -630,13 +636,13 @@ suite('p5.Shader', function() { it('should handle if-else-if chains', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - + const testShader = myp5.baseMaterialShader().modify(() => { const value = myp5.uniformFloat(() => 0.5); // middle value - + myp5.getPixelInputs(inputs => { let color = myp5.float(0.0); - + if (value > 0.8) { color = myp5.float(1.0); // white for high values } else if (value > 0.3) { @@ -644,32 +650,33 @@ suite('p5.Shader', function() { } else { color = myp5.float(0.0); // black for low values } - + inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - + + myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - + // Check that the center pixel is gray (medium condition was true) const pixelColor = myp5.get(25, 25); - expect(pixelColor[0]).toBeCloseTo(127, 5); // Red channel should be ~127 (gray) - expect(pixelColor[1]).toBeCloseTo(127, 5); // Green channel should be ~127 - expect(pixelColor[2]).toBeCloseTo(127, 5); // Blue channel should be ~127 + assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); it('should handle nested if statements', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - + const testShader = myp5.baseMaterialShader().modify(() => { const outerCondition = myp5.uniformFloat(() => 1.0); // true const innerCondition = myp5.uniformFloat(() => 1.0); // true - + myp5.getPixelInputs(inputs => { let color = myp5.float(0.0); - + if (outerCondition > 0.5) { if (innerCondition > 0.5) { color = myp5.float(1.0); // white for both conditions true @@ -679,32 +686,33 @@ suite('p5.Shader', function() { } else { color = myp5.float(0.0); // black for outer false } - + inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - + + myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - + // Check that the center pixel is white (both conditions were true) const pixelColor = myp5.get(25, 25); - expect(pixelColor[0]).toBeCloseTo(255, 0); // Red channel should be 255 (white) - expect(pixelColor[1]).toBeCloseTo(255, 0); // Green channel should be 255 - expect(pixelColor[2]).toBeCloseTo(255, 0); // Blue channel should be 255 + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) + assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 + assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); // Keep one direct API test for completeness it('should handle direct StrandsIf API usage', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - + const testShader = myp5.baseMaterialShader().modify(() => { const conditionValue = myp5.uniformFloat(() => 1.0); // true condition - + myp5.getPixelInputs(inputs => { let color = myp5.float(0.5); // initial gray - + const assignments = myp5.strandsIf( conditionValue, () => { @@ -715,22 +723,23 @@ suite('p5.Shader', function() { ).Else(() => { return { color: color }; // keep original in else branch }); - + color = assignments.color; - + inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - + + myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - + // Check that the center pixel is white (condition was true) const pixelColor = myp5.get(25, 25); - expect(pixelColor[0]).toBeCloseTo(255, 0); // Red channel should be 255 (white) - expect(pixelColor[1]).toBeCloseTo(255, 0); // Green channel should be 255 - expect(pixelColor[2]).toBeCloseTo(255, 0); // Blue channel should be 255 + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) + assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 + assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); }); }); From 05d0b8e7e0bad74906a6fa5b231b14295d871c1b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 2 Oct 2025 16:19:20 -0400 Subject: [PATCH 03/22] Fix tests --- src/strands/ir_types.js | 5 ++-- src/strands/p5.strands.js | 1 + src/strands/strands_conditionals.js | 40 ++++++++++++++++++++--------- src/strands/strands_glslBackend.js | 30 ++++++++-------------- test/unit/webgl/p5.Shader.js | 35 +++++++++++++------------ 5 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 93caf1824f..a888d3bb99 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -201,9 +201,10 @@ export const BlockType = { BRANCH: 'branch', IF_COND: 'if_cond', IF_BODY: 'if_body', - ELIF_BODY: 'elif_body', - ELIF_COND: 'elif_cond', + ELSE_COND: 'else_cond', ELSE_BODY: 'else_body', + SCOPE_START: 'scope_start', + SCOPE_END: 'scope_end', FOR: 'for', MERGE: 'merge', DEFAULT: 'default', diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 384b6068d7..b93f8adcbf 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -80,6 +80,7 @@ function strands(p5, fn) { } else { strandsCallback = shaderModifier; } + console.log(strandsCallback.toString()) // 2. Build the IR from JavaScript API const globalScope = createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index 636f7af9ef..73b53cb57f 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -55,7 +55,7 @@ function buildConditional(strandsContext, conditional) { const branchBlock = CFG.createBasicBlock(cfg, BlockType.BRANCH); CFG.addEdge(cfg, cfg.currentBlock, branchBlock); CFG.addEdge(cfg, branchBlock, mergeBlock); - + let previousBlock = branchBlock; for (let i = 0; i < branches.length; i++) { @@ -68,10 +68,19 @@ function buildConditional(strandsContext, conditional) { cfg.blockConditions[conditionBlock] = condition.id; previousBlock = conditionBlock; CFG.popBlock(cfg); + } else { + // This is an else branch - create an ELSE_COND block + const elseCondBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_COND); + CFG.addEdge(cfg, previousBlock, elseCondBlock); + previousBlock = elseCondBlock; } + // Create SCOPE_START block to mark beginning of branch scope + const scopeStartBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_START); + CFG.addEdge(cfg, previousBlock, scopeStartBlock); + const branchBlock = CFG.createBasicBlock(cfg, blockType); - CFG.addEdge(cfg, previousBlock, branchBlock); + CFG.addEdge(cfg, scopeStartBlock, branchBlock); branchBlocks.push(branchBlock); CFG.pushBlock(cfg, branchBlock); @@ -84,12 +93,19 @@ function buildConditional(strandsContext, conditional) { } } results.push(branchResults); + + // Create SCOPE_END block to mark end of branch scope + const scopeEndBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_END); if (cfg.currentBlock !== branchBlock) { - CFG.addEdge(cfg, cfg.currentBlock, mergeBlock); - CFG.popBlock(); + CFG.addEdge(cfg, cfg.currentBlock, scopeEndBlock); + CFG.popBlock(cfg); + } else { + CFG.addEdge(cfg, branchBlock, scopeEndBlock); + CFG.popBlock(cfg); } - CFG.addEdge(cfg, cfg.currentBlock, mergeBlock); - CFG.popBlock(cfg); + + CFG.addEdge(cfg, scopeEndBlock, mergeBlock); + previousBlock = scopeStartBlock; // Next condition should branch from the same point } // Push the branch block for modification to attach phi nodes there @@ -105,15 +121,15 @@ function buildConditional(strandsContext, conditional) { for (let i = 0; i < results.length; i++) { const branchResult = results[i]; const branchBlockID = branchBlocks[i]; - + CFG.pushBlockForModification(cfg, branchBlockID); - + for (const key in branchResult) { if (mergedAssignments[key]) { // Create an assignment statement: phiNode = branchResult[key] const phiNodeID = mergedAssignments[key].id; const sourceNodeID = branchResult[key].id; - + // Create an assignment operation node // Use dependsOn[0] for phiNodeID and dependsOn[1] for sourceNodeID // This represents: dependsOn[0] = dependsOn[1] (phiNode = sourceNode) @@ -122,12 +138,12 @@ function buildConditional(strandsContext, conditional) { dependsOn: [phiNodeID, sourceNodeID], phiBlocks: [] }; - + const assignmentID = DAG.getOrCreateNode(strandsContext.dag, assignmentNode); CFG.recordInBasicBlock(cfg, branchBlockID, assignmentID); } } - + CFG.popBlock(cfg); } @@ -138,7 +154,7 @@ function buildConditional(strandsContext, conditional) { function createPhiNode(strandsContext, phiInputs, varName) { console.log('createPhiNode called with varName:', varName, 'phiInputs:', phiInputs); - + // For now, create a simple phi node // We'll need to determine the proper dimension and baseType from the inputs const validInputs = phiInputs.filter(input => input.value.id !== null); diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 6a6453ed3c..3f9967fe10 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -88,38 +88,30 @@ const cfgHandlers = { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, - [BlockType.IF_BODY](blockID, strandsContext, generationContext) { - generationContext.write(`{`); - generationContext.indent++; + [BlockType.ELSE_COND](blockID, strandsContext, generationContext) { + generationContext.write(`else`); this[BlockType.DEFAULT](blockID, strandsContext, generationContext); - - // Assign values to phi nodes that this branch feeds into - this.assignPhiNodeValues(blockID, strandsContext, generationContext); - - generationContext.indent--; - generationContext.write(`}`); }, - [BlockType.ELIF_BODY](blockID, strandsContext, generationContext) { - generationContext.write(`{`); - generationContext.indent++; + [BlockType.IF_BODY](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); - // Assign values to phi nodes that this branch feeds into this.assignPhiNodeValues(blockID, strandsContext, generationContext); - - generationContext.indent--; - generationContext.write(`}`); }, + [BlockType.ELSE_BODY](blockID, strandsContext, generationContext) { - generationContext.write(`else {`); - generationContext.indent++; this[BlockType.DEFAULT](blockID, strandsContext, generationContext); - // Assign values to phi nodes that this branch feeds into this.assignPhiNodeValues(blockID, strandsContext, generationContext); + }, + + [BlockType.SCOPE_START](blockID, strandsContext, generationContext) { + generationContext.write(`{`); + generationContext.indent++; + }, + [BlockType.SCOPE_END](blockID, strandsContext, generationContext) { generationContext.indent--; generationContext.write(`}`); }, diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 97a475b839..a14c40c10b 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -420,7 +420,7 @@ suite('p5.Shader', function() { }); suite('p5.strands', () => { - it('does not break when arrays are in uniform callbacks', () => { + test('does not break when arrays are in uniform callbacks', () => { myp5.createCanvas(5, 5, myp5.WEBGL); const myShader = myp5.baseMaterialShader().modify(() => { const size = myp5.uniformVector2(() => [myp5.width, myp5.height]); @@ -440,7 +440,7 @@ suite('p5.Shader', function() { }); suite('if statement conditionals', () => { - it('should handle simple if statement with true condition', () => { + test('handle simple if statement with true condition', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { @@ -469,7 +469,7 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); - it('should handle simple if statement with false condition', () => { + test('handle simple if statement with false condition', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { @@ -498,7 +498,7 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); - it('should handle if-else statement', () => { + test('handle if-else statement', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { @@ -529,7 +529,7 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0 }); - it('should handle multiple variable assignments in if statement', () => { + test('handle multiple variable assignments in if statement', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { @@ -557,12 +557,12 @@ suite('p5.Shader', function() { // Check that the center pixel has the expected color (red=1.0, green=0.5, blue=0.0) const pixelColor = myp5.get(25, 25); - expect(pixelColor[0]).toBeCloseTo(255, 0); // Red channel should be 255 - expect(pixelColor[1]).toBeCloseTo(127, 5); // Green channel should be ~127 - expect(pixelColor[2]).toBeCloseTo(0, 5); // Blue channel should be ~0 + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0 }); - it('should handle modifications after if statement', () => { + test('handle modifications after if statement', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { @@ -596,12 +596,13 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); - it('should handle modifications after if statement in both branches', () => { + test('handle modifications after if statement in both branches', () => { myp5.createCanvas(100, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { - const uv = inputs.uv; + debugger + const uv = inputs.texCoord; const condition = uv.x > 0.5; // left half false, right half true let color = myp5.float(0.0); @@ -627,14 +628,14 @@ suite('p5.Shader', function() { // Check left side (false condition) const leftPixel = myp5.get(25, 25); - expect(leftPixel[0]).toBeCloseTo(102, 10); // 0.4 * 255 ≈ 102 + assert.approximately(leftPixel[0], 102, 5); // 0.4 * 255 ≈ 102 // Check right side (true condition) const rightPixel = myp5.get(75, 25); - expect(rightPixel[0]).toBeCloseTo(127, 10); // 0.5 * 255 ≈ 127 + assert.approximately(rightPixel[0], 127, 5); // 0.5 * 255 ≈ 127 }); - it('should handle if-else-if chains', () => { + test('handle if-else-if chains', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { @@ -667,7 +668,7 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); - it('should handle nested if statements', () => { + test('handle nested if statements', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { @@ -704,7 +705,7 @@ suite('p5.Shader', function() { }); // Keep one direct API test for completeness - it('should handle direct StrandsIf API usage', () => { + test('handle direct StrandsIf API usage', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { @@ -714,7 +715,7 @@ suite('p5.Shader', function() { let color = myp5.float(0.5); // initial gray const assignments = myp5.strandsIf( - conditionValue, + conditionValue.greaterThan(0), () => { let tmp = color.copy(); tmp = myp5.float(1.0); // set to white in if branch From 430988d0d5e04a7da9941dadba150effe59f8a2d Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 2 Oct 2025 16:24:30 -0400 Subject: [PATCH 04/22] Clean up logging --- src/strands/ir_builders.js | 55 ------------- src/strands/ir_cfg.js | 13 --- src/strands/ir_types.js | 18 ---- src/strands/p5.strands.js | 1 - src/strands/strands_api.js | 36 -------- src/strands/strands_conditionals.js | 46 +---------- src/strands/strands_glslBackend.js | 123 +--------------------------- src/strands/strands_node.js | 3 - src/strands/strands_transpiler.js | 44 ---------- test/unit/webgl/p5.Shader.js | 109 ------------------------ 10 files changed, 4 insertions(+), 444 deletions(-) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 9a1f4dda1a..7c1605bd32 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -4,7 +4,6 @@ import * as FES from './strands_FES' import { NodeType, OpCode, BaseType, DataType, BasePriority, OpCodeToSymbol, typeEquals, } from './ir_types'; import { createStrandsNode, StrandsNode } from './strands_node'; import { strandsBuiltinFunctions } from './strands_builtins'; - ////////////////////////////////////////////// // Builders for node graphs ////////////////////////////////////////////// @@ -24,7 +23,6 @@ export function scalarLiteralNode(strandsContext, typeInfo, value) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension }; } - export function variableNode(strandsContext, typeInfo, identifier) { const { cfg, dag } = strandsContext; const { dimension, baseType } = typeInfo; @@ -38,7 +36,6 @@ export function variableNode(strandsContext, typeInfo, identifier) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension }; } - export function unaryOpNode(strandsContext, nodeOrValue, opCode) { const { dag, cfg } = strandsContext; let dependsOn; @@ -61,7 +58,6 @@ export function unaryOpNode(strandsContext, nodeOrValue, opCode) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: node.dimension }; } - export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { const { dag, cfg } = strandsContext; // Construct a node for right if its just an array or number etc. @@ -74,7 +70,6 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) } let finalLeftNodeID = leftStrandsNode.id; let finalRightNodeID = rightStrandsNode.id; - // Check if we have to cast either node const leftType = DAG.extractNodeTypeInfo(dag, leftStrandsNode.id); const rightType = DAG.extractNodeTypeInfo(dag, rightStrandsNode.id); @@ -104,7 +99,6 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) } else if (leftType.baseType !== rightType.baseType || leftType.dimension !== rightType.dimension) { - if (leftType.dimension === 1 && rightType.dimension > 1) { cast.node = leftStrandsNode; cast.toType = rightType; @@ -125,9 +119,7 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) else { FES.userError('type error', `A vector of length ${leftType.dimension} operated with a vector of length ${rightType.dimension} is not allowed.`); } - const casted = primitiveConstructorNode(strandsContext, cast.toType, cast.node); - if (cast.node === leftStrandsNode) { leftStrandsNode = createStrandsNode(casted.id, casted.dimension, strandsContext); finalLeftNodeID = leftStrandsNode.id; @@ -136,7 +128,6 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) finalRightNodeID = rightStrandsNode.id; } } - const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode, @@ -148,7 +139,6 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: nodeData.dimension }; } - export function memberAccessNode(strandsContext, parentNode, componentNode, memberTypeInfo) { const { dag, cfg } = strandsContext; const nodeData = DAG.createNodeData({ @@ -162,10 +152,8 @@ export function memberAccessNode(strandsContext, parentNode, componentNode, memb CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: memberTypeInfo.dimension }; } - export function structInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) { const { cfg, dag, } = strandsContext; - if (dependsOn.length === 0) { for (const prop of structTypeInfo.properties) { const typeInfo = prop.dataType; @@ -180,7 +168,6 @@ export function structInstanceNode(strandsContext, structTypeInfo, identifier, d dependsOn.push(componentID); } } - const nodeData = DAG.createNodeData({ nodeType: NodeType.VARIABLE, dimension: structTypeInfo.properties.length, @@ -190,16 +177,12 @@ export function structInstanceNode(strandsContext, structTypeInfo, identifier, d }) const structID = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, structID); - return { id: structID, dimension: 0, components: dependsOn }; } - function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { const inputs = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; const mappedDependencies = []; let { dimension, baseType } = typeInfo; - - const dag = strandsContext.dag; let calculatedDimensions = 0; let originalNodeID = null; @@ -207,9 +190,7 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { if (dep instanceof StrandsNode) { const node = DAG.getNodeDataFromID(dag, dep.id); originalNodeID = dep.id; - console.log('Setting baseType from StrandsNode:', node.baseType); baseType = node.baseType; - if (node.opCode === OpCode.Nary.CONSTRUCTOR) { for (const inner of node.dependsOn) { mappedDependencies.push(inner); @@ -217,12 +198,10 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { } else { mappedDependencies.push(dep.id); } - calculatedDimensions += node.dimension; continue; } else if (typeof dep === 'number') { - console.log('Creating literal node for number:', dep, 'with baseType:', baseType); const { id, dimension } = scalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep); mappedDependencies.push(id); calculatedDimensions += dimension; @@ -246,7 +225,6 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { } return { originalNodeID, mappedDependencies, inferredTypeInfo }; } - export function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray) { const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, @@ -258,29 +236,22 @@ export function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); return id; } - export function primitiveConstructorNode(strandsContext, typeInfo, dependsOn) { const cfg = strandsContext.cfg; const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn); - const finalType = { baseType: typeInfo.baseType, dimension: inferredTypeInfo.dimension }; - const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies); - if (typeInfo.baseType !== BaseType.DEFER) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); } - return { id, dimension: finalType.dimension, components: mappedDependencies }; } - export function structConstructorNode(strandsContext, structTypeInfo, rawUserArgs) { const { cfg, dag } = strandsContext; const { identifer, properties } = structTypeInfo; - if (!(rawUserArgs.length === properties.length)) { FES.userError('type error', `You've tried to construct a ${structTypeInfo.typeName} struct with ${rawUserArgs.length} properties, but it expects ${properties.length} properties.\n` + @@ -288,7 +259,6 @@ export function structConstructorNode(strandsContext, structTypeInfo, rawUserArg `${properties.map(prop => prop.name + ' ' + prop.DataType.baseType + prop.DataType.dimension)}` ); } - const dependsOn = []; for (let i = 0; i < properties.length; i++) { const expectedProperty = properties[i]; @@ -302,7 +272,6 @@ export function structConstructorNode(strandsContext, structTypeInfo, rawUserArg ); } } - const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, @@ -314,7 +283,6 @@ export function structConstructorNode(strandsContext, structTypeInfo, rawUserArg CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: properties.length, components: structTypeInfo.components }; } - export function functionCallNode( strandsContext, functionName, @@ -323,7 +291,6 @@ export function functionCallNode( ) { const { cfg, dag } = strandsContext; const overloads = rawOverloads || strandsBuiltinFunctions[functionName]; - const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDepsToIDs(strandsContext, DataType.defer, rawUserArg)); const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); if (matchingArgsCounts.length === 0) { @@ -334,28 +301,23 @@ export function functionCallNode( const argsLengthStr = argsLengthArr.join(', or '); FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${preprocessedArgs.length} arguments were provided.`); } - const isGeneric = (T) => T.dimension === null; let bestOverload = null; let bestScore = 0; let inferredReturnType = null; let inferredDimension = null; - for (const overload of matchingArgsCounts) { let isValid = true; let similarity = 0; - for (let i = 0; i < preprocessedArgs.length; i++) { const preArg = preprocessedArgs[i]; const argType = preArg.inferredTypeInfo; const expectedType = overload.params[i]; let dimension = expectedType.dimension; - if (isGeneric(expectedType)) { if (inferredDimension === null || inferredDimension === 1) { inferredDimension = argType.dimension; } - if (inferredDimension !== argType.dimension && !(argType.dimension === 1 && inferredDimension >= 1) ) { @@ -368,16 +330,13 @@ export function functionCallNode( isValid = false; } } - if (argType.baseType === expectedType.baseType) { similarity += 2; } else if(expectedType.priority > argType.priority) { similarity += 1; } - } - if (isValid && (!bestOverload || similarity > bestScore)) { bestOverload = overload; bestScore = similarity; @@ -387,11 +346,9 @@ export function functionCallNode( } } } - if (bestOverload === null) { FES.userError('parameter validation', `No matching overload for ${functionName} was found!`); } - let dependsOn = []; for (let i = 0; i < bestOverload.params.length; i++) { const arg = preprocessedArgs[i]; @@ -408,7 +365,6 @@ export function functionCallNode( dependsOn.push(castedArgID); } } - const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, @@ -421,7 +377,6 @@ export function functionCallNode( CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: inferredReturnType.dimension }; } - export function statementNode(strandsContext, opCode) { const { dag, cfg } = strandsContext; const nodeData = DAG.createNodeData({ @@ -432,7 +387,6 @@ export function statementNode(strandsContext, opCode) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return id; } - export function swizzleNode(strandsContext, parentNode, swizzle) { const { dag, cfg } = strandsContext; const baseType = dag.baseTypes[parentNode.id]; @@ -448,7 +402,6 @@ export function swizzleNode(strandsContext, parentNode, swizzle) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: swizzle.length }; } - export function swizzleTrap(id, dimension, strandsContext, onRebind) { const swizzleSets = [ ['x', 'y', 'z', 'w'], @@ -479,11 +432,8 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { chars.every(c => swizzleSet.includes(c)) && new Set(chars).size === chars.length && target.dimension >= chars.length; - if (!valid) continue; - const dim = target.dimension; - // lanes are the underlying values of the target vector // e.g. lane 0 holds the value aliased by 'x', 'r', and 's' // the lanes array is in the 'correct' order @@ -492,7 +442,6 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { const { id, dimension } = swizzleNode(strandsContext, target, 'xyzw'[i]); lanes[i] = createStrandsNode(id, dimension, strandsContext); } - // The scalars array contains the individual components of the users values. // This may not be the most efficient way, as we swizzle each component individually, // so that .xyz becomes .x, .y, .z @@ -522,7 +471,6 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { } else { FES.userError('type error', `Unsupported RHS for swizzle assignment: ${value}`); } - // The canonical index refers to the actual value's position in the vector lanes // i.e. we are finding (3,2,1) from .zyx // We set the correct value in the lanes array @@ -530,7 +478,6 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { const canonicalIndex = swizzleSet.indexOf(chars[j]); lanes[canonicalIndex] = scalars[j]; } - const orig = DAG.getNodeDataFromID(strandsContext.dag, target.id); const baseType = orig?.baseType ?? BaseType.FLOAT; const { id: newID } = primitiveConstructorNode( @@ -538,9 +485,7 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { { baseType, dimension: dim }, lanes ); - target.id = newID; - // If we swizzle assign on a struct component i.e. // inputs.position.rg = [1, 2] // The onRebind callback will update the structs components so that it refers to the new values, diff --git a/src/strands/ir_cfg.js b/src/strands/ir_cfg.js index 16988ca1e0..0d2303aca7 100644 --- a/src/strands/ir_cfg.js +++ b/src/strands/ir_cfg.js @@ -1,8 +1,6 @@ import { BlockTypeToName } from "./ir_types"; import * as FES from './strands_FES' - // Todo: remove edges to simplify. Block order is always ordered already. - export function createControlFlowGraph() { return { // graph structure @@ -18,24 +16,20 @@ export function createControlFlowGraph() { currentBlock: -1, }; } - export function pushBlock(graph, blockID) { graph.blockStack.push(blockID); graph.blockOrder.push(blockID); graph.currentBlock = blockID; } - export function popBlock(graph) { graph.blockStack.pop(); const len = graph.blockStack.length; graph.currentBlock = graph.blockStack[len-1]; } - export function pushBlockForModification(graph, blockID) { graph.blockStack.push(blockID); graph.currentBlock = blockID; } - export function createBasicBlock(graph, blockType) { const id = graph.nextID++; graph.blockTypes[id] = blockType; @@ -44,12 +38,10 @@ export function createBasicBlock(graph, blockType) { graph.blockInstructions[id]= []; return id; } - export function addEdge(graph, from, to) { graph.outgoingEdges[from].push(to); graph.incomingEdges[to].push(from); } - export function recordInBasicBlock(graph, blockID, nodeID) { if (nodeID === undefined) { FES.internalError('undefined nodeID in `recordInBasicBlock()`'); @@ -60,7 +52,6 @@ export function recordInBasicBlock(graph, blockID, nodeID) { graph.blockInstructions[blockID] = graph.blockInstructions[blockID] || []; graph.blockInstructions[blockID].push(nodeID); } - export function getBlockDataFromID(graph, id) { return { id, @@ -70,17 +61,14 @@ export function getBlockDataFromID(graph, id) { blockInstructions: graph.blockInstructions[id], } } - export function printBlockData(graph, id) { const block = getBlockDataFromID(graph, id); block.blockType = BlockTypeToName[block.blockType]; console.log(block); } - export function sortCFG(adjacencyList, start) { const visited = new Set(); const postOrder = []; - function dfs(v) { if (visited.has(v)) { return; @@ -91,7 +79,6 @@ export function sortCFG(adjacencyList, start) { } postOrder.push(v); } - dfs(start); return postOrder.reverse(); } \ No newline at end of file diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index a888d3bb99..81bb4c0495 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -11,11 +11,9 @@ export const NodeType = { STATEMENT: 'statement', ASSIGNMENT: 'assignment', }; - export const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); - export const NodeTypeRequiredFields = { [NodeType.OPERATION]: ["opCode", "dependsOn", "dimension", "baseType"], [NodeType.LITERAL]: ["value", "dimension", "baseType"], @@ -26,11 +24,9 @@ export const NodeTypeRequiredFields = { [NodeType.STATEMENT]: ["opCode"], [NodeType.ASSIGNMENT]: ["dependsOn"] }; - export const StatementType = { DISCARD: 'discard', }; - export const BaseType = { FLOAT: "float", INT: "int", @@ -39,7 +35,6 @@ export const BaseType = { DEFER: "defer", SAMPLER2D: "sampler2D", }; - export const BasePriority = { [BaseType.FLOAT]: 3, [BaseType.INT]: 2, @@ -48,7 +43,6 @@ export const BasePriority = { [BaseType.DEFER]: -1, [BaseType.SAMPLER2D]: -10, }; - export const DataType = { float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3, }, float2: { fnName: "vec2", baseType: BaseType.FLOAT, dimension:2, priority: 3, }, @@ -84,31 +78,25 @@ export const structType = function (hookType) { } return structType; }; - export function isStructType(typeName) { return !isNativeType(typeName); } - export function isNativeType(typeName) { return Object.keys(DataType).includes(typeName); } - export const GenType = { FLOAT: { baseType: BaseType.FLOAT, dimension: null, priority: 3 }, INT: { baseType: BaseType.INT, dimension: null, priority: 2 }, BOOL: { baseType: BaseType.BOOL, dimension: null, priority: 1 }, } - export function typeEquals(nodeA, nodeB) { return (nodeA.dimension === nodeB.dimension) && (nodeA.baseType === nodeB.baseType); } - export const TypeInfoFromGLSLName = Object.fromEntries( Object.values(DataType) .filter(info => info.fnName !== null) .map(info => [info.fnName === 'texture' ? 'sampler2D' : info.fnName, info]) ); - export const OpCode = { Binary: { ADD: 0, @@ -143,7 +131,6 @@ export const OpCode = { DISCARD: 303, } }; - export const OperatorTable = [ { arity: "unary", name: "not", symbol: "!", opCode: OpCode.Unary.LOGICAL_NOT }, { arity: "unary", name: "neg", symbol: "-", opCode: OpCode.Unary.NEGATE }, @@ -162,7 +149,6 @@ export const OperatorTable = [ { arity: "binary", name: "and", symbol: "&&", opCode: OpCode.Binary.LOGICAL_AND }, { arity: "binary", name: "or", symbol: "||", opCode: OpCode.Binary.LOGICAL_OR }, ]; - export const ConstantFolding = { [OpCode.Binary.ADD]: (a, b) => a + b, [OpCode.Binary.SUBTRACT]: (a, b) => a - b, @@ -178,12 +164,10 @@ export const ConstantFolding = { [OpCode.Binary.LOGICAL_AND]: (a, b) => a && b, [OpCode.Binary.LOGICAL_OR]: (a, b) => a || b, }; - // export const SymbolToOpCode = {}; export const OpCodeToSymbol = {}; export const UnarySymbolToName = {}; export const BinarySymbolToName = {}; - for (const { symbol, opCode, name, arity } of OperatorTable) { // SymbolToOpCode[symbol] = opCode; OpCodeToSymbol[opCode] = symbol; @@ -194,7 +178,6 @@ for (const { symbol, opCode, name, arity } of OperatorTable) { BinarySymbolToName[symbol] = name; } } - export const BlockType = { GLOBAL: 'global', FUNCTION: 'function', @@ -209,7 +192,6 @@ export const BlockType = { MERGE: 'merge', DEFAULT: 'default', } - export const BlockTypeToName = Object.fromEntries( Object.entries(BlockType).map(([key, val]) => [val, key]) ); diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index b93f8adcbf..384b6068d7 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -80,7 +80,6 @@ function strands(p5, fn) { } else { strandsCallback = shaderModifier; } - console.log(strandsCallback.toString()) // 2. Build the IR from JavaScript API const globalScope = createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 9390f6376c..dd613163df 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -17,11 +17,9 @@ import * as FES from './strands_FES' import { getNodeDataFromID } from './ir_dag' import { StrandsNode, createStrandsNode } from './strands_node' import noiseGLSL from '../webgl/shaders/functions/noiseGLSL.glsl'; - ////////////////////////////////////////////// // User nodes ////////////////////////////////////////////// - export function initGlobalStrandsAPI(p5, fn, strandsContext) { // We augment the strands node with operations programatically // this means methods like .add, .sub, etc can be chained @@ -39,32 +37,26 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } } } - ////////////////////////////////////////////// // Unique Functions ////////////////////////////////////////////// fn.discard = function() { build.statementNode(strandsContext, OpCode.ControlFlow.DISCARD); } - fn.instanceID = function() { const node = build.variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, 'gl_InstanceID'); return createStrandsNode(node.id, node.dimension, strandsContext); } - // Internal methods use p5 static methods; user-facing methods use fn. // Some methods need to be used by both. - p5.strandsIf = function(conditionNode, ifBody) { return new StrandsConditional(strandsContext, conditionNode, ifBody); } fn.strandsIf = p5.strandsIf; - p5.strandsLoop = function(a, b, loopBody) { return null; } fn.strandsLoop = p5.strandsLoop; - p5.strandsNode = function(...args) { if (args.length === 1 && args[0] instanceof StrandsNode) { return args[0]; @@ -75,13 +67,11 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const { id, dimension } = build.primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, args.flat()); return createStrandsNode(id, dimension, strandsContext);//new StrandsNode(id, dimension, strandsContext); } - ////////////////////////////////////////////// // Builtins, uniforms, variable constructors ////////////////////////////////////////////// for (const [functionName, overrides] of Object.entries(strandsBuiltinFunctions)) { const isp5Function = overrides[0].isp5Function; - if (isp5Function) { const originalFn = fn[functionName]; fn[functionName] = function(...args) { @@ -105,14 +95,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } } } - // Add GLSL noise. TODO: Replace this with a backend-agnostic implementation const originalNoise = fn.noise; fn.noise = function (...args) { if (!strandsContext.active) { return originalNoise.apply(this, args); // fallback to regular p5.js noise } - strandsContext.vertexDeclarations.add(noiseGLSL); strandsContext.fragmentDeclarations.add(noiseGLSL); // Handle noise(x, y) as noise(vec2) @@ -122,7 +110,6 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } else { nodeArgs = args; } - const { id, dimension } = build.functionCallNode(strandsContext, 'noise', nodeArgs, { overloads: [{ params: [DataType.float2], @@ -131,14 +118,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); return createStrandsNode(id, dimension, strandsContext); }; - // Next is type constructors and uniform functions for (const type in DataType) { if (type === BaseType.DEFER) { continue; } const typeInfo = DataType[type]; - let pascalTypeName; if (/^[ib]vec/.test(typeInfo.fnName)) { pascalTypeName = typeInfo.fnName @@ -160,7 +145,6 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // documented these as fn[`uniform${pascalTypeName.replace('Vec', 'Vector')}`] = fn[`uniform${pascalTypeName}`]; } - const originalp5Fn = fn[typeInfo.fnName]; fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { @@ -176,14 +160,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } } } - ////////////////////////////////////////////// // Per-Hook functions ////////////////////////////////////////////// function createHookArguments(strandsContext, parameters){ const args = []; const dag = strandsContext.dag; - for (const param of parameters) { if(isStructType(param.type.typeName)) { const structTypeInfo = structType(param); @@ -210,7 +192,6 @@ function createHookArguments(strandsContext, parameters){ set(val) { const oldDependsOn = dag.dependsOn[structNode.id]; const newDependsOn = [...oldDependsOn]; - let newValueID; if (val instanceof StrandsNode) { newValueID = val.id; @@ -219,14 +200,12 @@ function createHookArguments(strandsContext, parameters){ let newVal = build.primitiveConstructorNode(strandsContext, propertyType.dataType, val); newValueID = newVal.id; } - newDependsOn[i] = newValueID; const newStructInfo = build.structInstanceNode(strandsContext, structTypeInfo, param.name, newDependsOn); structNode.id = newStructInfo.id; } }) } - args.push(structNode); } else /*if(isNativeType(paramType.typeName))*/ { @@ -238,7 +217,6 @@ function createHookArguments(strandsContext, parameters){ } return args; } - function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { if (!(returned instanceof StrandsNode)) { // try { @@ -254,7 +232,6 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName // ); // } } - const dag = strandsContext.dag; let returnedNodeID = returned.id; const receivedType = { @@ -274,10 +251,8 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); returnedNodeID = result.id; } - return returnedNodeID; } - export function createShaderHooksFunctions(strandsContext, fn, shader) { const availableHooks = { ...shader.hooks.vertex, @@ -285,36 +260,28 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { } const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); const { cfg, dag } = strandsContext; - for (const hookType of hookTypes) { const hookImplementation = function(hookUserCallback) { const entryBlockID = CFG.createBasicBlock(cfg, BlockType.FUNCTION); CFG.addEdge(cfg, cfg.currentBlock, entryBlockID); CFG.pushBlock(cfg, entryBlockID); - const args = createHookArguments(strandsContext, hookType.parameters); const userReturned = hookUserCallback(...args); const expectedReturnType = hookType.returnType; - let rootNodeID = null; - if(isStructType(expectedReturnType.typeName)) { const expectedStructType = structType(expectedReturnType); if (userReturned instanceof StrandsNode) { const returnedNode = getNodeDataFromID(strandsContext.dag, userReturned.id); - if (returnedNode.baseType !== expectedStructType.typeName) { FES.userError("type error", `You have returned a ${userReturned.baseType} from ${hookType.name} when a ${expectedStructType.typeName} was expected.`); } - const newDeps = returnedNode.dependsOn.slice(); - for (let i = 0; i < expectedStructType.properties.length; i++) { const expectedType = expectedStructType.properties[i].dataType; const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[userReturned.id], strandsContext); newDeps[i] = enforceReturnTypeMatch(strandsContext, expectedType, receivedNode, hookType.name); } - dag.dependsOn[userReturned.id] = newDeps; rootNodeID = userReturned.id; } @@ -338,13 +305,11 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const newStruct = build.structConstructorNode(strandsContext, expectedStructType, newStructDependencies); rootNodeID = newStruct.id; } - } else /*if(isNativeType(expectedReturnType.typeName))*/ { const expectedTypeInfo = TypeInfoFromGLSLName[expectedReturnType.typeName]; rootNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); } - strandsContext.hooks.push({ hookType, entryBlockID, @@ -354,7 +319,6 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { } strandsContext.windowOverrides[hookType.name] = window[hookType.name]; strandsContext.fnOverrides[hookType.name] = fn[hookType.name]; - window[hookType.name] = hookImplementation; fn[hookType.name] = hookImplementation; } diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index 73b53cb57f..f672a65b24 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -2,7 +2,6 @@ import * as CFG from './ir_cfg'; import * as DAG from './ir_dag'; import { BlockType, NodeType } from './ir_types'; import { StrandsNode, createStrandsNode } from './strands_node'; - export class StrandsConditional { constructor(strandsContext, condition, branchCallback) { // Condition must be a node... @@ -13,7 +12,6 @@ export class StrandsConditional { }]; this.ctx = strandsContext; } - ElseIf(condition, branchCallback) { this.branches.push({ condition, @@ -22,7 +20,6 @@ export class StrandsConditional { }); return this; } - Else(branchCallback = () => ({})) { this.branches.push({ condition: null, @@ -30,37 +27,28 @@ export class StrandsConditional { blockType: BlockType.ELSE_BODY }); const phiNodes = buildConditional(this.ctx, this); - - // Convert phi nodes to StrandsNodes for the user const assignments = {}; for (const [varName, phiNode] of Object.entries(phiNodes)) { assignments[varName] = createStrandsNode(phiNode.id, phiNode.dimension, this.ctx); } - return assignments; } } - function buildConditional(strandsContext, conditional) { const cfg = strandsContext.cfg; const branches = conditional.branches; - const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); const results = []; const branchBlocks = []; const mergedAssignments = {}; const phiBlockDependencies = {}; - // Create a BRANCH block to handle phi node declarations const branchBlock = CFG.createBasicBlock(cfg, BlockType.BRANCH); CFG.addEdge(cfg, cfg.currentBlock, branchBlock); CFG.addEdge(cfg, branchBlock, mergeBlock); - let previousBlock = branchBlock; - for (let i = 0; i < branches.length; i++) { const { condition, branchCallback, blockType } = branches[i]; - if (condition !== null) { const conditionBlock = CFG.createBasicBlock(cfg, BlockType.IF_COND); CFG.addEdge(cfg, previousBlock, conditionBlock); @@ -69,20 +57,15 @@ function buildConditional(strandsContext, conditional) { previousBlock = conditionBlock; CFG.popBlock(cfg); } else { - // This is an else branch - create an ELSE_COND block const elseCondBlock = CFG.createBasicBlock(cfg, BlockType.ELSE_COND); CFG.addEdge(cfg, previousBlock, elseCondBlock); previousBlock = elseCondBlock; } - - // Create SCOPE_START block to mark beginning of branch scope const scopeStartBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_START); CFG.addEdge(cfg, previousBlock, scopeStartBlock); - const branchBlock = CFG.createBasicBlock(cfg, blockType); CFG.addEdge(cfg, scopeStartBlock, branchBlock); branchBlocks.push(branchBlock); - CFG.pushBlock(cfg, branchBlock); const branchResults = branchCallback(); for (const key in branchResults) { @@ -93,8 +76,6 @@ function buildConditional(strandsContext, conditional) { } } results.push(branchResults); - - // Create SCOPE_END block to mark end of branch scope const scopeEndBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_END); if (cfg.currentBlock !== branchBlock) { CFG.addEdge(cfg, cfg.currentBlock, scopeEndBlock); @@ -103,33 +84,24 @@ function buildConditional(strandsContext, conditional) { CFG.addEdge(cfg, branchBlock, scopeEndBlock); CFG.popBlock(cfg); } - CFG.addEdge(cfg, scopeEndBlock, mergeBlock); - previousBlock = scopeStartBlock; // Next condition should branch from the same point + previousBlock = scopeStartBlock; } - - // Push the branch block for modification to attach phi nodes there + // Push the branch block for modification to avoid changing the ordering CFG.pushBlockForModification(cfg, branchBlock); - for (const key in phiBlockDependencies) { mergedAssignments[key] = createPhiNode(strandsContext, phiBlockDependencies[key], key); } - CFG.popBlock(cfg); - - // Now add phi assignments to each branch block for (let i = 0; i < results.length; i++) { const branchResult = results[i]; const branchBlockID = branchBlocks[i]; - CFG.pushBlockForModification(cfg, branchBlockID); - for (const key in branchResult) { if (mergedAssignments[key]) { // Create an assignment statement: phiNode = branchResult[key] const phiNodeID = mergedAssignments[key].id; const sourceNodeID = branchResult[key].id; - // Create an assignment operation node // Use dependsOn[0] for phiNodeID and dependsOn[1] for sourceNodeID // This represents: dependsOn[0] = dependsOn[1] (phiNode = sourceNode) @@ -138,35 +110,25 @@ function buildConditional(strandsContext, conditional) { dependsOn: [phiNodeID, sourceNodeID], phiBlocks: [] }; - const assignmentID = DAG.getOrCreateNode(strandsContext.dag, assignmentNode); CFG.recordInBasicBlock(cfg, branchBlockID, assignmentID); } } - CFG.popBlock(cfg); } - CFG.pushBlock(cfg, mergeBlock); - return mergedAssignments; } - function createPhiNode(strandsContext, phiInputs, varName) { - console.log('createPhiNode called with varName:', varName, 'phiInputs:', phiInputs); - - // For now, create a simple phi node - // We'll need to determine the proper dimension and baseType from the inputs + // Determine the proper dimension and baseType from the inputs const validInputs = phiInputs.filter(input => input.value.id !== null); if (validInputs.length === 0) { throw new Error(`No valid inputs for phi node for variable ${varName}`); } - // Get dimension and baseType from first valid input const firstInput = DAG.getNodeDataFromID(strandsContext.dag, validInputs[0].value.id); const dimension = firstInput.dimension; const baseType = firstInput.baseType; - const nodeData = { nodeType: 'phi', dimension, @@ -175,10 +137,8 @@ function createPhiNode(strandsContext, phiInputs, varName) { phiBlocks: phiInputs.map(input => input.blockId), phiInputs // Store the full phi input information }; - const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); - return { id, dimension, diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index 3f9967fe10..f35a1cdc37 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -1,7 +1,6 @@ import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType } from "./ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag"; import * as FES from './strands_FES' - function shouldCreateTemp(dag, nodeID) { const nodeType = dag.nodeTypes[nodeID]; if (nodeType !== NodeType.OPERATION) return false; @@ -9,7 +8,6 @@ function shouldCreateTemp(dag, nodeID) { const uses = dag.usedBy[nodeID] || []; return uses.length > 1; } - const TypeNames = { 'float1': 'float', 'float2': 'vec2', @@ -27,11 +25,9 @@ const TypeNames = { 'mat3': 'mat3x3', 'mat4': 'mat4x4', } - const cfgHandlers = { [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { const { dag, cfg } = strandsContext; - const instructions = cfg.blockInstructions[blockID] || []; for (const nodeID of instructions) { const nodeType = dag.nodeTypes[nodeID]; @@ -47,135 +43,69 @@ const cfgHandlers = { } } }, - [BlockType.BRANCH](blockID, strandsContext, generationContext) { - console.log(`Processing BRANCH block ${blockID}`); const { dag, cfg } = strandsContext; - // Find all phi nodes in this branch block and declare them const blockInstructions = cfg.blockInstructions[blockID] || []; - console.log(`Instructions in branch block ${blockID}:`, blockInstructions); - for (const nodeID of blockInstructions) { const node = getNodeDataFromID(dag, nodeID); - console.log(`Checking node ${nodeID} with nodeType: ${node.nodeType}`); - if (node.nodeType === NodeType.PHI) { - // Create a temporary variable for this phi node const tmp = `T${generationContext.nextTempID++}`; generationContext.tempNames[nodeID] = tmp; - - console.log(`Declared phi temp variable: ${tmp} for node ${nodeID}`); - const T = extractNodeTypeInfo(dag, nodeID); const typeName = glslBackend.getTypeName(T.baseType, T.dimension); - - // Declare the temporary variable generationContext.write(`${typeName} ${tmp};`); } } - - // Execute the default block handling for any remaining instructions in this block this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, - [BlockType.IF_COND](blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; const conditionID = cfg.blockConditions[blockID]; const condExpr = glslBackend.generateExpression(generationContext, dag, conditionID); - generationContext.write(`if (${condExpr})`); this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, - [BlockType.ELSE_COND](blockID, strandsContext, generationContext) { generationContext.write(`else`); this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, - [BlockType.IF_BODY](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); - // Assign values to phi nodes that this branch feeds into this.assignPhiNodeValues(blockID, strandsContext, generationContext); }, - - [BlockType.ELSE_BODY](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); - // Assign values to phi nodes that this branch feeds into this.assignPhiNodeValues(blockID, strandsContext, generationContext); }, - [BlockType.SCOPE_START](blockID, strandsContext, generationContext) { generationContext.write(`{`); generationContext.indent++; }, - [BlockType.SCOPE_END](blockID, strandsContext, generationContext) { generationContext.indent--; generationContext.write(`}`); }, - [BlockType.MERGE](blockID, strandsContext, generationContext) { - return this[BlockType.DEFAULT](blockID, strandsContext, generationContext); - const { dag, cfg } = strandsContext; - - // Handle phi nodes specially in merge blocks - const instructions = cfg.blockInstructions[blockID] || []; - for (const nodeID of instructions) { - const node = getNodeDataFromID(dag, nodeID); - - if (node.nodeType !== NodeType.PHI) { - debugger - console.log(`Handling node in merge block`) - // Handle non-phi nodes normally - const nodeType = dag.nodeTypes[nodeID]; - if (shouldCreateTemp(dag, nodeID)) { - const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID); - generationContext.write(declaration); - } - if (nodeType === NodeType.STATEMENT) { - glslBackend.generateStatement(generationContext, dag, nodeID); - } - } - } + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, - [BlockType.FUNCTION](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, - assignPhiNodeValues(blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; - - console.log(`assignPhiNodeValues called for blockID: ${blockID}`); - // Find all phi nodes that this block feeds into const successors = cfg.outgoingEdges[blockID] || []; - console.log(`Successors for block ${blockID}:`, successors); - for (const successorBlockID of successors) { const instructions = cfg.blockInstructions[successorBlockID] || []; - console.log(`Instructions in successor block ${successorBlockID}:`, instructions); - for (const nodeID of instructions) { const node = getNodeDataFromID(dag, nodeID); - console.log(`Checking node ${nodeID} with nodeType: ${node.nodeType}`); - if (node.nodeType === NodeType.PHI) { - console.log(`Found phi node ${nodeID} with phiBlocks:`, node.phiBlocks, 'dependsOn:', node.dependsOn); - // Find which input of this phi node corresponds to our block - // The phiBlocks array maps to the dependsOn array const branchIndex = node.phiBlocks?.indexOf(blockID); - console.log(`branchIndex for block ${blockID}:`, branchIndex); - if (branchIndex !== -1 && branchIndex < node.dependsOn.length) { const sourceNodeID = node.dependsOn[branchIndex]; const tempName = generationContext.tempNames[nodeID]; - - console.log(`Assigning phi node: ${tempName} = source ${sourceNodeID}`); - if (tempName && sourceNodeID !== null) { const sourceExpr = glslBackend.generateExpression(generationContext, dag, sourceNodeID); generationContext.write(`${tempName} = ${sourceExpr};`); @@ -185,43 +115,7 @@ const cfgHandlers = { } } }, - - declarePhiNodesForConditional(blockID, strandsContext, generationContext) { - const { dag, cfg } = strandsContext; - - console.log(`declarePhiNodesForConditional called for blockID: ${blockID}`); - - // Find all phi nodes in the merge blocks that this conditional feeds into - const successors = cfg.outgoingEdges[blockID] || []; - console.log(`Successors for conditional block ${blockID}:`, successors); - - for (const successorBlockID of successors) { - const blockInstructions = cfg.blockInstructions[successorBlockID] || []; - console.log(`Instructions in merge block ${successorBlockID}:`, blockInstructions); - - for (const nodeID of blockInstructions) { - const node = getNodeDataFromID(dag, nodeID); - console.log(`Checking node ${nodeID} with nodeType: ${node.nodeType}`); - - if (node.nodeType === NodeType.PHI) { - // Create a temporary variable for this phi node - const tmp = `T${generationContext.nextTempID++}`; - generationContext.tempNames[nodeID] = tmp; - - console.log(`Declared phi temp variable: ${tmp} for node ${nodeID}`); - - const T = extractNodeTypeInfo(dag, nodeID); - const typeName = glslBackend.getTypeName(T.baseType, T.dimension); - - // Declare the temporary variable - generationContext.write(`${typeName} ${tmp};`); - } - } - } - } } - - export const glslBackend = { hookEntry(hookType) { const firstLine = `(${hookType.parameters.flatMap((param) => { @@ -229,7 +123,6 @@ export const glslBackend = { }).join(', ')}) {`; return firstLine; }, - getTypeName(baseType, dimension) { const primitiveTypeName = TypeNames[baseType + dimension] if (!primitiveTypeName) { @@ -237,42 +130,34 @@ export const glslBackend = { } return primitiveTypeName; }, - generateUniformDeclaration(name, typeInfo) { return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`; }, - generateStatement(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); if (node.statementType === OpCode.ControlFlow.DISCARD) { generationContext.write('discard;'); } }, - generateAssignment(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); // dependsOn[0] = phiNodeID, dependsOn[1] = sourceNodeID const phiNodeID = node.dependsOn[0]; const sourceNodeID = node.dependsOn[1]; - const phiTempName = generationContext.tempNames[phiNodeID]; const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); - if (phiTempName && sourceExpr) { generationContext.write(`${phiTempName} = ${sourceExpr};`); } }, - generateDeclaration(generationContext, dag, nodeID) { const expr = this.generateExpression(generationContext, dag, nodeID); const tmp = `T${generationContext.nextTempID++}`; generationContext.tempNames[nodeID] = tmp; - const T = extractNodeTypeInfo(dag, nodeID); const typeName = this.getTypeName(T.baseType, T.dimension); return `${typeName} ${tmp} = ${expr};`; }, - generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) { const dag = strandsContext.dag; const rootNode = getNodeDataFromID(dag, rootNodeID); @@ -290,7 +175,6 @@ export const glslBackend = { } generationContext.write(`return ${this.generateExpression(generationContext, dag, rootNodeID)};`); }, - generateExpression(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); if (generationContext.tempNames?.[nodeID]) { @@ -304,10 +188,8 @@ export const glslBackend = { else { return node.value; } - case NodeType.VARIABLE: return node.identifier; - case NodeType.OPERATION: const useParantheses = node.usedBy.length > 0; if (node.opCode === OpCode.Nary.CONSTRUCTOR) { @@ -380,15 +262,12 @@ export const glslBackend = { } } } - case NodeType.ASSIGNMENT: FES.internalError(`ASSIGNMENT nodes should not be used as expressions`) - default: FES.internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`) } }, - generateBlock(blockID, strandsContext, generationContext) { const type = strandsContext.cfg.blockTypes[blockID]; const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT]; diff --git a/src/strands/strands_node.js b/src/strands/strands_node.js index ace776ff72..c706fae783 100644 --- a/src/strands/strands_node.js +++ b/src/strands/strands_node.js @@ -1,17 +1,14 @@ import { swizzleTrap } from './ir_builders'; - export class StrandsNode { constructor(id, dimension, strandsContext) { this.id = id; this.strandsContext = strandsContext; this.dimension = dimension; } - copy() { return createStrandsNode(this.id, this.dimension, this.strandsContext); } } - export function createStrandsNode(id, dimension, strandsContext, onRebind) { return new Proxy( new StrandsNode(id, dimension, strandsContext), diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index c86cf7ab9c..da0bdee4af 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -2,9 +2,7 @@ import { parse } from 'acorn'; import { ancestor, recursive } from 'acorn-walk'; import escodegen from 'escodegen'; import { UnarySymbolToName } from './ir_types'; - let blockVarCounter = 0; - function replaceBinaryOperator(codeSource) { switch (codeSource) { case '+': return 'add'; @@ -21,7 +19,6 @@ function replaceBinaryOperator(codeSource) { case '||': return 'or'; } } - function nodeIsUniform(ancestor) { return ancestor.type === 'CallExpression' && ( @@ -36,12 +33,9 @@ function nodeIsUniform(ancestor) { ) ); } - const ASTCallbacks = { UnaryExpression(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } - - const unaryFnName = UnarySymbolToName[node.operator]; const standardReplacement = (node) => { node.type = 'CallExpression' @@ -51,7 +45,6 @@ const ASTCallbacks = { } node.arguments = [node.argument] } - if (node.type === 'MemberExpression') { const property = node.argument.property.name; const swizzleSets = [ @@ -59,11 +52,9 @@ const ASTCallbacks = { ['r', 'g', 'b', 'a'], ['s', 't', 'p', 'q'] ]; - let isSwizzle = swizzleSets.some(set => [...property].every(char => set.includes(char)) ) && node.argument.type === 'MemberExpression'; - if (isSwizzle) { node.type = 'MemberExpression'; node.object = { @@ -210,11 +201,9 @@ const ASTCallbacks = { }, IfStatement(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } - // Transform if statement into strandsIf() call // The condition is evaluated directly, not wrapped in a function const condition = node.test; - // Create the then function const thenFunction = { type: 'ArrowFunctionExpression', @@ -224,7 +213,6 @@ const ASTCallbacks = { body: [node.consequent] } }; - // Start building the call chain: __p5.strandsIf(condition, then) let callExpression = { type: 'CallExpression', @@ -234,7 +222,6 @@ const ASTCallbacks = { }, arguments: [condition, thenFunction] }; - // Always chain .Else() even if there's no explicit else clause // This ensures the conditional completes and returns phi nodes let elseFunction; @@ -258,7 +245,6 @@ const ASTCallbacks = { } }; } - callExpression = { type: 'CallExpression', callee: { @@ -271,13 +257,10 @@ const ASTCallbacks = { }, arguments: [elseFunction] }; - // Analyze which outer scope variables are assigned in any branch const assignedVars = new Set(); - const analyzeBlock = (body) => { if (body.type !== 'BlockStatement') return; - // First pass: collect variable declarations within this block const localVars = new Set(); for (const stmt of body.body) { @@ -289,13 +272,11 @@ const ASTCallbacks = { } } } - // Second pass: find assignments to non-local variables for (const stmt of body.body) { if (stmt.type === 'ExpressionStatement' && stmt.expression.type === 'AssignmentExpression') { const left = stmt.expression.left; - if (left.type === 'Identifier') { // Direct variable assignment: x = value if (!localVars.has(left.name)) { @@ -311,11 +292,9 @@ const ASTCallbacks = { } } }; - // Analyze all branches for assignments to outer scope variables analyzeBlock(thenFunction.body); analyzeBlock(elseFunction.body); - if (assignedVars.size > 0) { // Add copying, reference replacement, and return statements to branch functions const addCopyingAndReturn = (functionBody, varsToReturn) => { @@ -323,11 +302,9 @@ const ASTCallbacks = { // Create temporary variables and copy statements const tempVarMap = new Map(); // original name -> temp name const copyStatements = []; - for (const varName of varsToReturn) { const tempName = `__copy_${varName}_${blockVarCounter++}`; tempVarMap.set(varName, tempName); - // let tempName = originalVar.copy() copyStatements.push({ type: 'VariableDeclaration', @@ -348,11 +325,9 @@ const ASTCallbacks = { kind: 'let' }); } - // Replace all references to original variables with temp variables const replaceReferences = (node) => { if (!node || typeof node !== 'object') return; - if (node.type === 'Identifier' && tempVarMap.has(node.name)) { node.name = tempVarMap.get(node.name); } else if (node.type === 'MemberExpression' && @@ -360,7 +335,6 @@ const ASTCallbacks = { tempVarMap.has(node.object.name)) { node.object.name = tempVarMap.get(node.object.name); } - // Recursively process all properties for (const key in node) { if (node.hasOwnProperty(key) && key !== 'parent') { @@ -372,13 +346,10 @@ const ASTCallbacks = { } } }; - // Apply reference replacement to all statements functionBody.body.forEach(replaceReferences); - // Insert copy statements at the beginning functionBody.body.unshift(...copyStatements); - // Add return statement with temp variable names const returnObj = { type: 'ObjectExpression', @@ -391,25 +362,20 @@ const ASTCallbacks = { shorthand: false })) }; - functionBody.body.push({ type: 'ReturnStatement', argument: returnObj }); } }; - addCopyingAndReturn(thenFunction.body, assignedVars); addCopyingAndReturn(elseFunction.body, assignedVars); - // Create a block variable to capture the return value const blockVar = `__block_${blockVarCounter++}`; - // Replace with a block statement containing: // 1. The conditional call assigned to block variable // 2. Assignments from block variable back to original variables const statements = []; - // 1. const blockVar = strandsIf().Else() statements.push({ type: 'VariableDeclaration', @@ -420,7 +386,6 @@ const ASTCallbacks = { }], kind: 'const' }); - // 2. Assignments for each modified variable for (const varName of assignedVars) { statements.push({ @@ -438,7 +403,6 @@ const ASTCallbacks = { } }); } - // Replace the if statement with a block statement node.type = 'BlockStatement'; node.body = statements; @@ -447,24 +411,20 @@ const ASTCallbacks = { node.type = 'ExpressionStatement'; node.expression = callExpression; } - delete node.test; delete node.consequent; delete node.alternate; }, } - export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { const ast = parse(sourceString, { ecmaVersion: 2021, locations: srcLocations }); - // First pass: transform everything except if statements using normal ancestor traversal const nonIfCallbacks = { ...ASTCallbacks }; delete nonIfCallbacks.IfStatement; ancestor(ast, nonIfCallbacks, undefined, { varyings: {} }); - // Second pass: transform if statements in post-order using recursive traversal const postOrderIfTransform = { IfStatement(node, state, c) { @@ -472,14 +432,11 @@ const ASTCallbacks = { if (node.test) c(node.test, state); if (node.consequent) c(node.consequent, state); if (node.alternate) c(node.alternate, state); - // Then apply the transformation to this node ASTCallbacks.IfStatement(node, state, []); } }; - recursive(ast, { varyings: {} }, postOrderIfTransform); - const transpiledSource = escodegen.generate(ast); const scopeKeys = Object.keys(scope); const internalStrandsCallback = new Function( @@ -498,4 +455,3 @@ const ASTCallbacks = { console.log(internalStrandsCallback.toString()) return () => internalStrandsCallback(p5, ...scopeKeys.map(key => scope[key])); } - diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index a14c40c10b..19b23145ef 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -1,8 +1,6 @@ import p5 from '../../../src/app.js'; - suite('p5.Shader', function() { var myp5; - beforeAll(function() { myp5 = new p5(function(p) { p.setup = function() { @@ -12,12 +10,10 @@ suite('p5.Shader', function() { }; }); }); - var testUniforms = function(shaderName, uniforms, expectedUniforms) { // assert(expectedUniforms.length === Object.keys(uniforms).length, // shaderName + ' expected ' + expectedUniforms.length + ' uniforms but has ' + // Object.keys(uniforms).length); - // test each one for (var i = 0; i < expectedUniforms.length; i++) { var uniform = uniforms[expectedUniforms[i]]; @@ -27,12 +23,10 @@ suite('p5.Shader', function() { ); } }; - var testAttributes = function(shaderName, attributes, expectedAttributes) { // assert(expectedAttributes.length === Object.keys(attributes).length, // shaderName + ' expected ' + expectedAttributes.length + // ' attributes but has ' + Object.keys(attributes).length); - // test each one for (var i = 0; i < expectedAttributes.length; i++) { var attribute = attributes[expectedAttributes[i]]; @@ -42,7 +36,6 @@ suite('p5.Shader', function() { ); } }; - var testShader = function( shaderName, shaderObj, @@ -54,15 +47,12 @@ suite('p5.Shader', function() { testUniforms(shaderName, shaderObj.uniforms, expectedUniforms); shaderObj.unbindShader(); }; - afterAll(function() { myp5.remove(); }); - suite('Shader', function() { test('Light Shader', function() { var expectedAttributes = ['aPosition', 'aNormal', 'aTexCoord']; - var expectedUniforms = [ 'uModelViewMatrix', 'uProjectionMatrix', @@ -93,7 +83,6 @@ suite('p5.Shader', function() { 'uLinearAttenuation', 'uQuadraticAttenuation' ]; - testShader( 'Light Shader', myp5._renderer._getLightShader(), @@ -103,13 +92,11 @@ suite('p5.Shader', function() { }); test('Color Shader definition', function() { var expectedAttributes = ['aPosition']; - var expectedUniforms = [ 'uModelViewMatrix', 'uProjectionMatrix', 'uMaterialColor' ]; - testShader( 'Color Shader', myp5._renderer._getColorShader(), @@ -119,12 +106,10 @@ suite('p5.Shader', function() { }); test('Immediate Mode Shader definition', function() { var expectedAttributes = ['aPosition', 'aVertexColor']; - var expectedUniforms = [ 'uModelViewMatrix', 'uProjectionMatrix' ]; - testShader( 'Immediate Mode Shader', myp5._renderer._getColorShader(), @@ -134,13 +119,11 @@ suite('p5.Shader', function() { }); test('Normal Shader definition', function() { var expectedAttributes = ['aPosition', 'aNormal']; - var expectedUniforms = [ 'uModelViewMatrix', 'uProjectionMatrix', 'uNormalMatrix' ]; - testShader( 'Normal Shader', myp5._renderer._getNormalShader(), @@ -155,7 +138,6 @@ suite('p5.Shader', function() { var immediateColorShader = myp5._renderer._getColorShader(); var selectedRetainedShader = myp5._renderer._getFillShader(); var selectedImmediateShader = myp5._renderer._getFillShader(); - // both color and light shader are valid, depending on // conditions set earlier. assert( @@ -231,60 +213,47 @@ suite('p5.Shader', function() { 'after call to emissiveMaterial()' ); }); - test('Able to setUniform empty arrays', function() { myp5.shader(myp5._renderer._getLightShader()); var s = myp5._renderer.states.userFillShader; - s.setUniform('uMaterialColor', []); s.setUniform('uLightingDirection', []); }); - test('Able to set shininess', function() { assert.deepEqual(myp5._renderer.states._useShininess, 1); myp5.shininess(50); assert.deepEqual(myp5._renderer.states._useShininess, 50); }); - test('Shader is reset after resetShader is called', function() { myp5.shader(myp5._renderer._getColorShader()); var prevShader = myp5._renderer.states.userFillShader; assert.isTrue(prevShader !== null); - myp5.resetShader(); var curShader = myp5._renderer.states.userFillShader; assert.isTrue(curShader === null); }); - suite('Hooks', function() { let myShader; - beforeEach(function() { myShader = myp5.createShader( ` precision highp float; - attribute vec3 aPosition; attribute vec2 aTexCoord; attribute vec4 aVertexColor; - uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; - varying vec2 vTexCoord; varying vec4 vVertexColor; - void main() { // Apply the camera transform vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); - // Tell WebGL where the vertex goes gl_Position = uProjectionMatrix * viewModelPosition; - // Pass along data to the fragment shader vTexCoord = aTexCoord; vVertexColor = aVertexColor; @@ -292,10 +261,8 @@ suite('p5.Shader', function() { `, ` precision highp float; - varying vec2 vTexCoord; varying vec4 vVertexColor; - void main() { // Tell WebGL what color to make the pixel gl_FragColor = HOOK_getVertexColor(vVertexColor); @@ -308,7 +275,6 @@ suite('p5.Shader', function() { } ); }); - test('available hooks show up in inspectHooks()', function() { const logs = []; const myLog = (...data) => logs.push(data.join(', ')); @@ -318,17 +284,14 @@ suite('p5.Shader', function() { console.log = oldLog; expect(logs.join('\n')).to.match(/vec4 getVertexColor/); }); - test('unfilled hooks do not have an AUGMENTED_HOOK define', function() { const modified = myShader.modify({}); expect(modified.fragSrc()).not.to.match(/#define AUGMENTED_HOOK_getVertexColor/); }); - test('anonymous function shaderModifier does not throw when parsed', function() { const callModify = () => myShader.modify(function() {}); expect(callModify).not.toThrowError(); }); - test('filled hooks do have an AUGMENTED_HOOK define', function() { const modified = myShader.modify({ 'vec4 getVertexColor': `(vec4 c) { @@ -338,7 +301,6 @@ suite('p5.Shader', function() { expect(modified.fragSrc()).to.match(/#define AUGMENTED_HOOK_getVertexColor/); }); }); - test('framebuffer textures are unbound when you draw to the framebuffer', function() { const sh = myp5.baseMaterialShader().modify({ uniforms: { @@ -349,19 +311,15 @@ suite('p5.Shader', function() { }` }); const fbo = myp5.createFramebuffer(); - myp5.shader(sh); sh.setUniform('myTex', fbo); - fbo.draw(() => myp5.background('red')); - sh.setUniform('myTex', fbo); myp5.noStroke(); myp5.plane(myp5.width, myp5.height); assert.deepEqual(myp5.get(0, 0), [255, 0, 0, 255]); }); }); - suite('hookTypes', function() { test('Produces expected types on baseFilterShader()', function() { const types = myp5.baseFilterShader().hookTypes('vec4 getColor'); @@ -418,7 +376,6 @@ suite('p5.Shader', function() { }); }); }); - suite('p5.strands', () => { test('does not break when arrays are in uniform callbacks', () => { myp5.createCanvas(5, 5, myp5.WEBGL); @@ -438,212 +395,164 @@ suite('p5.Shader', function() { myp5.plane(myp5.width, myp5.height); }).not.toThrowError(); }); - suite('if statement conditionals', () => { test('handle simple if statement with true condition', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 1.0); // true condition - myp5.getPixelInputs(inputs => { let color = myp5.float(0.5); // initial gray - if (condition > 0.5) { color = myp5.float(1.0); // set to white in if branch } - inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - // Check that the center pixel is white (condition was true) const pixelColor = myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); - test('handle simple if statement with false condition', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 0.0); // false condition - myp5.getPixelInputs(inputs => { let color = myp5.float(0.5); // initial gray - if (condition > 0.5) { color = myp5.float(1.0); // set to white in if branch } - inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - // Check that the center pixel is gray (condition was false, original value kept) const pixelColor = myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); - test('handle if-else statement', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 0.0); // false condition - myp5.getPixelInputs(inputs => { let color = myp5.float(0.5); // initial gray - if (condition > 0.5) { color = myp5.float(1.0); // white for true } else { color = myp5.float(0.0); // black for false } - inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - // Check that the center pixel is black (else branch executed) const pixelColor = myp5.get(25, 25); assert.approximately(pixelColor[0], 0, 5); // Red channel should be ~0 (black) assert.approximately(pixelColor[1], 0, 5); // Green channel should be ~0 assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0 }); - test('handle multiple variable assignments in if statement', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 1.0); // true condition - myp5.getPixelInputs(inputs => { let red = myp5.float(0.0); let green = myp5.float(0.0); let blue = myp5.float(0.0); - if (condition > 0.5) { red = myp5.float(1.0); green = myp5.float(0.5); blue = myp5.float(0.0); } - inputs.color = [red, green, blue, 1.0]; return inputs; }); }, { myp5 }); - myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - // Check that the center pixel has the expected color (red=1.0, green=0.5, blue=0.0) const pixelColor = myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0 }); - test('handle modifications after if statement', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 1.0); // true condition - myp5.getPixelInputs(inputs => { let color = myp5.float(0.0); // start with black - if (condition > 0.5) { color = myp5.float(1.0); // set to white in if branch } else { color = myp5.float(0.5); // set to gray in else branch } - // Modify the color after the if statement color = color * 0.5; // Should result in 0.5 * 1.0 = 0.5 (gray) - inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - // Check that the center pixel is gray (white * 0.5 = gray) const pixelColor = myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); - test('handle modifications after if statement in both branches', () => { myp5.createCanvas(100, 50, myp5.WEBGL); - const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { debugger const uv = inputs.texCoord; const condition = uv.x > 0.5; // left half false, right half true let color = myp5.float(0.0); - if (condition) { color = myp5.float(1.0); // white on right side } else { color = myp5.float(0.8); // light gray on left side } - // Multiply by 0.5 after the if statement color = color * 0.5; // Right side: 1.0 * 0.5 = 0.5 (medium gray) // Left side: 0.8 * 0.5 = 0.4 (darker gray) - inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - // Check left side (false condition) const leftPixel = myp5.get(25, 25); assert.approximately(leftPixel[0], 102, 5); // 0.4 * 255 ≈ 102 - // Check right side (true condition) const rightPixel = myp5.get(75, 25); assert.approximately(rightPixel[0], 127, 5); // 0.5 * 255 ≈ 127 }); - test('handle if-else-if chains', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - const testShader = myp5.baseMaterialShader().modify(() => { const value = myp5.uniformFloat(() => 0.5); // middle value - myp5.getPixelInputs(inputs => { let color = myp5.float(0.0); - if (value > 0.8) { color = myp5.float(1.0); // white for high values } else if (value > 0.3) { @@ -651,33 +560,26 @@ suite('p5.Shader', function() { } else { color = myp5.float(0.0); // black for low values } - inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - // Check that the center pixel is gray (medium condition was true) const pixelColor = myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); - test('handle nested if statements', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - const testShader = myp5.baseMaterialShader().modify(() => { const outerCondition = myp5.uniformFloat(() => 1.0); // true const innerCondition = myp5.uniformFloat(() => 1.0); // true - myp5.getPixelInputs(inputs => { let color = myp5.float(0.0); - if (outerCondition > 0.5) { if (innerCondition > 0.5) { color = myp5.float(1.0); // white for both conditions true @@ -687,33 +589,26 @@ suite('p5.Shader', function() { } else { color = myp5.float(0.0); // black for outer false } - inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - // Check that the center pixel is white (both conditions were true) const pixelColor = myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); - // Keep one direct API test for completeness test('handle direct StrandsIf API usage', () => { myp5.createCanvas(50, 50, myp5.WEBGL); - const testShader = myp5.baseMaterialShader().modify(() => { const conditionValue = myp5.uniformFloat(() => 1.0); // true condition - myp5.getPixelInputs(inputs => { let color = myp5.float(0.5); // initial gray - const assignments = myp5.strandsIf( conditionValue.greaterThan(0), () => { @@ -724,18 +619,14 @@ suite('p5.Shader', function() { ).Else(() => { return { color: color }; // keep original in else branch }); - color = assignments.color; - inputs.color = [color, color, color, 1.0]; return inputs; }); }, { myp5 }); - myp5.noStroke(); myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - // Check that the center pixel is white (condition was true) const pixelColor = myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) From 2d6799648679d8de39abeea3e920d1cdb9629eb9 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 3 Oct 2025 11:57:33 -0400 Subject: [PATCH 05/22] Add for loop handling --- src/strands/ir_builders.js | 4 +- src/strands/ir_dag.js | 19 +- src/strands/ir_types.js | 6 +- src/strands/strands_api.js | 16 +- src/strands/strands_conditionals.js | 27 +- src/strands/strands_for.js | 426 ++++++++++++++++++++++++++ src/strands/strands_glslBackend.js | 67 ++++- src/strands/strands_phi_utils.js | 30 ++ src/strands/strands_transpiler.js | 447 ++++++++++++++++++++++++++-- test/unit/webgl/p5.Shader.js | 238 +++++++++++++++ 10 files changed, 1217 insertions(+), 63 deletions(-) create mode 100644 src/strands/strands_for.js create mode 100644 src/strands/strands_phi_utils.js diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 7c1605bd32..bd4700c372 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -377,11 +377,11 @@ export function functionCallNode( CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: inferredReturnType.dimension }; } -export function statementNode(strandsContext, opCode) { +export function statementNode(strandsContext, statementType) { const { dag, cfg } = strandsContext; const nodeData = DAG.createNodeData({ nodeType: NodeType.STATEMENT, - opCode + statementType }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); diff --git a/src/strands/ir_dag.js b/src/strands/ir_dag.js index 7633d534c6..45eadb473a 100644 --- a/src/strands/ir_dag.js +++ b/src/strands/ir_dag.js @@ -6,8 +6,8 @@ import * as FES from './strands_FES'; ///////////////////////////////// export function createDirectedAcyclicGraph() { - const graph = { - nextID: 0, + const graph = { + nextID: 0, cache: new Map(), nodeTypes: [], baseTypes: [], @@ -21,16 +21,16 @@ export function createDirectedAcyclicGraph() { statementTypes: [], swizzles: [], }; - + return graph; } export function getOrCreateNode(graph, node) { // const key = getNodeKey(node); // const existing = graph.cache.get(key); - + // if (existing !== undefined) { - // return existing; + // return existing; // } else { const id = createNode(graph, node); // graph.cache.set(key, id); @@ -66,7 +66,7 @@ export function getNodeDataFromID(graph, id) { dependsOn: graph.dependsOn[id], usedBy: graph.usedBy[id], phiBlocks: graph.phiBlocks[id], - dimension: graph.dimensions[id], + dimension: graph.dimensions[id], baseType: graph.baseTypes[id], statementType: graph.statementTypes[id], swizzle: graph.swizzles[id], @@ -115,7 +115,7 @@ function getNodeKey(node) { function validateNode(node){ const nodeType = node.nodeType; const requiredFields = NodeTypeRequiredFields[nodeType]; - if (requiredFields.length === 2) { + if (requiredFields.length === 2) { FES.internalError(`Required fields for node type '${NodeTypeToName[nodeType]}' not defined. Please add them to the utils.js file in p5.strands!`) } const missingFields = []; @@ -124,7 +124,10 @@ function validateNode(node){ missingFields.push(field); } } + if (node.dependsOn?.some(v => v === undefined)) { + throw new Error('Undefined dependency!'); + } if (missingFields.length > 0) { FES.internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`); } -} \ No newline at end of file +} diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 81bb4c0495..b9138dbf90 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -21,11 +21,14 @@ export const NodeTypeRequiredFields = { [NodeType.CONSTANT]: ["value", "dimension", "baseType"], [NodeType.STRUCT]: [""], [NodeType.PHI]: ["dependsOn", "phiBlocks", "dimension", "baseType"], - [NodeType.STATEMENT]: ["opCode"], + [NodeType.STATEMENT]: ["statementType"], [NodeType.ASSIGNMENT]: ["dependsOn"] }; export const StatementType = { DISCARD: 'discard', + BREAK: 'break', + EXPRESSION: 'expression', // Used when we want to output a single expression as a statement, e.g. a for loop condition + EMPTY: 'empty', // Used for empty statements like ; in for loops }; export const BaseType = { FLOAT: "float", @@ -129,6 +132,7 @@ export const OpCode = { JUMP: 301, BRANCH_IF_FALSE: 302, DISCARD: 303, + BREAK: 304, } }; export const OperatorTable = [ diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index dd613163df..47c29fa226 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -8,10 +8,12 @@ import { TypeInfoFromGLSLName, isStructType, OpCode, + StatementType, // isNativeType } from './ir_types' import { strandsBuiltinFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' +import { StrandsFor } from './strands_for' import * as CFG from './ir_cfg' import * as FES from './strands_FES' import { getNodeDataFromID } from './ir_dag' @@ -41,8 +43,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Unique Functions ////////////////////////////////////////////// fn.discard = function() { - build.statementNode(strandsContext, OpCode.ControlFlow.DISCARD); + build.statementNode(strandsContext, StatementType.DISCARD); } + fn.break = function() { + build.statementNode(strandsContext, StatementType.BREAK); + }; + p5.break = fn.break; fn.instanceID = function() { const node = build.variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, 'gl_InstanceID'); return createStrandsNode(node.id, node.dimension, strandsContext); @@ -53,10 +59,10 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return new StrandsConditional(strandsContext, conditionNode, ifBody); } fn.strandsIf = p5.strandsIf; - p5.strandsLoop = function(a, b, loopBody) { - return null; - } - fn.strandsLoop = p5.strandsLoop; + p5.strandsFor = function(initialCb, conditionCb, updateCb, bodyCb, initialVars) { + return new StrandsFor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars).build(); + }; + fn.strandsFor = p5.strandsFor; p5.strandsNode = function(...args) { if (args.length === 1 && args[0] instanceof StrandsNode) { return args[0]; diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index f672a65b24..c8dcd7b852 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -2,6 +2,7 @@ import * as CFG from './ir_cfg'; import * as DAG from './ir_dag'; import { BlockType, NodeType } from './ir_types'; import { StrandsNode, createStrandsNode } from './strands_node'; +import { createPhiNode } from './strands_phi_utils'; export class StrandsConditional { constructor(strandsContext, condition, branchCallback) { // Condition must be a node... @@ -119,29 +120,3 @@ function buildConditional(strandsContext, conditional) { CFG.pushBlock(cfg, mergeBlock); return mergedAssignments; } -function createPhiNode(strandsContext, phiInputs, varName) { - // Determine the proper dimension and baseType from the inputs - const validInputs = phiInputs.filter(input => input.value.id !== null); - if (validInputs.length === 0) { - throw new Error(`No valid inputs for phi node for variable ${varName}`); - } - // Get dimension and baseType from first valid input - const firstInput = DAG.getNodeDataFromID(strandsContext.dag, validInputs[0].value.id); - const dimension = firstInput.dimension; - const baseType = firstInput.baseType; - const nodeData = { - nodeType: 'phi', - dimension, - baseType, - dependsOn: phiInputs.map(input => input.value.id).filter(id => id !== null), - phiBlocks: phiInputs.map(input => input.blockId), - phiInputs // Store the full phi input information - }; - const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); - CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); - return { - id, - dimension, - baseType - }; -} diff --git a/src/strands/strands_for.js b/src/strands/strands_for.js new file mode 100644 index 0000000000..a9f0749c44 --- /dev/null +++ b/src/strands/strands_for.js @@ -0,0 +1,426 @@ +import * as CFG from './ir_cfg'; +import * as DAG from './ir_dag'; +import { BlockType, NodeType, BaseType, StatementType, OpCode } from './ir_types'; +import { StrandsNode, createStrandsNode } from './strands_node'; +import { primitiveConstructorNode } from './ir_builders'; +import { createPhiNode } from './strands_phi_utils'; + +export class StrandsFor { + constructor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars) { + this.strandsContext = strandsContext; + this.initialCb = initialCb; + this.conditionCb = conditionCb; + this.updateCb = updateCb; + this.bodyCb = bodyCb; + this.initialVars = initialVars; + } + + build() { + const cfg = this.strandsContext.cfg; + const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); + + // Create a BRANCH block to handle phi node declarations + const branchBlock = CFG.createBasicBlock(cfg, BlockType.BRANCH); + CFG.addEdge(cfg, cfg.currentBlock, branchBlock); + CFG.addEdge(cfg, branchBlock, mergeBlock); + + // Initialize loop variable phi node + const { initialVar, phiNode } = this.initializeLoopVariable(cfg, branchBlock); + + // Execute condition and update callbacks to get nodes for analysis + CFG.pushBlock(cfg, cfg.currentBlock); + const loopVarNode = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext); + const conditionNode = this.conditionCb(loopVarNode); + const updateResult = this.updateCb(loopVarNode); + CFG.popBlock(cfg); + + // Check if loop has bounded iteration count + const isBounded = this.loopIsBounded(initialVar, conditionNode, updateResult); + + if (isBounded) { + this.buildBoundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult); + } else { + this.buildUnboundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult); + } + + // Update the phi nodes created in buildBoundedLoop with actual body results + const finalPhiNodes = this.phiNodesForBody; + CFG.pushBlockForModification(cfg, branchBlock); + for (const [varName, resultNode] of Object.entries(this.bodyResults)) { + if (varName !== 'loopVar' && finalPhiNodes[varName]) { + // Update the phi node's second input to use the actual body result + const phiNodeID = finalPhiNodes[varName].id; + const phiNodeData = DAG.getNodeDataFromID(this.strandsContext.dag, phiNodeID); + // Update the dependsOn array to include the actual body result + if (phiNodeData.dependsOn.length > 1) { + phiNodeData.dependsOn[1] = resultNode.id; + } + if (phiNodeData.phiInputs && phiNodeData.phiInputs.length > 1) { + phiNodeData.phiInputs[1].value = resultNode; + } + } + } + CFG.popBlock(cfg); + + // Create assignment nodes in the branch block for initial values + CFG.pushBlockForModification(cfg, branchBlock); + for (const [varName, initialValueNode] of Object.entries(this.initialVars)) { + if (varName !== 'loopVar' && finalPhiNodes[varName]) { + // Create an assignment statement: phiNode = initialValue + const phiNodeID = finalPhiNodes[varName].id; + const sourceNodeID = initialValueNode.id; + // Create an assignment operation node for the initial value + const assignmentNode = DAG.createNodeData({ + nodeType: NodeType.ASSIGNMENT, + dependsOn: [phiNodeID, sourceNodeID], + phiBlocks: [] + }); + const assignmentID = DAG.getOrCreateNode(this.strandsContext.dag, assignmentNode); + CFG.recordInBasicBlock(cfg, branchBlock, assignmentID); + } + } + CFG.popBlock(cfg); + + // Create assignment nodes in the final block after body execution (following conditionals pattern) + // After executing the body callback, cfg.currentBlock should be the final block in the control flow + CFG.pushBlockForModification(cfg, this.finalBodyBlock); + for (const [varName, resultNode] of Object.entries(this.bodyResults)) { + if (varName !== 'loopVar' && finalPhiNodes[varName]) { + // Create an assignment statement: phiNode = bodyResult[varName] + const phiNodeID = finalPhiNodes[varName].id; + const sourceNodeID = resultNode.id; + // Create an assignment operation node + // Use dependsOn[0] for phiNodeID and dependsOn[1] for sourceNodeID + // This represents: dependsOn[0] = dependsOn[1] (phiNode = sourceNode) + const assignmentNode = DAG.createNodeData({ + nodeType: NodeType.ASSIGNMENT, + dependsOn: [phiNodeID, sourceNodeID], + phiBlocks: [] + }); + const assignmentID = DAG.getOrCreateNode(this.strandsContext.dag, assignmentNode); + CFG.recordInBasicBlock(cfg, this.finalBodyBlock, assignmentID); + } + } + CFG.popBlock(cfg); + + // Convert phi nodes to StrandsNodes for the final result + const finalBodyResults = {}; + for (const [varName, phiNode] of Object.entries(finalPhiNodes)) { + finalBodyResults[varName] = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext); + } + + CFG.pushBlock(cfg, mergeBlock); + + return finalBodyResults; + } + + buildBoundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult) { + // For bounded loops, create FOR block with three statements: init, condition, update + const forBlock = CFG.createBasicBlock(cfg, BlockType.FOR); + CFG.addEdge(cfg, branchBlock, forBlock); + + // Now add only the specific nodes we need to the FOR block + CFG.pushBlock(cfg, forBlock); + + // 1. Init statement - assign initial value to phi node (or empty if no initializer) + if (initialVar) { + const initAssignmentNode = DAG.createNodeData({ + nodeType: NodeType.ASSIGNMENT, + dependsOn: [phiNode.id, initialVar.id], + phiBlocks: [] + }); + const initAssignmentID = DAG.getOrCreateNode(this.strandsContext.dag, initAssignmentNode); + CFG.recordInBasicBlock(cfg, forBlock, initAssignmentID); + } + + // 2. Condition statement - wrap in ExpressionStatement to force generation + const conditionStatementNode = DAG.createNodeData({ + nodeType: NodeType.STATEMENT, + statementType: StatementType.EXPRESSION, + dependsOn: [conditionNode.id], + phiBlocks: [] + }); + const conditionStatementID = DAG.getOrCreateNode(this.strandsContext.dag, conditionStatementNode); + CFG.recordInBasicBlock(cfg, forBlock, conditionStatementID); + + // 3. Update statement - create assignment of update result to phi node + const updateAssignmentNode = DAG.createNodeData({ + nodeType: NodeType.ASSIGNMENT, + dependsOn: [phiNode.id, updateResult.id], + phiBlocks: [] + }); + const updateAssignmentID = DAG.getOrCreateNode(this.strandsContext.dag, updateAssignmentNode); + CFG.recordInBasicBlock(cfg, forBlock, updateAssignmentID); + + CFG.popBlock(cfg); + + // Verify we have the right number of statements (2 or 3 depending on initializer) + const instructions = cfg.blockInstructions[forBlock] || []; + const expectedLength = initialVar ? 3 : 2; + if (instructions.length !== expectedLength) { + throw new Error(`FOR block must have exactly ${expectedLength} statements, got ${instructions.length}`); + } + + const scopeStartBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_START); + CFG.addEdge(cfg, forBlock, scopeStartBlock); + + const bodyBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT); + this.bodyBlock = bodyBlock; + CFG.addEdge(cfg, scopeStartBlock, bodyBlock); + + this.executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode); + + const scopeEndBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_END); + CFG.addEdge(cfg, bodyBlock, scopeEndBlock); + CFG.addEdge(cfg, scopeEndBlock, mergeBlock); + } + + buildUnboundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult) { + // For unbounded loops, create FOR block with infinite loop and break condition + const forBlock = CFG.createBasicBlock(cfg, BlockType.FOR); + CFG.addEdge(cfg, branchBlock, forBlock); + + // Create FOR block with three empty statements for for(;;) syntax + CFG.pushBlock(cfg, forBlock); + + // 1. Init statement - initialize loop variable or empty + if (initialVar) { + const initAssignmentNode = DAG.createNodeData({ + nodeType: NodeType.ASSIGNMENT, + dependsOn: [phiNode.id, initialVar.id], + phiBlocks: [] + }); + const initAssignmentID = DAG.getOrCreateNode(this.strandsContext.dag, initAssignmentNode); + CFG.recordInBasicBlock(cfg, forBlock, initAssignmentID); + } else { + // Create empty statement for init + const emptyInitNode = DAG.createNodeData({ + nodeType: NodeType.STATEMENT, + statementType: StatementType.EMPTY, + dependsOn: [], + phiBlocks: [] + }); + const emptyInitID = DAG.getOrCreateNode(this.strandsContext.dag, emptyInitNode); + CFG.recordInBasicBlock(cfg, forBlock, emptyInitID); + } + + // 2. Condition statement - empty for infinite loop + const emptyConditionNode = DAG.createNodeData({ + nodeType: NodeType.STATEMENT, + statementType: StatementType.EMPTY, + dependsOn: [], + phiBlocks: [] + }); + const emptyConditionID = DAG.getOrCreateNode(this.strandsContext.dag, emptyConditionNode); + CFG.recordInBasicBlock(cfg, forBlock, emptyConditionID); + + // 3. Update statement - empty for infinite loop + const emptyUpdateNode = DAG.createNodeData({ + nodeType: NodeType.STATEMENT, + statementType: StatementType.EMPTY, + dependsOn: [], + phiBlocks: [] + }); + const emptyUpdateID = DAG.getOrCreateNode(this.strandsContext.dag, emptyUpdateNode); + CFG.recordInBasicBlock(cfg, forBlock, emptyUpdateID); + + CFG.popBlock(cfg); + + const scopeStartBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_START); + CFG.addEdge(cfg, forBlock, scopeStartBlock); + + // Add break condition check right after scope start + const breakCheckBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT); + CFG.addEdge(cfg, scopeStartBlock, breakCheckBlock); + + CFG.pushBlock(cfg, breakCheckBlock); + + // Generate break statement: if (!condition) break; + // First, create the logical NOT of the condition: !condition + const condition = conditionNode; + const negatedCondition = this.createLogicalNotNode(condition); + + // Create a conditional break using the existing conditional structure + // We'll create an IF_COND block that leads to a break statement + const breakConditionBlock = CFG.createBasicBlock(cfg, BlockType.IF_COND); + CFG.addEdge(cfg, breakCheckBlock, breakConditionBlock); + cfg.blockConditions[breakConditionBlock] = negatedCondition.id; + + const breakStatementBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT); + CFG.addEdge(cfg, breakConditionBlock, breakStatementBlock); + + // Create the break statement in the break statement block + CFG.pushBlock(cfg, breakStatementBlock); + const breakStatementNode = DAG.createNodeData({ + nodeType: NodeType.STATEMENT, + statementType: StatementType.BREAK, + dependsOn: [], + phiBlocks: [] + }); + const breakStatementID = DAG.getOrCreateNode(this.strandsContext.dag, breakStatementNode); + CFG.recordInBasicBlock(cfg, breakStatementBlock, breakStatementID); + CFG.popBlock(cfg); + + // The break statement block leads to the merge block (exits the loop) + CFG.addEdge(cfg, breakStatementBlock, mergeBlock); + + CFG.popBlock(cfg); + + const bodyBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT); + this.bodyBlock = bodyBlock; + CFG.addEdge(cfg, breakCheckBlock, bodyBlock); + + this.executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode); + + const updateBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT); + CFG.addEdge(cfg, bodyBlock, updateBlock); + + // Update the loop variable in the update block (like bounded loops) + CFG.pushBlock(cfg, updateBlock); + const updateAssignmentNode = DAG.createNodeData({ + nodeType: NodeType.ASSIGNMENT, + dependsOn: [phiNode.id, updateResult.id], + phiBlocks: [] + }); + const updateAssignmentID = DAG.getOrCreateNode(this.strandsContext.dag, updateAssignmentNode); + CFG.recordInBasicBlock(cfg, updateBlock, updateAssignmentID); + CFG.popBlock(cfg); + + const scopeEndBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_END); + CFG.addEdge(cfg, updateBlock, scopeEndBlock); + + // Loop back to break check + CFG.addEdge(cfg, scopeEndBlock, breakCheckBlock); + + // Break condition exits to merge + CFG.addEdge(cfg, breakCheckBlock, mergeBlock); + } + + initializeLoopVariable(cfg, branchBlock) { + CFG.pushBlock(cfg, branchBlock); + let initialVar = this.initialCb(); + + // Convert to StrandsNode if it's not already one + if (!(initialVar instanceof StrandsNode)) { + const { id, dimension } = primitiveConstructorNode(this.strandsContext, { baseType: BaseType.FLOAT, dimension: 1 }, initialVar); + initialVar = createStrandsNode(id, dimension, this.strandsContext); + } + + // Create phi node for the loop variable in the BRANCH block + const phiNode = createPhiNode(this.strandsContext, [ + { value: initialVar, blockId: branchBlock }, + { value: initialVar, blockId: branchBlock } // Placeholder, will be updated later + ], 'loopVar'); + CFG.popBlock(cfg); + + return { initialVar, phiNode }; + } + + createLogicalNotNode(conditionNode) { + const notOperationNode = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Unary.LOGICAL_NOT, + baseType: BaseType.BOOL, + dimension: 1, + dependsOn: [conditionNode.id], + phiBlocks: [], + usedBy: [] + }); + const notOperationID = DAG.getOrCreateNode(this.strandsContext.dag, notOperationNode); + return createStrandsNode(notOperationID, 1, this.strandsContext); + } + + executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode) { + CFG.pushBlock(cfg, bodyBlock); + + // Create phi node references to pass to the body callback + const phiVars = {}; + const phiNodesForBody = {}; + CFG.pushBlockForModification(cfg, branchBlock); + for (const [varName, initialValueNode] of Object.entries(this.initialVars)) { + if (varName !== 'loopVar') { + // Create phi node that will be used for the final result + const varPhiNode = createPhiNode(this.strandsContext, [ + { value: initialValueNode, blockId: branchBlock }, // Initial value + { value: initialValueNode, blockId: bodyBlock } // Placeholder - will update after body execution + ], varName); + phiNodesForBody[varName] = varPhiNode; + phiVars[varName] = createStrandsNode(varPhiNode.id, varPhiNode.dimension, this.strandsContext); + } + } + CFG.popBlock(cfg); + + const loopVarNode = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext); + this.bodyResults = this.bodyCb(loopVarNode, phiVars); + this.phiNodesForBody = phiNodesForBody; + // Capture the final block after body execution before popping + this.finalBodyBlock = cfg.currentBlock; + CFG.popBlock(cfg); + } + + loopIsBounded(initialVar, conditionNode, updateVar) { + // A loop is considered "bounded" if we can determine at compile time that it will + // execute a known number of iterations. This happens when: + // 1. The condition compares the loop variable against a compile-time constant + // 2. At least one side of the comparison uses only literals (no variables/uniforms) + + if (!conditionNode) return false; + + // Analyze the condition node - it should be a comparison operation + const conditionData = DAG.getNodeDataFromID(this.strandsContext.dag, conditionNode.id); + + if (conditionData.nodeType !== NodeType.OPERATION) { + return false; + } + + // For a comparison like "i < bound", we need at least one side to use only literals + // The condition should have two dependencies: left and right operands + if (!conditionData.dependsOn || conditionData.dependsOn.length !== 2) { + return false; + } + + // Check if either operand uses only literals + const leftOperand = createStrandsNode(conditionData.dependsOn[0], 1, this.strandsContext); + const rightOperand = createStrandsNode(conditionData.dependsOn[1], 1, this.strandsContext); + + const leftUsesOnlyLiterals = this.nodeUsesOnlyLiterals(leftOperand); + const rightUsesOnlyLiterals = this.nodeUsesOnlyLiterals(rightOperand); + + // At least one side should use only literals for the loop to be bounded + return leftUsesOnlyLiterals || rightUsesOnlyLiterals; + } + + nodeUsesOnlyLiterals(node) { + // Recursively check if a node and all its dependencies use only literals + const nodeData = DAG.getNodeDataFromID(this.strandsContext.dag, node.id); + + switch (nodeData.nodeType) { + case NodeType.LITERAL: + return true; + + case NodeType.VARIABLE: + // Variables (like uniforms) make this branch unbounded + return false; + + case NodeType.PHI: + // Phi nodes (like loop variables) are not literals + return false; + + case NodeType.OPERATION: + // For operations, all dependencies must use only literals + if (nodeData.dependsOn) { + for (const depId of nodeData.dependsOn) { + const depNode = createStrandsNode(depId, 1, this.strandsContext); + if (!this.nodeUsesOnlyLiterals(depNode)) { + return false; + } + } + } + return true; + + default: + // Conservative: if we don't know the node type, assume not literal + return false; + } + } +} diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index f35a1cdc37..d4e9515793 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -1,4 +1,4 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType } from "./ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType } from "./ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag"; import * as FES from './strands_FES' function shouldCreateTemp(dag, nodeID) { @@ -92,6 +92,40 @@ const cfgHandlers = { [BlockType.FUNCTION](blockID, strandsContext, generationContext) { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); }, + [BlockType.FOR](blockID, strandsContext, generationContext) { + const { dag, cfg } = strandsContext; + const instructions = cfg.blockInstructions[blockID] || []; + + generationContext.write(`for (`); + + // Set flag to suppress semicolon on the last statement + const originalSuppressSemicolon = generationContext.suppressSemicolon; + + for (let i = 0; i < instructions.length; i++) { + const nodeID = instructions[i]; + const node = getNodeDataFromID(dag, nodeID); + const isLast = i === instructions.length - 1; + + // Suppress semicolon on the last statement + generationContext.suppressSemicolon = isLast; + + if (shouldCreateTemp(dag, nodeID)) { + const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID); + generationContext.write(declaration); + } + if (node.nodeType === NodeType.STATEMENT) { + glslBackend.generateStatement(generationContext, dag, nodeID); + } + if (node.nodeType === NodeType.ASSIGNMENT) { + glslBackend.generateAssignment(generationContext, dag, nodeID); + } + } + + // Restore original flag + generationContext.suppressSemicolon = originalSuppressSemicolon; + + generationContext.write(`)`); + }, assignPhiNodeValues(blockID, strandsContext, generationContext) { const { dag, cfg } = strandsContext; // Find all phi nodes that this block feeds into @@ -135,8 +169,19 @@ export const glslBackend = { }, generateStatement(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); - if (node.statementType === OpCode.ControlFlow.DISCARD) { - generationContext.write('discard;'); + const semicolon = generationContext.suppressSemicolon ? '' : ';'; + if (node.statementType === StatementType.DISCARD) { + generationContext.write(`discard${semicolon}`); + } else if (node.statementType === StatementType.BREAK) { + generationContext.write(`break${semicolon}`); + } else if (node.statementType === StatementType.EXPRESSION) { + // Generate the expression followed by semicolon (unless suppressed) + const exprNodeID = node.dependsOn[0]; + const expr = this.generateExpression(generationContext, dag, exprNodeID); + generationContext.write(`${expr}${semicolon}`); + } else if (node.statementType === StatementType.EMPTY) { + // Generate just a semicolon (unless suppressed) + generationContext.write(semicolon); } }, generateAssignment(generationContext, dag, nodeID) { @@ -146,8 +191,9 @@ export const glslBackend = { const sourceNodeID = node.dependsOn[1]; const phiTempName = generationContext.tempNames[phiNodeID]; const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); + const semicolon = generationContext.suppressSemicolon ? '' : ';'; if (phiTempName && sourceExpr) { - generationContext.write(`${phiTempName} = ${sourceExpr};`); + generationContext.write(`${phiTempName} = ${sourceExpr}${semicolon}`); } }, generateDeclaration(generationContext, dag, nodeID) { @@ -223,6 +269,19 @@ export const glslBackend = { const [lID, rID] = node.dependsOn; const left = this.generateExpression(generationContext, dag, lID); const right = this.generateExpression(generationContext, dag, rID); + + // Special case for modulo: use mod() function for floats in GLSL + if (node.opCode === OpCode.Binary.MODULO) { + const leftNode = getNodeDataFromID(dag, lID); + const rightNode = getNodeDataFromID(dag, rID); + // If either operand is float, use mod() function + if (leftNode.baseType === BaseType.FLOAT || rightNode.baseType === BaseType.FLOAT) { + return `mod(${left}, ${right})`; + } + // For integers, use % operator + return `(${left} % ${right})`; + } + const opSym = OpCodeToSymbol[node.opCode]; if (useParantheses) { return `(${left} ${opSym} ${right})`; diff --git a/src/strands/strands_phi_utils.js b/src/strands/strands_phi_utils.js new file mode 100644 index 0000000000..e73c4c34cd --- /dev/null +++ b/src/strands/strands_phi_utils.js @@ -0,0 +1,30 @@ +import * as CFG from './ir_cfg'; +import * as DAG from './ir_dag'; +import { NodeType } from './ir_types'; + +export function createPhiNode(strandsContext, phiInputs, varName) { + // Determine the proper dimension and baseType from the inputs + const validInputs = phiInputs.filter(input => input.value.id !== null); + if (validInputs.length === 0) { + throw new Error(`No valid inputs for phi node for variable ${varName}`); + } + // Get dimension and baseType from first valid input + const firstInput = DAG.getNodeDataFromID(strandsContext.dag, validInputs[0].value.id); + const dimension = firstInput.dimension; + const baseType = firstInput.baseType; + const nodeData = { + nodeType: NodeType.PHI, + dimension, + baseType, + dependsOn: phiInputs.map(input => input.value.id).filter(id => id !== null), + phiBlocks: phiInputs.map(input => input.blockId), + phiInputs // Store the full phi input information + }; + const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); + CFG.recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id); + return { + id, + dimension, + baseType + }; +} \ No newline at end of file diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index da0bdee4af..cbba69b213 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -13,8 +13,9 @@ function replaceBinaryOperator(codeSource) { case '==': case '===': return 'equalTo'; case '>': return 'greaterThan'; - case '>=': return 'greaterThanEqualTo'; + case '>=': return 'greaterEqual'; case '<': return 'lessThan'; + case '<=': return 'lessEqual'; case '&&': return 'and'; case '||': return 'or'; } @@ -78,6 +79,15 @@ const ASTCallbacks = { delete node.argument; delete node.operator; }, + BreakStatement(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + node.callee = { + type: 'Identifier', + name: '__p5.break' + }; + node.arguments = []; + node.type = 'CallExpression'; + }, VariableDeclarator(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } if (nodeIsUniform(node.init)) { @@ -274,7 +284,7 @@ const ASTCallbacks = { } // Second pass: find assignments to non-local variables for (const stmt of body.body) { - if (stmt.type === 'ExpressionStatement' && + if (stmt.type === 'ExpressionStatement' && stmt.expression.type === 'AssignmentExpression') { const left = stmt.expression.left; if (left.type === 'Identifier') { @@ -282,12 +292,15 @@ const ASTCallbacks = { if (!localVars.has(left.name)) { assignedVars.add(left.name); } - } else if (left.type === 'MemberExpression' && + } else if (left.type === 'MemberExpression' && left.object.type === 'Identifier') { // Property assignment: obj.prop = value if (!localVars.has(left.object.name)) { assignedVars.add(left.object.name); } + } else if (stmt.type === 'BlockStatement') { + // Recursively analyze nested block statements + analyzeBlock(stmt); } } } @@ -330,8 +343,8 @@ const ASTCallbacks = { if (!node || typeof node !== 'object') return; if (node.type === 'Identifier' && tempVarMap.has(node.name)) { node.name = tempVarMap.get(node.name); - } else if (node.type === 'MemberExpression' && - node.object.type === 'Identifier' && + } else if (node.type === 'MemberExpression' && + node.object.type === 'Identifier' && tempVarMap.has(node.object.name)) { node.object.name = tempVarMap.get(node.object.name); } @@ -372,11 +385,24 @@ const ASTCallbacks = { addCopyingAndReturn(elseFunction.body, assignedVars); // Create a block variable to capture the return value const blockVar = `__block_${blockVarCounter++}`; - // Replace with a block statement containing: - // 1. The conditional call assigned to block variable - // 2. Assignments from block variable back to original variables + // Replace with a block statement const statements = []; - // 1. const blockVar = strandsIf().Else() + // Make sure every assigned variable starts as a node + for (const varName of assignedVars) { + statements.push({ + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { type: 'Identifier', name: varName }, + right: { + type: 'CallExpression', + callee: { type: 'Identifier', name: '__p5.strandsNode' }, + arguments: [{ type: 'Identifier', name: varName }], + } + } + }); + } statements.push({ type: 'VariableDeclaration', declarations: [{ @@ -415,18 +441,397 @@ const ASTCallbacks = { delete node.consequent; delete node.alternate; }, + UpdateExpression(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + + // Transform ++var, var++, --var, var-- into assignment expressions + let operator; + if (node.operator === '++') { + operator = '+'; + } else if (node.operator === '--') { + operator = '-'; + } else { + return; // Unknown update operator + } + + // Convert to: var = var + 1 or var = var - 1 + const assignmentExpr = { + type: 'AssignmentExpression', + operator: '=', + left: node.argument, + right: { + type: 'BinaryExpression', + operator: operator, + left: node.argument, + right: { + type: 'Literal', + value: 1 + } + } + }; + + // Replace the update expression with the assignment expression + Object.assign(node, assignmentExpr); + delete node.prefix; + this.BinaryExpression(node.right, _state, [...ancestors, node]); + this.AssignmentExpression(node, _state, ancestors); + }, + ForStatement(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + + // Transform for statement into strandsFor() call + // for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars) + + // Create the initial callback from the for loop's init + let initialFunction; + if (node.init && node.init.type === 'VariableDeclaration') { + // Handle: for (let i = 0; ...) + const declaration = node.init.declarations[0]; + let initValue = declaration.init; + + const initAst = { type: 'Program', body: [{ type: 'ExpressionStatement', expression: initValue }] }; + initValue = initAst.body[0].expression; + + initialFunction = { + type: 'ArrowFunctionExpression', + params: [], + body: { + type: 'BlockStatement', + body: [{ + type: 'ReturnStatement', + argument: initValue + }] + } + }; + } else { + // Handle other cases - return a default value + initialFunction = { + type: 'ArrowFunctionExpression', + params: [], + body: { + type: 'BlockStatement', + body: [{ + type: 'ReturnStatement', + argument: { + type: 'Literal', + value: 0 + } + }] + } + }; + } + + // Create the condition callback + let conditionBody = node.test || { type: 'Literal', value: true }; + // Replace loop variable references with the parameter + if (node.init?.type === 'VariableDeclaration') { + const loopVarName = node.init.declarations[0].id.name; + conditionBody = this.replaceIdentifierReferences(conditionBody, loopVarName, 'loopVar'); + } + const conditionAst = { type: 'Program', body: [{ type: 'ExpressionStatement', expression: conditionBody }] }; + conditionBody = conditionAst.body[0].expression; + + const conditionFunction = { + type: 'ArrowFunctionExpression', + params: [{ type: 'Identifier', name: 'loopVar' }], + body: conditionBody + }; + + // Create the update callback + let updateFunction; + if (node.update) { + let updateExpr = node.update; + // Replace loop variable references with the parameter + if (node.init?.type === 'VariableDeclaration') { + const loopVarName = node.init.declarations[0].id.name; + updateExpr = this.replaceIdentifierReferences(updateExpr, loopVarName, 'loopVar'); + } + const updateAst = { type: 'Program', body: [{ type: 'ExpressionStatement', expression: updateExpr }] }; + // const nonControlFlowCallbacks = { ...ASTCallbacks }; + // delete nonControlFlowCallbacks.IfStatement; + // delete nonControlFlowCallbacks.ForStatement; + // ancestor(updateAst, nonControlFlowCallbacks, undefined, _state); + updateExpr = updateAst.body[0].expression; + + updateFunction = { + type: 'ArrowFunctionExpression', + params: [{ type: 'Identifier', name: 'loopVar' }], + body: { + type: 'BlockStatement', + body: [{ + type: 'ReturnStatement', + argument: updateExpr + }] + } + }; + } else { + updateFunction = { + type: 'ArrowFunctionExpression', + params: [{ type: 'Identifier', name: 'loopVar' }], + body: { + type: 'BlockStatement', + body: [{ + type: 'ReturnStatement', + argument: { type: 'Identifier', name: 'loopVar' } + }] + } + }; + } + + // Create the body callback + let bodyBlock = node.body.type === 'BlockStatement' ? node.body : { + type: 'BlockStatement', + body: [node.body] + }; + + // Replace loop variable references in the body + if (node.init?.type === 'VariableDeclaration') { + const loopVarName = node.init.declarations[0].id.name; + bodyBlock = this.replaceIdentifierReferences(bodyBlock, loopVarName, 'loopVar'); + } + + const bodyFunction = { + type: 'ArrowFunctionExpression', + params: [ + { type: 'Identifier', name: 'loopVar' }, + { type: 'Identifier', name: 'vars' } + ], + body: bodyBlock + }; + + // Analyze which outer scope variables are assigned in the loop body + const assignedVars = new Set(); + const analyzeBlock = (body) => { + if (body.type !== 'BlockStatement') return; + + for (const stmt of body.body) { + if (stmt.type === 'ExpressionStatement' && + stmt.expression.type === 'AssignmentExpression') { + const left = stmt.expression.left; + if (left.type === 'Identifier') { + assignedVars.add(left.name); + } + } else if (stmt.type === 'BlockStatement') { + // Recursively analyze nested block statements + analyzeBlock(stmt); + } + } + }; + + analyzeBlock(bodyFunction.body); + + if (assignedVars.size > 0) { + // Add copying, reference replacement, and return statements similar to if statements + const addCopyingAndReturn = (functionBody, varsToReturn) => { + if (functionBody.type === 'BlockStatement') { + const tempVarMap = new Map(); + const copyStatements = []; + + for (const varName of varsToReturn) { + const tempName = `__copy_${varName}_${blockVarCounter++}`; + tempVarMap.set(varName, tempName); + + copyStatements.push({ + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { type: 'Identifier', name: tempName }, + init: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'vars' }, + property: { type: 'Identifier', name: varName }, + computed: false + }, + property: { type: 'Identifier', name: 'copy' }, + computed: false + }, + arguments: [] + } + }], + kind: 'let' + }); + } + + // Replace references to original variables with temp variables + const replaceReferences = (node) => { + if (!node || typeof node !== 'object') return; + if (node.type === 'Identifier' && tempVarMap.has(node.name)) { + node.name = tempVarMap.get(node.name); + } + + for (const key in node) { + if (node.hasOwnProperty(key) && key !== 'parent') { + if (Array.isArray(node[key])) { + node[key].forEach(replaceReferences); + } else if (typeof node[key] === 'object') { + replaceReferences(node[key]); + } + } + } + }; + + functionBody.body.forEach(replaceReferences); + functionBody.body.unshift(...copyStatements); + + // Add return statement + const returnObj = { + type: 'ObjectExpression', + properties: Array.from(varsToReturn).map(varName => ({ + type: 'Property', + key: { type: 'Identifier', name: varName }, + value: { type: 'Identifier', name: tempVarMap.get(varName) }, + kind: 'init', + computed: false, + shorthand: false + })) + }; + + functionBody.body.push({ + type: 'ReturnStatement', + argument: returnObj + }); + } + }; + + addCopyingAndReturn(bodyFunction.body, assignedVars); + + // Create block variable and assignments similar to if statements + const blockVar = `__block_${blockVarCounter++}`; + const statements = []; + + // Create initial vars object from assigned variables + const initialVarsProperties = []; + for (const varName of assignedVars) { + initialVarsProperties.push({ + type: 'Property', + key: { type: 'Identifier', name: varName }, + value: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '__p5.strandsNode', + }, + arguments: [ + { type: 'Identifier', name: varName }, + ], + }, + kind: 'init', + method: false, + shorthand: false, + computed: false + }); + } + + const initialVarsObject = { + type: 'ObjectExpression', + properties: initialVarsProperties + }; + + // Create the strandsFor call + const callExpression = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '__p5.strandsFor' + }, + arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, initialVarsObject] + }; + + statements.push({ + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { type: 'Identifier', name: blockVar }, + init: callExpression + }], + kind: 'const' + }); + + // Add assignments back to original variables + for (const varName of assignedVars) { + statements.push({ + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { type: 'Identifier', name: varName }, + right: { + type: 'MemberExpression', + object: { type: 'Identifier', name: blockVar }, + property: { type: 'Identifier', name: varName }, + computed: false + } + } + }); + } + + node.type = 'BlockStatement'; + node.body = statements; + } else { + // No assignments, just replace with call expression + node.type = 'ExpressionStatement'; + node.expression = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '__p5.strandsFor' + }, + arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, { + type: 'ObjectExpression', + properties: [] + }] + }; + } + + delete node.init; + delete node.test; + delete node.update; + }, + + // Helper method to replace identifier references in AST nodes + replaceIdentifierReferences(node, oldName, newName) { + if (!node || typeof node !== 'object') return node; + + const replaceInNode = (n) => { + if (!n || typeof n !== 'object') return n; + + if (n.type === 'Identifier' && n.name === oldName) { + return { ...n, name: newName }; + } + + // Create a copy and recursively process properties + const newNode = { ...n }; + for (const key in n) { + if (n.hasOwnProperty(key) && key !== 'parent') { + if (Array.isArray(n[key])) { + newNode[key] = n[key].map(replaceInNode); + } else if (typeof n[key] === 'object') { + newNode[key] = replaceInNode(n[key]); + } + } + } + return newNode; + }; + + return replaceInNode(node); + } } export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { const ast = parse(sourceString, { ecmaVersion: 2021, locations: srcLocations }); - // First pass: transform everything except if statements using normal ancestor traversal - const nonIfCallbacks = { ...ASTCallbacks }; - delete nonIfCallbacks.IfStatement; - ancestor(ast, nonIfCallbacks, undefined, { varyings: {} }); - // Second pass: transform if statements in post-order using recursive traversal - const postOrderIfTransform = { + // First pass: transform everything except if/for statements using normal ancestor traversal + const nonControlFlowCallbacks = { ...ASTCallbacks }; + delete nonControlFlowCallbacks.IfStatement; + delete nonControlFlowCallbacks.ForStatement; + ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {} }); + // Second pass: transform if/for statements in post-order using recursive traversal + const postOrderControlFlowTransform = { IfStatement(node, state, c) { // First recursively process children if (node.test) c(node.test, state); @@ -434,9 +839,18 @@ const ASTCallbacks = { if (node.alternate) c(node.alternate, state); // Then apply the transformation to this node ASTCallbacks.IfStatement(node, state, []); + }, + ForStatement(node, state, c) { + // First recursively process children + if (node.init) c(node.init, state); + if (node.test) c(node.test, state); + if (node.update) c(node.update, state); + if (node.body) c(node.body, state); + // Then apply the transformation to this node + ASTCallbacks.ForStatement(node, state, []); } }; - recursive(ast, { varyings: {} }, postOrderIfTransform); + recursive(ast, { varyings: {} }, postOrderControlFlowTransform); const transpiledSource = escodegen.generate(ast); const scopeKeys = Object.keys(scope); const internalStrandsCallback = new Function( @@ -452,6 +866,5 @@ const ASTCallbacks = { transpiledSource.lastIndexOf('}') ).replaceAll(';', '') ); - console.log(internalStrandsCallback.toString()) return () => internalStrandsCallback(p5, ...scopeKeys.map(key => scope[key])); } diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 19b23145ef..bff000763c 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -2,6 +2,7 @@ import p5 from '../../../src/app.js'; suite('p5.Shader', function() { var myp5; beforeAll(function() { + window.IS_MINIFIED = true; myp5 = new p5(function(p) { p.setup = function() { p.createCanvas(100, 100, p.WEBGL); @@ -634,5 +635,242 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); }); + + suite('for loop statements', () => { + test('handle simple for loop with known iteration count', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); + + for (let i = 0; i < 3; i++) { + color = color + 0.1; + } + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should loop 3 times: 0.0 + 0.1 + 0.1 + 0.1 = 0.3 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 + assert.approximately(pixelColor[1], 77, 5); + assert.approximately(pixelColor[2], 77, 5); + }); + + test('handle for loop with variable as loop bound', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + const maxIterations = myp5.uniformInt(() => 2); + + myp5.getPixelInputs(inputs => { + let result = myp5.float(0.0); + + for (let i = 0; i < maxIterations; i++) { + result = result + 0.25; + } + + inputs.color = [result, result, result, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should loop 2 times: 0.0 + 0.25 + 0.25 = 0.5 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 + assert.approximately(pixelColor[1], 127, 5); + assert.approximately(pixelColor[2], 127, 5); + }); + + test('handle for loop modifying multiple variables', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let red = myp5.float(0.0); + let green = myp5.float(0.0); + + for (let i = 0; i < 4; i++) { + red = red + 0.125; // 4 * 0.125 = 0.5 + green = green + 0.25; // 4 * 0.25 = 1.0 + } + + inputs.color = [red, green, 0.0, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 + assert.approximately(pixelColor[1], 255, 5); // 1.0 * 255 = 255 + assert.approximately(pixelColor[2], 0, 5); // 0.0 * 255 = 0 + }); + + test('handle for loop with conditional inside', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let sum = myp5.float(0.0); + + for (let i = 0; i < 5; i++) { + if (i % 2 === 0) { + sum = sum + 0.1; // Add on even iterations: 0, 2, 4 + } + } + + inputs.color = [sum, sum, sum, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should add 0.1 three times (iterations 0, 2, 4): 3 * 0.1 = 0.3 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 + assert.approximately(pixelColor[1], 77, 5); + assert.approximately(pixelColor[2], 77, 5); + }); + + test('handle nested for loops', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let total = myp5.float(0.0); + + for (let i = 0; i < 2; i++) { + for (let j = 0; j < 3; j++) { + total = total + 0.05; // 2 * 3 = 6 iterations + } + } + + inputs.color = [total, total, total, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should run 6 times: 6 * 0.05 = 0.3 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 + assert.approximately(pixelColor[1], 77, 5); + assert.approximately(pixelColor[2], 77, 5); + }); + + test('handle for loop using loop variable in calculations', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let sum = myp5.float(0.0); + + for (let i = 1; i <= 3; i++) { + sum = sum + (i * 0.1); // 1*0.1 + 2*0.1 + 3*0.1 = 0.6 + } + + inputs.color = [sum, sum, sum, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should be: 0.1 + 0.2 + 0.3 = 0.6 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 153, 5); // 0.6 * 255 ≈ 153 + assert.approximately(pixelColor[1], 153, 5); + assert.approximately(pixelColor[2], 153, 5); + }); + + // Keep one direct API test for completeness + test('handle direct StrandsFor API usage', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let accumulator = myp5.float(0.0); + + const loopResult = myp5.strandsFor( + () => 0, + (loopVar) => loopVar < 4, + (loopVar) => loopVar + 1, + (loopVar, vars) => { + let newValue = vars.accumulator.copy(); + newValue = newValue + 0.125; + return { accumulator: newValue }; + }, + { accumulator: accumulator.copy() }, + ); + + accumulator = loopResult.accumulator; + inputs.color = [accumulator, accumulator, accumulator, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should loop 4 times: 4 * 0.125 = 0.5 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 + assert.approximately(pixelColor[1], 127, 5); + assert.approximately(pixelColor[2], 127, 5); + }); + + test('handle for loop with break statement', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let color = 0; + let maxIterations = 5; + + for (let i = 0; i < 100; i++) { + if (i >= maxIterations) { + break; + } + color = color + 0.1; + } + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should break after 5 iterations: 5 * 0.1 = 0.5 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 + }); + }); }); }); From 010530525593e60515a57c34ba90a61dbaa183d3 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 3 Oct 2025 13:43:34 -0400 Subject: [PATCH 06/22] Add swizzle test --- src/strands/strands_transpiler.js | 4 ++++ test/unit/webgl/p5.Shader.js | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index cbba69b213..ec7786783b 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -610,6 +610,10 @@ const ASTCallbacks = { const left = stmt.expression.left; if (left.type === 'Identifier') { assignedVars.add(left.name); + } else if (left.type === 'MemberExpression' && + left.object.type === 'Identifier') { + // Property assignment: obj.prop = value (includes swizzles) + assignedVars.add(left.object.name); } } else if (stmt.type === 'BlockStatement') { // Recursively analyze nested block statements diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index bff000763c..dd8853923f 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -664,6 +664,33 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[2], 77, 5); }); + test('handle swizzle assignments in loops', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let color = [0, 0, 0, 1]; + + for (let i = 0; i < 3; i++) { + color.rgb += 0.1; + } + + inputs.color = color; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should loop 3 times: 0.0 + 0.1 + 0.1 + 0.1 = 0.3 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 + assert.approximately(pixelColor[1], 77, 5); + assert.approximately(pixelColor[2], 77, 5); + }); + test('handle for loop with variable as loop bound', () => { myp5.createCanvas(50, 50, myp5.WEBGL); From bc506a1ea0c1e58fd897e2eae91c76237bf6b5a7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 3 Oct 2025 14:59:47 -0400 Subject: [PATCH 07/22] Reuse existing variables more in phi node generation --- src/strands/strands_codegen.js | 1 - src/strands/strands_glslBackend.js | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 911ab8376b..84c83a77da 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -41,7 +41,6 @@ export function generateShaderCode(strandsContext) { : TypeInfoFromGLSLName[hookType.returnType.typeName]; backend.generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType); hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); - console.log(hooksObj[`${hookType.returnType.typeName} ${hookType.name}`]); } hooksObj.vertexDeclarations = [...vertexDeclarations].join('\n'); diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index d4e9515793..b21da48ee4 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -50,6 +50,19 @@ const cfgHandlers = { for (const nodeID of blockInstructions) { const node = getNodeDataFromID(dag, nodeID); if (node.nodeType === NodeType.PHI) { + // Check if the phi node's first dependency already has a temp name + const dependsOn = node.dependsOn || []; + if (dependsOn.length > 0) { + const firstDependency = dependsOn[0]; + const existingTempName = generationContext.tempNames[firstDependency]; + if (existingTempName) { + // Reuse the existing temp name instead of creating a new one + generationContext.tempNames[nodeID] = existingTempName; + continue; // Skip declaration, just alias to existing variable + } + } + + // Otherwise, create a new temp variable for the phi node const tmp = `T${generationContext.nextTempID++}`; generationContext.tempNames[nodeID] = tmp; const T = extractNodeTypeInfo(dag, nodeID); @@ -192,7 +205,9 @@ export const glslBackend = { const phiTempName = generationContext.tempNames[phiNodeID]; const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); const semicolon = generationContext.suppressSemicolon ? '' : ';'; - if (phiTempName && sourceExpr) { + + // Skip assignment if target and source are the same variable + if (phiTempName && sourceExpr && phiTempName !== sourceExpr) { generationContext.write(`${phiTempName} = ${sourceExpr}${semicolon}`); } }, From b7317be7c2d6ceff130f2f3fd06dc3e4c46038e5 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 3 Oct 2025 15:08:35 -0400 Subject: [PATCH 08/22] Add ElseIf test even though the transpiler doesn't produce that, simplify IF_BODY block --- src/strands/ir_types.js | 1 - src/strands/strands_conditionals.js | 4 +-- test/unit/webgl/p5.Shader.js | 39 +++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index b9138dbf90..6446f13781 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -189,7 +189,6 @@ export const BlockType = { IF_COND: 'if_cond', IF_BODY: 'if_body', ELSE_COND: 'else_cond', - ELSE_BODY: 'else_body', SCOPE_START: 'scope_start', SCOPE_END: 'scope_end', FOR: 'for', diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index c8dcd7b852..da119d9229 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -17,7 +17,7 @@ export class StrandsConditional { this.branches.push({ condition, branchCallback, - blockType: BlockType.ELIF_BODY + blockType: BlockType.IF_BODY }); return this; } @@ -25,7 +25,7 @@ export class StrandsConditional { this.branches.push({ condition: null, branchCallback, - blockType: BlockType.ELSE_BODY + blockType: BlockType.IF_BODY }); const phiNodes = buildConditional(this.ctx, this); const assignments = {}; diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index dd8853923f..d558b6401b 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -634,6 +634,45 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); + test('handle direct StrandsIf ElseIf API usage', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + const value = myp5.uniformFloat(() => 0.5); // middle value + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); // initial black + const assignments = myp5.strandsIf( + value.greaterThan(0.8), + () => { + let tmp = color.copy(); + tmp = myp5.float(1.0); // white for high values + return { color: tmp }; + } + ).ElseIf( + value.greaterThan(0.3), + () => { + let tmp = color.copy(); + tmp = myp5.float(0.5); // gray for medium values + return { color: tmp }; + } + ).Else(() => { + let tmp = color.copy(); + tmp = myp5.float(0.0); // black for low values + return { color: tmp }; + }); + color = assignments.color; + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is gray (medium condition was true) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 + }); }); suite('for loop statements', () => { From e33bb50913078343f594ea9a0d38f9326d18d639 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 3 Oct 2025 15:10:33 -0400 Subject: [PATCH 09/22] Remove unused code --- src/strands/strands_glslBackend.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/strands/strands_glslBackend.js b/src/strands/strands_glslBackend.js index b21da48ee4..2ecda9ee7a 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/strands/strands_glslBackend.js @@ -87,10 +87,6 @@ const cfgHandlers = { this[BlockType.DEFAULT](blockID, strandsContext, generationContext); this.assignPhiNodeValues(blockID, strandsContext, generationContext); }, - [BlockType.ELSE_BODY](blockID, strandsContext, generationContext) { - this[BlockType.DEFAULT](blockID, strandsContext, generationContext); - this.assignPhiNodeValues(blockID, strandsContext, generationContext); - }, [BlockType.SCOPE_START](blockID, strandsContext, generationContext) { generationContext.write(`{`); generationContext.indent++; From 16c203c25fcfa140c53c4766e80774b2fb886fac Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 3 Oct 2025 15:29:30 -0400 Subject: [PATCH 10/22] Restore some whitespace --- src/strands/ir_builders.js | 49 ++++++++++++++++++++++++++++++++++++++ src/strands/ir_cfg.js | 12 ++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index bd4700c372..127d19e235 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -4,6 +4,7 @@ import * as FES from './strands_FES' import { NodeType, OpCode, BaseType, DataType, BasePriority, OpCodeToSymbol, typeEquals, } from './ir_types'; import { createStrandsNode, StrandsNode } from './strands_node'; import { strandsBuiltinFunctions } from './strands_builtins'; + ////////////////////////////////////////////// // Builders for node graphs ////////////////////////////////////////////// @@ -23,6 +24,7 @@ export function scalarLiteralNode(strandsContext, typeInfo, value) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension }; } + export function variableNode(strandsContext, typeInfo, identifier) { const { cfg, dag } = strandsContext; const { dimension, baseType } = typeInfo; @@ -36,6 +38,7 @@ export function variableNode(strandsContext, typeInfo, identifier) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension }; } + export function unaryOpNode(strandsContext, nodeOrValue, opCode) { const { dag, cfg } = strandsContext; let dependsOn; @@ -58,6 +61,7 @@ export function unaryOpNode(strandsContext, nodeOrValue, opCode) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: node.dimension }; } + export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) { const { dag, cfg } = strandsContext; // Construct a node for right if its just an array or number etc. @@ -70,6 +74,7 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) } let finalLeftNodeID = leftStrandsNode.id; let finalRightNodeID = rightStrandsNode.id; + // Check if we have to cast either node const leftType = DAG.extractNodeTypeInfo(dag, leftStrandsNode.id); const rightType = DAG.extractNodeTypeInfo(dag, rightStrandsNode.id); @@ -99,6 +104,7 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) } else if (leftType.baseType !== rightType.baseType || leftType.dimension !== rightType.dimension) { + if (leftType.dimension === 1 && rightType.dimension > 1) { cast.node = leftStrandsNode; cast.toType = rightType; @@ -119,7 +125,9 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) else { FES.userError('type error', `A vector of length ${leftType.dimension} operated with a vector of length ${rightType.dimension} is not allowed.`); } + const casted = primitiveConstructorNode(strandsContext, cast.toType, cast.node); + if (cast.node === leftStrandsNode) { leftStrandsNode = createStrandsNode(casted.id, casted.dimension, strandsContext); finalLeftNodeID = leftStrandsNode.id; @@ -128,6 +136,7 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) finalRightNodeID = rightStrandsNode.id; } } + const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode, @@ -139,6 +148,7 @@ export function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: nodeData.dimension }; } + export function memberAccessNode(strandsContext, parentNode, componentNode, memberTypeInfo) { const { dag, cfg } = strandsContext; const nodeData = DAG.createNodeData({ @@ -152,6 +162,7 @@ export function memberAccessNode(strandsContext, parentNode, componentNode, memb CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: memberTypeInfo.dimension }; } + export function structInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) { const { cfg, dag, } = strandsContext; if (dependsOn.length === 0) { @@ -168,6 +179,7 @@ export function structInstanceNode(strandsContext, structTypeInfo, identifier, d dependsOn.push(componentID); } } + const nodeData = DAG.createNodeData({ nodeType: NodeType.VARIABLE, dimension: structTypeInfo.properties.length, @@ -177,12 +189,15 @@ export function structInstanceNode(strandsContext, structTypeInfo, identifier, d }) const structID = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, structID); + return { id: structID, dimension: 0, components: dependsOn }; } + function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { const inputs = Array.isArray(dependsOn) ? dependsOn : [dependsOn]; const mappedDependencies = []; let { dimension, baseType } = typeInfo; + const dag = strandsContext.dag; let calculatedDimensions = 0; let originalNodeID = null; @@ -191,6 +206,7 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { const node = DAG.getNodeDataFromID(dag, dep.id); originalNodeID = dep.id; baseType = node.baseType; + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { for (const inner of node.dependsOn) { mappedDependencies.push(inner); @@ -198,6 +214,7 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { } else { mappedDependencies.push(dep.id); } + calculatedDimensions += node.dimension; continue; } @@ -225,6 +242,7 @@ function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) { } return { originalNodeID, mappedDependencies, inferredTypeInfo }; } + export function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray) { const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, @@ -236,22 +254,28 @@ export function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray const id = DAG.getOrCreateNode(strandsContext.dag, nodeData); return id; } + export function primitiveConstructorNode(strandsContext, typeInfo, dependsOn) { const cfg = strandsContext.cfg; const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn); + const finalType = { baseType: typeInfo.baseType, dimension: inferredTypeInfo.dimension }; + const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies); if (typeInfo.baseType !== BaseType.DEFER) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); } + return { id, dimension: finalType.dimension, components: mappedDependencies }; } + export function structConstructorNode(strandsContext, structTypeInfo, rawUserArgs) { const { cfg, dag } = strandsContext; const { identifer, properties } = structTypeInfo; + if (!(rawUserArgs.length === properties.length)) { FES.userError('type error', `You've tried to construct a ${structTypeInfo.typeName} struct with ${rawUserArgs.length} properties, but it expects ${properties.length} properties.\n` + @@ -259,6 +283,7 @@ export function structConstructorNode(strandsContext, structTypeInfo, rawUserArg `${properties.map(prop => prop.name + ' ' + prop.DataType.baseType + prop.DataType.dimension)}` ); } + const dependsOn = []; for (let i = 0; i < properties.length; i++) { const expectedProperty = properties[i]; @@ -272,6 +297,7 @@ export function structConstructorNode(strandsContext, structTypeInfo, rawUserArg ); } } + const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.CONSTRUCTOR, @@ -283,6 +309,7 @@ export function structConstructorNode(strandsContext, structTypeInfo, rawUserArg CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: properties.length, components: structTypeInfo.components }; } + export function functionCallNode( strandsContext, functionName, @@ -291,6 +318,7 @@ export function functionCallNode( ) { const { cfg, dag } = strandsContext; const overloads = rawOverloads || strandsBuiltinFunctions[functionName]; + const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDepsToIDs(strandsContext, DataType.defer, rawUserArg)); const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length); if (matchingArgsCounts.length === 0) { @@ -301,23 +329,28 @@ export function functionCallNode( const argsLengthStr = argsLengthArr.join(', or '); FES.userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${preprocessedArgs.length} arguments were provided.`); } + const isGeneric = (T) => T.dimension === null; let bestOverload = null; let bestScore = 0; let inferredReturnType = null; let inferredDimension = null; + for (const overload of matchingArgsCounts) { let isValid = true; let similarity = 0; + for (let i = 0; i < preprocessedArgs.length; i++) { const preArg = preprocessedArgs[i]; const argType = preArg.inferredTypeInfo; const expectedType = overload.params[i]; let dimension = expectedType.dimension; + if (isGeneric(expectedType)) { if (inferredDimension === null || inferredDimension === 1) { inferredDimension = argType.dimension; } + if (inferredDimension !== argType.dimension && !(argType.dimension === 1 && inferredDimension >= 1) ) { @@ -330,13 +363,16 @@ export function functionCallNode( isValid = false; } } + if (argType.baseType === expectedType.baseType) { similarity += 2; } else if(expectedType.priority > argType.priority) { similarity += 1; } + } + if (isValid && (!bestOverload || similarity > bestScore)) { bestOverload = overload; bestScore = similarity; @@ -346,9 +382,11 @@ export function functionCallNode( } } } + if (bestOverload === null) { FES.userError('parameter validation', `No matching overload for ${functionName} was found!`); } + let dependsOn = []; for (let i = 0; i < bestOverload.params.length; i++) { const arg = preprocessedArgs[i]; @@ -365,6 +403,7 @@ export function functionCallNode( dependsOn.push(castedArgID); } } + const nodeData = DAG.createNodeData({ nodeType: NodeType.OPERATION, opCode: OpCode.Nary.FUNCTION_CALL, @@ -377,6 +416,7 @@ export function functionCallNode( CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: inferredReturnType.dimension }; } + export function statementNode(strandsContext, statementType) { const { dag, cfg } = strandsContext; const nodeData = DAG.createNodeData({ @@ -387,6 +427,7 @@ export function statementNode(strandsContext, statementType) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return id; } + export function swizzleNode(strandsContext, parentNode, swizzle) { const { dag, cfg } = strandsContext; const baseType = dag.baseTypes[parentNode.id]; @@ -402,6 +443,7 @@ export function swizzleNode(strandsContext, parentNode, swizzle) { CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); return { id, dimension: swizzle.length }; } + export function swizzleTrap(id, dimension, strandsContext, onRebind) { const swizzleSets = [ ['x', 'y', 'z', 'w'], @@ -433,7 +475,9 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { new Set(chars).size === chars.length && target.dimension >= chars.length; if (!valid) continue; + const dim = target.dimension; + // lanes are the underlying values of the target vector // e.g. lane 0 holds the value aliased by 'x', 'r', and 's' // the lanes array is in the 'correct' order @@ -442,6 +486,7 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { const { id, dimension } = swizzleNode(strandsContext, target, 'xyzw'[i]); lanes[i] = createStrandsNode(id, dimension, strandsContext); } + // The scalars array contains the individual components of the users values. // This may not be the most efficient way, as we swizzle each component individually, // so that .xyz becomes .x, .y, .z @@ -471,6 +516,7 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { } else { FES.userError('type error', `Unsupported RHS for swizzle assignment: ${value}`); } + // The canonical index refers to the actual value's position in the vector lanes // i.e. we are finding (3,2,1) from .zyx // We set the correct value in the lanes array @@ -478,6 +524,7 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { const canonicalIndex = swizzleSet.indexOf(chars[j]); lanes[canonicalIndex] = scalars[j]; } + const orig = DAG.getNodeDataFromID(strandsContext.dag, target.id); const baseType = orig?.baseType ?? BaseType.FLOAT; const { id: newID } = primitiveConstructorNode( @@ -485,7 +532,9 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { { baseType, dimension: dim }, lanes ); + target.id = newID; + // If we swizzle assign on a struct component i.e. // inputs.position.rg = [1, 2] // The onRebind callback will update the structs components so that it refers to the new values, diff --git a/src/strands/ir_cfg.js b/src/strands/ir_cfg.js index 0d2303aca7..cf25a23d53 100644 --- a/src/strands/ir_cfg.js +++ b/src/strands/ir_cfg.js @@ -1,6 +1,8 @@ import { BlockTypeToName } from "./ir_types"; import * as FES from './strands_FES' + // Todo: remove edges to simplify. Block order is always ordered already. + export function createControlFlowGraph() { return { // graph structure @@ -16,20 +18,24 @@ export function createControlFlowGraph() { currentBlock: -1, }; } + export function pushBlock(graph, blockID) { graph.blockStack.push(blockID); graph.blockOrder.push(blockID); graph.currentBlock = blockID; } + export function popBlock(graph) { graph.blockStack.pop(); const len = graph.blockStack.length; graph.currentBlock = graph.blockStack[len-1]; } + export function pushBlockForModification(graph, blockID) { graph.blockStack.push(blockID); graph.currentBlock = blockID; } + export function createBasicBlock(graph, blockType) { const id = graph.nextID++; graph.blockTypes[id] = blockType; @@ -38,10 +44,12 @@ export function createBasicBlock(graph, blockType) { graph.blockInstructions[id]= []; return id; } + export function addEdge(graph, from, to) { graph.outgoingEdges[from].push(to); graph.incomingEdges[to].push(from); } + export function recordInBasicBlock(graph, blockID, nodeID) { if (nodeID === undefined) { FES.internalError('undefined nodeID in `recordInBasicBlock()`'); @@ -52,6 +60,7 @@ export function recordInBasicBlock(graph, blockID, nodeID) { graph.blockInstructions[blockID] = graph.blockInstructions[blockID] || []; graph.blockInstructions[blockID].push(nodeID); } + export function getBlockDataFromID(graph, id) { return { id, @@ -61,11 +70,13 @@ export function getBlockDataFromID(graph, id) { blockInstructions: graph.blockInstructions[id], } } + export function printBlockData(graph, id) { const block = getBlockDataFromID(graph, id); block.blockType = BlockTypeToName[block.blockType]; console.log(block); } + export function sortCFG(adjacencyList, start) { const visited = new Set(); const postOrder = []; @@ -79,6 +90,7 @@ export function sortCFG(adjacencyList, start) { } postOrder.push(v); } + dfs(start); return postOrder.reverse(); } \ No newline at end of file From 54a25523ccfd52721ff23a5bbba53a220ff17bbd Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 3 Oct 2025 16:29:28 -0400 Subject: [PATCH 11/22] Add contributor doc --- contributor_docs/p5.strands.md | 251 +++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 contributor_docs/p5.strands.md diff --git a/contributor_docs/p5.strands.md b/contributor_docs/p5.strands.md new file mode 100644 index 0000000000..a991f7a94c --- /dev/null +++ b/contributor_docs/p5.strands.md @@ -0,0 +1,251 @@ + + +# p5.strands Overview + +Shader programming is an area of creative coding that can feel like a dark art to many. People share lots of stunning visuals that are created with shaders, but shaders feel like a completely different way of coding, requiring you to learn a new language, pipeline, and paradigm. + +p5.strands hopes to address all of those issues by letting you write shader snippets in JavaScript and compiling it to OpenGL Shading Language (GLSL) for you! + +## Code processing pipeline + +At its core, p5.strands works in four steps: +1. The user writes a function in psuedo-JavaScript. +2. p5.strands transpiles that into actual JavaScript and rewrites aspects of your code. +3. The transpiled code is run. Variable modification function calls are tracked in a graph data structure. +4. p5.strands generates GLSL code from that graph. + +## Why pseudo-JavaScript? + +The code the user writes when using p5.strands is mostly JavaScript, with some extensions. Shader code heavily encourages use of vectors, and the extensions all make this as easy in JavaScript as in GLSL. +- In JavaScript, there is not a vector data type. In p5.strands, you create vectors by creating array, e.g. `myVec = [1, 0, 0]`. You can't use actual arrays in p5.strands; all arrays are fixed-size vectors. +- In JavaScript, you can only use mathematical operators like `+` between numbers and strings, not with vectors. In p5.strands, we allow use of these operators between vectors. +- In GLSL, you can do something called *swizzling*, where you can create new vectors out of the components of an existing vector, e.g. `myvec.xy`, `myvec.bgr`, or even `myvec.zzzz`. p5.strands adds support for this on its vectors. + +When we transpile the input code, we rewrite these into valid JavaScript. Array literals are turned into function calls like `vec3(1, 0, 0)` which return vector class instances. These instances are wrapped in a `Proxy` that handles property accesses that look like swizzles, and convertes them into sub-vector references. Operators between vectors like `a + b` are rewritten into method calls, like `a.add(b)`. + +If a user writes something like this: + +```js +baseMaterialShader().modify(() => { + const t = uniformFloat(() => millis()) + getWorldInputs((inputs) => { + inputs.position += [20, 25, 20] * sin(inputs.position.y * 0.05 + t * 0.004) + return inputs + }) +}) +``` + +...it gets transpiled to something like this: +```js +baseMaterialShader().modify(() => { + const t = uniformFloat('t', () => millis()) + getWorldInputs((inputs) => { + inputs.position = inputs.position.add(dynamicNode([20, 25, 20]).mult(sin(inputs.position.y.mult(0.05).add(dynamicNode(t).mult(0.004))))) + return inputs + }) +}) +``` + +## The program graph + +The overall structure of a shader program is represented by a **control-flow graph (CFG)**. This divides up a program into chunks that need to be outputted in linear order based on control flow. A program like the one below would get chunked up around the if statement: + +```js +// Start chunk 1 +let a = 0; +let b = 1; +// End chunk 1 + +// Start chunk 2 +if (a < 2) { + b = 10; +} +// End chunk 2 + +// Start chunk 3 +b += 2; +return b; +// End chunk 3 +``` + +We store the individual states that variables can be in as nodes in a **directed acyclic graph (DAG)**. This is a fancy name that basically means each of these variable states may depend on previous varible states, and outputs can't feed back into inputs. Each time you modify a variable, that represents a new state of that variable. For example, below, it is not sufficient to know that `c` depends on `a` and `b`; you also need to know *which version of `b`* it branched off from: + +```js +let a = 0; +let b = 1; +b += 1; +let c = a + b; +return c; +``` + +We can imagine giving each of these states a separate name to make it clearer. In fact, that's what we do when we output GLSL, because we don't need to preserve variable names. +```js +let a_0 = 0; +let b_0 = 1; +let b_1 = b_0 + 1; +let c_0 = b_1 + a_0; +return c_0; +``` + +When we generate GLSL from the graph, we start from the variables we need to output, the return values of the function (e.g. `c_0` in the example above.) From there, we can track dependencies through the DAG (in this case, `b_1` and `a_1`). Each dependency has their own dependencies. We make sure we output the dependencies for a node before the node itself. + +Each node in the DAG belongs to a chunk in the CFG. This is helpful because it helps us keep track of key points in the code. If we need to, for example, generate a temporary variable at the end of an if statement, we can refer to that CFG chunk rather than whatever the last value node in the if statement happens to be. + +## Control flow + +p5.strands has to convert any control flow that should show up in GLSL into function calls instead of JavaScript keywords. If we don't, they run in JavaScript, and are invisible to GLSL generation. For example, if you had a loop that runs 10 times that adds 1 each time, it would output the add 1 line 10 times rather than outputting a for loop. + + + + + + + + + + +
InputOutput without converting control flow
+ +```js +let a = 0; +for (let i = 0; i < 10; i++) { + a += 2; +} +return a; +``` + + + +```glsl +float a = 0.0; +a += 2.0; +a += 2.0; +a += 2.0; +a += 2.0; +a += 2.0; +a += 2.0; +a += 2.0; +a += 2.0; +a += 2.0; +a += 2.0; +return a; +``` + +
+ +However, once we have a function call instead of real control flow, we also need a way to make sure that when the users' javascript subsequently references nodes that were updated in the control flow, they properly reference the modified value after the `if` or `for` and not the original value. + + + + + + + + + + + + +
InputTranspiled without updating referencesStates without updating references
+ +```js +let a = 0; +for (let i = 0; i < 10; i++) { + a += 2; +} +let b = a + 1; +return b; +``` + + + +```js +let a = 0; +p5.strandsFor( + () => 0, + (i) => i.lessThan(10), + (i) => i.add(1), + + () => { + a = a.add(2); + } +); +let b = a.add(1); +return b; +``` + + + +```js +let a_0 = 0; + +p5.strandsFor( + // ... +) +// At this point, the final state of a is a_n + +// ...but since we didn't actually run the loop, +// b still refers to the initial state of a! +let b_0 = a_0.add(1); +return b; +``` + +
+ +For that, we make the function calls return updated values, and we generate JS code that assigns these updated values back to the original JS variables. So for loops end up transpiled to something like this, inspired by the JavaScript `reduce` function: + + + + + + + + + + +
InputTranspiled with updated references
+ +```js +let a = 0; +for (let i = 0; i < 10; i++) { + a += 2; +} +let b = a + 1; +return b; +``` + + + +```js +let a = 0; + +const outputState = p5.strandsFor( + () => 0, + (i) => i.lessThan(10), + (i) => i.add(1), + + // Explicitly output new state based on prev state + (i, prevState) => { + return { a: prevState.a.add(2) }; + }, + + { a } // Pass in initial state +); +a = outputState.a; // Update reference + +// b now correctly is based off of the final state of a +let b = a.add(1); +return b; +``` + +
+ +We use a special kind of node in the DAG called a **phi node**, something used in compilers to refer to the result of some conditional execution. In the example above, the state of `a` in the output state is represented by a phi node. + +In the CFG, we surround chunks producing phi nodes by a `BRANCH` and a `MERGE` chunk. In the `BRANCH` chunk, we can initialize phi nodes, sometimes giving them initial values. In the `MERGE` chunk, the value of the phi node has stabilized, and other nodes can use them as a dependency. + +## GLSL generation + +GLSL is currently the only output format we support, but p5.strands is designed to be able to generate multiple formats. Specifically, in WebGPU, they use the WebGPU Shading Language (WGSL). Our goal is that your same JavaScript p5.strands code can be used in WebGL or WebGPU without you having to do any modifications. + +To support this, p5.strands separates out code generation into **backends.** A backend is responsible for converting each type of CFG chunk into a string of shader source code. We currently have a GLSL backend, but in the future we'll have a WGSL backend too! From f9f51c4871df5672962e909ecc595ef06f762dc7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 4 Oct 2025 07:04:56 -0400 Subject: [PATCH 12/22] Update contributor_docs/p5.strands.md Co-authored-by: Perminder Singh <127239756+perminder-17@users.noreply.github.com> --- contributor_docs/p5.strands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributor_docs/p5.strands.md b/contributor_docs/p5.strands.md index a991f7a94c..098a64ace1 100644 --- a/contributor_docs/p5.strands.md +++ b/contributor_docs/p5.strands.md @@ -21,7 +21,7 @@ The code the user writes when using p5.strands is mostly JavaScript, with some e - In JavaScript, you can only use mathematical operators like `+` between numbers and strings, not with vectors. In p5.strands, we allow use of these operators between vectors. - In GLSL, you can do something called *swizzling*, where you can create new vectors out of the components of an existing vector, e.g. `myvec.xy`, `myvec.bgr`, or even `myvec.zzzz`. p5.strands adds support for this on its vectors. -When we transpile the input code, we rewrite these into valid JavaScript. Array literals are turned into function calls like `vec3(1, 0, 0)` which return vector class instances. These instances are wrapped in a `Proxy` that handles property accesses that look like swizzles, and convertes them into sub-vector references. Operators between vectors like `a + b` are rewritten into method calls, like `a.add(b)`. +When we transpile the input code, we rewrite these into valid JavaScript. Array literals are turned into function calls like `vec3(1, 0, 0)` which return vector class instances. These instances are wrapped in a `Proxy` that handles property accesses that look like swizzles, and converts them into sub-vector references. Operators between vectors like `a + b` are rewritten into method calls, like `a.add(b)`. If a user writes something like this: From 4dfca175685689a0ff9dde52e188665c2a21e6bf Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 4 Oct 2025 07:05:15 -0400 Subject: [PATCH 13/22] Update contributor_docs/p5.strands.md Co-authored-by: Perminder Singh <127239756+perminder-17@users.noreply.github.com> --- contributor_docs/p5.strands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributor_docs/p5.strands.md b/contributor_docs/p5.strands.md index 098a64ace1..2540445ffc 100644 --- a/contributor_docs/p5.strands.md +++ b/contributor_docs/p5.strands.md @@ -9,7 +9,7 @@ p5.strands hopes to address all of those issues by letting you write shader snip ## Code processing pipeline At its core, p5.strands works in four steps: -1. The user writes a function in psuedo-JavaScript. +1. The user writes a function in pseudo-JavaScript. 2. p5.strands transpiles that into actual JavaScript and rewrites aspects of your code. 3. The transpiled code is run. Variable modification function calls are tracked in a graph data structure. 4. p5.strands generates GLSL code from that graph. From 68c89709525cb5ad56b153a2d5dfc3f2b6b97e57 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 4 Oct 2025 07:05:34 -0400 Subject: [PATCH 14/22] Update contributor_docs/p5.strands.md Co-authored-by: Perminder Singh <127239756+perminder-17@users.noreply.github.com> --- contributor_docs/p5.strands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributor_docs/p5.strands.md b/contributor_docs/p5.strands.md index 2540445ffc..5e348abbaf 100644 --- a/contributor_docs/p5.strands.md +++ b/contributor_docs/p5.strands.md @@ -68,7 +68,7 @@ return b; // End chunk 3 ``` -We store the individual states that variables can be in as nodes in a **directed acyclic graph (DAG)**. This is a fancy name that basically means each of these variable states may depend on previous varible states, and outputs can't feed back into inputs. Each time you modify a variable, that represents a new state of that variable. For example, below, it is not sufficient to know that `c` depends on `a` and `b`; you also need to know *which version of `b`* it branched off from: +We store the individual states that variables can be in as nodes in a **directed acyclic graph (DAG)**. This is a fancy name that basically means each of these variable states may depend on previous variable states, and outputs can't feed back into inputs. Each time you modify a variable, that represents a new state of that variable. For example, below, it is not sufficient to know that `c` depends on `a` and `b`; you also need to know *which version of `b`* it branched off from: ```js let a = 0; From ba73b4b4a50136feef045dffb7906e1d8aa92ba8 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 4 Oct 2025 07:07:06 -0400 Subject: [PATCH 15/22] Update p5.strands.md --- contributor_docs/p5.strands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributor_docs/p5.strands.md b/contributor_docs/p5.strands.md index 5e348abbaf..43b5ff7a3c 100644 --- a/contributor_docs/p5.strands.md +++ b/contributor_docs/p5.strands.md @@ -89,7 +89,7 @@ return c_0; When we generate GLSL from the graph, we start from the variables we need to output, the return values of the function (e.g. `c_0` in the example above.) From there, we can track dependencies through the DAG (in this case, `b_1` and `a_1`). Each dependency has their own dependencies. We make sure we output the dependencies for a node before the node itself. -Each node in the DAG belongs to a chunk in the CFG. This is helpful because it helps us keep track of key points in the code. If we need to, for example, generate a temporary variable at the end of an if statement, we can refer to that CFG chunk rather than whatever the last value node in the if statement happens to be. +Each node in the DAG belongs to a chunk in the CFG. This helps us keep track of key points in the code. If we need to, for example, generate a temporary variable at the end of an if statement, we can refer to that CFG chunk rather than whatever the last value node in the if statement happens to be. ## Control flow From 597b878172638068eecb499284ce47d95da394a6 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 4 Oct 2025 07:35:54 -0400 Subject: [PATCH 16/22] Update strands_transpiler.js --- src/strands/strands_transpiler.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index ec7786783b..aed9e2ddb3 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -12,12 +12,16 @@ function replaceBinaryOperator(codeSource) { case '%': return 'mod'; case '==': case '===': return 'equalTo'; + case '!=': + case '!==': return 'notEqual'; case '>': return 'greaterThan'; case '>=': return 'greaterEqual'; case '<': return 'lessThan'; case '<=': return 'lessEqual'; case '&&': return 'and'; case '||': return 'or'; + // TODO: handle ** --> pow, but make it stay pow in + // GLSL instead of turning it back into ** } } function nodeIsUniform(ancestor) { From e12f1136213e963d1d90dfa20155f42a643adb0b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 5 Oct 2025 16:43:28 -0400 Subject: [PATCH 17/22] Wrap assigned values in strandsNode() --- src/strands/strands_transpiler.js | 17 +++++++++++++++++ test/unit/webgl/p5.Shader.js | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index aed9e2ddb3..b8e1da91ef 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -343,6 +343,7 @@ const ASTCallbacks = { }); } // Replace all references to original variables with temp variables + // and wrap literal assignments in strandsNode calls const replaceReferences = (node) => { if (!node || typeof node !== 'object') return; if (node.type === 'Identifier' && tempVarMap.has(node.name)) { @@ -352,6 +353,22 @@ const ASTCallbacks = { tempVarMap.has(node.object.name)) { node.object.name = tempVarMap.get(node.object.name); } + // Handle literal assignments to temp variables + if (node.type === 'AssignmentExpression' && + node.left.type === 'Identifier' && + tempVarMap.has(node.left.name) && + (node.right.type === 'Literal' || node.right.type === 'ArrayExpression')) { + // Wrap the right hand side in a strandsNode call to make sure + // it's not just a literal and has a type + node.right = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '__p5.strandsNode' + }, + arguments: [node.right] + }; + } // Recursively process all properties for (const key in node) { if (node.hasOwnProperty(key) && key !== 'parent') { diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index d558b6401b..dea7f47fed 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -419,6 +419,28 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); + test('handle simple if statement with simpler assignment', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 1.0); // true condition + myp5.getPixelInputs(inputs => { + let color = 1; // initial gray + if (condition > 0.5) { + color = 1; // set to white in if branch + } + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is white (condition was true) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) + assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 + assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 + }); test('handle simple if statement with false condition', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { From daee4a4cd3db5cde598720e326b550109e97ef26 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 6 Oct 2025 21:03:07 -0400 Subject: [PATCH 18/22] Put post-conditional assignments in a separate block --- src/strands/strands_conditionals.js | 34 +++++++++++++++-------------- test/unit/webgl/p5.Shader.js | 26 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index da119d9229..cd40e8cd91 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -41,6 +41,7 @@ function buildConditional(strandsContext, conditional) { const mergeBlock = CFG.createBasicBlock(cfg, BlockType.MERGE); const results = []; const branchBlocks = []; + const branchEndBlocks = []; const mergedAssignments = {}; const phiBlockDependencies = {}; // Create a BRANCH block to handle phi node declarations @@ -64,27 +65,28 @@ function buildConditional(strandsContext, conditional) { } const scopeStartBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_START); CFG.addEdge(cfg, previousBlock, scopeStartBlock); - const branchBlock = CFG.createBasicBlock(cfg, blockType); - CFG.addEdge(cfg, scopeStartBlock, branchBlock); - branchBlocks.push(branchBlock); - CFG.pushBlock(cfg, branchBlock); + const branchContentBlock = CFG.createBasicBlock(cfg, blockType); + CFG.addEdge(cfg, scopeStartBlock, branchContentBlock); + branchBlocks.push(branchContentBlock); + CFG.pushBlock(cfg, branchContentBlock); const branchResults = branchCallback(); for (const key in branchResults) { if (!phiBlockDependencies[key]) { - phiBlockDependencies[key] = [{ value: branchResults[key], blockId: branchBlock }]; + phiBlockDependencies[key] = [{ value: branchResults[key], blockId: branchContentBlock }]; } else { - phiBlockDependencies[key].push({ value: branchResults[key], blockId: branchBlock }); + phiBlockDependencies[key].push({ value: branchResults[key], blockId: branchContentBlock }); } } results.push(branchResults); + + // Create BRANCH_END block for phi assignments + const branchEndBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT); + CFG.addEdge(cfg, cfg.currentBlock, branchEndBlock); + branchEndBlocks.push(branchEndBlock); + CFG.popBlock(cfg); + const scopeEndBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_END); - if (cfg.currentBlock !== branchBlock) { - CFG.addEdge(cfg, cfg.currentBlock, scopeEndBlock); - CFG.popBlock(cfg); - } else { - CFG.addEdge(cfg, branchBlock, scopeEndBlock); - CFG.popBlock(cfg); - } + CFG.addEdge(cfg, branchEndBlock, scopeEndBlock); CFG.addEdge(cfg, scopeEndBlock, mergeBlock); previousBlock = scopeStartBlock; } @@ -96,8 +98,8 @@ function buildConditional(strandsContext, conditional) { CFG.popBlock(cfg); for (let i = 0; i < results.length; i++) { const branchResult = results[i]; - const branchBlockID = branchBlocks[i]; - CFG.pushBlockForModification(cfg, branchBlockID); + const branchEndBlockID = branchEndBlocks[i]; + CFG.pushBlockForModification(cfg, branchEndBlockID); for (const key in branchResult) { if (mergedAssignments[key]) { // Create an assignment statement: phiNode = branchResult[key] @@ -112,7 +114,7 @@ function buildConditional(strandsContext, conditional) { phiBlocks: [] }; const assignmentID = DAG.getOrCreateNode(strandsContext.dag, assignmentNode); - CFG.recordInBasicBlock(cfg, branchBlockID, assignmentID); + CFG.recordInBasicBlock(cfg, branchEndBlockID, assignmentID); } } CFG.popBlock(cfg); diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index dea7f47fed..de3efb6f8b 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -596,6 +596,32 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); + test('handle if-else-if chains in the else branch', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + const value = myp5.uniformFloat(() => 0.2); // middle value + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); + if (value > 0.8) { + color = myp5.float(1.0); // white for high values + } else if (value > 0.3) { + color = myp5.float(0.5); // gray for medium values + } else { + color = myp5.float(0.0); // black for low values + } + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is gray (medium condition was true) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 0, 5); // Red channel should be ~127 (gray) + assert.approximately(pixelColor[1], 0, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~127 + }); test('handle nested if statements', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { From 49bf94b1cdcd9a9b14f5de89e62048e8b085f4ca Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 7 Oct 2025 09:39:30 -0400 Subject: [PATCH 19/22] Fix detection of local variables in for loop --- src/strands/strands_transpiler.js | 28 ++++++++++++++++++----- test/unit/webgl/p5.Shader.js | 37 +++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index b8e1da91ef..746a238278 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -622,23 +622,41 @@ const ASTCallbacks = { // Analyze which outer scope variables are assigned in the loop body const assignedVars = new Set(); - const analyzeBlock = (body) => { + const analyzeBlock = (body, parentLocalVars = new Set()) => { if (body.type !== 'BlockStatement') return; + // First pass: collect variable declarations within this block + const localVars = new Set([...parentLocalVars]); + for (const stmt of body.body) { + if (stmt.type === 'VariableDeclaration') { + for (const decl of stmt.declarations) { + if (decl.id.type === 'Identifier') { + localVars.add(decl.id.name); + } + } + } + } + + // Second pass: find assignments to non-local variables for (const stmt of body.body) { if (stmt.type === 'ExpressionStatement' && stmt.expression.type === 'AssignmentExpression') { const left = stmt.expression.left; if (left.type === 'Identifier') { - assignedVars.add(left.name); + // Direct variable assignment: x = value + if (!localVars.has(left.name)) { + assignedVars.add(left.name); + } } else if (left.type === 'MemberExpression' && left.object.type === 'Identifier') { // Property assignment: obj.prop = value (includes swizzles) - assignedVars.add(left.object.name); + if (!localVars.has(left.object.name)) { + assignedVars.add(left.object.name); + } } } else if (stmt.type === 'BlockStatement') { - // Recursively analyze nested block statements - analyzeBlock(stmt); + // Recursively analyze nested block statements, passing down local vars + analyzeBlock(stmt, localVars); } } }; diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index de3efb6f8b..b305170a1d 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -893,6 +893,43 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[2], 77, 5); }); + test('handle nested for loops with state modification between loops', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let total = myp5.float(0.0); + + // Outer for loop + for (let i = 0; i < 2; i++) { + let innerSum = myp5.float(0.0); + + // Inner for loop + for (let j = 0; j < 3; j++) { + innerSum = innerSum + 0.1; // 3 * 0.1 = 0.3 per outer iteration + } + + // State modification between inner and outer loop + innerSum = innerSum * 0.5; // Multiply by 0.5: 0.3 * 0.5 = 0.15 + total = total + innerSum; // Add to total: 2 * 0.15 = 0.3 + } + + inputs.color = [total, total, total, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should be: 2 iterations * (3 * 0.1 * 0.5) = 2 * 0.15 = 0.3 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 + assert.approximately(pixelColor[1], 77, 5); + assert.approximately(pixelColor[2], 77, 5); + }); + test('handle for loop using loop variable in calculations', () => { myp5.createCanvas(50, 50, myp5.WEBGL); From 1e41b569cfbaa95c0c3d63a66c13e0ce95b7df37 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 7 Oct 2025 09:42:53 -0400 Subject: [PATCH 20/22] Add more complicated test --- test/unit/webgl/p5.Shader.js | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index b305170a1d..5829569286 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -893,6 +893,49 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[2], 77, 5); }); + test('handle complex nested for loops with multiple phi assignments', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let outerSum = myp5.float(0.0); + let globalCounter = myp5.float(0.0); + + // Outer for loop modifying multiple variables + for (let i = 0; i < 2; i++) { + let innerSum = myp5.float(0.0); + let localCounter = myp5.float(0.0); + + // Inner for loop also modifying multiple variables + for (let j = 0; j < 2; j++) { + innerSum = innerSum + 0.1; + localCounter = localCounter + 1.0; + globalCounter = globalCounter + 0.5; // This modifies outer scope + } + + // Complex state modification between loops involving all variables + innerSum = innerSum * localCounter; // 0.2 * 2.0 = 0.4 + outerSum = outerSum + innerSum; // Add to outer sum + globalCounter = globalCounter * 0.5; // Modify global again + } + + // Final result should be: 2 iterations * 0.4 = 0.8 for outerSum + // globalCounter: ((0 + 2*0.5)*0.5 + 2*0.5)*0.5 = ((1)*0.5 + 1)*0.5 = 1.5*0.5 = 0.75 + inputs.color = [outerSum, globalCounter, 0.0, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 204, 5); // 0.8 * 255 ≈ 204 + assert.approximately(pixelColor[1], 191, 5); // 0.75 * 255 ≈ 191 + assert.approximately(pixelColor[2], 0, 5); + }); + test('handle nested for loops with state modification between loops', () => { myp5.createCanvas(50, 50, myp5.WEBGL); From c206ef2863019adabbe47780eeccd7b961322487 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 7 Oct 2025 10:18:39 -0400 Subject: [PATCH 21/22] Fix clashing constuctor and function --- src/strands/ir_types.js | 31 +++++++++++++-- test/unit/webgl/p5.Shader.js | 74 ++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 6446f13781..966e9d2ec2 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -63,7 +63,7 @@ export const DataType = { mat3: { fnName: "mat3x3", baseType: BaseType.MAT, dimension:3, priority: 0, }, mat4: { fnName: "mat4x4", baseType: BaseType.MAT, dimension:4, priority: 0, }, defer: { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, - sampler2D: { fnName: "texture", baseType: BaseType.SAMPLER2D, dimension: 1, priority: -10 }, + sampler2D: { fnName: "sampler2D", baseType: BaseType.SAMPLER2D, dimension: 1, priority: -10 }, } export const structType = function (hookType) { let T = hookType.type === undefined ? hookType : hookType.type; @@ -85,7 +85,32 @@ export function isStructType(typeName) { return !isNativeType(typeName); } export function isNativeType(typeName) { - return Object.keys(DataType).includes(typeName); + // Check if it's in DataType keys (internal names like 'float4') + if (Object.keys(DataType).includes(typeName)) { + return true; + } + + // Check if it's a GLSL type name (like 'vec4', 'float', etc.) + const glslNativeTypes = { + 'float': true, + 'vec2': true, + 'vec3': true, + 'vec4': true, + 'int': true, + 'ivec2': true, + 'ivec3': true, + 'ivec4': true, + 'bool': true, + 'bvec2': true, + 'bvec3': true, + 'bvec4': true, + 'mat2': true, + 'mat3': true, + 'mat4': true, + 'sampler2D': true + }; + + return !!glslNativeTypes[typeName]; } export const GenType = { FLOAT: { baseType: BaseType.FLOAT, dimension: null, priority: 3 }, @@ -98,7 +123,7 @@ export function typeEquals(nodeA, nodeB) { export const TypeInfoFromGLSLName = Object.fromEntries( Object.values(DataType) .filter(info => info.fnName !== null) - .map(info => [info.fnName === 'texture' ? 'sampler2D' : info.fnName, info]) + .map(info => [info.fnName, info]) ); export const OpCode = { Binary: { diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 5829569286..11a5797906 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -1066,5 +1066,79 @@ suite('p5.Shader', function() { assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 }); }); + + suite('filter shader hooks', () => { + test('handle getColor hook with non-struct return type', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + myp5.getColor((inputs, canvasContent) => { + // Simple test - just return a constant color + return [1.0, 0.5, 0.0, 1.0]; // Orange color + }); + }, { myp5 }); + + // Create a simple scene to filter + myp5.background(0, 0, 255); // Blue background + + // Apply the filter + myp5.filter(testShader); + + // Check that the filter was applied (should be orange) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 0, 5); // Blue channel should be 0 + }); + + test('simple vector multiplication in filter shader', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + myp5.getColor((inputs, canvasContent) => { + // Test simple scalar * vector operation + const scalar = 0.5; + const vector = [1, 2]; + const result = scalar * vector; + return [result.x, result.y, 0, 1]; + }); + }, { myp5 }); + }); + + test('handle complex filter shader with for loop and vector operations', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + const r = myp5.uniformFloat(() => 3); // Small value for testing + myp5.getColor((inputs, canvasContent) => { + let sum = [0, 0, 0, 0]; + let samples = 1; + + for (let i = 0; i < r; i++) { + samples++; + sum += myp5.texture(canvasContent, inputs.texCoord + (i / r) * [ + myp5.sin(4 * myp5.PI * i / r), + myp5.cos(4 * myp5.PI * i / r) + ]); + } + + return sum / samples; + }); + }, { myp5 }); + + // Create a simple scene to filter + myp5.background(255, 0, 0); // Red background + + // Apply the filter + myp5.filter(testShader); + + // The result should be some variation of the red background + const pixelColor = myp5.get(25, 25); + // Just verify it ran without crashing - exact color will depend on sampling + assert.isNumber(pixelColor[0]); + assert.isNumber(pixelColor[1]); + assert.isNumber(pixelColor[2]); + }); + }); }); }); From 39af20ea1737aba8b2652db6b372d09cfc9b3623 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 7 Oct 2025 15:47:28 -0400 Subject: [PATCH 22/22] s/dynamicNode/strandsNode/g --- contributor_docs/p5.strands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributor_docs/p5.strands.md b/contributor_docs/p5.strands.md index 43b5ff7a3c..271e72439f 100644 --- a/contributor_docs/p5.strands.md +++ b/contributor_docs/p5.strands.md @@ -40,7 +40,7 @@ baseMaterialShader().modify(() => { baseMaterialShader().modify(() => { const t = uniformFloat('t', () => millis()) getWorldInputs((inputs) => { - inputs.position = inputs.position.add(dynamicNode([20, 25, 20]).mult(sin(inputs.position.y.mult(0.05).add(dynamicNode(t).mult(0.004))))) + inputs.position = inputs.position.add(strandsNode([20, 25, 20]).mult(sin(inputs.position.y.mult(0.05).add(strandsNode(t).mult(0.004))))) return inputs }) })