Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 114 additions & 17 deletions src/strands/strands_transpiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -2202,21 +2296,24 @@ 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,
initialShaderNameState,
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);
Expand Down
27 changes: 27 additions & 0 deletions test/unit/visual/cases/webgl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions test/unit/visual/cases/webgpu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"numScreenshots": 1
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"numScreenshots": 1
}
Loading