diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 82920f07c4..d4d7550368 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -187,6 +187,96 @@ function throwIfLoopProtectionInserted(ast) { ); } } +function isUnusedResultControlFlow(expr) { + return ( + expr.type === 'ConditionalExpression' || + (expr.type === 'LogicalExpression' && + (expr.operator === '&&' || expr.operator === '||')) + ); +} + +function toBlock(statements) { + return { type: 'BlockStatement', body: statements }; +} + +// Minifiers turn `if (c) body` into `c && body;`. When the result is thrown +// away, it's control flow rather than a bool expression, so it is rewritten +// back into an if statement. Returns the statements that replace `stmt`. +function restoreConditionalStatement(stmt) { + if (stmt.type !== 'ExpressionStatement') return [stmt]; + const expr = stmt.expression; + const asStatement = expression => ({ type: 'ExpressionStatement', expression }); + + if (expr.type === 'SequenceExpression') { + if (!expr.expressions.some(isUnusedResultControlFlow)) return [stmt]; + return expr.expressions.flatMap(e => + restoreConditionalStatement(asStatement(e)) + ); + } + if (expr.type === 'LogicalExpression' && isUnusedResultControlFlow(expr)) { + const test = + expr.operator === '&&' + ? expr.left + : { + type: 'UnaryExpression', + operator: '!', + prefix: true, + argument: expr.left + }; + return [ + { + type: 'IfStatement', + test, + consequent: toBlock(restoreConditionalStatement(asStatement(expr.right))), + alternate: null + } + ]; + } + if (expr.type === 'ConditionalExpression') { + return [ + { + type: 'IfStatement', + test: expr.test, + consequent: toBlock( + restoreConditionalStatement(asStatement(expr.consequent)) + ), + alternate: toBlock( + restoreConditionalStatement(asStatement(expr.alternate)) + ) + } + ]; + } + return [stmt]; +} + +function restoreMinifiedConditionals(ast, uniformCallbackNames) { + const inUniform = ancestors => + ancestors.some( + a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, uniformCallbackNames) + ); + const restoreBlockBody = (node, state, ancestors) => { + if (inUniform(ancestors)) return; + node.body = node.body.flatMap(restoreConditionalStatement); + }; + // Bodies without braces, e.g. `if (x) a && b;` + const restoreUnbracedBodies = (node, state, ancestors) => { + if (inUniform(ancestors)) return; + for (const key of ['consequent', 'alternate', 'body']) { + const child = node[key]; + if (child?.type === 'ExpressionStatement') { + node[key] = toBlock(restoreConditionalStatement(child)); + } + } + }; + ancestor(ast, { + Program: restoreBlockBody, + BlockStatement: restoreBlockBody, + IfStatement: restoreUnbracedBodies, + ForStatement: restoreUnbracedBodies, + WhileStatement: restoreUnbracedBodies + }); +} + // Helper function to check if a statement is a variable declaration with strands control flow init function statementContainsStrandsControlFlow(stmt) { // Check for variable declarations with strands control flow init @@ -2013,23 +2103,27 @@ function transformHelperFunctionEarlyReturns(ast, names) { * * Pipeline stages: * - * 1. Collect uniform callback names - * - Identifies functions passed into uniform() so they are excluded from transformation + * Collect uniform callback names + * - Identifies functions passed into uniform() so they are excluded from transformation + * + * restoreMinifiedConditionals + * - Turns `cond && action;`, `cond || action;` and `cond ? a : b;` statements, whose + * results are unused, back into if statements * - * 2. transformSetCallsInControlFlow - * - Rewrites `.set()` calls inside control flow into intermediate variable assignments + * transformSetCallsInControlFlow + * - Rewrites `.set()` calls inside control flow into intermediate variable assignments * - * 3. Non-control-flow transformations - * - Applies ASTCallbacks to transform expressions, assignments, etc. - * - Skips IfStatement and ForStatement (handled later) + * Non-control-flow transformations + * - Applies ASTCallbacks to transform expressions, assignments, etc. + * - Skips IfStatement and ForStatement (handled later) * - * 4. transformHelperFunctionEarlyReturns - * - Converts early returns in helper functions into a single return value pattern + * transformHelperFunctionEarlyReturns + * - Converts early returns in helper functions into a single return value pattern * - * 5. Control flow transformation (post-order) - * - Transforms IfStatement → __p5.strandsIf - * - Transforms ForStatement → __p5.strandsFor - * - Handles variable propagation across branches/loops + * Control flow transformation (post-order) + * - Transforms IfStatement → __p5.strandsIf + * - Transforms ForStatement → __p5.strandsFor + * - Handles variable propagation across branches/loops * * This staged approach ensures correct ordering and avoids transformation conflicts. */ @@ -2202,10 +2296,13 @@ export function transpileStrandsToJS( // Pre-pass: collect names of functions passed by reference as uniform callbacks const uniformCallbackNames = collectUniformCallbackNames(ast); - // Pass 1: transform .set() calls in control flow to use intermediate variables + // Turn minifier-style `cond && action;` statements back into ifs + restoreMinifiedConditionals(ast, uniformCallbackNames); + + // Transform .set() calls in control flow to use intermediate variables transformSetCallsInControlFlow(ast, uniformCallbackNames); - // Pass 2: transform non-control-flow nodes (operators, varyings, uniforms, arrays) + // Transform non-control-flow nodes (operators, varyings, uniforms, arrays) const { shaderNameMap, shaderNameState } = runNonControlFlowPass( ast, uniformCallbackNames, @@ -2213,10 +2310,10 @@ export function transpileStrandsToJS( p5 ); - // Pass 3: transform helper functions with early returns to use __returnValue pattern + // Transform helper functions with early returns to use __returnValue pattern transformHelperFunctionEarlyReturns(ast, uniformCallbackNames); - // Pass 4: transform if/for statements post-order into strandsIf/strandsFor calls + // Transform if/for statements post-order into strandsIf/strandsFor calls runControlFlowPass(ast, uniformCallbackNames); const callback = buildStrandsCallback(p5, ast, scope); diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 96afb6bf12..a985e03b9f 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1871,6 +1871,33 @@ visualSuite('WebGL', function () { screenshot(); }); + visualTest( + 'minifier-style && and || statements act as conditionals', + (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const shader = p5.baseFilterShader().modify( + () => { + p5.getColor(inputs => { + let value = 0; + let red = 0; + // Minifiers write `if (cond) action` as `cond && action` + inputs.texCoord.x > 0.5 && (value = 0.5); + inputs.texCoord.y > 0.5 || (value = value + 0.25); + // ...and `if (cond) a; else b` as `cond ? a : b` + inputs.texCoord.x > 0.75 && inputs.texCoord.y > 0.75 + ? (red = 1, value = 0) + : (red = value); + return [red, value, value, 1]; + }); + }, + { p5 } + ); + p5.background(0); + p5.filter(shader); + screenshot(); + } + ); + visualTest('lerp maps to mix in strands context', (p5, screenshot) => { p5.createCanvas(50, 50, p5.WEBGL); // lerp should behave identically to mix inside strands diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index f9f30887f4..9dd4c9a3e8 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -288,6 +288,33 @@ visualSuite('WebGPU', function () { await screenshot(); }); + visualTest( + 'minifier-style && and || statements act as conditionals', + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + const shader = p5.baseFilterShader().modify( + () => { + p5.getColor(inputs => { + let value = 0; + let red = 0; + // Minifiers write `if (cond) action` as `cond && action` + inputs.texCoord.x > 0.5 && (value = 0.5); + inputs.texCoord.y > 0.5 || (value = value + 0.25); + // ...and `if (cond) a; else b` as `cond ? a : b` + inputs.texCoord.x > 0.75 && inputs.texCoord.y > 0.75 + ? (red = 1, value = 0) + : (red = value); + return [red, value, value, 1]; + }); + }, + { p5 } + ); + p5.background(0); + p5.filter(shader); + await screenshot(); + } + ); + visualTest( 'filter shaders can sample a texture inside a conditional branch', async function (p5, screenshot) { diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/minifier-style && and || statements act as conditionals/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/minifier-style && and || statements act as conditionals/000.png new file mode 100644 index 0000000000..742505ea29 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/p5.strands/minifier-style && and || statements act as conditionals/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/minifier-style && and || statements act as conditionals/metadata.json b/test/unit/visual/screenshots/WebGL/p5.strands/minifier-style && and || statements act as conditionals/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/p5.strands/minifier-style && and || statements act as conditionals/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/minifier-style && and || statements act as conditionals/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/minifier-style && and || statements act as conditionals/000.png new file mode 100644 index 0000000000..742505ea29 Binary files /dev/null and b/test/unit/visual/screenshots/WebGPU/Shaders/minifier-style && and || statements act as conditionals/000.png differ diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/minifier-style && and || statements act as conditionals/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/minifier-style && and || statements act as conditionals/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Shaders/minifier-style && and || statements act as conditionals/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file