Skip to content

Commit 227c6a3

Browse files
authored
Merge branch 'dev-2.0' into ticket8278
2 parents 3d4d45e + 209f8d1 commit 227c6a3

File tree

8 files changed

+206
-1
lines changed

8 files changed

+206
-1
lines changed

src/strands/ir_types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export const OpCode = {
130130
Nary: {
131131
FUNCTION_CALL: 200,
132132
CONSTRUCTOR: 201,
133+
TERNARY: 202,
133134
},
134135
ControlFlow: {
135136
RETURN: 300,

src/strands/strands_api.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { strandsBuiltinFunctions } from './strands_builtins'
1616
import { StrandsConditional } from './strands_conditionals'
1717
import { StrandsFor } from './strands_for'
18+
import { buildTernary } from './strands_ternary'
1819
import * as CFG from './ir_cfg'
1920
import * as DAG from './ir_dag';
2021
import * as FES from './strands_FES'
@@ -194,6 +195,10 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
194195
return new StrandsFor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars).build();
195196
};
196197
augmentFn(fn, p5, 'strandsFor', p5.strandsFor);
198+
p5.strandsTernary = function(condition, ifTrue, ifFalse) {
199+
return buildTernary(strandsContext, condition, ifTrue, ifFalse);
200+
};
201+
augmentFn(fn, p5, 'strandsTernary', p5.strandsTernary);
197202
p5.strandsEarlyReturn = function(value) {
198203
const { dag, cfg } = strandsContext;
199204

src/strands/strands_ternary.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as DAG from './ir_dag';
2+
import * as CFG from './ir_cfg';
3+
import { NodeType, OpCode, BaseType } from './ir_types';
4+
import { createStrandsNode } from './strands_node';
5+
import * as FES from './strands_FES';
6+
7+
export function buildTernary(strandsContext, condition, ifTrue, ifFalse) {
8+
const { dag, cfg, p5 } = strandsContext;
9+
10+
// Ensure all inputs are StrandsNodes
11+
const condNode = condition?.isStrandsNode ? condition : p5.strandsNode(condition);
12+
const trueNode = ifTrue?.isStrandsNode ? ifTrue : p5.strandsNode(ifTrue);
13+
const falseNode = ifFalse?.isStrandsNode ? ifFalse : p5.strandsNode(ifFalse);
14+
15+
// Get type info for both nodes
16+
let trueType = DAG.extractNodeTypeInfo(dag, trueNode.id);
17+
let falseType = DAG.extractNodeTypeInfo(dag, falseNode.id);
18+
19+
// Propagate type from the known branch to any ASSIGN_ON_USE branch
20+
if (trueType.baseType === BaseType.ASSIGN_ON_USE && falseType.baseType !== BaseType.ASSIGN_ON_USE) {
21+
DAG.propagateTypeToAssignOnUse(dag, trueNode.id, falseType.baseType, falseType.dimension);
22+
trueType = DAG.extractNodeTypeInfo(dag, trueNode.id);
23+
} else if (falseType.baseType === BaseType.ASSIGN_ON_USE && trueType.baseType !== BaseType.ASSIGN_ON_USE) {
24+
DAG.propagateTypeToAssignOnUse(dag, falseNode.id, trueType.baseType, trueType.dimension);
25+
falseType = DAG.extractNodeTypeInfo(dag, falseNode.id);
26+
}
27+
28+
// After ASSIGN_ON_USE propagation, if both types are known, they must match
29+
if (
30+
trueType.baseType !== BaseType.ASSIGN_ON_USE &&
31+
falseType.baseType !== BaseType.ASSIGN_ON_USE &&
32+
(trueType.baseType !== falseType.baseType || trueType.dimension !== falseType.dimension)
33+
) {
34+
FES.userError('type error',
35+
'The true and false branches of a ternary expression must have the same type. ' +
36+
`Right now, the true branch is a ${trueType.baseType}${trueType.dimension}, and the false branch is a ${falseType.baseType}${falseType.dimension}.`
37+
);
38+
}
39+
40+
const resultType = trueType;
41+
42+
const nodeData = DAG.createNodeData({
43+
nodeType: NodeType.OPERATION,
44+
opCode: OpCode.Nary.TERNARY,
45+
dependsOn: [condNode.id, trueNode.id, falseNode.id],
46+
baseType: resultType.baseType,
47+
dimension: resultType.dimension,
48+
});
49+
50+
const id = DAG.getOrCreateNode(dag, nodeData);
51+
CFG.recordInBasicBlock(cfg, cfg.currentBlock, id);
52+
return createStrandsNode(id, resultType.dimension, strandsContext);
53+
}

src/strands/strands_transpiler.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,20 @@ const ASTCallbacks = {
465465
};
466466
node.arguments = [node.right];
467467
},
468+
ConditionalExpression(node, _state, ancestors) {
469+
if (ancestors.some(nodeIsUniform)) { return; }
470+
// Transform condition ? consequent : alternate
471+
// into __p5.strandsTernary(condition, consequent, alternate)
472+
const test = node.test;
473+
const consequent = node.consequent;
474+
const alternate = node.alternate;
475+
node.type = 'CallExpression';
476+
node.callee = { type: 'Identifier', name: '__p5.strandsTernary' };
477+
node.arguments = [test, consequent, alternate];
478+
delete node.test;
479+
delete node.consequent;
480+
delete node.alternate;
481+
},
468482
IfStatement(node, _state, ancestors) {
469483
if (ancestors.some(nodeIsUniform)) { return; }
470484
// Transform if statement into strandsIf() call

src/webgl/strands_glslBackend.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,13 @@ export const glslBackend = {
289289
const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg));
290290
return `${node.identifier}(${functionArgs.join(', ')})`;
291291
}
292+
if (node.opCode === OpCode.Nary.TERNARY) {
293+
const [condID, trueID, falseID] = node.dependsOn;
294+
const cond = this.generateExpression(generationContext, dag, condID);
295+
const trueExpr = this.generateExpression(generationContext, dag, trueID);
296+
const falseExpr = this.generateExpression(generationContext, dag, falseID);
297+
return `(${cond} ? ${trueExpr} : ${falseExpr})`;
298+
}
292299
if (node.opCode === OpCode.Binary.MEMBER_ACCESS) {
293300
const [lID, rID] = node.dependsOn;
294301
const lName = this.generateExpression(generationContext, dag, lID);

src/webgpu/strands_wgslBackend.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,13 @@ export const wgslBackend = {
396396
const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep));
397397
return `${T}(${deps.join(', ')})`;
398398
}
399+
if (node.opCode === OpCode.Nary.TERNARY) {
400+
const [condID, trueID, falseID] = node.dependsOn;
401+
const cond = this.generateExpression(generationContext, dag, condID);
402+
const trueExpr = this.generateExpression(generationContext, dag, trueID);
403+
const falseExpr = this.generateExpression(generationContext, dag, falseID);
404+
return `select(${falseExpr}, ${trueExpr}, ${cond})`;
405+
}
399406
if (node.opCode === OpCode.Nary.FUNCTION_CALL) {
400407
// Convert mod() function calls to % operator in WGSL
401408
if (node.identifier === 'mod' && node.dependsOn.length === 2) {

test/unit/webgl/p5.Shader.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,55 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca
12041204
});
12051205
});
12061206

1207+
suite('ternary expressions', () => {
1208+
test('ternary changes color based on left/right side of canvas', () => {
1209+
myp5.createCanvas(50, 25, myp5.WEBGL);
1210+
const testShader = myp5.baseMaterialShader().modify(() => {
1211+
myp5.getPixelInputs(inputs => {
1212+
inputs.color = inputs.texCoord.x > 0.5 ? [1, 0, 0, 1] : [0, 0, 1, 1];
1213+
return inputs;
1214+
});
1215+
}, { myp5 });
1216+
myp5.noStroke();
1217+
myp5.shader(testShader);
1218+
myp5.plane(myp5.width, myp5.height);
1219+
1220+
const leftPixel = myp5.get(12, 12);
1221+
assert.approximately(leftPixel[0], 0, 5);
1222+
assert.approximately(leftPixel[1], 0, 5);
1223+
assert.approximately(leftPixel[2], 255, 5);
1224+
1225+
const rightPixel = myp5.get(37, 12);
1226+
assert.approximately(rightPixel[0], 255, 5);
1227+
assert.approximately(rightPixel[1], 0, 5);
1228+
assert.approximately(rightPixel[2], 0, 5);
1229+
});
1230+
1231+
test('ternary with scalar values', () => {
1232+
myp5.createCanvas(50, 25, myp5.WEBGL);
1233+
const testShader = myp5.baseMaterialShader().modify(() => {
1234+
myp5.getPixelInputs(inputs => {
1235+
const brightness = inputs.texCoord.x > 0.5 ? 1.0 : 0.0;
1236+
inputs.color = [brightness, brightness, brightness, 1];
1237+
return inputs;
1238+
});
1239+
}, { myp5 });
1240+
myp5.noStroke();
1241+
myp5.shader(testShader);
1242+
myp5.plane(myp5.width, myp5.height);
1243+
1244+
const leftPixel = myp5.get(12, 12);
1245+
assert.approximately(leftPixel[0], 0, 5);
1246+
assert.approximately(leftPixel[1], 0, 5);
1247+
assert.approximately(leftPixel[2], 0, 5);
1248+
1249+
const rightPixel = myp5.get(37, 12);
1250+
assert.approximately(rightPixel[0], 255, 5);
1251+
assert.approximately(rightPixel[1], 255, 5);
1252+
assert.approximately(rightPixel[2], 255, 5);
1253+
});
1254+
});
1255+
12071256
suite('for loop statements', () => {
12081257
test('handle simple for loop with known iteration count', () => {
12091258
myp5.createCanvas(50, 50, myp5.WEBGL);
@@ -2215,5 +2264,26 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca
22152264
assert.include(errMsg, 'Expected properties');
22162265
assert.include(errMsg, 'Received properties');
22172266
});
2267+
2268+
test('ternary with mismatched branch types shows both types in error', () => {
2269+
myp5.createCanvas(50, 50, myp5.WEBGL);
2270+
2271+
try {
2272+
myp5.baseMaterialShader().modify(() => {
2273+
myp5.getPixelInputs(inputs => {
2274+
// float1 vs float4 - type mismatch
2275+
const val = inputs.texCoord.x > 0.5 ? myp5.float(1.0) : [1, 0, 0, 1];
2276+
inputs.color = [val, val, val, 1];
2277+
return inputs;
2278+
});
2279+
}, { myp5 });
2280+
} catch (e) { /* expected */ }
2281+
2282+
assert.isAbove(mockUserError.mock.calls.length, 0, 'FES.userError should have been called');
2283+
const errMsg = mockUserError.mock.calls[0][1];
2284+
assert.include(errMsg, 'ternary');
2285+
assert.include(errMsg, 'float1');
2286+
assert.include(errMsg, 'float4');
2287+
});
22182288
});
22192289
});

test/unit/webgpu/p5.Shader.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,6 @@ suite('WebGPU p5.Shader', function() {
490490
return [0.4, 0, 0, 1];
491491
});
492492
}, { myp5 });
493-
console.log(testShader.fragSrc())
494493

495494
myp5.background(255, 255, 255);
496495
myp5.filter(testShader);
@@ -502,6 +501,55 @@ suite('WebGPU p5.Shader', function() {
502501
});
503502
});
504503

504+
suite('ternary expressions', () => {
505+
test('ternary changes color based on left/right side of canvas', async () => {
506+
await myp5.createCanvas(50, 25, myp5.WEBGPU);
507+
const testShader = myp5.baseMaterialShader().modify(() => {
508+
myp5.getPixelInputs(inputs => {
509+
inputs.color = inputs.texCoord.x > 0.5 ? [1, 0, 0, 1] : [0, 0, 1, 1];
510+
return inputs;
511+
});
512+
}, { myp5 });
513+
myp5.noStroke();
514+
myp5.shader(testShader);
515+
myp5.plane(myp5.width, myp5.height);
516+
517+
const leftPixel = await myp5.get(12, 12);
518+
assert.approximately(leftPixel[0], 0, 5);
519+
assert.approximately(leftPixel[1], 0, 5);
520+
assert.approximately(leftPixel[2], 255, 5);
521+
522+
const rightPixel = await myp5.get(37, 12);
523+
assert.approximately(rightPixel[0], 255, 5);
524+
assert.approximately(rightPixel[1], 0, 5);
525+
assert.approximately(rightPixel[2], 0, 5);
526+
});
527+
528+
test('ternary with scalar values', async () => {
529+
await myp5.createCanvas(50, 25, myp5.WEBGPU);
530+
const testShader = myp5.baseMaterialShader().modify(() => {
531+
myp5.getPixelInputs(inputs => {
532+
const brightness = inputs.texCoord.x > 0.5 ? 1.0 : 0.0;
533+
inputs.color = [brightness, brightness, brightness, 1];
534+
return inputs;
535+
});
536+
}, { myp5 });
537+
myp5.noStroke();
538+
myp5.shader(testShader);
539+
myp5.plane(myp5.width, myp5.height);
540+
541+
const leftPixel = await myp5.get(12, 12);
542+
assert.approximately(leftPixel[0], 0, 5);
543+
assert.approximately(leftPixel[1], 0, 5);
544+
assert.approximately(leftPixel[2], 0, 5);
545+
546+
const rightPixel = await myp5.get(37, 12);
547+
assert.approximately(rightPixel[0], 255, 5);
548+
assert.approximately(rightPixel[1], 255, 5);
549+
assert.approximately(rightPixel[2], 255, 5);
550+
});
551+
});
552+
505553
suite('for loop statements', () => {
506554
test('handle simple for loop with known iteration count', async () => {
507555
await myp5.createCanvas(50, 50, myp5.WEBGPU);

0 commit comments

Comments
 (0)